Files
cleveragents-core/tools/controller/contracts/causes.py
T
drew 016b348117 feat(controller): grooming gate (Phase 0 + Phase 1 worker-shape dispatch)
Phase 0 (foundation):
- Cause enum (controller_events.cause) for action attribution
- Schema: grooming_decisions audit table; workflows gains
  grooming_evaluated_at + deferred_reason + deferred_at +
  deferred_target_workflow_id; pulls gains touched_files
- audit_comments: CLOSE / DEFER templates + render_comment_template
- forgejo_writes: close_issue + defer_issue 5-step crash-safe protocol
  (fingerprint dedup, error matrix, dry-run)
- patch_pr_state callback in forgejo_http
- grooming_config: 22-env-var frozen-dataclass config + log_effective
- pulls.touched_files cache extension (_pipeline_cache.py schema v8)
- reaper.reap_grooming_decisions audit-retention sweep
- reconciliation RESUME guard (deferred_reason)

Phase 1 (worker-queue shape, 2026-05-25):
- New state: GROOMING. New events: grooming_started, groom_verdict_
  {proceed,defer,close}. 5 new transitions; all invariants still clean
- GroomingInputV1 + GroomingOutputV1 Pydantic contracts
- outcomes._map_grooming_outcome routes verdicts to state-machine events
- prefetch.build_grooming_stage_b_input + list_open_prs callback
- scheduler GROOMING -> grooming_stage_b role
- promote: cfg-gated DISCOVERED -> GROOMING when CONTROLLER_GROOMING_
  ENABLED=true; issues skip grooming
- forgejo_writes decomposed: close_act/defer_act (Forgejo writes only;
  state-machine already transitioned) + close_decide_and_act/
  defer_decide_and_act (Phase 0 callers); _apply_workflow_transition
  is underscore-private
- grooming.py library: tokenization, suspicion scoring (Jaccard +
  weighted overlap), deterministic checks, action -> verdict mapping
- mcp/grooming_builder.py: 14-tool FastMCP server emits GroomingOutputV1
- .opencode/agents/grooming-stage-b.md: duplicate-detection agent
  prompt (claude-haiku-4-5)
- grooming_side_effects.run_grooming_side_effects_tick: per-state tick
  performs Forgejo writes after groom_verdict_{defer,close} fires.
  Filters on event_type='transition' + payload.event (centralizes the
  convention pending Phase 2's latest_transition_event helper)
- GroomingCallbacks frozen dataclass; loop.py + __main__.py wired

Worker role registry (single source of truth):
- worker/roles.py: WORKER_ROLES + WorkerRoleSpec + default_roles_csv
  + output_filename_for. agent_runner.ROLE_TO_MCP_MODULE / ROLE_TO_
  OUTPUT_MODEL derive from it; opencode_session.agent_name_for reads
  it for flat cases; all 6 prompt builders use output_filename_for;
  worker --roles default = default_roles_csv(); launcher script
  derives --roles via shell substitution. Cross-site invariant test
  enforces alignment across 5 sites + opencode.json MCP registry.

Phase 0 silent-bug fix:
- reconciliation.py RESUME guard SELECT now includes deferred_reason
  (was missing since Phase 0; guard was a silent no-op). Tightened
  from getattr to attribute access to fail fast on future omissions.

Tests (1456 total, +91 grooming-specific):
- test_grooming_phase0.py: 34 tests (orchestrator matrix, crash
  recovery, idempotency, dry-run)
- test_grooming_phase1.py: 60 tests (library, contracts, state
  machine, outcomes, scheduler, promote, prefetch, act-variants
  with signature parity, side-effect tick incl. natural-idempotency
  + executed-flag-skip + verdict-mismatch + reconciliation RESUME)
- test_mcp_builders.py TestGroomingBuilder: 29 tests (happy paths
  + 22 validation rules + Pydantic round-trip + master-tick-read-
  path companion)
- test_worker_agent_runner.py TestRoleMaps: cross-role wiring
  alignment + agent-prompt-vs-worker-fallback filename contract +
  inspect.signature equality (close_act/defer_act vs
  close_issue/defer_issue)
- test_state_machine.py: transition count 51 -> 56 +
  events_from_grooming

Live-validated end-to-end on 4 staged sentinel PRs (#55-#58) in
dry_run: agent emits verdicts via MCP, state-machine transitions
fire, side-effect tick writes audit row, deferred_reason gates
reconciliation RESUME correctly.

Deferred refinements + Phase 2 prerequisite (latest_transition_event
helper) tracked in .drew/regressions-plan.md "Phase 1 follow-up
backlog".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:05:29 -04:00

67 lines
2.4 KiB
Python

"""Controlled vocabulary for ``controller_events.cause`` attribution.
The ``cause`` column on ``controller_events`` disambiguates events whose
``event_type`` alone could have multiple legitimate sources — e.g.
``label-pause`` can be emitted by reconciliation reacting to a human
pulling the opt-in label OR by defer performing a controller-driven
pause. Telemetry queries that need to separate these (cost attribution,
audit forensics) filter on ``cause``.
Events whose ``event_type`` already uniquely identifies the trigger
(e.g. ``estimator_done`` — only the estimator emits it) leave
``cause = NULL``.
This module is dependency-free (stdlib ``enum`` only). All insert sites
import this enum and pass enum members, not bare strings, so an
introduced typo fails at import time rather than as a silently-NULL
column in production.
See ``.drew/regressions-plan.md`` — "``cause`` enum" section and
decisions #23 and #32.
"""
from __future__ import annotations
from enum import StrEnum
class Cause(StrEnum):
"""Why a ``controller_events`` row was written.
``StrEnum`` (stdlib, 3.11+) makes each member a real ``str``
instance whose ``__str__`` returns its value, so members serialise
correctly through ``sqlite3`` parameter binding without an explicit
``.value`` access at every insert site — ``f"{Cause.GROOMING_DEFER}"``
and ``str(Cause.GROOMING_DEFER)`` both yield ``"grooming_defer"``.
"""
# Grooming gate (Phase 0/1) — defer vs close attribution on
# events whose event_type doesn't uniquely identify the gate.
GROOMING_DEFER = "grooming_defer"
GROOMING_CLOSE = "grooming_close"
# Estimator gate (Phase 2) — abandonment decided at ANALYZING.
ESTIMATOR_ABANDON = "estimator_abandon"
# Reviewer gate (Phase 3) — abandonment decided at REVIEWING.
REVIEWER_ABANDON = "reviewer_abandon"
# Operator manual action — used by the documented clear-deferral
# SQL and any future operator-driven event insertions.
OPERATOR = "operator"
# Reactive detection of a human-initiated change via Forgejo UI
# (e.g. a human pulls the opt-in label).
HUMAN = "human"
# State changes detected by reconciliation (e.g. PR closed
# externally by someone other than the controller).
EXTERNAL = "external"
# Reserved for the Phase 6+ scope-evaluator pipeline that will
# clear deferrals after re-evaluating deferred PRs.
SCOPE_EVALUATOR = "scope_evaluator"
__all__ = ["Cause"]