reviewloop 0.2.1

Reproducible, guardrailed automation for academic review workflows on paperreview.ai
Documentation
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

---

## [0.2.1] — 2026-05-06

Non-blocking polish + defense-in-depth wave from eval3.

### Added

- **`Db::ensure_schema`** (was `init_schema`) — renamed for clarity (the
  function is idempotent and runs migrations, not just initialization).
  Split into private helpers (`enable_wal_mode`, `create_tables_if_missing`,
  `migrate_columns`, `create_indexes`) for targeted error context.
- **WAL pragma read-back**`ensure_schema` now warns when SQLite falls
  back to a non-WAL journal mode on file-backed DBs (silent failure was
  the prior behaviour).
- **Foreign config validation** (`validate_for_foreign_load`) — when the
  CLI loads a project config from the registry path (not cwd), it now
  rejects `fallback_script` paths outside `$HOME` and warns on absolute
  `widget_state_dir` outside `$HOME` and proxy URLs with embedded
  credentials. Audit event `foreign_config_loaded` recorded in the
  events table for every such load.

### Fixed

- **0o600 file permissions** on widget-state.json, config.toml, and the
  SQLite DB on Unix (was 0o644 from default umask). Failures are now
  logged via `tracing::warn!` rather than silently discarded.
- **Bar UX** — active-job labels reorder to `paper-id (status, in Xs)`
  with paper name leftmost; `attempt=N` dropped when N=0; project
  submenu headers now read `(N active, M failed)` instead of cryptic
  `(NA · MF)`.
- **Bar performance** — background poller holds one `Db` instance for
  its lifetime instead of reopening per 5s tick. `ensure_schema` runs
  once at bar startup, not 720 times per hour.
- **Error vocabulary** — standardized to `"X not found: {id}"` for
  lookup misses; reserved `"no longer exists"` for genuine post-lookup
  races. Eliminates 5 different phrasings of the same situation.
- **`daemon status` timestamps** human-readable path now suffixes with
  ` UTC` (was `Z` which users in non-UTC zones often misread). `--json`
  output unchanged (still RFC3339 with `Z`).
- **`cancel --reason`** default cleaned up — no-reason path now writes
  `"cancelled by user"` instead of redundant `"cancelled by user: cancelled by user"`.

### Documentation

- **`reviewloop init`** doc comment + success output now point to
  `reviewloop init project --project-id <id>` as the next step.
- **`reviewloop retry --include-failed`** help text expanded with
  ambiguity example and `--job-id` fallback hint.
- **`reviewloop daemon status`** on non-macOS now suggests a SQLite
  fallback query in its error message.
- **README "Exit codes" section** added.
- **`status --active`** now mentioned in the doc comment + README
  command reference.

## [0.2.0] — 2026-05-06

First minor release after the Phase 0–8 UX overhaul. Touches every layer of
the daemon, CLI, menu-bar app, and adds a macOS Widget extension.

### Highlights

- Per-paper config (`papers[]` with venue/backend/PDF) replaces the
  hardcoded single-paper `[paper]` table.
- Strict global ↔ project config layering with explicit override chain
  and `Redacted<T>` wrapper for sensitive fields.
- Menu-bar app rewritten as a fleet-wide multi-project dashboard.
- macOS WidgetKit extension reads `widget-state.json` written by the
  daemon every tick.
- `reviewloop cancel --job-id` and `reviewloop retry --job-id` now work
  from any directory via a per-project config registry.
- Schema migration is now data-preserving and self-versioned via
  `PRAGMA user_version`.
- 178 tests, 0 warnings, CI green on Ubuntu + macOS.

### Breaking Changes

- **`reviewloop import-token` exits 2 on immediate failure** (Phase 0,
  N5/U5). Previously always exited 0 on token write. Now polls job state
  immediately after attaching and exits 2 if the poll resolves to a
  failure status (`Failed`, `FailedNeedsManual`, `Timeout`).

  *Migration*: scripts treating exit 0 as "token attached" must also
  check `reviewloop status --paper-id <id>` or handle exit 2 explicitly.
  A future `--no-poll` flag will restore the old behaviour.

- **`reviewloop status --json` shape unified** (Phase 0, C5-followup).
  Both single-paper (`--paper-id X`) and multi-paper paths now return
  the same wrapper shape:

  ```json
  {
    "project_id": "<id>",
    "papers": [
      { "paper_id": "<id>", "rows": [...], "timeline": [...] }
    ]
  }
  ```

  *Migration*: tooling that consumed the old flat-array multi-paper
  output must unwrap `payload.papers` and iterate paper objects.

- **State-machine guard on `Db::update_job_state`** (Phase 0, A2).
  Validates transitions via `JobStatus::can_transition` before writing.
  Override paths (`retry --force`, `complete`, `cancel`) use
  `Db::update_job_state_unchecked`.

### Added

- **`reviewloop-bar` menu-bar app** (Phase 8). Multi-project fleet
  view: aggregate status, per-project submenus with active jobs and
  recent failures, click-to-retry / click-to-cancel / click-to-open
  artifacts and logs. Anti-aliased disc icon, MenuSignature throttling
  to avoid rebuilding while user is reading.
- **macOS Widget Extension** (preview, `apple/ReviewLoopWidget/`). Daemon
  writes `widget-state.json` snapshots every tick; SwiftUI WidgetKit
  extension renders glance UI in macOS desktop / Notification Center.
  Schema documented at `docs/widget-schema.md` with golden round-trip
  test (B5).
- **`reviewloop run <pdf>` quickstart** (Phase 6) — submit + watch +
  print artifact paths in one shot.
- **`reviewloop cancel`** (U10) — mark a job cancelled (terminal). Works
  from any cwd via `--job-id`.
- **`reviewloop retry --include-failed`** (U8) — extend retry candidate
  search to terminal failure statuses.
- **Project registry** (`projects` table) — per-project config paths
  recorded on every successful `load_runtime`. Enables `cmd_retry` to
  resolve the right per-project provider/polling/papers config when
  invoked from any cwd. Self-heals stale entries via
  `forget_project_registration` on `NotFound`.
- **`reviewloop daemon status`** with `--json` flag, tick health, last
  tick error, gmail OAuth status, proxy health.
- **`reviewloop paper add --venue`** (U3) — per-paper venue override.
- **OS notifications via `notify-rust`** (Phase 7) — terminal job state
  changes optionally surface as desktop notifications.
- **HTTP round-robin proxy pool with sequential failover** (commits
  82d5b75 + 3e79877). `core.proxies` accepts a list; failures emit DB
  events.
- **`core.widget_state_enabled`** (default `true`) and
  **`core.widget_state_dir`** (default `state_dir`) config fields.
- **`PRAGMA user_version` schema versioning** (B2). Migrations skip
  already-applied phases. Backfill UPDATEs guarded by column-existence
  checks via `PRAGMA table_info`.

### Fixed

- **Gmail OAuth tokens preemptively refreshed in daemon** (B6). Daemon
  checks `expires_at` ≤ 5 minutes from now before each Gmail API call;
  refreshes and persists. Refresh failure logs `warn!` with re-login
  hint and skips the iteration (no daemon crash). Long-running daemon
  no longer breaks IMAP/Gmail polling after the first hour.
- **`cmd_retry` cross-cwd dispatch correctness** (B1 + B3). Removed
  TOCTOU `exists()` precheck before `load_runtime_for_path`; self-heals
  registry on `io::ErrorKind::NotFound` via `forget_project_registration`
  in the error path. Logging-init reuse documented (no panic on second
  call). Two new regression tests:
  `load_runtime_for_path_does_not_panic_on_repeated_call` and
  `load_effective_config_for_job_self_heals_when_registered_path_missing`.
- **Schema migration data preservation** (B2). Backfill UPDATEs in
  `ensure_schema` are now gated on column existence; pre-existing data
  values survive the upgrade. Three new regression tests.
- **Bar app fleet-view rewrite** (commit 722c58b). Drops requirement
  for `REVIEWLOOP_PROJECT_ID`; reads all projects from shared DB.
  Per-project sub-grouping, legacy-bucket for empty `project_id`.
- **Bar disc icon** (commit c6eaf51). Replaces solid square with
  anti-aliased coloured disc.
- **Schema migration order** (commit 6859eae). Pre-Phase-0 DBs upgrade
  cleanly: tables → ensure_column_exists → indexes (was: all in one
  batch, indexes referenced columns the migration hadn't yet added).
- **Clippy `sort_by_key`** at `src/main.rs:2759` (commit 6859eae).
- **`notify-rust` Linux build** — dropped `default-features = false`
  which was stripping both dbus and zbus backends (commit 6859eae).
- **`reviewloop run` error wording** (B8). Now explains *why* a project
  config is required (state storage location) and suggests the fix.
- **`load_effective_config_for_job` error wording** (B7). Numbered
  steps, concrete `reviewloop status` command, plain-English explanation
  for both "never registered" and "path no longer exists" branches.
- **Bar "Submit new…" pre-validates** (B9). Rejects PDFs outside a
  configured project repo with a native `rfd` alert before spawning
  `reviewloop run`.

### UX

- **Error wording overhaul**: actionable steps, concrete commands, and
  plain-English context for the most-hit error sites in `cmd_retry`,
  `cmd_run`, and the bar's job-action paths.
- **Bar menu structure**: per-project submenu headers, active-job
  submenus with retry/cancel/open-artifacts/open-log actions, recent-
  failures list capped per project (5 each, fleet-wide via SQL window
  function).

### Documentation

- **`docs/widget-schema.md`** — Widget JSON schema v1 with field types,
  semantics, sample document, schema-bump procedure, cross-platform
  notes.
- **README "Deployment model" section** (B4) — documents the v0.2.0
  single-daemon-per-machine constraint.
- Per-command doc comments expanded (Phase 4, F4).

### Known limitations (v0.2.0)

- **Single daemon per machine.** The launchd label `ai.reviewloop.daemon`
  is hardcoded; installing the daemon from a second project repo
  overwrites the first plist. The bar shows fleet-wide job data from
  all projects, but the active daemon services jobs for only one
  project at a time. Multi-daemon (label-per-project) support is on the
  v0.3.0 roadmap.
- **Bar's "Pause / Resume daemon"** controls the single installed
  daemon. There is no per-project pause control.
- **Legacy data with `project_id = ''`** (pre-Phase-0): bar's "Retry"
  button cannot resolve a project config for these. Cancel works from
  any cwd. Manual SQL nuke via `DELETE FROM jobs WHERE project_id = ''`
  is the recommended cleanup; future `reviewloop db purge-legacy`
  command is on the v0.2.1 backlog.

### Internal

- 178 tests pass via `./scripts/quality-gates.sh`
  (`cargo fmt --check + clippy + cargo test`).
- 26 Rust source files in `src/`, 6 Swift in
  `apple/ReviewLoopWidget/`, ~13K LOC Rust + ~400 Swift.
- CI green on `ubuntu-latest` + `macos-latest`.