Files
roboco/roboco/services/gateway/choreographer/board.py
T
Renn FandRenzo F 05a83f45cb feat(auditor): waive_finding verb + findings queue panel
Wire the long-unwired mark_waived repo method to a new auditor-only
flow verb waive_finding, severity-scoped to minor/nit (blocker/major
must be fixed, never waived), requiring a note, with a task.finding_waived
audit event and no task status change. Add the verb to the IntentSpec
table (auto-derived into the auditor manifest), the flow_auditor route,
and the flow_server MCP tool.

Surface open review findings (cross-task, blocking-first) on the
auditor dashboard via ReviewFindingsRepository.list_open_findings and a
new findings field on AuditorDashboard. Restore the panel's 4-card
auditor layout with a new read-only FindingsQueuePanel as the 4th card.
2026-07-14 08:56:55 +02:00

173 lines
6.6 KiB
Python

"""Board + Auditor verbs (first per-role split).
Mixin extracted from ``_impl.py`` to prove the per-role pattern. Relies
on ``self.task`` and ``self._briefing_for`` from the base class via
Python's MRO. ``board_triage`` and ``auditor_triage`` are read-only
verbs that don't share helper code with any other role, making this
the safest first extraction.
The mixin inherits from ``ChoreographerHelpers`` only when type-checking
so mypy resolves ``self.task`` etc. to the typed surface. At runtime
the actual class is composed in ``__init__.py`` and inherits from
``_LegacyChoreographer`` (where the real implementations live).
"""
from __future__ import annotations
import contextlib
from typing import TYPE_CHECKING
from roboco.foundation.policy.content import Severity
from roboco.services.gateway.envelope import Envelope
from roboco.services.repositories.review_findings import (
STATUS_OPEN,
ReviewFindingsRepository,
)
if TYPE_CHECKING:
from uuid import UUID
from roboco.services.gateway.choreographer._protocol import ChoreographerHelpers
_Base = ChoreographerHelpers
else:
_Base = object
# Severity stored as the enum's string value (``f.severity.value`` at insert).
_BLOCKING = frozenset({Severity.BLOCKER.value, Severity.MAJOR.value})
class BoardMixin(_Base):
"""Board (Product Owner + Head Marketing) + Auditor verbs."""
async def board_triage(self, board_agent_id: UUID) -> Envelope:
"""Phase 4: Board triage — next strategic root task awaiting PM review.
The idle branch (no strategic root to review) is also what the
Product Owner's roadmap-exploration and Head of Marketing's
feature-spotlight-exploration one-shot spawns hit first (their
directly-assigned exploration task is never itself a "strategic root
awaiting PM review"): pass ``include_company_goals`` so the CEO's
charter (brand_voice/north_star) still reaches them there, without
paying for the rest of ``full``'s heavy sections on this low-
cardinality, board-only path.
"""
strategic = await self.task.list_strategic_for_board()
if strategic:
t = strategic[0]
return Envelope.ok(
status=str(t.status),
task_id=str(t.id),
next=(
f"review and call escalate_to_ceo(task_id='{t.id}', reason=...)"
" or i_am_idle"
),
context_briefing=await self._briefing_for(
board_agent_id, t.id, full=True
),
)
return Envelope.ok(
status="idle",
task_id=None,
next="no strategic-review work — i_am_idle",
context_briefing=await self._briefing_for(
board_agent_id, None, include_company_goals=True
),
)
async def auditor_triage(self, auditor_agent_id: UUID) -> Envelope:
"""Phase 4: Auditor triage — surfaces anomalies (long-running blocked, etc.)."""
anomalies = await self.task.list_long_running_blocked()
if anomalies:
t = anomalies[0]
return Envelope.ok(
status=str(t.status),
task_id=str(t.id),
next=(
"log a reflect-note observing the anomaly via "
f"note(scope='reflect', task_id='{t.id}', text='...')"
),
context_briefing=await self._briefing_for(
auditor_agent_id, t.id, full=True
),
)
return Envelope.ok(
status="idle",
task_id=None,
next="no anomalies — i_am_idle",
context_briefing=await self._briefing_for(auditor_agent_id, None),
)
async def waive_finding(
self, auditor_agent_id: UUID, finding_id: UUID, note: str
) -> Envelope:
"""Waive one minor/nit finding by id with a required note.
The auditor is the only role that can close a finding without a dev
fix — but only for non-blocking severity (minor/nit). Blocker/major
must be fixed, never waived. No task status change: the ledger row
moves ``open -> waived`` and an audit event records the decision.
``mark_waived`` is the long-unwired repo method this finally calls.
"""
repo = ReviewFindingsRepository(self.task.session)
row = await repo.get(finding_id)
if row is None:
return Envelope.not_found(
message=f"finding {str(finding_id)[:8]} not found.",
remediate=(
"find open findings via the task's GET /findings or triage()."
),
)
if row.status != STATUS_OPEN:
return Envelope.invalid_state(
message=(
f"finding {str(finding_id)[:8]} is already {row.status}; "
"only open findings can be waived."
),
remediate=(
"pick an open finding — waived/addressed/verified "
"rows are immutable."
),
)
if row.severity in _BLOCKING:
return Envelope.invalid_state(
message=(
f"finding {str(finding_id)[:8]} is {row.severity} — "
"blocker/major findings must be fixed, never waived."
),
remediate=(
"leave it for the dev to address; waive only minor/nit findings."
),
)
clean_note = note.strip()
if not clean_note:
return Envelope.invalid_state(
message=(
"a waive requires a note explaining why this finding "
"is not worth a fix."
),
remediate="pass note=<why this minor/nit is waived>.",
)
await repo.mark_waived(finding_id, clean_note)
with contextlib.suppress(Exception):
await self.audit.log_task_event(
event_type="task.finding_waived",
task_id=row.task_id,
agent_id=auditor_agent_id,
severity="info",
details={
"finding_id": str(finding_id),
"severity": row.severity,
"origin": row.origin,
"note": clean_note[:300],
},
)
return Envelope.ok(
status="waived",
task_id=str(row.task_id),
next=(
f"finding {str(finding_id)[:8]} waived; triage() for next item "
"or i_am_idle"
),
)