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).
This commit is contained in:
Lap Tran
2026-06-26 10:05:18 -04:00
parent fe43b9e1fc
commit bc7daf8590
666 changed files with 15994 additions and 69 deletions
+116
View File
@@ -0,0 +1,116 @@
# SPEC: parametrize-base-branch
## Problem
`scripts/status.py::_gate_worktree_drift` hard-codes `main` as the integration branch:
```python
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`:
```json
"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.