{"record":{"id":"896c38d6b79d8c75","repo":"zeroclaw-labs/zeroclaw","slug":"run-run-id-is-parked-at-an-approval-gate-amend","errorCode":null,"errorMessage":"run {run_id} is parked at an approval gate; amend/revise apply only to deterministic checkpoints — approve or deny instead","messagePattern":"run (.+?) is parked at an approval gate; amend/revise apply only to deterministic checkpoints — approve or deny instead","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/sop/approval/broker.rs","lineNumber":481,"sourceCode":"            .filter(|g| !g.is_empty())\n            && !self\n                .resolver\n                .is_member(engine.approval_config(), &principal, group)\n        {\n            return Ok(BrokerOutcome::NotAuthorized {\n                required_group: group.to_string(),\n            });\n        }\n\n        match decision {\n            // A single authorized deny cancels the run (no quorum on denial - fail-safe).\n            ApprovalDecision::Deny { .. } => Ok(BrokerOutcome::Resolved(\n                engine.resolve_gate(run_id, decision, principal)?,\n            )),\n            // Amend/Revise are deterministic-checkpoint decisions: an approval\n            // gate has no piped draft to edit and no predecessor to re-run. Fail\n            // closed BEFORE any vote or ledger side effect.\n            ApprovalDecision::Amend { .. } | ApprovalDecision::Revise { .. } => anyhow::bail!(\n                \"run {run_id} is parked at an approval gate; amend/revise apply only to \\\n                 deterministic checkpoints — approve or deny instead\"\n            ),\n            ApprovalDecision::Approve => {\n                // Unpoliced (no named policy) clears immediately - quorum-1 pass-through.\n                let Some((policy_name, cfg)) = policy.as_ref() else {\n                    return Ok(BrokerOutcome::Resolved(\n                        engine.resolve_gate(run_id, decision, principal)?,\n                    ));\n                };\n                let need = (cfg.quorum.max(1)) as usize;\n                if need <= 1 {\n                    return Ok(BrokerOutcome::Resolved(\n                        engine.resolve_gate(run_id, decision, principal)?,\n                    ));\n                }\n                // Refuse to record a quorum vote from a principal `approval_mode` would\n                // reject outright (the agent under OutOfBandRequired, an out-of-band","sourceCodeStart":463,"sourceCodeEnd":499,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/sop/approval/broker.rs#L463-L499","documentation":"The approval broker's resolve() was called with ApprovalDecision::Amend or ApprovalDecision::Revise while the run is parked at an approval gate. Amend/Revise are deterministic-checkpoint decisions (an editable piped draft, a predecessor step to re-run); an approval gate has neither, so the broker fails closed before any vote or ledger side effect.","triggerScenarios":"Calling broker resolve() with ApprovalDecision::Amend { .. } or Revise { .. } on a run whose park is an approval gate rather than a deterministic checkpoint — e.g. an approval UI that offers amend buttons for every parked run.","commonSituations":"Approval tooling that reuses one decision handler for both checkpoint and gate parks; state machine drift after upgrading semantics of Amend/Revise; automated responder defaulting to Amend on ambiguous content; misrouted run ids between a checkpoint reviewer and gate approver.","solutions":["Send ApprovalDecision::Approve or Deny for runs parked at approval gates.","Gate the Amend/Revise UI on the park kind — only offer them for deterministic checkpoints.","Inspect the run's park reason first to decide which decision variants are legal."],"exampleFix":"// before\nbroker.resolve(run_id, ApprovalDecision::Amend { draft }).await?; // run is at an approval gate\n\n// after\nmatch run_park_kind(run_id) {\n    ParkKind::ApprovalGate => broker.resolve(run_id, ApprovalDecision::Approve).await?,\n    ParkKind::Checkpoint => broker.resolve(run_id, ApprovalDecision::Amend { draft }).await?,\n}","handlingStrategy":"validation","validationCode":"fn legal_decisions(park: &ParkState) -> &'static [DecisionKind] {\n    match park {\n        ParkState::ApprovalGate => &[DecisionKind::Approve, DecisionKind::Deny],\n        ParkState::Checkpoint => &[DecisionKind::Approve, DecisionKind::Deny, DecisionKind::Amend, DecisionKind::Revise],\n    }\n}\nassert!(legal_decisions(&park).contains(&decision.kind()));","typeGuard":"fn is_approval_gate(run: &RunView) -> bool {\n    matches!(run.park_reason(), Some(ParkReason::ApprovalGate { .. }))\n}","tryCatchPattern":"match broker.resolve(run_id, decision).await {\n    Err(e) if e.to_string().contains(\"amend/revise apply only to deterministic checkpoints\") => {\n        eprintln!(\"run {run_id} is at an approval gate: resend as Approve or Deny\");\n    }\n    rest => rest?,\n}","preventionTips":["Derive the offered decision buttons from the run's park kind, not from a fixed menu.","Document the decision matrix (gate: Approve/Deny; checkpoint: +Amend/Revise) in reviewer tooling.","Never default ambiguous handler input to Amend — default to asking the operator."],"tags":["sop","approval","broker","decision","state-machine","fail-closed"],"backgroundTag":"invalid-state-transition","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}