7.9 KiB
SPEC: parametrize-base-branch
Problem
scripts/status.py::_gate_worktree_drift hard-codes main as the integration branch:
res = subprocess.run(
["git", "diff", "--name-only", "main...HEAD"],
cwd=worktree_path, capture_output=True, text=True, timeout=10, check=False,
)
Source: tasks/add-status-brakes/BUG_REPORT.md O3.
Hard-codes
mainas the integration branch. Projects onmaster/trunkwould show every file as out-of-scope (nomainto diff against → git errors → gate skips with warning).
On a project whose integration branch is master, trunk, develop, or release/x.y, git diff main...HEAD fails with fatal: bad revision main. The runner's drift gate prints WARNING: could not run git diff for drift check: ... and returns None (skip with warning, NOT halt). The drift gate is effectively disabled for every non-main project — a silent false-negative on the drift loop-death mode.
Goal
Replace the hardcoded "main" with a per-loop blast_radius.base_branch configuration field. The drift gate uses this branch for the git diff <base>...HEAD call.
Non-goals
- Multi-base-branch (e.g. "diff against ANY of these branches"). One base branch per loop.
- Auto-detecting the repo's default branch (
git symbolic-ref refs/remotes/origin/HEAD). Out of scope; operator setsbase_branchexplicitly inloop.json. - Validating the branch exists in the repo at
--create-looptime. Defer to runtime — the drift gate's "bad revision" path already skips with warning. - Backfilling
base_branchinto v1 loops via--upgrade-loops(separately tracked; v1.1 loops get the field via--create-looptemplate).
Schema addition (loop.json)
Add an optional base_branch field under blast_radius:
"blast_radius": {
"worktree": true,
"file_scope": ["src/", "tests/"],
"base_branch": "main"
}
blast_radius.base_branch(str, optional, default"main"): the integration branch to diff the worktree HEAD against in_gate_worktree_drift. Any string accepted as a git ref (branch name, tag, commit SHA).- Empty string coerces to
"main"with WARNING. Non-string types coerce viastr(...)with WARNING.None(key missing) → default"main"(silent).
Requirements
R1 — Drift gate reads base_branch
_gate_worktree_drift calls a new helper _base_branch(cfg) -> str to get the integration branch. Replaces the hardcoded "main" in the git diff argv.
R2 — Helper
_base_branch(cfg) returns:
"main"ifcfgis None orblast_radiusis missing orbase_branchis missing/None."main"(with stderr WARNING) ifbase_branchis an empty string.str(base_branch)if non-empty str.str(base_branch)(with stderr WARNING) if non-str type (int, bool, etc.).
R3 — Drift-gate bad-revision path stays warning-skip
If git diff <base>...HEAD fails (non-zero returncode OR exception), the gate logs WARNING: could not run git diff for drift check: {stderr} and returns None (no halt). Same behavior as v1 — operators running against a non-existent branch see a warning and a skipped gate, not a halt. Belt-and-suspenders: a wrong base_branch is admin error, not a drift event.
R4 — Template + create-loop plumbing
templates/loops/self-improvement/loop.jsonadds"base_branch": "main"to theblast_radiusblock. New loops created via--create-loopget the field by default.- Existing v1 loops WITHOUT
base_branchcontinue to work —_base_branchreturns"main". Backwards-compatible.
R5 — No new pip deps; no new files; stdlib only.
Test plan
Pure-function tests (no subprocess except where mocked git is needed). Tests in tests/test_base_branch.py (NEW):
- Helper default
main:_base_branch({})→"main"._base_branch({"blast_radius": {}})→"main"._base_branch({"blast_radius": {"base_branch": None}})→"main"(all silent). - Helper explicit value:
_base_branch({"blast_radius": {"base_branch": "trunk"}})→"trunk". - Helper empty string:
_base_branch({"blast_radius": {"base_branch": ""}})→"main"+ stderr WARNING captured. - Helper non-string:
_base_branch({"blast_radius": {"base_branch": 42}})→"42"+ WARNING. - Drift gate uses base_branch in argv (mocked subprocess): patch
subprocess.run, call_gate_worktree_drift(state={"worktree_path": "/tmp/wt"}, cfg={"blast_radius": {"file_scope": ["src/"], "base_branch": "trunk"}}, project=None), assert captured argv is["git", "diff", "--name-only", "trunk...HEAD"]. - Drift gate falls back to
mainwhen base_branch missing (mocked subprocess): assert argv uses"main"whenblast_radiuslacksbase_branch. - Drift gate handles bad revision (mocked subprocess returncode=128, stderr="fatal: bad revision 'trunk'"): assert gate returns
Noneand prints WARNING to stderr (captured viacapsys). - Drift gate still detects drift (mocked subprocess with names mtime.txt and out-of-scope
extraneous.txt): assert gate returns dict withhalt_reason="drift_detected"andout_of_scope_files=["extraneous.txt"]. Usesbase_branch="main". - Drift gate in-scope files don't halt (mocked subprocess returning only in-scope names): assert returns
None. - No worktree → None:
state={"worktree_path": None}→None. (Existing path; ensures R1 doesn't break.) - Empty file_scope → None:
cfg={"blast_radius": {"file_scope": [], "base_branch": "main"}}→None. (Existing path.) - Missing worktree_path → None:
state={"worktree_path": "/does/not/exist"}→None. - Template includes base_branch: load
templates/loops/self-improvement/loop.json, assertblast_radius.base_branch == "main".
Decisions
- D-B1: One base branch per loop (NOT a list). Schema simplicity; covers 95% of projects. Multi-base projects can use a SHA or tag if they need a moving target.
- D-B2: Default
"main"(most common on GitHub since 2020; matches v1 behavior). Operators override inloop.json. - D-B3: Bad-revision path stays WARNING-skip (NOT halt). v1 behavior preserved. A halt would punish operator misconfiguration; the existing drift-detection still triggers when the revision exists. Future: add
--validate-loopto catch misconfiguration at create/install time. Out of scope here. - D-B4: Empty string →
"main"with WARNING (not silent). Distinguishes "operator forgot the field" (None → silent default) from "operator set empty string" (probably a typo — flag it). - D-B5: No backfill on existing v1 loops. They get
"main"via the helper default; no--upgrade-loopsstep required. - D-B6: Drift gate test strategy = mock
subprocess.run. Pure-function; no live git; no worktree creation. Existingtests/test_status_brakes.pyuses the same pattern. - D-B7: Template edit is the public-facing default. New loops get
base_branch: mainwritten explicitly in theirloop.json(operator-visible).
Files touched
scripts/status.py— add_base_branch(cfg)helper; use in_gate_worktree_drift(line ~2191).templates/loops/self-improvement/loop.json— add"base_branch": "main"toblast_radius.design/loops/technical.md— noteblast_radius.base_branchin the schema enum; mention in §7 worktree-drift gate description.design/loops/functional.md— addbase_branchrow to blast_radius fields list.CHANGELOG.md— new entry under[unreleased].tests/test_base_branch.py(NEW) — 13 tests per plan above.
Out of scope
--validate-loopcommand (cross-references real branches in the repo). Filed toBACKLOG.md.- Auto-detect default branch via
git symbolic-ref. Filed toBACKLOG.md. - Multi-base-branch (list of integration branches). Filed to
BACKLOG.md.
Pipeline plan
research → research:awaiting_approval → research:approved → implement → code_review → code_review:awaiting_approval → code_review:approved → bug_find → adversarial_bug_find → doc_review → referee → complete.