Wire engine-side plan-approval gate (honour approval.auto_approve) #100
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_shrimp#100
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
The
approval.auto_approvesetting (Settings -> Auto-approve plans) now toggles and persists correctly, but turning it off has no effect: ado --planjob never pauses to ask for approval.The plan-approval workflow is only half-wired. The
plan_approval:<job_id>row is written byplan.approveand read back byplan.approval_status(UI display only). No code in the engine/agent loop consumes that row to gate execution. Jobs also start inmode: yolo, so nothing blocks.This is documented as an unfinished follow-up in the code:
crates/hero_shrimp/src/commands/do_cmd.rs(~L90-103): "the engine-side resume hook isn't wired up yet, so jobs run with --plan may stall in planning indefinitely."crates/hero_shrimp_server/src/rpc/methods/plan.rs(~L9-17): "an approval is captured but doesn't yet auto-resume execution."Proposed feature
Implement the engine-side approval gate so the toggle is enforced for web/server-run plans:
approval.auto_approveis false (and no per-agent override approves it), the agent loop pauses the job in anawaiting_approvalstate instead of executing.plan_approval:<job_id>state row: onapprove(oredit) resume execution; ondeclineabort cleanly.approval.auto_approveis true, resume without waiting (current pre-recorded-decision behaviour).Acceptance criteria
do --planjob pauses and waits for an approval decision before executing.approval_mode: requiredstill forces a human gate even when auto-approve is on.Notes
Follow-up to the checkbox enable/disable fix (#95). That fix made the flag toggle and persist; this issue covers the missing enforcement.
Implementation Spec for Issue #100
Objective
Wire the engine-side plan-approval gate so that
approval.auto_approve = falseactually pauses a plan-review job in anawaiting_approvalstate until an operator records a decision viaplan.approve, then resumes (approve/edit) or aborts (decline). Whenauto_approve = true(and no per-agent override forces a gate), the job resumes automatically. The gate must honor per-agentapproval_modeand the configauto_approve_agents/require_human_for_agentslists.Requirements
mode: plan_review/JobStartMode::PlanReview->AutonomyMode::PlanOnly) that publishes a plan must NOT silently flip tocompletedwhen approval is required. Instead it must enterawaiting_approvaland wait forplan_approval:<job_id>.plan_approval:<job_id>(written byplan.approveincrates/hero_shrimp_server/src/rpc/methods/plan.rs) must be consumed by server-side code, not just displayed.approve/edit-> resume execution (spawn an Execute follow-up on the same workspace; foredit, carry the revised plan text).decline-> abort cleanly (declinedterminal status).require_human_for_agentscontains the agent, or profileapproval_mode = Plan) -> always gate; else configauto_approve_agentscontains the agent ORapproval.auto_approve == true-> auto-resume; else gate.awaiting_approval/approved/declinedto the UI (details.gate_state+ job status) soPlanApproval.tsx/JobDrawer.tsxreflect the real gate.do_cmd.rsand the "follow-up not wired" note inplan.rsonce landed.Files to Modify/Create
crates/hero_shrimp_server/src/rpc/methods/job/proof_run.rs- insert the approval gate where a plan-review run finishes (theapproval_required == truecompletion block ~L904-922); add the poll-and-resolve helper and the auto-resume spawn.crates/hero_shrimp_server/src/rpc/methods/job/start.rs- stamp the resolved gate decision (auto_approvevsgate) and the agent name intodetailsat job start soproof_runcan decide without re-deriving config.crates/hero_shrimp_runtime/src/status.rs- recognizeawaiting_approvalanddeclinedas canonical status words (active / failed respectively).crates/hero_shrimp_server/src/rpc/methods/plan.rs- clarify/extend the doc + return note now that the engine consumes the row; no behavior change tomethod_plan_approve(it already persists the row).crates/hero_shrimp/src/commands/do_cmd.rs- delete/replace the stale stall-warning note (~L90-103).crates/hero_shrimp_web/ui/src/components/PlanApproval.tsxandcrates/hero_shrimp_web/ui/src/components/JobDrawer.tsx- read the newawaiting_approval/approved/declinedgate state for the chip.Implementation Plan
Step 1: Add canonical gate statuses
Files:
crates/hero_shrimp_runtime/src/status.rsphase(), classifyawaiting_approval(andpending_approval) asPhase::Activeexplicitly (comment + unit test) so it stays non-terminal. ClassifydeclinedasPhase::Failed(alongsidecancelled).canonical_mappingtest arrays.awaiting_approvalmust stay non-terminal so the resumer/UI keep the job waiting;declinedmust be terminal-failed so waiters stop polling.Dependencies: none.
Step 2: Compute and persist the gate decision at job start
Files:
crates/hero_shrimp_server/src/rpc/methods/job/start.rsmethod_job_start, afterpinned_agentis resolved andstart_modeis known, addfn resolve_plan_gate(runtime, mode, agent_name) -> &'static strreturning"none"(Yolo/Execute),"auto_approve", or"gate".JobStartMode::PlanReview): profileapproval_mode == Some(ApprovalMode::Plan)-> gate; configrequire_human_for_agentscontains agent -> gate; elseauto_approve_agentscontains agent -> auto_approve; elseapproval.auto_approve == true-> auto_approve; else gate. Forced gate wins overauto_approve == true.details["plan_gate"]and always-setdetails["agent_profile_name"]when an agent is pinned.Dependencies: none (parallel with Step 1).
Step 3: Insert the approval gate in the plan-review completion path
Files:
crates/hero_shrimp_server/src/rpc/methods/job/proof_run.rsrow.status == "completed" && final_details["approval_required"] == Some(true)and a plan is published (plan_state_has_current), branch onfinal_details["plan_gate"]:"auto_approve"/"none": keepcompleted, setgate_state = "approved", optionally record an auditplan_approval:<job_id>row (approve, "auto-approved")."gate": setrow.status = "awaiting_approval",gate_state = "awaiting_approval", persist BEFORE polling, then callawait_plan_approval(...).async fn await_plan_approval(...)that pollsdatabase.get_state_value("plan_approval:<artifact_job_id>")on a ~2s tick (timeout viaHERO_SHRIMP_PLAN_APPROVAL_TIMEOUT_SECS, default ~3600), parsesdecision/edits, returns a typed decision.approve/edit: spawn an Execute follow-up (pattern frommethod_job_follow_upineval.rs):StartProofRunRequest { mode: Yolo, proof: true, workspace_dir = plan job's workspace, session_id = same, parent_job_id = this job, prompt = "Execute the approved plan." (+ edits for edit) }, callstart_proof_run(runtime_for_repair, req). Then set gating jobstatus/gate_state = "approved",finished_at, persist.decline: setstatus/gate_state = "declined",last_error,finished_at, persist, release workspace leases.Dependencies: Steps 1 and 2.
Step 4: Clear the CLI stall note
Files:
crates/hero_shrimp/src/commands/do_cmd.rsdoonmode: yolo(out of scope to change) and keep the existing--auto-approvepre-record path; that path now genuinely unblocks the gate.Dependencies: Step 3.
Step 5: Update plan.rs documentation and return note
Files:
crates/hero_shrimp_server/src/rpc/methods/plan.rsnote_about_engine_handoffreturn field to reflect that the engine now consumes the row and auto-resumes/aborts. No persistence logic change.Dependencies: Step 3.
Step 6: Crash/restart recovery for awaiting_approval jobs (resumer)
Files:
crates/hero_shrimp_server/src/rpc/methods/job/reconcile.rs(and the boot-sweep/resumer)awaiting_approval, on restart checkplan_approval:<job_id>and apply the same resolve logic as Step 3 (resume on approve/edit, decline -> declined). Extract the resolve+resume logic into a shared function used by both the live poll and the resumer.Dependencies: Step 3.
Step 7: Surface gate state in the UI chip
Files:
crates/hero_shrimp_web/ui/src/components/PlanApproval.tsx,crates/hero_shrimp_web/ui/src/components/JobDrawer.tsxPlanApproval.tsx: infetchJobAndApproval, readdetails.gate_stateand drive the chip/buttons from it (Approve/Decline only whileawaiting_approval; terminal labels otherwise).JobDrawer.tsx: mapawaiting_approval-> "Awaiting approval",declined-> "Declined". Keepstore.tsjobPhase()in lockstep withstatus.rs.Dependencies: Steps 1, 3.
Step 8: Tests
Files:
proof_run.rs/ server test chunks,status.rs,start.rsstatus.rs:awaiting_approvalactive/non-terminal;declinedterminal-failed.resolve_plan_gatetable tests: Yolo -> none; PlanReview + auto_approve=false, no agent -> gate; PlanReview + auto_approve=true -> auto_approve; PlanReview + agent in require_human_for_agents (or profile Plan) + auto_approve=true -> gate; PlanReview + agent in auto_approve_agents + auto_approve=false -> auto_approve.Dependencies: Steps 1-3, 6.
Acceptance Criteria
awaiting_approvaland waits for a decision before executing.approved, Execute follow-up spawned) with no prompt.declined, leases released); edit resumes with the revised plan text.require_human_for_agents/ profileapproval_mode = Plan) still gates even whenauto_approveis true.resolve_plan_gateprecedence.do_cmd.rsandplan.rs; UI chip reflectsawaiting_approval/approved/declined.Notes
doalways sendsmode: yolo, sodo --plandoes NOT hit the new gate; it relies on the existing--auto-approvepre-record. The gate governsmode: plan_reviewjobs from web/server, matching the issue's framing. Do not change the CLI'smode.ApprovalMode::Requiredvariant (Plan | Default | AutoEdit | Yolo | SmartReview). Map per-agent "force gate" primarily to configrequire_human_for_agents, with profileapproval_mode == Some(Plan)as a secondary signal.plan_approval:<artifact_job_id>uses the slug job id (matching whatplan.approvewrites), NOT the numeric db id.plan.approveuses INSERT OR REPLACE; there is no delete API and the UI reads the same row. Do NOT delete the row after consuming - leave it as audit; guard resume to fire once.declinedbranch must release workspace leases; the approve/edit follow-up acquires its own viastart_proof_run- ensure the parent's leases are released/transferred to avoid a self-conflict (may needforce: trueon the resume request).Test Results
Build:
cargo build --workspace— passNew tests for this issue:
canonical_mapping: awaiting_approval/pending_approval -> Active, declined -> Failedresolve_plan_gate_precedence_table: gate-decision precedence (6 cases)parse_plan_decision(6 cases) +poll_plan_decisionhappy/edit/timeout (3 cases)Coverage note: Unit tests cover status classification, gate-decision precedence, and decision parsing/polling. The end-to-end gate flow (pause -> awaiting_approval, resume-on-approve spawns Execute follow-up, abort-on-decline releases leases, edit carries revised plan) is composed from these unit-tested building blocks but is not yet covered by an automated integration harness; it remains integration/manual verification.
Implementation Summary
The engine-side plan-approval gate is now wired so
approval.auto_approveis enforced for server/webplan_reviewjobs.Behavior
plan_reviewjob that publishes a plan now consults aplan_gatedecision computed at job start. When a human gate is required it parks inawaiting_approvalinstead of completing.plan_approval:<job_id>(the row written byplan.approve). On approve/edit it resumes by spawning an Execute follow-up on the same workspace/session (edit carries the revised plan text); on decline it aborts with statusdeclinedand releases workspace leases.auto_approve_agents), the job resumes automatically and is stampedapproved.require_human_for_agents, or profileapproval_mode = Plan) overrides auto-approve and always gates.Files changed
crates/hero_shrimp_runtime/src/status.rs-awaiting_approval/pending_approvalclassified as active (non-terminal);declinedas failed (terminal).crates/hero_shrimp_server/src/rpc/methods/job/start.rs-resolve_plan_gate(...)computesnone/auto_approve/gateand stampsdetails.plan_gate+details.agent_profile_name.crates/hero_shrimp_server/src/rpc/methods/job/approval_gate.rs(new) - sharedPlanDecision,parse_plan_decision,poll_plan_decision,resume_after_approval, reused by the live run and the reconciler.crates/hero_shrimp_server/src/rpc/methods/job/proof_run.rs- the gate branch: pause inawaiting_approval, poll, resume on approve/edit, abort on decline; auto-approve path stampsgate_stateand records an audit row.crates/hero_shrimp_server/src/rpc/methods/job/reconcile.rs- restart recovery forawaiting_approvaljobs.crates/hero_shrimp_server/src/rpc/methods/plan.rs- docs/return note updated (engine now consumes the decision); persistence unchanged.crates/hero_shrimp/src/commands/do_cmd.rs- removed the stale "resume hook not wired" stall note.crates/hero_shrimp_web/ui/src/components/PlanApproval.tsx,crates/hero_shrimp_web/ui/src/store.ts- UI readsdetails.gate_stateand surfacesawaiting_approval/approved/declined(JobDrawer chip routes through the centraljobStatusViewmapping in store.ts).Tests
cargo build --workspacepasses. Test totals: hero_shrimp_runtime 575 passed, hero_shrimp_server 314 passed, hero_shrimp 27 passed (0 failures).New coverage: status classification for the gate states;
resolve_plan_gateprecedence table (6 cases);parse_plan_decision(6 cases) andpoll_plan_decisionhappy/edit/timeout (3 cases).Caveat
The end-to-end gate flow (pause -> resume-on-approve spawns the Execute follow-up -> abort-on-decline releases leases -> edit carries revised plan) is composed from the unit-tested building blocks above but is not yet covered by an automated integration harness; that path still warrants manual/integration verification. CLI
do --planremains onmode: yoloand relies on the existing--auto-approvepre-record, which now genuinely unblocks the gate (per the issue's web/server scope).