From ab06f58e1e71ee03652055349ae9506adf6407ac Mon Sep 17 00:00:00 2001 From: Invoker Bot Date: Sat, 12 Sep 2026 18:02:23 +0000 Subject: [PATCH 1/3] =?UTF-8?q?invoker:=20wf-1789235894808-16/implement-re?= =?UTF-8?q?base-probe-script=20=E2=80=94=20Ship=20a=20rebase=20probe=20scr?= =?UTF-8?q?ipt=20whose=20setup=20failure=20is=20its=20own=20exit=20code.?= =?UTF-8?q?=20Review=20claim:=20A=20rebase=20probe=20that=20could=20not=20?= =?UTF-8?q?run=20exits=20unchecked,=20never=20conflicted.=20Review=20lane:?= =?UTF-8?q?=20behavior=20Safety=20invariant:=20A=20setup=20failure=20can?= =?UTF-8?q?=20never=20produce=20the=20conflict=20exit=20code;=20the=20scri?= =?UTF-8?q?pt=20reuses=20the=20contract=20already=20shipped=20at=20engine/?= =?UTF-8?q?hooks/gh-write-verification/verify=5Fpr=5Flanded=5Fon=5Ftrunk.s?= =?UTF-8?q?h=20lines=204-6=20(EXIT=5FOK=3D0,=20EXIT=5FFAIL=3D1,=20EXIT=5FU?= =?UTF-8?q?NCHECKED=3D3)=20rather=20than=20defining=20a=20new=20one.=20Eff?= =?UTF-8?q?ectiveness=20measurement:=20A=20fixture=20that=20removes=20the?= =?UTF-8?q?=20scratch=20directory=20without=20deregistering=20it=20makes?= =?UTF-8?q?=20the=20probe=20exit=203.=20The=20same=20fixture=20under=20a?= =?UTF-8?q?=20two-branch=20loop=20yields=20the=20conflict=20code,=20so=20t?= =?UTF-8?q?he=20test=20fails=20before=20the=20change=20and=20passes=20afte?= =?UTF-8?q?r.=20Slice=20rationale:=20One=20executable=20and=20its=20tests,?= =?UTF-8?q?=20with=20no=20caller=20changes,=20so=20the=20exit=20contract?= =?UTF-8?q?=20can=20be=20reviewed=20on=20its=20own.=20Architectural=20effe?= =?UTF-8?q?ct:=20Adds=20one=20shell=20script=20under=20scripts/=20and=20it?= =?UTF-8?q?s=20test=20module.=20Goal:=20Add=20scripts/probe=5Fbranch=5Freb?= =?UTF-8?q?ase.sh=20exiting=200=20on=20a=20clean=20rebase,=201=20on=20a=20?= =?UTF-8?q?real=20content=20conflict,=20and=203=20when=20the=20probe=20cou?= =?UTF-8?q?ld=20not=20run.=20Motivation:=20During=20a=20sweep=20of=20ninet?= =?UTF-8?q?een=20pull=20requests=20an=20inline=20loop=20reported=20three?= =?UTF-8?q?=20conflicts=20when=20no=20rebase=20had=20executed,=20because?= =?UTF-8?q?=20a=20stale=20worktree=20registration=20made=20setup=20fail=20?= =?UTF-8?q?into=20the=20same=20branch=20as=20a=20genuine=20conflict.=20The?= =?UTF-8?q?=20same=20two-branch=20shape=20gated=20a=20force-push=20and=20a?= =?UTF-8?q?n=20override=20merge=20on=20a=20clean=20result.=20Alternative?= =?UTF-8?q?=20considerations:=20Folding=20this=20into=20the=20caller=20was?= =?UTF-8?q?=20rejected=20because=20the=20exit=20contract=20is=20the=20revi?= =?UTF-8?q?ewable=20unit.=20Making=20the=20unchecked=20case=20exit=201=20w?= =?UTF-8?q?as=20rejected=20because=20it=20is=20the=20exact=20conflation=20?= =?UTF-8?q?being=20removed.=20Implementation=20details:=20Create=20scripts?= =?UTF-8?q?/probe=5Fbranch=5Frebase.sh=20taking=20a=20source=20ref=20and?= =?UTF-8?q?=20a=20base=20ref.=20Declare=20the=20three=20exit=20codes=20to?= =?UTF-8?q?=20match=20engine/hooks/gh-write-verification/verify=5Fpr=5Flan?= =?UTF-8?q?ded=5Fon=5Ftrunk.sh=20lines=204-6=20(EXIT=5FOK=3D0,=20EXIT=5FFA?= =?UTF-8?q?IL=3D1,=20EXIT=5FUNCHECKED=3D3).=20Any=20failure=20while=20crea?= =?UTF-8?q?ting=20the=20scratch=20worktree=20exits=203=20with=20the=20unde?= =?UTF-8?q?rlying=20error=20on=20stderr=20and=20must=20not=20attempt=20the?= =?UTF-8?q?=20rebase.=20Only=20a=20rebase=20that=20actually=20ran=20may=20?= =?UTF-8?q?yield=200=20or=201.=20Tear=20down=20with=20git=20worktree=20rem?= =?UTF-8?q?ove=20--force.=20Add=20tests/test=5Fprobe=5Fbranch=5Frebase.py?= =?UTF-8?q?=20with=20one=20case=20per=20exit=20code.=20Non-goals:=20Does?= =?UTF-8?q?=20not=20change=20any=20caller,=20does=20not=20resolve=20confli?= =?UTF-8?q?cts,=20does=20not=20alter=20merge=20commands,=20and=20does=20no?= =?UTF-8?q?t=20edit=20any=20skill.=20Layer:=20domain=20Feature=20state:=20?= =?UTF-8?q?active=20Files:=20scripts/probe=5Fbranch=5Frebase.sh,=20tests/t?= =?UTF-8?q?est=5Fprobe=5Fbranch=5Frebase.py=20Change=20types:=20-=20script?= =?UTF-8?q?s/probe=5Fbranch=5Frebase.sh:=20create=20-=20tests/test=5Fprobe?= =?UTF-8?q?=5Fbranch=5Frebase.py:=20create=20Acceptance=20criteria:=20-=20?= =?UTF-8?q?`python3=20-m=20unittest=20discover=20-s=20tests=20-p=20'test?= =?UTF-8?q?=5Fprobe=5Fbranch=5Frebase*.py'=20-v`=20exits=200.=20-=20The=20?= =?UTF-8?q?probe=20exits=203,=20not=201,=20when=20its=20scratch=20worktree?= =?UTF-8?q?=20cannot=20be=20created.=20-=20scripts/probe=5Fbranch=5Frebase?= =?UTF-8?q?.sh=20contains=20no=20`rm=20-rf`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Solution: Ship a rebase probe script whose setup failure is its own exit code. Review claim: A rebase probe that could not run exits unchecked, never conflicted. Review lane: behavior Safety invariant: A setup failure can never produce the conflict exit code; the script reuses the contract already shipped at engine/hooks/gh-write-verification/verify_pr_landed_on_trunk.sh lines 4-6 (EXIT_OK=0, EXIT_FAIL=1, EXIT_UNCHECKED=3) rather than defining a new one. Effectiveness measurement: A fixture that removes the scratch directory without deregistering it makes the probe exit 3. The same fixture under a two-branch loop yields the conflict code, so the test fails before the change and passes after. Slice rationale: One executable and its tests, with no caller changes, so the exit contract can be reviewed on its own. Architectural effect: Adds one shell script under scripts/ and its test module. Goal: Add scripts/probe_branch_rebase.sh exiting 0 on a clean rebase, 1 on a real content conflict, and 3 when the probe could not run. Motivation: During a sweep of nineteen pull requests an inline loop reported three conflicts when no rebase had executed, because a stale worktree registration made setup fail into the same branch as a genuine conflict. The same two-branch shape gated a force-push and an override merge on a clean result. Alternative considerations: Folding this into the caller was rejected because the exit contract is the reviewable unit. Making the unchecked case exit 1 was rejected because it is the exact conflation being removed. Implementation details: Create scripts/probe_branch_rebase.sh taking a source ref and a base ref. Declare the three exit codes to match engine/hooks/gh-write-verification/verify_pr_landed_on_trunk.sh lines 4-6 (EXIT_OK=0, EXIT_FAIL=1, EXIT_UNCHECKED=3). Any failure while creating the scratch worktree exits 3 with the underlying error on stderr and must not attempt the rebase. Only a rebase that actually ran may yield 0 or 1. Tear down with git worktree remove --force. Add tests/test_probe_branch_rebase.py with one case per exit code. Non-goals: Does not change any caller, does not resolve conflicts, does not alter merge commands, and does not edit any skill. Layer: domain Feature state: active Files: scripts/probe_branch_rebase.sh, tests/test_probe_branch_rebase.py Change types: - scripts/probe_branch_rebase.sh: create - tests/test_probe_branch_rebase.py: create Acceptance criteria: - `python3 -m unittest discover -s tests -p 'test_probe_branch_rebase*.py' -v` exits 0. - The probe exits 3, not 1, when its scratch worktree cannot be created. - scripts/probe_branch_rebase.sh contains no `rm -rf`. Invoker-Finalize-Id: 259aacb5-064f-4dbf-a36f-f3fd4445463d --- scripts/probe_branch_rebase.sh | 91 ++++++++++++++++++++ tests/test_probe_branch_rebase.py | 135 ++++++++++++++++++++++++++++++ 2 files changed, 226 insertions(+) create mode 100755 scripts/probe_branch_rebase.sh create mode 100644 tests/test_probe_branch_rebase.py diff --git a/scripts/probe_branch_rebase.sh b/scripts/probe_branch_rebase.sh new file mode 100755 index 00000000..358a0b1f --- /dev/null +++ b/scripts/probe_branch_rebase.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +set -uo pipefail + +EXIT_OK=0 +EXIT_FAIL=1 +EXIT_UNCHECKED=3 +EXIT_USAGE=64 + +usage() { + echo "usage: probe_branch_rebase.sh " >&2 + echo " exit ${EXIT_OK} OK rebases cleanly onto " >&2 + echo " exit ${EXIT_FAIL} FAIL rebase ran and found a content conflict" >&2 + echo " exit ${EXIT_UNCHECKED} UNCHECKED the probe could not run; nothing is proven" >&2 + echo " exit ${EXIT_USAGE} usage error" >&2 + exit "$EXIT_USAGE" +} + +unchecked() { + echo "UNCHECKED: $*" >&2 + exit "$EXIT_UNCHECKED" +} + +safe_name() { + printf '%s' "$1" | LC_ALL=C tr -c 'A-Za-z0-9._-' '_' | cut -c1-40 +} + +cleanup() { + if [ -n "${SCRATCH_DIR:-}" ]; then + git worktree remove --force "$SCRATCH_DIR" >/dev/null 2>&1 || true + fi +} + +[ "$#" -eq 2 ] || usage + +SOURCE_REF="$1" +BASE_REF="$2" +SCRATCH_DIR="" + +if ! git rev-parse --git-dir >/dev/null; then + unchecked "$(pwd) is not a git checkout" +fi + +if ! SCRATCH_ROOT="$(git rev-parse --git-path rebase-probe-worktrees)"; then + unchecked "could not resolve scratch worktree root" +fi + +case "$SCRATCH_ROOT" in + /*) ;; + *) + if ! REPO_ROOT="$(git rev-parse --show-toplevel)"; then + unchecked "could not resolve repository root" + fi + SCRATCH_ROOT="${REPO_ROOT}/${SCRATCH_ROOT}" + ;; +esac + +if ! mkdir -p "$SCRATCH_ROOT"; then + unchecked "could not create scratch worktree root: ${SCRATCH_ROOT}" +fi + +if ! git rev-parse --verify --quiet "${BASE_REF}^{commit}" >/dev/null; then + unchecked "base ref is not a commit: ${BASE_REF}" +fi + +if ! DIGEST="$(printf '%s\n%s\n' "$SOURCE_REF" "$BASE_REF" | git hash-object --stdin)"; then + unchecked "could not build scratch worktree name" +fi + +SAFE_SOURCE="$(safe_name "$SOURCE_REF")" +SAFE_BASE="$(safe_name "$BASE_REF")" +SCRATCH_DIR="${SCRATCH_ROOT}/probe-${SAFE_SOURCE}-onto-${SAFE_BASE}-${DIGEST:0:12}" + +if ! git worktree add --detach "$SCRATCH_DIR" "$SOURCE_REF"; then + unchecked "could not create scratch worktree: ${SCRATCH_DIR}" +fi + +trap cleanup EXIT + +if git -C "$SCRATCH_DIR" rebase "$BASE_REF"; then + exit "$EXIT_OK" +fi + +if ! UNMERGED="$(git -C "$SCRATCH_DIR" ls-files -u)"; then + unchecked "could not inspect rebase failure in scratch worktree: ${SCRATCH_DIR}" +fi + +if [ -n "$UNMERGED" ]; then + exit "$EXIT_FAIL" +fi + +unchecked "rebase failed without content conflicts: ${SOURCE_REF} onto ${BASE_REF}" diff --git a/tests/test_probe_branch_rebase.py b/tests/test_probe_branch_rebase.py new file mode 100644 index 00000000..b11656fb --- /dev/null +++ b/tests/test_probe_branch_rebase.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import os +import shutil +import string +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +REPO = Path(__file__).resolve().parents[1] +SCRIPT = REPO / "scripts" / "probe_branch_rebase.sh" +sys.path.insert(0, str(REPO / "scripts")) + +from git_test_repo import init_repo # noqa: E402 + +HERMETIC_ENV = dict( + os.environ, + GIT_CONFIG_GLOBAL=os.devnull, + GIT_CONFIG_NOSYSTEM="1", + GIT_AUTHOR_NAME="Fixture", + GIT_AUTHOR_EMAIL="fixture@example.invalid", + GIT_COMMITTER_NAME="Fixture", + GIT_COMMITTER_EMAIL="fixture@example.invalid", +) + + +class Fixture: + def __init__(self, root: Path): + self.root = root + init_repo(root, "-b", "main", env=HERMETIC_ENV) + self.commit("seed", {"file.txt": "seed\n"}) + + def git(self, *args: str) -> str: + return subprocess.run( + ["git", "-C", str(self.root), *args], + check=True, + capture_output=True, + text=True, + env=HERMETIC_ENV, + ).stdout.strip() + + def commit(self, message: str, files: dict[str, str]) -> None: + for path, text in files.items(): + target = self.root / path + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(text, encoding="utf-8") + self.git("add", path) + self.git("commit", "-q", "-m", message) + + def run_probe(self, source_ref: str, base_ref: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["bash", str(SCRIPT), source_ref, base_ref], + cwd=self.root, + capture_output=True, + text=True, + env=HERMETIC_ENV, + ) + + def scratch_path(self, source_ref: str, base_ref: str) -> Path: + allowed = set(string.ascii_letters + string.digits + "._-") + + def safe(value: str) -> str: + return "".join(ch if ch in allowed else "_" for ch in value)[:40] + + digest = subprocess.run( + ["git", "-C", str(self.root), "hash-object", "--stdin"], + input=f"{source_ref}\n{base_ref}\n", + check=True, + capture_output=True, + text=True, + env=HERMETIC_ENV, + ).stdout.strip()[:12] + scratch_root = Path(self.git("rev-parse", "--git-path", "rebase-probe-worktrees")) + if not scratch_root.is_absolute(): + scratch_root = self.root / scratch_root + return scratch_root / f"probe-{safe(source_ref)}-onto-{safe(base_ref)}-{digest}" + + def add_stale_probe_worktree(self, source_ref: str, base_ref: str) -> None: + scratch = self.scratch_path(source_ref, base_ref) + scratch.parent.mkdir(parents=True, exist_ok=True) + self.git("worktree", "add", "--detach", str(scratch), source_ref) + shutil.rmtree(scratch) + + +class ProbeBranchRebaseTest(unittest.TestCase): + def setUp(self): + self._tmp = tempfile.TemporaryDirectory() + self.addCleanup(self._tmp.cleanup) + self.repo = Fixture(Path(self._tmp.name) / "repo") + + def make_clean_rebase(self) -> None: + self.repo.git("checkout", "-q", "-b", "topic") + self.repo.commit("topic adds file", {"topic.txt": "topic\n"}) + self.repo.git("checkout", "-q", "main") + self.repo.commit("base adds file", {"base.txt": "base\n"}) + + def make_conflicting_rebase(self) -> None: + self.repo.git("checkout", "-q", "-b", "topic") + self.repo.commit("topic edits file", {"file.txt": "topic\n"}) + self.repo.git("checkout", "-q", "main") + self.repo.commit("base edits file", {"file.txt": "base\n"}) + + def test_clean_rebase_exits_ok(self): + self.make_clean_rebase() + + result = self.repo.run_probe("topic", "main") + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertFalse(self.repo.scratch_path("topic", "main").exists()) + + def test_content_conflict_exits_fail(self): + self.make_conflicting_rebase() + + result = self.repo.run_probe("topic", "main") + + self.assertEqual(result.returncode, 1, result.stderr) + self.assertIn("could not apply", result.stderr) + self.assertFalse(self.repo.scratch_path("topic", "main").exists()) + + def test_missing_registered_scratch_worktree_exits_unchecked(self): + self.make_conflicting_rebase() + self.repo.add_stale_probe_worktree("topic", "main") + + result = self.repo.run_probe("topic", "main") + + self.assertEqual(result.returncode, 3, result.stderr) + self.assertIn("missing but already registered worktree", result.stderr) + self.assertIn("UNCHECKED: could not create scratch worktree", result.stderr) + + +if __name__ == "__main__": + unittest.main() From 828364c53b6d86ca56d3e1dfa5cc883f2977943d Mon Sep 17 00:00:00 2001 From: Invoker Bot Date: Sat, 12 Sep 2026 18:03:17 +0000 Subject: [PATCH 2/3] =?UTF-8?q?invoker:=20wf-1789235894808-16/verify-rebas?= =?UTF-8?q?e-probe-script=20=E2=80=94=20Run=20the=20deterministic=20proof?= =?UTF-8?q?=20for=20the=20three-outcome=20rebase=20probe.=20Review=20claim?= =?UTF-8?q?:=20The=20probe's=20three=20exit=20codes=20are=20asserted=20by?= =?UTF-8?q?=20tests.=20Review=20lane:=20proof=20Safety=20invariant:=20Proo?= =?UTF-8?q?f-only;=20adds=20no=20product=20behavior.=20Effectiveness=20mea?= =?UTF-8?q?surement:=20The=20suite=20fails=20if=20the=20unchecked=20case?= =?UTF-8?q?=20returns=201=20instead=20of=203.=20Slice=20rationale:=20One?= =?UTF-8?q?=20proof=20slice=20for=20this=20step.=20Architectural=20effect:?= =?UTF-8?q?=20None;=20verification=20only.=20Goal:=20Prove=20the=20three-o?= =?UTF-8?q?utcome=20rebase=20probe=20deterministically.=20Motivation:=20Th?= =?UTF-8?q?e=20defect=20is=20a=20wrong=20exit=20path,=20so=20the=20proof?= =?UTF-8?q?=20exercises=20every=20exit=20path.=20Alternative=20considerati?= =?UTF-8?q?ons:=20Manual=20verification=20was=20rejected=20as=20non-determ?= =?UTF-8?q?inistic.=20Implementation=20details:=20Execute=20the=20command?= =?UTF-8?q?=20below=20as=20the=20terminal=20proof.=20Non-goals:=20No=20pro?= =?UTF-8?q?duct=20edits=20here;=20proof=20only.=20Layer:=20app=5Fregressio?= =?UTF-8?q?n=20Feature=20state:=20active?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exit code: 0 Invoker-Finalize-Id: a4316ef4-e3eb-4b23-bf12-3d12f74bcbb7 From 1a78c7f25cff0ced665bff253b3ee5712b9def78 Mon Sep 17 00:00:00 2001 From: Invoker Bot Date: Sat, 12 Sep 2026 18:04:02 +0000 Subject: [PATCH 3/3] =?UTF-8?q?invoker:=20wf-1789235894808-16/scrub-handof?= =?UTF-8?q?f-artifacts=20=E2=80=94=20Terminal=20check=20that=20no=20epheme?= =?UTF-8?q?ral=20inter-task=20handoff=20files=20remain.=20Review=20claim:?= =?UTF-8?q?=20The=20workflow=20leaves=20no=20scratch=20handoff=20artifacts?= =?UTF-8?q?=20behind.=20Review=20lane:=20cleanup=20Safety=20invariant:=20R?= =?UTF-8?q?ead-only;=20never=20deletes=20files,=20alters=20the=20index,=20?= =?UTF-8?q?or=20commits=20caller=20work.=20Effectiveness=20measurement:=20?= =?UTF-8?q?The=20gate=20fails=20when=20a=20plans/invoker-handoff.*=20or=20?= =?UTF-8?q?lens-*.json=20file=20is=20still=20present=20after=20the=20leaf?= =?UTF-8?q?=20tasks=20complete.=20Slice=20rationale:=20One=20terminal=20hy?= =?UTF-8?q?giene=20gate=20for=20the=20workflow.=20Architectural=20effect:?= =?UTF-8?q?=20None;=20check=20only.=20Goal:=20Confirm=20no=20ephemeral=20h?= =?UTF-8?q?andoff=20files=20survive=20the=20run.=20Motivation:=20Inter-tas?= =?UTF-8?q?k=20scratch=20files=20leak=20into=20diffs=20and=20read=20as=20p?= =?UTF-8?q?art=20of=20the=20work.=20Alternative=20considerations:=20Deleti?= =?UTF-8?q?ng=20them=20automatically=20was=20rejected;=20the=20gate=20repo?= =?UTF-8?q?rts,=20it=20does=20not=20mutate.=20Implementation=20details:=20?= =?UTF-8?q?Run=20scripts/scrub-handoff-artifacts.sh=20without=20--apply.?= =?UTF-8?q?=20Non-goals:=20No=20deletion,=20no=20index=20changes,=20no=20c?= =?UTF-8?q?ommits.=20Layer:=20app=5Fregression=20Feature=20state:=20active?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exit code: 0 Invoker-Finalize-Id: a726a99a-fef6-4971-8bb1-446252907754