feat(gateway): wire envelope introspection into qa.py + doc.py role mixins

Closes the Task 3 wiring loop: every Envelope construction site in the
QA and Documenter mixins now stamps current_state + valid_next_verbs.
Refactored doc._check_i_documented_inputs to take the loaded task as a
parameter so it can pass through to .with_introspection() without
re-fetching.

Task 3 of the 2026-05-08 gateway introspection plan is now complete
across _impl.py, qa.py, and doc.py.
This commit is contained in:
Renn F
2026-05-08 11:27:54 +02:00
parent 17630bf3fc
commit ebdbd7fc47
2 changed files with 23 additions and 16 deletions
+15 -9
View File
@@ -45,7 +45,7 @@ class DocMixin(_Base):
),
remediate="call give_me_work() to find an actionable doc task",
context_briefing=await self._briefing_for(doc_agent_id, task_id),
),
).with_introspection(task=t, role="documenter"),
agent_id=doc_agent_id,
task_id=task_id,
verb="claim_doc_task",
@@ -59,6 +59,7 @@ class DocMixin(_Base):
skip_sequence=True,
)
if guard:
guard.with_introspection(task=t, role="documenter")
return await self._emit_rejection(
self._with_briefing(
guard,
@@ -94,7 +95,7 @@ class DocMixin(_Base):
),
evidence=ev.as_dict(),
context_briefing=await self._briefing_for(doc_agent_id, task_id),
)
).with_introspection(task=t, role="documenter")
async def _verify_doc_owner(
self, doc_agent_id: UUID, task_id: UUID
@@ -114,7 +115,7 @@ class DocMixin(_Base):
message="not assigned to you",
remediate="claim it via claim_doc_task(task_id) first",
context_briefing=await self._briefing_for(doc_agent_id, task_id),
),
).with_introspection(task=t, role="documenter"),
agent_id=doc_agent_id,
task_id=task_id,
verb="i_documented",
@@ -122,7 +123,12 @@ class DocMixin(_Base):
return None, t
async def _check_i_documented_inputs(
self, doc_agent_id: UUID, task_id: UUID, notes: str, files: list[str]
self,
doc_agent_id: UUID,
task_id: UUID,
notes: str,
files: list[str],
task: Any,
) -> Envelope | None:
"""Validate notes length + files non-empty. Returns rejection or None."""
if not notes or len(notes) < settings.docs_notes_min_chars:
@@ -135,7 +141,7 @@ class DocMixin(_Base):
" Include each file in `files=...`."
),
context_briefing=await self._briefing_for(doc_agent_id, task_id),
),
).with_introspection(task=task, role="documenter"),
agent_id=doc_agent_id,
task_id=task_id,
verb="i_documented",
@@ -149,7 +155,7 @@ class DocMixin(_Base):
" listing the doc files written."
),
context_briefing=await self._briefing_for(doc_agent_id, task_id),
),
).with_introspection(task=task, role="documenter"),
agent_id=doc_agent_id,
task_id=task_id,
verb="i_documented",
@@ -167,11 +173,11 @@ class DocMixin(_Base):
Transitions awaiting_documentation → awaiting_pm_review.
"""
rejection, _ = await self._verify_doc_owner(doc_agent_id, task_id)
rejection, owned_task = await self._verify_doc_owner(doc_agent_id, task_id)
if rejection is not None:
return rejection
input_rejection = await self._check_i_documented_inputs(
doc_agent_id, task_id, notes, files
doc_agent_id, task_id, notes, files, owned_task
)
if input_rejection is not None:
return input_rejection
@@ -199,4 +205,4 @@ class DocMixin(_Base):
task_id=str(task_id),
next="idle until PM completes",
context_briefing=await self._briefing_for(doc_agent_id, task_id),
)
).with_introspection(task=t, role="documenter")
+8 -7
View File
@@ -55,7 +55,7 @@ class QAMixin(_Base):
),
remediate="call give_me_work() to find an actionable QA task",
context_briefing=await self._briefing_for(qa_agent_id, task_id),
),
).with_introspection(task=t, role="qa"),
agent_id=qa_agent_id,
task_id=task_id,
verb="claim_review",
@@ -69,6 +69,7 @@ class QAMixin(_Base):
skip_sequence=True,
)
if guard:
guard.with_introspection(task=t, role="qa")
return await self._emit_rejection(
self._with_briefing(
guard,
@@ -106,7 +107,7 @@ class QAMixin(_Base):
),
evidence=ev.as_dict(),
context_briefing=await self._briefing_for(qa_agent_id, task_id),
)
).with_introspection(task=t, role="qa")
async def _verify_qa_owner(
self, qa_agent_id: UUID, task_id: UUID, verb: str
@@ -126,7 +127,7 @@ class QAMixin(_Base):
message="not assigned to you",
remediate="claim it via claim_review(task_id) first",
context_briefing=await self._briefing_for(qa_agent_id, task_id),
),
).with_introspection(task=t, role="qa"),
agent_id=qa_agent_id,
task_id=task_id,
verb=verb,
@@ -150,7 +151,7 @@ class QAMixin(_Base):
missing,
task_id,
await self._briefing_for(qa_agent_id, task_id),
),
).with_introspection(task=t, role="qa"),
agent_id=qa_agent_id,
task_id=task_id,
verb=verb,
@@ -186,7 +187,7 @@ class QAMixin(_Base):
task_id=str(task_id),
next="idle until next QA work arrives",
context_briefing=await self._briefing_for(qa_agent_id, task_id),
)
).with_introspection(task=t, role="qa")
@staticmethod
def _check_qa_pass_gates(
@@ -240,7 +241,7 @@ class QAMixin(_Base):
message="fail_review requires at least one issue",
remediate="pass issues=['<concrete actionable issue>', ...]",
context_briefing=await self._briefing_for(qa_agent_id, task_id),
),
).with_introspection(task=t, role="qa"),
agent_id=qa_agent_id,
task_id=task_id,
verb="fail_review",
@@ -267,4 +268,4 @@ class QAMixin(_Base):
task_id=str(task_id),
next="idle — dev will revise and re-submit",
context_briefing=await self._briefing_for(qa_agent_id, task_id),
)
).with_introspection(task=t, role="qa")