feat(automation): proposal queue + approval gate (Arc D phase 2)#44
Conversation
Add src/automation_proposals.py: the durable intent queue and real approval gate for bounded automation. A proposal records intent (action type + target + description) and a status; it carries no precomputed payload so the executor (phase 3) derives fresh content at apply time. - AutomationProposal (frozen) with pending/approved/rejected/executed lifecycle. - build_automation_proposals merges candidate proposals into an existing queue, id-deduplicated; operator approvals and rejections are sticky (never reset by re-proposing, and rejected proposals are not resurrected). - approve_proposal / reject_proposal: guarded PENDING-only transitions that raise on unknown id (ProposalNotFoundError) or wrong source status (ProposalApprovalError). - executable_proposals / require_approved: the hard enforcement gate — only APPROVED proposals are ever executable (exact status match). - load_proposals / save_proposals: JSON persistence; missing file -> [], malformed/invalid-status/missing-field/non-dict entries -> ValueError. 25 tests. /code-review applied: fixed reject_proposal writing into approved_at (added rejected_at field) and hardened from_dict/load to honor the documented malformed->ValueError contract. Full suite green, ruff clean.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a5794c8c0d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ) | ||
| return cls( | ||
| proposal_id=str(data["proposal_id"]), | ||
| action_type=str(data["action_type"]), |
There was a problem hiding this comment.
Reject unknown persisted action types
When a proposal file contains an approved entry whose action_type is not one of the two supported actions, from_dict accepts it and the execution gate later filters only on status, so hand-edited or stale JSON can bypass the action whitelist that generation enforces. Validate the loaded action_type here (for example via _require_action_type) so only supported actions can become executable.
Useful? React with 👍 / 👎.
Arc D — Phase 2 of 3: durable proposal queue + real approval gate
Second rung. Adds the durable intent queue and the real enforcement gate that the phase-3 executor will sit behind. Still no execution here.
Design
pending → approved → executed, orpending → rejected.Surface (
src/automation_proposals.py)build_automation_proposals(candidates, *, action_type, created_at, existing)— id-deduplicated merge. Operator decisions are sticky: re-proposing never resets an existing approval and never resurrects a rejection.approve_proposal/reject_proposal— guarded PENDING-only transitions; raiseProposalNotFoundError(unknown id) /ProposalApprovalError(wrong source status).executable_proposals/require_approved— the hard gate: onlyAPPROVEDproposals are ever executable, exact status match. Code-review confirmed no non-approved status can slip through.load_proposals/save_proposals— JSON persistence; missing file →[]; malformed / invalid-status / missing-field / non-dict entries →ValueError.Tests / gates
tests/test_automation_proposals.py): id stability, sticky merge, no-resurrect, every transition guard, gate filtering, persistence round-trip + all malformed-input paths.ruff check .clean./code-reviewapplied: fixedreject_proposalcorruptingapproved_at(addedrejected_atfield); hardenedfrom_dict/load_proposalsto honor the malformed→ValueError contract.Next — phase 3: the gated executor (catalog-seed apply + auto-PR with dry-run default / never-main / skip-dirty / re-check-eligibility rails) plus the complete
propose → approve → executeCLI workflow.