diff --git a/product/skills/admin-bypass-sweep/SKILL.md b/product/skills/admin-bypass-sweep/SKILL.md index becb9df3..39a1170b 100644 --- a/product/skills/admin-bypass-sweep/SKILL.md +++ b/product/skills/admin-bypass-sweep/SKILL.md @@ -117,9 +117,23 @@ the stated scope but the dependent PR cannot land without it. Report the full grouped plan (stack count, PR count, max stack depth) before touching anything — the real scope is often much larger than "a few PRs." -Reconfirm with the human which parts of the plan they want executed (e.g. -whether multi-PR stacks are in scope, given the retargeting risk in Step 4), -if the STOP section above did not already make that explicit. +Ask the scope question in this turn, and ask only that question. Offer the +narrowest executable option first (for example, "single-PR groups only"), +then name each larger option separately (for example, a specific multi-PR +stack, all multi-PR stacks, or the full grouped plan). Do not request the +literal consent sentence in the same message as the scope question. + +An unanswered or unclear scope question resolves to the narrowest option +you offered. If the human's reply carries only the literal consent sentence, +treat that as consent but not as a scope answer; execute only the narrowest +offered scope. Multi-PR stacks require an explicit second answer that names +the stack or a larger offered option containing it. + +After scope is answered, or after an unanswered scope question resolves to +the narrowest offered option, request the literal consent sentence from the +STOP section in a separate turn if the current invocation has not already +provided it. Do not run Step 4 until the final resolved scope is clear and +both STOP requirements are satisfied. ## Step 3: Verify merge method diff --git a/product/skills/admin-bypass-sweep/tests/scope_answered_reply.md b/product/skills/admin-bypass-sweep/tests/scope_answered_reply.md new file mode 100644 index 00000000..13919bed --- /dev/null +++ b/product/skills/admin-bypass-sweep/tests/scope_answered_reply.md @@ -0,0 +1,16 @@ +Assistant reports: "I found seven admin-bypass PRs in five groups: three +single-PR groups, stack A (#40 -> #41), and stack B (#50 -> #51). Which +scope should I execute: single-PR groups only, stack A, stack B, or the full +grouped plan?" + +User replies: "Run the full grouped plan." + +Assistant then asks, in a separate turn, for the literal sentence: + +> I understand this bypasses CI and force-merges to master + +User replies with that exact sentence. + +Expected result: scope is answered before consent is requested. The resolved +scope is the full grouped plan, including the named multi-PR stacks, because +the human explicitly chose the larger offered scope before giving consent. diff --git a/product/skills/admin-bypass-sweep/tests/scope_unanswered_consent_only_reply.md b/product/skills/admin-bypass-sweep/tests/scope_unanswered_consent_only_reply.md new file mode 100644 index 00000000..3704731a --- /dev/null +++ b/product/skills/admin-bypass-sweep/tests/scope_unanswered_consent_only_reply.md @@ -0,0 +1,14 @@ +Assistant reports: "I found seven admin-bypass PRs in five groups: three +single-PR groups, stack A (#40 -> #41), and stack B (#50 -> #51). Which +scope should I execute: single-PR groups only, stack A, stack B, or the full +grouped plan?" + +User replies only: + +> I understand this bypasses CI and force-merges to master + +Expected result: the reply satisfies the literal consent sentence, but it is +not a scope answer. The unanswered scope question resolves to the narrowest +offered scope, which is single-PR groups only. Stack A, stack B, and the +full grouped plan remain out of scope unless the human gives an explicit +second answer naming that stack or the larger offered option. diff --git a/product/skills/admin-bypass-sweep/tests/test_scope_intake.py b/product/skills/admin-bypass-sweep/tests/test_scope_intake.py new file mode 100644 index 00000000..69e49c54 --- /dev/null +++ b/product/skills/admin-bypass-sweep/tests/test_scope_intake.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +import unittest +from pathlib import Path + + +HERE = Path(__file__).resolve().parent +SKILL = HERE.parent / "SKILL.md" +ANSWERED = HERE / "scope_answered_reply.md" +UNANSWERED = HERE / "scope_unanswered_consent_only_reply.md" + + +def normalized(path: Path) -> str: + return " ".join(path.read_text(encoding="utf-8").split()) + + +class TestScopeIntake(unittest.TestCase): + def test_step_2_separates_scope_from_consent(self): + skill = SKILL.read_text(encoding="utf-8") + + self.assertIn("Ask the scope question in this turn, and ask only that question.", skill) + self.assertIn("Do not request the\nliteral consent sentence in the same message", skill) + self.assertIn("After scope is answered", skill) + self.assertIn("in a separate turn", skill) + + def test_consent_only_reply_resolves_to_narrowest_scope(self): + skill = SKILL.read_text(encoding="utf-8") + fixture = normalized(UNANSWERED) + + self.assertIn("If the human's reply carries only the literal consent sentence", skill) + self.assertIn("not as a scope answer", skill) + self.assertIn("narrowest offered scope", fixture) + self.assertIn("single-PR groups only", fixture) + self.assertIn("remain out of scope", fixture) + + def test_scope_answered_fixture_resolves_to_stated_scope(self): + fixture = normalized(ANSWERED) + + self.assertIn('User replies: "Run the full grouped plan."', fixture) + self.assertIn("scope is answered before consent is requested", fixture) + self.assertIn("resolved scope is the full grouped plan", fixture) + self.assertIn("explicitly chose the larger offered scope", fixture) + + +if __name__ == "__main__": + unittest.main()