From 4d8f575f2a3f9e6e988516808c9d86bbdc75ea1f Mon Sep 17 00:00:00 2001 From: Bo Wu Date: Sat, 5 Sep 2026 18:38:44 -0700 Subject: [PATCH] Simplify pre-implementation plan alignment --- docs/architecture.md | 26 +- docs/architecture/system-architecture.md | 26 +- docs/getting-started.md | 125 +----- evaluation/README.md | 2 +- prompts/playbooks/agent-spawning.md | 3 +- prompts/playbooks/implementation-lifecycle.md | 61 +-- prompts/roles/decision-authority-reviewer.md | 133 ------ prompts/roles/plan-alignment-reviewer.md | 68 +++ runtime/src/runtime.rs | 406 +++++------------ runtime/src/subagent.rs | 29 +- runtime/src/supervisor.rs | 136 +++--- runtime/src/workflow.rs | 412 ++++++------------ tests/lifecycle.sh | 113 +++-- tests/malicious-orchestrator.sh | 100 ++--- tests/mock_orchestration_e2e.sh | 56 ++- tests/run.sh | 14 +- tests/test_migration_contracts.py | 7 +- tests/test_swe_outcomes.py | 37 +- 18 files changed, 620 insertions(+), 1134 deletions(-) delete mode 100644 prompts/roles/decision-authority-reviewer.md create mode 100644 prompts/roles/plan-alignment-reviewer.md diff --git a/docs/architecture.md b/docs/architecture.md index 8470074..a56ed59 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -18,11 +18,14 @@ flowchart TD U["Original task"] --> O["Orchestrator"] O --> SC["Contract scout"] SC --> C["Registered contract"] - C --> AR["Decision-authority review"] - AR --> P["Approved implementation context"] + O --> IP["Complete sealed iteration plan"] + U --> AR["Plan-alignment review"] + C --> AR + IP --> AR + AR --> P["Aligned implementation context"] P --> W["Path-scoped writer"] W --> D["Canonical Git diff"] - D --> R["Scope, technical, drift, and reflection reviews"] + D --> R["Technical and applicable scope/reflection reviews"] R --> F{"Open finding or todo?"} F -- yes --> P F -- no --> G["Supervisor completion gates"] @@ -124,13 +127,14 @@ pre-implementation -> implementation -> post-implementation -> complete ### Pre-implementation 1. The original task is stored immutably and hashed. -2. A read-only contract scout emits structured `must` and `must-not` rules. +2. When needed, a read-only contract scout emits structured `must` and + `must-not` rules. 3. The supervisor seals and registers the scout artifact. -4. A decision record selects a plan. -5. An independent authority reviewer receives the original task, exact contract, - and implementation context. -6. The writer gate opens only when the decision, plan, context, authority review, - and contract hashes agree. +4. The orchestrator submits one complete iteration plan. +5. An independent plan-alignment reviewer compares the complete sealed plan + directly with the original task and any registered contract. +6. The writer gate opens only when the review reports `aligned` and its + original-task, plan, context, and contract hashes agree. ### Implementation @@ -141,8 +145,8 @@ in parallel. ### Post-implementation -The control plane computes one canonical diff hash. Scope, technical, -decision-drift, and reflection reviewers receive: +The control plane computes one canonical diff hash. A technical reviewer and +any applicable scope or reflection reviewers receive: - the immutable original task; - the exact registered contract artifact; diff --git a/docs/architecture/system-architecture.md b/docs/architecture/system-architecture.md index 7e43535..3db9ea4 100644 --- a/docs/architecture/system-architecture.md +++ b/docs/architecture/system-architecture.md @@ -474,7 +474,7 @@ the authorized contract, the system uses a Simplex-style fallback: it issues no next operation permit, persists a supervisor-verified human-review request, ends the session in `human-review-required` state, and asks the user one bounded question. This is the same terminal authority pattern used when a -decision-authority review detects a user-owned scope or risk choice. A later +plan-alignment review finds that required input is absent. A later user answer starts a new session; model prose alone cannot clear the pending human boundary in the completed session. @@ -766,11 +766,15 @@ role instructions, and the immutable artifacts needed for their review. They also have read access to the session trace corpus and the supervisor-mediated, read-only `prod-mcp` evidence path defined in AD-006, so evidence selection by another agent is not a trust boundary. -Before implementation, the supervisor generates an immutable decision capsule -containing the workflow revision, committed decision, selected alternative, -original-task digest, and contract digest. Decision-authority evidence and the -implementation permit must bind to the same capsule digest; an orchestrator -summary cannot substitute for that binding. +Before implementation, one independent reviewer compares the authenticated +original request directly with the complete sealed iteration plan. The only +semantic outcomes are `aligned` and `misaligned`: aligned plans may proceed; +misaligned plans may not. A misaligned result returns to the orchestrator for a +new plan unless the reviewer identifies genuinely missing user input, in which +case the supervisor asks one bounded question. Review evidence binds directly +to both the original-task digest and sealed-plan digest. This binding prevents +input substitution but does not create a formal contract or a separate +authority decision. Provider and model selection remain deployment-owned. The orchestrator chooses roles and dependencies, not provider credentials, model names, or prices. @@ -811,11 +815,11 @@ immutable session grants, assignment ownership, diff binding, and the supervisor completion gate enforce these transitions. For source implementation, adaptivity happens at iteration boundaries. The -orchestrator submits one complete iteration plan containing the committed -decision, worker dependency graph, bounded ownership, and any additional -review requests. The supervisor records the plan digest, adds review -obligations derived from policy and persisted artifacts, and binds the -decision-authority capsule to that digest. Once sealed, the runtime—not the +orchestrator submits one complete iteration plan containing the implementation +context, worker dependency graph, bounded ownership, and any additional review +requests. The supervisor records the plan digest, obtains the binary alignment +review described above, and adds review obligations derived from policy and +persisted artifacts. Once sealed, the runtime—not the orchestrator—advances ready nodes, launches mutually independent agents, waits, finalizes durable evidence, freezes the candidate diff, and submits lifecycle transitions for that iteration. The runtime may report `needs_replan`, but it diff --git a/docs/getting-started.md b/docs/getting-started.md index 480a51e..9c965e1 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -151,35 +151,7 @@ project without copying prompt modules into it. The orchestrator normally performs the commands in this section. Operators use them for inspection or deliberate manual recovery. -### 1. Record the Decision - -Create a decision, record alternatives, and commit one plan: - -```bash -multiagent decision init DEC-001 --title "Choose the implementation" - -multiagent decision add-alternative DEC-001 \ - --plan-id PLAN-A \ - --summary "Small compatible change" \ - --proposed-by contract-scout-01 \ - --expected-outcome "Preserve behavior with minimal scope" - -multiagent decision add-assumption DEC-001 \ - --assumption-id ASSUME-1 \ - --statement "The public interface remains stable" \ - --validation-method "source and test inspection" - -multiagent decision commit DEC-001 \ - --selected-plan PLAN-A \ - --reason "Matches the registered contract" - -multiagent decision list -multiagent decision show DEC-001 -``` - -Decision records are durable under `$MULTIAGENT_STATE_DIR/decisions`. - -### 2. Register the Contract +### 1. Register a Contract When Needed For tasks with API, compatibility, security, benchmark, or hidden-contract risk, spawn a read-only scout: @@ -199,83 +171,30 @@ multiagent workflow contract-register "$MULTIAGENT_WORKFLOW_ID" \ The supervisor seals the scout result and records its hash. Later workers and reviewers receive the immutable original task and exact registered artifact. -### 3. Open the Implementation Gate +### 2. Execute One Sealed Iteration -After an independent decision-authority review passes, bind the approved -implementation context: +Write the complete `IterationPlan` JSON described in +`prompts/playbooks/implementation-lifecycle.md` under +`$MULTIAGENT_STATE_DIR`, then make one blocking call: ```bash -multiagent workflow prepare-implementation "$MULTIAGENT_WORKFLOW_ID" \ - --decision-id DEC-001 \ - --plan-id PLAN-A \ - --decision-revision 1 \ - --implementation-context /absolute/path/to/implementation-context.md \ - --authority-review review-01-authority - -multiagent workflow transition "$MULTIAGENT_WORKFLOW_ID" implementation +multiagent subagent execute-iteration \ + --plan-file "$MULTIAGENT_STATE_DIR/iteration-1.json" \ + --timeout 900 ``` -The context must contain the exact registered contract. A plan that contradicts -a registered `must-not` rule is rejected before a writer starts. - -### 4. Assign and Run a Worker +The runtime seals the plan, launches one read-only plan-alignment reviewer, +and compares the complete plan directly with the authenticated original task. +An aligned plan proceeds to bounded workers and post-implementation technical +review. A misaligned plan returns `needs_replan`; it asks the user only when +the reviewer identifies genuinely missing input. -Create metadata before spawning a writer: - -```bash -multiagent subagent assignment-create worker-01 \ - --assignment-id IMPL-001 \ - --role exploitation \ - --decision-id DEC-001 \ - --plan-id PLAN-A \ - --branch "$(git -C "$MULTIAGENT_ROOT" branch --show-current)" \ - --owned src/,tests/ - -SUBAGENT_CLI="${WORKER_CLI:-claude}" \ -multiagent subagent spawn worker-01 \ - --role worker \ - --own src/,tests/ \ - --assignment-id IMPL-001 \ - --workflow-id "$MULTIAGENT_WORKFLOW_ID" \ - --decision-id DEC-001 \ - --plan-id PLAN-A \ - --instruction-file /absolute/path/to/worker-instruction.md +The supervisor binds alignment evidence to the original-task and sealed-plan +digests, enforces worker path ownership, freezes the candidate diff, and runs +all applicable post-implementation reviews. The orchestrator must not replay +those internal transitions around the executor. -multiagent subagent wait worker-01 --timeout 1800 -multiagent subagent assignment-check worker-01 -``` - -Only the supervisor-authorized writer receives temporary access to its existing -owned paths. The global writer lease prevents a second writer from becoming -active at the same time. - -Update a durable checkpoint during long work: - -```bash -multiagent subagent checkpoint-update worker-01 \ - --step "implementation complete; focused tests running" \ - --idempotency "rerun focused tests before acceptance" \ - --status running -``` - -### 5. Review the Canonical Diff - -Freeze the current repository state: - -```bash -multiagent snapshot --root "$MULTIAGENT_ROOT" --base HEAD --format json -``` - -Transition to post-implementation with the reported hash, then run read-only -scope, technical, decision-drift, and reflection reviews. Review instructions -must include the original task, registered contract, approved context, and -canonical diff. Finalize each reviewer so the supervisor can seal its output. - -Record review findings and todos through `multiagent workflow` and -`multiagent subagent` commands. A changed diff invalidates previous acceptance. -An open finding returns the workflow to another pre-implementation iteration. - -### 6. Complete Atomically +### 3. Inspect Completion Inspect both gates: @@ -284,11 +203,9 @@ multiagent workflow completion-check "$MULTIAGENT_WORKFLOW_ID" multiagent subagent gate-check ``` -Request completion: - -```bash -multiagent orchestrator complete -``` +When `execute-iteration` reports `status=completed`, it has already requested +supervisor completion. A `needs_replan` result leaves durable findings for the +next iteration; do not mutate the sealed plan in place. The orchestrator cannot directly write `complete`. The supervisor runs the lifecycle and technical gates under the lifecycle lock and changes the phase diff --git a/evaluation/README.md b/evaluation/README.md index fb9f431..67f7f25 100644 --- a/evaluation/README.md +++ b/evaluation/README.md @@ -163,7 +163,7 @@ python3 -m evaluation.cli \ `baseline` is one ordinary Codex CLI invocation. `multiagent` runs the current production Rust/tmux lifecycle in Linux, including its contract scout, -authority reviewers, workers, verifiers, and final reviews. Build the exact +plan-alignment reviewers, workers, verifiers, and final reviews. Build the exact checkout before a live multiagent comparison: ```bash diff --git a/prompts/playbooks/agent-spawning.md b/prompts/playbooks/agent-spawning.md index 347a5c4..26a615b 100644 --- a/prompts/playbooks/agent-spawning.md +++ b/prompts/playbooks/agent-spawning.md @@ -6,8 +6,7 @@ replace, verify, or finalize worker windows or named subagents. ## Read-Only Reader Spawn Readers are investigation roles, not implementation assignments. Spawn them -without `--own`, `--assignment-id`, `--decision-id`, `--plan-id`, or -`--decision-revision`: +without `--own`, `--assignment-id`, `--decision-id`, or `--plan-id`: ```bash SUBAGENT_CLI="$VERIFIER_CLI" multiagent subagent spawn reader-01-question \ diff --git a/prompts/playbooks/implementation-lifecycle.md b/prompts/playbooks/implementation-lifecycle.md index 9cd4f14..8d560a9 100644 --- a/prompts/playbooks/implementation-lifecycle.md +++ b/prompts/playbooks/implementation-lifecycle.md @@ -6,7 +6,7 @@ use the selected Markdown runbook and reviewed ops path instead. Do not use this lifecycle for an external-only task that does not modify repository source. Such work uses `prompts/playbooks/reviewed-ops-cycle.md`. -## Iteration Contract +## Iteration Plan Adapt the role graph at the beginning of an iteration, then let the runtime execute that sealed graph. The orchestrator makes semantic choices between @@ -18,9 +18,9 @@ Normal lifecycle phases remain: pre-implementation -> implementation -> post-implementation -> complete post-implementation -> pre-implementation when a repair TODO remains -A substantive finding, changed assumption, expanded scope, risk change, or -needed user choice ends the current iteration. It must produce a newly reviewed -plan; never mutate a sealed plan in place. +A substantive alignment finding, changed assumption, expanded scope, risk +change, or needed user input ends the current iteration. It must produce a newly +reviewed plan; never mutate a sealed plan in place. ## Build One Complete Plan @@ -28,8 +28,9 @@ Read the authenticated task once through `multiagent workflow context`. Choose the smallest worker dependency graph that can satisfy it. Skip a contract scout when the task already gives an exact bounded artifact schema and values. Add a scout only when an unknown can materially change the plan. -The explicit task contract is already approved; ask the user only when -materially different outcomes remain consistent with it. +The original user request is authoritative. Ask the user only when required +information is genuinely absent or materially different outcomes remain +consistent with the complete request and available evidence. Write one UTF-8 JSON plan under `MULTIAGENT_STATE_DIR`, using exactly this schema: @@ -40,22 +41,7 @@ schema: "kind": "IterationPlan", "workflowId": "WORKFLOW_ID", "iteration": 1, - "decision": { - "id": "DECISION_ID", - "title": "single-line title", - "selectedPlan": "PLAN_ID", - "reason": "single-line reason", - "rollbackPolicy": "single-line rollback condition", - "alternatives": [ - { - "id": "PLAN_ID", - "summary": "single-line bounded plan", - "expectedOutcome": "single-line exact outcome", - "risk": "single-line residual risk" - } - ] - }, - "implementationContext": "Complete goal, authority basis, constraints, exact target paths, ownership, prohibitions, acceptance criteria, and unresolved risks.", + "implementationContext": "Complete goal, constraints, exact target paths, ownership, prohibitions, acceptance criteria, and unresolved risks.", "workers": [ { "id": "worker-primary-01", @@ -71,21 +57,19 @@ schema: Use `worker-ops-plan-01` for a bounded repository artifact whose deliverable is an operations plan so the launcher selects the focused planning role. Use -ordinary `worker-*` identities for other source work. Include all genuinely -material alternatives; do not add a fake alternative merely to populate the -decision ledger. Worker ownership must be non-overlapping. Dependencies name -other worker IDs. Put only `decision-drift`, `scope`, or `reflection` in -`additionalReviews`, and only when that extra review can affect acceptance. +ordinary `worker-*` identities for other source work. Worker ownership must be +non-overlapping. Dependencies name other worker IDs. Put only `scope` or +`reflection` in `additionalReviews`, and only when that extra review can affect +acceptance. On a repair iteration, `resolvesTodos` must exactly list every active direct TODO that the sealed worker graph will address. Resolve evidence or decision TODOs before submitting the plan; the runtime marks declared direct TODOs complete only after the candidate passes every supervisor review. -The supervisor always requires an independent decision-authority review and a -technical review for a produced diff. It also derives decision-drift review -when the committed decision contains multiple alternatives or assumptions, and -may add other obligations from persisted artifacts. The orchestrator may add -review but cannot remove supervisor obligations. +The supervisor always requires one independent plan-alignment review before +implementation and one technical review for a produced diff. It may add other +obligations from persisted artifacts. The orchestrator may add review but +cannot remove supervisor obligations. ## Execute the Sealed Iteration @@ -93,17 +77,18 @@ Make one blocking runtime call after the plan file is complete: multiagent subagent execute-iteration --plan-file PLAN_PATH --timeout 900 -The runtime validates and records the plan digest, materializes the committed -decision, launches exactly one digest-bound decision-authority reviewer, and -stops on authority findings. After acceptance it prepares the complete -implementation context, schedules ready worker nodes, waits and finalizes them, +The runtime validates and records the plan digest, launches exactly one +digest-bound plan-alignment reviewer, and stops on misalignment. After alignment +it prepares the complete implementation context, schedules ready worker nodes, +waits and finalizes them, checks the candidate against the union of owned paths, freezes the diff, asks the supervisor for review obligations, launches independent reviewers in parallel, records their structured evidence, and requests supervisor completion. Do not duplicate any of these commands around the executor. -The authority capsule is supervisor-generated and includes the sealed plan -digest. Neither the orchestrator nor a worker may manufacture or edit it. +The review is mechanically bound to both the sealed plan digest and the +authenticated original-task digest. This binding proves which inputs were +reviewed; it is not a substitute contract or a separate authority decision. Reviewer access remains mechanically read-only, and worker writes remain bounded by assignments. diff --git a/prompts/roles/decision-authority-reviewer.md b/prompts/roles/decision-authority-reviewer.md deleted file mode 100644 index df7e6ef..0000000 --- a/prompts/roles/decision-authority-reviewer.md +++ /dev/null @@ -1,133 +0,0 @@ -# Decision Authority Reviewer - -You are an independent read-only governance reviewer. You do not implement, -select a user-owned option, edit decision records, approve skips, or coordinate -workers. - -## Mandatory Output Protocol - -Your final response is parsed by the workflow runtime. The first non-empty line -must be exactly one of: - -- `verdict: orchestrator-may-decide` -- `verdict: user-choice-required` -- `verdict: insufficient-context` - -Do not write generic verdicts such as `ACCEPTED`, `REJECTED`, `PASS`, `FAIL`, or -`BLOCKING`. Do not add an introduction, Markdown heading, or code fence before -the verdict. - -When the verdict is `user-choice-required`, the supervisor mechanically seals -the `user-question`, terminates the Session without issuing further -authority, and returns that question to the human. Ask exactly one bounded -question ending in a question mark. - -Return exactly these fields in this order: - -1. `verdict:` using one value from the exact vocabulary above. -2. `authority-findings:` each decision, owner, trigger, and evidence. -3. `omitted-decisions:` consequential choices not represented in the records. -4. `evidence-requests:` bounded questions, sources, expected signals, and stop - conditions. -5. `user-question:` compact alternatives and tradeoffs when user choice is - required; otherwise `none`. -6. `review-record: type=decision-authority verdict=pass diff=-` only when the - verdict is `orchestrator-may-decide`; otherwise - `review-record: type=decision-authority verdict=findings diff=-`. -7. When the supervisor-owned semantic envelope supplies a `decision-review:` - marker pair, reproduce exactly one marker matching the review verdict as the - next line. A review is invalid without this binding. -8. When the supervisor-owned semantic envelope supplies a `contract-review:` - marker and the verdict passes, reproduce that exact marker as the final line. - -For a passing review without a registered contract, use this literal shape: - - verdict: orchestrator-may-decide - authority-findings: DECISION | owner=orchestrator | trigger=EXPLICIT_TASK | evidence=SOURCE - omitted-decisions: none - evidence-requests: none - user-question: none - review-record: type=decision-authority verdict=pass diff=- - decision-review: capsule-sha256=SUPERVISOR_DIGEST verdict=pass - -Replace only the uppercase example values with the review's actual evidence and -the exact digest supplied by the supervisor. Do not replace, rename, reorder, -or omit the field labels or binding lines. - -Review the original user request and follow-ups, relevant prior user or wiki -decisions, repository evidence, active TODOs, proposed decisions and -alternatives, and the proposed approved implementation context. Do not rely -only on the orchestrator's summary when primary evidence is available. - -This is a pre-implementation authority review, not a diff or artifact -verification. The target output is normally absent, empty, or unchanged at this -phase. Do not inspect or reject the current candidate file merely because the -selected plan has not yet been implemented, and never require implementation -as evidence needed to authorize implementation. When the task assignment names -a decision ID, attempt one bounded inspection with `multiagent decision show -DECISION_ID`, then assess the selected plan and stated outcome against the -original task. Read-only role isolation may deny direct decision-store access. -In that case, use only the `Supervisor-Generated Decision Authority Capsule`, -verify its decision ID, selected plan, revision, original-task digest, and any -contract digest, and bind the verdict to its exact SHA-256 marker. The -orchestrator's prose assignment is not a substitute for this capsule. If -neither direct decision evidence nor a valid supervisor capsule is available, -return `insufficient-context`. For an exact bounded task, a capsule-selected -alternative that enumerates the required artifact fields and prohibitions is -sufficient semantic plan evidence. -Post-implementation reviewers and supervisor diff gates—not this role—verify -that the worker actually produced it. - -Determine: - -- whether each consequential choice is explicitly recorded; -- whether bounded evidence collection could remove factual uncertainty; -- whether each decision is orchestrator-owned or user-owned; -- whether the proposed worker assignment embeds an unrecorded choice; and -- whether the implementation context faithfully preserves the approved contract. - -Treat task-relative deliverable paths as rooted in the authenticated target -repository. Return findings if a proposed plan redirects a requested source or -artifact into `MULTIAGENT_STATE_DIR`, a prompt directory, or another -control-plane location; an instruction file's location never changes the -deliverable target. - -When the supervisor-owned semantic envelope contains a registered contract -artifact, compare every `polarity=must` and `polarity=must-not` rule against the -proposed plan and implementation context. A compatibility alias, fallback, -embedded legacy field, or retained old path is not automatically safer: if it -contradicts a negative rule, return findings and block implementation. Do not -allow the orchestrator to substitute a paraphrased checklist for the artifact. - -User-owned triggers include public behavior or contracts, roles or -responsibilities, persisted state or migration, security or trust boundaries, -destructive or difficult-to-reverse behavior, material scope or cost, and -conflict with a prior explicit user decision. Treat uncertain authority as -user-owned. - -The original request is itself the user's decision for every behavior it -explicitly specifies. Do not reopen that behavior merely because the repository -contains multiple lookup helpers, representations, legacy paths, synonyms, or -possible edge-case policies. Read the task's clauses together; explanatory -parentheticals and named canonical forms refine the contract rather than create -new alternatives. Choosing the narrowest source-backed implementation that -directly realizes an explicit requirement is orchestrator-owned. - -Return `user-choice-required` only when at least two materially different -public outcomes remain compatible with the complete explicit request, bounded -source/test evidence does not select between them, and choosing one would add, -remove, or contradict public behavior. Name the exact unresolved conflict. Do -not escalate hypothetical collisions, normalization policies, compatibility -variants, or other unrequested behavior; preserve existing behavior and use the -narrowest contract-compatible default instead. - -Do not use agent agreement or majority preference as authority. A passing -review means the orchestrator may proceed under the recorded authority; it is -not approval of a user-owned choice. -# Execution mechanics are not user-owned decisions - -- When the caller has already specified the intended outcome and safety boundary, ordinary bounded execution mechanics remain orchestrator-owned. This includes pagination, cursor traversal, chunking, provider identifier resolution, bounded retries, and following related records or replies needed to produce the requested result. -- A per-request provider or runbook limit is not an instruction to truncate the caller's requested scope. Select a completeness-preserving bounded strategy that obeys each request limit and stop condition. -- Do not ask the caller to choose between a knowingly incomplete result and the complete result they already requested. Do not escalate implementation details merely because they affect latency, token use, or the number of bounded read calls. -- Escalate only when materially different user-visible outcomes remain after applying the original goal, runbook, and explicit safety constraints, or when a configured cost/risk threshold would be exceeded. -- Evidence needed to choose an execution path may be gathered by the ops role after the implementation gate. Do not create a circular requirement that demands production execution before authority review can pass. diff --git a/prompts/roles/plan-alignment-reviewer.md b/prompts/roles/plan-alignment-reviewer.md new file mode 100644 index 0000000..a1ddad8 --- /dev/null +++ b/prompts/roles/plan-alignment-reviewer.md @@ -0,0 +1,68 @@ +# Plan Alignment Reviewer + +You are an independent read-only pre-implementation reviewer. Compare the +authenticated original user request directly with the complete sealed iteration +plan. Do not implement, edit the plan, coordinate workers, or invent a formal +contract that replaces the user's words. + +## Mandatory Output Protocol + +Your final response is parsed by the workflow runtime. The first non-empty line +must be exactly one of: + +- `alignment: aligned` +- `alignment: misaligned` + +Return exactly these fields in this order: + +1. `alignment:` using one value from the exact vocabulary above. +2. `findings:` `none` when aligned; otherwise identify each material omission, + contradiction, or unrequested addition and cite the relevant user request and + plan clause. +3. `user-question:` exactly one bounded question ending in `?` only when the + misalignment cannot be corrected from the existing request and evidence; + otherwise `none`. +4. `review-record: type=plan-alignment verdict=pass diff=-` when aligned; + otherwise `review-record: type=plan-alignment verdict=findings diff=-`. +5. Reproduce exactly one supervisor-supplied `plan-alignment-review:` marker + matching the alignment result. A review is invalid without this binding, + but the binding is not a substitute contract or a semantic decision. +6. When the semantic envelope supplies a `contract-review:` marker and the plan + is aligned, reproduce that exact marker as the final line. + +An aligned review without a registered contract has this literal shape: + + alignment: aligned + findings: none + user-question: none + review-record: type=plan-alignment verdict=pass diff=- + plan-alignment-review: plan-sha256=SEALED_PLAN_DIGEST original-task-sha256=ORIGINAL_TASK_DIGEST alignment=aligned + +## Review Standard + +Read the original request and follow-ups as the primary statement of user +intention. Check the whole sealed plan, including its implementation context, +worker instructions, owned paths, TODO claims, dependencies, and additional +reviews. + +Return `misaligned` when the plan: + +- omits a material requested outcome, constraint, target, or acceptance test; +- contradicts the user's request or redirects the requested deliverable; +- adds material behavior, scope, cost, risk, or external effect the user did not + request; or +- delegates only a minor or incidental part while skipping the main task. + +Return `aligned` when the plan is a bounded implementation of the user's +intention. Ordinary implementation choices do not require user input when the +request and repository evidence select a safe, narrow path. + +Use `user-question: none` when the orchestrator can repair a misaligned plan by +following the existing request more faithfully. Ask the user only when required +information is genuinely absent or materially different user-visible outcomes +remain possible. Misalignment is still the only gate outcome in either case; +the question only tells the supervisor where correction must come from. + +This is not post-implementation verification. Do not require the requested code +or artifact to exist yet. The separate technical review checks the produced +diff after implementation. diff --git a/runtime/src/runtime.rs b/runtime/src/runtime.rs index f172f9b..b4252e3 100644 --- a/runtime/src/runtime.rs +++ b/runtime/src/runtime.rs @@ -45,7 +45,6 @@ struct IterationPlan { kind: String, workflow_id: String, iteration: u64, - decision: IterationDecision, implementation_context: String, workers: Vec, #[serde(default)] @@ -54,29 +53,6 @@ struct IterationPlan { additional_reviews: Vec, } -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -struct IterationDecision { - id: String, - title: String, - selected_plan: String, - reason: String, - #[serde(default)] - rollback_policy: String, - alternatives: Vec, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -struct IterationAlternative { - id: String, - summary: String, - #[serde(default)] - expected_outcome: String, - #[serde(default)] - risk: String, -} - #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] struct IterationWorker { @@ -1846,7 +1822,7 @@ pub fn subagent(args: &[String]) -> Result { fn print_subagent_usage() { println!( - "Usage:\n multiagent subagent spawn NAME [--own PATH[,PATH...] ...] [--assignment-id ID] [--workflow-id ID --decision-id ID --plan-id ID --decision-revision REV] [--branch BRANCH] [--start-commit COMMIT] [--role ROLE] [--access read-only|workspace-write] [--instruction TEXT | --instruction-file PATH | -- TEXT]\n multiagent subagent restore NAME [--force] [--instruction TEXT | --instruction-file PATH]\n multiagent subagent reviewed-ops-cycle OPS_NAME --request-file PATH --reviewer NAME [--timeout SECONDS]\n multiagent subagent execute-iteration --plan-file PATH [--timeout SECONDS]\n multiagent subagent list|recover-plan|restore-all|gate-check\n multiagent subagent poll|inspect|finalize|kill NAME [OPTIONS]\n multiagent subagent wait NAME [--timeout SECONDS] [--poll-interval SECONDS]\n\nAll durable state and tmux subprocess orchestration are implemented by the Rust CLI." + "Usage:\n multiagent subagent spawn NAME [--own PATH[,PATH...] ...] [--assignment-id ID] [--workflow-id ID] [--decision-id ID --plan-id ID] [--plan-sha256 SHA256] [--branch BRANCH] [--start-commit COMMIT] [--role ROLE] [--access read-only|workspace-write] [--instruction TEXT | --instruction-file PATH | -- TEXT]\n multiagent subagent restore NAME [--force] [--instruction TEXT | --instruction-file PATH]\n multiagent subagent reviewed-ops-cycle OPS_NAME --request-file PATH --reviewer NAME [--timeout SECONDS]\n multiagent subagent execute-iteration --plan-file PATH [--timeout SECONDS]\n multiagent subagent list|recover-plan|restore-all|gate-check\n multiagent subagent poll|inspect|finalize|kill NAME [OPTIONS]\n multiagent subagent wait NAME [--timeout SECONDS] [--poll-interval SECONDS]\n\nAll durable state and tmux subprocess orchestration are implemented by the Rust CLI." ); } @@ -1890,13 +1866,8 @@ fn spawn(cfg: &RuntimeConfig, args: &[String]) -> Result<(), String> { )?)?); index += 2; } - "--assignment-id" - | "--workflow-id" - | "--decision-id" - | "--plan-id" - | "--decision-revision" - | "--branch" - | "--start-commit" => { + "--assignment-id" | "--workflow-id" | "--decision-id" | "--plan-id" + | "--plan-sha256" | "--branch" | "--start-commit" => { assignment_values.insert( args[index].clone(), required_value(args, index, "spawn assignment metadata")?.to_string(), @@ -1976,6 +1947,23 @@ fn spawn(cfg: &RuntimeConfig, args: &[String]) -> Result<(), String> { if role == "reader" && access != CodexAccess::ReadOnly { return Err("reader roles require read-only access".into()); } + let plan_alignment = authority_role == "reviewer" + && role_prompt_name(name, &role) == Some("prompts/roles/plan-alignment-reviewer.md"); + let alignment_plan_sha256 = assignment_values + .get("--plan-sha256") + .map(String::as_str) + .unwrap_or(""); + let alignment_task_sha256 = if plan_alignment { + workflow::semantic_envelope( + assignment_values + .get("--workflow-id") + .map(String::as_str) + .unwrap_or(""), + )? + .original_task_sha256 + } else { + String::new() + }; require_command("tmux")?; let cli = &cfg.subagent_cli; @@ -1989,32 +1977,28 @@ fn spawn(cfg: &RuntimeConfig, args: &[String]) -> Result<(), String> { } reject_parallel_generic_worker_spawn(cfg, name)?; reject_additional_ops_identity(&cfg.state, name, authority_role)?; - let decision_authority = authority_role == "reviewer" - && role_prompt_name(name, &role) == Some("prompts/roles/decision-authority-reviewer.md"); if owned.is_empty() { - let required = [ - "--workflow-id", - "--decision-id", - "--plan-id", - "--decision-revision", - ]; - let exact_decision_metadata = decision_authority + let required = ["--workflow-id", "--plan-sha256"]; + let exact_alignment_metadata = plan_alignment && assignment_values.len() == required.len() && required .iter() .all(|flag| assignment_values.contains_key(*flag)); - if decision_authority && !exact_decision_metadata { - return Err("decision-authority reviewer requires --workflow-id, --decision-id, --plan-id, and --decision-revision".into()); + if plan_alignment && !exact_alignment_metadata { + return Err("plan-alignment reviewer requires --workflow-id and --plan-sha256".into()); } - if !decision_authority && !assignment_values.is_empty() { - return Err("spawn assignment metadata requires --own PATH; only exact decision capsule metadata is allowed for the decision-authority reviewer".into()); + if !plan_alignment && !assignment_values.is_empty() { + return Err("spawn assignment metadata requires --own PATH; only exact sealed-plan metadata is allowed for the plan-alignment reviewer".into()); } - if decision_authority { + if plan_alignment { let active_workflow = env_nonempty("MULTIAGENT_WORKFLOW_ID").unwrap_or_default(); if assignment_values.get("--workflow-id").map(String::as_str) != Some(active_workflow.as_str()) { - return Err("decision-authority reviewer workflow metadata does not match the active workflow".into()); + return Err( + "plan-alignment reviewer workflow metadata does not match the active workflow" + .into(), + ); } } } @@ -2102,7 +2086,7 @@ fn spawn(cfg: &RuntimeConfig, args: &[String]) -> Result<(), String> { fs::create_dir_all(&cfg.logs).map_err(io_error("create subagent log directory"))?; let executable = env::current_exe().map_err(io_error("resolve multiagent executable"))?; let metadata = format!( - "name={name}\nsession={}\nroot={}\nrole={}\naccess={}\ncodex_access={}\nworkflow_id={}\nwrite_policy={}\nlog_file={}\ntrace_dir={}\ncli={cli}\ncli_bin={binary}\nhelper={}\ncreated_at={}\n", + "name={name}\nsession={}\nroot={}\nrole={}\naccess={}\ncodex_access={}\nworkflow_id={}\niteration_plan_sha256={alignment_plan_sha256}\noriginal_task_sha256={alignment_task_sha256}\nwrite_policy={}\nlog_file={}\ntrace_dir={}\ncli={cli}\ncli_bin={binary}\nhelper={}\ncreated_at={}\n", cfg.session, cfg.root.display(), authority_role, @@ -2154,16 +2138,14 @@ fn spawn(cfg: &RuntimeConfig, args: &[String]) -> Result<(), String> { "--instruction-file".to_string(), instruction_path, ]; - if decision_authority { - for flag in ["--decision-id", "--plan-id", "--decision-revision"] { - command.push(flag.to_string()); - command.push( - assignment_values - .get(flag) - .cloned() - .ok_or_else(|| format!("decision-authority spawn requires {flag}"))?, - ); - } + if plan_alignment { + command.push("--plan-sha256".into()); + command.push( + assignment_values + .get("--plan-sha256") + .cloned() + .ok_or("plan-alignment spawn requires --plan-sha256")?, + ); } let command = command.iter().map(String::as_str).collect::>(); run_self_quiet(&command)?; @@ -2370,7 +2352,7 @@ fn execute_iteration(cfg: &RuntimeConfig, args: &[String]) -> Result<(), String> plan.iteration )); } - let active_todos = validate_iteration_todos(&plan)?; + validate_iteration_todos(&plan)?; let plan_sha256 = format!("{:x}", Sha256::digest(&plan_bytes)); let execution_dir = cfg @@ -2395,105 +2377,76 @@ fn execute_iteration(cfg: &RuntimeConfig, args: &[String]) -> Result<(), String> "--worker-count".into(), plan.workers.len().to_string(), ])?; - materialize_iteration_decision(&plan)?; - - let authority_name = format!("decision-authority-reviewer-{:02}", plan.iteration); - let selected = plan - .decision - .alternatives - .iter() - .find(|alternative| alternative.id == plan.decision.selected_plan) - .expect("validated selected iteration plan"); - let authority_instruction = format!( - "Review sealed iteration plan sha256={plan_sha256}. Selected outcome: {}. Implementation context: {}. Worker graph: {}. Exact owned paths: {}. Direct TODOs this plan claims to resolve after passing review: {}. Confirm that this bounded plan follows the authenticated task, addresses those TODOs, preserves authority boundaries, and contains no unauthorized operation or scope expansion.", - selected.expected_outcome, - plan.implementation_context, - plan.workers - .iter() - .map(|worker| format!("{}<-{}", worker.id, if worker.depends_on.is_empty() { "ready".into() } else { worker.depends_on.join(",") })) - .collect::>() - .join("; "), - owned_paths.join(","), - if active_todos.is_empty() { "none".into() } else { active_todos.iter().map(|todo| format!("{}: {}", todo.id, todo.summary)).collect::>().join("; ") }, + let alignment_name = format!("plan-alignment-reviewer-{:02}", plan.iteration); + let sealed_plan = std::str::from_utf8(&plan_bytes) + .map_err(|_| "iteration plan must be UTF-8 JSON".to_string())?; + let alignment_instruction = format!( + "Compare the authenticated original task with this complete sealed iteration plan. Check the main requested outcome, every material constraint, the implementation context, all worker instructions and owned paths, TODO claims, dependencies, and additional reviews. Return only aligned or misaligned using the role protocol.\n\nsealed-iteration-plan-sha256={plan_sha256}\n\n```json\n{sealed_plan}\n```" ); - let authority_review_id = format!("iteration-{}-authority", plan.iteration); + let alignment_review_id = format!("iteration-{}-alignment", plan.iteration); if !workflow::passing_review_recorded( &plan.workflow_id, - &authority_review_id, - "decision-authority", + &alignment_review_id, + "plan-alignment", )? { spawn( cfg, &[ - authority_name.clone(), + alignment_name.clone(), "--role".into(), "reviewer".into(), "--workflow-id".into(), plan.workflow_id.clone(), - "--decision-id".into(), - plan.decision.id.clone(), - "--plan-id".into(), - plan.decision.selected_plan.clone(), - "--decision-revision".into(), - plan.iteration.to_string(), + "--plan-sha256".into(), + plan_sha256.clone(), "--instruction".into(), - authority_instruction, + alignment_instruction, ], )?; wait( cfg, - &[authority_name.clone(), "--timeout".into(), timeout.clone()], + &[alignment_name.clone(), "--timeout".into(), timeout.clone()], )?; - let authority_message = agent_final_message(cfg, &authority_name)?; - finalize(cfg, std::slice::from_ref(&authority_name))?; - let authority_verdict = - review_output_verdict(&authority_message, "decision-authority", "-").ok_or_else( + let alignment_message = agent_final_message(cfg, &alignment_name)?; + finalize(cfg, std::slice::from_ref(&alignment_name))?; + let alignment_verdict = + review_output_verdict(&alignment_message, "plan-alignment", "-").ok_or_else( || { format!( - "authority reviewer output is missing the required structured marker: {authority_name}" + "plan-alignment reviewer output is missing the required structured marker: {alignment_name}" ) }, )?; record_iteration_review( &plan.workflow_id, - &authority_review_id, - "decision-authority", - authority_verdict, + &alignment_review_id, + "plan-alignment", + alignment_verdict, "-", - &authority_name, + &alignment_name, )?; - if authority_verdict == "findings" { + if alignment_verdict == "findings" { if let Some((question, _)) = - workflow::reviewer_human_review_question(&plan.workflow_id, &authority_name)? + workflow::reviewer_human_review_question(&plan.workflow_id, &alignment_name)? { - complete_reviewer_human_fallback(cfg, &authority_name, &question)?; + complete_reviewer_human_fallback(cfg, &alignment_name, &question)?; emit_iteration_result( "human_review_required", &plan, &plan_sha256, - "decision-authority-user-choice", + "plan-misaligned-user-input-required", None, )?; return Ok(()); } - emit_iteration_result( - "needs_replan", - &plan, - &plan_sha256, - "decision-authority-findings", - None, - )?; + emit_iteration_result("needs_replan", &plan, &plan_sha256, "plan-misaligned", None)?; return Ok(()); } } let implementation_context = format!( - "# Sealed Implementation Context\n\niteration-plan-sha256={plan_sha256}\nworkflow={}\niteration={}\ndecision={}\nselected-plan={}\n\n{}\n", - plan.workflow_id, - plan.iteration, - plan.decision.id, - plan.decision.selected_plan, - plan.implementation_context, + "# Sealed Implementation Context\n\niteration-plan-sha256={plan_sha256}\nworkflow={}\niteration={}\n\n{}\n", + plan.workflow_id, plan.iteration, plan.implementation_context, ); let context_file = execution_dir.join("implementation-context.md"); atomic_write( @@ -2505,16 +2458,12 @@ fn execute_iteration(cfg: &RuntimeConfig, args: &[String]) -> Result<(), String> "workflow".into(), "prepare-implementation".into(), plan.workflow_id.clone(), - "--decision-id".into(), - plan.decision.id.clone(), - "--plan-id".into(), - plan.decision.selected_plan.clone(), - "--decision-revision".into(), - plan.iteration.to_string(), + "--plan-sha256".into(), + plan_sha256.clone(), "--implementation-context".into(), context_file.display().to_string(), - "--authority-review".into(), - authority_review_id, + "--alignment-review".into(), + alignment_review_id, ])?; run_self_owned(&[ "workflow".into(), @@ -2681,52 +2630,6 @@ fn validate_iteration_plan( { return Err("iteration plan implementationContext must contain 1..65536 bytes".into()); } - for (label, value) in [ - ("decision.id", plan.decision.id.as_str()), - ("decision.title", plan.decision.title.as_str()), - ( - "decision.selectedPlan", - plan.decision.selected_plan.as_str(), - ), - ("decision.reason", plan.decision.reason.as_str()), - ( - "decision.rollbackPolicy", - plan.decision.rollback_policy.as_str(), - ), - ] { - if value.is_empty() || value.contains(['\n', '\r', '\t']) { - return Err(format!( - "iteration plan {label} must be a non-empty single-line value" - )); - } - } - if plan.decision.alternatives.is_empty() || plan.decision.alternatives.len() > 8 { - return Err("iteration plan requires 1..8 decision alternatives".into()); - } - let mut alternatives = BTreeSet::new(); - for alternative in &plan.decision.alternatives { - if alternative.id.is_empty() - || alternative.summary.is_empty() - || alternative.id.contains(['\n', '\r', '\t']) - || alternative.summary.contains(['\n', '\r', '\t']) - || alternative.expected_outcome.contains(['\n', '\r', '\t']) - || alternative.risk.contains(['\n', '\r', '\t']) - { - return Err( - "iteration decision alternatives must use non-empty single-line IDs and summaries" - .into(), - ); - } - if !alternatives.insert(alternative.id.as_str()) { - return Err(format!( - "duplicate iteration alternative: {}", - alternative.id - )); - } - } - if !alternatives.contains(plan.decision.selected_plan.as_str()) { - return Err("iteration selectedPlan does not name an alternative".into()); - } if plan.workers.is_empty() || plan.workers.len() > 32 { return Err("iteration plan requires 1..32 workers".into()); } @@ -2795,15 +2698,13 @@ fn validate_iteration_plan( } completed.extend(ready); } - let allowed_reviews = ["decision-drift", "scope", "reflection"]; + let allowed_reviews = ["scope", "reflection"]; if plan .additional_reviews .iter() .any(|kind| !allowed_reviews.contains(&kind.as_str())) { - return Err( - "additionalReviews may contain only decision-drift, scope, or reflection".into(), - ); + return Err("additionalReviews may contain only scope or reflection".into()); } for todo in &plan.resolves_todos { if todo.is_empty() @@ -2852,49 +2753,6 @@ fn validate_iteration_todos(plan: &IterationPlan) -> Result Result<(), String> { - if workflow::committed_decision_matches(&plan.decision.id, &plan.decision.selected_plan)? { - return Ok(()); - } - run_self_owned(&[ - "decision".into(), - "init".into(), - plan.decision.id.clone(), - "--title".into(), - plan.decision.title.clone(), - "--owner".into(), - "orchestrator".into(), - ])?; - for alternative in &plan.decision.alternatives { - run_self_owned(&[ - "decision".into(), - "add-alternative".into(), - plan.decision.id.clone(), - "--plan-id".into(), - alternative.id.clone(), - "--summary".into(), - alternative.summary.clone(), - "--proposed-by".into(), - "orchestrator".into(), - "--expected-outcome".into(), - alternative.expected_outcome.clone(), - "--risk".into(), - alternative.risk.clone(), - ])?; - } - run_self_owned(&[ - "decision".into(), - "commit".into(), - plan.decision.id.clone(), - "--selected-plan".into(), - plan.decision.selected_plan.clone(), - "--reason".into(), - plan.decision.reason.clone(), - "--rollback-policy".into(), - plan.decision.rollback_policy.clone(), - ]) -} - fn execute_worker_graph( cfg: &RuntimeConfig, plan: &IterationPlan, @@ -2942,12 +2800,6 @@ fn execute_worker_graph( spawn_args.extend([ "--workflow-id".into(), plan.workflow_id.clone(), - "--decision-id".into(), - plan.decision.id.clone(), - "--plan-id".into(), - plan.decision.selected_plan.clone(), - "--decision-revision".into(), - plan.iteration.to_string(), "--instruction-file".into(), instruction_file.display().to_string(), ]); @@ -3032,7 +2884,7 @@ fn record_iteration_review( "--verdict".into(), verdict.into(), ]; - if kind != "decision-authority" { + if kind != "plan-alignment" { args.push("--diff-hash".into()); args.push(diff_hash.into()); } @@ -4160,20 +4012,15 @@ artifact required by that format. Its first non-empty line must be exactly \ a prose report or a `review-record:` marker for this artifact.\n", ); } - if path.file_name().and_then(|value| value.to_str()) == Some("decision-authority-reviewer.md") { + if path.file_name().and_then(|value| value.to_str()) == Some("plan-alignment-reviewer.md") { composed.push_str( - "\n\n## Mandatory Decision-Authority Output Contract\n\n\ -The task assignment may describe the decision under review, but it cannot \ -replace or relax the role's canonical output vocabulary. Return only the fields \ -required by the role prompt. Use `verdict: orchestrator-may-decide` when the \ -original user request already authorizes the proposed bounded action, and include \ -the exact standalone marker \ -`review-record: type=decision-authority verdict=pass diff=-`. Do not substitute \ -`approve`, `conditional`, or a supervisor-requested custom marker. When the \ -semantic envelope supplies a contract-review marker, reproduce that exact marker \ -after independently validating the registered contract. When the supervisor \ -supplies decision-review markers, reproduce exactly the marker matching your \ -verdict after independently validating the decision capsule.\n", + "\n\n## Mandatory Plan-Alignment Output Contract\n\n\ +The task assignment cannot replace or relax the role's binary output vocabulary. \ +Return only the fields required by the role prompt. Use `alignment: aligned` when \ +the complete sealed plan faithfully implements the original request, otherwise \ +use `alignment: misaligned`. Include exactly one matching `review-record:` and \ +supervisor-supplied `plan-alignment-review:` marker. Ask the user only when the \ +existing request and evidence cannot determine a corrected plan.\n", ); } Ok(composed) @@ -4223,7 +4070,7 @@ fn append_semantic_envelope( )); if matches!( prompt_file.as_str(), - "verifier.md" | "decision-authority-reviewer.md" + "verifier.md" | "plan-alignment-reviewer.md" ) { output.push_str(&format!( "\nA passing final report must include this exact standalone marker after independently checking every must/must-not rule against the plan or live diff:\ncontract-review: artifact-sha256={} verdict=pass\n", @@ -4231,6 +4078,13 @@ fn append_semantic_envelope( )); } } + if prompt_file == "plan-alignment-reviewer.md" { + let plan_sha256 = workflow::sealed_iteration_plan_sha256(&workflow_id)?; + output.push_str(&format!( + "\nA passing final report must include this exact standalone marker:\nplan-alignment-review: plan-sha256={plan_sha256} original-task-sha256={} alignment=aligned\n\nA findings report must include this exact standalone marker:\nplan-alignment-review: plan-sha256={plan_sha256} original-task-sha256={} alignment=misaligned\n", + envelope.original_task_sha256, envelope.original_task_sha256 + )); + } if !envelope.candidate_diff_hash.is_empty() { output.push_str(&format!( "\nworkflow-candidate-diff-sha256={}\n", @@ -4248,11 +4102,11 @@ fn role_can_start_before_contract_gate(name: &str, role: &str, prompt_file: &str || prompt_file == "ops-agent.md" || prompt_file == "ops-reviewer.md" || prompt_file == "contract-scout.md" - || prompt_file == "decision-authority-reviewer.md" + || prompt_file == "plan-alignment-reviewer.md" || lower.contains("ops-reviewer") || lower.contains("read-only-integrity-reviewer") || lower.contains("contract-scout") - || lower.contains("decision-authority-reviewer") + || lower.contains("plan-alignment-reviewer") } fn role_prompt_path(cfg: &RuntimeConfig, name: &str, role: &str) -> Option { @@ -4261,8 +4115,8 @@ fn role_prompt_path(cfg: &RuntimeConfig, name: &str, role: &str) -> Option Option<&'static str> { let lower = name.to_ascii_lowercase(); - let relative = if lower.contains("decision-authority-reviewer") { - "prompts/roles/decision-authority-reviewer.md" + let relative = if lower.contains("plan-alignment-reviewer") { + "prompts/roles/plan-alignment-reviewer.md" } else if lower.contains("read-only-integrity-reviewer") { "prompts/roles/read-only-integrity-reviewer.md" } else if lower.contains("ops-reviewer") { @@ -4325,7 +4179,7 @@ fn codex_access_for_spawn(cfg: &RuntimeConfig, name: &str, role: &str) -> CodexA || role == "scout" || lower.starts_with("verifier-") || lower.contains("reviewer") - || lower.contains("decision-authority-reviewer") + || lower.contains("plan-alignment-reviewer") || matches!( prompt.as_deref(), Some( @@ -4334,7 +4188,7 @@ fn codex_access_for_spawn(cfg: &RuntimeConfig, name: &str, role: &str) -> CodexA | "read-only-integrity-reviewer.md" | "acceptance-scout.md" | "contract-scout.md" - | "decision-authority-reviewer.md" + | "plan-alignment-reviewer.md" | "scope-guard.md" | "validation-coordinator.md" ) @@ -4426,42 +4280,32 @@ fn implementation_context(cfg: &RuntimeConfig, name: &str) -> Result Result<(), String> { "lifecycle enforcement requires --workflow-id for exploitation assignments".into(), ); } - if options.decision_id.is_empty() { - return Err( - "lifecycle enforcement requires --decision-id for exploitation assignments".into(), - ); - } - if options.plan_id.is_empty() { - return Err( - "lifecycle enforcement requires --plan-id for exploitation assignments".into(), - ); - } - Some( - workflow::assignment_context(&workflow_id, &options.decision_id, &options.plan_id) - .map_err(|_| { - format!( - "workflow implementation gate rejected assignment for workflow {workflow_id}" - ) - })?, - ) + Some(workflow::assignment_context(&workflow_id).map_err(|_| { + format!("workflow implementation gate rejected assignment for workflow {workflow_id}") + })?) } else { None }; @@ -533,10 +518,14 @@ fn assignment_create(args: &[String]) -> Result<(), String> { ("decision_id", options.decision_id.as_str()), ("plan_id", options.plan_id.as_str()), ( - "decision_revision", + "iteration", + context.as_ref().map(|v| v.iteration.as_str()).unwrap_or(""), + ), + ( + "iteration_plan_sha256", context .as_ref() - .map(|v| v.decision_revision.as_str()) + .map(|v| v.iteration_plan_sha256.as_str()) .unwrap_or(""), ), ( diff --git a/runtime/src/supervisor.rs b/runtime/src/supervisor.rs index c037130..c7782af 100644 --- a/runtime/src/supervisor.rs +++ b/runtime/src/supervisor.rs @@ -179,44 +179,31 @@ fn register_launch(args: &[String], renew: bool) -> Result<(), String> { let state = config::state_dir()?; let workflow_id = env::var("MULTIAGENT_WORKFLOW_ID").unwrap_or_default(); let directory = state.join("launch-authorizations").join(name); - let decision_authority = role == "reviewer" && name.contains("decision-authority-reviewer"); + let plan_alignment = role == "reviewer" && name.contains("plan-alignment-reviewer"); let prior = if renew && directory.join("launch.env").is_file() { read_env_file(&directory.join("launch.env"))? } else { BTreeMap::new() }; - let decision_value = |flag: &str, key: &str| { + let metadata_value = |flag: &str, key: &str| { options .get(flag) .cloned() .or_else(|| prior.get(key).cloned()) .unwrap_or_default() }; - let decision_id = decision_value("--decision-id", "decision_id"); - let plan_id = decision_value("--plan-id", "plan_id"); - let decision_revision = decision_value("--decision-revision", "decision_revision"); - let decision_capsule = if decision_authority { - if workflow_id.is_empty() - || decision_id.is_empty() - || plan_id.is_empty() - || decision_revision.is_empty() - { - return Err("decision-authority launch requires workflow, decision, plan, and revision metadata".into()); + let plan_sha256 = metadata_value("--plan-sha256", "iteration_plan_sha256"); + let alignment_binding = if plan_alignment { + if workflow_id.is_empty() || plan_sha256.is_empty() { + return Err("plan-alignment launch requires workflow and sealed-plan metadata".into()); } - Some(crate::workflow::decision_authority_capsule( + Some(crate::workflow::plan_alignment_binding( &workflow_id, - &decision_id, - &plan_id, - &decision_revision, + &plan_sha256, )?) } else { - if options.contains_key("--decision-id") - || options.contains_key("--plan-id") - || options.contains_key("--decision-revision") - { - return Err( - "decision capsule metadata is reserved for the decision-authority reviewer".into(), - ); + if options.contains_key("--plan-sha256") { + return Err("sealed-plan metadata is reserved for the plan-alignment reviewer".into()); } None }; @@ -277,13 +264,13 @@ fn register_launch(args: &[String], renew: bool) -> Result<(), String> { "renewed launch cannot change role or coding-agent identity: {name}" )); } - if decision_authority - && current.get("decision_capsule_sha256").map(String::as_str) - != decision_capsule + if plan_alignment + && current.get("alignment_binding_sha256").map(String::as_str) + != alignment_binding .as_ref() - .map(|capsule| capsule.sha256.as_str()) + .map(|binding| binding.sha256.as_str()) { - return Err("renewed decision-authority launch changed the decision capsule".into()); + return Err("renewed plan-alignment launch changed the sealed binding".into()); } } else if renew { return Err(format!("launch authorization does not exist: {name}")); @@ -292,32 +279,41 @@ fn register_launch(args: &[String], renew: bool) -> Result<(), String> { .map_err(|error| format!("create launch authorization: {error}"))?; let mut instruction = fs::read(&instruction_source) .map_err(|error| format!("read registered instruction: {error}"))?; - if let Some(capsule) = &decision_capsule { + if let Some(binding) = &alignment_binding { instruction.extend_from_slice( format!( - "\n\n## Supervisor-Generated Decision Authority Capsule\n\n\ -This immutable capsule is the only selected-plan artifact authorized for this review. \ -Independently compare it with the original task. Include exactly one standalone decision-review marker matching your verdict.\n\n\ -decision-capsule-sha256={}\n{}\n\ -Passing marker: decision-review: capsule-sha256={} verdict=pass\n\ -Findings marker: decision-review: capsule-sha256={} verdict=findings\n", - capsule.sha256, capsule.content, capsule.sha256, capsule.sha256 + "\n\n## Supervisor-Generated Plan Alignment Binding\n\n\ +This binding identifies the exact original task and sealed plan under review. \ +It prevents input substitution but does not define user intent or grant authority.\n\n\ +alignment-binding-sha256={}\n{}\n\ +Aligned marker: plan-alignment-review: plan-sha256={} original-task-sha256={} alignment=aligned\n\ +Misaligned marker: plan-alignment-review: plan-sha256={} original-task-sha256={} alignment=misaligned\n", + binding.sha256, + binding.content, + binding.plan_sha256, + binding.original_task_sha256, + binding.plan_sha256, + binding.original_task_sha256 ) .as_bytes(), ); atomic_write_bytes( - &directory.join("decision-capsule.json"), - capsule.content.as_bytes(), + &directory.join("plan-alignment-binding.json"), + binding.content.as_bytes(), )?; } let instruction_path = directory.join("instruction.txt"); atomic_write_bytes(&instruction_path, &instruction)?; let metadata = format!( - "name={name}\nrole={role}\naccess={access}\nworkflow_id={workflow_id}\ncli={cli}\ncli_bin={cli_bin}\ninstruction_sha256={:x}\ndecision_id={decision_id}\nplan_id={plan_id}\ndecision_revision={decision_revision}\ndecision_capsule_sha256={}\nstate=registered\n", + "name={name}\nrole={role}\naccess={access}\nworkflow_id={workflow_id}\ncli={cli}\ncli_bin={cli_bin}\ninstruction_sha256={:x}\niteration_plan_sha256={plan_sha256}\noriginal_task_sha256={}\nalignment_binding_sha256={}\nstate=registered\n", Sha256::digest(&instruction), - decision_capsule + alignment_binding .as_ref() - .map(|capsule| capsule.sha256.as_str()) + .map(|binding| binding.original_task_sha256.as_str()) + .unwrap_or(""), + alignment_binding + .as_ref() + .map(|binding| binding.sha256.as_str()) .unwrap_or("") ); atomic_write_bytes(&directory.join("launch.env"), metadata.as_bytes())?; @@ -464,7 +460,7 @@ pub fn seal_role_output( atomic_write_bytes(&directory.join("last-message.txt"), &bytes)?; let completed_at = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true); let mut binding_metadata = String::new(); - let mut decision_metadata = String::new(); + let mut alignment_metadata = String::new(); if role == "reviewer" { let binding_path = trace_dir.join("review-binding.json"); if binding_path.exists() { @@ -486,32 +482,31 @@ pub fn seal_role_output( } let launch_directory = state.join("launch-authorizations").join(name); let launch = read_env_file(&launch_directory.join("launch.env"))?; - let capsule_hash = launch - .get("decision_capsule_sha256") + let binding_hash = launch + .get("alignment_binding_sha256") .map(String::as_str) .unwrap_or(""); - if !capsule_hash.is_empty() { - let capsule = fs::read(launch_directory.join("decision-capsule.json")) - .map_err(|error| format!("read supervisor decision capsule: {error}"))?; - if format!("{:x}", Sha256::digest(&capsule)) != capsule_hash { + if !binding_hash.is_empty() { + let binding = fs::read(launch_directory.join("plan-alignment-binding.json")) + .map_err(|error| format!("read supervisor plan-alignment binding: {error}"))?; + if format!("{:x}", Sha256::digest(&binding)) != binding_hash { return Err( - "supervisor decision capsule changed before evidence sealing".into(), + "supervisor plan-alignment binding changed before evidence sealing".into(), ); } - atomic_write_bytes(&directory.join("decision-capsule.json"), &capsule)?; - decision_metadata = format!( - "decision_id={}\nplan_id={}\ndecision_revision={}\ndecision_capsule_sha256={capsule_hash}\n", - launch.get("decision_id").map(String::as_str).unwrap_or(""), - launch.get("plan_id").map(String::as_str).unwrap_or(""), + atomic_write_bytes(&directory.join("plan-alignment-binding.json"), &binding)?; + alignment_metadata = format!( + "iteration_plan_sha256={}\noriginal_task_sha256={}\nalignment_binding_sha256={binding_hash}\n", launch - .get("decision_revision") + .get("iteration_plan_sha256") .map(String::as_str) - .unwrap_or("") + .unwrap_or(""), + launch.get("original_task_sha256").map(String::as_str).unwrap_or("") ); } } let metadata = format!( - "name={name}\nrole={role}\naccess=read-only\nworkflow_id={workflow_id}\nstate=completed\ncompleted_at={completed_at}\noutput_sha256={:x}\n{binding_metadata}{decision_metadata}", + "name={name}\nrole={role}\naccess=read-only\nworkflow_id={workflow_id}\nstate=completed\ncompleted_at={completed_at}\noutput_sha256={:x}\n{binding_metadata}{alignment_metadata}", Sha256::digest(&bytes) ); atomic_write_bytes(&directory.join("evidence.env"), metadata.as_bytes())?; @@ -556,10 +551,9 @@ fn write_launch_state( .unwrap_or("") )); for key in [ - "decision_id", - "plan_id", - "decision_revision", - "decision_capsule_sha256", + "iteration_plan_sha256", + "original_task_sha256", + "alignment_binding_sha256", ] { text.push_str(&format!( "{key}={}\n", @@ -613,7 +607,7 @@ fn parse_options(args: &[String]) -> Result, String> { | "--instruction-file" | "--decision-id" | "--plan-id" - | "--decision-revision" + | "--plan-sha256" ) || pair[1].contains(['\n', '\r']) { return Err(format!("invalid register-launch option: {}", pair[0])); @@ -1235,9 +1229,9 @@ pub fn start(_state: &Path, _executable: &Path) -> Result { #[cfg(test)] mod tests { - use super::launch_access; #[cfg(target_os = "linux")] use super::serve_connection; + use super::{launch_access, parse_options}; #[cfg(target_os = "linux")] use std::os::unix::net::UnixStream; @@ -1264,4 +1258,20 @@ mod tests { assert!(launch_access("reviewer", Some("workspace-write")).is_err()); assert!(launch_access("worker", Some("unknown")).is_err()); } + + #[test] + fn supervisor_accepts_only_current_plan_alignment_metadata() { + let options = parse_options(&[ + "--role".into(), + "reviewer".into(), + "--plan-sha256".into(), + "abc123".into(), + ]) + .unwrap(); + assert_eq!( + options.get("--plan-sha256").map(String::as_str), + Some("abc123") + ); + assert!(parse_options(&["--decision-revision".into(), "1".into()]).is_err()); + } } diff --git a/runtime/src/workflow.rs b/runtime/src/workflow.rs index 8518df4..dcfb2e6 100644 --- a/runtime/src/workflow.rs +++ b/runtime/src/workflow.rs @@ -18,14 +18,13 @@ const PHASES: &[&str] = &[ const ACTIVE: &[&str] = &["open", "assigned", "in-progress"]; const TODO_KINDS: &[&str] = &["direct", "evidence", "decision"]; const REVIEW_TYPES: &[&str] = &[ - "decision-authority", + "plan-alignment", "read-only-integrity", - "decision-drift", "scope", "technical", "reflection", ]; -const POST_REVIEWS: &[&str] = &["decision-drift", "scope", "technical", "reflection"]; +const POST_REVIEWS: &[&str] = &["scope", "technical", "reflection"]; const ENV_ORDER: &[&str] = &[ "workflow_id", "phase", @@ -36,14 +35,9 @@ const ENV_ORDER: &[&str] = &[ "contract_artifact", "contract_artifact_sha256", "preimplementation_gate", - "decision_id", - "plan_id", - "decision_revision", - "decision_capsule", - "decision_capsule_sha256", "implementation_context", "implementation_context_sha256", - "authority_review_id", + "alignment_review_id", "iteration_plan_sha256", "iteration_worker_count", "candidate_diff_hash", @@ -70,14 +64,14 @@ const USAGE: &str = r#"Usage: multiagent workflow context WORKFLOW_ID multiagent workflow contract-register WORKFLOW_ID --scout NAME multiagent workflow seal-iteration WORKFLOW_ID --plan-sha256 SHA256 --worker-count COUNT - multiagent workflow prepare-implementation WORKFLOW_ID --decision-id ID --plan-id ID --decision-revision REV --implementation-context PATH --authority-review ID + multiagent workflow prepare-implementation WORKFLOW_ID --plan-sha256 SHA256 --implementation-context PATH --alignment-review ID multiagent workflow transition WORKFLOW_ID PHASE [--diff-hash HASH] multiagent workflow add-todo WORKFLOW_ID TODO_ID --kind KIND --summary TEXT [--origin TEXT] multiagent workflow todo-status WORKFLOW_ID TODO_ID STATUS [--assignment-id ID] multiagent workflow resolve-todo WORKFLOW_ID TODO_ID --resolution STATUS --evidence TEXT [OPTIONS] multiagent workflow require-review WORKFLOW_ID OBLIGATION_ID --type TYPE --trigger TRIGGER --artifact-digest DIGEST --reason TEXT multiagent workflow record-review WORKFLOW_ID REVIEW_ID --type TYPE --verdict VERDICT [--diff-hash HASH] --evidence TEXT [--reviewer NAME] - multiagent workflow gate WORKFLOW_ID implementation|completion [--decision-id ID] [--plan-id ID] + multiagent workflow gate WORKFLOW_ID implementation|completion [--plan-sha256 SHA256] multiagent workflow completion-check WORKFLOW_ID multiagent workflow value WORKFLOW_ID KEY"#; @@ -112,7 +106,8 @@ pub fn run(args: &[String]) -> Result<(), String> { } pub struct AssignmentContext { - pub decision_revision: String, + pub iteration: String, + pub iteration_plan_sha256: String, pub implementation_context: String, pub implementation_context_sha256: String, } @@ -125,12 +120,11 @@ pub struct SemanticEnvelope { pub candidate_diff_hash: String, } -pub struct DecisionAuthorityCapsule { +pub struct PlanAlignmentBinding { pub content: String, pub sha256: String, - pub decision_id: String, - pub plan_id: String, - pub revision: String, + pub plan_sha256: String, + pub original_task_sha256: String, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -147,15 +141,12 @@ pub struct ActiveTodo { pub summary: String, } -pub fn assignment_context( - workflow_id: &str, - decision_id: &str, - plan_id: &str, -) -> Result { +pub fn assignment_context(workflow_id: &str) -> Result { let store = Store::configured()?; - let state = implementation_gate_state(&store, workflow_id, decision_id, plan_id, false)?; + let state = implementation_gate_state(&store, workflow_id, "", false)?; Ok(AssignmentContext { - decision_revision: state_value(&state, "decision_revision").to_string(), + iteration: state_value(&state, "iteration").to_string(), + iteration_plan_sha256: state_value(&state, "iteration_plan_sha256").to_string(), implementation_context: state_value(&state, "implementation_context").to_string(), implementation_context_sha256: state_value(&state, "implementation_context_sha256") .to_string(), @@ -177,91 +168,46 @@ pub fn semantic_envelope(workflow_id: &str) -> Result }) } -pub fn decision_authority_capsule( - workflow_id: &str, - decision_id: &str, - plan_id: &str, - revision: &str, -) -> Result { - valid_id("workflow ID", workflow_id)?; - valid_id("decision ID", decision_id)?; - valid_id("plan ID", plan_id)?; - if revision.is_empty() || !revision.chars().all(|value| value.is_ascii_digit()) { - return Err(format!("invalid decision revision: {revision}")); - } - +pub fn sealed_iteration_plan_sha256(workflow_id: &str) -> Result { let store = Store::configured()?; let paths = store.paths(workflow_id)?; let state = read_env(&paths.state, workflow_id)?; if state_value(&state, "phase") != "pre-implementation" { - return Err("decision capsule requires phase=pre-implementation".into()); + return Err("plan-alignment review requires phase=pre-implementation".into()); } - validate_original_task(&state)?; - validate_contract(&state)?; - if revision != state_value(&state, "iteration") { - return Err(format!( - "decision revision does not match workflow iteration: requested={revision} current={}", - state_value(&state, "iteration") - )); + let digest = state_value(&state, "iteration_plan_sha256"); + if digest.len() != 64 || !digest.chars().all(|value| value.is_ascii_hexdigit()) { + return Err("plan-alignment review requires a sealed iteration plan".into()); } - validate_committed_decision(decision_id, plan_id)?; + Ok(digest.to_string()) +} - let decision_dir = store.state_dir.join("decisions").join(decision_id); - let decision = read_simple_env(&decision_dir.join("decision.env"))?; - let outcome = read_simple_env(&decision_dir.join("outcome.env"))?; - let selected = read_lines(&decision_dir.join("alternatives.tsv"))? - .into_iter() - .map(|line| parse_fields::<8>(&line)) - .find(|row| row[0] == plan_id) - .ok_or_else(|| format!("selected decision alternative is missing: {plan_id}"))?; +pub fn plan_alignment_binding( + workflow_id: &str, + plan_sha256: &str, +) -> Result { + let sealed = sealed_iteration_plan_sha256(workflow_id)?; + if sealed != plan_sha256 { + return Err("plan-alignment digest does not match the sealed iteration plan".into()); + } + let envelope = semantic_envelope(workflow_id)?; let value = serde_json::json!({ "apiVersion": "multiagent.moveindustries.io/v1", - "kind": "DecisionAuthorityCapsule", + "kind": "PlanAlignmentBinding", "workflowId": workflow_id, - "revision": revision, - "iterationPlan": { - "sha256": state_value(&state, "iteration_plan_sha256"), - "workerCount": state_value(&state, "iteration_worker_count"), - }, - "originalTaskSha256": state_value(&state, "original_task_sha256"), - "contractArtifactSha256": state_value(&state, "contract_artifact_sha256"), - "decision": { - "id": decision_id, - "title": state_value(&decision, "title"), - "owner": state_value(&decision, "owner"), - "status": state_value(&decision, "status"), - }, - "selectedPlan": { - "id": selected[0], - "summary": selected[1], - "proposedBy": selected[2], - "branch": selected[3], - "assignmentName": selected[4], - "expectedOutcome": selected[5], - "risk": selected[6], - "addedAt": selected[7], - }, - "outcome": { - "selectedPlan": state_value(&outcome, "selected_plan"), - "reason": state_value(&outcome, "reason"), - "rollbackPolicy": state_value(&outcome, "rollback_policy"), - "reflectionDue": state_value(&outcome, "reflection_due"), - "committedAt": state_value(&outcome, "committed_at"), - "status": state_value(&outcome, "status"), - }, + "originalTaskSha256": envelope.original_task_sha256, + "iterationPlanSha256": plan_sha256, }); let content = format!( "{}\n", serde_json::to_string(&value) - .map_err(|error| format!("encode decision authority capsule: {error}"))? + .map_err(|error| format!("encode plan-alignment binding: {error}"))? ); - let sha256 = format!("{:x}", Sha256::digest(content.as_bytes())); - Ok(DecisionAuthorityCapsule { + Ok(PlanAlignmentBinding { + sha256: format!("{:x}", Sha256::digest(content.as_bytes())), content, - sha256, - decision_id: decision_id.into(), - plan_id: plan_id.into(), - revision: revision.into(), + plan_sha256: plan_sha256.into(), + original_task_sha256: envelope.original_task_sha256, }) } @@ -322,18 +268,6 @@ pub fn passing_review_recorded( })) } -pub fn committed_decision_matches(decision_id: &str, plan_id: &str) -> Result { - let decision = config::state_dir()? - .join("decisions") - .join(decision_id) - .join("decision.env"); - if !decision.is_file() { - return Ok(false); - } - validate_committed_decision(decision_id, plan_id)?; - Ok(true) -} - pub fn contract_or_approved_context(workflow_id: &str) -> Result { let store = Store::configured()?; let p = store.paths(workflow_id)?; @@ -530,12 +464,9 @@ fn initialize_id(id: &str, resume: bool) -> Result<(), String> { ("contract_artifact", ""), ("contract_artifact_sha256", ""), ("preimplementation_gate", "pending"), - ("decision_id", ""), - ("plan_id", ""), - ("decision_revision", ""), ("implementation_context", ""), ("implementation_context_sha256", ""), - ("authority_review_id", ""), + ("alignment_review_id", ""), ("iteration_plan_sha256", ""), ("iteration_worker_count", ""), ("candidate_diff_hash", ""), @@ -728,7 +659,7 @@ fn context(args: &[String]) -> Result<(), String> { "workflowId": id, "phase": bounded_state_label(state_value(&state, "phase"), "phase")?, "iteration": bounded_state_label(state_value(&state, "iteration"), "iteration")?, - "stateRevision": bounded_state_label(state_value(&state, "decision_revision"), "state revision")?, + "stateRevision": bounded_state_label(state_value(&state, "iteration"), "state revision")?, "originalTask": { "path": task_path, "sha256": state_value(&state, "original_task_sha256"), @@ -843,14 +774,10 @@ fn prepare(args: &[String]) -> Result<(), String> { } let id = &args[0]; let o = options(&args[1..])?; - let decision = required(&o, "--decision-id")?; - let plan = required(&o, "--plan-id")?; - let revision = required(&o, "--decision-revision")?; + let plan_sha256 = required(&o, "--plan-sha256")?; let context_arg = required(&o, "--implementation-context")?; - let authority = required(&o, "--authority-review")?; - valid_id("decision ID", decision)?; - valid_id("plan ID", plan)?; - valid_id("review ID", authority)?; + let alignment_review = required(&o, "--alignment-review")?; + valid_id("review ID", alignment_review)?; let store = Store::configured()?; let p = store.paths(id)?; let _lock = store.lock(&p)?; @@ -859,20 +786,19 @@ fn prepare(args: &[String]) -> Result<(), String> { return Err("prepare-implementation requires phase=pre-implementation".into()); } let reviews = read_reviews(&p.reviews)?; - let authority_review = reviews + let alignment = reviews .iter() - .find(|r| r.get(0) == authority && r.get(1) == "decision-authority" && r.get(2) == "pass") - .ok_or_else(|| { - "prepare-implementation requires a passing decision-authority review".to_string() - })?; - let capsule = decision_authority_capsule(id, decision, plan, revision)?; + .find(|r| { + r.get(0) == alignment_review && r.get(1) == "plan-alignment" && r.get(2) == "pass" + }) + .ok_or_else(|| "prepare-implementation requires an aligned plan review".to_string())?; + if state_value(&state, "iteration_plan_sha256") != plan_sha256 { + return Err( + "prepare-implementation plan digest does not match the sealed iteration plan".into(), + ); + } if secure_reviewer_evidence() { - validate_decision_authority_capsule_evidence( - &store, - id, - authority_review.get(7), - &capsule, - )?; + validate_plan_alignment_evidence(&store, id, alignment.get(7), plan_sha256)?; } let todos = read_todos(&p.todos)?; let blockers: Vec<&str> = todos @@ -941,18 +867,11 @@ fn prepare(args: &[String]) -> Result<(), String> { )); } } - let capsule_path = p.base.join("decision-authority-capsule.json"); - atomic_write(&capsule_path, &capsule.content)?; for (key, value) in [ ("preimplementation_gate", "passed".to_string()), - ("decision_id", decision.to_string()), - ("plan_id", plan.to_string()), - ("decision_revision", revision.to_string()), - ("decision_capsule", capsule_path.display().to_string()), - ("decision_capsule_sha256", capsule.sha256.clone()), ("implementation_context", context.display().to_string()), ("implementation_context_sha256", sha256(&context)?), - ("authority_review_id", authority.to_string()), + ("alignment_review_id", alignment_review.to_string()), ("updated_at", timestamp()), ] { state.insert(key.into(), value); @@ -961,12 +880,9 @@ fn prepare(args: &[String]) -> Result<(), String> { event( &p.events, "implementation_prepared", - &format!( - "decision_id={decision}\tplan_id={plan}\trevision={revision}\tcapsule_sha256={}\treview_id={authority}", - capsule.sha256 - ), + &format!("plan_sha256={plan_sha256}\treview_id={alignment_review}"), )?; - println!("implementation prepared\t{id}\t{decision}\t{plan}"); + println!("implementation prepared\t{id}\t{plan_sha256}"); Ok(()) } @@ -1004,7 +920,7 @@ fn transition(args: &[String]) -> Result<(), String> { )); } if current == "pre-implementation" { - implementation_gate_state(&store, id, "", "", true)?; + implementation_gate_state(&store, id, "", true)?; state.insert("phase".into(), "implementation".into()); } else if current == "implementation" { if diff.is_empty() { @@ -1024,17 +940,6 @@ fn transition(args: &[String]) -> Result<(), String> { "source diff requires independent technical validation", iteration, )?; - if decision_drift_required(&store.state_dir, state_value(&state, "decision_id"))? { - ensure_review_obligation( - &mut obligations, - &format!("auto-{iteration}-decision-drift"), - "decision-drift", - "candidate-diff", - diff, - "material alternatives or assumptions require an independent drift check", - iteration, - )?; - } if iteration.parse::().unwrap_or(1) > 1 { ensure_review_obligation( &mut obligations, @@ -1053,12 +958,9 @@ fn transition(args: &[String]) -> Result<(), String> { } let iteration = state_value(&state, "iteration").parse::().unwrap_or(1) + 1; for key in [ - "decision_revision", - "decision_capsule", - "decision_capsule_sha256", "implementation_context", "implementation_context_sha256", - "authority_review_id", + "alignment_review_id", "iteration_plan_sha256", "iteration_worker_count", "candidate_diff_hash", @@ -1084,16 +986,6 @@ fn transition(args: &[String]) -> Result<(), String> { Ok(()) } -fn decision_drift_required(state_dir: &Path, decision_id: &str) -> Result { - if decision_id.is_empty() { - return Ok(true); - } - let directory = state_dir.join("decisions").join(decision_id); - let alternatives = read_lines(&directory.join("alternatives.tsv"))?.len(); - let assumptions = read_lines(&directory.join("assumptions.tsv"))?.len(); - Ok(alternatives > 1 || assumptions > 0) -} - fn add_todo(args: &[String]) -> Result<(), String> { if args.len() < 2 { return Err("add-todo requires WORKFLOW_ID TODO_ID".into()); @@ -1276,9 +1168,9 @@ fn record_review(args: &[String]) -> Result<(), String> { let p = store.paths(id)?; let _lock = store.lock(&p)?; let state = read_env(&p.state, id)?; - let diff = if kind == "decision-authority" { + let diff = if kind == "plan-alignment" { if state_value(&state, "phase") != "pre-implementation" { - return Err("decision-authority review requires phase=pre-implementation".into()); + return Err("plan-alignment review requires phase=pre-implementation".into()); } "-" } else { @@ -1315,7 +1207,7 @@ fn record_review(args: &[String]) -> Result<(), String> { ], }); write_reviews(&p.reviews, &rows)?; - if verdict == "pass" && kind != "decision-authority" { + if verdict == "pass" && kind != "plan-alignment" { let mut obligations = read_review_obligations(&p.review_obligations)?; let mut changed = false; for obligation in obligations.iter_mut().filter(|obligation| { @@ -1443,16 +1335,10 @@ fn gate(args: &[String]) -> Result<(), String> { let store = Store::configured()?; match args[1].as_str() { "implementation" => { - let state = implementation_gate_state( - &store, - id, - opt(&o, "--decision-id"), - opt(&o, "--plan-id"), - false, - )?; + let state = implementation_gate_state(&store, id, opt(&o, "--plan-sha256"), false)?; println!( "gate passed\t{id}\timplementation\t{}\t{}", - state_value(&state, "decision_revision"), + state_value(&state, "iteration_plan_sha256"), state_value(&state, "implementation_context_sha256") ); } @@ -1905,8 +1791,8 @@ fn parse_human_review_request(report: &str) -> Result, let first = lines.first().copied().unwrap_or(""); let reason = if first.eq_ignore_ascii_case("Verdict: HUMAN_REVIEW_REQUIRED") { Some("ops-verification") - } else if first.eq_ignore_ascii_case("verdict: user-choice-required") { - Some("decision-authority") + } else if first.eq_ignore_ascii_case("alignment: misaligned") { + Some("plan-alignment") } else { None }; @@ -1918,7 +1804,7 @@ fn parse_human_review_request(report: &str) -> Result, } else { &["user-question:"] }; - let questions = lines + let values = lines .iter() .filter_map(|line| { labels.iter().find_map(|label| { @@ -1927,6 +1813,12 @@ fn parse_human_review_request(report: &str) -> Result, .map(|_| line[label.len()..].trim()) }) }) + .collect::>(); + if reason == "plan-alignment" && values.len() == 1 && values[0].eq_ignore_ascii_case("none") { + return Ok(None); + } + let questions = values + .into_iter() .filter(|value| !value.is_empty() && !value.eq_ignore_ascii_case("none")) .collect::>(); if questions.len() != 1 { @@ -2189,14 +2081,11 @@ fn source_implementation_started(state: &BTreeMap) -> bool { "contract_scout", "contract_artifact", "contract_artifact_sha256", - "decision_id", - "plan_id", - "decision_revision", - "decision_capsule", - "decision_capsule_sha256", "implementation_context", "implementation_context_sha256", - "authority_review_id", + "alignment_review_id", + "iteration_plan_sha256", + "iteration_worker_count", "candidate_diff_hash", "reviewed_diff_hash", ] @@ -2220,8 +2109,7 @@ fn value(args: &[String]) -> Result<(), String> { fn implementation_gate_state( store: &Store, id: &str, - expected_decision: &str, - expected_plan: &str, + expected_plan_sha256: &str, allow_pre: bool, ) -> Result, String> { let p = store.paths(id)?; @@ -2248,17 +2136,10 @@ fn implementation_gate_state( blockers.join(",") )); } - if !expected_decision.is_empty() && expected_decision != state_value(&state, "decision_id") { - return Err(format!( - "assignment decision {expected_decision} does not match workflow decision {}", - state_value(&state, "decision_id") - )); - } - if !expected_plan.is_empty() && expected_plan != state_value(&state, "plan_id") { - return Err(format!( - "assignment plan {expected_plan} does not match workflow plan {}", - state_value(&state, "plan_id") - )); + if !expected_plan_sha256.is_empty() + && expected_plan_sha256 != state_value(&state, "iteration_plan_sha256") + { + return Err("assignment plan digest does not match the active workflow plan".into()); } Ok(state) } @@ -2508,31 +2389,32 @@ fn validate_reviewer_evidence( "reviewer {reviewer} final message is missing marker: {marker}" )); } - if secure && kind == "decision-authority" { - let capsule_hash = state_value(&metadata, "decision_capsule_sha256"); - if capsule_hash.is_empty() { + if kind == "plan-alignment" { + let plan_hash = state_value(&metadata, "iteration_plan_sha256"); + let task_hash = state_value(&metadata, "original_task_sha256"); + if plan_hash.is_empty() || task_hash.is_empty() { return Err(format!( - "decision-authority reviewer evidence has no supervisor decision capsule: {reviewer}" + "plan-alignment reviewer evidence has no sealed input digests: {reviewer}" )); } - let capsule_marker = - format!("decision-review: capsule-sha256={capsule_hash} verdict={verdict}"); + let alignment = if verdict == "pass" { + "aligned" + } else { + "misaligned" + }; + let binding_marker = format!( + "plan-alignment-review: plan-sha256={plan_hash} original-task-sha256={task_hash} alignment={alignment}" + ); if !message .lines() - .any(|line| review_marker_matches(line, &capsule_marker)) + .any(|line| review_marker_matches(line, &binding_marker)) { return Err(format!( - "reviewer {reviewer} final message is missing marker: {capsule_marker}" - )); - } - let capsule_path = dir.join("decision-capsule.json"); - if !capsule_path.is_file() || sha256(&capsule_path)? != capsule_hash { - return Err(format!( - "decision-authority reviewer capsule evidence is missing or changed: {reviewer}" + "reviewer {reviewer} final message is missing marker: {binding_marker}" )); } } - if matches!(kind, "decision-authority" | "technical") { + if matches!(kind, "plan-alignment" | "technical") { let p = store.paths(workflow_id)?; let state = read_env(&p.state, workflow_id)?; validate_original_task(&state)?; @@ -2555,32 +2437,31 @@ fn validate_reviewer_evidence( Ok(()) } -fn validate_decision_authority_capsule_evidence( +fn validate_plan_alignment_evidence( store: &Store, workflow_id: &str, reviewer: &str, - capsule: &DecisionAuthorityCapsule, + plan_sha256: &str, ) -> Result<(), String> { valid_id("reviewer name", reviewer)?; let directory = store.state_dir.join("reviewer-evidence").join(reviewer); let metadata = read_simple_env(&directory.join("evidence.env"))?; for (key, expected) in [ ("workflow_id", workflow_id), - ("decision_id", capsule.decision_id.as_str()), - ("plan_id", capsule.plan_id.as_str()), - ("decision_revision", capsule.revision.as_str()), - ("decision_capsule_sha256", capsule.sha256.as_str()), + ("iteration_plan_sha256", plan_sha256), ] { if state_value(&metadata, key) != expected { return Err(format!( - "decision-authority review binding mismatch: {key} expected={expected} actual={}", + "plan-alignment review binding mismatch: {key} expected={expected} actual={}", state_value(&metadata, key) )); } } - let capsule_path = directory.join("decision-capsule.json"); - if !capsule_path.is_file() || sha256(&capsule_path)? != capsule.sha256 { - return Err("decision-authority review capsule does not match committed decision".into()); + let binding_hash = state_value(&metadata, "alignment_binding_sha256"); + let binding_path = directory.join("plan-alignment-binding.json"); + if binding_hash.is_empty() || !binding_path.is_file() || sha256(&binding_path)? != binding_hash + { + return Err("plan-alignment review binding evidence is missing or changed".into()); } Ok(()) } @@ -2804,27 +2685,6 @@ fn contract_rule_statement(rule: &str) -> &str { }) .unwrap_or("") } -fn validate_committed_decision(decision: &str, plan: &str) -> Result<(), String> { - let dir = config::state_dir()?.join("decisions").join(decision); - let meta = read_simple_env(&dir.join("decision.env"))?; - let outcome = read_simple_env(&dir.join("outcome.env"))?; - if state_value(&meta, "status") != "committed" { - return Err(format!("decision ledger is not committed: {decision}")); - } - let selected = state_value(&outcome, "selected_plan"); - if selected != plan { - return Err(format!( - "decision ledger selected plan {} does not match requested plan {plan}", - if selected.is_empty() { - "missing" - } else { - selected - } - )); - } - Ok(()) -} - fn read_env(path: &Path, id: &str) -> Result, String> { if !path.is_file() { return Err(format!("workflow lifecycle does not exist: {id}")); @@ -3013,9 +2873,9 @@ mod tests { let mut state = BTreeMap::from([("preimplementation_gate".into(), "pending".into())]); assert!(!source_implementation_started(&state)); - state.insert("decision_id".into(), "DEC-1".into()); + state.insert("iteration_plan_sha256".into(), "abc".into()); assert!(source_implementation_started(&state)); - state.remove("decision_id"); + state.remove("iteration_plan_sha256"); state.insert("contract_artifact".into(), "/tmp/contract.md".into()); assert!(source_implementation_started(&state)); @@ -3039,6 +2899,21 @@ mod tests { )); } + #[test] + fn plan_misalignment_routes_only_genuine_questions_to_the_user() { + let replan = "alignment: misaligned\nfindings: missed main task\nuser-question: none\n"; + assert_eq!(parse_human_review_request(replan).unwrap(), None); + + let question = "alignment: misaligned\nfindings: missing target\nuser-question: Which target repository should be changed?\n"; + assert_eq!( + parse_human_review_request(question).unwrap(), + Some(( + "Which target repository should be changed?".into(), + "plan-alignment".into() + )) + ); + } + #[test] fn read_only_shortcut_rejects_any_writer_launch() { let launch = |role: &str, access: &str, state: &str| { @@ -3082,47 +2957,6 @@ mod tests { ); } - #[test] - fn decision_drift_policy_tracks_material_choice_or_assumption() { - let root = std::env::temp_dir().join(format!( - "multiagent-drift-policy-test-{}", - std::process::id() - )); - let decision = root.join("decisions/decision-1"); - fs::create_dir_all(&decision).unwrap(); - fs::write( - decision.join("alternatives.tsv"), - "plan_id\tsummary\nplan-1\tone\n", - ) - .unwrap(); - fs::write( - decision.join("assumptions.tsv"), - "assumption_id\tstatement\n", - ) - .unwrap(); - assert!(!decision_drift_required(&root, "decision-1").unwrap()); - - fs::write( - decision.join("alternatives.tsv"), - "plan_id\tsummary\nplan-1\tone\nplan-2\ttwo\n", - ) - .unwrap(); - assert!(decision_drift_required(&root, "decision-1").unwrap()); - - fs::write( - decision.join("alternatives.tsv"), - "plan_id\tsummary\nplan-1\tone\n", - ) - .unwrap(); - fs::write( - decision.join("assumptions.tsv"), - "assumption_id\tstatement\na-1\tmaterial unknown\n", - ) - .unwrap(); - assert!(decision_drift_required(&root, "decision-1").unwrap()); - fs::remove_dir_all(root).unwrap(); - } - #[test] fn embedding_tasks_require_positive_and_negative_structural_rules() { let task = "WidgetConfig fields were embedded unnecessarily."; diff --git a/tests/lifecycle.sh b/tests/lifecycle.sh index 877e67a..92c9ab5 100755 --- a/tests/lifecycle.sh +++ b/tests/lifecycle.sh @@ -41,11 +41,10 @@ assert_contains "$TEST_TMP/lifecycle-env-bypass.out" \ "lifecycle enforcement requires --workflow-id" IMPLEMENTATION_CONTEXT="$TEST_TMP/approved-implementation-context.md" +PLAN_SHA="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" printf '%s\n' \ '# Approved implementation context' \ - 'decision: DEC-1' \ - 'plan: PLAN-1' \ - 'authority: orchestrator' \ + 'plan: implement the authenticated request' \ 'must-not-do: change public behavior' >"$IMPLEMENTATION_CONTEXT" PROMPT_BUNDLE="$TEST_TMP/orchestrator-bundle.md" @@ -195,45 +194,44 @@ wf init WF-LIFECYCLE >/dev/null wf init WF-REVIEW-EVIDENCE >/dev/null if MULTIAGENT_STATE_DIR="$TEST_STATE" MULTIAGENT_LIFECYCLE_ENFORCEMENT=1 \ "$MULTIAGENT" workflow record-review WF-REVIEW-EVIDENCE AUTH-MISSING \ - --type decision-authority --verdict pass --evidence "claimed pass" \ + --type plan-alignment --verdict pass --evidence "claimed pass" \ >"$TEST_TMP/missing-reviewer-evidence.out" 2>&1; then echo "expected enforced review without reviewer evidence to fail" >&2 exit 1 fi assert_contains "$TEST_TMP/missing-reviewer-evidence.out" "requires --reviewer NAME" -REVIEWER_STATE="$TEST_STATE/subagents/authority-reviewer-test" +REVIEWER_STATE="$TEST_STATE/subagents/plan-alignment-reviewer-test" mkdir -p "$REVIEWER_STATE" -printf '%s\n' 'role=reviewer' 'codex_access=read-only' >"$REVIEWER_STATE/meta.env" +printf '%s\n' 'role=reviewer' 'codex_access=read-only' \ + "iteration_plan_sha256=$PLAN_SHA" \ + 'original_task_sha256=1111111111111111111111111111111111111111111111111111111111111111' \ + >"$REVIEWER_STATE/meta.env" printf 'finalized\n' >"$REVIEWER_STATE/status" printf '2026-08-15T00:00:00Z\n' >"$REVIEWER_STATE/finalized_at" -printf 'review-record: type=decision-authority verdict=pass diff=-\n' \ +printf 'review-record: type=plan-alignment verdict=pass diff=-\n' \ >"$REVIEWER_STATE/last-message.txt" +printf 'plan-alignment-review: plan-sha256=%s original-task-sha256=%s alignment=aligned\n' \ + "$PLAN_SHA" '1111111111111111111111111111111111111111111111111111111111111111' \ + >>"$REVIEWER_STATE/last-message.txt" MULTIAGENT_STATE_DIR="$TEST_STATE" MULTIAGENT_LIFECYCLE_ENFORCEMENT=1 \ "$MULTIAGENT" workflow record-review WF-REVIEW-EVIDENCE AUTH-DURABLE \ - --type decision-authority --verdict pass --evidence "durable reviewer pass" \ - --reviewer authority-reviewer-test >/dev/null - -MULTIAGENT_STATE_DIR="$TEST_STATE" "$MULTIAGENT" decision init DEC-1 \ - --title "Lifecycle decision" --owner orchestrator >/dev/null -MULTIAGENT_STATE_DIR="$TEST_STATE" "$MULTIAGENT" decision add-alternative DEC-1 \ - --plan-id PLAN-1 --summary "Implement approved lifecycle plan" \ - --proposed-by orchestrator >/dev/null -MULTIAGENT_STATE_DIR="$TEST_STATE" "$MULTIAGENT" decision commit DEC-1 \ - --selected-plan PLAN-1 --reason "Authority review and evidence support this plan" >/dev/null + --type plan-alignment --verdict pass --evidence "durable reviewer pass" \ + --reviewer plan-alignment-reviewer-test >/dev/null if wf transition WF-LIFECYCLE implementation >"$TEST_TMP/no-permit.out" 2>&1; then echo "expected implementation without a permit to fail" >&2 exit 1 fi assert_contains "$TEST_TMP/no-permit.out" "implementation gate has not passed" -wf record-review WF-LIFECYCLE AUTH-1 \ - --type decision-authority --verdict pass \ - --evidence "independent authority review passed" >/dev/null +wf seal-iteration WF-LIFECYCLE --plan-sha256 "$PLAN_SHA" --worker-count 1 >/dev/null +wf record-review WF-LIFECYCLE ALIGN-1 \ + --type plan-alignment --verdict pass \ + --evidence "independent plan alignment passed" >/dev/null wf add-todo WF-LIFECYCLE TODO-EVIDENCE \ --kind evidence --summary "inspect persisted state" >/dev/null if wf prepare-implementation WF-LIFECYCLE \ - --decision-id DEC-1 --plan-id PLAN-1 --decision-revision 1 \ - --implementation-context "$IMPLEMENTATION_CONTEXT" --authority-review AUTH-1 \ + --plan-sha256 "$PLAN_SHA" \ + --implementation-context "$IMPLEMENTATION_CONTEXT" --alignment-review ALIGN-1 \ >"$TEST_TMP/evidence-open.out" 2>&1; then echo "expected active evidence TODO to block implementation" >&2 exit 1 @@ -242,21 +240,22 @@ assert_contains "$TEST_TMP/evidence-open.out" "active evidence/decision TODOs" wf resolve-todo WF-LIFECYCLE TODO-EVIDENCE \ --resolution completed --evidence "state inspected" >/dev/null wf prepare-implementation WF-LIFECYCLE \ - --decision-id DEC-1 --plan-id PLAN-1 --decision-revision 1 \ - --implementation-context "$IMPLEMENTATION_CONTEXT" --authority-review AUTH-1 >/dev/null + --plan-sha256 "$PLAN_SHA" \ + --implementation-context "$IMPLEMENTATION_CONTEXT" --alignment-review ALIGN-1 >/dev/null wf transition WF-LIFECYCLE implementation >/dev/null MULTIAGENT_ROOT="$TEST_REPO" MULTIAGENT_STATE_DIR="$TEST_STATE" \ MULTIAGENT_WORKFLOW_ID=WF-LIFECYCLE MULTIAGENT_LIFECYCLE_ENFORCEMENT=1 \ "$MULTIAGENT" subagent assignment-create worker-lifecycle \ --assignment-id LIFE-1 --role exploitation \ - --workflow-id WF-LIFECYCLE --decision-id DEC-1 --plan-id PLAN-1 \ + --workflow-id WF-LIFECYCLE \ --branch "$TEST_BRANCH" --owned README.md >/dev/null -assert_contains "$TEST_STATE/assignments/worker-lifecycle/assignment.env" "decision_revision=1" +assert_contains "$TEST_STATE/assignments/worker-lifecycle/assignment.env" "iteration=1" +assert_contains "$TEST_STATE/assignments/worker-lifecycle/assignment.env" "iteration_plan_sha256=$PLAN_SHA" assert_contains "$TEST_STATE/assignments/worker-lifecycle/assignment.env" "implementation_context_sha256=" printf '\ncontext drift\n' >>"$IMPLEMENTATION_CONTEXT" -if wf gate WF-LIFECYCLE implementation --decision-id DEC-1 --plan-id PLAN-1 \ +if wf gate WF-LIFECYCLE implementation --plan-sha256 "$PLAN_SHA" \ >"$TEST_TMP/context-drift.out" 2>&1; then echo "expected changed implementation context to invalidate the implementation gate" >&2 exit 1 @@ -300,33 +299,32 @@ EOF MULTIAGENT_STATE_DIR="$CONTRACT_STATE" "$MULTIAGENT" workflow contract-register \ WF-CONTRACT --scout contract-scout-01-widget >/dev/null CONTRACT_HASH="$(MULTIAGENT_STATE_DIR="$CONTRACT_STATE" "$MULTIAGENT" workflow value WF-CONTRACT contract_artifact_sha256)" +CONTRACT_PLAN_SHA="bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" +MULTIAGENT_STATE_DIR="$CONTRACT_STATE" "$MULTIAGENT" workflow seal-iteration \ + WF-CONTRACT --plan-sha256 "$CONTRACT_PLAN_SHA" --worker-count 1 >/dev/null -CONTRACT_REVIEWER="$CONTRACT_STATE/subagents/decision-authority-reviewer-contract" +CONTRACT_REVIEWER="$CONTRACT_STATE/subagents/plan-alignment-reviewer-contract" mkdir -p "$CONTRACT_REVIEWER" printf '%s\n' 'role=reviewer' 'codex_access=read-only' 'workflow_id=WF-CONTRACT' \ + "iteration_plan_sha256=$CONTRACT_PLAN_SHA" \ + 'original_task_sha256=2222222222222222222222222222222222222222222222222222222222222222' \ >"$CONTRACT_REVIEWER/meta.env" printf 'finalized\n' >"$CONTRACT_REVIEWER/status" printf '2026-08-17T00:00:00Z\n' >"$CONTRACT_REVIEWER/finalized_at" printf '%s\n' \ - 'review-record: type=decision-authority verdict=pass diff=-' \ + 'review-record: type=plan-alignment verdict=pass diff=-' \ + "plan-alignment-review: plan-sha256=$CONTRACT_PLAN_SHA original-task-sha256=2222222222222222222222222222222222222222222222222222222222222222 alignment=aligned" \ "contract-review: artifact-sha256=$CONTRACT_HASH verdict=pass" \ >"$CONTRACT_REVIEWER/last-message.txt" MULTIAGENT_STATE_DIR="$CONTRACT_STATE" MULTIAGENT_LIFECYCLE_ENFORCEMENT=1 \ - "$MULTIAGENT" workflow record-review WF-CONTRACT AUTH-CONTRACT \ - --type decision-authority --verdict pass --evidence "contract preserved" \ - --reviewer decision-authority-reviewer-contract >/dev/null -MULTIAGENT_STATE_DIR="$CONTRACT_STATE" "$MULTIAGENT" decision init DEC-CONTRACT \ - --title "Contract plan" --owner orchestrator >/dev/null -MULTIAGENT_STATE_DIR="$CONTRACT_STATE" "$MULTIAGENT" decision add-alternative DEC-CONTRACT \ - --plan-id PLAN-CONTRACT --summary "Apply the registered contract" \ - --proposed-by orchestrator >/dev/null -MULTIAGENT_STATE_DIR="$CONTRACT_STATE" "$MULTIAGENT" decision commit DEC-CONTRACT \ - --selected-plan PLAN-CONTRACT --reason "authority reviewer accepted the full artifact" >/dev/null + "$MULTIAGENT" workflow record-review WF-CONTRACT ALIGN-CONTRACT \ + --type plan-alignment --verdict pass --evidence "contract preserved" \ + --reviewer plan-alignment-reviewer-contract >/dev/null CONTRACT_CONTEXT="$TEST_TMP/contract-context.md" printf '# Compressed context that omits the negative structural rule\n' >"$CONTRACT_CONTEXT" if MULTIAGENT_STATE_DIR="$CONTRACT_STATE" "$MULTIAGENT" workflow prepare-implementation \ - WF-CONTRACT --decision-id DEC-CONTRACT --plan-id PLAN-CONTRACT --decision-revision 1 \ - --implementation-context "$CONTRACT_CONTEXT" --authority-review AUTH-CONTRACT \ + WF-CONTRACT --plan-sha256 "$CONTRACT_PLAN_SHA" \ + --implementation-context "$CONTRACT_CONTEXT" --alignment-review ALIGN-CONTRACT \ >"$TEST_TMP/contract-compression.out" 2>&1; then echo "expected compressed implementation context to be rejected" >&2 exit 1 @@ -336,8 +334,8 @@ assert_contains "$TEST_TMP/contract-compression.out" \ printf 'contract-artifact-sha256=%s\n' "$CONTRACT_HASH" >"$CONTRACT_CONTEXT" cat "$CONTRACT_SCOUT/last-message.txt" >>"$CONTRACT_CONTEXT" MULTIAGENT_STATE_DIR="$CONTRACT_STATE" "$MULTIAGENT" workflow prepare-implementation \ - WF-CONTRACT --decision-id DEC-CONTRACT --plan-id PLAN-CONTRACT --decision-revision 1 \ - --implementation-context "$CONTRACT_CONTEXT" --authority-review AUTH-CONTRACT >/dev/null + WF-CONTRACT --plan-sha256 "$CONTRACT_PLAN_SHA" \ + --implementation-context "$CONTRACT_CONTEXT" --alignment-review ALIGN-CONTRACT >/dev/null MULTIAGENT_STATE_DIR="$CONTRACT_STATE" "$MULTIAGENT" workflow transition \ WF-CONTRACT implementation >/dev/null printf '\nmutated\n' >>"$CONTRACT_SCOUT/last-message.txt" @@ -355,18 +353,13 @@ loop() { MULTIAGENT_STATE_DIR="$LOOP_STATE" "$MULTIAGENT" workflow "$@" } loop init WF-LOOP >/dev/null -MULTIAGENT_STATE_DIR="$LOOP_STATE" "$MULTIAGENT" decision init DEC-LOOP \ - --title "Loop decision" --owner orchestrator >/dev/null -MULTIAGENT_STATE_DIR="$LOOP_STATE" "$MULTIAGENT" decision add-alternative DEC-LOOP \ - --plan-id PLAN-LOOP --summary "Implement and re-evaluate findings" \ - --proposed-by orchestrator >/dev/null -MULTIAGENT_STATE_DIR="$LOOP_STATE" "$MULTIAGENT" decision commit DEC-LOOP \ - --selected-plan PLAN-LOOP --reason "Recorded lifecycle plan" >/dev/null -loop record-review WF-LOOP AUTH-LOOP \ - --type decision-authority --verdict pass --evidence "authority passed" >/dev/null +LOOP_PLAN_SHA="cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" +loop seal-iteration WF-LOOP --plan-sha256 "$LOOP_PLAN_SHA" --worker-count 1 >/dev/null +loop record-review WF-LOOP ALIGN-LOOP \ + --type plan-alignment --verdict pass --evidence "plan aligned" >/dev/null loop prepare-implementation WF-LOOP \ - --decision-id DEC-LOOP --plan-id PLAN-LOOP --decision-revision 1 \ - --implementation-context "$LOOP_CONTEXT" --authority-review AUTH-LOOP >/dev/null + --plan-sha256 "$LOOP_PLAN_SHA" \ + --implementation-context "$LOOP_CONTEXT" --alignment-review ALIGN-LOOP >/dev/null loop transition WF-LOOP implementation >/dev/null loop transition WF-LOOP post-implementation --diff-hash DIFF-LOOP >/dev/null loop record-review WF-LOOP TECH-FINDING \ @@ -377,17 +370,19 @@ loop add-todo WF-LOOP TODO-FOLLOWUP \ loop transition WF-LOOP pre-implementation >/dev/null assert_contains "$LOOP_STATE/workflows/WF-LOOP/lifecycle/lifecycle.env" "iteration=2" -loop record-review WF-LOOP AUTH-LOOP-2 \ - --type decision-authority --verdict pass --evidence "revised authority passed" >/dev/null +LOOP_PLAN_SHA_2="dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" +loop seal-iteration WF-LOOP --plan-sha256 "$LOOP_PLAN_SHA_2" --worker-count 1 >/dev/null +loop record-review WF-LOOP ALIGN-LOOP-2 \ + --type plan-alignment --verdict pass --evidence "revised plan aligned" >/dev/null printf 'revision 2\n' >"$LOOP_CONTEXT" loop prepare-implementation WF-LOOP \ - --decision-id DEC-LOOP --plan-id PLAN-LOOP --decision-revision 2 \ - --implementation-context "$LOOP_CONTEXT" --authority-review AUTH-LOOP-2 >/dev/null + --plan-sha256 "$LOOP_PLAN_SHA_2" \ + --implementation-context "$LOOP_CONTEXT" --alignment-review ALIGN-LOOP-2 >/dev/null loop transition WF-LOOP implementation >/dev/null loop transition WF-LOOP post-implementation --diff-hash DIFF-FINAL >/dev/null loop resolve-todo WF-LOOP TODO-FOLLOWUP \ --resolution completed --evidence "repair and verifier recheck passed" >/dev/null -for review_type in decision-drift scope technical reflection; do +for review_type in scope technical reflection; do if [[ "$review_type" == "technical" ]]; then reviewer_name="verifier-technical" else diff --git a/tests/malicious-orchestrator.sh b/tests/malicious-orchestrator.sh index d5f2cf0..ca1b006 100755 --- a/tests/malicious-orchestrator.sh +++ b/tests/malicious-orchestrator.sh @@ -88,10 +88,10 @@ else fi printf 'escaped\n' >"$TEST_REPO/forbidden/secret.txt" 2>/dev/null || true final_hash="$(cat "${TEST_REPO%/repo}/review-hash" 2>/dev/null || true)" -capsule_hash="$(printf '%s\n' "$prompt" | sed -n 's/^decision-capsule-sha256=//p' | head -n 1)" -printf 'ACCEPTED\nbuild-verification-passed: final-diff-sha256=%s compile_clean=true returncode=0\nreview-record: type=decision-authority verdict=pass diff=-\n' "$final_hash" >"$output" -if [[ -n "$capsule_hash" && "$output" != *decision-authority-reviewer-missing* ]]; then - printf 'decision-review: capsule-sha256=%s verdict=pass\n' "$capsule_hash" >>"$output" +alignment_marker="$(printf '%s\n' "$prompt" | sed -n 's/^Aligned marker: //p' | head -n 1)" +printf 'alignment: aligned\nfindings: none\nuser-question: none\nbuild-verification-passed: final-diff-sha256=%s compile_clean=true returncode=0\nreview-record: type=plan-alignment verdict=pass diff=-\n' "$final_hash" >"$output" +if [[ -n "$alignment_marker" && "$output" != *plan-alignment-reviewer-missing* ]]; then + printf '%s\n' "$alignment_marker" >>"$output" fi printf '{"type":"result","result":"completed"}\n' FAKE_CODEX @@ -160,6 +160,9 @@ as_ops() { } as_orchestrator "$MULTIAGENT" workflow init WF-ATTACK >/dev/null +PLAN_SHA="ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" +as_orchestrator "$MULTIAGENT" workflow seal-iteration WF-ATTACK \ + --plan-sha256 "$PLAN_SHA" --worker-count 1 >/dev/null # Exercise the real reader UID and Landlock boundary. Reaching the intentionally # missing runbook error proves the supervisor read and decoded the reader-owned @@ -268,77 +271,68 @@ if setpriv --reuid=10001 --regid=10001 --clear-groups env "${BASE_ENV[@]}" \ fi as_orchestrator mkdir -p "$STATE/subagents/forged-reviewer" -as_orchestrator sh -c 'printf "%s\n" "role=reviewer" "codex_access=read-only" >"$1/meta.env"; printf finalized >"$1/status"; printf now >"$1/finalized_at"; printf "%s\n" "review-record: type=decision-authority verdict=pass diff=-" >"$1/last-message.txt"' \ +as_orchestrator sh -c 'printf "%s\n" "role=reviewer" "codex_access=read-only" >"$1/meta.env"; printf finalized >"$1/status"; printf now >"$1/finalized_at"; printf "%s\n" "review-record: type=plan-alignment verdict=pass diff=-" >"$1/last-message.txt"' \ sh "$STATE/subagents/forged-reviewer" if as_orchestrator "$MULTIAGENT" workflow record-review WF-ATTACK FORGED \ - --type decision-authority --verdict pass --evidence forged \ + --type plan-alignment --verdict pass --evidence forged \ --reviewer forged-reviewer >/dev/null 2>&1; then echo "workflow accepted forged reviewer evidence" >&2 exit 1 fi -as_orchestrator "$MULTIAGENT" decision init ATTACK-DECISION \ - --title "Boundary authority decision" --owner orchestrator >/dev/null -as_orchestrator "$MULTIAGENT" decision add-alternative ATTACK-DECISION \ - --plan-id ATTACK-PLAN --summary "Exercise sealed authority evidence" \ - --proposed-by orchestrator --expected-outcome "review remains digest bound" \ - --risk high >/dev/null -as_orchestrator "$MULTIAGENT" decision commit ATTACK-DECISION \ - --selected-plan ATTACK-PLAN --reason "Exercise decision capsule boundary" >/dev/null - -AUTHORITY_REVIEWER="decision-authority-reviewer-attack" -as_orchestrator mkdir -p "$STATE/subagents/$AUTHORITY_REVIEWER" -as_orchestrator sh -c 'printf "%s\n" "perform independent authority review" >"$1"' sh \ - "$STATE/subagents/$AUTHORITY_REVIEWER/instruction.txt" -as_orchestrator "$MULTIAGENT" supervisor register-launch "$AUTHORITY_REVIEWER" \ +ALIGNMENT_REVIEWER="plan-alignment-reviewer-attack" +as_orchestrator mkdir -p "$STATE/subagents/$ALIGNMENT_REVIEWER" +as_orchestrator sh -c 'printf "%s\n" "compare sealed plan with original task" >"$1"' sh \ + "$STATE/subagents/$ALIGNMENT_REVIEWER/instruction.txt" +as_orchestrator "$MULTIAGENT" supervisor register-launch "$ALIGNMENT_REVIEWER" \ --role reviewer --cli codex --cli-bin "$TEST_ROOT/bin/codex" \ - --instruction-file "$STATE/subagents/$AUTHORITY_REVIEWER/instruction.txt" \ - --decision-id ATTACK-DECISION --plan-id ATTACK-PLAN --decision-revision 1 >/dev/null -as_orchestrator "$MULTIAGENT" role-agent-exec "$AUTHORITY_REVIEWER" -as_orchestrator sh -c 'printf "%s\n" "review-record: type=decision-authority verdict=findings diff=-" >"$1"' sh \ - "$STATE/subagents/$AUTHORITY_REVIEWER/last-message.txt" + --instruction-file "$STATE/subagents/$ALIGNMENT_REVIEWER/instruction.txt" \ + --plan-sha256 "$PLAN_SHA" >/dev/null +as_orchestrator "$MULTIAGENT" role-agent-exec "$ALIGNMENT_REVIEWER" +as_orchestrator sh -c 'printf "%s\n" "review-record: type=plan-alignment verdict=findings diff=-" >"$1"' sh \ + "$STATE/subagents/$ALIGNMENT_REVIEWER/last-message.txt" as_orchestrator "$MULTIAGENT" workflow record-review WF-ATTACK SEALED \ - --type decision-authority --verdict pass --evidence sealed \ - --reviewer "$AUTHORITY_REVIEWER" >/dev/null - -MISSING_CAPSULE_REVIEWER="decision-authority-reviewer-missing" -as_orchestrator mkdir -p "$STATE/subagents/$MISSING_CAPSULE_REVIEWER" -as_orchestrator sh -c 'printf "%s\n" "omit the required capsule marker" >"$1"' sh \ - "$STATE/subagents/$MISSING_CAPSULE_REVIEWER/instruction.txt" -as_orchestrator "$MULTIAGENT" supervisor register-launch "$MISSING_CAPSULE_REVIEWER" \ + --type plan-alignment --verdict pass --evidence sealed \ + --reviewer "$ALIGNMENT_REVIEWER" >/dev/null + +MISSING_BINDING_REVIEWER="plan-alignment-reviewer-missing" +as_orchestrator mkdir -p "$STATE/subagents/$MISSING_BINDING_REVIEWER" +as_orchestrator sh -c 'printf "%s\n" "omit the required alignment marker" >"$1"' sh \ + "$STATE/subagents/$MISSING_BINDING_REVIEWER/instruction.txt" +as_orchestrator "$MULTIAGENT" supervisor register-launch "$MISSING_BINDING_REVIEWER" \ --role reviewer --cli codex --cli-bin "$TEST_ROOT/bin/codex" \ - --instruction-file "$STATE/subagents/$MISSING_CAPSULE_REVIEWER/instruction.txt" \ - --decision-id ATTACK-DECISION --plan-id ATTACK-PLAN --decision-revision 1 >/dev/null -as_orchestrator "$MULTIAGENT" role-agent-exec "$MISSING_CAPSULE_REVIEWER" -if as_orchestrator "$MULTIAGENT" workflow record-review WF-ATTACK MISSING-CAPSULE \ - --type decision-authority --verdict pass --evidence "missing capsule marker" \ - --reviewer "$MISSING_CAPSULE_REVIEWER" >/dev/null 2>&1; then - echo "workflow accepted authority evidence without its decision capsule marker" >&2 + --instruction-file "$STATE/subagents/$MISSING_BINDING_REVIEWER/instruction.txt" \ + --plan-sha256 "$PLAN_SHA" >/dev/null +as_orchestrator "$MULTIAGENT" role-agent-exec "$MISSING_BINDING_REVIEWER" +if as_orchestrator "$MULTIAGENT" workflow record-review WF-ATTACK MISSING-BINDING \ + --type plan-alignment --verdict pass --evidence "missing alignment marker" \ + --reviewer "$MISSING_BINDING_REVIEWER" >/dev/null 2>&1; then + echo "workflow accepted alignment evidence without its binding marker" >&2 exit 1 fi ATTACK_CONTEXT="$TEST_ROOT/attack-context.md" printf 'approved context\n' >"$ATTACK_CONTEXT" if as_orchestrator "$MULTIAGENT" workflow prepare-implementation WF-ATTACK \ - --decision-id ATTACK-DECISION --plan-id ATTACK-PLAN --decision-revision 2 \ - --implementation-context "$ATTACK_CONTEXT" --authority-review SEALED \ + --plan-sha256 "0000000000000000000000000000000000000000000000000000000000000000" \ + --implementation-context "$ATTACK_CONTEXT" --alignment-review SEALED \ >/dev/null 2>&1; then - echo "workflow accepted authority evidence for a different decision revision" >&2 + echo "workflow accepted alignment evidence for a different plan digest" >&2 exit 1 fi as_orchestrator "$MULTIAGENT" workflow prepare-implementation WF-ATTACK \ - --decision-id ATTACK-DECISION --plan-id ATTACK-PLAN --decision-revision 1 \ - --implementation-context "$ATTACK_CONTEXT" --authority-review SEALED >/dev/null -grep -Eq '^decision_capsule_sha256=[0-9a-f]{64}$' \ + --plan-sha256 "$PLAN_SHA" \ + --implementation-context "$ATTACK_CONTEXT" --alignment-review SEALED >/dev/null +grep -Eq '^alignment_review_id=SEALED$' \ "$STATE/workflows/WF-ATTACK/lifecycle/lifecycle.env" cmp \ - "$STATE/workflows/WF-ATTACK/lifecycle/decision-authority-capsule.json" \ - "$STATE/reviewer-evidence/$AUTHORITY_REVIEWER/decision-capsule.json" + "$STATE/launch-authorizations/$ALIGNMENT_REVIEWER/plan-alignment-binding.json" \ + "$STATE/reviewer-evidence/$ALIGNMENT_REVIEWER/plan-alignment-binding.json" as_orchestrator sh -c 'printf "ACCEPTED\nbuild-verification-passed: final-diff-sha256=%s compile_clean=true returncode=0\n" "$2" >"$1"' sh \ - "$STATE/subagents/$AUTHORITY_REVIEWER/last-message.txt" "$BOUNDARY_HASH" + "$STATE/subagents/$ALIGNMENT_REVIEWER/last-message.txt" "$BOUNDARY_HASH" # Keep implementation verification distinct from the pre-implementation -# authority review. The completion gate recognizes only a technical verifier +# alignment review. The completion gate recognizes only a technical verifier # as evidence that the candidate diff was independently checked. TECHNICAL_VERIFIER="technical-verifier-attack" as_orchestrator mkdir -p "$STATE/subagents/$TECHNICAL_VERIFIER" @@ -372,7 +366,7 @@ if as_orchestrator "$MULTIAGENT" subagent todo-close closure-todo \ exit 1 fi as_orchestrator "$MULTIAGENT" subagent todo-close closure-todo \ - --verified-by "$AUTHORITY_REVIEWER" \ + --verified-by "$ALIGNMENT_REVIEWER" \ --recheck-json "{\"accepted\":true,\"finding_rechecked\":\"closure-finding\",\"final_diff_sha256\":\"$BOUNDARY_HASH\",\"commands\":[{\"cmd\":\"true\",\"rc\":0}]}" \ >/dev/null @@ -395,7 +389,7 @@ if as_orchestrator "$MULTIAGENT" subagent finding-dismiss supersession-finding \ fi grep -Fxq open "$STATE/todos/supersession-todo/status" as_orchestrator "$MULTIAGENT" subagent finding-dismiss supersession-finding \ - --verified-by "$AUTHORITY_REVIEWER" \ + --verified-by "$ALIGNMENT_REVIEWER" \ --recheck-json "{\"accepted\":true,\"source_finding_id\":\"supersession-finding\",\"disposition\":\"superseded\",\"evidence\":\"sealed reviewer adjudicated the stale requirement\",\"final_diff_sha256\":\"$BOUNDARY_HASH\"}" \ >/dev/null grep -Fxq superseded "$STATE/todos/supersession-todo/status" @@ -439,7 +433,7 @@ grep -Fq '"reason": "canceled"' \ "$STATE/logs/agents/reader-cleanup/supervisor-termination.json" if as_orchestrator sh -c 'printf forged >"$1"' sh \ - "$STATE/reviewer-evidence/$AUTHORITY_REVIEWER/last-message.txt" 2>/dev/null; then + "$STATE/reviewer-evidence/$ALIGNMENT_REVIEWER/last-message.txt" 2>/dev/null; then echo "orchestrator unexpectedly replaced sealed reviewer evidence" >&2 exit 1 fi diff --git a/tests/mock_orchestration_e2e.sh b/tests/mock_orchestration_e2e.sh index aa5ffb3..99e96c1 100755 --- a/tests/mock_orchestration_e2e.sh +++ b/tests/mock_orchestration_e2e.sh @@ -117,51 +117,50 @@ ma workflow init "$MULTIAGENT_WORKFLOW_ID" >/dev/null mkdir -p "$STATE/runtime_state" printf '%s\n' "$MULTIAGENT_WORKFLOW_ID" >"$STATE/runtime_state/active-workflow-id" -ma decision init DEC-MOCK --title "Mock source update" --owner orchestrator >/dev/null -ma decision add-alternative DEC-MOCK --plan-id PLAN-MOCK \ - --summary "Apply the authenticated bounded update" --proposed-by orchestrator >/dev/null -ma decision commit DEC-MOCK --selected-plan PLAN-MOCK \ - --reason "Submit the bounded plan for independent authority review" >/dev/null - -AUTH_REVIEWER="decision-authority-reviewer-mock" -printf 'Claude prompt ready\n' >"$MOCK_CAPTURES/$AUTH_REVIEWER.txt" -ma subagent spawn "$AUTH_REVIEWER" --role reviewer \ +PLAN_SHA="eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" +TASK_SHA="$(ma workflow value "$MULTIAGENT_WORKFLOW_ID" original_task_sha256)" +ma workflow seal-iteration "$MULTIAGENT_WORKFLOW_ID" \ + --plan-sha256 "$PLAN_SHA" --worker-count 1 >/dev/null + +ALIGNMENT_REVIEWER="plan-alignment-reviewer-mock" +printf 'Claude prompt ready\n' >"$MOCK_CAPTURES/$ALIGNMENT_REVIEWER.txt" +ma subagent spawn "$ALIGNMENT_REVIEWER" --role reviewer \ --workflow-id "$MULTIAGENT_WORKFLOW_ID" \ - --decision-id DEC-MOCK --plan-id PLAN-MOCK --decision-revision 1 \ - --instruction "Review the bounded implementation plan and authority." >/dev/null -cat >"$MOCK_CAPTURES/$AUTH_REVIEWER.txt" <<'EOF' -verdict: orchestrator-may-decide -authority-findings: none -review-record: type=decision-authority verdict=pass diff=- + --plan-sha256 "$PLAN_SHA" \ + --instruction "Compare the bounded implementation plan with the original request." >/dev/null +cat >"$MOCK_CAPTURES/$ALIGNMENT_REVIEWER.txt" </dev/null +cp "$MOCK_CAPTURES/$ALIGNMENT_REVIEWER.txt" "$STATE/subagents/$ALIGNMENT_REVIEWER/last-message.txt" +ma subagent finalize "$ALIGNMENT_REVIEWER" >/dev/null -ma workflow record-review "$MULTIAGENT_WORKFLOW_ID" AUTH-MOCK \ - --type decision-authority --verdict pass --evidence "mock authority review passed" \ - --reviewer "$AUTH_REVIEWER" >/dev/null +ma workflow record-review "$MULTIAGENT_WORKFLOW_ID" ALIGN-MOCK \ + --type plan-alignment --verdict pass --evidence "mock plan alignment passed" \ + --reviewer "$ALIGNMENT_REVIEWER" >/dev/null CONTEXT="$STATE/approved-context.md" cat >"$CONTEXT" <<'EOF' # Approved implementation context goal: update source.txt from before to after -decision: DEC-MOCK -plan: PLAN-MOCK -authority: authenticated caller plus independent authority reviewer +plan: update the requested file only owned-paths: source.txt must-do: preserve the bounded file contract must-not-do: change unrelated paths EOF ma workflow prepare-implementation "$MULTIAGENT_WORKFLOW_ID" \ - --decision-id DEC-MOCK --plan-id PLAN-MOCK --decision-revision 1 \ - --implementation-context "$CONTEXT" --authority-review AUTH-MOCK >/dev/null + --plan-sha256 "$PLAN_SHA" \ + --implementation-context "$CONTEXT" --alignment-review ALIGN-MOCK >/dev/null ma workflow transition "$MULTIAGENT_WORKFLOW_ID" implementation >/dev/null WORKER="worker-mock-implementation" printf 'Claude prompt ready\n' >"$MOCK_CAPTURES/$WORKER.txt" ma subagent spawn "$WORKER" --assignment-id ASSIGN-MOCK --branch "$BRANCH" \ --own source.txt --workflow-id "$MULTIAGENT_WORKFLOW_ID" \ - --decision-id DEC-MOCK --plan-id PLAN-MOCK --instruction-file "$CONTEXT" >/dev/null + --instruction-file "$CONTEXT" >/dev/null printf 'after\n' >"$REPO/source.txt" cat >"$MOCK_CAPTURES/$WORKER.txt" <<'EOF' Final status: completed @@ -175,8 +174,7 @@ DIFF_HASH="mock-diff-v1" ma workflow transition "$MULTIAGENT_WORKFLOW_ID" post-implementation \ --diff-hash "$DIFF_HASH" >/dev/null -for spec in "decision-drift reviewer-decision-drift-mock REVIEW-DRIFT" \ - "technical verifier-technical-mock REVIEW-TECH"; do +for spec in "technical verifier-technical-mock REVIEW-TECH"; do read -r review_type reviewer review_id <<<"$spec" printf 'Claude prompt ready\n' >"$MOCK_CAPTURES/$reviewer.txt" ma subagent spawn "$reviewer" --role reviewer \ @@ -200,7 +198,7 @@ grep -Fq 'role may use `multiagent ops read --request-file PATH`' \ "$ROOT/prompts/orchestrator.md" grep -Fq 'roles use the direct supervisor-mediated path' \ "$ROOT/prompts/playbooks/orchestration-routing.md" -[[ "$(grep -c '^new-window ' "$MOCK_LOG")" -eq 4 ]] +[[ "$(grep -c '^new-window ' "$MOCK_LOG")" -eq 3 ]] if find "$STATE/subagents" -mindepth 1 -maxdepth 1 -type d -name '*scout*' | grep -q .; then echo "mock workflow spawned an unnecessary scout" >&2 exit 1 diff --git a/tests/run.sh b/tests/run.sh index f24814f..da53ad4 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -1013,17 +1013,17 @@ assert_file_contains "$ROOT/prompts/playbooks/agent-spawning.md" 'Do not load, c assert_file_contains "$ROOT/prompts/playbooks/implementation-lifecycle.md" '"kind": "IterationPlan"' assert_file_contains "$ROOT/prompts/playbooks/implementation-lifecycle.md" 'multiagent subagent execute-iteration --plan-file' assert_file_contains "$ROOT/prompts/playbooks/implementation-lifecycle.md" 'worker-ops-plan-01' -assert_file_contains "$ROOT/prompts/playbooks/implementation-lifecycle.md" 'The supervisor always requires an independent decision-authority review' +assert_file_contains "$ROOT/prompts/playbooks/implementation-lifecycle.md" 'requires one independent plan-alignment review' assert_file_contains "$ROOT/prompts/playbooks/implementation-lifecycle.md" 'launches independent reviewers in' assert_file_contains "$ROOT/prompts/playbooks/implementation-lifecycle.md" 'status=needs_replan' assert_file_contains "$ROOT/prompts/playbooks/implementation-lifecycle.md" "do not spawn another final verifier" assert_file_contains "$ROOT/prompts/verifier.md" 'do not substitute the default `technical`' -assert_file_contains "$ROOT/prompts/roles/decision-authority-reviewer.md" "pre-implementation authority review" -assert_file_contains "$ROOT/prompts/roles/decision-authority-reviewer.md" "never require implementation" -assert_file_contains "$ROOT/prompts/roles/decision-authority-reviewer.md" "Supervisor-Generated Decision Authority Capsule" -assert_file_contains "$ROOT/prompts/roles/decision-authority-reviewer.md" "decision-review: capsule-sha256=" -assert_file_contains "$ROOT/prompts/playbooks/implementation-lifecycle.md" "Neither the orchestrator nor a worker may manufacture or edit it" -assert_file_contains "$ROOT/prompts/roles/decision-authority-reviewer.md" "instruction file's location never changes" +assert_file_contains "$ROOT/prompts/roles/plan-alignment-reviewer.md" "independent read-only pre-implementation reviewer" +assert_file_contains "$ROOT/prompts/roles/plan-alignment-reviewer.md" '`alignment: aligned`' +assert_file_contains "$ROOT/prompts/roles/plan-alignment-reviewer.md" '`alignment: misaligned`' +assert_file_contains "$ROOT/prompts/roles/plan-alignment-reviewer.md" "plan-alignment-review: plan-sha256=" +assert_file_contains "$ROOT/prompts/playbooks/implementation-lifecycle.md" "not a substitute contract or a separate authority decision" +assert_file_contains "$ROOT/prompts/roles/plan-alignment-reviewer.md" "skipping the main task" assert_file_contains "$ROOT/evaluation/native_solver/templates/swe_autonomous_appendix.md" "Skip the scout when the public task" assert_file_contains "$ROOT/evaluation/native_solver/templates/swe_autonomous_appendix.md" '`/app/_base_commit` is immutable adapter metadata' assert_file_contains "$ROOT/evaluation/native_solver/templates/swe_autonomous_appendix.md" '`ops_plan.json` means' diff --git a/tests/test_migration_contracts.py b/tests/test_migration_contracts.py index 3407e97..6efc2d8 100644 --- a/tests/test_migration_contracts.py +++ b/tests/test_migration_contracts.py @@ -718,14 +718,9 @@ def test_workflow_v1_state_resumes_and_rejects_invalid_phase(self): "contract_artifact", "contract_artifact_sha256", "preimplementation_gate", - "decision_id", - "plan_id", - "decision_revision", - "decision_capsule", - "decision_capsule_sha256", "implementation_context", "implementation_context_sha256", - "authority_review_id", + "alignment_review_id", "iteration_plan_sha256", "iteration_worker_count", "candidate_diff_hash", diff --git a/tests/test_swe_outcomes.py b/tests/test_swe_outcomes.py index d7378d3..0561233 100644 --- a/tests/test_swe_outcomes.py +++ b/tests/test_swe_outcomes.py @@ -66,9 +66,9 @@ def test_solver_timeout_reserve_remains_configurable(self): ): self.assertEqual(evalscope_multiagent_native_runner.solver_internal_timeout(3600), 3000) - def test_autonomous_authority_does_not_reopen_explicit_task_behavior(self): + def test_plan_alignment_uses_original_request_as_authority(self): root = Path(__file__).resolve().parents[1] - reviewer = (root / "prompts/roles/decision-authority-reviewer.md").read_text( + reviewer = (root / "prompts/roles/plan-alignment-reviewer.md").read_text( encoding="utf-8" ) lifecycle = (root / "prompts/playbooks/implementation-lifecycle.md").read_text( @@ -78,9 +78,9 @@ def test_autonomous_authority_does_not_reopen_explicit_task_behavior(self): root / "evaluation/native_solver/templates/swe_autonomous_appendix.md" ).read_text(encoding="utf-8") - self.assertIn("original request is itself the user's decision", reviewer) - self.assertIn("at least two materially different", reviewer) - self.assertIn("explicit task contract is already approved", lifecycle) + self.assertIn("original user request", reviewer) + self.assertIn("genuinely absent", reviewer) + self.assertIn("original user request is authoritative", lifecycle) self.assertIn("This run has no interactive user", autonomous) self.assertIn("narrowest backward-compatible interpretation", autonomous) self.assertIn("new contract outranks pre-change exact-call mocks", autonomous) @@ -100,27 +100,24 @@ def test_autonomous_authority_does_not_reopen_explicit_task_behavior(self): self.assertIn("command=... returncode=0", verifier) self.assertIn("validation lease for the narrowest visible behavior test", routing) - def test_authority_reviewer_requires_runtime_parseable_output(self): + def test_plan_alignment_reviewer_requires_runtime_parseable_output(self): root = Path(__file__).resolve().parents[1] - reviewer = (root / "prompts/roles/decision-authority-reviewer.md").read_text( + reviewer = (root / "prompts/roles/plan-alignment-reviewer.md").read_text( encoding="utf-8" ) self.assertIn("Your final response is parsed by the workflow runtime", reviewer) self.assertIn("first non-empty line\nmust be exactly one of", reviewer) - self.assertIn("Do not write generic verdicts such as `ACCEPTED`", reviewer) self.assertIn("Return exactly these fields in this order", reviewer) - self.assertIn("Do not replace, rename, reorder,\nor omit", reviewer) - self.assertIn("pre-implementation authority review", reviewer) - self.assertIn("never require implementation\nas evidence needed to authorize implementation", reviewer) - self.assertIn("multiagent decision show\nDECISION_ID", reviewer) - self.assertIn("Supervisor-Generated Decision Authority Capsule", reviewer) - self.assertIn("orchestrator's prose assignment is not a substitute", reviewer) - self.assertIn("decision-review: capsule-sha256=", reviewer) - self.assertIn("Post-implementation reviewers and supervisor diff gates", reviewer) - self.assertIn("instruction file's location never changes the\ndeliverable target", reviewer) + self.assertIn("`alignment: aligned`", reviewer) + self.assertIn("`alignment: misaligned`", reviewer) + self.assertIn("not a substitute contract", reviewer) + self.assertIn("Do not require the requested code", reviewer) + self.assertIn("plan-alignment-review: plan-sha256=", reviewer) + self.assertIn("separate technical review", reviewer) + self.assertIn("skipping the main task", reviewer) self.assertIn( - "review-record: type=decision-authority verdict=pass diff=-", + "review-record: type=plan-alignment verdict=pass diff=-", reviewer, ) @@ -146,8 +143,8 @@ def test_lifecycle_uses_minimal_canonical_parallel_role_graph(self): self.assertIn('"kind": "IterationPlan"', lifecycle) self.assertIn("multiagent subagent execute-iteration --plan-file", lifecycle) self.assertIn("worker-ops-plan-01", lifecycle) - self.assertIn("The supervisor always requires an independent decision-authority review", lifecycle) - self.assertIn("includes the sealed plan\ndigest", lifecycle) + self.assertIn("requires one independent plan-alignment review", lifecycle) + self.assertIn("sealed plan digest", lifecycle) self.assertIn("launches independent reviewers in\nparallel", lifecycle) self.assertIn("status=needs_replan", lifecycle) self.assertIn("do not\nreplay its internal transitions", lifecycle)