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:
@@ -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.
|
||||
Reference in New Issue
Block a user