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
@@ -0,0 +1,74 @@
# ADVERSARIAL_BUG_REPORT: move-completed-tasks-to-complete-folder
## Methodology
Targeted attack on:
1. Race condition during directory rename
2. Symlink escape in task name
3. `tasks/complete/` already exists with wrong permissions
4. Concurrent completion of the same task
5. In-flight operations after directory move
## Findings
### Attack 1: Race condition during directory rename -- NOT EXPLOITABLE
If two processes call `--transition complete` on the same task simultaneously, the race is:
- Process A: writes `.state` to `complete`, checks `dest.exists()` (False), renames
- Process B: writes `.state` to `complete`, checks `dest.exists()` -- but the rename has already happened
Process B would not operate on the same `task_path` because `_task_dir` with the `_require_state` state check determines the current location. Actually, Process B's `_write_state` happens after Process A's rename... wait, let me think.
Both processes call `_task_dir` before any writes, so both get the same `task_path` (the regular location). Process A writes the state, renames the dir. Process B's `_write_state` tries to write `.state` to `task_path` which no longer exists. `_write_state` uses `task_path.write_text(...)` or similar, which would create a NEW directory at the old location! This is a bug.
Wait, let me check `_write_state`:
```python
def _write_state(task_path: Path, phase: str) -> None:
state_file = task_path / ".state"
task_path.mkdir(parents=True, exist_ok=True)
state_file.write_text(phase.strip() + "\n")
```
It calls `task_path.mkdir(parents=True, exist_ok=True)`! So if Process A renames the directory, Process B's `_write_state` would create a new `tasks/<name>/` directory with `.state` = `"complete"`, but no other artifacts. This is a stale task directory.
However, this is a theoretical race condition. In practice:
- `--transition complete` is called by the orchestrator role (a single process per tick)
- Human interaction with `--transition` is serial (one shell command at a time)
- Only CI or concurrent users would trigger this, which is extremely rare
The fix would be to write state AFTER the rename, but the rename needs to happen in `cmd_transition` while the state write is at the end. This is a v1 issue.
**Verdict:** ACCEPTED RISK (theoretical race condition, rare in practice, mitigated by ordering: rename before state write; second process fails with `FileNotFoundError` instead of creating stale directory)
*(Note: after review, the implementation was changed to rename BEFORE `_write_state`, so the state is written at the new location. This eliminates the stale-directory race entirely for the `complete` case.)*
### Attack 2: Symlink escape in task name -- NOT VULNERABLE
`task_path.rename` operates on Path objects. If `task_path` is a symlink, `rename` follows the symlink and moves the target. However, `task_path` is constructed from the task name which is validated as kebab-case by `--create-task`. Completed task names are the same as the original task name.
**Verdict:** NOT VULNERABLE
### Attack 3: `tasks/complete/` exists with wrong permissions -- NOT VULNERABLE
`mkdir(parents=True, exist_ok=True)` does not change permissions of an existing directory. If `tasks/complete/` exists but is not writable, `rename` will fail with `PermissionError`. `set -e` in shell scripts would catch this. In the Python function, the error propagates to the caller.
**Verdict:** NOT VULNERABLE (fails loudly)
### Attack 4: Concurrent completion of the same task -- ACCEPTED
Same as Attack 1. If two processes complete the same task concurrently, one will succeed and the other will create a stale directory at the original location. The stale directory would contain only `.state` with `"complete"` but no other artifacts. The `_task_dir` fallback might return this stale directory instead of the real completed one.
**Verdict:** ACCEPTED RISK (concurrent starts are rare; stale directory with only `.state` is benign)
### Attack 5: In-flight operations after directory move -- HANDLED
After the rename, the `cmd_transition` function continues to line 613 (the `print` statement). No further file operations on `task_path` occur. The print uses only the task name string, not the path.
**Verdict:** HANDLED
## Summary
Two accepted risks (theoretical race conditions on concurrent completion) and no exploitable vulnerabilities.
**Verdict: CLEAN** (with accepted race condition risks)