Require and validate Controller Handoff sections in workflow final reports
- Upgrade SKILL.md §K compact format to the issue #182 canonical field set (Task/Repo/Role/Identity/Issue-PR/Branch-SHA/Files/Validation/Mutations/ Current status/Blockers/Next/Safety) plus role-specific field lists for review/merge, author, and queue/inventory tasks. - Point the review-pr, merge-pr, and start-issue template handoff lines at the exactly-titled Controller Handoff section with their role fields. - Add review_proofs.assess_controller_handoff(): reports without the exact section are 'missing' (downgraded), present-but-partial are 'incomplete' with the absent fields listed, and role extras are enforced per role. - Add TestControllerHandoff (8 tests) including a SKILL.md doc-contract test so the documented requirement cannot silently rot. The handoff supplements the full report; full-report validation rules are unchanged. Closes #182 Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
@@ -21,6 +21,7 @@ import unittest
|
||||
sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parent.parent))
|
||||
|
||||
from review_proofs import ( # noqa: E402
|
||||
assess_controller_handoff,
|
||||
assess_inventory_completeness,
|
||||
assess_self_review_contamination,
|
||||
assess_validation_report,
|
||||
@@ -576,5 +577,107 @@ class TestRepoNameDisambiguation(unittest.TestCase):
|
||||
self.assertTrue(result["can_claim_exhaustive"])
|
||||
|
||||
|
||||
class TestControllerHandoff(unittest.TestCase):
|
||||
"""Issue #182: final reports must end with a Controller Handoff."""
|
||||
|
||||
BASE_HANDOFF = "\n".join([
|
||||
"## Controller Handoff",
|
||||
"",
|
||||
"- Task: implement issue #182",
|
||||
"- Repo: Scaled-Tech-Consulting/Gitea-Tools",
|
||||
"- Role: author",
|
||||
"- Identity: jcwalker3 / prgs-author",
|
||||
"- Issue/PR: #182 / PR #999",
|
||||
"- Branch/SHA: feat/x @ 0fdc8f582026b72a229d59a172c0a63ac4aaeaf9",
|
||||
"- Files changed: review_proofs.py",
|
||||
"- Validation: 700 passed, 6 skipped",
|
||||
"- Mutations: one PR opened",
|
||||
"- Current status: PR open",
|
||||
"- Blockers: none",
|
||||
"- Next: review PR #999",
|
||||
"- Safety: no self-review; no self-merge; no secrets",
|
||||
])
|
||||
|
||||
def test_report_without_handoff_is_downgraded(self):
|
||||
result = assess_controller_handoff("long report text, no handoff")
|
||||
self.assertEqual(result["verdict"], "missing")
|
||||
self.assertTrue(result["downgraded"])
|
||||
|
||||
def test_wrong_title_is_downgraded(self):
|
||||
text = self.BASE_HANDOFF.replace(
|
||||
"## Controller Handoff", "## Handoff Summary")
|
||||
result = assess_controller_handoff(text)
|
||||
self.assertEqual(result["verdict"], "missing")
|
||||
|
||||
def test_complete_base_handoff_passes(self):
|
||||
result = assess_controller_handoff(
|
||||
"full report body...\n\n" + self.BASE_HANDOFF)
|
||||
self.assertEqual(result["verdict"], "complete")
|
||||
self.assertFalse(result["downgraded"])
|
||||
|
||||
def test_missing_base_fields_are_listed(self):
|
||||
text = "\n".join(
|
||||
line for line in self.BASE_HANDOFF.splitlines()
|
||||
if not line.startswith(("- Mutations:", "- Safety:")))
|
||||
result = assess_controller_handoff(text)
|
||||
self.assertEqual(result["verdict"], "incomplete")
|
||||
self.assertTrue(result["downgraded"])
|
||||
self.assertIn("Mutations", result["missing_fields"])
|
||||
self.assertIn("Safety", result["missing_fields"])
|
||||
|
||||
def test_review_role_requires_review_fields(self):
|
||||
result = assess_controller_handoff(self.BASE_HANDOFF, role="review")
|
||||
self.assertEqual(result["verdict"], "incomplete")
|
||||
self.assertIn("Pinned reviewed head", result["missing_fields"])
|
||||
self.assertIn("Merge result", result["missing_fields"])
|
||||
|
||||
complete = self.BASE_HANDOFF + "\n" + "\n".join([
|
||||
"- Selected PR: #999",
|
||||
"- Reviewer eligibility: passed",
|
||||
"- Pinned reviewed head: 0fdc8f582026b72a229d59a172c0a63ac4aaeaf9",
|
||||
"- Review decision: approve",
|
||||
"- Merge result: merged",
|
||||
"- Linked issue status: closed",
|
||||
"- Cleanup status: branch deleted",
|
||||
])
|
||||
result = assess_controller_handoff(complete, role="review")
|
||||
self.assertEqual(result["verdict"], "complete")
|
||||
|
||||
def test_author_role_requires_author_fields(self):
|
||||
complete = self.BASE_HANDOFF + "\n" + "\n".join([
|
||||
"- Selected issue: #182",
|
||||
"- Claim/comment status: comment-claimed",
|
||||
"- PR number opened: #999",
|
||||
"- No review/merge: confirmed",
|
||||
])
|
||||
result = assess_controller_handoff(complete, role="author")
|
||||
self.assertEqual(result["verdict"], "complete")
|
||||
|
||||
result = assess_controller_handoff(self.BASE_HANDOFF, role="author")
|
||||
self.assertEqual(result["verdict"], "incomplete")
|
||||
self.assertIn("No review/merge confirmation", result["missing_fields"])
|
||||
|
||||
def test_inventory_role_requires_inventory_fields(self):
|
||||
complete = self.BASE_HANDOFF + "\n" + "\n".join([
|
||||
"- Repositories checked: Gitea-Tools, mcp-control-plane",
|
||||
"- Open PR counts: 2 / 0",
|
||||
"- Selected PR or reason: none eligible (self-authored)",
|
||||
"- Inventory completeness: complete, no pagination needed",
|
||||
])
|
||||
result = assess_controller_handoff(complete, role="inventory")
|
||||
self.assertEqual(result["verdict"], "complete")
|
||||
|
||||
def test_skill_doc_declares_handoff_requirement(self):
|
||||
# Doc-contract: SKILL.md must keep requiring the exact section and
|
||||
# naming this validator, or the convention silently rots.
|
||||
skill = (
|
||||
__import__("pathlib").Path(__file__).resolve().parent.parent
|
||||
/ "skills" / "llm-project-workflow" / "SKILL.md"
|
||||
).read_text(encoding="utf-8")
|
||||
self.assertIn("## Controller Handoff", skill)
|
||||
self.assertIn("assess_controller_handoff", skill)
|
||||
self.assertIn("issue #182", skill)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user