Skip to content

feat(cli): Phase 2.5.D — chat input ergonomics + @-mention + !-shell (ADR-0061)#64

Merged
cemililik merged 34 commits into
mainfrom
development
Jul 3, 2026
Merged

feat(cli): Phase 2.5.D — chat input ergonomics + @-mention + !-shell (ADR-0061)#64
cemililik merged 34 commits into
mainfrom
development

Conversation

@cemililik

@cemililik cemililik commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Phase 2.5.D — CLI chat input ergonomics (@-mention + !-shell)

Delivers 2.5.D across both interactive surfaces (relavium chat + the 2.5.B Home): the pure input-editor
ergonomics plus the two data-moving affordances (@-mention file injection, !-shell command escape) behind
ADR-0061 (Accepted after a two-round
maintainer security review; the mandatory adversarial security pass ran inside the step-4/5 review loops).

Built as 5 steps, each: implement → opus adversarial-review round → fix verified → sonnet round → fix
verified. Every finding was adversarially verified before fixing (false positives dropped).

What landed

Ergonomics (no security surface, no ADR):

  • Ctrl+J newline + multi-line render; ↑/↓ history + Ctrl+R reverse-search; readline cursor/word/line motions.
  • A cursor-bearing EditorState + a shared reduceEditorMotion across both surfaces (one raw-mode owner preserved).

@-mention (ADR-0061):

  • Dir-navigable file completion (a synthetic .. ascend row + backspace-to-parent) reading through the same
    FsCapability read_file uses — the workspace jail, the sensitive-read confidentiality floor (expanded to
    .env* / .aws / .docker/config.json / .envrc / .dockercfg + .env/ as a directory), the listing-gate,
    and the binary/size guards. A user typing @path replaces the confirmAction prompt, never the floor.
  • The file is injected as UNTRUSTED, nonce-fenced, byte+line-bounded context (a per-injection random nonce on
    both <file> tags so file bytes can't forge/close the frame; a 128 KiB / 400-line bound, surrogate-safe, so a
    large file can't freeze the editor; control + bidi + framing chars stripped from the path).

!-shell (ADR-0061, the security-bearing half):

  • The additive AgentSession.runUserCommand (EA8) routes !command through the one run_command
    dispatch boundary, reusing #runTurn's dispatch-context construction verbatim: enforcePolicy(allowedCommands)
    before the mode-aware confirmActionspawn/shell:false (a quote-aware tokenizer, no metachar/glob/$var
    expansion). The user grant of run_command is scoped to the one-off dispatch and never reaches the model.
  • [chat].allowed_commands / allowed_command_globs default EMPTY ⇒ ! inert (secure-by-default; the reversal
    of an earlier curated-default, per the maintainer security review — run_command has no arg/file floor). A
    non-allowlisted !cmd gets an actionable, secret-free hint; output injects as UNTRUSTED, doubly-bounded context.
  • @/! are TTY-only — a plain / --json driver treats a leading @/! as a literal.

Review outcomes (folded-in, verified)

  • Steps 1–3 (ergonomics): ~30 findings fixed across the opus+sonnet rounds, incl. 4 security/regression (a
    Ctrl+A↔approve-always collision, an Alt+digit approval bypass, a Ctrl+W newline-cross, and a coalesced-chunk
    constant-updater HIGH).
  • Step 4 (@): opus 6 confirmed (TUI-freeze hard cap, .. navigation, unforgeable nonce frame, floor expansion,
    Home stale-resolve guard); sonnet 8 confirmed (submit-generation guard, Shift+Tab overlay-swallow, surrogate-safe
    truncation, a line cap, .env dir floor, bidi strip, .gitignore deferral reconciled).
  • Step 5 (!): opus + the mandatory security pass confirmed 0 defects after adversarial verification; the sonnet
    second pass caught a HIGH three reviewers agreed on — a !-command in flight left no host-visible busy signal,
    so a message typed mid-command reached sendMessageSessionStateErrorcrashed the session — now fixed
    with a shellBusy input gate on both surfaces (+ a LOW UserCommandOutcome type-hygiene fix).

Engine change (the one documented exception)

The 2.5.D "no engine/seam change" line is amended by ADR-0061: EA8 AgentSession.runUserCommand — one additive,
pure method (no platform import, no vendor type across the @relavium/llm seam; engine purity holds). Canonical
home: agent-session-spec.md.

Docs reconciled

config-spec.md ([chat].allowed_commands), cli/chat-session.md (Input ergonomics + @/!), cli/home.md
(keymap), contracts/agent-session-spec.md (runUserCommand), standards/security-review.md (input-layer trigger),
shared-core/built-in-tools.md (floor), roadmap/phase-2.5 + current.md + deferred-tasks.md; ADR-0061 Accepted.

Verification

pnpm run ci green (turbo lint/typecheck/test + typecheck:tools + build + format:check + lint:fence-check +
lint:engine-deps + lint:bundle-closure). Leakwatch clean across all touched dirs. New tests cover the pure
models (editor, mention, shell, injection), the fs floor, the engine runUserCommand (allowlist/approval/spawn/
lifecycle), config resolution, and the Home wiring (@-flow, !-run/deny/bare, the busy-gate regression).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added cursor-aware Home/chat editor with multi-line entry, Up/Down history recall, and Ctrl+R reverse search (Ctrl+J for newline).
    • Added interactive-only @ file completion and ! shell escape with pending “chips” that expand only at submit and ! execution gated by [chat].allowed_commands / [chat].allowed_command_globs.
  • Bug Fixes
    • Prevented stale @ completion reads and ! results from applying to later messages; input is gated while a shell command runs.
    • Improved recoverable interactive tool failure handling with a secret-free tool_failed hint.
  • Documentation
    • Updated config/security/contract/roadmap docs for @/! semantics and allowlist resolution.
  • Tests
    • Expanded coverage for editor actions, mention/shell framing, recovery behavior, and stricter sensitive-path read denial.

cemililik and others added 23 commits July 3, 2026 02:12
…re-recovery contract

PR #63 merged 2026-07-03 (merge e0f278d) — 2.5.E (reseat-less chat modes + per-tool
approval + mid-turn abort) plus the same-PR chat-UX follow-up (A/B: recoverToolFailures
+ the recoverable stamp + the tool_failed hint) are all in main. Update the canonical
homes:

- roadmap/current.md + phases/phase-2.5-cli-consolidation.md: 2.5.E 🟡→✅ Done
  (PR #63, 2026-07-03); the CLI-Consolidation spine (2.5.A/B/C/E) is complete; next is
  the 2.5.D/F/G experience arm. current.md "Last updated" → 2026-07-03.
- CLAUDE.md: same status flip + a one-line note on the tool-failure-recovery follow-up.
- reference/shared-core/tool-registry.md: document ToolExecutionError.recoverable in the
  error taxonomy (stricter than retryable; gates the chat within-turn recovery, idempotent
  reads only; workflows ignore it).
- reference/cli/chat-session.md: document the chat recover-from-idempotent-read behavior
  + the static secret-free tool_failed hint.
- roadmap/deferred-tasks.md: the two 2.5.E resolved items now say "PR #63, merged"; add a
  tracked follow-up (conversationally recover from a SCOPE denial — needs a HostDeniedError
  sub-class split so a scope denial is distinguishable from an approval/protected denial).

Docs-only; prettier clean.

Refs: ADR-0057
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ecurity model (Proposed)

Pins the 2.5.D security model for the two input-layer surfaces before
implementation: @-mention reads through the read_file fs jail + sensitive-path
confidentiality floor (consent replaces only the confirmAction prompt, never the
floor/binary/size cap); !-shell routes through the one run_command boundary
(enforcePolicy allowedCommands before the confirmAction mode floor) via an additive,
pure AgentSession.runUserCommand() that reuses #runTurn's dispatch context — never a
forked apps/cli context. Maintainer decisions locked: same-jail @ read-scope,
directory-navigable completion, untrusted @/! injected context, and a curated
read-only default [chat].allowed_commands (first-class out-of-the-box, allowlist floor
intact). Sibling of ADR-0055/0057 at the input layer, not an amendment. Flips to
Accepted after the mandatory adversarial security review over steps 4-5.

The pure-ergonomics half of 2.5.D (Ctrl+J/history/Ctrl+R/cursor) needs no ADR.

Refs: ADR-0061, ADR-0055, ADR-0057, ADR-0029
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…k Accepted

Two-round maintainer security review drove substantive corrections:

- !-shell default REVERSED from a "curated read-only allowlist" to EMPTY. A
  non-empty chat default is a chat-specific relaxation forbidden by
  security-review.md (empty ⇒ disabled symmetry), and — since run_command has no
  arg/file confidentiality floor (only the fs capability does) — read-only commands
  (cat/grep/git show) would reopen the confidential-exfil vector default-on
  (!cat .env → provider + transcript). !-shell stays first-class via a first-class
  opt-in: config + the 2.5.G onboarding seed + an actionable deny hint. Runtime
  approve-any is rejected (dissolves empty⇒disabled + the @/! consistency).

- @-mention confidentiality floor corrected to the real isSensitiveReadPath member
  set and EXPANDED (.env/.env.* + .aws + .docker/config.json) — a shared-fs hardening
  that also strengthens read_file on every surface. Behavior-change note recorded.

- Clarified: exact-full-command-string matching + opt-in globs; spawn/shell:false
  (never exec); output double-bounded (BoundedBuffer + applyOutputBounding); the
  completion listing-gate (sensitive paths neither listed nor read); @/! are
  TTY-only (literal under plain/--json); config-spec resolution semantics
  (empty default, per-field last-writer-wins, replace/opt-out).

Status → Accepted (the mandatory adversarial security review runs inside the
step-4/5 opus+sonnet review loops + the final PR review).

Refs: ADR-0061, ADR-0055, ADR-0057, ADR-0029
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mpt (2.5.D step 1)

Replace the bare-string chat prompt buffer with a cursor-bearing EditorState
({ text, cursor }) + pure, cursor-general primitives (insertAtCursor,
deleteBeforeCursor, emptyEditor, editorFromText, applyEditorAction), wiring ONLY
end-of-buffer behavior so there is ZERO user-visible change — the readline motions,
multiline, history, and completion of later 2.5.D steps build on this model without
re-plumbing. Migrate BOTH input owners (ChatApp's editor/editorRef ref-shadow and the
Home controller's state.input) so they stay in lockstep on the one shared reducer; the
coalesced-chunk op-not-value functional fold is preserved (edit ops still fold over the
accumulated EditorState). dropLastCodePoint stays (reused by deleteBeforeCursor + the
palette query). Tests updated to the EditorState model + new primitive / burst /
code-point cases; the Home view threads editor.text to the unchanged ChatView/HomeView.

turbo lint typecheck test build + format:check + fence-check + engine-deps +
bundle-closure all green (1192 tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…overage)

Opus adversarial review (6 dimensions → per-finding verification): 0 HIGH/MEDIUM
behavioral findings — the EditorState refactor is sound. Applied:

- chat-input.ts: the ChatKeyAction op-not-value doc-comment referenced the removed
  `setInput` updater; point it at the migrated `setEditor(cur => applyEditorAction(...))`
  (ChatApp) and the Home's synchronous `set({ input })` fold.
- chat-input.test.ts: the [type, Return] NOTE comment lumped the Home's synchronous
  `state.input.text` field under the ChatApp "ref-shadow" label; split them — ChatApp's
  ref MITIGATES the same-chunk limit, the Home's plain field RESOLVES it outright (matching
  home-controller.ts), no residual limit.
- home-controller.test.ts: the Home migration asserted only `.input.text`; assert the whole
  EditorState so the Home cursor (== end in step 1) is covered too.

The 6 refuted findings were accurate observations of intended step-1 behavior (code-point
vs grapheme backspace is pre-existing + preserved; the discarded render cursor + total
applyEditorAction are step-2 surface). All tests green (1192); prettier clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n coverage gaps)

Sonnet adversarial review (mutation-tested): 0 HIGH, 0 functional bugs. Confirmed
test-coverage gaps on the primitives step 2's cursor motions build directly on:

- home-controller.test.ts: the Home EditorState cursor was pinned in only ONE
  assertion. Reviewer proved via mutation (freeze the paste-branch cursor / freeze
  deleteBeforeCursor's decrement) that 48-49/49 tests stayed green. Assert the WHOLE
  EditorState on backspace-to-empty (cursor 0), the single-block paste (cursor 18), and
  the multi-chunk paste (cursor 13) — pinning insertAtCursor (paste) + deleteBeforeCursor
  (repeated) arithmetic on the Home surface.
- chat-input.test.ts: no `toBe` reference-identity assertion pinned the documented
  no-op `return editor` contract (the React Object.is render-skip); a regression returning
  a fresh equal object would pass every `toEqual`. Added toBe for all three no-op paths
  (empty insert, backspace-at-start, backspace-on-empty).

The 3rd confirmed finding (render threads editor.text, discarding cursor) is step-2
surface, already acknowledged — no step-1 action. Tests green (1193); prettier clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cursor-validity contract

The sonnet codepoint reviewer (re-run after a transient crash) verified: no HIGH bugs,
code-point/surrogate handling correct for every step-1 input (cursor is always 0 or
text.length). Two cheap regression tests added — an astral insert advancing the cursor by
2 UNITS, and a lone-surrogate deleteBeforeCursor with trailing content — to pin the
code-unit arithmetic before step 2 moves the cursor off the end.

The one MEDIUM finding is forward-looking (not a step-1 bug): the primitives TRUST a
valid, non-splitting, in-range cursor and would silently corrupt the buffer if fed a bad
one. Made that contract explicit in the EditorState JSDoc — step 2's motions must clamp
every movement to a code-point boundary within 0..text.length (owned + tested there).

Tests green (1193); prettier clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s, multiline render (2.5.D step 2)

Turn the chat prompt into a real readline editor on the step-1 EditorState:

- Cursor motions (all code-point-safe + range-clamped, closing the step-1 codepoint
  MEDIUM so the primitives can never be fed a mid-pair / out-of-range cursor): Left/Right,
  word-left/right (a modified arrow or Alt+B/F), line-start/line-end (Home/End or
  Ctrl+A/Ctrl+E, multiline-aware); kills Ctrl+W / Ctrl+U / Ctrl+K; Ctrl+J (a bare LF,
  which ink names 'enter' vs Return's 'return') / Shift+Enter (best-effort) insert a
  newline while Return submits.
- ONE keystroke contract: extract the shared reduceEditorMotion that BOTH reduceChatKey
  and reduceHomeKey delegate to, so the chat and bare-Home prompts get the identical
  editor (both are first-class line editors now) and can never drift.
- Multiline cursor render: a pure promptRows splitter + a shared PromptEditor component
  (used by ChatView and the Home view) that draws the cursor at its position across lines,
  each segment sanitized at the display boundary (no ANSI/OSC injection). The render
  boundary now threads the whole EditorState (text + cursor), not just the text.
- Hint bar surfaces "Ctrl+J newline" while composing.

New tests: motions/kills + code-point clamping (chat-input), the shared Home motions
(home-input), promptRows placement incl. multiline / astral / newline-boundary
(prompt-cursor). turbo lint typecheck test build + format:check + fence-check +
engine-deps + bundle-closure all green (1208 tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… incl. a security collision)

Opus adversarial review (5 dimensions → per-finding verification), 0 HIGH. Fixed:

- SECURITY (MEDIUM): Ctrl+A (newly a line-start motion) collided with the approval
  approve-always intercept — reduceApprovalKey matched char==='a' with no ctrl/meta
  guard, so a stray Ctrl+A during a pending approval silently selected the persistent,
  most-permissive approve-always, subverting the ADR-0057 fail-closed floor. Now only an
  UNMODIFIED letter answers (Ctrl+A/Ctrl+Y/Ctrl+N are swallowed); digit shortcuts stay
  unconditional.
- BUG (MEDIUM): lineStart used lastIndexOf('\n', i-1); at cursor 0, lastIndexOf(-1) clamps
  to 0 and false-matched a LEADING newline → Home/Ctrl+A jumped past the empty first line
  and Ctrl+U inverted its cut range, corrupting '\nabc' → '\n\nabc'. Guard i<=0.
- LOW: killRange now guards from>=to (not just ===) as defense-in-depth against an
  inverted range; the Home editor arm no longer bumps doctorRunId / clears a visible
  /doctor notice on a NO-OP boundary motion (reference-equality gate); a pasted CRLF/bare
  CR is normalized to LF in the appended text so no stray \r reaches the model/transcript.

New regression tests: the approval chord guard, line-start at cursor 0 on a leading-newline
buffer, CR normalization, controller-level Home motion/newline/kill dispatch, and the
no-op-motion-preserves-notice case. All gates green (1213 tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sonnet adversarial review (5 dimensions → per-finding verification), 0 HIGH. Fixed:

- SECURITY (MEDIUM): the approval-prompt DIGIT shortcuts (1/2/3) bypassed the opus-round
  `bare` guard, so Alt+1/2/3 (meta+digit, reachable via ink's ESC-prefixed parsing) still
  silently answered a pending approval — the same ADR-0057 fail-closed hole the letter fix
  closed, left open for digits. reduceApprovalKey now requires `bare` for ALL answers.
- BUG (MEDIUM): Ctrl+W (killRange word-back) was not line-scoped — wordLeft treats '\n' as
  ordinary whitespace, so Ctrl+W on/near a blank line wiped the whole previous line/paragraph
  in one keystroke. Clamp from = max(wordLeft, lineStart), matching the sibling kills.
- BUG (MEDIUM): the Home bracketed-paste path spliced raw content, bypassing the CRLF/CR→LF
  normalization the same PR added to the keystroke append — a pasted '\r' survived in the
  buffer + transcript. Normalize the paste payload too (both paths now consistent).
- LOW: word class now includes \p{M} so a base letter + combining diacritic is ONE word;
  the Home handleChatKey edit arm skips re-render on a no-op motion (reference-equality gate,
  mirroring handleHomeKey); PromptEditor documents its intentional reflow (a live editor must
  keep the cursor visible — truncating would hide it and break multiline).

New tests: Alt+digit approval guard, Ctrl+W line-scoping across newlines, combining-mark
word motion, and a promptRows+sanitize split-ESC injection test (no ANSI/OSC survives at any
cursor split). All gates green (1216 tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ep 3)

Add per-session command history + reverse-incremental search to the chat prompt, on
BOTH chat surfaces (standalone `relavium chat` + the in-Home chat):

- input-history.ts: a pure in-memory history model — recordHistory (dedup consecutive,
  skip empty), historyPrev/Next (save/restore the live draft, stop at the ends),
  resetHistoryNav (on a text edit); plus reverse-search over it (case-insensitive
  newest-first match, Ctrl+R steps older, foldReverseSearchKey submode contract). Not
  persisted across sessions (cross-session history is a tracked follow-up).
- Vertical Up/Down: moveCursor gains 'up'/'down' (column-preserving, code-point-snapped,
  a NO-OP at the top/bottom edge). reduceEditorMotion maps the arrows; the chat callers
  do a vertical move when possible and fall back to history recall at the edge, so
  multiline navigation and history coexist. The bare Home gets vertical motion only.
- Ctrl+R opens a keyboard-owning reverse-search submode (mirrors the / palette:
  mutually exclusive with it, yields to a pending approval), rendered by ReverseSearchView
  (sanitized). Enter loads the matched entry; Esc/Ctrl-C cancels.

New tests: the full history + reverse-search model, vertical-motion edge cases, and the
in-Home chat Up/Down + Ctrl+R end-to-end. All gates green (1227 tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…teraction bugs)

Opus adversarial review (5 dimensions → verification), 0 HIGH. Fixed:

- Reverse-search accept did not reset history navigation on EITHER surface, so if the user
  was mid Up-recall when they opened Ctrl+R, a subsequent Down called historyNext and
  silently clobbered the just-accepted entry with the stale pre-search draft. Both accept
  branches now resetHistoryNav (the accepted text becomes the live buffer).
- A bracketed PASTE while the in-Home reverse-search submode was open bypassed the search
  fold and leaked into the hidden input buffer — the paste `editable` gate checked
  `palette` but not `search`. Added `state.search === undefined` so paste is dropped while
  search owns the keyboard (mirroring the palette).
- A NO-OP edit keystroke (Ctrl+K at line end, backspace at column 0) while recalling history
  reset the nav (discarding the saved draft) BEFORE the no-op guard on both surfaces;
  reordered to reset only when the buffer actually changed.
- Refining a reverse-search query (typing after Ctrl+R-stepping to an older match) re-anchored
  to the newest match, yanking the selection forward; a new reverseSearchExtendQuery searches
  from the current match downward (extend narrows; backspace still widens from newest).

New tests: the query-extend anchor + the accept-then-Down no-clobber. All gates green (1229).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…on + 4)

Sonnet adversarial review (4 dimensions → verification). Fixed:

- HIGH regression (my own opus-round fix): the ChatApp edit case had been changed from a
  true functional setState updater to a constant `() => next` precomputed from
  editorRef.current, which is stale until the queued updater flushes — so a coalesced stdin
  chunk interleaving an edit with a move/history action silently DROPPED keystrokes (the
  reviewer empirically reproduced a-Left-b-Left-c-Left-d → "ad"). Restored true functional
  updaters for the edit, move, and reverse-search-accept cases; the no-op check + the
  idempotent resetHistoryNav now live INSIDE the updater so the fold is never broken again.
- MEDIUM: a multi-character paste while Ctrl+R search is open (the standalone chat has no
  bracketed-paste latch, so a paste is one multi-char event) corrupted the search query via
  the catch-all extend. foldReverseSearchKey now extends only on a SINGLE code point,
  dropping a blob (parity with the Home's paste gate).
- LOW (x3, ReverseSearchView first-class polish): add marginTop to match the palette's
  spacing; add a trailing "Ctrl+R older · Enter accept · Esc cancel" discoverability hint;
  render the query bold + the match dimmed with a ` → ` separator so neither is mistaken for
  the literal framing.

All gates green (1229 tests). (The HIGH is a React-wiring regression; the repo has no
ink-render test harness, so the fix restores the documented functional-updater pattern the
reducer-level burst tests already validate.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…/ .docker (2.5.D step 4a, ADR-0061)

The @-mention read (step 4) reuses read_file's isSensitiveReadPath confidentiality floor;
this expands that shared floor with the two most common in-repo secret stores it did not yet
cover — `.env` / `.env.*` dotenv files and an `.aws/` directory segment — plus
`.docker/config.json` (base64 registry auth). foldPathComponent already strips a trailing
dot, so `.env.` folds to `.env`; a lookalike like `.environment` (no `.env.` prefix) and a
plain non-`.docker` config.json are NOT floored. Because the floor lives in the shared Node
fs capability, this strengthens the model's own read_file on EVERY surface, not just the CLI
@-mention path (a workflow/agent read of a `.env` now fatally denies — the secure-by-default
direction; env vars still load via the process environment).

Canonical home reconciled: built-in-tools.md documents the read-floor member set. Leakwatch
clean; fs host suite green (90 tests).

Refs: ADR-0061, ADR-0055
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…4b-i, ADR-0061)

The pure, tested core of @-mention (the UI wiring lands next): a completion-submode state
+ foldMentionKey contract (↑/↓ select, Enter/Tab/'/' accept — a dir descends, a file
injects; backspace trims or drops the '@'; a multi-char paste blob is dropped like the
other submodes), and a MentionReader that is a THIN wrapper over the session's SAME
FsCapability — so the read/list go through that ONE audited boundary: the jail, the
sensitive-read confidentiality floor (the listing-gate skips .ssh/.env/.aws/… — never
even offered), the binary fail-close, and the 8 MiB cap are all enforced there, never a
raw node:fs read. Plus the injection formatter (untrusted, user-position <file> framing
with a tag-safe path) and the ~4-bytes/token size heuristic. Advisory noise-dir filtering
(node_modules/dist/.git/…); full .gitignore parsing is a tracked follow-up.

7 model + reader tests; lint/typecheck/build/bundle-closure green; Leakwatch clean.

Refs: ADR-0061
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tep 4b-ii)

Thread the read-only `MentionReader` (a thin wrapper over a `chat-read-only`
fs jail at the session's fs-scope tier + workspace) into both interactive
surfaces and add the dir-navigable `@`-completion submode:

- `ChatDriveContext.mentionReader` built in `runReplLoop` (TTY, non-`--json`
  only) via `assembleToolEnv({ profile: 'chat-read-only', … }).host.fs`; the
  Home builds the same in `startChat`. Read-only by construction — the mention
  path never writes; the listing-gate + confidentiality floor + binary/size
  guards are enforced by the one audited fs boundary.
- `mention.ts`: add a `loading` flag, the `mentionOpensAt` word-boundary open
  predicate, and a `restore` string on the close step so Esc/no-match restores
  the literal `@`+filter (nothing typed is eaten) while backspace-past-`@`
  deletes it.
- `chat-ink.tsx` (ChatApp) + `home-controller.ts` (in-Home chat): the async
  submode owns the keyboard (Esc/↑↓/Enter·Tab·'/'/backspace/filter), opens on
  `@` at a word boundary, lists through the fs jail (a stale resolve dropped by
  a dir guard), and on accept-file reads + injects `formatMentionInjection`
  framed, UNTRUSTED, user-position context at the cursor. A large file adds a
  soft token-warn note; a read rejection surfaces a static, secret-free note.
  Paste is dropped while the submode owns the keyboard (parity with palette /
  reverse-search). `@`/`/`/mention are all TTY-only; a plain/`--json` driver
  treats a leading `@` as a literal.
- `mention-view.tsx`: the completion overlay (windowed scroll around the
  selection, dirs marked `/`, loading vs no-match hints), every free-form field
  sanitized at the display boundary.

Tests: `@`-flow open→descend→accept→inject + Esc-restore + mid-word literal in
the Home controller; `mentionOpensAt` + the `restore`/`loading` model in
mention.test.ts; the pure `mentionWindow` scroll math. 1242 CLI tests green;
typecheck/lint/build/format/bundle-closure green; Leakwatch clean.

Refs: ADR-0061
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…breakout (2.5.D step 4)

A POSIX filename may contain a newline (or any C0/C1 control code), so a crafted
file could break out of the `<file path="…">` attribute or forge a second frame
when `@`-mentioned. Strip every control code (C0 incl. \n/\r/\t, DEL, C1) from
the path in addition to the framing chars (`<` `>` `"`), via a code-point filter
(no control-range regex — the lint floor). The content stays verbatim (it is
already flagged UNTRUSTED regardless of the framing).

Refs: ADR-0061
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ngs)

- HIGH→MED: hard-cap the injected file content (MENTION_MAX_INJECT_CHARS = 128
  KiB, head+tail+marker truncation) so a mentioned mid-size / multi-MB file no
  longer floods the live ink editor and freezes the TUI (the fs 8 MiB read cap
  was the only prior bound; the token-warn was soft-only).
- MED×2: `@` completion is now truly dir-navigable — a synthetic `..` ascend row
  at the top of every non-root listing + backspace-past-filter ascends one level
  below the root (parentDir, which only ever climbs toward '' — never escapes the
  jail). Previously a wrong descent forced a full cancel + re-@ from root.
- MED→LOW: the `<file>` injection frame is now unforgeable by file bytes — a
  per-injection random nonce fences BOTH tags (`<file id="NONCE" …>…</file:NONCE>`,
  mentionNonce()), so a file containing a literal `</file>`/`<file …>` can no
  longer close/forge the frame and smuggle instructions into the user turn.
- LOW: the read-side confidentiality floor now also refuses `.envrc` (direnv —
  routinely holds `export AWS_SECRET_ACCESS_KEY=…`) and legacy `.dockercfg`
  (fs.ts + built-in-tools.md reconciled).
- LOW: the Home `loadMentions` async resolve now carries acceptMention's
  session-identity guard (drop a cross-session / mode-changed stale listing).

Tests: parentDir, the `..` row, backspace-ascend, the nonce fence (open==close +
content-`</file>` cannot close), truncation head+tail+marker, `.envrc`/`.dockercfg`
floor. 1246 CLI tests green; typecheck/lint/build/format/bundle-closure green;
Leakwatch clean.

Refs: ADR-0061
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dings)

- MED: async accept-and-inject now carries a submit-generation guard (both
  surfaces) — a slow mention read that resolves AFTER Enter can no longer splice
  the file into the (cleared) buffer meant for the NEXT message. The prior guard
  caught only a chat SWAP (Home session-identity), not an in-chat submit.
- MED: Shift+Tab (the mode-cycle chord) is no longer swallowed by the open `@`
  overlay as a Tab accept — foldMentionKey accepts on PLAIN Tab only (a `shift`
  field added to MentionKey); Shift+Tab stays open (Esc then cycle).
- MED/LOW: boundInjectedContent no longer splits a UTF-16 surrogate pair at the
  truncation boundary (snapHead/snapTail) — no lone surrogate → no U+FFFD
  corruption in the model context / history.db transcript.
- HIGH→(bounded): the inject cap now bounds LINE count too (MENTION_MAX_INJECT_
  LINES = 400, head+tail+marker), not just bytes — a many-short-line file under
  the 128 KiB byte cap could still flood promptRows and freeze the TUI.
- MED: the read floor now refuses `.env` as a DIRECTORY segment (per-environment
  secret stores), not only a `.env` file (fs.ts + built-in-tools.md).
- LOW: formatMentionInjection strips Unicode bidi/format controls (U+202A–202E,
  U+2066–2069, LRM/RLM/ALM) from the injected path — a crafted name can no longer
  visually misrepresent itself at the review boundary. (Extending the shared
  display sanitizer is a deferred general hardening.)
- MED (doc): the `.gitignore`/`.relaviumignore` advisory completion trim is
  reconciled as a deferred follow-up (NOISE_DIRS is the v1 advisory trim, NOT a
  security control) — deferred-tasks.md + ADR-0061 consequences note.

Tests: Shift+Tab non-accept, surrogate-safe + line-cap truncation, the submit-
generation drop (deferred-read), `.env/` dir floor. 1248 CLI tests green;
typecheck/lint/build/format/fence/bundle-closure green; Leakwatch clean.

Refs: ADR-0061
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
….5.D step 5a, ADR-0061)

The additive engine method that routes a USER-invoked `!command` through the ONE
`run_command` boundary — reusing `#runTurn`'s dispatch-context construction
verbatim (factored into `#buildDispatchContext`): the session `toolPolicy`
(the `allowedCommands` allowlist, `enforcePolicy` BEFORE approval), the fsScope,
`gateApproved: false`, and the mode-aware `confirmAction` regime. So the `!`-shell
can never diverge from the audited command sandbox — no dispatch context is
re-assembled in `apps/cli`.

- The user command GRANTS `run_command` for its one-off dispatch (the user typed
  it — the grant is implicit); this never reaches the model (a direct dispatch,
  not a turn). The security gate stays the allowlist + confirmAction.
- The caller pre-tokenizes the line into `command` + `args` (no shell metachar
  expansion — `shell:false` spawn). The result is a classified discriminated
  union `UserCommandOutcome` (`ran` | `denied{allowlist}` | `failed` | `cancelled`)
  so the host renders each case and no raw error escapes; `denied.allowlist`
  distinguishes an allowlist miss (→ actionable opt-in hint) from an approval /
  mode deny. The engine stays pure (no platform import, no vendor type).
- Callable only when started + idle (a `!` never races a turn); leaves the
  session idle + reusable.

Config: `[chat].allowed_commands` + `allowed_command_globs` (→ engine camelCase
`allowedCommands` / `allowedCommandGlobs`), **empty/absent ⇒ `!`-shell disabled**
(the `empty ⇒ disabled` symmetry; no chat-specific relaxation).

Tests: allowlist miss (denied, no spawn), allowlisted run, glob run, approval
reject/approve, spawn-fault → failed (secret-free), lifecycle guard, reuse; the
config schema. Core + shared green (typecheck/lint/test/build/engine-deps).

Refs: ADR-0061
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, ADR-0061)

Thread the `[chat].allowed_commands` allowlist into the chat `toolPolicy` and
wire the `!`-shell input into both interactive surfaces:

- Config: resolve `[chat].allowed_commands` / `allowed_command_globs` (project
  REPLACES workspace, per field) into `ResolvedChatConfig`, and extend the chat
  `ToolPolicy` in `buildSessionRuntime` with them — the ONE allowlist a model
  `run_command` AND the user `!`-shell both enforce (empty ⇒ `!` disabled).
- `injection.ts` (new): the shared UNTRUSTED-context framing extracted from
  mention.ts — `injectionNonce`, `boundInjection` (surrogate-safe byte + line
  bounds), `sanitizeInjectionAttr` (control + bidi + framing strip), and
  `frameUntrusted` (`<tag id="NONCE" …>…</tag:NONCE>`). mention.ts now reuses it.
- `shell.ts` (new): the `!`-shell model — `isShellLine`, a quote-aware
  `tokenizeCommand` (NO shell expansion; metachars are literal argv under
  `shell:false`), `formatCommandInjection` (a nonce-fenced `<command cmd exit>`
  block), and the mode-aware, actionable `shellDenyHint`.
- ChatApp + Home: a submitted line starting with `!` (with a runner wired) is
  tokenized + run through `runUserCommand` instead of sent to the model; on
  `ran` the output injects as UNTRUSTED context into the cleared buffer (rides
  the next message), on `denied` it surfaces the allowlist / mode-switch hint,
  else a failure note. A submit-generation guard drops a stale resolve.
  `!` is TTY-only — a plain/`--json` driver treats a leading `!` as a literal.

Tests: the shell model (tokenizer, injection, deny hint), the config resolution,
and the Home wiring (run→inject, denied→hint→no-inject, bare `!`→message). Full
gate green (turbo lint/typecheck/test/build + format + fence + engine-deps +
bundle-closure); Leakwatch clean.

Refs: ADR-0061
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- HIGH: a `!`-shell command in flight left NO host-visible busy signal —
  `runUserCommand` makes the session busy (`#status:'running'`) but emits no
  session event, so the store's `state.status` stayed idle and the input gate
  never engaged. A plain message typed during a slow `!npm test` reached
  `sendMessage` → threw `SessionStateError` (session not idle) → nothing caught
  it → the whole chat/Home session CRASHED (losing the just-appended turn).
  Fix: a `shellBusy` flag on both surfaces (a ref-shadowed local in ChatApp; a
  `HomeControllerState` field in the Home) — set when a `!`-command starts,
  cleared in every settle branch (+ on chat end), OR'd into the `isRunning` gate
  that feeds `reduceChatKey` (so a mid-command keystroke can't reach
  `sendMessage`) and into the busy indicator + the paste gate. Ctrl-C (cancel)
  and Esc (abort) still precede the gate, so a stuck command stays interruptible.
- LOW: `UserCommandOutcome.ran` dropped the `truncated` flag — it was computed
  from the discarded model-facing `boundForModel` pass, describing data the
  caller never receives (the returned stdout/stderr are the process-arm–bounded
  identity `output`, and the host re-bounds + marks truncation on inject). A
  misleading flag on a security-bearing type; removed rather than half-right.

Tests: a regression that a message typed while a `!`-command is in flight is
gated (never sent, no onError crash) and the session recovers on resolve. Full
gate green; Leakwatch clean.

Refs: ADR-0061
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-0061)

Update every canonical-home doc the 2.5.D input layer touches:

- config-spec.md — `[chat].allowed_commands` / `allowed_command_globs` (the
  `!`-shell allowlist: exact match, EMPTY ⇒ disabled, project REPLACES workspace,
  the enforcePolicy-before-approval + no-chat-relaxation rationale).
- cli/chat-session.md — a new "Input ergonomics" section (Ctrl+J / history /
  Ctrl+R / readline motions) + the `@`-mention (fs-jail + floor reuse) and
  `!`-shell (allowlist → mode gate → shell:false; untrusted double-bounded output)
  contracts; TTY-only.
- cli/home.md — the 2.5.D keymap rows (Ctrl+J, ↑/↓, Ctrl+R, @, !) + the landed/
  forthcoming reconciliation.
- contracts/agent-session-spec.md — `runUserCommand` as a lifecycle operation
  (the documented engine exception, reusing #runTurn's dispatch context).
- standards/security-review.md — the chat input layer as a reuse-not-fork rule +
  a mandatory-review trigger (the @ read path, the ! boundary, the allowlist, the
  confidentiality floor).
- roadmap/phase-2.5 + current.md — 2.5.D implemented (PR pending merge); ADR-0061
  added to Required ADRs; EA8 (runUserCommand) in the engine-amendments appendix;
  the review-round outcomes recorded.

(built-in-tools.md, deferred-tasks.md, and ADR-0061 itself landed with the
step-4 commits.)

Refs: ADR-0061
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @cemililik, your pull request is larger than the review limit of 150000 diff characters

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds chat allowlist configuration and session policy wiring, a new user-command execution path, expanded filesystem read denial, shared untrusted framing helpers, cursor-based editor/history/reverse-search input handling, @ mention and ! shell TUI flows, chat/home wiring, and matching docs and tests.

Changes

Chat input ergonomics and shell/mention escape

Layer / File(s) Summary
Chat allowlist configuration
packages/shared/src/config.ts, apps/cli/src/config/resolve.ts, apps/cli/src/chat/session-host.ts, *.test.ts
allowed_commands and allowed_command_globs are added to chat config resolution and session tool policy, with fixtures and tests updated for the resolved chat shape.
User command execution path
packages/core/src/engine/agent-session.ts, packages/core/src/index.ts, packages/core/src/engine/agent-session.test.ts, docs/reference/contracts/agent-session-spec.md
AgentSession.runUserCommand() dispatches run_command and returns structured UserCommandOutcome results, with shared dispatch-context construction and spec/docs updates.
Sensitive read-path hardening
apps/cli/src/engine/tool-host/fs.ts, apps/cli/src/engine/tool-host/fs.test.ts, docs/reference/shared-core/built-in-tools.md
The CLI filesystem confidentiality floor expands to deny additional secret and credential locations, with tests and reference docs covering the new blocked paths.
Editor state, history, and prompt rendering
apps/cli/src/render/tui/prompt-cursor.ts, prompt-view.tsx, input-history.ts, reverse-search-view.tsx, chat-input.ts, home-input.ts, home-view.tsx, *.test.ts
The prompt model moves to EditorState with cursor motion/edit primitives, plus input history, reverse search, and prompt rendering updates.
@-mention and !-shell surfaces
apps/cli/src/render/tui/injection.ts, mention.ts, mention-view.tsx, shell.ts, attachments.ts, mention*.test.ts, shell.test.ts, attachments.test.ts, injection.test.ts
Shared untrusted framing is added with mention completion, filesystem reading, shell parsing, deny hints, pending attachments, and output framing for the two new interactive escapes.
Chat and Home TUI wiring
apps/cli/src/render/tui/chat-ink.tsx, home-app.tsx, home-controller.ts, home-controller.test.ts, apps/cli/src/commands/chat.ts, apps/cli/src/home/drive-home.tsx, apps/cli/src/home/drive-home.test.ts, apps/cli/src/chat/session-host.test.ts, apps/cli/src/chat/persister.test.ts, apps/cli/src/engine/media-wiring.test.ts
Chat and Home surfaces are wired to the new editor, overlays, mention reader, shell runner, and attachment model, with broad test coverage for the new flows.
Documentation, roadmap, and LLM updates
docs/decisions/0061-*.md, docs/roadmap/*, docs/reference/*, docs/standards/*, packages/llm/src/*
ADR-0061, roadmap status, security review rules, chat/session/config references, and DeepSeek/error-handling docs are updated.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ChatApp as ChatApp/HomeController
  participant MentionReader
  participant Session as AgentSession
  participant ShellRunner as runUserCommand
  User->>ChatApp: type @ or !
  ChatApp->>MentionReader: list/read candidates
  ChatApp->>ShellRunner: runShellCommand(command, args)
  ShellRunner->>Session: runUserCommand(command, args)
  Session-->>ChatApp: UserCommandOutcome
Loading
sequenceDiagram
  participant User
  participant AgentSession
  participant ToolRegistry
  participant ProcessHost
  User->>AgentSession: runUserCommand(command, args)
  AgentSession->>ToolRegistry: dispatch run_command
  ToolRegistry->>ProcessHost: spawn(command, args)
  ProcessHost-->>ToolRegistry: ProcessResult
  ToolRegistry-->>AgentSession: outcome or error
  AgentSession-->>User: UserCommandOutcome
Loading

Possibly related PRs

  • HodeTech/Relavium#54: The same CLI chat REPL/session wiring is extended here with allowlist-driven tool policy and interactive driver capabilities.
  • HodeTech/Relavium#61: This PR builds on the same Home TUI input stack by switching to EditorState and adding @/! overlays.
  • HodeTech/Relavium#63: The chat UX update here matches the recoverable-vs-fail-fast tool execution flow and tool_failed handling from that PR.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change set: chat input ergonomics plus the new @-mention and !-shell features for Phase 2.5.D.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch development

Comment @coderabbitai help to get the list of available commands.

Refs: ADR-0061
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements Phase 2.5.D of the CLI Consolidation, introducing advanced chat input ergonomics and secure input-layer features per ADR-0061. Key additions include a shared cursor-bearing EditorState supporting readline-style motions, multi-line prompt editing, command history with reverse-incremental search, @-mention file injection with directory-navigable completion, and !-shell command execution routed through the audited run_command boundary. The feedback recommends properly supporting standard Delete key behavior (deleting the character after the cursor) by separating it from the backspace action, providing a deleteAfterCursor helper, and updating the key mapping and action reducer.

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.

| { readonly kind: 'none' }
export type EditorEditAction =
| { readonly kind: 'append'; readonly char: string }
| { readonly kind: 'backspace' }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Add a new delete action to EditorEditAction to support standard delete-after-cursor behavior.

Suggested change
| { readonly kind: 'backspace' }
| { readonly kind: 'backspace' }
| { readonly kind: 'delete' }

if (key.ctrl === true && char === 'w') return { kind: 'kill', motion: 'word-back' };
if (key.ctrl === true && char === 'u') return { kind: 'kill', motion: 'to-line-start' };
if (key.ctrl === true && char === 'k') return { kind: 'kill', motion: 'to-line-end' };
if (key.backspace === true || key.delete === true) return { kind: 'backspace' };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The Delete key is currently mapped to the 'backspace' action, which deletes the character before the cursor. In standard text editors, the Delete key should delete the character after (under) the cursor. Map key.delete to a new 'delete' action instead.

  if (key.backspace === true) return { kind: 'backspace' };
  if (key.delete === true) return { kind: 'delete' };

const before = text.slice(0, cursor);
const trimmed = dropLastCodePoint(before);
return { text: trimmed + text.slice(cursor), cursor: cursor - (before.length - trimmed.length) };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Implement the deleteAfterCursor helper to support deleting the Unicode code point immediately after the cursor when the Delete key is pressed.

Suggested change
}
}
/**
* Delete the one Unicode code point immediately AFTER the cursor (one delete); a no-op at the end of the
* buffer.
*/
export function deleteAfterCursor(editor: EditorState): EditorState {
const { text, cursor } = editor;
if (cursor >= text.length) return editor;
const after = text.slice(cursor);
const code = after.codePointAt(0);
if (code === undefined) return editor;
const width = code > 0xffff ? 2 : 1;
return { text: text.slice(0, cursor) + after.slice(width), cursor };
}

Comment on lines 401 to +402
case 'backspace':
return dropLastCodePoint(buffer);
return deleteBeforeCursor(editor);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Handle the new 'delete' action in applyEditorAction by calling deleteAfterCursor.

Suggested change
case 'backspace':
return dropLastCodePoint(buffer);
return deleteBeforeCursor(editor);
case 'backspace':
return deleteBeforeCursor(editor);
case 'delete':
return deleteAfterCursor(editor);

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (8)
apps/cli/src/render/tui/mention-view.tsx (1)

51-73: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the nested ternary into a small statement/helper for readability.

♻️ Proposed refactor sketch
-      {state.loading ? (
-        <Text {...dimProps(color)} wrap="truncate-end">
-          loading…
-        </Text>
-      ) : visible.length === 0 ? (
-        <Text {...dimProps(color)} wrap="truncate-end">
-          no matching file
-        </Text>
-      ) : (
-        windowed.map((candidate, index) => { ... })
-      )}
+      {renderMentionBody(state.loading, visible.length, windowed, start, selected, color)}

Where renderMentionBody centralizes the three branches.

🤖 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/mention-view.tsx` around lines 51 - 73, The nested
ternary in renderMentionBody within mention-view.tsx should be refactored for
readability. Replace the inline loading/empty/results conditional with a small
statement or helper that handles the three branches separately, keeping the
existing behavior for state.loading, visible.length === 0, and
windowed.map(candidate). Make the branching logic explicit near
renderMentionBody so the JSX is easier to scan and maintain.

Source: Linters/SAST tools

apps/cli/src/render/tui/mention.ts (2)

106-148: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reduce foldMentionKey cognitive complexity (17 → flagged over the 15 limit).

The function handles five independent concerns (cancel, arrow movement, accept, backspace/ascend, printable insert) in one branchy body. Extracting each branch into a small named helper (e.g. foldMentionArrow, foldMentionAccept, foldMentionBackspace) would restore readability without changing behavior — the existing test suite in mention.test.ts should catch any regressions.

🤖 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/mention.ts` around lines 106 - 148, `foldMentionKey`
is doing too many independent branches in one function, pushing cognitive
complexity over the limit. Split the cancel, arrow navigation, accept,
backspace/ascend, and printable-character handling into small helper functions
such as `foldMentionArrow`, `foldMentionAccept`, and `foldMentionBackspace`,
then have `foldMentionKey` delegate to them while preserving the same return
shapes (`close`, `state`, `descend`, `accept`). Keep the behavior unchanged and
rely on the existing `mention.test.ts` coverage to verify the refactor.

Source: Linters/SAST tools


3-3: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use export … from for pure re-exports.

MENTION_MAX_INJECT_CHARS, MENTION_MAX_INJECT_LINES, and mentionNonce are plain renamed re-exports of injection.ts bindings; SonarCloud correctly flags all three (Lines 237, 238, 241). Using export … from avoids the intermediate import bindings.

♻️ Proposed refactor
-import { frameUntrusted, injectionNonce, INJECT_MAX_CHARS, INJECT_MAX_LINES } from './injection.js';
+import { frameUntrusted } from './injection.js';
+export {
+  injectionNonce as mentionNonce,
+  INJECT_MAX_CHARS as MENTION_MAX_INJECT_CHARS,
+  INJECT_MAX_LINES as MENTION_MAX_INJECT_LINES,
+} from './injection.js';
-/** The `@`-injection content bounds — the shared injection caps ({`@link` injection.ts}). Re-exported under the
- *  historical names the tests + callers reference. */
-export const MENTION_MAX_INJECT_CHARS = INJECT_MAX_CHARS;
-export const MENTION_MAX_INJECT_LINES = INJECT_MAX_LINES;
-
-/** A fresh per-injection fence nonce ({`@link` injectionNonce}) — re-exported under the `@`-scoped name. */
-export const mentionNonce = injectionNonce;

Also applies to: 237-241

🤖 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/mention.ts` at line 3, Replace the intermediate
import-and-rename re-exports in mention.ts with direct `export … from`
statements for the pure passthrough bindings coming from `injection.js`;
specifically, keep `frameUntrusted` as needed locally, but change
`MENTION_MAX_INJECT_CHARS`, `MENTION_MAX_INJECT_LINES`, and `mentionNonce` so
they are exported directly from `injection.js` instead of being imported and
re-exported via local bindings. Use the existing symbols `frameUntrusted`,
`injectionNonce`, `INJECT_MAX_CHARS`, and `INJECT_MAX_LINES` to locate the
refactor.

Source: Linters/SAST tools

apps/cli/src/render/tui/shell.ts (1)

32-64: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

tokenizeCommand trips the SonarCloud cognitive-complexity gate (16 > 15, reported as a check failure).

Barely over the threshold. Extracting the in-quote branch and the whitespace-token-flush into small helpers (or a tiny state step function) brings it under the limit without changing behavior.

🤖 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/shell.ts` around lines 32 - 64, tokenizeCommand
currently exceeds the cognitive-complexity limit because the parsing loop in
ShellCommand handling combines quote handling and token flushing in one block.
Refactor the logic in tokenizeCommand by extracting the in-quote branch and the
whitespace-delimited token flush into small helper functions or a tiny
state-step helper, keeping the same behavior for quotes, whitespace, and empty
quoted tokens while lowering complexity below the SonarCloud threshold.

Source: Linters/SAST tools

apps/cli/src/render/tui/chat-input.ts (1)

106-135: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

reduceEditorMotion trips the SonarCloud cognitive-complexity gate (30 > 15, reported as a check failure).

The long flat modifier if-chain is correct but exceeds the configured threshold. Consider splitting the motion mapping (arrows/word/line/vertical) and the kill mapping into small helpers so the top-level function stays under the limit.

🤖 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-input.ts` around lines 106 - 135,
reduceEditorMotion is failing the cognitive-complexity limit because the current
flat modifier chain is too large. Split the logic in reduceEditorMotion into
small helpers for each concern, such as enter/newline handling, cursor motion
mapping, kill actions, and text append normalization, then have the top-level
function delegate to them. Keep the behavior unchanged while reducing the
branching density in reduceEditorMotion and any new helper names you introduce.

Source: Linters/SAST tools

apps/cli/src/render/tui/input-history.ts (2)

21-26: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor: prefer .at(-1) over length-index access.

Static analysis flags this pattern; .at(-1) is more concise for the "last entry" lookup.

♻️ Proposed simplification
 export function recordHistory(history: InputHistory, line: string): InputHistory {
   if (line.length === 0) return resetHistoryNav(history);
-  const last = history.entries[history.entries.length - 1];
+  const last = history.entries.at(-1);
   const entries = last === line ? history.entries : [...history.entries, line];
   return { entries, navIndex: null, draft: '' };
 }
🤖 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/input-history.ts` around lines 21 - 26, The
last-entry lookup in recordHistory should use .at(-1) instead of indexing with
history.entries.length - 1. Update the recordHistory function in
input-history.ts to read the last entry via .at(-1) while preserving the
existing duplicate-check and resetHistoryNav behavior.

Source: Linters/SAST tools


82-89: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor: optional-chain simplification available.

entry !== undefined && entry.toLowerCase().includes(needle) can be an optional chain — flagged by static analysis.

♻️ Proposed simplification
   for (let i = Math.min(fromIndex, entries.length - 1); i >= 0; i--) {
     const entry = entries[i];
-    if (entry !== undefined && entry.toLowerCase().includes(needle)) return i;
+    if (entry?.toLowerCase().includes(needle) === true) return i;
   }
🤖 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/input-history.ts` around lines 82 - 89, The match
check in findMatch can be simplified by using optional chaining instead of an
explicit undefined guard. Update the condition inside findMatch in
input-history.ts so the entry lookup and lowercase include check use a concise
optional-chain form, while keeping the same behavior for undefined entries and
preserving the existing loop logic and return value.

Source: Linters/SAST tools

apps/cli/src/render/tui/home-controller.ts (1)

526-603: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

handleChatKey fails the SonarCloud quality gate (cognitive complexity 46 > 15).

The overlay routing (mention branch 534-558, reverse-search branch 562-575) and the open-conditions (579-602) are each self-contained and can be extracted into small pure helpers/handlers (e.g. routeMentionKey, routeSearchKey, tryOpenOverlay) that early-return a handled flag. This lowers the nesting/branch count without behavior change and clears the gate.

🤖 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 526 - 603,
`handleChatKey` is too complex and is failing the SonarCloud cognitive
complexity gate, so split the overlay routing and open-condition logic into
small helpers. Extract the mention handling, reverse-search handling, and
overlay-open checks from `handleChatKey` into focused functions such as
`routeMentionKey`, `routeSearchKey`, and `tryOpenOverlay`, each returning
whether the key was handled. Keep the existing behavior and early-return flow
intact while flattening the nesting in `handleChatKey` so the main method only
delegates to these helpers and then falls through to `reduceChatKey`.

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/render/tui/home-controller.ts`:
- Around line 467-500: The `UserCommandOutcome` import is resolving to `any`, so
the discriminated union in `handleShellOutcome` and other `runUserCommand` call
sites loses type safety. Fix the `@relavium/core` type resolution so the import
resolves to the actual declaration instead of an unresolved package type, and
verify that `UserCommandOutcome`, `runUserCommand`, and related outcome handling
regain proper narrowing on `kind` and payload fields.

In `@packages/core/src/engine/agent-session.test.ts`:
- Around line 1115-1125: The `leaves the session idle + reusable after a
command` test in `agent-session.test.ts` only verifies that `sendMessage` does
not throw, so make the expectation explicit by asserting the session remains
reusable after `runUserCommand('ls', [])` and `sendMessage('and now a
message')`. Use the existing `startedSession`, `runUserCommand`, and
`sendMessage` flow to check the resulting session state or a concrete
postcondition so the test fails loudly if the session is not left idle.

---

Nitpick comments:
In `@apps/cli/src/render/tui/chat-input.ts`:
- Around line 106-135: reduceEditorMotion is failing the cognitive-complexity
limit because the current flat modifier chain is too large. Split the logic in
reduceEditorMotion into small helpers for each concern, such as enter/newline
handling, cursor motion mapping, kill actions, and text append normalization,
then have the top-level function delegate to them. Keep the behavior unchanged
while reducing the branching density in reduceEditorMotion and any new helper
names you introduce.

In `@apps/cli/src/render/tui/home-controller.ts`:
- Around line 526-603: `handleChatKey` is too complex and is failing the
SonarCloud cognitive complexity gate, so split the overlay routing and
open-condition logic into small helpers. Extract the mention handling,
reverse-search handling, and overlay-open checks from `handleChatKey` into
focused functions such as `routeMentionKey`, `routeSearchKey`, and
`tryOpenOverlay`, each returning whether the key was handled. Keep the existing
behavior and early-return flow intact while flattening the nesting in
`handleChatKey` so the main method only delegates to these helpers and then
falls through to `reduceChatKey`.

In `@apps/cli/src/render/tui/input-history.ts`:
- Around line 21-26: The last-entry lookup in recordHistory should use .at(-1)
instead of indexing with history.entries.length - 1. Update the recordHistory
function in input-history.ts to read the last entry via .at(-1) while preserving
the existing duplicate-check and resetHistoryNav behavior.
- Around line 82-89: The match check in findMatch can be simplified by using
optional chaining instead of an explicit undefined guard. Update the condition
inside findMatch in input-history.ts so the entry lookup and lowercase include
check use a concise optional-chain form, while keeping the same behavior for
undefined entries and preserving the existing loop logic and return value.

In `@apps/cli/src/render/tui/mention-view.tsx`:
- Around line 51-73: The nested ternary in renderMentionBody within
mention-view.tsx should be refactored for readability. Replace the inline
loading/empty/results conditional with a small statement or helper that handles
the three branches separately, keeping the existing behavior for state.loading,
visible.length === 0, and windowed.map(candidate). Make the branching logic
explicit near renderMentionBody so the JSX is easier to scan and maintain.

In `@apps/cli/src/render/tui/mention.ts`:
- Around line 106-148: `foldMentionKey` is doing too many independent branches
in one function, pushing cognitive complexity over the limit. Split the cancel,
arrow navigation, accept, backspace/ascend, and printable-character handling
into small helper functions such as `foldMentionArrow`, `foldMentionAccept`, and
`foldMentionBackspace`, then have `foldMentionKey` delegate to them while
preserving the same return shapes (`close`, `state`, `descend`, `accept`). Keep
the behavior unchanged and rely on the existing `mention.test.ts` coverage to
verify the refactor.
- Line 3: Replace the intermediate import-and-rename re-exports in mention.ts
with direct `export … from` statements for the pure passthrough bindings coming
from `injection.js`; specifically, keep `frameUntrusted` as needed locally, but
change `MENTION_MAX_INJECT_CHARS`, `MENTION_MAX_INJECT_LINES`, and
`mentionNonce` so they are exported directly from `injection.js` instead of
being imported and re-exported via local bindings. Use the existing symbols
`frameUntrusted`, `injectionNonce`, `INJECT_MAX_CHARS`, and `INJECT_MAX_LINES`
to locate the refactor.

In `@apps/cli/src/render/tui/shell.ts`:
- Around line 32-64: tokenizeCommand currently exceeds the cognitive-complexity
limit because the parsing loop in ShellCommand handling combines quote handling
and token flushing in one block. Refactor the logic in tokenizeCommand by
extracting the in-quote branch and the whitespace-delimited token flush into
small helper functions or a tiny state-step helper, keeping the same behavior
for quotes, whitespace, and empty quoted tokens while lowering complexity below
the SonarCloud threshold.
🪄 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: 1dff4b0b-e276-432f-9ec1-9bc2022d4b7e

📥 Commits

Reviewing files that changed from the base of the PR and between e0f278d and 984f98c.

📒 Files selected for processing (51)
  • CLAUDE.md
  • apps/cli/src/chat/persister.test.ts
  • apps/cli/src/chat/session-host.test.ts
  • apps/cli/src/chat/session-host.ts
  • apps/cli/src/commands/chat.test.ts
  • apps/cli/src/commands/chat.ts
  • apps/cli/src/config/resolve.test.ts
  • apps/cli/src/config/resolve.ts
  • apps/cli/src/engine/media-wiring.test.ts
  • apps/cli/src/engine/tool-host/fs.test.ts
  • apps/cli/src/engine/tool-host/fs.ts
  • apps/cli/src/home/drive-home.tsx
  • apps/cli/src/render/tui/chat-ink.tsx
  • apps/cli/src/render/tui/chat-input.test.ts
  • apps/cli/src/render/tui/chat-input.ts
  • apps/cli/src/render/tui/home-app.tsx
  • apps/cli/src/render/tui/home-controller.test.ts
  • apps/cli/src/render/tui/home-controller.ts
  • apps/cli/src/render/tui/home-input.test.ts
  • apps/cli/src/render/tui/home-input.ts
  • apps/cli/src/render/tui/home-view.tsx
  • apps/cli/src/render/tui/injection.ts
  • apps/cli/src/render/tui/input-history.test.ts
  • apps/cli/src/render/tui/input-history.ts
  • apps/cli/src/render/tui/mention-view.test.ts
  • apps/cli/src/render/tui/mention-view.tsx
  • apps/cli/src/render/tui/mention.test.ts
  • apps/cli/src/render/tui/mention.ts
  • apps/cli/src/render/tui/prompt-cursor.test.ts
  • apps/cli/src/render/tui/prompt-cursor.ts
  • apps/cli/src/render/tui/prompt-view.tsx
  • apps/cli/src/render/tui/reverse-search-view.tsx
  • apps/cli/src/render/tui/shell.test.ts
  • apps/cli/src/render/tui/shell.ts
  • docs/decisions/0061-cli-input-layer-file-injection-and-shell-escape.md
  • docs/decisions/README.md
  • docs/reference/cli/chat-session.md
  • docs/reference/cli/home.md
  • docs/reference/contracts/agent-session-spec.md
  • docs/reference/contracts/config-spec.md
  • docs/reference/shared-core/built-in-tools.md
  • docs/reference/shared-core/tool-registry.md
  • docs/roadmap/current.md
  • docs/roadmap/deferred-tasks.md
  • docs/roadmap/phases/phase-2.5-cli-consolidation.md
  • docs/standards/security-review.md
  • packages/core/src/engine/agent-session.test.ts
  • packages/core/src/engine/agent-session.ts
  • packages/core/src/index.ts
  • packages/shared/src/config.test.ts
  • packages/shared/src/config.ts

Comment thread apps/cli/src/render/tui/home-controller.ts
Comment thread packages/core/src/engine/agent-session.test.ts
…lver

The bare `relavium` Home built an ENV-ONLY provider key resolver
(`createProviderResolver(io.env)`), so a key stored in the OS keychain via
`relavium provider add` (the normal path) was invisible — the first Home-chat
turn failed `provider_auth`, even though `relavium chat` worked (it IS
keychain-wired, via dispatch.ts's `keyResolvers`).

The Home is opened from `run.ts` (the bare-invocation path), NOT through
`dispatch.ts`, so it never received the keychain-backed resolver injection.
Fix: default the Home's `providers` + `mcpSecretResolver` to keychain-backed
(one shared `createOsKeychainStore()`), mirroring `dispatch.ts`'s `keyResolvers`
and matching the Home's existing self-wired production defaults (e.g.
`doctorProbes`). `createOsKeychainStore()` reads nothing until a key is
resolved, so it is side-effect-free; a test injects `providers` and never
reaches the keychain.

Regression test: with `providers` NOT injected, the resolver MUST receive the
keychain (a two-module mock asserts the wiring end-to-end) — this fails against
the old env-only code. Full gate green; Leakwatch clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
apps/cli/src/home/drive-home.test.ts (1)

35-38: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid the unsafe keychain as never cast.

Type the wrapper's keychain param as the resolver's real keychain type instead of unknown + as never, so the forwarding call is type-checked.

As per coding guidelines: "no any, no unsafe as, and prefer type guards."

♻️ Suggested typing
-    createProviderResolver: (env: Record<string, string | undefined>, keychain?: unknown) => {
+    createProviderResolver: (
+      env: Record<string, string | undefined>,
+      keychain?: Parameters<typeof actual.createProviderResolver>[1],
+    ) => {
       resolverKeychainArg.value = keychain;
-      return actual.createProviderResolver(env, keychain as never);
+      return actual.createProviderResolver(env, keychain);
     },
🤖 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/home/drive-home.test.ts` around lines 35 - 38, The wrapper in
createProviderResolver is using an unsafe keychain as never cast, which bypasses
type checking. Update the wrapper’s keychain parameter to the resolver’s
वास्तविक keychain type used by actual.createProviderResolver, then forward it
directly without casting so the call stays type-safe. Keep resolverKeychainArg
capture intact, and use the createProviderResolver symbol to align the test
helper signature with the real API.

Source: Coding guidelines

🤖 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.

Nitpick comments:
In `@apps/cli/src/home/drive-home.test.ts`:
- Around line 35-38: The wrapper in createProviderResolver is using an unsafe
keychain as never cast, which bypasses type checking. Update the wrapper’s
keychain parameter to the resolver’s वास्तविक keychain type used by
actual.createProviderResolver, then forward it directly without casting so the
call stays type-safe. Keep resolverKeychainArg capture intact, and use the
createProviderResolver symbol to align the test helper signature with the real
API.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b91e0f1a-733a-4494-ad52-67c24d763277

📥 Commits

Reviewing files that changed from the base of the PR and between 984f98c and 83d8d8a.

📒 Files selected for processing (2)
  • apps/cli/src/home/drive-home.test.ts
  • apps/cli/src/home/drive-home.tsx

cemililik and others added 3 commits July 3, 2026 13:57
…rror messages in chat

A DeepSeek "402 Insufficient Balance" (and any other unmapped provider HTTP
status) surfaced in `relavium chat` as a bare `error: internal` — reading as an
engine bug rather than the provider/account problem it is. Two-layer root cause:
`kindFromHttpStatus(402)` fell through to `unknown`, and `codeForLlmError`
maps `unknown` → `internal`.

Classification (all three providers, one point): map 402 → `auth` in the shared
`kindFromHttpStatus`. 402 Payment Required is a fatal account/billing problem
(insufficient balance, exhausted quota); the `auth` bucket → `provider_auth` and
is non-retryable (retrying the same provider cannot restore balance). Because
the Anthropic, OpenAI/DeepSeek and Gemini adapters all route HTTP status through
this one helper, the fix covers every provider.

Chat surfacing: the interactive footer previously dropped the message for every
code except the two approval-floor labels, so even after reclassification it
would show only `error: provider_auth`. Add a separately-justified
`PROVIDER_MESSAGE_CODES` set (`provider_auth` / `provider_rate_limit` /
`provider_unavailable` / `content_filter`) whose message is a provider-authored
status line — already secret-scrubbed at the `makeLlmError` seam choke point and
terminal-sanitized at render — so the footer now reads
`error: provider_auth — 402 Insufficient Balance`. `tool_failed` stays excluded
(its message may carry model/MCP context). The `run`/`--json` path already
rendered the message; this only closes the interactive-chat gap.

Docs: error-handling.md groups 402 with 401/403 under fatal.
Tests: kindFromHttpStatus(402) → auth (non-retryable); the chat footer renders
the provider status message for the provider_* / content_filter codes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… catalog

DeepSeek moved to versioned model ids. Add the two current canonical rows to
MODEL_PRICING, verified 2026-07-03 against api-docs.deepseek.com/quick_start/pricing:

  deepseek-v4-flash (default tier): in $0.14 / out $0.28 / cache-hit $0.0028 per MTok
  deepseek-v4-pro   (premium tier): in $0.435 / out $0.87 / cache-hit $0.003625 per MTok

Both carry a 1M-token context / 384K max output and serve non-thinking AND thinking
(default) modes on ONE id — the mode is a request param, not a separate model, so there
is no per-mode row. Without these rows an agent naming `deepseek-v4-flash` egressed fine
(provider inferred from the id prefix) but fell out of the pricing table, so its cost
tracked as $0 (UnknownModelError, best-effort zero); it now prices correctly.

The legacy `deepseek-chat` / `deepseek-reasoner` aliases are kept (relabeled "(legacy)",
prices unchanged — the pricing page no longer lists them, so they hold the last verified
2026-06-11 values) until they deprecate 2026-07-24 15:59 UTC, so existing agents/configs
and the conformance fixture keep working until then.

Also: repoint the DeepSeek `provider test` / `/doctor --deep` key-probe testModel from the
deprecating `deepseek-chat` to `deepseek-v4-flash`; refresh the DEEPSEEK_SUPPORTS comment
(v4-flash / -pro both expose thinking — per-provider capability flags unchanged). Tests
assert the two new rows' per-MTok prices; the MODEL_PRICING invariant test auto-covers
field completeness for the added keys.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, nits, Delete key)

Verified each finding against current code; fixed the valid ones, skipped the
rest (below) with reasons. Behavior-preserving except the Delete-key change.

Complexity (SonarCloud Critical, split into helpers — behavior identical, pinned
by existing tests):
- reduceEditorMotion → reduceCursorMotion + reduceKill (chat-input.ts)
- handleChatKey → routeMentionKey + routeSearchKey + tryOpenOverlay +
  applyChatAction (home-controller.ts)
- foldMentionKey → foldMentionArrow + foldMentionAccept + foldMentionBackspace
  (mention.ts)
- tokenizeCommand → stepTokenize + flushToken (shell.ts)

Feature (reviewer request): the Delete key now deletes the code point AFTER the
cursor (standard editor behavior), distinct from Backspace — a new `delete`
EditorEditAction + `deleteAfterCursor` primitive, mapped in reduceEditorMotion
and handled in both surfaces (tests updated).

Nits:
- input-history: `.at(-1)` for the last entry; optional chain in findMatch.
- injection / chat-input: `codePointAt(…) > 0xffff` for surrogate-pair detection
  (matches the file idiom; charCodeAt→codePointAt); `replaceAll` for the nonce.
- mention.ts: re-export the injection bounds/nonce via `export … from`.
- mention-view: extract the loading/empty/rows nested ternary into a render fn.
- home-controller / chat-ink: flip the `parsed !== undefined` submit if/else to a
  positive condition; guard-return in loadMentions' applyIfCurrent (drops the
  `x !== undefined && x.y` optional-chain smell while keeping the narrowing).
- agent-session: extract the `#userCommandSeq += 1` assignment from the id
  template; agent-session.test: assert the reusable-session postcondition loudly.
- drive-home.test: type the provider-mock wrapper to the real API (no `as never`).

Skipped: (1) the "UserCommandOutcome resolves to any" inline comment — a false
positive: the built `.d.ts` carries the full discriminated union and strict
typecheck (no-any) passes; the reviewer's IDE resolved a stale/unbuilt core.
(2) prompt-view array-index key — the prompt echo is a fully-recomputed,
non-reorderable positional line list with no per-line id, so the index is the
correct React key.

Full CI green (turbo lint/typecheck/test/build + format + fence + engine-deps +
bundle-closure); 1264 CLI tests; Leakwatch clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cemililik

Copy link
Copy Markdown
Contributor Author

@coderabbitai review all please

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

@cemililik: Sure, I'll do a full review of all the changes in this PR.

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/llm/src/pricing.ts (1)

179-201: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

deepseek-reasoner should use Flash rates, not Pro rates. packages/llm/src/pricing.ts:192-201
deepseek-reasoner maps to deepseek-v4-flash thinking mode, so this entry should match deepseek-chat’s Flash pricing (usd(0.14) / usd(0.28) / usd(0.0028)) instead of the current Pro rates.

🤖 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/llm/src/pricing.ts` around lines 179 - 201, The legacy pricing entry
for deepseek-reasoner is using the wrong rate tier in pricing.ts; update the
deepseek-reasoner object so it matches the Flash pricing used by deepseek-chat.
Adjust its inputPerMtokMicrocents, outputPerMtokMicrocents, and
cachedInputPerMtokMicrocents to the Flash values, keeping the existing
deepseek-reasoner symbol and surrounding legacy alias block intact.
🧹 Nitpick comments (2)
apps/cli/src/render/tui/chat-input.ts (1)

99-116: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Trim reduceCursorMotion cognitive complexity to clear the SonarCloud gate.

SonarCloud reports complexity 16 (limit 15) here, which fails the quality gate. Extracting the line-boundary detection (Home/End/Ctrl+A/Ctrl+E) into a small helper drops it below the threshold without behavior change.

♻️ Suggested extraction
+function reduceLineMotion(char: string, key: ChatKey): EditorEditAction | undefined {
+  if (key.home === true || (key.ctrl === true && char === 'a'))
+    return { kind: 'move', motion: 'line-start' };
+  if (key.end === true || (key.ctrl === true && char === 'e'))
+    return { kind: 'move', motion: 'line-end' };
+  return undefined;
+}
+
 function reduceCursorMotion(char: string, key: ChatKey): EditorEditAction | undefined {
   const wordMod = key.ctrl === true || key.meta === true;
   if (key.leftArrow === true) return { kind: 'move', motion: wordMod ? 'word-left' : 'left' };
   if (key.rightArrow === true) return { kind: 'move', motion: wordMod ? 'word-right' : 'right' };
   if (key.upArrow === true) return { kind: 'move', motion: 'up' };
   if (key.downArrow === true) return { kind: 'move', motion: 'down' };
-  if (key.home === true || (key.ctrl === true && char === 'a'))
-    return { kind: 'move', motion: 'line-start' };
-  if (key.end === true || (key.ctrl === true && char === 'e'))
-    return { kind: 'move', motion: 'line-end' };
+  const line = reduceLineMotion(char, key);
+  if (line !== undefined) return line;
   if (key.meta === true && char === 'b') return { kind: 'move', motion: 'word-left' };
   if (key.meta === true && char === 'f') return { kind: 'move', motion: 'word-right' };
   return undefined;
 }
🤖 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-input.ts` around lines 99 - 116, Reduce the
cognitive complexity of reduceCursorMotion by extracting the line-boundary key
handling (Home/End and Ctrl+A/Ctrl+E) into a small helper, then have
reduceCursorMotion delegate to it while preserving the existing motion behavior
for arrows, word motions, and no-op cases.

Source: Linters/SAST tools

apps/cli/src/render/tui/home-controller.ts (1)

600-675: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Split applyChatAction to clear the SonarCloud complexity gate.

SonarCloud reports cognitive complexity 23 (limit 15) here, failing the quality gate. The edit/backspace/delete/newline/kill, move, and submit arms are each self-contained — extracting them into small helpers (e.g. applyEditAction, applyMoveAction, applySubmitAction) leaves the switch as a thin dispatcher and drops complexity below the threshold without behavior change.

🤖 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 600 - 675, Split
applyChatAction into smaller helpers to reduce cognitive complexity below the
SonarCloud threshold. Extract the self-contained edit branch, move branch, and
submit branch from applyChatAction into dedicated helpers such as
applyEditAction, applyMoveAction, and applySubmitAction, then have the switch in
applyChatAction delegate to them while preserving the current
state/history/cancel 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.

Outside diff comments:
In `@packages/llm/src/pricing.ts`:
- Around line 179-201: The legacy pricing entry for deepseek-reasoner is using
the wrong rate tier in pricing.ts; update the deepseek-reasoner object so it
matches the Flash pricing used by deepseek-chat. Adjust its
inputPerMtokMicrocents, outputPerMtokMicrocents, and
cachedInputPerMtokMicrocents to the Flash values, keeping the existing
deepseek-reasoner symbol and surrounding legacy alias block intact.

---

Nitpick comments:
In `@apps/cli/src/render/tui/chat-input.ts`:
- Around line 99-116: Reduce the cognitive complexity of reduceCursorMotion by
extracting the line-boundary key handling (Home/End and Ctrl+A/Ctrl+E) into a
small helper, then have reduceCursorMotion delegate to it while preserving the
existing motion behavior for arrows, word motions, and no-op cases.

In `@apps/cli/src/render/tui/home-controller.ts`:
- Around line 600-675: Split applyChatAction into smaller helpers to reduce
cognitive complexity below the SonarCloud threshold. Extract the self-contained
edit branch, move branch, and submit branch from applyChatAction into dedicated
helpers such as applyEditAction, applyMoveAction, and applySubmitAction, then
have the switch in applyChatAction delegate to them while preserving the current
state/history/cancel behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 239f5c6f-720c-49c2-a9e5-ebcc6675196d

📥 Commits

Reviewing files that changed from the base of the PR and between 83d8d8a and d9e54d9.

📒 Files selected for processing (22)
  • apps/cli/src/engine/providers.ts
  • apps/cli/src/home/drive-home.test.ts
  • apps/cli/src/render/tui/chat-ink.tsx
  • apps/cli/src/render/tui/chat-input.test.ts
  • apps/cli/src/render/tui/chat-input.ts
  • apps/cli/src/render/tui/chat-projection.test.ts
  • apps/cli/src/render/tui/chat-projection.ts
  • apps/cli/src/render/tui/home-controller.ts
  • apps/cli/src/render/tui/home-input.test.ts
  • apps/cli/src/render/tui/injection.ts
  • apps/cli/src/render/tui/input-history.ts
  • apps/cli/src/render/tui/mention-view.tsx
  • apps/cli/src/render/tui/mention.ts
  • apps/cli/src/render/tui/shell.ts
  • docs/standards/error-handling.md
  • packages/core/src/engine/agent-session.test.ts
  • packages/core/src/engine/agent-session.ts
  • packages/llm/src/adapters/openai.ts
  • packages/llm/src/cost-tracker.test.ts
  • packages/llm/src/llm-error.test.ts
  • packages/llm/src/llm-error.ts
  • packages/llm/src/pricing.ts
✅ Files skipped from review due to trivial changes (2)
  • apps/cli/src/engine/providers.ts
  • packages/llm/src/adapters/openai.ts
🚧 Files skipped from review as they are similar to previous changes (11)
  • apps/cli/src/render/tui/injection.ts
  • apps/cli/src/render/tui/home-input.test.ts
  • apps/cli/src/render/tui/shell.ts
  • packages/core/src/engine/agent-session.test.ts
  • apps/cli/src/render/tui/mention-view.tsx
  • apps/cli/src/home/drive-home.test.ts
  • apps/cli/src/render/tui/input-history.ts
  • packages/core/src/engine/agent-session.ts
  • apps/cli/src/render/tui/mention.ts
  • apps/cli/src/render/tui/chat-ink.tsx
  • apps/cli/src/render/tui/chat-input.test.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
packages/llm/src/pricing.ts (1)

192-201: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

deepseek-reasoner should use DeepSeek-V4-Flash pricing packages/llm/src/pricing.ts:192-201 — this legacy ID routes to DeepSeek-V4-Flash, so the row should use flash rates (0.14 / 0.28 / 0.0028) instead of the current pro-tier values.

🤖 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/llm/src/pricing.ts` around lines 192 - 201, Update the
`deepseek-reasoner` pricing entry in `pricing.ts` so it matches the
DeepSeek-V4-Flash tier instead of the pro-tier values. In the
`deepseek-reasoner` object, keep the existing identity fields (`provider`,
`nativeId`, `displayName`) but change the pricing fields to the flash rates for
`inputPerMtokMicrocents`, `outputPerMtokMicrocents`, and
`cachedInputPerMtokMicrocents`.
🧹 Nitpick comments (5)
packages/core/src/engine/agent-session.test.ts (1)

1019-1131: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the cancelled outcome branch.

The suite exercises ran/denied/failed but not the cancelled branch of runUserCommand (hit when the registry throws ToolCancelledError, e.g. an Esc/abort() mid-command). Given this is a security-relevant shell-escape path, a test asserting outcome.kind === 'cancelled' when the host's process arm rejects with a cancellation would close the coverage gap.

✅ Suggested additional test
+  it('a cancel/abort mid-command classifies as `cancelled` (no raw error escapes)', async () => {
+    const { registry } = commandRegistry(() => Promise.reject(new ToolCancelledError('run_command')));
+    const { deps } = harness([], { toolPolicy: { allowedCommands: ['ls'] } }, registry);
+    const outcome = await startedSession(deps).runUserCommand('ls', []);
+    expect(outcome.kind).toBe('cancelled');
+  });
🤖 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/core/src/engine/agent-session.test.ts` around lines 1019 - 1131, Add
a test in AgentSession.runUserCommand’s `!`-shell escape suite that covers the
missing `cancelled` branch. Use the existing `commandRegistry` and
`startedSession` helpers to make the host process arm reject with a
cancellation-like error (the registry path that becomes ToolCancelledError),
then assert `outcome.kind === 'cancelled'` and that no spawn side effects occur.
Place it alongside the current `ran`/`denied`/`failed` cases so the coverage for
`runUserCommand` includes the cancellation path.
apps/cli/src/render/tui/home-controller.ts (1)

600-651: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reduce applyChatAction cognitive complexity to satisfy the SonarCloud gate. SonarCloud reports this as a [failure] (complexity 23 > 15). Consider extracting the move (history-recall) and submit (!-shell vs message) branches into small named helpers to bring it under the threshold and keep the CI quality gate green.

🤖 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 600 - 651,
`applyChatAction` is too complex for the SonarCloud threshold, so split the
larger `move` and `submit` branches into small named helpers to reduce the
cognitive complexity. Keep the existing behavior in `home-controller` by
extracting the history-recall logic and the shell-vs-message submit handling
into dedicated functions, then have `applyChatAction` delegate to them.

Source: Linters/SAST tools

apps/cli/src/render/tui/chat-input.ts (1)

99-116: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reduce cognitive complexity of reduceCursorMotion (SonarCloud: 16/15 allowed).

Splitting the arrow-key motions from the readline-chord motions (Home/End, Ctrl+A/E, Alt+B/F) into a small helper removes enough branches/mixed-operator conditions to get back under budget, with no behavior change.

♻️ Proposed refactor
+/** Readline-style chord motions: Home/End, Ctrl+A/Ctrl+E, Alt+B/Alt+F. */
+function reduceChordMotion(char: string, key: ChatKey): EditorEditAction | undefined {
+  if (key.home === true || (key.ctrl === true && char === 'a'))
+    return { kind: 'move', motion: 'line-start' };
+  if (key.end === true || (key.ctrl === true && char === 'e'))
+    return { kind: 'move', motion: 'line-end' };
+  if (key.meta === true && char === 'b') return { kind: 'move', motion: 'word-left' }; // readline Alt+B
+  if (key.meta === true && char === 'f') return { kind: 'move', motion: 'word-right' }; // readline Alt+F
+  return undefined;
+}
+
 function reduceCursorMotion(char: string, key: ChatKey): EditorEditAction | undefined {
   const wordMod = key.ctrl === true || key.meta === true;
   if (key.leftArrow === true) return { kind: 'move', motion: wordMod ? 'word-left' : 'left' };
   if (key.rightArrow === true) return { kind: 'move', motion: wordMod ? 'word-right' : 'right' };
   if (key.upArrow === true) return { kind: 'move', motion: 'up' };
   if (key.downArrow === true) return { kind: 'move', motion: 'down' };
-  if (key.home === true || (key.ctrl === true && char === 'a'))
-    return { kind: 'move', motion: 'line-start' };
-  if (key.end === true || (key.ctrl === true && char === 'e'))
-    return { kind: 'move', motion: 'line-end' };
-  if (key.meta === true && char === 'b') return { kind: 'move', motion: 'word-left' }; // readline Alt+B
-  if (key.meta === true && char === 'f') return { kind: 'move', motion: 'word-right' }; // readline Alt+F
-  return undefined;
+  return reduceChordMotion(char, key);
 }
🤖 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-input.ts` around lines 99 - 116, The
reduceCursorMotion function is over the cognitive complexity limit because it
mixes arrow handling with readline-style chord handling in one branchy block.
Refactor it by splitting the motion detection into a small helper for the
Home/End, Ctrl+A/E, and Alt+B/F cases while keeping the arrow-key logic in
reduceCursorMotion, and preserve the same EditorEditAction return values and
behavior.

Source: Linters/SAST tools

apps/cli/src/engine/tool-host/fs.ts (1)

545-551: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Error message is now stale — doesn't mention the newly-added .aws/.env/.docker denial reasons.

isSensitiveReadPath was expanded to cover .aws, .env/.env.*, and .docker/config.json, but the thrown message still only lists .ssh / .relavium / a git config / a credential dotfile. Since this is the user-facing, secret-free explanation surfaced when a read_file/@-mention read is refused, it should reflect the actual denial set for clarity.

✏️ Proposed message update
 function assertNotSensitiveReadPath(absoluteTarget: string): void {
   if (isSensitiveReadPath(absoluteTarget)) {
     throw new FsScopeDeniedError(
-      'refusing to read a credential/secret store (.ssh / .relavium / a git config / a credential dotfile) — ask the user to share any needed content instead',
+      'refusing to read a credential/secret store (.ssh / .relavium / .aws / .env / .docker config / a git config / a credential dotfile) — ask the user to share any needed content instead',
     );
   }
 }
🤖 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/engine/tool-host/fs.ts` around lines 545 - 551, The user-facing
denial message in assertNotSensitiveReadPath is stale and no longer matches the
paths blocked by isSensitiveReadPath. Update the FsScopeDeniedError text in
fs.ts to mention the full current sensitive-read set, including the newly denied
.aws, .env/.env.*, and .docker/config.json cases, so the refusal message stays
accurate for read_file and `@-mention` reads.
apps/cli/src/render/tui/mention.ts (1)

144-153: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Filter backspace isn't code-point aware — could split an astral character.

foldMentionBackspace trims the filter with state.filter.slice(0, -1), which can break a surrogate pair (e.g. an emoji in a filename filter), unlike deleteBeforeCursor/dropLastCodePoint used elsewhere in the editor primitives for the same reason. Impact is limited to cosmetic filter matching, not security, since visibleMentions only does substring matching.

🤖 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/mention.ts` around lines 144 - 153,
`foldMentionBackspace` currently removes the last UTF-16 unit from
`state.filter`, which can split astral characters. Update the backspace/delete
handling in `foldMentionBackspace` to trim by Unicode code point instead of
using `slice(0, -1)`, following the same approach as
`deleteBeforeCursor`/`dropLastCodePoint` used elsewhere. Keep the existing
behavior for descending to `parentDir` and closing at root, and only change the
filter-trimming step to be code-point safe.
🤖 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/render/tui/injection.ts`:
- Around line 4-13: The ADR-0061 relative link in the TUI injection doc comment
is resolving to the wrong path because the markdown relative traversal is one
level short. Update the reference in the shared UNTRUSTED-context framing
comment so it uses the correct number of parent-directory segments to reach
docs/decisions, and apply the same fix in the matching doc comment in shell.ts
to keep both links consistent.

In `@apps/cli/src/render/tui/shell.ts`:
- Line 5: The module doc comment in shell.ts has a broken relative link to
ADR-0061 because the path is one directory short. Update the markdown link
target in the top-of-file comment so it uses the correct relative traversal from
apps/cli/src/render/tui/ to docs/decisions/, matching the same fix applied in
injection.ts.

In `@docs/reference/contracts/agent-session-spec.md`:
- Line 56: The ADR-0061 markdown link in the agent session spec is resolving
from the wrong relative path. Update the `runUserCommand` entry so the reference
to `ADR-0061` uses the correct number of `../` segments from
`docs/reference/contracts/` to `docs/decisions/`, and keep the link in
kebab-case relative form.

In `@docs/reference/contracts/config-spec.md`:
- Around line 136-138: The [chat] config wording is inconsistent: it currently
says chat does not define its own command allowlist, but the new [chat] section
also introduces allowed_commands and allowed_command_globs. Update the
config-spec text around the chat policy and allowedCommands/allowedCommandGlobs
mapping so it clearly states whether chat resolves its own TOML keys or simply
reuses the workflow allowlist, and make the two paragraphs consistent with the
same underlying run_command/!-shell policy.

---

Outside diff comments:
In `@packages/llm/src/pricing.ts`:
- Around line 192-201: Update the `deepseek-reasoner` pricing entry in
`pricing.ts` so it matches the DeepSeek-V4-Flash tier instead of the pro-tier
values. In the `deepseek-reasoner` object, keep the existing identity fields
(`provider`, `nativeId`, `displayName`) but change the pricing fields to the
flash rates for `inputPerMtokMicrocents`, `outputPerMtokMicrocents`, and
`cachedInputPerMtokMicrocents`.

---

Nitpick comments:
In `@apps/cli/src/engine/tool-host/fs.ts`:
- Around line 545-551: The user-facing denial message in
assertNotSensitiveReadPath is stale and no longer matches the paths blocked by
isSensitiveReadPath. Update the FsScopeDeniedError text in fs.ts to mention the
full current sensitive-read set, including the newly denied .aws, .env/.env.*,
and .docker/config.json cases, so the refusal message stays accurate for
read_file and `@-mention` reads.

In `@apps/cli/src/render/tui/chat-input.ts`:
- Around line 99-116: The reduceCursorMotion function is over the cognitive
complexity limit because it mixes arrow handling with readline-style chord
handling in one branchy block. Refactor it by splitting the motion detection
into a small helper for the Home/End, Ctrl+A/E, and Alt+B/F cases while keeping
the arrow-key logic in reduceCursorMotion, and preserve the same
EditorEditAction return values and behavior.

In `@apps/cli/src/render/tui/home-controller.ts`:
- Around line 600-651: `applyChatAction` is too complex for the SonarCloud
threshold, so split the larger `move` and `submit` branches into small named
helpers to reduce the cognitive complexity. Keep the existing behavior in
`home-controller` by extracting the history-recall logic and the
shell-vs-message submit handling into dedicated functions, then have
`applyChatAction` delegate to them.

In `@apps/cli/src/render/tui/mention.ts`:
- Around line 144-153: `foldMentionBackspace` currently removes the last UTF-16
unit from `state.filter`, which can split astral characters. Update the
backspace/delete handling in `foldMentionBackspace` to trim by Unicode code
point instead of using `slice(0, -1)`, following the same approach as
`deleteBeforeCursor`/`dropLastCodePoint` used elsewhere. Keep the existing
behavior for descending to `parentDir` and closing at root, and only change the
filter-trimming step to be code-point safe.

In `@packages/core/src/engine/agent-session.test.ts`:
- Around line 1019-1131: Add a test in AgentSession.runUserCommand’s `!`-shell
escape suite that covers the missing `cancelled` branch. Use the existing
`commandRegistry` and `startedSession` helpers to make the host process arm
reject with a cancellation-like error (the registry path that becomes
ToolCancelledError), then assert `outcome.kind === 'cancelled'` and that no
spawn side effects occur. Place it alongside the current `ran`/`denied`/`failed`
cases so the coverage for `runUserCommand` includes the cancellation path.
🪄 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: 24e9e333-adf8-4f3c-a8f3-ef91769248fe

📥 Commits

Reviewing files that changed from the base of the PR and between e0f278d and d9e54d9.

📒 Files selected for processing (61)
  • CLAUDE.md
  • apps/cli/src/chat/persister.test.ts
  • apps/cli/src/chat/session-host.test.ts
  • apps/cli/src/chat/session-host.ts
  • apps/cli/src/commands/chat.test.ts
  • apps/cli/src/commands/chat.ts
  • apps/cli/src/config/resolve.test.ts
  • apps/cli/src/config/resolve.ts
  • apps/cli/src/engine/media-wiring.test.ts
  • apps/cli/src/engine/providers.ts
  • apps/cli/src/engine/tool-host/fs.test.ts
  • apps/cli/src/engine/tool-host/fs.ts
  • apps/cli/src/home/drive-home.test.ts
  • apps/cli/src/home/drive-home.tsx
  • apps/cli/src/render/tui/chat-ink.tsx
  • apps/cli/src/render/tui/chat-input.test.ts
  • apps/cli/src/render/tui/chat-input.ts
  • apps/cli/src/render/tui/chat-projection.test.ts
  • apps/cli/src/render/tui/chat-projection.ts
  • apps/cli/src/render/tui/home-app.tsx
  • apps/cli/src/render/tui/home-controller.test.ts
  • apps/cli/src/render/tui/home-controller.ts
  • apps/cli/src/render/tui/home-input.test.ts
  • apps/cli/src/render/tui/home-input.ts
  • apps/cli/src/render/tui/home-view.tsx
  • apps/cli/src/render/tui/injection.ts
  • apps/cli/src/render/tui/input-history.test.ts
  • apps/cli/src/render/tui/input-history.ts
  • apps/cli/src/render/tui/mention-view.test.ts
  • apps/cli/src/render/tui/mention-view.tsx
  • apps/cli/src/render/tui/mention.test.ts
  • apps/cli/src/render/tui/mention.ts
  • apps/cli/src/render/tui/prompt-cursor.test.ts
  • apps/cli/src/render/tui/prompt-cursor.ts
  • apps/cli/src/render/tui/prompt-view.tsx
  • apps/cli/src/render/tui/reverse-search-view.tsx
  • apps/cli/src/render/tui/shell.test.ts
  • apps/cli/src/render/tui/shell.ts
  • docs/decisions/0061-cli-input-layer-file-injection-and-shell-escape.md
  • docs/decisions/README.md
  • docs/reference/cli/chat-session.md
  • docs/reference/cli/home.md
  • docs/reference/contracts/agent-session-spec.md
  • docs/reference/contracts/config-spec.md
  • docs/reference/shared-core/built-in-tools.md
  • docs/reference/shared-core/tool-registry.md
  • docs/roadmap/current.md
  • docs/roadmap/deferred-tasks.md
  • docs/roadmap/phases/phase-2.5-cli-consolidation.md
  • docs/standards/error-handling.md
  • docs/standards/security-review.md
  • packages/core/src/engine/agent-session.test.ts
  • packages/core/src/engine/agent-session.ts
  • packages/core/src/index.ts
  • packages/llm/src/adapters/openai.ts
  • packages/llm/src/cost-tracker.test.ts
  • packages/llm/src/llm-error.test.ts
  • packages/llm/src/llm-error.ts
  • packages/llm/src/pricing.ts
  • packages/shared/src/config.test.ts
  • packages/shared/src/config.ts

Comment thread apps/cli/src/render/tui/injection.ts Outdated
Comment thread apps/cli/src/render/tui/shell.ts Outdated
Comment thread docs/reference/contracts/agent-session-spec.md Outdated
Comment thread docs/reference/contracts/config-spec.md Outdated
cemililik and others added 3 commits July 3, 2026 15:15
…pricing, complexity)

Verified each finding against current code; fixed the valid ones, skipped the
rest (below) with reasons.

Correctness:
- pricing.ts: `deepseek-reasoner` was priced at stale R1-era (pro-tier) rates
  (0.435/0.87) despite being the thinking-mode alias of v4-flash — the file's own
  comment states thinking is a request PARAM with no per-mode price, so it must
  match `deepseek-chat` (0.14/0.28). Repriced to flash.
- Broken ADR-0061 relative links (one `../` short): injection.ts / shell.ts /
  mention.ts / mention-view.tsx (4→5 `../` from tui/, verified vs ink-renderer.ts)
  and agent-session-spec.md (`../`→`../../decisions/`, verified vs its siblings).
- config-spec.md: reconciled the contradictory `[chat]` wording — it now states
  chat DOES define its own `allowed_commands` / `allowed_command_globs` keys that
  map to the SAME engine `allowedCommands` policy (not "no own allowlist").
- fs.ts: the sensitive-read denial message now names the full current floor
  (.aws / .env* / .docker config), not just .ssh/.relavium/git.
- mention.ts: `foldMentionBackspace` trims the filter by whole CODE POINT
  (`dropLastCodePoint`), so backspacing an astral char no longer leaves a lone
  surrogate (test added).

Complexity (SonarCloud Critical — split, behavior identical):
- reduceCursorMotion → reduceLineMotion (line-boundary chords) + arrows.
- applyChatAction → applyMoveAction + applySubmitAction.

Nits:
- home-controller: split loadMentions' undefined-check onto its own line (narrows
  `open` for the spread, drops the prefer-optional-chain smell).
- prompt-view: key each prompt row by its line START char offset — a stable,
  positional id (not the array index; identical lines never collide).

Tests: `runUserCommand` `cancelled` branch (mid-command cancel → aborted dispatch
→ ToolCancelledError). Full CI green (core 941, cli 1264; format/fence/engine-deps/
bundle-closure); Leakwatch clean.

Skipped: `palette-reducer.ts` has the same short ADR link but is a 2.5.C file
outside this PR's diff (a pre-existing, separately-tracked fix).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ess fixes

From the PR-wide adversarial review (17 confirmed of 29; no HIGH):
- reverse-search backspace (foldReverseSearchKey) now trims the query by whole
  CODE POINT (dropLastCodePoint), so backspacing an astral char never leaves a
  lone surrogate — parity with the @-mention filter + the editor (test added).
- ChatApp `applyEditor` folds against `editorRef.current` (the synchronous source
  of truth) and mirrors into React state, matching the palette/search/mention/
  shellBusy ref-shadows. The prior form mutated the ref INSIDE the queued updater,
  so React's `prev` (stale for the 2nd+ event of a no-flush coalesced chunk) could
  make a same-chunk decision-read (submit / mention-open) see a stale buffer.
- runUserCommand's finally clears `#abortingTurn` (mirrors sendMessage), so an
  Esc-abort of a `!`-command leaves no marker outliving the operation.

(The critic's two unverified gaps were checked + dismissed: the injected body IS
terminal-sanitized at the editor + transcript render boundaries via sanitizeInline
→ stripTerminalControls; the process arm HAS a 30s default timeout + process-group
SIGKILL.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ection

Redesign the 2.5.D chat input layer (`@`-mention / `!`-shell) from splicing
framed bytes into the compose editor to a pending-attachment (chip) model,
shared byte-for-byte across the standalone `ChatApp` and the bare-Home:

- New pure core `attachments.ts`: `PendingAttachment`, `mentionMarker` /
  `mentionPresent` (whitespace-bounded token scan), `buildOutbound`
  (expand-present-at-submit), `appendAttachment` (dedup-by-path + capped
  eviction + honest drop count), `commandResultPreview`, `fileAttachmentWarning`.
- `@` inserts a compact `@path` marker + queues a chip; the file expands into
  the SAME `frameUntrusted` nonce-fenced UNTRUSTED frame at submit, only if the
  marker survives (deleting it drops the file). `!` output is shown read-only +
  rides the next message as a chip. `Esc` at idle discards pending chips.
- A labeled `!`-busy indicator (the running command + an honest "Esc to cancel",
  since Esc aborts the command while Ctrl-C would end the session).
- Chip bar view; `processLine(raw, display?)` threads the compact transcript
  form while `history.db` + the model still get the full framed message.
- Byte-bound the `!` read-only preview (single-huge-line guard), fix the
  stderr-only preview blank line.

Security invariants unchanged (floor, jail, allowlist-before-approval, untrusted
brand, double-bounding, submit-generation guard, TTY-only) — the model receives
byte-identical framed context. Both adversarial review passes (security/parity +
race/UX) certified clean; findings fixed.

Tests: new `injection.test.ts` (frame/sanitize/bound/surrogate), `shellDenyHint`
branches, `appendAttachment` dedup+cap, Esc-clears-chips (idle) vs no-op (busy),
busy-label, stderr preview; existing inline-injection assertions updated to the
chip model. Full `pnpm run ci` green; Leakwatch clean.

Docs: ADR-0061 "Refined at implementation" append (append-only), chat-session /
home keymap (chips, `Delete` forward-delete, `Esc`).

Refs: ADR-0061

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/cli/src/render/tui/chat-ink.tsx (1)

414-441: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

UserCommandOutcome still collapses to any at the CLI seam. kind narrowing and exitCode/stdout/stderr remain unchecked here and in home-controller.ts; @relavium/core needs to expose a concrete discriminated union.

🤖 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-ink.tsx` around lines 414 - 441,
`handleShellOutcome` is still receiving an `any`-shaped `UserCommandOutcome`, so
the `kind` checks and `exitCode`/`stdout`/`stderr` access are not type-safe.
Update the `UserCommandOutcome` type exported from `@relavium/core` to a
concrete discriminated union with distinct `ran`, `denied`, and `cancelled`
branches, and then update both `handleShellOutcome` in `chat-ink.tsx` and the
corresponding logic in `home-controller.ts` to rely on that typed union without
`any` leakage.

Sources: Coding guidelines, Linters/SAST tools

🧹 Nitpick comments (1)
apps/cli/src/render/tui/attachments.ts (1)

111-140: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the nested ternary for readability.

Flagged by SonarCloud; the triple-nested conditional building combined's input is hard to scan at a glance.

♻️ Proposed refactor
-  const combined = boundInjection(
-    (stderr.length > 0
-      ? stdout.length > 0
-        ? `${stdout}\n[stderr] ${stderr}`
-        : `[stderr] ${stderr}`
-      : stdout
-    ).trimEnd(),
-  ).trimEnd();
+  const stderrBlock = stderr.length > 0 ? `[stderr] ${stderr}` : '';
+  const rawCombined =
+    stdout.length > 0 && stderrBlock.length > 0
+      ? `${stdout}\n${stderrBlock}`
+      : stdout.length > 0
+        ? stdout
+        : stderrBlock;
+  const combined = boundInjection(rawCombined.trimEnd()).trimEnd();
🤖 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/attachments.ts` around lines 111 - 140, The nested
ternary inside commandResultPreview is hard to read; replace the inline
conditional that builds the combined input with clearer branching or a small
helper variable. Keep the same behavior for stdout-only, stderr-only, and
both-stream cases, and preserve the existing trimming/boundInjection flow in
commandResultPreview.

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.

Outside diff comments:
In `@apps/cli/src/render/tui/chat-ink.tsx`:
- Around line 414-441: `handleShellOutcome` is still receiving an `any`-shaped
`UserCommandOutcome`, so the `kind` checks and `exitCode`/`stdout`/`stderr`
access are not type-safe. Update the `UserCommandOutcome` type exported from
`@relavium/core` to a concrete discriminated union with distinct `ran`,
`denied`, and `cancelled` branches, and then update both `handleShellOutcome` in
`chat-ink.tsx` and the corresponding logic in `home-controller.ts` to rely on
that typed union without `any` leakage.

---

Nitpick comments:
In `@apps/cli/src/render/tui/attachments.ts`:
- Around line 111-140: The nested ternary inside commandResultPreview is hard to
read; replace the inline conditional that builds the combined input with clearer
branching or a small helper variable. Keep the same behavior for stdout-only,
stderr-only, and both-stream cases, and preserve the existing
trimming/boundInjection flow in commandResultPreview.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2ecfaa43-45e5-441d-b4da-eab161af9019

📥 Commits

Reviewing files that changed from the base of the PR and between 5802127 and 2407456.

📒 Files selected for processing (16)
  • apps/cli/src/commands/chat.ts
  • apps/cli/src/render/tui/attachment-bar.tsx
  • apps/cli/src/render/tui/attachments.test.ts
  • apps/cli/src/render/tui/attachments.ts
  • apps/cli/src/render/tui/chat-ink.tsx
  • apps/cli/src/render/tui/home-app.tsx
  • apps/cli/src/render/tui/home-controller.test.ts
  • apps/cli/src/render/tui/home-controller.ts
  • apps/cli/src/render/tui/injection.test.ts
  • apps/cli/src/render/tui/input-history.test.ts
  • apps/cli/src/render/tui/input-history.ts
  • apps/cli/src/render/tui/shell.test.ts
  • docs/decisions/0061-cli-input-layer-file-injection-and-shell-escape.md
  • docs/reference/cli/chat-session.md
  • docs/reference/cli/home.md
  • packages/core/src/engine/agent-session.ts
✅ Files skipped from review due to trivial changes (1)
  • docs/reference/cli/home.md
🚧 Files skipped from review as they are similar to previous changes (7)
  • apps/cli/src/render/tui/shell.test.ts
  • apps/cli/src/render/tui/input-history.test.ts
  • docs/reference/cli/chat-session.md
  • apps/cli/src/render/tui/input-history.ts
  • docs/decisions/0061-cli-input-layer-file-injection-and-shell-escape.md
  • packages/core/src/engine/agent-session.ts
  • apps/cli/src/render/tui/home-controller.test.ts

…, bidi escapes)

Three SonarCloud/review nitpicks, each verified behavior-preserving:

- attachments.ts: flatten the nested ternary in `commandResultPreview` into a
  `stderrBlock` + filter/join — byte-identical `combined` for every stdout/stderr
  case (stdout-only, stderr-only, both, empty, whitespace).
- chat-ink.tsx: flip the busy-indicator ternary to a positive condition
  (`busyCommand === undefined`) — swaps the two branches only, no behavior change,
  resolves the no-negated-condition smell.
- injection.test.ts: rewrite the bidi-control test string with explicit `\u`
  escapes (RLO/RLM/LRI/ALM) so no literal bidirectional character lives in the
  source (removes the Trojan-Source hotspot); same runtime string, same assertion.

Skipped: the "`UserCommandOutcome` is any-shaped" comment — false positive. It is
already a concrete discriminated union (`ran`/`denied`/`failed`/`cancelled`) in
@relavium/core; both `handleShellOutcome` consumers narrow on `.kind` type-safely
with no `any`/`as`/`@ts-ignore`.

Typecheck clean; 1284 CLI tests pass; prettier + eslint clean. All fixes
adversarially verified byte-identical.

Refs: ADR-0061

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cemililik

Copy link
Copy Markdown
Contributor Author

@coderabbitai review all

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

@cemililik: Sure, I'll do a full review of all the changes in this PR again.

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
packages/core/src/engine/agent-session.ts (1)

241-250: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid the unsafe cast in the boundary guard.

This can be written as a pure type guard and validate the full ProcessResult shape, including durationMs. As per coding guidelines, **/*.{ts,tsx,cts,mts}: Use TypeScript strict mode everywhere; do not use any or unsafe as casts, and prefer type guards.

Suggested refactor
 function isProcessResult(value: unknown): value is ProcessResult {
   if (typeof value !== 'object' || value === null) return false;
-  const v = value as Record<string, unknown>;
   return (
-    typeof v['exitCode'] === 'number' &&
-    typeof v['stdout'] === 'string' &&
-    typeof v['stderr'] === 'string'
+    'exitCode' in value &&
+    typeof value.exitCode === 'number' &&
+    'stdout' in value &&
+    typeof value.stdout === 'string' &&
+    'stderr' in value &&
+    typeof value.stderr === 'string' &&
+    'durationMs' in value &&
+    typeof value.durationMs === 'number'
   );
 }
🤖 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/core/src/engine/agent-session.ts` around lines 241 - 250, The
boundary guard in isProcessResult still uses an unsafe cast and does not
validate the full ProcessResult shape. Rewrite it as a pure type guard using
explicit property checks on the unknown value without relying on any
Record<string, unknown> cast, and include durationMs in the validation alongside
exitCode, stdout, and stderr so the tool result is fully verified at the
boundary.

Source: Coding guidelines

apps/cli/src/engine/tool-host/fs.test.ts (1)

735-756: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add a non-credentials file under .aws/ to prove directory-wide denial.

Per ADR-0061, .aws is floored as a directory segment (any file under it), unlike .docker/config.json which is a specific file. The current test only exercises .aws/credentials, so it can't distinguish "denies this specific filename" from "denies the whole .aws/ directory" — a regression narrowing the check to just credentials would pass this suite.

✅ Suggested additional assertion
     await mkdir(join(workspace, '.aws'), { recursive: true });
     await writeFile(join(workspace, '.aws', 'credentials'), '[default]\naws_secret_access_key=x');
     await expect(sandboxed().readFile('.aws/credentials', {})).rejects.toBeInstanceOf(
       FsScopeDeniedError,
     );
+    // Prove the floor is DIRECTORY-wide (not just the `credentials` filename).
+    await writeFile(join(workspace, '.aws', 'config'), '[default]\nregion=us-east-1');
+    await expect(sandboxed().readFile('.aws/config', {})).rejects.toBeInstanceOf(
+      FsScopeDeniedError,
+    );
🤖 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/engine/tool-host/fs.test.ts` around lines 735 - 756, The fs
scope test only proves denial for .aws/credentials, so it does not verify that
the entire .aws directory is floored. Update the fs.test case around
sandboxed().readFile and FsScopeDeniedError to add at least one additional
non-credentials file under .aws/ and assert it is also denied, while keeping
.docker/config.json as the specific-file denial check.
docs/decisions/0061-cli-input-layer-file-injection-and-shell-escape.md (2)

1-315: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Consider trimming toward the "condensed MADR" form.

As per coding guidelines, docs/decisions/**/*.md should "Record non-trivial decisions as ADRs in docs/decisions/ using the condensed MADR form." At ~315 lines with multi-paragraph Context/Decision/Consequences and repeated "Considered" enumerations, this ADR is far denser than the condensed MADR template typically calls for. Consider factoring the exhaustive security-review rationale into the linked docs/standards/security-review.md and keeping the ADR itself to the decision + condensed consequences, linking out for 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 `@docs/decisions/0061-cli-input-layer-file-injection-and-shell-escape.md`
around lines 1 - 315, This ADR is too long and verbose for the condensed MADR
style expected in docs/decisions. Trim the long Context/Decision/Consequences
prose in ADR-0061 and move the exhaustive security-review rationale, attack
analysis, and repeated “Considered” details into the linked security-review and
reference docs. Keep the ADR focused on the decision, the key security
boundaries, and a short set of consequences, and use ADR-0061,
AgentSession.runUserCommand, `@-mention`, and !-shell as the main anchors for
locating what to condense.

Source: Coding guidelines


34-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

A Mermaid diagram would clarify the two-boundary trust model.

As per coding guidelines, "Use Mermaid diagrams in documentation when diagrams are needed." This ADR describes two distinct trust boundaries (@-mention → fs jail/floor; !-shell → enforcePolicy(allowedCommands)confirmAction → process arm) purely in prose across several paragraphs. A sequence or flow diagram showing where each input-layer feature re-enters the audited boundary would make the "reuse, never fork" invariant easier to verify at a glance.

Also applies to: 126-178

🤖 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/decisions/0061-cli-input-layer-file-injection-and-shell-escape.md`
around lines 34 - 59, Add a Mermaid diagram to this ADR section to visualize the
two trust boundaries and the “reuse, never fork” flow. The current prose
explains `@`-mention versus `!`-shell behavior, but it should be supplemented
with a sequence/flow diagram showing `apps/cli` input preprocessing, re-entry
into `ToolRegistry.dispatch`, and the audited path through
`enforcePolicy(allowedCommands)` and `confirmAction` versus the
`fs`/`isSensitiveReadPath` boundary. Place it near the existing discussion so
the `@`-mention and `!`-shell paths are easy to compare at a glance.

Source: Coding guidelines

🤖 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/config/resolve.ts`:
- Around line 119-122: The allowlist merge in resolveConfig currently lets
allowedCommands from the project override the workspace while
allowedCommandGlobs still fall back to the workspace, which breaks the intended
full override behavior. Update the logic around the allowedCommands and
allowedCommandGlobs fields so that when a project provides either command
allowlist, it does not inherit broader workspace globs; keep the
last-writer-wins behavior consistent with the ADR-0061 comment in resolveConfig.

---

Nitpick comments:
In `@apps/cli/src/engine/tool-host/fs.test.ts`:
- Around line 735-756: The fs scope test only proves denial for
.aws/credentials, so it does not verify that the entire .aws directory is
floored. Update the fs.test case around sandboxed().readFile and
FsScopeDeniedError to add at least one additional non-credentials file under
.aws/ and assert it is also denied, while keeping .docker/config.json as the
specific-file denial check.

In `@docs/decisions/0061-cli-input-layer-file-injection-and-shell-escape.md`:
- Around line 1-315: This ADR is too long and verbose for the condensed MADR
style expected in docs/decisions. Trim the long Context/Decision/Consequences
prose in ADR-0061 and move the exhaustive security-review rationale, attack
analysis, and repeated “Considered” details into the linked security-review and
reference docs. Keep the ADR focused on the decision, the key security
boundaries, and a short set of consequences, and use ADR-0061,
AgentSession.runUserCommand, `@-mention`, and !-shell as the main anchors for
locating what to condense.
- Around line 34-59: Add a Mermaid diagram to this ADR section to visualize the
two trust boundaries and the “reuse, never fork” flow. The current prose
explains `@`-mention versus `!`-shell behavior, but it should be supplemented
with a sequence/flow diagram showing `apps/cli` input preprocessing, re-entry
into `ToolRegistry.dispatch`, and the audited path through
`enforcePolicy(allowedCommands)` and `confirmAction` versus the
`fs`/`isSensitiveReadPath` boundary. Place it near the existing discussion so
the `@`-mention and `!`-shell paths are easy to compare at a glance.

In `@packages/core/src/engine/agent-session.ts`:
- Around line 241-250: The boundary guard in isProcessResult still uses an
unsafe cast and does not validate the full ProcessResult shape. Rewrite it as a
pure type guard using explicit property checks on the unknown value without
relying on any Record<string, unknown> cast, and include durationMs in the
validation alongside exitCode, stdout, and stderr so the tool result is fully
verified at the boundary.
🪄 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: 226ec815-0693-4ff2-8a0d-cc6199458d04

📥 Commits

Reviewing files that changed from the base of the PR and between e0f278d and e8007d5.

📒 Files selected for processing (65)
  • CLAUDE.md
  • apps/cli/src/chat/persister.test.ts
  • apps/cli/src/chat/session-host.test.ts
  • apps/cli/src/chat/session-host.ts
  • apps/cli/src/commands/chat.test.ts
  • apps/cli/src/commands/chat.ts
  • apps/cli/src/config/resolve.test.ts
  • apps/cli/src/config/resolve.ts
  • apps/cli/src/engine/media-wiring.test.ts
  • apps/cli/src/engine/providers.ts
  • apps/cli/src/engine/tool-host/fs.test.ts
  • apps/cli/src/engine/tool-host/fs.ts
  • apps/cli/src/home/drive-home.test.ts
  • apps/cli/src/home/drive-home.tsx
  • apps/cli/src/render/tui/attachment-bar.tsx
  • apps/cli/src/render/tui/attachments.test.ts
  • apps/cli/src/render/tui/attachments.ts
  • apps/cli/src/render/tui/chat-ink.tsx
  • apps/cli/src/render/tui/chat-input.test.ts
  • apps/cli/src/render/tui/chat-input.ts
  • apps/cli/src/render/tui/chat-projection.test.ts
  • apps/cli/src/render/tui/chat-projection.ts
  • apps/cli/src/render/tui/home-app.tsx
  • apps/cli/src/render/tui/home-controller.test.ts
  • apps/cli/src/render/tui/home-controller.ts
  • apps/cli/src/render/tui/home-input.test.ts
  • apps/cli/src/render/tui/home-input.ts
  • apps/cli/src/render/tui/home-view.tsx
  • apps/cli/src/render/tui/injection.test.ts
  • apps/cli/src/render/tui/injection.ts
  • apps/cli/src/render/tui/input-history.test.ts
  • apps/cli/src/render/tui/input-history.ts
  • apps/cli/src/render/tui/mention-view.test.ts
  • apps/cli/src/render/tui/mention-view.tsx
  • apps/cli/src/render/tui/mention.test.ts
  • apps/cli/src/render/tui/mention.ts
  • apps/cli/src/render/tui/prompt-cursor.test.ts
  • apps/cli/src/render/tui/prompt-cursor.ts
  • apps/cli/src/render/tui/prompt-view.tsx
  • apps/cli/src/render/tui/reverse-search-view.tsx
  • apps/cli/src/render/tui/shell.test.ts
  • apps/cli/src/render/tui/shell.ts
  • docs/decisions/0061-cli-input-layer-file-injection-and-shell-escape.md
  • docs/decisions/README.md
  • docs/reference/cli/chat-session.md
  • docs/reference/cli/home.md
  • docs/reference/contracts/agent-session-spec.md
  • docs/reference/contracts/config-spec.md
  • docs/reference/shared-core/built-in-tools.md
  • docs/reference/shared-core/tool-registry.md
  • docs/roadmap/current.md
  • docs/roadmap/deferred-tasks.md
  • docs/roadmap/phases/phase-2.5-cli-consolidation.md
  • docs/standards/error-handling.md
  • docs/standards/security-review.md
  • packages/core/src/engine/agent-session.test.ts
  • packages/core/src/engine/agent-session.ts
  • packages/core/src/index.ts
  • packages/llm/src/adapters/openai.ts
  • packages/llm/src/cost-tracker.test.ts
  • packages/llm/src/llm-error.test.ts
  • packages/llm/src/llm-error.ts
  • packages/llm/src/pricing.ts
  • packages/shared/src/config.test.ts
  • packages/shared/src/config.ts

Comment thread apps/cli/src/config/resolve.ts Outdated
cemililik and others added 2 commits July 3, 2026 22:41
…he run_command result guard

Verified each finding against current code; fixed the valid ones:

- config(cli): the [chat] `!`-shell allowlist (`allowed_commands` exact +
  `allowed_command_globs` globs) now resolves as a COUPLED unit — a project that
  sets EITHER field owns the whole allowlist and inherits NEITHER array from the
  workspace; both fall through only when the project sets neither. Per-field
  resolution let a project narrowing `allowed_commands` to `git status` silently
  keep the workspace's broader `allowed_command_globs` (e.g. `git *`) and still
  run `git push`, defeating the override guarantee (ADR-0061). Covers the `[]`
  opt-out too.
- core: `isProcessResult` validates the FULL ProcessResult shape via `in`-narrowing
  (no `Record<string, unknown>` cast) and now requires `durationMs` — a partial
  result at the run_command boundary fails loudly instead of passing through.
- test(cli): assert the whole `.aws/` directory is floored (add a non-credentials
  `.aws/config` denial), not only `.aws/credentials`.
- docs: config-spec.md allowlist-resolution prose updated to the coupled-unit
  behavior (canonical home); ADR-0061 gains a Mermaid diagram of the two
  "reuse, never fork" trust boundaries + a dated note on the allowlist coupling.

Tests: new coverage — commands-only/globs-only/neither/`[]`-opt-out resolution +
a durationMs-less result rejected end-to-end.

Skipped: "condense/trim ADR-0061" — an Accepted, security-reviewed ADR is
append-only (CLAUDE.md #9); wholesale trimming + relocating its two-round attack
analysis is non-minimal and risks losing the rationale append-only protects. A
dedicated reviewed docs pass (or a superseding ADR) is the right vehicle if wanted.

Also skipped: the "UserCommandOutcome is any-shaped" note — false positive; it is
already a concrete discriminated union and both consumers narrow type-safely.

Full CI green (one pre-existing MCP e2e timeout flake, passes in isolation);
prettier + all lint gates + Leakwatch clean. Fixes adversarially verified
(all CONFIRMED, no residual defects).

Refs: ADR-0061

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
….delete)

The chat prompt — both `relavium chat` and the bare-Home chat — forward-deleted
on Backspace. Root cause: ink 6.8.0 reports the Unix physical Backspace key
(DEL, `\x7f`) as `key.delete`, NOT `key.backspace` (its parse-keypress carries an
explicit TODO about the split), and a prior 2.5.D change routed `key.delete` to a
forward-delete. So pressing Backspace erased the character AFTER the cursor —
exactly the reported "arrow-left then delete removes to the right" behavior.

Fix: map BOTH `key.backspace` and `key.delete` to delete-before-cursor in
`reduceEditorMotion` — the same fold the palette / reverse-search / mention
submodes already use (the main editor was the lone inconsistency). The true
forward-Delete key (`\x1b[3~`) is also reported as `key.delete` and is
indistinguishable at ink's useInput layer (both yield input='' + key.delete), so
a reliable forward-delete binding isn't achievable; removed the non-functional
forward-delete machinery (the `{kind:'delete'}` action, `deleteAfterCursor`, and
its `case 'delete'` handlers) — the standard readline / ink-text-input behavior.

Docs: home.md + chat-session.md keymaps updated (Backspace/Delete both delete
backward). Tests updated. Verified against ink's parse-keypress source; full CLI
suite (1283) + lint + typecheck + format + bundle-closure + Leakwatch green; fix
adversarially verified (both checks CONFIRMED).

Refs: ADR-0061

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Jul 3, 2026

Copy link
Copy Markdown

@cemililik
cemililik merged commit d84d6b8 into main Jul 3, 2026
9 checks passed
cemililik added a commit that referenced this pull request Jul 3, 2026
…del refinement

PR #64 merged (2026-07-03), so 2.5.D (chat input ergonomics + the `@`/`!`
pending-attachment "chip" model) flips from "implemented, pending merge" to
✅ Done across the canonical status homes:

- roadmap/current.md + phases/phase-2.5-cli-consolidation.md: 2.5.D status → Done;
  note the post-implementation refinements recorded in ADR-0061's "Refined at
  implementation" append — the chip model (inline `@path` marker + read-only `!`
  preview, expanded into the SAME UNTRUSTED nonce-fenced frame only at submit),
  the coupled `[chat]` allowlist resolution, and the Backspace/`key.delete` fix.
- CLAUDE.md: status paragraph advanced to 2.5.D Done; next pickup is 2.5.F / G.
- `@`/`!` phase bullets updated to the chip presentation (frame applied at submit).

The spine (2.5.A/B/C/E) plus the first experience-arm workstream (2.5.D) are now
complete; 2.5.F / G remain in the experience arm.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant