mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(content): obligate role note sections like journals
Completes the note(scope='handoff') write-path (WIP 23e6ee57): every role
with a dedicated note section is now obligated to populate it, the same way
journals are obligated.
Obligations (foundation.policy.tracing):
- DEV_NOTES / PR_REVIEWER_NOTES / QUICK_CONTEXT_MIN_CHARS requirements +
checkers, wired onto i_am_done (dev_notes), delegate (quick_context), and
pr_pass / pr_fail / post_pr_review (pr_reviewer_notes).
- Fixes a latent bug: the docs-notes checker read dev_notes instead of
doc_notes (the documenter's section); the i_documented shim now feeds
doc_notes to match.
Auditor: a session-scoped note obligation on i_am_idle — the auditor owns no
delivery task and has no delivery verb, so it must have recorded an
observation within the window before going idle (JournalService.has_recent_entry).
Write-then-gate: persisted sections (dev_notes / quick_context) are
pre-written by the agent's note(scope='handoff') before the gated verb;
argument-borne sections (doc_notes / pr_reviewer_notes) are checked through a
SimpleNamespace shim, the same pattern qa_notes already uses.
Config: dev/pr_reviewer/quick_context min-chars (40/40/30), panel-tunable.
Plus per-gap remediation hints and full coverage (write-path routing,
ownership, validation->remediation, each obligation, the doc_notes fix).
Full make-quality green: 9777 passed, 95.6% coverage.
This commit is contained in:
@@ -45,7 +45,10 @@ from roboco.services.gateway.remediation import (
|
||||
hint_for_missing_progress,
|
||||
hint_for_missing_qa_notes,
|
||||
hint_for_missing_reflect,
|
||||
hint_for_short_dev_notes,
|
||||
hint_for_short_doc_notes,
|
||||
hint_for_short_pr_reviewer_notes,
|
||||
hint_for_short_quick_context,
|
||||
hint_for_unaddressed_acceptance_criteria,
|
||||
)
|
||||
|
||||
@@ -1876,6 +1879,7 @@ class Choreographer:
|
||||
auto-verify path. It is filtered here and re-asserted by the spec
|
||||
action's own preconditions.
|
||||
"""
|
||||
from roboco.config import settings as _settings
|
||||
from roboco.foundation.policy import tracing as _tr
|
||||
|
||||
has_reflect = await self.journal.has_reflect_for_task(agent_id, task_id)
|
||||
@@ -1890,6 +1894,9 @@ class Choreographer:
|
||||
journal_learning_present=has_learning,
|
||||
journal_struggle_present=has_struggle,
|
||||
journal_during_work_count=during_work_count,
|
||||
# DEV_NOTES_MIN_CHARS reads the persisted task.dev_notes — the dev
|
||||
# pre-writes it via note(scope='handoff') before i_am_done.
|
||||
dev_notes_min_chars=_settings.dev_notes_min_chars,
|
||||
)
|
||||
requirements: list[_tr.Requirement] = [
|
||||
r
|
||||
@@ -2004,7 +2011,15 @@ class Choreographer:
|
||||
and (datetime.now(UTC) - latest).total_seconds() <= window_seconds
|
||||
)
|
||||
|
||||
ctx = _tr.GateContext(journal_decision_present=fresh)
|
||||
# QUICK_CONTEXT_MIN_CHARS applies only to ``delegate`` (its
|
||||
# required-set is the only one carrying it); the PM pre-writes the
|
||||
# parent's quick_context via note(scope='handoff') before delegating.
|
||||
# Setting the threshold here is inert for unblock / escalate, which do
|
||||
# not require it.
|
||||
ctx = _tr.GateContext(
|
||||
journal_decision_present=fresh,
|
||||
quick_context_min_chars=_settings.quick_context_min_chars,
|
||||
)
|
||||
result = _tr.check_requirements(
|
||||
task=t,
|
||||
requirements=list(_tr.requirements_for(verb)),
|
||||
@@ -2189,6 +2204,17 @@ class Choreographer:
|
||||
min_chars=_roboco_settings.docs_notes_min_chars
|
||||
),
|
||||
"docs_files_non_empty": hint_for_missing_doc_files(),
|
||||
"dev_notes>=min": hint_for_short_dev_notes(
|
||||
min_chars=getattr(_roboco_settings, "dev_notes_min_chars", 40),
|
||||
task_id=tid,
|
||||
),
|
||||
"pr_reviewer_notes>=min": hint_for_short_pr_reviewer_notes(
|
||||
min_chars=getattr(_roboco_settings, "pr_reviewer_notes_min_chars", 40),
|
||||
),
|
||||
"quick_context>=min": hint_for_short_quick_context(
|
||||
min_chars=getattr(_roboco_settings, "quick_context_min_chars", 30),
|
||||
task_id=tid,
|
||||
),
|
||||
"journal:note_at_claim": (
|
||||
"pre-gateway parity P1: write a journal:note at claim. "
|
||||
f"Call note(scope='note', task_id='{tid}', "
|
||||
@@ -2969,22 +2995,21 @@ class Choreographer:
|
||||
),
|
||||
context_briefing=briefing,
|
||||
)
|
||||
if guard := await self._pending_assignment_guard(agent_id, briefing):
|
||||
return await self._emit_rejection(
|
||||
guard, agent_id=agent_id, task_id=None, verb="i_am_idle"
|
||||
)
|
||||
if guard := await self._pm_unfinished_review_guard(agent_id, briefing):
|
||||
return await self._emit_rejection(
|
||||
guard, agent_id=agent_id, task_id=None, verb="i_am_idle"
|
||||
)
|
||||
if guard := await self._pm_uncovered_decomposition_guard(agent_id, briefing):
|
||||
return await self._emit_rejection(
|
||||
guard, agent_id=agent_id, task_id=None, verb="i_am_idle"
|
||||
)
|
||||
if guard := await self._pm_uncovered_required_cells_guard(agent_id, briefing):
|
||||
return await self._emit_rejection(
|
||||
guard, agent_id=agent_id, task_id=None, verb="i_am_idle"
|
||||
)
|
||||
# Pre-idle guards, evaluated in order — the first that returns an
|
||||
# Envelope short-circuits to a rejection (kept as a loop so adding a
|
||||
# guard doesn't push this verb over the return-count bound).
|
||||
idle_guards = (
|
||||
self._pending_assignment_guard,
|
||||
self._pm_unfinished_review_guard,
|
||||
self._pm_uncovered_decomposition_guard,
|
||||
self._pm_uncovered_required_cells_guard,
|
||||
self._auditor_note_guard,
|
||||
)
|
||||
for guard_fn in idle_guards:
|
||||
if guard := await guard_fn(agent_id, briefing):
|
||||
return await self._emit_rejection(
|
||||
guard, agent_id=agent_id, task_id=None, verb="i_am_idle"
|
||||
)
|
||||
paused_ids = await self._auto_pause_in_progress_tasks(agent_id)
|
||||
await self.task.mark_agent_idle(agent_id)
|
||||
if paused_ids:
|
||||
@@ -3108,6 +3133,47 @@ class Choreographer:
|
||||
context_briefing=briefing,
|
||||
)
|
||||
|
||||
# The auditor must have recorded an observation within this window before
|
||||
# it may go idle (session-scoped note obligation; see _auditor_note_guard).
|
||||
_AUDITOR_IDLE_NOTE_WINDOW_SECONDS: int = 3600
|
||||
|
||||
async def _auditor_note_guard(
|
||||
self, agent_id: UUID, briefing: dict[str, Any]
|
||||
) -> Envelope | None:
|
||||
"""Refuse i_am_idle when the auditor has not recorded an observation.
|
||||
|
||||
Every role with a dedicated note section is obligated to populate it,
|
||||
the same way journals are obligated. The auditor's section is
|
||||
``auditor_notes``, but it owns no delivery task and has no delivery
|
||||
verb to hang the obligation on — so the obligation is session-scoped:
|
||||
before going idle the auditor must have recorded an observation within
|
||||
the last ``_AUDITOR_IDLE_NOTE_WINDOW_SECONDS`` (a reflect journal entry,
|
||||
or a note(scope='handoff') section write — either leaves a journal
|
||||
entry). Inert for every other role.
|
||||
"""
|
||||
agent = await self.task.agent_for(agent_id)
|
||||
if agent is None or str(agent.role) != "auditor":
|
||||
return None
|
||||
if await self.journal.has_recent_entry(
|
||||
agent_id, self._AUDITOR_IDLE_NOTE_WINDOW_SECONDS
|
||||
):
|
||||
return None
|
||||
return Envelope.invalid_state(
|
||||
message=(
|
||||
"as the auditor you must record an observation before going "
|
||||
"idle — your auditor_notes section is the artifact the company "
|
||||
"reviews, so it cannot be left empty."
|
||||
),
|
||||
remediate=(
|
||||
"call note(scope='reflect', text='<what you observed and any "
|
||||
"concern>') for a journal observation, or "
|
||||
"note(scope='handoff', task_id='<task>', section={'summary': "
|
||||
"'<observation>', 'severity': 'info'|'watch'|'risk'}) to fill a "
|
||||
"task's auditor_notes — then retry i_am_idle()."
|
||||
),
|
||||
context_briefing=briefing,
|
||||
)
|
||||
|
||||
async def _pm_uncovered_decomposition_guard(
|
||||
self, agent_id: UUID, briefing: dict[str, Any]
|
||||
) -> Envelope | None:
|
||||
|
||||
@@ -272,13 +272,13 @@ class DocMixin(_Base):
|
||||
persisted to the task yet (the spec runner / verb body writes
|
||||
them via the atomic action / pre-dispatch stamp), so we thread
|
||||
them through a SimpleNamespace shim with the minimal attributes
|
||||
the foundation checkers read off the task object (dev_notes +
|
||||
the foundation checkers read off the task object (doc_notes +
|
||||
documents — see foundation.policy.tracing._check_docs_notes_min_chars
|
||||
and _check_docs_files_non_empty).
|
||||
"""
|
||||
has_reflect = await self.journal.has_reflect_for_task(doc_agent_id, task_id)
|
||||
task_view = SimpleNamespace(
|
||||
dev_notes=notes,
|
||||
doc_notes=notes,
|
||||
documents=list(files),
|
||||
)
|
||||
ctx = _tr.GateContext(
|
||||
|
||||
@@ -229,7 +229,9 @@ class PRGateMixin(_Base):
|
||||
if isinstance(pre, Envelope):
|
||||
return pre
|
||||
t, agent, role_str, briefing, spec_ctx = pre
|
||||
gate = await self._gate_tracing(reviewer_agent_id, task_id, t, role_str, verb)
|
||||
gate = await self._gate_tracing(
|
||||
reviewer_agent_id, task_id, t, role_str, verb, notes=notes
|
||||
)
|
||||
if gate is not None:
|
||||
return gate
|
||||
runner = self._verb_runner()
|
||||
@@ -332,14 +334,28 @@ class PRGateMixin(_Base):
|
||||
t: Any,
|
||||
role_str: str,
|
||||
verb: str,
|
||||
*,
|
||||
notes: str,
|
||||
) -> Envelope | None:
|
||||
"""pr_pass / pr_fail require a journal:learning entry (parity with QA)."""
|
||||
"""pr_pass / pr_fail require a journal:learning entry (parity with QA)
|
||||
plus a substantive pr_reviewer_notes section.
|
||||
|
||||
The section note is the verb's own ``notes`` argument (the review
|
||||
verdict / issues), not yet persisted to the task, so it is threaded
|
||||
through a SimpleNamespace shim (the foundation checker reads
|
||||
``task.pr_reviewer_notes`` — same write-then-gate pattern as qa_notes).
|
||||
"""
|
||||
from roboco.config import settings as _settings
|
||||
|
||||
has_learning = await self.journal.has_learning_for_task(
|
||||
reviewer_agent_id, task_id
|
||||
)
|
||||
ctx = _tr.GateContext(journal_learning_present=has_learning)
|
||||
ctx = _tr.GateContext(
|
||||
journal_learning_present=has_learning,
|
||||
pr_reviewer_notes_min_chars=_settings.pr_reviewer_notes_min_chars,
|
||||
)
|
||||
result = _tr.check_requirements(
|
||||
task=SimpleNamespace(),
|
||||
task=SimpleNamespace(pr_reviewer_notes=notes),
|
||||
requirements=list(_tr.requirements_for(verb)),
|
||||
ctx=ctx,
|
||||
)
|
||||
|
||||
@@ -293,7 +293,7 @@ class PRReviewerMixin(_Base):
|
||||
verb="post_pr_review",
|
||||
)
|
||||
gate = await self._pr_review_tracing_gate(
|
||||
reviewer_agent_id, task_id, t, role_str
|
||||
reviewer_agent_id, task_id, t, role_str, body=body
|
||||
)
|
||||
if gate is not None:
|
||||
return gate
|
||||
@@ -359,15 +359,33 @@ class PRReviewerMixin(_Base):
|
||||
}
|
||||
|
||||
async def _pr_review_tracing_gate(
|
||||
self, reviewer_agent_id: UUID, task_id: UUID, t: Any, role_str: str
|
||||
self,
|
||||
reviewer_agent_id: UUID,
|
||||
task_id: UUID,
|
||||
t: Any,
|
||||
role_str: str,
|
||||
*,
|
||||
body: str,
|
||||
) -> Envelope | None:
|
||||
"""post_pr_review requires a journal:learning entry (parity with QA)."""
|
||||
"""post_pr_review requires a journal:learning entry (parity with QA)
|
||||
plus a substantive pr_reviewer_notes section.
|
||||
|
||||
The section note is the verb's own ``body`` argument (the change
|
||||
request), not yet persisted to the task, so it is threaded through a
|
||||
SimpleNamespace shim (the foundation checker reads
|
||||
``task.pr_reviewer_notes`` — same write-then-gate pattern as qa_notes).
|
||||
"""
|
||||
from roboco.config import settings as _settings
|
||||
|
||||
has_learning = await self.journal.has_learning_for_task(
|
||||
reviewer_agent_id, task_id
|
||||
)
|
||||
ctx = _tr.GateContext(journal_learning_present=has_learning)
|
||||
ctx = _tr.GateContext(
|
||||
journal_learning_present=has_learning,
|
||||
pr_reviewer_notes_min_chars=_settings.pr_reviewer_notes_min_chars,
|
||||
)
|
||||
result = _tr.check_requirements(
|
||||
task=SimpleNamespace(),
|
||||
task=SimpleNamespace(pr_reviewer_notes=body),
|
||||
requirements=list(_tr.requirements_for("post_pr_review")),
|
||||
ctx=ctx,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user