Files
Lap Tran bc7daf8590 Restore archived tasks, fix dashboard scroll-reset, bind ornith, add Playwright smoke test
- **Restore 82 completed tasks** from tasks/complete/ back to tasks/ top
  level (all <7 days old per the cleanup policy; premature bulk archive
  was fixed).
- **Dashboard: fix scroll-reset on auto-refresh** — renderBoard rebuilds
  the board via innerHTML every 2s, destroying each column-body's
  scrollTop. Now snapshots column-body scrollTop + board.scrollLeft +
  view.scrollTop before rebuild and restores after (matched by
  PHASE_GROUPS index).
- **Dashboard UI additions** (pre-existing unstaged work): approval
  section cards, transition buttons, inline artifact editor (textarea for
  writing missing SPEC/VERDICT/etc from the detail modal).
- **Bind ornith as Implement model** — config.md: Model explicit to
  omlx/Ornith-1.0-35B-4bit-mlx, context window 32768. Interactive
  autopilot already used ornith via opencode default; now explicit.
- **Fix cleanup stub** — automaton-cleanup.sh had a stale --project arg
  pointing at a pytest temp dir (test isolation leak). Rewired to point
  at ~/.automaton.
- **Fix plist-isolation test** — test asserted host plist doesn't exist,
  but a real install creates it. Now snapshots mtime before run, asserts
  unchanged after (only a write during the test counts as bleed).
- **New Playwright smoke test** (tests/test_dashboard_ui.py) — 2 tests:
  board renders tasks, column scroll survives auto-refresh tick.
  Verified the test fails without the scroll fix (scrollTop resets to 0).
  Skipped via importorskip when playwright is absent (main CI stays
  green).
- **Clarify SI loop scope in README** — new-project onboarding section
  documents the framework-scoped self-improvement loop and options
  (leave/pause/create project loop).
- **CHANGELOG** documents all changes including the known model-divergence
  gap (mde tasks marked complete but per-role model binding was never
  implemented).
2026-06-26 10:05:18 -04:00

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 main as the integration branch. Projects on master/trunk would show every file as out-of-scope (no main to 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 sets base_branch explicitly in loop.json.
  • Validating the branch exists in the repo at --create-loop time. Defer to runtime — the drift gate's "bad revision" path already skips with warning.
  • Backfilling base_branch into v1 loops via --upgrade-loops (separately tracked; v1.1 loops get the field via --create-loop template).

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 via str(...) 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" if cfg is None or blast_radius is missing or base_branch is missing/None.
  • "main" (with stderr WARNING) if base_branch is 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.json adds "base_branch": "main" to the blast_radius block. New loops created via --create-loop get the field by default.
  • Existing v1 loops WITHOUT base_branch continue to work — _base_branch returns "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):

  1. Helper default main: _base_branch({}) → "main". _base_branch({"blast_radius": {}}) → "main". _base_branch({"blast_radius": {"base_branch": None}}) → "main" (all silent).
  2. Helper explicit value: _base_branch({"blast_radius": {"base_branch": "trunk"}}) → "trunk".
  3. Helper empty string: _base_branch({"blast_radius": {"base_branch": ""}}) → "main" + stderr WARNING captured.
  4. Helper non-string: _base_branch({"blast_radius": {"base_branch": 42}}) → "42" + WARNING.
  5. 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"].
  6. Drift gate falls back to main when base_branch missing (mocked subprocess): assert argv uses "main" when blast_radius lacks base_branch.
  7. Drift gate handles bad revision (mocked subprocess returncode=128, stderr="fatal: bad revision 'trunk'"): assert gate returns None and prints WARNING to stderr (captured via capsys).
  8. Drift gate still detects drift (mocked subprocess with names mtime.txt and out-of-scope extraneous.txt): assert gate returns dict with halt_reason="drift_detected" and out_of_scope_files=["extraneous.txt"]. Uses base_branch="main".
  9. Drift gate in-scope files don't halt (mocked subprocess returning only in-scope names): assert returns None.
  10. No worktree → None: state={"worktree_path": None} → None. (Existing path; ensures R1 doesn't break.)
  11. Empty file_scope → None: cfg={"blast_radius": {"file_scope": [], "base_branch": "main"}} → None. (Existing path.)
  12. Missing worktree_path → None: state={"worktree_path": "/does/not/exist"} → None.
  13. Template includes base_branch: load templates/loops/self-improvement/loop.json, assert blast_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 in loop.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-loop to 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-loops step required.
  • D-B6: Drift gate test strategy = mock subprocess.run. Pure-function; no live git; no worktree creation. Existing tests/test_status_brakes.py uses the same pattern.
  • D-B7: Template edit is the public-facing default. New loops get base_branch: main written explicitly in their loop.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" to blast_radius.
  • design/loops/technical.md — note blast_radius.base_branch in the schema enum; mention in §7 worktree-drift gate description.
  • design/loops/functional.md — add base_branch row 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-loop command (cross-references real branches in the repo). Filed to BACKLOG.md.
  • Auto-detect default branch via git symbolic-ref. Filed to BACKLOG.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.