Phase 2.5 (CLI Consolidation) complete — 2.5.I/2.5.J close-out + Batch A–E → main#69
Conversation
2.5.H landed on main (PR #68, development→main). Add the workstream PR reference (#67 — the 2.5.H feature PR to development, matching how 2.5.A–G cite their feature PR; #68 is the dev→main release mechanism, like #66) to the 2.5.H status surfaces: phase-2.5-cli-consolidation.md (top status + the 2.5.H block header + Status line), current.md, and CLAUDE.md. Date-only; no other change. format:check + lint:fence-check clean. Refs: phase-2.5-cli-consolidation.md (2.5.H) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ternal fixes - Mark 2.5.G done (PR #66) across the phase-2.5 doc, current.md, and CLAUDE.md; milestone M2.5-2 (Home + entry + onboarding) reached. - Fix phase-2.5 internal inconsistencies: EA1-EA7 -> EA1-EA8, the broken #engine-amendments-appendix-ea1ea7 anchor -> -ea1ea8, and add EA8 -> ADR-0061 to the amendment->ADR mapping. - deferred-tasks: check off [chat].max_turns (RESOLVED in 2.5.G S11, PR #66); reword the CLI ToolHost entry to reflect the fs/process (2.5.A) + egress/os (2.5.E) arms are wired behind the approval floor -- only the desktop surface remains. Refs: docs/roadmap/phase-2.5-close-plan.md Step 1 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… reseat reconciliation Opus review of b4d9619 surfaced leftover inconsistencies from 2.5.G pulling ADR-0059 forward: - ADR index: 0059 was still "Proposed | 2026-06-28"; its body + this reconciliation treat it as a shipped 2.5.G deliverable. Set to Accepted | 2026-07-06. - phase-2.5 "Explicitly out of scope": drop "mid-session model reseat" from the Phase-2.6 bullet (it shipped in 2.5.G per ADR-0059, PR #66); keep session {{ctx.*}} interpolation (ADR-0060, still Proposed). - phase-2.6 2.6.C: add a status note (reseat shipped early in 2.5.G) and fix the now-false "When ADR-0059 flips to Accepted" conditional (it is Accepted; the ADR-0024 amendment note is already recorded). - deferred-tasks: tighten the desktop ToolHost phrasing ("no tool host wired yet" rather than "binds an empty host" — apps/desktop has no host at all yet). Refs: docs/roadmap/phase-2.5-close-plan.md Step 1 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…te + phase-2.6 reseat framing Sonnet review verified PR #66 merged 2026-07-07 (gh api merged_at 2026-07-07T10:10:40Z), not 2026-07-06 (the -06 is the ADR-0059/0066 Accepted date). The wrong date also self-contradicted existing "merged 2026-07-07" text for the same PR. - Correct every "PR #66, 2026-07-06" merge citation to 2026-07-07 across the phase-2.5 doc (top status, 2.5.G header + Status block, M2.5-2 row), current.md, CLAUDE.md, and the phase-2.6 2.6.C note. ADR-date "2026-07-06" strings (ADR-0059 Accepted, the ADR-0024 amendment) are left untouched — they are correct. - phase-2.6: add a single Status-block note so the higher-level Goal / Outcomes / In-scope / Milestones / Exit-criteria mentions of "mid-session model switching" are reconciled to "shipped early in 2.5.G" in one place (they framed a shipped feature as future work). - CLAUDE.md: align terminology to "post-2.5.G model-UX follow-up" (matching the phase-2.5 Status block + current.md). Refs: docs/roadmap/phase-2.5-close-plan.md Step 1 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rding + README Phase-2.5 prose 2.5.J Task 1. Per ADR-0050 the CLI history.db is UNENCRYPTED at rest (guarded by 0700/0600 owner-only permissions + the OS keychain; no credentials stored); only the desktop surface uses a SQLCipher-encrypted store. - uvp.md / vision.md: drop the false "encrypted local history" claim; state the real protection (owner-only file perms + keychain). - tutorials/cli/start-a-chat-session.md: "encrypted history.db" -> unencrypted + 0700/0600 per ADR-0050 (linked). - architecture/agent-sessions.md: the two surface-blind "encrypted ... SQLCipher" statements now carry the CLI-unencrypted vs desktop-SQLCipher split (same defect class the Task-1 scope missed; the phase-2.5 2.5.J task is about surface-blindness). - README.md: add a Phase-2.5 (CLI Consolidation) prose sentence to the Status para (per close-plan D5 — expand prose, do NOT mint global M3.5/M3.6 IDs). Left untouched (accurate): the CI-secret "encrypted CI secret" (GitHub Actions), and the desktop/VS Code docs whose surface genuinely uses SQLCipher. Refs: docs/roadmap/phase-2.5-close-plan.md Step 2 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…QLCipher sweep Opus review confirmed the commit was correct/complete for its named scope and surfaced same-class pre-existing leftovers. Completing the sweep so no CLI / cross-surface doc still over-claims CLI-side encryption: - architecture/local-first-and-security.md: the "Run history" security-table row now splits CLI-unencrypted (0600/0700 + keychain) vs desktop-SQLCipher. - tech-stack.md: the Database cell + the local-first-storage bullet now state the CLI store is unencrypted at rest (ADR-0050), keeping the desktop-SQLCipher fact. - decisions/0029: append-only dated amendment note (mirrors ADR-0024) clarifying rule (c)'s "encrypted in history.db" is surface-specific; body unchanged. - architecture/agent-sessions.md: tighten the keychain phrasing (keys live there; it does not "guard" history.db). - deferred-tasks.md: track the VS Code docs' "SQLCipher history.db = same store as CLI" contradiction as a Phase-4 forward-design follow-up (blocked on the cross-host physical-store decision; genuinely out of scope here). Refs: docs/roadmap/phase-2.5-close-plan.md Step 2 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…on + runs.db unencrypted
Sonnet review confirmed the commits accurate/complete and surfaced two refinements:
- uvp.md / vision.md: the added "owner-only file permissions" parenthetical is a
CLI-specific control, but the sentence is about sessions "resumable on any
surface" — attributing it across surfaces understates desktop's SQLCipher.
Make it surface-neutral ("kept on your machine; API keys ... in the OS keychain,
never at rest"), matching the plan's "drop 'encrypted', do not overclaim".
- reference/desktop/tauri-plugins.md: the tauri-plugin-sql row grouped runs.db with
history.db under SQLCipher, but runs.db is intentionally unencrypted + git-committed
(metadata only) on every surface. Scope encryption to history.db.
Refs: docs/roadmap/phase-2.5-close-plan.md Step 2
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ransaction (2.5.I S1) session-store.loadFull issued two independent SELECTs (session row, then messages). A concurrent writer -- a second `relavium` process, or a `run` sharing history.db -- committing an append + a session-total update BETWEEN them yields a torn read (a session whose totals do not match the returned transcript). Wrap the two reads in one deferred `db.transaction` so both come from a single WAL snapshot (mirrors the house transaction pattern; a read-only body COMMITs a no-op). Tests (packages/db/src/session-store.test.ts): a two-connection, temp-file snapshot suite -- WAL isolation needs a shared file, since two :memory: handles are separate DBs. One test reproduces loadFull's read pair inside the reader's transaction with a real committed write interleaved between the reads and asserts the snapshot is consistent (the write is invisible; a fresh read after sees it); a control shows the same interleave WITHOUT the transaction tears (stale session + post-write transcript). The write-side BEGIN IMMEDIATE + SQLITE_BUSY retry this pairs with lands in Step 4 (ADR-0064 §5 amendment). Refs: docs/roadmap/phase-2.5-close-plan.md Step 3 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…loadFull + comment/teardown
Opus review confirmed the fix correct on every concurrency dimension and surfaced
that the regression test guarded SQLite/drizzle snapshot behavior but NOT loadFull's
wrapper (removing the wrapper kept both tests green).
- Add a coupling test that spies the reader connection's `transaction` and asserts
loadFull opens exactly one (single-read methods open zero). Verified it FAILS when
the wrapper is reverted, so it now genuinely guards the fix.
- session-store.ts: correct the forward-reference comment ("(landed alongside)" was
false — the write-side BEGIN IMMEDIATE + retry lands in Step 4; drop the premature
§5 pin).
- Failure-isolate the temp-file teardown (try/finally) + vi.restoreAllMocks.
- deferred-tasks: track the analogous run-resume reconstruction torn-read exposure
(lower impact — event-sourced fold self-heals) surfaced in review.
Refs: docs/roadmap/phase-2.5-close-plan.md Step 3
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…the temp-file teardown Sonnet review mutation-verified the fix + coupling test (both correct) and found the teardown still asymmetric: if reader.close() threw, rmSync never ran and the temp dir leaked. Nest the finally so the dir is swept regardless of which close fails. Refs: docs/roadmap/phase-2.5-close-plan.md Step 3 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…etry on write paths (2.5.I S2)
`history.db` is a single shared file two concurrent `relavium` processes may write
(a `run` persisting events while a `chat` refreshes the model catalog — ADR-0064 §5).
Harden the write path:
- New packages/db/src/retry.ts (`withBusyRetry`): a bounded, FAIL-LOUD SQLITE_BUSY/
SQLITE_LOCKED retry with DETERMINISTIC linear backoff (no jitter, never Math.random,
per ADR-0040) via a synchronous Atomics.wait sleep. Rethrows the original error on an
exhausted budget or any non-lock fault — never silently drops a write (ADR-0050).
- Switch the multi-statement write transactions to BEGIN IMMEDIATE (`{ behavior:
'immediate' }`) so a DEFERRED read→write lock-upgrade race (SQLITE_BUSY_SNAPSHOT, which
busy_timeout does NOT cover) cannot occur: persistEvent (run history),
replaceProviderModels (the model-catalog bulk live-upsert), and the provider `upsert`
read-then-write (previously an unguarded read-then-write). Each is wrapped in withBusyRetry.
Single-statement writes rely on SQLite's built-in busy handler.
Docs:
- ADR-0064: the approved append-only amendment note (DB write-path concurrency), recording
the two repo-wide conventions (every write txn IMMEDIATE; every write routes through the
retry) as the concrete realization of §5's concurrent-process requirement. No new ADR.
- database-schema.md: a new "Concurrency & transaction behavior" section — the one canonical
home retry.ts and the store doc-comments cite.
Tests (203 pass): retry.test.ts unit cases (deterministic backoff, fail-loud, SQLITE_LOCKED,
maxAttempts) + a REAL SQLITE_BUSY contention suite (a held write lock released during the
injected backoff; and a never-released lock that fails loud). Plus an IMMEDIATE-coupling test
in each of the three store suites (spies db.transaction, asserts `{ behavior: 'immediate' }`) —
a revert to DEFERRED fails them.
Refs: docs/roadmap/phase-2.5-close-plan.md Step 4; ADR-0064 §5 amendment note
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…curacy + retry hardening Opus review confirmed the mechanism correct and surfaced a missed writer + doc overclaims: - HIGH: model-catalog `upsert` (the `relavium models pricing` read-then-write) was a FOURTH unguarded multi-statement writer that can race `models refresh` on the same (provider, model) — a lost update or a UNIQUE crash. Wrap it in the same withBusyRetry + BEGIN IMMEDIATE, and add its IMMEDIATE-coupling test. Update the database-schema.md + ADR-0064 enumerations to list all four writers (the docs had claimed "every multi-statement writer" while covering three). - MEDIUM: the database-schema.md loadFull bullet overclaimed. loadFull's read-txn guarantees SNAPSHOT consistency, not TURN atomicity — the CLI persister writes a turn's messages + totals as separate auto-committed statements, so a snapshot can observe messages ahead of their totals. Reword to what is actually guaranteed + track the non-atomic turn persister as a follow-up (deferred-tasks). - LOW: document the ~25s compounded worst-case block (5 attempts × 5s busy_timeout); floor maxAttempts at 1 (Math.max) so a stray 0/negative can't disable the first try; add a full-default-budget backoff-sequence test [25,50,75,100] pinning base×attempt. - retry.ts: caveat that BEGIN IMMEDIATE applies only to the outermost txn (a nested call demotes to SAVEPOINT); mirror it in database-schema.md. - NIT: failure-isolate the two contention-test teardowns (nested finally). Full db gate green; 207 tests pass. Refs: docs/roadmap/phase-2.5-close-plan.md Step 4 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rence GC writers + retry floor test Sonnet review mutation-tested the mechanism (all correct) and found the docs' "every multi-statement writer" claim overclaimed: media-reference-store's addReference / removeRunReferences / reclaimExpired are multi-statement read-then-write GC writers with no transaction wrapper — and media GC IS host-wired (2.S), so a chat GC sweep racing a run's media production is the exact ADR-0064 §5 scenario. reclaimExpired even documented an (now-false-under-concurrency) "single-connection ⇒ select-then-update is consistent" assumption. - Wrap all three in withBusyRetry + BEGIN IMMEDIATE (recordObject stays a single atomic upsert — no wrap). reclaimExpired now takes one clock read for both the cutoff and the soft-delete stamp (stable across retries). Add a coupling test asserting all three open an IMMEDIATE transaction. - Update the database-schema.md + ADR-0064 enumerations to list the media-reference GC writers, so "every multi-statement writer" is now accurate. - Add a retry test pinning the maxAttempts:0 floor (the 7b7d9ff hardening had no regression test). Full db gate green (207 tests); apps/cli media-gc consumers (19) still pass. Refs: docs/roadmap/phase-2.5-close-plan.md Step 4; ADR-0064 §5 amendment note Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two scenarios (close-plan D3) proving a `relavium chat` and a `relavium run` share one history.db safely (ADR-0064 §5): - Coexistence (in-process, two connections): the REAL runCommand persists run events on its own connection while a REAL SessionStore writes a transcript on a second connection, interleaved. Asserts both land intact and the loadFull snapshot is consistent. (One synchronous better-sqlite3 process can't overlap two txns, so this proves coexistence, not lock contention — see the two-process test for that.) - Contention (real two OS processes): two child processes race a burst of provider upserts (the BEGIN IMMEDIATE + withBusyRetry path) against the same file; asserts both exit 0 with empty stderr and all 2×N distinct providers landed (no escaped SQLITE_BUSY, no lost/corrupt row). This is the only form that reproduces real cross-process WAL write-lock contention. The child (concurrent-writer.mjs) imports the BUILT @relavium/db by an argv abs path (it can't use vitest source resolution); the test is visibly it.skipIf-skipped, never silently passed, if the dist is absent. Refs: docs/roadmap/phase-2.5-close-plan.md Step 5 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tion + honest framing Opus review empirically showed (≈100 controlled runs) the two-process test was a reliable cross-process COEXISTENCE smoke but a weak CONTENTION guard: distinct-name upserts + the always-on busy_timeout rarely contend, so it caught an IMMEDIATE-only regression ~2.5% and a retry-only regression 0%. The precise clause-guards already live deterministically in @relavium/db (provider-store.test.ts IMMEDIATE spy; retry.test.ts held-lock). - Give it real teeth deterministically: the parent now HOLDS the WAL write lock (BEGIN IMMEDIATE on a third connection) while the children spawn, so their first upsert genuinely busy-waits cross-process every run; released after a short hold. The pass condition never depends on the timing (best-effort force, not a flake). - Reframe the docstring + title honestly: a cross-process safety/coexistence smoke; point at provider-store.test.ts + retry.test.ts as the precise Step-4 guards. - Fix the stderr flake vector: assert exit 0 + no /SQLITE|Error/ line, not exact-empty stderr (a CI runner's benign Node warning would have broken it). - NIT: group node:crypto with the other node: builtins. 3× stable locally; typecheck + lint clean. Refs: docs/roadmap/phase-2.5-close-plan.md Step 5 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ces real contention Sonnet review MEASURED that the 150ms fixed hold was shorter than the child's ~183ms startup (Node bootstrap + the dynamic @relavium/db import), so contention never fired (0/23 trials); "forcing a busy-wait every run" was empirically false. - HIGH: replace the fixed timer with a deterministic READY handshake. The child prints READY immediately before its first write; the parent releases the held write lock only after BOTH children signal + a small margin — so a genuine cross-process busy-wait happens every run, independent of startup speed. Empirically verified: both children now block ~120-140ms on the held lock (was 0ms). A `close`-resolves-`ready` fallback means a dead child can never hang the wait. - MEDIUM: reconcile the docstring to the handshake mechanism (no more "every run" claim that the inline comment contradicted). - LOW: harden releaseLock cleanup — guard on `holder.sqlite.inTransaction` (never COMMIT with no open txn) and nest the finally (release → close → rmSync) so a failure in one step never skips the rest. 3× stable; typecheck + lint + format clean. Refs: docs/roadmap/phase-2.5-close-plan.md Step 5 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…I S4) The coverage the 2.K regression harness (offline `run` fixtures only) lacks: a transcript's full journey over a real file-backed history.db. - A fresh cassette-driven session (the Home graduating into chat via buildChatSession) persists a turn through the real createSessionPersister. - A chat-resume on a FRESH connection to the same file (loadFull → buildResumedChatSession) continues it, appending PAST the persisted max sequenceNumber. - exportSession serializes it to a .relavium.yaml. Asserts the roles land ['user','assistant','user','assistant'] gap-free [0,1,2,3] and both user turns survive into the exported scaffold. The interactive ink Home/chat TUI is out of scope (needs a render-test dep — SKIP-listed); this drives the exact session-host + persister + export seam the Home graduates into. Refs: docs/roadmap/phase-2.5-close-plan.md Step 6 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ng + total accumulation
Opus review confirmed the chain genuine (real cross-process resume, deterministic,
leak-free, both cassettes drive) and flagged assertions the test should make (all
values verified to flow correctly, just unasserted):
- MEDIUM: the resumed turn now uses a CAPTURING resolver that records its provider
request; assert the request carried the reconstructed PRIOR transcript ('first user
message' + 'reply from the fresh session') into the model's context — the defining
behavior of resume, previously untested at this file-backed seam.
- MEDIUM: assert the session totals DOUBLE across resume (adopt-then-accumulate) — a
resume that failed to seed the prior totals would reset the row to 1×.
- MEDIUM: assert both assistant replies survive into the exported YAML (not just the
user turns).
- LOW: assert resumed.sessionId === SESSION_ID; narrow `full` before mapping.
- NIT: disableMcp: true on the fresh build to state the offline intent explicitly.
Refs: docs/roadmap/phase-2.5-close-plan.md Step 6
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n shape + full usage guards Sonnet review traced the engine cost/reconstruction mechanics (all confirmed correct) and flagged assertions that could false-green: - MEDIUM: the context-threading proof checked only that the turn strings appeared SOMEWHERE in the request — a scrambled/duplicated/mis-tagged reconstruction would pass. Now assert the EXACT role+order+text array ([user, assistant, user] with the precise texts) reconstructSessionState threaded into the request. - MEDIUM: add `> 0` guards for freshOutput + freshCost (only freshInput had one), so the 2× accumulation assertion keeps teeth on all three axes if a fixture/price drifts. - LOW: assert exactly one provider call drove the resumed turn (toHaveLength(1)) before reading requests[0]. Refs: docs/roadmap/phase-2.5-close-plan.md Step 6 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Query-shape budgets for the hot read paths (the 2.5.B Home recent-sessions/runs
strips + `relavium list`), per close-plan D2 (shape, not flaky wall-clock):
EXPLAIN QUERY PLAN asserts each read is INDEX-served with NO filesort
(USE TEMP B-TREE) and NO full-table SCAN — the concrete guarantee the store docs
claim ("served off idx_… (no filesort)").
- listSessions → idx_agent_sessions_updated
- listRuns → idx_runs_created
- loadFull's message read → a single indexed range scan (no N+1, no full scan)
A dropped/renamed index, or an ORDER BY the index stops covering, flips the plan and
fails here. The other §2.5.I perf item — the 80×24 narrow-terminal degrade — already
exists and is asserted in render/tui/home-projection.test.ts (homeFitsTerminal /
tooSmallMessage at the boundary), so it needs no new coverage (close-plan D4).
Refs: docs/roadmap/phase-2.5-close-plan.md Step 7
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ages filesort gap + drift coupling
Opus review verified the replicas match the stores exactly and inspected real plans;
two MEDIUMs worth closing:
- MEDIUM: the loadMessages budget FALSE-PASSED a filesort regression — it omitted the
index-name pin + the USE TEMP B-TREE guard, so dropping idx_session_messages_seq
(which falls to idx_session_messages_session + a temp b-tree) still passed. Parameterize
the helper's table name and run loadMessages through it, pinning idx_session_messages_seq
and forbidding the temp b-tree — now all three reads share one enforced contract.
- MEDIUM: the test replicates the store queries (the stores don't expose their SQL);
strengthen the drift coupling from a bare "Mirrors" comment to explicit bidirectional
file:line back-references — added at the session-store listSessions/loadMessages and
run-history-store listRuns query sites, so a store ORDER BY edit trips the reviewer.
- LOW: drop `.limit` so the plan covers both the bounded Home strip AND the unbounded
chat-list/`relavium list` (identical plan); soften the "no N+1" wording to what a
single-statement EXPLAIN actually proves ("one indexed range scan, not a per-message read").
- NIT: document the covering-index + SQLite-wording caveats on the helper.
Refs: docs/roadmap/phase-2.5-close-plan.md Step 7
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rance + closed table union
Sonnet mutation-tested every budget (dropped each pinned index, confirmed all three now
fail correctly + that the pre-fix loadMessages false-passed the filesort). Two cheap
LOW hardenings:
- Loosen the index-served check to /USING (COVERING )?INDEX/ so a future .select()
narrowing (which flips SQLite to "USING COVERING INDEX") doesn't false-FAIL a
still-index-served plan.
- Type the helper's `table` as a closed union ('agent_sessions'|'runs'|
'session_messages') so a computed/metachar table name can never reach the RegExp.
Refs: docs/roadmap/phase-2.5-close-plan.md Step 7
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-mode smoke (2.5.I S6) The DB write-path hardening (BEGIN IMMEDIATE + the retry's Atomics.wait sleep + WAL locking), the two-process concurrency e2e, and the no-TTY raw-mode path are the parts most likely to diverge on Windows — but ci.yml ran ubuntu-only (release.yml already smokes the built binary cross-OS, but never the test suite). - Add an advisory `windows-concurrency` job to ci.yml: install + build, then the @relavium/db concurrency suite + the CLI concurrency/perf/session-chain harness on windows-latest, plus a headless raw-mode smoke (--help loads the native addons; a non-TTY `chat` must exit cleanly, never a setRawMode crash). The required check stays the ubuntu `ci` job; POSIX 0600/0700 assertions are not exercised (Windows no-op, ADR-0050). - Fix a real cross-OS bug the Windows job would expose: the two-process e2e's child did `await import(<filesystem path>)`, which fails on Windows (a bare `C:\…` path is not a valid import specifier). Pass the built @relavium/db as a `file://` URL (cross-OS); keep the path form only for the existence gate. Still green on macOS. Refs: docs/roadmap/phase-2.5-close-plan.md Step 8 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… guard + honest smoke framing
Opus review verified the file:// fix (empirically on Node 22), the YAML/pins, and the
vitest filter, and flagged:
- MEDIUM: the "raw-mode smoke" mislabeled what it does — `chat` selects the PLAIN driver
when stdout is a pipe (the driver-selection gate is TTY-gated), so ink/setRawMode is
never entered on ANY OS. Reword the job/step comments to what actually runs: the bundle
loads, and a piped `chat` picks the plain driver + exits cleanly on EOF (the smoke
guards that driver-selection gate, not Windows raw mode).
- MEDIUM: `rmSync(dir, { recursive, force })` has no EPERM/EBUSY retry — a real Windows
flake when deleting a dir that just held SQLite WAL sidecars. Add maxRetries:5 +
retryDelay:100 to all 13 SQLite-teardown rmSync sites the Windows job runs (the
concurrency + session-chain harness + the @relavium/db suite).
- LOW: add the Turborepo cache restore to the Windows job (parity with ci/coverage;
the cold build was tight against the 25-min budget). Soften the --help native-addon
claim (the real Windows addon exercise is the DB vitest step).
Full db suite (207) + CLI harness (6) green; lint + format clean.
Refs: docs/roadmap/phase-2.5-close-plan.md Step 8
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Y gate in the smoke comment Sonnet review verified everything empirically (207 db + 6 harness tests, YAML parse, tag→SHA, live rmSync repro) and found the step-level comment still misattributed the driver-selection gate to stdin. The gate is process.stdout.isTTY (selectChatDriver / io.stdoutIsTty); stdin's TTY-ness plays no role. Reword to name the actual gate (the smoke still exercises it — stdout is captured non-TTY inside the $(...)). Refs: docs/roadmap/phase-2.5-close-plan.md Step 8 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ith the 2.5.I coverage (2.5.I S7) The canonical harness reference documented only the 2.K run-fixture stream contract. Reconcile it with the Phase-2.5.I extensions: - Bump the Last-updated + Scope; add ADR-0064/0050 + database-schema.md to Related. - Add the new harness suites to "Where it lives": the concurrency e2e (+ its child fixture), the session-chain e2e, and the query-shape perf budgets. - Add a "§2.5.I extensions" section documenting each suite (what it proves, what it doesn't — e.g. the concurrency e2e is a cross-process safety smoke, the precise BEGIN IMMEDIATE/retry guards live in @relavium/db white-box tests; the 80×24 degrade is in home-projection.test.ts) + the advisory windows-concurrency CI lane. Refs: docs/roadmap/phase-2.5-close-plan.md Step 9 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ocess claim + full-suite wording Opus review verified the reconcile is highly accurate (every suite claim, store back-reference, Windows lane, and link checks out); two wording precisions: - LOW: "The harness is an in-process suite … does not spawn the built binary" now sits above a table listing the concurrency e2e, whose cross-process case DOES spawn child node processes. Scope the subject to "The `run`-fixture harness" and note the §2.5.I concurrency suite is the deliberate exception. - NIT: the Windows lane runs the FULL @relavium/db test suite, not a concurrency-only split — reword. Refs: docs/roadmap/phase-2.5-close-plan.md Step 9 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…olor flag (2.5.J S3)
Color was resolvable only via --no-color (raw.color !== false); no env input. Add the
standard env conventions + the symmetric --color flag (close-plan D6/D7).
- resolveColor(raw, env) with the precedence: --color/--no-color flag > NO_COLOR
(any non-empty ⇒ off, the no-color.org accessibility contract, wins over FORCE_COLOR)
> FORCE_COLOR (not 0/false/empty ⇒ on) > default-on. FORCE_COLOR is force-ON only;
disabling is NO_COLOR / --no-color.
- Add --color to BOOLEAN_FLAGS (symmetric with --no-color; both present ⇒ last wins).
- resolveGlobalOptions(raw, defaultCwd, env = {}); run.ts passes io.env.
- Color stays orthogonal to the output MODE (--json/CI/non-TTY suppress ANSI separately
in detectOutputMode — unchanged).
- Document in program.ts --help + commands.md (the precedence + the mode-orthogonality).
- Tests: the full precedence matrix in options.test.ts (NO_COLOR any-value-off incl.
'0'; empty falls through; FORCE_COLOR truthy-on / 0-false-empty fall-through;
NO_COLOR beats FORCE_COLOR; flag beats both directions) + --color extraction.
Refs: docs/roadmap/phase-2.5-close-plan.md Step 10; ADR-0047
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…l teeth + fix doc-link depth Opus review proved the FORCE_COLOR branch was inert: `color` defaults to `true` and the force-ON-only branch could only ALSO return `true`, so it never changed the outcome — yet it was documented + tested as a live feature (dead code shipped as a feature). - Adopt the supports-color convention so FORCE_COLOR has an observable effect: FORCE_COLOR=0/false ⇒ OFF (distinct from the default-on); any other value stays on. NO_COLOR still wins (checked first); an explicit --color/--no-color flag beats both. Update program.ts --help + commands.md to match, and rewrite the tests so FORCE_COLOR=0/false now assert OFF (the teeth) + a flag-beats-FORCE_COLOR=0 case. - LOW: fix the resolveColor JSDoc ADR-0047 link depth (../../../ → ../../../../, matching output-mode.ts / render/select.ts in the same tree). 20 options tests pass; typecheck + lint + format + fence clean. Refs: docs/roadmap/phase-2.5-close-plan.md Step 10; ADR-0047 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…E_COLOR framing Sonnet mutation-tested the fix (removing the FORCE_COLOR=0 branch fails a test; observ- ability confirmed end-to-end) and found a residual honesty gap: after FORCE_COLOR became opt-OUT-only, the "NO_COLOR beats FORCE_COLOR (opt-out wins over opt-in)" test + the JSDoc "intentionally beats" framing describe an opt-in FORCE_COLOR no longer has — the check ORDER is now inconsequential (a truthy FORCE_COLOR never force-ONs). - Reword the JSDoc: both env vars are opt-OUT signals; a truthy FORCE_COLOR has no independent effect, so there is no NO_COLOR-vs-FORCE_COLOR conflict to resolve. - Reword the test to what it actually verifies (NO_COLOR turns off even alongside FORCE_COLOR=1 — a truthy FORCE_COLOR never force-ONs over NO_COLOR). - (Also reconciled the untracked close-plan's D6 row to the shipped semantics.) Refs: docs/roadmap/phase-2.5-close-plan.md Step 10 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
….5.A git_status union pin - media-gc.test.ts: the "sweeps a row-less CAS orphan" case was non-deterministic — with orphanMinAgeMs:0 and no injected clock, `settledBefore` (Date.now() − 0) can land BELOW the just-put orphan's filesystem mtimeMs (coarse/rounded-up mtime), so `mtimeMs <= settledBefore` is false and the orphan is skipped as a concurrent writer. Inject a fixed +60s clock so the orphan is unambiguously settled. 5× stable. - session-host.test.ts: add the 2.5.A UNION pin — a chat session dispatching git_status end-to-end through the process arm (the arm + the session→host dispatch were each covered separately, never together). A real temp git repo + a git_status tool-call cassette; asserts the agent:tool_call annotation + the post-tool answer streams (the process arm ran it and the result folded back). The other Batch-A pins stay covered/tracked: the persister failed-turn REAL-token/cost fold is already pinned (persister.test.ts — "flushes the running cost on a failed turn" + 4 sibling cases), and the 2.I read-command pins remain in deferred-tasks (transitively covered by the process arm + read tests). Refs: docs/roadmap/phase-2.5-close-plan.md Step 11 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…it_status union pin
Two review findings on the 2.5.A git_status union pin (session-host.test.ts):
- MEDIUM: the pin under-asserted. It only checked the agent:tool_call
annotation + the cassette-driven 'clean' token — neither proves git_status
ran to a clean result. The reviewer's proposed `success === true` check is
itself inert for cleanliness: the registry stamps success:true on ANY
resolved dispatch, and git_status's spawn RESOLVES a ProcessResult for both
exit 0 and exit 128 ("not a repository"), so success cannot distinguish them.
The genuine lock is the tool-result outputSummary, which carries git's
captured `{"exitCode":0,…}` stdout — added that assertion (keeping success as
the weaker "git actually started" check) with an honest comment on what each
proves.
- NIT: moved `git init` inside the try so the mkdtemp'd dir can't leak if init
throws (git absent / hostile template hook) — the finally rmSync now covers it.
Verified: 52/52 pass, stable across 3 runs; typecheck/eslint/prettier green.
Refs: docs/roadmap/phase-2.5-close-plan.md (Step 11, Batch A)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… abort hint + line-bounded reasoning panel)
Two chat-TUI live-region polishes, both landed in the pure, unit-tested
projection formatters (the React view only arranges them):
- Persistent mid-stream abort hint (EA7). The abort affordance showed on the
pre-first-token status line ("Working… {elapsed} · Esc to stop") but vanished
once answer content streamed — even though Esc aborts the WHOLE turn. New pure
`streamingAbortHint(busy)` returns a compact "Esc to stop" only for a streaming
CONTENT line; a STATUS line (compaction/shell/pre-token) already carries its
inline hint, so it returns undefined (no double-print). Rendered as a dim line
beneath the streaming content in ChatView.
- Bound the EXPANDED reasoning panel by rendered ROWS. The panel body is capped
at MAX_LIVE_TOKEN_CHARS (4000) CHARS, which can wrap to 60+ rows on a short/
narrow terminal and flicker in the live region. `formatReasoningPanel` now
tails the expanded body to the last MAX_REASONING_PANEL_LINES (12) RENDERED
rows — each logical line counts as ceil(len/columns) wrapped rows (min 1), so
both many short lines AND one very long line are bounded; a single over-budget
line is head-sliced to its tail. The `…` head-elision marker now ORs the store
char-cap flag with this row tail. A new optional `columns` prop threads the
live width (ChatApp reads process.stdout.columns; the Home passes its
resize-tracked size.cols); absent ⇒ an 80-col fallback so the bound still
applies headless/in tests.
Kept OUT (per the batch scope): Ctrl+T-during-approval — it touches the approval
floor and lands with the Step 14 security batch.
Tests: +7 projection cases (streamingAbortHint status-vs-content; reasoning
row-tail for short lines / wrapped rows / single-over-budget head-slice / marker
OR / 80-col fallback). 54 projection + 656 render+chat tests pass; typecheck,
eslint, prettier, build, fence, engine-deps all green.
Refs: docs/roadmap/phase-2.5-close-plan.md (Step 12, Batch B); 2.5.H
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… div-by-zero floor Opus review of the 2.5.H reasoning-panel row bound — no blockers; applied the correctness/honesty items: - MEDIUM: `tailToRenderedRows` counts `.length` (UTF-16 units), not ink's display width, so a wide-glyph (CJK/emoji) reasoning stream under-counts rows (renders up to ~2× the budget) and the prepended `…` marker can add one more row. The code claimed a hard "last N rendered rows" bound. Documented it honestly as an APPROXIMATE, cosmetic anti-flicker guard (matching the store's existing `.length`-based char cap; no `string-width` runtime dependency wanted) and softened the `formatReasoningPanel` JSDoc accordingly. - NIT (latent div-by-zero): tightened the width guard from `columns > 0` to `Math.floor(columns) >= 1` so a fractional 0<columns<1 can't floor to 0 and make `rowsOf` divide by zero. Unreachable today (callers pass integer ≥1) — a provable floor. - NIT: clarified the ChatApp render-site comment — `process.stdout.columns` is typed `number` but is `undefined` off a TTY (the 80-col fallback covers it; moot since ChatApp only mounts on a TTY). - NIT (test gap): added the exact-fit boundary case — rows summing to EXACTLY the budget keep everything with NO marker (`tailed:false`, loop ends naturally). Tests: 55 projection cases pass; typecheck/eslint/prettier green. Refs: docs/roadmap/phase-2.5-close-plan.md (Step 12, Batch B); 2.5.H Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…w miscount
Sonnet review of the 2.5.H reasoning-panel row bound (caught a case the Opus
round missed):
- MEDIUM: a live reasoning buffer cut off right after a line break (very common
between stream chunks) ends in '\n', which `split('\n')` turns into a phantom
empty last segment. `rowsOf('')` clamps to 1, so that phantom spent a full
budget row and could prematurely DROP a real line + flash the `…` marker for
content that hasn't arrived yet (self-correcting on the next token, but a real
untested gap). Fix: strip exactly ONE trailing '\n' before splitting — the
not-yet-filled next line costs zero rows, while an intentional trailing blank
line ("\n\n") keeps its blank (only the final cursor newline is dropped).
- LOW: the `columns` prop JSDoc said "Absent or ≤0 ⇒ fallback" but the guard is
`Math.floor(columns) >= 1` (any < 1 falls back). Corrected the doc to "< 1".
- LOW: the div-by-zero guard the prior fix commit added was untested. Added a
case pinning columns ∈ {0, 0.5, -5, NaN} → 80-col fallback (no crash).
Tests: +4 cases (trailing-newline no-drop; intentional trailing blank kept;
fractional/non-positive width fallback). 58 projection cases pass;
typecheck/eslint/prettier green.
Refs: docs/roadmap/phase-2.5-close-plan.md (Step 12, Batch B); 2.5.H
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ctx.*}} deferred)
Attach actionable line/column diagnostics to a `.agent.yaml` YAML syntax fault,
the agent sibling of parser.ts's `syntaxErrorFrom`:
- AgentParseError gains optional 1-based `line`/`column` fields (parity with
WorkflowSyntaxError), populated for an `agent_syntax` YAMLParseError when the
loader reports a position (skipped for a bare stream error at offset -1). A new
`agentSyntaxErrorFrom` normalizes the decode throw: a positioned YAMLParseError
→ the YAML RULE message + folded `(source — line L, column C)` locator + the
structured fields; an anchor/alias ReferenceError (maxAliasCount:0) → a clear
source-free label with NO bogus position; else the generic fallback.
- The position is folded into the message (via a shared `locate()` suffix
mirroring the config loader's `(line L, column C)` shape) so every existing
`.message`-surfacing consumer (catalog `parseReason`, authoring, agent run)
shows it end-to-end — the structured fields are additionally exposed for
machine/--json readers.
- Echoing the YAML rule is secret-free by construction: `prettyErrors: false`
(yaml-decode.ts) makes the message the rule ALONE — no source snippet, no
authored key/value (verified: a duplicate-key fault does not echo the value).
A regression test pins that an authored (secret-shaped) value never leaks.
The Step 13 `{{ctx.*}}` session-interpolation half is DEFERRED to Phase 2.6 /
2.6.D per the maintainer's 2.5-close decision (2026-07-08): it is governed by
ADR-0060 (Proposed) which requires a new SessionContext taint-provenance marker +
a mandatory pre-Accept security review — not a plain resolveTemplate reuse, and
not appropriate to pull forward into the close-out. Recorded in deferred-tasks.md.
Tests: +2 agent-parser cases (line/col attach + secret-free syntax message) and
the alias case; existing syntax test extended. 982 core tests pass; catalog/list
consumer suites green; typecheck/eslint/prettier/build green.
Refs: docs/roadmap/phase-2.5-close-plan.md (Step 13, Batch C); ADR-0060 (deferral)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cause Opus review of the AgentParseError line/col change — no blockers; the security claim (echoed YAML rule is secret-free) held under a 14-case empirical sweep. Applied the one code finding (LOW): the anchor/alias branch of `agentSyntaxErrorFrom` called the 2-arg AgentParseError ctor and swallowed the underlying ReferenceError — diverging from parser.ts's sibling alias branch (which attaches `cause`) and from error-handling.md's no-swallow rule. Attach the cause (a fixed, non-secret "Alias resolution is disabled") to match. The two NITs (the `—` em-dash glyph; a commit-message wording nuance on --json exposure) are acceptable as-is per the review. Tests: 8 agent-parser cases pass; typecheck/eslint/prettier green. Refs: docs/roadmap/phase-2.5-close-plan.md (Step 13, Batch C) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…or + surfacing gap Sonnet review of the AgentParseError line/col change — no blockers; the security sweep held (12 more adversarial decode cases + a full enumeration of the yaml throw surface: no authored key/value ever rides the message). Applied: - MEDIUM-1 (regression test for the Opus-round cause fix): added `err.cause` assertions — the alias branch preserves the ReferenceError, the syntax branch the YAMLParseError. Reverting the cause fix now fails a test (testing.md). - LOW-1: pinned the pos-only `locate()` branch — a syntax fault with NO source ends in a clean `(line L, column C)`, no stray `—` separator, no `undefined`. - LOW-2: added a catalog end-to-end test — a broken `.agent.yaml` with a real YAML SYNTAX fault surfaces the line/column through `discoverCatalog`/parseReason (proving Step 13's diagnostic reaches the catalog surface, secret-free). - MEDIUM-2 (pre-existing, DEFERRED — recorded in deferred-tasks.md): a malformed `.agent.yaml` on the `chat --agent` / `agent run` surfaces reaches the top-level handler as exit 1 + a generic "internal error" (the diagnostic only actually surfaces via list/create/import). `resolveChatAgent` DELIBERATELY surfaces the raw AgentParseError — pinned by agent-source.test.ts "not a silent default or CliError" — so re-tagging is a cross-surface DESIGN decision conflicting with an existing test, not a mechanical fix; it predates this diff and is out of the fix(core) close-out scope. Documented with candidate fixes (wrap at the caller vs. teach the top-level renderError) + the absolute-vs-relative `source` note. Clarified the resolveChatAgent docstring to record the deliberate raw-surface. Also marked the original "AgentParseError line/col" deferred entry DONE. Tests: 9 agent-parser + 22 catalog/agent-source pass; core+cli typecheck, eslint, prettier green. Refs: docs/roadmap/phase-2.5-close-plan.md (Step 13, Batch C); deferred-tasks.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(Trojan-Source floor) Extend the shared render sanitizer to strip Unicode bidirectional/directional format controls at every display boundary (CVE-2021-42574 floor): - `stripTerminalControls` now also strips the standard Trojan-Source family — the embeddings/overrides U+202A–202E, the isolates U+2066–2069, and the marks LRM (U+200E) / RLM (U+200F) / ALM (U+061C). These live above the C0/C1 range the existing BARE_CONTROLS strips, so they previously survived — yet they reorder how a terminal renders a line, letting streamed/pasted text visually differ from its logical bytes (a spoofed path/command in an approval prompt). Every surface (chat, Home, run TUI, commands) imports this one primitive, so the floor is uniform. The regex is written with `\u` escapes only — never literal bidi bytes — so the source itself carries no Trojan-Source hazard. - ZWJ/ZWNJ (U+200D/U+200C) are deliberately PRESERVED — they are legitimate in emoji sequences and Indic/Arabic/Persian shaping; an over-broad strip would mangle names/emoji. (Pinned by a test.) - Reconciled the pre-existing provider.ts `BIDI_ZERO_WIDTH`: it is now a stricter SUPERSET (it additionally strips the zero-width/BOM family the general floor keeps, which a structured URL must reject) rather than a workaround for a floor that "MISSES bidi" — corrected its now-stale comment (the earlier "bidi is legitimate for RTL text" rationale is superseded: legitimate RTL renders from inherent character directionality, not these override controls). Tests: +4 projection cases (strip the full family; flatten an RLO spoof; preserve ZWJ/ZWNJ; strip through sanitizeInline). 62 projection + 484 render tests pass; 36 provider tests pass; typecheck/eslint/prettier green. Refs: docs/roadmap/phase-2.5-close-plan.md (Step 14, Batch D); ADR-0057 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…asoning toggle in the approval swallow
During a pending per-tool approval the fail-closed key-swallow (ADR-0057) owns
the keyboard and drops every key except [y]/[a]/[n]/[esc], so a user could not
expand the model's reasoning to inform the decision. Whitelist the ONE view-only
chord — Ctrl+T (toggle-reasoning) — through `reduceApprovalKey`:
- Security-verified side-effect-free before bypassing the swallow: `toggleReasoning`
(chat-store.ts) only flips a local `reasoningVisible` boolean and repaints — zero
effect on `approval`, session state, or the pending decision. It cannot answer,
cancel, or mutate anything a governed dispatch gates on.
- The whitelist is TIGHT: matched only for the exact Ctrl-without-meta 't' chord,
placed AFTER the `escape` (abort wins) and BEFORE the `bare` gate. A bare 't',
Ctrl+Meta+T, or Alt+T (ESC-prefixed) all stay swallowed — the approval input set
is exactly {y/1, a/2, n/r/3, esc} plus this view-only toggle. The most-permissive
approve/reject can still never be picked by a modified chord.
- Both surfaces (chat ChatApp + the in-Home chat) route approval keys through the
shared `reduceChatKey`/`toggle-reasoning` case, so one change fixes both.
Tests: +1 chat-input case (Ctrl+T → toggle-reasoning during approval; bare/meta
variants stay swallowed). 132 chat-input + home-controller tests pass;
typecheck/eslint/prettier green.
Refs: docs/roadmap/phase-2.5-close-plan.md (Step 14, Batch D); ADR-0057
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…on at the approval prompt Add a `[c]` "reject with reason" branch to the per-tool approval prompt (ADR-0057 deferred follow-up): instead of a bare "denied by the user", the user types WHY, and the reason rides the EXISTING `ToolApprovalDecision.reject.reason` seam into the denial the turn records/shows. The engine + type plumbing already existed (`ApprovalAnswer.reject.reason` → `toDecision` → `ToolApprovalDecision.reject.reason` → registry → `ToolDeniedByUserError`); this is the missing UI capture. - `[c]` at the prompt returns a new `reject-with-reason` action (chat-input.ts) — a pure UI transition that OPENS a small keyboard-owning reason-input sub-mode; it does NOT itself answer. The bare `[n]`/`[r]`/`3` reject (no reason) is unchanged, and a modified `c` (Ctrl+C / Alt+C) stays swallowed (fail-closed). - The capture sub-mode is wired into BOTH surfaces (chat `ChatApp` + the in-Home chat) — checked FIRST in the key handler: Esc CANCELS back to the [y]/[a]/[n] prompt (the approval stays pending — NOT an abort), plain Enter submits the reject with the reason, every other key edits the buffer (reusing the tested editor primitives). A stale capture (the approval settled out-of-band) is dropped. The reasonDraft is chat-scoped and reset on every session swap. - The reason is sanitized + bounded before it leaves the UI (`sanitizeApprovalReason`: strip terminal/bidi controls, collapse to one line, trim, cap at 300 chars) so a pasted wall of text or a control/bidi spoof can't blow up or spoof the error line, logs, or `--json`. An empty reason degrades to a plain reject. - SECURITY: the fail-closed floor is UNCHANGED — `[c]`+Enter is a reject with extra data; a governed dispatch still cannot proceed without an explicit decision, and ADR-0057's "a user deny is final" (`tool_denied`, non-retryable) is untouched. Tests: +1 chat-input reducer case, +4 sanitizeApprovalReason cases, +2 home-controller INTEGRATION cases (open on a real pending approval → type → reject-with-reason; and Esc-cancels-keeps-pending → later [n] plain-rejects). 672 render+chat tests pass; typecheck/eslint/prettier/build/fence/engine-deps green. Refs: docs/roadmap/phase-2.5-close-plan.md (Step 14, Batch D); ADR-0057 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Y fail-closed approval policy The no-TTY approval policy (deny a governed dispatch when there is no user to approve — never a hang, never an auto-approve) already shipped in 2.5.E (the "High 9" fix) and is covered end-to-end (agent-run.test.ts "DENIES a governed dispatch in the non-interactive one-shot"; chat.test.ts chatIsInteractive → reject-immediately). But it was hand-rolled as an inline reject lambda in TWO places (chat.ts + agent-run.ts) with slightly divergent reason strings — a drift risk on a security-critical floor. Step 14 "define + enforce one policy": extract the canonical `nonInteractiveApprovalPrompt(context)` into chat-mode.ts (the home of `ApprovalPrompt`) — every request rejects regardless of tool/mode, with the surface named in the secret-free reason — and use it from both the non-interactive chat driver and the one-shot `agent run`. Behavior is byte-identical; the policy now has ONE home. Tests: +2 chat-mode cases (denies any request/tool — never a hang/auto-approve; names the surface in the reason). 110 chat-mode/chat/agent-run tests pass; typecheck/eslint/prettier green. Refs: docs/roadmap/phase-2.5-close-plan.md (Step 14, Batch D); ADR-0057 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tional recovery
Make a pure SCOPE denial recoverable so the interactive chat turn can adapt
instead of dying — the "conversational recovery" of ADR-0057, scoped tightly to
fs/media (the maintainer's 2.5-close D14-c decision).
Taxonomy change (`changes the denial taxonomy`): the `recoverable` flag moves
from `ToolExecutionError` onto the base `ToolDispatchError` (default FALSE — the
safe direction; a forgotten opt-in just stays fatal). It now gates the chat
within-turn recovery for TWO disjoint sources: an idempotent host execution
failure (unchanged), AND a scope denial refused BEFORE any side effect. Exactly
two `tool_denied`s opt in:
- `ToolPolicyError('media_scope_denied')` — read_media's idempotent scope denial.
- the CLI fs arm's PURE scope-tier escape (`FsScopeDeniedError` "the path escapes
the allowed filesystem scope") — the canonical "path outside the workspace".
`isRecoverableToolError` (agent-turn.ts) simplifies to `recoverToolFailures &&
err.recoverable` (plus the always-correctable unknown_tool/invalid_args). A
recoverable scope denial is fed back as an `isError` tool_result so the model
adapts to an in-bounds path; the floor STILL denies every attempt (no bypass).
Kept FATAL (recoverable=false — no capability leak, no probe oracle, no
re-execution): a user reject / fail-closed approval, every other guardrail reason
(not_granted / command / domain / gate), an egress SSRF denial, and the fs
CONFIDENTIALITY (secret-store read) / protected-path / symlink / hard-link
refusals. Only on the chat surface (recoverToolFailures) — a WORKFLOW run stays
fail-fast/deterministic.
Tests: +2 agent-turn (recovers media_scope_denied when recoverToolFailures;
does NOT recover a not_granted guardrail denial even then), +2 fs (the scope
escape is recoverable:true; the confidentiality-floor read is recoverable:false).
985 core + 1626 cli tests pass; typecheck/eslint/prettier/build/engine-deps green.
Canonical taxonomy home updated (tool-registry.md).
Refs: docs/roadmap/phase-2.5-close-plan.md (Step 14, Batch D); ADR-0057
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…urity-review.md for Batch D Append-only ADR-0057 amendment note recording the Step-14 approval/security batch (the deferred `[c]`/non-TTY follow-ups landed, plus the SCOPE-denial recovery taxonomy change, the Ctrl+T approval whitelist, and the Trojan-Source floor) — the core guarantees are unchanged (no ungated governed dispatch, no protected-path / jail escape, read-only `ask` default). Extends security-review.md with a "CLI terminal-render safety + interactive approval" subsection: the bidi/Trojan-Source render floor, the fail-closed approval keyboard + the sanitized/bounded `[c]` reason, and the by-construction safety of the two recoverable scope denials (vs the fatal confidentiality/SSRF/user/guardrail denials). Refs: docs/roadmap/phase-2.5-close-plan.md (Step 14, Batch D); ADR-0057 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…+ doc accuracy + surrogate-safe cap Applied the security + Opus review findings (both returned 0 blocker / 0 high; all five guarantees traced + confirmed). Fixes: - MEDIUM (canonical-home drift, rule #8): agent-runner.md's failure-ladder table still called ToolPolicyError unconditionally "not fed back" — corrected to note the Step-14 `media_scope_denied` recoverable exception, linking the tool-registry.md taxonomy note (tool-registry.md was updated in D-3; the linked ladder home was missed). - LOW: two stale "`[c]` … deferred follow-up" comments (chat-store.ts PendingApproval, chat-mode-host.ts MakeChatModeEnv.prompt) — `[c]` landed in D-1; updated both to `[c] reason` + the reject-reason seam. - NIT: `sanitizeApprovalReason` capped by UTF-16 code unit — could split an astral pair into a lone surrogate at the 300-char boundary. Back off one unit when the cap lands on a high surrogate (+ a regression test). - Doc accuracy (security-review finding): the `recoverToolFailures` recovery surface is the chat-read-write host (chat / Home / one-shot `agent run`) — not "interactive chat only"; corrected in tool-registry.md, the ADR-0057 amendment, and security-review.md. Documented the accepted, secret-free scope-boundary probe residual (bounded by maxToolCorrections; the same round-bounded feedback pre-existed for an idempotent not-found read) + the createDirs idempotent-mkdir exception to "before any side effect". - deferred-tasks.md: marked the four now-landed 2.5.E entries DONE (bidi strip, `[c]`, scope-recovery, non-TTY policy); recorded the two optional Step-14 review follow-ups (approval-consent-line zero-width hardening; extract the `[c]` capture to a shared reducer for ChatApp test parity). Accepted as-is (documented): the `createChatModeControl` `interactive` default (true = the ink-REPL convention; the sole production caller always passes the computed flag; off-TTY the worst case is a hang, never an auto-approve). Tests: 67 projection cases pass; typecheck/eslint/prettier/fence green. Refs: docs/roadmap/phase-2.5-close-plan.md (Step 14, Batch D); ADR-0057 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…verToolFailures-surface phrasing Sonnet review of Batch D (0 blocker / 0 high; it independently re-traced the whole taxonomy + ran the full suites green). Applied: - MEDIUM (canonical-home consistency): the babd0e4 fixup corrected "interactive chat surface [only]" → the precise "recoverToolFailures surfaces (chat / Home / one-shot agent run)" in 3 docs but left 5 other spots (incl. the agent-runner.md row it wrote) saying "interactive chat surface". Propagated the precise phrasing to all five: ToolDispatchError.recoverable JSDoc (errors.ts), isRecoverableToolError JSDoc (agent-turn.ts), FsScopeDeniedError class doc (fs.ts), the agent-runner.md ladder row, and tool-registry.md's "Loop-correctable vs terminal" blockquote. Also fixed a duplicated {@link} the edit introduced in agent-turn.ts. - LOW: pinned `media_scope_denied`'s recoverability at its SOURCE — added `expect(err.recoverable).toBe(true)` to the builtins.test.ts read_media scope-denial case (previously only the agent-turn integration test covered it). Deferred to the close-out Final step (a whole-epic convention, not new to this batch): the commits' `Refs:` cite the untracked/scratch phase-2.5-close-plan.md — the Final phase-doc reconciliation + the PR body provide the durable record. Tests: 52 builtins cases pass (incl. the new assertion); core+cli typecheck, eslint, prettier green. Refs: docs/roadmap/phase-2.5-close-plan.md (Step 14, Batch D); ADR-0057 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make `wiredToolIds` (the ADR-0055 advertise-filter) read-only aware: with
`{ readOnly: true }` it stops advertising a WRITE-class tool (`fsWrite`) whose
fs arm IS wired but always denies the write on a read-only host — an
always-denied advertisement — the advertise-side complement to the read-only fs
arm's dispatch refusal (still authoritative). Read/list fs tools are unaffected;
the default (read-write) is unchanged, so the live `chat-read-write` session host
is inert. Closes the deferred "profile-unaware advertise-filter" item.
Tests: +1 assemble case (readOnly drops write_file, keeps read/list; default
keeps write_file). 15 tool-host tests pass; typecheck/eslint/prettier green.
Refs: docs/roadmap/phase-2.5-close-plan.md (Step 15, Batch E); ADR-0055
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…for @-mention Add a dependency-free, ReDoS-safe `.gitignore` / `.relaviumignore` matcher (gitignore.ts) and fold it into the `@`-mention completion candidate filter so `@` no longer surfaces files the project already ignores. A UX/privacy nicety, NOT a security control — the confidentiality floor + listing-gate remain the authoritative fs-capability enforcement (a gitignore file is untrusted project data; the matcher can only HIDE more, never widen what the jail refuses). - The matcher supports the common subset: blank/`#`-comment lines, `!` negation (last match wins), trailing-`/` directory-only, leading-`/`/mid-`/` anchoring vs. any-depth basename match, and the `*` (in-segment) / `**` (cross-dir) / `?` globs. glob→regex is linear (no nested quantifiers → ReDoS-safe). Regex-special chars in a literal pattern are escaped. Documented subset limits (deferred, and only ever UNDER-hide — never a security gap): nested per-dir ignore files (only the workspace-root files are read) + `[a-z]` char classes. - `createMentionReader` lazily reads the root ignore files ONCE (best-effort — a missing/unreadable/refused file degrades to no rules, never a crash), caches the compiled matcher, and drops an ignored entry from each listing. An ignored DIRECTORY is dropped too, so its children are never listed (the per-entry filter naturally respects "an ignored dir hides everything under it"). Tests: +12 gitignore matcher cases (each pattern family + negation + layering + escaping) and +1 mention integration case (real .gitignore + .relaviumignore filter a listing, negation survives). 27 mention/gitignore tests pass; typecheck/eslint/prettier green. Marked the deferred entry done. Refs: docs/roadmap/phase-2.5-close-plan.md (Step 15, Batch E); ADR-0061 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e 2.5 complete
Reconcile every status surface now that the 2.5 close-out has landed:
- 2.5.I (regression harness + DB concurrency hardening) and 2.5.J (docs-debt +
NO_COLOR/FORCE_COLOR/--color) marked ✅ Done; milestone M2.5-4 reached; Phase 2.5
declared complete — in the phase-2.5 doc, current.md, and CLAUDE.md.
- Recorded the doable-now Batch A–E backlog that landed alongside 2.5.I/J (each
implement → Opus → Sonnet, a security-review pass on the ADR-0057 approval batch):
A test-hardening, B 2.5.H TUI polish, C AgentParseError line/col, D the approval/
security batch (behind the append-only ADR-0057 amendment note), E the profile-aware
advertise-filter + the in-house .gitignore matcher.
- Recorded the DEFERRED follow-ups in deferred-tasks.md (maintainer call): the two
gate.ts-resume items (`relavium budget resume` + secret re-provide — both refactor
the security-sensitive cross-process resume path, best landed together with fresh
context), the session {{ctx.*}} interpolation (Proposed ADR-0060, Phase-2.6), and
extraRoots (blocked on its config source). Marked the four now-landed 2.5.E backlog
entries + the bidi-strip + gitignore entries done.
This gives the close-out a durable phase-doc record (the per-commit `Refs:` pointed
at the now-deleted scratch close-plan).
Full acceptance gate green: `pnpm turbo run lint typecheck test build` (24/24 tasks,
1641 CLI tests), `format:check`, `lint:fence-check`, `lint:engine-deps` all pass.
Refs: docs/roadmap/phases/phase-2.5-cli-consolidation.md (2.5.I/2.5.J, M2.5-4)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Sorry @cemililik, your pull request is larger than the review limit of 150000 diff characters
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (14)
✅ Files skipped from review due to trivial changes (4)
🚧 Files skipped from review as they are similar to previous changes (7)
📝 WalkthroughWalkthroughThis PR closes Phase 2.5 with concurrency hardening, approval-flow changes, parser and TUI updates, read-only tool filtering, and broad documentation refreshes. ChangesSQLite Concurrency Hardening
Estimated code review effort: 4 (Complex) | ~75 minutes Approval Flow and Recoverable Denials
Estimated code review effort: 4 (Complex) | ~60 minutes Parser, Color, Mention, and Render Updates
Estimated code review effort: 4 (Complex) | ~70 minutes Read-Only Tool Filtering and Misc Tests
Estimated code review effort: 2 (Simple) | ~15 minutes Documentation and Roadmap Updates
Estimated code review effort: 2 (Simple) | ~15 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant ChatInk
participant HomeController
participant Store
User->>ChatInk: press c during pending approval
ChatInk->>HomeController: reject-with-reason
HomeController-->>ChatInk: open reasonDraft
User->>ChatInk: Enter reason
ChatInk->>HomeController: submit sanitized reason
HomeController->>Store: reject approval
sequenceDiagram
participant Writer1
participant withBusyRetry
participant SQLiteDB
Writer1->>SQLiteDB: BEGIN IMMEDIATE
withBusyRetry->>SQLiteDB: BEGIN IMMEDIATE
SQLiteDB-->>withBusyRetry: SQLITE_BUSY
withBusyRetry->>withBusyRetry: sleep(backoff)
Writer1->>SQLiteDB: COMMIT
withBusyRetry->>SQLiteDB: retry fn
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Code Review
This pull request completes Phase 2.5 (CLI Consolidation) by introducing database concurrency hardening with BEGIN IMMEDIATE and deterministic retries, adding robust E2E test suites (concurrency, session-chain, and query-shape performance budgets), implementing a custom .gitignore matcher for completions, and addressing security concerns like Trojan-Source bidi overrides. Feedback from the review highlights a bug in the newly added .gitignore directory-matching logic, where files inside ignored directories bypass the rules, and provides a detailed refactoring suggestion to resolve it. Additionally, the reviewer recommends using optional chaining on process.stdout to prevent potential runtime crashes in headless or test environments.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| function globToRegExp(glob: string, anchored: boolean): RegExp { | ||
| let src = ''; | ||
| for (let i = 0; i < glob.length; i += 1) { | ||
| const ch = glob[i]; | ||
| if (ch === '*') { | ||
| if (glob[i + 1] === '*') { | ||
| i += 1; // consumed the second '*' | ||
| if (glob[i + 1] === '/') { | ||
| i += 1; // consumed the '/' | ||
| src += '(?:.*/)?'; // `**/` — zero or more leading directories | ||
| } else { | ||
| src += '.*'; // `**` (crosses `/`) | ||
| } | ||
| } else { | ||
| src += '[^/]*'; // a single `*` stays within a segment | ||
| } | ||
| } else if (ch === '?') { | ||
| src += '[^/]'; | ||
| } else if (ch !== undefined) { | ||
| src += escapeRe(ch); | ||
| } | ||
| } | ||
| const head = anchored ? '^' : '(?:^|/)'; | ||
| return new RegExp(`${head}${src}(?:/|$)`); | ||
| } |
There was a problem hiding this comment.
The current implementation of globToRegExp always appends (?:/|$) to the compiled regular expression. However, for directory-only rules (e.g., node_modules/), this causes files directly under the ignored directory (like node_modules/a.txt) to bypass the ignore rule when checked individually with isDir = false because rule.dirOnly && !isDir evaluates to true and skips the rule.
To fix this, we should allow globToRegExp to accept a dirOnly parameter and append / instead of (?:/|$) when dirOnly is true. This ensures that directory-only rules only match if the matched segment is followed by a slash (which is always true for descendants of the directory, or for the directory itself if we append a trailing slash).
function globToRegExp(glob: string, anchored: boolean, dirOnly = false): RegExp {
let src = '';
for (let i = 0; i < glob.length; i += 1) {
const ch = glob[i];
if (ch === '*') {
if (glob[i + 1] === '*') {
i += 1; // consumed the second '*'
if (glob[i + 1] === '/') {
i += 1; // consumed the '/'
src += '(?:.*/)?'; // **/ - zero or more leading directories
} else {
src += '.*'; // ** (crosses /)
}
} else {
src += '[^/]*'; // a single * stays within a segment
}
} else if (ch === '?') {
src += '[^/]';
} else if (ch !== undefined) {
src += escapeRe(ch);
}
}
const head = anchored ? '^' : '(?:^|/)';
const tail = dirOnly ? '/' : '(?:/|$)';
return new RegExp(`${head}${src}${tail}`);
}| function compileRule(raw: string): IgnoreRule | undefined { | ||
| const negate = raw.startsWith('!'); | ||
| let pattern = negate ? raw.slice(1) : raw; | ||
| const dirOnly = pattern.endsWith('/'); | ||
| if (dirOnly) pattern = pattern.slice(0, -1); | ||
| // Anchored if a `/` is at the start or the middle (a trailing `/` was already stripped above). | ||
| const anchored = pattern.startsWith('/') || pattern.includes('/'); | ||
| if (pattern.startsWith('/')) pattern = pattern.slice(1); | ||
| if (pattern.length === 0) return undefined; | ||
| return { re: globToRegExp(pattern, anchored), negate, dirOnly }; | ||
| } |
There was a problem hiding this comment.
Pass the dirOnly flag to globToRegExp so that directory-only rules are compiled with a trailing slash requirement.
| function compileRule(raw: string): IgnoreRule | undefined { | |
| const negate = raw.startsWith('!'); | |
| let pattern = negate ? raw.slice(1) : raw; | |
| const dirOnly = pattern.endsWith('/'); | |
| if (dirOnly) pattern = pattern.slice(0, -1); | |
| // Anchored if a `/` is at the start or the middle (a trailing `/` was already stripped above). | |
| const anchored = pattern.startsWith('/') || pattern.includes('/'); | |
| if (pattern.startsWith('/')) pattern = pattern.slice(1); | |
| if (pattern.length === 0) return undefined; | |
| return { re: globToRegExp(pattern, anchored), negate, dirOnly }; | |
| } | |
| function compileRule(raw: string): IgnoreRule | undefined { | |
| const negate = raw.startsWith('!'); | |
| let pattern = negate ? raw.slice(1) : raw; | |
| const dirOnly = pattern.endsWith('/'); | |
| if (dirOnly) pattern = pattern.slice(0, -1); | |
| // Anchored if a / is at the start or the middle (a trailing / was already stripped above). | |
| const anchored = pattern.startsWith('/') || pattern.includes('/'); | |
| if (pattern.startsWith('/')) pattern = pattern.slice(1); | |
| if (pattern.length === 0) return undefined; | |
| return { re: globToRegExp(pattern, anchored, dirOnly), negate, dirOnly }; | |
| } |
| ignores(relPath, isDir) { | ||
| const path = relPath.replace(/^\.\//, '').replace(/\/+$/, ''); // normalize a leading `./` + a trailing `/` | ||
| if (path.length === 0) return false; | ||
| let ignored = false; | ||
| for (const rule of rules) { | ||
| if (rule.dirOnly && !isDir) continue; // a `foo/` rule never matches a file | ||
| if (rule.re.test(path)) ignored = !rule.negate; // last matching rule wins (negation re-includes) | ||
| } | ||
| return ignored; | ||
| }, |
There was a problem hiding this comment.
Update the ignores function to append a trailing slash to path when isDir is true, and remove the manual rule.dirOnly check. Since directory-only rules are now compiled with a trailing slash requirement, they will correctly match directory paths (which now end in /) and descendant paths (which naturally contain /), while correctly ignoring file paths (which do not end in /).
ignores(relPath, isDir) {
const path = relPath.replace(/^\.\//, '').replace(/\/+$/, '') + (isDir ? '/' : '');
if (path.length === 0 || path === '/') return false;
let ignored = false;
for (const rule of rules) {
if (rule.re.test(path)) ignored = !rule.negate;
}
return ignored;
},| // `nowMs={Date.now()}`); the frame loop re-renders, so a resize is picked up on the next tick. | ||
| // `stdout.columns` is typed `number` but is `undefined` at runtime off a TTY — the formatter's 80-col | ||
| // fallback covers that (moot here anyway: ChatApp only mounts on a TTY via the driveInk gate). | ||
| columns={process.stdout.columns} |
There was a problem hiding this comment.
Using process.stdout.columns directly can cause a runtime TypeError if process.stdout is undefined (which can happen in certain non-TTY, headless, or test environments where standard streams are redirected or mocked).
Using optional chaining (process.stdout?.columns) is safer and prevents potential crashes.
| columns={process.stdout.columns} | |
| columns={process.stdout?.columns} |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
docs/roadmap/current.md (1)
417-424: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winKeep the status note in one blockquote.
The empty line between the two quoted paragraphs trips markdownlint MD028, so the docs CI may fail. Remove the separator or quote it as
>if you want a blank line.Suggested fix
> **Status:** ✅ **Done (PR `#66`, 2026-07-07)** — all **12** dependency-ordered steps (S1–S12) landed behind > [ADR-0063](../../decisions/0063-cli-config-write-contract.md)/[ADR-0064](../../decisions/0064-live-model-catalog.md)/[ADR-0065](../../decisions/0065-provider-economics-and-extensibility.md), > each with an Opus + Sonnet review round (six with an added security round: S2/S5/S6/S8/S9/S10); the post-2.5.G > model-UX follow-up ([ADR-0059](../../decisions/0059-cli-mid-session-model-reseat.md) mid-session reseat + > [ADR-0066](../../decisions/0066-normalized-reasoning-effort-control.md) reasoning-effort) landed in the same PR. > With 2.5.G done, **milestone M2.5-2** (Home + entry + onboarding) is reached. -> > **Scope expanded to Option A (2026-07-05).**🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/roadmap/current.md` around lines 417 - 424, The status note is split into two blockquoted paragraphs with a blank line between them, which triggers markdownlint MD028. In the current markdown section, keep the entire note in a single blockquote by removing the empty separator or converting the blank line to a quoted blank line, and verify the quoted text remains contiguous in the same blockquote block.Source: Linters/SAST tools
🧹 Nitpick comments (7)
docs/roadmap/deferred-tasks.md (1)
521-527: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMalformed link syntax:
{@linkgitignore.ts}won't render in Markdown.
{@link...}is a JSDoc/TSDoc tag, not Markdown syntax — in a.mdfile it will render as literal text{@linkgitignore.ts}rather than a link. As per coding guidelines,docs/**/*.mdshould use "relative links" for cross-references.📝 Proposed fix
- dependency-free, ReDoS-safe in-house matcher ({`@link` gitignore.ts}) folds the workspace-root `.gitignore` + + dependency-free, ReDoS-safe in-house matcher ([`gitignore.ts`](../../apps/cli/src/render/tui/gitignore.ts)) folds the workspace-root `.gitignore` +🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/roadmap/deferred-tasks.md` around lines 521 - 527, The roadmap entry uses JSDoc-style `{`@link` gitignore.ts}` inside a Markdown file, so it will not render as a link. Update the affected note in the deferred-tasks section to use a proper relative Markdown link for the `gitignore.ts` reference, keeping the rest of the wording intact. Use the existing `gitignore.ts` mention in that bullet as the anchor and ensure the cross-reference follows the docs/*.md relative-link convention.Source: Coding guidelines
apps/cli/src/render/tui/chat-projection.ts (1)
85-90: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value
charCodeAthere is correct — SonarCloud'scodePointAtsuggestion is a false positive.Detecting a lone leading surrogate at the cap boundary requires inspecting the UTF-16 code unit;
codePointAtwould return the combined code point (or the low half) and defeat the check. Consider a// NOSONAR/ inline disable to silence the analyzer rather than changing the logic.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/cli/src/render/tui/chat-projection.ts` around lines 85 - 90, The UTF-16 boundary check in chat-projection’s truncation logic should stay based on charCodeAt, not codePointAt, because it must detect a lone leading surrogate at the MAX_APPROVAL_REASON_CHARS cap. Update the surrounding comment or add a targeted NOSONAR/inline suppression near the truncation logic in chatProjection so SonarCloud ignores this false positive without changing the behavior of the slice/end calculation.Source: Linters/SAST tools
apps/cli/src/render/tui/home-controller.ts (1)
1327-1355: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the reason-capture routing to clear the failing complexity gate.
SonarCloud fails this function at cognitive complexity 19 (limit 15). The
reasonDraftcapture block is self-contained and follows the same shape asrouteMentionKey/routeSearchKey— extracting it into arouteReasonKey(...) : booleanhelper dropshandleChatKey's complexity and keeps the router flat.♻️ Extract
routeReasonKey+ // The open `[c]` typed-reason capture owns every key (Step 14) — parity with routeMentionKey/routeSearchKey. + // Returns whether the key was consumed (the capture was open). + const routeReasonKey = (active: HomeChatSession, input: string, key: ChatKey): boolean => { + const openReason = state.reasonDraft; + if (openReason === undefined) return false; + if (active.store.getSnapshot().approval === undefined) { + set({ reasonDraft: undefined }); // the approval vanished — discard the orphaned capture + return true; + } + if (key.escape === true) { + set({ reasonDraft: undefined }); // cancel the reason; the approval stays pending + return true; + } + if (key.return === true && key.shift !== true) { + const reason = sanitizeApprovalReason(openReason.text); + set({ reasonDraft: undefined }); + active.store.answerApproval( + reason === undefined ? { outcome: 'reject' } : { outcome: 'reject', reason }, + ); + return true; + } + const edit = reduceEditorMotion(input, key); + if (edit !== undefined) set({ reasonDraft: applyEditorAction(openReason, edit) }); + return true; + }; + const handleChatKey = (active: HomeChatSession, input: string, key: ChatKey): void => { if (tearingDown === active) return; // a key arriving mid-teardown must not drive sendMessage on a cancelled session - // The `[c]` typed-reason capture (Step 14) owns the keyboard while open — checked FIRST. ... - const openReason = state.reasonDraft; - if (openReason !== undefined) { - if (active.store.getSnapshot().approval === undefined) { - set({ reasonDraft: undefined }); - return; - } - if (key.escape === true) { - set({ reasonDraft: undefined }); - return; - } - if (key.return === true && key.shift !== true) { - const reason = sanitizeApprovalReason(openReason.text); - set({ reasonDraft: undefined }); - active.store.answerApproval( - reason === undefined ? { outcome: 'reject' } : { outcome: 'reject', reason }, - ); - return; - } - const edit = reduceEditorMotion(input, key); - if (edit !== undefined) set({ reasonDraft: applyEditorAction(openReason, edit) }); - return; - } + // The `[c]` typed-reason capture (Step 14) owns the keyboard while open — checked FIRST. + if (routeReasonKey(active, input, key)) return;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/cli/src/render/tui/home-controller.ts` around lines 1327 - 1355, The `handleChatKey` function is over the cognitive complexity limit because the `reasonDraft` capture logic is embedded inline. Extract that self-contained branch into a new `routeReasonKey(...) : boolean` helper, following the same routing pattern used by `routeMentionKey` and `routeSearchKey`, and have `handleChatKey` delegate to it before continuing with the remaining key handling. Keep the helper responsible for the full reason-capture flow, including stale approval checks, Escape cancel, Enter-to-reject, and editor updates, so the main router stays flat.Source: Linters/SAST tools
apps/cli/src/harness/perf-budget.e2e.test.ts (1)
36-44: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUnsafe
ascasts on the DB boundary.
params as ReadonlyArray<...>androw as { detail?: string }are unsafeascasts, which the project's TS guideline for.tsfiles disallows in favor of type guards. Since this only touches test-harness code readingbetter-sqlite3'sunknown/any-typed results, the risk is low, but a small runtime guard would keep this file compliant without much effort.As per coding guidelines,
**/*.{ts,tsx}: "TypeScript-first, strict. Noany, no unsafeas."♻️ Proposed type-guard alternative
+ function isBindValue(v: unknown): v is string | number | bigint | Buffer | null { + return ( + v === null || + typeof v === 'string' || + typeof v === 'number' || + typeof v === 'bigint' || + Buffer.isBuffer(v) + ); + } + function planFor(query: { toSQL: () => { sql: string; params: unknown[] } }): string[] { const { sql, params } = query.toSQL(); - const bind = params as ReadonlyArray<string | number | bigint | Buffer | null>; + if (!params.every(isBindValue)) { + throw new Error('unexpected non-primitive bind param from drizzle toSQL()'); + } const rows = client.sqlite.prepare(`EXPLAIN QUERY PLAN ${sql}`).all(...bind); - return rows.map((row) => (row as { detail?: string }).detail ?? ''); + return rows.map((row) => + typeof row === 'object' && row !== null && 'detail' in row && typeof row.detail === 'string' + ? row.detail + : '', + ); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/cli/src/harness/perf-budget.e2e.test.ts` around lines 36 - 44, The `planFor` helper in `perf-budget.e2e.test.ts` uses unsafe `as` casts at the SQLite boundary for both `params` and `row.detail`, which violates the TypeScript strictness guideline. Replace those casts with small runtime type guards around the `toSQL()` params and the `EXPLAIN QUERY PLAN` rows, then only pass validated bind values into `client.sqlite.prepare(...).all(...)` and safely read `detail` from rows in the map step.Source: Coding guidelines
docs/standards/security-review.md (1)
350-361: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRestates the canonical recoverable-denial taxonomy instead of linking to it.
This paragraph re-enumerates the same recoverable/fatal denial taxonomy already canonically documented in
docs/reference/shared-core/tool-registry.md(§Error taxonomy, the "recoverable" subsection). As per coding guidelines,docs/**/*.md: "Specs live in [docs/reference/]; link, don't restate. One canonical home per artifact."Consider trimming this to a short security summary that links to the tool-registry.md taxonomy for the authoritative enumeration, so future taxonomy changes only need updating in one place.
✏️ Example trim
-- **A recoverable SCOPE denial is safe by construction.** Only two `tool_denied`s are fed back to - the model for in-turn conversational recovery on the `recoverToolFailures` surfaces (the - chat-read-write host — `relavium chat` / the Home / the one-shot `agent run`; a WORKFLOW run stays - fatal/deterministic): a media scope denial and the fs **pure scope-tier escape** — both refused - BEFORE any side effect, with a **secret-free, path-free** reason, so the model adapts to an - in-bounds path while the floor still denies every attempt (no bypass, bounded by - `maxToolCorrections`). The scope-membership signal a probing model can gather over that bounded - window is an accepted, secret-free workspace-boundary oracle (the same round-bounded feedback - already existed for an idempotent not-found read). The **confidentiality** (secret-store read), - protected-path, symlink/hard-link, egress-SSRF, and user/guardrail denials stay FATAL — feeding - those back would re-deny, risk a re-execution, or leak a probe oracle. +- **A recoverable SCOPE denial is safe by construction.** Only two `tool_denied` reasons are fed + back to the model for in-turn conversational recovery (a WORKFLOW run stays fatal/deterministic). + See [tool-registry.md §Error taxonomy](../reference/shared-core/tool-registry.md#error-taxonomy) + for the authoritative list of recoverable vs. fatal denial reasons and the safety rationale.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/standards/security-review.md` around lines 350 - 361, The security-review paragraph is restating the recoverable/fatal denial taxonomy instead of pointing to the canonical source. Trim this section in docs/standards/security-review.md to a brief summary and add a link to the authoritative taxonomy in docs/reference/shared-core/tool-registry.md, keeping the detailed enumeration only in that canonical reference. Use the surrounding security policy text to preserve the high-level guidance while removing duplicated denial categories.Source: Coding guidelines
packages/db/src/model-catalog-store.ts (1)
491-589: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the transaction callback to clear the SonarCloud nesting failure.
The
withBusyRetry(() => db.transaction(() => { … }))wrapper pushed therows.map/branch bodies past the 4-level nesting threshold, tripping the failing SonarCloud check at Line 565. Hoisting the transaction body into a named local removes one level without changing behavior; apply the same shape toupsertfor consistency.♻️ Sketch: hoist the transaction body
- replaceProviderModels: (providerId, rows, now) => - withBusyRetry(() => - db.transaction( - () => { - let added = 0; - let updated = 0; - // … - return { added, updated, deactivated: deactivateResult.changes }; - }, - { behavior: 'immediate' }, - ), - ), + replaceProviderModels: (providerId, rows, now) => { + const applyReplace = (): ReplaceResult => { + let added = 0; + let updated = 0; + // … + return { added, updated, deactivated: deactivateResult.changes }; + }; + return withBusyRetry(() => db.transaction(applyReplace, { behavior: 'immediate' })); + },🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/db/src/model-catalog-store.ts` around lines 491 - 589, The nested inline callback in withBusyRetry(db.transaction(...)) is pushing the model-catalog refresh logic past SonarCloud’s nesting limit. Hoist the transaction body in model-catalog-store into a named local function, then pass that function to db.transaction and keep withBusyRetry wrapping it; preserve the existing add/update/deactivate logic and apply the same refactor shape to upsert for consistency.Source: Linters/SAST tools
packages/db/src/media-reference-store.ts (1)
171-202: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueNesting depth flagged by SonarCloud (Lines 189, 244).
removeRunReferencesandreclaimExpiredeach nestwithBusyRetry(() => db.transaction(() => { ... for (...) {...} }))four function levels deep. Functionally fine and well-commented, but extracting each transaction body into a small named helper (e.g.runRemoveRunReferencesTxn(db, runId, ts)) would flatten this and make each piece independently testable/readable.Also applies to: 223-262
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/db/src/media-reference-store.ts` around lines 171 - 202, The nested transaction logic in removeRunReferences is too deep and should be flattened for readability and testability. Extract the db.transaction body into a small named helper such as runRemoveRunReferencesTxn(db, runId, ts) (and similarly for reclaimExpired where applicable), then have removeRunReferences call that helper inside withBusyRetry so the control flow is clearer while preserving the existing behavior.Source: Linters/SAST tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/cli/src/harness/concurrency.e2e.test.ts`:
- Around line 94-119: The runChild helper in the concurrency e2e test can leave
the ready promise unresolved when spawn fails, causing Promise.all waits to hang
instead of surfacing the error. Update runChild so the child.on('error') path
also calls signalReady() before rejecting, and keep the existing close fallback
in place; use the runChild, ready, done, and signalReady symbols to make the fix
in the same helper.
In `@apps/cli/src/render/tui/gitignore.ts`:
- Around line 42-76: The globToRegExp helper in gitignore.ts is turning
workspace ignore globs into potentially backtracking-heavy regexes, which can
stall ignores() during mention completion on crafted patterns. Update
globToRegExp and its callers to avoid using greedy regex matching for untrusted
patterns, either by enforcing strict length/complexity limits before compiling
or by replacing the regex approach with a linear-time glob matcher that handles
the same anchored and segment-boundary behavior.
In `@docs/uvp.md`:
- Around line 22-23: The wording in the UVP copy overstates the security
guarantee by saying API keys are “never at rest,” which is too absolute for the
OS keychain. Update the sentence in the docs text to explicitly say the keys are
stored in the OS keychain and do not reach history.db or plaintext storage,
keeping the rest of the surrounding copy unchanged.
In `@README.md`:
- Around line 135-139: Update the README status text to match the current
roadmap: the summary under the Phase 2.5 section should no longer say only the
regression-harness/concurrency and documentation-consolidation lanes remain,
since docs/roadmap/current.md now marks 2.5.I and 2.5.J complete and Phase 2.5
finished. Adjust the wording in the README paragraph so it reflects that Phase
2.5 is complete while preserving the rest of the release/status messaging.
---
Outside diff comments:
In `@docs/roadmap/current.md`:
- Around line 417-424: The status note is split into two blockquoted paragraphs
with a blank line between them, which triggers markdownlint MD028. In the
current markdown section, keep the entire note in a single blockquote by
removing the empty separator or converting the blank line to a quoted blank
line, and verify the quoted text remains contiguous in the same blockquote
block.
---
Nitpick comments:
In `@apps/cli/src/harness/perf-budget.e2e.test.ts`:
- Around line 36-44: The `planFor` helper in `perf-budget.e2e.test.ts` uses
unsafe `as` casts at the SQLite boundary for both `params` and `row.detail`,
which violates the TypeScript strictness guideline. Replace those casts with
small runtime type guards around the `toSQL()` params and the `EXPLAIN QUERY
PLAN` rows, then only pass validated bind values into
`client.sqlite.prepare(...).all(...)` and safely read `detail` from rows in the
map step.
In `@apps/cli/src/render/tui/chat-projection.ts`:
- Around line 85-90: The UTF-16 boundary check in chat-projection’s truncation
logic should stay based on charCodeAt, not codePointAt, because it must detect a
lone leading surrogate at the MAX_APPROVAL_REASON_CHARS cap. Update the
surrounding comment or add a targeted NOSONAR/inline suppression near the
truncation logic in chatProjection so SonarCloud ignores this false positive
without changing the behavior of the slice/end calculation.
In `@apps/cli/src/render/tui/home-controller.ts`:
- Around line 1327-1355: The `handleChatKey` function is over the cognitive
complexity limit because the `reasonDraft` capture logic is embedded inline.
Extract that self-contained branch into a new `routeReasonKey(...) : boolean`
helper, following the same routing pattern used by `routeMentionKey` and
`routeSearchKey`, and have `handleChatKey` delegate to it before continuing with
the remaining key handling. Keep the helper responsible for the full
reason-capture flow, including stale approval checks, Escape cancel,
Enter-to-reject, and editor updates, so the main router stays flat.
In `@docs/roadmap/deferred-tasks.md`:
- Around line 521-527: The roadmap entry uses JSDoc-style `{`@link` gitignore.ts}`
inside a Markdown file, so it will not render as a link. Update the affected
note in the deferred-tasks section to use a proper relative Markdown link for
the `gitignore.ts` reference, keeping the rest of the wording intact. Use the
existing `gitignore.ts` mention in that bullet as the anchor and ensure the
cross-reference follows the docs/*.md relative-link convention.
In `@docs/standards/security-review.md`:
- Around line 350-361: The security-review paragraph is restating the
recoverable/fatal denial taxonomy instead of pointing to the canonical source.
Trim this section in docs/standards/security-review.md to a brief summary and
add a link to the authoritative taxonomy in
docs/reference/shared-core/tool-registry.md, keeping the detailed enumeration
only in that canonical reference. Use the surrounding security policy text to
preserve the high-level guidance while removing duplicated denial categories.
In `@packages/db/src/media-reference-store.ts`:
- Around line 171-202: The nested transaction logic in removeRunReferences is
too deep and should be flattened for readability and testability. Extract the
db.transaction body into a small named helper such as
runRemoveRunReferencesTxn(db, runId, ts) (and similarly for reclaimExpired where
applicable), then have removeRunReferences call that helper inside withBusyRetry
so the control flow is clearer while preserving the existing behavior.
In `@packages/db/src/model-catalog-store.ts`:
- Around line 491-589: The nested inline callback in
withBusyRetry(db.transaction(...)) is pushing the model-catalog refresh logic
past SonarCloud’s nesting limit. Hoist the transaction body in
model-catalog-store into a named local function, then pass that function to
db.transaction and keep withBusyRetry wrapping it; preserve the existing
add/update/deactivate logic and apply the same refactor shape to upsert for
consistency.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e7da5fc9-e826-48b5-861d-37d5ced7be73
📒 Files selected for processing (81)
.github/workflows/ci.ymlCLAUDE.mdREADME.mdapps/cli/src/chat/agent-source.tsapps/cli/src/chat/chat-mode-host.tsapps/cli/src/chat/chat-mode.test.tsapps/cli/src/chat/chat-mode.tsapps/cli/src/chat/session-host.test.tsapps/cli/src/commands/agent-run.tsapps/cli/src/commands/chat.tsapps/cli/src/commands/provider.tsapps/cli/src/engine/media-gc.test.tsapps/cli/src/engine/tool-host/assemble.test.tsapps/cli/src/engine/tool-host/assemble.tsapps/cli/src/engine/tool-host/errors.tsapps/cli/src/engine/tool-host/fs.test.tsapps/cli/src/engine/tool-host/fs.tsapps/cli/src/harness/concurrency.e2e.test.tsapps/cli/src/harness/fixtures/concurrent-writer.mjsapps/cli/src/harness/perf-budget.e2e.test.tsapps/cli/src/harness/session-chain.e2e.test.tsapps/cli/src/process/options.test.tsapps/cli/src/process/options.tsapps/cli/src/program.tsapps/cli/src/render/tui/chat-ink.tsxapps/cli/src/render/tui/chat-input.test.tsapps/cli/src/render/tui/chat-input.tsapps/cli/src/render/tui/chat-projection.test.tsapps/cli/src/render/tui/chat-projection.tsapps/cli/src/render/tui/chat-store.tsapps/cli/src/render/tui/gitignore.test.tsapps/cli/src/render/tui/gitignore.tsapps/cli/src/render/tui/home-app.tsxapps/cli/src/render/tui/home-controller.test.tsapps/cli/src/render/tui/home-controller.tsapps/cli/src/render/tui/mention.test.tsapps/cli/src/render/tui/mention.tsapps/cli/src/run.tsapps/cli/src/workflows/catalog.test.tsdocs/architecture/agent-sessions.mddocs/architecture/local-first-and-security.mddocs/decisions/0029-tool-policy-hardening.mddocs/decisions/0057-cli-chat-modes-and-per-tool-approval.mddocs/decisions/0064-live-model-catalog.mddocs/decisions/README.mddocs/reference/cli/commands.mddocs/reference/cli/regression-harness.mddocs/reference/desktop/database-schema.mddocs/reference/desktop/tauri-plugins.mddocs/reference/shared-core/agent-runner.mddocs/reference/shared-core/tool-registry.mddocs/roadmap/current.mddocs/roadmap/deferred-tasks.mddocs/roadmap/phases/phase-2.5-cli-consolidation.mddocs/roadmap/phases/phase-2.6-conversational-authoring.mddocs/standards/security-review.mddocs/tech-stack.mddocs/tutorials/cli/start-a-chat-session.mddocs/uvp.mddocs/vision.mdpackages/core/src/agent-parser.test.tspackages/core/src/agent-parser.tspackages/core/src/engine/agent-turn.test.tspackages/core/src/engine/agent-turn.tspackages/core/src/tools/builtins.test.tspackages/core/src/tools/errors.tspackages/db/src/client.test.tspackages/db/src/media-reference-store.test.tspackages/db/src/media-reference-store.tspackages/db/src/media-store.test.tspackages/db/src/media-write.test.tspackages/db/src/model-catalog-store.test.tspackages/db/src/model-catalog-store.tspackages/db/src/provider-store.test.tspackages/db/src/provider-store.tspackages/db/src/retry.test.tspackages/db/src/retry.tspackages/db/src/run-history-store.test.tspackages/db/src/run-history-store.tspackages/db/src/session-store.test.tspackages/db/src/session-store.ts
…g, docs, Sonar refactors Address the PR #69 review findings (verified each against current code; fixed the still-valid ones, kept changes minimal): Correctness / security: - gitignore.ts: replace the glob→regex compiler with a LINEAR two-pointer glob matcher — closes THREE findings at once: (1) the ReDoS on untrusted `.gitignore` patterns (a crafted `**`-chain could backtrack super-linearly through the compiled regex; the matcher has no regex, so it is bounded/linear), (2) the directory-only bug (a `cache/` rule now correctly ignores FILES under it — `cache/a.txt` — not just the dir itself), and (3) the two SonarCloud super-linear-regex flags (no regex left). +2 tests (descendant match, and a 64-`**` × 200-segment pattern returns instantly). - concurrency.e2e.test.ts: the `runChild` `error` handler now calls `signalReady()` before rejecting, so a spawn failure can't leave `await child.ready` hanging. - chat-ink.tsx: `process.stdout?.columns` — optional-chain `stdout`, which can be undefined in a headless/redirected-stream harness (`.columns` on it would throw). Docs accuracy: - uvp.md: "API keys … never at rest" → "in the OS keychain — never in history.db or any plaintext store" (the keychain IS at-rest storage; the claim was too absolute). - README.md: Phase 2.5 status → complete (M2.5-4), not "only 2.5.I/J remain". - current.md: fix the MD028 (merge the Status/Last-updated header blockquotes); bump the date. - deferred-tasks.md: the `{@link gitignore.ts}` won't render in Markdown → a proper relative link. - security-review.md: trim the restated denial taxonomy to a summary + link the canonical tool-registry.md §error-taxonomy (one home). Strict-TS / Sonar (maintainability): - perf-budget.e2e.test.ts: replace two `as` casts at the SQLite boundary with runtime guards (`toBindValue` + `detailOf` via `in`-narrowing). - chat-projection.ts: `// NOSONAR` + rationale on the intentional `charCodeAt` (codePointAt would combine a surrogate pair, defeating the lone-surrogate check). - home-controller.ts: extract `routeReasonKey` (cognitive complexity 19 → under 15). - media-reference-store.ts + model-catalog-store.ts: hoist the `db.transaction` bodies into named factory-scoped helpers (function-nesting under the limit). Skipped: the phase-2.5-doc MD028 (pre-existing in the 2.5.G section, not this diff); model-catalog `upsert` "for consistency" (not Sonar-flagged — kept minimal). Gate: `pnpm turbo run lint typecheck test build` (24/24), format:check, fence, engine-deps — all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Post-merge traceability: the 2.5.I/2.5.J + Batch A–E close-out landed via PR #69 (development → main, merged 2026-07-08). Add the durable PR reference to every status surface (phase-2.5 doc, current.md, CLAUDE.md, README) — the per-commit `Refs:` had pointed at the now-deleted scratch close-plan, so the PR number is the durable pointer — and note Phase 2.5 is merged to `main`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… + PR #69 refs Rigorous audit of deferred-tasks.md against what the 2.5 close-out actually shipped — mark done ONLY what is genuinely in the merged code (verified each against the source), and leave the intentionally-deferred items open: Newly marked [x] DONE (were still [ ] — done this round but missed): - Profile-unaware advertise-filter → `wiredToolIds({ readOnly })` (Step 15, E). - Compact abort hint during streaming → `streamingAbortHint` (Step 12, B). - Bound the EXPANDED reasoning panel by rendered LINES → `MAX_REASONING_PANEL_LINES` row-tail (Step 12, B). - Allow Ctrl+T / /thinking during a pending approval → the whitelist in `reduceApprovalKey` (Step 14, D-4). - Two transitively-covered test gaps → the git_status union pin was ADDED (session-host.test.ts) and the failed-turn persister fold was CONFIRMED already pinned (persister.test.ts) (Step 11, A). Also added the durable `PR #69` merge reference to the six earlier close-out entries, so all eleven closed items consistently cite the merge. Left [ ] on purpose (NOT closed by this round — verified): the two gate.ts-resume follow-ups (budget resume + secret re-provide), the session {{ctx.*}} interpolation (Proposed ADR-0060), extraRoots (blocked on its config source), the two Step-14 review follow-ups (approval-consent zero-width; extract the [c] reducer), the AgentParseError chat-surface diagnostic gap, and the cross-process gate-resolve TOCTOU (needs a store-level uniqueness constraint — BEGIN IMMEDIATE does not close it). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
All ten workstreams 2.5.A–J shipped (milestone M2.5-4, merged to `main` via PR #69); the go/no-go exit criteria hold and Phase 2.6 is unblocked. Reconcile the phase-status surfaces that still framed 2.5 as in-progress: - phase-2.5-cli-consolidation.md: banner "In progress" → "✅ COMPLETE"; mark the go/no-go exit criteria "all five met" at phase close. - current.md: retire the "active phase = Phase 2.5" pointer (2.5 is historical); name Phase 2.6 (Conversational Authoring, Planned/unblocked) as the next phase in both the intro and the "Not started yet" section. - phase-2.6-conversational-authoring.md: note the 2.5 dependency is satisfied — status "Planned — next up", now unblocked. - CLAUDE.md: the Phase-2.5 status paragraph lead "is now underway" → "✅ complete" (its tail already read complete), resolving the lead/tail contradiction. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>



Phase 2.5 (CLI Consolidation) →
main— releaseThis brings Phase 2.5 complete from
developmenttomain(152 commits). The Phase-2.5 spine + experience + observability workstreams (2.5.A–H) landed earlier ondevelopmentvia their own feature PRs (#60–#67); this PR additionally contains the 2.5 close-out completed in this session — the consolidation lanes 2.5.I / 2.5.J and the doable-now Batch A–E backlog — which reaches milestone M2.5-4 and declares Phase 2.5 complete.The close-out followed a strict per-step cadence: implement → Opus review → fix → Sonnet review → fix, with a dedicated security-review pass before the review rounds on the security-touching steps. Every step's toolchain gates were green before commit.
Close-out contents (this session)
2.5.I — regression harness + DB concurrency hardening
loadFullwrapped in a single read transaction (torn-read guard).BEGIN IMMEDIATEwith a bounded, deterministic (no-jitter, per ADR-0040)SQLITE_BUSY/SQLITE_LOCKEDretry (withBusyRetry, over the pre-existing WAL +busy_timeout). ADR-0064 §5 records the amendment.Home → chat → resume → exportcassette-chain, and query-shape perf budgets (EXPLAIN QUERY PLAN: index-served, no filesort) + the 80×24 degrade assertion. An advisory Windows CI lane runs the DB + harness suites + a headless no-TTY smoke.2.5.J — docs-debt + color
0700/0600+ keychain, no credentials at rest — ADR-0050) across uvp/vision/tutorial + the README spine.--color/--no-color>NO_COLOR(any non-empty ⇒ off) >FORCE_COLOR(0/false⇒ off, supports-color convention) > default-on, orthogonal to the--json/CI output mode, with a full test matrix.Batch A–E (doable-now backlog)
git_statusunion pin.Esc to stopabort hint during streaming + a line-bounded reasoning panel (last-N rendered rows, not raw chars).fix(core):AgentParseErrorline/col diagnostics (the session{{ctx.*}}interpolation half deferred — governed by the Proposed ADR-0060, Phase-2.6).[c]reject-with-typed-reason at the approval prompt (both surfaces; sanitized + length-bounded; the fail-closed floor unchanged);nonInteractiveApprovalPrompt;media_scope_deniedbecomerecoverableon therecoverToolFailuressurfaces; user/guardrail/SSRF/confidentiality denials stay fatal);.gitignore/.relaviumignorematcher for@-mention.Deferred (tracked in
deferred-tasks.md)gate.tscross-process-resume items —relavium budget resumeand secret re-provide on gate resume — are deferred to a focused follow-up (both refactor the same security-sensitive resume path; Post-M0: comprehensive-review fixes + Phase 0 completion docs #4 relaxes a fail-closed guarantee, so it's best landed together with fresh context + a security review, not at the tail of this session).{{ctx.*}}interpolation stays with the Proposed ADR-0060 (Phase-2.6).project-tierextraRootsstays blocked on its config source.Acceptance gate (green)
pnpm turbo run lint typecheck test build(24/24 tasks; 1641 CLI + 985 core tests),pnpm -w run format:check,pnpm lint:fence-check,pnpm lint:engine-deps— all pass. The@relavium/llmseam is untouched;packages/corestays platform-free; the two ADR touches are append-only amendment notes (ADR-0057, ADR-0064 §5); no new runtime dependency.🤖 Generated with Claude Code
Summary by CodeRabbit
--color/--no-colorand documentedNO_COLOR/FORCE_COLORprecedence.