976817fa22
The deterministic spine of the master controller. State machine is
pure data with 6 load-bearing invariants enforced via property tests.
Reaper resets stale-heartbeat workflow_attempts to pending. Pickup
guard transitions workflows to STUCK when an attempt has been
re-pended too many times without success.
tools/controller/state_machine.py:
- KNOWN_STATES = 12; TERMINAL_STATES = {MERGED, ABANDONED, STUCK,
CREATED_PR}. STUCK's only allowed exit is the operator-driven
operator_unstick event (back to DISCOVERED).
- 32 TRANSITIONS entries covering DISCOVERED → ANALYZING →
IMPLEMENTING ↔ AWAITING_CI / CONFLICT_RESOLVING / ESCALATING →
REVIEWING → MERGING → MERGED. Plus pickup_exhausted exits from
IMPLEMENTING/CONFLICT_RESOLVING/REVIEWING.
- 27 named events with descriptions. apply_event() lookup raises
IllegalTransitionError (lists legal events from current state)
or ValueError on unknown state (per v6 unknown-state guard).
- 6 LOAD-BEARING invariants for v1 (per v9 simplification):
1. no_path_implementing_to_reviewing_skips_ci (Hard Rule #1
constructional fix for the no-mans-land race)
2. terminal_states_have_no_exits (only STUCK→operator_unstick OK)
3. tier_monotonic_non_decreasing
4. every_pr_workflow_includes_reviewing
5. conflict_resolving_bounded (1st→IMPLEMENTING, 2nd→ESCALATING,
3rd→STUCK; structurally encoded)
6. escalation_deterministic
- reachable_from() honors cycles (DISCOVERED ∈ reachable(DISCOVERED)
via STUCK→operator_unstick path; AWAITING_CI self-loops via
ci_flake_retry).
tools/controller/reaper.py:
- reap_stale_attempts(): SELECT in_progress attempts whose
lock_heartbeat_at + lock_ttl_seconds < NOW (per-row TTL respects
per-role differences — estimator 180s, reviewer 720s, tier-2
implementer 2160s). UPDATEs status='pending', clears lock columns,
preserves pickup_count (the pickup guard handles that). Inserts
controller_events row with reason='lock-ttl-expired' per reap.
- Dialect-portable: Postgres uses interval arithmetic; SQLite uses
julianday(). Same logic either way.
tools/controller/pickup_guard.py:
- transition_exhausted_to_stuck(): finds attempts with status='pending'
AND pickup_count >= MAX_PICKUPS (default 3 per v6 blocker fix)
AND workflow not already terminal. Transitions workflow → STUCK,
marks attempt as 'reaped', inserts controller_events with
reason='attempt-pickup-exhausted' + pickup_count + max_pickups.
45 new tests:
- state_machine: basic shape (states partition, every transition uses
known states + defined events), apply_event success/error paths,
events_from + reachable_from helpers (including cycle awareness),
per-invariant zero-violations against the live table, per-invariant
monkeypatch-violations to prove the checks catch the bug class they
claim to, parametrised sanity check "every non-terminal can reach
some terminal".
- reaper: empty DB / fresh heartbeat / stale heartbeat reaped /
per-row TTL respected / event row created / only-in-progress
reaped / multiple stale attempts.
- pickup guard: empty DB / below limit / at limit / in-progress not
checked / terminal workflow skipped / event payload content /
default max_pickups matches v6.
Total: 229 controller tests; full auto_agents suite 2591 pass.
29 lines
1.3 KiB
Python
29 lines
1.3 KiB
Python
"""Controller package — DB-owned PR state machine.
|
|
|
|
See ``.drew/controller_state_machine.md`` for the full plan. This
|
|
package is the Phase 0+ implementation of that plan; it is being
|
|
built incrementally on the ``controller-state-machine`` branch.
|
|
|
|
The package layout mirrors the plan's responsibilities:
|
|
|
|
- ``contracts/`` — Pydantic V1 contracts for worker I/O. Strict-parse
|
|
helpers. The single source of truth for "what shape does a
|
|
reviewer / implementer / estimator / conflict-resolver / summarizer
|
|
produce."
|
|
- ``ci_summary_parsers/`` — deterministic per-CI-tool parsers that
|
|
consume raw Forgejo CI logs and produce ``CIFailure`` rows. Called
|
|
from the master's prefetch path.
|
|
- (later) ``master.py`` — singleton process: state-machine driver,
|
|
discovery, prefetch, reconciliation, Forgejo writes, merge.
|
|
- (later) ``worker.py`` — scalable process: dequeue, lock+heartbeat,
|
|
OpenCode invocation, output write.
|
|
- (later) ``state_machine.py`` — ``TRANSITIONS`` dict + property tests.
|
|
- (later) ``mcp/`` — per-role response-builder MCP servers.
|
|
"""
|
|
|
|
from . import state_machine # re-exported for tests
|
|
from . import reaper # re-exported for tests
|
|
from . import pickup_guard # re-exported for tests
|
|
|
|
__all__ = ["contracts", "state_machine", "reaper", "pickup_guard"]
|