feat(cli): read-only loop status + loop replay runtime verbs (Refs #55) - #70
Conversation
…er the EventStore New loop/runtime.py: status_report projects the event log once via the deterministic reducer and reconciles it with .loop/state.json (typed divergence findings); replay_report double-folds for determinism, reports illegal event sequences as typed findings (never a traceback), and checks the desynced-terminal window bidirectionally (file-without-event, event-without-file, value mismatch — the R-001 class). The store opens strictly read-only (file:...?mode=ro URI); loop/events.py and loop/reducer.py are byte-unchanged. --mode joins the existing _extract_mode_flag path; reducer domain checks run unconditionally. Exit codes mirror doctor: 0 healthy, 1 ran-and-found-a-problem, 2 cannot-attempt-a-report (missing/empty/corrupt store, ambiguous run_id via new RuntimeStoreError). This slice decides the previously-open §16 questions: store location .loop/events.db, one run per store (multi-run deferred). 27 new unconditional tests in scripts/test_loop_cli_status_replay.py. Refs #55 (phase 1 partial — status/replay; the run verb remains).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a7b3289bf7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| disk_terminal = json.loads(paths.terminal.read_text(encoding="utf-8")) if paths.terminal.exists() else None | ||
| except (OSError, json.JSONDecodeError): | ||
| disk_terminal = None |
There was a problem hiding this comment.
Treat malformed terminal files as present
When terminal_state.json exists but is invalid JSON (or unreadable) and the event log has no terminal event, this except collapses the file to None; the next branch then sees both sides as absent and replay returns ok: true with no findings. That misses the file-without-event half of the terminal desync window for exactly the corrupt terminal-file case, so the parse/read failure should produce a finding or preserve a separate “file present” marker instead of treating it as absent.
Useful? React with 👍 / 👎.
| if not isinstance(run_id, str): | ||
| raise RuntimeStoreError("corrupt_store", f"event store has invalid run_id: {path}") |
There was a problem hiding this comment.
Reject empty run IDs during discovery
If a store is tampered or raw-inserted with a single run_id = '', discovery accepts it because it only checks the type; the CLI then reports an illegal_event_sequence problem and exits like a replay failure rather than treating the store as un-attemptable/corrupt. Since event@1 requires a non-empty run_id, this guard should reject empty strings before returning the discovered run id.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
Adds new read-only runtime CLI verbs (status and replay) that project the .loop/events.db event log into a deterministic runtime projection, optionally validate under the existing --mode contract, and emit JSON reports with doctor-style exit codes.
Changes:
- Introduces
loop/runtime.pywithstatus_report()andreplay_report()over a strictly read-only SQLite connection. - Extends the CLI entrypoint to expose
loop status/loop replayand route--modeconsistently with doctor-family commands. - Adds contract-style tests for the new verbs and updates the repo-OS contract reference to pin
.loop/events.dbas the on-disk location.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| scripts/test_loop_cli_status_replay.py | New CLI + runtime report contract tests for status/replay, including read-only behavior assertions. |
| reference/repo-os-contract.md | Updates event-store location and clarifies single-run-per-store discovery for runtime readers. |
| loop/runtime.py | New read-only runtime reporting implementation over the SQLite event store (status + replay). |
| loop/main.py | Registers status/replay commands, documents them in --help, and wires --mode + exit codes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| from .completion import CompletionPolicyError, criteria_satisfy_completion | ||
| from .contract import ContractIssue | ||
| from .events import EVENT_SCHEMA_ID, EVENT_TYPES, validate_event | ||
| from .paths import resolve_loop_paths |
| files = sorted((workspace / ".loop").iterdir()) | ||
| before = {p.name: (p.stat().st_mtime_ns, hashlib.sha256(p.read_bytes()).hexdigest()) for p in files if p.is_file()} | ||
| status_report(workspace) | ||
| replay_report(workspace) | ||
| after = {p.name: (p.stat().st_mtime_ns, hashlib.sha256(p.read_bytes()).hexdigest()) for p in files if p.is_file()} | ||
| assert after == before |
…afe resume (#71) One bounded dispatch step per call over the SQLite EventStore: reducer-projected next-task selection (depends_on + event-log task_passed completions; TASKS.json stays read-only), a pluggable verifier seam defaulting to fail-loud VerifierNotImplementedError (exit 2, zero writes), EVENT-APPEND-FIRST crash safety (the CAS iteration_appended COMMIT is the single source-of-truth boundary; RUNLOG.md / state.json materialize idempotently from the recorded payload; resume is the same call replaying missing materializations), and Succeeded-only terminals via the existing emit.terminate / emit.sync_state_to_terminal. Adds an idempotency guard to emit.append_iteration (duplicate iteration_id skips the RUNLOG block, still writes state.json). 20 new unconditional tests incl. 3 real-SIGKILL crash-injection tests at the named commit barriers. Refs #55 (phase 1 partial: status/replay landed in #70; isolated verifier subprocess + typed stubs follow in the next slice).
Refs #55 (phase 1 partial — delivers
status/replay; the crash-safe single-workerrunverb follows separately; #55 stays open).phase3-run S2 — shipped via the governed Claudex lane (worker
gpt-5.6-terra/medium, codex session019f65a3-b696-76d3-b779-68298ccf4046, receiptcx_s2_verbs_a1, attempt 1 accepted).What
loop status <workspace>— folds.loop/events.dbonce through the deterministic reducer and reports the projected run state (terminal/in-flight, iteration cursor, active task, completion-policy satisfaction) plus whether.loop/state.jsonagrees, with typedContractIssue-shaped divergence findings.loop replay <workspace>— double-folds the log and asserts identical projections (determinism proof), surfaces illegal event sequences as typed findings (no tracebacks), and checks the desynced-terminal window bidirectionally (file-without-event, event-without-file, value mismatch — the R-001 split-write class, includingterminal_supersededcorrections the snapshot never absorbed).Mechanics: store opened strictly read-only (
file:…?mode=roURI) —loop/events.py/loop/reducer.pybyte-unchanged;--modejoins the existing_extract_mode_flagpath (reducer domain checks run unconditionally); exit codes mirror doctor (0 healthy / 1 problem found / 2 cannot-attempt via new typedRuntimeStoreError: missing, empty, corrupt store, ambiguous run_id). Decides the previously-open §16 spec questions:.loop/events.dblocation, one-run-per-store discovery (multi-run deferred); the stale "undecided" sentence inreference/repo-os-contract.mdis updated.Evidence
.loop/files; per-rowvalidate_eventresult discarded — both noted, non-blocking).terminal_supersededdesync flagged against the corrected terminal, raw-INSERT empty-run_id row surfaces as typedambiguous_run_id(kernelappendalready rejectsrun_id=""at write time — probe premise corrected against source).