diff --git a/.gitignore b/.gitignore index 79c2198..a210e93 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ design/ # IDE / editors .air/ .cursor/ +.codegraph/ .idea/ .vscode/ *.swp diff --git a/AGENTS.md b/AGENTS.md index f79a34c..03747b1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,6 +2,24 @@ # Work Bundle RULE START # ======================== # Work Bundle +## Unconditional agent boundaries + +These two boundaries apply to every agent, every task, and every workflow without exception: + +1. **DO NOT OVERENGINEER.** Implement only the requested behavior in its existing owner with the smallest sufficient change. Do not add speculative abstractions, gates, recovery systems, or repeated work without a concrete requirement. +2. **MAKE NO MISTAKES.** Verify assumptions against actual authority and source, check the affected behavior before claiming success, and correct discovered errors at their owning layer. Never guess, conceal uncertainty, fabricate evidence, or claim unverified completion. This is a mandatory working discipline, not permission to promise infallibility or add endless verification loops. + +## Evidence-first change principle + +Before or during evidence exploration, every agent must: + +1. Locate the feature in the codebase. +2. Find its corresponding design purpose and decisions in the knowledge base. +3. Find its corresponding orchestration evidence—specification, plan, and handoff—and Git history. Use that lineage to understand why each implementation was created, whether it introduced the defect, and whether it is a valid basis for the current user purpose. +4. If a legacy implementation introduced the defect, prefer reverting or correcting that implementation over adding another patch around it. +5. If a legacy implementation introduced the intended feature, understand its design and make the fewest updates necessary to satisfy the current request. +6. In either case, use available source-navigation tools—including CodeGraph when indexed, `rg`, `grep`, and equivalent tools—to find related references and update them consistently. + purpose: - Seeing this rule means that you are working with the `work-bundle` toolkit, it provides skills and rules to finish a bunch of works, including: - Work bundle skills: `/wb-*`, provide skills to manage a project as a `work-bundle` adapted workspace. @@ -26,10 +44,8 @@ must: - resolve `work_bundle_root` from `$work_bundle_config_root/bootstrap.yaml` -> `work_bundle_root` - resolve project registry from `$work_bundle_config_root/bootstrap.yaml` -> `project_registry` - resolve skill registry from `$work_bundle_config_root/bootstrap.yaml` -> `skill_registry` -- resolve effective `prefer_subagent` as `.work-bundle/project.yaml` -> `prefer_subagent`, then `$work_bundle_config_root/bootstrap.yaml` -> `prefer_subagent`, then `false` - before material implementation, establish or consume one Truth Basis containing purpose, as-is evidence, accepted decision authority, expected delta, and conflict status; after preflight and source grounding, lightweight planning runs one bounded `ks-what-is-helpful` gateway and records accepted authority or evidence-backed `none relevant`, while heavy execution compiles carried authority without executor retrieval - after each meaningful validated move, record a knowledge disposition of `none`, `update`, `supersede`, or `reclassify`; the lightweight completion owner resolves its approved `ks-*` follow-up, while heavy executors return task-local evidence only and final orchestration review owns heavy-path persistence follow-up -- treat `prefer_subagent` as permission to prefer sub-agent scheduling only when normal execution safety, write-scope, dependency, and fallback checks pass - use `work_bundle_root` only for toolkit assets, builtin skills, builtin rules, and references - use `work_bundle_config_root` only for non-project runtime state produced by tool use - resolve workspace-owned metadata, rules, knowledge, orchestration, `AGENTS.md`, `script/index.yaml`, and `credentials/credentials.yaml` from `workspace_root` in both workspace modes @@ -50,7 +66,6 @@ must_not: - treat utility discovery as permission to execute a script - inspect or transfer credential values through chat, prompts, subagent messages, tool arguments/results, terminal output, logs, handoffs, knowledge, or orchestration artifacts - infer registry paths without reading `bootstrap.yaml` when registry access is required -- let `prefer_subagent` bypass repository preflight, sub-agent capability checks, disjoint write-scope checks, dependency checks, or single-agent fallback - treat rule-store scope (`toolkit`, `global`, `project`) as separate from rule area directories such as `work-bundle`, `keep-summarizing`, and `orchestration` ## Rule Loading diff --git a/bin/work-bundle-ci b/bin/work-bundle-ci index 8aff869..3b0ffb7 100755 --- a/bin/work-bundle-ci +++ b/bin/work-bundle-ci @@ -13,6 +13,7 @@ PYTHON_VERSION = "3.13" PINNED_PACKAGES = ( "pytest==9.1.1", "pyyaml==6.0.3", + "jsonschema==4.25.1", "sqlite-vec==0.1.9", "fastembed==0.8.0", ) @@ -31,6 +32,24 @@ def _emit_progress(message: str) -> None: print(message, flush=True) +def _discovered_test_files(repo_root: Path) -> list[Path]: + completed = subprocess.run( + [ + "git", "ls-files", "--cached", "--others", "--exclude-standard", + "tests/test_*.py", + ], + cwd=repo_root, + check=True, + capture_output=True, + text=True, + ) + return [ + repo_root / relative + for relative in completed.stdout.splitlines() + if (repo_root / relative).is_file() + ] + + def run_release_gate( repo_root: Path, *, @@ -42,7 +61,7 @@ def run_release_gate( repo_root = repo_root.resolve() modules = sorted( _relative_test_path(repo_root, path) - for path in (test_files if test_files is not None else (repo_root / "tests").glob("test_*.py")) + for path in (test_files if test_files is not None else _discovered_test_files(repo_root)) ) failed_modules: list[str] = [] environment = {**os.environ, "PYTHONDONTWRITEBYTECODE": "1"} diff --git a/references/assets/infrastructure/contract/bootstrap-config-v1.schema.json b/references/assets/infrastructure/contract/bootstrap-config-v1.schema.json new file mode 100644 index 0000000..0176935 --- /dev/null +++ b/references/assets/infrastructure/contract/bootstrap-config-v1.schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://work-bundle.dev/schemas/infrastructure/bootstrap-config-v1", + "title": "WorkBundle bootstrap configuration v1", + "type": "object", + "additionalProperties": true, + "required": ["bootstrap_version", "authority", "work_bundle_root", "project_registry", "skill_registry"], + "properties": { + "bootstrap_version": {"const": "v1"}, + "authority": {"const": "canonical"}, + "work_bundle_root": {"type": "string", "minLength": 1}, + "project_registry": {"type": "string", "minLength": 1}, + "skill_registry": {"type": "string", "minLength": 1} + } +} diff --git a/references/assets/infrastructure/contract/infrastructure-schema-catalog-v1.yaml b/references/assets/infrastructure/contract/infrastructure-schema-catalog-v1.yaml new file mode 100644 index 0000000..d692daf --- /dev/null +++ b/references/assets/infrastructure/contract/infrastructure-schema-catalog-v1.yaml @@ -0,0 +1,14 @@ +schema_version: 1 +families: + bootstrap-config: + version: 1 + representation: yaml + schema: bootstrap-config-v1.schema.json + project-registry: + version: 1 + representation: yaml + schema: project-registry-v1.schema.json + workspace-project-metadata: + version: 1 + representation: yaml + schema: workspace-project-metadata-v4.schema.json diff --git a/references/assets/infrastructure/contract/project-registry-v1.schema.json b/references/assets/infrastructure/contract/project-registry-v1.schema.json new file mode 100644 index 0000000..6d4c64b --- /dev/null +++ b/references/assets/infrastructure/contract/project-registry-v1.schema.json @@ -0,0 +1,91 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://work-bundle.dev/schemas/infrastructure/project-registry-v1", + "title": "WorkBundle project registry v1", + "type": "object", + "additionalProperties": true, + "required": ["projects", "device_bindings"], + "properties": { + "registry_schema_version": {"const": 1}, + "projects": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true, + "required": ["slug"], + "properties": { + "slug": {"type": "string", "minLength": 1}, + "aliases": {"type": "array", "items": {"type": "string"}} + } + } + }, + "device_bindings": { + "type": "object", + "additionalProperties": {"$ref": "#/$defs/workspaceBinding"} + } + }, + "$defs": { + "workspaceBinding": { + "type": "object", + "additionalProperties": true, + "required": ["slug", "workspace_root", "repositories"], + "properties": { + "slug": {"type": "string", "minLength": 1}, + "workspace_root": {"type": "string", "minLength": 1}, + "repositories": { + "type": "object", + "additionalProperties": {"$ref": "#/$defs/repositoryBinding"} + } + } + }, + "repositoryBinding": { + "type": "object", + "additionalProperties": true, + "required": [ + "project_root", + "checkout_kind", + "observed_branch", + "observed_head", + "observed_at", + "git_common_dir" + ], + "properties": { + "project_root": {"type": "string", "minLength": 1}, + "checkout_kind": {"type": "string", "minLength": 1}, + "observed_branch": {"type": "string"}, + "observed_head": {"type": "string"}, + "observed_at": {"type": "string", "minLength": 1}, + "git_common_dir": {"type": "string"} + }, + "allOf": [ + { + "if": { + "properties": {"checkout_kind": {"enum": ["manual", "unmaterialized-member"]}}, + "required": ["checkout_kind"] + }, + "then": {}, + "else": { + "properties": { + "observed_branch": {"minLength": 1}, + "observed_head": {"minLength": 1}, + "git_common_dir": {"minLength": 1} + } + } + }, + { + "if": { + "properties": {"checkout_kind": {"const": "unmaterialized-member"}}, + "required": ["checkout_kind"] + }, + "then": { + "properties": { + "observed_branch": {"const": ""}, + "observed_head": {"const": ""}, + "git_common_dir": {"const": ""} + } + } + } + ] + } + } +} diff --git a/references/assets/infrastructure/contract/workspace-project-metadata-v4.schema.json b/references/assets/infrastructure/contract/workspace-project-metadata-v4.schema.json new file mode 100644 index 0000000..095972e --- /dev/null +++ b/references/assets/infrastructure/contract/workspace-project-metadata-v4.schema.json @@ -0,0 +1,125 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://work-bundle.dev/schemas/infrastructure/workspace-project-metadata-v4", + "title": "Portable WorkBundle workspace project metadata v4", + "type": "object", + "additionalProperties": true, + "required": ["metadata_version", "authority", "workspace", "control_plane", "source_repositories"], + "properties": { + "metadata_version": {"const": 4}, + "authority": {"const": "canonical"}, + "workspace": { + "type": "object", + "additionalProperties": true, + "required": ["id", "slug", "mode"], + "properties": { + "id": {"type": "string", "pattern": "^wb-[A-Za-z0-9][A-Za-z0-9._-]*$"}, + "slug": {"type": "string", "minLength": 1}, + "mode": {"enum": ["single-repository", "multi-repository", "composite"]} + } + }, + "control_plane": { + "type": "object", + "additionalProperties": true, + "required": ["schema_version", "repository", "sync_policy"], + "properties": { + "schema_version": {"const": 1}, + "repository": { + "type": "object", + "additionalProperties": true, + "required": ["remote"], + "properties": {"remote": {"type": "string"}} + }, + "sync_policy": { + "type": "object", + "additionalProperties": true, + "required": ["mode"], + "properties": {"mode": {"const": "manual"}} + } + } + }, + "source_repositories": { + "type": "array", + "minItems": 1, + "items": {"$ref": "#/$defs/repository"} + } + }, + "allOf": [ + {"not": {"anyOf": [ + {"required": ["workspace_root"]}, + {"required": ["project_root"]}, + {"required": ["observed_head"]}, + {"required": ["observed_at"]}, + {"required": ["observation_time"]}, + {"required": ["git_common_dir"]} + ]}} + ], + "$defs": { + "repository": { + "type": "object", + "additionalProperties": true, + "required": ["id", "role", "default_branch", "workspace_binding", "materialization", "operation_policy"], + "properties": { + "id": {"type": "string", "minLength": 1}, + "role": {"type": "string", "minLength": 1}, + "remote": { + "type": "object", + "additionalProperties": true, + "required": ["canonical"], + "properties": { + "canonical": {"type": ["string", "null"]}, + "aliases": {"type": "array", "items": {"type": "string"}} + } + }, + "locator": { + "type": "object", + "additionalProperties": true, + "required": ["type", "value"], + "properties": { + "type": {"type": "string", "minLength": 1}, + "value": {"type": "string", "minLength": 1} + } + }, + "default_branch": {"type": "string", "minLength": 1}, + "workspace_binding": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["type"], + "properties": {"type": {"const": "root"}} + }, + { + "type": "object", + "additionalProperties": false, + "required": ["type", "name"], + "properties": { + "type": {"const": "member"}, + "name": {"type": "string", "pattern": "^(?!\\.{1,2}$)[^/\\\\]+$"}, + "path": {"type": "string", "pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$)).+$"} + } + } + ] + }, + "materialization": { + "type": "object", + "additionalProperties": true, + "required": ["required"], + "properties": {"required": {"type": "boolean"}} + }, + "operation_policy": {"type": "string", "minLength": 1} + }, + "anyOf": [{"required": ["remote"]}, {"required": ["locator"]}], + "allOf": [ + {"not": {"anyOf": [ + {"required": ["workspace_root"]}, + {"required": ["project_root"]}, + {"required": ["observed_head"]}, + {"required": ["observed_at"]}, + {"required": ["observation_time"]}, + {"required": ["git_common_dir"]} + ]}} + ] + } + } +} diff --git a/references/assets/keep-summarizing/workflow.md b/references/assets/keep-summarizing/workflow.md index 4bcacd9..1330447 100644 --- a/references/assets/keep-summarizing/workflow.md +++ b/references/assets/keep-summarizing/workflow.md @@ -20,11 +20,12 @@ docs/ spec/ plan/ - handoff/ + result/ + review/ ``` `/.work-bundle/knowledge/` is the default durable source of truth for one managed workspace. A nested member cwd resolves upward to the containing workspace; source inspection remains scoped to that member `project_root`. Single-repository mode remains current with `workspace_root == project_root`. Legacy knowledge roots are readable only when explicitly selected for migration or read-only intake. -Handoff artifacts live under `.work-bundle/orchestration/handoff/` and are not durable knowledge. +Executor results and review artifacts live under `.work-bundle/orchestration/result/` and `.work-bundle/orchestration/review/`; they are not durable knowledge. ## V3 Lifecycle Authority Model diff --git a/references/assets/orchestration/contract/accepted-task-result-v1.schema.json b/references/assets/orchestration/contract/accepted-task-result-v1.schema.json new file mode 100644 index 0000000..1e92251 --- /dev/null +++ b/references/assets/orchestration/contract/accepted-task-result-v1.schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "accepted-task-result-v1", "type": "object", + "required": ["artifact_type", "schema_version", "id", "plan_id", "task_id", "product_identity", "product_sha256", "executor_result", "implementation_review", "validation_outcomes", "unresolved_material_defects", "knowledge_disposition", "knowledge_action", "date_created", "last_updated"], + "properties": { + "artifact_type": {"const": "accepted-task-result"}, "schema_version": {"const": 1}, "id": {"type": "string", "pattern": "^accepted-[a-z0-9][a-z0-9-]*$"}, + "plan_id": {"type": "string", "pattern": "^plan-[a-z0-9][a-z0-9-]*$"}, "task_id": {"type": "string", "pattern": "^task-[a-z0-9][a-z0-9-]*$"}, + "product_identity": {"type": "object", "required": ["kind", "sha256"], "properties": {"kind": {"enum": ["commit", "worktree"]}, "sha256": {"$ref": "#/$defs/sha"}}, "additionalProperties": false}, "product_sha256": {"$ref": "#/$defs/sha"}, + "executor_result": {"$ref": "#/$defs/reference"}, "implementation_review": {"anyOf": [{"type": "null"}, {"$ref": "#/$defs/reference"}]}, + "validation_outcomes": {"type": "array", "items": {"$ref": "#/$defs/observation"}}, "unresolved_material_defects": {"type": "array", "items": {"type": "string", "minLength": 1}}, + "knowledge_disposition": {"$ref": "#/$defs/knowledge"}, "knowledge_action": {"enum": ["none", "update", "supersede", "reclassify"]}, + "date_created": {"type": "string", "format": "date"}, "last_updated": {"type": "string", "format": "date"} + }, + "$defs": {"sha": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, "reference": {"type": "object", "required": ["id", "sha256"], "properties": {"id": {"type": "string", "minLength": 1}, "sha256": {"$ref": "#/$defs/sha"}}, "additionalProperties": false}, "observation": {"type": "object", "required": ["id", "result", "summary"], "properties": {"id": {"type": "string", "minLength": 1}, "result": {"enum": ["passed", "failed", "blocked", "not-run"]}, "summary": {"type": "string", "minLength": 1}}, "additionalProperties": false}, "knowledge": {"type": "object", "required": ["action", "reason"], "properties": {"action": {"enum": ["none", "update", "supersede", "reclassify"]}, "reason": {"type": "string", "minLength": 1}}, "additionalProperties": false}}, + "additionalProperties": false +} diff --git a/references/assets/orchestration/contract/artifact-family-catalog-v1.schema.json b/references/assets/orchestration/contract/artifact-family-catalog-v1.schema.json new file mode 100644 index 0000000..9cb1ee7 --- /dev/null +++ b/references/assets/orchestration/contract/artifact-family-catalog-v1.schema.json @@ -0,0 +1,108 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "artifact-family-catalog-v1", + "type": "object", + "required": ["catalog_id", "schema_version", "families"], + "properties": { + "catalog_id": {"type": "string", "pattern": "^[a-z][a-z0-9-]*-v[0-9]+$"}, + "schema_version": {"const": 1}, + "families": { + "type": "array", + "minItems": 1, + "items": {"$ref": "#/$defs/family"} + } + }, + "additionalProperties": false, + "$defs": { + "family": { + "type": "object", + "required": ["name", "schema", "representation", "anchor", "locator", "identity", "relationships", "lifecycle", "index"], + "properties": { + "name": {"type": "string", "pattern": "^[a-z][a-z0-9-]*$"}, + "schema": { + "type": "object", + "required": ["id", "path"], + "properties": { + "id": {"type": "string", "pattern": "^[a-z][a-z0-9-]*-v[0-9]+$"}, + "path": {"type": "string", "minLength": 1} + }, + "additionalProperties": false + }, + "representation": {"enum": ["yaml", "markdown-front-matter"]}, + "anchor": {"type": "string", "pattern": "^[a-z][a-z0-9_]*$"}, + "locator": { + "type": "object", + "required": ["template", "variables"], + "properties": { + "template": {"type": "string", "minLength": 1}, + "variables": { + "type": "array", + "items": {"type": "string", "pattern": "^[a-z][a-z0-9_]*$"}, + "uniqueItems": true + } + }, + "additionalProperties": false + }, + "identity": { + "type": "object", + "required": ["field", "pattern"], + "properties": { + "field": {"type": "string", "pattern": "^[a-z][a-z0-9_]*$"}, + "pattern": {"type": "string", "minLength": 1} + }, + "additionalProperties": false + }, + "relationships": { + "type": "object", + "required": ["bindings"], + "properties": { + "bindings": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "field", "required"], + "properties": { + "name": {"type": "string", "pattern": "^[a-z][a-z0-9_]*$"}, + "field": {"type": "string", "pattern": "^[a-z][a-z0-9_]*$"}, + "required": {"type": "boolean"} + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "lifecycle": { + "type": "object", + "required": ["authority", "states", "transitions"], + "properties": { + "authority": {"enum": ["location", "immutable-release"]}, + "states": {"type": "array", "minItems": 1, "items": {"type": "string"}, "uniqueItems": true}, + "transitions": { + "type": "object", + "additionalProperties": {"type": "array", "items": {"type": "string"}, "uniqueItems": true} + } + }, + "additionalProperties": false + }, + "index": { + "oneOf": [ + {"type": "object", "required": ["policy"], "properties": {"policy": {"const": "none"}}, "additionalProperties": false}, + { + "type": "object", + "required": ["path", "source_states", "projection", "format"], + "properties": { + "path": {"type": "string", "minLength": 1}, + "source_states": {"type": "array", "minItems": 1, "items": {"type": "string"}, "uniqueItems": true}, + "projection": {"type": "array", "minItems": 1, "items": {"type": "string"}, "uniqueItems": true}, + "format": {"const": "jsonl"} + }, + "additionalProperties": false + } + ] + } + }, + "additionalProperties": false + } + } +} diff --git a/references/assets/orchestration/contract/artifact-family-catalog-v1.yaml b/references/assets/orchestration/contract/artifact-family-catalog-v1.yaml new file mode 100644 index 0000000..11baaf4 --- /dev/null +++ b/references/assets/orchestration/contract/artifact-family-catalog-v1.yaml @@ -0,0 +1,23 @@ +catalog_id: artifact-family-catalog-v1 +schema_version: 1 +families: + - name: artifact-family-catalog + schema: + id: artifact-family-catalog-v1 + path: artifact-family-catalog-v1.schema.json + representation: yaml + anchor: toolkit_root + locator: + template: references/assets/orchestration/contract/artifact-family-catalog-v1.yaml + variables: [] + identity: + field: catalog_id + pattern: ^artifact-family-catalog-v[0-9]+$ + relationships: + bindings: [] + lifecycle: + authority: immutable-release + states: [released] + transitions: {} + index: + policy: none diff --git a/references/assets/orchestration/contract/artifact-family-catalog-v2.yaml b/references/assets/orchestration/contract/artifact-family-catalog-v2.yaml new file mode 100644 index 0000000..a0ec37e --- /dev/null +++ b/references/assets/orchestration/contract/artifact-family-catalog-v2.yaml @@ -0,0 +1,32 @@ +catalog_id: artifact-family-catalog-v2 +schema_version: 1 +families: + - name: artifact-family-catalog + schema: {id: artifact-family-catalog-v1, path: artifact-family-catalog-v1.schema.json} + representation: yaml + anchor: toolkit_root + locator: {template: references/assets/orchestration/contract/artifact-family-catalog-v2.yaml, variables: []} + identity: {field: catalog_id, pattern: '^artifact-family-catalog-v[0-9]+$'} + relationships: {bindings: []} + lifecycle: {authority: immutable-release, states: [released], transitions: {}} + index: {policy: none} + - name: specification + schema: {id: specification-v1, path: specification-v1.schema.json} + representation: markdown-front-matter + anchor: workspace_root + locator: + template: .work-bundle/orchestration/spec/{state}/{id}.spec.md + variables: [state, id] + identity: + field: id + pattern: '^spec-[a-z0-9][a-z0-9-]*$' + relationships: {bindings: []} + lifecycle: + authority: location + states: [active, archived] + transitions: {active: [archived], archived: []} + index: + path: .work-bundle/orchestration/spec/index.jsonl + source_states: [active, archived] + projection: [id, title, status, purpose, component, date_created, last_updated] + format: jsonl diff --git a/references/assets/orchestration/contract/artifact-family-catalog-v3.yaml b/references/assets/orchestration/contract/artifact-family-catalog-v3.yaml new file mode 100644 index 0000000..99a5140 --- /dev/null +++ b/references/assets/orchestration/contract/artifact-family-catalog-v3.yaml @@ -0,0 +1,91 @@ +catalog_id: artifact-family-catalog-v3 +schema_version: 1 +families: + - name: artifact-family-catalog + schema: {id: artifact-family-catalog-v1, path: artifact-family-catalog-v1.schema.json} + representation: yaml + anchor: toolkit_root + locator: {template: references/assets/orchestration/contract/artifact-family-catalog-v3.yaml, variables: []} + identity: {field: catalog_id, pattern: '^artifact-family-catalog-v[0-9]+$'} + relationships: {bindings: []} + lifecycle: {authority: immutable-release, states: [released], transitions: {}} + index: {policy: none} + - name: specification + schema: {id: specification-v1, path: specification-v1.schema.json} + representation: markdown-front-matter + anchor: workspace_root + locator: + template: .work-bundle/orchestration/spec/{state}/{id}.spec.md + variables: [state, id] + identity: {field: id, pattern: '^spec-[a-z0-9][a-z0-9-]*$'} + relationships: {bindings: []} + lifecycle: + authority: location + states: [active, archived] + transitions: {active: [archived], archived: []} + index: + path: .work-bundle/orchestration/spec/index.jsonl + source_states: [active, archived] + projection: [id, title, status, purpose, component, date_created, last_updated] + format: jsonl + - name: root-plan + schema: {id: plan-v1, path: plan-v1.schema.json} + representation: yaml + anchor: workspace_root + locator: + template: .work-bundle/orchestration/plan/{state}/{id}.plan.yaml + variables: [state, id] + identity: {field: id, pattern: '^plan-[a-z0-9][a-z0-9-]*$'} + relationships: + bindings: + - {name: source_spec, field: source_spec_id, required: true} + lifecycle: + authority: location + states: [active, archived] + transitions: {active: [archived], archived: []} + index: + path: .work-bundle/orchestration/plan/root-plan-index.jsonl + source_states: [active, archived] + projection: [artifact_type, id, source_spec_id, goal, status, date_created, last_updated] + format: jsonl + - name: phase + schema: {id: phase-v1, path: phase-v1.schema.json} + representation: yaml + anchor: workspace_root + locator: + template: .work-bundle/orchestration/plan/{state}/{plan}/{id}.phase.yaml + variables: [state, plan, id] + identity: {field: id, pattern: '^phase-[a-z0-9][a-z0-9-]*$'} + relationships: + bindings: + - {name: plan, field: plan_id, required: true} + lifecycle: + authority: location + states: [active, archived] + transitions: {active: [archived], archived: []} + index: + path: .work-bundle/orchestration/plan/phase-index.jsonl + source_states: [active, archived] + projection: [artifact_type, id, plan_id, name, status, order, date_created, last_updated] + format: jsonl + - name: task + schema: {id: task-v1, path: task-v1.schema.json} + representation: yaml + anchor: workspace_root + locator: + template: .work-bundle/orchestration/plan/{state}/{plan}/{phase}/{id}.task.yaml + variables: [state, plan, phase, id] + identity: {field: id, pattern: '^task-[a-z0-9][a-z0-9-]*$'} + relationships: + bindings: + - {name: plan, field: plan_id, required: true} + - {name: phase, field: phase_id, required: true} + lifecycle: + authority: location + states: [active, archived] + transitions: {active: [archived], archived: []} + index: + path: .work-bundle/orchestration/plan/task-index.jsonl + source_states: [active, archived] + projection: [artifact_type, id, plan_id, phase_id, name, status, order, task_type, date_created, last_updated] + format: jsonl diff --git a/references/assets/orchestration/contract/artifact-family-catalog-v4.yaml b/references/assets/orchestration/contract/artifact-family-catalog-v4.yaml new file mode 100644 index 0000000..30c665c --- /dev/null +++ b/references/assets/orchestration/contract/artifact-family-catalog-v4.yaml @@ -0,0 +1,162 @@ +catalog_id: artifact-family-catalog-v4 +schema_version: 1 +families: + - name: artifact-family-catalog + schema: {id: artifact-family-catalog-v1, path: artifact-family-catalog-v1.schema.json} + representation: yaml + anchor: toolkit_root + locator: {template: references/assets/orchestration/contract/artifact-family-catalog-v4.yaml, variables: []} + identity: {field: catalog_id, pattern: '^artifact-family-catalog-v[0-9]+$'} + relationships: {bindings: []} + lifecycle: {authority: immutable-release, states: [released], transitions: {}} + index: {policy: none} + - name: specification + schema: {id: specification-v1, path: specification-v1.schema.json} + representation: markdown-front-matter + anchor: workspace_root + locator: {template: '.work-bundle/orchestration/spec/{state}/{id}.spec.md', variables: [state, id]} + identity: {field: id, pattern: '^spec-[a-z0-9][a-z0-9-]*$'} + relationships: {bindings: []} + lifecycle: + authority: location + states: [active, archived] + transitions: {active: [archived], archived: []} + index: + path: .work-bundle/orchestration/spec/index.jsonl + source_states: [active, archived] + projection: [id, title, status, purpose, component, date_created, last_updated] + format: jsonl + - name: root-plan + schema: {id: plan-v1, path: plan-v1.schema.json} + representation: yaml + anchor: workspace_root + locator: {template: '.work-bundle/orchestration/plan/{state}/{id}.plan.yaml', variables: [state, id]} + identity: {field: id, pattern: '^plan-[a-z0-9][a-z0-9-]*$'} + relationships: + bindings: + - {name: source_spec, field: source_spec_id, required: true} + lifecycle: + authority: location + states: [active, archived] + transitions: {active: [archived], archived: []} + index: + path: .work-bundle/orchestration/plan/root-plan-index.jsonl + source_states: [active, archived] + projection: [artifact_type, id, source_spec_id, goal, status, date_created, last_updated] + format: jsonl + - name: phase + schema: {id: phase-v1, path: phase-v1.schema.json} + representation: yaml + anchor: workspace_root + locator: {template: '.work-bundle/orchestration/plan/{state}/{plan}/{id}.phase.yaml', variables: [state, plan, id]} + identity: {field: id, pattern: '^phase-[a-z0-9][a-z0-9-]*$'} + relationships: + bindings: + - {name: plan, field: plan_id, required: true} + lifecycle: + authority: location + states: [active, archived] + transitions: {active: [archived], archived: []} + index: + path: .work-bundle/orchestration/plan/phase-index.jsonl + source_states: [active, archived] + projection: [artifact_type, id, plan_id, name, status, order, date_created, last_updated] + format: jsonl + - name: task + schema: {id: task-v1, path: task-v1.schema.json} + representation: yaml + anchor: workspace_root + locator: {template: '.work-bundle/orchestration/plan/{state}/{plan}/{phase}/{id}.task.yaml', variables: [state, plan, phase, id]} + identity: {field: id, pattern: '^task-[a-z0-9][a-z0-9-]*$'} + relationships: + bindings: + - {name: plan, field: plan_id, required: true} + - {name: phase, field: phase_id, required: true} + lifecycle: + authority: location + states: [active, archived] + transitions: {active: [archived], archived: []} + index: + path: .work-bundle/orchestration/plan/task-index.jsonl + source_states: [active, archived] + projection: [artifact_type, id, plan_id, phase_id, name, status, order, task_type, date_created, last_updated] + format: jsonl + - name: executor-result + schema: {id: executor-result-v1, path: executor-result-v1.schema.json} + representation: yaml + anchor: workspace_root + locator: {template: '.work-bundle/orchestration/result/executor/{state}/{plan}/{task}/{id}.executor-result.yaml', variables: [state, plan, task, id]} + identity: {field: id, pattern: '^result-[a-z0-9][a-z0-9-]*$'} + relationships: + bindings: + - {name: plan, field: plan_id, required: true} + - {name: task, field: task_id, required: true} + lifecycle: + authority: location + states: [active, reviewed, superseded, archived] + transitions: + active: [reviewed, superseded, archived] + reviewed: [superseded, archived] + superseded: [archived] + archived: [] + index: + path: .work-bundle/orchestration/result/executor/executor-result-index.jsonl + source_states: [active, reviewed, superseded, archived] + projection: [artifact_type, id, plan_id, phase_id, task_id, result_state, date_created, last_updated] + format: jsonl + - name: implementation-review + schema: {id: implementation-review-v1, path: implementation-review-v1.schema.json} + representation: yaml + anchor: workspace_root + locator: {template: '.work-bundle/orchestration/review/implementation/{state}/{plan}/{id}.implementation-review.yaml', variables: [state, plan, id]} + identity: {field: id, pattern: '^review-[a-z0-9][a-z0-9-]*$'} + relationships: + bindings: + - {name: plan, field: plan_id, required: true} + - {name: task, field: task_id, required: false} + lifecycle: + authority: location + states: [active, superseded, archived] + transitions: {active: [superseded, archived], superseded: [archived], archived: []} + index: + path: .work-bundle/orchestration/review/implementation/implementation-review-index.jsonl + source_states: [active, superseded, archived] + projection: [artifact_type, id, plan_id, task_id, scope, verdict, target_sha256, date_created, last_updated] + format: jsonl + - name: accepted-task-result + schema: {id: accepted-task-result-v1, path: accepted-task-result-v1.schema.json} + representation: yaml + anchor: workspace_root + locator: {template: '.work-bundle/orchestration/result/accepted/{state}/{plan}/{task}/{id}.accepted-task-result.yaml', variables: [state, plan, task, id]} + identity: {field: id, pattern: '^accepted-[a-z0-9][a-z0-9-]*$'} + relationships: + bindings: + - {name: plan, field: plan_id, required: true} + - {name: task, field: task_id, required: true} + lifecycle: + authority: location + states: [active, superseded, archived] + transitions: {active: [superseded, archived], superseded: [archived], archived: []} + index: + path: .work-bundle/orchestration/result/accepted/accepted-task-result-index.jsonl + source_states: [active, superseded, archived] + projection: [artifact_type, id, plan_id, task_id, product_sha256, knowledge_action, date_created, last_updated] + format: jsonl + - name: final-workflow-review + schema: {id: final-workflow-review-v1, path: final-workflow-review-v1.schema.json} + representation: yaml + anchor: workspace_root + locator: {template: '.work-bundle/orchestration/review/final/{state}/{plan}/{id}.final-workflow-review.yaml', variables: [state, plan, id]} + identity: {field: id, pattern: '^final-[a-z0-9][a-z0-9-]*$'} + relationships: + bindings: + - {name: plan, field: plan_id, required: true} + lifecycle: + authority: location + states: [active, archived] + transitions: {active: [archived], archived: []} + index: + path: .work-bundle/orchestration/review/final/final-workflow-review-index.jsonl + source_states: [active, archived] + projection: [artifact_type, id, plan_id, verdict, archive_ready, target_sha256, date_created, last_updated] + format: jsonl diff --git a/references/assets/orchestration/contract/artifact-family-catalog-v5.yaml b/references/assets/orchestration/contract/artifact-family-catalog-v5.yaml new file mode 100644 index 0000000..1ad8b4c --- /dev/null +++ b/references/assets/orchestration/contract/artifact-family-catalog-v5.yaml @@ -0,0 +1,162 @@ +catalog_id: artifact-family-catalog-v5 +schema_version: 1 +families: + - name: artifact-family-catalog + schema: {id: artifact-family-catalog-v1, path: artifact-family-catalog-v1.schema.json} + representation: yaml + anchor: toolkit_root + locator: {template: references/assets/orchestration/contract/artifact-family-catalog-v5.yaml, variables: []} + identity: {field: catalog_id, pattern: '^artifact-family-catalog-v[0-9]+$'} + relationships: {bindings: []} + lifecycle: {authority: immutable-release, states: [released], transitions: {}} + index: {policy: none} + - name: specification + schema: {id: specification-v1, path: specification-v1.schema.json} + representation: markdown-front-matter + anchor: workspace_root + locator: {template: '.work-bundle/orchestration/spec/{state}/{id}.spec.md', variables: [state, id]} + identity: {field: id, pattern: '^spec-[a-z0-9][a-z0-9-]*$'} + relationships: {bindings: []} + lifecycle: + authority: location + states: [active, archived] + transitions: {active: [archived], archived: []} + index: + path: .work-bundle/orchestration/spec/index.jsonl + source_states: [active, archived] + projection: [id, title, status, purpose, component, date_created, last_updated] + format: jsonl + - name: root-plan + schema: {id: plan-v1, path: plan-v1.schema.json} + representation: yaml + anchor: workspace_root + locator: {template: '.work-bundle/orchestration/plan/{state}/{id}.plan.yaml', variables: [state, id]} + identity: {field: id, pattern: '^plan-[a-z0-9][a-z0-9-]*$'} + relationships: + bindings: + - {name: source_spec, field: source_spec_id, required: true} + lifecycle: + authority: location + states: [active, archived] + transitions: {active: [archived], archived: []} + index: + path: .work-bundle/orchestration/plan/root-plan-index.jsonl + source_states: [active, archived] + projection: [artifact_type, id, source_spec_id, goal, status, date_created, last_updated] + format: jsonl + - name: phase + schema: {id: phase-v1, path: phase-v1.schema.json} + representation: yaml + anchor: workspace_root + locator: {template: '.work-bundle/orchestration/plan/{state}/{plan}/{id}.phase.yaml', variables: [state, plan, id]} + identity: {field: id, pattern: '^phase-[a-z0-9][a-z0-9-]*$'} + relationships: + bindings: + - {name: plan, field: plan_id, required: true} + lifecycle: + authority: location + states: [active, archived] + transitions: {active: [archived], archived: []} + index: + path: .work-bundle/orchestration/plan/phase-index.jsonl + source_states: [active, archived] + projection: [artifact_type, id, plan_id, name, status, order, date_created, last_updated] + format: jsonl + - name: task + schema: {id: task-v2, path: task-v2.schema.json} + representation: yaml + anchor: workspace_root + locator: {template: '.work-bundle/orchestration/plan/{state}/{plan}/{phase}/{id}.task.yaml', variables: [state, plan, phase, id]} + identity: {field: id, pattern: '^task-[a-z0-9][a-z0-9-]*$'} + relationships: + bindings: + - {name: plan, field: plan_id, required: true} + - {name: phase, field: phase_id, required: true} + lifecycle: + authority: location + states: [active, archived] + transitions: {active: [archived], archived: []} + index: + path: .work-bundle/orchestration/plan/task-index.jsonl + source_states: [active, archived] + projection: [artifact_type, id, plan_id, phase_id, name, status, order, task_type, date_created, last_updated] + format: jsonl + - name: executor-result + schema: {id: executor-result-v1, path: executor-result-v1.schema.json} + representation: yaml + anchor: workspace_root + locator: {template: '.work-bundle/orchestration/result/executor/{state}/{plan}/{task}/{id}.executor-result.yaml', variables: [state, plan, task, id]} + identity: {field: id, pattern: '^result-[a-z0-9][a-z0-9-]*$'} + relationships: + bindings: + - {name: plan, field: plan_id, required: true} + - {name: task, field: task_id, required: true} + lifecycle: + authority: location + states: [active, reviewed, superseded, archived] + transitions: + active: [reviewed, superseded, archived] + reviewed: [superseded, archived] + superseded: [archived] + archived: [] + index: + path: .work-bundle/orchestration/result/executor/executor-result-index.jsonl + source_states: [active, reviewed, superseded, archived] + projection: [artifact_type, id, plan_id, phase_id, task_id, result_state, date_created, last_updated] + format: jsonl + - name: implementation-review + schema: {id: implementation-review-v2, path: implementation-review-v2.schema.json} + representation: yaml + anchor: workspace_root + locator: {template: '.work-bundle/orchestration/review/implementation/{state}/{plan}/{id}.implementation-review.yaml', variables: [state, plan, id]} + identity: {field: id, pattern: '^review-[a-z0-9][a-z0-9-]*$'} + relationships: + bindings: + - {name: plan, field: plan_id, required: true} + - {name: task, field: task_id, required: false} + lifecycle: + authority: location + states: [active, superseded, archived] + transitions: {active: [superseded, archived], superseded: [archived], archived: []} + index: + path: .work-bundle/orchestration/review/implementation/implementation-review-index.jsonl + source_states: [active, superseded, archived] + projection: [artifact_type, id, plan_id, task_id, scope, verdict, target_sha256, date_created, last_updated] + format: jsonl + - name: accepted-task-result + schema: {id: accepted-task-result-v1, path: accepted-task-result-v1.schema.json} + representation: yaml + anchor: workspace_root + locator: {template: '.work-bundle/orchestration/result/accepted/{state}/{plan}/{task}/{id}.accepted-task-result.yaml', variables: [state, plan, task, id]} + identity: {field: id, pattern: '^accepted-[a-z0-9][a-z0-9-]*$'} + relationships: + bindings: + - {name: plan, field: plan_id, required: true} + - {name: task, field: task_id, required: true} + lifecycle: + authority: location + states: [active, superseded, archived] + transitions: {active: [superseded, archived], superseded: [archived], archived: []} + index: + path: .work-bundle/orchestration/result/accepted/accepted-task-result-index.jsonl + source_states: [active, superseded, archived] + projection: [artifact_type, id, plan_id, task_id, product_sha256, knowledge_action, date_created, last_updated] + format: jsonl + - name: final-workflow-review + schema: {id: final-workflow-review-v1, path: final-workflow-review-v1.schema.json} + representation: yaml + anchor: workspace_root + locator: {template: '.work-bundle/orchestration/review/final/{state}/{plan}/{id}.final-workflow-review.yaml', variables: [state, plan, id]} + identity: {field: id, pattern: '^final-[a-z0-9][a-z0-9-]*$'} + relationships: + bindings: + - {name: plan, field: plan_id, required: true} + lifecycle: + authority: location + states: [active, archived] + transitions: {active: [archived], archived: []} + index: + path: .work-bundle/orchestration/review/final/final-workflow-review-index.jsonl + source_states: [active, archived] + projection: [artifact_type, id, plan_id, verdict, archive_ready, target_sha256, date_created, last_updated] + format: jsonl diff --git a/references/assets/orchestration/contract/executor-result-v1.schema.json b/references/assets/orchestration/contract/executor-result-v1.schema.json new file mode 100644 index 0000000..50a3a9d --- /dev/null +++ b/references/assets/orchestration/contract/executor-result-v1.schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "executor-result-v1", + "type": "object", + "required": ["artifact_type", "schema_version", "id", "plan_id", "phase_id", "task_id", "result_state", "summary", "changes", "validation_observations", "unresolved_product_blockers", "task_fit", "repository_observations", "codegraph_observations", "delegation", "knowledge_disposition", "date_created", "last_updated"], + "properties": { + "artifact_type": {"const": "executor-result"}, + "schema_version": {"const": 1}, + "id": {"type": "string", "pattern": "^result-[a-z0-9][a-z0-9-]*$"}, + "plan_id": {"type": "string", "pattern": "^plan-[a-z0-9][a-z0-9-]*$"}, + "phase_id": {"type": ["string", "null"]}, + "task_id": {"type": "string", "pattern": "^task-[a-z0-9][a-z0-9-]*$"}, + "result_state": {"enum": ["implemented", "partial", "blocked"]}, + "summary": {"type": "string", "minLength": 1}, + "changes": {"type": "array", "items": {"$ref": "#/$defs/change"}}, + "validation_observations": {"type": "array", "items": {"$ref": "#/$defs/observation"}}, + "unresolved_product_blockers": {"type": "array", "items": {"type": "string", "minLength": 1}}, + "task_fit": {"type": "object", "required": ["status", "summary"], "properties": {"status": {"enum": ["complete", "partial", "blocked"]}, "summary": {"type": "string", "minLength": 1}}, "additionalProperties": false}, + "repository_observations": {"type": "object"}, + "codegraph_observations": {"type": "object"}, + "delegation": {"type": "object"}, + "knowledge_disposition": {"$ref": "#/$defs/knowledge"}, + "date_created": {"type": "string", "format": "date"}, + "last_updated": {"type": "string", "format": "date"} + }, + "$defs": { + "change": {"type": "object", "required": ["path", "summary"], "properties": {"path": {"type": "string", "minLength": 1}, "summary": {"type": "string", "minLength": 1}}, "additionalProperties": false}, + "observation": {"type": "object", "required": ["id", "result", "summary"], "properties": {"id": {"type": "string", "minLength": 1}, "command": {"type": "string"}, "result": {"enum": ["passed", "failed", "blocked", "not-run"]}, "summary": {"type": "string", "minLength": 1}}, "additionalProperties": false}, + "knowledge": {"type": "object", "required": ["action", "reason"], "properties": {"action": {"enum": ["none", "update", "supersede", "reclassify"]}, "reason": {"type": "string", "minLength": 1}}, "additionalProperties": false} + }, + "additionalProperties": false +} diff --git a/references/assets/orchestration/contract/final-workflow-review-v1.schema.json b/references/assets/orchestration/contract/final-workflow-review-v1.schema.json new file mode 100644 index 0000000..07624c3 --- /dev/null +++ b/references/assets/orchestration/contract/final-workflow-review-v1.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "final-workflow-review-v1", "type": "object", + "required": ["artifact_type", "schema_version", "id", "plan_id", "specification_id", "plan_identity", "candidate_identity", "target_sha256", "coverage", "accepted_results", "accepted_reviews", "test_outcomes", "unresolved_material_defects", "knowledge_disposition", "knowledge_return", "repository_finalization", "verdict", "archive_ready", "reasons", "date_created", "last_updated"], + "properties": { + "artifact_type": {"const": "final-workflow-review"}, "schema_version": {"const": 1}, "id": {"type": "string", "pattern": "^final-[a-z0-9][a-z0-9-]*$"}, "plan_id": {"type": "string", "pattern": "^plan-[a-z0-9][a-z0-9-]*$"}, + "specification_id": {"type": "string", "minLength": 1}, "plan_identity": {"$ref": "#/$defs/reference"}, "candidate_identity": {"type": "object", "required": ["kind", "sha256"], "properties": {"kind": {"enum": ["commit", "worktree"]}, "sha256": {"$ref": "#/$defs/sha"}}, "additionalProperties": false}, "target_sha256": {"$ref": "#/$defs/sha"}, + "coverage": {"type": "object", "required": ["planned", "accepted", "missing"], "properties": {"planned": {"type": "integer", "minimum": 0}, "accepted": {"type": "integer", "minimum": 0}, "missing": {"type": "array", "items": {"type": "string"}}}, "additionalProperties": false}, + "accepted_results": {"type": "array", "items": {"$ref": "#/$defs/taskReference"}}, "accepted_reviews": {"type": "array", "items": {"$ref": "#/$defs/reference"}}, "test_outcomes": {"type": "array", "items": {"$ref": "#/$defs/observation"}}, "unresolved_material_defects": {"type": "array", "items": {"type": "string", "minLength": 1}}, + "knowledge_disposition": {"$ref": "#/$defs/knowledge"}, "knowledge_return": {"type": "object", "required": ["status", "reference"], "properties": {"status": {"enum": ["pending", "completed", "not-needed", "blocked"]}, "reference": {"type": ["string", "null"]}}, "additionalProperties": false}, + "repository_finalization": {"type": "object", "required": ["repositories"], "properties": {"repositories": {"type": "array", "minItems": 1, "items": {"type": "object", "required": ["repository_id", "root", "head"], "properties": {"repository_id": {"type": "string", "minLength": 1}, "root": {"type": "string", "minLength": 1}, "head": {"type": "string", "pattern": "^[0-9a-f]{40}$"}}, "additionalProperties": false}}}, "additionalProperties": false}, + "verdict": {"enum": ["accept", "repair", "blocked"]}, "archive_ready": {"type": "boolean"}, "reasons": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}, + "date_created": {"type": "string", "format": "date"}, "last_updated": {"type": "string", "format": "date"} + }, + "$defs": {"sha": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, "reference": {"type": "object", "required": ["id", "sha256"], "properties": {"id": {"type": "string", "minLength": 1}, "sha256": {"$ref": "#/$defs/sha"}}, "additionalProperties": false}, "taskReference": {"type": "object", "required": ["id", "task_id", "sha256"], "properties": {"id": {"type": "string", "minLength": 1}, "task_id": {"type": "string", "pattern": "^task-[a-z0-9][a-z0-9-]*$"}, "sha256": {"$ref": "#/$defs/sha"}}, "additionalProperties": false}, "observation": {"type": "object", "required": ["id", "result", "summary"], "properties": {"id": {"type": "string", "minLength": 1}, "result": {"enum": ["passed", "failed", "blocked", "not-run"]}, "summary": {"type": "string", "minLength": 1}}, "additionalProperties": false}, "knowledge": {"type": "object", "required": ["action", "reason"], "properties": {"action": {"enum": ["none", "update", "supersede", "reclassify"]}, "reason": {"type": "string", "minLength": 1}}, "additionalProperties": false}}, + "additionalProperties": false +} diff --git a/references/assets/orchestration/contract/handoff-executor-result-v1.md b/references/assets/orchestration/contract/handoff-executor-result-v1.md index ef24196..c3ba2f6 100644 --- a/references/assets/orchestration/contract/handoff-executor-result-v1.md +++ b/references/assets/orchestration/contract/handoff-executor-result-v1.md @@ -1,225 +1,13 @@ ---- -id: handoff-executor-result-v1 -type: contract -status: active -artifact_type: executor-result-handoff -default_format: yaml ---- +# Executor Result v1 -# Executor Result Handoff Contract +`executor-result-v1` is the canonical factual continuation record created after executing one plan task. Its machine structure, identity, bindings, location, lifecycle, and index projection are owned by `artifact-family-catalog-v4.yaml` and `executor-result-v1.schema.json`. -Executor-result handoffs are compact continuation artifacts from an executor to the orchestration agent. They default to sparse YAML and record only facts needed for continuation, review, and safety validation. +The agent-authored semantic content records implemented scope, changed paths, focused validation observations, unresolved product blockers, task fit, repository and CodeGraph observations, delegation provenance, and task-local knowledge disposition. -Templates define the maximum available fields, not mandatory output shape. Omit optional blocks when they do not apply. +An executor result never issues a product verdict. It contains no implementation-review decision, accepted-result decision, final-audit conclusion, repair recommendation, or knowledge-write authorization. A distinct reviewer compares the exact frozen implementation directly with the verified specification and plan. -## Default Path +Before creation or transition, validate the entire schema, identity, plan/task bindings, canonical path, collision state, and requested lifecycle operation. Write one artifact atomically. After creation, perform only lightweight integrity checks and rebuild the disposable index projection. If an index update fails after the canonical write, report the partial effect truthfully. -```text -.work-bundle/orchestration/handoff/executor/active/handoff-exec-YYYYMMDD-001-slug.yaml -``` +Missing or defective executor-result structure blocks continuation that requires that artifact. It does not veto direct product review when the exact implementation candidate, verified specification/plan, and claim-relevant observations are independently available. -## Sparse YAML Schema - -```yaml -id: handoff-exec-YYYYMMDD-001-slug -type: executor-result -status: active -lifecycle_authority: location-v1 -project: work-bundle -created_at: YYYY-MM-DD -updated_at: YYYY-MM-DD -related: - spec: spec-id-or-null - plan: plan-id-or-null - phase: phase-id-or-null - task: task-id-or-null - -result: - state: completed | blocked | partial | failed - summary: "One or two sentences maximum." - -changes: - files: - - path: path/to/file - action: created | modified | deleted | inspected - symbols: [] - notes: "Only when needed." - -validation: - commands: - - id: VAL-001 - invariant_ids: [INV-001] - command: "exact command" - result: passed | failed | skipped - note: "Failure reason or skip reason only." - -evidence_closure: - result: passed | incapable | contradictory | stale | wrong_boundary | failed | missing | unexecuted - invariants: - - id: INV-001 - boundary: unit | component | integration | runtime | ui_visual | performance | accessibility | inspection | other - freshness: current_task_batch - evidence_ids: [VAL-001] - closure_result: passed | incapable | contradictory | stale | wrong_boundary | failed | missing | unexecuted - repair_owner: null | task | plan | specification - -knowledge_disposition: - action: none | update | supersede | reclassify - reason: "Task-local post-validation evidence." - affected_authority: - - AUTH-NNN-or-allocated-source-id-or-task-scope-path - -contract_decoupling: - common_contract_group: CG-001 - common_contract_paths: - - path/to/contract.md - validation_scope: - - common-contract - - accepted-prior-handoffs - - task-local-files - forbidden_peer_validation: respected | violated | not-applicable - note: "Only include when needed." - -barrier: - id: BAR-001 - role: participant | convergence-owner - readiness: reached | blocked | not-applicable - participants_complete_or_blocked: true | false | null - note: "Only include when needed." - -convergence: - owner: task-id-or-null - status: ready | completed | blocked | not-applicable - checks: - - "exact command or inspection" - -defect_closure: - status: not-applicable | carried-to-review | completed | blocked - evidence: - - defect-id-or-path - note: "Review-only closure evidence; executors do not delete evidence." - -unresolved: - - "Only include blockers or issues that remain." - -task_fit_check: - task: path-or-id - result: clean | repaired | unresolved | skipped - artifacts_checked: - - compiled task brief - - assigned task - findings: [] - -repository: - - root: /absolute/path - target_kind: git-backed | local-project - preflight_kind: git-clean-worktree | local-project - baseline: initial | accepted-handoff - status: clean | blocked - metadata: - repository_id: null - expected_branch: null - actual_branch: null - branch_status: matched | mismatch | not-applicable | unknown - expected_commit: null - actual_commit: null - commit_status: matched | stale | missing | unborn | not-applicable | unknown - baseline_status: current | stale | unborn | not-git | unknown - -codegraph: - - root: /absolute/path - applicable: true | false - up_to_date: true | false - reason: null | no-index | sync-failed | not-source-code | blocked - -delegation_evidence: - delegated: true - owner_kind: subagent - agent_id: agent-or-provider-identity - run_id: task-run-identity - mechanism: host-native | execution-flow - -allocation_evidence: - allocated_rules: - - id: rule-id - status: loaded | condition-evaluated | skipped | unavailable - reason: null - allocated_skills: - - name: skill-name - status: used | acknowledged | skipped | unavailable - reason: null -``` - -## Required By Applicability - -- `id`, `type`, `status`, `lifecycle_authority: location-v1`, `project`, `created_at`, `related`, and `result` are always required for newly written handoffs. Embedded `status` is immutable creation metadata; current lifecycle status comes from the status-specific location. -- For a task-scoped executor-result, `related.plan` and `related.task` are required and must equal the assigned task's `plan_id` and `id`. Nested `related.plan` and flat `related_plan` must resolve to exactly one identity. Missing, null, conflicting, or mismatched plan identity fails closed before `Completed` and before `build-review-package` produces a review package. The shared `validate-executor-result` helper owns this gate. Do not infer plan identity from a local task ID. -- `changes.files` is required when files, symbols, artifacts, schemas, commands, or docs changed or were inspected as the task output. -- `validation.commands` records commands, tests, lints, inspections, or manual verification actually run or intentionally skipped by the executor. Focused test-first corroboration may be reported separately from compiled controller-owned final validation. A compiled final command that has not yet been independently observed is omitted rather than duplicated or fabricated merely to admit the immutable handoff; any supplied report must remain well formed and truthful. -- `evidence_closure` is required for a completed task whose compiled `evidence_capability.result` is `mapped`. Its invariant IDs, boundary, freshness, and evidence IDs must exactly match allocated task authority. A supplied executor report for referenced validation carries the allocated `id` and `invariant_ids`; direct harness observation reuses those compiled identities and remains mandatory at terminal validation even when no executor report exists. Only all-`passed` capable, current, correctly bounded harness evidence closes the task. Negative results fail closed and name the first repair owner: task for failed or stale implementation evidence; plan for missing, wrong-boundary, or incapable allocation; specification for contradictory accepted authority. Executor-authored closure is corroboration and cannot replace harness observation or semantic review. -- `knowledge_disposition` is required for every completed or partial meaningful move. It records task-local evidence only and does not authorize durable-knowledge retrieval or writes. A change action requires allocated `AUTH-NNN` aliases from the task's accepted decision authority, allocated source IDs, or exact paths already present in the compiled task scope; `none` requires an empty affected-authority list. Invented or unallocated AUTH aliases fail closed. -- `contract_decoupling` is required when a task is marked contract-decoupled or depends on a common contract group. -- `barrier` is required when a task is a barrier participant or convergence owner. -- `convergence` is required when the task owns post-barrier joint debug, integration checks, or cross-branch validation. -- `defect_closure` is required when a review task closes or carries specification-included defect evidence. -- `unresolved` is included only when blockers or issues remain. -- `task_fit_check` is required for completed and partial task results. It records the assigned task, result `clean|repaired|unresolved|skipped`, artifacts checked, and meaningful findings. -- Review requirements come from compiled task authority. Review packets, verdicts, receipts, accepted-result identities, observations, and later audit facts are wrong-owner fields and must not be written into a new executor-result handoff. A structurally complete review-required executor result is admitted before review; accepted-result materialization later joins it with the exact published review and current observations. -- `repository` is required when repository preflight, accepted baseline, changed paths, or blocker state matters for continuation. -- `repository[].metadata` is required when project metadata baseline was used for target resolution, branch checks, commit checks, or CodeGraph policy decisions. -- `codegraph` is required when source-code inspection or edits were in scope. Keep it compact: `root`, `applicable`, `up_to_date`, and required fallback or blocker facts are enough unless a failure needs detail. -- `delegation_evidence` is required for every task executor-result and is optional for non-task scopes. Its five-field closed shape proves mandatory subagent ownership without UI-specific semantics. -- `allocation_evidence` is required when allocated_rules or allocated_skills materially shaped execution or when an allocated rule/skill was unavailable, skipped, stale, or inapplicable. - -## Forbidden Executor-Result Fields - -Validation must reject executor-result handoffs that contain these top-level fields: - -```yaml -suggested_durable_conclusions: [] -durable_candidate_facts: [] -recommended_orchestration_review: [] -recommended_next_actions: [] -delegation: {} -deviations: [] -strategy_advice: [] -knowledge_persistence: [] -baseline: {} -acceptance_review: {} -accepted_result: {} -reviewer_run: {} -publication: {} -receipt: {} -``` - -Use `delegation_evidence` for compact delegation proof. Use `unresolved` and `task_fit_check.findings` for remaining issues instead of `deviations`. Do not include a top-level `baseline`; the helper owns pre-task baseline capture, and executor-result cannot supply or replace that baseline. - -## Safety Evidence - -Compact handoffs must not weaken safety gates: - -- Repository evidence must preserve root, target kind, preflight kind, baseline, and clean or blocked result when applicable. -- Metadata evidence must preserve repository id, expected and actual branch, expected and actual commit, branch status, commit status, and baseline status when project metadata preflight applies. -- CodeGraph evidence must preserve no-index fallback, sync-failed, stale, or blocker facts when applicable. -- Delegation evidence must preserve delegated state, `owner_kind: subagent`, minimum agent/run identity, and `host-native|execution-flow` mechanism. UI, visibility, fallback, controller-owner, and internal-worker fields are invalid. -- Executor validation evidence must list exact commands or inspections it actually performed and their result; it does not claim an unexecuted controller-owned final command. Executor-authored `result`, `exit_code`, or an equivalently named receipt block is corroboration, not independent proof and not authority for `Completed`. Direct helper observation of every compiled final validation in the bound worktree is the terminal evidence. -- Task-fit evidence must prove the executor followed the compiled brief and assigned task. Full specification, root-plan, and phase inspection is an escalation path when compiled context is inconsistent. -- Published review authority must identify review independence, the reviewed tree, verdict, and findings outside the executor-result handoff. Accepted-result materialization owns the join and never rewrites the original handoff. -- Executor-result handoffs must not retrieve or write `.work-bundle/knowledge/`. -- `knowledge_disposition.action` is exactly `none`, `update`, `supersede`, or `reclassify`; reasons and affected authority must not name knowledge paths or any `ks-*` skill, and review owns any approved persistence follow-up. -- Contract-decoupled handoffs must show validation against the common contract and accepted prior handoffs, not sibling in-progress implementation. -- Barrier handoffs must show whether the participant reached the barrier or blocked before convergence work is scheduled. -- Defect closure handoffs must use review-owned lifecycle evidence and must not delete defect evidence files. - -## Immutable Lifecycle Authority - -New handoffs are marked `lifecycle_authority: location-v1`. Their complete bytes never change after creation. The controller moves the same bytes among `active/`, `reviewed/`, `superseded/`, and `archived/`; the index derives current status from that location and lookups search every status directory. Same-state requests are no-ops and write neither artifact, override, index, nor dispatch evidence. - -Unmarked historical handoffs are not rewritten or bulk-migrated. Without an override, an unmarked file in `active/` uses a recognized embedded status and an unmarked file in a non-active status directory uses its location. On the first actual explicit status change, including return to `active`, the controller writes only `handoff/legacy-status-overrides/.json`, binding the complete-byte digest, type, related plan/task, and current status. A valid override then takes precedence and must agree with location. The index preserves a pre-existing duplicate identity only when every copy is unmarked, co-located in the same lifecycle directory, and has no override; identity-based lifecycle operations remain ambiguous and fail closed. New identities remain unique. Every other duplicate identity, type/folder disagreement, task/plan contradiction, or override digest/binding/location contradiction fails closed. - -## Format Guidance - -- Small task handoffs should normally be 20-60 lines. -- Medium executor task handoffs should be at most 120 lines; there is no minimum line count. -- Phase and plan result handoffs should normally stay under 180 lines unless real blockers, broad file changes, or many validation results justify more. -- Markdown is allowed only when a real blocker, failure, or broad cross-repository impact cannot be safely represented in sparse YAML. +Historical handoffs, embedded legacy statuses, filename inference, lifecycle override sidecars, and fallback indexes are unsupported. Current adapters neither inspect nor migrate their instances. diff --git a/references/assets/orchestration/contract/handoff-orchestration-v1.md b/references/assets/orchestration/contract/handoff-orchestration-v1.md index bd1df71..52956c0 100644 --- a/references/assets/orchestration/contract/handoff-orchestration-v1.md +++ b/references/assets/orchestration/contract/handoff-orchestration-v1.md @@ -1,24 +1,21 @@ --- id: handoff-orchestration-v1 -type: contract -status: legacy -artifact_type: orchestration-handoff +type: historical-exclusion +status: retired +artifact_type: retired-orchestration-handoff active_creation: false --- -# Orchestration Handoff Contract +# Historical Exclusion: Orchestration Handoffs -This contract is legacy-only. It remains as compatibility documentation for existing archived or historical `handoff-orch-*` artifacts, but the active workflow must not create new orchestration handoffs. +This file records a retired artifact name only. It is not a current contract and grants no compatibility authority. -Continuation state now comes from active specifications, plans, phases, tasks, indexes, and compact `executor-result` handoffs. +Current tools must not inspect, index, migrate, replay, preserve, or create `handoff-orch-*` artifacts. Historical instances may be discarded when no separately verified current semantic authority would be lost. -## Active Workflow Rule +Continuation state comes from canonical specifications, plan trees, executor results, implementation reviews, accepted task results, and final workflow reviews. -- Do not create new active `handoff-orch-*` artifacts. -- Do not advertise orchestration handoffs as an active continuation feature. -- Do not require orchestration handoffs for execution, review, or archive readiness. -- Keep existing archived or historical orchestration handoffs readable and indexable during migration. +## Exclusion Rule -## Legacy Shape - -Historical orchestration handoffs may contain narrative sections such as current objective, decisions made, implementation scope, risks, open questions, recommended next action, and related working artifacts. These sections are not an active creation template. +- Do not load this file as artifact-authoring authority. +- Do not add a reader, indexer, migration route, alias, sidecar, or fallback for retired orchestration handoffs. +- Do not require retired handoffs for execution, review, continuation, or archive readiness. diff --git a/references/assets/orchestration/contract/implementation-review-v1.schema.json b/references/assets/orchestration/contract/implementation-review-v1.schema.json new file mode 100644 index 0000000..91e0bbf --- /dev/null +++ b/references/assets/orchestration/contract/implementation-review-v1.schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "implementation-review-v1", + "type": "object", + "required": ["artifact_type", "schema_version", "id", "plan_id", "task_id", "scope", "specification_id", "plan_identity", "lightweight_plan_sha256", "target", "target_sha256", "implementor_agent_id", "reviewer", "reviewed_obligations", "focused_observations", "verdict", "findings", "date_created", "last_updated"], + "properties": { + "artifact_type": {"const": "implementation-review"}, "schema_version": {"const": 1}, + "id": {"type": "string", "pattern": "^review-[a-z0-9][a-z0-9-]*$"}, + "plan_id": {"type": "string", "pattern": "^plan-[a-z0-9][a-z0-9-]*$"}, "task_id": {"type": ["string", "null"]}, + "scope": {"enum": ["task", "integrated"]}, "specification_id": {"type": "string", "minLength": 1}, + "plan_identity": {"$ref": "#/$defs/idref"}, "lightweight_plan_sha256": {"$ref": "#/$defs/sha"}, + "target": {"type": "object", "required": ["kind", "sha256", "base_commit", "manifest"], "properties": {"kind": {"enum": ["commit", "worktree"]}, "sha256": {"$ref": "#/$defs/sha"}, "base_commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, "manifest": {"type": "array", "items": {"type": "object", "required": ["path", "sha256"], "properties": {"path": {"type": "string", "minLength": 1}, "sha256": {"$ref": "#/$defs/sha"}}, "additionalProperties": false}}}, "additionalProperties": false}, + "target_sha256": {"$ref": "#/$defs/sha"}, + "implementor_agent_id": {"type": "string", "minLength": 1}, + "reviewer": {"type": "object", "required": ["agent_id"], "properties": {"agent_id": {"type": "string", "minLength": 1}}, "additionalProperties": false}, + "reviewed_obligations": {"type": "array", "minItems": 1, "items": {"type": "object", "required": ["source_id", "status", "summary"], "properties": {"source_id": {"type": "string", "minLength": 1}, "status": {"enum": ["satisfied", "missing", "blocked"]}, "summary": {"type": "string", "minLength": 1}}, "additionalProperties": false}}, + "focused_observations": {"type": "array", "items": {"$ref": "#/$defs/observation"}}, + "verdict": {"enum": ["accept", "repair", "blocked"]}, + "findings": {"type": "array", "items": {"type": "object", "required": ["source_id", "boundary", "summary"], "properties": {"source_id": {"type": "string", "minLength": 1}, "boundary": {"type": "string", "minLength": 1}, "summary": {"type": "string", "minLength": 1}}, "additionalProperties": false}}, + "date_created": {"type": "string", "format": "date"}, "last_updated": {"type": "string", "format": "date"} + }, + "$defs": {"sha": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, "idref": {"type": "object", "required": ["id", "sha256"], "properties": {"id": {"type": "string", "minLength": 1}, "sha256": {"$ref": "#/$defs/sha"}}, "additionalProperties": false}, "observation": {"type": "object", "required": ["id", "result", "summary"], "properties": {"id": {"type": "string", "minLength": 1}, "result": {"enum": ["passed", "failed", "blocked", "not-run"]}, "summary": {"type": "string", "minLength": 1}}, "additionalProperties": false}}, + "additionalProperties": false +} diff --git a/references/assets/orchestration/contract/implementation-review-v2.schema.json b/references/assets/orchestration/contract/implementation-review-v2.schema.json new file mode 100644 index 0000000..bd46c0f --- /dev/null +++ b/references/assets/orchestration/contract/implementation-review-v2.schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "implementation-review-v2", + "type": "object", + "required": ["artifact_type", "schema_version", "id", "plan_id", "task_id", "scope", "specification_id", "plan_identity", "target", "target_sha256", "implementor_agent_id", "reviewer", "reviewed_obligations", "focused_observations", "verdict", "findings", "date_created", "last_updated"], + "properties": { + "artifact_type": {"const": "implementation-review"}, "schema_version": {"const": 2}, + "id": {"type": "string", "pattern": "^review-[a-z0-9][a-z0-9-]*$"}, + "plan_id": {"type": "string", "pattern": "^plan-[a-z0-9][a-z0-9-]*$"}, "task_id": {"type": ["string", "null"]}, + "scope": {"enum": ["task", "integrated"]}, "specification_id": {"type": "string", "minLength": 1}, + "plan_identity": {"$ref": "#/$defs/idref"}, + "target": {"type": "object", "required": ["kind", "sha256", "base_commit", "manifest"], "properties": {"kind": {"enum": ["commit", "worktree"]}, "sha256": {"$ref": "#/$defs/sha"}, "base_commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, "manifest": {"type": "array", "items": {"oneOf": [{"type": "object", "required": ["path", "state", "sha256"], "properties": {"path": {"type": "string", "minLength": 1}, "state": {"const": "present"}, "sha256": {"$ref": "#/$defs/sha"}}, "additionalProperties": false}, {"type": "object", "required": ["path", "state"], "properties": {"path": {"type": "string", "minLength": 1}, "state": {"const": "deleted"}}, "additionalProperties": false}]}}}, "additionalProperties": false}, + "target_sha256": {"$ref": "#/$defs/sha"}, + "implementor_agent_id": {"type": "string", "minLength": 1}, + "reviewer": {"type": "object", "required": ["agent_id"], "properties": {"agent_id": {"type": "string", "minLength": 1}}, "additionalProperties": false}, + "reviewed_obligations": {"type": "array", "minItems": 1, "items": {"type": "object", "required": ["source_id", "status", "summary"], "properties": {"source_id": {"type": "string", "minLength": 1}, "status": {"enum": ["satisfied", "missing", "blocked"]}, "summary": {"type": "string", "minLength": 1}}, "additionalProperties": false}}, + "focused_observations": {"type": "array", "items": {"$ref": "#/$defs/observation"}}, + "verdict": {"enum": ["accept", "repair", "blocked"]}, + "findings": {"type": "array", "items": {"type": "object", "required": ["source_id", "boundary", "summary"], "properties": {"source_id": {"type": "string", "minLength": 1}, "boundary": {"type": "string", "minLength": 1}, "summary": {"type": "string", "minLength": 1}}, "additionalProperties": false}}, + "date_created": {"type": "string", "format": "date"}, "last_updated": {"type": "string", "format": "date"} + }, + "$defs": {"sha": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, "idref": {"type": "object", "required": ["id", "sha256"], "properties": {"id": {"type": "string", "minLength": 1}, "sha256": {"$ref": "#/$defs/sha"}}, "additionalProperties": false}, "observation": {"type": "object", "required": ["id", "result", "summary"], "properties": {"id": {"type": "string", "minLength": 1}, "result": {"enum": ["passed", "failed", "blocked", "not-run"]}, "summary": {"type": "string", "minLength": 1}}, "additionalProperties": false}}, + "additionalProperties": false +} diff --git a/references/assets/orchestration/contract/phase-v1.md b/references/assets/orchestration/contract/phase-v1.md index 71f464f..2750a07 100644 --- a/references/assets/orchestration/contract/phase-v1.md +++ b/references/assets/orchestration/contract/phase-v1.md @@ -1,138 +1,9 @@ ---- -id: phase-001 -plan_id: plan-YYYYMMDD-001 -name: [Phase Name] -goal: [Concrete measurable phase goal] -status: Planned -order: 1 -date_created: YYYY-MM-DD -last_updated: YYYY-MM-DD -owner: [team/individual/agent] -depends_on: [] -parallelizable: true -path: .work-bundle/orchestration/plan/active/[plan-id]/phase-001-[slug].md -source_spec: - - .work-bundle/orchestration/spec/active/... -source_knowledge: - - carried by source specification -task_index: - - id: task-001 - name: [Task Name] - path: .work-bundle/orchestration/plan/active/[plan-id]/phase-001-[slug]/task-001-[slug].md - status: Planned - depends_on: [] -allocated_rules: - - id: [rule-id] - source: AGENTS.md|work-bundle-toolkit|work-bundle-global|work-bundle-project|builtin|plugin|other - path: [file path when file-backed, otherwise source label] - applies_when: [observable phase condition] - load_timing: before_task_work|before_rule_edit|before_script_edit|before_validation - enforcement: must|should -allocated_skills: - - name: [skill-name] - source: work-bundle|agents-skills|codex-skills|builtin|plugin|other - path: [file path when file-backed, otherwise source label] - applies_when: [observable phase condition] - use_timing: task_execution|phase_validation - required_for: [why child executors need this skill context] -completion_criteria: - - [measurable completion criterion] ---- +# Phase v1 semantic contract -# Phase 001: [Phase Name] +The current `phase` family is schema-owned YAML. Supply semantic YAML to `write-phase`; the store injects `artifact_type`, `schema_version`, `id`, `plan_id`, `name`, `status: planned`, `date_created`, and `last_updated` and writes the canonical `.phase.yaml` path. -![Status: Planned](https://img.shields.io/badge/status-Planned-blue) +Semantic input requires `order`, `source_ids`, `depends_on`, `task_index`, `barriers`, `validation`, `completion_criteria`, `allocated_rules`, and `allocated_skills`. -## Introduction +Create a phase only for an actual barrier or convergence boundary. An empty `barriers` array states that no real barrier exists and supports the explicit default phase. Do not optimize task or phase cardinality or introduce speculative splits. Preserve expected total orchestration cost, authoritative production path ownership, one production owner per path, a coherent mechanical increment, and bounded repair frontier. If execution proves work materially under-decomposed, reslice only the affected region rather than repeatedly enlarge a task. -[Short, concrete explanation of what this phase does and what exact outcome it must produce.] - -## 1. Requirements & Constraints - -List only the source-spec IDs this phase implements or validates. Do not paste full specification prose here. - -- **SPEC-REQ-001**: `REQ-001` — [one-line execution impact for this phase.] -- **SPEC-AC-001**: `AC-001` — [one-line validation impact for this phase.] -- **SPEC-CON-001**: `CON-001` — [one-line constraint impact for this phase.] -- **SPEC-OQ-001**: `OQ-001` — [one-line decision impact for this phase.] - -## 2. Dependencies - -### 2.1 Alternative Dependencies - -| Alternative | Required Decision | Must Be Determined Before Task | If Unresolved | -|---|---|---|---| -| ALT-001 | [accept/reject decision] | task-001 | stop execution|use declared assumption|skip affected task | - -### 2.2 Open Question Dependencies - -| Open Question | Required Resolution | Must Be Resolved Before Task | If Unresolved | -|---|---|---|---| -| OQ-001 | [specific answer required] | task-001 | stop execution|use declared assumption|skip affected task | - -### 2.3 File Dependencies - -| Required File | Must Exist Before Task | Validation Method | -|---|---|---| -| `[exact file path]` | task-001 | [How to confirm the file exists and is usable.] | - -### 2.4 Task Dependencies - -| Task | Depends On | Dependency Type | Reason | -|---|---|---|---| -| task-002 | task-001 | output|decision|file|test | [Why this dependency exists.] | - -### 2.5 Barrier Participants - -Include this table when the phase contains contract-decoupled parallel tasks. - -| Barrier | Contract Group | Participants | Readiness Criteria | Release Condition | Convergence Task | -|---|---|---|---|---|---| -| BAR-001 | CG-001 | task-002, task-003 | each participant completes or blocks with executor-result handoff | all participants reached barrier | task-004 | - -Participants validate against the common contract group, accepted prior handoffs, and their task-local files. They must not validate against sibling in-progress files or classify sibling work as stale before the convergence task. - -## 3. Task Map - -Resolve alternatives and open questions as leading tasks before implementation tasks. - -| Task | Name | Path | Status | Depends On | Parallelizable | Task Type | -|---|---|---|---|---|---|---| -| task-001 | Resolve Alternative ALT-001 | `.work-bundle/orchestration/plan/active/[plan-id]/phase-001-[slug]/task-001-resolve-alt-001.md` | Planned | - | false | decision | -| task-002 | [Implementation Task] | `.work-bundle/orchestration/plan/active/[plan-id]/phase-001-[slug]/task-002-[slug].md` | Planned | task-001 | true | implementation | - -## 4. Tests - -| ID | Test Type | Target | Related Task | Command | Expected Result | -|---|---|---|---|---|---| -| TEST-001 | unit|integration|model-behavior|manual | `[file/module/function/API]` | task-002 | `[command if applicable]` | [Measurable result.] | - -## 5. Generated Artifact Verification - -Record phase-level verification against the source specification and root plan. - -| ID | Check | Result | Repair | -|---|---|---|---| -| VERIFY-001 | Phase requirements cite the relevant source-spec IDs and do not duplicate long specification prose. | passed|repaired|blocked | [Same-turn repair or source-spec repair blocker.] | -| VERIFY-002 | Task map paths, dependencies, ordering, and safe parallelization flags match exact task write scopes and root-plan sequencing. | passed|repaired|blocked | [Same-turn repair or source-spec repair blocker.] | -| VERIFY-003 | Phase tests, completion criteria, and compact phase-scoped `executor-result` handoff requirement are present and consistent with child tasks. | passed|repaired|blocked | [Same-turn repair or source-spec repair blocker.] | -| VERIFY-004 | Phase `allocated_rules` and `allocated_skills` cover phase-wide signals and are carried into child tasks where executors need them. | passed|repaired|blocked | [Same-turn repair or source-spec repair blocker.] | -| VERIFY-005 | Barrier participant maps, post-barrier convergence dependencies, and contract-only validation boundaries are present when parallel tasks share a contract. | passed|repaired|blocked | [Same-turn repair or source-spec repair blocker.] | - -Repair generated phase or child-task drift, missing spec-ID alignment, dependency mistakes, unsafe parallelization, validation gaps, allocation gaps, and handoff gaps in the same planning turn. Stop for specification repair when the source spec cannot support a deterministic phase. - -Phase ordering must preserve Truth Basis continuity. An ordinary proof task that falsifies a consequential assumption precedes dependent broad edits without becoming a new checkpoint lifecycle. - -## 6. Completion Criteria - -- **DONE-REQ-001**: [Requirement validation result and evidence.] -- **DONE-CON-001**: [Constraint validation result and evidence.] -- **DONE-TEST-001**: [Test result summary.] -- **DONE-ACH-001**: [Phase achievement summary.] -- **DONE-BARRIER-001**: [Barrier readiness and convergence result when applicable.] -- **DONE-VERIFY-001**: Phase and child task artifacts were verified against source-spec IDs, dependencies, safe parallelization, validation, and handoff requirements before completion. -- **DONE-HANDOFF-001**: Executor invokes `create-handoff` and creates a compact phase-scoped `executor-result` handoff under `.work-bundle/orchestration/handoff/executor/active/` before reporting this phase as completed or blocked. - -## 7. Executor Handoff Requirements - -The executor must invoke `create-handoff` at the end of this phase and create a sparse YAML `executor-result` handoff. Include only applicable continuation and review evidence: completed tasks, changed or inspected files, symbols when useful, validation commands and results, unresolved blockers, phase-fit or task-fit evidence, repository/preflight evidence, compact CodeGraph evidence when source-code work was in scope, delegation_evidence when ownership was delegated, and Knowledge Base Update disposition when review must carry it forward. Omit empty sections, deviation narratives, durable-knowledge advice, next-action recommendations, and other executor advice fields. +Task ordering and dependencies must agree with `task_index`. Contract-decoupled parallel work names participants, readiness evidence, release conditions, forbidden sibling validation, and a post-barrier convergence owner. Validation and completion criteria remain semantic agent decisions; schema validation only proves declared shape and bindings. diff --git a/references/assets/orchestration/contract/phase-v1.schema.json b/references/assets/orchestration/contract/phase-v1.schema.json new file mode 100644 index 0000000..106c167 --- /dev/null +++ b/references/assets/orchestration/contract/phase-v1.schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "phase-v1", + "type": "object", + "required": ["artifact_type", "schema_version", "id", "plan_id", "name", "status", "order", "date_created", "last_updated", "source_ids", "depends_on", "task_index", "barriers", "validation", "completion_criteria", "allocated_rules", "allocated_skills"], + "properties": { + "artifact_type": {"const": "phase"}, + "schema_version": {"const": 1}, + "id": {"type": "string", "pattern": "^phase-[a-z0-9][a-z0-9-]*$"}, + "plan_id": {"type": "string", "pattern": "^plan-[a-z0-9][a-z0-9-]*$"}, + "name": {"type": "string", "minLength": 1}, + "status": {"const": "planned"}, + "order": {"type": "integer", "minimum": 1}, + "date_created": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"}, + "last_updated": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"}, + "source_ids": {"type": "array", "minItems": 1, "items": {"type": "string", "pattern": "^[A-Z][A-Z0-9_-]*-[0-9]+[A-Z]?$"}, "uniqueItems": true}, + "depends_on": {"type": "array", "items": {"type": "string", "pattern": "^phase-[a-z0-9][a-z0-9-]*$"}, "uniqueItems": true}, + "task_index": {"type": "array", "minItems": 1, "items": {"type": "object", "required": ["id", "order"], "properties": {"id": {"type": "string", "pattern": "^task-[a-z0-9][a-z0-9-]*$"}, "order": {"type": "integer", "minimum": 1}}, "additionalProperties": true}}, + "barriers": {"type": "array", "items": {"type": "object"}}, + "validation": {"type": "array", "minItems": 1, "items": {"type": "object"}}, + "completion_criteria": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}, + "allocated_rules": {"type": "array"}, + "allocated_skills": {"type": "array"} + }, + "additionalProperties": false +} diff --git a/references/assets/orchestration/contract/plan-v1.md b/references/assets/orchestration/contract/plan-v1.md index c748972..707ffb4 100644 --- a/references/assets/orchestration/contract/plan-v1.md +++ b/references/assets/orchestration/contract/plan-v1.md @@ -1,173 +1,15 @@ ---- -id: plan-YYYYMMDD-001 -goal: [Concise title describing the implementation plan goal] -purpose: [upgrade|refactor|feature|data|infrastructure|process|architecture|design] -component: [target component/module/system] -version: 1 -date_created: YYYY-MM-DD -last_updated: YYYY-MM-DD -owner: [team/individual/agent] -status: Planned -tags: - - [feature|upgrade|chore|architecture|migration|bug|data|process] -source_spec: - - .work-bundle/orchestration/spec/active/... -source_knowledge: - - carried by source specification -phase_index: - - id: phase-001 - name: [Phase Name] - path: .work-bundle/orchestration/plan/active/[plan-id]/phase-001-[slug].md - status: Planned - depends_on: [] - parallelizable: true -allocated_rules: - - id: [rule-id] - source: AGENTS.md|work-bundle-toolkit|work-bundle-global|work-bundle-project|builtin|plugin|other - path: [file path when file-backed, otherwise source label] - applies_when: [observable plan-wide condition] - load_timing: before_planning|before_task_work|before_validation - enforcement: must|should -allocated_skills: - - name: [skill-name] - source: work-bundle|agents-skills|codex-skills|builtin|plugin|other - path: [file path when file-backed, otherwise source label] - applies_when: [observable plan-wide condition] - use_timing: planning|task_execution|review - required_for: [why executors need this skill context] -evidence_capability: - result: mapped | no_validation_bearing_obligation - reason: [non-empty reason] - invariants: [stable per-invariant capability entries allocated to task IDs with closure_result initialized to pending] ---- +# Root Plan v1 semantic contract -# Implementation Plan: [Plan Goal] +The current `root-plan` family is schema-owned YAML. Supply semantic YAML to `write-plan`; do not add front matter, choose a filename, or repeat injected `artifact_type`, `schema_version`, `id`, `goal`, `purpose`, `component`, `version`, `source_spec_id`, `status`, `date_created`, or `last_updated` fields. -![Status: Planned](https://img.shields.io/badge/status-Planned-blue) +The semantic input requires `source_coverage`, `authority`, `strategy`, `phase_index`, `dependency_graph`, `risks`, `validation_strategy`, `completion_criteria`, `knowledge_base_update`, `semantic_loop`, and `execution_workspace`. Each coverage row names a stable source ID, obligation kind, non-empty phase/task ownership, and validation IDs when validation-bearing. Cite the verified specification by `source_spec_id`; presentation paths such as `.work-bundle/orchestration/spec/active/...` are not authority. -## Introduction +Decompose without optimizing task or phase cardinality. Bound expected total orchestration cost at concrete independently owned production, dependency, validation, review, and repair seams. Every authoritative production path needs a production owner; reject helper-only allocation. Keep a coherent mechanical increment with one owner, oracle, and repair frontier together. Split independently owned entry points only when current repository evidence proves distinct seams. Create a phase only for an actual barrier or convergence boundary; reject speculative splits. When a task is materially under-decomposed, return to the plan and reslice only the affected region; do not repeatedly enlarge it. -[Short, concrete introduction explaining what this plan implements, which specification it derives from, and the intended outcome.] +Assign parallel tasks only when dependencies are satisfied and write scopes are disjoint. Unsafe parallelization is explicitly blocked by dependency or scope evidence. Contract-decoupled work names a common contract group, barrier participants, readiness, release condition, convergence owner, and post-barrier convergence task. -## 1. Requirements & Constraints +Every task declares `evidence_capability`. Use `mapped` with a lightest-capable task-local oracle for validation-bearing obligations, or `no_validation_bearing_obligation` with a concrete reason. -Use a compact source-spec ID map. Do not paste long specification sections into the plan. +Run canonical static task admission before semantic review. Structural success cannot establish source-ID coverage, appropriate decomposition, capable validation, or executability. A distinct reviewer compares the stored tree directly with the verified specification and current source evidence and issues `accept`, `repair`, or `blocked`. -- **SPEC-REQ-001**: `REQ-001` — [one-line execution impact.] -- **SPEC-AC-001**: `AC-001` — [one-line acceptance impact.] -- **SPEC-CON-001**: `CON-001` — [one-line constraint impact.] -- **SPEC-OQ-001**: `OQ-001` — [one-line decision/blocker impact.] -- **PLAN-REQ-001**: [Plan-only execution requirement, if needed.] - -## 2. Source Specification & Knowledge - -| ID | Type | Path | Required Application | -|---|---|---|---| -| SRC-001 | specification | `.work-bundle/orchestration/spec/active/...` | [How the spec constrains this plan.] | -| CTX-001 | carried-context | `source specification` | [Accepted project knowledge is already carried in the spec; do not require plan executors to read `.work-bundle/knowledge/`.] | - -## 2.1 Knowledge Base Update Carry Forward - -- **Disposition**: required|not-needed|completed|blocked -- **Closure return**: missing|completed|not-needed|blocked -- **Source**: [Source specification Knowledge Base Update section or review decision.] -- **Review Gate**: [How review should resolve or validate the disposition before archive.] - -`Closure return` starts as `missing`. Final review updates it only from validated keep-summarizing return evidence. `archive-plan` aggregates accepted executor-result dispositions whose `related.plan` (or `related_plan`) unambiguously equals the current plan and fails `knowledge-blocked` when an accepted `update`, `supersede`, or `reclassify` requires closure but this return remains `missing` or `blocked`; rejected dispositions, accepted `none`, other-plan handoffs, and task-only handoffs do not trigger promotion. - -## 3. Phase Map - -| Phase | Name | Path | Status | Depends On | Parallelizable | Completion Gate | -|---|---|---|---|---|---|---| -| phase-001 | [Phase Name] | `.work-bundle/orchestration/plan/active/[plan-id]/phase-001-[slug].md` | Planned | - | true | [Measurable completion gate.] | - -## 3.1 Compactness Check - -Plans do not optimize task or phase cardinality. Bound expected total orchestration cost by decomposing only at concrete independently owned production, dependency, validation, review, and repair seams while preserving complete requirement coverage and Truth Basis continuity. Assign every authoritative production path to a production owner; helper-only allocation cannot leave its production path unowned. Keep a coherent mechanical increment with one owner, oracle, and repair frontier together. Create a phase only for an actual barrier or convergence boundary, and reject speculative splits unsupported by current authority, repository, dependency, ownership, validation, or acceptance evidence. - -When execution proves a task materially under-decomposed, return to the plan and reslice only the affected region around the evidenced seam. Preserve the original binding, baseline, and accepted unaffected regions; do not repeatedly enlarge the task. - -Plan review identity uses the canonical semantic plan projection shared by all lifecycle consumers; status-only or append-only evidence changes do not request review or reslicing, while authority, scope, dependency, acceptance, decomposition, or validation-allocation changes do. Before acceptance, canonical static task admission compiles every task and rejects missing dependencies, inconsistent authority, unsafe scope, and source-local execution artifacts. - -Every executable task declares the same five-field Truth Basis. When a consequential simplification or compatibility assumption exists, make the earliest ordinary task cheaply falsify it before broad edits. Do not add a risk score, checkpoint phase, or parallel lifecycle. - -Planning remains outside the bounded post-execution counter: plan and specification revisions do not consume post-execution review rounds. A task that reconciles an existing post-execution flow preserves its stable flow identity and declares the existing blocker/finalization context; shared admission, not plan wording, decides whether reconciliation may proceed. - -## 4. Desired Files - -| ID | File Type | Path | Purpose | Operation | Related Phase | -|---|---|---|---|---|---| -| FILE-001 | [api|data-model|domain-model|unit-test|page|documentation|other] | `[exact path]` | [Why this file is needed.] | create/update/delete/read | phase-001 | - -## 5. Alternatives - -- **ALT-001**: [Alternative approach.] - - **Status**: pending|accepted|rejected - - **Decision Required Before**: [phase-id/task-id] - - **Accepted When**: [Deterministic acceptance condition.] - - **Rejected Because**: [Concrete rejection reason, if rejected.] - -## 6. Open Questions - -- **OQ-001**: [Question blocking executable implementation.] - - **Required Decision**: [Specific decision needed.] - - **Must Be Resolved Before**: [phase-id/task-id] - - **Fallback If Unresolved**: stop execution|use declared assumption|skip affected task - -## 7. Tests - -| ID | Test Type | Target | Related Phase | Can Run With | Command | Expected Result | -|---|---|---|---|---|---|---| -| TEST-001 | unit|integration|model-behavior|manual | `[file/module/function/API]` | phase-001 | - | `[command if applicable]` | [Measurable result.] | - -Harness-executed integration commands run against the final accepted plan workspace after ordinary task integration and must be Git-observable-state-neutral. Do not declare a plan-level `files.write` envelope. - -When post-execution integrated review applies, record controller commands as lifecycle operations rather than plan validation rows: `begin-review-round` before review preparation, `complete-review-round` from stored review or factual audit-block evidence, `review-round-status` for diagnosis, and `finalize-with-blockers` only for unresolved bounded closure. Task review and artifact revision are not review rounds. - -## 7.1 Contract Groups, Barriers, And Convergence - -Use this section when parallel tasks share a stable common contract. - -| ID | Common Contract | Establishing Task | Participants | Barrier | Convergence Owner | -|---|---|---|---|---|---| -| CG-001 | `[contract artifact paths]` | task-001 | task-002, task-003 | BAR-001 releases after participants complete or block with handoffs | task-004 | - -Required rules: - -- Parallel participants depend on the common contract group and accepted prior handoffs, not sibling in-progress implementation. -- The barrier identifies participant tasks, readiness criteria, release condition, and post-barrier validation owner. -- Joint debug, integration tests, cross-branch behavior checks, and stale-peer classification belong to the convergence owner after barrier release. -- Contract groups and barriers must not bypass repository preflight, dependency checks, disjoint write-scope checks, validation, or handoff creation. - -## 8. Generated Artifact Verification - -Record the verification pass performed after generating the root plan, phases, and tasks. - -How to make tasks parallel: create or confirm a stable boundary artifact before branching work, then assign parallel tasks only when dependencies are satisfied and write scopes are disjoint. Use concrete plan evidence such as API contracts, port interfaces, repository contracts, DTO/schema contracts, event schemas, facades, command contracts, pipeline stage contracts, state tables, rule matrices, branch-by-abstraction, or expand-and-contract boundaries. Keep pattern rationale in the planning artifact; generated executor tasks should receive exact objectives, input/output artifacts, allowed and forbidden files, validation, convergence checks, and integration dependencies. - -| ID | Check | Scope | Result | Repair | -|---|---|---|---|---| -| VERIFY-001 | source-spec ID coverage maps every implemented requirement, constraint, resolved alternative, and resolved open question to plan/phase/task artifacts. | plan/phase/task | passed|repaired|blocked | [Same-turn repair or source-spec repair blocker.] | -| VERIFY-002 | Exact artifact paths, source files, target files, dependencies, task ordering, validation commands, and completion criteria are internally consistent. | plan/phase/task | passed|repaired|blocked | [Same-turn repair or source-spec repair blocker.] | -| VERIFY-003 | Safe parallelization is exposed where dependencies and write scopes allow, and unsafe parallelization is explicitly blocked by dependency or scope evidence. | plan/phase/task | passed|repaired|blocked | [Same-turn repair or source-spec repair blocker.] | -| VERIFY-004 | Every task, phase, and plan completion path requires `create-handoff` with a compact, sparse YAML `executor-result` handoff whose body stays applicability-based. | plan/phase/task | passed|repaired|blocked | [Same-turn repair or source-spec repair blocker.] | -| VERIFY-005 | `allocated_rules` and `allocated_skills` cover all material rule/skill conditions from the source specification, affected files, operation type, CodeGraph/Git needs, validation tasks, and any non-WorkBundle rule/skill sources already visible to the agent. | plan/phase/task | passed|repaired|blocked | [Same-turn repair or source-spec repair blocker.] | -| VERIFY-006 | Review-stable decomposition, production-path ownership, repair-frontier locality, contract group clarity, actual barrier correctness, co-worker isolation, convergence validation, and contract-only handoff criteria are present where applicable. | plan/phase/task | passed|repaired|blocked | [Same-turn repair or source-spec repair blocker.] | -| VERIFY-007 | Each task carries allocated Truth Basis authority and the earliest ordinary task falsifies consequential assumptions before broad simplification. | plan/phase/task | passed|repaired|blocked | [Same-turn repair or source-spec repair blocker.] | - -If any generated artifact drifts from the source specification, omits required spec-ID coverage, contains inconsistent paths or dependencies, lacks validation, lacks allocated rule/skill coverage, or lacks handoff criteria, repair the generated artifacts in the same planning turn and repeat this verification. If the source specification itself has unresolved questions, missing stable IDs, missing evidence, or contradictory instructions, stop for specification repair instead of inventing plan content. - -## 9. Completion Criteria - -- **DONE-REQ-001**: All requirements are validated or explicitly marked not applicable with reason. -- **DONE-CON-001**: All constraints are validated with concrete evidence. -- **DONE-TEST-001**: Required tests pass or have documented failure reason and remediation task. -- **DONE-ACH-001**: The implementation achieves the stated plan goal. -- **DONE-FILE-001**: Desired files are created, updated, deleted, or confirmed unnecessary. -- **DONE-VERIFY-001**: Generated root plan, phase, and task artifacts were verified against the source specification and repaired for drift, gaps, dependencies, validations, safe parallelization, and handoff requirements before completion. -- **DONE-HANDOFF-001**: Executor invokes `create-handoff` and creates a compact plan-scoped `executor-result` handoff under `.work-bundle/orchestration/handoff/executor/active/` before reporting the root plan as completed or blocked. The handoff carries only applicable continuation and review evidence, including Knowledge Base Update disposition when review must resolve it. - -## 10. Related Specifications / Further Reading - -- Related specification: `.work-bundle/orchestration/spec/active/...` -- Carried durable-knowledge context, if any: source specification front matter +The canonical semantic plan projection is `canonical-yaml-plan-tree-v1`. A status-only or append-only evidence change declared by the schema is excluded at the exact top-level control locations; all other semantics remain identity-bearing. Current plan and specification revisions do not consume post-execution review rounds. diff --git a/references/assets/orchestration/contract/plan-v1.schema.json b/references/assets/orchestration/contract/plan-v1.schema.json new file mode 100644 index 0000000..dbbd2ad --- /dev/null +++ b/references/assets/orchestration/contract/plan-v1.schema.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "plan-v1", + "type": "object", + "required": ["artifact_type", "schema_version", "id", "goal", "purpose", "component", "version", "source_spec_id", "status", "date_created", "last_updated", "source_coverage", "authority", "strategy", "phase_index", "dependency_graph", "risks", "validation_strategy", "completion_criteria", "knowledge_base_update", "semantic_loop", "execution_workspace"], + "properties": { + "artifact_type": {"const": "root-plan"}, + "schema_version": {"const": 1}, + "id": {"type": "string", "pattern": "^plan-[a-z0-9][a-z0-9-]*$"}, + "goal": {"type": "string", "minLength": 1}, + "purpose": {"type": "string", "minLength": 1}, + "component": {"type": "string", "minLength": 1}, + "version": {"type": "string", "minLength": 1}, + "source_spec_id": {"type": "string", "pattern": "^spec-[a-z0-9][a-z0-9-]*$"}, + "status": {"enum": ["draft", "verified", "superseded"]}, + "date_created": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"}, + "last_updated": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"}, + "source_coverage": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/coverage"}}, + "authority": {"type": "object", "minProperties": 1}, + "strategy": {"type": "object", "minProperties": 1}, + "phase_index": {"type": "array", "minItems": 1, "items": {"type": "object", "required": ["id", "order"], "properties": {"id": {"type": "string", "pattern": "^phase-[a-z0-9][a-z0-9-]*$"}, "order": {"type": "integer", "minimum": 1}}, "additionalProperties": true}}, + "dependency_graph": {"type": "object"}, + "risks": {"type": "array"}, + "validation_strategy": {"type": "array", "minItems": 1, "items": {"type": "object", "required": ["id", "kind"], "properties": {"id": {"type": "string", "minLength": 1}, "kind": {"type": "string", "minLength": 1}}, "additionalProperties": true}}, + "completion_criteria": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}, + "knowledge_base_update": {"type": "object", "minProperties": 1}, + "semantic_loop": {"type": "object", "minProperties": 1}, + "execution_workspace": { + "type": "object", + "required": ["isolation", "profile", "cleanup"], + "properties": { + "isolation": {"enum": ["required", "preferred", "existing"]}, + "profile": {"type": "string", "minLength": 1}, + "cleanup": {"enum": ["after_integration", "manual"]} + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "$defs": { + "coverage": { + "type": "object", + "required": ["source_id", "obligation_kind"], + "properties": { + "source_id": {"type": "string", "pattern": "^[A-Z][A-Z0-9_-]*-[0-9]+[A-Z]?$"}, + "obligation_kind": {"type": "string", "minLength": 1}, + "phase_ids": {"type": "array", "minItems": 1, "items": {"type": "string", "pattern": "^phase-[a-z0-9][a-z0-9-]*$"}, "uniqueItems": true}, + "task_ids": {"type": "array", "minItems": 1, "items": {"type": "string", "pattern": "^task-[a-z0-9][a-z0-9-]*$"}, "uniqueItems": true}, + "validation_ids": {"type": "array", "items": {"type": "string", "minLength": 1}, "uniqueItems": true} + }, + "anyOf": [{"required": ["phase_ids"]}, {"required": ["task_ids"]}], + "additionalProperties": false + } + } +} diff --git a/references/assets/orchestration/contract/specification-v1.md b/references/assets/orchestration/contract/specification-v1.md index 27ccaf3..0be02cb 100644 --- a/references/assets/orchestration/contract/specification-v1.md +++ b/references/assets/orchestration/contract/specification-v1.md @@ -1,10 +1,14 @@ --- +artifact_type: specification +schema_version: 1 id: spec-YYYYMMDD-001 title: [Concise Title Describing the Specification's Focus] project: status: draft date_created: [YYYY-MM-DD] last_updated: [Optional: YYYY-MM-DD] +purpose: [Concise implementation purpose] +component: [Owning component or bounded surface] source_knowledge: - path: .work-bundle/knowledge/notes/... constraint: [task-relevant accepted decision or constraint] @@ -17,6 +21,8 @@ execution_workspace: cleanup: after_integration|manual --- +The immutable `specification-v1` schema owns this structural front matter. The agent supplies the human-readable semantic body and semantic metadata, while `scripts/orch.py write-spec` owns family, schema version, identity, qualification, timestamps, canonical `.work-bundle/orchestration/spec/{state}/{id}.spec.md` location, atomic write, lifecycle movement, and index projection. Caller-authored structural overrides and filenames are invalid. + The front-matter `source_knowledge` contains accepted authority only, as established by bounded retrieval and Source Context reconciliation. Each accepted entry carries a provenance `path` and the already-reconciled task-relevant `constraint`. Candidate, background, blocked, and superseded knowledge remains classified in Source Context or Open Questions and must not appear in this carried-authority list. Downstream planning allocates deterministic `AUTH-NNN` aliases by list order so executor packets remain traceable without exposing knowledge paths. The compiler resolves each allocated alias to `AUTH-NNN: ` in the task brief and review package. # Introduction @@ -47,7 +53,7 @@ After the initial shell exists and before broad repository evidence gathering, r Required evidence: - `.work-bundle/project.yaml` availability and `metadata_version`. -- Source repository `id`, path, Git capability, expected `working_branch`, actual branch, expected `last_commit_id`, actual HEAD commit, branch status, and commit/baseline status when Git-backed. +- Source repository `id`, device-bound project root, Git capability, portable default branch, device-observed branch/HEAD, live branch/HEAD, and accepted-baseline status when Git-backed. - Registry locator consistency when the bootstrap-resolved project registry is used. - CodeGraph support, `.codegraph/` marker presence, index status, synced commit when available, and `no-index` or `not-indexed` fallback when absent. @@ -263,7 +269,9 @@ Do not instruct specification authors or executors to write durable knowledge di - The specification remains self-contained and does not require broad repository exploration before the shell exists. - The source context records neutral cross-stage retrieval anchors or a retrieval gap, and any named retrieval policy is used only for classification/output grouping. - The specification carries accepted authority context forward so downstream planning and execution do not need to read `.work-bundle/knowledge/`. -- The specification records project metadata preflight evidence including `working_branch`, `last_commit_id`, branch status, baseline status, and CodeGraph no-index fallback when applicable. +- The specification records metadata-v4 portable topology, device-local branch/HEAD observations, live branch/HEAD, accepted-baseline status, and CodeGraph no-index fallback when applicable. +- A distinct reviewer directly compares the concrete specification with the user purpose, accepted authority, workspace evidence, requirements, constraints, interfaces, acceptance criteria, validation targets, material conflicts, open questions, and scope. +- Supporting evidence, tests, doctors, indexes, handoffs, receipts, and knowledge state provide evidence but do not issue the semantic verdict. - The specification does not encode artifact-version counting as post-execution review policy; any residual forced-closure specification preserves unresolved claims without reopening product work. - WorkBundle project specifications record related active defects and expected review closure when applicable. - Material non-authority or opposing evidence is visible without shaping requirements unless resolved by user decision or accepted authority. diff --git a/references/assets/orchestration/contract/specification-v1.schema.json b/references/assets/orchestration/contract/specification-v1.schema.json new file mode 100644 index 0000000..f59f9f8 --- /dev/null +++ b/references/assets/orchestration/contract/specification-v1.schema.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "specification-v1", + "type": "object", + "required": ["artifact_type", "schema_version", "id", "title", "project", "status", "date_created", "last_updated", "purpose", "component", "version", "source_knowledge", "related_handoffs", "tags", "execution_workspace"], + "properties": { + "artifact_type": {"const": "specification"}, + "schema_version": {"const": 1}, + "id": {"type": "string", "pattern": "^spec-[a-z0-9][a-z0-9-]*$"}, + "title": {"type": "string", "minLength": 1}, + "project": {"type": "string", "minLength": 1}, + "status": {"enum": ["draft", "verified", "superseded"]}, + "date_created": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"}, + "last_updated": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"}, + "purpose": {"type": "string", "minLength": 1}, + "component": {"type": "string", "minLength": 1}, + "version": {"type": ["string", "number"]}, + "source_knowledge": {"type": "array", "items": {"type": "object", "required": ["path", "constraint"], "properties": {"path": {"type": "string", "minLength": 1}, "constraint": {"type": "string", "minLength": 1}}, "additionalProperties": false}}, + "related_handoffs": {"type": "array", "items": {"type": "string", "minLength": 1}}, + "tags": {"type": "array", "items": {"type": "string", "minLength": 1}, "uniqueItems": true}, + "execution_workspace": {"type": "object", "required": ["isolation", "profile", "cleanup"], "properties": {"isolation": {"enum": ["required", "preferred", "existing"]}, "profile": {"type": "string", "minLength": 1}, "cleanup": {"enum": ["after_integration", "manual"]}}, "additionalProperties": false} + }, + "additionalProperties": false +} diff --git a/references/assets/orchestration/contract/task-v1.md b/references/assets/orchestration/contract/task-v1.md index 654fa12..f6f51ea 100644 --- a/references/assets/orchestration/contract/task-v1.md +++ b/references/assets/orchestration/contract/task-v1.md @@ -1,172 +1,58 @@ ---- -id: task-001 -plan_id: plan-YYYYMMDD-001 -phase_id: phase-001 -name: [Task Name] -status: Planned +# Task v1 semantic contract + +The current `task` family is schema-owned YAML. Supply semantic YAML to `write-task`; the store injects `artifact_type`, `schema_version`, `id`, `plan_id`, `phase_id`, `name`, `status: planned`, `date_created`, and `last_updated` and writes the canonical `.task.yaml` path. + +Semantic input requires: + +```yaml order: 1 -task_type: decision|implementation|test|documentation|handoff -date_created: YYYY-MM-DD -last_updated: YYYY-MM-DD -owner: [team/individual/agent] -depends_on: [] -source_ids: [REQ-001, AC-001] +task_type: implementation +source_ids: [REQ-001A, AC-001] truth_basis: - purpose: [one bounded intended outcome] - as_is_evidence: [[exact source, test, or harness evidence]] - decision_authority: [none-relevant | [AUTH-NNN aliases]] - expected_delta: [[observable post-change behavior]] - conflict_status: clear|escalate -source_files: - - [exact source file path] -target_files: - - [exact target file path] -target_symbols: - - [class/function/module/interface] -completion_criteria: - - [measurable criterion] -methodology: - primary: tdd|systematic-debugging|direct|loop-coding - required_skills: - - [skill-name] -executor_profile: - capability: mechanical|standard|judgment - context_mode: compiled-brief - review_capability: standard|judgment - escalation: - after_failed_repairs: 2 - next_capability: standard|judgment -acceptance_review: - required: false - reviewer_independent: false - verdict: pending - reviewed_head: "" - findings: [] -allocated_rules: - - id: [rule-id] - source: [authority source] - path: [file path when file-backed] - applies_when: [observable task condition] - load_timing: before_task_work|before_source_inspection|before_script_edit|before_rule_edit|before_validation - enforcement: must|should -allocated_skills: - - name: [skill-name] - source: [authority source] - path: [file path when file-backed] - applies_when: [observable task condition] - use_timing: before_task_work|task_execution|validation - required_for: [why required] + purpose: + as_is_evidence: [] + decision_authority: [none-relevant] + expected_delta: [] + conflict_status: clear +depends_on: [] +source_files: [path/to/source.py] +target_files: [path/to/source.py] +target_symbols: [module.symbol] +interfaces: {consumes: [API-001], produces: []} +steps: [] validation: - - kind: process - command: exact command - proves: [claim] - expected: passed - - kind: inspection - command: inspection identifier - mechanism: named-harness-owned-mechanism - proves: [claim] + - id: VAL-001 + kind: process + command: pytest -q path/to/test.py + proves: [AC-001] expected: passed + invariant_ids: [INV-001] + capability_reason: evidence_capability: - result: mapped | no_validation_bearing_obligation - reason: [non-empty reason] + result: mapped + reason: invariants: - - {id: INV-001, source_ids: [REQ-001, AC-001], invariant: string, boundary: unit | component | integration | runtime | ui_visual | performance | accessibility | inspection | other, other_mechanism: required-when-other, oracle: VAL-001, capability_reason: string, freshness: current_task_batch, task_id: task-001, evidence_ids: [VAL-001], closure_result: pending} ---- - -# TASK-001: [Task Name] - -## Goal - -[One bounded outcome.] - -## Truth Basis - -The front-matter `truth_basis` is mandatory and uses the same five fields as the lightweight path. `decision_authority` is semantically distinct from generic `source_ids`: it is exactly `[none-relevant]` when verified reconciliation found no applicable durable authority, or a non-empty list of `AUTH-NNN` aliases allocated in order from the verified specification's accepted `source_knowledge`. The compiler resolves each allocated alias to `AUTH-NNN: ` from that specification mapping and copies the same resolved values into the disposable task brief and review package. Aliases stay traceable without placing knowledge paths in executor packets. Arbitrary prose, generic requirement IDs, candidate/background/blocked authority, and superseded authority fail closed. The compiler returns the existing `decision-blocked` route when `conflict_status` is `escalate`. Executors do not retrieve durable knowledge to rebuild this authority. - -## Source references - -List stable source IDs and their task-local effect. Do not duplicate full specification prose. Source IDs are authoritative specification IDs only. Do not cite `EXC-*` excellence proposal IDs; rejected, deferred, and not-material proposals stay out of executor briefs. - -| ID | Source path | Task-local effect | -| --- | --- | --- | -| REQ-001 | `.work-bundle/orchestration/spec/active/example.md` | [effect] | - -## Dependencies and contracts - -| Dependency | Required state | Reason | -| --- | --- | --- | -| task-000 | Completed | [reason] | - -For contract-decoupled work, name the common contract group, accepted prior handoffs, barrier, allowed validation scope, forbidden sibling validation, and convergence owner. - -## Files and interfaces - -| Path or interface | Read/write | Required usage | -| --- | --- | --- | -| `path/to/file` | write | [exact change] | - -## Implementation - -1. [Concrete file or symbol action.] -2. [Concrete file or symbol action.] - -## Validation - -Structured front-matter `validation` is the sole canonical terminal authority for new and updated tasks. Each item must carry explicit `kind: process|inspection`. Missing YAML `kind` fails closed and is not defaulted to `process`. TEST-ID source records are not executable terminal validation. Inspections must name a deterministic harness-owned `mechanism`. `named-harness-file-digest` compares a task-owned 64-character `digest` to the current write-scope file digest and can fail. Preserve `proves`, `expected`, `acceptable_results`, and `expected: skip|skipped` semantics. Executor-authored `kind` cannot choose process versus inspection. - -Body `## Validation` is optional non-authoritative presentation and must not grant or block terminal authority. Prefer omitting it on new tasks. Do not add a YAML-versus-body equality gate, renderer, or synchronization machinery. - -Validation evidence reuse is owned by the existing WOR-105 source/evaluation and completion-provenance models. Declare deterministic eligibility explicitly: - -```yaml -evidence_reuse: - mode: deterministic - max_age_seconds: 3600 - environment_inputs: [PYTHONHASHSEED] - dependency_files: [runtime.lock] - profile: pinned-python-validation - output_paths: [] - include_head: false + - id: INV-001 + source_ids: [AC-001] + invariant: + boundary: component + oracle: VAL-001 + capability_reason: + freshness: current_task_batch + task_id: task-001 + evidence_ids: [VAL-001] + closure_result: pending +completion_criteria: [] +methodology: {primary: tdd, required_skills: [dev-test-driven-development]} +executor_profile: {capability: judgment, context_mode: compiled-brief, review_capability: judgment} +acceptance_review: {required: false, reviewer_independent: false, verdict: pending, reviewed_head: '', findings: []} +allocated_rules: [] +allocated_skills: [] +handoff_contract: executor-result-v1 ``` -Only `mode` is needed for deterministic checks: its default freshness is 3600 seconds. Without a declaration, or with `mode: live`, freshness defaults to 0 (execute every time). A live check may explicitly declare bounded freshness. `max_age_seconds` is an integer 0–86400. Legacy `reuse_seconds` remains a deterministic opt-in, with HEAD binding retained; conflicting freshness fields fail closed. Skipped and failed observations do not create reusable positive evidence. - -The identity covers conservative material repository content and index state (including dirty/untracked and declared task-created inputs), semantic validation fields, runner/oracle code, declared dependency/profile identity, OS/architecture/runtime, explicitly relevant environment variables, and the execution binding/cwd. Use `include_head: true` for exact-commit claims such as release validation. Restoring the complete deterministic identity A → B → A may reuse its original fresh result; explicit provenance revocation still invalidates it. Local observations never substitute for GitHub platform evidence through an inferred equivalence. - -Generated WorkBundle runtime, handoff, review, and log artifacts are packaging, not implicit source inputs. Other observation outputs require exact repository-relative `output_paths`; explicit read/dependency inputs cannot also be output-only. This affects fingerprinting only. Initial result acceptance checks write scope, Git neutrality, handoff/task/plan identity, result shape, knowledge disposition, evidence closure, and authorization once, then persists compact accepted authority. Post-acceptance continuation revalidates compact authority and current claim-relevant observations without replaying the handoff. If an expensive check consumes an otherwise excluded artifact, declare it in `dependency_files`. Unknown external dependencies, unsupported links/submodules, or protected source inputs must not be approximated for reuse. Pin execution profiles and declare all relevant environment/dependency inputs; use fresh execution when coverage is uncertain. No per-feature dependency inference is performed. - -A legacy 3-column `Command or inspection | Proves | Expected` row without YAML `kind` is `legacy-untyped`. It fails closed until ordinary artifact repair migrates it to front-matter `kind: process|inspection`. Do not default it to `process`. Never shell-execute ambiguous legacy text. - -## Completion - -- Implementation criteria are satisfied. -- Fresh task validation evidence exists. -- The compiled task brief carries the accepted Truth Basis. When review is required, the review package carries the same values. -- For initial acceptance, a valid `executor-result-v1` handoff exists. Accepted-task repair, publication retry, finalization, and resume consume compact accepted authority and do not require another handoff. -- Shared completion validation has passed: task/plan identity, executor-result shape, fresh required validation, `knowledge_disposition`, and unresolved/blocker state. -- When `acceptance_review.required` is false or omitted, `Completed` does not require an independent reviewer or `accept`. -- When `acceptance_review.required` is true, `acceptance_review.verdict` is `accept`. -- A newly authored task acceptance record exposes `review_mode: initial|repair` and `review_target_kind: task`. Legacy records without these fields are tolerated only as initial-review migration input. -- When `task_fit_check.result` is `repaired`, completion requires `review_mode: repair`, the native review envelope fields, and exactly one `previous_review`. The closed `repair_frontier` binds that predecessor's review ID, blocking finding IDs, previous and repaired target identities, affected boundaries, and frozen evidence identity. Whole review history is not embedded or reacquired. -- Both the current and previous task-review records retain `required: true`, `reviewer_independent: true`, and `review_target_kind: task`; the adapter does not infer or overwrite those ownership facts. The accepted repaired target names the completed task and its `source_tree` plus `reviewed_head` must equal the helper-observed Git tree and head. -- Material redesign or changed authority, scope, acceptance, decomposition, or validation allocation requires a fresh `initial` review with `review_reset` bound to the prior review, classified reason, and current target and evidence. The reviewer may reuse the same agent identity when judgment-capable and independent by authorship/repair/decision/deliberation participation and review provenance; identity rotation is not a freshness requirement. -- Task and stage review results are first-class review-store records. Lifecycle admission takes only `{review_id, sha256}` plus the expected current target; it revalidates the provider-specific reviewer-run receipt before exposing a verdict or selecting a finding. Bare output, receipt, or finding objects are non-authoritative. -- The task-review product candidate contains accepted product requirements/boundaries, exact product source/diff identity, normalized harness-owned validation observations, and unresolved product concerns. Handoff, knowledge disposition, reviewer history, and publication/status/archive bookkeeping remain controller-owned and are excluded from reviewer judgment. Controller/orchestration code remains product when allocated by the task. -- A stored post-execution task repair review may recompute the compact accepted result while preserving its executor-result digest, validation evidence identities, owner, baseline, and knowledge disposition. This path performs no executor redispatch, replacement handoff, validation rerun, or review-history embedding. - -## Planning verification - -Before planning completes, view this task through the plan's semantic-convergence lenses: source-ID coverage, exact file and interface scope, dependencies, validation ownership, allocated rules and methodology, parallel/barrier safety, and compiled executor-context completeness. Repair discovered defects and record compact `semantic_loop` evidence at the owning plan level. - -The executor normally consumes the compiled task brief, task-scoped source/tests, and allocated methodology skill. Full specification, root-plan, or phase reading is an escalation path when compiled context is inconsistent or review finds a source-contract defect. +`decision_authority` is semantically distinct from generic `source_ids`. Use `none-relevant` only when the verified specification carries no accepted authority, otherwise use its ordered `AUTH-NNN` aliases; the compiler resolves `AUTH-NNN: `. A conflict status of `escalate` routes `decision-blocked`. `EXC-*`, rejected, deferred, candidate, background, blocked, or superseded authority never enters executor briefs. -## Methodology and capability allocation +Use `no_validation_bearing_obligation` only with a non-empty reason and no invariants. Otherwise every validation-bearing obligation has a task-owned invariant, `capability_reason`, `freshness`, and a capable oracle. Exact suffixed IDs such as `REQ-001A` remain intact. -- `mechanical`: one or two files with exact contracts and commands and little judgment. -- `standard`: multi-file coordination, pattern matching, debugging, or integration. -- `judgment`: architecture, concurrency, ambiguous tradeoffs, or high-risk review. -- Semantic artifacts allocate `dev-semantic-convergence`. -- Unexpected behavior allocates `dev-systematic-debugging`; diagnosed testable repair also allocates TDD. -- New or changed testable behavior allocates `dev-test-driven-development`. -- Acceptance review allocates `dev-code-review`. -- Configuration, generated, or non-testable mechanical work uses `direct` plus exact deterministic verification. +The compiler validates canonical family, parent bindings, source IDs, dependencies, safe exact scope, structured validation, and authority aliases. It does not decide semantic completeness, appropriate decomposition, evidence sufficiency, or acceptance. Those are direct reviewer judgments against the verified specification and concrete plan tree. diff --git a/references/assets/orchestration/contract/task-v1.schema.json b/references/assets/orchestration/contract/task-v1.schema.json new file mode 100644 index 0000000..d8dae5c --- /dev/null +++ b/references/assets/orchestration/contract/task-v1.schema.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "task-v1", + "type": "object", + "required": ["artifact_type", "schema_version", "id", "plan_id", "phase_id", "name", "status", "order", "task_type", "date_created", "last_updated", "source_ids", "truth_basis", "depends_on", "source_files", "target_files", "target_symbols", "interfaces", "steps", "validation", "evidence_capability", "completion_criteria", "methodology", "executor_profile", "acceptance_review", "allocated_rules", "allocated_skills", "handoff_contract"], + "properties": { + "artifact_type": {"const": "task"}, + "schema_version": {"const": 1}, + "id": {"type": "string", "pattern": "^task-[a-z0-9][a-z0-9-]*$"}, + "plan_id": {"type": "string", "pattern": "^plan-[a-z0-9][a-z0-9-]*$"}, + "phase_id": {"type": "string", "pattern": "^phase-[a-z0-9][a-z0-9-]*$"}, + "name": {"type": "string", "minLength": 1}, + "status": {"const": "planned"}, + "order": {"type": "integer", "minimum": 1}, + "task_type": {"type": "string", "minLength": 1}, + "date_created": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"}, + "last_updated": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"}, + "source_ids": {"type": "array", "minItems": 1, "items": {"type": "string", "pattern": "^[A-Z][A-Z0-9_-]*-[0-9]+[A-Z]?$"}, "uniqueItems": true}, + "truth_basis": {"type": "object", "required": ["purpose", "as_is_evidence", "decision_authority", "expected_delta", "conflict_status"], "properties": {"purpose": {"type": "string", "minLength": 1}, "as_is_evidence": {"type": "array", "minItems": 1}, "decision_authority": {"type": "array", "minItems": 1}, "expected_delta": {"type": "array", "minItems": 1}, "conflict_status": {"enum": ["clear", "escalate"]}}, "additionalProperties": false}, + "depends_on": {"type": "array", "items": {"type": "string", "pattern": "^task-[a-z0-9][a-z0-9-]*$"}, "uniqueItems": true}, + "source_files": {"type": "array", "items": {"type": "string", "minLength": 1}}, + "target_files": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}, + "target_symbols": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}, + "interfaces": {"type": "object"}, + "steps": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}, + "validation": {"type": "array", "minItems": 1, "items": {"type": "object", "required": ["kind"], "properties": {"id": {"type": "string"}, "kind": {"enum": ["process", "inspection"]}, "command": {"type": "string"}, "mechanism": {"type": "string"}, "proves": {"type": "array"}, "expected": {}}, "additionalProperties": true}}, + "evidence_capability": {"type": "object", "required": ["result", "reason", "invariants"], "additionalProperties": true}, + "completion_criteria": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}, + "methodology": {"type": "object", "minProperties": 1}, + "executor_profile": {"type": "object", "required": ["capability", "context_mode", "review_capability"], "additionalProperties": true}, + "acceptance_review": {"type": "object", "required": ["required"], "additionalProperties": true}, + "allocated_rules": {"type": "array"}, + "allocated_skills": {"type": "array"}, + "handoff_contract": {"const": "executor-result-v1"} + }, + "additionalProperties": false +} diff --git a/references/assets/orchestration/contract/task-v2.schema.json b/references/assets/orchestration/contract/task-v2.schema.json new file mode 100644 index 0000000..70b1db4 --- /dev/null +++ b/references/assets/orchestration/contract/task-v2.schema.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "task-v2", + "type": "object", + "required": ["artifact_type", "schema_version", "id", "plan_id", "phase_id", "name", "status", "order", "task_type", "date_created", "last_updated", "source_ids", "source_obligations", "truth_basis", "depends_on", "source_files", "target_files", "target_symbols", "interfaces", "steps", "validation", "evidence_capability", "completion_criteria", "methodology", "executor_profile", "acceptance_review", "allocated_rules", "allocated_skills", "handoff_contract"], + "properties": { + "artifact_type": {"const": "task"}, + "schema_version": {"const": 2}, + "id": {"type": "string", "pattern": "^task-[a-z0-9][a-z0-9-]*$"}, + "plan_id": {"type": "string", "pattern": "^plan-[a-z0-9][a-z0-9-]*$"}, + "phase_id": {"type": "string", "pattern": "^phase-[a-z0-9][a-z0-9-]*$"}, + "name": {"type": "string", "minLength": 1}, + "status": {"const": "planned"}, + "order": {"type": "integer", "minimum": 1}, + "task_type": {"type": "string", "minLength": 1}, + "date_created": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"}, + "last_updated": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"}, + "source_ids": {"type": "array", "minItems": 1, "items": {"type": "string", "pattern": "^[A-Z][A-Z0-9_-]*-[0-9]+[A-Z]?$"}, "uniqueItems": true}, + "source_obligations": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["source_id", "semantic"], + "properties": { + "source_id": {"type": "string", "pattern": "^[A-Z][A-Z0-9_-]*-[0-9]+[A-Z]?$"}, + "semantic": {"type": "string", "minLength": 1} + }, + "additionalProperties": false + } + }, + "truth_basis": {"type": "object", "required": ["purpose", "as_is_evidence", "decision_authority", "expected_delta", "conflict_status"], "properties": {"purpose": {"type": "string", "minLength": 1}, "as_is_evidence": {"type": "array", "minItems": 1}, "decision_authority": {"type": "array", "minItems": 1}, "expected_delta": {"type": "array", "minItems": 1}, "conflict_status": {"enum": ["clear", "escalate"]}}, "additionalProperties": false}, + "depends_on": {"type": "array", "items": {"type": "string", "pattern": "^task-[a-z0-9][a-z0-9-]*$"}, "uniqueItems": true}, + "source_files": {"type": "array", "items": {"type": "string", "minLength": 1}}, + "target_files": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}, + "target_symbols": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}, + "interfaces": {"type": "object"}, + "steps": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}, + "validation": {"type": "array", "minItems": 1, "items": {"type": "object", "required": ["kind"], "properties": {"id": {"type": "string"}, "kind": {"enum": ["process", "inspection"]}, "command": {"type": "string"}, "mechanism": {"type": "string"}, "proves": {"type": "array"}, "expected": {}}, "additionalProperties": true}}, + "evidence_capability": {"type": "object", "required": ["result", "reason", "invariants"], "additionalProperties": true}, + "completion_criteria": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}, + "methodology": {"type": "object", "minProperties": 1}, + "executor_profile": {"type": "object", "required": ["capability", "context_mode", "review_capability"], "additionalProperties": true}, + "acceptance_review": {"type": "object", "required": ["required"], "additionalProperties": true}, + "allocated_rules": {"type": "array"}, + "allocated_skills": {"type": "array"}, + "handoff_contract": {"const": "executor-result-v1"} + }, + "additionalProperties": false +} diff --git a/references/assets/orchestration/workflow.md b/references/assets/orchestration/workflow.md index 1d468e8..eda0ad0 100644 --- a/references/assets/orchestration/workflow.md +++ b/references/assets/orchestration/workflow.md @@ -1,321 +1,61 @@ -# Orchestration Workflow +# WorkBundle Orchestration Workflow -## Artifact chain +WorkBundle separates semantic judgment from schema-owned mechanics. Agents interpret user purpose, accepted authority, product correctness, findings, qualification, and acceptance. Scripts validate schema, identity, bindings, canonical paths, scope safety, lifecycle transitions, immutable bytes, and disposable index projections. -```text -specification -> plan -> phase -> task -> execute -> executor-result -> [optional task review] - | - v - final workflow audit and finalization -``` +## Shared foundation -Durable orchestration artifacts live under `.work-bundle/orchestration/`. Disposable task briefs, review packages, and lightweight development plans live under `.work-bundle/runtime/`; they have no active/archive/index lifecycle. Durable project knowledge remains owned by approved `ks-*` flows under `.work-bundle/knowledge/`. +Resolve metadata-v4 workspace and repository authority before source work. Establish one Truth Basis containing purpose, as-is evidence, decision authority, expected delta, and conflict status. Use one bounded knowledge gateway before lightweight planning; execution consumes carried authority and does not retrieve durable knowledge. -Every development path uses the same five-field Truth Basis: purpose, as-is evidence, accepted decision authority, expected delta, and conflict status. Lightweight plans keep it compact and ephemeral. Heavy planning normalizes it in each task, then compiles it into the brief and review package. A material conflict stops through the existing typed route; no method invents authority. +Canonical artifacts are authority. Derived indexes are regenerable projections. Validate all necessary structural facts before authoritative mutation. After creation, perform only lightweight integrity/postcondition checks and truthfully report unavoidable partial effects. -## Semantic and coding methods +## Specification -Semantic artifacts use `dev-semantic-convergence`: draft, view through caller-defined lenses, repair only discovered defects, and repeat until unchanged or blocked. Specifications use user purpose, authority support, requirement consistency, impact radius, knowledge disposition, and execution-workspace lenses. Plans use source-ID coverage, dependencies, write scopes, validation ownership, allocation, barrier safety, and executor-context completeness. +The agent authors complete specification semantics. The schema-backed writer owns structural fields, canonical location, immutable family identity, lifecycle movement, and index projection. A distinct reviewer judges user-purpose alignment, authority, requirement/constraint/interface/acceptance coverage, conflicts, open questions, and scope before the specification becomes verified. -Coding tasks declare one primary method: +## Planning -- `tdd` for testable new or changed behavior and diagnosed bug fixes; -- `systematic-debugging` before repairing unexpected behavior; -- `loop-coding` for behavior-preserving refactors with a green characterization baseline; -- `direct` for configuration, generated, documentation, and other non-testable mechanical work with exact deterministic checks. +The agent authors root-plan, phase, and task semantics from a verified specification. Each artifact cites exact source IDs. The plan assigns ownership, dependencies, write scopes, task-local methodology, validation capability, and completion criteria. Static admission may report structural and graph facts but cannot qualify the plan. A distinct reviewer judges decomposition and executability. -## Specification and planning +Do not optimize task or phase cardinality. Bound expected total orchestration cost at concrete independently owned production, dependency, validation, review, and repair seams. Every authoritative production path needs a production owner. Keep a coherent mechanical increment with one owner, oracle, and repair frontier together. Create phases only for an actual barrier or convergence boundary and reject speculative splits. When a task is materially under-decomposed, return to the plan and reslice only the affected region; do not repeatedly enlarge it. -Specifications preserve initial user-purpose evidence, bounded authority evidence, stable IDs, constraints, acceptance criteria, open questions, Knowledge Base Update disposition, and this policy when applicable: +Disposable task briefs and lightweight development plans compile accepted authority for execution. They are not durable semantic authority. -```yaml -execution_workspace: - isolation: required | preferred | existing - profile: default | - cleanup: after_integration | manual -``` +## Execution and executor results -Specification creation decides policy only; it does not provision a worktree. Planning carries that policy into executable tasks. +Executors work only within bound task/repository/write scope. Behavior changes use task-local methodology and claim-relevant focused validation. Executors report facts; they never accept the product. -Plans keep durable artifacts normalized and DRY. Every executable task cites source IDs, the accepted Truth Basis, exact file scope, dependencies, validation, methodology, allocated rules/skills, a provider-neutral executor profile, and acceptance-review requirements. Decomposition does not optimize task or phase cardinality: it bounds expected total orchestration cost with concrete independently owned production, dependency, validation, review, and repair seams while preserving exact dependencies and disjoint write scopes. Every authoritative production path has a production owner; helper-only allocation cannot leave the path unowned. A coherent mechanical increment with one owner, oracle, and repair frontier stays together. A phase exists only for an actual barrier or convergence boundary, and speculative splits unsupported by current authority, repository, dependency, ownership, validation, or acceptance evidence are rejected. When simplification depends on a consequential assumption, the earliest ordinary task cheaply falsifies it before broad edits; do not add a checkpoint phase or risk-score lifecycle. Contract-decoupled parallel tasks share a stable contract group, validate only against that contract plus accepted handoffs and task-local files, reach a named barrier, and defer joint checks to the convergence owner. +After execution, create one canonical `executor-result-v1` under the catalog-selected result location. It records implemented scope, changed paths, focused validation observations, unresolved product blockers, task fit, repository/CodeGraph facts, delegation, and knowledge disposition. Structural validation, collision checks, and binding checks occur before mutation. Historical handoffs, embedded statuses, override sidecars, and fallback indexes are unsupported and ignored. -When execution proves a task materially under-decomposed, return to the plan and reslice only the affected region at the newly evidenced seam. Preserve the original binding, baseline, and accepted unaffected regions; do not repeatedly enlarge the task or create a parallel retry lifecycle. +## Direct implementation review -Generated specifications and plans record compact semantic convergence evidence: +Freeze an exact commit or worktree candidate using a path-sorted changed-path manifest. A distinct reviewer compares the actual candidate directly with the verified specification and canonical plan, every planned feature and acceptance obligation, edge and failure behavior, and capable focused observations. -```yaml -semantic_loop: - result: converged | blocked - rounds: 2 - repaired: - - missing requirement coverage -``` +The reviewer issues `accept`, `repair`, or `blocked` from product correctness. Passing tests cannot hide missing behavior. Missing or defective historical artifacts, indexes, knowledge state, or controller ceremony are separate supporting-state defects unless they make the product ambiguous, unsafe, inaccessible, or impossible to review. Store the decision in one canonical `implementation-review-v1`. -## Compiled execution context +Reviewer execution may use read-only workspace isolation and transient provider diagnostics. Those operational facts do not become semantic or lifecycle authority and are not replayed by downstream consumers. -Before normal bounded execution, `build-task-brief` compiles the task, its cited source IDs, and the same five-field Truth Basis into a self-contained ephemeral packet. The packet resolves exact requirements, constraints, interfaces, file scope, methodology, allocated rules, validation commands, workspace root, handoff contract, and review requirement. Missing source IDs fail closed. Decision authority is semantically distinct from generic source IDs and must be `none-relevant` or an `AUTH-NNN` alias allocated from verified specification reconciliation; the compiler resolves each alias to `AUTH-NNN: ` without exposing knowledge paths. Invented, candidate, background, blocked, or superseded authority and any conflict block compilation. +## Accepted continuation -For pre-commit acceptance, `build-review-package --head worktree` includes tracked, staged, unstaged, and untracked changes under a stable worktree identity while withholding protected-path content. +After an accepted implementation review when required, create one canonical `accepted-task-result-v1`. It references the exact executor result, accepted implementation review, current validation outcomes, product identity, unresolved material defects, and knowledge disposition. Dependencies and final review consume this compact decision without redispatching the executor or reconstructing review history. -The normal executor reads the task brief, task-scoped source/tests, and explicitly allocated methodology skills. Full specification, root-plan, and phase reading is an escalation path when the brief is inconsistent or an acceptance reviewer detects a source-contract defect. Execution remains no-retrieval: executors do not query or read `.work-bundle/knowledge/`. +## Final workflow review -## Repository and workspace safety +A distinct final auditor performs one compact pass over plan/task coverage, accepted implementation verdicts, relevant current tests, unresolved material defects, final knowledge disposition/return, repository finalization facts, and truthful archive readiness. The auditor does not reread source for code quality or repeat implementation review. -Before compilation, delegation, or edits, resolve the containing workspace and every target repository from `.work-bundle/project.yaml`. Git-backed targets must match expected branch and accepted metadata baseline and report a clean worktree, unless a validated executor-result handoff explains exact expected changes. Never stash, reset, clean, restore, delete, or overwrite user work to pass preflight. +Store the verdict in one canonical `final-workflow-review-v1`. Deterministic finalization carries that supplied verdict and validates only canonical references, lifecycle state, clean baselines, archive destinations, index rebuilds, and binding release. Mechanical failure cannot manufacture or reinterpret a semantic verdict. -Use CodeGraph first only when a target contains `.codegraph/` and the work affects indexed source. Sync after preflight and before graph inspection, recheck cleanliness, and sync again after indexed changes. Record `no-index` and use bounded direct inspection when absent; do not initialize CodeGraph. +## Knowledge disposition -When isolation is required or preferred, `orch-execute-plan` selects or prepares an execution workspace and applies the named hydration profile. Temporary workspaces carry provenance and may be cleaned only when WorkBundle owns them, Git identity still matches, policy allows it, the worktree is clean, and durable provenance records confirmed integration or an explicit discarded/retired decision. Age makes a workspace stale for reporting but never proves a terminal lifecycle state. Never delete user or harness workspaces. Never copy credential values into task packets, prompts, handoffs, or worktrees; `credential-inject` uses the protected credential boundary. +Each meaningful validated move records `none`, `update`, `supersede`, or `reclassify`. Approved persistence is delegated to the appropriate `ks-*` owner. Orchestration never writes durable knowledge directly, and supporting knowledge state does not determine product qualification. -## Task execution and acceptance +## Current command surface -Full orchestration has three stage gates, separate from optional task review: -specification before `verified`, plan before execution, and integrated implementation -before plan `Completed` or archive. Native lifecycle commands read JSON/YAML -`stage-review-v1` envelopes under `.work-bundle/orchestration/reviews/`; a shape-valid -record or a self-declared `is_stale: false` is not sufficient. Direct writes with an -embedded terminal status also pass the gate. Binding creation/reuse and observed -task validation recheck current specification/plan reviews. Brief compilation alone -remains available for drafting. Lightweight development does not create these stages. +- `build-task-brief` +- `write-executor-result`, `list-executor-results`, `transition-executor-result`, `index-executor-results` +- `build-implementation-review-candidate` +- `write-implementation-review`, `list-implementation-reviews` +- `write-accepted-task-result`, `list-accepted-task-results` +- `write-final-workflow-review`, `list-final-workflow-reviews` +- `finalize-reviewed-plan` -`review_runtime.artifact_review_identity(path)` binds artifact ID, version (default -`1`), and SHA-256 of canonical parsed front matter plus the complete body. Only -top-level `status`, `last_updated`, and `updated_at` are excluded so the approved -mechanical status transition does not invalidate itself. All other fields, including -review links, requirements and validation definitions, remain bound. -`plan_review_identity(workspace_root, plan_path)` aggregates the root and every -phase/task Markdown artifact declaring that plan ID, keyed by path under the plan -store, plus the identities of its linked specifications. A source, plan-member, -version, or body edit requires fresh review; old target -records remain history. These are semantic identities, not raw file checksums. -The final identity uses the same plan identity plus the resolved source repository's -current Git tree. Final admission requires a clean tracked/untracked source state; -a dirty checkout cannot claim that its HEAD tree is the reviewed candidate. Archive -rechecks admission after downstream acceptance checks, before moving artifacts. -Missing/ambiguous source repositories fail closed. Local tests do not substitute for -platform-specific release evidence. `validate_stage_reviews` requires all three -actual current target identities supplied by its lifecycle caller, not by reviews. - -Reviewer capability is closed to `standard | judgment`. Evidence access uses -`direct_source | reproducible_snapshot | packet_only`; legacy `direct` maps to -direct-source access. Legacy `constrained_direct` and `carried_summary` context are -retained for blocked/repair evidence, never sole acceptance. Accepted review requires -direct-source or reproducible-snapshot context and no unavailable claim-relevant -evidence. Snapshot access additionally requires explicit snapshot artifact digests. -The record describes evidence access; lifecycle acceptance additionally requires -`reviewer_run: {run_id, sha256}` referencing a provider-specific reviewer-run receipt: -`reviewer-native-receipt-v1` for native host runs or -`reviewer-process-receipt-v1` for legacy sandboxed process runs. -Envelope validation alone (including historical records without that reference) is -not lifecycle admission. The gate resolves the controller-owned store through -`reviewer_runtime_root(workspace_root)` under `~/.work-bundle/reviewer-runtime/workspaces/`; -the envelope cannot select an arbitrary receipt path or store. - -Before workspace creation, the controller adds `stage_review_context` to a stage direct -evidence packet: `stage`, `target_identity`, `target_locator` (a copied control -artifact), `agent_id`, `capability`, `execution_id`, and `evidence_mode`. -For task review it instead adds native `task_review_context`, binding the task target, -review mode/frontier or reset, reviewer identity/capability, execution identity, and -evidence mode. Workspace creation admits it only when the source checkout is clean and -its exact HEAD/tree still equal that task target. -The frozen packet builder derives `evidence_mode` from available evidence; requesting -`direct_source` does not grant it. The legacy process sandbox denies live source/control -access. The ordinary native host path consumes the same explicit frozen evidence, -suppresses author transport and user configuration, disables tools, and rejects observed -tool activity; native host read-only policy is not OS process isolation. A mechanically complete -`stage-evidence-manifest-v1` yields `reproducible_snapshot`; missing evidence yields -`packet_only`, which cannot grant acceptance, even with `unavailable_evidence: []`. -The manifest binds stage/target identity, required locators, roles, artifact digests, -and semantic authority identities. Its closure is derived from the current artifacts: - -- Specification: the complete specification, carried `source_knowledge.constraint` - authority, and file inputs declared by `truth_basis.as_is_evidence`. Protected - knowledge origins are not retrieved; an absent carried constraint blocks completeness. -- Plan: the root, every phase/task declaring its plan ID, and every linked verified - specification (including member-specific links and their required authority). -- Integrated implementation: the same authority closure, the complete clean Git source - tree, and each validation-bearing member's current compact accepted result from its - execution binding. Include its stored native task review when present; an older accepted - representation remains valid without format migration. Historical executor handoffs are - never scanned. The existing completion-provenance file is included when present. - -For integrated snapshots, Git file modes/blob IDs reconstruct the exact target tree; -copied bytes are checked against those blobs before workspace creation. Symlinks, -submodules, unresolved/protected inputs, and unsupported authority references fail -closed. Ignored/generated dependencies are not a source-tree substitute: checks needing -them must declare the required inputs. The manifest and packet remain in the immutable -run receipt bundle after cleanup. Creation checks the manifest against live artifacts, -publication checks the frozen closure, and lifecycle admission re-derives current -stage membership and verifies the complete source-tree identity. Removing entries and -recomputing packet/receipt hashes cannot turn partial evidence into complete evidence. - -`stage_target_identity` computes the target from current source artifacts, and -workspace creation checks it again. Complete stage evidence is checked before any -reviewer launch. Use `run_native_reviewer` for the ordinary plugin-independent native path; -`reviewer-process-run` remains the legacy sandboxed process runner. Specification and -plan workers retain the stage-review contract; task and -integrated-implementation product workers return the compact `task_review` judgment -defined by `dev-code-review`. The controller constructs the native envelope from frozen target, independence, and evidence context, -then binds its canonical digest into the receipt. The controller then attaches the run -ID and SHA-256 of the immutable receipt bytes to that exact result and publishes the -task-or-stage envelope as a read-only review-store record. Publication validates the provider-specific reviewer-run receipt once and persists an immutable direct -current-authority binding. Later lifecycle consumers use the immutable direct current-authority binding and recheck only its exact record and current target; they do -not traverse predecessors or replay receipt completeness. Bare stdout, unattached -receipts, and bare findings remain observations. - -At publication, the lifecycle gate verifies review ID, exact result/target/profile, -successful completion, the provider-specific execution boundary, and immutable -packet/profile/event digests. Native receipts bind the executable, request, actual host run identity, sanitized -context, read-only policy, and absence of observed tool activity. Legacy process receipts -bind the sandbox, denied network, and scratch-only write boundary. Run-scoped evidence -remains available after workspace cleanup; full traces -are never embedded into the stage envelope. Missing, altered, failed, mutable, or -mismatched provenance cannot grant acceptance. Known execution IDs are obtained -from artifact `execution_id`, `author_execution_id(s)`, `repair_execution_id(s)` and -current plan execution bindings; overlap with the reviewer execution/run ID blocks -admission. Undeclared author identities cannot be inferred. - -The controller/runtime store is a trusted boundary, not a cryptographic defense -against a compromised same-OS-user host. The receipt proves the launched worker's -process/evidence boundary and binds its controller-selected capability; it does not -turn a mechanical fixture into semantic review. WOR-108 mandatory task ownership -and bounded context/history projection remain separate. - -Deterministic observation reuse retains the existing complete evidence identity and -freshness policy. The provenance store reserves each identity with an OS-released -file lock, executes outside the shared store lock, then rereads and publishes under -the shared lock. Different identities can run concurrently; identical identities -remain single-flight. Publication rejects an intervening mutation epoch or expired -freshness. Reservation lock files are retained to avoid splitting concurrent waiters; -they are runtime artifacts, not source inputs or a separate cache subsystem. - -Task code review consumes one product candidate compiled from accepted product -requirements/boundaries, exact product source/diff identity, normalized harness -observations, and unresolved product concerns. Handoff, knowledge, reviewer-history, -receipt/publication, status, and archive bookkeeping remain controller inputs and do -not enter product judgment. Controller/orchestration code is product when allocated -by the accepted task. - -Reviewer observations remain intact. The controller owns their classification, the -first broken owner or artifact, and the selected action through the agent-owned v2 -contract; current routing has no fixed class-to-remedy table or confirming-review step. - -Plan identity uses the documented `plan-structural-projection-v2`: lifecycle fields are -excluded only at designated structural locations, while unknown or substantive nested -fields and requirement text remain identity-bearing. The original projection remains -callable only for explicit legacy interpretation; current writes always use v2. - -The **acceptance once** lifecycle rule makes the harness strongly verify binding, -source/scope, subagent ownership, validation, and required review, then persists one -compact accepted result. Dependency release, finalization, resume, and archive consume -that result plus a current harness observation while its identity and freshness hold; -they do not replay transient acceptance evidence or historical handoff chains. A -status-only or append-only evidence change neither invalidates the canonical semantic -plan projection nor causes a terminal rerun. -A later stored task repair review recomposes this compact result through the existing -materializer while preserving executor-result, validation, owner, baseline, and -knowledge authority; it performs no executor redispatch, handoff rewrite, validation -rerun, or review-history embedding. - -Capability context projects trusted intent/evaluation seeds through the existing -typed-relation traversal (`light`: 1 hop, `standard`: 2, `deep`: 4), bounded by -`max_nodes`. Stale/non-authoritative nodes cannot be transit nodes; frontier and -stopping reason expose depth/node-budget limits. Required evaluation seeds retain -their ordering priority. This does not introduce an initial-versus-repair frontier. - -```text -scheduler selects executable task - -> compile task brief - -> choose provider-neutral capability - -> bind mandatory subagent owner or fail closed before mutation - -> dispatch every planner-approved disjoint ready task before waiting - -> subagent implements with declared methodology - -> run fresh task-local validation - -> creation-safe validation and atomic executor-result handoff write - -> optional task review when compiled review_required: true - -> compile bounded review package - -> independent `dev-code-review` - -> accept | repair | blocked - -> accepted-result materialization joins executor facts, observations, and stored review authority - -> Completed -``` - -Subagent executors own every implementation and repair mutation, task-local verification, and executor-result evidence, including a task-local knowledge disposition of `none`, `update`, `supersede`, or `reclassify`. They never invoke persistence or read knowledge. Product reviewers judge accepted product requirements/boundaries, exact source/diff, correctness, edge cases, normalized validation observations, unresolved product concerns, and unnecessary complexity. Controllers own disposition, handoff, provenance, publication, and lifecycle mechanics. Schedulers own dependencies, barriers, context compilation, neutral subagent binding, validation routing, and evidence shape; they do not perform code-quality review or mutate task write scope. - -Selecting `orch-execute-plan` requires a subagent owner for every task without a separate user opt-in. The production `TaskOwnershipScheduler` admission entry consumes a host-native adapter or, when available, an Execution-Flow adapter; host-native execution is sufficient and Execution Flow is optional. Evidence records only the minimum agent/run identity and mechanism. If none is available, execution fails closed before task mutation. Independent disjoint tasks in distinct execution workspaces dispatch before any wait; dependent, overlapping, or same-workspace tasks serialize. Acceptance uses the same scheduler entry to reject controller mutation, and repair dispatch uses `operation: repair` through the same adapter path. - -On `repair`, return blocking findings to the existing task owner, repair from the exact -previously reviewed source, rerun only claim-relevant invalidated validation, and -perform one scoped rereview. Preserve unaffected accepted executor/validation authority. -Initial acceptance uses one executor-result handoff; accepted-task source repair consumes -the compact accepted result; publication-only/control resume reuses the completed -judgment and never redispatches, rewrites a handoff, or reruns validation/review. - -On reviewer infrastructure or provider failure, repair the first broken runner/provider -against the same immutable review package; a capable independent reviewer may be reused, -and completed judgment publication is idempotent. Source identity, validation evidence, -plan decomposition, and review frontier remain unchanged. A finding-scoped repair under unchanged authority -carries the previous finding/evidence frontier and reviews only repaired boundaries. -Only a material authority, scope, acceptance, decomposition, or validation-allocation -change resets review to an initial frontier. - -A task becomes `Completed` only when implementation criteria, fresh validation, a valid immutable executor-result handoff, and a passing `validate-executor-result` check all exist. Review-required tasks additionally require exact stored `accept` authority, joined only during accepted-result materialization. Phase and plan status derive from accepted children plus declared dependency and barrier gates. - -`write-handoff` resolves the compiled task and runs its pure creation-safe projection before artifact or handoff-index mutation. This admits structurally complete executor facts before independent observation or review while rejecting wrong-owner review, receipt, publication, accepted-result, and audit fields. New handoffs use `lifecycle_authority: location-v1`: status directories own current lifecycle state, status changes move identical bytes, and same-state requests write nothing. Unmarked legacy artifacts retain embedded/location fallback until their first explicit status change creates the bounded digest/type/plan/task/status override; no historical bytes are rewritten. - -## Bounded post-execution review and closure - -The optional workspace policy in `.work-bundle/project.yaml#orchestration_control` fixes the post-execution review round limit at five and projects stable per-flow state. The append-only controller ledger under `.work-bundle/runtime/orchestration-control/` owns round history. Plan and specification revisions do not consume post-execution review rounds; neither do task reviews, reviewer/provider retries, publication retries, resumes, or branches. Legacy workspaces without this policy keep their existing behavior. - -Once all executor attempts are terminal, the controller runs `begin-review-round` before integrated-review evidence preparation or dispatch. An exact request ID and target identity is idempotent; a different target identity reserves a new round. After publication, `complete-review-round` consumes the immutable store-owned product review reference. If no product artifact exists because the controller was blocked, a factual audit-block may complete the attempt as blocked, but it must not impersonate a product verdict. Duplicate exact completion does not increment. `review-round-status` reports the frozen target, reserved/completed counts, and finalization state. - -An accepted round continues through the normal final workflow audit, knowledge gate, archive, and index refresh. Findings below the fifth completed round route through admission-controlled scoped repair. The fifth unresolved or blocked completion stops reconciliation and invokes `finalize-with-blockers`: persist finalization-required state, validate the supplied residual specification and clean source baselines, persist an active workspace blocker, finalize the review-owned knowledge disposition, archive the origin specification and plan and update their indexes without collision overwrite, release owned bindings, and persist terminal closure. Incomplete administrative stages remain explicit and retryable, but retry must not reopen product work. - -Shared admission uses operation classes instead of caller-selected labels. An exhausted flow refuses reconciliation before its blocker is written. An active workspace blocker refuses ordinary new work and other unexempted reconciliation; read-only diagnosis, round completion, blocker recording, knowledge return, and finalization remain available. Any bounded implementation exemption names the exact flow and blocker and restores its exact backed-up blocker without losing newer unrelated metadata. - -The builtin `orch-bounded-closure` rule and its index entry are the deployment target. If an already-installed workspace has a project-scope shim with that same rule ID, retain the project shim until builtin deployment is ready, then remove only that owned shim in the same bounded migration. Never enable both copies or mutate unrelated project rules. - -Current metadata migration renames only the legacy policy key to `post_execution_review_round_limit: 5`; it never scans or rewrites historical specifications, plans, handoffs, reviews, or evidence. - -## Failure routing - -Use only these blocker classes: - -```text -context-blocked missing or inconsistent compiled context -repository-blocked branch, baseline, metadata, or repository finalization failure -decision-blocked unresolved requirement, API, architecture, or authority decision -validation-blocked required validation absent or failing -review-blocked missing or rejected acceptance evidence -knowledge-blocked required ks-* work or return evidence incomplete -workspace-blocked execution workspace preparation, hydration, ownership, or cleanup failure -``` - -Resume the step that owns the failure. Repair a task for rejected implementation, a plan for decomposition defects, and a specification only for requirement, design, or authority defects. - -Before responding to any evaluator, review, validation, or lifecycle failure, classify -the exact failing assertion into a causal class and route it to the first owning layer. -An evaluator expectation cannot create source authority. Historical validation uses an -exact baseline and endpoint rather than an open-ended live HEAD, and issue-run artifacts -remain in the workspace control plane; proven historical cleanup does not turn old -accepted manifests into a live source inventory. - -## Final workflow audit - -`orch-review-plan` audits workflow completion, required optional reviews, declared plan-level/integration acceptance, handoff integrity, knowledge disposition, finalization gates, and archive readiness. It checks declared completion evidence against the compiled Truth Basis, source IDs, expected delta, and remaining AUTH constraints. It does not redo task code review, reread implementation for code quality, or start another implementation-review agent. - -Final review aggregates accepted task dispositions from execution and task-review evidence. Any accepted `update`, `supersede`, or `reclassify` promotes durable closure to `required` even when the specification's upstream Knowledge Base Update state was `not-needed`; accepted `none` does not. Rejected task dispositions do not trigger closure. Archive is allowed only after required optional reviews are accepted, declared plan-level/integration acceptance is recorded, validation and handoffs are coherent, barriers converged, the resulting Knowledge Base Update disposition is `completed` or `not-needed`, approved `ks-*` return evidence exists when required, and allowed commit/CodeGraph/metadata/archive/index mechanics complete or are explicitly inapplicable. Missing stored review authority is not a blocker when no compiled task set `review_required: true`. - -Knowledge closure gates final completion and archive; it never precedes specification, plan, task, or integrated-implementation review. - -Only approved keep-summarizing owners write durable knowledge. Final orchestration review owns approved persistence delegation and may invoke that owner, then validate returned paths or an evidence-backed no-write result; executors and orchestration itself must not write knowledge directly. - -Specification authoring materializes `impact_decisions` from bounded current-state evidence about the requested surface, upstream/downstream relations, validation surfaces, and relevant dirty work. A relation is material only when its disposition could change a requirement, constraint, acceptance criterion, user-observable or contractual outcome, architectural boundary, measurable quality target, validation target, or declared boundary. Each material relation is `accepted | excluded | blocking`: accepted relations use `projects_to` for stable specification IDs, excluded relations require evidence, and blocking relations prevent verification. Stop when further exploration could change none of those surfaces and record the reason; a greenfield result may use `none_relevant` only with the searched boundary, reason, and `stopping_reason`. Targeted Git history, prior work artifacts, execution evidence, or durable knowledge is an escalation for contradiction, unresolved ownership, material regression/causality, or suspected governing legacy decisions—not mandatory full-history archaeology or broad knowledge retrieval. This impact-decision view is compared by semantic convergence; repository traversal remains owned by specification authoring. - -Within existing Design Interrogation, specification authoring also records one `excellence_applicability` result after one compact pass: `no_material_opportunity` with an evidence-backed reason, or `material_opportunities` with proposals selected from task evidence and change shape rather than a universal checklist. Surface an option only when accepting or rejecting it could change a requirement, constraint, acceptance criterion, user-observable or contractual outcome, architectural boundary, measurable quality target, validation target, or declared boundary. Each proposal records user value, evidence, cost, risk, recommendation, and `accepted | rejected | deferred | not_material`; unanswered proposals become deferred. Only accepted proposals may project through stable IDs into authoritative requirements, constraints, interfaces, acceptance criteria, or validation targets. Other proposals remain traceable but excluded from planning, executor briefs, and acceptance obligations. The pass stops when further exploration could change none of those surfaces, records the reason, and ensures every surfaced proposal has a disposition. It does not add a lifecycle stage, force a recommendation, or make optional proposals blocking unless accepted projection is incomplete or an unresolved safety or authority conflict exists. The excellence-applicability view is compared by semantic convergence, while agent judgment owns opportunity materiality and recommendation quality. - -Planning allocates every accepted validation-bearing obligation or design decision to stable `evidence_capability` entries before execution. Each entry names `source_ids`, invariant, boundary, oracle, `capability_reason`, `freshness`, `task_id`, task-local `evidence_ids`, and initializes `closure_result: pending`; task briefs and review packages compile only the owning task's entries. A completed mapped task returns `evidence_closure` under the same INV/VAL identities. The harness observes those compiled validation items directly and closure fails on missing, incapable, contradictory, stale, wrong-boundary, failed, or unexecuted evidence, routing repair to the first owning task, plan, or specification. Executor-authored closure is corroboration, not independent proof. Use `no_validation_bearing_obligation + reason` only when no accepted validation-bearing obligation exists, never from a WOR-61 `none_relevant` impact result alone. Select the lightest capable boundary per invariant rather than imposing universal runtime, browser, visual, performance, or E2E proof. Mechanical helpers validate IDs, completeness, task ownership, provenance, and observed results; agents own semantic capability judgment. - -## Lightweight development lane - -Use `dev-create-task-plan` for bounded mechanical work with stable decisions. After preflight and source grounding it invokes one bounded `ks-what-is-helpful` gateway, carries accepted authority or evidence-backed `none relevant`, writes one disposable plan under `.work-bundle/runtime/dev-plans/`, and creates no orchestration artifact tree. Its lightweight completion owner records an evidence-backed no-write result for `none`; for `update`, `supersede`, or `reclassify`, it invokes the approved keep-summarizing lifecycle and validates return evidence before completion. Escalate to full orchestration for unresolved architecture/API/data/workflow decisions, wide impact, multiple repositories, migration/deployment sequencing, unresolved durable-knowledge decisions, or parallel contract/barrier needs. +Legacy orchestration handoff, executor validation/adoption, review-history, and legacy finalization commands are unsupported. diff --git a/references/assets/template/AGENTS.md b/references/assets/template/AGENTS.md index c893f1d..efd6567 100644 --- a/references/assets/template/AGENTS.md +++ b/references/assets/template/AGENTS.md @@ -1,4 +1,22 @@ # Work Bundle +## Unconditional agent boundaries + +These two boundaries apply to every agent, every task, and every workflow without exception: + +1. **DO NOT OVERENGINEER.** Implement only the requested behavior in its existing owner with the smallest sufficient change. Do not add speculative abstractions, gates, recovery systems, or repeated work without a concrete requirement. +2. **MAKE NO MISTAKES.** Verify assumptions against actual authority and source, check the affected behavior before claiming success, and correct discovered errors at their owning layer. Never guess, conceal uncertainty, fabricate evidence, or claim unverified completion. This is a mandatory working discipline, not permission to promise infallibility or add endless verification loops. + +## Evidence-first change principle + +Before or during evidence exploration, every agent must: + +1. Locate the feature in the codebase. +2. Find its corresponding design purpose and decisions in the knowledge base. +3. Find its corresponding orchestration evidence—specification, plan, and handoff—and Git history. Use that lineage to understand why each implementation was created, whether it introduced the defect, and whether it is a valid basis for the current user purpose. +4. If a legacy implementation introduced the defect, prefer reverting or correcting that implementation over adding another patch around it. +5. If a legacy implementation introduced the intended feature, understand its design and make the fewest updates necessary to satisfy the current request. +6. In either case, use available source-navigation tools—including CodeGraph when indexed, `rg`, `grep`, and equivalent tools—to find related references and update them consistently. + purpose: - Seeing this rule means that you are working with the `work-bundle` toolkit, it provides skills and rules to finish a bunch of works, including: - Work bundle skills: `/wb-*`, provide skills to manage a project as a `work-bundle` adapted workspace. @@ -29,9 +47,9 @@ must: - use `work_bundle_config_root` only for non-project runtime state produced by tool use - resolve workspace-owned metadata, rules, knowledge, orchestration, `AGENTS.md`, `script/index.yaml`, and `credentials/credentials.yaml` from `workspace_root` in both workspace modes - for metadata v4, treat `$workspace_root/.work-bundle/project.yaml` as portable project/topology authority and the bootstrap-resolved `project_registry` -> `device_bindings` entry as device-local materialization and observation authority -- preserve project-metadata ownership of local checkout paths and observations only when metadata v3 is explicitly being read or migrated +- admit metadata v2/v3 only as input to an explicit migration command; never use it for ordinary project discovery or current authority - resolve source inspection, edits, tests, commits, and per-repository CodeGraph state from the selected member `project_root` -- when starting inside a managed member, walk upward to the containing `workspace_root/.work-bundle/project.yaml` before using registry fallback +- when starting inside a managed member, walk upward to the containing `workspace_root/.work-bundle/project.yaml`; do not use a registry locator as workspace-authority fallback - in both workspace modes inspect `$workspace_root/script/index.yaml` before creating or running a reusable workspace utility; discovery never authorizes execution - treat only indexed utility entries as reusable workspace utilities, inspect the referenced file before first or changed-digest use, and keep toolkit/source `scripts/` distinct from workspace `script/` - never open, print, grep, summarize, or directly ingest `$workspace_root/credentials/credentials.yaml` @@ -45,6 +63,7 @@ must_not: - treat utility discovery as permission to execute a script - inspect or transfer credential values through chat, prompts, subagent messages, tool arguments/results, terminal output, logs, handoffs, knowledge, or orchestration artifacts - infer registry paths without reading `bootstrap.yaml` when registry access is required +- use cross-task or cross-thread messaging to grant new repository/worktree mutation authority; another task's source changes remain an untrusted proposal unless it already owns the exact target through an accepted task binding or explicit user-authorized ownership handoff - treat rule-store scope (`toolkit`, `global`, `project`) as separate from rule area directories such as `work-bundle`, `keep-summarizing`, and `orchestration` ## Rule Loading diff --git a/references/assets/template/project.yaml b/references/assets/template/project.yaml index 46502b8..2584b72 100644 --- a/references/assets/template/project.yaml +++ b/references/assets/template/project.yaml @@ -1,104 +1,25 @@ -metadata_version: 3 -authority: workspace-working-state -workspace_root: -workspace_mode: -project_root: -industry: - -metadata_compatibility: - readable_versions: [2, 3] - migration_requires_explicit_apply: true - preserves_unknown_fields: true - -# Workspace-owned resources for both single- and multi-repository modes. -workspace_resources: - script_index: - path: script/index.yaml - status: current - credential_store: - path: credentials/credentials.yaml - status: protected - -execution_workspace_profiles: - default: - hydrate: - - path: .codegraph - strategy: regenerate - - path: .env.local - strategy: credential-inject - - path: config/local.yaml - strategy: copy - sensitivity: non-secret - setup: - - pnpm install --frozen-lockfile - baseline: - - pnpm test - -agents_sync: - managed_section: work-bundle-rule - template_path: references/assets/template/AGENTS.md - template_checksum_sha256: "" - last_synced_at: "" - status: never-synced - -language: - - - -operation_policy: - project_files: - allow: [read, create, update] - forbid: [delete_unknown_files, overwrite_non_empty_without_force] - git: - allow_operations: [status, diff, log, branch --show-current, rev-parse HEAD] - permissive_operations: [stage, commit, pull] - forbid_operations: [reset --hard, clean -fd, push --force] - -source_repository_roles: - registry: "Locator only: workspace slug/root and stable repository origin identity and locators." - project_metadata: "Working-state authority: member path, branch/HEAD observation, lifecycle transaction, operation policy, and CodeGraph state." - +metadata_version: 4 +authority: canonical +workspace: + id: + slug: + mode: +control_plane: + schema_version: 1 + repository: + remote: + sync_policy: + mode: manual source_repositories: - id: - project_root: - origin_id: - checkout_kind: single-repository - git_control_root: /.git - git_control_scope: project - worktree_name: - git_repository: true - expected_branch: - base_ref: HEAD - observed_head: - observation_time: - baseline_status: current - lifecycle_status: active + role: source + remote: + canonical: + aliases: [] + default_branch: + workspace_binding: + type: + name: + materialization: + required: true operation_policy: inherit - codegraph: - supported: false - index_present: false - root: - status: not-indexed - synced_commit_id: "" - last_synced_at: "" - reason: no-index - -lifecycle_transaction: - state: published - registry_status: published - metadata_status: published - -# Optional for legacy workspaces. The controller owns round history under -# .work-bundle/runtime/orchestration-control/ and projects only stable state here. -orchestration_control: - schema_version: 1 - post_execution_review_round_limit: 5 - post_execution_review_flows: [] - blockers: [] - closed_flows: [] - implementation_exemptions: [] - -migration: - authority_owner: /wb-initialize-project - compatibility_window: metadata-v2-readable-until-explicit-v3-apply - doctor_flow: "Use /wb-initialize-project doctor for deterministic file-only repair." - migrate_flow: "Inspect or dry-run metadata v2, then use explicit --apply for v3 conversion." diff --git a/references/assets/template/projects.yaml b/references/assets/template/projects.yaml index c05cdaf..c9ed661 100644 --- a/references/assets/template/projects.yaml +++ b/references/assets/template/projects.yaml @@ -1,7 +1,7 @@ registry_schema_version: 1 source_repository_roles: registry: "Locator authority in all versions; metadata v4 device_bindings also own device-local materialization paths and observations." - project_metadata: "Metadata v4 portable project/topology authority; metadata v3 working-state authority during explicit v3 reads and migrations." + project_metadata: "Metadata v4 portable project/topology authority; metadata v2/v3 are explicit migration inputs only." projects: - slug: @@ -15,8 +15,7 @@ projects: origin_path: remote: "" git_repository: - # Compatibility locator for metadata v2 readers only. Mutable checkout - # state is never stored here and v3 writers use repository_origins. + # Historical locator retained only as explicit v2/v3 migration input. source_repositories: - id: path: @@ -25,7 +24,8 @@ projects: remote: "" git_repository: compatibility: - readable_project_metadata_versions: [2, 3, 4] + readable_project_metadata_versions: [4] + migratable_project_metadata_versions: [2, 3] source_repositories_role: locator-only status: active updated_at: 2026-05-18 diff --git a/references/evals/orchestration/evals.json b/references/evals/orchestration/evals.json index e9b643d..7066f95 100644 --- a/references/evals/orchestration/evals.json +++ b/references/evals/orchestration/evals.json @@ -28,37 +28,37 @@ { "id": 5, "prompt": "Create an executor-result handoff after implementation.", - "expected_output": "Selects create-handoff, writes a sparse YAML executor-result handoff under .work-bundle/orchestration/handoff/executor/active/, includes fields by applicability, rejects forbidden executor advice fields, updates the handoff index, and does not create an active orchestration handoff or retrieve durable knowledge during execution completion.", + "expected_output": "Selects create-handoff, writes one schema-valid canonical executor-result YAML under the catalog-selected result/executor active location, includes factual task and validation observations, rejects verdict/advice fields, updates the disposable index projection, and does not inspect or migrate historical handoffs.", "files": [] }, { "id": 6, "prompt": "Execute a phase whose next two tasks are independent and have disjoint write scopes, in an environment with sub-agents.", - "expected_output": "Selects execute-plan, passes planner-approved candidates to the production TaskOwnershipScheduler, binds independent tasks to separate subagents and isolated execution workspaces, dispatches both before awaiting either, validates executor handoffs against task/phase/plan/spec, updates task statuses, and continues scheduling.", + "expected_output": "Selects execute-plan, passes planner-approved candidates to the production TaskOwnershipScheduler, binds independent tasks to separate subagents and isolated execution workspaces, dispatches both before awaiting either, validates canonical executor results against task/phase/plan/spec bindings, and continues from compact accepted task results.", "files": [] }, { "id": 7, "prompt": "A sub-agent reports completion but its executor handoff omits validation evidence.", - "expected_output": "Rejects the compact handoff as invalid by applicability, does not mark the task complete, records the blocker, and requires corrected sparse YAML validation evidence before advancing.", + "expected_output": "Rejects the executor result as structurally invalid for continuation, does not manufacture acceptance, and requests a corrected canonical result; if the exact implementation and focused observations remain independently reviewable, product review may proceed while the continuation defect is repaired separately.", "files": [] }, { "id": 8, "prompt": "Finish the last task in a phase and then the last phase in a plan.", - "expected_output": "Creates sparse YAML phase-scoped and plan-scoped executor-result handoffs, updates phase and plan statuses to Completed, does not create an active orchestration handoff, reports review-plan as the next action, and does not archive artifacts.", + "expected_output": "Creates the task's canonical executor result, obtains any required direct implementation review, creates a compact accepted task result, reports review-plan as the next action when plan closure is ready, and does not archive artifacts.", "files": [] }, { "id": 9, "prompt": "Review an implemented plan where the project files do not satisfy the source specification.", - "expected_output": "Selects review-plan, audits accepted task-review and workflow evidence without redoing code review, routes an implementation rejection to the task repair loop, and creates a specification repair only when evidence proves a requirement, design, or authority defect.", + "expected_output": "Selects review-plan, directly compares the exact implementation candidate with every verified specification and plan obligation, issues repair from missing product behavior even when tests pass, and routes specification repair only when the defect belongs to requirement or authority.", "files": [] }, { "id": 10, "prompt": "Review an implemented plan where all specification, plan, handoff, and project file checks pass.", - "expected_output": "Selects review-plan, audits accepted task-review evidence, fresh validation, handoffs, knowledge disposition, and finalization gates; archives and refreshes indexes only after every required gate passes.", + "expected_output": "Selects review-plan, consumes accepted implementation verdicts and current test outcomes, checks plan coverage, material defects, knowledge return, repository facts, and archive readiness once, then lets deterministic finalization perform only lifecycle, index, baseline, destination, and binding mechanics.", "files": [] }, { @@ -130,7 +130,7 @@ { "id": 22, "prompt": "Execute a plan task with a delegated sub-agent, then prepare its executor-result handoff after implementation reveals a task-scoped gap.", - "expected_output": "Preserves preflight, accepted baseline, dependencies, and write scopes; routes operation repair through the production TaskOwnershipScheduler to a subagent, reruns fresh validation, writes a sparse handoff with neutral ownership evidence, compiles a bounded review package, and requires an accept review before completion.", + "expected_output": "Preserves preflight, accepted baseline, dependencies, and write scopes; routes task repair through the existing owner, reruns claim-relevant focused validation, writes a factual canonical executor result, freezes the exact candidate, and requires a distinct direct implementation review before accepted continuation.", "files": [] }, { @@ -576,6 +576,84 @@ "prompt": "One module exposes two entry points, but current repository evidence shows they share one production owner, oracle, and repair path.", "expected_output": "Does not split by symbol count; independently owned entry points become separate tasks only when current repository evidence proves distinct production or repair seams.", "files": [] + }, + { + "id": "STG3-01", + "prompt": "Create and verify a specification after structural tests pass, but the draft omits one user requirement and an interface constraint.", + "expected_output": "Uses the specification-v1 canonical .spec.md family for structure, but the distinct agent reviewer rejects the semantics for concrete missing coverage; tests and index state do not issue verification.", + "files": [] + }, + { + "id": "STG3-02", + "prompt": "Review a specification whose supporting evidence folder is incomplete while the candidate remains readable and its accepted authority, conflicts, requirements, acceptance criteria, and scope can be judged directly.", + "expected_output": "Reviews the concrete specification against user purpose and workspace facts; it does not reject solely for imperfect supporting ceremony and records only material semantic findings.", + "files": [] + }, + { + "id": "STG3-03", + "prompt": "Write a current specification while malformed historical *.md specifications remain beside the current store.", + "expected_output": "Writes and indexes only the catalog-declared .spec.md family, leaves legacy bytes untouched, and never adds a fallback scan or migration authority.", + "files": [] + }, + { + "id": "STG3-04", + "prompt": "A caller asks the specification writer to preserve its custom filename, embedded id, verified status, and timestamp because a receipt already exists.", + "expected_output": "Rejects caller structural overrides and filename selection before mutation; the receipt does not decide semantic qualification, which remains a distinct reviewer-agent judgment.", + "files": [] + }, + { + "id": "STG4-01", + "prompt": "Create a root plan, phase, and task from one canonical active verified specification while obsolete Markdown plans remain nearby.", + "expected_output": "Authors complete semantic YAML, uses the root-plan/phase/task schema-owned families and exact parent bindings, ignores obsolete Markdown without fallback or migration authority, and rebuilds the three family indexes without a second combined persisted index.", + "files": [] + }, + { + "id": "STG4-02", + "prompt": "All plan schemas and static admission checks pass, but one accepted interface and its validation obligation have no task owner.", + "expected_output": "The distinct semantic reviewer issues repair for missing ownership and capable validation; tests, doctors, indexes, receipts, and evidence volume cannot qualify the incomplete plan.", + "files": [] + }, + { + "id": "STG4-03", + "prompt": "A task cites the valid suffixed source ID REQ-001A from its bound verified specification.", + "expected_output": "Preserves REQ-001A exactly through canonical task storage and compiled authority; no secondary Markdown table or lossy source-ID parser is used.", + "files": [] + }, + { + "id": "STG4-04", + "prompt": "During planning, a caller requests task Completed state and immediate plan archive because structural tests passed.", + "expected_output": "Refuses with the Stage 5 boundary: Stage 4 owns draft/verified/superseded plan qualification and planned phase/tasks only; execution completion, handoff review, finalization, and archive remain downstream agent-owned workflows.", + "files": [] + }, + { + "id": "STG5-01", + "prompt": "Write an executor result while obsolete handoff files and embedded completed statuses remain nearby.", + "expected_output": "Creates one canonical executor-result-v1 from maintained YAML parsing, exact plan/task bindings, and pre-mutation validation; ignores legacy files without filename inference, override sidecars, or fallback indexing.", + "files": [] + }, + { + "id": "STG5-02", + "prompt": "A frozen worktree candidate passes its focused tests but omits one accepted behavior from the verified specification.", + "expected_output": "The distinct reviewer directly compares the exact manifest with every specification and plan obligation and records repair in implementation-review-v1; green tests do not hide missing behavior.", + "files": [] + }, + { + "id": "STG5-03", + "prompt": "A correct reviewable candidate has a stale index and no historical reviewer receipt.", + "expected_output": "The product reviewer may accept the exact candidate; the supporting-state defect is routed separately and cannot manufacture or veto the semantic verdict.", + "files": [] + }, + { + "id": "STG5-04", + "prompt": "A dependency needs the already accepted task outcome.", + "expected_output": "Consumes one canonical accepted-task-result-v1 referencing the executor result, exact accepted implementation review, current validation outcomes, product identity, defects, and knowledge disposition without receipt or history replay.", + "files": [] + }, + { + "id": "STG5-05", + "prompt": "Run final workflow closure after every planned task has a compact accepted result.", + "expected_output": "A distinct final auditor writes one final-workflow-review-v1 covering plan/task coverage, accepted verdicts, current tests, material defects, knowledge return, repository facts, and archive readiness without repeating code review or reconstructing history.", + "files": [] } ], "v4_evals": [ diff --git a/references/evals/script-authoring/evals.json b/references/evals/script-authoring/evals.json index 5d57eaa..242d124 100644 --- a/references/evals/script-authoring/evals.json +++ b/references/evals/script-authoring/evals.json @@ -20,6 +20,16 @@ "id": "explicit-policy", "prompt": "A caller supplies an authorized retention period and scope. Design a deletion utility that enforces them, including refusal outside scope. Is this an impermissible semantic decision?", "expected_output": "Implement explicit policy and safety constraints mechanically; do not invent retention policy or infer deletion authorization from a successful precheck." + }, + { + "id": "canonical-structural-owner", + "prompt": "Design a writer for a persisted orchestration artifact whose ID, schema version, anchor, path, parent binding, lifecycle, and derived index are structural authority. The caller supplies the semantic body and also asks to override the path for convenience.", + "expected_output": "Use the maintained versioned schema and artifact-family catalog as the single structural authority; validate bindings before an atomic write, reject caller overrides of canonical fields or paths, and report mechanics without deciding semantic correctness or acceptance." + }, + { + "id": "search-is-candidate-evidence", + "prompt": "A migration helper finds orchestration files by broad directory search. The first matching filename looks correct. May it use that match as the artifact identity and acceptance authority?", + "expected_output": "Use search only for a declared retrieval, index, navigation, or diagnostic job; treat hits as candidates, then canonical-read and schema-validate them. Never infer identity or semantic acceptance from a filename or search hit." } ] } diff --git a/references/wb-initialize-project-default-work-bundle-tree.yaml b/references/wb-initialize-project-default-work-bundle-tree.yaml index 88e611a..e3b9674 100644 --- a/references/wb-initialize-project-default-work-bundle-tree.yaml +++ b/references/wb-initialize-project-default-work-bundle-tree.yaml @@ -12,14 +12,21 @@ roots: - .work-bundle/orchestration/spec/archived - .work-bundle/orchestration/plan/active - .work-bundle/orchestration/plan/archived - - .work-bundle/orchestration/handoff/orchestration/active - - .work-bundle/orchestration/handoff/orchestration/archived - - .work-bundle/orchestration/handoff/executor/active - - .work-bundle/orchestration/handoff/executor/archived + - .work-bundle/orchestration/result/executor/active + - .work-bundle/orchestration/result/executor/reviewed + - .work-bundle/orchestration/result/executor/superseded + - .work-bundle/orchestration/result/executor/archived + - .work-bundle/orchestration/result/accepted/active + - .work-bundle/orchestration/result/accepted/superseded + - .work-bundle/orchestration/result/accepted/archived + - .work-bundle/orchestration/review/implementation/active + - .work-bundle/orchestration/review/implementation/superseded + - .work-bundle/orchestration/review/implementation/archived + - .work-bundle/orchestration/review/final/active + - .work-bundle/orchestration/review/final/archived - .work-bundle/orchestration/docs - .work-bundle/orchestration/principles - .work-bundle/orchestration/templates - - .work-bundle/orchestration/reviews - .work-bundle/orchestration/execution-state multi_repository_workspace_resources: - path: script/index.yaml diff --git a/references/wb-registry-layout-migration.yaml b/references/wb-registry-layout-migration.yaml index 03884b4..f318b38 100644 --- a/references/wb-registry-layout-migration.yaml +++ b/references/wb-registry-layout-migration.yaml @@ -8,10 +8,10 @@ layout: current: "4" supported: ["2", "3", "4"] steps: - - id: layout-v2-to-v3 + - id: layout-v2-to-v4 from: "2" - to: "3" - owner: migrate-project + to: "4" + owner: migrate-control-plane - id: layout-v3-to-v4 from: "3" to: "4" diff --git a/references/wb-workspace-metadata-v3-contract.yaml b/references/wb-workspace-metadata-v3-contract.yaml index 68482d2..deb7cef 100644 --- a/references/wb-workspace-metadata-v3-contract.yaml +++ b/references/wb-workspace-metadata-v3-contract.yaml @@ -1,6 +1,6 @@ id: workspace-metadata-v3-contract -status: current metadata_version: 3 +status: historical-migration-input authority: workspace-working-state compatibility: readable_versions: [2, 3] diff --git a/references/wb-workspace-metadata-v4-contract.yaml b/references/wb-workspace-metadata-v4-contract.yaml index 5688b4c..c2a1295 100644 --- a/references/wb-workspace-metadata-v4-contract.yaml +++ b/references/wb-workspace-metadata-v4-contract.yaml @@ -1,4 +1,5 @@ metadata_version: 4 +status: current authority_split: portable: .work-bundle/project.yaml device_local: bootstrap.project_registry#device_bindings diff --git a/rules/index.yaml b/rules/index.yaml index c388434..cae5729 100644 --- a/rules/index.yaml +++ b/rules/index.yaml @@ -95,16 +95,15 @@ rules: path: orchestration/orch-artifact-authoring.md applies_when: - an orchestration artifact is created or validated - - a specification, plan, phase, task, handoff, or orchestration document is authored or repaired + - a specification, plan, phase, task, executor result, implementation review, accepted task result, final workflow review, or orchestration document is authored or repaired enforcement: must load: conditional requires: [] - id: orch-bounded-closure path: orchestration/orch-bounded-closure.md applies_when: - - all executor attempts for an orchestration flow are terminal and post-execution integrated review is about to begin, complete, or report status - - a post-execution orchestration flow reaches its fifth completed review round or must be administratively closed with unresolved blockers - - orchestration dispatch or reconciliation encounters finalization-required state or an active workspace blocker + - orchestration admission evaluates active workspace blockers or a scoped implementation exemption + - orchestration dispatch or reconciliation is blocked by unresolved workspace blocker evidence enforcement: must load: conditional requires: [] @@ -112,7 +111,7 @@ rules: path: orchestration/orch-handoff-required.md applies_when: - a task, phase, or plan execution completes or is blocked - - an executor-result or orchestration handoff is created after orchestration or execution work + - an executor result is created after orchestration or execution work enforcement: must load: conditional requires: [] @@ -122,7 +121,7 @@ rules: - create-specification needs durable project knowledge before drafting - create-implementation-plan needs durable project knowledge or spec repair context - create-document needs durable project knowledge before drafting - - create-handoff needs durable project knowledge for an orchestration handoff outside execution completion + - create-handoff needs durable project knowledge for a canonical factual executor result outside execution completion - review-plan needs durable project knowledge for validation-backed review enforcement: must load: conditional @@ -252,6 +251,8 @@ rules: - orchestration workflow resolves project metadata before specification evidence, implementation planning, execution, review, or project scope updates - repository preflight evaluates metadata-v4 portable repositories together with device-local bindings - agent checks branch baseline, commit baseline, registry locator, or CodeGraph support for a source repository + - agent sends instructions to another task or thread that could authorize repository or worktree mutation + - agent imports, accepts, or merges source changes produced by another task or thread enforcement: must load: conditional requires: [] diff --git a/rules/keep-summarizing/ks-knowledge-boundary.md b/rules/keep-summarizing/ks-knowledge-boundary.md index 8e36bc1..8da8ab5 100644 --- a/rules/keep-summarizing/ks-knowledge-boundary.md +++ b/rules/keep-summarizing/ks-knowledge-boundary.md @@ -19,8 +19,8 @@ requires: [] - Treat `.work-bundle/knowledge/` as the default durable source of truth for one managed project. - Read a legacy knowledge root only when the user or task explicitly selects it for migration or read-only intake. - Keep durable note writes under `.work-bundle/knowledge/notes/`, `.work-bundle/knowledge/open-questions/`, or `.work-bundle/knowledge/context-packs/` only when the active directive allows persistence. -- Route specification, plan, task, handoff, and reader-facing artifact work to the matching `orch-*` rule or skill instead of treating that work as durable knowledge authoring. -- Treat `.work-bundle/orchestration/handoff/` as orchestration output, not durable knowledge. +- Route specification, plan, task, executor-result, review, and reader-facing artifact work to the matching `orch-*` rule or skill instead of treating that work as durable knowledge authoring. +- Treat `.work-bundle/orchestration/result/` and `.work-bundle/orchestration/review/` as orchestration output, not durable knowledge. ## Must Not diff --git a/rules/orchestration/orch-artifact-authoring.md b/rules/orchestration/orch-artifact-authoring.md index b669fea..cf5df9c 100644 --- a/rules/orchestration/orch-artifact-authoring.md +++ b/rules/orchestration/orch-artifact-authoring.md @@ -2,7 +2,7 @@ id: orch-artifact-authoring applies_when: - an orchestration artifact is created or validated - - a specification, plan, phase, task, handoff, or orchestration document is authored or repaired + - a specification, plan, phase, task, executor result, implementation review, accepted task result, final workflow review, or orchestration document is authored or repaired enforcement: must load: conditional requires: [] @@ -20,7 +20,10 @@ Keep orchestration artifacts human-readable, contract-compliant, and executable - Put the earliest ordinary falsification task before broad simplification when a consequential assumption exists; do not create a separate checkpoint lifecycle. - Load only the directive contract and template references required for the artifact being created or validated. -- Use human-readable Markdown for specifications, plans, phases, tasks, handoffs, and orchestration documents; keep compact YAML to front matter and index files where contracts require it. +- For a family registered in the maintained artifact-family catalog, supply the semantic payload to the shared artifact store and let that structural owner select the immutable schema, validate identity and declared bindings, resolve the canonical anchor/path, serialize, mutate atomically, apply location-owned lifecycle mechanics, and project any derived index. Structural success is evidence only; the responsible agent still owns semantic correctness, sufficiency, qualification, review, and acceptance. +- Treat registration as an owning-stage cutover. Until a specialized artifact family has a real immutable schema and complete structural policy, keep its current owner and reject it through the shared store; do not add permissive placeholder entries, fallback identities, sidecars, or compatibility authority. +- Search directories only when retrieval, index rebuilding, navigation, or diagnostics is the declared operation. Treat every hit as a candidate until maintained parsing, schema validation, and canonical-location checks succeed; neither a hit nor a passing structural check establishes semantic authority. +- Use the registered representation for each current family: human-readable Markdown/front matter for specifications and schema-owned YAML for root plans, phases, tasks, executor results, implementation reviews, accepted task results, and final workflow reviews. Do not create Markdown plan/phase/task compatibility copies or other Markdown compatibility copies for YAML families. - Reference stable spec IDs such as `REQ-`, `CON-`, `AC-`, `OQ-`, and `API-` in plans, phases, and tasks instead of repeating full requirement prose. - Provide concrete source files, target files, target symbols, validation instructions, and completion criteria in every task. - Carry execution context forward through spec-ID references plus file-level instructions only. @@ -35,7 +38,7 @@ Keep orchestration artifacts human-readable, contract-compliant, and executable - Keep source-context, extra-evidence-loop, open-question, Knowledge Base Update, and body-level `Quality gate: verified|blocked` sections in specifications when required by the specification contract. - Summarize spec intent at most once in a root plan, then cite IDs for downstream detail. - Require leading spec-repair tasks when a phase or task lacks stable IDs, exact paths, validation details, or file-level execution context. -- Update plan, phase, and handoff indexes when artifacts change. +- Let the shared store rebuild the distinct per-family indexes when registered artifacts change; derived indexes are projections and do not replace canonical artifacts. Contract loading by artifact type: @@ -45,8 +48,7 @@ Contract loading by artifact type: | Root plan | `plan-v1.md` | | Phase | `phase-v1.md` | | Task | `task-v1.md` | -| Orchestration handoff | `handoff-orchestration-v1.md` | -| Executor-result handoff | `handoff-executor-result-v1.md` | +| Executor result | `handoff-executor-result-v1.md` | ## Must Not @@ -56,11 +58,12 @@ Contract loading by artifact type: - Use broad globs such as `src/**` as the only source or target path without exact files or narrow symbol-level explanation. - Do not reslice a plan or request a fresh plan review for status-only or append-only evidence changes. - Split phases or tasks solely because of template habit, lifecycle labels, duplicated prose, a task-count target, or another cardinality preference when the coherent artifact remains complete and executable. -- Encode sibling in-progress implementation files as dependencies for contract-decoupled parallel task validation; use common contracts, accepted prior handoffs, and post-barrier convergence instead. +- Encode sibling in-progress implementation files as dependencies for contract-decoupled parallel task validation; use common contracts, accepted prior executor results, and post-barrier convergence instead. - Use a legacy plan-version limit field in a current specification, plan, phase, or task, or describe artifact revisions as post-execution review rounds. - Create phases or tasks whose target files are `.work-bundle/knowledge/**`. - Embed implementation plan tasks inside specifications. - Write raw chat logs, unsupported facts, or hidden reasoning into orchestration artifacts. +- Infer artifact identity, lifecycle state, relationships, or acceptance from filenames, headings, search order, or fallback defaults when a registered structural contract owns those facts. ## Validation @@ -71,6 +74,8 @@ Contract loading by artifact type: - Confirm no phase or task repeats more than a short one-line requirement summary without a spec-ID reference. - Confirm task files are self-contained for execution from the related spec plus their own instructions. - Confirm artifact sections satisfy the loaded contract or explicitly add missing required sections named by the directive. +- Confirm registered families use the shared structural owner, unregistered families have not gained placeholder authority, and any search is both declared and followed by canonical structural validation. +- Confirm plan semantic qualification came from direct agent review of specification coverage, ownership, dependencies, validation, authority, scope, and executability; structural checks and supporting ceremony did not issue that verdict. ## On Violation diff --git a/rules/orchestration/orch-bounded-closure.md b/rules/orchestration/orch-bounded-closure.md index 7af3a42..f4fddfc 100644 --- a/rules/orchestration/orch-bounded-closure.md +++ b/rules/orchestration/orch-bounded-closure.md @@ -1,51 +1,36 @@ --- id: orch-bounded-closure applies_when: - - all executor attempts for an orchestration flow are terminal and post-execution integrated review is about to begin, complete, or report status - - a post-execution orchestration flow reaches its fifth completed review round or must be administratively closed with unresolved blockers - - orchestration dispatch or reconciliation encounters finalization-required state or an active workspace blocker + - orchestration admission evaluates active workspace blockers or a scoped implementation exemption + - orchestration dispatch or reconciliation is blocked by unresolved workspace blocker evidence enforcement: must load: conditional requires: [] --- -# Bounded Post-Execution Closure +# Orchestration Admission and Blocker Boundary ## Purpose -Bound post-execution integrated review and repair without counting pre-execution specification or plan revisions, losing unresolved product truth, or duplicating the controller's mechanical state machine. +Keep ordinary orchestration mutation behind current workspace blocker authority while keeping every exemption bound to one exact blocker and flow. ## Must -- Resolve the working workspace before applying policy. `.work-bundle/project.yaml#orchestration_control` is the portable policy and stable-state projection owner; the controller ledger under `.work-bundle/runtime/orchestration-control/` is the canonical round-history owner. Legacy workspaces may omit the policy. -- Keep each portable flow projection limited to stable flow ID, execution-complete state, latest reserved and completed round IDs, frozen target identity, outcome, and finalization state/outcome. Do not embed review history, transient handoff chains, credentials, or device-local paths. -- Treat one post-execution review round as beginning only after all executor attempts are terminal. Plan and specification revisions do not consume post-execution review rounds. -- Use `begin-review-round` before evidence preparation or reviewer dispatch. An exact request ID and target identity is idempotent; the same request with a different target identity is a different request and reserves a new round. -- Use `complete-review-round` exactly once with either a store-owned immutable review reference for `accepted|findings` or a factual controller audit-block record for a blocked attempt. A factual controller audit-block must not impersonate a product verdict. Duplicate exact completion is a no-op and conflicting completion fails closed. -- Use `review-round-status` for current counts, latest frozen target, and finalization diagnostics. Do not reconstruct round state from plan versions, review files, resumes, branches, or handoff history. -- After an accepted outcome, proceed through the normal final audit, knowledge gate, archive, and index path. When unresolved findings or a blocked attempt complete the fifth completed round, stop reconciliation and use `finalize-with-blockers`. -- Apply forced-finalization order exactly: persist finalization-required state; validate the supplied residual specification and clean portable source baselines; persist an active workspace blocker; finalize the knowledge disposition from validated review-owned return evidence; archive the origin specification and plan and update their indexes without overwriting collisions; release owned bindings; then persist terminal closure. -- Preserve incomplete administrative stages for retry. A retry may finish knowledge, archive, binding-release, or terminal-record mechanics but must not reopen product work, add a sixth round, rerun accepted evidence, or erase the active blocker. -- Apply shared admission by operation class. An exhausted flow refuses reconciliation before its blocker is written. An active workspace blocker refuses ordinary new work and other unexempted reconciliation. Read-only diagnosis, round completion, blocker recording, knowledge return, and finalization remain available so the controller can explain and finish closure. -- Limit implementation exemptions to the exact flow and blocker, and restore the exact backed-up blocker while preserving newer unrelated metadata. -- Migrate only current workspace metadata by renaming the legacy policy key to `post_execution_review_round_limit: 5`. Do not inspect, reinterpret, or rewrite historical specifications, plans, handoffs, reviews, or evidence. -- During builtin deployment, retain the project shim until builtin deployment is ready, then remove only that owned shim as part of the same bounded migration so duplicate rule IDs never enter enabled rule stores. +- Resolve admission from the current workspace metadata authority before ordinary orchestration mutation. +- Deny ordinary dispatch or reconciliation when an active blocker has current, workspace-contained specification evidence and no matching active exemption. +- Match an implementation exemption by both the exact blocker ID and the exact authorized flow ID. +- Preserve unrelated blocker and exemption entries when retiring one scoped exemption. ## Must Not -- Do not use `review_revision_limit` as current execution policy or count specification/plan edits, task reviews, reviewer retries, publication retries, resumes, or branches as post-execution rounds. -- Do not bypass admission by relabeling an operation that still dispatches execution, review, or reconciliation. -- Do not call forced blocker closure for an accepted product outcome; accepted work follows normal final audit and archive gates. -- Do not let audit-block evidence assert semantic correctness, acceptance, findings, or product rejection. -- Do not overwrite archive collisions, remove unrelated blockers or bindings, mutate historical evidence, or create a parallel recovery subsystem. +- Do not infer admission from review history, review counts, or administrative finalization state. +- Do not treat a missing, malformed, escaping, or symlinked blocker specification as valid evidence. +- Do not broaden one blocker exemption to another blocker or flow. ## Validation -- Inspect the controller caller, canonical ledger/state transition definitions, final-audit consumer, and representative accepted, unresolved-fifth-round, pre-blocker refusal, active-workspace-blocker, duplicate-request, different-target, and administrative-retry scenarios. -- Confirm the rule index mirrors this front matter and the observable triggers are concrete. -- Confirm current instruction owners use the four controller commands and reserve the limit for post-execution rounds only. -- Confirm forced closure preserves a residual specification, portable source baselines, active blocker evidence, validated knowledge return, archive collision safety, owned-binding release, and retryable incomplete administrative state. +- Confirm admitted mutations have either no active blockers or an exact active blocker/flow exemption, and that denied operations name the controlling metadata and blocker evidence. ## On Violation -Stop the affected dispatch, reconciliation, review, archive, or finalization step. Report the controller status and metadata/blocker evidence, then resume only the first owning mechanical or semantic stage without replaying accepted work. +- Stop the affected mutation, report the controlling blocker or malformed exemption, and repair that owning metadata before retrying. diff --git a/rules/orchestration/orch-handoff-required.md b/rules/orchestration/orch-handoff-required.md index c87ff4b..9b5bfeb 100644 --- a/rules/orchestration/orch-handoff-required.md +++ b/rules/orchestration/orch-handoff-required.md @@ -2,77 +2,34 @@ id: orch-handoff-required applies_when: - a task, phase, or plan execution completes or is blocked - - an executor-result or orchestration handoff is created after orchestration or execution work + - an executor result is created after orchestration or execution work enforcement: must load: conditional requires: [] --- -# Orchestration Handoff Required +# Executor Result Required ## Purpose -Require one compact executor-result handoff for an initial executor result before its first acceptance. After acceptance, continuation consumes the compact accepted result; acquiring, publishing, or retrying review/control facts does not rewrite or replay an executor handoff. Durable knowledge and orchestration strategy decisions stay outside executor-result handoffs. +Require one canonical factual executor result for safe continuation without granting it product-review or acceptance authority. ## Must -- Require each completed or partial meaningful executor move to record a knowledge disposition of `none`, `update`, `supersede`, or `reclassify` with task-local evidence. -- Reject executor disposition text that names knowledge paths, invokes any `ks-*` skill, cites authority outside the compiled task scope, or uses paths outside that scope; final orchestration review owns approved persistence delegation. - -- Run the creation-safe task projection before atomically creating/indexing an `executor-result` handoff. Creation validation must not perform harness observation, review dispatch, or acceptance. -- Create an `executor-result` handoff before reporting a task, phase, or plan execution complete or blocked. -- For a task-scoped executor-result, require explicit `related.plan` and `related.task` matching the assigned task. Missing, null, conflicting, or mismatched plan identity fails closed before `Completed`, not only before `build-review-package`; do not infer plan ownership from a local task ID. -- Default executor-result handoffs to sparse YAML. Use Markdown only when a real blocker, failure, or broad cross-repository impact needs narrative that YAML cannot express safely. -- Include only executor-owned facts needed for continuation: identity, related artifacts, result state, concise summary, changed files, validation commands and results, unresolved blockers, `task_fit_check`, repository/preflight evidence, compact CodeGraph evidence, and `delegation_evidence`. Review, receipt, publication, accepted-result, and later audit facts remain outside the handoff. -- Omit empty optional blocks, placeholder headings, duplicated spec/plan/task prose, raw chat logs, private reasoning, unrelated history, generic reminders, and non-applicable sections. -- For completed or partial task results, include `task_fit_check` naming the related task, result `clean|repaired|unresolved|skipped`, and findings only when meaningful. Check the compiled task brief and assigned task; inspect full specification, plan, and phase artifacts only when compiled context is inconsistent or a reviewer finds a source-contract problem. -- For executor-result handoffs, preserve execution safety evidence where applicable: repository preflight or accepted-baseline evidence, validation evidence, drift/gap verification, unresolved blockers, and changed-path evidence. -- For executor-result handoffs, include compact CodeGraph evidence when source-code inspection or edits were in scope. The evidence must be no larger than `root`, `applicable`, `up_to_date`, and required fallback or blocker facts unless a failure needs more detail. -- For executor-result handoffs, explicitly record no-index fallback when a target repository lacks `.codegraph/`; do not omit CodeGraph evidence silently when source-code work was in scope. -- Use `delegation_evidence` as neutral proof of mandatory task ownership. It records only delegated state, `owner_kind: subagent`, minimum agent/run identity, and provider-neutral `host-native|execution-flow` mechanism. -- For contract-decoupled task handoffs, include compact `contract_decoupling` evidence: common contract group, common contracts checked, validation scope, `peer_implementation_validation_used: false`, and forbidden peer validation result. -- For barrier participants, include compact `barrier` evidence with barrier id, participant role, readiness `reached|blocked`, and whether convergence remains pending. -- For convergence owners, include compact `barrier` and `convergence` evidence showing every participant completed or blocked with executor-result handoffs before joint validation began. -- For review handoffs or review-adjacent executor results that carry specification-included defects, include `defect_closure` evidence only as review-owned lifecycle evidence or carry-forward status; executors must not delete defect evidence. -- Do not report execution complete while drift or gaps remain within task scope. Record out-of-scope findings as unresolved issues and block completion when they prevent conformance with the assigned artifacts. -- Keep executor-result handoffs on carried spec, plan, phase, task, declared handoff, and task-scoped source or test context only; do not retrieve durable knowledge during execution-completion handoffs. -- Update `.work-bundle/orchestration/handoff/index.jsonl` with id, type, status, path, project, timestamps, and related spec, plan, phase, and task links when helper/index support is available for the handoff format. -- Mark new handoffs `lifecycle_authority: location-v1`, derive current status from `active|reviewed|superseded|archived` location, move identical bytes for actual changes, and make same-state requests write-free no-ops. Preserve unmarked legacy fallback; on its first actual transition create only the digest/type/plan/task/status override. Permit the index to retain a pre-existing duplicate identity only for unmarked, same-directory, no-override copies; reject identity-based lifecycle mutation as ambiguous. Keep new identities unique and reject every other duplicate, type, binding, digest, override, or location contradiction. -- Require phase-scoped and plan-scoped `executor-result` handoffs when those scopes complete, using the same sparse structured contract. These handoffs are execution results, not review reports. -- Treat orchestration handoffs as legacy artifacts only. Do not create new `orchestration` handoffs from the active workflow. +- Create one canonical `executor-result-v1` after task execution, including factual scope, changed paths, focused observations, blockers, task fit, repository/CodeGraph facts, delegation, and knowledge disposition. +- Validate identity, bindings, schema, canonical path, collisions, and transition before mutation; after creation perform only lightweight integrity and index checks. +- Keep executor reporting separate from independent product judgment and controller finalization. +- Treat an invalid result as blocking only continuation that requires it. Permit direct product review when exact specification/plan, frozen implementation identity, and focused observations are independently available. ## Must Not -- Mark execution complete without the required handoff for the completed or blocked scope. -- Store handoffs under `.work-bundle/knowledge/`. -- Retrieve durable knowledge while creating executor-result handoffs during `execute-plan`. -- Include forbidden executor advice fields in executor-result handoffs: `suggested_durable_conclusions`, `durable_candidate_facts`, `recommended_orchestration_review`, `recommended_next_actions`, `delegation`, `deviations`, `strategy_advice`, `knowledge_persistence`, or `baseline`. -- Include `acceptance_review`, review/verdict/target/frontier/reset/receipt/publication data, accepted-result identity/time/observations, or later audit facts in a new executor-result handoff. -- Use executor-result handoffs for durable-knowledge persistence recommendations, phase/plan/spec review advice, or orchestration strategy advice. -- Omit changed files, validation evidence, unresolved blockers, or `task_fit_check` when they are applicable to the completed or partial result. -- Claim a clean result without recording the compiled brief and assigned task checked, repairs made, and recheck outcome. -- Omit applicable compact CodeGraph fallback, up-to-date, or blocker evidence from executor-result handoffs for source-code work. -- Omit neutral `delegation_evidence` from a task executor-result handoff, record a non-subagent owner, or add UI, visibility, fallback, or internal-worker fields to ownership provenance. -- Omit contract-only validation evidence from a contract-decoupled task handoff, or report peer implementation validation as used before barrier release. -- Omit barrier readiness evidence from a barrier participant handoff, or schedule convergence without participant completed/blocked handoffs. -- Skip handoff creation because subagent execution blocked or partial completion made the outcome informal. -- Create new active `handoff-orch-*` artifacts as continuation output. +- Do not create current orchestration handoffs, inspect or convert historical instances, infer identity from filenames, or use embedded legacy status. +- Do not put verdicts, acceptance, repair advice, final-audit conclusions, or knowledge-write authorization in executor results. ## Validation -- Confirm a handoff file exists under `.work-bundle/orchestration/handoff/` before completion is reported. -- Confirm handoff type and sparse executor-result fields match the completed scope by applicability, not by fixed Markdown section presence. -- Confirm executor-result handoffs are sparse YAML by default, omit empty optional fields, and reject forbidden executor advice fields. -- Confirm executor-result handoffs created during execution did not invoke knowledge retrieval. -- Confirm executor-result handoffs identify the compiled brief and assigned task checked; include findings, repairs, and final recheck evidence; and escalate to full source artifacts when compiled context is inconsistent. -- Confirm executor-result handoffs include applicable compact CodeGraph evidence for every source-code target: root, applicability, `up_to_date`, and no-index, sync-failed, stale, or blocker facts when used. -- Confirm task executor-result handoffs include closed neutral `delegation_evidence` with delegated state, subagent owner kind, agent/run identity, and provider-neutral mechanism. -- Confirm contract-decoupled task handoffs include common-contract validation scope and `peer_implementation_validation_used: false`. -- Confirm barrier participant and convergence-owner handoffs include readiness or release evidence by applicability. -- Confirm the handoff index entry reflects the new or updated handoff. -- Confirm the complete-byte digest is stable across lifecycle changes, rebuilt lookup derives marked state from location, bounded legacy override precedence survives restart, and same-state requests change no bytes or controller records. -- Confirm no active workflow creates new orchestration handoffs. +- Confirm the canonical artifact and bindings, factual closed content, immutable bytes, and truthful partial-effect reporting. ## On Violation -For an unaccepted initial executor result, stop completion reporting and create or repair its compact executor-result handoff. For an already accepted task, use its compact accepted result and route only the affected product, publication, or finalization owner; never redispatch execution merely because a handoff is absent from post-acceptance context. Remove forbidden advice fields and fill missing initial-result evidence only within that initial handoff. If active orchestration handoff creation is attempted, reject it and use active specs, plans, phases, tasks, indexes, and compact accepted results for continuation state. +- Stop the result write or dependent continuation, report the failed structural condition, and retry only with a corrected canonical executor result. diff --git a/rules/orchestration/orch-knowledge-gateway.md b/rules/orchestration/orch-knowledge-gateway.md index ca1142d..7dc803d 100644 --- a/rules/orchestration/orch-knowledge-gateway.md +++ b/rules/orchestration/orch-knowledge-gateway.md @@ -4,7 +4,7 @@ applies_when: - create-specification needs durable project knowledge before drafting - create-implementation-plan needs durable project knowledge or spec repair context - create-document needs durable project knowledge before drafting - - create-handoff needs durable project knowledge for an orchestration handoff outside execution completion + - create-handoff needs durable project knowledge for a canonical factual executor result outside execution completion - review-plan needs durable project knowledge for validation-backed review enforcement: must load: conditional @@ -33,7 +33,7 @@ Route orchestration access to durable project knowledge through the approved `ks | `create-specification` | `implementation_spec` | | `create-implementation-plan` | `implementation_plan` | | `create-document` | `customer_spec` | -| `create-handoff` (orchestration type) | `implementation_plan` | +| `create-handoff` (canonical `executor-result-v1`) | `implementation_plan` | | `review-plan` | `implementation_plan` | - Allow `candidate` and `background` context only as rationale, traceability, or promotion input—not as executable requirements. @@ -52,7 +52,7 @@ Route orchestration access to durable project knowledge through the approved `ks - Convert material or non-material non-authority context into requirements, constraints, acceptance criteria, tasks, decisions, or review conclusions without explicit resolution or accepted authority. - Block `create-specification` only because non-material unsettled notes exist. - retrieve durable knowledge during execute-plan; execution agents must not read `.work-bundle/knowledge/` directly. -- Apply this gateway rule to `execute-plan`, executor-result handoffs created during execution, or any execution-stage retrieval. +- Apply this gateway rule to `execute-plan`, executor results created during execution, or any execution-stage retrieval. - Defer required execution context to future `.work-bundle/knowledge/` lookup after planning completes. - Treat a stale repository commit baseline as proof that durable notes are stale without retrieval classification evidence from note metadata, supersession, current user decisions, or accepted authority. - Block bounded gateway retrieval solely because source-repository metadata preflight blocks source inspection, when the gateway and knowledge base are otherwise accessible. @@ -65,7 +65,7 @@ Route orchestration access to durable project knowledge through the approved `ks - Confirm only authority context shaped requirements, tasks, or review conclusions. - Confirm no direct `.work-bundle/knowledge/` browsing occurred from the active orchestration directive. - Confirm repository metadata blockers stopped only repository-trust-dependent work and did not prevent accessible bounded gateway discovery. -- Confirm `execute-plan` and execution-completion handoffs did not invoke this gateway. +- Confirm `execute-plan` and execution-completion executor results did not invoke this gateway. ## On Violation diff --git a/rules/orchestration/orch-orchestration-boundary.md b/rules/orchestration/orch-orchestration-boundary.md index 0de5ab2..672ea3a 100644 --- a/rules/orchestration/orch-orchestration-boundary.md +++ b/rules/orchestration/orch-orchestration-boundary.md @@ -8,56 +8,29 @@ load: conditional requires: [] --- -# Orchestration Platform Write Boundary +# Orchestration Platform Boundary ## Purpose -Define where orchestration artifacts live, how artifact roles stay separated across the execution chain, and how orchestration delegates durable knowledge work without taking ownership of durable knowledge writes. - -Orchestration artifacts are derived working material under `.work-bundle/orchestration/`. Keep-summarizing owns durable project knowledge under `.work-bundle/knowledge/`. +Keep every current orchestration artifact in its canonical role and keep structural mechanics separate from semantic judgment. ## Must -- Write generated orchestration artifacts only under `.work-bundle/orchestration/`. -- permit cross-skill invocation scheduling or handoff to approved ks-* owners for durable knowledge work. -- Consume and validate delegated `ks-*` return evidence before treating durable knowledge work as complete. -- Keep specifications, plans, phases, tasks, handoffs, and reviews in distinct roles across the execution chain: `spec -> plan -> phase -> task -> execute -> handoff`. -- Preserve artifact role separation: - -| Artifact | Role | -| --- | --- | -| **Specification** | Stable requirements, constraints, interfaces, acceptance criteria, alternatives, and open questions | -| **Root plan** | Execution strategy, sequencing, phase map, risk handling, validation strategy, and dependency graph | -| **Phase** | Bounded milestone grouping related tasks with only the spec IDs, decisions, files, and tests those tasks need | -| **Task** | One executable unit with exact source files, target files, symbols, steps, validation, completion criteria, and handoff requirements | -| **Handoff** | Executor or orchestration continuation evidence before advancing status | -| **Product review** | Independent judgment of one frozen product candidate against accepted product requirements and normalized observations | -| **Controller finalization** | Evidence admission, first-owner routing, lifecycle completion, and archive mechanics | - -- Reference spec IDs in downstream plans, phases, and tasks instead of duplicating full requirement prose. -- Carry only task-specific execution detail in task files after citing stable spec IDs. -- After acceptance, make downstream orchestration consume the compact accepted result and current harness observations; keep transient acceptance evidence and historical handoff chains out of dependency, finalization, resume, and archive context. -- Route post-execution integrated review and closure through the canonical bounded controller. An exhausted flow refuses reconciliation before its blocker is written; an active workspace blocker refuses ordinary new work and unexempted reconciliation, while diagnosis and closure operations remain available. -- Keep normal accepted final audit/archive distinct from unresolved fifth-round forced closure. Controller mechanics own counting, admission, durable control state, and administrative retry; agents own product semantics and knowledge curation. +- Keep specifications, plans, tasks, executor results, implementation reviews, accepted task results, and final workflow reviews in their catalog-owned roles under `.work-bundle/orchestration/`. +- Keep durable knowledge under `.work-bundle/knowledge/` and delegate approved writes to `ks-*` owners. +- Treat canonical artifacts as authority and indexes as disposable projections. +- Use executor results for facts, implementation reviews for product verdicts, accepted task results for dependency continuation, and final workflow reviews for compact closure judgment. +- Validate structural mechanics before mutation and keep post-write checks lightweight. ## Must Not -- Write orchestration artifacts under `.work-bundle/knowledge/`. -- directly create edit promote delete or index durable knowledge from orch-* skills. -- Store specifications, plans, phases, tasks, handoffs, or review outputs as durable knowledge notes. -- Duplicate full specifications inside plans or turn tasks into mini-specifications. -- Embed implementation plans inside specifications or make phase or task files read like new specifications. -- Perform orchestration artifact work from under the knowledge tree. -- Reconstruct accepted authority by replaying transient acceptance evidence or historical handoff chains after a compact accepted result is available. -- Bypass bounded admission by renaming an execution or reconciliation action as a read-only or closure operation. +- Do not merge artifact roles, reconstruct authority from history, create compatibility sidecars, or let structural helpers decide correctness or acceptance. +- Do not read, migrate, or rewrite historical orchestration artifacts during current-path work. ## Validation -- Confirm every created or updated artifact path resolves under `.work-bundle/orchestration/`. -- Confirm artifact content matches its role in the execution chain and does not absorb another artifact's responsibilities. -- Confirm any durable knowledge request is delegated to an approved `ks-*` owner rather than written directly. -- Confirm plans, phases, and tasks cite spec IDs and concrete file-level instructions rather than repeating long requirement prose. +- Confirm each artifact is schema-owned, canonically located, correctly bound, and consumed only for its stated role. ## On Violation -Stop the orchestration write, move or rewrite the artifact under the correct `.work-bundle/orchestration/` location and role, and delegate any durable knowledge work to the approved `ks-*` owner before continuing. +- Stop the affected write or consumption, route it to the canonical artifact owner, and correct the role, binding, or location before continuing. diff --git a/rules/orchestration/orch-review-completion.md b/rules/orchestration/orch-review-completion.md index baec33a..28bff8b 100644 --- a/rules/orchestration/orch-review-completion.md +++ b/rules/orchestration/orch-review-completion.md @@ -8,79 +8,29 @@ load: conditional requires: [] --- -# Orchestration Review Completion +# Direct Review and Completion ## Purpose -Keep final review focused on whether the WorkBundle workflow completed correctly. Independent task review is optional and owns task-scoped implementation quality only when a task explicitly required it. +Keep implementation acceptance in direct independent product review and keep final workflow closure compact, factual, and non-recursive. ## Must -- Confirm each required task review judged the accepted product requirements/boundaries, exact product source/diff, normalized validation observations, and unresolved product concerns before accepting the task. -- Admit a task-or-stage verdict or route a finding only from its immutable review-store reference after provider-specific reviewer-run receipt and exact current-target validation. Treat bare reviewer output, unattached receipts, and bare findings as observations only. -- Keep product review candidates limited to accepted product requirements/boundaries, exact source/diff identity, harness-owned normalized observations, and unresolved product concerns. Handoff, knowledge, reviewer-history, and publication/archive bookkeeping remain controller audit concerns and cannot become review inputs. Controller/orchestration code remains reviewable product when allocated by the task. -- On reviewer infrastructure or provider failure, preserve the immutable candidate and repair the first broken preparation/provider owner. A still-independent capable reviewer may be reused; infrastructure failure does not itself require identity rotation, source change, validation rerun, or another product review after a completed judgment. -- For a finding-scoped repair review under unchanged authority, carry exactly the previous finding/evidence frontier and review only the repaired identity and affected boundaries. Reset to an initial frontier only after a material authority, scope, acceptance, decomposition, or validation-allocation change. -- Check that declared completion evidence corresponds to the compiled Truth Basis, source IDs, expected delta, and remaining AUTH constraints. -- Before archive or completion, confirm every accepted validation-bearing invariant has a compiled `evidence_capability` entry and capable, current, correctly bounded harness-observed evidence under its allocated INV/VAL identities. Treat incapable green, contradiction, staleness, wrong-boundary, failure, missing, or unexecuted evidence as negative acceptance evidence, not closure. -- Use `no_validation_bearing_obligation + reason` only when no accepted validation-bearing obligation or design decision exists. Do not infer an empty evidence-capability map from a WOR-61 `none_relevant` impact result. -- Route first-owner repair for this pre-closure oracle-capability check: task repair for failed, stale, or unexecuted implementation evidence; plan repair for missing, wrong-boundary, or incapable allocation; specification repair for contradictory accepted authority. -- Keep this pre-closure oracle-capability check distinct from `RuntimeVerificationClassificationV1`. WOR-59 G9 remains the unchanged post-execution classifier and may use this map only as evidence when triggered. Mechanical helpers validate IDs, completeness, provenance, and observed results; agents own semantic capability judgment and must not impose a universal browser, E2E, production, or runtime gate. -- Missing exact stored `accept` review authority blocks only a task whose compiled `review_required` is true. Do not require universal task-review evidence or embedded handoff verdicts. -- Keep approved `ks-*` persistence delegation review-owned; executor disposition evidence never authorizes knowledge retrieval or writes. -- Knowledge closure gates final completion and archive; it never precedes specification, plan, task, or integrated-implementation review. -- After all executor attempts are terminal, reserve each post-execution review round with `begin-review-round` before integrated-review evidence preparation or dispatch. Complete it with `complete-review-round` only from the stored product review reference, or from a factual controller audit-block that must not impersonate a product verdict; use `review-round-status` for diagnostics. -- Treat an accepted post-execution review round as input to the normal final audit and archive gates. On the fifth completed round with unresolved findings or a blocked attempt, stop repair/reconciliation and invoke `finalize-with-blockers` to preserve a residual specification and active workspace blocker. -- During forced closure, require the controller sequence to persist finalization-required state, validate residual-specification and source-baseline inputs, record the blocker, finalize the review-owned knowledge return, archive origin artifacts and update their indexes, release owned bindings, and persist terminal closure. Retry incomplete administrative steps without reopening product work. - -- Audit spec, plan, phase, task, handoff, and required optional-review status coherence. -- Require fresh planned validation evidence and an `accept` task-review verdict wherever review is explicitly required. -- Verify declared dependency, barrier, and convergence gates from recorded evidence. -- Use declared plan-level/integration acceptance from recorded validation; do not start another implementation-review agent to produce plan-level acceptance. -- Aggregate only accepted task dispositions. Any accepted `update`, `supersede`, or `reclassify` promotes final durable closure to `required` even when the upstream specification says `not-needed`; accepted `none` and rejected dispositions do not trigger closure. -- Route missing evidence to its first owner: an initial executor result may require its handoff; an accepted-task source repair resumes the existing task owner with claim-relevant validation and a scoped rereview; publication-only/control resume uses the compact accepted result and retries control publication/finalization without executor redispatch, handoff rewrite, validation rerun, or review rerun. -- Route incomplete durable knowledge work to `knowledge-blocked` and resume the approved `ks-*` delegate-return path. -- Route incomplete repository metadata, index, workspace, or archive mechanics to `repository-blocked` or `workspace-blocked` and use bounded deterministic helpers. -- Require the execution-evidence-driven final Knowledge Base Update disposition to be `completed` or `not-needed` before archive; archive remains blocked while promoted closure lacks validated keep-summarizing return evidence. -- Create or require plan repair only for a decomposition defect, and specification repair only for a requirement, design, or authority defect. -- Complete allowed commit, applicable CodeGraph sync, metadata update, archive, and index refresh only after all gates allow finalization. -- When a post-execution runtime or UI defect is classified, or the accepted specification or plan explicitly claims runtime acceptance of a user-visible invariant, require a `RuntimeVerificationClassificationV1` before archive. Evaluate the original user request and accepted specification before the plan, task acceptance criteria, executor handoffs, produced commits, and execution-introduced behavior. -- Require `RuntimeVerificationClassificationV1` to carry `classification`, `invariant_trace`, `negative_evidence`, and `owning_repair`. Accepted classes are `execution_introduced_bug`, `implementation_gap`, `new_feature`, and `uncovered_fixture`. -- For an accepted-invariant `execution_introduced_bug` or `implementation_gap`, require `invariant_trace` to connect original requirement, specification invariant, owning plan task or acceptance criterion, changed commit, materialization, presentation, and runtime or UI proof. Passing component or unit tests alone is insufficient for this triggered runtime claim; do not impose a universal browser or UI gate when neither trigger applies. -- Permit `new_feature` or `uncovered_fixture` with an empty `invariant_trace` only when `negative_evidence` proves no matching original user request or accepted specification invariant and no plan, handoff, or produced-commit contradiction. -- Route `owning_repair` to the first broken artifact: task or acceptance criterion present plus implementation miss means task repair and re-review; accepted specification present plus plan omission means plan repair and resume from the owning step; original-request invariant omitted or contradicted by the accepted specification means specification repair. Only after those cases are excluded may a residual class stand. -- Keep classification agent-owned and evidence-linked. A helper may require and structurally validate the record but must not decide the semantic class. -- Keep same-scope specification-owned handling authoritative for a first-observed classification defect. Persist separate WorkBundle defect evidence only after `wb-defect-evaluation` classifies the finding as work-bundle-scoped or mixed and same-scope specification-owned handling no longer applies. +- Require a distinct implementation reviewer to compare the exact frozen candidate with every verified specification and plan obligation plus capable focused observations. +- Let the reviewer issue `accept`, `repair`, or `blocked` from product correctness. Green tests cannot hide missing behavior. +- Keep missing historical records, indexes, knowledge state, and controller ceremony outside the product verdict unless the product is ambiguous, unsafe, inaccessible, or impossible to review. +- Carry accepted task decisions through canonical `accepted-task-result-v1` records. +- Use one compact final workflow review for coverage, accepted verdicts, current tests, material defects, knowledge disposition/return, repository facts, and archive readiness. +- Keep finalization mechanical: canonical references, lifecycle, clean baselines, destinations, indexes, and binding release only. ## Must Not -- Do not broadly inspect project source to redo task code review. -- Do not reread implementation source for code quality. -- Do not start another implementation-review agent for plan-level acceptance. -- Do not repair implementation or test code during final review. -- Do not substitute project-file inspection for accepted task-review evidence on tasks that explicitly required review. -- Do not replan, change source, rerun validation, or reconstruct review history solely because reviewer infrastructure or provider failure requires replacement. -- Do not create a repair specification for every failed review gate. -- Do not archive while required knowledge, validation, review, repository, or workspace evidence is unresolved. -- Do not close an invariant on a green oracle that cannot observe it or that contradicts accepted authority. -- Do not treat WOR-59 G9 classification as this pre-closure oracle-capability check, or replace G9 with it. -- Do not infer an empty evidence-capability map from a WOR-61 `none_relevant` impact result. -- Do not impose a universal browser, E2E, production, or runtime gate. -- Do not directly write durable knowledge from orchestration. -- Do not count plan/specification revisions, task review, reviewer/provider retries, publication retries, or resumes as post-execution review rounds. -- Do not use forced blocker closure for an accepted outcome or allow a sixth post-execution review round. +- Do not repeat code review during final audit, reconstruct history, replay transient evidence, or let helpers infer semantic sufficiency. ## Validation -- Confirm every completed review-required task has fresh validation, a valid immutable executor-result handoff, and exact stored `accept` review evidence joined into its accepted result. -- Confirm missing stored review authority is not a blocker unless compiled task authority explicitly required review. -- Confirm declared completion evidence matches the compiled Truth Basis, source IDs, and AUTH constraints. -- Confirm every mapped invariant has capable, current, correctly bounded harness-observed evidence under its allocated INV/VAL identities, or a typed first-owner repair route; confirm `no_validation_bearing_obligation` is not inferred from WOR-61 `none_relevant`. -- Confirm this pre-closure oracle-capability check remains distinct from `RuntimeVerificationClassificationV1` and that WOR-59 G9 remains the unchanged post-execution classifier. -- Confirm blocker routing names the owning resume path instead of restarting the lifecycle. -- Confirm finalization and archive occur only after knowledge disposition and deterministic gates resolve. -- Confirm post-execution review-round reservation/completion evidence, the fifth-round boundary, and normal-versus-forced finalization route match `orch-bounded-closure`. +- Confirm exact candidate identity, distinct reviewers, obligation coverage, compact accepted results, and a non-recursive final audit. ## On Violation -Stop finalization, emit the smallest typed blocker, and resume the step that owns the missing or contradictory evidence. Repair a plan or specification only when the defect belongs to that artifact. +- Withhold acceptance or finalization, report the unmet review or closure condition, and return the affected scope to repair and distinct rereview. diff --git a/rules/work-bundle/wb-migrate-to-multi-repository.md b/rules/work-bundle/wb-migrate-to-multi-repository.md index dec53bb..57a27db 100644 --- a/rules/work-bundle/wb-migrate-to-multi-repository.md +++ b/rules/work-bundle/wb-migrate-to-multi-repository.md @@ -15,43 +15,31 @@ requires: ## Purpose -Require dry-run-first, source-preserving, recoverable migration from a supported single-repository workspace to a supported multi-repository workspace. +Route single-to-multi requests through current metadata-v4 transactions without reviving the retired metadata-v3 topology producer. ## Must -- Invoke `wb-migrate-to-multi-repository` with explicit source `project_root`, target `workspace_root`, workspace slug, repository identity/name, working branch, and base ref. -- Keep the authority-copy source distinct from the primary Git origin. When the authority root is not Git-backed, require an explicit origin selected from its declared reusable source repositories and preserve both states independently. -- Classify legacy topology from both workspace metadata and the bootstrap-resolved registry. Permit in-place metadata migration only when the evidence is unambiguously single-repository; route multiple repositories here and block identity disagreement or proposal drift. -- Run inspect and dry-run proposal before explicit apply and report source repository and nested `.work-bundle` Git state separately. Require the exact proposal-derived accepted-baseline ID before applying either dirty state. -- Preserve source repository, branch, worktree, `.work-bundle`, registry entry, script utilities, and credential store unchanged until target verification passes. -- Copy and verify WorkBundle state and indexed workspace utilities without following unsafe symlinks or treating transient caches as authority. -- Create an empty protected target credential store; require separate secure local transfer or recreation and never copy credential content automatically. -- Provision a workspace-local Git control store and named member worktree, then publish registry and metadata only after target verification. -- Verify SessionStart discovery, member preflight, workspace-local Git control, staged metadata/registry identities, resources, and source preservation before publishing any active state. -- Publish metadata v3 and the bootstrap-resolved locator registry atomically or recoverably after final verification, with before/after identity and digest evidence. -- Treat a provisioned checkout in `verified` state as internal and incomplete. A public `provision-member` success requires metadata and registry publication; matching verified retries resume publication and published retries replay without writes. -- Treat an exact verified checkout without a recovery record as an older incomplete WorkBundle checkout only when workspace-local control, origin, repository ID, branch, and base HEAD all match. Resume publication without claiming the adopted paths as rollback-owned; keep all non-matching paths as collisions. -- Permit `cleanup-member` to remove only recorded, unpublished, transaction-owned checkout/control paths. Never use cleanup to deregister published members or delete unrecorded paths. -- Record partial failure outside disposable owned paths as a redacted recoverable transaction supporting idempotent retry or rollback of migration-owned target paths only. -- Return an already published retry from the persisted complete result with the same transaction identity/context and no metadata, registry, target, or recovery-record write. +- Treat `migrate-to-multi-repository` as a retired typed refusal and follow its v4 guidance. +- For a new multi-repository workspace, use `init-workspace --mode multi-repository`; validate its portable metadata and matching device bindings before publication. +- For metadata v2/v3 input, use `migrate-control-plane` or `migrate-registered-projects` and require the exact accepted proposal or plan identity before apply. +- For an existing metadata-v4 workspace, use the proposal-bound `add-workspace-member` transaction. +- Preserve source repositories and unrelated workspace files, and keep portable topology separate from device-local paths and observations. +- Publish metadata v4 directly and validate the portable/device-binding join by stable workspace and repository IDs. ## Must Not -- Do not commit, clean, stash, reset, delete, deregister, relocate, or silently change the source workspace or repository. -- Do not create a direct linked worktree whose Git common directory remains outside `workspace_root`. -- Do not publish a false active target registry entry or reuse conflicting paths or branches. -- Do not let `migrate-project --force` override topology classification or report public provisioning success while metadata or registry publication is pending. -- Do not delete the recovery record when rolling back transaction-owned target paths. +- Do not invoke the historical topology migration module or publish metadata v3 as a current state. +- Do not use `provision-member` or `cleanup-member`; both are retired v3-mutating routes. +- Do not infer a device binding from a repository locator or permit a member path or Git common directory to escape `workspace_root`. +- Do not commit, clean, stash, reset, delete, deregister, relocate, or silently change a source repository. - Do not copy, print, index, delegate, or archive credential material. ## Validation -- Verify source preservation, copy inventory/digests, script-index consistency, credential exclusion, AGENTS merge, and target resource protection. -- Verify member path and absolute Git common directory are within `workspace_root`, branch/base/HEAD evidence matches, and metadata/registry converge. -- Verify SessionStart discovery and per-member preflight from nested target paths. -- Verify multi-source legacy input routes to this workflow and public member results never combine `status: passed` with pending publication. -- Verify failure recovery touches only transaction-owned target paths and leaves source authority active. +- Verify the selected v4 command, its dry-run/apply identity when applicable, schema-valid metadata v4, matching device bindings, and source preservation. +- Verify legacy v2/v3 input is admitted only by an explicit migration command and no intermediate metadata v3 state is published. +- Verify member and Git common-directory paths remain inside `workspace_root` and observations match live Git state before use. ## On Violation -Stop migration publication, preserve the source unchanged, record a redacted recoverable failure, and permit only idempotent retry or explicit rollback of validated migration-owned target paths. +Stop publication, preserve the source unchanged, and route to the matching v4 initialization, migration, member-add, attach, or doctor command. diff --git a/rules/work-bundle/wb-project-context-preflight.md b/rules/work-bundle/wb-project-context-preflight.md index 8c7e214..a43b60a 100644 --- a/rules/work-bundle/wb-project-context-preflight.md +++ b/rules/work-bundle/wb-project-context-preflight.md @@ -7,6 +7,8 @@ applies_when: - orchestration workflow resolves project metadata before specification evidence, implementation planning, execution, review, or project scope updates - repository preflight evaluates metadata-v4 portable repositories together with device-local bindings - agent checks branch baseline, commit baseline, registry locator, or CodeGraph support for a source repository + - agent sends instructions to another task or thread that could authorize repository or worktree mutation + - agent imports, accepts, or merges source changes produced by another task or thread enforcement: must load: conditional requires: [] @@ -23,21 +25,23 @@ Require agents to resolve the containing `workspace_root`, portable topology, an - Resolve `work_bundle_config_root` as `~/.work-bundle/`. - Read `$work_bundle_config_root/bootstrap.yaml` before resolving registry paths. - Resolve the project registry path from `$work_bundle_config_root/bootstrap.yaml` field `project_registry` when registry access is required. -- Resolve an explicit `--workspace-root` first, or an explicit `--project-root` to its containing workspace; otherwise walk upward from cwd for `.work-bundle/project.yaml` before using bounded registry fallback. +- Resolve an explicit `--workspace-root` first, or an explicit `--project-root` to its containing workspace; otherwise walk upward from cwd for `.work-bundle/project.yaml`. Do not use a registry locator as a workspace-authority fallback. - In single-repository compatibility mode, `$project_root/.work-bundle/project.yaml` is the same file because `project_root == workspace_root`; never apply that alias to a member root in multi-repository mode. - For metadata v4, treat `$workspace_root/.work-bundle/project.yaml` as portable project/topology authority for stable workspace identity, mode, source-repository identity, canonical remotes, root/member topology, materialization requirements, and portable operation policy. - For metadata v4, resolve device-local workspace root, control-plane checkout observations, member `project_root` paths, checkout kinds, observed branch/HEAD/time, and Git common directories only from `device_bindings` in the bootstrap-resolved `project_registry`. -- For metadata v3, preserve `$workspace_root/.work-bundle/project.yaml` as local working-state authority only during explicit v3 reads and migrations. -- Establish a compact workspace/member map from v4 portable metadata plus its matching device binding, or from explicit v3 metadata during compatibility work, before source inspection, planning, or edits. -- Treat metadata v2 as readable compatibility input. Do not silently relocate it, infer multi-repository topology, or create/move worktrees without explicit migration apply authority. +- Admit metadata v2/v3 only as explicit migration input; ordinary inspection, planning, execution, and review require metadata v4. +- Establish a compact workspace/member map from v4 portable metadata plus its matching device binding before source inspection, planning, or edits. +- Treat metadata v2/v3 as migration input only. Legacy `working_branch`, `last_commit_id`, and other local checkout fields are migration evidence, not current authority. Do not silently relocate legacy metadata, infer topology, or create/move worktrees without explicit migration apply authority. - Require explicit `single-repository` or `multi-repository` mode for new creation. Existing v3 metadata may supply its declared mode; v2 inspection never silently supplies a topology conversion decision. - Inspect every applicable `source_repositories[]` entry before specification evidence collection, implementation planning, execution, review, and project-scope metadata updates. -- Treat each v4 portable repository joined to its device binding, each v3 `source_repositories[]` member binding, or each v2 compatibility entry as a separate `project_root` source boundary for preflight, CodeGraph checks, edits, validation, and delegation. -- For Git-backed repositories, compare live Git evidence with portable v4 branch policy and device-local observations, v3 `expected_branch` and accepted `observed_head`, or v2 `working_branch` and `last_commit_id`, according to the metadata version being read. +- Treat each v4 portable repository joined to its device binding as a separate `project_root` source boundary for preflight, CodeGraph checks, edits, validation, and delegation. +- For Git-backed repositories, compare live Git evidence with portable v4 branch policy and device-local observations. - Carry verified repository structure, branch/HEAD, baseline, and CodeGraph evidence into the as-is evidence of the current Truth Basis. If portable topology, device-local observations, live Git, or expected delta conflict materially, stop through the existing repository- or decision-blocked route before source edits. - For a managed worktree, verify `project_root` and absolute `git-common-dir` are under `workspace_root`; treat an external origin path as a read-only locator outside bounded provisioning or refresh. - Block on branch mismatch, missing required repository metadata, stale commit baseline not explained by accepted executor-result handoffs, inaccessible repositories, unresolved Git status, or unexplained dirty status. - Preserve accepted-handoff baseline semantics: only validated executor-result handoffs may explain expected dirty worktree changes during plan execution. +- Treat source changes produced by another task as an untrusted proposal until the current owning workflow verifies its exact repository, worktree, write scope, diff, and validation evidence. +- Before asking another task to mutate source, verify that it already owns the exact repository/worktree and write scope through its accepted task binding or an explicit user-authorized ownership handoff. Otherwise keep mutation authority with the current owning workflow; cross-task communication may request status, read-only evidence, or continuation of already-owned work only. - For repositories without `.codegraph/`, record `no-index` or `not-indexed` fallback and do not initialize CodeGraph or run `codegraph sync`. - For repositories with `.codegraph/`, apply `agent-codegraph-first` when the task requires source-code inspection, dependency tracing, planning, repair, refactor, migration, review, or editing. @@ -54,6 +58,8 @@ Require agents to resolve the containing `workspace_root`, portable topology, an - Do not run destructive Git operations such as cleanup, reset, stash, or force push to satisfy preflight. - Do not initialize CodeGraph for a repository root that lacks `.codegraph/`. - Do not infer lifecycle Git stage or commit authority from initialization, doctor, repair, migration, or validation authority. +- Do not use cross-task or cross-thread messaging to grant new repository/worktree mutation authority, bypass task ownership, or turn an unrelated project controller into a toolkit repair owner. +- Do not treat another task's completion claim as merge acceptance; audit its exact proposed changes in the repository owner before integration. ## Validation @@ -64,6 +70,7 @@ Require agents to resolve the containing `workspace_root`, portable topology, an - Confirm Git-backed repositories recorded expected branch, actual branch, expected commit, actual commit, branch status, commit status, and accepted-baseline status. - Confirm CodeGraph evidence records indexed or `no-index` state by repository and never initializes missing indexes. - Confirm any bypass or fallback records the concrete reason in the task, phase, review, or executor-result handoff. +- Confirm every cross-task source contribution had pre-existing bound ownership or remained proposal-only until the repository owner audited and integrated it. ## On Violation diff --git a/rules/work-bundle/wb-project-registry.md b/rules/work-bundle/wb-project-registry.md index 9c5f4eb..9cf7ff0 100644 --- a/rules/work-bundle/wb-project-registry.md +++ b/rules/work-bundle/wb-project-registry.md @@ -22,11 +22,11 @@ Ensure project registration and lookup preserve the versioned authority split be - Treat registry project entries as locator authority for workspace slug/root, knowledge root, aliases, and stable repository origin `id`, `origin_path`, `remote`, and Git capability. - Treat metadata-v4 `device_bindings` in that same registry as device-local authority for materialized workspace/control-plane paths, member `project_root` paths, checkout kinds, and checkout/control-plane observations. - Treat metadata-v4 `$workspace_root/.work-bundle/project.yaml` as portable project/topology authority and forbid device-local paths or observations there. -- Preserve metadata-v3 `$workspace_root/.work-bundle/project.yaml` working-state authority only for explicit v3 reads and migrations. +- Admit metadata v2/v3 only through explicit migration commands; ordinary registry consumers require metadata v4. - Register every initialized single- or multi-repository workspace to `projects.yaml` under an explicit new or existing workspace slug, and create or update the corresponding workspace metadata. - When changing portable topology, update its durable authorities through an atomic or recoverable workflow; when attaching metadata v4, publish only the device-local binding after all materializations validate. -- Describe registry, device-binding, metadata-v4 portable, and metadata-v3 compatibility roles without collapsing them into one working-state model. -- Preserve metadata v2 locator entries as compatibility input during explicit migration, preserve unknown fields, and publish v3 origin/member separation only after target verification. +- Describe registry, device-binding, portable metadata-v4, and historical migration-input roles without collapsing them into one working-state model. +- Preserve metadata v2/v3 locator entries as explicit migration input, preserve unknown portable fields, and publish validated v4 directly after target verification. - Enumerate registered workspaces from the bootstrap-resolved project registry for layout upgrades; classify each entry as current, migratable, unsupported, missing, or blocked; apply registered version-to-version layout steps in deterministic order; and publish registry `layout_version` only after the target layout validates. ## Must Not @@ -45,7 +45,7 @@ Ensure project registration and lookup preserve the versioned authority split be - Verify all locator and device-binding IO uses only the bootstrap-resolved `project_registry`. - Verify initialized projects have both a registry entry and a matching `$project_root/.work-bundle/project.yaml`. - Verify metadata-v4 portable repositories remain path-free while matching device bindings contain local materialization and observation fields. -- Verify metadata-v3 local fields remain accepted only in explicit v3 compatibility reads and migrations. +- Verify metadata-v2/v3 local fields are admitted only by explicit migration commands and are never ordinary current reads. ## On Violation diff --git a/scripts/keep-summarizing/core.py b/scripts/keep-summarizing/core.py index c1ac59e..b242804 100644 --- a/scripts/keep-summarizing/core.py +++ b/scripts/keep-summarizing/core.py @@ -6,6 +6,7 @@ import argparse import datetime as dt import hashlib +import importlib.util import json import os import re @@ -15,6 +16,29 @@ from pathlib import Path +def _infrastructure_module(): + module_path = Path(__file__).resolve().parents[1] / "work-bundle" / "infrastructure.py" + existing = sys.modules.get("work_bundle_infrastructure") + if existing is not None: + if Path(str(getattr(existing, "__file__", ""))).resolve() != module_path: + raise ImportError("work_bundle_infrastructure module collision") + return existing + spec = importlib.util.spec_from_file_location("work_bundle_infrastructure", module_path) + if spec is None or spec.loader is None: + raise ImportError("cannot load work-bundle infrastructure") + module = importlib.util.module_from_spec(spec) + sys.modules["work_bundle_infrastructure"] = module + try: + spec.loader.exec_module(module) + except BaseException: + sys.modules.pop("work_bundle_infrastructure", None) + raise + return module + + +_infrastructure = _infrastructure_module() + + LEAF_PERSPECTIVES = { "background/domain-concepts", "background/business-context", @@ -165,74 +189,24 @@ def knowledge_root() -> Path: return skill_root() / "knowledge" -def default_registry_file() -> Path: - return Path.home() / ".work-bundle" / "registry" / "projects.yaml" - - def registry_file(args: argparse.Namespace | None = None) -> Path: - if args is not None: - explicit = getattr(args, "registry_file", None) - if explicit: - return Path(explicit).expanduser().resolve() - env_path = os.environ.get("KS_PROJECT_REGISTRY") - if env_path: - return Path(env_path).expanduser().resolve() - return default_registry_file() + return _infrastructure.resolve_project_registry_path() def work_bundle_knowledge_root(workspace_root: Path) -> Path: return workspace_root.resolve() / ".work-bundle" / "knowledge" -def find_work_bundle_knowledge(start: Path) -> Path | None: - current = start.resolve() - for candidate in [current, *current.parents]: - root = candidate / ".work-bundle" / "knowledge" - if root.exists(): - return root.resolve() - return None - - -def resolve_workspace_root(start: Path) -> Path | None: - """Find the nearest containing WorkBundle workspace without registry I/O.""" - current = start.expanduser().resolve() - if current.is_file(): - current = current.parent - for candidate in [current, *current.parents]: - if (candidate / ".work-bundle" / "project.yaml").is_file(): - return candidate - return None - - -def _workspace_root_from_registry_entry(entry: dict[str, object]) -> Path | None: - value = entry.get("workspace_root") or entry.get("work_bundle_root") - return Path(str(value)).expanduser().resolve() if value else None - - -def resolve_member_project_root(workspace_root: Path, start: Path) -> Path: - """Resolve a cwd/explicit path to its deepest declared source member.""" - metadata = workspace_root / ".work-bundle" / "project.yaml" - candidate = start.expanduser().resolve() - members: list[Path] = [] - in_repositories = False - for line in metadata.read_text(encoding="utf-8").splitlines(): - if line == "source_repositories:": - in_repositories = True - continue - if in_repositories and line and not line.startswith(" "): - break - if not in_repositories: - continue - stripped = line.strip() - if stripped.startswith("project_root:") or stripped.startswith("path:"): - value = stripped.split(":", 1)[1].strip().strip("'\"") - if value: - member = Path(value).expanduser().resolve() - if member == candidate or member in candidate.parents: - members.append(member) - if members: - return max(members, key=lambda path: len(path.parts)) - return workspace_root.resolve() +def _anchor_context(**selectors: object): + try: + return _infrastructure.resolve_anchor_context(**selectors) + except _infrastructure.InfrastructureError as exc: + raise SystemExit(exc.code) from exc + + +def resolve_workspace_root(start: Path) -> Path: + """Resolve a containing current workspace through schema and binding authority.""" + return _anchor_context(cwd=start).workspace_root def read_project_slug(root: Path, fallback: str) -> str: @@ -248,151 +222,10 @@ def read_project_slug(root: Path, fallback: str) -> str: return fallback -def yaml_quote(value: object) -> str: - text = str(value) - if not text: - return '""' - if re.search(r"[:#\n\r\t]|^\s|\s$|^-|^\[", text): - return json.dumps(text, ensure_ascii=False) - return text - - -def parse_yaml_value(value: str) -> object: - value = value.strip() - if value == "[]": - return [] - if value in {"true", "false"}: - return value == "true" - if len(value) >= 2 and value[0] == '"' and value[-1] == '"': - try: - return json.loads(value) - except json.JSONDecodeError: - return value[1:-1] - return value - - def registry_projects(path: Path) -> list[dict[str, object]]: - if not path.exists(): - return [] - lines = path.read_text(encoding="utf-8").splitlines() - projects: list[dict[str, object]] = [] - current: dict[str, object] | None = None - current_list: str | None = None - current_repo: dict[str, object] | None = None - in_projects = False - for line in lines: - if not line.strip() or line.lstrip().startswith("#"): - continue - if line == "projects:": - in_projects = True - continue - if not in_projects: - continue - project_start = re.match(r"^\s{2}-\s+slug:\s*(.+)$", line) - if project_start: - current = {"slug": str(parse_yaml_value(project_start.group(1))), "aliases": [], "source_repositories": []} - projects.append(current) - current_list = None - current_repo = None - continue - if current is None: - continue - top_field = re.match(r"^\s{4}([A-Za-z_][\w-]*):\s*(.*)$", line) - if top_field: - key, raw = top_field.group(1), top_field.group(2) - current_repo = None - if raw: - current[key] = parse_yaml_value(raw) - current_list = None - else: - current.setdefault(key, []) - current_list = key - continue - list_scalar = re.match(r"^\s{6}-\s+(.+)$", line) - if list_scalar and current_list == "aliases": - aliases = current.setdefault("aliases", []) - if isinstance(aliases, list): - aliases.append(str(parse_yaml_value(list_scalar.group(1)))) - continue - repo_start = re.match(r"^\s{6}-\s+path:\s*(.+)$", line) - if repo_start and current_list == "source_repositories": - repos = current.setdefault("source_repositories", []) - current_repo = {"path": str(parse_yaml_value(repo_start.group(1)))} - if isinstance(repos, list): - repos.append(current_repo) - continue - repo_field = re.match(r"^\s{8}([A-Za-z_][\w-]*):\s*(.*)$", line) - if repo_field and current_repo is not None: - current_repo[repo_field.group(1)] = parse_yaml_value(repo_field.group(2)) - return projects - - -def write_registry_projects(path: Path, projects: list[dict[str, object]]) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - lines = ["projects:"] - for project in sorted(projects, key=lambda item: str(item.get("slug", ""))): - lines.append(f" - slug: {yaml_quote(project.get('slug', ''))}") - lines.append(f" name: {yaml_quote(project.get('name', project.get('slug', '')))}") - lines.append(f" work_bundle_root: {yaml_quote(project.get('work_bundle_root', ''))}") - lines.append(f" knowledge_root: {yaml_quote(project.get('knowledge_root', ''))}") - aliases = project.get("aliases", []) - if isinstance(aliases, list) and aliases: - lines.append(" aliases:") - for alias in aliases: - lines.append(f" - {yaml_quote(alias)}") - else: - lines.append(" aliases: []") - repos = project.get("source_repositories", []) - lines.append(" source_repositories:") - if isinstance(repos, list) and repos: - for repo in repos: - if not isinstance(repo, dict): - continue - lines.append(f" - path: {yaml_quote(repo.get('path', ''))}") - lines.append(f" work_dir: {'true' if repo.get('work_dir') else 'false'}") - lines.append(f" remote: {yaml_quote(repo.get('remote', ''))}") - lines.append(f" status: {yaml_quote(project.get('status', 'active'))}") - lines.append(f" updated_at: {yaml_quote(project.get('updated_at', now_date()))}") - tmp = path.with_name(path.name + ".tmp") - tmp.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8") - tmp.replace(path) - - -def project_registry_entry(project: str, args: argparse.Namespace | None = None) -> dict[str, object] | None: - for entry in registry_projects(registry_file(args)): - if entry.get("slug") == project: - return entry - aliases = entry.get("aliases", []) - if isinstance(aliases, list) and project in aliases: - return entry - return None - - -def registry_entry_for_cwd(cwd: Path, args: argparse.Namespace | None = None) -> dict[str, object] | None: - cwd = cwd.resolve() - for entry in registry_projects(registry_file(args)): - for key in ["workspace_root", "work_bundle_root", "knowledge_root"]: - value = entry.get(key) - if value: - candidate = Path(str(value)).expanduser() - if candidate.exists() and is_relative_to(cwd, candidate): - return entry - repos = entry.get("source_repositories", []) - if isinstance(repos, list): - for repo in repos: - if isinstance(repo, dict) and repo.get("path"): - candidate = Path(str(repo["path"])).expanduser() - if candidate.exists() and is_relative_to(cwd, candidate): - return entry - return None - - -def registry_knowledge_root_for_project(project: str, args: argparse.Namespace | None = None) -> Path | None: - entry = project_registry_entry(project, args) - if not entry: - return None - root = entry.get("knowledge_root") - return Path(str(root)).expanduser().resolve() if root else None + document = _infrastructure.load_project_registry() + projects = document.get("projects") + return [dict(item) for item in projects if isinstance(item, dict)] if isinstance(projects, list) else [] def resolve_knowledge_base(args: argparse.Namespace | None = None) -> tuple[Path, str]: @@ -402,46 +235,26 @@ def resolve_knowledge_base(args: argparse.Namespace | None = None) -> tuple[Path return Path(explicit_root).resolve(), "work-bundle" workspace_arg = getattr(args, "workspace_root", None) if workspace_arg: - workspace = Path(workspace_arg).expanduser().resolve() - return work_bundle_knowledge_root(workspace), "work-bundle" + context = _anchor_context(workspace_root=workspace_arg) + return work_bundle_knowledge_root(context.workspace_root), "work-bundle" project_root = getattr(args, "project_root", None) if project_root: explicit = Path(project_root).expanduser().resolve() - workspace = resolve_workspace_root(explicit) - if workspace: - return work_bundle_knowledge_root(workspace), "work-bundle" - entry = registry_entry_for_cwd(explicit, args) - if entry: - registry_workspace = _workspace_root_from_registry_entry(entry) - if registry_workspace: - return work_bundle_knowledge_root(registry_workspace), "registry" - return work_bundle_knowledge_root(explicit), "work-bundle" + context = _anchor_context(project_root=explicit, cwd=explicit) + return work_bundle_knowledge_root(context.workspace_root), "work-bundle" cwd_arg = getattr(args, "cwd", None) if cwd_arg: - found = find_work_bundle_knowledge(Path(cwd_arg)) - if found: - return found, "work-bundle" - entry = registry_entry_for_cwd(Path(cwd_arg), args) - if entry and entry.get("knowledge_root"): - return Path(str(entry["knowledge_root"])).expanduser().resolve(), "registry" - found = find_work_bundle_knowledge(Path(os.getcwd())) - if found: - return found, "work-bundle" - entry = registry_entry_for_cwd(Path(os.getcwd()), args) - if entry and entry.get("knowledge_root"): - return Path(str(entry["knowledge_root"])).expanduser().resolve(), "registry" - raise SystemExit("No .work-bundle/knowledge root found. Pass --project-root or --knowledge-root explicitly.") + context = _anchor_context(cwd=Path(cwd_arg)) + return work_bundle_knowledge_root(context.workspace_root), "work-bundle" + context = _anchor_context(cwd=Path(os.getcwd())) + return work_bundle_knowledge_root(context.workspace_root), "work-bundle" def project_dir(project: str, args: argparse.Namespace | None = None) -> Path: if not re.fullmatch(r"[a-z0-9][a-z0-9._-]*", project): raise SystemExit(f"Invalid project slug: {project}") - if args is not None and not getattr(args, "knowledge_root", None) and not getattr(args, "project_root", None): - registered = registry_knowledge_root_for_project(project, args) - if registered: - return registered base, mode = resolve_knowledge_base(args) - root = base.resolve() if mode in {"work-bundle", "registry"} else (base / project).resolve() + root = base.resolve() if mode == "work-bundle" else (base / project).resolve() allowed = base.resolve() if allowed != root and allowed not in root.parents: raise SystemExit("Resolved project path is outside knowledge root.") diff --git a/scripts/keep-summarizing/dispatcher.py b/scripts/keep-summarizing/dispatcher.py index e83b4f8..f8556da 100644 --- a/scripts/keep-summarizing/dispatcher.py +++ b/scripts/keep-summarizing/dispatcher.py @@ -7,7 +7,7 @@ from doctor import cmd_doctor from git_ops import cmd_git from indexes import cmd_index, cmd_index_open_questions -from migration import cmd_migrate_legacy, cmd_migrate_v3 +from migration import cmd_migrate_v3 from notes import cmd_breakdown_design, cmd_output, cmd_write_note from project import cmd_init, cmd_resolve from query import cmd_query @@ -17,8 +17,8 @@ RECOGNIZED_COMMANDS = frozenset({ "init", "resolve", "write-note", "index", "query", "index-open-questions", "git", "doctor", "output", "breakdown-design", "add-question", - "list-questions", "match-questions", "resolve-question", "migrate-legacy", - "migrate-v3", "register-project", "unregister-project", "list-projects", + "list-questions", "match-questions", "resolve-question", "migrate-v3", + "register-project", "unregister-project", "list-projects", "registry-doctor", }) @@ -119,11 +119,6 @@ def add_resolution_args(command: argparse.ArgumentParser) -> None: resolve_question.add_argument("--resolved-by-note") add_resolution_args(resolve_question) resolve_question.set_defaults(func=cmd_resolve_question) - migrate = sub.add_parser("migrate-legacy") - migrate.add_argument("--project", required=True) - migrate.add_argument("--legacy-root") - add_resolution_args(migrate) - migrate.set_defaults(func=cmd_migrate_legacy) migrate_v3 = sub.add_parser("migrate-v3") migrate_v3.add_argument("--project", required=True) migrate_v3.add_argument("--dry-run", action="store_true") diff --git a/scripts/keep-summarizing/doctor.py b/scripts/keep-summarizing/doctor.py index 864bb3f..7902504 100644 --- a/scripts/keep-summarizing/doctor.py +++ b/scripts/keep-summarizing/doctor.py @@ -1,4 +1,5 @@ from core import * +from core import _anchor_context from indexes import markdown_files, open_question_files, v3_note_issues def cmd_doctor(args: argparse.Namespace) -> None: @@ -97,6 +98,13 @@ def cmd_doctor(args: argparse.Namespace) -> None: for issue in issues: print(issue) raise SystemExit(1) - if not project_registry_entry(args.project, args): - print(f"warning: project is not registered: {args.project}") + selectors: dict[str, object] = {} + if getattr(args, "workspace_root", None): + selectors["workspace_root"] = args.workspace_root + elif getattr(args, "project_root", None): + selectors["project_root"] = args.project_root + selectors["cwd"] = args.project_root + else: + selectors["cwd"] = getattr(args, "cwd", None) or os.getcwd() + _anchor_context(**selectors) print("ok") diff --git a/scripts/keep-summarizing/migration.py b/scripts/keep-summarizing/migration.py index 1c56cf2..5c1f266 100644 --- a/scripts/keep-summarizing/migration.py +++ b/scripts/keep-summarizing/migration.py @@ -1,70 +1,5 @@ from core import * -from indexes import cmd_index, markdown_files -from registry import upsert_registry_project - -LEGACY_PERSPECTIVE_MAP = { - "architecture": "architecture/component-boundary", - "code-structure": "implementation/backend/module-structure", - "data-flow": "workflow/data-flow", - "decisions": "architecture/decisions", - "glossary": "background/glossary", - "patterns": "architecture/patterns", - "process-flow": "workflow/process-flow", -} - - -def remap_legacy_markdown(text: str, perspective: str) -> str: - mapped = LEGACY_PERSPECTIVE_MAP.get(perspective, perspective) - return re.sub(rf"^perspective:\s*{re.escape(perspective)}\s*$", f"perspective: {mapped}", text, flags=re.MULTILINE) - - -def copy_tree_markdown(source: Path, target: Path, remap_legacy_perspectives: bool = False) -> int: - count = 0 - if not source.exists(): - return count - for path in sorted(source.glob("**/*.md")): - rel = path.relative_to(source) - text = path.read_text(encoding="utf-8") - if remap_legacy_perspectives and rel.parts: - first = rel.parts[0] - if first in LEGACY_PERSPECTIVE_MAP: - mapped = Path(LEGACY_PERSPECTIVE_MAP[first]) - rel = mapped / Path(*rel.parts[1:]) - text = remap_legacy_markdown(text, first) - destination = target / rel - destination.parent.mkdir(parents=True, exist_ok=True) - if not destination.exists(): - destination.write_text(text, encoding="utf-8") - count += 1 - return count - - -def cmd_migrate_legacy(args: argparse.Namespace) -> None: - destination = project_dir(args.project, args) - legacy_base = Path(args.legacy_root).resolve() if args.legacy_root else knowledge_root().resolve() - legacy = (legacy_base / args.project).resolve() - if not legacy.exists(): - raise SystemExit(f"Legacy knowledge repo not found: {legacy}") - destination.mkdir(parents=True, exist_ok=True) - legacy_project_yaml = legacy / "project.yaml" - if legacy_project_yaml.exists(): - project_yaml = legacy_project_yaml.read_text(encoding="utf-8").replace(f"knowledge/{args.project}", ".work-bundle/knowledge") - (destination / "project.yaml").write_text(project_yaml, encoding="utf-8") - elif not (destination / "project.yaml").exists(): - write_project_yaml(destination, args.project, None) - migrated = 0 - migrated += copy_tree_markdown(legacy / "notes", destination / "notes", remap_legacy_perspectives=True) - migrated += copy_tree_markdown(legacy / "open-questions", destination / "open-questions", remap_legacy_perspectives=True) - migrated += copy_tree_markdown(legacy / "context-packs", destination / "context-packs") - handoff_source = legacy / "handoffs" - if handoff_source.exists(): - project_root = Path(getattr(args, "project_root", "") or os.getcwd()).resolve() - handoff_target = project_root / ".work-bundle" / "orchestration" / "handoff" / "orchestration" / "active" - migrated += copy_tree_markdown(handoff_source, handoff_target) - cmd_index(args) - project_root = Path(getattr(args, "project_root", "") or destination.parent.parent).resolve() - upsert_registry_project(args.project, project_root, args, name=args.project, sources=[str(project_root)]) - print(f"migrated {migrated} markdown files") +from indexes import markdown_files @@ -115,4 +50,3 @@ def cmd_migrate_v3(args: argparse.Namespace) -> None: target = migration_root / "v3-inventory.jsonl" target.write_text("\n".join(json.dumps(record, ensure_ascii=False) for record in records) + ("\n" if records else ""), encoding="utf-8") print(f"wrote {len(records)} inventory records to {target}") - diff --git a/scripts/keep-summarizing/project.py b/scripts/keep-summarizing/project.py index c2f4cfa..4a8bcd7 100644 --- a/scripts/keep-summarizing/project.py +++ b/scripts/keep-summarizing/project.py @@ -1,4 +1,5 @@ from core import * +from core import _anchor_context from indexes import cmd_index from registry import upsert_registry_project @@ -30,26 +31,11 @@ def cmd_init(args: argparse.Namespace) -> None: def cmd_resolve(args: argparse.Namespace) -> None: cwd = Path(args.cwd or os.getcwd()).resolve() - workspace = resolve_workspace_root(cwd) - if workspace: - root = work_bundle_knowledge_root(workspace) - print(read_project_slug(root, workspace.name)) - return - registry_entry = registry_entry_for_cwd(cwd, args) - if registry_entry: - print(registry_entry.get("slug")) - return - base, mode = resolve_knowledge_base(args) - if mode in {"work-bundle", "registry"}: - print(read_project_slug(base, base.parent.parent.name)) - return - for project_yaml in knowledge_root().glob("*/project.yaml"): - root = project_yaml.parent.resolve() - if cwd == root or root in cwd.parents: - print(project_yaml.parent.name) - return - text = project_yaml.read_text(encoding="utf-8") - if str(cwd) in text: - print(project_yaml.parent.name) - return - raise SystemExit("No matching project knowledge repo found.") + if getattr(args, "workspace_root", None): + context = _anchor_context(workspace_root=args.workspace_root) + elif getattr(args, "project_root", None): + context = _anchor_context(project_root=args.project_root, cwd=args.project_root) + else: + context = _anchor_context(cwd=cwd) + root = work_bundle_knowledge_root(context.workspace_root) + print(read_project_slug(root, context.workspace_root.name)) diff --git a/scripts/ks.py b/scripts/ks.py index d9d3dd8..2767344 100755 --- a/scripts/ks.py +++ b/scripts/ks.py @@ -3,6 +3,7 @@ # requires-python = ">=3.13" # dependencies = [ # "pyyaml==6.0.3", +# "jsonschema==4.25.1", # "sqlite-vec==0.1.9", # "fastembed==0.8.0", # ] @@ -26,6 +27,7 @@ RUNTIME_DEPENDENCIES = ( ("yaml", "pyyaml"), + ("jsonschema", "jsonschema"), ("sqlite_vec", "sqlite-vec"), ("fastembed", "fastembed"), ) diff --git a/scripts/orch.py b/scripts/orch.py index 7a84a32..048ef02 100755 --- a/scripts/orch.py +++ b/scripts/orch.py @@ -1,17 +1,52 @@ #!/usr/bin/env python3 +# /// script +# requires-python = ">=3.13" +# dependencies = [ +# "pyyaml==6.0.3", +# "jsonschema==4.25.1", +# ] +# /// """Compatibility entrypoint for orchestration helpers.""" from __future__ import annotations import importlib.util +import os +import shutil import sys from pathlib import Path +from typing import Mapping, Sequence + +RUNTIME_DEPENDENCIES = (("yaml", "pyyaml"), ("jsonschema", "jsonschema")) +UV_REEXEC_ENV = "WORK_BUNDLE_PUBLIC_UV_REEXEC" SCRIPT_ROOT = Path(__file__).resolve().parent sys.path.insert(0, str(SCRIPT_ROOT)) from invocation_observation import invoke_observed +def _missing_runtime_dependencies() -> list[str]: + return [distribution for module, distribution in RUNTIME_DEPENDENCIES if importlib.util.find_spec(module) is None] + + +def _ensure_managed_runtime( + *, argv: Sequence[str] | None = None, environ: Mapping[str, str] | None = None +) -> tuple[bool, str | None]: + missing = _missing_runtime_dependencies() + if not missing: + return True, None + environment = dict(os.environ if environ is None else environ) + if environment.get(UV_REEXEC_ENV) == "1": + return False, "WB_RUNTIME_DEPENDENCY_UNAVAILABLE: uv could not hydrate " + ", ".join(missing) + uv = shutil.which("uv") + if uv is None: + return False, "WB_RUNTIME_DEPENDENCY_UNAVAILABLE: install uv to provide " + ", ".join(missing) + arguments = list(sys.argv if argv is None else argv) + environment[UV_REEXEC_ENV] = "1" + os.execve(uv, [uv, "run", str(Path(__file__).resolve()), *arguments[1:]], environment) + return False, "WB_RUNTIME_DEPENDENCY_UNAVAILABLE: uv re-execution returned unexpectedly" + + def _load_dispatcher(): module_path = SCRIPT_ROOT / "orchestration" / "dispatcher.py" sys.path.insert(0, str(module_path.parent)) @@ -24,6 +59,10 @@ def _load_dispatcher(): def main() -> int: + ready, failure = _ensure_managed_runtime() + if not ready: + print(failure, file=sys.stderr) + return 1 dispatcher = _load_dispatcher() return invoke_observed( "orch", diff --git a/scripts/orchestration/README.md b/scripts/orchestration/README.md index a74a014..c855203 100644 --- a/scripts/orchestration/README.md +++ b/scripts/orchestration/README.md @@ -2,28 +2,28 @@ Implementation modules in this directory are the manual maintenance surface for orchestration helpers. -The top-level `../orch.py` entrypoint remains for compatibility with existing agent instructions. Implementation is split by artifact area (`specs.py`, `plans.py`, `handoffs.py`, `documents.py`, `doctor.py`), with `dispatcher.py` only wiring commands. +The top-level `../orch.py` entrypoint is the public command surface. Implementation is split by current artifact area (`specs.py`, `plans.py`, `handoffs.py`, `review_runtime.py`, `documents.py`, `doctor.py`), with `dispatcher.py` only wiring commands. Despite its historical module name, `handoffs.py` owns canonical `executor-result-v1` records; it does not create or read legacy handoff artifacts. Command examples: ```bash python3 scripts/orch.py write-spec --title "" --purpose "<purpose>" --component "<component>" --content-file <file> python3 scripts/orch.py write-plan --title "<title>" --purpose "<purpose>" --component "<component>" --content-file <file> +python3 scripts/orch.py write-executor-result --id <result-id> --plan-id <plan-id> --task-id <task-id> --content-file <file> +python3 scripts/orch.py write-implementation-review --id <review-id> --plan-id <plan-id> --task-id <task-id> --source-root <repository> --content-file <file> +python3 scripts/orch.py write-final-workflow-review --id <review-id> --plan-id <plan-id> --content-file <file> python3 scripts/orch.py doctor ``` -Orchestration artifacts resolve from the containing `workspace_root`, including when invoked inside a nested member. Repository inspection, tests, preflight, commits, and CodeGraph remain scoped to each selected member `project_root`. Execution consumes carried spec/plan/task/handoff context and never reads `.work-bundle/knowledge/` or credential values directly. +Orchestration artifacts resolve from the containing `workspace_root`, including when invoked inside a nested member. Repository inspection, tests, preflight, commits, and CodeGraph remain scoped to each selected member `project_root`. Execution consumes carried specification, plan, and task context and never reads `.work-bundle/knowledge/` or credential values directly. -## Reusing validation observations +Current canonical outputs are stored by family: -For deterministic source-bound checks, declare `evidence_reuse: {mode: deterministic}` in task validation. After establishing the task execution binding, run `python3 scripts/orch.py observe-task-validation --project-root <workspace> --task <task>` to record actual results before writing the handoff. `validate-executor-result`, review packaging, and completion checks reuse eligible successful evidence instead of launching the observation again. Running a command manually does not seed provenance. +- executor results: `.work-bundle/orchestration/result/executor/` +- accepted task results: `.work-bundle/orchestration/result/accepted/` +- implementation reviews: `.work-bundle/orchestration/review/implementation/` +- final workflow reviews: `.work-bundle/orchestration/review/final/` -`evaluation_identity.validation_source_identity` computes the conservative material source tree and index identity, including dirty/untracked content and declared ignored task inputs. It separates generated WorkBundle runtime/handoff/review/log evidence from source inputs. Other output-only paths must be explicitly declared; they cannot overlap declared read/dependency inputs. Source trees use Git-compatible blob/tree hashes without writing Git objects or changing the index. This is content-based evidence, so exact A → B → A restoration can reuse a fresh result. Explicit provenance revocation still advances the existing epoch. +Writers enforce schema, identity, bindings, and canonical location before mutation. Direct semantic review remains the authority for product correctness; indexes and doctor output are regenerable structural observations, not acceptance verdicts. -`completion_provenance.observe_validation` projects source, semantic check fields (ID/kind, command/mechanism, expected/acceptable results, invariant IDs and digest), task authority, runner/oracle, environment and freshness into `ObservationIdentityV1`. It uses the same `.work-bundle/runtime/completion-provenance` store that owns execution bindings; there is no separate cache store or adapter module. Old adapter records are not adopted as acceptance evidence. - -Environment identity is OS, architecture, interpreter implementation/version/binary digest, declared `dependency_files`, `profile`, and only named `environment_inputs`. Values and command output are persisted only as digests. Cwd and execution/task binding remain distinct, so local evidence cannot accidentally satisfy GitHub Ubuntu/macOS acceptance. Use `include_head: true` for exact-release-commit claims. The profile must accurately cover the actual runner; undeclared mutable tools, services or external inputs make deterministic reuse ineligible. - -Deterministic declarations default to 3600 seconds; undeclared/live checks default to fresh execution. Live checks may declare `max_age_seconds` explicitly (0–86400); skipped/failed observations are not positive reusable evidence. Legacy `reuse_seconds` is still accepted with its HEAD-bound semantics. Unsupported links/submodules or protected source inputs fall back to fresh observation. See the task contract for policy fields. - -Every acceptance call still checks handoff/task/plan identity, binding, write scope, result shape, knowledge disposition, evidence closure, authorization and Git-neutrality. Fingerprint exclusions do not grant write authority. This is not per-feature dependency pruning, and it does not replace platform-specific release/CI gates. +Use `build-implementation-review-candidate`, `write-implementation-review`, `write-accepted-task-result`, and `write-final-workflow-review` for the direct review path. Use `finalize-reviewed-plan` only after the required canonical review artifacts exist. diff --git a/scripts/orchestration/artifact_inputs.py b/scripts/orchestration/artifact_inputs.py index c76d66d..2b2a8f1 100644 --- a/scripts/orchestration/artifact_inputs.py +++ b/scripts/orchestration/artifact_inputs.py @@ -1,154 +1,24 @@ """Core-independent orchestration artifact parsing and bounded input resolution.""" from __future__ import annotations -import json -import re from pathlib import Path from typing import Any +from artifact_store import ( + canonical_artifact_path, + family_policy, + load_catalog, + read_artifact, + read_markdown_artifact, + read_yaml_mapping, +) -def _split_top_level(value: str, delimiter: str = ",") -> list[str]: - parts: list[str] = [] - start = 0 - depth = 0 - quote: str | None = None - escaped = False - for index, char in enumerate(value): - if escaped: - escaped = False - continue - if quote and char == "\\": - escaped = True - continue - if char in {'"', "'"}: - if quote == char: - quote = None - elif quote is None: - quote = char - continue - if quote: - continue - if char in "[{(": - depth += 1 - elif char in "]})": - depth -= 1 - elif char == delimiter and depth == 0: - parts.append(value[start:index].strip()) - start = index + 1 - parts.append(value[start:].strip()) - return [part for part in parts if part] - - -def _split_key_value(value: str) -> tuple[str, str]: - if ":" not in value: - raise SystemExit(f"Invalid YAML mapping entry: {value}") - key, raw = value.split(":", 1) - return key.strip().strip("'\""), raw.strip() - - -def _parse_scalar(value: str) -> Any: - value = value.strip() - if not value: - return "" - if value.startswith("[") and value.endswith("]"): - inner = value[1:-1].strip() - return [] if not inner else [_parse_scalar(part) for part in _split_top_level(inner)] - if value.startswith("{") and value.endswith("}"): - inner = value[1:-1].strip() - result: dict[str, Any] = {} - for part in _split_top_level(inner): - key, raw = _split_key_value(part) - result[key] = _parse_scalar(raw) - return result - if value[:1] == value[-1:] and value[:1] in {'"', "'"}: - if value.startswith('"'): - try: - return json.loads(value) - except json.JSONDecodeError: - pass - return value[1:-1] - lowered = value.lower() - if lowered in {"true", "false"}: - return lowered == "true" - if lowered in {"null", "none", "~"}: - return None - if re.fullmatch(r"-?\d+", value): - return int(value) - return value - - -def parse_yaml_subset(text: str) -> dict[str, Any]: - """Parse the compact YAML subset used by orchestration contracts.""" - - rows: list[tuple[int, str]] = [] - for raw in text.splitlines(): - if not raw.strip() or raw.lstrip().startswith("#"): - continue - indent = len(raw) - len(raw.lstrip(" ")) - rows.append((indent, raw.strip())) - - def parse_block(index: int, indent: int) -> tuple[Any, int]: - if index >= len(rows) or rows[index][0] < indent: - return {}, index - is_list = rows[index][0] == indent and rows[index][1].startswith("- ") - container: Any = [] if is_list else {} - while index < len(rows): - row_indent, content = rows[index] - if row_indent < indent: - break - if row_indent > indent: - raise SystemExit(f"Invalid YAML indentation near: {content}") - if is_list: - if not content.startswith("- "): - break - item_text = content[2:].strip() - if not item_text: - item, index = parse_block(index + 1, indent + 2) - elif item_text.startswith("{"): - item = _parse_scalar(item_text) - index += 1 - elif ":" in item_text and not item_text.startswith(("'", '"', "`")): - key, raw_value = _split_key_value(item_text) - item = {key: _parse_scalar(raw_value)} - index += 1 - if index < len(rows) and rows[index][0] > indent: - continuation, index = parse_block(index, indent + 2) - if not isinstance(continuation, dict): - raise SystemExit(f"Invalid YAML list mapping near: {item_text}") - item.update(continuation) - else: - item = _parse_scalar(item_text) - index += 1 - container.append(item) - continue - - if content.startswith("- "): - break - key, raw_value = _split_key_value(content) - index += 1 - if raw_value: - container[key] = _parse_scalar(raw_value) - elif index < len(rows) and rows[index][0] > indent: - container[key], index = parse_block(index, rows[index][0]) - else: - container[key] = {} - return container, index - - if not rows: - return {} - parsed, index = parse_block(0, rows[0][0]) - if index != len(rows) or not isinstance(parsed, dict): - raise SystemExit("Expected a YAML mapping") - return parsed - +SPEC_CATALOG = Path(__file__).resolve().parents[2] / "references/assets/orchestration/contract/artifact-family-catalog-v3.yaml" def _read_structured(path: Path) -> tuple[dict[str, Any], str]: raw = path.read_text(encoding="utf-8") if raw.startswith("---\n"): - end = raw.find("\n---\n", 4) - if end < 0: - raise SystemExit(f"Unterminated front matter: {path}") - return parse_yaml_subset(raw[4:end]), raw[end + 5 :] - return parse_yaml_subset(raw), "" + return read_markdown_artifact(path) + return read_yaml_mapping(path), "" def _as_list(value: Any) -> list[Any]: @@ -171,22 +41,26 @@ def _input_path(raw: str | Path, root: Path, allowed: Path, label: str) -> Path: def _resolve_spec_paths(root: Path, task_data: dict[str, Any], plan_data: dict[str, Any]) -> list[Path]: - references = _as_list(task_data.get("source_spec")) or _as_list(plan_data.get("source_spec")) + if "source_spec" in task_data or "source_spec" in plan_data: + raise SystemExit("Legacy source_spec aliases are unsupported; use source_spec_id") + references = _as_list(task_data.get("source_spec_id")) or _as_list(plan_data.get("source_spec_id")) if not references: - raise SystemExit("Task/root plan does not declare source_spec") - spec_root = root / ".work-bundle/orchestration/spec" + raise SystemExit("Task/root plan does not declare source_spec_id") + policy = family_policy(load_catalog(SPEC_CATALOG), "specification") + anchors = {"workspace_root": root} result: list[Path] = [] for reference in references: raw = str(reference) if "/" in raw or raw.endswith(".md"): - result.append(_input_path(raw, root, spec_root, "source specification")) - continue - matches: list[Path] = [] - for candidate in sorted(spec_root.glob("*/*.md")): - data, _ = _read_structured(candidate) - if str(data.get("id", "")) == raw: - matches.append(candidate) + raise SystemExit("Source specification paths are unsupported; use source_spec_id") + matches = [ + (state, canonical_artifact_path(policy, anchors, identity=raw, state=state)) + for state in ("active", "archived") + ] + matches = [(state, path) for state, path in matches if path.is_file()] if len(matches) != 1: - raise SystemExit(f"Expected one source specification for {raw}; found {len(matches)}") - result.append(matches[0]) + raise SystemExit(f"Source specification not found at canonical location for {raw}") + state, path = matches[0] + read_artifact(SPEC_CATALOG, "specification", anchors, identity=raw, state=state) + result.append(path) return result diff --git a/scripts/orchestration/artifact_store.py b/scripts/orchestration/artifact_store.py new file mode 100644 index 0000000..75b0a61 --- /dev/null +++ b/scripts/orchestration/artifact_store.py @@ -0,0 +1,530 @@ +"""Schema-backed structural mechanics for registered orchestration artifacts. + +This module reports structural facts and failures. It does not decide semantic +correctness, evidence sufficiency, qualification, review, or acceptance. +""" +from __future__ import annotations + +import copy +import hashlib +import json +import os +from pathlib import Path +import re +import stat +import string +import tempfile +from typing import Any, Mapping + +import jsonschema +import yaml + + +_CATALOG_SCHEMA = ( + Path(__file__).resolve().parents[2] + / "references/assets/orchestration/contract/artifact-family-catalog-v1.schema.json" +) + + +class _MaintainedLoader(yaml.SafeLoader): + """Safe maintained YAML with date-like orchestration scalars kept as text.""" + + +_MaintainedLoader.yaml_implicit_resolvers = copy.deepcopy(yaml.SafeLoader.yaml_implicit_resolvers) +for _key, _resolvers in list(_MaintainedLoader.yaml_implicit_resolvers.items()): + _MaintainedLoader.yaml_implicit_resolvers[_key] = [ + resolver for resolver in _resolvers if resolver[0] != "tag:yaml.org,2002:timestamp" + ] + + +def _fail(message: str) -> None: + raise SystemExit(message) + + +def _yaml_mapping(text: str, *, source: str) -> dict[str, Any]: + try: + value = yaml.load(text, Loader=_MaintainedLoader) + except yaml.YAMLError as exc: + _fail(f"Invalid YAML in {source}: {exc}") + if not isinstance(value, dict): + _fail(f"Expected a YAML mapping in {source}") + return value + + +def read_yaml_mapping(path: Path) -> dict[str, Any]: + return _yaml_mapping(path.read_text(encoding="utf-8"), source=str(path)) + + +def read_markdown_artifact(path: Path) -> tuple[dict[str, Any], str]: + text = path.read_text(encoding="utf-8") + return parse_markdown_artifact(text, source=str(path)) + + +def parse_markdown_artifact(text: str, *, source: str = "content") -> tuple[dict[str, Any], str]: + if not text.startswith("---\n"): + _fail(f"Missing Markdown front matter: {source}") + end = text.find("\n---\n", 4) + if end < 0: + _fail(f"Unterminated front matter: {source}") + return _yaml_mapping(text[4:end], source=f"{source} front matter"), text[end + 5 :] + + +def parse_yaml_mapping(text: str, *, source: str = "content") -> dict[str, Any]: + return _yaml_mapping(text, source=source) + + +def parse_yaml_value(text: str, *, source: str = "content") -> Any: + try: + return yaml.load(text, Loader=_MaintainedLoader) + except yaml.YAMLError as exc: + _fail(f"Invalid YAML in {source}: {exc}") + + +def _json_mapping(path: Path) -> dict[str, Any]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + _fail(f"Invalid JSON Schema {path}: {exc}") + if not isinstance(value, dict): + _fail(f"Expected JSON Schema mapping: {path}") + return value + + +def _schema_path(catalog_path: Path, policy: Mapping[str, Any]) -> Path: + raw = str(policy["schema"]["path"]) + candidate = Path(raw) + if candidate.is_absolute(): + _fail(f"Artifact schema path escapes catalog directory: {raw}") + resolved = (catalog_path.parent / candidate).resolve() + catalog_root = catalog_path.parent.resolve() + if resolved != catalog_root and catalog_root not in resolved.parents: + _fail(f"Artifact schema path escapes catalog directory: {raw}") + if not resolved.is_file(): + _fail(f"Artifact schema not found: {resolved}") + return resolved + + +def _safe_relative(raw: str, *, label: str, reject_glob: bool = False) -> Path: + path = Path(raw) + segments = raw.split("/") + if ( + path.is_absolute() + or not raw + or "\\" in raw + or any(part in {"", ".", ".."} for part in segments) + or path.as_posix() != raw + ): + _fail(f"{label} is not a canonical relative path: {raw}") + if reject_glob and any(character in raw for character in "*?[]"): + _fail(f"{label} contains literal glob syntax: {raw}") + return path + + +def load_catalog(catalog_path: Path) -> dict[str, Any]: + catalog_path = catalog_path.resolve() + catalog = read_yaml_mapping(catalog_path) + schema = _json_mapping(_CATALOG_SCHEMA) + try: + jsonschema.Draft202012Validator(schema).validate(catalog) + except jsonschema.ValidationError as exc: + _fail(f"Invalid artifact-family catalog {catalog_path}: {exc.message}") + names: set[str] = set() + for policy in catalog["families"]: + name = str(policy["name"]) + if name in names: + _fail(f"Duplicate artifact family: {name}") + names.add(name) + schema_path = _schema_path(catalog_path, policy) + artifact_schema = _json_mapping(schema_path) + expected = str(policy["schema"]["id"]) + if artifact_schema.get("$id") != expected: + _fail( + f"Artifact schema identity mismatch for {name}: " + f"expected {expected}, found {artifact_schema.get('$id', '<missing>')}" + ) + binding_names = [str(item["name"]) for item in policy["relationships"]["bindings"]] + binding_fields = [str(item["field"]) for item in policy["relationships"]["bindings"]] + if len(binding_names) != len(set(binding_names)) or len(binding_fields) != len(set(binding_fields)): + _fail(f"Incomplete artifact binding policy for {name}: duplicate name or field") + locator_template = str(policy["locator"]["template"]) + _safe_relative( + locator_template, + label=f"Artifact locator policy for {name}", + reject_glob=True, + ) + parsed_template = list(string.Formatter().parse(locator_template)) + if any(format_spec or conversion for _literal, _field, format_spec, conversion in parsed_template): + _fail(f"Incomplete artifact locator policy for {name}: formatting is not supported") + placeholders = { + field_name + for _literal, field_name, _format, _conversion in parsed_template + if field_name is not None + } + locator_variables = set(policy["locator"]["variables"]) + if placeholders != locator_variables: + _fail(f"Incomplete artifact locator policy for {name}: variables do not match template") + supported_locator_variables = {"id", "state", *binding_names} + unsupported_locator_variables = sorted(locator_variables - supported_locator_variables) + if unsupported_locator_variables: + _fail( + f"Incomplete artifact locator policy for {name}: unsupported variables: " + f"{', '.join(unsupported_locator_variables)}" + ) + states = set(policy["lifecycle"]["states"]) + transitions = policy["lifecycle"]["transitions"] + if not set(transitions).issubset(states) or any( + not set(targets).issubset(states) for targets in transitions.values() + ): + _fail(f"Incomplete artifact lifecycle policy for {name}: unknown state") + if policy["lifecycle"]["authority"] == "location": + if "id" not in locator_variables: + _fail(f"Incomplete artifact locator policy for {name}: does not distinguish identity") + if (len(states) > 1 or any(transitions.values())) and "state" not in locator_variables: + _fail( + f"Incomplete artifact locator policy for {name}: " + "does not distinguish lifecycle state" + ) + index = policy["index"] + if index.get("policy") != "none": + _safe_relative( + str(index["path"]), + label=f"Artifact index policy for {name}", + reject_glob=True, + ) + if ( + not set(index["source_states"]).issubset(states) + or policy["identity"]["field"] not in index["projection"] + ): + _fail(f"Incomplete artifact index policy for {name}") + return catalog + + +def family_policy(catalog: Mapping[str, Any], family: str) -> dict[str, Any]: + matches = [item for item in catalog.get("families", []) if item.get("name") == family] + if len(matches) != 1: + _fail(f"Unregistered artifact family: {family}") + return dict(matches[0]) + + +def _validated_bindings( + policy: Mapping[str, Any], + data: Mapping[str, Any], + bindings: Mapping[str, str] | None, + *, + derive_from_data: bool = False, +) -> dict[str, str]: + supplied = dict(bindings or {}) + result: dict[str, str] = {} + definitions = policy["relationships"]["bindings"] + declared_names = {str(binding["name"]) for binding in definitions} + unknown = sorted(set(supplied) - declared_names) + if unknown: + _fail(f"Unknown artifact binding: {', '.join(unknown)}") + for binding in definitions: + name = str(binding["name"]) + field = str(binding["field"]) + actual = data.get(field) + expected = supplied.get(name) + if derive_from_data and expected is None and actual is not None: + expected = str(actual) + if binding["required"] and expected is None: + _fail(f"Missing required artifact binding: {name}") + if expected is not None and str(actual) != str(expected): + _fail(f"Artifact binding mismatch for {name}: expected {expected}, found {actual}") + if expected is not None: + result[name] = str(expected) + return result + + +def validate_artifact( + policy: Mapping[str, Any], + data: Mapping[str, Any], + *, + catalog_path: Path, + bindings: Mapping[str, str] | None = None, + derive_bindings: bool = False, +) -> dict[str, str]: + identity_field = str(policy["identity"]["field"]) + identity = data.get(identity_field) + if not isinstance(identity, str) or re.fullmatch(str(policy["identity"]["pattern"]), identity) is None: + _fail(f"Invalid artifact identity for {identity_field}: {identity!r}") + schema_path = _schema_path(catalog_path.resolve(), policy) + schema = _json_mapping(schema_path) + try: + jsonschema.Draft202012Validator(schema).validate(dict(data)) + except jsonschema.ValidationError as exc: + _fail(f"Artifact schema validation failed for {policy['name']}: {exc.message}") + return _validated_bindings( + policy, data, bindings, derive_from_data=derive_bindings + ) + + +def canonical_artifact_path( + policy: Mapping[str, Any], + anchors: Mapping[str, Path], + *, + identity: str, + state: str, + bindings: Mapping[str, str] | None = None, +) -> Path: + if re.fullmatch(str(policy["identity"]["pattern"]), identity) is None: + _fail(f"Invalid artifact identity: {identity}") + anchor_name = str(policy["anchor"]) + if anchor_name not in anchors: + _fail(f"Missing artifact anchor: {anchor_name}") + states = [str(item) for item in policy["lifecycle"]["states"]] + if state not in states: + _fail(f"Invalid artifact lifecycle state for {policy['name']}: {state}") + variables = {"id": identity, "state": state, **dict(bindings or {})} + declared = set(policy["locator"]["variables"]) + missing = sorted(declared - set(variables)) + if missing: + _fail(f"Missing artifact locator variables: {', '.join(missing)}") + try: + rendered = str(policy["locator"]["template"]).format(**{key: variables[key] for key in declared}) + except (KeyError, ValueError) as exc: + _fail(f"Invalid artifact locator template for {policy['name']}: {exc}") + relative = _safe_relative(rendered, label="Artifact locator") + anchor = Path(anchors[anchor_name]).resolve() + target = (anchor / relative).resolve() + if target != anchor and anchor not in target.parents: + _fail(f"Artifact locator escapes anchor: {rendered}") + return target + + +def serialize_artifact( + policy: Mapping[str, Any], data: Mapping[str, Any], body: str | None = None +) -> bytes: + encoded = yaml.safe_dump(dict(data), allow_unicode=True, sort_keys=True) + if policy["representation"] == "yaml": + if body not in {None, ""}: + _fail(f"YAML artifact family does not accept a semantic body: {policy['name']}") + return encoded.encode("utf-8") + if policy["representation"] == "markdown-front-matter": + semantic_body = (body or "").rstrip() + return (f"---\n{encoded}---\n" + (f"{semantic_body}\n" if semantic_body else "")).encode("utf-8") + _fail(f"Unsupported artifact representation: {policy['representation']}") + + +def serialize_markdown_mapping(data: Mapping[str, Any], body: str) -> bytes: + encoded = yaml.safe_dump(dict(data), allow_unicode=True, sort_keys=False) + return (f"---\n{encoded}---\n" + body).encode("utf-8") + + +def atomic_write_bytes(path: Path, content: bytes) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + existing_mode = stat.S_IMODE(path.stat().st_mode) if path.exists() else None + if existing_mode is None: + current_umask = os.umask(0) + os.umask(current_umask) + creation_mode = 0o666 & ~current_umask + else: + creation_mode = existing_mode + descriptor, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent) + try: + with os.fdopen(descriptor, "wb") as stream: + stream.write(content) + stream.flush() + os.fsync(stream.fileno()) + os.chmod(temporary, creation_mode) + os.replace(temporary, path) + except BaseException: + try: + os.unlink(temporary) + except FileNotFoundError: + pass + raise + + +def _read_stored( + catalog_path: Path, + policy: Mapping[str, Any], + path: Path, + *, + bindings: Mapping[str, str] | None, + derive_bindings: bool = False, +) -> tuple[dict[str, Any], str, dict[str, str]]: + if policy["representation"] == "yaml": + data, body = read_yaml_mapping(path), "" + else: + data, body = read_markdown_artifact(path) + validated = validate_artifact( + policy, + data, + catalog_path=catalog_path, + bindings=bindings, + derive_bindings=derive_bindings, + ) + return data, body, validated + + +def read_artifact( + catalog_path: Path, + family: str, + anchors: Mapping[str, Path], + *, + identity: str, + state: str, + bindings: Mapping[str, str] | None = None, +) -> dict[str, Any]: + catalog_path = catalog_path.resolve() + policy = family_policy(load_catalog(catalog_path), family) + path = canonical_artifact_path( + policy, anchors, identity=identity, state=state, bindings=bindings + ) + if not path.is_file(): + _fail(f"Artifact not found at canonical location: {path}") + data, body, validated = _read_stored(catalog_path, policy, path, bindings=bindings) + if str(data[policy["identity"]["field"]]) != identity: + _fail(f"Artifact identity does not match canonical location: {path}") + result = _result( + policy, path, data, validated, state=state, index_effect="none", partial_effect=False + ) + result["data"] = data + result["body"] = body + return result + + +def _result( + policy: Mapping[str, Any], path: Path, data: Mapping[str, Any], bindings: Mapping[str, str], + *, state: str, index_effect: str, partial_effect: bool, +) -> dict[str, Any]: + return { + "family": policy["name"], + "identity": data[policy["identity"]["field"]], + "path": str(path), + "schema": policy["schema"]["id"], + "state": state, + "digest": hashlib.sha256(path.read_bytes()).hexdigest(), + "validated_bindings": dict(bindings), + "index_effect": index_effect, + "partial_effect": partial_effect, + } + + +def write_artifact( + catalog_path: Path, + family: str, + anchors: Mapping[str, Path], + data: Mapping[str, Any], + *, + state: str, + bindings: Mapping[str, str] | None = None, + body: str | None = None, + rebuild: bool = True, +) -> dict[str, Any]: + catalog_path = catalog_path.resolve() + policy = family_policy(load_catalog(catalog_path), family) + validated = validate_artifact(policy, data, catalog_path=catalog_path, bindings=bindings) + identity = str(data[policy["identity"]["field"]]) + path = canonical_artifact_path(policy, anchors, identity=identity, state=state, bindings=validated) + content = serialize_artifact(policy, data, body) + atomic_write_bytes(path, content) + stored, _stored_body, stored_bindings = _read_stored( + catalog_path, policy, path, bindings=validated + ) + expected = canonical_artifact_path( + policy, anchors, identity=str(stored[policy["identity"]["field"]]), state=state, bindings=stored_bindings + ) + if expected != path: + _fail(f"Stored artifact is in the wrong canonical location: {path}") + index_effect = "not-requested" + if rebuild and policy["index"].get("policy") != "none": + try: + rebuild_index(catalog_path, family, anchors) + index_effect = "rebuilt" + except (OSError, SystemExit) as exc: + _fail(f"Artifact was written but index rebuild failed (partial effect): {exc}") + return _result( + policy, path, stored, stored_bindings, state=state, + index_effect=index_effect, partial_effect=False, + ) + + +def transition_artifact( + catalog_path: Path, + family: str, + anchors: Mapping[str, Path], + *, + identity: str, + current_state: str, + target_state: str, + bindings: Mapping[str, str] | None = None, +) -> dict[str, Any]: + catalog_path = catalog_path.resolve() + policy = family_policy(load_catalog(catalog_path), family) + source = canonical_artifact_path(policy, anchors, identity=identity, state=current_state, bindings=bindings) + if not source.is_file(): + _fail(f"Artifact source not found: {source}") + data, _body, validated = _read_stored(catalog_path, policy, source, bindings=bindings) + canonical_source = canonical_artifact_path( + policy, anchors, identity=str(data[policy["identity"]["field"]]), state=current_state, bindings=validated + ) + if canonical_source != source: + _fail(f"Artifact source is in the wrong canonical location: {source}") + if target_state == current_state: + return _result(policy, source, data, validated, state=current_state, index_effect="none", partial_effect=False) + allowed = policy["lifecycle"]["transitions"].get(current_state, []) + if target_state not in allowed: + _fail(f"Artifact lifecycle transition is not allowed: {current_state} -> {target_state}") + target = canonical_artifact_path(policy, anchors, identity=identity, state=target_state, bindings=validated) + if target.exists(): + _fail(f"Artifact lifecycle destination collision: {target}") + target.parent.mkdir(parents=True, exist_ok=True) + os.replace(source, target) + moved, _moved_body, moved_bindings = _read_stored(catalog_path, policy, target, bindings=validated) + return _result(policy, target, moved, moved_bindings, state=target_state, index_effect="not-rebuilt", partial_effect=False) + + +def _candidate_pattern(policy: Mapping[str, Any], state: str) -> str: + template = str(policy["locator"]["template"]) + values = {name: "*" for name in policy["locator"]["variables"]} + values["state"] = state + try: + return template.format(**values) + except (KeyError, ValueError) as exc: + _fail(f"Invalid artifact index locator for {policy['name']}: {exc}") + + +def rebuild_index( + catalog_path: Path, family: str, anchors: Mapping[str, Path] +) -> dict[str, Any]: + catalog_path = catalog_path.resolve() + policy = family_policy(load_catalog(catalog_path), family) + index_policy = policy["index"] + if index_policy.get("policy") == "none": + _fail(f"Artifact family has no index policy: {family}") + anchor = Path(anchors[str(policy["anchor"])]).resolve() + rows: list[dict[str, Any]] = [] + identities: set[str] = set() + for state in index_policy["source_states"]: + pattern = _candidate_pattern(policy, str(state)) + for candidate in sorted(anchor.glob(pattern)): + try: + data, _body, bindings = _read_stored( + catalog_path, + policy, + candidate, + bindings=None, + derive_bindings=True, + ) + identity = str(data[policy["identity"]["field"]]) + expected = canonical_artifact_path( + policy, anchors, identity=identity, state=str(state), bindings=bindings + ) + if candidate.resolve() != expected: + _fail(f"wrong canonical placement: expected {expected}") + if identity in identities: + _fail(f"duplicate identity: {identity}") + identities.add(identity) + rows.append({field: data[field] for field in index_policy["projection"]}) + except (OSError, SystemExit) as exc: + _fail(f"Invalid index candidate {candidate}: {exc}") + rows.sort(key=lambda row: str(row[policy["identity"]["field"]])) + index_relative = _safe_relative(str(index_policy["path"]), label="Artifact index path") + target = (anchor / index_relative).resolve() + if target != anchor and anchor not in target.parents: + _fail(f"Artifact index path escapes anchor: {index_policy['path']}") + content = "".join(json.dumps(row, ensure_ascii=False, sort_keys=True) + "\n" for row in rows).encode("utf-8") + atomic_write_bytes(target, content) + return {"path": str(target), "count": len(rows), "digest": hashlib.sha256(content).hexdigest()} diff --git a/scripts/orchestration/bounded_closure.py b/scripts/orchestration/bounded_closure.py index 0d772f7..cc95ddb 100644 --- a/scripts/orchestration/bounded_closure.py +++ b/scripts/orchestration/bounded_closure.py @@ -1,40 +1,31 @@ #!/usr/bin/env python3 -"""Canonical bounded post-execution review-round state owner. +"""Workspace admission against portable orchestration blockers. -The append-only round ledger is controller state. Portable metadata receives -only the latest per-flow projection, so plan revisions and review artifacts can -never become the counter authority. +This module intentionally owns no review rounds, reviewer process, receipts, +publication, or workflow finalization. Its current surface is limited to +workspace discovery, admission checks, and scoped blocker-exception cleanup. """ from __future__ import annotations from contextlib import contextmanager -from datetime import datetime, timezone import fcntl import hashlib -import json import os from pathlib import Path import re -import subprocess import tempfile -from typing import Any, Iterator, Mapping, Sequence +from typing import Any, Iterator, Mapping import yaml -ROUND_LIMIT = 5 -LEDGER_SCHEMA = "post-execution-review-ledger-v1" -FLOW_STATES_KEY = "post_execution_review_flows" -TERMINAL_ATTEMPT_STATES = frozenset( - {"completed", "blocked", "partial", "failed", "cancelled"} -) -ROUND_OUTCOMES = frozenset({"accepted", "findings", "blocked"}) -ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]*$") SHA256_RE = re.compile(r"^[0-9a-f]{64}$") -GIT_OID_RE = re.compile(r"^[0-9a-f]{40}$") ADMISSION_OPERATIONS = frozenset({ - "ordinary_new", "reconciliation", "round_completion", "read_only", - "blocker_recording", "knowledge_return", "finalization", + "ordinary_new", + "reconciliation", + "read_only", + "blocker_recording", + "knowledge_return", }) MUTATING_ADMISSION_OPERATIONS = frozenset({"ordinary_new", "reconciliation"}) @@ -46,43 +37,6 @@ def __init__(self, code: str, detail: str | None = None) -> None: self.detail = detail -def _utc_now() -> str: - return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") - - -def _identifier(value: object, field: str) -> str: - if not isinstance(value, str) or not ID_RE.fullmatch(value): - raise BoundedClosureError("WB_POST_EXECUTION_INPUT_INVALID", field) - return value - - -def _target_identity(value: object) -> dict[str, object]: - if not isinstance(value, Mapping): - raise BoundedClosureError("WB_POST_EXECUTION_INPUT_INVALID", "target_identity") - required = {"artifact_id", "revision", "sha256", "source_tree"} - if set(value) != required: - raise BoundedClosureError("WB_POST_EXECUTION_INPUT_INVALID", "target_identity") - artifact_id = _identifier(value.get("artifact_id"), "target_identity.artifact_id") - revision = value.get("revision") - digest = value.get("sha256") - source_tree = value.get("source_tree") - if ( - not isinstance(revision, str) - or not revision - or not isinstance(digest, str) - or not SHA256_RE.fullmatch(digest) - or not isinstance(source_tree, str) - or not source_tree - ): - raise BoundedClosureError("WB_POST_EXECUTION_INPUT_INVALID", "target_identity") - return { - "artifact_id": artifact_id, - "revision": revision, - "sha256": digest, - "source_tree": source_tree, - } - - def _workspace_root(value: Path) -> Path: root = value.expanduser().resolve() metadata = root / ".work-bundle/project.yaml" @@ -136,7 +90,10 @@ def resolve_working_workspace(start: Path, *, workspace_id: str | None = None) - if isinstance(item, Mapping) and isinstance(item.get("project_root"), str) ) bound_identity = workspace_id or next((part for part in current.parts if part == identity), None) - if bound_identity == identity and (workspace_id is not None or any(path == current or path in current.parents for path in locators)): + if bound_identity == identity and ( + workspace_id is not None + or any(path == current or path in current.parents for path in locators) + ): return root if (root / ".work-bundle/project.yaml").is_file() else None return None @@ -159,14 +116,7 @@ def _policy(metadata: Mapping[str, Any], *, required: bool) -> Mapping[str, Any] control = metadata.get("orchestration_control") if control is None and not required: return None - if not isinstance(control, Mapping): - raise BoundedClosureError("WB_POST_EXECUTION_POLICY_INVALID") - if "review_revision_limit" in control: - raise BoundedClosureError("WB_POST_EXECUTION_LEGACY_POLICY_REJECTED") - if ( - control.get("schema_version") != 1 - or control.get("post_execution_review_round_limit") != ROUND_LIMIT - ): + if not isinstance(control, Mapping) or control.get("schema_version") != 1: raise BoundedClosureError("WB_POST_EXECUTION_POLICY_INVALID") return control @@ -188,13 +138,16 @@ def _active_blockers(root: Path, control: Mapping[str, Any]) -> list[Mapping[str blocker_id = blocker.get("id") reference = blocker.get("specification") if not isinstance(blocker_id, str) or not blocker_id or not isinstance(reference, str) or not reference: - raise BoundedClosureError("WB_ORCHESTRATION_BLOCKER_EVIDENCE_INVALID", str(_metadata_path(root))) + raise BoundedClosureError( + "WB_ORCHESTRATION_BLOCKER_EVIDENCE_INVALID", str(_metadata_path(root)) + ) candidate = Path(reference) spec = candidate.resolve(strict=False) if candidate.is_absolute() else (root / candidate).resolve(strict=False) if not spec.is_relative_to(store) or spec.is_symlink() or not spec.is_file(): raise BoundedClosureError( "WB_ORCHESTRATION_BLOCKER_EVIDENCE_INVALID", - f"metadata={_metadata_path(root)} blocker={blocker_id} specification={spec} rule=orch-bounded-closure; read the metadata entry and blocking specification", + f"metadata={_metadata_path(root)} blocker={blocker_id} specification={spec} " + "rule=orch-bounded-closure; read the metadata entry and blocking specification", ) active.append(blocker) return active @@ -203,7 +156,7 @@ def _active_blockers(root: Path, control: Mapping[str, Any]) -> list[Mapping[str def require_orchestration_admission( root: Path, *, operation: str, flow_id: str | None ) -> dict[str, object]: - """Guard one operation using portable state from the resolved working workspace.""" + """Guard one operation using portable state from the resolved workspace.""" workspace = _workspace_root(root) if operation not in ADMISSION_OPERATIONS: @@ -217,17 +170,7 @@ def require_orchestration_admission( control = _policy(metadata, required=False) assert control is not None blockers = _active_blockers(workspace, control) - flows = _control_list(control, FLOW_STATES_KEY) - if operation == "reconciliation": - if not isinstance(flow_id, str) or not flow_id: - raise BoundedClosureError("WB_ORCHESTRATION_FLOW_REQUIRED") - state = next((item for item in flows if item.get("flow_id") == flow_id), None) - if state is not None and state.get("finalization_required") is True: - raise BoundedClosureError( - "WB_ORCHESTRATION_FINALIZATION_REQUIRED", - f"flow={flow_id} metadata={_metadata_path(workspace)} rule=orch-bounded-closure", - ) - if operation in MUTATING_ADMISSION_OPERATIONS and blockers: + if blockers: exemptions = _control_list(control, "implementation_exemptions") exempt_ids = { item.get("blocker_id") @@ -240,67 +183,21 @@ def require_orchestration_admission( spec = Path(spec_ref) if Path(spec_ref).is_absolute() else workspace / spec_ref raise BoundedClosureError( "WB_ORCHESTRATION_ADMISSION_BLOCKED", - f"metadata={_metadata_path(workspace)} blocker={denied['id']} entry=orchestration_control.blockers specification={spec.resolve()} rule=orch-bounded-closure; read the metadata entry and blocking specification", + f"metadata={_metadata_path(workspace)} blocker={denied['id']} " + f"entry=orchestration_control.blockers specification={spec.resolve()} " + "rule=orch-bounded-closure; read the metadata entry and blocking specification", ) return {"status": "admitted", "legacy": False, "workspace": str(workspace)} -def restore_implementation_exception( - root: Path, *, backup_path: Path, backup_sha256: str, flow_id: str, blocker_id: str -) -> bool: - """Retire one scoped exception and merge its exact original blocker back.""" - - workspace = _workspace_root(root) - backup = backup_path.expanduser().resolve() - try: - content = backup.read_bytes() - except OSError as error: - raise BoundedClosureError("WB_ORCHESTRATION_BACKUP_INVALID") from error - if not SHA256_RE.fullmatch(backup_sha256) or hashlib.sha256(content).hexdigest() != backup_sha256: - raise BoundedClosureError("WB_ORCHESTRATION_BACKUP_INVALID") - try: - original = yaml.safe_load(content) - except yaml.YAMLError as error: - raise BoundedClosureError("WB_ORCHESTRATION_BACKUP_INVALID") from error - if not isinstance(original, Mapping) or not isinstance(original.get("orchestration_control"), Mapping): - raise BoundedClosureError("WB_ORCHESTRATION_BACKUP_INVALID") - original_control = original["orchestration_control"] - original_blockers = _control_list(original_control, "blockers") - original_blocker = next((dict(item) for item in original_blockers if item.get("id") == blocker_id), None) - if original_blocker is None: - raise BoundedClosureError("WB_ORCHESTRATION_BACKUP_INVALID", blocker_id) - with _locked(workspace): - metadata = _load_metadata(workspace) - control = _policy(metadata, required=True) - assert isinstance(control, dict) - exemptions = _control_list(control, "implementation_exemptions") - retained = [dict(item) for item in exemptions if not (item.get("flow_id") == flow_id and item.get("blocker_id") == blocker_id)] - blockers = [dict(item) for item in _control_list(control, "blockers") if item.get("id") != blocker_id] - blockers.append(original_blocker) - control["implementation_exemptions"] = retained - control["blockers"] = blockers - _atomic_write(_metadata_path(workspace), yaml.safe_dump(metadata, sort_keys=False, allow_unicode=True).encode("utf-8")) - return True - - -def bounded_review_enabled(root: Path) -> bool: - """Return whether the optional current workspace policy is enabled.""" - - workspace = root.expanduser().resolve() - if not _metadata_path(workspace).is_file(): - return False - return _policy(_load_metadata(workspace), required=False) is not None - - -def _state_paths(root: Path) -> tuple[Path, Path]: - directory = root / ".work-bundle/runtime/orchestration-control" - return directory / "post-execution-review-rounds-v1.json", directory / ".rounds.lock" +def _lock_path(root: Path) -> Path: + return root / ".work-bundle/runtime/orchestration-control/.admission.lock" @contextmanager def _locked(root: Path) -> Iterator[None]: - state_path, lock_path = _state_paths(root) - state_path.parent.mkdir(parents=True, exist_ok=True) + lock_path = _lock_path(root) + lock_path.parent.mkdir(parents=True, exist_ok=True) with lock_path.open("a+b") as stream: fcntl.flock(stream.fileno(), fcntl.LOCK_EX) try: @@ -309,23 +206,6 @@ def _locked(root: Path) -> Iterator[None]: fcntl.flock(stream.fileno(), fcntl.LOCK_UN) -def _load_ledger(root: Path) -> dict[str, Any]: - path, _ = _state_paths(root) - if not path.exists(): - return {"schema": LEDGER_SCHEMA, "flows": {}} - try: - value = json.loads(path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError) as error: - raise BoundedClosureError("WB_POST_EXECUTION_LEDGER_INVALID") from error - if ( - not isinstance(value, dict) - or value.get("schema") != LEDGER_SCHEMA - or not isinstance(value.get("flows"), dict) - ): - raise BoundedClosureError("WB_POST_EXECUTION_LEDGER_INVALID") - return value - - def _atomic_write(path: Path, content: bytes) -> None: path.parent.mkdir(parents=True, exist_ok=True) descriptor, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent) @@ -345,948 +225,60 @@ def _atomic_write(path: Path, content: bytes) -> None: temporary.unlink(missing_ok=True) -def _write_ledger(root: Path, ledger: Mapping[str, Any]) -> None: - path, _ = _state_paths(root) - _atomic_write(path, (json.dumps(ledger, indent=2, sort_keys=True) + "\n").encode()) - - -def _flow_projection(flow: Mapping[str, Any]) -> dict[str, Any]: - rounds = flow["rounds"] - latest = rounds[-1] if rounds else None - completed = [item for item in rounds if item["state"] == "completed"] - projection = { - "flow_id": flow["flow_id"], - "execution_complete": flow["execution_complete"], - "latest_reserved_round_id": latest["round_id"] if latest else None, - "latest_completed_round_id": completed[-1]["round_id"] if completed else None, - "frozen_target_identity": latest["target_identity"] if latest else None, - "outcome": completed[-1]["outcome"] if completed else None, - "finalization_required": bool(flow.get("finalization_required")), - } - finalization = flow.get("finalization") - if isinstance(finalization, Mapping): - projection["finalization_state"] = finalization.get("state") - projection["finalization_outcome"] = finalization.get("outcome") - return projection - - -def _write_metadata_projection(root: Path, metadata: dict[str, Any], ledger: Mapping[str, Any]) -> None: - control = metadata["orchestration_control"] - assert isinstance(control, dict) - control[FLOW_STATES_KEY] = [ - _flow_projection(flow) - for _, flow in sorted(ledger["flows"].items()) - ] - rendered = yaml.safe_dump(metadata, sort_keys=False, allow_unicode=True).encode("utf-8") - _atomic_write(_metadata_path(root), rendered) - - -def _terminal_attempts(values: Sequence[Mapping[str, Any]]) -> list[dict[str, str]]: - if isinstance(values, (str, bytes)) or not isinstance(values, Sequence): - raise BoundedClosureError("WB_POST_EXECUTION_INPUT_INVALID", "executor_attempts") - attempts: list[dict[str, str]] = [] - identities: set[str] = set() - for raw in values: - if not isinstance(raw, Mapping): - raise BoundedClosureError("WB_POST_EXECUTION_INPUT_INVALID", "executor_attempts") - identity = _identifier(raw.get("execution_id"), "executor_attempts.execution_id") - state = raw.get("state") - if identity in identities or not isinstance(state, str): - raise BoundedClosureError("WB_POST_EXECUTION_INPUT_INVALID", "executor_attempts") - if state not in TERMINAL_ATTEMPT_STATES: - raise BoundedClosureError("WB_POST_EXECUTION_NOT_TERMINAL", identity) - if raw.get("mutation_active") is True or raw.get("active") is True: - raise BoundedClosureError("WB_POST_EXECUTION_NOT_TERMINAL", identity) - identities.add(identity) - attempts.append({"execution_id": identity, "state": state}) - return sorted(attempts, key=lambda item: item["execution_id"]) - - -def _missing_evidence(values: Sequence[str]) -> list[str]: - if isinstance(values, (str, bytes)) or not isinstance(values, Sequence): - raise BoundedClosureError("WB_POST_EXECUTION_INPUT_INVALID", "known_missing_evidence") - if not all(isinstance(value, str) and value.strip() for value in values): - raise BoundedClosureError("WB_POST_EXECUTION_INPUT_INVALID", "known_missing_evidence") - return sorted(set(values)) - - -def _stored_review_reference( - root: Path, reference: Mapping[str, Any] -) -> dict[str, str]: - if ( - set(reference) != {"review_id", "sha256"} - or not isinstance(reference.get("review_id"), str) - or not SHA256_RE.fullmatch(str(reference.get("sha256") or "")) - ): - raise BoundedClosureError("WB_POST_EXECUTION_EVIDENCE_INVALID") - review_id = _identifier(reference["review_id"], "review_reference.review_id") - store = (root / ".work-bundle/orchestration/reviews").resolve() - path = (store / f"{review_id}.json").resolve(strict=False) - if ( - not path.is_relative_to(store) - or path.is_symlink() - or not path.is_file() - or path.stat().st_mode & 0o222 - or hashlib.sha256(path.read_bytes()).hexdigest() != reference["sha256"] - ): - raise BoundedClosureError("WB_POST_EXECUTION_EVIDENCE_INVALID") - return {"review_id": review_id, "sha256": str(reference["sha256"])} - - -def _public_round(record: Mapping[str, Any]) -> dict[str, Any]: - return { - key: record[key] - for key in ( - "flow_id", "round_id", "round_number", "request_id", "review_id", - "target_identity", "execution_complete", "terminal_attempts", - "known_missing_evidence", "state", "outcome", "finalization_required", - ) - if key in record - } - - -def begin_review_round( - root: Path, - *, - flow_id: str, - request_id: str, - review_id: str, - target_identity: Mapping[str, Any], - executor_attempts: Sequence[Mapping[str, Any]], - known_missing_evidence: Sequence[str], -) -> dict[str, Any]: - """Serialize and persist a post-execution round before review preparation.""" - - workspace = _workspace_root(root) - flow_id = _identifier(flow_id, "flow_id") - request_id = _identifier(request_id, "request_id") - review_id = _identifier(review_id, "review_id") - target = _target_identity(target_identity) - attempts = _terminal_attempts(executor_attempts) - missing = _missing_evidence(known_missing_evidence) - with _locked(workspace): - metadata = _load_metadata(workspace) - _policy(metadata, required=True) - ledger = _load_ledger(workspace) - flows = ledger["flows"] - flow = flows.get(flow_id) - if flow is None: - flow = { - "flow_id": flow_id, - "execution_complete": True, - "terminal_attempts": attempts, - "known_missing_evidence": missing, - "finalization_required": False, - "rounds": [], - } - flows[flow_id] = flow - elif ( - flow.get("terminal_attempts") != attempts - or flow.get("known_missing_evidence") != missing - ): - raise BoundedClosureError("WB_POST_EXECUTION_BOUNDARY_COLLISION") - rounds = flow["rounds"] - for existing in rounds: - if ( - existing["request_id"] == request_id - and existing["target_identity"] == target - ): - if existing["review_id"] != review_id: - raise BoundedClosureError("WB_POST_EXECUTION_REQUEST_COLLISION") - return _public_round(existing) - if flow.get("finalization_required"): - raise BoundedClosureError("WB_POST_EXECUTION_FINALIZATION_REQUIRED") - if len(rounds) >= ROUND_LIMIT: - raise BoundedClosureError("WB_POST_EXECUTION_FINALIZATION_REQUIRED") - number = len(rounds) + 1 - record = { - "flow_id": flow_id, - "round_id": f"{flow_id}:round:{number:03d}", - "round_number": number, - "request_id": request_id, - "review_id": review_id, - "target_identity": target, - "execution_complete": True, - "terminal_attempts": attempts, - "known_missing_evidence": missing, - "state": "reserved", - "outcome": None, - "finalization_required": False, - "reserved_at": _utc_now(), - } - rounds.append(record) - _write_ledger(workspace, ledger) - _write_metadata_projection(workspace, metadata, ledger) - return _public_round(record) - - -def _find_round(ledger: Mapping[str, Any], flow_id: str, round_id: str) -> dict[str, Any]: - flow = ledger["flows"].get(flow_id) - if not isinstance(flow, dict): - raise BoundedClosureError("WB_POST_EXECUTION_ROUND_NOT_FOUND") - for record in flow.get("rounds", []): - if record.get("round_id") == round_id: - return record - raise BoundedClosureError("WB_POST_EXECUTION_ROUND_NOT_FOUND") - - -def review_round_binding( - root: Path, *, review_id: str, target_identity: Mapping[str, Any] -) -> dict[str, Any] | None: - """Resolve a reservation for integrated review, or ``None`` for legacy policy.""" - - workspace = root.expanduser().resolve() - if not _metadata_path(workspace).is_file(): - return None - metadata = _load_metadata(workspace) - if _policy(metadata, required=False) is None: - return None - review_id = _identifier(review_id, "review_id") - target = _target_identity(target_identity) - with _locked(workspace): - ledger = _load_ledger(workspace) - matches = [ - record - for flow in ledger["flows"].values() - for record in flow.get("rounds", []) - if record.get("review_id") == review_id - ] - if len(matches) != 1: - raise BoundedClosureError("WB_POST_EXECUTION_ROUND_REQUIRED") - record = matches[0] - if record.get("target_identity") != target or record.get("state") != "reserved": - raise BoundedClosureError("WB_POST_EXECUTION_ROUND_BINDING_INVALID") - return _public_round(record) - - -def mark_review_round_prepared(root: Path, *, flow_id: str, round_id: str) -> dict[str, Any]: - workspace = _workspace_root(root) - with _locked(workspace): - metadata = _load_metadata(workspace) - _policy(metadata, required=True) - ledger = _load_ledger(workspace) - record = _find_round(ledger, flow_id, round_id) - if record["state"] != "reserved": - raise BoundedClosureError("WB_POST_EXECUTION_ROUND_BINDING_INVALID") - record["state"] = "prepared" - record["prepared_at"] = _utc_now() - _write_ledger(workspace, ledger) - _write_metadata_projection(workspace, metadata, ledger) - return _public_round(record) - - -def require_review_round_execution( - root: Path, *, binding: Mapping[str, Any] -) -> dict[str, Any]: - """Admit reviewer execution only for its exact live, unjudged round.""" - - workspace = _workspace_root(root) - if not isinstance(binding, Mapping): - raise BoundedClosureError("WB_POST_EXECUTION_ROUND_BINDING_INVALID") - flow_id = _identifier(binding.get("flow_id"), "binding.flow_id") - round_id = _identifier(binding.get("round_id"), "binding.round_id") - review_id = _identifier(binding.get("review_id"), "binding.review_id") - request_id = _identifier(binding.get("request_id"), "binding.request_id") - target = _target_identity(binding.get("target_identity")) - with _locked(workspace): - metadata = _load_metadata(workspace) - _policy(metadata, required=True) - ledger = _load_ledger(workspace) - record = _find_round(ledger, flow_id, round_id) - if ( - record.get("review_id") != review_id - or record.get("request_id") != request_id - or record.get("target_identity") != target - or record.get("round_number") != binding.get("round_number") - ): - raise BoundedClosureError("WB_POST_EXECUTION_ROUND_BINDING_INVALID") - if record.get("state") == "completed": - raise BoundedClosureError("WB_POST_EXECUTION_JUDGMENT_ALREADY_RECORDED") - flow = ledger["flows"][flow_id] - if flow.get("finalization_required") is True: - raise BoundedClosureError("WB_POST_EXECUTION_FINALIZATION_REQUIRED") - if record.get("state") != "prepared": - raise BoundedClosureError("WB_POST_EXECUTION_ROUND_BINDING_INVALID") - return _public_round(record) - - -def review_round_publication_binding( - root: Path, *, review_id: str, target_identity: Mapping[str, Any] -) -> dict[str, Any] | None: - """Fail closed before a bounded integrated review publication writes.""" - - workspace = root.expanduser().resolve() - if not _metadata_path(workspace).is_file(): - return None - metadata = _load_metadata(workspace) - if _policy(metadata, required=False) is None: - return None - review_id = _identifier(review_id, "review_id") - target = _target_identity(target_identity) - with _locked(workspace): - ledger = _load_ledger(workspace) - matches = [ - record - for flow in ledger["flows"].values() - for record in flow.get("rounds", []) - if record.get("review_id") == review_id - and record.get("target_identity") == target - ] - if len(matches) != 1: - raise BoundedClosureError("WB_POST_EXECUTION_ROUND_REQUIRED") - if matches[0].get("state") not in {"prepared", "completed"}: - raise BoundedClosureError("WB_POST_EXECUTION_ROUND_BINDING_INVALID") - return _public_round(matches[0]) - - -def complete_review_round( - root: Path, - *, - flow_id: str, - round_id: str, - outcome: str, - review_reference: Mapping[str, Any] | None = None, - audit_block: Mapping[str, Any] | None = None, -) -> dict[str, Any]: - """Complete one reserved round exactly once with product or audit evidence.""" - - workspace = _workspace_root(root) - flow_id = _identifier(flow_id, "flow_id") - round_id = _identifier(round_id, "round_id") - if outcome not in ROUND_OUTCOMES: - raise BoundedClosureError("WB_POST_EXECUTION_INPUT_INVALID", "outcome") - if (review_reference is None) == (audit_block is None): - raise BoundedClosureError("WB_POST_EXECUTION_EVIDENCE_INVALID") - if outcome in {"accepted", "findings"} and review_reference is None: - raise BoundedClosureError("WB_POST_EXECUTION_EVIDENCE_INVALID") - if review_reference is not None: - evidence = {"review_reference": _stored_review_reference(workspace, review_reference)} - else: - if not isinstance(audit_block, Mapping) or not audit_block: - raise BoundedClosureError("WB_POST_EXECUTION_EVIDENCE_INVALID") - evidence = {"audit_block": dict(audit_block)} - requested_completion = {"outcome": outcome, **evidence} - with _locked(workspace): - metadata = _load_metadata(workspace) - _policy(metadata, required=True) - ledger = _load_ledger(workspace) - record = _find_round(ledger, flow_id, round_id) - if record["state"] == "completed": - if record.get("completion") != requested_completion: - raise BoundedClosureError("WB_POST_EXECUTION_JUDGMENT_COLLISION") - return _public_round(record) - if record["state"] not in {"reserved", "prepared"}: - raise BoundedClosureError("WB_POST_EXECUTION_ROUND_BINDING_INVALID") - if review_reference is not None and record["state"] != "prepared": - raise BoundedClosureError("WB_POST_EXECUTION_ROUND_BINDING_INVALID") - if review_reference is not None and review_reference["review_id"] != record["review_id"]: - raise BoundedClosureError("WB_POST_EXECUTION_EVIDENCE_INVALID") - record.update( - state="completed", - outcome=outcome, - completion=requested_completion, - completed_at=_utc_now(), - ) - flow = ledger["flows"][flow_id] - completed_count = sum(item["state"] == "completed" for item in flow["rounds"]) - flow["finalization_required"] = outcome == "accepted" or completed_count >= ROUND_LIMIT - record["finalization_required"] = flow["finalization_required"] - _write_ledger(workspace, ledger) - _write_metadata_projection(workspace, metadata, ledger) - return _public_round(record) - - -def complete_published_review_round( - root: Path, - *, - review_id: str, - target_identity: Mapping[str, Any], - outcome: str, - review_reference: Mapping[str, Any], -) -> dict[str, Any] | None: - """Bind stored integrated-review authority to its reserved round. - - Legacy workspaces have no bounded policy and therefore return ``None``. - """ - - workspace = root.expanduser().resolve() - if not _metadata_path(workspace).is_file(): - return None - metadata = _load_metadata(workspace) - if _policy(metadata, required=False) is None: - return None - review_id = _identifier(review_id, "review_id") - target = _target_identity(target_identity) - with _locked(workspace): - ledger = _load_ledger(workspace) - matches = [ - record - for flow in ledger["flows"].values() - for record in flow.get("rounds", []) - if record.get("review_id") == review_id - and record.get("target_identity") == target - ] - if len(matches) != 1: - raise BoundedClosureError("WB_POST_EXECUTION_ROUND_REQUIRED") - flow_id = str(matches[0]["flow_id"]) - round_id = str(matches[0]["round_id"]) - return complete_review_round( - workspace, - flow_id=flow_id, - round_id=round_id, - outcome=outcome, - review_reference=review_reference, - ) - - -def review_round_status(root: Path, *, flow_id: str) -> dict[str, Any]: - workspace = _workspace_root(root) - flow_id = _identifier(flow_id, "flow_id") - _policy(_load_metadata(workspace), required=True) - with _locked(workspace): - ledger = _load_ledger(workspace) - flow = ledger["flows"].get(flow_id) - if not isinstance(flow, Mapping): - return { - "flow_id": flow_id, - "execution_complete": False, - "reserved_rounds": 0, - "completed_rounds": 0, - "latest_round": None, - "finalization_required": False, - } - rounds = flow["rounds"] - return { - "flow_id": flow_id, - "execution_complete": bool(flow["execution_complete"]), - "reserved_rounds": len(rounds), - "completed_rounds": sum(item["state"] == "completed" for item in rounds), - "latest_round": _public_round(rounds[-1]) if rounds else None, - "finalization_required": bool(flow.get("finalization_required")), - } - - -def _portable_source_baselines( - values: Sequence[Mapping[str, Any]], -) -> tuple[list[dict[str, str]], list[tuple[Path, dict[str, str]]]]: - if isinstance(values, (str, bytes)) or not isinstance(values, Sequence) or not values: - raise BoundedClosureError("WB_POST_EXECUTION_INPUT_INVALID", "source_baselines") - portable: list[dict[str, str]] = [] - local: list[tuple[Path, dict[str, str]]] = [] - repository_ids: set[str] = set() - for value in values: - if not isinstance(value, Mapping) or set(value) != { - "repository_id", "project_root", "commit", "tree" - }: - raise BoundedClosureError("WB_POST_EXECUTION_INPUT_INVALID", "source_baselines") - repository_id = _identifier(value.get("repository_id"), "source_baselines.repository_id") - commit = value.get("commit") - tree = value.get("tree") - project_root = value.get("project_root") - if ( - repository_id in repository_ids - or not isinstance(commit, str) - or not GIT_OID_RE.fullmatch(commit) - or not isinstance(tree, str) - or not GIT_OID_RE.fullmatch(tree) - or not isinstance(project_root, str) - or not project_root - ): - raise BoundedClosureError("WB_POST_EXECUTION_INPUT_INVALID", "source_baselines") - repository_ids.add(repository_id) - identity = {"repository_id": repository_id, "commit": commit, "tree": tree} - portable.append(identity) - local.append((Path(project_root).expanduser().resolve(), identity)) - portable.sort(key=lambda item: item["repository_id"]) - local.sort(key=lambda item: item[1]["repository_id"]) - return portable, local - - -def _knowledge_return(value: Mapping[str, Any]) -> dict[str, str | None]: - if not isinstance(value, Mapping) or set(value) != {"status", "evidence_ref"}: - raise BoundedClosureError("WB_POST_EXECUTION_KNOWLEDGE_RETURN_INVALID") - status = value.get("status") - evidence_ref = value.get("evidence_ref") - if status == "completed": - evidence_ref = _identifier(evidence_ref, "knowledge_return.evidence_ref") - elif status == "not-needed": - if evidence_ref is not None: - raise BoundedClosureError("WB_POST_EXECUTION_KNOWLEDGE_RETURN_INVALID") - else: - raise BoundedClosureError("WB_POST_EXECUTION_KNOWLEDGE_RETURN_INVALID") - return {"status": str(status), "evidence_ref": evidence_ref} - - -def _residual_spec_identity( - workspace: Path, path: Path, residual_spec_id: str -) -> dict[str, str]: - active_root = (workspace / ".work-bundle/orchestration/spec/active").resolve() - candidate = path.expanduser().resolve(strict=False) - if candidate.is_symlink() or not candidate.is_file(): - raise BoundedClosureError("WB_POST_EXECUTION_RESIDUAL_SPEC_INVALID") - try: - content = candidate.read_bytes() - text = content.decode("utf-8") - if not text.startswith("---\n") or "\n---\n" not in text[4:]: - raise ValueError("front matter") - raw, body = text[4:].split("\n---\n", 1) - front_matter = yaml.safe_load(raw) - except (OSError, UnicodeError, yaml.YAMLError, ValueError) as error: - raise BoundedClosureError("WB_POST_EXECUTION_RESIDUAL_SPEC_INVALID") from error - if ( - not isinstance(front_matter, Mapping) - or front_matter.get("id") != residual_spec_id - or front_matter.get("status") != "active" - or not body.strip() - ): - raise BoundedClosureError("WB_POST_EXECUTION_RESIDUAL_SPEC_INVALID") - target = candidate if candidate.is_relative_to(active_root) else active_root / candidate.name - if target.exists(): - if target.is_symlink() or not target.is_file() or target.read_bytes() != content: - raise BoundedClosureError("WB_POST_EXECUTION_RESIDUAL_SPEC_COLLISION") - else: - _atomic_write(target, content) - return { - "id": residual_spec_id, - "path": target.relative_to(workspace).as_posix(), - "sha256": hashlib.sha256(content).hexdigest(), - } - - -def _validate_source_baselines( - local: Sequence[tuple[Path, Mapping[str, str]]], -) -> None: - for root, identity in local: - if not root.is_dir(): - raise BoundedClosureError( - "WB_POST_EXECUTION_SOURCE_BASELINE_INVALID", identity["repository_id"] - ) - status = subprocess.run( - ["git", "-C", str(root), "status", "--porcelain", "--untracked-files=all"], - capture_output=True, - text=True, - check=False, - ) - if status.returncode != 0: - raise BoundedClosureError( - "WB_POST_EXECUTION_SOURCE_BASELINE_INVALID", identity["repository_id"] - ) - if status.stdout.strip(): - raise BoundedClosureError( - "WB_POST_EXECUTION_SOURCE_BASELINE_DIRTY", identity["repository_id"] - ) - head = subprocess.run( - ["git", "-C", str(root), "rev-parse", "HEAD"], - capture_output=True, - text=True, - check=False, - ) - tree = subprocess.run( - ["git", "-C", str(root), "rev-parse", "HEAD^{tree}"], - capture_output=True, - text=True, - check=False, - ) - if ( - head.returncode != 0 - or tree.returncode != 0 - or head.stdout.strip() != identity["commit"] - or tree.stdout.strip() != identity["tree"] - ): - raise BoundedClosureError( - "WB_POST_EXECUTION_SOURCE_BASELINE_CONFLICT", identity["repository_id"] - ) - - -def _public_finalization(record: Mapping[str, Any]) -> dict[str, Any]: - return { - key: record[key] - for key in ( - "flow_id", "request_id", "state", "outcome", "authority", - "blocker_id", "residual_spec", "origin_spec_id", "origin_plan_id", - "source_baselines", "knowledge_return", "administrative", - ) - if key in record - } - - -def _update_finalization( - workspace: Path, flow_id: str, **updates: object -) -> dict[str, Any]: - with _locked(workspace): - metadata = _load_metadata(workspace) - _policy(metadata, required=True) - ledger = _load_ledger(workspace) - flow = ledger["flows"].get(flow_id) - if not isinstance(flow, dict) or not isinstance(flow.get("finalization"), dict): - raise BoundedClosureError("WB_POST_EXECUTION_FINALIZATION_STATE_INVALID") - flow["finalization"].update(updates) - _write_ledger(workspace, ledger) - _write_metadata_projection(workspace, metadata, ledger) - return dict(flow["finalization"]) - - -def _record_finalization_incomplete( - workspace: Path, flow_id: str, stage: str, detail: str -) -> None: - with _locked(workspace): - metadata = _load_metadata(workspace) - _policy(metadata, required=True) - ledger = _load_ledger(workspace) - flow = ledger["flows"].get(flow_id) - if not isinstance(flow, dict) or not isinstance(flow.get("finalization"), dict): - return - finalization = flow["finalization"] - administrative = finalization.setdefault("administrative", {}) - administrative[stage] = "incomplete" - finalization["state"] = "administrative_incomplete" - finalization["incomplete"] = {"stage": stage, "detail": detail} - _write_ledger(workspace, ledger) - _write_metadata_projection(workspace, metadata, ledger) - - -def finalize_with_blockers( +def restore_implementation_exception( root: Path, *, + backup_path: Path, + backup_sha256: str, flow_id: str, - request_id: str, blocker_id: str, - residual_spec_id: str, - residual_spec: Path, - origin_spec_id: str, - origin_plan_id: str, - source_baselines: Sequence[Mapping[str, Any]], - knowledge_return: Mapping[str, Any], - operator_authorized: bool = False, -) -> dict[str, Any]: - """Administratively close one bounded flow while preserving unresolved truth.""" +) -> bool: + """Retire one scoped exception and merge its exact original blocker back.""" workspace = _workspace_root(root) - flow_id = _identifier(flow_id, "flow_id") - request_id = _identifier(request_id, "request_id") - blocker_id = _identifier(blocker_id, "blocker_id") - residual_spec_id = _identifier(residual_spec_id, "residual_spec_id") - origin_spec_id = _identifier(origin_spec_id, "origin_spec_id") - origin_plan_id = _identifier(origin_plan_id, "origin_plan_id") - if origin_plan_id != flow_id or not isinstance(operator_authorized, bool): - raise BoundedClosureError("WB_POST_EXECUTION_INPUT_INVALID", "origin_plan_id") - portable_baselines, local_baselines = _portable_source_baselines(source_baselines) - portable_request = { - "flow_id": flow_id, - "request_id": request_id, - "blocker_id": blocker_id, - "residual_spec_id": residual_spec_id, - "origin_spec_id": origin_spec_id, - "origin_plan_id": origin_plan_id, - "source_baselines": portable_baselines, - "authority": "operator" if operator_authorized else "exhausted-rounds", - } - - # Persist the closed-to-reconciliation state before validating any supplied - # administrative input. Retry may repair those inputs, but may not reopen - # product review or executor work. - with _locked(workspace): - metadata = _load_metadata(workspace) - _policy(metadata, required=True) - ledger = _load_ledger(workspace) - flow = ledger["flows"].get(flow_id) - if flow is None: - flow = { - "flow_id": flow_id, - "execution_complete": True, - "terminal_attempts": [], - "known_missing_evidence": [], - "finalization_required": False, - "rounds": [], - } - ledger["flows"][flow_id] = flow - if not isinstance(flow, dict): - raise BoundedClosureError("WB_POST_EXECUTION_FINALIZATION_STATE_INVALID") - if not flow.get("finalization_required") and not operator_authorized: - raise BoundedClosureError("WB_POST_EXECUTION_FINALIZATION_NOT_AUTHORIZED") - existing = flow.get("finalization") - if isinstance(existing, Mapping): - existing_request = {key: existing.get(key) for key in portable_request} - if existing_request != portable_request: - raise BoundedClosureError("WB_POST_EXECUTION_FINALIZATION_COLLISION") - if existing.get("state") == "closed": - return _public_finalization(existing) - elif existing is not None: - raise BoundedClosureError("WB_POST_EXECUTION_FINALIZATION_STATE_INVALID") - else: - flow["finalization"] = { - **portable_request, - "state": "required", - "outcome": None, - "administrative": {}, - "required_at": _utc_now(), - } - flow["finalization_required"] = True - _write_ledger(workspace, ledger) - _write_metadata_projection(workspace, metadata, ledger) - - residual = _residual_spec_identity(workspace, residual_spec, residual_spec_id) - _validate_source_baselines(local_baselines) - _update_finalization( - workspace, - flow_id, - state="validated", - residual_spec=residual, - incomplete=None, - ) - - blocker = { - "id": blocker_id, - "status": "active", - "origin_plan": origin_plan_id, - "origin_spec": origin_spec_id, - "specification": residual["path"], - "source_baselines": portable_baselines, - } - with _locked(workspace): - metadata = _load_metadata(workspace) - control = _policy(metadata, required=True) - assert isinstance(control, dict) - blockers = [dict(item) for item in _control_list(control, "blockers")] - existing_blocker = next((item for item in blockers if item.get("id") == blocker_id), None) - if existing_blocker is not None and existing_blocker != blocker: - raise BoundedClosureError("WB_POST_EXECUTION_BLOCKER_COLLISION", blocker_id) - if existing_blocker is None: - blockers.append(blocker) - control["blockers"] = blockers - _atomic_write( - _metadata_path(workspace), - yaml.safe_dump(metadata, sort_keys=False, allow_unicode=True).encode("utf-8"), - ) - _update_finalization(workspace, flow_id, state="blocker_recorded") - try: - knowledge = _knowledge_return(knowledge_return) - except BoundedClosureError as error: - _record_finalization_incomplete(workspace, flow_id, "knowledge_return", str(error)) - raise - with _locked(workspace): - metadata = _load_metadata(workspace) - _policy(metadata, required=True) - ledger = _load_ledger(workspace) - flow = ledger["flows"].get(flow_id) - if not isinstance(flow, dict) or not isinstance(flow.get("finalization"), dict): - raise BoundedClosureError("WB_POST_EXECUTION_FINALIZATION_STATE_INVALID") - finalization = flow["finalization"] - existing_knowledge = finalization.get("knowledge_return") - if existing_knowledge is not None and existing_knowledge != knowledge: - raise BoundedClosureError("WB_POST_EXECUTION_FINALIZATION_COLLISION") - finalization.update( - state="knowledge_finalized", knowledge_return=knowledge, incomplete=None - ) - _write_ledger(workspace, ledger) - _write_metadata_projection(workspace, metadata, ledger) - - import argparse - from plans import ( - archive_plan_for_forced_finalization, - release_plan_bindings_for_forced_finalization, - ) - from specs import archive_spec_for_forced_finalization - - args = argparse.Namespace(project_root=str(workspace), workspace_root=str(workspace)) + backup = backup_path.expanduser().resolve() try: - archive_spec_for_forced_finalization(args, origin_spec_id) - _update_finalization( - workspace, flow_id, administrative={"origin_spec": "completed"} - ) - archive_plan_for_forced_finalization(args, origin_plan_id) - _update_finalization( - workspace, - flow_id, - administrative={"origin_spec": "completed", "origin_plan": "completed"}, - ) - except (OSError, SystemExit) as error: - _record_finalization_incomplete(workspace, flow_id, "archive", str(error)) - raise BoundedClosureError("WB_POST_EXECUTION_FINALIZATION_INCOMPLETE", str(error)) from error - + content = backup.read_bytes() + except OSError as error: + raise BoundedClosureError("WB_ORCHESTRATION_BACKUP_INVALID") from error + if not SHA256_RE.fullmatch(backup_sha256) or hashlib.sha256(content).hexdigest() != backup_sha256: + raise BoundedClosureError("WB_ORCHESTRATION_BACKUP_INVALID") try: - binding_result = release_plan_bindings_for_forced_finalization(workspace, origin_plan_id) - except (OSError, SystemExit) as error: - _record_finalization_incomplete(workspace, flow_id, "ownership_release", str(error)) - raise BoundedClosureError("WB_POST_EXECUTION_FINALIZATION_INCOMPLETE", str(error)) from error - if binding_result["incomplete"]: - detail = json.dumps(binding_result["incomplete"], sort_keys=True) - _record_finalization_incomplete(workspace, flow_id, "ownership_release", detail) - raise BoundedClosureError("WB_POST_EXECUTION_FINALIZATION_INCOMPLETE", detail) - administrative = { - "origin_spec": "completed", - "origin_plan": "completed", - "ownership_release": "completed", - } - _update_finalization(workspace, flow_id, administrative=administrative) - - closure = { - "flow_id": flow_id, - "outcome": "closed_with_blockers", - "blocker_id": blocker_id, - "residual_spec": residual, - "source_baselines": portable_baselines, - "knowledge_return": knowledge, - } + original = yaml.safe_load(content) + except yaml.YAMLError as error: + raise BoundedClosureError("WB_ORCHESTRATION_BACKUP_INVALID") from error + if not isinstance(original, Mapping) or not isinstance(original.get("orchestration_control"), Mapping): + raise BoundedClosureError("WB_ORCHESTRATION_BACKUP_INVALID") + original_control = original["orchestration_control"] + original_blockers = _control_list(original_control, "blockers") + original_blocker = next( + (dict(item) for item in original_blockers if item.get("id") == blocker_id), None + ) + if original_blocker is None: + raise BoundedClosureError("WB_ORCHESTRATION_BACKUP_INVALID", blocker_id) with _locked(workspace): metadata = _load_metadata(workspace) control = _policy(metadata, required=True) assert isinstance(control, dict) - closures = [dict(item) for item in _control_list(control, "closed_flows")] - existing_closure = next((item for item in closures if item.get("flow_id") == flow_id), None) - if existing_closure is not None: - comparable = {key: existing_closure.get(key) for key in closure} - if comparable != closure: - raise BoundedClosureError("WB_POST_EXECUTION_FINALIZATION_COLLISION") - else: - closures.append({**closure, "closed_at": _utc_now()}) - control["closed_flows"] = closures - _atomic_write( - _metadata_path(workspace), - yaml.safe_dump(metadata, sort_keys=False, allow_unicode=True).encode("utf-8"), + exemptions = _control_list(control, "implementation_exemptions") + retained = [ + dict(item) + for item in exemptions + if not ( + item.get("flow_id") == flow_id + and item.get("blocker_id") == blocker_id ) - record = _update_finalization( - workspace, - flow_id, - state="closed", - outcome="closed_with_blockers", - administrative=administrative, - incomplete=None, - closed_at=_utc_now(), - ) - return _public_finalization(record) - - -CONTROLLER_COMMANDS = frozenset({ - "begin-review-round", - "complete-review-round", - "review-round-status", - "finalize-with-blockers", -}) - - -def _json_argument(value: str) -> object: - try: - return json.loads(value) - except json.JSONDecodeError as error: - import argparse - raise argparse.ArgumentTypeError(f"invalid controller JSON: {error.msg}") from error - - -def _controller_workspace(args: Any) -> Path: - raw = getattr(args, "workspace_root", None) or getattr(args, "project_root", None) - start = Path(raw).expanduser() if raw else Path.cwd() - workspace = resolve_working_workspace( - start, workspace_id=getattr(args, "workspace_id", None) - ) - if workspace is None: - raise BoundedClosureError("WB_POST_EXECUTION_WORKSPACE_INVALID") - return workspace - - -def cmd_begin_review_round(args: Any) -> None: - result = begin_review_round( - _controller_workspace(args), - flow_id=args.flow_id, - request_id=args.request_id, - review_id=args.review_id, - target_identity=args.target_identity, - executor_attempts=args.executor_attempts, - known_missing_evidence=args.known_missing_evidence, - ) - print(json.dumps(result, sort_keys=True)) - - -def cmd_complete_review_round(args: Any) -> None: - result = complete_review_round( - _controller_workspace(args), - flow_id=args.flow_id, - round_id=args.round_id, - outcome=args.outcome, - review_reference=args.review_reference, - audit_block=args.audit_block, - ) - print(json.dumps(result, sort_keys=True)) - - -def cmd_review_round_status(args: Any) -> None: - print(json.dumps( - review_round_status(_controller_workspace(args), flow_id=args.flow_id), - sort_keys=True, - )) - - -def cmd_finalize_with_blockers(args: Any) -> None: - result = finalize_with_blockers( - _controller_workspace(args), - flow_id=args.flow_id, - request_id=args.request_id, - blocker_id=args.blocker_id, - residual_spec_id=args.residual_spec_id, - residual_spec=Path(args.residual_spec), - origin_spec_id=args.origin_spec_id, - origin_plan_id=args.origin_plan_id, - source_baselines=args.source_baselines, - knowledge_return=args.knowledge_return, - operator_authorized=args.operator_authorized, - ) - print(json.dumps(result, sort_keys=True)) - - -def configure_begin_review_round_parser(parser: Any) -> None: - parser.add_argument("--flow-id", required=True) - parser.add_argument("--request-id", required=True) - parser.add_argument("--review-id", required=True) - parser.add_argument("--target-identity", required=True, type=_json_argument) - parser.add_argument("--executor-attempts", required=True, type=_json_argument) - parser.add_argument("--known-missing-evidence", default=[], type=_json_argument) - parser.set_defaults(func=cmd_begin_review_round) - - -def configure_complete_review_round_parser(parser: Any) -> None: - parser.add_argument("--flow-id", required=True) - parser.add_argument("--round-id", required=True) - parser.add_argument("--outcome", required=True, choices=sorted(ROUND_OUTCOMES)) - parser.add_argument("--review-reference", type=_json_argument) - parser.add_argument("--audit-block", type=_json_argument) - parser.set_defaults(func=cmd_complete_review_round) - - -def configure_review_round_status_parser(parser: Any) -> None: - parser.add_argument("--flow-id", required=True) - parser.set_defaults(func=cmd_review_round_status) - - -def configure_finalize_with_blockers_parser(parser: Any) -> None: - parser.add_argument("--flow-id", required=True) - parser.add_argument("--request-id", required=True) - parser.add_argument("--blocker-id", required=True) - parser.add_argument("--residual-spec-id", required=True) - parser.add_argument("--residual-spec", required=True) - parser.add_argument("--origin-spec-id", required=True) - parser.add_argument("--origin-plan-id", required=True) - parser.add_argument("--source-baselines", required=True, type=_json_argument) - parser.add_argument("--knowledge-return", required=True, type=_json_argument) - parser.add_argument("--operator-authorized", action="store_true") - parser.set_defaults(func=cmd_finalize_with_blockers) - - -def migrate_bounded_review_policy(root: Path) -> bool: - """Rename only the current metadata policy key; history is never inspected.""" - - workspace = _workspace_root(root) - with _locked(workspace): - metadata = _load_metadata(workspace) - control = metadata.get("orchestration_control") - if not isinstance(control, dict) or "review_revision_limit" not in control: - return False - if "post_execution_review_round_limit" in control: - raise BoundedClosureError("WB_POST_EXECUTION_POLICY_INVALID") - control["post_execution_review_round_limit"] = control.pop("review_revision_limit") - if control["post_execution_review_round_limit"] != ROUND_LIMIT: - raise BoundedClosureError("WB_POST_EXECUTION_POLICY_INVALID") + ] + blockers = [ + dict(item) + for item in _control_list(control, "blockers") + if item.get("id") != blocker_id + ] + blockers.append(original_blocker) + control["implementation_exemptions"] = retained + control["blockers"] = blockers _atomic_write( _metadata_path(workspace), yaml.safe_dump(metadata, sort_keys=False, allow_unicode=True).encode("utf-8"), ) - return True + return True diff --git a/scripts/orchestration/completion_provenance.py b/scripts/orchestration/completion_provenance.py index c2cdb13..29992a2 100644 --- a/scripts/orchestration/completion_provenance.py +++ b/scripts/orchestration/completion_provenance.py @@ -716,6 +716,10 @@ def run(): if source_identity() != source or validation_environment_identity(root, policy) != environment: raise SystemExit("validation-blocked: inputs changed while obtaining evidence") if finalization_id is not None: + # The store-owned observation binds compiled authority, oracle, runner, + # environment and source. Reuse keeps its ID; a legitimately fresh + # observation must not collide with an older immutable consumer claim. + finalization_id = f"{finalization_id}:{record.observation_id}" record = _claim_reused_observation( store, record, diff --git a/scripts/orchestration/core.py b/scripts/orchestration/core.py index f835445..d3c6c3b 100644 --- a/scripts/orchestration/core.py +++ b/scripts/orchestration/core.py @@ -5,17 +5,67 @@ import argparse import datetime as dt +import importlib.util import json import os import re import shutil +import sys from pathlib import Path +def _artifact_store_module(): + """Load the orchestration sibling even when this file is loaded directly.""" + module_path = Path(__file__).with_name("artifact_store.py").resolve() + existing = sys.modules.get("artifact_store") + if existing is not None: + if Path(str(getattr(existing, "__file__", ""))).resolve() != module_path: + raise ImportError("artifact_store module collision") + return existing + spec = importlib.util.spec_from_file_location("artifact_store", module_path) + if spec is None or spec.loader is None: + raise ImportError("cannot load orchestration artifact_store") + module = importlib.util.module_from_spec(spec) + sys.modules["artifact_store"] = module + try: + spec.loader.exec_module(module) + except BaseException: + sys.modules.pop("artifact_store", None) + raise + return module + + +_artifact_store = _artifact_store_module() +atomic_write_bytes = _artifact_store.atomic_write_bytes +parse_markdown_artifact = _artifact_store.parse_markdown_artifact + + +def _infrastructure_module(): + """Load the shared infrastructure owner without making the hyphenated path a package.""" + module_path = Path(__file__).resolve().parents[1] / "work-bundle" / "infrastructure.py" + existing = sys.modules.get("work_bundle_infrastructure") + if existing is not None: + if Path(str(getattr(existing, "__file__", ""))).resolve() != module_path: + raise ImportError("work_bundle_infrastructure module collision") + return existing + spec = importlib.util.spec_from_file_location("work_bundle_infrastructure", module_path) + if spec is None or spec.loader is None: + raise ImportError("cannot load work-bundle infrastructure") + module = importlib.util.module_from_spec(spec) + sys.modules["work_bundle_infrastructure"] = module + try: + spec.loader.exec_module(module) + except BaseException: + sys.modules.pop("work_bundle_infrastructure", None) + raise + return module + + +_infrastructure = _infrastructure_module() + + SPEC_STATUSES = {"draft", "active", "verified", "implemented", "reviewed", "superseded", "archived"} PLAN_STATUSES = {"Planned", "In progress", "Completed", "Deprecated", "On Hold"} -HANDOFF_STATUSES = {"active", "reviewed", "archived", "superseded"} -HANDOFF_TYPES = {"orchestration", "executor-result"} RETRIEVAL_ROLES = {"authority", "candidate", "background", "blocked"} # Directive policies describe classification/output intent only. Knowledge # discovery remains neutral and cross-stage before agent authority classification. @@ -47,169 +97,59 @@ def is_relative_to(path: Path, parent: Path) -> bool: return path == parent or parent in path.parents -def _walk_workspace_root(start: Path) -> Path | None: - current = start.expanduser().resolve() - if current.is_file(): - current = current.parent - for candidate in [current, *current.parents]: - if (candidate / ".work-bundle" / "project.yaml").is_file(): - return candidate - return None - - def project_registry_path() -> Path: - config_root = Path(os.environ.get("WB_CONFIG_ROOT", Path.home() / ".work-bundle")).expanduser() - bootstrap = config_root / "bootstrap.yaml" - registry_value = "$work_bundle_config_root/registry/projects.yaml" - if bootstrap.is_file(): - for line in bootstrap.read_text(encoding="utf-8").splitlines(): - if line.strip().startswith("project_registry:"): - registry_value = line.split(":", 1)[1].strip().strip("'\"") - break - registry_value = registry_value.replace("$work_bundle_config_root", str(config_root)) - return Path(registry_value).expanduser().resolve() - - -def _registry_workspace_candidates(start: Path) -> list[Path]: - config_root = Path(os.environ.get("WB_CONFIG_ROOT", Path.home() / ".work-bundle")).expanduser() - if not (config_root / "bootstrap.yaml").is_file(): - return [] - registry = project_registry_path() - if not registry.is_file(): - return [] - projects: list[dict[str, object]] = [] - current: dict[str, object] | None = None - for line in registry.read_text(encoding="utf-8").splitlines(): - stripped = line.strip() - if line.startswith(" - slug:"): - current = {"locators": []} - projects.append(current) - continue - if current is None: - continue - if stripped.startswith("workspace_root:"): - value = stripped.split(":", 1)[1].strip().strip("'\"") - if value: - current["root"] = Path(value).expanduser().resolve() - elif stripped.startswith("origin_path:") or stripped.startswith("path:"): - value = stripped.split(":", 1)[1].strip().strip("'\"") - if value: - locators = current["locators"] - assert isinstance(locators, list) - locators.append(Path(value).expanduser().resolve()) - - current_path = start.resolve() - matches: list[Path] = [] - for project in projects: - root = project.get("root") - if not isinstance(root, Path): - continue - locators = [root, *[path for path in project["locators"] if isinstance(path, Path)]] - if any(locator == current_path or locator in current_path.parents for locator in locators): - matches.append(root) - return matches + try: + return _infrastructure.resolve_project_registry_path() + except _infrastructure.InfrastructureError as exc: + raise SystemExit(exc.code) from exc def resolve_workspace_root(args: argparse.Namespace) -> Path: explicit_workspace = getattr(args, "workspace_root", None) - if explicit_workspace: - root = Path(explicit_workspace).expanduser().resolve() - if not (root / ".work-bundle" / "project.yaml").is_file(): - raise SystemExit(f"No workspace metadata found at: {root}") - return root - explicit_project = getattr(args, "project_root", None) - start = Path(explicit_project).expanduser() if explicit_project else Path.cwd() - found = _walk_workspace_root(start) - if found: - return found - if explicit_project: - return start.resolve() - - current = start.resolve() - matching = _registry_workspace_candidates(current) - if matching: - return max(matching, key=lambda path: len(path.parts)) - raise SystemExit("No workspace root found. Pass --workspace-root/--project-root or run inside a work bundle.") + try: + context = _infrastructure.resolve_anchor_context( + workspace_root=Path(explicit_workspace) if explicit_workspace else None, + project_root=Path(explicit_project) if explicit_project else None, + cwd=Path.cwd(), + ) + except _infrastructure.InfrastructureError as exc: + raise SystemExit(exc.code) from exc + return context.workspace_root def _member_roots(root: Path) -> list[Path]: - metadata = root / ".work-bundle" / "project.yaml" - text = metadata.read_text(encoding="utf-8") - if re.search(r"^metadata_version:\s*4\s*$", text, re.MULTILINE): - workspace_id = "" - in_workspace = False - for line in text.splitlines(): - if line == "workspace:": - in_workspace = True - continue - if in_workspace and line and not line.startswith(" "): - break - if in_workspace and line.strip().startswith("id:"): - workspace_id = line.split(":", 1)[1].strip().strip("'\"") - break - registry = project_registry_path() - if not registry.is_file() or not workspace_id: - return [] - roots: list[Path] = [] - in_bindings = False - in_target = False - in_repositories = False - for line in registry.read_text(encoding="utf-8").splitlines(): - if line == "device_bindings:": - in_bindings = True - continue - if in_bindings and line and not line.startswith(" "): - break - if not in_bindings: - continue - if re.match(r"^ [^\s].*:$", line): - in_target = line.strip()[:-1].strip("'\"") == workspace_id - in_repositories = False - continue - if in_target and line == " repositories:": - in_repositories = True - continue - if in_target and in_repositories and line.startswith(" project_root:"): - value = line.split(":", 1)[1].strip().strip("'\"") - if value: - roots.append(Path(value).expanduser().resolve()) - return roots - roots: list[Path] = [] - in_repositories = False - for line in text.splitlines(): - if line == "source_repositories:": - in_repositories = True - continue - if in_repositories and line and not line.startswith(" "): - break - if not in_repositories: - continue - stripped = line.strip() - if stripped.startswith("project_root:") or stripped.startswith("path:"): - value = stripped.split(":", 1)[1].strip().strip("'\"") - if value: - roots.append(Path(value).expanduser().resolve()) - return roots + try: + metadata = _infrastructure.load_workspace_metadata(root) + registry = _infrastructure.load_project_registry() + binding = _infrastructure.join_workspace_binding(metadata, registry, expected_workspace_root=root) + except _infrastructure.InfrastructureError: + return [] + repositories = binding.get("repositories") + if not isinstance(repositories, dict): + return [] + return [ + Path(str(item["project_root"])).expanduser().resolve() + for item in repositories.values() + if isinstance(item, dict) and item.get("project_root") + ] def resolve_member_project_root(args: argparse.Namespace, workspace: Path | None = None) -> Path: - root = workspace or resolve_workspace_root(args) + explicit_workspace = workspace or getattr(args, "workspace_root", None) explicit_project = getattr(args, "project_root", None) - candidate = Path(explicit_project).expanduser().resolve() if explicit_project else Path.cwd().resolve() - members = [member for member in _member_roots(root) if member == candidate or member in candidate.parents] - if members: - return max(members, key=lambda path: len(path.parts)) - if candidate == root or root in candidate.parents: - return root - if not explicit_project and getattr(args, "workspace_root", None): - return root - raise SystemExit(f"Project root is not a managed member of workspace: {candidate}") - - -def project_root(args: argparse.Namespace) -> Path: - """Compatibility alias for the workspace authority root.""" - return resolve_workspace_root(args) + try: + context = _infrastructure.resolve_anchor_context( + workspace_root=Path(explicit_workspace) if explicit_workspace else None, + project_root=Path(explicit_project) if explicit_project else None, + cwd=Path.cwd(), + member_required=True, + ) + except _infrastructure.InfrastructureError as exc: + raise SystemExit(exc.code) from exc + if context.project_root is None: + raise SystemExit("WB_PROJECT_ROOT_AMBIGUOUS") + return context.project_root def work_bundle(args: argparse.Namespace) -> Path: @@ -266,23 +206,13 @@ def read_front_matter(path: Path) -> tuple[dict[str, object], str]: text = path.read_text(encoding="utf-8") if not text.startswith("---\n"): return {}, text - end = text.find("\n---\n", 4) - if end == -1: - return {}, text - raw = text[4:end] - body = text[end + 5 :] - data: dict[str, object] = {} - for line in raw.splitlines(): - if ":" in line and not line.startswith(" "): - key, value = line.split(":", 1) - data[key.strip()] = value.strip() + data, body = parse_markdown_artifact(text, source=str(path)) return data, body def write_text_safely(path: Path, content: str, args: argparse.Namespace) -> None: target = ensure_under_orchestration(path, args) - target.parent.mkdir(parents=True, exist_ok=True) - target.write_text(content.rstrip() + "\n", encoding="utf-8") + atomic_write_bytes(target, (content.rstrip() + "\n").encode("utf-8")) def sequence_id(root: Path, prefix: str) -> str: @@ -313,17 +243,21 @@ def init_dirs(args: argparse.Namespace) -> None: "spec/archived", "plan/active", "plan/archived", - "handoff/orchestration/active", - "handoff/orchestration/archived", - "handoff/executor/active", - "handoff/executor/archived", + "result/executor/active", + "result/executor/reviewed", + "result/executor/superseded", + "result/executor/archived", + "result/accepted/active", + "result/accepted/superseded", + "result/accepted/archived", + "review/implementation/active", + "review/implementation/superseded", + "review/implementation/archived", + "review/final/active", + "review/final/archived", "docs", ]: (root / directory).mkdir(parents=True, exist_ok=True) - for index in ["spec/index.jsonl", "plan/index.jsonl", "handoff/index.jsonl"]: - path = root / index - path.parent.mkdir(parents=True, exist_ok=True) - path.touch(exist_ok=True) def rel(path: Path, args: argparse.Namespace) -> str: @@ -345,10 +279,12 @@ def move_to_archive(path: Path, active_root: Path, archived_root: Path) -> Path: return target -def count_by_status(rows: list[dict[str, object]]) -> dict[str, int]: +def count_by_status( + rows: list[dict[str, object]], *, status_key: str = "status" +) -> dict[str, int]: counts: dict[str, int] = {} for row in rows: - status = str(row.get("status", "unknown")) + status = str(row.get(status_key, "unknown")) counts[status] = counts.get(status, 0) + 1 return counts diff --git a/scripts/orchestration/current_review_authority.py b/scripts/orchestration/current_review_authority.py deleted file mode 100644 index 0c7d527..0000000 --- a/scripts/orchestration/current_review_authority.py +++ /dev/null @@ -1,110 +0,0 @@ -"""Direct immutable authority bindings for already-published current reviews.""" - -from __future__ import annotations - -import hashlib -import json -import re -from pathlib import Path -from typing import Any, Mapping - - -AUTHORITY_SCHEMA = "stage-review-v2" -_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]*$") -_BINDING_KEYS = frozenset( - { - "review_id", - "record_sha256", - "stage", - "review_target_kind", - "review_mode", - "verdict", - "target_identity", - } -) - - -def authority_path(root: Path, review_id: str) -> Path: - if not _ID_RE.fullmatch(review_id): - raise ValueError("current review authority review_id is invalid") - store = root.expanduser().resolve() / ".work-bundle/orchestration/review-authority" - path = (store / f"{review_id}.json").resolve(strict=False) - if not path.is_relative_to(store.resolve()): - raise ValueError("current review authority path escapes authority store") - return path - - -def _canonical_digest(value: Mapping[str, Any]) -> str: - return hashlib.sha256( - json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode() - ).hexdigest() - - -def _binding(record: Mapping[str, Any], record_sha256: str) -> dict[str, Any]: - target = record.get("target_identity") - if not isinstance(target, Mapping): - raise ValueError("current review authority target identity is invalid") - return { - "review_id": str(record.get("review_id") or ""), - "record_sha256": record_sha256, - "stage": str(record.get("stage") or "plan"), - "review_target_kind": str(record.get("review_target_kind") or "stage"), - "review_mode": str(record.get("review_mode") or "initial"), - "verdict": str(record.get("verdict") or ""), - "target_identity": dict(target), - } - - -def write_authority(root: Path, record: Mapping[str, Any], record_sha256: str) -> Path: - """Persist one idempotent digest-bound current authority after publication checks.""" - - binding = _binding(record, record_sha256) - document = { - "schema": AUTHORITY_SCHEMA, - "current_authority": binding, - "authority_sha256": _canonical_digest(binding), - } - content = (json.dumps(document, indent=2, sort_keys=True) + "\n").encode() - path = authority_path(root, binding["review_id"]) - path.parent.mkdir(parents=True, exist_ok=True) - if path.exists(): - if ( - path.is_symlink() - or not path.is_file() - or path.stat().st_mode & 0o222 - or path.read_bytes() != content - ): - raise ValueError("current review authority identity collision") - else: - with path.open("xb") as stream: - stream.write(content) - path.chmod(0o444) - return path - - -def load_authority( - root: Path, record: Mapping[str, Any], record_sha256: str -) -> dict[str, Any] | None: - """Load a v2 binding without consulting receipts or predecessor records.""" - - path = authority_path(root, str(record.get("review_id") or "")) - if not path.exists(): - return None - if path.is_symlink() or not path.is_file() or path.stat().st_mode & 0o222: - raise ValueError("current review authority is missing or mutable") - try: - document = json.loads(path.read_bytes()) - except (OSError, json.JSONDecodeError) as error: - raise ValueError("current review authority is unreadable") from error - if not isinstance(document, dict) or set(document) != { - "schema", "current_authority", "authority_sha256" - } or document.get("schema") != AUTHORITY_SCHEMA: - raise ValueError("current review authority shape is invalid") - binding = document.get("current_authority") - if not isinstance(binding, dict) or set(binding) != _BINDING_KEYS: - raise ValueError("current review authority binding shape is invalid") - if document.get("authority_sha256") != _canonical_digest(binding): - raise ValueError("current review authority digest mismatch") - if binding != _binding(record, record_sha256): - raise ValueError("current review authority does not bind the stored record") - return binding diff --git a/scripts/orchestration/dispatcher.py b/scripts/orchestration/dispatcher.py index a7834da..6f621f3 100644 --- a/scripts/orchestration/dispatcher.py +++ b/scripts/orchestration/dispatcher.py @@ -2,68 +2,79 @@ from __future__ import annotations import argparse -import json - -from core import HANDOFF_TYPES, resolve_workspace_root -from bounded_closure import ( - BoundedClosureError, - configure_begin_review_round_parser, - configure_complete_review_round_parser, - configure_finalize_with_blockers_parser, - configure_review_round_status_parser, - require_orchestration_admission, - resolve_working_workspace, -) +import importlib from doctor import cmd_doctor -from documents import cmd_git_status, cmd_next_action_candidates, cmd_related, cmd_state, cmd_write_doc from execution_context import ( - cmd_build_review_package, + cmd_build_implementation_review_candidate, cmd_build_task_brief, - cmd_observe_task_validation, - task_flow_id, - cmd_validate_executor_result, ) -from handoffs import cmd_index_handoffs, cmd_list_handoffs, cmd_set_handoff_status, cmd_write_handoff +from handoffs import ( + cmd_index_executor_results, + cmd_list_executor_results, + cmd_transition_executor_result, + cmd_write_executor_result, +) from init import cmd_init -from plans import cmd_archive_plan, cmd_index_plans, cmd_list_plans, cmd_set_plan_status, cmd_write_phase, cmd_write_plan, cmd_write_task +from review_runtime import ( + cmd_list_accepted_task_results, + cmd_list_final_workflow_reviews, + cmd_list_implementation_reviews, + cmd_write_accepted_task_result, + cmd_write_final_workflow_review, + cmd_write_implementation_review, +) from repository_preflight import cmd_repository_preflight -from specs import cmd_index_specs, cmd_list_specs, cmd_set_spec_status, cmd_write_spec -RECOGNIZED_COMMANDS = frozenset({ - "init", "doctor", "state", "next-action-candidates", "git-status", - "repository-preflight", "build-task-brief", "build-review-package", - "validate-executor-result", "observe-task-validation", - "related", "write-doc", "write-spec", - "list-specs", "set-spec-status", "index-specs", "write-plan", "list-plans", - "set-plan-status", "archive-plan", "index-plans", "write-phase", "write-task", - "write-handoff", "list-handoffs", "set-handoff-status", "index-handoffs", - "begin-review-round", "complete-review-round", "review-round-status", - "finalize-with-blockers", -}) +def _lazy_command(module_name: str, function_name: str): + """Keep unrelated lifecycle modules outside the selected command graph.""" -def _runtime_json(value: str) -> object: - try: - return json.loads(value) - except json.JSONDecodeError as error: - raise argparse.ArgumentTypeError(f"invalid controller runtime JSON: {error.msg}") from error + def invoke(args: argparse.Namespace) -> None: + function = getattr(importlib.import_module(module_name), function_name) + function(args) + return invoke -def _add_acceptance_runtime_inputs(parser: argparse.ArgumentParser) -> None: - """Expose harness observations without adding them to durable executor results.""" - parser.add_argument("--mutation-events", type=_runtime_json) - parser.add_argument("--accepted-dependency-deltas", type=_runtime_json) - parser.add_argument("--prior-ownership", type=_runtime_json) - parser.add_argument("--repair-continuity", type=_runtime_json) - parser.add_argument("--authorized-replacements", type=_runtime_json) +cmd_write_spec = _lazy_command("specs", "cmd_write_spec") +cmd_list_specs = _lazy_command("specs", "cmd_list_specs") +cmd_set_spec_status = _lazy_command("specs", "cmd_set_spec_status") +cmd_index_specs = _lazy_command("specs", "cmd_index_specs") +cmd_write_plan = _lazy_command("plans", "cmd_write_plan") +cmd_list_plans = _lazy_command("plans", "cmd_list_plans") +cmd_set_plan_status = _lazy_command("plans", "cmd_set_plan_status") +cmd_index_plans = _lazy_command("plans", "cmd_index_plans") +cmd_write_phase = _lazy_command("plans", "cmd_write_phase") +cmd_write_task = _lazy_command("plans", "cmd_write_task") +cmd_finalize_reviewed_plan = _lazy_command("plans", "cmd_finalize_reviewed_plan") +cmd_git_status = _lazy_command("documents", "cmd_git_status") +cmd_next_action_candidates = _lazy_command("documents", "cmd_next_action_candidates") +cmd_related = _lazy_command("documents", "cmd_related") +cmd_state = _lazy_command("documents", "cmd_state") +cmd_write_doc = _lazy_command("documents", "cmd_write_doc") + +RECOGNIZED_COMMANDS = frozenset({ + "init", "doctor", "state", "next-action-candidates", "git-status", + "repository-preflight", "build-task-brief", + "related", "write-doc", "write-spec", + "list-specs", "set-spec-status", "index-specs", "write-plan", "list-plans", + "set-plan-status", "index-plans", "write-phase", "write-task", + "write-executor-result", "list-executor-results", + "transition-executor-result", "index-executor-results", + "build-implementation-review-candidate", "write-implementation-review", + "list-implementation-reviews", "write-accepted-task-result", + "list-accepted-task-results", "write-final-workflow-review", + "list-final-workflow-reviews", "finalize-reviewed-plan", +}) def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser() parser.add_argument("--project-root") + parser.add_argument("--workspace-root") parent = argparse.ArgumentParser(add_help=False) - parent.add_argument("--project-root") + parent.add_argument("--project-root", default=argparse.SUPPRESS) + parent.add_argument("--workspace-root", default=argparse.SUPPRESS) parent.add_argument("--workspace-id") parent.add_argument("--execution-id") parent.add_argument("--repository-id") @@ -86,22 +97,6 @@ def build_parser() -> argparse.ArgumentParser: task_brief = sub.add_parser("build-task-brief", parents=[parent]) task_brief.add_argument("--task", required=True) task_brief.set_defaults(func=cmd_build_task_brief) - review_package = sub.add_parser("build-review-package", parents=[parent]) - review_package.add_argument("--task", required=True) - review_package.add_argument("--handoff") - review_package.add_argument("--base", required=True) - review_package.add_argument("--head", required=True) - review_package.add_argument("--validation-observation-id", action="append", default=[]) - _add_acceptance_runtime_inputs(review_package) - review_package.set_defaults(func=cmd_build_review_package) - validate_result = sub.add_parser("validate-executor-result", parents=[parent]) - validate_result.add_argument("--task", required=True) - validate_result.add_argument("--handoff", required=True) - _add_acceptance_runtime_inputs(validate_result) - validate_result.set_defaults(func=cmd_validate_executor_result) - observe_validation = sub.add_parser("observe-task-validation", parents=[parent]) - observe_validation.add_argument("--task", required=True) - observe_validation.set_defaults(func=cmd_observe_task_validation) related = sub.add_parser("related", parents=[parent]) related.add_argument("--id", required=True) related.set_defaults(func=cmd_related) @@ -117,7 +112,6 @@ def build_parser() -> argparse.ArgumentParser: write_spec.add_argument("--content-file", required=True) write_spec.add_argument("--status", default="draft") write_spec.add_argument("--id") - write_spec.add_argument("--filename") write_spec.set_defaults(func=cmd_write_spec) list_specs = sub.add_parser("list-specs", parents=[parent]) list_specs.add_argument("--status") @@ -133,8 +127,9 @@ def build_parser() -> argparse.ArgumentParser: write_plan.add_argument("--component", required=True) write_plan.add_argument("--version", default="1") write_plan.add_argument("--content-file", required=True) - write_plan.add_argument("--status", default="Planned") + write_plan.add_argument("--status", default="draft") write_plan.add_argument("--id") + write_plan.add_argument("--source-spec-id", required=True) write_plan.add_argument("--filename") write_plan.set_defaults(func=cmd_write_plan) list_plans = sub.add_parser("list-plans", parents=[parent]) @@ -146,20 +141,14 @@ def build_parser() -> argparse.ArgumentParser: set_plan.add_argument("--status", required=True) set_plan.add_argument("--kind", choices=["plan", "phase", "task"]) set_plan.add_argument("--plan-id") - set_plan.add_argument("--handoff") - _add_acceptance_runtime_inputs(set_plan) set_plan.set_defaults(func=cmd_set_plan_status) - archive_plan = sub.add_parser("archive-plan", parents=[parent]) - archive_plan.add_argument("--id", required=True) - _add_acceptance_runtime_inputs(archive_plan) - archive_plan.set_defaults(func=cmd_archive_plan) sub.add_parser("index-plans", parents=[parent]).set_defaults(func=cmd_index_plans) write_phase = sub.add_parser("write-phase", parents=[parent]) write_phase.add_argument("--plan-id", required=True) write_phase.add_argument("--phase-id", required=True) write_phase.add_argument("--title", required=True) write_phase.add_argument("--content-file", required=True) - write_phase.add_argument("--status", default="Planned") + write_phase.add_argument("--status", default="planned") write_phase.set_defaults(func=cmd_write_phase) write_task = sub.add_parser("write-task", parents=[parent]) write_task.add_argument("--plan-id", required=True) @@ -167,76 +156,62 @@ def build_parser() -> argparse.ArgumentParser: write_task.add_argument("--task-id", required=True) write_task.add_argument("--title", required=True) write_task.add_argument("--content-file", required=True) - write_task.add_argument("--status", default="Planned") + write_task.add_argument("--status", default="planned") write_task.set_defaults(func=cmd_write_task) - write_handoff = sub.add_parser("write-handoff", parents=[parent]) - write_handoff.add_argument("--type", required=True) - write_handoff.add_argument("--title", required=True) - write_handoff.add_argument("--content-file", required=True) - write_handoff.add_argument("--related-spec") - write_handoff.add_argument("--related-plan") - write_handoff.add_argument("--related-phase") - write_handoff.add_argument("--related-task") - write_handoff.add_argument("--status", default="active") - write_handoff.add_argument("--id") - write_handoff.add_argument("--format", choices=["yaml", "markdown"]) - write_handoff.set_defaults(func=cmd_write_handoff) - list_handoffs = sub.add_parser("list-handoffs", parents=[parent]) - list_handoffs.add_argument("--type", choices=sorted(HANDOFF_TYPES)) - list_handoffs.add_argument("--status") - list_handoffs.set_defaults(func=cmd_list_handoffs) - set_handoff = sub.add_parser("set-handoff-status", parents=[parent]) - set_handoff.add_argument("--id", required=True) - set_handoff.add_argument("--status", required=True) - set_handoff.set_defaults(func=cmd_set_handoff_status) - sub.add_parser("index-handoffs", parents=[parent]).set_defaults(func=cmd_index_handoffs) - begin_round = sub.add_parser("begin-review-round", parents=[parent]) - configure_begin_review_round_parser(begin_round) - complete_round = sub.add_parser("complete-review-round", parents=[parent]) - configure_complete_review_round_parser(complete_round) - round_status = sub.add_parser("review-round-status", parents=[parent]) - configure_review_round_status_parser(round_status) - forced_finalization = sub.add_parser("finalize-with-blockers", parents=[parent]) - configure_finalize_with_blockers_parser(forced_finalization) + write_result = sub.add_parser("write-executor-result", parents=[parent]) + for flag in ("id", "plan-id", "task-id", "content-file"): + write_result.add_argument(f"--{flag}", required=True) + write_result.add_argument("--phase-id") + write_result.set_defaults(func=cmd_write_executor_result) + list_results = sub.add_parser("list-executor-results", parents=[parent]) + list_results.add_argument("--plan-id") + list_results.add_argument("--task-id") + list_results.set_defaults(func=cmd_list_executor_results) + transition_result = sub.add_parser("transition-executor-result", parents=[parent]) + for flag in ("id", "plan-id", "task-id", "current-state", "target-state"): + transition_result.add_argument(f"--{flag}", required=True) + transition_result.set_defaults(func=cmd_transition_executor_result) + sub.add_parser("index-executor-results", parents=[parent]).set_defaults(func=cmd_index_executor_results) + candidate = sub.add_parser("build-implementation-review-candidate", parents=[parent]) + candidate.add_argument("--source-root", required=True) + candidate.add_argument("--kind", choices=["commit", "worktree"], required=True) + candidate.add_argument("--base-commit", required=True) + candidate.add_argument("--changed-path", action="append", default=[]) + candidate.set_defaults(func=cmd_build_implementation_review_candidate) + for command, function, task_optional in ( + ("write-implementation-review", cmd_write_implementation_review, True), + ("write-accepted-task-result", cmd_write_accepted_task_result, False), + ("write-final-workflow-review", cmd_write_final_workflow_review, None), + ): + current = sub.add_parser(command, parents=[parent]) + current.add_argument("--id", required=True) + current.add_argument("--plan-id", required=True) + if task_optional is not None: + current.add_argument("--task-id", required=not task_optional) + if command == "write-implementation-review": + current.add_argument("--source-root", required=True) + current.add_argument("--content-file", required=True) + current.set_defaults(func=function) + for command, function, has_task in ( + ("list-implementation-reviews", cmd_list_implementation_reviews, True), + ("list-accepted-task-results", cmd_list_accepted_task_results, True), + ("list-final-workflow-reviews", cmd_list_final_workflow_reviews, False), + ): + current = sub.add_parser(command, parents=[parent]) + current.add_argument("--plan-id") + if has_task: + current.add_argument("--task-id") + current.set_defaults(func=function) + finalize = sub.add_parser("finalize-reviewed-plan", parents=[parent]) + finalize.add_argument("--plan-id", required=True) + finalize.add_argument("--final-review-id", required=True) + finalize.set_defaults(func=cmd_finalize_reviewed_plan) return parser -def _require_public_admission(args: argparse.Namespace) -> None: - operation = { - "write-spec": "ordinary_new", "write-plan": "ordinary_new", - "write-phase": "reconciliation", "write-task": "reconciliation", - "build-task-brief": "reconciliation", "build-review-package": "reconciliation", - "observe-task-validation": "reconciliation", - "begin-review-round": "reconciliation", - "complete-review-round": "round_completion", - "review-round-status": "read_only", - "finalize-with-blockers": "finalization", - "archive-plan": "finalization", "set-spec-status": "finalization", - "set-plan-status": "finalization", "set-handoff-status": "finalization", - }.get(args.command) - if operation is not None: - root = resolve_working_workspace(resolve_workspace_root(args)) - flow_id = ( - getattr(args, "flow_id", None) - or getattr(args, "plan_id", None) - or getattr(args, "id", None) - ) - if flow_id is None and getattr(args, "task", None): - flow_id = task_flow_id(args) - try: - if root is not None: - require_orchestration_admission(root, operation=operation, flow_id=flow_id) - except BoundedClosureError as error: - raise SystemExit(str(error)) from error - - def main() -> int: args = build_parser().parse_args() - _require_public_admission(args) - try: - args.func(args) - except BoundedClosureError as error: - raise SystemExit(str(error)) from error + args.func(args) return 0 diff --git a/scripts/orchestration/doctor.py b/scripts/orchestration/doctor.py index 79cf26a..571b679 100644 --- a/scripts/orchestration/doctor.py +++ b/scripts/orchestration/doctor.py @@ -1,390 +1,93 @@ -import json - -from core import * -from handoffs import index_handoffs -from plans import index_plans -from specs import load_index - +"""Read-only structural doctor for the current orchestration surface.""" +from __future__ import annotations -FORBIDDEN_EXECUTOR_RESULT_FIELDS = { - "suggested_durable_conclusions", - "durable_candidate_facts", - "recommended_orchestration_review", - "recommended_next_actions", - "delegation", - "deviations", - "strategy_advice", - "knowledge_persistence", - "baseline", +import argparse +import json +from pathlib import Path + +from artifact_store import family_policy, load_catalog + + +CATALOG = ( + Path(__file__).resolve().parents[2] + / "references/assets/orchestration/contract/artifact-family-catalog-v5.yaml" +) +STAGE5_FAMILIES = ( + "executor-result", + "implementation-review", + "accepted-task-result", + "final-workflow-review", +) +CURRENT_COMMANDS = { + "write-executor-result", "list-executor-results", "transition-executor-result", + "index-executor-results", "build-implementation-review-candidate", + "write-implementation-review", "list-implementation-reviews", + "write-accepted-task-result", "list-accepted-task-results", + "write-final-workflow-review", "list-final-workflow-reviews", + "finalize-reviewed-plan", } -FORBIDDEN_MARKED_EXECUTOR_RESULT_FIELDS = { - "acceptance_review", "accepted_result", "reviewer_run", "publication", "receipt" +RETIRED_COMMANDS = { + "write-handoff", "list-handoffs", "set-handoff-status", "index-handoffs", + "build-review-package", "validate-executor-result", "observe-task-validation", + "begin-review-round", "complete-review-round", "review-round-status", + "finalize-accepted-plan", "finalize-with-blockers", "archive-plan", } -def check_contract_terms(issues: list[str], path: Path, label: str, required_terms: list[str]) -> None: - if not path.exists(): - issues.append(f"missing {label}: {path}") +def _require_terms(issues: list[str], path: Path, terms: tuple[str, ...]) -> None: + if not path.is_file(): + issues.append(f"missing current contract: {path}") return text = path.read_text(encoding="utf-8") - for term in required_terms: + for term in terms: if term not in text: - issues.append(f"{label} missing workflow contract term: {term}") + issues.append(f"{path} missing current contract term: {term}") -def check_eval_shape(issues: list[str], path: Path) -> None: - if not path.exists(): - issues.append(f"missing orchestration evals: {path}") - return +def cmd_doctor(_args: argparse.Namespace) -> None: + issues: list[str] = [] + root = Path(__file__).resolve().parents[2] try: - data = json.loads(path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError) as exc: - issues.append(f"invalid orchestration eval JSON: {exc}") - return - cases = data.get("evals") - if not isinstance(cases, list): - issues.append("orchestration eval JSON missing evals list") - return - seen_ids: set[object] = set() - for index, case in enumerate(cases): - if not isinstance(case, dict): - issues.append(f"orchestration eval entry {index} is not an object") - continue - missing = [key for key in ("id", "prompt", "expected_output", "files") if key not in case] - if missing: - issues.append(f"orchestration eval entry {index} missing fields: {', '.join(missing)}") - case_id = case.get("id") - if not isinstance(case_id, (int, str)): - issues.append(f"orchestration eval entry {index} has invalid id") - continue - if case_id in seen_ids: - issues.append(f"duplicate orchestration eval id: {case_id}") - seen_ids.add(case_id) - - -def check_forbidden_active_dependencies(issues: list[str], paths: list[Path]) -> None: - forbidden_runtime_file = "HAB" "ITS.md" - positive_role_context_terms = ( - "## Role Context", - "Use `wb-select-role-context`", - "Invoke `wb-select-role-context`", - "Required Skill: `wb-select-role-context`", - ) - for path in paths: - if not path.exists(): - continue - text = path.read_text(encoding="utf-8") - if forbidden_runtime_file in text: - issues.append(f"active orchestration contract depends on forbidden runtime file: {path}") - for term in positive_role_context_terms: - if term in text: - issues.append(f"active orchestration contract reintroduces role-context dependency: {path}") - - -def check_active_handoff_contract(issues: list[str], root: Path) -> None: - active_orchestration = root / "handoff" / "orchestration" / "active" - if active_orchestration.exists(): - for path in active_orchestration.iterdir(): - if path.is_file(): - issues.append(f"active orchestration handoff is retired: {path.relative_to(root)}") - - executor_root = root / "handoff" / "executor" - for status in HANDOFF_STATUSES: - for pattern in ("*.yaml", "*.yml"): - for path in (executor_root / status).glob(pattern): - lines = path.read_text(encoding="utf-8").splitlines() - marked = any(line == "lifecycle_authority: location-v1" for line in lines) - for line in lines: - if not line or line[0].isspace() or ":" not in line: - continue - field = line.split(":", 1)[0] - if field in FORBIDDEN_EXECUTOR_RESULT_FIELDS or ( - marked and field in FORBIDDEN_MARKED_EXECUTOR_RESULT_FIELDS - ): - issues.append( - f"executor-result handoff contains forbidden field {field}: " - f"{path.relative_to(root)}" - ) - - -def index_row_identity(index_scope: str, row: dict[str, object]) -> tuple[object, ...]: - row_type = str(row.get("type", index_scope)) - row_id = str(row.get("id", "")) - if index_scope == "plan" and row_type == "phase": - return (row_type, row.get("plan_id"), row_id) - if index_scope == "plan" and row_type == "task": - return (row_type, row.get("plan_id"), row.get("phase_id"), row_id) - if index_scope == "handoff": - return ( - row_type, - row.get("related_plan"), - row.get("related_phase"), - row.get("related_task"), - row_id, + catalog = load_catalog(CATALOG) + for family in STAGE5_FAMILIES: + policy = family_policy(catalog, family) + if policy["representation"] != "yaml" or policy["lifecycle"]["authority"] != "location": + issues.append(f"current family policy is not schema/location owned: {family}") + except (OSError, SystemExit) as error: + issues.append(f"invalid current artifact-family catalog: {error}") + + from dispatcher import RECOGNIZED_COMMANDS + + missing = sorted(CURRENT_COMMANDS - RECOGNIZED_COMMANDS) + retained = sorted(RETIRED_COMMANDS & RECOGNIZED_COMMANDS) + if missing: + issues.append(f"missing current orchestration commands: {', '.join(missing)}") + if retained: + issues.append(f"retired orchestration commands remain public: {', '.join(retained)}") + + for name in ("orch-execute-plan", "orch-create-handoff", "orch-review-plan"): + _require_terms( + issues, + root / "skills" / name / "SKILL.md", + ("## Self-check", "- [ ]"), ) - return (row_type, row_id) - + _require_terms( + issues, + root / "references/assets/orchestration/contract/handoff-executor-result-v1.md", + ("executor-result-v1", "canonical", "product verdict"), + ) + evals = root / "references/evals/orchestration/evals.json" + try: + cases = json.loads(evals.read_text(encoding="utf-8")).get("evals") + if not isinstance(cases, list) or not cases: + issues.append("orchestration evals do not contain current cases") + except (OSError, json.JSONDecodeError) as error: + issues.append(f"invalid orchestration evals: {error}") -def cmd_doctor(args: argparse.Namespace) -> None: - init_dirs(args) - issues = [] - root = orchestration_root(args) - bundle_root = Path(__file__).resolve().parents[2] - for required in ["spec/active", "spec/archived", "spec/index.jsonl", "plan/active", "plan/archived", "plan/index.jsonl", "handoff/orchestration/archived", "handoff/executor/active", "handoff/executor/archived", "handoff/index.jsonl", "docs"]: - if not (root / required).exists(): - issues.append(f"missing {required}") - for index_scope, index in [ - ("spec", "spec/index.jsonl"), - ("plan", "plan/index.jsonl"), - ("handoff", "handoff/index.jsonl"), - ]: - seen: set[tuple[object, ...]] = set() - for row in load_index(root / index): - identity = index_row_identity(index_scope, row) - if identity in seen: - issues.append(f"duplicate {index_scope} identity {identity}") - seen.add(identity) - path = project_root(args) / str(row.get("path", "")) - if not is_relative_to(path, root): - issues.append(f"index path escapes orchestration root: {row}") - active_artifact_roots = [ - root / "spec" / "active", - root / "plan" / "active", - root / "handoff" / "executor" / "active", - root / "docs", - ] - for active_root in active_artifact_roots: - for path in active_root.glob("**/*.md"): - if ".work-bundle/knowledge" in path.resolve().as_posix(): - issues.append(f"artifact under knowledge root: {path}") - for path in (root / "spec" / "active").glob("**/*.md"): - if artifact_mentions_retrieval_without_roles(path): - issues.append(f"retrieval artifact lacks role labels: {path.relative_to(root)}") - check_active_handoff_contract(issues, root) - skill_root = bundle_root / "skills" - orchestration_evals = bundle_root / "references" / "evals" / "orchestration" / "evals.json" - check_eval_shape(issues, orchestration_evals) - orch_skill_policy_map = { - "orch-create-specification": "implementation_spec", - "orch-create-implementation-plan": "implementation_plan", - "orch-create-document": "customer_spec", - "orch-create-handoff": "implementation_plan", - "orch-review-plan": "implementation_plan", - "orch-execute-plan": "execution", - } - for skill_name, policy in orch_skill_policy_map.items(): - path = skill_root / skill_name / "SKILL.md" - if not path.exists(): - issues.append(f"missing orch skill file for policy check: {skill_name}") - continue - text = path.read_text(encoding="utf-8") - if skill_name == "orch-execute-plan": - if "no-retrieval stage" not in text and "must not invoke retrieval" not in text: - issues.append("orch-execute-plan lacks explicit no-retrieval rule") - elif skill_name == "orch-create-specification": - required_terms = [ - "polarity-neutral and stage/perspective/status-neutral query anchors", - "classification and output-grouping intent, not a discovery-stage lifecycle filter", - "supporting, opposing, constraining, unresolved/open-question", - "execution does not require `.work-bundle/knowledge/` reads", - ] - for required in required_terms: - if required not in text: - issues.append( - "orch-create-specification missing no-stage-gate contract term: " - f"{required}" - ) - elif policy not in text and "Knowledge Gateway" in text: - issues.append(f"orch skill does not mention mapped retrieval policy {policy}: {skill_name}") - ks_what_is_helpful_skill = skill_root / "ks-what-is-helpful" / "SKILL.md" - if not ks_what_is_helpful_skill.exists(): - issues.append("missing ks-what-is-helpful skill file") - else: - text = ks_what_is_helpful_skill.read_text(encoding="utf-8") - for required in [ - "Gateway mode", - "ks.py query", - "policy_hint", - "mechanical candidate and trace evidence", - "semantic relevance, authority, polarity, conflict, materiality", - "authority", - "candidate", - "background", - "blocked", - ]: - if required not in text: - issues.append(f"ks-what-is-helpful missing gateway contract term: {required}") - for path in [bundle_root / "references" / "assets" / "keep-summarizing" / "workflow.md"]: - if path.exists(): - text = path.read_text(encoding="utf-8") - if "notes/<leaf-perspective>" in text or "status: archived" in text: - issues.append(f"keep-summarizing doc advertises legacy path/status: {path.relative_to(bundle_root)}") - workflow_contracts = [ - ( - bundle_root / "references" / "assets" / "orchestration" / "workflow.md", - "orchestration workflow", - [ - "Disposable task briefs, review packages, and lightweight development plans", - "build-task-brief", - "optional task review", - "acceptance_review.required: true", - "A task becomes `Completed` only when", - "Final workflow audit", - ], - ), - ( - bundle_root / "references" / "assets" / "orchestration" / "contract" / "handoff-executor-result-v1.md", - "executor-result handoff contract", - [ - "default_format: yaml", - "Required By Applicability", - "Forbidden Executor-Result Fields", - "task_fit_check:", - "delegation_evidence:", - "reason: null | no-index | sync-failed | not-source-code | blocked", - ], - ), - ( - bundle_root / "scripts" / "orchestration" / "handoffs.py", - "handoff helper", - [ - 'HANDOFF_EXTENSIONS = (".md", ".yaml", ".yml")', - "Active orchestration handoff creation is retired", - '"yaml" if args.type == "executor-result" else "markdown"', - ], - ), - ( - bundle_root / "references" / "assets" / "keep-summarizing" / "workflow.md", - "keep-summarizing workflow", - [ - "neutral hybrid candidate discovery followed by explicit authority, candidate, background, blocked, polarity, materiality, and blocker classification", - ], - ), - ( - skill_root / "orch-create-specification" / "SKILL.md", - "orch-create-specification skill", - [ - "authority, candidate, background, or blocked", - "polarity-neutral and stage/perspective/status-neutral query anchors", - "classification and output-grouping intent", - "semantic_loop:", - "Quality gate: verified|blocked", - ], - ), - ( - bundle_root / "rules" / "orchestration" / "orch-knowledge-gateway.md", - "orch-knowledge-gateway rule", - [ - "Discover relevant candidates across allowed lifecycle partitions", - "classification and output-grouping intent", - "not as a discovery-stage lifecycle filter", - "Treat a directive retrieval policy such as `implementation_spec` as a stage-gated discovery filter", - "retrieval policy did not stage-gate candidate discovery", - "future knowledge-base lookup", - ], - ), - ( - bundle_root / "references" / "assets" / "orchestration" / "contract" / "specification-v1.md", - "specification contract", - [ - "neutral and cross-stage", - "classification/output intent rather than a discovery-stage filter", - "supporting, opposing, constraining, unresolved/open-question", - "downstream planning and execution do not need to read `.work-bundle/knowledge/`", - ], - ), - ( - bundle_root / "scripts" / "orchestration" / "core.py", - "orchestration core policy helper", - [ - "Directive policies describe classification/output intent only", - '"discovery": "neutral-cross-stage"', - '"usage": "classification-output-intent"', - ], - ), - ( - skill_root / "orch-create-implementation-plan" / "SKILL.md", - "orch-create-implementation-plan skill", - [ - "source-ID coverage", - "dev-semantic-convergence", - "context_mode: compiled-brief", - "EXC-*", - "executor briefs", - ], - ), - ( - skill_root / "orch-execute-plan" / "SKILL.md", - "orch-execute-plan skill", - [ - "Before compilation, capability selection, delegation, or edits", - "every target repository", - "build-task-brief", - "validate-executor-result", - "reviewer_independent: false", - ], - ), - ( - skill_root / "orch-execute-plan" / "SKILL.md", - "orch-execute-plan skill", - ["Execution Constraints (skill-owned)", "no-retrieval stage", "record `no-index`"], - ), - ( - ks_what_is_helpful_skill, - "ks-what-is-helpful skill", - [ - "A retrieval policy is caller intent for later classification, not a discovery-stage filter", - "Classify and rank", - "Scripts must not decide semantic relevance, authority, polarity, conflict, materiality", - "Do not convert non-authority results into requirements, tasks, decisions, or review conclusions", - ], - ), - ( - bundle_root / "rules" / "agent-codegraph-first.md", - "CodeGraph-first rule", - ["targeted repository root contains `.codegraph/`", "Do not skip CodeGraph silently", "record the concrete fallback reason"], - ), - ( - orchestration_evals, - "orchestration evals", - [ - "material candidate knowledge conflicts with the user purpose", - "outside the implementation_spec lifecycle", - "classification and output-grouping intent", - "does not require downstream knowledge-base lookup", - "quality gate is verified", - "accepted independent task review", - "task-review verdict", - "target repository has no .codegraph directory", - "sparse YAML", - "active orchestration handoff", - ], - ), - ( - skill_root / "orch-review-plan" / "SKILL.md", - "orch-review-plan skill", - [ - "workflow audit and deterministic finalizer", - "compiled Truth Basis", - "Knowledge Base Update disposition is `completed` or `not-needed`", - "approved `ks-*` return evidence", - "Do not broadly inspect source", - "Do not create a repair specification for every failed gate", - "orch-review-completion", - ], - ), - ] - for path, label, required_terms in workflow_contracts: - check_contract_terms(issues, path, label, required_terms) - check_forbidden_active_dependencies( + _require_terms( issues, - [ - skill_root / "orch-create-specification" / "SKILL.md", - skill_root / "orch-create-implementation-plan" / "SKILL.md", - skill_root / "orch-execute-plan" / "SKILL.md", - bundle_root / "references" / "assets" / "orchestration" / "workflow.md", - bundle_root / "rules" / "agent-codegraph-first.md", - ], + root / "scripts/work-bundle/stage_events.py", + ("operational_metadata_only", "finding_recorded", "artifact_digest"), ) if issues: for issue in issues: diff --git a/scripts/orchestration/documents.py b/scripts/orchestration/documents.py index ded7167..01deb6d 100644 --- a/scripts/orchestration/documents.py +++ b/scripts/orchestration/documents.py @@ -1,10 +1,9 @@ from core import * -from handoffs import index_handoffs +from handoffs import list_executor_results from plans import index_plans -from specs import index_specs, load_index +from specs import index_specs def cmd_write_doc(args: argparse.Namespace) -> None: - init_dirs(args) content = Path(args.content_file).read_text(encoding="utf-8") target = orchestration_root(args) / "docs" / f"{slugify(args.title)}.md" write_text_safely(target, content, args) @@ -12,41 +11,37 @@ def cmd_write_doc(args: argparse.Namespace) -> None: def cmd_state(args: argparse.Namespace) -> None: - init_dirs(args) state = { "specs": count_by_status(index_specs(args)), "plans": count_by_status(index_plans(args)), - "handoffs": count_by_status(index_handoffs(args)), + "executor_results": count_by_status( + list_executor_results(args), status_key="result_state" + ), "docs": len(list((orchestration_root(args) / "docs").glob("*.md"))), } print(json.dumps(state, ensure_ascii=False)) def cmd_related(args: argparse.Namespace) -> None: - init_dirs(args) - rows = [] - for index in ["spec/index.jsonl", "plan/index.jsonl", "handoff/index.jsonl"]: - rows.extend(load_index(orchestration_root(args) / index)) + rows = [*index_specs(args), *index_plans(args), *list_executor_results(args)] for row in rows: if args.id in json.dumps(row, ensure_ascii=False): print(json.dumps(row, ensure_ascii=False)) def cmd_next_action_candidates(args: argparse.Namespace) -> None: - init_dirs(args) - for row in index_handoffs(args): - if row.get("type") == "executor-result" and row.get("status") == "active": - print(json.dumps({"action": "review-executor-handoff", "handoff_id": row.get("id"), "reason": "active executor handoff exists"}, ensure_ascii=False)) + for row in list_executor_results(args): + if row.get("artifact_type") == "executor-result" and row.get("result_state") == "active": + print(json.dumps({"action": "review-executor-result", "executor_result_id": row.get("id"), "reason": "active executor result exists"}, ensure_ascii=False)) for row in index_plans(args): - if row.get("type") == "task" and row.get("status") in {"Planned", "In progress"}: + if row.get("type") == "task" and row.get("status") == "planned": print(json.dumps({"action": "continue-task", "task_id": row.get("id"), "plan_id": row.get("plan_id"), "phase_id": row.get("phase_id"), "reason": "task is executable or in progress"}, ensure_ascii=False)) def cmd_git_status(args: argparse.Namespace) -> None: - root = project_root(args) + root = resolve_workspace_root(args) git = root / ".git" if not git.exists(): print(json.dumps({"git": "absent", "project_root": str(root)}, ensure_ascii=False)) return print(json.dumps({"git": "present", "project_root": str(root)}, ensure_ascii=False)) - diff --git a/scripts/orchestration/evaluation_identity.py b/scripts/orchestration/evaluation_identity.py index d075e16..039a6fd 100644 --- a/scripts/orchestration/evaluation_identity.py +++ b/scripts/orchestration/evaluation_identity.py @@ -351,8 +351,8 @@ def validation_interval_identity( # Generated evidence is packaging, not product input. Other output locations # must be declared explicitly; never guess from a filename such as "result.json". OBSERVATION_ARTIFACT_ROOTS = ( - ".work-bundle/runtime/", ".work-bundle/orchestration/handoff/", - ".work-bundle/orchestration/reviews/", ".work-bundle/logs/", + ".work-bundle/runtime/", ".work-bundle/orchestration/result/", + ".work-bundle/orchestration/review/", ".work-bundle/logs/", ) diff --git a/scripts/orchestration/execution_context.py b/scripts/orchestration/execution_context.py index fffce74..a9bf9d4 100644 --- a/scripts/orchestration/execution_context.py +++ b/scripts/orchestration/execution_context.py @@ -13,7 +13,6 @@ import sys from pathlib import Path from typing import Any, Iterable, Mapping -from datetime import datetime, timezone # Both CLI families contain a top-level ``core.py``. Mixed-process harnesses @@ -28,17 +27,22 @@ sys.modules["core"] = _loaded_core _core_spec.loader.exec_module(_loaded_core) -from core import _member_roots, is_relative_to, read_front_matter, resolve_workspace_root -from artifact_inputs import (_split_top_level, _split_key_value, _parse_scalar, parse_yaml_subset, - _read_structured, _as_list, _input_path, _resolve_spec_paths) -from repository_preflight import capture_repository_evidence, task_caused_paths +from core import _member_roots, resolve_workspace_root +from artifact_inputs import _read_structured, _as_list, _input_path, _resolve_spec_paths +from artifact_store import ( + canonical_artifact_path, + family_policy, + load_catalog, + read_artifact, + read_yaml_mapping, + rebuild_index, +) +from repository_preflight import capture_repository_evidence from task_ownership import ( canonical_relative_path, OwnershipBlocker, - RepairContinuity, - normalize_subagent_provenance, - validate_task_acceptance_ownership, ) +from review_identity import source_obligation_records SOURCE_ID_TOKEN = r"[A-Z][A-Z0-9_-]*-\d+[A-Z]?" @@ -46,6 +50,7 @@ AUTH_ALIAS_RE = re.compile(r"^AUTH-\d{3}$") EXCELLENCE_PROPOSAL_RE = re.compile(r"^EXC-\d+$") SAFE_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$") +PLAN_CATALOG = Path(__file__).resolve().parents[2] / "references/assets/orchestration/contract/artifact-family-catalog-v5.yaml" SENSITIVE_KEY_RE = re.compile( r"(?:^|[_-])(credential_values?|password|passwd|secret|api[_-]?key|access[_-]?token|private[_-]?key)(?:$|[_-])", re.IGNORECASE, @@ -475,11 +480,21 @@ def _artifact_id(data: dict[str, Any], key: str, path: Path) -> str: def _find_plan(root: Path, plan_id: str) -> tuple[Path, dict[str, Any]]: plan_root = root / ".work-bundle/orchestration/plan" matches: list[tuple[Path, dict[str, Any]]] = [] - for status in ("active", "archived"): - for candidate in sorted((plan_root / status).glob("*.md")): - data, _ = _read_structured(candidate) - if str(data.get("id", "")) == plan_id: - matches.append((candidate, data)) + policy = family_policy(load_catalog(PLAN_CATALOG), "root-plan") + anchors = {"workspace_root": root} + for state in ("active", "archived"): + candidate = canonical_artifact_path( + policy, anchors, identity=plan_id, state=state + ) + if not candidate.is_file(): + continue + raw = read_yaml_mapping(candidate) + source_spec_id = str(raw.get("source_spec_id") or "") + result = read_artifact( + PLAN_CATALOG, "root-plan", anchors, identity=plan_id, state=state, + bindings={"source_spec": source_spec_id}, + ) + matches.append((candidate, dict(result["data"]))) if len(matches) != 1: raise SystemExit(f"Expected one root plan for {plan_id}; found {len(matches)} under {plan_root}") return matches[0] @@ -487,92 +502,6 @@ def _find_plan(root: Path, plan_id: str) -> tuple[Path, dict[str, Any]]: -def _strip_markup(text: str) -> str: - return re.sub(r"\s+", " ", text.strip()).strip() - - -def _source_records(path: Path, body: str) -> dict[str, str]: - records: dict[str, str] = {} - - def add(identifier: str, value: str) -> None: - if AUTH_ALIAS_RE.fullmatch(identifier) or EXCELLENCE_PROPOSAL_RE.fullmatch(identifier): - return - value = _strip_markup(value) - if not value: - return - previous = records.get(identifier) - if previous is not None and previous != value: - raise SystemExit(f"Ambiguous source ID {identifier} in {path}") - records[identifier] = value - - lines = body.splitlines() - for index, line in enumerate(lines): - block = re.match(rf"^(\s+)({SOURCE_ID_TOKEN})\s*:\s*$", line) - if not block: - continue - base_indent = len(block.group(1)) - detail: list[str] = [] - for child in lines[index + 1 :]: - if not child.strip(): - continue - child_indent = len(child) - len(child.lstrip()) - if child_indent <= base_indent: - break - detail.append(child.strip()) - if detail: - add(block.group(2), " ".join(detail)) - - for line in lines: - bullet = re.match( - rf"^\s*[-*]\s+\*\*({SOURCE_ID_TOKEN})(?::\*\*\s*|\*\*\s*:\s*)(.+)$", - line, - ) - if bullet: - add(bullet.group(1), bullet.group(2)) - continue - titled_bullet = re.match( - rf"^\s*[-*]\s+\*\*({SOURCE_ID_TOKEN})\s+[—-]\s+(.+?)(?::\*\*\s*|\*\*\s*:\s*)(.+)$", - line, - ) - if titled_bullet: - title = titled_bullet.group(2).strip() - detail = titled_bullet.group(3).strip() - add(titled_bullet.group(1), f"{title}: {detail}") - continue - bold_plain = re.match( - rf"^\s*\*\*({SOURCE_ID_TOKEN})(?::\*\*\s*|\*\*\s*:\s*)(.+)$", - line, - ) - if bold_plain: - add(bold_plain.group(1), bold_plain.group(2)) - continue - titled_plain = re.match( - rf"^\s*\*\*({SOURCE_ID_TOKEN})\s+[—-]\s+(.+?)(?::\*\*\s*|\*\*\s*:\s*)(.+)$", - line, - ) - if titled_plain: - title = titled_plain.group(2).strip() - detail = titled_plain.group(3).strip() - add(titled_plain.group(1), f"{title}: {detail}") - continue - heading = re.match( - rf"^#{{2,6}}\s+({SOURCE_ID_TOKEN})(?:(?:\s+[:—-]?\s*)|(?:[:—-]\s*))(.*)$", - line, - ) - if heading and heading.group(2).strip(): - add(heading.group(1), heading.group(2)) - continue - plain = re.match(rf"^\s*({SOURCE_ID_TOKEN})\s*:\s*(.+)$", line) - if plain: - add(plain.group(1), plain.group(2)) - continue - if line.strip().startswith("|"): - cells = [cell.strip().strip("*") for cell in line.strip().strip("|").split("|")] - if cells and SOURCE_ID_RE.fullmatch(cells[0]): - add(cells[0], " | ".join(cell for cell in cells[1:] if cell)) - return records - - def _assert_no_credential_values(value: Any, context: str = "packet") -> None: if isinstance(value, dict): for key, child in value.items(): @@ -614,77 +543,18 @@ def _nonempty_text(value: Any) -> str | None: return text or None -def _handoff_identity_text(value: Any) -> str | None: - if value is None: - return None - text = str(value).strip() - if not text or text.lower() in {"null", "none", "~"}: - return None - return text -def explicit_handoff_plan_identities(handoff: dict[str, Any]) -> list[str]: - related = handoff.get("related") if isinstance(handoff.get("related"), dict) else {} - identities: list[str] = [] - for raw in (related.get("plan"), handoff.get("related_plan")): - text = _handoff_identity_text(raw) - if text and text not in identities: - identities.append(text) - return identities -def unique_explicit_handoff_plan_id(handoff: dict[str, Any]) -> str | None: - identities = explicit_handoff_plan_identities(handoff) - if len(identities) != 1: - return None - return identities[0] - - -def _assert_task_handoff_identity(handoff: dict[str, Any], task_id: str, plan_id: str) -> None: - related = handoff.get("related") if isinstance(handoff.get("related"), dict) else {} - related_task = related.get("task") or handoff.get("related_task") - if related_task != task_id: - raise SystemExit(f"Handoff task mismatch: expected {task_id}, got {related_task or 'missing'}") - identities = explicit_handoff_plan_identities(handoff) - if not identities: - raise SystemExit(f"Handoff plan identity missing: expected {plan_id}") - if len(identities) > 1: - raise SystemExit(f"Handoff plan identity conflict: {' vs '.join(identities)}") - if identities[0] != plan_id: - raise SystemExit(f"Handoff plan mismatch: expected {plan_id}, got {identities[0]}") - - -def _handoff_review_required(handoff: dict[str, Any]) -> bool: - review = handoff.get("acceptance_review") - if not isinstance(review, dict) or not review: - return False - return review.get("required", False) is True -def _handoff_ineligible_for_closure(handoff: dict[str, Any]) -> bool: - result = handoff.get("result") if isinstance(handoff.get("result"), dict) else {} - state = result.get("state") - review = handoff.get("acceptance_review") if isinstance(handoff.get("acceptance_review"), dict) else {} - verdict = review.get("verdict") - unresolved = _as_list(handoff.get("unresolved")) - if state in {"blocked", "failed", "partial"}: - return True - if verdict in {"repair", "blocked"}: - return True - if unresolved: - return True - return False -def _handoff_eligible_for_closure(handoff: dict[str, Any], *, review_required: bool | None = None) -> bool: - if _handoff_ineligible_for_closure(handoff): - return False - required = _handoff_review_required(handoff) if review_required is None else review_required - if required: - review = handoff.get("acceptance_review") - return isinstance(review, dict) and review.get("verdict") == "accept" - result = handoff.get("result") if isinstance(handoff.get("result"), dict) else {} - return result.get("state") == "completed" + + + + def _source_knowledge_entry(entry: Any) -> tuple[str | None, str | None]: @@ -720,15 +590,6 @@ def _verified_specification_authority(source_paths: list[Path]) -> dict[str, str return accepted -def _allocated_decision_aliases(truth_basis: dict[str, Any]) -> set[str]: - aliases: set[str] = set() - for value in _as_list(truth_basis.get("decision_authority")): - alias = str(value).split(":", 1)[0].strip() - if AUTH_ALIAS_RE.fullmatch(alias): - aliases.add(alias) - return aliases - - def read_structured_artifact(path: Path) -> dict[str, Any]: data, _ = _read_structured(path) return data @@ -888,7 +749,6 @@ def _encoded_bytes(value: Any) -> int: def compiled_context_metrics( task_brief: dict[str, Any], *, - review_package: str | None = None, evidence_projection: list[dict[str, Any]] | None = None, omitted_by_reference_bytes: int = 0, expansion_reason: str | None = None, @@ -912,67 +772,11 @@ def compiled_context_metrics( "evidence_projection_bytes": _encoded_bytes( evidence_projection if evidence_projection is not None else task_brief.get("evidence_capability", {}) ), - "review_package_bytes": len(review_package.encode("utf-8")) if review_package is not None else 0, "omitted_by_reference_bytes": max(0, int(omitted_by_reference_bytes)), "expansion_reason": expansion_reason, } -def project_validation_evidence( - items: list[dict[str, Any]], - *, - evidence_capability: dict[str, Any], - observed: list[dict[str, Any]] | None = None, - expansion_reason: str | None = None, -) -> list[dict[str, Any]]: - """Project successes as compact receipts and expand only explicit failures.""" - - if expansion_reason not in CONTEXT_EXPANSION_REASONS: - raise SystemExit("evidence projection expansion_reason is invalid") - observed_by_id = { - str(item.get("id")): item for item in (observed or []) if isinstance(item, dict) and item.get("id") - } - invariant_by_evidence: dict[str, dict[str, Any]] = {} - for invariant in _as_list(evidence_capability.get("invariants")): - if not isinstance(invariant, dict): - continue - for evidence_id in _as_list(invariant.get("evidence_ids")): - invariant_by_evidence.setdefault(str(evidence_id), invariant) - projected: list[dict[str, Any]] = [] - for position, item in enumerate(items, start=1): - evidence_id = str(item.get("id") or f"validation-{position:03d}") - result = str(item.get("result") or "ambiguous") - if result not in {"passed", "skipped"}: - reason = expansion_reason or ("failed_validation" if result == "failed" else "ambiguity") - if reason not in CONTEXT_EXPANSION_REASONS - {None}: - raise SystemExit("evidence projection expansion_reason is invalid") - projected.append({ - "id": evidence_id, - "digest": semantic_digest({"command": item.get("command"), "result": result}), - "result": result, - "expansion_reason": reason, - "authority_effect": "observation_only", - "lifecycle_action_authorized": False, - "details": dict(item), - }) - continue - invariant = invariant_by_evidence.get(evidence_id, {}) - observation = observed_by_id.get(evidence_id, {}) - projected.append({ - "id": evidence_id, - "command": item.get("command"), - "invariant_ids": list(_as_list(item.get("invariant_ids"))), - "observation_id": observation.get("observation_id"), - "digest": semantic_digest({"command": item.get("command"), "result": result}), - "result": result, - "boundary": invariant.get("boundary", "component"), - "freshness": invariant.get("freshness", "current_task_batch"), - "invalidation_receipt": observation.get("observation_id") or semantic_digest( - {"evidence_id": evidence_id, "result": result} - ), - "expansion_reason": None, - }) - return projected def _compile_evidence_capability( @@ -1029,176 +833,10 @@ def _compile_evidence_capability( return {"result": "mapped", "reason": reason, "invariants": compiled} -def _validate_evidence_closure( - handoff: dict[str, Any], - task: dict[str, Any], - state: str, - reported_commands: dict[str, dict[str, Any]], - observed_validation: list[dict[str, Any]] | None, - *, - creation_safe: bool = False, -) -> dict[str, Any]: - capability = task.get("evidence_capability") - if not isinstance(capability, dict): - raise SystemExit("Compiled task is missing evidence_capability authority") - if capability.get("result") == "no_validation_bearing_obligation": - return {"result": "no_validation_bearing_obligation", "invariants": []} - if capability.get("result") != "mapped" or state != "completed": - return {"result": "not-terminal", "invariants": []} - if observed_validation is None and not creation_safe: - raise SystemExit("evidence-closure-blocked: completed mapped invariants require independent harness observation") - closure = handoff.get("evidence_closure") - if not isinstance(closure, dict): - raise SystemExit("Executor result is missing evidence_closure for mapped invariants") - allocated = { - str(item.get("id")): item - for item in _as_list(capability.get("invariants")) - if isinstance(item, dict) and _nonempty_text(item.get("id")) - } - entries = [item for item in _as_list(closure.get("invariants")) if isinstance(item, dict)] - by_id = {str(item.get("id")): item for item in entries if _nonempty_text(item.get("id"))} - if len(by_id) != len(entries) or set(by_id) != set(allocated): - raise SystemExit("evidence-closure-blocked: mapped invariant closure IDs are missing or unexpected; route plan") - validation_by_id = { - str(item.get("id")): item - for item in _as_list(task.get("validation")) - if isinstance(item, dict) and _nonempty_text(item.get("id")) - } - observed_by_id = { - str(item.get("id")): item - for item in (observed_validation or []) - if isinstance(item, dict) and _nonempty_text(item.get("id")) - } - for invariant_id, expected in allocated.items(): - actual = by_id[invariant_id] - if actual.get("boundary") != expected.get("boundary"): - raise SystemExit(f"evidence-closure-blocked: {invariant_id} is wrong-boundary; route plan") - if actual.get("freshness") != expected.get("freshness"): - raise SystemExit(f"evidence-closure-blocked: {invariant_id} is stale; route task") - evidence_ids = [str(value) for value in _as_list(actual.get("evidence_ids"))] - if evidence_ids != [str(value) for value in _as_list(expected.get("evidence_ids"))]: - raise SystemExit(f"evidence-closure-blocked: {invariant_id} evidence mapping is missing; route plan") - closure_result = str(actual.get("closure_result") or "missing") - if closure_result not in EVIDENCE_CLOSURE_RESULTS: - raise SystemExit(f"evidence-closure-blocked: {invariant_id} has invalid closure_result") - if closure_result != "passed": - repair_owner = str(actual.get("repair_owner") or "task") - expected_owner = EVIDENCE_REPAIR_OWNERS[closure_result] - if repair_owner != expected_owner: - raise SystemExit( - f"evidence-closure-blocked: {invariant_id} {closure_result} must route {expected_owner}" - ) - raise SystemExit( - f"evidence-closure-blocked: {invariant_id} is {closure_result}; route {repair_owner}" - ) - for evidence_id in evidence_ids: - validation = validation_by_id.get(evidence_id) - if validation is None: - raise SystemExit(f"evidence-closure-blocked: {invariant_id} evidence {evidence_id} is missing; route plan") - command = str(validation.get("command") or "").strip() - reported = reported_commands.get(command) - if isinstance(reported, dict): - if str(reported.get("id") or "") != evidence_id or invariant_id not in _as_list(reported.get("invariant_ids")): - raise SystemExit(f"evidence-closure-blocked: reported evidence identity for {invariant_id} is missing; route task") - if reported.get("result") != "passed": - raise SystemExit(f"evidence-closure-blocked: {invariant_id} evidence {evidence_id} failed; route task") - if not creation_safe: - observed = observed_by_id.get(evidence_id) - if not isinstance(observed, dict) or invariant_id not in _as_list(observed.get("invariant_ids")): - raise SystemExit(f"evidence-closure-blocked: harness evidence for {invariant_id} is missing; route task") - if observed.get("result") != "passed": - raise SystemExit(f"evidence-closure-blocked: harness evidence {evidence_id} failed; route task") - if closure.get("result") != "passed": - raise SystemExit("evidence-closure-blocked: aggregate closure result is not passed") - return {"result": "passed", "invariants": entries} - - -def evaluate_knowledge_closure_state( - *, - upstream_disposition: str, - accepted_task_handoffs: list[dict[str, Any]], - closure_return: str = "missing", - review_required_by_task: dict[str, bool] | None = None, -) -> dict[str, Any]: - if upstream_disposition not in {"required", "not-needed", "completed", "blocked"}: - raise SystemExit("Invalid upstream Knowledge Base Update disposition") - if closure_return not in {"missing", "completed", "not-needed", "blocked"}: - raise SystemExit("Invalid knowledge closure return state") - - triggers: list[dict[str, str]] = [] - for handoff in accepted_task_handoffs: - related = handoff.get("related") if isinstance(handoff.get("related"), dict) else {} - task_id = str(related.get("task") or "") - compiled_required = None - if review_required_by_task is not None and task_id in review_required_by_task: - compiled_required = review_required_by_task[task_id] - if not _handoff_eligible_for_closure(handoff, review_required=compiled_required): - continue - disposition = handoff.get("knowledge_disposition") - if not isinstance(disposition, dict): - raise SystemExit("Accepted task handoff knowledge disposition is required") - action = disposition.get("action") - if action is None and "action" in disposition: - action = "none" - if action not in KNOWLEDGE_DISPOSITION_ACTIONS: - raise SystemExit("Accepted task handoff knowledge disposition action is invalid") - if action != "none": - triggers.append({"task": task_id or "unknown", "action": str(action)}) - - closure_required = upstream_disposition in {"required", "blocked"} or bool(triggers) - if not closure_required: - disposition = upstream_disposition - if disposition == "completed": - return {"disposition": "completed", "archive_blocked": False, "triggers": triggers} - return {"disposition": "not-needed", "archive_blocked": False, "triggers": triggers} - if closure_return in {"completed", "not-needed"}: - return {"disposition": closure_return, "archive_blocked": False, "triggers": triggers} - if closure_return == "blocked": - return {"disposition": "blocked", "archive_blocked": True, "triggers": triggers} - return {"disposition": "required", "archive_blocked": True, "triggers": triggers} - - -def _validated_knowledge_disposition( - handoff: dict[str, Any], - accepted_source_ids: list[str], - accepted_authority_paths: list[str], - allocated_decision_aliases: set[str], -) -> dict[str, Any]: - raw = handoff.get("knowledge_disposition") - if not isinstance(raw, dict): - raise SystemExit("Executor result knowledge disposition is required") - action = raw.get("action") - if action is None and "action" in raw: - action = "none" - if action not in KNOWLEDGE_DISPOSITION_ACTIONS: - raise SystemExit( - "Executor result knowledge disposition action must be none, update, supersede, or reclassify" - ) - reason = raw.get("reason") - if not isinstance(reason, str) or not reason.strip(): - raise SystemExit("Executor result knowledge disposition reason must be non-empty") - affected = _as_list(raw.get("affected_authority")) - if any(not isinstance(value, str) or not value.strip() for value in affected): - raise SystemExit("Executor result affected_authority must contain only non-empty strings") - if action == "none" and affected: - raise SystemExit("Executor result knowledge disposition none must not name affected authority") - if action != "none" and not affected: - raise SystemExit("Executor result knowledge disposition change must name affected authority") - disposition_text = "\n".join([reason, *affected]) - if KNOWLEDGE_PERSISTENCE_INSTRUCTION_RE.search(disposition_text): - raise SystemExit("Executor result knowledge disposition must not instruct knowledge access or writes") - for authority in affected: - if AUTH_ALIAS_RE.fullmatch(authority): - if authority not in allocated_decision_aliases: - raise SystemExit("Executor result knowledge disposition cites unallocated decision authority") - continue - if SOURCE_ID_RE.fullmatch(authority): - if authority not in accepted_source_ids: - raise SystemExit("Executor result knowledge disposition cites unallocated source authority") - continue - if authority not in accepted_authority_paths: - raise SystemExit("Executor result knowledge disposition path must be in compiled task scope") - return {"action": action, "reason": reason.strip(), "affected_authority": affected} + + + + _EW_MODULE = None @@ -1266,18 +904,6 @@ def _iter_task_bindings(control_root: Path) -> list[dict[str, Any]]: return bindings -def _scopes_overlap(left: list[str], right: list[str]) -> bool: - first = {str(path).removeprefix("./") for path in left} - second = {str(path).removeprefix("./") for path in right} - if first & second: - return True - for item in first: - for other in second: - if _write_scope_match(item, [other]) or _write_scope_match(other, [item]): - return True - return False - - def _assert_no_overlapping_mutating_siblings(control_root: Path, binding: dict[str, Any]) -> None: execution_path = Path(str(binding.get("execution_path") or "")).resolve() for other in _iter_task_bindings(control_root): @@ -1349,8 +975,9 @@ def create_or_load_task_execution_binding( ] except OwnershipBlocker as error: raise SystemExit(f"Task execution binding scope is unsafe: {error.reason}") from error - from review_runtime import require_plan_reviews - require_plan_reviews(control_root, _find_plan(control_root, plan_id)[0]) + plan = read_structured_artifact(_find_plan(control_root, plan_id)[0]) + if str(plan.get("status") or "").lower() != "verified": + raise SystemExit("Task execution binding requires a verified canonical plan") path = _binding_path(control_root, plan_id, task_id) if path.exists(): binding = load_task_execution_binding(control_root, plan_id, task_id) @@ -1430,3256 +1057,81 @@ def load_task_execution_binding(control_root: Path, plan_id: str, task_id: str) return binding -ACCEPTED_TASK_RESULT_SCHEMA = "accepted-task-result-v1" -LEGACY_ACCEPTED_TASK_RESULT_FIELDS = { - "schema", "plan_id", "task_id", "binding_id", "baseline_identity", - "accepted_source", "authority_projection", "executor_result_digest", - "validation_evidence_ids", "review_id", "owner_identity", "accepted_at", - "invalidation", -} -ACCEPTED_TASK_RESULT_FIELDS = { - *LEGACY_ACCEPTED_TASK_RESULT_FIELDS, - "knowledge_disposition", -} -ACCEPTED_AUTHORITY_PROJECTION_FIELDS = { - "task_digest", "binding_digest", "scope_digest", "validation_obligations_digest", - "required_review_digest", "ownership_digest", -} - - -def _canonical_task_scopes(task: Mapping[str, Any]) -> dict[str, list[str]]: - files = task.get("files") if isinstance(task.get("files"), Mapping) else {} - try: - return { - "read": sorted(canonical_relative_path(str(path)) for path in _as_list(files.get("read"))), - "write": sorted(canonical_relative_path(str(path)) for path in _as_list(files.get("write"))), - "forbidden": sorted( - canonical_relative_path(str(path), allow_tree_pattern=True) - for path in _as_list(files.get("forbidden")) - ), - } - except OwnershipBlocker as error: - raise SystemExit(f"accepted task result scope is unsafe: {error.reason}") from error - -def _accepted_task_projection(task: Mapping[str, Any]) -> dict[str, Any]: - authority = task.get("semantic_authority") if isinstance(task.get("semantic_authority"), Mapping) else {} - topology_fields = ( - "phase_id", "parallel_group", "common_contract", "barrier", - "barrier_participants", "convergence_owner", "integration_owner", - ) - return { - "plan_id": str(task.get("plan_id") or ""), - "task_id": str(task.get("task_id") or ""), - "depends_on": sorted(str(value) for value in _as_list(task.get("depends_on"))), - "topology": {key: task.get(key) for key in topology_fields if key in task}, - "source_ids": sorted(str(value) for value in _as_list(task.get("source_ids"))), - "semantic_authority": { - "records": authority.get("records", {}), - "interface_semantics": authority.get("interface_semantics", {}), - "validation_semantics": authority.get("validation_semantics", {}), - }, - } -def _accepted_validation_projection(task: Mapping[str, Any]) -> list[dict[str, Any]]: - return [ - { - key: item.get(key) - for key in ("id", "command", "boundary", "freshness") - if key in item - } - for item in _as_list(task.get("validation")) - if isinstance(item, Mapping) - ] -def _accepted_binding_projection(binding: Mapping[str, Any]) -> dict[str, Any]: - ownership = binding.get("ownership") if isinstance(binding.get("ownership"), Mapping) else {} - return { - "workspace_id": str(binding.get("workspace_id") or ""), - "execution_id": str(binding.get("execution_id") or ""), - "repository_id": str(binding.get("repository_id") or ""), - "execution_path": str(Path(str(binding.get("execution_path") or "")).resolve()), - "git_identity": dict(binding.get("git_identity", {})) if isinstance(binding.get("git_identity"), Mapping) else {}, - "binding_id": str(ownership.get("binding_id") or ""), - "baseline_identity": _accepted_baseline_identity(binding), - } -def _accepted_baseline_identity(binding: Mapping[str, Any]) -> dict[str, str]: - baseline = binding.get("baseline") if isinstance(binding.get("baseline"), Mapping) else {} - identity = {"head": str(baseline.get("head") or ""), "tree": str(baseline.get("tree") or "")} - if any(not re.fullmatch(r"[0-9a-f]{40}|[0-9a-f]{64}", value) for value in identity.values()): - raise SystemExit("accepted task result baseline identity is invalid") - return identity -def _accepted_review_projection(review: Mapping[str, Any]) -> dict[str, Any]: - projection: dict[str, Any] = { - "required": review.get("required") is True, - "review_id": str(review.get("review_id")) if review.get("review_id") else None, - "verdict": "accepted" if review.get("verdict") in {"accept", "accepted"} else review.get("verdict"), - } - for key in ("review_mode", "repair_frontier", "target_identity"): - if key in review and review.get(key) is not None: - projection[key] = review[key] - return projection -def _accepted_ownership_projection( - task: Mapping[str, Any], binding: Mapping[str, Any], owner_identity: Mapping[str, Any] -) -> dict[str, Any]: - ownership = binding.get("ownership") if isinstance(binding.get("ownership"), Mapping) else {} - return { - "required_executor_profile": task.get("executor_profile", {}), - "binding_id": str(ownership.get("binding_id") or ""), - "original_owner": str(ownership.get("original_owner") or task.get("task_id") or ""), - "owner_identity": dict(owner_identity), - } +def capture_task_baseline_once(binding: dict[str, Any], control_root: Path | None = None) -> dict[str, Any]: + existing = binding.get("baseline") + if isinstance(existing, dict) and existing.get("head"): + return binding + try: + evidence = capture_repository_evidence(Path(str(binding["execution_path"]))) + except RuntimeError as error: + raise SystemExit(str(error)) from error + updated = dict(binding) + updated["baseline"] = evidence + root = control_root or Path(str(binding.get("control_root") or "")) + if not root.is_dir(): + raise SystemExit("Task execution binding control root is required to persist baseline") + _persist_binding(updated, root) + return updated -def _accepted_authority_projection( - task: Mapping[str, Any], - binding: Mapping[str, Any], - *, - accepted_review: Mapping[str, Any], - owner_identity: Mapping[str, Any], -) -> dict[str, str]: - return { - "task_digest": semantic_digest(_accepted_task_projection(task)), - "binding_digest": semantic_digest(_accepted_binding_projection(binding)), - "scope_digest": semantic_digest(_canonical_task_scopes(task)), - "validation_obligations_digest": semantic_digest(_accepted_validation_projection(task)), - "required_review_digest": semantic_digest(_accepted_review_projection(accepted_review)), - "ownership_digest": semantic_digest( - _accepted_ownership_projection(task, binding, owner_identity) - ), - } -def _accepted_source_state_digest( - *, - plan_id: str, - task_id: str, - binding_id: str, - baseline_identity: Mapping[str, str], - head: object, - tree: object, - authority_projection: Mapping[str, Any], - knowledge_disposition: Mapping[str, Any] | None = None, -) -> str: - state = { - "plan_id": plan_id, - "task_id": task_id, - "binding_id": binding_id, - "baseline_identity": dict(baseline_identity), - "accepted_source": {"head": head, "tree": tree}, - "authority_projection": dict(authority_projection), - } - if knowledge_disposition is not None: - state["knowledge_disposition"] = dict(knowledge_disposition) - return semantic_digest(state) -def build_accepted_task_result( - task: Mapping[str, Any], - binding: Mapping[str, Any], - handoff: Mapping[str, Any], - validated: Mapping[str, Any], - *, - accepted_review: Mapping[str, Any] | None = None, - accepted_at: str | None = None, -) -> dict[str, Any]: - """Project a strongly validated executor result into compact durable authority.""" - - if validated.get("result_state") != "completed": - raise SystemExit("accepted task result requires a completed validated result") - ownership = validated.get("task_ownership") - if not isinstance(ownership, Mapping): - raise SystemExit("accepted task result requires validated subagent ownership") - if str(task.get("plan_id") or "") != str(binding.get("plan_id") or ""): - raise SystemExit("accepted task result plan binding mismatch") - if str(task.get("task_id") or "") != str(binding.get("task_id") or ""): - raise SystemExit("accepted task result task binding mismatch") - try: - accepted_ownership = normalize_subagent_provenance(ownership) - except OwnershipBlocker as error: - raise SystemExit(f"accepted task result ownership is invalid: {error.reason}") from error - review = dict(accepted_review) if isinstance(accepted_review, Mapping) else {} - if task.get("review_required") is True and ( - review.get("required") is not True or review.get("verdict") not in {"accept", "accepted"} - ): - raise SystemExit("accepted task result requires the accepted mandatory review") - observed = [item for item in _as_list(validated.get("observed_validation")) if isinstance(item, Mapping)] - validation_ids = sorted( - str( - item.get("observation_id") - or item.get("id") - or f"validation:{semantic_digest(dict(item))}" - ) - for item in observed - ) - if _as_list(task.get("validation")) and not validation_ids: - raise SystemExit("accepted task result requires observed validation evidence") - review_id = str(review.get("review_id")) if review.get("review_id") else None - if task.get("review_required") is True and not review_id: - raise SystemExit("accepted task result requires an accepted review identity") - result = handoff.get("result") if isinstance(handoff.get("result"), Mapping) else {} - changes = handoff.get("changes") if isinstance(handoff.get("changes"), Mapping) else {} - fit = handoff.get("task_fit_check") if isinstance(handoff.get("task_fit_check"), Mapping) else {} - plan_id = str(task.get("plan_id") or "") - task_id = str(task.get("task_id") or "") - binding_ownership = binding.get("ownership") if isinstance(binding.get("ownership"), Mapping) else {} - binding_id = str(binding_ownership.get("binding_id") or "") - baseline_identity = _accepted_baseline_identity(binding) - authority_projection = _accepted_authority_projection( - task, - binding, - accepted_review=review, - owner_identity=accepted_ownership, - ) - knowledge_disposition = validated.get("knowledge_disposition") - if not isinstance(knowledge_disposition, Mapping): - raise SystemExit("accepted task result requires validated knowledge disposition") - knowledge_disposition = dict(knowledge_disposition) - evidence = capture_repository_evidence(Path(str(binding.get("execution_path") or "")).resolve()) - accepted_source = {"head": evidence.get("head"), "tree": evidence.get("tree")} - accepted_source["state_digest"] = _accepted_source_state_digest( - plan_id=plan_id, - task_id=task_id, - binding_id=binding_id, - baseline_identity=baseline_identity, - head=accepted_source["head"], - tree=accepted_source["tree"], - authority_projection=authority_projection, - knowledge_disposition=knowledge_disposition, - ) - return { - "schema": ACCEPTED_TASK_RESULT_SCHEMA, - "plan_id": plan_id, - "task_id": task_id, - "binding_id": binding_id, - "baseline_identity": baseline_identity, - "accepted_source": accepted_source, - "authority_projection": authority_projection, - "executor_result_digest": semantic_digest({ - "state": result.get("state"), - "summary": result.get("summary"), - "changes": changes.get("files", []), - "task_fit": {"task": fit.get("task"), "result": fit.get("result")}, - }), - "validation_evidence_ids": validation_ids, - "review_id": review_id, - "owner_identity": accepted_ownership, - "knowledge_disposition": knowledge_disposition, - "accepted_at": accepted_at or datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), - "invalidation": None, - } -def assert_accepted_task_result_current( - task: Mapping[str, Any], binding: Mapping[str, Any], accepted: Mapping[str, Any] -) -> None: - """Fail closed when current claim-relevant authority differs from acceptance.""" - - if accepted.get("schema") != ACCEPTED_TASK_RESULT_SCHEMA: - raise SystemExit("accepted task result schema is invalid") - accepted_fields = frozenset(accepted) - if accepted_fields not in { - frozenset(LEGACY_ACCEPTED_TASK_RESULT_FIELDS), - frozenset(ACCEPTED_TASK_RESULT_FIELDS), - }: - raise SystemExit("accepted task result shape is not closed") - if accepted.get("invalidation") is not None: - raise SystemExit("accepted task result was explicitly invalidated") - accepted_source = accepted.get("accepted_source") - authority_projection = accepted.get("authority_projection") - owner_identity = accepted.get("owner_identity") - if not isinstance(accepted_source, Mapping) or set(accepted_source) != {"head", "tree", "state_digest"}: - raise SystemExit("accepted task result source shape is not closed") - if not isinstance(authority_projection, Mapping) or set(authority_projection) != ACCEPTED_AUTHORITY_PROJECTION_FIELDS: - raise SystemExit("accepted task result authority projection is not closed") - if not isinstance(owner_identity, Mapping): - raise SystemExit("accepted task result owner identity is invalid") - knowledge_disposition = accepted.get("knowledge_disposition") - if "knowledge_disposition" in accepted: - if not isinstance(knowledge_disposition, Mapping): - raise SystemExit("accepted task result knowledge disposition is invalid") - task_files = task.get("files") if isinstance(task.get("files"), Mapping) else {} - truth_basis = task.get("truth_basis") if isinstance(task.get("truth_basis"), Mapping) else {} - try: - current_disposition = _validated_knowledge_disposition( - {"knowledge_disposition": dict(knowledge_disposition)}, - [str(value) for value in _as_list(task.get("source_ids"))], - [ - str(value) - for value in [ - *_as_list(task_files.get("read")), - *_as_list(task_files.get("write")), - ] - ], - _allocated_decision_aliases(truth_basis), - ) - except SystemExit as error: - raise SystemExit("accepted task result knowledge disposition is invalid") from error - if dict(knowledge_disposition) != current_disposition: - raise SystemExit("accepted task result knowledge disposition is invalid") - current_projection = { - "task_digest": semantic_digest(_accepted_task_projection(task)), - "binding_digest": semantic_digest(_accepted_binding_projection(binding)), - "scope_digest": semantic_digest(_canonical_task_scopes(task)), - "validation_obligations_digest": semantic_digest(_accepted_validation_projection(task)), - "ownership_digest": semantic_digest( - _accepted_ownership_projection(task, binding, owner_identity) - ), - } - binding_ownership = binding.get("ownership") if isinstance(binding.get("ownership"), Mapping) else {} - baseline_identity = _accepted_baseline_identity(binding) - checks = { - "plan": str(task.get("plan_id") or "") == accepted.get("plan_id"), - "task": ( - str(task.get("task_id") or "") == accepted.get("task_id") - and current_projection["task_digest"] == authority_projection.get("task_digest") - ), - "binding": ( - str(binding_ownership.get("binding_id") or "") == accepted.get("binding_id") - and baseline_identity == accepted.get("baseline_identity") - and current_projection["binding_digest"] == authority_projection.get("binding_digest") - ), - "scope": current_projection["scope_digest"] == authority_projection.get("scope_digest"), - "validation": current_projection["validation_obligations_digest"] == authority_projection.get("validation_obligations_digest"), - "review": (task.get("review_required") is True) == bool(accepted.get("review_id")), - "ownership": current_projection["ownership_digest"] == authority_projection.get("ownership_digest"), - "source": accepted_source.get("state_digest") == _accepted_source_state_digest( - plan_id=str(accepted.get("plan_id") or ""), - task_id=str(accepted.get("task_id") or ""), - binding_id=str(accepted.get("binding_id") or ""), - baseline_identity=( - accepted.get("baseline_identity") - if isinstance(accepted.get("baseline_identity"), Mapping) - else {} - ), - head=accepted_source.get("head"), - tree=accepted_source.get("tree"), - authority_projection=authority_projection, - knowledge_disposition=( - knowledge_disposition - if isinstance(knowledge_disposition, Mapping) - else None - ), - ), - } - for label, current in checks.items(): - if not current: - raise SystemExit(f"accepted task result is stale: {label} authority changed") -def materialize_accepted_task_result( - control_root: Path, - task: Mapping[str, Any], - handoff: Mapping[str, Any], - validated: Mapping[str, Any], - *, - accepted_review: Mapping[str, Any] | None = None, - accepted_at: str | None = None, -) -> dict[str, Any]: - """Persist exactly one current accepted result in the existing task binding.""" - root = control_root.expanduser().resolve() - binding = load_task_execution_binding(root, str(task.get("plan_id") or ""), str(task.get("task_id") or "")) - accepted = build_accepted_task_result( - task, - binding, - handoff, - validated, - accepted_review=accepted_review, - accepted_at=accepted_at, - ) - updated = dict(binding) - updated["accepted_result"] = accepted - _persist_binding(updated, root) - return accepted -def load_current_accepted_task_result( - control_root: Path, task: Mapping[str, Any] -) -> tuple[dict[str, Any], dict[str, Any]]: - """Load compact post-acceptance authority without replaying its evidence history.""" - root = control_root.expanduser().resolve() - binding = load_task_execution_binding( - root, str(task.get("plan_id") or ""), str(task.get("task_id") or "") - ) - accepted = binding.get("accepted_result") - if not isinstance(accepted, Mapping): - raise SystemExit("accepted task result is missing") - assert_accepted_task_result_current(task, binding, accepted) - return binding, dict(accepted) -def _load_materialized_accepted_task_result( - root: Path, task: Mapping[str, Any] -) -> tuple[dict[str, Any], dict[str, Any]]: - """Load and authenticate compact acceptance without requiring old authority to be current.""" - binding = load_task_execution_binding( - root, str(task.get("plan_id") or ""), str(task.get("task_id") or "") - ) - prior = binding.get("accepted_result") - if not isinstance(prior, Mapping): - raise SystemExit("accepted task result is missing") - fields = frozenset(prior) - if prior.get("schema") != ACCEPTED_TASK_RESULT_SCHEMA or fields not in { - frozenset(LEGACY_ACCEPTED_TASK_RESULT_FIELDS), frozenset(ACCEPTED_TASK_RESULT_FIELDS) - }: - raise SystemExit("materialized accepted task result shape is invalid") - ownership = binding.get("ownership") if isinstance(binding.get("ownership"), Mapping) else {} - source = prior.get("accepted_source") - projection = prior.get("authority_projection") - disposition = prior.get("knowledge_disposition") - if ( - prior.get("plan_id") != str(task.get("plan_id") or "") - or prior.get("task_id") != str(task.get("task_id") or "") - or prior.get("binding_id") != ownership.get("binding_id") - or prior.get("baseline_identity") != _accepted_baseline_identity(binding) - or prior.get("invalidation") is not None - or not isinstance(source, Mapping) - or set(source) != {"head", "tree", "state_digest"} - or not isinstance(projection, Mapping) - or set(projection) != ACCEPTED_AUTHORITY_PROJECTION_FIELDS - or not isinstance(prior.get("owner_identity"), Mapping) - or ("knowledge_disposition" in prior and not isinstance(disposition, Mapping)) - ): - raise SystemExit("materialized accepted task result identity is invalid") - expected_state = _accepted_source_state_digest( - plan_id=str(prior["plan_id"]), task_id=str(prior["task_id"]), - binding_id=str(prior["binding_id"]), baseline_identity=prior["baseline_identity"], - head=source.get("head"), tree=source.get("tree"), authority_projection=projection, - knowledge_disposition=disposition if isinstance(disposition, Mapping) else None, - ) - if source.get("state_digest") != expected_state: - raise SystemExit("materialized accepted task result compact authority is invalid") - return binding, dict(prior) -def _validation_observation_task(task: Mapping[str, Any]) -> dict[str, Any]: - """Bind the complete compiled validation definition into observation authority.""" - projected = dict(task) - capability = ( - dict(task.get("evidence_capability")) - if isinstance(task.get("evidence_capability"), Mapping) - else {} - ) - capability["validation_definition_projection"] = [ - dict(item) - for item in _as_list(task.get("validation")) - if isinstance(item, Mapping) - ] - projected["evidence_capability"] = capability - return projected -def _transition_changed_paths(root: Path, previous: str, current: str) -> set[str]: - """Return both sides of every changed path without rename coalescing.""" - changed = subprocess.run( - [ - "git", "-C", str(root), "diff", "--no-renames", "--name-only", - previous, current, - ], - capture_output=True, - text=True, - ) - if changed.returncode: - raise RuntimeError("Git transition paths are unavailable") - return {path for path in changed.stdout.splitlines() if path} -def _claim_bound_validation_observations( - binding: Mapping[str, Any], - task: Mapping[str, Any], - evidence: Mapping[str, Any], - observation_ids: Sequence[str], - *, - reviewed_head: str | None = None, -) -> list[dict[str, Any]]: - """Resolve existing observations against current validation claim identities without replay.""" - - ids = list(observation_ids) - validation_items = [item for item in _as_list(task.get("validation")) if isinstance(item, Mapping)] - if ( - len(ids) != len(set(ids)) - or len(ids) != len(validation_items) - or any(not isinstance(item, str) or not item for item in ids) - ): - raise SystemExit("validation evidence is missing, duplicate, or extra") - class _ObservationUnavailable(RuntimeError): - pass - def no_validation_replay(_: dict[str, Any]) -> dict[str, Any]: - raise _ObservationUnavailable - execution_path = Path(str(binding.get("execution_path") or "")).expanduser().resolve() - store = _completion_provenance_module().ManagedProvenanceStore( - Path(str(binding.get("control_root") or task.get("workspace", {}).get("root") or "")) - / ".work-bundle/runtime/completion-provenance" - ) - def reviewed_observation(item: Mapping[str, Any], observation_id: str) -> dict[str, Any]: - if ( - not reviewed_head - or evidence.get("status") != "clean" - or evidence.get("entries") - or _as_list(task.get("depends_on")) - ): - raise _ObservationUnavailable - current_head = str(evidence.get("head") or "") - ancestor = subprocess.run( - ["git", "-C", str(execution_path), "merge-base", "--is-ancestor", reviewed_head, current_head], - capture_output=True, - text=True, - ) - history = subprocess.run( - [ - "git", "-C", str(execution_path), "rev-list", "--first-parent", - "--reverse", f"{reviewed_head}..{current_head}", - ], - capture_output=True, - text=True, - ) - if ancestor.returncode or history.returncode: - raise _ObservationUnavailable - policy = _completion_provenance_module().validation_reuse_policy(item) - files = task.get("files") if isinstance(task.get("files"), Mapping) else {} - claim_paths = [ - *_as_list(files.get("read")), - *_as_list(files.get("write")), - *policy["dependency_files"], - ] - previous = reviewed_head - for commit in filter(None, history.stdout.splitlines()): - try: - changed_paths = _transition_changed_paths(execution_path, previous, commit) - except RuntimeError as error: - raise _ObservationUnavailable from error - if any( - _write_scope_match(path, [str(scope) for scope in claim_paths]) - for path in changed_paths - ): - raise _ObservationUnavailable - previous = commit - - module = _completion_provenance_module() - record = module.load_observation(store, observation_id).to_dict() - if policy["max_age_seconds"] == 0: - finalization_prefix = ( - f"initial-acceptance:{task.get('plan_id')}:{task.get('task_id')}:" - ) - consumed_by = record.get("consumed_by_finalization") - if ( - not isinstance(consumed_by, str) - or not consumed_by.startswith(finalization_prefix) - or consumed_by == finalization_prefix - ): - raise _ObservationUnavailable - producer_item = dict(item) - producer_item["evidence_reuse"] = { - **policy, - "max_age_seconds": 86400, - } - policy = module.validation_reuse_policy(producer_item) - reviewed_tree = _git(execution_path, "rev-parse", f"{reviewed_head}^{{tree}}").strip() - tree_listing = subprocess.run( - ["git", "-C", str(execution_path), "ls-tree", "-rz", "--full-tree", reviewed_head], - capture_output=True, - text=True, - ) - if tree_listing.returncode or record.get("product_tree") != reviewed_tree: - raise _ObservationUnavailable - index = [] - for entry in filter(None, tree_listing.stdout.split("\0")): - metadata, path = entry.split("\t", 1) - mode, kind, oid = metadata.split(" ", 2) - if kind != "blob": - raise _ObservationUnavailable - index.append((path, f"{mode} {oid} 0")) - definition = { - key: item.get(key) - for key in ( - "id", "kind", "command", "mechanism", "expected", - "acceptable_results", "invariant_ids", "digest", "proves", - ) - } - helper_dir = Path(module.__file__).parent - runners = { - name: hashlib.sha256((helper_dir / name).read_bytes()).hexdigest() - for name in ( - "completion_provenance.py", "execution_context.py", - "evaluation_identity.py", "repository_preflight.py", - ) - } - claim_task = _validation_observation_task(task) - authority = { - key: claim_task.get(key) - for key in ( - "source_ids", "requirements", "constraints", "interfaces", - "truth_basis", "files", "evidence_capability", - ) - } - bound = { - key: binding[key] - for key in ( - "workspace_id", "execution_id", "repository_id", "plan_id", - "task_id", "execution_path", - ) - } - source = { - "tree": reviewed_tree, - "index_digest": module._canonical_digest(index), - } - expected_state = module._canonical_digest( - { - "source": source, - "environment": module.validation_environment_identity(execution_path, policy), - "binding": bound, - "head": reviewed_head if policy["include_head"] else None, - } - ) - expected_oracle = module._canonical_digest( - { - "authority": authority, - "check": definition, - "runner": runners, - "freshness_policy": policy, - } - ) - if ( - record.get("command_digest") != module._canonical_digest(definition) - or record.get("state_digest") != expected_state - or record.get("oracle_digest") != expected_oracle - or record.get("mutation_epoch") != store.mutation_epoch - or datetime.fromisoformat(str(record.get("freshness_deadline")).replace("Z", "+00:00")) - < datetime.now(timezone.utc) - ): - raise _ObservationUnavailable - return record - matched: list[dict[str, Any]] = [] - for position, item in enumerate(validation_items, start=1): - try: - observation = _completion_provenance_module().observe_validation( - binding, - _validation_observation_task(task), - item, - evidence, - no_validation_replay, - lambda: capture_repository_evidence(execution_path), - ) - record = _completion_provenance_module().load_observation( - store, str(observation.get("observation_id") or "") - ).to_dict() - except (_ObservationUnavailable, _completion_provenance_module().CompletionProvenanceError) as error: - try: - record = reviewed_observation(item, ids[position - 1]) - observation = {"observation_id": record["observation_id"]} - except ( - _ObservationUnavailable, - _completion_provenance_module().CompletionProvenanceError, - KeyError, - TypeError, - ValueError, - ) as fallback_error: - raise SystemExit( - "an existing current claim-bound validation observation is required" - ) from fallback_error - if record["result"]["exit_code"] != 0: - raise SystemExit("validation evidence is not a passing observation") - matched.append( - { - "id": item.get("id") or f"validation-{position:03d}", - "command": item.get("command"), - "invariant_ids": list(_as_list(item.get("invariant_ids"))), - "observation_id": observation["observation_id"], - "result": "passed", - "product_tree": record["product_tree"], - } - ) - if set(ids) != {item["observation_id"] for item in matched}: - raise SystemExit("validation evidence does not bind current task claims") - return matched -def materialize_accepted_task_review( - control_root: Path, - task: Mapping[str, Any], - review_reference: Mapping[str, Any], - causal_classification: Mapping[str, Any], - *, - accepted_at: str | None = None, - validation_evidence_ids: Sequence[str] | None = None, - executor_handoff: Mapping[str, Any] | None = None, - validated_executor_result: Mapping[str, Any] | None = None, -) -> dict[str, Any]: - """Compose executor authority with one published current task review.""" - root = control_root.expanduser().resolve() - initial_acceptance = ( - executor_handoff is not None or validated_executor_result is not None - ) - if initial_acceptance: - expected_initial = { - "causal_class": "initial_acceptance", - "affected_task": str(task.get("task_id") or ""), - "authorized_lifecycle_action": "materialize_accepted_result", - } - if ( - not isinstance(executor_handoff, Mapping) - or not isinstance(validated_executor_result, Mapping) - or not isinstance(causal_classification, Mapping) - or dict(causal_classification) != expected_initial - or validation_evidence_ids is not None - ): - raise SystemExit( - "initial accepted task review requires exact executor result authority" - ) - binding = load_task_execution_binding( - root, str(task.get("plan_id") or ""), str(task.get("task_id") or "") - ) - try: - from review_runtime import ( - ReviewContractError, - load_stored_review, - stored_review_target_identity, - ) - target_identity = stored_review_target_identity(root, review_reference) - review, validated_review = load_stored_review( - root, - review_reference, - current_target_identity=target_identity, - ) - except (ReviewContractError, KeyError, TypeError, ValueError) as error: - raise SystemExit(f"Accepted initial task review is invalid: {error}") from error - task_id = str(task.get("task_id") or "") - if ( - task.get("review_required") is not True - or validated_review.verdict != "accepted" - or validated_review.review_mode != "initial" - or validated_review.repair_frontier is not None - or validated_review.review_reset is not None - or validated_review.target_identity.get("artifact_id") != task_id - ): - raise SystemExit("accepted initial task review must bind the exact current task") - owner = validated_executor_result.get("task_ownership") - if not isinstance(owner, Mapping): - raise SystemExit("accepted initial task review requires validated executor ownership") - if validated_review.reviewer.get("agent_id") == owner.get("agent_id"): - raise SystemExit("accepted initial task review must be independent from the executor owner") - execution_path = Path(str(binding.get("execution_path") or "")).expanduser().resolve() - try: - evidence = capture_repository_evidence(execution_path) - except RuntimeError as error: - raise SystemExit("accepted initial task review Git identity is unavailable") from error - identity = validated_review.target_identity - if ( - evidence.get("status") != "clean" - or evidence.get("entries") - or evidence.get("head") != identity.get("revision") - or evidence.get("tree") != identity.get("source_tree") - or review.get("reviewed_head") != identity.get("revision") - ): - raise SystemExit( - "accepted initial task review does not match the clean exact source identity" - ) - accepted = build_accepted_task_result( - task, - binding, - executor_handoff, - validated_executor_result, - accepted_review=review, - accepted_at=accepted_at, - ) - updated = dict(binding) - updated["accepted_result"] = accepted - _persist_binding(updated, root) - return accepted - - expected_classification = { - "causal_class", "affected_task", "authorized_lifecycle_action", - } - if ( - not isinstance(causal_classification, Mapping) - or set(causal_classification) != expected_classification - or causal_classification.get("causal_class") not in { - "claim_relevant_drift", "implementation_defect", - } - or causal_classification.get("affected_task") != str(task.get("task_id") or "") - or causal_classification.get("authorized_lifecycle_action") - != "rematerialize_accepted_result" - ): - raise SystemExit("accepted task review requires an exact controller causal classification") - binding, prior = _load_materialized_accepted_task_result(root, task) - if task.get("review_required") is not True: - raise SystemExit("accepted task repair review requires mandatory task review authority") - try: - from review_runtime import ( - ReviewContractError, - load_stored_review, - stored_review_target_identity, - ) - - target_identity = stored_review_target_identity(root, review_reference) - review, validated_review = load_stored_review( - root, - review_reference, - current_target_identity=target_identity, - ) - except (ReviewContractError, KeyError, TypeError, ValueError) as error: - raise SystemExit(f"Accepted task repair review is invalid: {error}") from error - task_id = str(task.get("task_id") or "") - plan_id = str(task.get("plan_id") or "") - frontier = validated_review.repair_frontier - reset = validated_review.review_reset - previous_review = review.get("previous_review") - previous_kind = ( - previous_review.get("review_target_kind") - if isinstance(previous_review, Mapping) - else None - ) - previous_identity = ( - frontier["previous_reviewed_identity"] if frontier is not None - else previous_review.get("target_identity") if isinstance(previous_review, Mapping) - else {} - ) - previous_artifact = previous_identity.get("artifact_id") - previous_owner_matches = ( - previous_kind == "task" and previous_artifact == task_id - ) or ( - previous_kind == "stage" - and previous_review.get("stage") == "integrated_implementation" - and previous_artifact == plan_id - ) - if ( - validated_review.verdict != "accepted" - or validated_review.target_identity.get("artifact_id") != task_id - or not previous_owner_matches - ): - raise SystemExit("accepted task review must bind the exact current task and predecessor owner") - if validated_review.review_mode == "repair": - if frontier is None or frontier["repaired_identity"].get("artifact_id") != task_id: - raise SystemExit("accepted task repair review must bind the exact repair frontier") - elif ( - reset is None - or reset.get("reason_class") not in {"scope", "validation_allocation"} - or reset.get("prior_review_id") != prior.get("review_id") - ): - raise SystemExit("accepted task initial review must reset exact prior scope or validation authority") - reviewer = validated_review.reviewer - owner = prior.get("owner_identity") if isinstance(prior.get("owner_identity"), Mapping) else {} - if reviewer.get("agent_id") == owner.get("agent_id"): - raise SystemExit("accepted task repair review must be independent from the executor owner") - execution_path = Path(str(binding.get("execution_path") or "")).expanduser().resolve() - try: - evidence = capture_repository_evidence(execution_path) - except RuntimeError as error: - raise SystemExit("accepted task repair review Git identity is unavailable") from error - identity = validated_review.target_identity - reviewed_head = str(identity.get("revision") or "") - if ( - evidence.get("status") != "clean" - or evidence.get("entries") - or review.get("reviewed_head") != reviewed_head - ): - raise SystemExit("accepted task repair review does not match the clean exact source identity") - if not re.fullmatch(r"[0-9a-f]{40}|[0-9a-f]{64}", reviewed_head): - raise SystemExit("accepted task repair review target revision is invalid") - reviewed_commit = subprocess.run( - ["git", "-C", str(execution_path), "rev-parse", "--verify", f"{reviewed_head}^{{commit}}"], - capture_output=True, - text=True, - ) - reviewed_tree = subprocess.run( - ["git", "-C", str(execution_path), "rev-parse", "--verify", f"{reviewed_head}^{{tree}}"], - capture_output=True, - text=True, - ) - if ( - reviewed_commit.returncode - or reviewed_tree.returncode - or reviewed_commit.stdout.strip() != reviewed_head - ): - raise SystemExit("accepted task repair review target revision does not resolve exactly") - if reviewed_tree.stdout.strip() != identity.get("source_tree"): - raise SystemExit("accepted task repair review target revision/tree identity is mismatched") - ancestor = subprocess.run( - [ - "git", "-C", str(execution_path), "merge-base", "--is-ancestor", - reviewed_head, str(evidence.get("head") or ""), - ], - capture_output=True, - text=True, - ) - if ancestor.returncode != 0: - raise SystemExit("accepted task repair review target is not an ancestor of current HEAD") - - prior_validation_digest = prior["authority_projection"]["validation_obligations_digest"] - current_validation_digest = semantic_digest(_accepted_validation_projection(task)) - accepted_source_changed = reviewed_head != prior["accepted_source"]["head"] - if validation_evidence_ids is None and not accepted_source_changed and ( - prior_validation_digest == current_validation_digest - ): - current_validation_ids = list(prior["validation_evidence_ids"]) - else: - matched_validation = _claim_bound_validation_observations( - binding, - task, - evidence, - validation_evidence_ids or prior["validation_evidence_ids"], - reviewed_head=reviewed_head, - ) - current_validation_ids = sorted(item["observation_id"] for item in matched_validation) - - authority_projection = _accepted_authority_projection( - task, binding, accepted_review=review, owner_identity=prior["owner_identity"] - ) - accepted_source = {"head": reviewed_head, "tree": reviewed_tree.stdout.strip()} - knowledge_disposition = prior.get("knowledge_disposition") - accepted_source["state_digest"] = _accepted_source_state_digest( - plan_id=str(prior["plan_id"]), - task_id=str(prior["task_id"]), - binding_id=str(prior["binding_id"]), - baseline_identity=prior["baseline_identity"], - head=accepted_source["head"], - tree=accepted_source["tree"], - authority_projection=authority_projection, - knowledge_disposition=( - knowledge_disposition if isinstance(knowledge_disposition, Mapping) else None - ), - ) - accepted = dict(prior) - accepted.update( - accepted_source=accepted_source, - authority_projection=authority_projection, - review_id=validated_review.review_id, - accepted_at=accepted_at or datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), - validation_evidence_ids=current_validation_ids, - ) - updated = dict(binding) - updated["accepted_result"] = accepted - _persist_binding(updated, root) - return accepted - - -def materialize_accepted_task_repair_review( - control_root: Path, - task: Mapping[str, Any], - review: Mapping[str, Any], - *, - accepted_at: str | None = None, - validation_evidence_ids: Sequence[str] | None = None, -) -> dict[str, Any]: - """Compatibility wrapper for unchanged-authority standalone task repair review.""" - - return materialize_accepted_task_review( - control_root, - task, - review, - { - "causal_class": "implementation_defect", - "affected_task": str(task.get("task_id") or ""), - "authorized_lifecycle_action": "rematerialize_accepted_result", - }, - accepted_at=accepted_at, - validation_evidence_ids=validation_evidence_ids, - ) - - -def has_persisted_accepted_task_result( - control_root: Path, plan_id: str, task_id: str -) -> bool: - """Detect the irreversible accepted-result lifecycle without validating history.""" - - path = _binding_path(control_root.expanduser().resolve(), plan_id, task_id) - if not path.exists(): - return False - binding = _read_binding_file(path) - return isinstance(binding.get("accepted_result"), Mapping) - - -def capture_task_baseline_once(binding: dict[str, Any], control_root: Path | None = None) -> dict[str, Any]: - existing = binding.get("baseline") - if isinstance(existing, dict) and existing.get("head"): - return binding - try: - evidence = capture_repository_evidence(Path(str(binding["execution_path"]))) - except RuntimeError as error: - raise SystemExit(str(error)) from error - updated = dict(binding) - updated["baseline"] = evidence - root = control_root or Path(str(binding.get("control_root") or "")) - if not root.is_dir(): - raise SystemExit("Task execution binding control root is required to persist baseline") - _persist_binding(updated, root) - return updated - - -FILE_DIGEST_RE = re.compile(r"^[0-9a-f]{64}$") - - -def _write_scope_file_digest(execution_root: Path, task: dict[str, Any]) -> str: - digest = hashlib.sha256() - files = task.get("files") if isinstance(task.get("files"), dict) else {} - try: - paths = sorted( - canonical_relative_path(str(relative)) for relative in _as_list(files.get("write")) - ) - except OwnershipBlocker as error: - raise SystemExit(f"Declared write scope is unsafe: {error.reason}") from error - for relative in paths: - digest.update(relative.encode("utf-8")) - digest.update(b"\0") - path = execution_root / relative - if path.is_file() and not path.is_symlink(): - digest.update(path.read_bytes()) - else: - digest.update(b"MISSING") - digest.update(b"\n") - return digest.hexdigest() - - -def _run_named_inspection(mechanism: str, execution_root: Path, task: dict[str, Any], item: dict[str, Any]) -> str: - if mechanism != "named-harness-file-digest": - raise SystemExit(f"Unknown inspection mechanism: {mechanism}") - expected = str(item.get("digest") or "").strip().lower() - if not FILE_DIGEST_RE.fullmatch(expected): - raise SystemExit("named-harness-file-digest requires a 64-character hex digest") - actual = _write_scope_file_digest(execution_root, task) - return "passed" if actual == expected else "failed" - - -def _observe_validation_item(item: dict[str, Any], execution_root: Path, task: dict[str, Any], receipt: dict | None = None) -> dict[str, Any]: - command = str(item.get("command") or "").strip() - kind = str(item.get("kind") or "").strip().lower() - if kind not in VALIDATION_KINDS: - raise SystemExit( - "Task validation kind must be process or inspection; untyped structured validation is legacy-untyped" - ) - allowed = _acceptable_validation_results(item) - expected = str(item.get("expected") or "").strip().lower() - if expected in {"skip", "skipped"} and "skipped" in allowed: - observed = {"command": command, "result": "skipped", "kind": kind} - if kind == "inspection": - observed["mechanism"] = str(item.get("mechanism") or "").strip() - observed.update({"id": item.get("id"), "invariant_ids": list(_as_list(item.get("invariant_ids")))}) - return observed - if kind == "inspection": - mechanism = str(item.get("mechanism") or "").strip() - if not mechanism: - raise SystemExit("Inspection validation requires a named harness-owned mechanism") - result_value = _run_named_inspection(mechanism, execution_root, task, item) - return {"command": command, "result": result_value, "kind": "inspection", "mechanism": mechanism, "id": item.get("id"), "invariant_ids": list(_as_list(item.get("invariant_ids")))} - started_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") - completed = subprocess.run( - command, - shell=True, - cwd=str(execution_root), - capture_output=True, - text=True, - check=False, - ) - if receipt is not None: - receipt.update(exit_code=completed.returncode, - stdout_digest=hashlib.sha256(completed.stdout.encode()).hexdigest(), - stderr_digest=hashlib.sha256(completed.stderr.encode()).hexdigest(), - started_at=started_at, completed_at=datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")) - return { - "command": command, - "result": "passed" if completed.returncode == 0 else "failed", - "kind": "process", - "id": item.get("id"), - "invariant_ids": list(_as_list(item.get("invariant_ids"))), - } - - -def _path_is_forbidden(relative: str, forbidden: list[str]) -> bool: - try: - normalized = canonical_relative_path(relative) - except OwnershipBlocker as error: - raise SystemExit(f"Observed mutation path is unsafe: {relative}") from error - for pattern in forbidden: - try: - pat = canonical_relative_path(str(pattern), allow_tree_pattern=True) - except OwnershipBlocker as error: - raise SystemExit(f"Declared forbidden scope is unsafe: {pattern}") from error - if pat.endswith("/**"): - prefix = pat[:-3] - if normalized == prefix or normalized.startswith(f"{prefix}/"): - return True - elif normalized == pat: - return True - return False - - -def _assert_task_caused_delta_in_write_scope( - caused: list[str], - task_files: dict[str, Any], - *, - accepted_dependency_paths: set[str] | None = None, -) -> None: - write_paths = [str(path) for path in _as_list(task_files.get("write"))] - forbidden = [str(path) for path in _as_list(task_files.get("forbidden"))] - for relative in caused: - if relative in (accepted_dependency_paths or set()): - continue - if _path_is_forbidden(relative, forbidden) or not _write_scope_match(relative, write_paths): - raise SystemExit(f"Unauthorized task-caused delta outside write scope: {relative}") - - -def _exact_handoff_reference( - handoff_root: Path, - reference: object, -) -> tuple[Path, dict[str, Any]]: - if not isinstance(reference, Mapping) or set(reference) != {"handoff_id", "handoff_sha256"}: - raise SystemExit("accepted dependency handoff reference must use the closed identity shape") - handoff_id = str(reference["handoff_id"]) - matches: list[tuple[Path, dict[str, Any]]] = [] - for path in sorted(handoff_root.glob("executor/*/*")): - if not path.is_file() or path.is_symlink(): - continue - try: - document, _ = _read_structured(path) - except (OSError, SystemExit, ValueError): - continue - if document.get("id") == handoff_id: - matches.append((path, document)) - if len(matches) != 1: - raise SystemExit(f"accepted dependency handoff identity is missing or ambiguous: {handoff_id}") - path, handoff = matches[0] - if hashlib.sha256(path.read_bytes()).hexdigest() != str(reference["handoff_sha256"]): - raise SystemExit(f"accepted dependency handoff identity is stale: {handoff_id}") - return path, handoff - - -def _review_without_history(review: Mapping[str, Any]) -> dict[str, Any]: - return {key: value for key, value in review.items() if key != "previous_review"} - - -RECOVERY_RECEIPT_SCHEMA = "accepted-result-recovery-receipt-v1" -RECOVERY_RECEIPT_KEYS = { - "receipt_id", - "schema", - "plan_id", - "task_id", - "binding_id", - "binding_sha256", - "baseline_head", - "baseline_tree", - "expected_base_head", - "expected_base_tree", - "expected_base_query", - "proposed_recovered_result", - "observed_at", - "freshness", -} -LEGACY_RECOVERY_RECEIPT_KEYS = { - "receipt_id", - "schema", - "plan_id", - "task_id", - "binding_id", - "binding_sha256", - "baseline_head", - "baseline_tree", - "expected_base_head", - "expected_base_tree", - "queried_historical_revision", - "handoff_stores", - "handoff_index", - "absence_result", - "observed_at", - "freshness", -} - - -def _recovery_receipt_path( - control_root: Path, plan_id: str, task_id: str, receipt_id: str -) -> Path: - if not all(SAFE_ID_RE.fullmatch(value) for value in (plan_id, task_id, receipt_id)): - raise SystemExit("accepted-result recovery receipt identity is unsafe") - return ( - control_root.expanduser().resolve() - / ".work-bundle/runtime/execution" - / plan_id - / task_id - / "accepted-result-recovery" - / f"{receipt_id}.json" - ) - - -def _is_recoverable_accepted_base( - handoff: Mapping[str, Any], - plan_id: str, - task_id: str, - expected_base_head: str, - expected_base_tree: str, -) -> bool: - related = handoff.get("related") if isinstance(handoff.get("related"), Mapping) else {} - result = handoff.get("result") if isinstance(handoff.get("result"), Mapping) else {} - review = ( - handoff.get("acceptance_review") - if isinstance(handoff.get("acceptance_review"), Mapping) - else {} - ) - reset = review.get("review_reset") if isinstance(review.get("review_reset"), Mapping) else {} - if reset.get("reason_class") == "authority": - # This is the newly reconstructed whole-task result, never the missing - # historical accepted base whose absence authorizes reconstruction. - return False - if ( - handoff.get("type") != "executor-result" - or related.get("plan") != plan_id - or related.get("task") != task_id - or result.get("state") != "completed" - or review.get("verdict") != "accept" - ): - return False - identity = review.get("target_identity") if isinstance(review.get("target_identity"), Mapping) else {} - if ( - identity.get("artifact_id") != task_id - or identity.get("revision") != expected_base_head - or identity.get("source_tree") != expected_base_tree - or review.get("reviewed_head") != expected_base_head - ): - return False - try: - from review_runtime import ReviewContractError, validate_task_acceptance_review - - validate_task_acceptance_review(review) - except (ReviewContractError, KeyError, TypeError, ValueError): - return False - return True - - -def _accepted_base_query_snapshot( - control_root: Path, - plan_id: str, - task_id: str, - expected_base_head: str, - expected_base_tree: str, - proposed_handoff_id: str, - proposed_review_id: str, - final_head: str, - final_tree: str, -) -> dict[str, Any]: - handoff_root = control_root / ".work-bundle/orchestration/handoff" - recoverable_ids: list[str] = [] - proposal_records: list[dict[str, Any]] = [] - for status in ("active", "archived"): - for path in sorted((handoff_root / "executor" / status).glob("*")): - if not path.is_file() or path.is_symlink(): - continue - try: - handoff, _ = _read_structured(path) - except (OSError, SystemExit, ValueError): - continue - handoff_id = str(handoff.get("id") or "") - if _is_recoverable_accepted_base( - handoff, - plan_id, - task_id, - expected_base_head, - expected_base_tree, - ): - recoverable_ids.append(handoff_id) - related = handoff.get("related") if isinstance(handoff.get("related"), Mapping) else {} - review = ( - handoff.get("acceptance_review") - if isinstance(handoff.get("acceptance_review"), Mapping) - else {} - ) - identity = ( - review.get("target_identity") - if isinstance(review.get("target_identity"), Mapping) - else {} - ) - if handoff_id != proposed_handoff_id and review.get("review_id") != proposed_review_id: - continue - result = handoff.get("result") if isinstance(handoff.get("result"), Mapping) else {} - proposal_records.append( - { - "handoff_id": handoff_id, - "review_id": str(review.get("review_id") or ""), - "path": path.relative_to(control_root).as_posix(), - "sha256": hashlib.sha256(path.read_bytes()).hexdigest(), - "exact": ( - handoff_id == proposed_handoff_id - and handoff.get("type") == "executor-result" - and related.get("plan") == plan_id - and related.get("task") == task_id - and result.get("state") == "completed" - and review.get("review_id") == proposed_review_id - and review.get("reviewed_head") == final_head - and identity.get("artifact_id") == task_id - and identity.get("revision") == final_head - and identity.get("source_tree") == final_tree - ), - } - ) - - index_path = handoff_root / "index.jsonl" - if not index_path.is_file() or index_path.is_symlink(): - raise SystemExit("accepted-result recovery requires the native handoff index") - proposal_index_entries: list[dict[str, Any]] = [] - for number, line in enumerate(index_path.read_text(encoding="utf-8").splitlines(), 1): - if not line.strip(): - continue - try: - entry = json.loads(line) - except json.JSONDecodeError as error: - raise SystemExit(f"accepted-result recovery handoff index is invalid at line {number}") from error - if not isinstance(entry, dict): - raise SystemExit(f"accepted-result recovery handoff index is invalid at line {number}") - if entry.get("id") == proposed_handoff_id: - proposal_index_entries.append(entry) - - exact_records = [record for record in proposal_records if record["exact"]] - proposal_state = "absent" - if proposal_records or proposal_index_entries: - if len(proposal_records) != 1 or len(proposal_index_entries) != 1: - proposal_state = "ambiguous" - elif not exact_records: - proposal_state = "mismatch" - else: - proposal_path = str(exact_records[0]["path"]) - index_entry = proposal_index_entries[0] - if ( - index_entry.get("related_plan") != plan_id - or index_entry.get("related_task") != task_id - or index_entry.get("path") != proposal_path - ): - proposal_state = "mismatch" - else: - proposal_state = "published" - - query_identity = { - "plan_id": plan_id, - "task_id": task_id, - "expected_base_head": expected_base_head, - "expected_base_tree": expected_base_tree, - } - return { - "expected_base_query": { - "sha256": semantic_digest(query_identity), - "result": "present" if recoverable_ids else "absent", - }, - "recoverable_base_handoff_ids": sorted(recoverable_ids), - "proposal_state": proposal_state, - } - - -def _legacy_recovery_global_snapshot( - control_root: Path, - plan_id: str, - task_id: str, - expected_base_head: str, - expected_base_tree: str, -) -> dict[str, Any]: - """Reconstruct the obsolete revision-11 global-digest snapshot.""" - - handoff_root = control_root / ".work-bundle/orchestration/handoff" - stores: dict[str, dict[str, Any]] = {} - for status in ("active", "archived"): - records: list[dict[str, str]] = [] - for path in sorted((handoff_root / "executor" / status).glob("*")): - if not path.is_file() or path.is_symlink(): - continue - try: - handoff, _ = _read_structured(path) - except (OSError, SystemExit, ValueError): - continue - related = handoff.get("related") if isinstance(handoff.get("related"), Mapping) else {} - if related.get("plan") != plan_id or related.get("task") != task_id: - continue - records.append( - { - "handoff_id": str(handoff.get("id") or ""), - "path": path.relative_to(control_root).as_posix(), - "sha256": hashlib.sha256(path.read_bytes()).hexdigest(), - } - ) - stores[status] = { - "store_id": f"executor/{status}", - "records": records, - "sha256": semantic_digest(records), - } - - index_path = handoff_root / "index.jsonl" - if not index_path.is_file() or index_path.is_symlink(): - raise SystemExit("accepted-result recovery requires the native handoff index") - index_entries: list[dict[str, Any]] = [] - for number, line in enumerate(index_path.read_text(encoding="utf-8").splitlines(), 1): - if not line.strip(): - continue - try: - entry = json.loads(line) - except json.JSONDecodeError as error: - raise SystemExit( - f"accepted-result recovery handoff index is invalid at line {number}" - ) from error - if not isinstance(entry, dict): - raise SystemExit(f"accepted-result recovery handoff index is invalid at line {number}") - if entry.get("related_plan") == plan_id and entry.get("related_task") == task_id: - index_entries.append(entry) - index_identity = { - "index_id": "handoff/index.jsonl", - "path": index_path.relative_to(control_root).as_posix(), - "sha256": hashlib.sha256(index_path.read_bytes()).hexdigest(), - "projected_entries_sha256": semantic_digest(index_entries), - } - historical_revision = semantic_digest( - { - "expected_base_head": expected_base_head, - "expected_base_tree": expected_base_tree, - "handoff_stores": stores, - "handoff_index": index_identity, - } - ) - return { - "queried_historical_revision": historical_revision, - "handoff_stores": stores, - "handoff_index": index_identity, - } - - -def _valid_recovered_result( - handoff: Mapping[str, Any], plan_id: str, task_id: str -) -> bool: - related = handoff.get("related") if isinstance(handoff.get("related"), Mapping) else {} - result = handoff.get("result") if isinstance(handoff.get("result"), Mapping) else {} - review = ( - handoff.get("acceptance_review") - if isinstance(handoff.get("acceptance_review"), Mapping) - else {} - ) - reset = review.get("review_reset") if isinstance(review.get("review_reset"), Mapping) else {} - evidence = review.get("evidence") if isinstance(review.get("evidence"), Mapping) else {} - reviewer = review.get("reviewer") if isinstance(review.get("reviewer"), Mapping) else {} - identity = review.get("target_identity") if isinstance(review.get("target_identity"), Mapping) else {} - if ( - handoff.get("type") != "executor-result" - or related.get("plan") != plan_id - or related.get("task") != task_id - or result.get("state") != "completed" - or review.get("verdict") != "accept" - or review.get("review_mode") != "initial" - or review.get("review_target_kind") != "task" - or review.get("reviewer_independent") is not True - or review.get("repair_frontier") is not None - or reset.get("reason_class") != "authority" - or evidence.get("unavailable_evidence") != [] - or reviewer.get("capability") != "judgment" - or identity.get("artifact_id") != task_id - or review.get("reviewed_head") != identity.get("revision") - ): - return False - try: - from review_runtime import ReviewContractError, validate_task_acceptance_review - - validate_task_acceptance_review(review) - owner = normalize_subagent_provenance( - handoff.get("delegation_evidence") - if isinstance(handoff.get("delegation_evidence"), Mapping) - else None - ) - except (ReviewContractError, OwnershipBlocker, KeyError, TypeError, ValueError): - return False - return reviewer.get("agent_id") != owner.get("agent_id") - - -def _write_query_scoped_recovery_receipt( - control_root: Path, - plan_id: str, - task_id: str, - binding: Mapping[str, Any], - binding_path: Path, - expected_base_head: str, - expected_base_tree: str, - proposed_handoff_id: str, - proposed_review_id: str, - final_head: str, - final_tree: str, - expected_base_query: Mapping[str, Any], -) -> dict[str, str]: - baseline = binding.get("baseline") if isinstance(binding.get("baseline"), Mapping) else {} - observed_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") - receipt_id = ( - f"accepted-result-recovery-{task_id}-" - f"{str(expected_base_query['sha256'])[:12]}-" - f"{hashlib.sha256(observed_at.encode()).hexdigest()[:12]}" - ) - receipt = { - "receipt_id": receipt_id, - "schema": RECOVERY_RECEIPT_SCHEMA, - "plan_id": plan_id, - "task_id": task_id, - "binding_id": str((binding.get("ownership") or {}).get("binding_id") or ""), - "binding_sha256": hashlib.sha256(binding_path.read_bytes()).hexdigest(), - "baseline_head": str(baseline.get("head") or ""), - "baseline_tree": str(baseline.get("tree") or ""), - "expected_base_head": expected_base_head, - "expected_base_tree": expected_base_tree, - "expected_base_query": dict(expected_base_query), - "proposed_recovered_result": { - "handoff_id": proposed_handoff_id, - "review_id": proposed_review_id, - "final_head": final_head, - "final_tree": final_tree, - }, - "observed_at": observed_at, - "freshness": "current_validation_attempt", - } - path = _recovery_receipt_path(control_root, plan_id, task_id, receipt_id) - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(json.dumps(receipt, indent=2, sort_keys=True) + "\n", encoding="utf-8") - return { - "receipt_id": receipt_id, - "receipt_sha256": hashlib.sha256(path.read_bytes()).hexdigest(), - } - - -def create_accepted_base_absence_receipt( - control_root: Path, - plan_id: str, - task_id: str, - expected_base_head: str, - expected_base_tree: str, - proposed_handoff_id: str, - proposed_review_id: str, - final_head: str, - final_tree: str, -) -> dict[str, str]: - """Persist helper-observed proof that no native accepted-result base survives.""" - - control_root = control_root.expanduser().resolve() - binding_path = _binding_path(control_root, plan_id, task_id) - binding = load_task_execution_binding(control_root, plan_id, task_id) - baseline = binding.get("baseline") if isinstance(binding.get("baseline"), Mapping) else {} - baseline_head = str(baseline.get("head") or "") - baseline_tree = str(baseline.get("tree") or "") - if not baseline_head or not baseline_tree: - raise SystemExit("accepted-result recovery requires the original one-time task baseline") - if not SAFE_ID_RE.fullmatch(proposed_handoff_id) or not SAFE_ID_RE.fullmatch(proposed_review_id): - raise SystemExit("accepted-result recovery proposed identity is unsafe") - execution_root = Path(str(binding.get("execution_path") or "")).resolve() - resolved_expected_head = _resolve_commit(execution_root, expected_base_head) - resolved_expected_tree = _git( - execution_root, "rev-parse", f"{resolved_expected_head}^{{tree}}" - ).strip() - if resolved_expected_tree != expected_base_tree: - raise SystemExit("accepted-result recovery expected base Git identity is mismatched") - resolved_final_head = _resolve_commit(execution_root, final_head) - resolved_final_tree = _git(execution_root, "rev-parse", f"{resolved_final_head}^{{tree}}").strip() - if resolved_final_tree != final_tree: - raise SystemExit("accepted-result recovery proposed final Git identity is mismatched") - snapshot = _accepted_base_query_snapshot( - control_root, - plan_id, - task_id, - resolved_expected_head, - resolved_expected_tree, - proposed_handoff_id, - proposed_review_id, - resolved_final_head, - resolved_final_tree, - ) - if snapshot["recoverable_base_handoff_ids"]: - raise SystemExit("accepted-result recovery rejected: recoverable accepted base handoff exists") - if snapshot["proposal_state"] != "absent": - raise SystemExit("accepted-result recovery rejected: proposed result already exists or is ambiguous") - return _write_query_scoped_recovery_receipt( - control_root, - plan_id, - task_id, - binding, - binding_path, - resolved_expected_head, - resolved_expected_tree, - proposed_handoff_id, - proposed_review_id, - resolved_final_head, - resolved_final_tree, - snapshot["expected_base_query"], - ) - - -def adopt_existing_recovered_result( - control_root: Path, - plan_id: str, - task_id: str, - expected_base_head: str, - expected_base_tree: str, - handoff_id: str, - handoff_sha256: str, - review_id: str, - final_head: str, - final_tree: str, - prior_receipt_reference: object, -) -> dict[str, str]: - """Adopt one already-published result whose rev11 receipt only globally staled.""" - - control_root = control_root.expanduser().resolve() - if not isinstance(prior_receipt_reference, Mapping) or set(prior_receipt_reference) != { - "receipt_id", "receipt_sha256" - }: - raise SystemExit("post-publication adoption prior receipt reference is invalid") - if not SAFE_ID_RE.fullmatch(handoff_id) or not SAFE_ID_RE.fullmatch(review_id): - raise SystemExit("post-publication adoption proposed identity is unsafe") - - binding_path = _binding_path(control_root, plan_id, task_id) - binding = load_task_execution_binding(control_root, plan_id, task_id) - baseline = binding.get("baseline") if isinstance(binding.get("baseline"), Mapping) else {} - binding_id = str((binding.get("ownership") or {}).get("binding_id") or "") - binding_sha256 = hashlib.sha256(binding_path.read_bytes()).hexdigest() - execution_root = Path(str(binding.get("execution_path") or "")).resolve() - resolved_expected_head = _resolve_commit(execution_root, expected_base_head) - resolved_expected_tree = _git( - execution_root, "rev-parse", f"{resolved_expected_head}^{{tree}}" - ).strip() - if resolved_expected_tree != expected_base_tree: - raise SystemExit("post-publication adoption expected base Git identity is mismatched") - resolved_final_head = _resolve_commit(execution_root, final_head) - resolved_final_tree = _git( - execution_root, "rev-parse", f"{resolved_final_head}^{{tree}}" - ).strip() - if resolved_final_tree != final_tree: - raise SystemExit("post-publication adoption final Git identity is mismatched") - - prior_receipt_id = str(prior_receipt_reference["receipt_id"]) - prior_path = _recovery_receipt_path(control_root, plan_id, task_id, prior_receipt_id) - if not prior_path.is_file() or prior_path.is_symlink(): - raise SystemExit("post-publication adoption prior receipt is missing") - if hashlib.sha256(prior_path.read_bytes()).hexdigest() != str( - prior_receipt_reference["receipt_sha256"] - ): - raise SystemExit("post-publication adoption prior receipt digest is stale") - try: - prior = json.loads(prior_path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError) as error: - raise SystemExit("post-publication adoption prior receipt is invalid") from error - if not isinstance(prior, dict) or set(prior) != LEGACY_RECOVERY_RECEIPT_KEYS: - raise SystemExit("post-publication adoption requires the closed revision-11 receipt schema") - if ( - prior.get("receipt_id") != prior_receipt_id - or prior.get("schema") != RECOVERY_RECEIPT_SCHEMA - or prior.get("plan_id") != plan_id - or prior.get("task_id") != task_id - or prior.get("binding_id") != binding_id - or prior.get("binding_sha256") != binding_sha256 - or prior.get("baseline_head") != baseline.get("head") - or prior.get("baseline_tree") != baseline.get("tree") - or prior.get("expected_base_head") != resolved_expected_head - or prior.get("expected_base_tree") != resolved_expected_tree - or prior.get("absence_result") != "accepted_base_absent" - or prior.get("freshness") != "current_validation_attempt" - or not isinstance(prior.get("observed_at"), str) - ): - raise SystemExit("post-publication adoption prior receipt has a non-global defect") - - stores = prior.get("handoff_stores") - index_identity = prior.get("handoff_index") - if not isinstance(stores, Mapping) or set(stores) != {"active", "archived"}: - raise SystemExit("post-publication adoption prior receipt stores are invalid") - recorded_ids: set[str] = set() - for status in ("active", "archived"): - store = stores[status] - if not isinstance(store, Mapping) or set(store) != {"store_id", "records", "sha256"}: - raise SystemExit("post-publication adoption prior receipt store is invalid") - records = store.get("records") - if ( - store.get("store_id") != f"executor/{status}" - or not isinstance(records, list) - or store.get("sha256") != semantic_digest(records) - ): - raise SystemExit("post-publication adoption prior receipt store digest is invalid") - for record in records: - if not isinstance(record, Mapping) or set(record) != {"handoff_id", "path", "sha256"}: - raise SystemExit("post-publication adoption prior receipt record is invalid") - recorded_ids.add(str(record.get("handoff_id") or "")) - record_path = (control_root / str(record.get("path") or "")).resolve() - try: - record_path.relative_to(control_root) - except ValueError as error: - raise SystemExit("post-publication adoption prior receipt record path is unsafe") from error - if ( - not record_path.is_file() - or record_path.is_symlink() - or hashlib.sha256(record_path.read_bytes()).hexdigest() != record.get("sha256") - ): - raise SystemExit("post-publication adoption prior receipt record is stale") - try: - recorded_handoff, _ = _read_structured(record_path) - except (OSError, SystemExit, ValueError) as error: - raise SystemExit("post-publication adoption prior receipt record is invalid") from error - if _is_recoverable_accepted_base( - recorded_handoff, - plan_id, - task_id, - resolved_expected_head, - resolved_expected_tree, - ): - raise SystemExit("post-publication adoption prior expected-base query was not absent") - if handoff_id in recorded_ids: - raise SystemExit("post-publication adoption candidate was already present in the prior receipt") - if not isinstance(index_identity, Mapping) or set(index_identity) != { - "index_id", "path", "sha256", "projected_entries_sha256" - }: - raise SystemExit("post-publication adoption prior receipt index is invalid") - if ( - index_identity.get("index_id") != "handoff/index.jsonl" - or index_identity.get("path") != ".work-bundle/orchestration/handoff/index.jsonl" - or any( - not re.fullmatch(r"[0-9a-f]{64}", str(index_identity.get(field) or "")) - for field in ("sha256", "projected_entries_sha256") - ) - ): - raise SystemExit("post-publication adoption prior receipt index digest is invalid") - prior_revision = semantic_digest( - { - "expected_base_head": resolved_expected_head, - "expected_base_tree": resolved_expected_tree, - "handoff_stores": stores, - "handoff_index": index_identity, - } - ) - if prior.get("queried_historical_revision") != prior_revision: - raise SystemExit("post-publication adoption prior receipt historical revision is invalid") - expected_receipt_prefix = f"accepted-result-recovery-{task_id}-{prior_revision[:12]}-" - observed_suffix = hashlib.sha256(str(prior["observed_at"]).encode()).hexdigest()[:12] - if prior_receipt_id != f"{expected_receipt_prefix}{observed_suffix}": - raise SystemExit("post-publication adoption prior receipt identity is invalid") - - snapshot = _accepted_base_query_snapshot( - control_root, - plan_id, - task_id, - resolved_expected_head, - resolved_expected_tree, - handoff_id, - review_id, - resolved_final_head, - resolved_final_tree, - ) - if snapshot["recoverable_base_handoff_ids"]: - raise SystemExit("post-publication adoption rejected: recoverable accepted base exists") - if snapshot["proposal_state"] != "published": - raise SystemExit("post-publication adoption candidate is missing, mismatched, or ambiguous") - - handoff_root = control_root / ".work-bundle/orchestration/handoff" - _, candidate = _exact_handoff_reference( - handoff_root, {"handoff_id": handoff_id, "handoff_sha256": handoff_sha256} - ) - if not _valid_recovered_result(candidate, plan_id, task_id): - raise SystemExit("post-publication adoption candidate is not a valid recovered result") - for path in sorted(handoff_root.glob("executor/*/*")): - if not path.is_file() or path.is_symlink(): - continue - if hashlib.sha256(path.read_bytes()).hexdigest() == handoff_sha256: - continue - try: - other, _ = _read_structured(path) - except (OSError, SystemExit, ValueError): - continue - if _valid_recovered_result(other, plan_id, task_id): - raise SystemExit("post-publication adoption rejected: valid competing recovered result exists") - - current_legacy = _legacy_recovery_global_snapshot( - control_root, - plan_id, - task_id, - resolved_expected_head, - resolved_expected_tree, - ) - if all(prior.get(field) == current_legacy[field] for field in current_legacy): - raise SystemExit("post-publication adoption prior receipt is not globally stale") - return _write_query_scoped_recovery_receipt( - control_root, - plan_id, - task_id, - binding, - binding_path, - resolved_expected_head, - resolved_expected_tree, - handoff_id, - review_id, - resolved_final_head, - resolved_final_tree, - snapshot["expected_base_query"], - ) - - -def validate_accepted_base_absence_receipt( - control_root: Path, - plan_id: str, - task_id: str, - reference: object, -) -> dict[str, Any]: - if not isinstance(reference, Mapping) or set(reference) != {"receipt_id", "receipt_sha256"}: - raise SystemExit("accepted-result recovery receipt reference must use the closed identity shape") - receipt_id = str(reference["receipt_id"]) - path = _recovery_receipt_path(control_root, plan_id, task_id, receipt_id) - if not path.is_file() or path.is_symlink(): - raise SystemExit("accepted-result recovery receipt is missing") - if hashlib.sha256(path.read_bytes()).hexdigest() != str(reference["receipt_sha256"]): - raise SystemExit("accepted-result recovery receipt is stale") - try: - receipt = json.loads(path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError) as error: - raise SystemExit("accepted-result recovery receipt is invalid") from error - if not isinstance(receipt, dict) or set(receipt) != RECOVERY_RECEIPT_KEYS: - raise SystemExit("accepted-result recovery receipt must use the closed schema") - if ( - receipt.get("receipt_id") != receipt_id - or receipt.get("schema") != RECOVERY_RECEIPT_SCHEMA - or receipt.get("plan_id") != plan_id - or receipt.get("task_id") != task_id - or receipt.get("freshness") != "current_validation_attempt" - ): - raise SystemExit("accepted-result recovery receipt identity is mismatched") - binding_path = _binding_path(control_root, plan_id, task_id) - binding = load_task_execution_binding(control_root, plan_id, task_id) - baseline = binding.get("baseline") if isinstance(binding.get("baseline"), Mapping) else {} - if ( - receipt.get("binding_id") != (binding.get("ownership") or {}).get("binding_id") - or receipt.get("binding_sha256") != hashlib.sha256(binding_path.read_bytes()).hexdigest() - or receipt.get("baseline_head") != baseline.get("head") - or receipt.get("baseline_tree") != baseline.get("tree") - ): - raise SystemExit("accepted-result recovery receipt binding or baseline is stale") - execution_root = Path(str(binding.get("execution_path") or "")).resolve() - expected_head = _resolve_commit(execution_root, str(receipt.get("expected_base_head") or "")) - expected_tree = _git(execution_root, "rev-parse", f"{expected_head}^{{tree}}").strip() - if receipt.get("expected_base_tree") != expected_tree: - raise SystemExit("accepted-result recovery receipt expected base identity is stale") - proposal = receipt.get("proposed_recovered_result") - if not isinstance(proposal, Mapping) or set(proposal) != { - "handoff_id", "review_id", "final_head", "final_tree" - }: - raise SystemExit("accepted-result recovery receipt proposed result is invalid") - final_head = _resolve_commit(execution_root, str(proposal.get("final_head") or "")) - final_tree = _git(execution_root, "rev-parse", f"{final_head}^{{tree}}").strip() - if proposal.get("final_tree") != final_tree: - raise SystemExit("accepted-result recovery receipt proposed final identity is stale") - snapshot = _accepted_base_query_snapshot( - control_root, - plan_id, - task_id, - expected_head, - expected_tree, - str(proposal.get("handoff_id") or ""), - str(proposal.get("review_id") or ""), - final_head, - final_tree, - ) - if snapshot["recoverable_base_handoff_ids"]: - raise SystemExit("accepted-result recovery rejected: recoverable accepted base handoff exists") - if receipt.get("expected_base_query") != snapshot["expected_base_query"]: - raise SystemExit("accepted-result recovery receipt expected-base query is stale") - if snapshot["proposal_state"] in {"mismatch", "ambiguous"}: - raise SystemExit("accepted-result recovery proposed result is mismatched or ambiguous") - return {**receipt, "proposal_state": snapshot["proposal_state"]} - - -def _recovered_accepted_dependency_paths( - task: dict[str, Any], - execution_root: Path, - descriptors: list[Mapping[str, object]], -) -> set[str]: - required_fields = { - "task_id", - "execution_baseline_recovery", - "recovered_result", - "accepted_result_delta", - "integrated_base", - "integrated_head", - } - recovery_fields = { - "binding_id", - "binding_sha256", - "baseline_head", - "baseline_tree", - "recovery_receipt", - } - delta_fields = { - "expected_base_head", - "expected_base_tree", - "final_head", - "final_tree", - } - dependencies = {str(value) for value in _as_list(task.get("depends_on"))} - plan_id = str(task.get("plan_id") or "") - workspace = task.get("workspace") if isinstance(task.get("workspace"), dict) else {} - control_root = Path(str(workspace.get("root") or "")).resolve() - handoff_root = control_root / ".work-bundle/orchestration/handoff" - current_head = _resolve_commit(execution_root, "HEAD") - admitted: set[str] = set() - seen_dependencies: set[str] = set() - for descriptor in descriptors: - if set(descriptor) != required_fields: - raise SystemExit( - "accepted_dependency_deltas recovery entries must use the closed runtime identity shape" - ) - dependency_id = str(descriptor["task_id"]) - if dependency_id not in dependencies: - raise SystemExit(f"accepted_dependency_deltas names undeclared dependency: {dependency_id}") - if dependency_id in seen_dependencies: - raise SystemExit(f"accepted_dependency_deltas duplicates dependency: {dependency_id}") - seen_dependencies.add(dependency_id) - recovery = descriptor["execution_baseline_recovery"] - if not isinstance(recovery, Mapping) or set(recovery) != recovery_fields: - raise SystemExit("execution_baseline_recovery must use the closed runtime identity shape") - - binding_path = _binding_path(control_root, plan_id, dependency_id) - binding = load_task_execution_binding(control_root, plan_id, dependency_id) - baseline = binding.get("baseline") if isinstance(binding.get("baseline"), Mapping) else {} - binding_id = str((binding.get("ownership") or {}).get("binding_id") or "") - binding_digest = hashlib.sha256(binding_path.read_bytes()).hexdigest() - if ( - recovery.get("binding_id") != binding_id - or recovery.get("binding_sha256") != binding_digest - or recovery.get("baseline_head") != baseline.get("head") - or recovery.get("baseline_tree") != baseline.get("tree") - ): - raise SystemExit("execution_baseline_recovery binding or original baseline is mismatched") - receipt = validate_accepted_base_absence_receipt( - control_root, - plan_id, - dependency_id, - recovery.get("recovery_receipt"), - ) - if receipt.get("proposal_state") != "published": - raise SystemExit("accepted-result recovery proposed result is not published and indexed") - - delta = descriptor["accepted_result_delta"] - if not isinstance(delta, Mapping) or set(delta) != delta_fields: - raise SystemExit("accepted_result_delta must use the closed runtime identity shape") - proposal = receipt["proposed_recovered_result"] - if ( - delta.get("expected_base_head") != receipt.get("expected_base_head") - or delta.get("expected_base_tree") != receipt.get("expected_base_tree") - or delta.get("final_head") != proposal.get("final_head") - or delta.get("final_tree") != proposal.get("final_tree") - ): - raise SystemExit("accepted_result_delta is mismatched with the recovery receipt") - recovered_reference = descriptor["recovered_result"] - if ( - not isinstance(recovered_reference, Mapping) - or recovered_reference.get("handoff_id") != proposal.get("handoff_id") - ): - raise SystemExit("recovered result is mismatched with the recovery receipt proposal") - - _, recovered = _exact_handoff_reference(handoff_root, recovered_reference) - related = recovered.get("related") if isinstance(recovered.get("related"), Mapping) else {} - result = recovered.get("result") if isinstance(recovered.get("result"), Mapping) else {} - review = ( - recovered.get("acceptance_review") - if isinstance(recovered.get("acceptance_review"), Mapping) - else {} - ) - reset = review.get("review_reset") if isinstance(review.get("review_reset"), Mapping) else {} - evidence = review.get("evidence") if isinstance(review.get("evidence"), Mapping) else {} - reviewer = review.get("reviewer") if isinstance(review.get("reviewer"), Mapping) else {} - identity = review.get("target_identity") if isinstance(review.get("target_identity"), Mapping) else {} - if ( - related.get("plan") != plan_id - or related.get("task") != dependency_id - or result.get("state") != "completed" - or review.get("verdict") != "accept" - or review.get("review_mode") != "initial" - or review.get("review_target_kind") != "task" - or review.get("reviewer_independent") is not True - or review.get("repair_frontier") is not None - or reset.get("reason_class") != "authority" - or evidence.get("unavailable_evidence") != [] - or reviewer.get("capability") != "judgment" - or identity.get("artifact_id") != dependency_id - or review.get("reviewed_head") != identity.get("revision") - or review.get("review_id") != proposal.get("review_id") - ): - raise SystemExit( - "recovered result requires a fresh complete independent whole-task initial authority review with empty unavailable_evidence" - ) - try: - from review_runtime import ReviewContractError, validate_task_acceptance_review - - validate_task_acceptance_review(review) - except ReviewContractError as error: - raise SystemExit(f"recovered result task review is invalid: {error}") from error - try: - owner = normalize_subagent_provenance( - recovered.get("delegation_evidence") - if isinstance(recovered.get("delegation_evidence"), Mapping) - else None - ) - except OwnershipBlocker as error: - raise SystemExit(f"recovered result ownership is invalid: {error}") from error - if reviewer.get("agent_id") == owner.get("agent_id"): - raise SystemExit("recovered result reviewer is not independent from the task owner") - - source_base = _resolve_commit(execution_root, str(delta.get("expected_base_head") or "")) - source_head = _resolve_commit(execution_root, str(delta.get("final_head") or "")) - source_tree = _git(execution_root, "rev-parse", f"{source_head}^{{tree}}").strip() - source_base_tree = _git(execution_root, "rev-parse", f"{source_base}^{{tree}}").strip() - if delta.get("expected_base_tree") != source_base_tree: - raise SystemExit("accepted_result_delta expected base Git identity is mismatched") - if delta.get("final_tree") != source_tree or identity.get("source_tree") != source_tree: - raise SystemExit("recovered result Git identity is mismatched") - if subprocess.run( - ["git", "-C", str(execution_root), "merge-base", "--is-ancestor", source_base, source_head], - capture_output=True, - check=False, - ).returncode: - raise SystemExit("recovered result source chain is non-ancestral") - integrated_base = _resolve_commit(execution_root, str(descriptor["integrated_base"])) - integrated_head = _resolve_commit(execution_root, str(descriptor["integrated_head"])) - if subprocess.run( - ["git", "-C", str(execution_root), "merge-base", "--is-ancestor", integrated_head, current_head], - capture_output=True, - check=False, - ).returncode: - raise SystemExit("recovered result integration checkpoint is not current") - source_diff = _git(execution_root, "diff", "--binary", source_base, source_head, "--") - integrated_diff = _git( - execution_root, "diff", "--binary", integrated_base, integrated_head, "--" - ) - if source_diff != integrated_diff: - raise SystemExit("recovered result integration checkpoint is mismatched") - paths = { - path - for line in _git( - execution_root, "diff", "--name-status", source_base, source_head, "--" - ).splitlines() - for path in _paths_from_name_status(line) - } - if paths and _git( - execution_root, "diff", "--name-only", integrated_head, "--", *sorted(paths) - ).strip(): - raise SystemExit("recovered dependency path changed after integration") - admitted.update(paths) - return admitted - - -def _cumulative_accepted_dependency_paths( - task: dict[str, Any], - execution_root: Path, - descriptors: list[Mapping[str, object]], -) -> set[str]: - required_fields = { - "task_id", "accepted_result_base", "review_chain", "integrated_base", "integrated_head" - } - dependencies = {str(value) for value in _as_list(task.get("depends_on"))} - plan_id = str(task.get("plan_id") or "") - workspace = task.get("workspace") if isinstance(task.get("workspace"), dict) else {} - handoff_root = ( - Path(str(workspace.get("root") or "")).resolve() - / ".work-bundle/orchestration/handoff" - ) - current_head = _resolve_commit(execution_root, "HEAD") - admitted: set[str] = set() - seen_dependencies: set[str] = set() - for descriptor in descriptors: - if set(descriptor) != required_fields: - raise SystemExit( - "accepted_dependency_deltas cumulative entries must use the closed runtime identity shape" - ) - dependency_id = str(descriptor["task_id"]) - if dependency_id not in dependencies: - raise SystemExit(f"accepted_dependency_deltas names undeclared dependency: {dependency_id}") - if dependency_id in seen_dependencies: - raise SystemExit(f"accepted_dependency_deltas duplicates dependency: {dependency_id}") - seen_dependencies.add(dependency_id) - chain_references = descriptor["review_chain"] - if (not isinstance(chain_references, list) or not chain_references - or any(not isinstance(item, Mapping) for item in chain_references)): - raise SystemExit("accepted dependency review_chain must be non-empty and exact") - reference_ids = [str(item.get("handoff_id") or "") for item in chain_references] - if len(reference_ids) != len(set(reference_ids)): - raise SystemExit("accepted dependency review_chain cannot contain duplicated handoffs") - - _, base_handoff = _exact_handoff_reference( - handoff_root, descriptor["accepted_result_base"] - ) - base_related = base_handoff.get("related") if isinstance(base_handoff.get("related"), dict) else {} - base_result = base_handoff.get("result") if isinstance(base_handoff.get("result"), dict) else {} - base_review = base_handoff.get("acceptance_review") if isinstance(base_handoff.get("acceptance_review"), dict) else {} - if (base_related.get("plan") != plan_id or base_related.get("task") != dependency_id - or base_result.get("state") != "completed" or base_review.get("verdict") != "accept"): - raise SystemExit("accepted dependency result base is not an accepted plan/task result") - - chain: list[tuple[dict[str, Any], dict[str, Any], dict[str, Any]]] = [] - for reference in chain_references: - _, handoff = _exact_handoff_reference(handoff_root, reference) - related = handoff.get("related") if isinstance(handoff.get("related"), dict) else {} - result = handoff.get("result") if isinstance(handoff.get("result"), dict) else {} - review = handoff.get("acceptance_review") if isinstance(handoff.get("acceptance_review"), dict) else {} - if related.get("plan") != plan_id or related.get("task") != dependency_id: - raise SystemExit("accepted dependency review_chain plan/task identity is mismatched") - chain.append((handoff, result, review)) - - try: - from review_runtime import ( - ReviewContractError, - review_evidence_identity, - validate_task_acceptance_review, - ) - validate_task_acceptance_review(base_review) - base_identity = base_review.get("target_identity") - if (not isinstance(base_identity, Mapping) - or base_identity.get("artifact_id") != dependency_id - or base_review.get("reviewed_head") != base_identity.get("revision")): - raise SystemExit("accepted dependency result base identity is mismatched") - previous_review = base_review - seen_review_ids = {str(base_review.get("review_id") or "")} - for _, result, review in chain: - # The bounded previous_review embedded in each chain link lets the - # native sequence validator enforce repair continuity and material- - # change reset isolation without reacquiring older history. - validate_task_acceptance_review(review) - review_id = str(review.get("review_id") or "") - identity = review.get("target_identity") - if (review_id in seen_review_ids or not isinstance(identity, Mapping) - or identity.get("artifact_id") != dependency_id - or review.get("reviewed_head") != identity.get("revision")): - raise SystemExit("accepted dependency review_chain review identity is duplicated or mismatched") - seen_review_ids.add(review_id) - revision = _resolve_commit(execution_root, str(identity.get("revision") or "")) - tree = _git(execution_root, "rev-parse", f"{revision}^{{tree}}").strip() - if tree != identity.get("source_tree"): - raise SystemExit("accepted dependency review_chain Git identity is mismatched") - carried = review.get("previous_review") - if (not isinstance(carried, Mapping) or "previous_review" in carried - or _review_without_history(carried) != _review_without_history(previous_review)): - raise SystemExit("accepted dependency review_chain prior review is missing or mismatched") - mode = review.get("review_mode") - if mode == "repair": - frontier = review.get("repair_frontier") - if not isinstance(frontier, Mapping): - raise SystemExit("accepted dependency review_chain repair frontier is missing") - blocking = [ - str(item.get("finding_id")) - for item in _as_list(previous_review.get("findings")) - if isinstance(item, Mapping) and item.get("severity") == "blocking" - ] - if (frontier.get("prior_review_id") != previous_review.get("review_id") - or frontier.get("previous_reviewed_identity") != previous_review.get("target_identity") - or frontier.get("repaired_identity") != review.get("target_identity") - or list(frontier.get("blocking_finding_ids") or []) != blocking - or frontier.get("frozen_evidence_reference") != review_evidence_identity(previous_review)): - raise SystemExit("accepted dependency review_chain repair continuity is mismatched") - else: - reset = review.get("review_reset") - if not isinstance(reset, Mapping) or reset.get("prior_review_id") != previous_review.get("review_id"): - raise SystemExit("accepted dependency review_chain initial reset is non-contiguous") - if review.get("verdict") not in {"repair", "accept"}: - raise SystemExit("accepted dependency review_chain contains a blocked review") - if review.get("verdict") == "accept" and result.get("state") != "completed": - raise SystemExit("accepted dependency review_chain accepted result is incomplete") - previous_review = review - except ReviewContractError as error: - raise SystemExit(f"accepted dependency review_chain is invalid: {error}") from error - - _, final_result, final_review = chain[-1] - if final_review.get("verdict") != "accept" or final_result.get("state") != "completed": - raise SystemExit("accepted dependency review_chain final result is not accepted") - base_identity = base_review.get("target_identity") - final_identity = final_review.get("target_identity") - if not isinstance(base_identity, Mapping) or not isinstance(final_identity, Mapping): - raise SystemExit("accepted dependency cumulative result identity is incomplete") - for identity in (base_identity, final_identity): - revision = _resolve_commit(execution_root, str(identity.get("revision") or "")) - tree = _git(execution_root, "rev-parse", f"{revision}^{{tree}}").strip() - if tree != identity.get("source_tree"): - raise SystemExit("accepted dependency cumulative Git identity is mismatched") - - final_review_id = str(final_review.get("review_id") or "") - chain_ids = set(reference_ids) - successor_edges: list[tuple[str, str]] = [] - for path in sorted(handoff_root.glob("executor/*/*")): - if not path.is_file() or path.is_symlink(): - continue - try: - candidate, _ = _read_structured(path) - except (OSError, SystemExit, ValueError): - continue - if str(candidate.get("id") or "") in chain_ids: - continue - related = candidate.get("related") if isinstance(candidate.get("related"), dict) else {} - review = candidate.get("acceptance_review") if isinstance(candidate.get("acceptance_review"), dict) else {} - frontier = review.get("repair_frontier") if isinstance(review.get("repair_frontier"), dict) else {} - reset = review.get("review_reset") if isinstance(review.get("review_reset"), dict) else {} - prior_id = frontier.get("prior_review_id") or reset.get("prior_review_id") - if (related.get("plan") == plan_id and related.get("task") == dependency_id - and isinstance(prior_id, str) and prior_id): - successor_edges.append((prior_id, str(review.get("review_id") or ""))) - reachable = {final_review_id} - while True: - additions = {current for prior, current in successor_edges if prior in reachable} - if additions.issubset(reachable): - break - reachable.update(additions) - if any(prior in reachable for prior, _ in successor_edges): - raise SystemExit("accepted dependency review_chain terminal result is stale") - - source_base = _resolve_commit(execution_root, str(base_identity.get("revision") or "")) - source_head = _resolve_commit(execution_root, str(final_identity.get("revision") or "")) - if subprocess.run( - ["git", "-C", str(execution_root), "merge-base", "--is-ancestor", source_base, source_head], - capture_output=True, - check=False, - ).returncode: - raise SystemExit("accepted dependency cumulative source chain is non-ancestral") - integrated_base = _resolve_commit(execution_root, str(descriptor["integrated_base"])) - integrated_head = _resolve_commit(execution_root, str(descriptor["integrated_head"])) - if subprocess.run( - ["git", "-C", str(execution_root), "merge-base", "--is-ancestor", integrated_head, current_head], - capture_output=True, - check=False, - ).returncode: - raise SystemExit("accepted dependency cumulative integration checkpoint is not current") - source_diff = _git(execution_root, "diff", "--binary", source_base, source_head, "--") - integrated_diff = _git( - execution_root, "diff", "--binary", integrated_base, integrated_head, "--" - ) - if source_diff != integrated_diff: - raise SystemExit("accepted dependency cumulative integration checkpoint is mismatched") - paths = { - path - for line in _git(execution_root, "diff", "--name-status", source_base, source_head, "--").splitlines() - for path in _paths_from_name_status(line) - } - if paths and _git( - execution_root, "diff", "--name-only", integrated_head, "--", *sorted(paths) - ).strip(): - raise SystemExit("accepted dependency cumulative path changed after integration") - admitted.update(paths) - return admitted - - -def _accepted_dependency_paths( - task: dict[str, Any], - execution_root: Path, - accepted_dependency_deltas: Iterable[Mapping[str, object]] | None, -) -> set[str]: - descriptors = list(accepted_dependency_deltas or []) - if not descriptors: - dependencies = {str(value) for value in _as_list(task.get("depends_on"))} - if not dependencies: - return set() - workspace = task.get("workspace") if isinstance(task.get("workspace"), dict) else {} - control_root = Path(str(workspace.get("root") or "")).resolve() - plan_id = str(task.get("plan_id") or "") - for dependency_id in sorted(dependencies): - matches: list[Path] = [] - plan_root = control_root / ".work-bundle/orchestration/plan" - for status in ("active", "archived"): - for path in sorted((plan_root / status).glob("**/*.md")): - try: - document, _ = _read_structured(path) - except (OSError, SystemExit, ValueError): - continue - if ( - str(document.get("id") or "") == dependency_id - and str(document.get("plan_id") or "") == plan_id - ): - matches.append(path) - if len(matches) != 1: - raise SystemExit( - f"accepted dependency task authority is missing or ambiguous: {dependency_id}" - ) - compile_args = argparse.Namespace( - project_root=str(control_root), - workspace_root=str(control_root), - task=str(matches[0]), - handoff=None, - base=None, - head=None, - workspace_id=None, - execution_id=None, - repository_id=None, - execution_runtime_root=None, - mutation_events=None, - accepted_dependency_deltas=None, - prior_ownership=None, - repair_continuity=None, - authorized_replacements=None, - ) - _, brief_document = _compile_task_brief(compile_args) - load_current_accepted_task_result(control_root, brief_document["task_brief"]) - return set() - cumulative_fields = { - "task_id", "accepted_result_base", "review_chain", "integrated_base", "integrated_head" - } - recovery_fields = { - "task_id", "execution_baseline_recovery", "recovered_result", "accepted_result_delta", - "integrated_base", "integrated_head" - } - cumulative = [ - item for item in descriptors if isinstance(item, Mapping) and set(item) == cumulative_fields - ] - recovered = [ - item for item in descriptors if isinstance(item, Mapping) and set(item) == recovery_fields - ] - if cumulative or recovered: - if len(cumulative) + len(recovered) != len(descriptors): - raise SystemExit("accepted_dependency_deltas cannot mix closed and legacy identities") - dependency_ids = [str(item["task_id"]) for item in [*cumulative, *recovered]] - if len(dependency_ids) != len(set(dependency_ids)): - raise SystemExit("accepted_dependency_deltas duplicates dependency") - return ( - _cumulative_accepted_dependency_paths(task, execution_root, cumulative) - | _recovered_accepted_dependency_paths(task, execution_root, recovered) - ) - dependencies = {str(value) for value in _as_list(task.get("depends_on"))} - plan_id = str(task.get("plan_id") or "") - workspace = task.get("workspace") if isinstance(task.get("workspace"), dict) else {} - control_root = Path(str(workspace.get("root") or "")).resolve() - handoff_root = control_root / ".work-bundle/orchestration/handoff" - admitted: set[str] = set() - chain_tail_by_dependency: dict[str, dict[str, Any]] = {} - chain_by_dependency: dict[str, dict[str, Any]] = {} - last_checkpoint_by_path: dict[str, tuple[str, str]] = {} - current_head = _resolve_commit(execution_root, "HEAD") - required_fields = { - "task_id", "handoff_id", "handoff_sha256", "integrated_base", "integrated_head" - } - for descriptor in descriptors: - if not isinstance(descriptor, Mapping) or set(descriptor) != required_fields: - raise SystemExit("accepted_dependency_deltas entries must use the closed runtime identity shape") - dependency_id = str(descriptor["task_id"]) - handoff_id = str(descriptor["handoff_id"]) - if dependency_id not in dependencies: - raise SystemExit(f"accepted_dependency_deltas names undeclared dependency: {dependency_id}") - matches: list[tuple[Path, dict[str, Any]]] = [] - for path in sorted(handoff_root.glob("executor/*/*")): - if not path.is_file() or path.is_symlink(): - continue - try: - document, _ = _read_structured(path) - except (OSError, SystemExit, ValueError): - continue - if document.get("id") == handoff_id: - matches.append((path, document)) - if len(matches) != 1: - raise SystemExit(f"accepted dependency handoff identity is missing or ambiguous: {handoff_id}") - handoff_path, handoff = matches[0] - actual_digest = hashlib.sha256(handoff_path.read_bytes()).hexdigest() - if actual_digest != str(descriptor["handoff_sha256"]): - raise SystemExit(f"accepted dependency handoff identity is stale: {handoff_id}") - related = handoff.get("related") if isinstance(handoff.get("related"), dict) else {} - result = handoff.get("result") if isinstance(handoff.get("result"), dict) else {} - review = handoff.get("acceptance_review") if isinstance(handoff.get("acceptance_review"), dict) else {} - frontier = review.get("repair_frontier") if isinstance(review.get("repair_frontier"), dict) else {} - previous = frontier.get("previous_reviewed_identity") if isinstance(frontier.get("previous_reviewed_identity"), dict) else {} - repaired = frontier.get("repaired_identity") if isinstance(frontier.get("repaired_identity"), dict) else {} - if related.get("plan") != plan_id or related.get("task") != dependency_id: - raise SystemExit( - f"accepted dependency handoff plan/task identity is mismatched: {handoff_id}" - ) - if (result.get("state") != "completed" or review.get("verdict") != "accept" - or review.get("review_mode") != "repair" - or repaired != review.get("target_identity")): - raise SystemExit(f"dependency handoff is not an accepted repair result: {handoff_id}") - source_base = str(previous.get("revision") or "") - source_head = str(repaired.get("revision") or "") - integrated_base = _resolve_commit(execution_root, str(descriptor["integrated_base"])) - integrated_head = _resolve_commit(execution_root, str(descriptor["integrated_head"])) - if not source_base or not source_head: - raise SystemExit(f"accepted dependency repair identity is incomplete: {handoff_id}") - for commit, identity in ((source_base, previous), (source_head, repaired)): - resolved = _resolve_commit(execution_root, commit) - tree = _git(execution_root, "rev-parse", f"{resolved}^{{tree}}").strip() - if tree != identity.get("source_tree"): - raise SystemExit(f"accepted dependency Git identity is mismatched: {handoff_id}") - prior_link = chain_tail_by_dependency.get(dependency_id) - if prior_link is not None: - if previous != prior_link["repaired_identity"]: - raise SystemExit( - "accepted dependency source chain is not ordered and contiguous: " - f"{handoff_id}" - ) - if integrated_base != prior_link["integrated_head"]: - raise SystemExit( - "accepted dependency integrated chain is non-adjacent: " - f"{handoff_id}" - ) - if subprocess.run( - ["git", "-C", str(execution_root), "merge-base", "--is-ancestor", integrated_head, current_head], - capture_output=True, - check=False, - ).returncode: - raise SystemExit(f"accepted dependency integration checkpoint is not current: {handoff_id}") - source_diff = _git(execution_root, "diff", "--binary", source_base, source_head, "--") - integrated_diff = _git( - execution_root, "diff", "--binary", integrated_base, integrated_head, "--" - ) - if source_diff != integrated_diff: - raise SystemExit(f"accepted dependency integration checkpoint is mismatched: {handoff_id}") - paths = { - path - for line in _git(execution_root, "diff", "--name-status", source_base, source_head, "--").splitlines() - for path in _paths_from_name_status(line) - } - chain_tail_by_dependency[dependency_id] = { - "repaired_identity": repaired, - "integrated_head": integrated_head, - } - chain = chain_by_dependency.setdefault( - dependency_id, - { - "first_previous_identity": previous, - "last_repaired_identity": repaired, - "accepted_edges": set(), - }, - ) - chain["last_repaired_identity"] = repaired - chain["accepted_edges"].add( - semantic_digest({"previous": previous, "repaired": repaired}) - ) - for path in paths: - last_checkpoint_by_path[path] = (integrated_head, handoff_id) - for dependency_id, chain in chain_by_dependency.items(): - for path in sorted(handoff_root.glob("executor/*/*")): - if not path.is_file() or path.is_symlink(): - continue - try: - candidate, _ = _read_structured(path) - except (OSError, SystemExit, ValueError): - continue - related = candidate.get("related") if isinstance(candidate.get("related"), dict) else {} - result = candidate.get("result") if isinstance(candidate.get("result"), dict) else {} - review = candidate.get("acceptance_review") if isinstance(candidate.get("acceptance_review"), dict) else {} - frontier = review.get("repair_frontier") if isinstance(review.get("repair_frontier"), dict) else {} - previous = frontier.get("previous_reviewed_identity") if isinstance(frontier.get("previous_reviewed_identity"), dict) else {} - repaired = frontier.get("repaired_identity") if isinstance(frontier.get("repaired_identity"), dict) else {} - if (related.get("plan") != plan_id or related.get("task") != dependency_id - or result.get("state") != "completed" - or review.get("verdict") != "accept" or review.get("review_mode") != "repair" - or repaired != review.get("target_identity") or not previous or not repaired): - continue - edge = semantic_digest({"previous": previous, "repaired": repaired}) - if edge in chain["accepted_edges"]: - continue - if repaired == chain["first_previous_identity"]: - raise SystemExit( - "accepted dependency repair chain is not anchored at its known accepted start: " - f"{dependency_id}" - ) - if previous == chain["last_repaired_identity"]: - raise SystemExit( - "accepted dependency repair chain is incomplete at its known accepted head: " - f"{dependency_id}" - ) - for path, (integrated_head, handoff_id) in last_checkpoint_by_path.items(): - if _git(execution_root, "diff", "--name-only", integrated_head, "--", path).strip(): - raise SystemExit(f"accepted dependency path changed after integration: {handoff_id}") - admitted.add(path) - return admitted - - -def _observe_completed_validation( - handoff: dict[str, Any], - task: dict[str, Any], - required_items: list[dict[str, Any]], - reported_commands: dict[str, dict[str, Any]] | None, - *, - workspace_id: str | None = None, - execution_id: str | None = None, - repository_id: str | None = None, - execution_runtime_root: str | None = None, - accepted_dependency_deltas: Iterable[Mapping[str, object]] | None = None, -) -> list[dict[str, Any]]: - if "harness_receipt" in handoff or ( - isinstance(handoff.get("validation"), dict) and "harness_receipt" in handoff["validation"] - ): - raise SystemExit("Executor-minted harness_receipt is not independent proof") - control_root_raw = (task.get("workspace") or {}).get("root") if isinstance(task.get("workspace"), dict) else None - if not control_root_raw: - raise SystemExit("Task execution binding is missing harness provenance") - control_root = Path(str(control_root_raw)) - from review_runtime import require_plan_reviews - require_plan_reviews(control_root, _find_plan(control_root, str(task["plan_id"]))[0]) - binding = load_task_execution_binding(control_root, str(task["plan_id"]), str(task["task_id"])) - fit = handoff.get("task_fit_check") if isinstance(handoff.get("task_fit_check"), Mapping) else {} - if ( - isinstance(binding.get("accepted_result"), Mapping) - and fit.get("result") != "repaired" - ): - raise SystemExit( - "accepted task result already exists; consume it without rerunning validation" - ) - if workspace_id and str(binding.get("workspace_id") or "") != str(workspace_id): - raise SystemExit("Task execution binding workspace_id mismatch") - if execution_id and str(binding.get("execution_id") or "") != str(execution_id): - raise SystemExit("Task execution binding execution_id mismatch") - if repository_id and str(binding.get("repository_id") or "") != str(repository_id): - raise SystemExit("Task execution binding repository_id mismatch") - if execution_runtime_root and Path(str(binding.get("runtime_root") or "")).resolve() != Path( - execution_runtime_root - ).expanduser().resolve(): - raise SystemExit("Task execution binding runtime root mismatch") - baseline = binding.get("baseline") - if not isinstance(baseline, dict) or not baseline.get("head"): - raise SystemExit("Task execution binding is missing harness provenance baseline") - execution_root = Path(str(binding["execution_path"])) - try: - pre_batch = capture_repository_evidence(execution_root) - except RuntimeError as error: - raise SystemExit(str(error)) from error - observed_items: list[dict[str, Any]] = [] - task_files = task.get("files") if isinstance(task.get("files"), dict) else {} - accepted_paths = _accepted_dependency_paths(task, execution_root, accepted_dependency_deltas) - _assert_task_caused_delta_in_write_scope( - task_caused_paths(baseline, pre_batch, execution_root), - task_files, - accepted_dependency_paths=accepted_paths, - ) - for item in required_items: - policy = _completion_provenance_module().validation_reuse_policy(item) - observed_item = item - finalization_id = None - if policy["max_age_seconds"] == 0: - # A live check cannot be reusable by later lifecycle consumers, but - # initial acceptance still needs one immutable producer-to-consumer - # observation. Keep it recoverable for the atomic acceptance call; - # the stable finalization claim and accepted-result guard prevent a - # later validation dispatch from treating it as reusable evidence. - observed_item = dict(item) - observed_item["evidence_reuse"] = { - **policy, - "max_age_seconds": 86400, - } - finalization_identity = semantic_digest( - { - "command": str(item.get("command") or "").strip(), - "repair": fit.get("result") == "repaired", - "source": pre_batch, - } - ) - finalization_id = ( - f"initial-acceptance:{task['plan_id']}:{task['task_id']}:" - f"{finalization_identity}" - ) - observed = _completion_provenance_module().observe_validation( - binding, _validation_observation_task(task), observed_item, pre_batch, - lambda receipt: _observe_validation_item(item, execution_root, task, receipt), - lambda: capture_repository_evidence(execution_root), - finalization_id=finalization_id, - ) - command = str(item.get("command")).strip() - reported_item = reported_commands.get(command) if reported_commands is not None else None - if reported_item is not None and reported_item.get("result") != observed["result"]: - raise SystemExit( - f"Executor result validation for {command} does not match observed {observed['result']}" - ) - allowed = _acceptable_validation_results(item) - if observed["result"] not in allowed: - allowed_text = " or ".join(sorted(allowed)) - raise SystemExit( - f"Observed validation for {command} must be {allowed_text}; got {observed['result']}" - ) - observed_items.append(observed) - try: - post_batch = capture_repository_evidence(execution_root) - except RuntimeError as error: - raise SystemExit(str(error)) from error - in_batch = task_caused_paths(pre_batch, post_batch, execution_root) - if in_batch: - raise SystemExit( - "validation-blocked: authoritative validation batch mutated Git-observable state; " - "rerun the full batch after ordinary task work" - ) - caused = task_caused_paths(baseline, post_batch, execution_root) - task_files = task.get("files") if isinstance(task.get("files"), dict) else {} - _assert_task_caused_delta_in_write_scope( - caused, task_files, accepted_dependency_paths=accepted_paths - ) - if binding.get("mutating") is True: - updated = dict(binding) - updated["mutating"] = False - _persist_binding(updated, control_root) - return observed_items - - -def _task_evidence_applicability(task: dict[str, Any]) -> dict[str, dict[str, Any]]: - compiled = task.get("evidence_applicability") - if compiled is None: - return task_evidence_applicability(task) - if not isinstance(compiled, dict): - raise SystemExit("Task evidence_applicability must be a mapping") - normalized: dict[str, dict[str, Any]] = {} - for kind in ("metadata", "repository", "codegraph"): - item = compiled.get(kind) - if not isinstance(item, dict) or not isinstance(item.get("required"), bool): - raise SystemExit(f"Task evidence_applicability.{kind}.required must be boolean") - reasons = item.get("reasons") - if not isinstance(reasons, list) or any(not isinstance(reason, str) for reason in reasons): - raise SystemExit(f"Task evidence_applicability.{kind}.reasons must be a string list") - normalized[kind] = {"required": item["required"], "reasons": list(reasons)} - return normalized - - -def _validated_repository_evidence(handoff: dict[str, Any], metadata_required: bool) -> list[dict[str, Any]]: - entries = handoff.get("repository") - if not isinstance(entries, list) or not entries: - raise SystemExit("Executor result is missing applicable repository evidence") - validated: list[dict[str, Any]] = [] - for entry in entries: - if not isinstance(entry, dict): - raise SystemExit("Executor result repository evidence entries must be mappings") - root = Path(str(entry.get("root") or "")) - if not root.is_absolute(): - raise SystemExit("Executor result repository evidence root must be absolute") - if entry.get("target_kind") not in {"git-backed", "local-project"}: - raise SystemExit("Executor result repository evidence target_kind is invalid") - if entry.get("preflight_kind") not in {"git-clean-worktree", "local-project"}: - raise SystemExit("Executor result repository evidence preflight_kind is invalid") - if entry.get("baseline") not in {"initial", "accepted-handoff"}: - raise SystemExit("Executor result repository evidence baseline is invalid") - if entry.get("status") not in {"clean", "blocked"}: - raise SystemExit("Executor result repository evidence status is invalid") - if metadata_required: - metadata = entry.get("metadata") - required_fields = { - "repository_id", - "expected_branch", - "actual_branch", - "branch_status", - "expected_commit", - "actual_commit", - "commit_status", - "baseline_status", - } - if not isinstance(metadata, dict) or not required_fields.issubset(metadata): - raise SystemExit("Executor result repository metadata evidence is missing required fields") - validated.append(entry) - return validated - - -def _validated_codegraph_evidence( - handoff: dict[str, Any], repository_entries: list[dict[str, Any]] -) -> list[dict[str, Any]]: - entries = handoff.get("codegraph") - if not isinstance(entries, list) or not entries: - raise SystemExit("Executor result is missing applicable CodeGraph evidence") - repository_roots = {str(Path(str(entry["root"])).resolve()) for entry in repository_entries} - validated: list[dict[str, Any]] = [] - for entry in entries: - if not isinstance(entry, dict): - raise SystemExit("Executor result CodeGraph evidence entries must be mappings") - root = Path(str(entry.get("root") or "")) - if not root.is_absolute(): - raise SystemExit("Executor result CodeGraph evidence root must be absolute") - if repository_roots and str(root.resolve()) not in repository_roots: - raise SystemExit("Executor result CodeGraph evidence root has no matching repository evidence") - applicable = entry.get("applicable") - up_to_date = entry.get("up_to_date") - reason = entry.get("reason") - if not isinstance(applicable, bool) or not isinstance(up_to_date, bool): - raise SystemExit("Executor result CodeGraph applicable and up_to_date must be boolean") - if applicable: - if not up_to_date or reason not in {None, ""}: - raise SystemExit("Applicable CodeGraph evidence must be up_to_date without a failure reason") - elif up_to_date or reason != "no-index": - raise SystemExit("Non-applicable CodeGraph evidence must be explicit no-index") - validated.append(entry) - return validated - - -def _observe_repository_and_codegraph_evidence( - task: dict[str, Any], - repository_entries: list[dict[str, Any]], - codegraph_entries: list[dict[str, Any]], - *, - codegraph_required: bool, - accepted_dependency_deltas: Iterable[Mapping[str, object]] | None = None, -) -> None: - workspace = task.get("workspace") if isinstance(task.get("workspace"), dict) else {} - control_root_raw = workspace.get("root") - if not control_root_raw: - raise SystemExit("Task execution binding is missing harness provenance") - binding = load_task_execution_binding( - Path(str(control_root_raw)), str(task["plan_id"]), str(task["task_id"]) - ) - execution_root = Path(str(binding["execution_path"])).resolve() - repository = next( - (entry for entry in repository_entries if Path(str(entry["root"])).resolve() == execution_root), - None, - ) - if repository is None: - raise SystemExit("Executor repository evidence does not match the helper-observed execution binding") - try: - observed_repository = capture_repository_evidence(execution_root) - except RuntimeError as error: - raise SystemExit(str(error)) from error - baseline = binding.get("baseline") - if not isinstance(baseline, dict) or not baseline.get("head"): - raise SystemExit("Task execution binding is missing harness provenance baseline") - caused = task_caused_paths(baseline, observed_repository, execution_root) - task_files = task.get("files") if isinstance(task.get("files"), dict) else {} - _assert_task_caused_delta_in_write_scope( - caused, - task_files, - accepted_dependency_paths=_accepted_dependency_paths( - task, execution_root, accepted_dependency_deltas - ), - ) - - metadata = repository.get("metadata") - if isinstance(metadata, dict): - branch = subprocess.run( - ["git", "-C", str(execution_root), "branch", "--show-current"], - capture_output=True, - text=True, - check=False, - ) - if branch.returncode != 0: - raise SystemExit("Helper-observed repository branch identity is unavailable") - actual_branch = branch.stdout.strip() - actual_commit = str(observed_repository.get("head") or "") - if metadata.get("actual_branch") != actual_branch: - raise SystemExit("Executor repository branch does not match helper-observed identity") - if metadata.get("actual_commit") != actual_commit: - raise SystemExit("Executor repository commit does not match helper-observed identity") - expected_branch = metadata.get("expected_branch") - expected_commit = metadata.get("expected_commit") - observed_branch_status = ( - "not-applicable" if not expected_branch else "matched" if expected_branch == actual_branch else "mismatch" - ) - observed_commit_status = ( - "not-applicable" if not expected_commit else "matched" if expected_commit == actual_commit else "stale" - ) - if metadata.get("branch_status") != observed_branch_status: - raise SystemExit("Executor repository branch status contradicts helper observation") - if metadata.get("commit_status") != observed_commit_status: - raise SystemExit("Executor repository commit status contradicts helper observation") - if not codegraph_required: - return - codegraph = next( - (entry for entry in codegraph_entries if Path(str(entry["root"])).resolve() == execution_root), - None, - ) - if codegraph is None: - raise SystemExit("Executor CodeGraph evidence does not match the helper-observed execution binding") - marker_exists = (execution_root / ".codegraph").is_dir() - if marker_exists and codegraph.get("applicable") is not True: - raise SystemExit("Helper-observed CodeGraph marker contradicts executor no-index evidence") - if not marker_exists and ( - codegraph.get("applicable") is not False or codegraph.get("reason") != "no-index" - ): - raise SystemExit("Helper-observed missing CodeGraph marker requires explicit no-index evidence") - if marker_exists: - try: - status = subprocess.run( - ["codegraph", "status", "--json", str(execution_root)], - capture_output=True, - text=True, - check=False, - ) - except FileNotFoundError as error: - raise SystemExit("Helper-observed CodeGraph status is unavailable") from error - if status.returncode != 0: - raise SystemExit("Helper-observed CodeGraph status is unavailable") - try: - observed_codegraph = json.loads(status.stdout) - except json.JSONDecodeError as error: - raise SystemExit("Helper-observed CodeGraph status is malformed") from error - pending = observed_codegraph.get("pendingChanges") - index = observed_codegraph.get("index") - up_to_date = ( - observed_codegraph.get("initialized") is True - and Path(str(observed_codegraph.get("projectPath") or "")).resolve() == execution_root - and isinstance(pending, dict) - and all(pending.get(kind) == 0 for kind in ("added", "modified", "removed")) - and observed_codegraph.get("worktreeMismatch") is None - and isinstance(index, dict) - and index.get("reindexRecommended") is False - ) - if codegraph.get("up_to_date") is not up_to_date: - raise SystemExit("Executor CodeGraph up_to_date claim contradicts helper-observed status") - - -def validate_executor_result_for_task( - handoff: dict[str, Any], - task: dict[str, Any], - *, - observe: bool = False, - workspace_id: str | None = None, - execution_id: str | None = None, - repository_id: str | None = None, - execution_runtime_root: str | None = None, - mutation_events: Iterable[Mapping[str, object]] | None = None, - accepted_dependency_deltas: Iterable[Mapping[str, object]] | None = None, - prior_ownership: Mapping[str, Mapping[str, object]] | None = None, - repair_continuity: Mapping[str, Mapping[str, object] | RepairContinuity] | None = None, - review_repair_frontier: Mapping[str, object] | None = None, - authorized_replacements: Iterable[str] | None = None, - preparing_review: bool = False, - repair_review_preparation: bool = False, - creation_safe: bool = False, -) -> dict[str, Any]: - if repair_review_preparation and not preparing_review: - raise SystemExit("repair review preparation requires preparing_review") - if handoff.get("type") != "executor-result": - raise SystemExit("Handoff is not executor-result") - for field in FORBIDDEN_EXECUTOR_RESULT_FIELDS: - if field in handoff: - raise SystemExit(f"Executor result contains forbidden field {field}") - if creation_safe or preparing_review: - for field in FORBIDDEN_CREATION_CONTROL_FIELDS: - if field in handoff: - raise SystemExit(f"Executor result contains wrong-owner field {field}") - task_id = str(task.get("task_id") or "") - plan_id = str(task.get("plan_id") or "") - if not task_id or not plan_id: - raise SystemExit("Task brief is missing task_id or plan_id") - _assert_task_handoff_identity(handoff, task_id, plan_id) - result = handoff.get("result") - if not isinstance(result, dict) or result.get("state") not in VALID_RESULT_STATES: - raise SystemExit("Executor result state must be completed, blocked, partial, or failed") - state = str(result["state"]) - unresolved = _as_list(handoff.get("unresolved")) - if state == "completed" and unresolved: - raise SystemExit("Executor result completed state cannot include unresolved blockers") - task_files = task.get("files") if isinstance(task.get("files"), dict) else {} - accepted_authority_paths = [ - str(value) for value in [*_as_list(task_files.get("read")), *_as_list(task_files.get("write"))] - ] - truth_basis = task.get("truth_basis") if isinstance(task.get("truth_basis"), dict) else {} - knowledge_disposition = _validated_knowledge_disposition( - handoff, - list(task.get("source_ids", [])), - accepted_authority_paths, - _allocated_decision_aliases(truth_basis), - ) - if state in {"completed", "partial"}: - _assert_task_fit_check(handoff, task_id, state) - _assert_changed_paths_in_write_scope(handoff, task_files) - if preparing_review or creation_safe or "acceptance_review" not in handoff: - acceptance_review_sequence = None - else: - acceptance_review_sequence = _assert_handoff_review_matches_task(handoff, task, state) - required_items = [ - item - for item in _as_list(task.get("validation")) - if isinstance(item, dict) and item.get("command") - ] - capability = task.get("evidence_capability") if isinstance(task.get("evidence_capability"), dict) else {} - observed_validation = None - evidence_closure = None - reported_commands: dict[str, dict[str, Any]] = {} - if state == "completed" and required_items: - reported_value = handoff.get("validation") - if reported_value is not None and not isinstance(reported_value, dict): - raise SystemExit("Executor result validation report must be a mapping") - reported = reported_value if isinstance(reported_value, dict) else {} - reported_items = reported.get("commands", []) - if not isinstance(reported_items, list): - raise SystemExit("Executor result validation commands must be a list") - for reported_item in reported_items: - if not isinstance(reported_item, dict): - raise SystemExit("Executor result validation commands must contain mappings") - command = str(reported_item.get("command") or "").strip() - if not command or command in reported_commands: - raise SystemExit("Executor result reported validation command is missing or duplicated") - if reported_item.get("result") not in {"passed", "failed", "skipped"}: - raise SystemExit("Executor result reported validation result is invalid") - reported_commands[command] = reported_item - for item in required_items: - command = str(item.get("command")).strip() - if command not in reported_commands: - if creation_safe or observe or capability.get("result") == "mapped": - continue - raise SystemExit(f"Executor result is missing fresh required validation: {command}") - reported_item = reported_commands[command] - compiled_kind = str(item.get("kind") or "").strip().lower() - if compiled_kind == "legacy-untyped": - raise SystemExit("Untyped validation item is legacy-untyped") - if compiled_kind == "inspection": - mechanism = str(item.get("mechanism") or "").strip() - reported_mechanism = str(reported_item.get("mechanism") or "").strip() - if not mechanism or reported_mechanism != mechanism: - raise SystemExit(f"Executor result is missing inspection mechanism: {mechanism}") - allowed = _acceptable_validation_results(item) - result_value = reported_item.get("result") - if result_value not in allowed: - allowed_text = " or ".join(sorted(allowed)) - raise SystemExit( - f"Executor result validation for {command} must be {allowed_text}; got {result_value}" - ) - if state == "completed" and capability.get("result") == "mapped": - if not observe and not creation_safe: - raise SystemExit( - "evidence-closure-blocked: completed mapped invariants require independent harness observation" - ) - if creation_safe: - evidence_closure = _validate_evidence_closure( - handoff, - task, - state, - reported_commands, - None, - creation_safe=True, - ) - else: - evidence_closure = _validate_evidence_closure( - handoff, task, state, reported_commands, None - ) - - evidence_applicability = _task_evidence_applicability(task) - repository_entries: list[dict[str, Any]] = [] - codegraph_entries: list[dict[str, Any]] = [] - if evidence_applicability["repository"]["required"]: - repository_entries = _validated_repository_evidence( - handoff, evidence_applicability["metadata"]["required"] - ) - elif evidence_applicability["metadata"]["required"]: - repository_entries = _validated_repository_evidence(handoff, True) - if evidence_applicability["codegraph"]["required"]: - codegraph_entries = _validated_codegraph_evidence(handoff, repository_entries) - if creation_safe: - delegation = handoff.get("delegation_evidence") - if not isinstance(delegation, Mapping): - raise SystemExit("Executor result is missing delegation_evidence") - try: - normalize_subagent_provenance(delegation) - except OwnershipBlocker as error: - raise SystemExit(str(error)) from error - return { - "knowledge_disposition": knowledge_disposition, - "unresolved": unresolved, - "result_state": state, - "evidence_applicability": evidence_applicability, - "evidence_closure": evidence_closure, - } - if observe and (repository_entries or codegraph_entries): - _observe_repository_and_codegraph_evidence( - task, - repository_entries, - codegraph_entries, - codegraph_required=evidence_applicability["codegraph"]["required"], - accepted_dependency_deltas=accepted_dependency_deltas, - ) - - task_ownership = None - fit = handoff.get("task_fit_check") if isinstance(handoff.get("task_fit_check"), dict) else {} - requires_repair_continuity = ( - fit.get("result") == "repaired" - and (repair_review_preparation or not preparing_review) - and acceptance_review_sequence != "initial-reset" - ) - if ( - state == "completed" - and required_items - and observe - and requires_repair_continuity - ): - if mutation_events is None: - raise AcceptanceOwnershipError( - "review-blocked: completed task requires harness-owned mutation_events evidence" - ) - runtime_mutation_events = list(mutation_events) - if any(not isinstance(event, Mapping) for event in runtime_mutation_events): - raise AcceptanceOwnershipError("Runtime mutation_events must contain mappings") - try: - task_ownership = validate_task_acceptance_ownership( - delegation_evidence=( - handoff.get("delegation_evidence") - if isinstance(handoff.get("delegation_evidence"), dict) - else None - ), - mutation_events=runtime_mutation_events, - write_scope=_as_list(task_files.get("write")), - validations_passed=True, - operation="repair", - ) - _validate_repair_acceptance_continuity( - task=task, - current_ownership=task_ownership, - prior_ownership=prior_ownership, - repair_continuity=repair_continuity, - review_repair_frontier=review_repair_frontier, - authorized_replacements=authorized_replacements, - ) - except OwnershipBlocker as error: - raise AcceptanceOwnershipError(str(error)) from error - - if state == "completed" and required_items and observe: - observed_validation = _observe_completed_validation( - handoff, - task, - required_items, - reported_commands, - workspace_id=workspace_id, - execution_id=execution_id, - repository_id=repository_id, - execution_runtime_root=execution_runtime_root, - accepted_dependency_deltas=accepted_dependency_deltas, - ) - evidence_closure = _validate_evidence_closure( - handoff, task, state, reported_commands, observed_validation - ) - if state == "completed" and capability.get("result") == "mapped": - if observed_validation is None or not isinstance(evidence_closure, dict) or evidence_closure.get("result") != "passed": - raise SystemExit( - "evidence-closure-blocked: completed mapped invariants require produced harness observations and passed evidence closure" - ) - if state == "completed": - if mutation_events is None: - raise AcceptanceOwnershipError( - "review-blocked: completed task requires harness-owned mutation_events evidence" - ) - runtime_mutation_events = list(mutation_events) - if any(not isinstance(event, Mapping) for event in runtime_mutation_events): - raise AcceptanceOwnershipError("Runtime mutation_events must contain mappings") - operation = "repair" if fit.get("result") == "repaired" else "implementation" - try: - task_ownership = validate_task_acceptance_ownership( - delegation_evidence=( - handoff.get("delegation_evidence") - if isinstance(handoff.get("delegation_evidence"), dict) - else None - ), - mutation_events=runtime_mutation_events, - write_scope=_as_list(task_files.get("write")), - validations_passed=True, - operation=operation, - ) - if operation == "repair" and requires_repair_continuity: - _validate_repair_acceptance_continuity( - task=task, - current_ownership=task_ownership, - prior_ownership=prior_ownership, - repair_continuity=repair_continuity, - review_repair_frontier=review_repair_frontier, - authorized_replacements=authorized_replacements, - ) - except OwnershipBlocker as error: - raise AcceptanceOwnershipError(str(error)) from error - return { - "knowledge_disposition": knowledge_disposition, - "unresolved": unresolved, - "result_state": state, - "evidence_applicability": evidence_applicability, - "evidence_closure": evidence_closure, - **({"task_ownership": task_ownership} if task_ownership is not None else {}), - **({"observed_validation": observed_validation} if observed_validation is not None else {}), - } - - -def validate_executor_result_creation_for_task( - handoff: dict[str, Any], task: dict[str, Any] -) -> dict[str, Any]: - """Validate immutable executor facts without observation, review, or acceptance.""" - - return validate_executor_result_for_task( - handoff, task, observe=False, preparing_review=False, creation_safe=True - ) - - -def _validate_repair_acceptance_continuity( - *, - task: Mapping[str, object], - current_ownership: Mapping[str, object], - prior_ownership: Mapping[str, Mapping[str, object]] | None, - repair_continuity: Mapping[str, Mapping[str, object] | RepairContinuity] | None, - review_repair_frontier: Mapping[str, object] | None, - authorized_replacements: Iterable[str] | None, -) -> None: - """Bind repair acceptance to the scheduler's original owner and identities.""" - - task_id = str(task.get("task_id") or "") - if prior_ownership is not None and not isinstance(prior_ownership, Mapping): - raise OwnershipBlocker("review-blocked", "repair prior_ownership must be a task mapping") - if repair_continuity is not None and not isinstance(repair_continuity, Mapping): - raise OwnershipBlocker("review-blocked", "repair_continuity must be a task mapping") - previous_value = (prior_ownership or {}).get(task_id) - continuity_value = (repair_continuity or {}).get(task_id) - if previous_value is None: - raise OwnershipBlocker("review-blocked", f"{task_id} repair lacks prior owner") - if continuity_value is None: - raise OwnershipBlocker("review-blocked", f"{task_id} repair lacks continuity identities") - previous = normalize_subagent_provenance(previous_value) - if isinstance(continuity_value, RepairContinuity): - continuity = continuity_value - else: - if not isinstance(continuity_value, Mapping) or set(continuity_value) != { - "binding_id", - "baseline_identity", - "evidence_identity", - "previous_review_identity", - }: - raise OwnershipBlocker("review-blocked", f"{task_id} repair continuity is not closed") - try: - continuity = RepairContinuity(**{key: str(value) for key, value in continuity_value.items()}) - except (TypeError, ValueError) as error: - raise OwnershipBlocker("review-blocked", f"{task_id} repair continuity is invalid") from error - - workspace = task.get("workspace") if isinstance(task.get("workspace"), Mapping) else {} - control_root = Path(str(workspace.get("root") or "")).expanduser().resolve() - if not control_root.is_dir(): - raise OwnershipBlocker("review-blocked", f"{task_id} repair control root is unavailable") - binding = load_task_execution_binding(control_root, str(task.get("plan_id") or ""), task_id) - baseline = binding.get("baseline") - if not isinstance(baseline, Mapping) or not baseline.get("head"): - raise OwnershipBlocker("review-blocked", f"{task_id} repair baseline identity is unavailable") - frontier = review_repair_frontier or {} - ownership = binding.get("ownership") if isinstance(binding.get("ownership"), Mapping) else {} - try: - expected = RepairContinuity( - binding_id=str(ownership.get("binding_id") or ""), - baseline_identity=semantic_digest(dict(baseline)), - evidence_identity=str(frontier.get("frozen_evidence_reference") or ""), - previous_review_identity=str(frontier.get("prior_review_id") or ""), - ) - except ValueError as error: - raise OwnershipBlocker( - "review-blocked", f"{task_id} repair frontier continuity is unavailable" - ) from error - if continuity != expected: - raise OwnershipBlocker("review-blocked", f"{task_id} repair continuity identities do not match") - if isinstance(authorized_replacements, (str, bytes, Mapping)): - raise OwnershipBlocker("review-blocked", "authorized_replacements must be task IDs") - replacements = {str(value) for value in (authorized_replacements or [])} - replaced = current_ownership.get("agent_id") != previous.get("agent_id") - if replaced and task_id not in replacements: - raise OwnershipBlocker( - "review-blocked", f"{task_id} repair owner replacement is not authorized" - ) - - -def _acceptable_validation_results(item: dict[str, Any]) -> set[str]: - raw = item.get("acceptable_results") - if isinstance(raw, list) and raw: - allowed = {str(value).strip() for value in raw if str(value).strip()} - if not allowed.issubset({"passed", "skipped", "failed"}): - raise SystemExit("Task validation acceptable_results must be passed, skipped, or failed") - return allowed - expected = str(item.get("expected") or "").strip().lower() - if expected in {"skipped", "skip"}: - return {"passed", "skipped"} - return {"passed"} - - -def _assert_task_fit_check(handoff: dict[str, Any], task_id: str, state: str) -> None: - fit = handoff.get("task_fit_check") - if not isinstance(fit, dict) or not fit: - raise SystemExit("Executor result completed or partial state requires task_fit_check") - fit_task = fit.get("task") or fit.get("related_task") - if fit_task != task_id: - raise SystemExit( - f"Executor result task_fit_check task mismatch: expected {task_id}, got {fit_task or 'missing'}" - ) - allowed = {"clean", "repaired"} if state == "completed" else TASK_FIT_RESULTS - if fit.get("result") not in allowed: - allowed_text = " or ".join(sorted(allowed)) - raise SystemExit(f"Executor result task_fit_check result must be {allowed_text}") - - -def _assert_changed_paths_in_write_scope(handoff: dict[str, Any], task_files: dict[str, Any]) -> None: - try: - read_scope = { - canonical_relative_path(str(path)) for path in _as_list(task_files.get("read")) - } - write_scope = { - canonical_relative_path(str(path)) for path in _as_list(task_files.get("write")) - } - except OwnershipBlocker as error: - raise SystemExit(f"Declared write scope is unsafe: {error.reason}") from error - changes = handoff.get("changes") if isinstance(handoff.get("changes"), dict) else {} - for item in _as_list(changes.get("files")): - if not isinstance(item, dict): - raise SystemExit("Executor result file entry must be a mapping with a non-empty path") - path = str(item.get("path") or "").strip() - if not path: - raise SystemExit("Executor result file entry must provide a non-empty path") - try: - canonical = canonical_relative_path(path) - except OwnershipBlocker as error: - raise SystemExit(f"Executor result changed path is unsafe: {path}") from error - if item.get("action") == "inspected" and canonical not in read_scope | write_scope: - raise SystemExit( - f"Executor result inspected path is outside task inspection scope: {path}" - ) - if item.get("action") != "inspected" and canonical not in write_scope: - raise SystemExit(f"Executor result changed path is outside task write scope: {path}") - - -def _assert_handoff_review_matches_task( - handoff: dict[str, Any], task: dict[str, Any], state: str -) -> str | None: - compiled_required = task.get("review_required") is True - review = handoff.get("acceptance_review") if isinstance(handoff.get("acceptance_review"), dict) else {} - handoff_required = review.get("required") is True - if compiled_required != handoff_required: - raise SystemExit("Executor result acceptance_review.required must match compiled review_required") - if compiled_required and state == "completed" and review.get("verdict") != "accept": - raise SystemExit("Review-required task cannot complete without acceptance_review.verdict: accept") - if not compiled_required or state != "completed": - return None - fit = handoff.get("task_fit_check") if isinstance(handoff.get("task_fit_check"), dict) else {} - repaired = fit.get("result") == "repaired" - mode = review.get("review_mode") - reset_initial = mode == "initial" and review.get("review_reset") is not None - if repaired and mode != "repair" and not reset_initial: - raise SystemExit("Repaired task completion requires a sequenced repair acceptance_review") - if mode is not None: - try: - from review_runtime import ReviewContractError, validate_task_acceptance_review - validated = validate_task_acceptance_review(review) - except (ReviewContractError, TypeError, ValueError) as error: - raise SystemExit(f"Task acceptance review is invalid: {error}") from error - if validated.verdict != "accepted": - raise SystemExit("Review-required task cannot complete without an accepted task review") - if validated.target_identity["artifact_id"] != str(task.get("task_id")): - raise SystemExit("Task acceptance review target identity does not match the completed task") - if validated.review_mode == "repair": - repositories = [ - item for item in _as_list(handoff.get("repository")) - if isinstance(item, dict) and item.get("target_kind") == "git-backed" - ] - if len(repositories) != 1: - raise SystemExit("Task repair review requires one observed Git repository identity") - root = Path(str(repositories[0].get("root") or "")).expanduser().resolve() - head = subprocess.run(["git", "-C", str(root), "rev-parse", "HEAD"], capture_output=True, text=True) - tree = subprocess.run(["git", "-C", str(root), "rev-parse", "HEAD^{tree}"], capture_output=True, text=True) - if head.returncode or tree.returncode: - raise SystemExit("Task repair review Git identity is unavailable") - if (review.get("reviewed_head") != head.stdout.strip() - or validated.target_identity["source_tree"] != tree.stdout.strip()): - raise SystemExit("Task repair review does not match the observed Git head/tree identity") - if validated.review_mode == "initial" and validated.review_reset is not None: - return "initial-reset" - return validated.review_mode - return None - - -def _yaml_scalar(value: Any) -> str: - if isinstance(value, _SemanticReference): - return value.reference_id - if value is True: - return "true" - if value is False: - return "false" - if value is None: - return "null" - if isinstance(value, (int, float)): - return str(value) - text = str(value) - if re.fullmatch(r"[A-Za-z0-9_./*:-]+", text) and ": " not in text: - return text - return json.dumps(text, ensure_ascii=False) +def _yaml_scalar(value: Any) -> str: + if isinstance(value, _SemanticReference): + return value.reference_id + if value is True: + return "true" + if value is False: + return "false" + if value is None: + return "null" + if isinstance(value, (int, float)): + return str(value) + text = str(value) + if re.fullmatch(r"[A-Za-z0-9_./*:-]+", text) and ": " not in text: + return text + return json.dumps(text, ensure_ascii=False) def _dump_yaml(value: Any, indent: int = 0) -> list[str]: @@ -4722,31 +1174,6 @@ def _dump_yaml(value: Any, indent: int = 0) -> list[str]: return [f"{prefix}{_yaml_scalar(value)}"] -def _section(body: str, name: str) -> list[str]: - lines = body.splitlines() - start: int | None = None - for index, line in enumerate(lines): - if re.match(rf"^##\s+(?:\d+(?:\.\d+)?\s+)?{re.escape(name)}\s*$", line.strip(), re.IGNORECASE): - start = index + 1 - break - if start is None: - return [] - end = next((index for index in range(start, len(lines)) if lines[index].startswith("## ")), len(lines)) - return lines[start:end] - - -def _section_table(body: str, name: str) -> list[list[str]]: - rows: list[list[str]] = [] - for line in _section(body, name): - if not line.strip().startswith("|"): - continue - cells = [cell.strip() for cell in line.strip().strip("|").split("|")] - if not cells or all(re.fullmatch(r":?-{3,}:?", cell) for cell in cells): - continue - rows.append(cells) - return rows[1:] if rows else [] - - VALIDATION_KINDS = {"process", "inspection"} @@ -4776,9 +1203,7 @@ def _compile_structured_validation_item(item: Any) -> dict[str, Any]: return compiled -def _compile_task_validation( - task: dict[str, Any], task_body: str, source_ids: list[str], records: dict[str, str] -) -> list[Any]: +def _compile_task_validation(task: dict[str, Any]) -> list[Any]: validation_items = _as_list(task.get("validation")) if validation_items: task_policy = task.get("evidence_reuse") @@ -4792,43 +1217,40 @@ def _compile_task_validation( raise SystemExit("Task validation items must be mappings") compiled.append(_compile_structured_validation_item(item)) return compiled - if _section_table(task_body, "Validation"): - raise SystemExit( - "Untyped Validation table row is legacy-untyped; migrate to front-matter validation with explicit kind" - ) raise SystemExit( - "Task validation must declare front-matter items with explicit kind; TEST-ID fallback is not executable terminal validation" + "Task validation must declare structured items with explicit kind" ) -def _task_context(args: argparse.Namespace) -> tuple[Path, Path, dict[str, Any], str, dict[str, str], list[Path]]: +def _task_context(args: argparse.Namespace) -> tuple[Path, Path, dict[str, Any], dict[str, str], list[Path]]: root = resolve_workspace_root(args) task_root = root / ".work-bundle/orchestration/plan" task_path = _input_path(args.task, root, task_root, "task") - task_data, task_body = _read_structured(task_path) + task_data, _ = _read_structured(task_path) task_id = _artifact_id(task_data, "id", task_path) plan_id = _artifact_id(task_data, "plan_id", task_path) - _, plan_data = _find_plan(root, plan_id) + phase_id = _artifact_id(task_data, "phase_id", task_path) + plan_path, plan_data = _find_plan(root, plan_id) + state = plan_path.relative_to(task_root).parts[0] + task_result = read_artifact( + PLAN_CATALOG, "task", {"workspace_root": root}, identity=task_id, + state=state, bindings={"plan": plan_id, "phase": phase_id}, + ) + if Path(str(task_result["path"])).resolve() != task_path.resolve(): + raise SystemExit(f"Task is not at its canonical location: {task_path}") + read_artifact( + PLAN_CATALOG, "phase", {"workspace_root": root}, identity=phase_id, + state=state, bindings={"plan": plan_id}, + ) source_paths = _resolve_spec_paths(root, task_data, plan_data) - records: dict[str, str] = {} + records = source_obligation_records(task_data, label="Canonical task") for source_path in source_paths: - _, body = _read_structured(source_path) - for identifier, value in _source_records(source_path, body).items(): - if identifier in records and records[identifier] != value: - raise SystemExit(f"Ambiguous source ID {identifier} across linked specifications") - records[identifier] = value - return root, task_path, task_data, task_body, records, source_paths - - -def task_flow_id(args: argparse.Namespace) -> str: - """Resolve a task's bound plan identity without compiling or writing artifacts.""" - - root = resolve_workspace_root(args) - task_path = _input_path( - args.task, root, root / ".work-bundle/orchestration/plan", "task" - ) - task, _ = _read_structured(task_path) - return _artifact_id(task, "plan_id", task_path) + source_data, _body = _read_structured(source_path) + if source_path.parent.name != "active" or source_data.get("status") != "verified": + raise SystemExit( + f"Task source specification must be canonical, active, and verified: {source_path}" + ) + return root, task_path, task_data, records, source_paths def _contains_resolved_source_record(value: Any, record: str) -> bool: @@ -4844,6 +1266,8 @@ def _contains_resolved_source_record(value: Any, record: str) -> bool: STATIC_TASK_FIELDS = frozenset( { "id", + "artifact_type", + "schema_version", "plan_id", "phase_id", "name", @@ -4857,12 +1281,14 @@ def _contains_resolved_source_record(value: Any, record: str) -> bool: "owner", "depends_on", "source_ids", + "source_obligations", "truth_basis", "source_files", "target_files", "forbidden_files", "target_symbols", "interfaces", + "steps", "completion_criteria", "methodology", "executor_profile", @@ -4889,6 +1315,7 @@ def _contains_resolved_source_record(value: Any, record: str) -> bool: "accepted_result_references", "evidence_reference", "evidence_references", + "handoff_contract", } ) @@ -5035,23 +1462,121 @@ def static_plan_task_admission( plan_root = root / ".work-bundle/orchestration/plan" if not plan_path.resolve().is_relative_to(plan_root.resolve()): raise SystemExit("plan review static-admission-blocked: root plan escapes plan store") - if content is None: - plan, _ = _read_structured(plan_path) - else: - if not content.startswith("---\n") or "\n---\n" not in content[4:]: - raise SystemExit("plan review static-admission-blocked: root plan lacks front matter") - plan = parse_yaml_subset(content[4:].split("\n---\n", 1)[0]) - if not isinstance(plan, dict): - raise SystemExit("plan review static-admission-blocked: root plan front matter is invalid") + if content is not None: + raise SystemExit( + "plan review static-admission-blocked: in-memory plan content is not canonical stored authority" + ) + plan, _ = _read_structured(plan_path) plan_id = _artifact_id(plan, "id", plan_path) + canonical_plan_path, _canonical_plan = _find_plan(root, plan_id) + if canonical_plan_path.resolve() != plan_path.resolve(): + raise SystemExit("plan review static-admission-blocked: root plan is not canonical") + state = canonical_plan_path.relative_to(plan_root).parts[0] + phase_index = rebuild_index( + PLAN_CATALOG, "phase", {"workspace_root": root} + ) + phase_rows = [ + json.loads(line) + for line in Path(str(phase_index["path"])).read_text(encoding="utf-8").splitlines() + if line.strip() + ] + phase_rows = [row for row in phase_rows if str(row.get("plan_id") or "") == plan_id] + phase_data: dict[str, dict[str, Any]] = {} + for row in phase_rows: + phase_id = str(row["id"]) + stored = read_artifact( + PLAN_CATALOG, "phase", {"workspace_root": root}, identity=phase_id, + state=state, bindings={"plan": plan_id}, + ) + phase_data[phase_id] = dict(stored["data"]) + declared_phase_ids = { + str(item.get("id") or "") for item in _as_list(plan.get("phase_index")) + if isinstance(item, dict) + } + if declared_phase_ids != set(phase_data): + raise SystemExit( + "plan review static-admission-blocked: root phase_index does not match canonical phases" + ) task_paths: list[Path] = [] - for path in sorted(plan_root.rglob("*.md")): - if path == plan_path: + task_index = rebuild_index( + PLAN_CATALOG, "task", {"workspace_root": root} + ) + task_rows: list[dict[str, Any]] = [] + for line in Path(str(task_index["path"])).read_text(encoding="utf-8").splitlines(): + if not line.strip(): continue - data, _ = _read_structured(path) - if str(data.get("plan_id") or "") != plan_id or not data.get("phase_id"): + data = json.loads(line) + if str(data.get("plan_id") or "") != plan_id: continue + task_rows.append(data) + path = canonical_artifact_path( + family_policy(load_catalog(PLAN_CATALOG), "task"), + {"workspace_root": root}, identity=str(data["id"]), state=state, + bindings={"plan": plan_id, "phase": str(data["phase_id"])}, + ) + if not path.is_file(): + raise SystemExit( + f"plan review static-admission-blocked: task is not in plan lifecycle state: {data['id']}" + ) task_paths.append(path) + task_ids_by_phase = { + phase_id: { + str(row["id"]) for row in task_rows if str(row.get("phase_id") or "") == phase_id + } + for phase_id in phase_data + } + for phase_id, data in phase_data.items(): + declared = { + str(item.get("id") or "") for item in _as_list(data.get("task_index")) + if isinstance(item, dict) + } + if declared != task_ids_by_phase[phase_id]: + raise SystemExit( + f"plan review static-admission-blocked: {phase_id} task_index does not match canonical tasks" + ) + all_task_ids = {str(row["id"]) for row in task_rows} + validation_ids = { + str(item.get("id") or "") for item in _as_list(plan.get("validation_strategy")) + if isinstance(item, dict) + } + for coverage in _as_list(plan.get("source_coverage")): + if not isinstance(coverage, dict): + continue + unknown_phases = set(map(str, _as_list(coverage.get("phase_ids")))) - set(phase_data) + unknown_tasks = set(map(str, _as_list(coverage.get("task_ids")))) - all_task_ids + unknown_validation = set(map(str, _as_list(coverage.get("validation_ids")))) - validation_ids + if unknown_phases or unknown_tasks or unknown_validation: + raise SystemExit( + "plan review static-admission-blocked: source_coverage references unknown plan members or validation IDs" + ) + phase_dependencies = { + phase_id: [str(value) for value in _as_list(data.get("depends_on"))] + for phase_id, data in phase_data.items() + } + for phase_id, required in phase_dependencies.items(): + invalid = [value for value in required if value == phase_id or value not in phase_data] + if invalid: + raise SystemExit( + f"plan review static-admission-blocked: {phase_id} has impossible dependency {', '.join(invalid)}" + ) + phase_visiting: set[str] = set() + phase_visited: set[str] = set() + + def visit_phase(phase_id: str) -> None: + if phase_id in phase_visiting: + raise SystemExit( + f"plan review static-admission-blocked: phase dependency cycle includes {phase_id}" + ) + if phase_id in phase_visited: + return + phase_visiting.add(phase_id) + for dependency in phase_dependencies[phase_id]: + visit_phase(dependency) + phase_visiting.remove(phase_id) + phase_visited.add(phase_id) + + for phase_id in phase_dependencies: + visit_phase(phase_id) compiled: list[dict[str, Any]] = [] by_id: dict[str, Path] = {} for task_path in task_paths: @@ -5103,7 +1628,7 @@ def visit(task_id: str) -> None: def _compile_task_brief(args: argparse.Namespace) -> tuple[Path, dict[str, Any]]: - root, task_path, task, task_body, records, source_paths = _task_context(args) + root, task_path, task, records, source_paths = _task_context(args) task_id = _artifact_id(task, "id", task_path) plan_id = _artifact_id(task, "plan_id", task_path) source_ids = [str(item) for item in _as_list(task.get("source_ids"))] @@ -5142,25 +1667,13 @@ def _compile_task_brief(args: argparse.Namespace) -> tuple[Path, dict[str, Any]] requirement = item.get("requirement") or item.get("applies_when") or item.get("enforcement") rules.append({"id": item["id"], "requirement": requirement or "Apply this allocated rule."}) - interfaces = task.get("interfaces") if isinstance(task.get("interfaces"), dict) else {} - if not interfaces: - inferred_interfaces: dict[str, list[str]] = {"consumes": [], "produces": []} - for cells in _section_table(task_body, "Files and interfaces"): - identifier = next((cell.strip("` ") for cell in cells if SOURCE_ID_RE.fullmatch(cell.strip("` "))), None) - direction = next((cell.lower() for cell in cells if cell.lower() in {"consume", "consumes", "produce", "produces"}), None) - if identifier and direction: - inferred_interfaces["produces" if direction.startswith("produce") else "consumes"].append(identifier) - interfaces = inferred_interfaces - api_ids = [sid for sid in source_ids if sid.startswith(("API-", "IFACE-"))] - if api_ids and not _as_list(interfaces.get("consumes")) and not _as_list(interfaces.get("produces")): - interfaces = {"consumes": api_ids, "produces": []} - - validation_value = _compile_task_validation(task, task_body, source_ids, records) + interfaces = dict(task["interfaces"]) + + validation_value = _compile_task_validation(task) executor_profile = _compile_executor_profile(task, task_path) evidence_applicability = task_evidence_applicability(task) - goal_lines = [line.strip() for line in _section(task_body, "Goal") if line.strip()] - resolved_goal = task.get("goal") or (goal_lines[0] if goal_lines else None) or task.get("name") or task_id + resolved_goal = task.get("name") or task_id source_by_kind = { "requirements": [f"{sid}: {records[sid]}" for sid in source_ids if not sid.startswith(("CON-", "API-", "IFACE-", "TEST-"))], "constraints": [f"{sid}: {records[sid]}" for sid in source_ids if sid.startswith("CON-")], @@ -5245,573 +1758,44 @@ def _compile_task_brief(args: argparse.Namespace) -> tuple[Path, dict[str, Any]] def build_task_brief(args: argparse.Namespace) -> Path: target, brief = _compile_task_brief(args) - from bounded_closure import require_orchestration_admission - require_orchestration_admission( - resolve_workspace_root(args), operation="reconciliation", - flow_id=str(brief["task_brief"]["plan_id"]), - ) _maybe_bind_execution_from_args(args, brief["task_brief"]) target.parent.mkdir(parents=True, exist_ok=True) target.write_text("\n".join(_dump_yaml(brief)) + "\n", encoding="utf-8") return target -def _git(root: Path, *arguments: str) -> str: - result = subprocess.run( - ["git", "-C", str(root), *arguments], - check=False, - capture_output=True, - text=True, - ) - if result.returncode != 0: - raise SystemExit(f"Git command failed for review package: {' '.join(arguments[:2])}") - return result.stdout -def _resolve_commit(root: Path, reference: str) -> str: - return _git(root, "rev-parse", "--verify", f"{reference}^{{commit}}").strip() -def _untracked_diff(root: Path, path: str) -> str: - if _protected_project_path(path, root): - return f"diff --git a/{path} b/{path}\nnew file mode (content withheld: protected path)\n" - result = subprocess.run( - ["git", "-C", str(root), "diff", "--no-ext-diff", "--binary", "--no-index", "--", "/dev/null", path], - check=False, - capture_output=True, - text=True, - ) - if result.returncode not in {0, 1}: - raise SystemExit(f"Git command failed for untracked review path: {path}") - return result.stdout -def _paths_from_name_status(line: str) -> list[str]: - cells = line.split("\t") - return cells[1:] if len(cells) > 1 else [] -def _write_scope_match(path: str, write_paths: list[str]) -> bool: - try: - normalized = canonical_relative_path(path) - except OwnershipBlocker as error: - raise SystemExit(f"Observed mutation path is unsafe: {path}") from error - for write in write_paths: - try: - write_n = canonical_relative_path(str(write)) - except OwnershipBlocker as error: - raise SystemExit(f"Declared write scope is unsafe: {write}") from error - if normalized == write_n or normalized.startswith(f"{write_n}/"): - return True - return False -def _partition_name_status( - names: list[str], write_paths: list[str] -) -> tuple[list[str], list[str]]: - in_scope: list[str] = [] - out_scope: list[str] = [] - for line in names: - paths = _paths_from_name_status(line) - scoped = [path for path in paths if _write_scope_match(path, write_paths)] - other = [path for path in paths if path not in scoped] - status = line.split("\t", 1)[0] - if scoped: - in_scope.append(line if not other else "\t".join([status, *scoped])) - if other: - out_scope.append(line if not scoped else "\t".join([status, *other])) - return in_scope, out_scope - - -def _bounded_changed_diff( - root: Path, base: str, head: str | None, names: list[str] -) -> str: - path_groups = [_paths_from_name_status(line) for line in names] - safe_paths = sorted( - {path for paths in path_groups if not any(_protected_project_path(p, root) for p in paths) for path in paths} - ) - arguments = ["diff", "--no-ext-diff", "--binary", "--unified=3", base] - if head is not None: - arguments.append(head) - diff = _git(root, *arguments, "--", *safe_paths) if safe_paths else "" - protected = sorted( - {path for paths in path_groups if any(_protected_project_path(p, root) for p in paths) for path in paths} - ) - for path in protected: - diff += f"diff --git a/{path} b/{path}\n(content withheld: protected path)\n" - return diff - - -def _review_diff( - root: Path, base: str, head_reference: str, write_paths: list[str] -) -> tuple[str, str, list[str], list[str]]: - if head_reference.lower() not in WORKTREE_REFS: - head = _resolve_commit(root, head_reference) - names = [line for line in _git(root, "diff", "--name-status", base, head, "--").splitlines() if line] - in_scope, out_scope = _partition_name_status(names, write_paths) - diff = _bounded_changed_diff(root, base, head, in_scope) - return head, diff, in_scope, out_scope - - names = [line for line in _git(root, "diff", "--name-status", base, "--").splitlines() if line] - untracked = [line for line in _git(root, "ls-files", "--others", "--exclude-standard", "--").splitlines() if line] - for path in untracked: - names.append(f"A\t{path}") - in_scope, out_scope = _partition_name_status(names, write_paths) - diff = _bounded_changed_diff(root, base, None, in_scope) - untracked_set = set(untracked) - for line in in_scope: - for path in _paths_from_name_status(line): - if path in untracked_set: - diff += _untracked_diff(root, path) - digest = hashlib.sha256(("\n".join(in_scope) + "\n" + diff).encode("utf-8")).hexdigest() - return f"worktree:{digest}", diff, in_scope, out_scope - - -def _redact_diff(text: str) -> str: - lines: list[str] = [] - for line in text.splitlines(): - if "BEGIN PRIVATE KEY" in line or "END PRIVATE KEY" in line: - lines.append("<redacted credential material>") - continue - lines.append(SENSITIVE_ASSIGNMENT_RE.sub(lambda match: f"{match.group(1)}: <redacted>", line)) - return "\n".join(lines) -def _markdown_items(values: list[Any], empty: str = "None.") -> list[str]: - if not values: - return [f"- {empty}"] - result = [] - for value in values: - if isinstance(value, dict): - summary = ", ".join(f"{key}: {item}" for key, item in value.items()) - result.append(f"- `{summary}`") - else: - result.append(f"- {value}") - return result -def build_product_review_candidate( - *, - task: Mapping[str, Any], - base: str, - head: str, - diff: str, - changed_files: Sequence[str], - changed_symbols: Sequence[str], - validation_observations: Sequence[Mapping[str, Any]], - repair_context: Mapping[str, Any] | None = None, - knowledge_disposition: Mapping[str, Any] | None = None, - unresolved: Sequence[Any] = (), -) -> dict[str, Any]: - """Build the sole semantic task-review input, excluding transport bookkeeping.""" - - candidate = { - "task_authority": { - "task_id": str(task.get("task_id") or ""), - "plan_id": str(task.get("plan_id") or ""), - "goal": task.get("goal"), - "requirements": list(_as_list(task.get("requirements"))), - "constraints": list(_as_list(task.get("constraints"))), - "accepted_boundaries": list(_as_list( - (task.get("truth_basis") or {}).get("decision_authority") - if isinstance(task.get("truth_basis"), Mapping) else [] - )), - "files": task.get("files", {}), - "interfaces": task.get("interfaces", {}), - }, - "source": { - "base": base, - "head": head, - "diff": diff, - "changed_files": list(changed_files), - }, - "validation_observations": [dict(item) for item in validation_observations], - "repair_context": ( - { - "blocking_finding_ids": list(_as_list(repair_context.get("blocking_finding_ids"))), - "affected_boundaries": list(_as_list(repair_context.get("affected_boundaries"))), - } - if isinstance(repair_context, Mapping) - else None - ), - "unresolved": list(unresolved), - } - forbidden = {"handoff", "acceptance_review", "publication", "reviewer_run"} - if forbidden.intersection(candidate): - raise SystemExit("product review candidate contains publication bookkeeping") - _assert_no_credential_values(candidate, "product review candidate") - return candidate -def _task_review_target_identity( - task: Mapping[str, Any], head: str, source_tree: str | None -) -> dict[str, Any]: - identity = { - "artifact_id": str(task.get("task_id") or ""), - "revision": head, - "source_tree": source_tree, - } - return { - **identity, - "sha256": semantic_digest( - {"task": _accepted_task_projection(task), "source": identity} - ), - } -def _stored_task_repair_preparation( - root: Path, - task: Mapping[str, Any], - *, - base: str, - repaired_identity: Mapping[str, Any], -) -> tuple[dict[str, Any] | None, dict[str, Any] | None]: - """Derive one repair frontier from immutable published controller authority.""" - - from review_runtime import ( - ReviewContractError, - _repair_frontier, - load_stored_review, - review_evidence_identity, - ) - task_id = str(task.get("task_id") or "") - matches: list[tuple[dict[str, Any], Any]] = [] - store = root / ".work-bundle/orchestration/reviews" - for path in sorted(store.glob("*.json")) if store.is_dir() else []: - try: - raw = path.read_bytes() - candidate = json.loads(raw) - except (OSError, json.JSONDecodeError): - continue - if not isinstance(candidate, dict): - continue - identity = candidate.get("target_identity") - if ( - candidate.get("review_target_kind") != "task" - or candidate.get("verdict") != "repair" - or not isinstance(identity, Mapping) - or identity.get("artifact_id") != task_id - or identity.get("revision") != base - ): - continue - reference = { - "review_id": str(candidate.get("review_id") or ""), - "sha256": hashlib.sha256(raw).hexdigest(), - } - try: - record, validated = load_stored_review( - root, reference, current_target_identity=identity - ) - except (ReviewContractError, KeyError, TypeError, ValueError) as error: - raise SystemExit(f"review-blocked: prior task review is invalid: {error}") from error - matches.append((record, validated)) - if not matches: - return None, None - if len(matches) != 1: - raise SystemExit("review-blocked: prior task repair review is ambiguous") - previous, validated = matches[0] - blocking = [ - item - for item in _as_list(previous.get("findings")) - if isinstance(item, Mapping) - and item.get("severity") == "blocking" - and item.get("recommended_owner") == "task_owner" - ] - finding_ids = [str(item.get("finding_id") or "") for item in blocking] - boundaries = sorted( - { - str(evidence.get("locator") or "") - for item in blocking - for evidence in _as_list(item.get("evidence")) - if isinstance(evidence, Mapping) and evidence.get("locator") - } - ) - if not finding_ids or any(not value for value in finding_ids) or not boundaries: - raise SystemExit("review-blocked: prior repair review lacks closed blocking boundaries") - frontier = { - "prior_review_id": validated.review_id, - "blocking_finding_ids": finding_ids, - "previous_reviewed_identity": dict(validated.target_identity), - "repaired_identity": dict(repaired_identity), - "affected_boundaries": boundaries, - "frozen_evidence_reference": review_evidence_identity(previous), - } - try: - return previous, dict(_repair_frontier(frontier)) - except (ReviewContractError, KeyError, TypeError, ValueError) as error: - raise SystemExit(f"review-blocked: invalid derived repair frontier: {error}") from error -def build_review_package(args: argparse.Namespace) -> Path: - if not args.base or not args.head: - raise SystemExit("build-review-package requires --base and --head") - target, brief_document = _compile_task_brief(args) - root = resolve_workspace_root(args) - task = brief_document["task_brief"] - task_id = str(task["task_id"]) - plan_id = str(task.get("plan_id") or "") - if not plan_id: - raise SystemExit(f"Task brief is missing plan_id for {task_id}") - handoff: dict[str, Any] = {} - validated: dict[str, Any] = {} - accepted: dict[str, Any] | None = None - binding_path = _binding_path(root, plan_id, task_id) - raw_binding = _read_binding_file(binding_path) if binding_path.is_file() else {} - if isinstance(raw_binding.get("accepted_result"), Mapping): - binding = load_task_execution_binding(root, plan_id, task_id) - _, accepted = _load_materialized_accepted_task_result(root, task) - accepted_source = accepted["accepted_source"] - if not isinstance(accepted_source, Mapping): - raise SystemExit("review-blocked: accepted source identity is invalid") - execution_root = Path(str(binding["execution_path"])).resolve() - if _resolve_commit(execution_root, str(args.base)) != accepted_source["head"]: - raise SystemExit("review-blocked: accepted-task repair base must be the accepted source") - review_mode = "repair" - elif args.handoff: - handoff_root = root / ".work-bundle/orchestration/handoff" - handoff_path = _input_path(args.handoff, root, handoff_root, "handoff") - handoff, _ = _read_structured(handoff_path) - # Reject malformed immutable executor facts before controller-runtime - # lookup can obscure the actual creation/admission owner. - validate_executor_result_creation_for_task(handoff, task) - review_mode = "initial" - else: - raise SystemExit("build-review-package requires an initial executor handoff or accepted task result") - if review_mode not in {"initial", "repair"}: - raise SystemExit("review-blocked: review_mode must be initial or repair") - if accepted is None: - binding = load_task_execution_binding(root, plan_id, task_id) - execution_root = Path(str(binding["execution_path"])).resolve() - - 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( - execution_root, base, str(args.head), write_paths - ) - head_tree = ( - None - if head.startswith("worktree:") - else _git(execution_root, "rev-parse", f"{head}^{{tree}}").strip() - ) - target_identity = _task_review_target_identity(task, head, head_tree) - previous_review: dict[str, Any] | None = None - repair_frontier: dict[str, Any] | None = None - if accepted is None: - previous_review, repair_frontier = _stored_task_repair_preparation( - root, task, base=base, repaired_identity=target_identity - ) - review_mode = "repair" if repair_frontier is not None else "initial" - validated = validate_executor_result_for_task( - handoff, - task, - observe=True, - preparing_review=True, - repair_review_preparation=review_mode == "repair", - review_repair_frontier=repair_frontier, - **_observation_kwargs(args), - ) - if repair_frontier is not None: - base_tree = _git(execution_root, "rev-parse", f"{base}^{{tree}}").strip() - if head.startswith("worktree:"): - raise SystemExit("review-blocked: repair review requires a committed repaired identity") - head_tree = _git(execution_root, "rev-parse", f"{head}^{{tree}}").strip() - if repair_frontier["previous_reviewed_identity"]["source_tree"] != base_tree: - raise SystemExit("review-blocked: repair base does not match previous reviewed identity") - if repair_frontier["repaired_identity"]["source_tree"] != head_tree: - raise SystemExit("review-blocked: repair head does not match repaired identity") - omitted_diff_bytes = 0 - 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)}) - ) or "unknown" - if head.startswith("worktree:"): - raise SystemExit( - "review-blocked: oversized uncommitted task-local diff has no immutable " - f"source identity (oversized paths: {oversized})" - ) - omitted_diff_bytes = len(diff.encode("utf-8")) - base_tree = _git(execution_root, "rev-parse", f"{base}^{{tree}}").strip() - head_tree = _git(execution_root, "rev-parse", f"{head}^{{tree}}").strip() - diff_digest = hashlib.sha256(diff.encode("utf-8")).hexdigest() - changed_paths = sorted( - {path for line in name_status for path in _paths_from_name_status(line)} - ) - diff = "\n".join( - [ - "Exact source diff reference (content omitted from this bounded packet).", - f"Base commit: {base}", - f"Base tree: {base_tree}", - f"Head commit: {head}", - f"Head tree: {head_tree}", - f"Task-local diff SHA-256: {diff_digest}", - "Changed paths:", - *[f"- {path}" for path in changed_paths], - "Review the exact Git diff between these commits, restricted to the changed paths above.", - ] - ) - else: - diff = _redact_diff(diff) - changes = handoff.get("changes") if isinstance(handoff.get("changes"), dict) else {} - handoff_files = [item for item in _as_list(changes.get("files")) if isinstance(item, dict)] - symbols = sorted( - {str(symbol) for item in handoff_files for symbol in _as_list(item.get("symbols")) if symbol} - ) - validation = handoff.get("validation") if isinstance(handoff.get("validation"), dict) else {} - validation_commands = [item for item in _as_list(validation.get("commands")) if isinstance(item, dict)] - compiled_validation = { - str(item.get("command") or ""): item - for item in _as_list(task.get("validation")) - if isinstance(item, dict) - } - normalized_validation = [] - if accepted is not None: - observation_ids = list(getattr(args, "validation_observation_id", None) or []) - if not observation_ids: - raise SystemExit( - "review-blocked: accepted-task source repair requires explicit current validation observations" - ) - try: - repository_evidence = capture_repository_evidence(execution_root) - except RuntimeError as error: - raise SystemExit(f"review-blocked: repository identity is unavailable: {error}") from error - if repository_evidence.get("head") != head: - raise SystemExit("review-blocked: validation observations require the exact clean review head") - try: - normalized_validation = _claim_bound_validation_observations( - binding, task, repository_evidence, observation_ids - ) - except SystemExit as error: - raise SystemExit(f"review-blocked: {error}") from error - else: - for position, item in enumerate(validation_commands, start=1): - compiled = compiled_validation.get(str(item.get("command") or ""), {}) - normalized_validation.append({**item, "id": item.get("id") or compiled.get("id") or f"validation-{position:03d}"}) - evidence_projection = project_validation_evidence( - normalized_validation, - evidence_capability=task.get("evidence_capability") if isinstance(task.get("evidence_capability"), dict) else {}, - observed=(normalized_validation if accepted is not None else validated.get("observed_validation")), - expansion_reason=("failed_validation" if any(item.get("result") == "failed" for item in normalized_validation) else None), - ) - unresolved = _as_list(handoff.get("unresolved")) - evidence = { - "changed_files": name_status, - "changed_symbols": symbols, - "validation": evidence_projection, - "unresolved": unresolved, - } - _assert_no_credential_values(evidence, "review evidence") - - candidate = build_product_review_candidate( - task=task, - base=base, - head=head, - diff=diff, - changed_files=name_status, - changed_symbols=symbols, - validation_observations=evidence_projection, - repair_context=repair_frontier, - unresolved=unresolved, - ) - authority = candidate["task_authority"] - source = candidate["source"] - required = [ - f"Goal: {authority.get('goal')}", *authority.get("requirements", []), - *authority.get("constraints", []), *authority.get("accepted_boundaries", []), - ] - interfaces = authority.get("interfaces", {}) - if isinstance(interfaces, dict): - required.extend(_as_list(interfaces.get("consumes"))) - required.extend(_as_list(interfaces.get("produces"))) - allowed_scope = list(dict.fromkeys([*authority.get("files", {}).get("write", []), *authority.get("files", {}).get("read", [])])) - lines = [ - "# Task Review Package", - "", - f"Task: {task_id}", - f"Base: {source['base']}", - f"Head: {source['head']}", - f"Review mode: {review_mode}", - "", - "## Required behavior", - *_markdown_items(required), - "", - "## Allowed scope", - *_markdown_items(allowed_scope), - "", - "## Changed files", - *_markdown_items(source["changed_files"]), - "", - "## Validation reported", - *_markdown_items(candidate["validation_observations"]), - "", - "## Product repair context", - *_markdown_items( - [candidate["repair_context"]] if candidate["repair_context"] is not None else [] - ), - "", - "## Unresolved product concerns", - *_markdown_items(candidate["unresolved"]), - "", - "## Diff", - "```diff", - source["diff"].rstrip(), - "```", - ] - if out_of_scope: - lines.extend( - [ - "", - "## Out-of-scope changes", - *_markdown_items(out_of_scope), - ] - ) - lines.extend( - [ - "", - "## Review rubric", - "1. Required behavior is satisfied.", - "2. Listed out-of-scope diagnostics are expected sibling or prior changes, not a defect in this task.", - "3. Accepted product requirements, exact product source/diff, and test oracle agree.", - "4. Validation observations are sufficient and task-scoped.", - "5. Correctness, edge cases, compatibility, and code quality have no blocking defect.", - ] - ) - package = "\n".join(lines).rstrip() + "\n" - if out_of_scope and "## Out-of-scope changes" not in package: - raise SystemExit("Review package omitted required out-of-scope changes section") - _assert_no_credential_values(package, "review package") - review_target = target.with_name("review-package.md") - review_target.parent.mkdir(parents=True, exist_ok=True) - review_target.write_text(package, encoding="utf-8") - if accepted is None: - preparation = { - "review_mode": review_mode, - "review_target_kind": "task", - "repair_frontier": repair_frontier, - "review_reset": None, - "target_identity": target_identity, - **({"previous_review": previous_review} if previous_review is not None else {}), - } - review_target.with_name("review-preparation.json").write_text( - json.dumps(preparation, indent=2, sort_keys=True) + "\n", encoding="utf-8" - ) - metrics = compiled_context_metrics( - task, - review_package=package, - evidence_projection=evidence_projection, - omitted_by_reference_bytes=omitted_diff_bytes, - expansion_reason=next( - (item.get("expansion_reason") for item in evidence_projection if item.get("expansion_reason")), - None, - ), - ) - review_target.with_name("review-package-metrics.json").write_text( - json.dumps({"compiled_context_metrics": metrics}, indent=2, sort_keys=True) + "\n", - encoding="utf-8", - ) - return review_target + + + + + + + + + def cmd_build_task_brief(args: argparse.Namespace) -> None: @@ -5819,101 +1803,14 @@ def cmd_build_task_brief(args: argparse.Namespace) -> None: print(target.relative_to(resolve_workspace_root(args)).as_posix()) -def cmd_build_review_package(args: argparse.Namespace) -> None: - target = build_review_package(args) - print(target.relative_to(resolve_workspace_root(args)).as_posix()) -def cmd_observe_task_validation(args: argparse.Namespace) -> None: - _, brief_document = _compile_task_brief(args) - task = brief_document["task_brief"] - from bounded_closure import require_orchestration_admission, resolve_working_workspace - authority = resolve_working_workspace(resolve_workspace_root(args)) - if authority is not None: - require_orchestration_admission( - authority, - operation="reconciliation", - flow_id=str(task["plan_id"]), - ) - for item in task["validation"]: - policy = _completion_provenance_module().validation_reuse_policy(item) - if policy["max_age_seconds"] == 0: - raise SystemExit( - "non-reusable validation must use validate-executor-result so its " - "single observation is captured by initial acceptance" - ) - runtime = _observation_kwargs(args) - observed = _observe_completed_validation( - {}, - task, - task["validation"], - None, - **{ - key: runtime[key] - for key in ( - "workspace_id", - "execution_id", - "repository_id", - "execution_runtime_root", - "accepted_dependency_deltas", - ) - }, - ) - print(json.dumps({"validation": observed}, sort_keys=True)) -def cmd_validate_executor_result(args: argparse.Namespace) -> None: - if not args.handoff: - raise SystemExit("validate-executor-result requires --handoff") - _, brief_document = _compile_task_brief(args) - root = resolve_workspace_root(args) - task = brief_document["task_brief"] - handoff_root = root / ".work-bundle/orchestration/handoff" - handoff_path = _input_path(args.handoff, root, handoff_root, "handoff") - handoff, _ = _read_structured(handoff_path) - validated = validate_executor_result_for_task( - handoff, task, observe=True, **_observation_kwargs(args) - ) - if validated.get("result_state") == "completed" and task.get("review_required") is not True: - materialize_accepted_task_result(root, task, handoff, validated) - print(handoff_path.relative_to(root).as_posix()) -def cmd_create_accepted_base_absence_receipt(args: argparse.Namespace) -> None: - root = resolve_workspace_root(args) - reference = create_accepted_base_absence_receipt( - root, - str(args.plan_id), - str(args.task_id), - str(args.expected_head), - str(args.expected_tree), - str(args.proposed_handoff_id), - str(args.proposed_review_id), - str(args.final_head), - str(args.final_tree), - ) - print(json.dumps(reference, sort_keys=True)) -def cmd_adopt_existing_recovered_result(args: argparse.Namespace) -> None: - root = resolve_workspace_root(args) - reference = adopt_existing_recovered_result( - root, - str(args.plan_id), - str(args.task_id), - str(args.expected_head), - str(args.expected_tree), - str(args.handoff_id), - str(args.handoff_sha256), - str(args.review_id), - str(args.final_head), - str(args.final_tree), - { - "receipt_id": str(args.prior_receipt_id), - "receipt_sha256": str(args.prior_receipt_sha256), - }, - ) - print(json.dumps(reference, sort_keys=True)) def _observation_kwargs(args: argparse.Namespace) -> dict[str, Any]: @@ -5952,3 +1849,103 @@ def _maybe_bind_execution_from_args(args: argparse.Namespace, brief: dict[str, A forbidden_scope=[str(path) for path in _as_list((brief.get("files") or {}).get("forbidden"))], ) capture_task_baseline_once(binding, control_root) + + +def build_implementation_review_candidate( + *, + source_root: Path, + kind: str, + base_commit: str, + changed_paths: Iterable[str], +) -> dict[str, Any]: + """Freeze an exact commit or worktree manifest without requiring clean HEAD. + + This function validates identity and scope facts only. It does not judge the + implementation, its tests, or its acceptability. + """ + + root = source_root.expanduser().resolve() + if kind not in {"commit", "worktree"}: + raise SystemExit(f"Unsupported implementation candidate kind: {kind}") + if re.fullmatch(r"[0-9a-f]{40}", base_commit) is None: + raise SystemExit("Implementation candidate requires a 40-character base commit") + resolved = subprocess.run( + ["git", "-C", str(root), "rev-parse", f"{base_commit}^{{commit}}"], + capture_output=True, text=True, check=False, + ) + if resolved.returncode or resolved.stdout.strip() != base_commit: + raise SystemExit("Implementation candidate base commit is not a canonical Git commit") + normalized: list[str] = [] + for raw in changed_paths: + path = Path(str(raw)) + if path.is_absolute() or not path.parts or any(part in {"", ".", ".."} for part in path.parts): + raise SystemExit(f"Implementation candidate path is not canonical: {raw}") + value = path.as_posix() + if value in normalized: + raise SystemExit(f"Implementation candidate path is duplicated: {value}") + normalized.append(value) + manifest: list[dict[str, str]] = [] + for relative in sorted(normalized): + if kind == "commit": + observed = subprocess.run( + ["git", "-C", str(root), "show", f"{base_commit}:{relative}"], + capture_output=True, check=False, + ) + if observed.returncode: + raise SystemExit(f"Implementation candidate commit path is unavailable: {relative}") + content = observed.stdout + entry = { + "path": relative, + "state": "present", + "sha256": hashlib.sha256(content).hexdigest(), + } + else: + worktree_path = root / relative + path = worktree_path.resolve(strict=False) + if root not in path.parents or worktree_path.is_symlink(): + raise SystemExit(f"Implementation candidate path is unavailable: {relative}") + if worktree_path.is_file(): + content = worktree_path.read_bytes() + entry = { + "path": relative, + "state": "present", + "sha256": hashlib.sha256(content).hexdigest(), + } + elif worktree_path.exists(): + raise SystemExit(f"Implementation candidate path is unavailable: {relative}") + else: + base_type = subprocess.run( + ["git", "-C", str(root), "cat-file", "-t", f"{base_commit}:{relative}"], + capture_output=True, + text=True, + check=False, + ) + if base_type.returncode or base_type.stdout.strip() != "blob": + raise SystemExit(f"Implementation candidate path is unavailable: {relative}") + entry = {"path": relative, "state": "deleted"} + manifest.append(entry) + manifest_bytes = "".join( + ( + f"present {item['sha256']} {item['path']}\n" + if item["state"] == "present" + else f"deleted - {item['path']}\n" + ) + for item in manifest + ).encode("utf-8") + return { + "kind": kind, + "base_commit": base_commit, + "manifest": manifest, + "sha256": hashlib.sha256(manifest_bytes).hexdigest(), + } + + +def cmd_build_implementation_review_candidate(args: argparse.Namespace) -> None: + source_root = Path(str(args.source_root)).expanduser().resolve() + candidate = build_implementation_review_candidate( + source_root=source_root, + kind=str(args.kind), + base_commit=str(args.base_commit), + changed_paths=getattr(args, "changed_path", []), + ) + print(json.dumps(candidate, ensure_ascii=False, sort_keys=True)) diff --git a/scripts/orchestration/handoffs.py b/scripts/orchestration/handoffs.py index e42f3ea..28d1239 100644 --- a/scripts/orchestration/handoffs.py +++ b/scripts/orchestration/handoffs.py @@ -1,524 +1,155 @@ -import hashlib -import os -import tempfile - -from core import * -from execution_context import explicit_handoff_plan_identities -from specs import replace_front_matter_value - -HANDOFF_EXTENSIONS = (".md", ".yaml", ".yml") -LIFECYCLE_AUTHORITY = "location-v1" -LEGACY_OVERRIDE_KEYS = { - "handoff_id", "sha256", "type", "related_plan", "related_task", "status" +"""Current schema-owned executor-result storage. + +Executor results are factual continuation records. They do not contain or +infer product-review verdicts. +""" +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import Any + +from core import now_date, resolve_workspace_root +from artifact_store import ( + canonical_artifact_path, + family_policy, + load_catalog, + read_yaml_mapping, + rebuild_index, + transition_artifact, + write_artifact, +) + + +CATALOG_PATH = ( + Path(__file__).resolve().parents[2] + / "references/assets/orchestration/contract/artifact-family-catalog-v4.yaml" +) +FAMILY = "executor-result" +STRUCTURAL_FIELDS = { + "artifact_type", "schema_version", "id", "plan_id", "phase_id", "task_id", + "date_created", "last_updated", +} +FORBIDDEN_SEMANTIC_FIELDS = { + "verdict", "review", "review_receipt", "publication", "accepted_result", + "final_audit", "recommended_repair", "knowledge_write_authorization", } -HANDOFF_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$") -def _handoff_paths(root: Path) -> list[Path]: - return sorted( - path - for path in root.glob("*/*/*") - if path.is_file() and path.suffix in HANDOFF_EXTENSIONS - ) +def _anchors(args: argparse.Namespace) -> dict[str, Path]: + return {"workspace_root": resolve_workspace_root(args)} + + +def _policy() -> dict[str, Any]: + return family_policy(load_catalog(CATALOG_PATH), FAMILY) def _read_compact_yaml_metadata(path: Path) -> dict[str, object]: - data: dict[str, object] = {} - related: dict[str, object] = {} - current: str | None = None - for line in path.read_text(encoding="utf-8").splitlines(): - stripped = line.strip() - if not stripped or stripped.startswith("#"): - continue - if line.startswith((" ", "-")): - if current == "related" and ":" in stripped and not stripped.startswith("-"): - key, value = stripped.split(":", 1) - related[key.strip()] = value.strip() or None - continue - if ":" not in line: - continue - key, value = line.split(":", 1) - key = key.strip() - value = value.strip() - current = key - if value: - data[key] = value.strip("'\"") - elif key == "related": - data[key] = related - if related: - data["related"] = related - return data + """Maintained YAML parsing retained for non-current read-only callers.""" + + return read_yaml_mapping(path) -def _read_handoff_metadata(path: Path) -> dict[str, object]: - if path.suffix == ".md": - fm, _ = read_front_matter(path) - return fm - return _read_compact_yaml_metadata(path) - - -def _related_value(metadata: dict[str, object], flat_key: str, nested_key: str) -> object: - if flat_key in metadata: - return metadata.get(flat_key) - related = metadata.get("related") - if isinstance(related, dict): - return related.get(nested_key) - return None - - -def _handoff_sequence_id(root: Path, prefix: str) -> str: - date = now_date().replace("-", "") - numbers: list[int] = [] - pattern = re.compile(rf"^{re.escape(prefix)}-{date}-(\d+)") - for path in root.glob(f"**/{prefix}-{date}-*"): - if path.suffix not in HANDOFF_EXTENSIONS: - continue - match = pattern.match(path.stem) - if match: - numbers.append(int(match.group(1))) - return f"{prefix}-{date}-{(max(numbers) if numbers else 0) + 1:03d}" - - -def _handoff_identity_text(value: object) -> str | None: - if value is None: - return None - text = str(value).strip() - if not text or text.lower() in {"null", "none", "~"}: - return None - return text - - -def _task_scoped_related(existing: dict[str, object]) -> bool: - related = existing.get("related") if isinstance(existing.get("related"), dict) else {} - return bool(_handoff_identity_text(related.get("task")) or _handoff_identity_text(existing.get("related_task"))) - - -def _explicit_related_identities( - metadata: dict[str, object], flat_key: str, nested_key: str -) -> list[str]: - related = metadata.get("related") if isinstance(metadata.get("related"), dict) else {} - identities: list[str] = [] - for raw in (related.get(nested_key), metadata.get(flat_key)): - value = _handoff_identity_text(raw) - if value and value not in identities: - identities.append(value) - return identities - - -def _fill_missing_related_plan(content: str, plan_id: str) -> str: - lines = content.splitlines() - for index, line in enumerate(lines): - stripped = line.strip() - if not stripped.startswith("related:"): - continue - rest = stripped[len("related:") :].strip() - if rest.startswith("{") and rest.endswith("}"): - inner = rest[1:-1].strip() - lines[index] = f"related: {{plan: {plan_id}, {inner}}}" if inner else f"related: {{plan: {plan_id}}}" - return "\n".join(lines).rstrip() + "\n" - if not rest: - lines.insert(index + 1, f" plan: {plan_id}") - return "\n".join(lines).rstrip() + "\n" - raise SystemExit("Handoff plan identity missing: expected an explicit related.plan") - - -def _reconcile_task_handoff_plan(content: str, existing: dict[str, object], fields: dict[str, object]) -> str: - if not _task_scoped_related(existing): - return content - identities = explicit_handoff_plan_identities(existing) - arg_plan = _handoff_identity_text(fields.get("related_plan")) - if len(identities) > 1: - raise SystemExit(f"Handoff plan identity conflict: {' vs '.join(identities)}") - if len(identities) == 1: - if arg_plan and identities[0] != arg_plan: - raise SystemExit(f"Handoff plan mismatch: expected {arg_plan}, got {identities[0]}") - return content - if not arg_plan: - raise SystemExit("Handoff plan identity missing: expected an explicit related.plan") - return _fill_missing_related_plan(content, arg_plan) - - -def _ensure_yaml_metadata(content: str, fields: dict[str, object]) -> str: - existing = _read_compact_yaml_metadata_from_text(content) - lines: list[str] = [] - for key in ( - "id", "type", "status", "lifecycle_authority", "project", "created_at", "updated_at" - ): - if key not in existing: - lines.append(f"{key}: {fields[key]}") - if "related" not in existing: - lines.extend( - [ - "related:", - f" spec: {fields['related_spec']}", - f" plan: {fields['related_plan']}", - f" phase: {fields['related_phase']}", - f" task: {fields['related_task']}", - ] +def _semantic_input(path: Path) -> dict[str, Any]: + data = read_yaml_mapping(path) + overrides = sorted(STRUCTURAL_FIELDS.intersection(data)) + forbidden = sorted(FORBIDDEN_SEMANTIC_FIELDS.intersection(data)) + if overrides: + raise SystemExit( + "Executor-result semantic input contains structural field override: " + + ", ".join(overrides) + ) + if forbidden: + raise SystemExit( + "Executor-result semantic input contains forbidden field: " + + ", ".join(forbidden) ) - else: - content = _reconcile_task_handoff_plan(content, existing, fields) - if not lines: - return content - return "\n".join(lines) + "\n\n" + content.strip() + "\n" - - -def _read_compact_yaml_metadata_from_text(text: str) -> dict[str, object]: - data: dict[str, object] = {} - related: dict[str, object] = {} - current: str | None = None - for line in text.splitlines(): - stripped = line.strip() - if not stripped or stripped.startswith("#"): - continue - if line.startswith((" ", "-")): - if current == "related" and ":" in stripped and not stripped.startswith("-"): - key, value = stripped.split(":", 1) - related[key.strip()] = value.strip() or None - continue - if ":" not in line: - continue - key, value = line.split(":", 1) - key = key.strip() - value = value.strip() - current = key - if value: - data[key] = value.strip("'\"") - elif key == "related": - data[key] = related - if related: - data["related"] = related return data -def _replace_yaml_top_level_value(path: Path, key: str, value: str) -> None: - lines = path.read_text(encoding="utf-8").splitlines() - for index, line in enumerate(lines): - if line.startswith(f"{key}:"): - lines[index] = f"{key}: {value}" - break - else: - lines.insert(0, f"{key}: {value}") - path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8") - - -def _status_location(path: Path, root: Path) -> tuple[str, str]: - try: - folder, status, _name = path.resolve().relative_to(root.resolve()).parts - except (ValueError, TypeError) as error: - raise SystemExit(f"Handoff path is outside the lifecycle store: {path}") from error - if status not in HANDOFF_STATUSES: - raise SystemExit(f"Invalid handoff status location: {status}") - return folder, status - - -def _legacy_overrides(root: Path) -> dict[str, tuple[Path, dict[str, object]]]: - override_root = root / "legacy-status-overrides" - records: dict[str, tuple[Path, dict[str, object]]] = {} - for path in sorted(override_root.glob("*.json")): - try: - value = json.loads(path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError) as error: - raise SystemExit(f"Invalid legacy handoff status override: {path}") from error - if not isinstance(value, dict) or set(value) != LEGACY_OVERRIDE_KEYS: - raise SystemExit(f"Invalid legacy handoff status override shape: {path}") - handoff_id = str(value.get("handoff_id") or "") - if not handoff_id or path.stem != handoff_id: - raise SystemExit(f"Legacy handoff status override identity mismatch: {path}") - if handoff_id in records: - raise SystemExit(f"Duplicate legacy handoff status override identity: {handoff_id}") - records[handoff_id] = (path, value) - return records - - -def _resolved_handoff_status( - path: Path, - metadata: dict[str, object], - location_status: str, - override: tuple[Path, dict[str, object]] | None, -) -> str: - marked = metadata.get("lifecycle_authority") == LIFECYCLE_AUTHORITY - if marked: - if override is not None: - raise SystemExit( - f"Marked handoff has an invalid legacy status override: {metadata.get('id', path.stem)}" - ) - return location_status - if metadata.get("lifecycle_authority") not in (None, ""): - raise SystemExit(f"Invalid handoff lifecycle authority: {metadata.get('lifecycle_authority')}") - if override is not None: - _override_path, record = override - related_plan = _related_value(metadata, "related_plan", "plan") - related_task = _related_value(metadata, "related_task", "task") - digest = hashlib.sha256(path.read_bytes()).hexdigest() - if ( - record.get("sha256") != digest - or record.get("type") != metadata.get("type") - or record.get("related_plan") != related_plan - or record.get("related_task") != related_task - or record.get("status") not in HANDOFF_STATUSES - or record.get("status") != location_status - ): - raise SystemExit( - f"Legacy handoff status override contradicts digest, binding, or location: {record.get('handoff_id')}" - ) - return str(record["status"]) - if location_status != "active": - return location_status - embedded = str(metadata.get("status") or "active") - if embedded not in HANDOFF_STATUSES: - raise SystemExit(f"Invalid legacy embedded handoff status: {embedded}") - return embedded - - -def _collect_handoff_rows(args: argparse.Namespace) -> list[dict[str, object]]: - root = orchestration_root(args) / "handoff" - overrides = _legacy_overrides(root) - rows: list[dict[str, object]] = [] - seen: dict[str, list[tuple[Path, dict[str, object], str, str]]] = {} - for path in _handoff_paths(root): - metadata = _read_handoff_metadata(path) - if not metadata: - continue - if metadata.get("lifecycle_authority") == LIFECYCLE_AUTHORITY and not metadata.get("id"): - raise SystemExit(f"Marked handoff is missing identity: {path}") - handoff_id = str(metadata.get("id") or path.stem) - folder, location_status = _status_location(path, root) - handoff_type = str(metadata.get("type") or "") - expected_folder = "orchestration" if handoff_type == "orchestration" else "executor" - if handoff_type not in HANDOFF_TYPES or folder != expected_folder: - raise SystemExit(f"Handoff type/folder disagreement: {handoff_id}") - plan_identities = explicit_handoff_plan_identities(metadata) - if len(plan_identities) > 1: - raise SystemExit(f"Handoff plan identity conflict: {' vs '.join(plan_identities)}") - task_identities = _explicit_related_identities(metadata, "related_task", "task") - if len(task_identities) > 1: - raise SystemExit(f"Handoff task identity conflict: {' vs '.join(task_identities)}") - prior = seen.setdefault(handoff_id, []) - if prior: - candidates = [*prior, (path, metadata, folder, location_status)] - colocated_unmarked_legacy = ( - handoff_id not in overrides - and len({(item[2], item[3]) for item in candidates}) == 1 - and all( - item[1].get("lifecycle_authority") in (None, "") - for item in candidates - ) - ) - if not colocated_unmarked_legacy: - raise SystemExit( - f"Duplicate handoff identity across lifecycle locations: {handoff_id}" - ) - prior.append((path, metadata, folder, location_status)) - current_status = _resolved_handoff_status( - path, metadata, location_status, overrides.get(handoff_id) +def _bindings(args: argparse.Namespace) -> dict[str, str]: + return {"plan": str(args.plan_id), "task": str(args.task_id)} + + +def _assert_identity_available(args: argparse.Namespace) -> None: + policy = _policy() + for state in policy["lifecycle"]["states"]: + target = canonical_artifact_path( + policy, + _anchors(args), + identity=str(args.id), + state=str(state), + bindings=_bindings(args), ) - rows.append({"id": handoff_id, "type": handoff_type, "status": current_status, "path": rel(path, args), "project": metadata.get("project", ""), "created_at": metadata.get("created_at", ""), "updated_at": metadata.get("updated_at", ""), "related_spec": _related_value(metadata, "related_spec", "spec"), "related_plan": _related_value(metadata, "related_plan", "plan"), "related_phase": _related_value(metadata, "related_phase", "phase"), "related_task": _related_value(metadata, "related_task", "task")}) - orphans = sorted(set(overrides) - set(seen)) - if orphans: - raise SystemExit(f"Legacy handoff status override has no handoff: {', '.join(orphans)}") - return rows - - -def index_handoffs(args: argparse.Namespace) -> list[dict[str, object]]: - root = orchestration_root(args) / "handoff" - rows = _collect_handoff_rows(args) - _atomic_text( - root / "index.jsonl", - "\n".join(json.dumps(row, ensure_ascii=False) for row in rows), + if target.exists(): + raise SystemExit(f"Executor-result canonical identity collision: {args.id}") + + +def write_executor_result(args: argparse.Namespace) -> dict[str, Any]: + semantic = _semantic_input(Path(str(args.content_file))) + _assert_identity_available(args) + today = now_date() + data = { + **semantic, + "artifact_type": FAMILY, + "schema_version": 1, + "id": str(args.id), + "plan_id": str(args.plan_id), + "phase_id": getattr(args, "phase_id", None), + "task_id": str(args.task_id), + "date_created": today, + "last_updated": today, + } + return write_artifact( + CATALOG_PATH, + FAMILY, + _anchors(args), + data, + state="active", + bindings=_bindings(args), ) - return rows - - -def _atomic_text(path: Path, content: str) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - descriptor, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent) - try: - with os.fdopen(descriptor, "w", encoding="utf-8") as stream: - stream.write(content if not content or content.endswith("\n") else content + "\n") - os.replace(temporary, path) - except BaseException: - try: - os.unlink(temporary) - except FileNotFoundError: - pass - raise - - -def _managed_creation_admission(args: argparse.Namespace, content: str) -> None: - root = resolve_workspace_root(args) - if not (root / ".work-bundle/project.yaml").is_file(): - raise SystemExit( - "Executor-result creation requires a managed WorkBundle workspace" - ) - from artifact_inputs import parse_yaml_subset - from execution_context import _compile_task_brief, validate_executor_result_creation_for_task - - handoff = parse_yaml_subset(content) - related = handoff.get("related") if isinstance(handoff.get("related"), dict) else {} - task_id = str(related.get("task") or handoff.get("related_task") or "") - plan_id = str(related.get("plan") or handoff.get("related_plan") or "") - if not task_id or not plan_id: - raise SystemExit("Managed executor-result creation requires related.plan and related.task") - plan_root = root / ".work-bundle/orchestration/plan" - candidates = [ - path - for status in ("active", "archived") - for path in (plan_root / status).glob("**/*.md") - if path.name.startswith("task-") + + +def _index_rows(args: argparse.Namespace) -> list[dict[str, Any]]: + result = rebuild_index(CATALOG_PATH, FAMILY, _anchors(args)) + path = Path(str(result["path"])) + return [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line] + + +def list_executor_results(args: argparse.Namespace) -> list[dict[str, Any]]: + rows = _index_rows(args) + plan_id = getattr(args, "plan_id", None) + task_id = getattr(args, "task_id", None) + return [ + row for row in rows + if (not plan_id or row.get("plan_id") == plan_id) + and (not task_id or row.get("task_id") == task_id) ] - matches: list[Path] = [] - from artifact_inputs import _read_structured - for path in candidates: - task, _body = _read_structured(path) - if str(task.get("id") or "") == task_id and str(task.get("plan_id") or "") == plan_id: - matches.append(path) - if len(matches) != 1: - raise SystemExit(f"Expected one task {plan_id}/{task_id} for handoff creation; found {len(matches)}") - compile_args = argparse.Namespace( - project_root=getattr(args, "project_root", None), - workspace_id=getattr(args, "workspace_id", None), - execution_id=getattr(args, "execution_id", None), - repository_id=getattr(args, "repository_id", None), - execution_runtime_root=getattr(args, "execution_runtime_root", None), - task=str(matches[0]), handoff=None, base=None, head=None, + + +def cmd_write_executor_result(args: argparse.Namespace) -> None: + print(json.dumps(write_executor_result(args), ensure_ascii=False, sort_keys=True)) + + +def cmd_list_executor_results(args: argparse.Namespace) -> None: + for row in list_executor_results(args): + print(json.dumps(row, ensure_ascii=False, sort_keys=True)) + + +def cmd_transition_executor_result(args: argparse.Namespace) -> None: + result = transition_artifact( + CATALOG_PATH, + FAMILY, + _anchors(args), + identity=str(args.id), + current_state=str(args.current_state), + target_state=str(args.target_state), + bindings=_bindings(args), ) - _path, document = _compile_task_brief(compile_args) - validate_executor_result_creation_for_task(handoff, document["task_brief"]) - - -def _assert_new_executor_metadata( - content: str, *, handoff_id: str, handoff_type: str, status: str -) -> None: - metadata = _read_compact_yaml_metadata_from_text(content) - expected = { - "id": handoff_id, - "type": handoff_type, - "status": status, - "lifecycle_authority": LIFECYCLE_AUTHORITY, - } - for field, value in expected.items(): - if metadata.get(field) != value: - raise SystemExit( - f"Handoff creation metadata mismatch for {field}: " - f"expected {value}, got {metadata.get(field) or 'missing'}" - ) - - -def cmd_write_handoff(args: argparse.Namespace) -> None: - init_dirs(args) - if args.type not in HANDOFF_TYPES: - raise SystemExit(f"Invalid handoff type: {args.type}") - if args.status not in HANDOFF_STATUSES: - raise SystemExit(f"Invalid handoff status: {args.status}") - if args.type == "orchestration" and args.status != "archived": - raise SystemExit("Active orchestration handoff creation is retired; use executor-result handoffs.") - hprefix = "handoff-orch" if args.type == "orchestration" else "handoff-exec" - hid = args.id or _handoff_sequence_id(orchestration_root(args) / "handoff", hprefix) - if not HANDOFF_ID_RE.fullmatch(str(hid)): - raise SystemExit(f"Invalid handoff identity: {hid}") - folder = "orchestration" if args.type == "orchestration" else "executor" - content = Path(args.content_file).read_text(encoding="utf-8") - fields = {"id": hid, "type": args.type, "title": args.title, "status": args.status, "lifecycle_authority": LIFECYCLE_AUTHORITY, "project": project_root(args).name, "created_at": now_date(), "updated_at": now_date(), "related_spec": args.related_spec or "null", "related_plan": args.related_plan or "null", "related_phase": args.related_phase or "null", "related_task": args.related_task or "null"} - handoff_format = args.format or ("yaml" if args.type == "executor-result" else "markdown") - if handoff_format == "yaml" and args.type != "executor-result": - raise SystemExit("YAML handoff writing is only supported for executor-result handoffs.") - content = _ensure_yaml_metadata(content, fields) if handoff_format == "yaml" else ensure_front_matter(content, fields) - if args.type == "executor-result": - _assert_new_executor_metadata( - content, handoff_id=str(hid), handoff_type=args.type, status=args.status - ) - _managed_creation_admission(args, content) - rows = _collect_handoff_rows(args) - if any(row.get("id") == hid for row in rows): - raise SystemExit(f"Duplicate handoff identity across lifecycle locations: {hid}") - target_status_dir = args.status - suffix = ".yaml" if handoff_format == "yaml" else ".md" - target = orchestration_root(args) / "handoff" / folder / target_status_dir / f"{hid}-{slugify(args.title)}{suffix}" - if target.exists(): - raise SystemExit(f"Handoff lifecycle target already exists: {target}") - index_path = orchestration_root(args) / "handoff/index.jsonl" - previous_index = index_path.read_bytes() if index_path.is_file() else None - try: - _atomic_text(target, content) - index_handoffs(args) - except BaseException: - target.unlink(missing_ok=True) - if previous_index is None: - index_path.unlink(missing_ok=True) - else: - index_path.write_bytes(previous_index) - raise - print(rel(target, args)) - - -def cmd_index_handoffs(args: argparse.Namespace) -> None: - print(f"indexed {len(index_handoffs(args))} handoffs") - - -def cmd_list_handoffs(args: argparse.Namespace) -> None: - rows = index_handoffs(args) - for row in rows: - if args.status and row.get("status") != args.status: - continue - if args.type and row.get("type") != args.type: - continue - print(json.dumps(row, ensure_ascii=False)) - - -def cmd_set_handoff_status(args: argparse.Namespace) -> None: - if args.status not in HANDOFF_STATUSES: - raise SystemExit(f"Invalid handoff status: {args.status}") - if not HANDOFF_ID_RE.fullmatch(str(args.id)): - raise SystemExit(f"Invalid handoff identity: {args.id}") - rows = _collect_handoff_rows(args) - matches = [row for row in rows if row.get("id") == args.id] - if not matches: - raise SystemExit(f"Handoff not found: {args.id}") - if len(matches) != 1: - raise SystemExit(f"Handoff identity is ambiguous for lifecycle operation: {args.id}") - match = matches[0] - path = artifact_path_from_row(match, args) - if match.get("status") == args.status: - print(args.id) - return - metadata = _read_handoff_metadata(path) - folder = "orchestration" if match.get("type") == "orchestration" else "executor" - target = orchestration_root(args) / "handoff" / folder / args.status / path.name - if target.exists() and target.resolve() != path.resolve(): - raise SystemExit(f"Handoff lifecycle target already exists: {target}") - marked = metadata.get("lifecycle_authority") == LIFECYCLE_AUTHORITY - override_path = orchestration_root(args) / "handoff/legacy-status-overrides" / f"{args.id}.json" - old_override = override_path.read_bytes() if override_path.is_file() else None - index_path = orchestration_root(args) / "handoff/index.jsonl" - old_index = index_path.read_bytes() if index_path.is_file() else None - moved = target.resolve() != path.resolve() - try: - if moved: - target.parent.mkdir(parents=True, exist_ok=True) - os.replace(path, target) - if not marked: - record = { - "handoff_id": str(args.id), - "sha256": hashlib.sha256(target.read_bytes()).hexdigest(), - "type": metadata.get("type"), - "related_plan": _related_value(metadata, "related_plan", "plan"), - "related_task": _related_value(metadata, "related_task", "task"), - "status": args.status, - } - _atomic_text(override_path, json.dumps(record, sort_keys=True)) - index_handoffs(args) - except BaseException: - if moved and target.exists(): - path.parent.mkdir(parents=True, exist_ok=True) - os.replace(target, path) - if old_override is None: - override_path.unlink(missing_ok=True) - else: - override_path.write_bytes(old_override) - if old_index is None: - index_path.unlink(missing_ok=True) - else: - index_path.write_bytes(old_index) - raise - print(args.id) + rebuild_index(CATALOG_PATH, FAMILY, _anchors(args)) + print(json.dumps(result, ensure_ascii=False, sort_keys=True)) + + +def cmd_index_executor_results(args: argparse.Namespace) -> None: + print(json.dumps(rebuild_index(CATALOG_PATH, FAMILY, _anchors(args)), sort_keys=True)) diff --git a/scripts/orchestration/legacy_wor107_migration.py b/scripts/orchestration/legacy_wor107_migration.py deleted file mode 100644 index 9f83f64..0000000 --- a/scripts/orchestration/legacy_wor107_migration.py +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env python3 -"""Narrow compatibility validator for the historical WOR-107 stop boundary.""" - -from __future__ import annotations - -import argparse -import json -from pathlib import Path -from typing import Any, Mapping - -import yaml - - -ISSUE_ID = "WOR-107" -FAILURE_CODE = "WB_MIGRATION_STOP_BOUNDARY_INVALID" -DEPRECATION_DIAGNOSTIC = ( - "deprecated: assert-migration-stop is a legacy migration compatibility alias" -) - - -class LegacyMigrationBoundaryError(ValueError): - pass - - -def _read_document(path: Path) -> Mapping[str, Any]: - value = yaml.safe_load(path.read_text(encoding="utf-8")) - if not isinstance(value, Mapping): - raise LegacyMigrationBoundaryError("migration handoff must be a mapping") - return value - - -def cmd_assert_migration_stop(argv: list[str]) -> int: - parser = argparse.ArgumentParser(prog="legacy_wor107_migration.py") - parser.add_argument("--instance", type=Path, required=True) - parser.add_argument("--required-excluded", nargs="+", required=True) - parsed = parser.parse_args(argv) - try: - instance = _read_document(parsed.instance) - if instance.get("issue") != ISSUE_ID: - raise LegacyMigrationBoundaryError( - f"migration handoff issue must be {ISSUE_ID}" - ) - excluded = instance.get("excluded_work") - if not isinstance(excluded, list) or any( - not isinstance(item, str) for item in excluded - ): - raise LegacyMigrationBoundaryError( - "excluded_work must be a list of strings" - ) - missing = [item for item in parsed.required_excluded if item not in excluded] - if missing: - raise LegacyMigrationBoundaryError( - f"migration stop boundary missing exclusions: {', '.join(missing)}" - ) - except (OSError, yaml.YAMLError, LegacyMigrationBoundaryError) as error: - print( - json.dumps( - { - "status": "blocked", - "failure_code": FAILURE_CODE, - "detail": str(error), - }, - sort_keys=True, - ) - ) - return 1 - print( - json.dumps( - { - "status": "passed", - "issue": ISSUE_ID, - "excluded_work": parsed.required_excluded, - }, - sort_keys=True, - ) - ) - return 0 - - -def main(argv: list[str] | None = None) -> int: - return cmd_assert_migration_stop(argv or []) - - -if __name__ == "__main__": - import sys - - raise SystemExit(main(sys.argv[1:])) diff --git a/scripts/orchestration/plans.py b/scripts/orchestration/plans.py index 33adabb..9da42a1 100644 --- a/scripts/orchestration/plans.py +++ b/scripts/orchestration/plans.py @@ -1,771 +1,219 @@ +import json import subprocess -from datetime import datetime, timezone from core import * -from core import _member_roots from execution_context import ( - AcceptanceOwnershipError, - cmd_validate_executor_result, - evaluate_knowledge_closure_state, - read_structured_artifact, - unique_explicit_handoff_plan_id, - validate_executor_result_for_task, - _compile_task_brief, - _observation_kwargs, - _parse_scalar, - _execution_workspace_module, _iter_task_bindings, _persist_binding, - has_persisted_accepted_task_result, - load_task_execution_binding, - load_current_accepted_task_result, - semantic_digest, +) +from artifact_store import ( + canonical_artifact_path, + family_policy, + load_catalog, + read_artifact, + read_yaml_mapping, + rebuild_index, + validate_artifact, + write_artifact, ) from completion_provenance import ( CompletionProvenanceError, ManagedProvenanceStore, - load_observation, release_completion_binding, + validate_execution_binding_ownership, + validate_ownership_shape, ) -from handoffs import _read_compact_yaml_metadata -from repository_preflight import ( - _metadata_repository_entries, - capture_repository_evidence, - task_caused_paths, -) -from specs import load_index, replace_front_matter_value -from review_runtime import require_plan_reviews +from review_identity import canonical_plan_tree_identity, source_obligation_records -def _plan_knowledge_field(body: str, label: str) -> str | None: - section = re.search( - r"^##\s+(?:2\.1\s+)?Knowledge Base Update Carry Forward\s*$([\s\S]*?)(?=^##\s|\Z)", - body, - re.MULTILINE, - ) - if not section: - return None - rendered_label = re.escape(label) - match = re.search( - rf"^-\s+(?:\*\*{rendered_label}\*\*|{rendered_label}):[ \t]*([^\s]+)[ \t]*$", - section.group(1), - re.MULTILINE, - ) - return match.group(1) if match else None -def _assert_archive_knowledge_gate( - args: argparse.Namespace, - plan_id: str, - root_path: Path, - validated: list[tuple[dict[str, object], dict[str, object]]], -) -> None: - _, body = read_front_matter(root_path) - upstream = _plan_knowledge_field(body, "Disposition") - if upstream is None: - raise SystemExit("knowledge-blocked: plan has no Knowledge Base Update disposition") - closure_return = _plan_knowledge_field(body, "Closure return") or "missing" - legacy = [result for result, _brief in validated if "knowledge_disposition" not in result] - if legacy: - if upstream == "required" and closure_return == "completed": - return - raise SystemExit( - "knowledge-blocked: legacy accepted results require plan-level required/completed closure" - ) - handoffs = [ - { - "related": {"plan": plan_id, "task": result.get("task_id")}, - "result": {"state": "completed"}, - "acceptance_review": { - "required": brief.get("review_required") is True, - "verdict": "accept", - }, - "knowledge_disposition": result.get("knowledge_disposition"), - } - for result, brief in validated - ] - review_required_by_task = { - str(brief.get("task_id") or ""): brief.get("review_required") is True for _handoff, brief in validated - } - gate = evaluate_knowledge_closure_state( - upstream_disposition=upstream, - accepted_task_handoffs=handoffs, - closure_return=closure_return, - review_required_by_task=review_required_by_task, - ) - if gate["archive_blocked"]: - triggers = ", ".join(f"{item['task']}:{item['action']}" for item in gate["triggers"]) - detail = triggers or str(gate["disposition"]) - raise SystemExit(f"knowledge-blocked: archive requires resolved durable closure ({detail})") -def _plan_executor_handoffs(args: argparse.Namespace, plan_id: str) -> list[dict[str, object]]: - handoffs: list[dict[str, object]] = [] - handoff_root = orchestration_root(args) / "handoff" / "executor" - for path in sorted(handoff_root.glob("*/*")): - if not path.is_file() or path.suffix not in {".yaml", ".yml"}: - continue - compact = _read_compact_yaml_metadata(path) - if isinstance(compact.get("related"), str): - related = _parse_scalar(str(compact["related"])) - if isinstance(related, dict): - compact["related"] = related - compact_plan_id = unique_explicit_handoff_plan_id(compact) - if compact_plan_id is not None and compact_plan_id != plan_id: - continue - handoff = read_structured_artifact(path) - if unique_explicit_handoff_plan_id(handoff) != plan_id: - continue - handoffs.append(handoff) - return handoffs +CATALOG_PATH = Path(__file__).resolve().parents[2] / "references/assets/orchestration/contract/artifact-family-catalog-v5.yaml" +PLAN_FAMILIES = ("root-plan", "phase", "task") +PLAN_QUALIFICATION_STATUSES = {"draft", "verified", "superseded"} +PLAN_QUALIFICATION_TRANSITIONS = { + "draft": {"verified", "superseded"}, + "verified": {"superseded"}, + "superseded": set(), +} +PLANNED_STATUS = "planned" +PLAN_STRUCTURAL_INPUT_FIELDS = { + "artifact_type", "schema_version", "id", "goal", "purpose", "component", + "version", "source_spec_id", "status", "date_created", "last_updated", +} +PHASE_STRUCTURAL_INPUT_FIELDS = { + "artifact_type", "schema_version", "id", "plan_id", "name", "status", + "date_created", "last_updated", +} +TASK_STRUCTURAL_INPUT_FIELDS = { + "artifact_type", "schema_version", "id", "plan_id", "phase_id", "name", + "status", "date_created", "last_updated", +} -def _find_plan_task_path(args: argparse.Namespace, plan_id: str, task_id: str) -> Path | None: - matches = [ - row - for row in index_plans(args) - if row.get("type") == "task" and row.get("id") == task_id and row.get("plan_id") == plan_id - ] - if len(matches) != 1: - return None - return artifact_path_from_row(matches[0], args) - - -def _try_validate_task_handoff( - args: argparse.Namespace, plan_id: str, handoff: dict[str, object] -) -> tuple[dict[str, object], dict[str, object]] | None: - related = handoff.get("related") if isinstance(handoff.get("related"), dict) else {} - task_id = related.get("task") - if not task_id: - return None - task_path = _find_plan_task_path(args, plan_id, str(task_id)) - if task_path is None: - return None - compile_args = argparse.Namespace( - project_root=getattr(args, "project_root", None), - workspace_root=getattr(args, "workspace_root", None), - task=str(task_path), - handoff=None, - base=None, - head=None, - **_observation_kwargs(args), - ) - try: - _, brief_document = _compile_task_brief(compile_args) - brief = brief_document["task_brief"] - capability = brief.get("evidence_capability") if isinstance(brief.get("evidence_capability"), dict) else {} - validate_executor_result_for_task( - handoff, - brief, - observe=capability.get("result") == "mapped", - **_observation_kwargs(args), - ) - except AcceptanceOwnershipError: - raise - except SystemExit: - return None - return handoff, brief - - -def _validated_plan_task_handoffs( - args: argparse.Namespace, plan_id: str -) -> list[tuple[dict[str, object], dict[str, object]]]: - validated: list[tuple[dict[str, object], dict[str, object]]] = [] - for handoff in _plan_executor_handoffs(args, plan_id): - pair = _try_validate_task_handoff(args, plan_id, handoff) - if pair is not None: - validated.append(pair) - return validated - - -def _plan_section_table_parts(body: str, name: str) -> tuple[list[str], list[list[str]]]: - section = re.search( - rf"^##\s+(?:\d+(?:\.\d+)*\.?\s+)?{re.escape(name)}\s*$([\s\S]*?)(?=^##\s|\Z)", - body, - re.MULTILINE, - ) - if not section: - return [], [] - rows: list[list[str]] = [] - for line in section.group(1).splitlines(): - if not line.strip().startswith("|"): - continue - cells = [cell.strip() for cell in line.strip().strip("|").split("|")] - if not cells or all(re.fullmatch(r":?-{3,}:?", cell) for cell in cells): - continue - rows.append(cells) - return (rows[0], rows[1:]) if rows else ([], []) +def _plan_anchors(args: argparse.Namespace) -> dict[str, Path]: + return {"workspace_root": resolve_workspace_root(args)} -def _plan_section_table(body: str, name: str) -> list[list[str]]: - _header, rows = _plan_section_table_parts(body, name) - return rows +def _plan_policy(family: str) -> dict[str, object]: + return family_policy(load_catalog(CATALOG_PATH), family) -def _declared_integration_commands(body: str) -> list[str]: - commands: list[str] = [] - header, rows = _plan_section_table_parts(body, "Tests") - normalized = [re.sub(r"\s+", " ", cell.strip().lower()) for cell in header] - if "test type" not in normalized or "command" not in normalized: - return [] - test_type_index = normalized.index("test type") - command_index = normalized.index("command") - for cells in rows: - if max(test_type_index, command_index) >= len(cells): - continue - test_type = cells[test_type_index].lower() - if "integration" not in test_type or "unit|integration" in test_type: - continue - command = cells[command_index].strip().strip("`") - if command and command not in {"-", "[command if applicable]"}: - commands.append(command) - return commands +def _semantic_yaml(path: Path, forbidden: set[str], label: str) -> dict[str, object]: + data = read_yaml_mapping(path) + overrides = sorted(forbidden.intersection(data)) + if overrides: + raise SystemExit( + f"{label} semantic input contains structural field override: " + + ", ".join(overrides) + ) + return data -_MATERIAL_CHANGE_ACTIONS = {"created", "modified", "deleted"} -_MATERIAL_RESULT_STATES = {"completed", "partial"} +def _family_bindings(family: str, row: dict[str, object]) -> dict[str, str]: + if family == "root-plan": + return {"source_spec": str(row["source_spec_id"])} + if family == "phase": + return {"plan": str(row["plan_id"])} + return {"plan": str(row["plan_id"]), "phase": str(row["phase_id"])} -def _git_tree_id(root: Path, spec: str) -> str | None: - result = subprocess.run( - ["git", "-C", str(root), "rev-parse", "--verify", f"{spec}^{{tree}}"], - capture_output=True, - text=True, - check=False, +def _canonical_row(args: argparse.Namespace, family: str, row: dict[str, object]) -> dict[str, object]: + identity = str(row["id"]) + bindings = _family_bindings(family, row) + matches: list[tuple[str, Path]] = [] + for state in ("active", "archived"): + path = canonical_artifact_path( + _plan_policy(family), _plan_anchors(args), identity=identity, + state=state, bindings=bindings, + ) + if path.is_file(): + matches.append((state, path)) + if len(matches) != 1: + raise SystemExit( + f"Planning artifact identity must resolve to one canonical location: {family} {identity}" + ) + state, path = matches[0] + stored = read_artifact( + CATALOG_PATH, family, _plan_anchors(args), identity=identity, + state=state, bindings=bindings, + )["data"] + presentation = dict(stored) + presentation.update( + { + "type": "plan" if family == "root-plan" else family, + "title": stored.get("goal") or stored.get("name"), + "path": rel(path, args), + "state": state, + "created_at": stored["date_created"], + "updated_at": stored["last_updated"], + } ) - if result.returncode != 0: - return None - return result.stdout.strip() or None - - -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 {} - reviewed_head = str(review.get("reviewed_head") or "").strip() - if reviewed_head: - identities.append(reviewed_head) - repositories = handoff.get("repository") - if isinstance(repositories, dict): - repositories = [repositories] - if isinstance(repositories, list): - for repository in repositories: - if not isinstance(repository, dict): - continue - metadata = repository.get("metadata") if isinstance(repository.get("metadata"), dict) else {} - actual_commit = str(metadata.get("actual_commit") or "").strip() - if actual_commit: - identities.append(actual_commit) - return identities - - -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: - return tree - 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( + return presentation + + +def _index_rows(args: argparse.Namespace, family: str) -> list[dict[str, object]]: + result = rebuild_index(CATALOG_PATH, family, _plan_anchors(args)) + index_path = Path(str(result["path"])) + return [ + json.loads(line) + for line in index_path.read_text(encoding="utf-8").splitlines() + if line.strip() + ] + + +def _identity_collision( args: argparse.Namespace, - plan_id: str, - validated: list[tuple[dict[str, object], dict[str, object]]], - commands: list[str], -) -> Path: - entries: list[tuple[Path, str]] = [] - 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): - 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: - entries.append((Path(recorded).expanduser().resolve(), identity)) - handoff_has_provenance = True - if not handoff_has_provenance: - try: - fallback = _resolve_final_plan_workspace(args, plan_id) - except (OSError, 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 = _plan_task_order(args, 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: - 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): - 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: 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)) - raise SystemExit("acceptance-blocked: final plan repository is ambiguous") - - -def _acceptance_result_detail(results: set[str]) -> str: - if not results: - return "missing" - if len(results) > 1: - return "contradictory" - return next(iter(results)) - - -def _handoff_command_result(handoff: dict[str, object], command: str) -> str | None: - validation = handoff.get("validation") if isinstance(handoff.get("validation"), dict) else {} - items = validation.get("commands") if isinstance(validation.get("commands"), list) else [] - for item in items: - if not isinstance(item, dict): - continue - if str(item.get("command") or "").strip() == command: - return str(item.get("result") or "") - return None - - -def _handoff_has_material_changes(handoff: dict[str, object], brief: dict[str, object]) -> bool: - changes = handoff.get("changes") if isinstance(handoff.get("changes"), dict) else {} - items = changes.get("files") if isinstance(changes.get("files"), list) else [] - for item in items: - if isinstance(item, dict) and str(item.get("action") or "") in _MATERIAL_CHANGE_ACTIONS: - return True - result = handoff.get("result") if isinstance(handoff.get("result"), dict) else {} - if str(result.get("state") or "") not in _MATERIAL_RESULT_STATES: - return False - files = brief.get("files") if isinstance(brief.get("files"), dict) else {} - write = files.get("write") - return bool(write) if isinstance(write, list) else False - - -def _accepted_plan_repository_bindings( - args: argparse.Namespace, plan_id: str -) -> list[dict[str, object]]: - bindings: list[dict[str, object]] = [] - control_root = resolve_workspace_root(args) - for row in index_plans(args): - if row.get("type") != "task" or row.get("plan_id") != plan_id: - continue - task_id = str(row.get("id") or "") - if not has_persisted_accepted_task_result(control_root, plan_id, task_id): - continue - binding, _accepted = _load_current_task_acceptance( - args, artifact_path_from_row(row, args) - ) - bindings.append(binding) - return bindings + family: str, + identity: str, + *, + bindings: dict[str, str] | None = None, +) -> bool: + policy = _plan_policy(family) + return any( + canonical_artifact_path( + policy, + _plan_anchors(args), + identity=identity, + state=str(state), + bindings=bindings, + ).exists() + for state in policy["lifecycle"]["states"] + ) -def _registered_repository_roots(workspace: Path) -> dict[str, Path]: - member_roots = set(_member_roots(workspace)) - registered: dict[str, Path] = {} - for entry in _metadata_repository_entries(workspace): - repository_id = str(entry.get("id") or "").strip() - raw_root = str(entry.get("project_root") or entry.get("path") or "").strip() - if not repository_id or not raw_root: - continue - candidate = Path(raw_root).expanduser() - candidate = (workspace / candidate).resolve() if not candidate.is_absolute() else candidate.resolve() - # Only device/local member roots are eligible. Remote/origin locators never - # appear in this intersection. - if candidate not in member_roots: - continue - if repository_id in registered: - raise SystemExit("acceptance-blocked: registered repository identity is duplicated") - registered[repository_id] = candidate - return registered +def _next_plan_id(args: argparse.Namespace) -> str: + prefix = f"plan-{now_date().replace('-', '')}-" + sequence = 1 + while _identity_collision(args, "root-plan", f"{prefix}{sequence:03d}"): + sequence += 1 + return f"{prefix}{sequence:03d}" -def _validate_final_workspace_selectors( - args: argparse.Namespace, bindings: list[dict[str, object]] +def _validate_candidate( + family: str, data: dict[str, object], bindings: dict[str, str] ) -> None: - selectors = { - "workspace_id": getattr(args, "workspace_id", None), - "execution_id": getattr(args, "execution_id", None), - "runtime_root": getattr(args, "execution_runtime_root", None), - } - supplied = {field: value for field, value in selectors.items() if value} - if not supplied: - return - - def matches(binding: dict[str, object]) -> bool: - for field, value in supplied.items(): - actual = binding.get(field) - if field == "runtime_root": - if Path(str(actual or "")).expanduser().resolve() != Path( - str(value) - ).expanduser().resolve(): - return False - elif str(actual or "") != str(value): - return False - return True - - if any(matches(binding) for binding in bindings): - return - if len(supplied) == 1: - field = next(iter(supplied)) - raise SystemExit( - f"acceptance-blocked: {field.replace('_', ' ')} selector conflicts with accepted task authority" - ) - raise SystemExit( - "acceptance-blocked: selector tuple conflicts with accepted task authority" + validate_artifact( + _plan_policy(family), + data, + catalog_path=CATALOG_PATH, + bindings=bindings, ) -def _resolve_final_plan_workspace( - args: argparse.Namespace, plan_id: str | None = None -) -> Path: - workspace = resolve_workspace_root(args) - try: - members = _member_roots(workspace) - except OSError: - members = [] - if len(members) <= 1: - target = members[0] if members else workspace - if not target.is_dir(): - raise SystemExit("acceptance-blocked: final plan workspace is missing") - return target - - bindings = _accepted_plan_repository_bindings(args, plan_id) if plan_id else [] - _validate_final_workspace_selectors(args, bindings) - accepted_repository_ids = { - str(binding.get("repository_id") or "").strip() for binding in bindings - } - if "" in accepted_repository_ids: - raise SystemExit("acceptance-blocked: accepted task repository authority is missing") - if len(accepted_repository_ids) > 1: - raise SystemExit("acceptance-blocked: accepted task repository authority disagrees") - - explicit_repository_id = str(getattr(args, "repository_id", None) or "").strip() - accepted_repository_id = next(iter(accepted_repository_ids), "") - if ( - explicit_repository_id - and accepted_repository_id - and explicit_repository_id != accepted_repository_id - ): - raise SystemExit( - "acceptance-blocked: repository selector conflicts with accepted task authority" - ) - repository_id = explicit_repository_id or accepted_repository_id - if not repository_id: - raise SystemExit("acceptance-blocked: final plan workspace is ambiguous") - target = _registered_repository_roots(workspace).get(repository_id) - if target is None: - raise SystemExit( - "acceptance-blocked: authorized final plan repository is not a registered local member" - ) - if not target.is_dir(): - raise SystemExit("acceptance-blocked: final plan workspace is missing") - return target - - -def _observe_archive_command(command: str, workspace: Path) -> str: - completed = subprocess.run( - command, - shell=True, - cwd=str(workspace), - capture_output=True, - text=True, - check=False, +def _active_root_plan(args: argparse.Namespace, plan_id: str) -> dict[str, object]: + policy = _plan_policy("root-plan") + path = canonical_artifact_path( + policy, + _plan_anchors(args), + identity=plan_id, + state="active", + ) + if not path.is_file(): + raise SystemExit(f"Root plan is not canonical and active: {plan_id}") + raw = read_yaml_mapping(path) + source_spec_id = str(raw.get("source_spec_id") or "") + if not source_spec_id: + raise SystemExit(f"Root plan source binding is invalid: {plan_id}") + return dict( + read_artifact( + CATALOG_PATH, + "root-plan", + _plan_anchors(args), + identity=plan_id, + state="active", + bindings={"source_spec": source_spec_id}, + )["data"] ) - return "passed" if completed.returncode == 0 else "failed" -def _assert_archive_command_state_neutral(command: str, workspace: Path) -> None: - try: - pre = capture_repository_evidence(workspace) - except RuntimeError as error: - raise SystemExit(f"acceptance-blocked: {error}") from error - result = _observe_archive_command(command, workspace) - if result != "passed": - raise SystemExit(f"acceptance-blocked: declared plan-level acceptance {command} is {result}") - try: - post = capture_repository_evidence(workspace) - except RuntimeError as error: - raise SystemExit(f"acceptance-blocked: {error}") from error - caused = task_caused_paths(pre, post, workspace) - if caused or pre != post: - raise SystemExit( - "acceptance-blocked: declared plan-level acceptance mutated Git-observable state" - ) +def _active_artifact( + args: argparse.Namespace, family: str, identity: str, bindings: dict[str, str] +) -> dict[str, object]: + return dict( + read_artifact( + CATALOG_PATH, family, _plan_anchors(args), identity=identity, + state="active", bindings=bindings, + )["data"] + ) -def _observe_archive_obligations( - control_root: Path, - command: str, - workspace: Path, - validated: list[tuple[dict[str, object], dict[str, object]]], -) -> list[dict[str, object]]: - """Consume exact accepted task observations without rerunning validation.""" - store = ManagedProvenanceStore( - control_root / ".work-bundle/runtime/completion-provenance" - ) - matches: list[tuple[dict[str, object], dict[str, object], dict[str, object]]] = [] - for accepted, task in validated: - if accepted.get("schema") != "accepted-task-result-v1": - continue - for item in task.get("validation", []): - if isinstance(item, dict) and str(item.get("command") or "").strip() == command: - matches.append((accepted, task, item)) - if not matches: - return [] - - observed: list[dict[str, object]] = [] - for accepted, task, item in matches: - evidence_ids = accepted.get("validation_evidence_ids") - if not isinstance(evidence_ids, list) or not evidence_ids: - raise SystemExit( - "acceptance-blocked: accepted task result has no harness observation" - ) - definition = { - key: item.get(key) - for key in ( - "id", "kind", "command", "mechanism", "expected", - "acceptable_results", "invariant_ids", "digest", "proves", - ) - } - expected_command_digest = semantic_digest(definition) - accepted_source = accepted.get("accepted_source") - accepted_tree = ( - accepted_source.get("tree") if isinstance(accepted_source, dict) else None - ) - accepted_observation_id: str | None = None - for evidence_id in evidence_ids: - try: - record = load_observation(store, str(evidence_id)).to_dict() - except CompletionProvenanceError: - continue - if ( - record.get("command_digest") == expected_command_digest - and record.get("product_tree") == accepted_tree - and isinstance(record.get("result"), dict) - and record["result"].get("exit_code") == 0 - ): - accepted_observation_id = str(evidence_id) - break - if accepted_observation_id is None: - raise SystemExit( - "acceptance-blocked: accepted task result does not reference an accepted harness observation" - ) - observed.append( - { - "id": str(item.get("id") or ""), - "observation_id": accepted_observation_id, - "result": "passed", - } - ) - return observed -def _assert_archive_plan_acceptance( - args: argparse.Namespace, - plan_id: str, - root_path: Path, - validated: list[tuple[dict[str, object], dict[str, object]]], -) -> None: - _, body = read_front_matter(root_path) - commands = _declared_integration_commands(body) - if not commands: - return - 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)] - uses_accepted_results = any( - result.get("schema") == "accepted-task-result-v1" for result, _brief in validated - ) - control_root = resolve_workspace_root(args) if uses_accepted_results else None - for command in commands: - if control_root is not None: - observed = _observe_archive_obligations( - control_root, command, git_root, validated - ) - if observed: - continue - raise SystemExit( - f"acceptance-blocked: no accepted validation obligation for {command}" - ) - terminal_results: set[str] = set() - for handoff, _brief in validated: - result = _handoff_command_result(handoff, command) - if result is None: - continue - tree = _verified_handoff_tree(git_root, handoff) - if terminal_tree and tree == terminal_tree: - terminal_results.add(result) - if terminal_results: - if terminal_results == {"passed"}: - continue - raise SystemExit( - f"acceptance-blocked: declared plan-level acceptance {command} is {_acceptance_result_detail(terminal_results)}" - ) - # Historical task evidence is not terminal plan authority. The archive - # gate obtains one fresh state-neutral observation below instead. - if control_root is not None: - return - workspace = git_root if material else _resolve_final_plan_workspace(args, plan_id) - for command in commands: - _assert_archive_command_state_neutral(command, workspace) -def _index_front_matter_scalars( - front_matter: dict[str, object], path: Path, args: argparse.Namespace -) -> dict[str, object]: - """Decode quoted scalar values only for the flat plan index projection.""" - - normalized = dict(front_matter) - for key, value in front_matter.items(): - if ( - not isinstance(value, str) - or len(value) < 2 - or value[0] != value[-1] - or value[0] not in {"'", '"'} - ): - continue - parsed = _parse_scalar(value) - if not isinstance(parsed, str): - raise SystemExit( - f"Invalid quoted plan index scalar {key}: {rel(path, args)}" - ) - normalized[key] = parsed - return normalized def index_plans(args: argparse.Namespace) -> list[dict[str, object]]: - root = orchestration_root(args) / "plan" - rows = [] - for path in sorted(root.glob("active/*.md")) + sorted(root.glob("archived/*.md")): - fm, _ = read_front_matter(path) - if not fm: - continue - fm = _index_front_matter_scalars(fm, path, args) - rows.append( - { - "type": "plan", - "id": fm.get("id", path.stem), - "title": fm.get("goal", fm.get("title", path.stem)), - "status": fm.get("status", "Planned"), - "path": rel(path, args), - "purpose": fm.get("purpose", ""), - "component": fm.get("component", ""), - "created_at": fm.get("date_created", ""), - "updated_at": fm.get("last_updated", ""), - } - ) - for path in sorted(root.glob("active/*/phase-*.md")) + sorted(root.glob("archived/*/phase-*.md")): - fm, _ = read_front_matter(path) - if not fm: - continue - fm = _index_front_matter_scalars(fm, path, args) - rows.append( - { - "type": "phase", - "id": fm.get("id", path.stem), - "plan_id": fm.get("plan_id", path.parent.name), - "title": fm.get("name", fm.get("title", path.stem)), - "status": fm.get("status", "Planned"), - "path": rel(path, args), - "created_at": fm.get("date_created", ""), - "updated_at": fm.get("last_updated", ""), - } - ) - nested_task_paths = list(root.glob("active/*/phase-*/*.md")) + list( - root.glob("archived/*/phase-*/*.md") - ) - direct_task_paths = list(root.glob("active/*/task-*.md")) + list( - root.glob("archived/*/task-*.md") - ) - for path in sorted({*nested_task_paths, *direct_task_paths}): - fm, _ = read_front_matter(path) - if not fm: - continue - fm = _index_front_matter_scalars(fm, path, args) - direct_layout = path.parent.parent.name in {"active", "archived"} - if direct_layout and ( - fm.get("plan_id") != path.parent.name or not fm.get("phase_id") - ): - raise SystemExit(f"Invalid direct task identity: {rel(path, args)}") - inferred_plan_id = path.parent.name if direct_layout else path.parents[1].name - inferred_phase_id = "" if direct_layout else path.parent.name - rows.append( - { - "type": "task", - "id": fm.get("id", path.stem), - "plan_id": fm.get("plan_id", inferred_plan_id), - "phase_id": fm.get("phase_id", inferred_phase_id), - "title": fm.get("name", fm.get("title", path.stem)), - "status": fm.get("status", "Planned"), - "path": rel(path, args), - "task_type": fm.get("task_type", ""), - "created_at": fm.get("date_created", ""), - "updated_at": fm.get("last_updated", ""), - } - ) - (root / "index.jsonl").write_text("\n".join(json.dumps(row, ensure_ascii=False) for row in rows) + ("\n" if rows else ""), encoding="utf-8") - return rows + rows = [ + _canonical_row(args, family, row) + for family in PLAN_FAMILIES + for row in _index_rows(args, family) + ] + return sorted(rows, key=lambda row: (str(row["artifact_type"]), str(row["id"]))) def cmd_index_plans(args: argparse.Namespace) -> None: @@ -773,26 +221,47 @@ def cmd_index_plans(args: argparse.Namespace) -> None: def cmd_write_plan(args: argparse.Namespace) -> None: - from bounded_closure import require_orchestration_admission, resolve_working_workspace - authority = resolve_working_workspace(resolve_workspace_root(args)) - if authority is not None: - require_orchestration_admission(authority, operation="ordinary_new", flow_id=args.id) - init_dirs(args) - if args.status not in PLAN_STATUSES: - raise SystemExit(f"Invalid plan status: {args.status}") - pid = args.id or sequence_id(orchestration_root(args) / "plan" / "active", "plan") - filename = args.filename or f"{args.purpose}-{slugify(args.component)}-{args.version}.md" - content = Path(args.content_file).read_text(encoding="utf-8") - content = ensure_front_matter(content, {"id": pid, "goal": args.title, "purpose": args.purpose, "component": args.component, "version": args.version, "date_created": now_date(), "last_updated": now_date(), "owner": "agent", "status": args.status}) - target = orchestration_root(args) / "plan" / "active" / filename - from execution_context import parse_yaml_subset - effective_status = parse_yaml_subset(content.split("---", 2)[1]).get("status") - if effective_status in {"In progress", "Completed"} or args.status in {"In progress", "Completed"}: - require_plan_reviews(project_root(args), target, content=content, - source_root=_resolve_final_plan_workspace(args, pid) if "Completed" in {effective_status, args.status} else None) - write_text_safely(target, content, args) - index_plans(args) - print(rel(target, args)) + if getattr(args, "filename", None): + raise SystemExit("Plan filename override is not supported by the canonical family") + if args.status not in PLAN_QUALIFICATION_STATUSES: + raise SystemExit(f"Invalid plan qualification status: {args.status}") + semantic = _semantic_yaml( + Path(args.content_file), PLAN_STRUCTURAL_INPUT_FIELDS, "Root plan" + ) + pid = args.id or _next_plan_id(args) + source_spec_id = str(getattr(args, "source_spec_id", "") or "") + if not source_spec_id: + raise SystemExit("Root plan requires --source-spec-id") + today = now_date() + data = { + **semantic, + "artifact_type": "root-plan", + "schema_version": 1, + "id": pid, + "goal": args.title, + "purpose": args.purpose, + "component": args.component, + "version": args.version, + "source_spec_id": source_spec_id, + "status": args.status, + "date_created": today, + "last_updated": today, + } + bindings = {"source_spec": source_spec_id} + _validate_candidate("root-plan", data, bindings) + source = read_artifact( + CATALOG_PATH, "specification", _plan_anchors(args), + identity=source_spec_id, state="active", + ) + if source["data"].get("status") != "verified": + raise SystemExit("Root plan source specification must be active and verified") + if _identity_collision(args, "root-plan", pid, bindings=bindings): + raise SystemExit(f"Root plan canonical identity collision: {pid}") + result = write_artifact( + CATALOG_PATH, "root-plan", _plan_anchors(args), data, state="active", + bindings=bindings, + ) + print(rel(Path(str(result["path"])), args)) def cmd_list_plans(args: argparse.Namespace) -> None: @@ -805,427 +274,450 @@ def cmd_list_plans(args: argparse.Namespace) -> None: print(json.dumps(row, ensure_ascii=False)) -def _load_current_task_acceptance( - args: argparse.Namespace, task_path: Path -) -> tuple[dict[str, object], dict[str, object]]: - compile_args = argparse.Namespace( - project_root=getattr(args, "project_root", None), - workspace_root=getattr(args, "workspace_root", None), - task=str(task_path), - handoff=None, - base=None, - head=None, - **_observation_kwargs(args), - ) - _, brief_document = _compile_task_brief(compile_args) - return load_current_accepted_task_result( - resolve_workspace_root(args), brief_document["task_brief"] - ) -def _task_brief_at(args: argparse.Namespace, task_path: Path) -> dict[str, object]: - compile_args = argparse.Namespace( - project_root=getattr(args, "project_root", None), - workspace_root=getattr(args, "workspace_root", None), - task=str(task_path), - handoff=None, - base=None, - head=None, - **_observation_kwargs(args), - ) - _, brief_document = _compile_task_brief(compile_args) - return brief_document["task_brief"] -def _assert_task_dependencies_current(args: argparse.Namespace, task_path: Path) -> None: - front_matter, _body = read_front_matter(task_path) - if not front_matter.get("depends_on"): - return - brief = _task_brief_at(args, task_path) - rows = index_plans(args) - for dependency_id in brief.get("depends_on", []): - matches = [ - row for row in rows - if row.get("type") == "task" - and row.get("plan_id") == brief.get("plan_id") - and row.get("id") == dependency_id - ] - if len(matches) != 1 or matches[0].get("status") != "Completed": - raise SystemExit(f"dependency-blocked: {dependency_id} is not completed") - _load_current_task_acceptance(args, artifact_path_from_row(matches[0], args)) - - -def _accepted_plan_task_results( - args: argparse.Namespace, plan_id: str -) -> list[tuple[dict[str, object], dict[str, object]]]: - accepted: list[tuple[dict[str, object], dict[str, object]]] = [] - for row in index_plans(args): - if row.get("type") != "task" or row.get("plan_id") != plan_id: - continue - if row.get("status") != "Completed": - raise SystemExit(f"acceptance-blocked: task {row.get('id')} is not completed") - path = artifact_path_from_row(row, args) - _binding, result = _load_current_task_acceptance(args, path) - accepted.append((result, _task_brief_at(args, path))) - return accepted - - -def _plan_uses_accepted_result_authority(args: argparse.Namespace, plan_id: str) -> bool: - control_root = resolve_workspace_root(args) - for row in index_plans(args): - if row.get("type") != "task" or row.get("plan_id") != plan_id: - continue - if has_persisted_accepted_task_result( - control_root, plan_id, str(row.get("id") or "") - ): - return True - return False -def _assert_phase_tasks_accepted(args: argparse.Namespace, phase_id: str, plan_id: str) -> None: - rows = [ - row for row in index_plans(args) - if row.get("type") == "task" - and row.get("plan_id") == plan_id - and row.get("phase_id") == phase_id - ] - for row in rows: - if row.get("status") != "Completed": - raise SystemExit(f"acceptance-blocked: task {row.get('id')} is not completed") - _load_current_task_acceptance(args, artifact_path_from_row(row, args)) -def _assert_completed_task_authority( - args: argparse.Namespace, task_path: Path -) -> dict[str, object]: - try: - _binding, accepted = _load_current_task_acceptance(args, task_path) - return accepted - except SystemExit as error: - missing_initial_binding = str(error) == "Task execution binding is missing harness provenance" - if missing_initial_binding: - front_matter, _body = read_front_matter(task_path) - published = has_persisted_accepted_task_result( - resolve_workspace_root(args), - str(front_matter.get("plan_id") or ""), - str(front_matter.get("id") or ""), - ) - else: - published = False - if str(error) != "accepted task result is missing" and not ( - missing_initial_binding and not published - ): - raise - handoff = getattr(args, "handoff", None) - if not handoff: - raise SystemExit("set-plan-status Completed for a task requires --handoff") - cmd_validate_executor_result( - argparse.Namespace( - project_root=getattr(args, "project_root", None), - workspace_root=getattr(args, "workspace_root", None), - task=str(task_path), - handoff=str(handoff), - base=None, - head=None, - **_observation_kwargs(args), - ) - ) - _binding, accepted = _load_current_task_acceptance(args, task_path) - return accepted - - -def _release_completed_task_binding(args: argparse.Namespace, row: dict[str, object]) -> dict[str, object]: - """Release and persist API-006 ownership after executor-result validation succeeds.""" - - control_root = resolve_workspace_root(args) - plan_id = str(row["plan_id"]) - task_id = str(row["id"]) - binding = load_task_execution_binding(control_root, plan_id, task_id) - accepted = binding.get("accepted_result") if isinstance(binding.get("accepted_result"), dict) else {} - artifact_digest = semantic_digest(accepted) if accepted else None - event = { - "event_id": "event-template", - "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), - "process_id": "process-plan-status", - "stage": "task-completion", - "attempt_id": str(binding["execution_id"]), - "event_type": "binding_released", - "enforcement_mode": "native", - "join_ids": { - "specification_id": None, - "plan_id": plan_id, - "phase_id": str(row.get("phase_id") or "") or None, - "task_id": task_id, - "review_id": None, - "evaluation_id": None, - }, - "clocks": {"wall_ms": 0, "active_ms": 0, "billed_ms": None}, - "finding_class": None, - "return_reason": "validated completion", - "owner": task_id, - "identity": { - "product_tree": (accepted.get("accepted_source") or {}).get("tree"), - "artifact_digest": artifact_digest, - "mutation_epoch": 0, - }, - "privacy": "operational_metadata_only", - } - store = ManagedProvenanceStore(control_root / ".work-bundle/runtime/completion-provenance") - released = release_completion_binding( - store, - str(binding["ownership"]["binding_id"]), - owner=task_id, - stage_event_workspace=control_root, - stage_event=event, - ).to_dict() - updated = {**binding, "ownership": released} - _persist_binding(updated, control_root) - if released["target_kind"] == "isolated_worktree": - _execution_workspace_module().retain_binding_owner( - Path(str(binding["runtime_root"])), - str(binding["workspace_id"]), - str(binding["execution_id"]), - str(binding["repository_id"]), - ownership=released, - ) - return released + + + + + + def cmd_set_plan_status(args: argparse.Namespace) -> None: - if args.status not in PLAN_STATUSES: - raise SystemExit(f"Invalid plan status: {args.status}") - rows = index_plans(args) kind = getattr(args, "kind", None) - plan_id = getattr(args, "plan_id", None) - matches = [ - row - for row in rows - if row.get("id") == args.id - and (not kind or row.get("type") == kind) - and (not plan_id or row.get("plan_id") == plan_id) - ] - if not matches: - raise SystemExit(f"Plan artifact not found: {args.id}") - if len(matches) > 1: - selectors = [] - if not kind: - selectors.append("--kind plan|phase|task") - if not plan_id: - selectors.append("--plan-id PLAN_ID") - guidance = f"; pass {' and '.join(selectors)}" if selectors else "; supplied selectors remain ambiguous" - raise SystemExit(f"Multiple plan artifacts match {args.id}{guidance}") - row = matches[0] - path = artifact_path_from_row(row, args) - if row.get("type") == "task" and args.status in {"In progress", "Completed"}: - _assert_task_dependencies_current(args, path) - if row.get("type") == "phase" and args.status == "Completed": - _assert_phase_tasks_accepted(args, str(row["id"]), str(row["plan_id"])) - if row.get("type") == "plan" and args.status in {"In progress", "Completed"}: - require_plan_reviews(project_root(args), path, - source_root=_resolve_final_plan_workspace(args, str(row["id"])) if args.status == "Completed" else None) - if args.status == "Completed": - _accepted_plan_task_results(args, str(row["id"])) - if args.status == "Completed" and row.get("type") == "task": - _assert_completed_task_authority(args, path) - _release_completed_task_binding(args, row) - replace_front_matter_value(path, "status", args.status) - if args.status == "Deprecated": - active_root = orchestration_root(args) / "plan" / "active" - archived_root = orchestration_root(args) / "plan" / "archived" - if is_relative_to(path, active_root): - move_to_archive(path, active_root, archived_root) - index_plans(args) + if kind in {"phase", "task"}: + raise SystemExit("stage5-required: phase/task execution-state mutation is not owned by Stage 4") + if args.status not in PLAN_QUALIFICATION_STATUSES: + raise SystemExit(f"Invalid plan qualification status: {args.status}") + rows = [row for row in _index_rows(args, "root-plan") if row["id"] == args.id] + if len(rows) != 1: + raise SystemExit(f"Root plan not found at canonical location: {args.id}") + row = rows[0] + bindings = _family_bindings("root-plan", row) + data = _active_artifact(args, "root-plan", args.id, bindings) + if data["status"] == args.status: + print(args.id) + return + if args.status not in PLAN_QUALIFICATION_TRANSITIONS[str(data["status"])]: + raise SystemExit( + f"Invalid plan qualification transition: {data['status']} -> {args.status}" + ) + data["status"] = args.status + data["last_updated"] = now_date() + write_artifact( + CATALOG_PATH, "root-plan", _plan_anchors(args), data, state="active", + bindings=bindings, + ) print(args.id) def cmd_archive_plan(args: argparse.Namespace) -> None: - rows = index_plans(args) - root_match = next((row for row in rows if row.get("type") == "plan" and row.get("id") == args.id), None) - if not root_match: - raise SystemExit(f"Plan artifact not found: {args.id}") - - active_root = orchestration_root(args) / "plan" / "active" - archived_root = orchestration_root(args) / "plan" / "archived" - moved = [] - - root_path = artifact_path_from_row(root_match, args) - require_plan_reviews( - project_root(args), root_path, - source_root=_resolve_final_plan_workspace(args, args.id), + raise SystemExit( + "unsupported: use finalize-reviewed-plan with current canonical artifacts" ) - if _plan_uses_accepted_result_authority(args, args.id): - validated = _accepted_plan_task_results(args, args.id) - else: - # Pre-accepted-result plans retain a bounded migration path. New plans - # switch irreversibly once any task publishes durable accepted authority. - validated = _validated_plan_task_handoffs(args, args.id) - _assert_archive_knowledge_gate(args, args.id, root_path, validated) - _assert_archive_plan_acceptance(args, args.id, root_path, validated) - if is_relative_to(root_path, active_root): - replace_front_matter_value(root_path, "status", "Completed") - moved.append(move_to_archive(root_path, active_root, archived_root)) - - 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 / 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) - shutil.move(str(active_plan_dir), str(archived_plan_dir)) - moved.append(archived_plan_dir) - - if not moved: - raise SystemExit(f"Plan is not active or has no active files: {args.id}") - - index_plans(args) - for path in moved: - print(rel(path, args)) - - -def archive_plan_for_forced_finalization(args: argparse.Namespace, plan_id: str) -> list[Path]: - """Move one origin plan tree without running acceptance observation. - - Bounded forced closure has already recorded the unresolved product truth. - This helper performs only retry-safe administrative moves and indexing. - """ +def _raise_finalization_partial( + completed_operations: list[dict[str, str]], + failed_operation: dict[str, str], + error: BaseException, +) -> None: + raise SystemExit(json.dumps({ + "status": "partial", + "code": "WB_FINALIZATION_PARTIAL_EFFECT", + "mutation_started": True, + "completed_operations": completed_operations, + "failed_operation": failed_operation, + "error": str(error), + }, ensure_ascii=False, sort_keys=True)) - rows = index_plans(args) - plan_matches = [ - row for row in rows if row.get("type") == "plan" and row.get("id") == plan_id - ] - active_root = orchestration_root(args) / "plan" / "active" - archived_root = orchestration_root(args) / "plan" / "archived" - active = [row for row in plan_matches if is_relative_to(artifact_path_from_row(row, args), active_root)] - archived = [row for row in plan_matches if is_relative_to(artifact_path_from_row(row, args), archived_root)] - if len(active) > 1 or len(archived) > 1 or (active and archived): - raise SystemExit(f"Forced finalization plan archive collision: {plan_id}") - if not active and not archived: - raise SystemExit(f"Forced finalization origin plan not found: {plan_id}") - - root_path = artifact_path_from_row((active or archived)[0], args) - 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") == plan_id - and is_relative_to(artifact_path_from_row(row, args), active_root) - } - indexed_archived_dirs = { - archived_root / artifact_path_from_row(row, args).relative_to(archived_root).parts[0] - for row in rows - if row.get("type") == "task" - and row.get("plan_id") == plan_id - and is_relative_to(artifact_path_from_row(row, args), archived_root) - } - if len(indexed_active_dirs) > 1 or len(indexed_archived_dirs) > 1 or ( - indexed_active_dirs and indexed_archived_dirs - ): - raise SystemExit(f"Forced finalization plan directory collision: {plan_id}") - if active and 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 / plan_id - - targets = [] - if active: - targets.append(archived_root / root_path.relative_to(active_root)) - if active_plan_dir.exists(): - targets.append(archived_root / active_plan_dir.relative_to(active_root)) - collisions = [path for path in targets if path.exists()] - if collisions: - raise SystemExit(f"Forced finalization plan archive collision: {collisions[0]}") - - moved = [] - if active: - moved.append(move_to_archive(root_path, active_root, archived_root)) - else: - moved.append(root_path) - if active_plan_dir.exists(): - moved.append(move_to_archive(active_plan_dir, active_root, archived_root)) - elif indexed_archived_dirs: - moved.append(next(iter(indexed_archived_dirs))) - index_plans(args) - return moved - - -def release_plan_bindings_for_forced_finalization( - control_root: Path, plan_id: str + +def _release_plan_bindings( + control_root: Path, + plan_id: str, + bindings: list[dict[str, object]], + completed_operations: list[dict[str, str]], ) -> dict[str, object]: - """Release only bindings whose current ownership can truthfully terminate.""" + """Release current task bindings whose ownership can truthfully terminate.""" store = ManagedProvenanceStore( control_root / ".work-bundle/runtime/completion-provenance" ) released: list[str] = [] - incomplete: list[dict[str, str]] = [] - for binding in _iter_task_bindings(control_root): + for binding in bindings: if binding.get("plan_id") != plan_id: continue ownership = binding.get("ownership") - if not isinstance(ownership, dict): - incomplete.append({"task_id": str(binding.get("task_id") or "unknown"), "reason": "ownership-invalid"}) - continue + assert isinstance(ownership, dict) state = str(ownership.get("state") or "") if state == "released": released.append(str(binding.get("task_id") or "")) continue - if state not in {"active", "releasable"}: - incomplete.append({"task_id": str(binding.get("task_id") or "unknown"), "reason": f"ownership-{state}"}) - continue + task_id = str(binding.get("task_id") or "unknown") + binding_id = str(ownership["binding_id"]) + release_operation = { + "operation": "binding-release", + "task_id": task_id, + "binding_id": binding_id, + } try: updated_ownership = release_completion_binding( store, - str(ownership["binding_id"]), + binding_id, owner=str(ownership["original_owner"]), ).to_dict() - except (KeyError, CompletionProvenanceError) as error: - incomplete.append({"task_id": str(binding.get("task_id") or "unknown"), "reason": str(error)}) - continue - _persist_binding({**binding, "ownership": updated_ownership}, control_root) - released.append(str(binding.get("task_id") or "")) - return {"released": sorted(value for value in released if value), "incomplete": incomplete} + except (Exception, SystemExit) as error: + _raise_finalization_partial(completed_operations, release_operation, error) + completed_operations.append({ + "operation": "binding-provenance-release", + "task_id": task_id, + "binding_id": binding_id, + }) + persist_operation = { + "operation": "binding-file-persist", + "task_id": task_id, + "binding_id": binding_id, + } + try: + _persist_binding({**binding, "ownership": updated_ownership}, control_root) + except (Exception, SystemExit) as error: + _raise_finalization_partial(completed_operations, persist_operation, error) + completed_operations.append(persist_operation) + released.append(task_id) + return {"released": sorted(value for value in released if value)} def cmd_write_phase(args: argparse.Namespace) -> None: - from bounded_closure import require_orchestration_admission, resolve_working_workspace - authority = resolve_working_workspace(resolve_workspace_root(args)) - if authority is not None: - require_orchestration_admission(authority, operation="reconciliation", flow_id=args.plan_id) - content = Path(args.content_file).read_text(encoding="utf-8") - content = ensure_front_matter(content, {"id": args.phase_id, "plan_id": args.plan_id, "name": args.title, "status": args.status, "date_created": now_date(), "last_updated": now_date()}) - target = orchestration_root(args) / "plan" / "active" / args.plan_id / f"{args.phase_id}-{slugify(args.title)}.md" - write_text_safely(target, content, args) - index_plans(args) - print(rel(target, args)) + if args.status != PLANNED_STATUS: + raise SystemExit("Phase status must be planned; execution states require Stage 5") + semantic = _semantic_yaml( + Path(args.content_file), PHASE_STRUCTURAL_INPUT_FIELDS, "Phase" + ) + today = now_date() + data = { + **semantic, + "artifact_type": "phase", "schema_version": 1, + "id": args.phase_id, "plan_id": args.plan_id, "name": args.title, + "status": PLANNED_STATUS, "date_created": today, "last_updated": today, + } + bindings = {"plan": args.plan_id} + _validate_candidate("phase", data, bindings) + _active_root_plan(args, str(args.plan_id)) + if _identity_collision(args, "phase", args.phase_id, bindings=bindings): + raise SystemExit(f"Phase canonical identity collision: {args.phase_id}") + result = write_artifact( + CATALOG_PATH, "phase", _plan_anchors(args), data, state="active", + bindings=bindings, + ) + print(rel(Path(str(result["path"])), args)) def cmd_write_task(args: argparse.Namespace) -> None: - from bounded_closure import require_orchestration_admission, resolve_working_workspace - authority = resolve_working_workspace(resolve_workspace_root(args)) - if authority is not None: - require_orchestration_admission(authority, operation="reconciliation", flow_id=args.plan_id) - content = Path(args.content_file).read_text(encoding="utf-8") - content = ensure_front_matter(content, {"id": args.task_id, "phase_id": args.phase_id, "plan_id": args.plan_id, "name": args.title, "status": args.status, "date_created": now_date(), "last_updated": now_date()}) - plan_dir = orchestration_root(args) / "plan" / "active" / args.plan_id - phase_dirs = sorted(plan_dir.glob(f"{args.phase_id}-*")) - phase_dir = next((path for path in phase_dirs if path.is_dir()), plan_dir / f"{args.phase_id}-{slugify(args.phase_id)}") - target = phase_dir / f"{args.task_id}-{slugify(args.title)}.md" - write_text_safely(target, content, args) - index_plans(args) - print(rel(target, args)) + if args.status != PLANNED_STATUS: + raise SystemExit("Task status must be planned; execution states require Stage 5") + semantic = _semantic_yaml( + Path(args.content_file), TASK_STRUCTURAL_INPUT_FIELDS, "Task" + ) + source_obligation_records(semantic, label="Task") + today = now_date() + data = { + **semantic, + "artifact_type": "task", "schema_version": 2, + "id": args.task_id, "plan_id": args.plan_id, "phase_id": args.phase_id, + "name": args.title, "status": PLANNED_STATUS, + "date_created": today, "last_updated": today, + } + bindings = {"plan": args.plan_id, "phase": args.phase_id} + _validate_candidate("task", data, bindings) + _active_root_plan(args, str(args.plan_id)) + try: + read_artifact( + CATALOG_PATH, + "phase", + _plan_anchors(args), + identity=str(args.phase_id), + state="active", + bindings={"plan": str(args.plan_id)}, + ) + except (FileNotFoundError, SystemExit) as error: + raise SystemExit( + f"Task parent phase is not canonical for plan {args.plan_id}: {args.phase_id}" + ) from error + if _identity_collision(args, "task", args.task_id, bindings=bindings): + raise SystemExit(f"Task canonical identity collision: {args.task_id}") + result = write_artifact( + CATALOG_PATH, "task", _plan_anchors(args), data, state="active", + bindings=bindings, + ) + print(rel(Path(str(result["path"])), args)) + + +def cmd_finalize_reviewed_plan(args: argparse.Namespace) -> None: + """Mechanically archive one exact accepted current plan and release bindings.""" + + from artifact_store import transition_artifact + from review_runtime import CURRENT_CATALOG + + anchors = _plan_anchors(args) + review = read_artifact( + CURRENT_CATALOG, + "final-workflow-review", + anchors, + identity=str(args.final_review_id), + state="active", + bindings={"plan": str(args.plan_id)}, + ) + data = review["data"] + if data.get("verdict") != "accept" or data.get("archive_ready") is not True: + raise SystemExit("Final workflow review does not authorize archive readiness") + knowledge = data.get("knowledge_return") + if not isinstance(knowledge, dict) or knowledge.get("status") not in {"completed", "not-needed"}: + raise SystemExit("Final workflow review knowledge return is not closed") + + plan_rows = [row for row in _index_rows(args, "root-plan") if row.get("id") == args.plan_id] + if len(plan_rows) != 1: + raise SystemExit("Finalization requires one canonical plan identity") + plan_row = plan_rows[0] + plan_bindings = _family_bindings("root-plan", plan_row) + plan_record = read_artifact( + CATALOG_PATH, "root-plan", anchors, identity=str(args.plan_id), + state="active", bindings=plan_bindings, + ) + plan_ref = data.get("plan_identity") + if ( + not isinstance(plan_ref, dict) + or plan_ref.get("id") != args.plan_id + or plan_ref != canonical_plan_tree_identity( + resolve_workspace_root(args), str(args.plan_id), state="active" + ) + or data.get("specification_id") != plan_record["data"].get("source_spec_id") + ): + raise SystemExit("Final workflow review plan/specification identity is stale") + + task_rows = [row for row in _index_rows(args, "task") if row.get("plan_id") == args.plan_id] + task_ids = {str(row["id"]) for row in task_rows} + review_required_by_task: dict[str, bool] = {} + for row in task_rows: + task_id = str(row["id"]) + task_record = read_artifact( + CATALOG_PATH, + "task", + anchors, + identity=task_id, + state="active", + bindings=_family_bindings("task", row), + ) + acceptance_review = task_record["data"].get("acceptance_review") + required = acceptance_review.get("required") if isinstance(acceptance_review, dict) else None + if type(required) is not bool: + raise SystemExit(f"Finalization task acceptance-review contract is invalid: {task_id}") + review_required_by_task[task_id] = required + accepted_refs = data.get("accepted_results") + if not isinstance(accepted_refs, list) or {str(ref.get("task_id")) for ref in accepted_refs if isinstance(ref, dict)} != task_ids: + raise SystemExit("Final workflow review does not reference every planned task exactly once") + if len(accepted_refs) != len(task_ids): + raise SystemExit("Final workflow review accepted-result coverage is duplicated") + coverage = data.get("coverage") + if not isinstance(coverage, dict) or coverage != {"planned": len(task_ids), "accepted": len(task_ids), "missing": []}: + raise SystemExit("Final workflow review coverage does not match canonical tasks") + + accepted_records: list[tuple[dict[str, object], dict[str, str]]] = [] + executor_records: list[tuple[dict[str, object], dict[str, str], str]] = [] + declared_review_refs = { + (str(reference.get("id")), str(reference.get("sha256"))) + for reference in data.get("accepted_reviews", []) if isinstance(reference, dict) + } + expected_review_refs: set[tuple[str, str]] = set() + for reference in accepted_refs: + task_id = str(reference["task_id"]) + bindings = {"plan": str(args.plan_id), "task": task_id} + record = read_artifact( + CURRENT_CATALOG, "accepted-task-result", anchors, + identity=str(reference["id"]), state="active", bindings=bindings, + ) + if record["digest"] != reference.get("sha256") or record["data"].get("task_id") != reference["task_id"]: + raise SystemExit("Final workflow review accepted-result reference is stale") + accepted_data = record["data"] + implementation_ref = accepted_data.get("implementation_review") + if implementation_ref is None: + if review_required_by_task[task_id]: + raise SystemExit("Final workflow review omits a required implementation review") + elif isinstance(implementation_ref, dict): + implementation_identity = ( + str(implementation_ref.get("id")), + str(implementation_ref.get("sha256")), + ) + if implementation_identity not in declared_review_refs: + raise SystemExit("Final workflow review implementation-review reference is invalid") + expected_review_refs.add(implementation_identity) + else: + raise SystemExit("Final workflow review implementation-review reference is invalid") + executor_ref = accepted_data.get("executor_result") + executor_matches = [] + if isinstance(executor_ref, dict): + for state in _plan_policy("executor-result")["lifecycle"]["states"]: + try: + executor_matches.append(read_artifact( + CURRENT_CATALOG, "executor-result", anchors, + identity=str(executor_ref.get("id")), state=str(state), bindings=bindings, + )) + except (FileNotFoundError, SystemExit): + continue + if len(executor_matches) != 1 or executor_matches[0]["digest"] != executor_ref.get("sha256"): + raise SystemExit("Final workflow review accepted-result executor reference is stale") + executor_records.append((executor_matches[0], bindings, str(executor_matches[0]["state"]))) + accepted_records.append((record, bindings)) + + if declared_review_refs != expected_review_refs: + raise SystemExit("Final workflow review implementation-review coverage is not exact") + + review_records: list[dict[str, object]] = [] + for reference in data.get("accepted_reviews", []): + record = read_artifact( + CURRENT_CATALOG, "implementation-review", anchors, + identity=str(reference["id"]), state="active", bindings={"plan": str(args.plan_id)}, + ) + if record["digest"] != reference.get("sha256") or record["data"].get("verdict") != "accept": + raise SystemExit("Final workflow review implementation-review reference is stale") + review_records.append(record) + + repository = data.get("repository_finalization") + repositories = repository.get("repositories") if isinstance(repository, dict) else None + if not isinstance(repositories, list) or not repositories: + raise SystemExit("Final workflow review requires concrete repository baselines") + for baseline in repositories: + root = Path(str(baseline.get("root") or "")).expanduser().resolve() + head = subprocess.run(["git", "-C", str(root), "rev-parse", "HEAD"], capture_output=True, text=True, check=False) + dirty = subprocess.run(["git", "-C", str(root), "status", "--porcelain"], capture_output=True, text=True, check=False) + if head.returncode or dirty.returncode or head.stdout.strip() != baseline.get("head") or dirty.stdout: + raise SystemExit(f"Finalization repository baseline is not exact and clean: {baseline.get('repository_id')}") + + workspace_root = resolve_workspace_root(args) + bindings = [binding for binding in _iter_task_bindings(workspace_root) if binding.get("plan_id") == args.plan_id] + for binding in bindings: + ownership = binding.get("ownership") + try: + validated_ownership = validate_ownership_shape(ownership) if isinstance(ownership, dict) else None + if validated_ownership is not None and validated_ownership.get("state") != "released": + validate_execution_binding_ownership( + workspace_root / ".work-bundle/runtime/completion-provenance", + validated_ownership, + ) + except CompletionProvenanceError as error: + raise SystemExit("Finalization task binding ownership is invalid") from error + if validated_ownership is None or validated_ownership.get("state") not in {"active", "releasable", "released"}: + raise SystemExit("Finalization task binding cannot be safely released") + if validated_ownership["state"] in {"active", "releasable"} and ( + validated_ownership.get("current_owner") != validated_ownership.get("original_owner") + or validated_ownership.get("repair_owner") is not None + or validated_ownership.get("rereview_owner") is not None + or ( + validated_ownership["state"] == "releasable" + and validated_ownership.get("releasable") is not True + ) + ): + raise SystemExit("Finalization task binding cannot be safely released") + + # Check every archive destination before the first mutation. + transitions: list[tuple[Path, str, str, dict[str, str]]] = [] + transition_states: dict[tuple[str, str], str] = {} + for record, bindings_for_result, state in executor_records: + identity = str(record["data"]["id"]) + transitions.append((CURRENT_CATALOG, "executor-result", identity, bindings_for_result)) + transition_states[("executor-result", identity)] = state + for record, bindings_for_result in accepted_records: + transitions.append((CURRENT_CATALOG, "accepted-task-result", str(record["data"]["id"]), bindings_for_result)) + for record in review_records: + transitions.append((CURRENT_CATALOG, "implementation-review", str(record["data"]["id"]), {"plan": str(args.plan_id)})) + for row in task_rows: + transitions.append((CATALOG_PATH, "task", str(row["id"]), _family_bindings("task", row))) + phase_rows = [row for row in _index_rows(args, "phase") if row.get("plan_id") == args.plan_id] + for row in phase_rows: + transitions.append((CATALOG_PATH, "phase", str(row["id"]), _family_bindings("phase", row))) + transitions.append((CATALOG_PATH, "root-plan", str(args.plan_id), plan_bindings)) + transitions.append((CURRENT_CATALOG, "final-workflow-review", str(args.final_review_id), {"plan": str(args.plan_id)})) + seen_transitions: set[tuple[str, str]] = set() + for catalog, family, identity, bindings_for_item in transitions: + transition_key = (family, identity) + if transition_key in seen_transitions: + raise SystemExit(f"Finalization contains a duplicate transition: {family}/{identity}") + seen_transitions.add(transition_key) + current_state = transition_states.get(transition_key, "active") + policy = family_policy(load_catalog(catalog), family) + if "archived" not in policy["lifecycle"]["transitions"].get(current_state, []): + raise SystemExit( + f"Finalization transition is not permitted: {family}/{identity} {current_state}->archived" + ) + source = canonical_artifact_path( + policy, + anchors, + identity=identity, + state=current_state, + bindings=bindings_for_item, + ) + if not source.is_file() or source.is_symlink(): + raise SystemExit(f"Finalization archive source is unavailable: {source}") + try: + read_artifact( + catalog, + family, + anchors, + identity=identity, + state=current_state, + bindings=bindings_for_item, + ) + except (FileNotFoundError, SystemExit, ValueError) as error: + raise SystemExit( + f"Finalization archive source is invalid: {family}/{identity}" + ) from error + destination = canonical_artifact_path( + policy, anchors, + identity=identity, state="archived", bindings=bindings_for_item, + ) + if destination.exists(): + raise SystemExit(f"Finalization archive destination already exists: {destination}") + + completed_operations: list[dict[str, str]] = [] + released = _release_plan_bindings( + workspace_root, + str(args.plan_id), + bindings, + completed_operations, + ) + results = [] + for catalog, family, identity, bindings_for_item in transitions: + operation = { + "operation": "artifact-transition", + "family": family, + "identity": identity, + } + try: + results.append(transition_artifact( + catalog, family, anchors, identity=identity, + current_state=transition_states.get((family, identity), "active"), + target_state="archived", bindings=bindings_for_item, + )) + except (Exception, SystemExit) as error: + _raise_finalization_partial(completed_operations, operation, error) + completed_operations.append(operation) + for catalog, family, _identity, _bindings_for_item in transitions: + operation = { + "operation": "index-rebuild", + "family": family, + "identity": _identity, + } + try: + rebuild_index(catalog, family, anchors) + except (Exception, SystemExit) as error: + _raise_finalization_partial(completed_operations, operation, error) + completed_operations.append(operation) + result = { + "plan_id": str(args.plan_id), "final_review_id": str(args.final_review_id), + "archived": len(results), "released_tasks": released["released"], + } + print(json.dumps(result, ensure_ascii=False, sort_keys=True)) diff --git a/scripts/orchestration/repository_preflight.py b/scripts/orchestration/repository_preflight.py index cca9e32..06df86f 100644 --- a/scripts/orchestration/repository_preflight.py +++ b/scripts/orchestration/repository_preflight.py @@ -6,14 +6,11 @@ import argparse import hashlib import json -import re import subprocess from pathlib import Path from typing import Iterable, Mapping -import yaml - -from core import project_registry_path, resolve_workspace_root +from core import _infrastructure, resolve_workspace_root STATUS_COMMAND = ["git", "status", "--porcelain=v1", "--untracked-files=all"] @@ -67,68 +64,18 @@ def _front_matter_lists(path: Path) -> dict[str, list[str]]: return result -def _parse_value(value: str) -> object: - value = value.strip().strip("'\"") - if value == "true": - return True - if value == "false": - return False - return value - - -def _metadata_scalar(text: str, key: str) -> str: - match = re.search(rf"^{re.escape(key)}:\s*(.*?)\s*$", text, re.MULTILINE) - return match.group(1).strip().strip("'\"") if match else "" - - -def _device_binding_repositories(workspace_id: str) -> dict[str, dict[str, object]]: - registry = project_registry_path() - if not registry.is_file() or not workspace_id: - return {} - in_bindings = False - in_workspace = False - in_repositories = False - current_repository = "" - repositories: dict[str, dict[str, object]] = {} - for line in registry.read_text(encoding="utf-8").splitlines(): - if line == "device_bindings:": - in_bindings = True - continue - if in_bindings and line and not line.startswith(" "): - break - if not in_bindings: - continue - if re.match(r"^ [^\s].*:$", line): - current_id = line.strip()[:-1].strip("'\"") - in_workspace = current_id == workspace_id - in_repositories = False - current_repository = "" - continue - if not in_workspace: - continue - if line == " repositories:": - in_repositories = True - continue - if in_repositories and re.match(r"^ [^\s].*:$", line): - current_repository = line.strip()[:-1].strip("'\"") - repositories[current_repository] = {"id": current_repository} - continue - if in_repositories and current_repository and line.startswith(" ") and ":" in line: - key, value = line.strip().split(":", 1) - repositories[current_repository][key] = _parse_value(value) - return repositories - - def _v4_metadata_repository_entries(root: Path, text: str) -> list[dict[str, object]]: try: - document = yaml.safe_load(text) - except yaml.YAMLError: - return [] - if not isinstance(document, Mapping): - return [] - workspace = document.get("workspace") - workspace_id = str(workspace.get("id") or "") if isinstance(workspace, Mapping) else "" - local = _device_binding_repositories(workspace_id) + document = _infrastructure.load_workspace_metadata(root) + registry = _infrastructure.load_project_registry() + binding = _infrastructure.join_workspace_binding( + document, registry, expected_workspace_root=root + ) + except _infrastructure.InfrastructureError as exc: + raise SystemExit(exc.code) from exc + local = binding.get("repositories") + if not isinstance(local, Mapping): + raise SystemExit("WB_DEVICE_BINDING_REPOSITORIES_MISSING") repositories = document.get("source_repositories") if not isinstance(repositories, list): return [] @@ -179,56 +126,7 @@ def _metadata_repository_entries(root: Path) -> list[dict[str, object]]: metadata = root / ".work-bundle" / "project.yaml" if not metadata.exists(): return [] - text = metadata.read_text(encoding="utf-8") - if _metadata_scalar(text, "metadata_version") == "4": - return _v4_metadata_repository_entries(root, text) - repositories: list[dict[str, object]] = [] - in_source_repositories = False - current: dict[str, object] | None = None - current_nested: dict[str, object] | None = None - nested_key: str | None = None - for line in text.splitlines(): - if line == "source_repositories:": - in_source_repositories = True - continue - if in_source_repositories and line and not line.startswith(" "): - break - stripped = line.strip() - if not in_source_repositories or not stripped: - continue - if line.startswith(" - "): - if current is not None: - repositories.append(current) - current = {} - current_nested = None - nested_key = None - item = stripped[2:] - if ":" in item: - key, value = item.split(":", 1) - current[key.strip()] = _parse_value(value) - continue - if current is None: - continue - if line.startswith(" ") and not line.startswith(" ") and ":" in stripped: - key, value = stripped.split(":", 1) - key = key.strip() - value = value.strip() - current_nested = None - nested_key = None - if value == "" and key in {"branch_check", "codegraph"}: - current[key] = {} - current_nested = current[key] # type: ignore[assignment] - nested_key = key - else: - current[key] = _parse_value(value) - continue - if line.startswith(" ") and current_nested is not None and ":" in stripped: - key, value = stripped.split(":", 1) - current_nested[key.strip()] = _parse_value(value) - continue - if current is not None: - repositories.append(current) - return repositories + return _v4_metadata_repository_entries(root, metadata.read_text(encoding="utf-8")) def _metadata_repositories(root: Path) -> list[Path]: @@ -328,10 +226,15 @@ def resolve_target_repositories( if resolved: return _enrich_with_metadata(root, resolved) metadata_path = root / ".work-bundle" / "project.yaml" - if metadata_path.is_file() and _metadata_scalar( - metadata_path.read_text(encoding="utf-8"), "metadata_version" - ) == "4": - return _v4_metadata_targets(root) + if metadata_path.is_file(): + try: + document = _infrastructure.parse_yaml_mapping( + metadata_path.read_text(encoding="utf-8"), source=str(metadata_path) + ) + except _infrastructure.InfrastructureError as exc: + raise SystemExit(exc.code) from exc + if document.get("metadata_version") == 4: + return _v4_metadata_targets(root) return _enrich_with_metadata( root, _resolve_candidates(root, ((path, "project-metadata") for path in _metadata_repositories(root))), diff --git a/scripts/orchestration/review_identity.py b/scripts/orchestration/review_identity.py index e3615f6..24d3ed9 100644 --- a/scripts/orchestration/review_identity.py +++ b/scripts/orchestration/review_identity.py @@ -4,184 +4,205 @@ import hashlib import json -import re from pathlib import Path from typing import Any, Mapping -from artifact_inputs import parse_yaml_subset - - -STRUCTURAL_PLAN_PROJECTION_SCHEMA = "plan-structural-projection-v2" - -# V2 ignores lifecycle data only at these documented structural locations. -# ``*`` denotes one list element; names that happen to match at any other path -# remain substantive and therefore affect identity. -V2_LIFECYCLE_LOCATIONS = frozenset( - { - ("status",), - ("last_updated",), - ("updated_at",), - ("accepted_result",), - ("accepted_results",), - ("accepted_result_reference",), - ("accepted_result_references",), - ("evidence_reference",), - ("evidence_references",), - ("review_id",), - ("target_identity",), - ("review_mode",), - ("repair_frontier",), - ("review_reset",), - ("task_index", "*", "status"), - ("phase_index", "*", "status"), - ("acceptance_review", "verdict"), - ("acceptance_review", "reviewed_head"), - ("acceptance_review", "findings"), - } +from artifact_store import ( + canonical_artifact_path, + family_policy, + load_catalog, + read_artifact, + read_yaml_mapping, ) -# This body field is lifecycle wherever its exact Markdown field syntax occurs. -# It is deliberately not scoped by a section heading: headings are presentation, -# and must never decide which surrounding requirement prose enters identity. -V2_BODY_LIFECYCLE_FIELDS = frozenset({"Closure return"}) -LEGACY_APPEND_ONLY_FIELDS = frozenset( +CANONICAL_YAML_PLAN_PROJECTION_SCHEMA = "canonical-yaml-plan-tree-v1" +CURRENT_PLAN_CATALOG = ( + Path(__file__).resolve().parents[2] + / "references/assets/orchestration/contract/artifact-family-catalog-v5.yaml" +) +CANONICAL_YAML_CONTROL_FIELDS = frozenset( { - "accepted_result", - "accepted_results", - "accepted_result_reference", - "accepted_result_references", - "evidence_reference", - "evidence_references", - "review_id", - "target_identity", - "review_mode", - "repair_frontier", - "review_reset", + "status", "date_created", "last_updated", "updated_at", "review_id", + "target_identity", "review_mode", "repair_frontier", "review_reset", } ) +def canonical_yaml_plan_value(value: Mapping[str, object]) -> dict[str, object]: + """Remove only declared top-level lifecycle/control fields from one YAML member.""" -def _artifact_parts(path: Path, content: str | None = None) -> tuple[dict[str, Any], str]: - text = path.read_text(encoding="utf-8") if content is None else content.rstrip() + "\n" - if not text.startswith("---\n") or "\n---\n" not in text[4:]: - raise SystemExit(f"stage review: missing artifact front matter: {path}") - raw, body = text[4:].split("\n---\n", 1) - metadata = parse_yaml_subset(raw) - if not isinstance(metadata, dict) or not metadata.get("id"): - raise SystemExit(f"stage review: missing artifact identity: {path}") - return metadata, body - - -def _location_matches(path: tuple[str, ...]) -> bool: - return any( - len(location) == len(path) - and all(expected == "*" or expected == actual for expected, actual in zip(location, path)) - for location in V2_LIFECYCLE_LOCATIONS - ) - - -def _structural_value_v2(value: Any, path: tuple[str, ...] = ()) -> Any: - if isinstance(value, dict): - return { - key: _structural_value_v2(child, (*path, key)) - for key, child in sorted(value.items()) - if not _location_matches((*path, key)) - } - if isinstance(value, list): - return [_structural_value_v2(child, (*path, "*")) for child in value] - return value - - -def structural_plan_artifact_v2(path: Path, *, content: str | None = None) -> dict[str, Any]: - """Project one artifact using only the explicit V2 lifecycle locations. - - Apart from exact documented lifecycle fields, the complete Markdown body is - structural. No heading or section pattern can exclude requirements. - """ - - metadata, body = _artifact_parts(path, content) - lifecycle_label = re.escape(next(iter(V2_BODY_LIFECYCLE_FIELDS))) - closure_pattern = re.compile( - rf"^(?P<prefix>-\s+(?:\*\*{lifecycle_label}\*\*|{lifecycle_label}):[ \t]*)" - r"(?:missing|completed|not-needed|blocked)(?P<suffix>[ \t]*)$", - re.MULTILINE, - ) - structural_body = closure_pattern.sub(r"\g<prefix>missing\g<suffix>", body) - return {"metadata": _structural_value_v2(metadata), "body": structural_body} - - -def legacy_semantic_plan_value(value: Any, *, top_level: bool = False) -> Any: - """Original name-based projection retained only for legacy interpretation.""" - - if isinstance(value, dict): - projected: dict[str, Any] = {} - created = value.get("date_created") - for key, child in sorted(value.items()): - if key in LEGACY_APPEND_ONLY_FIELDS: - continue - if top_level and key in {"status", "last_updated", "updated_at"}: - continue - if key == "status": - projected[key] = "Planned" - elif key in {"last_updated", "updated_at"} and created is not None: - projected[key] = legacy_semantic_plan_value(created) - elif key == "verdict": - projected[key] = "pending" - elif key == "reviewed_head": - projected[key] = "" - elif key == "findings": - projected[key] = [] - else: - projected[key] = legacy_semantic_plan_value(child) - return projected - if isinstance(value, list): - return [legacy_semantic_plan_value(child) for child in value] - return value - - -def legacy_semantic_plan_body(body: str) -> str: - """Original heading-based closure normalization for legacy interpretation.""" - - section_pattern = re.compile( - r"^##\s+(?:2\.1\s+)?Knowledge Base Update Carry Forward\s*$" - r"[\s\S]*?(?=^##\s|\Z)", - re.MULTILINE, - ) - closure_pattern = re.compile( - r"^(?P<prefix>-\s+(?:\*\*Closure\ return\*\*|Closure\ return):[ \t]*)" - r"(?:missing|completed|not-needed|blocked)(?P<suffix>[ \t]*)$", - re.MULTILINE, - ) - - def normalize_closure(match: re.Match[str]) -> str: - return closure_pattern.sub(r"\g<prefix>missing\g<suffix>", match.group(0)) - - return section_pattern.sub(normalize_closure, body) + return { + key: child + for key, child in sorted(value.items()) + if key not in CANONICAL_YAML_CONTROL_FIELDS + and not key.startswith("accepted_result") + and not key.startswith("evidence_reference") + } -def legacy_semantic_plan_artifact( - path: Path, *, content: str | None = None +def load_canonical_plan_tree( + root: Path, + plan_id: str, + *, + state: str | None = None, ) -> dict[str, Any]: - metadata, body = _artifact_parts(path, content) + """Load one schema-valid plan tree through its declared canonical relationships.""" + + anchors = {"workspace_root": root.expanduser().resolve()} + catalog = load_catalog(CURRENT_PLAN_CATALOG) + root_policy = family_policy(catalog, "root-plan") + states = [state] if state is not None else ["active", "archived"] + roots: list[dict[str, Any]] = [] + for candidate_state in states: + path = canonical_artifact_path( + root_policy, + anchors, + identity=plan_id, + state=str(candidate_state), + ) + if not path.is_file(): + continue + candidate = read_yaml_mapping(path) + source_spec_id = str(candidate.get("source_spec_id") or "") + roots.append( + read_artifact( + CURRENT_PLAN_CATALOG, + "root-plan", + anchors, + identity=plan_id, + state=str(candidate_state), + bindings={"source_spec": source_spec_id}, + ) + ) + if len(roots) != 1: + raise SystemExit(f"Plan identity requires one canonical root plan: {plan_id}") + + root_record = roots[0] + root_data = dict(root_record["data"]) + lifecycle_state = str(root_record["state"]) + phase_ids = [ + str(item.get("id") or "") + for item in root_data.get("phase_index", []) + if isinstance(item, dict) + ] + if not phase_ids or len(phase_ids) != len(set(phase_ids)): + raise SystemExit(f"Plan identity requires a unique canonical phase index: {plan_id}") + + phases: dict[str, dict[str, Any]] = {} + tasks: dict[str, dict[str, Any]] = {} + for phase_id in phase_ids: + phase_record = read_artifact( + CURRENT_PLAN_CATALOG, + "phase", + anchors, + identity=phase_id, + state=lifecycle_state, + bindings={"plan": plan_id}, + ) + phase_data = dict(phase_record["data"]) + phases[phase_id] = phase_data + task_ids = [ + str(item.get("id") or "") + for item in phase_data.get("task_index", []) + if isinstance(item, dict) + ] + if not task_ids or len(task_ids) != len(set(task_ids)): + raise SystemExit( + f"Plan identity requires a unique canonical task index: {phase_id}" + ) + for task_id in task_ids: + if task_id in tasks: + raise SystemExit(f"Plan identity contains duplicate task: {task_id}") + task_record = read_artifact( + CURRENT_PLAN_CATALOG, + "task", + anchors, + identity=task_id, + state=lifecycle_state, + bindings={"plan": plan_id, "phase": phase_id}, + ) + tasks[task_id] = dict(task_record["data"]) return { - "metadata": legacy_semantic_plan_value(metadata, top_level=True), - "body": legacy_semantic_plan_body(body), + "state": lifecycle_state, + "root": root_data, + "phases": phases, + "tasks": tasks, } -def plan_artifact_projection_digest(projection: Mapping[str, Any]) -> str: - payload = json.dumps( - [projection["metadata"], projection["body"]], - sort_keys=True, - default=str, - separators=(",", ":"), +def canonical_plan_tree_identity( + root: Path, + plan_id: str, + *, + state: str | None = None, +) -> dict[str, str]: + """Digest schema-valid canonical content, independent of filenames and indexes.""" + + tree = load_canonical_plan_tree(root, plan_id, state=state) + members: list[dict[str, object]] = [ + { + "family": "root-plan", + "id": plan_id, + "value": canonical_yaml_plan_value(tree["root"]), + } + ] + members.extend( + { + "family": "phase", + "id": phase_id, + "value": canonical_yaml_plan_value(data), + } + for phase_id, data in sorted(tree["phases"].items()) ) - return hashlib.sha256(payload.encode()).hexdigest() - - -def semantic_plan_member_key(plan_root: Path, path: Path) -> str: - parts = list(path.relative_to(plan_root).parts) - if parts and parts[0] == "archived": - parts[0] = "active" - return Path(*parts).as_posix() + members.extend( + { + "family": "task", + "id": task_id, + "value": canonical_yaml_plan_value(data), + } + for task_id, data in sorted(tree["tasks"].items()) + ) + payload = { + "projection_schema": CANONICAL_YAML_PLAN_PROJECTION_SCHEMA, + "members": members, + } + encoded = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode() + return {"id": plan_id, "sha256": hashlib.sha256(encoded).hexdigest()} + + +def canonical_task_data(root: Path, plan_id: str, task_id: str) -> dict[str, Any]: + tree = load_canonical_plan_tree(root, plan_id) + try: + return dict(tree["tasks"][task_id]) + except KeyError as error: + raise SystemExit( + f"Canonical plan {plan_id} does not contain task {task_id}" + ) from error + + +def source_obligation_records( + data: Mapping[str, object], *, label: str = "Task" +) -> dict[str, str]: + """Validate the mechanical binding between task source IDs and supplied semantics.""" + + raw_source_ids = data.get("source_ids") + if not isinstance(raw_source_ids, list): + raise SystemExit(f"{label} requires source_ids") + source_ids = [str(value) for value in raw_source_ids] + obligations = data.get("source_obligations") + if not isinstance(obligations, list): + raise SystemExit(f"{label} requires source_obligations") + records: dict[str, str] = {} + for item in obligations: + if not isinstance(item, dict): + raise SystemExit(f"{label} source_obligations entries must be mappings") + source_id = str(item.get("source_id") or "") + semantic = item.get("semantic") + if source_id in records: + raise SystemExit(f"{label} source_obligations duplicate source_id: {source_id}") + if not isinstance(semantic, str) or not semantic.strip(): + raise SystemExit(f"{label} source_obligations require non-empty semantic values") + records[source_id] = semantic.strip() + if set(records) != set(source_ids) or len(records) != len(source_ids): + raise SystemExit(f"{label} source_obligations must exactly bind source_ids") + return records diff --git a/scripts/orchestration/review_runtime.py b/scripts/orchestration/review_runtime.py index 66da006..6ce86d9 100644 --- a/scripts/orchestration/review_runtime.py +++ b/scripts/orchestration/review_runtime.py @@ -1,2293 +1,272 @@ #!/usr/bin/env python3 +"""Current Stage 5 review, accepted-result, and final-review adapters.""" + from __future__ import annotations import argparse -import hashlib import importlib.util import json -import re -import subprocess import sys -from dataclasses import dataclass -from datetime import datetime, timezone from pathlib import Path -from typing import Any, Mapping, Sequence -from artifact_inputs import _as_list, _input_path, _read_structured, _resolve_spec_paths, parse_yaml_subset -import bounded_closure -from review_identity import ( - STRUCTURAL_PLAN_PROJECTION_SCHEMA, - legacy_semantic_plan_artifact, - plan_artifact_projection_digest, - semantic_plan_member_key, - structural_plan_artifact_v2, -) -from current_review_authority import ( - authority_path as _current_review_authority_path, - load_authority as _load_current_review_authority, - write_authority as _write_current_review_authority, -) - - -ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]*$") -SHA256_RE = re.compile(r"^[0-9a-f]{64}$") -GIT_OID_RE = re.compile(r"^[0-9a-f]{40}$") - -FINDING_STAGES = frozenset({"specification", "plan", "implementation", "validation", "environment"}) -FINDING_SEVERITIES = frozenset({"blocking", "non_blocking", "advisory"}) -OBLIGATION_BASES = frozenset({"accepted_requirement", "essential_safety", "evidence_integrity", "none"}) -TERMINAL_FINDING_DISPOSITIONS = frozenset({"accepted", "rejected"}) -STAGE_REVIEW_STAGES = frozenset({"specification", "plan", "integrated_implementation"}) -REVIEW_VERDICTS = frozenset({"accepted", "repair", "blocked"}) -REVIEW_MODES = frozenset({"initial", "repair"}) -REVIEW_TARGET_KINDS = frozenset({"task", "stage"}) -MATERIAL_CHANGE_CLASSES = frozenset( - {"material_redesign", "authority", "scope", "acceptance", "decomposition", "validation_allocation"} -) -EVIDENCE_CAUSAL_CLASSES = frozenset( - { - "claim_relevant_drift", - "implementation_defect", - "authority_plan_gap", - "evaluator_control_defect", - "non_claim_relevant", - } -) -EVIDENCE_CAUSAL_ROUTES: dict[str, tuple[str, str]] = { - "claim_relevant_drift": ("revalidate_claim", "route_current_owner"), - "implementation_defect": ("repair_task", "route_current_owner"), - "authority_plan_gap": ("repair_authority_plan", "route_current_owner"), - "evaluator_control_defect": ("repair_evaluator_control", "route_evaluator_control_owner"), - "non_claim_relevant": ("none", "diagnostic_only"), -} -EVIDENCE_CAUSAL_COMPARISONS = { - "claim_relevant_drift": "claim_relevant", - "implementation_defect": "claim_relevant", - "authority_plan_gap": "claim_relevant", - "evaluator_control_defect": "evaluator_only", - "non_claim_relevant": "unrelated", -} -PARTICIPATION_FIELDS = ( - "authorship", - "repair_participation", - "decision_participation", - "deliberation_participation", -) +from typing import Any, Mapping -ROUTES: dict[str, tuple[str, str, str]] = { - "specification_gap": ("specification", "specification_owner", "reopen_specification"), - "decomposition_gap": ("plan", "plan_owner", "repair_plan"), - "allocation_gap": ("plan", "plan_owner", "reslice_plan"), - "implementation_defect": ("implementation", "task_owner", "repair_task"), - "validation_oracle_defect": ("validation_oracle", "oracle_owner", "repair_oracle"), - "environment_failure": ("environment", "environment_owner", "recover_environment"), - "advisory_enhancement": ("implementation", "backlog_owner", "record_advisory"), -} -FINDING_CLASSES = frozenset(ROUTES) -FINDING_ARTIFACTS = frozenset( - {"specification", "plan", "task", "implementation", "validation_oracle", "environment"} -) -FINDING_OWNERS = frozenset( - {"specification_owner", "plan_owner", "task_owner", "oracle_owner", "environment_owner", "backlog_owner"} -) -FINDING_ACTIONS = frozenset(value[2] for value in ROUTES.values()) - -FINDING_KEYS = frozenset( - { - "finding_id", - "stage", - "class", - "severity", - "first_broken_artifact", - "obligation_basis", - "evidence", - "target_identity", - "summary", - "recommended_owner", - "disposition", - } +from artifact_store import ( + canonical_artifact_path, family_policy, load_catalog, read_artifact, + read_yaml_mapping, rebuild_index, ) -FINDING_V2_KEYS = frozenset( - { - "schema", "finding_id", "stage", "reviewer_observation", "evidence", - "target_identity", "summary", "controller_decision", - } -) -REVIEWER_OBSERVATION_KEYS = frozenset( - {"finding_id", "severity", "requirement_id", "boundary", "evidence", "expected", "observed", "owner"} -) -CONTROLLER_DECISION_KEYS = frozenset( - { - "classification", "first_broken_artifact", "affected_owner", "action", - "obligation_basis", "evidence_basis", - } -) -TARGET_KEYS = frozenset({"artifact_id", "revision", "sha256", "source_tree"}) -AFFECTED_REGION_KEYS = frozenset({"task_ids", "paths", "interfaces", "validation_oracles"}) -BINDING_IDENTITY_KEYS = frozenset({"binding_id", "sha256"}) -BASELINE_IDENTITY_KEYS = frozenset({"head", "tree"}) -PLAN_RETURN_KEYS = frozenset( - { - "finding_id", - "first_broken_artifact", - "return_to", - "action", - "execution_state", - "affected_region", - "returned_authority_identity", - "preserved_evidence_identities", - "resume_requires", - "original_binding_identity", - "original_baseline_identity", - "preserve_valid_work_and_evidence", - "silent_expansion_allowed", - } -) -EVIDENCE_ITEM_KEYS = frozenset({"kind", "locator", "digest_or_identity", "observation"}) -EVIDENCE_CAUSAL_CLASSIFICATION_KEYS = frozenset( - { - "observation_reference", - "accepted_authority_comparison", - "causal_class", - "affected_claim", - "affected_owner", - "authorized_lifecycle_action", - "disposition", - } -) -ACCEPTED_AUTHORITY_COMPARISON_KEYS = frozenset({"authority_identity", "result", "basis"}) -STAGE_REVIEW_KEYS = frozenset( - {"review_id", "review_mode", "review_target_kind", "repair_frontier", "review_reset", "stage", "target_identity", "reviewer", "evidence", "verdict", "findings", "started_at", "completed_at", "staleness"} -) -LEGACY_STAGE_REVIEW_KEYS = STAGE_REVIEW_KEYS - {"review_mode", "review_target_kind", "repair_frontier", "review_reset"} -REPAIR_FRONTIER_KEYS = frozenset( - {"prior_review_id", "blocking_finding_ids", "previous_reviewed_identity", "repaired_identity", "affected_boundaries", "frozen_evidence_reference"} -) -REVIEW_RESET_KEYS = frozenset({"prior_review_id", "reason_class", "reason"}) -REVIEWER_KEYS = frozenset( - {"agent_id", "capability", *PARTICIPATION_FIELDS, "context_origin"} +from core import now_date, resolve_workspace_root +from review_identity import ( + canonical_plan_tree_identity, + canonical_task_data, + load_canonical_plan_tree, ) -REVIEW_EVIDENCE_KEYS = frozenset({"mode", "capabilities", "unavailable_evidence", "commands", "artifacts"}) -COMMAND_KEYS = frozenset({"command_id", "purpose", "exit_code", "output_digest"}) -ARTIFACT_KEYS = frozenset({"path", "sha256"}) -STALENESS_KEYS = frozenset({"is_stale", "reason", "supersedes"}) - - -class ReviewContractError(ValueError): - pass - - -def reviewer_runtime_root(root: Path) -> Path: - """Controller-owned runtime location; never selected by a review envelope.""" - workspace_key = hashlib.sha256(str(root.resolve()).encode()).hexdigest() - return Path.home() / ".work-bundle/reviewer-runtime/workspaces" / workspace_key - - -def stage_target_identity(root: Path, stage: str, path: Path, *, source_root: Path | None = None) -> dict[str, Any]: - identity = artifact_review_identity(path) if stage == "specification" else plan_review_identity(root, path) - if stage == "integrated_implementation": - if source_root is None: - raise ReviewContractError("review provenance requires source repository") - result = subprocess.run(["git", "-C", str(source_root), "status", "--porcelain", "--untracked-files=all"], capture_output=True, text=True) - tree = subprocess.run(["git", "-C", str(source_root), "rev-parse", "HEAD^{tree}"], capture_output=True, text=True) - if result.returncode or result.stdout.strip() or tree.returncode: - raise ReviewContractError("review provenance requires clean source tree") - identity["source_tree"] = tree.stdout.strip() - return identity -_ACCEPTED_RESULT_FIELDS = { - "schema", "plan_id", "task_id", "binding_id", "baseline_identity", - "accepted_source", "authority_projection", "executor_result_digest", - "validation_evidence_ids", "review_id", "owner_identity", "accepted_at", - "invalidation", +CURRENT_CATALOG = Path(__file__).resolve().parents[2] / "references/assets/orchestration/contract/artifact-family-catalog-v5.yaml" +CURRENT_FAMILIES = {"implementation-review", "accepted-task-result", "final-workflow-review"} +CURRENT_STRUCTURAL_FIELDS = { + "artifact_type", "schema_version", "id", "plan_id", "task_id", + "target_sha256", "product_sha256", "knowledge_action", "date_created", "last_updated", } -_ACCEPTED_AUTHORITY_FIELDS = { - "task_digest", "binding_digest", "scope_digest", "validation_obligations_digest", - "required_review_digest", "ownership_digest", -} - - -def accepted_result_state_digest(accepted: Mapping[str, Any]) -> str: - """Recompute the compact accepted-result identity without importing execution runtime.""" - - source = _mapping(accepted.get("accepted_source"), "accepted task result source") - state = { - "plan_id": accepted.get("plan_id"), "task_id": accepted.get("task_id"), - "binding_id": accepted.get("binding_id"), - "baseline_identity": dict(_mapping(accepted.get("baseline_identity"), "accepted baseline")), - "accepted_source": {"head": source.get("head"), "tree": source.get("tree")}, - "authority_projection": dict(_mapping(accepted.get("authority_projection"), "accepted authority")), - } - if "knowledge_disposition" in accepted: - state["knowledge_disposition"] = dict( - _mapping(accepted.get("knowledge_disposition"), "accepted knowledge disposition") - ) - return hashlib.sha256( - json.dumps(state, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode() - ).hexdigest() - - -def _accepted_task_stage_evidence( - root: Path, - plan_id: str, - task_id: str, - task_path: Path, - *, - validate_native_receipt: bool, -) -> tuple[Path | None, Path | None, str | None]: - binding = root / ".work-bundle/runtime/execution" / plan_id / task_id / "execution-binding.json" - if binding.is_symlink() or not binding.is_file() or not binding.resolve().is_relative_to(root.resolve()): - return None, None, f"accepted_task_result_missing:{task_id}" - try: - from execution_context import assert_accepted_task_result_current, compile_task_authority - - payload = _mapping(json.loads(binding.read_text()), "task execution binding") - accepted = _mapping(payload.get("accepted_result"), "accepted task result") - fields = set(accepted) - if frozenset(fields) not in { - frozenset(_ACCEPTED_RESULT_FIELDS), - frozenset(_ACCEPTED_RESULT_FIELDS | {"knowledge_disposition"}), - }: - raise ReviewContractError("accepted task result shape is not closed") - source = _mapping(accepted.get("accepted_source"), "accepted task result source") - authority = _mapping(accepted.get("authority_projection"), "accepted task result authority") - ownership = _mapping(payload.get("ownership"), "task execution ownership") - observations = accepted.get("validation_evidence_ids") - if ( - accepted.get("schema") != "accepted-task-result-v1" - or accepted.get("plan_id") != plan_id or accepted.get("task_id") != task_id - or accepted.get("binding_id") != ownership.get("binding_id") - or payload.get("plan_id") != plan_id or payload.get("task_id") != task_id - or accepted.get("invalidation") is not None - or set(source) != {"head", "tree", "state_digest"} - or set(authority) != _ACCEPTED_AUTHORITY_FIELDS - or not isinstance(observations, list) or not observations - or any(not isinstance(item, str) or not item for item in observations) - or len(observations) != len(set(observations)) - or source.get("state_digest") != accepted_result_state_digest(accepted) - ): - raise ReviewContractError("accepted task result binding or evidence is invalid") - if validate_native_receipt: - try: - current_task = compile_task_authority(root, task_path) - except SystemExit as error: - raise ReviewContractError(str(error)) from error - try: - assert_accepted_task_result_current(current_task, payload, accepted) - except SystemExit as error: - raise ReviewContractError(str(error)) from error - review_path = None - if accepted.get("review_id"): - review_path = _review_store_path(root, str(accepted["review_id"])) - if review_path.exists(): - if review_path.is_symlink() or review_path.stat().st_mode & 0o222: - raise ReviewContractError("stored current task review is mutable") - review = _mapping(json.loads(review_path.read_text()), "stored current task review") - validated = _validated_review_envelope(review) - if ( - validated.review_id != accepted["review_id"] - or review.get("review_target_kind") != "task" - or validated.verdict != "accepted" - or validated.target_identity.get("artifact_id") != task_id - or validated.target_identity.get("revision") != source.get("head") - or validated.target_identity.get("source_tree") != source.get("tree") - ): - raise ReviewContractError("stored current task review does not bind accepted source") - if validate_native_receipt: - _validate_reviewer_run(root, review) - else: - # Accepted tasks predating native publication remain authoritative. - review_path = None - return binding, review_path, None - except (OSError, ValueError, TypeError, ReviewContractError): - return binding, None, f"accepted_task_result_invalid:{task_id}" - - -def stage_evidence_requirements( - root: Path, stage: str, target: Path, *, validate_native_receipts: bool = True -) -> tuple[dict[str, str], list[str]]: - """Derive the stage's evidence closure, not a caller-selected context projection. - - Carried knowledge constraints are authority in the specification itself. Their - protected origin notes are deliberately not retrieved. Validation acceptance - remains owned by the existing lifecycle gates; here we require its evidence - to be available to the reviewer, including the native observation store. - """ - required: dict[str, str] = {} - missing: list[str] = [] - - def control(path: Path, role: str) -> None: - if path.is_symlink() or not path.resolve().is_relative_to(root.resolve()): - raise ReviewContractError("stage evidence path escapes control store") - required["control:" + path.relative_to(root).as_posix()] = role - - control(target, "target") - data, _ = _read_structured(target) - members = [target] - specifications = [target] if stage == "specification" else [] - if stage != "specification": - for path in sorted((root / ".work-bundle/orchestration/plan").rglob("*.md")): - path = _input_path(path, root, root / ".work-bundle/orchestration/plan", "stage plan member") - item, _ = _read_structured(path) - if item.get("plan_id") == data.get("id"): - control(path, "plan_member") - members.append(path) - for member in members: - item, _ = _read_structured(member) - for spec in _resolve_spec_paths(root, item, data): - if spec not in specifications: - specifications.append(spec) - control(spec, "verified_specification") - if _read_structured(spec)[0].get("status") != "verified": - missing.append("verified_specification:" + spec.relative_to(root).as_posix()) - for spec in specifications: - item, _ = _read_structured(spec) - for authority in _as_list(item.get("source_knowledge")): - if not isinstance(authority, dict) or not str(authority.get("constraint", "")).strip(): - missing.append("carried_authority:" + spec.relative_to(root).as_posix()) - basis = item.get("truth_basis", {}) - if isinstance(basis, dict): - for reference in _as_list(basis.get("as_is_evidence")): - # File locators are explicit inputs, not prose or arbitrary URLs. - if not isinstance(reference, str): - missing.append("unsupported_truth_basis_reference") - continue - locator = reference if reference.startswith(("source:", "control:")) else "source:" + reference - scope, relative = locator.split(":", 1) - path = Path(relative) - if path.is_absolute() or ".." in path.parts or not relative or scope not in {"source", "control"}: - missing.append("unsupported_truth_basis_reference") - elif scope == "control": - control(root / path, "truth_basis") - else: - required[locator] = "truth_basis" - if stage == "integrated_implementation": - for member in members: - item, _ = _read_structured(member) - if not item.get("validation"): - continue - task_id = str(item.get("id") or "") - binding, review, failure = _accepted_task_stage_evidence( - root, str(data.get("id") or ""), task_id, member, - validate_native_receipt=validate_native_receipts, - ) - if failure: - missing.append(failure) - continue - assert binding is not None - control(binding, "accepted_task_result") - if review is not None: - control(review, "accepted_task_review") - # Preserve native identities/receipts; do not invent a parallel validation store. - path = root / ".work-bundle/runtime/completion-provenance/completion-provenance-v1.json" - if path.is_file(): - control(path, "validation_observation") - return required, sorted(set(missing)) - - -def source_snapshot_entries(source_root: Path) -> list[dict[str, str]]: - """Exact committed regular-file tree; symlinks/submodules fail closed.""" - result = subprocess.run(["git", "-C", str(source_root), "ls-tree", "-rz", "HEAD"], capture_output=True) - if result.returncode: - raise ReviewContractError("stage evidence source tree unavailable") - entries = [] - for row in result.stdout.split(b"\0"): - if not row: - continue - header, raw_path = row.split(b"\t", 1) - mode, kind, oid = header.decode().split() - path = raw_path.decode("utf-8") - if kind != "blob" or mode not in {"100644", "100755"}: - raise ReviewContractError("stage evidence snapshot requires regular files (no symlinks/submodules)") - entries.append({"locator": "source:" + path, "git_mode": mode, "git_blob": oid}) - return entries -def snapshot_tree_identity(entries: list[dict[str, str]]) -> str: - tree: dict[str, Any] = {} - for entry in entries: - locator = entry["locator"] - if not locator.startswith("source:") or entry["git_mode"] not in {"100644", "100755"}: - raise ReviewContractError("invalid source snapshot entry") - parts = locator[7:].split("/") - if any(part in {"", ".", ".."} for part in parts): - raise ReviewContractError("invalid source snapshot path") - node = tree - for part in parts[:-1]: - node = node.setdefault(part, {}) - if not isinstance(node, dict): - raise ReviewContractError("source snapshot path collision") - if parts[-1] in node: - raise ReviewContractError("source snapshot duplicate path") - node[parts[-1]] = (entry["git_mode"], entry["git_blob"]) - - def digest(node: dict[str, Any]) -> str: - rows = [] - for name, value in node.items(): - directory = isinstance(value, dict) - mode, oid = ("40000", digest(value)) if directory else value - rows.append((name.encode() + (b"/" if directory else b""), mode.encode() + b" " + name.encode() + b"\0" + bytes.fromhex(oid))) - content = b"".join(row for _, row in sorted(rows)) - return hashlib.sha1(b"tree " + str(len(content)).encode() + b"\0" + content).hexdigest() - return digest(tree) - - -def stage_evidence_manifest(root: Path, source_root: Path, context: Mapping[str, Any], artifacts: list[dict[str, Any]]) -> dict[str, Any]: - locator = str(context["target_locator"]) - if not locator.startswith("control:"): - raise ReviewContractError("stage evidence target must be control-local") - target = root / locator[8:] - if context.get("review_mode", "initial") == "repair": - # Repair packets carry the repaired target and a compact immutable - # reference to prior evidence. Full durable history remains lazy. - required, missing = {locator: "target"}, [] - else: - required, missing = stage_evidence_requirements(root, str(context["stage"]), target) - source = (source_snapshot_entries(source_root) - if context["stage"] == "integrated_implementation" and context.get("review_mode", "initial") == "initial" - else []) - for entry in source: - required.setdefault(entry["locator"], "source_tree") - available = {item["locator"]: item for item in artifacts} - for entry in source: - path = source_root / entry["locator"][7:] - content = path.read_bytes() - blob = hashlib.sha1(b"blob " + str(len(content)).encode() + b"\0" + content).hexdigest() - if path.is_symlink() or blob != entry["git_blob"]: - raise ReviewContractError("stage evidence source bytes differ from committed tree") - entries = [] - for key, role in sorted(required.items()): - if key not in available: - missing.append(key) - continue - entry = {"locator": key, "role": role, "sha256": available[key]["sha256"]} - if role in {"target", "plan_member", "verified_specification"}: - path = root / key[8:] - entry["identity"] = _stage_authority_identity( - path, stage=str(context["stage"]), role=role - ) - entries.append(entry) - return {"schema": "stage-evidence-manifest-v1", "stage": context["stage"], - "target_identity": context["target_identity"], "entries": entries, - "source_tree": source, "missing": sorted(set(missing)), - **({"repair_frontier_reference": context["repair_frontier"]["frozen_evidence_reference"]} - if context.get("review_mode") == "repair" else {})} - - -def _stage_authority_identity( - path: Path, - *, - stage: str, - role: str, - content: str | None = None, -) -> dict[str, Any]: - if stage != "specification" and role in {"target", "plan_member"}: - identity = artifact_review_identity(path, content=content) - identity["sha256"] = plan_artifact_projection_digest( - structural_plan_artifact_v2(path, content=content) - ) - return identity - return artifact_review_identity(path, content=content) - - -def validate_stage_evidence( - root: Path, - context: Mapping[str, Any], - packet: Mapping[str, Any], - *, - frozen_control_evidence: Mapping[str, str] | None = None, -) -> None: - manifest = packet.get("stage_evidence_manifest") - if (not isinstance(manifest, dict) or manifest.get("schema") != "stage-evidence-manifest-v1" - or manifest.get("stage") != context["stage"] or manifest.get("target_identity") != context["target_identity"] - or manifest.get("missing") != [] or context.get("evidence_mode") != "reproducible_snapshot"): - raise ReviewContractError("stage evidence requires a complete reproducible snapshot") - target = str(context["target_locator"]) - if not target.startswith("control:"): - raise ReviewContractError("stage evidence target must be control-local") - if context.get("review_mode", "initial") == "repair": - frontier = _repair_frontier(context.get("repair_frontier")) - if manifest.get("repair_frontier_reference") != frontier["frozen_evidence_reference"]: - raise ReviewContractError("repair stage evidence does not bind frozen evidence") - required, missing = {target: "target"}, [] - else: - required, missing = stage_evidence_requirements( - root, str(context["stage"]), root / target[8:], validate_native_receipts=False - ) - source = manifest.get("source_tree", []) - if context["stage"] == "integrated_implementation" and context.get("review_mode", "initial") == "initial": - if snapshot_tree_identity(source) != context["target_identity"]["source_tree"]: - raise ReviewContractError("stage evidence source snapshot is incomplete") - for entry in source: - required.setdefault(entry["locator"], "source_tree") - elif source: - raise ReviewContractError("unexpected source snapshot") - entries = {entry["locator"]: entry for entry in manifest.get("entries", [])} - artifacts = {entry["locator"]: entry for entry in packet["artifacts"]} - if missing or set(entries) != set(required) or len(entries) != len(manifest["entries"]): - raise ReviewContractError("stage evidence closure is incomplete") - for locator, role in required.items(): - entry = entries[locator] - if entry.get("role") != role or locator not in artifacts or entry.get("sha256") != artifacts[locator].get("sha256"): - raise ReviewContractError("stage evidence artifact binding mismatch") - if role in {"target", "plan_member", "verified_specification"}: - path = root / locator[8:] - current_identity = _stage_authority_identity( - path, stage=str(context["stage"]), role=role - ) - if entry.get("identity") == current_identity: - continue - frozen_content = (frozen_control_evidence or {}).get(locator) - if frozen_content is None: - raise ReviewContractError("stage evidence authority identity changed") - if ( - hashlib.sha256(frozen_content.encode()).hexdigest() - != artifacts[locator].get("sha256") - ): - raise ReviewContractError("stage evidence artifact binding mismatch") - frozen_raw_identity = artifact_review_identity(path, content=frozen_content) - frozen_semantic_identity = _stage_authority_identity( - path, - stage=str(context["stage"]), - role=role, - content=frozen_content, - ) - if ( - entry.get("identity") != frozen_raw_identity - or frozen_semantic_identity != current_identity - ): - raise ReviewContractError("stage evidence authority identity changed") - - -def _known_execution_ids(root: Path, stage: str, identity: Mapping[str, Any]) -> set[str]: - area = root / ".work-bundle/orchestration" / ("spec" if stage == "specification" else "plan") - ids: set[str] = set() - for path in area.rglob("*.md"): - if not path.resolve().is_relative_to(area.resolve()): - raise ReviewContractError("review provenance artifact path escapes store") - data, _ = _read_structured(path) - if identity["artifact_id"] not in {str(data.get("id", "")), str(data.get("plan_id", ""))}: - continue - for field in ("execution_id", "author_execution_id", "repair_execution_id", "author_execution_ids", "repair_execution_ids"): - value = data.get(field, []) - ids.update(str(item) for item in (value if isinstance(value, list) else [value]) if item) - if stage != "specification": - bindings = root / ".work-bundle/runtime/execution" / str(identity["artifact_id"]) - for path in bindings.glob("*/execution-binding.json"): - if not path.resolve().is_relative_to(bindings.resolve()): - raise ReviewContractError("review provenance binding path escapes store") - binding = json.loads(path.read_text()) - if binding.get("execution_id"): - ids.add(str(binding["execution_id"])) - return ids - - -def _known_task_execution_ids(root: Path, task_id: str) -> set[str]: - ids: set[str] = set() - runtime = root / ".work-bundle/runtime/execution" - for path in runtime.glob(f"*/{task_id}/execution-binding.json"): - if not path.resolve().is_relative_to(runtime.resolve()): - raise ReviewContractError("task review provenance binding path escapes store") - binding = json.loads(path.read_text()) - for field in ("execution_id",): - if binding.get(field): - ids.add(str(binding[field])) - ownership = binding.get("ownership") if isinstance(binding.get("ownership"), Mapping) else {} - for field in ("run_id", "agent_id"): - if ownership.get(field): - ids.add(str(ownership[field])) - return ids - - -def _native_reviewer_module(): - path = Path(__file__).resolve().parents[1] / "work-bundle/reviewer_workspace.py" - existing = sys.modules.get("reviewer_workspace") - if existing is not None: - if Path(existing.__file__).resolve() != path: - raise ReviewContractError("native reviewer module collision") - return existing - spec = importlib.util.spec_from_file_location("reviewer_workspace", path) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -def _validate_native_run_proof(receipt, packet, result, path, immutable_file, canonical): - """Re-derive observed host identity/result from immutable actual run input/output.""" - runtime = _native_reviewer_module() - try: - stdout = immutable_file(path.with_suffix(".stdout.jsonl")) - stderr = immutable_file(path.with_suffix(".stderr.txt")) - request_bytes = immutable_file(path.with_suffix(".request.json")) - request = json.loads(request_bytes) - controller_path = path.with_suffix(".controller.json") - controller_evidence = ( - json.loads(immutable_file(controller_path)) if controller_path.exists() else None - ) - combined_evidence = [ - *request.get("evidence", []), - *(controller_evidence or []), - ] - launch = json.loads(immutable_file(path.with_suffix(".launch.json"))) - argv = launch["argv"] - host_id, worker = runtime.parse_native_reviewer_transcript(stdout.decode(), stderr.decode()) - if (receipt.get("host_run_id") != host_id or receipt.get("isolation") != runtime.NATIVE_ISOLATION - or receipt.get("stdout_sha256") != hashlib.sha256(stdout).hexdigest() - or receipt.get("stderr_sha256") != hashlib.sha256(stderr).hexdigest() - or receipt.get("request_sha256") != hashlib.sha256(request_bytes).hexdigest() - or receipt.get("argv_sha256") != canonical(argv) - or receipt.get("executable_sha256") != launch.get("executable_sha256") - or not SHA256_RE.fullmatch(str(receipt.get("executable_sha256", ""))) - or not Path(argv[0]).is_absolute() - or argv != runtime._native_reviewer_argv(Path(argv[0]), Path(argv[9]), argv[11]) - or set(request) != {"instructions", "review_input", "evidence"} - or request["review_input"] != runtime._native_review_input( - packet, combined_evidence if controller_evidence is not None else None - ) or not isinstance(request["instructions"], str) - or not request["instructions"].strip()): - raise ValueError("native launch/input mismatch") - evidence = request["evidence"] - artifacts = runtime._native_review_artifacts(packet, combined_evidence) - if len(evidence) != len(artifacts): - raise ValueError("native input evidence mismatch") - for expected, actual in zip(artifacts, evidence): - if (set(actual) != {*expected, "content"} or any(actual[key] != value for key, value in expected.items()) - or hashlib.sha256(actual["content"].encode()).hexdigest() != expected["sha256"]): - raise ValueError("native input evidence mismatch") - if controller_evidence is not None: - controller_locators = runtime._controller_only_artifact_locators( - packet, combined_evidence - ) - controller_artifacts = [ - item for item in packet["artifacts"] - if item.get("locator") in controller_locators - ] - if len(controller_evidence) != len(controller_artifacts): - raise ValueError("native controller evidence mismatch") - for expected, actual in zip(controller_artifacts, controller_evidence): - if ( - set(actual) != {*expected, "content"} - or any(actual[key] != value for key, value in expected.items()) - or hashlib.sha256(actual["content"].encode()).hexdigest() != expected["sha256"] - ): - raise ValueError("native controller evidence mismatch") - key = "task_review_context" if result.get("review_target_kind", "stage") == "task" else "stage_review_context" - context = {**packet[key], "agent_id": host_id, "execution_id": host_id} - compact = key == "task_review_context" or (context.get("stage") == "integrated_implementation" and "task_review" in worker) - if compact: - observed = runtime._task_product_judgment_review( - worker, review_id=receipt["review_id"], context=context, packet=packet, - started_at=receipt["started_at"], completed_at=receipt["completed_at"], - previous_review=result.get("previous_review"), integrated_stage=key == "stage_review_context") - else: - observed = runtime._stage_product_judgment_review( - worker, review_id=receipt["review_id"], context=context, packet=packet, - started_at=receipt["started_at"], completed_at=receipt["completed_at"], - previous_review=result.get("previous_review")) - if ( - observed != _reviewer_bound_review(result) - or receipt.get("review_result") != observed - or receipt.get(key) != context - ): - raise ValueError("native judgment/result mismatch") - frozen_control_evidence: dict[str, str] = {} - for item in combined_evidence: - locator = item.get("locator") - content = item.get("content") - if not isinstance(locator, str) or not locator.startswith("control:"): - continue - if not isinstance(content, str) or locator in frozen_control_evidence: - raise ValueError("native control evidence is invalid") - frozen_control_evidence[locator] = content - return context, frozen_control_evidence - except (ValueError, TypeError, KeyError, IndexError, AttributeError, runtime.ReviewerWorkspaceError) as error: - raise ReviewContractError("native reviewer-run provenance does not bind this accepted review") from error - - -def _validate_reviewer_run(root: Path, review: Mapping[str, Any]) -> None: - reference = review.get("reviewer_run") - if not isinstance(reference, dict) or set(reference) != {"run_id", "sha256"}: - raise ReviewContractError("reviewer-run provenance receipt is required") - run_id = _identifier(reference["run_id"], "reviewer_run.run_id") - if not re.fullmatch(r"reviewer-run-[0-9a-f-]{36}", run_id): - raise ReviewContractError("reviewer-run provenance identity is invalid") - runtime = reviewer_runtime_root(root).resolve() - path = runtime / "receipts/reviewer-process" / f"{run_id}.json" - def immutable_file(target: Path) -> bytes: - if (target.is_symlink() or not target.resolve().is_relative_to(runtime) - or not target.is_file() or target.stat().st_mode & 0o222): - raise ReviewContractError("reviewer-run provenance is missing or mutable") - return target.read_bytes() - raw = immutable_file(path) - if hashlib.sha256(raw).hexdigest() != reference["sha256"]: - raise ReviewContractError("reviewer-run provenance receipt digest mismatch") - receipt = _mapping(json.loads(raw), "reviewer-run provenance receipt") - started = _rfc3339_utc(receipt.get("started_at"), "reviewer receipt started_at") - completed = _rfc3339_utc(receipt.get("completed_at"), "reviewer receipt completed_at") - if started > completed or completed > datetime.now(timezone.utc): - raise ReviewContractError("reviewer-run provenance has invalid completion time") - packet = _mapping(json.loads(immutable_file(path.with_suffix(".packet.json"))), "reviewer-run provenance packet") - def canonical(value: Any) -> str: - return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()).hexdigest() - result = {key: value for key, value in review.items() if key != "reviewer_run"} - bound_result = _reviewer_bound_review(result) - receipt_result = receipt.get("review_result", result) - kind = str(review.get("review_target_kind") or "stage") - context_key = "task_review_context" if kind == "task" else "stage_review_context" - context = _mapping(receipt.get(context_key, {}), "reviewer-run provenance context") - native = receipt.get("schema") == "reviewer-native-receipt-v1" - packet_context = packet.get(context_key) - frozen_control_evidence: dict[str, str] = {} - if native: - packet_context, frozen_control_evidence = _validate_native_run_proof( - receipt, packet, result, path, immutable_file, canonical - ) - mode = "direct_source" if review["evidence"]["mode"] == "direct" else review["evidence"]["mode"] - review_context = { - "review_mode": review.get("review_mode", "initial"), - "review_target_kind": review.get("review_target_kind", "stage"), - "repair_frontier": review.get("repair_frontier"), - "review_reset": review.get("review_reset"), - } - receipt_context = { - "review_mode": context.get("review_mode", "initial"), - "review_target_kind": context.get("review_target_kind", "stage"), - "repair_frontier": context.get("repair_frontier"), - "review_reset": context.get("review_reset"), - } - if (receipt.get("schema") not in {"reviewer-process-receipt-v1", "reviewer-native-receipt-v1"} or receipt.get("run_id") != run_id - or receipt.get("review_id") != review["review_id"] or receipt.get("status") != "passed" - or receipt.get("exit_code") != 0 - or receipt.get("review_result_sha256") != canonical(receipt_result) - or ("review_result" in receipt and receipt_result != bound_result) - or ("review_result" not in receipt and result != bound_result) - or receipt.get("packet_sha256") != canonical(packet) or packet_context != context - or context.get("target_identity") != review["target_identity"] - or (kind == "stage" and context.get("stage") != review["stage"]) - or context.get("agent_id") != review["reviewer"]["agent_id"] - or context.get("evidence_mode") != review["reviewer"]["context_origin"] - or context.get("capability") != review["reviewer"]["capability"] or context.get("evidence_mode") != mode - or review_context != receipt_context - or (not native and receipt.get("isolation") != {"mechanism": "sandbox-exec", "network": "denied", "write_scope": "scratch"}) - or not context.get("execution_id") - or (not native and receipt.get("sandbox_profile_sha256") != hashlib.sha256(immutable_file(path.with_suffix(".profile.sb"))).hexdigest()) - or receipt.get("event_log_sha256") != hashlib.sha256(immutable_file(path.with_suffix(".events.jsonl"))).hexdigest()): - raise ReviewContractError("reviewer-run provenance does not bind this accepted review") - known = ( - _known_task_execution_ids(root, str(review["target_identity"]["artifact_id"])) - if kind == "task" - else _known_execution_ids(root, str(review["stage"]), review["target_identity"]) - ) - if kind == "stage": - validate_stage_evidence( - root, - context, - packet, - frozen_control_evidence=frozen_control_evidence, - ) - if run_id in known or context["execution_id"] in known: - raise ReviewContractError("reviewer-run provenance overlaps author/repair execution") - - -def artifact_review_identity(path: Path, *, content: str | None = None) -> dict[str, Any]: - """Semantic artifact identity; only lifecycle bookkeeping is non-semantic. - - Body, version, links, validation definitions and all other metadata remain bound. - This permits the approved status transition without invalidating its own review. - """ - text = path.read_text(encoding="utf-8") if content is None else content.rstrip() + "\n" - if not text.startswith("---\n") or "\n---\n" not in text[4:]: - raise SystemExit(f"stage review: missing artifact front matter: {path}") - raw, body = text[4:].split("\n---\n", 1) - metadata = parse_yaml_subset(raw) - if not isinstance(metadata, dict) or not metadata.get("id"): - raise SystemExit(f"stage review: missing artifact identity: {path}") - semantic = {key: value for key, value in metadata.items() if key not in { - "status", "last_updated", "updated_at", - }} - payload = json.dumps([semantic, body], sort_keys=True, default=str, separators=(",", ":")) - return {"artifact_id": str(metadata["id"]), "revision": str(metadata.get("version", "1")), - "sha256": hashlib.sha256(payload.encode()).hexdigest(), "source_tree": None} - - -def _plan_projection( - root: Path, - plan_path: Path, - *, - content: str | None, - artifact_projector: Any, - schema: str | None, -) -> dict[str, Any]: - """Build a plan graph projection with a selected versioned artifact projector.""" +def plan_review_identity(root: Path, plan_path: Path, *, content: str | None = None) -> dict[str, Any]: + """Digest one canonical, schema-valid plan tree.""" plan_root = root / ".work-bundle/orchestration/plan" if not plan_path.resolve().is_relative_to(plan_root.resolve()): - raise SystemExit("stage review: root plan escapes plan store") - root_projection = artifact_projector(plan_path, content=content) - plan_data = root_projection["metadata"] - plan_id = str(plan_data["id"]) - members = { - semantic_plan_member_key(plan_root, plan_path): plan_artifact_projection_digest( - root_projection - ) - } - for path in sorted(plan_root.rglob("*.md")): - if path == plan_path: - continue - if not path.resolve().is_relative_to(plan_root.resolve()): - raise SystemExit("stage review: plan member escapes plan store") - data, _ = _read_structured(path) - if str(data.get("plan_id", "")) != plan_id: - continue - members[semantic_plan_member_key(plan_root, path)] = plan_artifact_projection_digest( - artifact_projector(path) - ) - specifications = [ - artifact_review_identity(path) for path in _resolve_spec_paths(root, {}, plan_data) - ] - projection = { - "members": members, - "specifications": specifications, - } - if schema is not None: - projection["schema"] = schema - return projection - - -def legacy_semantic_plan_projection( - root: Path, plan_path: Path, *, content: str | None = None -) -> dict[str, Any]: - """Return the original projection exclusively for interpreting legacy identities.""" - - return _plan_projection( - root, - plan_path, - content=content, - artifact_projector=legacy_semantic_plan_artifact, - schema=None, - ) - - -def structural_plan_projection_v2( - root: Path, plan_path: Path, *, content: str | None = None -) -> dict[str, Any]: - """Return the documented V2 structural projection.""" - - return _plan_projection( - root, - plan_path, - content=content, - artifact_projector=structural_plan_artifact_v2, - schema=STRUCTURAL_PLAN_PROJECTION_SCHEMA, - ) - - -def semantic_plan_projection( - root: Path, plan_path: Path, *, content: str | None = None -) -> dict[str, Any]: - """Compatibility entry point; all current callers receive V2.""" - - return structural_plan_projection_v2(root, plan_path, content=content) - - -def _require_current_review(root: Path, stage: str, identity: Mapping[str, Any]) -> None: - """Consume one current direct record; publication owns historical verification.""" - accepted = [] - review_ids: set[str] = set() - unsupported_legacy = False - review_root = root / ".work-bundle/orchestration/reviews" - try: - for path in sorted(review_root.rglob("*")): - if path.suffix not in {".json", ".yaml", ".yml"} or not path.is_file(): - continue - if not path.resolve().is_relative_to(review_root.resolve()): - raise ReviewContractError("review record escapes review store") - value = _read_document(path) - if not isinstance(value, dict) or "review_id" not in value or "stage" not in value: - continue - if value["stage"] == stage: - review_key = str(value["review_id"]) - if review_key in review_ids: - raise ReviewContractError("stage review IDs must be globally unique") - review_ids.add(review_key) - # Old target records remain history, not current acceptance candidates. - if value.get("target_identity") == identity: - record = _validated_current_review_envelope(value) - raw_digest = hashlib.sha256(path.read_bytes()).hexdigest() - direct = _load_current_review_authority(root, value, raw_digest) - if direct is None: - try: - if path.stat().st_mode & 0o222: - raise ReviewContractError( - "legacy current review lacks direct immutable authority" - ) - _validate_legacy_current_record(value) - except ReviewContractError: - unsupported_legacy = True - continue - if record.stage == stage and record.target_identity == identity: - accepted.append(record) - latest_time = max((_rfc3339_utc(item.completed_at, "completed_at") for item in accepted), default=None) - latest = [item for item in accepted if _rfc3339_utc(item.completed_at, "completed_at") == latest_time] - if latest and all(item.verdict == "accepted" and not item.staleness["is_stale"] for item in latest): - return - except (ValueError, OSError) as error: - raise SystemExit(f"stage review blocked: {error}") from error - if unsupported_legacy: - raise SystemExit("stage review blocked: legacy current review lacks direct immutable authority") - raise SystemExit(f"stage review blocked: fresh accepted {stage} review required for {dict(identity)}") - - -def require_specification_review(root: Path, path: Path, *, content: str | None = None) -> None: - if not path.resolve().is_relative_to((root / ".work-bundle/orchestration/spec").resolve()): - raise SystemExit("stage review: specification escapes spec store") - _require_current_review(root, "specification", artifact_review_identity(path, content=content)) - - -def plan_review_identity(root: Path, plan_path: Path, *, content: str | None = None) -> dict[str, Any]: - projection = semantic_plan_projection(root, plan_path, content=content) - identity = artifact_review_identity(plan_path, content=content) - payload = json.dumps(projection, sort_keys=True, default=str) - identity["sha256"] = hashlib.sha256(payload.encode()).hexdigest() - return identity - - -def legacy_plan_review_identity( - root: Path, plan_path: Path, *, content: str | None = None -) -> dict[str, Any]: - """Interpret a pre-V2 identity without using it for new review writes.""" - - projection = legacy_semantic_plan_projection(root, plan_path, content=content) - identity = artifact_review_identity(plan_path, content=content) - payload = json.dumps(projection, sort_keys=True, default=str) - identity["sha256"] = hashlib.sha256(payload.encode()).hexdigest() - return identity - - -def require_plan_reviews(root: Path, plan_path: Path, *, source_root: Path | None = None, - content: str | None = None) -> None: - data = (_read_structured(plan_path)[0] if content is None - else parse_yaml_subset(content.split("---", 2)[1])) - for spec in _resolve_spec_paths(root, {}, data): - require_specification_review(root, spec) - identity = plan_review_identity(root, plan_path, content=content) - from execution_context import static_plan_task_admission - static_plan_task_admission(root, plan_path, content=content) - _require_current_review(root, "plan", identity) - if source_root is not None: - def git(*args: str) -> str: - result = subprocess.run(["git", "-C", str(source_root), *args], capture_output=True, text=True) - if result.returncode: - raise SystemExit("stage review: final source repository unavailable") - return result.stdout.strip() - if git("status", "--porcelain", "--untracked-files=all"): - raise SystemExit("stage review: final source must be clean, including untracked files") - final = dict(identity, source_tree=git("rev-parse", "HEAD^{tree}")) - _require_current_review(root, "integrated_implementation", final) - - -@dataclass(frozen=True) -class ReviewFindingV1: - finding_id: str - stage: str - finding_class: str - severity: str - first_broken_artifact: str - obligation_basis: str - evidence: tuple[Mapping[str, Any], ...] - target_identity: Mapping[str, Any] - summary: str - recommended_owner: str - disposition: str - - -@dataclass(frozen=True) -class ReviewFindingV2: - finding_id: str - stage: str - reviewer_observation: Mapping[str, Any] - evidence: tuple[Mapping[str, Any], ...] - target_identity: Mapping[str, Any] - summary: str - controller_decision: Mapping[str, Any] | None - - @property - def severity(self) -> str: - return str(self.reviewer_observation["severity"]) - - @property - def finding_class(self) -> str | None: - return ( - str(self.controller_decision["classification"]) - if self.controller_decision is not None else None - ) - - @property - def first_broken_artifact(self) -> str | None: - return ( - str(self.controller_decision["first_broken_artifact"]) - if self.controller_decision is not None else None - ) - - @property - def recommended_owner(self) -> str | None: - return ( - str(self.controller_decision["affected_owner"]) - if self.controller_decision is not None else None - ) - - @property - def disposition(self) -> str | None: - return ( - str(self.controller_decision["action"]) - if self.controller_decision is not None else None - ) - - -@dataclass(frozen=True) -class EvidenceCausalClassificationV1: - """Agent-authored causal judgment required before evidence can route action.""" - - observation_reference: str - accepted_authority_comparison: Mapping[str, str] - causal_class: str - affected_claim: str - affected_owner: str - authorized_lifecycle_action: str - disposition: str - - -@dataclass(frozen=True) -class StageReviewV1: - review_id: str - review_mode: str - review_target_kind: str - repair_frontier: Mapping[str, Any] | None - review_reset: Mapping[str, Any] | None - stage: str - target_identity: Mapping[str, Any] - reviewer: Mapping[str, Any] - evidence: Mapping[str, Any] - verdict: str - findings: tuple[ReviewFindingV1 | ReviewFindingV2, ...] - started_at: str - completed_at: str - staleness: Mapping[str, Any] - - -def _mapping(value: Any, name: str) -> Mapping[str, Any]: - if not isinstance(value, Mapping): - raise ReviewContractError(f"{name} must be an object") - return value - - -def _closed(record: Mapping[str, Any], required: frozenset[str], name: str) -> None: - missing = sorted(required - record.keys()) - unknown = sorted(record.keys() - required) - if missing: - raise ReviewContractError(f"{name} missing required fields: {', '.join(missing)}") - if unknown: - raise ReviewContractError(f"{name} contains unknown fields: {', '.join(unknown)}") - - -def _nonempty(value: Any, name: str) -> str: - if not isinstance(value, str) or not value.strip(): - raise ReviewContractError(f"{name} must be a non-empty string") - return value - - -def _identifier(value: Any, name: str) -> str: - text = _nonempty(value, name) - if not ID_RE.fullmatch(text): - raise ReviewContractError(f"{name} is not a valid id") - return text - - -def _enum(value: Any, allowed: frozenset[str] | set[str], name: str) -> str: - if value not in allowed: - raise ReviewContractError(f"{name} must be one of: {', '.join(sorted(allowed))}") - return str(value) - - -def _string_list(value: Any, name: str) -> list[str]: - if not isinstance(value, list) or any(not isinstance(item, str) or not item.strip() for item in value): - raise ReviewContractError(f"{name} must be a list of non-empty strings") - return value - - -def _rfc3339_utc(value: Any, name: str) -> datetime: - text = _nonempty(value, name) - if not text.endswith("Z"): - raise ReviewContractError(f"{name} must be RFC3339 UTC") - try: - return datetime.fromisoformat(text.removesuffix("Z") + "+00:00") - except ValueError as error: - raise ReviewContractError(f"{name} must be RFC3339 UTC") from error - - -def _target_identity(value: Any, name: str = "target_identity") -> Mapping[str, Any]: - target = _mapping(value, name) - _closed(target, TARGET_KEYS, name) - _identifier(target["artifact_id"], f"{name}.artifact_id") - _nonempty(target["revision"], f"{name}.revision") - if not isinstance(target["sha256"], str) or not SHA256_RE.fullmatch(target["sha256"]): - raise ReviewContractError(f"{name}.sha256 must be a lowercase SHA-256") - source_tree = target["source_tree"] - if source_tree is not None and (not isinstance(source_tree, str) or not GIT_OID_RE.fullmatch(source_tree)): - raise ReviewContractError(f"{name}.source_tree must be a Git object id or null") - return target - - -def review_evidence_identity(value: Mapping[str, Any]) -> str: - """Return a compact immutable identity for reusable review evidence.""" - record = _mapping(value, "review") - evidence = _mapping(record.get("evidence"), "evidence") - payload = {"review_id": record.get("review_id"), "evidence": evidence, - "reviewer_run": record.get("reviewer_run")} - return hashlib.sha256( - json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode() - ).hexdigest() - - -def _repair_frontier(value: Any) -> Mapping[str, Any]: - frontier = _mapping(value, "repair_frontier") - _closed(frontier, REPAIR_FRONTIER_KEYS, "repair_frontier") - _identifier(frontier["prior_review_id"], "repair_frontier.prior_review_id") - finding_ids = _string_list(frontier["blocking_finding_ids"], "repair_frontier.blocking_finding_ids") - if not finding_ids or len(finding_ids) != len(set(finding_ids)): - raise ReviewContractError("repair_frontier.blocking_finding_ids must be non-empty and unique") - _target_identity(frontier["previous_reviewed_identity"], "repair_frontier.previous_reviewed_identity") - _target_identity(frontier["repaired_identity"], "repair_frontier.repaired_identity") - boundaries = _string_list(frontier["affected_boundaries"], "repair_frontier.affected_boundaries") - if not boundaries or len(boundaries) != len(set(boundaries)): - raise ReviewContractError("repair_frontier.affected_boundaries must be non-empty and unique") - reference = frontier["frozen_evidence_reference"] - if not isinstance(reference, str) or not SHA256_RE.fullmatch(reference): - raise ReviewContractError("repair_frontier.frozen_evidence_reference must be a lowercase SHA-256") - return frontier - - -def _review_reset(value: Any) -> Mapping[str, Any]: - reset = _mapping(value, "review_reset") - _closed(reset, REVIEW_RESET_KEYS, "review_reset") - _identifier(reset["prior_review_id"], "review_reset.prior_review_id") - _enum(reset["reason_class"], MATERIAL_CHANGE_CLASSES, "review_reset.reason_class") - _nonempty(reset["reason"], "review_reset.reason") - return reset - - -def classify_first_broken_owner(finding_class: str) -> tuple[str, str, str]: - try: - return ROUTES[finding_class] - except KeyError as error: - raise ReviewContractError(f"class is not classified: {finding_class}") from error - - -def validate_evidence_causal_classification( - value: Mapping[str, Any], -) -> EvidenceCausalClassificationV1: - """Validate an agent's classification; this helper never infers a semantic class.""" - - record = _mapping(value, "evidence causal classification") - _closed(record, EVIDENCE_CAUSAL_CLASSIFICATION_KEYS, "evidence causal classification") - observation = _nonempty(record["observation_reference"], "observation_reference") - comparison = _mapping(record["accepted_authority_comparison"], "accepted_authority_comparison") - _closed(comparison, ACCEPTED_AUTHORITY_COMPARISON_KEYS, "accepted_authority_comparison") - authority_identity = _nonempty( - comparison["authority_identity"], "accepted_authority_comparison.authority_identity" - ) - basis = _nonempty(comparison["basis"], "accepted_authority_comparison.basis") - causal_class = _enum(record["causal_class"], EVIDENCE_CAUSAL_CLASSES, "causal_class") - expected_comparison = EVIDENCE_CAUSAL_COMPARISONS[causal_class] - if comparison["result"] != expected_comparison: - raise ReviewContractError("accepted authority comparison does not support the causal class") - affected_claim = _nonempty(record["affected_claim"], "affected_claim") - affected_owner = _nonempty(record["affected_owner"], "affected_owner") - expected_action, expected_disposition = EVIDENCE_CAUSAL_ROUTES[causal_class] - if record["authorized_lifecycle_action"] != expected_action: - raise ReviewContractError("authorized lifecycle action does not match the causal class") - if record["disposition"] != expected_disposition: - raise ReviewContractError("classification disposition does not match the causal class") - return EvidenceCausalClassificationV1( - observation_reference=observation, - accepted_authority_comparison={ - "authority_identity": authority_identity, - "result": expected_comparison, - "basis": basis, - }, - causal_class=causal_class, - affected_claim=affected_claim, - affected_owner=affected_owner, - authorized_lifecycle_action=expected_action, - disposition=expected_disposition, - ) - - -def review_remains_current_after_observation( - classification: Mapping[str, Any], *, reviewed_claim: str -) -> bool: - """Apply a classification to one review claim without treating raw evidence as authority.""" - - record = validate_evidence_causal_classification(classification) - claim = _nonempty(reviewed_claim, "reviewed_claim") - return not ( - record.causal_class - in {"claim_relevant_drift", "implementation_defect", "authority_plan_gap"} - and record.affected_claim == claim + raise SystemExit("plan identity path escapes the canonical plan store") + if content is not None: + raise SystemExit("plan identity requires canonical stored authority") + root_data = read_yaml_mapping(plan_path) + plan_id = str(root_data.get("id") or "") + tree = load_canonical_plan_tree(root, plan_id) + lifecycle = str(tree["state"]) + expected = canonical_artifact_path( + _policy("root-plan"), + {"workspace_root": root}, + identity=plan_id, + state=lifecycle, ) + if expected.resolve() != plan_path.resolve(): + raise SystemExit("plan identity requires the canonical root plan path") + return canonical_plan_tree_identity(root, plan_id, state=lifecycle) -def _affected_region(value: Any) -> dict[str, list[str]]: - region = _mapping(value, "affected region") - _closed(region, AFFECTED_REGION_KEYS, "affected region") - result: dict[str, list[str]] = {} - for field in ("task_ids", "interfaces", "validation_oracles"): - items = _string_list(region[field], f"affected region.{field}") - if len(items) != len(set(items)) or any(not ID_RE.fullmatch(item) for item in items): - raise ReviewContractError(f"affected region.{field} must contain unique valid ids") - result[field] = list(items) - if not result["task_ids"]: - raise ReviewContractError("affected region.task_ids must be non-empty") - paths = _string_list(region["paths"], "affected region.paths") - if len(paths) != len(set(paths)): - raise ReviewContractError("affected region.paths must be unique") - for item in paths: - path = Path(item) - if path.is_absolute() or any(part in {"", ".", ".."} for part in path.parts): - raise ReviewContractError("affected region.paths must be canonical relative paths") - result["paths"] = list(paths) - return {field: result[field] for field in ("task_ids", "paths", "interfaces", "validation_oracles")} +def _workspace(args: argparse.Namespace) -> Path: + return resolve_workspace_root(args) -def _binding_identity(value: Any) -> dict[str, str]: - identity = _mapping(value, "binding identity") - _closed(identity, BINDING_IDENTITY_KEYS, "binding identity") - binding_id = _identifier(identity["binding_id"], "binding identity.binding_id") - digest = identity["sha256"] - if not isinstance(digest, str) or not SHA256_RE.fullmatch(digest): - raise ReviewContractError("binding identity.sha256 must be a lowercase SHA-256") - return {"binding_id": binding_id, "sha256": digest} +def _anchors(args: argparse.Namespace) -> dict[str, Path]: + return {"workspace_root": _workspace(args)} -def _baseline_identity(value: Any) -> dict[str, str]: - identity = _mapping(value, "baseline identity") - _closed(identity, BASELINE_IDENTITY_KEYS, "baseline identity") - if any( - not isinstance(identity[field], str) or not GIT_OID_RE.fullmatch(identity[field]) - for field in BASELINE_IDENTITY_KEYS - ): - raise ReviewContractError("baseline identity must contain exact Git head and tree ids") - return {"head": str(identity["head"]), "tree": str(identity["tree"])} +def _policy(family: str) -> dict[str, Any]: + if family not in CURRENT_FAMILIES and family not in { + "executor-result", "root-plan", "phase", "task" + }: + raise SystemExit(f"Unsupported current review family: {family}") + return family_policy(load_catalog(CURRENT_CATALOG), family) -def _validate_finding_evidence(value: Any) -> tuple[Mapping[str, Any], ...]: - if not isinstance(value, list): - raise ReviewContractError("evidence must be a list") - evidence: list[Mapping[str, Any]] = [] - for index, raw_item in enumerate(value): - item = _mapping(raw_item, f"evidence[{index}]") - _closed(item, EVIDENCE_ITEM_KEYS, f"evidence[{index}]") - _enum(item["kind"], {"authority", "source", "test", "runtime", "environment"}, f"evidence[{index}].kind") - for key in ("locator", "digest_or_identity", "observation"): - _nonempty(item[key], f"evidence[{index}].{key}") - evidence.append(item) - return tuple(evidence) +def _semantic_input(args: argparse.Namespace, family: str) -> dict[str, Any]: + data = read_yaml_mapping(Path(str(args.content_file))) + overrides = sorted(CURRENT_STRUCTURAL_FIELDS.intersection(data)) + if overrides: + raise SystemExit(f"{family} semantic input contains structural field override: " + ", ".join(overrides)) + return data -def _validate_review_finding_v1(value: Mapping[str, Any]) -> ReviewFindingV1: - record = _mapping(value, "review_finding_v1") - _closed(record, FINDING_KEYS, "review_finding_v1") - finding_id = _identifier(record["finding_id"], "finding_id") - stage = _enum(record["stage"], FINDING_STAGES, "stage") - finding_class = _enum(record["class"], set(ROUTES), "class") - severity = _enum(record["severity"], FINDING_SEVERITIES, "severity") - obligation_basis = _enum(record["obligation_basis"], OBLIGATION_BASES, "obligation_basis") - expected_artifact, expected_owner, expected_disposition = classify_first_broken_owner(finding_class) - if record["first_broken_artifact"] != expected_artifact or record["recommended_owner"] != expected_owner: - raise ReviewContractError("review finding routing does not match the first broken artifact and owner") - disposition = _enum( - record["disposition"], - {expected_disposition, *TERMINAL_FINDING_DISPOSITIONS}, - "disposition", - ) - evidence = _validate_finding_evidence(record["evidence"]) - if severity == "blocking" and (obligation_basis == "none" or not evidence): - raise ReviewContractError("blocking finding requires a non-none obligation basis and evidence") - if finding_class == "advisory_enhancement" and severity == "blocking": - raise ReviewContractError("advisory_enhancement cannot be blocking without accepted reclassification") - target = _target_identity(record["target_identity"]) - summary = _nonempty(record["summary"], "summary") - return ReviewFindingV1( - finding_id, - stage, - finding_class, - severity, - expected_artifact, - obligation_basis, - evidence, - target, - summary, - expected_owner, - disposition, - ) - - -def _validate_review_finding_v2(value: Mapping[str, Any]) -> ReviewFindingV2: - record = _mapping(value, "review_finding_v2") - _closed(record, FINDING_V2_KEYS, "review_finding_v2") - if record["schema"] != "review-finding-v2": - raise ReviewContractError("review_finding_v2.schema is invalid") - finding_id = _identifier(record["finding_id"], "finding_id") - stage = _enum(record["stage"], FINDING_STAGES, "stage") - observation = _mapping(record["reviewer_observation"], "reviewer_observation") - _closed(observation, REVIEWER_OBSERVATION_KEYS, "reviewer_observation") - if observation["finding_id"] != finding_id: - raise ReviewContractError("reviewer_observation finding_id does not match finding identity") - _enum(observation["severity"], FINDING_SEVERITIES, "reviewer_observation.severity") - for field in REVIEWER_OBSERVATION_KEYS - {"severity"}: - _nonempty(observation[field], f"reviewer_observation.{field}") - evidence = _validate_finding_evidence(record["evidence"]) - target = _target_identity(record["target_identity"]) - summary = _nonempty(record["summary"], "summary") - decision_raw = record["controller_decision"] - decision: Mapping[str, Any] | None = None - if decision_raw is not None: - supplied = _mapping(decision_raw, "controller_decision") - _closed(supplied, CONTROLLER_DECISION_KEYS, "controller_decision") - classification = _enum(supplied["classification"], FINDING_CLASSES, "controller_decision.classification") - artifact = _enum(supplied["first_broken_artifact"], FINDING_ARTIFACTS, "controller_decision.first_broken_artifact") - owner = _enum(supplied["affected_owner"], FINDING_OWNERS, "controller_decision.affected_owner") - action = _enum( - supplied["action"], {*FINDING_ACTIONS, *TERMINAL_FINDING_DISPOSITIONS}, - "controller_decision.action", - ) - obligation = _enum(supplied["obligation_basis"], OBLIGATION_BASES, "controller_decision.obligation_basis") - basis = _nonempty(supplied["evidence_basis"], "controller_decision.evidence_basis") - if observation["severity"] == "blocking" and (obligation == "none" or not evidence): - raise ReviewContractError("blocking controller decision requires obligation and evidence") - decision = { - "classification": classification, "first_broken_artifact": artifact, - "affected_owner": owner, "action": action, - "obligation_basis": obligation, "evidence_basis": basis, - } - return ReviewFindingV2( - finding_id=finding_id, stage=stage, reviewer_observation=dict(observation), - evidence=evidence, target_identity=target, summary=summary, - controller_decision=decision, - ) - - -def validate_review_finding(value: Mapping[str, Any]) -> ReviewFindingV1 | ReviewFindingV2: - """Validate v2 agent-owned findings or preserve the exact legacy v1 interpretation.""" - if value.get("schema") == "review-finding-v2": - return _validate_review_finding_v2(value) - return _validate_review_finding_v1(value) - - -def classify_review_observation( - value: Mapping[str, Any], decision: Mapping[str, Any] -) -> dict[str, Any]: - """Attach one controller-authored decision without inferring it from observation fields.""" - finding = _validate_review_finding_v2(value) - if finding.controller_decision is not None: - raise ReviewContractError("controller decision is already present") - classified = {**dict(value), "controller_decision": dict(decision)} - _validate_review_finding_v2(classified) - return classified - - -def apply_review_finding_decisions( - review: Mapping[str, Any], decisions: Mapping[str, Mapping[str, Any]] -) -> dict[str, Any]: - """Apply controller decisions to every unclassified v2 observation in one review.""" - record = dict(_mapping(review, "review")) - findings = record.get("findings") - if not isinstance(findings, list) or not isinstance(decisions, Mapping): - raise ReviewContractError("review findings and controller decisions must be collections") - required = { - str(item.get("finding_id")) - for item in findings - if isinstance(item, Mapping) - and item.get("schema") == "review-finding-v2" - and item.get("controller_decision") is None - } - if set(decisions) != required: - raise ReviewContractError("controller decisions must match unclassified v2 findings exactly") - classified = [] - for item in findings: - if isinstance(item, Mapping) and item.get("finding_id") in required: - classified.append(classify_review_observation(item, decisions[str(item["finding_id"])])) - else: - classified.append(item) - result = {**record, "findings": classified} - _validated_review_envelope(result) +def _bindings(family: str, *, plan_id: str, task_id: str | None = None) -> dict[str, str]: + result = {"plan": plan_id} + if family == "accepted-task-result" or (family == "implementation-review" and task_id): + if not task_id: + raise SystemExit(f"{family} requires task binding") + result["task"] = task_id return result -def _reviewer_bound_review(value: Mapping[str, Any]) -> dict[str, Any]: - """Project controller decisions out of a review while retaining reviewer observations.""" - record = dict(value) - findings = record.get("findings") - if isinstance(findings, list): - record["findings"] = [ - {**dict(item), "controller_decision": None} - if isinstance(item, Mapping) and item.get("schema") == "review-finding-v2" - else item - for item in findings - ] - return record - - -def _route_review_finding( - value: Mapping[str, Any], *, previous_scope_expansions: int = 0, - affected_region: Mapping[str, Any] | None = None, - unaffected_evidence_identities: Sequence[Mapping[str, Any]] = (), - original_binding_identity: Mapping[str, Any] | None = None, - original_baseline_identity: Mapping[str, Any] | None = None, - controller_decision: Mapping[str, Any] | None = None, -) -> dict[str, Any]: - candidate = ( - classify_review_observation(value, controller_decision) - if controller_decision is not None else value - ) - finding = validate_review_finding(candidate) - if isinstance(finding, ReviewFindingV2): - if finding.controller_decision is None: - raise ReviewContractError("v2 routing requires a controller decision") - expected_action = finding.disposition - assert expected_action is not None - else: - expected_action = ROUTES[finding.finding_class][2] - if finding.disposition in TERMINAL_FINDING_DISPOSITIONS: - raise ReviewContractError("terminal adjudicator disposition cannot authorize repair mutation") - if isinstance(finding, ReviewFindingV1) and finding.disposition != expected_action: - raise ReviewContractError("review finding routing disposition is invalid") - if ( - not isinstance(previous_scope_expansions, int) - or isinstance(previous_scope_expansions, bool) - or previous_scope_expansions < 0 - ): - raise ReviewContractError("previous_scope_expansions must be a non-negative integer") - result = { - "finding_id": finding.finding_id, - "first_broken_artifact": finding.first_broken_artifact, - "return_to": finding.recommended_owner, - "action": expected_action, - "execution_state": ( - "paused_for_reslice" if expected_action == "reslice_plan" else "returned_for_repair" - ), - "preserve_valid_work_and_evidence": True, - "silent_expansion_allowed": False, - } - if expected_action != "reslice_plan": - if ( - affected_region is not None - or unaffected_evidence_identities - or original_binding_identity is not None - or original_baseline_identity is not None - ): - raise ReviewContractError("affected region applies only to a plan reslice") - return result - - region = _affected_region(affected_region) - binding = _binding_identity(original_binding_identity) - baseline = _baseline_identity(original_baseline_identity) - preserved = [ - dict(_target_identity(item, "unaffected_evidence_identity")) - for item in unaffected_evidence_identities - ] - preserved_ids = [item["artifact_id"] for item in preserved] - if len(preserved_ids) != len(set(preserved_ids)) or set(region["task_ids"]).intersection(preserved_ids): - raise ReviewContractError("affected region and unaffected evidence must be disjoint and unambiguous") - result.update( - { - "affected_region": region, - "returned_authority_identity": dict(finding.target_identity), - "preserved_evidence_identities": preserved, - "resume_requires": "accepted_repaired_plan_authority", - "original_binding_identity": binding, - "original_baseline_identity": baseline, - } - ) - return result - - -def _validated_review_envelope(value: Mapping[str, Any]) -> StageReviewV1: - """Validate one bounded task-or-stage review without walking older history.""" - - if value.get("review_target_kind") == "task": - return validate_task_acceptance_review(value) - previous = value.get("previous_review") - envelope = {key: item for key, item in value.items() if key != "previous_review"} - current = validate_stage_review(envelope) - if current.review_mode == "repair": - if not isinstance(previous, Mapping) or "previous_review" in previous: - raise ReviewContractError("stage repair review requires exactly one previous_review") - return validate_review_sequence(envelope, previous_review=previous) - if current.review_reset is not None: - if not isinstance(previous, Mapping) or "previous_review" in previous: - raise ReviewContractError("stage reset review requires exactly one previous_review") - return validate_review_sequence( - envelope, - previous_review=previous, - material_change=str(current.review_reset["reason_class"]), - ) - return validate_review_sequence(envelope) - - -def _validated_current_review_envelope(value: Mapping[str, Any]) -> StageReviewV1: - """Interpret one digest-bound current record without predecessor traversal.""" - - record = {key: item for key, item in value.items() if key != "previous_review"} - if record.get("review_target_kind") == "task": - return validate_task_review_record(record) - return validate_stage_review(record) - - -def _validate_legacy_current_record(value: Mapping[str, Any]) -> None: - """Admit a legacy current record only when it retains its sealed run binding.""" - - reviewer_run = value.get("reviewer_run") - if ( - not isinstance(reviewer_run, Mapping) - or set(reviewer_run) != {"run_id", "sha256"} - or not isinstance(reviewer_run.get("run_id"), str) - or not ID_RE.fullmatch(str(reviewer_run["run_id"])) - or not isinstance(reviewer_run.get("sha256"), str) - or not SHA256_RE.fullmatch(str(reviewer_run["sha256"])) - ): - raise ReviewContractError( - "legacy current review lacks direct immutable authority" - ) - - -def _bounded_stage_predecessor(value: Mapping[str, Any]) -> dict[str, Any]: - """Project one stored predecessor without recursively embedding its history.""" - - return {key: item for key, item in value.items() if key != "previous_review"} - - -def _stage_review_predecessor_id(record: StageReviewV1) -> str | None: - if record.review_mode == "repair": - assert record.repair_frontier is not None - return str(record.repair_frontier["prior_review_id"]) - if record.review_reset is not None: - return str(record.review_reset["prior_review_id"]) - return None +def _available(args: argparse.Namespace, family: str, bindings: Mapping[str, str]) -> None: + policy = _policy(family) + for state in policy["lifecycle"]["states"]: + if canonical_artifact_path(policy, _anchors(args), identity=str(args.id), state=str(state), bindings=bindings).exists(): + raise SystemExit(f"{family} canonical identity collision: {args.id}") -def _validate_stored_stage_chain( - value: Mapping[str, Any], - historical: Mapping[str, Mapping[str, Any]], - *, - visiting: frozenset[str] = frozenset(), -) -> StageReviewV1: - """Validate bounded predecessor projections against complete stored history.""" - - record = _validated_review_envelope(value) - review_id = record.review_id - if review_id in visiting: - raise ReviewContractError("stage review predecessor chain contains a cycle") - predecessor_id = _stage_review_predecessor_id(record) - if predecessor_id is None: - return record - predecessor = historical.get(predecessor_id) - if predecessor is None: - raise ReviewContractError("stage re-review predecessor is missing") - _validate_stored_stage_chain( - predecessor, - historical, - visiting=visiting | {review_id}, - ) - supplied = value.get("previous_review") - if supplied != _bounded_stage_predecessor(predecessor): - raise ReviewContractError( - "stage re-review predecessor projection does not match stored predecessor" - ) - return record - - -def _stored_stage_history(root: Path, stage: str) -> dict[str, Mapping[str, Any]]: - review_root = root.expanduser().resolve() / ".work-bundle/orchestration/reviews" - historical: dict[str, Mapping[str, Any]] = {} - for path in sorted(review_root.rglob("*")): - if path.suffix not in {".json", ".yaml", ".yml"} or not path.is_file(): - continue - if not path.resolve().is_relative_to(review_root.resolve()): - raise ReviewContractError("review record escapes review store") - value = _read_document(path) - if not isinstance(value, dict) or value.get("stage") != stage or "review_id" not in value: - continue - review_id = str(value["review_id"]) - if review_id in historical: - raise ReviewContractError("stage review IDs must be globally unique") - historical[review_id] = value - return historical - - -def _review_store_path(root: Path, review_id: str) -> Path: - store = root.expanduser().resolve() / ".work-bundle/orchestration/reviews" - path = (store / f"{_identifier(review_id, 'review_id')}.json").resolve(strict=False) - if not path.is_relative_to(store.resolve()): - raise ReviewContractError("stored review path escapes review store") - return path - - -def current_review_authority_path(root: Path, review_id: str) -> Path: - """Return the safe v2 direct-authority path for diagnostics and tests.""" +def _write(args: argparse.Namespace, family: str, data: Mapping[str, Any], bindings: Mapping[str, str]) -> dict[str, Any]: + from artifact_store import write_artifact + _available(args, family, bindings) + today = now_date() + schema_id = str(_policy(family)["schema"]["id"]) try: - return _current_review_authority_path(root, review_id) - except ValueError as error: - raise ReviewContractError(str(error)) from error - - -def publish_review( - root: Path, - review: Mapping[str, Any], - *, - current_target_identity: Mapping[str, Any], -) -> dict[str, str]: - """Publish a natively receipted current task-or-stage review exactly once.""" - - record = dict(_mapping(review, "review publication")) - validated = _validated_review_envelope(record) - if validated.verdict == "repair" and any( - isinstance(item, ReviewFindingV2) and item.controller_decision is None - for item in validated.findings - ): - raise ReviewContractError( - "repair publication requires a controller decision for every v2 observation" - ) - if record.get("review_target_kind", "stage") == "stage" and _stage_review_predecessor_id(validated): - validated = _validate_stored_stage_chain( - record, _stored_stage_history(root, validated.stage) - ) - current = dict(_target_identity(current_target_identity, "current_target_identity")) - if validated.target_identity != current: - raise ReviewContractError("review publication target is not current") - authority = bounded_closure.resolve_working_workspace(root) - try: - if authority is not None: - bounded_closure.require_orchestration_admission( - authority, - operation=("round_completion" if validated.stage == "integrated_implementation" - and record.get("review_target_kind", "stage") == "stage" else "ordinary_new"), - flow_id=str(current.get("artifact_id") or ""), - ) - except bounded_closure.BoundedClosureError as error: - raise ReviewContractError(str(error)) from error - if ( - validated.stage == "integrated_implementation" - and record.get("review_target_kind", "stage") == "stage" - ): - try: - bounded_closure.review_round_publication_binding( - root, - review_id=validated.review_id, - target_identity=current, - ) - except bounded_closure.BoundedClosureError as error: - raise ReviewContractError(str(error)) from error - _validate_reviewer_run(root.expanduser().resolve(), record) - path = _review_store_path(root, validated.review_id) - content = (json.dumps(record, indent=2, sort_keys=True) + "\n").encode("utf-8") - digest = hashlib.sha256(content).hexdigest() - path.parent.mkdir(parents=True, exist_ok=True) - if path.exists(): - if path.is_symlink() or path.read_bytes() != content: - raise ReviewContractError("stored review identity collision") - else: - with path.open("xb") as stream: - stream.write(content) - path.chmod(0o444) - reference = {"review_id": validated.review_id, "sha256": digest} - try: - _write_current_review_authority(root, record, digest) - except ValueError as error: - raise ReviewContractError(str(error)) from error - if ( - validated.stage == "integrated_implementation" - and record.get("review_target_kind", "stage") == "stage" - ): - outcome = { - "accepted": "accepted", - "repair": "findings", - "blocked": "blocked", - }[validated.verdict] - try: - bounded_closure.complete_published_review_round( - root, - review_id=validated.review_id, - target_identity=current, - outcome=outcome, - review_reference=reference, - ) - except bounded_closure.BoundedClosureError as error: - raise ReviewContractError(str(error)) from error - return reference - + schema_version = int(schema_id.rsplit("-v", 1)[1]) + except (IndexError, ValueError) as error: + raise SystemExit(f"Current {family} schema has no numeric version: {schema_id}") from error + document = { + **dict(data), "artifact_type": family, "schema_version": schema_version, + "id": str(args.id), "plan_id": str(args.plan_id), + "task_id": getattr(args, "task_id", None), "date_created": today, "last_updated": today, + } + if family == "final-workflow-review": + document.pop("task_id", None) + return write_artifact(CURRENT_CATALOG, family, _anchors(args), document, state="active", bindings=bindings) -def load_stored_review( - root: Path, - reference: Mapping[str, Any], - *, - current_target_identity: Mapping[str, Any], -) -> tuple[dict[str, Any], StageReviewV1]: - """Load stored review authority and revalidate its receipt and current target.""" - if not isinstance(reference, Mapping) or set(reference) != {"review_id", "sha256"}: - raise ReviewContractError("stored review reference is required") - path = _review_store_path(root, str(reference.get("review_id") or "")) +def _validate_plan_authority(args: argparse.Namespace, data: Mapping[str, Any]) -> dict[str, Any]: + plan_id = str(args.plan_id) + current = canonical_plan_tree_identity(_workspace(args), plan_id) + supplied = data.get("plan_identity") + tree = load_canonical_plan_tree(_workspace(args), plan_id) if ( - path.is_symlink() - or not path.is_file() - or path.stat().st_mode & 0o222 + not isinstance(supplied, dict) + or supplied != current + or data.get("specification_id") != tree["root"].get("source_spec_id") ): - raise ReviewContractError("stored review is missing or mutable") - raw = path.read_bytes() - if hashlib.sha256(raw).hexdigest() != reference.get("sha256"): - raise ReviewContractError("stored review digest mismatch") - record = dict(_mapping(json.loads(raw), "stored review")) - validated = _validated_current_review_envelope(record) - try: - direct = _load_current_review_authority(root, record, str(reference["sha256"])) - if direct is None: - _validate_legacy_current_record(record) - except ValueError as error: - raise ReviewContractError(str(error)) from error - current = dict(_target_identity(current_target_identity, "current_target_identity")) - if validated.target_identity != current: - raise ReviewContractError("stored review target is not current") - return record, validated + raise SystemExit("plan/specification identity is stale") + return tree -def stored_review_target_identity( - root: Path, reference: Mapping[str, Any] -) -> dict[str, Any]: - """Read only a digest-bound target hint; this does not admit review authority.""" +def _rows(args: argparse.Namespace, family: str) -> list[dict[str, Any]]: + result = rebuild_index(CURRENT_CATALOG, family, _anchors(args)) + rows = [json.loads(line) for line in Path(str(result["path"])).read_text(encoding="utf-8").splitlines() if line] + plan_id, task_id = getattr(args, "plan_id", None), getattr(args, "task_id", None) + return [row for row in rows if (not plan_id or row.get("plan_id") == plan_id) and (not task_id or row.get("task_id") == task_id)] - if not isinstance(reference, Mapping) or set(reference) != {"review_id", "sha256"}: - raise ReviewContractError("stored review reference is required") - path = _review_store_path(root, str(reference.get("review_id") or "")) - if path.is_symlink() or not path.is_file() or path.stat().st_mode & 0o222: - raise ReviewContractError("stored review is missing or mutable") - raw = path.read_bytes() - if hashlib.sha256(raw).hexdigest() != reference.get("sha256"): - raise ReviewContractError("stored review digest mismatch") - record = _mapping(json.loads(raw), "stored review") - return dict(_target_identity(record.get("target_identity"), "stored review target_identity")) - -def route_stored_review_verdict( - root: Path, - review_reference: Mapping[str, Any], - *, - current_target_identity: Mapping[str, Any], - finding_id: str | None = None, - previous_scope_expansions: int = 0, - affected_region: Mapping[str, Any] | None = None, - unaffected_evidence_identities: Sequence[Mapping[str, Any]] = (), - original_binding_identity: Mapping[str, Any] | None = None, - original_baseline_identity: Mapping[str, Any] | None = None, - controller_decision: Mapping[str, Any] | None = None, -) -> dict[str, Any]: - """Expose a verdict or route one finding only from stored current authority.""" - - record, validated = load_stored_review( - root, review_reference, current_target_identity=current_target_identity - ) - if finding_id is None: - return { - "review_id": validated.review_id, - "verdict": validated.verdict, - "target_identity": dict(validated.target_identity), - } - matches = [ - item for item in record.get("findings", []) - if isinstance(item, Mapping) and item.get("finding_id") == finding_id - ] +def _reference(args: argparse.Namespace, family: str, identity: str, bindings: Mapping[str, str]) -> dict[str, Any]: + matches = [] + for state in _policy(family)["lifecycle"]["states"]: + path = canonical_artifact_path(_policy(family), _anchors(args), identity=identity, state=str(state), bindings=bindings) + if path.is_file(): + matches.append(read_artifact(CURRENT_CATALOG, family, _anchors(args), identity=identity, state=str(state), bindings=bindings)) if len(matches) != 1: - raise ReviewContractError("stored review does not contain exactly one selected finding") - return _route_review_finding( - matches[0], - previous_scope_expansions=previous_scope_expansions, - affected_region=affected_region, - unaffected_evidence_identities=unaffected_evidence_identities, - original_binding_identity=original_binding_identity, - original_baseline_identity=original_baseline_identity, - controller_decision=controller_decision, - ) - + raise SystemExit(f"Current {family} reference is not canonical: {identity}") + return matches[0] -# The public legacy name now enforces stored authority too. Pure classification tests -# use the explicitly private helper and cannot be mistaken for lifecycle routing. -route_review_verdict = route_stored_review_verdict +def _candidate_validator(): + path = Path(__file__).resolve().parents[1] / "work-bundle/reviewer_workspace.py" + spec = importlib.util.spec_from_file_location("_wb_current_candidate", path) + if spec is None or spec.loader is None: + raise RuntimeError("unable to load exact-candidate validator") + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module -def resume_plan_return( - value: Mapping[str, Any], *, - workspace_root: Path, - plan_path: Path, - current_binding_identity: Mapping[str, Any], - current_baseline_identity: Mapping[str, Any], - current_unaffected_evidence_identities: Sequence[Mapping[str, Any]], -) -> dict[str, Any]: - """Admit a paused affected region only from repaired authority and unchanged evidence.""" - record = _mapping(value, "plan_return") - _closed(record, PLAN_RETURN_KEYS, "plan_return") - _identifier(record["finding_id"], "plan_return.finding_id") - if ( - record["first_broken_artifact"] != "plan" - or record["return_to"] != "plan_owner" - or record["action"] != "reslice_plan" - or record["execution_state"] != "paused_for_reslice" - or record["resume_requires"] != "accepted_repaired_plan_authority" - or record["preserve_valid_work_and_evidence"] is not True - or record["silent_expansion_allowed"] is not False - ): - raise ReviewContractError("plan return is not a paused bounded reslice") - region = _affected_region(record["affected_region"]) - original_binding = _binding_identity(record["original_binding_identity"]) - original_baseline = _baseline_identity(record["original_baseline_identity"]) - if _binding_identity(current_binding_identity) != original_binding: - raise ReviewContractError("original binding identity changed during bounded reslice") - if _baseline_identity(current_baseline_identity) != original_baseline: - raise ReviewContractError("original baseline identity changed during bounded reslice") - returned = dict( - _target_identity(record["returned_authority_identity"], "returned_authority_identity") - ) +def write_implementation_review(args: argparse.Namespace) -> dict[str, Any]: + data = _semantic_input(args, "implementation-review") + _validate_plan_authority(args, data) + reviewer = data.get("reviewer") + if not isinstance(reviewer, dict) or set(reviewer) != {"agent_id"}: + raise SystemExit("Implementation review requires one concrete reviewer identity") + implementor = str(data.get("implementor_agent_id") or "") + source_root = Path(str(getattr(args, "source_root", "") or "")) + if not implementor or not str(reviewer.get("agent_id") or "") or not source_root: + raise SystemExit("Implementation review requires concrete source, implementor, and reviewer identities") try: - repaired = dict(plan_review_identity(Path(workspace_root), Path(plan_path))) - _require_current_review(Path(workspace_root), "plan", repaired) - except (OSError, SystemExit, ValueError) as error: - raise ReviewContractError( - "resume requires current accepted repaired plan-review authority" - ) from error - if repaired["artifact_id"] != returned["artifact_id"] or repaired == returned: - raise ReviewContractError("resume requires new accepted repaired authority") - - preserved = [ - dict(_target_identity(item, "preserved_evidence_identity")) - for item in record["preserved_evidence_identities"] - ] - preserved_ids = [item["artifact_id"] for item in preserved] - if len(preserved_ids) != len(set(preserved_ids)) or set(region["task_ids"]).intersection(preserved_ids): - raise ReviewContractError("affected region and unaffected evidence must be disjoint and unambiguous") - current = [ - dict(_target_identity(item, "current_unaffected_evidence_identity")) - for item in current_unaffected_evidence_identities - ] - if current != preserved: - raise ReviewContractError("unaffected evidence identities changed during bounded reslice") - resumed = dict(record) - resumed["execution_state"] = "ready_from_repaired_authority" - resumed["returned_authority_identity"] = repaired - return resumed - - -def transition_review_finding( - value: Mapping[str, Any], disposition: str -) -> ReviewFindingV1: - finding = validate_review_finding(value) - if finding.disposition in TERMINAL_FINDING_DISPOSITIONS: - raise ReviewContractError("terminal review finding disposition cannot transition") - if disposition not in TERMINAL_FINDING_DISPOSITIONS: - raise ReviewContractError("finding lifecycle transition requires an adjudicator disposition") - updated = dict(value) - updated["disposition"] = disposition - return validate_review_finding(updated) - - -def validate_stage_review( - value: Mapping[str, Any], *, current_target_identity: Mapping[str, Any] | None = None -) -> StageReviewV1: - record = _mapping(value, "stage_review_v1") - envelope = {key: item for key, item in record.items() if key != "reviewer_run"} - keys = set(envelope) - if not LEGACY_STAGE_REVIEW_KEYS.issubset(keys) or not keys.issubset(STAGE_REVIEW_KEYS): - _closed(envelope, STAGE_REVIEW_KEYS, "stage_review_v1") - if "reviewer_run" in record: - reference = _mapping(record["reviewer_run"], "reviewer_run") - _closed(reference, frozenset({"run_id", "sha256"}), "reviewer_run") - _identifier(reference["run_id"], "reviewer_run.run_id") - if not isinstance(reference["sha256"], str) or not SHA256_RE.fullmatch(reference["sha256"]): - raise ReviewContractError("reviewer_run.sha256 must be a lowercase SHA-256") - review_id = _identifier(record["review_id"], "review_id") - review_mode = _enum(record.get("review_mode", "initial"), REVIEW_MODES, "review_mode") - review_target_kind = _enum(record.get("review_target_kind", "stage"), REVIEW_TARGET_KINDS, "review_target_kind") - raw_frontier = record.get("repair_frontier") - raw_reset = record.get("review_reset") - if review_mode == "repair": - if raw_frontier is None: - raise ReviewContractError("repair review requires repair_frontier") - if raw_reset is not None: - raise ReviewContractError("repair review cannot carry review_reset; require a fresh initial review") - repair_frontier = _repair_frontier(raw_frontier) - review_reset = None - else: - if raw_frontier is not None: - raise ReviewContractError("initial review cannot carry repair_frontier") - repair_frontier = None - review_reset = _review_reset(raw_reset) if raw_reset is not None else None - stage = _enum(record["stage"], STAGE_REVIEW_STAGES, "stage") - if review_target_kind != "stage": - raise ReviewContractError("stage_review_v1 review_target_kind must be stage") - target = _target_identity(record["target_identity"]) - if repair_frontier is not None and repair_frontier["repaired_identity"] != target: - raise ReviewContractError("repair frontier repaired identity must equal target_identity") - reviewer = _mapping(record["reviewer"], "reviewer") - _closed(reviewer, REVIEWER_KEYS, "reviewer") - _identifier(reviewer["agent_id"], "reviewer.agent_id") - _enum(reviewer["capability"], {"standard", "judgment"}, "reviewer.capability") - for field in PARTICIPATION_FIELDS: - _enum(reviewer[field], {"none", "present"}, f"reviewer.{field}") - _enum(reviewer["context_origin"], {"direct_source", "reproducible_snapshot", "packet_only", "carried_summary"}, "reviewer.context_origin") - - evidence = _mapping(record["evidence"], "evidence") - _closed(evidence, REVIEW_EVIDENCE_KEYS, "evidence") - _enum(evidence["mode"], {"direct_source", "reproducible_snapshot", "packet_only", "direct", "constrained_direct"}, "evidence.mode") - _string_list(evidence["capabilities"], "evidence.capabilities") - _string_list(evidence["unavailable_evidence"], "evidence.unavailable_evidence") - commands = evidence["commands"] - artifacts = evidence["artifacts"] - if not isinstance(commands, list) or not isinstance(artifacts, list): - raise ReviewContractError("evidence commands and artifacts must be lists") - for index, raw_command in enumerate(commands): - command = _mapping(raw_command, f"evidence.commands[{index}]") - _closed(command, COMMAND_KEYS, f"evidence.commands[{index}]") - _identifier(command["command_id"], f"evidence.commands[{index}].command_id") - _nonempty(command["purpose"], f"evidence.commands[{index}].purpose") - if not isinstance(command["exit_code"], int) or isinstance(command["exit_code"], bool): - raise ReviewContractError(f"evidence.commands[{index}].exit_code must be an integer") - if not isinstance(command["output_digest"], str) or not SHA256_RE.fullmatch(command["output_digest"]): - raise ReviewContractError(f"evidence.commands[{index}].output_digest must be a lowercase SHA-256") - for index, raw_artifact in enumerate(artifacts): - artifact = _mapping(raw_artifact, f"evidence.artifacts[{index}]") - _closed(artifact, ARTIFACT_KEYS, f"evidence.artifacts[{index}]") - _nonempty(artifact["path"], f"evidence.artifacts[{index}].path") - if not isinstance(artifact["sha256"], str) or not SHA256_RE.fullmatch(artifact["sha256"]): - raise ReviewContractError(f"evidence.artifacts[{index}].sha256 must be a lowercase SHA-256") - - verdict = _enum(record["verdict"], REVIEW_VERDICTS, "verdict") - findings_raw = record["findings"] - if not isinstance(findings_raw, list): - raise ReviewContractError("findings must be a list") - findings = tuple(validate_review_finding(_mapping(item, "finding")) for item in findings_raw) - started = _rfc3339_utc(record["started_at"], "started_at") - completed = _rfc3339_utc(record["completed_at"], "completed_at") - if completed < started: - raise ReviewContractError("completed_at must not precede started_at") - staleness = _mapping(record["staleness"], "staleness") - _closed(staleness, STALENESS_KEYS, "staleness") - if not isinstance(staleness["is_stale"], bool): - raise ReviewContractError("staleness.is_stale must be boolean") - reason = staleness["reason"] - supersedes = staleness["supersedes"] - if reason is not None: - _nonempty(reason, "staleness.reason") - if supersedes is not None: - _identifier(supersedes, "staleness.supersedes") - if staleness["is_stale"] and reason is None: - raise ReviewContractError("stale review requires staleness.reason") - if not staleness["is_stale"] and reason is not None: - raise ReviewContractError("current review cannot carry a staleness.reason") - if current_target_identity is not None: - current = _target_identity(current_target_identity, "current_target_identity") - changed = any(target[key] != current[key] for key in TARGET_KEYS) - if changed and not staleness["is_stale"]: - raise ReviewContractError("review target changed and the review must be stale") - if not changed and staleness["is_stale"]: - raise ReviewContractError("review target is unchanged but the review is marked stale") - if verdict == "accepted": - blocking = [item.finding_id for item in findings if item.severity == "blocking"] - if blocking: - raise ReviewContractError("accepted review cannot contain blocking findings") - for field in PARTICIPATION_FIELDS: - if reviewer[field] != "none": - raise ReviewContractError(f"accepted review requires reviewer.{field}: none") - if reviewer["context_origin"] not in {"direct_source", "reproducible_snapshot"}: - raise ReviewContractError("accepted review requires direct_source or reproducible_snapshot context") - if evidence["mode"] in {"packet_only", "constrained_direct"} or evidence["unavailable_evidence"]: - raise ReviewContractError("accepted review requires complete claim-relevant evidence, not packet-only or constrained evidence") - if evidence["mode"] == "reproducible_snapshot" or reviewer["context_origin"] == "reproducible_snapshot": - if evidence["mode"] != "reproducible_snapshot" or not artifacts: - raise ReviewContractError("snapshot review requires explicit reproducible_snapshot artifacts") - return StageReviewV1( - review_id, - review_mode, - review_target_kind, - repair_frontier, - review_reset, - stage, - target, - reviewer, - evidence, - verdict, - findings, - str(record["started_at"]), - str(record["completed_at"]), - staleness, - ) - - -def validate_review_sequence( - value: Mapping[str, Any], *, previous_review: Mapping[str, Any] | None = None, - material_change: str | None = None, - _task_owned_finding_subset: bool = False, -) -> StageReviewV1: - """Bind a re-review to its exact predecessor without replaying history.""" - current = validate_stage_review(value) - if previous_review is None: - if current.review_mode == "repair" or current.review_reset is not None: - raise ReviewContractError("re-review requires the exact previous review") - return current - previous = validate_stage_review(previous_review) - if current.review_mode == "repair": - if material_change is not None: - _enum(material_change, MATERIAL_CHANGE_CLASSES, "material_change") - raise ReviewContractError("material change requires a fresh initial review") - frontier = current.repair_frontier - assert frontier is not None - if previous.verdict != "repair" or frontier["prior_review_id"] != previous.review_id: - raise ReviewContractError("repair frontier must bind the exact prior repair review") - if frontier["previous_reviewed_identity"] != previous.target_identity: - raise ReviewContractError("repair frontier previous reviewed identity does not match prior review") - blocking_findings = { - item.finding_id: item for item in previous.findings if item.severity == "blocking" - } - selected_findings = set(frontier["blocking_finding_ids"]) - if _task_owned_finding_subset: - if not selected_findings.issubset(blocking_findings): - raise ReviewContractError( - "task repair frontier contains unknown blocking finding IDs" - ) - if any( - blocking_findings[finding_id].recommended_owner != "task_owner" - or blocking_findings[finding_id].disposition != "repair_task" - for finding_id in selected_findings - ): - raise ReviewContractError( - "task repair frontier may select only task-owned blocking findings" - ) - elif selected_findings != set(blocking_findings): - raise ReviewContractError("repair frontier blocking finding IDs do not match prior review") - if frontier["frozen_evidence_reference"] != review_evidence_identity(previous_review): - raise ReviewContractError("repair frontier frozen evidence reference does not match prior review") - return current - if material_change is not None: - _enum(material_change, MATERIAL_CHANGE_CLASSES, "material_change") - reset = current.review_reset - if (reset is None or reset["prior_review_id"] != previous.review_id - or reset["reason_class"] != material_change): - raise ReviewContractError("material change requires a recorded fresh initial review reset") - if current.reviewer["capability"] != "judgment": - raise ReviewContractError("reset requires a capable independent judgment reviewer") - elif current.review_reset is not None: - raise ReviewContractError("review_reset requires a classified material change") - return current - - -def _task_review_as_stage(value: Mapping[str, Any]) -> dict[str, Any]: - """Adapt the existing task acceptance record to the native review validator.""" - record = _mapping(value, "task acceptance_review") - verdict = {"accept": "accepted", "repair": "repair", "blocked": "blocked"}.get(record.get("verdict")) - if verdict is None: - raise ReviewContractError("task acceptance_review verdict must be accept, repair, or blocked") - return { - "review_id": record.get("review_id"), - "review_mode": record.get("review_mode"), - "review_target_kind": "stage", - "repair_frontier": record.get("repair_frontier"), - "review_reset": record.get("review_reset"), - # This is an adapter discriminator, not a fourth stage-review seat. - "stage": "plan", - "target_identity": record.get("target_identity"), - "reviewer": record.get("reviewer"), - "evidence": record.get("evidence"), - "verdict": verdict, - "findings": record.get("findings"), - "started_at": record.get("started_at"), - "completed_at": record.get("completed_at"), - "staleness": record.get("staleness"), - **({"reviewer_run": record["reviewer_run"]} if "reviewer_run" in record else {}), - } - - -def validate_task_acceptance_review(value: Mapping[str, Any]) -> StageReviewV1: - """Validate a task acceptance review and its one bounded predecessor.""" - record = _mapping(value, "task acceptance_review") - current = validate_task_review_record(record) - mode = current.review_mode - previous = record.get("previous_review") - if mode == "repair": - if not isinstance(previous, Mapping): - raise ReviewContractError("task repair review requires its exact previous_review") - if "previous_review" in previous: - raise ReviewContractError("task repair review may carry exactly one previous_review; older history stays lazy") - if previous.get("review_target_kind") == "stage": - validated_previous = validate_stage_review(previous) - if validated_previous.stage != "integrated_implementation": - raise ReviewContractError( - "task repair review stage predecessor must be integrated_implementation" - ) - return validate_review_sequence( - _task_review_as_stage(record), - previous_review=previous, - _task_owned_finding_subset=True, - ) - validate_task_review_record(previous) - return validate_review_sequence(_task_review_as_stage(record), previous_review=_task_review_as_stage(previous)) - if record.get("review_reset") is not None: - if not isinstance(previous, Mapping): - raise ReviewContractError("task initial reset requires its exact previous_review") - validate_task_review_record(previous) - reset = _mapping(record["review_reset"], "review_reset") - return validate_review_sequence( - _task_review_as_stage(record), previous_review=_task_review_as_stage(previous), material_change=str(reset.get("reason_class")) + checked = _candidate_validator().validate_current_candidate_and_independence( + source_root, data.get("target"), + reviewer_agent_id=str(reviewer["agent_id"]), implementor_agent_id=implementor, ) - return validate_review_sequence(_task_review_as_stage(record)) - - -def validate_task_review_record(value: Mapping[str, Any]) -> StageReviewV1: - """Validate one native task-review record without traversing history.""" - record = _mapping(value, "task acceptance_review") - if (record.get("required") is not True or record.get("review_target_kind") != "task" - or record.get("reviewer_independent") is not True): - raise ReviewContractError( - "task acceptance_review requires required: true, reviewer_independent: true, and review_target_kind: task" + except ValueError as error: + raise SystemExit(str(error)) from error + data["target"] = checked["target"] + data["target_sha256"] = checked["target"]["sha256"] + if data.get("scope") == "task" and not getattr(args, "task_id", None): + raise SystemExit("Task implementation review requires task binding") + return _write(args, "implementation-review", data, _bindings("implementation-review", plan_id=str(args.plan_id), task_id=getattr(args, "task_id", None))) + + +def list_implementation_reviews(args: argparse.Namespace) -> list[dict[str, Any]]: + return _rows(args, "implementation-review") + + +def write_accepted_task_result(args: argparse.Namespace) -> dict[str, Any]: + data = _semantic_input(args, "accepted-task-result") + product, executor, review, knowledge = (data.get(key) for key in ("product_identity", "executor_result", "implementation_review", "knowledge_disposition")) + if not all(isinstance(value, dict) for value in (product, executor, knowledge)): + raise SystemExit("Accepted task result requires canonical product, executor, and knowledge facts") + task = canonical_task_data(_workspace(args), str(args.plan_id), str(args.task_id)) + acceptance_review = task.get("acceptance_review") + required = acceptance_review.get("required") if isinstance(acceptance_review, dict) else None + if type(required) is not bool: + raise SystemExit("Canonical task acceptance_review.required must be boolean") + if required and review is None: + raise SystemExit("Canonical task requires an implementation review") + executor_record = _reference(args, "executor-result", str(executor.get("id")), {"plan": str(args.plan_id), "task": str(args.task_id)}) + if executor_record["digest"] != executor.get("sha256"): + raise SystemExit("Accepted task result executor digest mismatch") + if review is not None: + if not isinstance(review, dict): + raise SystemExit("Accepted task result implementation review reference is invalid") + reviewed = _reference(args, "implementation-review", str(review.get("id")), _bindings("implementation-review", plan_id=str(args.plan_id), task_id=str(args.task_id))) + if reviewed["digest"] != review.get("sha256") or reviewed["data"].get("verdict") != "accept": + raise SystemExit("Accepted task result requires an exact accept implementation review") + if reviewed["data"].get("target_sha256") != product.get("sha256"): + raise SystemExit("Accepted task result product identity does not match review target") + data["product_sha256"] = product.get("sha256") + data["knowledge_action"] = knowledge.get("action") + return _write(args, "accepted-task-result", data, _bindings("accepted-task-result", plan_id=str(args.plan_id), task_id=str(args.task_id))) + + +def list_accepted_task_results(args: argparse.Namespace) -> list[dict[str, Any]]: + return _rows(args, "accepted-task-result") + + +def write_final_workflow_review(args: argparse.Namespace) -> dict[str, Any]: + data = _semantic_input(args, "final-workflow-review") + _validate_plan_authority(args, data) + candidate = data.get("candidate_identity") + if not isinstance(candidate, dict) or not isinstance(candidate.get("sha256"), str): + raise SystemExit("Final workflow review requires exact candidate identity") + seen_tasks: set[str] = set() + for reference in data.get("accepted_results", []): + if not isinstance(reference, dict) or not str(reference.get("task_id") or ""): + raise SystemExit("Final workflow review contains invalid accepted-result reference") + task_id = str(reference["task_id"]) + if task_id in seen_tasks: + raise SystemExit("Final workflow review duplicates an accepted task") + seen_tasks.add(task_id) + accepted = _reference( + args, "accepted-task-result", str(reference.get("id")), + {"plan": str(args.plan_id), "task": task_id}, ) - _enum(record.get("review_mode"), REVIEW_MODES, "task acceptance_review.review_mode") - return validate_stage_review(_task_review_as_stage(record)) - + if accepted["digest"] != reference.get("sha256"): + raise SystemExit("Final workflow review accepted-result digest mismatch") + for reference in data.get("accepted_reviews", []): + reviewed = _reference(args, "implementation-review", str(reference.get("id")), {"plan": str(args.plan_id)}) + if reviewed["digest"] != reference.get("sha256") or reviewed["data"].get("verdict") != "accept": + raise SystemExit("Final workflow review requires exact accepted implementation reviews") + data["target_sha256"] = candidate["sha256"] + return _write(args, "final-workflow-review", data, _bindings("final-workflow-review", plan_id=str(args.plan_id))) -def validate_stage_reviews( - values: Sequence[Mapping[str, Any]], *, - current_target_identities: Mapping[str, Mapping[str, Any]] | None = None, -) -> dict[str, StageReviewV1]: - if current_target_identities is None or set(current_target_identities) != STAGE_REVIEW_STAGES: - raise ReviewContractError("actual current target identities are required for all three stages") - records = [validate_stage_review(value) for value in values] - ids = [record.review_id for record in records] - if len(ids) != len(set(ids)): - raise ReviewContractError("stage review IDs must be globally unique") - raw_by_id = {record.review_id: value for record, value in zip(records, values)} - sequenced = [] - for record, value in zip(records, values): - if record.review_mode == "repair": - assert record.repair_frontier is not None - prior = raw_by_id.get(str(record.repair_frontier["prior_review_id"])) - if prior is None: - raise ReviewContractError("repair review predecessor is missing") - record = validate_review_sequence(value, previous_review=prior) - elif record.review_reset is not None: - prior = raw_by_id.get(str(record.review_reset["prior_review_id"])) - if prior is None: - raise ReviewContractError("initial review reset predecessor is missing") - record = validate_review_sequence( - value, previous_review=prior, material_change=str(record.review_reset["reason_class"]) - ) - sequenced.append(record) - countable: dict[str, StageReviewV1] = {} - for record in sorted((item for item in sequenced if item.review_target_kind == "stage"), key=lambda item: item.completed_at): - current = _target_identity(current_target_identities[record.stage], "current_target_identity") - if record.target_identity == current: - countable.pop(record.stage, None) - if record.verdict == "accepted" and record.staleness["is_stale"] is False and record.target_identity == current: - countable[record.stage] = record - if set(countable) != STAGE_REVIEW_STAGES or len(countable) != 3: - raise ReviewContractError("exactly three mandatory stage identities must have current accepted reviews") - return countable +def list_final_workflow_reviews(args: argparse.Namespace) -> list[dict[str, Any]]: + return _rows(args, "final-workflow-review") -def validate_contract_instance( - definition: str, value: Mapping[str, Any] -) -> ReviewFindingV1 | ReviewFindingV2 | StageReviewV1: - if definition in {"reviewFinding", "review_finding_v1"}: - return _validate_review_finding_v1(value) - if definition in {"reviewFindingV2", "review_finding_v2"}: - return _validate_review_finding_v2(value) - if definition == "API-001": - return validate_review_finding(value) - if definition in {"stageReview", "stage_review_v1", "API-002"}: - return validate_stage_review(value) - raise ReviewContractError(f"unsupported contract definition: {definition}") +def cmd_write_implementation_review(args: argparse.Namespace) -> None: + print(json.dumps(write_implementation_review(args), ensure_ascii=False, sort_keys=True)) -def _read_document(path: Path) -> Mapping[str, Any]: - text = path.read_text(encoding="utf-8") - try: - value = json.loads(text) - except json.JSONDecodeError: - try: - import yaml - except ImportError as error: - raise ReviewContractError("YAML input requires PyYAML") from error - value = yaml.safe_load(text) - return _mapping(value, str(path)) +def cmd_list_implementation_reviews(args: argparse.Namespace) -> None: + for row in list_implementation_reviews(args): print(json.dumps(row, ensure_ascii=False, sort_keys=True)) -def _validate_schema_definition(schema: Mapping[str, Any], definition: str, value: Mapping[str, Any]) -> None: - definitions = _mapping(schema.get("$defs"), "$defs") - if definition not in definitions: - raise ReviewContractError(f"schema definition not found: {definition}") - if definition in {"reviewFinding", "reviewFindingV2", "stageReview"}: - validate_contract_instance(definition, value) - try: - from jsonschema import Draft202012Validator - except ImportError as error: - if definition in {"reviewFinding", "reviewFindingV2", "stageReview"}: - return - raise ReviewContractError(f"contract definition requires jsonschema: {definition}") from error - document = dict(schema) - document["$ref"] = f"#/$defs/{definition}" - errors = sorted(Draft202012Validator(document).iter_errors(value), key=lambda item: list(item.path)) - if errors: - raise ReviewContractError(errors[0].message) +def cmd_write_accepted_task_result(args: argparse.Namespace) -> None: + print(json.dumps(write_accepted_task_result(args), ensure_ascii=False, sort_keys=True)) -def cmd_validate_contract(argv: list[str]) -> int: - parser = argparse.ArgumentParser(prog="review_runtime.py validate-contract") - parser.add_argument("--schema", type=Path, required=True) - parser.add_argument("--definition", required=True) - parser.add_argument("--instance", type=Path, required=True) - parsed = parser.parse_args(argv) - try: - schema = _read_document(parsed.schema) - instance = _read_document(parsed.instance) - _validate_schema_definition(schema, parsed.definition, instance) - except (OSError, ReviewContractError) as error: - print(json.dumps({"status": "blocked", "failure_code": "WB_REVIEW_CONTRACT_INVALID", "detail": str(error)}, sort_keys=True)) - return 1 - print(json.dumps({"status": "passed", "definition": parsed.definition, "instance": str(parsed.instance)}, sort_keys=True)) - return 0 - - -def cmd_assert_migration_stop(argv: list[str]) -> int: - """Deprecated compatibility alias for the isolated legacy validator.""" - - module_path = Path(__file__).with_name("legacy_wor107_migration.py") - spec = importlib.util.spec_from_file_location( - "_wb_legacy_wor107_migration", module_path - ) - if spec is None or spec.loader is None: - raise RuntimeError(f"unable to load legacy migration utility: {module_path}") - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - print(module.DEPRECATION_DIAGNOSTIC, file=sys.stderr) - return module.cmd_assert_migration_stop(argv) +def cmd_list_accepted_task_results(args: argparse.Namespace) -> None: + for row in list_accepted_task_results(args): print(json.dumps(row, ensure_ascii=False, sort_keys=True)) -def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser(prog="review_runtime.py") - parser.add_argument("command", choices=("validate-contract", "assert-migration-stop")) - parsed, remaining = parser.parse_known_args(argv) - if parsed.command == "validate-contract": - return cmd_validate_contract(remaining) - return cmd_assert_migration_stop(remaining) +def cmd_write_final_workflow_review(args: argparse.Namespace) -> None: + print(json.dumps(write_final_workflow_review(args), ensure_ascii=False, sort_keys=True)) -if __name__ == "__main__": - raise SystemExit(main()) +def cmd_list_final_workflow_reviews(args: argparse.Namespace) -> None: + for row in list_final_workflow_reviews(args): print(json.dumps(row, ensure_ascii=False, sort_keys=True)) diff --git a/scripts/orchestration/specs.py b/scripts/orchestration/specs.py index ff4c972..074e1e8 100644 --- a/scripts/orchestration/specs.py +++ b/scripts/orchestration/specs.py @@ -1,79 +1,109 @@ +"""Schema-owned current specification-family adapter.""" +from __future__ import annotations + from core import * from bounded_closure import require_orchestration_admission, resolve_working_workspace -from review_runtime import require_specification_review +from artifact_store import ( + atomic_write_bytes, + canonical_artifact_path, family_policy, load_catalog, parse_markdown_artifact, + read_artifact, rebuild_index, serialize_markdown_mapping, transition_artifact, + write_artifact, +) + +CATALOG_PATH = Path(__file__).resolve().parents[2] / "references/assets/orchestration/contract/artifact-family-catalog-v3.yaml" +FAMILY = "specification" +QUALIFICATION_STATUSES = {"draft", "verified", "superseded"} +STRUCTURAL_INPUT_FIELDS = { + "artifact_type", "schema_version", "id", "title", "status", "date_created", + "last_updated", "purpose", "component", "version", +} + + +def _anchors(args: argparse.Namespace) -> dict[str, Path]: + return {"workspace_root": resolve_workspace_root(args)} + + +def _policy() -> dict[str, object]: + return family_policy(load_catalog(CATALOG_PATH), FAMILY) + + +def _path(args: argparse.Namespace, identity: str, state: str) -> Path: + return canonical_artifact_path(_policy(), _anchors(args), identity=identity, state=state) + + +def _located(args: argparse.Namespace, identity: str) -> tuple[str, Path]: + candidates = [(state, _path(args, identity, state)) for state in ("active", "archived")] + existing = [(state, path) for state, path in candidates if path.is_file()] + if len(existing) > 1: + raise SystemExit(f"Specification canonical identity collision: {identity}") + if not existing: + raise SystemExit(f"Specification not found at canonical location: {identity}") + return existing[0] + + +def _row(args: argparse.Namespace, data: dict[str, object]) -> dict[str, object]: + identity = str(data["id"]) + _state, path = _located(args, identity) + return { + "type": "spec", "id": identity, "title": data["title"], + "status": data["status"], "path": rel(path, args), + "purpose": data["purpose"], "component": data["component"], + "created_at": data["date_created"], "updated_at": data["last_updated"], + } + def index_specs(args: argparse.Namespace) -> list[dict[str, object]]: - root = orchestration_root(args) / "spec" - rows = [] - for path in sorted(root.glob("*/*.md")): - fm, _ = read_front_matter(path) - if not fm: - continue - rows.append( - { - "type": "spec", - "id": fm.get("id", path.stem), - "title": fm.get("title", path.stem), - "status": fm.get("status", "draft"), - "path": rel(path, args), - "purpose": fm.get("purpose", ""), - "component": fm.get("component", ""), - "created_at": fm.get("date_created", fm.get("created_at", "")), - "updated_at": fm.get("last_updated", fm.get("updated_at", "")), - } - ) - target = root / "index.jsonl" - target.write_text("\n".join(json.dumps(row, ensure_ascii=False) for row in rows) + ("\n" if rows else ""), encoding="utf-8") - return rows + result = rebuild_index(CATALOG_PATH, FAMILY, _anchors(args)) + raw_rows = [json.loads(line) for line in Path(str(result["path"])).read_text(encoding="utf-8").splitlines() if line.strip()] + return [_row(args, row) for row in raw_rows] def cmd_index_specs(args: argparse.Namespace) -> None: print(f"indexed {len(index_specs(args))} specs") +def _semantic_input(path: Path) -> tuple[dict[str, object], str]: + text = path.read_text(encoding="utf-8") + if not text.startswith("---\n"): + return {}, text + data, body = parse_markdown_artifact(text, source=str(path)) + overrides = sorted(STRUCTURAL_INPUT_FIELDS.intersection(data)) + if overrides: + raise SystemExit("Specification semantic input contains structural field override: " + ", ".join(overrides)) + return data, body + + +def _next_identity(args: argparse.Namespace) -> str: + date = now_date().replace("-", "") + existing = list((orchestration_root(args) / "spec").glob(f"*/spec-{date}-*.spec.md")) + return f"spec-{date}-{len(existing) + 1:03d}" + + def cmd_write_spec(args: argparse.Namespace) -> None: authority = resolve_working_workspace(resolve_workspace_root(args)) if authority is not None: require_orchestration_admission(authority, operation="ordinary_new", flow_id=args.id) - init_dirs(args) - if args.status not in SPEC_STATUSES: - raise SystemExit(f"Invalid spec status: {args.status}") - root = orchestration_root(args) / "spec" / ("archived" if args.status == "archived" else "active") - sid = args.id or sequence_id(root, "spec") - filename = args.filename or f"{sid}-{slugify(args.title)}.md" - content = Path(args.content_file).read_text(encoding="utf-8") - content = ensure_front_matter( - content, - { - "id": sid, - "title": args.title, - "status": args.status, - "date_created": now_date(), - "last_updated": now_date(), - "purpose": args.purpose, - "component": args.component, - "version": args.version, - }, - ) - target = root / filename - from execution_context import parse_yaml_subset - effective_status = parse_yaml_subset(content.split("---", 2)[1]).get("status") - if effective_status in {"verified", "archived"} or args.status in {"verified", "archived"}: - require_specification_review(project_root(args), target, content=content) - write_text_safely(target, content, args) - index_specs(args) - print(rel(target, args)) - - -def load_index(path: Path) -> list[dict[str, object]]: - if not path.exists(): - return [] - return [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()] + if getattr(args, "filename", None): + raise SystemExit("Specification filename override is not supported by the canonical family") + if args.status not in QUALIFICATION_STATUSES: + raise SystemExit(f"Invalid spec qualification status: {args.status}") + identity = args.id or _next_identity(args) + if _path(args, identity, "active").exists() or _path(args, identity, "archived").exists(): + raise SystemExit(f"Specification canonical identity collision: {identity}") + semantic, body = _semantic_input(Path(args.content_file)) + today = now_date() + data = { + **semantic, "artifact_type": FAMILY, "schema_version": 1, "id": identity, + "title": args.title, "status": args.status, "date_created": today, + "last_updated": today, "purpose": args.purpose, "component": args.component, + "version": args.version, + } + result = write_artifact(CATALOG_PATH, FAMILY, _anchors(args), data, state="active", body=body) + print(rel(Path(str(result["path"])), args)) def cmd_list_specs(args: argparse.Namespace) -> None: - rows = index_specs(args) - for row in rows: + for row in index_specs(args): if args.status and row.get("status") != args.status: continue print(json.dumps(row, ensure_ascii=False)) @@ -83,69 +113,53 @@ def replace_front_matter_value(path: Path, key: str, value: str) -> None: text = path.read_text(encoding="utf-8") if not text.startswith("---\n"): raise SystemExit(f"Missing front matter: {path}") - end = text.find("\n---\n", 4) - raw = text[4:end] - body = text[end + 5 :] - lines = [] - replaced = False - for line in raw.splitlines(): - if line.startswith(f"{key}:"): - lines.append(f"{key}: {value}") - replaced = True - elif line.startswith("last_updated:") or line.startswith("updated_at:"): - lines.append(f"{line.split(':', 1)[0]}: {now_date()}") - else: - lines.append(line) - if not replaced: - lines.append(f"{key}: {value}") - path.write_text("---\n" + "\n".join(lines) + "\n---\n" + body, encoding="utf-8") + data, body = parse_markdown_artifact(text, source=str(path)) + data[key] = value + for timestamp in ("last_updated", "updated_at"): + if timestamp in data: + data[timestamp] = now_date() + atomic_write_bytes(path, serialize_markdown_mapping(data, body)) + + +def _rebuild_after_transition(args: argparse.Namespace, identity: str) -> None: + try: + rebuild_index(CATALOG_PATH, FAMILY, _anchors(args)) + except (OSError, SystemExit) as exc: + raise SystemExit( + f"Specification {identity} moved but index rebuild failed (partial effect): {exc}" + ) from exc def cmd_set_spec_status(args: argparse.Namespace) -> None: - if args.status not in SPEC_STATUSES: - raise SystemExit(f"Invalid spec status: {args.status}") - rows = index_specs(args) - match = next((row for row in rows if row.get("id") == args.id), None) - if not match: - raise SystemExit(f"Spec not found: {args.id}") - path = project_root(args) / str(match["path"]) - if args.status in {"verified", "archived"}: - require_specification_review(project_root(args), path) - replace_front_matter_value(path, "status", args.status) if args.status == "archived": - target = orchestration_root(args) / "spec" / "archived" / path.name - target.parent.mkdir(parents=True, exist_ok=True) - shutil.move(str(path), str(target)) - index_specs(args) + state, _current_path = _located(args, args.id) + if state == "archived": + print(args.id) + return + transition_artifact(CATALOG_PATH, FAMILY, _anchors(args), identity=args.id, current_state="active", target_state="archived") + _rebuild_after_transition(args, args.id) + print(args.id) + return + if args.status not in QUALIFICATION_STATUSES: + raise SystemExit(f"Invalid spec qualification status: {args.status}") + state, _current_path = _located(args, args.id) + if state != "active": + raise SystemExit("Archived specification qualification cannot be changed") + current = read_artifact(CATALOG_PATH, FAMILY, _anchors(args), identity=args.id, state="active") + data = dict(current["data"]) + if data["status"] == args.status: + print(args.id) + return + data["status"] = args.status + data["last_updated"] = now_date() + write_artifact(CATALOG_PATH, FAMILY, _anchors(args), data, state="active", body=str(current["body"])) print(args.id) def archive_spec_for_forced_finalization(args: argparse.Namespace, spec_id: str) -> Path: - """Move one origin specification without asserting product acceptance. - - This narrow helper is owned by the bounded-closure finalizer. An already - moved artifact is a retry-safe success; simultaneous active and archived - identities are an explicit collision. - """ - - rows = index_specs(args) - matches = [row for row in rows if row.get("id") == spec_id] - active = [row for row in matches if "/spec/active/" in f"/{row.get('path', '')}"] - archived = [row for row in matches if "/spec/archived/" in f"/{row.get('path', '')}"] - if len(active) > 1 or len(archived) > 1 or (active and archived): - raise SystemExit(f"Forced finalization spec archive collision: {spec_id}") - if archived: - return project_root(args) / str(archived[0]["path"]) - if not active: - raise SystemExit(f"Forced finalization origin spec not found: {spec_id}") - path = project_root(args) / str(active[0]["path"]) - target = orchestration_root(args) / "spec" / "archived" / path.name - if target.exists(): - raise SystemExit(f"Forced finalization spec archive collision: {target}") - moved = move_to_archive( - path, - orchestration_root(args) / "spec" / "active", - orchestration_root(args) / "spec" / "archived", - ) - index_specs(args) - return moved + state, path = _located(args, spec_id) + if state == "archived": + return path + result = transition_artifact(CATALOG_PATH, FAMILY, _anchors(args), identity=spec_id, current_state="active", target_state="archived") + _rebuild_after_transition(args, spec_id) + return Path(str(result["path"])) diff --git a/scripts/wb.py b/scripts/wb.py index f5d38fc..1a830b5 100755 --- a/scripts/wb.py +++ b/scripts/wb.py @@ -1,17 +1,52 @@ #!/usr/bin/env python3 +# /// script +# requires-python = ">=3.13" +# dependencies = [ +# "pyyaml==6.0.3", +# "jsonschema==4.25.1", +# ] +# /// """Compatibility entrypoint for work-bundle helpers.""" from __future__ import annotations import importlib.util +import os +import shutil import sys from pathlib import Path +from typing import Mapping, Sequence + +RUNTIME_DEPENDENCIES = (("yaml", "pyyaml"), ("jsonschema", "jsonschema")) +UV_REEXEC_ENV = "WORK_BUNDLE_PUBLIC_UV_REEXEC" SCRIPT_ROOT = Path(__file__).resolve().parent sys.path.insert(0, str(SCRIPT_ROOT)) from invocation_observation import invoke_observed +def _missing_runtime_dependencies() -> list[str]: + return [distribution for module, distribution in RUNTIME_DEPENDENCIES if importlib.util.find_spec(module) is None] + + +def _ensure_managed_runtime( + *, argv: Sequence[str] | None = None, environ: Mapping[str, str] | None = None +) -> tuple[bool, str | None]: + missing = _missing_runtime_dependencies() + if not missing: + return True, None + environment = dict(os.environ if environ is None else environ) + if environment.get(UV_REEXEC_ENV) == "1": + return False, "WB_RUNTIME_DEPENDENCY_UNAVAILABLE: uv could not hydrate " + ", ".join(missing) + uv = shutil.which("uv") + if uv is None: + return False, "WB_RUNTIME_DEPENDENCY_UNAVAILABLE: install uv to provide " + ", ".join(missing) + arguments = list(sys.argv if argv is None else argv) + environment[UV_REEXEC_ENV] = "1" + os.execve(uv, [uv, "run", str(Path(__file__).resolve()), *arguments[1:]], environment) + return False, "WB_RUNTIME_DEPENDENCY_UNAVAILABLE: uv re-execution returned unexpectedly" + + def _load_dispatcher(): module_path = SCRIPT_ROOT / "work-bundle" / "dispatcher.py" sys.path.insert(0, str(module_path.parent)) @@ -24,6 +59,10 @@ def _load_dispatcher(): def main() -> int: + ready, failure = _ensure_managed_runtime() + if not ready: + print(failure, file=sys.stderr) + return 1 dispatcher = _load_dispatcher() return invoke_observed( "wb", diff --git a/scripts/work-bundle/README.md b/scripts/work-bundle/README.md index 9d0604b..13fde3e 100644 --- a/scripts/work-bundle/README.md +++ b/scripts/work-bundle/README.md @@ -2,18 +2,15 @@ Implementation modules in this directory are the manual maintenance surface for work-bundle helpers. -The top-level `../wb.py` entrypoint remains for compatibility with existing agent instructions. Implementation is split by skill area (`rules.py`, `project.py`, `member.py`, `doctor.py`, `metadata_profile.py`, `skill_registry.py`), with `dispatcher.py` only wiring commands. +The top-level `../wb.py` entrypoint is the supported public command. It declares and hydrates its maintained YAML and JSON Schema runtime before loading implementation modules. Implementation is split by skill area, with `dispatcher.py` only wiring commands. Command examples: ```bash -python3 scripts/wb.py init-project <root> --mode single-repository -python3 scripts/wb.py init-project <root> --mode multi-repository --workspace-root <workspace-root> -python3 scripts/wb.py show-project --workspace-root <workspace-root> --project-root <member-project-root> +python3 scripts/wb.py init-workspace <workspace-root> --mode single-repository --slug <slug> --repository <id>=<source-remote> --dry-run +python3 scripts/wb.py init-workspace <workspace-root> --mode multi-repository --slug <slug> --repository <id>=<source-remote> --dry-run +python3 scripts/wb.py show-project --workspace-root <workspace-root> python3 scripts/wb.py credential-list --workspace-root <workspace-root> -python3 scripts/wb.py migrate-to-multi-repository <source-project-root> --target-workspace-root <target> --origin <primary-git-origin> --repository-id <id> --repository-name <name> --workspace-slug <slug> --working-branch <branch> --dry-run -python3 scripts/wb.py provision-member --workspace-root <workspace> --workspace-slug <slug> --origin <origin-root> --repository-id <id> --working-branch <branch> --base-ref <ref> --dry-run -python3 scripts/wb.py cleanup-member --workspace-root <workspace> --repository-id <id> --dry-run python3 scripts/wb.py migrate-control-plane <workspace-root> --dry-run python3 scripts/wb.py migrate-control-plane <workspace-root> --accepted-proposal-id <proposal-id> --apply python3 scripts/wb.py migrate-registered-projects --dry-run @@ -41,16 +38,16 @@ python3 scripts/wb.py defect-write-index python3 scripts/wb.py defect-archive-evidence <evidence-id-or-path> --action completed ``` -Prefer `--scope` for `create-rules` and `validate-rules`: `toolkit` resolves to `$work_bundle_root/rules/`, `global` resolves to `$work_bundle_config_root/rules/`, and `project` resolves to `<workspace-root>/.work-bundle/rules/`. The project-root form remains a single-repository compatibility alias. +Prefer `--scope` for `create-rules` and `validate-rules`: `toolkit` resolves to `$work_bundle_root/rules/`, `global` resolves to `$work_bundle_config_root/rules/`, and `project` resolves to `<workspace-root>/.work-bundle/rules/`. -Multi-repository workspace utilities live under singular `<workspace-root>/script/` and are reusable only when declared in `script/index.yaml`; discovery never runs them. Their credential values stay solely in protected, ignored `<workspace-root>/credentials/credentials.yaml`. Single-repository workspaces contain neither runtime folder. Toolkit helpers remain under plural `scripts/`, and credential values are never accepted by these command lines. +Workspace utilities live under singular `<workspace-root>/script/` in both metadata-v4 modes and are reusable only when declared in `script/index.yaml`; discovery never runs them. Their credential values stay solely in protected, ignored `<workspace-root>/credentials/credentials.yaml`. Toolkit helpers remain under plural `scripts/`, and credential values are never accepted by these command lines. -`migrate-project` is an in-place metadata upgrader only. Metadata v2 dry-run classifies metadata and registry topology and returns an accepted proposal ID; multiple repositories route to `migrate-to-multi-repository`. When the authority root is not Git-backed, use `--origin` for the concrete primary Git repository. `provision-member --apply` reports success only after the verified checkout, workspace metadata member, and locator registry origin are all published recoverably. Exact older verified checkouts without recovery records resume publication; `cleanup-member` deletes only recorded, unpublished, transaction-owned paths. +`init-project`, `initialize-project`, `migrate-project`, `migrate-to-multi-repository`, `provision-member`, and `cleanup-member` are retired typed refusals because their former implementations produced or mutated metadata v3. `init-workspace` is the only current producer. Use `migrate-control-plane` for one historical v2/v3 workspace and `migrate-registered-projects` for registry-wide migration; both publish validated v4 directly. Use `add-workspace-member` for current v4 topology changes and attach/doctor for binding repair. -`migrate-registered-projects` enumerates the bootstrap-resolved project registry, classifies each entry as current, migratable, unsupported, missing, or blocked, and dry-runs a deterministic version-to-version plan. Apply requires that exact plan ID. Layout steps are registered in `references/wb-registry-layout-migration.yaml` (`2 -> 3` then `3 -> 4`); registry schema version stays distinct from project layout version. Registry `layout_version` is published only after the target layout validates. A failed project restores its pre-migration workspace and registry bytes and never marks the entry current. +`migrate-registered-projects` enumerates the bootstrap-resolved project registry, classifies each entry as current, migratable, unsupported, missing, or blocked, and dry-runs a deterministic migration plan. Apply requires that exact plan ID. Historical v2/v3 inputs converge on v4; registry schema version stays distinct from project layout version. Registry `layout_version` is published only after the target layout validates. A failed project restores its pre-migration workspace and registry bytes and never marks the entry current. `add-workspace-member` is the v4 composite-member transaction. Dry-run and apply fail closed unless the current workspace binding, matching `workspace_root`, root repository local binding, and a valid root Git checkout are present, and the live root origin plus observed branch match the portable root `remote.canonical` and `default_branch`. Dry-run validates required request values and the rendered target metadata before emitting a digest-bound proposal that records current/target mode, unchanged root identity, member id/name/path/remote/branch, exclude patterns, device-binding delta, and the live metadata digest. The first accepted apply converts `single-repository` to `composite` and adds the named nested member; later applies are add-only. A pre-existing member checkout is accepted only when its remote and observed branch already match the request; transaction-owned clone/checkout is re-verified against `--default-branch`. Matching replay is a no-op only when the member checkout exists with matching remote/branch, the registry member binding points at that exact path with `checkout_kind: nested-member`, and the root exclude contains the member pattern; otherwise apply fails closed without mutation and attach/doctor remain the repair path. A different remote or path collides. Portable composite validation rejects duplicate member names and paths. Root Git exclusion uses device-local `.git/info/exclude` with `checkout_kind: nested-member`; attach/doctor reapply those lines and fail closed if the member path is root-index tracked. Rollback restores metadata, registry, and transaction-owned exclude lines and removes only transaction-owned member state. It does not invent remotes, create GitHub repositories, extend v3 `provision-member`, or rewrite the root source repository. -`migrate-control-plane` upgrades metadata v3 to portable v4 only after the exact dry-run proposal is accepted. For single-repository mode it preserves `workspace_root == project_root`, writes a portable `root` workspace binding, keeps machine-local observations in the user registry, and ensures the source repository excludes `.work-bundle/`. An existing compatible ignore rule is left untouched; otherwise WorkBundle records the local realization rule in `.git/info/exclude` rather than rewriting user `.gitignore`. `AGENTS.md` remains a separate concern: tracked content stays tracked and synchronization preserves user-authored content outside the managed section. To reconstruct another device, clone the control-plane repository as `<workspace-root>/.work-bundle`, then attach with source materialization enabled. Root materialization initializes and checks out the configured source remote in place, preserves the cloned control plane and pre-existing user paths, and rolls back only transaction-created source state on failure. +`migrate-control-plane` upgrades historical metadata v2 or v3 to portable v4 only after the exact dry-run proposal is accepted. For single-repository mode it writes a portable `root` workspace binding, keeps machine-local observations in the user registry, and ensures the source repository excludes `.work-bundle/`. An existing compatible ignore rule is left untouched; otherwise WorkBundle records the local realization rule in `.git/info/exclude` rather than rewriting user `.gitignore`. `AGENTS.md` remains a separate concern: tracked content stays tracked and synchronization preserves user-authored content outside the managed section. To reconstruct another device, clone the control-plane repository as `<workspace-root>/.work-bundle`, then attach with source materialization enabled. Root materialization initializes and checks out the configured source remote in place, preserves the cloned control plane and pre-existing user paths, and rolls back only transaction-created source state on failure. The runtime skill registry resolved from `~/.work-bundle/bootstrap.yaml` field `skill_registry` is external-only. Built-in skills under `$work_bundle_root/skills/`, including `wb-credential-use` and `wb-migrate-to-multi-repository`, are toolkit-owned and must not be registered. For external candidates only, inspect and validate a compact `type: external` proposal first; `register-skill --confirmed` is forbidden until the user explicitly confirms its role, stage, and output mappings. diff --git a/scripts/work-bundle/control_plane.py b/scripts/work-bundle/control_plane.py index 2b52d0c..eede645 100644 --- a/scripts/work-bundle/control_plane.py +++ b/scripts/work-bundle/control_plane.py @@ -3,13 +3,10 @@ import argparse import hashlib import json -import os from pathlib import Path import re import shutil import subprocess -import sys -import tempfile from typing import Iterable from urllib.parse import parse_qsl, urlsplit @@ -20,7 +17,20 @@ resolve_work_bundle_root, utc_now_rfc3339, ) -from workspace_resources import _load_yaml, ensure_workspace_resources +from workspace_resources import ( + CREDENTIAL_TEMPLATE, + SCRIPT_INDEX_TEMPLATE, + _load_yaml, + ensure_workspace_resources, +) +from infrastructure import ( + InfrastructureError, + atomic_write_text, + dump_canonical_yaml, + join_workspace_binding, + parse_yaml_mapping, + validate_infrastructure_document, +) VERSION = "4" @@ -40,6 +50,32 @@ "metadata_compatibility", } RESERVED_V4_KEYS = {"metadata_version", "authority", "workspace", "control_plane", "source_repositories"} +CURRENT_ORCHESTRATION_STORE_ROOTS = ( + "orchestration/spec/active", + "orchestration/spec/archived", + "orchestration/plan/active", + "orchestration/plan/archived", + "orchestration/result/executor/active", + "orchestration/result/executor/reviewed", + "orchestration/result/executor/superseded", + "orchestration/result/executor/archived", + "orchestration/result/accepted/active", + "orchestration/result/accepted/superseded", + "orchestration/result/accepted/archived", + "orchestration/review/implementation/active", + "orchestration/review/implementation/superseded", + "orchestration/review/implementation/archived", + "orchestration/review/final/active", + "orchestration/review/final/archived", +) +CURRENT_ORCHESTRATION_PORTABLE_PATHS = ( + "orchestration/spec/", + "orchestration/plan/", + "orchestration/result/executor/", + "orchestration/result/accepted/", + "orchestration/review/implementation/", + "orchestration/review/final/", +) class ControlPlaneError(RuntimeError): @@ -49,74 +85,6 @@ def __init__(self, code: str, details: dict[str, object] | None = None) -> None: self.details = details or {} -def deferred_remote_task_identity(repository_root: Path) -> dict[str, str]: - """Compute identity from the canonical orchestration repository evidence.""" - - root = repository_root.expanduser().resolve() - orchestration_root = Path(__file__).resolve().parents[1] / "orchestration" - command = "\n".join( - [ - "import json, sys", - "from pathlib import Path", - "sys.path.insert(0, sys.argv[1])", - "from repository_preflight import capture_repository_evidence", - "print(json.dumps(capture_repository_evidence(Path(sys.argv[2])), sort_keys=True))", - ] - ) - captured = subprocess.run( - [sys.executable, "-c", command, str(orchestration_root), str(root)], - capture_output=True, - check=False, - text=True, - ) - try: - evidence = json.loads(captured.stdout) if captured.returncode == 0 else None - except json.JSONDecodeError: - evidence = None - if not isinstance(evidence, dict): - raise ControlPlaneError("WB_CONTROL_PLANE_REVIEW_REPOSITORY_INVALID") - head = str(evidence.get("head") or "") - tree = str(evidence.get("tree") or "") - if not re.fullmatch(r"[0-9a-f]{40}", head) or not re.fullmatch(r"[0-9a-f]{40}", tree): - raise ControlPlaneError("WB_CONTROL_PLANE_REVIEW_REPOSITORY_INVALID") - if evidence.get("status") != "clean" or evidence.get("entries"): - raise ControlPlaneError("WB_CONTROL_PLANE_REVIEW_TASK_TREE_DIRTY") - digest = hashlib.sha256( - json.dumps(evidence, sort_keys=True, separators=(",", ":")).encode("utf-8") - ).hexdigest() - return { - "reviewed_head": f"{head}+repository-evidence-sha256:{digest}", - "reviewed_tree": tree, - "repository_evidence_sha256": digest, - } - - -def validate_deferred_remote_independent_review_identity( - repository_root: Path, *, task_id: str -) -> dict[str, object]: - """Load the selected real review and bind its acceptance to the current Git tree.""" - - raw_path = os.environ.get("WOR105_C02_REVIEW", "").strip() - if not raw_path: - raise ControlPlaneError("WB_CONTROL_PLANE_REVIEW_ARTIFACT_REQUIRED") - review_path = Path(raw_path).expanduser().resolve() - if not review_path.is_file(): - raise ControlPlaneError("WB_CONTROL_PLANE_REVIEW_ARTIFACT_MISSING") - review = _load_yaml(read(review_path)) - if not isinstance(review, dict): - raise ControlPlaneError("WB_CONTROL_PLANE_REVIEW_IDENTITY_MISMATCH") - identity = deferred_remote_task_identity(repository_root) - if ( - review.get("task_id") != task_id - or review.get("reviewer_independent") is not True - or review.get("verdict") != "accept" - or review.get("reviewed_head") != identity["reviewed_head"] - or review.get("reviewed_tree") != identity["reviewed_tree"] - ): - raise ControlPlaneError("WB_CONTROL_PLANE_REVIEW_IDENTITY_MISMATCH") - return dict(review) - - def _yaml_scalar(text: str, key: str) -> str: match = re.search(rf"^{re.escape(key)}:\s*(.*?)\s*$", text, re.MULTILINE) return match.group(1).strip().strip('"').strip("'") if match else "" @@ -166,7 +134,7 @@ def _agents_contract_block() -> str: ]) -def _sync_agents(workspace_root: Path) -> list[str]: +def _render_synced_agents(workspace_root: Path) -> str: template = _agents_template() managed = f"{AGENTS_START}\n{template}{AGENTS_END}\n" path = workspace_root / "AGENTS.md" @@ -199,7 +167,12 @@ def _sync_agents(workspace_root: Path) -> list[str]: rendered = current.rstrip("\n") + "\n\n" + managed else: rendered = managed - return [str(path)] if _atomic_write(path, rendered) else [] + return rendered + + +def _sync_agents(workspace_root: Path) -> list[str]: + path = workspace_root / "AGENTS.md" + return [str(path)] if _atomic_write(path, _render_synced_agents(workspace_root)) else [] def _repository_execution_issues(path: Path, expected_branch: str, repository_id: str) -> list[str]: @@ -372,12 +345,12 @@ def _resolved_declared_remote(value: object, repository_path: Path) -> str: return _resolved_git_remote(local) -def _registry_repository_remotes(workspace_root: Path) -> dict[str, str]: +def _registry_repository_entries(workspace_root: Path) -> list[dict[str, object]]: registry = resolve_project_registry_path() document = _load_yaml(read(registry)) if registry.is_file() else {} projects = document.get("projects") if isinstance(document, dict) else None if not isinstance(projects, list): - return {} + return [] control = (workspace_root / ".work-bundle").resolve() matches: list[dict[str, object]] = [] for project in projects: @@ -389,23 +362,30 @@ def _registry_repository_remotes(workspace_root: Path) -> dict[str, str]: if len(matches) > 1: raise ControlPlaneError("WB_CONTROL_PLANE_REGISTRY_WORKSPACE_AMBIGUOUS") if not matches: - return {} + return [] repositories = matches[0].get("repository_origins") if not isinstance(repositories, list): repositories = matches[0].get("source_repositories") if not isinstance(repositories, list): - return {} - result: dict[str, str] = {} + return [] + result: list[dict[str, object]] = [] for repository in repositories: if not isinstance(repository, dict): continue repository_id = str(repository.get("id") or "") - remote = str(repository.get("remote") or "") - if repository_id and remote: - result[repository_id] = remote + if repository_id: + result.append(dict(repository)) return result +def _registry_repository_remotes(workspace_root: Path) -> dict[str, str]: + return { + str(item.get("id")): str(item.get("remote")) + for item in _registry_repository_entries(workspace_root) + if item.get("id") and item.get("remote") + } + + def _is_local_remote(remote: str, repository_path: Path) -> bool: return _local_remote_path(remote, repository_path) is not None @@ -440,34 +420,24 @@ def _canonical_migration_remote( def _workspace_slug(workspace_root: Path, text: str) -> str: - workspace_block = _block(text, "workspace") - nested_slug = re.search(r"^\s{2}slug:\s*(.*?)\s*$", workspace_block, re.MULTILINE) - return (nested_slug.group(1).strip().strip('"').strip("'") if nested_slug else workspace_root.name) or "workspace" + workspace = parse_yaml_mapping(text, source="project metadata").get("workspace") + return (str(workspace.get("slug") or "") if isinstance(workspace, dict) else workspace_root.name) or "workspace" def _workspace_id(text: str) -> str: - workspace_block = _block(text, "workspace") - match = re.search(r"^\s{2}id:\s*(.*?)\s*$", workspace_block, re.MULTILINE) - return match.group(1).strip().strip('"').strip("'") if match else "" + workspace = parse_yaml_mapping(text, source="project metadata").get("workspace") + return str(workspace.get("id") or "") if isinstance(workspace, dict) else "" def _workspace_value(text: str, key: str) -> str: - match = re.search(rf"^\s{{2}}{re.escape(key)}:\s*(.*?)\s*$", _block(text, "workspace"), re.MULTILINE) - return match.group(1).strip().strip('"').strip("'") if match else "" + workspace = parse_yaml_mapping(text, source="project metadata").get("workspace") + return str(workspace.get(key) or "") if isinstance(workspace, dict) else "" def _control_plane_remote(text: str) -> str: - block = _block(text, "control_plane") - in_repository = False - for line in block.splitlines(): - if line == " repository:": - in_repository = True - continue - if in_repository and line.startswith(" remote:"): - return line.split(":", 1)[1].strip().strip('"').strip("'") - if in_repository and line.startswith(" ") and not line.startswith(" "): - break - return "" + control = parse_yaml_mapping(text, source="project metadata").get("control_plane") + repository = control.get("repository") if isinstance(control, dict) else None + return str(repository.get("remote") or "") if isinstance(repository, dict) else "" def _v3_repositories(text: str) -> list[dict[str, object]]: @@ -478,22 +448,10 @@ def _v3_repositories(text: str) -> list[dict[str, object]]: return repositories -def _source_repository_bounds(lines: list[str]) -> tuple[int, int] | None: - start = next((i for i, line in enumerate(lines) - if re.match(r"^source_repositories:(?:\s|$)", line)), None) - if start is None: - return None - # Any non-comment root content ends the list, including quoted keys and - # document markers. Readers and writers must agree on this boundary. - end = next((i for i in range(start + 1, len(lines)) - if re.match(r"^[^\s#]", lines[i])), len(lines)) - return start, end - - def _v4_repositories(text: str) -> list[dict[str, object]]: - lines = text.splitlines(keepends=True) - bounds = _source_repository_bounds(lines) - repositories = _parse_list_items("".join(lines[bounds[0]:bounds[1]])) if bounds else [] + document = parse_yaml_mapping(text, source="project metadata") + raw_repositories = document.get("source_repositories") + repositories = [dict(item) for item in raw_repositories if isinstance(item, dict)] if isinstance(raw_repositories, list) else [] # The generic parser retains remote as a mapping. Normalize its canonical field. for repository in repositories: remote = repository.get("remote") @@ -508,7 +466,7 @@ def _v4_repositories(text: str) -> list[dict[str, object]]: locator = repository.get("locator") repository["locator_type"] = str(locator.get("type", "")) if isinstance(locator, dict) else "" repository["materialization_raw"] = ( - str(materialization.get("required", "")) if isinstance(materialization, dict) else "" + materialization.get("required", "") if isinstance(materialization, dict) else "" ) repository["materialization_state"] = ( str(materialization.get("state", "")) if isinstance(materialization, dict) else "" @@ -575,6 +533,27 @@ def _render_v4( if mode not in {"single-repository", "multi-repository"}: raise ControlPlaneError("WB_CONTROL_PLANE_WORKSPACE_MODE_INVALID") repositories = _v3_repositories(v3_text) + if not repositories and _yaml_scalar(v3_text, "metadata_version") == "2": + registered = _registry_repository_entries(workspace_root) + if not registered: + registered = [{ + "id": workspace_root.name, + "path": str(workspace_root), + "remote": _git_remote(workspace_root), + "git_repository": bool(_git(workspace_root, "rev-parse", "--git-dir")), + }] + repositories = [ + { + "id": str(item.get("id") or workspace_root.name), + "project_root": str(item.get("path") or workspace_root), + "origin_id": str(item.get("id") or workspace_root.name), + "git_repository": bool(item.get("git_repository", True)), + "expected_branch": str(item.get("expected_branch") or "main"), + "remote": str(item.get("remote") or ""), + "operation_policy": "inherit", + } + for item in registered + ] if not repositories: raise ControlPlaneError("WB_CONTROL_PLANE_REPOSITORIES_MISSING") if mode == "single-repository" and len(repositories) != 1: @@ -681,19 +660,9 @@ def _proposal( def _atomic_write(path: Path, text: str) -> bool: - path.parent.mkdir(parents=True, exist_ok=True) if read(path) == text: return False - descriptor, temporary = tempfile.mkstemp(prefix=f".{path.name}.", dir=str(path.parent)) - try: - with os.fdopen(descriptor, "w", encoding="utf-8") as stream: - stream.write(text) - stream.flush() - os.fsync(stream.fileno()) - os.replace(temporary, path) - finally: - if os.path.exists(temporary): - os.unlink(temporary) + atomic_write_text(path, text) return True @@ -704,13 +673,25 @@ def _atomic_publish(payloads: dict[Path, str]) -> list[str]: for path, text in payloads.items(): if _atomic_write(path, text): changed.append(str(path)) - except (OSError, ControlPlaneError): + except (OSError, ControlPlaneError, InfrastructureError): + rollback_failures: list[str] = [] for path, value in before.items(): - if value is None: - path.unlink(missing_ok=True) - else: - path.parent.mkdir(parents=True, exist_ok=True) - path.write_bytes(value) + try: + if value is None: + if path.exists(): + path.unlink(missing_ok=True) + else: + if path.is_file() and path.read_bytes() == value: + continue + path.parent.mkdir(parents=True, exist_ok=True) + atomic_write_text(path, value.decode("utf-8")) + except (OSError, InfrastructureError): + rollback_failures.append(str(path)) + if rollback_failures: + raise ControlPlaneError( + "WB_CONTROL_PLANE_TRANSACTION_RECOVERY_REQUIRED", + {"rollback_failures": rollback_failures}, + ) raise ControlPlaneError("WB_CONTROL_PLANE_TRANSACTION_FAILED") return changed @@ -751,123 +732,12 @@ def _source_tracks_control_plane(workspace_root: Path) -> bool: def _source_exclude_payload(workspace_root: Path) -> tuple[Path, str] | None: if not (workspace_root / ".git").exists(): return None - ignored = subprocess.run( - ["git", "-C", str(workspace_root), "check-ignore", "-q", "--no-index", ".work-bundle/project.yaml"], - check=False, - capture_output=True, - text=True, - ).returncode == 0 - if ignored: - return None path = workspace_root / ".git/info/exclude" current = read(path) - rendered = current - if rendered and not rendered.endswith("\n"): - rendered += "\n" - rendered += ".work-bundle/\n" - return path, rendered - - -def _binding_block_bounds(lines: list[str]) -> tuple[int, int] | None: - try: - start = next(i for i, line in enumerate(lines) if line == "device_bindings:") - except StopIteration: + rendered = _exclude_text_with_source_and_members(current, ()) + if rendered == current: return None - end = start + 1 - while end < len(lines) and (not lines[end] or lines[end].startswith(" ")): - end += 1 - return start, end - - -def _parse_bindings(text: str) -> dict[str, dict[str, object]]: - lines = text.splitlines() - bounds = _binding_block_bounds(lines) - if not bounds: - return {} - start, end = bounds - document = _load_yaml("\n".join(lines[start:end]) + "\n") - if not isinstance(document, dict) or not isinstance(document.get("device_bindings"), dict): - return {} - result: dict[str, dict[str, object]] = {} - for key, value in document["device_bindings"].items(): - if isinstance(value, dict): - result[str(key)] = value - return result - - -def _render_nested(lines: list[str], indent: int, key: str, value: object) -> None: - prefix = " " * indent - if isinstance(value, dict): - lines.append(f"{prefix}{key}:") - for nested_key, nested_value in value.items(): - _render_nested(lines, indent + 2, str(nested_key), nested_value) - elif isinstance(value, list): - if not value: - lines.append(f"{prefix}{key}: []") - elif all(not isinstance(item, (dict, list)) for item in value): - lines.append(f"{prefix}{key}: [{', '.join(_quote(item) for item in value)}]") - else: - lines.append(f"{prefix}{key}:") - for item in value: - if isinstance(item, dict): - lines.append(f"{prefix} -") - for nested_key, nested_value in item.items(): - _render_nested(lines, indent + 4, str(nested_key), nested_value) - elif isinstance(value, bool): - lines.append(f"{prefix}{key}: {str(value).lower()}") - elif value is None: - lines.append(f"{prefix}{key}: null") - else: - lines.append(f"{prefix}{key}: {_quote(value)}") - - -def _render_bindings(bindings: dict[str, dict[str, object]]) -> str: - lines = ["device_bindings:"] - for workspace_id in sorted(bindings): - binding = bindings[workspace_id] - lines.extend( - [ - f" {_quote(workspace_id)}:", - f" slug: {_quote(binding.get('slug'))}", - f" workspace_root: {_quote(binding.get('workspace_root'))}", - f" control_plane_path: {_quote(binding.get('control_plane_path'))}", - f" control_plane_remote: {_quote(binding.get('control_plane_remote'))}", - f" observed_control_plane_head: {_quote(binding.get('observed_control_plane_head'))}", - ] - ) - known_binding_fields = { - "slug", "workspace_root", "control_plane_path", "control_plane_remote", - "observed_control_plane_head", "repositories", - } - for key in sorted(set(binding) - known_binding_fields): - value = binding.get(key) - _render_nested(lines, 4, key, value) - lines.append(" repositories:") - repositories = binding.get("repositories") - if isinstance(repositories, dict): - for repository_id in sorted(repositories): - repository = repositories[repository_id] - if not isinstance(repository, dict): - continue - lines.extend( - [ - f" {_quote(repository_id)}:", - f" project_root: {_quote(repository.get('project_root'))}", - f" checkout_kind: {_quote(repository.get('checkout_kind'))}", - f" observed_branch: {_quote(repository.get('observed_branch'))}", - f" observed_head: {_quote(repository.get('observed_head'))}", - f" observed_at: {_quote(repository.get('observed_at'))}", - ] - ) - known_repository_fields = { - "project_root", "checkout_kind", "observed_branch", "observed_head", - "observed_at", "git_common_dir", - } - for key in sorted(set(repository) - known_repository_fields): - value = repository.get(key) - _render_nested(lines, 8, key, value) - lines.append(f" git_common_dir: {_quote(repository.get('git_common_dir'))}") - return "\n".join(lines) + "\n" + return path, rendered def _write_bindings(bindings: dict[str, dict[str, object]]) -> Path: @@ -877,21 +747,22 @@ def _write_bindings(bindings: dict[str, dict[str, object]]) -> Path: def _bindings_document(bindings: dict[str, dict[str, object]], original: str) -> str: - lines = original.splitlines() - bounds = _binding_block_bounds(lines) - replacement = _render_bindings(bindings).splitlines() - if bounds: - start, end = bounds - lines = lines[:start] + replacement + lines[end:] - else: - if lines and lines[-1]: - lines.append("") - lines.extend(replacement) - return "\n".join(lines).rstrip() + "\n" + document = parse_yaml_mapping(original, source="project registry") + document.setdefault("projects", []) + document["device_bindings"] = bindings + validate_infrastructure_document(document, family="project-registry") + return dump_canonical_yaml(document) def _registry_bindings() -> dict[str, dict[str, object]]: - return _parse_bindings(read(resolve_project_registry_path())) + registry = resolve_project_registry_path() + document = parse_yaml_mapping(read(registry) or "projects: []\ndevice_bindings: {}\n", source="project registry") + bindings = document.get("device_bindings") + return { + str(key): dict(value) + for key, value in bindings.items() + if isinstance(value, dict) + } if isinstance(bindings, dict) else {} def _binding_from_v3( @@ -900,13 +771,19 @@ def _binding_from_v3( local: dict[str, dict[str, object]] = {} for repository in repositories: path = Path(str(repository.get("project_root") or "")).expanduser().resolve() + observed_branch = _git(path, "branch", "--show-current") + observed_head = _git(path, "rev-parse", "HEAD") + git_common_dir = _git(path, "rev-parse", "--git-common-dir") + checkout_kind = str(repository.get("checkout_kind") or "external") + if not observed_branch and not observed_head and not git_common_dir: + checkout_kind = "manual" local[str(repository.get("id") or "")] = { "project_root": str(path), - "checkout_kind": str(repository.get("checkout_kind") or "external"), - "observed_branch": _git(path, "branch", "--show-current"), - "observed_head": _git(path, "rev-parse", "HEAD"), + "checkout_kind": checkout_kind, + "observed_branch": observed_branch, + "observed_head": observed_head, "observed_at": utc_now_rfc3339(), - "git_common_dir": _git(path, "rev-parse", "--git-common-dir"), + "git_common_dir": git_common_dir, } control = workspace_root / ".work-bundle" return { @@ -921,25 +798,15 @@ def _binding_from_v3( def _portable_failures(text: str) -> list[str]: failures: list[str] = [] - if _yaml_scalar(text, "metadata_version") != VERSION: - failures.append("WB_CONTROL_PLANE_METADATA_VERSION_INVALID") - workspace_id = _workspace_id(text) - if not workspace_id.startswith("wb-"): - failures.append("WB_CONTROL_PLANE_WORKSPACE_ID_INVALID") - if not _workspace_value(text, "slug"): - failures.append("WB_CONTROL_PLANE_WORKSPACE_SLUG_MISSING") - mode = _workspace_value(text, "mode") - if mode not in {"single-repository", "multi-repository", "composite"}: - failures.append("WB_CONTROL_PLANE_WORKSPACE_MODE_INVALID") - control = _block(text, "control_plane") - if not re.search(r"^\s{2}schema_version:\s*1\s*$", control, re.MULTILINE): - failures.append("WB_CONTROL_PLANE_SCHEMA_VERSION_INVALID") - if not re.search(r"^\s{4}mode:\s*manual\s*$", control, re.MULTILINE): - failures.append("WB_CONTROL_PLANE_SYNC_POLICY_INVALID") - forbidden = ("workspace_root", "project_root", "observed_head", "observation_time", "git_control_root") - for key in forbidden: - if re.search(rf"^\s*{key}:\s*", text, re.MULTILINE): - failures.append(f"WB_CONTROL_PLANE_PORTABLE_FIELD_FORBIDDEN:{key}") + try: + document = validate_infrastructure_document( + parse_yaml_mapping(text, source="project metadata"), + family="workspace-project-metadata", + ) + except InfrastructureError as exc: + return [exc.code] + workspace = document["workspace"] + mode = str(workspace["mode"]) try: repositories = _v4_repositories(text) configured_control_remote = _control_plane_remote(text) @@ -1134,30 +1001,85 @@ def cmd_init_workspace(args: list[str]) -> int: return 0 bindings = _registry_bindings() control = workspace_root / ".work-bundle" + local_repositories: dict[str, dict[str, object]] = {} + for repository in repositories: + repository_id = str(repository["id"]) + project_root = workspace_root if parsed.mode == "single-repository" else workspace_root / repository_id + local_repositories[repository_id] = { + "project_root": str(project_root), + "checkout_kind": "workspace-root" if parsed.mode == "single-repository" else "unmaterialized-member", + "observed_branch": _git(project_root, "branch", "--show-current") if project_root.exists() else "", + "observed_head": _git(project_root, "rev-parse", "HEAD") if project_root.exists() else "", + "observed_at": utc_now_rfc3339(), + "git_common_dir": _git(project_root, "rev-parse", "--git-common-dir") if project_root.exists() else "", + } bindings[workspace_id] = { "slug": parsed.slug, "workspace_root": str(workspace_root), "control_plane_path": str(control), "control_plane_remote": "", "observed_control_plane_head": "", - "repositories": {}, + "repositories": local_repositories, } registry = resolve_project_registry_path() - changed = _atomic_publish({ + registry_text = _bindings_document(bindings, read(registry) or "projects: []\n") + try: + portable = validate_infrastructure_document( + parse_yaml_mapping(rendered, source=str(metadata)), family="workspace-project-metadata" + ) + registry_document = validate_infrastructure_document( + parse_yaml_mapping(registry_text, source=str(registry)), family="project-registry" + ) + from infrastructure import join_workspace_binding + join_workspace_binding(portable, registry_document, expected_workspace_root=workspace_root) + except InfrastructureError as exc: + out({"command": "init-workspace", "status": "issues-found", "failure_code": exc.code, "changed_files": []}) + return 1 + payloads = { metadata: rendered, control / ".gitignore": gitignore_text, - registry: _bindings_document(bindings, read(registry) or "projects: []\n"), - }) - for relative in ("knowledge/notes", "knowledge/open-questions", "knowledge/context-packs", "knowledge/indexes", "orchestration/spec/active", "orchestration/plan/active", "orchestration/handoff", "orchestration/docs", "orchestration/principles", "rules", "git", "runtime", "orchestration/execution-state"): - path = control / relative - if not path.exists(): - path.mkdir(parents=True) - changed.append(str(path)) - changed.extend(ensure_workspace_resources(workspace_root)) - changed.extend(_sync_agents(workspace_root)) - if parsed.mode == "single-repository" and (workspace_root / ".git").exists(): - if _ensure_source_local_excludes(workspace_root): - changed.append(str(workspace_root / ".git/info/exclude")) + registry: registry_text, + workspace_root / "AGENTS.md": _render_synced_agents(workspace_root), + } + script_index = workspace_root / "script/index.yaml" + credential_file = workspace_root / "credentials/credentials.yaml" + if not script_index.exists(): + payloads[script_index] = SCRIPT_INDEX_TEMPLATE + if not credential_file.exists(): + payloads[credential_file] = CREDENTIAL_TEMPLATE + source_exclusion = _source_exclude_payload(workspace_root) if parsed.mode == "single-repository" else None + if source_exclusion is not None: + payloads[source_exclusion[0]] = source_exclusion[1] + created_directories: list[Path] = [] + try: + for relative in ( + "knowledge/notes", + "knowledge/open-questions", + "knowledge/context-packs", + "knowledge/indexes", + *CURRENT_ORCHESTRATION_STORE_ROOTS, + "orchestration/docs", + "orchestration/principles", + "rules", + "git", + "runtime", + "orchestration/execution-state", + ): + path = control / relative + if not path.exists(): + path.mkdir(parents=True) + created_directories.append(path) + changed = _atomic_publish(payloads) + except (OSError, ControlPlaneError): + for path in reversed(created_directories): + try: + path.rmdir() + except OSError: + pass + raise + changed.extend(str(path) for path in created_directories) + credential_file.parent.chmod(0o700) + credential_file.chmod(0o600) out({"command": "init-workspace", "status": "passed", "dry_run": False, "workspace_id": workspace_id, "changed_files": sorted(set(changed))}) return 0 @@ -1395,11 +1317,11 @@ def cmd_publish_control_plane(args: list[str]) -> int: return 0 -def apply_layout_v3_to_v4( +def apply_historical_layout_to_v4( workspace_root: Path, remote_overrides: dict[str, str] | None = None, ) -> dict[str, object]: - """Upgrade metadata v3 to portable v4, publishing device bindings atomically with metadata.""" + """Upgrade historical metadata v2 or v3 directly to portable v4.""" workspace_root = workspace_root.expanduser().resolve() metadata_path = workspace_root / ".work-bundle/project.yaml" before = read(metadata_path) @@ -1410,7 +1332,10 @@ def apply_layout_v3_to_v4( raise ControlPlaneError("WB_CONTROL_PLANE_SOURCE_TRACKS_CONTROL_PLANE") proposal = _proposal(workspace_root, before, remote_overrides) gitignore_text = _merged_local_only_gitignore(read(workspace_root / ".work-bundle/.gitignore")) - backup = workspace_root / ".work-bundle/runtime/migrations" / str(proposal["proposal_id"]) / "project-v3.yaml" + source_version = _yaml_scalar(before, "metadata_version") + if source_version not in {"2", "3"}: + raise ControlPlaneError("WB_CONTROL_PLANE_MIGRATION_SOURCE_UNSUPPORTED") + backup = workspace_root / ".work-bundle/runtime/migrations" / str(proposal["proposal_id"]) / f"project-v{source_version}.yaml" gitignore = workspace_root / ".work-bundle/.gitignore" bindings = _registry_bindings() workspace_id = str(proposal["workspace_id"]) @@ -1454,6 +1379,36 @@ def cmd_migrate_control_plane(args: list[str]) -> int: workspace_root = Path(parsed.workspace_root).expanduser().resolve() metadata_path = workspace_root / ".work-bundle/project.yaml" before = read(metadata_path) + try: + source_document = parse_yaml_mapping(before, source=str(metadata_path)) + except InfrastructureError as exc: + out({ + "command": "migrate-control-plane", + "status": "issues-found", + "failure_code": exc.code, + "changed_files": [], + }) + return 1 + source_version = source_document.get("metadata_version") + if source_version == 4: + out({ + "command": "migrate-control-plane", + "status": "passed", + "dry_run": bool(parsed.dry_run), + "workspace_root": str(workspace_root), + "migration": {"from_version": 4, "to_version": 4, "disposition": "current"}, + "changed_files": [], + }) + return 0 + if source_version not in {2, 3, "2", "3"}: + out({ + "command": "migrate-control-plane", + "status": "issues-found", + "failure_code": "WB_CONTROL_PLANE_MIGRATION_SOURCE_UNSUPPORTED", + "metadata_version": source_version, + "changed_files": [], + }) + return 1 try: remote_overrides = { str(item["id"]): str(item["remote"]) @@ -1499,7 +1454,13 @@ def cmd_migrate_control_plane(args: list[str]) -> int: {"id": item.get("id"), "canonical_remote": canonical_remote(item.get("remote"))} for item in proposal["repositories"] if isinstance(item, dict) ], - "portable_paths": ["project.yaml", "knowledge/", "orchestration/spec/", "orchestration/plan/", "orchestration/handoff/", "orchestration/docs/", "rules/"], + "portable_paths": [ + "project.yaml", + "knowledge/", + *CURRENT_ORCHESTRATION_PORTABLE_PATHS, + "orchestration/docs/", + "rules/", + ], "local_only_paths": ["git/", "runtime/", "orchestration/execution-state/"], "control_plane_git": { "currently_initialized": ( @@ -1519,7 +1480,7 @@ def cmd_migrate_control_plane(args: list[str]) -> int: out({**base, "status": "issues-found", "failure_code": "WB_CONTROL_PLANE_PROPOSAL_STALE", "changed_files": []}) return 1 try: - applied = apply_layout_v3_to_v4(workspace_root, remote_overrides) + applied = apply_historical_layout_to_v4(workspace_root, remote_overrides) except ControlPlaneError as exc: out({**base, "status": "issues-found", "failure_code": exc.code, "changed_files": [], **exc.details}) return 1 @@ -1685,66 +1646,59 @@ def _classify_workspace_member( return "absent" -def _render_member_metadata_block(member: dict[str, str], *, multi: bool = False) -> str: +def _member_metadata_record(member: dict[str, str], *, multi: bool = False) -> dict[str, object]: state = member.get("materialization", "") - remote_line = f" canonical: {_quote(member['remote'])}" if member["remote"] else " canonical: null" - lines = [ - f" - id: {_quote(member['repository_id'])}", - " role: source", - " remote:", - remote_line, - " aliases: []", - f" default_branch: {_quote(member['default_branch'])}", - " workspace_binding:", - " type: member", - f" name: {_quote(member['name'])}", - *([] if multi else [f" path: {_quote(member['path'])}"]), - " materialization:", - " required: true", - *([f" state: {state}"] if state else []), - ] + binding: dict[str, object] = {"type": "member", "name": member["name"]} + if not multi: + binding["path"] = member["path"] + record: dict[str, object] = { + "id": member["repository_id"], + "role": "source", + "remote": {"canonical": member["remote"] or None, "aliases": []}, + "default_branch": member["default_branch"], + "workspace_binding": binding, + "materialization": {"required": True}, + "operation_policy": "inherit", + } if state: - lines.extend( - [ - " deferred_remote:", - f" proposal_id: {_quote(member['proposal_id'])}", - f" transaction_id: {_quote(member['transaction_id'])}", - f" replay_key: {_quote(member['replay_key'])}", - ] - ) - lines.append(" operation_policy: inherit") - return "\n".join(lines) + "\n" + record["materialization"]["state"] = state + record["deferred_remote"] = { + "proposal_id": member["proposal_id"], + "transaction_id": member["transaction_id"], + "replay_key": member["replay_key"], + } + return record def _append_member_metadata(text: str, member: dict[str, str]) -> str: - if _workspace_value(text, "mode") == "single-repository": - text = re.sub(r"^(\s{2}mode: )single-repository\s*$", r"\1composite", text, count=1, flags=re.MULTILINE) - block = _render_member_metadata_block(member, multi=_workspace_value(text, "mode") == "multi-repository") - lines = text.splitlines(keepends=True) - bounds = _source_repository_bounds(lines) - if bounds is None: + document = parse_yaml_mapping(text, source="project metadata") + workspace = document.get("workspace") + repositories = document.get("source_repositories") + if not isinstance(workspace, dict) or not isinstance(repositories, list): raise ControlPlaneError("WB_CONTROL_PLANE_METADATA_INVALID") - _, end = bounds - prefix = "".join(lines[:end]) - return prefix + ("" if prefix.endswith("\n") else "\n") + block + "".join(lines[end:]) + if workspace.get("mode") == "single-repository": + workspace["mode"] = "composite" + repositories.append( + _member_metadata_record(member, multi=workspace.get("mode") == "multi-repository") + ) + validate_infrastructure_document(document, family="workspace-project-metadata") + return dump_canonical_yaml(document) def _replace_member_metadata(text: str, repository_id: str, member: dict[str, str]) -> str: - lines = text.splitlines(keepends=True) - bounds = _source_repository_bounds(lines) - if bounds is None: + document = parse_yaml_mapping(text, source="project metadata") + workspace = document.get("workspace") + repositories = document.get("source_repositories") + if not isinstance(workspace, dict) or not isinstance(repositories, list): raise ControlPlaneError("WB_CONTROL_PLANE_METADATA_INVALID") - start, end = bounds - starts = [index for index in range(start + 1, end) if re.match(r"^ - id:\s*", lines[index])] - for position, item_start in enumerate(starts): - raw_id = lines[item_start].split(":", 1)[1].strip().strip('"').strip("'") - if raw_id != repository_id: + for index, repository in enumerate(repositories): + if not isinstance(repository, dict) or str(repository.get("id") or "") != repository_id: continue - item_end = starts[position + 1] if position + 1 < len(starts) else end - rendered = _render_member_metadata_block( - member, multi=_workspace_value(text, "mode") == "multi-repository" + repositories[index] = _member_metadata_record( + member, multi=workspace.get("mode") == "multi-repository" ) - return "".join(lines[:item_start]) + rendered + "".join(lines[item_end:]) + validate_infrastructure_document(document, family="workspace-project-metadata") + return dump_canonical_yaml(document) raise ControlPlaneError("WB_CONTROL_PLANE_DEFERRED_REMOTE_MISSING") @@ -2303,6 +2257,16 @@ def rollback_attach() -> None: "observed_control_plane_head": _git(control, "rev-parse", "HEAD"), "repositories": local_repositories, } + registry_preview = parse_yaml_mapping( + _bindings_document(bindings, read(resolve_project_registry_path()) or "projects: []\n"), + source="project registry", + ) + portable_preview = parse_yaml_mapping(read(metadata_path), source=str(metadata_path)) + join_workspace_binding( + portable_preview, + registry_preview, + expected_workspace_root=workspace_root, + ) readiness_failures = [] repositories_by_id = {str(item.get("id") or ""): item for item in repositories} for repository_id, local in local_repositories.items(): @@ -2315,6 +2279,9 @@ def rollback_attach() -> None: project_root, str(portable.get("default_branch") or ""), repository_id ) ) + except InfrastructureError as exc: + rollback_attach() + raise ControlPlaneError(exc.code, exc.details) from exc except ControlPlaneError: rollback_attach() raise diff --git a/scripts/work-bundle/core.py b/scripts/work-bundle/core.py index a3b4c2d..f8d9251 100644 --- a/scripts/work-bundle/core.py +++ b/scripts/work-bundle/core.py @@ -9,6 +9,14 @@ from datetime import datetime, timezone from pathlib import Path +from infrastructure import ( + atomic_write_text, + load_bootstrap, + parse_yaml_mapping, + resolve_config_root, + resolve_project_registry_path as infrastructure_registry_path, +) + CUSTOMIZED_SKILL_ROOT = Path(__file__).resolve().parents[2] / 'skills' GLOBAL_SKILL_REGISTRY = '~/.work-bundle/skills/skill-registry.yaml' @@ -34,22 +42,17 @@ RULES = ['repository-boundary', 'lifecycle-authority', 'skill-registry', 'domain-profile', 'doctor-readonly', 'runtime-artifact-format', 'security-exclusion'] CLI_HELP_EPILOG = '''Canonical consolidated command surface: - init-project <project-root> --mode <single-repository|multi-repository> [--workspace-root <workspace-root>] + init-workspace <workspace-root> --slug <slug> --repository <id=remote> --mode <single-repository|multi-repository> (--dry-run|--apply) show-project [--workspace-root <workspace-root> | --project-root <project-root>] validate-project <project-root> --dry-run doctor-project <project-root> [--repair] [--force] - migrate-project <project-root> --dry-run migrate-control-plane <workspace-root> (--dry-run|--apply --accepted-proposal-id <id>) migrate-registered-projects (--dry-run|--apply --accepted-plan-id <id>) [--slug <slug>] - init-workspace <workspace-root> --slug <slug> --repository <id=remote> (--dry-run|--apply) publish-control-plane <workspace-root> --remote <git-remote> (--dry-run|--apply) attach-workspace <workspace-root> [--materialize none|missing|all] (--dry-run|--apply) doctor-workspace <workspace-root> [--repair] add-workspace-member <workspace-root> --repository-id <id> --remote <observed-url> --name <binding-name> --path <relative-path> --default-branch <branch> (--dry-run|--accepted-proposal-id <id> --apply) detach-workspace <workspace-root> --apply - migrate-to-multi-repository <source-project-root> --target-workspace-root <target> [--origin <git-origin>] ... - provision-member --workspace-root <workspace-root> --origin <git-origin> ... (--dry-run|--apply) - cleanup-member --workspace-root <workspace-root> --repository-id <id> (--dry-run|--apply) execution-workspace-prepare --workspace-root <workspace-root> --source-repository <repo> ... execution-workspace-status --runtime-root <runtime-root> --workspace-id <id> ... execution-workspace-mark-terminal --runtime-root <runtime-root> --workspace-id <id> --status <integrated|discarded|retired> --evidence <reference> @@ -67,6 +70,12 @@ generate-domain-profile => generate-project-metadata-profile merge-domain-profile => merge-project-metadata-profile validate-domain-profile => validate-project-metadata-profile + +Retired metadata commands return typed migration guidance: + init-project, initialize-project => init-workspace + migrate-project => migrate-control-plane or migrate-registered-projects + migrate-to-multi-repository => init-workspace or migrate-control-plane + provision-member, cleanup-member => add-workspace-member or doctor-workspace ''' @@ -78,13 +87,22 @@ def read(path: Path) -> str: return path.read_text(encoding='utf-8') if path.exists() else '' +def compact_yaml_map(text: str) -> dict[str, str]: + """Compatibility view over the maintained YAML parser for scalar config fields.""" + document = parse_yaml_mapping(text, source='configuration') + return { + str(key): '' if value is None else str(value) + for key, value in document.items() + if not isinstance(value, (dict, list)) + } + + def write(path: Path, data: str, overwrite: bool = True) -> bool: - path.parent.mkdir(parents=True, exist_ok=True) if path.exists() and not overwrite: return False if read(path) == data: return False - path.write_text(data, encoding='utf-8') + atomic_write_text(path, data) return True @@ -111,30 +129,11 @@ def duty_items(text: str, key: str) -> list[str]: def work_bundle_config_root() -> Path: - override = os.environ.get(WORK_BUNDLE_CONFIG_ROOT_ENV, '').strip() - if override: - return Path(override).expanduser().resolve() - return Path.home() / '.work-bundle' + return resolve_config_root() def resolve_project_registry_path() -> Path: - config_root = work_bundle_config_root() - bootstrap_path = config_root / GLOBAL_BOOTSTRAP_FILE_NAME - bootstrap = compact_yaml_map(read(bootstrap_path)) if bootstrap_path.is_file() else {} - value = bootstrap.get('project_registry', '$work_bundle_config_root/registry/projects.yaml') - value = value.replace('$work_bundle_config_root', str(config_root)) - return Path(value).expanduser().resolve() - - -def compact_yaml_map(text: str) -> dict[str, str]: - data: dict[str, str] = {} - for raw in text.splitlines(): - line = raw.strip() - if not line or line.startswith('#') or ':' not in line: - continue - key, value = line.split(':', 1) - data[key.strip()] = value.strip().strip('"').strip("'") - return data + return infrastructure_registry_path() def utc_now_rfc3339() -> str: @@ -150,7 +149,7 @@ def resolve_work_bundle_root() -> Path | None: config_root = work_bundle_config_root() bootstrap_path = config_root / GLOBAL_BOOTSTRAP_FILE_NAME - bootstrap = compact_yaml_map(read(bootstrap_path)) if bootstrap_path.is_file() else {} + bootstrap = load_bootstrap() if bootstrap_path.is_file() else {} bootstrap_root_raw = bootstrap.get('work_bundle_root', '').strip() if bootstrap_root_raw: candidate = Path(bootstrap_root_raw).expanduser() diff --git a/scripts/work-bundle/dispatcher.py b/scripts/work-bundle/dispatcher.py index 9957285..0fb7657 100644 --- a/scripts/work-bundle/dispatcher.py +++ b/scripts/work-bundle/dispatcher.py @@ -2,7 +2,6 @@ import argparse import importlib.util -import subprocess import sys from pathlib import Path @@ -40,37 +39,6 @@ from registry_layout import cmd_migrate_registered_projects -def _load_review_runtime(): - return _load_reviewer_workspace()._review_runtime() - - -def _load_legacy_wor107_migration(): - module_path = ( - Path(__file__).resolve().parents[1] - / 'orchestration' - / 'legacy_wor107_migration.py' - ) - spec = importlib.util.spec_from_file_location( - '_wb_legacy_wor107_migration', module_path - ) - if spec is None or spec.loader is None: - raise RuntimeError(f'Unable to load legacy migration utility: {module_path}') - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -def _load_reviewer_workspace(): - module_path = Path(__file__).with_name('reviewer_workspace.py') - spec = importlib.util.spec_from_file_location('_wb_reviewer_workspace', module_path) - if spec is None or spec.loader is None: - raise RuntimeError(f'Unable to load reviewer workspace runtime: {module_path}') - module = importlib.util.module_from_spec(spec) - sys.modules[spec.name] = module - spec.loader.exec_module(module) - return module - - def _load_evaluation_identity(): module_path = Path(__file__).resolve().parents[1] / 'orchestration' / 'evaluation_identity.py' spec = importlib.util.spec_from_file_location('_wb_evaluation_identity', module_path) @@ -96,7 +64,6 @@ def _load_evaluation_identity(): 'validate-project-initialization': 'validate-project', 'validate-runtime-artifacts': 'doctor', 'validate-repository-health': 'repository-health', - 'validate-workflow-branches': 'workflow-branches', } EXECUTION_WORKSPACE_COMMANDS = frozenset({ 'execution-workspace-prepare', @@ -117,15 +84,12 @@ def _load_evaluation_identity(): 'validate-project', 'create-rules', 'validate-rules', 'defect-ensure-store', 'defect-create-evidence', 'defect-build-index', 'defect-write-index', 'defect-archive-evidence', 'defect-migrate-store', - 'validate-contract', 'assert-migration-stop', - 'reviewer-workspace-create', 'reviewer-workspace-operation', - 'reviewer-workspace-cleanup', 'reviewer-process-run', 'evaluation-identity-freeze', 'evaluation-identity-complete', 'evaluation-identity-transition', 'stage-event-append', 'stage-event-query', 'stage-event-export', 'doctor', 'repository-health', 'validate-directive-wiring', 'validate-skill-registry', 'validate-work-bundle-rules', - 'render-doctor-report', 'workflow-branches', + 'render-doctor-report', 'generate-project-metadata-profile', 'merge-project-metadata-profile', 'validate-project-metadata-profile', 'inspect-skill', 'validate-registry-entry', 'register-skill', 'merge-skill-hints', @@ -135,27 +99,7 @@ def _load_evaluation_identity(): | COMMAND_ALIASES.keys() | LEGACY_DEFECT_COMMANDS.keys() | LEGACY_COMMAND_MIGRATIONS.keys() -) | frozenset({ - 'begin-review-round', 'complete-review-round', 'review-round-status', - 'finalize-with-blockers', -}) - - -def _run_orchestration_controller(command: str, arguments: list[str]) -> int: - """Route the second public CLI family to the canonical controller owner.""" - - dispatcher = Path(__file__).resolve().parents[1] / 'orchestration' / 'dispatcher.py' - completed = subprocess.run( - [sys.executable, str(dispatcher), command, *arguments], - text=True, - capture_output=True, - check=False, - ) - if completed.stdout: - sys.stdout.write(completed.stdout) - if completed.stderr: - sys.stderr.write(completed.stderr) - return completed.returncode +) def main() -> int: @@ -174,11 +118,6 @@ def main() -> int: if command in LEGACY_COMMAND_MIGRATIONS: return cmd_legacy_command_removed(command, LEGACY_COMMAND_MIGRATIONS[command]) command = COMMAND_ALIASES.get(command, command) - if command in { - 'begin-review-round', 'complete-review-round', 'review-round-status', - 'finalize-with-blockers', - }: - return _run_orchestration_controller(command, parsed.args) if command == 'migrate-work-bundle-config': return cmd_migrate_work_bundle_config(parsed.args) if command in {'init-project', 'initialize-project'}: @@ -217,14 +156,6 @@ def main() -> int: return cmd_provision_member(parsed.args) if command == 'cleanup-member': return cmd_cleanup_member(parsed.args) - if command == 'validate-contract': - return _load_review_runtime().cmd_validate_contract(parsed.args) - if command == 'assert-migration-stop': - legacy = _load_legacy_wor107_migration() - print(legacy.DEPRECATION_DIAGNOSTIC, file=sys.stderr) - return legacy.cmd_assert_migration_stop(parsed.args) - if command in {'reviewer-workspace-create', 'reviewer-workspace-operation', 'reviewer-workspace-cleanup', 'reviewer-process-run'}: - return _load_reviewer_workspace().cmd_reviewer_workspace(command, parsed.args) if command in {'evaluation-identity-freeze', 'evaluation-identity-complete', 'evaluation-identity-transition'}: return _load_evaluation_identity().cmd_evaluation_identity(command, parsed.args) if command in {'stage-event-append', 'stage-event-query', 'stage-event-export'}: @@ -270,8 +201,6 @@ def main() -> int: return cmd_doctor(parsed.args) if command == 'render-doctor-report': return cmd_doctor(parsed.args, report=True) - if command == 'workflow-branches': - return cmd_doctor(parsed.args, workflow=True) if command == 'generate-project-metadata-profile': return cmd_domain_profile(parsed.args) if command == 'merge-project-metadata-profile': diff --git a/scripts/work-bundle/doctor.py b/scripts/work-bundle/doctor.py index 5d4e32c..635939a 100644 --- a/scripts/work-bundle/doctor.py +++ b/scripts/work-bundle/doctor.py @@ -1,22 +1,11 @@ from core import * from project import inspect_project, project_failures -def cmd_doctor(args: list[str], report: bool = False, workflow: bool = False) -> int: +def cmd_doctor(args: list[str], report: bool = False) -> int: parser = argparse.ArgumentParser(prog='wb.py doctor') parser.add_argument('project_root') parsed = parser.parse_args(args) project_root = Path(parsed.project_root).resolve() - if workflow: - required = ['success', 'blocked', 'invalid', 'missing-context', 'repair-needed', 'no-op-idempotent', 'read-only-diagnosis'] - evidence = project_root / '.work-bundle/orchestration/reviews/branch-validation/evidence.json' - if not evidence.exists(): - out({'status': 'issues-found', 'failures': ['missing_branch_evidence'], 'required_branches': required}) - return 1 - data = json.loads(read(evidence)) - seen = {item.get('branch') for item in data.get('evidence', [])} - missing = [branch for branch in required if branch not in seen] - out({'status': 'passed' if not missing else 'issues-found', 'missing': missing, 'evidence': str(evidence)}) - return 0 if not missing else 1 data = inspect_project(project_root) failures = project_failures(data, strict=False, include_roles=True) if report: @@ -31,4 +20,3 @@ def cmd_doctor(args: list[str], report: bool = False, workflow: bool = False) -> else: out({'status': 'passed' if not failures else 'issues-found', 'failures': failures, 'files_changed': 'none', 'data': data}) return 0 if not failures else 1 - diff --git a/scripts/work-bundle/infrastructure.py b/scripts/work-bundle/infrastructure.py new file mode 100644 index 0000000..a93a1a4 --- /dev/null +++ b/scripts/work-bundle/infrastructure.py @@ -0,0 +1,557 @@ +"""Shared structural contracts for WorkBundle infrastructure metadata.""" + +from __future__ import annotations + +from copy import deepcopy +from dataclasses import dataclass +import json +import os +from pathlib import Path +import subprocess +import tempfile +from typing import Any, Mapping, Sequence + +import jsonschema +import yaml + + +CATALOG_RELATIVE_PATH = Path( + "references/assets/infrastructure/contract/infrastructure-schema-catalog-v1.yaml" +) +CONFIG_ROOT_TOKEN = "$work_bundle_config_root" + + +class InfrastructureError(RuntimeError): + """Typed structural failure consumable by public dispatchers.""" + + def __init__(self, code: str, message: str, *, details: Mapping[str, Any] | None = None): + super().__init__(message) + self.code = code + self.details = dict(details or {}) + + +@dataclass(frozen=True) +class AnchorContext: + config_root: Path + workspace_root: Path + project_root: Path | None + workspace_id: str + repository_id: str | None + + +class _UniqueKeyLoader(yaml.SafeLoader): + pass + + +_UniqueKeyLoader.yaml_implicit_resolvers = deepcopy(yaml.SafeLoader.yaml_implicit_resolvers) +for first_character, resolvers in list(_UniqueKeyLoader.yaml_implicit_resolvers.items()): + _UniqueKeyLoader.yaml_implicit_resolvers[first_character] = [ + (tag, expression) + for tag, expression in resolvers + if tag != "tag:yaml.org,2002:timestamp" + ] + + +def _construct_mapping(loader: _UniqueKeyLoader, node: yaml.MappingNode, deep: bool = False) -> dict[Any, Any]: + result: dict[Any, Any] = {} + for key_node, value_node in node.value: + key = loader.construct_object(key_node, deep=deep) + if key in result: + raise yaml.constructor.ConstructorError( + "while constructing a mapping", + node.start_mark, + f"found duplicate key {key!r}", + key_node.start_mark, + ) + result[key] = loader.construct_object(value_node, deep=deep) + return result + + +_UniqueKeyLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _construct_mapping) + + +def _toolkit_root(toolkit_root: str | Path | None) -> Path: + return Path(toolkit_root).expanduser().resolve() if toolkit_root else Path(__file__).resolve().parents[2] + + +def parse_yaml_mapping(text: str, *, source: str) -> dict[str, Any]: + try: + loaded = yaml.load(text, Loader=_UniqueKeyLoader) + except yaml.YAMLError as exc: + raise InfrastructureError( + "WB_INFRASTRUCTURE_YAML_INVALID", f"Invalid YAML in {source}: {exc}", details={"source": source} + ) from exc + if not isinstance(loaded, dict): + raise InfrastructureError( + "WB_INFRASTRUCTURE_MAPPING_REQUIRED", + f"Infrastructure document must be a mapping: {source}", + details={"source": source}, + ) + return loaded + + +def load_yaml_mapping(path: str | Path) -> dict[str, Any]: + source_path = Path(path).expanduser().resolve() + try: + text = source_path.read_text(encoding="utf-8") + except OSError as exc: + raise InfrastructureError( + "WB_INFRASTRUCTURE_READ_FAILED", f"Unable to read {source_path}: {exc}", details={"path": str(source_path)} + ) from exc + return parse_yaml_mapping(text, source=str(source_path)) + + +def load_schema_catalog(*, toolkit_root: str | Path | None = None) -> dict[str, Any]: + root = _toolkit_root(toolkit_root) + catalog = load_yaml_mapping(root / CATALOG_RELATIVE_PATH) + if catalog.get("schema_version") != 1 or not isinstance(catalog.get("families"), dict): + raise InfrastructureError( + "WB_INFRASTRUCTURE_CATALOG_INVALID", "Infrastructure schema catalog must be version 1" + ) + return catalog + + +def _schema_for(family: str, *, toolkit_root: str | Path | None) -> dict[str, Any]: + root = _toolkit_root(toolkit_root) + catalog = load_schema_catalog(toolkit_root=root) + descriptor = catalog["families"].get(family) + if not isinstance(descriptor, dict) or descriptor.get("version") != 1: + raise InfrastructureError( + "WB_INFRASTRUCTURE_SCHEMA_FAMILY_UNKNOWN", f"Unknown infrastructure schema family: {family}" + ) + relative = descriptor.get("schema") + if not isinstance(relative, str) or not relative: + raise InfrastructureError("WB_INFRASTRUCTURE_CATALOG_INVALID", f"Missing schema path for {family}") + contract_root = (root / CATALOG_RELATIVE_PATH).parent.resolve() + schema_path = (contract_root / relative).resolve() + if schema_path.parent != contract_root: + raise InfrastructureError("WB_INFRASTRUCTURE_CATALOG_INVALID", f"Schema path escapes catalog: {relative}") + try: + schema = json.loads(schema_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise InfrastructureError( + "WB_INFRASTRUCTURE_SCHEMA_READ_FAILED", f"Unable to load schema {schema_path}: {exc}" + ) from exc + if not isinstance(schema, dict): + raise InfrastructureError("WB_INFRASTRUCTURE_SCHEMA_INVALID", f"Schema is not an object: {schema_path}") + return schema + + +def _require_unique_strings(values: Sequence[Mapping[str, Any]], key: str, *, family: str) -> None: + seen: set[str] = set() + for item in values: + value = item.get(key) + if isinstance(value, str) and value in seen: + raise InfrastructureError( + "WB_INFRASTRUCTURE_ID_DUPLICATE", + f"Duplicate {key} {value!r} in {family}", + details={"family": family, "key": key, "value": value}, + ) + if isinstance(value, str): + seen.add(value) + + +def validate_infrastructure_document( + data: Mapping[str, Any], *, family: str, toolkit_root: str | Path | None = None +) -> dict[str, Any]: + schema = _schema_for(family, toolkit_root=toolkit_root) + try: + jsonschema.Draft202012Validator.check_schema(schema) + jsonschema.Draft202012Validator(schema).validate(data) + except (jsonschema.SchemaError, jsonschema.ValidationError) as exc: + path = "/".join(str(part) for part in getattr(exc, "absolute_path", ())) + raise InfrastructureError( + "WB_INFRASTRUCTURE_SCHEMA_INVALID", + f"{family} failed structural validation{f' at {path}' if path else ''}: {exc.message}", + details={"family": family, "path": path}, + ) from exc + if family == "workspace-project-metadata": + repositories = data.get("source_repositories", []) + if isinstance(repositories, list): + _require_unique_strings(repositories, "id", family=family) + workspace = data.get("workspace", {}) + mode = workspace.get("mode") if isinstance(workspace, Mapping) else None + root_count = 0 + member_names: set[str] = set() + for repository in repositories: + binding = repository.get("workspace_binding", {}) + binding_type = binding.get("type") if isinstance(binding, Mapping) else None + if binding_type == "root": + root_count += 1 + elif binding_type == "member": + name = binding.get("name") + if name in member_names: + raise InfrastructureError( + "WB_INFRASTRUCTURE_ID_DUPLICATE", + f"Duplicate workspace member name {name!r}", + details={"family": family, "key": "workspace_binding.name", "value": name}, + ) + member_names.add(name) + invalid_topology = ( + (mode == "single-repository" and (len(repositories) != 1 or root_count != 1)) + or (mode == "multi-repository" and root_count != 0) + or (mode == "composite" and root_count != 1) + ) + if invalid_topology: + raise InfrastructureError( + "WB_INFRASTRUCTURE_SCHEMA_INVALID", + f"Repository bindings do not satisfy {mode!r} workspace topology", + details={"family": family, "mode": mode}, + ) + elif family == "project-registry": + projects = data.get("projects", []) + if isinstance(projects, list): + _require_unique_strings(projects, "slug", family=family) + return deepcopy(dict(data)) + + +def load_infrastructure_document( + path: str | Path, *, family: str, toolkit_root: str | Path | None = None +) -> dict[str, Any]: + return validate_infrastructure_document( + load_yaml_mapping(path), family=family, toolkit_root=toolkit_root + ) + + +def dump_canonical_yaml(data: Mapping[str, Any]) -> str: + return yaml.safe_dump(dict(data), allow_unicode=True, default_flow_style=False, sort_keys=True) + + +def atomic_write_text(path: str | Path, content: str) -> None: + target = Path(path).expanduser().resolve() + target.parent.mkdir(parents=True, exist_ok=True) + temporary: Path | None = None + try: + descriptor, temporary_name = tempfile.mkstemp(prefix=f".{target.name}.", dir=target.parent) + temporary = Path(temporary_name) + with os.fdopen(descriptor, "w", encoding="utf-8", newline="") as handle: + handle.write(content) + handle.flush() + os.fsync(handle.fileno()) + os.replace(temporary, target) + temporary = None + except OSError as exc: + raise InfrastructureError( + "WB_INFRASTRUCTURE_ATOMIC_WRITE_FAILED", f"Unable to atomically write {target}: {exc}" + ) from exc + finally: + if temporary is not None: + try: + temporary.unlink() + except OSError: + pass + + +def resolve_config_root(config_root: str | Path | None = None) -> Path: + return Path(config_root).expanduser().resolve() if config_root else (Path.home() / ".work-bundle").resolve() + + +def load_bootstrap( + *, config_root: str | Path | None = None, toolkit_root: str | Path | None = None +) -> dict[str, Any]: + root = resolve_config_root(config_root) + return load_infrastructure_document( + root / "bootstrap.yaml", family="bootstrap-config", toolkit_root=toolkit_root + ) + + +def resolve_project_registry_path( + *, config_root: str | Path | None = None, toolkit_root: str | Path | None = None +) -> Path: + root = resolve_config_root(config_root) + raw = load_bootstrap(config_root=root, toolkit_root=toolkit_root)["project_registry"] + if raw == CONFIG_ROOT_TOKEN: + return root + prefix = CONFIG_ROOT_TOKEN + "/" + return ((root / raw[len(prefix) :]) if raw.startswith(prefix) else Path(raw).expanduser()).resolve() + + +def load_project_registry( + *, config_root: str | Path | None = None, toolkit_root: str | Path | None = None +) -> dict[str, Any]: + return load_infrastructure_document( + resolve_project_registry_path(config_root=config_root, toolkit_root=toolkit_root), + family="project-registry", + toolkit_root=toolkit_root, + ) + + +def find_workspace_root(start: str | Path) -> Path | None: + candidate = Path(start).expanduser().resolve() + if candidate.is_file(): + candidate = candidate.parent + for current in (candidate, *candidate.parents): + if (current / ".work-bundle/project.yaml").is_file(): + return current + return None + + +def load_workspace_metadata( + workspace_root: str | Path, *, toolkit_root: str | Path | None = None +) -> dict[str, Any]: + return load_infrastructure_document( + Path(workspace_root).expanduser().resolve() / ".work-bundle/project.yaml", + family="workspace-project-metadata", + toolkit_root=toolkit_root, + ) + + +def _is_within(path: Path, root: Path) -> bool: + try: + path.relative_to(root) + return True + except ValueError: + return False + + +def join_workspace_binding( + metadata: Mapping[str, Any], + registry: Mapping[str, Any], + *, + expected_workspace_root: str | Path | None = None, +) -> dict[str, Any]: + workspace = metadata.get("workspace") + workspace_id = workspace.get("id") if isinstance(workspace, Mapping) else None + bindings = registry.get("device_bindings") + binding = bindings.get(workspace_id) if isinstance(bindings, Mapping) else None + if not isinstance(binding, Mapping): + raise InfrastructureError( + "WB_INFRASTRUCTURE_WORKSPACE_BINDING_MISSING", + f"No device binding exists for workspace {workspace_id!r}", + details={"workspace_id": workspace_id}, + ) + if binding.get("slug") != workspace.get("slug"): + raise InfrastructureError( + "WB_INFRASTRUCTURE_WORKSPACE_BINDING_CONTRADICTORY", + "The device binding slug contradicts portable workspace metadata", + ) + actual_workspace = Path(str(binding.get("workspace_root", ""))).expanduser().resolve() + if expected_workspace_root is not None and actual_workspace != Path(expected_workspace_root).expanduser().resolve(): + raise InfrastructureError( + "WB_INFRASTRUCTURE_WORKSPACE_BINDING_CONTRADICTORY", + "The device binding workspace root contradicts the selected workspace", + ) + portable_repositories = metadata.get("source_repositories") + bound_repositories = binding.get("repositories") + if not isinstance(portable_repositories, list) or not isinstance(bound_repositories, Mapping): + raise InfrastructureError("WB_INFRASTRUCTURE_BINDING_INVALID", "Workspace binding repositories are invalid") + portable_ids = {repository.get("id") for repository in portable_repositories if isinstance(repository, Mapping)} + if set(bound_repositories) - portable_ids: + raise InfrastructureError( + "WB_INFRASTRUCTURE_REPOSITORY_BINDING_CONTRADICTORY", + "Device binding contains repositories absent from portable metadata", + ) + normalized_repositories: dict[str, dict[str, Any]] = {} + mode = workspace.get("mode") if isinstance(workspace, Mapping) else None + for repository in portable_repositories: + repository_id = repository.get("id") if isinstance(repository, Mapping) else None + local = bound_repositories.get(repository_id) + materialization = repository.get("materialization", {}) if isinstance(repository, Mapping) else {} + required = materialization.get("required", False) if isinstance(materialization, Mapping) else False + materialization_state = materialization.get("state") if isinstance(materialization, Mapping) else None + if not isinstance(local, Mapping): + if materialization_state in {"deferred", "failed"}: + continue + if required: + raise InfrastructureError( + "WB_INFRASTRUCTURE_REPOSITORY_BINDING_MISSING", + f"No device binding exists for repository {repository_id!r}", + ) + continue + project_root = Path(str(local.get("project_root", ""))).expanduser().resolve() + portable_binding = repository.get("workspace_binding", {}) + root_binding = portable_binding.get("type") == "root" + expected_project_root = ( + actual_workspace + if root_binding or mode == "single-repository" + else (actual_workspace / str(portable_binding.get("path") or portable_binding.get("name") or "")).resolve() + ) + if project_root != expected_project_root: + raise InfrastructureError( + "WB_INFRASTRUCTURE_PROJECT_ROOT_ESCAPE", + f"Repository {repository_id!r} does not match its portable workspace binding", + details={"repository_id": repository_id, "project_root": str(project_root)}, + ) + checkout_kind = str(local.get("checkout_kind") or "") + observed_at = str(local.get("observed_at") or "") + if not checkout_kind or not observed_at: + raise InfrastructureError( + "WB_INFRASTRUCTURE_OBSERVATION_MISSING", + f"Repository {repository_id!r} device binding lacks checkout or observation evidence", + details={"repository_id": repository_id}, + ) + portable_locator = repository.get("locator") if isinstance(repository, Mapping) else None + if checkout_kind == "manual": + if not isinstance(portable_locator, Mapping) or portable_locator.get("type") != "manual": + raise InfrastructureError( + "WB_INFRASTRUCTURE_REPOSITORY_BINDING_CONTRADICTORY", + f"Repository {repository_id!r} uses a manual binding without a portable manual locator", + ) + elif checkout_kind == "unmaterialized-member": + if any(str(local.get(field) or "") for field in ("observed_branch", "observed_head", "git_common_dir")): + raise InfrastructureError( + "WB_INFRASTRUCTURE_REPOSITORY_BINDING_CONTRADICTORY", + f"Unmaterialized repository {repository_id!r} carries invented checkout observations", + ) + else: + missing_observations = [ + field for field in ("observed_branch", "observed_head", "git_common_dir") + if not str(local.get(field) or "") + ] + if missing_observations: + raise InfrastructureError( + "WB_INFRASTRUCTURE_OBSERVATION_MISSING", + f"Repository {repository_id!r} device binding lacks checkout observations", + details={"repository_id": repository_id, "fields": missing_observations}, + ) + common_raw = str(local.get("git_common_dir") or "") + if common_raw: + common_dir = Path(common_raw).expanduser() + if not common_dir.is_absolute(): + common_dir = project_root / common_dir + common_dir = common_dir.resolve() + if not _is_within(common_dir, actual_workspace): + raise InfrastructureError( + "WB_INFRASTRUCTURE_GIT_COMMON_DIR_ESCAPE", + f"Repository {repository_id!r} git common directory escapes the workspace", + ) + normalized = deepcopy(dict(local)) + normalized["project_root"] = project_root + normalized_repositories[str(repository_id)] = normalized + result = deepcopy(dict(binding)) + result["workspace_root"] = actual_workspace + result["repositories"] = normalized_repositories + return result + + +def resolve_anchor_context( + *, + workspace_root: str | Path | None = None, + project_root: str | Path | None = None, + cwd: str | Path | None = None, + config_root: str | Path | None = None, + toolkit_root: str | Path | None = None, + member_required: bool = False, +) -> AnchorContext: + selected_project = Path(project_root).expanduser().resolve() if project_root is not None else None + selected_workspace = Path(workspace_root).expanduser().resolve() if workspace_root is not None else None + current = Path(cwd).expanduser().resolve() if cwd is not None else Path.cwd().resolve() + inferred = find_workspace_root(selected_project or current) + if selected_workspace is None: + selected_workspace = inferred + elif selected_project is not None and inferred is not None and inferred != selected_workspace: + raise InfrastructureError( + "WB_INFRASTRUCTURE_ANCHOR_CONFLICT", "Workspace and project selectors do not identify the same workspace" + ) + if selected_workspace is None: + raise InfrastructureError( + "WB_INFRASTRUCTURE_WORKSPACE_NOT_FOUND", "No containing WorkBundle workspace metadata was found" + ) + metadata = load_workspace_metadata(selected_workspace, toolkit_root=toolkit_root) + registry = load_project_registry(config_root=config_root, toolkit_root=toolkit_root) + binding = join_workspace_binding(metadata, registry, expected_workspace_root=selected_workspace) + repositories = binding["repositories"] + matches: list[tuple[str, Path]] = [] + anchor = selected_project or (current if workspace_root is None else None) + if anchor is not None: + for repository_id, local in repositories.items(): + if local.get("checkout_kind") == "unmaterialized-member": + continue + candidate = local["project_root"] + if anchor == candidate or (selected_project is None and _is_within(anchor, candidate)): + matches.append((repository_id, candidate)) + matches.sort(key=lambda item: len(item[1].parts), reverse=True) + repository_id: str | None = matches[0][0] if matches else None + resolved_project: Path | None = matches[0][1] if matches else None + if selected_project is not None and resolved_project is None: + unmaterialized = [ + identifier for identifier, local in repositories.items() + if local.get("checkout_kind") == "unmaterialized-member" and local["project_root"] == selected_project + ] + if unmaterialized: + raise InfrastructureError( + "WB_INFRASTRUCTURE_REPOSITORY_UNMATERIALIZED", + f"Selected repository {unmaterialized[0]!r} is explicitly unmaterialized", + details={"repository_id": unmaterialized[0]}, + ) + raise InfrastructureError( + "WB_INFRASTRUCTURE_PROJECT_BINDING_MISSING", "Selected project does not match an exact device-bound member" + ) + if member_required and resolved_project is None: + candidates = [ + (identifier, local["project_root"]) + for identifier, local in repositories.items() + if local.get("checkout_kind") != "unmaterialized-member" + ] + if len(candidates) != 1: + if not candidates and any( + local.get("checkout_kind") == "unmaterialized-member" for local in repositories.values() + ): + raise InfrastructureError( + "WB_INFRASTRUCTURE_REPOSITORY_UNMATERIALIZED", + "A member is required but all device-bound repositories are explicitly unmaterialized", + ) + raise InfrastructureError( + "WB_INFRASTRUCTURE_MEMBER_AMBIGUOUS", + "A member is required but the workspace does not have exactly one eligible binding", + details={"candidate_count": len(candidates)}, + ) + repository_id, resolved_project = candidates[0] + if resolved_project is not None: + if not resolved_project.is_dir(): + raise InfrastructureError( + "WB_INFRASTRUCTURE_PROJECT_ROOT_MISSING", + f"Selected repository root does not exist: {resolved_project}", + ) + local = repositories[str(repository_id)] + if local.get("checkout_kind") == "manual": + workspace_id = str(metadata["workspace"]["id"]) + return AnchorContext( + config_root=resolve_config_root(config_root), + workspace_root=selected_workspace, + project_root=resolved_project, + workspace_id=workspace_id, + repository_id=repository_id, + ) + for field, command in ( + ("observed_branch", ("branch", "--show-current")), + ("observed_head", ("rev-parse", "HEAD")), + ): + expected = str(local.get(field) or "") + if not expected: + raise InfrastructureError( + "WB_INFRASTRUCTURE_OBSERVATION_MISSING", + f"Device observation {field} is missing for repository {repository_id!r}", + details={"repository_id": repository_id, "field": field}, + ) + completed = subprocess.run( + ["git", "-C", str(resolved_project), *command], + capture_output=True, text=True, check=False, + ) + if completed.returncode != 0 or completed.stdout.strip() != expected: + raise InfrastructureError( + "WB_INFRASTRUCTURE_OBSERVATION_STALE", + f"Device observation {field} is stale for repository {repository_id!r}", + details={"repository_id": repository_id, "field": field}, + ) + expected_common = Path(str(local["git_common_dir"])).expanduser() + if not expected_common.is_absolute(): + expected_common = resolved_project / expected_common + completed = subprocess.run( + ["git", "-C", str(resolved_project), "rev-parse", "--path-format=absolute", "--git-common-dir"], + capture_output=True, text=True, check=False, + ) + if completed.returncode != 0 or Path(completed.stdout.strip()).resolve() != expected_common.resolve(): + raise InfrastructureError( + "WB_INFRASTRUCTURE_OBSERVATION_STALE", + f"Device observation git_common_dir is stale for repository {repository_id!r}", + details={"repository_id": repository_id, "field": "git_common_dir"}, + ) + workspace_id = str(metadata["workspace"]["id"]) + return AnchorContext( + config_root=resolve_config_root(config_root), + workspace_root=selected_workspace, + project_root=resolved_project, + workspace_id=workspace_id, + repository_id=repository_id, + ) diff --git a/scripts/work-bundle/member.py b/scripts/work-bundle/member.py deleted file mode 100644 index a256a88..0000000 --- a/scripts/work-bundle/member.py +++ /dev/null @@ -1,698 +0,0 @@ -from __future__ import annotations - -import hashlib -import json -import re -from pathlib import Path - -from migration import _atomic_write, _member_preflight, _session_start_workspace_root -from worktree import ( - ProvisionMemberError, - _remove_created_member, - provision_member, - verify_git_control_scope, -) - - -class MemberLifecycleError(RuntimeError): - def __init__(self, code: str, result: dict[str, object] | None = None) -> None: - super().__init__(code) - self.code = code - self.result = result or {} - - -def _transaction_id( - workspace_root: Path, - origin: Path, - repository_id: str, - branch: str, - base_ref: str, -) -> str: - value = f'{workspace_root.resolve()}:{origin.resolve()}:{repository_id}:{branch}:{base_ref}' - return f"provision-{hashlib.sha256(value.encode('utf-8')).hexdigest()[:20]}" - - -def _record_path(workspace_root: Path, repository_id: str) -> Path: - return workspace_root / '.work-bundle' / 'transactions' / f'provision-{repository_id}.json' - - -def _read_record(path: Path) -> dict[str, object] | None: - if not path.is_file(): - return None - try: - value = json.loads(path.read_text(encoding='utf-8')) - except (OSError, json.JSONDecodeError) as exc: - raise MemberLifecycleError('WB_MEMBER_RECOVERY_INVALID') from exc - if not isinstance(value, dict): - raise MemberLifecycleError('WB_MEMBER_RECOVERY_INVALID') - return value - - -def _write_record(path: Path, payload: dict[str, object]) -> None: - _atomic_write(path, (json.dumps(payload, sort_keys=True) + '\n').encode('utf-8')) - - -def _context_matches(record: dict[str, object], context: dict[str, object]) -> bool: - return record.get('id') == context['transaction_id'] and record.get('context') == context - - -def _registry_project( - registry_path: Path, - workspace_root: Path, - workspace_slug: str | None, -) -> tuple[str, dict[str, object]]: - from project import _project_blocks - - matches: list[dict[str, object]] = [] - for project in _project_blocks(registry_path): - slug = str(project.get('slug') or '') - work_bundle_root = str(project.get('work_bundle_root') or '') - registered_root = Path(work_bundle_root).expanduser().resolve().parent if work_bundle_root else None - if workspace_slug and slug == workspace_slug: - matches.append(project) - elif not workspace_slug and registered_root == workspace_root.resolve(): - matches.append(project) - if len(matches) != 1: - raise MemberLifecycleError( - 'WB_MEMBER_WORKSPACE_REGISTRY_NOT_FOUND' if not matches else 'WB_MEMBER_WORKSPACE_REGISTRY_AMBIGUOUS' - ) - project = matches[0] - slug = str(project.get('slug') or '') - if not slug: - raise MemberLifecycleError('WB_MEMBER_WORKSPACE_SLUG_MISSING') - work_bundle_root = str(project.get('work_bundle_root') or '') - if work_bundle_root and Path(work_bundle_root).expanduser().resolve() != workspace_root / '.work-bundle': - raise MemberLifecycleError('WB_MEMBER_WORKSPACE_REGISTRY_MISMATCH') - return slug, project - - -def _list_bounds(lines: list[str], key: str, start: int, end: int, indent: str) -> tuple[int, int] | None: - prefix = f'{indent}{key}:' - for index in range(start, end): - if lines[index].startswith(prefix): - cursor = index + 1 - while cursor < end: - line = lines[cursor] - if line and not line.startswith(indent + ' '): - break - cursor += 1 - return index, cursor - return None - - -def _project_bounds(lines: list[str], slug: str) -> tuple[int, int]: - starts = [index for index, line in enumerate(lines) if line.startswith(' - slug:')] - for position, start in enumerate(starts): - value = lines[start].split(':', 1)[1].strip().strip('"\'') - if value == slug: - return start, starts[position + 1] if position + 1 < len(starts) else len(lines) - raise MemberLifecycleError('WB_MEMBER_WORKSPACE_REGISTRY_NOT_FOUND') - - -def _items(lines: list[str], bounds: tuple[int, int] | None) -> list[dict[str, str]]: - if bounds is None: - return [] - start, end = bounds - result: list[dict[str, str]] = [] - current: dict[str, str] | None = None - for line in lines[start + 1:end]: - if line.startswith(' - '): - if current is not None: - result.append(current) - current = {} - item = line.strip()[2:] - if ':' in item: - key, value = item.split(':', 1) - current[key.strip()] = value.strip().strip('"\'') - elif current is not None and line.startswith(' ') and ':' in line: - key, value = line.strip().split(':', 1) - current[key.strip()] = value.strip().strip('"\'') - if current is not None: - result.append(current) - return result - - -def _append_registry_item( - lines: list[str], - slug: str, - key: str, - item_lines: list[str], -) -> list[str]: - project_start, project_end = _project_bounds(lines, slug) - bounds = _list_bounds(lines, key, project_start, project_end, ' ') - if bounds is None: - insertion = next( - (index for index in range(project_start + 1, project_end) if lines[index].startswith(' status:')), - project_end, - ) - return lines[:insertion] + [f' {key}:', *item_lines] + lines[insertion:] - start, end = bounds - if lines[start].strip().endswith('[]'): - replacement = [f' {key}:', *item_lines] - return lines[:start] + replacement + lines[start + 1:] - return lines[:end] + item_lines + lines[end:] - - -def _registry_candidate( - current: str, - slug: str, - origin: Path, - repository_id: str, -) -> str: - from project import _git_remote, _yaml_string - - lines = current.splitlines() - project_start, project_end = _project_bounds(lines, slug) - origins_bounds = _list_bounds(lines, 'repository_origins', project_start, project_end, ' ') - sources_bounds = _list_bounds(lines, 'source_repositories', project_start, project_end, ' ') - origin_items = _items(lines, origins_bounds) - source_items = _items(lines, sources_bounds) - expected = str(origin.resolve()) - for item in [*origin_items, *source_items]: - if item.get('id') != repository_id: - continue - existing = item.get('origin_path') or item.get('path') or '' - if existing and str(Path(existing).expanduser().resolve()) != expected: - raise MemberLifecycleError('WB_MEMBER_REPOSITORY_ID_CONFLICT') - - if not any(item.get('id') == repository_id for item in origin_items): - lines = _append_registry_item(lines, slug, 'repository_origins', [ - f' - id: {_yaml_string(repository_id)}', - f' origin_path: {_yaml_string(origin.resolve())}', - f' remote: {_yaml_string(_git_remote(origin))}', - ' git_repository: true', - ]) - project_start, project_end = _project_bounds(lines, slug) - sources_bounds = _list_bounds(lines, 'source_repositories', project_start, project_end, ' ') - source_items = _items(lines, sources_bounds) - if not any(item.get('id') == repository_id for item in source_items): - lines = _append_registry_item(lines, slug, 'source_repositories', [ - f' - id: {_yaml_string(repository_id)}', - f' path: {_yaml_string(origin.resolve())}', - ' checkout_role: development', - ' work_dir: false', - f' remote: {_yaml_string(_git_remote(origin))}', - ' git_repository: true', - ]) - return '\n'.join(lines).rstrip() + '\n' - - -def _metadata_candidate( - current: str, - workspace_root: Path, - member: dict[str, object], - repository_id: str, - branch: str, - base_ref: str, - transaction_id: str, -) -> str: - from project import ( - _git_head, - _metadata_source_repositories, - _replace_top_level_block, - _yaml_block_bounds, - _yaml_string, - utc_now_rfc3339, - ) - - member_root = Path(str(member['project_root'])).resolve() - control_root = Path(str(member['git_control_root'])).resolve() - repositories = _metadata_source_repositories(current) - for repository in repositories: - if str(repository.get('id') or '') != repository_id: - continue - existing_root = Path(str(repository.get('project_root') or repository.get('path') or '')).expanduser().resolve() - if existing_root != member_root: - raise MemberLifecycleError('WB_MEMBER_REPOSITORY_ID_CONFLICT') - return current - - codegraph_present = (member_root / '.codegraph').is_dir() - entry = [ - f' - id: {_yaml_string(repository_id)}', - f' project_root: {_yaml_string(member_root)}', - f' origin_id: {_yaml_string(repository_id)}', - ' checkout_kind: managed-worktree', - f' git_control_root: {_yaml_string(control_root)}', - ' git_control_scope: workspace', - f' worktree_name: {_yaml_string(repository_id)}', - ' git_repository: true', - f' expected_branch: {_yaml_string(branch)}', - f' base_ref: {_yaml_string(base_ref)}', - f' observed_head: {_yaml_string(_git_head(member_root))}', - f' observation_time: {_yaml_string(utc_now_rfc3339())}', - ' baseline_status: current', - ' lifecycle_status: active', - ' operation_policy: inherit', - ' codegraph:', - f' supported: {str(codegraph_present).lower()}', - f' index_present: {str(codegraph_present).lower()}', - f' root: {_yaml_string(member_root)}', - f" status: {'current' if codegraph_present else 'not-indexed'}", - ' synced_commit_id: ""', - ' last_synced_at: ""', - f" reason: {'\"\"' if codegraph_present else 'no-index'}", - ] - lines = current.splitlines() - bounds = _yaml_block_bounds(lines, 'source_repositories') - if not bounds: - raise MemberLifecycleError('WB_MEMBER_METADATA_SOURCES_MISSING') - _, end = bounds - lines = lines[:end] + entry + lines[end:] - transaction_block = '\n'.join([ - 'lifecycle_transaction:', - f' id: {_yaml_string(transaction_id)}', - ' state: published', - ' registry_status: published', - ' metadata_status: published', - ]) - lines, _ = _replace_top_level_block(lines, transaction_block, 'lifecycle_transaction') - return '\n'.join(lines).rstrip() + '\n' - - -def _member_result(member: dict[str, object], transaction_id: str) -> dict[str, object]: - return { - 'repository_id': member['repository_id'], - 'project_root': member['project_root'], - 'git_control_root': member['git_control_root'], - 'branch': member['branch'], - 'base_ref': member['base_ref'], - 'verification': member['verification'], - 'transaction_id': transaction_id, - } - - -def _matching_checkout( - workspace_root: Path, - origin: Path, - repository_id: str, - branch: str, - base_ref: str, - *, - require_base: bool, -) -> dict[str, object] | None: - from project import _git_branch, _git_value - - target = workspace_root / repository_id - control = workspace_root / '.work-bundle' / 'git' / f'{repository_id}.git' - if not target.is_dir() or not control.is_dir(): - return None - try: - verification = verify_git_control_scope(workspace_root, target) - except (OSError, RuntimeError): - return None - if not verification['valid'] or _git_branch(target) != branch: - return None - remote = _git_value(target, 'remote', 'get-url', 'origin') - if remote: - remote_path = Path(remote).expanduser() - if remote_path.exists() and remote_path.resolve() != origin.resolve(): - return None - if require_base: - expected_head = _git_value(origin, 'rev-parse', base_ref) - actual_head = _git_value(target, 'rev-parse', 'HEAD') - if not expected_head or expected_head != actual_head: - return None - return { - 'repository_id': repository_id, - 'project_root': str(target), - 'git_control_root': str(control), - 'branch': branch, - 'base_ref': base_ref, - 'verification': verification, - 'git_actions': [], - } - - -def provision_member_lifecycle( - workspace_root: Path, - origin: Path, - repository_id: str, - branch: str, - base_ref: str = 'HEAD', - *, - workspace_slug: str | None = None, - dry_run: bool = False, - fail_stage: str | None = None, -) -> dict[str, object]: - from project import ( - _metadata_failures, - _metadata_source_repositories, - _workspace_metadata_failures, - _yaml_scalar, - project_registry_path, - ) - - workspace_root = workspace_root.expanduser().resolve() - origin = origin.expanduser().resolve() - if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._-]*', repository_id): - raise MemberLifecycleError('WB_REPOSITORY_ID_INVALID') - metadata_path = workspace_root / '.work-bundle/project.yaml' - registry_path = project_registry_path() - if not metadata_path.is_file(): - raise MemberLifecycleError('WB_MEMBER_METADATA_MISSING') - metadata_before = metadata_path.read_bytes() - metadata_text = metadata_before.decode('utf-8') - if _yaml_scalar(metadata_text, 'metadata_version') != '3': - raise MemberLifecycleError('WB_MEMBER_METADATA_V3_REQUIRED') - if _yaml_scalar(metadata_text, 'workspace_mode') != 'multi-repository': - raise MemberLifecycleError('WB_MEMBER_MULTI_REPOSITORY_REQUIRED') - declared_root = _yaml_scalar(metadata_text, 'workspace_root') - if not declared_root or Path(declared_root).expanduser().resolve() != workspace_root: - raise MemberLifecycleError('WB_MEMBER_WORKSPACE_ROOT_MISMATCH') - if not origin.is_dir() or not (origin / '.git').exists(): - raise MemberLifecycleError('WB_MEMBER_ORIGIN_INVALID') - slug, registry_entry = _registry_project(registry_path, workspace_root, workspace_slug) - registry_before = registry_path.read_bytes() - transaction_id = _transaction_id(workspace_root, origin, repository_id, branch, base_ref) - context = { - 'transaction_id': transaction_id, - 'workspace_root': str(workspace_root), - 'workspace_slug': slug, - 'origin': str(origin), - 'repository_id': repository_id, - 'branch': branch, - 'base_ref': base_ref, - } - record_path = _record_path(workspace_root, repository_id) - record = _read_record(record_path) - target = workspace_root / repository_id - control = workspace_root / '.work-bundle' / 'git' / f'{repository_id}.git' - proposal = { - 'workspace_root': str(workspace_root), - 'workspace_slug': slug, - 'origin': str(origin), - 'repository_id': repository_id, - 'working_branch': branch, - 'base_ref': base_ref, - 'metadata_before_sha256': hashlib.sha256(metadata_before).hexdigest(), - 'registry_before_sha256': hashlib.sha256(registry_before).hexdigest(), - } - metadata_repositories = _metadata_source_repositories(metadata_text) - for repository in metadata_repositories: - if str(repository.get('id') or '') != repository_id: - continue - existing_root = Path( - str(repository.get('project_root') or repository.get('path') or '') - ).expanduser().resolve() - if existing_root != target: - raise MemberLifecycleError('WB_MEMBER_REPOSITORY_ID_CONFLICT', {'proposal': proposal}) - _registry_candidate(registry_before.decode('utf-8'), slug, origin, repository_id) - matching_orphan = None - if not record and target.exists() and any(target.iterdir()): - matching_orphan = _matching_checkout( - workspace_root, origin, repository_id, branch, base_ref, require_base=True - ) - metadata_ids = {str(item.get('id') or '') for item in metadata_repositories} - registry_ids = { - str(item.get('id') or '') - for item in registry_entry.get('source_repositories', []) - if isinstance(item, dict) - } - if ( - not record - and matching_orphan - and repository_id in metadata_ids - and repository_id in registry_ids - ): - public_member = _member_result(matching_orphan, transaction_id) - return { - 'status': 'passed', - 'mode': 'multi-repository', - 'dry_run': dry_run, - 'idempotent': True, - 'result': { - **public_member, - 'metadata_status': 'published', - 'registry_status': 'published', - }, - 'changed_files': [], - 'git_actions': [], - 'transaction': { - 'id': transaction_id, - 'state': 'published', - 'owned_paths': [], - 'registry_status': 'published', - 'metadata_status': 'published', - 'resume_source': 'converged-authorities', - }, - 'validation_results': {'metadata_and_registry_converged': True}, - 'failures': [], - } - if dry_run: - if target.exists() and any(target.iterdir()) and not ( - (record and _context_matches(record, context)) or matching_orphan - ): - raise MemberLifecycleError('WB_WORKTREE_TARGET_COLLISION', {'proposal': proposal}) - return { - 'status': 'proposed', - 'mode': 'multi-repository', - 'dry_run': True, - 'proposal': proposal, - 'changed_files': [], - 'git_actions': [], - 'transaction': { - 'id': transaction_id, - 'state': 'verified' if matching_orphan else 'proposed', - 'resume_source': 'verified-orphan' if matching_orphan else 'new-checkout', - 'owned_paths': [], - 'registry_status': 'pending', - 'metadata_status': 'pending', - }, - 'failures': [], - } - - if record and record.get('state') == 'published' and _context_matches(record, context): - member = _matching_checkout( - workspace_root, origin, repository_id, branch, base_ref, require_base=False - ) - published_result = record.get('published_result') - if member is None or not isinstance(published_result, dict): - raise MemberLifecycleError('WB_MEMBER_PUBLISHED_RECOVERY_INVALID') - metadata_ids = {str(item.get('id') or '') for item in _metadata_source_repositories(metadata_text)} - registry_ids = { - str(item.get('id') or '') - for item in registry_entry.get('source_repositories', []) - if isinstance(item, dict) - } - if repository_id not in metadata_ids or repository_id not in registry_ids: - raise MemberLifecycleError('WB_MEMBER_PUBLISHED_STATE_DIVERGED') - replay = json.loads(json.dumps(published_result)) - replay['idempotent'] = True - replay['changed_files'] = [] - return replay - - member = None - checkout_owned = False - if record and record.get('state') == 'verified' and _context_matches(record, context): - member = _matching_checkout( - workspace_root, origin, repository_id, branch, base_ref, require_base=True - ) - if member is None: - raise MemberLifecycleError('WB_MEMBER_VERIFIED_STATE_DIVERGED') - checkout_owned = bool(record.get('checkout_owned', True)) - elif target.exists() and any(target.iterdir()): - member = matching_orphan or _matching_checkout( - workspace_root, origin, repository_id, branch, base_ref, require_base=True - ) - if member is None: - raise MemberLifecycleError('WB_WORKTREE_TARGET_COLLISION', {'proposal': proposal}) - # A checkout without a recovery record is safe to resume only after exact - # origin/branch/base/control verification. Its paths are not claimed for - # rollback because prior transaction ownership cannot be proven. - checkout_owned = False - - try: - if member is None: - try: - member = provision_member(workspace_root, origin, repository_id, branch, base_ref) - except ProvisionMemberError as exc: - raise MemberLifecycleError(exc.code, exc.result) from exc - checkout_owned = True - verified_record = { - 'id': transaction_id, - 'state': 'verified', - 'context': context, - 'checkout_owned': checkout_owned, - 'registry_status': 'unchanged', - 'metadata_status': 'unchanged', - 'member': _member_result(member, transaction_id), - } - _write_record(record_path, verified_record) - if fail_stage == 'verified': - raise MemberLifecycleError('WB_MEMBER_INJECTED_VERIFIED_FAILURE') - - metadata_candidate = _metadata_candidate( - metadata_text, workspace_root, member, repository_id, branch, base_ref, transaction_id - ) - registry_candidate = _registry_candidate( - registry_before.decode('utf-8'), slug, origin, repository_id - ) - member_preflight = _member_preflight(workspace_root, member, branch) - discovery_root = _session_start_workspace_root(Path(str(member['project_root']))) - if not member_preflight['passed'] or discovery_root != workspace_root: - raise MemberLifecycleError('WB_MEMBER_FINAL_VERIFICATION_FAILED') - candidate_entry = dict(registry_entry) - candidate_sources = list(candidate_entry.get('source_repositories', [])) - if not any( - isinstance(item, dict) and str(item.get('id') or '') == repository_id - for item in candidate_sources - ): - candidate_sources.append({ - 'id': repository_id, - 'path': str(origin), - 'checkout_role': 'development', - 'work_dir': False, - 'remote': '', - 'git_repository': True, - }) - candidate_entry['source_repositories'] = candidate_sources - candidate_failures = [ - *_metadata_failures(workspace_root, metadata_candidate, candidate_entry), - *_workspace_metadata_failures(workspace_root, metadata_candidate), - ] - if candidate_failures: - raise MemberLifecycleError('WB_MEMBER_CANDIDATE_INVALID', {'failures': candidate_failures}) - - if metadata_path.read_bytes() != metadata_before or registry_path.read_bytes() != registry_before: - raise MemberLifecycleError('WB_MEMBER_PUBLICATION_BASELINE_CHANGED') - - if fail_stage == 'metadata-publication': - raise MemberLifecycleError('WB_MEMBER_INJECTED_METADATA_PUBLICATION_FAILURE') - _atomic_write(metadata_path, metadata_candidate.encode('utf-8')) - if fail_stage == 'registry-publication': - raise MemberLifecycleError('WB_MEMBER_INJECTED_REGISTRY_PUBLICATION_FAILURE') - _atomic_write(registry_path, registry_candidate.encode('utf-8')) - if metadata_path.read_text(encoding='utf-8') != metadata_candidate or registry_path.read_text(encoding='utf-8') != registry_candidate: - raise MemberLifecycleError('WB_MEMBER_PUBLICATION_VERIFY_FAILED') - - public_member = _member_result(member, transaction_id) - result = { - 'status': 'passed', - 'mode': 'multi-repository', - 'dry_run': False, - 'result': { - **public_member, - 'metadata_status': 'published', - 'registry_status': 'published', - }, - 'changed_files': [str(control), str(target), str(metadata_path), str(registry_path), str(record_path)], - 'git_actions': [], - 'transaction': { - 'id': transaction_id, - 'state': 'published', - 'owned_paths': [str(control), str(target), str(metadata_path), str(registry_path), str(record_path)], - 'registry_status': 'published', - 'metadata_status': 'published', - 'publication': { - 'metadata_before': hashlib.sha256(metadata_before).hexdigest(), - 'metadata_after': hashlib.sha256(metadata_candidate.encode('utf-8')).hexdigest(), - 'registry_before': hashlib.sha256(registry_before).hexdigest(), - 'registry_after': hashlib.sha256(registry_candidate.encode('utf-8')).hexdigest(), - }, - }, - 'validation_results': { - 'member_preflight': member_preflight, - 'session_start_discovery': {'passed': True, 'workspace_root': str(discovery_root)}, - 'metadata_and_registry_converged': True, - }, - 'failures': [], - } - _write_record(record_path, { - 'id': transaction_id, - 'state': 'published', - 'context': context, - 'checkout_owned': checkout_owned, - 'registry_status': 'published', - 'metadata_status': 'published', - 'published_result': result, - }) - return result - except Exception as exc: - code = exc.code if isinstance(exc, MemberLifecycleError) else 'WB_MEMBER_PUBLICATION_FAILED' - if metadata_path.read_bytes() != metadata_before: - _atomic_write(metadata_path, metadata_before) - if registry_path.read_bytes() != registry_before: - _atomic_write(registry_path, registry_before) - rollback: dict[str, object] = {'state': 'not-required'} - if checkout_owned and (target.exists() or control.exists()): - rollback = _remove_created_member( - control, target, branch, - keep_control=False, keep_target=False, delete_branch=True, - ) - failure_record = { - 'id': transaction_id, - 'state': 'failed', - 'context': context, - 'failure_code': code, - 'checkout_owned': checkout_owned, - 'registry_status': 'unchanged', - 'metadata_status': 'unchanged', - 'rollback': rollback, - } - _write_record(record_path, failure_record) - detail = exc.result if isinstance(exc, MemberLifecycleError) else {} - raise MemberLifecycleError(code, {**detail, 'transaction': failure_record, 'recovery_record': str(record_path)}) from exc - - -def cleanup_member_lifecycle( - workspace_root: Path, - repository_id: str, - *, - dry_run: bool = False, -) -> dict[str, object]: - """Remove only a recorded, unpublished, transaction-owned checkout.""" - from project import _metadata_source_repositories - - workspace_root = workspace_root.expanduser().resolve() - if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._-]*', repository_id): - raise MemberLifecycleError('WB_REPOSITORY_ID_INVALID') - metadata_path = workspace_root / '.work-bundle/project.yaml' - if not metadata_path.is_file(): - raise MemberLifecycleError('WB_MEMBER_METADATA_MISSING') - published_ids = { - str(item.get('id') or '') - for item in _metadata_source_repositories(metadata_path.read_text(encoding='utf-8')) - } - if repository_id in published_ids: - raise MemberLifecycleError('WB_MEMBER_CLEANUP_PUBLISHED') - record_path = _record_path(workspace_root, repository_id) - record = _read_record(record_path) - if not record: - raise MemberLifecycleError('WB_MEMBER_CLEANUP_RECOVERY_MISSING') - context = record.get('context') - if ( - not isinstance(context, dict) - or context.get('workspace_root') != str(workspace_root) - or context.get('repository_id') != repository_id - or record.get('state') not in {'verified', 'failed'} - or record.get('checkout_owned') is not True - ): - raise MemberLifecycleError('WB_MEMBER_CLEANUP_NOT_OWNED') - target = workspace_root / repository_id - control = workspace_root / '.work-bundle' / 'git' / f'{repository_id}.git' - branch = str(context.get('branch') or '') - result = { - 'status': 'proposed' if dry_run else 'passed', - 'dry_run': dry_run, - 'repository_id': repository_id, - 'changed_files': [] if dry_run else [str(path) for path in (target, control) if path.exists()], - 'git_actions': [], - 'transaction': { - 'id': record.get('id'), - 'state': 'cleanup-proposed' if dry_run else 'cleaned', - 'owned_paths': [str(target), str(control)], - 'registry_status': 'unchanged', - 'metadata_status': 'unchanged', - }, - 'failures': [], - } - if dry_run: - return result - _remove_created_member( - control, target, branch, - keep_control=False, keep_target=False, delete_branch=True, - ) - record['state'] = 'cleaned' - record['cleanup'] = {'state': 'completed', 'metadata_status': 'unchanged', 'registry_status': 'unchanged'} - _write_record(record_path, record) - return result diff --git a/scripts/work-bundle/migration.py b/scripts/work-bundle/migration.py deleted file mode 100644 index 8b58cd8..0000000 --- a/scripts/work-bundle/migration.py +++ /dev/null @@ -1,997 +0,0 @@ -from __future__ import annotations - -import hashlib -import importlib.util -import json -import os -import shutil -import stat -import subprocess -import tempfile -from dataclasses import dataclass, field -from datetime import datetime, timezone -from pathlib import Path -from typing import Iterable - -from workspace_resources import ensure_workspace_resources, validate_script_index -from worktree import provision_member - - -TRANSIENT_NAMES = frozenset({'.cache', '.tmp', '__pycache__', '.pytest_cache'}) -RECOVERY_DIRECTORY = '.work-bundle-migration-transactions' -TRANSACTION_STAGES = ( - 'copy-authority', - 'workspace-resources', - 'member-provision', - 'final-verification', - 'metadata-publication', - 'registry-publication', -) - - -class MigrationError(Exception): - def __init__( - self, - code: str, - transaction_record: Path | None = None, - *, - result: dict[str, object] | None = None, - ) -> None: - self.code = code - self.transaction_record = transaction_record - self.result = result or {} - super().__init__(code) - - -def _utc_now() -> str: - return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace('+00:00', 'Z') - - -def _digest(path: Path) -> str: - value = hashlib.sha256() - with path.open('rb') as stream: - for chunk in iter(lambda: stream.read(65536), b''): - value.update(chunk) - return value.hexdigest() - - -def _run_git(root: Path, *args: str) -> subprocess.CompletedProcess[str]: - return subprocess.run( - ['git', '-C', str(root), *args], - check=False, - capture_output=True, - text=True, - ) - - -def validate_migration_proposal(origin: Path, branch: str, base_ref: str) -> dict[str, object]: - """Validate origin-local worktree and ref availability without writing Git state.""" - origin = origin.resolve() - common_dir = _run_git(origin, 'rev-parse', '--path-format=absolute', '--git-common-dir') - if common_dir.returncode: - raise MigrationError('WB_MIGRATION_ORIGIN_GIT_UNAVAILABLE') - evidence: dict[str, object] = { - 'working_branch': branch, - 'base_ref': base_ref, - 'origin_git_common_dir': str(Path(common_dir.stdout.strip()).resolve()), - 'changed_files': [], - 'git_actions': [], - } - worktrees = _run_git(origin, 'worktree', 'list', '--porcelain') - if worktrees.returncode: - raise MigrationError('WB_MIGRATION_ORIGIN_GIT_UNAVAILABLE', result=evidence) - if f'branch refs/heads/{branch}' in worktrees.stdout.splitlines(): - raise MigrationError('WB_WORKTREE_BRANCH_CONFLICT', result=evidence) - - resolved = _run_git(origin, 'rev-parse', '--verify', '--quiet', f'{base_ref}^{{commit}}') - if resolved.returncode: - local_branch_available = False - if base_ref.startswith('origin/') and len(base_ref) > len('origin/'): - local_name = base_ref.removeprefix('origin/') - local = _run_git(origin, 'show-ref', '--verify', '--quiet', f'refs/heads/{local_name}') - local_branch_available = local.returncode == 0 - evidence['local_branch_available'] = local_branch_available - code = ( - 'WB_MIGRATION_LOCAL_ORIGIN_BASE_REF_UNAVAILABLE' - if local_branch_available - else 'WB_MIGRATION_BASE_REF_UNAVAILABLE' - ) - raise MigrationError(code, result=evidence) - evidence['resolved_base_commit'] = resolved.stdout.strip() - return evidence - - -def source_git_state(root: Path) -> dict[str, object]: - """Return bounded Git facts without modifying the repository.""" - resolved = root.resolve() - inside = _run_git(resolved, 'rev-parse', '--is-inside-work-tree') - if inside.returncode or inside.stdout.strip() != 'true': - return { - 'root': str(resolved), - 'git_repository': False, - 'branch': '', - 'head': '', - 'dirty': False, - 'status_entries': [], - } - branch = _run_git(resolved, 'branch', '--show-current') - head = _run_git(resolved, 'rev-parse', 'HEAD') - status_result = _run_git(resolved, 'status', '--porcelain=v1', '--untracked-files=all') - if status_result.returncode: - raise MigrationError('WB_MIGRATION_GIT_STATE_UNRESOLVED') - entries = [line for line in status_result.stdout.splitlines() if line] - return { - 'root': str(resolved), - 'git_repository': True, - 'branch': branch.stdout.strip() if branch.returncode == 0 else '', - 'head': head.stdout.strip() if head.returncode == 0 else '', - 'dirty': bool(entries), - 'status_entries': entries, - } - - -def work_bundle_git_state(source: Path) -> dict[str, object]: - root = source.resolve() / '.work-bundle' - if not root.is_dir(): - return { - 'root': str(root), - 'git_repository': False, - 'branch': '', - 'head': '', - 'dirty': False, - 'status_entries': [], - } - return source_git_state(root) - - -def _entry(path: Path, root: Path) -> dict[str, object]: - relative = str(path.relative_to(root)) - mode = stat.S_IMODE(path.lstat().st_mode) - if path.is_symlink(): - raise MigrationError('WB_MIGRATION_UNSAFE_SYMLINK') - if path.is_dir(): - return {'path': relative, 'type': 'directory', 'mode': mode, 'mtime_ns': path.stat().st_mtime_ns} - if path.is_file(): - return { - 'path': relative, - 'type': 'file', - 'mode': mode, - 'mtime_ns': path.stat().st_mtime_ns, - 'digest': _digest(path), - } - raise MigrationError('WB_MIGRATION_UNSUPPORTED_FILE_TYPE') - - -def _is_transient(path: Path, root: Path) -> bool: - return any(part in TRANSIENT_NAMES for part in path.relative_to(root).parts) - - -def _inventory(root: Path, *, exclude_transient: bool = True) -> list[dict[str, object]]: - if not root.exists(): - return [] - result: list[dict[str, object]] = [] - for path in sorted(root.rglob('*')): - if exclude_transient and _is_transient(path, root): - continue - result.append(_entry(path, root)) - return result - - -def _inventory_digest(inventory: Iterable[dict[str, object]]) -> str: - encoded = json.dumps(list(inventory), sort_keys=True, separators=(',', ':')).encode('utf-8') - return hashlib.sha256(encoded).hexdigest() - - -def _preservation_inventory(root: Path) -> list[dict[str, object]]: - """Ignore incidental Git-index timestamps while preserving all bytes and modes.""" - inventory = _inventory(root) - for item in inventory: - if '.git' in Path(str(item['path'])).parts: - item.pop('mtime_ns', None) - return inventory - - -def _baseline_evidence( - source_state: dict[str, object], - nested_state: dict[str, object], - member_origin_state: dict[str, object] | None = None, -) -> dict[str, object]: - origin_state = member_origin_state or source_state - facts = { - 'source_repository_dirty': bool(source_state['dirty']), - 'work_bundle_git_dirty': bool(nested_state['dirty']), - 'member_origin_dirty': bool(origin_state['dirty']), - 'source_head': source_state['head'], - 'work_bundle_head': nested_state['head'], - 'member_origin_head': origin_state['head'], - 'source_status_entries': source_state['status_entries'], - 'work_bundle_status_entries': nested_state['status_entries'], - 'member_origin_status_entries': origin_state['status_entries'], - } - token = hashlib.sha256(json.dumps(facts, sort_keys=True).encode('utf-8')).hexdigest() - return {**facts, 'id': token} - - -def inspect_migration(source: Path, target: Path, origin: Path | None = None) -> dict[str, object]: - source, target = source.resolve(), target.resolve() - origin = (origin or source).resolve() - source_state = source_git_state(source) - nested_state = work_bundle_git_state(source) - origin_state = source_git_state(origin) - return { - 'source_root': str(source), - 'target_root': str(target), - 'member_origin_root': str(origin), - 'source_exists': source.is_dir(), - 'target_exists': target.exists(), - 'work_bundle_exists': (source / '.work-bundle').is_dir(), - 'credential_store_present': (source / 'credentials/credentials.yaml').is_file(), - 'source_repository_git': source_state, - 'work_bundle_git': nested_state, - 'member_origin_git': origin_state, - 'accepted_baseline_evidence': _baseline_evidence(source_state, nested_state, origin_state), - } - - -def propose_migration( - source: Path, - target: Path, - repository_id: str, - branch: str, - base_ref: str, - *, - origin: Path | None = None, - workspace_slug: str | None = None, - repository_name: str | None = None, - additional_repository_origins: list[dict[str, object]] | None = None, -) -> dict[str, object]: - origin = (origin or source).resolve() - inspection = inspect_migration(source, target, origin) - proposal_validation = validate_migration_proposal(origin, branch, base_ref) - slug = workspace_slug or target.name - name = repository_name or repository_id - return { - **inspection, - 'workspace_slug': slug, - 'repository_id': repository_id, - 'repository_name': name, - 'working_branch': branch, - 'base_ref': base_ref, - 'additional_repository_origins': list(additional_repository_origins or []), - 'dry_run': True, - 'changed_files': [], - 'git_actions': [], - 'proposal_validation': proposal_validation, - 'credential_action': 'create-empty-protected-store', - 'apply_requires_accepted_baseline': bool( - inspection['source_repository_git']['dirty'] - or inspection['work_bundle_git']['dirty'] - or inspection['member_origin_git']['dirty'] - ), - } - - -def verify_copy(source: Path, target: Path) -> bool: - return _inventory(source / '.work-bundle') == _inventory(target / '.work-bundle') - - -@dataclass -class MigrationTransaction: - target_root: Path - transaction_id: str - owned_paths: list[Path] = field(default_factory=list) - state: str = 'proposed' - failure_code: str | None = None - context: dict[str, object] = field(default_factory=dict) - target_root_created: bool = False - - @property - def recovery_path(self) -> Path: - return self.target_root.parent / RECOVERY_DIRECTORY / f'{self.transaction_id}.json' - - def own(self, path: Path) -> None: - resolved = path.resolve(strict=False) - root = self.target_root.resolve(strict=False) - if resolved != root and root not in resolved.parents: - raise MigrationError('WB_MIGRATION_PATH_ESCAPE') - if resolved not in self.owned_paths: - self.owned_paths.append(resolved) - - def evidence(self, **extra: object) -> dict[str, object]: - result: dict[str, object] = { - 'id': self.transaction_id, - 'state': self.state, - 'failure_code': self.failure_code, - 'owned_paths': [str(path) for path in sorted(self.owned_paths)], - 'metadata_status': extra.pop('metadata_status', 'pending'), - 'registry_status': extra.pop('registry_status', 'pending'), - 'updated_at': _utc_now(), - } - result.update(extra) - if self.context: - result['context'] = self.context - return result - - def persist(self, **extra: object) -> Path: - path = self.recovery_path - path.parent.mkdir(parents=True, exist_ok=True) - payload = self.evidence(**extra) - temporary = path.with_suffix('.tmp') - temporary.write_text(json.dumps(payload, sort_keys=True) + '\n', encoding='utf-8') - os.replace(temporary, path) - return path - - -def rollback_owned_paths(transaction: MigrationTransaction) -> dict[str, object]: - for path in sorted(transaction.owned_paths, key=lambda value: len(value.parts), reverse=True): - if path.is_symlink() or path.is_file(): - path.unlink(missing_ok=True) - elif path.is_dir(): - shutil.rmtree(path) - root = transaction.target_root.resolve(strict=False) - if transaction.target_root_created and root.is_dir() and not any(root.iterdir()): - root.rmdir() - transaction.state = 'rolled-back' - transaction.persist(metadata_status='rolled-back', registry_status='unchanged') - return transaction.evidence(metadata_status='rolled-back', registry_status='unchanged') - - -def _copy_tree(source: Path, target: Path) -> None: - if not source.is_dir(): - return - _inventory(source) - shutil.copytree( - source, - target, - copy_function=shutil.copy2, - symlinks=False, - ignore=lambda _root, names: [name for name in names if name in TRANSIENT_NAMES], - dirs_exist_ok=False, - ) - - -def _yaml_string(value: object) -> str: - return json.dumps(str(value), ensure_ascii=True) - - -def _git_remote(source: Path) -> str: - result = _run_git(source, 'remote', 'get-url', 'origin') - return result.stdout.strip() if result.returncode == 0 else '' - - -def _top_level_unknown_blocks(text: str, known: set[str]) -> list[str]: - lines = text.splitlines() - result: list[str] = [] - index = 0 - while index < len(lines): - line = lines[index] - if not line or line.startswith((' ', '#')) or ':' not in line: - index += 1 - continue - key = line.split(':', 1)[0] - end = index + 1 - while end < len(lines) and (not lines[end] or lines[end].startswith((' ', '\t'))): - end += 1 - if key not in known: - result.append('\n'.join(lines[index:end]).rstrip()) - index = end - return result - - -def _metadata_text( - current: str, - target: Path, - member: dict[str, object], - repository_id: str, - branch: str, - base_ref: str, - transaction_id: str, -) -> str: - member_root = Path(str(member['project_root'])).resolve() - control_root = Path(str(member['git_control_root'])).resolve() - head = _run_git(member_root, 'rev-parse', 'HEAD') - observed_head = head.stdout.strip() if head.returncode == 0 else '' - codegraph_present = (member_root / '.codegraph').is_dir() - known = { - 'metadata_version', 'authority', 'workspace_root', 'workspace_mode', 'project_root', - 'industry', 'prefer_subagent', 'metadata_compatibility', 'workspace_resources', - 'language', 'operation_policy', 'source_repository_roles', - 'source_repositories', 'lifecycle_transaction', 'migration', - } - lines = [ - 'metadata_version: 3', - 'authority: workspace-working-state', - f'workspace_root: {_yaml_string(target.resolve())}', - 'workspace_mode: multi-repository', - f'project_root: {_yaml_string(member_root)}', - f'industry: {_yaml_string(repository_id)}', - 'metadata_compatibility:', - ' readable_versions: [2, 3]', - ' migration_requires_explicit_apply: true', - ' preserves_unknown_fields: true', - 'workspace_resources:', - ' script_index:', - ' path: script/index.yaml', - ' status: current', - ' credential_store:', - ' path: credentials/credentials.yaml', - ' status: protected', - 'operation_policy:', - ' project_files:', - ' allow: [read, create, update]', - ' forbid: [delete_unknown_files, overwrite_non_empty_without_force]', - ' git:', - ' allow_operations: [status, diff, log, branch --show-current, rev-parse HEAD]', - ' permissive_operations: [stage, commit, pull]', - ' forbid_operations: [reset --hard, clean -fd, push --force]', - 'source_repository_roles:', - ' registry: "Locator only: workspace slug/root and stable repository origin identity and locators."', - ' project_metadata: "Working-state authority: member path, branch/HEAD observation, lifecycle transaction, operation policy, and CodeGraph state."', - 'source_repositories:', - f' - id: {_yaml_string(repository_id)}', - f' project_root: {_yaml_string(member_root)}', - f' origin_id: {_yaml_string(repository_id)}', - ' checkout_kind: managed-worktree', - f' git_control_root: {_yaml_string(control_root)}', - ' git_control_scope: workspace', - f' worktree_name: {_yaml_string(repository_id)}', - ' git_repository: true', - f' expected_branch: {_yaml_string(branch)}', - f' base_ref: {_yaml_string(base_ref)}', - f' observed_head: {_yaml_string(observed_head)}', - f' observation_time: {_yaml_string(_utc_now())}', - ' baseline_status: current', - ' lifecycle_status: active', - ' operation_policy: inherit', - ' codegraph:', - f' supported: {str(codegraph_present).lower()}', - f' index_present: {str(codegraph_present).lower()}', - f' root: {_yaml_string(member_root)}', - f" status: {'current' if codegraph_present else 'not-indexed'}", - ' synced_commit_id: ""', - ' last_synced_at: ""', - f" reason: {'\"\"' if codegraph_present else 'no-index'}", - 'lifecycle_transaction:', - f' id: {_yaml_string(transaction_id)}', - ' state: published', - ' registry_status: published', - ' metadata_status: published', - 'migration:', - ' authority_owner: /wb-initialize-project', - ' compatibility_window: metadata-v2-readable-until-explicit-v3-apply', - ' doctor_flow: "Use /wb-initialize-project doctor for deterministic file-only repair."', - ' migrate_flow: "Inspect/dry-run single-to-multi migration, then explicitly apply."', - ] - unknown = _top_level_unknown_blocks(current, known) - if unknown: - lines.extend([''] + unknown) - return '\n'.join(lines).rstrip() + '\n' - - -def _registry_entry_lines( - workspace_root: Path, - workspace_slug: str, - repository_id: str, - repository_name: str, - source: Path, - additional_origins: list[dict[str, object]], -) -> list[str]: - origins = [{ - 'id': repository_id, - 'origin_path': str(source.resolve()), - 'remote': _git_remote(source), - 'git_repository': True, - }, *additional_origins] - lines = [ - f' - slug: {workspace_slug}', - f' name: {_yaml_string(repository_name)}', - f' workspace_root: {_yaml_string(workspace_root.resolve())}', - f' work_bundle_root: {_yaml_string(workspace_root.resolve() / ".work-bundle")}', - f' knowledge_root: {_yaml_string(workspace_root.resolve() / ".work-bundle/knowledge")}', - ' aliases: []', - ' repository_origins:', - ] - for origin in origins: - lines.extend([ - f" - id: {_yaml_string(origin.get('id', ''))}", - f" origin_path: {_yaml_string(origin.get('origin_path', ''))}", - f" remote: {_yaml_string(origin.get('remote', ''))}", - f" git_repository: {str(bool(origin.get('git_repository', True))).lower()}", - ]) - lines.extend([ - ' source_repositories:', - f' - id: {_yaml_string(repository_id)}', - f' path: {_yaml_string(source.resolve())}', - ' checkout_role: truth', - ' work_dir: false', - f' remote: {_yaml_string(_git_remote(source))}', - ' git_repository: true', - ' compatibility:', - ' readable_project_metadata_versions: [2, 3]', - ' source_repositories_role: locator-only', - ' status: active', - f' updated_at: {_utc_now()[:10]}', - ]) - return lines - - -def _registry_text( - current: str, - workspace_root: Path, - workspace_slug: str, - repository_id: str, - repository_name: str, - source: Path, - additional_origins: list[dict[str, object]], -) -> str: - lines = current.splitlines() if current.strip() else ['projects:'] - if not any(line.strip() == 'projects:' for line in lines): - lines.extend(['', 'projects:']) - replacement = _registry_entry_lines( - workspace_root, workspace_slug, repository_id, repository_name, source, additional_origins - ) - starts = [index for index, line in enumerate(lines) if line.startswith(' - slug:')] - for position, start in enumerate(starts): - value = lines[start].split(':', 1)[1].strip().strip('"\'') - if value != workspace_slug: - continue - end = starts[position + 1] if position + 1 < len(starts) else len(lines) - return '\n'.join(lines[:start] + replacement + lines[end:]).rstrip() + '\n' - if lines and lines[-1] != '': - lines.append('') - return '\n'.join(lines + replacement).rstrip() + '\n' - - -def _atomic_write(path: Path, payload: bytes) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - descriptor, temporary = tempfile.mkstemp(prefix=f'.{path.name}.', dir=str(path.parent)) - try: - with os.fdopen(descriptor, 'wb') as stream: - stream.write(payload) - stream.flush() - os.fsync(stream.fileno()) - os.replace(temporary, path) - finally: - if os.path.exists(temporary): - os.unlink(temporary) - - -def publish_transaction( - transaction: MigrationTransaction, - metadata_path: Path, - metadata_text: str, - registry_path: Path, - registry_text: str, - *, - fail_stage: str | None = None, -) -> dict[str, object]: - before = { - metadata_path: metadata_path.read_bytes() if metadata_path.is_file() else None, - registry_path: registry_path.read_bytes() if registry_path.is_file() else None, - } - transaction.context['publication'] = { - 'metadata_before': hashlib.sha256(before[metadata_path] or b'').hexdigest(), - 'metadata_after': hashlib.sha256(metadata_text.encode('utf-8')).hexdigest(), - 'registry_before': hashlib.sha256(before[registry_path] or b'').hexdigest(), - 'registry_after': hashlib.sha256(registry_text.encode('utf-8')).hexdigest(), - } - transaction.state = 'applying' - try: - if fail_stage == 'metadata-publication': - raise MigrationError('WB_MIGRATION_INJECTED_METADATA_PUBLICATION_FAILURE') - _atomic_write(metadata_path, metadata_text.encode('utf-8')) - if fail_stage == 'registry-publication': - raise MigrationError('WB_MIGRATION_INJECTED_REGISTRY_PUBLICATION_FAILURE') - _atomic_write(registry_path, registry_text.encode('utf-8')) - if metadata_path.read_text(encoding='utf-8') != metadata_text or registry_path.read_text(encoding='utf-8') != registry_text: - raise MigrationError('WB_MIGRATION_PUBLICATION_VERIFY_FAILED') - except Exception as exc: - for path, payload in before.items(): - if payload is None: - path.unlink(missing_ok=True) - else: - _atomic_write(path, payload) - if isinstance(exc, MigrationError): - raise - raise MigrationError('WB_MIGRATION_PUBLICATION_FAILED') from exc - transaction.state = 'published' - return transaction.evidence( - metadata_status='published', - registry_status='published', - publication=transaction.context['publication'], - ) - - -def _failure(fail_stage: str | None, current: str) -> None: - if fail_stage == current: - raise MigrationError(f'WB_MIGRATION_INJECTED_{current.upper().replace("-", "_")}_FAILURE') - - -def _published_state_valid(target: Path, registry_path: Path, workspace_slug: str) -> bool: - metadata = target / '.work-bundle/project.yaml' - if not metadata.is_file() or not registry_path.is_file(): - return False - return ( - 'metadata_version: 3' in metadata.read_text(encoding='utf-8') - and 'workspace_mode: multi-repository' in metadata.read_text(encoding='utf-8') - and f' - slug: {workspace_slug}' in registry_path.read_text(encoding='utf-8') - ) - - -def _persisted_published_result(transaction: MigrationTransaction) -> dict[str, object]: - path = transaction.recovery_path - if not path.is_file(): - raise MigrationError('WB_MIGRATION_PUBLISHED_RECOVERY_MISSING') - try: - record = json.loads(path.read_text(encoding='utf-8')) - except (OSError, json.JSONDecodeError) as exc: - raise MigrationError('WB_MIGRATION_PUBLISHED_RECOVERY_INVALID') from exc - result = record.get('published_result') if isinstance(record, dict) else None - context = record.get('context') if isinstance(record, dict) else None - if ( - record.get('id') != transaction.transaction_id - or record.get('state') != 'published' - or not isinstance(context, dict) - or not isinstance(result, dict) - or not isinstance(result.get('transaction'), dict) - or result['transaction'].get('id') != transaction.transaction_id - or result['transaction'].get('context') != context - ): - raise MigrationError('WB_MIGRATION_PUBLISHED_RECOVERY_INCOMPLETE') - replay = json.loads(json.dumps(result)) - replay['idempotent'] = True - return replay - - -def _session_start_workspace_root(member_root: Path) -> Path: - """Use the SessionStart resolver without invoking registry or hook IO.""" - module_path = Path(__file__).resolve().parents[2] / 'bin' / 'work-bundle-session-start.py' - specification = importlib.util.spec_from_file_location('work_bundle_session_start_migration_check', module_path) - if specification is None or specification.loader is None: - raise MigrationError('WB_MIGRATION_SESSION_START_UNAVAILABLE') - module = importlib.util.module_from_spec(specification) - specification.loader.exec_module(module) - return Path(module.resolve_workspace_root(member_root)).resolve() - - -def _member_preflight(target: Path, member: dict[str, object], branch: str) -> dict[str, object]: - member_root = Path(str(member['project_root'])).resolve() - branch_result = _run_git(member_root, 'branch', '--show-current') - head_result = _run_git(member_root, 'rev-parse', 'HEAD') - common_result = _run_git(member_root, 'rev-parse', '--path-format=absolute', '--git-common-dir') - actual_branch = branch_result.stdout.strip() if branch_result.returncode == 0 else '' - observed_head = head_result.stdout.strip() if head_result.returncode == 0 else '' - common = Path(common_result.stdout.strip()).resolve() if common_result.returncode == 0 else Path('/') - scope_valid = ( - member_root != target.resolve() - and target.resolve() in member_root.parents - and common != target.resolve() - and target.resolve() in common.parents - ) - result = { - 'repository_id': member.get('repository_id'), - 'lifecycle_state': 'verified' if scope_valid and actual_branch == branch and observed_head else 'failed', - 'expected_branch': branch, - 'actual_branch': actual_branch, - 'branch_status': 'matched' if actual_branch == branch else 'mismatch', - 'observed_head': observed_head, - 'git_common_dir': str(common), - 'git_control_scope_valid': scope_valid, - } - result['passed'] = result['lifecycle_state'] == 'verified' - return result - - -def verify_target_before_publish( - target: Path, - member: dict[str, object], - metadata_text: str, - registry_text: str, - workspace_slug: str, - repository_id: str, - branch: str, -) -> dict[str, object]: - member_root = Path(str(member['project_root'])).resolve() - discovered = _session_start_workspace_root(member_root) - preflight = _member_preflight(target, member, branch) - credential_file = target / 'credentials' / 'credentials.yaml' - validations = { - 'script_index': {'passed': not validate_script_index(target)}, - 'credential_store': { - 'passed': ( - credential_file.is_file() - and stat.S_IMODE(credential_file.parent.stat().st_mode) == 0o700 - and stat.S_IMODE(credential_file.stat().st_mode) == 0o600 - ), - }, - 'session_start_discovery': { - 'passed': discovered == target.resolve(), - 'workspace_root': str(discovered), - 'member_root': str(member_root), - }, - 'member_preflight': preflight, - 'metadata_candidate': { - 'passed': all(token in metadata_text for token in ( - 'metadata_version: 3', - 'workspace_mode: multi-repository', - f' - id: {_yaml_string(repository_id)}', - ' lifecycle_status: active', - )), - }, - 'registry_candidate': {'passed': f' - slug: {workspace_slug}' in registry_text}, - } - validations['passed'] = all( - bool(value.get('passed')) for key, value in validations.items() - if key != 'passed' and isinstance(value, dict) - ) - return validations - - -def apply_migration( - source: Path, - target: Path, - repository_id: str, - branch: str, - base_ref: str = 'HEAD', - *, - origin: Path | None = None, - workspace_slug: str | None = None, - repository_name: str | None = None, - additional_repository_origins: list[dict[str, object]] | None = None, - accepted_baseline_id: str | None = None, - registry_path: Path | None = None, - fail_stage: str | None = None, - retry: bool = False, -) -> dict[str, object]: - source, target = source.resolve(), target.resolve() - origin = (origin or source).resolve() - slug = workspace_slug or target.name - name = repository_name or repository_id - if registry_path is None: - from project import project_registry_path - registry_path = project_registry_path() - registry_path = registry_path.resolve() - proposal = propose_migration( - source, - target, - repository_id, - branch, - base_ref, - origin=origin, - workspace_slug=slug, - repository_name=name, - additional_repository_origins=additional_repository_origins, - ) - baseline = proposal['accepted_baseline_evidence'] - if proposal['apply_requires_accepted_baseline'] and accepted_baseline_id != baseline['id']: - raise MigrationError('WB_MIGRATION_ACCEPTED_BASELINE_REQUIRED') - transaction_identity = ( - f'{source}:{target}:{slug}:{repository_id}' - if origin == source - else f'{source}:{origin}:{target}:{slug}:{repository_id}' - ) - transaction_id = hashlib.sha256(transaction_identity.encode('utf-8')).hexdigest()[:20] - target_preexisted = target.exists() - transaction = MigrationTransaction(target, transaction_id, target_root_created=not target_preexisted) - transaction.context.update({ - 'source_root': str(source), - 'member_origin_root': str(origin), - 'target_root': str(target), - 'workspace_slug': slug, - 'repository_id': repository_id, - 'working_branch': branch, - 'base_ref': base_ref, - 'baseline_id': baseline['id'], - 'source_repository_dirty': baseline['source_repository_dirty'], - 'work_bundle_git_dirty': baseline['work_bundle_git_dirty'], - 'member_origin_dirty': baseline['member_origin_dirty'], - 'target_root_preexisting': target_preexisted, - 'member': { - 'lifecycle_state': 'not-started', - 'expected_branch': branch, - 'base_ref': base_ref, - 'observed_git': None, - 'verification': None, - }, - 'metadata_identity': { - 'old': None, - 'new': {'version': 3, 'workspace_root': str(target), 'workspace_mode': 'multi-repository'}, - }, - 'registry_identity': { - 'old': {'workspace_slug': slug, 'published': False}, - 'new': {'workspace_slug': slug, 'workspace_root': str(target), 'status': 'active'}, - }, - }) - if retry and _published_state_valid(target, registry_path, slug): - return _persisted_published_result(transaction) - if target.exists() and any(target.iterdir()): - raise MigrationError('WB_MIGRATION_TARGET_NOT_EMPTY') - source_snapshot = { - 'repository_git': source_git_state(source), - 'work_bundle_git': work_bundle_git_state(source), - 'member_origin_git': source_git_state(origin), - 'work_bundle_inventory': _preservation_inventory(source / '.work-bundle'), - 'script_inventory': _inventory(source / 'script'), - 'agents_digest': _digest(source / 'AGENTS.md') if (source / 'AGENTS.md').is_file() else None, - } - registry_before = registry_path.read_bytes() if registry_path.is_file() else None - publication_complete = False - try: - target.mkdir(parents=True, exist_ok=True) - work_bundle_target = target / '.work-bundle' - transaction.own(work_bundle_target) - _copy_tree(source / '.work-bundle', work_bundle_target) - _failure(fail_stage, 'copy-authority') - if not verify_copy(source, target): - raise MigrationError('WB_MIGRATION_COPY_MISMATCH') - if (source / 'AGENTS.md').is_file(): - agents_target = target / 'AGENTS.md' - transaction.own(agents_target) - shutil.copy2(source / 'AGENTS.md', agents_target) - credentials_target = target / 'credentials' - transaction.own(credentials_target) - transaction.own(target / 'script') - transaction.own(target / 'roles') - transaction.own(target / '.gitignore') - ensure_workspace_resources(target) - from project import ensure_project_layout, sync_agents_managed_section - ensure_project_layout(target) - sync_agents_managed_section(target) - _failure(fail_stage, 'workspace-resources') - script_failures = validate_script_index(target) - if script_failures: - raise MigrationError('WB_MIGRATION_SCRIPT_INDEX_INVALID') - control_target = target / '.work-bundle/git' / f'{repository_id}.git' - member_target = target / repository_id - transaction.own(control_target) - transaction.own(member_target) - transaction.context['member']['lifecycle_state'] = 'provisioning' - member = provision_member(target, origin, repository_id, branch, base_ref) - member_preflight = _member_preflight(target, member, branch) - transaction.context['member'] = { - 'lifecycle_state': 'verified' if member_preflight['passed'] else 'failed', - 'expected_branch': branch, - 'base_ref': base_ref, - 'observed_git': { - 'branch': member_preflight['actual_branch'], - 'head': member_preflight['observed_head'], - 'git_common_dir': member_preflight['git_common_dir'], - }, - 'verification': { - 'passed': member_preflight['passed'], - 'git_control_scope_valid': member_preflight['git_control_scope_valid'], - }, - } - _failure(fail_stage, 'member-provision') - current_metadata = (target / '.work-bundle/project.yaml').read_text(encoding='utf-8') - transaction.context['metadata_identity']['old'] = { - 'version': next((line.split(':', 1)[1].strip() for line in current_metadata.splitlines() if line.startswith('metadata_version:')), ''), - 'digest': hashlib.sha256(current_metadata.encode('utf-8')).hexdigest(), - } - metadata_text = _metadata_text( - current_metadata, target, member, repository_id, branch, base_ref, transaction_id - ) - current_registry = registry_path.read_text(encoding='utf-8') if registry_path.is_file() else 'projects:\n' - transaction.context['registry_identity']['old'] = { - 'workspace_slug': slug, - 'published': f' - slug: {slug}' in current_registry, - 'digest': hashlib.sha256(current_registry.encode('utf-8')).hexdigest(), - } - registry_text = _registry_text( - current_registry, - target, - slug, - repository_id, - name, - origin, - list(additional_repository_origins or []), - ) - _failure(fail_stage, 'final-verification') - validation_results = verify_target_before_publish( - target, member, metadata_text, registry_text, slug, repository_id, branch - ) - source_before_publication = { - 'repository_git': source_git_state(source), - 'work_bundle_git': work_bundle_git_state(source), - 'member_origin_git': source_git_state(origin), - 'work_bundle_inventory': _preservation_inventory(source / '.work-bundle'), - 'script_inventory': _inventory(source / 'script'), - 'agents_digest': _digest(source / 'AGENTS.md') if (source / 'AGENTS.md').is_file() else None, - } - validation_results['source_preservation'] = {'passed': source_before_publication == source_snapshot} - validation_results['passed'] = bool(validation_results['passed'] and validation_results['source_preservation']['passed']) - transaction.context['member']['verification']['target_validation_passed'] = validation_results['passed'] - if not validation_results['passed']: - raise MigrationError('WB_MIGRATION_FINAL_VERIFICATION_FAILED') - publication = publish_transaction( - transaction, - target / '.work-bundle/project.yaml', - metadata_text, - registry_path, - registry_text, - fail_stage=fail_stage, - ) - publication_complete = True - source_after = { - 'repository_git': source_git_state(source), - 'work_bundle_git': work_bundle_git_state(source), - 'member_origin_git': source_git_state(origin), - 'work_bundle_inventory': _preservation_inventory(source / '.work-bundle'), - 'script_inventory': _inventory(source / 'script'), - 'agents_digest': _digest(source / 'AGENTS.md') if (source / 'AGENTS.md').is_file() else None, - } - if source_after != source_snapshot: - raise MigrationError('WB_MIGRATION_SOURCE_CHANGED') - result = { - 'status': 'published', - 'source_root': str(source), - 'target_root': str(target), - 'workspace_slug': slug, - 'member': member, - 'copied_inventory_and_digests': { - 'work_bundle': _inventory_digest(source_snapshot['work_bundle_inventory']), - }, - 'skipped_sensitive_and_transient_paths': ['credentials/credentials.yaml', 'script', *sorted(TRANSIENT_NAMES)], - 'script_index_validation': 'passed', - 'agents_merge_status': ( - 'managed-section-synchronized' - if source_snapshot['agents_digest'] - else 'managed-section-created' - ), - 'metadata_and_registry_status': {'metadata': 'published', 'registry': 'published'}, - 'source_preservation_checks': { - 'repository_git': True, - 'work_bundle_git': True, - 'member_origin_git': True, - 'authority_inventory': True, - 'script_inventory': True, - 'agents': True, - }, - 'transaction': publication, - 'transaction_record': str(transaction.recovery_path), - 'validation_results': validation_results, - 'retry_or_rollback_instructions': { - 'retry': 'retry with the same accepted_baseline_id', - 'rollback': 'remove transaction-owned target paths only; preserve the recovery record', - }, - 'source_repository_git': proposal['source_repository_git'], - 'work_bundle_git': proposal['work_bundle_git'], - 'member_origin_git': proposal['member_origin_git'], - 'accepted_baseline_id': baseline['id'], - 'git_actions': [], - } - transaction.persist( - metadata_status='published', - registry_status='published', - source_preserved=True, - baseline_id=baseline['id'], - published_result=result, - ) - return result - except Exception as exc: - code = exc.code if isinstance(exc, MigrationError) else 'WB_MIGRATION_FAILED' - if publication_complete: - if registry_before is None: - registry_path.unlink(missing_ok=True) - else: - _atomic_write(registry_path, registry_before) - transaction.state = 'failed' - transaction.failure_code = code - rollback_owned_paths(transaction) - transaction.state = 'failed' - record = transaction.persist( - metadata_status='rolled-back', - registry_status='unchanged', - source_preserved=( - source_git_state(source) == source_snapshot['repository_git'] - and work_bundle_git_state(source) == source_snapshot['work_bundle_git'] - and source_git_state(origin) == source_snapshot['member_origin_git'] - ), - baseline_id=baseline['id'], - ) - raise MigrationError(code, record) from exc - - -def retry_transaction(*args: object, **kwargs: object) -> dict[str, object]: - kwargs['retry'] = True - return apply_migration(*args, **kwargs) diff --git a/scripts/work-bundle/project.py b/scripts/work-bundle/project.py index d642ca9..ea4c351 100644 --- a/scripts/work-bundle/project.py +++ b/scripts/work-bundle/project.py @@ -1,34 +1,27 @@ from __future__ import annotations import hashlib -import shutil import subprocess from core import * from bootstrap_config import default_toolkit_root -from workspace import WorkspaceContext, WorkspaceTransaction -from workspace_resources import ensure_workspace_resources, validate_script_index, _load_yaml -try: - import yaml as _yaml_module -except ImportError: - _yaml_module = None +from workspace_resources import validate_script_index, _load_yaml +from infrastructure import ( + InfrastructureError, + atomic_write_text, + dump_canonical_yaml, + load_project_registry, + parse_yaml_mapping, + resolve_anchor_context, + validate_infrastructure_document, +) -def migrate_project_metadata_v3(source_root: Path, target_root: Path, repository_id: str, branch: str, base_ref: str = 'HEAD', apply: bool = False, **options: object) -> dict[str, object]: - from migration import apply_migration, propose_migration - if not apply: - options.pop('accepted_baseline_id', None) - return propose_migration(source_root, target_root, repository_id, branch, base_ref, **options) - return apply_migration(source_root, target_root, repository_id, branch, base_ref, **options) - DIAG_REFERENCE_ASSET_MISSING = 'WB_REFERENCE_ASSET_MISSING' INIT_TREE_MANIFEST = 'references/wb-initialize-project-default-work-bundle-tree.yaml' INIT_WORK_BUNDLE_GITIGNORE = 'references/wb-initialize-project-default-work-bundle-gitignore' -INIT_RULE_INDEX = 'references/wb-initialize-project-default-rule-index.yaml' -INIT_PROJECT_TEMPLATE = 'references/assets/template/project.yaml' INIT_AGENTS_TEMPLATE = 'references/assets/template/AGENTS.md' -PROJECT_REGISTRY_TEMPLATE = 'references/assets/template/projects.yaml' AGENTS_SYNC_MANAGED_SECTION = 'work-bundle-rule' AGENTS_SYNC_TEMPLATE_PATH = INIT_AGENTS_TEMPLATE AGENTS_RULE_START_MARKER = '\n'.join([ @@ -41,7 +34,6 @@ def migrate_project_metadata_v3(source_root: Path, target_root: Path, repository '# Work Bundle RULE END', '# ========================', ]) -REQUIRED_PROJECT_GITIGNORE = ['.work-bundle/', 'AGENTS.md', 'credentials/'] PROJECT_METADATA_V3_REQUIRED_FIELDS = [ 'metadata_version', 'authority', @@ -57,11 +49,6 @@ def migrate_project_metadata_v3(source_root: Path, target_root: Path, repository 'metadata_version', 'authority', 'project_root', 'source_repository_roles', 'operation_policy', 'source_repositories', 'migration', ] -PROJECT_METADATA_VERSION = '3' -REGISTRY_SCHEMA_VERSION = '1' -_PROJECT_BLOCK_LOAD_ERRORS: tuple[type[BaseException], ...] = (ValueError, TypeError) -if _yaml_module is not None: - _PROJECT_BLOCK_LOAD_ERRORS = _PROJECT_BLOCK_LOAD_ERRORS + (_yaml_module.YAMLError,) SOURCE_REPOSITORY_ROLES = { 'registry': 'Locator only: workspace slug/root and stable repository origin identity and locators.', 'project_metadata': 'Working-state authority: member path, branch/HEAD observation, lifecycle transaction, operation policy, and CodeGraph state.', @@ -74,17 +61,6 @@ def migrate_project_metadata_v3(source_root: Path, target_root: Path, repository 'review', 'project_scope_update', ] -INIT_FORCE_REL_PATHS = frozenset({ - 'AGENTS.md', - '.work-bundle/project.yaml', - '.work-bundle/rules/index.yaml', - '.work-bundle/knowledge/project.yaml', -}) -MIGRATE_FORCE_REL_PATHS = frozenset({ - '.work-bundle/project.yaml', -}) - - class ReferenceAssetError(Exception): def __init__(self, path: str, code: str = DIAG_REFERENCE_ASSET_MISSING) -> None: self.path = path @@ -142,10 +118,6 @@ def _init_orchestration_dirs() -> list[str]: return _work_bundle_relative_paths(_init_tree_roots(), 'orchestration/') -def _init_knowledge_dirs() -> list[str]: - return _work_bundle_relative_paths(_init_tree_roots(), 'knowledge/') - - def _init_gitignore_patterns() -> list[str]: lines = [] for raw in _require_reference_text(INIT_WORK_BUNDLE_GITIGNORE).splitlines(): @@ -155,31 +127,6 @@ def _init_gitignore_patterns() -> list[str]: return lines -def _yaml_string(value: object) -> str: - text = str(value or '') - if not text: - return '""' - if re.search(r'[\s:#\[\]{},&*?|\-<>=!%@`"\']', text): - return '"' + text.replace('\\', '\\\\').replace('"', '\\"') + '"' - return text - - -def _source_repository_state_from_locator(locator: dict[str, object], fallback_slug: str) -> dict[str, object]: - raw_path = locator.get('origin_path') or locator.get('path') - resolved = Path(str(raw_path)).expanduser().resolve() if raw_path else Path.cwd().resolve() - state = _source_repository_state(resolved, fallback_slug) - source_id = str(locator.get('id') or state['id']) - state['id'] = source_id - state['work_dir'] = bool(locator.get('work_dir', False)) - state['checkout_role'] = _checkout_role(locator, source_id) - state['checkout_kind'] = 'single-repository' if state['checkout_role'] == 'truth' else 'local-project' - state['git_control_scope'] = 'project' if state.get('git_repository') else 'not-applicable' - locator_remote = str(locator.get('remote') or '') - if locator_remote: - state['remote'] = locator_remote - return state - - def _checkout_role(source: dict[str, object], source_id: str = '') -> str: explicit = str(source.get('checkout_role') or '') if explicit: @@ -190,119 +137,6 @@ def _checkout_role(source: dict[str, object], source_id: str = '') -> str: return 'development' if bool(source.get('work_dir')) else 'auxiliary' -def _registry_source_repository_states(project_root: Path, name: str | None, registry_entry_data: dict[str, object] | None) -> list[dict[str, object]]: - slug = _slug_from_root(project_root, name) - if not registry_entry_data: - return [_source_repository_state(project_root, slug)] - sources = registry_entry_data.get('source_repositories') - if not isinstance(sources, list) or not sources: - return [_source_repository_state(project_root, slug)] - states: list[dict[str, object]] = [] - for index, source in enumerate(sources): - if not isinstance(source, dict): - continue - states.append(_source_repository_state_from_locator(source, f'{slug}-{index + 1}')) - return states or [_source_repository_state(project_root, slug)] - - -def _source_repositories_block(repositories: list[dict[str, object]]) -> str: - lines = ['source_repositories:'] - for repo in repositories: - codegraph = repo.get('codegraph') if isinstance(repo.get('codegraph'), dict) else {} - lines.extend([ - f" - id: {_yaml_string(repo.get('id'))}", - f" project_root: {_yaml_string(repo.get('project_root') or repo.get('path'))}", - f" origin_id: {_yaml_string(repo.get('origin_id') or repo.get('id'))}", - f" checkout_kind: {_yaml_string(repo.get('checkout_kind') or 'single-repository')}", - f" git_control_root: {_yaml_string(repo.get('git_control_root'))}", - f" git_control_scope: {_yaml_string(repo.get('git_control_scope') or 'project')}", - f" worktree_name: {_yaml_string(repo.get('worktree_name') or repo.get('id'))}", - f" git_repository: {str(bool(repo.get('git_repository'))).lower()}", - f" expected_branch: {_yaml_string(repo.get('expected_branch') or repo.get('working_branch'))}", - f" base_ref: {_yaml_string(repo.get('base_ref') or 'HEAD')}", - f" observed_head: {_yaml_string(repo.get('observed_head') or repo.get('last_commit_id'))}", - f" observation_time: {_yaml_string(repo.get('observation_time') or utc_now_rfc3339())}", - f" baseline_status: {repo.get('baseline_status', 'current')}", - f" lifecycle_status: {repo.get('lifecycle_status', 'active')}", - f" operation_policy: {repo.get('operation_policy', 'inherit')}", - ]) - lines.extend([ - " codegraph:", - f" supported: {str(bool(codegraph.get('supported'))).lower()}", - f" index_present: {str(bool(codegraph.get('index_present'))).lower()}", - f" root: {_yaml_string(codegraph.get('root') or repo.get('path'))}", - f" status: {codegraph.get('status', 'not-indexed')}", - f" synced_commit_id: {_yaml_string(codegraph.get('synced_commit_id'))}", - f" last_synced_at: {_yaml_string(codegraph.get('last_synced_at'))}", - f" reason: {_yaml_string(codegraph.get('reason'))}", - ]) - return '\n'.join(lines) - - -def _source_repository_roles_block() -> str: - return '\n'.join([ - 'source_repository_roles:', - f" registry: {_yaml_string(SOURCE_REPOSITORY_ROLES['registry'])}", - f" project_metadata: {_yaml_string(SOURCE_REPOSITORY_ROLES['project_metadata'])}", - ]) - - -def _replace_top_level_block(lines: list[str], block: str, key: str) -> tuple[list[str], bool]: - replacement = block.splitlines() - bounds = _yaml_block_bounds(lines, key) - if not bounds: - if lines and lines[-1] != '': - lines.append('') - return lines + replacement, True - start, end = bounds - if lines[start:end] == replacement: - return lines, False - return lines[:start] + replacement + lines[end:], True - - -def _render_project_metadata( - project_root: Path, - name: str | None = None, - registry_entry_data: dict[str, object] | None = None, - workspace_root: Path | None = None, - mode: str = 'single-repository', -) -> str: - template = _require_reference_text(INIT_PROJECT_TEMPLATE) - slug = _slug_from_root(project_root, name) - repositories = _registry_source_repository_states(project_root, name, registry_entry_data) - repo = repositories[0] - replacements = { - '<absolute-path-to-workspace-root>': str((workspace_root or project_root).resolve()), - '<single-repository|multi-repository>': mode, - '<absolute-path-to-project-root>': str(project_root.resolve()), - '<industry-or-domain>': name or slug, - '<runtime-or-framework>': 'unspecified', - '<stable-repository-id>': repo['id'], - '<absolute-path-to-source-repository>': repo['path'], - '<required-working-branch>': repo['working_branch'], - '<git-head-commit-or-empty-for-non-git>': repo['last_commit_id'], - '<rfc3339-observation-time>': repo['observation_time'], - '<commit|stage|pull>': 'commit,stage,pull', - '<push>': 'push', - '<reset --hard>': 'reset --hard', - } - rendered = template - for key, value in replacements.items(): - rendered = rendered.replace(key, value) - rendered_lines, _ = _replace_top_level_block( - rendered.splitlines(), - _source_repositories_block(repositories), - 'source_repositories', - ) - rendered_lines, _ = _replace_top_level_block( - rendered_lines, - _source_repository_roles_block(), - 'source_repository_roles', - ) - rendered = '\n'.join(rendered_lines) - return rendered.rstrip() + '\n' - - def _git_value(project_root: Path, *args: str) -> str: result = subprocess.run( ['git', '-C', str(project_root), *args], @@ -329,15 +163,6 @@ def _git_remote(project_root: Path) -> str: return _git_value(project_root, 'remote', 'get-url', 'origin') -def _git_command_ok(project_root: Path, *args: str) -> bool: - return subprocess.run( - ['git', '-C', str(project_root), *args], - check=False, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ).returncode == 0 - - def _metadata_commit_drift_allowed(project_root: Path, expected: str, actual: str) -> bool: return not expected or not actual or expected == actual @@ -487,42 +312,41 @@ def _yaml_block_bounds(lines: list[str], key: str) -> tuple[int, int] | None: return start, end -def _agents_sync_metadata_lines(checksum: str, status: str, synced_at: str) -> list[str]: - return [ - 'agents_sync:', - f' managed_section: {AGENTS_SYNC_MANAGED_SECTION}', - f' template_path: {AGENTS_SYNC_TEMPLATE_PATH}', - f' template_checksum_sha256: "{checksum}"', - f' last_synced_at: "{synced_at}"', - f' status: {status}', - ] +def _validated_workspace_metadata(path: Path) -> dict[str, object]: + document = parse_yaml_mapping(read(path), source=str(path)) + return validate_infrastructure_document( + document, + family='workspace-project-metadata', + ) def _metadata_agents_checksum(path: Path) -> str: - lines = read(path).splitlines() - bounds = _yaml_block_bounds(lines, 'agents_sync') - if not bounds: + document = _validated_workspace_metadata(path) + agents_sync = document.get('agents_sync') + if not isinstance(agents_sync, dict): return '' - start, end = bounds - for line in lines[start + 1:end]: - stripped = line.strip() - if stripped.startswith('template_checksum_sha256:'): - return stripped.split(':', 1)[1].strip().strip('"').strip("'") - return '' + return str(agents_sync.get('template_checksum_sha256') or '') def _update_project_agents_sync(project_root: Path, checksum: str, status: str) -> bool: path = project_root / '.work-bundle/project.yaml' - synced_at = utc_now_rfc3339() - replacement = _agents_sync_metadata_lines(checksum, status, synced_at) - lines = read(path).splitlines() - bounds = _yaml_block_bounds(lines, 'agents_sync') - if bounds: - start, end = bounds - rendered_lines = lines[:start] + replacement + lines[end:] - else: - rendered_lines = lines + replacement - return write(path, '\n'.join(rendered_lines).rstrip() + '\n') + document = _validated_workspace_metadata(path) + document['agents_sync'] = { + 'managed_section': AGENTS_SYNC_MANAGED_SECTION, + 'template_path': AGENTS_SYNC_TEMPLATE_PATH, + 'template_checksum_sha256': checksum, + 'last_synced_at': utc_now_rfc3339(), + 'status': status, + } + validated = validate_infrastructure_document( + document, + family='workspace-project-metadata', + ) + rendered = dump_canonical_yaml(validated) + if read(path) == rendered: + return False + atomic_write_text(path, rendered) + return True def _yaml_scalar(text: str, key: str) -> str: @@ -745,153 +569,6 @@ def _workspace_metadata_failures(project_root: Path, metadata_text: str) -> list return failures -def _top_level_block_text(text: str, key: str) -> str: - lines = text.splitlines() - bounds = _yaml_block_bounds(lines, key) - if not bounds: - return '' - start, end = bounds - return '\n'.join(lines[start:end]).rstrip() - - -def _replace_or_append_scalar(lines: list[str], key: str, value: str) -> tuple[list[str], bool]: - rendered: list[str] = [] - replaced = False - changed = False - new_line = f'{key}: {value}' - for line in lines: - if line.startswith(f'{key}:'): - rendered.append(new_line) - replaced = True - changed = changed or line != new_line - else: - rendered.append(line) - if not replaced: - rendered.append(new_line) - changed = True - return rendered, changed - - -def _append_missing_top_level_block(lines: list[str], current_text: str, rendered_template: str, key: str) -> tuple[list[str], bool]: - if _yaml_block_bounds(lines, key): - return lines, False - block = _top_level_block_text(rendered_template, key) - if not block: - return lines, False - if lines and lines[-1] != '': - lines.append('') - lines.extend(block.splitlines()) - return lines, True - - -def migrate_project_metadata_v2( - project_root: Path, - name: str | None = None, - registry_entry_data: dict[str, object] | None = None, -) -> bool: - metadata_path = project_root / '.work-bundle/project.yaml' - if not metadata_path.is_file(): - return False - current = read(metadata_path) - if _yaml_scalar(current, 'metadata_version') == PROJECT_METADATA_VERSION: - return False - rendered = _render_project_metadata(project_root, name, registry_entry_data) - rendered_keys = { - line.split(':', 1)[0] for line in rendered.splitlines() - if line and not line.startswith((' ', '#')) and ':' in line - } - current_lines = current.splitlines() - unknown_blocks: list[str] = [] - index = 0 - while index < len(current_lines): - line = current_lines[index] - if not line or line.startswith((' ', '#')) or ':' not in line: - index += 1 - continue - key = line.split(':', 1)[0] - end = index + 1 - while end < len(current_lines) and (not current_lines[end] or current_lines[end].startswith((' ', '\t'))): - end += 1 - if key not in rendered_keys: - unknown_blocks.append('\n'.join(current_lines[index:end]).rstrip()) - index = end - next_text = rendered.rstrip() - if unknown_blocks: - next_text += '\n\n' + '\n\n'.join(unknown_blocks) - return write(metadata_path, next_text.rstrip() + '\n') - - -def _workspace_root_from_registry_entry(entry: dict[str, object], fallback_root: Path) -> Path: - work_bundle_root = str(entry.get('work_bundle_root') or '') - if work_bundle_root: - root = Path(work_bundle_root).expanduser().resolve() - if root.name == '.work-bundle': - return root.parent - return fallback_root.expanduser().resolve() - - -def _merge_metadata_source_repositories(current: list[dict[str, object]], desired: list[dict[str, object]]) -> list[dict[str, object]]: - merged: list[dict[str, object]] = [] - for desired_repo in desired: - matched = next((repo for repo in current if _same_source_repository(repo, desired_repo)), None) - if matched is None: - merged.append(desired_repo) - continue - refreshed = dict(matched) - observation_changed = ( - matched.get('observed_head') != desired_repo.get('observed_head') - or matched.get('expected_branch') != desired_repo.get('expected_branch') - ) - for key in ( - 'id', - 'path', - 'checkout_role', - 'work_dir', - 'remote', - 'git_repository', - 'working_branch', - 'branch_required', - 'branch_check', - 'last_commit_id', - 'baseline_status', - 'codegraph', - 'project_root', 'origin_id', 'checkout_kind', 'git_control_root', - 'git_control_scope', 'worktree_name', 'expected_branch', 'base_ref', - 'observed_head', 'lifecycle_status', 'operation_policy', - ): - refreshed[key] = desired_repo.get(key) - if observation_changed or not refreshed.get('observation_time'): - refreshed['observation_time'] = desired_repo.get('observation_time') - merged.append(refreshed) - return merged - - -def sync_project_metadata_from_registry_entry( - entry: dict[str, object], - name: str | None = None, - fallback_root: Path | None = None, -) -> tuple[bool, Path, str]: - workspace_root = _workspace_root_from_registry_entry(entry, fallback_root or Path.cwd()) - metadata_path = workspace_root / '.work-bundle/project.yaml' - if not metadata_path.is_file(): - return False, metadata_path, 'missing' - current_text = read(metadata_path) - rendered = _render_project_metadata(workspace_root, name or str(entry.get('name') or entry.get('slug') or ''), entry) - current_repositories = _metadata_source_repositories(current_text) - desired_repositories = _metadata_source_repositories(rendered) - merged_repositories = _merge_metadata_source_repositories(current_repositories, desired_repositories) - lines = current_text.splitlines() - changed = False - lines, roles_changed = _replace_top_level_block(lines, _source_repository_roles_block(), 'source_repository_roles') - changed = changed or roles_changed - lines, repositories_changed = _replace_top_level_block(lines, _source_repositories_block(merged_repositories), 'source_repositories') - changed = changed or repositories_changed - if changed: - write(metadata_path, '\n'.join(lines).rstrip() + '\n') - return True, metadata_path, 'updated' - return False, metadata_path, 'current' - - def sync_agents_managed_section(project_root: Path, dry_run: bool = False, force: bool = False) -> dict[str, object]: agents_path = project_root / 'AGENTS.md' metadata_path = project_root / '.work-bundle/project.yaml' @@ -950,127 +627,15 @@ def sync_agents_managed_section(project_root: Path, dry_run: bool = False, force } -def _ensure_lines(path: Path, lines: list[str]) -> bool: - current = read(path).splitlines() - changed = False - for line in lines: - if line not in current: - current.append(line) - changed = True - if changed or not path.exists(): - write(path, '\n'.join(current).rstrip() + '\n') - return changed - - def _has_ignore(lines: list[str], wanted: str) -> bool: variants = {wanted, wanted.rstrip('/'), '/' + wanted.rstrip('/')} return any(line.strip() in variants for line in lines) -def _rel_project_path(project_root: Path, path: Path) -> str: - return str(path.relative_to(project_root)).replace('\\', '/') - - def _project_rule_store_root(project_root: Path) -> Path: return project_root / '.work-bundle' / 'rules' -def _template_overwrite(project_root: Path, path: Path, force: bool, scope: str) -> bool: - if not force: - return False - rel = _rel_project_path(project_root, path) - allowed = INIT_FORCE_REL_PATHS if scope == 'init' else MIGRATE_FORCE_REL_PATHS - return rel in allowed - - -def _cleanup_retired_bootstrap(project_root: Path) -> tuple[list[str], list[dict[str, str]], str | None]: - bootstrap_dir = project_root / 'references/bootstrap' - if not bootstrap_dir.exists(): - return [], [], None - changed: list[str] = [] - retired_artifacts: list[dict[str, str]] = [] - archive_root = project_root / '.work-bundle/orchestration/docs' / f'legacy-bootstrap-archive-{utc_now_rfc3339()[:10]}' - for path in sorted(bootstrap_dir.rglob('*')): - if not path.is_file(): - continue - rel = path.relative_to(bootstrap_dir) - rel_text = str(rel).replace('\\', '/') - dest = archive_root / rel - dest.parent.mkdir(parents=True, exist_ok=True) - if write(dest, read(path)): - changed.append(str(dest)) - retired_artifacts.append({ - 'source': _rel_project_path(project_root, path), - 'relative_path': rel_text, - 'archive_path': _rel_project_path(project_root, dest), - 'action': 'archived-and-removed', - }) - shutil.rmtree(bootstrap_dir) - changed.append(_rel_project_path(project_root, bootstrap_dir)) - return changed, retired_artifacts, _rel_project_path(project_root, archive_root) - - -def _retire_legacy_rules_contract(project_root: Path) -> tuple[list[str], dict[str, str] | None, str | None]: - contract_path = project_root / 'rules/contract.yaml' - if not contract_path.is_file(): - return [], None, None - changed: list[str] = [] - source = _rel_project_path(project_root, contract_path) - archive_root = project_root / '.work-bundle/orchestration/docs' / f'legacy-rules-contract-archive-{utc_now_rfc3339()[:10]}' - dest = archive_root / 'contract.yaml' - dest.parent.mkdir(parents=True, exist_ok=True) - if write(dest, read(contract_path)): - changed.append(str(dest)) - contract_path.unlink() - changed.append(source) - artifact = { - 'source': source, - 'archive_path': _rel_project_path(project_root, dest), - 'action': 'archived-and-removed', - } - return changed, artifact, _rel_project_path(project_root, archive_root) - - -def _render_bootstrap_retirement_report_section( - retired_artifacts: list[dict[str, str]], - archive_root: str | None, -) -> list[str]: - if not retired_artifacts: - return [] - lines = [ - '', - '## Retired Legacy Bootstrap Artifacts', - '', - f"- archive_root: {archive_root}", - f"- retired_count: {len(retired_artifacts)}", - '', - ] - for artifact in retired_artifacts: - lines.extend([ - f"- source: {artifact.get('source')}", - f" archive_path: {artifact.get('archive_path')}", - f" action: {artifact.get('action')}", - ]) - return lines - - -def _render_rules_contract_retirement_report_section( - artifact: dict[str, str] | None, - archive_root: str | None, -) -> list[str]: - if not artifact: - return [] - return [ - '', - '## Retired Legacy Rules Contract', - '', - f"- archive_root: {archive_root}", - f"- source: {artifact.get('source')}", - f" archive_path: {artifact.get('archive_path')}", - f" action: {artifact.get('action')}", - ] - - def inspect_project(project_root: Path) -> dict: wb = project_root / '.work-bundle' rules = _project_rule_store_root(project_root) @@ -1157,252 +722,16 @@ def project_failures(data: dict, strict: bool = True, include_roles: bool = Fals return failures -def _refresh_registered_project_metadata(current: str, rendered: str) -> str: - current_repositories = _metadata_source_repositories(current) - desired_repositories = _metadata_source_repositories(rendered) - merged_repositories = _merge_metadata_source_repositories(current_repositories, desired_repositories) - lines = current.splitlines() - for key, value in ( - ('metadata_version', PROJECT_METADATA_VERSION), - ('authority', 'canonical'), - ('workspace_root', _yaml_scalar(rendered, 'workspace_root')), - ('workspace_mode', _yaml_scalar(rendered, 'workspace_mode')), - ('project_root', _yaml_scalar(rendered, 'project_root')), - ): - lines, _ = _replace_or_append_scalar(lines, key, value) - lines, _ = _replace_top_level_block( - lines, - _top_level_block_text(rendered, 'workspace_resources'), - 'workspace_resources', - ) - lines, _ = _replace_top_level_block(lines, _source_repository_roles_block(), 'source_repository_roles') - lines, _ = _replace_top_level_block(lines, _source_repositories_block(merged_repositories), 'source_repositories') - return '\n'.join(lines).rstrip() + '\n' - - -def ensure_project_layout(project_root: Path) -> list[str]: - """Create non-authority workspace structure without changing metadata or Git.""" - wb = project_root / '.work-bundle' - knowledge = wb / 'knowledge' - changed: list[str] = [] - if _ensure_lines(project_root / '.gitignore', REQUIRED_PROJECT_GITIGNORE): - changed.append(str(project_root / '.gitignore')) - for directory in _init_tree_roots(): - if directory == 'rules': - directory = '.work-bundle/rules' - path = project_root / directory - if not path.exists(): - path.mkdir(parents=True, exist_ok=True) - changed.append(str(path)) - for role in ROLE_NAMES: - role_path = project_root / 'roles' / f'{role}.yaml' - role_text = '\n'.join([ - f'id: {role}', 'status: current', - 'domain_profile: .work-bundle/project.yaml', 'duty_profile:', - ' stance: project-specific responsibilities must be resolved before work', - ' skilled_at: []', ' quality_focus: []', - ' must_resolve_from_context:', ' - project-metadata', '', - ]) - if write(role_path, role_text, overwrite=False): - changed.append(str(role_path)) - knowledge_project = knowledge / 'project.yaml' - if write(knowledge_project, 'id: project\nstatus: current\n', overwrite=False): - changed.append(str(knowledge_project)) - if _ensure_lines(wb / '.gitignore', _init_gitignore_patterns()): - changed.append(str(wb / '.gitignore')) - rule_index_path = _project_rule_store_root(project_root) / 'index.yaml' - if write( - rule_index_path, - _require_reference_text(INIT_RULE_INDEX).rstrip() + '\n', - overwrite=False, - ): - changed.append(str(rule_index_path)) - return sorted(set(changed)) - - -def apply_project( - project_root: Path, - init_git: bool = True, - create_override: bool = False, - name: str | None = None, - force: bool = False, - scope: str = 'init', - registry_entry_data: dict[str, object] | None = None, - return_details: bool = False, - workspace_root: Path | None = None, - mode: str = 'single-repository', -) -> list[str] | tuple[list[str], dict[str, object]]: - wb = project_root / '.work-bundle' - knowledge = wb / 'knowledge' - changed = ensure_project_layout(project_root) - changed.extend(ensure_workspace_resources((workspace_root or project_root).resolve())) - knowledge_project = knowledge / 'project.yaml' - if _template_overwrite(project_root, knowledge_project, force, scope) and write( - knowledge_project, 'id: project\nstatus: current\n', overwrite=True - ): - changed.append(str(knowledge_project)) - rule_index_path = _project_rule_store_root(project_root) / 'index.yaml' - if _template_overwrite(project_root, rule_index_path, force, scope) and write( - rule_index_path, _require_reference_text(INIT_RULE_INDEX).rstrip() + '\n', overwrite=True - ): - changed.append(str(rule_index_path)) - project_metadata_path = project_root / '.work-bundle/project.yaml' - project_metadata = _render_project_metadata(project_root, name, registry_entry_data, workspace_root, mode) - if project_metadata_path.is_file(): - current_metadata = read(project_metadata_path) - current_version = _yaml_scalar(current_metadata, 'metadata_version') - if current_version in {'1', '2'}: - project_metadata = current_metadata - elif registry_entry_data is not None: - project_metadata = _refresh_registered_project_metadata(current_metadata, project_metadata) - if write( - project_metadata_path, - project_metadata, - overwrite=_template_overwrite(project_root, project_metadata_path, force, scope), - ): - changed.append(str(project_metadata_path)) - agents_result = sync_agents_managed_section(project_root, force=force) - changed.extend(str(path) for path in agents_result.get('changed_files', [])) - if create_override: - path = wb / 'orchestration/skill-registry.override.yaml' - if write(path, 'id: project-skill-registry-override\nstatus: current\noverrides: {}\n', overwrite=False): - changed.append(str(path)) - changed = sorted(set(changed)) - if return_details: - return changed, agents_result - return changed - - -def repair_project(project_root: Path, force: bool = False, return_details: bool = False) -> list[str] | tuple[list[str], dict[str, object]]: - current_metadata = read(project_root / '.work-bundle/project.yaml') - if ( - _yaml_scalar(current_metadata, 'metadata_version') == '3' - and _yaml_scalar(current_metadata, 'workspace_mode') == 'multi-repository' - ): - registry_entry_data, _ = find_registry_entry(project_root) - changed = ensure_project_layout(project_root) - changed.extend(ensure_workspace_resources(project_root)) - agents_result = sync_agents_managed_section(project_root, force=force) - changed.extend(str(path) for path in agents_result.get('changed_files', [])) - if registry_entry_data is not None: - metadata_changed, refreshed_path, _ = sync_project_metadata_from_registry_entry( - registry_entry_data, - fallback_root=project_root, - ) - if metadata_changed: - changed.append(str(refreshed_path)) - changed = sorted(set(changed)) - if return_details: - return changed, agents_result - return changed - registry_entry_data, _ = find_registry_entry(project_root) - changed, agents_result = apply_project( - project_root, - init_git=False, - force=force, - scope='init' if force else 'migrate', - registry_entry_data=registry_entry_data, - return_details=True, - ) - data = inspect_project(project_root) - metadata_path = project_root / '.work-bundle/project.yaml' - if data.get('project_metadata_required_fields_missing') and not read(metadata_path).strip(): - rendered = _render_project_metadata(project_root) - if write(metadata_path, rendered, overwrite=force): - changed.append(str(metadata_path)) - if registry_entry_data is not None: - metadata_changed, refreshed_path, _ = sync_project_metadata_from_registry_entry( - registry_entry_data, - fallback_root=project_root, - ) - if metadata_changed: - changed.append(str(refreshed_path)) - contract_changed, _, _ = _retire_legacy_rules_contract(project_root) - changed.extend(contract_changed) - if force: - bootstrap_changed, _, _ = _cleanup_retired_bootstrap(project_root) - changed.extend(bootstrap_changed) - changed = sorted(set(changed)) - if return_details: - return changed, agents_result - return changed - - def _slug_from_root(project_root: Path, name: str | None = None) -> str: raw = name or project_root.name or "project" slug = re.sub(r"[^A-Za-z0-9_.-]+", "-", raw.strip().lower()).strip("-") return slug or "project" -def _bootstrap_value(key: str, default: str) -> str: - config_root = work_bundle_config_root() - bootstrap = compact_yaml_map(read(config_root / GLOBAL_BOOTSTRAP_FILE_NAME)) - value = bootstrap.get(key, default) - return value.replace("$work_bundle_config_root", str(config_root)) - - def project_registry_path() -> Path: return resolve_project_registry_path() -def _set_yaml_scalar(path: Path, key: str, value: str) -> bool: - lines = read(path).splitlines() - rendered: list[str] = [] - new_line = f'{key}: {value}' - replaced = False - changed = False - for line in lines: - if line.strip().startswith(f'{key}:'): - replaced = True - rendered.append(new_line) - if line != new_line: - changed = True - continue - rendered.append(line) - if not replaced: - rendered.append(new_line) - changed = True - if changed or not path.exists(): - write(path, '\n'.join(rendered).rstrip() + '\n') - return changed - - -def _ensure_registry_schema_version(text: str, version: str = REGISTRY_SCHEMA_VERSION) -> str: - rendered = text if text.endswith('\n') else text + '\n' - for line in rendered.splitlines(): - if line.startswith('registry_schema_version:'): - return rendered - insert = [f'registry_schema_version: {version}'] - lines = rendered.splitlines() - if lines and lines[0].strip(): - return '\n'.join(insert + lines).rstrip() + '\n' - return '\n'.join(insert + lines).rstrip() + '\n' - - -def _ensure_source_repository_roles(text: str) -> str: - rendered = text if text.endswith('\n') else text + '\n' - lines = rendered.splitlines() - if _yaml_block_bounds(lines, 'source_repository_roles'): - return rendered - block_lines = _source_repository_roles_block().splitlines() - projects = _yaml_block_bounds(lines, 'projects') - insert_at = projects[0] if projects else len(lines) - if insert_at < len(lines) and block_lines and block_lines[-1] != '': - block_lines.append('') - return '\n'.join(lines[:insert_at] + block_lines + lines[insert_at:]).rstrip() + '\n' - - -def _ensure_device_bindings(text: str) -> str: - rendered = text if text.endswith('\n') else text + '\n' - lines = rendered.splitlines() - if _yaml_block_bounds(lines, 'device_bindings'): - return rendered - if lines and lines[-1] != '': - lines.append('') - lines.append('device_bindings: {}') - return '\n'.join(lines).rstrip() + '\n' - - def _normalize_loaded_registry_value(value: object) -> object: if isinstance(value, dict): return {str(key): _normalize_loaded_registry_value(item) for key, item in value.items()} @@ -1416,77 +745,6 @@ def _normalize_loaded_registry_value(value: object) -> object: return value -def _project_blocks_line_scoped(text: str) -> list[dict[str, object]]: - projects: list[dict[str, object]] = [] - current: dict[str, object] | None = None - current_list: str | None = None - current_repo: dict[str, object] | None = None - in_projects = False - for raw in text.splitlines(): - line = raw.rstrip() - stripped = line.strip() - if not stripped or stripped.startswith('#'): - continue - if stripped.startswith('projects:'): - in_projects = True - continue - if line and not line.startswith((' ', '\t')): - if in_projects: - break - continue - if not in_projects: - continue - if line.startswith(" - "): - if current is not None: - projects.append(current) - current = {} - current_list = None - current_repo = None - key, value = stripped[2:].split(":", 1) - current[key.strip()] = value.strip().strip('"') - continue - if current is None: - continue - if stripped.startswith("- ") and current_list: - item = stripped[2:].strip() - if current_list == "aliases": - current.setdefault("aliases", []).append(item) - elif current_list == "source_repositories": - current_repo = {} - current.setdefault("source_repositories", []).append(current_repo) - if ":" in item: - key, value = item.split(":", 1) - current_repo[key.strip()] = value.strip().strip('"') - continue - if line.startswith(" ") and not line.startswith(" "): - key, value = stripped.split(":", 1) - key = key.strip() - value = value.strip() - current_list = None - current_repo = None - if value == "": - if key in {"aliases", "source_repositories"}: - current[key] = [] - current_list = key - else: - current[key] = {} - elif value == "[]": - current[key] = [] - else: - current[key] = value.strip('"') - continue - if line.startswith(" ") and current_repo is not None and ":" in stripped: - key, value = stripped.split(":", 1) - value = value.strip().strip('"') - if value in {"true", "false"}: - current_repo[key.strip()] = value == "true" - else: - current_repo[key.strip()] = value - if current is not None: - projects.append(current) - return projects - - def _project_blocks_from_document(document: object) -> list[dict[str, object]] | None: if not isinstance(document, dict): return None @@ -1505,133 +763,18 @@ def _project_blocks_from_document(document: object) -> list[dict[str, object]] | def _project_blocks(path: Path) -> list[dict[str, object]]: - text = read(path) - if not text.strip(): + if not path.is_file(): return [] - try: - loaded = _project_blocks_from_document(_load_yaml(text)) - except _PROJECT_BLOCK_LOAD_ERRORS: - loaded = None - if loaded is not None: - return loaded - lines = text.splitlines() - bounds = _yaml_block_bounds(lines, 'projects') - scoped = '\n'.join(lines[bounds[0]:bounds[1]]) + '\n' if bounds else text - return _project_blocks_line_scoped(scoped) - - -def _render_registry_scalar(value: object) -> str: - if isinstance(value, bool): - return 'true' if value else 'false' - if value is None: - return 'null' - if isinstance(value, (int, float)) and not isinstance(value, bool): - return str(value) - return _yaml_string(value) - - -def _render_registry_value(lines: list[str], indent: int, key: str, value: object) -> None: - prefix = ' ' * indent - if isinstance(value, dict): - if not value: - lines.append(f'{prefix}{key}: {{}}') - return - lines.append(f'{prefix}{key}:') - for nested_key, nested_value in value.items(): - _render_registry_value(lines, indent + 2, str(nested_key), nested_value) - return - if isinstance(value, list): - if not value: - lines.append(f'{prefix}{key}: []') - return - lines.append(f'{prefix}{key}:') - for item in value: - if isinstance(item, dict): - entries = list(item.items()) - if not entries: - lines.append(f'{prefix} - {{}}') - continue - first_key, first_value = entries[0] - if isinstance(first_value, (dict, list)): - lines.append(f'{prefix} -') - for nested_key, nested_value in entries: - _render_registry_value(lines, indent + 4, str(nested_key), nested_value) - continue - lines.append(f'{prefix} - {first_key}: {_render_registry_scalar(first_value)}') - for nested_key, nested_value in entries[1:]: - _render_registry_value(lines, indent + 4, str(nested_key), nested_value) - elif isinstance(item, list): - lines.append(f'{prefix} - []') - else: - lines.append(f'{prefix} - {_render_registry_scalar(item)}') - return - lines.append(f'{prefix}{key}: {_render_registry_scalar(value)}') - - -def _render_projects_collection(projects: list[dict[str, object]]) -> str: - known = { - 'slug', 'name', 'work_bundle_root', 'knowledge_root', 'aliases', - 'layout_version', 'source_repositories', 'status', 'updated_at', - } - lines = ['projects:'] - if not projects: - lines[0] = 'projects: []' - return '\n'.join(lines) - for project in sorted(projects, key=lambda item: str(item.get("slug", ""))): - lines.append(f" - slug: {project.get('slug', '')}") - lines.append(f" name: {project.get('name', project.get('slug', ''))}") - lines.append(f" work_bundle_root: {project.get('work_bundle_root', '')}") - lines.append(f" knowledge_root: {project.get('knowledge_root', '')}") - aliases = project.get("aliases") if isinstance(project.get("aliases"), list) else [] - if aliases: - lines.append(" aliases:") - for alias in aliases: - lines.append(f" - {alias}") - else: - lines.append(" aliases: []") - if project.get("layout_version") not in {None, ''}: - lines.append(f" layout_version: {project.get('layout_version')}") - extras = [ - key for key in project - if key not in known - ] - for key in extras: - _render_registry_value(lines, 4, str(key), project.get(key)) - sources = project.get("source_repositories") if isinstance(project.get("source_repositories"), list) else [] - lines.append(" source_repositories:") - for index, source in enumerate(sources or [{"path": project.get("project_root", ""), "work_dir": True, "remote": ""}]): - if not isinstance(source, dict): - continue - source_id = str(source.get("id", "") or _source_repository_id(str(project.get("slug", "project")))) - lines.append(f" - id: {source_id}") - lines.append(f" path: {source.get('path', '')}") - lines.append(f" checkout_role: {_checkout_role(source, source_id)}") - lines.append(f" work_dir: {str(bool(source.get('work_dir', index == 0))).lower()}") - remote = str(source.get("remote", "")) - lines.append(f' remote: "{remote}"' if remote else ' remote: ""') - lines.append(f" git_repository: {str(bool(source.get('git_repository', False))).lower()}") - lines.append(f" status: {project.get('status', 'active')}") - lines.append(f" updated_at: {project.get('updated_at', utc_now_rfc3339()[:10])}") - return "\n".join(lines) - - -def _render_projects(projects: list[dict[str, object]], original: str | None = None) -> str: - collection = _render_projects_collection(projects) - if original is None: - return _source_repository_roles_block() + '\n' + collection + '\n' - lines = original.splitlines() - lines, _ = _replace_top_level_block(lines, collection, 'projects') - rendered = _ensure_registry_schema_version('\n'.join(lines).rstrip() + '\n') - rendered = _ensure_source_repository_roles(rendered) - return _ensure_device_bindings(rendered) - - -def _project_registry_template_text() -> str: - path = _resolved_work_bundle_root() / PROJECT_REGISTRY_TEMPLATE - if path.is_file(): - text = read(path) - return text if text.endswith("\n") else text + "\n" - return "projects: []\n" + document = parse_yaml_mapping(read(path), source=str(path)) + validated = validate_infrastructure_document(document, family='project-registry') + loaded = _project_blocks_from_document(validated) + if loaded is None: + raise InfrastructureError( + 'WB_INFRASTRUCTURE_SCHEMA_INVALID', + f'project-registry failed structural validation: {path}', + details={'family': 'project-registry', 'path': str(path)}, + ) + return loaded def _normalize_registry_path(value: object) -> str: @@ -1676,21 +819,6 @@ def _same_source_repository(left: dict[str, object], right: dict[str, object]) - return bool(left_id and right_id and left_id == right_id) -def _registry_source_from_root(project_root: Path, name: str | None) -> dict[str, object]: - entry = registry_entry(project_root, name) - sources = entry.get("source_repositories") - if isinstance(sources, list) and sources and isinstance(sources[0], dict): - return sources[0] - repo = _source_repository_state(project_root, _slug_from_root(project_root, name)) - return { - "id": repo["id"], - "path": repo["path"], - "work_dir": True, - "remote": repo["remote"], - "git_repository": repo["git_repository"], - } - - def _unique_source_repository_id(slug: str, source: dict[str, object], used_ids: set[str]) -> str: source_id = str(source.get("id") or "") if source_id and source_id not in used_ids: @@ -1807,10 +935,9 @@ def upsert_project_registry( source_repositories: list[dict[str, object]] | None = None, ) -> tuple[dict[str, object], bool, Path]: path = project_registry_path() - path.parent.mkdir(parents=True, exist_ok=True) - if not path.exists(): - write(path, _project_registry_template_text()) - projects = _project_blocks(path) + document = load_project_registry() + raw_projects = document.get("projects") + projects = [dict(item) for item in raw_projects if isinstance(item, dict)] if isinstance(raw_projects, list) else [] incoming = registry_entry(project_root, name, aliases if aliases is not None else None) entry = incoming changed = False @@ -1845,9 +972,11 @@ def upsert_project_registry( next_projects.append(incoming) entry = incoming changed = True - rendered = _render_projects(next_projects, read(path)) + document["projects"] = next_projects + validate_infrastructure_document(document, family="project-registry") + rendered = dump_canonical_yaml(document) if read(path) != rendered: - write(path, rendered) + atomic_write_text(path, rendered) changed = True return entry, changed, path @@ -1855,7 +984,11 @@ def upsert_project_registry( def find_registry_entry(project_root: Path) -> tuple[dict[str, object] | None, Path]: path = project_registry_path() target = str(project_root.resolve()) - for project in _project_blocks(path): + document = load_project_registry() + projects = document.get("projects") + for project in projects if isinstance(projects, list) else []: + if not isinstance(project, dict): + continue if project.get("work_bundle_root") == str(project_root / ".work-bundle"): return project, path sources = project.get("source_repositories") @@ -1867,160 +1000,6 @@ def find_registry_entry(project_root: Path) -> tuple[dict[str, object] | None, P return None, path -def assess_legacy_topology( - project_root: Path, - metadata_text: str, - registry_entry_data: dict[str, object] | None, - registry_origin_data: list[dict[str, object]] | None = None, -) -> dict[str, object]: - """Classify legacy metadata without converting repository locators into members.""" - project_root = project_root.expanduser().resolve() - metadata_sources = _metadata_source_repositories(metadata_text) - registry_sources = ( - registry_entry_data.get('source_repositories') - if isinstance(registry_entry_data, dict) - else [] - ) - registry_sources = registry_sources if isinstance(registry_sources, list) else [] - - def identities(sources: object, identity_kind: str) -> list[dict[str, str]]: - result: list[dict[str, str]] = [] - if not isinstance(sources, list): - return result - for source in sources: - if not isinstance(source, dict): - continue - raw_path = source.get('project_root') or source.get('origin_path') or source.get('path') - result.append({ - 'id': str(source.get('id') or ''), - 'path': str(Path(str(raw_path)).expanduser().resolve()) if raw_path else '', - 'kind': identity_kind, - }) - return result - - metadata_identities = identities(metadata_sources, 'member') - registry_member_identities = identities(registry_sources, 'member') - registry_origin_identities = identities(registry_origin_data or [], 'origin') - registry_identities = [*registry_member_identities, *registry_origin_identities] - registry_identities = [ - dict(identity) - for identity in { - (item['id'], item['path']): item - for item in registry_identities - }.values() - ] - conflicts: list[str] = [] - by_id: dict[str, set[str]] = {} - for identity in [*metadata_identities, *registry_member_identities]: - if identity['id'] and identity['path']: - by_id.setdefault(identity['id'], set()).add(identity['path']) - for source_id, paths in sorted(by_id.items()): - if len(paths) > 1: - conflicts.append(f'repository-id-path-conflict:{source_id}') - - paths = { - identity['path'] - for identity in [*metadata_identities, *registry_identities] - if identity['path'] - } - if conflicts: - classification = 'topology-conflict' - failure_code = 'WB_MIGRATION_TOPOLOGY_CONFLICT' - elif len(paths) > 1: - classification = 'multi-repository-migration-required' - failure_code = 'WB_MIGRATION_MULTI_REPOSITORY_WORKFLOW_REQUIRED' - elif paths and paths != {str(project_root)}: - classification = 'topology-conflict' - failure_code = 'WB_MIGRATION_TOPOLOGY_CONFLICT' - else: - classification = 'single-compatible' - failure_code = '' - return { - 'classification': classification, - 'failure_code': failure_code, - 'metadata_sources': metadata_identities, - 'registry_sources': registry_identities, - 'distinct_repository_paths': sorted(paths), - 'conflicts': conflicts, - 'required_command': 'migrate-to-multi-repository' if classification == 'multi-repository-migration-required' else '', - } - - -def _metadata_migration_proposal( - project_root: Path, - metadata_text: str, - registry_path: Path, - registry_entry_data: dict[str, object] | None, - name: str | None, - force: bool, -) -> dict[str, object]: - topology = assess_legacy_topology( - project_root, - metadata_text, - registry_entry_data, - _registry_origin_locators(registry_path, registry_entry_data), - ) - facts = { - 'project_root': str(project_root.resolve()), - 'name': name or '', - 'force': force, - 'metadata_sha256': hashlib.sha256(metadata_text.encode('utf-8')).hexdigest(), - 'registry_topology_sha256': hashlib.sha256( - json.dumps(topology['registry_sources'], sort_keys=True).encode('utf-8') - ).hexdigest(), - 'topology': topology, - } - proposal_id = hashlib.sha256(json.dumps(facts, sort_keys=True).encode('utf-8')).hexdigest() - return {'id': proposal_id, **facts} - - -def _registry_origin_locators( - registry_path: Path, - registry_entry_data: dict[str, object] | None, -) -> list[dict[str, object]]: - if not registry_path.is_file() or not isinstance(registry_entry_data, dict): - return [] - slug = str(registry_entry_data.get('slug') or '') - if not slug: - return [] - lines = registry_path.read_text(encoding='utf-8').splitlines() - starts = [index for index, line in enumerate(lines) if line.startswith(' - slug:')] - project_bounds: tuple[int, int] | None = None - for position, start in enumerate(starts): - value = lines[start].split(':', 1)[1].strip().strip('"\'') - if value == slug: - project_bounds = (start, starts[position + 1] if position + 1 < len(starts) else len(lines)) - break - if project_bounds is None: - return [] - start, end = project_bounds - origin_start = next( - (index for index in range(start + 1, end) if lines[index].startswith(' repository_origins:')), - None, - ) - if origin_start is None: - return [] - origins: list[dict[str, object]] = [] - current: dict[str, object] | None = None - for line in lines[origin_start + 1:end]: - if line and not line.startswith(' '): - break - if line.startswith(' - '): - if current is not None: - origins.append(current) - current = {} - item = line.strip()[2:] - if ':' in item: - key, value = item.split(':', 1) - current[key.strip()] = value.strip().strip('"\'') - elif current is not None and line.startswith(' ') and ':' in line: - key, value = line.strip().split(':', 1) - current[key.strip()] = value.strip().strip('"\'') - if current is not None: - origins.append(current) - return origins - - def list_project_registry() -> tuple[list[dict[str, object]], Path]: path = project_registry_path() return _project_blocks(path), path @@ -2028,7 +1007,11 @@ def list_project_registry() -> tuple[list[dict[str, object]], Path]: def remove_project_registry(project: str) -> tuple[bool, Path]: path = project_registry_path() - projects = _project_blocks(path) + if not path.is_file(): + return False, path + document = load_project_registry() + raw_projects = document.get('projects') + projects = [dict(item) for item in raw_projects if isinstance(item, dict)] if isinstance(raw_projects, list) else [] kept: list[dict[str, object]] = [] removed = False for entry in projects: @@ -2039,7 +1022,9 @@ def remove_project_registry(project: str) -> tuple[bool, Path]: continue kept.append(entry) if removed: - write(path, _render_projects(kept, read(path))) + document['projects'] = kept + validated = validate_infrastructure_document(document, family='project-registry') + atomic_write_text(path, dump_canonical_yaml(validated)) return removed, path @@ -2063,15 +1048,6 @@ def project_registry_issues() -> list[str]: return issues -def _reference_failure_payload(exc: ReferenceAssetError, command: str) -> dict[str, object]: - return { - 'command': command, - 'status': 'issues-found', - 'failures': [exc.code], - 'missing_reference': exc.path, - } - - def _agents_sync_output(result: dict[str, object]) -> dict[str, object]: return { 'status': result.get('agents_status'), @@ -2091,7 +1067,12 @@ def _session_start_payload(project_root: Path) -> dict[str, object]: runtime = resolve_bootstrap_runtime() bootstrap_path = Path(str(runtime.get('global_bootstrap_path'))) work_bundle_root = runtime.get('resolved_work_bundle_root') - registry_path = project_registry_path() + registry_path = Path(str(runtime.get('work_bundle_config_root'))) / 'registry/projects.yaml' + if bootstrap_path.is_file(): + try: + registry_path = project_registry_path() + except InfrastructureError: + pass metadata_path = project_root / '.work-bundle/project.yaml' agents_path = project_root / 'AGENTS.md' return { @@ -2116,15 +1097,20 @@ def _session_start_payload(project_root: Path) -> dict[str, object]: def _session_start_metadata_warnings(project_root: Path) -> list[str]: metadata_path = project_root / '.work-bundle/project.yaml' - metadata = read(metadata_path) - version = _yaml_scalar(metadata, 'metadata_version') - required = PROJECT_METADATA_V3_REQUIRED_FIELDS if version == '3' else PROJECT_METADATA_V2_REQUIRED_FIELDS - missing = [field for field in required if f'{field}:' not in metadata] - if version not in {'2', '3'}: - missing.insert(0, 'metadata_version[2|3]') - if not missing: - return [] - return [_session_start_warning(f'project metadata missing required fields: {", ".join(missing)}', project_root)] + try: + metadata = parse_yaml_mapping(read(metadata_path), source=str(metadata_path)) + except InfrastructureError as exc: + return [_session_start_warning(f'project metadata invalid: {exc.code}', project_root)] + version = metadata.get('metadata_version') + if version in {2, 3, '2', '3'}: + return [_session_start_warning(f'historical project metadata v{version} requires migration', project_root)] + if version != 4: + return [_session_start_warning(f'project metadata version unsupported: {version!r}', project_root)] + try: + resolve_anchor_context(workspace_root=project_root, cwd=project_root) + except InfrastructureError as exc: + return [_session_start_warning(f'current project metadata or device binding invalid: {exc.code}', project_root)] + return [] def cmd_session_start(args: list[str]) -> int: @@ -2185,11 +1171,18 @@ def cmd_session_start(args: list[str]) -> int: print(f"session-start skipped: {'; '.join(warnings)}") return 0 - entry, registry = find_registry_entry(project_root) - data['registry_path'] = str(registry) - data['registry_status'] = 'registered' if entry else 'not-registered' - if entry is None: - warnings.append(_session_start_warning('project registry entry missing', project_root)) + try: + context = resolve_anchor_context(workspace_root=project_root, cwd=project_root) + portable = parse_yaml_mapping(read(metadata_path), source=str(metadata_path)) + workspace = portable.get('workspace') if isinstance(portable, dict) else None + entry = { + 'workspace_id': context.workspace_id, + 'slug': workspace.get('slug') if isinstance(workspace, dict) else project_root.name, + 'workspace_root': str(context.workspace_root), + } + data['registry_status'] = 'registered' + except InfrastructureError as exc: + warnings.append(_session_start_warning(f'project registry binding invalid: {exc.code}', project_root)) data['warnings'] = warnings if parsed.json: out(data) @@ -2236,100 +1229,17 @@ def cmd_session_start(args: list[str]) -> int: def cmd_init_project(args: list[str]) -> int: - parser = argparse.ArgumentParser(prog="wb.py init-project") - parser.add_argument("project_root") - parser.add_argument("--mode", choices=['single-repository', 'multi-repository']) - parser.add_argument("--workspace-root") - parser.add_argument("--name") - parser.add_argument("--force", action="store_true") - parser.add_argument("--dry-run", action="store_true") - parser.add_argument("--disable-work-bundle-git", action="store_true") - parser.add_argument("--create-project-skill-override", action="store_true") - parsed = parser.parse_args(args) - project_root = Path(parsed.project_root).expanduser().resolve() - workspace_root = Path(parsed.workspace_root).expanduser().resolve() if parsed.workspace_root else project_root - existing_metadata = read(workspace_root / '.work-bundle/project.yaml') - declared_mode = _yaml_scalar(existing_metadata, 'workspace_mode') - if parsed.mode is None and not declared_mode: - out({ - 'command': 'init-project', - 'status': 'issues-found', - 'mode': None, - 'dry_run': parsed.dry_run, - 'changed_files': [], - 'git_actions': [], - 'failures': ['WB_WORKSPACE_MODE_REQUIRED'], - }) - return 1 - mode = parsed.mode or declared_mode - try: - WorkspaceContext(workspace_root, mode, project_root if mode == 'single-repository' else None).validate() - except ValueError as exc: - out({'command': 'init-project', 'status': 'issues-found', 'failures': [str(exc)]}) - return 1 - changed: list[str] | str = "none" - agents_result: dict[str, object] = { - 'agents_status': 'skipped-dry-run' if parsed.dry_run else 'skipped', - 'template_checksum_sha256': '', + out({ + 'command': 'init-project', + 'status': 'issues-found', + 'failure_code': 'WB_CURRENT_INIT_COMMAND_RETIRED', 'changed_files': [], - 'warnings': [], - 'failures': [], - 'dry_run': parsed.dry_run, - } - if not parsed.dry_run: - resource_changes = ensure_workspace_resources(workspace_root) - existing_entry, _ = find_registry_entry(project_root) - try: - changed, agents_result = apply_project( - project_root, - init_git=not parsed.disable_work_bundle_git, - create_override=parsed.create_project_skill_override, - name=parsed.name, - force=parsed.force, - scope='init', - registry_entry_data=existing_entry, - return_details=True, - workspace_root=workspace_root, - mode=mode, - ) - except ReferenceAssetError as exc: - out(_reference_failure_payload(exc, 'init-project')) - return 1 - entry, registry_changed, registry = upsert_project_registry(project_root, parsed.name) - if registry_changed and isinstance(changed, list): - changed.append(str(registry)) - if isinstance(changed, list): - changed.extend(resource_changes) - else: - entry = registry_entry(project_root, parsed.name) - registry = project_registry_path() - data = inspect_project(project_root) - failures = project_failures(data, strict=not parsed.dry_run, include_roles=False) - data.update({ - "command": "init-project", - "registry_path": str(registry), - "registry_entry": entry, - "status": "passed" if not failures else "issues-found", - "failures": failures, - "agents_status": agents_result.get('agents_status'), - "agents_sync": _agents_sync_output(agents_result), - "mode": mode, - "dry_run": parsed.dry_run, - "git_actions": [], - "transaction": { - "id": f"init-{_slug_from_root(workspace_root, parsed.name)}", - "state": "proposed" if parsed.dry_run else ("published" if not failures else "failed"), - "owned_paths": sorted(set(changed if isinstance(changed, list) else [])) if not parsed.dry_run else [], - "registry_status": "unchanged" if parsed.dry_run else ("published" if not failures else "failed"), - "metadata_status": "unchanged" if parsed.dry_run else ("published" if not failures else "failed"), + 'migration': { + 'current_creation': 'wb.py init-workspace <workspace-root> --slug <slug> --repository <id=remote> (--dry-run|--apply)', + 'historical_metadata': 'wb.py migrate-control-plane <workspace-root> (--dry-run|--apply --accepted-proposal-id <id>)', }, }) - if parsed.dry_run: - data["dry_run"] = True - else: - data["changed_files"] = sorted(set(changed if isinstance(changed, list) else [])) - out(data) - return 0 if not failures else 1 + return 1 def cmd_register_project_command(args: list[str]) -> int: @@ -2337,14 +1247,27 @@ def cmd_register_project_command(args: list[str]) -> int: parser.add_argument("project_root") parser.add_argument("--name") parsed = parser.parse_args(args) - project_root = Path(parsed.project_root).expanduser().resolve() + selected_root = Path(parsed.project_root).expanduser().resolve() + try: + context = resolve_anchor_context( + workspace_root=selected_root if (selected_root / '.work-bundle/project.yaml').is_file() else None, + project_root=selected_root if not (selected_root / '.work-bundle/project.yaml').is_file() else None, + cwd=selected_root, + ) + except InfrastructureError as exc: + out({ + 'command': 'register-project', + 'status': 'issues-found', + 'failure_code': exc.code, + 'changed_files': [], + }) + return 1 + project_root = context.workspace_root + metadata_path = project_root / '.work-bundle/project.yaml' entry, registry_changed, registry = upsert_project_registry(project_root, parsed.name) - metadata_changed, metadata_path, metadata_status = sync_project_metadata_from_registry_entry(entry, parsed.name, project_root) changed_files: list[str] = [] if registry_changed: changed_files.append(str(registry)) - if metadata_changed: - changed_files.append(str(metadata_path)) out({ "command": "register-project", "status": "updated" if changed_files else "skipped", @@ -2352,7 +1275,7 @@ def cmd_register_project_command(args: list[str]) -> int: "registry_entry": entry, "project": entry, "project_metadata_path": str(metadata_path), - "project_metadata_status": metadata_status, + "project_metadata_status": 'portable-v4-unchanged', "source_repository_roles": SOURCE_REPOSITORY_ROLES, "changed_files": sorted(changed_files), }) @@ -2365,24 +1288,23 @@ def cmd_show_project(args: list[str]) -> int: roots.add_argument("--project-root") roots.add_argument("--workspace-root") parsed = parser.parse_args(args) - selected_root = parsed.project_root or parsed.workspace_root or "." - project_root = Path(selected_root).expanduser().resolve() - if _yaml_scalar(read(project_root / ".work-bundle/project.yaml"), "metadata_version") == "4": + selected_root = Path(parsed.project_root or parsed.workspace_root or ".").expanduser().resolve() + try: + context = resolve_anchor_context( + workspace_root=selected_root if parsed.workspace_root or (selected_root / ".work-bundle/project.yaml").is_file() else None, + project_root=selected_root if parsed.project_root and not (selected_root / ".work-bundle/project.yaml").is_file() else None, + cwd=selected_root, + ) + except InfrastructureError as exc: + out({'command': 'show-project', 'status': 'issues-found', 'failure_code': exc.code, 'changed_files': []}) + return 1 + workspace_root = context.workspace_root + version = _yaml_scalar(read(workspace_root / ".work-bundle/project.yaml"), "metadata_version") + if version == "4": from control_plane import cmd_doctor_workspace - return cmd_doctor_workspace([str(project_root)], command_name="show-project") - data = inspect_project(project_root) - entry, registry = find_registry_entry(project_root) - failures = project_failures(data, strict=False, include_roles=False) - data.update({ - "command": "show-project", - "registry_path": str(registry), - "registry_status": "registered" if entry else "not-registered", - "registry_entry": entry, - "status": "passed" if not failures else "issues-found", - "failures": failures, - }) - out(data) - return 0 + return cmd_doctor_workspace([str(workspace_root)], command_name="show-project") + out({'command': 'show-project', 'status': 'issues-found', 'failure_code': 'WB_METADATA_MIGRATION_REQUIRED', 'metadata_version': version, 'changed_files': []}) + return 1 def cmd_validate_project(args: list[str]) -> int: @@ -2390,396 +1312,69 @@ def cmd_validate_project(args: list[str]) -> int: parser.add_argument("project_root") parser.add_argument("--dry-run", action="store_true") parsed = parser.parse_args(args) - project_root = Path(parsed.project_root).expanduser().resolve() - if _yaml_scalar(read(project_root / ".work-bundle/project.yaml"), "metadata_version") == "4": + selected_root = Path(parsed.project_root).expanduser().resolve() + try: + context = resolve_anchor_context( + workspace_root=selected_root if (selected_root / ".work-bundle/project.yaml").is_file() else None, + project_root=selected_root if not (selected_root / ".work-bundle/project.yaml").is_file() else None, + cwd=selected_root, + ) + except InfrastructureError as exc: + out({'command': 'validate-project', 'status': 'issues-found', 'failure_code': exc.code, 'changed_files': []}) + return 1 + workspace_root = context.workspace_root + version = _yaml_scalar(read(workspace_root / ".work-bundle/project.yaml"), "metadata_version") + if version == "4": from control_plane import cmd_doctor_workspace - return cmd_doctor_workspace([str(project_root)], command_name="validate-project") - data = inspect_project(project_root) - entry, registry = find_registry_entry(project_root) - failures = project_failures(data, strict=True, include_roles=False) - if entry is None: - failures.append("project_not_registered") - data.update({ - "command": "validate-project", - "registry_path": str(registry), - "registry_status": "registered" if entry else "not-registered", - "registry_entry": entry, - "status": "passed" if not failures else "issues-found", - "failures": failures, - }) - out(data) - return 0 if not failures else 1 + return cmd_doctor_workspace([str(workspace_root)], command_name="validate-project") + out({'command': 'validate-project', 'status': 'issues-found', 'failure_code': 'WB_METADATA_MIGRATION_REQUIRED', 'metadata_version': version, 'changed_files': []}) + return 1 def cmd_provision_member(args: list[str]) -> int: - from member import MemberLifecycleError, provision_member_lifecycle - parser = argparse.ArgumentParser(prog='wb.py provision-member') - parser.add_argument('--workspace-root', required=True) - parser.add_argument('--origin', required=True) - parser.add_argument('--repository-id', required=True) - parser.add_argument('--working-branch', required=True) - parser.add_argument('--base-ref', default='HEAD') - parser.add_argument('--workspace-slug') - mode = parser.add_mutually_exclusive_group(required=True) - mode.add_argument('--dry-run', action='store_true') - mode.add_argument('--apply', action='store_true') - parsed = parser.parse_args(args) - workspace_root = Path(parsed.workspace_root).expanduser().resolve() - origin = Path(parsed.origin).expanduser().resolve() - try: - result = provision_member_lifecycle( - workspace_root, - origin, - parsed.repository_id, - parsed.working_branch, - parsed.base_ref, - workspace_slug=parsed.workspace_slug, - dry_run=parsed.dry_run, - ) - except MemberLifecycleError as exc: - out({ - 'command': 'provision-member', - 'status': 'issues-found', - 'mode': 'multi-repository', - 'dry_run': parsed.dry_run, - 'failure_code': exc.code, - 'failures': [exc.code], - 'result': exc.result, - 'git_actions': [], - }) - return 1 - out({'command': 'provision-member', **result}) - return 0 + out({ + 'command': 'provision-member', 'status': 'issues-found', + 'failure_code': 'WB_V3_MEMBER_COMMAND_RETIRED', 'changed_files': [], + 'current_command': 'wb.py add-workspace-member <workspace-root> --repository-id <id> --remote <remote> --name <name> --path <path> --default-branch <branch> --dry-run', + }) + return 1 def cmd_cleanup_member(args: list[str]) -> int: - from member import MemberLifecycleError, cleanup_member_lifecycle - parser = argparse.ArgumentParser(prog='wb.py cleanup-member') - parser.add_argument('--workspace-root', required=True) - parser.add_argument('--repository-id', required=True) - mode = parser.add_mutually_exclusive_group(required=True) - mode.add_argument('--dry-run', action='store_true') - mode.add_argument('--apply', action='store_true') - parsed = parser.parse_args(args) - try: - result = cleanup_member_lifecycle( - Path(parsed.workspace_root), parsed.repository_id, dry_run=parsed.dry_run - ) - except MemberLifecycleError as exc: - out({ - 'command': 'cleanup-member', 'status': 'issues-found', - 'failure_code': exc.code, 'failures': [exc.code], - 'result': exc.result, 'git_actions': [], - }) - return 1 - out({'command': 'cleanup-member', **result}) - return 0 + out({ + 'command': 'cleanup-member', 'status': 'issues-found', + 'failure_code': 'WB_V3_MEMBER_COMMAND_RETIRED', 'changed_files': [], + 'repair': 'Use attach-workspace or doctor-workspace for current v4 binding repair.', + }) + return 1 def cmd_migrate_to_multi_repository(args: list[str]) -> int: - from migration import MigrationError - parser = argparse.ArgumentParser(prog='wb.py migrate-to-multi-repository') - parser.add_argument('source_project_root') - parser.add_argument('--target-workspace-root', required=True) - parser.add_argument('--repository-id', required=True) - parser.add_argument('--repository-name', required=True) - parser.add_argument( - '--origin', - help='Git repository used to provision the primary member; defaults to source_project_root', - ) - parser.add_argument('--workspace-slug', required=True) - parser.add_argument('--working-branch', required=True) - parser.add_argument('--base-ref', default='HEAD') - parser.add_argument('--accepted-baseline-id') - parser.add_argument('--additional-origin', action='append', default=[], metavar='ID=PATH') - mode = parser.add_mutually_exclusive_group(required=True) - mode.add_argument('--dry-run', action='store_true'); mode.add_argument('--apply', action='store_true') - parsed = parser.parse_args(args) - source_root = Path(parsed.source_project_root).expanduser().resolve() - primary_origin = Path(parsed.origin).expanduser().resolve() if parsed.origin else source_root - if not parsed.origin and not _is_git_repository(source_root): - out({ - 'command': 'migrate-to-multi-repository', 'status': 'issues-found', - 'failure_code': 'WB_MIGRATION_ORIGIN_REQUIRED', - }) - return 1 - if not _is_git_repository(primary_origin): - out({ - 'command': 'migrate-to-multi-repository', 'status': 'issues-found', - 'failure_code': 'WB_MIGRATION_ORIGIN_INVALID', - }) - return 1 - declared_paths = { - Path(str(item.get('path') or item.get('project_root') or '')).expanduser().resolve() - for item in _metadata_source_repositories(read(source_root / '.work-bundle/project.yaml')) - if str(item.get('path') or item.get('project_root') or '') - } - source_registry_entry, _ = find_registry_entry(source_root) - if isinstance(source_registry_entry, dict): - for item in source_registry_entry.get('source_repositories', []): - if isinstance(item, dict) and str(item.get('path') or ''): - declared_paths.add(Path(str(item['path'])).expanduser().resolve()) - if parsed.origin and declared_paths and primary_origin not in declared_paths: - out({ - 'command': 'migrate-to-multi-repository', 'status': 'issues-found', - 'failure_code': 'WB_MIGRATION_ORIGIN_NOT_DECLARED', - }) - return 1 - additional_origins: list[dict[str, object]] = [] - for value in parsed.additional_origin: - if '=' not in value: - parser.error('--additional-origin must use ID=PATH') - origin_id, origin_path = value.split('=', 1) - if not origin_id or not origin_path: - parser.error('--additional-origin must use non-empty ID=PATH') - additional_origins.append({ - 'id': origin_id, - 'origin_path': str(Path(origin_path).expanduser().resolve()), - 'remote': '', - 'git_repository': (Path(origin_path).expanduser() / '.git').exists(), - }) - try: - result = migrate_project_metadata_v3( - source_root, Path(parsed.target_workspace_root), - parsed.repository_id, parsed.working_branch, parsed.base_ref, parsed.apply, - origin=primary_origin, - workspace_slug=parsed.workspace_slug, repository_name=parsed.repository_name, - accepted_baseline_id=parsed.accepted_baseline_id, - additional_repository_origins=additional_origins, - ) - except (MigrationError, ValueError, RuntimeError) as exc: - payload = { - 'command': 'migrate-to-multi-repository', - 'status': 'issues-found', - 'failure_code': exc.code if isinstance(exc, MigrationError) else str(exc), - } - if isinstance(exc, MigrationError) and exc.result: - payload['result'] = exc.result - payload['changed_files'] = exc.result.get('changed_files', []) - payload['git_actions'] = exc.result.get('git_actions', []) - out(payload) - return 1 - out({'command':'migrate-to-multi-repository','status':'passed','result':result}) - return 0 - - -def apply_layout_v2_to_v3( - project_root: Path, - name: str | None = None, - force: bool = False, - registry_entry_data: dict[str, object] | None = None, -) -> dict[str, object]: - """Upgrade in-place metadata v2 to v3 without publishing registry current-state.""" - project_root = project_root.expanduser().resolve() - metadata_path = project_root / '.work-bundle/project.yaml' - current_text = read(metadata_path) - if _yaml_scalar(current_text, 'metadata_version') == PROJECT_METADATA_VERSION: - return { - 'status': 'passed', - 'from_version': PROJECT_METADATA_VERSION, - 'to_version': PROJECT_METADATA_VERSION, - 'changed_files': [], - 'failures': [], - } - entry = registry_entry_data - if entry is None: - entry, _ = find_registry_entry(project_root) - topology = assess_legacy_topology(project_root, current_text, entry) - if topology['classification'] != 'single-compatible': - failure_code = str(topology.get('failure_code') or 'WB_MIGRATION_TOPOLOGY_CONFLICT') - return { - 'status': 'failed', - 'from_version': _yaml_scalar(current_text, 'metadata_version'), - 'to_version': PROJECT_METADATA_VERSION, - 'changed_files': [], - 'failures': [failure_code], - 'failure_code': failure_code, - 'topology_assessment': topology, - } - changed, _agents_result = apply_project( - project_root, - init_git=False, - name=name, - force=force, - scope='migrate', - registry_entry_data=entry, - return_details=True, - ) - if migrate_project_metadata_v2(project_root, name, entry): - changed.append(str(metadata_path)) - contract_changed, _, _ = _retire_legacy_rules_contract(project_root) - changed.extend(contract_changed) - bootstrap_changed, _, _ = _cleanup_retired_bootstrap(project_root) - changed.extend(bootstrap_changed) - after = inspect_project(project_root) - failures = project_failures(after, strict=False, include_roles=False) - version = _yaml_scalar(read(metadata_path), 'metadata_version') - if version != PROJECT_METADATA_VERSION: - failures = failures or ['WB_REGISTRY_LAYOUT_VERSION_MISMATCH:' + version] - return { - 'status': 'passed' if not failures else 'failed', - 'from_version': '2', - 'to_version': PROJECT_METADATA_VERSION, - 'changed_files': sorted(set(changed)), - 'failures': failures, - 'failure_code': failures[0] if failures else '', - } + out({ + 'command': 'migrate-to-multi-repository', + 'status': 'issues-found', + 'failure_code': 'WB_TOPOLOGY_MIGRATION_COMMAND_RETIRED', + 'changed_files': [], + 'migration': { + 'new_workspace': 'wb.py init-workspace <workspace-root> --mode multi-repository --slug <slug> --repository <id=remote> --apply', + 'historical_metadata': 'wb.py migrate-control-plane <workspace-root> --dry-run', + }, + }) + return 1 def cmd_migrate_project(args: list[str]) -> int: - parser = argparse.ArgumentParser(prog="wb.py migrate-project") - parser.add_argument("project_root") - parser.add_argument("--name") - parser.add_argument("--force", action="store_true") - parser.add_argument("--accepted-proposal-id") - action = parser.add_mutually_exclusive_group() - action.add_argument("--dry-run", action="store_true") - action.add_argument("--apply", action="store_true") - parsed = parser.parse_args(args) - project_root = Path(parsed.project_root).expanduser().resolve() - before = inspect_project(project_root) - metadata_path = project_root / '.work-bundle/project.yaml' - current_text = read(metadata_path) - current_version = _yaml_scalar(current_text, 'metadata_version') - registry_entry_data, registry = find_registry_entry(project_root) - proposal_evidence = _metadata_migration_proposal( - project_root, current_text, registry, registry_entry_data, parsed.name, parsed.force - ) - topology = proposal_evidence['topology'] - if not parsed.apply: - proposal = '' - if topology['classification'] == 'single-compatible': - proposal = _render_project_metadata(project_root, parsed.name, registry_entry_data) - topology_failure = str(topology.get('failure_code') or '') - failures = ( - [topology_failure] - if topology_failure - else ([] if parsed.dry_run else ['WB_MIGRATION_EXPLICIT_ACTION_REQUIRED']) - ) - data = { - 'command': 'migrate-project', - 'status': 'passed' if parsed.dry_run and not failures else 'issues-found', - 'mode': 'single-repository' if topology['classification'] == 'single-compatible' else topology['classification'], - 'dry_run': True, - 'changed_files': [], - 'git_actions': [], - 'failures': failures, - 'topology_assessment': topology, - 'migration': { - 'from_version': current_version, - 'to_version': PROJECT_METADATA_VERSION, - 'preserves_unknown_fields': True, - 'proposed_sha256': hashlib.sha256(proposal.encode('utf-8')).hexdigest() if proposal else '', - 'proposal_id': proposal_evidence['id'], - 'apply_requires_accepted_proposal': current_version == '2', - }, - 'transaction': { - 'id': f"metadata-{_slug_from_root(project_root, parsed.name)}", - 'state': 'proposed', - 'owned_paths': [str(metadata_path)], - 'registry_status': 'unchanged', - 'metadata_status': 'pending', - }, - } - out(data) - return 0 if parsed.dry_run and not failures else 1 - if topology['classification'] != 'single-compatible': - failure_code = str(topology.get('failure_code') or 'WB_MIGRATION_TOPOLOGY_CONFLICT') - out({ - 'command': 'migrate-project', - 'status': 'issues-found', - 'mode': topology['classification'], - 'dry_run': False, - 'failures': [failure_code], - 'topology_assessment': topology, - 'changed_files': [], - 'git_actions': [], - }) - return 1 - if current_version == '2' and not parsed.accepted_proposal_id: - out({ - 'command': 'migrate-project', 'status': 'issues-found', 'dry_run': False, - 'failures': ['WB_MIGRATION_PROPOSAL_REQUIRED'], 'changed_files': [], 'git_actions': [], - 'proposal_id': proposal_evidence['id'], - }) - return 1 - if current_version == '2' and parsed.accepted_proposal_id != proposal_evidence['id']: - out({ - 'command': 'migrate-project', 'status': 'issues-found', 'dry_run': False, - 'failures': ['WB_MIGRATION_PROPOSAL_STALE'], 'changed_files': [], 'git_actions': [], - 'proposal_id': proposal_evidence['id'], - }) - return 1 - try: - changed, agents_result = apply_project( - project_root, - init_git=False, - name=parsed.name, - force=parsed.force, - scope='migrate', - return_details=True, - ) - except ReferenceAssetError as exc: - out(_reference_failure_payload(exc, 'migrate-project')) - return 1 - entry, registry_changed, registry = upsert_project_registry(project_root, parsed.name) - if registry_changed: - changed.append(str(registry)) - if migrate_project_metadata_v2(project_root, parsed.name, entry): - changed.append(str(project_root / '.work-bundle/project.yaml')) - contract_changed, retired_rules_contract, rules_contract_archive = _retire_legacy_rules_contract(project_root) - changed.extend(contract_changed) - bootstrap_changed, retired_artifacts, archive_root = _cleanup_retired_bootstrap(project_root) - changed.extend(bootstrap_changed) - after = inspect_project(project_root) - failures = project_failures(after, strict=True, include_roles=False) - report = project_root / ".work-bundle" / "orchestration" / "docs" / f"migration-report-{utc_now_rfc3339()[:10]}.md" - report_lines = [ - "# Work-Bundle Project Migration Report", - "", - f"- project_root: {project_root}", - f"- status: {'passed' if not failures else 'issues-found'}", - f"- before_status: {'passed' if not project_failures(before, strict=False, include_roles=False) else 'issues-found'}", - f"- changed_files: {len(set(changed))}", - f"- force: {parsed.force}", - ] - report_lines.extend(_render_bootstrap_retirement_report_section(retired_artifacts, archive_root)) - report_lines.extend(_render_rules_contract_retirement_report_section(retired_rules_contract, rules_contract_archive)) - report_lines.append("") - report_text = "\n".join(report_lines) - if write(report, report_text, overwrite=False): - changed.append(str(report)) out({ - "command": "migrate-project", - "status": "passed" if not failures else "issues-found", - "failures": failures, - "changed_files": sorted(set(changed)), - "agents_status": agents_result.get('agents_status'), - "agents_sync": _agents_sync_output(agents_result), - "migration_report": str(report), - "retired_bootstrap": { - "archive_root": archive_root, - "artifacts": retired_artifacts, - }, - "retired_rules_contract": { - "archive_root": rules_contract_archive, - "artifact": retired_rules_contract, - }, - "registry_path": str(registry), - "registry_entry": entry, - "before_status": "passed" if not project_failures(before, strict=False, include_roles=False) else "issues-found", - "mode": _yaml_scalar(read(metadata_path), 'workspace_mode') or 'single-repository', - "dry_run": False, - "git_actions": [], - "transaction": { - "id": f"metadata-{_slug_from_root(project_root, parsed.name)}", - "state": "published" if not failures else "failed", - "owned_paths": sorted(set(changed)), - "registry_status": "published" if registry_changed else "unchanged", - "metadata_status": "published" if str(metadata_path) in changed else "unchanged", + 'command': 'migrate-project', + 'status': 'issues-found', + 'failure_code': 'WB_METADATA_MIGRATION_COMMAND_RETIRED', + 'changed_files': [], + 'migration': { + 'single_workspace': 'wb.py migrate-control-plane <workspace-root> (--dry-run|--apply --accepted-proposal-id <id>)', + 'registered_workspaces': 'wb.py migrate-registered-projects (--dry-run|--apply --accepted-plan-id <id>)', }, }) - return 0 if not failures else 1 + return 1 def cmd_doctor_project(args: list[str]) -> int: @@ -2788,53 +1383,24 @@ def cmd_doctor_project(args: list[str]) -> int: parser.add_argument('--force', action='store_true') parser.add_argument('--repair', action='store_true') parsed = parser.parse_args(args) - project_root = Path(parsed.project_root).expanduser().resolve() - if _yaml_scalar(read(project_root / ".work-bundle/project.yaml"), "metadata_version") == "4": + selected_root = Path(parsed.project_root).expanduser().resolve() + try: + context = resolve_anchor_context( + workspace_root=selected_root if (selected_root / ".work-bundle/project.yaml").is_file() else None, + project_root=selected_root if not (selected_root / ".work-bundle/project.yaml").is_file() else None, + cwd=selected_root, + ) + except InfrastructureError as exc: + out({'command': 'doctor-project', 'status': 'issues-found', 'failure_code': exc.code, 'changed_files': []}) + return 1 + workspace_root = context.workspace_root + version = _yaml_scalar(read(workspace_root / ".work-bundle/project.yaml"), "metadata_version") + if version == "4": from control_plane import cmd_doctor_workspace - routed = [str(project_root)] + (["--repair"] if parsed.repair else []) + routed = [str(workspace_root)] + (["--repair"] if parsed.repair else []) return cmd_doctor_workspace(routed, command_name="doctor-project") - changed: list[str] = [] - agents_result: dict[str, object] = { - 'agents_status': 'not-run', - 'template_checksum_sha256': '', - 'changed_files': [], - 'warnings': [], - 'failures': [], - 'dry_run': False, - } - if parsed.repair: - try: - changed, agents_result = repair_project(project_root, force=parsed.force, return_details=True) - except ReferenceAssetError as exc: - out(_reference_failure_payload(exc, 'doctor-project')) - return 1 - data = inspect_project(project_root) - failures = project_failures(data, strict=parsed.repair, include_roles=True) - data.update({ - 'command': 'doctor-project', - 'status': 'passed' if not failures else 'issues-found', - 'failures': failures, - 'changed_files': sorted(set(changed)), - 'agents_status': agents_result.get('agents_status'), - 'agents_sync': _agents_sync_output(agents_result), - 'mode': _yaml_scalar(read(project_root / '.work-bundle/project.yaml'), 'workspace_mode') or 'single-repository-compatibility', - 'dry_run': not parsed.repair, - 'git_actions': [], - 'finding_classification': { - 'repairable': [item for item in failures if item in {'project_gitignore', 'project_ignores_work_bundle', 'project_ignores_agents', 'agents_md', 'work_bundle', 'work_bundle_gitignore', 'knowledge_root', 'orchestration_root', 'rules_root', 'rule_index'}], - 'advisory': [item for item in failures if item.endswith('baseline_status_stale')], - 'blocking': [item for item in failures if item not in {'project_gitignore', 'project_ignores_work_bundle', 'project_ignores_agents', 'agents_md', 'work_bundle', 'work_bundle_gitignore', 'knowledge_root', 'orchestration_root', 'rules_root', 'rule_index'} and not item.endswith('baseline_status_stale')], - }, - 'transaction': { - 'id': f"doctor-{_slug_from_root(project_root)}", - 'state': 'published' if parsed.repair and not failures else ('failed' if parsed.repair else 'proposed'), - 'owned_paths': sorted(set(changed)), - 'registry_status': 'unchanged', - 'metadata_status': 'published' if str(project_root / '.work-bundle/project.yaml') in changed else 'unchanged', - }, - }) - out(data) - return 0 if not failures else 1 + out({'command': 'doctor-project', 'status': 'issues-found', 'failure_code': 'WB_METADATA_MIGRATION_REQUIRED', 'metadata_version': version, 'changed_files': []}) + return 1 def cmd_project(args: list[str], apply: bool = False, inspect_only: bool = False, repo_model: bool = False) -> int: diff --git a/scripts/work-bundle/registry_layout.py b/scripts/work-bundle/registry_layout.py index bf19853..aefd580 100644 --- a/scripts/work-bundle/registry_layout.py +++ b/scripts/work-bundle/registry_layout.py @@ -18,6 +18,12 @@ write, ) from workspace_resources import _load_yaml +from infrastructure import ( + InfrastructureError, + dump_canonical_yaml, + parse_yaml_mapping, + validate_infrastructure_document, +) CATALOG_REFERENCE = Path('references/wb-registry-layout-migration.yaml') @@ -112,16 +118,33 @@ def _normalize_version(value: object) -> str: return text -def detect_registry_schema_version(text: str, catalog: MigrationCatalog) -> str: - for line in text.splitlines(): - if line.startswith('registry_schema_version:'): - return str(line.split(':', 1)[1].strip().strip('"').strip("'")) - return catalog.registry_schema_implicit +def _parse_registry_document(text: str) -> dict[str, object]: + try: + return parse_yaml_mapping(text, source='project registry') + except InfrastructureError as exc: + raise RegistryLayoutError(exc.code, details=exc.details) from exc + + +def _validate_registry_document(document: dict[str, object]) -> dict[str, object]: + try: + return validate_infrastructure_document(document, family='project-registry') + except InfrastructureError as exc: + raise RegistryLayoutError(exc.code, details=exc.details) from exc + + +def detect_registry_schema_version( + document: dict[str, object], catalog: MigrationCatalog +) -> str: + declared = document.get('registry_schema_version') + return str(declared) if declared is not None else catalog.registry_schema_implicit def detect_layout_version(metadata_text: str) -> str: - from project import _yaml_scalar - return _normalize_version(_yaml_scalar(metadata_text, 'metadata_version')) + try: + document = parse_yaml_mapping(metadata_text, source='workspace project metadata') + except InfrastructureError as exc: + raise RegistryLayoutError(exc.code, details=exc.details) from exc + return _normalize_version(document.get('metadata_version')) def migration_path( @@ -252,59 +275,33 @@ def restore_workspace(snapshot: dict[str, object]) -> None: _remove_created_credential_store(workspace_root) -def _project_block_bounds(lines: list[str], slug: str) -> tuple[int, int] | None: - start: int | None = None - for index, line in enumerate(lines): - if not line.startswith(' - slug:'): - continue - value = line.split(':', 1)[1].strip().strip('"').strip("'") - if value == slug: - start = index - break - if start is None: - return None - end = start + 1 - while end < len(lines): - line = lines[end] - if line.startswith(' - '): - break - if line and not line.startswith((' ', '#')): - break - end += 1 - return start, end - - def set_entry_layout_version(text: str, slug: str, version: str) -> str: - lines = text.splitlines() - bounds = _project_block_bounds(lines, slug) - if bounds is None: + document = _validate_registry_document(_parse_registry_document(text)) + projects = document.get('projects') + if not isinstance(projects, list): raise RegistryLayoutError( 'WB_REGISTRY_LAYOUT_ENTRY_MISSING', slug=slug, to_version=version, failed_step='registry-publication', ) - start, end = bounds - field = f' layout_version: {version}' - replaced = False - for index in range(start, end): - if lines[index].startswith(' layout_version:'): - lines[index] = field - replaced = True - break - if not replaced: - insert_at = start + 1 - for index in range(start, end): - if lines[index].startswith(' status:'): - insert_at = index - break - lines.insert(insert_at, field) - return '\n'.join(lines).rstrip() + '\n' + match = next( + (item for item in projects if isinstance(item, dict) and str(item.get('slug') or '') == slug), + None, + ) + if match is None: + raise RegistryLayoutError( + 'WB_REGISTRY_LAYOUT_ENTRY_MISSING', slug=slug, to_version=version, + failed_step='registry-publication', + ) + match['layout_version'] = int(version) if version.isdigit() else version + return dump_canonical_yaml(_validate_registry_document(document)) def ensure_registry_schema_version(text: str, version: str) -> str: - from project import _ensure_registry_schema_version - return _ensure_registry_schema_version(text, version) + document = _validate_registry_document(_parse_registry_document(text)) + document['registry_schema_version'] = int(version) if version.isdigit() else version + return dump_canonical_yaml(_validate_registry_document(document)) def classify_registered_project( @@ -344,6 +341,14 @@ def classify_registered_project( return result metadata_text = read(metadata_path) layout_version = detect_layout_version(metadata_text) + if layout_version == catalog.layout_current: + try: + validate_infrastructure_document( + parse_yaml_mapping(metadata_text, source=str(metadata_path)), + family='workspace-project-metadata', + ) + except InfrastructureError as exc: + raise RegistryLayoutError(exc.code, slug=slug, details=exc.details) from exc result['layout_version'] = layout_version result['workspace_root'] = str(workspace_root.resolve()) if workspace_root.exists() else str(workspace_root) result['metadata_digest'] = _metadata_digest(metadata_text) @@ -391,7 +396,6 @@ def _classify_blockers( path: list[LayoutMigrationStep], ) -> list[dict[str, str]]: blockers: list[dict[str, str]] = [] - from project import assess_legacy_topology from control_plane import ( ControlPlaneError, _proposal, @@ -399,16 +403,7 @@ def _classify_blockers( _source_tracks_control_plane, ) for step in path: - if step.step_id == 'layout-v2-to-v3': - topology = assess_legacy_topology(workspace_root, metadata_text, entry) - failure = str(topology.get('failure_code') or '') - if failure: - blockers.append({ - 'code': failure, - 'step': step.step_id, - 'required_command': str(topology.get('required_command') or ''), - }) - elif step.step_id == 'layout-v3-to-v4': + if step.step_id in {'layout-v2-to-v4', 'layout-v3-to-v4'}: protected = _protected_tracked_paths(workspace_root) if protected: blockers.append({ @@ -443,14 +438,16 @@ def inspect_registered_projects( if not registry_path.is_file(): raise RegistryLayoutError('WB_REGISTRY_LAYOUT_REGISTRY_MISSING', details={'path': str(registry_path)}) registry_text = read(registry_path) - registry_schema_version = detect_registry_schema_version(registry_text, catalog) + registry_document = _parse_registry_document(registry_text) + registry_schema_version = detect_registry_schema_version(registry_document, catalog) if registry_schema_version not in catalog.registry_schema_supported: raise RegistryLayoutError( 'WB_REGISTRY_SCHEMA_UNSUPPORTED', details={'registry_schema_version': registry_schema_version}, ) - from project import _project_blocks - entries = _project_blocks(registry_path) + registry_document = _validate_registry_document(registry_document) + from project import _project_blocks_from_document + entries = _project_blocks_from_document(registry_document) or [] projects = [ classify_registered_project(entry, catalog, registry_schema_version=registry_schema_version) for entry in sorted(entries, key=lambda item: str(item.get('slug') or '')) @@ -496,16 +493,9 @@ def apply_layout_step( workspace_root: Path, entry: dict[str, object], ) -> dict[str, object]: - if step.step_id == 'layout-v2-to-v3': - from project import apply_layout_v2_to_v3 - return apply_layout_v2_to_v3( - workspace_root, - name=str(entry.get('name') or entry.get('slug') or ''), - registry_entry_data=entry, - ) - if step.step_id == 'layout-v3-to-v4': - from control_plane import apply_layout_v3_to_v4 - return apply_layout_v3_to_v4(workspace_root) + if step.step_id in {'layout-v2-to-v4', 'layout-v3-to-v4'}: + from control_plane import apply_historical_layout_to_v4 + return apply_historical_layout_to_v4(workspace_root) raise RegistryLayoutError( 'WB_REGISTRY_LAYOUT_STEP_UNKNOWN', slug=str(entry.get('slug') or ''), @@ -781,7 +771,6 @@ def migrate_registered_projects( validate: Callable[[Path, str], list[str]] = validate_layout_version, ) -> dict[str, object]: catalog = load_migration_catalog() - from project import _project_blocks plan = inspect_registered_projects(slug=slug, catalog=catalog) payload: dict[str, object] = { 'command': 'migrate-registered-projects', @@ -805,9 +794,13 @@ def migrate_registered_projects( payload['failure_code'] = 'WB_REGISTRY_LAYOUT_PLAN_STALE' return payload registry_path = resolve_project_registry_path() + registry_document = _validate_registry_document( + _parse_registry_document(read(registry_path)) + ) + from project import _project_blocks_from_document entries = { str(entry.get('slug') or ''): entry - for entry in _project_blocks(registry_path) + for entry in (_project_blocks_from_document(registry_document) or []) } results: list[dict[str, object]] = [] changed: list[str] = [] diff --git a/scripts/work-bundle/reviewer_workspace.py b/scripts/work-bundle/reviewer_workspace.py index 7be1380..9482325 100644 --- a/scripts/work-bundle/reviewer_workspace.py +++ b/scripts/work-bundle/reviewer_workspace.py @@ -1,2158 +1,145 @@ +#!/usr/bin/env python3 +"""Structural admission for exact Stage 5 implementation-review candidates. + +This module owns no reviewer process, receipt, publication, round, or lifecycle +state. It checks only Git/source identity and reviewer independence. +""" + from __future__ import annotations -import argparse -import base64 -from datetime import datetime, timezone import hashlib -import importlib.util -import json -import os -from pathlib import Path -import platform import re -import shutil import subprocess -import sys -import uuid - - -SAFE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$") -SCOPES = frozenset({"source", "control"}) -NETWORK_STATES = frozenset({"denied"}) -VALIDATOR_KINDS = frozenset({"json", "sha256", "command"}) -TERMINAL_VERDICTS = frozenset({"accepted", "repair", "blocked"}) -NATIVE_DISABLED_FEATURES = ( - "shell_tool", "unified_exec", "code_mode", "code_mode_host", "apps", "hooks", - "browser_use", "browser_use_external", "browser_use_full_cdp_access", "computer_use", - "in_app_browser", "image_generation", "multi_agent", "view_image", "workspace_dependencies", - "tool_suggest", "skill_search", "sleep_tool", "goals", "memories", "remote_plugin", "recommended_plugins", -) -NATIVE_ISOLATION = { - "mechanism": "native-host-read-only", "network": "model-transport", - "write_scope": "read-only", "context": "fresh-native-host-context-with-explicit-evidence", - "host_skill_catalog": "may-be-present", - "tools": "disabled-and-no-observed-activity", "os_process_isolation": False, -} -NATIVE_REVIEW_REQUEST_MAX_CHARS = 1_048_576 - - -def _structured_evidence(content: str) -> dict[str, object] | None: - try: - value = json.loads(content) - except json.JSONDecodeError: - try: - value = _review_runtime().parse_yaml_subset(content) - except (SystemExit, ValueError, TypeError): - return None - return value if isinstance(value, dict) else None - - -def _integrated_change_manifest( - packet: dict[str, object], evidence: list[dict[str, object]], -) -> tuple[dict[str, object], set[str], dict[str, str] | None] | None: - context = packet.get("stage_review_context") - if not isinstance(context, dict) or context.get("stage") != "integrated_implementation": - return None - target = context.get("target_identity") - target_tree = target.get("source_tree") if isinstance(target, dict) else None - candidates: list[tuple[dict[str, object], set[str], dict[str, str] | None]] = [] - for item in evidence: - if not str(item.get("locator") or "").startswith("control:"): - continue - content = item.get("content") - if not isinstance(content, str): - continue - value = _structured_evidence(content) - if not isinstance(value, dict): - continue - baseline = value.get("baseline") - endpoint = value.get("endpoint") - comparison = value.get("comparison") - if not all(isinstance(part, dict) for part in (baseline, endpoint, comparison)): - continue - paths = comparison.get("paths") - if endpoint.get("tree") != target_tree or not isinstance(paths, list): - continue - exact_diff = comparison.get("exact_diff") - normalized_diff = None - if exact_diff is not None: - if ( - not isinstance(exact_diff, dict) - or set(exact_diff) != {"command", "locator", "sha256"} - or not str(exact_diff.get("locator") or "").startswith("control:") - or not re.fullmatch(r"[0-9a-f]{64}", str(exact_diff.get("sha256") or "")) - or not isinstance(exact_diff.get("command"), str) - ): - raise ReviewerWorkspaceError("WB_REVIEW_CHANGE_MANIFEST_INVALID") - normalized_diff = { - "command": exact_diff["command"], - "locator": exact_diff["locator"], - "sha256": exact_diff["sha256"], - } - locators: set[str] = set() - valid = True - for raw in paths: - if not isinstance(raw, dict) or set(raw) != {"status", "path"}: - valid = False - break - path = Path(str(raw.get("path") or "")) - if ( - raw.get("status") not in {"added", "modified", "deleted"} - or path.is_absolute() - or not path.parts - or ".." in path.parts - ): - valid = False - break - if raw["status"] != "deleted": - locators.add("source:" + path.as_posix()) - if valid: - candidates.append((value, locators, normalized_diff)) - if len(candidates) > 1: - raise ReviewerWorkspaceError("WB_REVIEW_CHANGE_MANIFEST_AMBIGUOUS") - return candidates[0] if candidates else None - - -def _stage_evidence_roles(packet: dict[str, object]) -> dict[str, str]: - manifest = packet.get("stage_evidence_manifest") - entries = manifest.get("entries") if isinstance(manifest, dict) else [] - return { - str(item.get("locator") or ""): str(item.get("role") or "") - for item in entries - if isinstance(item, dict) - } - - -def _controller_only_artifact_locators( - packet: dict[str, object], evidence: list[dict[str, object]], -) -> set[str]: - context = packet.get("stage_review_context") - if not isinstance(context, dict) or context.get("stage") != "integrated_implementation": - return set() - roles = _stage_evidence_roles(packet) - result: set[str] = set() - for item in evidence: - locator = str(item.get("locator") or "") - if roles.get(locator) in {"accepted_task_result", "validation_observation"}: - result.add(locator) - continue - if "/handoff/" in locator: - result.add(locator) - continue - content = item.get("content") - value = _structured_evidence(content) if isinstance(content, str) else None - schema = str(value.get("schema") or "") if isinstance(value, dict) else "" - if ( - isinstance(value, dict) - and ( - value.get("type") == "executor-result" - or "lifecycle" in schema - or {"execution_id", "ownership", "accepted_result"}.issubset(value) - ) - ): - result.add(locator) - return result - - -def _integrated_product_evidence( - packet: dict[str, object], evidence: list[dict[str, object]], -) -> dict[str, object]: - roles = _stage_evidence_roles(packet) - accepted_results: list[dict[str, object]] = [] - accepted_observation_ids: set[str] = set() - observation_stores: list[dict[str, object]] = [] - unresolved: list[object] = [] - for item in evidence: - locator = str(item.get("locator") or "") - content = item.get("content") - value = _structured_evidence(content) if isinstance(content, str) else None - if not isinstance(value, dict): - continue - if roles.get(locator) == "accepted_task_result": - accepted = value.get("accepted_result") - if not isinstance(accepted, dict): - continue - ids = [str(value) for value in accepted.get("validation_evidence_ids", [])] - accepted_observation_ids.update(ids) - accepted_results.append( - { - "task_id": accepted.get("task_id"), - "accepted_source": accepted.get("accepted_source"), - "validation_evidence_ids": ids, - "review_id": accepted.get("review_id"), - "invalidation": accepted.get("invalidation"), - } - ) - elif roles.get(locator) == "validation_observation": - observation_stores.append(value) - elif locator not in _controller_only_artifact_locators(packet, [item]): - unresolved.extend(value.get("unresolved", []) if isinstance(value.get("unresolved"), list) else []) - observations = [] - for store in observation_stores: - for item in store.get("observations", []): - if not isinstance(item, dict) or item.get("observation_id") not in accepted_observation_ids: - continue - result = item.get("result") if isinstance(item.get("result"), dict) else {} - observations.append( - { - "observation_id": item.get("observation_id"), - "product_tree": item.get("product_tree"), - "command_digest": item.get("command_digest"), - "oracle_digest": item.get("oracle_digest"), - "result": { - key: result.get(key) - for key in ( - "exit_code", "stdout_digest", "stderr_digest", - "started_at", "completed_at", - ) - }, - } - ) - return { - "accepted_results": sorted(accepted_results, key=lambda item: str(item.get("task_id") or "")), - "validation_observations": sorted( - observations, key=lambda item: str(item.get("observation_id") or "") - ), - "unresolved_product_concerns": unresolved, - } +from pathlib import Path +from typing import Any, Mapping -def _native_review_artifacts( - packet: dict[str, object], evidence: list[dict[str, object]], -) -> list[dict[str, object]]: - manifest = _integrated_change_manifest(packet, evidence) - artifacts = packet.get("artifacts") - if not isinstance(artifacts, list) or manifest is None: - return [item for item in artifacts or [] if isinstance(item, dict)] - _, changed, exact_diff = manifest - if exact_diff is not None: - matching = [ - item - for item in artifacts - if isinstance(item, dict) and item.get("locator") == exact_diff["locator"] - ] - if len(matching) != 1 or matching[0].get("sha256") != exact_diff["sha256"]: - raise ReviewerWorkspaceError("WB_REVIEW_CHANGE_MANIFEST_INVALID") - changed = set() - controller_only = _controller_only_artifact_locators(packet, evidence) - return [ - item - for item in artifacts - if isinstance(item, dict) - and item.get("locator") not in controller_only - and ( - not str(item.get("locator") or "").startswith("source:") - or item.get("locator") in changed - ) - ] +class ReviewerWorkspaceError(ValueError): + pass -def _validate_integrated_change_manifest( - source_root: Path, packet: dict[str, object], evidence: list[dict[str, object]], -) -> None: - selected = _integrated_change_manifest(packet, evidence) - if selected is None: - return - manifest, changed, exact_diff = selected - baseline = manifest["baseline"] - endpoint = manifest["endpoint"] - comparison = manifest["comparison"] - baseline_head = str(baseline.get("head") or "") - endpoint_head = str(endpoint.get("head") or "") - expected_command = f"git diff --name-status {baseline_head}..{endpoint_head}" - if comparison.get("command") != expected_command: - raise ReviewerWorkspaceError("WB_REVIEW_CHANGE_MANIFEST_INVALID") - head = subprocess.run( - ["git", "-C", str(source_root), "rev-parse", "HEAD"], capture_output=True, text=True - ) - tree = subprocess.run( - ["git", "-C", str(source_root), "rev-parse", f"{endpoint_head}^{{tree}}"], - capture_output=True, - text=True, - ) - ancestor = subprocess.run( - ["git", "-C", str(source_root), "merge-base", "--is-ancestor", baseline_head, endpoint_head], - capture_output=True, - ) - diff = subprocess.run( - ["git", "-C", str(source_root), "diff", "--name-status", baseline_head, endpoint_head], - capture_output=True, - text=True, - ) - binary_diff = subprocess.run( - ["git", "-C", str(source_root), "diff", "--binary", baseline_head, endpoint_head], - capture_output=True, +def _git(root: Path, *args: str, binary: bool = False) -> bytes | str: + completed = subprocess.run( + ["git", "-C", str(root), *args], capture_output=True, + text=not binary, check=False, ) - status_names = {"A": "added", "M": "modified", "D": "deleted"} - actual: list[dict[str, str]] = [] - if not diff.returncode: - for row in diff.stdout.splitlines(): - columns = row.split("\t") - if len(columns) != 2 or columns[0] not in status_names: - raise ReviewerWorkspaceError("WB_REVIEW_CHANGE_MANIFEST_INVALID") - actual.append({"status": status_names[columns[0]], "path": columns[1]}) - paths = comparison.get("paths") - packet_locators = { - str(item.get("locator") or "") - for item in packet.get("artifacts", []) - if isinstance(item, dict) - } - exact_diff_valid = True - if exact_diff is not None: - supplied = [item for item in evidence if item.get("locator") == exact_diff["locator"]] - expected_binary_command = f"git diff --binary {baseline_head}..{endpoint_head}" - exact_diff_valid = ( - len(supplied) == 1 - and exact_diff["command"] == expected_binary_command - and not binary_diff.returncode - and supplied[0].get("sha256") == exact_diff["sha256"] - and hashlib.sha256(binary_diff.stdout).hexdigest() == exact_diff["sha256"] - and str(supplied[0].get("content") or "").encode("utf-8") == binary_diff.stdout - ) - if ( - head.returncode - or head.stdout.strip() != endpoint_head - or tree.returncode - or tree.stdout.strip() != endpoint.get("tree") - or ancestor.returncode - or diff.returncode - or comparison.get("path_count") != len(actual) - or paths != actual - or not changed.issubset(packet_locators) - or not exact_diff_valid - ): - raise ReviewerWorkspaceError("WB_REVIEW_CHANGE_MANIFEST_INVALID") - - -def _stderr_reports_forbidden_native_activity(stderr: str) -> bool: - """Recognize protocol evidence on stderr without interpreting free-form logs.""" - for line in stderr.splitlines(): - try: - event = json.loads(line) - except json.JSONDecodeError: - continue - if not isinstance(event, dict): - continue - kind = event.get("type") - if kind in {"turn.failed", "turn.cancelled", "error"}: - return True - if kind == "item.completed": - item = event.get("item") - if not isinstance(item, dict) or item.get("type") not in {"agent_message", "reasoning"}: - return True - return False - - -def parse_native_reviewer_transcript(raw: str, stderr: str = "") -> tuple[str, dict[str, object]]: - """Accept one completed fresh host turn, never a supplied verdict or tool run.""" - thread_id = None - phase = "new" - judgment = None - model_activity = False - try: - if _stderr_reports_forbidden_native_activity(stderr): - raise ValueError("stderr contains forbidden structured activity") - for line in raw.splitlines(): - event = json.loads(line) - kind = event["type"] - if kind == "thread.started" and phase == "new": - thread_id = str(uuid.UUID(event["thread_id"])) - phase = "ready" - elif kind == "turn.started" and phase == "ready": - phase = "running" - elif kind == "item.completed" and phase in {"ready", "running"}: - item = event["item"] - if phase == "ready" and item["type"] == "error" and ( - str(item.get("message", "")).startswith("Under-development features enabled: skip_host_skill_discovery.") - or str(item.get("message", "")).startswith("Code Mode is unavailable because code-mode host is disabled.") - ): - continue - if (phase == "running" and not model_activity and item["type"] == "error" - and item.get("message") == "Skill descriptions were shortened to fit the skills context budget. Codex can still see every skill, but some descriptions are shorter. Disable unused skills or plugins to leave more room for the rest."): - # Observed host initialization notice, not an attempted tool - # or model failure. Native catalog metadata may be present. - continue - if phase != "running" or item["type"] not in {"agent_message", "reasoning"}: - raise ValueError("unexpected host activity") - model_activity = True - if item["type"] == "agent_message": - if judgment is not None: - raise ValueError("substantive judgment must be terminal") - try: - candidate = json.loads(item["text"]) - except json.JSONDecodeError: - continue - if not isinstance(candidate, dict): - raise ValueError("judgment must be an object") - judgment = candidate - elif judgment is not None: - raise ValueError("substantive judgment must be terminal") - elif kind == "turn.completed" and phase == "running" and judgment is not None: - phase = "complete" - else: - raise ValueError("unexpected host activity") - if phase != "complete" or not thread_id: - raise ValueError("incomplete host run") - return thread_id, judgment - except (ValueError, TypeError, KeyError, AttributeError) as error: - raise ReviewerWorkspaceError("WB_REVIEW_NATIVE_TRANSCRIPT_INVALID") from error - - -def _native_reviewer_argv(executable: Path, workspace: Path, model: str) -> list[str]: - return [str(executable), "exec", "--ignore-user-config", "--sandbox", "read-only", "--ephemeral", - "--json", "--skip-git-repo-check", "-C", str(workspace), "-m", model, - "-c", 'model_reasoning_effort="medium"', "-c", "project_doc_max_bytes=0", - "-c", 'web_search="disabled"', "--enable", "skip_host_skill_discovery", - *[part for feature in NATIVE_DISABLED_FEATURES for part in ("--disable", feature)], "-"] - - -def _run_native_process(workspace: Path, argv: list[str], request: str) -> subprocess.CompletedProcess[str]: - # Desktop transport/session variables would reconnect the reviewer to author - # capabilities even when native user config is suppressed. Never inherit them. - environment = {"PATH": "/usr/bin:/bin:/usr/sbin:/sbin", "HOME": str(Path.home()), - "TMPDIR": str(workspace / "scratch")} - if os.environ.get("CODEX_HOME"): - environment["CODEX_HOME"] = os.environ["CODEX_HOME"] - return subprocess.run(argv, cwd=workspace, env=environment, input=request, text=True, - capture_output=True, check=False, timeout=1800) - + if completed.returncode: + detail = completed.stderr if isinstance(completed.stderr, str) else completed.stderr.decode("utf-8", "replace") + raise ReviewerWorkspaceError(f"WB_REVIEW_GIT_IDENTITY_INVALID: {detail.strip()}") + return completed.stdout -def _retain_native_diagnostics( - runtime_root, run_id, review_id, argv, request_bytes, executable_digest, completed, - *, started_at, completed_at, packet, state, sealed, native_controller_evidence, - executable_unchanged, -): - """Freeze one complete native run before admission; this is never a receipt.""" - directory = runtime_root / "diagnostics/reviewer-native" / run_id - if not _inside(runtime_root, directory): - raise ReviewerWorkspaceError("WB_REVIEW_RUNTIME_PATH_ESCAPE") - directory.mkdir(parents=True, exist_ok=False) - items = { - "request.json": request_bytes, - "stdout.jsonl": completed.stdout.encode(), - "stderr.txt": completed.stderr.encode(), - "launch.json": json.dumps({"argv": argv, "executable_sha256": executable_digest}, sort_keys=True).encode(), - "packet.json": json.dumps(packet, sort_keys=True).encode(), - "events.jsonl": Path(str(sealed["event_log_path"])).read_bytes(), - "control.json": json.dumps({ - key: state.get(key) - for key in ( - "task_review_previous_review", "stage_review_previous_review", - "post_execution_review", - ) - if state.get(key) is not None - }, sort_keys=True, ensure_ascii=False).encode(), - } - if native_controller_evidence is not None: - items["controller.json"] = json.dumps( - native_controller_evidence, sort_keys=True, ensure_ascii=False - ).encode() - items["capture.json"] = json.dumps({ - "schema": "reviewer-native-capture-v2", "status": "captured-unadmitted", - "run_id": run_id, "review_id": review_id, "exit_code": completed.returncode, - "started_at": started_at, "completed_at": completed_at, - "packet_sha256": _canonical_digest(packet), - "event_log_sha256": sealed["event_log_sha256"], - "event_log_mode": sealed["event_log_mode"], - "executable_unchanged": executable_unchanged, - "artifacts": {name: _sha256_bytes(content) for name, content in items.items()}, - }, sort_keys=True).encode() - for name, content in items.items(): - target = directory / name - with target.open("xb") as stream: - stream.write(content) - target.chmod(0o400) - return str(directory) - - -def _native_review_input( - packet: dict[str, object], evidence: list[dict[str, object]] | None = None, -) -> dict[str, object]: - key = "task_review_context" if "task_review_context" in packet else "stage_review_context" - context = packet[key] - artifacts = packet["artifacts"] - integrated = key == "stage_review_context" and context.get("stage") == "integrated_implementation" - if integrated and evidence is not None: - controller_only = _controller_only_artifact_locators(packet, evidence) - artifacts = [ - item - for item in artifacts - if isinstance(item, dict) and item.get("locator") not in controller_only - ] - result = { - "target_identity": context["target_identity"], - "artifacts": artifacts, - "product_evidence": _integrated_product_evidence(packet, evidence), - } - if context.get("repair_frontier") is not None: - result["repair_frontier"] = context["repair_frontier"] - return result - if key == "task_review_context" or context.get("stage") == "integrated_implementation": - return {"target_identity": context["target_identity"], "artifacts": artifacts} - result = { - "stage": context["stage"], - "target_identity": context["target_identity"], - "artifacts": artifacts, - } - if context.get("repair_frontier") is not None: - result["repair_frontier"] = context["repair_frontier"] - return result +def _commit(root: Path, value: object) -> str: + raw = str(value or "") + if not re.fullmatch(r"[0-9a-f]{40}", raw): + raise ReviewerWorkspaceError("WB_REVIEW_CURRENT_TARGET_INVALID") + resolved = str(_git(root, "rev-parse", f"{raw}^{{commit}}")).strip() + if resolved != raw: + raise ReviewerWorkspaceError("WB_REVIEW_CURRENT_TARGET_INVALID") + return resolved -def run_native_reviewer(workspace: Path, executable: Path, *, model: str, review_instructions: str) -> dict[str, object]: - """Run a fresh native host judgment over explicit evidence; no plugin required. - Native read-only policy is not the legacy OS process sandbox. The host may - use its authentication/model transport; no author thread transport propagates, - and any observed tool activity makes the result inadmissible. - """ - executable = executable.expanduser().resolve() - workspace = workspace.expanduser().resolve() - if not executable.is_file() or not os.access(executable, os.X_OK): - raise ReviewerWorkspaceError( - "WB_REVIEW_NATIVE_CAPABILITY_UNAVAILABLE", - {"capability": "native_reviewer_executable"}, - ) - if not model: - raise ReviewerWorkspaceError( - "WB_REVIEW_NATIVE_CAPABILITY_UNAVAILABLE", - {"capability": "native_reviewer_model"}, - ) - if not review_instructions.strip(): - raise ReviewerWorkspaceError("WB_REVIEW_COMMAND_INVALID") - packet, state = _load_workspace(workspace) - if not ("stage_review_context" in packet or "task_review_context" in packet): - raise ReviewerWorkspaceError("WB_REVIEW_NATIVE_CONTEXT_REQUIRED") - round_state = state.get("post_execution_review") - context = packet.get("stage_review_context") or packet.get("task_review_context") - authority_value = state.get("admission_workspace") - authority = Path(str(authority_value)) if isinstance(authority_value, str) else None - try: - if authority is not None: - _bounded_closure().require_orchestration_admission( - authority, - operation="round_completion" if isinstance(round_state, dict) else "ordinary_new", - flow_id=(str(round_state["flow_id"]) if isinstance(round_state, dict) - else str(context["target_identity"].get("artifact_id"))), - ) - except _bounded_closure().BoundedClosureError as error: - raise ReviewerWorkspaceError(error.code, {"detail": error.detail or str(error)}) from error - control_evidence = [] - for item in packet["artifacts"]: - if str(item.get("locator") or "").startswith("control:"): - content = _evidence_path(workspace, item["locator"]).read_bytes().decode("utf-8") - control_evidence.append({**item, "content": content}) - controller_locators = _controller_only_artifact_locators(packet, control_evidence) - controller_evidence = [ - item for item in control_evidence if item.get("locator") in controller_locators - ] - selected_artifacts = _native_review_artifacts(packet, control_evidence) - evidence = [] - for item in selected_artifacts: - # Text-mode reads normalize CRLF. The model input must preserve the exact - # frozen bytes whose digest will be revalidated during publication. - content = _evidence_path(workspace, item["locator"]).read_bytes().decode("utf-8") - if _sha256_bytes(content.encode("utf-8")) != item["sha256"]: - raise ReviewerWorkspaceError("WB_REVIEW_EVIDENCE_MUTATED") - evidence.append({**item, "content": content}) - request = { - "instructions": review_instructions, - "review_input": _native_review_input(packet, control_evidence), - "evidence": evidence, - } - if len(json.dumps(request, sort_keys=True, ensure_ascii=False)) > NATIVE_REVIEW_REQUEST_MAX_CHARS: - raise ReviewerWorkspaceError( - "WB_REVIEW_NATIVE_INPUT_TOO_LARGE", - {"max_chars": NATIVE_REVIEW_REQUEST_MAX_CHARS}, +def _manifest_bytes(manifest: list[dict[str, str]]) -> bytes: + return "".join( + ( + f"present {item['sha256']} {item['path']}\n" + if item["state"] == "present" + else f"deleted - {item['path']}\n" ) - argv = _native_reviewer_argv(executable, workspace, model) - return _run_reviewer( - workspace, argv, native_request=request, - native_controller_evidence=controller_evidence, - ) - - -def _review_runtime(): - orchestration = Path(__file__).resolve().parents[1] / "orchestration" - existing = sys.modules.get("review_runtime") - if existing is not None: - if Path(existing.__file__).resolve() != orchestration / "review_runtime.py": - raise ReviewerWorkspaceError("WB_REVIEW_RUNTIME_MODULE_COLLISION") - return existing - spec = importlib.util.spec_from_file_location("review_runtime", orchestration / "review_runtime.py") - module = importlib.util.module_from_spec(spec) - original_path = list(sys.path) - try: - sys.path.insert(0, str(orchestration)) - sys.modules["review_runtime"] = module - spec.loader.exec_module(module) - except BaseException: - sys.modules.pop("review_runtime", None) - raise - finally: - sys.path[:] = original_path - return module - - -def _bounded_closure(): - orchestration = Path(__file__).resolve().parents[1] / "orchestration" - existing = sys.modules.get("bounded_closure") - if existing is not None: - if Path(existing.__file__).resolve() != orchestration / "bounded_closure.py": - raise ReviewerWorkspaceError("WB_REVIEW_RUNTIME_MODULE_COLLISION") - return existing - spec = importlib.util.spec_from_file_location( - "bounded_closure", orchestration / "bounded_closure.py" - ) - module = importlib.util.module_from_spec(spec) - sys.modules["bounded_closure"] = module - try: - spec.loader.exec_module(module) - except BaseException: - sys.modules.pop("bounded_closure", None) - raise - return module - - -def _validate_stage_context( - context: object, *, require_re_review_predecessor: bool = False, -) -> dict[str, object]: - fields = {"stage", "target_identity", "target_locator", "agent_id", "capability", "execution_id", "evidence_mode"} - repair_fields = {"review_mode", "review_target_kind", "repair_frontier", "review_reset"} - allowed = { - frozenset(fields), - frozenset(fields | repair_fields), - frozenset(fields | repair_fields | {"previous_review"}), - } - if not isinstance(context, dict) or frozenset(context) not in allowed: - raise ReviewerWorkspaceError("WB_REVIEW_STAGE_CONTEXT_INVALID") - if (context["stage"] not in {"specification", "plan", "integrated_implementation"} - or context["capability"] not in {"standard", "judgment"} - or context["evidence_mode"] not in {"direct_source", "reproducible_snapshot", "packet_only"} - or not all(isinstance(context[key], str) and context[key] for key in ("agent_id", "execution_id"))): - raise ReviewerWorkspaceError("WB_REVIEW_STAGE_CONTEXT_INVALID") - _review_runtime()._target_identity(context["target_identity"]) - if repair_fields.issubset(context): - try: - mode = _review_runtime()._enum(context["review_mode"], _review_runtime().REVIEW_MODES, "review_mode") - _review_runtime()._enum(context["review_target_kind"], _review_runtime().REVIEW_TARGET_KINDS, "review_target_kind") - if mode == "repair": - _review_runtime()._repair_frontier(context["repair_frontier"]) - if context["review_reset"] is not None: - raise ValueError("repair reset") - elif context["repair_frontier"] is not None: - raise ValueError("initial frontier") - re_review = mode == "repair" or context["review_reset"] is not None - if "previous_review" in context: - previous = context.get("previous_review") - if not isinstance(previous, dict) or "previous_review" in previous: - raise ValueError("re-review predecessor") - if require_re_review_predecessor and re_review and "previous_review" not in context: - raise ValueError("missing re-review predecessor") - if not re_review and "previous_review" in context: - raise ValueError("unexpected predecessor") - except (ValueError, TypeError): - raise ReviewerWorkspaceError("WB_REVIEW_STAGE_CONTEXT_INVALID") from None - return context - - -def _validate_task_context(context: object) -> dict[str, object]: - fields = { - "target_identity", "agent_id", "capability", "execution_id", "evidence_mode", - "review_mode", "review_target_kind", "repair_frontier", "review_reset", - } - allowed = {frozenset(fields), frozenset(fields | {"previous_review"})} - if not isinstance(context, dict) or frozenset(context) not in allowed: - raise ReviewerWorkspaceError("WB_REVIEW_TASK_CONTEXT_INVALID") - if ( - context["review_target_kind"] != "task" - or context["capability"] not in {"standard", "judgment"} - or context["evidence_mode"] not in {"direct_source", "reproducible_snapshot", "packet_only"} - or not all(isinstance(context[key], str) and context[key] for key in ("agent_id", "execution_id")) - ): - raise ReviewerWorkspaceError("WB_REVIEW_TASK_CONTEXT_INVALID") - _review_runtime()._target_identity(context["target_identity"]) - try: - mode = _review_runtime()._enum(context["review_mode"], _review_runtime().REVIEW_MODES, "review_mode") - if mode == "repair": - _review_runtime()._repair_frontier(context["repair_frontier"]) - if context["review_reset"] is not None: - raise ValueError("repair reset") - elif context["repair_frontier"] is not None: - raise ValueError("initial frontier") - except (ValueError, TypeError): - raise ReviewerWorkspaceError("WB_REVIEW_TASK_CONTEXT_INVALID") from None - return context - - -def _validate_task_source_identity(source_root: Path, context: dict[str, object]) -> None: - identity = context["target_identity"] - assert isinstance(identity, dict) - head = subprocess.run( - ["git", "-C", str(source_root), "rev-parse", "HEAD"], capture_output=True, text=True - ) - tree = subprocess.run( - ["git", "-C", str(source_root), "rev-parse", "HEAD^{tree}"], capture_output=True, text=True - ) - status = subprocess.run( - ["git", "-C", str(source_root), "status", "--porcelain=v1"], capture_output=True, text=True - ) - if ( - head.returncode or tree.returncode or status.returncode or status.stdout - or head.stdout.strip() != identity.get("revision") - or tree.stdout.strip() != identity.get("source_tree") - ): - raise ReviewerWorkspaceError("WB_REVIEW_TASK_TARGET_MISMATCH") - - -class ReviewerWorkspaceError(RuntimeError): - def __init__(self, code: str, result: dict[str, object] | None = None) -> None: - super().__init__(code) - self.code = code - self.result = result or {} - - -def _sha256_bytes(value: bytes) -> str: - return hashlib.sha256(value).hexdigest() - - -def _canonical_digest(value: object) -> str: - encoded = json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8") - return _sha256_bytes(encoded) - - -def _safe_id(value: str) -> str: - if not SAFE_ID.fullmatch(value): - raise ReviewerWorkspaceError("WB_REVIEW_ID_INVALID") - return value - - -def _split_locator(locator: object) -> tuple[str, Path]: - text = str(locator or "") - scope, separator, raw_path = text.partition(":") - if not separator or scope not in SCOPES: - if scope == "host": - raise ReviewerWorkspaceError( - "WB_REVIEW_HOST_CONFIG_READ_DENIED", {"classification": "denied", "scope": "host"} - ) - raise ReviewerWorkspaceError("WB_REVIEW_LOCATOR_INVALID", {"classification": "denied"}) - relative = Path(raw_path) - if not raw_path or relative.is_absolute() or ".." in relative.parts or relative == Path("."): - raise ReviewerWorkspaceError("WB_REVIEW_PATH_ESCAPE_DENIED", {"classification": "denied"}) - return scope, relative - - -def _inside(root: Path, candidate: Path) -> bool: - resolved_root = root.resolve() - resolved_candidate = candidate.resolve(strict=False) - return resolved_candidate == resolved_root or resolved_root in resolved_candidate.parents - + for item in manifest + ).encode("utf-8") -def _normalized_roots(values: list[Path]) -> list[Path]: - roots: list[Path] = [] - for value in values: - root = Path(value).expanduser().resolve() - if not root.exists() or root.is_symlink(): - raise ReviewerWorkspaceError("WB_REVIEW_PROTECTED_ROOT_INVALID") - roots.append(root) - if not roots: - raise ReviewerWorkspaceError("WB_REVIEW_PROTECTED_ROOTS_REQUIRED") - return roots - -def _source_path( +def validate_current_candidate_and_independence( source_root: Path, - control_root: Path, - protected_roots: list[Path], - locator: object, -) -> tuple[str, Path, Path]: - scope, relative = _split_locator(locator) - root = source_root if scope == "source" else control_root - root = root.expanduser().resolve() - candidate = root / relative - if not _inside(root, candidate): - raise ReviewerWorkspaceError("WB_REVIEW_PATH_ESCAPE_DENIED", {"classification": "denied"}) - if any(_inside(protected, candidate) for protected in protected_roots): - raise ReviewerWorkspaceError( - "WB_REVIEW_PROTECTED_READ_DENIED", {"classification": "denied", "scope": scope} - ) - if not candidate.is_file() or candidate.is_symlink(): - raise ReviewerWorkspaceError("WB_REVIEW_DIRECT_EVIDENCE_MISSING", {"locator": f"{scope}:{relative.as_posix()}"}) - return scope, relative, candidate - - -def build_direct_evidence_packet( + target: object, *, - source_root: Path, - control_root: Path, - protected_roots: list[Path], - artifacts: list[str], - search_roots: list[str], - validators: list[dict[str, object]], - sentinels: list[str], - network_state: str, - stage_review_context: dict[str, object] | None = None, - task_review_context: dict[str, object] | None = None, -) -> dict[str, object]: - """Copy only named direct evidence into a location-free packet. - - Origin roots are used while compiling the packet and are deliberately not - serialized. Review operations therefore have no path capability back to - source, control-plane, registry, credentials, or host configuration. - """ - if network_state not in NETWORK_STATES: - raise ReviewerWorkspaceError("WB_REVIEW_NETWORK_STATE_REQUIRED") - source_root = source_root.expanduser().resolve() - control_root = control_root.expanduser().resolve() - protected = _normalized_roots(protected_roots) - records: list[dict[str, object]] = [] - seen: set[str] = set() - for locator in artifacts: - scope, relative, candidate = _source_path(source_root, control_root, protected, locator) - normalized = f"{scope}:{relative.as_posix()}" - if normalized in seen: - raise ReviewerWorkspaceError("WB_REVIEW_ARTIFACT_DUPLICATE", {"locator": normalized}) - seen.add(normalized) - content = candidate.read_bytes() - records.append( - { - "locator": normalized, - "sha256": _sha256_bytes(content), - "content_base64": base64.b64encode(content).decode("ascii"), - } - ) - normalized_search: list[str] = [] - for locator in search_roots: - scope, relative = _split_locator(locator) - root = source_root if scope == "source" else control_root - candidate = root / relative - if not _inside(root, candidate) or not candidate.is_dir() or candidate.is_symlink(): - raise ReviewerWorkspaceError("WB_REVIEW_SEARCH_ROOT_INVALID", {"locator": locator}) - if any(_inside(item, candidate) for item in protected): - raise ReviewerWorkspaceError("WB_REVIEW_PROTECTED_READ_DENIED", {"classification": "denied"}) - normalized_search.append(f"{scope}:{relative.as_posix()}") - normalized_validators: list[dict[str, object]] = [] - validator_ids: set[str] = set() - for raw in validators: - validator_id = _safe_id(str(raw.get("validator_id") or "")) - kind = str(raw.get("kind") or "") - locator = str(raw.get("artifact") or "") - argv = raw.get("argv") - command_valid = kind == "command" and isinstance(argv, list) and bool(argv) and all( - isinstance(item, str) and item for item in argv - ) - artifact_valid = kind in {"json", "sha256"} and locator in seen - if validator_id in validator_ids or kind not in VALIDATOR_KINDS or not (command_valid or artifact_valid): - raise ReviewerWorkspaceError("WB_REVIEW_VALIDATOR_INVALID", {"validator_id": validator_id}) - validator_ids.add(validator_id) - normalized = {"validator_id": validator_id, "kind": kind} - if command_valid: - normalized["argv"] = list(argv) + reviewer_agent_id: str, + implementor_agent_id: str, +) -> dict[str, Any]: + """Recompute the declared candidate and require concrete distinct identities.""" + + root = source_root.expanduser().resolve() + _git(root, "rev-parse", "--git-dir") + if not isinstance(target, Mapping) or set(target) != {"kind", "sha256", "base_commit", "manifest"}: + raise ReviewerWorkspaceError("WB_REVIEW_CURRENT_TARGET_INVALID") + kind = target.get("kind") + if kind not in {"commit", "worktree"}: + raise ReviewerWorkspaceError("WB_REVIEW_CURRENT_TARGET_INVALID") + base = _commit(root, target.get("base_commit")) + raw_manifest = target.get("manifest") + if not isinstance(raw_manifest, list): + raise ReviewerWorkspaceError("WB_REVIEW_CURRENT_TARGET_INVALID") + manifest: list[dict[str, str]] = [] + for raw in raw_manifest: + if not isinstance(raw, Mapping): + raise ReviewerWorkspaceError("WB_REVIEW_CURRENT_TARGET_INVALID") + raw_state = raw.get("state") + if not isinstance(raw_state, str) or raw_state not in {"present", "deleted"}: + raise ReviewerWorkspaceError("WB_REVIEW_CURRENT_TARGET_INVALID") + state = raw_state + expected_fields = {"path", "state", "sha256"} if state == "present" else {"path", "state"} + if set(raw) != expected_fields: + raise ReviewerWorkspaceError("WB_REVIEW_CURRENT_TARGET_INVALID") + relative = Path(str(raw.get("path") or "")) + if relative.is_absolute() or not relative.parts or any(part in {"", ".", ".."} for part in relative.parts): + raise ReviewerWorkspaceError("WB_REVIEW_CURRENT_TARGET_INVALID") + name = relative.as_posix() + if kind == "commit": + if state != "present": + raise ReviewerWorkspaceError("WB_REVIEW_CURRENT_TARGET_INVALID") + content = _git(root, "show", f"{base}:{name}", binary=True) + assert isinstance(content, bytes) else: - normalized["artifact"] = locator - normalized_validators.append(normalized) - sentinel_records: list[dict[str, str]] = [] - for locator in sentinels: - scope, relative, candidate = _source_path(source_root, control_root, protected, locator) - sentinel_records.append( - {"locator": f"{scope}:{relative.as_posix()}", "sha256": _sha256_bytes(candidate.read_bytes())} - ) - if stage_review_context is not None and task_review_context is not None: - raise ReviewerWorkspaceError("WB_REVIEW_CONTEXT_AMBIGUOUS") - stage_fields = {} - if stage_review_context is not None: - context = dict( - _validate_stage_context( - stage_review_context, require_re_review_predecessor=True - ) - ) - manifest = _review_runtime().stage_evidence_manifest(control_root, source_root, context, records) - # This runtime denies live source access. Only a complete frozen closure - # is a reproducible snapshot; a caller's direct-source label grants nothing. - context["evidence_mode"] = "packet_only" if manifest["missing"] else "reproducible_snapshot" - stage_fields = {"stage_review_context": context, "stage_evidence_manifest": manifest} - elif task_review_context is not None: - context = dict(_validate_task_context(task_review_context)) - _validate_task_source_identity(source_root, context) - stage_fields = {"task_review_context": context} - return { - "schema": "review-direct-evidence-packet-v1", - **stage_fields, - "artifacts": records, - "search_roots": normalized_search, - "validators": normalized_validators, - "sentinels": sentinel_records, - "network": {"state": network_state, "mechanism": "sandbox-exec-deny-network"}, - "policy_roots": { - "source": str(source_root), - "control": str(control_root), - "protected": [str(item) for item in protected], - }, - } - - -def _workspace_paths(runtime_root: Path, review_id: str) -> tuple[Path, Path]: - root = runtime_root.expanduser().resolve() - review_id = _safe_id(review_id) - workspace = (root / "reviews" / review_id).resolve(strict=False) - state = (root / ".state" / f"{review_id}.json").resolve(strict=False) - if not _inside(root, workspace) or not _inside(root, state): - raise ReviewerWorkspaceError("WB_REVIEW_RUNTIME_PATH_ESCAPE") - return workspace, state - - -def _public_packet(packet: dict[str, object]) -> dict[str, object]: - artifacts = packet.get("artifacts") - if not isinstance(artifacts, list): - raise ReviewerWorkspaceError("WB_REVIEW_PACKET_INVALID") - result = { - **{key: value for key, value in packet.items() if key != "policy_roots"}, - "artifacts": [ - {key: value for key, value in item.items() if key != "content_base64"} - for item in artifacts - if isinstance(item, dict) - ], - } - for context_key in ("stage_review_context", "task_review_context"): - context = result.get(context_key) - if isinstance(context, dict) and "previous_review" in context: - result[context_key] = { - key: value for key, value in context.items() if key != "previous_review" - } - return result - - -def _sb_quote(path: Path) -> str: - return '"' + str(path).replace("\\", "\\\\").replace('"', '\\"') + '"' - - -def _runtime_read_roots() -> list[Path]: - executable = Path(sys.executable).expanduser() - candidates = { - Path(value).expanduser().resolve() - for value in (sys.prefix, sys.exec_prefix, sys.base_prefix, sys.base_exec_prefix) - if value - } - candidates.add(executable.parent.parent.resolve()) - candidates.add(executable.resolve().parents[1]) - return sorted((path for path in candidates if path != Path("/")), key=str) - - -def _sandbox_profile(workspace: Path, policy: dict[str, object], validators: list[object]) -> str: - roots = [Path(str(policy["source"])), Path(str(policy["control"]))] - roots.extend(Path(str(value)) for value in policy.get("protected", []) if isinstance(value, str)) - denied_reads = " ".join(f"(subpath {_sb_quote(path.resolve())})" for path in roots) - runtime_reads = " ".join( - f"(subpath {_sb_quote(path)})" for path in _runtime_read_roots() - ) - return "\n".join( - [ - "(version 1)", - "(deny default)", - '(import "system.sb")', - "(allow process*)", - f"(allow file-read* (subpath {_sb_quote(workspace)}) {runtime_reads})", - f"(deny file-read* {denied_reads})", - f"(allow file-write* (subpath {_sb_quote(workspace / 'scratch')}))", - "(allow file-write-data (literal \"/dev/null\"))", - f"(deny file-write* {denied_reads})", - "(deny network*)", - "", - ] - ) - - -def _path_identity_digest(path: Path) -> str: - resolved = path.expanduser().resolve() - if not resolved.exists() or resolved.is_symlink(): - raise ReviewerWorkspaceError("WB_REVIEW_POLICY_ROOT_INVALID") - stat = resolved.stat() - return _canonical_digest( - { - "path": str(resolved), - "device": stat.st_dev, - "inode": stat.st_ino, - "mode": stat.st_mode, - "kind": "directory" if resolved.is_dir() else "file", - } - ) - - -def _root_identity_digests(source: Path, control: Path, protected: list[Path]) -> dict[str, object]: - return { - "source": _path_identity_digest(source), - "control": _path_identity_digest(control), - "protected": sorted(_path_identity_digest(path) for path in protected), - } - - -def _artifact_digest(workspace: Path, packet: dict[str, object]) -> str: - current: list[dict[str, str]] = [] - for item in packet.get("artifacts", []): - if not isinstance(item, dict): - raise ReviewerWorkspaceError("WB_REVIEW_WORKSPACE_INVALID") - locator = str(item.get("locator") or "") - target = _evidence_path(workspace, locator) - current.append({"locator": locator, "sha256": _sha256_bytes(target.read_bytes())}) - return _canonical_digest(current) - - -def _sentinel_digest( - source_root: Path, - control_root: Path, - protected_roots: list[Path], - sentinels: list[object], -) -> str: - current: list[dict[str, str]] = [] - for item in sentinels: - if not isinstance(item, dict): - raise ReviewerWorkspaceError("WB_REVIEW_SENTINEL_INVALID") - locator = str(item.get("locator") or "") - _, _, target = _source_path(source_root, control_root, protected_roots, locator) - current.append({"locator": locator, "sha256": _sha256_bytes(target.read_bytes())}) - return _canonical_digest(current) - - -def create_reviewer_workspace( - runtime_root: Path, - review_id: str, - packet: dict[str, object], - *, - source_root: Path | None = None, - control_root: Path | None = None, - protected_roots: list[Path] | None = None, -) -> dict[str, object]: - if packet.get("schema") != "review-direct-evidence-packet-v1": - raise ReviewerWorkspaceError("WB_REVIEW_PACKET_INVALID") - network = packet.get("network") - if not isinstance(network, dict) or network.get("state") not in NETWORK_STATES: - raise ReviewerWorkspaceError("WB_REVIEW_NETWORK_STATE_REQUIRED") - workspace, state_path = _workspace_paths(runtime_root, review_id) - if workspace.exists() or workspace.is_symlink() or state_path.exists(): - raise ReviewerWorkspaceError("WB_REVIEW_WORKSPACE_COLLISION") - artifacts = packet.get("artifacts") - if not isinstance(artifacts, list): - raise ReviewerWorkspaceError("WB_REVIEW_PACKET_INVALID") - public_packet = _public_packet(packet) - policy = packet.get("policy_roots") - if not isinstance(policy, dict): - raise ReviewerWorkspaceError("WB_REVIEW_PROTECTED_ROOTS_REQUIRED") - effective_source = Path(str(source_root or policy.get("source") or "")).expanduser().resolve() - effective_control = Path(str(control_root or policy.get("control") or "")).expanduser().resolve() - effective_protected = _normalized_roots( - protected_roots or [Path(str(value)) for value in policy.get("protected", []) if isinstance(value, str)] - ) - if effective_source != Path(str(policy.get("source"))).resolve() or effective_control != Path(str(policy.get("control"))).resolve(): - raise ReviewerWorkspaceError("WB_REVIEW_POLICY_ROOT_MISMATCH") - if "stage_review_context" in packet: - context = _validate_stage_context(packet["stage_review_context"]) - # A caller cannot relabel stale copied bytes with a fresh target identity. - for raw in artifacts: - if not isinstance(raw, dict): - raise ReviewerWorkspaceError("WB_REVIEW_PACKET_INVALID") - _, _, current_artifact = _source_path(effective_source, effective_control, effective_protected, raw.get("locator")) - if _sha256_bytes(current_artifact.read_bytes()) != raw.get("sha256"): - raise ReviewerWorkspaceError("WB_REVIEW_STAGE_PACKET_STALE") - scope, _, target = _source_path(effective_source, effective_control, effective_protected, context["target_locator"]) - if scope != "control" or context["target_locator"] not in {item.get("locator") for item in artifacts}: - raise ReviewerWorkspaceError("WB_REVIEW_STAGE_TARGET_MISSING") - current = _review_runtime().stage_target_identity(effective_control, str(context["stage"]), target, - source_root=effective_source) - if current != context["target_identity"]: - raise ReviewerWorkspaceError("WB_REVIEW_STAGE_TARGET_MISMATCH") - manifest = _review_runtime().stage_evidence_manifest(effective_control, effective_source, context, artifacts) - mode = "packet_only" if manifest["missing"] else "reproducible_snapshot" - if packet.get("stage_evidence_manifest") != manifest or context["evidence_mode"] != mode: - raise ReviewerWorkspaceError("WB_REVIEW_STAGE_EVIDENCE_MISMATCH") - control_evidence = [] - for raw in artifacts: - if not isinstance(raw, dict) or not str(raw.get("locator") or "").startswith("control:"): - continue - try: - content = base64.b64decode(str(raw.get("content_base64") or ""), validate=True).decode("utf-8") - except (ValueError, UnicodeDecodeError): - raise ReviewerWorkspaceError("WB_REVIEW_PACKET_INVALID") from None - control_evidence.append( - { - **{key: value for key, value in raw.items() if key != "content_base64"}, - "content": content, - } - ) - _validate_integrated_change_manifest(effective_source, public_packet, control_evidence) - elif "task_review_context" in packet: - context = _validate_task_context(packet["task_review_context"]) - _validate_task_source_identity(effective_source, context) - review_round = None - if ( - "stage_review_context" in packet - and isinstance(context, dict) - and context.get("stage") == "integrated_implementation" - ): - try: - review_round = _bounded_closure().review_round_binding( - effective_control, - review_id=review_id, - target_identity=context["target_identity"], - ) - except _bounded_closure().BoundedClosureError as error: - raise ReviewerWorkspaceError(error.code) from error - authority = _bounded_closure().resolve_working_workspace(effective_control) - try: - if authority is not None: - _bounded_closure().require_orchestration_admission( - authority, - operation="round_completion" if review_round is not None else "ordinary_new", - flow_id=(str(review_round["flow_id"]) if review_round is not None - else str(context["target_identity"].get("artifact_id")) - if "context" in locals() and isinstance(context, dict) else None), - ) - except _bounded_closure().BoundedClosureError as error: - raise ReviewerWorkspaceError(error.code, {"detail": error.detail or str(error)}) from error - try: - workspace.mkdir(parents=True) - scope_digests: dict[str, list[str]] = {"source": [], "control": []} - for raw in artifacts: - if not isinstance(raw, dict): - raise ReviewerWorkspaceError("WB_REVIEW_PACKET_INVALID") - scope, relative = _split_locator(raw.get("locator")) - try: - content = base64.b64decode(str(raw.get("content_base64") or ""), validate=True) - except (ValueError, TypeError): - raise ReviewerWorkspaceError("WB_REVIEW_PACKET_INVALID") from None - digest = _sha256_bytes(content) - if digest != raw.get("sha256"): - raise ReviewerWorkspaceError("WB_REVIEW_PACKET_DIGEST_MISMATCH") - target = workspace / "evidence" / scope / relative - if not _inside(workspace, target): - raise ReviewerWorkspaceError("WB_REVIEW_RUNTIME_PATH_ESCAPE") - target.parent.mkdir(parents=True, exist_ok=True) - target.write_bytes(content) - target.chmod(0o444) - scope_digests[scope].append(f"{relative.as_posix()}\0{digest}") - packet_path = workspace / "packet.json" - packet_path.write_text(json.dumps(public_packet, indent=2, sort_keys=True) + "\n", encoding="utf-8") - packet_path.chmod(0o444) - (workspace / "scratch").mkdir() - sandbox_profile = workspace / "sandbox.sb" - sandbox_profile.write_text( - _sandbox_profile(workspace, policy, list(public_packet.get("validators", []))), encoding="utf-8" - ) - sandbox_profile.chmod(0o444) - evidence_digest = _artifact_digest(workspace, public_packet) - sentinel_digest = _sentinel_digest( - effective_source, effective_control, effective_protected, list(public_packet.get("sentinels", [])) - ) - previous_review_state = {} - for context_key, state_key in ( - ("task_review_context", "task_review_previous_review"), - ("stage_review_context", "stage_review_previous_review"), - ): - context = packet.get(context_key) - if isinstance(context, dict) and "previous_review" in context: - previous_review_state[state_key] = context["previous_review"] - state = { - "schema": "reviewer-workspace-state-v1", - "owner": "work-bundle", - "review_id": review_id, - "workspace_token": f"reviews/{review_id}", - "packet_sha256": _canonical_digest(public_packet), - "source_evidence_digest": _sha256_bytes("\n".join(sorted(scope_digests["source"])).encode("utf-8")), - "control_evidence_digest": _sha256_bytes("\n".join(sorted(scope_digests["control"])).encode("utf-8")), - "network": dict(network), - "sandbox": {"mechanism": "sandbox-exec", "profile_sha256": _sha256_bytes(sandbox_profile.read_bytes())}, - "evidence_digest": evidence_digest, - "sentinel_digest": sentinel_digest, - "root_identity_digests": _root_identity_digests( - effective_source, effective_control, effective_protected - ), - "status": "active", - **({"admission_workspace": str(authority)} if authority is not None else {}), - **({"post_execution_review": review_round} if review_round is not None else {}), - **previous_review_state, - } - state_path.parent.mkdir(parents=True, exist_ok=True) - state_path.write_text(json.dumps(state, indent=2, sort_keys=True) + "\n", encoding="utf-8") - if review_round is not None: - try: - _bounded_closure().mark_review_round_prepared( - effective_control, - flow_id=str(review_round["flow_id"]), - round_id=str(review_round["round_id"]), + worktree_path = root / relative + path = worktree_path.resolve(strict=False) + if root not in path.parents or worktree_path.is_symlink(): + raise ReviewerWorkspaceError("WB_REVIEW_CURRENT_TARGET_INVALID") + if worktree_path.is_file(): + if state != "present": + raise ReviewerWorkspaceError("WB_REVIEW_CURRENT_TARGET_INVALID") + content = worktree_path.read_bytes() + elif worktree_path.exists(): + raise ReviewerWorkspaceError("WB_REVIEW_CURRENT_TARGET_INVALID") + else: + if state != "deleted": + raise ReviewerWorkspaceError("WB_REVIEW_CURRENT_TARGET_INVALID") + base_type = subprocess.run( + ["git", "-C", str(root), "cat-file", "-t", f"{base}:{name}"], + capture_output=True, + text=True, + check=False, ) - except _bounded_closure().BoundedClosureError as error: - raise ReviewerWorkspaceError(error.code) from error - except Exception: - shutil.rmtree(workspace, ignore_errors=True) - state_path.unlink(missing_ok=True) - raise - return { - "status": "prepared", - "workspace_path": str(workspace), - "state_path": str(state_path), - "network": network, - "packet_sha256": state["packet_sha256"], - "evidence_digest": state["evidence_digest"], - "sentinel_digest": state["sentinel_digest"], - } - - -def _load_workspace(workspace: Path) -> tuple[dict[str, object], dict[str, object]]: - workspace = workspace.expanduser().resolve() - packet_path = workspace / "packet.json" - if not packet_path.is_file() or packet_path.is_symlink(): - raise ReviewerWorkspaceError("WB_REVIEW_WORKSPACE_INVALID") - try: - packet = json.loads(packet_path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError): - raise ReviewerWorkspaceError("WB_REVIEW_WORKSPACE_INVALID") from None - artifacts = packet.get("artifacts") - validators = packet.get("validators") - if not isinstance(artifacts, list) or not isinstance(validators, list): - raise ReviewerWorkspaceError("WB_REVIEW_WORKSPACE_INVALID") - by_locator = {str(item.get("locator")): item for item in artifacts if isinstance(item, dict)} - by_validator = {str(item.get("validator_id")): item for item in validators if isinstance(item, dict)} - return packet, {"artifacts": by_locator, "validators": by_validator} - - -def _evidence_path(workspace: Path, locator: object) -> Path: - scope, relative = _split_locator(locator) - target = workspace / "evidence" / scope / relative - if not _inside(workspace, target): - raise ReviewerWorkspaceError("WB_REVIEW_RUNTIME_PATH_ESCAPE") - return target - - -def enforce_reviewer_write_scope(locator: object) -> None: - text = str(locator or "") - if text.startswith("source:"): - raise ReviewerWorkspaceError("WB_REVIEW_SOURCE_WRITE_DENIED", {"classification": "denied"}) - if text.startswith("control:"): - raise ReviewerWorkspaceError("WB_REVIEW_CONTROL_WRITE_DENIED", {"classification": "denied"}) - raise ReviewerWorkspaceError("WB_REVIEW_WRITE_DENIED", {"classification": "denied"}) - - -def _runtime_identity(workspace: Path) -> tuple[Path, str, dict[str, object]]: - workspace = workspace.expanduser().resolve() - if workspace.parent.name != "reviews": - raise ReviewerWorkspaceError("WB_REVIEW_WORKSPACE_INVALID") - runtime_root = workspace.parent.parent - review_id = _safe_id(workspace.name) - state_path = runtime_root / ".state" / f"{review_id}.json" - try: - state = json.loads(state_path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError): - raise ReviewerWorkspaceError("WB_REVIEW_PROVENANCE_INVALID") from None - if not isinstance(state, dict) or state.get("review_id") != review_id or state.get("owner") != "work-bundle": - raise ReviewerWorkspaceError("WB_REVIEW_PROVENANCE_INVALID") - return runtime_root, review_id, state - - -def _append_denial_event(workspace: Path, error: ReviewerWorkspaceError, operation: object) -> None: - runtime_root, review_id, state = _runtime_identity(workspace) - events_path = (runtime_root / "events" / f"{review_id}.jsonl").resolve(strict=False) - if not _inside(runtime_root, events_path): - raise ReviewerWorkspaceError("WB_REVIEW_RUNTIME_PATH_ESCAPE") - events_path.parent.mkdir(parents=True, exist_ok=True) - if events_path.exists(): - events_path.chmod(0o600) - operation_name = str(operation or "unknown") - if operation_name not in {"read", "write", "search", "validate", "network"}: - operation_name = "unknown" - event = { - "schema": "reviewer-denial-event-v1", - "event_id": f"review-denial-{uuid.uuid4()}", - "timestamp": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), - "review_id": review_id, - "packet_sha256": state.get("packet_sha256"), - "event_type": "reviewer_operation_denied", - "denial_code": error.code, - "operation": operation_name, - "privacy": "operational_metadata_only", - } - descriptor = os.open(events_path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600) - try: - os.write(descriptor, (json.dumps(event, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")) - finally: - os.close(descriptor) - events_path.chmod(0o400) - - -def _seal_event_log(runtime_root: Path, review_id: str) -> dict[str, object]: - events_path = (runtime_root / "events" / f"{review_id}.jsonl").resolve(strict=False) - if not _inside(runtime_root, events_path): - raise ReviewerWorkspaceError("WB_REVIEW_RUNTIME_PATH_ESCAPE") - events_path.parent.mkdir(parents=True, exist_ok=True) - if not events_path.exists(): - descriptor = os.open(events_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o400) - os.close(descriptor) - events_path.chmod(0o400) - return { - "event_log_path": str(events_path), - "event_log_sha256": _sha256_bytes(events_path.read_bytes()), - "event_log_mode": "0400", - } - - -def _sandbox_denied(completed: subprocess.CompletedProcess[str]) -> bool: - if completed.returncode == 0: - return False - detail = f"{completed.stdout}\n{completed.stderr}".lower() - return ( - completed.returncode < 0 - or "operation not permitted" in detail - or "sandbox violation" in detail - or "permissionerror" in detail - ) - - -def _run_sandboxed_process(workspace: Path, argv: list[str]) -> subprocess.CompletedProcess[str]: - workspace = workspace.expanduser().resolve() - _, _, state = _runtime_identity(workspace) - if platform.system() != "Darwin" or not Path("/usr/bin/sandbox-exec").is_file(): - raise ReviewerWorkspaceError("WB_REVIEW_SANDBOX_UNAVAILABLE", {"classification": "denied"}) - if not argv or not all(isinstance(item, str) and item for item in argv): - raise ReviewerWorkspaceError("WB_REVIEW_COMMAND_INVALID", {"classification": "denied"}) - executable = Path(argv[0]).expanduser() - if not executable.is_absolute() or not executable.is_file(): - raise ReviewerWorkspaceError("WB_REVIEW_COMMAND_INVALID", {"classification": "denied"}) - allowed_runtime_roots = [ - Path("/System"), - Path("/usr"), - Path("/bin"), - Path("/sbin"), - *_runtime_read_roots(), - ] - if not any(_inside(root, executable) for root in allowed_runtime_roots): - raise ReviewerWorkspaceError("WB_REVIEW_COMMAND_INVALID", {"classification": "denied"}) - profile = workspace / "sandbox.sb" - sandbox_state = state.get("sandbox") if isinstance(state.get("sandbox"), dict) else {} - if not profile.is_file() or profile.is_symlink() or _sha256_bytes(profile.read_bytes()) != sandbox_state.get("profile_sha256"): - raise ReviewerWorkspaceError("WB_REVIEW_SANDBOX_PROFILE_INVALID", {"classification": "denied"}) - scratch = workspace / "scratch" - environment = { - "PATH": "/usr/bin:/bin:/usr/sbin:/sbin", - "HOME": str(scratch / "home"), - "TMPDIR": str(scratch / "tmp"), - "PYTHONDONTWRITEBYTECODE": "1", - } - (scratch / "home").mkdir(parents=True, exist_ok=True) - (scratch / "tmp").mkdir(parents=True, exist_ok=True) - return subprocess.run( - ["/usr/bin/sandbox-exec", "-f", str(profile), *argv], - cwd=workspace, - env=environment, - text=True, - capture_output=True, - check=False, - ) - - -def run_sandboxed_validator(workspace: Path, argv: list[str]) -> subprocess.CompletedProcess[str]: - """Run one frozen validator argv inside the macOS process sandbox.""" - return _run_sandboxed_process(workspace, argv) - - -def _task_product_judgment_review( - judgment: object, - *, - review_id: str, - context: dict[str, object], - packet: dict[str, object], - started_at: str, - completed_at: str, - previous_review: object = None, - integrated_stage: bool = False, -) -> dict[str, object]: - """Preserve compact reviewer observations inside a controller-decidable envelope.""" - if not isinstance(judgment, dict) or set(judgment) != {"task_review"}: - raise ReviewerWorkspaceError("WB_REVIEW_TASK_OUTPUT_INVALID") - product = judgment["task_review"] - if not isinstance(product, dict) or set(product) != {"reviewed_head", "verdict", "findings"}: - raise ReviewerWorkspaceError("WB_REVIEW_TASK_OUTPUT_INVALID") - identity = context["target_identity"] - if ( - not isinstance(identity, dict) - or product["reviewed_head"] != ( - identity.get("source_tree") if integrated_stage else identity.get("revision") - ) - or product["verdict"] not in {"accept", "repair"} - or not isinstance(product["findings"], list) - ): - raise ReviewerWorkspaceError("WB_REVIEW_TASK_OUTPUT_INVALID") - if product["verdict"] == "repair" and not product["findings"]: - raise ReviewerWorkspaceError("WB_REVIEW_TASK_OUTPUT_INVALID") - findings = [] - for item in product["findings"]: - expected = {"finding_id", "severity", "requirement_id", "boundary", "evidence", "expected", "observed", "owner"} - if ( - not isinstance(item, dict) or set(item) != expected - or item["severity"] not in {"blocking", "advisory"} - or not all(isinstance(item[key], str) and item[key].strip() for key in expected - {"severity"}) - ): - raise ReviewerWorkspaceError("WB_REVIEW_TASK_OUTPUT_INVALID") - findings.append({ - "schema": "review-finding-v2", - "finding_id": item["finding_id"], "stage": "implementation", - "reviewer_observation": dict(item), - "evidence": [{ - "kind": "source", "locator": item["boundary"], - "digest_or_identity": _canonical_digest(item), - "observation": f"{item['evidence']} Expected: {item['expected']} Observed: {item['observed']}", - }], - "target_identity": identity, - "summary": f"{item['requirement_id']}: {item['observed']}", - "controller_decision": None, - }) - artifacts = [ - {"path": item["locator"], "sha256": item["sha256"]} - for item in packet.get("artifacts", []) if isinstance(item, dict) - ] - result = { - "required": True, "reviewer_independent": True, "review_id": review_id, - "reviewed_head": identity["revision"], "review_mode": context.get("review_mode", "initial"), - "review_target_kind": "task", "repair_frontier": context.get("repair_frontier"), - "review_reset": context.get("review_reset"), "target_identity": identity, - "reviewer": { - "agent_id": context["agent_id"], "capability": context["capability"], - "authorship": "none", "repair_participation": "none", - "decision_participation": "none", "deliberation_participation": "none", - "context_origin": context["evidence_mode"], - }, - "evidence": { - "mode": context["evidence_mode"], "capabilities": ["product review judgment"], - "unavailable_evidence": [], "commands": [], "artifacts": artifacts, - }, - "verdict": product["verdict"], "findings": findings, - "started_at": started_at, "completed_at": completed_at, - "staleness": {"is_stale": False, "reason": None, "supersedes": None}, - } - if context.get("review_mode", "initial") == "repair" or context.get("review_reset") is not None: - if not isinstance(previous_review, dict): - raise ReviewerWorkspaceError("WB_REVIEW_TASK_CONTROL_INPUT_INVALID") - result["previous_review"] = previous_review - if integrated_stage: - result.pop("required") - result.pop("reviewer_independent") - result.pop("reviewed_head") - result["stage"] = "integrated_implementation" - result["review_target_kind"] = "stage" - result["verdict"] = "accepted" if product["verdict"] == "accept" else "repair" - return result - - -def _stage_product_judgment_review( - judgment, *, review_id, context, packet, started_at, completed_at, - previous_review=None, -): - """Compose native stage authority without asking the reviewer to invent it.""" - if not isinstance(judgment, dict) or set(judgment) != {"stage_review"}: - raise ReviewerWorkspaceError("WB_REVIEW_STAGE_OUTPUT_INVALID") - product = judgment["stage_review"] - if (not isinstance(product, dict) or set(product) != {"target_identity", "verdict", "findings"} - or product["target_identity"] != context["target_identity"] - or product["verdict"] not in TERMINAL_VERDICTS or not isinstance(product["findings"], list)): - raise ReviewerWorkspaceError("WB_REVIEW_STAGE_OUTPUT_INVALID") - result = { - "review_id": review_id, "stage": context["stage"], "target_identity": context["target_identity"], - "review_mode": context.get("review_mode", "initial"), "review_target_kind": "stage", - "repair_frontier": context.get("repair_frontier"), "review_reset": context.get("review_reset"), - "reviewer": {"agent_id": context["agent_id"], "capability": context["capability"], - "authorship": "none", "repair_participation": "none", "decision_participation": "none", - "deliberation_participation": "none", "context_origin": context["evidence_mode"]}, - "evidence": {"mode": context["evidence_mode"], "capabilities": ["product review judgment"], - "unavailable_evidence": [], "commands": [], - "artifacts": [{"path": item["locator"], "sha256": item["sha256"]} for item in packet["artifacts"]]}, - "verdict": product["verdict"], "findings": product["findings"], - "started_at": started_at, "completed_at": completed_at, - "staleness": {"is_stale": False, "reason": None, "supersedes": None}, - } - if context.get("review_mode", "initial") == "repair" or context.get("review_reset") is not None: - if not isinstance(previous_review, dict): - raise ReviewerWorkspaceError("WB_REVIEW_STAGE_CONTROL_INPUT_INVALID") - result["previous_review"] = previous_review - return result - - -def _native_capture_artifacts(runtime_root: Path, run_id: str) -> tuple[Path, dict[str, object], dict[str, bytes]]: - """Load a complete immutable v2 run capture; legacy diagnostics are not promotable.""" - runtime_root = runtime_root.expanduser().resolve() - run_id = _safe_id(run_id) - directory = (runtime_root / "diagnostics/reviewer-native" / run_id).resolve(strict=False) - if not _inside(runtime_root, directory): - raise ReviewerWorkspaceError("WB_REVIEW_RUNTIME_PATH_ESCAPE") - capture_path = directory / "capture.json" - try: - if capture_path.is_symlink() or not capture_path.is_file() or capture_path.stat().st_mode & 0o222: - raise ValueError("mutable capture") - capture = json.loads(capture_path.read_bytes()) - except (OSError, ValueError, TypeError, json.JSONDecodeError): - raise ReviewerWorkspaceError("WB_REVIEW_NATIVE_CAPTURE_INCOMPLETE") from None - required = { - "request.json", "stdout.jsonl", "stderr.txt", "launch.json", "packet.json", - "events.jsonl", "control.json", - } - artifacts = capture.get("artifacts") if isinstance(capture, dict) else None - if ( - capture.get("schema") != "reviewer-native-capture-v2" - or capture.get("status") != "captured-unadmitted" - or capture.get("run_id") != run_id - or not re.fullmatch(r"reviewer-run-[0-9a-f-]{36}", run_id) - or not isinstance(capture.get("review_id"), str) - or _safe_id(capture["review_id"]) != capture["review_id"] - or not isinstance(capture.get("started_at"), str) - or not isinstance(capture.get("completed_at"), str) - or not isinstance(artifacts, dict) - or not required.issubset(artifacts) - or capture.get("event_log_mode") != "0400" - ): - raise ReviewerWorkspaceError("WB_REVIEW_NATIVE_CAPTURE_INCOMPLETE") - contents: dict[str, bytes] = {} - try: - for name, digest in artifacts.items(): - if not isinstance(name, str) or Path(name).name != name: - raise ValueError("invalid artifact name") - path = directory / name - if path.is_symlink() or not path.is_file() or path.stat().st_mode & 0o222: - raise ValueError("mutable artifact") - content = path.read_bytes() - if _sha256_bytes(content) != digest: - raise ValueError("artifact digest mismatch") - contents[name] = content - packet = json.loads(contents["packet.json"]) - if _canonical_digest(packet) != capture.get("packet_sha256"): - raise ValueError("packet digest mismatch") - if _sha256_bytes(contents["events.jsonl"]) != capture.get("event_log_sha256"): - raise ValueError("event log mismatch") - started = datetime.fromisoformat(capture["started_at"].replace("Z", "+00:00")) - completed = datetime.fromisoformat(capture["completed_at"].replace("Z", "+00:00")) - if started.tzinfo is None or completed.tzinfo is None or started > completed: - raise ValueError("invalid capture timing") - except (OSError, ValueError, TypeError, KeyError, json.JSONDecodeError): - raise ReviewerWorkspaceError("WB_REVIEW_NATIVE_CAPTURE_INCOMPLETE") from None - return directory, capture, contents - - -def _write_immutable_once(path: Path, content: bytes) -> None: - if path.exists(): - if path.is_symlink() or not path.is_file() or path.read_bytes() != content or path.stat().st_mode & 0o222: - raise ReviewerWorkspaceError("WB_REVIEW_NATIVE_RECEIPT_COLLISION") - return - with path.open("xb") as stream: - stream.write(content) - path.chmod(0o400) - - -def complete_native_reviewer_capture(runtime_root: Path, run_id: str) -> dict[str, object]: - """Idempotently turn one complete future capture into the ordinary native receipt.""" - runtime_root = runtime_root.expanduser().resolve() - diagnostic_path, capture, items = _native_capture_artifacts(runtime_root, run_id) - if capture.get("exit_code") != 0: - raise ReviewerWorkspaceError( - "WB_REVIEW_NATIVE_PROCESS_FAILED", - {"exit_code": capture.get("exit_code"), "diagnostic_path": str(diagnostic_path)}, - ) - if capture.get("executable_unchanged") is not True: - raise ReviewerWorkspaceError( - "WB_REVIEW_NATIVE_EXECUTABLE_MUTATED", {"diagnostic_path": str(diagnostic_path)} - ) - try: - packet = json.loads(items["packet.json"]) - request = json.loads(items["request.json"]) - launch = json.loads(items["launch.json"]) - control = json.loads(items["control.json"]) - controller_evidence = ( - json.loads(items["controller.json"]) if "controller.json" in items else None - ) - combined_evidence = [*request["evidence"], *(controller_evidence or [])] - expected_input = _native_review_input( - packet, combined_evidence if controller_evidence is not None else None - ) - expected_artifacts = _native_review_artifacts(packet, combined_evidence) - if ( - set(request) != {"instructions", "review_input", "evidence"} - or not isinstance(request["instructions"], str) - or not request["instructions"].strip() - or request["review_input"] != expected_input - or len(request["evidence"]) != len(expected_artifacts) - ): - raise ValueError("native request mismatch") - for expected, supplied in zip(expected_artifacts, request["evidence"]): - if ( - not isinstance(supplied, dict) - or set(supplied) != {*expected, "content"} - or any(supplied.get(key) != value for key, value in expected.items()) - or _sha256_bytes(str(supplied["content"]).encode()) != expected["sha256"] - ): - raise ValueError("native evidence mismatch") - if controller_evidence is not None: - controller_locators = _controller_only_artifact_locators(packet, combined_evidence) - controller_artifacts = [ - item for item in packet["artifacts"] - if item.get("locator") in controller_locators - ] - if len(controller_evidence) != len(controller_artifacts): - raise ValueError("native controller evidence mismatch") - for expected, supplied in zip(controller_artifacts, controller_evidence): - if ( - not isinstance(supplied, dict) - or set(supplied) != {*expected, "content"} - or any(supplied.get(key) != value for key, value in expected.items()) - or _sha256_bytes(str(supplied["content"]).encode()) != expected["sha256"] - ): - raise ValueError("native controller evidence mismatch") - host_run_id, worker_output = parse_native_reviewer_transcript( - items["stdout.jsonl"].decode(), items["stderr.txt"].decode() - ) - context_key = "stage_review_context" if "stage_review_context" in packet else "task_review_context" - context = ( - _validate_stage_context(packet[context_key]) - if context_key == "stage_review_context" - else _validate_task_context(packet[context_key]) - ) - context = {**context, "agent_id": host_run_id, "execution_id": host_run_id} - compact_integrated = ( - context_key == "stage_review_context" - and context.get("stage") == "integrated_implementation" - and isinstance(worker_output, dict) - and "task_review" in worker_output - ) - previous_key = ( - "task_review_previous_review" - if context_key == "task_review_context" - else "stage_review_previous_review" - ) - review = ( - _task_product_judgment_review( - worker_output, - review_id=capture["review_id"], context=context, packet=packet, - started_at=capture["started_at"], completed_at=capture["completed_at"], - previous_review=control.get(previous_key), integrated_stage=compact_integrated, - ) - if context_key == "task_review_context" or compact_integrated - else _stage_product_judgment_review( - worker_output, - review_id=capture["review_id"], context=context, packet=packet, - started_at=capture["started_at"], completed_at=capture["completed_at"], - previous_review=control.get(previous_key), - ) - ) - validated = ( - _review_runtime()._validated_review_envelope(review) - if context_key == "stage_review_context" - else _review_runtime().validate_task_acceptance_review(review) - ) - mode = "direct_source" if validated.evidence["mode"] == "direct" else validated.evidence["mode"] - if ( - "reviewer_run" in review - or validated.review_id != capture["review_id"] - or (context_key == "stage_review_context" and validated.stage != context["stage"]) - or validated.target_identity != context["target_identity"] - or validated.reviewer["agent_id"] != context["agent_id"] - or validated.reviewer["context_origin"] != context["evidence_mode"] - or validated.reviewer["capability"] != context["capability"] - or mode != context["evidence_mode"] - ): - raise ValueError("native judgment mismatch") - except ReviewerWorkspaceError as error: - raise ReviewerWorkspaceError( - error.code, {**error.result, "diagnostic_path": str(diagnostic_path)} - ) from error - except (ValueError, TypeError, KeyError, IndexError, AttributeError, json.JSONDecodeError) as error: - raise ReviewerWorkspaceError( - "WB_REVIEW_NATIVE_CAPTURE_INVALID", {"diagnostic_path": str(diagnostic_path)} - ) from error - - argv = launch["argv"] - if ( - not isinstance(argv, list) - or len(argv) <= 11 - or not re.fullmatch(r"[0-9a-f]{64}", str(launch.get("executable_sha256") or "")) - or not isinstance(argv[0], str) - or not Path(argv[0]).is_absolute() - or argv != _native_reviewer_argv(Path(argv[0]), Path(argv[9]), argv[11]) - ): - raise ReviewerWorkspaceError( - "WB_REVIEW_NATIVE_CAPTURE_INVALID", {"diagnostic_path": str(diagnostic_path)} - ) - receipt = { - "schema": "reviewer-native-receipt-v1", "run_id": run_id, - "review_id": capture["review_id"], "status": "passed", - "packet_sha256": capture["packet_sha256"], "sandbox_profile_sha256": None, - "argv_sha256": _canonical_digest(argv), "exit_code": 0, - "stdout_sha256": _sha256_bytes(items["stdout.jsonl"]), - "stderr_sha256": _sha256_bytes(items["stderr.txt"]), - "event_log_sha256": capture["event_log_sha256"], - "event_log_mode": capture["event_log_mode"], - "started_at": capture["started_at"], "completed_at": capture["completed_at"], - "host_run_id": host_run_id, - "request_sha256": _sha256_bytes(items["request.json"]), - "executable_sha256": launch["executable_sha256"], - "isolation": dict(NATIVE_ISOLATION), context_key: context, - "review_result_sha256": _canonical_digest(review), "review_result": review, - } - receipt_path = ( - runtime_root / "receipts/reviewer-process" / f"{run_id}.json" - ).resolve(strict=False) - if not _inside(runtime_root, receipt_path): - raise ReviewerWorkspaceError("WB_REVIEW_RUNTIME_PATH_ESCAPE") - receipt_path.parent.mkdir(parents=True, exist_ok=True) - for suffix in ( - "packet.json", "events.jsonl", "request.json", "stdout.jsonl", "stderr.txt", - "launch.json", "controller.json", - ): - if suffix in items: - _write_immutable_once(receipt_path.with_suffix(f".{suffix}"), items[suffix]) - receipt_bytes = (json.dumps(receipt, indent=2, sort_keys=True) + "\n").encode() - _write_immutable_once(receipt_path, receipt_bytes) - reference = {"run_id": run_id, "sha256": _sha256_bytes(receipt_bytes)} + if base_type.returncode or base_type.stdout.strip() != "blob": + raise ReviewerWorkspaceError("WB_REVIEW_CURRENT_TARGET_INVALID") + content = None + if state == "present": + assert content is not None + digest = hashlib.sha256(content).hexdigest() + if raw.get("sha256") != digest: + raise ReviewerWorkspaceError("WB_REVIEW_CURRENT_TARGET_DIGEST_MISMATCH") + manifest.append({"path": name, "state": state, "sha256": digest}) + else: + manifest.append({"path": name, "state": state}) + if [item["path"] for item in manifest] != sorted({item["path"] for item in manifest}): + raise ReviewerWorkspaceError("WB_REVIEW_CURRENT_TARGET_INVALID") + aggregate = hashlib.sha256(_manifest_bytes(manifest)).hexdigest() + if target.get("sha256") != aggregate: + raise ReviewerWorkspaceError("WB_REVIEW_CURRENT_TARGET_DIGEST_MISMATCH") + if not reviewer_agent_id.strip() or not implementor_agent_id.strip(): + raise ReviewerWorkspaceError("WB_REVIEW_CURRENT_REVIEWER_INVALID") + if reviewer_agent_id == implementor_agent_id: + raise ReviewerWorkspaceError("WB_REVIEW_CURRENT_REVIEWER_NOT_INDEPENDENT") return { - **receipt, "receipt_path": str(receipt_path), - "event_log_path": str(receipt_path.with_suffix(".events.jsonl")), - "diagnostic_path": str(diagnostic_path), "reviewer_run": reference, - } - - -def run_sandboxed_reviewer(workspace: Path, argv: list[str]) -> dict[str, object]: - """Launch the entire reviewer under the frozen deny-default profile.""" - return _run_reviewer(workspace, argv) - - -def _run_reviewer( - workspace: Path, - argv: list[str], - *, - native_request: dict[str, object] | None = None, - native_controller_evidence: list[dict[str, object]] | None = None, -) -> dict[str, object]: - workspace = workspace.expanduser().resolve() - runtime_root, review_id, state = _runtime_identity(workspace) - if state.get("post_execution_review") is not None and state.get("status") == "judged": - raise ReviewerWorkspaceError("WB_POST_EXECUTION_JUDGMENT_ALREADY_RECORDED") - packet, _ = _load_workspace(workspace) - round_state = state.get("post_execution_review") - context = packet.get("stage_review_context") or packet.get("task_review_context") - authority_value = state.get("admission_workspace") - authority = Path(str(authority_value)) if isinstance(authority_value, str) else None - try: - if authority is not None: - _bounded_closure().require_orchestration_admission( - authority, - operation="round_completion" if isinstance(round_state, dict) else "ordinary_new", - flow_id=(str(round_state["flow_id"]) if isinstance(round_state, dict) - else str(context["target_identity"].get("artifact_id")) - if isinstance(context, dict) else None), - ) - except _bounded_closure().BoundedClosureError as error: - raise ReviewerWorkspaceError(error.code, {"detail": error.detail or str(error)}) from error - if _canonical_digest(packet) != state.get("packet_sha256") or _artifact_digest(workspace, packet) != state.get("evidence_digest"): - raise ReviewerWorkspaceError("WB_REVIEW_EVIDENCE_MUTATED") - if "stage_review_context" in packet: - try: - _review_runtime().validate_stage_evidence( - workspace / "evidence/control", - _validate_stage_context(packet["stage_review_context"]), - packet, - ) - except (ValueError, OSError, SystemExit) as error: - raise ReviewerWorkspaceError("WB_REVIEW_STAGE_EVIDENCE_INCOMPLETE") from error - if isinstance(round_state, dict): - if authority is None: - raise ReviewerWorkspaceError("WB_POST_EXECUTION_ROUND_BINDING_INVALID") - try: - _bounded_closure().require_review_round_execution( - authority, - binding=round_state, - ) - except _bounded_closure().BoundedClosureError as error: - raise ReviewerWorkspaceError( - error.code, - {"detail": error.detail or str(error)}, - ) from error - started_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") - run_id = f"reviewer-run-{uuid.uuid4()}" - native = native_request is not None - request_bytes = json.dumps(native_request, sort_keys=True, ensure_ascii=False).encode() if native else b"" - executable_digest = _sha256_bytes(Path(argv[0]).read_bytes()) if native else None - completed = (_run_native_process(workspace, argv, request_bytes.decode()) if native - else _run_sandboxed_process(workspace, argv)) - if native: - if _artifact_digest(workspace, packet) != state.get("evidence_digest"): - raise ReviewerWorkspaceError("WB_REVIEW_EVIDENCE_MUTATED") - completed_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") - sealed = _seal_event_log(runtime_root, review_id) - executable_unchanged = _sha256_bytes(Path(argv[0]).read_bytes()) == executable_digest - diagnostic_path = _retain_native_diagnostics( - runtime_root, run_id, review_id, argv, request_bytes, executable_digest, completed, - started_at=started_at, completed_at=completed_at, packet=packet, state=state, - sealed=sealed, native_controller_evidence=native_controller_evidence, - executable_unchanged=executable_unchanged, - ) - if completed.returncode != 0: - raise ReviewerWorkspaceError("WB_REVIEW_NATIVE_PROCESS_FAILED", { - "exit_code": completed.returncode, "diagnostic_path": diagnostic_path}) - if not executable_unchanged: - raise ReviewerWorkspaceError("WB_REVIEW_NATIVE_EXECUTABLE_MUTATED", {"diagnostic_path": diagnostic_path}) - try: - result = complete_native_reviewer_capture(runtime_root, run_id) - except ReviewerWorkspaceError as error: - raise ReviewerWorkspaceError(error.code, {**error.result, "diagnostic_path": diagnostic_path}) from error - if state.get("post_execution_review") is not None: - state["status"] = "judged" - state["judgment_sha256"] = result.get("review_result_sha256") - state_path = runtime_root / ".state" / f"{review_id}.json" - temporary = state_path.with_suffix(".tmp") - temporary.write_text(json.dumps(state, indent=2, sort_keys=True) + "\n", encoding="utf-8") - os.replace(temporary, state_path) - return result - if _artifact_digest(workspace, packet) != state.get("evidence_digest"): - raise ReviewerWorkspaceError("WB_REVIEW_EVIDENCE_MUTATED") - denied = _sandbox_denied(completed) - if denied: - _append_denial_event( - workspace, - ReviewerWorkspaceError("WB_REVIEW_SANDBOX_DENIED", {"classification": "denied"}), - "validate", - ) - sealed = _seal_event_log(runtime_root, review_id) - sandbox_state = state.get("sandbox") if isinstance(state.get("sandbox"), dict) else {} - receipt = { - "schema": "reviewer-process-receipt-v1", - "run_id": run_id, - "review_id": review_id, - "status": "denied" if denied else ("passed" if completed.returncode == 0 else "failed"), - "packet_sha256": state.get("packet_sha256"), - "sandbox_profile_sha256": sandbox_state.get("profile_sha256"), - "argv_sha256": _canonical_digest(argv), - "exit_code": completed.returncode, - "stdout_sha256": _sha256_bytes(completed.stdout.encode("utf-8")), - "stderr_sha256": _sha256_bytes(completed.stderr.encode("utf-8")), - "event_log_sha256": sealed["event_log_sha256"], - "event_log_mode": sealed["event_log_mode"], - "started_at": started_at, - "completed_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), + "target": {"kind": kind, "sha256": aggregate, "base_commit": base, "manifest": manifest}, + "reviewer": {"agent_id": reviewer_agent_id}, + "implementor_agent_id": implementor_agent_id, } - context_key = "stage_review_context" if "stage_review_context" in packet else ( - "task_review_context" if "task_review_context" in packet else None - ) - if context_key is not None: - context = ( - _validate_stage_context(packet[context_key]) - if context_key == "stage_review_context" - else _validate_task_context(packet[context_key]) - ) - try: - worker_output = json.loads(completed.stdout) - compact_integrated = ( - context_key == "stage_review_context" - and context.get("stage") == "integrated_implementation" - and isinstance(worker_output, dict) and "task_review" in worker_output - ) - review = ( - _task_product_judgment_review( - worker_output, review_id=review_id, context=context, packet=packet, - started_at=started_at, completed_at=receipt["completed_at"], - previous_review=state.get( - "task_review_previous_review" - if context_key == "task_review_context" - else "stage_review_previous_review" - ), - integrated_stage=compact_integrated, - ) - if context_key == "task_review_context" or compact_integrated - else worker_output - ) - validated = ( - _review_runtime()._validated_review_envelope(review) - if context_key == "stage_review_context" - else _review_runtime().validate_task_acceptance_review(review) - ) - except (ValueError, TypeError) as error: - code = "WB_REVIEW_TASK_OUTPUT_INVALID" if context_key == "task_review_context" else "WB_REVIEW_STAGE_OUTPUT_INVALID" - raise ReviewerWorkspaceError(code) from error - mode = "direct_source" if validated.evidence["mode"] == "direct" else validated.evidence["mode"] - review_context = { - "review_mode": review.get("review_mode", "initial"), - "review_target_kind": review.get("review_target_kind", "stage"), - "repair_frontier": review.get("repair_frontier"), - "review_reset": review.get("review_reset"), - } - packet_context = { - "review_mode": context.get("review_mode", "initial"), - "review_target_kind": context.get("review_target_kind", "stage"), - "repair_frontier": context.get("repair_frontier"), - "review_reset": context.get("review_reset"), - } - if ("reviewer_run" in review or validated.review_id != review_id - or (context_key == "stage_review_context" and validated.stage != context["stage"]) - or validated.target_identity != context["target_identity"] - or validated.reviewer["agent_id"] != context["agent_id"] - or validated.reviewer["context_origin"] != context["evidence_mode"] - or validated.reviewer["capability"] != context["capability"] or mode != context["evidence_mode"] - or review_context != packet_context): - raise ReviewerWorkspaceError("WB_REVIEW_STAGE_OUTPUT_MISMATCH") - if validated.verdict == "accepted" and context_key == "stage_review_context": - try: - _review_runtime().validate_stage_evidence(workspace / "evidence/control", context, packet) - except (ValueError, OSError, SystemExit) as error: - raise ReviewerWorkspaceError("WB_REVIEW_STAGE_EVIDENCE_INCOMPLETE") from error - receipt[context_key] = context - receipt["review_result_sha256"] = _canonical_digest(review) - if context_key == "task_review_context" or compact_integrated: - receipt["review_result"] = review - receipt["isolation"] = {"mechanism": "sandbox-exec", "network": "denied", "write_scope": "scratch"} - if state.get("post_execution_review") is not None: - state["status"] = "judged" - state["judgment_sha256"] = receipt.get("review_result_sha256") - state_path = runtime_root / ".state" / f"{review_id}.json" - temporary = state_path.with_suffix(".tmp") - temporary.write_text(json.dumps(state, indent=2, sort_keys=True) + "\n", encoding="utf-8") - os.replace(temporary, state_path) - receipt_path = (runtime_root / "receipts" / "reviewer-process" / f"{run_id}.json").resolve(strict=False) - if not _inside(runtime_root, receipt_path): - raise ReviewerWorkspaceError("WB_REVIEW_RUNTIME_PATH_ESCAPE") - receipt_path.parent.mkdir(parents=True, exist_ok=True) - # Retain immutable run-scoped evidence after workspace cleanup or later runs. - retained_items = [("packet.json", json.dumps(packet, sort_keys=True).encode()), - ("events.jsonl", Path(str(sealed["event_log_path"])).read_bytes())] - retained_items.append(("profile.sb", (workspace / "sandbox.sb").read_bytes())) - for suffix, content in retained_items: - retained = receipt_path.with_suffix(f".{suffix}") - with retained.open("xb") as stream: - stream.write(content) - retained.chmod(0o400) - with receipt_path.open("x", encoding="utf-8") as stream: - stream.write(json.dumps(receipt, indent=2, sort_keys=True) + "\n") - receipt_path.chmod(0o400) - reference = {"run_id": run_id, "sha256": _sha256_bytes(receipt_path.read_bytes())} - return {**receipt, "receipt_path": str(receipt_path), "event_log_path": sealed["event_log_path"], - **({"reviewer_run": reference} if context_key is not None else {})} - - -def _execute_reviewer_request(workspace: Path, request: dict[str, object]) -> dict[str, object]: - workspace = workspace.expanduser().resolve() - packet, indexes = _load_workspace(workspace) - operation = str(request.get("operation") or "") - if operation == "write": - enforce_reviewer_write_scope(request.get("artifact")) - if operation == "network": - raise ReviewerWorkspaceError("WB_REVIEW_NETWORK_DENIED", {"classification": "denied"}) - if operation == "read": - locator = str(request.get("artifact") or "") - _split_locator(locator) - record = indexes["artifacts"].get(locator) - if not isinstance(record, dict): - raise ReviewerWorkspaceError("WB_REVIEW_PROTECTED_READ_DENIED", {"classification": "denied"}) - target = _evidence_path(workspace, locator) - if _sha256_bytes(target.read_bytes()) != record.get("sha256"): - raise ReviewerWorkspaceError("WB_REVIEW_EVIDENCE_MUTATED") - return {"status": "allowed", "artifact": locator, "content": target.read_text(encoding="utf-8")} - if operation == "search": - pattern = str(request.get("pattern") or "") - if not pattern or len(pattern) > 256 or "\n" in pattern: - raise ReviewerWorkspaceError("WB_REVIEW_SEARCH_PATTERN_INVALID") - allowed_roots = packet.get("search_roots") - if not isinstance(allowed_roots, list): - raise ReviewerWorkspaceError("WB_REVIEW_WORKSPACE_INVALID") - matches: list[str] = [] - for locator, record in sorted(indexes["artifacts"].items()): - if not isinstance(record, dict) or not any( - locator == root or locator.startswith(f"{root.rstrip('/')}/") for root in allowed_roots - ): - continue - target = _evidence_path(workspace, locator) - if _sha256_bytes(target.read_bytes()) != record.get("sha256"): - raise ReviewerWorkspaceError("WB_REVIEW_EVIDENCE_MUTATED") - for number, line in enumerate(target.read_text(encoding="utf-8").splitlines(), start=1): - if pattern in line: - matches.append(f"{locator}:{number}:{line.strip()}") - return {"status": "allowed", "matches": matches} - if operation == "validate": - validator_id = str(request.get("validator_id") or "") - validator = indexes["validators"].get(validator_id) - if not isinstance(validator, dict): - raise ReviewerWorkspaceError("WB_REVIEW_VALIDATOR_DENIED", {"classification": "denied"}) - if validator.get("kind") == "command": - argv = validator.get("argv") - if not isinstance(argv, list) or not argv or not all(isinstance(item, str) and item for item in argv): - raise ReviewerWorkspaceError("WB_REVIEW_VALIDATOR_DENIED", {"classification": "denied"}) - completed = run_sandboxed_validator(workspace, argv) - if completed.returncode: - if _sandbox_denied(completed): - raise ReviewerWorkspaceError( - "WB_REVIEW_SANDBOX_DENIED", - {"classification": "denied", "validator_id": validator_id, "exit_code": completed.returncode}, - ) - return { - "status": "allowed", - "validator_id": validator_id, - "result": "failed", - "exit_code": completed.returncode, - "stdout_sha256": _sha256_bytes(completed.stdout.encode("utf-8")), - "stderr_sha256": _sha256_bytes(completed.stderr.encode("utf-8")), - } - return { - "status": "allowed", - "validator_id": validator_id, - "result": "passed", - "exit_code": 0, - "stdout_sha256": _sha256_bytes(completed.stdout.encode("utf-8")), - "stderr_sha256": _sha256_bytes(completed.stderr.encode("utf-8")), - } - locator = str(validator.get("artifact") or "") - record = indexes["artifacts"].get(locator) - target = _evidence_path(workspace, locator) - if not isinstance(record, dict) or _sha256_bytes(target.read_bytes()) != record.get("sha256"): - raise ReviewerWorkspaceError("WB_REVIEW_EVIDENCE_MUTATED") - if validator.get("kind") == "json": - try: - json.loads(target.read_text(encoding="utf-8")) - except json.JSONDecodeError: - return {"status": "allowed", "validator_id": validator_id, "result": "failed"} - result = "passed" - elif validator.get("kind") == "sha256": - result = str(record["sha256"]) - else: - raise ReviewerWorkspaceError("WB_REVIEW_VALIDATOR_DENIED", {"classification": "denied"}) - return {"status": "allowed", "validator_id": validator_id, "result": result} - raise ReviewerWorkspaceError("WB_REVIEW_OPERATION_DENIED", {"classification": "denied"}) - -def execute_reviewer_request(workspace: Path, request: dict[str, object]) -> dict[str, object]: - try: - return _execute_reviewer_request(workspace, request) - except ReviewerWorkspaceError as error: - _append_denial_event(workspace, error, request.get("operation")) - raise - -def cleanup_reviewer_workspace( - runtime_root: Path, - review_id: str, - *, - terminal_review: dict[str, object] | None = None, - source_root: Path | None = None, - control_root: Path | None = None, - protected_roots: list[Path] | None = None, - terminal_evidence: str | None = None, -) -> dict[str, object]: - workspace, state_path = _workspace_paths(runtime_root, review_id) - if terminal_evidence is not None or not isinstance(terminal_review, dict): - raise ReviewerWorkspaceError("WB_REVIEW_TERMINAL_RECORD_INVALID") - if not state_path.is_file() or state_path.is_symlink(): - raise ReviewerWorkspaceError("WB_REVIEW_PROVENANCE_MISSING") - try: - state = json.loads(state_path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError): - raise ReviewerWorkspaceError("WB_REVIEW_PROVENANCE_INVALID") from None - if ( - not isinstance(state, dict) - or state.get("owner") != "work-bundle" - or state.get("review_id") != review_id - or state.get("workspace_token") != f"reviews/{review_id}" - or not workspace.is_dir() - or workspace.is_symlink() - ): - raise ReviewerWorkspaceError("WB_REVIEW_PROVENANCE_INVALID") - required_terminal = {"schema", "review_id", "packet_sha256", "verdict", "evidence_digest", "sentinel_digest"} - if ( - set(terminal_review) != required_terminal - or terminal_review.get("schema") != "reviewer-terminal-review-v1" - or terminal_review.get("review_id") != review_id - or terminal_review.get("packet_sha256") != state.get("packet_sha256") - or terminal_review.get("verdict") not in TERMINAL_VERDICTS - ): - raise ReviewerWorkspaceError("WB_REVIEW_TERMINAL_RECORD_INVALID") - packet_path = workspace / "packet.json" - try: - packet = json.loads(packet_path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError): - raise ReviewerWorkspaceError("WB_REVIEW_PROVENANCE_INVALID") from None - if _canonical_digest(packet) != state.get("packet_sha256"): - raise ReviewerWorkspaceError("WB_REVIEW_PROVENANCE_INVALID") - evidence_digest = _artifact_digest(workspace, packet) - if source_root is None or control_root is None: - raise ReviewerWorkspaceError("WB_REVIEW_TERMINAL_RECORD_INVALID") - protected = _normalized_roots(protected_roots or []) - supplied_root_identities = _root_identity_digests( - source_root.expanduser().resolve(), control_root.expanduser().resolve(), protected - ) - if supplied_root_identities != state.get("root_identity_digests"): - raise ReviewerWorkspaceError("WB_REVIEW_ROOT_IDENTITY_MISMATCH") - sentinel_digest = _sentinel_digest( - source_root.expanduser().resolve(), - control_root.expanduser().resolve(), - protected, - list(packet.get("sentinels", [])), - ) - if ( - evidence_digest != state.get("evidence_digest") - or sentinel_digest != state.get("sentinel_digest") - or terminal_review.get("evidence_digest") != evidence_digest - or terminal_review.get("sentinel_digest") != sentinel_digest - ): - raise ReviewerWorkspaceError("WB_REVIEW_TERMINAL_EVIDENCE_CHANGED") - terminal_record_digest = _canonical_digest(terminal_review) - receipt = { - "schema": "reviewer-workspace-cleanup-v1", - "review_id": review_id, - "owner": "work-bundle", - "packet_sha256": state["packet_sha256"], - "terminal_review_sha256": terminal_record_digest, - "evidence_digest": evidence_digest, - "sentinel_digest": sentinel_digest, - "status": "cleaned", - } - resolved_runtime = runtime_root.expanduser().resolve() - receipt_path = (resolved_runtime / "receipts" / f"{review_id}.json").resolve(strict=False) - if not _inside(resolved_runtime, receipt_path): - raise ReviewerWorkspaceError("WB_REVIEW_RUNTIME_PATH_ESCAPE") - receipt_path.parent.mkdir(parents=True, exist_ok=True) - pending = {**receipt, "status": "cleanup-pending"} - receipt_path.write_text(json.dumps(pending, indent=2, sort_keys=True) + "\n", encoding="utf-8") - shutil.rmtree(workspace) - state_path.unlink() - receipt_path.write_text(json.dumps(receipt, indent=2, sort_keys=True) + "\n", encoding="utf-8") - return {**receipt, "receipt_path": str(receipt_path)} +def main() -> int: + raise SystemExit("no public reviewer-process command; use orch.py write-implementation-review") -def cmd_reviewer_workspace(command: str, argv: list[str]) -> int: - parser = argparse.ArgumentParser(prog=f"wb.py {command}") - parser.add_argument("--runtime-root", required=True) - parser.add_argument("--review-id", required=True) - if command == "reviewer-workspace-create": - parser.add_argument("--packet", required=True) - elif command == "reviewer-process-run": - parser.add_argument("--argv-json", required=True) - elif command == "reviewer-workspace-operation": - parser.add_argument("--request", required=True) - elif command == "reviewer-workspace-cleanup": - parser.add_argument("--terminal-review", required=True) - parser.add_argument("--source-root", required=True) - parser.add_argument("--control-root", required=True) - parser.add_argument("--protected-root", action="append", required=True) - else: - raise ReviewerWorkspaceError("WB_REVIEW_COMMAND_INVALID") - args = parser.parse_args(argv) - runtime_root = Path(args.runtime_root) - if command == "reviewer-workspace-create": - result = create_reviewer_workspace(runtime_root, args.review_id, json.loads(Path(args.packet).read_text(encoding="utf-8"))) - elif command == "reviewer-process-run": - argv_value = json.loads(Path(args.argv_json).read_text(encoding="utf-8")) - if not isinstance(argv_value, list): - raise ReviewerWorkspaceError("WB_REVIEW_COMMAND_INVALID") - workspace, _ = _workspace_paths(runtime_root, args.review_id) - result = run_sandboxed_reviewer(workspace, argv_value) - elif command == "reviewer-workspace-operation": - workspace, _ = _workspace_paths(runtime_root, args.review_id) - result = execute_reviewer_request(workspace, json.loads(Path(args.request).read_text(encoding="utf-8"))) - else: - result = cleanup_reviewer_workspace( - runtime_root, - args.review_id, - terminal_review=json.loads(Path(args.terminal_review).read_text(encoding="utf-8")), - source_root=Path(args.source_root), - control_root=Path(args.control_root), - protected_roots=[Path(value) for value in args.protected_root], - ) - print(json.dumps(result, sort_keys=True)) - return 0 +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/work-bundle/stage_events.py b/scripts/work-bundle/stage_events.py index 7240393..f64b8ad 100644 --- a/scripts/work-bundle/stage_events.py +++ b/scripts/work-bundle/stage_events.py @@ -1,4 +1,8 @@ -"""Append-only, privacy-safe stage telemetry for WorkBundle orchestration.""" +"""Append-only, privacy-safe diagnostic telemetry. + +Stage events report operational facts only. They never issue or reinterpret a +product-review verdict, artifact qualification, or lifecycle decision. +""" from __future__ import annotations @@ -56,7 +60,6 @@ "allocated_skill_bytes", "capability_projection_bytes", "evidence_projection_bytes", - "review_package_bytes", "omitted_by_reference_bytes", "expansion_reason", } diff --git a/scripts/work-bundle/workspace_resources.py b/scripts/work-bundle/workspace_resources.py index 74435c4..c87ba3e 100644 --- a/scripts/work-bundle/workspace_resources.py +++ b/scripts/work-bundle/workspace_resources.py @@ -1,14 +1,10 @@ from __future__ import annotations -import ast from pathlib import Path import re from typing import Any -try: - import yaml -except ImportError: # The toolkit remains usable in the dependency-free runtime. - yaml = None +import yaml SCRIPT_INDEX_TEMPLATE = '''version: 1 @@ -83,99 +79,7 @@ def ensure_workspace_resources(workspace_root: Path, *, create_script_index: boo } -def _scalar(value: str) -> object: - value = value.strip() - if value == '[]': - return [] - if value.startswith('[') and value.endswith(']'): - body = value[1:-1].strip() - if not body: - return [] - return [_scalar(item) for item in body.split(',')] - if value in {'true', 'false'}: - return value == 'true' - if re.fullmatch(r'-?\d+', value): - return int(value) - if value.startswith(('"', "'")): - parsed = ast.literal_eval(value) - if not isinstance(parsed, str): - raise ValueError('quoted scalar is not a string') - return parsed - if any(token in value for token in ('{', '}')) or value.startswith(('&', '*', '!')): - raise ValueError('unsupported YAML token') - return value - - -def _fallback_yaml_load(text: str) -> object: - """Parse the closed, indentation-based YAML subset used by this contract.""" - lines: list[tuple[int, str]] = [] - for raw in text.splitlines(): - if '\t' in raw: - raise ValueError('tabs are forbidden') - stripped = raw.strip() - if not stripped or stripped.startswith('#'): - continue - indent = len(raw) - len(raw.lstrip(' ')) - if indent % 2: - raise ValueError('indentation must use pairs of spaces') - lines.append((indent, stripped)) - - def parse(index: int, indent: int) -> tuple[object, int]: - if index >= len(lines) or lines[index][0] != indent: - raise ValueError('invalid indentation') - if lines[index][1].startswith('- '): - result: list[object] = [] - while index < len(lines) and lines[index][0] == indent and lines[index][1].startswith('- '): - item = lines[index][1][2:].strip() - index += 1 - if ':' in item: - key, raw_value = item.split(':', 1) - mapping: dict[str, object] = {key.strip(): _scalar(raw_value) if raw_value.strip() else None} - if index < len(lines) and lines[index][0] > indent: - nested, index = parse(index, lines[index][0]) - if not isinstance(nested, dict) or set(mapping) & set(nested): - raise ValueError('invalid list mapping') - mapping.update(nested) - result.append(mapping) - elif item: - result.append(_scalar(item)) - elif index < len(lines) and lines[index][0] > indent: - nested, index = parse(index, lines[index][0]) - result.append(nested) - else: - raise ValueError('empty list item') - return result, index - - result_map: dict[str, object] = {} - while index < len(lines) and lines[index][0] == indent and not lines[index][1].startswith('- '): - item = lines[index][1] - if ':' not in item: - raise ValueError('mapping item lacks colon') - key, raw_value = item.split(':', 1) - key = key.strip() - if not key or key in result_map: - raise ValueError('empty or duplicate mapping key') - index += 1 - if raw_value.strip(): - result_map[key] = _scalar(raw_value) - elif index < len(lines) and lines[index][0] > indent: - result_map[key], index = parse(index, lines[index][0]) - else: - result_map[key] = None - return result_map, index - - if not lines: - raise ValueError('empty YAML') - parsed, end = parse(0, lines[0][0]) - if lines[0][0] != 0 or end != len(lines): - raise ValueError('trailing or indented root content') - return parsed - - def _load_yaml(text: str) -> object: - if yaml is None: - return _fallback_yaml_load(text) - class UniqueKeyLoader(yaml.SafeLoader): pass @@ -201,7 +105,7 @@ def validate_script_index(workspace_root: Path) -> list[str]: return ['WB_SCRIPT_INDEX_MISSING'] try: document = _load_yaml(path.read_text(encoding='utf-8')) - except (ValueError, TypeError, SyntaxError, getattr(yaml, 'YAMLError', ValueError) if yaml else ValueError): + except (ValueError, TypeError, SyntaxError, yaml.YAMLError): return ['WB_SCRIPT_INDEX_YAML_INVALID'] if not isinstance(document, dict) or set(document) != {'version', 'entry_contract', 'scripts'}: return ['WB_SCRIPT_INDEX_INVALID'] diff --git a/skills/orch-create-handoff/SKILL.md b/skills/orch-create-handoff/SKILL.md index 32c4852..5a0d4fe 100644 --- a/skills/orch-create-handoff/SKILL.md +++ b/skills/orch-create-handoff/SKILL.md @@ -1,110 +1,26 @@ --- name: orch-create-handoff -description: 'Create compact executor-result handoffs for continuation or evidence.' +description: Create a canonical factual executor result for safe WorkBundle continuation without issuing product acceptance. --- -# orch-create-handoff +# Create an Executor Result -## Scope +Use this skill after task execution or when a current executor result must be repaired. The current machine boundary is `executor-result-v1`; do not create orchestration handoffs or convert historical artifacts. -Create compact executor-result handoffs for continuation or evidence. Orchestration handoffs are legacy artifacts only and are not created by the active workflow. +## Workflow -## Workflow Reference +1. Confirm the exact plan/task bindings, identity, lifecycle operation, and canonical catalog location. +2. Summarize only factual implemented scope, changed paths, validation observations, unresolved product blockers, task fit, repository/CodeGraph observations, delegation provenance, and task-local knowledge disposition. +3. Validate the full semantic input, bindings, identity, canonical path, collision state, and requested transition before mutation. +4. Create one immutable YAML artifact atomically. Treat the derived index as a regenerable projection. +5. After creation, perform only lightweight integrity checks. If index rebuild fails after the artifact write, report the partial effect truthfully. -Use `references/assets/orchestration/workflow.md` as the shared workflow authority. +A malformed or missing executor result blocks only continuation that requires it. An independent reviewer may still judge an exact reviewable product candidate from the verified specification/plan, frozen implementation identity, and focused observations. -## Context +## Self-check -Inspect relevant `.work-bundle/orchestration/` specs, plans, phases, tasks, previous executor-result handoffs, test results, and current execution state. - -Executor-result handoffs created during `execute-plan` must not retrieve durable knowledge. They may use only carried spec, plan, phase, task, declared handoff, and task-scoped source/test context. - -Do not include raw chat logs, speculative reasoning, or unrelated history. Record missing, stale, contradictory, or uncertain context as assumptions, risks, or open questions. - -## Types - -- `executor-result`: implementation result from an executor agent to the orchestration agent. -- `orchestration`: legacy-only historical handoff type. Do not create new active orchestration handoffs from this workflow. - -Infer type from purpose when absent. - -## Output Layout - -```text -.work-bundle/orchestration/handoff/ - executor/active/handoff-exec-YYYYMMDD-001-[slug].yaml - orchestration/archived/ - executor/archived/ - index.jsonl -``` - -Handoffs are orchestration artifacts, not durable project knowledge. - -Use sparse YAML by default for executor-result handoffs. Existing archived Markdown handoffs remain legacy-compatible and indexable; do not convert them unless explicitly scoped. - -## Required Content - -Executor-result handoffs follow `rules/orchestration/orch-handoff-required.md` and `references/assets/orchestration/contract/handoff-executor-result-v1.md`. - -Always include identity, related artifacts, result state, and concise summary. Include other fields only when applicable for continuation or review: - -- `changes.files` for changed or inspected files, symbols, artifacts, schemas, commands, or docs. -- `validation.commands` for commands, tests, inspections, or intentional skips. -- `unresolved` only for remaining blockers or issues. -- `task_fit_check` for completed or partial task results, covering the compiled task brief and assigned task. Escalate to full source artifacts only for inconsistent compiled context or a reviewer-reported source-contract problem. -- `repository` when repository preflight, accepted baseline, changed paths, or blocker state matters. -- `codegraph` when source-code inspection or edits were in scope; keep it to `root`, `applicable`, `up_to_date`, and fallback/blocker facts unless more detail is needed. -- `delegation_evidence` for task ownership; record only delegated state, `owner_kind: subagent`, minimum agent/run identity, and provider-neutral mechanism. - -## Hard Rules - -- Do not store handoffs under `.work-bundle/knowledge/`. -- Do not create new active `handoff-orch-*` artifacts or offer orchestration handoff creation as an active workflow path. -- Do not implement source changes, edit application/test files, run migrations, apply patches, or execute plan tasks while creating a handoff. -- Create the handoff only for the initial executor result. If execution is already authorized, resume the owning workflow after that handoff without demanding repeated permission. Already accepted tasks consume their compact accepted result and do not create another handoff merely to acquire review, publication, or finalization facts. -- Do not include raw chat logs, private reasoning, or unrelated history. -- Do not include durable-knowledge recommendations, orchestration review recommendations, executor advice fields, or strategy advice in executor-result handoffs. -- Stop if source artifact paths or current state are unknown. -- Executor-result handoffs must list changed files or inspected artifacts, validation, unresolved blockers when present, and compact `task_fit_check` when applicable. -- Executor-result handoffs must not omit applicable `codegraph:` or task `delegation_evidence:` and must not include UI, visibility, fallback, or controller-ownership fields. - -## Status and Index - -Statuses: - -```text -active | reviewed | archived | superseded -``` - -Update `.work-bundle/orchestration/handoff/index.jsonl` with `id`, `type`, `status`, `path`, `project`, `created_at`, `updated_at`, `related_spec`, `related_plan`, `related_phase`, and `related_task`. Use `null` for unavailable relationships. - -## Contracts - -Load only when creating or validating: - -- `references/assets/orchestration/contract/handoff-executor-result-v1.md` -- `references/assets/orchestration/contract/handoff-orchestration-v1.md` only for explicit legacy validation of existing orchestration handoffs. - -## Validation - -Confirm required sparse YAML metadata exists, referenced specs/plans/phases/tasks/files are listed when applicable, unresolved blockers are explicit when present, executor-result fields are complete by applicability rather than fixed section presence, forbidden executor advice fields are absent, applicable `codegraph:` evidence includes compact up-to-date or fallback/blocker facts, task executor-result handoffs include neutral `delegation_evidence` with agent/run identity and mechanism, raw chat is excluded, no handoff is written under `.work-bundle/knowledge/`, no active orchestration handoff is created, and execution-completion handoffs did not invoke retrieval. - -## Runtime Rules - -- `orch-orchestration-boundary`: `rules/orchestration/orch-orchestration-boundary.md` -- `orch-artifact-authoring`: `rules/orchestration/orch-artifact-authoring.md` -- `orch-handoff-required`: `rules/orchestration/orch-handoff-required.md` - -Central `AGENTS.md` owns rule discovery and loading. Load the runtime rules above when their indexed conditions apply. - -## Scripts - -Use `scripts/orch.py` when deterministic helper behavior is needed. - -## Additional References - -- `references/assets/orchestration/contract/` - -## Boundary - -Platform write boundary and durable-knowledge prohibition: follow `orch-orchestration-boundary` (`rules/orchestration/orch-orchestration-boundary.md`). +- [ ] The artifact is at the catalog-selected path and bound to the exact plan/task. +- [ ] The content is factual and contains no verdict, acceptance, final-audit, repair-advice, or knowledge-write fields. +- [ ] Structural validation and collision checks completed before mutation. +- [ ] Post-write work was limited to integrity and index projection checks. +- [ ] Any partial effect or separate supporting-state defect is reported without changing product meaning. diff --git a/skills/orch-create-implementation-plan/SKILL.md b/skills/orch-create-implementation-plan/SKILL.md index 0c49a07..4f9319f 100644 --- a/skills/orch-create-implementation-plan/SKILL.md +++ b/skills/orch-create-implementation-plan/SKILL.md @@ -7,7 +7,15 @@ description: 'Create executable WorkBundle plans, phases, and tasks from a verif ## Entry gate -Plan only from a verified active specification with converged semantics, resolved blockers, stable source IDs, explicit knowledge disposition, and coherent repository evidence. Repair missing authority. Compile only verified authoritative scope. Do not allocate `EXC-*` proposal IDs or rejected, deferred, or not-material excellence proposals as `source_ids`, task scope, or executor briefs. Accepted excellence work enters planning only through the stable requirement, constraint, interface, acceptance-criterion, or validation-target IDs it projected to. +Use this skill when a verified specification needs an executable WorkBundle plan tree. Plan only from one canonical active `verified` specification with converged semantics, stable source IDs, resolved material conflicts, explicit knowledge disposition, and coherent current repository evidence. Repair missing specification authority before planning. Do not use this skill for lightweight coding plans or post-execution review/finalization. + +Compile only verified authoritative scope. Do not allocate `EXC-*` proposal IDs or rejected, deferred, or not-material excellence proposals as `source_ids`, task scope, or executor briefs. Accepted excellence work enters planning only through the stable requirement, constraint, interface, acceptance-criterion, or validation-target IDs it projected to. + +## Canonical output + +Author semantic YAML for the `root-plan`, `phase`, and `task` families. Invoke `write-plan`, `write-phase`, and `write-task`; the shared store injects family/schema identity, IDs, qualification/status, dates, and parent bindings and selects `.plan.yaml`, `.phase.yaml`, and `.task.yaml` canonical locations. Do not choose filenames, embed structural overrides, infer parents from directories, or create Markdown compatibility copies. + +The root plan binds `source_spec_id`. Every phase binds `plan_id`. Every task binds both `plan_id` and `phase_id`. Use one explicit default phase when no actual barrier or convergence split is needed. ## Planning workflow @@ -20,8 +28,11 @@ Plan only from a verified active specification with converged semantics, resolve 7. When a consequential simplification or compatibility assumption exists, make the earliest ordinary task cheaply falsify it before broad edits. Do not add a risk score, checkpoint phase, or parallel lifecycle. 8. When execution proves a task materially under-decomposed, return to the plan and reslice only the affected region around the newly evidenced seam. Preserve the original binding, baseline, and accepted unaffected regions; do not repeatedly enlarge the task. 9. Use the canonical semantic plan projection for review identity and freshness; status-only or append-only evidence changes do not require plan review or reslicing, while authority, scope, dependency, acceptance, decomposition, or validation-allocation changes do. -10. Before plan acceptance, invoke canonical static task admission for every task through the task compiler. Do not duplicate its admission predicates in the planner. -11. Keep planning revision policy separate from post-execution control: plan and specification revisions do not consume post-execution review rounds. Do not allocate a plan-version counter, reviewer retry, task review, or publication retry against the workspace round limit. When planning remediation for an existing flow, preserve its stable flow identity and let shared admission refuse exhausted or workspace-blocked reconciliation. +10. Before semantic review, invoke canonical static task admission for every task through the task compiler. Treat schema, family, canonical placement, parent binding, dependency, scope, authority-alias, and validation-shape results as structural facts only. Do not duplicate its predicates in the planner or let it decide semantic completeness. +11. Give a distinct reviewer the verified specification, the concrete canonical plan tree, and bounded current source evidence. The reviewer maps every accepted requirement, constraint, interface, acceptance criterion, validation target, accepted `DEC-*`, and resolved stable open-question outcome to production ownership and capable validation, then issues `accept`, `repair`, or `blocked` directly. Tests, doctors, indexes, receipts, handoffs, and evidence volume do not issue this semantic verdict. Repair findings at the first owning plan/phase/task layer and re-review the exact repaired tree once. +12. Keep planning qualification (`draft`, `verified`, `superseded`) separate from execution state and finalization. Phase/task execution states, handoff/review receipt gates, plan completion, and archive/finalization belong to the downstream execution/review stage. + +Current plan and specification revisions do not consume post-execution review rounds. ## Methodology allocation @@ -102,3 +113,15 @@ Central `AGENTS.md` owns rule discovery and loading. Load the runtime rules abov ## Boundary Follow `orch-orchestration-boundary`. Do not read durable knowledge directly during downstream execution. + +## Self-check + +Before returning a plan candidate, confirm from the stored YAML and compiler output that: + +- the root plan binds exactly one canonical active verified specification and every phase/task has exact canonical parent bindings; +- every accepted specification obligation has explicit phase/task ownership and validation IDs where validation-bearing; +- every authoritative production path has one production owner, exact files/symbols, dependencies, steps, methodology, rules/skills, capable oracle, and measurable completion criteria; +- every phase is justified by an actual barrier or convergence boundary, or the tree uses one explicit default phase; +- static admission passes without legacy Markdown, fallback filenames, broad scans, or a second persisted combined index; +- a distinct reviewer directly judged coverage, decomposition, ownership, dependencies, validation, authority, scope, and executability and the stored qualification does not exceed that verdict; +- no task/phase completion, handoff review, receipt, finalization, or archive semantics were added during planning. diff --git a/skills/orch-create-specification/SKILL.md b/skills/orch-create-specification/SKILL.md index 73bf75d..ad022d4 100644 --- a/skills/orch-create-specification/SKILL.md +++ b/skills/orch-create-specification/SKILL.md @@ -33,6 +33,20 @@ Use `existing` for small/manual work, `preferred` for autonomous multi-task work Specification repair remains pre-execution authoring: plan and specification revisions do not consume post-execution review rounds. A residual specification created for forced closure preserves unresolved claims and clean source-baseline identities; it does not reopen the exhausted flow or authorize another review round. +11. Materialize the current artifact through `scripts/orch.py write-spec`. Supply semantic front matter and the human-readable body; the specification family schema owns structural fields, canonical `.work-bundle/orchestration/spec/active/<id>.spec.md` location, parsing, atomic writes, and the derived index. Do not select a filename or copy a legacy specification path. + +## Independent semantic review + +Before marking the specification `verified`, give the exact candidate, original user purpose, accepted authority, and current workspace evidence to a distinct reviewer agent. The reviewer directly decides whether the specification: + +- matches the user purpose and accepted authority; +- covers all requirements, constraints, interfaces, acceptance criteria, validation targets, and open questions; +- resolves material conflicts without promoting non-authority evidence; +- uses the current specification family and workspace-root anchor; and +- stays within scope without absorbing later implementation stages. + +Repair concrete findings at their first owning layer, then ask the reviewer to check the repaired candidate. Supporting evidence files do not issue the semantic verdict. Missing ceremony, receipts, indexes, handoffs, or perfect evidence files is not a semantic rejection unless the candidate becomes ambiguous, unsafe, unreadable, or impossible to review. + ## Semantic convergence Use `dev-semantic-convergence` with these lenses: @@ -77,3 +91,11 @@ Central `AGENTS.md` owns rule discovery and loading. Load the runtime rules abov ## Boundary Follow `orch-orchestration-boundary`. + +## Self-check + +- [ ] The specification preserves the user purpose and only accepted authority shapes requirements. +- [ ] Requirements, constraints, interfaces, acceptance criteria, validation targets, conflicts, scope boundaries, and open questions are complete and mutually consistent. +- [ ] The artifact uses the canonical `.spec.md` family path and contains no caller-authored structural override or filename. +- [ ] A distinct reviewer judged the concrete semantics directly; scripts and supporting state supplied structural evidence only. +- [ ] The quality gate, semantic loop, execution-workspace policy, and Knowledge Base Update disposition agree. diff --git a/skills/orch-execute-plan/SKILL.md b/skills/orch-execute-plan/SKILL.md index 55b0240..4089cb0 100644 --- a/skills/orch-execute-plan/SKILL.md +++ b/skills/orch-execute-plan/SKILL.md @@ -1,98 +1,28 @@ --- name: orch-execute-plan -description: 'Execute a WorkBundle task, phase, or plan through mandatory subagent task ownership, compiled task briefs, task-local methodology, optional independent acceptance review, and dependency-aware scheduling.' +description: Execute verified WorkBundle tasks and produce factual canonical executor results for direct independent product review. --- -# orch-execute-plan +# Execute a WorkBundle Plan -## Execution Constraints (skill-owned) +Execute only the task authority supplied by the controller. Use its verified specification, canonical plan/task, compiled Truth Basis, accepted dependency results, allocated rules, and task-local methodology. Do not retrieve durable knowledge during execution or broaden repository/write scope. -Execution is a no-retrieval stage. Use the selected task, its compiled Truth Basis and cited specification values, declared prior handoffs, task-scoped source/tests, and allocated methodology. Do not query or read `.work-bundle/knowledge/`. +## Execution -## Scheduler-Owned Constraints +1. Verify the exact task, bindings, source baseline, write scope, dependencies, and validation obligations before mutation. +2. Follow the task methodology. For behavior changes, use GROUND → RED → GREEN → REFACTOR and retain focused observations that can disprove the claim. +3. Keep each worker inside its existing task ownership. A worker reports source changes and factual observations; it does not accept the product. +4. Freeze the resulting commit or worktree candidate with a path-sorted changed-path manifest. +5. Write one canonical `executor-result-v1` with implemented scope, changed paths, focused observations, unresolved product blockers, task fit, repository/CodeGraph facts, delegation provenance, and knowledge disposition. +6. When review is required, send the exact candidate, verified specification and plan, every obligation, and focused observations to a distinct reviewer. Supporting-state defects are routed separately unless the product itself is ambiguous, unsafe, inaccessible, or impossible to review. +7. On `repair`, resume the owning task and review the new exact candidate. On `blocked`, report the product blocker. On `accept`, let the controller create the compact accepted task result. -1. Resolve target task, phase, or plan and its executable dependency queue. -2. Before compilation, capability selection, delegation, or edits, resolve the workspace and every target repository from `.work-bundle/project.yaml`. Git-backed targets must match branch and accepted metadata baseline and be clean unless validated handoffs explain exact changes. Never mutate user work to pass preflight. -3. Record CodeGraph applicability per target. When `.codegraph/` exists for indexed source work, sync after preflight and query it before broad inspection; recheck cleanliness and sync after changes. Otherwise record `no-index` and use bounded direct inspection. Do not initialize CodeGraph. -4. Select or prepare the declared execution workspace and hydration profile. Record provenance. Cleanup may remove only a clean WorkBundle-owned workspace whose expected Git identity still matches, whose policy allows cleanup, and whose durable lifecycle state confirms integration or an explicit discarded/retired decision. Age alone is report-only; never delete user or harness workspaces. -5. After scheduler workspace selection or preparation and before material edits, create or load one harness-owned task execution binding that carries plan/task identity, `workspace_id`/`execution_id`/`repository_id`, and exact path/Git provenance. Keep it in runtime/execution-workspace state outside the mutation envelope. Compile and read task/spec artifacts from the control WorkBundle root. Execute process and Git evidence against the bound execution repository. Do not point orchestration `--project-root` at an isolated worktree to load gitignored `.work-bundle/orchestration/**`. Capture the pre-task baseline once from that bound repository via the helper; later brief rebuild or repair must not recapture or replace it. Executor handoff and other supported executor-facing interfaces cannot supply or replace that baseline. Same-user filesystem rewrite of helper runtime files is out of scope. Mutating siblings on the same execution path isolate via prepare_worktree or serialize even when write scopes are disjoint; a shared worktree must not host them. Do not add a path-ownership ledger. -6. Compile the bounded task brief: +Executor results must not contain product verdicts, recommended repair strategy, final-audit conclusions, or knowledge-write authorization. Missing historical records, indexes, or handoffs do not become product findings. -```bash -python3 scripts/orch.py build-task-brief --task <task-path> -``` +## Self-check -Missing source IDs; decision authority other than `none-relevant` or an `AUTH-NNN` alias whose carried constraint was reconciled in the verified specification; `conflict_status: escalate`; inconsistent scope; or unsafe workspace state fails closed with the existing typed blocker. Truth Basis conflict uses `decision-blocked`. The compiled brief includes `AUTH-NNN: <carried constraint>`, not the alias alone. -7. Choose the provider-neutral capability from the task profile. Consume planner-proven dependencies, write scopes, common-contract groups, barriers, convergence ownership, and isolation requirements. Dispatch every ready independent task with disjoint write scope to a separate execution workspace before awaiting any result. Serialize dependent, overlapping, or same-workspace mutation. Contract-decoupled participants validate against the common contract, accepted prior handoffs, and task-local files; they reach the named barrier before convergence work. -8. Selecting `orch-execute-plan` makes every implementation and repair task subagent-owned. Use the production `TaskOwnershipScheduler` entry in `scripts/orchestration/task_ownership.py` with a host-native adapter or an optional Execution-Flow adapter; host-native execution is sufficient and Execution Flow is optional. Do not reproduce its admission logic in prompts or detached helpers. Before any task mutation, it confirms adapter availability and binds each dispatched task to validated neutral agent/run provenance. If no subagent is available, it fails closed with `workspace-blocked`; there is no controller or single-agent fallback. Do not substitute `reviewer_independent: false` for a missing task owner. The orchestration thread may schedule, compile briefs, coordinate barriers, validate results, route reviews, and manage lifecycle, but it must not implement or repair task write scope. -9. Use `TaskOwnershipScheduler.validate_acceptance` to validate neutral `delegation_evidence` and reject acceptance when mutation provenance shows the controller changed task-owned write scope, even if validation is green. Run the pure creation-safe projection before the helper atomically writes/indexes the immutable executor result; it rejects malformed executor facts and wrong-owner review/control fields without observing, reviewing, or accepting. For a mapped capability task, report each validation under its compiled evidence ID and invariant IDs, and add `evidence_closure` using the allocated boundary, freshness, and evidence IDs. The later terminal helper observes required process/inspection items in the bound worktree as one Git-state-neutral batch, reuses the compiled identities, and rejects missing, incapable, contradictory, stale, wrong-boundary, failed, or unexecuted evidence before authorizing from post-execution task-caused delta. Executor closure claims are corroboration, not harness proof. Compile `build-review-package` and assign `dev-code-review` only when compiled `review_required: true`. The scheduler does not perform code-quality review. - -```bash -python3 scripts/orch.py validate-executor-result --task <task-path> --handoff <handoff-path> -``` - -## Executor-Owned Constraints - -- Follow the compiled brief and its exact read/write/forbidden scope. -- Own implementation and repair task mutation as the bound subagent; return neutral `agent_id`, `run_id`, and `mechanism` provenance without UI or visibility fields. -- Load or acknowledge allocated rules and methodology before the operation they govern. -- Create or load the harness-owned task execution binding before material edits; capture the pre-task baseline once; run process commands and named inspections only in the bound execution repository. -- Apply `systematic-debugging` before proposing a root-cause fix for unexpected behavior. -- Apply TDD to testable new/changed behavior and diagnosed fixes; use direct deterministic verification for non-testable mechanical artifacts. -- Run fresh claim-relevant validation after the final edit. -- Write a sparse `executor-result-v1` handoff containing only executor-owned task identity, changed paths, validation, repository/CodeGraph fallback, allocated obligations, unresolved blockers, local task-fit evidence, and a knowledge disposition of `none`, `update`, `supersede`, or `reclassify`. Omit review, receipt, publication, accepted-result, and later audit facts. -- Knowledge disposition contains task-local evidence only. It must not name knowledge paths, invoke any `ks-*` skill, or authorize persistence; final orchestration review owns approved follow-up. -- Do not perform acceptance judgment or mark a review-required task complete. -- Trigger `wb-defect-evaluation` only for a new unintended WorkBundle-related conflict, error, failed validation, or contradictory workflow behavior. Stop once visible relatedness is established; no chain-of-thought or exhaustive tracing is required. - -## Independent task review - -When compiled `review_required: true`, validate initial executor facts without demanding or embedding the future review verdict, then build the product candidate only from accepted product requirements/boundaries, exact base/current product source and diff identity, harness-owned normalized validation observations, and unresolved product concerns. Handoff, knowledge, reviewer-history, receipt/publication, status, and archive bookkeeping are not product-review inputs or findings. Controller/orchestration code is still product when the task allocates it. Skip this hop when review is not required. - -```bash -python3 scripts/orch.py build-review-package \ - --task <task-path> --handoff <handoff-path> --base <git-ref> --head <git-ref> -``` - -Use `--head worktree` for pre-commit review; the compiler includes tracked, staged, unstaged, and untracked changes, assigns a stable worktree identity, and withholds protected-path content. - -The reviewer uses only that bounded product candidate and `dev-code-review`, returning compact `accept|repair` product judgment. Invalid or incomplete input is a controller input/runner failure outside the product verdict. The controller composes the native envelope, verifies independent provenance, and publishes it. A task or stage verdict becomes lifecycle authority only after the exact result and its provider-specific reviewer-run receipt are stored and validated against controller-authorized target identity. - -If reviewer infrastructure or provider failure prevents a verdict, preserve the immutable package and repair the first broken runner/provider owner. A capable independent reviewer may be reused. Do not change source, rerun validation, reslice, or require identity rotation for provider availability. Publication retry after a completed judgment reuses the exact result and receipt. - -On `repair`, preserve reviewer observations verbatim while the controller supplies the classification, first broken owner or artifact, and selected action through the v2 agent-owned decision contract. Return blocking findings to the existing task owner only when that is the controller-selected route; there is no fixed class-to-remedy table or confirming review. Repair from the exact previously reviewed source, rerun only claim-relevant validation invalidated by the repair, and perform one scoped rereview of the affected frontier. Reuse unaffected executor and validation authority. Only a material authority, scope, acceptance, decomposition, or validation-allocation change resets to an initial frontier. Publication-only/control resume never redispatches the executor or reruns validation/review. - -## Completion semantics - -A task becomes `Completed` only when implementation criteria, fresh validation, neutral subagent ownership provenance, no controller mutation of task write scope, a valid immutable executor-result handoff, and a passing `validate-executor-result` check all exist. A review-required task additionally needs exact stored `accept` authority; accepted-result materialization alone joins it with executor facts and current observations. Phase and plan completion derive from accepted children and declared dependency, barrier, and convergence gates. - -Enforce acceptance once: after the helper verifies binding, source/scope, subagent ownership, validation, and stored required-review authority, persist one compact accepted result. A later stored task repair review recomposes that result through the existing materializer, preserving executor and validation authority without redispatch, handoff rewrite, validation rerun, or embedded review history. Dependency release and later lifecycle steps consume the compact result plus current harness observations; they do not replay transient acceptance evidence or historical handoff chains. - -Use typed blockers: - -```text -context-blocked | repository-blocked | decision-blocked | validation-blocked -review-blocked | knowledge-blocked | workspace-blocked -``` - -Resume the owning step. Do not restart the lifecycle or create a repair specification for an ordinary task rejection. - -## Post-execution bounded review - -Task review remains optional and task-scoped; it never consumes the workspace's post-execution review round budget. After every executor attempt for the flow is terminal and the integrated candidate is ready, the controller runs `begin-review-round` before preparing evidence or dispatching integrated review. The exact request ID plus frozen target identity is idempotent; a different target identity reserves a new round. - -After immutable product review publication, the controller runs `complete-review-round` with its store-owned review reference. When controller preparation or dispatch is factually blocked before a product artifact exists, completion may instead carry an audit-block record; that record must not impersonate a product verdict. Use `review-round-status` to diagnose the count and finalization state. - -An accepted round proceeds to the normal final audit. Findings below the fifth completed round return to the existing task owner through admission-controlled reconciliation and claim-relevant repair. The fifth unresolved or factually blocked completion stops repair and requires `finalize-with-blockers`; the executor must not run a sixth round or reopen product work during an administrative retry. - -## Runtime Rules - -- `orch-orchestration-boundary`: `rules/orchestration/orch-orchestration-boundary.md` -- `orch-handoff-required`: `rules/orchestration/orch-handoff-required.md` -- `orch-bounded-closure`: `rules/orchestration/orch-bounded-closure.md` - -Central `AGENTS.md` owns rule discovery and loading. Load the runtime rules above when their indexed conditions apply. - -## Boundary - -Follow `orch-orchestration-boundary`, `orch-handoff-required`, and `orch-bounded-closure`. +- [ ] The executed task and repository/write scope match accepted authority. +- [ ] The executor result is canonical, factual, schema-valid, and bound to the exact plan/task. +- [ ] Focused observations are current and claim-relevant; green tests do not substitute for checking every obligation. +- [ ] A distinct reviewer, not the executor or controller, authored any product verdict. +- [ ] No receipt, history replay, or supporting-state defect issued or changed a semantic verdict. diff --git a/skills/orch-review-plan/SKILL.md b/skills/orch-review-plan/SKILL.md index 18eb670..048d7a4 100644 --- a/skills/orch-review-plan/SKILL.md +++ b/skills/orch-review-plan/SKILL.md @@ -1,119 +1,30 @@ --- name: orch-review-plan -description: 'Audit WorkBundle workflow completion, task acceptance evidence, handoffs, knowledge disposition, repository finalization, and archive readiness after execution.' +description: Perform direct implementation review and compact final WorkBundle workflow review against exact verified authority and frozen candidates. --- -# orch-review-plan +# Review WorkBundle Implementation and Closure -## Review question +Use direct product review for implementation correctness and one compact final workflow review for closure. These are separate judgments. -Did the approved WorkBundle workflow complete correctly, with required optional reviews, declared plan-level/integration acceptance, handoffs, knowledge disposition, repository finalization, and archive readiness? +## Implementation review -This is a workflow audit and deterministic finalizer. Independent `dev-code-review` owns task-scoped implementation quality when review is explicitly required. +The reviewer must be distinct from the implementor. Compare the exact frozen commit or worktree candidate directly with the verified specification and canonical plan. Inspect every planned feature, acceptance obligation, edge/failure behavior, and claim-relevant focused observation. Passing tests cannot hide omitted behavior. -The audit has no mandatory Execution Flow dependency: host-native execution is sufficient and Execution Flow is optional. +Issue `accept`, `repair`, or `blocked` from product correctness. Missing or defective indexes, historical handoffs, knowledge state, or controller ceremony are separate supporting-state defects unless they make the actual product ambiguous, unsafe, inaccessible, or impossible to review. Store the verdict and findings in one canonical `implementation-review-v1`. -## Audit +## Accepted continuation -Verify: +After `accept`, the controller creates one `accepted-task-result-v1` that references the exact executor result, accepted implementation review when required, current validation outcomes, product identity, material defects, and knowledge disposition. Consumers reuse this compact decision; they do not reconstruct review history. -- specification, plan, phase, and task status coherence; -- executor-result handoffs by applicability; -- declared completion evidence corresponds to the compiled Truth Basis, source IDs, expected delta, and remaining AUTH constraints; -- every mapped invariant has capable, current, correctly bounded harness-observed evidence under its allocated INV/VAL identities; treat incapable green, contradiction, staleness, wrong-boundary, failure, missing, or unexecuted evidence as negative acceptance evidence and route first-owner repair: task repair for failed, stale, or unexecuted implementation evidence; plan repair for missing, wrong-boundary, or incapable allocation; specification repair for contradictory accepted authority; -- missing stored `accept` review authority blocks only a task whose compiled `review_required` is true; do not require universal task-review evidence or embedded handoff verdicts; -- exact stored `accept` authority only for those explicitly required reviews, joined during accepted-result materialization; -- declared plan-level/integration acceptance observed on the final integrated workspace; do not start another implementation-review agent to produce plan-level acceptance; -- aggregate accepted task dispositions before applying the final knowledge gate: any accepted `update`, `supersede`, or `reclassify` makes durable closure required even when the upstream specification said `not-needed`; accepted `none` and rejected task dispositions do not trigger closure; -- record validated delegate-return state in the root plan's existing Knowledge Base Update `Closure return` field so the deterministic `archive-plan` helper enforces the same aggregate gate; -- planned validation evidence exists and is fresh for the accepted task result; -- declared dependency, barrier, and convergence gates occurred; -- the resulting final Knowledge Base Update disposition is `completed` or `not-needed` before archive; -- approved `ks-*` return evidence exists when durable knowledge was required; -- allowed commit, applicable CodeGraph sync, metadata update, archive, and index refresh completed or are explicitly not applicable. -- dependency, finalization, resume, and archive decisions consume compact accepted results and current harness observations without replaying transient evidence or historical handoff chains. -- required task and stage verdicts are strongly checked with provider-specific reviewer-run receipts at publication, then later admitted from immutable direct current-authority bindings and still-current targets without receipt or predecessor replay; bare output, unattached receipts, and bare findings are not lifecycle authority. -- product findings concern accepted product requirements/boundaries, exact product source/diff, normalized harness observations, and unresolved product concerns; handoff, knowledge disposition, reviewer history, and publication/status/archive bookkeeping stay with controller audit. +## Final workflow review -## Evidence capability correspondence +A distinct final auditor checks only plan/task coverage, accepted implementation verdicts, relevant current test outcomes, unresolved material defects, knowledge disposition/return, repository finalization facts, and truthful archive readiness. Do not reread source for code quality or repeat implementation review. Write one `final-workflow-review-v1`; deterministic finalization validates canonical references, lifecycle state, clean baselines, archive destinations, indexes, and binding release without inventing or reinterpreting the verdict. -Before archive or completion, every accepted validation-bearing invariant must have a compiled `evidence_capability` entry and capable, current, correctly bounded harness-observed evidence under its allocated INV/VAL identities. Incapable green, contradiction, staleness, wrong-boundary, failure, missing, or unexecuted evidence is negative acceptance evidence, not closure. +## Self-check -Use `no_validation_bearing_obligation + reason` only when no accepted validation-bearing obligation or design decision exists. Do not infer an empty evidence-capability map from a WOR-61 `none_relevant` impact result. - -Route first-owner repair for this pre-closure oracle-capability check: task repair for failed, stale, or unexecuted implementation evidence; plan repair for missing, wrong-boundary, or incapable allocation; specification repair for contradictory accepted authority. Mechanical helpers validate IDs, completeness, provenance, and observed results; agents own semantic capability judgment. This is not a universal browser, E2E, production, or runtime gate. - -Keep this pre-closure oracle-capability check distinct from `RuntimeVerificationClassificationV1`. WOR-59 G9 remains the unchanged post-execution classifier and may use this map only as evidence when triggered. - -## Runtime verification classification - -When a runtime or UI defect is reported after execution, or an accepted specification or plan explicitly claims runtime acceptance of a user-visible invariant, record a `RuntimeVerificationClassificationV1` before archive or residual feature routing. Review the authority chain in order: original user request and accepted specification; compiled plan and task acceptance criteria; executor handoffs and produced commits; then execution-introduced behavior. - -The record contains `classification`, `invariant_trace`, `negative_evidence`, and `owning_repair`. `classification` is one of `execution_introduced_bug`, `implementation_gap`, `new_feature`, or `uncovered_fixture`. For `execution_introduced_bug` and `implementation_gap` tied to an accepted invariant, `invariant_trace` must connect original requirement, specification invariant, owning plan task or acceptance criterion, changed commit, materialization, presentation, and runtime or UI proof. Passing component or unit tests alone is insufficient for this triggered runtime claim. This is not a universal browser or UI gate for plans without either trigger. - -`new_feature` or `uncovered_fixture` may have an empty `invariant_trace` only when `negative_evidence` records no matching original request or accepted specification invariant and no contradiction in the plan, handoff, or produced commit. Route `owning_repair` to the first broken artifact: an invariant already present in the task or acceptance criterion requires task repair and re-review; a specification invariant omitted from plan decomposition requires plan repair and resume from the owning step; an original-request invariant omitted or contradicted by the specification requires specification repair. Only after those routes are excluded may a residual class stand. - -Classification remains agent-owned and evidence-linked. A helper may require the record and validate its structure, but must not decide the semantic class. This audit must not expand into a broad source-quality reread or create another implementation-review agent. - -Keep same-scope specification-owned handling authoritative for a first-observed classification defect. Persist separate WorkBundle defect evidence only after `wb-defect-evaluation` classifies the finding as work-bundle-scoped or mixed and same-scope specification-owned handling no longer applies. - -Use project files only for bounded identity and finalization evidence. Do not broadly inspect source to decide code quality, redo task review, reread implementation for code quality, repair source/tests, or start another implementation-review agent for plan-level acceptance. - -## Typed routing - -```text -missing initial executor handoff - -> review-blocked -> resume initial result owner -accepted-task source repair - -> existing task owner -> claim-relevant validation -> scoped rereview -publication/status/archive control failure - -> controller owner -> reuse compact accepted result and completed review -knowledge work or return evidence incomplete - -> knowledge-blocked -> resume approved ks-* delegate-return path -metadata/index/repository finalization incomplete - -> repository-blocked -> bounded deterministic helper -workspace preparation/cleanup/finalization incomplete - -> workspace-blocked -> bounded execution-workspace helper -implementation rejected - -> task repair and independent re-review -failed, stale, or unexecuted implementation evidence - -> task repair -incapable, missing, or wrong-boundary allocation - -> plan repair -contradictory accepted authority - -> specification repair -plan decomposition defect - -> repair plan only -requirement/design/authority defect - -> repair specification -``` - -Do not create a repair specification for every failed gate. - -## Knowledge delegate-return - -When the upstream disposition or aggregate accepted task dispositions make closure `required`, invoke the approved keep-summarizing owner with accepted implementation, validation, handoff, review, and decision evidence. Review owns approved persistence delegation; executor disposition evidence never invokes a `ks-*` skill. Validate structural-value result, written or updated durable paths or evidence-backed no-write rationale, index rebuild status, blockers, and completion state. Resume only from that return evidence. Orchestration does not directly create, edit, promote, delete, or index durable knowledge, and archive remains blocked until the validated return resolves required closure. - -Knowledge closure gates final completion and archive; it never precedes specification, plan, task, or integrated-implementation review. - -## Finalization - -Keep audit judgment and deterministic finalization together in this skill for now; do not create `orch-finalize-plan`. After every audit gate passes, invoke the smallest existing helper for allowed commit, CodeGraph sync, project metadata update, archive, and index refresh. Clean only a WorkBundle-owned execution workspace when policy and proven Git identity allow it. - -For post-execution integrated review, first confirm every executor attempt is terminal, then call `begin-review-round` before evidence preparation or reviewer dispatch. Complete the reserved round with `complete-review-round` using the immutable stored product review reference, or a factual controller audit-block when no product review artifact exists. The audit-block records controller failure only and must not impersonate a product verdict. Use `review-round-status` to report the current frozen target, completed count, finalization requirement, and blocker route. - -An accepted post-execution review round follows the normal final audit, knowledge, repository, archive, and index gates above. Findings below the fifth completed round route to scoped repair. The fifth unresolved or blocked round routes to `finalize-with-blockers`: persist finalization-required state, validate the residual specification and clean source baselines, persist the active workspace blocker, validate and record the review-owned knowledge return, archive the origin specification and plan and update their indexes without collision overwrite, release owned bindings, and persist terminal closure. Retry an incomplete administrative stage without reopening product work or rerunning review. - -Archive remains blocked while any required knowledge, validation, review, handoff, repository, workspace, or unsettled decision evidence is incomplete or contradictory. - -## Runtime Rules - -- `orch-orchestration-boundary`: `rules/orchestration/orch-orchestration-boundary.md` -- `orch-review-completion`: `rules/orchestration/orch-review-completion.md` -- `orch-bounded-closure`: `rules/orchestration/orch-bounded-closure.md` - -Central `AGENTS.md` owns rule discovery and loading. Load the runtime rules above when their indexed conditions apply. - -## Boundary - -Follow `orch-orchestration-boundary`, `orch-review-completion`, and `orch-bounded-closure`. +- [ ] The implementation verdict covers every specification and plan obligation against the exact candidate. +- [ ] The reviewer is distinct and findings identify the affected requirement and product boundary. +- [ ] Supporting-state defects were routed separately and did not veto otherwise correct reviewable work. +- [ ] The final workflow review is compact and does not repeat code review or reconstruct history. +- [ ] Finalization carried the agent verdict and performed only mechanical checks and transitions. diff --git a/skills/wb-create-script/SKILL.md b/skills/wb-create-script/SKILL.md index d6f9bdc..275f3d0 100644 --- a/skills/wb-create-script/SKILL.md +++ b/skills/wb-create-script/SKILL.md @@ -22,7 +22,9 @@ For example, a checker can report malformed links and passing tests. Neither est ## Shape the implementation - Put behavior in its existing owner; keep command routing separate from reusable operation logic. Avoid duplicating lifecycle policy in adapters or creating a framework for a bounded helper. -- Accept information through explicit inputs or the current authoritative store. Use versioned schemas or catalogs where they already own shared data; do not externalize every ordinary constant or embed project-specific exceptions in generic code. +- Accept information through explicit inputs or the current authoritative store. For persisted or exchanged records that are reread, indexed, or lifecycle-authoritative, use their maintained versioned schema and artifact-family catalog as the structural authority. Released schemas are immutable; incompatible changes create a new version, and distinct semantic roles use distinct schema families. Do not externalize every ordinary constant or embed project-specific exceptions in generic code. +- Keep structural ownership mechanical: scripts may own schema selection, artifact type and identity, canonical anchor/location, declared parent-child bindings, maintained parsing and serialization, validation, atomic mutation, lifecycle transitions, and derived index projection. Callers own the semantic payload and cannot override those structural fields. +- Parse structure through a maintained format implementation. Search text or directories only when retrieval, index rebuilding, navigation, or diagnostics is the declared operation; treat every hit as a candidate until the canonical structured reader validates it. Do not use regexes, headings, substrings, broad globs, filenames, or successful unrelated checks to infer structural or semantic authority. - Define output and failure semantics that callers can act on: measured result, affected scope, and any partial effects. A successful exit means the declared operation succeeded, not that the surrounding project is accepted. A refusal identifies the failed condition, not an invented product verdict or mandatory repair workflow. - Separate inspection from mutation where it helps safe use. Validate effect-bearing inputs before writes; preserve unrelated content. Define overwrite, retry, and partial-failure behavior proportional to the operation. Do not assume every command can be idempotent; make non-repeatable effects explicit. - Consume existing authoritative results through their supported interface. Recompute only when relevant inputs changed or the contract requires it; do not reconstruct historical activity merely to make an interface convenient. @@ -38,3 +40,11 @@ Inspect the decision boundary as an agent: what does each check actually establi If the boundary is crossed, repair the first owning interface or implementation and recheck the affected behavior. Do not add a second checker to legitimize the first one's unsupported decision. Retain useful checks and accurate facts. Return the implemented contract, changed files, actual validation results, and any remaining limitation. Do not add acceptance gates, external actions, or workflow stages beyond the user's task. + +## Self-check + +- Does the script implement facts, explicit supplied policy, or authorized mechanics without deciding semantic correctness, sufficiency, qualification, remediation, or acceptance? +- For every persisted or exchanged record it owns, is there one real versioned schema/catalog/locator/parser path, with no fallback identity, filename inference, or compatibility authority? +- Are searches limited to declared retrieval, index, navigation, or diagnostic jobs, with hits treated as candidates until canonical read and any required agent judgment? +- Are effect-bearing inputs and declared relationships validated before mutation, writes atomic where required, unrelated bytes preserved, and partial effects reported truthfully? +- Do normal, meaningful invalid, retry/mutation, producer, and consumer tests exercise the observable contract rather than prescribed wording? diff --git a/skills/wb-initialize-project/SKILL.md b/skills/wb-initialize-project/SKILL.md index c398b4b..e232929 100644 --- a/skills/wb-initialize-project/SKILL.md +++ b/skills/wb-initialize-project/SKILL.md @@ -1,193 +1,64 @@ --- name: wb-initialize-project -description: Initialize, validate, doctor, or migrate single- and multi-repository WorkBundle workspaces via scripts/wb.py dispatcher commands. +description: Initialize, validate, doctor, or migrate single- and multi-repository WorkBundle workspaces through the current metadata-v4 control-plane commands. Use for workspace creation, attachment, registry/device binding, or explicit historical metadata migration. --- -# wb-initialize-project +# Initialize a WorkBundle Workspace -## Purpose +Use the public `scripts/wb.py` entrypoint. It owns its maintained YAML and JSON Schema runtime; do not substitute release CI or direct module imports for the supported command. -Initialize, doctor, validate, register, inspect, or migrate a project as a work-bundle adapted workspace using mechanical dispatcher commands only. +## Current authority -## Inputs +- Resolve `work_bundle_config_root` as `~/.work-bundle/` and read its `bootstrap.yaml` before locating registries or toolkit assets. +- Treat `$workspace_root/.work-bundle/project.yaml` metadata v4 as portable project/topology authority for identity, repository topology, canonical remote, branch policy, and operation policy. +- Treat the matching `device_bindings[workspace_id]` in the bootstrap-resolved project registry as local workspace/member path and observation authority. +- Keep `work_bundle_config_root`, `workspace_root`, and each selected member `project_root` distinct. A member selector must join to the selected workspace by workspace and repository IDs; never infer authority from a locator path. +- Use `script/index.yaml` for workspace utility discovery in both repository modes. Discovery does not authorize execution. +- Discover project rules from `$workspace_root/.work-bundle/rules/index.yaml`. +- The root `rules/index.yaml` is legacy-only, so preserve it as a legacy artifact only during explicit migration. +- Read enabled rule indexes first, then load every applicable rule body in full according to its metadata. +- Never open or ingest `credentials/credentials.yaml`; validate only its protected structure and permissions. -- `workspace_root`: authority root that owns `.work-bundle/`, `AGENTS.md`, `script/`, and `credentials/`; in multi-repository mode it also owns managed members. -- `project_root`: one concrete source repository checkout; equal to `workspace_root` in single-repository mode and a member path in multi-repository mode. -- Explicit `mode`: `single-repository` or `multi-repository` for new initialization. -- `~/.work-bundle/bootstrap.yaml` for `project_registry` and `work_bundle_root` resolution. -- Optional `WB_WORK_BUNDLE_ROOT` environment override when the agent must pass an explicit toolkit root to dispatcher commands. -- Work-bundle reference templates and manifests under the bootstrap-resolved work-bundle root: - - `references/assets/template/project.yaml` - - `references/assets/template/projects.yaml` - - `references/assets/template/AGENTS.md` - - `references/wb-initialize-project-default-work-bundle-tree.yaml` -- Git CLI for mechanical repository capability, branch, and HEAD-commit inspection when the project root is Git-backed. -- Optional `.codegraph/` marker under each source repository; absence means CodeGraph is unsupported for that repository and must be reported as `no-index`, not initialized. - -## Must - -Invoke project lifecycle behavior only through `python3 scripts/wb.py` dispatcher commands. Do not create, modify, or validate `scripts/work-bundle/project.py` or other script modules from this skill. - -| Mode | Command | -|---|---| -| Initialize | `init-project <root> --mode <single-repository|multi-repository> [--workspace-root <workspace-root>] [--name <name>] [--force] [--dry-run] [--disable-work-bundle-git] [--create-project-skill-override]` | -| Doctor | `doctor-project <root> [--repair] [--force]` | -| Inspect only | `show-project [--workspace-root <workspace-root> | --project-root <project-root>]` | -| Strict validate | `validate-project <root> [--dry-run]` | -| Register only | `register-project <root> [--name <name>]` | -| Inspect metadata migration | `migrate-project <root> [--name <name>] --dry-run` | -| Apply metadata migration | `migrate-project <root> [--name <name>] [--force] [--accepted-proposal-id <id>] --apply` | -| Inspect portable-control migration | `migrate-control-plane <workspace-root> [--repository-remote <id>=<canonical-remote>] --dry-run` | -| Apply portable-control migration | `migrate-control-plane <workspace-root> [--repository-remote <id>=<canonical-remote>] --accepted-proposal-id <id> --apply` | -| Inspect registry-wide layout migration | `migrate-registered-projects --dry-run [--slug <slug>]` | -| Apply registry-wide layout migration | `migrate-registered-projects --apply --accepted-plan-id <id> [--slug <slug>]` | -| Attach portable workspace | `attach-workspace <workspace-root> [--materialize <none|missing|all>] [--repository-path <id>=<path>] (--dry-run|--apply)` | -| Doctor portable workspace | `doctor-workspace <workspace-root> [--repair]` | -| Add v4 workspace member | `add-workspace-member <workspace-root> --repository-id <id> --remote <observed-url> --name <binding-name> --path <relative-path> --default-branch <branch> (--dry-run|--accepted-proposal-id <id> --apply)` | -| Provision member | `provision-member --workspace-root <workspace-root> [--workspace-slug <slug>] --origin <origin-root> --repository-id <id> --working-branch <branch> --base-ref <ref> [--dry-run|--apply]` | -| Cleanup member | `cleanup-member --workspace-root <workspace-root> --repository-id <id> (--dry-run|--apply)` | - -`initialize-project` remains a compatibility alias for `init-project`; prefer `init-project` in new instructions. - -The explicit `--workspace-root` and `--project-root` selectors remain available only on commands whose live help lists them, such as `show-project`. New creation must reject a missing or contradictory mode/root combination rather than silently infer topology. Single-repository mode is current and fully supported, not legacy or transitional. - -**Portable v4 migration guardrails:** before `migrate-control-plane`, load every applicable rule body in full, including project context, registry authority, lifecycle, repository boundary, security exclusion, and defect routing. Do not sample those rules by keyword. Resolve canonical remotes from explicit `--repository-remote` input, registry locator authority, and the live origin chain. When authoritative network remotes conflict, stop and ask the user which remote is canonical; rerun the exact dry-run with `--repository-remote` after the decision. During this workflow, do not edit the project registry directly and do not change an external repository's Git config. `show-project`, `validate-project`, and `doctor-project` route metadata-version-4 workspaces to v4 control-plane validation; repair must never rewrite portable v4 metadata into v3 shape. - -**Preserve behavior (default):** commands preserve existing non-empty files and user-authored `AGENTS.md` content outside the WorkBundle managed section. Re-running `init-project` on a healthy project reports `changed_files: []`. - -**`init-project --force`:** may overwrite init-managed template files only: `.work-bundle/project.yaml`, `.work-bundle/rules/index.yaml`, `.work-bundle/knowledge/project.yaml`. For `AGENTS.md`, force refreshes only the WorkBundle managed section from `references/assets/template/AGENTS.md` and preserves user-authored content outside that section. - -**`migrate-project --force`:** narrower migration-only repair subset; overwrites `.work-bundle/project.yaml` only. For `AGENTS.md`, migration may convert legacy whole-file template content or stale managed sections to the current marker-bounded managed section without taking ownership of the whole file. - -`migrate-registered-projects --dry-run` inspects every bootstrap-resolved registry entry, reports layout version vs registry schema version, and lists the ordered version-to-version steps that would run. Apply requires that exact plan ID. Already-current entries are no-ops. Registry `layout_version` is written only after the target layout validates; a failed project restores recoverable pre-migration state and is not marked current. - -For metadata v2, `migrate-project --dry-run` classifies topology from project metadata plus the bootstrap-resolved registry and returns a proposal ID. In-place apply is allowed only for `single-compatible` evidence and requires that exact ID. Multiple repository locators route to `migrate-to-multi-repository`; registry/metadata identity conflicts and proposal drift fail closed. `--force` never overrides topology classification. - -`provision-member --dry-run` returns `status: proposed` without writes. Apply treats checkout verification as an internal state and returns `status: passed` only after the member binding and origin locator are recoverably published to workspace metadata and the project registry. Matching verified transactions resume publication, published transactions replay without writes, and unrelated targets remain collisions. - -`add-workspace-member --dry-run` validates the current workspace binding, matching workspace root, requested remote/branch and rendered portable metadata before returning a digest-bound proposal without writes. Apply requires that exact proposal and rechecks checkout state before recoverable publication. A pre-existing checkout must be on `--default-branch`; it is never rollback-owned. Newly cloned checkouts are removed if publication fails. - -- **Single/composite:** require the root source binding and valid Git checkout with matching remote/branch. The first single-repository apply converts to composite; later adds preserve composite mode. Publish the nested-member binding and owned root-source exclusion. Replay requires both binding and exclusion to match. -- **Multi-repository:** preserve the non-Git workspace root and multi-repository mode. Require a direct member with `--name` equal to `--path`; reject protected resource paths, symlinks and external Git common directories. Verify existing required members and the new checkout, including branch and cleanliness. Publish the portable member name and device-local `managed-worktree` binding without creating root `.git` or composite exclusions. Replay requires the same checkout and complete matching binding. - -Use this command for v4 membership additions, not v3 `provision-member` or manual metadata edits. Missing/inconsistent local state fails closed; explicit attach/doctor remains the repair path. - -An exact workspace-local checkout created by an older WorkBundle version may have no recovery record. `provision-member` adopts it only when control scope, origin, repository ID, branch, and base HEAD all match; dry-run reports `resume_source: verified-orphan`. It never claims that adopted checkout as rollback-owned. `cleanup-member` is limited to recorded, unpublished, transaction-owned checkouts; published members require a separate deregistration workflow and unrecorded paths are never deleted. - -**`init-project --dry-run` / `validate-project --dry-run`:** inspect and report mechanical failures without writing project files. - -**Registry and slug (per `wb-project-registry`):** - -- Resolve the project registry path from `bootstrap.yaml` field `project_registry`. -- Register every initialized project to `projects.yaml` as a new workspace slug or an existing workspace slug. -- Derive the workspace slug from `--name` when provided; otherwise from the project root directory name (normalized lowercase alphanumeric with hyphens). -- On slug or workspace-root match, merge registry entries and preserve existing aliases, origins, compatibility locators, and unknown fields unless explicitly replaced. -- Keep registry project entries locator-oriented. For metadata v4, the registry's separate `device_bindings` section owns device-local workspace/control-plane paths, member materialization paths, checkout kinds, and observations. -- Model origins and workspace members independently. Multiple workspaces may register the same origin ID while owning independent local control stores, named worktrees, and distinct working branches. -- Preserve every registered origin/member identity and unknown field during initialize, repair, registration, and migration; never collapse multi-member state to the command cwd. -- When changing durable topology, update its durable authorities atomically or recoverably. For metadata-v4 attach/doctor, preserve portable `project.yaml` and publish only the bootstrap-resolved device binding after verification. -- Treat public member-provision success as a converged state: workspace-local checkout verified, metadata member published, and registry origin published. Never report success with pending publication states. -- Carry version-aware role descriptions: metadata-v4 `project.yaml` is portable project/topology authority, metadata-v4 `device_bindings` are device-local materialization/observation authority, and metadata-v3 project metadata remains working-state authority only during explicit v3 reads or migrations. -- Ask for the workspace slug decision only when it is missing and blocking. - -**Workspace metadata v3 and v2 compatibility:** - -- Render new or explicitly migrated `.work-bundle/project.yaml` with `metadata_version: 3`, `workspace_root`, explicit `workspace_mode`, workspace resource status, operation policy, and member bindings. -- During explicit metadata-v3 reads and migrations only, keep `.work-bundle/project.yaml` as workspace-local authority for member working state, expected branch/base ref, observed HEAD/time, lifecycle state, operation policy, and CodeGraph state; never apply those local fields to metadata v4. -- Read metadata v2 during the compatibility window, preserve unknown fields, and require inspect/dry-run/explicit apply before converting topology or moving worktrees. -- Render `operation_policy.project_files` with non-destructive file operations and `operation_policy.git` with allowed read operations, permissive stage/commit/pull operations, and forbidden destructive operations including `reset --hard`, `clean -fd`, and `push --force`. -- Render v3 `source_repositories[]` members with stable `id`, `project_root`, `origin_id`, checkout kind, workspace-local control-store binding, worktree name, expected branch, base ref, observed HEAD/time, baseline/lifecycle status, operation policy, and nested CodeGraph state. -- In single-repository mode require `workspace_root == project_root`; do not silently alter its existing Git tracking policy. -- In canonical multi-repository mode require each managed `project_root` and absolute Git common directory to remain beneath `workspace_root`. -- For Git-backed repositories, mechanically record live branch and HEAD as observations; keep expected branch/base ref as declared policy and provision input. -- For non-Git repositories, set `git_repository: false`, `branch_required: false`, empty `last_commit_id`, and `baseline_status: not-git`. -- For repositories without `.codegraph/`, set `codegraph.supported: false`, `codegraph.index_present: false`, `codegraph.status: not-indexed`, and `codegraph.reason: no-index`. Do not run `codegraph init` or `codegraph sync`. -- For repositories with `.codegraph/`, report marker presence only; synchronization remains owned by orchestration review behavior, not initialization. - -**Initialization structure:** - -- Create `$workspace_root/.work-bundle/knowledge/{context-packs,indexes,notes,open-questions}`. -- Create the full `.work-bundle/orchestration` subtree at initialization: - - `orchestration/spec/{active,archived}` - - `orchestration/plan/{active,archived}` - - `orchestration/handoff/orchestration/{active,archived}` - - `orchestration/handoff/executor/{active,archived}` - - `orchestration/{docs,principles,templates,reviews,execution-state}` -- Directory membership is driven by `references/wb-initialize-project-default-work-bundle-tree.yaml`. -- In both workspace modes, create or preserve `$workspace_root/script/index.yaml` from its empty v1 template and never auto-execute indexed utilities. -- In both workspace modes, create or preserve `$workspace_root/credentials/credentials.yaml` as the sole credential-directory file, enforce protection, and keep the directory Git-ignored without reading values. -- In both workspace modes, render and validate the `workspace_resources` metadata block. In single-repository mode, keep `script/` available to the source repository's established tracking policy while excluding `credentials/` and `.work-bundle/` without replacing existing ignore content or untracking user-owned paths. -- In multi-repository mode place runtime Git control stores beneath `$workspace_root/.work-bundle/git/` and exclude them from workspace-management commits and broad scans. -- Render `.work-bundle/project.yaml` from `references/assets/template/project.yaml`. -- Render `.work-bundle/project.yaml` with metadata v3 workspace/member state from mechanical Git and per-member `.codegraph/` inspection. -- Render `.work-bundle/project.yaml` with an `agents_sync` section that owns WorkBundle `AGENTS.md` checksum and sync-status state. -- Create, append, or refresh `AGENTS.md` with the WorkBundle managed section from `references/assets/template/AGENTS.md`; do not overwrite user-authored content outside the managed section. -- Create or preserve required `.gitignore` entries. -- Create or preserve the current project rule-store index at `.work-bundle/rules/index.yaml`; root `rules/index.yaml` is legacy-only and is not current project rule authority. -- Create the declared `.work-bundle/knowledge` structure without staging, committing, or initializing Git; Git ownership requires a separate explicitly authorized workflow. -- Bind registry IO to the bootstrap-resolved `project_registry` using `references/assets/template/projects.yaml`; metadata-v4 device bindings own local paths/observations, while metadata-v3 project metadata owns working-state fields only in explicit v3 compatibility workflows. -- Fail mechanically when a required reference asset is missing; do not invent fallback content. - -**Validation scope:** mechanical checks only — file presence, directory structure, schema keys, registry status, metadata version, source repository fields, branch mismatch, stale baseline commit, registry/project repository ID mismatch, operation policy shape, CodeGraph metadata shape, and Git status. No semantic prose or bootstrap-artifact checks. - -## Doctor Mode - -Use `doctor-project` as the canonical doctor command. - -- `doctor-project` without `--repair`: inspect and report mechanical failures only. -- `doctor-project --repair`: repair deterministic structure defects; default repair preserves existing non-empty user content. -- `doctor-project --repair --force`: repair with init-scoped template overwrite permission, while limiting `AGENTS.md` changes to the WorkBundle managed section. -- Every lifecycle result reports `git_actions: []`; initialization, doctor, metadata migration, and member provisioning never infer stage or commit authority. -- Report v3 workspace/member failures and v2 compatibility failures using machine-readable keys for stale metadata, missing repository state, branch/HEAD mismatch, stale baseline, registry/metadata mismatch, invalid operation policy, invalid workspace resources, and invalid CodeGraph shape. -- With `--repair --force`, refresh branch and commit baselines for all registered checkouts while preserving their IDs, paths, checkout roles, and unknown user fields. -- Do not rewrite user-authored project content without explicit `--force`. -- Do not migrate registry identity without preserving the old slug mapping or reporting the required user decision. - -## Migration Mode - -Use `migrate-project` only for unambiguous single-repository legacy layout upgrades. Use `migrate-to-multi-repository` when legacy evidence contains multiple repositories. - -- Detect legacy `.work-bundle` layout, missing registry fields, obsolete template sections, retired bootstrap artifacts, legacy `rules/contract.yaml`, and moved template paths. -- Preserve existing knowledge notes, open questions, orchestration artifacts, Git history, and project identity. -- Add missing current files and directories without deleting unknown files. -- Preserve legacy metadata v1/v2 compatibility reads and provide explicit v2-to-v3 migration with `--dry-run` proposal, `--apply` conversion, origin/member mapping, conflict evidence, and unknown-field preservation. -- Convert legacy whole-file WorkBundle `AGENTS.md` template content to the current managed section when needed, preserving content outside managed sections. -- Write a migration report under `.work-bundle/orchestration/docs/migration-report-YYYY-MM-DD.md`. -- When retired legacy bootstrap artifacts are present, archive evidence under `.work-bundle/orchestration/docs/legacy-bootstrap-archive-YYYY-MM-DD/`, remove active legacy bootstrap paths, and list retired artifacts in the migration report. -- When legacy `rules/contract.yaml` is present, archive it under `.work-bundle/orchestration/docs/legacy-rules-contract-archive-YYYY-MM-DD/` and remove the active file. -- When legacy root `rules/index.yaml` is present, preserve it as a legacy artifact only; do not restore, overwrite, or validate it as current project rule authority. -- `migrate-project --force` applies migration-only structural repair; it does not broaden overwrite to general init-managed files. - -## Must Not - -- Load, create, require, validate, or reference retired legacy bootstrap artifacts or paths. -- Create or validate scripts; this skill consumes dispatcher commands only. -- Open, print, grep, serialize, copy, or migrate credential values; only structural credential-store validation is permitted. -- Create a managed worktree whose project root or Git common directory remains outside `workspace_root`. -- Eagerly scan all work-bundle skills, rules, or references beyond command output. -- Store project registry state under `project_root` or the work-bundle root. -- Delete existing knowledge, orchestration artifacts, registry data, or unknown user files. -- Create specifications, plans, phases, tasks, reviews, or handoffs during initialization. -- Stage, commit, reset, clean, stash, checkout, or otherwise mutate Git state from ordinary lifecycle commands. - -## Output - -- Initialized, doctored, validated, registered, or migrated project workspace. -- Updated bootstrap-resolved project-registry locator or metadata-v4 device binding when its lifecycle command runs. -- JSON command output with `status`, `failures`, registry status, metadata version/mode/resources/member evidence, redacted lifecycle transaction state, AGENTS sync evidence, and changed files where applicable. Compatibility reads retain existing v2 evidence fields until explicit migration. -- Migration report and optional legacy-bootstrap archive paths under `.work-bundle/orchestration/docs/` when migration retires legacy artifacts. - -## On Failure - -- Stop before destructive changes. -- Report the blocking file, missing reference asset, registry entry, slug/mode decision, metadata schema defect, branch/HEAD mismatch, stale baseline, registry/metadata mismatch, workspace-resource defect, or CodeGraph inconsistency from command JSON `failures`. -- Ask at most one blocking question when slug or registry identity is unresolved. - -## Runtime Rules - -- `wb-project-context-preflight`: `rules/work-bundle/wb-project-context-preflight.md` -- `wb-project-registry`: `rules/work-bundle/wb-project-registry.md` -- For workspace utility integration, read `skills/wb-create-script/references/workspace-integration.md` from the toolkit root. Use `wb-create-script` only when authoring or changing script behavior, not for an ordinary initialization command. -- `rule-work-bundle-security-exclusion`: `rules/security-exclusion.md` -- `wb-credential-use`: `rules/work-bundle/wb-credential-use.md` only when a task or utility requires a credential. -- `wb-migrate-to-multi-repository`: `rules/work-bundle/wb-migrate-to-multi-repository.md` only for explicit topology migration. +## Public operations + +Create current workspaces with: + +```text +python3 scripts/wb.py init-workspace <workspace-root> --slug <slug> --repository <id=remote> --mode <single-repository|multi-repository> (--dry-run|--apply) +``` + +Attach local materializations with `attach-workspace`, inspect with `show-project`, validate with `validate-project`, and diagnose with `doctor-workspace` or `doctor-project`. These operations consume the same schema-owned metadata-v4 and device-binding join. + +`init-project` and `initialize-project` are retired and return `WB_CURRENT_INIT_COMMAND_RETIRED`; they must not create metadata v3 or act as producer aliases. + +Historical metadata v2/v3 is migration input only: + +- Use `migrate-control-plane <workspace-root> --dry-run` to produce one proposal for a single workspace. +- Apply only with the exact accepted proposal ID; publish validated metadata v4 directly without installing an intermediate current v3. +- Use `migrate-registered-projects` for the registry-wide v2/v3 path. +- Preserve historical bytes/unknown portable fields where the migration contract allows them. Unsupported versions or ambiguous topology fail before mutation. + +Agents do not edit the project registry directly and do not change an external repository's Git config. Supply semantic inputs such as `--repository <id=remote>` to the owning public transaction. + +## Mutation boundary + +Validate schemas, workspace/member containment, branch policy, canonical remotes, device-binding joins, and effect-bearing inputs before writes. Publish project metadata and registry/device bindings atomically or through the existing recoverable transaction. Preserve unrelated registry entries, templates, rules, knowledge, orchestration artifacts, and user-authored AGENTS content. + +In both workspace modes, create or preserve `$workspace_root/script/index.yaml`. +In both workspace modes, create or preserve `$workspace_root/credentials/credentials.yaml`; never read credential values. + +Scripts report structural facts and failures only. The acting agent determines whether the created or migrated workspace fulfills the user's purpose; doctor success is not semantic acceptance. + +## Self-check + +- Did ordinary creation emit only schema-valid metadata v4 and a matching device binding, with no local path or observation in portable metadata? +- Are v2/v3 accepted only by explicit migration commands, with retired creation spellings refusing rather than writing legacy state? +- Did config, workspace, and selected member roots remain distinct, and did every member join by stable IDs without locator/path fallback? +- Were maintained YAML/schema validation and all effect-bearing checks completed before atomic or recoverable mutation? +- Did I preserve unrelated files and avoid credential values, Git staging/commit, utility execution, and semantic acceptance claims? + +## Runtime rules + +- `wb-project-context-preflight` +- `wb-project-registry` +- `rule-work-bundle-security-exclusion` diff --git a/skills/wb-migrate-to-multi-repository/SKILL.md b/skills/wb-migrate-to-multi-repository/SKILL.md index 8d8d05f..e62806a 100644 --- a/skills/wb-migrate-to-multi-repository/SKILL.md +++ b/skills/wb-migrate-to-multi-repository/SKILL.md @@ -1,22 +1,30 @@ --- name: wb-migrate-to-multi-repository -description: Inspect, dry-run, apply, verify, retry, or roll back a source-preserving migration from a single-repository WorkBundle project to a multi-repository workspace. Use when the user explicitly requests this topology migration. +description: Route a requested legacy topology migration to the current metadata-v4 workspace creation or explicit historical metadata migration path. The former v3-producing public command is retired. --- -# Migrate To Multi-Repository Workspace +# Route Multi-Repository Migration -Load `rules/work-bundle/wb-migrate-to-multi-repository.md`, project preflight/registry rules, and security exclusion. Resolve explicit source authority `project_root`, target `workspace_root`, workspace slug, repository ID/name, working branch, base ref, optional primary Git `origin`, and any additional origin locators. When the authority root is not Git-backed, `--origin` is required and must select one of its declared reusable Git source repositories. +Do not run the former `migrate-to-multi-repository` implementation. The public command returns `WB_TOPOLOGY_MIGRATION_COMMAND_RETIRED` because it produced metadata v3, which is no longer a current format. -Always run inspect and dry-run before requesting explicit apply authority. Report the source repository and nested `.work-bundle` Git states separately. When either is dirty, pass the exact accepted-baseline ID returned by the proposal; never synthesize or bypass it. Never clean, stash, reset, commit, delete, relocate, or deregister source state. +Choose the current v4 owner from the user's purpose: -Treat multiple legacy repository locators as a routing signal, not proof that a valid multi-repository workspace already exists. The in-place `migrate-project` command must stop and route such evidence here; only this workflow may create workspace-local control stores and managed worktrees. +- For a new multi-repository workspace, use `init-workspace <workspace-root> --mode multi-repository --slug <slug> --repository <id=remote> ...` with dry-run before apply. +- For an existing workspace whose `.work-bundle/project.yaml` is metadata v2 or v3, use `migrate-control-plane <workspace-root> --dry-run`, then apply only the exact accepted proposal ID. +- For registry-wide historical migration, use `migrate-registered-projects` with its exact accepted plan ID. +- For adding a repository to an existing current workspace, use the v4 `add-workspace-member` proposal/apply transaction. -On apply, copy and verify `.work-bundle`, preserve nested Git history and unknown files, copy indexed `script/`, merge managed AGENTS content, exclude credential content, create an empty protected credential store, and provision a workspace-local control store plus named worktree. Publish registry authority only after all target verification passes. +Preserve source repositories and unrelated workspace files. Never use the historical Python migration module as a public producer, never publish an intermediate metadata v3 document, and never treat a repository locator as a device binding. -Verify SessionStart discovery, member preflight, workspace-local Git control, staged metadata/registry identities, resources, and source preservation before publication. Publish metadata v3 and the bootstrap-resolved locator registry through one atomic-or-recoverable transaction only after every check passes. Keep built-in skills outside the external skill registry. +## Self-check -On failure, retain a redacted transaction record outside disposable owned paths. Permit only idempotent retry with the same accepted baseline or rollback of transaction-owned target paths; restore partial publication without leaving a false active member. For an already published transaction, replay the persisted complete result and stable transaction evidence without writing or republishing. Never expose credential contents or sensitive paths. +- Did the selected command emit or preserve schema-valid metadata v4 only? +- If the input was v2/v3, was it admitted solely through an explicit historical migration command? +- Are portable topology and device-local paths still separated and joined by stable IDs? +- Did I avoid staging, committing, deleting, or mutating source state outside the chosen v4 transaction? -For later `provision-member` operations, checkout verification is internal. Public success requires the new member in workspace metadata and its origin in the bootstrap-resolved registry through the same recoverable publication boundary. A matching verified-but-unpublished transaction resumes; it is not an unrelated target collision. +## Runtime rules -An older verified checkout may predate recovery records. Resume it only after exact workspace-local control, origin, repository ID, branch, and base-HEAD verification. Do not delete it through cleanup unless a recovery record proves it is unpublished and transaction-owned. +- `wb-project-context-preflight` +- `wb-project-registry` +- `rule-work-bundle-security-exclusion` diff --git a/tests/conftest.py b/tests/conftest.py index 8f1ec50..4767d74 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,18 +1,9 @@ from __future__ import annotations import pytest -import sys @pytest.fixture(autouse=True) def disable_invocation_observation(monkeypatch: pytest.MonkeyPatch) -> None: """Keep the test suite out of the user's real WorkBundle usage database.""" monkeypatch.setenv("WORK_BUNDLE_INVOCATION_LOG", "0") - - -@pytest.fixture(autouse=True) -def isolated_reviewer_receipt_store(tmp_path, monkeypatch): - """Exercise native receipt lookup without using the user's runtime store.""" - module = sys.modules.get("review_runtime") - if module is not None: - monkeypatch.setattr(module, "reviewer_runtime_root", lambda root: tmp_path.parent / f"reviewer-runtime-{tmp_path.name}") diff --git a/tests/reviewer_run_fixtures.py b/tests/reviewer_run_fixtures.py deleted file mode 100644 index b42ba56..0000000 --- a/tests/reviewer_run_fixtures.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Native stage receipt fixtures; only the OS process boundary is stubbed by default.""" -from copy import deepcopy -import hashlib -import json -from pathlib import Path -import subprocess -import sys -from unittest.mock import patch -import uuid - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts/work-bundle")) -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts/orchestration")) -import reviewer_workspace -import review_runtime - - -def bind_review_receipt(root, record, *, real_process=False, execution_id=None): - review = deepcopy(record) - review.pop("reviewer_run", None) - review["review_id"] = f"review-{uuid.uuid4()}" - area = "spec" if review["stage"] == "specification" else "plan" - from execution_context import _read_structured - target = next(path for path in (root / f".work-bundle/orchestration/{area}").glob("*/*.md") - if _read_structured(path)[0].get("id") == review["target_identity"]["artifact_id"]) - locator = "control:" + target.relative_to(root).as_posix() - protected = root / ".work-bundle/protected-test" - protected.mkdir(parents=True, exist_ok=True) - context = {"stage": review["stage"], "target_identity": review["target_identity"], "target_locator": locator, - "agent_id": review["reviewer"]["agent_id"], "capability": review["reviewer"]["capability"], - "execution_id": execution_id or f"worker-{uuid.uuid4()}", - "evidence_mode": "direct_source" if review["evidence"]["mode"] == "direct" else review["evidence"]["mode"]} - required, missing = review_runtime.stage_evidence_requirements(root, review["stage"], target) - assert not missing, missing - if review["stage"] == "integrated_implementation": - required.update({entry["locator"]: "source_tree" for entry in review_runtime.source_snapshot_entries(root)}) - packet = reviewer_workspace.build_direct_evidence_packet(source_root=root, control_root=root, - protected_roots=[protected], artifacts=list(required), search_roots=[], validators=[], sentinels=[], - network_state="denied", stage_review_context=context) - review["evidence"]["mode"] = packet["stage_review_context"]["evidence_mode"] - review["reviewer"]["context_origin"] = review["evidence"]["mode"] - review["evidence"]["artifacts"] = [{"path": item["locator"], "sha256": item["sha256"]} for item in packet["artifacts"]] - created = reviewer_workspace.create_reviewer_workspace(review_runtime.reviewer_runtime_root(root), review["review_id"], packet) - workspace = Path(created["workspace_path"]) - output = json.dumps(review) - if real_process: - # A separate sandboxed fixture worker inspects frozen evidence before emitting its verdict. - argv = ["/bin/sh", "-c", 'test -r "$1" && printf "%s" "$2"', "reviewer", - "evidence/" + locator.replace(":", "/", 1), output] - receipt = reviewer_workspace.run_sandboxed_reviewer(workspace, argv) - else: - with patch.object(reviewer_workspace, "_run_sandboxed_process", - return_value=subprocess.CompletedProcess(["fixture-worker"], 0, output, "")): - receipt = reviewer_workspace.run_sandboxed_reviewer(workspace, ["fixture-worker"]) - receipt_path = Path(receipt["receipt_path"]) - review["reviewer_run"] = {"run_id": receipt["run_id"], "sha256": hashlib.sha256(receipt_path.read_bytes()).hexdigest()} - return review diff --git a/tests/test_blocking_fact_skill.py b/tests/test_blocking_fact_skill.py index bf95319..73e555f 100644 --- a/tests/test_blocking_fact_skill.py +++ b/tests/test_blocking_fact_skill.py @@ -58,15 +58,13 @@ def test_existing_admission_consumes_targeted_resolution_and_exemptions(tmp_path metadata.parent.mkdir() for name in ("b1.md", "b2.md"): (tmp_path / name).write_text("# Selected product requirements\n") - old_closure = {"origin_plan": "old-flow", "closure_outcome": "closed_with_blockers"} control = { "schema_version": 1, - "post_execution_review_round_limit": 5, "blockers": [ - {"id": "B1", "status": "active", "specification": "b1.md", "reason": "missing receipt"}, + {"id": "B1", "status": "active", "specification": "b1.md", "reason": "unresolved requirement"}, {"id": "B2", "status": "active", "specification": "b2.md", "reason": "unrelated"}, ], - "closed_flows": [old_closure.copy()], + "operator_notes": {"preserve": True}, "implementation_exemptions": [], } data = {"metadata_version": 4, "orchestration_control": control, "custom": {"keep": True}} @@ -83,7 +81,7 @@ def admit(operation="ordinary_new", flow="repair-B1"): save() with pytest.raises(bounded_closure.BoundedClosureError, match="blocker=B2"): admit() - # Supply a later agent decision only for B1, without a receipt or review file. + # Supply a later agent decision only for B1, without auxiliary state. control["implementation_exemptions"] = [] control["blockers"][0].update(status="resolved", resolution={"judgment": "semantic pass"}) save() @@ -91,7 +89,7 @@ def admit(operation="ordinary_new", flow="repair-B1"): admit() reread = yaml.safe_load(metadata.read_text()) assert reread["custom"] == {"keep": True} - assert reread["orchestration_control"]["closed_flows"] == [old_closure] + assert reread["orchestration_control"]["operator_notes"] == {"preserve": True} # An independent explicit exemption for B2 demonstrates B1 no longer gates admission. control["implementation_exemptions"] = [{"blocker_id": "B2", "flow_id": "repair-B1", "status": "active"}] save() diff --git a/tests/test_bounded_closure_workflow.py b/tests/test_bounded_closure_workflow.py deleted file mode 100644 index 8e1abda..0000000 --- a/tests/test_bounded_closure_workflow.py +++ /dev/null @@ -1,420 +0,0 @@ -from __future__ import annotations - -import hashlib -import json -import os -from pathlib import Path -import subprocess -import sys - -import yaml - - -REPO_ROOT = Path(__file__).resolve().parents[1] -ENTRYPOINTS = ( - REPO_ROOT / "scripts/orch.py", - REPO_ROOT / "scripts/wb.py", -) -ORCHESTRATION = REPO_ROOT / "scripts/orchestration" -if str(ORCHESTRATION) not in sys.path: - sys.path.insert(0, str(ORCHESTRATION)) - -import bounded_closure # noqa: E402 - - -def _git(root: Path, *args: str) -> str: - return subprocess.check_output(["git", "-C", str(root), *args], text=True).strip() - - -def _run( - entrypoint: Path, - cwd: Path, - *arguments: str, - env: dict[str, str] | None = None, -) -> subprocess.CompletedProcess[str]: - return subprocess.run( - [sys.executable, str(entrypoint), *arguments], - cwd=cwd, - env=env, - text=True, - capture_output=True, - check=False, - ) - - -def _snapshot(root: Path) -> dict[str, str]: - return { - path.relative_to(root).as_posix(): hashlib.sha256(path.read_bytes()).hexdigest() - for path in root.rglob("*") - if path.is_file() - } - - -def _write_workspace(tmp_path: Path) -> tuple[Path, Path, Path, dict[str, str], Path, str]: - workspace = tmp_path / "workspace" - member = workspace / "repositories/product-main" - worktree = tmp_path / "execution-worktree" - member.mkdir(parents=True) - worktree.mkdir() - - original_blocker = workspace / ".work-bundle/orchestration/spec/active/spec-wor113.md" - original_blocker.parent.mkdir(parents=True) - original_blocker.write_text("# WOR-113 unresolved\n", encoding="utf-8") - metadata = { - "metadata_version": 4, - "workspace": {"id": "workspace-test", "mode": "multi-repository"}, - "orchestration_control": { - "schema_version": 1, - "post_execution_review_round_limit": 5, - "blockers": [ - { - "id": "BLOCK-WOR113", - "status": "active", - "origin_plan": "plan-wor113", - "origin_spec": "spec-wor113", - "specification": original_blocker.relative_to(workspace).as_posix(), - } - ], - "implementation_exemptions": [ - { - "flow_id": "plan-flow", - "blocker_id": "BLOCK-WOR113", - "status": "active", - } - ], - "unrelated_extension": {"preserve": ["newer", "state"]}, - }, - } - metadata_path = workspace / ".work-bundle/project.yaml" - metadata_path.parent.mkdir(parents=True, exist_ok=True) - metadata_path.write_text(yaml.safe_dump(metadata, sort_keys=False), encoding="utf-8") - backup = tmp_path / "pre-exemption-project.yaml" - original = { - **metadata, - "orchestration_control": { - **metadata["orchestration_control"], - "implementation_exemptions": [], - }, - } - backup.write_text(yaml.safe_dump(original, sort_keys=False), encoding="utf-8") - backup_sha256 = hashlib.sha256(backup.read_bytes()).hexdigest() - - active_spec = workspace / ".work-bundle/orchestration/spec/active" - active_plan = workspace / ".work-bundle/orchestration/plan/active" - (active_plan / "plan-flow").mkdir(parents=True) - (active_spec / "spec-origin.md").write_text( - "---\nid: spec-origin\nstatus: verified\n---\n# Origin specification\n", - encoding="utf-8", - ) - residual = active_spec / "spec-residual.md" - residual.write_text( - "---\nid: spec-residual\nstatus: active\n---\n" - "# Residual findings\n\n- Product acceptance remains unresolved.\n", - encoding="utf-8", - ) - (active_plan / "plan-origin.md").write_text( - "---\nid: plan-flow\nstatus: In progress\n---\n# Origin plan\n", - encoding="utf-8", - ) - (active_plan / "plan-flow/task-001.md").write_text( - "---\nid: task-001\nplan_id: plan-flow\nphase_id: phase-001\n" - "status: Completed\n---\n# Terminal task\n", - encoding="utf-8", - ) - - source = tmp_path / "clean-source" - source.mkdir() - subprocess.run(["git", "-C", str(source), "init", "-q"], check=True) - subprocess.run( - ["git", "-C", str(source), "config", "user.email", "test@example.com"], - check=True, - ) - subprocess.run( - ["git", "-C", str(source), "config", "user.name", "Test"], check=True - ) - (source / "product.txt").write_text("unresolved\n", encoding="utf-8") - subprocess.run(["git", "-C", str(source), "add", "."], check=True) - subprocess.run(["git", "-C", str(source), "commit", "-qm", "baseline"], check=True) - baseline = { - "repository_id": "product-main", - "project_root": str(source), - "commit": _git(source, "rev-parse", "HEAD"), - "tree": _git(source, "rev-parse", "HEAD^{tree}"), - } - - config = tmp_path / "config" - registry = config / "registry/projects.yaml" - registry.parent.mkdir(parents=True) - registry.write_text( - yaml.safe_dump( - { - "device_bindings": { - "workspace-test": { - "workspace_root": str(workspace), - "repositories": [ - { - "repository_id": "product-main", - "project_root": str(member), - } - ], - } - } - }, - sort_keys=False, - ), - encoding="utf-8", - ) - (config / "bootstrap.yaml").write_text( - yaml.safe_dump({"project_registry": str(registry)}, sort_keys=False), - encoding="utf-8", - ) - return workspace, member, worktree, baseline, backup, backup_sha256 - - -def test_two_stage_public_workflow_exhausts_finalizes_restores_and_blocks_without_side_effects( - tmp_path: Path, -) -> None: - workspace, member, worktree, baseline, backup, backup_sha256 = _write_workspace( - tmp_path - ) - env = {**os.environ, "WB_CONFIG_ROOT": str(tmp_path / "config")} - target = { - "artifact_id": "plan-flow", - "revision": "final", - "sha256": hashlib.sha256(b"final-target").hexdigest(), - "source_tree": baseline["tree"], - } - attempts = [{"execution_id": "executor-1", "state": "completed"}] - - for number in range(1, 6): - begin = _run( - ENTRYPOINTS[(number - 1) % 2], - member, - "begin-review-round", - "--project-root", - str(member), - "--flow-id", - "plan-flow", - "--request-id", - f"round-request-{number}", - "--review-id", - f"review-double-{number}", - "--target-identity", - json.dumps({**target, "revision": str(number)}), - "--executor-attempts", - json.dumps(attempts), - "--known-missing-evidence", - json.dumps(["integrated_product_acceptance"]), - env=env, - ) - assert begin.returncode == 0, begin.stderr - reserved = json.loads(begin.stdout) - complete = _run( - ENTRYPOINTS[number % 2], - workspace, - "complete-review-round", - "--project-root", - str(workspace), - "--flow-id", - "plan-flow", - "--round-id", - reserved["round_id"], - "--outcome", - "blocked", - "--audit-block", - json.dumps( - { - "code": "reviewer-double-residual", - "round": number, - "missing": ["integrated_product_acceptance"], - } - ), - env=env, - ) - assert complete.returncode == 0, complete.stderr - - before_refusal = _snapshot(workspace) - sixth = _run( - ENTRYPOINTS[0], - workspace, - "begin-review-round", - "--project-root", - str(workspace), - "--flow-id", - "plan-flow", - "--request-id", - "round-request-6", - "--review-id", - "review-double-6", - "--target-identity", - json.dumps(target), - "--executor-attempts", - json.dumps(attempts), - "--known-missing-evidence", - json.dumps(["integrated_product_acceptance"]), - env=env, - ) - assert sixth.returncode != 0 - assert "WB_ORCHESTRATION_FINALIZATION_REQUIRED" in sixth.stderr - assert _snapshot(workspace) == before_refusal - - finalized = _run( - ENTRYPOINTS[1], - worktree, - "finalize-with-blockers", - "--project-root", - str(workspace), - "--flow-id", - "plan-flow", - "--request-id", - "finalize-request-1", - "--blocker-id", - "BLOCK-plan-flow", - "--residual-spec-id", - "spec-residual", - "--residual-spec", - str(workspace / ".work-bundle/orchestration/spec/active/spec-residual.md"), - "--origin-spec-id", - "spec-origin", - "--origin-plan-id", - "plan-flow", - "--source-baselines", - json.dumps([baseline]), - "--knowledge-return", - json.dumps({"status": "not-needed", "evidence_ref": None}), - env=env, - ) - assert finalized.returncode == 0, finalized.stderr - result = json.loads(finalized.stdout) - assert result["outcome"] == "closed_with_blockers" - assert result["source_baselines"] == [ - {key: baseline[key] for key in ("repository_id", "commit", "tree")} - ] - assert not (workspace / ".work-bundle/orchestration/spec/active/spec-origin.md").exists() - assert (workspace / ".work-bundle/orchestration/spec/archived/spec-origin.md").is_file() - assert not (workspace / ".work-bundle/orchestration/plan/active/plan-origin.md").exists() - assert (workspace / ".work-bundle/orchestration/plan/archived/plan-origin.md").is_file() - assert not (workspace / ".work-bundle/orchestration/reviews").exists() - - metadata_path = workspace / ".work-bundle/project.yaml" - current = yaml.safe_load(metadata_path.read_text(encoding="utf-8")) - other_spec = workspace / ".work-bundle/orchestration/spec/active/spec-other.md" - other_spec.write_text("# Other unresolved work\n", encoding="utf-8") - current["orchestration_control"]["blockers"].append( - { - "id": "BLOCK-OTHER", - "status": "active", - "origin_plan": "plan-other", - "specification": other_spec.relative_to(workspace).as_posix(), - } - ) - current["orchestration_control"]["unrelated_extension"] = { - "preserve": ["newer", "state", "after-finalization"] - } - metadata_path.write_text(yaml.safe_dump(current, sort_keys=False), encoding="utf-8") - - assert hashlib.sha256(backup.read_bytes()).hexdigest() == backup_sha256 - assert bounded_closure.restore_implementation_exception( - workspace, - backup_path=backup, - backup_sha256=backup_sha256, - flow_id="plan-flow", - blocker_id="BLOCK-WOR113", - ) - restored = yaml.safe_load(metadata_path.read_text(encoding="utf-8"))[ - "orchestration_control" - ] - assert restored["implementation_exemptions"] == [] - assert {blocker["id"] for blocker in restored["blockers"]} == { - "BLOCK-WOR113", - "BLOCK-plan-flow", - "BLOCK-OTHER", - } - assert restored["unrelated_extension"] == { - "preserve": ["newer", "state", "after-finalization"] - } - restored["blockers"].sort( - key=lambda blocker: blocker["id"] != "BLOCK-WOR113" - ) - current = yaml.safe_load(metadata_path.read_text(encoding="utf-8")) - current["orchestration_control"] = restored - metadata_path.write_text(yaml.safe_dump(current, sort_keys=False), encoding="utf-8") - - candidate = tmp_path / "candidate.md" - candidate.write_text("# Must not be consumed\n", encoding="utf-8") - contexts = ( - (ENTRYPOINTS[0], workspace, ("--project-root", str(workspace))), - (ENTRYPOINTS[0], member, ("--project-root", str(member))), - (ENTRYPOINTS[0], worktree, ("--project-root", str(workspace))), - ) - before_denials = _snapshot(workspace) - for index, (entrypoint, cwd, locator) in enumerate(contexts, start=1): - denied = _run( - entrypoint, - cwd, - "write-spec", - *locator, - "--title", - "Denied candidate", - "--purpose", - "prove pre-side-effect refusal", - "--component", - "test", - "--content-file", - str(candidate), - "--id", - f"spec-denied-{index}", - env=env, - ) - assert denied.returncode != 0 - assert all( - token in denied.stderr - for token in ( - "WB_ORCHESTRATION_ADMISSION_BLOCKED", - str(metadata_path), - "BLOCK-WOR113", - "spec-wor113.md", - "orch-bounded-closure", - ) - ), denied.stderr - assert _snapshot(workspace) == before_denials - - for index, (cwd, locator) in enumerate( - ( - (workspace, ("--project-root", str(workspace))), - (member, ("--project-root", str(member))), - (worktree, ("--project-root", str(workspace))), - ), - start=1, - ): - denied = _run( - ENTRYPOINTS[1], - cwd, - "begin-review-round", - *locator, - "--flow-id", - f"unrelated-flow-{index}", - "--request-id", - f"denied-round-request-{index}", - "--review-id", - f"denied-review-double-{index}", - "--target-identity", - json.dumps(target), - "--executor-attempts", - json.dumps(attempts), - "--known-missing-evidence", - json.dumps(["integrated_product_acceptance"]), - env=env, - ) - assert denied.returncode != 0 - assert all( - token in denied.stderr - for token in ( - "WB_ORCHESTRATION_ADMISSION_BLOCKED", - str(metadata_path), - "BLOCK-WOR113", - "spec-wor113.md", - "orch-bounded-closure", - ) - ), denied.stderr - assert _snapshot(workspace) == before_denials diff --git a/tests/test_bounded_review_lifecycle.py b/tests/test_bounded_review_lifecycle.py deleted file mode 100644 index 8f4b6d2..0000000 --- a/tests/test_bounded_review_lifecycle.py +++ /dev/null @@ -1,347 +0,0 @@ -from __future__ import annotations - -import sys -from concurrent.futures import ThreadPoolExecutor -import hashlib -from pathlib import Path - -import pytest - - -REPO_ROOT = Path(__file__).resolve().parents[1] -ORCHESTRATION = REPO_ROOT / "scripts" / "orchestration" -if str(ORCHESTRATION) not in sys.path: - sys.path.insert(0, str(ORCHESTRATION)) - -import bounded_closure # noqa: E402 - - -def _workspace(tmp_path: Path, *, limit: int = 5) -> Path: - metadata = tmp_path / ".work-bundle/project.yaml" - metadata.parent.mkdir(parents=True) - metadata.write_text( - "metadata_version: 4\n" - "workspace:\n" - " id: workspace-test\n" - " slug: test\n" - " mode: single-repository\n" - "orchestration_control:\n" - " schema_version: 1\n" - f" post_execution_review_round_limit: {limit}\n", - encoding="utf-8", - ) - return tmp_path - - -def _target(revision: str = "a" * 40) -> dict[str, object]: - return { - "artifact_id": "plan-flow", - "revision": revision, - "sha256": "b" * 64, - "source_tree": "c" * 40, - } - - -def _begin(root: Path, number: int = 1, **overrides: object) -> dict[str, object]: - arguments: dict[str, object] = { - "flow_id": "flow-stable", - "request_id": f"request-{number}", - "review_id": f"review-{number}", - "target_identity": _target(chr(96 + number) * 40), - "executor_attempts": [ - {"execution_id": "executor-1", "state": "completed"}, - ], - "known_missing_evidence": [], - } - arguments.update(overrides) - return bounded_closure.begin_review_round(root, **arguments) - - -@pytest.mark.parametrize( - "missing", - ["accepted_result", "validation_receipt", "reviewer_provenance"], -) -def test_round_one_allows_each_known_missing_evidence_but_refuses_active_executor( - tmp_path: Path, missing: str, -) -> None: - root = _workspace(tmp_path) - - with pytest.raises( - bounded_closure.BoundedClosureError, - match="WB_POST_EXECUTION_NOT_TERMINAL", - ): - _begin( - root, - executor_attempts=[{"execution_id": "executor-active", "state": "running"}], - ) - - reserved = _begin( - root, - known_missing_evidence=[missing], - ) - - assert reserved["round_number"] == 1 - assert reserved["execution_complete"] is True - assert reserved["known_missing_evidence"] == [missing] - assert reserved["state"] == "reserved" - - -def test_begin_is_idempotent_only_for_exact_request_and_target(tmp_path: Path) -> None: - root = _workspace(tmp_path) - first = _begin(root) - - assert _begin(root) == first - changed = _begin( - root, - review_id="review-2", - target_identity=_target("d" * 40), - ) - - assert changed["round_number"] == 2 - assert changed["round_id"] != first["round_id"] - - -def test_completed_rounds_survive_plan_revision_and_fifth_requires_finalization( - tmp_path: Path, -) -> None: - root = _workspace(tmp_path) - - for number in range(1, 6): - reserved = _begin(root, number) - completed = bounded_closure.complete_review_round( - root, - flow_id="flow-stable", - round_id=str(reserved["round_id"]), - outcome="blocked", - audit_block={ - "code": "missing-evidence", - "missing": ["accepted_result"], - }, - ) - assert completed["round_number"] == number - assert completed["state"] == "completed" - - status = bounded_closure.review_round_status(root, flow_id="flow-stable") - assert status["completed_rounds"] == 5 - assert status["finalization_required"] is True - with pytest.raises( - bounded_closure.BoundedClosureError, - match="WB_POST_EXECUTION_FINALIZATION_REQUIRED", - ): - _begin(root, 6) - - -def test_duplicate_completion_does_not_increment_or_change_judgment(tmp_path: Path) -> None: - root = _workspace(tmp_path) - reserved = _begin(root) - arguments = { - "flow_id": "flow-stable", - "round_id": str(reserved["round_id"]), - "outcome": "blocked", - "audit_block": {"code": "preparation-failed", "missing": ["review"]}, - } - - first = bounded_closure.complete_review_round(root, **arguments) - assert bounded_closure.complete_review_round(root, **arguments) == first - assert bounded_closure.review_round_status(root, flow_id="flow-stable")[ - "completed_rounds" - ] == 1 - with pytest.raises( - bounded_closure.BoundedClosureError, - match="WB_POST_EXECUTION_JUDGMENT_COLLISION", - ): - bounded_closure.complete_review_round( - root, - **{**arguments, "audit_block": {"code": "different", "missing": []}}, - ) - - -def test_reviewer_execution_requires_exact_live_prepared_unjudged_round( - tmp_path: Path, -) -> None: - root = _workspace(tmp_path) - reserved = _begin(root) - - with pytest.raises( - bounded_closure.BoundedClosureError, - match="WB_POST_EXECUTION_ROUND_BINDING_INVALID", - ): - bounded_closure.require_review_round_execution(root, binding=reserved) - - prepared = bounded_closure.mark_review_round_prepared( - root, - flow_id="flow-stable", - round_id=str(reserved["round_id"]), - ) - assert bounded_closure.require_review_round_execution( - root, binding=reserved - ) == prepared - - bounded_closure.complete_review_round( - root, - flow_id="flow-stable", - round_id=str(reserved["round_id"]), - outcome="blocked", - audit_block={"code": "controller-blocked", "missing": ["review"]}, - ) - with pytest.raises( - bounded_closure.BoundedClosureError, - match="WB_POST_EXECUTION_JUDGMENT_ALREADY_RECORDED", - ): - bounded_closure.require_review_round_execution(root, binding=reserved) - - -def test_fifth_prepared_round_can_dispatch_but_other_accepted_round_closes_flow( - tmp_path: Path, -) -> None: - fifth_root = _workspace(tmp_path / "fifth") - for number in range(1, 5): - reserved = _begin(fifth_root, number) - bounded_closure.complete_review_round( - fifth_root, - flow_id="flow-stable", - round_id=str(reserved["round_id"]), - outcome="blocked", - audit_block={"code": "controller-blocked", "missing": ["review"]}, - ) - fifth = _begin(fifth_root, 5) - bounded_closure.mark_review_round_prepared( - fifth_root, - flow_id="flow-stable", - round_id=str(fifth["round_id"]), - ) - assert bounded_closure.require_review_round_execution( - fifth_root, binding=fifth - )["round_number"] == 5 - - accepted_root = _workspace(tmp_path / "accepted") - first = _begin(accepted_root, 1) - bounded_closure.mark_review_round_prepared( - accepted_root, - flow_id="flow-stable", - round_id=str(first["round_id"]), - ) - second = _begin(accepted_root, 2) - bounded_closure.mark_review_round_prepared( - accepted_root, - flow_id="flow-stable", - round_id=str(second["round_id"]), - ) - review_path = accepted_root / ".work-bundle/orchestration/reviews/review-1.json" - review_path.parent.mkdir(parents=True) - review_path.write_text('{"verdict":"accepted"}\n', encoding="utf-8") - review_path.chmod(0o444) - bounded_closure.complete_review_round( - accepted_root, - flow_id="flow-stable", - round_id=str(first["round_id"]), - outcome="accepted", - review_reference={ - "review_id": "review-1", - "sha256": hashlib.sha256(review_path.read_bytes()).hexdigest(), - }, - ) - - with pytest.raises( - bounded_closure.BoundedClosureError, - match="WB_POST_EXECUTION_FINALIZATION_REQUIRED", - ): - bounded_closure.require_review_round_execution( - accepted_root, binding=second - ) - - -def test_accepted_round_stops_further_review_before_limit(tmp_path: Path) -> None: - root = _workspace(tmp_path) - reserved = _begin(root) - bounded_closure.mark_review_round_prepared( - root, - flow_id="flow-stable", - round_id=str(reserved["round_id"]), - ) - review_path = root / ".work-bundle/orchestration/reviews/review-1.json" - review_path.parent.mkdir(parents=True) - review_path.write_text('{"verdict":"accepted"}\n', encoding="utf-8") - review_path.chmod(0o444) - bounded_closure.complete_review_round( - root, - flow_id="flow-stable", - round_id=str(reserved["round_id"]), - outcome="accepted", - review_reference={ - "review_id": "review-1", - "sha256": hashlib.sha256(review_path.read_bytes()).hexdigest(), - }, - ) - - assert bounded_closure.review_round_status( - root, flow_id="flow-stable" - )["finalization_required"] is True - with pytest.raises( - bounded_closure.BoundedClosureError, - match="WB_POST_EXECUTION_FINALIZATION_REQUIRED", - ): - _begin(root, 2) - - -def test_product_outcome_requires_store_owned_immutable_review_reference( - tmp_path: Path, -) -> None: - root = _workspace(tmp_path) - reserved = _begin(root) - bounded_closure.mark_review_round_prepared( - root, - flow_id="flow-stable", - round_id=str(reserved["round_id"]), - ) - - with pytest.raises( - bounded_closure.BoundedClosureError, - match="WB_POST_EXECUTION_EVIDENCE_INVALID", - ): - bounded_closure.complete_review_round( - root, - flow_id="flow-stable", - round_id=str(reserved["round_id"]), - outcome="accepted", - review_reference={"review_id": "review-1", "sha256": "e" * 64}, - ) - - -def test_competing_reservations_cannot_create_round_six(tmp_path: Path) -> None: - root = _workspace(tmp_path) - - def reserve(number: int) -> str: - try: - return str(_begin(root, number)["round_id"]) - except bounded_closure.BoundedClosureError as error: - return error.code - - with ThreadPoolExecutor(max_workers=10) as pool: - outcomes = list(pool.map(reserve, range(1, 11))) - - round_ids = {value for value in outcomes if value.startswith("flow-stable:round:")} - assert round_ids == { - f"flow-stable:round:{number:03d}" for number in range(1, 6) - } - assert outcomes.count("WB_POST_EXECUTION_FINALIZATION_REQUIRED") == 5 - - -def test_current_metadata_migration_renames_legacy_policy_only(tmp_path: Path) -> None: - root = _workspace(tmp_path) - metadata = root / ".work-bundle/project.yaml" - metadata.write_text( - metadata.read_text(encoding="utf-8").replace( - "post_execution_review_round_limit", "review_revision_limit" - ), - encoding="utf-8", - ) - - with pytest.raises( - bounded_closure.BoundedClosureError, - match="WB_POST_EXECUTION_LEGACY_POLICY_REJECTED", - ): - _begin(root) - assert bounded_closure.migrate_bounded_review_policy(root) is True - assert "review_revision_limit" not in metadata.read_text(encoding="utf-8") - assert _begin(root)["round_number"] == 1 diff --git a/tests/test_ci_release_gate.py b/tests/test_ci_release_gate.py index d8c24f1..8e356f9 100644 --- a/tests/test_ci_release_gate.py +++ b/tests/test_ci_release_gate.py @@ -78,16 +78,27 @@ def fake_run(command, **kwargs): def test_release_gate_inputs_are_tracked_and_execution_independent() -> None: - tracked = subprocess.run( - ["git", "ls-files", "tests/test_*.py"], + eligible = subprocess.run( + [ + "git", "ls-files", "--cached", "--others", "--exclude-standard", + "tests/test_*.py", + ], cwd=REPO_ROOT, check=True, capture_output=True, text=True, ).stdout.splitlines() - discovered = sorted(path.relative_to(REPO_ROOT).as_posix() for path in (REPO_ROOT / "tests").glob("test_*.py")) + discovered = sorted(path for path in eligible if (REPO_ROOT / path).is_file()) - assert tracked == discovered + observed = _gate_api()( + REPO_ROOT, + python_executable="/python", + run_command=lambda command, **kwargs: subprocess.CompletedProcess( + command, 0, stdout="", stderr="" + ), + emit=lambda _line: None, + ) + assert observed["modules"] == discovered for path in [CI_ENTRY, REPO_ROOT / "bin" / "work-bundle-skill", REPO_ROOT / ".github" / "workflows" / "ci.yml"]: subprocess.run( ["git", "ls-files", "--error-unmatch", path.relative_to(REPO_ROOT).as_posix()], diff --git a/tests/test_completion_provenance.py b/tests/test_completion_provenance.py index bce8614..ddca545 100644 --- a/tests/test_completion_provenance.py +++ b/tests/test_completion_provenance.py @@ -30,7 +30,6 @@ validate_resume_owner, ) import execution_context # noqa: E402 -import plans # noqa: E402 import completion_provenance # noqa: E402 @@ -207,7 +206,7 @@ def test_kernel_ids_are_globally_unique_across_managed_store(tmp_path): def test_kernel_execution_context_creates_typed_binding_ownership(tmp_path, monkeypatch): # Unit-test ownership after the independently tested stage-gate boundary. monkeypatch.setattr(execution_context, "_find_plan", lambda *_: (tmp_path / "plan.md", {})) - monkeypatch.setattr("review_runtime.require_plan_reviews", lambda *_: None) + (tmp_path / "plan.md").write_text("status: verified\n", encoding="utf-8") execution_root = tmp_path / "execution" execution_root.mkdir() runtime_root = tmp_path / "runtime" @@ -246,7 +245,7 @@ def load_state(*_args): def test_kernel_execution_context_rejects_missing_malformed_or_store_mismatched_ownership(tmp_path, monkeypatch): monkeypatch.setattr(execution_context, "_find_plan", lambda *_: (tmp_path / "plan.md", {})) - monkeypatch.setattr("review_runtime.require_plan_reviews", lambda *_: None) + (tmp_path / "plan.md").write_text("status: verified\n", encoding="utf-8") execution_root = tmp_path / "execution" execution_root.mkdir() runtime_root = tmp_path / "runtime" @@ -543,39 +542,3 @@ def test_execution_workspace_cleanup_rejects_retained_binding(): module.assert_binding_released_for_cleanup(retained) released = {**retained, "state": "released", "current_owner": "task-c01", "repair_owner": None, "releasable": True} assert module.assert_binding_released_for_cleanup(released)["original_owner"] == "task-c01" - - -def test_completed_task_transition_releases_active_binding_and_persists_workspace_owner(tmp_path, monkeypatch): - control_root = tmp_path / "workspace" - (control_root / ".work-bundle").mkdir(parents=True) - store = ManagedProvenanceStore(control_root / ".work-bundle/runtime/completion-provenance") - created = FailureOwnershipV1.create(store, "binding:plan-001:task-c01", "isolated_worktree", "task-c01") - binding = { - "plan_id": "plan-001", - "task_id": "task-c01", - "workspace_id": "workspace-001", - "execution_id": "execution-c01", - "repository_id": "repo-main", - "runtime_root": str(tmp_path / "runtime"), - "ownership": created.to_dict(), - } - persisted = [] - retained = [] - - monkeypatch.setattr(plans, "resolve_workspace_root", lambda _args: control_root) - monkeypatch.setattr(plans, "load_task_execution_binding", lambda *_args: binding) - monkeypatch.setattr(plans, "_persist_binding", lambda value, _root: persisted.append(value)) - monkeypatch.setattr( - plans, - "_execution_workspace_module", - lambda: type("Workspace", (), {"retain_binding_owner": staticmethod(lambda *args, **kwargs: retained.append((args, kwargs)))})(), - ) - - released = plans._release_completed_task_binding( - type("Args", (), {"workspace_root": str(control_root)})(), - {"id": "task-c01", "plan_id": "plan-001", "phase_id": "phase-c"}, - ) - - assert released["state"] == "released" - assert persisted[0]["ownership"] == released - assert retained[0][1]["ownership"] == released diff --git a/tests/test_control_plane_v4.py b/tests/test_control_plane_v4.py index 8e5b3ba..ce7adb6 100644 --- a/tests/test_control_plane_v4.py +++ b/tests/test_control_plane_v4.py @@ -11,6 +11,9 @@ import tempfile import unittest +import pytest +import yaml + REPO_ROOT = Path(__file__).resolve().parents[1] WORK_BUNDLE_SCRIPTS = REPO_ROOT / "scripts/work-bundle" @@ -19,17 +22,13 @@ if loaded_core_path is not None and WORK_BUNDLE_SCRIPTS not in loaded_core_path.parents: sys.modules.pop("core", None) sys.path.insert(0, str(WORK_BUNDLE_SCRIPTS)) -from workspace_resources import CREDENTIAL_TEMPLATE, SCRIPT_INDEX_TEMPLATE -from control_plane import ( - ControlPlaneError, - deferred_remote_task_identity, - validate_deferred_remote_independent_review_identity, -) +from workspace_resources import CREDENTIAL_TEMPLATE, SCRIPT_INDEX_TEMPLATE, _load_yaml +from control_plane import ControlPlaneError def run_wb(config_root: Path, *args: str) -> subprocess.CompletedProcess[str]: env = os.environ.copy() - env["WB_CONFIG_ROOT"] = str(config_root) + env["HOME"] = str(config_root.parent) return subprocess.run( [sys.executable, str(REPO_ROOT / "scripts/wb.py"), *args], cwd=REPO_ROOT, @@ -42,7 +41,7 @@ def run_wb(config_root: Path, *args: str) -> subprocess.CompletedProcess[str]: def run_orch(config_root: Path, *args: str) -> subprocess.CompletedProcess[str]: env = os.environ.copy() - env["WB_CONFIG_ROOT"] = str(config_root) + env["HOME"] = str(config_root.parent) return subprocess.run( [sys.executable, str(REPO_ROOT / "scripts/orch.py"), *args], cwd=REPO_ROOT, @@ -61,7 +60,7 @@ def git(path: Path, *args: str) -> str: def config_root(tmp_path: Path) -> Path: - root = tmp_path / "config" + root = tmp_path / ".work-bundle" (root / "registry").mkdir(parents=True) (root / "bootstrap.yaml").write_text( "\n".join( @@ -142,6 +141,26 @@ def make_v3_workspace(tmp_path: Path) -> tuple[Path, Path, str]: return workspace, remote, head +def contain_v3_checkout(workspace: Path) -> Path: + metadata = workspace / ".work-bundle/project.yaml" + original = metadata.read_text(encoding="utf-8") + document = yaml.safe_load(original) + old_checkout = Path(str(document["project_root"])) + checkout = workspace / "source-main" + old_checkout.rename(checkout) + replacements = { + f"project_root: {old_checkout}": f"project_root: {checkout}", + f"git_control_root: {old_checkout / '.git'}": f"git_control_root: {checkout / '.git'}", + } + lines = [] + for line in original.splitlines(): + stripped = line.strip() + replacement = replacements.get(stripped) + lines.append(f"{line[: len(line) - len(line.lstrip())]}{replacement}" if replacement else line) + metadata.write_text("\n".join(lines) + "\n", encoding="utf-8") + return checkout + + def make_v3_single_workspace(tmp_path: Path, *, tracked_agents: bool = False) -> tuple[Path, Path, str]: remote = tmp_path / "single-source.git" workspace = tmp_path / "single-workspace" @@ -219,6 +238,40 @@ def migrate(config: Path, workspace: Path) -> dict[str, object]: return json.loads(applied.stdout) +def test_migrate_control_plane_routes_current_and_unsupported_before_legacy_proposal(tmp_path: Path) -> None: + config = config_root(tmp_path) + workspace = tmp_path / "current-workspace" + initialized = run_wb( + config, + "init-workspace", + str(workspace), + "--mode", + "multi-repository", + "--slug", + "current", + "--optional-repository", + "source=ssh://git@example.test/source.git", + "--apply", + ) + assert initialized.returncode == 0, initialized.stdout + initialized.stderr + + for action in ("--dry-run", "--apply"): + current = run_wb(config, "migrate-control-plane", str(workspace), action) + assert current.returncode == 0, current.stdout + current.stderr + payload = json.loads(current.stdout) + assert payload["migration"] == {"from_version": 4, "to_version": 4, "disposition": "current"} + assert payload["changed_files"] == [] + + unsupported = tmp_path / "unsupported" + unsupported.joinpath(".work-bundle").mkdir(parents=True) + unsupported.joinpath(".work-bundle/project.yaml").write_text( + "metadata_version: 99\nlegacy_shape: deliberately-incomplete\n", encoding="utf-8" + ) + rejected = run_wb(config, "migrate-control-plane", str(unsupported), "--dry-run") + assert rejected.returncode == 1 + assert json.loads(rejected.stdout)["failure_code"] == "WB_CONTROL_PLANE_MIGRATION_SOURCE_UNSUPPORTED" + + def portable_multi_workspace( tmp_path: Path, repositories: list[tuple[str, Path]] ) -> tuple[Path, bytes]: @@ -258,6 +311,15 @@ def test_v3_to_v4_migration_is_deterministic_and_splits_local_state(tmp_path: Pa assert "workspace_root" in first_data["proposal"]["local_fields_to_move"] assert first_data["proposal"]["repositories"][0]["id"] == "source-main" assert "runtime/" in first_data["proposal"]["local_only_paths"] + portable_paths = first_data["proposal"]["portable_paths"] + assert "orchestration/handoff/" not in portable_paths + for current in ( + "orchestration/result/executor/", + "orchestration/result/accepted/", + "orchestration/review/implementation/", + "orchestration/review/final/", + ): + assert current in portable_paths applied = run_wb( config, @@ -368,43 +430,11 @@ def test_migration_uses_registry_remote_when_live_origin_chain_ends_locally(tmp_ assert json.loads(proposed.stdout)["proposal"]["repositories"][0]["canonical_remote"] == "ssh://git@example.test/team/source" -def test_fallback_yaml_scalar_rejects_yaml_indicators_but_allows_mid_scalar_ampersand() -> None: - script = """ -import json -import workspace_resources - -workspace_resources.yaml = None -values = {} -for label, scalar in { - "anchor": "&anchor value", - "alias": "*alias", - "core_tag": "!!str value", - "custom_tag": "!custom value", - "verbatim_tag": "!<tag:example.com,2026:x> value", -}.items(): - try: - workspace_resources._load_yaml(f"value: {scalar}\\n") - except ValueError as exc: - values[label] = str(exc) -values["path"] = workspace_resources._load_yaml("value: /Volumes/ext/DTM&RPG\\n")["value"] -print(json.dumps(values, sort_keys=True)) -""" - result = subprocess.run( - [sys.executable, "-c", script], - cwd=REPO_ROOT / "scripts/work-bundle", - check=False, - capture_output=True, - text=True, - ) - assert result.returncode == 0, result.stdout + result.stderr - assert json.loads(result.stdout) == { - "alias": "unsupported YAML token", - "anchor": "unsupported YAML token", - "core_tag": "unsupported YAML token", - "custom_tag": "unsupported YAML token", - "path": "/Volumes/ext/DTM&RPG", - "verbatim_tag": "unsupported YAML token", - } +def test_maintained_yaml_parser_is_safe_and_accepts_normal_scalars() -> None: + assert _load_yaml("value: /Volumes/ext/DTM&RPG\n") == {"value": "/Volumes/ext/DTM&RPG"} + assert _load_yaml("value: {nested: true}\n") == {"value": {"nested": True}} + with pytest.raises(yaml.YAMLError): + _load_yaml("value: !custom unsafe\n") def test_migration_preserves_existing_control_plane_gitignore_content(tmp_path: Path) -> None: @@ -506,7 +536,7 @@ def test_migration_blocks_registry_and_live_network_remote_conflict_without_over def test_attach_and_doctor_resolve_local_source_origin_chain(tmp_path: Path) -> None: config_a = config_root(tmp_path / "device-a") workspace, network_remote, _ = make_v3_workspace(tmp_path / "fixture") - checkout = tmp_path / "fixture/source" + checkout = contain_v3_checkout(workspace) local_origin = tmp_path / "local-origin" subprocess.run(["git", "clone", "-q", "--", str(network_remote), str(local_origin)], check=True) git(local_origin, "remote", "set-url", "origin", "ssh://git@example.test/team/source.git") @@ -543,6 +573,9 @@ def test_legacy_project_commands_accept_v4_and_doctor_repair_preserves_portable_ workspace, _, _ = make_v3_workspace(tmp_path) migrate(config, workspace) checkout = tmp_path / "source" + contained_checkout = workspace / "source-main" + checkout.rename(contained_checkout) + checkout = contained_checkout attached = run_wb( config, "attach-workspace", @@ -641,6 +674,28 @@ def test_single_repository_init_creates_workspace_resources(tmp_path: Path) -> N assert credential_file.read_text(encoding="utf-8") == CREDENTIAL_TEMPLATE assert credential_file.parent.stat().st_mode & 0o777 == 0o700 assert credential_file.stat().st_mode & 0o777 == 0o600 + orchestration = workspace / ".work-bundle/orchestration" + for retired in ("handoff", "plan/index.jsonl", "handoff/index.jsonl"): + assert not (orchestration / retired).exists() + for current in ( + "spec/active", + "spec/archived", + "plan/active", + "plan/archived", + "result/executor/active", + "result/executor/reviewed", + "result/executor/superseded", + "result/executor/archived", + "result/accepted/active", + "result/accepted/superseded", + "result/accepted/archived", + "review/implementation/active", + "review/implementation/superseded", + "review/implementation/archived", + "review/final/active", + "review/final/archived", + ): + assert (orchestration / current).is_dir() def test_single_repository_init_preserves_workspace_resources(tmp_path: Path) -> None: @@ -894,12 +949,12 @@ def test_attach_reconstructs_distinct_device_binding_without_portable_diff(tmp_p (control_b / "project.yaml").write_bytes(portable) (control_b / "knowledge").mkdir() config_b = config_root(tmp_path / "device-b-config") - attached = run_wb(config_b, "attach-workspace", str(workspace_b), "--materialize", "none", "--apply") + attached = run_wb(config_b, "attach-workspace", str(workspace_b), "--materialize", "missing", "--apply") assert attached.returncode == 0, attached.stdout + attached.stderr data = json.loads(attached.stdout) assert data["portable_status"] == "passed" - assert data["execution_ready"] is False - assert data["repositories"][0]["state"] == "absent" + assert data["execution_ready"] is True + assert data["repositories"][0]["state"] == "materialized-managed" assert (control_b / "project.yaml").read_bytes() == portable assert str(workspace_b) in (config_b / "registry/projects.yaml").read_text(encoding="utf-8") assert (workspace_b / "script/index.yaml").is_file() @@ -919,6 +974,9 @@ def test_attach_adopts_only_matching_remote_and_detach_is_local_only(tmp_path: P _, compatible, _ = make_remote(tmp_path / "matching", "compatible") git(compatible, "remote", "set-url", "origin", str(remote)) + contained_compatible = workspace / "source-main" + compatible.rename(contained_compatible) + compatible = contained_compatible attached = run_wb( config_b, "attach-workspace", @@ -933,6 +991,7 @@ def test_attach_adopts_only_matching_remote_and_detach_is_local_only(tmp_path: P assert json.loads(attached.stdout)["repositories"][0]["state"] == "compatible-existing" _, conflict, _ = make_remote(tmp_path / "conflict", "wrong") + git(compatible, "remote", "set-url", "origin", str(conflict.parent / "wrong.git")) rejected = run_wb( config_b, "attach-workspace", @@ -940,7 +999,7 @@ def test_attach_adopts_only_matching_remote_and_detach_is_local_only(tmp_path: P "--materialize", "none", "--repository-path", - f"source-main={conflict}", + f"source-main={compatible}", "--apply", ) assert rejected.returncode == 1 @@ -970,6 +1029,9 @@ def test_orchestration_preflight_resolves_v4_local_binding(tmp_path: Path) -> No migrate(config, workspace) _, checkout, _ = make_remote(tmp_path / "attached", "checkout") git(checkout, "remote", "set-url", "origin", str(remote)) + contained_checkout = workspace / "source-main" + checkout.rename(contained_checkout) + checkout = contained_checkout attached = run_wb( config, "attach-workspace", @@ -982,7 +1044,7 @@ def test_orchestration_preflight_resolves_v4_local_binding(tmp_path: Path) -> No ) assert attached.returncode == 0, attached.stdout + attached.stderr - preflight = run_orch(config, "repository-preflight", "--project-root", str(workspace)) + preflight = run_orch(config, "repository-preflight", "--workspace-root", str(workspace)) assert preflight.returncode == 0, preflight.stdout + preflight.stderr repositories = json.loads(preflight.stdout)["repository_preflight"]["repositories"] assert [row["path"] for row in repositories] == [str(checkout.resolve())] @@ -1024,7 +1086,7 @@ def test_v4_attach_doctor_and_preflight_share_bootstrap_resolved_registry(tmp_pa assert attached.returncode == 0, attached.stdout + attached.stderr doctor = run_wb(config, "doctor-workspace", str(workspace)) assert doctor.returncode == 0, doctor.stdout + doctor.stderr - preflight = run_orch(config, "repository-preflight", "--project-root", str(workspace)) + preflight = run_orch(config, "repository-preflight", "--workspace-root", str(workspace)) assert preflight.returncode == 0, preflight.stdout + preflight.stderr registry_text = custom_registry.read_text(encoding="utf-8") @@ -1140,6 +1202,9 @@ def test_doctor_repair_preserves_existing_and_unknown_local_binding_fields(tmp_p migrate(config, workspace) _, checkout, _ = make_remote(tmp_path / "attached", "checkout") git(checkout, "remote", "set-url", "origin", str(remote)) + contained_checkout = workspace / "source-main" + checkout.rename(contained_checkout) + checkout = contained_checkout attached = run_wb( config, "attach-workspace", @@ -1173,6 +1238,7 @@ def test_doctor_repair_preserves_existing_and_unknown_local_binding_fields(tmp_p def test_doctor_workspace_repair_does_not_create_script_index(tmp_path: Path) -> None: config = config_root(tmp_path / "config-root") workspace, _, _ = make_v3_workspace(tmp_path / "fixture") + contain_v3_checkout(workspace) migrate(config, workspace) script_index = workspace / "script/index.yaml" script_index.unlink(missing_ok=True) @@ -1257,6 +1323,9 @@ def test_orchestration_blocks_when_current_local_head_outgrows_device_observatio migrate(config, workspace) _, checkout, _ = make_remote(tmp_path / "attached", "checkout") git(checkout, "remote", "set-url", "origin", str(remote)) + contained_checkout = workspace / "source-main" + checkout.rename(contained_checkout) + checkout = contained_checkout attached = run_wb( config, "attach-workspace", @@ -1273,7 +1342,7 @@ def test_orchestration_blocks_when_current_local_head_outgrows_device_observatio (checkout / "later.txt").write_text("later\n", encoding="utf-8") git(checkout, "add", "later.txt") git(checkout, "commit", "-q", "-m", "later local commit") - preflight = run_orch(config, "repository-preflight", "--project-root", str(workspace)) + preflight = run_orch(config, "repository-preflight", "--workspace-root", str(workspace)) payload = json.loads(preflight.stdout)["repository_preflight"] row = payload["repositories"][0] assert payload["status"] == "blocked" @@ -1347,6 +1416,9 @@ def test_doctor_reports_deleted_bound_checkout_not_ready(tmp_path: Path) -> None migrate(config, workspace) _, checkout, _ = make_remote(tmp_path / "attached", "checkout") git(checkout, "remote", "set-url", "origin", str(remote)) + contained_checkout = workspace / "source-main" + checkout.rename(contained_checkout) + checkout = contained_checkout assert run_wb(config, "attach-workspace", str(workspace), "--repository-path", f"source-main={checkout}", "--materialize", "none", "--apply").returncode == 0 checkout.rename(checkout.with_name("checkout-moved")) doctor = run_wb(config, "doctor-workspace", str(workspace)) @@ -1391,8 +1463,7 @@ def test_v4_schema_rejects_duplicate_repository_ids_and_invalid_mode(tmp_path: P metadata.write_text(text.replace(" mode: multi-repository", " mode: invalid").replace("agents_sync:", duplicate + "agents_sync:", 1), encoding="utf-8") doctor = run_wb(config, "doctor-workspace", str(workspace)) failures = json.loads(doctor.stdout)["portable"]["failures"] - assert "WB_CONTROL_PLANE_WORKSPACE_MODE_INVALID" in failures - assert "WB_CONTROL_PLANE_REPOSITORY_ID_DUPLICATE:source-main" in failures + assert failures == ["WB_INFRASTRUCTURE_SCHEMA_INVALID"] def test_non_git_v3_member_migrates_with_manual_locator(tmp_path: Path) -> None: @@ -1657,6 +1728,9 @@ def test_attach_and_doctor_report_wrong_branch_and_dirty_checkout_not_ready(tmp_ migrate(config, workspace) _, checkout, _ = make_remote(tmp_path / "attached", "checkout") git(checkout, "remote", "set-url", "origin", str(remote)) + contained_checkout = workspace / "source-main" + checkout.rename(contained_checkout) + checkout = contained_checkout git(checkout, "checkout", "-q", "-b", "wrong") (checkout / "dirty.txt").write_text("dirty\n", encoding="utf-8") attached = run_wb(config, "attach-workspace", str(workspace), "--repository-path", f"source-main={checkout}", "--materialize", "none", "--apply") @@ -1681,7 +1755,7 @@ def test_attach_converges_duplicate_agents_sections(tmp_path: Path) -> None: block = f"# ========================\n# Work Bundle RULE START\n# ========================\n{template}# ========================\n# Work Bundle RULE END\n# ========================\n" (workspace_b / "AGENTS.md").write_text("user-before\n" + block + "user-middle\n" + block + "user-after\n", encoding="utf-8") config_b = config_root(tmp_path / "config-b") - attached = run_wb(config_b, "attach-workspace", str(workspace_b), "--materialize", "none", "--apply") + attached = run_wb(config_b, "attach-workspace", str(workspace_b), "--materialize", "missing", "--apply") assert attached.returncode == 0, attached.stdout + attached.stderr agents = (workspace_b / "AGENTS.md").read_text(encoding="utf-8") assert agents.count("# Work Bundle RULE START") == 1 @@ -1720,6 +1794,38 @@ def init_single_v4( return config, workspace, remote, workspace_id +def test_register_project_uses_structured_v4_registry_without_binding_loss(tmp_path: Path) -> None: + config, workspace, _, workspace_id = init_single_v4(tmp_path, attach=False) + registry = config / "registry/projects.yaml" + before = yaml.safe_load(registry.read_text(encoding="utf-8")) + + registered = run_wb(config, "register-project", str(workspace), "--name", "renamed-demo") + + assert registered.returncode == 0, registered.stdout + registered.stderr + after = yaml.safe_load(registry.read_text(encoding="utf-8")) + assert after["device_bindings"][workspace_id] == before["device_bindings"][workspace_id] + matching = [entry for entry in after["projects"] if entry["slug"] == "renamed-demo"] + assert len(matching) == 1 + assert matching[0]["name"] == "renamed-demo" + + +def test_register_project_rejects_malformed_v4_before_registry_mutation(tmp_path: Path) -> None: + config, workspace, _, _ = init_single_v4(tmp_path, attach=False) + registry = config / "registry/projects.yaml" + registry_before = registry.read_bytes() + metadata = workspace / ".work-bundle/project.yaml" + metadata.write_text( + metadata.read_text(encoding="utf-8").replace("authority: canonical\n", ""), + encoding="utf-8", + ) + + registered = run_wb(config, "register-project", str(workspace), "--name", "must-not-write") + + assert registered.returncode == 1, registered.stdout + registered.stderr + assert json.loads(registered.stdout)["failure_code"] == "WB_INFRASTRUCTURE_SCHEMA_INVALID" + assert registry.read_bytes() == registry_before + + def add_workspace_member_args( workspace: Path, remote: Path | str, @@ -1773,108 +1879,6 @@ def write_composite_metadata(workspace: Path, *, include_root: bool = True, memb metadata.write_text(text, encoding="utf-8") -def test_deferred_remote_independent_review_identity() -> None: - if not os.environ.get("WOR105_C02_REVIEW"): - raise unittest.SkipTest("WOR105_C02_REVIEW selects the real independent review artifact") - validated = validate_deferred_remote_independent_review_identity(REPO_ROOT, task_id="task-c02") - assert validated["reviewed_tree"] == git(REPO_ROOT, "rev-parse", "HEAD^{tree}") - - -def canonical_repository_identity(repository: Path) -> dict[str, str]: - orchestration = REPO_ROOT / "scripts/orchestration" - command = "\n".join( - [ - "import hashlib, json, sys", - "from pathlib import Path", - f"sys.path.insert(0, {str(orchestration)!r})", - "from repository_preflight import capture_repository_evidence", - "evidence = capture_repository_evidence(Path(sys.argv[1]))", - "digest = hashlib.sha256(json.dumps(evidence, sort_keys=True, separators=(',', ':')).encode('utf-8')).hexdigest()", - "print(json.dumps({'head': evidence['head'], 'tree': evidence['tree'], 'digest': digest}))", - ] - ) - result = subprocess.run( - [sys.executable, "-c", command, str(repository)], - cwd=REPO_ROOT, - check=True, - capture_output=True, - text=True, - ) - return json.loads(result.stdout) - - -def test_deferred_remote_task_identity_equals_canonical_repository_evidence(tmp_path) -> None: - _, repository, _ = make_remote(tmp_path, "canonical-identity-source") - canonical = canonical_repository_identity(repository) - identity = deferred_remote_task_identity(repository) - bespoke_evidence = { - "repository": repository.resolve().name, - "branch": git(repository, "branch", "--show-current"), - "head": canonical["head"], - "tree": canonical["tree"], - "status": "clean", - } - bespoke_digest = hashlib.sha256( - json.dumps(bespoke_evidence, sort_keys=True, separators=(",", ":")).encode("utf-8") - ).hexdigest() - - assert identity["repository_evidence_sha256"] == canonical["digest"] - assert identity["repository_evidence_sha256"] != bespoke_digest - - -def test_deferred_remote_review_identity_contract_rejects_mismatch(tmp_path, monkeypatch) -> None: - _, repository, _ = make_remote(tmp_path, "identity-source") - canonical = canonical_repository_identity(repository) - identity = deferred_remote_task_identity(repository) - assert identity["repository_evidence_sha256"] == canonical["digest"] - review_path = tmp_path / "review.yaml" - accepted = { - "task_id": "task-c02", - "reviewer_independent": True, - "verdict": "accept", - "reviewed_head": identity["reviewed_head"], - "reviewed_tree": identity["reviewed_tree"], - } - review_path.write_text(json.dumps(accepted), encoding="utf-8") - monkeypatch.setenv("WOR105_C02_REVIEW", str(review_path)) - assert validate_deferred_remote_independent_review_identity( - repository, task_id="task-c02" - )["reviewed_head"] == identity["reviewed_head"] - - for key, value in ( - ("task_id", "task-c01"), - ("reviewer_independent", False), - ("verdict", "repair"), - ("reviewed_head", "0" * 40 + "+repository-evidence-sha256:" + "0" * 64), - ("reviewed_tree", "0" * 40), - ): - review_path.write_text(json.dumps({**accepted, key: value}), encoding="utf-8") - with unittest.TestCase().assertRaisesRegex(ControlPlaneError, "REVIEW_IDENTITY_MISMATCH"): - validate_deferred_remote_independent_review_identity(repository, task_id="task-c02") - - bespoke_evidence = { - "repository": repository.resolve().name, - "branch": git(repository, "branch", "--show-current"), - "head": canonical["head"], - "tree": canonical["tree"], - "status": "clean", - } - bespoke_digest = hashlib.sha256( - json.dumps(bespoke_evidence, sort_keys=True, separators=(",", ":")).encode("utf-8") - ).hexdigest() - review_path.write_text( - json.dumps( - { - **accepted, - "reviewed_head": f"{canonical['head']}+repository-evidence-sha256:{bespoke_digest}", - } - ), - encoding="utf-8", - ) - with unittest.TestCase().assertRaisesRegex(ControlPlaneError, "REVIEW_IDENTITY_MISMATCH"): - validate_deferred_remote_independent_review_identity(repository, task_id="task-c02") - - class CompositeMemberLifecycleTests(unittest.TestCase): def setUp(self) -> None: self._tmp = tempfile.TemporaryDirectory() @@ -1897,7 +1901,7 @@ def test_v4_composite_schema_requires_exactly_one_named_root(self) -> None: write_composite_metadata(workspace, include_root=False) doctor = run_wb(config, "doctor-workspace", str(workspace)) failures = json.loads(doctor.stdout)["portable"]["failures"] - self.assertIn("WB_CONTROL_PLANE_COMPOSITE_ROOT_BINDING_INVALID", failures) + self.assertEqual(failures, ["WB_INFRASTRUCTURE_SCHEMA_INVALID"]) def test_v4_composite_schema_requires_at_least_one_named_member(self) -> None: config, workspace, _, _ = init_single_v4(self.tmp_path) @@ -1968,8 +1972,7 @@ def test_v4_existing_modes_still_reject_crossed_bindings(self) -> None: ) doctor = run_wb(config, "doctor-workspace", str(workspace)) failures = json.loads(doctor.stdout)["portable"]["failures"] - self.assertIn("WB_CONTROL_PLANE_MEMBER_BINDING_INVALID:extra-member", failures) - self.assertIn("WB_CONTROL_PLANE_SINGLE_REPOSITORY_BINDING_INVALID", failures) + self.assertEqual(failures, ["WB_INFRASTRUCTURE_SCHEMA_INVALID"]) def test_add_workspace_member_dry_run_emits_digest_bound_proposal_without_writes(self) -> None: config, workspace, _, workspace_id = init_single_v4(self.tmp_path) @@ -2054,12 +2057,13 @@ def test_add_workspace_member_first_apply_converts_and_publishes_recoverably(sel metadata = (workspace / ".work-bundle/project.yaml").read_text(encoding="utf-8") self.assertIn(" mode: composite", metadata) self.assertIn(workspace_id, metadata) - self.assertIn(" - id: source-main", metadata) - self.assertIn(" type: root", metadata) - self.assertIn(" - id: execution-flow", metadata) - self.assertIn(" type: member", metadata) - self.assertIn(" name: execution-flow", metadata) - self.assertIn(" path: execution-flow", metadata) + repositories = yaml.safe_load(metadata)["source_repositories"] + self.assertEqual([item["id"] for item in repositories], ["source-main", "execution-flow"]) + self.assertEqual(repositories[0]["workspace_binding"], {"type": "root"}) + self.assertEqual( + repositories[1]["workspace_binding"], + {"type": "member", "name": "execution-flow", "path": "execution-flow"}, + ) self.assertNotIn("workspace_root:", metadata) self.assertNotIn("project_root:", metadata) self.assertTrue((workspace / "execution-flow/README.md").is_file()) @@ -2122,9 +2126,10 @@ def test_add_workspace_member_later_apply_is_add_only(self) -> None: ) self.assertEqual(applied.returncode, 0, applied.stdout + applied.stderr) metadata = (workspace / ".work-bundle/project.yaml").read_text(encoding="utf-8") - self.assertEqual(metadata.count(" - id: source-main"), 1) - self.assertEqual(metadata.count(" - id: execution-flow"), 1) - self.assertEqual(metadata.count(" - id: second-flow"), 1) + repository_ids = [item["id"] for item in yaml.safe_load(metadata)["source_repositories"]] + self.assertEqual(repository_ids.count("source-main"), 1) + self.assertEqual(repository_ids.count("execution-flow"), 1) + self.assertEqual(repository_ids.count("second-flow"), 1) self.assertIn(" mode: composite", metadata) self.assertTrue((workspace / "execution-flow/README.md").is_file()) self.assertTrue((workspace / "second-flow/README.md").is_file()) @@ -2358,7 +2363,13 @@ def test_add_workspace_member_preflight_rejects_mismatched_or_incomplete_root_bi self.assertEqual(json.loads(mismatched.stdout)["failure_code"], "WB_CONTROL_PLANE_BINDING_ROOT_MISMATCH") incomplete = init_single_v4(self.tmp_path / "incomplete", slug="incomplete-demo", attach=False) - incomplete_config, incomplete_workspace, _, _ = incomplete + incomplete_config, incomplete_workspace, _, incomplete_workspace_id = incomplete + incomplete_registry = incomplete_config / "registry/projects.yaml" + incomplete_document = yaml.safe_load(incomplete_registry.read_text(encoding="utf-8")) + incomplete_document["device_bindings"][incomplete_workspace_id]["repositories"].pop("source-main") + incomplete_registry.write_text( + yaml.safe_dump(incomplete_document, sort_keys=False), encoding="utf-8" + ) missing_root = run_wb( incomplete_config, *add_workspace_member_args(incomplete_workspace, member_remote), diff --git a/tests/test_dev_skill_contracts.py b/tests/test_dev_skill_contracts.py index 4661473..c749d29 100644 --- a/tests/test_dev_skill_contracts.py +++ b/tests/test_dev_skill_contracts.py @@ -15,13 +15,10 @@ def test_wb_initialize_skill_matches_live_cli() -> None: text = skill_text("wb-initialize-project") epilog = (REPO_ROOT / "scripts" / "work-bundle" / "core.py").read_text(encoding="utf-8") - assert "`init-project <root> --mode <single-repository|multi-repository> [--workspace-root <workspace-root>]" in text - assert "init-project <project-root> --mode <single-repository|multi-repository> [--workspace-root <workspace-root>]" in epilog - assert "`doctor-project <root> [--workspace-root" not in text - assert "`validate-project <root> [--workspace-root" not in text - assert "`doctor-project <root> [--repair] [--force]`" in text - assert "`validate-project <root> [--dry-run]`" in text - assert "Existing command names and `--project-root` remain supported for single-repository projects." not in text + assert "init-workspace <workspace-root>" in text + assert "init-workspace <workspace-root>" in epilog + assert "WB_CURRENT_INIT_COMMAND_RETIRED" in text + assert "v2/v3 is migration input only" in text def test_semantic_convergence_contract_is_bounded_and_reports_compact_result() -> None: diff --git a/tests/test_execution_artifact_placement.py b/tests/test_execution_artifact_placement.py index 0b3290f..73ce361 100644 --- a/tests/test_execution_artifact_placement.py +++ b/tests/test_execution_artifact_placement.py @@ -13,7 +13,6 @@ import execution_context # noqa: E402 from core import resolve_execution_artifact_path # noqa: E402 -from test_orchestration_execution_context import git, workspace # noqa: E402 @pytest.mark.parametrize( @@ -75,39 +74,6 @@ def test_static_admission_allows_only_proven_historical_cleanup_targets( ) -def test_static_task_admits_prebinding_cleanup_of_current_tracked_artifact( - tmp_path: Path, -) -> None: - root, _spec, task_path = workspace(tmp_path) - historical = root / "tests/test_wor108_context_projection.py" - historical.parent.mkdir(parents=True) - historical.write_text("def test_historical(): pass\n", encoding="utf-8") - task_path.write_text( - task_path.read_text(encoding="utf-8") - .replace( - "goal: Compile a bounded executor packet.\n", - "goal: Remove execution-only source residue.\n" - "completion_criteria: [Listed execution-only wrappers are absent from source.]\n", - ) - .replace( - "purpose: Compile a bounded executor packet.", - "purpose: Remove execution-only source residue.", - ) - .replace( - "write: [scripts/orchestration/execution_context.py]", - "write: [tests/test_wor108_context_projection.py]", - ), - encoding="utf-8", - ) - git(root, "add", ".") - git(root, "commit", "-qm", "tracked historical cleanup target") - - brief = execution_context.static_task_brief(root, task_path) - - assert brief["files"]["write"] == ["tests/test_wor108_context_projection.py"] - assert not (root / ".work-bundle/runtime").exists() - - def test_execution_artifacts_resolve_to_workspace_root_outside_source_member(tmp_path: Path) -> None: workspace = tmp_path / "workspace" source = workspace / "work-bundle-main" diff --git a/tests/test_execution_workspace.py b/tests/test_execution_workspace.py index dba644a..9f4ac43 100644 --- a/tests/test_execution_workspace.py +++ b/tests/test_execution_workspace.py @@ -56,14 +56,13 @@ def prepare_fixture(tmp_path: Path, *, execution_id: str = "exec-1") -> tuple[Pa return source, runtime, result -def test_project_template_declares_hydration_profiles_and_runtime_ignore() -> None: +def test_project_template_declares_portable_v4_metadata_and_runtime_ignore() -> None: project_template = (REPO_ROOT / "references/assets/template/project.yaml").read_text(encoding="utf-8") ignore_template = (REPO_ROOT / "references/assets/template/.gitignore.template").read_text(encoding="utf-8") - assert "execution_workspace_profiles:" in project_template - assert "strategy: regenerate" in project_template - assert "strategy: credential-inject" in project_template - assert "strategy: copy" in project_template - assert "sensitivity: non-secret" in project_template + assert "metadata_version: 4" in project_template + assert "control_plane:" in project_template + assert "workspace_binding:" in project_template + assert "execution_workspace_profiles:" not in project_template assert ".work-bundle/" in ignore_template diff --git a/tests/test_forced_orchestration_finalization.py b/tests/test_forced_orchestration_finalization.py deleted file mode 100644 index adfefb3..0000000 --- a/tests/test_forced_orchestration_finalization.py +++ /dev/null @@ -1,420 +0,0 @@ -from __future__ import annotations - -import hashlib -import json -from pathlib import Path -import subprocess -import sys - -import pytest -import yaml - - -REPO_ROOT = Path(__file__).resolve().parents[1] -ORCHESTRATION = REPO_ROOT / "scripts" / "orchestration" -if str(ORCHESTRATION) not in sys.path: - sys.path.insert(0, str(ORCHESTRATION)) - -import bounded_closure # noqa: E402 -import completion_provenance # noqa: E402 -import plans # noqa: E402 - - -def _git(root: Path, *args: str) -> str: - return subprocess.check_output(["git", "-C", str(root), *args], text=True).strip() - - -def _workspace(tmp_path: Path) -> tuple[Path, dict[str, str]]: - root = tmp_path / "workspace" - metadata = root / ".work-bundle/project.yaml" - metadata.parent.mkdir(parents=True) - metadata.write_text( - yaml.safe_dump( - { - "metadata_version": 4, - "workspace": {"id": "workspace-test", "mode": "single-repository"}, - "orchestration_control": { - "schema_version": 1, - "post_execution_review_round_limit": 5, - }, - }, - sort_keys=False, - ), - encoding="utf-8", - ) - active_spec = root / ".work-bundle/orchestration/spec/active" - active_plan = root / ".work-bundle/orchestration/plan/active" - active_spec.mkdir(parents=True) - (active_plan / "plan-flow").mkdir(parents=True) - (active_spec / "spec-origin.md").write_text( - "---\nid: spec-origin\nstatus: verified\n---\n# Original\n", encoding="utf-8" - ) - (active_plan / "plan-origin.md").write_text( - "---\nid: plan-flow\nstatus: In progress\n---\n# Plan\n", encoding="utf-8" - ) - (active_plan / "plan-flow/task-001.md").write_text( - "---\nid: task-001\nplan_id: plan-flow\nphase_id: phase-001\nstatus: Completed\n---\n# Task\n", - encoding="utf-8", - ) - residual = active_spec / "spec-residual.md" - residual.write_text( - "---\nid: spec-residual\nstatus: active\n---\n" - "# Residual findings\n\n- REQ-005 remains unaccepted because receipt-1 is missing.\n", - encoding="utf-8", - ) - source = tmp_path / "source" - source.mkdir() - subprocess.run(["git", "-C", str(source), "init", "-q"], check=True) - subprocess.run(["git", "-C", str(source), "config", "user.email", "test@example.com"], check=True) - subprocess.run(["git", "-C", str(source), "config", "user.name", "Test"], check=True) - (source / "product.txt").write_text("unresolved\n", encoding="utf-8") - subprocess.run(["git", "-C", str(source), "add", "."], check=True) - subprocess.run(["git", "-C", str(source), "commit", "-qm", "baseline"], check=True) - return root, { - "repository_id": "product-main", - "project_root": str(source), - "commit": _git(source, "rev-parse", "HEAD"), - "tree": _git(source, "rev-parse", "HEAD^{tree}"), - } - - -def _exhaust(root: Path) -> None: - for number in range(1, 6): - round_record = bounded_closure.begin_review_round( - root, - flow_id="plan-flow", - request_id=f"request-{number}", - review_id=f"review-{number}", - target_identity={ - "artifact_id": "plan-flow", - "revision": str(number), - "sha256": hashlib.sha256(str(number).encode()).hexdigest(), - "source_tree": "a" * 40, - }, - executor_attempts=[{"execution_id": "executor-1", "state": "completed"}], - known_missing_evidence=["accepted_result"], - ) - bounded_closure.complete_review_round( - root, - flow_id="plan-flow", - round_id=str(round_record["round_id"]), - outcome="blocked", - audit_block={"code": "missing-evidence", "missing": ["accepted_result"]}, - ) - - -def _finalize(root: Path, baseline: dict[str, str], **overrides: object) -> dict[str, object]: - arguments: dict[str, object] = { - "flow_id": "plan-flow", - "request_id": "finalize-1", - "blocker_id": "BLOCK-plan-flow", - "residual_spec_id": "spec-residual", - "residual_spec": root / ".work-bundle/orchestration/spec/active/spec-residual.md", - "origin_spec_id": "spec-origin", - "origin_plan_id": "plan-flow", - "source_baselines": [baseline], - "knowledge_return": {"status": "not-needed", "evidence_ref": None}, - "operator_authorized": False, - } - arguments.update(overrides) - return bounded_closure.finalize_with_blockers(root, **arguments) - - -def test_fifth_round_finalization_persists_blocker_then_archives_origins_and_is_retry_safe( - tmp_path: Path, -) -> None: - root, baseline = _workspace(tmp_path) - _exhaust(root) - - first = _finalize(root, baseline) - second = _finalize(root, baseline) - - assert second == first - assert first["outcome"] == "closed_with_blockers" - control = yaml.safe_load((root / ".work-bundle/project.yaml").read_text(encoding="utf-8"))[ - "orchestration_control" - ] - assert control["blockers"] == [ - { - "id": "BLOCK-plan-flow", - "status": "active", - "origin_plan": "plan-flow", - "origin_spec": "spec-origin", - "specification": ".work-bundle/orchestration/spec/active/spec-residual.md", - "source_baselines": [ - { - "repository_id": "product-main", - "commit": baseline["commit"], - "tree": baseline["tree"], - } - ], - } - ] - assert control["closed_flows"][0]["outcome"] == "closed_with_blockers" - assert not (root / ".work-bundle/orchestration/spec/active/spec-origin.md").exists() - assert (root / ".work-bundle/orchestration/spec/archived/spec-origin.md").is_file() - assert not (root / ".work-bundle/orchestration/plan/active/plan-origin.md").exists() - assert (root / ".work-bundle/orchestration/plan/archived/plan-origin.md").is_file() - assert (root / ".work-bundle/orchestration/plan/archived/plan-flow/task-001.md").is_file() - - -def test_forced_finalization_requires_exhaustion_or_explicit_operator_authority( - tmp_path: Path, -) -> None: - root, baseline = _workspace(tmp_path) - - with pytest.raises( - bounded_closure.BoundedClosureError, - match="WB_POST_EXECUTION_FINALIZATION_NOT_AUTHORIZED", - ): - _finalize(root, baseline) - - result = _finalize(root, baseline, operator_authorized=True) - assert result["outcome"] == "closed_with_blockers" - - -def test_invalid_or_dirty_source_baseline_stops_before_blocker_and_archive(tmp_path: Path) -> None: - root, baseline = _workspace(tmp_path) - _exhaust(root) - Path(baseline["project_root"]).joinpath("product.txt").write_text("dirty\n", encoding="utf-8") - - with pytest.raises( - bounded_closure.BoundedClosureError, - match="WB_POST_EXECUTION_SOURCE_BASELINE_DIRTY", - ): - _finalize(root, baseline) - - control = yaml.safe_load((root / ".work-bundle/project.yaml").read_text(encoding="utf-8"))[ - "orchestration_control" - ] - assert control.get("blockers", []) == [] - assert (root / ".work-bundle/orchestration/spec/active/spec-origin.md").is_file() - ledger = json.loads( - (root / ".work-bundle/runtime/orchestration-control/post-execution-review-rounds-v1.json").read_text( - encoding="utf-8" - ) - ) - assert ledger["flows"]["plan-flow"]["finalization"]["state"] == "required" - - -@pytest.mark.parametrize("entrypoint", ["orch.py", "wb.py"]) -def test_both_public_cli_families_run_the_shared_forced_finalizer( - tmp_path: Path, entrypoint: str, -) -> None: - root, baseline = _workspace(tmp_path) - _exhaust(root) - result = subprocess.run( - [ - sys.executable, - str(REPO_ROOT / "scripts" / entrypoint), - "finalize-with-blockers", - "--project-root", - str(root), - "--flow-id", - "plan-flow", - "--request-id", - "finalize-cli", - "--blocker-id", - "BLOCK-plan-flow", - "--residual-spec-id", - "spec-residual", - "--residual-spec", - str(root / ".work-bundle/orchestration/spec/active/spec-residual.md"), - "--origin-spec-id", - "spec-origin", - "--origin-plan-id", - "plan-flow", - "--source-baselines", - json.dumps([baseline]), - "--knowledge-return", - json.dumps({"status": "not-needed", "evidence_ref": None}), - ], - text=True, - capture_output=True, - check=False, - ) - - assert result.returncode == 0, result.stderr - assert json.loads(result.stdout)["outcome"] == "closed_with_blockers" - - -def test_interruption_after_archive_is_recorded_and_retry_finishes_without_replay( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, -) -> None: - root, baseline = _workspace(tmp_path) - _exhaust(root) - original_release = plans.release_plan_bindings_for_forced_finalization - - with monkeypatch.context() as scoped: - scoped.setattr( - plans, - "release_plan_bindings_for_forced_finalization", - lambda *_args: (_ for _ in ()).throw(OSError("injected release interruption")), - ) - with pytest.raises( - bounded_closure.BoundedClosureError, - match="WB_POST_EXECUTION_FINALIZATION_INCOMPLETE", - ): - _finalize(root, baseline) - - ledger = json.loads( - (root / ".work-bundle/runtime/orchestration-control/post-execution-review-rounds-v1.json").read_text( - encoding="utf-8" - ) - ) - finalization = ledger["flows"]["plan-flow"]["finalization"] - assert finalization["state"] == "administrative_incomplete" - assert finalization["incomplete"]["stage"] == "ownership_release" - assert (root / ".work-bundle/orchestration/plan/archived/plan-origin.md").is_file() - - monkeypatch.setattr(plans, "release_plan_bindings_for_forced_finalization", original_release) - assert _finalize(root, baseline)["outcome"] == "closed_with_blockers" - - -def test_retry_finishes_plan_directory_move_after_root_file_was_already_moved( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, -) -> None: - root, baseline = _workspace(tmp_path) - _exhaust(root) - original_move = plans.move_to_archive - plan_move_count = 0 - - def interrupt_second_plan_move(path: Path, active: Path, archived: Path) -> Path: - nonlocal plan_move_count - plan_move_count += 1 - if plan_move_count == 2: - raise OSError("injected directory move interruption") - return original_move(path, active, archived) - - with monkeypatch.context() as scoped: - scoped.setattr(plans, "move_to_archive", interrupt_second_plan_move) - with pytest.raises( - bounded_closure.BoundedClosureError, - match="WB_POST_EXECUTION_FINALIZATION_INCOMPLETE", - ): - _finalize(root, baseline) - - assert (root / ".work-bundle/orchestration/plan/archived/plan-origin.md").is_file() - assert (root / ".work-bundle/orchestration/plan/active/plan-flow/task-001.md").is_file() - assert _finalize(root, baseline)["outcome"] == "closed_with_blockers" - assert (root / ".work-bundle/orchestration/plan/archived/plan-flow/task-001.md").is_file() - - -def test_forced_finalization_releases_active_plan_binding(tmp_path: Path) -> None: - root, baseline = _workspace(tmp_path) - _exhaust(root) - store = completion_provenance.ManagedProvenanceStore( - root / ".work-bundle/runtime/completion-provenance" - ) - ownership = completion_provenance.FailureOwnershipV1.create( - store, - "binding-plan-flow-task-001", - "local_project", - "task-001", - ).to_dict() - binding_path = root / ".work-bundle/runtime/execution/plan-flow/task-001/execution-binding.json" - binding_path.parent.mkdir(parents=True) - binding_path.write_text( - json.dumps({"plan_id": "plan-flow", "task_id": "task-001", "ownership": ownership}), - encoding="utf-8", - ) - - _finalize(root, baseline) - - updated = json.loads(binding_path.read_text(encoding="utf-8")) - assert updated["ownership"]["state"] == "released" - - -def test_finalization_never_launches_validation_or_review_subprocesses( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, -) -> None: - root, baseline = _workspace(tmp_path) - _exhaust(root) - real_run = bounded_closure.subprocess.run - observed: list[list[str]] = [] - - def guarded(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: - observed.append(argv) - assert argv[0] == "git" - assert not any(token in {"pytest", "reviewer", "validate-executor-result"} for token in argv) - return real_run(argv, **kwargs) - - monkeypatch.setattr(bounded_closure.subprocess, "run", guarded) - _finalize(root, baseline) - - assert observed - - -def test_supplied_residual_spec_is_materialized_as_active_blocker_evidence(tmp_path: Path) -> None: - root, baseline = _workspace(tmp_path) - _exhaust(root) - active = root / ".work-bundle/orchestration/spec/active/spec-residual.md" - content = active.read_bytes() - active.unlink() - supplied = tmp_path / "supplied-residual.md" - supplied.write_bytes(content) - - result = _finalize(root, baseline, residual_spec=supplied) - - assert result["residual_spec"]["path"].endswith("spec/active/supplied-residual.md") - assert (root / result["residual_spec"]["path"]).read_bytes() == content - - -def test_invalid_knowledge_return_leaves_blocker_before_archive_and_can_be_retried( - tmp_path: Path, -) -> None: - root, baseline = _workspace(tmp_path) - _exhaust(root) - - with pytest.raises( - bounded_closure.BoundedClosureError, - match="WB_POST_EXECUTION_KNOWLEDGE_RETURN_INVALID", - ): - _finalize( - root, - baseline, - knowledge_return={"status": "blocked", "evidence_ref": None}, - ) - - control = yaml.safe_load((root / ".work-bundle/project.yaml").read_text(encoding="utf-8"))[ - "orchestration_control" - ] - assert control["blockers"][0]["id"] == "BLOCK-plan-flow" - assert (root / ".work-bundle/orchestration/plan/active/plan-origin.md").is_file() - assert _finalize(root, baseline)["outcome"] == "closed_with_blockers" - - -def test_begin_review_round_public_admission_uses_explicit_flow_id(tmp_path: Path) -> None: - root, _baseline = _workspace(tmp_path) - result = subprocess.run( - [ - sys.executable, - str(REPO_ROOT / "scripts/orch.py"), - "begin-review-round", - "--project-root", - str(root), - "--flow-id", - "plan-flow", - "--request-id", - "request-admission", - "--review-id", - "review-admission", - "--target-identity", - json.dumps( - { - "artifact_id": "plan-flow", - "revision": "1", - "sha256": "b" * 64, - "source_tree": "c" * 40, - } - ), - "--executor-attempts", - json.dumps([{"execution_id": "executor-1", "state": "completed"}]), - ], - text=True, - capture_output=True, - check=False, - ) - - assert result.returncode == 0, result.stderr - assert json.loads(result.stdout)["flow_id"] == "plan-flow" diff --git a/tests/test_infrastructure_metadata.py b/tests/test_infrastructure_metadata.py new file mode 100644 index 0000000..d5e11b1 --- /dev/null +++ b/tests/test_infrastructure_metadata.py @@ -0,0 +1,366 @@ +from __future__ import annotations + +import importlib.util +from pathlib import Path +import subprocess +import sys + +import pytest +import yaml + + +REPO_ROOT = Path(__file__).resolve().parents[1] +MODULE_PATH = REPO_ROOT / "scripts/work-bundle/infrastructure.py" + + +def load_infrastructure(): + spec = importlib.util.spec_from_file_location("wb_infrastructure_test", MODULE_PATH) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def portable_metadata(*, workspace_id: str = "wb-example", mode: str = "multi-repository") -> dict[str, object]: + binding: dict[str, object] = {"type": "root" if mode == "single-repository" else "member"} + if mode != "single-repository": + binding["name"] = "source" + return { + "metadata_version": 4, + "authority": "canonical", + "workspace": {"id": workspace_id, "slug": "example", "mode": mode}, + "control_plane": { + "schema_version": 1, + "repository": {"remote": ""}, + "sync_policy": {"mode": "manual"}, + }, + "source_repositories": [ + { + "id": "source", + "role": "source", + "remote": {"canonical": "ssh://git@example.test/source", "aliases": []}, + "default_branch": "main", + "workspace_binding": binding, + "materialization": {"required": True}, + "operation_policy": "inherit", + } + ], + } + + +def registry_document(workspace: Path, member: Path, *, workspace_id: str = "wb-example") -> dict[str, object]: + branch = subprocess.run( + ["git", "-C", str(member), "branch", "--show-current"], check=True, capture_output=True, text=True + ).stdout.strip() + head = subprocess.run( + ["git", "-C", str(member), "rev-parse", "HEAD"], check=True, capture_output=True, text=True + ).stdout.strip() + common = subprocess.run( + ["git", "-C", str(member), "rev-parse", "--path-format=absolute", "--git-common-dir"], + check=True, capture_output=True, text=True, + ).stdout.strip() + return { + "registry_schema_version": 1, + "projects": [ + { + "slug": "example", + "name": "example", + "workspace_root": str(workspace), + "work_bundle_root": str(workspace / ".work-bundle"), + "knowledge_root": str(workspace / ".work-bundle/knowledge"), + "aliases": [], + "status": "active", + } + ], + "device_bindings": { + workspace_id: { + "slug": "example", + "workspace_root": str(workspace), + "control_plane_path": str(workspace / ".work-bundle"), + "control_plane_remote": "", + "observed_control_plane_head": "", + "repositories": { + "source": { + "project_root": str(member), + "checkout_kind": "managed-worktree", + "observed_branch": branch, + "observed_head": head, + "observed_at": "2026-09-19T00:00:00Z", + "git_common_dir": common, + } + }, + } + }, + } + + +def write_context(tmp_path: Path, *, member_count: int = 1) -> tuple[Path, Path, Path]: + workspace = tmp_path / "workspace" + config = tmp_path / "config" + registry = config / "registry/projects.yaml" + workspace.joinpath(".work-bundle").mkdir(parents=True) + members = [workspace / ("source" if index == 0 else f"source-{index + 1}") for index in range(member_count)] + for member in members: + member.mkdir() + subprocess.run(["git", "init", "-q", "-b", "main", str(member)], check=True) + subprocess.run(["git", "-C", str(member), "config", "user.email", "test@example.com"], check=True) + subprocess.run(["git", "-C", str(member), "config", "user.name", "Test"], check=True) + member.joinpath("README.md").write_text("fixture\n", encoding="utf-8") + subprocess.run(["git", "-C", str(member), "add", "README.md"], check=True) + subprocess.run(["git", "-C", str(member), "commit", "-q", "-m", "fixture"], check=True) + metadata = portable_metadata() + if member_count > 1: + repositories = metadata["source_repositories"] + assert isinstance(repositories, list) + repositories.append( + { + "id": "source-2", + "role": "source", + "remote": {"canonical": "ssh://git@example.test/source-2", "aliases": []}, + "default_branch": "main", + "workspace_binding": {"type": "member", "name": "source-2"}, + "materialization": {"required": True}, + "operation_policy": "inherit", + } + ) + workspace.joinpath(".work-bundle/project.yaml").write_text(yaml.safe_dump(metadata, sort_keys=False)) + registry.parent.mkdir(parents=True) + registry_data = registry_document(workspace, members[0]) + if member_count > 1: + bindings = registry_data["device_bindings"] + assert isinstance(bindings, dict) + repositories = bindings["wb-example"]["repositories"] + repositories["source-2"] = registry_document( + workspace, members[1], workspace_id="unused" + )["device_bindings"]["unused"]["repositories"]["source"] + registry.write_text(yaml.safe_dump(registry_data, sort_keys=False)) + config.joinpath("bootstrap.yaml").write_text( + "\n".join( + [ + "bootstrap_version: v1", + "authority: canonical", + f"work_bundle_root: {REPO_ROOT}", + 'project_registry: "$work_bundle_config_root/registry/projects.yaml"', + 'skill_registry: "$work_bundle_config_root/registry/skill-registry.yaml"', + "", + ] + ) + ) + return config, workspace, members[0] + + +def test_catalog_has_separate_immutable_infrastructure_families() -> None: + infrastructure = load_infrastructure() + catalog = infrastructure.load_schema_catalog(toolkit_root=REPO_ROOT) + assert set(catalog["families"]) == { + "bootstrap-config", + "workspace-project-metadata", + "project-registry", + } + assert all(item["version"] == 1 for item in catalog["families"].values()) + + +def test_maintained_yaml_equivalence_and_canonical_dump() -> None: + infrastructure = load_infrastructure() + inline = infrastructure.parse_yaml_mapping( + "metadata_version: 4\nauthority: canonical\nworkspace: {id: wb-example, slug: example, mode: multi-repository}\n", + source="inline", + ) + block = infrastructure.parse_yaml_mapping( + "# comment\nmetadata_version: 4\nauthority: 'canonical'\nworkspace:\n id: wb-example\n slug: example\n mode: multi-repository\n", + source="block", + ) + assert inline == block + assert infrastructure.parse_yaml_mapping( + infrastructure.dump_canonical_yaml(inline), source="round-trip" + ) == inline + + +def test_schema_rejects_local_portable_fields_and_duplicate_repository_ids(tmp_path: Path) -> None: + infrastructure = load_infrastructure() + invalid = portable_metadata() + invalid["workspace_root"] = str(tmp_path) + with pytest.raises(infrastructure.InfrastructureError) as local: + infrastructure.validate_infrastructure_document( + invalid, family="workspace-project-metadata", toolkit_root=REPO_ROOT + ) + assert local.value.code == "WB_INFRASTRUCTURE_SCHEMA_INVALID" + + duplicate = portable_metadata() + repositories = duplicate["source_repositories"] + assert isinstance(repositories, list) + repositories.append(dict(repositories[0])) + with pytest.raises(infrastructure.InfrastructureError) as repeated: + infrastructure.validate_infrastructure_document( + duplicate, family="workspace-project-metadata", toolkit_root=REPO_ROOT + ) + assert repeated.value.code == "WB_INFRASTRUCTURE_ID_DUPLICATE" + + +def test_schema_accepts_extensible_bootstrap_and_manual_repository_locator() -> None: + infrastructure = load_infrastructure() + bootstrap = { + "bootstrap_version": "v1", + "authority": "canonical", + "work_bundle_root": "/toolkit", + "project_registry": "$work_bundle_config_root/registry/projects.yaml", + "skill_registry": "$work_bundle_config_root/registry/skill-registry.yaml", + "prefer_subagent": False, + } + assert infrastructure.validate_infrastructure_document( + bootstrap, family="bootstrap-config", toolkit_root=REPO_ROOT + )["prefer_subagent"] is False + + metadata = portable_metadata() + repository = metadata["source_repositories"][0] + del repository["remote"] + repository["locator"] = {"type": "manual", "value": "source"} + infrastructure.validate_infrastructure_document( + metadata, family="workspace-project-metadata", toolkit_root=REPO_ROOT + ) + + +def test_join_and_anchor_matrix_keep_workspace_and_member_distinct(tmp_path: Path) -> None: + infrastructure = load_infrastructure() + config, workspace, member = write_context(tmp_path) + nested = member / "nested" + nested.mkdir() + + workspace_only = infrastructure.resolve_anchor_context( + cwd=workspace, config_root=config, toolkit_root=REPO_ROOT + ) + assert workspace_only.workspace_root == workspace.resolve() + assert workspace_only.project_root is None + + from_member = infrastructure.resolve_anchor_context( + cwd=nested, config_root=config, toolkit_root=REPO_ROOT + ) + assert from_member.workspace_root == workspace.resolve() + assert from_member.project_root == member.resolve() + assert from_member.repository_id == "source" + + explicit = infrastructure.resolve_anchor_context( + workspace_root=workspace, + project_root=member, + cwd=tmp_path, + config_root=config, + toolkit_root=REPO_ROOT, + ) + assert explicit.workspace_root != explicit.project_root + + +def test_member_required_is_ambiguous_and_never_selects_first(tmp_path: Path) -> None: + infrastructure = load_infrastructure() + config, workspace, _ = write_context(tmp_path, member_count=2) + with pytest.raises(infrastructure.InfrastructureError) as failure: + infrastructure.resolve_anchor_context( + workspace_root=workspace, + config_root=config, + toolkit_root=REPO_ROOT, + member_required=True, + ) + assert failure.value.code == "WB_INFRASTRUCTURE_MEMBER_AMBIGUOUS" + + +def test_missing_binding_and_path_escape_fail_without_locator_fallback(tmp_path: Path) -> None: + infrastructure = load_infrastructure() + metadata = portable_metadata() + with pytest.raises(infrastructure.InfrastructureError) as missing: + infrastructure.join_workspace_binding(metadata, {"projects": [], "device_bindings": {}}) + assert missing.value.code == "WB_INFRASTRUCTURE_WORKSPACE_BINDING_MISSING" + + workspace = tmp_path / "workspace" + workspace.mkdir() + outside = tmp_path / "outside" + outside.mkdir() + registry = { + "projects": [{"slug": "example", "aliases": []}], + "device_bindings": {"wb-example": { + "slug": "example", + "workspace_root": str(workspace), + "repositories": {"source": { + "project_root": str(outside), + "checkout_kind": "managed-worktree", + "observed_branch": "main", + "observed_head": "0" * 40, + "observed_at": "2026-09-19T00:00:00Z", + "git_common_dir": str(outside / ".git"), + }}, + }}, + } + with pytest.raises(infrastructure.InfrastructureError) as escaped: + infrastructure.join_workspace_binding( + metadata, registry, expected_workspace_root=workspace + ) + assert escaped.value.code == "WB_INFRASTRUCTURE_PROJECT_ROOT_ESCAPE" + + +def test_materialized_binding_requires_complete_observation_evidence(tmp_path: Path) -> None: + infrastructure = load_infrastructure() + config, _, _ = write_context(tmp_path) + registry = yaml.safe_load((config / "registry/projects.yaml").read_text(encoding="utf-8")) + local = registry["device_bindings"]["wb-example"]["repositories"]["source"] + local.pop("observed_head") + with pytest.raises(infrastructure.InfrastructureError) as invalid: + infrastructure.validate_infrastructure_document( + registry, family="project-registry", toolkit_root=REPO_ROOT + ) + assert invalid.value.code == "WB_INFRASTRUCTURE_SCHEMA_INVALID" + + +def test_explicitly_unmaterialized_binding_carries_no_invented_observations(tmp_path: Path) -> None: + infrastructure = load_infrastructure() + workspace = tmp_path / "workspace" + workspace.mkdir() + metadata = portable_metadata() + repository = metadata["source_repositories"][0] + repository["materialization"]["required"] = False + registry = { + "registry_schema_version": 1, + "projects": [{"slug": "example", "aliases": []}], + "device_bindings": {"wb-example": { + "slug": "example", + "workspace_root": str(workspace), + "repositories": {"source": { + "project_root": str(workspace / "source"), + "checkout_kind": "unmaterialized-member", + "observed_branch": "", + "observed_head": "", + "observed_at": "2026-09-19T00:00:00Z", + "git_common_dir": "", + }}, + }}, + } + local = registry["device_bindings"]["wb-example"]["repositories"]["source"] + local.update({ + "checkout_kind": "unmaterialized-member", + "observed_branch": "", + "observed_head": "", + "git_common_dir": "", + }) + joined = infrastructure.join_workspace_binding( + metadata, registry, expected_workspace_root=workspace + ) + assert joined["repositories"]["source"]["checkout_kind"] == "unmaterialized-member" + + repository["materialization"]["required"] = True + joined_required = infrastructure.join_workspace_binding( + metadata, registry, expected_workspace_root=workspace + ) + assert joined_required["repositories"]["source"]["checkout_kind"] == "unmaterialized-member" + + +def test_atomic_write_preserves_old_bytes_when_replace_fails(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + infrastructure = load_infrastructure() + target = tmp_path / "metadata.yaml" + target.write_bytes(b"before\n") + + def fail_replace(_source: object, _target: object) -> None: + raise OSError("injected") + + monkeypatch.setattr(infrastructure.os, "replace", fail_replace) + with pytest.raises(infrastructure.InfrastructureError) as failure: + infrastructure.atomic_write_text(target, "after\n") + assert failure.value.code == "WB_INFRASTRUCTURE_ATOMIC_WRITE_FAILED" + assert target.read_bytes() == b"before\n" diff --git a/tests/test_invocation_observation.py b/tests/test_invocation_observation.py index 67b6f63..4a936ee 100644 --- a/tests/test_invocation_observation.py +++ b/tests/test_invocation_observation.py @@ -222,6 +222,32 @@ def test_subparser_vocabulary_matches_exported_recognized_set(path: Path) -> Non and isinstance(call.args[0], ast.Constant) and isinstance(call.args[0].value, str) } + for node in ast.walk(tree): + if not isinstance(node, ast.For) or not isinstance(node.target, ast.Tuple): + continue + command_target = node.target.elts[0] + if not isinstance(command_target, ast.Name) or not isinstance(node.iter, (ast.Tuple, ast.List)): + continue + dynamically_routed = any( + isinstance(call, ast.Call) + and isinstance(call.func, ast.Attribute) + and call.func.attr == "add_parser" + and call.args + and isinstance(call.args[0], ast.Name) + and call.args[0].id == command_target.id + for statement in node.body + for call in ast.walk(statement) + ) + if not dynamically_routed: + continue + routed.update( + item.elts[0].value + for item in node.iter.elts + if isinstance(item, ast.Tuple) + and item.elts + and isinstance(item.elts[0], ast.Constant) + and isinstance(item.elts[0].value, str) + ) exported: set[str] | None = None for node in tree.body: if not isinstance(node, ast.Assign): diff --git a/tests/test_multi_repository_member.py b/tests/test_multi_repository_member.py index f0fd544..f8d8132 100644 --- a/tests/test_multi_repository_member.py +++ b/tests/test_multi_repository_member.py @@ -167,7 +167,7 @@ def test_deferred_remote_attach_interruption_rolls_back_and_retry_converges(mult registry = config / "registry/projects.yaml" before = metadata.read_bytes(), registry.read_bytes() original_publish = control_plane._atomic_publish - monkeypatch.setenv("WB_CONFIG_ROOT", str(config)) + monkeypatch.setenv("HOME", str(config.parent)) monkeypatch.setattr(control_plane, "_atomic_publish", lambda payloads: (_ for _ in ()).throw(OSError("injected"))) result = control_plane.cmd_attach_deferred_remote( @@ -383,81 +383,3 @@ def test_multi_member_add_only_and_collisions(multi, tmp_path): assert result.returncode == 1 assert before == metadata.read_bytes() assert len(yaml.safe_load(before)["source_repositories"]) == 3 - - -@pytest.mark.parametrize("mode", ["single", "multi"]) -@pytest.mark.parametrize("shape", ["commented-header", "anchored-header", "quoted-key", "quoted-nested", "commented-control", "owner-flow", "owner-comment"]) -@pytest.mark.parametrize("dependency_free", [False, True]) -def test_member_add_preserves_yaml_section_boundaries(multi, tmp_path, monkeypatch, mode, shape, dependency_free): - config, workspace, remote = multi - if mode == "single": - config, workspace, _, _ = init_single_v4(tmp_path / "single") - metadata = workspace / ".work-bundle/project.yaml" - text = metadata.read_text() - if shape == "commented-header": - text = text.replace("source_repositories:", "source_repositories: # managed members") - elif shape == "anchored-header": - text = text.replace("source_repositories:", "source_repositories: &sources") - elif shape == "quoted-key": - text = text.replace("agents_sync:", "'custom_owner_field': retained\nagents_sync:", 1) - elif shape == "quoted-nested": - text = text.replace("agents_sync:", "'owner_settings':\n contact:\n team: engineering\nagents_sync:", 1) - elif shape == "commented-control": - text = text.replace("control_plane:", "control_plane: # portable settings") - elif shape == "owner-flow": - text += "owner_settings: {}\n" - else: - text += "owner_settings: # retained\n team: engineering\n" - metadata.write_text(text) - original = yaml.safe_load(text) - if dependency_free: - # Exercise the actual CLI without its optional YAML dependency, while - # retaining PyYAML in the test process as an independent output oracle. - no_yaml = tmp_path / "no-yaml" - no_yaml.mkdir() - (no_yaml / "yaml.py").write_text("raise ImportError('dependency-free regression')\n") - monkeypatch.setenv("PYTHONPATH", str(no_yaml)) - proposal = propose(config, workspace, remote) - assert metadata.read_text() == text - result = apply(config, workspace, remote, proposal) - assert result.returncode == 0, result.stdout + result.stderr - document = yaml.safe_load(metadata.read_text()) - assert len(document["source_repositories"]) == 2 - assert document["source_repositories"][0] == original["source_repositories"][0] - for key, value in original.items(): - if key not in {"source_repositories", "workspace"}: - assert document[key] == value - if shape == "quoted-key": - assert document["custom_owner_field"] == "retained" - elif shape == "commented-header": - assert "source_repositories: # managed members" in metadata.read_text() - elif shape == "anchored-header": - assert "source_repositories: &sources" in metadata.read_text() - elif shape == "commented-control": - assert "control_plane: # portable settings" in metadata.read_text() - elif shape == "owner-comment": - assert "owner_settings: # retained\n team: engineering\n" in metadata.read_text() - replay = apply(config, workspace, remote, propose(config, workspace, remote)) - assert replay.returncode == 0, replay.stdout + replay.stderr - assert json.loads(replay.stdout)["changed_files"] == [] - - -def test_member_add_rejects_misplaced_rendered_block_without_mutation(multi, monkeypatch, capsys): - import control_plane - - config, workspace, remote = multi - monkeypatch.setenv("WB_CONFIG_ROOT", str(config)) - # Inject the publication defect reported in review: the generated member - # appears after a root scalar instead of inside source_repositories. - monkeypatch.setattr(control_plane, "_append_member_metadata", lambda text, member: - text + control_plane._render_member_metadata_block(member, multi=True)) - metadata = workspace / ".work-bundle/project.yaml" - registry = config / "registry/projects.yaml" - before = metadata.read_bytes(), registry.read_bytes() - result = control_plane.cmd_add_workspace_member(add_workspace_member_args(workspace, remote)[1:] + ["--dry-run"]) - assert result == 1 - output = capsys.readouterr() - assert json.loads(output.out)["failure_code"] == "WB_CONTROL_PLANE_METADATA_INVALID" - assert "Traceback" not in output.err - assert before == (metadata.read_bytes(), registry.read_bytes()) - assert not (workspace / "execution-flow").exists() diff --git a/tests/test_native_review_integration.py b/tests/test_native_review_integration.py deleted file mode 100644 index d817df4..0000000 --- a/tests/test_native_review_integration.py +++ /dev/null @@ -1,858 +0,0 @@ -from __future__ import annotations - -from copy import deepcopy -import hashlib -import json -import os -from pathlib import Path -import shlex -import shutil -import subprocess -import sys - -import pytest -from reviewer_run_fixtures import bind_review_receipt - - -REPO_ROOT = Path(__file__).resolve().parents[1] -ORCHESTRATION = REPO_ROOT / "scripts" / "orchestration" -WORK_BUNDLE = REPO_ROOT / "scripts" / "work-bundle" -for module_root in (WORK_BUNDLE, ORCHESTRATION): - if str(module_root) not in sys.path: - sys.path.insert(0, str(module_root)) - -import execution_context # noqa: E402 -import completion_provenance # noqa: E402 -import execution_workspace # noqa: E402 -import review_runtime # noqa: E402 -import reviewer_workspace # noqa: E402 - - -def _git(root: Path, *arguments: str) -> str: - completed = subprocess.run( - ["git", *arguments], cwd=root, check=True, capture_output=True, text=True - ) - return completed.stdout.strip() - - -def _persist_production_binding( - control: Path, - source: Path, - task: dict[str, object], - *, - execution_id: str, - baseline: dict[str, str], -) -> dict[str, object]: - plan_id = str(task["plan_id"]) - task_id = str(task["task_id"]) - workspace_id = "workspace-native" - repository_id = "repo-native" - runtime_root = control / "execution-workspaces" - registered = execution_workspace.register_existing( - source, - workspace_id=workspace_id, - execution_id=execution_id, - repository_id=repository_id, - created_for=task_id, - owner="harness", - runtime_root=runtime_root, - ) - ownership = completion_provenance.execution_binding_ownership( - control / ".work-bundle/runtime/completion-provenance", - binding_id=f"binding:{plan_id}:{task_id}", - target_kind="git_backed", - owner=task_id, - ) - binding = { - "plan_id": plan_id, - "task_id": task_id, - "workspace_id": workspace_id, - "execution_id": execution_id, - "repository_id": repository_id, - "runtime_root": str(runtime_root), - "execution_path": str(source.resolve()), - "control_root": str(control.resolve()), - "state_path": registered["state_path"], - "git_identity": registered["git_identity"], - "write_scope": list(task.get("files", {}).get("write", [])), - "forbidden_scope": list(task.get("files", {}).get("forbidden", [])), - "ownership": ownership, - "mutating": True, - "baseline": baseline, - } - execution_context._persist_binding(binding, control) - return execution_context.load_task_execution_binding(control, plan_id, task_id) - - -def _establish_reviewed_plan_authority(control: Path, plan_id: str) -> None: - orchestration = control / ".work-bundle/orchestration" - metadata = control / ".work-bundle/project.yaml" - metadata.parent.mkdir(parents=True, exist_ok=True) - metadata.write_text( - f"metadata_version: 3\nworkspace_root: {control}\nworkspace_mode: single-repository\n", - encoding="utf-8", - ) - specification = orchestration / "spec/active/spec.md" - plan = orchestration / "plan/active/plan.md" - specification.parent.mkdir(parents=True, exist_ok=True) - plan.parent.mkdir(parents=True, exist_ok=True) - specification.write_text( - "---\nid: spec-native\nstatus: verified\n" - "requirements: [{id: REQ-NATIVE, requirement: product.py defines VALUE as 1.}]\n" - "---\n- **REQ-NATIVE**: product.py defines VALUE as 1.\n", - encoding="utf-8", - ) - plan.write_text( - f"---\nid: {plan_id}\nstatus: Planned\nsource_spec: [spec-native]\n---\nNative test plan.\n", - encoding="utf-8", - ) - for stage, identity in ( - ("specification", review_runtime.artifact_review_identity(specification)), - ("plan", review_runtime.plan_review_identity(control, plan)), - ): - record = { - "review_id": f"review-{stage}-{plan_id}", - "stage": stage, - "target_identity": identity, - "review_mode": "initial", - "review_target_kind": "stage", - "repair_frontier": None, - "review_reset": None, - "reviewer": { - "agent_id": f"reviewer-{stage}", "capability": "judgment", - "authorship": "none", "repair_participation": "none", - "decision_participation": "none", "deliberation_participation": "none", - "context_origin": "direct_source", - }, - "evidence": { - "mode": "direct", "capabilities": ["source inspection"], - "unavailable_evidence": [], "commands": [], "artifacts": [], - }, - "verdict": "accepted", "findings": [], - "started_at": "2026-09-09T00:00:00Z", - "completed_at": "2026-09-09T00:01:00Z", - "staleness": {"is_stale": False, "reason": None, "supersedes": None}, - } - record = bind_review_receipt(control, record) - review_runtime.publish_review( - control, record, current_target_identity=record["target_identity"] - ) - - -def _harness_validation(control: Path, source: Path) -> tuple[dict[str, object], Path]: - counter = control / "harness-validation-count.txt" - program = control / "validate-product.py" - program.write_text( - "from pathlib import Path\n" - "import sys\n" - "counter = Path(sys.argv[2])\n" - "count = int(counter.read_text()) if counter.exists() else 0\n" - "counter.write_text(str(count + 1))\n" - "namespace = {}\n" - "exec(Path(sys.argv[1]).read_text(), namespace)\n" - "raise SystemExit(0 if namespace.get('VALUE') == 1 else 1)\n", - encoding="utf-8", - ) - command = shlex.join( - [sys.executable, str(program), str(source / "product.py"), str(counter)] - ) - return { - "id": "VAL-NATIVE", - "kind": "process", - "command": command, - "invariant_ids": ["INV-NATIVE"], - "capability_reason": "The harness process directly checks the required product value.", - "proves": "REQ-NATIVE", - "expected": "passed", - "evidence_reuse": {"mode": "deterministic", "max_age_seconds": 3600}, - }, counter - - -def _validation_capability(task_id: str) -> dict[str, object]: - return { - "result": "mapped", - "reason": "The harness command directly falsifies an incorrect product value.", - "invariants": [{ - "id": "INV-NATIVE", - "source_ids": ["REQ-NATIVE"], - "invariant": "product.py defines VALUE as 1.", - "boundary": "product.py", - "oracle": "VAL-NATIVE", - "capability_reason": "The harness imports and checks the exact value.", - "freshness": "current_task_batch", - "task_id": task_id, - "evidence_ids": ["VAL-NATIVE"], - "closure_result": "pending", - }], - } - - -def _invocation_counts( - executor_events: list[dict[str, object]], receipt: dict[str, object], counter: Path, -) -> tuple[int, int, int]: - review_events = [ - json.loads(line) - for line in Path(str(receipt["receipt_path"])).with_suffix(".stdout.jsonl").read_text().splitlines() - ] - return ( - sum(event.get("type") == "thread.started" for event in executor_events), - sum(event.get("type") == "thread.started" for event in review_events), - int(counter.read_text(encoding="utf-8")), - ) - - -def _native_events(result: dict[str, object]) -> str: - events = [ - { - "type": "thread.started", - "thread_id": "01a0821d-f359-7d60-a9bd-90dd0e006166", - }, - {"type": "turn.started"}, - { - "type": "item.completed", - "item": { - "id": "judgment", - "type": "agent_message", - "text": json.dumps(result), - }, - }, - {"type": "turn.completed", "usage": {}}, - ] - return "\n".join(json.dumps(event) for event in events) - - -def test_plugin_absent_native_review_publishes_once_then_materializes_initial_acceptance( - tmp_path: Path, monkeypatch, -) -> None: - source = tmp_path / "source" - control = tmp_path / "control" - source.mkdir() - control.mkdir() - (source / "product.py").write_text("VALUE = 1\n", encoding="utf-8") - _git(source, "init", "-q") - _git(source, "add", "product.py") - _git( - source, - "-c", - "user.name=Test", - "-c", - "user.email=test@example.invalid", - "commit", - "-qm", - "fixture", - ) - head = _git(source, "rev-parse", "HEAD") - tree = _git(source, "rev-parse", "HEAD^{tree}") - _establish_reviewed_plan_authority(control, "plan-native") - validation, validation_counter = _harness_validation(control, source) - - task = { - "plan_id": "plan-native", - "task_id": "task-native", - "source_ids": ["REQ-NATIVE"], - "goal": "Create the requested product constant", - "requirements": ["product.py must define VALUE with the integer value 1."], - "constraints": ["The implementation must remain within product.py."], - "truth_basis": {"decision_authority": []}, - "files": {"read": ["product.py"], "write": ["product.py"], "forbidden": []}, - "validation": [validation], - "evidence_capability": _validation_capability("task-native"), - "review_required": True, - "workspace": {"root": str(control)}, - } - _persist_production_binding( - control, - source, - task, - execution_id="executor-run", - baseline={"head": head, "tree": tree}, - ) - original_handoff = { - "type": "executor-result", - "related": {"plan": "plan-native", "task": "task-native"}, - "result": {"state": "completed", "summary": "Implemented native fixture."}, - "changes": {"files": [{"path": "product.py", "change": "updated"}]}, - "task_fit_check": {"task": "task-native", "result": "clean"}, - "knowledge_disposition": { - "action": "none", - "reason": "No durable authority changed.", - "affected_authority": [], - }, - "delegation_evidence": { - "delegated": True, - "owner_kind": "subagent", - "agent_id": "executor-agent", - "run_id": "executor-run", - "mechanism": "host-native", - }, - "repository": [{ - "root": str(source.resolve()), - "target_kind": "git-backed", - "preflight_kind": "git-clean-worktree", - "baseline": "initial", - "status": "clean", - }], - "codegraph": [{ - "root": str(source.resolve()), - "applicable": False, - "up_to_date": False, - "reason": "no-index", - }], - "evidence_closure": { - "result": "passed", - "invariants": [{ - "id": "INV-NATIVE", - "boundary": "product.py", - "freshness": "current_task_batch", - "evidence_ids": ["VAL-NATIVE"], - "closure_result": "passed", - "repair_owner": None, - }], - }, - "validation": { - "commands": [ - { - "id": "VAL-NATIVE", - "command": validation["command"], - "invariant_ids": ["INV-NATIVE"], - "result": "passed", - } - ] - }, - } - validated = execution_context.validate_executor_result_for_task( - original_handoff, - task, - observe=True, - mutation_events=[{"actor_kind": "subagent", "paths": ["product.py"]}], - preparing_review=True, - ) - handoff_before_review = deepcopy(original_handoff) - target_identity = { - "artifact_id": "task-native", - "revision": head, - "sha256": hashlib.sha256(json.dumps(task, sort_keys=True).encode()).hexdigest(), - "source_tree": tree, - } - task_review_context = { - "target_identity": target_identity, - "agent_id": "unbound-native-reviewer", - "capability": "judgment", - "execution_id": "unbound-native-reviewer", - "evidence_mode": "reproducible_snapshot", - "review_mode": "initial", - "review_target_kind": "task", - "repair_frontier": None, - "review_reset": None, - } - protected = control / ".protected" - protected.mkdir() - authority = control / "task-authority.json" - authority.write_text(json.dumps(task, sort_keys=True), encoding="utf-8") - packet = reviewer_workspace.build_direct_evidence_packet( - source_root=source, - control_root=control, - protected_roots=[protected], - artifacts=[ - "source:product.py", - "control:task-authority.json", - "control:.work-bundle/runtime/completion-provenance/completion-provenance-v1.json", - ], - search_roots=[], - validators=[], - sentinels=[], - network_state="denied", - task_review_context=task_review_context, - ) - created = reviewer_workspace.create_reviewer_workspace( - review_runtime.reviewer_runtime_root(control), "review-native-initial", packet - ) - judgment = { - "task_review": {"reviewed_head": head, "verdict": "accept", "findings": []} - } - native_calls = 0 - - def run_native(*_args): - nonlocal native_calls - native_calls += 1 - return subprocess.CompletedProcess([], 0, _native_events(judgment), "") - - monkeypatch.setattr(reviewer_workspace, "_run_native_process", run_native) - - receipt = reviewer_workspace.run_native_reviewer( - Path(str(created["workspace_path"])), - Path(sys.executable), - model="test-model", - review_instructions=( - "Judge the task authority, source, and harness-owned validation evidence. Return repair " - "for unmet requirements; otherwise return accept." - ), - ) - review = {**receipt["review_result"], "reviewer_run": receipt["reviewer_run"]} - reference = review_runtime.publish_review( - control, review, current_target_identity=target_identity - ) - fixture_executor_events = [{"type": "thread.started", "thread_id": "executor-run"}] - counts_after_publication = _invocation_counts( - fixture_executor_events, receipt, validation_counter - ) - assert review_runtime.publish_review( - control, review, current_target_identity=target_identity - ) == reference - counts_after_publication_retry = _invocation_counts( - fixture_executor_events, receipt, validation_counter - ) - accepted = execution_context.materialize_accepted_task_review( - control, - task, - reference, - { - "causal_class": "initial_acceptance", - "affected_task": "task-native", - "authorized_lifecycle_action": "materialize_accepted_result", - }, - executor_handoff=original_handoff, - validated_executor_result=validated, - ) - _, consumed = execution_context.load_current_accepted_task_result(control, task) - counts_after_consumption = _invocation_counts( - fixture_executor_events, receipt, validation_counter - ) - stored = execution_context.load_task_execution_binding(control, "plan-native", "task-native") - - assert native_calls == 1 - assert counts_after_publication == counts_after_publication_retry == counts_after_consumption == ( - 1, 1, 1 - ) - assert original_handoff == handoff_before_review - assert accepted == consumed == stored["accepted_result"] - assert accepted["baseline_identity"] == {"head": head, "tree": tree} - assert accepted["review_id"] == review["review_id"] - assert accepted["validation_evidence_ids"] == [ - validated["observed_validation"][0]["observation_id"] - ] - assert accepted["owner_identity"]["agent_id"] == "executor-agent" - assert review["reviewer"]["agent_id"] != accepted["owner_identity"]["agent_id"] - request = json.loads(Path(receipt["receipt_path"]).with_suffix(".request.json").read_text()) - assert "execution-flow" not in json.dumps(request).lower() - - task["validation"][0]["command"] = "python -m pytest tests/changed.py -q" - with pytest.raises(SystemExit, match="validation authority changed"): - execution_context.load_current_accepted_task_result(control, task) - - -def test_missing_native_host_capability_fails_before_review_dispatch( - tmp_path: Path, monkeypatch, -) -> None: - dispatched = False - - def unexpected_dispatch(*_args): - nonlocal dispatched - dispatched = True - raise AssertionError("review dispatch must not occur") - - monkeypatch.setattr(reviewer_workspace, "_run_native_process", unexpected_dispatch) - missing = tmp_path / "missing-native-host" - with pytest.raises( - reviewer_workspace.ReviewerWorkspaceError, - match="WB_REVIEW_NATIVE_CAPABILITY_UNAVAILABLE", - ) as failure: - reviewer_workspace.run_native_reviewer( - tmp_path, - missing, - model="test-model", - review_instructions="Review the supplied task.", - ) - - assert failure.value.result == {"capability": "native_reviewer_executable"} - assert dispatched is False - - -def test_incorrect_executor_result_cannot_reach_acceptance_by_matching_review_shape( - tmp_path: Path, monkeypatch, -) -> None: - source = tmp_path / "source" - control = tmp_path / "control" - source.mkdir() - control.mkdir() - (source / "product.py").write_text("VALUE = 0\n", encoding="utf-8") - _git(source, "init", "-q") - _git(source, "add", "product.py") - _git( - source, - "-c", - "user.name=Test", - "-c", - "user.email=test@example.invalid", - "commit", - "-qm", - "incorrect executor output", - ) - head = _git(source, "rev-parse", "HEAD") - tree = _git(source, "rev-parse", "HEAD^{tree}") - _establish_reviewed_plan_authority(control, "plan-negative") - validation, validation_counter = _harness_validation(control, source) - task = { - "plan_id": "plan-negative", - "task_id": "task-negative", - "source_ids": ["REQ-NATIVE"], - "goal": "Produce the required product value", - "requirements": ["product.py must define VALUE with the integer value 1."], - "constraints": [], - "truth_basis": {"decision_authority": ["REQ-NATIVE is authoritative."]}, - "files": {"read": ["product.py"], "write": ["product.py"], "forbidden": []}, - "validation": [validation], - "evidence_capability": _validation_capability("task-negative"), - "review_required": True, - "workspace": {"root": str(control)}, - } - _persist_production_binding( - control, - source, - task, - execution_id="executor-negative", - baseline={"head": head, "tree": tree}, - ) - handoff = { - "type": "executor-result", - "related": {"plan": "plan-negative", "task": "task-negative"}, - "result": {"state": "completed", "summary": "Produced the requested value."}, - "changes": {"files": [{"path": "product.py", "change": "updated"}]}, - "task_fit_check": {"task": "task-negative", "result": "clean"}, - "knowledge_disposition": { - "action": "none", "reason": "No durable authority changed.", "affected_authority": [], - }, - "delegation_evidence": { - "delegated": True, - "owner_kind": "subagent", - "agent_id": "executor-negative", - "run_id": "executor-negative", - "mechanism": "host-native", - }, - "repository": [{ - "root": str(source.resolve()), "target_kind": "git-backed", - "preflight_kind": "git-clean-worktree", "baseline": "initial", "status": "clean", - }], - "codegraph": [{ - "root": str(source.resolve()), "applicable": False, - "up_to_date": False, "reason": "no-index", - }], - "evidence_closure": { - "result": "passed", - "invariants": [{ - "id": "INV-NATIVE", "boundary": "product.py", - "freshness": "current_task_batch", "evidence_ids": ["VAL-NATIVE"], - "closure_result": "passed", "repair_owner": None, - }], - }, - "validation": {"commands": [{ - "id": "VAL-NATIVE", "command": validation["command"], - "invariant_ids": ["INV-NATIVE"], "result": "passed", - }]}, - } - review_calls = 0 - - def unexpected_review(*_args): - nonlocal review_calls - review_calls += 1 - raise AssertionError("failed harness validation must block review dispatch") - - monkeypatch.setattr(reviewer_workspace, "_run_native_process", unexpected_review) - - with pytest.raises(SystemExit, match="does not match observed failed"): - execution_context.validate_executor_result_for_task( - handoff, - task, - observe=True, - mutation_events=[{"actor_kind": "subagent", "paths": ["product.py"]}], - preparing_review=True, - ) - stored = execution_context.load_task_execution_binding( - control, "plan-negative", "task-negative" - ) - assert validation_counter.read_text(encoding="utf-8") == "1" - assert review_calls == 0 - assert "accepted_result" not in stored - - -@pytest.mark.skipif( - os.environ.get("WB_NATIVE_REVIEW_INTEGRATION") != "1", - reason="set WB_NATIVE_REVIEW_INTEGRATION=1 for the genuine native host observation", -) -def test_live_plugin_absent_native_execution_review_publication_and_acceptance( - tmp_path: Path, monkeypatch, -) -> None: - executable_text = os.environ.get("WB_NATIVE_REVIEW_EXECUTABLE") or shutil.which("codex") - if not executable_text: - pytest.fail("native_executor_executable capability is unavailable") - executable = Path(executable_text).expanduser().resolve() - model = os.environ.get("WB_NATIVE_REVIEW_MODEL", "gpt-6-astra") - source = tmp_path / "source" - control = tmp_path / "control" - scratch = tmp_path / "scratch" - source.mkdir() - control.mkdir() - scratch.mkdir() - (source / "README.md").write_text("native integration fixture\n", encoding="utf-8") - _git(source, "init", "-q") - _git(source, "add", "README.md") - _git( - source, - "-c", - "user.name=Test", - "-c", - "user.email=test@example.invalid", - "commit", - "-qm", - "baseline", - ) - baseline_head = _git(source, "rev-parse", "HEAD") - baseline_tree = _git(source, "rev-parse", "HEAD^{tree}") - - executor_argv = [ - str(executable), - "exec", - "--ignore-user-config", - "--sandbox", - "workspace-write", - "--ephemeral", - "--json", - "--skip-git-repo-check", - "-C", - str(source), - "-m", - model, - "-c", - 'model_reasoning_effort="medium"', - "-c", - "project_doc_max_bytes=0", - "-c", - 'web_search="disabled"', - "--enable", - "skip_host_skill_discovery", - "--disable", - "remote_plugin", - "--disable", - "recommended_plugins", - "--disable", - "apps", - "--disable", - "multi_agent", - "-", - ] - environment = { - "PATH": os.environ.get("PATH", "/usr/bin:/bin:/usr/sbin:/sbin"), - "HOME": str(Path.home()), - "TMPDIR": str(scratch), - } - if os.environ.get("CODEX_HOME"): - environment["CODEX_HOME"] = os.environ["CODEX_HOME"] - executor = subprocess.run( - executor_argv, - cwd=source, - env=environment, - input=( - "Create product.py in this workspace with exactly this UTF-8 content: " - "VALUE = 1 followed by one newline. Do not use network access. " - "Finish only after verifying the file exists." - ), - text=True, - capture_output=True, - check=False, - timeout=1800, - ) - assert executor.returncode == 0, executor.stderr - assert (source / "product.py").read_bytes() == b"VALUE = 1\n" - executor_events = [json.loads(line) for line in executor.stdout.splitlines()] - executor_ids = [ - event["thread_id"] - for event in executor_events - if event.get("type") == "thread.started" - ] - assert len(executor_ids) == 1 - assert sum(event.get("type") == "turn.completed" for event in executor_events) == 1 - _git(source, "add", "product.py") - _git( - source, - "-c", - "user.name=Test", - "-c", - "user.email=test@example.invalid", - "commit", - "-qm", - "native executor result", - ) - head = _git(source, "rev-parse", "HEAD") - tree = _git(source, "rev-parse", "HEAD^{tree}") - _establish_reviewed_plan_authority(control, "plan-native-live") - validation, validation_counter = _harness_validation(control, source) - - task = { - "plan_id": "plan-native-live", - "task_id": "task-native-live", - "source_ids": ["REQ-NATIVE"], - "goal": "Create the requested product constant", - "requirements": ["product.py must define VALUE with the integer value 1."], - "constraints": ["The implementation must remain within product.py."], - "truth_basis": { - "decision_authority": ["The requested VALUE behavior is authoritative."], - }, - "files": {"read": ["product.py"], "write": ["product.py"], "forbidden": []}, - "validation": [validation], - "evidence_capability": _validation_capability("task-native-live"), - "review_required": True, - "workspace": {"root": str(control)}, - } - _persist_production_binding( - control, - source, - task, - execution_id=executor_ids[0], - baseline={"head": baseline_head, "tree": baseline_tree}, - ) - handoff = { - "type": "executor-result", - "related": {"plan": "plan-native-live", "task": "task-native-live"}, - "result": {"state": "completed", "summary": "Created product.py."}, - "changes": {"files": [{"path": "product.py", "change": "created"}]}, - "task_fit_check": {"task": "task-native-live", "result": "clean"}, - "knowledge_disposition": { - "action": "none", - "reason": "No durable authority changed.", - "affected_authority": [], - }, - "delegation_evidence": { - "delegated": True, - "owner_kind": "subagent", - "agent_id": executor_ids[0], - "run_id": executor_ids[0], - "mechanism": "host-native", - }, - "repository": [{ - "root": str(source.resolve()), - "target_kind": "git-backed", - "preflight_kind": "git-clean-worktree", - "baseline": "initial", - "status": "clean", - }], - "codegraph": [{ - "root": str(source.resolve()), - "applicable": False, - "up_to_date": False, - "reason": "no-index", - }], - "evidence_closure": { - "result": "passed", - "invariants": [{ - "id": "INV-NATIVE", - "boundary": "product.py", - "freshness": "current_task_batch", - "evidence_ids": ["VAL-NATIVE"], - "closure_result": "passed", - "repair_owner": None, - }], - }, - "validation": {"commands": [{ - "id": "VAL-NATIVE", - "command": validation["command"], - "invariant_ids": ["INV-NATIVE"], - "result": "passed", - }]}, - } - validated = execution_context.validate_executor_result_for_task( - handoff, - task, - observe=True, - mutation_events=[{"actor_kind": "subagent", "paths": ["product.py"]}], - preparing_review=True, - ) - target_identity = { - "artifact_id": "task-native-live", - "revision": head, - "sha256": hashlib.sha256(json.dumps(task, sort_keys=True).encode()).hexdigest(), - "source_tree": tree, - } - context = { - "target_identity": target_identity, - "agent_id": "unbound-native-reviewer", - "capability": "judgment", - "execution_id": "unbound-native-reviewer", - "evidence_mode": "reproducible_snapshot", - "review_mode": "initial", - "review_target_kind": "task", - "repair_frontier": None, - "review_reset": None, - } - protected = control / ".protected" - protected.mkdir() - authority = control / "task-authority.json" - authority.write_text(json.dumps(task, sort_keys=True), encoding="utf-8") - packet = reviewer_workspace.build_direct_evidence_packet( - source_root=source, - control_root=control, - protected_roots=[protected], - artifacts=[ - "source:product.py", - "control:task-authority.json", - "control:.work-bundle/runtime/completion-provenance/completion-provenance-v1.json", - ], - search_roots=[], - validators=[], - sentinels=[], - network_state="denied", - task_review_context=context, - ) - created = reviewer_workspace.create_reviewer_workspace( - review_runtime.reviewer_runtime_root(control), "review-native-live", packet - ) - receipt = reviewer_workspace.run_native_reviewer( - Path(str(created["workspace_path"])), - executable, - model=model, - review_instructions=( - "Independently judge the supplied task source against task-authority.json. Return a " - "task_review JSON object for the supplied reviewed_head. Set verdict to accept only " - "when every requirement is satisfied; otherwise set it to repair and report concrete " - "findings using the required review contract." - ), - ) - review = {**receipt["review_result"], "reviewer_run": receipt["reviewer_run"]} - assert review["reviewer"]["agent_id"] != executor_ids[0] - reference = review_runtime.publish_review( - control, review, current_target_identity=target_identity - ) - counts_after_publication = _invocation_counts( - executor_events, receipt, validation_counter - ) - assert review_runtime.publish_review( - control, review, current_target_identity=target_identity - ) == reference - counts_after_publication_retry = _invocation_counts( - executor_events, receipt, validation_counter - ) - - accepted = execution_context.materialize_accepted_task_review( - control, - task, - reference, - { - "causal_class": "initial_acceptance", - "affected_task": "task-native-live", - "authorized_lifecycle_action": "materialize_accepted_result", - }, - executor_handoff=handoff, - validated_executor_result=validated, - ) - _, consumed = execution_context.load_current_accepted_task_result(control, task) - counts_after_consumption = _invocation_counts( - executor_events, receipt, validation_counter - ) - assert counts_after_publication == counts_after_publication_retry == counts_after_consumption == ( - 1, 1, 1 - ) - assert consumed == accepted - assert accepted["review_id"] == review["review_id"] - assert accepted["baseline_identity"] == { - "head": baseline_head, - "tree": baseline_tree, - } diff --git a/tests/test_orchestration_accepted_result.py b/tests/test_orchestration_accepted_result.py deleted file mode 100644 index 3fcd142..0000000 --- a/tests/test_orchestration_accepted_result.py +++ /dev/null @@ -1,995 +0,0 @@ -from __future__ import annotations - -from copy import deepcopy -from pathlib import Path -import subprocess -import sys -from types import SimpleNamespace - -import pytest - - -ORCHESTRATION = Path(__file__).resolve().parents[1] / "scripts" / "orchestration" -loaded_core = sys.modules.get("core") -loaded_core_path = Path(getattr(loaded_core, "__file__", "")) if loaded_core is not None else None -if loaded_core_path is not None and ORCHESTRATION not in loaded_core_path.parents: - sys.modules.pop("core", None) -sys.path.insert(0, str(ORCHESTRATION)) - -import execution_context # noqa: E402 -import review_runtime # noqa: E402 -from task_ownership import ( # noqa: E402 - OwnershipBlocker, - TaskCandidate, - canonical_relative_path, - validate_task_acceptance_ownership, -) - - -OID_A = "a" * 40 -OID_B = "b" * 40 -OID_C = "c" * 40 -OID_D = "d" * 40 - -ACCEPTED_RESULT_FIELDS = { - "schema", - "plan_id", - "task_id", - "binding_id", - "baseline_identity", - "accepted_source", - "authority_projection", - "executor_result_digest", - "validation_evidence_ids", - "review_id", - "owner_identity", - "knowledge_disposition", - "accepted_at", - "invalidation", -} - - -def _git(root: Path, *arguments: str) -> str: - completed = subprocess.run( - ["git", *arguments], cwd=root, check=True, capture_output=True, text=True - ) - return completed.stdout.strip() - - -def _task(root: Path) -> dict[str, object]: - return { - "plan_id": "plan-001", - "task_id": "task-001", - "depends_on": ["task-000"], - "source_ids": ["REQ-001"], - "files": { - "read": ["src/./read.py"], - "write": ["src//a.py"], - "forbidden": ["secrets/key.txt"], - }, - "validation": [ - { - "id": "VAL-001", - "kind": "process", - "command": "pytest -q", - "boundary": "component", - "freshness": "current_task_batch", - } - ], - "review_required": True, - "executor_profile": {"capability": "judgment"}, - "workspace": {"root": str(root)}, - } - - -def _binding(root: Path) -> dict[str, object]: - return { - "plan_id": "plan-001", - "task_id": "task-001", - "workspace_id": "ws-001", - "execution_id": "exec-001", - "repository_id": "repo-001", - "execution_path": str(root), - "control_root": str(root), - "git_identity": {"branch_ref": "refs/heads/main"}, - "baseline": {"head": OID_A, "tree": OID_B}, - "ownership": { - "binding_id": "binding:plan-001:task-001", - "state": "active", - "current_owner": "task-001", - "history": [{"event": "created"}], - }, - } - - -def _record_validation_observation( - root: Path, - binding: dict[str, object], - task: dict[str, object], - *, - live_initial_acceptance: bool = False, -) -> str: - item = task["validation"][0] - assert isinstance(item, dict) - if live_initial_acceptance: - item["evidence_reuse"] = { - "mode": "live", "max_age_seconds": 0, - "environment_inputs": [], "include_head": True, - } - observed_item = { - **item, - "evidence_reuse": {**item["evidence_reuse"], "max_age_seconds": 86400}, - } - else: - item.setdefault("evidence_reuse", { - "mode": "deterministic", "max_age_seconds": 3600, - "environment_inputs": [], "include_head": False, - }) - observed_item = item - evidence = execution_context.capture_repository_evidence(root) - - def observe(receipt: dict[str, object]) -> dict[str, object]: - receipt.update({ - "exit_code": 0, - "stdout_digest": "1" * 64, - "stderr_digest": "2" * 64, - "started_at": "2026-09-08T01:00:00Z", - "completed_at": "2026-09-08T01:00:01Z", - }) - return { - "id": item.get("id"), "command": item.get("command"), - "invariant_ids": item.get("invariant_ids", []), "result": "passed", - } - - observed = execution_context._completion_provenance_module().observe_validation( - binding, execution_context._validation_observation_task(task), observed_item, evidence, observe, - lambda: execution_context.capture_repository_evidence(root), - finalization_id=( - f"initial-acceptance:{task['plan_id']}:{task['task_id']}:fixture" - if live_initial_acceptance else None - ), - ) - return str(observed["observation_id"]) - - -def _orthogonally_advanced_observation(tmp_path: Path) -> tuple[dict, dict, str, str]: - _git(tmp_path, "init", "-q") - _git(tmp_path, "config", "user.email", "test@example.com") - _git(tmp_path, "config", "user.name", "Test") - for relative in ("src/read.py", "src/a.py", "config/test.ini"): - target = tmp_path / relative - target.parent.mkdir(parents=True, exist_ok=True) - target.write_text(f"# {relative}\n", encoding="utf-8") - _git(tmp_path, "add", ".") - _git(tmp_path, "commit", "-qm", "reviewed task source") - reviewed_head = _git(tmp_path, "rev-parse", "HEAD") - (tmp_path / ".git/info/exclude").write_text(".work-bundle/\n", encoding="utf-8") - task = _task(tmp_path) - task["depends_on"] = [] - task["validation"][0]["evidence_reuse"] = { - "mode": "deterministic", - "max_age_seconds": 3600, - "environment_inputs": [], - "dependency_files": ["config/test.ini"], - "include_head": False, - } - binding = _binding(tmp_path) - observation_id = _record_validation_observation(tmp_path, binding, task) - - (tmp_path / "orthogonal.py").write_text("VALUE = 1\n", encoding="utf-8") - _git(tmp_path, "add", "orthogonal.py") - _git(tmp_path, "commit", "-qm", "integrate orthogonal task") - return task, binding, observation_id, reviewed_head - - -def test_claim_bound_observation_survives_only_orthogonal_head_progress(tmp_path: Path) -> None: - task, binding, observation_id, reviewed_head = _orthogonally_advanced_observation(tmp_path) - matched = execution_context._claim_bound_validation_observations( - binding, - task, - execution_context.capture_repository_evidence(tmp_path), - [observation_id], - reviewed_head=reviewed_head, - ) - assert [item["observation_id"] for item in matched] == [observation_id] - - dependent_task = deepcopy(task) - dependent_task["depends_on"] = ["task-upstream"] - with pytest.raises(SystemExit, match="claim-bound"): - execution_context._claim_bound_validation_observations( - binding, - dependent_task, - execution_context.capture_repository_evidence(tmp_path), - [observation_id], - reviewed_head=reviewed_head, - ) - - -def test_claim_bound_observation_rejects_intermediate_dependency_change_reverted_at_head( - tmp_path: Path, -) -> None: - task, binding, observation_id, reviewed_head = _orthogonally_advanced_observation(tmp_path) - original = (tmp_path / "config/test.ini").read_text(encoding="utf-8") - (tmp_path / "config/test.ini").write_text("changed=true\n", encoding="utf-8") - _git(tmp_path, "add", "config/test.ini") - _git(tmp_path, "commit", "-qm", "change validation dependency") - (tmp_path / "config/test.ini").write_text(original, encoding="utf-8") - _git(tmp_path, "add", "config/test.ini") - _git(tmp_path, "commit", "-qm", "restore validation dependency") - with pytest.raises(SystemExit, match="claim-bound"): - execution_context._claim_bound_validation_observations( - binding, - task, - execution_context.capture_repository_evidence(tmp_path), - [observation_id], - reviewed_head=reviewed_head, - ) - - -def test_transition_changed_paths_exposes_both_sides_of_claim_path_rename( - tmp_path: Path, -) -> None: - _git(tmp_path, "init", "-q") - _git(tmp_path, "config", "user.email", "test@example.com") - _git(tmp_path, "config", "user.name", "Test") - claim_path = tmp_path / "config/test.ini" - claim_path.parent.mkdir(parents=True) - claim_path.write_text("enabled=true\n", encoding="utf-8") - _git(tmp_path, "add", ".") - _git(tmp_path, "commit", "-qm", "claim source") - previous = _git(tmp_path, "rev-parse", "HEAD") - _git(tmp_path, "mv", "config/test.ini", "orthogonal.ini") - _git(tmp_path, "commit", "-qm", "rename claim source") - - assert execution_context._transition_changed_paths( - tmp_path, previous, _git(tmp_path, "rev-parse", "HEAD") - ) == {"config/test.ini", "orthogonal.ini"} - - -def test_claim_bound_observation_rejects_claim_path_rename_then_revert( - tmp_path: Path, -) -> None: - task, binding, observation_id, reviewed_head = _orthogonally_advanced_observation(tmp_path) - _git(tmp_path, "mv", "config/test.ini", "temporary.ini") - _git(tmp_path, "commit", "-qm", "rename validation dependency") - _git(tmp_path, "mv", "temporary.ini", "config/test.ini") - _git(tmp_path, "commit", "-qm", "restore validation dependency path") - - with pytest.raises(SystemExit, match="claim-bound"): - execution_context._claim_bound_validation_observations( - binding, - task, - execution_context.capture_repository_evidence(tmp_path), - [observation_id], - reviewed_head=reviewed_head, - ) - - -def test_claim_bound_observation_rejects_unprojected_validation_field_drift( - tmp_path: Path, -) -> None: - task, binding, observation_id, reviewed_head = _orthogonally_advanced_observation(tmp_path) - task["validation"][0]["capability_reason"] = "A changed claim-bearing reason." - - with pytest.raises(SystemExit, match="claim-bound"): - execution_context._claim_bound_validation_observations( - binding, - task, - execution_context.capture_repository_evidence(tmp_path), - [observation_id], - reviewed_head=reviewed_head, - ) - - -def _handoff() -> dict[str, object]: - return { - "type": "executor-result", - "related": {"plan": "plan-001", "task": "task-001"}, - "result": {"state": "completed", "summary": "Implemented the bounded slice."}, - "changes": {"files": [{"path": "src/a.py", "change": "updated"}]}, - "task_fit_check": {"task": "task-001", "result": "clean"}, - "knowledge_disposition": {"action": "none", "affected_authority": []}, - "acceptance_review": {"required": True, "verdict": "accept", "review_id": "review-001"}, - "delegation_evidence": { - "delegated": True, - "owner_kind": "subagent", - "agent_id": "agent-001", - "run_id": "run-001", - "mechanism": "host-native", - }, - "validation": {"commands": [{"command": "pytest -q", "result": "passed"}]}, - } - - -def _validated() -> dict[str, object]: - return { - "result_state": "completed", - "knowledge_disposition": { - "action": "none", - "reason": "No durable authority changed.", - "affected_authority": [], - }, - "task_ownership": { - "delegated": True, - "owner_kind": "subagent", - "agent_id": "agent-001", - "run_id": "run-001", - "mechanism": "host-native", - }, - "observed_validation": [{"id": "VAL-001", "observation_id": "obs-001", "result": "passed"}], - } - - -def _build_accepted_task_result( - task: dict[str, object], - binding: dict[str, object], - handoff: dict[str, object], - validated: dict[str, object], - **kwargs: object, -) -> dict[str, object]: - review = handoff.get("acceptance_review") - assert isinstance(review, dict) - return execution_context.build_accepted_task_result( - task, binding, handoff, validated, accepted_review=review, **kwargs - ) - - -def test_common_accepted_result_path_rejects_embedded_handoff_review( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - monkeypatch.setattr( - execution_context, - "capture_repository_evidence", - lambda _root: {"head": OID_A, "tree": OID_B, "entries": {}, "status": "clean"}, - ) - - with pytest.raises(SystemExit, match="accepted mandatory review"): - execution_context.build_accepted_task_result( - _task(tmp_path), _binding(tmp_path), _handoff(), _validated() - ) - - -def test_repair_review_preparation_derives_exact_stored_controller_frontier( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - previous_identity = { - "artifact_id": "task-001", - "revision": OID_A, - "sha256": "1" * 64, - "source_tree": OID_B, - } - previous = { - "review_id": "review-prior", - "review_target_kind": "task", - "verdict": "repair", - "target_identity": previous_identity, - "evidence": {"mode": "direct", "capabilities": [], "commands": [], "artifacts": [], "unavailable_evidence": []}, - "reviewer_run": {"run_id": "reviewer-run-prior", "sha256": "2" * 64}, - "findings": [ - { - "finding_id": "F-001", - "severity": "blocking", - "recommended_owner": "task_owner", - "evidence": [{"locator": "source:src/a.py"}], - } - ], - } - store = tmp_path / ".work-bundle/orchestration/reviews" - store.mkdir(parents=True) - path = store / "review-prior.json" - path.write_text(__import__("json").dumps(previous), encoding="utf-8") - path.chmod(0o444) - validated = SimpleNamespace(review_id="review-prior", target_identity=previous_identity) - monkeypatch.setattr( - review_runtime, - "load_stored_review", - lambda *_args, **_kwargs: (previous, validated), - ) - repaired_identity = { - "artifact_id": "task-001", - "revision": OID_C, - "sha256": "3" * 64, - "source_tree": OID_D, - } - - loaded, frontier = execution_context._stored_task_repair_preparation( - tmp_path, _task(tmp_path), base=OID_A, repaired_identity=repaired_identity - ) - - assert loaded == previous - assert frontier == { - "prior_review_id": "review-prior", - "blocking_finding_ids": ["F-001"], - "previous_reviewed_identity": previous_identity, - "repaired_identity": repaired_identity, - "affected_boundaries": ["source:src/a.py"], - "frozen_evidence_reference": review_runtime.review_evidence_identity(previous), - } - - -def test_shared_scope_canonicalizer_normalizes_equivalent_paths_and_rejects_unsafe() -> None: - assert canonical_relative_path("src/./a.py") == "src/a.py" - assert canonical_relative_path("src//a.py") == "src/a.py" - - for unsafe in ("", ".", "../src/a.py", "/src/a.py", "src\\a.py", "src/*.py"): - with pytest.raises(OwnershipBlocker, match="unsafe|empty"): - canonical_relative_path(unsafe) - - with pytest.raises(SystemExit, match="unsafe"): - execution_context._task_scope_paths(["../src/a.py"], Path.cwd(), "write scope") - - -def test_declared_and_observed_scopes_use_the_same_canonical_semantics() -> None: - with pytest.raises(OwnershipBlocker, match="controller mutated"): - validate_task_acceptance_ownership( - delegation_evidence=_handoff()["delegation_evidence"], - mutation_events=[{"actor_kind": "controller", "paths": ["src/./a.py"]}], - write_scope=["src//a.py"], - validations_passed=True, - ) - - with pytest.raises(OwnershipBlocker, match="unsafe"): - TaskCandidate( - task_id="task-001", - dependencies=(), - write_scope=("../outside.py",), - execution_workspace="bound-worktree", - ) - - -def test_accepted_result_is_deterministic_current_authority_not_handoff_history( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - task = _task(tmp_path) - binding = _binding(tmp_path) - monkeypatch.setattr( - execution_context, - "capture_repository_evidence", - lambda _root: {"head": OID_A, "tree": OID_B, "entries": {}, "status": "dirty"}, - ) - - first = _build_accepted_task_result( - task, binding, _handoff(), _validated(), accepted_at="2026-09-06T10:00:00Z" - ) - appended = deepcopy(binding) - appended["ownership"]["history"].append({"event": "audit-appended"}) - second = _build_accepted_task_result( - task, appended, _handoff(), _validated(), accepted_at="2026-09-06T10:00:00Z" - ) - - assert first == second - assert set(first) == ACCEPTED_RESULT_FIELDS - assert first["schema"] == "accepted-task-result-v1" - assert set(first["authority_projection"]) == { - "task_digest", - "binding_digest", - "scope_digest", - "validation_obligations_digest", - "required_review_digest", - "ownership_digest", - } - assert set(first["accepted_source"]) == {"head", "tree", "state_digest"} - assert first["baseline_identity"] == {"head": OID_A, "tree": OID_B} - assert first["accepted_source"]["state_digest"] == execution_context.semantic_digest( - { - "plan_id": first["plan_id"], - "task_id": first["task_id"], - "binding_id": first["binding_id"], - "baseline_identity": {"head": OID_A, "tree": OID_B}, - "accepted_source": {"head": OID_A, "tree": OID_B}, - "authority_projection": first["authority_projection"], - "knowledge_disposition": first["knowledge_disposition"], - } - ) - assert first["validation_evidence_ids"] == ["obs-001"] - assert first["knowledge_disposition"] == _validated()["knowledge_disposition"] - assert "mutation_events" not in repr(first) - assert "validation" not in first["executor_result_digest"] - execution_context.assert_accepted_task_result_current(task, appended, first) - - tampered_disposition = deepcopy(first) - tampered_disposition["knowledge_disposition"] = { - "action": "update", - "reason": "Tampered after acceptance.", - "affected_authority": ["REQ-001"], - } - with pytest.raises(SystemExit, match="accepted task result.*source"): - execution_context.assert_accepted_task_result_current(task, binding, tampered_disposition) - - changed_scope = deepcopy(task) - changed_scope["files"]["write"] = ["src/other.py"] - with pytest.raises(SystemExit, match="accepted task result.*scope"): - execution_context.assert_accepted_task_result_current(changed_scope, appended, first) - - changed_source = deepcopy(task) - changed_source["source_ids"] = ["REQ-002"] - with pytest.raises(SystemExit, match="accepted task result.*task"): - execution_context.assert_accepted_task_result_current(changed_source, appended, first) - - changed_binding = deepcopy(binding) - changed_binding["execution_id"] = "exec-002" - with pytest.raises(SystemExit, match="accepted task result.*binding"): - execution_context.assert_accepted_task_result_current(task, changed_binding, first) - - changed_review = deepcopy(task) - changed_review["review_required"] = False - with pytest.raises(SystemExit, match="review"): - execution_context.assert_accepted_task_result_current(changed_review, binding, first) - - invalidated = deepcopy(first) - invalidated["invalidation"] = {"reason": "accepted source changed"} - with pytest.raises(SystemExit, match="explicitly invalidated"): - execution_context.assert_accepted_task_result_current(task, binding, invalidated) - - -@pytest.mark.parametrize("predecessor_kind", ["task", "integrated_stage"]) -def test_standalone_repair_review_rematerializes_compact_result_without_executor_replay( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, predecessor_kind: str -) -> None: - _git(tmp_path, "init", "-q") - _git(tmp_path, "config", "user.email", "test@example.com") - _git(tmp_path, "config", "user.name", "Test") - source = tmp_path / "source.py" - source.write_text("VALUE = 1\n") - _git(tmp_path, "add", "source.py") - _git(tmp_path, "commit", "-qm", "accepted executor result") - accepted_head = _git(tmp_path, "rev-parse", "HEAD") - accepted_tree = _git(tmp_path, "rev-parse", "HEAD^{tree}") - task = _task(tmp_path) - task["depends_on"] = [] - binding = _binding(tmp_path) - binding["baseline"] = {"head": accepted_head, "tree": accepted_tree} - prior = _build_accepted_task_result( - task, - binding, - _handoff(), - _validated(), - accepted_at="2026-09-08T01:00:00Z", - ) - binding["accepted_result"] = prior - previous_identity = { - "artifact_id": "task-001" if predecessor_kind == "task" else "plan-001", - "revision": accepted_head, - "sha256": "1" * 64, - "source_tree": accepted_tree, - } - source.write_text("VALUE = 2\n") - _git(tmp_path, "add", "source.py") - _git(tmp_path, "commit", "-qm", "repair reviewed endpoint") - reviewed_head = _git(tmp_path, "rev-parse", "HEAD") - reviewed_tree = _git(tmp_path, "rev-parse", "HEAD^{tree}") - repaired_identity = { - "artifact_id": "task-001", - "revision": reviewed_head, - "sha256": "2" * 64, - "source_tree": reviewed_tree, - } - reviewer = { - "agent_id": "reviewer-001", - "capability": "judgment", - "authorship": "none", - "repair_participation": "none", - "decision_participation": "none", - "deliberation_participation": "none", - "context_origin": "direct_source", - } - evidence = { - "mode": "direct", - "capabilities": ["bounded source inspection"], - "unavailable_evidence": [], - "commands": [], - "artifacts": [], - } - previous_review: dict[str, object] = { - "verdict": "repair", - "review_id": "review-finding-001", - "reviewed_head": accepted_head, - "review_mode": "initial", - "review_target_kind": "task", - "repair_frontier": None, - "review_reset": None, - "target_identity": previous_identity, - "reviewer": reviewer, - "evidence": evidence, - "findings": [{ - "finding_id": "FINDING-001", - "stage": "implementation", - "class": "implementation_defect", - "severity": "blocking", - "first_broken_artifact": "implementation", - "obligation_basis": "accepted_requirement", - "evidence": [{ - "kind": "test", - "locator": "tests/test_orchestration_accepted_result.py", - "digest_or_identity": "red-001", - "observation": "standalone repair review could not be materialized", - }], - "target_identity": previous_identity, - "summary": "Repair acceptance was coupled to executor redispatch.", - "recommended_owner": "task_owner", - "disposition": "repair_task", - }], - "started_at": "2026-09-08T01:01:00Z", - "completed_at": "2026-09-08T01:02:00Z", - "staleness": {"is_stale": False, "reason": None, "supersedes": None}, - } - if predecessor_kind == "task": - previous_review.update( - required=True, - reviewer_independent=True, - reviewed_head=accepted_head, - ) - else: - previous_review.pop("reviewed_head") - previous_review["review_target_kind"] = "stage" - previous_review["stage"] = "integrated_implementation" - repair_review = { - **previous_review, - "required": True, - "reviewer_independent": True, - "review_target_kind": "task", - "verdict": "accept", - "review_id": "review-repair-001", - "reviewed_head": reviewed_head, - "review_mode": "repair", - "target_identity": repaired_identity, - "findings": [], - "previous_review": previous_review, - "repair_frontier": { - "prior_review_id": "review-finding-001", - "blocking_finding_ids": ["FINDING-001"], - "previous_reviewed_identity": previous_identity, - "repaired_identity": repaired_identity, - "affected_boundaries": ["scripts/orchestration/execution_context.py"], - "frozen_evidence_reference": review_runtime.review_evidence_identity(previous_review), - }, - "started_at": "2026-09-08T01:03:00Z", - "completed_at": "2026-09-08T01:04:00Z", - } - (tmp_path / ".git/info/exclude").write_text(".work-bundle/\n", encoding="utf-8") - observation_id = _record_validation_observation( - tmp_path, binding, task, live_initial_acceptance=True - ) - (tmp_path / "unrelated.py").write_text("UNCHANGED_FRONTIER = True\n") - _git(tmp_path, "add", "unrelated.py") - _git(tmp_path, "commit", "-qm", "later unrelated lifecycle progress") - persisted: dict[str, object] = {} - monkeypatch.setattr(execution_context, "load_task_execution_binding", lambda *_: binding) - monkeypatch.setattr(execution_context, "_persist_binding", lambda value, _root: persisted.update(value)) - monkeypatch.setattr( - execution_context, - "build_accepted_task_result", - lambda *_args, **_kwargs: pytest.fail("executor handoff must not be replayed"), - ) - - monkeypatch.setattr(review_runtime, "_validate_reviewer_run", lambda *_: None) - review_reference = review_runtime.publish_review( - tmp_path, repair_review, current_target_identity=repaired_identity - ) - repaired = execution_context.materialize_accepted_task_repair_review( - tmp_path, - task, - review_reference, - accepted_at="2026-09-08T01:05:00Z", - validation_evidence_ids=[observation_id], - ) - - for field in ( - "baseline_identity", - "executor_result_digest", - "owner_identity", - "knowledge_disposition", - ): - assert repaired[field] == prior[field] - assert repaired["accepted_source"]["head"] == reviewed_head - assert repaired["accepted_source"]["tree"] == reviewed_tree - assert repaired["review_id"] == "review-repair-001" - assert repaired["validation_evidence_ids"] == [observation_id] - assert repaired["authority_projection"]["required_review_digest"] == execution_context.semantic_digest( - execution_context._accepted_review_projection(repair_review) - ) - assert persisted["accepted_result"] == repaired - assert "previous_review" not in repr(repaired) - - divergent_head = _git(tmp_path, "commit-tree", accepted_tree, "-m", "divergent endpoint") - cases = [ - (reviewed_head, accepted_tree, "revision/tree identity is mismatched"), - ("f" * 40, accepted_tree, "target revision does not resolve"), - (divergent_head, accepted_tree, "not an ancestor"), - ] - for index, (target_head, target_tree, message) in enumerate(cases): - invalid = deepcopy(repair_review) - invalid["review_id"] = f"review-invalid-target-{index}" - invalid_identity = { - **invalid["target_identity"], - "revision": target_head, - "source_tree": target_tree, - } - invalid["reviewed_head"] = target_head - invalid["target_identity"] = invalid_identity - invalid["repair_frontier"]["repaired_identity"] = invalid_identity - invalid_reference = review_runtime.publish_review( - tmp_path, invalid, current_target_identity=invalid_identity - ) - with pytest.raises(SystemExit, match=message): - execution_context.materialize_accepted_task_repair_review( - tmp_path, task, invalid_reference, accepted_at="2026-09-08T01:05:00Z" - ) - if predecessor_kind == "integrated_stage": - wrong_plan = deepcopy(repair_review) - wrong_plan["review_id"] = "review-wrong-predecessor-plan" - wrong_identity = { - **wrong_plan["previous_review"]["target_identity"], - "artifact_id": "plan-other", - } - wrong_plan["previous_review"]["target_identity"] = wrong_identity - wrong_plan["previous_review"]["findings"][0]["target_identity"] = wrong_identity - wrong_plan["repair_frontier"]["previous_reviewed_identity"] = wrong_identity - wrong_plan_reference = review_runtime.publish_review( - tmp_path, wrong_plan, current_target_identity=repaired_identity - ) - with pytest.raises(SystemExit, match="exact current task and predecessor owner"): - execution_context.materialize_accepted_task_repair_review( - tmp_path, task, wrong_plan_reference - ) - - wrong_stage = deepcopy(repair_review) - wrong_stage["review_id"] = "review-wrong-predecessor-stage" - wrong_stage["previous_review"]["stage"] = "plan" - with pytest.raises(review_runtime.ReviewContractError, match="stage predecessor must be integrated_implementation"): - review_runtime.publish_review(tmp_path, wrong_stage, current_target_identity=repaired_identity) - - -def test_standalone_review_recomposes_changed_task_authority_without_executor_replay( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - old_task = _task(tmp_path) - binding = _binding(tmp_path) - monkeypatch.setattr( - execution_context, - "capture_repository_evidence", - lambda _root: {"head": OID_A, "tree": OID_B, "status": "clean", "entries": {}}, - ) - prior = _build_accepted_task_result( - old_task, binding, _handoff(), _validated(), accepted_at="2026-09-08T02:00:00Z" - ) - binding["accepted_result"] = prior - current_task = deepcopy(old_task) - current_task["files"]["write"] = ["src/a.py", "src/b.py"] - current_task["validation"][0]["command"] = "pytest -q tests/current" - previous_identity = { - "artifact_id": "task-001", "revision": OID_A, - "sha256": "1" * 64, "source_tree": OID_B, - } - current_identity = { - "artifact_id": "task-001", "revision": OID_C, - "sha256": "2" * 64, "source_tree": OID_D, - } - review = { - "required": True, - "reviewer_independent": True, - "review_id": "review-current-authority", - "reviewed_head": OID_C, - "review_mode": "initial", - "review_target_kind": "task", - "repair_frontier": None, - "review_reset": { - "prior_review_id": "review-001", - "reason_class": "scope", - "reason": "Current task scope authority changed.", - }, - "target_identity": current_identity, - "reviewer": {"agent_id": "reviewer-current"}, - "verdict": "accept", - "previous_review": { - "review_id": "review-001", - "review_target_kind": "task", - "target_identity": previous_identity, - }, - } - validated_review = SimpleNamespace( - review_id="review-current-authority", - review_mode="initial", - verdict="accepted", - target_identity=current_identity, - repair_frontier=None, - review_reset=review["review_reset"], - reviewer={"agent_id": "reviewer-current"}, - ) - monkeypatch.setattr(execution_context, "load_task_execution_binding", lambda *_: binding) - monkeypatch.setattr( - review_runtime, - "load_stored_review", - lambda _root, _reference, **_kwargs: (review, validated_review), - ) - monkeypatch.setattr( - review_runtime, - "stored_review_target_identity", - lambda _root, _reference: current_identity, - ) - monkeypatch.setattr( - execution_context, - "capture_repository_evidence", - lambda _root: {"head": OID_C, "tree": OID_D, "status": "clean", "entries": {}}, - ) - - def git_result(arguments, **_kwargs): - if "merge-base" in arguments: - return SimpleNamespace(returncode=0, stdout="", stderr="") - value = OID_D if str(arguments[-1]).endswith("^{tree}") else OID_C - return SimpleNamespace(returncode=0, stdout=value + "\n", stderr="") - - monkeypatch.setattr(execution_context.subprocess, "run", git_result) - persisted: dict[str, object] = {} - monkeypatch.setattr(execution_context, "_persist_binding", lambda value, _root: persisted.update(value)) - monkeypatch.setattr( - execution_context, - "build_accepted_task_result", - lambda *_args, **_kwargs: pytest.fail("executor result replayed"), - ) - monkeypatch.setattr( - execution_context, - "_claim_bound_validation_observations", - lambda *_args, **_kwargs: [{"observation_id": "obs-current"}], - ) - - accepted = execution_context.materialize_accepted_task_review( - tmp_path, - current_task, - {"review_id": "review-current-authority", "sha256": "9" * 64}, - { - "causal_class": "claim_relevant_drift", - "affected_task": "task-001", - "authorized_lifecycle_action": "rematerialize_accepted_result", - }, - validation_evidence_ids=["obs-current"], - ) - - for field in ( - "baseline_identity", "executor_result_digest", - "owner_identity", "knowledge_disposition", - ): - assert accepted[field] == prior[field] - assert accepted["validation_evidence_ids"] == ["obs-current"] - assert accepted["accepted_source"]["head"] == OID_C - assert accepted["accepted_source"]["tree"] == OID_D - assert accepted["authority_projection"] == execution_context._accepted_authority_projection( - current_task, binding, accepted_review=review, owner_identity=prior["owner_identity"] - ) - assert persisted["accepted_result"] == accepted - assert "previous_review" not in repr(accepted) - assert "causal_class" not in repr(accepted) - - -def test_legacy_accepted_result_without_disposition_remains_current_for_nonknowledge_consumers( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - task = _task(tmp_path) - binding = _binding(tmp_path) - monkeypatch.setattr( - execution_context, - "capture_repository_evidence", - lambda _root: {"head": OID_A, "tree": OID_B, "entries": {}, "status": "clean"}, - ) - accepted = _build_accepted_task_result( - task, binding, _handoff(), _validated(), accepted_at="2026-09-06T10:00:00Z" - ) - legacy = deepcopy(accepted) - legacy.pop("knowledge_disposition") - legacy["accepted_source"]["state_digest"] = execution_context._accepted_source_state_digest( - plan_id=legacy["plan_id"], - task_id=legacy["task_id"], - binding_id=legacy["binding_id"], - baseline_identity=legacy["baseline_identity"], - head=legacy["accepted_source"]["head"], - tree=legacy["accepted_source"]["tree"], - authority_projection=legacy["authority_projection"], - ) - - execution_context.assert_accepted_task_result_current(task, binding, legacy) - - -def test_actual_accepted_repair_review_mode_and_frontier_are_digest_authority( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - monkeypatch.setattr( - execution_context, - "capture_repository_evidence", - lambda _root: {"head": OID_A, "tree": OID_B, "entries": {}, "status": "dirty"}, - ) - task = _task(tmp_path) - binding = _binding(tmp_path) - initial = _build_accepted_task_result( - task, binding, _handoff(), _validated(), accepted_at="2026-09-06T10:00:00Z" - ) - repaired_handoff = deepcopy(_handoff()) - repaired_handoff["acceptance_review"].update( - { - "review_mode": "repair", - "repair_frontier": { - "prior_review_id": "review-prior", - "frozen_evidence_reference": "evidence-001", - }, - } - ) - repaired = _build_accepted_task_result( - task, binding, repaired_handoff, _validated(), accepted_at="2026-09-06T10:00:00Z" - ) - - assert ( - initial["authority_projection"]["required_review_digest"] - != repaired["authority_projection"]["required_review_digest"] - ) - assert repaired["authority_projection"]["required_review_digest"] == execution_context.semantic_digest( - { - "required": True, - "review_id": "review-001", - "verdict": "accepted", - "review_mode": "repair", - "repair_frontier": repaired_handoff["acceptance_review"]["repair_frontier"], - } - ) - execution_context.assert_accepted_task_result_current(task, binding, repaired) - - -def test_unrelated_repository_advance_does_not_stale_accepted_task_result( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - repository = {"head": OID_A, "tree": OID_B, "entries": {}, "status": "dirty"} - monkeypatch.setattr(execution_context, "capture_repository_evidence", lambda _root: repository) - task = _task(tmp_path) - binding = _binding(tmp_path) - accepted = _build_accepted_task_result( - task, binding, _handoff(), _validated(), accepted_at="2026-09-06T10:00:00Z" - ) - - repository = {"head": OID_C, "tree": OID_D, "entries": {}, "status": "clean"} - execution_context.assert_accepted_task_result_current(task, binding, accepted) - - -def test_dependency_topology_change_invalidates_accepted_task_result( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - monkeypatch.setattr( - execution_context, - "capture_repository_evidence", - lambda _root: {"head": OID_A, "tree": OID_B, "entries": {}, "status": "dirty"}, - ) - task = _task(tmp_path) - binding = _binding(tmp_path) - accepted = _build_accepted_task_result( - task, binding, _handoff(), _validated(), accepted_at="2026-09-06T10:00:00Z" - ) - - changed = deepcopy(task) - changed["depends_on"] = ["task-other"] - with pytest.raises(SystemExit, match="task"): - execution_context.assert_accepted_task_result_current(changed, binding, accepted) - - -def test_materialize_persists_one_result_in_existing_binding( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - task = _task(tmp_path) - binding = _binding(tmp_path) - persisted: list[dict[str, object]] = [] - monkeypatch.setattr(execution_context, "load_task_execution_binding", lambda *_args: deepcopy(binding)) - monkeypatch.setattr(execution_context, "_persist_binding", lambda value, _root: persisted.append(value)) - monkeypatch.setattr( - execution_context, - "capture_repository_evidence", - lambda _root: {"head": OID_A, "tree": OID_B, "entries": {}, "status": "dirty"}, - ) - - accepted = execution_context.materialize_accepted_task_result( - tmp_path, - task, - _handoff(), - _validated(), - accepted_review=_handoff()["acceptance_review"], - accepted_at="2026-09-06T10:00:00Z", - ) - - assert len(persisted) == 1 - assert persisted[0]["accepted_result"] == accepted - assert persisted[0]["ownership"] == binding["ownership"] diff --git a/tests/test_orchestration_accepted_result_lifecycle.py b/tests/test_orchestration_accepted_result_lifecycle.py deleted file mode 100644 index 81e9768..0000000 --- a/tests/test_orchestration_accepted_result_lifecycle.py +++ /dev/null @@ -1,822 +0,0 @@ -from __future__ import annotations - -import argparse -from copy import deepcopy -import importlib.util -import json -from pathlib import Path -import sys -from types import SimpleNamespace - -import pytest - - -REPO_ROOT = Path(__file__).resolve().parents[1] -ORCHESTRATION = REPO_ROOT / "scripts" / "orchestration" -sys.path.insert(0, str(ORCHESTRATION)) - -import execution_context # noqa: E402 -import plans # noqa: E402 -import repository_preflight # noqa: E402 -from test_orchestration_accepted_result import ( # noqa: E402 - _binding, - _build_accepted_task_result, - _handoff, - _task, - _validated, -) - - -def _dispatcher(): - previous_core = sys.modules.get("core") - core_spec = importlib.util.spec_from_file_location("core", ORCHESTRATION / "core.py") - assert core_spec is not None and core_spec.loader is not None - core_module = importlib.util.module_from_spec(core_spec) - sys.modules["core"] = core_module - try: - core_spec.loader.exec_module(core_module) - spec = importlib.util.spec_from_file_location( - "orchestration_accepted_result_dispatcher", ORCHESTRATION / "dispatcher.py" - ) - assert spec is not None and spec.loader is not None - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - finally: - if previous_core is None: - sys.modules.pop("core", None) - else: - sys.modules["core"] = previous_core - - -def test_current_accepted_result_does_not_read_handoff_or_replay_validation( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - task = _task(tmp_path) - binding = _binding(tmp_path) - monkeypatch.setattr( - execution_context, - "capture_repository_evidence", - lambda _root: {"head": "a" * 40, "tree": "b" * 40, "entries": {}, "status": "clean"}, - ) - accepted = _build_accepted_task_result( - task, binding, _handoff(), _validated(), accepted_at="2026-09-07T00:00:00Z" - ) - binding["accepted_result"] = accepted - monkeypatch.setattr(execution_context, "load_task_execution_binding", lambda *_args: binding) - - current_binding, current = execution_context.load_current_accepted_task_result( - tmp_path, task - ) - - assert current_binding is binding - assert current == accepted - - -def test_task_completion_reuses_current_accepted_result_without_handoff( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - task_path = tmp_path / "task.md" - task_path.write_text("---\nid: task-001\nplan_id: plan-001\n---\n", encoding="utf-8") - accepted = {"schema": "accepted-task-result-v1"} - calls: list[str] = [] - monkeypatch.setattr(plans, "_load_current_task_acceptance", lambda *_args: ({}, accepted)) - monkeypatch.setattr( - plans, - "cmd_validate_executor_result", - lambda _args: calls.append("replayed"), - ) - - assert plans._assert_completed_task_authority(argparse.Namespace(), task_path) == accepted - assert calls == [] - - -def test_task_completion_does_not_replay_when_persisted_authority_is_stale( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - task_path = tmp_path / "task.md" - task_path.write_text("---\nid: task-001\nplan_id: plan-001\n---\n", encoding="utf-8") - calls: list[str] = [] - monkeypatch.setattr( - plans, - "_load_current_task_acceptance", - lambda *_args: (_ for _ in ()).throw( - SystemExit("accepted task result is stale: scope authority changed") - ), - ) - monkeypatch.setattr( - plans, "cmd_validate_executor_result", lambda _args: calls.append("replayed") - ) - - with pytest.raises(SystemExit, match="stale: scope"): - plans._assert_completed_task_authority( - argparse.Namespace(handoff="historical.yaml"), task_path - ) - assert calls == [] - - -def test_dependency_and_phase_gates_consume_only_current_accepted_results( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - task_path = tmp_path / "task.md" - task_path.write_text( - "---\nid: task-002\nplan_id: plan-001\nphase_id: phase-001\n" - "depends_on: [task-001]\n---\n", - encoding="utf-8", - ) - rows = [ - { - "type": "task", - "id": "task-001", - "plan_id": "plan-001", - "phase_id": "phase-001", - "status": "Completed", - "path": "dependency.md", - }, - { - "type": "task", - "id": "task-002", - "plan_id": "plan-001", - "phase_id": "phase-001", - "status": "Completed", - "path": "task.md", - }, - ] - loaded: list[str] = [] - monkeypatch.setattr(plans, "index_plans", lambda _args: rows) - monkeypatch.setattr( - plans, - "_task_brief_at", - lambda _args, path: { - "plan_id": "plan-001", - "task_id": "task-002" if path.name == "task.md" else "task-001", - "depends_on": ["task-001"] if path.name == "task.md" else [], - }, - ) - monkeypatch.setattr( - plans, - "artifact_path_from_row", - lambda row, _args: tmp_path / str(row["path"]), - ) - monkeypatch.setattr( - plans, - "_load_current_task_acceptance", - lambda _args, path: (loaded.append(path.name), ({}, {"schema": "accepted-task-result-v1"}))[1], - ) - - plans._assert_task_dependencies_current(argparse.Namespace(), task_path) - plans._assert_phase_tasks_accepted( - argparse.Namespace(), "phase-001", "plan-001" - ) - - assert loaded == ["dependency.md", "dependency.md", "task.md"] - - -def test_archive_switches_irreversibly_to_accepted_results_without_handoff_replay( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - plan_root = tmp_path / ".work-bundle/orchestration/plan/active" - task_root = plan_root / "plan" / "phase-001" - task_root.mkdir(parents=True) - (plan_root / "plan.md").write_text( - "---\nid: plan-001\nstatus: Completed\n---\n", encoding="utf-8" - ) - (task_root / "task.md").write_text( - "---\nid: task-001\nplan_id: plan-001\nphase_id: phase-001\n" - "status: Completed\n---\n", - encoding="utf-8", - ) - binding_path = ( - tmp_path - / ".work-bundle/runtime/execution/plan-001/task-001/execution-binding.json" - ) - binding_path.parent.mkdir(parents=True) - binding_path.write_text( - json.dumps({"accepted_result": {"schema": "accepted-task-result-v1"}}), - encoding="utf-8", - ) - accepted = [ - ( - { - "schema": "accepted-task-result-v1", - "task_id": "task-001", - "knowledge_disposition": { - "action": "none", - "reason": "No durable authority changed.", - "affected_authority": [], - }, - }, - {"task_id": "task-001", "review_required": False}, - ) - ] - calls: list[object] = [] - monkeypatch.setattr( - plans, "require_plan_reviews", lambda *_args, **_kwargs: calls.append("review") - ) - monkeypatch.setattr( - plans, - "_accepted_plan_task_results", - lambda *_args: calls.append("accepted") or accepted, - ) - monkeypatch.setattr( - plans, - "_validated_plan_task_handoffs", - lambda *_args: (_ for _ in ()).throw(AssertionError("handoff replayed")), - ) - monkeypatch.setattr( - plans, "_assert_archive_knowledge_gate", lambda *_args: calls.append(_args[-1]) - ) - monkeypatch.setattr( - plans, "_assert_archive_plan_acceptance", lambda *_args: calls.append(_args[-1]) - ) - - plans.cmd_archive_plan(argparse.Namespace(project_root=str(tmp_path), id="plan-001")) - - assert calls == ["review", "accepted", accepted, accepted] - assert (tmp_path / ".work-bundle/orchestration/plan/archived/plan.md").is_file() - - -def test_archive_consumes_bound_validation_observation_without_rerun( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - command = "pytest -q tests/test_claim.py" - item = { - "id": "VAL-001", - "kind": "process", - "command": command, - "expected": "passed", - "invariant_ids": ["INV-001"], - } - definition = { - key: item.get(key) - for key in ( - "id", "kind", "command", "mechanism", "expected", - "acceptable_results", "invariant_ids", "digest", "proves", - ) - } - accepted = { - "schema": "accepted-task-result-v1", - "plan_id": "plan-001", - "task_id": "task-001", - "validation_evidence_ids": ["observation-001"], - "accepted_source": {"tree": "b" * 40}, - } - record = { - "observation_id": "observation-001", - "product_tree": "b" * 40, - "command_digest": execution_context.semantic_digest(definition), - "result": {"exit_code": 0}, - } - monkeypatch.setattr( - plans, "load_observation", lambda *_args: SimpleNamespace(to_dict=lambda: record) - ) - observed = plans._observe_archive_obligations( - tmp_path, - command, - tmp_path, - [(accepted, {"plan_id": "plan-001", "task_id": "task-001", "validation": [item]})], - ) - - assert observed == [{"id": "VAL-001", "observation_id": "observation-001", "result": "passed"}] - - -def _write_multi_repository_workspace(root: Path, members: dict[str, Path]) -> None: - metadata = root / ".work-bundle/project.yaml" - metadata.parent.mkdir(parents=True, exist_ok=True) - lines = [ - "metadata_version: 3", - f"workspace_root: {root}", - "workspace_mode: multi-repository", - "source_repositories:", - ] - for repository_id, project_root in members.items(): - project_root.mkdir(parents=True, exist_ok=True) - lines.extend( - [ - f" - id: {repository_id}", - f" project_root: {project_root}", - f" origin: /origin/{repository_id}.git", - ] - ) - metadata.write_text("\n".join(lines) + "\n", encoding="utf-8") - - -def test_final_plan_workspace_uses_unanimous_accepted_repository_authority( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - members = { - "work-bundle-main": tmp_path / "work-bundle-main", - "execution-flow": tmp_path / "execution-flow", - "work-bundle-mcp": tmp_path / "work-bundle-mcp", - } - _write_multi_repository_workspace(tmp_path, members) - rows = [ - { - "type": "task", - "id": f"task-00{number}", - "plan_id": "plan-001", - "status": "Completed", - "path": f"task-00{number}.md", - } - for number in range(1, 4) - ] - monkeypatch.setattr(plans, "index_plans", lambda _args: rows) - monkeypatch.setattr(plans, "has_persisted_accepted_task_result", lambda *_args: True) - monkeypatch.setattr( - plans, - "artifact_path_from_row", - lambda row, _args: tmp_path / str(row["path"]), - ) - monkeypatch.setattr( - plans, - "_load_current_task_acceptance", - lambda _args, path: ( - { - "workspace_id": "workspace-001", - "execution_id": f"exec-{path.stem}", - "repository_id": "work-bundle-main", - "runtime_root": str(tmp_path / "runtime"), - }, - {"schema": "accepted-task-result-v1"}, - ), - ) - - selected = plans._resolve_final_plan_workspace( - argparse.Namespace( - project_root=str(tmp_path), - workspace_id="workspace-001", - execution_id="exec-task-003", - repository_id=None, - execution_runtime_root=str(tmp_path / "runtime"), - ), - "plan-001", - ) - - assert selected == members["work-bundle-main"].resolve() - assert selected != Path("/origin/work-bundle-main.git") - - -@pytest.mark.parametrize( - ("repository_ids", "selector", "message"), - [ - ( - ["work-bundle-main", "execution-flow"], - None, - "accepted task repository authority disagrees", - ), - ( - ["work-bundle-main", "work-bundle-main"], - "execution-flow", - "repository selector conflicts with accepted task authority", - ), - ( - ["missing-member", "missing-member"], - None, - "authorized final plan repository is not a registered local member", - ), - ], -) -def test_final_plan_workspace_rejects_conflict_or_missing_member( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - repository_ids: list[str], - selector: str | None, - message: str, -) -> None: - members = { - "work-bundle-main": tmp_path / "work-bundle-main", - "execution-flow": tmp_path / "execution-flow", - "work-bundle-mcp": tmp_path / "work-bundle-mcp", - } - _write_multi_repository_workspace(tmp_path, members) - rows = [ - { - "type": "task", - "id": f"task-00{number}", - "plan_id": "plan-001", - "status": "Completed", - "path": f"task-00{number}.md", - } - for number in range(1, 3) - ] - monkeypatch.setattr(plans, "index_plans", lambda _args: rows) - monkeypatch.setattr(plans, "has_persisted_accepted_task_result", lambda *_args: True) - monkeypatch.setattr( - plans, - "artifact_path_from_row", - lambda row, _args: tmp_path / str(row["path"]), - ) - monkeypatch.setattr( - plans, - "_load_current_task_acceptance", - lambda _args, path: ( - { - "workspace_id": "workspace-001", - "execution_id": f"exec-{path.stem}", - "repository_id": repository_ids[int(path.stem[-1]) - 1], - "runtime_root": str(tmp_path / "runtime"), - }, - {"schema": "accepted-task-result-v1"}, - ), - ) - - with pytest.raises(SystemExit, match=message): - plans._resolve_final_plan_workspace( - argparse.Namespace(project_root=str(tmp_path), repository_id=selector), - "plan-001", - ) - - -def test_final_plan_workspace_rejects_selectors_split_across_accepted_bindings( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - members = { - "work-bundle-main": tmp_path / "work-bundle-main", - "execution-flow": tmp_path / "execution-flow", - "work-bundle-mcp": tmp_path / "work-bundle-mcp", - } - _write_multi_repository_workspace(tmp_path, members) - rows = [ - { - "type": "task", - "id": f"task-00{number}", - "plan_id": "plan-001", - "status": "Completed", - "path": f"task-00{number}.md", - } - for number in range(1, 3) - ] - monkeypatch.setattr(plans, "index_plans", lambda _args: rows) - monkeypatch.setattr(plans, "has_persisted_accepted_task_result", lambda *_args: True) - monkeypatch.setattr( - plans, - "artifact_path_from_row", - lambda row, _args: tmp_path / str(row["path"]), - ) - monkeypatch.setattr( - plans, - "_load_current_task_acceptance", - lambda _args, path: ( - { - "workspace_id": "workspace-A" if path.stem.endswith("1") else "workspace-B", - "execution_id": "execution-A" if path.stem.endswith("1") else "execution-B", - "repository_id": "work-bundle-main", - "runtime_root": str( - tmp_path / ("runtime-A" if path.stem.endswith("1") else "runtime-B") - ), - }, - {"schema": "accepted-task-result-v1"}, - ), - ) - - with pytest.raises(SystemExit, match="selector tuple conflicts"): - plans._resolve_final_plan_workspace( - argparse.Namespace( - project_root=str(tmp_path), - workspace_id="workspace-A", - execution_id="execution-B", - repository_id=None, - execution_runtime_root=str(tmp_path / "runtime-A"), - ), - "plan-001", - ) - - -def test_registered_repository_roots_rejects_duplicate_identity_at_same_root( - tmp_path: Path, -) -> None: - shared = tmp_path / "work-bundle-main" - members = { - "work-bundle-main": shared, - "execution-flow": tmp_path / "execution-flow", - } - _write_multi_repository_workspace(tmp_path, members) - metadata = tmp_path / ".work-bundle/project.yaml" - metadata.write_text( - metadata.read_text(encoding="utf-8") - + " - id: work-bundle-main\n" - + f" project_root: {shared}\n", - encoding="utf-8", - ) - - with pytest.raises(SystemExit, match="registered repository identity is duplicated"): - plans._registered_repository_roots(tmp_path) - - -def test_final_plan_workspace_resolves_indentless_v4_registered_member( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, -) -> None: - members = { - "work-bundle-main": tmp_path / "work-bundle-main", - "execution-flow": tmp_path / "execution-flow", - } - for member in members.values(): - member.mkdir() - metadata = tmp_path / ".work-bundle/project.yaml" - metadata.parent.mkdir() - metadata.write_text( - "metadata_version: 4\n" - "workspace:\n" - " id: workspace-v4\n" - " mode: multi-repository\n" - "source_repositories:\n" - "- id: work-bundle-main\n" - " role: source\n" - " default_branch: main\n" - "- id: execution-flow\n" - " role: source\n" - " default_branch: main\n", - encoding="utf-8", - ) - registry = tmp_path / "projects.yaml" - registry.write_text( - "device_bindings:\n" - " workspace-v4:\n" - " repositories:\n" - " work-bundle-main:\n" - f" project_root: {members['work-bundle-main']}\n" - " execution-flow:\n" - f" project_root: {members['execution-flow']}\n", - encoding="utf-8", - ) - monkeypatch.setattr(repository_preflight, "project_registry_path", lambda: registry) - monkeypatch.setattr(plans, "_member_roots", lambda _workspace: list(members.values())) - - selected = plans._resolve_final_plan_workspace( - argparse.Namespace(project_root=str(tmp_path), repository_id="work-bundle-main"), - ) - - assert selected == members["work-bundle-main"].resolve() - - -def test_archive_knowledge_gate_aggregates_new_results_and_bounds_legacy_bridge( - tmp_path: Path, -) -> None: - plan = tmp_path / "plan.md" - args = argparse.Namespace() - update = { - "schema": "accepted-task-result-v1", - "task_id": "task-001", - "knowledge_disposition": { - "action": "update", - "reason": "Stable authority changed.", - "affected_authority": ["REQ-001"], - }, - } - brief = {"task_id": "task-001", "review_required": True} - plan.write_text( - "---\nid: plan-001\n---\n\n## 2.1 Knowledge Base Update Carry Forward\n\n" - "- **Disposition**: not-needed\n- **Closure return**: missing\n", - encoding="utf-8", - ) - with pytest.raises(SystemExit, match="task-001:update"): - plans._assert_archive_knowledge_gate(args, "plan-001", plan, [(update, brief)]) - - plan.write_text( - plan.read_text(encoding="utf-8").replace( - "Closure return**: missing", "Closure return**: completed" - ), - encoding="utf-8", - ) - plans._assert_archive_knowledge_gate(args, "plan-001", plan, [(update, brief)]) - - legacy = deepcopy(update) - legacy.pop("knowledge_disposition") - with pytest.raises(SystemExit, match="legacy accepted results require plan-level required/completed"): - plans._assert_archive_knowledge_gate(args, "plan-001", plan, [(legacy, brief)]) - plan.write_text( - plan.read_text(encoding="utf-8").replace( - "Disposition**: not-needed", "Disposition**: required" - ), - encoding="utf-8", - ) - plans._assert_archive_knowledge_gate(args, "plan-001", plan, [(legacy, brief)]) - - -@pytest.mark.parametrize( - "section", - [ - "## 2.1 Knowledge Base Update Carry Forward\n\n" - "- **Disposition**: required\n- **Closure return**: completed\n", - "## Knowledge Base Update Carry Forward\n\n" - "- Disposition: required\n- Closure return: completed\n", - ], -) -def test_plan_knowledge_fields_accept_settled_numbered_and_plain_syntax(section: str) -> None: - assert plans._plan_knowledge_field(section, "Disposition") == "required" - assert plans._plan_knowledge_field(section, "Closure return") == "completed" - - -def test_archive_knowledge_gate_consumes_plain_completed_closure(tmp_path: Path) -> None: - plan = tmp_path / "plan.md" - plan.write_text( - "---\nid: plan-001\n---\n\n## Knowledge Base Update Carry Forward\n\n" - "- Disposition: required\n- Closure return: completed\n", - encoding="utf-8", - ) - legacy = {"schema": "accepted-task-result-v1", "task_id": "task-001"} - - plans._assert_archive_knowledge_gate( - argparse.Namespace(), - "plan-001", - plan, - [(legacy, {"task_id": "task-001", "review_required": True})], - ) - - -def _write_mixed_layout_plan(root: Path) -> tuple[Path, Path, Path, Path, Path]: - active = root / ".work-bundle/orchestration/plan/active" - plan = active / "plan-direct.md" - plan_dir = active / "plan-direct" - phase_direct = plan_dir / "phase-001.md" - task_direct = plan_dir / "task-001.md" - phase_nested = plan_dir / "phase-002.md" - task_nested = plan_dir / "phase-002/task-002.md" - task_nested.parent.mkdir(parents=True) - plan.write_text( - '---\nid: "plan-direct"\nstatus: "Completed"\n---\n', encoding="utf-8" - ) - phase_direct.write_text( - "---\nid: 'phase-001'\nplan_id: \"plan-direct\"\nstatus: 'Completed'\n---\n", - encoding="utf-8", - ) - phase_nested.write_text( - "---\nid: phase-002\nplan_id: plan-direct\nstatus: Completed\n---\n", - encoding="utf-8", - ) - task_direct.write_text( - '---\nid: "task-001"\nplan_id: "plan-direct"\nphase_id: "phase-001"\n' - 'status: "Planned"\ndepends_on: []\n---\n', - encoding="utf-8", - ) - task_nested.write_text( - "---\nid: 'task-002'\nplan_id: 'plan-direct'\nphase_id: 'phase-002'\n" - "status: 'Completed'\ndepends_on: []\n---\n", - encoding="utf-8", - ) - return plan, phase_direct, task_direct, phase_nested, task_nested - - -def test_plan_index_and_status_support_direct_and_nested_task_layouts( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - _plan, _phase_direct, task_direct, _phase_nested, task_nested = _write_mixed_layout_plan( - tmp_path - ) - args = argparse.Namespace(project_root=str(tmp_path)) - - rows = plans.index_plans(args) - tasks = [row for row in rows if row["type"] == "task"] - - assert sorted((row["id"], row["plan_id"], row["phase_id"]) for row in tasks) == [ - ("task-001", "plan-direct", "phase-001"), - ("task-002", "plan-direct", "phase-002"), - ] - assert {Path(str(row["path"])).name for row in tasks} == { - task_direct.name, - task_nested.name, - } - monkeypatch.setattr(plans, "_assert_task_dependencies_current", lambda *_args: None) - monkeypatch.setattr(plans, "_assert_completed_task_authority", lambda *_args: {}) - monkeypatch.setattr(plans, "_release_completed_task_binding", lambda *_args: {}) - plans.cmd_set_plan_status( - argparse.Namespace( - project_root=str(tmp_path), - id="task-001", - plan_id="plan-direct", - kind="task", - status="Completed", - ) - ) - assert "status: Completed" in task_direct.read_text(encoding="utf-8") - - -@pytest.mark.parametrize( - "identity", - [ - "plan_id: other-plan\nphase_id: phase-001\n", - "plan_id: plan-direct\n", - ], -) -def test_direct_task_index_rejects_ambiguous_identity(tmp_path: Path, identity: str) -> None: - task = tmp_path / ".work-bundle/orchestration/plan/active/plan-direct/task-001.md" - task.parent.mkdir(parents=True) - task.write_text(f"---\nid: task-001\n{identity}---\n", encoding="utf-8") - - with pytest.raises(SystemExit, match="Invalid direct task identity"): - plans.index_plans(argparse.Namespace(project_root=str(tmp_path))) - - -def test_phase_and_archive_consumers_include_direct_tasks( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - _plan, _phase_direct, task_direct, _phase_nested, _task_nested = _write_mixed_layout_plan( - tmp_path - ) - args = argparse.Namespace(project_root=str(tmp_path)) - with pytest.raises(SystemExit, match="task task-001 is not completed"): - plans._assert_phase_tasks_accepted(args, "phase-001", "plan-direct") - - task_direct.write_text( - task_direct.read_text(encoding="utf-8").replace( - 'status: "Planned"', 'status: "Completed"' - ), - encoding="utf-8", - ) - accepted: list[str] = [] - monkeypatch.setattr( - plans, - "_load_current_task_acceptance", - lambda _args, path: accepted.append(path.name) or ({}, {}), - ) - plans._assert_phase_tasks_accepted(args, "phase-001", "plan-direct") - assert accepted == ["task-001.md"] - - accepted.clear() - monkeypatch.setattr( - plans, - "_task_brief_at", - lambda _args, path: {"task_id": path.stem}, - ) - results = plans._accepted_plan_task_results(args, "plan-direct") - assert {brief["task_id"] for _result, brief in results} == {"task-001", "task-002"} - assert set(accepted) == {"task-001.md", "task-002.md"} - - monkeypatch.setattr(plans, "require_plan_reviews", lambda *_args, **_kwargs: None) - monkeypatch.setattr(plans, "_plan_uses_accepted_result_authority", lambda *_args: False) - monkeypatch.setattr(plans, "_validated_plan_task_handoffs", lambda *_args: []) - monkeypatch.setattr(plans, "_assert_archive_knowledge_gate", lambda *_args: None) - monkeypatch.setattr(plans, "_assert_archive_plan_acceptance", lambda *_args: None) - plans.cmd_archive_plan(argparse.Namespace(project_root=str(tmp_path), id="plan-direct")) - - archived_rows = plans.index_plans(args) - assert { - (row["id"], row["type"]) - for row in archived_rows - if row.get("plan_id") == "plan-direct" - } == { - ("phase-001", "phase"), - ("phase-002", "phase"), - ("task-001", "task"), - ("task-002", "task"), - } - - -def test_declared_integration_commands_follow_table_headers() -> None: - five_columns = ( - "## 7. Tests\n\n" - "| ID | Test Type | Target | Command | Expected Result |\n" - "|---|---|---|---|---|\n" - "| TEST-001 | integration | archive | `env true` | passed |\n" - ) - reordered = ( - "## Tests\n\n" - "| Command | Expected Result | Target | Test Type | ID | Can Run With |\n" - "|---|---|---|---|---|---|\n" - "| `python -m pytest -q` | passed | archive | integration | TEST-002 | - |\n" - ) - - assert plans._declared_integration_commands(five_columns) == ["env true"] - assert plans._declared_integration_commands(reordered) == ["python -m pytest -q"] - - -def test_missing_terminal_plan_proof_executes_once_state_neutrally( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - subprocess = __import__("subprocess") - subprocess.run(["git", "init", "-q", "-b", "main"], cwd=tmp_path, check=True) - subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=tmp_path, check=True) - subprocess.run(["git", "config", "user.name", "Test"], cwd=tmp_path, check=True) - (tmp_path / "tracked.txt").write_text("stable\n", encoding="utf-8") - subprocess.run(["git", "add", "."], cwd=tmp_path, check=True) - subprocess.run(["git", "commit", "-qm", "base"], cwd=tmp_path, check=True) - plan = tmp_path / "plan.md" - plan.write_text( - "---\nid: plan-001\n---\n\n## 7. Tests\n\n" - "| ID | Test Type | Target | Command | Expected Result |\n" - "|---|---|---|---|---|\n" - "| TEST-001 | integration | archive | `env true` | passed |\n", - encoding="utf-8", - ) - observed: list[str] = [] - monkeypatch.setattr(plans, "_material_repository_root", lambda *_args: tmp_path) - monkeypatch.setattr( - plans, - "_observe_archive_command", - lambda command, _workspace: observed.append(command) or "passed", - ) - - plans._assert_archive_plan_acceptance( - argparse.Namespace(project_root=str(tmp_path)), "plan-001", plan, [] - ) - - assert observed == ["env true"] - - -def test_recovery_commands_are_not_public_dispatcher_actions() -> None: - dispatcher = _dispatcher() - - assert "create-accepted-base-absence-receipt" not in dispatcher.RECOGNIZED_COMMANDS - assert "adopt-existing-recovered-result" not in dispatcher.RECOGNIZED_COMMANDS - with pytest.raises(SystemExit): - dispatcher.build_parser().parse_args(["create-accepted-base-absence-receipt"]) diff --git a/tests/test_orchestration_artifact_foundation.py b/tests/test_orchestration_artifact_foundation.py new file mode 100644 index 0000000..d6870d2 --- /dev/null +++ b/tests/test_orchestration_artifact_foundation.py @@ -0,0 +1,566 @@ +from __future__ import annotations + +import argparse +import hashlib +import json +import stat +from pathlib import Path +import subprocess +import sys + +import pytest +import yaml + + +REPO_ROOT = Path(__file__).resolve().parents[1] +ORCH_ROOT = REPO_ROOT / "scripts" / "orchestration" +sys.path.insert(0, str(ORCH_ROOT)) + +from artifact_inputs import _read_structured +from artifact_store import ( + atomic_write_bytes, + canonical_artifact_path, + family_policy, + load_catalog, + read_markdown_artifact, + read_artifact, + read_yaml_mapping, + rebuild_index, + serialize_artifact, + transition_artifact, + validate_artifact, + write_artifact, +) +from specs import CATALOG_PATH as SPEC_CATALOG, archive_spec_for_forced_finalization, index_specs, replace_front_matter_value + + +RUNTIME_CATALOG = ( + REPO_ROOT + / "references/assets/orchestration/contract/artifact-family-catalog-v1.yaml" +) + + +@pytest.fixture(autouse=True) +def isolated_workspace_context(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + workspace_id = "wb-artifact-foundation" + control = tmp_path / ".work-bundle" + control.mkdir(parents=True, exist_ok=True) + metadata = { + "metadata_version": 4, + "authority": "canonical", + "workspace": {"id": workspace_id, "slug": "artifact-foundation", "mode": "single-repository"}, + "control_plane": { + "schema_version": 1, + "repository": {"remote": ""}, + "sync_policy": {"mode": "manual"}, + }, + "source_repositories": [ + { + "id": "source", + "role": "source", + "locator": {"type": "manual", "value": "test-fixture"}, + "default_branch": "main", + "workspace_binding": {"type": "root"}, + "materialization": {"required": True}, + "operation_policy": "inherit", + } + ], + } + (control / "project.yaml").write_text(yaml.safe_dump(metadata, sort_keys=False), encoding="utf-8") + home = tmp_path / "home" + config = home / ".work-bundle" + registry = config / "registry/projects.yaml" + registry.parent.mkdir(parents=True) + registry_document = { + "registry_schema_version": 1, + "projects": [], + "device_bindings": { + workspace_id: { + "slug": "artifact-foundation", + "workspace_root": str(tmp_path), + "control_plane_path": str(control), + "control_plane_remote": "", + "observed_control_plane_head": "", + "repositories": { + "source": { + "project_root": str(tmp_path), + "checkout_kind": "manual", + "observed_branch": "", + "observed_head": "", + "observed_at": "2026-09-19T00:00:00Z", + "git_common_dir": "", + } + }, + } + }, + } + registry.write_text(yaml.safe_dump(registry_document, sort_keys=False), encoding="utf-8") + (config / "bootstrap.yaml").write_text( + "\n".join( + [ + "bootstrap_version: v1", + "authority: canonical", + f"work_bundle_root: {REPO_ROOT}", + 'project_registry: "$work_bundle_config_root/registry/projects.yaml"', + 'skill_registry: "$work_bundle_config_root/registry/skill-registry.yaml"', + "", + ] + ), + encoding="utf-8", + ) + monkeypatch.setenv("HOME", str(home)) + + +def _temporary_catalog(tmp_path: Path) -> Path: + schema_dir = tmp_path / "schemas" + schema_dir.mkdir(parents=True) + schema = { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "note-v1", + "type": "object", + "required": ["id", "parent_id", "value"], + "properties": { + "id": {"type": "string", "pattern": r"^note-[0-9]+[a-z]?$"}, + "parent_id": {"type": "string", "pattern": r"^parent-[0-9]+$"}, + "value": {"type": "string"}, + }, + "additionalProperties": False, + } + (schema_dir / "note-v1.schema.json").write_text(json.dumps(schema), encoding="utf-8") + catalog = { + "catalog_id": "artifact-family-catalog-test-v1", + "schema_version": 1, + "families": [ + { + "name": "note", + "schema": {"id": "note-v1", "path": "schemas/note-v1.schema.json"}, + "representation": "yaml", + "anchor": "workspace_root", + "locator": { + "template": "artifacts/{state}/{id}.yaml", + "variables": ["state", "id"], + }, + "identity": {"field": "id", "pattern": r"^note-[0-9]+[a-z]?$"}, + "relationships": { + "bindings": [ + {"name": "parent", "field": "parent_id", "required": True} + ] + }, + "lifecycle": { + "authority": "location", + "states": ["active", "archived"], + "transitions": {"active": ["archived"], "archived": []}, + }, + "index": { + "path": "artifacts/index.jsonl", + "source_states": ["active", "archived"], + "projection": ["id", "parent_id", "value"], + "format": "jsonl", + }, + } + ], + } + path = tmp_path / "catalog.yaml" + path.write_text(yaml.safe_dump(catalog, sort_keys=False), encoding="utf-8") + return path + + +def test_runtime_catalog_is_valid_and_registers_only_itself() -> None: + catalog = load_catalog(RUNTIME_CATALOG) + assert catalog["catalog_id"] == "artifact-family-catalog-v1" + assert [item["name"] for item in catalog["families"]] == ["artifact-family-catalog"] + policy = family_policy(catalog, "artifact-family-catalog") + assert policy["schema"]["id"] == "artifact-family-catalog-v1" + assert policy["locator"]["template"] == ( + "references/assets/orchestration/contract/artifact-family-catalog-v1.yaml" + ) + schema = RUNTIME_CATALOG.with_name("artifact-family-catalog-v1.schema.json") + assert hashlib.sha256(schema.read_bytes()).hexdigest() == ( + "f7272e56eb04a9c13dd9ba64e24c9e905730abf252a43ca07664f2f35e401935" + ) + with pytest.raises(SystemExit, match="Unregistered artifact family"): + family_policy(catalog, "specification") + + +def test_catalog_rejects_duplicates_schema_escape_and_schema_identity_mismatch(tmp_path: Path) -> None: + catalog_path = _temporary_catalog(tmp_path) + content = yaml.safe_load(catalog_path.read_text(encoding="utf-8")) + content["families"].append(dict(content["families"][0])) + catalog_path.write_text(yaml.safe_dump(content, sort_keys=False), encoding="utf-8") + with pytest.raises(SystemExit, match="Duplicate artifact family"): + load_catalog(catalog_path) + + catalog_path = _temporary_catalog(tmp_path / "escape") + content = yaml.safe_load(catalog_path.read_text(encoding="utf-8")) + content["families"][0]["schema"]["path"] = "../outside.json" + catalog_path.write_text(yaml.safe_dump(content, sort_keys=False), encoding="utf-8") + with pytest.raises(SystemExit, match="schema path escapes"): + load_catalog(catalog_path) + + catalog_path = _temporary_catalog(tmp_path / "identity") + content = yaml.safe_load(catalog_path.read_text(encoding="utf-8")) + content["families"][0]["schema"]["id"] = "wrong-v1" + catalog_path.write_text(yaml.safe_dump(content, sort_keys=False), encoding="utf-8") + with pytest.raises(SystemExit, match="schema identity mismatch"): + load_catalog(catalog_path) + + catalog_path = _temporary_catalog(tmp_path / "locator-escape") + content = yaml.safe_load(catalog_path.read_text(encoding="utf-8")) + content["families"][0]["locator"]["template"] = "../artifacts/{state}/{id}.yaml" + catalog_path.write_text(yaml.safe_dump(content, sort_keys=False), encoding="utf-8") + with pytest.raises(SystemExit, match="locator policy.*canonical relative path"): + load_catalog(catalog_path) + + +@pytest.mark.parametrize( + "template", + [ + "artifacts//{state}/{id}.yaml", + "artifacts/./{state}/{id}.yaml", + "artifacts/*/{state}/{id}.yaml", + "artifacts/**/{state}/{id}.yaml", + "artifacts/?/{state}/{id}.yaml", + "artifacts/[ab]/{state}/{id}.yaml", + ], +) +def test_catalog_rejects_noncanonical_and_literal_glob_locator_templates( + tmp_path: Path, template: str +) -> None: + catalog_path = _temporary_catalog(tmp_path) + content = yaml.safe_load(catalog_path.read_text(encoding="utf-8")) + content["families"][0]["locator"]["template"] = template + catalog_path.write_text(yaml.safe_dump(content, sort_keys=False), encoding="utf-8") + + with pytest.raises(SystemExit, match="Artifact locator policy"): + load_catalog(catalog_path) + + +def test_catalog_rejects_noncanonical_and_literal_glob_index_paths(tmp_path: Path) -> None: + for suffix, index_path in ( + ("alias", "artifacts//index.jsonl"), + ("glob", "artifacts/*-index.jsonl"), + ): + catalog_path = _temporary_catalog(tmp_path / suffix) + content = yaml.safe_load(catalog_path.read_text(encoding="utf-8")) + content["families"][0]["index"]["path"] = index_path + catalog_path.write_text(yaml.safe_dump(content, sort_keys=False), encoding="utf-8") + + with pytest.raises(SystemExit, match="Artifact index policy"): + load_catalog(catalog_path) + + +def test_catalog_rejects_locator_variables_without_structural_sources(tmp_path: Path) -> None: + catalog_path = _temporary_catalog(tmp_path) + content = yaml.safe_load(catalog_path.read_text(encoding="utf-8")) + content["families"][0]["locator"] = { + "template": "artifacts/{foo}.yaml", + "variables": ["foo"], + } + catalog_path.write_text(yaml.safe_dump(content, sort_keys=False), encoding="utf-8") + + with pytest.raises(SystemExit, match="unsupported variables: foo"): + load_catalog(catalog_path) + + +@pytest.mark.parametrize( + ("template", "variables", "missing"), + [ + ("artifacts/{state}/note.yaml", ["state"], "identity"), + ("artifacts/{id}.yaml", ["id"], "lifecycle state"), + ], +) +def test_location_owned_catalog_requires_identity_and_state_distinguishing_locator( + tmp_path: Path, template: str, variables: list[str], missing: str +) -> None: + catalog_path = _temporary_catalog(tmp_path) + content = yaml.safe_load(catalog_path.read_text(encoding="utf-8")) + content["families"][0]["locator"] = {"template": template, "variables": variables} + catalog_path.write_text(yaml.safe_dump(content, sort_keys=False), encoding="utf-8") + + with pytest.raises(SystemExit, match=f"does not distinguish {missing}"): + load_catalog(catalog_path) + + +def test_maintained_yaml_and_markdown_readers_preserve_equivalent_structure(tmp_path: Path) -> None: + inline = tmp_path / "inline.yaml" + block = tmp_path / "block.yaml" + inline.write_text("id: spec-20260919-001a\nrelated: {plan: plan-001, task: task-001}\n", encoding="utf-8") + block.write_text("id: spec-20260919-001a\nrelated:\n plan: plan-001\n task: task-001\n", encoding="utf-8") + assert read_yaml_mapping(inline) == read_yaml_mapping(block) + + markdown = tmp_path / "artifact.md" + markdown.write_text("---\nid: spec-20260919-001a\nrelated: {plan: plan-001}\n---\nBody\n", encoding="utf-8") + data, body = read_markdown_artifact(markdown) + assert data["id"] == "spec-20260919-001a" + assert data["related"] == {"plan": "plan-001"} + assert body == "Body\n" + assert _read_structured(markdown) == (data, body) + + malformed = tmp_path / "bad.yaml" + malformed.write_text("id: [unterminated\n", encoding="utf-8") + with pytest.raises(SystemExit, match="Invalid YAML"): + read_yaml_mapping(malformed) + + +def test_generic_store_validates_location_bindings_atomicity_and_lifecycle(tmp_path: Path) -> None: + catalog_path = _temporary_catalog(tmp_path) + catalog = load_catalog(catalog_path) + policy = family_policy(catalog, "note") + anchors = {"workspace_root": tmp_path} + data = {"id": "note-001a", "parent_id": "parent-001", "value": "ok"} + bindings = {"parent": "parent-001"} + + assert canonical_artifact_path(policy, anchors, identity="note-001a", state="active") == ( + tmp_path / "artifacts/active/note-001a.yaml" + ) + assert validate_artifact(policy, data, catalog_path=catalog_path, bindings=bindings) == bindings + with pytest.raises(SystemExit, match="Missing required artifact binding"): + validate_artifact(policy, data, catalog_path=catalog_path) + assert serialize_artifact(policy, data) == serialize_artifact(policy, dict(reversed(list(data.items())))) + + result = write_artifact( + catalog_path, + "note", + anchors, + data, + state="active", + bindings=bindings, + rebuild=False, + ) + path = Path(result["path"]) + assert result["identity"] == "note-001a" + assert result["validated_bindings"] == bindings + assert result["partial_effect"] is False + read_result = read_artifact( + catalog_path, "note", anchors, identity="note-001a", state="active", bindings=bindings + ) + assert read_result["data"] == data + original = path.read_bytes() + + with pytest.raises(SystemExit, match="binding mismatch"): + write_artifact( + catalog_path, + "note", + anchors, + {**data, "value": "changed"}, + state="active", + bindings={"parent": "parent-999"}, + rebuild=False, + ) + assert path.read_bytes() == original + + same = transition_artifact( + catalog_path, "note", anchors, identity="note-001a", current_state="active", target_state="active", bindings=bindings + ) + assert same["path"] == str(path) + assert path.read_bytes() == original + + archived = transition_artifact( + catalog_path, "note", anchors, identity="note-001a", current_state="active", target_state="archived", bindings=bindings + ) + archived_path = Path(archived["path"]) + assert archived_path.read_bytes() == original + assert not path.exists() + + path.parent.mkdir(parents=True, exist_ok=True) + atomic_write_bytes(path, original) + with pytest.raises(SystemExit, match="destination collision"): + transition_artifact( + catalog_path, "note", anchors, identity="note-001a", current_state="active", target_state="archived", bindings=bindings + ) + + +def test_atomic_write_preserves_existing_file_mode(tmp_path: Path) -> None: + path = tmp_path / "artifact.yaml" + path.write_bytes(b"before\n") + path.chmod(0o644) + + atomic_write_bytes(path, b"after\n") + + assert path.read_bytes() == b"after\n" + assert stat.S_IMODE(path.stat().st_mode) == 0o644 + + +def test_atomic_write_uses_normal_creation_mode_for_new_file(tmp_path: Path) -> None: + ordinary = tmp_path / "ordinary.yaml" + ordinary.write_bytes(b"ordinary\n") + atomic = tmp_path / "atomic.yaml" + + atomic_write_bytes(atomic, b"atomic\n") + + assert stat.S_IMODE(atomic.stat().st_mode) == stat.S_IMODE(ordinary.stat().st_mode) + + +def test_declared_index_rebuild_validates_candidates_and_refuses_duplicates(tmp_path: Path) -> None: + catalog_path = _temporary_catalog(tmp_path) + anchors = {"workspace_root": tmp_path} + for identity, state in [("note-001a", "active"), ("note-002", "archived")]: + write_artifact( + catalog_path, + "note", + anchors, + {"id": identity, "parent_id": "parent-001", "value": identity}, + state=state, + bindings={"parent": "parent-001"}, + rebuild=False, + ) + result = rebuild_index(catalog_path, "note", anchors) + rows = [json.loads(line) for line in Path(result["path"]).read_text(encoding="utf-8").splitlines()] + assert [row["id"] for row in rows] == ["note-001a", "note-002"] + + invalid = tmp_path / "artifacts/active/not-canonical.yaml" + invalid.write_text("id: note-003\nparent_id: parent-001\nvalue: bad-place\n", encoding="utf-8") + previous = Path(result["path"]).read_bytes() + with pytest.raises(SystemExit, match="Invalid index candidate"): + rebuild_index(catalog_path, "note", anchors) + assert Path(result["path"]).read_bytes() == previous + + +def test_declared_index_refuses_duplicate_identity_across_states(tmp_path: Path) -> None: + catalog_path = _temporary_catalog(tmp_path) + anchors = {"workspace_root": tmp_path} + payload = {"id": "note-001", "parent_id": "parent-001", "value": "duplicate"} + for state in ("active", "archived"): + write_artifact( + catalog_path, + "note", + anchors, + payload, + state=state, + bindings={"parent": "parent-001"}, + rebuild=False, + ) + with pytest.raises(SystemExit, match="duplicate identity"): + rebuild_index(catalog_path, "note", anchors) + + +def _spec_args(root: Path) -> argparse.Namespace: + return argparse.Namespace(project_root=None, workspace_root=str(root)) + + +def _canonical_spec(root: Path, identity: str = "spec-001a") -> Path: + data = { + "artifact_type": "specification", "schema_version": 1, "id": identity, + "title": "Exact", "status": "draft", "date_created": "2099-01-01", + "last_updated": "2099-01-01", "purpose": "Foundation regression", + "component": "orchestration", "version": "1.0", "project": "demo", + "source_knowledge": [], "related_handoffs": [], "tags": ["test"], + "execution_workspace": {"isolation": "existing", "profile": "default", "cleanup": "manual"}, + } + result = write_artifact( + SPEC_CATALOG, "specification", {"workspace_root": root}, data, + state="active", body="# Specification\n", + ) + return Path(str(result["path"])) + + +def test_spec_index_uses_only_canonical_family_and_ignores_legacy_markdown(tmp_path: Path) -> None: + root = tmp_path / ".work-bundle/orchestration/spec/active" + root.mkdir(parents=True) + _canonical_spec(tmp_path) + rows = index_specs(_spec_args(tmp_path)) + assert rows[0]["id"] == "spec-001a" + + missing = root / "missing.md" + missing.write_text("---\ntitle: Legacy\n---\n", encoding="utf-8") + index_before = (tmp_path / ".work-bundle/orchestration/spec/index.jsonl").read_bytes() + assert [row["id"] for row in index_specs(_spec_args(tmp_path))] == ["spec-001a"] + assert (tmp_path / ".work-bundle/orchestration/spec/index.jsonl").read_bytes() == index_before + + +def test_spec_front_matter_mutation_uses_maintained_yaml_and_atomic_write(tmp_path: Path) -> None: + path = tmp_path / "spec.md" + path.write_text("---\nid: spec-001a\nrelated: {plan: plan-001}\nstatus: active\nlast_updated: 2020-01-01\n---\nBody\n", encoding="utf-8") + replace_front_matter_value(path, "status", "reviewed") + data, body = read_markdown_artifact(path) + assert data["id"] == "spec-001a" + assert data["related"] == {"plan": "plan-001"} + assert data["status"] == "reviewed" + assert body == "Body\n" + + +def test_forced_archive_uses_explicit_identity_without_weakening_strict_index(tmp_path: Path) -> None: + active = _canonical_spec(tmp_path, "spec-origin") + index_specs(_spec_args(tmp_path)) + + archived = archive_spec_for_forced_finalization(_spec_args(tmp_path), "spec-origin") + + assert archived == tmp_path / ".work-bundle/orchestration/spec/archived/spec-origin.spec.md" + assert archived.read_text(encoding="utf-8").endswith("# Specification\n") + assert not active.exists() + rows = [ + json.loads(line) + for line in (tmp_path / ".work-bundle/orchestration/spec/index.jsonl") + .read_text(encoding="utf-8") + .splitlines() + ] + assert rows[0]["id"] == "spec-origin" + + assert archive_spec_for_forced_finalization(_spec_args(tmp_path), "spec-origin") == archived + + +def test_forced_archive_rejects_incomplete_specification(tmp_path: Path) -> None: + active = tmp_path / ".work-bundle/orchestration/spec/active/spec-origin.spec.md" + active.parent.mkdir(parents=True) + active.write_text("---\nid: spec-origin\n---\nLegacy body\n", encoding="utf-8") + + with pytest.raises(SystemExit, match="schema validation|Artifact schema"): + archive_spec_for_forced_finalization(_spec_args(tmp_path), "spec-origin") + + assert active.is_file() + + +def test_write_reports_index_failure_as_partial_effect(tmp_path: Path) -> None: + catalog_path = _temporary_catalog(tmp_path) + index_path = tmp_path / "artifacts/index.jsonl" + index_path.mkdir(parents=True) + with pytest.raises(SystemExit, match="(?i)artifact was written.*index rebuild failed"): + write_artifact( + catalog_path, + "note", + {"workspace_root": tmp_path}, + {"id": "note-001", "parent_id": "parent-001", "value": "ok"}, + state="active", + bindings={"parent": "parent-001"}, + rebuild=True, + ) + assert (tmp_path / "artifacts/active/note-001.yaml").is_file() + + +def test_public_spec_dispatcher_paths_use_strict_shared_primitives(tmp_path: Path) -> None: + content = tmp_path / "content.md" + content.write_text( + "---\nproject: demo\nsource_knowledge: []\nrelated_handoffs: []\ntags: [test]\n" + "execution_workspace: {isolation: existing, profile: default, cleanup: manual}\n" + "---\n# Specification\n", + encoding="utf-8", + ) + base = [sys.executable, str(REPO_ROOT / "scripts/orch.py")] + + def run(*arguments: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [*base, *arguments, "--project-root", str(tmp_path)], + cwd=REPO_ROOT, + text=True, + capture_output=True, + check=False, + ) + + written = run( + "write-spec", "--id", "spec-001a", "--title", "Exact", "--purpose", "test", + "--component", "foundation", "--content-file", str(content), "--status", "draft", + ) + assert written.returncode == 0, written.stderr + assert "spec-001a" in written.stdout + + indexed = run("index-specs") + assert indexed.returncode == 0, indexed.stderr + listed = run("list-specs") + assert listed.returncode == 0, listed.stderr + assert json.loads(listed.stdout.splitlines()[-1])["id"] == "spec-001a" + + transitioned = run("set-spec-status", "--id", "spec-001a", "--status", "verified") + assert transitioned.returncode == 0, transitioned.stderr + listed_active = run("list-specs", "--status", "verified") + assert listed_active.returncode == 0, listed_active.stderr + assert json.loads(listed_active.stdout.splitlines()[-1])["status"] == "verified" diff --git a/tests/test_orchestration_blocking_admission.py b/tests/test_orchestration_blocking_admission.py index 0a9323f..1e662b2 100644 --- a/tests/test_orchestration_blocking_admission.py +++ b/tests/test_orchestration_blocking_admission.py @@ -1,195 +1,143 @@ from __future__ import annotations import hashlib -import argparse from pathlib import Path -import subprocess import sys import pytest import yaml + REPO_ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(REPO_ROOT / "scripts/orchestration")) import bounded_closure as bounded # noqa: E402 -import plans # noqa: E402 - - -@pytest.mark.parametrize("writer", [plans.cmd_write_task, plans.cmd_write_phase]) -@pytest.mark.parametrize("condition", ["blocker", "exhausted", "exempt", "legacy"]) -@pytest.mark.parametrize("from_member", [False, True]) -def test_direct_plan_writers_enforce_admission_before_mutation( - tmp_path: Path, writer, condition: str, from_member: bool, -) -> None: - control = None if condition == "legacy" else _control() - if condition == "exhausted": - control["blockers"] = [] - control["post_execution_review_flows"] = [ - {"flow_id": "allowed-flow", "finalization_required": True} - ] - root = _workspace(tmp_path, control=control) - spec = root / ".work-bundle/orchestration/spec/active/block.md" - spec.parent.mkdir(parents=True) - spec.write_text("# unresolved\n", encoding="utf-8") - content = root / "input.md" - content.write_text("# Writer input\n", encoding="utf-8") - member = root / "member" - member.mkdir() - args = argparse.Namespace( - project_root=str(member if from_member else root), - content_file=str(content), plan_id="other-flow" if condition == "blocker" else "allowed-flow", - phase_id="phase-001", task_id="task-001", title="Direct", status="Planned", - ) - - def snapshot(): - return { - str(path.relative_to(root)): path.read_bytes() if path.is_file() else None - for path in root.rglob("*") - } - - before = snapshot() - if condition in {"blocker", "exhausted"}: - code = "WB_ORCHESTRATION_ADMISSION_BLOCKED" if condition == "blocker" else "WB_ORCHESTRATION_FINALIZATION_REQUIRED" - with pytest.raises(bounded.BoundedClosureError, match=code): - writer(args) - assert snapshot() == before - else: - writer(args) - outputs = list((root / ".work-bundle/orchestration/plan/active").rglob("*-direct.md")) - assert len(outputs) == 1 - assert "Writer input" in outputs[0].read_text() - assert (root / ".work-bundle/orchestration/plan/index.jsonl").is_file() def _workspace(tmp_path: Path, *, control: dict[str, object] | None) -> Path: wb = tmp_path / ".work-bundle" wb.mkdir(parents=True) - metadata: dict[str, object] = {"metadata_version": 4, "workspace": {"id": "ws-1"}} + metadata: dict[str, object] = { + "metadata_version": 4, + "workspace": {"id": "ws-1"}, + } if control is not None: metadata["orchestration_control"] = control - (wb / "project.yaml").write_text(yaml.safe_dump(metadata, sort_keys=False), encoding="utf-8") + (wb / "project.yaml").write_text( + yaml.safe_dump(metadata, sort_keys=False), encoding="utf-8" + ) return tmp_path def _control() -> dict[str, object]: return { "schema_version": 1, - "post_execution_review_round_limit": 5, "blockers": [{ - "id": "BLOCK-1", "status": "active", "origin_plan": "old-flow", + "id": "BLOCK-1", + "status": "active", + "origin_plan": "old-flow", "specification": ".work-bundle/orchestration/spec/active/block.md", }], "implementation_exemptions": [{ - "flow_id": "allowed-flow", "blocker_id": "BLOCK-1", "status": "active", + "flow_id": "allowed-flow", + "blocker_id": "BLOCK-1", + "status": "active", }], } -def test_active_blocker_denies_other_flow_and_names_evidence(tmp_path: Path) -> None: - root = _workspace(tmp_path, control=_control()) +def _write_blocker_evidence(root: Path) -> Path: spec = root / ".work-bundle/orchestration/spec/active/block.md" spec.parent.mkdir(parents=True) spec.write_text("# unresolved\n", encoding="utf-8") + return spec + + +def test_active_blocker_denies_other_flow_and_names_evidence(tmp_path: Path) -> None: + root = _workspace(tmp_path, control=_control()) + spec = _write_blocker_evidence(root) with pytest.raises(bounded.BoundedClosureError) as captured: - bounded.require_orchestration_admission(root, operation="ordinary_new", flow_id="other-flow") + bounded.require_orchestration_admission( + root, operation="ordinary_new", flow_id="other-flow" + ) assert captured.value.code == "WB_ORCHESTRATION_ADMISSION_BLOCKED" detail = captured.value.detail or "" - assert all(value in detail for value in (str(root / ".work-bundle/project.yaml"), "BLOCK-1", str(spec), "orch-bounded-closure")) - assert bounded.require_orchestration_admission(root, operation="ordinary_new", flow_id="allowed-flow")["status"] == "admitted" - assert bounded.require_orchestration_admission(root, operation="read_only", flow_id=None)["status"] == "admitted" - - -def test_legacy_absence_is_allowed_but_malformed_or_missing_evidence_fails_closed(tmp_path: Path) -> None: - legacy = _workspace(tmp_path / "legacy", control=None) - assert bounded.require_orchestration_admission(legacy, operation="ordinary_new", flow_id="new")["legacy"] is True - - malformed = _workspace(tmp_path / "malformed", control={"schema_version": 1}) + assert all(value in detail for value in ( + str(root / ".work-bundle/project.yaml"), + "BLOCK-1", + str(spec), + "orch-bounded-closure", + )) + assert bounded.require_orchestration_admission( + root, operation="ordinary_new", flow_id="allowed-flow" + )["status"] == "admitted" + assert bounded.require_orchestration_admission( + root, operation="read_only", flow_id=None + )["status"] == "admitted" + + +def test_absent_control_is_allowed_but_malformed_or_missing_evidence_fails_closed( + tmp_path: Path, +) -> None: + absent = _workspace(tmp_path / "absent", control=None) + assert bounded.require_orchestration_admission( + absent, operation="ordinary_new", flow_id="new" + )["status"] == "admitted" + + malformed = _workspace( + tmp_path / "malformed", + control={"schema_version": 1, "blockers": {}}, + ) with pytest.raises(bounded.BoundedClosureError, match="WB_POST_EXECUTION_POLICY_INVALID"): - bounded.require_orchestration_admission(malformed, operation="ordinary_new", flow_id="new") + bounded.require_orchestration_admission( + malformed, operation="ordinary_new", flow_id="new" + ) missing = _workspace(tmp_path / "missing", control=_control()) - with pytest.raises(bounded.BoundedClosureError, match="WB_ORCHESTRATION_BLOCKER_EVIDENCE_INVALID"): - bounded.require_orchestration_admission(missing, operation="ordinary_new", flow_id="other") - - -def test_exhausted_flow_denies_reconciliation_before_blocker_exists(tmp_path: Path) -> None: - control = _control() - control["blockers"] = [] - control["implementation_exemptions"] = [] - control["post_execution_review_flows"] = [{"flow_id": "flow-1", "finalization_required": True}] - root = _workspace(tmp_path, control=control) - with pytest.raises(bounded.BoundedClosureError, match="WB_ORCHESTRATION_FINALIZATION_REQUIRED"): - bounded.require_orchestration_admission(root, operation="reconciliation", flow_id="flow-1") - assert bounded.require_orchestration_admission(root, operation="finalization", flow_id="flow-1")["status"] == "admitted" + with pytest.raises( + bounded.BoundedClosureError, + match="WB_ORCHESTRATION_BLOCKER_EVIDENCE_INVALID", + ): + bounded.require_orchestration_admission( + missing, operation="ordinary_new", flow_id="other" + ) -def test_restore_exception_merges_backup_blocker_without_erasing_newer_control(tmp_path: Path) -> None: +def test_restore_exception_merges_backup_blocker_without_erasing_newer_control( + tmp_path: Path, +) -> None: original = _control() - original["review_revision_limit"] = original.pop("post_execution_review_round_limit") original.pop("implementation_exemptions") backup = tmp_path / "before.yaml" - backup.write_text(yaml.safe_dump({"metadata_version": 4, "orchestration_control": original}, sort_keys=False), encoding="utf-8") + backup.write_text( + yaml.safe_dump( + {"metadata_version": 4, "orchestration_control": original}, + sort_keys=False, + ), + encoding="utf-8", + ) digest = hashlib.sha256(backup.read_bytes()).hexdigest() current = _control() current["newer_field"] = {"preserve": True} - current["blockers"] = [{"id": "OTHER", "status": "active", "specification": "other.md"}] + current["blockers"] = [{ + "id": "OTHER", + "status": "active", + "specification": "other.md", + }] root = _workspace(tmp_path / "workspace", control=current) - bounded.restore_implementation_exception(root, backup_path=backup, backup_sha256=digest, - flow_id="allowed-flow", blocker_id="BLOCK-1") - restored = yaml.safe_load((root / ".work-bundle/project.yaml").read_text(encoding="utf-8"))["orchestration_control"] - assert restored["post_execution_review_round_limit"] == 5 + bounded.restore_implementation_exception( + root, + backup_path=backup, + backup_sha256=digest, + flow_id="allowed-flow", + blocker_id="BLOCK-1", + ) + + restored = yaml.safe_load( + (root / ".work-bundle/project.yaml").read_text(encoding="utf-8") + )["orchestration_control"] assert restored["newer_field"] == {"preserve": True} assert {item["id"] for item in restored["blockers"]} == {"OTHER", "BLOCK-1"} assert restored.get("implementation_exemptions", []) == [] - - -def test_public_dispatcher_refuses_before_creating_spec_files(tmp_path: Path) -> None: - control = _control() - control["implementation_exemptions"] = [] - root = _workspace(tmp_path, control=control) - spec = root / ".work-bundle/orchestration/spec/active/block.md" - spec.parent.mkdir(parents=True) - spec.write_text("# unresolved\n", encoding="utf-8") - content = root / "input.md" - content.write_text("# candidate\n", encoding="utf-8") - result = subprocess.run( - [ - sys.executable, str(REPO_ROOT / "scripts/orchestration/dispatcher.py"), - "write-spec", "--project-root", str(root), "--title", "Denied", - "--purpose", "test", "--component", "test", "--content-file", str(content), - "--id", "spec-denied", - ], - text=True, capture_output=True, check=False, - ) - assert result.returncode != 0 - assert all(value in result.stderr for value in ("BLOCK-1", "block.md", "orch-bounded-closure")), result.stderr - assert not (root / ".work-bundle/orchestration/spec/active/spec-denied-denied.md").exists() - - -def test_controller_observation_uses_bound_plan_for_current_scoped_exception(tmp_path: Path) -> None: - root = _workspace(tmp_path, control=_control()) - spec = root / ".work-bundle/orchestration/spec/active/block.md" - spec.parent.mkdir(parents=True) - spec.write_text("# unresolved\n", encoding="utf-8") - task = root / ".work-bundle/orchestration/plan/active/plan-current/task-002.md" - task.parent.mkdir(parents=True) - task.write_text("---\nid: task-002\nplan_id: allowed-flow\n---\n", encoding="utf-8") - resolver = ( - "import argparse,sys; " - f"sys.path.insert(0, {str(REPO_ROOT / 'scripts/orchestration')!r}); " - "from execution_context import task_flow_id; " - f"print(task_flow_id(argparse.Namespace(project_root={str(root)!r}, task={task.relative_to(root).as_posix()!r})))" - ) - resolved = subprocess.run( - [sys.executable, "-c", resolver], text=True, capture_output=True, check=True - ).stdout.strip() - bounded.require_orchestration_admission(root, operation="reconciliation", flow_id=resolved) - task.write_text("---\nid: task-002\nplan_id: unrelated-flow\n---\n", encoding="utf-8") - unrelated = subprocess.run( - [sys.executable, "-c", resolver], text=True, capture_output=True, check=True - ).stdout.strip() - with pytest.raises(bounded.BoundedClosureError, match="WB_ORCHESTRATION_ADMISSION_BLOCKED"): - bounded.require_orchestration_admission(root, operation="reconciliation", flow_id=unrelated) diff --git a/tests/test_orchestration_context_projection.py b/tests/test_orchestration_context_projection.py deleted file mode 100644 index 04981fd..0000000 --- a/tests/test_orchestration_context_projection.py +++ /dev/null @@ -1,1443 +0,0 @@ -from __future__ import annotations - -import builtins -import hashlib -import json -import subprocess -import sys -from copy import deepcopy -from pathlib import Path - -import pytest - - -REPO_ROOT = Path(__file__).resolve().parents[1] -ORCHESTRATION = REPO_ROOT / "scripts" / "orchestration" -WORK_BUNDLE = REPO_ROOT / "scripts" / "work-bundle" -for path in (WORK_BUNDLE, ORCHESTRATION): - if str(path) not in sys.path: - sys.path.insert(0, str(path)) - -import execution_context # noqa: E402 -import review_runtime # noqa: E402 -from stage_events import StageEventError, validate_stage_event # noqa: E402 -from test_orchestration_execution_context import ( # noqa: E402 - _bind_task_execution, - _compiled_brief, - _ensure_source_file, - args, - build_review_package, - build_task_brief, - git, - workspace, -) -from test_stage_events import event # noqa: E402 - - -def _document(root: Path, task: Path) -> dict: - return execution_context._read_structured(build_task_brief(args(root, task)))[0] - - -def _delegation_evidence() -> dict[str, object]: - return { - "delegated": True, - "owner_kind": "subagent", - "agent_id": "ctx-fixture-agent", - "run_id": "ctx-fixture-run", - "mechanism": "host-native", - } - - -def _without_terminal_evidence(brief: dict[str, object], reason: str) -> dict[str, object]: - projected = deepcopy(brief) - projected["validation"] = [] - projected["evidence_capability"] = { - "result": "no_validation_bearing_obligation", - "reason": reason, - "invariants": [], - } - projected["evidence_applicability"] = { - kind: {"required": False, "reasons": []} - for kind in ("metadata", "repository", "codegraph") - } - return projected - - -def test_ctx_01_unrelated_runtime_history_does_not_inflate_unchanged_task_brief( - tmp_path: Path, -) -> None: - root, _, task = workspace(tmp_path) - first = build_task_brief(args(root, task)).read_bytes() - history = root / ".work-bundle/runtime/history/unrelated.jsonl" - history.parent.mkdir(parents=True) - history.write_text("{\"provenance\":\"x\"}\n" * 50_000, encoding="utf-8") - (history.parent / "accepted-handoffs.json").write_text("[]\n", encoding="utf-8") - - second = build_task_brief(args(root, task)).read_bytes() - - assert second == first - - -def test_ctx_01_repair_package_uses_frontier_without_reacquiring_review_history( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, -) -> None: - root, _, task = workspace(tmp_path) - task.write_text( - task.read_text(encoding="utf-8").replace( - "acceptance_review:\n required: false\n", - "acceptance_review:\n required: true\n", - ), - encoding="utf-8", - ) - scoped = _ensure_source_file(root) - git(root, "add", ".") - git(root, "commit", "-qm", "reviewed") - base = git(root, "rev-parse", "HEAD") - base_tree = git(root, "rev-parse", "HEAD^{tree}") - brief = _compiled_brief(root, task) - _bind_task_execution(root, brief) - scoped.write_text("def compile_task():\n return 'repaired'\n", encoding="utf-8") - git(root, "add", str(scoped.relative_to(root))) - git(root, "commit", "-qm", "repaired") - head = git(root, "rev-parse", "HEAD") - head_tree = git(root, "rev-parse", "HEAD^{tree}") - identity = lambda revision, tree, digest: { - "artifact_id": "task-004", "revision": revision, "sha256": digest, "source_tree": tree - } - frontier = { - "prior_review_id": "review-prior", - "blocking_finding_ids": ["RF-FINDING-1"], - "previous_reviewed_identity": identity( - base, base_tree, execution_context.semantic_digest("old") - ), - "repaired_identity": identity( - head, head_tree, execution_context.semantic_digest("new") - ), - "affected_boundaries": ["compile_task"], - "frozen_evidence_reference": execution_context.semantic_digest("frozen"), - } - monkeypatch.setattr( - execution_context, - "_stored_task_repair_preparation", - lambda *_args, **_kwargs: ( - {"review_id": "review-prior", "history": "MUST-NOT-BE-PROJECTED"}, - frontier, - ), - ) - handoff = { - "id": "handoff-task-004", - "type": "executor-result", - "related": {"plan": "plan-001", "task": "task-004"}, - "result": {"state": "partial"}, - "task_fit_check": {"task": "task-004", "result": "repaired"}, - "delegation_evidence": _delegation_evidence(), - "changes": {"files": [{"path": "scripts/orchestration/execution_context.py", "action": "modified"}]}, - "repository": [{ - "root": str(root.resolve()), "target_kind": "git-backed", - "preflight_kind": "git-clean-worktree", "baseline": "initial", "status": "clean", - }], - "codegraph": [{"root": str(root.resolve()), "applicable": False, "up_to_date": False, "reason": "no-index"}], - "knowledge_disposition": {"action": "none", "reason": "No stable authority changed.", "affected_authority": []}, - } - handoff_path = root / ".work-bundle/orchestration/handoff/executor/active/handoff-task-004.yaml" - handoff_path.parent.mkdir(parents=True) - handoff_path.write_text("\n".join(execution_context._dump_yaml(handoff)) + "\n", encoding="utf-8") - - package = build_review_package( - args(root, task, handoff=str(handoff_path), base=base, head=head) - ).read_text(encoding="utf-8") - - assert "Review mode: repair" in package - assert "RF-FINDING-1" in package - assert "compile_task" in package - assert execution_context.semantic_digest("frozen") not in package - assert "MUST-NOT-BE-PROJECTED" not in package - assert f"Base: {base}" in package and f"Head: {head}" in package - - -def test_ctx_02_semantic_authority_is_retained_once_and_referenced_by_id( - tmp_path: Path, -) -> None: - root, _, task = workspace(tmp_path) - document = _document(root, task) - brief = document["task_brief"] - rendered = build_task_brief(args(root, task)).read_text(encoding="utf-8") - - assert brief["semantic_authority"]["records"]["REQ-003"]["canonical_field"] == "requirements" - assert brief["semantic_authority"]["records"]["API-002"]["canonical_field"] == "interface_semantics" - assert brief["interfaces"] == {"consumes": ["API-002"], "produces": ["API-002"]} - assert brief["validation"][0]["proves"] == "TEST-004" - for meaning in ( - "Retry exactly three times before returning failure.", - "Never write outside the assigned files.", - "`compile_task(task: Path) -> dict[str, object]`", - "Focused pytest exits with status 0.", - ): - assert rendered.count(meaning) == 1 - - -def test_ctx_03_executor_capability_projection_contains_only_allocated_capabilities( - tmp_path: Path, -) -> None: - root, _, task = workspace(tmp_path) - brief = _document(root, task)["task_brief"] - - assert brief["capability_projection"] == { - "executor": {"capability": "mechanical", "reason": "task executor_profile allocation"}, - "rules": [{"id": "scoped-rule", "reason": "Keep the executor packet bounded."}], - "skills": [{"id": "dev-test-driven-development", "reason": "task methodology allocation"}], - "traversal": "runtime_only", - } - assert "parent-rule" not in json.dumps(brief) - - -def test_ctx_04_success_evidence_projects_compact_receipt_not_history_or_stdout() -> None: - projected = execution_context.project_validation_evidence( - [{"id": "VAL-001", "command": "pytest -q", "result": "passed", "stdout": "large output", "history": [1, 2]}], - evidence_capability={ - "invariants": [{"boundary": "component", "freshness": "current_task_batch", "evidence_ids": ["VAL-001"]}] - }, - observed=[{"id": "VAL-001", "observation_id": "observation-001", "result": "passed"}], - ) - - assert projected == [{ - "id": "VAL-001", - "command": "pytest -q", - "invariant_ids": [], - "observation_id": "observation-001", - "digest": execution_context.semantic_digest({"command": "pytest -q", "result": "passed"}), - "result": "passed", - "boundary": "component", - "freshness": "current_task_batch", - "invalidation_receipt": "observation-001", - "expansion_reason": None, - }] - assert "stdout" not in json.dumps(projected) - assert "history" not in json.dumps(projected) - - -def test_ctx_05_failed_evidence_expands_only_with_allowed_reason() -> None: - item = {"id": "VAL-001", "command": "pytest -q", "result": "failed", "stderr": "assertion failed"} - projected = execution_context.project_validation_evidence( - [item], evidence_capability={"invariants": []}, expansion_reason="failed_validation" - ) - - assert projected[0]["expansion_reason"] == "failed_validation" - assert projected[0]["details"] == item - with pytest.raises(SystemExit, match="expansion_reason"): - execution_context.project_validation_evidence( - [item], evidence_capability={"invariants": []}, expansion_reason="whole_history" - ) - - -def test_ctx_06_no_retrieval_escape_hatch_and_context_metrics_use_existing_telemetry( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - root, _, task = workspace(tmp_path) - document = _document(root, task) - brief = document["task_brief"] - metrics = document["compiled_context_metrics"] - - assert brief["runtime_context"] == { - "authority": "compiled", - "histories": "runtime_lazy", - "executor_retrieval": "forbidden", - } - assert all(isinstance(value, int) and value >= 0 for key, value in metrics.items() if key != "expansion_reason") - assert metrics["expansion_reason"] is None - assert "hard_limit" not in json.dumps(document) - - compiled_packet = _without_terminal_evidence( - brief, "CTX-06 executes only from its already-compiled packet." - ) - handoff = { - "type": "executor-result", - "related": {"plan": brief["plan_id"], "task": brief["task_id"]}, - "result": {"state": "completed"}, - "task_fit_check": {"task": brief["task_id"], "result": "clean"}, - "delegation_evidence": _delegation_evidence(), - "knowledge_disposition": { - "action": "none", - "reason": "No stable authority changed.", - "affected_authority": [], - }, - } - original_open = builtins.open - original_read_text = Path.read_text - original_read_bytes = Path.read_bytes - - def retrieval_is_denied(file: object) -> bool: - candidate = str(file) - return ( - ".work-bundle/knowledge" in candidate - or ".work-bundle/orchestration" in candidate - ) - - def deny_runtime_retrieval(file: object, *args: object, **kwargs: object): - if retrieval_is_denied(file): - raise AssertionError(f"executor retrieval attempted: {file}") - return original_open(file, *args, **kwargs) - - def deny_path_text(file: Path, *args: object, **kwargs: object) -> str: - if retrieval_is_denied(file): - raise AssertionError(f"executor retrieval attempted: {file}") - return original_read_text(file, *args, **kwargs) - - def deny_path_bytes(file: Path) -> bytes: - if retrieval_is_denied(file): - raise AssertionError(f"executor retrieval attempted: {file}") - return original_read_bytes(file) - - monkeypatch.setattr(builtins, "open", deny_runtime_retrieval) - monkeypatch.setattr(Path, "read_text", deny_path_text) - monkeypatch.setattr(Path, "read_bytes", deny_path_bytes) - accepted = execution_context.validate_executor_result_for_task( - handoff, compiled_packet, mutation_events=[] - ) - assert accepted["result_state"] == "completed" - assert accepted["task_ownership"]["agent_id"] == "ctx-fixture-agent" - - payload = event() - payload["compiled_context_metrics"] = metrics - assert validate_stage_event(payload).compiled_context_metrics == metrics - invalid = deepcopy(payload) - invalid["compiled_context_metrics"]["expansion_reason"] = "whole_history" - with pytest.raises(StageEventError, match="WB_STAGE_EVENT_CONTEXT_METRICS_INVALID"): - validate_stage_event(invalid) - - -def test_completed_task_acceptance_requires_subagent_delegation_evidence(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - brief = _without_terminal_evidence( - _document(root, task)["task_brief"], "Ownership-only acceptance fixture." - ) - handoff = { - "type": "executor-result", - "related": {"plan": brief["plan_id"], "task": brief["task_id"]}, - "result": {"state": "completed"}, - "task_fit_check": {"task": brief["task_id"], "result": "clean"}, - "knowledge_disposition": { - "action": "none", - "reason": "No stable authority changed.", - "affected_authority": [], - }, - } - - with pytest.raises(SystemExit, match="workspace-blocked.*subagent ownership"): - execution_context.validate_executor_result_for_task( - handoff, brief, mutation_events=[] - ) - - -def test_completed_task_acceptance_rejects_controller_task_scope_mutation( - tmp_path: Path, -) -> None: - root, _, task = workspace(tmp_path) - brief = _without_terminal_evidence( - _document(root, task)["task_brief"], "Ownership-only acceptance fixture." - ) - handoff = { - "type": "executor-result", - "related": {"plan": brief["plan_id"], "task": brief["task_id"]}, - "result": {"state": "completed"}, - "task_fit_check": {"task": brief["task_id"], "result": "clean"}, - "delegation_evidence": _delegation_evidence(), - "knowledge_disposition": { - "action": "none", - "reason": "No stable authority changed.", - "affected_authority": [], - }, - } - - with pytest.raises(SystemExit, match="review-blocked.*controller mutated"): - execution_context.validate_executor_result_for_task( - handoff, - brief, - mutation_events=[{ - "actor_kind": "controller", - "paths": [brief["files"]["write"][0]], - }], - ) - assert "mutation_events" not in handoff - durable_history = {**handoff, "mutation_events": []} - with pytest.raises(SystemExit, match="forbidden field mutation_events"): - execution_context.validate_executor_result_for_task(durable_history, brief) - - -def test_repair_acceptance_requires_exact_runtime_owner_and_continuity( - tmp_path: Path, -) -> None: - root, _, task = workspace(tmp_path) - scoped = _ensure_source_file(root) - git(root, "add", ".") - git(root, "commit", "-qm", "baseline") - brief = _without_terminal_evidence( - _document(root, task)["task_brief"], "Repair ownership fixture." - ) - binding = _bind_task_execution(root, brief) - scoped.write_text("def compile_task():\n return 'repair'\n", encoding="utf-8") - frontier = { - "prior_review_id": "review-prior", - "frozen_evidence_reference": execution_context.semantic_digest("evidence"), - } - handoff = { - "type": "executor-result", - "related": {"plan": brief["plan_id"], "task": brief["task_id"]}, - "result": {"state": "completed"}, - "task_fit_check": {"task": brief["task_id"], "result": "repaired"}, - "delegation_evidence": _delegation_evidence(), - "knowledge_disposition": { - "action": "none", - "reason": "No stable authority changed.", - "affected_authority": [], - }, - } - continuity = { - brief["task_id"]: { - "binding_id": binding["ownership"]["binding_id"], - "baseline_identity": execution_context.semantic_digest(binding["baseline"]), - "evidence_identity": frontier["frozen_evidence_reference"], - "previous_review_identity": frontier["prior_review_id"], - } - } - prior = {brief["task_id"]: _delegation_evidence()} - - accepted = execution_context.validate_executor_result_for_task( - handoff, - brief, - mutation_events=[], - prior_ownership=prior, - repair_continuity=continuity, - review_repair_frontier=frontier, - ) - assert accepted["task_ownership"]["agent_id"] == "ctx-fixture-agent" - - replacement = deepcopy(handoff) - replacement["delegation_evidence"] = { - **_delegation_evidence(), - "agent_id": "unrelated-agent", - "run_id": "unrelated-run", - } - with pytest.raises(SystemExit, match="replacement is not authorized"): - execution_context.validate_executor_result_for_task( - replacement, - brief, - mutation_events=[], - prior_ownership=prior, - repair_continuity=continuity, - review_repair_frontier=frontier, - ) - execution_context.validate_executor_result_for_task( - replacement, - brief, - mutation_events=[], - prior_ownership=prior, - repair_continuity=continuity, - review_repair_frontier=frontier, - authorized_replacements={brief["task_id"]}, - ) - - stale = deepcopy(continuity) - stale[brief["task_id"]]["baseline_identity"] = "stale" - with pytest.raises(SystemExit, match="continuity identities do not match"): - execution_context.validate_executor_result_for_task( - handoff, - brief, - mutation_events=[], - prior_ownership=prior, - repair_continuity=stale, - review_repair_frontier=frontier, - ) - - -def test_accepted_dependency_repair_chain_is_ordered_contiguous_and_last_wins( - tmp_path: Path, -) -> None: - root, _, _ = workspace(tmp_path) - dependency_path = "references/assets/orchestration/workflow.md" - dependency = root / dependency_path - dependency.parent.mkdir(parents=True, exist_ok=True) - dependency.write_text("version zero\n", encoding="utf-8") - git(root, "add", ".") - git(root, "commit", "-qm", "chain baseline") - commits = [git(root, "rev-parse", "HEAD")] - trees = [git(root, "rev-parse", "HEAD^{tree}")] - for number in range(1, 4): - dependency.write_text(f"version {number}\n", encoding="utf-8") - git(root, "add", dependency_path) - git(root, "commit", "-qm", f"accepted dependency repair {number}") - commits.append(git(root, "rev-parse", "HEAD")) - trees.append(git(root, "rev-parse", "HEAD^{tree}")) - unrelated = root / "tests/task-local.txt" - unrelated.parent.mkdir(parents=True, exist_ok=True) - unrelated.write_text("dependent task change\n", encoding="utf-8") - git(root, "add", str(unrelated.relative_to(root))) - git(root, "commit", "-qm", "dependent task change") - - task = { - "task_id": "dependent-task", - "plan_id": "plan-001", - "depends_on": ["task-dependency"], - "workspace": {"root": str(root)}, - } - handoff_root = root / ".work-bundle/orchestration/handoff/executor/active" - handoff_root.mkdir(parents=True, exist_ok=True) - - def identity(index: int) -> dict[str, object]: - return { - "artifact_id": "task-dependency", - "revision": commits[index], - "sha256": execution_context.semantic_digest( - {"commit": commits[index], "tree": trees[index]} - ), - "source_tree": trees[index], - } - - handoffs: list[dict[str, object]] = [] - descriptors: list[dict[str, object]] = [] - paths: list[Path] = [] - for index in range(1, 4): - handoff_id = f"handoff-chain-{index}" - handoff = { - "id": handoff_id, - "type": "executor-result", - "related": {"plan": "plan-001", "task": "task-dependency"}, - "result": {"state": "completed"}, - "acceptance_review": { - "required": True, - "verdict": "accept", - "review_mode": "repair", - "target_identity": identity(index), - "repair_frontier": { - "previous_reviewed_identity": identity(index - 1), - "repaired_identity": identity(index), - }, - }, - } - path = handoff_root / f"{handoff_id}.yaml" - path.write_text("\n".join(execution_context._dump_yaml(handoff)) + "\n", encoding="utf-8") - handoffs.append(handoff) - paths.append(path) - descriptors.append( - { - "task_id": "task-dependency", - "handoff_id": handoff_id, - "handoff_sha256": hashlib.sha256(path.read_bytes()).hexdigest(), - "integrated_base": commits[index - 1], - "integrated_head": commits[index], - } - ) - - assert execution_context._accepted_dependency_paths(task, root, descriptors) == { - dependency_path - } - - with pytest.raises(SystemExit, match="anchored|incomplete"): - execution_context._accepted_dependency_paths(task, root, descriptors[1:]) - with pytest.raises(SystemExit, match="anchored|incomplete"): - execution_context._accepted_dependency_paths(task, root, descriptors[-1:]) - - with pytest.raises(SystemExit, match="ordered|source.*contiguous"): - execution_context._accepted_dependency_paths( - task, root, [descriptors[1], descriptors[0], descriptors[2]] - ) - with pytest.raises(SystemExit, match="source.*contiguous|incomplete"): - execution_context._accepted_dependency_paths(task, root, [descriptors[0], descriptors[2]]) - nonadjacent = deepcopy(descriptors) - nonadjacent[1] = {**nonadjacent[1], "integrated_base": commits[0]} - with pytest.raises(SystemExit, match="integrated.*contiguous|non-adjacent"): - execution_context._accepted_dependency_paths(task, root, nonadjacent) - missing = deepcopy(descriptors) - missing[1] = {**missing[1], "handoff_id": "handoff-chain-missing"} - with pytest.raises(SystemExit, match="missing or ambiguous"): - execution_context._accepted_dependency_paths(task, root, missing) - - rejected = deepcopy(handoffs[1]) - rejected["acceptance_review"]["verdict"] = "pending" - paths[1].write_text( - "\n".join(execution_context._dump_yaml(rejected)) + "\n", encoding="utf-8" - ) - unaccepted = deepcopy(descriptors) - unaccepted[1] = { - **unaccepted[1], - "handoff_sha256": hashlib.sha256(paths[1].read_bytes()).hexdigest(), - } - with pytest.raises(SystemExit, match="not an accepted repair"): - execution_context._accepted_dependency_paths(task, root, unaccepted) - paths[1].write_text( - "\n".join(execution_context._dump_yaml(handoffs[1])) + "\n", encoding="utf-8" - ) - - dependency.write_text("unaccepted later mutation\n", encoding="utf-8") - with pytest.raises(SystemExit, match="changed after integration"): - execution_context._accepted_dependency_paths(task, root, descriptors) - - -def test_cumulative_accepted_result_delta_requires_complete_final_review_chain( - tmp_path: Path, -) -> None: - root, _, _ = workspace(tmp_path) - dependency_path = "references/assets/orchestration/workflow.md" - repair_path = "scripts/orchestration/dependency_repair.py" - final_path = "tests/dependency_result.md" - dependency = root / dependency_path - dependency.parent.mkdir(parents=True, exist_ok=True) - dependency.write_text("accepted base\n", encoding="utf-8") - git(root, "add", ".") - git(root, "commit", "-qm", "accepted base") - commits = [git(root, "rev-parse", "HEAD")] - trees = [git(root, "rev-parse", "HEAD^{tree}")] - for label, changed_path in ( - ("initial repair target", dependency_path), - ("accepted repair", repair_path), - ("fresh accepted result", final_path), - ): - changed = root / changed_path - changed.parent.mkdir(parents=True, exist_ok=True) - changed.write_text(label + "\n", encoding="utf-8") - git(root, "add", changed_path) - git(root, "commit", "-qm", label) - commits.append(git(root, "rev-parse", "HEAD")) - trees.append(git(root, "rev-parse", "HEAD^{tree}")) - local = root / "tests/task-local.txt" - local.parent.mkdir(parents=True, exist_ok=True) - local.write_text("dependent task\n", encoding="utf-8") - git(root, "add", str(local.relative_to(root))) - git(root, "commit", "-qm", "dependent task") - - def identity(index: int) -> dict[str, object]: - return { - "artifact_id": "task-dependency", - "revision": commits[index], - "sha256": execution_context.semantic_digest( - {"commit": commits[index], "tree": trees[index]} - ), - "source_tree": trees[index], - } - - def reviewer(agent: str) -> dict[str, object]: - return { - "agent_id": agent, - "capability": "judgment", - "authorship": "none", - "repair_participation": "none", - "decision_participation": "none", - "deliberation_participation": "none", - "context_origin": "direct_source", - } - - def review(review_id: str, index: int, verdict: str) -> dict[str, object]: - return { - "required": True, - "reviewer_independent": True, - "verdict": verdict, - "reviewed_head": commits[index], - "review_id": review_id, - "review_mode": "initial", - "review_target_kind": "task", - "repair_frontier": None, - "review_reset": None, - "target_identity": identity(index), - "reviewer": reviewer("reviewer-" + review_id), - "evidence": { - "mode": "direct", - "capabilities": ["source inspection"], - "unavailable_evidence": [], - "commands": [], - "artifacts": [], - }, - "findings": [], - "started_at": f"2026-09-06T00:0{index}:00Z", - "completed_at": f"2026-09-06T00:0{index}:30Z", - "staleness": {"is_stale": False, "reason": None, "supersedes": None}, - } - - base_review = review("review-base", 0, "accept") - initial_repair = review("review-initial-repair", 1, "repair") - initial_repair["review_reset"] = { - "prior_review_id": "review-base", - "reason_class": "validation_allocation", - "reason": "Validation authority changed.", - } - initial_repair["previous_review"] = base_review - initial_repair["findings"] = [{ - "finding_id": "CHAIN-FINDING", - "stage": "implementation", - "class": "implementation_defect", - "severity": "blocking", - "first_broken_artifact": "implementation", - "obligation_basis": "accepted_requirement", - "evidence": [{ - "kind": "test", "locator": "CHAIN", "digest_or_identity": "red", - "observation": "repair required", - }], - "target_identity": identity(1), - "summary": "Repair the chain.", - "recommended_owner": "task_owner", - "disposition": "repair_task", - }] - accepted_repair = review("review-accepted-repair", 2, "accept") - accepted_repair["review_mode"] = "repair" - accepted_repair["repair_frontier"] = { - "prior_review_id": "review-initial-repair", - "blocking_finding_ids": ["CHAIN-FINDING"], - "previous_reviewed_identity": identity(1), - "repaired_identity": identity(2), - "affected_boundaries": [dependency_path], - "frozen_evidence_reference": review_runtime.review_evidence_identity(initial_repair), - } - accepted_repair["previous_review"] = { - key: value for key, value in initial_repair.items() if key != "previous_review" - } - final_review = review("review-final", 3, "accept") - final_review["review_reset"] = { - "prior_review_id": "review-accepted-repair", - "reason_class": "validation_allocation", - "reason": "Cumulative result validation changed.", - } - final_review["previous_review"] = { - key: value for key, value in accepted_repair.items() if key != "previous_review" - } - - handoff_root = root / ".work-bundle/orchestration/handoff/executor/active" - handoff_root.mkdir(parents=True, exist_ok=True) - records = [ - ("handoff-base", base_review, "completed"), - ("handoff-initial-repair", initial_repair, "partial"), - ("handoff-accepted-repair", accepted_repair, "completed"), - ("handoff-final", final_review, "completed"), - ] - references: dict[str, dict[str, str]] = {} - for handoff_id, acceptance, state in records: - path = handoff_root / f"{handoff_id}.yaml" - document = { - "id": handoff_id, - "type": "executor-result", - "related": {"plan": "plan-001", "task": "task-dependency"}, - "result": {"state": state}, - "acceptance_review": acceptance, - } - rendered = "\n".join(execution_context._dump_yaml(document)) + "\n" - path.write_text(rendered.replace(": none\n", ': "none"\n'), encoding="utf-8") - references[handoff_id] = { - "handoff_id": handoff_id, - "handoff_sha256": hashlib.sha256(path.read_bytes()).hexdigest(), - } - descriptor = { - "task_id": "task-dependency", - "accepted_result_base": references["handoff-base"], - "review_chain": [ - references["handoff-initial-repair"], - references["handoff-accepted-repair"], - references["handoff-final"], - ], - "integrated_base": commits[0], - "integrated_head": commits[3], - } - task = { - "task_id": "dependent-task", - "plan_id": "plan-001", - "depends_on": ["task-dependency"], - "workspace": {"root": str(root)}, - } - - final_handoff_path = handoff_root / "handoff-final.yaml" - original_final_bytes = final_handoff_path.read_bytes() - same_reviewer_document = deepcopy(document) - same_reviewer_document["acceptance_review"]["reviewer"] = deepcopy( - accepted_repair["reviewer"] - ) - rendered = "\n".join(execution_context._dump_yaml(same_reviewer_document)) + "\n" - final_handoff_path.write_text( - rendered.replace(": none\n", ': "none"\n'), encoding="utf-8" - ) - same_reviewer = deepcopy(descriptor) - same_reviewer["review_chain"][-1]["handoff_sha256"] = hashlib.sha256( - final_handoff_path.read_bytes() - ).hexdigest() - assert execution_context._accepted_dependency_paths(task, root, [same_reviewer]) == { - dependency_path, repair_path, final_path - } - final_handoff_path.write_bytes(original_final_bytes) - - assert execution_context._accepted_dependency_paths(task, root, [descriptor]) == { - dependency_path, repair_path, final_path - } - missing = deepcopy(descriptor) - missing["review_chain"] = missing["review_chain"][1:] - with pytest.raises(SystemExit, match="chain|prior"): - execution_context._accepted_dependency_paths(task, root, [missing]) - reordered = deepcopy(descriptor) - reordered["review_chain"][0], reordered["review_chain"][1] = ( - reordered["review_chain"][1], reordered["review_chain"][0] - ) - with pytest.raises(SystemExit, match="chain|prior|ordered"): - execution_context._accepted_dependency_paths(task, root, [reordered]) - intermediate = deepcopy(descriptor) - intermediate["review_chain"] = intermediate["review_chain"][:1] - intermediate["integrated_head"] = commits[1] - with pytest.raises(SystemExit, match="final|accept"): - execution_context._accepted_dependency_paths(task, root, [intermediate]) - wrong_checkpoint = deepcopy(descriptor) - wrong_checkpoint["integrated_head"] = commits[2] - with pytest.raises(SystemExit, match="checkpoint|mismatched"): - execution_context._accepted_dependency_paths(task, root, [wrong_checkpoint]) - stale_identity = deepcopy(descriptor) - stale_identity["review_chain"][-1]["handoff_sha256"] = "0" * 64 - with pytest.raises(SystemExit, match="stale"): - execution_context._accepted_dependency_paths(task, root, [stale_identity]) - - commits.append(git(root, "rev-parse", "HEAD")) - trees.append(git(root, "rev-parse", "HEAD^{tree}")) - later_review = review("review-later", 4, "repair") - later_review["review_reset"] = { - "prior_review_id": "review-final", - "reason_class": "validation_allocation", - "reason": "A later accepted result superseded the terminal result.", - } - later_review["previous_review"] = { - key: value for key, value in final_review.items() if key != "previous_review" - } - later_review["findings"] = [deepcopy(initial_repair["findings"][0])] - later_review["findings"][0]["finding_id"] = "LATER-FINDING" - later_review["findings"][0]["target_identity"] = identity(4) - later_path = handoff_root / "handoff-later.yaml" - later_document = { - "id": "handoff-later", - "type": "executor-result", - "related": {"plan": "plan-001", "task": "task-dependency"}, - "result": {"state": "partial"}, - "acceptance_review": later_review, - } - rendered = "\n".join(execution_context._dump_yaml(later_document)) + "\n" - later_path.write_text(rendered.replace(": none\n", ': "none"\n'), encoding="utf-8") - with pytest.raises(SystemExit, match="terminal result is stale"): - execution_context._accepted_dependency_paths(task, root, [descriptor]) - - -def test_authority_recovery_receipt_is_helper_created_fresh_and_rechecked( - tmp_path: Path, -) -> None: - root, _, task_path = workspace(tmp_path) - dependency_path = "references/assets/orchestration/workflow.md" - dependency = root / dependency_path - dependency.parent.mkdir(parents=True, exist_ok=True) - dependency.write_text("original accepted baseline\n", encoding="utf-8") - git(root, "add", ".") - git(root, "commit", "-qm", "original dependency baseline") - baseline = git(root, "rev-parse", "HEAD") - baseline_tree = git(root, "rev-parse", "HEAD^{tree}") - - dependent = _compiled_brief(root, task_path) - dependency_brief = deepcopy(dependent) - dependency_brief["task_id"] = "task-dependency" - binding = _bind_task_execution( - root, - dependency_brief, - execution_id="dependency-exec", - write_scope=[dependency_path], - ) - binding_path = ( - root - / ".work-bundle/runtime/execution/plan-001/task-dependency/execution-binding.json" - ) - binding_digest = hashlib.sha256(binding_path.read_bytes()).hexdigest() - - dependency.write_text("missing accepted result base\n", encoding="utf-8") - git(root, "add", dependency_path) - git(root, "commit", "-qm", "missing accepted result base") - expected_base_head = git(root, "rev-parse", "HEAD") - expected_base_tree = git(root, "rev-parse", "HEAD^{tree}") - - dependency.write_text("fresh whole-task accepted result\n", encoding="utf-8") - git(root, "add", dependency_path) - git(root, "commit", "-qm", "fresh recovered dependency result") - recovered_head = git(root, "rev-parse", "HEAD") - recovered_tree = git(root, "rev-parse", "HEAD^{tree}") - - def identity(commit: str, tree: str) -> dict[str, object]: - return { - "artifact_id": "task-dependency", - "revision": commit, - "sha256": execution_context.semantic_digest({"commit": commit, "tree": tree}), - "source_tree": tree, - } - - def reviewer(agent_id: str) -> dict[str, object]: - return { - "agent_id": agent_id, - "capability": "judgment", - "authorship": "none", - "repair_participation": "none", - "decision_participation": "none", - "deliberation_participation": "none", - "context_origin": "direct_source", - } - - previous = { - "required": True, - "reviewer_independent": True, - "verdict": "accept", - "reviewed_head": expected_base_head, - "review_id": "review-dependency-accepted-historical", - "review_mode": "initial", - "review_target_kind": "task", - "repair_frontier": None, - "review_reset": None, - "target_identity": identity(expected_base_head, expected_base_tree), - "reviewer": reviewer("historical-reviewer"), - "evidence": { - "mode": "direct", - "capabilities": ["whole-task source review"], - "unavailable_evidence": [], - "commands": [], - "artifacts": [], - }, - "findings": [], - "started_at": "2026-09-06T01:00:00Z", - "completed_at": "2026-09-06T01:01:00Z", - "staleness": {"is_stale": False, "reason": None, "supersedes": None}, - } - recovered_review = { - **deepcopy(previous), - "review_id": "review-dependency-authority-recovery", - "reviewed_head": recovered_head, - "target_identity": identity(recovered_head, recovered_tree), - "reviewer": reviewer("fresh-recovery-reviewer"), - "review_reset": { - "prior_review_id": previous["review_id"], - "reason_class": "authority", - "reason": "The accepted-result handoff bytes are unavailable.", - }, - "previous_review": previous, - "started_at": "2026-09-06T01:02:00Z", - "completed_at": "2026-09-06T01:03:00Z", - } - recovered_handoff = { - "id": "handoff-recovered-dependency", - "type": "executor-result", - "status": "active", - "project": "fixture", - "created_at": "2026-09-06", - "updated_at": "2026-09-06", - "related": {"plan": "plan-001", "task": "task-dependency"}, - "result": {"state": "completed"}, - "delegation_evidence": { - "delegated": True, - "owner_kind": "subagent", - "agent_id": "dependency-owner", - "run_id": "dependency-run", - "mechanism": "host-native", - }, - "acceptance_review": recovered_review, - } - handoff_dir = root / ".work-bundle/orchestration/handoff/executor/active" - handoff_dir.mkdir(parents=True, exist_ok=True) - recovered_path = handoff_dir / "handoff-recovered-dependency.yaml" - index = root / ".work-bundle/orchestration/handoff/index.jsonl" - index.write_text("", encoding="utf-8") - receipt_reference = execution_context.create_accepted_base_absence_receipt( - root, - "plan-001", - "task-dependency", - expected_base_head, - expected_base_tree, - recovered_handoff["id"], - recovered_review["review_id"], - recovered_head, - recovered_tree, - ) - recovered_path.write_text( - ("\n".join(execution_context._dump_yaml(recovered_handoff)) + "\n").replace( - ": none\n", ': "none"\n' - ), - encoding="utf-8", - ) - index.write_text( - json.dumps({ - "id": recovered_handoff["id"], - "type": "executor-result", - "status": "active", - "path": str(recovered_path.relative_to(root)), - "related_plan": "plan-001", - "related_task": "task-dependency", - }) + "\n", - encoding="utf-8", - ) - - assert set(receipt_reference) == {"receipt_id", "receipt_sha256"} - receipt_path = execution_context._recovery_receipt_path( - root, "plan-001", "task-dependency", receipt_reference["receipt_id"] - ) - receipt = json.loads(receipt_path.read_text(encoding="utf-8")) - assert receipt["schema"] == "accepted-result-recovery-receipt-v1" - assert receipt["binding_id"] == binding["ownership"]["binding_id"] - assert receipt["binding_sha256"] == binding_digest - assert receipt["baseline_head"] == baseline - assert receipt["baseline_tree"] == baseline_tree - assert receipt["expected_base_query"]["result"] == "absent" - assert receipt["freshness"] == "current_validation_attempt" - assert receipt["proposed_recovered_result"] == { - "handoff_id": recovered_handoff["id"], - "review_id": recovered_review["review_id"], - "final_head": recovered_head, - "final_tree": recovered_tree, - } - - recovered_reference = { - "handoff_id": recovered_handoff["id"], - "handoff_sha256": hashlib.sha256(recovered_path.read_bytes()).hexdigest(), - } - descriptor = { - "task_id": "task-dependency", - "execution_baseline_recovery": { - "binding_id": binding["ownership"]["binding_id"], - "binding_sha256": binding_digest, - "baseline_head": baseline, - "baseline_tree": baseline_tree, - "recovery_receipt": receipt_reference, - }, - "recovered_result": recovered_reference, - "accepted_result_delta": { - "expected_base_head": expected_base_head, - "expected_base_tree": expected_base_tree, - "final_head": recovered_head, - "final_tree": recovered_tree, - }, - "integrated_base": expected_base_head, - "integrated_head": recovered_head, - } - dependent["depends_on"] = ["task-dependency"] - assert execution_context._accepted_dependency_paths(dependent, root, [descriptor]) == { - dependency_path - } - - empty_records: list[dict[str, str]] = [] - legacy_stores = { - status: { - "store_id": f"executor/{status}", - "records": empty_records, - "sha256": execution_context.semantic_digest(empty_records), - } - for status in ("active", "archived") - } - legacy_index = { - "index_id": "handoff/index.jsonl", - "path": ".work-bundle/orchestration/handoff/index.jsonl", - "sha256": hashlib.sha256(b"").hexdigest(), - "projected_entries_sha256": execution_context.semantic_digest([]), - } - legacy_revision = execution_context.semantic_digest({ - "expected_base_head": expected_base_head, - "expected_base_tree": expected_base_tree, - "handoff_stores": legacy_stores, - "handoff_index": legacy_index, - }) - legacy_observed_at = "2026-09-06T01:01:00Z" - legacy_receipt_id = ( - f"accepted-result-recovery-task-dependency-{legacy_revision[:12]}-" - f"{hashlib.sha256(legacy_observed_at.encode()).hexdigest()[:12]}" - ) - legacy_receipt = { - "receipt_id": legacy_receipt_id, - "schema": "accepted-result-recovery-receipt-v1", - "plan_id": "plan-001", - "task_id": "task-dependency", - "binding_id": binding["ownership"]["binding_id"], - "binding_sha256": binding_digest, - "baseline_head": baseline, - "baseline_tree": baseline_tree, - "expected_base_head": expected_base_head, - "expected_base_tree": expected_base_tree, - "queried_historical_revision": legacy_revision, - "handoff_stores": legacy_stores, - "handoff_index": legacy_index, - "absence_result": "accepted_base_absent", - "observed_at": legacy_observed_at, - "freshness": "current_validation_attempt", - } - legacy_receipt_path = execution_context._recovery_receipt_path( - root, "plan-001", "task-dependency", legacy_receipt_id - ) - legacy_receipt_path.write_text( - json.dumps(legacy_receipt, indent=2, sort_keys=True) + "\n", encoding="utf-8" - ) - legacy_reference = { - "receipt_id": legacy_receipt_id, - "receipt_sha256": hashlib.sha256(legacy_receipt_path.read_bytes()).hexdigest(), - } - handoff_bytes = recovered_path.read_bytes() - index_bytes = index.read_bytes() - adopted_reference = execution_context.adopt_existing_recovered_result( - root, - "plan-001", - "task-dependency", - expected_base_head, - expected_base_tree, - recovered_handoff["id"], - recovered_reference["handoff_sha256"], - recovered_review["review_id"], - recovered_head, - recovered_tree, - legacy_reference, - ) - assert recovered_path.read_bytes() == handoff_bytes - assert index.read_bytes() == index_bytes - adopted_descriptor = deepcopy(descriptor) - adopted_descriptor["execution_baseline_recovery"]["recovery_receipt"] = adopted_reference - assert execution_context._accepted_dependency_paths( - dependent, root, [adopted_descriptor] - ) == {dependency_path} - - assert recovered_path.read_bytes() == handoff_bytes - assert index.read_bytes() == index_bytes - - with pytest.raises(SystemExit, match="prior receipt.*missing"): - execution_context.adopt_existing_recovered_result( - root, - "plan-001", - "task-dependency", - expected_base_head, - expected_base_tree, - recovered_handoff["id"], - recovered_reference["handoff_sha256"], - recovered_review["review_id"], - recovered_head, - recovered_tree, - {"receipt_id": "missing-prior-receipt", "receipt_sha256": "0" * 64}, - ) - with pytest.raises(SystemExit, match="stale"): - execution_context.adopt_existing_recovered_result( - root, - "plan-001", - "task-dependency", - expected_base_head, - expected_base_tree, - recovered_handoff["id"], - "0" * 64, - recovered_review["review_id"], - recovered_head, - recovered_tree, - legacy_reference, - ) - - invalid_legacy = deepcopy(legacy_receipt) - invalid_legacy["baseline_head"] = expected_base_head - invalid_observed_at = "2026-09-06T01:01:01Z" - invalid_legacy["observed_at"] = invalid_observed_at - invalid_legacy_id = ( - f"accepted-result-recovery-task-dependency-{legacy_revision[:12]}-" - f"{hashlib.sha256(invalid_observed_at.encode()).hexdigest()[:12]}" - ) - invalid_legacy["receipt_id"] = invalid_legacy_id - invalid_legacy_path = execution_context._recovery_receipt_path( - root, "plan-001", "task-dependency", invalid_legacy_id - ) - invalid_legacy_path.write_text( - json.dumps(invalid_legacy, indent=2, sort_keys=True) + "\n", encoding="utf-8" - ) - with pytest.raises(SystemExit, match="prior receipt|baseline|non-global"): - execution_context.adopt_existing_recovered_result( - root, - "plan-001", - "task-dependency", - expected_base_head, - expected_base_tree, - recovered_handoff["id"], - recovered_reference["handoff_sha256"], - recovered_review["review_id"], - recovered_head, - recovered_tree, - { - "receipt_id": invalid_legacy_id, - "receipt_sha256": hashlib.sha256(invalid_legacy_path.read_bytes()).hexdigest(), - }, - ) - - wrong_delta_base = deepcopy(descriptor) - wrong_delta_base["accepted_result_delta"]["expected_base_head"] = baseline - wrong_delta_base["accepted_result_delta"]["expected_base_tree"] = baseline_tree - wrong_delta_base["integrated_base"] = baseline - with pytest.raises(SystemExit, match="accepted_result_delta.*mismatch"): - execution_context._accepted_dependency_paths(dependent, root, [wrong_delta_base]) - - caller_assertion = deepcopy(descriptor) - caller_assertion["execution_baseline_recovery"]["accepted_base_absent"] = True - with pytest.raises(SystemExit, match="closed|shape"): - execution_context._accepted_dependency_paths(dependent, root, [caller_assertion]) - - recovered_review_with_gap = deepcopy(recovered_review) - recovered_review_with_gap["evidence"]["unavailable_evidence"] = ["historical handoff"] - recovered_handoff_with_gap = deepcopy(recovered_handoff) - recovered_handoff_with_gap["acceptance_review"] = recovered_review_with_gap - recovered_path.write_text( - ("\n".join(execution_context._dump_yaml(recovered_handoff_with_gap)) + "\n").replace( - ": none\n", ': "none"\n' - ), - encoding="utf-8", - ) - with pytest.raises(SystemExit, match="unavailable_evidence|complete"): - execution_context._accepted_dependency_paths(dependent, root, [{ - **descriptor, - "recovered_result": { - **recovered_reference, - "handoff_sha256": hashlib.sha256(recovered_path.read_bytes()).hexdigest(), - }, - }]) - recovered_path.write_text( - ("\n".join(execution_context._dump_yaml(recovered_handoff)) + "\n").replace( - ": none\n", ': "none"\n' - ), - encoding="utf-8", - ) - - extra = handoff_dir / "unrelated-record.yaml" - extra.write_text( - "id: unrelated-record\ntype: executor-result\n" - "related: {plan: plan-other, task: task-other}\nresult: {state: partial}\n", - encoding="utf-8", - ) - index.write_text( - index.read_text(encoding="utf-8") - + json.dumps({"id": "unrelated-record", "related_plan": "plan-other", "related_task": "task-other"}) - + "\n", - encoding="utf-8", - ) - assert execution_context._accepted_dependency_paths(dependent, root, [descriptor]) == { - dependency_path - } - - competitor = deepcopy(recovered_handoff) - competitor["id"] = "handoff-competing-recovered-dependency" - competitor["acceptance_review"]["review_id"] = "review-competing-recovery" - competitor_path = handoff_dir / "handoff-competing-recovered-dependency.yaml" - competitor_path.write_text( - ("\n".join(execution_context._dump_yaml(competitor)) + "\n").replace( - ": none\n", ': "none"\n' - ), - encoding="utf-8", - ) - before_competitor_index = index.read_bytes() - index.write_text( - index.read_text(encoding="utf-8") - + json.dumps({ - "id": competitor["id"], - "type": "executor-result", - "status": "active", - "path": str(competitor_path.relative_to(root)), - "related_plan": "plan-001", - "related_task": "task-dependency", - }) - + "\n", - encoding="utf-8", - ) - with pytest.raises(SystemExit, match="competing recovered result"): - execution_context.adopt_existing_recovered_result( - root, - "plan-001", - "task-dependency", - expected_base_head, - expected_base_tree, - recovered_handoff["id"], - recovered_reference["handoff_sha256"], - recovered_review["review_id"], - recovered_head, - recovered_tree, - legacy_reference, - ) - competitor_path.unlink() - index.write_bytes(before_competitor_index) - - archived_dir = root / ".work-bundle/orchestration/handoff/executor/archived" - archived_dir.mkdir(parents=True, exist_ok=True) - expected_base_handoff = deepcopy(recovered_handoff) - expected_base_handoff["id"] = "handoff-restored-accepted-base" - expected_base_handoff["acceptance_review"] = previous - expected_base_path = archived_dir / "handoff-restored-accepted-base.yaml" - expected_base_path.write_text( - ("\n".join(execution_context._dump_yaml(expected_base_handoff)) + "\n").replace( - ": none\n", ': "none"\n' - ), - encoding="utf-8", - ) - with pytest.raises(SystemExit, match="recoverable accepted base"): - execution_context._accepted_dependency_paths(dependent, root, [descriptor]) - with pytest.raises(SystemExit, match="recoverable accepted base"): - execution_context.adopt_existing_recovered_result( - root, - "plan-001", - "task-dependency", - expected_base_head, - expected_base_tree, - recovered_handoff["id"], - recovered_reference["handoff_sha256"], - recovered_review["review_id"], - recovered_head, - recovered_tree, - legacy_reference, - ) - expected_base_path.unlink() - - duplicate_path = archived_dir / "handoff-recovered-dependency-duplicate.yaml" - duplicate_path.write_bytes(recovered_path.read_bytes()) - with pytest.raises(SystemExit, match="ambiguous"): - execution_context._accepted_dependency_paths(dependent, root, [descriptor]) - with pytest.raises(SystemExit, match="ambiguous"): - execution_context.adopt_existing_recovered_result( - root, - "plan-001", - "task-dependency", - expected_base_head, - expected_base_tree, - recovered_handoff["id"], - recovered_reference["handoff_sha256"], - recovered_review["review_id"], - recovered_head, - recovered_tree, - legacy_reference, - ) - duplicate_path.unlink() - - mismatched = deepcopy(recovered_handoff) - mismatched["acceptance_review"]["review_id"] = "different-proposed-review" - recovered_path.write_text( - ("\n".join(execution_context._dump_yaml(mismatched)) + "\n").replace( - ": none\n", ': "none"\n' - ), - encoding="utf-8", - ) - with pytest.raises(SystemExit, match="proposal|proposed|mismatch"): - execution_context._accepted_dependency_paths(dependent, root, [{ - **descriptor, - "recovered_result": { - **recovered_reference, - "handoff_sha256": hashlib.sha256(recovered_path.read_bytes()).hexdigest(), - }, - }]) - - -def test_authority_recovery_receipt_rejects_recoverable_accepted_base(tmp_path: Path) -> None: - root, _, task_path = workspace(tmp_path) - git(root, "add", ".") - git(root, "commit", "-qm", "baseline") - brief = _compiled_brief(root, task_path) - brief["task_id"] = "task-dependency" - _bind_task_execution(root, brief, execution_id="dependency-exec") - handoff_dir = root / ".work-bundle/orchestration/handoff/executor/archived" - handoff_dir.mkdir(parents=True, exist_ok=True) - commit = git(root, "rev-parse", "HEAD") - tree = git(root, "rev-parse", "HEAD^{tree}") - accepted = { - "id": "handoff-historical-accepted-base", - "type": "executor-result", - "related": {"plan": "plan-001", "task": "task-dependency"}, - "result": {"state": "completed"}, - "acceptance_review": { - "required": True, - "reviewer_independent": True, - "verdict": "accept", - "reviewed_head": commit, - "review_id": "review-historical-accepted-base", - "review_mode": "initial", - "review_target_kind": "task", - "repair_frontier": None, - "review_reset": None, - "target_identity": { - "artifact_id": "task-dependency", - "revision": commit, - "sha256": execution_context.semantic_digest({"commit": commit, "tree": tree}), - "source_tree": tree, - }, - "reviewer": { - "agent_id": "historical-reviewer", - "capability": "judgment", - "authorship": "none", - "repair_participation": "none", - "decision_participation": "none", - "deliberation_participation": "none", - "context_origin": "direct_source", - }, - "evidence": { - "mode": "direct", - "capabilities": ["whole-task source review"], - "unavailable_evidence": [], - "commands": [], - "artifacts": [], - }, - "findings": [], - "started_at": "2026-09-06T01:00:00Z", - "completed_at": "2026-09-06T01:01:00Z", - "staleness": {"is_stale": False, "reason": None, "supersedes": None}, - }, - } - accepted_path = handoff_dir / "handoff-historical-accepted-base.yaml" - accepted_path.write_text( - ("\n".join(execution_context._dump_yaml(accepted)) + "\n").replace( - ": none\n", ': "none"\n' - ), - encoding="utf-8", - ) - index = root / ".work-bundle/orchestration/handoff/index.jsonl" - index.write_text("", encoding="utf-8") - - with pytest.raises(SystemExit, match="recoverable accepted base"): - execution_context.create_accepted_base_absence_receipt( - root, - "plan-001", - "task-dependency", - commit, - tree, - "handoff-proposed-recovery", - "review-proposed-recovery", - commit, - tree, - ) - - distinct = root / "tests/distinct-expected-base.txt" - distinct.parent.mkdir(parents=True, exist_ok=True) - distinct.write_text("distinct expected identity\n", encoding="utf-8") - git(root, "add", str(distinct.relative_to(root))) - git(root, "commit", "-qm", "distinct expected base identity") - distinct_head = git(root, "rev-parse", "HEAD") - distinct_tree = git(root, "rev-parse", "HEAD^{tree}") - reference = execution_context.create_accepted_base_absence_receipt( - root, - "plan-001", - "task-dependency", - distinct_head, - distinct_tree, - "handoff-proposed-recovery", - "review-proposed-recovery", - distinct_head, - distinct_tree, - ) - receipt_path = execution_context._recovery_receipt_path( - root, "plan-001", "task-dependency", reference["receipt_id"] - ) - receipt = json.loads(receipt_path.read_text(encoding="utf-8")) - assert receipt["expected_base_head"] == distinct_head - assert receipt["expected_base_tree"] == distinct_tree - - -def test_rf_07_brief_rebuild_retains_original_execution_binding_and_baseline( - tmp_path: Path, -) -> None: - root, _, task = workspace(tmp_path) - scoped = _ensure_source_file(root) - git(root, "add", ".") - git(root, "commit", "-qm", "baseline") - brief = _compiled_brief(root, task) - original = _bind_task_execution(root, brief) - scoped.write_text("def compile_task():\n return 'repair'\n", encoding="utf-8") - git(root, "add", str(scoped.relative_to(root))) - git(root, "commit", "-qm", "repair") - - build_task_brief(args(root, task)) - retained = execution_context.capture_task_baseline_once( - execution_context.load_task_execution_binding(root, "plan-001", "task-004") - ) - - assert retained["ownership"]["binding_id"] == original["ownership"]["binding_id"] - assert retained["baseline"] == original["baseline"] - assert retained["baseline"]["head"] != git(root, "rev-parse", "HEAD") diff --git a/tests/test_orchestration_evidence_classification.py b/tests/test_orchestration_evidence_classification.py deleted file mode 100644 index 7b312a6..0000000 --- a/tests/test_orchestration_evidence_classification.py +++ /dev/null @@ -1,147 +0,0 @@ -from __future__ import annotations - -import subprocess -import sys -from pathlib import Path - -import pytest - - -REPO_ROOT = Path(__file__).resolve().parents[1] -ORCHESTRATION = REPO_ROOT / "scripts" / "orchestration" -sys.path.insert(0, str(ORCHESTRATION)) - -from evaluation_identity import ( # noqa: E402 - EvaluationIdentityError, - validation_interval_identity, -) -from execution_context import project_validation_evidence # noqa: E402 -from review_runtime import ( # noqa: E402 - ReviewContractError, - review_remains_current_after_observation, - validate_evidence_causal_classification, -) - - -CAUSAL_ROUTES = { - "claim_relevant_drift": ("claim-owner", "revalidate_claim", "route_current_owner", "claim_relevant"), - "implementation_defect": ("task-owner", "repair_task", "route_current_owner", "claim_relevant"), - "authority_plan_gap": ("plan-owner", "repair_authority_plan", "route_current_owner", "claim_relevant"), - "evaluator_control_defect": ("evaluator-owner", "repair_evaluator_control", "route_evaluator_control_owner", "evaluator_only"), - "non_claim_relevant": ("controller", "none", "diagnostic_only", "unrelated"), -} - - -def classification(causal_class: str, *, claim: str = "claim-001") -> dict[str, object]: - owner, action, disposition, comparison = CAUSAL_ROUTES[causal_class] - return { - "observation_reference": "observation-001", - "accepted_authority_comparison": { - "authority_identity": "authority-sha256:abc", - "result": comparison, - "basis": "Compared the observation with the accepted claim and validation allocation.", - }, - "causal_class": causal_class, - "affected_claim": claim, - "affected_owner": owner, - "authorized_lifecycle_action": action, - "disposition": disposition, - } - - -@pytest.mark.parametrize("causal_class", list(CAUSAL_ROUTES)) -def test_agent_owned_causal_classification_routes_all_five_classes(causal_class: str) -> None: - record = classification(causal_class) - validated = validate_evidence_causal_classification(record) - assert validated.causal_class == causal_class - assert validated.authorized_lifecycle_action == record["authorized_lifecycle_action"] - - -def test_raw_or_misrouted_evidence_cannot_manufacture_lifecycle_authority() -> None: - with pytest.raises(ReviewContractError, match="classification"): - validate_evidence_causal_classification({"observation_reference": "failed-test"}) - - wrong = classification("non_claim_relevant") - wrong["authorized_lifecycle_action"] = "repair_task" - with pytest.raises(ReviewContractError, match="action"): - validate_evidence_causal_classification(wrong) - - projected = project_validation_evidence( - [{"id": "VAL-001", "command": "false", "result": "failed"}], - evidence_capability={"invariants": []}, - ) - assert projected[0]["authority_effect"] == "observation_only" - assert projected[0]["lifecycle_action_authorized"] is False - - -@pytest.mark.parametrize("causal_class", ["evaluator_control_defect", "non_claim_relevant"]) -def test_unrelated_or_evaluator_observation_keeps_independent_review_current(causal_class: str) -> None: - assert review_remains_current_after_observation( - classification(causal_class), reviewed_claim="claim-001" - ) - - -def test_only_claim_relevant_current_owner_class_can_reopen_matching_review_claim() -> None: - assert not review_remains_current_after_observation( - classification("implementation_defect"), reviewed_claim="claim-001" - ) - assert review_remains_current_after_observation( - classification("implementation_defect", claim="different-claim"), - reviewed_claim="claim-001", - ) - - -def git(root: Path, *args: str) -> str: - return subprocess.run( - ["git", "-C", str(root), *args], text=True, capture_output=True, check=True - ).stdout.strip() - - -def commit(root: Path, name: str, content: str) -> str: - (root / name).write_text(content, encoding="utf-8") - git(root, "add", name) - git(root, "commit", "-qm", content) - return git(root, "rev-parse", "HEAD") - - -def test_historical_validation_stays_bound_to_frozen_endpoint_after_later_commits(tmp_path: Path) -> None: - git(tmp_path, "init", "-q") - git(tmp_path, "config", "user.name", "Test") - git(tmp_path, "config", "user.email", "test@example.com") - baseline = commit(tmp_path, "product.txt", "baseline") - endpoint = commit(tmp_path, "product.txt", "accepted endpoint") - manifest = tmp_path / "manifest.json" - manifest.write_text('{"scope":"accepted"}', encoding="utf-8") - - before = validation_interval_identity( - tmp_path, baseline_revision=baseline, endpoint_revision=endpoint, - endpoint_mode="frozen", manifest_path=manifest, - ) - commit(tmp_path, "later.txt", "future work") - after = validation_interval_identity( - tmp_path, baseline_revision=baseline, endpoint_revision=endpoint, - endpoint_mode="frozen", manifest_path=manifest, - ) - assert after == before - assert after["endpoint"]["revision"] == endpoint - - -def test_live_head_requires_explicit_current_contract(tmp_path: Path) -> None: - git(tmp_path, "init", "-q") - git(tmp_path, "config", "user.name", "Test") - git(tmp_path, "config", "user.email", "test@example.com") - baseline = commit(tmp_path, "product.txt", "baseline") - manifest = tmp_path / "manifest.json" - manifest.write_text("{}", encoding="utf-8") - - with pytest.raises(EvaluationIdentityError, match="live HEAD"): - validation_interval_identity( - tmp_path, baseline_revision=baseline, endpoint_revision="HEAD", - endpoint_mode="frozen", manifest_path=manifest, - ) - - current = validation_interval_identity( - tmp_path, baseline_revision=baseline, endpoint_revision="HEAD", - endpoint_mode="current", manifest_path=manifest, - ) - assert current["endpoint"]["revision"] == git(tmp_path, "rev-parse", "HEAD") diff --git a/tests/test_orchestration_execution_context.py b/tests/test_orchestration_execution_context.py deleted file mode 100644 index 235343c..0000000 --- a/tests/test_orchestration_execution_context.py +++ /dev/null @@ -1,4276 +0,0 @@ -from __future__ import annotations - -import argparse -import hashlib -import importlib.util -import json -import re -import shlex -import subprocess -import sys -from copy import deepcopy -from datetime import timedelta -from pathlib import Path - -import pytest -from reviewer_run_fixtures import bind_review_receipt - - -REPO_ROOT = Path(__file__).resolve().parents[1] -ORCHESTRATION = REPO_ROOT / "scripts" / "orchestration" -sys.path.insert(0, str(ORCHESTRATION)) - -import execution_context # noqa: E402 -from execution_context import build_review_package, build_task_brief # noqa: E402 - - -def test_product_review_candidate_excludes_handoff_and_publication_bookkeeping() -> None: - task = { - "task_id": "task-006", "plan_id": "plan-001", "goal": "Review product behavior", - "requirements": ["REQ-REV-004"], "constraints": [], - "truth_basis": {"purpose": "product review"}, - "semantic_authority": {"requirements": ["REQ-REV-004"]}, - "evidence_capability": {"mode": "direct"}, - "files": {"read": [], "write": ["src/a.py"]}, "interfaces": {}, - "allocated_rules": [], - "methodology": {"primary": "dev-test-driven-development", "skills": []}, - } - candidate = execution_context.build_product_review_candidate( - task=task, base="a" * 40, head="b" * 40, - diff="diff --git a/src/a.py b/src/a.py\n", changed_files=["M\tsrc/a.py"], - changed_symbols=["run"], - validation_observations=[{"id": "VAL-006", "result": "passed"}], - ) - encoded = json.dumps(candidate, sort_keys=True) - assert all(term not in encoded for term in ( - "handoff", "acceptance_review", "reviewer_run", "knowledge_disposition", - "methodology", "allocated_rules", "semantic_authority", "evidence_capability", - )) - assert candidate["task_authority"]["task_id"] == "task-006" - - -def test_creation_safe_projection_admits_before_review_and_rejects_review_facts() -> None: - task = { - "task_id": "task-001", - "plan_id": "plan-001", - "source_ids": [], - "truth_basis": {}, - "files": {"read": [], "write": []}, - "validation": [], - "evidence_capability": { - "result": "no_validation_bearing_obligation", - "reason": "No validation-bearing obligation.", - "invariants": [], - }, - "review_required": True, - } - handoff = { - "type": "executor-result", - "related": {"plan": "plan-001", "task": "task-001"}, - "result": {"state": "completed"}, - "task_fit_check": {"task": "task-001", "result": "clean"}, - "delegation_evidence": { - "delegated": True, - "owner_kind": "subagent", - "agent_id": "agent-001", - "run_id": "run-001", - "mechanism": "host-native", - }, - "knowledge_disposition": { - "action": "none", - "reason": "No stable authority changed.", - "affected_authority": [], - }, - } - - validated = execution_context.validate_executor_result_creation_for_task(handoff, task) - assert validated["result_state"] == "completed" - - with pytest.raises(SystemExit, match="wrong-owner field acceptance_review"): - execution_context.validate_executor_result_creation_for_task( - {**handoff, "acceptance_review": {"required": True, "verdict": "pending"}}, - task, - ) - - -def test_changed_path_scope_distinguishes_inspection_from_mutation() -> None: - files = {"read": ["docs/contract.md"], "write": ["src/runtime.py"]} - - execution_context._assert_changed_paths_in_write_scope( - {"changes": {"files": [{"path": "docs/contract.md", "action": "inspected"}]}}, - files, - ) - - with pytest.raises(SystemExit, match="write scope"): - execution_context._assert_changed_paths_in_write_scope( - {"changes": {"files": [{"path": "docs/contract.md", "action": "modified"}]}}, - files, - ) - with pytest.raises(SystemExit, match="inspection scope"): - execution_context._assert_changed_paths_in_write_scope( - {"changes": {"files": [{"path": "docs/unallocated.md", "action": "inspected"}]}}, - files, - ) - - -@pytest.mark.parametrize( - "file_entry", - [ - {"action": "inspected"}, - {"path": "", "action": "inspected"}, - ], -) -def test_changed_path_scope_rejects_missing_or_empty_paths(file_entry: dict[str, str]) -> None: - with pytest.raises(SystemExit, match="non-empty path"): - execution_context._assert_changed_paths_in_write_scope( - {"changes": {"files": [file_entry]}}, - {"read": ["docs/contract.md"], "write": ["src/runtime.py"]}, - ) - - -def _load_orchestration_dispatcher(): - path = ORCHESTRATION / "dispatcher.py" - spec = importlib.util.spec_from_file_location( - "test_orchestration_execution_context_dispatcher", path - ) - assert spec is not None and spec.loader is not None - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -orchestration_dispatcher = _load_orchestration_dispatcher() - - -def _counted_validation(tmp_path: Path, reuse_seconds: int = 3600, suffix: str = ""): - root, _, task = workspace(tmp_path) - _ensure_source_file(root) - counter = tmp_path / "invocations.txt" - script = f"from pathlib import Path; p=Path({str(counter)!r}); p.write_text(str(int(p.read_text())+1) if p.exists() else '1')" - command = f"{shlex.quote(sys.executable)} -c {shlex.quote(script + suffix)}" - _set_process_validation(task, command, reuse_seconds=reuse_seconds, - evidence_reuse={"environment_inputs": ["PYTHONHASHSEED"]}) - brief = _compiled_brief(root, task) - _bind_task_execution(root, brief) - handoff = _read_handoff(_handoff_for_command(root, command)) - return root, task, brief, handoff, counter - - -def test_handoff_validation_reuses_current_observation(tmp_path: Path) -> None: - _, _, brief, handoff, counter = _counted_validation(tmp_path) - first = _validate_observed(handoff, brief) - handoff["result"]["summary"] = "Reworded summary of the same result" - second = _validate_observed(handoff, brief) - assert counter.read_text() == "1" - assert first["observed_validation"][0]["observation_id"] == second["observed_validation"][0]["reuse_of"] - - -def test_terminal_observation_accepts_omitted_executor_corroboration(tmp_path: Path) -> None: - _, _, brief, handoff, counter = _counted_validation(tmp_path) - handoff.pop("validation") - - validated = _validate_observed(handoff, brief) - - assert validated["observed_validation"][0]["result"] == "passed" - assert counter.read_text() == "1" - - -def test_observe_task_validation_records_before_handoff(tmp_path: Path, capsys) -> None: - root, task, brief, handoff, counter = _counted_validation(tmp_path) - execution_context.cmd_observe_task_validation(args(root, task)) - assert json.loads(capsys.readouterr().out)["validation"][0]["result"] == "passed" - _validate_observed(handoff, brief) - assert counter.read_text() == "1" - - -def test_live_validation_is_captured_once_by_atomic_initial_acceptance( - tmp_path: Path, capsys, -) -> None: - root, task, brief, handoff, counter = _counted_validation( - tmp_path, reuse_seconds=0 - ) - - with pytest.raises(SystemExit, match="non-reusable.*validate-executor-result"): - execution_context.cmd_observe_task_validation(args(root, task)) - assert not counter.exists() - - validated = _validate_observed(handoff, brief) - observed = validated["observed_validation"][0] - assert observed["result"] == "passed" - assert observed["observation_id"].startswith("observation-") - assert counter.read_text() == "1" - accepted = execution_context.materialize_accepted_task_result( - root, brief, handoff, validated - ) - _, consumed = execution_context.load_current_accepted_task_result(root, brief) - assert consumed == accepted - assert accepted["validation_evidence_ids"] == [observed["observation_id"]] - assert counter.read_text() == "1" - - store = root / ".work-bundle/runtime/completion-provenance/completion-provenance-v1.json" - provenance = json.loads(store.read_text()) - saved = next( - item - for item in provenance["observations"] - if item["observation_id"] == observed["observation_id"] - ) - assert set(saved["result"]) == { - "exit_code", "stdout_digest", "stderr_digest", "started_at", "completed_at" - } - assert provenance["consumptions"][observed["observation_id"]].startswith( - "initial-acceptance:" - ) - with pytest.raises(SystemExit, match="already exists.*without rerunning"): - _validate_observed(handoff, brief) - assert counter.read_text() == "1" - - -def test_live_validation_allows_one_continuity_checked_source_repair( - tmp_path: Path, -) -> None: - root, _, brief, handoff, counter = _counted_validation( - tmp_path, reuse_seconds=0 - ) - validated = _validate_observed(handoff, brief) - execution_context.materialize_accepted_task_result(root, brief, handoff, validated) - execution_context.load_current_accepted_task_result(root, brief) - assert counter.read_text() == "1" - - source = root / WRITE_SCOPE_FILE - source.write_text(source.read_text() + "\n# scoped repair\n") - repaired = deepcopy(handoff) - repaired["task_fit_check"]["result"] = "repaired" - repaired["acceptance_review"] = { - "required": False, - "repair_frontier": { - "prior_review_id": "review-prior", - "frozen_evidence_reference": "evidence-prior", - }, - } - binding = execution_context.load_task_execution_binding( - root, str(brief["plan_id"]), str(brief["task_id"]) - ) - continuity = { - "binding_id": binding["ownership"]["binding_id"], - "baseline_identity": execution_context.semantic_digest(binding["baseline"]), - "evidence_identity": "evidence-prior", - "previous_review_identity": "review-prior", - } - with pytest.raises( - execution_context.AcceptanceOwnershipError, match="repair.*continuity" - ): - execution_context.validate_executor_result_for_task( - repaired, - brief, - observe=True, - mutation_events=[{"actor_kind": "subagent", "paths": [WRITE_SCOPE_FILE]}], - prior_ownership={str(brief["task_id"]): handoff["delegation_evidence"]}, - repair_continuity=None, - ) - assert counter.read_text() == "1" - - repaired_result = execution_context.validate_executor_result_for_task( - repaired, - brief, - observe=True, - mutation_events=[{"actor_kind": "subagent", "paths": [WRITE_SCOPE_FILE]}], - prior_ownership={str(brief["task_id"]): handoff["delegation_evidence"]}, - repair_continuity={str(brief["task_id"]): continuity}, - review_repair_frontier=repaired["acceptance_review"]["repair_frontier"], - ) - - assert repaired_result["result_state"] == "completed" - assert repaired_result["observed_validation"][0]["observation_id"] != validated[ - "observed_validation" - ][0]["observation_id"] - assert counter.read_text() == "2" - - -@pytest.mark.parametrize("change", ["source", "oracle", "environment"]) -def test_handoff_validation_invalidates_changed_inputs(tmp_path: Path, monkeypatch, change: str) -> None: - root, _, brief, handoff, counter = _counted_validation(tmp_path) - _validate_observed(handoff, brief) - if change == "source": - (root / WRITE_SCOPE_FILE).write_text("# changed implementation\n") - elif change == "oracle": - brief = deepcopy(brief) - brief["validation"][0]["proves"] = "Changed oracle obligation" - else: - monkeypatch.setenv("PYTHONHASHSEED", "42") - _validate_observed(handoff, brief) - assert counter.read_text() == "2" - - -def test_handoff_validation_live_check_retry_consumes_same_initial_observation( - tmp_path: Path, -) -> None: - _, _, brief, handoff, counter = _counted_validation(tmp_path, reuse_seconds=0) - first = _validate_observed(handoff, brief) - second = _validate_observed(handoff, brief) - assert counter.read_text() == "1" - assert first["observed_validation"][0]["observation_id"] == second[ - "observed_validation" - ][0]["reuse_of"] - - -def test_handoff_validation_expiry_requires_new_observation(tmp_path: Path, monkeypatch) -> None: - _, _, brief, handoff, counter = _counted_validation(tmp_path) - _validate_observed(handoff, brief) - cp = execution_context._completion_provenance_module() - clock = cp.datetime - class Later(clock): - @classmethod - def now(cls, tz=None): - return clock.now(tz) + timedelta(seconds=3601) - monkeypatch.setattr(cp, "datetime", Later) - _validate_observed(handoff, brief) - assert counter.read_text() == "2" - - -def test_handoff_validation_restored_identity_reuses_original(tmp_path: Path) -> None: - root, _, brief, handoff, counter = _counted_validation(tmp_path) - target = root / WRITE_SCOPE_FILE - original = target.read_text() - _validate_observed(handoff, brief) - target.write_text("# changed\n") - _validate_observed(handoff, brief) - target.write_text(original) - _validate_observed(handoff, brief) - assert counter.read_text() == "2" - - -def test_handoff_validation_ignores_undeclared_volatile_environment(tmp_path: Path, monkeypatch) -> None: - _, _, brief, handoff, counter = _counted_validation(tmp_path) - _validate_observed(handoff, brief) - monkeypatch.setenv("TMPDIR", "/some/other/transient/location") - monkeypatch.setenv("UNRELATED_REQUEST_ID", "another-invocation") - _validate_observed(handoff, brief) - assert counter.read_text() == "1" - - -def test_validation_uses_existing_completion_evidence_store(tmp_path: Path) -> None: - root, _, brief, handoff, _ = _counted_validation(tmp_path) - _validate_observed(handoff, brief) - store = root / ".work-bundle/runtime/completion-provenance/completion-provenance-v1.json" - assert len(json.loads(store.read_text())["observations"]) == 1 - - -@pytest.mark.parametrize("field", ["task", "knowledge", "closure", "result", "scope"]) -def test_handoff_reuse_keeps_structural_checks(tmp_path: Path, field: str) -> None: - _, _, brief, handoff, counter = _counted_validation(tmp_path) - if field == "closure": - mapped, claims, _, _ = evidence_closure_fixture() - brief["evidence_capability"] = mapped["evidence_capability"] - brief["validation"][0].update(id="VAL-001", invariant_ids=["INV-001"]) - handoff["validation"]["commands"][0].update(id="VAL-001", invariant_ids=["INV-001"]) - handoff.update(claims) - _validate_observed(handoff, brief) - if field == "task": - handoff["related"]["task"] = "wrong-task" - elif field == "knowledge": - handoff["knowledge_disposition"]["action"] = "invented" - elif field == "closure": - handoff["evidence_closure"] = {"result": "passed", "invariants": []} - elif field == "result": - handoff["validation"]["commands"][0]["result"] = "invented" - else: - handoff["changes"] = {"files": [{"path": "unauthorized.py", "action": "modified"}]} - with pytest.raises(SystemExit): - _validate_observed(handoff, brief) - assert counter.read_text() == "1" - - -@pytest.mark.parametrize("component", ["os", "architecture"]) -def test_handoff_reuse_keeps_platform_identities_distinct(tmp_path: Path, monkeypatch, component: str) -> None: - _, _, brief, handoff, counter = _counted_validation(tmp_path) - _validate_observed(handoff, brief) - cp = execution_context._completion_provenance_module() - monkeypatch.setattr(cp.platform, "system" if component == "os" else "machine", lambda: "different-platform") - _validate_observed(handoff, brief) - assert counter.read_text() == "2" - - -@pytest.mark.parametrize("field", ["id", "expected", "acceptable_results", "invariant_ids", "command"]) -def test_handoff_reuse_covers_semantic_check_fields(tmp_path: Path, field: str) -> None: - _, _, brief, handoff, counter = _counted_validation(tmp_path) - _validate_observed(handoff, brief) - item = brief["validation"][0] - reported = handoff["validation"]["commands"][0] - changed = {"id": "VAL-OTHER", "expected": "pass", "acceptable_results": ["passed", "failed"], - "invariant_ids": ["INV-OTHER"], "command": item["command"] + "; true"} - item[field] = changed[field] - if field in {"id", "invariant_ids", "command"}: - reported[field] = changed[field] - _validate_observed(handoff, brief) - assert counter.read_text() == "2" - - -def test_handoff_reuse_checks_binding_on_every_call(tmp_path: Path) -> None: - _, _, brief, handoff, counter = _counted_validation(tmp_path) - _validate_observed(handoff, brief) - with pytest.raises(SystemExit, match="execution_id mismatch"): - execution_context.validate_executor_result_for_task(handoff, brief, observe=True, execution_id="other-execution") - assert counter.read_text() == "1" - - -def test_handoff_receipts_do_not_self_invalidate_validation(tmp_path: Path) -> None: - root, _, brief, handoff, counter = _counted_validation(tmp_path) - first = _validate_observed(handoff, brief) - receipt = root / ".work-bundle/runtime/validation-summary.json" - receipt.write_text('{"summary":"another observation artifact"}') - second = _validate_observed(handoff, brief) - assert counter.read_text() == "1" - assert second["observed_validation"][0]["reuse_of"] == first["observed_validation"][0]["observation_id"] - - -def test_handoff_reuse_distinguishes_process_from_inspection(tmp_path: Path) -> None: - root, _, brief, handoff, counter = _counted_validation(tmp_path) - first = _validate_observed(handoff, brief) - brief["validation"][0].update(kind="inspection", mechanism="named-harness-file-digest", - digest=execution_context._write_scope_file_digest(root, brief)) - handoff["validation"]["commands"][0]["mechanism"] = "named-harness-file-digest" - second = _validate_observed(handoff, brief) - assert second["observed_validation"][0]["observation_id"] != first["observed_validation"][0]["observation_id"] - assert second["observed_validation"][0]["kind"] == "inspection" - assert counter.read_text() == "1" - - -def test_skipped_observation_is_not_reusable(tmp_path: Path) -> None: - _, _, brief, handoff, counter = _counted_validation(tmp_path) - brief["validation"][0].update(expected="skipped", acceptable_results=["skipped"]) - handoff["validation"]["commands"][0]["result"] = "skipped" - for _ in range(2): - result = _validate_observed(handoff, brief) - assert "observation_id" not in result["observed_validation"][0] - assert not counter.exists() - - -def test_failed_observation_is_reexecuted_after_source_repair(tmp_path: Path) -> None: - suffix = f"; raise SystemExit('broken' in Path({WRITE_SCOPE_FILE!r}).read_text())" - root, _, brief, handoff, counter = _counted_validation(tmp_path, suffix=suffix) - (root / WRITE_SCOPE_FILE).write_text("broken") - with pytest.raises(SystemExit, match="does not match observed failed"): - _validate_observed(handoff, brief) - (root / WRITE_SCOPE_FILE).write_text("repaired") - _validate_observed(handoff, brief) - _validate_observed(handoff, brief) - assert counter.read_text() == "2" - - -@pytest.mark.parametrize("mutation", [False, True]) -def test_handoff_validation_failed_checks_are_not_cached(tmp_path: Path, mutation: bool) -> None: - suffix = f"; Path({WRITE_SCOPE_FILE!r}).write_text('changed')" if mutation else "; raise SystemExit(1)" - root, _, brief, handoff, counter = _counted_validation(tmp_path, suffix=suffix) - original = (root / WRITE_SCOPE_FILE).read_text() - for _ in range(2): - with pytest.raises(SystemExit, match="validation-blocked|does not match observed failed"): - _validate_observed(handoff, brief) - (root / WRITE_SCOPE_FILE).write_text(original) - assert counter.read_text() == "2" - - -@pytest.mark.parametrize("seconds", [-1, True, 1.5, "3600", 86401]) -def test_validation_reuse_rejects_invalid_policy(seconds) -> None: - with pytest.raises(SystemExit, match="reuse_seconds"): - execution_context._compile_structured_validation_item({"kind": "process", "reuse_seconds": seconds}) - - -def test_observe_task_validation_is_dispatched() -> None: - completed = subprocess.run([sys.executable, str(REPO_ROOT / "scripts/orch.py"), "observe-task-validation", "--help"], capture_output=True, text=True) - assert completed.returncode == 0, completed.stderr - assert "--task" in completed.stdout - - -@pytest.mark.parametrize("command", ["validate-executor-result", "set-plan-status"]) -def test_normal_cli_projects_controller_mutation_events_into_acceptance( - tmp_path: Path, command: str -) -> None: - root, task, brief, handoff, _ = _counted_validation(tmp_path) - handoff_path = root / ".work-bundle/orchestration/handoff/executor/active/handoff-task-004.yaml" - handoff_path.write_text( - "\n".join(execution_context._dump_yaml(handoff)) + "\n", encoding="utf-8" - ) - runtime = json.dumps( - [{"actor_kind": "controller", "paths": [brief["files"]["write"][0]]}] - ) - if command == "validate-executor-result": - operation = [command, "--task", str(task), "--handoff", str(handoff_path)] - else: - operation = [ - command, - "--id", - "task-004", - "--kind", - "task", - "--plan-id", - "plan-001", - "--status", - "Completed", - "--handoff", - str(handoff_path), - ] - parsed = orchestration_dispatcher.build_parser().parse_args( - [*operation, "--project-root", str(root), "--mutation-events", runtime] - ) - with pytest.raises(SystemExit, match="controller mutated task-owned implementation scope"): - parsed.func(parsed) - task_data, _ = execution_context._read_structured(task) - assert task_data.get("status") != "Completed" - - -@pytest.mark.parametrize("command", ["validate-executor-result", "set-plan-status"]) -def test_completed_task_cli_rejects_missing_controller_mutation_evidence( - tmp_path: Path, command: str -) -> None: - root, task, _, handoff, _ = _counted_validation(tmp_path) - handoff_path = root / ".work-bundle/orchestration/handoff/executor/active/handoff-task-004.yaml" - handoff_path.write_text( - "\n".join(execution_context._dump_yaml(handoff)) + "\n", encoding="utf-8" - ) - if command == "validate-executor-result": - operation = [command, "--task", str(task), "--handoff", str(handoff_path)] - else: - operation = [ - command, - "--id", - "task-004", - "--kind", - "task", - "--plan-id", - "plan-001", - "--status", - "Completed", - "--handoff", - str(handoff_path), - ] - parsed = orchestration_dispatcher.build_parser().parse_args( - [*operation, "--project-root", str(root)] - ) - with pytest.raises(SystemExit, match="mutation.*evidence|mutation_events"): - parsed.func(parsed) - task_data, _ = execution_context._read_structured(task) - assert task_data.get("status") != "Completed" - - -@pytest.mark.parametrize("command", ["validate-executor-result", "set-plan-status"]) -def test_completed_task_cli_accepts_complete_no_controller_mutation_evidence( - tmp_path: Path, command: str -) -> None: - root, task, _, handoff, _ = _counted_validation(tmp_path) - handoff_path = root / ".work-bundle/orchestration/handoff/executor/active/handoff-task-004.yaml" - handoff_path.write_text( - "\n".join(execution_context._dump_yaml(handoff)) + "\n", encoding="utf-8" - ) - if command == "validate-executor-result": - operation = [command, "--task", str(task), "--handoff", str(handoff_path)] - else: - operation = [ - command, - "--id", - "task-004", - "--kind", - "task", - "--plan-id", - "plan-001", - "--status", - "Completed", - "--handoff", - str(handoff_path), - ] - parsed = orchestration_dispatcher.build_parser().parse_args( - [*operation, "--project-root", str(root), "--mutation-events", "[]"] - ) - parsed.func(parsed) - if command == "set-plan-status": - task_data, _ = execution_context._read_structured(task) - assert task_data["status"] == "Completed" - - -@pytest.mark.parametrize( - "command", ["validate-executor-result", "set-plan-status", "build-review-package"] -) -def test_acceptance_cli_projects_all_controller_owned_runtime_inputs(command: str) -> None: - expected = { - "mutation_events": [{"actor_kind": "controller", "paths": ["src/a.py"]}], - "accepted_dependency_deltas": [{"task_id": "task-a"}], - "prior_ownership": {"task-b": {"agent_id": "agent-a"}}, - "repair_continuity": {"task-b": {"binding_id": "binding-b"}}, - "authorized_replacements": ["task-b"], - } - if command == "validate-executor-result": - operation = [command, "--task", "task.md", "--handoff", "handoff.yaml"] - elif command == "set-plan-status": - operation = [command, "--id", "task-b", "--status", "Completed"] - else: - operation = [ - command, - "--task", - "task.md", - "--handoff", - "handoff.yaml", - "--base", - "base", - "--head", - "head", - ] - argv = list(operation) - for name, value in expected.items(): - argv.extend(["--" + name.replace("_", "-"), json.dumps(value)]) - - parsed = orchestration_dispatcher.build_parser().parse_args(argv) - - projected = execution_context._observation_kwargs(parsed) - assert {name: projected[name] for name in expected} == expected - - -def test_source_records_keep_letter_suffixed_ids_distinct(tmp_path: Path) -> None: - specification = tmp_path / "spec.md" - body = ( - "### API-003 — Descriptor and pagination\n" - "### API-003A — Closed resource identity inventory\n" - "- **AC-005A:** Closed applicability bindings.\n" - "- **AC-005B**: Legacy outside-colon form.\n" - "- **REQ-001 — Repair first:** Repair the prerelease schema.\n" - "**DELTA-017 — Accepted resolution:** Repair in place.\n" - "| AC-005C | Closed skill-to-rule edge inventory |\n" - ) - - records = execution_context._source_records(specification, body) - - assert records == { - "API-003": "Descriptor and pagination", - "API-003A": "Closed resource identity inventory", - "AC-005A": "Closed applicability bindings.", - "AC-005B": "Legacy outside-colon form.", - "REQ-001": "Repair first: Repair the prerelease schema.", - "DELTA-017": "Accepted resolution: Repair in place.", - "AC-005C": "Closed skill-to-rule edge inventory", - } - - -def test_source_records_reject_malformed_suffixes_and_colonless_bullets(tmp_path: Path) -> None: - specification = tmp_path / "spec.md" - body = ( - "### API-003 — Descriptor and pagination\n" - "### API-003a — Lowercase suffix must not alias.\n" - "- **REQ-001** prose without a delimiter\n" - ) - - assert execution_context._source_records(specification, body) == { - "API-003": "Descriptor and pagination", - } - assert execution_context.SOURCE_ID_RE.fullmatch("API-003A") - assert not execution_context.SOURCE_ID_RE.fullmatch("API-003a") - assert not execution_context.SOURCE_ID_RE.fullmatch("API-003-A") - assert not execution_context.SOURCE_ID_RE.fullmatch("API-003AB") - - -def test_source_records_keep_compound_ids_and_parse_structured_contract_blocks(tmp_path: Path) -> None: - specification = tmp_path / "spec.md" - body = ( - "- **DEC-101-001**: Stable feature identity.\n" - "- **DEC-101-002**: Stable feature metadata.\n" - "```yaml\n" - "api_contracts:\n" - " API-005:\n" - " schema: observation_identity_v1\n" - " required: [observation_id, mutation_epoch]\n" - " API-006:\n" - " schema: execution_binding_ownership_v1\n" - " required: [binding_id, current_owner]\n" - "```\n" - ) - - records = execution_context._source_records(specification, body) - - assert records["DEC-101-001"] == "Stable feature identity." - assert records["DEC-101-002"] == "Stable feature metadata." - assert "observation_identity_v1" in records["API-005"] - assert "mutation_epoch" in records["API-005"] - assert "execution_binding_ownership_v1" in records["API-006"] - assert "current_owner" in records["API-006"] - assert "DEC-101" not in records - - -def test_compile_evidence_capability_maps_stable_task_local_invariants() -> None: - validation = [{"id": "VAL-001", "invariant_ids": ["INV-001"], "capability_reason": "Observes violation."}] - task = {"evidence_capability": {"result": "mapped", "reason": "Required.", "invariants": [{"id": "INV-001", "source_ids": ["REQ-001"], "invariant": "Observable behavior", "boundary": "unit", "oracle": "VAL-001", "capability_reason": "Unit oracle distinguishes violation.", "freshness": "current_task_batch", "task_id": "task-001", "evidence_ids": ["VAL-001"], "closure_result": "pending"}]}} - result = execution_context._compile_evidence_capability(task, "task-001", ["REQ-001"], validation) - assert result is not None and result["invariants"][0]["id"] == "INV-001" - - -def test_compile_evidence_capability_requires_closure_result() -> None: - validation = [{"id": "VAL-001", "invariant_ids": ["INV-001"], "capability_reason": "Observes violation."}] - invariant = {"id": "INV-001", "source_ids": ["REQ-001"], "invariant": "Observable behavior", "boundary": "unit", "oracle": "VAL-001", "capability_reason": "Unit oracle distinguishes violation.", "freshness": "current_task_batch", "task_id": "task-001", "evidence_ids": ["VAL-001"]} - task = {"evidence_capability": {"result": "mapped", "reason": "Required.", "invariants": [invariant]}} - with pytest.raises(SystemExit, match="closure_result"): - execution_context._compile_evidence_capability(task, "task-001", ["REQ-001"], validation) - - -def test_compile_evidence_capability_rejects_preclosed_invariant() -> None: - validation = [{"id": "VAL-001", "invariant_ids": ["INV-001"], "capability_reason": "Observes violation."}] - invariant = {"id": "INV-001", "source_ids": ["REQ-001"], "invariant": "Observable behavior", "boundary": "unit", "oracle": "VAL-001", "capability_reason": "Unit oracle distinguishes violation.", "freshness": "current_task_batch", "task_id": "task-001", "evidence_ids": ["VAL-001"], "closure_result": "passed"} - task = {"evidence_capability": {"result": "mapped", "reason": "Required.", "invariants": [invariant]}} - with pytest.raises(SystemExit, match="initialized to pending"): - execution_context._compile_evidence_capability(task, "task-001", ["REQ-001"], validation) - - -def test_compile_evidence_capability_requires_explicit_result() -> None: - with pytest.raises(SystemExit, match="required"): - execution_context._compile_evidence_capability({}, "task-001", ["REQ-001"], []) - - -def test_compile_evidence_capability_allows_agent_decided_bookkeeping_empty_map() -> None: - task = {"evidence_capability": {"result": "no_validation_bearing_obligation", "reason": "Accepted IDs are bookkeeping-only and make no closure claim.", "invariants": []}} - result = execution_context._compile_evidence_capability(task, "task-001", ["REQ-001"], []) - assert result is not None and result["invariants"] == [] - - -def evidence_closure_fixture(*, boundary: str = "component", result: str = "passed") -> tuple[dict, dict, dict, list[dict]]: - task = { - "task_id": "task-001", - "validation": [{"id": "VAL-001", "invariant_ids": ["INV-001"], "command": "true"}], - "evidence_capability": { - "result": "mapped", - "reason": "Required.", - "invariants": [{"id": "INV-001", "boundary": "component", "freshness": "current_task_batch", "evidence_ids": ["VAL-001"], "closure_result": "pending"}], - }, - } - handoff = { - "evidence_closure": { - "result": result, - "invariants": [{"id": "INV-001", "boundary": boundary, "freshness": "current_task_batch", "evidence_ids": ["VAL-001"], "closure_result": result, "repair_owner": None}], - } - } - reported = {"true": {"command": "true", "id": "VAL-001", "invariant_ids": ["INV-001"], "result": "passed"}} - observed = [{"command": "true", "id": "VAL-001", "invariant_ids": ["INV-001"], "result": "passed", "kind": "process"}] - return task, handoff, reported, observed - - -def test_evidence_closure_requires_mapped_terminal_record() -> None: - task, _, reported, observed = evidence_closure_fixture() - with pytest.raises(SystemExit, match="missing evidence_closure"): - execution_context._validate_evidence_closure({}, task, "completed", reported, observed) - - -@pytest.mark.parametrize( - ("closure_result", "repair_owner"), - [ - ("incapable", "plan"), - ("contradictory", "specification"), - ("stale", "task"), - ("wrong_boundary", "plan"), - ("failed", "task"), - ("missing", "plan"), - ("unexecuted", "task"), - ], -) -def test_evidence_closure_blocks_negative_results_and_routes_owner( - closure_result: str, repair_owner: str -) -> None: - task, handoff, reported, observed = evidence_closure_fixture(result=closure_result) - handoff["evidence_closure"]["invariants"][0]["repair_owner"] = repair_owner - with pytest.raises(SystemExit, match=f"{closure_result}.*{repair_owner}"): - execution_context._validate_evidence_closure(handoff, task, "completed", reported, observed) - - -def test_evidence_closure_rejects_wrong_boundary() -> None: - task, handoff, reported, observed = evidence_closure_fixture(boundary="unit") - with pytest.raises(SystemExit, match="wrong-boundary"): - execution_context._validate_evidence_closure(handoff, task, "completed", reported, observed) - - -def test_evidence_closure_accepts_capable_component_without_ui_gate() -> None: - task, handoff, reported, observed = evidence_closure_fixture() - result = execution_context._validate_evidence_closure(handoff, task, "completed", reported, observed) - assert result["result"] == "passed" - - -def test_evidence_closure_rejects_passed_executor_claim_without_harness_observation() -> None: - task, handoff, reported, _ = evidence_closure_fixture() - with pytest.raises(SystemExit, match="independent harness observation"): - execution_context._validate_evidence_closure(handoff, task, "completed", reported, None) - - -@pytest.mark.parametrize( - ("closure_result", "wrong_owner", "expected_owner"), - [("incapable", "task", "plan"), ("contradictory", "task", "specification")], -) -def test_evidence_closure_rejects_incorrect_first_repair_owner( - closure_result: str, wrong_owner: str, expected_owner: str -) -> None: - task, handoff, reported, observed = evidence_closure_fixture(result=closure_result) - handoff["evidence_closure"]["invariants"][0]["repair_owner"] = wrong_owner - with pytest.raises(SystemExit, match=f"must route {expected_owner}"): - execution_context._validate_evidence_closure(handoff, task, "completed", reported, observed) - - -def test_evidence_closure_rejects_executor_report_without_allocated_identity() -> None: - task, handoff, reported, observed = evidence_closure_fixture() - reported["true"].pop("id") - with pytest.raises(SystemExit, match="reported evidence identity"): - execution_context._validate_evidence_closure(handoff, task, "completed", reported, observed) - - -def test_evidence_closure_rejects_harness_observation_without_allocated_identity() -> None: - task, handoff, reported, observed = evidence_closure_fixture() - observed[0].pop("id") - with pytest.raises(SystemExit, match="harness evidence"): - execution_context._validate_evidence_closure(handoff, task, "completed", reported, observed) - - -def _sparse_mapped_creation_fixture() -> tuple[dict, dict, list[dict]]: - task, handoff, _, observed = evidence_closure_fixture() - task.update( - { - "plan_id": "plan-001", - "source_ids": [], - "files": {"read": [], "write": []}, - "truth_basis": {}, - "review_required": False, - "evidence_applicability": { - "metadata": {"required": False, "reasons": []}, - "repository": {"required": False, "reasons": []}, - "codegraph": {"required": False, "reasons": []}, - }, - } - ) - handoff.update( - { - "type": "executor-result", - "related": {"plan": "plan-001", "task": "task-001"}, - "result": {"state": "completed"}, - "task_fit_check": {"task": "task-001", "result": "clean"}, - "delegation_evidence": { - "delegated": True, - "owner_kind": "subagent", - "agent_id": "agent-001", - "run_id": "run-001", - "mechanism": "host-native", - }, - "knowledge_disposition": { - "action": "none", - "reason": "No stable authority changed.", - "affected_authority": [], - }, - } - ) - return task, handoff, observed - - -def test_creation_safe_projection_admits_unreported_controller_final_validation() -> None: - task, handoff, observed = _sparse_mapped_creation_fixture() - - created = execution_context.validate_executor_result_creation_for_task(handoff, task) - assert created["result_state"] == "completed" - - with pytest.raises(SystemExit, match="independent harness observation"): - execution_context.validate_executor_result_for_task( - handoff, task, observe=False, mutation_events=[] - ) - terminal_closure = execution_context._validate_evidence_closure( - handoff, task, "completed", {}, observed - ) - assert terminal_closure["result"] == "passed" - - -def test_creation_safe_projection_rejects_malformed_optional_executor_report() -> None: - task, handoff, _ = _sparse_mapped_creation_fixture() - handoff["validation"] = { - "commands": [{"command": "focused-test", "result": "invented"}] - } - - with pytest.raises(SystemExit, match="reported validation result"): - execution_context.validate_executor_result_creation_for_task(handoff, task) - - -def test_completed_mapped_result_requires_produced_observation_batch() -> None: - task, handoff, _, _ = evidence_closure_fixture() - task.update( - { - "plan_id": "plan-001", - "source_ids": [], - "files": {"read": [], "write": []}, - "truth_basis": {}, - "validation": [], - "review_required": False, - "evidence_applicability": { - "metadata": {"required": False, "reasons": []}, - "repository": {"required": False, "reasons": []}, - "codegraph": {"required": False, "reasons": []}, - }, - } - ) - handoff.update( - { - "type": "executor-result", - "related": {"plan": "plan-001", "task": "task-001"}, - "result": {"state": "completed"}, - "task_fit_check": {"task": "task-001", "result": "clean"}, - "knowledge_disposition": {"action": "none", "reason": "No stable authority changed.", "affected_authority": []}, - } - ) - with pytest.raises(SystemExit, match="produced harness observations"): - execution_context.validate_executor_result_for_task(handoff, task, observe=True) - - -ACCEPTED_AUTHORITY_PATH = ".work-bundle/knowledge/notes/accepted-authority.md" -ACCEPTED_AUTHORITY = "AUTH-001" -ACCEPTED_CONSTRAINT = "Executors must not retrieve durable knowledge to reconstruct authority." -DECOY_KNOWLEDGE = "This decoy note must never appear in compiled authority." - - -def git(path: Path, *args: str) -> str: - result = subprocess.run(["git", "-C", str(path), *args], check=True, capture_output=True, text=True) - return result.stdout.strip() - - -def workspace(tmp_path: Path) -> tuple[Path, Path, Path]: - root = tmp_path / "workspace" - root.mkdir() - git(root, "init", "-q", "-b", "main") - git(root, "config", "user.email", "test@example.com") - git(root, "config", "user.name", "Test") - metadata = root / ".work-bundle" / "project.yaml" - metadata.parent.mkdir(parents=True) - metadata.write_text( - "metadata_version: 3\n" - f"workspace_root: {root}\n" - "workspace_mode: single-repository\n", - encoding="utf-8", - ) - - spec = root / ".work-bundle/orchestration/spec/active/spec-001.md" - spec.parent.mkdir(parents=True) - spec.write_text( - "---\n" - "id: spec-001\n" - "status: verified\n" - "source_knowledge:\n" - f" - path: {ACCEPTED_AUTHORITY_PATH}\n" - f" constraint: {ACCEPTED_CONSTRAINT}\n" - "---\n\n" - "# Compiler contract\n\n" - "- **REQ-003**: Retry exactly three times before returning failure.\n" - "- **CON-002**: Never write outside the assigned files.\n" - "- **API-002**: `compile_task(task: Path) -> dict[str, object]`\n" - "- **TEST-004**: Focused pytest exits with status 0.\n", - encoding="utf-8", - ) - - plan = root / ".work-bundle/orchestration/plan/active/compiler-plan.md" - plan.parent.mkdir(parents=True) - plan.write_text( - "---\n" - "id: plan-001\n" - "source_spec: [.work-bundle/orchestration/spec/active/spec-001.md]\n" - "allocated_rules: [{id: parent-rule, requirement: must-not-be-inherited}]\n" - "---\n\n# Plan\n", - encoding="utf-8", - ) - - task = root / ".work-bundle/orchestration/plan/active/plan-001/phase-001/task-004.md" - task.parent.mkdir(parents=True) - task.write_text( - "---\n" - "id: task-004\n" - "plan_id: plan-001\n" - "phase_id: phase-001\n" - "goal: Compile a bounded executor packet.\n" - "source_ids: [REQ-003, CON-002, API-002, TEST-004]\n" - "truth_basis:\n" - " purpose: Compile a bounded executor packet.\n" - " as_is_evidence: [scripts/orchestration/execution_context.py]\n" - f" decision_authority: [{ACCEPTED_AUTHORITY}]\n" - " expected_delta: [API-002]\n" - " conflict_status: clear\n" - "files:\n" - " read: [scripts/orchestration/core.py]\n" - " write: [scripts/orchestration/execution_context.py]\n" - " forbidden: [.work-bundle/knowledge/**, credentials/**]\n" - "interfaces:\n" - " consumes: [API-002]\n" - " produces: [API-002]\n" - "methodology:\n" - " primary: tdd\n" - " skills: [dev-test-driven-development]\n" - "allocated_rules:\n" - " - {id: scoped-rule, requirement: Keep the executor packet bounded.}\n" - "allocated_skills:\n" - " - {name: dev-test-driven-development}\n" - "executor_profile:\n" - " capability: mechanical\n" - " context_mode: compiled-brief\n" - "acceptance_review:\n" - " required: false\n" - "evidence_capability:\n" - " result: no_validation_bearing_obligation\n" - " reason: This shared fixture leaves capability semantics to scenario-specific tests.\n" - " invariants: []\n" - "validation:\n" - " - {kind: process, command: uv run --with pytest pytest -q tests/test_one.py, proves: TEST-004, expected: exit 0}\n" - "---\n\n# Task\n", - encoding="utf-8", - ) - return root, spec, task - - -def test_set_spec_status_verified_compiles_task_brief(tmp_path: Path) -> None: - from specs import cmd_set_spec_status - from test_orchestration_reviews import stage_review - from review_runtime import artifact_review_identity, publish_review - - root, spec, task = workspace(tmp_path) - spec.write_text( - spec.read_text(encoding="utf-8").replace("status: verified\n", "status: draft\n"), - encoding="utf-8", - ) - - review = stage_review("specification") - review["target_identity"] = artifact_review_identity(spec) - review = bind_review_receipt(root, review) - publish_review(root, review, current_target_identity=review["target_identity"]) - cmd_set_spec_status( - argparse.Namespace(project_root=str(root), workspace_root=None, id="spec-001", status="verified") - ) - target = build_task_brief(args(root, task)) - - assert target.is_file() - assert "status: verified" in spec.read_text(encoding="utf-8") - - -def test_build_task_brief_accepts_quoted_source_prose(tmp_path: Path) -> None: - root, spec, task = workspace(tmp_path) - spec.write_text( - spec.read_text(encoding="utf-8").replace( - "Never write outside the assigned files.", - 'Never write outside the "quoted" assigned files.', - ), - encoding="utf-8", - ) - - target = build_task_brief(args(root, task)) - - assert target.is_file() - brief, _ = execution_context._read_structured(target) - assert any( - 'Never write outside the "quoted" assigned files.' in constraint - for constraint in brief["task_brief"]["constraints"] - ) - - -def test_set_plan_status_uses_plan_id_to_disambiguate(tmp_path: Path) -> None: - from plans import cmd_set_plan_status - - root, _, task = workspace(tmp_path) - second = root / ".work-bundle/orchestration/plan/active/plan-002/phase-001/task-004.md" - second.parent.mkdir(parents=True) - second.write_text( - task.read_text(encoding="utf-8").replace("plan_id: plan-001\n", "plan_id: plan-002\n"), - encoding="utf-8", - ) - - cmd_set_plan_status( - argparse.Namespace( - project_root=str(root), - workspace_root=None, - id="task-004", - plan_id="plan-002", - status="In progress", - kind="task", - handoff=None, - ) - ) - - first_data, _ = execution_context._read_structured(task) - second_data, _ = execution_context._read_structured(second) - assert first_data.get("status") != "In progress" - assert second_data["status"] == "In progress" - - -def args(root: Path, task: Path, **overrides: object) -> argparse.Namespace: - values: dict[str, object] = { - "project_root": str(root), - "workspace_root": None, - "task": str(task), - "handoff": None, - "base": None, - "head": None, - "workspace_id": None, - "execution_id": None, - "repository_id": None, - "execution_runtime_root": None, - "mutation_events": [], - } - values.update(overrides) - return argparse.Namespace(**values) - - -def carry_accepted_constraint(spec: Path) -> None: - spec.write_text( - spec.read_text(encoding="utf-8").replace( - f" - {ACCEPTED_AUTHORITY_PATH}\n", - f" - path: {ACCEPTED_AUTHORITY_PATH}\n constraint: {ACCEPTED_CONSTRAINT}\n", - ), - encoding="utf-8", - ) - - -def write_decoy_knowledge(root: Path) -> Path: - knowledge = root / ACCEPTED_AUTHORITY_PATH - knowledge.parent.mkdir(parents=True, exist_ok=True) - knowledge.write_text(DECOY_KNOWLEDGE + "\n", encoding="utf-8") - return knowledge - - -WRITE_SCOPE_FILE = "scripts/orchestration/execution_context.py" -TASK_VALIDATION_COMMAND = "uv run --with pytest pytest -q tests/test_one.py" -HANDOFF_COMPLETION = ( - "result: {state: completed}\n" - "task_fit_check: {task: task-004, result: clean}\n" - "validation:\n" - " commands:\n" - f" - {{command: {TASK_VALIDATION_COMMAND}, result: passed}}\n" -) - - -def evidence_blocks(root: Path, *, codegraph: str = "no-index") -> str: - if codegraph == "no-index": - codegraph_block = ( - " applicable: false\n" - " up_to_date: false\n" - " reason: no-index\n" - ) - else: - codegraph_block = ( - " applicable: true\n" - " up_to_date: true\n" - " reason: null\n" - ) - return ( - "repository:\n" - f" - root: {root.resolve()}\n" - " target_kind: git-backed\n" - " preflight_kind: git-clean-worktree\n" - " baseline: initial\n" - " status: clean\n" - "codegraph:\n" - f" - root: {root.resolve()}\n" - f"{codegraph_block}" - "delegation_evidence:\n" - " delegated: true\n" - " owner_kind: subagent\n" - " agent_id: execution-context-fixture-agent\n" - " run_id: execution-context-fixture-run\n" - " mechanism: host-native\n" - ) - - -def committed_review_base(root: Path) -> str: - source = root / WRITE_SCOPE_FILE - source.parent.mkdir(parents=True, exist_ok=True) - source.write_text("def compile_task():\n return 'old'\n", encoding="utf-8") - git(root, "add", ".") - git(root, "commit", "-qm", "base") - return git(root, "rev-parse", "HEAD") - - -def write_executor_handoff(root: Path, disposition: str) -> Path: - handoff = root / ".work-bundle/orchestration/handoff/executor/active/handoff-task-004.yaml" - handoff.parent.mkdir(parents=True, exist_ok=True) - handoff.write_text( - "id: handoff-task-004\n" - "type: executor-result\n" - "related: {plan: plan-001, task: task-004}\n" - f"{HANDOFF_COMPLETION}" - f"{evidence_blocks(root)}" - "knowledge_disposition:\n" - + disposition, - encoding="utf-8", - ) - return handoff - - -def retarget_plan(root: Path, task: Path, plan_id: str) -> None: - plan = root / ".work-bundle/orchestration/plan/active/compiler-plan.md" - plan.write_text( - plan.read_text(encoding="utf-8").replace("id: plan-001\n", f"id: {plan_id}\n"), - encoding="utf-8", - ) - task.write_text( - task.read_text(encoding="utf-8").replace("plan_id: plan-001\n", f"plan_id: {plan_id}\n"), - encoding="utf-8", - ) - - -def write_related_handoff(root: Path, related_block: str) -> Path: - handoff = root / ".work-bundle/orchestration/handoff/executor/active/handoff-task-004.yaml" - handoff.parent.mkdir(parents=True, exist_ok=True) - handoff.write_text( - "id: handoff-task-004\n" - "type: executor-result\n" - f"{related_block}" - f"{HANDOFF_COMPLETION}" - f"{evidence_blocks(root)}" - "knowledge_disposition:\n" - " action: none\n" - " reason: No stable authority changed.\n" - " affected_authority: []\n", - encoding="utf-8", - ) - return handoff - - -COMPILED_AUTHORITY = f"{ACCEPTED_AUTHORITY}: {ACCEPTED_CONSTRAINT}" - - -def test_build_task_brief_resolves_source_ids_and_keeps_allocations_task_local(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - - target = build_task_brief(args(root, task)) - packet = target.read_text(encoding="utf-8") - - assert target == root / ".work-bundle/runtime/execution/plan-001/task-004/task-brief.yaml" - assert "Retry exactly three times before returning failure." in packet - assert "Never write outside the assigned files." in packet - assert "`compile_task(task: Path) -> dict[str, object]`" in packet - assert "Focused pytest exits with status 0." in packet - assert "scoped-rule" in packet - assert "dev-test-driven-development" in packet - assert "parent-rule" not in packet - assert ".work-bundle/knowledge/**" in packet - assert ".work-bundle/knowledge/notes" not in packet - assert "handoff_contract: executor-result-v1" in packet - assert "review_required: false" in packet - assert "review_required: true" not in packet - assert "truth_basis:" in packet - assert 'purpose: "Compile a bounded executor packet."' in packet - assert ACCEPTED_AUTHORITY in packet - assert COMPILED_AUTHORITY in packet - assert ACCEPTED_CONSTRAINT in packet - assert ACCEPTED_AUTHORITY_PATH not in packet.split("truth_basis:", 1)[1].split("expected_delta:", 1)[0] - assert "conflict_status: clear" in packet - - -@pytest.mark.parametrize("malformed", ["null", "standard", "[standard]"]) -def test_build_task_brief_rejects_present_non_mapping_executor_profile( - tmp_path: Path, malformed: str -) -> None: - root, _, task = workspace(tmp_path) - content = task.read_text(encoding="utf-8") - content = re.sub( - r"executor_profile:\n(?: .*\n)+?acceptance_review:", - f"executor_profile: {malformed}\nacceptance_review:", - content, - ) - task.write_text(content, encoding="utf-8") - - with pytest.raises(SystemExit, match=r"executor_profile.*mapping"): - build_task_brief(args(root, task)) - - -@pytest.mark.parametrize("profile", ["{}", "{context_mode: compiled-brief}", "{capability: unknown}"]) -def test_build_task_brief_rejects_missing_or_unknown_executor_capability( - tmp_path: Path, profile: str -) -> None: - root, _, task = workspace(tmp_path) - content = task.read_text(encoding="utf-8") - content = re.sub( - r"executor_profile:\n(?: .*\n)+?acceptance_review:", - f"executor_profile: {profile}\nacceptance_review:", - content, - ) - task.write_text(content, encoding="utf-8") - - with pytest.raises(SystemExit, match=r"executor_profile\.capability"): - build_task_brief(args(root, task)) - - -def test_build_task_brief_defaults_only_an_omitted_executor_profile(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - content = re.sub( - r"executor_profile:\n(?: .*\n)+?acceptance_review:", - "acceptance_review:", - task.read_text(encoding="utf-8"), - ) - task.write_text(content, encoding="utf-8") - - brief = execution_context._compile_task_brief(args(root, task))[1]["task_brief"] - - assert brief["executor_profile"] == { - "capability": "standard", - "context_mode": "compiled-brief", - } - - -def test_build_task_brief_preserves_valid_executor_profile_fields_verbatim(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - content = task.read_text(encoding="utf-8").replace( - " context_mode: compiled-brief\nacceptance_review:", - " context_mode: isolated\n" - " review_capability: judgment\n" - " escalation: {after_failed_repairs: 2}\n" - " future_option: [alpha, beta]\n" - "acceptance_review:", - ) - task.write_text(content, encoding="utf-8") - - profile = execution_context._compile_task_brief(args(root, task))[1]["task_brief"]["executor_profile"] - - assert profile == { - "capability": "mechanical", - "context_mode": "isolated", - "review_capability": "judgment", - "escalation": {"after_failed_repairs": 2}, - "future_option": ["alpha", "beta"], - } - - -@pytest.mark.parametrize( - ("task_authority", "expected"), - [ - ({}, {"metadata": [], "repository": [], "codegraph": []}), - ( - {"project_metadata_required": True}, - {"metadata": ["project-metadata-preflight"], "repository": [], "codegraph": []}, - ), - ( - {"execution_binding": {"target_kind": "local-project"}}, - {"metadata": [], "repository": [], "codegraph": []}, - ), - ( - {"execution_binding": {"target_kind": "git-backed"}}, - {"metadata": [], "repository": ["repository-target-binding"], "codegraph": []}, - ), - ( - {"changed_paths": ["src/core.ts"]}, - {"metadata": [], "repository": ["changed-paths"], "codegraph": []}, - ), - ( - {"files": {"read": ["src/core.ts"], "write": []}}, - { - "metadata": [], - "repository": ["source-inspection"], - "codegraph": ["source-inspection"], - }, - ), - ( - {"files": {"read": [], "write": ["src/core.ts"]}}, - { - "metadata": [], - "repository": ["source-editing"], - "codegraph": ["source-editing"], - }, - ), - ( - {"files": {"read": ["README.md"]}, "source_files": ["src/core.ts"]}, - { - "metadata": [], - "repository": ["source-inspection"], - "codegraph": ["source-inspection"], - }, - ), - ( - {"files": {"write": ["README.md"]}, "target_files": ["src/core.ts"]}, - { - "metadata": [], - "repository": ["source-editing"], - "codegraph": ["source-editing"], - }, - ), - ( - {"target_symbols": ["compile_task"]}, - { - "metadata": [], - "repository": ["source-analysis"], - "codegraph": ["source-analysis"], - }, - ), - ( - {"validation": [{"kind": "process", "command": "pytest tests/test_core.py"}]}, - { - "metadata": [], - "repository": ["source-validation"], - "codegraph": ["source-validation"], - }, - ), - ], -) -def test_evidence_applicability_decision_table_is_monotonic_and_reason_coded( - task_authority: dict, expected: dict -) -> None: - result = execution_context.task_evidence_applicability(task_authority) - - for key, reasons in expected.items(): - assert result[key] == {"required": bool(reasons), "reasons": reasons} - - -def test_compiled_brief_preserves_the_shared_evidence_applicability_result(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - task_data, _ = execution_context._read_structured(task) - - brief = execution_context._compile_task_brief(args(root, task))[1]["task_brief"] - - assert brief["evidence_applicability"] == execution_context.task_evidence_applicability(task_data) - - -def test_build_task_brief_fails_closed_when_truth_basis_is_missing(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - content = task.read_text(encoding="utf-8") - content = re.sub(r"truth_basis:\n(?: .*\n){5}", "", content) - task.write_text(content, encoding="utf-8") - - with pytest.raises(SystemExit, match="Task Truth Basis is required"): - build_task_brief(args(root, task)) - - -def test_build_task_brief_routes_truth_basis_conflict_to_typed_blocker(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - task.write_text( - task.read_text(encoding="utf-8").replace("conflict_status: clear", "conflict_status: escalate"), - encoding="utf-8", - ) - - with pytest.raises(SystemExit, match="decision-blocked"): - build_task_brief(args(root, task)) - - -def test_build_task_brief_accepts_explicit_none_relevant_decision_authority(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - task.write_text( - task.read_text(encoding="utf-8").replace( - f"decision_authority: [{ACCEPTED_AUTHORITY}]", - "decision_authority: [none-relevant]", - ), - encoding="utf-8", - ) - - packet = build_task_brief(args(root, task)).read_text(encoding="utf-8") - - assert "none-relevant" in packet - - -def test_build_task_brief_rejects_none_relevant_from_unverified_specification(tmp_path: Path) -> None: - root, spec, task = workspace(tmp_path) - spec.write_text(spec.read_text(encoding="utf-8").replace("status: verified", "status: draft"), encoding="utf-8") - task.write_text( - task.read_text(encoding="utf-8").replace( - f"decision_authority: [{ACCEPTED_AUTHORITY}]", "decision_authority: [none-relevant]" - ), - encoding="utf-8", - ) - - with pytest.raises(SystemExit, match="requires a verified specification"): - build_task_brief(args(root, task)) - - -@pytest.mark.parametrize( - "authority", - [ - "invented design decision", - "REQ-003", - ".work-bundle/knowledge/notes/candidate.md", - ".work-bundle/knowledge/notes/background.md", - ".work-bundle/knowledge/notes/blocked.md", - ".work-bundle/knowledge/notes/superseded.md", - ], -) -def test_build_task_brief_rejects_decision_authority_not_carried_by_verified_spec( - tmp_path: Path, authority: str -) -> None: - root, _, task = workspace(tmp_path) - task.write_text( - task.read_text(encoding="utf-8").replace( - f"decision_authority: [{ACCEPTED_AUTHORITY}]", - f"decision_authority: [{authority}]", - ), - encoding="utf-8", - ) - - with pytest.raises(SystemExit, match="decision_authority.*verified specification authority"): - build_task_brief(args(root, task)) - - -def test_build_task_brief_does_not_allocate_aliases_for_non_authority_source_context(tmp_path: Path) -> None: - root, spec, task = workspace(tmp_path) - spec.write_text( - spec.read_text(encoding="utf-8").replace( - "# Compiler contract", - "# Compiler contract\n\n## Source Context\n\n- **Candidate**: `.work-bundle/knowledge/notes/candidate.md` remains non-authority.", - ), - encoding="utf-8", - ) - task.write_text( - task.read_text(encoding="utf-8").replace( - ACCEPTED_AUTHORITY, "AUTH-002" - ), - encoding="utf-8", - ) - - with pytest.raises(SystemExit, match="decision_authority.*verified specification authority"): - build_task_brief(args(root, task)) - - -@pytest.mark.parametrize( - ("upstream", "review_verdict", "action", "closure_return", "expected"), - [ - ("not-needed", "accept", "update", "missing", ("required", True)), - ("not-needed", "accept", "supersede", "completed", ("completed", False)), - ("not-needed", "accept", "reclassify", "not-needed", ("not-needed", False)), - ("not-needed", "repair", "update", "missing", ("not-needed", False)), - ("not-needed", "accept", "none", "missing", ("not-needed", False)), - ("required", "accept", "none", "blocked", ("blocked", True)), - ], -) -def test_final_knowledge_closure_is_driven_by_accepted_task_dispositions( - upstream: str, - review_verdict: str, - action: str, - closure_return: str, - expected: tuple[str, bool], -) -> None: - handoffs = [ - { - "related": {"task": "task-004"}, - "result": {"state": "completed"}, - "acceptance_review": {"verdict": review_verdict}, - "knowledge_disposition": { - "action": action, - "reason": "Task-local evidence.", - "affected_authority": [] if action == "none" else [ACCEPTED_AUTHORITY], - }, - } - ] - - result = execution_context.evaluate_knowledge_closure_state( - upstream_disposition=upstream, - accepted_task_handoffs=handoffs, - closure_return=closure_return, - ) - - assert (result["disposition"], result["archive_blocked"]) == expected - - -def test_build_task_brief_preserves_review_not_required_from_task_contract(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - task.write_text( - task.read_text(encoding="utf-8").replace( - "validation:\n", - "acceptance_review:\n required: false\nvalidation:\n", - ), - encoding="utf-8", - ) - - packet = build_task_brief(args(root, task)).read_text(encoding="utf-8") - - assert "review_required: false" in packet - assert "review_required: true" not in packet - - -def test_build_task_brief_preserves_explicit_review_requirement(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - task.write_text( - task.read_text(encoding="utf-8").replace( - "acceptance_review:\n required: false\n", - "acceptance_review:\n required: true\n", - ), - encoding="utf-8", - ) - - packet = build_task_brief(args(root, task)).read_text(encoding="utf-8") - - assert "review_required: true" in packet - - -def test_build_task_brief_fails_closed_for_missing_source_id_without_reading_knowledge(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - task.write_text(task.read_text(encoding="utf-8").replace("TEST-004]", "TEST-004, REQ-999]"), encoding="utf-8") - knowledge = root / ".work-bundle/knowledge/notes/hidden.md" - knowledge.parent.mkdir(parents=True) - knowledge.write_text("- **REQ-999**: This must never be used.\n", encoding="utf-8") - - with pytest.raises(SystemExit, match=r"REQ-999.*spec-001\.md"): - build_task_brief(args(root, task)) - - assert not (root / ".work-bundle/runtime/execution/plan-001/task-004/task-brief.yaml").exists() - - -def test_build_task_brief_fails_closed_for_excellence_proposal_source_id(tmp_path: Path) -> None: - root, spec, task = workspace(tmp_path) - spec.write_text( - spec.read_text(encoding="utf-8") + "- **EXC-001**: Deferred visual hierarchy improvement.\n", - encoding="utf-8", - ) - task.write_text(task.read_text(encoding="utf-8").replace("TEST-004]", "TEST-004, EXC-001]"), encoding="utf-8") - - with pytest.raises(SystemExit, match=r"EXC-001.*excellence proposal"): - build_task_brief(args(root, task)) - - assert not (root / ".work-bundle/runtime/execution/plan-001/task-004/task-brief.yaml").exists() - - -def test_build_task_brief_omits_unallocated_excellence_proposal_text(tmp_path: Path) -> None: - root, spec, task = workspace(tmp_path) - spec.write_text( - spec.read_text(encoding="utf-8") + "- **EXC-001**: Deferred visual hierarchy improvement.\n", - encoding="utf-8", - ) - - packet = build_task_brief(args(root, task)).read_text(encoding="utf-8") - - assert "EXC-001" not in packet - assert "Deferred visual hierarchy" not in packet - - -def test_build_task_brief_reads_current_task_contract_sections(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - content = task.read_text(encoding="utf-8") - content = content.replace("goal: Compile a bounded executor packet.\n", "") - content = content.replace(" skills: [dev-test-driven-development]", " required_skills: [dev-test-driven-development]") - content = re.sub(r"interfaces:\n(?: .*\n){2}", "", content) - content = content.replace( - "# Task\n", - "# Task\n\n" - "## Goal\n\nCompile from the current task contract.\n\n" - "## Files and interfaces\n\n" - "| Path or interface | Read/write | Required usage |\n" - "| --- | --- | --- |\n" - "| API-002 | consumes | Exact compiler signature |\n\n" - "## Validation\n\n" - "Non-authoritative presentation only.\n\n" - "| Command or inspection | Proves | Expected |\n" - "| --- | --- | --- |\n" - "| `echo BODY-TABLE-IS-NOT-AUTHORITY` | CON-002 | failed |\n", - ) - task.write_text(content, encoding="utf-8") - - packet = build_task_brief(args(root, task)).read_text(encoding="utf-8") - - assert "Compile from the current task contract." in packet - assert "dev-test-driven-development" in packet - assert "API-002: `compile_task(task: Path) -> dict[str, object]`" in packet - assert "Focused pytest exits with status 0." in packet - - -def test_build_task_brief_rejects_credential_values_before_writing_packet(tmp_path: Path) -> None: - root, spec, task = workspace(tmp_path) - spec.write_text( - spec.read_text(encoding="utf-8") - + "- **REQ-005**: credential_value: SYNTHETIC-CANARY-DO-NOT-LEAK\n", - encoding="utf-8", - ) - task.write_text(task.read_text(encoding="utf-8").replace("TEST-004]", "TEST-004, REQ-005]"), encoding="utf-8") - - with pytest.raises(SystemExit, match="credential-like value") as error: - build_task_brief(args(root, task)) - - assert "SYNTHETIC-CANARY" not in str(error.value) - assert not (root / ".work-bundle/runtime/execution/plan-001/task-004/task-brief.yaml").exists() - - -def test_build_task_brief_rejects_protected_credential_path_scope(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - task.write_text( - task.read_text(encoding="utf-8").replace( - "read: [scripts/orchestration/core.py]", "read: [credentials/credentials.yaml]" - ), - encoding="utf-8", - ) - - with pytest.raises(SystemExit, match="forbidden protected path"): - build_task_brief(args(root, task)) - - assert not (root / ".work-bundle/runtime/execution/plan-001/task-004/task-brief.yaml").exists() - - -def test_build_review_package_contains_only_bounded_task_diff_and_evidence(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - task.write_text( - task.read_text(encoding="utf-8") - .replace( - " result: no_validation_bearing_obligation\n" - " reason: This shared fixture leaves capability semantics to scenario-specific tests.\n" - " invariants: []\n", - " result: mapped\n" - " reason: This scenario verifies review-package propagation.\n" - " invariants:\n" - " - {id: INV-001, source_ids: [REQ-003, TEST-004], invariant: Review package carries allocated capability, boundary: unit, oracle: VAL-001, capability_reason: The focused process distinguishes omission, freshness: current_task_batch, task_id: task-004, evidence_ids: [VAL-001], closure_result: pending}\n", - ) - .replace( - " - {kind: process, command: uv run --with pytest pytest -q tests/test_one.py, proves: TEST-004, expected: exit 0}\n", - " - {id: VAL-001, invariant_ids: [INV-001], capability_reason: The focused process distinguishes omission, kind: process, command: uv run --with pytest pytest -q tests/test_one.py, proves: TEST-004, expected: exit 0}\n", - ), - encoding="utf-8", - ) - source = root / WRITE_SCOPE_FILE - source.parent.mkdir(parents=True, exist_ok=True) - source.write_text("def compile_task():\n return 'old'\n", encoding="utf-8") - git(root, "add", ".") - git(root, "commit", "-qm", "base") - base = git(root, "rev-parse", "HEAD") - source.write_text( - "def compile_task():\n password = 'DIFF-CANARY-DO-NOT-LEAK'\n return 'new'\n", - encoding="utf-8", - ) - git(root, "add", WRITE_SCOPE_FILE) - git(root, "commit", "-qm", "head") - head = git(root, "rev-parse", "HEAD") - - handoff = root / ".work-bundle/orchestration/handoff/executor/active/handoff-task-004.yaml" - handoff.parent.mkdir(parents=True) - handoff.write_text( - "id: handoff-task-004\n" - "type: executor-result\n" - "related:\n" - " plan: plan-001\n" - " task: task-004\n" - "result: {state: partial}\n" - "task_fit_check: {task: task-004, result: unresolved}\n" - "changes:\n" - " files:\n" - f" - {{path: {WRITE_SCOPE_FILE}, action: modified, symbols: [compile_task]}}\n" - "validation:\n" - " commands:\n" - f" - {{command: {TASK_VALIDATION_COMMAND}, result: passed}}\n" - "unresolved:\n" - " - Confirm retry timing with the caller.\n" - f"{evidence_blocks(root)}" - "knowledge_disposition:\n" - " action: none\n" - " reason: No stable authority changed.\n" - " affected_authority: []\n" - "session_history: SHOULD-NOT-APPEAR\n", - encoding="utf-8", - ) - _enable_passing_observation(root, task, handoff) - - 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 WRITE_SCOPE_FILE in package - assert "compile_task" in package - assert "return 'new'" in package - assert "DIFF-CANARY-DO-NOT-LEAK" not in package - assert "password: <redacted>" in package - assert "result: passed" in package - assert "Confirm retry timing with the caller." in package - assert "## Review rubric" in package - assert "## Knowledge disposition" not in package - assert "SHOULD-NOT-APPEAR" not in package - assert ".work-bundle/knowledge/notes" not in package - - -def test_initial_completed_result_can_prepare_required_review_without_future_verdict(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - task.write_text(task.read_text().replace( - "acceptance_review:\n required: false\n", - "acceptance_review:\n required: true\n", - )) - base = committed_review_base(root) - handoff = write_executor_handoff( - root, " action: none\n reason: No stable authority changed.\n affected_authority: []\n" - ) - _enable_passing_observation(root, task, handoff) - - package = build_review_package(args(root, task, handoff=str(handoff), base=base, head=base)) - assert package.is_file() - handoff_data = _read_handoff(handoff) - brief = _compiled_brief(root, task) - validated = _validate_observed(handoff_data, brief) - assert validated["result_state"] == "completed" - with pytest.raises(SystemExit, match="accepted mandatory review"): - execution_context.materialize_accepted_task_result( - root, brief, handoff_data, validated - ) - - -def test_postacceptance_review_package_ignores_stale_handoff_and_requires_current_observation(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - base = committed_review_base(root) - handoff = write_executor_handoff( - root, " action: none\n reason: No stable authority changed.\n affected_authority: []\n" - ) - _set_process_validation( - task, PASSING_PROCESS, - evidence_reuse={ - "mode": "deterministic", "max_age_seconds": 3600, - "environment_inputs": ["PYTHONHASHSEED"], "include_head": False, - }, - ) - brief = _compiled_brief(root, task) - assert brief["validation"][0]["evidence_reuse"]["max_age_seconds"] == 3600 - _bind_task_execution(root, brief) - handoff.write_text( - handoff.read_text().replace(TASK_VALIDATION_COMMAND, json.dumps(PASSING_PROCESS)) - ) - validated = _validate_observed(_read_handoff(handoff), brief) - execution_context.materialize_accepted_task_result(root, brief, _read_handoff(handoff), validated) - - source = root / WRITE_SCOPE_FILE - source.write_text(source.read_text() + "\n# reviewed repair\n") - git(root, "add", WRITE_SCOPE_FILE) - git(root, "commit", "-qm", "repair") - binding = execution_context.load_task_execution_binding(root, "plan-001", "task-004") - repository_evidence = execution_context.capture_repository_evidence(root) - observed = execution_context._completion_provenance_module().observe_validation( - binding, execution_context._validation_observation_task(brief), - brief["validation"][0], repository_evidence, - lambda receipt: execution_context._observe_validation_item( - brief["validation"][0], root, brief, receipt - ), - lambda: execution_context.capture_repository_evidence(root), - ) - observation_id = observed["observation_id"] - - controller_resume = root / ".work-bundle/runtime/controller-resume.json" - controller_resume.parent.mkdir(parents=True, exist_ok=True) - controller_resume.write_text('{"publication": "retry"}\n') - head = git(root, "rev-parse", "HEAD") - - unrelated_check = {**brief["validation"][0], "id": "VAL-UNRELATED", "command": "true"} - unrelated_observed = execution_context._completion_provenance_module().observe_validation( - binding, execution_context._validation_observation_task(brief), - unrelated_check, execution_context.capture_repository_evidence(root), - lambda receipt: execution_context._observe_validation_item( - unrelated_check, root, brief, receipt - ), - lambda: execution_context.capture_repository_evidence(root), - ) - handoff.write_text("this: [is: stale") - - with pytest.raises(SystemExit, match="claim-bound|does not bind current task claims"): - build_review_package(args( - root, task, handoff=str(handoff), base=base, head=head, - validation_observation_id=[unrelated_observed["observation_id"]], - )) - - package = build_review_package(args( - root, task, handoff=str(handoff), base=base, head=head, - validation_observation_id=[observation_id], - )).read_text() - assert "reviewed repair" in package - assert observation_id in package - assert PASSING_PROCESS in package - assert "invariant_ids" in package - - material_source = root / "unrelated.txt" - material_source.write_text("new material source\n") - git(root, "add", "unrelated.txt") - git(root, "commit", "-qm", "material source change") - with pytest.raises(SystemExit, match="claim-bound"): - build_review_package(args( - root, task, handoff=str(handoff), base=base, - head=git(root, "rev-parse", "HEAD"), - validation_observation_id=[observation_id], - )) - - -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, evidence_root=execution_root) - - 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 - source.parent.mkdir(parents=True, exist_ok=True) - source.write_text("def compile_task():\n return 'old'\n", encoding="utf-8") - git(root, "add", ".") - git(root, "commit", "-qm", "base") - base = git(root, "rev-parse", "HEAD") - source.write_text("def compile_task():\n return 'working'\n", encoding="utf-8") - new_test = root / "tests/test_compiler.py" - new_test.parent.mkdir() - new_test.write_text("def test_compile_task():\n assert True\n", encoding="utf-8") - task.write_text( - task.read_text(encoding="utf-8").replace( - f"write: [{WRITE_SCOPE_FILE}]", - f"write: [{WRITE_SCOPE_FILE}, tests/test_compiler.py]", - ), - encoding="utf-8", - ) - - handoff = root / ".work-bundle/orchestration/handoff/executor/active/handoff-task-004.yaml" - handoff.parent.mkdir(parents=True) - handoff.write_text( - "id: handoff-task-004\n" - "type: executor-result\n" - "related: {plan: plan-001, task: task-004}\n" - f"{HANDOFF_COMPLETION}" - f"{evidence_blocks(root)}" - "knowledge_disposition:\n" - " action: none\n" - " reason: No stable authority changed.\n" - " affected_authority: []\n", - encoding="utf-8", - ) - _enable_passing_observation(root, task, handoff) - - target = build_review_package( - args(root, task, handoff=str(handoff), base=base, head="worktree") - ) - package = target.read_text(encoding="utf-8") - - assert re.search(r"Head: worktree:[0-9a-f]{64}", package) - assert f"M\t{WRITE_SCOPE_FILE}" in package - assert "A\ttests/test_compiler.py" in package - assert "return 'working'" in package - assert "def test_compile_task" in package - - -def test_build_review_package_never_reads_tracked_protected_diff_content(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - protected = root / "credentials/credentials.yaml" - protected.parent.mkdir() - protected.write_text("credential_id: safe-reference\n", encoding="utf-8") - git(root, "add", ".") - git(root, "commit", "-qm", "base") - base = git(root, "rev-parse", "HEAD") - protected.write_text("opaque_value: TRACKED-PROTECTED-CANARY\n", encoding="utf-8") - handoff = root / ".work-bundle/orchestration/handoff/executor/active/handoff-task-004.yaml" - handoff.parent.mkdir(parents=True) - handoff.write_text( - "id: handoff-task-004\n" - "type: executor-result\n" - "related: {plan: plan-001, task: task-004}\n" - f"{HANDOFF_COMPLETION}" - f"{evidence_blocks(root)}" - "knowledge_disposition:\n" - " action: none\n" - " reason: No stable authority changed.\n" - " affected_authority: []\n", - encoding="utf-8", - ) - _enable_passing_observation(root, task, handoff) - - package = build_review_package( - args(root, task, handoff=str(handoff), base=base, head="worktree") - ).read_text(encoding="utf-8") - - assert "credentials/credentials.yaml" in package - assert "TRACKED-PROTECTED-CANARY" not in package - - -def test_build_review_package_rejects_invalid_knowledge_disposition(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - source = root / WRITE_SCOPE_FILE - source.parent.mkdir(parents=True, exist_ok=True) - source.write_text("def compile_task():\n return 'old'\n", encoding="utf-8") - git(root, "add", ".") - git(root, "commit", "-qm", "base") - base = git(root, "rev-parse", "HEAD") - handoff = root / ".work-bundle/orchestration/handoff/executor/active/handoff-task-004.yaml" - handoff.parent.mkdir(parents=True) - handoff.write_text( - "id: handoff-task-004\n" - "type: executor-result\n" - "related: {plan: plan-001, task: task-004}\n" - f"{HANDOFF_COMPLETION}" - "knowledge_disposition:\n" - " action: write-now\n" - " reason: Executor should persist knowledge.\n", - encoding="utf-8", - ) - - with pytest.raises(SystemExit, match="knowledge disposition action"): - build_review_package(args(root, task, handoff=str(handoff), base=base, head=base)) - - -@pytest.mark.parametrize( - "disposition", - [ - " action: update\n reason: Stable authority changed.\n affected_authority: []\n", - " action: update\n reason: Run ks-write-knowledge now.\n affected_authority: [REQ-003]\n", - " action: update\n reason: Run ks-track-open-questions now.\n affected_authority: [REQ-003]\n", - " action: update\n reason: Stable authority changed.\n affected_authority: [.work-bundle/knowledge/notes/new.md]\n", - " action: update\n reason: Stable authority changed.\n affected_authority: [../../outside/authority.md]\n", - " action: update\n reason: Stable authority changed.\n affected_authority: [credentials/credentials.yaml]\n", - ], -) -def test_build_review_package_rejects_unbounded_knowledge_disposition( - tmp_path: Path, disposition: str -) -> None: - root, _, task = workspace(tmp_path) - source = root / WRITE_SCOPE_FILE - source.parent.mkdir(parents=True, exist_ok=True) - source.write_text("def compile_task():\n return 'old'\n", encoding="utf-8") - git(root, "add", ".") - git(root, "commit", "-qm", "base") - base = git(root, "rev-parse", "HEAD") - handoff = root / ".work-bundle/orchestration/handoff/executor/active/handoff-task-004.yaml" - handoff.parent.mkdir(parents=True) - handoff.write_text( - "id: handoff-task-004\n" - "type: executor-result\n" - "related: {plan: plan-001, task: task-004}\n" - f"{HANDOFF_COMPLETION}" - "knowledge_disposition:\n" - + disposition, - encoding="utf-8", - ) - - with pytest.raises(SystemExit, match="knowledge disposition"): - build_review_package(args(root, task, handoff=str(handoff), base=base, head=base)) - - -def test_build_task_brief_compiles_auth_alias_with_carried_constraint(tmp_path: Path) -> None: - root, spec, task = workspace(tmp_path) - carry_accepted_constraint(spec) - write_decoy_knowledge(root) - - packet = build_task_brief(args(root, task)).read_text(encoding="utf-8") - - assert COMPILED_AUTHORITY in packet - assert ACCEPTED_CONSTRAINT in packet - assert "decision_authority:" in packet - assert ACCEPTED_AUTHORITY_PATH not in packet.split("truth_basis:", 1)[1].split("expected_delta:", 1)[0] - assert DECOY_KNOWLEDGE not in packet - - -def test_build_review_package_receives_same_resolved_auth_semantics(tmp_path: Path) -> None: - root, spec, task = workspace(tmp_path) - carry_accepted_constraint(spec) - write_decoy_knowledge(root) - base = committed_review_base(root) - handoff = write_executor_handoff( - root, - " action: none\n reason: No stable authority changed.\n affected_authority: []\n", - ) - _enable_passing_observation(root, task, handoff) - - brief = build_task_brief(args(root, task)).read_text(encoding="utf-8") - package = build_review_package( - args(root, task, handoff=str(handoff), base=base, head=base) - ).read_text(encoding="utf-8") - - assert COMPILED_AUTHORITY in brief - assert COMPILED_AUTHORITY in package - assert ACCEPTED_CONSTRAINT in package - assert "## Accepted Truth Basis" not in package - assert ACCEPTED_AUTHORITY_PATH not in package - assert DECOY_KNOWLEDGE not in package - - -def test_build_task_brief_compiles_auth_without_reading_durable_knowledge(tmp_path: Path) -> None: - root, spec, task = workspace(tmp_path) - carry_accepted_constraint(spec) - knowledge = write_decoy_knowledge(root) - - packet = build_task_brief(args(root, task)).read_text(encoding="utf-8") - - assert COMPILED_AUTHORITY in packet - assert knowledge.read_text(encoding="utf-8") == DECOY_KNOWLEDGE + "\n" - assert DECOY_KNOWLEDGE not in packet - assert ACCEPTED_AUTHORITY_PATH not in packet.split("forbidden:", 1)[0] - - -def test_build_task_brief_fails_closed_when_auth_lacks_carried_constraint(tmp_path: Path) -> None: - root, spec, task = workspace(tmp_path) - spec.write_text( - spec.read_text(encoding="utf-8").replace( - f" - path: {ACCEPTED_AUTHORITY_PATH}\n constraint: {ACCEPTED_CONSTRAINT}\n", - f" - {ACCEPTED_AUTHORITY_PATH}\n", - ), - encoding="utf-8", - ) - - with pytest.raises(SystemExit, match="carried semantic constraint"): - build_task_brief(args(root, task)) - - -@pytest.mark.parametrize("action", ["update", "supersede", "reclassify"]) -def test_build_review_package_accepts_allocated_auth_in_knowledge_disposition( - tmp_path: Path, action: str -) -> None: - root, spec, task = workspace(tmp_path) - carry_accepted_constraint(spec) - base = committed_review_base(root) - handoff = write_executor_handoff( - root, - f" action: {action}\n reason: Stable accepted authority changed.\n" - f" affected_authority: [{ACCEPTED_AUTHORITY}]\n", - ) - _enable_passing_observation(root, task, handoff) - - package = build_review_package( - args(root, task, handoff=str(handoff), base=base, head=base) - ).read_text(encoding="utf-8") - - assert COMPILED_AUTHORITY in package - assert ACCEPTED_CONSTRAINT in package - assert f"action: {action}" not in package - assert ACCEPTED_AUTHORITY in package - assert "## Knowledge disposition" not in package - assert ACCEPTED_AUTHORITY_PATH not in package - - -def test_build_review_package_rejects_unallocated_auth_in_knowledge_disposition(tmp_path: Path) -> None: - root, spec, task = workspace(tmp_path) - carry_accepted_constraint(spec) - base = committed_review_base(root) - handoff = write_executor_handoff( - root, - " action: update\n reason: Stable accepted authority changed.\n" - " affected_authority: [AUTH-002]\n", - ) - - with pytest.raises(SystemExit, match="unallocated decision authority"): - build_review_package(args(root, task, handoff=str(handoff), base=base, head=base)) - - -def test_build_review_package_rejects_missing_plan_identity(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - retarget_plan(root, task, "plan-B") - base = committed_review_base(root) - handoff = write_related_handoff(root, "related:\n task: task-004\n") - review_target = root / ".work-bundle/runtime/execution/plan-B/task-004/review-package.md" - - with pytest.raises(SystemExit, match="Handoff plan identity missing"): - build_review_package(args(root, task, handoff=str(handoff), base=base, head=base)) - - assert not review_target.exists() - - -def test_build_review_package_rejects_null_plan_identity(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - retarget_plan(root, task, "plan-B") - base = committed_review_base(root) - handoff = write_related_handoff(root, "related:\n plan: null\n task: task-004\n") - - with pytest.raises(SystemExit, match="Handoff plan identity missing"): - build_review_package(args(root, task, handoff=str(handoff), base=base, head=base)) - - -def test_build_review_package_rejects_wrong_explicit_plan(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - retarget_plan(root, task, "plan-B") - base = committed_review_base(root) - handoff = write_related_handoff(root, "related:\n plan: plan-A\n task: task-004\n") - - with pytest.raises(SystemExit, match="Handoff plan mismatch: expected plan-B, got plan-A"): - build_review_package(args(root, task, handoff=str(handoff), base=base, head=base)) - - -def test_build_review_package_rejects_conflicting_plan_identities(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - retarget_plan(root, task, "plan-B") - base = committed_review_base(root) - handoff = write_related_handoff( - root, - "related:\n plan: plan-B\n task: task-004\nrelated_plan: plan-A\n", - ) - - with pytest.raises(SystemExit, match="Handoff plan identity conflict"): - build_review_package(args(root, task, handoff=str(handoff), base=base, head=base)) - - -def test_build_review_package_accepts_matching_plan_identity(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - retarget_plan(root, task, "plan-B") - base = committed_review_base(root) - handoff = write_related_handoff(root, "related:\n plan: plan-B\n task: task-004\n") - _enable_passing_observation(root, task, handoff) - - target = build_review_package(args(root, task, handoff=str(handoff), base=base, head=base)) - - assert target == root / ".work-bundle/runtime/execution/plan-B/task-004/review-package.md" - assert target.is_file() - - -def _archive_task(root: Path, task: Path) -> Path: - archived = root / ".work-bundle/orchestration/plan/archived/plan-001/phase-001" / task.name - archived.parent.mkdir(parents=True, exist_ok=True) - archived.write_text(task.read_text(encoding="utf-8"), encoding="utf-8") - return archived - - -def _omit_acceptance_review(task: Path) -> None: - task.write_text( - task.read_text(encoding="utf-8").replace("acceptance_review:\n required: false\n", ""), - encoding="utf-8", - ) - - -def _compiled_brief(root: Path, task: Path) -> dict: - _, brief = execution_context._compile_task_brief(args(root, task)) - return brief["task_brief"] - - -def _read_handoff(path: Path) -> dict: - data, _ = execution_context._read_structured(path) - return data - - -@pytest.mark.parametrize("acceptance_review", ["", "acceptance_review: {}\n"]) -@pytest.mark.parametrize("archive", [False, True]) -def test_omitted_or_empty_acceptance_review_defaults_review_not_required( - tmp_path: Path, acceptance_review: str, archive: bool -) -> None: - root, _, task = workspace(tmp_path) - _omit_acceptance_review(task) - if acceptance_review: - task.write_text( - task.read_text(encoding="utf-8").replace("validation:\n", f"{acceptance_review}validation:\n"), - encoding="utf-8", - ) - target = _archive_task(root, task) if archive else task - - packet = build_task_brief(args(root, target)).read_text(encoding="utf-8") - - assert "review_required: false" in packet - assert "review_required: true" not in packet - - -def test_build_task_brief_fails_closed_for_directory_only_write_scope(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - (root / "scripts/orchestration").mkdir(parents=True, exist_ok=True) - task.write_text( - task.read_text(encoding="utf-8").replace( - f"write: [{WRITE_SCOPE_FILE}]", - "write: [scripts/orchestration]", - ), - encoding="utf-8", - ) - - with pytest.raises(SystemExit, match="directory|module"): - build_task_brief(args(root, task)) - - -def test_build_task_brief_fails_closed_for_module_only_write_scope(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - task.write_text( - task.read_text(encoding="utf-8").replace( - f"write: [{WRITE_SCOPE_FILE}]", - "write: [scripts.orchestration.execution_context]", - ), - encoding="utf-8", - ) - - with pytest.raises(SystemExit, match="directory|module"): - build_task_brief(args(root, task)) - - -@pytest.mark.parametrize("write_path", ["Dockerfile", "Makefile", "LICENSE", ".gitignore"]) -def test_build_task_brief_allows_existing_extensionless_write_files( - tmp_path: Path, write_path: str -) -> None: - root, _, task = workspace(tmp_path) - (root / write_path).write_text("exact-file\n", encoding="utf-8") - task.write_text( - task.read_text(encoding="utf-8").replace( - f"write: [{WRITE_SCOPE_FILE}]", - f"write: [{write_path}]", - ), - encoding="utf-8", - ) - - packet = build_task_brief(args(root, task)).read_text(encoding="utf-8") - - assert write_path in packet - assert "review_required: false" in packet - - -@pytest.mark.parametrize("write_path", ["Dockerfile", "Makefile", "LICENSE", ".gitignore"]) -def test_build_task_brief_allows_new_extensionless_write_files( - tmp_path: Path, write_path: str -) -> None: - root, _, task = workspace(tmp_path) - task.write_text( - task.read_text(encoding="utf-8").replace( - f"write: [{WRITE_SCOPE_FILE}]", - f"write: [{write_path}]", - ), - encoding="utf-8", - ) - - packet = build_task_brief(args(root, task)).read_text(encoding="utf-8") - - assert write_path in packet - - -def test_validate_executor_result_rejects_missing_plan_without_review_package(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - retarget_plan(root, task, "plan-B") - handoff_path = write_related_handoff(root, "related:\n task: task-004\n") - review_target = root / ".work-bundle/runtime/execution/plan-B/task-004/review-package.md" - brief = _compiled_brief(root, task) - - with pytest.raises(SystemExit, match="Handoff plan identity missing"): - _validate_observed(_read_handoff(handoff_path), brief) - - assert not review_target.exists() - - -def test_validate_executor_result_rejects_mismatched_plan_without_review_package(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - retarget_plan(root, task, "plan-B") - handoff_path = write_related_handoff(root, "related:\n plan: plan-A\n task: task-004\n") - brief = _compiled_brief(root, task) - - with pytest.raises(SystemExit, match="Handoff plan mismatch: expected plan-B, got plan-A"): - _validate_observed(_read_handoff(handoff_path), brief) - - -def test_validate_executor_result_rejects_invalid_disposition_without_review_package( - tmp_path: Path, -) -> None: - root, _, task = workspace(tmp_path) - handoff_path = write_executor_handoff( - root, - " action: write-now\n reason: Executor should persist knowledge.\n", - ) - brief = _compiled_brief(root, task) - - with pytest.raises(SystemExit, match="knowledge disposition action"): - _validate_observed(_read_handoff(handoff_path), brief) - - -def test_validate_executor_result_rejects_completed_result_with_unresolved(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - handoff_path = write_executor_handoff( - root, - " action: none\n reason: No stable authority changed.\n affected_authority: []\n" - "unresolved:\n - leftover blocker\n", - ) - brief = _compiled_brief(root, task) - - with pytest.raises(SystemExit, match="unresolved|blocker"): - _validate_observed(_read_handoff(handoff_path), brief) - - -def test_creation_safe_result_may_omit_controller_required_validation(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - handoff = _read_handoff(_completed_handoff_payload(root)) - handoff.pop("validation") - brief = _compiled_brief(root, task) - - created = execution_context.validate_executor_result_creation_for_task( - handoff, brief - ) - assert created["result_state"] == "completed" - with pytest.raises(SystemExit, match="missing fresh required validation"): - execution_context.validate_executor_result_for_task( - handoff, brief, observe=False, mutation_events=[] - ) - - -def test_validate_executor_result_cli_rejects_missing_plan_identity(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - retarget_plan(root, task, "plan-B") - handoff = write_related_handoff(root, "related:\n task: task-004\n") - review_target = root / ".work-bundle/runtime/execution/plan-B/task-004/review-package.md" - - with pytest.raises(SystemExit, match="Handoff plan identity missing"): - execution_context.cmd_validate_executor_result( - args(root, task, handoff=str(handoff)) - ) - - assert not review_target.exists() - - -def test_review_package_keeps_sibling_and_rename_paths_as_out_of_scope_diagnostics( - tmp_path: Path, -) -> None: - root, _, task = workspace(tmp_path) - scoped = root / WRITE_SCOPE_FILE - scoped.parent.mkdir(parents=True, exist_ok=True) - scoped.write_text("def compile_task():\n return 'old'\n", encoding="utf-8") - sibling = root / "src/sibling.py" - sibling.parent.mkdir(parents=True, exist_ok=True) - sibling.write_text("SIBLING_OLD = 1\n", encoding="utf-8") - companion = root / "src/generated_companion.py" - companion.write_text("COMPANION_OLD = 1\n", encoding="utf-8") - git(root, "add", ".") - git(root, "commit", "-qm", "base") - base = git(root, "rev-parse", "HEAD") - scoped.write_text("def compile_task():\n return 'scoped'\n", encoding="utf-8") - sibling.write_text("SIBLING_NEW = 2\n", encoding="utf-8") - git(root, "mv", "src/generated_companion.py", "src/generated_companion.renamed.py") - handoff = _bind_passing_observation(root, task) - - package = build_review_package( - args(root, task, handoff=str(handoff), base=base, head="worktree") - ).read_text(encoding="utf-8") - diff = package.split("## Diff", 1)[1].split("## Review rubric", 1)[0] - diagnostics = package.split("## Out-of-scope changes", 1)[1].split("## ", 1)[0] - - assert "## Out-of-scope changes" in package - assert "return 'scoped'" in diff - assert "SIBLING_NEW" not in diff - assert "src/sibling.py" in diagnostics - assert "src/generated_companion.py" in diagnostics - assert "src/generated_companion.renamed.py" in diagnostics - assert "No out-of-scope change is present" not in package - - -def test_review_package_projects_committed_oversized_diff_by_exact_source_identity( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - root, _, task = workspace(tmp_path) - monkeypatch.setattr(execution_context, "MAX_DIFF_BYTES", 120) - scoped = root / WRITE_SCOPE_FILE - scoped.parent.mkdir(parents=True, exist_ok=True) - scoped.write_text("def compile_task():\n return 'old'\n", encoding="utf-8") - outsider = root / "src/huge_sibling.py" - outsider.parent.mkdir(parents=True, exist_ok=True) - outsider.write_text("OUTSIDE = 'x'\n", encoding="utf-8") - git(root, "add", ".") - git(root, "commit", "-qm", "base") - base = git(root, "rev-parse", "HEAD") - scoped.write_text("def compile_task():\n return 'IN-SCOPE-OVERFLOW-PAYLOAD'\n", encoding="utf-8") - outsider.write_text("OUTSIDE = '" + ("Y" * 400) + "'\n", encoding="utf-8") - git(root, "add", ".") - git(root, "commit", "-qm", "oversized committed task result") - head = git(root, "rev-parse", "HEAD") - head_tree = git(root, "rev-parse", "HEAD^{tree}") - handoff = _bind_passing_observation(root, task) - - target = build_review_package( - args(root, task, handoff=str(handoff), base=base, head=head) - ) - package = target.read_text(encoding="utf-8") - metrics = json.loads( - target.with_name("review-package-metrics.json").read_text(encoding="utf-8") - )["compiled_context_metrics"] - - assert "Exact source diff reference" in package - assert f"Base commit: {base}" in package - assert f"Head commit: {head}" in package - assert f"Head tree: {head_tree}" in package - assert WRITE_SCOPE_FILE in package - assert "IN-SCOPE-OVERFLOW-PAYLOAD" not in package - assert metrics["omitted_by_reference_bytes"] > 0 - - -def test_review_package_does_not_overflow_on_out_of_scope_payload( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - root, _, task = workspace(tmp_path) - monkeypatch.setattr(execution_context, "MAX_DIFF_BYTES", 800) - scoped = root / WRITE_SCOPE_FILE - scoped.parent.mkdir(parents=True, exist_ok=True) - scoped.write_text("def compile_task():\n return 'old'\n", encoding="utf-8") - outsider = root / "src/huge_sibling.py" - outsider.parent.mkdir(parents=True, exist_ok=True) - outsider.write_text("OUTSIDE = 'x'\n", encoding="utf-8") - git(root, "add", ".") - git(root, "commit", "-qm", "base") - base = git(root, "rev-parse", "HEAD") - scoped.write_text("def compile_task():\n return 'ok'\n", encoding="utf-8") - outsider.write_text("OUTSIDE = '" + ("Z" * 4000) + "'\n", encoding="utf-8") - handoff = _bind_passing_observation(root, task) - - package = build_review_package( - args(root, task, handoff=str(handoff), base=base, head="worktree") - ).read_text(encoding="utf-8") - - assert "return 'ok'" in package - assert "Z" * 50 not in package - assert "src/huge_sibling.py" in package.split("## Out-of-scope changes", 1)[1] - - -def test_no_review_completed_update_promotes_closure_when_return_missing() -> None: - handoffs = [ - { - "related": {"plan": "plan-001", "task": "task-004"}, - "result": {"state": "completed"}, - "acceptance_review": {"required": False}, - "knowledge_disposition": { - "action": "update", - "reason": "Task-local evidence.", - "affected_authority": [ACCEPTED_AUTHORITY], - }, - } - ] - - result = execution_context.evaluate_knowledge_closure_state( - upstream_disposition="not-needed", - accepted_task_handoffs=handoffs, - closure_return="missing", - ) - - assert (result["disposition"], result["archive_blocked"]) == ("required", True) - assert result["triggers"] == [{"task": "task-004", "action": "update"}] - - -def test_review_required_update_without_accept_is_not_closure_eligible() -> None: - handoffs = [ - { - "related": {"plan": "plan-001", "task": "task-004"}, - "result": {"state": "completed"}, - "acceptance_review": {"required": True, "verdict": "pending"}, - "knowledge_disposition": { - "action": "update", - "reason": "Task-local evidence.", - "affected_authority": [ACCEPTED_AUTHORITY], - }, - } - ] - - result = execution_context.evaluate_knowledge_closure_state( - upstream_disposition="not-needed", - accepted_task_handoffs=handoffs, - closure_return="missing", - ) - - assert (result["disposition"], result["archive_blocked"]) == ("not-needed", False) - - -@pytest.mark.parametrize("state", ["blocked", "failed", "partial"]) -def test_ineligible_result_states_do_not_promote_closure(state: str) -> None: - handoffs = [ - { - "related": {"plan": "plan-001", "task": "task-004"}, - "result": {"state": state}, - "unresolved": ["still open"] if state == "partial" else [], - "acceptance_review": {"required": False}, - "knowledge_disposition": { - "action": "update", - "reason": "Task-local evidence.", - "affected_authority": [ACCEPTED_AUTHORITY], - }, - } - ] - - result = execution_context.evaluate_knowledge_closure_state( - upstream_disposition="not-needed", - accepted_task_handoffs=handoffs, - closure_return="missing", - ) - - assert (result["disposition"], result["archive_blocked"]) == ("not-needed", False) - - -def test_missing_result_state_is_not_closure_eligible() -> None: - handoffs = [ - { - "related": {"plan": "plan-001", "task": "task-004"}, - "result": {"state": None}, - "acceptance_review": {"required": False}, - "knowledge_disposition": { - "action": "update", - "reason": "Task-local evidence.", - "affected_authority": [ACCEPTED_AUTHORITY], - }, - } - ] - - result = execution_context.evaluate_knowledge_closure_state( - upstream_disposition="not-needed", - accepted_task_handoffs=handoffs, - closure_return="missing", - ) - - assert (result["disposition"], result["archive_blocked"]) == ("not-needed", False) - - -def _completed_handoff_payload( - root: Path, - *, - validation_result: str = "passed", - extra: str = "", -) -> Path: - handoff = root / ".work-bundle/orchestration/handoff/executor/active/handoff-task-004.yaml" - handoff.parent.mkdir(parents=True, exist_ok=True) - handoff.write_text( - "id: handoff-task-004\n" - "type: executor-result\n" - "related: {plan: plan-001, task: task-004}\n" - "result: {state: completed}\n" - "task_fit_check: {task: task-004, result: clean}\n" - "validation:\n" - " commands:\n" - f" - {{command: {TASK_VALIDATION_COMMAND}, result: {validation_result}}}\n" - f"{evidence_blocks(root)}" - "knowledge_disposition:\n" - " action: none\n" - " reason: No stable authority changed.\n" - " affected_authority: []\n" - f"{extra}", - encoding="utf-8", - ) - return handoff - - -def test_validate_executor_result_rejects_failed_required_command(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - handoff = _completed_handoff_payload(root, validation_result="failed") - brief = _compiled_brief(root, task) - - with pytest.raises(SystemExit, match="failed|passed|validation"): - _validate_observed(_read_handoff(handoff), brief) - - -@pytest.mark.parametrize("missing", ["repository", "codegraph"]) -def test_validate_executor_result_fails_closed_when_applicable_evidence_is_missing( - tmp_path: Path, missing: str -) -> None: - root, _, task = workspace(tmp_path) - handoff = _read_handoff(_completed_handoff_payload(root)) - handoff.pop(missing) - brief = _compiled_brief(root, task) - - with pytest.raises(SystemExit, match=f"(?i){missing}"): - execution_context.validate_executor_result_for_task(handoff, brief) - - -@pytest.mark.parametrize( - "malformed", - [ - None, - [], - [{"root": "/tmp/repo", "applicable": False, "up_to_date": True, "reason": "no-index"}], - [{"root": "/tmp/repo", "applicable": False, "up_to_date": False, "reason": None}], - [{"root": "/tmp/repo", "applicable": True, "up_to_date": False, "reason": None}], - ], -) -def test_validate_executor_result_rejects_malformed_codegraph_evidence( - tmp_path: Path, malformed: object -) -> None: - root, _, task = workspace(tmp_path) - handoff = _read_handoff(_completed_handoff_payload(root)) - handoff["codegraph"] = malformed - brief = _compiled_brief(root, task) - - with pytest.raises(SystemExit, match="CodeGraph|codegraph"): - execution_context.validate_executor_result_for_task(handoff, brief) - - -def test_validate_executor_result_accepts_explicit_shaped_no_index_evidence(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - handoff = _read_handoff(_completed_handoff_payload(root)) - brief = _compiled_brief(root, task) - - validated = execution_context.validate_executor_result_for_task( - handoff, brief, mutation_events=[] - ) - - assert validated["evidence_applicability"] == brief["evidence_applicability"] - - -def test_helper_observed_codegraph_marker_overrides_executor_no_index_claim(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - _set_process_validation(task, PASSING_PROCESS) - (root / ".codegraph").mkdir() - brief = _compiled_brief(root, task) - _bind_task_execution(root, brief) - handoff = _read_handoff(_handoff_for_command(root, PASSING_PROCESS)) - - with pytest.raises(SystemExit, match="CodeGraph|codegraph|no-index"): - execution_context.validate_executor_result_for_task(handoff, brief, observe=True) - - -def test_helper_observation_accepts_metadata_only_applicability_without_codegraph( - tmp_path: Path, -) -> None: - root, _, task = workspace(tmp_path) - _set_process_validation(task, PASSING_PROCESS) - brief = _compiled_brief(root, task) - brief["evidence_applicability"] = { - "metadata": {"required": True, "reasons": ["project-metadata-preflight"]}, - "repository": {"required": False, "reasons": []}, - "codegraph": {"required": False, "reasons": []}, - } - _bind_task_execution(root, brief) - handoff = _read_handoff(_handoff_for_command(root, PASSING_PROCESS)) - handoff.pop("codegraph") - actual_branch = git(root, "branch", "--show-current") - actual_commit = git(root, "rev-parse", "HEAD") - handoff["repository"][0]["metadata"] = { - "repository_id": "repo1", - "expected_branch": actual_branch, - "actual_branch": actual_branch, - "branch_status": "matched", - "expected_commit": actual_commit, - "actual_commit": actual_commit, - "commit_status": "matched", - "baseline_status": "current", - } - - validated = execution_context.validate_executor_result_for_task( - handoff, brief, observe=True, mutation_events=[] - ) - - assert validated["evidence_applicability"]["codegraph"]["required"] is False - - -def test_helper_observation_rejects_executor_repository_identity_that_is_not_live( - tmp_path: Path, -) -> None: - root, _, task = workspace(tmp_path) - _set_process_validation(task, PASSING_PROCESS) - brief = _compiled_brief(root, task) - brief["evidence_applicability"] = { - "metadata": {"required": True, "reasons": ["project-metadata-preflight"]}, - "repository": {"required": False, "reasons": []}, - "codegraph": {"required": False, "reasons": []}, - } - _bind_task_execution(root, brief) - handoff = _read_handoff(_handoff_for_command(root, PASSING_PROCESS)) - handoff.pop("codegraph") - handoff["repository"][0]["metadata"] = { - "repository_id": "repo1", - "expected_branch": git(root, "branch", "--show-current"), - "actual_branch": git(root, "branch", "--show-current"), - "branch_status": "matched", - "expected_commit": "forged-commit", - "actual_commit": "forged-commit", - "commit_status": "matched", - "baseline_status": "current", - } - - with pytest.raises(SystemExit, match="repository|commit|identity|observed"): - execution_context.validate_executor_result_for_task(handoff, brief, observe=True) - - -def test_helper_observation_rejects_unverifiable_codegraph_up_to_date_claim( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, -) -> None: - root, _, task = workspace(tmp_path) - _set_process_validation(task, PASSING_PROCESS) - (root / ".codegraph").mkdir() - brief = _compiled_brief(root, task) - _bind_task_execution(root, brief) - handoff = _read_handoff(_handoff_for_command(root, PASSING_PROCESS, extra="")) - handoff["codegraph"] = [ - {"root": str(root.resolve()), "applicable": True, "up_to_date": True, "reason": None} - ] - original_run = execution_context.subprocess.run - - def missing_codegraph(args: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: - if args[0] == "codegraph": - raise FileNotFoundError("codegraph") - return original_run(args, **kwargs) - - monkeypatch.setattr(execution_context.subprocess, "run", missing_codegraph) - - with pytest.raises(SystemExit, match="CodeGraph status is unavailable"): - execution_context.validate_executor_result_for_task(handoff, brief, observe=True) - - -def test_validate_executor_result_rejects_skipped_required_command(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - handoff = _completed_handoff_payload(root, validation_result="skipped") - brief = _compiled_brief(root, task) - - with pytest.raises(SystemExit, match="skipped|passed|validation"): - _validate_observed(_read_handoff(handoff), brief) - - -def test_validate_executor_result_allows_skipped_when_task_expected_skip(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - _ensure_source_file(root) - task.write_text( - task.read_text(encoding="utf-8").replace( - f"{{kind: process, command: {TASK_VALIDATION_COMMAND}, proves: TEST-004, expected: exit 0}}", - f"{{kind: process, command: {TASK_VALIDATION_COMMAND}, proves: TEST-004, expected: skipped, acceptable_results: [passed, skipped]}}", - ), - encoding="utf-8", - ) - handoff = _completed_handoff_payload(root, validation_result="skipped") - brief = _compiled_brief(root, task) - _bind_task_execution(root, brief) - - validated = _validate_observed(_read_handoff(handoff), brief) - - assert validated["result_state"] == "completed" - - -def test_validate_executor_result_does_not_treat_skip_substring_as_authorization(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - task.write_text( - task.read_text(encoding="utf-8").replace( - "expected: exit 0", - "expected: must not skip", - ), - encoding="utf-8", - ) - handoff = _completed_handoff_payload(root, validation_result="skipped") - brief = _compiled_brief(root, task) - - with pytest.raises(SystemExit, match="skipped|passed|validation"): - _validate_observed(_read_handoff(handoff), brief) - - -def test_validate_executor_result_rejects_unresolved_task_fit_for_completed(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - handoff = write_executor_handoff( - root, - " action: none\n reason: No stable authority changed.\n affected_authority: []\n", - ) - handoff.write_text( - handoff.read_text(encoding="utf-8").replace( - "task_fit_check: {task: task-004, result: clean}\n", - "task_fit_check: {task: task-004, result: unresolved}\n", - ), - encoding="utf-8", - ) - brief = _compiled_brief(root, task) - - with pytest.raises(SystemExit, match="task_fit_check|unresolved|clean|repaired"): - _validate_observed(_read_handoff(handoff), brief) - - -def test_validate_executor_result_rejects_skipped_task_fit_for_completed(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - handoff = write_executor_handoff( - root, - " action: none\n reason: No stable authority changed.\n affected_authority: []\n", - ) - handoff.write_text( - handoff.read_text(encoding="utf-8").replace( - "task_fit_check: {task: task-004, result: clean}\n", - "task_fit_check: {task: task-004, result: skipped}\n", - ), - encoding="utf-8", - ) - brief = _compiled_brief(root, task) - - with pytest.raises(SystemExit, match="task_fit_check|skipped|clean|repaired"): - _validate_observed(_read_handoff(handoff), brief) - - -def test_validate_executor_result_rejects_out_of_scope_changed_path(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - handoff = _completed_handoff_payload( - root, - extra=( - "changes:\n" - " files:\n" - " - {path: src/outsider.py}\n" - ), - ) - brief = _compiled_brief(root, task) - - with pytest.raises(SystemExit, match="write scope|out-of-scope|unauthorized"): - _validate_observed(_read_handoff(handoff), brief) - - -def test_validate_executor_result_rejects_missing_task_fit_check(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - handoff = write_executor_handoff( - root, - " action: none\n reason: No stable authority changed.\n affected_authority: []\n", - ) - text = handoff.read_text(encoding="utf-8").replace( - "task_fit_check: {task: task-004, result: clean}\n", - "", - ) - handoff.write_text(text, encoding="utf-8") - brief = _compiled_brief(root, task) - - with pytest.raises(SystemExit, match="task_fit_check"): - _validate_observed(_read_handoff(handoff), brief) - - -def test_validate_executor_result_rejects_review_required_downgrade(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - task.write_text( - task.read_text(encoding="utf-8").replace( - "acceptance_review:\n required: false\n", - "acceptance_review:\n required: true\n", - ), - encoding="utf-8", - ) - handoff = write_executor_handoff( - root, - " action: none\n reason: No stable authority changed.\n affected_authority: []\n", - ) - handoff.write_text( - handoff.read_text(encoding="utf-8").replace( - "result: {state: completed}\n", - "result: {state: completed}\nacceptance_review: {required: false}\n", - ), - encoding="utf-8", - ) - brief = _compiled_brief(root, task) - - with pytest.raises(SystemExit, match="review"): - _validate_observed(_read_handoff(handoff), brief) - - -def test_validate_executor_result_rejects_review_required_upgrade(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - handoff = write_executor_handoff( - root, - " action: none\n reason: No stable authority changed.\n affected_authority: []\n", - ) - text = handoff.read_text(encoding="utf-8") - handoff.write_text( - text.replace( - "result: {state: completed}\n", - "result: {state: completed}\nacceptance_review: {required: true, verdict: pending}\n", - ), - encoding="utf-8", - ) - brief = _compiled_brief(root, task) - - with pytest.raises(SystemExit, match="review"): - _validate_observed(_read_handoff(handoff), brief) - - -def _repair_completion_fixture() -> tuple[dict, dict]: - brief = { - "task_id": "task-rf", "plan_id": "plan-rf", "review_required": True, - "source_ids": [], "files": {"read": [], "write": [], "forbidden": []}, - "truth_basis": {}, "validation": [], - "evidence_applicability": { - "metadata": {"required": False, "reasons": []}, - "repository": {"required": False, "reasons": []}, - "codegraph": {"required": False, "reasons": []}, - }, - "evidence_capability": {"result": "no_validation_bearing_obligation", "invariants": []}, - } - handoff = { - "type": "executor-result", "related": {"plan": "plan-rf", "task": "task-rf"}, - "result": {"state": "completed"}, - "task_fit_check": {"task": "task-rf", "result": "repaired"}, - "acceptance_review": {"required": True, "verdict": "accept"}, - "delegation_evidence": { - "delegated": True, - "owner_kind": "subagent", - "agent_id": "repair-fixture-agent", - "run_id": "repair-fixture-run", - "mechanism": "host-native", - }, - "knowledge_disposition": {"action": "none", "reason": "No authority change.", "affected_authority": []}, - } - return brief, handoff - - -def test_initial_review_preparation_accepts_sparse_repaired_task_fit() -> None: - brief, handoff = _repair_completion_fixture() - handoff.pop("acceptance_review") - - prepared = execution_context.validate_executor_result_for_task( - handoff, - brief, - observe=True, - preparing_review=True, - mutation_events=[], - ) - - assert prepared["result_state"] == "completed" - assert prepared["task_ownership"]["agent_id"] == "repair-fixture-agent" - - -def test_terminal_validation_accepts_sparse_review_required_executor_result() -> None: - brief, handoff = _repair_completion_fixture() - handoff.pop("acceptance_review") - handoff["task_fit_check"]["result"] = "clean" - - validated = execution_context.validate_executor_result_for_task( - handoff, - brief, - observe=True, - mutation_events=[], - ) - - assert validated["result_state"] == "completed" - assert validated["task_ownership"]["agent_id"] == "repair-fixture-agent" - - -def test_terminal_cli_defers_review_required_result_materialization( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] -) -> None: - handoff_path = tmp_path / "handoff.yaml" - handoff_path.write_text("type: executor-result\n", encoding="utf-8") - task = {"task_id": "task-rf", "plan_id": "plan-rf", "review_required": True} - monkeypatch.setattr( - execution_context, - "_compile_task_brief", - lambda _args: (tmp_path / "task.md", {"task_brief": task}), - ) - monkeypatch.setattr(execution_context, "resolve_workspace_root", lambda _args: tmp_path) - monkeypatch.setattr( - execution_context, "_input_path", lambda *_args: handoff_path - ) - monkeypatch.setattr( - execution_context, "_read_structured", lambda _path: ({}, "") - ) - monkeypatch.setattr( - execution_context, - "validate_executor_result_for_task", - lambda *_args, **_kwargs: {"result_state": "completed"}, - ) - monkeypatch.setattr( - execution_context, - "materialize_accepted_task_result", - lambda *_args, **_kwargs: pytest.fail( - "review-required materialization must wait for stored review authority" - ), - ) - - execution_context.cmd_validate_executor_result( - argparse.Namespace(handoff=str(handoff_path)) - ) - - assert capsys.readouterr().out.strip() == "handoff.yaml" - - -def test_repair_review_preparation_requires_prior_owner_continuity() -> None: - brief, handoff = _repair_completion_fixture() - handoff.pop("acceptance_review") - - with pytest.raises( - execution_context.AcceptanceOwnershipError, match="repair lacks prior owner" - ): - execution_context.validate_executor_result_for_task( - handoff, - brief, - observe=True, - preparing_review=True, - repair_review_preparation=True, - mutation_events=[], - ) - - -def test_rf_task_repair_completion_rejects_unsequenced_acceptance_review() -> None: - brief, handoff = _repair_completion_fixture() - with pytest.raises(SystemExit, match="repair.*review|review.*repair"): - execution_context.validate_executor_result_for_task(handoff, brief) - - -def _material_reset_task_review() -> dict: - prior_identity = { - "artifact_id": "task-rf", "revision": "1", "sha256": "1" * 64, "source_tree": None, - } - current_identity = { - "artifact_id": "task-rf", "revision": "1", "sha256": "2" * 64, "source_tree": None, - } - prior = { - "required": True, - "reviewer_independent": True, - "verdict": "repair", - "review_id": "review-prior-material", - "review_mode": "initial", - "review_target_kind": "task", - "repair_frontier": None, - "review_reset": None, - "target_identity": prior_identity, - "reviewer": { - "agent_id": "reviewer-prior", - "capability": "judgment", - "authorship": "none", - "repair_participation": "none", - "decision_participation": "none", - "deliberation_participation": "none", - "context_origin": "direct_source", - }, - "evidence": { - "mode": "direct", "capabilities": ["source inspection"], - "unavailable_evidence": [], "commands": [], "artifacts": [], - }, - "findings": [{ - "finding_id": "RF-MATERIAL-1", - "stage": "implementation", - "class": "implementation_defect", - "severity": "blocking", - "first_broken_artifact": "implementation", - "obligation_basis": "accepted_requirement", - "evidence": [{ - "kind": "test", "locator": "RF", "digest_or_identity": "RF-RED", - "observation": "failed", - }], - "target_identity": prior_identity, - "summary": "Accepted boundary changed materially.", - "recommended_owner": "task_owner", - "disposition": "repair_task", - }], - "started_at": "2026-09-06T00:00:00Z", - "completed_at": "2026-09-06T00:01:00Z", - "staleness": {"is_stale": False, "reason": None, "supersedes": None}, - } - return { - "required": True, - "reviewer_independent": True, - "verdict": "accept", - "review_id": "review-current-material-reset", - "review_mode": "initial", - "review_target_kind": "task", - "repair_frontier": None, - "review_reset": { - "prior_review_id": prior["review_id"], - "reason_class": "scope", - "reason": "Accepted write scope changed.", - }, - "target_identity": current_identity, - "reviewer": {**prior["reviewer"], "agent_id": "reviewer-reset"}, - "evidence": { - "mode": "direct", "capabilities": ["source inspection"], - "unavailable_evidence": [], "commands": [], "artifacts": [], - }, - "findings": [], - "previous_review": prior, - "started_at": "2026-09-06T00:02:00Z", - "completed_at": "2026-09-06T00:03:00Z", - "staleness": {"is_stale": False, "reason": None, "supersedes": None}, - } - - -def test_rf_repaired_task_completion_accepts_native_fresh_initial_review_reset() -> None: - brief, handoff = _repair_completion_fixture() - handoff["acceptance_review"] = _material_reset_task_review() - - accepted = execution_context.validate_executor_result_for_task( - handoff, brief, mutation_events=[] - ) - - assert accepted["result_state"] == "completed" - assert accepted["task_ownership"]["agent_id"] == "repair-fixture-agent" - - -def test_rf_repaired_task_completion_rejects_unclassified_initial_review_reset() -> None: - brief, handoff = _repair_completion_fixture() - handoff["acceptance_review"] = _material_reset_task_review() - handoff["acceptance_review"]["review_reset"]["reason_class"] = "fixture_recovery" - - with pytest.raises(SystemExit, match="review_reset|material_change"): - execution_context.validate_executor_result_for_task( - handoff, brief, mutation_events=[] - ) - - -@pytest.mark.parametrize( - "mutation_events", - [ - [{}], - [{"paths": ["src/a.py"]}], - [{"actor_kind": "controller"}], - [{"actor_kind": 42, "paths": ["src/a.py"]}], - [{"actor_kind": "unknown", "paths": ["src/a.py"]}], - [{"actor_kind": "subagent", "paths": "src/a.py"}], - [{"actor_kind": "subagent", "paths": []}], - [{"actor_kind": "subagent", "paths": [42]}], - [{"actor_kind": "subagent", "paths": ["../src/a.py"]}], - [{"actor_kind": "subagent", "paths": ["/tmp/src/a.py"]}], - [{"actor_kind": "subagent", "paths": ["src/a.py"], "extra": True}], - ], -) -def test_completed_task_rejects_malformed_mutation_evidence( - tmp_path: Path, mutation_events: object -) -> None: - _, _, brief, handoff, _ = _counted_validation(tmp_path) - - with pytest.raises(SystemExit, match="mutation_events|mutation event"): - execution_context.validate_executor_result_for_task( - handoff, - brief, - observe=True, - mutation_events=mutation_events, - ) - - -def test_completed_task_accepts_complete_subagent_mutation_evidence(tmp_path: Path) -> None: - _, _, brief, handoff, _ = _counted_validation(tmp_path) - - accepted = execution_context.validate_executor_result_for_task( - handoff, - brief, - observe=True, - mutation_events=[{"actor_kind": "subagent", "paths": [WRITE_SCOPE_FILE]}], - ) - - assert accepted["result_state"] == "completed" - - -def test_completed_task_rejects_controller_mutation_with_dot_segment() -> None: - brief, handoff = _repair_completion_fixture() - brief["review_required"] = False - brief["files"]["write"] = ["src/a.py"] - handoff["task_fit_check"]["result"] = "clean" - handoff["acceptance_review"] = {"required": False} - - with pytest.raises(SystemExit, match="controller mutated task-owned implementation scope"): - execution_context.validate_executor_result_for_task( - handoff, - brief, - mutation_events=[{"actor_kind": "controller", "paths": ["src/./a.py"]}], - ) - - -def test_completed_task_rejects_controller_mutation_with_repeated_separator() -> None: - brief, handoff = _repair_completion_fixture() - brief["review_required"] = False - brief["files"]["write"] = ["src/a.py"] - handoff["task_fit_check"]["result"] = "clean" - handoff["acceptance_review"] = {"required": False} - - with pytest.raises(SystemExit, match="controller mutated task-owned implementation scope"): - execution_context.validate_executor_result_for_task( - handoff, - brief, - mutation_events=[{"actor_kind": "controller", "paths": ["src//a.py"]}], - ) - - -def test_rf_task_repair_completion_rejects_stale_repair_frontier(tmp_path: Path) -> None: - brief, handoff = _repair_completion_fixture() - git(tmp_path, "init", "-q") - git(tmp_path, "config", "user.name", "Test") - git(tmp_path, "config", "user.email", "test@example.com") - source = tmp_path / "source.py" - source.write_text("OLD = True\n") - git(tmp_path, "add", ".") - git(tmp_path, "commit", "-qm", "old") - old_tree = git(tmp_path, "rev-parse", "HEAD^{tree}") - source.write_text("NEW = True\n") - git(tmp_path, "add", ".") - git(tmp_path, "commit", "-qm", "repaired") - repaired_head = git(tmp_path, "rev-parse", "HEAD") - repaired_tree = git(tmp_path, "rev-parse", "HEAD^{tree}") - old = {"artifact_id": "task-rf", "revision": "1", "sha256": "1" * 64, "source_tree": old_tree} - new = {"artifact_id": "task-rf", "revision": "1", "sha256": "2" * 64, "source_tree": repaired_tree} - prior = { - "required": True, "reviewer_independent": True, "verdict": "repair", "review_id": "review-prior", - "review_mode": "initial", "review_target_kind": "task", "repair_frontier": None, "review_reset": None, - "target_identity": old, - "reviewer": {"agent_id": "reviewer-prior", "capability": "judgment", "authorship": "none", "repair_participation": "none", "decision_participation": "none", "deliberation_participation": "none", "context_origin": "direct_source"}, - "evidence": {"mode": "direct", "capabilities": ["source inspection"], "unavailable_evidence": [], "commands": [], "artifacts": []}, - "findings": [{ - "finding_id": "RF-FINDING-1", "stage": "implementation", "class": "implementation_defect", "severity": "blocking", - "first_broken_artifact": "implementation", "obligation_basis": "accepted_requirement", - "evidence": [{"kind": "test", "locator": "RF", "digest_or_identity": "RF-RED", "observation": "failed"}], - "target_identity": old, "summary": "repair", "recommended_owner": "task_owner", "disposition": "repair_task", - }], - "started_at": "2026-09-06T00:00:00Z", "completed_at": "2026-09-06T00:01:00Z", - "staleness": {"is_stale": False, "reason": None, "supersedes": None}, - } - from review_runtime import review_evidence_identity - handoff["acceptance_review"] = { - "required": True, "reviewer_independent": True, "verdict": "accept", "review_id": "review-repair", - "reviewed_head": repaired_head, - "review_mode": "repair", "review_target_kind": "task", "review_reset": None, - "repair_frontier": { - "prior_review_id": "review-prior", "blocking_finding_ids": ["RF-FINDING-1"], - "previous_reviewed_identity": old, "repaired_identity": new, - "affected_boundaries": ["scripts/orchestration/execution_context.py:_assert_handoff_review_matches_task"], - "frozen_evidence_reference": review_evidence_identity(prior), - }, - "target_identity": new, - "reviewer": {**prior["reviewer"], "agent_id": "reviewer-new"}, - "evidence": {"mode": "direct", "capabilities": ["bounded source inspection"], "unavailable_evidence": [], "commands": [], "artifacts": []}, - "findings": [], "previous_review": prior, - "started_at": "2026-09-06T00:02:00Z", "completed_at": "2026-09-06T00:03:00Z", - "staleness": {"is_stale": False, "reason": None, "supersedes": None}, - } - handoff["repository"] = [{"root": str(tmp_path), "target_kind": "git-backed", "preflight_kind": "git-clean-worktree", "baseline": "initial", "status": "clean"}] - execution_context._assert_handoff_review_matches_task(handoff, brief, "completed") - stale = deepcopy(handoff) - stale["acceptance_review"]["target_identity"] = {**new, "sha256": "3" * 64} - with pytest.raises(SystemExit, match="repaired identity|frontier"): - execution_context._assert_handoff_review_matches_task(stale, brief, "completed") - for field, value in (("required", False), ("reviewer_independent", False), ("review_target_kind", "wrong-kind")): - invalid_prior = deepcopy(handoff) - invalid_prior["acceptance_review"]["previous_review"][field] = value - with pytest.raises(SystemExit, match="previous|task acceptance|review_target_kind"): - execution_context._assert_handoff_review_matches_task(invalid_prior, brief, "completed") - fabricated = deepcopy(handoff) - fabricated_identity = {**new, "sha256": "3" * 64, "source_tree": "f" * 40} - fabricated["acceptance_review"]["target_identity"] = fabricated_identity - fabricated["acceptance_review"]["repair_frontier"]["repaired_identity"] = fabricated_identity - with pytest.raises(SystemExit, match="head|tree|Git identity"): - execution_context._assert_handoff_review_matches_task(fabricated, brief, "completed") - - -def test_no_review_update_stays_closure_eligible_when_handoff_self_upgrades() -> None: - handoffs = [ - { - "related": {"plan": "plan-001", "task": "task-004"}, - "result": {"state": "completed"}, - "acceptance_review": {"required": True, "verdict": "pending"}, - "knowledge_disposition": { - "action": "update", - "reason": "Task-local evidence.", - "affected_authority": [ACCEPTED_AUTHORITY], - }, - } - ] - - result = execution_context.evaluate_knowledge_closure_state( - upstream_disposition="not-needed", - accepted_task_handoffs=handoffs, - closure_return="missing", - review_required_by_task={"task-004": False}, - ) - - assert (result["disposition"], result["archive_blocked"]) == ("required", True) - - -def test_set_plan_status_completed_requires_validated_handoff(tmp_path: Path) -> None: - from plans import cmd_set_plan_status - - root, _, task = workspace(tmp_path) - with pytest.raises(SystemExit, match="handoff"): - cmd_set_plan_status( - argparse.Namespace( - project_root=str(root), - id="task-004", - status="Completed", - kind="task", - ) - ) - - failed = _completed_handoff_payload(root, validation_result="failed") - with pytest.raises(SystemExit, match="failed|passed|validation"): - cmd_set_plan_status( - argparse.Namespace( - project_root=str(root), - id="task-004", - status="Completed", - kind="task", - handoff=str(failed), - ) - ) - - handoff = _completed_handoff_payload(root, validation_result="passed") - _ensure_source_file(root) - _set_process_validation(task, PASSING_PROCESS) - brief = _compiled_brief(root, task) - _bind_task_execution(root, brief) - handoff = _handoff_for_command(root, PASSING_PROCESS) - cmd_set_plan_status( - argparse.Namespace( - project_root=str(root), - id="task-004", - status="Completed", - kind="task", - handoff=str(handoff), - mutation_events=[], - ) - ) - data, _ = execution_context._read_structured(task) - assert data["status"] == "Completed" - - -DEFAULT_TASK_VALIDATION = ( - f" - {{kind: process, command: {TASK_VALIDATION_COMMAND}, proves: TEST-004, expected: exit 0}}\n" -) -UNTYPED_LEGACY_COMMAND = "echo LEGACY-UNTYPED-MUST-NOT-RUN" - - -def _set_task_validation(task: Path, yaml_block: str, body: str = "") -> None: - content = task.read_text(encoding="utf-8").replace( - f"validation:\n{DEFAULT_TASK_VALIDATION}", - yaml_block, - ) - if body: - content = content.replace("# Task\n", f"# Task\n\n{body}") - task.write_text(content, encoding="utf-8") - - -def _capture_subprocess_after_setup(monkeypatch: pytest.MonkeyPatch) -> list[str]: - calls: list[str] = [] - real_run = execution_context.subprocess.run - - def _run(*argv: object, **kwargs: object) -> subprocess.CompletedProcess[str]: - rendered = " ".join(str(part) for part in argv[0]) if argv and isinstance(argv[0], (list, tuple)) else " ".join(str(part) for part in argv) - calls.append(rendered) - if UNTYPED_LEGACY_COMMAND in rendered: - raise AssertionError(f"subprocess must not run untyped validation text: {rendered}") - return real_run(*argv, **kwargs) - - monkeypatch.setattr(execution_context.subprocess, "run", _run) - return calls - - -def test_structured_validation_kind_compiles_into_brief(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - _set_task_validation( - task, - "validation:\n" - " - kind: process\n" - f" command: {TASK_VALIDATION_COMMAND}\n" - " proves: TEST-004\n" - " expected: passed\n" - " - kind: inspection\n" - " command: inspect-write-scope\n" - " mechanism: named-harness-file-digest\n" - " proves: CON-002\n" - " expected: passed\n", - ) - - brief = _compiled_brief(root, task) - process_item, inspection_item = brief["validation"] - - assert process_item["kind"] == "process" - assert process_item["command"] == TASK_VALIDATION_COMMAND - assert inspection_item["kind"] == "inspection" - assert inspection_item["mechanism"] == "named-harness-file-digest" - assert "Never write outside the assigned files." in inspection_item["proves"] - - -def test_untyped_three_column_row_fails_closed_without_subprocess( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - root, _, task = workspace(tmp_path) - calls = _capture_subprocess_after_setup(monkeypatch) - _set_task_validation( - task, - "", - "## Validation\n\n" - "| Command or inspection | Proves | Expected |\n" - "| --- | --- | --- |\n" - f"| `{UNTYPED_LEGACY_COMMAND}` | TEST-004 | passed |\n", - ) - - with pytest.raises(SystemExit, match="legacy-untyped"): - build_task_brief(args(root, task)) - - assert not any(UNTYPED_LEGACY_COMMAND in call for call in calls) - assert not (root / ".work-bundle/runtime/execution/plan-001/task-004/task-brief.yaml").exists() - - -def test_differing_body_validation_table_does_not_fail_when_yaml_is_present(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - _set_task_validation( - task, - "validation:\n" - f" - {{kind: process, command: {TASK_VALIDATION_COMMAND}, proves: TEST-004, expected: passed}}\n", - "## Validation\n\n" - "| Command or inspection | Proves | Expected |\n" - "| --- | --- | --- |\n" - "| `echo BODY-TABLE-MUST-NOT-BLOCK` | CON-002 | failed |\n", - ) - - brief = _compiled_brief(root, task) - - assert len(brief["validation"]) == 1 - assert brief["validation"][0]["kind"] == "process" - assert brief["validation"][0]["command"] == TASK_VALIDATION_COMMAND - assert "BODY-TABLE-MUST-NOT-BLOCK" not in str(brief["validation"]) - - -def test_executor_authored_kind_is_ignored(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - _set_task_validation( - task, - "validation:\n" - " - kind: inspection\n" - " command: inspect-write-scope\n" - " mechanism: named-harness-file-digest\n" - " proves: TEST-004\n" - " expected: passed\n", - ) - handoff = _completed_handoff_payload(root) - payload = _read_handoff(handoff) - payload["validation"] = { - "commands": [{"command": "inspect-write-scope", "result": "passed", "kind": "process"}] - } - brief = _compiled_brief(root, task) - - with pytest.raises(SystemExit, match="mechanism|inspection|kind"): - _validate_observed(payload, brief) - - assert brief["validation"][0]["kind"] == "inspection" - assert brief["validation"][0]["mechanism"] == "named-harness-file-digest" - - -def test_validation_proves_expected_and_acceptable_results_preserved(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - _set_task_validation( - task, - "validation:\n" - f" - {{kind: process, command: {TASK_VALIDATION_COMMAND}, proves: TEST-004, expected: skipped, acceptable_results: [passed, skipped]}}\n", - ) - handoff = _completed_handoff_payload(root, validation_result="skipped") - brief = _compiled_brief(root, task) - item = brief["validation"][0] - - assert item["kind"] == "process" - assert "Focused pytest exits with status 0." in item["proves"] - assert item["expected"] == "skipped" - assert item["acceptable_results"] == ["passed", "skipped"] - - _ensure_source_file(root) - _bind_task_execution(root, brief) - validated = _validate_observed(_read_handoff(handoff), brief) - - assert validated["result_state"] == "completed" - - -def test_expected_skipped_without_acceptable_results_is_preserved(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - _set_task_validation( - task, - "validation:\n" - f" - {{kind: process, command: {TASK_VALIDATION_COMMAND}, proves: TEST-004, expected: skipped}}\n", - ) - handoff = _completed_handoff_payload(root, validation_result="skipped") - brief = _compiled_brief(root, task) - - assert brief["validation"][0]["expected"] == "skipped" - assert "acceptable_results" not in brief["validation"][0] - - _ensure_source_file(root) - _bind_task_execution(root, brief) - validated = _validate_observed(_read_handoff(handoff), brief) - - assert validated["result_state"] == "completed" - - -def test_four_column_body_table_is_not_an_authority_compile_path( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - root, _, task = workspace(tmp_path) - calls = _capture_subprocess_after_setup(monkeypatch) - _set_task_validation( - task, - "", - "## Validation\n\n" - "| Kind | Command or inspection | Proves | Expected |\n" - "| --- | --- | --- | --- |\n" - f"| process | `{UNTYPED_LEGACY_COMMAND}` | TEST-004 | passed |\n", - ) - - with pytest.raises(SystemExit, match="legacy-untyped"): - build_task_brief(args(root, task)) - - assert not any(UNTYPED_LEGACY_COMMAND in call for call in calls) - packet = root / ".work-bundle/runtime/execution/plan-001/task-004/task-brief.yaml" - assert not packet.exists() - - -PASSING_PROCESS = "true" -FAILING_PROCESS = "false" -WORK_BUNDLE_SCRIPTS = REPO_ROOT / "scripts" / "work-bundle" - - -def _execution_workspace(): - if str(WORK_BUNDLE_SCRIPTS) not in sys.path: - sys.path.insert(0, str(WORK_BUNDLE_SCRIPTS)) - import execution_workspace as module - - return module - - -def _ensure_source_file(root: Path, relative: str = WRITE_SCOPE_FILE, content: str = "def compile_task():\n return 'old'\n") -> Path: - path = root / relative - path.parent.mkdir(parents=True, exist_ok=True) - if not path.exists(): - path.write_text(content, encoding="utf-8") - return path - - -def _write_scope_digest(root: Path, relative: str = WRITE_SCOPE_FILE) -> str: - digest = hashlib.sha256() - digest.update(relative.encode("utf-8")) - digest.update(b"\0") - path = root / relative - if path.is_file() and not path.is_symlink(): - digest.update(path.read_bytes()) - else: - digest.update(b"MISSING") - digest.update(b"\n") - return digest.hexdigest() - - -def _ensure_git_head(root: Path) -> None: - result = subprocess.run( - ["git", "-C", str(root), "rev-parse", "--verify", "HEAD"], - capture_output=True, - text=True, - ) - if result.returncode != 0: - git(root, "add", ".") - git(root, "commit", "-qm", "bind-base") - - -def _bind_task_execution( - root: Path, - brief: dict, - *, - execution_root: Path | None = None, - runtime_root: Path | None = None, - workspace_id: str = "ws1", - execution_id: str = "exec1", - repository_id: str = "repo1", - capture_baseline: bool = True, - write_scope: list[str] | None = None, -) -> dict: - execution_root = (execution_root or root).resolve() - runtime_root = (runtime_root or (root.parent / "ew-runtime")).resolve() - ignore = execution_root / ".gitignore" - existing = ignore.read_text(encoding="utf-8") if ignore.exists() else "" - if ".work-bundle/" not in existing: - ignore.write_text(existing + ".work-bundle/\n", encoding="utf-8") - _ensure_git_head(execution_root) - ew = _execution_workspace() - record = ew.state_path(runtime_root, workspace_id, execution_id, repository_id) - if not record.exists(): - ew.register_existing( - execution_root, - workspace_id=workspace_id, - execution_id=execution_id, - repository_id=repository_id, - created_for=str(brief.get("task_id") or "task-004"), - owner="harness", - runtime_root=runtime_root, - ) - # The harness fixture represents an independently accepted current plan. - from test_orchestration_reviews import stage_review - from review_runtime import ( - artifact_review_identity, - plan_review_identity, - publish_review, - ) - plan_path, plan_data = execution_context._find_plan(root, str(brief["plan_id"])) - for spec in execution_context._resolve_spec_paths(root, {}, plan_data): - review = stage_review("specification") - review["target_identity"] = artifact_review_identity(spec) - review = bind_review_receipt(root, review) - publish_review( - root, review, current_target_identity=review["target_identity"] - ) - review = stage_review("plan") - review["target_identity"] = plan_review_identity(root, plan_path) - review = bind_review_receipt(root, review) - publish_review(root, review, current_target_identity=review["target_identity"]) - binding = execution_context.create_or_load_task_execution_binding( - control_root=root, - plan_id=str(brief["plan_id"]), - task_id=str(brief["task_id"]), - workspace_id=workspace_id, - execution_id=execution_id, - repository_id=repository_id, - runtime_root=runtime_root, - write_scope=write_scope or list((brief.get("files") or {}).get("write") or []), - forbidden_scope=list((brief.get("files") or {}).get("forbidden") or []), - ) - if capture_baseline: - execution_context.capture_task_baseline_once(binding) - binding = execution_context.load_task_execution_binding(root, str(brief["plan_id"]), str(brief["task_id"])) - return binding - - -def _bind_passing_observation(root: Path, task: Path) -> Path: - _set_process_validation(task, PASSING_PROCESS) - brief = _compiled_brief(root, task) - _bind_task_execution(root, brief) - return _handoff_for_command(root, PASSING_PROCESS) - - -def _enable_passing_observation(root: Path, task: Path, handoff: Path) -> None: - _set_process_validation(task, PASSING_PROCESS) - brief = _compiled_brief(root, task) - _bind_task_execution(root, brief) - text = handoff.read_text(encoding="utf-8") - if TASK_VALIDATION_COMMAND in text: - handoff.write_text(text.replace(TASK_VALIDATION_COMMAND, json.dumps(PASSING_PROCESS)), encoding="utf-8") - - -def _validate_observed(handoff: dict, brief: dict) -> dict: - return execution_context.validate_executor_result_for_task( - handoff, brief, observe=True, mutation_events=[] - ) - - -def _set_process_validation(task: Path, command: str, **fields: object) -> None: - extra = "".join(f", {key}: {json.dumps(value)}" for key, value in fields.items()) - _set_task_validation( - task, - "validation:\n" - f" - {{kind: process, command: {json.dumps(command)}, proves: TEST-004, expected: passed{extra}}}\n", - ) - - -def _handoff_for_command( - root: Path, - command: str, - result: str = "passed", - extra: str = "", - *, - evidence_root: Path | None = None, -) -> Path: - handoff = root / ".work-bundle/orchestration/handoff/executor/active/handoff-task-004.yaml" - handoff.parent.mkdir(parents=True, exist_ok=True) - handoff.write_text( - "id: handoff-task-004\n" - "type: executor-result\n" - "related: {plan: plan-001, task: task-004}\n" - "result: {state: completed}\n" - "task_fit_check: {task: task-004, result: clean}\n" - "validation:\n" - " commands:\n" - f" - {{command: {json.dumps(command)}, result: {result}}}\n" - f"{evidence_blocks(evidence_root or root)}" - "knowledge_disposition:\n" - " action: none\n" - " reason: No stable authority changed.\n" - " affected_authority: []\n" - f"{extra}", - encoding="utf-8", - ) - return handoff - - -def test_completed_without_harness_provenance_fails_closed(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - _set_process_validation(task, PASSING_PROCESS) - handoff = _handoff_for_command(root, PASSING_PROCESS) - brief = _compiled_brief(root, task) - - with pytest.raises(SystemExit, match="harness|binding|provenance"): - _validate_observed(_read_handoff(handoff), brief) - - -def test_nonzero_process_fails_unless_acceptable_results_include_failed(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - _ensure_source_file(root) - _set_process_validation(task, FAILING_PROCESS) - brief = _compiled_brief(root, task) - _bind_task_execution(root, brief) - handoff = _handoff_for_command(root, FAILING_PROCESS, result="failed") - - with pytest.raises(SystemExit, match="failed|passed|acceptable"): - _validate_observed(_read_handoff(handoff), brief) - - task.write_text( - task.read_text(encoding="utf-8").replace( - f"command: {json.dumps(FAILING_PROCESS)}, proves: TEST-004, expected: passed", - f"command: {json.dumps(FAILING_PROCESS)}, proves: TEST-004, expected: passed, acceptable_results: [failed]", - ), - encoding="utf-8", - ) - brief = _compiled_brief(root, task) - # Changing acceptable results is a semantic plan repair requiring a fresh review. - _bind_task_execution(root, brief) - validated = _validate_observed(_read_handoff(handoff), brief) - assert validated["result_state"] == "completed" - - passed_label = _handoff_for_command(root, FAILING_PROCESS, result="passed") - with pytest.raises(SystemExit, match="passed|failed|observed"): - _validate_observed(_read_handoff(passed_label), brief) - - -def test_expected_skipped_observes_skipped_without_running_process(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - root, _, task = workspace(tmp_path) - _ensure_source_file(root) - _set_task_validation( - task, - "validation:\n" - f" - {{kind: process, command: {json.dumps(FAILING_PROCESS)}, proves: TEST-004, expected: skipped}}\n", - ) - brief = _compiled_brief(root, task) - _bind_task_execution(root, brief) - calls = _capture_subprocess_after_setup(monkeypatch) - handoff = _handoff_for_command(root, FAILING_PROCESS, result="skipped") - - validated = _validate_observed(_read_handoff(handoff), brief) - - assert validated["result_state"] == "completed" - assert not any(FAILING_PROCESS in call for call in calls) - - -def test_named_inspection_does_not_use_executor_exit_code(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - _ensure_source_file(root) - digest = _write_scope_digest(root) - _set_task_validation( - task, - "validation:\n" - " - kind: inspection\n" - " command: inspect-write-scope\n" - " mechanism: named-harness-file-digest\n" - f" digest: {digest}\n" - " proves: TEST-004\n" - " expected: passed\n", - ) - brief = _compiled_brief(root, task) - _bind_task_execution(root, brief) - handoff = _handoff_for_command(root, "inspect-write-scope") - payload = _read_handoff(handoff) - payload["validation"] = { - "commands": [ - { - "command": "inspect-write-scope", - "result": "passed", - "exit_code": 0, - "mechanism": "named-harness-file-digest", - } - ] - } - - validated = _validate_observed(payload, brief) - - assert validated["result_state"] == "completed" - assert "exit_code" not in str(validated.get("observed_validation") or {}) - - -def test_wrong_worktree_cannot_grant_completed(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - _ensure_source_file(root) - git(root, "add", ".") - git(root, "commit", "-qm", "base") - _set_process_validation(task, PASSING_PROCESS) - brief = _compiled_brief(root, task) - ew = _execution_workspace() - runtime = tmp_path / "ew-runtime" - other = ew.prepare_worktree( - root, - workspace_id="ws1", - execution_id="other-exec", - repository_id="repo1", - branch="codex/other-exec", - created_for="other-task", - runtime_root=runtime, - ) - other_root = Path(str(other["execution_workspace_state"]["path"])) - _bind_task_execution( - root, - brief, - execution_root=other_root, - runtime_root=runtime, - execution_id="other-exec", - ) - (other_root / "unauthorized-other.py").write_text("leak\n", encoding="utf-8") - handoff = _handoff_for_command(root, PASSING_PROCESS, evidence_root=other_root) - - with pytest.raises(SystemExit, match="write scope|unauthorized|delta"): - _validate_observed(_read_handoff(handoff), brief) - - -def test_control_root_observation_cannot_authorize_isolated_worktree(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - _ensure_source_file(root) - git(root, "add", ".") - git(root, "commit", "-qm", "base") - marker_command = "python3 -c \"import pathlib,sys; sys.exit(0 if pathlib.Path('bound-marker.txt').exists() else 2)\"" - # quoted below via json.dumps in helpers - _set_process_validation(task, marker_command) - brief = _compiled_brief(root, task) - (root / "bound-marker.txt").write_text("control-only\n", encoding="utf-8") - ew = _execution_workspace() - runtime = tmp_path / "ew-runtime" - isolated = ew.prepare_worktree( - root, - workspace_id="ws1", - execution_id="iso-exec", - repository_id="repo1", - branch="codex/iso-exec", - created_for="task-004", - runtime_root=runtime, - ) - isolated_root = Path(str(isolated["execution_workspace_state"]["path"])) - _bind_task_execution( - root, - brief, - execution_root=isolated_root, - runtime_root=runtime, - execution_id="iso-exec", - ) - handoff = _handoff_for_command(root, marker_command) - - with pytest.raises(SystemExit, match="failed|passed|observed|binding"): - _validate_observed(_read_handoff(handoff), brief) - - -def test_in_batch_mutation_is_validation_blocked(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - _ensure_source_file(root) - mutating = ( - "python3 -c \"from pathlib import Path; " - f"p=Path('{WRITE_SCOPE_FILE}'); p.parent.mkdir(parents=True, exist_ok=True); " - "p.write_text(p.read_text()+'# mutated\\n' if p.exists() else '# mutated\\n')\"" - ) - _set_task_validation( - task, - "validation:\n" - f" - {{kind: process, command: {json.dumps(PASSING_PROCESS)}, proves: TEST-004, expected: passed}}\n" - f" - {{kind: process, command: {json.dumps(mutating)}, proves: TEST-004, expected: passed}}\n", - ) - brief = _compiled_brief(root, task) - _bind_task_execution(root, brief) - handoff = root / ".work-bundle/orchestration/handoff/executor/active/handoff-task-004.yaml" - handoff.parent.mkdir(parents=True, exist_ok=True) - handoff.write_text( - "id: handoff-task-004\n" - "type: executor-result\n" - "related: {plan: plan-001, task: task-004}\n" - "result: {state: completed}\n" - "task_fit_check: {task: task-004, result: clean}\n" - "validation:\n" - " commands:\n" - f" - {{command: {json.dumps(PASSING_PROCESS)}, result: passed}}\n" - f" - {{command: {json.dumps(mutating)}, result: passed}}\n" - f"{evidence_blocks(root)}" - "knowledge_disposition:\n" - " action: none\n" - " reason: No stable authority changed.\n" - " affected_authority: []\n", - encoding="utf-8", - ) - - with pytest.raises(SystemExit, match="validation-blocked"): - _validate_observed(_read_handoff(handoff), brief) - - -def test_omitted_unauthorized_path_fails_closed(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - _ensure_source_file(root) - _set_process_validation(task, PASSING_PROCESS) - brief = _compiled_brief(root, task) - _bind_task_execution(root, brief) - (root / "src/unauthorized.py").parent.mkdir(parents=True, exist_ok=True) - (root / "src/unauthorized.py").write_text("leak\n", encoding="utf-8") - handoff = _handoff_for_command(root, PASSING_PROCESS) - - with pytest.raises(SystemExit, match="write scope|unauthorized|delta"): - _validate_observed(_read_handoff(handoff), brief) - - reported = _handoff_for_command( - root, - PASSING_PROCESS, - extra="changes:\n files:\n - {path: src/unauthorized.py, action: created}\n", - ) - with pytest.raises(SystemExit, match="write scope|unauthorized|delta"): - _validate_observed(_read_handoff(reported), brief) - - -def test_further_edit_to_baseline_dirty_path_is_task_caused(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - scoped = _ensure_source_file(root) - outsider = root / "src/preexisting.py" - outsider.parent.mkdir(parents=True, exist_ok=True) - outsider.write_text("OLD\n", encoding="utf-8") - _set_process_validation(task, PASSING_PROCESS) - brief = _compiled_brief(root, task) - _bind_task_execution(root, brief) - outsider.write_text("NEW\n", encoding="utf-8") - scoped.write_text("def compile_task():\n return 'new'\n", encoding="utf-8") - handoff = _handoff_for_command( - root, - PASSING_PROCESS, - extra=f"changes:\n files:\n - {{path: {WRITE_SCOPE_FILE}, action: modified}}\n", - ) - - with pytest.raises(SystemExit, match="write scope|unauthorized|delta"): - _validate_observed(_read_handoff(handoff), brief) - - -def test_committed_delta_is_checked_when_worktree_is_clean(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - _ensure_source_file(root) - git(root, "add", ".") - git(root, "commit", "-qm", "base") - _set_process_validation(task, PASSING_PROCESS) - brief = _compiled_brief(root, task) - _bind_task_execution(root, brief) - leaked = root / "src/committed_leak.py" - leaked.parent.mkdir(parents=True, exist_ok=True) - leaked.write_text("committed-leak\n", encoding="utf-8") - git(root, "add", "src/committed_leak.py") - git(root, "commit", "-qm", "task commit") - handoff = _handoff_for_command(root, PASSING_PROCESS) - - with pytest.raises(SystemExit, match="write scope|unauthorized|delta"): - _validate_observed(_read_handoff(handoff), brief) - - -def test_overlapping_mutating_siblings_in_shared_worktree_are_blocked(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - _ensure_source_file(root) - sibling = root / ".work-bundle/orchestration/plan/active/plan-001/phase-001/task-005.md" - sibling.write_text(task.read_text(encoding="utf-8").replace("id: task-004\n", "id: task-005\n"), encoding="utf-8") - _set_process_validation(task, PASSING_PROCESS) - _set_process_validation(sibling, PASSING_PROCESS) - brief_a = _compiled_brief(root, task) - brief_b = _compiled_brief(root, sibling) - _bind_task_execution(root, brief_a, execution_id="exec-a") - - with pytest.raises(SystemExit, match="isolate|serialize|overlapping"): - _bind_task_execution(root, brief_b, execution_id="exec-b") - - -def test_later_brief_rebuild_does_not_recapture_task_baseline(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - scoped = _ensure_source_file(root) - git(root, "add", ".") - git(root, "commit", "-qm", "base") - _set_process_validation(task, PASSING_PROCESS) - brief = _compiled_brief(root, task) - binding = _bind_task_execution(root, brief) - original_head = binding["baseline"]["head"] - scoped.write_text("def compile_task():\n return 'after-baseline'\n", encoding="utf-8") - git(root, "add", WRITE_SCOPE_FILE) - git(root, "commit", "-qm", "after baseline") - build_task_brief(args(root, task)) - rebuilt = execution_context.capture_task_baseline_once( - execution_context.load_task_execution_binding(root, "plan-001", "task-004") - ) - - assert rebuilt["baseline"]["head"] == original_head - assert rebuilt["baseline"]["head"] != git(root, "rev-parse", "HEAD") - - handoff = _handoff_for_command( - root, - PASSING_PROCESS, - extra=f"changes:\n files:\n - {{path: {WRITE_SCOPE_FILE}, action: modified}}\n", - ) - validated = _validate_observed(_read_handoff(handoff), brief) - assert validated["result_state"] == "completed" - - -def test_set_plan_status_completed_observes_bound_worktree(tmp_path: Path) -> None: - from plans import cmd_set_plan_status - - root, _, task = workspace(tmp_path) - _ensure_source_file(root) - _set_process_validation(task, PASSING_PROCESS) - brief = _compiled_brief(root, task) - _bind_task_execution(root, brief) - handoff = _handoff_for_command( - root, - PASSING_PROCESS, - extra=f"changes:\n files:\n - {{path: {WRITE_SCOPE_FILE}, action: modified}}\n", - ) - - cmd_set_plan_status( - argparse.Namespace( - project_root=str(root), - id="task-004", - status="Completed", - kind="task", - handoff=str(handoff), - workspace_id=None, - execution_id=None, - repository_id=None, - execution_runtime_root=None, - mutation_events=[], - ) - ) - data, _ = execution_context._read_structured(task) - assert data["status"] == "Completed" - - -def test_executor_minted_harness_receipt_fails_closed(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - _ensure_source_file(root) - _set_process_validation(task, PASSING_PROCESS) - brief = _compiled_brief(root, task) - _bind_task_execution(root, brief) - handoff = _handoff_for_command(root, PASSING_PROCESS) - payload = _read_handoff(handoff) - payload["harness_receipt"] = {"result": "passed", "exit_code": 0} - - with pytest.raises(SystemExit, match="harness_receipt|receipt"): - _validate_observed(payload, brief) - - -def test_execution_workspace_module_loads_with_orchestration_only_sys_path() -> None: - script = r""" -import sys -from pathlib import Path - -orch = Path("scripts/orchestration").resolve() -work_bundle = Path("scripts/work-bundle").resolve() -sys.path.insert(0, str(orch)) -sys.path[:] = [ - item for item in sys.path - if Path(item).resolve() != work_bundle -] -import execution_context -execution_context._EW_MODULE = None -module = execution_context._execution_workspace_module() -assert callable(getattr(module, "load_state", None)) -""" - completed = subprocess.run( - [sys.executable, "-c", script], - cwd=REPO_ROOT, - capture_output=True, - text=True, - check=False, - ) - assert completed.returncode == 0, completed.stderr - - -def test_execution_context_rebinds_sibling_core_after_work_bundle_core() -> None: - script = ( - "import sys; " - f"sys.path.insert(0, {str(REPO_ROOT / 'scripts/work-bundle')!r}); " - "import core; " - f"sys.path.insert(0, {str(ORCHESTRATION)!r}); " - "import execution_context; " - "assert execution_context.resolve_workspace_root.__module__ == 'core'; " - f"assert sys.modules['core'].__file__ == {str(ORCHESTRATION / 'core.py')!r}" - ) - completed = subprocess.run( - [sys.executable, "-c", script], capture_output=True, text=True, check=False - ) - assert completed.returncode == 0, completed.stderr - - -def test_structured_validation_without_kind_fails_closed_without_subprocess( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - root, _, task = workspace(tmp_path) - calls = _capture_subprocess_after_setup(monkeypatch) - _set_task_validation( - task, - "validation:\n" - f" - {{command: {json.dumps(UNTYPED_LEGACY_COMMAND)}, proves: TEST-004, expected: passed}}\n", - ) - - with pytest.raises(SystemExit, match="kind|legacy-untyped"): - build_task_brief(args(root, task)) - - assert not any(UNTYPED_LEGACY_COMMAND in call for call in calls) - - -def test_test_id_fallback_is_not_executable_terminal_validation( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - root, _, task = workspace(tmp_path) - calls = _capture_subprocess_after_setup(monkeypatch) - _set_task_validation(task, "") - - with pytest.raises(SystemExit, match="kind|legacy-untyped|TEST"): - build_task_brief(args(root, task)) - - assert not any("Focused pytest" in call for call in calls) - assert not any(UNTYPED_LEGACY_COMMAND in call for call in calls) - - -def test_disjoint_mutating_siblings_in_shared_worktree_are_blocked(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - _ensure_source_file(root) - sibling = root / ".work-bundle/orchestration/plan/active/plan-001/phase-001/task-005.md" - sibling.write_text( - task.read_text(encoding="utf-8") - .replace("id: task-004\n", "id: task-005\n") - .replace( - "write: [scripts/orchestration/execution_context.py]", - "write: [tests/test_compiler.py]", - ), - encoding="utf-8", - ) - _set_process_validation(task, PASSING_PROCESS) - _set_process_validation(sibling, PASSING_PROCESS) - brief_a = _compiled_brief(root, task) - brief_b = _compiled_brief(root, sibling) - _bind_task_execution(root, brief_a, execution_id="exec-a") - - with pytest.raises(SystemExit, match="isolate|serialize"): - _bind_task_execution(root, brief_b, execution_id="exec-b") - - -def test_index_only_post_baseline_mutation_is_task_caused(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - _ensure_source_file(root) - outsider = root / "src/preexisting.py" - outsider.parent.mkdir(parents=True, exist_ok=True) - outsider.write_text("A\n", encoding="utf-8") - git(root, "add", ".") - git(root, "commit", "-qm", "base") - outsider.write_text("B\n", encoding="utf-8") - git(root, "add", "src/preexisting.py") - outsider.write_text("C\n", encoding="utf-8") - _set_process_validation(task, PASSING_PROCESS) - brief = _compiled_brief(root, task) - _bind_task_execution(root, brief) - outsider.write_text("D\n", encoding="utf-8") - git(root, "add", "src/preexisting.py") - outsider.write_text("C\n", encoding="utf-8") - handoff = _handoff_for_command(root, PASSING_PROCESS) - - with pytest.raises(SystemExit, match="write scope|unauthorized|delta"): - _validate_observed(_read_handoff(handoff), brief) - - -def test_executor_handoff_cannot_supply_baseline(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - _ensure_source_file(root) - _set_process_validation(task, PASSING_PROCESS) - brief = _compiled_brief(root, task) - _bind_task_execution(root, brief) - handoff = _handoff_for_command(root, PASSING_PROCESS) - payload = _read_handoff(handoff) - payload["baseline"] = {"head": "0" * 40} - - with pytest.raises(SystemExit, match="forbidden field baseline|baseline"): - _validate_observed(payload, brief) - - -def test_named_inspection_without_digest_fails_closed(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - _ensure_source_file(root) - _set_task_validation( - task, - "validation:\n" - " - kind: inspection\n" - " command: inspect-write-scope\n" - " mechanism: named-harness-file-digest\n" - " proves: TEST-004\n" - " expected: passed\n", - ) - brief = _compiled_brief(root, task) - _bind_task_execution(root, brief) - handoff = _handoff_for_command(root, "inspect-write-scope") - payload = _read_handoff(handoff) - payload["validation"] = { - "commands": [{"command": "inspect-write-scope", "result": "passed", "mechanism": "named-harness-file-digest"}] - } - - with pytest.raises(SystemExit, match="digest|falsif|inspection"): - _validate_observed(payload, brief) - - -def test_named_inspection_wrong_digest_fails(tmp_path: Path) -> None: - root, _, task = workspace(tmp_path) - _ensure_source_file(root) - _set_task_validation( - task, - "validation:\n" - " - kind: inspection\n" - " command: inspect-write-scope\n" - " mechanism: named-harness-file-digest\n" - f" digest: {'0' * 64}\n" - " proves: TEST-004\n" - " expected: passed\n", - ) - brief = _compiled_brief(root, task) - _bind_task_execution(root, brief) - handoff = _handoff_for_command(root, "inspect-write-scope") - payload = _read_handoff(handoff) - payload["validation"] = { - "commands": [{"command": "inspect-write-scope", "result": "passed", "mechanism": "named-harness-file-digest"}] - } - - with pytest.raises(SystemExit, match="digest|failed|inspection"): - _validate_observed(payload, brief) diff --git a/tests/test_orchestration_observation_reuse.py b/tests/test_orchestration_observation_reuse.py deleted file mode 100644 index 7330cc9..0000000 --- a/tests/test_orchestration_observation_reuse.py +++ /dev/null @@ -1,347 +0,0 @@ -from __future__ import annotations - -import json -import shlex -import subprocess -import sys -from copy import deepcopy -from pathlib import Path - -import pytest - - -REPO_ROOT = Path(__file__).resolve().parents[1] -ORCHESTRATION = REPO_ROOT / "scripts" / "orchestration" -sys.path.insert(0, str(ORCHESTRATION)) - -import completion_provenance # noqa: E402 -import execution_context # noqa: E402 -import plans # noqa: E402 -from repository_preflight import capture_repository_evidence # noqa: E402 - - -def _git(root: Path, *arguments: str) -> str: - completed = subprocess.run( - ["git", *arguments], cwd=root, check=True, capture_output=True, text=True - ) - return completed.stdout.strip() - - -def _fixture(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): - root = tmp_path / "repo" - root.mkdir() - _git(root, "init", "-q") - _git(root, "config", "user.email", "test@example.com") - _git(root, "config", "user.name", "Test") - source = root / "runtime.py" - source.write_text("VALUE = 1\n") - _git(root, "add", "runtime.py") - _git(root, "commit", "-qm", "baseline") - - control_root = tmp_path / "control" - control_root.mkdir() - counter = tmp_path / "executions.txt" - script = ( - "from pathlib import Path; " - f"p=Path({str(counter)!r}); " - "p.write_text(str(int(p.read_text()) + 1) if p.exists() else '1')" - ) - command = f"{shlex.quote(sys.executable)} -c {shlex.quote(script)}" - item = { - "id": "VAL-001", - "kind": "process", - "command": command, - "expected": "passed", - "invariant_ids": ["INV-001"], - "proves": ["REQ-001"], - "evidence_reuse": { - "mode": "deterministic", - "max_age_seconds": 3600, - "include_head": False, - }, - } - task = { - "plan_id": "plan-001", - "task_id": "task-001", - "source_ids": ["REQ-001"], - "requirements": ["REQ-001: reuse terminal evidence"], - "constraints": [], - "interfaces": {}, - "truth_basis": {"conflict_status": "clear"}, - "files": {"read": ["runtime.py"], "write": ["runtime.py"], "forbidden": []}, - "evidence_capability": {"result": "mapped"}, - "validation": [item], - } - evidence = capture_repository_evidence(root) - binding = { - "control_root": str(control_root), - "execution_path": str(root), - "workspace_id": "workspace-001", - "execution_id": "execution-001", - "repository_id": "repository-001", - "plan_id": "plan-001", - "task_id": "task-001", - "git_identity": {"branch_ref": "refs/heads/master"}, - "baseline": {"head": evidence["head"], "tree": evidence["tree"]}, - "ownership": { - "binding_id": "binding:plan-001:task-001", - "state": "active", - "current_owner": "task-001", - "original_owner": "task-001", - "history": [{"event": "created"}], - }, - } - observed = completion_provenance.observe_validation( - binding, - task, - item, - evidence, - lambda receipt: execution_context._observe_validation_item(item, root, task, receipt), - lambda: capture_repository_evidence(root), - ) - accepted = _accepted_result(task, binding, str(observed["observation_id"])) - monkeypatch.setattr(plans, "load_task_execution_binding", lambda *_: binding) - return root, control_root, counter, accepted, task, binding, command, observed - - -def _accepted_result(task: dict, binding: dict, observation_id: str) -> dict: - ownership = { - "delegated": True, - "owner_kind": "subagent", - "agent_id": "fixture-agent", - "run_id": f"fixture-{task['task_id']}", - "mechanism": "host-native", - } - return execution_context.build_accepted_task_result( - task, - binding, - { - "result": {"state": "completed", "summary": "Validated fixture."}, - "changes": {"files": [{"path": "runtime.py", "change": "updated"}]}, - "task_fit_check": {"task": task["task_id"], "result": "clean"}, - }, - { - "result_state": "completed", - "task_ownership": ownership, - "observed_validation": [ - {"id": task["validation"][0]["id"], "observation_id": observation_id, "result": "passed"} - ], - "knowledge_disposition": { - "action": "none", - "reason": "Fixture changes no durable authority.", - "affected_authority": [], - }, - }, - accepted_at="2026-09-13T00:00:00Z", - ) - - -def _archive(control_root: Path, root: Path, command: str, accepted: dict, task: dict): - return plans._observe_archive_obligations( - control_root, command, root, [(accepted, task)] - ) - - -def _public_archive( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - control_root: Path, - root: Path, - command: str, - accepted: dict, - task: dict, -): - plan = tmp_path / "plan.md" - plan.write_text( - "## Tests\n\n| Test Type | Command |\n| --- | --- |\n" - f"| Integration | `{command}` |\n" - ) - monkeypatch.setattr(plans, "_material_repository_root", lambda *_: root) - monkeypatch.setattr(plans, "resolve_workspace_root", lambda *_: control_root) - plans._assert_archive_plan_acceptance( - type("Args", (), {"project_root": str(control_root), "workspace_root": str(control_root)})(), - "plan-001", - plan, - [(accepted, task)], - ) - - -def test_task_level_reuse_policy_is_applied_to_each_validation_obligation(): - compiled = execution_context._compile_task_validation( - { - "validation": [ - {"id": "VAL-001", "kind": "process", "command": "true", "expected": "passed"} - ], - "evidence_reuse": { - "mode": "deterministic", - "max_age_seconds": 120, - "include_head": False, - }, - }, - "", - [], - {}, - ) - - assert compiled[0]["evidence_reuse"]["max_age_seconds"] == 120 - - -def test_completion_then_archive_reuses_one_current_terminal_observation(tmp_path, monkeypatch): - root, control, counter, accepted, task, _binding, command, first = _fixture(tmp_path, monkeypatch) - - observed = _archive(control, root, command, accepted, task) - - assert counter.read_text() == "1" - assert observed == [ - {"id": "VAL-001", "observation_id": first["observation_id"], "result": "passed"} - ] - state = json.loads( - (control / ".work-bundle/runtime/completion-provenance/completion-provenance-v1.json").read_text() - ) - assert state["consumptions"] == {} - - -@pytest.mark.parametrize("change", ["source", "dependency", "validation"]) -def test_current_authority_rejects_claim_relevant_changes_without_archive_rerun( - tmp_path, monkeypatch, change -): - root, _control, counter, accepted, task, binding, _command, _first = _fixture( - tmp_path, monkeypatch - ) - changed = deepcopy(task) - if change == "source": - accepted["accepted_source"]["tree"] = "f" * 40 - expected = "source authority changed" - elif change == "dependency": - changed["depends_on"] = ["task-upstream"] - expected = "task authority changed" - else: - changed["validation"][0]["command"] = "python -m pytest -q" - expected = "validation authority changed" - - with pytest.raises(SystemExit, match=expected): - execution_context.assert_accepted_task_result_current(changed, binding, accepted) - - assert counter.read_text() == "1" - - -def test_changed_validation_allocation_requires_reassessment_without_archive_rerun(tmp_path, monkeypatch): - root, control, counter, accepted, task, _binding, command, _first = _fixture(tmp_path, monkeypatch) - changed = deepcopy(task) - changed["validation"][0]["invariant_ids"] = ["INV-002"] - - with pytest.raises(SystemExit, match="accepted harness observation"): - _public_archive(tmp_path, monkeypatch, control, root, command, accepted, changed) - - assert counter.read_text() == "1" - state = json.loads( - (control / ".work-bundle/runtime/completion-provenance/completion-provenance-v1.json").read_text() - ) - assert len(state["observations"]) == 1 - - -def test_time_stale_observation_remains_explicitly_bound_after_acceptance(tmp_path, monkeypatch): - root, control, counter, accepted, task, _binding, command, first = _fixture(tmp_path, monkeypatch) - store_path = control / ".work-bundle/runtime/completion-provenance/completion-provenance-v1.json" - state = json.loads(store_path.read_text()) - state["observations"][0]["freshness_deadline"] = "2000-01-01T00:00:00Z" - store_path.write_text(json.dumps(state)) - - observed = _archive(control, root, command, accepted, task) - - assert counter.read_text() == "1" - assert observed == [ - {"id": "VAL-001", "observation_id": first["observation_id"], "result": "passed"} - ] - - -def test_malformed_harness_observation_blocks_without_archive_rerun(tmp_path, monkeypatch): - root, control, counter, accepted, task, _binding, command, _first = _fixture(tmp_path, monkeypatch) - store_path = control / ".work-bundle/runtime/completion-provenance/completion-provenance-v1.json" - state = json.loads(store_path.read_text()) - del state["observations"][0]["result"]["exit_code"] - store_path.write_text(json.dumps(state)) - - with pytest.raises(SystemExit, match="accepted harness observation"): - _public_archive(tmp_path, monkeypatch, control, root, command, accepted, task) - - assert counter.read_text() == "1" - assert len(json.loads(store_path.read_text())["observations"]) == 1 - - -def test_distinct_accepted_obligations_each_reuse_without_lifecycle_replay(tmp_path, monkeypatch): - root, control, counter, accepted, task, _binding, command, _ = _fixture(tmp_path, monkeypatch) - other = deepcopy(task) - other["task_id"] = "task-002" - other["validation"][0]["id"] = "VAL-002" - evidence = capture_repository_evidence(root) - binding_two = { - "control_root": str(control), - "execution_path": str(root), - "workspace_id": "workspace-001", - "execution_id": "execution-001", - "repository_id": "repository-001", - "plan_id": "plan-001", - "task_id": "task-002", - "git_identity": {"branch_ref": "refs/heads/master"}, - "baseline": {"head": evidence["head"], "tree": evidence["tree"]}, - "ownership": { - "binding_id": "binding:plan-001:task-002", - "state": "active", - "current_owner": "task-002", - "original_owner": "task-002", - "history": [{"event": "created"}], - }, - } - second = completion_provenance.observe_validation( - binding_two, - other, - other["validation"][0], - evidence, - lambda receipt: execution_context._observe_validation_item(other["validation"][0], root, other, receipt), - lambda: capture_repository_evidence(root), - ) - accepted_two = _accepted_result(other, binding_two, str(second["observation_id"])) - monkeypatch.setattr( - plans, - "load_task_execution_binding", - lambda _root, _plan, task_id: binding_two if task_id == "task-002" else { - **binding_two, "task_id": "task-001" - }, - ) - - observed = _archive(control, root, command, accepted, task) - observed += _archive(control, root, command, accepted_two, other) - - assert counter.read_text() == "2" - assert {item["observation_id"] for item in observed} == { - accepted["validation_evidence_ids"][0], - accepted_two["validation_evidence_ids"][0], - } - - -def test_executor_authored_receipt_id_is_not_independent_archive_proof(tmp_path, monkeypatch): - root, control, counter, accepted, task, _binding, command, _ = _fixture(tmp_path, monkeypatch) - accepted["validation_evidence_ids"] = ["executor-forged-observation"] - - with pytest.raises(SystemExit, match="accepted harness observation"): - _public_archive(tmp_path, monkeypatch, control, root, command, accepted, task) - - assert counter.read_text() == "1" - - -def test_archive_acceptance_uses_observation_contract_instead_of_direct_runner(tmp_path, monkeypatch): - root, control, counter, accepted, task, _binding, command, _ = _fixture(tmp_path, monkeypatch) - plan = tmp_path / "plan.md" - plan.write_text( - "## Tests\n\n| Test Type | Command |\n| --- | --- |\n" - f"| Integration | `{command}` |\n" - ) - monkeypatch.setattr(plans, "_material_repository_root", lambda *_: root) - monkeypatch.setattr(plans, "resolve_workspace_root", lambda *_: control) - monkeypatch.setattr(plans, "_assert_archive_command_state_neutral", lambda *_: pytest.fail("direct replay")) - args = type("Args", (), {"project_root": str(control), "workspace_root": str(control)})() - - plans._assert_archive_plan_acceptance(args, "plan-001", plan, [(accepted, task)]) - - assert counter.read_text() == "1" diff --git a/tests/test_orchestration_plan_return.py b/tests/test_orchestration_plan_return.py deleted file mode 100644 index cb28422..0000000 --- a/tests/test_orchestration_plan_return.py +++ /dev/null @@ -1,443 +0,0 @@ -from __future__ import annotations - -import json -import sys -from copy import deepcopy -from pathlib import Path - -import pytest -from reviewer_run_fixtures import bind_review_receipt - - -REPO_ROOT = Path(__file__).resolve().parents[1] -ORCHESTRATION = REPO_ROOT / "scripts" / "orchestration" -WORK_BUNDLE = REPO_ROOT / "scripts" / "work-bundle" -for path in (ORCHESTRATION, WORK_BUNDLE): - if str(path) not in sys.path: - sys.path.insert(0, str(path)) - -from review_runtime import ( # noqa: E402 - ReviewContractError, - classify_first_broken_owner, - publish_review, - resume_plan_return, - _route_review_finding as route_review_verdict, -) -from stage_events import StageEventError # noqa: E402 - - -ZERO_SHA = "0" * 64 -ZERO_TREE = "0" * 40 - - -def identity(artifact_id: str, revision: str = "1") -> dict[str, object]: - return { - "artifact_id": artifact_id, - "revision": revision, - "sha256": ZERO_SHA, - "source_tree": ZERO_TREE, - } - - -def binding_identity() -> dict[str, str]: - return {"binding_id": "binding-task-003", "sha256": "1" * 64} - - -def baseline_identity() -> dict[str, str]: - return {"head": "2" * 40, "tree": "3" * 40} - - -def affected_region() -> dict[str, list[str]]: - return { - "task_ids": ["task-003"], - "paths": ["scripts/orchestration/review_runtime.py"], - "interfaces": ["API-PD-001"], - "validation_oracles": ["VAL-004"], - } - - -def allocation_gap() -> dict[str, object]: - artifact, owner, disposition = classify_first_broken_owner("allocation_gap") - return { - "finding_id": "finding-under-decomposed", - "stage": "implementation", - "class": "allocation_gap", - "severity": "blocking", - "first_broken_artifact": artifact, - "obligation_basis": "accepted_requirement", - "evidence": [ - { - "kind": "runtime", - "locator": "task-004", - "digest_or_identity": "repair-frontier-separated", - "observation": "The task now has two independently owned repair regions.", - } - ], - "target_identity": identity("plan-001"), - "summary": "The affected task is materially under-decomposed.", - "recommended_owner": owner, - "disposition": disposition, - } - - -def event(event_id: str = "event-economics", **updates: object) -> dict[str, object]: - value: dict[str, object] = { - "event_id": event_id, - "timestamp": "2026-09-07T00:00:00Z", - "process_id": "process-001", - "stage": "integrated_implementation", - "attempt_id": "attempt-001", - "event_type": "stage_completed", - "enforcement_mode": "native", - "join_ids": { - "specification_id": "spec-001", - "plan_id": "plan-001", - "phase_id": "phase-001", - "task_id": None, - "review_id": "review-001", - "evaluation_id": None, - }, - "clocks": {"wall_ms": 13, "active_ms": 8, "billed_ms": None}, - "finding_class": None, - "return_reason": None, - "owner": "plan_owner", - "identity": { - "product_tree": ZERO_TREE, - "artifact_digest": ZERO_SHA, - "mutation_epoch": 2, - }, - "privacy": "operational_metadata_only", - } - value.update(updates) - return value - - -def test_pd_07_returns_only_affected_region_and_preserves_unaffected_evidence() -> None: - preserved = [identity("task-001"), identity("task-002")] - - routed = route_review_verdict( - allocation_gap(), - affected_region=affected_region(), - unaffected_evidence_identities=preserved, - original_binding_identity=binding_identity(), - original_baseline_identity=baseline_identity(), - ) - - assert routed["execution_state"] == "paused_for_reslice" - assert routed["affected_region"] == affected_region() - assert routed["returned_authority_identity"] == identity("plan-001") - assert routed["preserved_evidence_identities"] == preserved - assert routed["resume_requires"] == "accepted_repaired_plan_authority" - assert routed["original_binding_identity"] == binding_identity() - assert routed["original_baseline_identity"] == baseline_identity() - assert routed["silent_expansion_allowed"] is False - - -def test_pd_07_resume_waits_for_current_accepted_plan_review_and_exact_preserved_state( - tmp_path: Path, -) -> None: - orch = tmp_path / ".work-bundle/orchestration" - spec = orch / "spec/active/spec.md" - plan = orch / "plan/active/plan.md" - spec.parent.mkdir(parents=True) - plan.parent.mkdir(parents=True) - spec.write_text("---\nid: spec-test\nstatus: verified\n---\nAuthority\n") - plan.write_text("---\nid: plan-001\nstatus: Planned\nsource_spec: [spec-test]\n---\nOriginal\n") - preserved = [identity("task-001")] - routed = route_review_verdict( - allocation_gap(), - affected_region=affected_region(), - unaffected_evidence_identities=preserved, - original_binding_identity=binding_identity(), - original_baseline_identity=baseline_identity(), - ) - plan.write_text(plan.read_text().replace("Original", "Resliced")) - - with pytest.raises(ReviewContractError, match="accepted repaired plan-review authority"): - resume_plan_return( - routed, - workspace_root=tmp_path, - plan_path=plan, - current_binding_identity=binding_identity(), - current_baseline_identity=baseline_identity(), - current_unaffected_evidence_identities=preserved, - ) - - from review_runtime import plan_review_identity - - review = { - "review_id": "review-plan", - "stage": "plan", - "target_identity": plan_review_identity(tmp_path, plan), - "reviewer": { - "agent_id": "reviewer-1", - "capability": "judgment", - "authorship": "none", - "repair_participation": "none", - "decision_participation": "none", - "deliberation_participation": "none", - "context_origin": "direct_source", - }, - "evidence": { - "mode": "direct", - "capabilities": ["source inspection"], - "unavailable_evidence": [], - "commands": [], - "artifacts": [], - }, - "verdict": "accepted", - "findings": [], - "started_at": "2026-09-07T00:00:00Z", - "completed_at": "2026-09-07T00:01:00Z", - "staleness": {"is_stale": False, "reason": None, "supersedes": None}, - } - review = bind_review_receipt(tmp_path, review) - publish_review(tmp_path, review, current_target_identity=review["target_identity"]) - - changed = deepcopy(preserved) - changed[0]["revision"] = "2" - with pytest.raises(ReviewContractError, match="unaffected evidence"): - resume_plan_return( - routed, - workspace_root=tmp_path, - plan_path=plan, - current_binding_identity=binding_identity(), - current_baseline_identity=baseline_identity(), - current_unaffected_evidence_identities=changed, - ) - - with pytest.raises(ReviewContractError, match="binding identity"): - resume_plan_return( - routed, - workspace_root=tmp_path, - plan_path=plan, - current_binding_identity={**binding_identity(), "sha256": "4" * 64}, - current_baseline_identity=baseline_identity(), - current_unaffected_evidence_identities=preserved, - ) - - with pytest.raises(ReviewContractError, match="baseline identity"): - resume_plan_return( - routed, - workspace_root=tmp_path, - plan_path=plan, - current_binding_identity=binding_identity(), - current_baseline_identity={**baseline_identity(), "tree": "4" * 40}, - current_unaffected_evidence_identities=preserved, - ) - - resumed = resume_plan_return( - routed, - workspace_root=tmp_path, - plan_path=plan, - current_binding_identity=binding_identity(), - current_baseline_identity=baseline_identity(), - current_unaffected_evidence_identities=preserved, - ) - assert resumed["execution_state"] == "ready_from_repaired_authority" - assert resumed["preserved_evidence_identities"] == preserved - - -@pytest.mark.parametrize( - "region", - [ - {}, - {**affected_region(), "task_ids": ["task-003", "task-003"]}, - {**affected_region(), "paths": ["../escape"]}, - {**affected_region(), "validation_oracles": [""]}, - ], -) -def test_pd_07_rejects_unbounded_or_ambiguous_affected_regions(region) -> None: - with pytest.raises(ReviewContractError, match="affected region"): - route_review_verdict( - allocation_gap(), - affected_region=region, - original_binding_identity=binding_identity(), - original_baseline_identity=baseline_identity(), - ) - - -def test_pd_08_stage_event_path_isolates_same_process_different_plan_economics( - tmp_path: Path, -) -> None: - from stage_events import append_stage_event, query_stage_events - - events = [ - event("phase", stage="implementation", event_type="stage_started"), - event( - "task-a", - stage="implementation", - event_type="stage_started", - join_ids={**event()["join_ids"], "task_id": "task-a"}, - ), - event( - "task-b", - stage="implementation", - event_type="stage_started", - join_ids={**event()["join_ids"], "task_id": "task-b"}, - ), - event( - "noise-task", - stage="implementation", - event_type="stage_started", - join_ids={ - **event()["join_ids"], - "plan_id": "plan-noise", - "phase_id": "phase-noise", - "task_id": "task-noise", - "review_id": "review-noise", - }, - ), - event( - "noise-plan-review", - stage="plan", - event_type="stage_completed", - join_ids={ - **event()["join_ids"], - "plan_id": "plan-noise", - "review_id": "review-noise", - }, - ), - event("plan-review", stage="plan", event_type="stage_completed"), - event( - "noise-scope-repair", - event_type="reslice_recorded", - finding_class="allocation_gap", - attempt_id="noise-scope", - join_ids={**event()["join_ids"], "plan_id": "plan-noise"}, - ), - event( - "scope-repair", - event_type="reslice_recorded", - finding_class="allocation_gap", - attempt_id="repair-scope", - ), - event( - "task-repair", - event_type="work_returned", - finding_class="implementation_defect", - attempt_id="repair-task", - join_ids={**event()["join_ids"], "task_id": "task-a"}, - ), - event( - "noise-task-repair", - event_type="work_returned", - finding_class="implementation_defect", - attempt_id="noise-task-repair", - join_ids={ - **event()["join_ids"], - "plan_id": "plan-noise", - "task_id": "task-noise", - "review_id": "review-noise", - }, - ), - event( - "suite-first", - event_type="suite_started", - attempt_id="validation", - join_ids={**event()["join_ids"], "evaluation_id": "eval-001"}, - ), - event( - "suite-rerun", - event_type="suite_started", - attempt_id="validation-2", - join_ids={**event()["join_ids"], "evaluation_id": "eval-001"}, - ), - event( - "noise-suite-first", - event_type="suite_started", - attempt_id="noise-validation", - join_ids={ - **event()["join_ids"], - "plan_id": "plan-noise", - "evaluation_id": "eval-noise", - }, - ), - event( - "noise-suite-rerun", - event_type="suite_started", - attempt_id="noise-validation-2", - join_ids={ - **event()["join_ids"], - "plan_id": "plan-noise", - "evaluation_id": "eval-noise", - }, - ), - event( - "noise-green", - timestamp="2026-09-07T00:00:00.100Z", - event_type="suite_completed", - attempt_id="noise-validation-2", - join_ids={ - **event()["join_ids"], - "plan_id": "plan-noise", - "evaluation_id": "eval-noise", - }, - ), - event( - "green", - timestamp="2026-09-07T00:00:01Z", - event_type="suite_completed", - attempt_id="validation-2", - join_ids={**event()["join_ids"], "evaluation_id": "eval-001"}, - ), - event( - "accepted", - timestamp="2026-09-07T00:00:02.200Z", - stage="integrated_implementation", - event_type="stage_completed", - attempt_id="final", - ), - ] - result = None - for item in events: - result = append_stage_event(tmp_path, item) - - assert result is not None - assert result.planning_economics == { - "initial_cardinality": {"phases": 1, "tasks": 2}, - "plan_revisions": 1, - "plan_reviews": 1, - "scope_allocation_repairs": 1, - "task_review_repairs": 1, - "validation_reruns": 1, - "first_green_to_final_accept_ms": 1200, - } - assert query_stage_events(tmp_path)[-1].planning_economics == result.planning_economics - - schema = json.loads( - (REPO_ROOT / "references/assets/orchestration/contract/stage-event-v1.schema.json").read_text() - ) - assert schema["$defs"]["stageEvent"]["properties"]["planning_economics"] == { - "$ref": "#/$defs/planningEconomics" - } - assert schema["$defs"]["planningEconomics"]["additionalProperties"] is False - - -def test_pd_08_rejects_unbound_integrated_economics_emission(tmp_path: Path) -> None: - from stage_events import append_stage_event - - unbound = event( - "unbound-final", - join_ids={**event()["join_ids"], "plan_id": None}, - ) - - with pytest.raises(StageEventError, match="WB_STAGE_EVENT_ECONOMICS_SCOPE_INVALID"): - append_stage_event(tmp_path, unbound) - - -def test_pd_08_rejects_caller_injected_economics(tmp_path: Path) -> None: - from stage_events import append_stage_event - - value = event() - value["planning_economics"] = { - "initial_cardinality": {"phases": 0, "tasks": 100_000}, - "plan_revisions": 0, - "plan_reviews": 0, - "scope_allocation_repairs": 0, - "task_review_repairs": 0, - "validation_reruns": 0, - "first_green_to_final_accept_ms": None, - } - with pytest.raises(StageEventError, match="WB_STAGE_EVENT_ECONOMICS_INVALID"): - append_stage_event(tmp_path, value) diff --git a/tests/test_orchestration_planning_contracts.py b/tests/test_orchestration_planning_contracts.py index f456615..d24a96d 100644 --- a/tests/test_orchestration_planning_contracts.py +++ b/tests/test_orchestration_planning_contracts.py @@ -66,3 +66,40 @@ def test_pd_pressure_rows_cover_review_stable_decomposition() -> None: assert "return to the plan" in combined["PD-07"] assert "speculative" in combined["PD-09"] assert "independently owned entry points" in combined["PD-10"] + + +def test_stage4_contracts_use_schema_owned_yaml_and_direct_semantic_review() -> None: + planner = read("skills/orch-create-implementation-plan/SKILL.md") + rule = read("rules/orchestration/orch-artifact-authoring.md") + workflow = read("references/assets/orchestration/workflow.md") + contracts = "\n".join( + read(path) + for path in [ + "references/assets/orchestration/contract/plan-v1.md", + "references/assets/orchestration/contract/phase-v1.md", + "references/assets/orchestration/contract/task-v1.md", + ] + ) + for token in ["schema-owned YAML", "root-plan", "phase", "task"]: + assert token in planner + workflow + contracts + for token in [ + "distinct reviewer", "verified specification", "ownership", "dependencies", + "validation", "authority", "scope", "executability", + ]: + assert token in planner + assert "## Self-check" in planner + assert "Do not create Markdown plan/phase/task compatibility copies" in rule + assert "tests, doctors, indexes, receipts, handoffs" in planner.lower() + assert "stage5-required" not in planner + + +def test_stage4_evals_cover_cutover_semantic_review_and_exact_source_ids() -> None: + payload = json.loads(read("references/evals/orchestration/evals.json")) + cases = {str(case["id"]): case for case in payload["evals"]} + assert {"STG4-01", "STG4-02", "STG4-03", "STG4-04"} <= cases.keys() + combined = " ".join( + f"{cases[key]['prompt']} {cases[key]['expected_output']}" + for key in ("STG4-01", "STG4-02", "STG4-03", "STG4-04") + ) + for token in ["schema-owned", "distinct semantic reviewer", "REQ-001A", "Stage 5"]: + assert token in combined diff --git a/tests/test_orchestration_planning_scenarios.py b/tests/test_orchestration_planning_scenarios.py deleted file mode 100644 index 87326c2..0000000 --- a/tests/test_orchestration_planning_scenarios.py +++ /dev/null @@ -1,599 +0,0 @@ -from __future__ import annotations - -import json -import sys -from pathlib import Path - -import pytest -from reviewer_run_fixtures import bind_review_receipt - - -ROOT = Path(__file__).resolve().parents[1] -ORCHESTRATION = ROOT / "scripts/orchestration" -WORK_BUNDLE = ROOT / "scripts/work-bundle" -sys.path.insert(0, str(ORCHESTRATION)) - -from review_runtime import ( # noqa: E402 - ReviewContractError, - plan_review_identity, - publish_review, - resume_plan_return, - _route_review_finding as route_review_verdict, -) -import execution_context # noqa: E402 -from test_orchestration_accepted_result import _binding, _handoff, _task, _validated # noqa: E402 - -sys.path.insert(0, str(WORK_BUNDLE)) -from stage_events import derive_planning_economics, validate_stage_event # noqa: E402 - - -ZERO_SHA = "0" * 64 -ZERO_TREE = "0" * 40 - - -def read(path: str) -> str: - return (ROOT / path).read_text(encoding="utf-8") - - -def stage_event( - event_id: str, - *, - stage: str = "implementation", - event_type: str = "stage_started", - task_id: str | None = None, - review_id: str | None = None, - timestamp: str = "2026-09-07T00:00:00Z", - attempt_id: str | None = None, - finding_class: str | None = None, - evaluation_id: str | None = None, -) -> object: - return validate_stage_event( - { - "event_id": event_id, - "timestamp": timestamp, - "process_id": "process-001", - "stage": stage, - "attempt_id": attempt_id or event_id, - "event_type": event_type, - "enforcement_mode": "native", - "join_ids": { - "specification_id": "spec-001", - "plan_id": "plan-001", - "phase_id": "phase-001", - "task_id": task_id, - "review_id": review_id, - "evaluation_id": evaluation_id, - }, - "clocks": {"wall_ms": 1, "active_ms": 1, "billed_ms": None}, - "finding_class": finding_class, - "return_reason": None, - "owner": "plan_owner", - "identity": { - "product_tree": ZERO_TREE, - "artifact_digest": ZERO_SHA, - "mutation_epoch": 1, - }, - "privacy": "operational_metadata_only", - } - ) - - -def development_case(case_id: str) -> dict[str, object]: - payload = json.loads(read("references/evals/development/evals.json")) - return next(case for case in payload["evals"] if case["id"] == case_id) - - -def orchestration_case(case_id: str) -> dict[str, object]: - payload = json.loads(read("references/evals/orchestration/evals.json")) - return next(case for case in payload["evals"] if case["id"] == case_id) - - -def assert_normative_case( - case_id: str, - *, - prompt: str, - expected_output: str, - skill_path: str, - owning_clause: str, -) -> None: - assert orchestration_case(case_id) == { - "id": case_id, - "prompt": prompt, - "expected_output": expected_output, - "files": [], - } - assert owning_clause in read(skill_path) - - -def assert_development_case( - case_id: str, - *, - prompt: str, - expected_output: str, - owning_clauses: tuple[str, ...], -) -> None: - assert development_case(case_id) == { - "id": case_id, - "prompt": prompt, - "expected_output": expected_output, - } - skill = read("skills/dev-create-task-plan/SKILL.md") - for clause in owning_clauses: - assert clause in skill - - -def allocation_gap() -> dict[str, object]: - return { - "finding_id": "finding-under-decomposed", - "stage": "implementation", - "class": "allocation_gap", - "severity": "blocking", - "first_broken_artifact": "plan", - "obligation_basis": "accepted_requirement", - "evidence": [ - { - "kind": "runtime", - "locator": "task-003", - "digest_or_identity": "independent-repair-frontiers", - "observation": "Two independently owned regions now fail separately.", - } - ], - "target_identity": { - "artifact_id": "plan-001", - "revision": "1", - "sha256": ZERO_SHA, - "source_tree": ZERO_TREE, - }, - "summary": "The task is materially under-decomposed.", - "recommended_owner": "plan_owner", - "disposition": "reslice_plan", - } - - -def test_pd_01_cardinality_never_overrides_evidenced_runtime_seams() -> None: - assert_normative_case( - "PD-01", - prompt="Plan a change whose production seams support six tasks, while a reviewer proposes a three-task target to make the plan shorter.", - expected_output="Rejects the task-count target and does not optimize task or phase cardinality; it uses the six evidenced ownership, dependency, validation, review, and repair seams when they bound expected total orchestration cost.", - skill_path="skills/orch-create-implementation-plan/SKILL.md", - owning_clause="Do not optimize task or phase cardinality. Decompose only at concrete independently owned production, dependency, validation, review, and repair seams so expected total orchestration cost remains bounded", - ) - - -def test_pd_02_helper_allocation_cannot_leave_production_lifecycle_unowned() -> None: - assert_normative_case( - "PD-02", - prompt="A plan allocates tests and a helper refactor but leaves the authoritative production path with no implementation owner.", - expected_output="Rejects helper-only allocation until every authoritative production path has a production owner and the production change, validation, and repair responsibility are explicitly allocated.", - skill_path="skills/orch-create-implementation-plan/SKILL.md", - owning_clause="Assign every authoritative production path to a production owner; reject helper-only allocation while its production path is unowned.", - ) - - -def test_pd_03_executor_acceptance_path_has_explicit_controller_authority( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - assert_normative_case( - "PD-02", - prompt="A plan allocates tests and a helper refactor but leaves the authoritative production path with no implementation owner.", - expected_output="Rejects helper-only allocation until every authoritative production path has a production owner and the production change, validation, and repair responsibility are explicitly allocated.", - skill_path="skills/orch-create-implementation-plan/SKILL.md", - owning_clause="Assign every authoritative production path to a production owner; reject helper-only allocation while its production path is unowned.", - ) - - task = _task(tmp_path) - binding = _binding(tmp_path) - persisted: list[dict[str, object]] = [] - monkeypatch.setattr( - execution_context, - "load_task_execution_binding", - lambda *_args: binding, - ) - monkeypatch.setattr( - execution_context, - "_persist_binding", - lambda value, _root: persisted.append(value), - ) - monkeypatch.setattr( - execution_context, - "capture_repository_evidence", - lambda _root: { - "head": "a" * 40, - "tree": "b" * 40, - "entries": {}, - "status": "clean", - }, - ) - handoff = _handoff() - stored_review = handoff.pop("acceptance_review") - accepted = execution_context.materialize_accepted_task_result( - tmp_path, - task, - handoff, - _validated(), - accepted_review=stored_review, - accepted_at="2026-09-07T00:00:00Z", - ) - assert "acceptance_review" not in handoff - assert persisted == [{**binding, "accepted_result": accepted}] - assert accepted["schema"] == "accepted-task-result-v1" - assert accepted["owner_identity"]["owner_kind"] == "subagent" - assert "mutation_events" not in repr(accepted) - - -def test_pd_04_independently_repairable_entry_points_remain_distinct() -> None: - assert_normative_case( - "PD-03", - prompt="A planner groups two changes that have different owners, validation oracles, and independently routable repair outcomes.", - expected_output="Splits at the evidenced ownership, oracle, and repair frontier so a failure returns to the smallest affected plan region without widening unrelated accepted work.", - skill_path="skills/orch-create-implementation-plan/SKILL.md", - owning_clause="preserving independently falsifiable increments, short evidence loops, exact dependencies, disjoint write scopes, bounded failure radius, and review boundaries", - ) - - -def test_pd_05_coherent_mechanical_increment_is_not_micro_tasked_by_file_count() -> None: - assert_normative_case( - "PD-04", - prompt="A planner proposes splitting one production edit, its direct contract test, and its local documentation merely because three files are involved.", - expected_output="Keeps the coherent mechanical increment together under one production owner, oracle, and repair frontier; file count is not a decomposition seam.", - skill_path="skills/orch-create-implementation-plan/SKILL.md", - owning_clause="Keep one coherent mechanical increment with one owner, oracle, and repair frontier together.", - ) - - -def test_pd_06_producer_convergence_requires_a_real_barrier_and_owner() -> None: - assert_normative_case( - "PD-05", - prompt="A plan creates a new phase for each lifecycle label even though no dependency barrier or convergence boundary separates the work.", - expected_output="Rejects lifecycle-label phases and creates a phase only for an actual barrier or convergence boundary with concrete readiness and ownership evidence.", - skill_path="skills/orch-create-implementation-plan/SKILL.md", - owning_clause="Create a phase only for an actual barrier or convergence boundary, with explicit barrier ID, readiness evidence, and convergence owner.", - ) - - -def test_pd_07_load_bearing_specification_authority_survives_compaction() -> None: - assert_normative_case( - "PD-06", - prompt="A specification is shortened by deleting a unique validation target and compatibility constraint while retaining repeated summary prose.", - expected_output="Restores a complete, nonredundant authority set: preserves every load-bearing field required downstream and removes duplicate prose rather than unique authority.", - skill_path="skills/orch-create-specification/SKILL.md", - owning_clause="Preserve every load-bearing requirement, constraint, interface, acceptance criterion, validation target, and decision needed downstream; such authority must not be removed merely to make the artifact smaller. Reject duplicate prose that adds no authority.", - ) - - -def test_pd_08_planning_economics_are_derived_without_cardinality_judgment() -> None: - records = [ - stage_event("phase"), - stage_event("task-a", task_id="task-a"), - stage_event("task-b", task_id="task-b"), - stage_event( - "scope-repair", - event_type="reslice_recorded", - attempt_id="repair-scope", - finding_class="allocation_gap", - ), - stage_event( - "task-repair", - event_type="work_returned", - task_id="task-a", - review_id="review-task-a", - attempt_id="repair-task", - finding_class="implementation_defect", - ), - stage_event("suite-first", event_type="suite_started", evaluation_id="eval-001"), - stage_event("suite-rerun", event_type="suite_started", evaluation_id="eval-001"), - stage_event( - "plan-review", - stage="plan", - event_type="stage_completed", - review_id="review-plan", - ), - stage_event( - "green", - event_type="suite_completed", - evaluation_id="eval-001", - timestamp="2026-09-07T00:00:01Z", - ), - stage_event( - "accepted", - stage="integrated_implementation", - event_type="stage_completed", - review_id="review-001", - timestamp="2026-09-07T00:00:02.200Z", - ), - ] - result = derive_planning_economics(records, process_id="process-001", plan_id="plan-001") - assert result["initial_cardinality"] == {"phases": 1, "tasks": 2} - assert result["plan_revisions"] == 1 - assert result["plan_reviews"] == 1 - assert result["scope_allocation_repairs"] == 1 - assert result["task_review_repairs"] == 1 - assert result["validation_reruns"] == 1 - assert result["first_green_to_final_accept_ms"] == 1200 - assert set(result) == { - "initial_cardinality", - "plan_revisions", - "plan_reviews", - "scope_allocation_repairs", - "task_review_repairs", - "validation_reruns", - "first_green_to_final_accept_ms", - } - - -def test_pd_09_under_decomposition_returns_only_the_affected_plan_region( - tmp_path: Path, -) -> None: - assert_normative_case( - "PD-07", - prompt="Execution proves one task materially under-decomposed after its repair frontier separates into two independently owned regions.", - expected_output="Stops repeatedly enlarging the task, requires a return to the plan, and reslices only the affected region while preserving the original binding, baseline, accepted unaffected regions, and typed repair route.", - skill_path="skills/orch-create-implementation-plan/SKILL.md", - owning_clause="When execution proves a task materially under-decomposed, return to the plan and reslice only the affected region around the newly evidenced seam. Preserve the original binding, baseline, and accepted unaffected regions; do not repeatedly enlarge the task.", - ) - binding = {"binding_id": "binding-task-003", "sha256": "1" * 64} - baseline = {"head": "2" * 40, "tree": "3" * 40} - unaffected = [ - { - "artifact_id": "task-001", - "revision": "1", - "sha256": ZERO_SHA, - "source_tree": ZERO_TREE, - } - ] - region = { - "task_ids": ["task-003"], - "paths": ["scripts/orchestration/review_runtime.py"], - "interfaces": ["API-PD-001"], - "validation_oracles": ["VAL-004"], - } - routed = route_review_verdict( - allocation_gap(), - affected_region=region, - unaffected_evidence_identities=unaffected, - original_binding_identity=binding, - original_baseline_identity=baseline, - ) - assert routed["execution_state"] == "paused_for_reslice" - assert routed["affected_region"] == region - assert routed["preserved_evidence_identities"] == unaffected - assert routed["silent_expansion_allowed"] is False - - orch = tmp_path / ".work-bundle/orchestration" - spec = orch / "spec/active/spec.md" - plan = orch / "plan/active/plan.md" - spec.parent.mkdir(parents=True) - plan.parent.mkdir(parents=True) - spec.write_text("---\nid: spec-test\nstatus: verified\n---\nAuthority\n", encoding="utf-8") - plan.write_text( - "---\nid: plan-001\nstatus: Planned\nsource_spec: [spec-test]\n---\nOriginal\n", - encoding="utf-8", - ) - plan.write_text(plan.read_text(encoding="utf-8").replace("Original", "Resliced"), encoding="utf-8") - - with pytest.raises(ReviewContractError, match="accepted repaired plan-review authority"): - resume_plan_return( - routed, - workspace_root=tmp_path, - plan_path=plan, - current_binding_identity=binding, - current_baseline_identity=baseline, - current_unaffected_evidence_identities=unaffected, - ) - - review = { - "review_id": "review-plan", - "stage": "plan", - "target_identity": plan_review_identity(tmp_path, plan), - "reviewer": { - "agent_id": "reviewer-1", - "capability": "judgment", - "authorship": "none", - "repair_participation": "none", - "decision_participation": "none", - "deliberation_participation": "none", - "context_origin": "direct_source", - }, - "evidence": { - "mode": "direct", - "capabilities": ["source inspection"], - "unavailable_evidence": [], - "commands": [], - "artifacts": [], - }, - "verdict": "accepted", - "findings": [], - "started_at": "2026-09-07T00:00:00Z", - "completed_at": "2026-09-07T00:01:00Z", - "staleness": {"is_stale": False, "reason": None, "supersedes": None}, - } - review = bind_review_receipt(tmp_path, review) - publish_review(tmp_path, review, current_target_identity=review["target_identity"]) - - resumed = resume_plan_return( - routed, - workspace_root=tmp_path, - plan_path=plan, - current_binding_identity=binding, - current_baseline_identity=baseline, - current_unaffected_evidence_identities=unaffected, - ) - assert resumed["execution_state"] == "ready_from_repaired_authority" - assert resumed["preserved_evidence_identities"] == unaffected - - -def test_pd_10_hypothetical_defects_do_not_create_speculative_tasks() -> None: - assert_normative_case( - "PD-09", - prompt="A planner proposes separate hardening, compatibility, and recovery tasks without current authority, repository, dependency, validation, or acceptance evidence for them.", - expected_output="Rejects speculative fragmentation and adds no tasks until a current material seam proves the scope; it does not create a second review, retry, or recovery subsystem.", - skill_path="skills/orch-create-implementation-plan/SKILL.md", - owning_clause="Do not create speculative splits unsupported by current authority, repository, dependency, ownership, validation, or acceptance evidence.", - ) - - -def test_pd_11_same_owner_pre_mutation_path_amendment_stays_lightweight() -> None: - assert_development_case( - "dev-lightweight-pre-mutation-one-file-amendment", - prompt="Before mutation, source grounding shows that a lightweight plan must add one exact file owned by the same implementation owner; purpose, accepted authority, expected delta, impact radius, ownership, validation boundary, and completion claim are materially unchanged.", - expected_output="Amends Files.Modify once with the exact additional path and records the supporting evidence before the first write, while keeping the same disposable lightweight plan.", - owning_clauses=( - "Before the first write, one explicit plan amendment may add exactly one additional path to `Files.Modify` when it has the same implementation owner and purpose, decision authority, expected delta, impact radius, ownership, validation boundary, and completion claim remain materially unchanged.", - "Record the exact path and supporting evidence in the existing disposable plan.", - ), - ) - - -def test_pd_12_material_lightweight_scope_pressure_escalates() -> None: - assert_development_case( - "dev-lightweight-material-under-decomposition", - prompt="Execution reveals that the proposed extra file introduces a new production owner and an independent validation boundary.", - expected_output="Treats the task as materially under-decomposed and escalates to full orchestration instead of repeatedly expanding the lightweight plan.", - owning_clauses=( - "If new evidence makes the task materially under-decomposed—a new production or lifecycle owner, independent validation boundary, wide impact, API or workflow decision, second repository, or barrier or convergence topology—stop and escalate to full orchestration.", - ), - ) - assert_development_case( - "dev-lightweight-amendment-after-mutation", - prompt="A lightweight task has already mutated an authorized file when it discovers one more file that would otherwise satisfy the bounded amendment conditions.", - expected_output="Does not amend the mutation envelope after mutation has begun; stops and escalates to full orchestration with the concrete scope evidence.", - owning_clauses=( - "The amendment must not be repeated or made after mutation begins.", - ), - ) - - -def test_pd_13_normal_lightweight_change_remains_one_disposable_plan() -> None: - assert_development_case( - "dev-lightweight-algorithm-not-settled", - prompt="Plan a bounded mechanical change whose algorithm is not yet chosen, while purpose, accepted authority, expected delta, and impact radius are settled.", - expected_output="Allows the disposable lightweight plan because eligibility does not require a settled implementation strategy; it does not import executor-result, Completed, or a review package. Eval JSON stores this as a pressure scenario; presence is not executed agent-behavior proof.", - owning_clauses=( - "Create a bounded mechanical plan when purpose, accepted or `none relevant` authority, expected delta, and impact radius are settled even if the internal algorithm is not chosen.", - "Eligibility does not require the internal implementation strategy to be settled.", - ), - ) - assert_development_case( - "dev-lightweight-amendment-lane-separation", - prompt="A pre-mutation one-file amendment remains same-owner and mechanically bounded, but the agent proposes adding an executor result, task state, review package, and archive record for assurance.", - expected_output="Allows only the exact bounded Files.Modify amendment and rejects heavy lifecycle artifacts; the lightweight lane remains one disposable plan.", - owning_clauses=( - "Keep one disposable `.work-bundle/runtime/dev-plans/` artifact.", - "Do not import executor-result, `Completed`, review package, archive helper, or heavy Knowledge Base Update closure into the lightweight lane.", - ), - ) - - -def test_pd_14_equivalent_under_decomposition_routes_by_lane_without_widening( - tmp_path: Path, -) -> None: - assert_development_case( - "dev-lightweight-material-under-decomposition", - prompt="Execution reveals that the proposed extra file introduces a new production owner and an independent validation boundary.", - expected_output="Treats the task as materially under-decomposed and escalates to full orchestration instead of repeatedly expanding the lightweight plan.", - owning_clauses=( - "If new evidence makes the task materially under-decomposed—a new production or lifecycle owner, independent validation boundary, wide impact, API or workflow decision, second repository, or barrier or convergence topology—stop and escalate to full orchestration.", - ), - ) - assert_normative_case( - "PD-07", - prompt="Execution proves one task materially under-decomposed after its repair frontier separates into two independently owned regions.", - expected_output="Stops repeatedly enlarging the task, requires a return to the plan, and reslices only the affected region while preserving the original binding, baseline, accepted unaffected regions, and typed repair route.", - skill_path="skills/orch-create-implementation-plan/SKILL.md", - owning_clause="When execution proves a task materially under-decomposed, return to the plan and reslice only the affected region around the newly evidenced seam. Preserve the original binding, baseline, and accepted unaffected regions; do not repeatedly enlarge the task.", - ) - binding = {"binding_id": "binding-task-003", "sha256": "1" * 64} - baseline = {"head": "2" * 40, "tree": "3" * 40} - unaffected = [ - { - "artifact_id": "task-001", - "revision": "1", - "sha256": ZERO_SHA, - "source_tree": ZERO_TREE, - }, - { - "artifact_id": "task-002", - "revision": "1", - "sha256": "4" * 64, - "source_tree": "5" * 40, - }, - ] - region = { - "task_ids": ["task-003"], - "paths": ["scripts/orchestration/review_runtime.py"], - "interfaces": ["API-PD-001"], - "validation_oracles": ["VAL-004"], - } - routed = route_review_verdict( - allocation_gap(), - affected_region=region, - unaffected_evidence_identities=unaffected, - original_binding_identity=binding, - original_baseline_identity=baseline, - ) - assert routed["action"] == "reslice_plan" - assert routed["affected_region"] == region - assert routed["preserved_evidence_identities"] == unaffected - assert routed["silent_expansion_allowed"] is False - assert routed["preserve_valid_work_and_evidence"] is True - - orch = tmp_path / ".work-bundle/orchestration" - spec = orch / "spec/active/spec.md" - plan = orch / "plan/active/plan.md" - spec.parent.mkdir(parents=True) - plan.parent.mkdir(parents=True) - spec.write_text("---\nid: spec-test\nstatus: verified\n---\nAuthority\n", encoding="utf-8") - plan.write_text( - "---\nid: plan-001\nstatus: Planned\nsource_spec: [spec-test]\n---\nResliced\n", - encoding="utf-8", - ) - with pytest.raises(ReviewContractError, match="accepted repaired plan-review authority"): - resume_plan_return( - routed, - workspace_root=tmp_path, - plan_path=plan, - current_binding_identity=binding, - current_baseline_identity=baseline, - current_unaffected_evidence_identities=unaffected, - ) - - review = { - "review_id": "review-plan-pd14", - "stage": "plan", - "target_identity": plan_review_identity(tmp_path, plan), - "reviewer": { - "agent_id": "reviewer-pd14", - "capability": "judgment", - "authorship": "none", - "repair_participation": "none", - "decision_participation": "none", - "deliberation_participation": "none", - "context_origin": "direct_source", - }, - "evidence": { - "mode": "direct", - "capabilities": ["source inspection"], - "unavailable_evidence": [], - "commands": [], - "artifacts": [], - }, - "verdict": "accepted", - "findings": [], - "started_at": "2026-09-07T00:00:00Z", - "completed_at": "2026-09-07T00:01:00Z", - "staleness": {"is_stale": False, "reason": None, "supersedes": None}, - } - review = bind_review_receipt(tmp_path, review) - publish_review(tmp_path, review, current_target_identity=review["target_identity"]) - - resumed = resume_plan_return( - routed, - workspace_root=tmp_path, - plan_path=plan, - current_binding_identity=binding, - current_baseline_identity=baseline, - current_unaffected_evidence_identities=unaffected, - ) - assert resumed["execution_state"] == "ready_from_repaired_authority" - assert resumed["affected_region"] == region - assert resumed["preserved_evidence_identities"] == unaffected diff --git a/tests/test_orchestration_plans.py b/tests/test_orchestration_plans.py new file mode 100644 index 0000000..2370966 --- /dev/null +++ b/tests/test_orchestration_plans.py @@ -0,0 +1,536 @@ +from __future__ import annotations + +import argparse +import json +import os +import subprocess +import sys +from pathlib import Path + +import pytest +import yaml + + +REPO_ROOT = Path(__file__).resolve().parents[1] +ORCHESTRATION = REPO_ROOT / "scripts" / "orchestration" +sys.path.insert(0, str(ORCHESTRATION)) + +from artifact_store import family_policy, load_catalog, read_artifact # noqa: E402 +import plans # noqa: E402 +import specs # noqa: E402 +import bounded_closure # noqa: E402 + + +CATALOG = REPO_ROOT / "references/assets/orchestration/contract/artifact-family-catalog-v5.yaml" + + +def _args(root: Path, **overrides: object) -> argparse.Namespace: + values: dict[str, object] = { + "workspace_root": str(root), + "project_root": None, + "id": "plan-stage4", + "title": "Stage 4 plan", + "purpose": "Create executable planning semantics", + "component": "orchestration", + "version": "1.0", + "content_file": "", + "status": "draft", + "filename": None, + "source_spec_id": "spec-stage4", + "plan_id": None, + "phase_id": None, + "task_id": None, + "kind": None, + "handoff": None, + } + values.update(overrides) + return argparse.Namespace(**values) + + +@pytest.fixture +def workspace(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: + for relative in [ + ".work-bundle/orchestration/spec/active", + ".work-bundle/orchestration/spec/archived", + ".work-bundle/orchestration/plan/active", + ".work-bundle/orchestration/plan/archived", + ]: + (tmp_path / relative).mkdir(parents=True) + monkeypatch.setattr(bounded_closure, "resolve_working_workspace", lambda _root: None) + for module in (plans, specs): + monkeypatch.setattr(module, "resolve_workspace_root", lambda _args: tmp_path) + if hasattr(module, "resolve_working_workspace"): + monkeypatch.setattr(module, "resolve_working_workspace", lambda _root: None) + monkeypatch.setattr( + module, + "orchestration_root", + lambda _args: tmp_path / ".work-bundle/orchestration", + ) + monkeypatch.setattr(module, "init_dirs", lambda _args: None) + monkeypatch.setattr( + module, + "rel", + lambda path, _args: Path(path).resolve().relative_to(tmp_path.resolve()).as_posix(), + ) + spec_input = tmp_path / "spec-input.md" + spec_input.write_text( + "---\nproject: demo\nsource_knowledge: []\nrelated_handoffs: []\ntags: [orchestration]\n" + "execution_workspace: {isolation: existing, profile: default, cleanup: manual}\n---\n" + "# Specification\n\n- **REQ-001A:** Preserve the exact suffixed source identifier.\n" + "- **AC-001:** Compile the canonical task.\n", + encoding="utf-8", + ) + specs.cmd_write_spec( + argparse.Namespace( + workspace_root=str(tmp_path), project_root=None, id="spec-stage4", + title="Stage 4 source", purpose="Drive Stage 4", component="orchestration", + version="1.0", content_file=str(spec_input), status="verified", filename=None, + ) + ) + return tmp_path + + +def _write_yaml(path: Path, value: dict[str, object]) -> Path: + path.write_text(yaml.safe_dump(value, sort_keys=False), encoding="utf-8") + return path + + +def _plan_semantics() -> dict[str, object]: + return { + "source_coverage": [ + { + "source_id": "REQ-001A", + "obligation_kind": "requirement", + "phase_ids": ["phase-stage4"], + "task_ids": ["task-stage4"], + "validation_ids": ["VAL-001"], + } + ], + "authority": {"decision_authority": ["none-relevant"]}, + "strategy": {"summary": "One coherent phase and task."}, + "phase_index": [{"id": "phase-stage4", "order": 1}], + "dependency_graph": {"edges": []}, + "risks": [], + "validation_strategy": [{"id": "VAL-001", "kind": "process"}], + "completion_criteria": ["REQ-001A and AC-001 are validated."], + "knowledge_base_update": {"action": "update"}, + "semantic_loop": {"result": "converged"}, + "execution_workspace": {"isolation": "existing", "profile": "default", "cleanup": "manual"}, + } + + +def _phase_semantics() -> dict[str, object]: + return { + "order": 1, + "source_ids": ["REQ-001A", "AC-001"], + "depends_on": [], + "task_index": [{"id": "task-stage4", "order": 1}], + "barriers": [], + "validation": [{"id": "VAL-001", "kind": "process"}], + "completion_criteria": ["The task compiles."], + "allocated_rules": [], + "allocated_skills": ["dev-test-driven-development"], + } + + +def _task_semantics() -> dict[str, object]: + return { + "order": 1, + "task_type": "implementation", + "source_ids": ["REQ-001A", "AC-001"], + "source_obligations": [ + { + "source_id": "REQ-001A", + "semantic": "Preserve exact specification authority in the compiled task.", + }, + { + "source_id": "AC-001", + "semantic": "The canonical YAML task compiles from structured plan authority.", + }, + ], + "truth_basis": { + "purpose": "Preserve exact specification authority.", + "as_is_evidence": ["Current compiler consumes structured tasks."], + "decision_authority": ["none-relevant"], + "expected_delta": ["Canonical YAML task compiles."], + "conflict_status": "clear", + }, + "depends_on": [], + "source_files": ["scripts/orchestration/execution_context.py"], + "target_files": ["scripts/orchestration/execution_context.py"], + "target_symbols": ["_task_context"], + "interfaces": {"consumes": ["REQ-001A"], "produces": []}, + "steps": ["Adapt the canonical reader."], + "validation": [ + { + "id": "VAL-001", "kind": "process", "command": "pytest -q", + "proves": ["AC-001"], "expected": "passed", + "invariant_ids": ["INV-001"], + "capability_reason": "The focused process check exercises compilation.", + } + ], + "evidence_capability": { + "result": "mapped", + "reason": "The process check exercises compilation.", + "invariants": [ + { + "id": "INV-001", "source_ids": ["AC-001"], + "invariant": "The canonical task compiles.", "boundary": "component", + "oracle": "VAL-001", "capability_reason": "The compiler is invoked directly.", + "freshness": "current_task_batch", "task_id": "task-stage4", + "evidence_ids": ["VAL-001"], "closure_result": "pending", + } + ], + }, + "completion_criteria": ["The canonical task compiles."], + "methodology": {"primary": "tdd", "required_skills": ["dev-test-driven-development"]}, + "executor_profile": {"capability": "judgment", "context_mode": "compiled-brief", "review_capability": "judgment"}, + "acceptance_review": {"required": False, "reviewer_independent": False, "verdict": "pending", "reviewed_head": "", "findings": []}, + "allocated_rules": [], + "allocated_skills": ["dev-test-driven-development"], + "handoff_contract": "executor-result-v1", + } + + +def _create_tree(workspace: Path, tmp_path: Path) -> tuple[Path, Path, Path]: + plan_input = _write_yaml(tmp_path / "plan.yaml", _plan_semantics()) + plans.cmd_write_plan(_args(workspace, content_file=str(plan_input))) + phase_input = _write_yaml(tmp_path / "phase.yaml", _phase_semantics()) + plans.cmd_write_phase( + _args( + workspace, id=None, plan_id="plan-stage4", phase_id="phase-stage4", + title="Stage 4 phase", content_file=str(phase_input), status="planned", + ) + ) + task_input = _write_yaml(tmp_path / "task.yaml", _task_semantics()) + plans.cmd_write_task( + _args( + workspace, id=None, plan_id="plan-stage4", phase_id="phase-stage4", + task_id="task-stage4", title="Stage 4 task", content_file=str(task_input), + status="planned", + ) + ) + root = workspace / ".work-bundle/orchestration/plan/active" + return ( + root / "plan-stage4.plan.yaml", + root / "plan-stage4/phase-stage4.phase.yaml", + root / "plan-stage4/phase-stage4/task-stage4.task.yaml", + ) + + +def test_catalog_v5_registers_distinct_canonical_yaml_planning_families() -> None: + assert load_catalog(CATALOG)["catalog_id"] == "artifact-family-catalog-v5" + catalog = load_catalog(CATALOG) + for family, suffix in ( + ("root-plan", ".plan.yaml"), + ("phase", ".phase.yaml"), + ("task", ".task.yaml"), + ): + policy = family_policy(catalog, family) + assert policy["representation"] == "yaml" + assert policy["locator"]["template"].endswith(suffix) + assert policy["index"]["path"].endswith(f"{family}-index.jsonl") + + +def test_write_read_and_index_canonical_plan_tree(workspace: Path, tmp_path: Path) -> None: + root_path, phase_path, task_path = _create_tree(workspace, tmp_path) + assert root_path.is_file() and phase_path.is_file() and task_path.is_file() + assert not list(root_path.parent.rglob("*.md")) + + rows = plans.index_plans(_args(workspace)) + assert [(row["artifact_type"], row["id"]) for row in rows] == [ + ("phase", "phase-stage4"), + ("root-plan", "plan-stage4"), + ("task", "task-stage4"), + ] + assert not (workspace / ".work-bundle/orchestration/plan/index.jsonl").exists() + task = read_artifact( + CATALOG, "task", {"workspace_root": workspace}, identity="task-stage4", + state="active", bindings={"plan": "plan-stage4", "phase": "phase-stage4"}, + )["data"] + assert task["source_ids"] == ["REQ-001A", "AC-001"] + + +def test_structural_override_and_unverified_source_fail_before_mutation( + workspace: Path, tmp_path: Path, +) -> None: + bad = _plan_semantics() + bad["id"] = "plan-evil" + content = _write_yaml(tmp_path / "bad-plan.yaml", bad) + with pytest.raises(SystemExit, match="structural field override"): + plans.cmd_write_plan(_args(workspace, content_file=str(content))) + assert not list((workspace / ".work-bundle/orchestration/plan/active").glob("*.plan.yaml")) + + specs.cmd_set_spec_status( + argparse.Namespace(workspace_root=str(workspace), project_root=None, id="spec-stage4", status="draft") + ) + content = _write_yaml(tmp_path / "plan.yaml", _plan_semantics()) + with pytest.raises(SystemExit, match="verified"): + plans.cmd_write_plan(_args(workspace, content_file=str(content))) + assert not list((workspace / ".work-bundle/orchestration/plan/active").glob("*.plan.yaml")) + + +def test_collision_and_wrong_parent_preserve_existing_bytes(workspace: Path, tmp_path: Path) -> None: + root_path, phase_path, task_path = _create_tree(workspace, tmp_path) + before = {path: path.read_bytes() for path in (root_path, phase_path, task_path)} + with pytest.raises(SystemExit, match="collision"): + plans.cmd_write_plan(_args(workspace, content_file=str(tmp_path / "plan.yaml"))) + wrong = _write_yaml(tmp_path / "wrong-task.yaml", _task_semantics()) + with pytest.raises(SystemExit, match="parent|phase|canonical"): + plans.cmd_write_task( + _args( + workspace, plan_id="plan-stage4", phase_id="phase-missing", + task_id="task-other", title="Wrong", content_file=str(wrong), status="planned", + ) + ) + assert {path: path.read_bytes() for path in before} == before + + +def test_schema_invalid_task_fails_before_mutation(workspace: Path, tmp_path: Path) -> None: + _root_path, _phase_path, task_path = _create_tree(workspace, tmp_path) + task_path.unlink() + plans.index_plans(_args(workspace)) + invalid = _task_semantics() + invalid["validation"] = "pytest -q" + content = _write_yaml(tmp_path / "invalid-task.yaml", invalid) + + with pytest.raises(SystemExit, match="schema validation failed"): + plans.cmd_write_task( + _args( + workspace, id=None, plan_id="plan-stage4", phase_id="phase-stage4", + task_id="task-stage4", title="Invalid task", content_file=str(content), + status="planned", + ) + ) + + assert not task_path.exists() + + +def test_task_source_obligations_must_exactly_bind_source_ids_before_write( + workspace: Path, tmp_path: Path, +) -> None: + _root_path, _phase_path, task_path = _create_tree(workspace, tmp_path) + task_path.unlink() + plans.index_plans(_args(workspace)) + invalid = _task_semantics() + invalid["source_obligations"] = invalid["source_obligations"][:1] + content = _write_yaml(tmp_path / "invalid-obligations.yaml", invalid) + + with pytest.raises(SystemExit, match="exactly bind source_ids"): + plans.cmd_write_task( + _args( + workspace, id=None, plan_id="plan-stage4", phase_id="phase-stage4", + task_id="task-stage4", title="Invalid task", content_file=str(content), + status="planned", + ) + ) + + assert not task_path.exists() + + +def test_status_is_planning_only_and_phase_task_execution_state_is_deferred( + workspace: Path, tmp_path: Path, +) -> None: + root_path, _phase_path, _task_path = _create_tree(workspace, tmp_path) + plans.cmd_set_plan_status(_args(workspace, id="plan-stage4", status="verified")) + assert yaml.safe_load(root_path.read_text())["status"] == "verified" + with pytest.raises(SystemExit, match="stage5-required"): + plans.cmd_set_plan_status( + _args(workspace, id="task-stage4", kind="task", plan_id="plan-stage4", status="Completed") + ) + + +def test_plan_qualification_transitions_are_monotonic( + workspace: Path, tmp_path: Path, +) -> None: + root_path, _phase_path, _task_path = _create_tree(workspace, tmp_path) + plans.cmd_set_plan_status(_args(workspace, id="plan-stage4", status="verified")) + with pytest.raises(SystemExit, match="Invalid plan qualification transition"): + plans.cmd_set_plan_status(_args(workspace, id="plan-stage4", status="draft")) + assert yaml.safe_load(root_path.read_text())["status"] == "verified" + + plans.cmd_set_plan_status(_args(workspace, id="plan-stage4", status="superseded")) + with pytest.raises(SystemExit, match="Invalid plan qualification transition"): + plans.cmd_set_plan_status(_args(workspace, id="plan-stage4", status="verified")) + assert yaml.safe_load(root_path.read_text())["status"] == "superseded" + + +def test_automatic_plan_ids_advance_without_collision( + workspace: Path, tmp_path: Path, +) -> None: + first = _write_yaml(tmp_path / "plan-one.yaml", _plan_semantics()) + second = _write_yaml(tmp_path / "plan-two.yaml", _plan_semantics()) + plans.cmd_write_plan(_args(workspace, id=None, content_file=str(first))) + plans.cmd_write_plan(_args(workspace, id=None, content_file=str(second))) + + ids = [ + row["id"] for row in plans.index_plans(_args(workspace)) + if row["artifact_type"] == "root-plan" + ] + date = plans.now_date().replace("-", "") + assert ids == [f"plan-{date}-001", f"plan-{date}-002"] + + +@pytest.mark.parametrize( + "mutate", + [ + lambda value: value["source_coverage"][0].update(phase_ids=[], task_ids=[]), + lambda value: value.update( + execution_workspace={ + "isolation": "invented", "profile": "default", "cleanup": "manual", + } + ), + ], +) +def test_root_plan_schema_rejects_empty_coverage_and_unknown_workspace_policy( + workspace: Path, tmp_path: Path, mutate, +) -> None: + semantic = _plan_semantics() + mutate(semantic) + content = _write_yaml(tmp_path / "invalid-plan.yaml", semantic) + with pytest.raises(SystemExit, match="schema validation failed"): + plans.cmd_write_plan(_args(workspace, content_file=str(content))) + assert not list((workspace / ".work-bundle/orchestration/plan/active").glob("*.plan.yaml")) + + +def test_per_family_indexes_are_deterministic_and_contain_no_fallback_fields( + workspace: Path, tmp_path: Path, +) -> None: + _create_tree(workspace, tmp_path) + first = { + path.name: path.read_bytes() + for path in (workspace / ".work-bundle/orchestration/plan").glob("*-index.jsonl") + } + plans.index_plans(_args(workspace)) + second = { + path.name: path.read_bytes() + for path in (workspace / ".work-bundle/orchestration/plan").glob("*-index.jsonl") + } + assert first == second + for payload in second.values(): + for line in payload.decode().splitlines(): + row = json.loads(line) + assert row["id"] + assert "path" not in row + + +def test_corrupt_derived_index_is_rebuilt_from_canonical_artifacts( + workspace: Path, tmp_path: Path, +) -> None: + root_path, _phase_path, _task_path = _create_tree(workspace, tmp_path) + index = workspace / ".work-bundle/orchestration/plan/root-plan-index.jsonl" + index.write_text("not-json\n", encoding="utf-8") + + rows = plans.index_plans(_args(workspace)) + + assert root_path.is_file() + assert any(row["id"] == "plan-stage4" for row in rows) + assert json.loads(index.read_text().strip())["id"] == "plan-stage4" + + +def test_public_entrypoint_writes_and_lists_canonical_plan_tree(tmp_path: Path) -> None: + workspace_id = "wb-stage4-entrypoint" + control = tmp_path / ".work-bundle" + control.mkdir() + (control / "project.yaml").write_text( + "metadata_version: 4\nauthority: canonical\n" + f"workspace: {{id: {workspace_id}, slug: stage4, mode: single-repository}}\n" + "control_plane: {schema_version: 1, repository: {remote: ''}, sync_policy: {mode: manual}}\n" + "source_repositories:\n" + " - id: source\n role: source\n locator: {type: manual, value: fixture}\n" + " default_branch: main\n workspace_binding: {type: root}\n" + " materialization: {required: true}\n operation_policy: inherit\n", + encoding="utf-8", + ) + home = tmp_path / "home" + config = home / ".work-bundle" + (config / "registry").mkdir(parents=True) + (config / "bootstrap.yaml").write_text( + "bootstrap_version: v1\nauthority: canonical\n" + f"work_bundle_root: {REPO_ROOT}\n" + 'project_registry: "$work_bundle_config_root/registry/projects.yaml"\n' + 'skill_registry: "$work_bundle_config_root/registry/skill-registry.yaml"\n', + encoding="utf-8", + ) + (config / "registry/projects.yaml").write_text( + "registry_schema_version: 1\nprojects: []\ndevice_bindings:\n" + f" {workspace_id}:\n slug: stage4\n workspace_root: {tmp_path}\n" + f" control_plane_path: {control}\n control_plane_remote: ''\n" + " observed_control_plane_head: ''\n repositories:\n source:\n" + f" project_root: {tmp_path}\n checkout_kind: manual\n" + " observed_branch: ''\n observed_head: ''\n" + " observed_at: '2099-01-01T00:00:00Z'\n git_common_dir: ''\n", + encoding="utf-8", + ) + spec_input = tmp_path / "spec.md" + spec_input.write_text( + "---\nproject: demo\nsource_knowledge: []\nrelated_handoffs: []\ntags: [stage4]\n" + "execution_workspace: {isolation: existing, profile: default, cleanup: manual}\n---\n" + "# Specification\n\n- **REQ-001A:** Preserve the exact ID.\n", + encoding="utf-8", + ) + plan_input = _write_yaml(tmp_path / "plan.yaml", _plan_semantics()) + env = {**os.environ, "HOME": str(home)} + entry = str(REPO_ROOT / "scripts/orch.py") + create_spec = subprocess.run( + [ + sys.executable, entry, "write-spec", "--workspace-root", str(tmp_path), + "--id", "spec-stage4", "--title", "Source", "--purpose", "Stage 4", + "--component", "orchestration", "--status", "verified", + "--content-file", str(spec_input), + ], + cwd=REPO_ROOT, env=env, text=True, capture_output=True, check=False, + ) + assert create_spec.returncode == 0, create_spec.stderr + create_plan = subprocess.run( + [ + sys.executable, entry, "write-plan", "--workspace-root", str(tmp_path), + "--id", "plan-stage4", "--source-spec-id", "spec-stage4", + "--title", "Plan", "--purpose", "Stage 4", "--component", "orchestration", + "--content-file", str(plan_input), + ], + cwd=REPO_ROOT, env=env, text=True, capture_output=True, check=False, + ) + assert create_plan.returncode == 0, create_plan.stderr + phase_input = _write_yaml(tmp_path / "phase.yaml", _phase_semantics()) + create_phase = subprocess.run( + [ + sys.executable, entry, "write-phase", "--workspace-root", str(tmp_path), + "--plan-id", "plan-stage4", "--phase-id", "phase-stage4", + "--title", "Phase", "--content-file", str(phase_input), + ], + cwd=REPO_ROOT, env=env, text=True, capture_output=True, check=False, + ) + assert create_phase.returncode == 0, create_phase.stderr + task_input = _write_yaml(tmp_path / "task.yaml", _task_semantics()) + create_task = subprocess.run( + [ + sys.executable, entry, "write-task", "--workspace-root", str(tmp_path), + "--plan-id", "plan-stage4", "--phase-id", "phase-stage4", + "--task-id", "task-stage4", "--title", "Task", + "--content-file", str(task_input), + ], + cwd=REPO_ROOT, env=env, text=True, capture_output=True, check=False, + ) + assert create_task.returncode == 0, create_task.stderr + listed = subprocess.run( + [sys.executable, entry, "list-plans", "--workspace-root", str(tmp_path)], + cwd=REPO_ROOT, env=env, text=True, capture_output=True, check=False, + ) + assert listed.returncode == 0, listed.stderr + rows = [json.loads(line) for line in listed.stdout.strip().splitlines()] + assert [(row["artifact_type"], row["id"]) for row in rows] == [ + ("phase", "phase-stage4"), + ("root-plan", "plan-stage4"), + ("task", "task-stage4"), + ] + assert {row["path"] for row in rows} == { + ".work-bundle/orchestration/plan/active/plan-stage4.plan.yaml", + ".work-bundle/orchestration/plan/active/plan-stage4/phase-stage4.phase.yaml", + ".work-bundle/orchestration/plan/active/plan-stage4/phase-stage4/task-stage4.task.yaml", + } + doctor = subprocess.run( + [sys.executable, entry, "doctor", "--workspace-root", str(tmp_path)], + cwd=REPO_ROOT, env=env, text=True, capture_output=True, check=False, + ) + assert "invalid current planning families" not in doctor.stdout + doctor.stderr diff --git a/tests/test_orchestration_prewrite_and_navigation.py b/tests/test_orchestration_prewrite_and_navigation.py new file mode 100644 index 0000000..01e7a43 --- /dev/null +++ b/tests/test_orchestration_prewrite_and_navigation.py @@ -0,0 +1,415 @@ +from __future__ import annotations + +import argparse +import json +from pathlib import Path +import subprocess +import sys + +import pytest +import yaml + + +REPO_ROOT = Path(__file__).resolve().parents[1] +ORCHESTRATION = REPO_ROOT / "scripts" / "orchestration" +loaded_core = sys.modules.get("core") +loaded_core_path = Path(getattr(loaded_core, "__file__", "")) if loaded_core is not None else None +if loaded_core_path is not None and ORCHESTRATION not in loaded_core_path.parents: + sys.modules.pop("core", None) +sys.path.insert(0, str(ORCHESTRATION)) + +import core # noqa: E402 +import documents # noqa: E402 +import doctor # noqa: E402 +import evaluation_identity # noqa: E402 +import handoffs # noqa: E402 +import plans # noqa: E402 +import specs # noqa: E402 + + +def _args(workspace: Path, **overrides: object) -> argparse.Namespace: + values: dict[str, object] = { + "workspace_root": str(workspace), + "project_root": None, + "id": "plan-prewrite", + "title": "Pre-write boundary", + "purpose": "Reject invalid content without workspace mutation", + "component": "orchestration", + "version": "1.0", + "content_file": "", + "status": "draft", + "filename": None, + "source_spec_id": "spec-prewrite", + "plan_id": None, + "task_id": None, + } + values.update(overrides) + return argparse.Namespace(**values) + + +def _bind_clean_workspace( + monkeypatch: pytest.MonkeyPatch, workspace: Path, *modules: object +) -> None: + for module in modules: + monkeypatch.setattr(module, "resolve_workspace_root", lambda _args: workspace) + if hasattr(module, "resolve_working_workspace"): + monkeypatch.setattr(module, "resolve_working_workspace", lambda _root: None) + if hasattr(module, "orchestration_root"): + monkeypatch.setattr( + module, + "orchestration_root", + lambda _args: workspace / ".work-bundle/orchestration", + ) + if hasattr(module, "rel"): + monkeypatch.setattr( + module, + "rel", + lambda path, _args: Path(path).resolve().relative_to(workspace).as_posix(), + ) + + +def test_invalid_specification_and_plan_inputs_do_not_mutate_clean_workspace( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, +) -> None: + workspace = tmp_path / "workspace" + workspace.mkdir() + _bind_clean_workspace(monkeypatch, workspace, core, specs, plans) + + invalid_spec = tmp_path / "invalid-spec.md" + invalid_spec.write_text("---\nid: caller-owned\n---\nBody\n", encoding="utf-8") + with pytest.raises(SystemExit, match="structural field override"): + specs.cmd_write_spec( + _args(workspace, id="spec-prewrite", content_file=str(invalid_spec)) + ) + assert not (workspace / ".work-bundle").exists() + + invalid_plan = tmp_path / "invalid-plan.yaml" + invalid_plan.write_text("{}\n", encoding="utf-8") + monkeypatch.setattr( + plans, + "read_artifact", + lambda *_args, **_kwargs: {"data": {"status": "verified"}}, + ) + with pytest.raises(SystemExit, match="schema validation failed"): + plans.cmd_write_plan(_args(workspace, content_file=str(invalid_plan))) + assert not (workspace / ".work-bundle").exists() + + +def test_current_initialization_creates_no_retired_handoff_or_aggregate_index_state( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, +) -> None: + workspace = tmp_path / "workspace" + workspace.mkdir() + monkeypatch.setattr( + core, + "orchestration_root", + lambda _args: workspace / ".work-bundle/orchestration", + ) + + core.init_dirs(_args(workspace)) + + root = workspace / ".work-bundle/orchestration" + for retired in ( + "handoff/orchestration", + "handoff/executor", + "handoff/index.jsonl", + "plan/index.jsonl", + ): + assert not (root / retired).exists() + for current in ( + "spec/active", + "plan/active", + "result/executor/active", + "result/accepted/active", + "review/implementation/active", + "review/final/active", + ): + assert (root / current).is_dir() + + +def test_current_specification_and_plan_writes_create_only_canonical_stores( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, +) -> None: + workspace = tmp_path / "workspace" + workspace.mkdir() + _bind_clean_workspace(monkeypatch, workspace, core, specs, plans) + spec_input = tmp_path / "spec.md" + spec_input.write_text( + "---\nproject: demo\nsource_knowledge: []\nrelated_handoffs: []\n" + "tags: [current]\nexecution_workspace: " + "{isolation: existing, profile: default, cleanup: manual}\n---\n" + "# Current specification\n", + encoding="utf-8", + ) + specs.cmd_write_spec( + _args( + workspace, + id="spec-prewrite", + content_file=str(spec_input), + status="verified", + ) + ) + plan_input = tmp_path / "plan.yaml" + plan_input.write_text( + yaml.safe_dump( + { + "source_coverage": [ + { + "source_id": "REQ-001", + "obligation_kind": "requirement", + "phase_ids": ["phase-prewrite"], + } + ], + "authority": {"decision": "source specification"}, + "strategy": {"summary": "one phase"}, + "phase_index": [{"id": "phase-prewrite", "order": 1}], + "dependency_graph": {}, + "risks": [], + "validation_strategy": [{"id": "VAL-001", "kind": "process"}], + "completion_criteria": ["The current plan is stored."], + "knowledge_base_update": {"action": "none"}, + "semantic_loop": {"result": "converged"}, + "execution_workspace": { + "isolation": "existing", + "profile": "default", + "cleanup": "manual", + }, + }, + sort_keys=False, + ), + encoding="utf-8", + ) + plans.cmd_write_plan(_args(workspace, content_file=str(plan_input))) + + root = workspace / ".work-bundle/orchestration" + assert (root / "spec/active/spec-prewrite.spec.md").is_file() + assert (root / "plan/active/plan-prewrite.plan.yaml").is_file() + for retired in ( + "handoff/orchestration", + "handoff/executor", + "handoff/index.jsonl", + "plan/index.jsonl", + ): + assert not (root / retired).exists() + + +def test_initialization_manifest_provisions_current_stores_only() -> None: + manifest = yaml.safe_load( + (REPO_ROOT / "references/wb-initialize-project-default-work-bundle-tree.yaml").read_text( + encoding="utf-8" + ) + ) + roots = set(manifest["roots"]) + for retired in ( + ".work-bundle/orchestration/handoff/orchestration/active", + ".work-bundle/orchestration/handoff/executor/active", + ".work-bundle/orchestration/reviews", + ): + assert retired not in roots + for current in ( + ".work-bundle/orchestration/result/executor/active", + ".work-bundle/orchestration/result/accepted/active", + ".work-bundle/orchestration/review/implementation/active", + ".work-bundle/orchestration/review/final/active", + ): + assert current in roots + + +def test_doctor_uses_the_same_current_catalog_as_runtime_writers() -> None: + assert doctor.CATALOG.resolve() == plans.CATALOG_PATH.resolve() + assert doctor.CATALOG.name == "artifact-family-catalog-v5.yaml" + + +def test_legacy_knowledge_migration_and_handoff_constants_are_not_public() -> None: + help_result = subprocess.run( + [sys.executable, str(REPO_ROOT / "scripts/ks.py"), "--help"], + cwd=REPO_ROOT, + text=True, + capture_output=True, + check=False, + ) + assert help_result.returncode == 0, help_result.stderr + assert "migrate-v3" in help_result.stdout + assert "migrate-legacy" not in help_result.stdout + assert not hasattr(core, "HANDOFF_STATUSES") + assert not hasattr(core, "HANDOFF_TYPES") + + +def test_evaluation_observation_exclusions_use_current_result_and_review_stores() -> None: + roots = set(evaluation_identity.OBSERVATION_ARTIFACT_ROOTS) + assert ".work-bundle/orchestration/result/" in roots + assert ".work-bundle/orchestration/review/" in roots + assert ".work-bundle/orchestration/handoff/" not in roots + assert ".work-bundle/orchestration/reviews/" not in roots + + +def test_current_rules_and_knowledge_reference_have_no_legacy_handoff_store() -> None: + artifact_rule = ( + REPO_ROOT / "rules/orchestration/orch-artifact-authoring.md" + ).read_text(encoding="utf-8") + knowledge_rule = ( + REPO_ROOT / "rules/keep-summarizing/ks-knowledge-boundary.md" + ).read_text(encoding="utf-8") + workflow = ( + REPO_ROOT / "references/assets/keep-summarizing/workflow.md" + ).read_text(encoding="utf-8") + assert "handoff-orchestration-v1.md" not in artifact_rule + assert "Orchestration handoff" not in artifact_rule + for text in (knowledge_rule, workflow): + assert ".work-bundle/orchestration/handoff/" not in text + assert ".work-bundle/orchestration/result/" in text + assert ".work-bundle/orchestration/review/" in text + + +def test_wor126_orchestration_rules_satisfy_contract_and_index_metadata() -> None: + rule_ids = ( + "orch-bounded-closure", + "orch-handoff-required", + "orch-orchestration-boundary", + "orch-review-completion", + ) + index = yaml.safe_load((REPO_ROOT / "rules/index.yaml").read_text(encoding="utf-8")) + entries = {entry["id"]: entry for entry in index["rules"]} + for rule_id in rule_ids: + path = REPO_ROOT / "rules/orchestration" / f"{rule_id}.md" + text = path.read_text(encoding="utf-8") + assert text.startswith("---\n") + end = text.index("\n---\n", 4) + front_matter = yaml.safe_load(text[4:end]) + for section in ("Purpose", "Must", "Must Not", "Validation", "On Violation"): + assert f"## {section}" in text + assert entries[rule_id] == { + "id": rule_id, + "path": f"orchestration/{rule_id}.md", + "applies_when": front_matter["applies_when"], + "enforcement": front_matter["enforcement"], + "load": front_matter["load"], + "requires": front_matter["requires"], + } + + +def test_retired_workflow_branch_reader_and_handoff_contract_are_excluded() -> None: + help_result = subprocess.run( + [sys.executable, str(REPO_ROOT / "scripts/wb.py"), "--help"], + cwd=REPO_ROOT, + text=True, + capture_output=True, + check=False, + ) + assert help_result.returncode == 0, help_result.stderr + assert "workflow-branches" not in help_result.stdout + doctor_source = (REPO_ROOT / "scripts/work-bundle/doctor.py").read_text(encoding="utf-8") + dispatcher_source = (REPO_ROOT / "scripts/work-bundle/dispatcher.py").read_text(encoding="utf-8") + assert "orchestration/reviews" not in doctor_source + assert "workflow-branches" not in dispatcher_source + contract = ( + REPO_ROOT + / "references/assets/orchestration/contract/handoff-orchestration-v1.md" + ).read_text(encoding="utf-8") + assert "historical exclusion" in contract.lower() + assert "no compatibility authority" in contract.lower() + assert "readable and indexable" not in contract.lower() + + +def test_current_orchestration_guidance_names_executor_results_and_direct_reviews() -> None: + readme = (REPO_ROOT / "scripts/orchestration/README.md").read_text(encoding="utf-8") + for current in ( + "write-executor-result", + "write-implementation-review", + "write-final-workflow-review", + ".work-bundle/orchestration/result/executor/", + ".work-bundle/orchestration/review/implementation/", + ): + assert current in readme + for retired in ( + "observe-task-validation", + "validate-executor-result", + "runtime/handoff/review", + "before writing the handoff", + ): + assert retired not in readme + + rule_path = REPO_ROOT / "rules/orchestration/orch-knowledge-gateway.md" + rule_text = rule_path.read_text(encoding="utf-8") + assert "canonical `executor-result-v1`" in rule_text + assert "orchestration handoff" not in rule_text + assert "executor-result handoff" not in rule_text + assert "execution-completion handoffs" not in rule_text + + index = yaml.safe_load((REPO_ROOT / "rules/index.yaml").read_text(encoding="utf-8")) + entry = next(item for item in index["rules"] if item["id"] == "orch-knowledge-gateway") + front_matter = yaml.safe_load(rule_text[4 : rule_text.index("\n---\n", 4)]) + assert entry["applies_when"] == front_matter["applies_when"] + + +def _patch_navigation_sources(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(documents, "init_dirs", lambda _args: None) + monkeypatch.setattr( + documents, + "index_specs", + lambda _args: [{"artifact_type": "specification", "id": "spec-current", "status": "verified"}], + ) + monkeypatch.setattr( + documents, + "index_plans", + lambda _args: [{"artifact_type": "task", "id": "task-current", "status": "planned"}], + ) + results = lambda _args: [ + { + "artifact_type": "executor-result", + "id": "result-current", + "plan_id": "plan-current", + "task_id": "task-current", + "result_state": "active", + } + ] + if hasattr(documents, "list_executor_results"): + monkeypatch.setattr(documents, "list_executor_results", results) + else: + monkeypatch.setattr(documents, "index_handoffs", results) + + +def test_public_navigation_uses_current_executor_result_names_and_fields( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], +) -> None: + _patch_navigation_sources(monkeypatch) + monkeypatch.setattr( + documents, + "orchestration_root", + lambda _args: tmp_path / ".work-bundle/orchestration", + ) + + documents.cmd_state(_args(tmp_path)) + state = json.loads(capsys.readouterr().out) + assert state["executor_results"] == {"active": 1} + assert "handoffs" not in state + + documents.cmd_next_action_candidates(_args(tmp_path)) + action = json.loads(capsys.readouterr().out) + assert action == { + "action": "review-executor-result", + "executor_result_id": "result-current", + "reason": "active executor result exists", + } + + +def test_related_navigation_never_reads_legacy_aggregate_indexes( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], +) -> None: + _patch_navigation_sources(monkeypatch) + monkeypatch.setattr( + documents, + "orchestration_root", + lambda _args: tmp_path / ".work-bundle/orchestration", + ) + monkeypatch.setattr( + documents, + "load_index", + lambda _path: (_ for _ in ()).throw(AssertionError("legacy aggregate index read")), + raising=False, + ) + + documents.cmd_related(_args(tmp_path, id="result-current")) + + assert json.loads(capsys.readouterr().out)["id"] == "result-current" + assert not hasattr(handoffs, "index_handoffs") diff --git a/tests/test_orchestration_repository_preflight.py b/tests/test_orchestration_repository_preflight.py index fd81280..6b68081 100644 --- a/tests/test_orchestration_repository_preflight.py +++ b/tests/test_orchestration_repository_preflight.py @@ -45,71 +45,6 @@ def test_malformed_accepted_baseline_is_typed(tmp_path: Path) -> None: preflight_module._load_baselines(str(malformed)) -def write_project_metadata(project: Path, repo: Path, *, branch: str = "main", commit: str | None = None) -> None: - head = commit if commit is not None else git(repo, "rev-parse", "HEAD") - (project / ".work-bundle").mkdir(parents=True, exist_ok=True) - (project / ".work-bundle" / "project.yaml").write_text( - "\n".join( - [ - "metadata_version: 2", - "source_repositories:", - " - id: repo-main", - f" path: {repo.resolve()}", - " work_dir: true", - ' remote: ""', - " git_repository: true", - f" working_branch: {branch}", - " branch_required: true", - " last_commit_id: " + head, - " baseline_status: current", - " codegraph:", - " supported: false", - " index_present: false", - f" root: {repo.resolve()}", - " status: not-indexed", - ' synced_commit_id: ""', - ' last_synced_at: ""', - " reason: no-index", - "", - ] - ), - encoding="utf-8", - ) - - -def write_workspace_metadata_v3(workspace: Path, repo: Path) -> None: - head = git(repo, "rev-parse", "HEAD") - (workspace / ".work-bundle").mkdir(parents=True, exist_ok=True) - (workspace / ".work-bundle" / "project.yaml").write_text( - "\n".join( - [ - "metadata_version: 3", - f"workspace_root: {workspace.resolve()}", - "workspace_mode: multi-repository", - "source_repositories:", - " - id: repo-main", - f" project_root: {repo.resolve()}", - " origin_id: origin-main", - " checkout_kind: managed-worktree", - " git_repository: true", - " expected_branch: main", - f" observed_head: {head}", - " baseline_status: current", - " codegraph:", - " supported: false", - " index_present: false", - f" root: {repo.resolve()}", - " status: not-indexed", - ' synced_commit_id: ""', - ' last_synced_at: ""', - " reason: no-index", - "", - ] - ), - encoding="utf-8", - ) - - def write_workspace_metadata_v4(workspace: Path, workspace_id: str = "wb-test") -> None: (workspace / ".work-bundle").mkdir(parents=True, exist_ok=True) (workspace / ".work-bundle" / "project.yaml").write_text( @@ -121,6 +56,12 @@ def write_workspace_metadata_v4(workspace: Path, workspace_id: str = "wb-test") f" id: {workspace_id}", " slug: test", " mode: multi-repository", + "control_plane:", + " schema_version: 1", + " repository:", + ' remote: ""', + " sync_policy:", + " mode: manual", "source_repositories:", " - id: repo-main", " role: source", @@ -132,6 +73,7 @@ def write_workspace_metadata_v4(workspace: Path, workspace_id: str = "wb-test") " name: repo-main", " materialization:", " required: true", + " operation_policy: inherit", "", ] ), @@ -141,26 +83,34 @@ def write_workspace_metadata_v4(workspace: Path, workspace_id: str = "wb-test") def write_v4_registry( path: Path, + workspace: Path, repo: Path | None, *, workspace_id: str = "wb-test", observed_branch: str = "main", observed_head: str = "", ) -> None: - repository_lines = [" repo-main:"] + repository_lines: list[str] = [] if repo is not None: + repository_lines.append(" repo-main:") repository_lines.append(f" project_root: {repo.resolve()}") - if observed_branch: + repository_lines.append(" checkout_kind: managed-worktree") + repository_lines.append(f" git_common_dir: {repo.resolve() / '.git'}") + repository_lines.append(" observed_at: 2026-09-20T00:00:00Z") + if repo is not None and observed_branch: repository_lines.append(f" observed_branch: {observed_branch}") - if observed_head: - repository_lines.append(f" observed_head: {observed_head}") + if repo is not None and observed_head: + repository_lines.append(f" observed_head: {json.dumps(observed_head)}") path.write_text( "\n".join( [ - "metadata_version: 4", + "registry_schema_version: 1", + "projects: []", "device_bindings:", f" {workspace_id}:", - " repositories:", + " slug: test", + f" workspace_root: {workspace.resolve()}", + " repositories:" if repository_lines else " repositories: {}", *repository_lines, "", ] @@ -169,6 +119,16 @@ def write_v4_registry( ) +def use_registry(monkeypatch: pytest.MonkeyPatch, registry: Path) -> None: + monkeypatch.setattr( + preflight_module._infrastructure, + "load_project_registry", + lambda: preflight_module._infrastructure.load_infrastructure_document( + registry, family="project-registry", toolkit_root=REPO_ROOT + ), + ) + + @pytest.mark.parametrize("indentless", [False, True]) def test_v4_repository_entries_accept_yaml_sequence_indentation_and_merge_device_binding( tmp_path: Path, @@ -177,30 +137,18 @@ def test_v4_repository_entries_accept_yaml_sequence_indentation_and_merge_device ) -> None: workspace = tmp_path / "workspace" workspace.mkdir() - repo = repository(tmp_path) + repo = repository(workspace, "repo-main") registry = tmp_path / "projects.yaml" - marker = "- " if indentless else " - " - child = " " if indentless else " " + write_workspace_metadata_v4(workspace) metadata = workspace / ".work-bundle/project.yaml" - metadata.parent.mkdir() - metadata.write_text( - "metadata_version: 4\n" - "workspace:\n" - " id: wb-test\n" - " mode: multi-repository\n" - "source_repositories:\n" - f"{marker}id: repo-main\n" - f"{child}role: source\n" - f"{child}remote:\n" - f"{child} canonical: https://example.com/repo.git\n" - f"{child}default_branch: main\n" - f"{child}materialization:\n" - f"{child} required: true\n", - encoding="utf-8", - ) + if indentless: + text = metadata.read_text(encoding="utf-8") + start = text.index("source_repositories:\n") + len("source_repositories:\n") + tail = [line[2:] if line.startswith(" ") else line for line in text[start:].splitlines()] + metadata.write_text(text[:start] + "\n".join(tail) + "\n", encoding="utf-8") head = git(repo, "rev-parse", "HEAD") - write_v4_registry(registry, repo, observed_head=head) - monkeypatch.setattr(preflight_module, "project_registry_path", lambda: registry) + write_v4_registry(registry, workspace, repo, observed_head=head) + use_registry(monkeypatch, registry) entries = preflight_module._metadata_repository_entries(workspace) @@ -219,16 +167,11 @@ def test_v4_preflight_keeps_missing_device_observation_as_typed_failure( workspace.mkdir() registry = tmp_path / "projects.yaml" write_workspace_metadata_v4(workspace) - write_v4_registry(registry, None) - monkeypatch.setattr(preflight_module, "project_registry_path", lambda: registry) + write_v4_registry(registry, workspace, None) + use_registry(monkeypatch, registry) - result = repository_preflight(resolve_target_repositories(workspace)) - - assert result["repository_preflight"]["status"] == "blocked" - row = result["repository_preflight"]["repositories"][0] - assert row["status"] == "missing-observation" - assert row["failure_code"] == "WB_REPOSITORY_OBSERVATION_PROJECT_ROOT_MISSING" - assert row["metadata"]["repository_id"] == "repo-main" + with pytest.raises(SystemExit, match="WB_INFRASTRUCTURE_REPOSITORY_BINDING_MISSING"): + resolve_target_repositories(workspace) def test_v4_preflight_detects_stale_device_head_without_refreshing_registry( @@ -236,12 +179,12 @@ def test_v4_preflight_detects_stale_device_head_without_refreshing_registry( ) -> None: workspace = tmp_path / "workspace" workspace.mkdir() - repo = repository(tmp_path) + repo = repository(workspace, "repo-main") registry = tmp_path / "projects.yaml" write_workspace_metadata_v4(workspace) - write_v4_registry(registry, repo, observed_head="0" * 40) + write_v4_registry(registry, workspace, repo, observed_head="0" * 40) before = registry.read_text(encoding="utf-8") - monkeypatch.setattr(preflight_module, "project_registry_path", lambda: registry) + use_registry(monkeypatch, registry) result = repository_preflight(resolve_target_repositories(workspace)) @@ -387,114 +330,6 @@ def test_git_backed_post_sync_style_unexplained_change_blocks(tmp_path: Path) -> assert row["unexplained_changes"] == ["?? codegraph-side-effect.txt"] -def test_resolution_prefers_task_write_scopes_then_metadata(tmp_path: Path) -> None: - project = tmp_path / "project" - project.mkdir() - target = repository(tmp_path, "target") - fallback = repository(tmp_path, "fallback") - write_project_metadata(project, fallback) - task = project / "task.md" - task.write_text(f"---\ntarget_files:\n - {target / 'new.py'}\nsource_files:\n - .work-bundle/project.yaml\n---\n", encoding="utf-8") - - assert resolve_target_repositories(project, [task]) == [ - {"path": str(target.resolve()), "source": "task-write-scope"} - ] - assert resolve_target_repositories(project) == [ - { - "path": str(fallback.resolve()), - "source": "project-metadata", - "metadata": { - "id": "repo-main", - "path": str(fallback.resolve()), - "work_dir": True, - "remote": "", - "git_repository": True, - "working_branch": "main", - "branch_required": True, - "last_commit_id": git(fallback, "rev-parse", "HEAD"), - "baseline_status": "current", - "codegraph": { - "supported": False, - "index_present": False, - "root": str(fallback.resolve()), - "status": "not-indexed", - "synced_commit_id": "", - "last_synced_at": "", - "reason": "no-index", - }, - }, - } - ] - - -def test_metadata_preflight_reports_branch_commit_and_codegraph_no_index(tmp_path: Path) -> None: - project = tmp_path / "project" - project.mkdir() - repo = repository(tmp_path) - write_project_metadata(project, repo) - - result = repository_preflight(resolve_target_repositories(project)) - - payload = result["repository_preflight"] - assert payload["status"] == "passed" - row = payload["repositories"][0] - assert row["metadata"]["repository_id"] == "repo-main" - assert row["metadata"]["branch_status"] == "matched" - assert row["metadata"]["commit_status"] == "matched" - assert row["metadata"]["codegraph"]["actual_index_present"] is False - assert row["metadata"]["codegraph"]["reason"] == "no-index" - - -def test_v3_workspace_metadata_resolves_and_preflights_member_root(tmp_path: Path) -> None: - workspace = tmp_path / "workspace" - workspace.mkdir() - member = repository(workspace, "member") - write_workspace_metadata_v3(workspace, member) - - targets = resolve_target_repositories(workspace) - assert targets[0]["path"] == str(member.resolve()) - assert targets[0]["source"] == "project-metadata" - - result = repository_preflight(targets) - row = result["repository_preflight"]["repositories"][0] - assert result["repository_preflight"]["status"] == "passed" - assert row["metadata"]["expected_branch"] == "main" - assert row["metadata"]["actual_branch"] == "main" - assert row["metadata"]["commit_status"] == "matched" - assert row["metadata"]["codegraph"]["reason"] == "no-index" - - -def test_metadata_preflight_blocks_branch_mismatch(tmp_path: Path) -> None: - project = tmp_path / "project" - project.mkdir() - repo = repository(tmp_path) - write_project_metadata(project, repo, branch="wrong") - - result = repository_preflight(resolve_target_repositories(project)) - row = result["repository_preflight"]["repositories"][0] - - assert result["repository_preflight"]["status"] == "blocked" - assert row["status"] == "branch-mismatch" - assert row["metadata"]["branch_status"] == "mismatch" - - -def test_metadata_preflight_blocks_stale_commit(tmp_path: Path) -> None: - project = tmp_path / "project" - project.mkdir() - repo = repository(tmp_path) - old_head = git(repo, "rev-parse", "HEAD") - (repo / "tracked.txt").write_text("later\n", encoding="utf-8") - git(repo, "commit", "-am", "later") - write_project_metadata(project, repo, commit=old_head) - - result = repository_preflight(resolve_target_repositories(project)) - row = result["repository_preflight"]["repositories"][0] - - assert result["repository_preflight"]["status"] == "blocked" - assert row["status"] == "stale-baseline" - assert row["metadata"]["commit_status"] == "stale" - - def test_resolution_excludes_orchestration_artifacts_and_falls_through_to_source( tmp_path: Path, ) -> None: @@ -540,23 +375,6 @@ def test_resolution_keeps_explicit_source_target_inside_nested_repository(tmp_pa ] -def test_cli_outputs_machine_usable_json(tmp_path: Path) -> None: - repo = repository(tmp_path) - command = [ - sys.executable, - str(REPO_ROOT / "scripts" / "orch.py"), - "repository-preflight", - "--project-root", - str(tmp_path), - "--repository", - str(repo), - ] - result = subprocess.run(command, check=True, capture_output=True, text=True) - payload = json.loads(result.stdout) - assert payload["repository_preflight"]["status"] == "passed" - assert payload["repository_preflight"]["repositories"][0]["status"] == "clean" - - def test_repository_preflight_help_describes_accepted_baseline_contract() -> None: result = subprocess.run( [ diff --git a/tests/test_orchestration_review_frontier.py b/tests/test_orchestration_review_frontier.py deleted file mode 100644 index 3d720b9..0000000 --- a/tests/test_orchestration_review_frontier.py +++ /dev/null @@ -1,312 +0,0 @@ -from __future__ import annotations - -import hashlib -import json -import subprocess -import sys -from copy import deepcopy -from pathlib import Path - -import pytest - - -REPO_ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(REPO_ROOT / "scripts" / "orchestration")) -sys.path.insert(0, str(REPO_ROOT / "scripts" / "work-bundle")) - -import review_runtime # noqa: E402 -import reviewer_workspace # noqa: E402 - - -ZERO_SHA = "0" * 64 - - -def identity(name: str, digest: str) -> dict[str, object]: - return {"artifact_id": name, "revision": "1", "sha256": digest, "source_tree": None} - - -def finding(target: dict[str, object], finding_id: str = "RF-FINDING-1") -> dict[str, object]: - return { - "finding_id": finding_id, - "stage": "implementation", - "class": "implementation_defect", - "severity": "blocking", - "first_broken_artifact": "implementation", - "obligation_basis": "accepted_requirement", - "evidence": [{"kind": "test", "locator": "RF", "digest_or_identity": "RF-RED", "observation": "failed"}], - "target_identity": target, - "summary": "Repair this bounded defect.", - "recommended_owner": "task_owner", - "disposition": "repair_task", - } - - -def review(*, target: dict[str, object], mode: str = "initial", kind: str = "stage", agent: str = "reviewer-new") -> dict[str, object]: - return { - "review_id": f"review-{mode}-{agent}", - "review_mode": mode, - "review_target_kind": kind, - "repair_frontier": None, - "review_reset": None, - "stage": "plan", - "target_identity": target, - "reviewer": { - "agent_id": agent, - "capability": "judgment", - "authorship": "none", - "repair_participation": "none", - "decision_participation": "none", - "deliberation_participation": "none", - "context_origin": "direct_source", - }, - "evidence": {"mode": "direct", "capabilities": ["bounded source inspection"], "unavailable_evidence": [], "commands": [], "artifacts": []}, - "verdict": "accepted", - "findings": [], - "started_at": "2026-09-06T00:00:00Z", - "completed_at": "2026-09-06T00:01:00Z", - "staleness": {"is_stale": False, "reason": None, "supersedes": None}, - } - - -def repair_pair() -> tuple[dict[str, object], dict[str, object]]: - old = identity("task-rf", "1" * 64) - new = identity("task-rf", "2" * 64) - prior = review(target=old, agent="reviewer-prior") - prior["review_id"] = "review-prior" - prior["verdict"] = "repair" - prior["findings"] = [finding(old)] - current = review(target=new, mode="repair") - current["repair_frontier"] = { - "prior_review_id": prior["review_id"], - "blocking_finding_ids": ["RF-FINDING-1"], - "previous_reviewed_identity": old, - "repaired_identity": new, - "affected_boundaries": ["scripts/orchestration/review_runtime.py:validate_stage_review"], - "frozen_evidence_reference": review_runtime.review_evidence_identity(prior), - } - return prior, current - - -def test_rf_01_initial_and_repair_modes_are_native_and_closed() -> None: - initial = review(target=identity("task-rf", ZERO_SHA)) - validated = review_runtime.validate_stage_review(initial) - assert (validated.review_mode, validated.review_target_kind, validated.repair_frontier) == ("initial", "stage", None) - schema = json.loads((REPO_ROOT / "references/assets/orchestration/contract/stage-review-v1.schema.json").read_text()) - assert schema["$defs"]["reviewMode"]["enum"] == ["initial", "repair"] - assert schema["$defs"]["reviewTargetKind"]["enum"] == ["task", "stage"] - initial["repair_frontier"] = {"unexpected": True} - with pytest.raises(review_runtime.ReviewContractError, match="initial review cannot carry"): - review_runtime.validate_stage_review(initial) - - -def test_rf_02_repair_frontier_binds_prior_findings_evidence_and_exact_identities() -> None: - prior, current = repair_pair() - validated = review_runtime.validate_review_sequence(current, previous_review=prior) - assert validated.repair_frontier["previous_reviewed_identity"] == prior["target_identity"] - schema = json.loads((REPO_ROOT / "references/assets/orchestration/contract/stage-review-v1.schema.json").read_text()) - assert set(schema["$defs"]["repairFrontier"]["required"]) == set(current["repair_frontier"]) - tampered = deepcopy(current) - tampered["repair_frontier"]["blocking_finding_ids"] = ["UNKNOWN"] - with pytest.raises(review_runtime.ReviewContractError, match="blocking finding IDs"): - review_runtime.validate_review_sequence(tampered, previous_review=prior) - - -def test_rf_03_repair_reuses_frozen_evidence_without_copying_history() -> None: - prior, current = repair_pair() - prior["evidence"]["artifacts"] = [{"path": f"history/{index}.json", "sha256": ZERO_SHA} for index in range(100)] - current["repair_frontier"]["frozen_evidence_reference"] = review_runtime.review_evidence_identity(prior) - validated = review_runtime.validate_review_sequence(current, previous_review=prior) - assert validated.evidence["artifacts"] == [] - assert len(json.dumps(validated.repair_frontier)) < len(json.dumps(prior["evidence"])) - - -@pytest.mark.parametrize("reason_class", ["material_redesign", "authority", "scope", "acceptance", "decomposition", "validation_allocation"]) -def test_rf_04_material_change_requires_fresh_initial_review(reason_class: str) -> None: - prior, repair = repair_pair() - with pytest.raises(review_runtime.ReviewContractError, match="fresh initial review"): - review_runtime.validate_review_sequence(repair, previous_review=prior, material_change=reason_class) - reset = review(target=repair["target_identity"], agent="reviewer-reset") - reset["review_reset"] = {"prior_review_id": prior["review_id"], "reason_class": reason_class, "reason": "accepted boundary changed"} - assert review_runtime.validate_review_sequence(reset, previous_review=prior, material_change=reason_class).review_mode == "initial" - - -def test_rf_05_reset_allows_same_independent_reviewer_and_keeps_safeguards() -> None: - prior, repair = repair_pair() - reset = review(target=repair["target_identity"], agent=prior["reviewer"]["agent_id"]) - reset["review_reset"] = {"prior_review_id": prior["review_id"], "reason_class": "scope", "reason": "scope changed"} - assert review_runtime.validate_review_sequence( - reset, previous_review=prior, material_change="scope" - ).reviewer["agent_id"] == prior["reviewer"]["agent_id"] - - participating = deepcopy(reset) - participating["reviewer"]["repair_participation"] = "present" - with pytest.raises(review_runtime.ReviewContractError, match="repair_participation"): - review_runtime.validate_review_sequence( - participating, previous_review=prior, material_change="scope" - ) - - nonjudgment = deepcopy(reset) - nonjudgment["reviewer"]["capability"] = "standard" - with pytest.raises(review_runtime.ReviewContractError, match="judgment reviewer"): - review_runtime.validate_review_sequence( - nonjudgment, previous_review=prior, material_change="scope" - ) - - -def test_rf_06_repair_rejects_stale_or_relabelled_identity() -> None: - prior, current = repair_pair() - current["target_identity"] = identity("task-rf", "3" * 64) - with pytest.raises(review_runtime.ReviewContractError, match="repaired identity"): - review_runtime.validate_review_sequence(current, previous_review=prior) - - -def test_rf_07_repair_still_detects_regression_in_affected_boundary() -> None: - prior, current = repair_pair() - current["findings"] = [finding(current["target_identity"], "RF-REGRESSION")] - with pytest.raises(review_runtime.ReviewContractError, match="accepted review cannot contain blocking"): - review_runtime.validate_review_sequence(current, previous_review=prior) - - -def test_rf_08_repair_packet_is_bounded_for_task_and_stage_targets(tmp_path: Path) -> None: - prior, current = repair_pair() - target = tmp_path / "target.md" - target.write_text("---\nid: task-rf\n---\nrepaired\n") - current["target_identity"] = review_runtime.artifact_review_identity(target) - current["repair_frontier"]["repaired_identity"] = current["target_identity"] - context = { - "stage": "plan", "target_identity": current["target_identity"], "target_locator": "control:target.md", - "agent_id": "reviewer-new", "capability": "judgment", "execution_id": "exec-rf", - "evidence_mode": "reproducible_snapshot", "review_mode": "repair", "review_target_kind": "stage", - "repair_frontier": current["repair_frontier"], "review_reset": None, - } - artifact = {"locator": "control:target.md", "sha256": hashlib.sha256(target.read_bytes()).hexdigest(), "content_base64": ""} - manifest = review_runtime.stage_evidence_manifest(tmp_path, tmp_path, context, [artifact]) - assert manifest["missing"] == [] - assert [entry["locator"] for entry in manifest["entries"]] == ["control:target.md"] - assert manifest["repair_frontier_reference"] == current["repair_frontier"]["frozen_evidence_reference"] - assert reviewer_workspace._validate_stage_context(context)["review_target_kind"] == "stage" - - task_prior = {key: value for key, value in prior.items() if key != "stage"} - task_prior.update(required=True, reviewer_independent=True, review_target_kind="task") - task_current = {key: value for key, value in current.items() if key != "stage"} - task_current.update(required=True, reviewer_independent=True, verdict="accept", review_target_kind="task", previous_review=task_prior) - assert review_runtime.validate_task_acceptance_review(task_current).target_identity == current["target_identity"] - - -def test_rf_01_h1_repairs_recorded_a_without_reacquiring_unrecorded_latent_b(tmp_path: Path) -> None: - target = tmp_path / "repair-a.md" - latent = tmp_path / "latent-b.md" - old_content = "---\nid: artifact-rf01\nversion: 1\n---\nA is broken\n" - new_content = old_content.replace("broken", "repaired") - target.write_text(new_content) - latent.write_text("B is latent and outside H1's recorded surface.\n") - old_identity = review_runtime.artifact_review_identity(target, content=old_content) - new_identity = review_runtime.artifact_review_identity(target) - - initial_h1 = review(target=old_identity, agent="reviewer-h1") - initial_h1.update(review_id="review-h1", verdict="repair", findings=[finding(old_identity, "RF-A")]) - repair_a = review(target=new_identity, mode="repair", agent="reviewer-repair-a") - repair_a["repair_frontier"] = { - "prior_review_id": "review-h1", "blocking_finding_ids": ["RF-A"], - "previous_reviewed_identity": old_identity, "repaired_identity": new_identity, - "affected_boundaries": ["control:repair-a.md"], - "frozen_evidence_reference": review_runtime.review_evidence_identity(initial_h1), - } - assert review_runtime.validate_review_sequence(repair_a, previous_review=initial_h1).verdict == "accepted" - - context = { - "stage": "plan", "target_identity": new_identity, "target_locator": "control:repair-a.md", - "agent_id": "reviewer-repair-a", "capability": "judgment", "execution_id": "exec-rf01", - "evidence_mode": "reproducible_snapshot", "review_mode": "repair", "review_target_kind": "stage", - "repair_frontier": repair_a["repair_frontier"], "review_reset": None, - } - manifest = review_runtime.stage_evidence_manifest( - tmp_path, tmp_path, context, - [{"locator": "control:repair-a.md", "sha256": hashlib.sha256(target.read_bytes()).hexdigest()}], - ) - assert manifest["missing"] == [] - assert [entry["locator"] for entry in manifest["entries"]] == ["control:repair-a.md"] - assert "latent-b.md" not in json.dumps(manifest) - - -@pytest.mark.parametrize( - ("invariant", "finding_class", "obligation", "first_broken"), - [ - ("security", "implementation_defect", "essential_safety", "implementation"), - ("ownership", "implementation_defect", "accepted_requirement", "implementation"), - ("destructive-safety", "implementation_defect", "essential_safety", "implementation"), - ("evidence-integrity", "validation_oracle_defect", "evidence_integrity", "validation_oracle"), - ], -) -def test_rf_03_capable_untouched_invariant_stays_blocking_during_narrow_repair( - invariant: str, finding_class: str, obligation: str, first_broken: str -) -> None: - prior, narrow = repair_pair() - safety = finding(narrow["target_identity"], f"RF-SAFETY-{invariant}") - safety.update( - **{ - "class": finding_class, - "obligation_basis": obligation, - "first_broken_artifact": first_broken, - "recommended_owner": review_runtime.classify_first_broken_owner(finding_class)[1], - "disposition": review_runtime.classify_first_broken_owner(finding_class)[2], - } - ) - safety["evidence"] = [{ - "kind": "test", "locator": f"accepted:{invariant}", - "digest_or_identity": f"RF-03-{invariant}", "observation": "capable evidence still fails", - }] - narrow.update(verdict="repair", findings=[safety]) - assert review_runtime.validate_review_sequence(narrow, previous_review=prior).verdict == "repair" - routed = review_runtime._route_review_finding(safety) - assert routed["first_broken_artifact"] == first_broken - assert routed["preserve_valid_work_and_evidence"] is True - - -def test_rf_06_final_broad_integrated_review_rediscovers_and_classifies_latent_b(tmp_path: Path) -> None: - for args in (("init", "-q"), ("config", "user.name", "Test"), ("config", "user.email", "test@example.com")): - subprocess.run(["git", "-C", str(tmp_path), *args], check=True) - (tmp_path / ".gitignore").write_text(".work-bundle/\n") - (tmp_path / "direct-a.py").write_text("A = 'repaired'\n") - (tmp_path / "latent-b.py").write_text("B = 'deferred-non-load-bearing'\n") - subprocess.run(["git", "-C", str(tmp_path), "add", "."], check=True) - subprocess.run(["git", "-C", str(tmp_path), "commit", "-qm", "integrated"], check=True) - spec = tmp_path / ".work-bundle/orchestration/spec/active/spec-rf06.md" - spec.parent.mkdir(parents=True) - spec.write_text("---\nid: spec-rf06\nversion: 1\nstatus: verified\n---\nAccepted scope.\n") - plan = tmp_path / ".work-bundle/orchestration/plan/active/plan-rf06.md" - plan.parent.mkdir(parents=True) - plan.write_text("---\nid: plan-rf06\nversion: 1\nstatus: In progress\nsource_spec: [.work-bundle/orchestration/spec/active/spec-rf06.md]\n---\nFinal broad review.\n") - target_identity = review_runtime.stage_target_identity( - tmp_path, "integrated_implementation", plan, source_root=tmp_path - ) - artifacts = [ - {"locator": "control:.work-bundle/orchestration/plan/active/plan-rf06.md", "sha256": hashlib.sha256(plan.read_bytes()).hexdigest()}, - {"locator": "control:.work-bundle/orchestration/spec/active/spec-rf06.md", "sha256": hashlib.sha256(spec.read_bytes()).hexdigest()}, - ] - for entry in review_runtime.source_snapshot_entries(tmp_path): - path = tmp_path / entry["locator"].removeprefix("source:") - artifacts.append({"locator": entry["locator"], "sha256": hashlib.sha256(path.read_bytes()).hexdigest()}) - context = { - "stage": "integrated_implementation", "target_identity": target_identity, - "target_locator": "control:.work-bundle/orchestration/plan/active/plan-rf06.md", - "agent_id": "reviewer-final", "capability": "judgment", "execution_id": "exec-rf06", - "evidence_mode": "reproducible_snapshot", "review_mode": "initial", "review_target_kind": "stage", - "repair_frontier": None, "review_reset": None, - } - manifest = review_runtime.stage_evidence_manifest(tmp_path, tmp_path, context, artifacts) - assert manifest["missing"] == [] - assert "source:latent-b.py" in {entry["locator"] for entry in manifest["source_tree"]} - assert next(entry for entry in manifest["entries"] if entry["locator"] == "source:latent-b.py")["role"] == "source_tree" - - latent_finding = finding(target_identity, "RF-LATENT-B") - latent_finding.update( - **{"class": "advisory_enhancement", "severity": "advisory", "obligation_basis": "none", - "first_broken_artifact": "implementation", "recommended_owner": "backlog_owner", "disposition": "record_advisory"} - ) - latent_finding["evidence"] = [{ - "kind": "source", "locator": "source:latent-b.py", "digest_or_identity": "RF-06-B", - "observation": "Final broad review rediscovered deferred non-load-bearing B.", - }] - assert review_runtime.validate_review_finding(latent_finding).finding_id == "RF-LATENT-B" - assert review_runtime._route_review_finding(latent_finding)["return_to"] == "backlog_owner" diff --git a/tests/test_orchestration_reviews.py b/tests/test_orchestration_reviews.py deleted file mode 100644 index 1fcd372..0000000 --- a/tests/test_orchestration_reviews.py +++ /dev/null @@ -1,1555 +0,0 @@ -from __future__ import annotations - -import base64 -import hashlib -import json -import subprocess -import sys -from copy import deepcopy -from pathlib import Path - -import pytest -from reviewer_run_fixtures import bind_review_receipt - - -REPO_ROOT = Path(__file__).resolve().parents[1] -ORCHESTRATION = REPO_ROOT / "scripts" / "orchestration" -sys.path.insert(0, str(ORCHESTRATION)) -import review_runtime # noqa: E402 -import bounded_closure # noqa: E402 - -from review_runtime import ( # noqa: E402 - ReviewContractError, - classify_first_broken_owner, - publish_review, - review_evidence_identity, - route_stored_review_verdict, - _route_review_finding as route_review_verdict, - transition_review_finding, - validate_contract_instance, - validate_review_sequence, - validate_stage_review, - validate_stage_reviews, - validate_task_acceptance_review, -) - - -ZERO_SHA = "0" * 64 -ZERO_TREE = "0" * 40 - - -def finding(finding_class: str = "implementation_defect") -> dict[str, object]: - artifact, owner, disposition = classify_first_broken_owner(finding_class) - return { - "finding_id": f"finding-{finding_class}", - "stage": "implementation", - "class": finding_class, - "severity": "blocking", - "first_broken_artifact": artifact, - "obligation_basis": "accepted_requirement", - "evidence": [ - { - "kind": "test", - "locator": "tests/test_orchestration_reviews.py", - "digest_or_identity": "VAL-B01", - "observation": "The focused oracle observed the contract failure.", - } - ], - "target_identity": { - "artifact_id": "task-b01", - "revision": "1", - "sha256": ZERO_SHA, - "source_tree": ZERO_TREE, - }, - "summary": "A classified review finding.", - "recommended_owner": owner, - "disposition": disposition, - } - - -def stage_review(stage: str) -> dict[str, object]: - return { - "review_id": f"review-{stage}", - "stage": stage, - "target_identity": { - "artifact_id": f"artifact-{stage}", - "revision": "1", - "sha256": ZERO_SHA, - "source_tree": None if stage != "integrated_implementation" else ZERO_TREE, - }, - "reviewer": { - "agent_id": "reviewer-1", - "capability": "judgment", - "authorship": "none", - "repair_participation": "none", - "decision_participation": "none", - "deliberation_participation": "none", - "context_origin": "direct_source", - }, - "evidence": { - "mode": "direct", - "capabilities": ["source inspection"], - "unavailable_evidence": [], - "commands": [], - "artifacts": [], - }, - "verdict": "accepted", - "findings": [], - "started_at": "2026-09-04T00:00:00Z", - "completed_at": "2026-09-04T00:01:00Z", - "staleness": {"is_stale": False, "reason": None, "supersedes": None}, - } - - -def test_task_repair_review_binds_authoritative_integrated_stage_predecessor() -> None: - previous = stage_review("integrated_implementation") - previous.update( - review_id="review-integrated-finding", - review_mode="initial", - review_target_kind="stage", - repair_frontier=None, - review_reset=None, - verdict="repair", - ) - previous["target_identity"] = { - "artifact_id": "task-005", - "revision": "a" * 40, - "sha256": "1" * 64, - "source_tree": "b" * 40, - } - blocking = finding() - blocking["finding_id"] = "WOR112-T005-INT-001" - blocking["target_identity"] = previous["target_identity"] - evaluator_control = finding("validation_oracle_defect") - evaluator_control["finding_id"] = "WOR112-EVALUATOR-CONTROL-001" - evaluator_control["target_identity"] = previous["target_identity"] - previous["findings"] = [blocking, evaluator_control] - repaired_identity = { - "artifact_id": "task-005", - "revision": "c" * 40, - "sha256": "2" * 64, - "source_tree": "d" * 40, - } - current = { - **stage_review("plan"), - "required": True, - "reviewer_independent": True, - "review_id": "review-task-repair", - "reviewed_head": repaired_identity["revision"], - "review_mode": "repair", - "review_target_kind": "task", - "repair_frontier": { - "prior_review_id": "review-integrated-finding", - "blocking_finding_ids": ["WOR112-T005-INT-001"], - "previous_reviewed_identity": previous["target_identity"], - "repaired_identity": repaired_identity, - "affected_boundaries": ["scripts/orchestration/review_runtime.py"], - "frozen_evidence_reference": review_evidence_identity(previous), - }, - "review_reset": None, - "target_identity": repaired_identity, - "verdict": "accept", - "findings": [], - "previous_review": previous, - } - - validated = validate_task_acceptance_review(current) - - assert validated.review_id == "review-task-repair" - assert validated.repair_frontier["prior_review_id"] == "review-integrated-finding" - - for finding_ids, message in ( - (["UNKNOWN-FINDING"], "unknown blocking finding IDs"), - (["WOR112-EVALUATOR-CONTROL-001"], "only task-owned blocking findings"), - ([], "must be non-empty"), - ): - invalid = deepcopy(current) - invalid["repair_frontier"]["blocking_finding_ids"] = finding_ids - with pytest.raises(ReviewContractError, match=message): - validate_task_acceptance_review(invalid) - - -def test_material_change_reset_allows_same_independent_judgment_reviewer() -> None: - previous = stage_review("plan") - previous.update( - review_mode="initial", - review_target_kind="stage", - repair_frontier=None, - review_reset=None, - ) - current = deepcopy(previous) - current["review_id"] = "review-plan-current" - current["target_identity"] = { - **previous["target_identity"], - "revision": "2", - "sha256": "2" * 64, - } - current["review_reset"] = { - "prior_review_id": previous["review_id"], - "reason_class": "scope", - "reason": "The accepted plan scope materially changed.", - } - - validated = validate_review_sequence( - current, previous_review=previous, material_change="scope" - ) - - assert validated.reviewer["agent_id"] == previous["reviewer"]["agent_id"] - assert validated.review_reset["prior_review_id"] == previous["review_id"] - - -@pytest.mark.parametrize( - ("field", "value", "message"), - [ - ("authorship", "present", "accepted review requires reviewer.authorship"), - ("capability", "standard", "judgment reviewer"), - ], -) -def test_material_change_reset_still_rejects_nonindependent_or_nonjudgment_reviewer( - field: str, value: str, message: str -) -> None: - previous = stage_review("plan") - previous.update( - review_mode="initial", - review_target_kind="stage", - repair_frontier=None, - review_reset=None, - ) - current = deepcopy(previous) - current["review_id"] = "review-plan-current" - current["target_identity"] = { - **previous["target_identity"], - "revision": "2", - "sha256": "2" * 64, - } - current["review_reset"] = { - "prior_review_id": previous["review_id"], - "reason_class": "scope", - "reason": "The accepted plan scope materially changed.", - } - current["reviewer"][field] = value - - with pytest.raises(ReviewContractError, match=message): - validate_review_sequence( - current, previous_review=previous, material_change="scope" - ) - - -@pytest.mark.parametrize( - ("finding_class", "expected"), - [ - ("specification_gap", ("specification", "specification_owner", "reopen_specification")), - ("decomposition_gap", ("plan", "plan_owner", "repair_plan")), - ("allocation_gap", ("plan", "plan_owner", "reslice_plan")), - ("implementation_defect", ("implementation", "task_owner", "repair_task")), - ("validation_oracle_defect", ("validation_oracle", "oracle_owner", "repair_oracle")), - ("environment_failure", ("environment", "environment_owner", "recover_environment")), - ("advisory_enhancement", ("implementation", "backlog_owner", "record_advisory")), - ], -) -def test_api_001_routes_every_class_to_first_broken_owner( - finding_class: str, expected: tuple[str, str, str] -) -> None: - assert classify_first_broken_owner(finding_class) == expected - record = finding(finding_class) - if finding_class == "advisory_enhancement": - record["severity"] = "advisory" - record["obligation_basis"] = "none" - validated = validate_contract_instance("reviewFinding", record) - assert validated.finding_class == finding_class - route_context = {} - if finding_class == "allocation_gap": - route_context = { - "affected_region": { - "task_ids": ["task-b01"], - "paths": [], - "interfaces": [], - "validation_oracles": [], - }, - "original_binding_identity": {"binding_id": "binding-b01", "sha256": "1" * 64}, - "original_baseline_identity": {"head": ZERO_TREE, "tree": ZERO_TREE}, - } - assert route_review_verdict(record, **route_context)["return_to"] == expected[1] - - -def test_review_store_is_required_before_public_finding_routing( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - record = stage_review("integrated_implementation") - record.update( - review_id="review-task-publication", - review_mode="initial", - review_target_kind="stage", - repair_frontier=None, - review_reset=None, - verdict="repair", - ) - item = finding() - item["target_identity"] = record["target_identity"] - record["findings"] = [item] - record["reviewer_run"] = { - "run_id": "reviewer-run-00000000-0000-0000-0000-000000000001", - "sha256": ZERO_SHA, - } - monkeypatch.setattr(review_runtime, "_validate_reviewer_run", lambda *_: None) - - with pytest.raises(ReviewContractError, match="stored review"): - route_stored_review_verdict( - tmp_path, item, current_target_identity=record["target_identity"] - ) - with pytest.raises(ReviewContractError, match="stored review"): - review_runtime.route_review_verdict( - tmp_path, item, current_target_identity=record["target_identity"] - ) - - reference = publish_review( - tmp_path, record, current_target_identity=record["target_identity"] - ) - routed = route_stored_review_verdict( - tmp_path, - reference, - current_target_identity=record["target_identity"], - finding_id=item["finding_id"], - ) - assert routed["return_to"] == "task_owner" - - -def test_finalization_required_refuses_direct_publication_before_store_side_effect( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - metadata = tmp_path / ".work-bundle/project.yaml" - metadata.parent.mkdir(parents=True) - metadata.write_text( - "metadata_version: 4\n" - "workspace: {id: workspace-final, slug: final, mode: single-repository}\n" - "orchestration_control:\n" - " schema_version: 1\n" - " post_execution_review_round_limit: 5\n", - encoding="utf-8", - ) - current = stage_review("integrated_implementation")["target_identity"] - reserved = bounded_closure.begin_review_round( - tmp_path, - flow_id="flow-final", - request_id="request-1", - review_id="review-counted", - target_identity=current, - executor_attempts=[{"execution_id": "executor-1", "state": "completed"}], - known_missing_evidence=[], - ) - bounded_closure.mark_review_round_prepared( - tmp_path, - flow_id="flow-final", - round_id=str(reserved["round_id"]), - ) - counted_path = ( - tmp_path / ".work-bundle/orchestration/reviews/review-counted.json" - ) - counted_path.parent.mkdir(parents=True) - counted_path.write_text('{"verdict":"accepted"}\n', encoding="utf-8") - counted_path.chmod(0o444) - bounded_closure.complete_review_round( - tmp_path, - flow_id="flow-final", - round_id=reserved["round_id"], - outcome="accepted", - review_reference={ - "review_id": "review-counted", - "sha256": hashlib.sha256(counted_path.read_bytes()).hexdigest(), - }, - ) - blocked = stage_review("integrated_implementation") - blocked.update( - review_id="review-after-finalization", - review_mode="initial", - review_target_kind="stage", - repair_frontier=None, - review_reset=None, - reviewer_run={ - "run_id": "reviewer-run-00000000-0000-0000-0000-000000000001", - "sha256": ZERO_SHA, - }, - ) - monkeypatch.setattr(review_runtime, "_validate_reviewer_run", lambda *_: None) - - with pytest.raises(ReviewContractError, match="ROUND_REQUIRED|FINALIZATION_REQUIRED"): - publish_review(tmp_path, blocked, current_target_identity=current) - - assert not ( - tmp_path - / ".work-bundle/orchestration/reviews/review-after-finalization.json" - ).exists() - - -@pytest.mark.parametrize("field", ["capabilities", "unavailable_evidence"]) -def test_api_002_rejects_empty_evidence_strings(field: str) -> None: - review = stage_review("specification") - review["evidence"][field] = [""] - - with pytest.raises(ReviewContractError, match=field): - validate_stage_review(review) - - schema = json.loads( - (REPO_ROOT / "references/assets/orchestration/contract/stage-review-v1.schema.json").read_text( - encoding="utf-8" - ) - ) - assert schema["$defs"]["stageReview"]["properties"]["evidence"]["properties"][field]["items"][ - "minLength" - ] == 1 - - -@pytest.mark.parametrize("capability", ["mechanical", "arbitrary", "", None]) -def test_stage_reviewer_capability_is_closed(capability): - record = stage_review("plan") - record["reviewer"]["capability"] = capability - with pytest.raises(ReviewContractError, match="capability"): - validate_stage_review(record) - - -@pytest.mark.parametrize("mode", ["packet_only", "constrained_direct"]) -def test_packet_or_constrained_evidence_cannot_accept(mode): - record = stage_review("plan") - record["evidence"]["mode"] = mode - with pytest.raises(ReviewContractError): - validate_stage_review(record) - - -def test_stage_batch_requires_actual_current_targets(): - records = [stage_review(s) for s in ("specification", "plan", "integrated_implementation")] - with pytest.raises(ReviewContractError, match="current target"): - validate_stage_reviews(records) - - -def test_api_001_rejects_unclassified_wrong_layer_and_unauthorized_blocking_advisory() -> None: - with pytest.raises(ReviewContractError, match="class"): - classify_first_broken_owner("unknown") - - wrong_layer = finding("implementation_defect") - wrong_layer["recommended_owner"] = "plan_owner" - with pytest.raises(ReviewContractError, match="routing"): - validate_contract_instance("reviewFinding", wrong_layer) - - advisory = finding("advisory_enhancement") - with pytest.raises(ReviewContractError, match="advisory_enhancement"): - validate_contract_instance("reviewFinding", advisory) - - -def test_api_001_reslice_pauses_repeated_expansion_and_preserves_evidence() -> None: - routed = route_review_verdict( - finding("allocation_gap"), - previous_scope_expansions=1, - affected_region={ - "task_ids": ["task-b01"], - "paths": [], - "interfaces": [], - "validation_oracles": [], - }, - original_binding_identity={"binding_id": "binding-b01", "sha256": "1" * 64}, - original_baseline_identity={"head": ZERO_TREE, "tree": ZERO_TREE}, - ) - assert routed == { - "finding_id": "finding-allocation_gap", - "first_broken_artifact": "plan", - "return_to": "plan_owner", - "action": "reslice_plan", - "execution_state": "paused_for_reslice", - "affected_region": { - "task_ids": ["task-b01"], - "paths": [], - "interfaces": [], - "validation_oracles": [], - }, - "returned_authority_identity": { - "artifact_id": "task-b01", - "revision": "1", - "sha256": ZERO_SHA, - "source_tree": ZERO_TREE, - }, - "preserved_evidence_identities": [], - "resume_requires": "accepted_repaired_plan_authority", - "original_binding_identity": {"binding_id": "binding-b01", "sha256": "1" * 64}, - "original_baseline_identity": {"head": ZERO_TREE, "tree": ZERO_TREE}, - "preserve_valid_work_and_evidence": True, - "silent_expansion_allowed": False, - } - - -def test_api_001_finding_lifecycle_allows_only_adjudication_after_routing() -> None: - record = finding("implementation_defect") - accepted = transition_review_finding(record, "accepted") - assert accepted.disposition == "accepted" - with pytest.raises(ReviewContractError, match="terminal"): - transition_review_finding({**record, "disposition": "accepted"}, "rejected") - with pytest.raises(ReviewContractError, match="adjudicator"): - transition_review_finding(record, "repair_plan") - - -def test_api_002_requires_independent_direct_accepted_review_and_current_target() -> None: - record = stage_review("plan") - validated = validate_stage_review(record, current_target_identity=record["target_identity"]) - assert validated.stage == "plan" - - coauthored = deepcopy(record) - coauthored["reviewer"]["authorship"] = "present" # type: ignore[index] - with pytest.raises(ReviewContractError, match="authorship"): - validate_stage_review(coauthored) - - changed_target = deepcopy(record["target_identity"]) - changed_target["sha256"] = "1" * 64 # type: ignore[index] - with pytest.raises(ReviewContractError, match="stale"): - validate_stage_review(record, current_target_identity=changed_target) - - -def test_api_002_counts_exactly_three_mandatory_stage_identities() -> None: - reviews = [stage_review(stage) for stage in ("specification", "plan", "integrated_implementation")] - current = {review["stage"]: review["target_identity"] for review in reviews} - assert set(validate_stage_reviews(reviews, current_target_identities=current)) == { - "specification", - "plan", - "integrated_implementation", - } - with pytest.raises(ReviewContractError, match="exactly three"): - validate_stage_reviews(reviews[:2], current_target_identities=current) - - duplicate = deepcopy(reviews[-1]) - duplicate["review_id"] = reviews[0]["review_id"] - with pytest.raises(ReviewContractError, match="unique"): - validate_stage_reviews([*reviews, duplicate], current_target_identities=current) - - -def test_lifecycle_gate_reads_current_artifact_not_claimed_staleness(tmp_path): - import review_runtime - root = tmp_path / ".work-bundle/orchestration" - spec = root / "spec/active/spec.md" - spec.parent.mkdir(parents=True) - spec.write_text("---\nid: spec-test\nversion: 1\nstatus: draft\n---\nRequirement A\n") - with pytest.raises(SystemExit, match="review"): - review_runtime.require_specification_review(tmp_path, spec) - review = stage_review("specification") - review["target_identity"] = review_runtime.artifact_review_identity(spec) - reviews = root / "reviews" - reviews.mkdir() - review = bind_review_receipt(tmp_path, review) - review_runtime.publish_review( - tmp_path, review, current_target_identity=review["target_identity"] - ) - review_runtime.require_specification_review(tmp_path, spec) - spec.write_text(spec.read_text().replace("draft", "verified")) - review_runtime.require_specification_review(tmp_path, spec) - spec.write_text(spec.read_text().replace("Requirement A", "Requirement B")) - with pytest.raises(SystemExit, match="review"): - review_runtime.require_specification_review(tmp_path, spec) - - -def _reviewed_plan_fixture(root, *, provenance=True): - import review_runtime - orch = root / ".work-bundle/orchestration" - metadata = root / ".work-bundle/project.yaml" - metadata.parent.mkdir(parents=True, exist_ok=True) - metadata.write_text( - f"metadata_version: 3\nworkspace_root: {root}\nworkspace_mode: single-repository\n" - ) - spec = orch / "spec/active/spec.md" - plan = orch / "plan/active/plan.md" - for path, text in ((spec, "id: spec-test\nstatus: verified\nrequirements: [{id: REQ-001, requirement: Preserve accepted stage authority.}]"), - (plan, "id: plan-test\nstatus: Planned\nsource_spec: [spec-test]")): - path.parent.mkdir(parents=True, exist_ok=True) - body = ( - "- **REQ-001**: Preserve accepted stage authority.\nOriginal body\n" - if path == spec else "Original body\n" - ) - path.write_text(f"---\n{text}\n---\n{body}") - reviews = orch / "reviews" - for stage, identity in (("specification", review_runtime.artifact_review_identity(spec)), - ("plan", review_runtime.plan_review_identity(root, plan))): - review = stage_review(stage) - review["target_identity"] = identity - if provenance: - review = bind_review_receipt(root, review) - review_runtime.publish_review( - root, review, current_target_identity=review["target_identity"] - ) - else: - reviews.mkdir(exist_ok=True) - (reviews / f"{stage}.json").write_text(json.dumps(review)) - return spec, plan, reviews - - -def _stage_review_path(reviews: Path, stage: str) -> Path: - matches = [ - path - for path in reviews.glob("*.json") - if json.loads(path.read_text()).get("stage") == stage - ] - assert len(matches) == 1, matches - return matches[0] - - -def _write_stage_task(plan: Path, *, review_required: bool = False, command: str = "check-claim") -> Path: - task = plan.parent / "task.md" - task.write_text( - "---\n" - "id: task-test\nplan_id: plan-test\nphase_id: phase-test\ndepends_on: []\n" - "goal: Preserve accepted stage authority.\n" - "source_ids: [REQ-001]\n" - "truth_basis: {purpose: Preserve authority, as_is_evidence: [source.txt], decision_authority: [none-relevant], expected_delta: [stage authority], conflict_status: clear}\n" - "files: {read: [source.txt], write: [], forbidden: [credentials/**]}\n" - "methodology: {primary: tdd, skills: [dev-test-driven-development]}\n" - "allocated_rules: []\n" - "executor_profile: {capability: standard, context_mode: compiled-brief}\n" - f"acceptance_review: {{required: {str(review_required).lower()}}}\n" - "evidence_capability: {result: mapped, reason: Direct command proves the stage claim, invariants: [{id: INV-STAGE, source_ids: [REQ-001], invariant: Accepted authority remains current, boundary: component, oracle: VAL-1, capability_reason: Direct command can falsify drift, freshness: current_task_batch, task_id: task-test, evidence_ids: [VAL-1], closure_result: pending}]}\n" - f"validation: [{{id: VAL-1, kind: process, command: {json.dumps(command)}, invariant_ids: [INV-STAGE], capability_reason: Direct command can falsify drift, proves: REQ-001, expected: passed}}]\n" - "---\nTask\n" - ) - return task - - -@pytest.mark.parametrize("stage", ["plan", "integrated_implementation"]) -def test_target_only_packet_cannot_declare_direct_source(tmp_path, stage): - import reviewer_workspace - import review_runtime - spec, plan, _ = _reviewed_plan_fixture(tmp_path, provenance=False) - spec.write_text(spec.read_text().replace("status: draft", "status: verified")) - protected = tmp_path / "protected" - protected.mkdir() - if stage == "integrated_implementation": - for args in (["init", "-q"], ["config", "user.name", "Test"], ["config", "user.email", "test@example.com"]): - subprocess.run(["git", "-C", str(tmp_path), *args], check=True) - (tmp_path / ".gitignore").write_text(".work-bundle/\nprotected/\n") - (tmp_path / "source.txt").write_text("claim-relevant source") - subprocess.run(["git", "-C", str(tmp_path), "add", "."], check=True) - subprocess.run(["git", "-C", str(tmp_path), "commit", "-qm", "baseline"], check=True) - locator = "control:" + plan.relative_to(tmp_path).as_posix() - packet = reviewer_workspace.build_direct_evidence_packet( - source_root=tmp_path, control_root=tmp_path, protected_roots=[protected], - artifacts=[locator], search_roots=[], validators=[], sentinels=[], network_state="denied", - stage_review_context={"stage": stage, "target_locator": locator, - "target_identity": review_runtime.stage_target_identity(tmp_path, stage, plan, source_root=tmp_path), - "agent_id": "reviewer", "capability": "judgment", "execution_id": "worker", - "evidence_mode": "direct_source"}) - assert packet["stage_review_context"]["evidence_mode"] == "packet_only" - assert packet["stage_evidence_manifest"]["missing"] - packet["stage_review_context"]["evidence_mode"] = "direct_source" - with pytest.raises(review_runtime.ReviewContractError, match="complete reproducible snapshot"): - review_runtime.validate_stage_evidence(tmp_path, packet["stage_review_context"], packet) - - -@pytest.mark.parametrize("removed", [None, "target", "plan_member", "verified_specification", "source_tree", "accepted_task_result"]) -def test_complete_snapshot_gate_rechecks_membership_after_receipt_rehash(tmp_path, removed): - import hashlib - import review_runtime - _, plan, _ = _reviewed_plan_fixture(tmp_path, provenance=False) - task = _write_stage_task(plan, command="test -f source.txt") - handoff = tmp_path / ".work-bundle/orchestration/handoff/executor/active/result.yaml" - handoff.parent.mkdir(parents=True) - handoff.write_text("related: {plan: plan-test, task: task-test}\nvalidation: {commands: [{command: test -f source.txt, result: passed}]}\n") - _write_compact_accepted_result(tmp_path) - for args in (["init", "-q"], ["config", "user.name", "Test"], ["config", "user.email", "test@example.com"]): - subprocess.run(["git", "-C", str(tmp_path), *args], check=True) - (tmp_path / ".gitignore").write_text(".work-bundle/\n") - (tmp_path / "source.txt").write_text("source") - nested = tmp_path / "nested" - nested.mkdir() - (nested / "executable").write_text("#!/bin/sh\nexit 0\n") - (nested / "executable").chmod(0o755) - subprocess.run(["git", "-C", str(tmp_path), "add", "."], check=True) - subprocess.run(["git", "-C", str(tmp_path), "commit", "-qm", "baseline"], check=True) - review = stage_review("integrated_implementation") - review["target_identity"] = review_runtime.stage_target_identity(tmp_path, review["stage"], plan, source_root=tmp_path) - review = bind_review_receipt(tmp_path, review) - assert review["evidence"]["mode"] == "reproducible_snapshot" - review_runtime._validate_reviewer_run(tmp_path, review) - if removed is None: - return - receipt_path = review_runtime.reviewer_runtime_root(tmp_path) / "receipts/reviewer-process" / (review["reviewer_run"]["run_id"] + ".json") - packet_path = receipt_path.with_suffix(".packet.json") - packet = json.loads(packet_path.read_text()) - manifest = packet["stage_evidence_manifest"] - omitted = next(entry["locator"] for entry in manifest["entries"] if entry["role"] == removed) - manifest["entries"] = [entry for entry in manifest["entries"] if entry["locator"] != omitted] - packet["artifacts"] = [entry for entry in packet["artifacts"] if entry["locator"] != omitted] - # Also remove the Git entry: the complete tree identity must still reject it. - manifest["source_tree"] = [entry for entry in manifest["source_tree"] if entry["locator"] != omitted] - packet_path.chmod(0o600) - packet_path.write_text(json.dumps(packet)) - packet_path.chmod(0o400) - receipt = json.loads(receipt_path.read_text()) - receipt["packet_sha256"] = hashlib.sha256(json.dumps(packet, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()).hexdigest() - receipt_path.chmod(0o600) - receipt_path.write_text(json.dumps(receipt)) - receipt_path.chmod(0o400) - review["reviewer_run"]["sha256"] = hashlib.sha256(receipt_path.read_bytes()).hexdigest() - with pytest.raises(review_runtime.ReviewContractError, match="stage evidence"): - review_runtime._validate_reviewer_run(tmp_path, review) - - -def test_plan_snapshot_requires_verified_linked_specification(tmp_path): - import review_runtime - spec, plan, _ = _reviewed_plan_fixture(tmp_path, provenance=False) - spec.write_text(spec.read_text().replace("status: verified", "status: draft")) - _, missing = review_runtime.stage_evidence_requirements(tmp_path, "plan", plan) - assert any(item.startswith("verified_specification:") for item in missing) - - -def test_integrated_snapshot_requires_evidence_for_each_declared_check(tmp_path): - import review_runtime - _, plan, _ = _reviewed_plan_fixture(tmp_path, provenance=False) - task = _write_stage_task(plan) - handoff = tmp_path / ".work-bundle/orchestration/handoff/executor/active/result.yaml" - handoff.parent.mkdir(parents=True) - handoff.write_text("related: {plan: plan-test, task: task-test}\nvalidation: {commands: [{command: unrelated-check, result: passed}]}\n") - _, missing = review_runtime.stage_evidence_requirements(tmp_path, "integrated_implementation", plan) - assert "accepted_task_result_missing:task-test" in missing - - -def _write_compact_accepted_result( - root: Path, *, task: Path | None = None, review_id: str | None = None -) -> Path: - import execution_context - - task = task or _write_stage_task(root / ".work-bundle/orchestration/plan/active/plan.md") - compiled_task = execution_context.static_task_brief(root, task) - binding = root / ".work-bundle/runtime/execution/plan-test/task-test/execution-binding.json" - binding.parent.mkdir(parents=True, exist_ok=True) - baseline = {"head": "a" * 40, "tree": "b" * 40} - owner = {"delegated": True, "owner_kind": "subagent", "agent_id": "/root/task", "run_id": "run-1", "mechanism": "host-native"} - binding_payload = { - "plan_id": "plan-test", "task_id": "task-test", - "workspace_id": "workspace-test", "execution_id": "execution-test", - "repository_id": "repository-test", "execution_path": str(root.resolve()), - "git_identity": {}, "baseline": baseline, - "ownership": {"binding_id": "binding:plan-test:task-test", "original_owner": "task-test"}, - } - accepted_review = { - "required": review_id is not None, "review_id": review_id, - "verdict": "accept" if review_id is not None else None, - } - authority = execution_context._accepted_authority_projection( - compiled_task, binding_payload, accepted_review=accepted_review, owner_identity=owner - ) - knowledge = {"action": "none", "reason": "No durable knowledge delta.", "affected_authority": []} - accepted = { - "schema": "accepted-task-result-v1", "plan_id": "plan-test", "task_id": "task-test", - "binding_id": "binding:plan-test:task-test", "baseline_identity": baseline, - "accepted_source": {"head": "c" * 40, "tree": "d" * 40}, - "authority_projection": authority, "executor_result_digest": "7" * 64, - "validation_evidence_ids": ["observation-val-1"], "review_id": review_id, - "owner_identity": owner, - "knowledge_disposition": knowledge, "accepted_at": "2026-09-08T00:00:00Z", "invalidation": None, - } - accepted["accepted_source"]["state_digest"] = review_runtime.accepted_result_state_digest(accepted) - binding_payload["accepted_result"] = accepted - binding.write_text(json.dumps(binding_payload)) - task_brief = binding.with_name("task-brief.yaml") - task_brief.write_text( - "\n".join(execution_context._dump_yaml({"task_brief": compiled_task})) + "\n" - ) - return binding - - -def test_integrated_snapshot_uses_compact_acceptance_not_handoff_history(tmp_path): - _, plan, _ = _reviewed_plan_fixture(tmp_path, provenance=False) - task = _write_stage_task(plan) - binding = _write_compact_accepted_result(tmp_path, task=task) - misleading = tmp_path / ".work-bundle/orchestration/handoff/executor/active/broken.yaml" - misleading.parent.mkdir(parents=True) - misleading.write_text("invalid:\n badly indented\n historical: true\n") - - required, missing = review_runtime.stage_evidence_requirements( - tmp_path, "integrated_implementation", plan - ) - - assert missing == [] - assert required["control:" + binding.relative_to(tmp_path).as_posix()] == "accepted_task_result" - assert not any("handoff" in locator for locator in required) - - -def test_native_integrated_review_bounds_large_unchanged_tree_to_exact_change_manifest( - tmp_path: Path, monkeypatch, -) -> None: - import reviewer_workspace - - _, plan, _ = _reviewed_plan_fixture(tmp_path, provenance=False) - task = _write_stage_task(plan) - protected = tmp_path / ".work-bundle/protected-test" - protected.mkdir(parents=True, exist_ok=True) - (tmp_path / ".gitignore").write_text(".work-bundle/\n", encoding="utf-8") - (tmp_path / "unchanged-large.txt").write_text("x" * 1_100_000, encoding="utf-8") - (tmp_path / "source.txt").write_text("before\n", encoding="utf-8") - for arguments in ( - ["init", "-q"], - ["config", "user.name", "Test"], - ["config", "user.email", "test@example.invalid"], - ["add", "."], - ["commit", "-qm", "baseline"], - ): - subprocess.run(["git", "-C", str(tmp_path), *arguments], check=True) - baseline = subprocess.check_output( - ["git", "-C", str(tmp_path), "rev-parse", "HEAD"], text=True - ).strip() - (tmp_path / "source.txt").write_text("after\n" * 100_000, encoding="utf-8") - subprocess.run(["git", "-C", str(tmp_path), "add", "source.txt"], check=True) - subprocess.run( - ["git", "-C", str(tmp_path), "commit", "-qm", "claim change"], check=True - ) - endpoint = subprocess.check_output( - ["git", "-C", str(tmp_path), "rev-parse", "HEAD"], text=True - ).strip() - tree = subprocess.check_output( - ["git", "-C", str(tmp_path), "rev-parse", "HEAD^{tree}"], text=True - ).strip() - binding = _write_compact_accepted_result(tmp_path, task=task) - provenance = tmp_path / ".work-bundle/runtime/completion-provenance/completion-provenance-v1.json" - provenance.parent.mkdir(parents=True, exist_ok=True) - provenance.write_text( - json.dumps( - { - "schema": "completion-provenance-v1", - "observations": [ - { - "observation_id": "observation-val-1", - "product_tree": tree, - "command_digest": "1" * 64, - "oracle_digest": "2" * 64, - "result": { - "exit_code": 0, - "stdout_digest": "3" * 64, - "stderr_digest": "4" * 64, - "started_at": "2026-09-09T00:00:00Z", - "completed_at": "2026-09-09T00:00:01Z", - }, - "controller_history": "relevant-raw-history-must-not-reach-model", - }, - { - "observation_id": "observation-unrelated", - "result": {"exit_code": 1}, - "controller_history": "unrelated-history-must-not-reach-model", - }, - ], - } - ), - encoding="utf-8", - ) - handoff = tmp_path / ".work-bundle/orchestration/handoff/executor/active/history.json" - handoff.parent.mkdir(parents=True, exist_ok=True) - handoff.write_text( - json.dumps({"type": "executor-result", "internal": "handoff-must-not-reach-model"}), - encoding="utf-8", - ) - lifecycle = tmp_path / ".work-bundle/runtime/task-lifecycle.json" - lifecycle.write_text( - json.dumps({"schema": "task-lifecycle-v1", "internal": "lifecycle-must-not-reach-model"}), - encoding="utf-8", - ) - exact_diff = tmp_path / ".work-bundle/runtime/integrated-source.diff" - exact_diff.parent.mkdir(parents=True, exist_ok=True) - exact_diff.write_bytes( - subprocess.check_output( - ["git", "-C", str(tmp_path), "diff", "--binary", baseline, endpoint] - ) - ) - change_manifest = tmp_path / ".work-bundle/runtime/change-manifest.json" - change_manifest.parent.mkdir(parents=True, exist_ok=True) - change_manifest.write_text( - json.dumps( - { - "baseline": {"head": baseline}, - "endpoint": {"head": endpoint, "tree": tree}, - "comparison": { - "command": f"git diff --name-status {baseline}..{endpoint}", - "path_count": 1, - "paths": [{"status": "modified", "path": "source.txt"}], - "exact_diff": { - "command": f"git diff --binary {baseline}..{endpoint}", - "locator": "control:.work-bundle/runtime/integrated-source.diff", - "sha256": hashlib.sha256(exact_diff.read_bytes()).hexdigest(), - }, - }, - } - ), - encoding="utf-8", - ) - identity = review_runtime.stage_target_identity( - tmp_path, "integrated_implementation", plan, source_root=tmp_path - ) - locator = "control:" + plan.relative_to(tmp_path).as_posix() - required, missing = review_runtime.stage_evidence_requirements( - tmp_path, "integrated_implementation", plan - ) - assert missing == [] - required.update( - {entry["locator"]: "source_tree" for entry in review_runtime.source_snapshot_entries(tmp_path)} - ) - required["control:" + change_manifest.relative_to(tmp_path).as_posix()] = "change_manifest" - required["control:" + exact_diff.relative_to(tmp_path).as_posix()] = "exact_diff" - required["control:" + handoff.relative_to(tmp_path).as_posix()] = "handoff" - required["control:" + lifecycle.relative_to(tmp_path).as_posix()] = "lifecycle" - packet = reviewer_workspace.build_direct_evidence_packet( - source_root=tmp_path, - control_root=tmp_path, - protected_roots=[protected], - artifacts=list(required), - search_roots=[], - validators=[], - sentinels=[], - network_state="denied", - stage_review_context={ - "stage": "integrated_implementation", - "target_locator": locator, - "target_identity": identity, - "agent_id": "reviewer-large-tree", - "capability": "judgment", - "execution_id": "reviewer-large-tree-run", - "evidence_mode": "direct_source", - }, - ) - exact_bytes = exact_diff.read_bytes() - tampered = deepcopy(packet) - tampered_bytes = b"controller supplied the wrong diff\n" - exact_diff.write_bytes(tampered_bytes) - tampered_artifact = next( - item - for item in tampered["artifacts"] - if item["locator"] == "control:.work-bundle/runtime/integrated-source.diff" - ) - tampered_artifact["sha256"] = hashlib.sha256(tampered_bytes).hexdigest() - tampered_artifact["content_base64"] = base64.b64encode(tampered_bytes).decode("ascii") - with pytest.raises( - reviewer_workspace.ReviewerWorkspaceError, match="CHANGE_MANIFEST_INVALID" - ): - reviewer_workspace.create_reviewer_workspace( - review_runtime.reviewer_runtime_root(tmp_path), "review-wrong-diff", tampered - ) - exact_diff.write_bytes(exact_bytes) - created = reviewer_workspace.create_reviewer_workspace( - review_runtime.reviewer_runtime_root(tmp_path), "review-large-tree", packet - ) - captured: dict[str, str] = {} - - def native_process(_workspace, _argv, request): - captured["request"] = request - events = [ - {"type": "thread.started", "thread_id": "01a0821d-f359-7d60-a9bd-90dd0e006166"}, - {"type": "turn.started"}, - {"type": "item.completed", "item": {"id": "judgment", "type": "agent_message", "text": json.dumps({ - "stage_review": {"target_identity": identity, "verdict": "accepted", "findings": []} - })}}, - {"type": "turn.completed", "usage": {}}, - ] - return subprocess.CompletedProcess([], 0, "\n".join(json.dumps(event) for event in events), "") - - monkeypatch.setattr(reviewer_workspace, "_run_native_process", native_process) - receipt = reviewer_workspace.run_native_reviewer( - Path(str(created["workspace_path"])), - Path(sys.executable), - model="test-model", - review_instructions="Assess the accepted requirements and exact changed source.", - ) - request = json.loads(captured["request"]) - supplied = {item["locator"] for item in request["evidence"]} - metadata = {item["locator"] for item in request["review_input"]["artifacts"]} - product_authority = { - item["locator"] - for item in packet["stage_evidence_manifest"]["entries"] - if item["role"] in {"target", "plan_member", "verified_specification"} - } - assert len(captured["request"]) <= reviewer_workspace.NATIVE_REVIEW_REQUEST_MAX_CHARS - assert product_authority - assert product_authority <= supplied - assert "source:source.txt" not in supplied - assert "source:unchanged-large.txt" not in supplied - assert "control:.work-bundle/runtime/integrated-source.diff" in supplied - supplied_diff = next( - item["content"] - for item in request["evidence"] - if item["locator"] == "control:.work-bundle/runtime/integrated-source.diff" - ) - assert supplied_diff.encode("utf-8") == exact_diff.read_bytes() - binding_locator = "control:" + binding.relative_to(tmp_path).as_posix() - provenance_locator = "control:" + provenance.relative_to(tmp_path).as_posix() - assert binding_locator not in supplied | metadata - assert provenance_locator not in supplied | metadata - assert "control:" + handoff.relative_to(tmp_path).as_posix() not in supplied | metadata - assert "control:" + lifecycle.relative_to(tmp_path).as_posix() not in supplied | metadata - encoded_request = captured["request"] - assert "relevant-raw-history-must-not-reach-model" not in encoded_request - assert "unrelated-history-must-not-reach-model" not in encoded_request - assert "handoff-must-not-reach-model" not in encoded_request - assert "lifecycle-must-not-reach-model" not in encoded_request - product_evidence = request["review_input"]["product_evidence"] - assert [item["task_id"] for item in product_evidence["accepted_results"]] == ["task-test"] - assert [ - item["observation_id"] for item in product_evidence["validation_observations"] - ] == ["observation-val-1"] - assert request["review_input"]["target_identity"]["source_tree"] == tree - assert any( - item["locator"] == "source:unchanged-large.txt" - for item in request["review_input"]["artifacts"] - ) - review_runtime._validate_reviewer_run( - tmp_path, - {**receipt["review_result"], "reviewer_run": receipt["reviewer_run"]}, - ) - - -def test_integrated_snapshot_includes_native_review_when_present_and_rejects_invalid_compact_authority(tmp_path, monkeypatch): - _, plan, _ = _reviewed_plan_fixture(tmp_path, provenance=False) - task = _write_stage_task(plan, review_required=True) - binding = _write_compact_accepted_result(tmp_path, task=task, review_id="review-task-current") - accepted = json.loads(binding.read_text())["accepted_result"] - review = { - **stage_review("plan"), "required": True, "reviewer_independent": True, - "review_id": "review-task-current", "reviewed_head": accepted["accepted_source"]["head"], - "review_mode": "initial", "review_target_kind": "task", "repair_frontier": None, - "review_reset": None, "target_identity": { - "artifact_id": "task-test", "revision": accepted["accepted_source"]["head"], - "sha256": "8" * 64, "source_tree": accepted["accepted_source"]["tree"], - }, "verdict": "accept", - } - review_path = tmp_path / ".work-bundle/orchestration/reviews/review-task-current.json" - review_path.parent.mkdir(parents=True, exist_ok=True) - review_path.write_text(json.dumps(review)) - review_path.chmod(0o444) - monkeypatch.setattr(review_runtime, "_validate_reviewer_run", lambda *_: None) - required, missing = review_runtime.stage_evidence_requirements(tmp_path, "integrated_implementation", plan) - assert missing == [] - assert required["control:" + review_path.relative_to(tmp_path).as_posix()] == "accepted_task_review" - - payload = json.loads(binding.read_text()) - payload["accepted_result"]["accepted_source"]["state_digest"] = "0" * 64 - binding.write_text(json.dumps(payload)) - _, missing = review_runtime.stage_evidence_requirements(tmp_path, "integrated_implementation", plan) - assert "accepted_task_result_invalid:task-test" in missing - - -@pytest.mark.parametrize("mutation", ["task", "scope", "validation", "binding"]) -def test_integrated_snapshot_rejects_self_consistent_accepted_result_after_current_authority_drift( - tmp_path: Path, mutation: str -) -> None: - _, plan, _ = _reviewed_plan_fixture(tmp_path, provenance=False) - task = _write_stage_task(plan) - binding = _write_compact_accepted_result(tmp_path, task=task) - accepted_before = json.loads(binding.read_text())["accepted_result"] - assert accepted_before["accepted_source"]["state_digest"] == review_runtime.accepted_result_state_digest( - accepted_before - ) - - if mutation == "task": - task.write_text(task.read_text().replace("depends_on: []", "depends_on: [task-prior]")) - elif mutation == "scope": - task.write_text(task.read_text().replace("read: [source.txt]", "read: [source.txt, other.txt]")) - elif mutation == "validation": - task.write_text(task.read_text().replace("command: \"check-claim\"", "command: \"changed-check\"")) - else: - payload = json.loads(binding.read_text()) - payload["workspace_id"] = "workspace-changed" - binding.write_text(json.dumps(payload)) - - _, missing = review_runtime.stage_evidence_requirements( - tmp_path, "integrated_implementation", plan - ) - assert "accepted_task_result_invalid:task-test" in missing - - -def test_manually_authored_accepted_review_cannot_advance_lifecycle(tmp_path): - import review_runtime - spec, _, _ = _reviewed_plan_fixture(tmp_path, provenance=False) - # Valid shape and an exact current target are not reviewer execution proof. - with pytest.raises(SystemExit, match="direct immutable authority"): - review_runtime.require_specification_review(tmp_path, spec) - - -@pytest.mark.skipif(sys.platform != "darwin", reason="native sandbox-exec boundary is macOS-only") -def test_native_reviewer_receipt_advances_lifecycle_and_survives_cleanup(tmp_path): - import review_runtime - import reviewer_workspace - import argparse - import specs - spec, _, reviews = _reviewed_plan_fixture(tmp_path, provenance=False) - spec.write_text(spec.read_text().replace("status: verified", "status: draft")) - path = _stage_review_path(reviews, "specification") - record = json.loads(path.read_text()) - bound = bind_review_receipt(tmp_path, record, real_process=True) - path.unlink() - review_runtime.publish_review( - tmp_path, bound, current_target_identity=bound["target_identity"] - ) - runtime = review_runtime.reviewer_runtime_root(tmp_path) - state = json.loads((runtime / ".state" / f"{bound['review_id']}.json").read_text()) - terminal = {"schema": "reviewer-terminal-review-v1", "review_id": bound["review_id"], - "verdict": "accepted", **{key: state[key] for key in ("packet_sha256", "evidence_digest", "sentinel_digest")}} - reviewer_workspace.cleanup_reviewer_workspace(runtime, bound["review_id"], terminal_review=terminal, - source_root=tmp_path, control_root=tmp_path, protected_roots=[tmp_path / ".work-bundle/protected-test"]) - specs.cmd_set_spec_status(argparse.Namespace(project_root=str(tmp_path), id="spec-test", status="verified")) - assert "status: verified" in spec.read_text() - - -@pytest.mark.parametrize("change", ["missing", "digest", "review_id", "target", "result", "mutable", "packet", "profile", "events", "future"]) -def test_stage_receipt_integrity_failures_block_acceptance(tmp_path, change): - import review_runtime - spec, _, _ = _reviewed_plan_fixture(tmp_path, provenance=False) - record = stage_review("specification") - record["target_identity"] = review_runtime.artifact_review_identity(spec) - record = bind_review_receipt(tmp_path, record) - receipt = review_runtime.reviewer_runtime_root(tmp_path) / "receipts/reviewer-process" / f"{record['reviewer_run']['run_id']}.json" - if change == "missing": - receipt.unlink() - elif change == "digest": - record["reviewer_run"]["sha256"] = ZERO_SHA - elif change == "review_id": - record["review_id"] = "forged-review" - elif change == "target": - spec.write_text(spec.read_text().replace("Original body", "different target")) - record["target_identity"] = review_runtime.artifact_review_identity(spec) - elif change == "result": - record["evidence"]["capabilities"].append("forged evidence claim") - elif change == "mutable": - receipt.chmod(0o600) - elif change == "future": - import hashlib - value = json.loads(receipt.read_text()) - value["completed_at"] = "2999-01-01T00:00:00Z" - receipt.chmod(0o600) - receipt.write_text(json.dumps(value)) - receipt.chmod(0o400) - record["reviewer_run"]["sha256"] = hashlib.sha256(receipt.read_bytes()).hexdigest() - else: - suffix = {"packet": ".packet.json", "profile": ".profile.sb", "events": ".events.jsonl"}[change] - receipt.with_suffix(suffix).unlink() - with pytest.raises(review_runtime.ReviewContractError, match="provenance"): - review_runtime.publish_review( - tmp_path, record, current_target_identity=record["target_identity"] - ) - - -@pytest.mark.parametrize("field", ["author_execution_id", "repair_execution_id"]) -def test_known_author_or_repair_execution_cannot_receive_stage_credit(tmp_path, field): - import review_runtime - spec, _, reviews = _reviewed_plan_fixture(tmp_path, provenance=False) - spec.write_text(spec.read_text().replace("status: verified", f"status: draft\n{field}: same-worker")) - record = stage_review("specification") - record["target_identity"] = review_runtime.artifact_review_identity(spec) - record = bind_review_receipt(tmp_path, record, execution_id="same-worker") - with pytest.raises(review_runtime.ReviewContractError, match="overlaps author/repair"): - review_runtime.publish_review( - tmp_path, record, current_target_identity=record["target_identity"] - ) - - -def test_current_plan_execution_binding_excludes_its_worker_from_review(tmp_path): - import review_runtime - _, plan, reviews = _reviewed_plan_fixture(tmp_path) - record = json.loads(_stage_review_path(reviews, "plan").read_text()) - record = bind_review_receipt(tmp_path, record, execution_id="bound-author") - binding = tmp_path / ".work-bundle/runtime/execution/plan-test/task-1/execution-binding.json" - binding.parent.mkdir(parents=True) - binding.write_text(json.dumps({"execution_id": "bound-author"})) - with pytest.raises(review_runtime.ReviewContractError, match="overlaps author/repair"): - review_runtime.publish_review( - tmp_path, record, current_target_identity=record["target_identity"] - ) - - -def test_old_packet_bytes_cannot_be_relabelled_as_current_target(tmp_path): - import review_runtime - import reviewer_workspace - spec, _, reviews = _reviewed_plan_fixture(tmp_path) - record = json.loads(_stage_review_path(reviews, "specification").read_text()) - runtime = review_runtime.reviewer_runtime_root(tmp_path) - workspace = runtime / "reviews" / record["review_id"] - packet = json.loads((workspace / "packet.json").read_text()) - spec.write_text(spec.read_text().replace("Original body", "new requirement")) - packet["stage_review_context"]["target_identity"] = review_runtime.artifact_review_identity(spec) - packet["policy_roots"] = {"source": str(tmp_path), "control": str(tmp_path), - "protected": [str(tmp_path / ".work-bundle/protected-test")]} - with pytest.raises(reviewer_workspace.ReviewerWorkspaceError, match="STAGE_PACKET_STALE"): - reviewer_workspace.create_reviewer_workspace(runtime, "relabelled", packet) - - -def _native_spec_receipt( - tmp_path, monkeypatch, *, observed_events=None, crlf=False, returncode=0, stderr="" -): - import reviewer_workspace - spec, _, reviews = _reviewed_plan_fixture(tmp_path) - record = json.loads(_stage_review_path(reviews, "specification").read_text()) - record.pop("reviewer_run") - workspace = review_runtime.reviewer_runtime_root(tmp_path) / "reviews" / record["review_id"] - previous_packet = json.loads((workspace / "packet.json").read_text()) - if crlf: - spec.write_bytes(spec.read_bytes().replace(b"\n", b"\r\n")) - packet = reviewer_workspace.build_direct_evidence_packet( - source_root=tmp_path, - control_root=tmp_path, - protected_roots=[tmp_path / ".work-bundle/protected-test"], - artifacts=[item["locator"] for item in previous_packet["artifacts"]], - search_roots=[], - validators=[], - sentinels=[], - network_state="denied", - stage_review_context=previous_packet["stage_review_context"], - ) - record["review_id"] += "-native-crlf" if crlf else "-native" - created = reviewer_workspace.create_reviewer_workspace( - review_runtime.reviewer_runtime_root(tmp_path), record["review_id"], packet - ) - workspace = Path(created["workspace_path"]) - host_id = "01a0821d-f359-7d60-a9bd-90dd0e006166" - events = [ - {"type": "thread.started", "thread_id": host_id}, {"type": "turn.started"}, - {"type": "item.completed", "item": {"id": "1", "type": "agent_message", "text": json.dumps({ - "stage_review": {key: record[key] for key in ("target_identity", "verdict", "findings")}})}}, - {"type": "turn.completed", "usage": {}}, - ] - if observed_events is not None: - events = observed_events - monkeypatch.setattr(reviewer_workspace, "_run_native_process", lambda *_: - subprocess.CompletedProcess( - [], returncode, "\n".join(json.dumps(event) for event in events), stderr - )) - receipt = reviewer_workspace.run_native_reviewer(workspace, Path(sys.executable), model="test-model", - review_instructions="Assess supplied specification and return its stage judgment.") - result = {**receipt["review_result"], "reviewer_run": receipt["reviewer_run"]} - return spec, receipt, result - - -def test_native_crlf_evidence_preserves_exact_bytes_through_publication(tmp_path, monkeypatch): - spec, receipt, result = _native_spec_receipt(tmp_path, monkeypatch, crlf=True) - assert b"\r\n" in spec.read_bytes() - request = json.loads(Path(receipt["receipt_path"]).with_suffix(".request.json").read_text()) - locator = "control:" + spec.relative_to(tmp_path).as_posix() - supplied = next(item for item in request["evidence"] if item["locator"] == locator) - assert supplied["content"].encode("utf-8") == spec.read_bytes() - current = review_runtime.artifact_review_identity(spec) - reference = publish_review(tmp_path, result, current_target_identity=current) - loaded, accepted = review_runtime.load_stored_review(tmp_path, reference, current_target_identity=current) - assert loaded == result and accepted.verdict == "accepted" - - -def test_failed_native_admission_retains_actual_unadmitted_diagnostics(tmp_path, monkeypatch): - import reviewer_workspace - events = [{"type": "thread.started", "thread_id": "01a0821d-f359-7d60-a9bd-90dd0e006166"}, - {"type": "turn.started"}, - {"type": "item.completed", "item": {"id": "tool", "type": "command_execution"}}] - with pytest.raises(reviewer_workspace.ReviewerWorkspaceError, match="NATIVE_TRANSCRIPT") as failed: - _native_spec_receipt(tmp_path, monkeypatch, observed_events=events) - diagnostic = Path(failed.value.result["diagnostic_path"]) - assert diagnostic.is_relative_to(review_runtime.reviewer_runtime_root(tmp_path) / "diagnostics") - assert [json.loads(line) for line in (diagnostic / "stdout.jsonl").read_text().splitlines()] == events - assert (diagnostic / "request.json").is_file() - assert (diagnostic / "stderr.txt").is_file() - assert (diagnostic / "launch.json").is_file() - metadata = json.loads((diagnostic / "capture.json").read_text()) - assert metadata["status"] == "captured-unadmitted" - assert metadata["started_at"] <= metadata["completed_at"] - assert {"packet.json", "events.jsonl"} <= set(metadata["artifacts"]) - assert "review_result" not in metadata and "reviewer_run" not in metadata - assert all(not item.stat().st_mode & 0o222 for item in diagnostic.iterdir()) - assert not (review_runtime.reviewer_runtime_root(tmp_path) / "receipts/reviewer-process" / (metadata["run_id"] + ".json")).exists() - - -def test_failed_native_process_cannot_complete_captured_receipt(tmp_path, monkeypatch): - import reviewer_workspace - with pytest.raises(reviewer_workspace.ReviewerWorkspaceError, match="NATIVE_PROCESS_FAILED") as failed: - _native_spec_receipt( - tmp_path, monkeypatch, returncode=23, - stderr="transport terminated before a successful process exit\n", - ) - diagnostic = Path(failed.value.result["diagnostic_path"]) - capture = json.loads((diagnostic / "capture.json").read_text()) - assert capture["exit_code"] == 23 - with pytest.raises(reviewer_workspace.ReviewerWorkspaceError, match="NATIVE_PROCESS_FAILED"): - reviewer_workspace.complete_native_reviewer_capture( - review_runtime.reviewer_runtime_root(tmp_path), capture["run_id"] - ) - assert not ( - review_runtime.reviewer_runtime_root(tmp_path) - / "receipts/reviewer-process" - / f"{capture['run_id']}.json" - ).exists() - - -def test_plugin_absent_native_review_publishes_and_consumes_actual_host_identity(tmp_path, monkeypatch): - spec, receipt, result = _native_spec_receipt(tmp_path, monkeypatch) - assert result["reviewer"]["agent_id"] == receipt["host_run_id"] - assert receipt["isolation"]["mechanism"] == "native-host-read-only" - assert receipt["isolation"]["os_process_isolation"] is False - request = json.loads(Path(receipt["receipt_path"]).with_suffix(".request.json").read_text()) - assert set(request["review_input"]) == {"stage", "target_identity", "artifacts"} - assert "execution_id" not in json.dumps(request["review_input"]) - current = review_runtime.artifact_review_identity(spec) - reference = publish_review(tmp_path, result, current_target_identity=current) - loaded, accepted = review_runtime.load_stored_review(tmp_path, reference, current_target_identity=current) - assert loaded == result - assert accepted.verdict == "accepted" - spec.write_text(spec.read_text().replace("Original body", "Changed obligation")) - with pytest.raises(ReviewContractError, match="current"): - publish_review(tmp_path, result, current_target_identity=review_runtime.artifact_review_identity(spec)) - - -@pytest.mark.parametrize("change", ["isolation", "host_id", "result", "raw_result", "request", "stderr", "argv"]) -def test_native_receipt_rejects_resealed_false_provenance(tmp_path, monkeypatch, change): - import hashlib - _, receipt, result = _native_spec_receipt(tmp_path, monkeypatch) - path = Path(receipt["receipt_path"]) - saved = json.loads(path.read_text()) - if change == "isolation": - saved["isolation"] = {"mechanism": "sandbox-exec", "network": "denied", "write_scope": "scratch"} - elif change == "host_id": - saved["host_run_id"] = "author-alias" - elif change == "result": - result["verdict"] = "blocked" - saved["review_result"] = {key: value for key, value in result.items() if key != "reviewer_run"} - saved["review_result_sha256"] = hashlib.sha256(json.dumps(saved["review_result"], sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()).hexdigest() - else: - suffix = {"raw_result": ".stdout.jsonl", "request": ".request.json", "stderr": ".stderr.txt", "argv": ".launch.json"}[change] - proof = path.with_suffix(suffix) - proof.chmod(0o600) - if change == "raw_result": - proof.write_text(json.dumps({"verdict": "accepted"})) - saved["stdout_sha256"] = hashlib.sha256(proof.read_bytes()).hexdigest() - elif change == "request": - value = json.loads(proof.read_text()) - value["evidence"][0]["content"] += "changed" - proof.write_text(json.dumps(value)) - saved["request_sha256"] = hashlib.sha256(proof.read_bytes()).hexdigest() - elif change == "stderr": - proof.write_text(json.dumps({ - "type": "item.completed", - "item": {"id": "tool", "type": "command_execution"}, - }) + "\n") - saved["stderr_sha256"] = hashlib.sha256(proof.read_bytes()).hexdigest() - else: - value = json.loads(proof.read_text()) - value["argv"].remove("--ignore-user-config") - proof.write_text(json.dumps(value)) - saved["argv_sha256"] = hashlib.sha256(json.dumps(value["argv"], sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()).hexdigest() - proof.chmod(0o400) - path.chmod(0o600) - path.write_text(json.dumps(saved)) - path.chmod(0o400) - result["reviewer_run"]["sha256"] = hashlib.sha256(path.read_bytes()).hexdigest() - with pytest.raises(ReviewContractError, match="provenance"): - review_runtime._validate_reviewer_run(tmp_path, result) - - -@pytest.mark.parametrize("change", ["review_id", "target", "capability", "context_origin", "failed"]) -def test_launcher_does_not_publish_acceptance_for_unbound_worker_output(tmp_path, monkeypatch, change): - import reviewer_workspace - import review_runtime - spec, _, reviews = _reviewed_plan_fixture(tmp_path) - record = json.loads(_stage_review_path(reviews, "specification").read_text()) - workspace = review_runtime.reviewer_runtime_root(tmp_path) / "reviews" / record["review_id"] - record.pop("reviewer_run") - if change == "review_id": - record["review_id"] = "another-review" - elif change == "target": - record["target_identity"]["sha256"] = ZERO_SHA - elif change == "context_origin": - record["reviewer"]["context_origin"] = "direct_source" - elif change == "capability": - record["reviewer"]["capability"] = "standard" - monkeypatch.setattr(reviewer_workspace, "_run_sandboxed_process", - lambda *_: subprocess.CompletedProcess(["worker"], 1 if change == "failed" else 0, json.dumps(record), "")) - if change != "failed": - with pytest.raises(reviewer_workspace.ReviewerWorkspaceError, match="STAGE_OUTPUT_MISMATCH"): - reviewer_workspace.run_sandboxed_reviewer(workspace, ["worker"]) - else: - receipt = reviewer_workspace.run_sandboxed_reviewer(workspace, ["worker"]) - import hashlib - record["reviewer_run"] = {"run_id": receipt["run_id"], "sha256": hashlib.sha256(Path(receipt["receipt_path"]).read_bytes()).hexdigest()} - with pytest.raises(review_runtime.ReviewContractError, match="provenance"): - review_runtime.publish_review( - tmp_path, record, current_target_identity=record["target_identity"] - ) - - -def test_plan_execution_transition_and_binding_reject_stale_plan(tmp_path): - import argparse - import plans - import execution_context - _, plan, reviews = _reviewed_plan_fixture(tmp_path) - args = argparse.Namespace(project_root=str(tmp_path), id="plan-test", status="In progress") - plans.cmd_set_plan_status(args) - plan.write_text(plan.read_text().replace("Original body", "Changed obligation")) - with pytest.raises(SystemExit, match="review"): - plans.cmd_set_plan_status(args) - with pytest.raises(SystemExit, match="review"): - execution_context.create_or_load_task_execution_binding(control_root=tmp_path, - plan_id="plan-test", task_id="task-test", workspace_id="ws", execution_id="exec", - repository_id="repo", runtime_root=tmp_path / "runtime") - assert not (tmp_path / "runtime").exists() - - -@pytest.mark.parametrize("kind,status", [("spec", "verified"), ("plan", "In progress"), ("plan", "Completed")]) -def test_write_cannot_bypass_stage_gate_with_embedded_status(tmp_path, kind, status): - import argparse - import specs - import plans - content = tmp_path / "input.md" - content.write_text(f"---\nid: artifact-test\nstatus: {status}\n---\nBody\n") - args = argparse.Namespace(project_root=str(tmp_path), content_file=str(content), - id="artifact-test", title="Test", purpose="upgrade", component="test", version="1", - filename="artifact.md", status="draft" if kind == "spec" else "Planned") - with pytest.raises(SystemExit, match="review|source_spec"): - (specs.cmd_write_spec if kind == "spec" else plans.cmd_write_plan)(args) - assert not (tmp_path / f".work-bundle/orchestration/{kind}/active/artifact.md").exists() - - -def test_plan_member_edit_and_forged_fresh_flag_cannot_reuse_review(tmp_path): - import review_runtime - _, plan, reviews = _reviewed_plan_fixture(tmp_path) - task = plan.parent / "plan-test/phase-1/task-1.md" - task.parent.mkdir(parents=True) - task.write_text("---\nid: task-1\nplan_id: plan-test\n---\nNew command\n") - with pytest.raises(SystemExit, match="plan review"): - review_runtime.require_plan_reviews(tmp_path, plan) - records = [stage_review(stage) for stage in ("specification", "plan", "integrated_implementation")] - current = {item["stage"]: deepcopy(item["target_identity"]) for item in records} - current["plan"]["sha256"] = "a" * 64 - with pytest.raises(ReviewContractError, match="exactly three"): - validate_stage_reviews(records, current_target_identities=current) - - -def test_new_spec_review_does_not_refresh_old_plan_review(tmp_path): - import review_runtime - spec, plan, reviews = _reviewed_plan_fixture(tmp_path) - spec.write_text(spec.read_text().replace("Original body", "Changed requirement")) - replacement = stage_review("specification") - replacement["target_identity"] = review_runtime.artifact_review_identity(spec) - replacement = bind_review_receipt(tmp_path, replacement) - review_runtime.publish_review( - tmp_path, replacement, current_target_identity=replacement["target_identity"] - ) - with pytest.raises(SystemExit, match="plan review"): - review_runtime.require_plan_reviews(tmp_path, plan) - - -@pytest.mark.parametrize("mode,context,accepted", [ - ("direct_source", "direct_source", True), ("direct", "direct_source", True), - ("reproducible_snapshot", "reproducible_snapshot", True), - ("packet_only", "packet_only", False), ("constrained_direct", "direct_source", False), - ("direct_source", "carried_summary", False), -]) -def test_evidence_mode_schema_and_runtime_agree(mode, context, accepted): - jsonschema = pytest.importorskip("jsonschema") - record = stage_review("plan") - record["evidence"]["mode"] = mode - record["reviewer"]["context_origin"] = context - if mode == "reproducible_snapshot": - record["evidence"]["artifacts"] = [{"path": "snapshot.json", "sha256": ZERO_SHA}] - schema = json.loads((REPO_ROOT / "references/assets/orchestration/contract/stage-review-v1.schema.json").read_text()) - if accepted: - validate_stage_review(record) - jsonschema.validate(record, schema) - else: - with pytest.raises(ReviewContractError): - validate_stage_review(record) - with pytest.raises(jsonschema.ValidationError): - jsonschema.validate(record, schema) - - -@pytest.mark.parametrize("transition", ["Completed", "archive"]) -def test_final_transition_binds_current_source_tree(tmp_path, monkeypatch, transition): - import argparse - import plans - import review_runtime - _, plan, reviews = _reviewed_plan_fixture(tmp_path) - def git(*args): - return subprocess.check_output(["git", "-C", str(tmp_path), *args], text=True).strip() - git("init", "-q") - git("config", "user.name", "Test") - git("config", "user.email", "test@example.com") - (tmp_path / ".gitignore").write_text(".work-bundle/\n") - source = tmp_path / "source.txt" - source.write_text("A") - git("add", ".") - git("commit", "-qm", "baseline") - args = argparse.Namespace(project_root=str(tmp_path), id="plan-test", status="Completed") - run = (lambda: plans.cmd_archive_plan(args)) if transition == "archive" else (lambda: plans.cmd_set_plan_status(args)) - # Only downstream task/knowledge checks are isolated; stage admission is real. - monkeypatch.setattr(plans, "_validated_plan_task_handoffs", lambda *_: []) - monkeypatch.setattr(plans, "_assert_archive_knowledge_gate", lambda *_: None) - monkeypatch.setattr(plans, "_assert_archive_plan_acceptance", lambda *_: None) - with pytest.raises(SystemExit, match="integrated_implementation"): - run() - review = stage_review("integrated_implementation") - review["target_identity"] = dict(review_runtime.plan_review_identity(tmp_path, plan), - source_tree=git("rev-parse", "HEAD^{tree}")) - review = bind_review_receipt(tmp_path, review) - review_runtime.publish_review( - tmp_path, review, current_target_identity=review["target_identity"] - ) - source.write_text("B") - with pytest.raises(SystemExit, match="clean"): - run() - git("add", "source.txt") - git("commit", "-qm", "changed source") - with pytest.raises(SystemExit, match="integrated_implementation"): - run() - review["target_identity"]["source_tree"] = git("rev-parse", "HEAD^{tree}") - review = bind_review_receipt(tmp_path, review) - review_runtime.publish_review( - tmp_path, review, current_target_identity=review["target_identity"] - ) - run() - - -def test_api_002_preserves_but_does_not_count_stale_accepted_review() -> None: - stale = stage_review("plan") - stale["review_id"] = "review-plan-old" - stale["staleness"] = {"is_stale": True, "reason": "target digest changed", "supersedes": None} - assert validate_stage_review(stale).staleness["is_stale"] is True - reviews = [stage_review(stage) for stage in ("specification", "plan", "integrated_implementation")] - countable = validate_stage_reviews([stale, *reviews], current_target_identities={review["stage"]: review["target_identity"] for review in reviews}) - assert countable["plan"].review_id == "review-plan" - - -def test_validate_contract_and_migration_stop_cli(tmp_path: Path) -> None: - instance = tmp_path / "finding.json" - instance.write_text(json.dumps(finding()), encoding="utf-8") - schema = REPO_ROOT / "references/assets/orchestration/contract/review-finding-v1.schema.json" - direct = subprocess.run( - [ - sys.executable, - str(ORCHESTRATION / "review_runtime.py"), - "validate-contract", - "--schema", - str(schema), - "--definition", - "reviewFinding", - "--instance", - str(instance), - ], - text=True, - capture_output=True, - check=False, - ) - assert direct.returncode == 0, direct.stdout + direct.stderr - assert json.loads(direct.stdout)["status"] == "passed" - - handoff = tmp_path / "handoff.json" - handoff.write_text( - json.dumps({"issue": "WOR-107", "excluded_work": ["WOR-66", "WOR-79", "WOR-107", "work-bundle-mcp mutation"]}), - encoding="utf-8", - ) - dispatched = subprocess.run( - [ - sys.executable, - str(REPO_ROOT / "scripts/wb.py"), - "assert-migration-stop", - "--instance", - str(handoff), - "--required-excluded", - "WOR-66", - "WOR-79", - "WOR-107", - "work-bundle-mcp mutation", - ], - text=True, - capture_output=True, - check=False, - ) - assert dispatched.returncode == 0, dispatched.stdout + dispatched.stderr - assert json.loads(dispatched.stdout)["status"] == "passed" diff --git a/tests/test_orchestration_semantic_plan_identity.py b/tests/test_orchestration_semantic_plan_identity.py index 5be09e7..589f8df 100644 --- a/tests/test_orchestration_semantic_plan_identity.py +++ b/tests/test_orchestration_semantic_plan_identity.py @@ -1,13 +1,10 @@ from __future__ import annotations -import hashlib -import json -import shutil import sys from copy import deepcopy from pathlib import Path -import pytest +import yaml REPO_ROOT = Path(__file__).resolve().parents[1] @@ -15,211 +12,89 @@ sys.path.insert(0, str(ORCHESTRATION)) import review_runtime # noqa: E402 -from artifact_inputs import _read_structured, _resolve_spec_paths # noqa: E402 - - -def _plan_graph(root: Path) -> tuple[Path, Path, Path]: - spec = root / ".work-bundle/orchestration/spec/active/spec-test.md" - plan_root = root / ".work-bundle/orchestration/plan/active" - plan = plan_root / "plan-test.md" - phase = plan_root / "plan-test/phase-001.md" - task = plan_root / "plan-test/phase-001/task-001.md" - task.parent.mkdir(parents=True) - spec.parent.mkdir(parents=True) - spec.write_text( - "---\nid: spec-test\nversion: 1\nstatus: verified\n---\n\n# Specification\n", - encoding="utf-8", - ) - plan.write_text( - "---\nid: plan-test\nversion: 1\ndate_created: 2026-09-08\n" - "last_updated: 2026-09-08\nstatus: Planned\n" - "source_spec: [.work-bundle/orchestration/spec/active/spec-test.md]\n" - "---\n\n# Plan\n", - encoding="utf-8", - ) - phase.write_text( - "---\nid: phase-001\nplan_id: plan-test\ndate_created: 2026-09-08\n" - "last_updated: 2026-09-08\nstatus: Planned\n" - "task_index:\n - {id: task-001, status: Planned}\n---\n\n# Phase\n", - encoding="utf-8", - ) - task.write_text( - "---\nid: task-001\nplan_id: plan-test\nphase_id: phase-001\n" - "date_created: 2026-09-08\nlast_updated: 2026-09-08\n" - "status: Planned\ndepends_on: []\nsource_ids: [REQ-001]\n" - "target_files: [implementation.py]\nvalidation: [{id: VAL-001, kind: process}]\n" - "acceptance_review: {required: true, verdict: pending, reviewed_head: '', findings: []}\n" - "---\n\n# Task\n", - encoding="utf-8", - ) - return plan, phase, task - - -def _legacy_plan_identity(root: Path, plan: Path) -> dict[str, object]: - plan_root = root / ".work-bundle/orchestration/plan" - identity = review_runtime.artifact_review_identity(plan) - members = {str(plan.relative_to(plan_root)): identity["sha256"]} - for path in sorted(plan_root.rglob("*.md")): - if path == plan: - continue - data, _ = _read_structured(path) - if str(data.get("plan_id", "")) == identity["artifact_id"]: - members[str(path.relative_to(plan_root))] = review_runtime.artifact_review_identity(path)[ - "sha256" - ] - plan_data = _read_structured(plan)[0] - specifications = [ - review_runtime.artifact_review_identity(path) - for path in _resolve_spec_paths(root, {}, plan_data) - ] - identity["sha256"] = hashlib.sha256( - json.dumps({"members": members, "specifications": specifications}, sort_keys=True).encode() - ).hexdigest() - return identity - - -def test_legacy_semantic_projector_preserves_accepted_baseline_identity(tmp_path: Path) -> None: - plan, _phase, _task = _plan_graph(tmp_path) - - assert review_runtime.legacy_plan_review_identity(tmp_path, plan) == _legacy_plan_identity( - tmp_path, plan - ) - assert review_runtime.plan_review_identity(tmp_path, plan) != _legacy_plan_identity( - tmp_path, plan - ) - - -def test_legacy_identity_preserves_missing_knowledge_closure_compatibility(tmp_path: Path) -> None: - plan, _phase, _task = _plan_graph(tmp_path) - plan.write_text( - plan.read_text() - + "\n## Knowledge Base Update Carry Forward\n\n" - "- Disposition: required\n- Closure return: missing\n" - "- Source: accepted specification\n- Review Gate: resolve before archive\n", - encoding="utf-8", - ) - - assert review_runtime.legacy_plan_review_identity(tmp_path, plan) == _legacy_plan_identity( - tmp_path, plan - ) +from artifact_store import transition_artifact, write_artifact # noqa: E402 +from test_orchestration_plans import CATALOG, _create_tree, workspace # noqa: E402 -def test_active_to_archived_rotation_preserves_semantic_plan_identity(tmp_path: Path) -> None: - plan, _phase, _task = _plan_graph(tmp_path) - original = review_runtime.plan_review_identity(tmp_path, plan) - archived = plan.parents[1] / "archived" - archived.mkdir() - archived_plan = archived / plan.name - archived_graph = archived / plan.stem +def _rewrite(path: Path, family: str, root: Path, mutate) -> None: + data = yaml.safe_load(path.read_text()) + mutate(data) + bindings = {"source_spec": data["source_spec_id"]} if family == "root-plan" else {"plan": data["plan_id"]} + if family == "task": + bindings["phase"] = data["phase_id"] + write_artifact(CATALOG, family, {"workspace_root": root}, data, state="active", bindings=bindings) - shutil.move(str(plan), archived_plan) - shutil.move(str(plan.with_suffix("")), archived_graph) - - assert review_runtime.plan_review_identity(tmp_path, archived_plan) == original +def test_qualification_and_dates_do_not_change_identity( + workspace: Path, tmp_path: Path, +) -> None: + plan, _phase, _task = _create_tree(workspace, tmp_path) + original = deepcopy(review_runtime.plan_review_identity(workspace, plan)) + _rewrite( + plan, "root-plan", workspace, + lambda data: data.update(status="verified", last_updated="2099-02-02"), + ) + assert review_runtime.plan_review_identity(workspace, plan) == original -def test_progress_and_append_only_evidence_do_not_change_semantic_plan_identity(tmp_path: Path) -> None: - plan, phase, task = _plan_graph(tmp_path) - original = review_runtime.plan_review_identity(tmp_path, plan) - plan.write_text( - plan.read_text() - .replace("status: Planned", "status: In progress") - .replace("last_updated: 2026-09-08", "last_updated: 2026-09-09") - ) - phase.write_text( - phase.read_text() - .replace("status: Planned", "status: Completed") - .replace("last_updated: 2026-09-08", "last_updated: 2026-09-09") - .replace("---\n\n# Phase", "accepted_result_references: [result-phase-001]\n---\n\n# Phase") - ) - task.write_text( - task.read_text() - .replace("status: Planned", "status: Completed") - .replace("last_updated: 2026-09-08", "last_updated: 2026-09-09") - .replace( - "acceptance_review: {required: true, verdict: pending, reviewed_head: '', findings: []}", - "acceptance_review: {required: true, verdict: accept, reviewed_head: abc, findings: []}", - ) - .replace( - "---\n\n# Task", - "accepted_result: result-task-001\nevidence_references: [VAL-001-observation]\n---\n\n# Task", - ) +def test_every_substantive_yaml_field_changes_plan_tree_identity( + workspace: Path, tmp_path: Path, +) -> None: + plan, _phase, task = _create_tree(workspace, tmp_path) + original = review_runtime.plan_review_identity(workspace, plan) + _rewrite( + task, "task", workspace, + lambda data: data["steps"].append("Run the focused regression test."), ) - assert review_runtime.plan_review_identity(tmp_path, plan) == original + assert review_runtime.plan_review_identity(workspace, plan) != original -@pytest.mark.parametrize( - ("heading", "label"), - [ - ("## 2.1 Knowledge Base Update Carry Forward", "**Closure return**"), - ("## Knowledge Base Update Carry Forward", "Closure return"), - ], -) -def test_knowledge_closure_lifecycle_change_is_v2_and_legacy_compatible( - tmp_path: Path, heading: str, label: str +def test_noncanonical_yaml_candidate_does_not_change_plan_tree_identity( + workspace: Path, tmp_path: Path, ) -> None: - plan, _phase, _task = _plan_graph(tmp_path) - plan.write_text( - plan.read_text() - + f"\n{heading}\n\n- **Disposition**: required\n- {label}: missing\n", + plan, _phase, _task = _create_tree(workspace, tmp_path) + original = review_runtime.plan_review_identity(workspace, plan) + stray = plan.parent / "stray.yaml" + stray.write_text( + yaml.safe_dump({"plan_id": "plan-stage4", "invented": "not canonical"}), encoding="utf-8", ) - original = review_runtime.plan_review_identity(tmp_path, plan) - legacy = review_runtime.legacy_plan_review_identity(tmp_path, plan) - plan.write_text(plan.read_text().replace(f"{label}: missing", f"{label}: completed")) + assert review_runtime.plan_review_identity(workspace, plan) == original - assert review_runtime.plan_review_identity(tmp_path, plan) == original - assert review_runtime.legacy_plan_review_identity(tmp_path, plan) == legacy - -@pytest.mark.parametrize( - ("before", "after"), - [ - ("Disposition: not-needed", "Disposition: required"), - ("Source: no durable update", "Source: accepted task findings"), - ("Review Gate: no follow-up", "Review Gate: persist accepted findings"), - ], -) -def test_substantive_knowledge_change_invalidates_plan_review_identity( - tmp_path: Path, before: str, after: str +def test_nested_acceptance_allocation_remains_substantive( + workspace: Path, tmp_path: Path, ) -> None: - plan, _phase, _task = _plan_graph(tmp_path) - plan.write_text( - plan.read_text() - + "\n## Knowledge Base Update Carry Forward\n\n" - "- Disposition: not-needed\n- Closure return: missing\n" - "- Source: no durable update\n- Review Gate: no follow-up\n", - encoding="utf-8", + plan, _phase, task = _create_tree(workspace, tmp_path) + original = review_runtime.plan_review_identity(workspace, plan) + _rewrite( + task, "task", workspace, + lambda data: data["acceptance_review"].update(required=True, reviewer_independent=True), ) - original = review_runtime.plan_review_identity(tmp_path, plan) - plan.write_text( - plan.read_text().replace(before, after), - encoding="utf-8", - ) + assert review_runtime.plan_review_identity(workspace, plan) != original - assert review_runtime.plan_review_identity(tmp_path, plan) != original - - -@pytest.mark.parametrize( - ("field", "before", "after"), - [ - ("dependency", "depends_on: []", "depends_on: [task-000]"), - ("source authority", "source_ids: [REQ-001]", "source_ids: [REQ-002]"), - ("scope", "target_files: [implementation.py]", "target_files: [other.py]"), - ("validation", "id: VAL-001", "id: VAL-002"), - ("review allocation", "required: true", "required: false"), - ], -) -def test_executable_task_contract_changes_semantic_plan_identity( - tmp_path: Path, field: str, before: str, after: str -) -> None: - plan, _phase, task = _plan_graph(tmp_path) - original = deepcopy(review_runtime.plan_review_identity(tmp_path, plan)) - task.write_text(task.read_text().replace(before, after), encoding="utf-8") +def test_active_to_archived_tree_transition_preserves_semantic_identity( + workspace: Path, tmp_path: Path, +) -> None: + plan, _phase, _task = _create_tree(workspace, tmp_path) + original = review_runtime.plan_review_identity(workspace, plan) + anchors = {"workspace_root": workspace} + transition_artifact( + CATALOG, "task", anchors, identity="task-stage4", current_state="active", + target_state="archived", bindings={"plan": "plan-stage4", "phase": "phase-stage4"}, + ) + transition_artifact( + CATALOG, "phase", anchors, identity="phase-stage4", current_state="active", + target_state="archived", bindings={"plan": "plan-stage4"}, + ) + transition_artifact( + CATALOG, "root-plan", anchors, identity="plan-stage4", current_state="active", + target_state="archived", bindings={"source_spec": "spec-stage4"}, + ) + archived = workspace / ".work-bundle/orchestration/plan/archived/plan-stage4.plan.yaml" - assert review_runtime.plan_review_identity(tmp_path, plan) != original, field + assert review_runtime.plan_review_identity(workspace, archived) == original diff --git a/tests/test_orchestration_skill_rule_boundary.py b/tests/test_orchestration_skill_rule_boundary.py index 7d8a565..e375f99 100644 --- a/tests/test_orchestration_skill_rule_boundary.py +++ b/tests/test_orchestration_skill_rule_boundary.py @@ -1,433 +1,117 @@ -import re +from __future__ import annotations + from pathlib import Path +import re + +import yaml REPO_ROOT = Path(__file__).resolve().parents[1] +STAGE5_SKILLS = ("orch-execute-plan", "orch-create-handoff", "orch-review-plan") def read(path: str) -> str: return (REPO_ROOT / path).read_text(encoding="utf-8") -def test_runtime_rule_paths_exist_without_duplicated_loading_algorithm() -> None: - for path in sorted(REPO_ROOT.glob("skills/orch-*/SKILL.md")): - text = path.read_text(encoding="utf-8") +def test_stage5_skills_reference_existing_rules_without_local_loading_algorithm() -> None: + for name in STAGE5_SKILLS: + text = read(f"skills/{name}/SKILL.md") + assert "## Rule Loading (mandatory)" not in text for rule_path in re.findall(r"`(rules/orchestration/[^`]+)`", text): - assert (REPO_ROOT / rule_path).is_file(), f"{path.name}: {rule_path}" - if path.name == "SKILL.md" and path.parent.name in { - "orch-create-specification", - "orch-create-implementation-plan", - "orch-execute-plan", - "orch-review-plan", - }: - assert "## Rule Loading (mandatory)" not in text - assert "Central `AGENTS.md` owns rule discovery and loading" in text - - -def test_obsolete_role_context_surface_is_removed() -> None: - for relative in [ - "skills/wb-select-role-context/SKILL.md", - "rules/role-context.md", - "references/wb-select-role-context-contract.yaml", - "scripts/work-bundle/role_context.py", - ]: - assert not (REPO_ROOT / relative).exists(), relative + assert (REPO_ROOT / rule_path).is_file(), f"{name}: {rule_path}" - for relative in [ - "skills/orch-create-document/SKILL.md", - "skills/orch-create-handoff/SKILL.md", - "references/assets/keep-summarizing/workflow.md", - "scripts/work-bundle/README.md", - "scripts/work-bundle/core.py", - "scripts/work-bundle/dispatcher.py", - "scripts/work-bundle/metadata_profile.py", - "scripts/work-bundle/project.py", - "rules/index.yaml", - ]: - text = read(relative).lower() - assert "wb-select-role-context" not in text, relative - assert "role-context" not in text, relative - assert "role context" not in text, relative - -def test_specification_uses_compact_semantic_convergence_and_workspace_policy() -> None: - text = read("skills/orch-create-specification/SKILL.md") - for token in [ - "dev-semantic-convergence", - "user-purpose coverage", - "authority and evidence support", - "requirement, constraint, and open-question consistency", - "impact radius", - "Knowledge Base Update disposition", - "execution-workspace policy", - "semantic_loop:", - "Quality gate: verified|blocked", - "Initial User Purpose Evidence", - "Design Interrogation", - "impact-decision view", - "accepted | excluded | blocking", - "none_relevant", - "stopping_reason", - "projects_to", - "excellence-applicability view", - "no_material_opportunity", - "material_opportunities", - "accepted | rejected | deferred | not_material", - ]: - assert token in text - assert "Extra evidence loop" not in text - - -def test_planner_allocates_methodology_capability_and_bounded_context() -> None: - text = read("skills/orch-create-implementation-plan/SKILL.md") - for token in [ - "expected total orchestration cost", - "independently falsifiable", - "bounded failure radius", - "coherent mechanical increment", - "source-ID coverage", - "dev-systematic-debugging", - "dev-test-driven-development", - "dev-code-review", - "mechanical", - "standard", - "judgment", - "context_mode: compiled-brief", - "acceptance_review:", - "acceptance_review.required: false", - "Do not infer", - "soft applicability prose", - "after_failed_repairs: 2", - "common contract group", - "post-barrier convergence task", - "Truth Basis", - "earliest ordinary task", - "cheaply falsify", - "Do not add a risk score", - "EXC-", - "deferred", - "executor briefs", - ]: - assert token in text +def test_stage5_skills_end_with_observable_self_checks() -> None: + for name in STAGE5_SKILLS: + text = read(f"skills/{name}/SKILL.md") + self_check = text.rsplit("## Self-check", 1)[-1] + assert text.count("## Self-check") == 1 + assert self_check.count("- [ ]") >= 4 + assert len(self_check) < 2200 -def test_execute_skill_uses_compiler_independent_review_and_typed_blockers() -> None: +def test_execute_skill_separates_executor_facts_from_reviewer_verdict() -> None: text = read("skills/orch-execute-plan/SKILL.md") - for token in [ - "## Execution Constraints (skill-owned)", - "## Scheduler-Owned Constraints", - "## Executor-Owned Constraints", - "build-task-brief", - "validate-executor-result", - "build-review-package", - "dev-code-review", - "The scheduler does not perform code-quality review", - "every implementation and repair task subagent-owned", - "TaskOwnershipScheduler", - "TaskOwnershipScheduler.validate_acceptance", - "there is no controller or single-agent fallback", - "must not implement or repair task write scope", - "one scoped rereview", - "review_required: true", - "validate initial executor facts without demanding or embedding the future review verdict", - "stored required-review authority", - "context-blocked", - "repository-blocked", - "decision-blocked", - "validation-blocked", - "review-blocked", - "knowledge-blocked", - "workspace-blocked", - "no-index", - "no-retrieval", - "Truth Basis", - "knowledge disposition", - "task-local evidence", - "review owns", - ]: + for token in ( + "compiled Truth Basis", + "path-sorted changed-path manifest", + "executor-result-v1", + "distinct reviewer", + "green tests do not substitute", + "No receipt, history replay", + ): assert token in text + assert "does not accept the product" in text + + +def test_handoff_skill_owns_only_canonical_factual_continuation() -> None: + text = read("skills/orch-create-handoff/SKILL.md") + for token in ( + "executor-result-v1", + "exact plan/task bindings", + "Validate the full semantic input", + "immutable YAML artifact", + "derived index", + "lightweight integrity checks", + "partial effect", + ): + assert token in text + assert "do not create orchestration handoffs" in text -def test_task_ownership_contract_is_provider_neutral_and_not_visibility_specific() -> None: - for relative in [ - "skills/orch-create-handoff/SKILL.md", - "rules/orchestration/orch-handoff-required.md", - "references/assets/orchestration/contract/handoff-executor-result-v1.md", - ]: - text = read(relative) - for token in ["delegation_evidence", "owner_kind", "agent", "run", "mechanism"]: - assert token in text, f"{relative}: {token}" - for retired in ["visible_reference", "internal_spawn_used_for_task_delegation", "single-agent-fallback"]: - assert retired not in text, f"{relative}: {retired}" - - -def test_final_review_is_workflow_audit_not_code_review() -> None: +def test_review_skill_separates_product_review_from_compact_final_audit() -> None: text = read("skills/orch-review-plan/SKILL.md") - for token in [ - "workflow audit", - "Independent `dev-code-review` owns task-scoped implementation quality", - "compiled Truth Basis", - "AUTH constraints", - "universal task-review evidence", - "implementation-review agent", - "explicitly required", - "review-blocked", - "knowledge-blocked", - "repository-blocked", - "workspace-blocked", - "repair plan only", - "repair specification", - "Do not broadly inspect source", - "Do not create a repair specification for every failed gate", - "aggregate accepted task dispositions", - "accepted `update`, `supersede`, or `reclassify`", - "rejected task dispositions", - "archive remains blocked", - "RuntimeVerificationClassificationV1", - "invariant_trace", - "negative_evidence", - "owning_repair", - "execution_introduced_bug", - "implementation_gap", - "new_feature", - "uncovered_fixture", - "evidence_capability", - "INV/VAL", - "incapable green", - "pre-closure oracle-capability check", - "no_validation_bearing_obligation", - "none_relevant", - ]: + for token in ( + "exact frozen commit or worktree candidate", + "Passing tests cannot hide omitted behavior", + "implementation-review-v1", + "accepted-task-result-v1", + "final-workflow-review-v1", + "Do not reread source for code quality or repeat implementation review", + ): assert token in text -def test_review_enforces_evidence_capability_before_closure() -> None: - for relative in [ - "skills/orch-review-plan/SKILL.md", - "rules/orchestration/orch-review-completion.md", - ]: - text = read(relative) - for token in [ - "evidence_capability", - "INV/VAL", - "incapable green", - "wrong-boundary", - "harness-observed", - "no_validation_bearing_obligation", - "none_relevant", - "pre-closure oracle-capability check", - "task repair", - "plan repair", - "specification repair", - "RuntimeVerificationClassificationV1", - "WOR-59 G9 remains the unchanged post-execution classifier", - "universal browser, E2E, production, or runtime gate", - ]: - assert token in text, f"{relative}: {token}" - - -def test_runtime_verification_classification_contract_routes_the_first_broken_artifact() -> None: - for relative in [ - "skills/orch-review-plan/SKILL.md", +def test_active_rules_keep_semantic_judgment_agent_owned_and_non_recursive() -> None: + paths = ( + "rules/orchestration/orch-handoff-required.md", "rules/orchestration/orch-review-completion.md", - ]: - text = read(relative) - for token in [ - "RuntimeVerificationClassificationV1", - "original user request", - "accepted specification", - "invariant_trace", - "negative_evidence", - "execution_introduced_bug", - "implementation_gap", - "new_feature", - "uncovered_fixture", - "owning_repair", - "presentation", - "wb-defect-evaluation", - "work-bundle-scoped or mixed", - "same-scope specification-owned", - "task repair", - "plan repair", - "specification repair", - ]: - assert token in text, f"{relative}: {token}" - assert "unit tests alone" in text - assert "must not decide the semantic class" in text - - -def test_durable_owners_state_current_acceptance_and_review_semantics() -> None: - required = { - "rules/lifecycle-authority.md": [ - "compact accepted result", - "acceptance once", - "historical handoff chains", - "current harness observation", - ], - "rules/repository-boundary.md": [ - "issue-run artifacts", - "workspace control plane", - "exact baseline and endpoint", - "live `HEAD`", - "historical cleanup", - ], - "rules/work-bundle/wb-defect-evaluation.md": [ - "causal class", - "first owning layer", - "before responding", - ], - "rules/orchestration/orch-artifact-authoring.md": [ - "canonical semantic plan projection", - "static task admission", - "status-only", - ], - "rules/orchestration/orch-orchestration-boundary.md": [ - "compact accepted result", - "transient acceptance evidence", - "historical handoff chains", - ], - "rules/orchestration/orch-review-completion.md": [ - "reviewer infrastructure or provider failure", - "publication-only/control resume", - "finding-scoped repair review", - "previous finding/evidence frontier", - ], - "skills/orch-create-implementation-plan/SKILL.md": [ - "canonical semantic plan projection", - "static task admission", - "status-only or append-only evidence", - ], - "skills/orch-execute-plan/SKILL.md": [ - "compact accepted result", - "acceptance once", - "preserve the immutable package", - "affected frontier", - ], - "skills/orch-review-plan/SKILL.md": [ - "compact accepted results", - "historical handoff chains", - "current harness observations", - ], - "references/assets/orchestration/contract/plan-v1.md": [ - "canonical semantic plan projection", - "static task admission", - "status-only or append-only evidence", - ], - } - for relative, tokens in required.items(): - text = read(relative) - for token in tokens: - assert token in text, f"{relative}: {token}" - - -def test_workflow_makes_task_review_optional_on_the_chain() -> None: - text = read("references/assets/orchestration/workflow.md") - for token in [ - "optional task review", - "validate-executor-result", - "optional task review when compiled review_required: true", - "accepted-result materialization joins executor facts, observations, and stored review authority", - ]: - assert token in text - assert "-> independent dev-code-review" not in text - - -def test_orchestration_doctor_uses_optional_review_anchors() -> None: - text = (REPO_ROOT / "scripts/orchestration/doctor.py").read_text(encoding="utf-8") - - assert '"optional task review"' in text - assert '"acceptance_review.required: true"' in text - assert '"independent dev-code-review"' not in text - - -def test_doctor_execute_path_requires_validate_not_universal_review() -> None: - text = read("scripts/orchestration/doctor.py") - start = text.index('skill_root / "orch-execute-plan" / "SKILL.md"') - first_list = text[start:].split("[", 1)[1].split("]", 1)[0] - assert "validate-executor-result" in first_list - assert "acceptance_review.verdict: accept" not in first_list - assert "build-review-package" not in first_list - - review_start = text.index('skill_root / "orch-review-plan" / "SKILL.md"') - review_list = text[review_start:].split("[", 1)[1].split("]", 1)[0] - assert "acceptance_review.verdict: accept" not in review_list - assert "compiled Truth Basis" in review_list - - -def test_orch_doctor_remains_read_only() -> None: - text = read("skills/orch-doctor/SKILL.md") - assert "## Read-Only Constraints (skill-owned)" in text - assert "Files changed: none" in text - - -def test_bounded_closure_contract_converges_policy_controller_and_consumers() -> None: - rule = read("rules/orchestration/orch-bounded-closure.md") - index = read("rules/index.yaml") - execute = read("skills/orch-execute-plan/SKILL.md") - review = read("skills/orch-review-plan/SKILL.md") - review_rule = read("rules/orchestration/orch-review-completion.md") - artifact_rule = read("rules/orchestration/orch-artifact-authoring.md") - boundary_rule = read("rules/orchestration/orch-orchestration-boundary.md") - planner = read("skills/orch-create-implementation-plan/SKILL.md") - specification = read("skills/orch-create-specification/SKILL.md") - workflow = read("references/assets/orchestration/workflow.md") - plan_contract = read("references/assets/orchestration/contract/plan-v1.md") - specification_contract = read( - "references/assets/orchestration/contract/specification-v1.md" + "rules/orchestration/orch-bounded-closure.md", + "rules/orchestration/orch-orchestration-boundary.md", ) - metadata_template = read("references/assets/template/project.yaml") - - assert "id: orch-bounded-closure" in rule - assert "path: orchestration/orch-bounded-closure.md" in index - for token in [ - "all executor attempts are terminal", + corpus = "\n".join(read(path) for path in paths).lower() + for token in ( + "executor-result", + "implementation review", + "accepted task result", + "final workflow review", + "product verdict", + ): + assert token in corpus + for retired in ( "begin-review-round", - "complete-review-round", - "review-round-status", - "finalize-with-blockers", - "exact request ID and target identity", - "different target identity", - "factual controller audit-block", - "must not impersonate a product verdict", - "fifth completed round", - "normal final audit", - "persist finalization-required state", - "persist an active workspace blocker", - "finalize the knowledge disposition", - "archive the origin specification and plan", - "release owned bindings", - "persist terminal closure", - "must not reopen product work", - ]: - assert token in rule, token - - for text in (execute, review, review_rule, workflow): - assert "post-execution review round" in text - assert "fifth" in text - assert "finalize-with-blockers" in text - - for text in (artifact_rule, planner, specification, plan_contract, specification_contract): - assert "plan and specification revisions do not consume" in text - assert "review_revision_limit" not in text - - for token in [ - "metadata_version: 3", - "authority: workspace-working-state", - "workspace_root: <absolute-path-to-workspace-root>", - "project_root: <absolute-path-to-project-root>", - "orchestration_control:", - "post_execution_review_round_limit: 5", - "post_execution_review_flows: []", - "blockers: []", - "closed_flows: []", - "implementation_exemptions: []", - ]: - assert token in metadata_template, token - assert "prefer_subagent" not in metadata_template - - for text in (boundary_rule, workflow): - assert "exhausted flow refuses reconciliation before its blocker is written" in text - assert "active workspace blocker refuses ordinary new work" in text - - assert "retain the project shim until builtin deployment" in workflow - assert "remove only that owned shim" in workflow + "publication receipt", + "current-authority sidecar", + ): + assert retired not in corpus + assert "do not infer admission from review history" in corpus + + +def test_bounded_closure_rule_selection_depends_only_on_admission_and_blockers() -> None: + text = read("rules/orchestration/orch-bounded-closure.md") + metadata = yaml.safe_load(text.split("---", 2)[1]) + triggers = "\n".join(metadata["applies_when"]).lower() + assert "review round" not in triggers + assert "fifth" not in triggers + assert "multi-round" not in triggers + assert "admission" in triggers + assert "blocker" in triggers + + +def test_stage_events_are_diagnostic_only() -> None: + text = read("scripts/work-bundle/stage_events.py") + for token in ("operational_metadata_only", "finding_recorded", "artifact_digest"): + assert token in text + assert "never issue or reinterpret a" in text + assert "product-review verdict, artifact qualification, or lifecycle decision" in text diff --git a/tests/test_orchestration_specifications.py b/tests/test_orchestration_specifications.py new file mode 100644 index 0000000..0c1b466 --- /dev/null +++ b/tests/test_orchestration_specifications.py @@ -0,0 +1,297 @@ +from __future__ import annotations + +import argparse +import json +import os +from pathlib import Path +import subprocess +import sys + +import pytest + + +REPO_ROOT = Path(__file__).resolve().parents[1] +ORCH_ROOT = REPO_ROOT / "scripts" / "orchestration" +sys.path.insert(0, str(ORCH_ROOT)) + +from artifact_inputs import _resolve_spec_paths +from artifact_store import family_policy, load_catalog, read_artifact +import specs +import execution_context + + +CATALOG = REPO_ROOT / "references/assets/orchestration/contract/artifact-family-catalog-v2.yaml" + + +def _args(root: Path, content: Path | None = None, **overrides: object) -> argparse.Namespace: + values: dict[str, object] = { + "workspace_root": str(root), + "project_root": None, + "id": "spec-20990101-001a", + "title": "Canonical specification", + "purpose": "Prove the current specification family", + "component": "orchestration", + "version": "1.0", + "content_file": str(content) if content else "", + "status": "draft", + "filename": None, + } + values.update(overrides) + return argparse.Namespace(**values) + + +@pytest.fixture +def workspace(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: + (tmp_path / ".work-bundle/orchestration/spec/active").mkdir(parents=True) + (tmp_path / ".work-bundle/orchestration/spec/archived").mkdir(parents=True) + monkeypatch.setattr(specs, "resolve_workspace_root", lambda _args: tmp_path) + monkeypatch.setattr(specs, "resolve_working_workspace", lambda _root: None) + monkeypatch.setattr(specs, "orchestration_root", lambda _args: tmp_path / ".work-bundle/orchestration") + monkeypatch.setattr(specs, "init_dirs", lambda _args: None) + monkeypatch.setattr( + specs, + "rel", + lambda path, _args: Path(path).resolve().relative_to(tmp_path.resolve()).as_posix(), + ) + return tmp_path + + +def _content(path: Path, *, extra: str = "", body: str = "# Complete semantic specification\n\n- **REQ-001A:** Preserve the suffixed requirement.\n") -> None: + path.write_text( + "---\n" + "project: demo\n" + "source_knowledge: []\n" + "related_handoffs: []\n" + "tags: [orchestration]\n" + "execution_workspace:\n" + " isolation: existing\n" + " profile: default\n" + " cleanup: manual\n" + f"{extra}" + "---\n" + f"{body}", + encoding="utf-8", + ) + + +def test_catalog_v2_registers_canonical_specification_family() -> None: + policy = family_policy(load_catalog(CATALOG), "specification") + assert policy["schema"]["id"] == "specification-v1" + assert policy["locator"]["template"] == ( + ".work-bundle/orchestration/spec/{state}/{id}.spec.md" + ) + assert policy["index"]["path"] == ".work-bundle/orchestration/spec/index.jsonl" + + +def test_write_read_index_qualify_archive_and_suffix_round_trip( + workspace: Path, tmp_path: Path, +) -> None: + content = tmp_path / "content.md" + _content(content) + args = _args(workspace, content) + + specs.cmd_write_spec(args) + active = workspace / ".work-bundle/orchestration/spec/active/spec-20990101-001a.spec.md" + assert active.is_file() + stored = read_artifact( + CATALOG, + "specification", + {"workspace_root": workspace}, + identity="spec-20990101-001a", + state="active", + ) + assert stored["data"]["status"] == "draft" + assert "REQ-001A" in stored["body"] + assert not hasattr(execution_context, "_source_records") + rows = specs.index_specs(args) + assert rows == [ + { + "type": "spec", + "id": "spec-20990101-001a", + "title": "Canonical specification", + "status": "draft", + "path": ".work-bundle/orchestration/spec/active/spec-20990101-001a.spec.md", + "purpose": "Prove the current specification family", + "component": "orchestration", + "created_at": stored["data"]["date_created"], + "updated_at": stored["data"]["last_updated"], + } + ] + + specs.cmd_set_spec_status(_args(workspace, id=args.id, status="verified")) + assert specs.index_specs(args)[0]["status"] == "verified" + specs.cmd_set_spec_status(_args(workspace, id=args.id, status="archived")) + assert not active.exists() + assert (workspace / ".work-bundle/orchestration/spec/archived/spec-20990101-001a.spec.md").is_file() + + +def test_legacy_markdown_is_ignored_and_only_canonical_identity_lookup_is_supported( + workspace: Path, tmp_path: Path, +) -> None: + legacy = workspace / ".work-bundle/orchestration/spec/active/spec-legacy.md" + legacy.write_text("---\nid: [broken\n---\n", encoding="utf-8") + content = tmp_path / "content.md" + _content(content) + args = _args(workspace, content) + specs.cmd_write_spec(args) + + assert [row["id"] for row in specs.index_specs(args)] == ["spec-20990101-001a"] + assert _resolve_spec_paths( + workspace, {}, {"source_spec_id": "spec-20990101-001a"} + ) == [workspace / ".work-bundle/orchestration/spec/active/spec-20990101-001a.spec.md"] + with pytest.raises(SystemExit, match="Legacy source_spec aliases are unsupported"): + _resolve_spec_paths(workspace, {}, {"source_spec": "spec-20990101-001a"}) + with pytest.raises(SystemExit, match="paths are unsupported"): + _resolve_spec_paths( + workspace, + {}, + { + "source_spec_id": ( + ".work-bundle/orchestration/spec/active/" + "spec-20990101-001a.spec.md" + ) + }, + ) + with pytest.raises(SystemExit, match="canonical location"): + _resolve_spec_paths(workspace, {}, {"source_spec_id": "spec-legacy"}) + + +def test_structural_override_collision_and_filename_override_fail_before_mutation( + workspace: Path, tmp_path: Path, +) -> None: + content = tmp_path / "content.md" + _content(content, extra="id: spec-evil\n") + args = _args(workspace, content) + with pytest.raises(SystemExit, match="structural field override"): + specs.cmd_write_spec(args) + assert not list((workspace / ".work-bundle/orchestration/spec/active").glob("*.spec.md")) + + _content(content) + with pytest.raises(SystemExit, match="filename override"): + specs.cmd_write_spec(_args(workspace, content, filename="custom.md")) + specs.cmd_write_spec(args) + before = (workspace / ".work-bundle/orchestration/spec/index.jsonl").read_bytes() + with pytest.raises(SystemExit, match="collision"): + specs.cmd_write_spec(args) + assert (workspace / ".work-bundle/orchestration/spec/index.jsonl").read_bytes() == before + + +def test_shared_front_matter_mutation_remains_available_to_plan_consumers( + tmp_path: Path, +) -> None: + path = tmp_path / "plan.md" + path.write_text( + "---\nid: plan-001\nstatus: Planned\nupdated_at: 2020-01-01\n---\nBody\n", + encoding="utf-8", + ) + specs.replace_front_matter_value(path, "status", "In progress") + data, body = specs.parse_markdown_artifact(path.read_text(encoding="utf-8"), source=str(path)) + assert data["status"] == "In progress" + assert data["updated_at"] != "2020-01-01" + assert body == "Body\n" + + +def test_archive_reports_partial_effect_when_index_rebuild_fails( + workspace: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, +) -> None: + content = tmp_path / "content.md" + _content(content) + args = _args(workspace, content) + specs.cmd_write_spec(args) + original = specs.rebuild_index + calls = 0 + + def fail_after_move(*values: object, **options: object) -> object: + nonlocal calls + calls += 1 + if calls == 1: + raise SystemExit("injected index failure") + return original(*values, **options) + + monkeypatch.setattr(specs, "rebuild_index", fail_after_move) + with pytest.raises(SystemExit, match="moved but index rebuild failed.*partial effect"): + specs.cmd_set_spec_status(_args(workspace, id=args.id, status="archived")) + assert (workspace / ".work-bundle/orchestration/spec/archived/spec-20990101-001a.spec.md").is_file() + + +def test_skill_contract_requires_direct_semantic_review_and_self_check() -> None: + skill = (REPO_ROOT / "skills/orch-create-specification/SKILL.md").read_text(encoding="utf-8") + for term in [ + "user purpose", + "accepted authority", + "requirements, constraints, interfaces, acceptance criteria", + "material conflicts", + "scope", + "## Self-check", + "Supporting evidence files do not issue the semantic verdict", + ]: + assert term in skill + assert "require_specification_review" not in skill + + +def test_current_public_entrypoint_writes_canonical_family(tmp_path: Path) -> None: + workspace_id = "wb-stage3-entrypoint" + control = tmp_path / ".work-bundle" + control.mkdir() + (control / "project.yaml").write_text( + "metadata_version: 4\nauthority: canonical\n" + f"workspace: {{id: {workspace_id}, slug: demo, mode: single-repository}}\n" + "control_plane: {schema_version: 1, repository: {remote: ''}, sync_policy: {mode: manual}}\n" + "source_repositories:\n" + " - id: source\n role: source\n locator: {type: manual, value: fixture}\n" + " default_branch: main\n workspace_binding: {type: root}\n" + " materialization: {required: true}\n operation_policy: inherit\n", + encoding="utf-8", + ) + home = tmp_path / "home" + config = home / ".work-bundle" + (config / "registry").mkdir(parents=True) + (config / "bootstrap.yaml").write_text( + "bootstrap_version: v1\nauthority: canonical\n" + f"work_bundle_root: {REPO_ROOT}\n" + 'project_registry: "$work_bundle_config_root/registry/projects.yaml"\n' + 'skill_registry: "$work_bundle_config_root/registry/skill-registry.yaml"\n', + encoding="utf-8", + ) + (config / "registry/projects.yaml").write_text( + "registry_schema_version: 1\nprojects: []\ndevice_bindings:\n" + f" {workspace_id}:\n slug: demo\n workspace_root: {tmp_path}\n" + f" control_plane_path: {control}\n control_plane_remote: ''\n" + " observed_control_plane_head: ''\n repositories:\n source:\n" + f" project_root: {tmp_path}\n checkout_kind: manual\n" + " observed_branch: ''\n observed_head: ''\n observed_at: '2026-09-19T00:00:00Z'\n git_common_dir: ''\n", + encoding="utf-8", + ) + content = tmp_path / "content.md" + _content(content) + result = subprocess.run( + [ + sys.executable, str(REPO_ROOT / "scripts/orch.py"), "write-spec", + "--workspace-root", str(tmp_path), "--id", "spec-20990101-002b", + "--title", "Entrypoint", "--purpose", "Public command", + "--component", "orchestration", "--content-file", str(content), + ], + cwd=REPO_ROOT, + env={**os.environ, "HOME": str(home)}, + text=True, + capture_output=True, + check=False, + ) + assert result.returncode == 0, result.stderr + assert (control / "orchestration/spec/active/spec-20990101-002b.spec.md").is_file() + doctor = subprocess.run( + [ + sys.executable, str(REPO_ROOT / "scripts/orch.py"), "doctor", + "--workspace-root", str(tmp_path), + ], + cwd=REPO_ROOT, + env={**os.environ, "HOME": str(home)}, + text=True, + capture_output=True, + check=False, + ) + doctor_output = doctor.stdout + doctor.stderr + assert "invalid current specification family" not in doctor_output + assert "spec index identity lacks one canonical artifact" not in doctor_output + assert "spec index does not match canonical current family" not in doctor_output + assert "index path escapes orchestration root" not in doctor_output diff --git a/tests/test_orchestration_stage5_current_path.py b/tests/test_orchestration_stage5_current_path.py new file mode 100644 index 0000000..27ca8d7 --- /dev/null +++ b/tests/test_orchestration_stage5_current_path.py @@ -0,0 +1,1160 @@ +from __future__ import annotations + +import argparse +import hashlib +import json +from pathlib import Path +import subprocess +import sys + +import pytest +import yaml + + +REPO_ROOT = Path(__file__).resolve().parents[1] +ORCHESTRATION = REPO_ROOT / "scripts" / "orchestration" +sys.path.insert(0, str(ORCHESTRATION)) + +from artifact_store import family_policy, load_catalog, read_artifact, write_artifact # noqa: E402 +import dispatcher # noqa: E402 +import artifact_store # noqa: E402 +import execution_context # noqa: E402 +import handoffs # noqa: E402 +import plans # noqa: E402 +import review_runtime # noqa: E402 + + +CATALOG = REPO_ROOT / "references/assets/orchestration/contract/artifact-family-catalog-v5.yaml" + + +def _args(root: Path, **overrides: object) -> argparse.Namespace: + values: dict[str, object] = { + "workspace_root": str(root), + "project_root": None, + "id": None, + "plan_id": None, + "phase_id": None, + "task_id": None, + "state": None, + "current_state": None, + "target_state": None, + "scope": None, + "content_file": None, + "source_root": None, + } + values.update(overrides) + return argparse.Namespace(**values) + + +@pytest.fixture +def workspace(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: + (tmp_path / ".work-bundle/orchestration").mkdir(parents=True) + subprocess.run(["git", "init", "-q", str(tmp_path)], check=True) + subprocess.run(["git", "-C", str(tmp_path), "config", "user.email", "stage5@example.invalid"], check=True) + subprocess.run(["git", "-C", str(tmp_path), "config", "user.name", "Stage 5"], check=True) + source = tmp_path / "src/current.py" + source.parent.mkdir(parents=True) + source.write_text("print('committed')\n", encoding="utf-8") + (tmp_path / ".gitignore").write_text(".work-bundle/\n", encoding="utf-8") + subprocess.run(["git", "-C", str(tmp_path), "add", "src/current.py", ".gitignore"], check=True) + subprocess.run(["git", "-C", str(tmp_path), "commit", "-qm", "base"], check=True) + monkeypatch.setattr(handoffs, "resolve_workspace_root", lambda _args: tmp_path) + monkeypatch.setattr( + review_runtime, "resolve_workspace_root", lambda _args: tmp_path, raising=False + ) + monkeypatch.setattr(plans, "resolve_workspace_root", lambda _args: tmp_path) + return tmp_path + + +def _write_yaml(tmp_path: Path, name: str, data: dict[str, object], *, flow: bool = False) -> Path: + path = tmp_path / name + path.write_text( + yaml.safe_dump(data, sort_keys=False, default_flow_style=flow), encoding="utf-8" + ) + return path + + +def _executor_semantics() -> dict[str, object]: + return { + "result_state": "implemented", + "summary": "Implemented the bounded task.", + "changes": [{"path": "src/current.py", "summary": "Added current behavior."}], + "validation_observations": [ + {"id": "VAL-001", "command": "pytest -q", "result": "passed", "summary": "Focused test passed."} + ], + "unresolved_product_blockers": [], + "task_fit": {"status": "complete", "summary": "The planned task is implemented."}, + "repository_observations": {"repository_id": "source", "baseline": "abc123", "dirty": True}, + "codegraph_observations": {"status": "no-index", "summary": "Repository is not indexed."}, + "delegation": {"agent_id": "worker-1", "role": "implementor"}, + "knowledge_disposition": {"action": "update", "reason": "Stable boundary changed."}, + } + + +def _candidate(root: Path, *, kind: str = "worktree") -> dict[str, object]: + base = subprocess.run( + ["git", "-C", str(root), "rev-parse", "HEAD"], check=True, + capture_output=True, text=True, + ).stdout.strip() + return execution_context.build_implementation_review_candidate( + source_root=root, kind=kind, base_commit=base, changed_paths=["src/current.py"] + ) + + +def _review_semantics( + candidate: dict[str, object], + *, + plan_identity: dict[str, str] | None = None, + verdict: str = "accept", +) -> dict[str, object]: + return { + "scope": "task", + "specification_id": "spec-stage5", + "plan_identity": plan_identity or {"id": "plan-stage5", "sha256": "1" * 64}, + "target": candidate, + "implementor_agent_id": "worker-1", + "reviewer": {"agent_id": "reviewer-1"}, + "reviewed_obligations": [ + {"source_id": "REQ-001", "status": "satisfied", "summary": "Source and tests cover the obligation."} + ], + "focused_observations": [ + {"id": "VAL-001", "result": "passed", "summary": "Focused behavior passed."} + ], + "verdict": verdict, + "findings": [], + } + + +def _write_stage5_plan_tree( + workspace: Path, *, review_required: bool = True +) -> dict[str, str]: + anchors = {"workspace_root": workspace} + today = "2026-09-20" + plan = { + "artifact_type": "root-plan", "schema_version": 1, "id": "plan-stage5", + "goal": "Finish Stage 5", "purpose": "Verify finalization", "component": "orchestration", + "version": "1", "source_spec_id": "spec-stage5", "status": "verified", + "date_created": today, "last_updated": today, + "source_coverage": [{"source_id": "REQ-009", "obligation_kind": "requirement", "task_ids": ["task-stage5"]}], + "authority": {"spec": "spec-stage5"}, "strategy": {"method": "direct"}, + "phase_index": [{"id": "phase-stage5", "order": 1}], "dependency_graph": {}, + "risks": [], "validation_strategy": [{"id": "VAL-001", "kind": "process"}], + "completion_criteria": ["Archived mechanically"], "knowledge_base_update": {"action": "none"}, + "semantic_loop": {"status": "closed"}, + "execution_workspace": {"isolation": "existing", "profile": "test", "cleanup": "manual"}, + } + plan_written = write_artifact( + CATALOG, "root-plan", anchors, plan, state="active", + bindings={"source_spec": "spec-stage5"}, + ) + phase = { + "artifact_type": "phase", "schema_version": 1, "id": "phase-stage5", "plan_id": "plan-stage5", + "name": "Stage 5", "status": "planned", "order": 1, "date_created": today, "last_updated": today, + "source_ids": ["REQ-009"], "depends_on": [], "task_index": [{"id": "task-stage5", "order": 1}], + "barriers": [], "validation": [{}], "completion_criteria": ["Done"], "allocated_rules": [], "allocated_skills": [], + } + write_artifact( + CATALOG, "phase", anchors, phase, state="active", + bindings={"plan": "plan-stage5"}, + ) + task = { + "artifact_type": "task", "schema_version": 2, "id": "task-stage5", "plan_id": "plan-stage5", + "phase_id": "phase-stage5", "name": "Finalize", "status": "planned", "order": 1, + "task_type": "implementation", "date_created": today, "last_updated": today, + "source_ids": ["REQ-009"], + "source_obligations": [{"source_id": "REQ-009", "semantic": "Complete Stage 5 finalization."}], + "truth_basis": {"purpose": "finalize", "as_is_evidence": ["current"], "decision_authority": ["spec"], "expected_delta": ["archive"], "conflict_status": "clear"}, + "depends_on": [], "source_files": [], "target_files": ["src/current.py"], "target_symbols": ["main"], + "interfaces": {}, "steps": ["finalize"], "validation": [{"id": "VAL-001", "kind": "process"}], + "evidence_capability": {"result": "mapped", "reason": "test", "invariants": []}, + "completion_criteria": ["Done"], "methodology": {"name": "tdd"}, + "executor_profile": {"capability": "implementation", "context_mode": "bounded", "review_capability": "none"}, + "acceptance_review": {"required": review_required}, "allocated_rules": [], "allocated_skills": [], + "handoff_contract": "executor-result-v1", + } + write_artifact( + CATALOG, "task", anchors, task, state="active", + bindings={"plan": "plan-stage5", "phase": "phase-stage5"}, + ) + plan_path = Path(str(plan_written["path"])) + return review_runtime.plan_review_identity(workspace, plan_path) + + +def test_catalog_v5_registers_exact_stage5_families_and_policies() -> None: + catalog = load_catalog(CATALOG) + assert catalog["catalog_id"] == "artifact-family-catalog-v5" + for family in ( + "executor-result", + "implementation-review", + "accepted-task-result", + "final-workflow-review", + ): + policy = family_policy(catalog, family) + assert policy["representation"] == "yaml" + assert policy["anchor"] == "workspace_root" + assert policy["lifecycle"]["authority"] == "location" + assert policy["index"]["format"] == "jsonl" + + executor = family_policy(catalog, "executor-result") + assert executor["locator"]["template"] == ( + ".work-bundle/orchestration/result/executor/{state}/{plan}/{task}/{id}.executor-result.yaml" + ) + assert executor["lifecycle"]["transitions"]["active"] == ["reviewed", "superseded", "archived"] + assert family_policy(catalog, "implementation-review")["schema"] == { + "id": "implementation-review-v2", + "path": "implementation-review-v2.schema.json", + } + + +def test_executor_result_round_trip_inline_block_index_and_transition( + workspace: Path, tmp_path: Path, +) -> None: + first = _write_yaml(tmp_path, "first.yaml", _executor_semantics()) + second = _write_yaml(tmp_path, "second.yaml", _executor_semantics(), flow=True) + for identity, content in (("result-stage5-a", first), ("result-stage5-b", second)): + handoffs.cmd_write_executor_result( + _args( + workspace, + id=identity, + plan_id="plan-stage5", + task_id="task-stage5", + content_file=str(content), + ) + ) + + rows = handoffs.list_executor_results(_args(workspace)) + assert [row["id"] for row in rows] == ["result-stage5-a", "result-stage5-b"] + assert rows[0]["task_id"] == rows[1]["task_id"] == "task-stage5" + handoffs.cmd_transition_executor_result( + _args( + workspace, + id="result-stage5-a", + plan_id="plan-stage5", + task_id="task-stage5", + current_state="active", + target_state="reviewed", + ) + ) + stored = read_artifact( + CATALOG, + "executor-result", + {"workspace_root": workspace}, + identity="result-stage5-a", + state="reviewed", + bindings={"plan": "plan-stage5", "task": "task-stage5"}, + ) + assert stored["data"]["result_state"] == "implemented" + + +def test_executor_result_rejects_overrides_duplicates_and_review_verdict_before_mutation( + workspace: Path, tmp_path: Path, +) -> None: + bad = _executor_semantics() + bad["verdict"] = "accept" + content = _write_yaml(tmp_path, "bad.yaml", bad) + with pytest.raises(SystemExit, match="forbidden|schema validation"): + handoffs.cmd_write_executor_result( + _args( + workspace, + id="result-stage5", + plan_id="plan-stage5", + task_id="task-stage5", + content_file=str(content), + ) + ) + assert not list(workspace.rglob("*.executor-result.yaml")) + + content = _write_yaml(tmp_path, "good.yaml", _executor_semantics()) + args = _args( + workspace, + id="result-stage5", + plan_id="plan-stage5", + task_id="task-stage5", + content_file=str(content), + ) + handoffs.cmd_write_executor_result(args) + before = next(workspace.rglob("*.executor-result.yaml")).read_bytes() + with pytest.raises(SystemExit, match="collision"): + handoffs.cmd_write_executor_result(args) + assert next(workspace.rglob("*.executor-result.yaml")).read_bytes() == before + + +def test_review_accepted_result_and_final_review_form_compact_current_chain( + workspace: Path, tmp_path: Path, +) -> None: + plan_identity = _write_stage5_plan_tree(workspace) + executor_input = _write_yaml(tmp_path, "executor.yaml", _executor_semantics()) + handoffs.cmd_write_executor_result( + _args( + workspace, + id="result-stage5", + plan_id="plan-stage5", + task_id="task-stage5", + content_file=str(executor_input), + ) + ) + executor_path = next(workspace.rglob("*.executor-result.yaml")) + executor_digest = hashlib.sha256(executor_path.read_bytes()).hexdigest() + + candidate = _candidate(workspace) + review_input = _write_yaml( + tmp_path, "review.yaml", + _review_semantics(candidate, plan_identity=plan_identity), + ) + review_runtime.cmd_write_implementation_review( + _args( + workspace, + id="review-stage5", + plan_id="plan-stage5", + task_id="task-stage5", + source_root=str(workspace), + content_file=str(review_input), + ) + ) + review_path = next(workspace.rglob("*.implementation-review.yaml")) + review_digest = hashlib.sha256(review_path.read_bytes()).hexdigest() + + accepted_input = _write_yaml( + tmp_path, + "accepted.yaml", + { + "product_identity": {"kind": "worktree", "sha256": candidate["sha256"]}, + "executor_result": {"id": "result-stage5", "sha256": executor_digest}, + "implementation_review": {"id": "review-stage5", "sha256": review_digest}, + "validation_outcomes": [{"id": "VAL-001", "result": "passed", "summary": "Focused pass."}], + "unresolved_material_defects": [], + "knowledge_disposition": {"action": "update", "reason": "Stable boundary changed."}, + }, + ) + review_runtime.cmd_write_accepted_task_result( + _args( + workspace, + id="accepted-stage5", + plan_id="plan-stage5", + task_id="task-stage5", + content_file=str(accepted_input), + ) + ) + accepted_path = next(workspace.rglob("*.accepted-task-result.yaml")) + accepted_digest = hashlib.sha256(accepted_path.read_bytes()).hexdigest() + + final_input = _write_yaml( + tmp_path, + "final.yaml", + { + "specification_id": "spec-stage5", + "plan_identity": plan_identity, + "candidate_identity": {"kind": "worktree", "sha256": candidate["sha256"]}, + "coverage": {"planned": 1, "accepted": 1, "missing": []}, + "accepted_results": [{"id": "accepted-stage5", "task_id": "task-stage5", "sha256": accepted_digest}], + "accepted_reviews": [{"id": "review-stage5", "sha256": review_digest}], + "test_outcomes": [{"id": "VAL-001", "result": "passed", "summary": "Focused pass."}], + "unresolved_material_defects": [], + "knowledge_disposition": {"action": "update", "reason": "Ready for owner follow-up."}, + "knowledge_return": {"status": "pending", "reference": None}, + "repository_finalization": { + "repositories": [{ + "repository_id": "source", "root": str(workspace), + "head": subprocess.run( + ["git", "-C", str(workspace), "rev-parse", "HEAD"], check=True, + capture_output=True, text=True, + ).stdout.strip(), + }] + }, + "verdict": "accept", + "archive_ready": False, + "reasons": ["All planned work has an accepted product review."], + }, + ) + review_runtime.cmd_write_final_workflow_review( + _args( + workspace, + id="final-stage5", + plan_id="plan-stage5", + content_file=str(final_input), + ) + ) + assert [row["id"] for row in review_runtime.list_implementation_reviews(_args(workspace))] == ["review-stage5"] + assert [row["id"] for row in review_runtime.list_accepted_task_results(_args(workspace))] == ["accepted-stage5"] + assert [row["id"] for row in review_runtime.list_final_workflow_reviews(_args(workspace))] == ["final-stage5"] + + +def test_review_verdict_remains_agent_authored_and_supporting_state_is_not_required( + workspace: Path, tmp_path: Path, +) -> None: + plan_identity = _write_stage5_plan_tree(workspace) + review_input = _write_yaml( + tmp_path, "review.yaml", + _review_semantics( + _candidate(workspace), plan_identity=plan_identity, verdict="repair" + ), + ) + review_runtime.cmd_write_implementation_review( + _args( + workspace, + id="review-omission", + plan_id="plan-stage5", + task_id="task-stage5", + source_root=str(workspace), + content_file=str(review_input), + ) + ) + row = review_runtime.list_implementation_reviews(_args(workspace))[0] + assert row["verdict"] == "repair" + assert "receipt" not in row and "publication" not in row and "history" not in row + + +def test_review_writer_rejects_stale_plan_tree_identity_before_mutation( + workspace: Path, tmp_path: Path, +) -> None: + _write_finalization_case(workspace) + content = _write_yaml( + tmp_path, + "stale-plan-review.yaml", + _review_semantics(_candidate(workspace)), + ) + + with pytest.raises(SystemExit, match="plan.*identity.*stale"): + review_runtime.cmd_write_implementation_review( + _args( + workspace, + id="review-stale-plan", + plan_id="plan-stage5", + task_id="task-stage5", + source_root=str(workspace), + content_file=str(content), + ) + ) + + assert not list(workspace.rglob("review-stale-plan.implementation-review.yaml")) + + +def test_accepted_result_enforces_canonical_task_review_requirement_before_write( + workspace: Path, tmp_path: Path, +) -> None: + _write_finalization_case(workspace, review_required=True, review_reference="none") + executor_path = next(workspace.rglob("result-stage5.executor-result.yaml")) + candidate = _candidate(workspace) + content = _write_yaml( + tmp_path, + "missing-required-review.yaml", + { + "product_identity": {"kind": "worktree", "sha256": candidate["sha256"]}, + "executor_result": { + "id": "result-stage5", + "sha256": hashlib.sha256(executor_path.read_bytes()).hexdigest(), + }, + "implementation_review": None, + "validation_outcomes": [], + "unresolved_material_defects": [], + "knowledge_disposition": {"action": "none", "reason": "No durable update."}, + }, + ) + + with pytest.raises(SystemExit, match="requires an implementation review"): + review_runtime.cmd_write_accepted_task_result( + _args( + workspace, + id="accepted-missing-review", + plan_id="plan-stage5", + task_id="task-stage5", + content_file=str(content), + ) + ) + + assert not list(workspace.rglob("accepted-missing-review.accepted-task-result.yaml")) + + +def test_final_review_writer_rejects_stale_plan_tree_identity_before_write( + workspace: Path, tmp_path: Path, +) -> None: + _write_finalization_case(workspace) + stored = next(workspace.rglob("final-stage5.final-workflow-review.yaml")) + semantic = yaml.safe_load(stored.read_text(encoding="utf-8")) + for field in review_runtime.CURRENT_STRUCTURAL_FIELDS: + semantic.pop(field, None) + semantic["plan_identity"] = {"id": "plan-stage5", "sha256": "0" * 64} + content = _write_yaml(tmp_path, "stale-final-review.yaml", semantic) + + with pytest.raises(SystemExit, match="plan.*identity.*stale"): + review_runtime.cmd_write_final_workflow_review( + _args( + workspace, + id="final-stale-plan", + plan_id="plan-stage5", + content_file=str(content), + ) + ) + + assert not list(workspace.rglob("final-stale-plan.final-workflow-review.yaml")) + + +def _write_finalization_case( + workspace: Path, + *, + review_required: bool = True, + review_reference: str = "valid", +) -> None: + anchors = {"workspace_root": workspace} + today = "2026-09-20" + plan_identity = _write_stage5_plan_tree( + workspace, review_required=review_required + ) + + candidate = _candidate(workspace) + review_written = None + if review_reference == "valid": + review = { + **_review_semantics(candidate, plan_identity=plan_identity), "artifact_type": "implementation-review", "schema_version": 2, + "id": "review-stage5", "plan_id": "plan-stage5", "task_id": "task-stage5", + "target_sha256": candidate["sha256"], "date_created": today, "last_updated": today, + } + review_written = write_artifact(CATALOG, "implementation-review", anchors, review, state="active", bindings={"plan": "plan-stage5", "task": "task-stage5"}) + executor = { + **_executor_semantics(), "artifact_type": "executor-result", "schema_version": 1, + "id": "result-stage5", "plan_id": "plan-stage5", "phase_id": "phase-stage5", + "task_id": "task-stage5", "date_created": today, "last_updated": today, + } + executor_written = write_artifact(CATALOG, "executor-result", anchors, executor, state="active", bindings={"plan": "plan-stage5", "task": "task-stage5"}) + accepted = { + "artifact_type": "accepted-task-result", "schema_version": 1, "id": "accepted-stage5", + "plan_id": "plan-stage5", "task_id": "task-stage5", + "product_identity": {"kind": "worktree", "sha256": candidate["sha256"]}, "product_sha256": candidate["sha256"], + "executor_result": {"id": "result-stage5", "sha256": executor_written["digest"]}, + "implementation_review": ( + {"id": "review-stage5", "sha256": review_written["digest"]} + if review_written is not None + else ({"id": "review-missing", "sha256": "f" * 64} if review_reference == "invalid" else None) + ), + "validation_outcomes": [], "unresolved_material_defects": [], + "knowledge_disposition": {"action": "none", "reason": "No durable update."}, "knowledge_action": "none", + "date_created": today, "last_updated": today, + } + accepted_written = write_artifact(CATALOG, "accepted-task-result", anchors, accepted, state="active", bindings={"plan": "plan-stage5", "task": "task-stage5"}) + head = subprocess.run(["git", "-C", str(workspace), "rev-parse", "HEAD"], check=True, capture_output=True, text=True).stdout.strip() + final = { + "artifact_type": "final-workflow-review", "schema_version": 1, "id": "final-stage5", "plan_id": "plan-stage5", + "specification_id": "spec-stage5", "plan_identity": plan_identity, + "candidate_identity": {"kind": "worktree", "sha256": candidate["sha256"]}, "target_sha256": candidate["sha256"], + "coverage": {"planned": 1, "accepted": 1, "missing": []}, + "accepted_results": [{"id": "accepted-stage5", "task_id": "task-stage5", "sha256": accepted_written["digest"]}], + "accepted_reviews": ( + [{"id": "review-stage5", "sha256": review_written["digest"]}] + if review_written is not None + else ([{"id": "review-missing", "sha256": "f" * 64}] if review_reference == "invalid" else []) + ), + "test_outcomes": [], "unresolved_material_defects": [], + "knowledge_disposition": {"action": "none", "reason": "No durable update."}, + "knowledge_return": {"status": "not-needed", "reference": None}, + "repository_finalization": {"repositories": [{"repository_id": "source", "root": str(workspace), "head": head}]}, + "verdict": "accept", "archive_ready": True, "reasons": ["Mechanically ready."], + "date_created": today, "last_updated": today, + } + write_artifact(CATALOG, "final-workflow-review", anchors, final, state="active", bindings={"plan": "plan-stage5"}) + + +def test_finalize_reviewed_plan_verifies_references_clean_git_and_archives( + workspace: Path, +) -> None: + _write_finalization_case(workspace) + + dirty = workspace / "untracked.txt" + dirty.write_text("dirty\n", encoding="utf-8") + with pytest.raises(SystemExit, match="not exact and clean"): + plans.cmd_finalize_reviewed_plan(_args(workspace, plan_id="plan-stage5", final_review_id="final-stage5")) + dirty.unlink() + plans.cmd_finalize_reviewed_plan(_args(workspace, plan_id="plan-stage5", final_review_id="final-stage5")) + assert list((workspace / ".work-bundle/orchestration/plan/archived").rglob("*.plan.yaml")) + assert list((workspace / ".work-bundle/orchestration/review/final/archived").rglob("*.final-workflow-review.yaml")) + + +def test_finalizer_rejects_plan_tree_change_after_final_review( + workspace: Path, +) -> None: + _write_finalization_case(workspace) + task_path = next(workspace.rglob("task-stage5.task.yaml")) + task = yaml.safe_load(task_path.read_text(encoding="utf-8")) + task["steps"].append("changed after review") + write_artifact( + CATALOG, + "task", + {"workspace_root": workspace}, + task, + state="active", + bindings={"plan": "plan-stage5", "phase": "phase-stage5"}, + ) + + with pytest.raises(SystemExit, match="plan/specification identity is stale"): + plans.cmd_finalize_reviewed_plan( + _args(workspace, plan_id="plan-stage5", final_review_id="final-stage5") + ) + + assert list( + (workspace / ".work-bundle/orchestration/plan/active").rglob("*.plan.yaml") + ) + + +def test_finalize_reviewed_plan_allows_null_review_when_task_does_not_require_it( + workspace: Path, +) -> None: + _write_finalization_case( + workspace, review_required=False, review_reference="none" + ) + + plans.cmd_finalize_reviewed_plan( + _args(workspace, plan_id="plan-stage5", final_review_id="final-stage5") + ) + + assert list( + (workspace / ".work-bundle/orchestration/plan/archived").rglob("*.plan.yaml") + ) + + +@pytest.mark.parametrize("review_reference", ["none", "invalid"]) +def test_finalize_reviewed_plan_rejects_missing_or_invalid_required_review( + workspace: Path, + review_reference: str, +) -> None: + _write_finalization_case( + workspace, review_required=True, review_reference=review_reference + ) + + with pytest.raises(SystemExit, match="implementation review|implementation-review"): + plans.cmd_finalize_reviewed_plan( + _args(workspace, plan_id="plan-stage5", final_review_id="final-stage5") + ) + + assert list( + (workspace / ".work-bundle/orchestration/plan/active").rglob("*.plan.yaml") + ) + + +def _ownership(binding_id: str) -> dict[str, object]: + return { + "binding_id": binding_id, + "target_kind": "local_project", + "state": "active", + "original_owner": "controller", + "current_owner": "controller", + "reason": "task execution", + "repair_owner": None, + "rereview_owner": None, + "releasable": False, + "history": [{ + "transition_id": f"transition-{binding_id}", + "from": "active", + "to": "active", + "owner": "controller", + "reason": "task execution", + "timestamp": "2026-09-20T00:00:00Z", + }], + } + + +def test_finalizer_reports_later_binding_release_partial_effects( + workspace: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + _write_finalization_case(workspace) + bindings = [ + {"plan_id": "plan-stage5", "task_id": "task-a", "ownership": _ownership("binding-a")}, + {"plan_id": "plan-stage5", "task_id": "task-b", "ownership": _ownership("binding-b")}, + ] + monkeypatch.setattr(plans, "_iter_task_bindings", lambda _root: bindings) + monkeypatch.setattr(plans, "_persist_binding", lambda _binding, _root: None) + monkeypatch.setattr( + plans, + "validate_execution_binding_ownership", + lambda _root, ownership: ownership, + ) + calls = 0 + + class Released: + def __init__(self, ownership: dict[str, object]) -> None: + self.ownership = ownership + + def to_dict(self) -> dict[str, object]: + return {**self.ownership, "state": "released", "releasable": True} + + def release(_store: object, binding_id: str, *, owner: str) -> Released: + nonlocal calls + calls += 1 + if calls == 2: + raise plans.CompletionProvenanceError("injected later binding failure") + return Released(_ownership(binding_id)) + + monkeypatch.setattr(plans, "release_completion_binding", release) + + with pytest.raises(SystemExit) as captured: + plans.cmd_finalize_reviewed_plan( + _args(workspace, plan_id="plan-stage5", final_review_id="final-stage5") + ) + payload = json.loads(str(captured.value)) + assert payload["status"] == "partial" + assert payload["code"] == "WB_FINALIZATION_PARTIAL_EFFECT" + assert payload["completed_operations"][-1]["task_id"] == "task-a" + assert payload["failed_operation"] == { + "operation": "binding-release", + "task_id": "task-b", + "binding_id": "binding-b", + } + + +def test_finalizer_reports_transition_partial_effects( + workspace: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + _write_finalization_case(workspace) + real_transition = artifact_store.transition_artifact + calls = 0 + + def transition(*args: object, **kwargs: object) -> dict[str, object]: + nonlocal calls + calls += 1 + if calls == 2: + raise RuntimeError("injected later transition failure") + return real_transition(*args, **kwargs) + + monkeypatch.setattr(artifact_store, "transition_artifact", transition) + + with pytest.raises(SystemExit) as captured: + plans.cmd_finalize_reviewed_plan( + _args(workspace, plan_id="plan-stage5", final_review_id="final-stage5") + ) + payload = json.loads(str(captured.value)) + assert payload["status"] == "partial" + assert payload["completed_operations"][-1]["operation"] == "artifact-transition" + assert payload["failed_operation"]["operation"] == "artifact-transition" + assert payload["failed_operation"]["family"] == "accepted-task-result" + + +def test_finalizer_reports_index_rebuild_partial_effects( + workspace: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + _write_finalization_case(workspace) + real_transition = artifact_store.transition_artifact + real_rebuild = plans.rebuild_index + mutation_started = False + + def transition(*args: object, **kwargs: object) -> dict[str, object]: + nonlocal mutation_started + result = real_transition(*args, **kwargs) + mutation_started = True + return result + + def rebuild(*args: object, **kwargs: object) -> dict[str, object]: + if mutation_started: + raise RuntimeError("injected index rebuild failure") + return real_rebuild(*args, **kwargs) + + monkeypatch.setattr(artifact_store, "transition_artifact", transition) + monkeypatch.setattr(plans, "rebuild_index", rebuild) + + with pytest.raises(SystemExit) as captured: + plans.cmd_finalize_reviewed_plan( + _args(workspace, plan_id="plan-stage5", final_review_id="final-stage5") + ) + payload = json.loads(str(captured.value)) + assert payload["status"] == "partial" + assert any( + operation["operation"] == "artifact-transition" + for operation in payload["completed_operations"] + ) + assert payload["failed_operation"]["operation"] == "index-rebuild" + + +def test_worktree_candidate_identity_is_exact_and_does_not_require_clean_head(tmp_path: Path) -> None: + subprocess.run(["git", "init", "-q", str(tmp_path)], check=True) + subprocess.run(["git", "-C", str(tmp_path), "config", "user.email", "stage5@example.invalid"], check=True) + subprocess.run(["git", "-C", str(tmp_path), "config", "user.name", "Stage 5"], check=True) + changed = tmp_path / "changed.py" + changed.write_text("print('base')\n", encoding="utf-8") + subprocess.run(["git", "-C", str(tmp_path), "add", "changed.py"], check=True) + subprocess.run(["git", "-C", str(tmp_path), "commit", "-qm", "base"], check=True) + base = subprocess.run(["git", "-C", str(tmp_path), "rev-parse", "HEAD"], check=True, capture_output=True, text=True).stdout.strip() + changed.write_text("print('current')\n", encoding="utf-8") + candidate = execution_context.build_implementation_review_candidate( + source_root=tmp_path, + kind="worktree", + base_commit=base, + changed_paths=["changed.py"], + ) + expected_manifest = [ + { + "path": "changed.py", + "state": "present", + "sha256": hashlib.sha256(changed.read_bytes()).hexdigest(), + } + ] + assert candidate["manifest"] == expected_manifest + assert candidate["sha256"] == hashlib.sha256( + f"present {expected_manifest[0]['sha256']} changed.py\n".encode() + ).hexdigest() + + +def test_worktree_candidate_admits_deleted_base_path_with_explicit_state( + workspace: Path, +) -> None: + base = subprocess.run( + ["git", "-C", str(workspace), "rev-parse", "HEAD"], + check=True, + capture_output=True, + text=True, + ).stdout.strip() + deleted = workspace / "src/current.py" + deleted.unlink() + + candidate = execution_context.build_implementation_review_candidate( + source_root=workspace, + kind="worktree", + base_commit=base, + changed_paths=["src/current.py"], + ) + + assert candidate["manifest"] == [{"path": "src/current.py", "state": "deleted"}] + assert candidate["sha256"] == hashlib.sha256( + b"deleted - src/current.py\n" + ).hexdigest() + checked = review_runtime._candidate_validator().validate_current_candidate_and_independence( + workspace, + candidate, + reviewer_agent_id="reviewer-1", + implementor_agent_id="worker-1", + ) + assert checked["target"] == candidate + + +def test_implementation_review_v2_schema_accepts_only_explicit_deletion_state( + workspace: Path, +) -> None: + base = subprocess.run( + ["git", "-C", str(workspace), "rev-parse", "HEAD"], + check=True, + capture_output=True, + text=True, + ).stdout.strip() + (workspace / "src/current.py").unlink() + candidate = execution_context.build_implementation_review_candidate( + source_root=workspace, + kind="worktree", + base_commit=base, + changed_paths=["src/current.py"], + ) + review = { + **_review_semantics(candidate), + "artifact_type": "implementation-review", + "schema_version": 2, + "id": "review-deletion", + "plan_id": "plan-stage5", + "task_id": "task-stage5", + "target_sha256": candidate["sha256"], + "date_created": "2026-09-20", + "last_updated": "2026-09-20", + } + + written = write_artifact( + CATALOG, + "implementation-review", + {"workspace_root": workspace}, + review, + state="active", + bindings={"plan": "plan-stage5", "task": "task-stage5"}, + ) + assert written["schema"] == "implementation-review-v2" + + malformed = json.loads(json.dumps(review)) + malformed["id"] = "review-ambiguous-deletion" + malformed["target"]["manifest"][0]["sha256"] = hashlib.sha256(b"").hexdigest() + with pytest.raises(SystemExit, match="Artifact schema validation failed"): + write_artifact( + CATALOG, + "implementation-review", + {"workspace_root": workspace}, + malformed, + state="active", + bindings={"plan": "plan-stage5", "task": "task-stage5"}, + ) + + +def test_worktree_candidate_rejects_path_absent_from_worktree_and_base( + workspace: Path, +) -> None: + base = subprocess.run( + ["git", "-C", str(workspace), "rev-parse", "HEAD"], + check=True, + capture_output=True, + text=True, + ).stdout.strip() + target = { + "kind": "worktree", + "base_commit": base, + "manifest": [{"path": "never-existed.py", "state": "deleted"}], + "sha256": hashlib.sha256( + b"deleted - never-existed.py\n" + ).hexdigest(), + } + + with pytest.raises(SystemExit, match="unavailable"): + execution_context.build_implementation_review_candidate( + source_root=workspace, + kind="worktree", + base_commit=base, + changed_paths=["never-existed.py"], + ) + validator = review_runtime._candidate_validator() + with pytest.raises(validator.ReviewerWorkspaceError, match="TARGET_INVALID"): + validator.validate_current_candidate_and_independence( + workspace, + target, + reviewer_agent_id="reviewer-1", + implementor_agent_id="worker-1", + ) + + +def test_worktree_candidate_admits_existing_empty_file_without_base_entry( + workspace: Path, +) -> None: + base = subprocess.run( + ["git", "-C", str(workspace), "rev-parse", "HEAD"], + check=True, + capture_output=True, + text=True, + ).stdout.strip() + empty = workspace / "empty.py" + empty.write_bytes(b"") + + candidate = execution_context.build_implementation_review_candidate( + source_root=workspace, + kind="worktree", + base_commit=base, + changed_paths=["empty.py"], + ) + + assert candidate["manifest"] == [ + { + "path": "empty.py", + "state": "present", + "sha256": hashlib.sha256(b"").hexdigest(), + } + ] + checked = review_runtime._candidate_validator().validate_current_candidate_and_independence( + workspace, + candidate, + reviewer_agent_id="reviewer-1", + implementor_agent_id="worker-1", + ) + assert checked["target"] == candidate + + +def test_worktree_candidate_distinguishes_deleted_path_from_empty_file( + workspace: Path, +) -> None: + base = subprocess.run( + ["git", "-C", str(workspace), "rev-parse", "HEAD"], + check=True, + capture_output=True, + text=True, + ).stdout.strip() + path = workspace / "src/current.py" + path.unlink() + deleted = execution_context.build_implementation_review_candidate( + source_root=workspace, + kind="worktree", + base_commit=base, + changed_paths=["src/current.py"], + ) + + path.write_bytes(b"") + empty = execution_context.build_implementation_review_candidate( + source_root=workspace, + kind="worktree", + base_commit=base, + changed_paths=["src/current.py"], + ) + + assert deleted["manifest"] == [{"path": "src/current.py", "state": "deleted"}] + assert empty["manifest"] == [ + { + "path": "src/current.py", + "state": "present", + "sha256": hashlib.sha256(b"").hexdigest(), + } + ] + assert deleted["sha256"] != empty["sha256"] + + +@pytest.mark.parametrize( + ("manifest", "aggregate"), + [ + ( + [{"path": "src/current.py", "state": "deleted"}], + hashlib.sha256(b"deleted - src/current.py\n").hexdigest(), + ), + ( + [ + { + "path": "src/current.py", + "state": "present", + "sha256": hashlib.sha256(b"").hexdigest(), + } + ], + hashlib.sha256( + f"present {hashlib.sha256(b'').hexdigest()} src/current.py\n".encode() + ).hexdigest(), + ), + ], +) +def test_worktree_candidate_rejects_manifest_state_that_disagrees_with_path( + workspace: Path, + manifest: list[dict[str, str]], + aggregate: str, +) -> None: + base = subprocess.run( + ["git", "-C", str(workspace), "rev-parse", "HEAD"], + check=True, + capture_output=True, + text=True, + ).stdout.strip() + target = { + "kind": "worktree", + "base_commit": base, + "manifest": manifest, + "sha256": aggregate, + } + if manifest[0]["state"] == "present": + (workspace / "src/current.py").unlink() + + validator = review_runtime._candidate_validator() + with pytest.raises(validator.ReviewerWorkspaceError, match="TARGET_INVALID"): + validator.validate_current_candidate_and_independence( + workspace, + target, + reviewer_agent_id="reviewer-1", + implementor_agent_id="worker-1", + ) + + +def test_commit_candidate_uses_commit_bytes_and_review_recomputes_manifest( + workspace: Path, tmp_path: Path, +) -> None: + committed = (workspace / "src/current.py").read_bytes() + commit_candidate = _candidate(workspace, kind="commit") + (workspace / "src/current.py").write_text("print('worktree')\n", encoding="utf-8") + worktree_candidate = _candidate(workspace, kind="worktree") + assert commit_candidate["manifest"][0]["sha256"] == hashlib.sha256(committed).hexdigest() + assert commit_candidate["sha256"] != worktree_candidate["sha256"] + + plan_identity = _write_stage5_plan_tree(workspace) + tampered = _review_semantics(worktree_candidate, plan_identity=plan_identity) + tampered["target"]["manifest"][0]["sha256"] = "0" * 64 + review_input = _write_yaml(tmp_path, "tampered-review.yaml", tampered) + with pytest.raises(SystemExit, match="DIGEST_MISMATCH"): + review_runtime.write_implementation_review( + _args( + workspace, id="review-tampered", plan_id="plan-stage5", + task_id="task-stage5", source_root=str(workspace), + content_file=str(review_input), + ) + ) + assert not list(workspace.rglob("review-tampered.implementation-review.yaml")) + + +def test_review_writer_derives_independence_from_concrete_identities( + workspace: Path, tmp_path: Path, +) -> None: + plan_identity = _write_stage5_plan_tree(workspace) + semantics = _review_semantics(_candidate(workspace), plan_identity=plan_identity) + semantics["reviewer"] = {"agent_id": "worker-1"} + content = _write_yaml(tmp_path, "self-review.yaml", semantics) + with pytest.raises(SystemExit, match="NOT_INDEPENDENT"): + review_runtime.write_implementation_review( + _args( + workspace, id="review-self", plan_id="plan-stage5", + task_id="task-stage5", source_root=str(workspace), + content_file=str(content), + ) + ) + + +def test_work_bundle_dispatcher_has_no_reviewer_process_or_round_commands() -> None: + source = (REPO_ROOT / "scripts/work-bundle/dispatcher.py").read_text(encoding="utf-8") + for command in ( + "reviewer-workspace-create", "reviewer-workspace-operation", + "reviewer-workspace-cleanup", "reviewer-process-run", + "begin-review-round", "complete-review-round", "review-round-status", + ): + assert command not in source + + +def test_dispatcher_exposes_only_current_stage5_commands() -> None: + current = { + "write-executor-result", + "list-executor-results", + "transition-executor-result", + "index-executor-results", + "build-implementation-review-candidate", + "write-implementation-review", + "list-implementation-reviews", + "write-accepted-task-result", + "list-accepted-task-results", + "write-final-workflow-review", + "list-final-workflow-reviews", + "finalize-reviewed-plan", + } + retired = { + "write-handoff", + "list-handoffs", + "set-handoff-status", + "index-handoffs", + "begin-review-round", + "complete-review-round", + "review-round-status", + "finalize-accepted-plan", + "finalize-with-blockers", + "build-review-package", + "validate-executor-result", + "observe-task-validation", + "archive-plan", + } + assert current <= dispatcher.RECOGNIZED_COMMANDS + assert retired.isdisjoint(dispatcher.RECOGNIZED_COMMANDS) + help_text = dispatcher.build_parser().format_help() + assert "write-executor-result" in help_text + assert "write-handoff" not in help_text + + +def test_current_dispatcher_import_graph_omits_retired_authority_modules() -> None: + completed = subprocess.run( + [sys.executable, "-c", ( + "import sys; sys.path.insert(0, 'scripts/orchestration'); import dispatcher; " + "assert 'bounded_closure' not in sys.modules; " + "assert 'current_review_authority' not in sys.modules" + )], + cwd=REPO_ROOT, capture_output=True, text=True, check=False, + ) + assert completed.returncode == 0, completed.stderr + + +def test_changed_skills_end_with_practical_self_checks() -> None: + for name in ("orch-execute-plan", "orch-create-handoff", "orch-review-plan"): + text = (REPO_ROOT / "skills" / name / "SKILL.md").read_text(encoding="utf-8") + tail = text[-2200:] + assert "## Self-check" in tail + assert "- [ ]" in tail + + +def test_active_stage5_guidance_has_no_receipt_or_review_round_authority() -> None: + paths = [ + REPO_ROOT / "skills/orch-execute-plan/SKILL.md", + REPO_ROOT / "skills/orch-create-handoff/SKILL.md", + REPO_ROOT / "skills/orch-review-plan/SKILL.md", + REPO_ROOT / "references/assets/orchestration/workflow.md", + REPO_ROOT / "rules/orchestration/orch-handoff-required.md", + REPO_ROOT / "rules/orchestration/orch-review-completion.md", + REPO_ROOT / "rules/orchestration/orch-bounded-closure.md", + ] + forbidden = ("review receipt", "publication receipt", "current-authority sidecar", "begin-review-round") + for path in paths: + text = path.read_text(encoding="utf-8").lower() + assert not any(value in text for value in forbidden), path diff --git a/tests/test_orchestration_static_task_admission.py b/tests/test_orchestration_static_task_admission.py index 0fcc0c2..4323e88 100644 --- a/tests/test_orchestration_static_task_admission.py +++ b/tests/test_orchestration_static_task_admission.py @@ -1,9 +1,11 @@ from __future__ import annotations +import json import sys from pathlib import Path import pytest +import yaml REPO_ROOT = Path(__file__).resolve().parents[1] @@ -11,84 +13,136 @@ sys.path.insert(0, str(ORCHESTRATION)) import execution_context # noqa: E402 -import review_runtime # noqa: E402 -from test_orchestration_execution_context import workspace # noqa: E402 +from artifact_store import write_artifact # noqa: E402 +from test_orchestration_plans import CATALOG, _create_tree, workspace # noqa: E402 -def _plan(root: Path) -> Path: - return root / ".work-bundle/orchestration/plan/active/compiler-plan.md" +@pytest.fixture(autouse=True) +def canonical_compiler_root(workspace: Path, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(execution_context, "resolve_workspace_root", lambda _args: workspace) -def test_static_plan_admission_compiles_every_task_without_runtime_state(tmp_path: Path) -> None: - root, _spec, first = workspace(tmp_path) - second = first.with_name("task-005.md") - second.write_text( - first.read_text() - .replace("id: task-004", "id: task-005") - .replace("phase_id: phase-001\n", "phase_id: phase-001\ndepends_on: [task-004]\n") - .replace("task_id: task-004", "task_id: task-005"), - encoding="utf-8", +def test_static_plan_admission_compiles_canonical_yaml_task( + workspace: Path, tmp_path: Path, +) -> None: + plan, _phase, task = _create_tree(workspace, tmp_path) + + admitted = execution_context.static_plan_task_admission(workspace, plan) + + assert [item["task_id"] for item in admitted] == ["task-stage4"] + assert admitted[0]["source_ids"] == ["REQ-001A", "AC-001"] + assert task.suffixes == [".task", ".yaml"] + assert not (workspace / ".work-bundle/runtime").exists() + + +def test_compiler_uses_task_source_obligations_not_specification_body_syntax( + workspace: Path, tmp_path: Path, +) -> None: + plan, _phase, _task = _create_tree(workspace, tmp_path) + specification = ( + workspace + / ".work-bundle/orchestration/spec/active/spec-stage4.spec.md" ) - first.write_text( - first.read_text().replace( - "---\n\n# Task", - "accepted_result: result-task-004\nevidence_references: [VAL-004-observation]\n---\n\n# Task", - ), + text = specification.read_text(encoding="utf-8") + end = text.find("\n---\n", 4) + assert end > 0 + specification.write_text( + text[: end + 5] + + "# Prose only\n\nThe body deliberately contains no source-ID headings, bullets, or tables.\n", encoding="utf-8", ) - admitted = execution_context.static_plan_task_admission(root, _plan(root)) + admitted = execution_context.static_plan_task_admission(workspace, plan) - assert [item["task_id"] for item in admitted] == ["task-004", "task-005"] - assert not (root / ".work-bundle/runtime").exists() + assert admitted[0]["requirements"] == [ + "REQ-001A: Preserve exact specification authority in the compiled task.", + "AC-001: The canonical YAML task compiles from structured plan authority.", + ] -def test_static_plan_admission_rejects_missing_dependency(tmp_path: Path) -> None: - root, _spec, task = workspace(tmp_path) - task.write_text( - task.read_text().replace( - "phase_id: phase-001\n", "phase_id: phase-001\ndepends_on: [task-missing]\n" - ) +def test_static_plan_admission_rejects_unknown_dependency( + workspace: Path, tmp_path: Path, +) -> None: + plan, _phase, task = _create_tree(workspace, tmp_path) + data = yaml.safe_load(task.read_text()) + data["depends_on"] = ["task-missing"] + write_artifact( + CATALOG, "task", {"workspace_root": workspace}, data, state="active", + bindings={"plan": "plan-stage4", "phase": "phase-stage4"}, ) with pytest.raises(SystemExit, match="static-admission-blocked.*task-missing"): - execution_context.static_plan_task_admission(root, _plan(root)) - - -@pytest.mark.parametrize( - ("needle", "replacement", "message"), - [ - ("id: task-004", "id: task-004\nunsupported_contract: true", "unsupported"), - ( - "write: [scripts/orchestration/execution_context.py]", - "write: [orchestration/executions/plan-test/result.yaml]", - "execution artifact", - ), - ], -) -def test_static_plan_admission_rejects_known_static_contract_errors( - tmp_path: Path, needle: str, replacement: str, message: str + execution_context.static_plan_task_admission(workspace, plan) + + +def test_static_plan_admission_ignores_obsolete_markdown_candidate( + workspace: Path, tmp_path: Path, ) -> None: - root, _spec, task = workspace(tmp_path) - task.write_text(task.read_text().replace(needle, replacement), encoding="utf-8") + plan, _phase, _task = _create_tree(workspace, tmp_path) + legacy = plan.parent / "plan-stage4/phase-stage4/task-shadow.md" + legacy.write_text("---\nid: task-shadow\nplan_id: plan-stage4\nphase_id: phase-stage4\n---\n") + + admitted = execution_context.static_plan_task_admission(workspace, plan) - with pytest.raises(SystemExit, match=message): - execution_context.static_plan_task_admission(root, _plan(root)) + assert [item["task_id"] for item in admitted] == ["task-stage4"] -def test_plan_review_gate_runs_static_admission_before_acceptance(tmp_path: Path, monkeypatch) -> None: - root, _spec, task = workspace(tmp_path) - task.write_text( - task.read_text().replace( - "phase_id: phase-001\n", "phase_id: phase-001\ndepends_on: [task-missing]\n" - ) +def test_task_index_is_derived_and_rebuilt_before_compilation( + workspace: Path, tmp_path: Path, +) -> None: + plan, _phase, _task = _create_tree(workspace, tmp_path) + index = workspace / ".work-bundle/orchestration/plan/task-index.jsonl" + row = json.loads(index.read_text()) + row["phase_id"] = "phase-wrong" + index.write_text(json.dumps(row) + "\n") + + admitted = execution_context.static_plan_task_admission(workspace, plan) + + assert admitted[0]["task_id"] == "task-stage4" + + +def test_static_plan_admission_rejects_scope_escape( + workspace: Path, tmp_path: Path, +) -> None: + plan, _phase, task = _create_tree(workspace, tmp_path) + data = yaml.safe_load(task.read_text()) + data["target_files"] = ["../outside.py"] + write_artifact( + CATALOG, "task", {"workspace_root": workspace}, data, state="active", + bindings={"plan": "plan-stage4", "phase": "phase-stage4"}, + ) + + with pytest.raises(SystemExit, match="static-admission-blocked.*write scope"): + execution_context.static_plan_task_admission(workspace, plan) + + +def test_static_plan_admission_rejects_task_dependency_cycle( + workspace: Path, tmp_path: Path, +) -> None: + plan, phase, task = _create_tree(workspace, tmp_path) + phase_data = yaml.safe_load(phase.read_text()) + phase_data["task_index"] = [ + {"id": "task-stage4", "order": 1}, + {"id": "task-stage4-next", "order": 2}, + ] + write_artifact( + CATALOG, "phase", {"workspace_root": workspace}, phase_data, state="active", + bindings={"plan": "plan-stage4"}, + ) + first = yaml.safe_load(task.read_text()) + first["depends_on"] = ["task-stage4-next"] + write_artifact( + CATALOG, "task", {"workspace_root": workspace}, first, state="active", + bindings={"plan": "plan-stage4", "phase": "phase-stage4"}, ) - monkeypatch.setattr(review_runtime, "require_specification_review", lambda *_args, **_kwargs: None) - monkeypatch.setattr( - review_runtime, - "_require_current_review", - lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("review accepted before admission")), + second = yaml.safe_load(task.read_text()) + second.update({"id": "task-stage4-next", "name": "Next task", "order": 2}) + second["depends_on"] = ["task-stage4"] + second["evidence_capability"]["invariants"][0]["task_id"] = "task-stage4-next" + write_artifact( + CATALOG, "task", {"workspace_root": workspace}, second, state="active", + bindings={"plan": "plan-stage4", "phase": "phase-stage4"}, ) - with pytest.raises(SystemExit, match="static-admission-blocked"): - review_runtime.require_plan_reviews(root, _plan(root)) + with pytest.raises(SystemExit, match="static-admission-blocked: dependency cycle"): + execution_context.static_plan_task_admission(workspace, plan) diff --git a/tests/test_orchestration_subagent_ownership.py b/tests/test_orchestration_subagent_ownership.py index 2d6cfc4..e5ccf01 100644 --- a/tests/test_orchestration_subagent_ownership.py +++ b/tests/test_orchestration_subagent_ownership.py @@ -1,9 +1,6 @@ from __future__ import annotations from pathlib import Path -import json -import os -import subprocess import sys import pytest @@ -64,19 +61,6 @@ def wait(self, handle: object) -> object: return {"handoff": f"handoff-{handle}", "accepted": True} -def run_wb(config_root: Path, *args: str, cwd: Path) -> subprocess.CompletedProcess[str]: - env = os.environ.copy() - env["WB_CONFIG_ROOT"] = str(config_root) - return subprocess.run( - [sys.executable, str(REPO_ROOT / "scripts" / "wb.py"), *args], - cwd=cwd, - env=env, - check=False, - capture_output=True, - text=True, - ) - - def test_sg01_execute_plan_requires_implicit_subagent_ownership() -> None: adapter = RecordingAdapter() result = TaskOwnershipScheduler(adapter).run_wave( @@ -117,25 +101,6 @@ def test_sg03_legacy_preference_has_no_behavioral_effect( ) (registry / "projects.yaml").write_text("projects: []\n", encoding="utf-8") (registry / "skill-registry.yaml").write_text("skills: []\n", encoding="utf-8") - project = tmp_path / "project" - project.mkdir() - initialized = run_wb( - config_root, - "init-project", - str(project), - "--mode", - "single-repository", - cwd=project, - ) - assert initialized.returncode == 0, initialized.stdout + initialized.stderr - metadata = project / ".work-bundle" / "project.yaml" - metadata.write_text(metadata.read_text(encoding="utf-8") + preference, encoding="utf-8") - - shown = run_wb(config_root, "show-project", "--project-root", str(project), cwd=project) - assert shown.returncode == 0, shown.stdout + shown.stderr - assert "prefer_subagent" not in json.loads(shown.stdout) - assert "prefer_subagent" not in (project / "AGENTS.md").read_text(encoding="utf-8") - assert "prefer_subagent" not in json.loads(initialized.stdout) adapter = RecordingAdapter() TaskOwnershipScheduler(adapter).run_wave( diff --git a/tests/test_orchestration_workflow_contracts.py b/tests/test_orchestration_workflow_contracts.py index 0a4e2cb..50f2f8f 100644 --- a/tests/test_orchestration_workflow_contracts.py +++ b/tests/test_orchestration_workflow_contracts.py @@ -1,2396 +1,112 @@ from __future__ import annotations import argparse -import importlib.util import json from pathlib import Path -import subprocess import sys import pytest REPO_ROOT = Path(__file__).resolve().parents[1] -ORCH_ROOT = REPO_ROOT / "scripts" / "orchestration" -sys.path.insert(0, str(ORCH_ROOT)) - -from doctor import check_active_handoff_contract -from execution_context import ( - build_review_package, - evaluate_knowledge_closure_state, - validate_executor_result_for_task, -) -import execution_context -from handoffs import cmd_set_handoff_status, cmd_write_handoff, index_handoffs -import handoffs -from plans import _material_repository_root, _verified_handoff_tree - - -@pytest.fixture(autouse=True) -def accepted_stage_boundary_for_legacy_archive_unit_tests(monkeypatch): - """These tests isolate task evidence/knowledge/archive semantics. - - Real stage admission (including missing/stale review and source transitions) - is tested without this stub in test_orchestration_reviews.py. - """ - monkeypatch.setattr("plans.require_plan_reviews", lambda *args, **kwargs: None) - - -@pytest.fixture -def lower_level_handoff_writer(monkeypatch): - """Isolate lifecycle mechanics from managed creation admission.""" - monkeypatch.setattr("handoffs._managed_creation_admission", lambda *_args: None) +ORCHESTRATION = REPO_ROOT / "scripts" / "orchestration" +sys.path.insert(0, str(ORCHESTRATION)) + +from doctor import cmd_doctor # noqa: E402 +import dispatcher # noqa: E402 + + +CURRENT_STAGE5_COMMANDS = { + "write-executor-result", + "list-executor-results", + "transition-executor-result", + "index-executor-results", + "build-implementation-review-candidate", + "write-implementation-review", + "list-implementation-reviews", + "write-accepted-task-result", + "list-accepted-task-results", + "write-final-workflow-review", + "list-final-workflow-reviews", + "finalize-reviewed-plan", +} + +RETIRED_COMMANDS = { + "write-handoff", + "list-handoffs", + "set-handoff-status", + "index-handoffs", + "build-review-package", + "validate-executor-result", + "observe-task-validation", + "begin-review-round", + "complete-review-round", + "review-round-status", + "archive-plan", + "finalize-accepted-plan", + "finalize-with-blockers", +} def read(path: str) -> str: return (REPO_ROOT / path).read_text(encoding="utf-8") -def load_orchestration_dispatcher(): - previous_core = sys.modules.get("core") - core_spec = importlib.util.spec_from_file_location("core", ORCH_ROOT / "core.py") - assert core_spec is not None and core_spec.loader is not None - core_module = importlib.util.module_from_spec(core_spec) - sys.modules["core"] = core_module - try: - core_spec.loader.exec_module(core_module) - dispatcher_spec = importlib.util.spec_from_file_location( - "orchestration_workflow_contracts_dispatcher", ORCH_ROOT / "dispatcher.py" - ) - assert dispatcher_spec is not None and dispatcher_spec.loader is not None - dispatcher = importlib.util.module_from_spec(dispatcher_spec) - dispatcher_spec.loader.exec_module(dispatcher) - return dispatcher - finally: - if previous_core is None: - sys.modules.pop("core", None) - else: - sys.modules["core"] = previous_core - - -def evals() -> list[dict[str, object]]: - return json.loads(read("references/evals/orchestration/evals.json"))["evals"] - - -def handoff_args(tmp_path: Path, **overrides: object) -> argparse.Namespace: - values: dict[str, object] = { - "project_root": str(tmp_path), - "content_file": str(tmp_path / "handoff-content.txt"), - "type": "executor-result", - "status": "active", - "id": "handoff-exec-20990101-001", - "title": "Task Result", - "format": None, - "related_spec": "spec-001", - "related_plan": "plan-001", - "related_phase": "phase-001", - "related_task": "task-001", - } - values.update(overrides) - return argparse.Namespace(**values) - - -def archive_args(project_root: Path, plan_id: str, **overrides: object) -> argparse.Namespace: - values: dict[str, object] = { - "project_root": str(project_root), - "id": plan_id, - "mutation_events": [], - } - values.update(overrides) - return argparse.Namespace(**values) - - -def _task_brief(*, plan_id: str = "plan-001", task_id: str = "task-001") -> dict[str, object]: - return { - "task_id": task_id, - "plan_id": plan_id, - "source_ids": [], - "files": {"read": [], "write": []}, - "truth_basis": {}, - "validation": [], - "evidence_capability": { - "result": "no_validation_bearing_obligation", - "reason": "This structural fixture makes no validation-bearing closure claim.", - "invariants": [], - }, - "review_required": False, - } - - -def _completed_executor_result( - *, - plan: str | None = "plan-001", - task: str = "task-001", - acceptance_review: dict[str, object] | None = None, -) -> dict[str, object]: - related: dict[str, object] = {"task": task} - if plan is not None: - related["plan"] = plan - handoff: dict[str, object] = { - "type": "executor-result", - "related": related, - "result": {"state": "completed"}, - "task_fit_check": {"task": task, "result": "clean"}, - "delegation_evidence": { - "delegated": True, - "owner_kind": "subagent", - "agent_id": "workflow-contract-fixture-agent", - "run_id": "workflow-contract-fixture-run", - "mechanism": "host-native", - }, - "knowledge_disposition": { - "action": "none", - "reason": "No stable authority changed.", - "affected_authority": [], - }, - } - if acceptance_review is not None: - handoff["acceptance_review"] = acceptance_review - return handoff - - -def test_handoff_helper_rejects_unsupported_unmanaged_executor_creation(tmp_path: Path) -> None: - content = tmp_path / "handoff-content.txt" - content.write_text("result:\n state: completed\n summary: ok\n", encoding="utf-8") - with pytest.raises(SystemExit, match="managed WorkBundle workspace"): - cmd_write_handoff(handoff_args(tmp_path, content_file=str(content))) - assert not list((tmp_path / ".work-bundle/orchestration/handoff/executor").glob("*/*.yaml")) - - -def test_managed_handoff_creation_runs_complete_creation_projection( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch, -) -> None: - metadata = tmp_path / ".work-bundle/project.yaml" - metadata.parent.mkdir(parents=True) - metadata.write_text("metadata_version: 4\n", encoding="utf-8") - task = tmp_path / ".work-bundle/orchestration/plan/active/plan-001/phase-001/task-001.md" - task.parent.mkdir(parents=True) - task.write_text("---\nid: task-001\nplan_id: plan-001\n---\n", encoding="utf-8") - observed: list[tuple[dict[str, object], dict[str, object]]] = [] - monkeypatch.setattr( - execution_context, - "_compile_task_brief", - lambda _args: (task, {"task_brief": {"task_id": "task-001", "plan_id": "plan-001"}}), - ) - monkeypatch.setattr( - execution_context, - "validate_executor_result_creation_for_task", - lambda result, brief: observed.append((result, brief)), - ) - - handoffs._managed_creation_admission( - handoff_args(tmp_path), - "type: executor-result\nrelated: {plan: plan-001, task: task-001}\nresult: {state: completed}\n", - ) - - assert observed[0][0]["result"]["state"] == "completed" - assert observed[0][1] == {"task_id": "task-001", "plan_id": "plan-001"} - - -def test_handoff_creation_rejects_conflicting_writer_identity_before_mutation( - tmp_path: Path, -) -> None: - content = tmp_path / "handoff-content.txt" - content.write_text( - "id: conflicting-id\nresult:\n state: completed\n summary: no\n", - encoding="utf-8", - ) - args = handoff_args(tmp_path, content_file=str(content)) - - with pytest.raises(SystemExit, match="metadata mismatch for id"): - cmd_write_handoff(args) - - handoff_root = tmp_path / ".work-bundle/orchestration/handoff" - assert not list((handoff_root / "executor").glob("*/*.yaml")) - assert not (handoff_root / "index.jsonl").read_text(encoding="utf-8") - - -def test_handoff_lifecycle_preserves_marked_bytes_and_rebuilds_status( - tmp_path: Path, lower_level_handoff_writer, -) -> None: - content = tmp_path / "handoff-content.txt" - content.write_text("result:\n state: completed\n summary: ok\n", encoding="utf-8") - args = handoff_args(tmp_path, content_file=str(content)) - cmd_write_handoff(args) - initial = next(item for item in index_handoffs(args) if item["id"] == args.id) - initial_path = tmp_path / str(initial["path"]) - initial_bytes = initial_path.read_bytes() - assert b"lifecycle_authority: location-v1" in initial_bytes - - cmd_set_handoff_status(handoff_args(tmp_path, id=args.id, status="reviewed")) - reviewed = next(item for item in index_handoffs(args) if item["id"] == args.id) - reviewed_path = tmp_path / str(reviewed["path"]) - assert reviewed["status"] == "reviewed" - assert "/reviewed/" in reviewed_path.as_posix() - assert reviewed_path.read_bytes() == initial_bytes - - index_before = (tmp_path / ".work-bundle/orchestration/handoff/index.jsonl").read_bytes() - cmd_set_handoff_status(handoff_args(tmp_path, id=args.id, status="reviewed")) - assert reviewed_path.read_bytes() == initial_bytes - assert (tmp_path / ".work-bundle/orchestration/handoff/index.jsonl").read_bytes() == index_before - - -def test_legacy_explicit_return_to_active_uses_digest_bound_override(tmp_path: Path) -> None: - path = ( - tmp_path - / ".work-bundle/orchestration/handoff/executor/active/legacy-result.yaml" - ) - path.parent.mkdir(parents=True) - path.write_text( - "id: legacy-result\ntype: executor-result\nstatus: reviewed\n" - "related:\n plan: plan-001\n task: task-001\n", - encoding="utf-8", - ) - original = path.read_bytes() - args = handoff_args(tmp_path, id="legacy-result") - assert next(item for item in index_handoffs(args) if item["id"] == "legacy-result")["status"] == "reviewed" - - cmd_set_handoff_status(handoff_args(tmp_path, id="legacy-result", status="active")) - override = ( - tmp_path - / ".work-bundle/orchestration/handoff/legacy-status-overrides/legacy-result.json" - ) - record = json.loads(override.read_text(encoding="utf-8")) - assert record == { - "handoff_id": "legacy-result", - "related_plan": "plan-001", - "related_task": "task-001", - "sha256": __import__("hashlib").sha256(original).hexdigest(), - "status": "active", - "type": "executor-result", - } - assert path.read_bytes() == original - assert next(item for item in index_handoffs(args) if item["id"] == "legacy-result")["status"] == "active" - - -def test_handoff_index_fails_closed_on_duplicate_identity(tmp_path: Path) -> None: - active = tmp_path / ".work-bundle/orchestration/handoff/executor/active/result.yaml" - reviewed = tmp_path / ".work-bundle/orchestration/handoff/executor/reviewed/result.yaml" - active.parent.mkdir(parents=True) - reviewed.parent.mkdir(parents=True) - content = "id: duplicate\ntype: executor-result\nstatus: active\nlifecycle_authority: location-v1\n" - active.write_text(content, encoding="utf-8") - reviewed.write_text(content, encoding="utf-8") - - with pytest.raises(SystemExit, match="Duplicate handoff identity"): - index_handoffs(handoff_args(tmp_path)) - - -def _write_unmarked_legacy_handoff( - tmp_path: Path, - *, - name: str, - project: str, - status_location: str = "active", - phase: str = "phase-legacy", - task: str = "task-legacy", -) -> Path: - path = ( - tmp_path - / f".work-bundle/orchestration/handoff/executor/{status_location}/{name}.yaml" - ) - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text( - "id: legacy-duplicate\n" - "type: executor-result\n" - "status: active\n" - f"project: {project}\n" - "related:\n" - " plan: plan-legacy\n" - f" phase: {phase}\n" - f" task: {task}\n", - encoding="utf-8", - ) - return path - - -def test_handoff_index_preserves_unrelated_colocated_unmarked_legacy_duplicates( - tmp_path: Path, -) -> None: - first = _write_unmarked_legacy_handoff( - tmp_path, name="legacy-first", project="project-first" - ) - second = _write_unmarked_legacy_handoff( - tmp_path, name="legacy-second", project="project-second" - ) - - rows = [ - row - for row in index_handoffs(handoff_args(tmp_path)) - if row["id"] == "legacy-duplicate" - ] - - assert {row["project"] for row in rows} == {"project-first", "project-second"} - assert {tmp_path / str(row["path"]) for row in rows} == {first, second} - - -def test_handoff_index_preserves_same_project_colocated_unmarked_legacy_duplicate( - tmp_path: Path, -) -> None: - first = _write_unmarked_legacy_handoff( - tmp_path, - name="legacy-first", - project="same-project", - status_location="archived", - phase="phase-first", - task="task-first", - ) - second = _write_unmarked_legacy_handoff( - tmp_path, - name="legacy-second", - project="same-project", - status_location="archived", - phase="phase-second", - task="task-second", - ) +def test_current_dispatcher_exposes_stage5_cutover_without_legacy_aliases() -> None: + assert CURRENT_STAGE5_COMMANDS <= dispatcher.RECOGNIZED_COMMANDS + assert RETIRED_COMMANDS.isdisjoint(dispatcher.RECOGNIZED_COMMANDS) + help_text = dispatcher.build_parser().format_help() + for command in CURRENT_STAGE5_COMMANDS: + assert command in help_text + for command in RETIRED_COMMANDS: + assert command not in help_text - rows = [ - row - for row in index_handoffs(handoff_args(tmp_path)) - if row["id"] == "legacy-duplicate" - ] - assert {row["project"] for row in rows} == {"same-project"} - assert {row["related_phase"] for row in rows} == {"phase-first", "phase-second"} - assert {row["related_task"] for row in rows} == {"task-first", "task-second"} - assert {tmp_path / str(row["path"]) for row in rows} == {first, second} - - -def test_handoff_index_rejects_cross_location_unmarked_legacy_duplicate( - tmp_path: Path, -) -> None: - _write_unmarked_legacy_handoff( - tmp_path, name="legacy-first", project="project-first" - ) - _write_unmarked_legacy_handoff( - tmp_path, - name="legacy-second", - project="project-second", - status_location="reviewed", - ) - - with pytest.raises(SystemExit, match="Duplicate handoff identity"): - index_handoffs(handoff_args(tmp_path)) - - -def test_handoff_index_rejects_override_for_unmarked_legacy_duplicates( - tmp_path: Path, -) -> None: - first = _write_unmarked_legacy_handoff( - tmp_path, name="legacy-first", project="project-first" - ) - _write_unmarked_legacy_handoff( - tmp_path, name="legacy-second", project="project-second" - ) - override = ( - tmp_path - / ".work-bundle/orchestration/handoff/legacy-status-overrides/legacy-duplicate.json" - ) - override.parent.mkdir(parents=True) - override.write_text( - json.dumps( - { - "handoff_id": "legacy-duplicate", - "sha256": __import__("hashlib").sha256(first.read_bytes()).hexdigest(), - "type": "executor-result", - "related_plan": "plan-legacy", - "related_task": "task-legacy", - "status": "active", - } - ), - encoding="utf-8", - ) - - with pytest.raises(SystemExit, match="Duplicate handoff identity"): - index_handoffs(handoff_args(tmp_path)) - - -def test_handoff_status_change_rejects_ambiguous_legacy_duplicate( - tmp_path: Path, -) -> None: - first = _write_unmarked_legacy_handoff( - tmp_path, name="legacy-first", project="project-first" - ) - second = _write_unmarked_legacy_handoff( - tmp_path, name="legacy-second", project="project-second" - ) - originals = {first: first.read_bytes(), second: second.read_bytes()} - - with pytest.raises(SystemExit, match="ambiguous"): - cmd_set_handoff_status( - handoff_args(tmp_path, id="legacy-duplicate", status="reviewed") - ) - - assert {path: path.read_bytes() for path in originals} == originals - assert not ( - tmp_path - / ".work-bundle/orchestration/handoff/legacy-status-overrides/legacy-duplicate.json" - ).exists() - - -def test_handoff_creation_rejects_identity_owned_by_legacy_duplicates( - tmp_path: Path, lower_level_handoff_writer, -) -> None: - first = _write_unmarked_legacy_handoff( - tmp_path, name="legacy-first", project="project-first" - ) - second = _write_unmarked_legacy_handoff( - tmp_path, name="legacy-second", project="project-second" - ) - content = tmp_path / "handoff-content.txt" - content.write_text("result:\n state: completed\n", encoding="utf-8") - - with pytest.raises(SystemExit, match="Duplicate handoff identity"): - cmd_write_handoff( - handoff_args( - tmp_path, - id="legacy-duplicate", - content_file=str(content), - ) - ) - - assert first.is_file() - assert second.is_file() - assert not list( - (tmp_path / ".work-bundle/orchestration/handoff/executor/active").glob( - "legacy-duplicate-task-result.*" - ) - ) - - -@pytest.mark.parametrize("target_status", ["active", "reviewed", "superseded", "archived"]) -def test_marked_handoff_supports_every_target_status( - tmp_path: Path, target_status: str, lower_level_handoff_writer, -) -> None: - content = tmp_path / "handoff-content.txt" - content.write_text("result:\n state: blocked\n", encoding="utf-8") - args = handoff_args(tmp_path, content_file=str(content)) - cmd_write_handoff(args) - original_row = next(item for item in index_handoffs(args) if item["id"] == args.id) - original = (tmp_path / str(original_row["path"])).read_bytes() - - cmd_set_handoff_status(handoff_args(tmp_path, id=args.id, status=target_status)) - row = next(item for item in index_handoffs(args) if item["id"] == args.id) - assert row["status"] == target_status - assert f"/{target_status}/" in f"/{row['path']}" - assert (tmp_path / str(row["path"])).read_bytes() == original - - -def test_legacy_nonactive_to_active_survives_reindex_and_restart(tmp_path: Path) -> None: - path = tmp_path / ".work-bundle/orchestration/handoff/executor/superseded/legacy.yaml" - path.parent.mkdir(parents=True) - path.write_text( - "id: legacy-moved\ntype: executor-result\nstatus: reviewed\n" - "related:\n plan: plan-001\n task: task-001\n", - encoding="utf-8", - ) - original = path.read_bytes() - args = handoff_args(tmp_path, id="legacy-moved") - assert next(item for item in index_handoffs(args) if item["id"] == "legacy-moved")["status"] == "superseded" - - cmd_set_handoff_status(handoff_args(tmp_path, id="legacy-moved", status="active")) - active = tmp_path / ".work-bundle/orchestration/handoff/executor/active/legacy.yaml" - assert active.read_bytes() == original - assert next(item for item in index_handoffs(args) if item["id"] == "legacy-moved")["status"] == "active" - - -def test_legacy_override_digest_or_location_contradiction_fails_closed(tmp_path: Path) -> None: - path = tmp_path / ".work-bundle/orchestration/handoff/executor/active/legacy.yaml" - path.parent.mkdir(parents=True) - path.write_text( - "id: legacy-invalid\ntype: executor-result\nstatus: reviewed\n" - "related:\n plan: plan-001\n task: task-001\n", - encoding="utf-8", - ) - override = tmp_path / ".work-bundle/orchestration/handoff/legacy-status-overrides/legacy-invalid.json" - override.parent.mkdir(parents=True) - override.write_text(json.dumps({ - "handoff_id": "legacy-invalid", "sha256": "0" * 64, - "type": "executor-result", "related_plan": "plan-001", - "related_task": "task-001", "status": "reviewed", - }), encoding="utf-8") - - with pytest.raises(SystemExit, match="contradicts digest, binding, or location"): - index_handoffs(handoff_args(tmp_path)) - - -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_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" - 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": []}, - }, - {"task_id": "task-001", "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"}]}, - }, - {"task_id": "task-002", "files": {"write": ["feature.ts"]}}, - ), - ] - 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] - ) == 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-010", "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._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( - argparse.Namespace(project_root=str(tmp_path)), "plan-001", validated, [command] - ) - - -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" +def test_current_workflow_defines_direct_review_and_compact_final_audit() -> None: + workflow = read("references/assets/orchestration/workflow.md").lower() + for family in ( + "executor-result-v1", + "implementation-review-v1", + "accepted-task-result-v1", + "final-workflow-review-v1", ): - _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, lower_level_handoff_writer, -) -> None: - content = tmp_path / "handoff-content.txt" - content.write_text( - "related:\n task: task-001\nresult:\n state: completed\n summary: ok\n", - encoding="utf-8", - ) - cmd_write_handoff( - handoff_args(tmp_path, content_file=str(content), related_plan="plan-B", related_task="task-001") - ) - row = next(item for item in index_handoffs(handoff_args(tmp_path)) if item["id"] == "handoff-exec-20990101-001") - written = (tmp_path / row["path"]).read_text(encoding="utf-8") - assert "plan: plan-B" in written - assert "task: task-001" in written - - -def test_write_handoff_rejects_conflicting_plan_identity( - tmp_path: Path, lower_level_handoff_writer, -) -> None: - content = tmp_path / "handoff-content.txt" - content.write_text( - "related:\n plan: plan-A\n task: task-001\nresult:\n state: completed\n summary: ok\n", - encoding="utf-8", - ) - with pytest.raises(SystemExit, match="Handoff plan mismatch"): - cmd_write_handoff( - handoff_args(tmp_path, content_file=str(content), related_plan="plan-B", related_task="task-001") - ) - - -def test_write_handoff_rejects_nested_and_flat_plan_conflict( - tmp_path: Path, lower_level_handoff_writer, -) -> None: - content = tmp_path / "handoff-content.txt" - content.write_text( - "related:\n plan: plan-B\n task: task-001\nrelated_plan: plan-A\n" - "result:\n state: completed\n summary: ok\n", - encoding="utf-8", - ) - with pytest.raises(SystemExit, match="Handoff plan identity conflict"): - cmd_write_handoff( - handoff_args(tmp_path, content_file=str(content), related_plan="plan-B", related_task="task-001") - ) - - -def test_handoff_helper_rejects_active_orchestration_handoff(tmp_path: Path) -> None: - content = tmp_path / "handoff-content.txt" - content.write_text("# retired\n", encoding="utf-8") - args = handoff_args(tmp_path, content_file=str(content), type="orchestration", id="handoff-orch-20990101-001") - with pytest.raises(SystemExit, match="Active orchestration handoff creation is retired"): - cmd_write_handoff(args) - - -def test_doctor_rejects_forbidden_executor_fields_and_retired_handoffs(tmp_path: Path) -> None: - root = tmp_path / ".work-bundle/orchestration/handoff" - executor = root / "executor/active" - orchestration = root / "orchestration/active" - executor.mkdir(parents=True) - orchestration.mkdir(parents=True) - (executor / "bad.yaml").write_text("id: bad\nrecommended_next_actions: []\n", encoding="utf-8") - (orchestration / "bad.md").write_text("# retired\n", encoding="utf-8") - issues: list[str] = [] - check_active_handoff_contract(issues, tmp_path / ".work-bundle/orchestration") - assert any("forbidden field recommended_next_actions" in issue for issue in issues) - assert any("active orchestration handoff is retired" in issue for issue in issues) - - -def test_specification_contract_uses_semantic_loop_and_workspace_policy() -> None: - contract = read("references/assets/orchestration/contract/specification-v1.md") - for token in [ - "Initial User Purpose Evidence", - "Draft Requirement Breakdown", - "Source Context", - "Design Interrogation", - "Knowledge Base Update", - "Quality gate: verified|blocked", - "execution_workspace:", - "isolation: required|preferred|existing", - "semantic_loop:", - "dev-semantic-convergence", - "front-matter `source_knowledge` contains accepted authority only", - "Candidate, background, blocked, and superseded", - "AUTH-NNN: <carried constraint>", - ]: - assert token in contract - assert "Extra evidence loop" not in contract - - -def test_specification_contract_requires_bounded_impact_decisions() -> None: - contract = read("references/assets/orchestration/contract/specification-v1.md") - skill = read("skills/orch-create-specification/SKILL.md") - workflow = read("references/assets/orchestration/workflow.md") - evals = read("references/evals/orchestration/evals.json") - for text in (contract, skill, workflow): - for token in [ - "impact_decisions", - "accepted | excluded | blocking", - "none_relevant", - "stopping_reason", - "projects_to", - "current-state evidence", - "dirty work", - ]: - assert token in text - assert "durable knowledge" in text - assert "projects_to" in text and "stable" in text - assert "user-observable or contractual outcome" in text - assert "measurable quality target" in text - assert "Stop when further exploration could change none of those surfaces and record the reason" in text - for text in (contract, skill): - assert "blocking" in text and "open question" in text.lower() - assert "blocking relations prevent verification" in workflow - assert "impact-decision view" in skill - assert "keep repository traversal out of `dev-semantic-convergence`" in skill - assert "Git history, prior work artifacts, execution evidence, or durable knowledge" in skill - assert "user did not mention" in contract - assert "existing downstream consumer" in evals - assert "greenfield isolated utility" in evals - assert "mandatory full-history archaeology" in evals - assert "prior work artifacts, execution evidence, or durable knowledge" in evals - assert "related-but-non-material relation" in evals - - -def test_specification_contract_requires_bounded_excellence_applicability() -> None: - contract = read("references/assets/orchestration/contract/specification-v1.md") - skill = read("skills/orch-create-specification/SKILL.md") - workflow = read("references/assets/orchestration/workflow.md") - evals = read("references/evals/orchestration/evals.json") - for text in (contract, skill, workflow): - for token in [ - "excellence_applicability", - "no_material_opportunity", - "material_opportunities", - "accepted | rejected | deferred | not_material", - "Only accepted proposals", - "unanswered proposals become deferred", - "evidence", - "cost", - "risk", - "recommendation", - ]: - assert token in text - assert "universal checklist" in text - assert "one compact pass" in text - assert "accepting or rejecting it could change a requirement" in text - assert "user-observable or contractual outcome" in text - assert "measurable quality target" in text - assert "further exploration could change none of those surfaces" in text - assert "excellence-applicability view" in skill - plan_skill = read("skills/orch-create-implementation-plan/SKILL.md") - assert "EXC-*" in plan_skill or "EXC-" in plan_skill - assert "deferred" in plan_skill and "executor briefs" in plan_skill - assert "user-visible request with no evidenced adjacent improvement" in evals - assert "silently implements a deferred proposal" in evals - assert "accepted proposal" in evals and "stable authoritative" in evals - assert "universal product-quality checklist" in evals - assert "related-but-non-material adjacent idea" in evals - assert "source_ids: [EXC-001]" in evals - - -def test_planning_contract_allocates_evidence_capability() -> None: - plan = read("references/assets/orchestration/contract/plan-v1.md") - task = read("references/assets/orchestration/contract/task-v1.md") - skill = read("skills/orch-create-implementation-plan/SKILL.md") - workflow = read("references/assets/orchestration/workflow.md") - for text in (plan, task, skill, workflow): - assert "evidence_capability" in text - assert "no_validation_bearing_obligation" in text - for text in (task, skill, workflow): - assert "capability_reason" in text - assert "freshness" in text - assert "task" in text.lower() - assert "WOR-61 `none_relevant`" in skill - assert "lightest capable" in skill - assert "universal runtime" in workflow - - -def test_archive_plan_uses_accepted_execution_dispositions_as_knowledge_gate(tmp_path: Path) -> None: - from plans import cmd_archive_plan - from test_orchestration_execution_context import ACCEPTED_AUTHORITY, workspace, write_executor_handoff - - root, _, _ = workspace(tmp_path) - _append_plan_knowledge(root, closure_return="missing") - write_executor_handoff( - root, - f" action: update\n reason: Stable authority changed.\n affected_authority: [{ACCEPTED_AUTHORITY}]\n", - ) - - with pytest.raises(SystemExit, match="knowledge-blocked"): - cmd_archive_plan(archive_args(root, "plan-001")) - - assert (root / ".work-bundle/orchestration/plan/active/compiler-plan.md").is_file() - - -def test_archive_plan_completed_handoff_rejects_missing_harness_mutation_evidence( - tmp_path: Path, -) -> None: - from plans import cmd_archive_plan - from test_orchestration_execution_context import workspace, write_executor_handoff - - root, _, _ = workspace(tmp_path) - _append_plan_knowledge(root, closure_return="missing") - write_executor_handoff( - root, - " action: none\n" - " reason: No stable authority changed.\n" - " affected_authority: []\n", - ) - - with pytest.raises(SystemExit, match="mutation.*evidence|mutation_events"): - cmd_archive_plan(argparse.Namespace(project_root=str(root), id="plan-001")) - - assert (root / ".work-bundle/orchestration/plan/active/compiler-plan.md").is_file() - - -def test_archive_plan_cli_accepts_explicit_harness_mutation_evidence() -> None: - parsed = load_orchestration_dispatcher().build_parser().parse_args( - [ - "archive-plan", - "--id", - "plan-001", - "--mutation-events", - "[]", - ] - ) - - assert parsed.mutation_events == [] - - -def test_archive_plan_rejects_controller_task_scope_mutation_evidence( - tmp_path: Path, -) -> None: - from plans import cmd_archive_plan - from test_orchestration_execution_context import ( - WRITE_SCOPE_FILE, - workspace, - write_executor_handoff, - ) - - root, _, _ = workspace(tmp_path) - _append_plan_knowledge(root, closure_return="missing") - write_executor_handoff( - root, - " action: none\n" - " reason: No stable authority changed.\n" - " affected_authority: []\n", - ) - - with pytest.raises(SystemExit, match="controller mutated task-owned implementation scope"): - cmd_archive_plan( - archive_args( - root, - "plan-001", - mutation_events=[ - {"actor_kind": "controller", "paths": [WRITE_SCOPE_FILE]} - ], - ) - ) - - assert (root / ".work-bundle/orchestration/plan/active/compiler-plan.md").is_file() - - -@pytest.mark.parametrize( - ("verdict", "action", "closure_return"), - [ - ("repair", "update", "missing"), - ("accept", "none", "missing"), - ("accept", "reclassify", "completed"), - ], -) -def test_archive_plan_allows_only_resolved_or_non_triggering_dispositions( - tmp_path: Path, verdict: str, action: str, closure_return: str -) -> None: - from plans import cmd_archive_plan - - plan_root = tmp_path / ".work-bundle/orchestration/plan/active" - handoff_root = tmp_path / ".work-bundle/orchestration/handoff/executor/active" - plan_root.mkdir(parents=True) - handoff_root.mkdir(parents=True) - (plan_root / "plan.md").write_text( - "---\nid: plan-001\nstatus: Completed\n---\n\n" - "## 2.1 Knowledge Base Update Carry Forward\n\n" - "- **Disposition**: not-needed\n" - f"- **Closure return**: {closure_return}\n", - encoding="utf-8", - ) - (handoff_root / "task.yaml").write_text( - "id: handoff-001\ntype: executor-result\nstatus: active\n" - "related: {plan: plan-001, task: task-001}\n" - f"acceptance_review: {{verdict: {verdict}}}\n" - "knowledge_disposition:\n" - f" action: {action}\n" - " reason: Task-local evidence.\n" - f" affected_authority: {'[]' if action == 'none' else '[AUTH-001]'}\n", - encoding="utf-8", - ) - - cmd_archive_plan(archive_args(tmp_path, "plan-001")) - - assert (tmp_path / ".work-bundle/orchestration/plan/archived/plan.md").is_file() - - -def _write_archive_plan( - tmp_path: Path, - plan_id: str, - *, - disposition: str = "not-needed", - closure_return: str = "missing", -) -> None: - plan_root = tmp_path / ".work-bundle/orchestration/plan/active" - plan_root.mkdir(parents=True, exist_ok=True) - (plan_root / f"{plan_id}.md").write_text( - f"---\nid: {plan_id}\nstatus: Completed\n---\n\n" - "## 2.1 Knowledge Base Update Carry Forward\n\n" - f"- **Disposition**: {disposition}\n" - f"- **Closure return**: {closure_return}\n", - encoding="utf-8", - ) - task = plan_root / f"{plan_id}/phase-001/task.md" - task.parent.mkdir(parents=True, exist_ok=True) - task.write_text( - f"---\nid: task-001\nplan_id: {plan_id}\nphase_id: phase-001\nstatus: Completed\n---\n", - encoding="utf-8", - ) - - -FOLLOW_ON_WRITE_SCOPE_FILE = "scripts/orchestration/plans.py" -ARCHIVE_NEUTRAL_COMMAND = "env true" - - -def _append_plan_knowledge(root: Path, *, closure_return: str = "missing") -> None: - plan = root / ".work-bundle/orchestration/plan/active/compiler-plan.md" - plan.write_text( - plan.read_text(encoding="utf-8") - + "\n## 2.1 Knowledge Base Update Carry Forward\n\n" - + "- **Disposition**: not-needed\n" - + f"- **Closure return**: {closure_return}\n", - encoding="utf-8", - ) - - -def _append_plan_integration_command(root: Path, command: str) -> None: - plan = root / ".work-bundle/orchestration/plan/active/compiler-plan.md" - plan.write_text( - plan.read_text(encoding="utf-8") - + "\n## 7. Tests\n\n" - + "| ID | Test Type | Target | Related Phase | Can Run With | Command | Expected Result |\n" - + "|---|---|---|---|---|---|---|\n" - + f"| TEST-099 | integration | full harness | phase-001 | - | `{command}` | all tests pass |\n", - encoding="utf-8", - ) - - -def _write_follow_on_plan_task( - root: Path, *, task_id: str = "task-005", write_file: str = FOLLOW_ON_WRITE_SCOPE_FILE -) -> Path: - source = root / ".work-bundle/orchestration/plan/active/plan-001/phase-001/task-004.md" - task = source.with_name(f"{task_id}.md") - task.write_text( - source.read_text(encoding="utf-8") - .replace("id: task-004\n", f"id: {task_id}\n") - .replace( - "write: [scripts/orchestration/execution_context.py]\n", - f"write: [{write_file}]\n", - ), - encoding="utf-8", - ) - return task - - -def _git_commit_file(root: Path, relative: str, content: str, message: str) -> str: - from test_orchestration_execution_context import git - - path = root / relative - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(content, encoding="utf-8") - git(root, "add", "--", relative) - git(root, "commit", "-qm", message) - return git(root, "rev-parse", "HEAD") - - -def _git_write_tree(root: Path) -> str: - from test_orchestration_execution_context import git - - git(root, "add", "-A") - return git(root, "write-tree") - - -def _record_repository_commit(evidence: str, actual_commit: str) -> str: - marker = " status: clean\n" - assert marker in evidence - return evidence.replace( - marker, - marker + " metadata:\n" + f" actual_commit: {actual_commit}\n", - 1, - ) - - -def _complete_evidence_blocks(root: Path, *, actual_commit: str | None = None) -> str: - from test_orchestration_execution_context import evidence_blocks - - evidence = evidence_blocks(root) - return evidence if actual_commit is None else _record_repository_commit(evidence, actual_commit) - - -def _write_follow_on_executor_handoff( - root: Path, - *, - task_id: str, - created_at: str, - write_file: str = FOLLOW_ON_WRITE_SCOPE_FILE, - extra_command: str | None = None, - extra_result: str = "passed", - actual_commit: str | None = None, - reviewed_head: str | None = None, -) -> Path: - from test_orchestration_execution_context import TASK_VALIDATION_COMMAND - - extra = "" if extra_command is None else f" - {{command: {extra_command}, result: {extra_result}}}\n" - review = "" if reviewed_head is None else f"acceptance_review: {{reviewed_head: {reviewed_head}}}\n" - evidence = _complete_evidence_blocks(root, actual_commit=actual_commit) - handoff = root / f".work-bundle/orchestration/handoff/executor/active/handoff-{task_id}.yaml" - handoff.parent.mkdir(parents=True, exist_ok=True) - handoff.write_text( - f"id: handoff-{task_id}\n" - "type: executor-result\n" - f"created_at: {created_at}\n" - f"related: {{plan: plan-001, task: {task_id}}}\n" - "result: {state: completed}\n" - f"task_fit_check: {{task: {task_id}, result: clean}}\n" - "changes:\n" - " files:\n" - f" - {{path: {write_file}, action: modified}}\n" - "validation:\n" - " commands:\n" - f" - {{command: {TASK_VALIDATION_COMMAND}, result: passed}}\n" - f"{extra}" - f"{review}" - f"{evidence}" - "knowledge_disposition:\n" - " action: none\n" - " reason: No stable authority changed.\n" - " affected_authority: []\n", - encoding="utf-8", - ) - return handoff - - -def _write_earlier_integration_pass(root: Path, command: str, *, created_at: str) -> Path: - from test_orchestration_execution_context import TASK_VALIDATION_COMMAND, git, write_executor_handoff - - try: - head = git(root, "rev-parse", "HEAD") - except subprocess.CalledProcessError: - git(root, "add", ".") - git(root, "commit", "-qm", "integration acceptance baseline") - head = git(root, "rev-parse", "HEAD") - - handoff = write_executor_handoff( - root, - " action: none\n reason: No stable authority changed.\n affected_authority: []\n", - ) - content = ( - handoff.read_text(encoding="utf-8") - .replace("id: handoff-task-004\n", f"id: handoff-task-004\ncreated_at: {created_at}\n") - .replace( - f"- {{command: {TASK_VALIDATION_COMMAND}, result: passed}}\n", - f"- {{command: {TASK_VALIDATION_COMMAND}, result: passed}}\n" - f" - {{command: {command}, result: passed}}\n", - ) - ) - handoff.write_text(_record_repository_commit(content, head), encoding="utf-8") - return handoff - - -def _write_archive_handoff( - tmp_path: Path, - filename: str, - related: str, - *, - verdict: str | None = "accept", - action: str = "update", - location: str = "active", - result_state: str | None = None, -) -> None: - handoff_root = tmp_path / ".work-bundle/orchestration/handoff/executor" / location - handoff_root.mkdir(parents=True, exist_ok=True) - affected = "[]" if action == "none" else "[AUTH-001]" - review_line = "" if verdict is None else f"acceptance_review: {{verdict: {verdict}}}\n" - result_line = "" if result_state is None else f"result: {{state: {result_state}}}\n" - (handoff_root / filename).write_text( - f"id: {filename.rsplit('.', 1)[0]}\ntype: executor-result\nstatus: {location}\n" - f"related: {related}\n" - f"{result_line}" - f"{review_line}" - "knowledge_disposition:\n" - f" action: {action}\n" - " reason: Task-local evidence.\n" - f" affected_authority: {affected}\n", - encoding="utf-8", - ) - - -def test_archive_plan_ignores_foreign_plan_handoff_with_colliding_task_id(tmp_path: Path) -> None: - from plans import cmd_archive_plan - - _write_archive_plan(tmp_path, "plan-A") - _write_archive_plan(tmp_path, "plan-B") - _write_archive_handoff(tmp_path, "plan-a.yaml", "{plan: plan-A, task: task-001}") - - cmd_archive_plan(archive_args(tmp_path, "plan-B")) - - assert (tmp_path / ".work-bundle/orchestration/plan/archived/plan-B.md").is_file() - assert (tmp_path / ".work-bundle/orchestration/plan/active/plan-A.md").is_file() - - -def test_archive_plan_skips_unrelated_unparseable_executor_yaml(tmp_path: Path) -> None: - from plans import cmd_archive_plan - - _write_archive_plan(tmp_path, "plan-A") - _write_archive_plan(tmp_path, "plan-B") - handoff_root = tmp_path / ".work-bundle/orchestration/handoff/executor/active" - handoff_root.mkdir(parents=True, exist_ok=True) - (handoff_root / "foreign-block.yaml").write_text( - "id: foreign\n" - "type: executor-result\n" - "related:\n" - " plan: plan-A\n" - " task: task-001\n" - "summary: >\n" - " unsupported folded scalar\n", - encoding="utf-8", - ) - (handoff_root / "foreign-inline.yaml").write_text( - "id: foreign-inline\n" - "type: executor-result\n" - "related: {plan: plan-A, task: task-001}\n" - "summary: >\n" - " unsupported folded scalar\n", - encoding="utf-8", - ) - - cmd_archive_plan(archive_args(tmp_path, "plan-B")) - - assert (tmp_path / ".work-bundle/orchestration/plan/archived/plan-B.md").is_file() - assert (tmp_path / ".work-bundle/orchestration/plan/active/plan-A.md").is_file() - - -def test_archive_plan_ignores_task_only_handoff_with_ambiguous_task_id(tmp_path: Path) -> None: - from plans import cmd_archive_plan - - _write_archive_plan(tmp_path, "plan-B") - _write_archive_handoff(tmp_path, "task-only.yaml", "{task: task-001}") - - cmd_archive_plan(archive_args(tmp_path, "plan-B")) - - assert (tmp_path / ".work-bundle/orchestration/plan/archived/plan-B.md").is_file() - - -def test_archive_plan_ignores_archived_foreign_handoff_with_colliding_task_id(tmp_path: Path) -> None: - from plans import cmd_archive_plan - - _write_archive_plan(tmp_path, "plan-A") - _write_archive_plan(tmp_path, "plan-B") - _write_archive_handoff( - tmp_path, "historical.yaml", "{plan: plan-A, task: task-001}", location="archived" - ) - - cmd_archive_plan(archive_args(tmp_path, "plan-B")) - - assert (tmp_path / ".work-bundle/orchestration/plan/archived/plan-B.md").is_file() - - -def test_archive_plan_same_plan_accepted_update_still_blocks_unresolved_closure(tmp_path: Path) -> None: - from plans import cmd_archive_plan - from test_orchestration_execution_context import ACCEPTED_AUTHORITY, workspace, write_executor_handoff - - root, _, _ = workspace(tmp_path) - _append_plan_knowledge(root, closure_return="missing") - write_executor_handoff( - root, - f" action: update\n reason: Task-local evidence.\n affected_authority: [{ACCEPTED_AUTHORITY}]\n", - ) - - with pytest.raises(SystemExit, match="knowledge-blocked"): - cmd_archive_plan(archive_args(root, "plan-001")) - - assert (root / ".work-bundle/orchestration/plan/active/compiler-plan.md").is_file() - - -def test_archive_plan_same_plan_resolved_closure_allows_archive(tmp_path: Path) -> None: - from plans import cmd_archive_plan - - _write_archive_plan(tmp_path, "plan-B", closure_return="completed") - _write_archive_handoff(tmp_path, "plan-b.yaml", "{plan: plan-B, task: task-001}") - - cmd_archive_plan(archive_args(tmp_path, "plan-B")) - - assert (tmp_path / ".work-bundle/orchestration/plan/archived/plan-B.md").is_file() - - -def test_task_contract_compiles_methodology_capability_and_review() -> None: - contract = read("references/assets/orchestration/contract/task-v1.md") - for token in [ - "source_ids:", - "truth_basis:", - "as_is_evidence:", - "decision_authority:", - "expected_delta:", - "conflict_status: clear|escalate", - "decision-blocked", - "semantically distinct from generic `source_ids`", - "none-relevant", - "verified specification's accepted `source_knowledge`", - "AUTH-NNN: <carried constraint>", - "methodology:", - "tdd|systematic-debugging|direct|loop-coding", - "executor_profile:", - "mechanical|standard|judgment", - "context_mode: compiled-brief", - "after_failed_repairs: 2", - "acceptance_review:", - "verdict: pending", - "Fresh task validation evidence exists", - "acceptance_review.verdict", - "EXC-*", - "executor briefs", - ]: - assert token in contract - - -def test_task_contract_defines_material_review_freshness_without_identity_rotation() -> None: - contract = read("references/assets/orchestration/contract/task-v1.md") - - for token in [ - "`review_reset` bound to the prior review, classified reason, and current target and evidence", - "may reuse the same agent identity", - "judgment-capable", - "authorship/repair/decision/deliberation participation", - "review provenance", - ]: - assert token in contract - assert "may not reuse the repair reviewer identity" not in contract - - -def test_executor_result_contract_keeps_review_authority_outside_handoff() -> None: - contract = read("references/assets/orchestration/contract/handoff-executor-result-v1.md") - for token in [ - "acceptance_review:", - "lifecycle_authority: location-v1", - "wrong-owner fields", - "accepted-result materialization later joins", - "complete bytes never change after creation", - "legacy-status-overrides/<handoff-id>.json", - "knowledge_disposition:", - "none | update | supersede | reclassify", - "review owns any approved persistence follow-up", - "must not name knowledge paths or any `ks-*` skill", - "exact paths already present in the compiled task scope", - "allocated `AUTH-NNN` aliases", - "related.plan", - "must equal the assigned task's `plan_id` and `id`", - "fails closed before `Completed` and before `build-review-package`", - "current lifecycle status comes from the status-specific location", - ]: - assert token in contract - - -def test_workflow_separates_durable_artifacts_from_runtime_packets() -> None: - workflow = read("references/assets/orchestration/workflow.md") - for token in [ - "Disposable task briefs, review packages, and lightweight development plans", - ".work-bundle/runtime/", - "no active/archive/index lifecycle", - "build-task-brief", - "Missing source IDs fail closed", - "Full specification, root-plan, and phase reading is an escalation path", - "Execution remains no-retrieval", - "AUTH-NNN: <carried constraint>", - "same five-field Truth Basis", - "earliest ordinary task", - "knowledge disposition", - "review owns approved persistence", - "expected total orchestration cost", - "accepted task dispositions", - ]: - assert token in workflow - - -def test_workflow_assigns_review_ownership_and_repair_loop() -> None: - workflow = read("references/assets/orchestration/workflow.md") - for token in [ - "Product reviewers judge accepted product requirements", - "Schedulers own dependencies", - "they do not perform code-quality review", - "requires a subagent owner for every task", - "fails closed before task mutation", - "dispatch before any wait", - "one scoped rereview", - "A task becomes `Completed` only when", - "Review-required tasks additionally require exact stored `accept` authority", - "optional task review when compiled review_required: true", - "accepted Truth Basis", - "normalized validation observations", - ]: - assert token in workflow - - -def test_workflow_uses_accepted_results_without_lifecycle_replay() -> None: - workflow = read("references/assets/orchestration/workflow.md") - for token in [ - "acceptance once", - "compact accepted result", - "historical handoff chains", - "transient acceptance evidence", - "current harness observation", - "reviewer infrastructure or provider failure", - "same immutable review package", - "previous finding/evidence frontier", - "status-only or append-only evidence", - "causal class", - "first owning layer", - "exact baseline and endpoint", - "issue-run artifacts", - ]: - assert token in workflow - - -def test_review_rule_uses_typed_resume_routing() -> None: - rule = read("rules/orchestration/orch-review-completion.md") - for token in [ - "knowledge-blocked", - "repository-blocked", - "workspace-blocked", - "Route missing evidence to its first owner", - "publication-only/control resume", - "plan repair only for a decomposition defect", - "specification repair only for a requirement, design, or authority defect", - "Do not create a repair specification for every failed review gate", - "accepted `update`, `supersede`, or `reclassify`", - "rejected dispositions", - "archive", - "evidence_capability", - "INV/VAL", - "incapable green", - "pre-closure oracle-capability check", - "no_validation_bearing_obligation", - "WOR-59 G9 remains the unchanged post-execution classifier", - ]: - assert token in rule - - -def test_workflow_preserves_repository_codegraph_workspace_and_secret_safety() -> None: - workflow = read("references/assets/orchestration/workflow.md") - for token in [ - ".work-bundle/project.yaml", - "Never stash, reset, clean, restore, delete, or overwrite user work", - "CodeGraph first only when a target contains `.codegraph/`", - "Record `no-index`", - "Never delete user or harness workspaces", - "Never copy credential values", - "credential-inject", - ]: - assert token in workflow - - -def test_evals_cover_twenty_migration_behaviors() -> None: - cases = evals() - prompts = "\n".join(str(case["prompt"]) for case in cases) - expected = "\n".join(str(case["expected_output"]) for case in cases) - for token in [ - "accepted independent task review", - "no task-review verdict", - "wrong API requirement", - "Knowledge Base Update disposition required", - "semantic view finds one missing constraint", - "omits one spec ID", - "low-judgment two-file implementation", - "Compile a task brief", - "changes testable production behavior", - "configuration-only task", - "unexpected retry bug", - "independent task reviewer", - "second repeated repair rejection", - "lightweight mechanical plan", - "provenance owner is user", - "credential-inject", - "Hydrate .codegraph", - "before the final edit", - "durable knowledge update is unresolved", - "compiled brief is valid", - "ordinary characterization task", - "conflict_status escalate", - "asks to invoke a ks-* writer", - "implementation and tests do not match", - "device-local checkout observations disagree", - ]: - assert token in prompts - for token in [ - "review-blocked", - "specification repair", - "knowledge-blocked", - "semantic_loop", - "capability mechanical", - "fails closed on missing IDs", - "systematic debugging", - "escalates to full orchestration", - "Refuses deletion", - "does not blindly copy or symlink", - "evidence is stale", - "full specification, root-plan, and phase reads", - ]: - assert token in expected - - -def test_no_review_completed_handoff_does_not_require_accept_or_reviewer() -> None: - execute = read("skills/orch-execute-plan/SKILL.md") - for token in [ - "A review-required task additionally needs exact stored `accept` authority", - "assign `dev-code-review` only when compiled `review_required: true`", - "Skip this hop when review is not required", - ]: - assert token in execute - assert "verdict: accept" not in str(_completed_executor_result()) - - validated = validate_executor_result_for_task( - _completed_executor_result(), _task_brief(), mutation_events=[] - ) - assert validated["result_state"] == "completed" - assert validated["knowledge_disposition"]["action"] == "none" - - -def test_execute_plan_requires_bound_observation_and_isolate_or_serialize() -> None: - execute = read("skills/orch-execute-plan/SKILL.md") - plan = read("skills/orch-create-implementation-plan/SKILL.md") - contract = read("references/assets/orchestration/contract/handoff-executor-result-v1.md") - for token in [ - "harness-owned task execution binding", - "capture the pre-task baseline once", - "cannot supply or replace that baseline", - "bound execution repository", - "isolate via prepare_worktree or serialize", - "Git-state-neutral", - "validate-executor-result", - ]: - assert token in execute - assert "mutating siblings on the same execution path isolate via prepare_worktree or serialize" in plan - assert "cannot supply or replace that baseline" in contract - assert "corroboration" in contract - assert "not independent proof" in contract or "not authority" in contract - - -def test_optional_review_package_does_not_absorb_sibling_task_files( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - from test_orchestration_execution_context import ( - WRITE_SCOPE_FILE, - args as review_args, - git, - workspace, - write_executor_handoff, - ) - - root, _, task_b = workspace(tmp_path) - scoped = root / WRITE_SCOPE_FILE - scoped.parent.mkdir(parents=True, exist_ok=True) - scoped.write_text("def compile_task():\n return 'old'\n", encoding="utf-8") - task_a_file = root / "src/task_a.py" - task_a_file.parent.mkdir(parents=True, exist_ok=True) - task_a_file.write_text("TASK_A_OLD = 1\n", encoding="utf-8") - git(root, "add", ".") - git(root, "commit", "-qm", "base") - base = git(root, "rev-parse", "HEAD") - scoped.write_text("def compile_task():\n return 'task-b'\n", encoding="utf-8") - task_a_file.write_text("TASK_A_NEW = 2\n", encoding="utf-8") - from test_orchestration_execution_context import _bind_passing_observation - - monkeypatch.setattr("review_runtime.require_plan_reviews", lambda *_args: None) - handoff = _bind_passing_observation(root, task_b) - - package = build_review_package( - review_args(root, task_b, handoff=str(handoff), base=base, head="worktree") - ).read_text(encoding="utf-8") - diff = package.split("## Diff", 1)[1].split("## Out-of-scope changes", 1)[0] - diagnostics = package.split("## Out-of-scope changes", 1)[1].split("## ", 1)[0] - - assert "## Out-of-scope changes" in package - assert "return 'task-b'" in diff - assert "TASK_A_NEW" not in diff - assert "src/task_a.py" not in diff - assert "src/task_a.py" in diagnostics - assert WRITE_SCOPE_FILE in package.split("## Changed files", 1)[1].split("## ", 1)[0] - - -def test_failing_declared_plan_acceptance_blocks_archive_without_second_reviewer(tmp_path: Path) -> None: - from plans import cmd_archive_plan - - review = read("skills/orch-review-plan/SKILL.md") - workflow = read("references/assets/orchestration/workflow.md") - for token in [ - "declared plan-level/integration acceptance observed on the final integrated workspace", - "do not start another implementation-review agent to produce plan-level acceptance", - "Archive remains blocked while any required knowledge, validation, review", - ]: - assert token in review - for token in [ - "declared plan-level/integration acceptance is recorded", - "It does not redo task code review, reread implementation for code quality, or start another implementation-review agent", - "Missing stored review authority is not a blocker when no compiled task set `review_required: true`", - ]: - assert token in workflow - - command = "uv run --with pytest pytest -q tests/test_plan_acceptance.py" - _write_archive_plan(tmp_path, "plan-B") - plan = tmp_path / ".work-bundle/orchestration/plan/active/plan-B.md" - plan.write_text( - plan.read_text(encoding="utf-8") - + "\n## 7. Tests\n\n" - + "| ID | Test Type | Target | Related Phase | Can Run With | Command | Expected Result |\n" - + "|---|---|---|---|---|---|---|\n" - + f"| TEST-099 | integration | full harness | phase-001 | - | `{command}` | all tests pass |\n", - encoding="utf-8", - ) - _write_archive_handoff( - tmp_path, - "plan-b.yaml", - "{plan: plan-B, task: task-001}", - verdict=None, - action="none", - result_state="completed", - ) - handoff = tmp_path / ".work-bundle/orchestration/handoff/executor/active/plan-b.yaml" - handoff.write_text( - handoff.read_text(encoding="utf-8") - + "validation:\n" - + " commands:\n" - + f" - {{command: {command}, result: failed}}\n", - encoding="utf-8", - ) - - with pytest.raises(SystemExit, match="acceptance-blocked"): - cmd_archive_plan(archive_args(tmp_path, "plan-B")) - - assert (tmp_path / ".work-bundle/orchestration/plan/active/plan-B.md").is_file() - - -def _mapped_archive_workspace( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -): - from test_orchestration_execution_context import ( - PASSING_PROCESS, - _bind_task_execution, - _compiled_brief, - _handoff_for_command, - _set_task_validation, - git, - workspace, - ) - - command = ARCHIVE_NEUTRAL_COMMAND - root, _, task = workspace(tmp_path) - task.write_text( - task.read_text(encoding="utf-8").replace( - "evidence_capability:\n" - " result: no_validation_bearing_obligation\n" - " reason: This shared fixture leaves capability semantics to scenario-specific tests.\n" - " invariants: []\n", - "evidence_capability:\n" - " result: mapped\n" - " reason: Archive re-entry must observe mapped invariants.\n" - " invariants:\n" - " - {id: INV-001, source_ids: [REQ-003], invariant: Observable behavior, boundary: integration, oracle: VAL-001, capability_reason: Process oracle distinguishes violation., freshness: current_task_batch, task_id: task-004, evidence_ids: [VAL-001], closure_result: pending}\n", - ), - encoding="utf-8", - ) - _set_task_validation( - task, - "validation:\n" - f" - {{kind: process, command: {json.dumps(PASSING_PROCESS)}, proves: TEST-004, expected: passed, id: VAL-001, invariant_ids: [INV-001], capability_reason: Process oracle distinguishes violation.}}\n", - ) - _append_plan_knowledge(root, closure_return="missing") - _append_plan_integration_command(root, command) - git(root, "add", ".") - git(root, "commit", "-qm", "mapped archive workspace") - brief = _compiled_brief(root, task) - monkeypatch.setattr("review_runtime.require_plan_reviews", lambda *_args: None) - binding = _bind_task_execution(root, brief) - handoff = _handoff_for_command(root, PASSING_PROCESS) - handoff.write_text( - handoff.read_text(encoding="utf-8").replace( - f"- {{command: {json.dumps(PASSING_PROCESS)}, result: passed}}\n", - f"- {{command: {json.dumps(PASSING_PROCESS)}, result: passed, id: VAL-001, invariant_ids: [INV-001]}}\n" - f" - {{command: {command}, result: passed}}\n", - ) - + "evidence_closure:\n" - + " result: passed\n" - + " invariants:\n" - + " - {id: INV-001, boundary: integration, freshness: current_task_batch, evidence_ids: [VAL-001], closure_result: passed}\n", - encoding="utf-8", - ) - return root, binding, command - - -def test_archive_plan_accepts_mapped_invariant_handoff_with_harness_observation( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - from plans import cmd_archive_plan - - root, _binding, _command = _mapped_archive_workspace(tmp_path, monkeypatch) - - cmd_archive_plan(archive_args(root, "plan-001")) - - assert (root / ".work-bundle/orchestration/plan/archived/compiler-plan.md").is_file() - - -def test_archive_plan_does_not_replay_task_validation_from_execution_binding( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - from plans import cmd_archive_plan - - root, binding, command = _mapped_archive_workspace(tmp_path, monkeypatch) - - cmd_archive_plan( - archive_args( - root, - "plan-001", - workspace_id=binding["workspace_id"], - execution_id=binding["execution_id"], - repository_id=binding["repository_id"], - execution_runtime_root=binding["runtime_root"], - ) - ) - assert (root / ".work-bundle/orchestration/plan/archived/compiler-plan.md").is_file() - - restored = tmp_path / "restored-mapped" - restored.mkdir() - restored_root, restored_binding, _command = _mapped_archive_workspace( - restored, monkeypatch - ) - cmd_archive_plan( - archive_args( - restored_root, - "plan-001", - workspace_id="wrong-workspace", - execution_id=restored_binding["execution_id"], - repository_id=restored_binding["repository_id"], - execution_runtime_root=restored_binding["runtime_root"], - ) - ) - assert (restored_root / ".work-bundle/orchestration/plan/archived/compiler-plan.md").is_file() - - -def test_passing_declared_plan_acceptance_allows_archive_without_second_reviewer(tmp_path: Path) -> None: - from plans import cmd_archive_plan - from test_orchestration_execution_context import ( - TASK_VALIDATION_COMMAND, - git, - workspace, - write_executor_handoff, - ) - - command = ARCHIVE_NEUTRAL_COMMAND - root, _, _ = workspace(tmp_path) - _append_plan_knowledge(root, closure_return="missing") - plan = root / ".work-bundle/orchestration/plan/active/compiler-plan.md" - plan.write_text( - plan.read_text(encoding="utf-8") - + "\n## 7. Tests\n\n" - + "| ID | Test Type | Target | Related Phase | Can Run With | Command | Expected Result |\n" - + "|---|---|---|---|---|---|---|\n" - + f"| TEST-099 | integration | full harness | phase-001 | - | `{command}` | all tests pass |\n", - encoding="utf-8", - ) - git(root, "add", ".") - git(root, "commit", "-qm", "final workspace") - handoff = write_executor_handoff( - root, - " action: none\n reason: No stable authority changed.\n affected_authority: []\n", - ) - handoff.write_text( - handoff.read_text(encoding="utf-8").replace( - f"- {{command: {TASK_VALIDATION_COMMAND}, result: passed}}\n", - f"- {{command: {TASK_VALIDATION_COMMAND}, result: passed}}\n" - f" - {{command: {command}, result: passed}}\n", - ), - encoding="utf-8", - ) - - cmd_archive_plan(archive_args(root, "plan-001")) - - assert (root / ".work-bundle/orchestration/plan/archived/compiler-plan.md").is_file() - - -def test_unvalidated_handoff_cannot_satisfy_declared_plan_acceptance(tmp_path: Path) -> None: - from plans import cmd_archive_plan - - command = "uv run --with pytest pytest -q tests/test_plan_acceptance.py" - _write_archive_plan(tmp_path, "plan-B") - plan = tmp_path / ".work-bundle/orchestration/plan/active/plan-B.md" - plan.write_text( - plan.read_text(encoding="utf-8") - + "\n## 7. Tests\n\n" - + "| ID | Test Type | Target | Related Phase | Can Run With | Command | Expected Result |\n" - + "|---|---|---|---|---|---|---|\n" - + f"| TEST-099 | integration | full harness | phase-001 | - | `{command}` | all tests pass |\n", - encoding="utf-8", - ) - _write_archive_handoff( - tmp_path, - "plan-b.yaml", - "{plan: plan-B, task: task-001}", - verdict=None, - action="none", - result_state="completed", - ) - handoff = tmp_path / ".work-bundle/orchestration/handoff/executor/active/plan-b.yaml" - handoff.write_text( - handoff.read_text(encoding="utf-8") - + "validation:\n" - + " commands:\n" - + f" - {{command: {command}, result: passed}}\n", - encoding="utf-8", - ) - - with pytest.raises(SystemExit, match="acceptance-blocked"): - cmd_archive_plan(archive_args(tmp_path, "plan-B")) - - -def _record_tree_fresh_integration_pass(root: Path, command: str) -> str: - from test_orchestration_execution_context import TASK_VALIDATION_COMMAND, git, write_executor_handoff - - git(root, "add", ".") - git(root, "commit", "-qm", "tree-fresh acceptance baseline") - head = git(root, "rev-parse", "HEAD") - handoff = write_executor_handoff( - root, - " action: none\n reason: No stable authority changed.\n affected_authority: []\n", - ) - content = handoff.read_text(encoding="utf-8").replace( - f"- {{command: {TASK_VALIDATION_COMMAND}, result: passed}}\n", - f"- {{command: {TASK_VALIDATION_COMMAND}, result: passed}}\n" - f" - {{command: {command}, result: passed}}\n", - ) - handoff.write_text(_record_repository_commit(content, head), encoding="utf-8") - return head - - -def test_tree_fresh_executor_pass_without_harness_observation_blocks_archive(tmp_path: Path) -> None: - from plans import cmd_archive_plan - from test_orchestration_execution_context import workspace - - command = "env false" - root, _, _ = workspace(tmp_path) - _append_plan_knowledge(root, closure_return="missing") - _append_plan_integration_command(root, command) - _record_tree_fresh_integration_pass(root, command) - - with pytest.raises(SystemExit, match="acceptance-blocked"): - cmd_archive_plan(archive_args(root, "plan-001")) - - assert (root / ".work-bundle/orchestration/plan/active/compiler-plan.md").is_file() - - -def test_task_worktree_command_pass_cannot_archive_different_final_workspace(tmp_path: Path) -> None: - from plans import cmd_archive_plan - from test_orchestration_execution_context import git, workspace - - command = "test -f worktree-only-marker" - root, _, _ = workspace(tmp_path) - _append_plan_knowledge(root, closure_return="missing") - _append_plan_integration_command(root, command) - git(root, "add", ".") - git(root, "commit", "-qm", "final workspace") - worktree = tmp_path / "isolated-task-worktree" - git(root, "worktree", "add", str(worktree), "HEAD") - (worktree / "worktree-only-marker").write_text("isolated\n", encoding="utf-8") - _write_earlier_integration_pass(root, command, created_at="2026-08-17") - - with pytest.raises(SystemExit, match="acceptance-blocked"): - cmd_archive_plan(archive_args(root, "plan-001")) - - assert (root / ".work-bundle/orchestration/plan/active/compiler-plan.md").is_file() - assert not (root / "worktree-only-marker").exists() - - -def test_passing_mutating_integration_command_blocks_archive(tmp_path: Path) -> None: - from plans import cmd_archive_plan - from test_orchestration_execution_context import workspace - - command = "touch mutated-by-acceptance.txt" - root, _, _ = workspace(tmp_path) - _append_plan_knowledge(root, closure_return="missing") - _append_plan_integration_command(root, command) - _record_tree_fresh_integration_pass(root, command) - - with pytest.raises(SystemExit, match="acceptance-blocked"): - cmd_archive_plan(archive_args(root, "plan-001")) - - assert (root / ".work-bundle/orchestration/plan/active/compiler-plan.md").is_file() - - -def test_contradictory_validated_plan_acceptance_blocks_archive(tmp_path: Path) -> None: - from plans import cmd_archive_plan - from test_orchestration_execution_context import ( - TASK_VALIDATION_COMMAND, - workspace, - write_executor_handoff, - ) - - command = "uv run --with pytest pytest -q tests/test_plan_acceptance.py" - root, _, _ = workspace(tmp_path) - _append_plan_knowledge(root, closure_return="missing") - plan = root / ".work-bundle/orchestration/plan/active/compiler-plan.md" - plan.write_text( - plan.read_text(encoding="utf-8") - + "\n## 7. Tests\n\n" - + "| ID | Test Type | Target | Related Phase | Can Run With | Command | Expected Result |\n" - + "|---|---|---|---|---|---|---|\n" - + f"| TEST-099 | integration | full harness | phase-001 | - | `{command}` | all tests pass |\n", - encoding="utf-8", - ) - passed = write_executor_handoff( - root, - " action: none\n reason: No stable authority changed.\n affected_authority: []\n", - ) - passed.write_text( - passed.read_text(encoding="utf-8").replace( - f"- {{command: {TASK_VALIDATION_COMMAND}, result: passed}}\n", - f"- {{command: {TASK_VALIDATION_COMMAND}, result: passed}}\n" - f" - {{command: {command}, result: passed}}\n", - ), - encoding="utf-8", - ) - failed = passed.parent / "handoff-task-004-failed.yaml" - failed.write_text( - passed.read_text(encoding="utf-8") - .replace("id: handoff-task-004\n", "id: handoff-task-004-failed\n") - .replace(f"- {{command: {command}, result: passed}}\n", f"- {{command: {command}, result: failed}}\n"), - encoding="utf-8", - ) - - with pytest.raises(SystemExit, match="acceptance-blocked"): - cmd_archive_plan(archive_args(root, "plan-001")) - - -def test_stale_plan_acceptance_after_later_material_task_blocks_archive(tmp_path: Path) -> None: - from plans import cmd_archive_plan - from test_orchestration_execution_context import workspace - - command = "uv run --with pytest pytest -q tests/test_plan_acceptance.py" - root, _, _ = workspace(tmp_path) - _append_plan_knowledge(root, closure_return="missing") - _append_plan_integration_command(root, command) - _write_earlier_integration_pass(root, command, created_at="2026-08-15") - _write_follow_on_plan_task(root) - later = _git_commit_file( - root, - FOLLOW_ON_WRITE_SCOPE_FILE, - "def archive_plan():\n return 'later'\n", - "task-005", - ) - _write_follow_on_executor_handoff( - root, - task_id="task-005", - created_at="2026-08-16", - actual_commit=later, - ) - - with pytest.raises(SystemExit, match="acceptance-blocked:.*is failed"): - cmd_archive_plan(archive_args(root, "plan-001")) - - assert (root / ".work-bundle/orchestration/plan/active/compiler-plan.md").is_file() - - -def test_fresh_plan_acceptance_rerun_after_later_task_allows_archive(tmp_path: Path) -> None: - from plans import cmd_archive_plan - from test_orchestration_execution_context import WRITE_SCOPE_FILE, workspace - - command = ARCHIVE_NEUTRAL_COMMAND - root, _, _ = workspace(tmp_path) - _append_plan_knowledge(root, closure_return="missing") - _append_plan_integration_command(root, command) - _git_commit_file(root, WRITE_SCOPE_FILE, "def compile_task():\n return 'old'\n", "task-004") - _write_earlier_integration_pass(root, command, created_at="2026-08-15") - _write_follow_on_plan_task(root) - later = _git_commit_file( - root, - FOLLOW_ON_WRITE_SCOPE_FILE, - "def archive_plan():\n return 'fresh'\n", - "task-005", - ) - _write_follow_on_executor_handoff( - root, - task_id="task-005", - created_at="2026-08-16", - extra_command=command, - actual_commit=later, - ) - - cmd_archive_plan(archive_args(root, "plan-001")) - - 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(archive_args(root, "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(archive_args(root, "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 - - command = "uv run --with pytest pytest -q tests/test_plan_acceptance.py" - root, _, _ = workspace(tmp_path) - _append_plan_knowledge(root, closure_return="missing") - _append_plan_integration_command(root, command) - _write_follow_on_plan_task(root, task_id="task-010", write_file=WRITE_SCOPE_FILE) - first = _git_commit_file(root, WRITE_SCOPE_FILE, "def compile_task():\n return 'first'\n", "task-010") - _write_follow_on_executor_handoff( - root, - task_id="task-010", - created_at="2026-08-16", - write_file=WRITE_SCOPE_FILE, - extra_command=command, - actual_commit=first, - ) - _write_follow_on_plan_task(root, task_id="task-002") - later = _git_commit_file( - root, - FOLLOW_ON_WRITE_SCOPE_FILE, - "def archive_plan():\n return 'later'\n", - "task-002", - ) - _write_follow_on_executor_handoff( - root, - task_id="task-002", - created_at="2026-08-16", - actual_commit=later, - ) - - with pytest.raises(SystemExit, match="acceptance-blocked:.*is failed"): - cmd_archive_plan(archive_args(root, "plan-001")) - - assert (root / ".work-bundle/orchestration/plan/active/compiler-plan.md").is_file() - - -def test_same_day_out_of_id_order_fresh_rerun_allows_archive(tmp_path: Path) -> None: - from plans import cmd_archive_plan - from test_orchestration_execution_context import WRITE_SCOPE_FILE, workspace - - command = ARCHIVE_NEUTRAL_COMMAND - root, _, _ = workspace(tmp_path) - _append_plan_knowledge(root, closure_return="missing") - _append_plan_integration_command(root, command) - _write_follow_on_plan_task(root, task_id="task-010", write_file=WRITE_SCOPE_FILE) - first = _git_commit_file(root, WRITE_SCOPE_FILE, "def compile_task():\n return 'first'\n", "task-010") - _write_follow_on_executor_handoff( - root, - task_id="task-010", - created_at="2026-08-16", - write_file=WRITE_SCOPE_FILE, - extra_command=command, - actual_commit=first, - ) - _write_follow_on_plan_task(root, task_id="task-002") - later = _git_commit_file( - root, - FOLLOW_ON_WRITE_SCOPE_FILE, - "def archive_plan():\n return 'later'\n", - "task-002", - ) - _write_follow_on_executor_handoff( - root, - task_id="task-002", - created_at="2026-08-16", - extra_command=command, - actual_commit=later, - ) - - cmd_archive_plan(archive_args(root, "plan-001")) - - assert (root / ".work-bundle/orchestration/plan/archived/compiler-plan.md").is_file() - - -def test_historical_failed_plan_acceptance_does_not_poison_fresh_head_pass(tmp_path: Path) -> None: - from plans import cmd_archive_plan - from test_orchestration_execution_context import WRITE_SCOPE_FILE, workspace - - command = ARCHIVE_NEUTRAL_COMMAND - root, _, _ = workspace(tmp_path) - _append_plan_knowledge(root, closure_return="missing") - _append_plan_integration_command(root, command) - _write_follow_on_plan_task(root, task_id="task-010", write_file=WRITE_SCOPE_FILE) - first = _git_commit_file(root, WRITE_SCOPE_FILE, "def compile_task():\n return 'broken'\n", "task-010-fail") - _write_follow_on_executor_handoff( - root, - task_id="task-010", - created_at="2026-08-16", - write_file=WRITE_SCOPE_FILE, - extra_command=command, - extra_result="failed", - actual_commit=first, - ) - _write_follow_on_plan_task(root, task_id="task-002") - later = _git_commit_file( - root, - FOLLOW_ON_WRITE_SCOPE_FILE, - "def archive_plan():\n return 'repaired'\n", - "task-002-pass", - ) - _write_follow_on_executor_handoff( - root, - task_id="task-002", - created_at="2026-08-16", - extra_command=command, - actual_commit=later, - ) - - cmd_archive_plan(archive_args(root, "plan-001")) - - assert (root / ".work-bundle/orchestration/plan/archived/compiler-plan.md").is_file() - - -def test_same_tree_contradictory_plan_acceptance_still_blocks_archive(tmp_path: Path) -> None: - from plans import cmd_archive_plan - from test_orchestration_execution_context import WRITE_SCOPE_FILE, workspace - - command = "uv run --with pytest pytest -q tests/test_plan_acceptance.py" - root, _, _ = workspace(tmp_path) - _append_plan_knowledge(root, closure_return="missing") - _append_plan_integration_command(root, command) - head = _git_commit_file(root, WRITE_SCOPE_FILE, "def compile_task():\n return 'now'\n", "terminal") - _write_follow_on_plan_task(root, task_id="task-010", write_file=WRITE_SCOPE_FILE) - _write_follow_on_executor_handoff( - root, - task_id="task-010", - created_at="2026-08-16", - write_file=WRITE_SCOPE_FILE, - extra_command=command, - extra_result="failed", - actual_commit=head, - ) - _write_follow_on_executor_handoff( - root, - task_id="task-004", - created_at="2026-08-16", - write_file=WRITE_SCOPE_FILE, - extra_command=command, - actual_commit=head, - ) - - with pytest.raises(SystemExit, match="acceptance-blocked:.*contradictory"): - cmd_archive_plan(archive_args(root, "plan-001")) - - -def test_precommit_tree_pass_survives_same_tree_finalization_commit(tmp_path: Path) -> None: - from plans import cmd_archive_plan - from test_orchestration_execution_context import WRITE_SCOPE_FILE, git, workspace - - command = ARCHIVE_NEUTRAL_COMMAND - root, _, _ = workspace(tmp_path) - _append_plan_knowledge(root, closure_return="missing") - _append_plan_integration_command(root, command) - first = _git_commit_file(root, WRITE_SCOPE_FILE, "def compile_task():\n return 'base'\n", "task-010") - _write_follow_on_plan_task(root, task_id="task-010", write_file=WRITE_SCOPE_FILE) - _write_follow_on_executor_handoff( - root, - task_id="task-010", - created_at="2026-08-16", - write_file=WRITE_SCOPE_FILE, - extra_command=command, - actual_commit=first, - ) - _write_follow_on_plan_task(root, task_id="task-002") - (root / FOLLOW_ON_WRITE_SCOPE_FILE).write_text("def archive_plan():\n return 'final'\n", encoding="utf-8") - tree = _git_write_tree(root) - _write_follow_on_executor_handoff( - root, - task_id="task-002", - created_at="2026-08-16", - extra_command=command, - reviewed_head=tree, - ) - git(root, "commit", "-qm", "finalize") - - cmd_archive_plan(archive_args(root, "plan-001")) - - assert (root / ".work-bundle/orchestration/plan/archived/compiler-plan.md").is_file() - - -def test_archive_plan_no_review_completed_update_blocks_until_closure_return( - tmp_path: Path, -) -> None: - from plans import cmd_archive_plan - from test_orchestration_execution_context import ACCEPTED_AUTHORITY, workspace, write_executor_handoff - - root, _, _ = workspace(tmp_path) - _append_plan_knowledge(root, closure_return="missing") - write_executor_handoff( - root, - f" action: update\n reason: Task-local evidence.\n affected_authority: [{ACCEPTED_AUTHORITY}]\n", - ) - - with pytest.raises(SystemExit, match="knowledge-blocked"): - cmd_archive_plan(archive_args(root, "plan-001")) - - assert (root / ".work-bundle/orchestration/plan/active/compiler-plan.md").is_file() - - -def test_archive_plan_ignores_unvalidated_update_handoff(tmp_path: Path) -> None: - from plans import cmd_archive_plan - - _write_archive_plan(tmp_path, "plan-B") - _write_archive_handoff( - tmp_path, - "plan-b.yaml", - "{plan: plan-B, task: task-001}", - verdict=None, - result_state="completed", - ) - - cmd_archive_plan(archive_args(tmp_path, "plan-B")) - - assert (tmp_path / ".work-bundle/orchestration/plan/archived/plan-B.md").is_file() - - -def test_archive_plan_review_required_cannot_downgrade_via_omitted_required(tmp_path: Path) -> None: - from plans import cmd_archive_plan - from test_orchestration_execution_context import ACCEPTED_AUTHORITY, workspace, write_executor_handoff - - root, _, task = workspace(tmp_path) - task.write_text( - task.read_text(encoding="utf-8").replace( - "acceptance_review:\n required: false\n", - "acceptance_review:\n required: true\n", - ), - encoding="utf-8", - ) - _append_plan_knowledge(root, closure_return="missing") - write_executor_handoff( - root, - f" action: update\n reason: Task-local evidence.\n affected_authority: [{ACCEPTED_AUTHORITY}]\n", - ) - - with pytest.raises(SystemExit, match="knowledge-blocked"): - cmd_archive_plan(archive_args(root, "plan-001")) - - assert (root / ".work-bundle/orchestration/plan/active/compiler-plan.md").is_file() - - -def test_missing_or_wrong_plan_identity_cannot_complete_without_review_package() -> None: - missing = _completed_executor_result(plan=None) - with pytest.raises(SystemExit, match="Handoff plan identity missing"): - validate_executor_result_for_task(missing, _task_brief()) - - mismatched = _completed_executor_result(plan="plan-A") - with pytest.raises(SystemExit, match="Handoff plan mismatch: expected plan-001, got plan-A"): - validate_executor_result_for_task(mismatched, _task_brief()) - - -def test_review_required_task_fails_closed_until_independent_accept() -> None: - execute = read("skills/orch-execute-plan/SKILL.md") - review = read("skills/orch-review-plan/SKILL.md") - contract = read("references/assets/orchestration/contract/handoff-executor-result-v1.md") - for token in [ - "Do not perform acceptance judgment or mark a review-required task complete", - "A review-required task additionally needs exact stored `accept` authority", - ]: - assert token in execute - for token in [ - "missing stored `accept` review authority blocks only a task whose compiled `review_required` is true", - "exact stored `accept` authority only for those explicitly required reviews", - ]: - assert token in review - assert "accepted-result materialization later joins it with the exact published review" in contract - - pending = { - "related": {"plan": "plan-001", "task": "task-001"}, - "result": {"state": "completed"}, - "acceptance_review": {"required": True, "verdict": "pending"}, - "knowledge_disposition": { - "action": "update", - "reason": "Task-local evidence.", - "affected_authority": ["AUTH-001"], - }, - } - closure = evaluate_knowledge_closure_state( - upstream_disposition="not-needed", - accepted_task_handoffs=[pending], - closure_return="missing", - ) - assert (closure["disposition"], closure["archive_blocked"]) == ("not-needed", False) - - review_required_handoff = _completed_executor_result( - acceptance_review={"required": True, "verdict": "pending"} - ) - with pytest.raises(SystemExit, match="accept|review"): - validate_executor_result_for_task( - review_required_handoff, {**_task_brief(), "review_required": True} - ) - - accepted = _completed_executor_result( - acceptance_review={"required": True, "verdict": "accept"} - ) - validated = validate_executor_result_for_task( - accepted, - {**_task_brief(), "review_required": True}, - mutation_events=[], - ) - assert validated["result_state"] == "completed" - - -def test_workflow_distinguishes_native_and_legacy_process_review_provenance() -> None: - workflow = read("references/assets/orchestration/workflow.md") - - for token in [ - "`reviewer-native-receipt-v1` for native host runs", - "`reviewer-process-receipt-v1` for legacy sandboxed process runs", - "`run_native_reviewer` for the ordinary plugin-independent native path", - "native host read-only policy is not OS process isolation", - "provider-specific execution boundary", - "Publication validates the provider-specific reviewer-run receipt once", - "Later lifecycle consumers use the immutable direct current-authority binding", - ]: - assert token in workflow - for process_only_claim in [ - "referencing a native `reviewer-process-receipt-v1`", - "Run the worker with `reviewer-process-run` using that runtime root", - "completion, sandbox/network/write boundary, and immutable packet/profile/event", - "recheck its receipt", - ]: - assert process_only_claim not in workflow - - -def test_current_orchestration_instructions_do_not_depend_on_execution_flow() -> None: - owners = [ - read("references/assets/orchestration/workflow.md"), - read("skills/orch-execute-plan/SKILL.md"), - read("skills/orch-review-plan/SKILL.md"), - ] - for owner in owners: - assert "host-native execution is sufficient" in owner - assert "Execution Flow is optional" in owner - - -def test_review_contract_owners_use_common_provenance_and_final_knowledge_gate() -> None: - provenance_owners = [ - "skills/orch-execute-plan/SKILL.md", - "skills/orch-review-plan/SKILL.md", - "references/assets/orchestration/contract/task-v1.md", - "rules/orchestration/orch-review-completion.md", - "references/assets/orchestration/workflow.md", - ] - for owner in provenance_owners: - assert "provider-specific reviewer-run receipt" in read(owner), owner - - final_gate_owners = [ - "skills/orch-review-plan/SKILL.md", - "rules/orchestration/orch-review-completion.md", - "references/assets/orchestration/workflow.md", - ] - for owner in final_gate_owners: - text = read(owner) - assert "Knowledge closure gates final completion and archive" in text, owner - assert "never precedes specification, plan, task, or integrated-implementation review" in text, owner - - -def test_overlapping_writes_are_not_parallelizable() -> None: - execute = read("skills/orch-execute-plan/SKILL.md") - create = read("skills/orch-create-implementation-plan/SKILL.md") - workflow = read("references/assets/orchestration/workflow.md") - plan = read("references/assets/orchestration/contract/plan-v1.md") - assert "planner-proven dependencies, write scopes" in execute - assert "Dispatch every ready independent task with disjoint write scope" in execute - assert "Serialize dependent, overlapping, or same-workspace mutation" in execute - assert "disjoint write scopes" in create - assert "Independent disjoint tasks in distinct execution workspaces dispatch before any wait" in workflow - assert "disjoint write scopes" in workflow - assert "assign parallel tasks only when dependencies are satisfied and write scopes are disjoint" in plan - assert "unsafe parallelization is explicitly blocked by dependency or scope evidence" in plan - - -def test_plan_contract_has_no_placeholder_markdown_links() -> None: - plan = read("references/assets/orchestration/contract/plan-v1.md") - - assert "](.work-bundle/orchestration/spec/active/...)" not in plan - assert "`.work-bundle/orchestration/spec/active/...`" in plan + assert family in workflow + assert "reviewer compares the actual candidate directly" in workflow + assert "does not reread source for code quality or repeat implementation review" in workflow + for retired in ( + "reviewer-native-receipt", + "reviewer-process-receipt", + "current-authority sidecar", + "begin-review-round", + "legacy-status-overrides", + ): + assert retired not in workflow + + +def test_executor_result_contract_is_schema_owned_and_non_accepting() -> None: + contract = read("references/assets/orchestration/contract/handoff-executor-result-v1.md").lower() + assert "executor-result-v1" in contract + assert "canonical" in contract + assert "product verdict" in contract + assert "filename inference" in contract and "unsupported" in contract + assert "legacy statuses" in contract + + +def test_current_orchestration_evals_cover_stage5_boundary() -> None: + cases = json.loads(read("references/evals/orchestration/evals.json"))["evals"] + corpus = "\n".join( + f"{case.get('prompt', '')}\n{case.get('expected_output', '')}" for case in cases + ).lower() + for term in ( + "executor-result-v1", + "implementation-review-v1", + "accepted-task-result-v1", + "final-workflow-review-v1", + "frozen worktree", + ): + assert term in corpus -def test_dev_create_task_plan_tests_omit_heavy_orchestration_requirements() -> None: - skill = read("skills/dev-create-task-plan/SKILL.md") - assert "Do not import executor-result, `Completed`, review package, archive helper, or heavy Knowledge Base Update closure" in skill - tests = read("tests/test_dev_skill_contracts.py") - assert "dev-create-task-plan" in tests - assert ".work-bundle/runtime/dev-plans/" in tests +def test_current_doctor_accepts_the_repaired_contract(capsys: pytest.CaptureFixture[str]) -> None: + cmd_doctor(argparse.Namespace()) + assert capsys.readouterr().out.strip() == "ok" diff --git a/tests/test_project_initialization.py b/tests/test_project_initialization.py deleted file mode 100644 index 5181ae0..0000000 --- a/tests/test_project_initialization.py +++ /dev/null @@ -1,2089 +0,0 @@ -from __future__ import annotations - -import json -import os -import subprocess -import sys -import time -from pathlib import Path - -import pytest - - -REPO_ROOT = Path(__file__).resolve().parents[1] -LEGACY_BOOTSTRAP_POINTER = "references/bootstrap" -FULL_ORCHESTRATION_DIRS = [ - ".work-bundle/orchestration/spec/active", - ".work-bundle/orchestration/spec/archived", - ".work-bundle/orchestration/plan/active", - ".work-bundle/orchestration/plan/archived", - ".work-bundle/orchestration/handoff/orchestration/active", - ".work-bundle/orchestration/handoff/orchestration/archived", - ".work-bundle/orchestration/handoff/executor/active", - ".work-bundle/orchestration/handoff/executor/archived", - ".work-bundle/orchestration/docs", - ".work-bundle/orchestration/principles", - ".work-bundle/orchestration/templates", - ".work-bundle/orchestration/reviews", - ".work-bundle/orchestration/execution-state", -] -PROJECT_REGISTRY_ENTRY_FIELDS = ( - "slug", - "name", - "work_bundle_root", - "knowledge_root", - "aliases", - "source_repositories", - "status", - "updated_at", -) -PROJECT_REGISTRY_SOURCE_FIELDS = ( - "id", - "path", - "checkout_role", - "work_dir", - "remote", - "git_repository", -) - - -def run_wb(config_root: Path, *args: str, cwd: Path | None = None) -> subprocess.CompletedProcess[str]: - env = os.environ.copy() - env.update( - { - "WB_CONFIG_ROOT": str(config_root), - "GIT_AUTHOR_NAME": "Test", - "GIT_AUTHOR_EMAIL": "test@example.com", - "GIT_COMMITTER_NAME": "Test", - "GIT_COMMITTER_EMAIL": "test@example.com", - } - ) - return subprocess.run( - [sys.executable, str(REPO_ROOT / "scripts/wb.py"), *args], - cwd=cwd or REPO_ROOT, - env=env, - check=False, - capture_output=True, - text=True, - ) - - -def git(path: Path, *args: str) -> str: - result = subprocess.run(["git", "-C", str(path), *args], check=True, capture_output=True, text=True) - return result.stdout.strip() - - -def bootstrap_config(tmp_path: Path, work_bundle_root: Path | None = None) -> Path: - config_root = tmp_path / "config" - registry = config_root / "registry" - registry.mkdir(parents=True) - (config_root / "bootstrap.yaml").write_text( - "\n".join( - [ - "bootstrap_version: v1", - "authority: canonical", - f"work_bundle_root: {work_bundle_root or REPO_ROOT}", - 'project_registry: "$work_bundle_config_root/registry/projects.yaml"', - 'skill_registry: "$work_bundle_config_root/registry/skill-registry.yaml"', - "", - ] - ), - encoding="utf-8", - ) - (registry / "projects.yaml").write_text("projects: []\n", encoding="utf-8") - return config_root - - -def _import_wb_project(): - script_root = REPO_ROOT / "scripts" / "work-bundle" - for module_name in ("project", "bootstrap_config", "core"): - module = sys.modules.get(module_name) - module_file = Path(getattr(module, "__file__", "")) if module is not None else None - if module_file is not None and script_root not in module_file.parents: - sys.modules.pop(module_name, None) - sys.path.insert(0, str(script_root)) - import project as wb_project # type: ignore[import-not-found] - - return wb_project - - -def _cleanup_wb_project_modules() -> None: - if sys.path and sys.path[0] == str(REPO_ROOT / "scripts" / "work-bundle"): - sys.path.pop(0) - for module_name in ("project", "bootstrap_config", "core"): - sys.modules.pop(module_name, None) - - -def _init_managed_text_files(project: Path) -> list[Path]: - candidates = [ - project / ".gitignore", - project / "AGENTS.md", - project / ".work-bundle/project.yaml", - project / ".work-bundle/knowledge/project.yaml", - project / ".work-bundle/rules/index.yaml", - project / ".work-bundle/.gitignore", - ] - candidates.extend(sorted(project.glob("roles/*.yaml"))) - return [path for path in candidates if path.is_file()] - - -def test_init_managed_text_files_track_current_rule_store(tmp_path: Path) -> None: - project = tmp_path / "project" - current_rule_index = project / ".work-bundle/rules/index.yaml" - legacy_rule_index = project / "rules/index.yaml" - current_rule_index.parent.mkdir(parents=True) - legacy_rule_index.parent.mkdir(parents=True) - current_rule_index.write_text("rules: []\n", encoding="utf-8") - legacy_rule_index.write_text("rules: []\n", encoding="utf-8") - - managed_files = _init_managed_text_files(project) - - assert current_rule_index in managed_files - assert legacy_rule_index not in managed_files - - -def _minimal_work_bundle_root(tmp_path: Path, *, include_project_template: bool = True) -> Path: - root = tmp_path / "work-bundle-root" - for relative in [ - "references/wb-initialize-project-default-work-bundle-tree.yaml", - "references/wb-initialize-project-default-work-bundle-gitignore", - "references/wb-initialize-project-default-rule-index.yaml", - "references/assets/template/AGENTS.md", - "references/assets/template/projects.yaml", - ]: - source = REPO_ROOT / relative - target = root / relative - target.parent.mkdir(parents=True, exist_ok=True) - target.write_text(source.read_text(encoding="utf-8"), encoding="utf-8") - if include_project_template: - project_template = REPO_ROOT / "references/assets/template/project.yaml" - target = root / "references/assets/template/project.yaml" - target.parent.mkdir(parents=True, exist_ok=True) - target.write_text(project_template.read_text(encoding="utf-8"), encoding="utf-8") - return root - - -def test_init_project_creates_structure_without_git_actions_and_is_idempotent(tmp_path: Path) -> None: - config_root = bootstrap_config(tmp_path) - project = tmp_path / "project" - project.mkdir() - git(project, "init", "-q", "-b", "main") - git(project, "config", "user.email", "test@example.com") - git(project, "config", "user.name", "Test") - - init = run_wb(config_root, "init-project", str(project), "--mode", "single-repository", "--name", "demo") - assert init.returncode == 0, init.stdout + init.stderr - init_data = json.loads(init.stdout) - assert init_data["status"] == "passed" - assert init_data["failures"] == [] - - for relative in [ - ".work-bundle/knowledge/context-packs", - ".work-bundle/knowledge/indexes", - ".work-bundle/knowledge/notes", - ".work-bundle/knowledge/open-questions", - *FULL_ORCHESTRATION_DIRS, - ]: - assert (project / relative).is_dir() - - assert not (project / "rules/contract.yaml").exists() - assert (project / ".work-bundle/rules/index.yaml").is_file() - assert (project / ".work-bundle/rules/index.yaml").read_text(encoding="utf-8") == "rules: []\n" - assert not (project / "rules/index.yaml").exists() - assert init_data["git_actions"] == [] - assert init_data["transaction"]["state"] == "published" - assert (project / "script/index.yaml").is_file() - assert (project / "credentials/credentials.yaml").is_file() - assert (project / "credentials").stat().st_mode & 0o777 == 0o700 - assert (project / "credentials/credentials.yaml").stat().st_mode & 0o777 == 0o600 - assert "credentials/" in (project / ".gitignore").read_text(encoding="utf-8").splitlines() - metadata_text = (project / ".work-bundle/project.yaml").read_text(encoding="utf-8") - assert "workspace_resources:" in metadata_text - assert subprocess.run(["git", "-C", str(project), "rev-parse", "--verify", "HEAD"], check=False, capture_output=True).returncode != 0 - assert git(project, "diff", "--cached", "--name-only") == "" - assert not (project / ".work-bundle/knowledge/.git").exists() - - validate = run_wb(config_root, "validate-project", str(project)) - assert validate.returncode == 0, validate.stdout + validate.stderr - validate_data = json.loads(validate.stdout) - assert validate_data["status"] == "passed" - assert validate_data["rules_root_authority"] == ".work-bundle/rules" - assert validate_data["rule_index"] is True - assert validate_data["legacy_rule_index"] is False - - time.sleep(1.1) - rerun = run_wb(config_root, "init-project", str(project), "--mode", "single-repository", "--name", "demo") - assert rerun.returncode == 0, rerun.stdout + rerun.stderr - assert json.loads(rerun.stdout)["changed_files"] == [] - - -def test_new_init_requires_explicit_mode_without_writes(tmp_path: Path) -> None: - config_root = bootstrap_config(tmp_path) - project = tmp_path / "project" - project.mkdir() - git(project, "init", "-q", "-b", "main") - head_before = subprocess.run(["git", "-C", str(project), "rev-parse", "--verify", "HEAD"], check=False, capture_output=True).returncode - result = run_wb(config_root, "init-project", str(project), "--name", "demo") - data = json.loads(result.stdout) - assert result.returncode == 1 - assert data["failures"] == ["WB_WORKSPACE_MODE_REQUIRED"] - assert data["changed_files"] == [] - assert not (project / ".work-bundle").exists() - assert subprocess.run(["git", "-C", str(project), "rev-parse", "--verify", "HEAD"], check=False, capture_output=True).returncode == head_before - assert git(project, "diff", "--cached", "--name-only") == "" - - -def test_metadata_v2_migration_requires_dry_run_then_explicit_apply(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - metadata_path = project / ".work-bundle/project.yaml" - metadata_path.write_text( - f"metadata_version: 2\nauthority: canonical\nproject_root: {project.resolve()}\ncustom_user_field: keep-me\n", - encoding="utf-8", - ) - before = metadata_path.read_bytes() - missing_action = run_wb(config_root, "migrate-project", str(project), "--name", "demo") - assert missing_action.returncode == 1 - assert json.loads(missing_action.stdout)["failures"] == ["WB_MIGRATION_EXPLICIT_ACTION_REQUIRED"] - dry_run = run_wb(config_root, "migrate-project", str(project), "--dry-run", "--name", "demo") - assert dry_run.returncode == 0 - assert metadata_path.read_bytes() == before - proposal_id = json.loads(dry_run.stdout)["migration"]["proposal_id"] - applied = run_wb( - config_root, - "migrate-project", - str(project), - "--apply", - "--name", - "demo", - "--accepted-proposal-id", - proposal_id, - ) - assert applied.returncode == 0, applied.stdout + applied.stderr - assert "metadata_version: 3" in metadata_path.read_text(encoding="utf-8") - assert "custom_user_field: keep-me" in metadata_path.read_text(encoding="utf-8") - assert json.loads(applied.stdout)["git_actions"] == [] - - -def test_metadata_v2_multi_source_routes_to_explicit_workspace_migration(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - extra = tmp_path / "library" - extra.mkdir() - git(extra, "init", "-q", "-b", "main") - metadata = project / ".work-bundle/project.yaml" - metadata.write_text( - "\n".join( - [ - "metadata_version: 2", - "authority: canonical", - f"project_root: {project.resolve()}", - "source_repositories:", - " - id: demo-main", - f" path: {project.resolve()}", - " - id: demo-library", - f" path: {extra.resolve()}", - "", - ] - ), - encoding="utf-8", - ) - registry = config_root / "registry/projects.yaml" - registry.write_text( - "\n".join( - [ - "projects:", - " - slug: demo", - " name: demo", - f" work_bundle_root: {project.resolve() / '.work-bundle'}", - f" knowledge_root: {project.resolve() / '.work-bundle/knowledge'}", - " aliases: []", - " source_repositories:", - " - id: demo-main", - f" path: {project.resolve()}", - " git_repository: true", - " - id: demo-library", - f" path: {extra.resolve()}", - " git_repository: true", - " status: active", - " updated_at: 2026-08-11", - "", - ] - ), - encoding="utf-8", - ) - metadata_before = metadata.read_bytes() - registry_before = registry.read_bytes() - - result = run_wb(config_root, "migrate-project", str(project), "--dry-run", "--name", "demo") - data = json.loads(result.stdout) - - assert result.returncode == 1 - assert data["mode"] == "multi-repository-migration-required" - assert data["failures"] == ["WB_MIGRATION_MULTI_REPOSITORY_WORKFLOW_REQUIRED"] - assert data["topology_assessment"]["required_command"] == "migrate-to-multi-repository" - assert metadata.read_bytes() == metadata_before - assert registry.read_bytes() == registry_before - - forced = run_wb( - config_root, - "migrate-project", - str(project), - "--apply", - "--force", - "--name", - "demo", - "--accepted-proposal-id", - data["migration"]["proposal_id"], - ) - assert forced.returncode == 1 - assert json.loads(forced.stdout)["failures"] == ["WB_MIGRATION_MULTI_REPOSITORY_WORKFLOW_REQUIRED"] - assert metadata.read_bytes() == metadata_before - assert registry.read_bytes() == registry_before - - -def test_metadata_v2_apply_rejects_stale_proposal(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - metadata = project / ".work-bundle/project.yaml" - metadata.write_text( - f"metadata_version: 2\nauthority: canonical\nproject_root: {project.resolve()}\n", - encoding="utf-8", - ) - dry_run = run_wb(config_root, "migrate-project", str(project), "--dry-run", "--name", "demo") - proposal_id = json.loads(dry_run.stdout)["migration"]["proposal_id"] - metadata.write_text(metadata.read_text(encoding="utf-8") + "user_change: preserve\n", encoding="utf-8") - - applied = run_wb( - config_root, - "migrate-project", - str(project), - "--apply", - "--name", - "demo", - "--accepted-proposal-id", - proposal_id, - ) - - assert applied.returncode == 1 - assert json.loads(applied.stdout)["failures"] == ["WB_MIGRATION_PROPOSAL_STALE"] - assert "user_change: preserve" in metadata.read_text(encoding="utf-8") - - -def test_metadata_v2_topology_identity_disagreement_blocks(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - conflicting = tmp_path / "conflicting" - conflicting.mkdir() - metadata = project / ".work-bundle/project.yaml" - metadata.write_text( - "\n".join( - [ - "metadata_version: 2", - f"project_root: {project.resolve()}", - "source_repositories:", - " - id: demo-main", - f" path: {conflicting.resolve()}", - "", - ] - ), - encoding="utf-8", - ) - - result = run_wb(config_root, "migrate-project", str(project), "--dry-run", "--name", "demo") - data = json.loads(result.stdout) - - assert result.returncode == 1 - assert data["mode"] == "topology-conflict" - assert data["failures"] == ["WB_MIGRATION_TOPOLOGY_CONFLICT"] - assert data["changed_files"] == [] - - -def test_doctor_repair_preserves_head_and_single_repository_resources(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - head_before = git(project, "rev-parse", "HEAD") - staged_before = git(project, "diff", "--cached", "--name-only") - script_before = (project / "script/index.yaml").read_bytes() - credential_before = (project / "credentials/credentials.yaml").read_bytes() - result = run_wb(config_root, "doctor-project", str(project), "--repair") - data = json.loads(result.stdout) - assert result.returncode == 0, result.stdout + result.stderr - assert data["git_actions"] == [] - assert (project / "script/index.yaml").read_bytes() == script_before - assert (project / "credentials/credentials.yaml").read_bytes() == credential_before - assert git(project, "rev-parse", "HEAD") == head_before - assert git(project, "diff", "--cached", "--name-only") == staged_before - - -def test_validate_single_repository_accepts_workspace_resource_directories(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - result = run_wb(config_root, "validate-project", str(project)) - data = json.loads(result.stdout) - - assert result.returncode == 0, result.stdout + result.stderr - assert data["failures"] == [] - - -def test_migrate_project_writes_report_without_breaking_validation(tmp_path: Path) -> None: - config_root = bootstrap_config(tmp_path) - project = tmp_path / "project" - project.mkdir() - git(project, "init", "-q", "-b", "main") - git(project, "config", "user.email", "test@example.com") - git(project, "config", "user.name", "Test") - assert run_wb(config_root, "init-project", str(project), "--mode", "single-repository", "--name", "demo").returncode == 0 - - migrated = run_wb(config_root, "migrate-project", str(project), "--apply", "--name", "demo") - assert migrated.returncode == 0, migrated.stdout + migrated.stderr - data = json.loads(migrated.stdout) - assert data["status"] == "passed" - assert Path(data["migration_report"]).is_file() - - validate = run_wb(config_root, "validate-project", str(project)) - assert validate.returncode == 0, validate.stdout + validate.stderr - - -def test_registry_upsert_preserves_aliases_and_sources(tmp_path: Path) -> None: - config_root = bootstrap_config(tmp_path) - project = tmp_path / "project" - project.mkdir() - resolved = project.resolve() - registry_path = config_root / "registry" / "projects.yaml" - wb_root = str(resolved / ".work-bundle") - kb_root = str(resolved / ".work-bundle" / "knowledge") - registry_path.write_text( - "\n".join( - [ - "projects:", - " - slug: demo", - " name: demo", - f" work_bundle_root: {wb_root}", - f" knowledge_root: {kb_root}", - " aliases:", - " - demo", - " - sample", - " source_repositories:", - f" - path: {resolved}", - " work_dir: true", - ' remote: ""', - " status: active", - " updated_at: 2026-01-01", - "", - ] - ), - encoding="utf-8", - ) - - first = run_wb(config_root, "register-project", str(project), "--name", "demo") - assert first.returncode == 0, first.stdout + first.stderr - first_data = json.loads(first.stdout) - assert first_data["status"] in {"skipped", "updated"} - entry = first_data["project"] - assert entry["aliases"] == ["demo", "sample"] - assert len(entry["source_repositories"]) == 1 - assert entry["source_repositories"][0]["path"] == str(resolved) - - text = registry_path.read_text(encoding="utf-8") - assert "sample" in text - assert "id: demo-main" in text - assert "git_repository:" in text - - -def test_register_project_adds_repository_to_registry_and_project_metadata(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - extra_repo = tmp_path / "library" - extra_repo.mkdir() - git(extra_repo, "init", "-q", "-b", "main") - git(extra_repo, "config", "user.email", "test@example.com") - git(extra_repo, "config", "user.name", "Test") - (extra_repo / "README.md").write_text("# Library\n", encoding="utf-8") - git(extra_repo, "add", "README.md") - git(extra_repo, "commit", "-m", "chore: seed library") - - registered = run_wb(config_root, "register-project", str(extra_repo), "--name", "demo") - assert registered.returncode == 0, registered.stdout + registered.stderr - data = json.loads(registered.stdout) - - assert data["status"] == "updated" - assert data["project_metadata_status"] == "updated" - assert data["source_repository_roles"]["registry"].startswith("Locator only") - assert data["source_repository_roles"]["project_metadata"].startswith("Working-state authority") - assert str(config_root / "registry/projects.yaml") in data["changed_files"] - assert str(project / ".work-bundle/project.yaml") in data["changed_files"] - assert [source["id"] for source in data["registry_entry"]["source_repositories"]] == ["demo-main", "demo-library"] - - registry_text = (config_root / "registry/projects.yaml").read_text(encoding="utf-8") - assert "source_repository_roles:" in registry_text - assert f"path: {extra_repo.resolve()}" in registry_text - - metadata_text = (project / ".work-bundle/project.yaml").read_text(encoding="utf-8") - wb_project = _import_wb_project() - try: - repositories = wb_project._metadata_source_repositories(metadata_text) - finally: - _cleanup_wb_project_modules() - - assert "source_repository_roles:" in metadata_text - assert [repo["path"] for repo in repositories] == [str(project.resolve()), str(extra_repo.resolve())] - assert repositories[1]["git_repository"] is True - assert repositories[1]["working_branch"] == "main" - assert repositories[1]["last_commit_id"] == git(extra_repo, "rev-parse", "HEAD") - assert repositories[1]["codegraph"]["reason"] == "no-index" - - -def test_registry_upsert_replaces_aliases_when_explicit(tmp_path: Path, monkeypatch) -> None: - config_root = bootstrap_config(tmp_path) - monkeypatch.setenv("WB_CONFIG_ROOT", str(config_root)) - project = tmp_path / "project" - project.mkdir() - resolved = project.resolve() - registry_path = config_root / "registry" / "projects.yaml" - wb_root = str(resolved / ".work-bundle") - kb_root = str(resolved / ".work-bundle" / "knowledge") - registry_path.write_text( - "\n".join( - [ - "projects:", - " - slug: demo", - " name: demo", - f" work_bundle_root: {wb_root}", - f" knowledge_root: {kb_root}", - " aliases:", - " - demo", - " - sample", - " source_repositories:", - f" - path: {resolved}", - " work_dir: true", - ' remote: ""', - " status: active", - " updated_at: 2026-01-01", - "", - ] - ), - encoding="utf-8", - ) - - wb_project = _import_wb_project() - try: - entry, changed, _ = wb_project.upsert_project_registry(project.resolve(), "demo", ["only-alias"]) - assert changed is True - assert entry["aliases"] == ["only-alias"] - assert entry["updated_at"] != "2026-01-01" - finally: - _cleanup_wb_project_modules() - - -def _init_fixture_project(tmp_path: Path) -> tuple[Path, Path]: - config_root = bootstrap_config(tmp_path) - project = tmp_path / "project" - project.mkdir() - git(project, "init", "-q", "-b", "main") - git(project, "config", "user.email", "test@example.com") - git(project, "config", "user.name", "Test") - (project / "README.md").write_text("# Fixture\n", encoding="utf-8") - git(project, "add", "README.md") - git(project, "commit", "-m", "chore: seed fixture") - assert run_wb(config_root, "init-project", str(project), "--mode", "single-repository", "--name", "demo").returncode == 0 - return config_root, project - - -def test_doctor_project_routed_and_returns_mechanical_json(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - result = run_wb(config_root, "doctor-project", str(project)) - assert result.returncode in {0, 1}, result.stdout + result.stderr - data = json.loads(result.stdout) - assert data["command"] == "doctor-project" - assert "status" in data - assert "failures" in data - assert "changed_files" in data - - -def test_validate_project_uses_current_work_bundle_rules_without_root_rules(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - root_rules = project / "rules" - if root_rules.exists(): - for path in sorted(root_rules.rglob("*"), reverse=True): - if path.is_file(): - path.unlink() - elif path.is_dir(): - path.rmdir() - root_rules.rmdir() - - result = run_wb(config_root, "validate-project", str(project)) - assert result.returncode == 0, result.stdout + result.stderr - data = json.loads(result.stdout) - assert data["status"] == "passed" - assert data["rules_root"] is True - assert data["rule_index"] is True - assert data["rules_root_authority"] == ".work-bundle/rules" - assert data["legacy_rules_root"] is False - assert "rules_root" not in data["failures"] - assert "rule_index" not in data["failures"] - - -def test_doctor_repair_does_not_restore_legacy_root_rule_index(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - legacy_rule_index = project / "rules/index.yaml" - assert not legacy_rule_index.exists() - - repaired = run_wb(config_root, "doctor-project", str(project), "--repair") - assert repaired.returncode == 0, repaired.stdout + repaired.stderr - data = json.loads(repaired.stdout) - - assert data["status"] == "passed" - assert data["rule_index"] is True - assert data["rules_root_authority"] == ".work-bundle/rules" - assert data["legacy_rule_index"] is False - assert not legacy_rule_index.exists() - - -def test_doctor_repair_creates_missing_project_rule_index(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - current_rule_index = project / ".work-bundle/rules/index.yaml" - current_rule_index.unlink() - - repaired = run_wb(config_root, "doctor-project", str(project), "--repair") - - assert repaired.returncode == 0, repaired.stdout + repaired.stderr - assert current_rule_index.read_text(encoding="utf-8") == "rules: []\n" - assert not (project / "rules/index.yaml").exists() - - -def test_agents_force_checksum_only_reports_unchanged(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - agents_path = project / "AGENTS.md" - metadata_path = project / ".work-bundle/project.yaml" - agents_before = agents_path.read_bytes() - metadata_lines = metadata_path.read_text(encoding="utf-8").splitlines() - checksum_index = next( - index for index, line in enumerate(metadata_lines) if "template_checksum_sha256:" in line - ) - metadata_lines[checksum_index] = ' template_checksum_sha256: "stale"' - metadata_path.write_text("\n".join(metadata_lines) + "\n", encoding="utf-8") - - refreshed = run_wb( - config_root, - "init-project", - str(project), - "--mode", - "single-repository", - "--name", - "demo", - "--force", - ) - - assert refreshed.returncode == 0, refreshed.stdout + refreshed.stderr - data = json.loads(refreshed.stdout) - assert data["agents_status"] == "unchanged" - assert data["agents_sync"]["changed_files"] == [str(metadata_path)] - assert agents_path.read_bytes() == agents_before - - -def test_init_force_overwrites_init_managed_templates_only(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - agents_path = project / "AGENTS.md" - custom_agents = "# Custom Agents\n" - agents_path.write_text(custom_agents, encoding="utf-8") - role_path = project / "roles" / "project-manager.yaml" - custom_role = "id: project-manager\ncustom: true\n" - role_path.write_text(custom_role, encoding="utf-8") - - without_force = run_wb(config_root, "init-project", str(project), "--mode", "single-repository", "--name", "demo") - assert without_force.returncode == 0, without_force.stdout + without_force.stderr - assert agents_path.read_text(encoding="utf-8").startswith(custom_agents.rstrip() + "\n\n") - assert role_path.read_text(encoding="utf-8") == custom_role - - agents_path.write_text(custom_agents, encoding="utf-8") - with_force = run_wb(config_root, "init-project", str(project), "--mode", "single-repository", "--name", "demo", "--force") - assert with_force.returncode == 0, with_force.stdout + with_force.stderr - force_data = json.loads(with_force.stdout) - assert str(agents_path) in force_data["changed_files"] - assert force_data["agents_status"] == "updated" - assert force_data["agents_sync"]["template_checksum_sha256"] - assert force_data["agents_sync"]["changed_files"] == [str(agents_path)] - assert agents_path.read_text(encoding="utf-8").startswith(custom_agents.rstrip() + "\n\n") - assert role_path.read_text(encoding="utf-8") == custom_role - - -def test_migrate_project_retires_legacy_bootstrap_without_force(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - bootstrap_dir = project / "references/bootstrap" - bootstrap_dir.mkdir(parents=True, exist_ok=True) - legacy_file = bootstrap_dir / "agent-bootstrap.md" - legacy_file.write_text("# legacy bootstrap\n", encoding="utf-8") - - migrated = run_wb(config_root, "migrate-project", str(project), "--apply", "--name", "demo") - assert migrated.returncode == 0, migrated.stdout + migrated.stderr - migrate_data = json.loads(migrated.stdout) - - assert not bootstrap_dir.exists() - assert migrate_data["retired_bootstrap"]["archive_root"] is not None - assert len(migrate_data["retired_bootstrap"]["artifacts"]) == 1 - artifact = migrate_data["retired_bootstrap"]["artifacts"][0] - assert artifact["source"] == "references/bootstrap/agent-bootstrap.md" - assert artifact["action"] == "archived-and-removed" - archive_root = project / migrate_data["retired_bootstrap"]["archive_root"] - assert (archive_root / "agent-bootstrap.md").is_file() - assert (archive_root / "agent-bootstrap.md").read_text(encoding="utf-8") == "# legacy bootstrap\n" - - report_text = Path(migrate_data["migration_report"]).read_text(encoding="utf-8") - assert "## Retired Legacy Bootstrap Artifacts" in report_text - assert "references/bootstrap/agent-bootstrap.md" in report_text - assert migrate_data["retired_bootstrap"]["archive_root"] in report_text - - -def test_init_project_does_not_create_references_bootstrap(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - assert not (project / "references/bootstrap").exists() - validate = run_wb(config_root, "validate-project", str(project)) - assert validate.returncode == 0, validate.stdout + validate.stderr - assert LEGACY_BOOTSTRAP_POINTER not in validate.stdout - - -def test_init_created_files_contain_no_legacy_bootstrap_pointers(tmp_path: Path) -> None: - _, project = _init_fixture_project(tmp_path) - offenders: list[str] = [] - for path in _init_managed_text_files(project): - if LEGACY_BOOTSTRAP_POINTER in path.read_text(encoding="utf-8"): - offenders.append(str(path.relative_to(project))) - assert offenders == [] - - -def test_registry_parser_tracks_projects_template_schema(tmp_path: Path) -> None: - template_path = REPO_ROOT / "references/assets/template/projects.yaml" - template_text = template_path.read_text(encoding="utf-8") - assert "source_repository_roles:" in template_text - assert "Locator authority in all versions" in template_text - assert "metadata v4 device_bindings" in template_text - assert "Metadata v4 portable project/topology authority" in template_text - assert "metadata v3 working-state authority" in template_text - assert "projects:" in template_text - for field in PROJECT_REGISTRY_ENTRY_FIELDS: - assert field in template_text - - wb_project = _import_wb_project() - try: - sample: dict[str, object] = { - "slug": "demo", - "name": "demo", - "work_bundle_root": "/tmp/project/.work-bundle", - "knowledge_root": "/tmp/project/.work-bundle/knowledge", - "aliases": ["demo", "sample"], - "source_repositories": [ - { - "id": "demo-main", - "path": "/tmp/project", - "checkout_role": "truth", - "work_dir": True, - "remote": "origin", - "git_repository": True, - }, - ], - "status": "active", - "updated_at": "2026-06-11", - } - rendered = wb_project._render_projects([sample]) - assert rendered.startswith("source_repository_roles:\n") - assert "\nprojects:\n" in rendered - registry_file = tmp_path / "projects.yaml" - registry_file.write_text(rendered, encoding="utf-8") - parsed = wb_project._project_blocks(registry_file) - assert len(parsed) == 1 - entry = parsed[0] - for field in PROJECT_REGISTRY_ENTRY_FIELDS: - assert field in entry - for field in PROJECT_REGISTRY_SOURCE_FIELDS: - assert field in entry["source_repositories"][0] - assert entry["slug"] == sample["slug"] - assert entry["aliases"] == sample["aliases"] - assert entry["source_repositories"] == sample["source_repositories"] - assert wb_project._registry_entries_equivalent(entry, sample) - finally: - _cleanup_wb_project_modules() - - -def test_project_blocks_do_not_absorb_device_bindings(tmp_path: Path) -> None: - wb_project = _import_wb_project() - try: - rendered = ( - (REPO_ROOT / "tests/fixtures/registry-layout/registry/mixed-device-bindings.yaml") - .read_text(encoding="utf-8") - .replace("__SLUG_A__", "alpha") - .replace("__ROOT_A__", "/tmp/alpha") - .replace("__REPO_A__", "alpha-main") - .replace("__REMOTE_A__", "/tmp/alpha.git") - .replace("__SLUG_B__", "beta") - .replace("__ROOT_B__", "/tmp/beta") - .replace("__REPO_B__", "beta-main") - .replace("__REMOTE_B__", "/tmp/beta.git") - ) - registry_file = tmp_path / "projects.yaml" - registry_file.write_text(rendered, encoding="utf-8") - parsed = wb_project._project_blocks(registry_file) - assert [entry["slug"] for entry in parsed] == ["alpha", "beta"] - assert parsed[0]["work_bundle_root"] == "/tmp/alpha/.work-bundle" - assert parsed[1]["work_bundle_root"] == "/tmp/beta/.work-bundle" - assert parsed[1]["slug"] == "beta" - assert parsed[0]["custom_entry_field"] == "keep-entry-a" - line_parsed = wb_project._project_blocks_line_scoped(rendered) - assert [entry["slug"] for entry in line_parsed] == ["alpha", "beta"] - assert line_parsed[1]["slug"] == "beta" - assert line_parsed[1]["work_bundle_root"] == "/tmp/beta/.work-bundle" - assert "workspace_root" not in line_parsed[1] - finally: - _cleanup_wb_project_modules() - - -def test_registry_upsert_preserves_schema_bindings_and_unknown_top_level(tmp_path: Path, monkeypatch) -> None: - config_root = bootstrap_config(tmp_path) - monkeypatch.setenv("WB_CONFIG_ROOT", str(config_root)) - project = tmp_path / "project" - project.mkdir() - resolved = project.resolve() - registry_path = config_root / "registry" / "projects.yaml" - registry_path.write_text( - "\n".join( - [ - "registry_schema_version: 1", - "source_repository_roles:", - ' registry: "Locator authority in all versions."', - ' project_metadata: "Working-state authority."', - "custom_registry_field: keep-registry", - "projects:", - " - slug: demo", - " name: demo", - f" work_bundle_root: {resolved / '.work-bundle'}", - f" knowledge_root: {resolved / '.work-bundle' / 'knowledge'}", - " aliases: []", - " custom_entry_field: keep-entry", - " source_repositories:", - " - id: demo-main", - f" path: {resolved}", - " checkout_role: truth", - " work_dir: true", - ' remote: ""', - " git_repository: true", - " status: active", - " updated_at: 2026-01-01", - "device_bindings:", - " wb-unrelated:", - " slug: unrelated-device", - " workspace_root: /tmp/unrelated-device", - " custom_binding_field: keep-unrelated", - " repositories: {}", - "", - ] - ), - encoding="utf-8", - ) - wb_project = _import_wb_project() - try: - entry, changed, _ = wb_project.upsert_project_registry(resolved, "demo", ["demo"]) - assert changed is True - assert entry["aliases"] == ["demo"] - text = registry_path.read_text(encoding="utf-8") - assert text.startswith("registry_schema_version: 1\n") - assert 'registry: "Locator authority in all versions."' in text - assert 'project_metadata: "Working-state authority."' in text - assert "custom_registry_field: keep-registry" in text - assert "custom_entry_field:" in text - assert "keep-entry" in text - assert "wb-unrelated:" in text - assert "custom_binding_field: keep-unrelated" in text - assert "workspace_root: /tmp/unrelated-device" in text - finally: - _cleanup_wb_project_modules() - - -def test_init_fails_mechanically_when_required_template_missing(tmp_path: Path) -> None: - work_bundle_root = _minimal_work_bundle_root(tmp_path, include_project_template=False) - config_root = bootstrap_config(tmp_path, work_bundle_root=work_bundle_root) - project = tmp_path / "project" - project.mkdir() - git(project, "init", "-q", "-b", "main") - git(project, "config", "user.email", "test@example.com") - git(project, "config", "user.name", "Test") - - init = run_wb(config_root, "init-project", str(project), "--mode", "single-repository", "--name", "demo") - assert init.returncode == 1, init.stdout + init.stderr - data = json.loads(init.stdout) - assert data["command"] == "init-project" - assert data["status"] == "issues-found" - assert data["failures"] == ["WB_REFERENCE_ASSET_MISSING"] - assert data["missing_reference"].endswith("references/assets/template/project.yaml") - - -def test_healthy_reinit_reports_empty_changed_files(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - rerun = run_wb(config_root, "init-project", str(project), "--mode", "single-repository", "--name", "demo") - assert rerun.returncode == 0, rerun.stdout + rerun.stderr - rerun_data = json.loads(rerun.stdout) - assert rerun_data["status"] == "passed" - assert rerun_data["changed_files"] == [] - - -def test_migrate_force_does_not_overwrite_agents_md(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - bootstrap_dir = project / "references/bootstrap" - bootstrap_dir.mkdir(parents=True, exist_ok=True) - (bootstrap_dir / "agent-bootstrap.md").write_text("# legacy bootstrap\n", encoding="utf-8") - agents_path = project / "AGENTS.md" - custom_agents = "# Custom Agents\n" - agents_path.write_text(custom_agents, encoding="utf-8") - - migrated = run_wb(config_root, "migrate-project", str(project), "--apply", "--name", "demo", "--force") - assert migrated.returncode == 0, migrated.stdout + migrated.stderr - migrate_data = json.loads(migrated.stdout) - assert agents_path.read_text(encoding="utf-8").startswith(custom_agents.rstrip() + "\n\n") - assert migrate_data["agents_status"] == "updated" - assert migrate_data["agents_sync"]["template_checksum_sha256"] - assert str(agents_path) in migrate_data["agents_sync"]["changed_files"] - assert not bootstrap_dir.exists() - assert any("legacy-bootstrap-archive" in path for path in migrate_data["changed_files"]) - report_text = Path(migrate_data["migration_report"]).read_text(encoding="utf-8") - assert "## Retired Legacy Bootstrap Artifacts" in report_text - assert migrate_data["retired_bootstrap"]["archive_root"] in report_text - - -def test_migrate_force_wraps_legacy_agents_template_without_duplicate(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - agents_path = project / "AGENTS.md" - template = (REPO_ROOT / "references/assets/template/AGENTS.md").read_text(encoding="utf-8") - agents_path.write_text(template, encoding="utf-8") - - migrated = run_wb(config_root, "migrate-project", str(project), "--apply", "--name", "demo", "--force") - assert migrated.returncode == 0, migrated.stdout + migrated.stderr - data = json.loads(migrated.stdout) - text = agents_path.read_text(encoding="utf-8") - - assert data["agents_status"] == "updated" - assert data["agents_sync"]["warnings"] == ["legacy-template-wrapped"] - assert text.count("# Work Bundle RULE START") == 1 - assert text.count("# Work Bundle RULE END") == 1 - assert text.startswith("# ========================\n# Work Bundle RULE START") - - -def test_doctor_repair_refreshes_agents_section_and_preserves_user_content(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - agents_path = project / "AGENTS.md" - agents_path.write_text( - "\n".join( - [ - "# User Rules", - "keep this before", - "# ========================", - "# Work Bundle RULE START", - "# ========================", - "stale managed body", - "# ========================", - "# Work Bundle RULE END", - "# ========================", - "keep this after", - "", - ] - ), - encoding="utf-8", - ) - - repaired = run_wb(config_root, "doctor-project", str(project), "--repair") - assert repaired.returncode == 0, repaired.stdout + repaired.stderr - data = json.loads(repaired.stdout) - text = agents_path.read_text(encoding="utf-8") - - assert data["agents_status"] == "updated" - assert data["agents_sync"]["template_checksum_sha256"] - assert str(agents_path) in data["agents_sync"]["changed_files"] - assert "stale managed body" not in text - assert "keep this before" in text - assert "keep this after" in text - assert text.count("# Work Bundle RULE START") == 1 - - -def test_migrate_project_retires_legacy_rules_contract(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - contract_path = project / "rules/contract.yaml" - contract_path.parent.mkdir(parents=True, exist_ok=True) - contract_path.write_text("id: work-bundle-rule-contract\nstatus: current\n", encoding="utf-8") - - migrated = run_wb(config_root, "migrate-project", str(project), "--apply", "--name", "demo") - assert migrated.returncode == 0, migrated.stdout + migrated.stderr - migrate_data = json.loads(migrated.stdout) - - assert not contract_path.exists() - assert migrate_data["retired_rules_contract"]["archive_root"] is not None - artifact = migrate_data["retired_rules_contract"]["artifact"] - assert artifact is not None - assert artifact["source"] == "rules/contract.yaml" - assert artifact["action"] == "archived-and-removed" - archive_root = project / migrate_data["retired_rules_contract"]["archive_root"] - assert (archive_root / "contract.yaml").is_file() - - report_text = Path(migrate_data["migration_report"]).read_text(encoding="utf-8") - assert "## Retired Legacy Rules Contract" in report_text - assert "rules/contract.yaml" in report_text - - -def test_migrate_project_preserves_legacy_root_rule_index_as_non_authority(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - legacy_rule_index = project / "rules/index.yaml" - legacy_rule_index.parent.mkdir(parents=True, exist_ok=True) - legacy_text = "id: legacy-root-rule-index\nrules: []\n" - legacy_rule_index.write_text(legacy_text, encoding="utf-8") - current_rule_index = project / ".work-bundle/rules/index.yaml" - current_text = current_rule_index.read_text(encoding="utf-8") - - migrated = run_wb(config_root, "migrate-project", str(project), "--apply", "--name", "demo") - assert migrated.returncode == 0, migrated.stdout + migrated.stderr - - assert legacy_rule_index.read_text(encoding="utf-8") == legacy_text - assert current_rule_index.read_text(encoding="utf-8") == current_text - - validate = run_wb(config_root, "validate-project", str(project)) - assert validate.returncode == 0, validate.stdout + validate.stderr - data = json.loads(validate.stdout) - assert data["status"] == "passed" - assert data["rules_root_authority"] == ".work-bundle/rules" - assert data["legacy_rules_authority"] == "legacy-artifact" - assert data["legacy_rule_index"] is True - - -def test_validate_project_omits_pointer_diagnostics(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - result = run_wb(config_root, "validate-project", str(project)) - assert result.returncode == 0, result.stdout + result.stderr - data = json.loads(result.stdout) - assert data["path_model"]["work_bundle_root"] - assert "prefer_subagent" not in data - for key in ( - "work_bundle_root_pointer_path", - "work_bundle_root_pointer_exists", - "work_bundle_root_pointer_state", - "work_bundle_root_pointer_diagnostic", - "work_bundle_root_pointer_reason", - ): - assert key not in data - - -def test_wb_work_bundle_root_env_overrides_bootstrap(tmp_path: Path) -> None: - work_bundle_root = _minimal_work_bundle_root(tmp_path) - config_root, project = _init_fixture_project(tmp_path) - metadata_version = "" - for line in (project / ".work-bundle/project.yaml").read_text(encoding="utf-8").splitlines(): - if line.startswith("metadata_version:"): - metadata_version = line.split(":", 1)[1].strip() - break - assert metadata_version != "4" - env = os.environ.copy() - env["WB_CONFIG_ROOT"] = str(config_root) - env["WB_WORK_BUNDLE_ROOT"] = str(work_bundle_root) - result = subprocess.run( - [sys.executable, str(REPO_ROOT / "scripts/wb.py"), "show-project", "--project-root", str(project)], - cwd=project, - env=env, - check=False, - capture_output=True, - text=True, - ) - assert result.returncode == 0, result.stdout + result.stderr - data = json.loads(result.stdout) - assert "path_model" in data - assert data["path_model"]["work_bundle_root"] == str(work_bundle_root.resolve()) - - -def test_migrate_work_bundle_config_migrates_legacy_bootstrap(tmp_path: Path) -> None: - config_root = tmp_path / "config" - config_root.mkdir() - legacy_bootstrap = "\n".join( - [ - "bootstrap_version: 1", - "authority: canonical", - "work_bundle_config_root: ~/.work-bundle", - f"root_pointer: {config_root / 'work-bundle-root.yaml'}", - f"project_registry: {config_root / 'registry/projects.yaml'}", - f"skill_registry: {config_root / 'skills/skill-registry.yaml'}", - "", - ] - ) - (config_root / "bootstrap.yaml").write_text(legacy_bootstrap, encoding="utf-8") - (config_root / "work-bundle-root.yaml").write_text( - "\n".join( - [ - "pointer_version: 1", - f"work_bundle_root: {REPO_ROOT}", - "updated_at: 2026-01-01T00:00:00Z", - "", - ] - ), - encoding="utf-8", - ) - (config_root / "skills").mkdir() - (config_root / "skills" / "skill-registry.yaml").write_text("skills: []\n", encoding="utf-8") - (config_root / "registry").mkdir() - (config_root / "registry" / "projects.yaml").write_text("projects: []\n", encoding="utf-8") - - migrated = run_wb( - config_root, - "migrate-work-bundle-config", - "--toolkit-root", - str(REPO_ROOT), - ) - assert migrated.returncode == 0, migrated.stdout + migrated.stderr - data = json.loads(migrated.stdout) - assert data["legacy_bootstrap"] is True - assert data["work_bundle_root"] == str(REPO_ROOT.resolve()) - bootstrap_text = (config_root / "bootstrap.yaml").read_text(encoding="utf-8") - assert "bootstrap_version: v1" in bootstrap_text - assert f"work_bundle_root: {REPO_ROOT.resolve()}" in bootstrap_text - assert "$work_bundle_config_root/registry/skill-registry.yaml" in bootstrap_text - assert (config_root / "registry" / "skill-registry.yaml").is_file() - assert (config_root / "archive").is_dir() - assert not (config_root / "work-bundle-root.yaml").exists() - if data.get("retired_root_pointer"): - assert Path(data["retired_root_pointer"]).is_file() - - -def test_templates_omit_retired_subagent_preference() -> None: - bootstrap_text = (REPO_ROOT / "references/assets/template/bootstrap.yaml").read_text(encoding="utf-8") - project_text = (REPO_ROOT / "references/assets/template/project.yaml").read_text(encoding="utf-8") - agents_text = (REPO_ROOT / "references/assets/template/AGENTS.md").read_text(encoding="utf-8") - - assert "prefer_subagent" not in bootstrap_text - assert "prefer_subagent" not in project_text - assert "agents_sync:" in project_text - assert "template_checksum_sha256: \"\"" in project_text - assert "status: never-synced" in project_text - assert "prefer_subagent" not in agents_text - - -def test_project_metadata_v3_records_git_and_codegraph_state(tmp_path: Path) -> None: - wb_project = _import_wb_project() - try: - project = tmp_path / "project" - project.mkdir() - git(project, "init", "-q", "-b", "main") - git(project, "config", "user.email", "test@example.com") - git(project, "config", "user.name", "Test") - (project / "README.md").write_text("# Demo\n", encoding="utf-8") - git(project, "add", "README.md") - git(project, "commit", "-m", "chore: seed") - head = git(project, "rev-parse", "HEAD") - - rendered = wb_project._render_project_metadata(project, "demo") - metadata = wb_project._metadata_source_repositories(rendered) - - assert "metadata_version: 3" in rendered - assert "workspace_mode: single-repository" in rendered - assert "operation_policy:" in rendered - assert metadata[0]["id"] == "demo-main" - assert metadata[0]["git_repository"] is True - assert metadata[0]["working_branch"] == "main" - assert metadata[0]["last_commit_id"] == head - assert metadata[0]["baseline_status"] == "current" - assert metadata[0]["codegraph"]["status"] == "not-indexed" - assert metadata[0]["codegraph"]["reason"] == "no-index" - finally: - _cleanup_wb_project_modules() - - -def test_validate_project_reports_metadata_branch_mismatch(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - metadata_path = project / ".work-bundle/project.yaml" - metadata_path.write_text( - metadata_path.read_text(encoding="utf-8").replace("expected_branch: main", "expected_branch: wrong-branch"), - encoding="utf-8", - ) - - result = run_wb(config_root, "validate-project", str(project)) - assert result.returncode == 1, result.stdout + result.stderr - data = json.loads(result.stdout) - - assert data["status"] == "issues-found" - assert "WB_PROJECT_METADATA_INVALID" in data["failures"] - assert "source_repositories[0].branch_mismatch" in data["failures"] - - -def test_validate_project_reports_stale_metadata_baseline(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - metadata_path = project / ".work-bundle/project.yaml" - current_head = git(project, "rev-parse", "HEAD") - metadata_path.write_text( - metadata_path.read_text(encoding="utf-8") - .replace("observed_head:", f"observed_head: {current_head}") - .replace("baseline_status: unborn", "baseline_status: current"), - encoding="utf-8", - ) - git(project, "add", "-f", ".work-bundle/project.yaml") - git(project, "commit", "-m", "chore: refresh metadata baseline") - refreshed_head = git(project, "rev-parse", "HEAD") - metadata_path.write_text( - metadata_path.read_text(encoding="utf-8").replace(current_head, refreshed_head), - encoding="utf-8", - ) - (project / "README.md").write_text("# Later change\n", encoding="utf-8") - git(project, "add", "README.md") - git(project, "commit", "-m", "chore: later change") - - result = run_wb(config_root, "validate-project", str(project)) - assert result.returncode == 1, result.stdout + result.stderr - data = json.loads(result.stdout) - - assert "WB_PROJECT_METADATA_INVALID" in data["failures"] - assert "source_repositories[0].baseline_status_stale" in data["failures"] - - -def test_force_refresh_preserves_truth_and_development_checkouts(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - development = tmp_path / "project-development" - development.mkdir() - git(development, "init", "-q", "-b", "feature/demo") - git(development, "config", "user.email", "test@example.com") - git(development, "config", "user.name", "Test") - (development / "README.md").write_text("# Development\n", encoding="utf-8") - git(development, "add", "README.md") - git(development, "commit", "-m", "chore: seed development") - - registry_path = config_root / "registry" / "projects.yaml" - registry_path.write_text( - "\n".join( - [ - "projects:", - " - slug: demo", - " name: demo", - f" work_bundle_root: {project.resolve() / '.work-bundle'}", - f" knowledge_root: {project.resolve() / '.work-bundle' / 'knowledge'}", - " aliases: []", - " source_repositories:", - " - id: demo-main", - f" path: {project.resolve()}", - " checkout_role: truth", - " work_dir: false", - ' remote: ""', - " git_repository: true", - " - id: demo-development", - f" path: {development.resolve()}", - " checkout_role: development", - " work_dir: true", - ' remote: ""', - " git_repository: true", - " status: active", - " updated_at: 2026-01-01", - "", - ] - ), - encoding="utf-8", - ) - metadata_path = project / ".work-bundle/project.yaml" - metadata_path.write_text( - metadata_path.read_text(encoding="utf-8").replace( - "operation_policy:\n", - "custom_user_field: keep-me\n\noperation_policy:\n", - ), - encoding="utf-8", - ) - - refreshed = run_wb(config_root, "init-project", str(project), "--mode", "single-repository", "--name", "demo", "--force") - assert refreshed.returncode == 0, refreshed.stdout + refreshed.stderr - metadata_text = metadata_path.read_text(encoding="utf-8") - wb_project = _import_wb_project() - try: - repositories = wb_project._metadata_source_repositories(metadata_text) - finally: - _cleanup_wb_project_modules() - - assert [repo["id"] for repo in repositories] == ["demo-main", "demo-development"] - assert [repo["checkout_role"] for repo in repositories] == ["truth", "development"] - assert repositories[0]["working_branch"] == "main" - assert repositories[1]["working_branch"] == "feature/demo" - assert repositories[1]["last_commit_id"] == git(development, "rev-parse", "HEAD") - assert "custom_user_field: keep-me" in metadata_text - - -def test_doctor_repair_refreshes_all_registered_checkout_baselines(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - metadata_path = project / ".work-bundle/project.yaml" - recorded_head = git(project, "rev-parse", "HEAD") - (project / "README.md").write_text("# Advanced\n", encoding="utf-8") - git(project, "add", "README.md") - git(project, "commit", "-m", "chore: advance truth branch") - actual_head = git(project, "rev-parse", "HEAD") - assert recorded_head != actual_head - metadata_text = metadata_path.read_text(encoding="utf-8") - metadata_lines = metadata_text.splitlines() - baseline_index = next(index for index, line in enumerate(metadata_lines) if line.strip().startswith("observed_head:")) - metadata_lines[baseline_index] = f" observed_head: {'0' * 40}" - metadata_path.write_text("\n".join(metadata_lines) + "\n", encoding="utf-8") - - diagnosed = run_wb(config_root, "doctor-project", str(project)) - assert diagnosed.returncode == 1 - assert "source_repositories[0].baseline_status_stale" in json.loads(diagnosed.stdout)["failures"] - - repaired = run_wb(config_root, "doctor-project", str(project), "--repair") - assert repaired.returncode == 0, repaired.stdout + repaired.stderr - repair_data = json.loads(repaired.stdout) - assert str(metadata_path) in repair_data["changed_files"] - repaired_baseline = repair_data["project_source_repositories"][0]["last_commit_id"] - assert repaired_baseline != "0" * 40 - assert git(project, "merge-base", "--is-ancestor", repaired_baseline, git(project, "rev-parse", "HEAD")) == "" - assert repair_data["project_source_repositories"][0]["checkout_role"] == "truth" - - -def test_doctor_force_refreshes_v3_member_observations(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - metadata_path = project / ".work-bundle/project.yaml" - metadata_text = metadata_path.read_text(encoding="utf-8") - metadata_text = metadata_text.replace( - "workspace_mode: single-repository", "workspace_mode: multi-repository" - ) - metadata_lines = metadata_text.splitlines() - baseline_index = next( - index for index, line in enumerate(metadata_lines) if line.strip().startswith("observed_head:") - ) - metadata_lines[baseline_index] = f" observed_head: {'0' * 40}" - metadata_path.write_text("\n".join(metadata_lines) + "\n", encoding="utf-8") - (project / "README.md").write_text("# Advanced member\n", encoding="utf-8") - git(project, "add", "README.md") - git(project, "commit", "-m", "chore: advance member") - actual_head = git(project, "rev-parse", "HEAD") - - repaired = run_wb(config_root, "doctor-project", str(project), "--repair", "--force") - - assert repaired.returncode == 0, repaired.stdout + repaired.stderr - assert f"observed_head: {actual_head}" in metadata_path.read_text(encoding="utf-8") - - -def test_migrate_project_upgrades_v1_metadata_and_preserves_unknown_fields(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - metadata_path = project / ".work-bundle/project.yaml" - metadata_path.write_text( - "\n".join( - [ - "metadata_version: 1", - "authority: canonical", - f"project_root: {project.resolve()}", - "industry: legacy", - "custom_user_field: keep-me", - "migration:", - " authority_owner: /wb-initialize-project", - "", - ] - ), - encoding="utf-8", - ) - - migrated = run_wb(config_root, "migrate-project", str(project), "--apply", "--name", "demo") - assert migrated.returncode == 0, migrated.stdout + migrated.stderr - text = metadata_path.read_text(encoding="utf-8") - - assert "metadata_version: 3" in text - assert "custom_user_field: keep-me" in text - assert "operation_policy:" in text - assert "source_repositories:" in text - - -def test_migrate_project_routes_registry_multi_source_to_workspace_migration(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - extra_repo = tmp_path / "library" - extra_repo.mkdir() - git(extra_repo, "init", "-q", "-b", "main") - resolved_project = project.resolve() - resolved_extra = extra_repo.resolve() - registry_path = config_root / "registry" / "projects.yaml" - registry_path.write_text( - "\n".join( - [ - "projects:", - " - slug: demo", - " name: demo", - f" work_bundle_root: {resolved_project / '.work-bundle'}", - f" knowledge_root: {resolved_project / '.work-bundle' / 'knowledge'}", - " aliases: []", - " repository_origins:", - " - id: demo-main", - f" origin_path: {resolved_project}", - " git_repository: true", - " - id: demo-library", - f" origin_path: {resolved_extra}", - " git_repository: true", - " source_repositories:", - " - id: demo-main", - f" path: {resolved_project}", - " work_dir: true", - ' remote: ""', - " git_repository: true", - " status: active", - " updated_at: 2026-01-01", - "", - ] - ), - encoding="utf-8", - ) - metadata_path = project / ".work-bundle/project.yaml" - metadata_path.write_text( - "\n".join( - [ - "metadata_version: 1", - "authority: canonical", - f"project_root: {resolved_project}", - "industry: legacy", - "", - ] - ), - encoding="utf-8", - ) - - before = metadata_path.read_bytes() - migrated = run_wb(config_root, "migrate-project", str(project), "--dry-run", "--name", "demo") - data = json.loads(migrated.stdout) - assert migrated.returncode == 1 - assert data["mode"] == "multi-repository-migration-required" - assert data["topology_assessment"]["required_command"] == "migrate-to-multi-repository" - assert metadata_path.read_bytes() == before - - -def test_migrate_project_accepts_same_id_origin_and_member_paths(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - origin = tmp_path / "origin-locator" - origin.mkdir() - registry_path = config_root / "registry" / "projects.yaml" - registry_path.write_text( - "\n".join( - [ - "projects:", - " - slug: demo", - " name: demo", - f" work_bundle_root: {project.resolve() / '.work-bundle'}", - f" knowledge_root: {project.resolve() / '.work-bundle' / 'knowledge'}", - " aliases: []", - " repository_origins:", - " - id: demo-main", - f" origin_path: {origin.resolve()}", - " git_repository: true", - " source_repositories:", - " - id: demo-main", - f" path: {project.resolve()}", - " work_dir: true", - ' remote: ""', - " git_repository: true", - " status: active", - " updated_at: 2026-01-01", - "", - ] - ), - encoding="utf-8", - ) - metadata_path = project / ".work-bundle/project.yaml" - metadata_path.write_text( - "\n".join( - [ - "metadata_version: 1", - "authority: canonical", - f"project_root: {project.resolve()}", - "industry: legacy", - "", - ] - ), - encoding="utf-8", - ) - - migrated = run_wb(config_root, "migrate-project", str(project), "--dry-run", "--name", "demo") - - assert migrated.returncode == 1 - data = json.loads(migrated.stdout) - assert data["mode"] == "multi-repository-migration-required" - assert data["topology_assessment"]["conflicts"] == [] - assert data["topology_assessment"]["required_command"] == "migrate-to-multi-repository" - - -def _seed_committed_repository(path: Path) -> None: - path.mkdir() - git(path, "init", "-q", "-b", "main") - git(path, "config", "user.email", "test@example.com") - git(path, "config", "user.name", "Test") - (path / "README.md").write_text("seed\n", encoding="utf-8") - git(path, "add", "README.md") - git(path, "commit", "-q", "-m", "seed") - - -def _init_multi_workspace(tmp_path: Path) -> tuple[Path, Path]: - config_root = bootstrap_config(tmp_path) - workspace = tmp_path / "workspace" - _seed_committed_repository(workspace) - initialized = run_wb( - config_root, - "init-project", - str(workspace), - "--mode", - "multi-repository", - "--workspace-root", - str(workspace), - "--name", - "multi", - ) - assert initialized.returncode == 0, initialized.stdout + initialized.stderr - assert (workspace / "script/index.yaml").is_file() - assert (workspace / "credentials/credentials.yaml").is_file() - assert "workspace_resources:" in (workspace / ".work-bundle/project.yaml").read_text(encoding="utf-8") - return config_root, workspace - - -def test_provision_member_cli_publishes_both_authorities_and_replays_idempotently(tmp_path: Path) -> None: - config_root, workspace = _init_multi_workspace(tmp_path) - origin = tmp_path / "origin" - _seed_committed_repository(origin) - args = ( - "provision-member", - "--workspace-root", - str(workspace), - "--workspace-slug", - "multi", - "--origin", - str(origin), - "--repository-id", - "repo-two", - "--working-branch", - "feature-two", - "--base-ref", - "HEAD", - ) - proposed = run_wb(config_root, *args, "--dry-run") - assert proposed.returncode == 0 - assert json.loads(proposed.stdout)["status"] == "proposed" - - metadata = workspace / ".work-bundle/project.yaml" - registry = config_root / "registry/projects.yaml" - metadata.write_text(metadata.read_text(encoding="utf-8") + "custom_workspace_field: keep\n", encoding="utf-8") - registry.write_text( - registry.read_text(encoding="utf-8").replace(" status: active", " custom_locator_field: keep\n status: active", 1), - encoding="utf-8", - ) - credential = workspace / "credentials/credentials.yaml" - credential_stat = (credential.stat().st_mode, credential.stat().st_mtime_ns, credential.stat().st_size) - origin_head = git(origin, "rev-parse", "HEAD") - origin_status = git(origin, "status", "--short") - - applied = run_wb(config_root, *args, "--apply") - data = json.loads(applied.stdout) - assert applied.returncode == 0, applied.stdout + applied.stderr - assert data["status"] == "passed" - assert data["transaction"]["state"] == "published" - assert data["transaction"]["metadata_status"] == "published" - assert data["transaction"]["registry_status"] == "published" - assert "pending" not in json.dumps(data) - assert ' - id: "repo-two"\n project_root:' in metadata.read_text(encoding="utf-8") - assert "custom_workspace_field: keep" in metadata.read_text(encoding="utf-8") - assert "repository_origins:" in registry.read_text(encoding="utf-8") - assert ' - id: "repo-two"' in registry.read_text(encoding="utf-8") - assert "custom_locator_field: keep" in registry.read_text(encoding="utf-8") - assert (credential.stat().st_mode, credential.stat().st_mtime_ns, credential.stat().st_size) == credential_stat - assert git(origin, "rev-parse", "HEAD") == origin_head - assert git(origin, "status", "--short") == origin_status - shown = run_wb(config_root, "show-project", "--project-root", str(workspace)) - assert shown.returncode == 0, shown.stdout + shown.stderr - assert {item["id"] for item in json.loads(shown.stdout)["project_source_repositories"]} >= {"repo-two"} - nested_session = run_wb( - config_root, "session-start", "--project-root", str(workspace / "repo-two"), - "--dry-run", "--json", - ) - assert nested_session.returncode == 0, nested_session.stdout + nested_session.stderr - assert json.loads(nested_session.stdout)["project_root"] == str(workspace.resolve()) - validated = run_wb(config_root, "validate-project", str(workspace), "--dry-run") - assert validated.returncode == 0, validated.stdout + validated.stderr - - metadata_bytes = metadata.read_bytes() - registry_bytes = registry.read_bytes() - record = workspace / ".work-bundle/transactions/provision-repo-two.json" - record_bytes = record.read_bytes() - mtimes = (metadata.stat().st_mtime_ns, registry.stat().st_mtime_ns, record.stat().st_mtime_ns) - replayed = run_wb(config_root, *args, "--apply") - replay_data = json.loads(replayed.stdout) - assert replayed.returncode == 0 - assert replay_data["idempotent"] is True - assert replay_data["changed_files"] == [] - assert metadata.read_bytes() == metadata_bytes - assert registry.read_bytes() == registry_bytes - assert record.read_bytes() == record_bytes - assert (metadata.stat().st_mtime_ns, registry.stat().st_mtime_ns, record.stat().st_mtime_ns) == mtimes - - record.unlink() - metadata_mtime = metadata.stat().st_mtime_ns - registry_mtime = registry.stat().st_mtime_ns - converged = run_wb(config_root, *args, "--apply") - converged_data = json.loads(converged.stdout) - assert converged.returncode == 0, converged.stdout + converged.stderr - assert converged_data["idempotent"] is True - assert converged_data["transaction"]["resume_source"] == "converged-authorities" - assert converged_data["changed_files"] == [] - assert not record.exists() - assert metadata.stat().st_mtime_ns == metadata_mtime - assert registry.stat().st_mtime_ns == registry_mtime - - -def test_provision_member_cli_rejects_unrelated_non_empty_target(tmp_path: Path) -> None: - config_root, workspace = _init_multi_workspace(tmp_path) - origin = tmp_path / "origin" - _seed_committed_repository(origin) - target = workspace / "repo-two" - target.mkdir() - (target / "user-file").write_text("preserve\n", encoding="utf-8") - - applied = run_wb( - config_root, - "provision-member", - "--workspace-root", - str(workspace), - "--workspace-slug", - "multi", - "--origin", - str(origin), - "--repository-id", - "repo-two", - "--working-branch", - "feature-two", - "--apply", - ) - - assert applied.returncode == 1 - assert json.loads(applied.stdout)["failure_code"] == "WB_WORKTREE_TARGET_COLLISION" - assert (target / "user-file").read_text(encoding="utf-8") == "preserve\n" - assert not (workspace / ".work-bundle/git/repo-two.git").exists() - - -def test_provision_member_adopts_exact_verified_orphan_without_recovery_record(tmp_path: Path) -> None: - config_root, workspace = _init_multi_workspace(tmp_path) - origin = tmp_path / "origin" - _seed_committed_repository(origin) - script_root = REPO_ROOT / "scripts/work-bundle" - sys.path.insert(0, str(script_root)) - try: - import worktree # type: ignore[import-not-found] - - worktree.provision_member(workspace, origin, "repo-two", "feature-two") - finally: - sys.path.remove(str(script_root)) - for module_name in ("worktree", "workspace"): - sys.modules.pop(module_name, None) - record = workspace / ".work-bundle/transactions/provision-repo-two.json" - assert not record.exists() - args = ( - "provision-member", "--workspace-root", str(workspace), - "--workspace-slug", "multi", "--origin", str(origin), - "--repository-id", "repo-two", "--working-branch", "feature-two", - "--base-ref", "HEAD", - ) - proposed = run_wb(config_root, *args, "--dry-run") - proposal_data = json.loads(proposed.stdout) - assert proposed.returncode == 0, proposed.stdout + proposed.stderr - assert proposal_data["transaction"]["state"] == "verified" - assert proposal_data["transaction"]["resume_source"] == "verified-orphan" - applied = run_wb(config_root, *args, "--apply") - data = json.loads(applied.stdout) - assert applied.returncode == 0, applied.stdout + applied.stderr - assert data["status"] == "passed" - assert data["transaction"]["state"] == "published" - assert ' - id: "repo-two"' in (workspace / ".work-bundle/project.yaml").read_text(encoding="utf-8") - - -def test_cleanup_member_command_removes_only_recorded_unpublished_owned_checkout(tmp_path: Path) -> None: - config_root, workspace = _init_multi_workspace(tmp_path) - origin = tmp_path / "origin" - _seed_committed_repository(origin) - script_root = REPO_ROOT / "scripts/work-bundle" - sys.path.insert(0, str(script_root)) - try: - import member # type: ignore[import-not-found] - import worktree # type: ignore[import-not-found] - - provisioned = worktree.provision_member(workspace, origin, "repo-two", "feature-two") - transaction_id = member._transaction_id(workspace, origin, "repo-two", "feature-two", "HEAD") - context = { - "transaction_id": transaction_id, - "workspace_root": str(workspace.resolve()), - "workspace_slug": "multi", - "origin": str(origin.resolve()), - "repository_id": "repo-two", - "branch": "feature-two", - "base_ref": "HEAD", - } - member._write_record(member._record_path(workspace, "repo-two"), { - "id": transaction_id, "state": "verified", "context": context, - "checkout_owned": True, "registry_status": "unchanged", - "metadata_status": "unchanged", "member": member._member_result(provisioned, transaction_id), - }) - finally: - sys.path.remove(str(script_root)) - for module_name in ("member", "migration", "worktree", "workspace", "project", "core", "bootstrap_config"): - sys.modules.pop(module_name, None) - dry_run = run_wb( - config_root, "cleanup-member", "--workspace-root", str(workspace), - "--repository-id", "repo-two", "--dry-run", - ) - assert dry_run.returncode == 0 - assert (workspace / "repo-two").is_dir() - applied = run_wb( - config_root, "cleanup-member", "--workspace-root", str(workspace), - "--repository-id", "repo-two", "--apply", - ) - assert applied.returncode == 0, applied.stdout + applied.stderr - assert not (workspace / "repo-two").exists() - assert not (workspace / ".work-bundle/git/repo-two.git").exists() - record = json.loads((workspace / ".work-bundle/transactions/provision-repo-two.json").read_text(encoding="utf-8")) - assert record["state"] == "cleaned" - - -def test_show_project_accepts_workspace_root_alias(tmp_path: Path) -> None: - config_root, workspace = _init_multi_workspace(tmp_path) - shown = run_wb(config_root, "show-project", "--workspace-root", str(workspace)) - assert shown.returncode == 0, shown.stdout + shown.stderr - assert json.loads(shown.stdout)["project_root"] == str(workspace.resolve()) - - -def test_migrate_to_multi_repository_cli_dry_run_accepts_external_origin(tmp_path: Path) -> None: - config_root = bootstrap_config(tmp_path) - authority = tmp_path / "authority" - origin = tmp_path / "origin" - target = tmp_path / "workspace" - authority.mkdir() - (authority / ".work-bundle").mkdir() - (authority / ".work-bundle/project.yaml").write_text( - "metadata_version: 2\nauthority: canonical\n", encoding="utf-8" - ) - _seed_committed_repository(origin) - proposed = run_wb( - config_root, - "migrate-to-multi-repository", str(authority), - "--target-workspace-root", str(target), - "--origin", str(origin), - "--repository-id", "repo-one", - "--repository-name", "Repository One", - "--workspace-slug", "workspace-one", - "--working-branch", "feature/workspace", - "--base-ref", "HEAD", - "--dry-run", - ) - data = json.loads(proposed.stdout) - assert proposed.returncode == 0, proposed.stdout + proposed.stderr - assert data["status"] == "passed" - assert data["result"]["member_origin_root"] == str(origin.resolve()) - assert data["result"]["changed_files"] == [] - missing_origin = run_wb( - config_root, - "migrate-to-multi-repository", str(authority), - "--target-workspace-root", str(target), - "--repository-id", "repo-one", - "--repository-name", "Repository One", - "--workspace-slug", "workspace-one", - "--working-branch", "feature/workspace", - "--dry-run", - ) - assert missing_origin.returncode == 1 - assert json.loads(missing_origin.stdout)["failure_code"] == "WB_MIGRATION_ORIGIN_REQUIRED" - - -@pytest.mark.parametrize("stage", ["metadata-publication", "registry-publication"]) -def test_provision_member_publication_failure_restores_authorities_and_owned_checkout( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - stage: str, -) -> None: - config_root, workspace = _init_multi_workspace(tmp_path) - origin = tmp_path / "origin" - _seed_committed_repository(origin) - monkeypatch.setenv("WB_CONFIG_ROOT", str(config_root)) - script_root = REPO_ROOT / "scripts/work-bundle" - sys.path.insert(0, str(script_root)) - try: - import member # type: ignore[import-not-found] - - metadata = workspace / ".work-bundle/project.yaml" - registry = config_root / "registry/projects.yaml" - metadata_before = metadata.read_bytes() - registry_before = registry.read_bytes() - with pytest.raises(member.MemberLifecycleError): - member.provision_member_lifecycle( - workspace, - origin, - "repo-two", - "feature-two", - workspace_slug="multi", - fail_stage=stage, - ) - assert metadata.read_bytes() == metadata_before - assert registry.read_bytes() == registry_before - assert not (workspace / "repo-two").exists() - assert not (workspace / ".work-bundle/git/repo-two.git").exists() - record = workspace / ".work-bundle/transactions/provision-repo-two.json" - assert json.loads(record.read_text(encoding="utf-8"))["state"] == "failed" - finally: - sys.path.remove(str(script_root)) - for module_name in ("member", "migration", "worktree", "project", "core", "bootstrap_config"): - sys.modules.pop(module_name, None) - - -def test_provision_member_resumes_matching_verified_checkout(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - config_root, workspace = _init_multi_workspace(tmp_path) - origin = tmp_path / "origin" - _seed_committed_repository(origin) - monkeypatch.setenv("WB_CONFIG_ROOT", str(config_root)) - script_root = REPO_ROOT / "scripts/work-bundle" - sys.path.insert(0, str(script_root)) - try: - import member # type: ignore[import-not-found] - import worktree # type: ignore[import-not-found] - - provisioned = worktree.provision_member(workspace, origin, "repo-two", "feature-two") - transaction_id = member._transaction_id(workspace, origin, "repo-two", "feature-two", "HEAD") - context = { - "transaction_id": transaction_id, - "workspace_root": str(workspace.resolve()), - "workspace_slug": "multi", - "origin": str(origin.resolve()), - "repository_id": "repo-two", - "branch": "feature-two", - "base_ref": "HEAD", - } - member._write_record( - member._record_path(workspace, "repo-two"), - { - "id": transaction_id, - "state": "verified", - "context": context, - "checkout_owned": True, - "registry_status": "unchanged", - "metadata_status": "unchanged", - "member": member._member_result(provisioned, transaction_id), - }, - ) - finally: - sys.path.remove(str(script_root)) - for module_name in ("member", "migration", "worktree", "project", "core", "bootstrap_config"): - sys.modules.pop(module_name, None) - - resumed = run_wb( - config_root, - "provision-member", - "--workspace-root", - str(workspace), - "--workspace-slug", - "multi", - "--origin", - str(origin), - "--repository-id", - "repo-two", - "--working-branch", - "feature-two", - "--base-ref", - "HEAD", - "--apply", - ) - assert resumed.returncode == 0, resumed.stdout + resumed.stderr - assert json.loads(resumed.stdout)["transaction"]["state"] == "published" - - -def test_runtime_has_no_subagent_preference_resolver(tmp_path: Path, monkeypatch) -> None: - script_root = REPO_ROOT / "scripts" / "work-bundle" - module = sys.modules.get("core") - module_file = Path(getattr(module, "__file__", "")) if module is not None else None - if module_file is not None and script_root not in module_file.parents: - sys.modules.pop("core", None) - sys.path.insert(0, str(script_root)) - try: - import core # type: ignore[import-not-found] - - assert not hasattr(core, "resolve_effective_prefer_subagent") - assert "prefer_subagent" not in core.resolve_bootstrap_runtime() - finally: - if sys.path and sys.path[0] == str(REPO_ROOT / "scripts" / "work-bundle"): - sys.path.pop(0) - sys.modules.pop("core", None) - - -def test_agents_sync_creates_missing_agents_md_and_updates_metadata(tmp_path: Path) -> None: - wb_project = _import_wb_project() - try: - project = tmp_path / "project" - metadata = project / ".work-bundle/project.yaml" - metadata.parent.mkdir(parents=True) - metadata.write_text(wb_project._render_project_metadata(project), encoding="utf-8") - - result = wb_project.sync_agents_managed_section(project) - agents_text = (project / "AGENTS.md").read_text(encoding="utf-8") - metadata_text = metadata.read_text(encoding="utf-8") - - assert result["agents_status"] == "created" - assert wb_project.AGENTS_RULE_START_MARKER in agents_text - assert wb_project.AGENTS_RULE_END_MARKER in agents_text - managed = agents_text.split(wb_project.AGENTS_RULE_START_MARKER, 1)[1].split( - wb_project.AGENTS_RULE_END_MARKER, 1 - )[0] - assert "checksum" not in managed.lower() - assert f'template_checksum_sha256: "{result["template_checksum_sha256"]}"' in metadata_text - assert "status: current" in metadata_text - finally: - _cleanup_wb_project_modules() - - -def test_agents_sync_appends_to_existing_agents_without_section(tmp_path: Path) -> None: - wb_project = _import_wb_project() - try: - project = tmp_path / "project" - metadata = project / ".work-bundle/project.yaml" - metadata.parent.mkdir(parents=True) - metadata.write_text(wb_project._render_project_metadata(project), encoding="utf-8") - agents = project / "AGENTS.md" - agents.write_text("# User Rules\nkeep this\n", encoding="utf-8") - - result = wb_project.sync_agents_managed_section(project) - text = agents.read_text(encoding="utf-8") - - assert result["agents_status"] == "updated" - assert text.startswith("# User Rules\nkeep this\n\n") - assert text.count(wb_project.AGENTS_RULE_START_MARKER) == 1 - finally: - _cleanup_wb_project_modules() - - -def test_agents_sync_replaces_stale_managed_section(tmp_path: Path) -> None: - wb_project = _import_wb_project() - try: - project = tmp_path / "project" - metadata = project / ".work-bundle/project.yaml" - metadata.parent.mkdir(parents=True) - metadata.write_text(wb_project._render_project_metadata(project), encoding="utf-8") - stale_block = ( - f"{wb_project.AGENTS_RULE_START_MARKER}\n" - "old managed body\n" - f"{wb_project.AGENTS_RULE_END_MARKER}\n" - ) - agents = project / "AGENTS.md" - agents.write_text(f"# User\n{stale_block}tail\n", encoding="utf-8") - - result = wb_project.sync_agents_managed_section(project) - text = agents.read_text(encoding="utf-8") - - assert result["agents_status"] == "updated" - assert "old managed body" not in text - assert text.startswith("# User\n") - assert text.endswith("tail\n") - assert result["template_checksum_sha256"] in metadata.read_text(encoding="utf-8") - finally: - _cleanup_wb_project_modules() - - -def test_agents_sync_current_section_is_idempotent(tmp_path: Path) -> None: - wb_project = _import_wb_project() - try: - project = tmp_path / "project" - metadata = project / ".work-bundle/project.yaml" - metadata.parent.mkdir(parents=True) - metadata.write_text(wb_project._render_project_metadata(project), encoding="utf-8") - - first = wb_project.sync_agents_managed_section(project) - agents_before = (project / "AGENTS.md").read_text(encoding="utf-8") - metadata_before = metadata.read_text(encoding="utf-8") - second = wb_project.sync_agents_managed_section(project) - - assert first["agents_status"] == "created" - assert second["agents_status"] == "unchanged" - assert second["changed_files"] == [] - assert (project / "AGENTS.md").read_text(encoding="utf-8") == agents_before - assert metadata.read_text(encoding="utf-8") == metadata_before - finally: - _cleanup_wb_project_modules() - - -def test_agents_sync_consolidates_multiple_managed_sections(tmp_path: Path) -> None: - wb_project = _import_wb_project() - try: - project = tmp_path / "project" - metadata = project / ".work-bundle/project.yaml" - metadata.parent.mkdir(parents=True) - metadata.write_text(wb_project._render_project_metadata(project), encoding="utf-8") - block = ( - f"{wb_project.AGENTS_RULE_START_MARKER}\n" - "old managed body\n" - f"{wb_project.AGENTS_RULE_END_MARKER}\n" - ) - agents = project / "AGENTS.md" - agents.write_text(f"top\n{block}middle\n{block}bottom\n", encoding="utf-8") - - result = wb_project.sync_agents_managed_section(project) - text = agents.read_text(encoding="utf-8") - - assert result["agents_status"] == "updated" - assert result["warnings"] == ["multiple-managed-sections-consolidated"] - assert text.count(wb_project.AGENTS_RULE_START_MARKER) == 1 - assert "top\n" in text - assert "middle\n" in text - assert "bottom\n" in text - finally: - _cleanup_wb_project_modules() - - -def test_init_project_metadata_records_agents_sync_checksum(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - metadata = project / ".work-bundle/project.yaml" - agents_text = (project / "AGENTS.md").read_text(encoding="utf-8") - metadata_text = metadata.read_text(encoding="utf-8") - - assert "agents_sync:" in metadata_text - assert "status: current" in metadata_text - assert "template_checksum_sha256: \"\"" not in metadata_text - assert "# Work Bundle RULE START" in agents_text - assert "# Work Bundle RULE END" in agents_text - managed = agents_text.split("# Work Bundle RULE START", 1)[1].split("# Work Bundle RULE END", 1)[0] - assert "checksum" not in managed.lower() - - rerun = run_wb(config_root, "init-project", str(project), "--mode", "single-repository", "--name", "demo") - assert rerun.returncode == 0, rerun.stdout + rerun.stderr - assert json.loads(rerun.stdout)["changed_files"] == [] - - -def test_retired_subagent_preference_command_is_not_exposed(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - - result = run_wb(config_root, "set-prefer-subagent", "enable", "--scope", "global", "--project-root", str(project)) - assert result.returncode == 2 - assert "unknown command" in result.stderr - - -def test_legacy_subagent_preference_input_does_not_appear_in_show_output(tmp_path: Path) -> None: - config_root, project = _init_fixture_project(tmp_path) - metadata = project / ".work-bundle/project.yaml" - metadata.write_text(metadata.read_text(encoding="utf-8") + "prefer_subagent: true\n", encoding="utf-8") - show = run_wb(config_root, "show-project", "--project-root", str(project)) - assert show.returncode == 0, show.stdout + show.stderr - show_data = json.loads(show.stdout) - assert "prefer_subagent" not in show_data - - -def test_migrate_work_bundle_config_resolves_legacy_pointer_without_toolkit_flag(tmp_path: Path) -> None: - config_root = tmp_path / "config" - config_root.mkdir() - legacy_bootstrap = "\n".join( - [ - "bootstrap_version: 1", - "authority: canonical", - f"root_pointer: {config_root / 'work-bundle-root.yaml'}", - f"project_registry: {config_root / 'registry/projects.yaml'}", - f"skill_registry: {config_root / 'skills/skill-registry.yaml'}", - "", - ] - ) - (config_root / "bootstrap.yaml").write_text(legacy_bootstrap, encoding="utf-8") - (config_root / "work-bundle-root.yaml").write_text( - "\n".join( - [ - "pointer_version: 1", - f"work_bundle_root: {REPO_ROOT}", - "updated_at: 2026-01-01T00:00:00Z", - "", - ] - ), - encoding="utf-8", - ) - (config_root / "skills").mkdir() - (config_root / "skills" / "skill-registry.yaml").write_text("skills: []\n", encoding="utf-8") - (config_root / "registry").mkdir() - (config_root / "registry" / "projects.yaml").write_text("projects: []\n", encoding="utf-8") - - migrated = run_wb(config_root, "migrate-work-bundle-config") - assert migrated.returncode == 0, migrated.stdout + migrated.stderr - data = json.loads(migrated.stdout) - assert data["work_bundle_root"] == str(REPO_ROOT.resolve()) - assert not (config_root / "work-bundle-root.yaml").exists() diff --git a/tests/test_public_runtime_hydration.py b/tests/test_public_runtime_hydration.py new file mode 100644 index 0000000..057a350 --- /dev/null +++ b/tests/test_public_runtime_hydration.py @@ -0,0 +1,140 @@ +from __future__ import annotations + +import importlib.util +import json +import os +from pathlib import Path +import subprocess +import sys + +import pytest + + +REPO_ROOT = Path(__file__).resolve().parents[1] + + +def load_wrapper(name: str): + path = REPO_ROOT / f"scripts/{name}.py" + spec = importlib.util.spec_from_file_location(f"runtime_wrapper_{name}", path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +@pytest.mark.parametrize("name", ["orch", "wb"]) +def test_public_wrapper_declares_same_pinned_runtime(name: str) -> None: + text = (REPO_ROOT / f"scripts/{name}.py").read_text(encoding="utf-8") + assert '# requires-python = ">=3.13"' in text + assert '"pyyaml==6.0.3"' in text + assert '"jsonschema==4.25.1"' in text + wrapper = load_wrapper(name) + assert wrapper.RUNTIME_DEPENDENCIES == ( + ("yaml", "pyyaml"), + ("jsonschema", "jsonschema"), + ) + + +def test_keep_summarizing_wrapper_declares_shared_infrastructure_dependencies() -> None: + text = (REPO_ROOT / "scripts/ks.py").read_text(encoding="utf-8") + assert '"pyyaml==6.0.3"' in text + assert '"jsonschema==4.25.1"' in text + wrapper = load_wrapper("ks") + assert ("yaml", "pyyaml") in wrapper.RUNTIME_DEPENDENCIES + assert ("jsonschema", "jsonschema") in wrapper.RUNTIME_DEPENDENCIES + + +@pytest.mark.parametrize("name", ["orch", "wb"]) +def test_missing_uv_is_typed_and_actionable(name: str, monkeypatch: pytest.MonkeyPatch) -> None: + wrapper = load_wrapper(name) + monkeypatch.setattr(wrapper, "_missing_runtime_dependencies", lambda: ["yaml", "jsonschema"]) + monkeypatch.setattr(wrapper.shutil, "which", lambda _name: None) + ready, failure = wrapper._ensure_managed_runtime(argv=[f"{name}.py", "--help"], environ={}) + assert ready is False + assert failure is not None + assert failure.startswith("WB_RUNTIME_DEPENDENCY_UNAVAILABLE:") + assert "install uv" in failure + + +@pytest.mark.parametrize("name", ["orch", "wb"]) +def test_recursion_guard_returns_typed_failure(name: str, monkeypatch: pytest.MonkeyPatch) -> None: + wrapper = load_wrapper(name) + monkeypatch.setattr(wrapper, "_missing_runtime_dependencies", lambda: ["yaml"]) + ready, failure = wrapper._ensure_managed_runtime( + argv=[f"{name}.py", "--help"], + environ={wrapper.UV_REEXEC_ENV: "1"}, + ) + assert ready is False + assert failure is not None and "uv could not hydrate" in failure + + +@pytest.mark.parametrize("name", ["orch", "wb"]) +def test_uv_reexec_uses_current_wrapper_and_preserves_arguments(name: str, monkeypatch: pytest.MonkeyPatch) -> None: + wrapper = load_wrapper(name) + monkeypatch.setattr(wrapper, "_missing_runtime_dependencies", lambda: ["yaml"]) + monkeypatch.setattr(wrapper.shutil, "which", lambda _name: "/opt/test/uv") + observed: dict[str, object] = {} + + def fake_execve(executable: str, argv: list[str], environment: dict[str, str]) -> None: + observed.update(executable=executable, argv=argv, environment=environment) + raise RuntimeError("stop") + + monkeypatch.setattr(wrapper.os, "execve", fake_execve) + with pytest.raises(RuntimeError, match="stop"): + wrapper._ensure_managed_runtime( + argv=[f"{name}.py", "doctor", "--project-root", "/tmp/example"], environ={"KEEP": "yes"} + ) + assert observed["executable"] == "/opt/test/uv" + assert observed["argv"] == [ + "/opt/test/uv", + "run", + str((REPO_ROOT / f"scripts/{name}.py").resolve()), + "doctor", + "--project-root", + "/tmp/example", + ] + assert observed["environment"][wrapper.UV_REEXEC_ENV] == "1" + assert observed["environment"]["KEEP"] == "yes" + + +@pytest.mark.parametrize( + ("command", "failure_code"), + [ + ("provision-member", "WB_V3_MEMBER_COMMAND_RETIRED"), + ("cleanup-member", "WB_V3_MEMBER_COMMAND_RETIRED"), + ("migrate-to-multi-repository", "WB_TOPOLOGY_MIGRATION_COMMAND_RETIRED"), + ], +) +def test_v3_mutating_public_commands_are_typed_refusals( + tmp_path: Path, command: str, failure_code: str +) -> None: + environment = os.environ.copy() + environment["HOME"] = str(tmp_path) + completed = subprocess.run( + [sys.executable, str(REPO_ROOT / "scripts/wb.py"), command], + cwd=REPO_ROOT, + env=environment, + text=True, + capture_output=True, + check=False, + ) + assert completed.returncode == 1, completed.stdout + completed.stderr + assert json.loads(completed.stdout)["failure_code"] == failure_code + + +def test_removed_wor107_migration_stop_route_is_not_publicly_dispatchable( + tmp_path: Path, +) -> None: + environment = os.environ.copy() + environment["HOME"] = str(tmp_path) + completed = subprocess.run( + [sys.executable, str(REPO_ROOT / "scripts/wb.py"), "assert-migration-stop"], + cwd=REPO_ROOT, + env=environment, + text=True, + capture_output=True, + check=False, + ) + assert completed.returncode == 2, completed.stdout + completed.stderr + assert "unknown command: assert-migration-stop" in completed.stderr diff --git a/tests/test_registry_layout_migration.py b/tests/test_registry_layout_migration.py index acc9f18..35fe5ce 100644 --- a/tests/test_registry_layout_migration.py +++ b/tests/test_registry_layout_migration.py @@ -6,6 +6,8 @@ import sys from pathlib import Path +import pytest + REPO_ROOT = Path(__file__).resolve().parents[1] FIXTURES = REPO_ROOT / "tests/fixtures/registry-layout" @@ -27,6 +29,12 @@ migration_path, validate_layout_version, ) +from infrastructure import InfrastructureError # noqa: E402 +from project import ( # noqa: E402 + list_project_registry, + project_registry_issues, + remove_project_registry, +) def restore_work_bundle_import_boundary() -> None: @@ -47,7 +55,7 @@ def run_wb(config_root: Path, *args: str) -> subprocess.CompletedProcess[str]: env = os.environ.copy() env.update( { - "WB_CONFIG_ROOT": str(config_root), + "HOME": str(config_root.parent), "WB_WORK_BUNDLE_ROOT": str(REPO_ROOT), "GIT_AUTHOR_NAME": "Test", "GIT_AUTHOR_EMAIL": "test@example.com", @@ -80,7 +88,7 @@ def render_fixture(path: Path, **replacements: str) -> str: def bootstrap_config(tmp_path: Path) -> Path: - config = tmp_path / "config" + config = tmp_path / ".work-bundle" (config / "registry").mkdir(parents=True) (config / "bootstrap.yaml").write_text( "\n".join( @@ -187,14 +195,100 @@ def payload(result: subprocess.CompletedProcess[str]) -> dict[str, object]: return json.loads(result.stdout) +@pytest.mark.parametrize( + "operation", + [ + list_project_registry, + project_registry_issues, + lambda: remove_project_registry("apparent-project"), + ], + ids=["list-projects", "registry-doctor", "unregister-project"], +) +def test_current_registry_consumers_reject_malformed_yaml_without_mutation( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, operation +) -> None: + config = bootstrap_config(tmp_path) + registry = config / "registry/projects.yaml" + registry.write_text( + "\n".join( + [ + "registry_schema_version: 1", + "projects:", + " - slug: apparent-project", + " aliases: []", + "device_bindings: {}", + "malformed: [unterminated", + "", + ] + ), + encoding="utf-8", + ) + before = registry.read_bytes() + monkeypatch.setenv("HOME", str(config.parent)) + monkeypatch.setenv("WB_WORK_BUNDLE_ROOT", str(REPO_ROOT)) + + with pytest.raises(InfrastructureError) as caught: + operation() + + assert caught.value.code == "WB_INFRASTRUCTURE_YAML_INVALID" + assert registry.read_bytes() == before + + +def test_registered_project_migration_rejects_malformed_registry_yaml_before_action( + tmp_path: Path, +) -> None: + config = bootstrap_config(tmp_path) + registry = config / "registry/projects.yaml" + registry.write_text( + "registry_schema_version: 1\nprojects: [\ndevice_bindings: {}\n", + encoding="utf-8", + ) + before = registry.read_bytes() + + result = run_wb(config, "migrate-registered-projects", "--dry-run") + + assert result.returncode == 1 + assert payload(result)["failure_code"] == "WB_INFRASTRUCTURE_YAML_INVALID" + assert registry.read_bytes() == before + + +@pytest.mark.parametrize( + "operation", + [ + list_project_registry, + project_registry_issues, + lambda: remove_project_registry("apparent-project"), + ], + ids=["list-projects", "registry-doctor", "unregister-project"], +) +def test_current_registry_consumers_reject_unsupported_schema_without_mutation( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, operation +) -> None: + config = bootstrap_config(tmp_path) + registry = config / "registry/projects.yaml" + registry.write_text( + "registry_schema_version: 99\nprojects: []\ndevice_bindings: {}\n", + encoding="utf-8", + ) + before = registry.read_bytes() + monkeypatch.setenv("HOME", str(config.parent)) + monkeypatch.setenv("WB_WORK_BUNDLE_ROOT", str(REPO_ROOT)) + + with pytest.raises(InfrastructureError) as caught: + operation() + + assert caught.value.code == "WB_INFRASTRUCTURE_SCHEMA_INVALID" + assert registry.read_bytes() == before + + def test_catalog_registers_explicit_version_to_version_steps() -> None: catalog = load_migration_catalog(REPO_ROOT) assert catalog.layout_current == "4" assert catalog.registry_schema_current == "1" - assert [step.step_id for step in catalog.steps] == ["layout-v2-to-v3", "layout-v3-to-v4"] + assert [step.step_id for step in catalog.steps] == ["layout-v2-to-v4", "layout-v3-to-v4"] path = migration_path("2", catalog) assert path is not None - assert [(step.from_version, step.to_version) for step in path] == [("2", "3"), ("3", "4")] + assert [(step.from_version, step.to_version) for step in path] == [("2", "4")] assert migration_path("4", catalog) == [] assert migration_path("9", catalog) is None @@ -279,7 +373,6 @@ def test_populated_device_bindings_do_not_reassign_project_roots(tmp_path: Path) assert "custom_registry_field: keep-registry" in after assert "custom_entry_field: keep-entry-a" in after assert "custom_entry_field: keep-entry-b" in after - assert after.index("projects:") < after.index("device_bindings:") unrelated_start = after.index("wb-unrelated:") next_binding = after.find("\n wb-", unrelated_start + 1) unrelated_block = after[unrelated_start: next_binding if next_binding != -1 else None] @@ -370,8 +463,8 @@ def test_multi_version_sequential_upgrade_v2_to_v4(tmp_path: Path) -> None: data = payload(proposed) project = data["projects"][0] assert project["classification"] == "migratable" - assert [step["id"] for step in project["steps"]] == ["layout-v2-to-v3", "layout-v3-to-v4"] - assert [step["from_version"] for step in project["steps"]] == ["2", "3"] + assert [step["id"] for step in project["steps"]] == ["layout-v2-to-v4"] + assert [step["from_version"] for step in project["steps"]] == ["2"] applied = run_wb( config, "migrate-registered-projects", "--apply", "--accepted-plan-id", str(data["plan_id"]) @@ -480,7 +573,7 @@ def test_blocked_multi_repository_v2(tmp_path: Path) -> None: assert proposed.returncode == 0, proposed.stdout + proposed.stderr project = payload(proposed)["projects"][0] assert project["classification"] == "blocked" - assert project["failure_code"] == "WB_MIGRATION_MULTI_REPOSITORY_WORKFLOW_REQUIRED" + assert project["failure_code"] == "WB_CONTROL_PLANE_SINGLE_REPOSITORY_COUNT_INVALID" def test_validation_failure_after_transformation_restores_state(tmp_path: Path, monkeypatch) -> None: @@ -490,7 +583,7 @@ def test_validation_failure_after_transformation_restores_state(tmp_path: Path, registry = write_registry(config, [project_block("validate-fail", workspace, repo_id, str(remote))]) before_registry = registry.read_bytes() before_metadata = (workspace / ".work-bundle/project.yaml").read_bytes() - monkeypatch.setenv("WB_CONFIG_ROOT", str(config)) + monkeypatch.setenv("HOME", str(config.parent)) monkeypatch.setenv("WB_WORK_BUNDLE_ROOT", str(REPO_ROOT)) def failing_validate(root: Path, version: str) -> list[str]: @@ -523,11 +616,11 @@ def test_intermediate_step_failure_restores_pre_migration_state(tmp_path: Path, registry = write_registry(config, [project_block("mid-fail", workspace, repo_id, str(remote))]) before_registry = registry.read_bytes() before_metadata = (workspace / ".work-bundle/project.yaml").read_bytes() - monkeypatch.setenv("WB_CONFIG_ROOT", str(config)) + monkeypatch.setenv("HOME", str(config.parent)) monkeypatch.setenv("WB_WORK_BUNDLE_ROOT", str(REPO_ROOT)) def fail_v4(step, root, entry): - if step.step_id == "layout-v3-to-v4": + if step.step_id == "layout-v2-to-v4": return { "status": "failed", "failures": ["WB_REGISTRY_LAYOUT_INJECTED_STEP_FAILURE"], @@ -544,7 +637,7 @@ def fail_v4(step, root, entry): ) assert result["status"] == "issues-found" diagnostic = result["diagnostics"][0] - assert diagnostic["failed_step"] == "layout-v3-to-v4" + assert diagnostic["failed_step"] == "layout-v2-to-v4" assert diagnostic["from_version"] == "2" assert diagnostic["to_version"] == "4" assert diagnostic["failure_code"] == "WB_REGISTRY_LAYOUT_INJECTED_STEP_FAILURE" @@ -572,7 +665,7 @@ def test_failed_migration_preserves_symlink_and_nested_credentials( outside_link.symlink_to(outside) before_registry = registry.read_bytes() before_metadata = (workspace / ".work-bundle/project.yaml").read_bytes() - monkeypatch.setenv("WB_CONFIG_ROOT", str(config)) + monkeypatch.setenv("HOME", str(config.parent)) monkeypatch.setenv("WB_WORK_BUNDLE_ROOT", str(REPO_ROOT)) def failing_validate(root: Path, version: str) -> list[str]: @@ -639,7 +732,7 @@ def test_dry_run_output_and_migration_ordering_are_deterministic(tmp_path: Path) assert first["projects"] == second["projects"] assert [item["slug"] for item in first["projects"]] == ["alpha", "zebra"] assert [step["id"] for step in first["projects"][0]["steps"]] == ["layout-v3-to-v4"] - assert [step["id"] for step in first["projects"][1]["steps"]] == ["layout-v2-to-v3", "layout-v3-to-v4"] + assert [step["id"] for step in first["projects"][1]["steps"]] == ["layout-v2-to-v4"] def test_stale_plan_id_does_not_mutate(tmp_path: Path) -> None: diff --git a/tests/test_review_agent_decisions.py b/tests/test_review_agent_decisions.py deleted file mode 100644 index a4b97db..0000000 --- a/tests/test_review_agent_decisions.py +++ /dev/null @@ -1,154 +0,0 @@ -from __future__ import annotations - -import json -import sys -from pathlib import Path - -import pytest - - -ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(ROOT / "scripts/orchestration")) -sys.path.insert(0, str(ROOT / "scripts/work-bundle")) - -import review_runtime # noqa: E402 -import reviewer_workspace # noqa: E402 - - -ZERO_SHA = "0" * 64 -ZERO_TREE = "0" * 40 - - -def observation() -> dict[str, object]: - raw = { - "finding_id": "finding-observed", - "severity": "blocking", - "requirement_id": "REQ-ROUTE", - "boundary": "src/runtime.py:route", - "evidence": "The current route is fixed by the adapter.", - "expected": "The controller selects a bounded action.", - "observed": "Severity selected repair_task.", - "owner": "reviewer-observed-owner", - } - return { - "schema": "review-finding-v2", - "finding_id": raw["finding_id"], - "stage": "implementation", - "reviewer_observation": raw, - "evidence": [{ - "kind": "source", - "locator": raw["boundary"], - "digest_or_identity": ZERO_SHA, - "observation": raw["evidence"], - }], - "target_identity": { - "artifact_id": "task-route", "revision": "1", "sha256": ZERO_SHA, - "source_tree": ZERO_TREE, - }, - "summary": "REQ-ROUTE: Severity selected repair_task.", - "controller_decision": None, - } - - -def decision(**overrides: object) -> dict[str, object]: - value = { - "classification": "implementation_defect", - "first_broken_artifact": "plan", - "affected_owner": "plan_owner", - "action": "repair_plan", - "obligation_basis": "accepted_requirement", - "evidence_basis": "The accepted plan owns the missing routing allocation.", - } - value.update(overrides) - return value - - -def test_v2_controller_decision_routes_without_class_to_remedy_mapping() -> None: - raw = observation() - validated = review_runtime.validate_review_finding(raw) - assert validated.controller_decision is None - assert validated.reviewer_observation == raw["reviewer_observation"] - - classified = review_runtime.classify_review_observation(raw, decision()) - routed = review_runtime._route_review_finding(classified) - assert routed["first_broken_artifact"] == "plan" - assert routed["return_to"] == "plan_owner" - assert routed["action"] == "repair_plan" - - -def test_v2_routing_requires_agent_decision_and_rejects_unsafe_representation() -> None: - with pytest.raises(review_runtime.ReviewContractError, match="controller decision"): - review_runtime._route_review_finding(observation()) - with pytest.raises(review_runtime.ReviewContractError, match="affected_owner"): - review_runtime.classify_review_observation( - observation(), decision(affected_owner="arbitrary_owner") - ) - terminal = review_runtime.classify_review_observation( - observation(), decision(action="accepted") - ) - with pytest.raises(review_runtime.ReviewContractError, match="terminal"): - review_runtime._route_review_finding(terminal) - reslice = review_runtime.classify_review_observation( - observation(), decision(action="reslice_plan") - ) - with pytest.raises(review_runtime.ReviewContractError, match="affected region"): - review_runtime._route_review_finding(reslice) - - -def test_v1_fixed_mapping_remains_legacy_only() -> None: - legacy = { - "finding_id": "legacy-finding", "stage": "implementation", - "class": "implementation_defect", "severity": "blocking", - "first_broken_artifact": "implementation", "obligation_basis": "accepted_requirement", - "evidence": [{"kind": "source", "locator": "src/runtime.py", "digest_or_identity": ZERO_SHA, - "observation": "Legacy observation."}], - "target_identity": {"artifact_id": "task-route", "revision": "1", "sha256": ZERO_SHA, - "source_tree": ZERO_TREE}, - "summary": "Legacy finding.", "recommended_owner": "task_owner", - "disposition": "repair_task", - } - assert review_runtime._route_review_finding(legacy)["action"] == "repair_task" - legacy["disposition"] = "repair_plan" - with pytest.raises(review_runtime.ReviewContractError, match="disposition"): - review_runtime.validate_review_finding(legacy) - - -def test_task_adapter_preserves_reviewer_observation_without_routing_decision() -> None: - identity = { - "artifact_id": "task-route", "revision": "1", "sha256": ZERO_SHA, - "source_tree": ZERO_TREE, - } - context = { - "target_identity": identity, "agent_id": "reviewer", "capability": "judgment", - "execution_id": "reviewer-run", "evidence_mode": "reproducible_snapshot", - "review_mode": "initial", "review_target_kind": "task", "repair_frontier": None, - "review_reset": None, - } - raw = observation()["reviewer_observation"] - judgment = {"task_review": { - "reviewed_head": identity["revision"], "verdict": "repair", "findings": [raw] - }} - result = reviewer_workspace._task_product_judgment_review( - judgment, review_id="review-route", context=context, packet={"artifacts": []}, - started_at="2026-09-13T00:00:00Z", completed_at="2026-09-13T00:01:00Z", - ) - finding = result["findings"][0] - assert finding["reviewer_observation"] == raw - assert finding["controller_decision"] is None - assert "class" not in finding and "disposition" not in finding - - -def test_review_finding_v2_schema_matches_runtime_shape() -> None: - schema = json.loads( - (ROOT / "references/assets/orchestration/contract/review-finding-v2.schema.json").read_text() - ) - assert schema["$id"] == "urn:work-bundle:orchestration:review-finding:v2" - assert set(schema["$defs"]["reviewFindingV2"]["required"]) == set( - review_runtime.FINDING_V2_KEYS - ) - assert review_runtime.validate_contract_instance( - "reviewFindingV2", observation() - ).finding_id == "finding-observed" - assert review_runtime.validate_contract_instance( - "API-001", observation() - ).finding_id == "finding-observed" diff --git a/tests/test_review_current_authority.py b/tests/test_review_current_authority.py deleted file mode 100644 index 35938e1..0000000 --- a/tests/test_review_current_authority.py +++ /dev/null @@ -1,176 +0,0 @@ -from __future__ import annotations - -import json -import sys -from copy import deepcopy -from pathlib import Path - -import pytest - - -REPO_ROOT = Path(__file__).resolve().parents[1] -ORCHESTRATION = REPO_ROOT / "scripts" / "orchestration" -sys.path.insert(0, str(ORCHESTRATION)) - -import review_runtime # noqa: E402 -from test_orchestration_reviews import stage_review # noqa: E402 - - -def _reset_pair() -> tuple[dict[str, object], dict[str, object]]: - previous = stage_review("plan") - previous.update( - review_mode="initial", - review_target_kind="stage", - repair_frontier=None, - review_reset=None, - ) - current = deepcopy(previous) - current.update( - review_id="review-plan-current", - review_reset={ - "prior_review_id": previous["review_id"], - "reason_class": "scope", - "reason": "Accepted plan scope changed.", - }, - previous_review=previous, - ) - return previous, current - - -def test_v2_publication_persists_direct_immutable_current_binding( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - previous, current = _reset_pair() - monkeypatch.setattr(review_runtime, "_validate_reviewer_run", lambda *_args: None) - review_runtime.publish_review( - tmp_path, previous, current_target_identity=previous["target_identity"] - ) - reference = review_runtime.publish_review( - tmp_path, current, current_target_identity=current["target_identity"] - ) - - authority_path = review_runtime.current_review_authority_path( - tmp_path, str(current["review_id"]) - ) - authority = json.loads(authority_path.read_text(encoding="utf-8")) - assert authority["schema"] == "stage-review-v2" - assert authority["current_authority"]["record_sha256"] == reference["sha256"] - assert not authority_path.stat().st_mode & 0o222 - - -def test_current_v2_authority_load_does_not_replay_predecessor_or_receipt( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - previous, current = _reset_pair() - monkeypatch.setattr(review_runtime, "_validate_reviewer_run", lambda *_args: None) - previous_reference = review_runtime.publish_review( - tmp_path, previous, current_target_identity=previous["target_identity"] - ) - reference = review_runtime.publish_review( - tmp_path, current, current_target_identity=current["target_identity"] - ) - review_runtime._review_store_path(tmp_path, previous_reference["review_id"]).unlink() - review_runtime.current_review_authority_path( - tmp_path, previous_reference["review_id"] - ).unlink() - monkeypatch.setattr( - review_runtime, - "_stored_stage_history", - lambda *_args: pytest.fail("historical predecessor traversal"), - ) - monkeypatch.setattr( - review_runtime, - "_validate_reviewer_run", - lambda *_args: pytest.fail("receipt completeness replay"), - ) - - loaded, validated = review_runtime.load_stored_review( - tmp_path, reference, current_target_identity=current["target_identity"] - ) - - assert loaded == current - assert validated.review_id == current["review_id"] - - -def test_current_review_gate_consumes_direct_authority_without_history( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - previous, current = _reset_pair() - monkeypatch.setattr(review_runtime, "_validate_reviewer_run", lambda *_args: None) - previous_reference = review_runtime.publish_review( - tmp_path, previous, current_target_identity=previous["target_identity"] - ) - review_runtime.publish_review( - tmp_path, current, current_target_identity=current["target_identity"] - ) - review_runtime._review_store_path(tmp_path, previous_reference["review_id"]).unlink() - review_runtime.current_review_authority_path( - tmp_path, previous_reference["review_id"] - ).unlink() - monkeypatch.setattr( - review_runtime, - "_stored_stage_history", - lambda *_args: pytest.fail("current gate traversed history"), - ) - monkeypatch.setattr( - review_runtime, - "_validate_reviewer_run", - lambda *_args: pytest.fail("current gate replayed receipt"), - ) - - review_runtime._require_current_review( - tmp_path, "plan", current["target_identity"] - ) - - -def test_corrupt_direct_binding_fails_closed(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - current = stage_review("plan") - monkeypatch.setattr(review_runtime, "_validate_reviewer_run", lambda *_args: None) - reference = review_runtime.publish_review( - tmp_path, current, current_target_identity=current["target_identity"] - ) - authority_path = review_runtime.current_review_authority_path( - tmp_path, reference["review_id"] - ) - authority_path.chmod(0o600) - authority = json.loads(authority_path.read_text(encoding="utf-8")) - authority["authority_sha256"] = "0" * 64 - authority_path.write_text(json.dumps(authority), encoding="utf-8") - authority_path.chmod(0o444) - - with pytest.raises(review_runtime.ReviewContractError, match="authority digest"): - review_runtime.load_stored_review( - tmp_path, reference, current_target_identity=current["target_identity"] - ) - - -def test_legacy_adapter_validates_only_the_current_record( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - previous, current = _reset_pair() - current["reviewer_run"] = {"run_id": "legacy-run", "sha256": "a" * 64} - path = review_runtime._review_store_path(tmp_path, str(current["review_id"])) - path.parent.mkdir(parents=True) - raw = (json.dumps(current, indent=2, sort_keys=True) + "\n").encode() - path.write_bytes(raw) - path.chmod(0o444) - reference = { - "review_id": current["review_id"], - "sha256": __import__("hashlib").sha256(raw).hexdigest(), - } - monkeypatch.setattr( - review_runtime, - "_stored_stage_history", - lambda *_args: pytest.fail("legacy predecessor traversal"), - ) - monkeypatch.setattr( - review_runtime, - "_validate_reviewer_run", - lambda *_args: pytest.fail("legacy receipt replay"), - ) - - _loaded, validated = review_runtime.load_stored_review( - tmp_path, reference, current_target_identity=current["target_identity"] - ) - - assert validated.review_id == current["review_id"] diff --git a/tests/test_review_runtime_responsibility_integration.py b/tests/test_review_runtime_responsibility_integration.py deleted file mode 100644 index e7672a5..0000000 --- a/tests/test_review_runtime_responsibility_integration.py +++ /dev/null @@ -1,85 +0,0 @@ -from __future__ import annotations - -import json -import subprocess -import sys -from pathlib import Path - - -REPO_ROOT = Path(__file__).resolve().parents[1] -ORCHESTRATION = REPO_ROOT / "scripts" / "orchestration" - - -def test_wor107_policy_is_owned_only_by_narrow_legacy_utility(tmp_path: Path) -> None: - generic = (ORCHESTRATION / "review_runtime.py").read_text(encoding="utf-8") - assert "WOR-107" not in generic - - instance = tmp_path / "handoff.json" - instance.write_text( - json.dumps( - { - "issue": "WOR-107", - "excluded_work": ["WOR-66", "WOR-107"], - } - ), - encoding="utf-8", - ) - arguments = [ - "--instance", - str(instance), - "--required-excluded", - "WOR-66", - "WOR-107", - ] - direct = subprocess.run( - [sys.executable, str(ORCHESTRATION / "legacy_wor107_migration.py"), *arguments], - text=True, - capture_output=True, - check=False, - ) - alias = subprocess.run( - [ - sys.executable, - str(ORCHESTRATION / "review_runtime.py"), - "assert-migration-stop", - *arguments, - ], - text=True, - capture_output=True, - check=False, - ) - - assert direct.returncode == alias.returncode == 0 - assert json.loads(direct.stdout) == json.loads(alias.stdout) - assert "deprecated" in alias.stderr.lower() - assert direct.stderr == "" - - -def test_public_dispatcher_alias_preserves_legacy_result_and_warns(tmp_path: Path) -> None: - instance = tmp_path / "handoff.json" - instance.write_text( - json.dumps({"issue": "WOR-107", "excluded_work": ["WOR-107"]}), - encoding="utf-8", - ) - completed = subprocess.run( - [ - sys.executable, - str(REPO_ROOT / "scripts" / "wb.py"), - "assert-migration-stop", - "--instance", - str(instance), - "--required-excluded", - "WOR-107", - ], - text=True, - capture_output=True, - check=False, - ) - - assert completed.returncode == 0 - assert json.loads(completed.stdout) == { - "excluded_work": ["WOR-107"], - "issue": "WOR-107", - "status": "passed", - } - assert "deprecated" in completed.stderr.lower() diff --git a/tests/test_review_structural_identity_v2.py b/tests/test_review_structural_identity_v2.py deleted file mode 100644 index c57fe9d..0000000 --- a/tests/test_review_structural_identity_v2.py +++ /dev/null @@ -1,127 +0,0 @@ -from __future__ import annotations - -import sys -from pathlib import Path - -import pytest - - -REPO_ROOT = Path(__file__).resolve().parents[1] -ORCHESTRATION = REPO_ROOT / "scripts" / "orchestration" -sys.path.insert(0, str(ORCHESTRATION)) - -import review_runtime # noqa: E402 -from test_orchestration_semantic_plan_identity import _plan_graph # noqa: E402 - - -def _replace(path: Path, before: str, after: str) -> None: - path.write_text(path.read_text(encoding="utf-8").replace(before, after), encoding="utf-8") - - -def test_v2_projection_is_explicitly_versioned(tmp_path: Path) -> None: - plan, _phase, _task = _plan_graph(tmp_path) - - projection = review_runtime.semantic_plan_projection(tmp_path, plan) - - assert projection["schema"] == "plan-structural-projection-v2" - assert projection == review_runtime.structural_plan_projection_v2(tmp_path, plan) - assert review_runtime.plan_review_identity(tmp_path, plan) != review_runtime.legacy_plan_review_identity( - tmp_path, plan - ) - - -def test_only_documented_lifecycle_locations_are_ignored(tmp_path: Path) -> None: - plan, phase, task = _plan_graph(tmp_path) - original = review_runtime.plan_review_identity(tmp_path, plan) - - _replace(plan, "status: Planned", "status: In progress") - _replace(plan, "last_updated: 2026-09-08", "last_updated: 2026-09-09") - _replace(phase, "status: Planned", "status: Completed") - _replace(phase, "last_updated: 2026-09-08", "last_updated: 2026-09-09") - _replace(phase, "status: Planned}", "status: Completed}") - _replace( - task, - "status: Planned", - "status: Completed", - ) - _replace(task, "last_updated: 2026-09-08", "last_updated: 2026-09-09") - _replace( - task, - "acceptance_review: {required: true, verdict: pending, reviewed_head: '', findings: []}", - "acceptance_review: {required: true, verdict: accepted, reviewed_head: abc, findings: [done]}", - ) - _replace(task, "---\n\n# Task", "accepted_result: result-task-001\n---\n\n# Task") - - assert review_runtime.plan_review_identity(tmp_path, plan) == original - - -@pytest.mark.parametrize( - ("field", "value", "changed"), - [ - ("status", "draft", "accepted"), - ("findings", "[one]", "[two]"), - ("accepted_result", "result-one", "result-two"), - ("future_contract", "one", "two"), - ], -) -def test_nested_substantive_and_unknown_fields_affect_v2_identity( - tmp_path: Path, field: str, value: str, changed: str -) -> None: - plan, _phase, task = _plan_graph(tmp_path) - _replace( - task, - "---\n\n# Task", - f"extension:\n {field}: {value}\n---\n\n# Task", - ) - original = review_runtime.plan_review_identity(tmp_path, plan) - - _replace(task, f" {field}: {value}", f" {field}: {changed}") - - assert review_runtime.plan_review_identity(tmp_path, plan) != original - - -def test_phase_index_status_is_lifecycle_but_dependencies_are_substantive(tmp_path: Path) -> None: - plan, _phase, _task = _plan_graph(tmp_path) - _replace( - plan, - "source_spec:", - "phase_index: [{id: phase-001, status: Planned, depends_on: []}]\nsource_spec:", - ) - original = review_runtime.plan_review_identity(tmp_path, plan) - - _replace(plan, "status: Planned, depends_on", "status: Completed, depends_on") - assert review_runtime.plan_review_identity(tmp_path, plan) == original - - _replace(plan, "depends_on: []", "depends_on: [phase-000]") - assert review_runtime.plan_review_identity(tmp_path, plan) != original - - -def test_requirement_text_cannot_be_hidden_by_heading_format(tmp_path: Path) -> None: - plan, _phase, _task = _plan_graph(tmp_path) - plan.write_text( - plan.read_text(encoding="utf-8") - + "\n## Knowledge Base Update Carry Forward\n\n- Requirement: preserve evidence\n", - encoding="utf-8", - ) - original = review_runtime.plan_review_identity(tmp_path, plan) - - _replace(plan, "Requirement: preserve evidence", "Requirement: discard evidence") - - assert review_runtime.plan_review_identity(tmp_path, plan) != original - - -def test_legacy_algorithm_remains_directly_callable(tmp_path: Path) -> None: - plan, _phase, task = _plan_graph(tmp_path) - task.write_text( - task.read_text(encoding="utf-8").replace( - "---\n\n# Task", "extension: {status: draft}\n---\n\n# Task" - ), - encoding="utf-8", - ) - legacy = review_runtime.legacy_plan_review_identity(tmp_path, plan) - current = review_runtime.plan_review_identity(tmp_path, plan) - - _replace(task, "extension: {status: draft}", "extension: {status: accepted}") - - assert review_runtime.legacy_plan_review_identity(tmp_path, plan) == legacy - assert review_runtime.plan_review_identity(tmp_path, plan) != current diff --git a/tests/test_reviewer_workspace.py b/tests/test_reviewer_workspace.py deleted file mode 100644 index 29c9a14..0000000 --- a/tests/test_reviewer_workspace.py +++ /dev/null @@ -1,1428 +0,0 @@ -from __future__ import annotations - -import hashlib -import json -import platform -from pathlib import Path -import subprocess -import sys -from unittest.mock import patch - -import pytest - - -REPO_ROOT = Path(__file__).resolve().parents[1] -DARWIN_SANDBOX_SHELL = "/bin/sh" -WORK_BUNDLE_SCRIPTS = REPO_ROOT / "scripts" / "work-bundle" -if str(WORK_BUNDLE_SCRIPTS) not in sys.path: - sys.path.insert(0, str(WORK_BUNDLE_SCRIPTS)) - -from reviewer_workspace import ( # noqa: E402 - ReviewerWorkspaceError, - build_direct_evidence_packet, - cleanup_reviewer_workspace, - create_reviewer_workspace, - enforce_reviewer_write_scope, - execute_reviewer_request, -) -import reviewer_workspace # noqa: E402 - -ORCHESTRATION_SCRIPTS = REPO_ROOT / "scripts" / "orchestration" -if str(ORCHESTRATION_SCRIPTS) not in sys.path: - sys.path.insert(0, str(ORCHESTRATION_SCRIPTS)) -import bounded_closure # noqa: E402 - - -def native_events(result, *, thread_id="01a0821d-f359-7d60-a9bd-90dd0e006166"): - return "\n".join(json.dumps(event) for event in [ - {"type": "thread.started", "thread_id": thread_id}, - {"type": "turn.started"}, - {"type": "item.completed", "item": {"id": "item-1", "type": "agent_message", "text": json.dumps(result)}}, - {"type": "turn.completed", "usage": {"input_tokens": 10, "output_tokens": 10}}, - ]) - - -def test_native_transcript_requires_one_actual_fresh_completed_judgment(): - run, result = reviewer_workspace.parse_native_reviewer_transcript(native_events({"verdict": "repair"})) - assert run == "01a0821d-f359-7d60-a9bd-90dd0e006166" - assert result == {"verdict": "repair"} - for forged in [json.dumps({"verdict": "accept"}), native_events({}) + "\n" + native_events({}), - native_events({}).rsplit("\n", 1)[0]]: - with pytest.raises(ReviewerWorkspaceError, match="NATIVE_TRANSCRIPT"): - reviewer_workspace.parse_native_reviewer_transcript(forged) - - -def test_native_transcript_retains_ancillary_stderr_but_rejects_structured_forbidden_activity(): - ancillary = ( - "2026-09-12T18:40:42.858443Z ERROR codex_core::tools::router: " - "error=code-mode host is disabled\n" - "2026-09-12T18:41:30.252413Z ERROR codex_core::models_manager: " - "failed to refresh models: timed out\n" - "unverified host log claim: command_execution may have failed\n" - ) - assert reviewer_workspace.parse_native_reviewer_transcript( - native_events({"verdict": "accepted"}), ancillary - )[1] == {"verdict": "accepted"} - - attempted = json.dumps({ - "type": "item.completed", - "item": {"id": "tool", "type": "command_execution"}, - }) - with pytest.raises(ReviewerWorkspaceError, match="NATIVE_TRANSCRIPT"): - reviewer_workspace.parse_native_reviewer_transcript( - native_events({"verdict": "accepted"}), attempted - ) - - -def test_native_transcript_requires_one_terminal_substantive_judgment(): - thread_id = "01a0821d-f359-7d60-a9bd-90dd0e006166" - commentary = [ - {"type": "thread.started", "thread_id": thread_id}, - {"type": "turn.started"}, - {"type": "item.completed", "item": { - "id": "commentary", "type": "agent_message", "text": "Inspecting frozen evidence." - }}, - {"type": "item.completed", "item": { - "id": "judgment", "type": "agent_message", "text": json.dumps({"verdict": "accepted"}) - }}, - {"type": "turn.completed", "usage": {}}, - ] - assert reviewer_workspace.parse_native_reviewer_transcript( - "\n".join(json.dumps(event) for event in commentary) - )[1] == {"verdict": "accepted"} - - for extra in [ - {"id": "conflict", "type": "agent_message", "text": json.dumps({"verdict": "repair"})}, - {"id": "late-commentary", "type": "agent_message", "text": "One more thought."}, - ]: - events = commentary[:-1] - events.append({"type": "item.completed", "item": extra}) - events.append(commentary[-1]) - with pytest.raises(ReviewerWorkspaceError, match="NATIVE_TRANSCRIPT"): - reviewer_workspace.parse_native_reviewer_transcript( - "\n".join(json.dumps(event) for event in events) - ) - - -def test_native_observed_catalog_notice_is_initialization_only(): - notice = {"type": "item.completed", "item": {"id": "warning", "type": "error", "message": - "Skill descriptions were shortened to fit the skills context budget. Codex can still see every skill, but some descriptions are shorter. Disable unused skills or plugins to leave more room for the rest."}} - events = native_events({"verdict": "repair"}).splitlines() - events.insert(2, json.dumps(notice)) - assert reviewer_workspace.parse_native_reviewer_transcript("\n".join(events))[1] == {"verdict": "repair"} - events.pop(2) - events.insert(3, json.dumps(notice)) - with pytest.raises(ReviewerWorkspaceError, match="NATIVE_TRANSCRIPT"): - reviewer_workspace.parse_native_reviewer_transcript("\n".join(events)) - events.pop(3) - notice["item"]["message"] += " A tool also failed." - events.insert(2, json.dumps(notice)) - with pytest.raises(ReviewerWorkspaceError, match="NATIVE_TRANSCRIPT"): - reviewer_workspace.parse_native_reviewer_transcript("\n".join(events)) - - -@pytest.mark.parametrize("kind", ["command_execution", "mcp_tool_call", "collab_tool_call", "error", "file_change"]) -def test_native_transcript_rejects_all_observed_tool_or_failure_activity(kind): - events = native_events({}).splitlines() - events.insert(2, json.dumps({"type": "item.completed", "item": {"id": "tool", "type": kind}})) - with pytest.raises(ReviewerWorkspaceError, match="NATIVE_TRANSCRIPT"): - reviewer_workspace.parse_native_reviewer_transcript("\n".join(events)) - - -def test_native_process_does_not_inherit_author_transport_or_config(tmp_path, monkeypatch): - monkeypatch.setenv("CODEX_APP_TOOLS_PIPE_PATH", "caller-transport") - monkeypatch.setenv("CODEX_THREAD_ID", "author-thread") - monkeypatch.setenv("CODEX_SESSION_ID", "author-session") - monkeypatch.setenv("BASH_ENV", "/host/instructions") - with patch.object(reviewer_workspace.subprocess, "run", return_value=subprocess.CompletedProcess([], 0, "", "")) as launch: - reviewer_workspace._run_native_process(tmp_path, ["/bin/codex"], "bounded packet") - kwargs = launch.call_args.kwargs - assert set(kwargs["env"]) <= {"PATH", "HOME", "TMPDIR", "CODEX_HOME"} - assert kwargs["input"] == "bounded packet" - assert kwargs["timeout"] > 0 - - -def sha256(path: Path) -> str: - return hashlib.sha256(path.read_bytes()).hexdigest() - - -@pytest.fixture -def review_roots(tmp_path: Path) -> tuple[Path, Path, Path]: - source = tmp_path / "source" - control = tmp_path / "control" - runtime = tmp_path / "runtime" - (source / "src").mkdir(parents=True) - (source / "src" / "target.py").write_text("def target():\n return 1\n", encoding="utf-8") - (source / ".wor105-review-sentinel").write_text("immutable-source-sentinel-v1\n", encoding="utf-8") - (control / "orchestration" / "reviews").mkdir(parents=True) - (control / "orchestration" / "reviews" / "target.json").write_text('{"valid": true}\n', encoding="utf-8") - (control / "orchestration" / "docs" / "wor105").mkdir(parents=True) - (control / "orchestration" / "docs" / "wor105" / ".review-sentinel").write_text( - "immutable-control-sentinel-v1\n", encoding="utf-8" - ) - (control / "credentials").mkdir() - (control / "credentials" / "credentials.yaml").write_text("secret-value\n", encoding="utf-8") - return source, control, runtime - - -def packet(source: Path, control: Path) -> dict[str, object]: - return build_direct_evidence_packet( - source_root=source, - control_root=control, - protected_roots=[control / "credentials"], - artifacts=["source:src/target.py", "control:orchestration/reviews/target.json"], - search_roots=["source:src"], - validators=[ - {"validator_id": "target-json", "kind": "json", "artifact": "control:orchestration/reviews/target.json"}, - {"validator_id": "source-digest", "kind": "sha256", "artifact": "source:src/target.py"}, - ], - sentinels=["source:.wor105-review-sentinel", "control:orchestration/docs/wor105/.review-sentinel"], - network_state="denied", - ) - - -def test_reviewer_dispatch_rechecks_live_round_before_launch( - review_roots: tuple[Path, Path, Path], -) -> None: - source, control, runtime = review_roots - metadata = control / ".work-bundle/project.yaml" - metadata.parent.mkdir(parents=True) - metadata.write_text( - "metadata_version: 4\n" - "workspace: {id: workspace-review, slug: review, mode: single-repository}\n" - "orchestration_control:\n" - " schema_version: 1\n" - " post_execution_review_round_limit: 5\n", - encoding="utf-8", - ) - target = { - "artifact_id": "plan-live-round", - "revision": "a" * 40, - "sha256": "b" * 64, - "source_tree": "c" * 40, - } - reserved = bounded_closure.begin_review_round( - control, - flow_id="plan-live-round", - request_id="request-live-round", - review_id="review-live-round", - target_identity=target, - executor_attempts=[{"execution_id": "executor-1", "state": "completed"}], - known_missing_evidence=[], - ) - bounded_closure.mark_review_round_prepared( - control, - flow_id="plan-live-round", - round_id=str(reserved["round_id"]), - ) - created = create_reviewer_workspace(runtime, "review-live-round", packet(source, control)) - state_path = Path(str(created["state_path"])) - state = json.loads(state_path.read_text(encoding="utf-8")) - state["admission_workspace"] = str(control) - state["post_execution_review"] = reserved - state_path.write_text(json.dumps(state, indent=2, sort_keys=True) + "\n", encoding="utf-8") - bounded_closure.complete_review_round( - control, - flow_id="plan-live-round", - round_id=str(reserved["round_id"]), - outcome="blocked", - audit_block={"code": "repair-required"}, - ) - - with patch.object(reviewer_workspace, "_run_sandboxed_process") as launch: - with pytest.raises( - ReviewerWorkspaceError, - match="WB_POST_EXECUTION_JUDGMENT_ALREADY_RECORDED", - ): - reviewer_workspace.run_sandboxed_reviewer( - Path(str(created["workspace_path"])), ["reviewer"] - ) - launch.assert_not_called() - - -def test_packet_rejects_protected_and_outside_reads(review_roots: tuple[Path, Path, Path]) -> None: - source, control, _ = review_roots - - with pytest.raises(ReviewerWorkspaceError, match="WB_REVIEW_PROTECTED_READ_DENIED"): - build_direct_evidence_packet( - source_root=source, - control_root=control, - protected_roots=[control / "credentials"], - artifacts=["control:credentials/credentials.yaml"], - search_roots=[], - validators=[], - sentinels=[], - network_state="denied", - ) - with pytest.raises(ReviewerWorkspaceError, match="WB_REVIEW_PATH_ESCAPE_DENIED"): - build_direct_evidence_packet( - source_root=source, - control_root=control, - protected_roots=[control / "credentials"], - artifacts=["source:../host-config"], - search_roots=[], - validators=[], - sentinels=[], - network_state="denied", - ) - - -def test_workspace_contains_copied_direct_evidence_and_declares_network_denied( - review_roots: tuple[Path, Path, Path] -) -> None: - source, control, runtime = review_roots - - result = create_reviewer_workspace(runtime, "review-001", packet(source, control)) - - workspace = Path(str(result["workspace_path"])) - state = json.loads(Path(str(result["state_path"])).read_text(encoding="utf-8")) - assert state["owner"] == "work-bundle" - assert state["review_id"] == "review-001" - assert state["network"] == {"state": "denied", "mechanism": "sandbox-exec-deny-network"} - assert state["sandbox"]["mechanism"] == "sandbox-exec" - assert state["source_evidence_digest"] - assert state["control_evidence_digest"] - assert (workspace / "evidence" / "source" / "src" / "target.py").is_file() - assert (workspace / "evidence" / "control" / "orchestration" / "reviews" / "target.json").is_file() - assert not (workspace / "evidence" / "control" / "credentials").exists() - assert "source_root" not in json.dumps(state) - assert "control_root" not in json.dumps(state) - - -@pytest.mark.parametrize("transport", ["sandbox", "native"]) -def test_task_review_worker_output_receives_native_bound_receipt( - review_roots: tuple[Path, Path, Path], transport: str -) -> None: - source, control, _runtime = review_roots - review_runtime = reviewer_workspace._review_runtime() - runtime = review_runtime.reviewer_runtime_root(control) - subprocess.run(["git", "init", "-q", str(source)], check=True) - subprocess.run(["git", "-C", str(source), "add", "src/target.py", ".wor105-review-sentinel"], check=True) - subprocess.run( - ["git", "-C", str(source), "-c", "user.name=Test", "-c", "user.email=test@example.invalid", "commit", "-qm", "fixture"], - check=True, - ) - head = subprocess.check_output(["git", "-C", str(source), "rev-parse", "HEAD"], text=True).strip() - tree = subprocess.check_output(["git", "-C", str(source), "rev-parse", "HEAD^{tree}"], text=True).strip() - identity = {"artifact_id": "task-006", "revision": head, "sha256": "1" * 64, "source_tree": tree} - context = { - "target_identity": identity, - "agent_id": "reviewer-task", - "capability": "judgment", - "execution_id": "review-execution-task", - "evidence_mode": "reproducible_snapshot", - "review_mode": "initial", - "review_target_kind": "task", - "repair_frontier": None, - "review_reset": None, - } - direct = build_direct_evidence_packet( - source_root=source, - control_root=control, - protected_roots=[control / "credentials"], - artifacts=["source:src/target.py"], - search_roots=[], validators=[], sentinels=[], network_state="denied", - task_review_context=context, - ) - created = create_reviewer_workspace(runtime, "review-task-native", direct) - judgment = {"task_review": { - "reviewed_head": head, - "verdict": "repair", - "findings": [{ - "finding_id": "finding-product-value", - "severity": "blocking", - "requirement_id": "REQ-VALUE", - "boundary": "src/target.py:target", - "evidence": "The returned value violates the requirement.", - "expected": "Return the accepted value.", - "observed": "A different value is returned.", - "owner": "task_owner", - }], - }} - if transport == "native": - with patch.object(reviewer_workspace, "_run_native_process", - return_value=subprocess.CompletedProcess([], 0, native_events(judgment), "")): - receipt = reviewer_workspace.run_native_reviewer(Path(str(created["workspace_path"])), Path(sys.executable), - model="test-model", review_instructions="Assess the accepted product requirements against the source.") - request = json.loads(Path(receipt["receipt_path"]).with_suffix(".request.json").read_text()) - assert set(request["review_input"]) == {"target_identity", "artifacts"} - assert "task_review_context" not in json.dumps(request) - else: - with patch.object(reviewer_workspace, "_run_sandboxed_process", - return_value=subprocess.CompletedProcess(["reviewer"], 0, json.dumps(judgment), "")): - receipt = reviewer_workspace.run_sandboxed_reviewer(Path(str(created["workspace_path"])), ["reviewer"]) - - assert receipt["status"] == "passed" - assert receipt["task_review_context"]["target_identity"] == identity - assert set(receipt["reviewer_run"]) == {"run_id", "sha256"} - assert receipt["review_result"]["reviewer"]["agent_id"] == ( - receipt["host_run_id"] if transport == "native" else "reviewer-task") - assert receipt["review_result"]["verdict"] == "repair" - finding = receipt["review_result"]["findings"][0] - assert finding["finding_id"] == "finding-product-value" - assert finding["evidence"][0]["locator"] == "src/target.py:target" - assert "The returned value violates the requirement." in finding["evidence"][0]["observation"] - assert finding["reviewer_observation"] == judgment["task_review"]["findings"][0] - assert finding["controller_decision"] is None - with pytest.raises(review_runtime.ReviewContractError, match="controller decision"): - review_runtime.publish_review( - control, receipt["review_result"] | {"reviewer_run": receipt["reviewer_run"]}, - current_target_identity=identity, - ) - classified = review_runtime.apply_review_finding_decisions( - receipt["review_result"], - {finding["finding_id"]: { - "classification": "implementation_defect", - "first_broken_artifact": "implementation", - "affected_owner": "task_owner", - "action": "repair_task", - "obligation_basis": "accepted_requirement", - "evidence_basis": "The accepted task requirement binds the observed return value.", - }}, - ) - reference = review_runtime.publish_review( - control, classified | {"reviewer_run": receipt["reviewer_run"]}, - current_target_identity=identity, - ) - stored, validated = review_runtime.load_stored_review( - control, reference, current_target_identity=identity - ) - assert stored["findings"] == classified["findings"] - assert validated.findings[0].finding_id == "finding-product-value" - assert review_runtime._route_review_finding(stored["findings"][0])["action"] == "repair_task" - - empty_repair = {"task_review": { - "reviewed_head": head, - "verdict": "repair", - "findings": [], - }} - with pytest.raises(ReviewerWorkspaceError, match="WB_REVIEW_TASK_OUTPUT_INVALID"): - reviewer_workspace._task_product_judgment_review( - empty_repair, - review_id="review-empty-repair", - context=context, - packet=direct, - started_at="2026-09-09T00:00:00Z", - completed_at="2026-09-09T00:01:00Z", - ) - - -def test_completed_native_capture_resumes_ordinary_receipt_idempotently( - review_roots: tuple[Path, Path, Path], -) -> None: - source, control, _ = review_roots - runtime = reviewer_workspace._review_runtime().reviewer_runtime_root(control) - subprocess.run(["git", "init", "-q", str(source)], check=True) - subprocess.run(["git", "-C", str(source), "add", "src/target.py", ".wor105-review-sentinel"], check=True) - subprocess.run( - ["git", "-C", str(source), "-c", "user.name=Test", "-c", "user.email=test@example.invalid", - "commit", "-qm", "fixture"], - check=True, - ) - head = subprocess.check_output(["git", "-C", str(source), "rev-parse", "HEAD"], text=True).strip() - tree = subprocess.check_output(["git", "-C", str(source), "rev-parse", "HEAD^{tree}"], text=True).strip() - identity = {"artifact_id": "task-resume", "revision": head, "sha256": "4" * 64, "source_tree": tree} - context = { - "target_identity": identity, "agent_id": "reserved", "capability": "judgment", - "execution_id": "reserved-run", "evidence_mode": "reproducible_snapshot", - "review_mode": "initial", "review_target_kind": "task", "repair_frontier": None, - "review_reset": None, - } - direct = build_direct_evidence_packet( - source_root=source, control_root=control, protected_roots=[control / "credentials"], - artifacts=["source:src/target.py"], search_roots=[], validators=[], sentinels=[], - network_state="denied", task_review_context=context, - ) - created = create_reviewer_workspace(runtime, "review-native-resume", direct) - judgment = {"task_review": {"reviewed_head": head, "verdict": "accept", "findings": []}} - completed = subprocess.CompletedProcess( - [], 0, native_events(judgment), - "2026-09-12T18:41:30Z ERROR codex_core::models_manager: failed to refresh models: timed out\n", - ) - parse_failure = ReviewerWorkspaceError("WB_REVIEW_NATIVE_TRANSCRIPT_INVALID") - with patch.object(reviewer_workspace, "_run_native_process", return_value=completed), patch.object( - reviewer_workspace, "parse_native_reviewer_transcript", side_effect=parse_failure - ): - with pytest.raises(ReviewerWorkspaceError, match="NATIVE_TRANSCRIPT") as failed: - reviewer_workspace.run_native_reviewer( - Path(str(created["workspace_path"])), Path(sys.executable), model="test-model", - review_instructions="Assess the frozen task evidence.", - ) - - diagnostic = Path(failed.value.result["diagnostic_path"]) - capture = json.loads((diagnostic / "capture.json").read_text()) - assert capture["schema"] == "reviewer-native-capture-v2" - assert capture["started_at"] <= capture["completed_at"] - assert {"packet.json", "events.jsonl", "request.json", "stdout.jsonl", "stderr.txt", "launch.json"} <= set(capture["artifacts"]) - assert all(not item.stat().st_mode & 0o222 for item in diagnostic.iterdir()) - - receipt = reviewer_workspace.complete_native_reviewer_capture(runtime, capture["run_id"]) - repeated = reviewer_workspace.complete_native_reviewer_capture(runtime, capture["run_id"]) - assert repeated["reviewer_run"] == receipt["reviewer_run"] - assert repeated["review_result"] == receipt["review_result"] - assert receipt["status"] == "passed" - assert receipt["host_run_id"] == "01a0821d-f359-7d60-a9bd-90dd0e006166" - assert Path(receipt["receipt_path"]).with_suffix(".events.jsonl").read_bytes() == ( - diagnostic / "events.jsonl" - ).read_bytes() - review_runtime = reviewer_workspace._review_runtime() - review = {**receipt["review_result"], "reviewer_run": receipt["reviewer_run"]} - reference = review_runtime.publish_review( - control, review, current_target_identity=identity - ) - stored, accepted = review_runtime.load_stored_review( - control, reference, current_target_identity=identity - ) - assert stored == review - assert accepted.verdict == "accepted" - - -def test_legacy_native_diagnostic_cannot_be_promoted_to_receipt(tmp_path: Path) -> None: - runtime = tmp_path / "runtime" - run_id = "reviewer-run-01a0821d-f359-7d60-a9bd-90dd0e006166" - diagnostic = runtime / "diagnostics" / "reviewer-native" / run_id - diagnostic.mkdir(parents=True) - (diagnostic / "capture.json").write_text(json.dumps({ - "schema": "reviewer-native-diagnostic-v1", "status": "unadmitted", - "run_id": run_id, "review_id": "review-legacy", "exit_code": 0, - "captured_at": "2026-09-12T18:41:30Z", "artifacts": {}, - })) - with pytest.raises(ReviewerWorkspaceError, match="NATIVE_CAPTURE_INCOMPLETE"): - reviewer_workspace.complete_native_reviewer_capture(runtime, run_id) - - -def test_compact_task_judgment_composes_repair_and_reset_predecessors() -> None: - runtime = reviewer_workspace - old_identity = {"artifact_id": "task-006", "revision": "a" * 40, "sha256": "1" * 64, "source_tree": "b" * 40} - base_context = { - "target_identity": old_identity, "agent_id": "reviewer-task", "capability": "judgment", - "execution_id": "review-execution-task", "evidence_mode": "direct_source", - "review_mode": "initial", "review_target_kind": "task", "repair_frontier": None, - "review_reset": None, - } - blocking = {"task_review": {"reviewed_head": old_identity["revision"], "verdict": "repair", "findings": [{ - "finding_id": "finding-product", "severity": "blocking", "requirement_id": "REQ-1", - "boundary": "src/product.py:run", "evidence": "return value differs", "expected": "one", - "observed": "zero", "owner": "task_owner", - }]}} - previous = runtime._task_product_judgment_review( - blocking, review_id="review-prior", context=base_context, packet={"artifacts": []}, - started_at="2026-09-08T00:00:00Z", completed_at="2026-09-08T00:01:00Z", - ) - new_identity = {"artifact_id": "task-006", "revision": "c" * 40, "sha256": "2" * 64, "source_tree": "d" * 40} - repair_context = {**base_context, "target_identity": new_identity, "review_mode": "repair", "repair_frontier": { - "prior_review_id": "review-prior", "blocking_finding_ids": ["finding-product"], - "previous_reviewed_identity": old_identity, "repaired_identity": new_identity, - "affected_boundaries": ["src/product.py:run"], - "frozen_evidence_reference": reviewer_workspace._review_runtime().review_evidence_identity(previous), - }} - repaired = runtime._task_product_judgment_review( - {"task_review": {"reviewed_head": new_identity["revision"], "verdict": "accept", "findings": []}}, - review_id="review-repaired", context=repair_context, packet={"artifacts": []}, - started_at="2026-09-08T00:02:00Z", completed_at="2026-09-08T00:03:00Z", - previous_review=previous, - ) - assert reviewer_workspace._review_runtime().validate_task_acceptance_review(repaired).verdict == "accepted" - - reset_context = { - **base_context, "target_identity": new_identity, - "review_reset": {"prior_review_id": "review-prior", "reason_class": "scope", "reason": "Accepted scope changed."}, - } - reset = runtime._task_product_judgment_review( - {"task_review": {"reviewed_head": new_identity["revision"], "verdict": "accept", "findings": []}}, - review_id="review-reset", context=reset_context, packet={"artifacts": []}, - started_at="2026-09-08T00:02:00Z", completed_at="2026-09-08T00:03:00Z", - previous_review=previous, - ) - assert reviewer_workspace._review_runtime().validate_task_acceptance_review(reset).verdict == "accepted" - - -def test_compact_integrated_product_judgment_gets_controller_owned_stage_envelope() -> None: - identity = {"artifact_id": "plan-006", "revision": "6", "sha256": "1" * 64, "source_tree": "b" * 40} - context = { - "stage": "integrated_implementation", "target_identity": identity, - "target_locator": "control:.work-bundle/orchestration/plan/active/plan.md", - "agent_id": "reviewer-integrated", "capability": "judgment", - "execution_id": "review-execution-integrated", "evidence_mode": "direct_source", - } - review = reviewer_workspace._task_product_judgment_review( - {"task_review": {"reviewed_head": identity["source_tree"], "verdict": "accept", "findings": []}}, - review_id="review-integrated", context=context, packet={"artifacts": []}, - started_at="2026-09-08T00:00:00Z", completed_at="2026-09-08T00:01:00Z", - integrated_stage=True, - ) - validated = reviewer_workspace._review_runtime().validate_stage_review(review) - assert validated.stage == "integrated_implementation" - assert validated.verdict == "accepted" - - -def test_native_compact_integrated_repair_keeps_exact_predecessor_controller_only( - review_roots: tuple[Path, Path, Path], -) -> None: - source, control, _ = review_roots - runtime = reviewer_workspace._review_runtime() - subprocess.run(["git", "init", "-q", str(source)], check=True) - subprocess.run(["git", "-C", str(source), "add", "."], check=True) - subprocess.run( - [ - "git", "-C", str(source), "-c", "user.name=Test", - "-c", "user.email=test@example.invalid", "commit", "-qm", "fixture", - ], - check=True, - ) - tree = subprocess.check_output( - ["git", "-C", str(source), "rev-parse", "HEAD^{tree}"], text=True - ).strip() - specification = control / ".work-bundle" / "orchestration" / "spec" / "verified" / "spec.md" - specification.parent.mkdir(parents=True) - specification.write_text( - "---\nid: spec-repair\nstatus: verified\n---\n# Specification\n", - encoding="utf-8", - ) - target = control / ".work-bundle" / "orchestration" / "plan" / "active" / "plan.md" - target.parent.mkdir(parents=True) - target.write_text( - "---\nid: plan-repair\nstatus: active\nsource_spec: [spec-repair]\n---\n# Plan\n", - encoding="utf-8", - ) - current_identity = runtime.stage_target_identity( - control, "integrated_implementation", target, source_root=source - ) - previous_identity = {**current_identity, "source_tree": "a" * 40} - previous_context = { - "stage": "integrated_implementation", - "target_identity": previous_identity, - "target_locator": "control:.work-bundle/orchestration/plan/active/plan.md", - "agent_id": "reviewer-previous", - "capability": "judgment", - "execution_id": "reviewer-previous-run", - "evidence_mode": "reproducible_snapshot", - "review_mode": "initial", - "review_target_kind": "stage", - "repair_frontier": None, - "review_reset": None, - } - previous = reviewer_workspace._task_product_judgment_review( - {"task_review": { - "reviewed_head": previous_identity["source_tree"], - "verdict": "repair", - "findings": [{ - "finding_id": "finding-live-oracle", - "severity": "blocking", - "requirement_id": "AC-009", - "boundary": "tests/test_native_review_integration.py", - "evidence": "validation invocation is not observed", - "expected": "one harness-owned validation", - "observed": "zero harness-owned validations", - "owner": "task_owner", - }], - }}, - review_id="review-integrated-previous", - context=previous_context, - packet={"artifacts": []}, - started_at="2026-09-09T00:00:00Z", - completed_at="2026-09-09T00:01:00Z", - integrated_stage=True, - ) - frontier = { - "prior_review_id": previous["review_id"], - "blocking_finding_ids": ["finding-live-oracle"], - "previous_reviewed_identity": previous_identity, - "repaired_identity": current_identity, - "affected_boundaries": ["tests/test_native_review_integration.py"], - "frozen_evidence_reference": runtime.review_evidence_identity(previous), - } - context = { - **previous_context, - "target_identity": current_identity, - "agent_id": "reviewer-current", - "execution_id": "reviewer-current-run", - "review_mode": "repair", - "repair_frontier": frontier, - "previous_review": previous, - } - packet = build_direct_evidence_packet( - source_root=source, - control_root=control, - protected_roots=[control / "credentials"], - artifacts=["control:.work-bundle/orchestration/plan/active/plan.md"], - search_roots=[], validators=[], sentinels=[], network_state="denied", - stage_review_context=context, - ) - created = create_reviewer_workspace( - runtime.reviewer_runtime_root(control), "review-integrated-repair", packet - ) - judgment = {"task_review": { - "reviewed_head": current_identity["source_tree"], - "verdict": "accept", - "findings": [], - }} - with patch.object( - reviewer_workspace, - "_run_native_process", - return_value=subprocess.CompletedProcess([], 0, native_events(judgment), ""), - ): - receipt = reviewer_workspace.run_native_reviewer( - Path(str(created["workspace_path"])), Path(sys.executable), - model="test-model", review_instructions="Review only the repaired product frontier.", - ) - request = json.loads(Path(receipt["receipt_path"]).with_suffix(".request.json").read_text()) - assert "previous_review" not in request["review_input"] - assert request["review_input"]["repair_frontier"] == frontier - review = {**receipt["review_result"], "reviewer_run": receipt["reviewer_run"]} - assert review["previous_review"] == previous - assert review["repair_frontier"] == frontier - history = control / ".work-bundle" / "orchestration" / "reviews" - history.mkdir(parents=True, exist_ok=True) - (history / f"{previous['review_id']}.json").write_text( - json.dumps(previous), encoding="utf-8" - ) - reference = runtime.publish_review(control, review, current_target_identity=current_identity) - stored, _ = runtime.load_stored_review( - control, reference, current_target_identity=current_identity - ) - assert stored["previous_review"] == previous - runtime._require_current_review(control, "integrated_implementation", current_identity) - - (source / "src" / "target.py").write_text( - "def target():\n return 2\n", encoding="utf-8" - ) - subprocess.run(["git", "-C", str(source), "add", "."], check=True) - subprocess.run( - [ - "git", "-C", str(source), "-c", "user.name=Test", - "-c", "user.email=test@example.invalid", "commit", "-qm", "accepted reset", - ], - check=True, - ) - reset_identity = runtime.stage_target_identity( - control, "integrated_implementation", target, source_root=source - ) - reset_context = { - **previous_context, - "target_identity": reset_identity, - "agent_id": "reviewer-reset", - "execution_id": "reviewer-reset-run", - "review_mode": "initial", - "repair_frontier": None, - "review_reset": { - "prior_review_id": review["review_id"], - "reason_class": "acceptance", - "reason": "Accepted source identity changed.", - }, - "previous_review": runtime._bounded_stage_predecessor(review), - } - nested_context = {**reset_context, "previous_review": review} - with pytest.raises(ReviewerWorkspaceError, match="STAGE_CONTEXT_INVALID"): - build_direct_evidence_packet( - source_root=source, - control_root=control, - protected_roots=[control / "credentials"], - artifacts=["control:.work-bundle/orchestration/plan/active/plan.md"], - search_roots=[], validators=[], sentinels=[], network_state="denied", - stage_review_context=nested_context, - ) - required, missing = runtime.stage_evidence_requirements( - control, "integrated_implementation", target - ) - assert missing == [] - required.update({ - item["locator"]: "source_tree" - for item in runtime.source_snapshot_entries(source) - }) - reset_packet = build_direct_evidence_packet( - source_root=source, - control_root=control, - protected_roots=[control / "credentials"], - artifacts=list(required), - search_roots=[], validators=[], sentinels=[], network_state="denied", - stage_review_context=reset_context, - ) - reset_created = create_reviewer_workspace( - runtime.reviewer_runtime_root(control), "review-integrated-reset", reset_packet - ) - reset_judgment = {"task_review": { - "reviewed_head": reset_identity["source_tree"], - "verdict": "accept", - "findings": [], - }} - with patch.object( - reviewer_workspace, - "_run_native_process", - return_value=subprocess.CompletedProcess([], 0, native_events(reset_judgment), ""), - ): - reset_receipt = reviewer_workspace.run_native_reviewer( - Path(str(reset_created["workspace_path"])), Path(sys.executable), - model="test-model", review_instructions="Review the current accepted product evidence.", - ) - reset_request = json.loads( - Path(reset_receipt["receipt_path"]).with_suffix(".request.json").read_text() - ) - assert "previous_review" not in reset_request["review_input"] - reset_review = { - **reset_receipt["review_result"], - "reviewer_run": reset_receipt["reviewer_run"], - } - assert reset_review["previous_review"] == runtime._bounded_stage_predecessor(review) - reset_reference = runtime.publish_review( - control, reset_review, current_target_identity=reset_identity - ) - runtime.load_stored_review( - control, reset_reference, current_target_identity=reset_identity - ) - runtime._require_current_review( - control, "integrated_implementation", reset_identity - ) - - chain = { - previous["review_id"]: previous, - review["review_id"]: review, - reset_review["review_id"]: reset_review, - } - forged = json.loads(json.dumps(reset_review)) - forged["previous_review"]["reviewer"]["agent_id"] = "forged-reviewer" - with pytest.raises( - runtime.ReviewContractError, match="projection does not match" - ): - runtime._validate_stored_stage_chain(forged, chain) - with pytest.raises(runtime.ReviewContractError, match="predecessor is missing"): - runtime._validate_stored_stage_chain( - reset_review, {previous["review_id"]: previous} - ) - cyclic = json.loads(json.dumps(reset_review)) - cyclic["review_reset"]["prior_review_id"] = cyclic["review_id"] - cyclic["previous_review"] = runtime._bounded_stage_predecessor(cyclic) - with pytest.raises(runtime.ReviewContractError, match="contains a cycle"): - runtime._validate_stored_stage_chain( - cyclic, {cyclic["review_id"]: cyclic} - ) - - malformed = { - **reset_review, - "review_id": "review-integrated-extra-history", - "previous_review": {**review, "previous_review": previous}, - } - (history / "review-integrated-extra-history.json").write_text( - json.dumps(malformed), encoding="utf-8" - ) - # Extra historical links are not part of the selected current authority. - runtime._require_current_review(control, "integrated_implementation", reset_identity) - - -def test_incomplete_stage_snapshot_fails_before_reviewer_process_launch( - review_roots: tuple[Path, Path, Path] -) -> None: - source, control, runtime = review_roots - subprocess.run(["git", "init", "-q", str(source)], check=True) - subprocess.run(["git", "-C", str(source), "add", "src/target.py", ".wor105-review-sentinel"], check=True) - subprocess.run( - ["git", "-C", str(source), "-c", "user.name=Test", "-c", "user.email=test@example.invalid", "commit", "-qm", "fixture"], - check=True, - ) - plan = control / ".work-bundle/orchestration/plan/active/plan.md" - plan.parent.mkdir(parents=True) - spec = control / ".work-bundle/orchestration/spec/active/spec.md" - spec.parent.mkdir(parents=True) - spec.write_text("---\nid: spec-preflight\nstatus: verified\n---\nSpec\n", encoding="utf-8") - plan.write_text("---\nid: plan-preflight\nstatus: Planned\nsource_spec: [spec-preflight]\n---\nPlan\n", encoding="utf-8") - task = plan.parent / "task.md" - task.write_text( - "---\nid: task-preflight\nplan_id: plan-preflight\nvalidation: [{id: VAL-1, command: true}]\n---\nTask\n", - encoding="utf-8", - ) - locator = "control:" + plan.relative_to(control).as_posix() - context = { - "stage": "integrated_implementation", - "target_identity": reviewer_workspace._review_runtime().stage_target_identity( - control, "integrated_implementation", plan, source_root=source - ), - "target_locator": locator, - "agent_id": "reviewer-preflight", - "capability": "judgment", - "execution_id": "reviewer-preflight-run", - "evidence_mode": "direct_source", - } - incomplete = build_direct_evidence_packet( - source_root=source, control_root=control, - protected_roots=[control / "credentials"], artifacts=[locator], search_roots=[], - validators=[], sentinels=[], network_state="denied", stage_review_context=context, - ) - assert incomplete["stage_evidence_manifest"]["missing"] - created = create_reviewer_workspace(runtime, "review-preflight", incomplete) - with patch.object(reviewer_workspace, "_run_sandboxed_process") as launch: - with pytest.raises(ReviewerWorkspaceError, match="STAGE_EVIDENCE_INCOMPLETE"): - reviewer_workspace.run_sandboxed_reviewer( - Path(str(created["workspace_path"])), ["reviewer"] - ) - launch.assert_not_called() - - -def test_stage_evidence_survives_plan_lifecycle_markers_but_not_product_changes( - review_roots: tuple[Path, Path, Path] -) -> None: - source, control, _runtime = review_roots - subprocess.run(["git", "init", "-q", str(source)], check=True) - subprocess.run(["git", "-C", str(source), "add", "."], check=True) - subprocess.run( - [ - "git", "-C", str(source), "-c", "user.name=Test", - "-c", "user.email=test@example.invalid", "commit", "-qm", "fixture", - ], - check=True, - ) - plan = control / ".work-bundle/orchestration/plan/active/plan-lifecycle.md" - task = control / ".work-bundle/orchestration/plan/active/plan-lifecycle/task-001.md" - spec = control / ".work-bundle/orchestration/spec/active/spec-lifecycle.md" - task.parent.mkdir(parents=True) - spec.parent.mkdir(parents=True) - spec.write_text( - "---\nid: spec-lifecycle\nstatus: verified\n---\n\n# Specification\n", - encoding="utf-8", - ) - plan.write_text( - "---\nid: plan-lifecycle\nstatus: In progress\n" - "accepted_results: [result-task-001]\n" - "source_spec: [.work-bundle/orchestration/spec/active/spec-lifecycle.md]\n" - "---\n\n# Plan\n\n" - "## Knowledge Base Update Carry Forward\n\n" - "- Disposition: required\n- Closure return: missing\n" - "- Source: accepted specification\n- Review Gate: resolve before archive\n", - encoding="utf-8", - ) - task.write_text( - "---\nid: task-001\nplan_id: plan-lifecycle\nphase_id: phase-001\n" - "status: In progress\n---\n\n# Task\n", - encoding="utf-8", - ) - runtime = reviewer_workspace._review_runtime() - metadata = control / ".work-bundle/project.yaml" - metadata.parent.mkdir(parents=True, exist_ok=True) - metadata.write_text( - "metadata_version: 4\n" - "workspace: {id: workspace-review, slug: review, mode: single-repository}\n" - "orchestration_control:\n" - " schema_version: 1\n" - " post_execution_review_round_limit: 5\n", - encoding="utf-8", - ) - identity = runtime.stage_target_identity( - control, "integrated_implementation", plan, source_root=source - ) - locator = "control:" + plan.relative_to(control).as_posix() - context = { - "stage": "integrated_implementation", - "target_identity": identity, - "target_locator": locator, - "agent_id": "reviewer-lifecycle", - "capability": "judgment", - "execution_id": "reviewer-lifecycle-run", - "evidence_mode": "direct_source", - } - required, missing = runtime.stage_evidence_requirements( - control, "integrated_implementation", plan - ) - assert missing == [] - required.update( - {item["locator"]: "source_tree" for item in runtime.source_snapshot_entries(source)} - ) - def legacy_stage_identity(path: Path, **kwargs: object) -> dict[str, object]: - return runtime.artifact_review_identity(path, content=kwargs.get("content")) - - with patch.object(runtime, "_stage_authority_identity", side_effect=legacy_stage_identity): - packet = build_direct_evidence_packet( - source_root=source, - control_root=control, - protected_roots=[control / "credentials"], - artifacts=list(required), - search_roots=[], - validators=[], - sentinels=[], - network_state="denied", - stage_review_context=context, - ) - with pytest.raises( - ReviewerWorkspaceError, match="WB_POST_EXECUTION_ROUND_REQUIRED" - ): - create_reviewer_workspace( - runtime.reviewer_runtime_root(control), "review-lifecycle", packet - ) - reserved = bounded_closure.begin_review_round( - control, - flow_id="plan-lifecycle", - request_id="integrated-review-request-1", - review_id="review-lifecycle", - target_identity=identity, - executor_attempts=[ - {"execution_id": "task-001-attempt-1", "state": "completed"} - ], - known_missing_evidence=[], - ) - created = create_reviewer_workspace( - runtime.reviewer_runtime_root(control), "review-lifecycle", packet - ) - assert bounded_closure.review_round_status( - control, flow_id="plan-lifecycle" - )["latest_round"]["state"] == "prepared" - judgment = { - "task_review": { - "reviewed_head": identity["source_tree"], - "verdict": "accept", - "findings": [], - } - } - with patch.object( - reviewer_workspace, - "_run_native_process", - return_value=subprocess.CompletedProcess([], 0, native_events(judgment), ""), - ): - receipt = reviewer_workspace.run_native_reviewer( - Path(str(created["workspace_path"])), - Path(sys.executable), - model="test-model", - review_instructions="Review the frozen product evidence.", - ) - with pytest.raises( - ReviewerWorkspaceError, match="WB_POST_EXECUTION_JUDGMENT_ALREADY_RECORDED" - ): - reviewer_workspace.run_native_reviewer( - Path(str(created["workspace_path"])), - Path(sys.executable), - model="test-model", - review_instructions="Review the frozen product evidence again.", - ) - review = {**receipt["review_result"], "reviewer_run": receipt["reviewer_run"]} - reference = runtime.publish_review(control, review, current_target_identity=identity) - assert runtime.publish_review( - control, review, current_target_identity=identity - ) == reference - completed_round = bounded_closure.review_round_status( - control, flow_id="plan-lifecycle" - )["latest_round"] - assert completed_round["round_id"] == reserved["round_id"] - assert completed_round["state"] == "completed" - assert completed_round["outcome"] == "accepted" - - request = json.loads( - Path(receipt["receipt_path"]).with_suffix(".request.json").read_text() - ) - assert locator in {item["locator"] for item in request["evidence"]} - controller_path = Path(receipt["receipt_path"]).with_suffix(".controller.json") - controller_evidence = json.loads(controller_path.read_text()) - assert locator not in {item["locator"] for item in controller_evidence} - - plan.write_text( - plan.read_text(encoding="utf-8") - .replace("status: In progress", "status: Completed") - .replace("Closure return: missing", "Closure return: completed"), - encoding="utf-8", - ) - task.write_text( - task.read_text(encoding="utf-8").replace("status: In progress", "status: Completed"), - encoding="utf-8", - ) - assert runtime.stage_target_identity( - control, "integrated_implementation", plan, source_root=source - ) == identity - runtime._require_current_review(control, "integrated_implementation", identity) - - plan.write_text( - plan.read_text(encoding="utf-8").replace( - "Review Gate: resolve before archive", "Review Gate: bypass product review" - ), - encoding="utf-8", - ) - changed_identity = runtime.stage_target_identity( - control, "integrated_implementation", plan, source_root=source - ) - assert changed_identity != identity - with pytest.raises(SystemExit, match="fresh accepted integrated_implementation review"): - runtime._require_current_review( - control, "integrated_implementation", changed_identity - ) - - -def test_bounded_read_search_and_validators_are_allowed(review_roots: tuple[Path, Path, Path]) -> None: - source, control, runtime = review_roots - created = create_reviewer_workspace(runtime, "review-002", packet(source, control)) - workspace = Path(str(created["workspace_path"])) - - read = execute_reviewer_request(workspace, {"operation": "read", "artifact": "source:src/target.py"}) - search = execute_reviewer_request(workspace, {"operation": "search", "pattern": "return 1"}) - valid = execute_reviewer_request(workspace, {"operation": "validate", "validator_id": "target-json"}) - digest = execute_reviewer_request(workspace, {"operation": "validate", "validator_id": "source-digest"}) - - assert read["status"] == "allowed" and "def target" in str(read["content"]) - assert search["status"] == "allowed" and search["matches"] == ["source:src/target.py:2:return 1"] - assert valid == {"status": "allowed", "validator_id": "target-json", "result": "passed"} - assert digest["result"] == sha256(source / "src" / "target.py") - - -@pytest.mark.parametrize( - ("operation_request", "code"), - [ - ({"operation": "write", "artifact": "source:.wor105-review-sentinel", "content": "changed"}, "WB_REVIEW_SOURCE_WRITE_DENIED"), - ({"operation": "write", "artifact": "control:orchestration/docs/wor105/.review-sentinel", "content": "changed"}, "WB_REVIEW_CONTROL_WRITE_DENIED"), - ({"operation": "read", "artifact": "control:credentials/credentials.yaml"}, "WB_REVIEW_PROTECTED_READ_DENIED"), - ({"operation": "read", "artifact": "host:~/.gitconfig"}, "WB_REVIEW_HOST_CONFIG_READ_DENIED"), - ({"operation": "network", "target": "https://example.invalid"}, "WB_REVIEW_NETWORK_DENIED"), - ], -) -def test_reviewer_operations_mechanically_deny_forbidden_effects( - review_roots: tuple[Path, Path, Path], operation_request: dict[str, str], code: str -) -> None: - source, control, runtime = review_roots - source_before = sha256(source / ".wor105-review-sentinel") - control_before = sha256(control / "orchestration" / "docs" / "wor105" / ".review-sentinel") - created = create_reviewer_workspace(runtime, "review-003", packet(source, control)) - - with pytest.raises(ReviewerWorkspaceError, match=code) as exc: - execute_reviewer_request(Path(str(created["workspace_path"])), operation_request) - - assert exc.value.result["classification"] == "denied" - assert sha256(source / ".wor105-review-sentinel") == source_before - assert sha256(control / "orchestration" / "docs" / "wor105" / ".review-sentinel") == control_before - - -def test_write_scope_denies_origin_tokens_and_cleanup_requires_owned_terminal_state( - review_roots: tuple[Path, Path, Path] -) -> None: - source, control, runtime = review_roots - created = create_reviewer_workspace(runtime, "review-004", packet(source, control)) - workspace = Path(str(created["workspace_path"])) - state_path = Path(str(created["state_path"])) - - with pytest.raises(ReviewerWorkspaceError, match="WB_REVIEW_SOURCE_WRITE_DENIED"): - enforce_reviewer_write_scope("source:src/target.py") - with pytest.raises(ReviewerWorkspaceError, match="WB_REVIEW_TERMINAL_RECORD_INVALID"): - cleanup_reviewer_workspace(runtime, "review-004", terminal_evidence="") - - terminal = { - "schema": "reviewer-terminal-review-v1", - "review_id": "review-004", - "packet_sha256": created["packet_sha256"], - "verdict": "accepted", - "evidence_digest": created["evidence_digest"], - "sentinel_digest": created["sentinel_digest"], - } - cleaned = cleanup_reviewer_workspace( - runtime, - "review-004", - terminal_review=terminal, - source_root=source, - control_root=control, - protected_roots=[control / "credentials"], - ) - - assert cleaned["status"] == "cleaned" - assert cleaned["terminal_review_sha256"] - assert not workspace.exists() - assert not state_path.exists() - assert source.exists() and control.exists() - - -def test_dispatcher_exposes_reviewer_workspace_commands() -> None: - result = subprocess.run( - [sys.executable, str(REPO_ROOT / "scripts" / "wb.py"), "reviewer-workspace-create", "--help"], - cwd=REPO_ROOT, - text=True, - capture_output=True, - check=False, - ) - - assert result.returncode == 0 - assert "--packet" in result.stdout - dispatcher = (REPO_ROOT / "scripts" / "work-bundle" / "dispatcher.py").read_text(encoding="utf-8") - assert "reviewer-workspace-operation" in dispatcher - assert "reviewer-workspace-cleanup" in dispatcher - - -def test_exact_protected_roots_block_nonheuristic_private_path(review_roots: tuple[Path, Path, Path]) -> None: - source, control, _ = review_roots - protected = control / "opaque-store" - protected.mkdir() - (protected / "material.txt").write_text("private\n", encoding="utf-8") - - with pytest.raises(ReviewerWorkspaceError, match="WB_REVIEW_PROTECTED_READ_DENIED"): - build_direct_evidence_packet( - source_root=source, - control_root=control, - protected_roots=[protected], - artifacts=["control:opaque-store/material.txt"], - search_roots=[], - validators=[], - sentinels=[], - network_state="denied", - ) - - -@pytest.mark.skipif(platform.system() != "Darwin", reason="sandbox-exec is the accepted macOS process boundary") -def test_sandboxed_process_denies_origin_write_protected_read_and_network( - review_roots: tuple[Path, Path, Path], tmp_path: Path -) -> None: - source, control, runtime = review_roots - registry = tmp_path / "opaque-live-registry" - registry.mkdir() - protected_file = registry / "store.data" - protected_file.write_text("private registry\n", encoding="utf-8") - source_sentinel = source / ".wor105-review-sentinel" - source_before = sha256(source_sentinel) - direct_packet = build_direct_evidence_packet( - source_root=source, - control_root=control, - protected_roots=[registry, control / "credentials"], - artifacts=["source:src/target.py"], - search_roots=["source:src"], - validators=[ - { - "validator_id": "write-probe", - "kind": "command", - "argv": [sys.executable, "-c", f"open({str(source_sentinel)!r}, 'w').write('changed')"], - }, - { - "validator_id": "read-probe", - "kind": "command", - "argv": [sys.executable, "-c", f"open({str(protected_file)!r}).read()"], - }, - { - "validator_id": "network-probe", - "kind": "command", - "argv": [sys.executable, "-c", "import socket; socket.socket().connect(('127.0.0.1', 9))"], - }, - { - "validator_id": "bounded-read", - "kind": "command", - "argv": ["/bin/cat", "evidence/source/src/target.py"], - }, - { - "validator_id": "bounded-search", - "kind": "command", - "argv": ["/usr/bin/grep", "return 1", "evidence/source/src/target.py"], - }, - ], - sentinels=["source:.wor105-review-sentinel", "control:orchestration/docs/wor105/.review-sentinel"], - network_state="denied", - ) - created = create_reviewer_workspace( - runtime, - "review-sandbox", - direct_packet, - source_root=source, - control_root=control, - protected_roots=[registry, control / "credentials"], - ) - workspace = Path(str(created["workspace_path"])) - - for validator_id in ("write-probe", "read-probe", "network-probe"): - with pytest.raises(ReviewerWorkspaceError, match="WB_REVIEW_SANDBOX_DENIED"): - execute_reviewer_request(workspace, {"operation": "validate", "validator_id": validator_id}) - - assert execute_reviewer_request( - workspace, {"operation": "validate", "validator_id": "bounded-read"} - )["result"] == "passed" - assert execute_reviewer_request( - workspace, {"operation": "validate", "validator_id": "bounded-search"} - )["result"] == "passed" - assert sha256(source_sentinel) == source_before - - -def test_every_denied_request_appends_unique_privacy_safe_event(review_roots: tuple[Path, Path, Path]) -> None: - source, control, runtime = review_roots - created = create_reviewer_workspace(runtime, "review-events", packet(source, control)) - workspace = Path(str(created["workspace_path"])) - - for operation in ({"operation": "network", "target": "secret-target"}, {"operation": "write", "artifact": "source:x"}): - with pytest.raises(ReviewerWorkspaceError): - execute_reviewer_request(workspace, operation) - - events_path = runtime / "events" / "review-events.jsonl" - events = [json.loads(line) for line in events_path.read_text(encoding="utf-8").splitlines()] - assert len(events) == 2 - assert len({event["event_id"] for event in events}) == 2 - assert all(event["privacy"] == "operational_metadata_only" for event in events) - serialized = json.dumps(events) - assert "secret-target" not in serialized - assert "source:x" not in serialized - - -def test_sandbox_denial_requires_a_failed_process() -> None: - incidental = subprocess.CompletedProcess( - ["reviewer"], 0, stdout="", stderr="sandbox violation: harmless probe denied" - ) - denied = subprocess.CompletedProcess( - ["reviewer"], 1, stdout="", stderr="sandbox violation: operation not permitted" - ) - ordinary_failure = subprocess.CompletedProcess( - ["reviewer"], 1, stdout="", stderr="reviewer assertion failed" - ) - - assert reviewer_workspace._sandbox_denied(incidental) is False - assert reviewer_workspace._sandbox_denied(denied) is True - assert reviewer_workspace._sandbox_denied(ordinary_failure) is False - - -def test_sandbox_profile_allows_split_environment_and_base_runtime_roots( - monkeypatch: pytest.MonkeyPatch, tmp_path: Path -) -> None: - environment_prefix = tmp_path / "uv-environment" - base_prefix = tmp_path / "python-framework" - executable = environment_prefix / "bin" / "python" - policy = { - "source": str(tmp_path / "source"), - "control": str(tmp_path / "control"), - "protected": [str(tmp_path / "control" / "credentials")], - } - monkeypatch.setattr(reviewer_workspace.sys, "prefix", str(environment_prefix)) - monkeypatch.setattr(reviewer_workspace.sys, "exec_prefix", str(environment_prefix / "exec")) - monkeypatch.setattr(reviewer_workspace.sys, "base_prefix", str(base_prefix)) - monkeypatch.setattr(reviewer_workspace.sys, "base_exec_prefix", str(base_prefix / "exec")) - monkeypatch.setattr(reviewer_workspace.sys, "executable", str(executable)) - - profile = reviewer_workspace._sandbox_profile(tmp_path / "review", policy, []) - - for root in (environment_prefix, environment_prefix / "exec", base_prefix, base_prefix / "exec"): - assert f'(subpath "{root}")' in profile - - -def test_cleanup_rejects_arbitrary_terminal_text(review_roots: tuple[Path, Path, Path]) -> None: - source, control, runtime = review_roots - create_reviewer_workspace(runtime, "review-terminal", packet(source, control)) - - with pytest.raises(ReviewerWorkspaceError, match="WB_REVIEW_TERMINAL_RECORD_INVALID"): - cleanup_reviewer_workspace(runtime, "review-terminal", terminal_evidence="accepted") - - -@pytest.mark.parametrize("mutate", ["evidence", "sentinel"]) -def test_cleanup_rejects_changed_evidence_or_origin_sentinel( - review_roots: tuple[Path, Path, Path], mutate: str -) -> None: - source, control, runtime = review_roots - review_id = f"review-changed-{mutate}" - created = create_reviewer_workspace(runtime, review_id, packet(source, control)) - workspace = Path(str(created["workspace_path"])) - terminal = { - "schema": "reviewer-terminal-review-v1", - "review_id": review_id, - "packet_sha256": created["packet_sha256"], - "verdict": "accepted", - "evidence_digest": created["evidence_digest"], - "sentinel_digest": created["sentinel_digest"], - } - if mutate == "evidence": - target = workspace / "evidence" / "source" / "src" / "target.py" - target.chmod(0o644) - target.write_text("changed\n", encoding="utf-8") - else: - (source / ".wor105-review-sentinel").write_text("changed\n", encoding="utf-8") - - with pytest.raises(ReviewerWorkspaceError, match="WB_REVIEW_TERMINAL_EVIDENCE_CHANGED"): - cleanup_reviewer_workspace( - runtime, - review_id, - terminal_review=terminal, - source_root=source, - control_root=control, - protected_roots=[control / "credentials"], - ) - - -@pytest.mark.skipif(platform.system() != "Darwin", reason="sandbox-exec is the accepted macOS process boundary") -def test_entire_reviewer_process_is_deny_default_and_receipted( - review_roots: tuple[Path, Path, Path] -) -> None: - source, control, runtime = review_roots - created = create_reviewer_workspace(runtime, "review-process", packet(source, control)) - workspace = Path(str(created["workspace_path"])) - source_target = source / "src" / "target.py" - - denied = reviewer_workspace.run_sandboxed_reviewer( - workspace, - [DARWIN_SANDBOX_SHELL, "-c", 'IFS= read -r line < "$1"', "reviewer", str(source_target)], - ) - allowed = reviewer_workspace.run_sandboxed_reviewer( - workspace, - [ - DARWIN_SANDBOX_SHELL, - "-c", - "{ IFS= read -r first; IFS= read -r second; } < evidence/source/src/target.py; " - "[ \"$second\" = ' return 1' ] && printf passed > scratch/result", - ], - ) - - assert denied["status"] == "denied" - assert allowed["status"] == "passed" - receipt = json.loads(Path(str(allowed["receipt_path"])).read_text(encoding="utf-8")) - assert receipt["packet_sha256"] == created["packet_sha256"] - assert receipt["sandbox_profile_sha256"] - assert receipt["argv_sha256"] - - -@pytest.mark.skipif(platform.system() != "Darwin", reason="sandbox-exec is the accepted macOS process boundary") -def test_deny_default_blocks_omitted_host_root_and_event_truncation( - review_roots: tuple[Path, Path, Path], tmp_path: Path -) -> None: - source, control, runtime = review_roots - omitted_host_root = tmp_path / "host-config-not-listed" - omitted_host_root.mkdir() - omitted_file = omitted_host_root / "opaque" - omitted_file.write_text("host private\n", encoding="utf-8") - created = create_reviewer_workspace(runtime, "review-sealed-events", packet(source, control)) - workspace = Path(str(created["workspace_path"])) - - first = reviewer_workspace.run_sandboxed_reviewer( - workspace, - [DARWIN_SANDBOX_SHELL, "-c", 'IFS= read -r line < "$1"', "reviewer", str(omitted_file)], - ) - event_path = runtime / "events" / "review-sealed-events.jsonl" - before = event_path.read_bytes() - second = reviewer_workspace.run_sandboxed_reviewer( - workspace, - [DARWIN_SANDBOX_SHELL, "-c", 'printf truncated > "$1"', "reviewer", str(event_path)], - ) - - assert first["status"] == second["status"] == "denied" - assert event_path.read_bytes().startswith(before) - assert event_path.stat().st_mode & 0o777 == 0o400 - assert second["event_log_sha256"] == sha256(event_path) - assert b"truncated" not in event_path.read_bytes() - - -def test_cleanup_rejects_substitute_roots_with_identical_sentinels( - review_roots: tuple[Path, Path, Path], tmp_path: Path -) -> None: - source, control, runtime = review_roots - created = create_reviewer_workspace(runtime, "review-root-identity", packet(source, control)) - substitute = tmp_path / "substitute-source" - (substitute / "src").mkdir(parents=True) - (substitute / "src" / "target.py").write_text("def target():\n return 1\n", encoding="utf-8") - (substitute / ".wor105-review-sentinel").write_text("immutable-source-sentinel-v1\n", encoding="utf-8") - terminal = { - "schema": "reviewer-terminal-review-v1", - "review_id": "review-root-identity", - "packet_sha256": created["packet_sha256"], - "verdict": "accepted", - "evidence_digest": created["evidence_digest"], - "sentinel_digest": created["sentinel_digest"], - } - - with pytest.raises(ReviewerWorkspaceError, match="WB_REVIEW_ROOT_IDENTITY_MISMATCH"): - cleanup_reviewer_workspace( - runtime, - "review-root-identity", - terminal_review=terminal, - source_root=substitute, - control_root=control, - protected_roots=[control / "credentials"], - ) - - -def test_dispatcher_exposes_whole_reviewer_process_launcher() -> None: - result = subprocess.run( - [sys.executable, str(REPO_ROOT / "scripts" / "wb.py"), "reviewer-process-run", "--help"], - cwd=REPO_ROOT, - text=True, - capture_output=True, - check=False, - ) - - assert result.returncode == 0 - assert "--argv-json" in result.stdout diff --git a/tests/test_rule_contracts.py b/tests/test_rule_contracts.py index 888335a..8ac2f5e 100644 --- a/tests/test_rule_contracts.py +++ b/tests/test_rule_contracts.py @@ -174,16 +174,16 @@ def test_scoped_validate_rules_resolves_toolkit_root(tmp_path: Path) -> None: def test_scoped_validate_rules_resolves_global_and_project_roots(tmp_path: Path) -> None: - config = tmp_path / "config" + config = tmp_path / ".work-bundle" global_root = config / "rules" project = tmp_path / "project" project_root = project / ".work-bundle" / "rules" for root, rule_id in [(global_root, "global-cross-cutting"), (project_root, "project-cross-cutting")]: root.mkdir(parents=True) (root / f"{rule_id}.md").write_text(valid_rule_md(rule_id), encoding="utf-8") - assert run_wb("create-rules", str(root), env={"WB_CONFIG_ROOT": str(config)}).returncode == 0 + assert run_wb("create-rules", str(root), env={"HOME": str(tmp_path)}).returncode == 0 - global_result = run_wb("validate-rules", "--scope", "global", env={"WB_CONFIG_ROOT": str(config)}) + global_result = run_wb("validate-rules", "--scope", "global", env={"HOME": str(tmp_path)}) global_payload = json.loads(global_result.stdout) assert global_result.returncode == 0, global_result.stdout + global_result.stderr assert global_payload["scope"] == "global" @@ -195,7 +195,7 @@ def test_scoped_validate_rules_resolves_global_and_project_roots(tmp_path: Path) "project", "--project-root", str(project), - env={"WB_CONFIG_ROOT": str(config)}, + env={"HOME": str(tmp_path)}, ) project_payload = json.loads(project_result.stdout) assert project_result.returncode == 0, project_result.stdout + project_result.stderr @@ -226,10 +226,10 @@ def test_toolkit_create_rules_blocks_when_project_root_differs_from_work_bundle_ def test_effective_rule_registry_reports_optional_missing_and_duplicate_ids(tmp_path: Path) -> None: sys.path.insert(0, str(REPO_ROOT / "scripts" / "work-bundle")) old_root = os.environ.get("WB_WORK_BUNDLE_ROOT") - old_config = os.environ.get("WB_CONFIG_ROOT") + old_home = os.environ.get("HOME") try: os.environ["WB_WORK_BUNDLE_ROOT"] = str(tmp_path / "toolkit") - os.environ["WB_CONFIG_ROOT"] = str(tmp_path / "config") + os.environ["HOME"] = str(tmp_path / "home") sys.modules.pop("rules", None) import rules as rules_module @@ -250,10 +250,10 @@ def test_effective_rule_registry_reports_optional_missing_and_duplicate_ids(tmp_ os.environ.pop("WB_WORK_BUNDLE_ROOT", None) else: os.environ["WB_WORK_BUNDLE_ROOT"] = old_root - if old_config is None: - os.environ.pop("WB_CONFIG_ROOT", None) + if old_home is None: + os.environ.pop("HOME", None) else: - os.environ["WB_CONFIG_ROOT"] = old_config + os.environ["HOME"] = old_home if sys.path and sys.path[0] == str(REPO_ROOT / "scripts" / "work-bundle"): sys.path.pop(0) @@ -529,58 +529,69 @@ def test_defect_rules_support_same_scope_specification_owned_handling() -> None: assert "evidence persistence is not required" in evidence -def test_orchestration_rules_require_contract_barrier_and_review_settlement_evidence() -> None: +def test_orchestration_rules_define_closed_executor_result_and_direct_review_boundary() -> None: handoff = (REPO_ROOT / "rules/orchestration/orch-handoff-required.md").read_text(encoding="utf-8") review = (REPO_ROOT / "rules/orchestration/orch-review-completion.md").read_text(encoding="utf-8") - assert "contract_decoupling" in handoff - assert "common contracts checked" in handoff - assert "`peer_implementation_validation_used: false`" in handoff - assert "barrier id, participant role, readiness `reached|blocked`" in handoff - assert "every participant completed or blocked with executor-result handoffs before joint validation began" in handoff - - assert "fails closed before `Completed`" in handoff - assert "not only before `build-review-package`" in handoff - - assert "compiled Truth Basis" in review - assert "AUTH constraints" in review - assert "universal task-review evidence" in review - assert "implementation-review agent" in review - assert "explicitly required" in review - assert "Route missing evidence to its first owner" in review - assert "publication-only/control resume uses the compact accepted result" in review - assert "Route incomplete durable knowledge work to `knowledge-blocked`" in review - assert "Create or require plan repair only for a decomposition defect" in review - assert "specification repair only for a requirement, design, or authority defect" in review - assert "Do not create a repair specification for every failed review gate" in review - assert "evidence_capability" in review - assert "INV/VAL" in review - assert "incapable green" in review - assert "pre-closure oracle-capability check" in review - assert "no_validation_bearing_obligation" in review - assert "WOR-59 G9 remains the unchanged post-execution classifier" in review - - -def test_execution_and_review_skills_carry_task003_flow_requirements() -> None: + for current in ( + "canonical `executor-result-v1`", + "factual scope, changed paths, focused observations", + "separate from independent product judgment", + "Permit direct product review", + "Do not put verdicts, acceptance, repair advice", + ): + assert current in handoff + for retired in ( + "contract_decoupling", + "peer_implementation_validation_used", + "build-review-package", + "publication receipt", + ): + assert retired not in handoff + + for current in ( + "distinct implementation reviewer", + "exact frozen candidate", + "canonical `accepted-task-result-v1`", + "one compact final workflow review", + "Keep finalization mechanical", + ): + assert current in review + for retired in ("review round", "review-of-review", "publication-only/control resume"): + assert retired not in review + + +def test_execution_and_review_skills_define_current_optional_direct_review_flow() -> None: execute = (REPO_ROOT / "skills/orch-execute-plan/SKILL.md").read_text(encoding="utf-8") review = (REPO_ROOT / "skills/orch-review-plan/SKILL.md").read_text(encoding="utf-8") - assert "Contract-decoupled participants validate against the common contract" in execute - assert "accepted prior handoffs" in execute - assert "reach the named barrier before convergence work" in execute - assert "The scheduler does not perform code-quality review" in execute - assert "validate-executor-result" in execute - assert "validate initial executor facts without demanding or embedding the future review verdict" in execute - assert "stored required-review authority" in execute + for current in ( + "path-sorted changed-path manifest", + "canonical `executor-result-v1`", + "When review is required", + "distinct reviewer", + "does not accept the product", + ): + assert current in execute + for retired in ( + "accepted prior handoffs", + "named barrier", + "validate-executor-result", + "embedded legacy status", + ): + assert retired not in execute - assert "Independent `dev-code-review` owns task-scoped implementation quality" in review - assert "compiled Truth Basis" in review - assert "implementation-review agent" in review - assert "do not require universal task-review evidence or embedded handoff verdicts" in review - assert "Do not broadly inspect source" in review - assert "knowledge-blocked" in review - assert "repair plan only" in review - assert "repair specification" in review + for current in ( + "exact frozen commit or worktree candidate", + "implementation-review-v1", + "accepted implementation review when required", + "one compact final workflow review", + "they do not reconstruct review history", + "Do not reread source for code quality or repeat implementation review", + ): + assert current in review + for retired in ("review round", "publication receipt"): + assert retired not in review def test_initialize_project_guidance_matches_create_rule_project_scope() -> None: @@ -608,7 +619,7 @@ def test_initialize_project_v4_migration_guardrails_and_pressure_scenarios() -> assert "migrate-control-plane" in initialize assert "migrate-registered-projects" in initialize - assert "--repository-remote" in initialize + assert "--repository <id=remote>" in initialize assert "load every applicable rule body in full" in initialize assert "do not edit the project registry directly" in initialize assert "do not change an external repository's Git config" in initialize @@ -658,7 +669,7 @@ def test_v4_portable_and_device_local_authority_contracts_converge() -> None: assert 'registry/projects.yaml' not in control_plane assert 'registry/projects.yaml' not in repository_preflight assert "resolve_project_registry_path" in control_plane - assert "project_registry_path" in repository_preflight + assert "_infrastructure.join_workspace_binding" in repository_preflight def test_initialize_project_pressure_scenario_covers_v4_authority_and_member_rollback() -> None: diff --git a/tests/test_session_start_hook.py b/tests/test_session_start_hook.py index ef9feb9..7a8df1f 100644 --- a/tests/test_session_start_hook.py +++ b/tests/test_session_start_hook.py @@ -1,32 +1,103 @@ from __future__ import annotations import json +import importlib.util import os import subprocess import sys from pathlib import Path -from test_project_initialization import REPO_ROOT, bootstrap_config, git, run_wb - +import pytest +import yaml +REPO_ROOT = Path(__file__).resolve().parents[1] HOOK = REPO_ROOT / "bin" / "work-bundle-session-start.py" +def load_work_bundle_project_module(): + module_path = REPO_ROOT / "scripts/work-bundle/project.py" + module_dir = str(module_path.parent) + old_core = sys.modules.pop("core", None) + sys.path.insert(0, module_dir) + try: + spec = importlib.util.spec_from_file_location("session_start_project", module_path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + finally: + if sys.path and sys.path[0] == module_dir: + sys.path.pop(0) + if old_core is not None: + sys.modules["core"] = old_core + + +def git(path: Path, *args: str) -> str: + completed = subprocess.run( + ["git", "-C", str(path), *args], + check=True, + capture_output=True, + text=True, + ) + return completed.stdout.strip() + + +def bootstrap_config(tmp_path: Path) -> Path: + config = tmp_path / "config" + (config / "registry").mkdir(parents=True) + (config / "bootstrap.yaml").write_text( + "\n".join( + [ + "bootstrap_version: v1", + "authority: canonical", + f"work_bundle_root: {REPO_ROOT}", + 'project_registry: "$work_bundle_config_root/registry/projects.yaml"', + 'skill_registry: "$work_bundle_config_root/registry/skill-registry.yaml"', + "", + ] + ), + encoding="utf-8", + ) + (config / "registry/projects.yaml").write_text("projects: []\n", encoding="utf-8") + return config + + +def run_wb(config_root: Path, *args: str) -> subprocess.CompletedProcess[str]: + env = os.environ.copy() + env["HOME"] = str(config_root.parent) + return subprocess.run( + [sys.executable, str(REPO_ROOT / "scripts/wb.py"), *args], + cwd=REPO_ROOT, env=env, check=False, capture_output=True, text=True, + ) + + def _init_project(tmp_path: Path) -> tuple[Path, Path]: - config_root = bootstrap_config(tmp_path) + generated_config = bootstrap_config(tmp_path) + config_root = tmp_path / ".work-bundle" + generated_config.rename(config_root) project = tmp_path / "project" project.mkdir() git(project, "init", "-q", "-b", "main") git(project, "config", "user.email", "test@example.com") git(project, "config", "user.name", "Test") + remote = tmp_path / "project.git" + subprocess.run(["git", "init", "--bare", "-q", str(remote)], check=True) + project.joinpath("README.md").write_text("demo\n", encoding="utf-8") + git(project, "add", "README.md") + git(project, "commit", "-q", "-m", "initial") + git(project, "remote", "add", "origin", str(remote)) + git(project, "push", "-q", "-u", "origin", "main") init = run_wb( config_root, - "init-project", + "init-workspace", str(project), "--mode", "single-repository", - "--name", + "--slug", "demo", + "--repository", + f"source={remote}", + "--apply", ) assert init.returncode == 0, init.stdout + init.stderr return config_root, project @@ -34,7 +105,7 @@ def _init_project(tmp_path: Path) -> tuple[Path, Path]: def _run_hook(config_root: Path, stdin: str, cwd: Path) -> subprocess.CompletedProcess[str]: env = os.environ.copy() - env["WB_CONFIG_ROOT"] = str(config_root) + env["HOME"] = str(config_root.parent) return subprocess.run( [sys.executable, str(HOOK)], input=stdin, @@ -53,7 +124,7 @@ def test_session_start_initialized_project_is_idempotent(tmp_path: Path) -> None assert first.returncode == 0, first.stdout + first.stderr first_data = json.loads(first.stdout) assert first_data["command"] == "session-start" - assert first_data["status"] == "passed" + assert first_data["status"] == "passed", json.dumps(first_data, indent=2) assert first_data["registry_status"] == "registered" assert first_data["agents_status"] == "unchanged" assert first_data["changed_files"] == [] @@ -70,7 +141,9 @@ def test_session_start_initialized_project_is_idempotent(tmp_path: Path) -> None def test_session_start_uninitialized_project_skips_without_agents_write(tmp_path: Path) -> None: - config_root = bootstrap_config(tmp_path) + generated_config = bootstrap_config(tmp_path) + config_root = tmp_path / ".work-bundle" + generated_config.rename(config_root) project = tmp_path / "uninitialized" project.mkdir() @@ -170,6 +243,53 @@ def test_session_start_repairs_stale_metadata_without_rewriting_agents(tmp_path: assert data["project_agents_checksum"].startswith("sha256:") +def test_session_start_accepts_equivalent_flow_style_agents_sync_without_rewrite( + tmp_path: Path, +) -> None: + config_root, project = _init_project(tmp_path) + metadata_path = project / ".work-bundle/project.yaml" + document = yaml.safe_load(metadata_path.read_text(encoding="utf-8")) + agents_sync = document.pop("agents_sync") + rendered = yaml.safe_dump(document, allow_unicode=True, sort_keys=False).rstrip() + "\n" + rendered += "agents_sync: " + json.dumps(agents_sync, separators=(",", ":")) + "\n" + metadata_path.write_text(rendered, encoding="utf-8") + before = metadata_path.read_bytes() + + result = run_wb(config_root, "session-start", "--project-root", str(project), "--json") + + assert result.returncode == 0, result.stdout + result.stderr + data = json.loads(result.stdout) + assert data["status"] == "passed" + assert data["agents_status"] == "unchanged" + assert data["changed_files"] == [] + assert metadata_path.read_bytes() == before + + +def test_agents_sync_owner_rejects_invalid_v4_before_any_write( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + config_root, project = _init_project(tmp_path) + agents_path = project / "AGENTS.md" + metadata_path = project / ".work-bundle/project.yaml" + agents_path.write_text("# User agents content\n", encoding="utf-8") + metadata_path.write_text( + metadata_path.read_text(encoding="utf-8").replace("authority: canonical\n", ""), + encoding="utf-8", + ) + agents_before = agents_path.read_bytes() + metadata_before = metadata_path.read_bytes() + monkeypatch.setenv("HOME", str(config_root.parent)) + monkeypatch.setenv("WB_WORK_BUNDLE_ROOT", str(REPO_ROOT)) + project_module = load_work_bundle_project_module() + + with pytest.raises(project_module.InfrastructureError) as caught: + project_module.sync_agents_managed_section(project) + + assert caught.value.code == "WB_INFRASTRUCTURE_SCHEMA_INVALID" + assert agents_path.read_bytes() == agents_before + assert metadata_path.read_bytes() == metadata_before + + def test_session_start_wraps_legacy_template(tmp_path: Path) -> None: config_root, project = _init_project(tmp_path) agents_path = project / "AGENTS.md" @@ -196,7 +316,7 @@ def test_session_start_skips_invalid_project_metadata_with_migration_warning(tmp data = json.loads(result.stdout) assert data["status"] == "skipped" assert data["changed_files"] == [] - assert "project metadata missing required fields" in " ".join(data["warnings"]) + assert "project metadata version unsupported" in " ".join(data["warnings"]) assert "wb-initialize-project migrate" in " ".join(data["warnings"]) assert (project / "AGENTS.md").read_text(encoding="utf-8") == agents_before diff --git a/tests/test_work_bundle_defect_evidence.py b/tests/test_work_bundle_defect_evidence.py index 797d12a..d2ae859 100644 --- a/tests/test_work_bundle_defect_evidence.py +++ b/tests/test_work_bundle_defect_evidence.py @@ -22,7 +22,7 @@ def prepare_cwd(tmp_path: Path, catalog: str = CATALOG) -> Path: def run_wb(tmp_path: Path, *args: str, cwd: Path | None = None) -> subprocess.CompletedProcess[str]: env = os.environ.copy() - env["WB_CONFIG_ROOT"] = str(tmp_path / "config") + env["HOME"] = str(tmp_path) return subprocess.run( [sys.executable, str(WB), *args], cwd=cwd or prepare_cwd(tmp_path), @@ -65,7 +65,7 @@ def test_defect_ensure_store_creates_directories(tmp_path: Path) -> None: assert payload["status"] == "ok" assert Path(payload["active"]).is_dir() assert Path(payload["archived"]).is_dir() - assert Path(payload["root"]) == tmp_path / "config" / "defect" + assert Path(payload["root"]) == tmp_path / ".work-bundle" / "defect" assert not (Path.home() / ".work-bundle" / "defect" / "active" / "__pytest_marker__").exists() @@ -163,7 +163,7 @@ def test_defect_create_evidence_rejects_archived_without_action(tmp_path: Path) def test_defect_create_evidence_rejects_status_directory_mismatch(tmp_path: Path) -> None: create_active(tmp_path, "status-mismatch") - path = tmp_path / "config" / "defect" / "active" / f"{evidence_id('status-mismatch')}.yaml" + path = tmp_path / ".work-bundle" / "defect" / "active" / f"{evidence_id('status-mismatch')}.yaml" path.write_text(path.read_text(encoding="utf-8").replace("status: active", "status: archived"), encoding="utf-8") result = run_wb(tmp_path, "defect-build-index") diff --git a/tests/test_work_bundle_defect_migration.py b/tests/test_work_bundle_defect_migration.py index 74d6bf4..37ae9e2 100644 --- a/tests/test_work_bundle_defect_migration.py +++ b/tests/test_work_bundle_defect_migration.py @@ -18,7 +18,7 @@ def run_wb(tmp_path: Path, *args: str) -> subprocess.CompletedProcess[str]: cwd = tmp_path / "cwd" cwd.mkdir(parents=True, exist_ok=True) env = os.environ.copy() - env["WB_CONFIG_ROOT"] = str(tmp_path / "config") + env["HOME"] = str(tmp_path) return subprocess.run( [sys.executable, str(WB), *args], cwd=cwd, @@ -34,7 +34,7 @@ def evidence_id(slug: str) -> str: def write_legacy_record(tmp_path: Path, *, status: str = "active", slug: str = "migration-record") -> tuple[Path, bytes]: - root = tmp_path / "config" / "violation" + root = tmp_path / ".work-bundle" / "violation" (root / "active").mkdir(parents=True, exist_ok=True) (root / "archived").mkdir(parents=True, exist_ok=True) action = "null" if status == "active" else "completed" @@ -67,7 +67,7 @@ def record_fingerprint(root: Path) -> str: def publish_incomplete_destination(tmp_path: Path, *, mismatch: bool = False) -> tuple[Path, Path]: legacy_path, _ = write_legacy_record(tmp_path) legacy = legacy_path.parents[1] - destination = tmp_path / "config" / "defect" + destination = tmp_path / ".work-bundle" / "defect" shutil.copytree(legacy, destination) fingerprint = record_fingerprint(destination) marker = { @@ -89,8 +89,8 @@ def test_defect_migrate_store_preserves_record_bytes_and_rebuilds_index(tmp_path assert result.returncode == 0, result.stdout + result.stderr payload = json.loads(result.stdout) assert payload["migration_status"] == "migrated" - defect = tmp_path / "config" / "defect" - assert not (tmp_path / "config" / "violation").exists() + defect = tmp_path / ".work-bundle" / "defect" + assert not (tmp_path / ".work-bundle" / "violation").exists() assert (defect / "active" / legacy_path.name).read_bytes() == original assert legacy_path.stem in (defect / "index.yaml").read_text(encoding="utf-8") assert not (defect / ".migration-marker.json").exists() @@ -111,12 +111,12 @@ def test_defect_non_migration_command_blocks_before_destination_creation(tmp_pat result = run_wb(case, command, *arguments) assert result.returncode == 1, command assert "defect-migrate-store" in result.stdout - assert not (case / "config" / "defect").exists() + assert not (case / ".work-bundle" / "defect").exists() def test_defect_non_migration_commands_block_on_staging_or_marker(tmp_path: Path) -> None: staging_case = tmp_path / "staging" - staging = staging_case / "config" / ".defect-migration-staging" + staging = staging_case / ".work-bundle" / ".defect-migration-staging" staging.mkdir(parents=True) staging_result = run_wb(staging_case, "defect-ensure-store") assert staging_result.returncode == 1 @@ -125,7 +125,7 @@ def test_defect_non_migration_commands_block_on_staging_or_marker(tmp_path: Path marker_case = tmp_path / "marker" ensured = run_wb(marker_case, "defect-ensure-store") assert ensured.returncode == 0 - marker = marker_case / "config" / "defect" / ".migration-marker.json" + marker = marker_case / ".work-bundle" / "defect" / ".migration-marker.json" marker.write_text("{}", encoding="utf-8") marker_result = run_wb(marker_case, "defect-build-index") assert marker_result.returncode == 1 @@ -134,10 +134,10 @@ def test_defect_non_migration_commands_block_on_staging_or_marker(tmp_path: Path def test_defect_migrate_store_fails_closed_when_both_roots_are_unmarked(tmp_path: Path) -> None: write_legacy_record(tmp_path) - defect = tmp_path / "config" / "defect" + defect = tmp_path / ".work-bundle" / "defect" (defect / "active").mkdir(parents=True) (defect / "archived").mkdir() - config = tmp_path / "config" + config = tmp_path / ".work-bundle" before = sorted(path.relative_to(config).as_posix() for path in config.rglob("*")) result = run_wb(tmp_path, "defect-migrate-store") @@ -152,7 +152,7 @@ def test_defect_migrate_store_is_noop_when_neither_store_exists(tmp_path: Path) assert result.returncode == 0, result.stdout + result.stderr assert json.loads(result.stdout)["migration_status"] == "no-store" - assert not (tmp_path / "config" / "defect").exists() + assert not (tmp_path / ".work-bundle" / "defect").exists() def test_defect_migrate_store_rejects_invalid_legacy_without_destination(tmp_path: Path) -> None: @@ -163,7 +163,7 @@ def test_defect_migrate_store_rejects_invalid_legacy_without_destination(tmp_pat assert result.returncode == 1 assert path.read_text(encoding="utf-8") == "invalid\n" - assert not (tmp_path / "config" / "defect").exists() + assert not (tmp_path / ".work-bundle" / "defect").exists() assert original != path.read_bytes() @@ -199,18 +199,18 @@ def test_defect_migrate_store_finalizes_destination_only_marker(tmp_path: Path) def test_defect_migrate_store_replaces_owned_staging_beside_legacy(tmp_path: Path) -> None: write_legacy_record(tmp_path) - staging = tmp_path / "config" / ".defect-migration-staging" + staging = tmp_path / ".work-bundle" / ".defect-migration-staging" staging.mkdir() (staging / ".staging-owner").write_text("work-bundle:defect-migrate-store:v1\n", encoding="utf-8") (staging / "partial").write_text("partial", encoding="utf-8") result = run_wb(tmp_path, "defect-migrate-store") assert result.returncode == 0, result.stdout + result.stderr assert not staging.exists() - assert (tmp_path / "config" / "defect").exists() + assert (tmp_path / ".work-bundle" / "defect").exists() def test_defect_migrate_store_rejects_unowned_staging(tmp_path: Path) -> None: - staging = tmp_path / "config" / ".defect-migration-staging" + staging = tmp_path / ".work-bundle" / ".defect-migration-staging" staging.mkdir(parents=True) (staging / "user-file").write_text("preserve", encoding="utf-8") result = run_wb(tmp_path, "defect-migrate-store") @@ -221,7 +221,7 @@ def test_defect_migrate_store_rejects_unowned_staging(tmp_path: Path) -> None: def test_defect_migrate_store_is_idempotent_after_success(tmp_path: Path) -> None: write_legacy_record(tmp_path) first = run_wb(tmp_path, "defect-migrate-store") - destination = tmp_path / "config" / "defect" + destination = tmp_path / ".work-bundle" / "defect" before = {path.relative_to(destination).as_posix(): path.read_bytes() for path in destination.rglob("*") if path.is_file()} second = run_wb(tmp_path, "defect-migrate-store") assert first.returncode == 0 @@ -231,7 +231,7 @@ def test_defect_migrate_store_is_idempotent_after_success(tmp_path: Path) -> Non def test_defect_migrate_store_rejects_invalid_destination_only(tmp_path: Path) -> None: - destination = tmp_path / "config" / "defect" + destination = tmp_path / ".work-bundle" / "defect" destination.mkdir(parents=True) sentinel = destination / "preserve" sentinel.write_text("user-state", encoding="utf-8") @@ -258,4 +258,4 @@ def test_legacy_command_fails_with_guidance_without_store_effects(tmp_path: Path assert payload["diagnostic"] == "WB_LEGACY_COMMAND_REMOVED" assert payload["replacement_command"] == replacement assert legacy_path.read_bytes() == original - assert not (case / "config" / "defect").exists() + assert not (case / ".work-bundle" / "defect").exists() diff --git a/tests/test_workspace_discovery.py b/tests/test_workspace_discovery.py index d6b0bf2..a7a328a 100644 --- a/tests/test_workspace_discovery.py +++ b/tests/test_workspace_discovery.py @@ -2,9 +2,14 @@ import argparse import importlib.util +import os +import subprocess import sys from pathlib import Path +import pytest +import yaml + REPO_ROOT = Path(__file__).resolve().parents[1] @@ -31,38 +36,80 @@ def load_module(name: str, path: Path): ) -def write_workspace_metadata(workspace: Path, member: Path) -> None: +def write_workspace_metadata(workspace: Path, member: Path, *, mode: str = "multi-repository") -> None: metadata = workspace / ".work-bundle" / "project.yaml" metadata.parent.mkdir(parents=True) - metadata.write_text( - "\n".join( - [ - "metadata_version: 3", - f"workspace_root: {workspace.resolve()}", - "workspace_mode: multi-repository", - "source_repositories:", - " - id: member-main", - f" project_root: {member.resolve()}", - " origin_id: origin-main", - " checkout_kind: managed-worktree", - " expected_branch: feature/workspace", - " observed_head: abc123", - " baseline_status: current", - "", - ] - ), + binding = {"type": "root"} if mode == "single-repository" else {"type": "member", "name": member.name} + document = { + "metadata_version": 4, + "authority": "canonical", + "workspace": {"id": "wb-discovery", "slug": "demo", "mode": mode}, + "control_plane": {"schema_version": 1, "repository": {"remote": ""}, "sync_policy": {"mode": "manual"}}, + "source_repositories": [{ + "id": "member-main", + "role": "source", + "locator": {"type": "manual", "value": "fixture"}, + "default_branch": "feature/workspace", + "workspace_binding": binding, + "materialization": {"required": True}, + "operation_policy": "inherit", + }], + } + metadata.write_text(yaml.safe_dump(document, sort_keys=False), encoding="utf-8") + config = Path.home() / ".work-bundle" + registry = config / "registry/projects.yaml" + registry.parent.mkdir(parents=True, exist_ok=True) + registry.write_text(yaml.safe_dump({ + "registry_schema_version": 1, + "projects": [{"slug": "demo", "aliases": []}], + "device_bindings": {"wb-discovery": { + "slug": "demo", + "workspace_root": str(workspace.resolve()), + "control_plane_path": str(metadata.parent.resolve()), + "control_plane_remote": "", + "observed_control_plane_head": "", + "repositories": {"member-main": { + "project_root": str(member.resolve()), + "checkout_kind": "manual", + "observed_branch": "", + "observed_head": "", + "observed_at": "2026-09-19T00:00:00Z", + "git_common_dir": "", + }}, + }}, + }, sort_keys=False), encoding="utf-8") + + +@pytest.fixture(autouse=True) +def isolated_config(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + home = tmp_path / "home" + config = home / ".work-bundle" + config.mkdir(parents=True) + (config / "bootstrap.yaml").write_text( + "\n".join([ + "bootstrap_version: v1", + "authority: canonical", + f"work_bundle_root: {REPO_ROOT}", + 'project_registry: "$work_bundle_config_root/registry/projects.yaml"', + 'skill_registry: "$work_bundle_config_root/registry/skill-registry.yaml"', + "", + ]), encoding="utf-8", ) + monkeypatch.setenv("HOME", str(home)) -def test_nested_member_resolves_workspace_and_member_independently(tmp_path: Path) -> None: +def test_nested_member_resolves_workspace_and_member_independently( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: workspace = tmp_path / "workspace" member = workspace / "service-api" deep = member / "src" / "feature" deep.mkdir(parents=True) write_workspace_metadata(workspace, member) - args = argparse.Namespace(workspace_root=None, project_root=str(deep)) + monkeypatch.chdir(deep) + args = argparse.Namespace(workspace_root=None, project_root=None) assert orchestration_core.resolve_workspace_root(args) == workspace.resolve() assert orchestration_core.resolve_member_project_root(args) == member.resolve() @@ -96,44 +143,93 @@ def test_keep_summarizing_uses_workspace_knowledge_from_member_path(tmp_path: Pa args = argparse.Namespace( knowledge_root=None, workspace_root=None, - project_root=str(deep), - cwd=None, + project_root=None, + cwd=str(deep), registry_file=None, ) assert keep_core.resolve_workspace_root(deep) == workspace.resolve() - assert keep_core.resolve_member_project_root(workspace, deep) == member.resolve() assert keep_core.resolve_knowledge_base(args) == ( workspace.resolve() / ".work-bundle" / "knowledge", "work-bundle", ) -def test_single_repository_compatibility_resolves_same_root(tmp_path: Path) -> None: +def test_keep_summarizing_cwd_requires_matching_device_binding(tmp_path: Path) -> None: + workspace = tmp_path / "workspace" + member = workspace / "member" + deep = member / "nested" + deep.mkdir(parents=True) + write_workspace_metadata(workspace, member) + registry = Path.home() / ".work-bundle/registry/projects.yaml" + document = yaml.safe_load(registry.read_text(encoding="utf-8")) + document["device_bindings"] = {} + registry.write_text(yaml.safe_dump(document, sort_keys=False), encoding="utf-8") + args = argparse.Namespace( + knowledge_root=None, + workspace_root=None, + project_root=None, + cwd=str(deep), + registry_file=None, + ) + + with pytest.raises(SystemExit, match="WB_INFRASTRUCTURE_WORKSPACE_BINDING_MISSING"): + keep_core.resolve_knowledge_base(args) + + +def test_keep_summarizing_resolve_and_doctor_use_v4_anchor_join(tmp_path: Path) -> None: + workspace = tmp_path / "workspace" + member = workspace / "member" + deep = member / "nested" + deep.mkdir(parents=True) + write_workspace_metadata(workspace, member) + knowledge = workspace / ".work-bundle/knowledge" + knowledge.mkdir() + knowledge.joinpath("project.yaml").write_text("slug: demo\n", encoding="utf-8") + dispatcher = REPO_ROOT / "scripts/keep-summarizing/dispatcher.py" + env = os.environ.copy() + + resolved = subprocess.run( + [sys.executable, str(dispatcher), "resolve", "--cwd", str(deep)], + env=env, check=False, capture_output=True, text=True, + ) + assert resolved.returncode == 0, resolved.stdout + resolved.stderr + assert resolved.stdout.strip() == "demo" + + healthy = subprocess.run( + [sys.executable, str(dispatcher), "doctor", "--project", "demo", "--cwd", str(deep)], + env=env, check=False, capture_output=True, text=True, + ) + assert healthy.returncode == 0, healthy.stdout + healthy.stderr + assert healthy.stdout.strip() == "ok" + + registry = Path.home() / ".work-bundle/registry/projects.yaml" + document = yaml.safe_load(registry.read_text(encoding="utf-8")) + document["device_bindings"] = {} + registry.write_text(yaml.safe_dump(document, sort_keys=False), encoding="utf-8") + missing = subprocess.run( + [sys.executable, str(dispatcher), "resolve", "--cwd", str(deep)], + env=env, check=False, capture_output=True, text=True, + ) + assert missing.returncode != 0 + assert "WB_INFRASTRUCTURE_WORKSPACE_BINDING_MISSING" in missing.stderr + + +def test_single_repository_compatibility_resolves_same_root( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: project = tmp_path / "single" deep = project / "src" / "nested" deep.mkdir(parents=True) - metadata = project / ".work-bundle" / "project.yaml" - metadata.parent.mkdir(parents=True) - metadata.write_text( - "\n".join( - [ - "metadata_version: 2", - "source_repositories:", - " - id: single-main", - f" path: {project.resolve()}", - "", - ] - ), - encoding="utf-8", - ) + write_workspace_metadata(project, project, mode="single-repository") - args = argparse.Namespace(workspace_root=None, project_root=str(deep)) + monkeypatch.chdir(deep) + args = argparse.Namespace(workspace_root=None, project_root=None) assert orchestration_core.resolve_workspace_root(args) == project.resolve() assert orchestration_core.resolve_member_project_root(args) == project.resolve() -def test_orchestration_registry_fallback_maps_origin_to_workspace( +def test_orchestration_does_not_use_registry_origin_as_workspace_fallback( tmp_path: Path, monkeypatch ) -> None: workspace = tmp_path / "workspace" @@ -142,29 +238,8 @@ def test_orchestration_registry_fallback_maps_origin_to_workspace( member.mkdir(parents=True) origin.mkdir(parents=True) write_workspace_metadata(workspace, member) - config = tmp_path / "config" - registry = config / "registry" / "projects.yaml" - registry.parent.mkdir(parents=True) - registry.write_text( - "\n".join( - [ - "projects:", - " - slug: demo", - f" workspace_root: {workspace.resolve()}", - " repository_origins:", - " - id: origin-main", - f" origin_path: {(tmp_path / 'origin').resolve()}", - "", - ] - ), - encoding="utf-8", - ) - (config / "bootstrap.yaml").write_text( - f"project_registry: {registry.resolve()}\n", - encoding="utf-8", - ) - monkeypatch.setenv("WB_CONFIG_ROOT", str(config)) monkeypatch.chdir(origin) args = argparse.Namespace(workspace_root=None, project_root=None) - assert orchestration_core.resolve_workspace_root(args) == workspace.resolve() + with pytest.raises(SystemExit, match="WB_INFRASTRUCTURE_WORKSPACE_NOT_FOUND"): + orchestration_core.resolve_workspace_root(args) diff --git a/tests/test_workspace_migration.py b/tests/test_workspace_migration.py deleted file mode 100644 index 3908f37..0000000 --- a/tests/test_workspace_migration.py +++ /dev/null @@ -1,519 +0,0 @@ -from __future__ import annotations - -import hashlib -import json -import os -import stat -import subprocess -import sys -from pathlib import Path - -import pytest - -sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'scripts/work-bundle')) - -import migration -from migration import ( - MigrationError, - MigrationTransaction, - TRANSACTION_STAGES, - apply_migration, - inspect_migration, - propose_migration, - retry_transaction, - rollback_owned_paths, - source_git_state, - work_bundle_git_state, -) -from workspace_resources import SCRIPT_INDEX_TEMPLATE - - -def test_reviewer_fixture_preserves_work_bundle_lazy_import_resolution(isolated_reviewer_receipt_store): - # The global reviewer fixture is active before this lazy import chain. - import project - import bootstrap_config - import core - assert Path(core.__file__).resolve().parent == Path(migration.__file__).resolve().parent - assert bootstrap_config.GLOBAL_BOOTSTRAP_FILE_NAME == core.GLOBAL_BOOTSTRAP_FILE_NAME - - -def test_reviewer_runtime_loading_preserves_work_bundle_import_namespace(tmp_path): - script = ''' -import sys -from pathlib import Path -sys.path.insert(0, sys.argv[1]) -import core -import reviewer_workspace -original_path = list(sys.path) -runtime = reviewer_workspace._review_runtime() -artifact = Path(sys.argv[2]) -assert runtime.artifact_review_identity(artifact)["artifact_id"] == "spec-test" -assert sys.path == original_path -import project, bootstrap_config -assert bootstrap_config.GLOBAL_BOOTSTRAP_FILE_NAME == core.GLOBAL_BOOTSTRAP_FILE_NAME -assert "execution_context" not in sys.modules -''' - artifact = tmp_path / "spec.md" - artifact.write_text("---\nid: spec-test\n---\nTest\n") - result = subprocess.run([sys.executable, "-c", script, str(Path(migration.__file__).parent), str(artifact)], capture_output=True, text=True) - assert result.returncode == 0, result.stderr - - -def git(root: Path, *args: str) -> str: - return subprocess.check_output(['git', '-C', str(root), *args], text=True).strip() - - -def seed(root: Path, *, dirty_source: bool = True, dirty_nested: bool = True) -> None: - root.mkdir() - subprocess.run(['git', '-C', str(root), 'init', '-q', '-b', 'main'], check=True) - subprocess.run(['git', '-C', str(root), 'config', 'user.email', 'test@example.com'], check=True) - subprocess.run(['git', '-C', str(root), 'config', 'user.name', 'Test'], check=True) - (root / '.gitignore').write_text('.work-bundle/\nscript/\ncredentials/\nAGENTS.md\n', encoding='utf-8') - (root / 'README.md').write_text('seed\n', encoding='utf-8') - subprocess.run(['git', '-C', str(root), 'add', '.gitignore', 'README.md'], check=True) - subprocess.run(['git', '-C', str(root), 'commit', '-q', '-m', 'seed'], check=True) - - wb = root / '.work-bundle' - (wb / 'orchestration').mkdir(parents=True) - (wb / 'project.yaml').write_text( - 'metadata_version: 2\nauthority: canonical\ncustom_preserved:\n value: yes\n', - encoding='utf-8', - ) - (wb / 'unknown').mkdir() - unknown = wb / 'unknown' / 'maintain.sh' - unknown.write_text('#!/bin/sh\nexit 0\n', encoding='utf-8') - unknown.chmod(0o750) - os.utime(unknown, ns=(1_700_000_000_000_000_000, 1_700_000_000_000_000_000)) - subprocess.run(['git', '-C', str(wb), 'init', '-q', '-b', 'knowledge-main'], check=True) - subprocess.run(['git', '-C', str(wb), 'config', 'user.email', 'test@example.com'], check=True) - subprocess.run(['git', '-C', str(wb), 'config', 'user.name', 'Test'], check=True) - subprocess.run(['git', '-C', str(wb), 'add', '.'], check=True) - subprocess.run(['git', '-C', str(wb), 'commit', '-q', '-m', 'authority'], check=True) - (wb / 'history.txt').write_text('history\n', encoding='utf-8') - subprocess.run(['git', '-C', str(wb), 'add', 'history.txt'], check=True) - subprocess.run(['git', '-C', str(wb), 'commit', '-q', '-m', 'history'], check=True) - (wb / '.cache').mkdir() - (wb / '.cache' / 'ignored.bin').write_bytes(b'cache') - if dirty_nested: - (wb / 'nested-dirty.txt').write_text('accepted dirty state\n', encoding='utf-8') - - (root / 'script').mkdir() - (root / 'script' / 'index.yaml').write_text(SCRIPT_INDEX_TEMPLATE, encoding='utf-8') - (root / 'credentials').mkdir() - synthetic_value = ''.join(('fixture', '-', 'private', '-', 'value')) - (root / 'credentials' / 'credentials.yaml').write_text(synthetic_value, encoding='utf-8') - (root / 'AGENTS.md').write_text('user-authored heading\n', encoding='utf-8') - if dirty_source: - (root / 'README.md').write_text('seed\naccepted dirty state\n', encoding='utf-8') - - -def source_snapshot(root: Path) -> dict[str, object]: - return { - 'repo': source_git_state(root), - 'nested': work_bundle_git_state(root), - 'repo_head': git(root, 'rev-parse', 'HEAD'), - 'nested_head': git(root / '.work-bundle', 'rev-parse', 'HEAD'), - 'unknown': (root / '.work-bundle/unknown/maintain.sh').read_bytes(), - 'unknown_mode': stat.S_IMODE((root / '.work-bundle/unknown/maintain.sh').stat().st_mode), - 'unknown_mtime': (root / '.work-bundle/unknown/maintain.sh').stat().st_mtime_ns, - 'agents': (root / 'AGENTS.md').read_bytes(), - } - - -def registry(path: Path) -> None: - path.parent.mkdir(parents=True) - path.write_text( - 'registry_note: preserve\nprojects:\n - slug: existing\n name: "Existing"\n status: active\n', - encoding='utf-8', - ) - - -def proposal(source: Path, target: Path) -> dict[str, object]: - return propose_migration( - source, - target, - 'repo-one', - 'feature/workspace', - 'HEAD', - workspace_slug='workspace-one', - repository_name='Repository One', - additional_repository_origins=[{ - 'id': 'repo-two', - 'origin_path': '/non-sensitive/origin-two', - 'remote': '', - 'git_repository': True, - }], - ) - - -def apply(source: Path, target: Path, registry_path: Path, *, fail_stage: str | None = None) -> dict[str, object]: - dry_run = proposal(source, target) - return apply_migration( - source, - target, - 'repo-one', - 'feature/workspace', - 'HEAD', - workspace_slug='workspace-one', - repository_name='Repository One', - additional_repository_origins=[{ - 'id': 'repo-two', - 'origin_path': '/non-sensitive/origin-two', - 'remote': '', - 'git_repository': True, - }], - accepted_baseline_id=str(dry_run['accepted_baseline_evidence']['id']), - registry_path=registry_path, - fail_stage=fail_stage, - ) - - -def test_proposal_reports_complete_inputs_and_separate_dirty_states(tmp_path: Path) -> None: - source, target = tmp_path / 'source', tmp_path / 'target' - seed(source) - inspection = inspect_migration(source, target) - dry_run = proposal(source, target) - assert inspection['source_repository_git']['dirty'] is True - assert inspection['work_bundle_git']['dirty'] is True - assert dry_run['workspace_slug'] == 'workspace-one' - assert dry_run['repository_name'] == 'Repository One' - assert dry_run['working_branch'] == 'feature/workspace' - assert dry_run['base_ref'] == 'HEAD' - assert dry_run['additional_repository_origins'][0]['id'] == 'repo-two' - assert dry_run['apply_requires_accepted_baseline'] is True - assert len(dry_run['accepted_baseline_evidence']['id']) == 64 - assert dry_run['changed_files'] == [] and not target.exists() - - -def test_proposal_rejects_working_branch_checked_out_in_origin_common_dir(tmp_path: Path) -> None: - source, target, occupied = tmp_path / 'source', tmp_path / 'target', tmp_path / 'occupied' - seed(source, dirty_source=False, dirty_nested=False) - subprocess.run( - ['git', '-C', str(source), 'worktree', 'add', '-q', '-b', 'feature/occupied', str(occupied), 'HEAD'], - check=True, - ) - - with pytest.raises(MigrationError) as raised: - propose_migration(source, target, 'repo-one', 'feature/occupied', 'HEAD') - - assert raised.value.code == 'WB_WORKTREE_BRANCH_CONFLICT' - assert raised.value.result['changed_files'] == [] - assert raised.value.result['working_branch'] == 'feature/occupied' - assert not target.exists() - - -def test_proposal_distinguishes_missing_origin_main_from_local_main(tmp_path: Path) -> None: - source, target = tmp_path / 'source', tmp_path / 'target' - seed(source, dirty_source=False, dirty_nested=False) - - with pytest.raises(MigrationError) as raised: - propose_migration(source, target, 'repo-one', 'feature/workspace', 'origin/main') - - assert raised.value.code == 'WB_MIGRATION_LOCAL_ORIGIN_BASE_REF_UNAVAILABLE' - assert raised.value.result['changed_files'] == [] - assert raised.value.result['base_ref'] == 'origin/main' - assert raised.value.result['local_branch_available'] is True - assert not target.exists() - - valid = propose_migration(source, target, 'repo-one', 'feature/workspace', 'main') - assert valid['changed_files'] == [] - assert not target.exists() - - -def test_invalid_proposal_prevents_apply_from_reaching_member_provisioning( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - source, target = tmp_path / 'source', tmp_path / 'target' - seed(source, dirty_source=False, dirty_nested=False) - monkeypatch.setattr( - migration, - 'provision_member', - lambda *_args, **_kwargs: pytest.fail('provision_member must not run'), - ) - - with pytest.raises(MigrationError, match='LOCAL_ORIGIN_BASE_REF_UNAVAILABLE'): - apply_migration(source, target, 'repo-one', 'feature/workspace', 'origin/main') - - assert not target.exists() - - -def test_dirty_apply_requires_exact_accepted_baseline(tmp_path: Path) -> None: - source, target, registry_path = tmp_path / 'source', tmp_path / 'target', tmp_path / 'config/projects.yaml' - seed(source) - registry(registry_path) - with pytest.raises(MigrationError, match='ACCEPTED_BASELINE_REQUIRED'): - apply_migration( - source, target, 'repo-one', 'feature/workspace', registry_path=registry_path, - workspace_slug='workspace-one', repository_name='Repository One', - ) - assert not target.exists() - assert 'workspace-one' not in registry_path.read_text(encoding='utf-8') - - -def test_apply_preserves_source_and_publishes_v3_and_locator(tmp_path: Path) -> None: - source, target, registry_path = tmp_path / 'source', tmp_path / 'target', tmp_path / 'config/projects.yaml' - seed(source) - registry(registry_path) - before = source_snapshot(source) - result = apply(source, target, registry_path) - assert result['status'] == 'published' - assert result['metadata_and_registry_status'] == {'metadata': 'published', 'registry': 'published'} - historical_identity = hashlib.sha256( - f'{source.resolve()}:{target.resolve()}:workspace-one:repo-one'.encode('utf-8') - ).hexdigest()[:20] - assert result['transaction']['id'] == historical_identity - assert source_snapshot(source) == before - metadata = (target / '.work-bundle/project.yaml').read_text(encoding='utf-8') - assert 'metadata_version: 3' in metadata - assert 'workspace_mode: multi-repository' in metadata - assert 'checkout_kind: managed-worktree' in metadata - assert 'git_control_scope: workspace' in metadata - assert 'custom_preserved:' in metadata - registry_text = registry_path.read_text(encoding='utf-8') - assert 'registry_note: preserve' in registry_text - assert ' - slug: existing' in registry_text - assert ' - slug: workspace-one' in registry_text - assert 'repository_origins:' in registry_text and 'repo-two' in registry_text - member = Path(str(result['member']['project_root'])) - common = Path(git(member, 'rev-parse', '--path-format=absolute', '--git-common-dir')).resolve() - assert target.resolve() in common.parents - assert target.resolve() in member.resolve().parents - assert (target / '.work-bundle/unknown/maintain.sh').read_bytes() == before['unknown'] - assert stat.S_IMODE((target / '.work-bundle/unknown/maintain.sh').stat().st_mode) == before['unknown_mode'] - assert (target / '.work-bundle/unknown/maintain.sh').stat().st_mtime_ns == before['unknown_mtime'] - assert git(target / '.work-bundle', 'rev-list', '--count', 'HEAD') == '2' - assert not (target / '.work-bundle/.cache').exists() - agents_text = (target / 'AGENTS.md').read_text(encoding='utf-8') - assert agents_text.startswith(before['agents'].decode('utf-8')) - assert '# Work Bundle RULE START' in agents_text - credential = target / 'credentials/credentials.yaml' - assert credential.read_text(encoding='utf-8') == 'version: 1\ncredentials: []\n' - assert stat.S_IMODE(credential.parent.stat().st_mode) == 0o700 - assert stat.S_IMODE(credential.stat().st_mode) == 0o600 - assert result['script_index_validation'] == 'passed' - assert (target / '.gitignore').is_file() - assert (target / 'roles/solution-architect.yaml').is_file() - assert 'credentials/' in (target / '.work-bundle/.gitignore').read_text(encoding='utf-8') - assert 'git/' in (target / '.work-bundle/.gitignore').read_text(encoding='utf-8') - record = json.loads(Path(str(result['transaction_record'])).read_text(encoding='utf-8')) - assert record['state'] == 'published' - assert record['source_preserved'] is True - - -def test_non_git_authority_can_provision_from_explicit_external_origin(tmp_path: Path) -> None: - source, origin, target = tmp_path / 'authority', tmp_path / 'origin', tmp_path / 'target' - registry_path = tmp_path / 'config/projects.yaml' - seed(origin, dirty_source=True, dirty_nested=False) - source.mkdir() - (source / '.work-bundle').mkdir() - (source / '.work-bundle/project.yaml').write_text( - 'metadata_version: 2\nauthority: canonical\n', encoding='utf-8' - ) - (source / 'script').mkdir() - (source / 'script/index.yaml').write_text(SCRIPT_INDEX_TEMPLATE, encoding='utf-8') - (source / 'script/legacy.py').write_text('raise SystemExit("must not migrate")\n', encoding='utf-8') - (source / 'AGENTS.md').write_text('legacy authority\n', encoding='utf-8') - registry(registry_path) - dry_run = propose_migration( - source, target, 'repo-one', 'feature/workspace', 'HEAD', origin=origin, - workspace_slug='workspace-one', repository_name='Repository One', - ) - result = apply_migration( - source, target, 'repo-one', 'feature/workspace', 'HEAD', origin=origin, - workspace_slug='workspace-one', repository_name='Repository One', - accepted_baseline_id=str(dry_run['accepted_baseline_evidence']['id']), - registry_path=registry_path, - ) - assert result['status'] == 'published' - assert result['member_origin_git']['dirty'] is True - assert result['member']['project_root'] == str((target / 'repo-one').resolve()) - assert not (target / 'script/legacy.py').exists() - assert (source / 'script/legacy.py').is_file() - assert 'script' in result['skipped_sensitive_and_transient_paths'] - assert 'script' not in result['copied_inventory_and_digests'] - assert git(target / 'repo-one', 'rev-parse', 'HEAD') == git(origin, 'rev-parse', 'HEAD') - registry_text = registry_path.read_text(encoding='utf-8') - assert f'origin_path: "{origin.resolve()}"' in registry_text - - -@pytest.mark.parametrize('stage', TRANSACTION_STAGES) -def test_each_stage_failure_preserves_source_registry_and_recovery(tmp_path: Path, stage: str) -> None: - source = tmp_path / f'source-{stage}' - target = tmp_path / f'target-{stage}' - registry_path = tmp_path / f'config-{stage}/projects.yaml' - seed(source) - registry(registry_path) - source_before = source_snapshot(source) - registry_before = registry_path.read_bytes() - with pytest.raises(MigrationError) as raised: - apply(source, target, registry_path, fail_stage=stage) - assert source_snapshot(source) == source_before - assert registry_path.read_bytes() == registry_before - assert not target.exists() - record_path = raised.value.transaction_record - assert record_path is not None and record_path.is_file() - record = json.loads(record_path.read_text(encoding='utf-8')) - assert record['state'] == 'failed' - assert record['registry_status'] == 'unchanged' - assert record['source_preserved'] is True - synthetic_value = ''.join(('fixture', '-', 'private', '-', 'value')) - assert synthetic_value not in json.dumps(record) - - -def test_failed_transaction_retries_and_published_retry_is_idempotent(tmp_path: Path) -> None: - source, target, registry_path = tmp_path / 'source', tmp_path / 'target', tmp_path / 'config/projects.yaml' - seed(source) - registry(registry_path) - dry_run = proposal(source, target) - options = { - 'workspace_slug': 'workspace-one', - 'repository_name': 'Repository One', - 'accepted_baseline_id': dry_run['accepted_baseline_evidence']['id'], - 'registry_path': registry_path, - } - with pytest.raises(MigrationError): - apply_migration( - source, target, 'repo-one', 'feature/workspace', fail_stage='member-provision', **options - ) - repaired = retry_transaction(source, target, 'repo-one', 'feature/workspace', **options) - assert repaired['status'] == 'published' - registry_after = registry_path.read_bytes() - metadata_path = target / '.work-bundle/project.yaml' - metadata_after = metadata_path.read_bytes() - recovery_path = Path(str(repaired['transaction_record'])) - recovery_after = recovery_path.read_bytes() - recovery_mtime = recovery_path.stat().st_mtime_ns - idempotent = retry_transaction(source, target, 'repo-one', 'feature/workspace', **options) - assert idempotent['status'] == 'published' and idempotent['idempotent'] is True - required = { - 'copied_inventory_and_digests', - 'skipped_sensitive_and_transient_paths', - 'script_index_validation', - 'agents_merge_status', - 'member', - 'source_preservation_checks', - 'validation_results', - 'retry_or_rollback_instructions', - 'source_repository_git', - 'work_bundle_git', - 'accepted_baseline_id', - } - assert required <= idempotent.keys() - assert idempotent['transaction']['id'] == repaired['transaction']['id'] - assert idempotent['transaction']['context'] == repaired['transaction']['context'] - assert idempotent['member'] == repaired['member'] - assert idempotent['validation_results'] == repaired['validation_results'] - assert registry_path.read_bytes() == registry_after - assert metadata_path.read_bytes() == metadata_after - assert recovery_path.read_bytes() == recovery_after - assert recovery_path.stat().st_mtime_ns == recovery_mtime - - -def test_rollback_is_owned_path_only_and_idempotent(tmp_path: Path) -> None: - target = tmp_path / 'target' - owned = target / 'owned' - unrelated = target / 'unrelated.txt' - owned.mkdir(parents=True) - unrelated.write_text('preserve\n', encoding='utf-8') - transaction = MigrationTransaction(target, 'owned-only') - transaction.own(owned) - first = rollback_owned_paths(transaction) - second = rollback_owned_paths(transaction) - assert first['state'] == 'rolled-back' and second['state'] == 'rolled-back' - assert unrelated.read_text(encoding='utf-8') == 'preserve\n' - assert transaction.recovery_path.is_file() - - -def test_unsafe_symlink_and_collision_fail_without_publication(tmp_path: Path) -> None: - source, target, registry_path = tmp_path / 'source', tmp_path / 'target', tmp_path / 'config/projects.yaml' - seed(source) - registry(registry_path) - (source / '.work-bundle/link').symlink_to(source / 'README.md') - registry_before = registry_path.read_bytes() - with pytest.raises(MigrationError, match='UNSAFE_SYMLINK'): - apply(source, target, registry_path) - assert not target.exists() and registry_path.read_bytes() == registry_before - (source / '.work-bundle/link').unlink() - target.mkdir() - (target / 'user-file').write_text('preserve\n', encoding='utf-8') - with pytest.raises(MigrationError, match='TARGET_NOT_EMPTY'): - apply(source, target, registry_path) - assert (target / 'user-file').read_text(encoding='utf-8') == 'preserve\n' - - -def test_failed_apply_preserves_preexisting_empty_target_root(tmp_path: Path) -> None: - source, target, registry_path = tmp_path / 'source', tmp_path / 'target', tmp_path / 'config/projects.yaml' - seed(source) - target.mkdir() - registry(registry_path) - with pytest.raises(MigrationError): - apply(source, target, registry_path, fail_stage='workspace-resources') - assert target.is_dir() - assert list(target.iterdir()) == [] - - -def test_failure_evidence_carries_member_git_verification_and_publication_identities(tmp_path: Path) -> None: - source, target, registry_path = tmp_path / 'source', tmp_path / 'target', tmp_path / 'config/projects.yaml' - seed(source) - registry(registry_path) - with pytest.raises(MigrationError) as raised: - apply(source, target, registry_path, fail_stage='metadata-publication') - record = json.loads(raised.value.transaction_record.read_text(encoding='utf-8')) - context = record['context'] - assert context['member']['lifecycle_state'] == 'verified' - assert context['member']['observed_git']['branch'] == 'feature/workspace' - assert context['member']['observed_git']['head'] - assert context['member']['verification']['passed'] is True - assert context['member']['verification']['target_validation_passed'] is True - assert context['metadata_identity']['old']['version'] == '2' - assert context['metadata_identity']['new'] == { - 'version': 3, - 'workspace_root': str(target.resolve()), - 'workspace_mode': 'multi-repository', - } - assert context['registry_identity']['old']['published'] is False - assert context['registry_identity']['new']['status'] == 'active' - assert context['publication']['metadata_before'] - assert context['publication']['registry_after'] - - -def test_final_verification_precedes_all_publication(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - source, target, registry_path = tmp_path / 'source', tmp_path / 'target', tmp_path / 'config/projects.yaml' - seed(source) - registry(registry_path) - registry_before = registry_path.read_bytes() - writes: list[Path] = [] - original = migration._atomic_write - - def recording_write(path: Path, payload: bytes) -> None: - writes.append(path.resolve()) - original(path, payload) - - monkeypatch.setattr(migration, '_atomic_write', recording_write) - with pytest.raises(MigrationError, match='FINAL_VERIFICATION'): - apply(source, target, registry_path, fail_stage='final-verification') - assert registry_path.resolve() not in writes - assert registry_path.read_bytes() == registry_before - assert not target.exists() - - -def test_success_result_has_discovery_preflight_recovery_and_baseline_contract(tmp_path: Path) -> None: - source, target, registry_path = tmp_path / 'source', tmp_path / 'target', tmp_path / 'config/projects.yaml' - seed(source) - registry(registry_path) - result = apply(source, target, registry_path) - validations = result['validation_results'] - assert validations['passed'] is True - assert validations['session_start_discovery'] == { - 'passed': True, - 'workspace_root': str(target.resolve()), - 'member_root': str((target / 'repo-one').resolve()), - } - assert validations['member_preflight']['passed'] is True - assert validations['member_preflight']['branch_status'] == 'matched' - assert validations['source_preservation']['passed'] is True - assert result['source_repository_git']['dirty'] is True - assert result['work_bundle_git']['dirty'] is True - assert result['accepted_baseline_id'] == proposal(source, target)['accepted_baseline_evidence']['id'] - assert 'same accepted_baseline_id' in result['retry_or_rollback_instructions']['retry'] - assert 'transaction-owned target paths only' in result['retry_or_rollback_instructions']['rollback']