diff --git a/review_proofs.py b/review_proofs.py index 925e554..838e6f1 100644 --- a/review_proofs.py +++ b/review_proofs.py @@ -1046,6 +1046,8 @@ HANDOFF_ROLE_FIELDS = { ("Continuation mode", ("continuation mode", "continuation")), ("Existing PR", ("existing pr", "pr number")), ("PR author", ("pr author", "existing pr author")), + ("Issue claim status", ("issue claim", "claim status", + "status:in-progress")), ("Branch", ("branch", "existing branch")), ("Old PR head", ("old pr head", "old head")), ("New PR head", ("new pr head", "new head")), @@ -1054,6 +1056,13 @@ HANDOFF_ROLE_FIELDS = { ), } +# Canonical secret/provenance sweep for comparable continuation runs (#189). +CANONICAL_SECRET_SWEEP_COMMAND = ( + "git diff prgs/master...HEAD | rg -i " + "'(token|password|secret|api[_-]?key|authorization:)'" +) +CANONICAL_SECRET_SWEEP_SCOPE = "full feature-branch diff against prgs/master" + def _handoff_section_lines(report_text): """Return the lines of the Controller Handoff section, or None.""" @@ -1657,18 +1666,89 @@ def assess_fresh_issue_selection(classifications: list[dict] | None) -> dict: } +def canonical_secret_sweep_report(*, clean: bool) -> dict: + """Return a sweep report using the canonical command/scope (#189).""" + return { + "method": CANONICAL_SECRET_SWEEP_COMMAND, + "scope": CANONICAL_SECRET_SWEEP_SCOPE, + "clean": clean, + } + + +def assess_force_with_lease_push_report( + report_text: str, + *, + old_remote_head: str | None = None, + expected_lease_head: str | None = None, + new_pushed_head: str | None = None, + branch_pushed: str | None = None, + used_force_with_lease: bool | None = None, +) -> dict: + """Issue #189: force-with-lease pushes must disclose lease evidence.""" + if not used_force_with_lease: + return {"complete": True, "downgraded": False, "reasons": []} + + text = report_text or "" + lower = text.lower() + reasons = [] + + if "force-with-lease" not in lower and "force with lease" not in lower: + reasons.append( + "continuation push used force-with-lease but report does not say so" + ) + + for label, sha in ( + ("old remote", old_remote_head), + ("lease", expected_lease_head), + ("pushed", new_pushed_head), + ): + if not sha: + reasons.append(f"force-with-lease proof missing {label} head SHA") + elif not _FULL_SHA.match(sha.lower()): + reasons.append( + f"force-with-lease {label} head SHA is not a full 40-hex SHA" + ) + elif sha.lower() not in lower: + reasons.append( + f"continuation report missing {label} head SHA in evidence" + ) + + if branch_pushed: + if branch_pushed.lower() not in lower: + reasons.append("continuation report missing pushed branch name") + only_branch_tokens = ( + "only feature branch", + "push branch only", + "only the feature branch", + "only pushed feature branch", + ) + if not any(t in lower for t in only_branch_tokens): + reasons.append( + "continuation report missing confirmation that only the " + "feature branch was pushed" + ) + + return { + "complete": not reasons, + "downgraded": bool(reasons), + "reasons": reasons, + } + + def assess_continuation_mode_report( report_text: str, *, pr_number: int | None = None, pr_author: str | None = None, + issue_number: int | None = None, + issue_claim_status: str | None = None, branch: str | None = None, old_head_sha: str | None = None, new_head_sha: str | None = None, session_authored_pr: bool | None = None, continuation_allowed_reason: str | None = None, ) -> dict: - """Issue #188: continuation mode must disclose full PR evidence.""" + """Issue #188/#189: continuation mode must disclose full PR evidence.""" text = report_text or "" lower = text.lower() reasons = [] @@ -1681,6 +1761,23 @@ def assess_continuation_mode_report( reasons.append(f"continuation report missing PR #{pr_number}") if pr_author and pr_author.lower() not in lower: reasons.append("continuation report missing PR author") + if issue_number is not None: + if ( + f"#{issue_number}" not in lower + and f"issue #{issue_number}" not in lower + and f"issue {issue_number}" not in lower + ): + reasons.append(f"continuation report missing issue #{issue_number}") + if issue_claim_status: + claim_lower = issue_claim_status.lower() + claim_tokens = ( + claim_lower, + "claim status", + "issue claim", + "status:in-progress", + ) + if not any(t in lower for t in claim_tokens): + reasons.append("continuation report missing issue claim status") if branch and branch.lower() not in lower: reasons.append("continuation report missing branch name") @@ -1817,12 +1914,26 @@ def assess_issue_selection_final_report( report_text, pr_number=proof.get("pr_number"), pr_author=proof.get("pr_author"), + issue_number=proof.get("issue_number"), + issue_claim_status=proof.get("issue_claim_status"), branch=proof.get("branch"), old_head_sha=proof.get("old_head_sha"), new_head_sha=proof.get("new_head_sha"), session_authored_pr=proof.get("session_authored_pr"), continuation_allowed_reason=proof.get("continuation_allowed_reason"), ) + push_proof = proof.get("push_proof") or {} + checks["force_with_lease_push"] = assess_force_with_lease_push_report( + report_text, + old_remote_head=push_proof.get("old_remote_head"), + expected_lease_head=push_proof.get("expected_lease_head"), + new_pushed_head=push_proof.get("new_pushed_head"), + branch_pushed=push_proof.get("branch_pushed"), + used_force_with_lease=push_proof.get("used_force_with_lease"), + ) + sweep = proof.get("secret_sweep") + if sweep is not None: + checks["secret_sweep"] = assess_secret_sweep(sweep) reasons = [] downgraded = False @@ -1831,6 +1942,11 @@ def assess_issue_selection_final_report( if verdict in ("missing", "incomplete"): downgraded = True reasons.extend(result.get("reasons") or []) + elif result.get("proven") is False: + downgraded = True + reasons.extend( + f"{name}: {r}" for r in (result.get("reasons") or []) + ) elif result.get("downgraded") or not result.get("complete", True): downgraded = True reasons.extend( diff --git a/skills/llm-project-workflow/SKILL.md b/skills/llm-project-workflow/SKILL.md index 1145bcd..7aee9ca 100644 --- a/skills/llm-project-workflow/SKILL.md +++ b/skills/llm-project-workflow/SKILL.md @@ -379,11 +379,16 @@ Role-specific fields (append to the compact block): `Linked issue status:`, `Cleanup status:` - author tasks: `Selected issue:`, `Claim/comment status:`, `PR number opened:`, `No review/merge:` (explicit confirmation) -- continuation tasks (#188): `Continuation mode:`, `Existing PR:`, - `PR author:`, `Branch:`, `Old PR head:`, `New PR head:`, - `Session authored PR:`, `Why continuation allowed:` — issues with open - PRs are excluded from fresh selection unless operator explicitly requests - continuation (`review_proofs.classify_issue_for_selection`, +- continuation tasks (#188/#189): `Continuation mode:`, `Existing PR:`, + `PR author:`, `Issue claim status:`, `Branch:`, `Old PR head:`, + `New PR head:`, `Session authored PR:`, `Why continuation allowed:` — + when rebasing with `git push --force-with-lease`, also record + `Old remote head:`, `Lease head:`, `Pushed head:`, and confirm + `Push branch only:` (feature branch only). Use the canonical secret sweep + from `review_proofs.CANONICAL_SECRET_SWEEP_COMMAND` so runs are + comparable. Issues with open PRs are excluded from fresh selection unless + the operator explicitly requests continuation + (`review_proofs.classify_issue_for_selection`, `assess_issue_selection_final_report`) - queue/inventory tasks: `Repositories checked:`, `Open PR counts:`, `Selected PR or reason none selected:`, `Inventory completeness:` diff --git a/tests/test_review_proofs.py b/tests/test_review_proofs.py index 8ce6298..751d48a 100644 --- a/tests/test_review_proofs.py +++ b/tests/test_review_proofs.py @@ -28,6 +28,9 @@ from review_proofs import ( # noqa: E402 assess_capability_proof, assess_contradictory_no_pr_claim, assess_continuation_mode_report, + assess_force_with_lease_push_report, + canonical_secret_sweep_report, + CANONICAL_SECRET_SWEEP_COMMAND, assess_controller_handoff, assess_edited_pr_inventory_coverage, assess_empty_queue_report, @@ -1761,6 +1764,7 @@ class TestIssueSelectionContinuation(unittest.TestCase): "- Continuation mode: issue #182 continuation", "- Existing PR: #186", "- PR author: jcwalker3", + "- Issue claim status: status:in-progress", "- Branch: feat/issue-182-controller-handoff", f"- Old PR head: {self.OLD_SHA}", f"- New PR head: {self.NEW_SHA}", @@ -1773,6 +1777,8 @@ class TestIssueSelectionContinuation(unittest.TestCase): continuation_proof={ "pr_number": 186, "pr_author": "jcwalker3", + "issue_number": 182, + "issue_claim_status": "status:in-progress", "branch": "feat/issue-182-controller-handoff", "old_head_sha": self.OLD_SHA, "new_head_sha": self.NEW_SHA, @@ -1785,5 +1791,136 @@ class TestIssueSelectionContinuation(unittest.TestCase): self.assertEqual(result["grade"], "A") +class TestContinuationModeProofs(unittest.TestCase): + """Issue #189: formal continuation proofs beyond #188 selection wall.""" + + OLD_SHA = PINNED + NEW_SHA = OTHER + REMOTE_OLD = "601c608c00000000000000000000000000000000" + LEASE_SHA = "45c5cac2bc49dd112766ea218b18a71e9c5f8e99" + PUSHED_SHA = "45c5cac2bc49dd112766ea218b18a71e9c5f8e99" + + def test_force_with_lease_requires_lease_evidence(self): + report = ( + "Issue #182 continuation. force-with-lease push to " + "feat/issue-182-controller-handoff-enforcement." + ) + result = assess_force_with_lease_push_report( + report, + old_remote_head=self.REMOTE_OLD, + expected_lease_head=self.LEASE_SHA, + new_pushed_head=self.PUSHED_SHA, + branch_pushed="feat/issue-182-controller-handoff-enforcement", + used_force_with_lease=True, + ) + self.assertTrue(result["downgraded"]) + + def test_force_with_lease_complete_with_full_evidence(self): + report = ( + "Issue #182 continuation with git push --force-with-lease. " + f"Old remote head {self.REMOTE_OLD}. " + f"Lease head {self.LEASE_SHA}. " + f"Pushed head {self.PUSHED_SHA}. " + "Branch feat/issue-182-controller-handoff-enforcement. " + "Push branch only: only the feature branch was pushed." + ) + result = assess_force_with_lease_push_report( + report, + old_remote_head=self.REMOTE_OLD, + expected_lease_head=self.LEASE_SHA, + new_pushed_head=self.PUSHED_SHA, + branch_pushed="feat/issue-182-controller-handoff-enforcement", + used_force_with_lease=True, + ) + self.assertTrue(result["complete"]) + + def test_continuation_requires_issue_claim_status(self): + report = ( + "Issue #182 continuation mode. PR #186. " + f"old head {self.OLD_SHA} new head {self.NEW_SHA}. " + "PR author jcwalker3. Branch feat/issue-182-test." + ) + result = assess_continuation_mode_report( + report, + pr_number=186, + pr_author="jcwalker3", + issue_number=182, + issue_claim_status="status:in-progress", + branch="feat/issue-182-test", + old_head_sha=self.OLD_SHA, + new_head_sha=self.NEW_SHA, + ) + self.assertTrue(result["downgraded"]) + + def test_canonical_secret_sweep_helper(self): + sweep = canonical_secret_sweep_report(clean=True) + self.assertEqual(sweep["method"], CANONICAL_SECRET_SWEEP_COMMAND) + self.assertTrue(sweep["clean"]) + + def test_continuation_final_report_with_push_and_sweep_earns_a(self): + branch = "feat/issue-182-controller-handoff-enforcement" + report = "\n".join([ + "Issue #182 continuation mode — status:in-progress already set.", + f"PR #186 updated via git push --force-with-lease.", + f"Old PR head {self.OLD_SHA}; new PR head {self.NEW_SHA}.", + f"Old remote head {self.REMOTE_OLD}.", + f"Lease head {self.LEASE_SHA}. Pushed head {self.PUSHED_SHA}.", + f"Branch {branch}. Push branch only: only the feature branch.", + "PR author: jcwalker3. Session authored PR: yes.", + "Continuation allowed: operator requested rebase.", + "Open PR inventory included PR #186.", + "## Controller Handoff", + "- Task: continuation", + "- Repo: Scaled-Tech-Consulting/Gitea-Tools", + "- Role: author", + "- Identity: prgs-author", + "- Issue/PR: #182 / PR #186", + f"- Branch/SHA: {branch}", + "- Files changed: review_proofs.py", + "- Validation: tests passed", + "- Mutations: push_branch", + "- Workspace mutations: none", + "- Current status: PR mergeable", + "- Blockers: none", + "- Next: review", + "- Safety: no review/merge", + "- Continuation mode: issue #182 continuation", + "- Existing PR: #186", + "- PR author: jcwalker3", + "- Issue claim status: status:in-progress", + f"- Branch: {branch}", + f"- Old PR head: {self.OLD_SHA}", + f"- New PR head: {self.NEW_SHA}", + "- Session authored PR: yes", + "- Why continuation allowed: operator requested rebase", + ]) + result = assess_issue_selection_final_report( + report, + mode="continuation", + continuation_proof={ + "pr_number": 186, + "pr_author": "jcwalker3", + "issue_number": 182, + "issue_claim_status": "status:in-progress", + "branch": branch, + "old_head_sha": self.OLD_SHA, + "new_head_sha": self.NEW_SHA, + "session_authored_pr": True, + "continuation_allowed_reason": "operator requested rebase", + "push_proof": { + "old_remote_head": self.REMOTE_OLD, + "expected_lease_head": self.LEASE_SHA, + "new_pushed_head": self.PUSHED_SHA, + "branch_pushed": branch, + "used_force_with_lease": True, + }, + "secret_sweep": canonical_secret_sweep_report(clean=True), + }, + edited_pr_numbers=[186], + inventoried_pr_numbers=[186], + ) + self.assertEqual(result["grade"], "A") + + if __name__ == "__main__": unittest.main()