Skip to content

feat(vscode-ext): add VS Code extension that launches the Raven TUI - #373

Closed
LivXue wants to merge 655 commits into
mainfrom
feat/vscode_tui_extension
Closed

feat(vscode-ext): add VS Code extension that launches the Raven TUI#373
LivXue wants to merge 655 commits into
mainfrom
feat/vscode_tui_extension

Conversation

@LivXue

@LivXue LivXue commented Aug 28, 2026

Copy link
Copy Markdown
Member

Summary

Adds vscode-ext, a VS Code extension that opens the Raven TUI in an editor-area tab next to the active file, mirroring the OpenCode extension model.

  • raven.openTui command exposed as an editor title button (menus.editor/title) plus a command palette entry
  • raven executable resolution: raven.executablePath or RAVEN_BIN (with ~ expanded and directories rejected), then which / where, then a bash/zsh login shell probe for GUI-launched VS Code that lacks the shell rc PATH, then uv run raven
  • single-terminal lifecycle: reuses the running terminal while it is alive and recreates it after a normal TUI exit closes it
  • the terminal process is raven itself (shellPath/shellArgs), so arguments reach raven through argv with no host-shell expansion on any platform
  • launches raven with the login shell environment merged in: bash/zsh profile env on POSIX, and on Windows the VS Code-configured PowerShell edition is probed first with both editions' profiles merged, so proxy settings and API keys defined in the user's shell profile reach raven
  • narrow vscode facade keeps the launcher logic unit-testable (3 files, 40 vitest tests); PATH and env probing live in pathProbe.ts behind an injected runner
  • packaged for the marketplace: README, LICENSE, Apache-2.0 license field, repository metadata, @vscode/vsce with package/publish scripts
  • wired into the Makefile (lint-vscode-ext / test-vscode-ext / build-vscode-ext), pre-commit eslint + prettier hooks, and a CI job on Node 22

Post-review UI updates: the launch button lives in the editor title bar (menus.editor/title) instead of the status bar, the TUI opens as an editor-area tab (viewColumn Beside), and the version is 0.1.2 for marketplace publication.

Type

  • Fix
  • Feature
  • Docs
  • CI / tooling
  • Refactor
  • Other

Verification

  • npm run lint (pass), npm run type-check (pass), npm run build (pass), npm run fmt (pass), npm test (3 files, 40 tests pass)

  • npm audit --audit-level=critical (pass, 0 vulnerabilities)

  • npm run package builds raven-vscode-0.1.2.vsix (11 files, 12.41 KB)

  • commitlint and scripts/check_commit_messages.py over the branch (pass)

  • CI on the branch: VS Code extension checks, TUI checks, bridge checks, commit messages, pull request title, repository files, python lint, Windows self-upgrade, and pre-commit diff all pass; the unrelated Python unit job was still pending at review time

  • Relevant tests pass locally

  • Relevant lint / type checks pass locally

  • User-facing docs or screenshots are updated when needed

Risk

  • Security impact considered
  • Backward compatibility considered
  • Rollback path is clear for risky changes

No existing behavior changes: the extension is a new top-level package and the repo wiring only adds targets, hooks, and one CI job. Arguments reach raven through argv, so the extension performs no shell quoting. The extension launches raven with the user's login shell environment merged in, which can include proxy settings and API keys defined in that shell's profile; this mirrors what a direct terminal run would have and is the intended behavior. Rollback is deleting vscode-ext/ and reverting the wiring commit.

Related Issues

N/A

Review rounds (gloryfromca):

  • Round 1 (three P1 findings): fixed in ad9bacc. Raven now launches as the terminal process itself (shellPath/shellArgs), so a normal TUI exit closes the terminal and the next Raven: Open TUI recreates it, and arguments reach raven through argv without host-shell expansion on any platform; buildSendText was dropped with its tests. The five new files missing file-level purpose docs (ravenBin.test.ts, terminal.test.ts, pathProbe.test.ts, eslint.config.mjs, vitest.config.mts) now carry them.

  • Round 2: no blockers, can merge.

  • Rounds 3-4 (stale PR description): corrected here; Summary, Verification, and Risk now describe the direct-argv design and the current 23-test suite. LivXue confirmed all three round-1 findings fixed in ad9bacc. No code change was required for this round.

  • Round 5 (P1 lockfile drift): fixed in 48fd061. package-lock.json still identified the root package as 0.1.0 while package.json read 0.1.1, so the packaged VSIX was 0.1.1 but the committed lock metadata said 0.1.0. The lockfile was regenerated through npm (npm install) so both the top-level version and packages[""].version in package-lock.json now agree on 0.1.1. npm run lint / type-check / test / build / fmt pass.

Review round 6 (P1 PowerShell profile): fixed in 1e73eef. The Windows env probe no longer passes -NoProfile, so powershell.exe loads the user profile before the environment is read; -NonInteractive avoids prompts and the timeout covers profile startup cost.

Review round 7 (P1 PowerShell edition): fixed in 466e9aa. The probe reads terminal.integrated.defaultProfile.windows to select the user's actual PowerShell edition, probes it first, and merges both editions with the preferred one winning, so PowerShell 7 profile variables reach raven.

Round 7 follow-up (stale description): the Summary, Verification, and Risk sections above were corrected to the current 0.1.2 / 32-test / merged-login-env state so the squash commit body matches this revision; gates re-ran on 466e9aa (lint, type-check, test: 3 files / 32 tests, build, fmt, audit, and raven-vscode-0.1.2.vsix packaging).

Review round 8 (P1 source-based / auto-detected profiles): fixed in 8463ac1. The Windows shell hint now comes from vscode.env.shell, which VS Code itself resolves from terminal.integrated.defaultProfile including source-based and auto-detected profiles (no literal path needed); the manual profiles.windows lookup remains only as a fallback inside the pure resolveWindowsShellHint helper, which has direct unit coverage. Gates re-ran on the new head: lint, type-check, test: 3 files / 36 tests, build, fmt, audit, and raven-vscode-0.1.2.vsix packaging; CI extension, TUI, bridge, Windows self-upgrade, pre-commit diff, and python lint checks pass.

LivXue review (two findings): both fixed, verified on the current head.

  • pathProbe.ts: the Windows env probe now derives the VS Code-configured shell from terminal.integrated.defaultProfile.windows and profiles.windows, probes that edition first, and merges the powershell.exe and pwsh environments with the preferred edition winning conflicts; tests cover the PowerShell 7 profile case.
  • extension.ts: the shell hint now comes from vscode.env.shell, which VS Code resolves from terminal.integrated.defaultProfile including source-based and auto-detected profiles; the manual profiles.windows path lookup remains only as a fallback. resolveWindowsShellHint is a pure function with tests covering env.shell preference, path fallback, and source-based profiles without a path.

Gates re-ran after both fixes: npm run lint, type-check, test (3 files, 36 tests), build, and fmt all pass.

Review round 9 (docs bug sweep #7): fixed in bfb1b7d. raven.extraArgs is now sanitized at the config boundary (sanitizeExtraArgs), so a misconfigured string cannot spread into single-character argv entries; only string entries of an array survive. Live figures updated to 40 tests and a 12.41 KB VSIX.

userName20260323 and others added 30 commits August 24, 2026 05:59
…ng local patches

## Change description

> Swap the vendored `subagents/raven-research/Raven-X` checkout from `ce225550` to upstream `main` at `71abb5a6` (7 commits, 55 files, +12495/-324), and take the host-side consequences with it.
>
> The checkout is patch-free again, which is the point of taking this commit now. It had been carrying six locally patched files so the ask_user fixes could run here before upstream had them. Upstream took five of the six back, and declined the sixth for the reason already recorded against it. The tree is now byte-identical to an upstream commit, verified by archive-and-diff. The two patches that carried behaviour were already live here and are unchanged in substance: the turn answering a clarify is classified as research from the commit marker rather than by consulting the conversation gate, and `observers["research_trail"]` carries the rendered trail where the launcher can reach it.
>
> `pyproject.toml` and `uv.lock` are byte-identical across the swap, so the editable venv carries over with no `uv sync`.
>
> Host side, carried with it:
>
> - `config.json` enables `drFlow.askUser` in `first_turn` mode, drops `ask_user` from `disabledTools`, turns `finalShape.requireMarker` off, and rewrites `identityOverride`, which had told the model there was nobody to consult. The everos and memory blocks are deliberately untouched.
> - `run.py` learns the clarify handoff as a third shape of committed reply, read from `turn_end.awaiting_user` and tested before `finish_reason`, because the prose route carries an ordinary `stop` and would otherwise ship a page of questions as a finished report. It appends the research trail to the reply: the report template tells the model not to close with its sources, promising a record this launcher never received. A blank line before the trail keeps its `---` a real rule rather than a setext heading.
> - `run.py` restates two contracts the new build changed. `--wait-skill-extract` is inert upstream now; `--flush-skill-buffer` is live again and blocks on one request bounded by `flush_timeout_s`, still passed on every turn because a lone `-m` turn trips no boundary and the alternative is not "promote later" but "never".
> - `subagent.json` says the first reply is normally questions rather than the report. That string is what the dispatching model reads, and both ways a caller gets it wrong are silent.
> - `README.md` records the swap, the retired patch set, the verification that proves it, the ask_user calling contract, and corrects two stale claims the swap surfaced.
>
> Upstream changes worth naming for a reader: the everos write path is rebuilt (turn writes detach after 5s, deferred capture, dedup, 422 fixes); plugin config slices are validated and a memory identity mismatch is reported, both non-fatal; the test suite stops resolving the reply-language directive from the developer's own config, which had moved eight prompt-byte shas on a Chinese host.

## Type of change

- [ ] Bug fix
- [x] New feature
- [ ] Document
- [ ] Others

## Related issues (if there is)

> Fix #1

## Checklists

### Development

- [x] Lint rules pass locally
- [x] Application changes have been tested thoroughly
- [x] Automated tests covering modified code pass

## Risk

User-visible behaviour changes, and how to roll back:

- The research agent now asks clarifying questions on its first turn (`askUser.mode: "first_turn"`), and its first reply to a caller is questions rather than the report. Callers must relay the questions and send the answers back to the same instance. Roll back by disabling `drFlow.askUser` in `config.json`.
- The research trail (queries, pages read) is appended to the reply the host shows, where it previously was not.
- `--flush-skill-buffer` now blocks on one promotion request per turn; a slow everos costs the turn the adapter's `flush_timeout_s` (bounded by the launcher's `--timeout`).
- `drFlow.version` moved down to `dr@3.4-filetools-askuser`; older folded labels are rejected by the loader, so reverting `config.json` alone would not load on the new tree (and vice versa). The full rollback is the previous vendored tree plus its `config.json`, both recoverable from this repo's history.

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

### Security

- [ ] Security impact of change has been considered
- [ ] Code follows company security practices and guidelines

### Code review

- [x] Merge request has a descriptive title and context useful to a reviewer. Screenshots or screencasts are attached as necessary
- [ ] Merge request linked to JIRA ticket when applicable
## Summary

Move fixture and live session rows behind the sessions data source. Route session opening and counting through that source, keep the live boot skeleton hold inside the rail store, and remove the final shared session writes plus three shell verbs.

The shared-coupling ratchet now reports zero live writes to unowned bindings, two held legacy containers, and 53 shell verbs.

## Type

- [ ] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [x] Refactor
- [ ] Other

## Verification

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [x] User-facing docs or screenshots are updated when needed

- `npm run --prefix ui type-check`
- `npm test --prefix ui` (47 files, 613 tests)
- `npm run gen:check --prefix ui`
- `npm run lint:i18n --prefix ui-tui`
- `npm run --prefix ui build`
- `python3 ui/build.py`
- `node ui/scripts/check-page.mjs`
- `node ui/scripts/count-shared-globals.mjs`
- `node ui/scripts/check-css.mjs`
- `uv run pytest tests/test_ui_language_repaint.py` (9 tests)
- `uv run --frozen --python 3.12 --extra dev python scripts/check_large_files.py glhttps/main..HEAD`
- `git diff --check`
- Changed-text control-byte sweep

No user-facing documentation or screenshot update is needed because DOM and class output remain frozen.

## Risk

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

The main risk is session switching or live boot rendering against stale fixture rows. Tests cover source-owned opening, skeleton hold and release, settings session counts, and parked turns. Reverting this merge restores the previous shared binding path.

## Related Issues

N/A
## Summary

Move the stable workspace record into the modern workspace store. Route live turn numbering, workspace reads, and parked-session snapshots through typed store methods, remove the stale `cmds` snapshot field, and retire the `wsState` shell verb.

The shared-coupling ratchet now reports zero live writes to unowned bindings, one held legacy container, and 52 shell verbs.

## Type

- [ ] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [x] Refactor
- [ ] Other

## Verification

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [x] User-facing docs or screenshots are updated when needed

- `npm run --prefix ui type-check`
- `npm test --prefix ui` (47 files, 617 tests)
- `npm run gen:check --prefix ui`
- `npm run lint:i18n --prefix ui-tui`
- `npm run --prefix ui build`
- `python3 ui/build.py`
- `node ui/scripts/check-page.mjs`
- `node ui/scripts/count-shared-globals.mjs`
- `node ui/scripts/check-css.mjs`
- `uv run pytest tests/test_ui_language_repaint.py` (9 tests)
- `uv run --frozen --python 3.12 --extra dev python scripts/check_large_files.py glhttps/main..HEAD`
- `git diff --check`
- Changed-text control-byte sweep

No user-facing documentation or screenshot update is needed because DOM and class output remain frozen.

## Risk

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

The main risk is losing workspace rows or turn numbering across a session switch. Tests cover the stable state identity, exact parked snapshot fields, restore behavior, queue and turn-phase round trips, and the workspace renderer. Reverting this merge restores the previous shared global path.

## Related Issues

N/A
## Summary

`raven acp` becomes an agent an editor can launch: it negotiates, mints and reloads sessions, runs a turn, streams the reply and its tool calls, asks before acting, and answers a cancel while a turn is in flight.

Six commits, each one concern and each buildable on its own (verified: every commit was checked out into its own worktree, imported, and its own tests run).

**The process model is a self-contained engine**, not a client of a resident gateway. That was the earlier preference and it does not survive the topology: an ACP agent IS a stdio child of the editor, so there is nothing to mount onto unless a gateway happens to be running, and an agent that only works when another service is up is not an agent an editor can launch.

Notes on the parts that are not obvious:

- Every inbound frame is handled in its own task, because `session/prompt` is suspended for as long as the turn takes and `session/cancel` has to be read during it. Shutdown answers instead of cancelling: on EOF the pending prompts are settled as cancelled and the handlers return through their own code, so a finite input (a scripted client, a smoke test) still gets its replies.
- A prompt is never answered with a JSON-RPC error. A failed turn is explained as message content and then ends with a stop reason. Measured from the other side on another implementation: an error in reply to a turn-shaped request makes clients tear the whole turn down.
- The schema is vendored and pinned, and its blindness is measured rather than assumed: `additionalProperties` appears 118 times and is true in every one, `method` is an unconstrained string, and `params` accepts null. Validating a frame against the union therefore accepts an invented method name, so the tests validate payloads against their own definition and check method names against the metaschema separately.
- Questions take one of two paths by what the client declared. With elicitation forms, a question with no preset answers can come back as text. Without them it rides a permission request and a synthesised tool call, which the payload marks as synthesised rather than dressing up as a real one. The case that has no answer at all, a free-text question to a client with no elicitation, is recorded in the compatibility matrix instead of being papered over.
- Model selection goes through `session/set_config_option`. `session/set_model` and `models.availableModels` appear in older material and do not exist in the schema.
- Secrets are redacted before anything reaches the client, including the value under a key whose name says it is a secret. A credential file's path is deliberately NOT redacted: a path is not a secret, and hiding it would hide that the agent read credentials at all.
- Six command families ask before running on this surface. What that does not reach is written down: a sub-agent builds its own tool with its own policy and no responder, so a delegated command still runs unprompted. Registering the matchers there would refuse those commands rather than ask about them, which is a different product decision and is left open rather than half-closed.

Two fields were already on the wire with no declaration on either side, and are declared here: `file_change` on `tool.complete` and `blocking` on `tool.start`. Neither is cosmetic, because both declarations forbid unknown fields, so a validating consumer drops the whole event rather than the unknown key. Two new gates close the hole the existing union test left: one compares each variant's payload field names across the two declarations, the other validates every frame the outlet actually emits against the declared union. The second is what found `blocking`.

Known fidelity losses, all in `docs/specs/2026-08-21-acp-agent-compatibility.md`: `tool_call_update` always reports completed because the event carries no success flag; `rawInput` is not sent because it carries a command line verbatim; there is no plan because its priority field is required and has no source here.

Rebased onto current main (which moved nineteen commits during this work; the
rebase is clean and the whole suite was re-run on top of it). The two commits main gained while this was in review (the squashes of !164 and !166) made four of the original commits redundant; they were dropped, and the only thing main lacked from them, the process-group integration smoke test, is back as its own commit. Verified by diffing the rebuilt branch against the pre-rebase state: the only difference is 38 lines that main's reviewed version of !164 has and the local copy did not, so nothing was lost.

Two gaps that earlier rounds recorded rather than fixed are closed here, and
both were the same mistake from different angles: a check that looks present and
is not reached.

A delegated command ran unannounced, because the families a surface asks about
were registered on one tool while a sub-agent builds its own. They are declared
per surface now and every tool built under it inherits them. What a delegated
command gets is a refusal with a reason, not a prompt, because that tool has no
approval responder and a tool that cannot ask fails closed. A ContextVar carries
the declaration rather than a module global: measured, not preferred -- the
global version passed every test run alone and failed the file, because any test
that started a server changed the process for every test after it.

A sandbox made the safer configuration ask LESS. The gate skipped the whole
classification when the executor was sandboxed. A microVM does contain what a
command does to files, so the deny list and the contained families are still
skipped; it does not contain a push, an install, or a connection to another
machine, and those are exactly the families this surface exists to ask about.

One overlap worth naming rather than leaving to be found: `feat(ui): a turn ends
with the files it produced` on main renders a turn's files in the web
transcript. That reads a tool call's own metadata; the `media` event here is the
turn-level `MediaOut` the outlet used to eat. Different producers, and both were
concepts before this branch -- only the second had no wire event.

Reviewers: @chandler.zhang, @Blockchain-Key.

## Verification

- `uv run --frozen --all-extras pytest -q`: see the run recorded below.
- `uv run --frozen python scripts/coverage_gate.py diff --base-ref origin/main`: see below.
- `uv run --frozen python scripts/coverage_gate.py ratchet`: see below.
- `uv run --frozen ruff check raven/ tests/` and `ruff format --check`: clean.
- ACP integration suites (stdio smoke, adversarial, self-verification, tool cancellation) run with `-m ""`.
- `raven/acp/` unit coverage: 100 percent line and 100 percent branch.
- Front-end gates for the wire-contract commit, run against a pinned toolchain: `npm run lint:rpc` reports generated.ts in sync, `npm run type-check` exits 0, `npm test` passes 102 files and 1353 tests.
- Every new test was checked to fail without its fix, including the two new contract gates.

## Risk

The user-visible additions are a new CLI surface (`raven acp`) and three optional wire fields plus one new event variant, all defaulted, so a client that ignores them behaves as before.

The change with the widest reach is the stderr log sink no longer printing local variables. It is one line, and the reason it is in this branch is that an editor speaking a protocol over stdout surfaces the agent's stderr to the user, so the leak has an audience here.

Shell approval gains six command families, but only for a surface that registers them. The terminal registers none and behaves exactly as before.

Rollback is a plain revert of the branch: no data migration, no persisted format change.
## Summary

Defer the shared page boot until the assembled script has installed every live data source. Offline fixture mode uses the same deferred boot, while live mode claims it synchronously and queues the first data-driven paint from the final live manifest part.

Remove the last live mutation of demo-owned storage. The shared-coupling ratchet now reports zero live writes to unowned bindings, zero held legacy containers, and 52 shell verbs.

The rebase onto the latest main exposed an incomplete generated web RPC client in that new base. Regenerate it from the merged OpenRPC schema so the branch and main contract gate include the new blocking tool call, file change, and media event types.

## Type

- [ ] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [x] Refactor
- [ ] Other

## Verification

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [x] User-facing docs or screenshots are updated when needed

- `npm run --prefix ui type-check`
- `npm test --prefix ui` (48 files, 620 tests)
- `npm run gen:check --prefix ui`
- `npm run lint:i18n --prefix ui-tui`
- `npm run --prefix ui build`
- `python3 ui/build.py`
- `node ui/scripts/check-page.mjs`
- `node ui/scripts/count-shared-globals.mjs`
- `node ui/scripts/check-css.mjs`
- `uv run pytest tests/test_ui_language_repaint.py` (9 tests)
- `uv run --frozen --python 3.12 --extra dev python scripts/check_large_files.py refs/fdceb/main..HEAD`
- `git diff --check refs/fdceb/main..HEAD`
- Changed-text control-byte sweep

The design inventory and turn-machine notes now record the corrected boot order. The frozen page DOM and stylesheet are unchanged.

## Risk

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

The main risk is leaving the live page unbooted or opening an absent fixture session. Dedicated boot-order tests cover offline fixture boot, live ownership of the first paint, an empty initial live session source, and placement after every synchronous source installer. The generated RPC diff is deterministic output from the schema already on main. Reverting this merge restores immediate demo boot and the live cleanup path.

## Related Issues

N/A
## Summary

Move the shared duration formatter into the modern shell bundle and import it directly from composer, transcript, subagents, and dag. Reuse the existing title formatter directly from subagents instead of routing both pure helpers through RavenShell.

Move the gateway host platform onto DS.workspace, where the host-side reveal and open-in-app actions already live. Retire the three corresponding shell verbs and reduce the shared-coupling ratchet from 52 to 49 verbs while the other two axes remain zero.

## Type

- [ ] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [x] Refactor
- [ ] Other

## Verification

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [x] User-facing docs or screenshots are updated when needed

- `npm run --prefix ui type-check`
- `npm test --prefix ui` (49 files, 621 tests)
- `npm run gen:check --prefix ui`
- `npm run lint:i18n --prefix ui-tui`
- `npm run --prefix ui build`
- `python3 ui/build.py`
- `node ui/scripts/check-page.mjs`
- `node ui/scripts/count-shared-globals.mjs`
- `node ui/scripts/check-css.mjs`
- `uv run pytest tests/test_ui_language_repaint.py` (9 tests)
- `uv run --frozen --python 3.12 --extra dev python scripts/check_large_files.py refs/fdceb/main_shell..HEAD`
- `git diff --check refs/fdceb/main_shell..HEAD`
- Changed-text control-byte sweep

No user-facing documentation or screenshot update is needed. Duration wording, title stripping, page DOM, class names, and the frozen stylesheet remain unchanged.

## Risk

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

The main risk is changing compact duration boundaries or using the browser platform for gateway-side actions. Unit tests pin the duration boundaries, island tests now exercise the production formatter instead of test-only shell fakes, and both fixture and live workspace sources expose the same gateway platform closure used before. Reverting this merge restores the three shell verbs.

## Related Issues

N/A
## Summary

Move composer draft persistence, debounce, and session ownership into the composer island. Preserve the new-draft claim when the first message creates a real session, and let the rail delete drafts through their owner instead of a legacy shell verb.

Move uploaded image preview bytes into a narrow modern cache shared by composer and transcript. Retire six shell verbs and reduce the shared-coupling ratchet from 49 to 43 while the other two axes remain zero.

## Type

- [ ] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [x] Refactor
- [ ] Other

## Verification

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [x] User-facing docs or screenshots are updated when needed

- `npm run --prefix ui type-check`
- `npm test --prefix ui` (49 files, 626 tests)
- `npm run gen:check --prefix ui`
- `npm run lint:i18n --prefix ui-tui`
- `npm run --prefix ui build`
- `python3 ui/build.py`
- `node ui/scripts/check-page.mjs`
- `node ui/scripts/count-shared-globals.mjs`
- `node ui/scripts/check-css.mjs`
- `uv run pytest tests/test_ui_language_repaint.py` (9 tests)
- `uv run --frozen --python 3.12 --extra dev python scripts/check_large_files.py refs/fdceb/main_before_composer_push..HEAD`
- `git diff --check refs/fdceb/main_before_composer_push..HEAD`
- Changed-text control-byte sweep

No user-facing documentation or screenshot update is needed. The page DOM, class names, draft storage key and limit, debounce timing, attachment note format, and frozen stylesheet remain unchanged.

## Risk

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

The main risk is filing unsent text under the wrong conversation or losing the in-memory image preview after upload. Focused tests cover debounce timing, session switching, new-session claims, explicit deletion, send cleanup, upload caching, and transcript rendering. Reverting this merge restores the six legacy shell verbs.

## Related Issues

N/A
## Summary

Move the main transcript tail-follow state into the transcript feature. Let transcript rendering and composer scrolling share that owner directly, while the legacy layers keep only a narrow RavenIslands adapter for their remaining calls.

Route composer notes directly to the transcript and pane drag measurements directly to the composer. Retire five shell verbs and reduce the shared-coupling ratchet from 43 to 38 while the other two axes remain zero.

## Type

- [ ] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [x] Refactor
- [ ] Other

## Verification

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [x] User-facing docs or screenshots are updated when needed

- `npm run --prefix ui type-check`
- `npm test --prefix ui` (50 files, 627 tests)
- `npm run gen:check --prefix ui`
- `npm run lint:i18n --prefix ui-tui`
- `npm run --prefix ui build`
- `python3 ui/build.py`
- `node ui/scripts/check-page.mjs`
- `node ui/scripts/count-shared-globals.mjs`
- `node ui/scripts/check-css.mjs`
- `uv run pytest tests/test_ui_language_repaint.py` (9 tests)
- `uv run --frozen --python 3.12 --extra dev python scripts/check_large_files.py refs/fdceb/main_before_tail_push..HEAD`
- `git diff --check refs/fdceb/main_before_tail_push..HEAD`
- Changed-text control-byte sweep

No user-facing documentation or screenshot update is needed. The page DOM, class names, instant tail scroll, note rendering, pane bounds, and frozen stylesheet remain unchanged.

## Risk

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

The main risk is forcing the transcript back to the tail while the reader is reviewing earlier output, or failing to update the composer offset during a pane drag. Focused tests cover the shared stuck state, instant scroll behavior, pill visibility, direct transcript notes, and per-frame dock measurements. Reverting this merge restores the five legacy shell verbs.

## Related Issues

N/A
## Summary

A direct chat with Claude Code over ACP now renders its tool calls under Claude
Code's own tool names, so the transcript reads as a Claude Code conversation
rather than a generic one. This matches the choice already shipped for codex,
and for the same reason: knowing which agent ran a call is worth more than one
vocabulary shared across transports.

**The read boundary stops renaming.** `tool_vocabulary.RAVEN_NAME` mapped twelve
claude-agent-acp names into Raven's own (`Bash` to `exec`, `Glob` to `find`) on
the way to a client. Those twelve entries are gone. The ACP spec-kind entries
above them stay, because the base dialect still reports `kind` and other
adapters are read through it. No migration is needed: the stored record always
held `Bash`, the rename happened on the way out, and a direct chat folds its
live and its settled turns through one function, so the two change together
and cannot disagree.

`ARGUMENT_KEY` gains eleven entries in exchange, one per name except `Task`,
each mapped to the key that name's old target already resolved to. Because
every value is the old resolution, the promoted arguments come out identical
and only the displayed name moves. Without it, `Glob` and `Grep` rows would
report the directory searched and lose the pattern.

**The TUI gains a verb table keyed by those names**, beside the codex one, so a
run of six reads still collapses to one line instead of falling to the generic
rule. Row shaping follows, because the shell-label branch, the path-clipping
set and the quoted-needle set were all keyed by Raven's names: `Bash` had to
join the first or a row would have regressed from two program names to a
40-character clip of the raw command.

**A Bash row now shows the model's own description of what it was for.** The
adapter sends Claude's `description` beside the command, and the row was
showing a label derived from the pipeline instead. It arrives as a new optional
`EpisodeTool.intent`, preferred inside the one function that both the folded
and the expanded subject reach, so the two cannot diverge. The expanded detail
block still shows the whole command.

**The web UI needed a fallback.** Its tool-renderer registry is keyed by
`Bash`, `Read`, `Write`, `Edit`, `Glob` and `Grep`, and a direct chat's
messages do reach it. Once the names stopped being renamed, `Read`, `Write` and
`Edit` began hitting renderers whose shared path helper read only `file_path`,
while the read boundary promotes that subject onto `path` -- so the path
vanished from the row, and the default renderer that used to print the
arguments no longer ran. The helper now accepts both, `file_path` first, since
the web UI's own agent sends that spelling. Reworking those renderers to speak
Claude Code's vocabulary the way the TUI's tables do is left out of scope.

`TodoWrite` never emits a tool call at all: the adapter routes its state to a
plan frame, so the plan row is now named for the tool behind it, symmetric with
codex's.

## Type

- [ ] Fix
- [x] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

All run after rebasing onto the current `main`:

- `uv run pytest -q` -- 10766 passed, 0 failed, 50 skipped, exit 0
- `npm test --prefix ui-tui -- --no-file-parallelism` -- 106 files, 1505
  passed, 0 failed, 13 skipped
- `npm run type-check --prefix ui-tui` -- clean
- `npm run lint --prefix ui-tui` -- 0 errors
- `scripts/check_commit_messages.py origin/main..HEAD` -- exit 0
- `make check-large-files` -- clean
- `make lint-python` -- 3 errors, in `raven/agent/tools/deliver.py`,
  `raven/cli/tui_commands.py` and `tests/test_channels_outlet.py`. All three
  are byte-identical to `main` on this branch, so the failures are inherited
  rather than introduced.

`--no-file-parallelism` is not optional for the TUI suite: it flakes under
default worker parallelism above roughly a hundred files, and a flake there
cannot be told apart from a real break.

Six existing assertions encoded the vocabulary this change removes, and were
migrated. Only their name assertions changed; every argument assertion in them
still passes exactly as written, which is what shows the re-keyed
`ARGUMENT_KEY` preserved behaviour rather than merely appearing to.

Evidence for the frame shapes is 224 frames captured from real sessions plus
the adapter's own published source. Only `Bash` and `Read` were exercised on a
live wire; every other name's input shape is read from the adapter's
`toolInfoFromToolUse` switch, which ships unminified, and the argument tests
are written to those declared shapes. The design doc separates what was
measured from what was derived, and says which is which per name.

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [x] User-facing docs or screenshots are updated when needed

## Risk

Every tool row in a claude_code direct chat changes its verb. `read 6 files`
becomes `Read 6 files`; a shell row becomes `Bash` plus the model's own
description where one was sent, and the derived program label where it was not.
A nested Claude Code subagent call no longer borrows the `delegated` verb and
reads as `Task`. Already-stored sessions render the new way on resume, because
the rename was never in the record -- there is no migration and no mixed state.

Deliberately not covered: the `interrupted` flag the adapter sends alongside a
command's output is left unread. It appeared zero times across the 224 captured
frames, so its shape on a real interruption is unknown, and handling it would
mean untested code written against a guessed shape.

Rollback is a plain revert. Restoring the twelve `RAVEN_NAME` entries returns
every row to Raven's vocabulary; the TUI table then simply goes unconsulted
rather than breaking, and the web UI fallback is independent and safe to keep
either way.

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

## Related Issues

N/A
## Summary

Move edit, write, and unified diff row construction into the workspace feature. Let the legacy workspace adapters and the transcript call renderer share the same typed implementation.

Route delegated run stage rendering through DS.agents directly instead of sending it through RavenShell and back to the same source. Retire four pass-through shell verbs and reduce the shared-coupling ratchet from 38 to 34 while the other two axes remain zero.

## Type

- [ ] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [x] Refactor
- [ ] Other

## Verification

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [x] User-facing docs or screenshots are updated when needed

- `npm run --prefix ui type-check`
- `npm test --prefix ui` (51 files, 630 tests)
- `npm run gen:check --prefix ui`
- `npm run lint:i18n --prefix ui-tui`
- `npm run --prefix ui build`
- `python3 ui/build.py`
- `node ui/scripts/check-page.mjs`
- `node ui/scripts/count-shared-globals.mjs`
- `node ui/scripts/check-css.mjs`
- `uv run pytest tests/test_ui_language_repaint.py` (9 tests)
- `uv run --frozen --python 3.12 --extra dev python scripts/check_large_files.py origin/main...HEAD`
- `git diff --check origin/main...HEAD`
- Changed-text control-byte sweep

No user-facing documentation or screenshot update is needed. The hunk row shape, context folding, line counts, delegated transcript DOM, page classes, and frozen stylesheet remain unchanged.

## Risk

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

The main risk is changing diff line numbers or failing to paint a delegated record. Focused tests pin edit context folding, write truncation, unified diff headers and numbering, and all three delegated record paths through the agents source. Reverting this merge restores the four shell pass-through verbs.

## Related Issues

N/A
## Summary

Move clipboard behavior into the modern shell bundle, read browser platform and language facts directly, and expose the running Raven version through `DS.settings`.

This retires `copyToClip`, `lang`, `appVersion`, `modKey`, and `isMac` from `RavenShell`, reducing the guarded shell surface from 34 verbs to 29 without changing the served DOM, class names, styles, or single-file build contract.

## Type

- [ ] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [x] Refactor
- [ ] Other

## Verification

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed

- `npm run --prefix ui type-check`
- `npm test --prefix ui` (52 files, 633 tests)
- `npm run gen:check --prefix ui`
- `npm run lint:i18n --prefix ui-tui`
- `npm run --prefix ui build`
- `python3 ui/build.py`
- `node ui/scripts/check-page.mjs`
- `node ui/scripts/count-shared-globals.mjs` (0 / 0 / 29)
- `node ui/scripts/check-css.mjs`
- `uv run pytest tests/test_ui_language_repaint.py` (9 passed)
- `uv run --frozen --python 3.12 --extra dev python scripts/check_large_files.py glhttps/main..HEAD`
- Changed-text control-byte sweep
- `git diff --check glhttps/main...HEAD`

## Risk

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

Clipboard refusal remains silent, the offline page still no-ops when the Clipboard API is unavailable, and the running version remains unknown until `system.version` answers. Rollback is the single squash commit.

## Related Issues

N/A

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
## Summary

Route every modern toast and context-menu caller directly to the existing `shell/toast` and `shell/menu` writers. Move `ToastAction` and `MenuItem` beside the writers that own those contracts, and remove both calls from `RavenShell`.

Tests that assert writer output now mock the owning writer modules. Other shell fakes drop the two required placeholders they no longer need. The served DOM, class names, styles, legacy bare-name callers, and single-file build contract remain unchanged. The guarded shell surface falls from 29 verbs to 27.

## Type

- [ ] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [x] Refactor
- [ ] Other

## Verification

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed

- `npm run --prefix ui type-check`
- `npm test --prefix ui` (52 files, 633 tests)
- `npm run gen:check --prefix ui`
- `npm run lint:i18n --prefix ui-tui`
- `npm run --prefix ui build`
- `python3 ui/build.py`
- `node ui/scripts/check-page.mjs`
- `node ui/scripts/count-shared-globals.mjs` (0 / 0 / 27)
- `node ui/scripts/check-css.mjs`
- `uv run pytest tests/test_ui_language_repaint.py` (9 passed)
- `uv run --frozen --python 3.12 --extra dev python scripts/check_large_files.py glhttps/main..HEAD`
- Changed-text control-byte sweep
- `git diff --check glhttps/main...HEAD`

## Risk

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

The direct imports are the same functions already published for the concatenated layers. Existing writer unit tests continue to cover DOM behavior, and feature tests preserve assertions for notices, actions, and menu entries. Rollback is the single squash commit.

## Related Issues

N/A

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
## Summary

Move slash-command names and help text onto `DS.composer`, beside the command catalogue they describe. Route execution-reach labels through a small modern catalogue helper, then remove the four obsolete `RavenShell` verbs and the dead demo reach formatter.

The served DOM, class names, styles, live source overrides, and single-file build contract remain unchanged. The guarded shell surface falls from 27 verbs to 23.

## Type

- [ ] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [x] Refactor
- [ ] Other

## Verification

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed

- `npm run --prefix ui type-check`
- `npm test --prefix ui` (53 files, 635 tests)
- `npm run gen:check --prefix ui`
- `npm run lint:i18n --prefix ui-tui`
- `npm run --prefix ui build`
- `python3 ui/build.py`
- `node ui/scripts/check-page.mjs`
- `node ui/scripts/count-shared-globals.mjs` (0 / 0 / 23)
- `node ui/scripts/check-css.mjs`
- `uv run pytest tests/test_ui_language_repaint.py` (9 passed)
- `uv run --frozen --python 3.12 --extra dev python scripts/check_large_files.py glhttps/main..HEAD`
- Changed-text control-byte sweep
- `git diff --check glhttps/main...HEAD`

## Risk

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

Both modes already share the same slash catalogue and translation function. Focused tests cover every reach key, the unknown-key fallback, slash rendering, and the affected settings and skills views. Rollback is the single squash commit.

## Related Issues

N/A

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
## Summary

Replace the legacy workspace sidebar with a persistent floating desk and a responsive multi-pane workspace.

- Group registered agents with their dynamically sorted instances and preserve real backend registration types.
- Open diffs, files, and subagent conversations in up to four responsive panes without breaking legacy workspace entry points.
- Reuse the transcript and composer behavior for resumable subagent conversations, including optimistic messages and queued turns.
- Add draggable and resizable desk geometry, launcher anchoring and snap behavior, pane resizing, fullscreen controls, and responsive layout limits.
- Route transcript web links to the user's external browser and keep file navigation inside the active file pane.

## Type

- [ ] Fix
- [x] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

- `node ui/scripts/check-css.mjs` - passed.
- `npm run type-check --prefix ui` - passed.
- `npm test --prefix ui` - passed, 50 files and 627 tests.
- `npm run build --prefix ui` - passed.
- `make check-large-files` - passed.
- `git diff origin/main...HEAD --check` - passed.

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed

## Risk

The change replaces workspace presentation and routing while retaining compatibility adapters for existing sidebar actions. Rollback is the squash commit revert. No dependency or persistence schema changes are included.

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

## Related Issues

N/A
…hat shadow a folder

## Summary

A pull that updates `subagents/` needs `install.sh` run again, and nothing said so.
Three things go stale; the script now addresses all three, and says which one applied.

Two of them re-running already fixed. A folder an upgrade **added** has no venv, and a
checkout whose dependencies moved needs a sync. Both are now visible: each folder's line
carries the delta `uv sync` produced (`4 added, 1 updated`, or `no change`), and the
summary names the folders this run built for the first time. Neither needs an argument.
What was missing was any signal that re-running was necessary, which is why nobody did.
The delta is read from a captured stream because `uv` writes it to stderr, which the
existing `> /dev/null` never suppressed - a four-folder run already printed that chatter,
interleaved and attributed to nothing.

The third could not be fixed by re-running at all. A config row written by an older
`install.py` **outranks the folder's own manifest**, because the table takes the whole
stored row: a manifest the pull updated never reaches the roster, and a command baked
against a tree that has since moved keeps naming the old path. Every stored row naming a
folder in the tree is now listed with the fields that disagree, and `--prune-stale`
deletes them so discovery supplies the row instead.

Key decisions:

- **Reporting is unconditional; deleting is not.** A row here is the user's config. The
  ordering that moved registration out of this script - a first install runs it before a
  configured raven exists - is the same one that makes a config write unsafe to assume,
  so the default path still writes nothing outside the tree and the flag is the whole
  consent. With no host raven to read the config through, the step is skipped rather than
  failed, and it never creates a config file that was not already there.
- **The match is by name, and a name match cannot tell an outdated snapshot from a row
  someone tuned by hand.** Rather than guess, both consequences are reported per row: a
  row edited in the web UI is deleted along with the rest, and a row carrying
  `enabled: false` is the only place that switch lives - a purely discovered row cannot be
  switched off, so deleting one re-enables that agent. Neither warning is suppressed by
  the flag.
- **Both sides of the comparison go through the schema.** `recommendedLlm` annotates the
  installer and is not a config field, so the schema drops it and a stored row can never
  carry one; comparing raw would have reported it as differing on every row, on every run,
  forever.
- **The backup is the rollback, so it is taken from the bytes on disk.** The write path
  expands a stored row to every defaulted field, so a copy taken through it would not
  restore what was there. It is written before any deletion, and opened at `0600` rather
  than chmodded afterwards, because an `openai` row among these carries its own api key.
- **A requested prune that could not run exits non-zero.** The flag names an action, and
  reporting exit 0 after declining to perform it tells a scripted caller the opposite of
  the truth. Plain reporting stays advisory and still exits 0.

## Type

- [ ] Fix
- [x] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

At the current head:

```
uv run pytest -q -p no:randomly
-> 10779 passed, 51 skipped, 13 deselected, 0 failed
```

**The first pipeline was red, and it found a real hole in this suite.** One test failed
with `install.sh: line 70: python3: command not found`. The narrowed `PATH` that makes
`command -v raven` find nothing also removed the `python3` the script's own helpers run,
and the suite borrowed one back from `/usr/bin`; the CI image ships none, because `uv`
brings its own interpreter. Every local run was green for exactly that reason, which is
the isolation failure this repo keeps filing - a suite reading the machine rather than
building what it needs.

Two tests were wrong in the same way and had been passing anyway: they name an
interpreter that runs and cannot import raven, pointed at `/usr/bin/python3`, so on a
machine without that file they exercised "the interpreter does not exist" while asserting
against the message for a different condition. Both now use a shim.

Reproduced rather than reasoned about. With `/usr/bin` and `/bin` mirrored into a
directory holding every tool except `python*`, the pre-fix suite fails exactly as CI did
and passes with the fix restored:

```
run A (mirrored PATH, fix reverted) -> 1 failed, 16 passed, 1 skipped
                                       the same test, the same message as CI
run B (mirrored PATH, fix restored) -> 17 passed, 1 skipped
```

`main` was green on all six of its most recent pipelines, including this branch's base, so
the red was this branch's and was not read as inherited.

```
uv run ruff check tests/test_subagents_install_script.py subagents/  -> All checks passed!
uv run ruff format --check tests/test_subagents_install_script.py    -> 1 file already formatted
bash -n subagents/install.sh                                         -> clean
uv run python scripts/check_commit_messages.py origin/main..HEAD     -> exit 0
make check-large-files                                               -> exit 0
```

The script had no test at all before this. The new suite drives it under `bash` against a
tree, a host config and a `uv` built for the test, and pins `RAVEN_HOME` on every run
including the runs meant to find no host raven: the step deletes rows from the host's
config, so a test that fell through to the developer's own would delete their agents.

**Proof the tests are load-bearing, not theatre.** Every test was watched failing before
the code existed (9 red, 5 vacuously green). The 5 that could only pass vacuously were
then checked by mutation - each guard removed on its own, and the matching test observed
going red:

| mutation | test that went red |
|---|---|
| `mode` forced to `prune` | a colliding row survives a run without the flag |
| collision filter removed | a row no folder claims is not reported |
| `--dry-run` ignored | dry run refuses to prune |
| backup taken with no collisions | pruning nothing writes no backup |

End to end against the real four folders and a copy of a real host config: 4 vendored rows
deleted, 4 unrelated rows kept, the backup byte-identical to the pre-state, and the real
config untouched.

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [x] User-facing docs or screenshots are updated when needed

`subagents/README.md` gains an `After a pull` section covering all three cases and both
costs of the name match, and its exit-status sentence is corrected for the new condition.

## Risk

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

The user-visible change on a plain run is additional output: a per-folder sync delta, a
`newly built` summary line, and a report of the config rows that shadow a folder. No
config is written without `--prune-stale`.

With the flag, rows are deleted from `~/.raven/config.json`. That is the point, and it is
also the irreversible part: the deletion takes any web-UI edit on those rows with it, and
re-enables an agent whose stored row was the only thing switching it off. Both are reported
per row before anything happens, and the previous list is backed up beside the script
first. Restoring that file is the rollback.

Security: the backup can hold an `openai` entry with its own api key. It is created at
`0600` rather than chmodded after the write, and lands in a directory the repo already
ignores. Nothing else new reaches disk, and no route, RPC or other network surface is
added or widened.

Two callers run this script from inside raven (`subagents.build` over RPC, and onboarding).
Neither passes `--prune-stale`, so a build triggered from the page reads the config and
does not write it. Both read the last line of the script's output as an error detail when a
build fails; that line was the summary before this change and still is, so it has not moved.

Known gaps, disclosed rather than left to be found:

- The test that proves an unwritable backup aborts the prune skips as root, which is how
  this suite already handles permission cases. That path was instead verified by hand
  against an unwritable destination: the specific message appeared, the config kept all 8
  rows, and the generic one was correctly not printed over it.
- The `uv` delta parser is exercised only against a stand-in whose output format was
  sampled from real `uv`. A future change to that format would go unnoticed.
- The report lists `command` and `resumeCommand` as differing whenever the stored row was
  written against a tree at another path. That is correct - it is the case
  `_launcher_is_gone` exists for - but it means running the script from a copy of the tree
  reports every row.
- Three files carry a `ruff` I001 that predates this branch and is not fixed here, because
  the fix would widen the diff into unrelated files: `raven/agent/tools/deliver.py`,
  `raven/cli/tui_commands.py` and `tests/test_channels_outlet.py`. Each was confirmed
  failing on `origin/main` itself. The pipeline runs only the unit suite, so nothing
  catches them before the monthly GitHub PR.

## Related Issues

N/A
## Summary

Two changes, one MR because they were found together and touch the same tests: a
production bug where `ps` output is truncated by `COLUMNS`, and a test-suite
speedup that cuts the per-test timeout budgets.

### Bug: `ps` truncates the command line it is asked to read

`ps -o command=` truncates to `$COLUMNS`, which defaults to 80 when the
environment variable is unset - and it is unset in every context that calls
this, because none of the three readers has a terminal. All three look for a
marker that sits at the *end* of a command line, behind an absolute interpreter
path, so past column 80 the marker is gone and each one answers the opposite of
the truth.

Measured on the machine this was found on, against the live everos server
running under a stock `uv tool install` layout: the command line is 129
characters and its marker starts at column 83. With `-ww`: `_is_everos_server`
returns True. Without: False, and `ps` returns exactly 80 characters.

- `_is_everos_server` guards the signal path. When it wrongly returns False,
  `find_recorded_server` also fails its second check (the resolved root is past
  column 80 too), so raven reports "no server" while one is running. The
  dangerous direction: the caller concludes nothing is there and starts a
  second one.
- `_cmdline_of` promises "the full command line"; truncation makes that false
  rather than merely imprecise.
- the tracing viewer checks for `server.js` at the end of a node argv, so
  `_stop_viewer` cannot recognise the process it is meant to stop.

`-ww` applied to all three. The existing regression test had been passing for a
reason that had nothing to do with the code: the marker's column depends on
where the checkout is, so it was green in a shallow clone and red in a git
worktree, whose extra path segments pushed the marker past column 80. It now
pins `COLUMNS` and pads the command line so it exercises the truncation
wherever it runs, and it was confirmed to go red with `-ww` removed and green
with it restored.

### Tests: stop paying a full second for budgets that exist to be exceeded

These cases assert that a budget ran out. The work they race against outlasts it
by design (a 5s sleep versus a 1s timeout, a stub that never answers), so the
budget's size buys nothing, and shrinking one makes the case both cheaper and
more certain: the outcome under test is the timeout winning, and a smaller
budget gives the thing that must not happen less room, not more.

The largest single cost was not a budget. `cli_dispatch` runs the command
through `asyncio.to_thread`, and a thread cannot be cancelled - so when the
dispatch times out the command runs on, and the loop's shutdown joins it. The
10-second sleep in the fake `slow` command was therefore paid in full by the
teardown, 9.7s of it after the assertion had already passed. The sleep is now
1s against a 0.05s budget. This is a rare F1 near-miss where the giant teardown
was the actual defect.

Measured on the eight files where a cut actually lands: 64.3s -> 31.5s.

A ninth file was in the first revision and is not any more. Two ACP cases had
their `ready_timeout_ms` lowered from 1000 to 50, and `verify_agent` computes
`budget = max(1.0, ready_timeout_ms / 1000)` - so both values produce the same
one-second budget and the cases still cost 1.01s and 1.03s. Reverted rather than
made to work: reaching under that floor means extracting an inline `max(1.0, ...)`
into something a test can patch, which is a production change for two seconds and
unrelated to this branch. Leaving them in would have been worse than not having
them, because they read as if that config value governed the cost of those tests.

## Type

- [x] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

At the rebased head:

```
uv run pytest -q -p no:randomly
-> full suite: 10766 passed, 50 skipped, 13 deselected, 0 failed
uv run ruff check raven/ tests/ (my changed files)   -> clean
uv run ruff format --check (my changed files)         -> clean
make check-large-files                                -> exit 0
uv run python scripts/check_commit_messages.py         -> exit 0
```

**Reproduced before claiming fixed.** Two lockstep checks:

1. The regression test is load-bearing: with `-ww` removed from
   `_is_everos_server`, `test_ps_recognises_a_process_whose_command_line_says_everos`
   goes red; restored, it goes green.
2. The bug was provable on main's behaviour: against the live everos PID,
   `ps -p <pid> -o command=` returned 80 chars and the marker was absent; the
   same process with `-ww` returned 129 chars and the marker was present.

**Flake-proofing is measured, not asserted.** Attempting to shrink these
budgets could buy a flaky suite if the timeout stops winning the race against
the work under starvation, so the shrunk cases were run pinned to one core with
a competitor pinned to the same core - a harsher model of the contended
single-core `tests` runner CI uses:

```
30 runs of the shrunk cases  -> 0 failures  (38 selected per run)
3 runs of all nine files     -> 0 failures
```

Those runs were taken before the two ACP edits were reverted, so they cover a
superset of what ships: the reverted cases were among the 38 and passed in every
one of the 30 runs at their original one-second budget, which is the value they
are back to.

The 08-18 baseline for this suite (same files, old budgets, same machine) was
64.26s; the new one is 31.47s. The historical-version timing used `git archive`
of `a3e34fac` with the current venv, run on the same box.

After reverting the two no-op ACP edits the same nine files measured 33.4s
against 32.4s with them in place, which is inside the run-to-run noise and is the
evidence that those edits saved nothing.

## Risk

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

**Bug fix:** no user-visible behaviour change except that raven now recognises
its own everos server and the tracing viewer's stop command, when the command
line is longer than 80 characters. That is the correct behaviour; the previous
behaviour was wrong only in that it kept a second server from being started
when it should have. Rollback is the revert - the three diffs touch only the
`ps` argv.

**Test speedup:** the only production change (beyond the bug fix) is in the
test file `tests/test_rpc_cli_dispatch.py`'s `slow` command sleep (10s -> 1s).
A 1s sleep still outlasts the 0.05s budget by 20x, and the 30-run starvation
test confirms the race still resolves correctly. `sleep 1.0` in a test is
already an acknowledgement that a test may outlast its own timeout, so the
shrink is not a race-risk.

**Deliberately not shrunk**, because for these the direction is reversed and a
smaller budget buys flakiness:

- `test_live_child_still_gets_the_full_budget` asserts a probe count derived
  from the budget and the poll interval, so the budget is the subject rather
  than overhead. Its docstring says not to cut it short.
- the reparented-child case needs its launcher to reach the line that writes
  the pidfile before the timeout kills the process group.
- `wait_for(read_recv.receive(), ...)` asserts that a message ARRIVES inside
  the budget, which is the one shape where a tighter bound is a worse test.
- two `ensure_everos_server` calls raise before the poll loop starts, so their
  budget is never spent and changing it would be diff noise. (Three calls were in
  fact left at 1.0: those two, plus the one on `test_live_child_still_gets_the_full_budget`,
  which is the budget-is-the-subject case above.)

## Related Issues

N/A

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
…essions

## Summary

Serve the built-in generic agent (the `Raven` row) over ACP and complete the
ACP server's session surface so raven-as-agent is usable end to end.

- A config row of a seed's name with `kind: "acp"` redeclares the built-in
  generic agent over this raven's own `raven acp`: command and RAVEN_HOME are
  resolved at materialization (config stays machine-portable), the write guard
  and web RPC list/toggle follow the same rule (cli/openai rows still cannot
  claim a seed name), and the acp row is editable like any config row.
- The ACP server implements `session/resume` and declares
  `sessionCapabilities.resume`, which is what a raven acting as its client
  reads to report the row resumable - an instance handle then continues the
  session instead of starting fresh.
- `session/close` and `session/delete` drop a session from the connection and,
  for delete, from the engine's store; a turn still running is cancelled, and
  unknown ids are refused with -32002 (delete also handles lazy sessions that
  were minted but never prompted).
- Tool rows carry the emit site's verdict: ToolResult/ToolOutput gain an `ok`
  field (exec reports exit_code, policy refusals report false), ToolEvent /
  RpcOutlet / the TUI and web wire contracts pass it through, and
  `tool_call_update` reports `failed` instead of always `completed`.
- `session_info_update` (auto title, announced once per change) and
  `available_commands_update` (a deliberate white-list of six user-facing
  actions, announced per session) notifications are served.
- ACP capability snapshots are backfilled at startup: a background,
  sequential verification of enabled acp rows with missing or stale snapshots,
  so a fresh install reports stateful instead of hiding the agent from the
  instance picker until someone runs a Test. Skipped on the acp channel itself
  to avoid a verify cascade.
- The ACP compatibility matrix documents all of it.

Review round one landed five fixes in five commits: the snapshot backfill now
refreshes the materialized table after recording (and defers on mounted stacks
that have no registry to refresh), `session/delete` cancels a running turn
before touching the store, MCP maps `isError` into the result verdict, and the
native TUI consumes the verdict on its live path.

Scope is Python runtime + generated client types only; the two refactor
commits already on main (ui/ and ui-tui/ changes) are not touched.

## Type

- [ ] Fix
- [x] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [x] User-facing docs or screenshots are updated when needed

- `uv run pytest tests/test_acp_*.py tests/test_subagent_*.py tests/test_update_subagents.py tests/test_rpc_subagents.py tests/test_rpc_spine.py tests/test_rpc_bootstrap.py tests/test_cli_gateway_page.py tests/test_mcp_client.py` -> 1697 passed, 1 skipped at the review-round head; the two files the reviewer's run pointed at pass
- `uv run pytest tests/test_acp_methods.py tests/test_rpc_spine.py tests/test_rpc_contract_shapes.py tests/test_acp_updates.py` -> 388 passed (post-rebase sweep)
- `uv run ruff check` on the changed files -> clean
- `npm run gen:check --prefix ui` -> generated client matches the contract (gen:rpc run for ui-tui)
- `make lint` reports three pre-existing I001 import-order findings in files
  this MR does not touch (`raven/agent/tools/deliver.py`, `raven/cli/tui_commands.py`,
  `tests/test_channels_outlet.py`); all changed files pass ruff.
- E2E against the installed `raven acp` (real provider, temp RAVEN_HOME): 29/29 protocol checks pass, including handshake capabilities, command menu, tool failed flag, permission ask-and-reject, cancel, load/resume with context continuation, close/delete lifecycle, and multi-session coexistence

- The pre-submit sweep ran against `origin/main...HEAD` and produced two
  follow-up fixes, landed in one commit: the startup snapshot backfill task is
  now held by reference (asyncio keeps only a weak reference, so an unrefed
  task can be collected mid-run), and the web transcript prefers the emit
  site's `ok` verdict over the text heuristic, kept as the backstop for an old
  server that does not send the field. Verification below was re-run at that
  head.

Checked and deliberately not fixed:
  - The web and tui transcripts do not render a distinct failed row yet other
    than the text the error preview already carries; this MR makes the verdict
    available on the wire and generated client types, and leaves the visual
    treatment to the frontend work that owns it.
  - `session/set_mode`, `logout`, audio, `plan`, fs/terminal client features and
    per-session MCP servers remain unsupported per the compatibility matrix;
    this MR closes session lifecycle, resume and failure visibility, not the
    whole protocol surface.
  - A failed verify in the startup backfill leaves the row stateless (listed,
    not hidden) until a later Test or probe=true listing; making the backfill
    retry later is a deliberate deferral.
## Risk

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

The generic agent's default dispatch changes only when config redeclares it
as acp; without that row everything behaves as before (the seed row and its
in-process transport are unchanged, and old code ignores such a row with a
warning). Instance handles for the built-in row are now ACP-backed, so their
statefulness comes from a capability snapshot that the startup backfill
produces - a missing snapshot leaves the row listed but stateless, never
unusable. RAVEN_HOME is inherited by the child engine by design, so an acp
redeclaration runs under the same config and provider as the host.

## Related Issues

N/A

Co-authored-by: Claude (deepseek-v4-flash-vision-exp) <noreply@anthropic.com>
## Summary

Move the served front end's appearance preference and desktop-notification preference into modern shell owners. Preserve the existing localStorage keys, default-light first frame, document dataset contract, native desktop theme message, background-only notification rule, and forced settings test notice.

The concat boot and live turn layers keep their established `lookLoad` and `ntfPush` names as thin shims into the modern bundle. Remove the obsolete appearance fields from demo state and the `look`, `ntf`, and `themeSet` entries from `RavenShell`. The served DOM, class names, styles, and single-file build contract remain unchanged. The guarded shell surface falls from 24 verbs to 21.

## Type

- [ ] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [x] Refactor
- [ ] Other

## Verification

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed

- `npm run --prefix ui type-check`
- `npm test --prefix ui` (55 files, 644 tests)
- `npm run gen:check --prefix ui`
- `npm run lint:i18n --prefix ui-tui`
- `npm run --prefix ui build`
- `python3 ui/build.py`
- `node ui/scripts/check-page.mjs`
- `node ui/scripts/count-shared-globals.mjs` (0 / 0 / 21)
- `node ui/scripts/check-css.mjs`
- `uv run pytest tests/test_ui_language_repaint.py` (9 passed)
- `uv run --frozen --python 3.12 --extra dev python scripts/check_large_files.py glhttps/main..HEAD`
- Changed-text control-byte sweep
- `git diff --check glhttps/main...HEAD`

## Risk

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

Focused tests cover stored and malformed appearance preferences, every document dataset field, the effective system theme sent to the native host, direct theme toggles, notification persistence, permission and focus gates, the forced test notice, and both Settings page connections. Rollback is the single squash commit.

## Related Issues

N/A

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
…ts trace

## Summary

A running sub-agent DAG node now shows what it is doing, in the TUI transcript, without a click.

Under each running node's row sits one line carrying the tail of its sub-agent's message
stream, refreshed while it works, so a reader watches the run rather than inferring it from a
spinner. Clicking the row (or the node's box in the graph) opens a fixed-height box holding
the node's conversation trace, drawn by the transcript's own message renderer. Clicking again
returns to the line. `/dag <node>` gained the same trace, printed in full.

The whole feature is client-side. `dag.node` already returned a running node's transcript from
the live activity the runner publishes, and the ACP collector republishes the whole transcript
on every frame, so a read is a complete snapshot rather than a delta. No backend change, no
schema change, no client regeneration.

Three decisions worth stating, because each one is a constraint the rest hangs off:

- **The box is a constant height, always.** Its footer is drawn whether or not anything was
  cut, and a short trace is blank-padded. A box that grew as a run produced messages would
  shove every row beneath it several times a second, and the transcript's height model could
  then not state a panel's height without folding the trace first, while the fold would need
  the height. Fixing the height cuts that knot.
- **The stream line is a character tail, not a step ticker.** It shows the last line's worth of
  whatever the agent is producing, so it visibly moves; a structured one-step-per-line view
  would sit still through a long tool call and cost a row per node.
- **What a click opens widened.** A node used to expand to its prompt template, so a node
  whose call carried no template was not clickable. There is a trace now, so a node is
  expandable unless it is still pending with no template - the one case where a box would open
  onto nothing. The row and the node's box in the graph share one rule, so they cannot
  disagree.

A live run against real ACP agents drove out four defects that a green test suite did not,
and review a fifth. They are worth recording because they are one shape: a bound that was
correct for the state the code used to be in, kept after the state changed underneath it.

- The poll read its runs from the live turn state, which is cleared when the turn ends. A
  `run_subagent_dag` call returns as soon as it launches the run in the background, so a node
  routinely outlives its turn and the poll tore down after a single read. It now reads the
  runs pinned onto their tool rows as well, the live copy winning where both name a run.
- Polling a pinned run then exposed that a pinned run's status is a frozen copy, never revised.
  The poll therefore cannot stop on the displayed status; it stops on the status the server
  reports in the response, and gives up on a node after a bounded number of consecutive failed
  reads so a pruned run directory cannot be polled forever.
- That stop condition was itself wrong in a way worth naming: it asked whether the status was
  anything other than pending or running. `dag.node` reports a null status until the run writes
  its manifest, and a poll's first read of a node lands inside that window - so every node was
  marked finished on its first read. A negative exclusion list fails open; it is now a positive
  test over the four statuses that mean done.
- The line echoed the node's own prompt back until the agent spoke, because the server brackets
  a trace with the prompt. It now skips that row, so a node that has produced nothing says so.
- Review caught the last one: an expanded node was skipped once the store held any trace for it,
  which predated the status-based stop and was that stop's stand-in. A node expanded while it
  streams holds a trace by the time it goes terminal, so the read still owed at that moment -
  the one carrying the final answer - was suppressed and the box kept the last live snapshot.
  Leaving `running` rearms the poll, so one read per generation now bounds it and the response's
  terminal status settles it for good.

## Type

- [ ] Fix
- [x] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

Run at the branch head, after rebasing onto main:

- `cd ui-tui && npm test -- --no-file-parallelism` -> 110 files, 1584 passed, 13 skipped, 0 failed.
  The flag is required: these ink render tests flake under default worker parallelism at this
  suite size.
- `uv run pytest -q` -> 10783 passed, 51 skipped, 13 deselected, 0 failed. No Python changed;
  this is evidence of that rather than a claim.
- `cd ui-tui && npm run type-check` -> 0 errors. `cd ui && npm run type-check` -> 0 errors. The
  second is cheap insurance: it is the client that fails silently when a shared type widens.
- `cd ui-tui && npm run lint` -> 0 errors, 23 warnings. The 23 match the pre-change baseline;
  one is a react-compiler advisory on the poll's dependency-array suppression, which follows
  the same pattern an existing hook in this tree already uses.
- `cd ui-tui && npm run lint:rpc` -> generated.ts in sync.
- `scripts/check_commit_messages.py origin/main..HEAD` -> exit 0.
- `make check-large-files` -> exit 0.
- Driven live in the TUI against real ACP sub-agents: the tail moves through the node's own
  tool calls and their results, the line reads `working...` before the agent has produced
  anything, the poll stops when the node finishes, clicking opens the box on the tail of the
  trace and clicking again restores the line, and the node's box in the graph toggles the same
  slot.

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [x] User-facing docs or screenshots are updated when needed

Every new test was checked against its own absence: the fix was reverted, the test was watched
to fail, and the fix restored. That mattered here - the suite was green while the feature was
silently doing nothing, so a passing test is weaker evidence than usual on this change.

## Risk

Every DAG panel gains rows: one per running node, twelve for an expanded one. That is the
feature, but a live panel is taller than it was.

The poll issues one `dag.node` read per running-or-expanded node every 500ms, bounded by a
DAG's parallelism, over a local socket. Each response carries that node's transcript, capped
server-side. If it proves heavy the narrower request is a tail-only parameter, deliberately not
built up front.

Three gaps are known and deliberately not addressed here:

- A finished node keeps showing its stream line, because the pinned run's status is never
  revised once its turn closes. The poll stops correctly; only the row's appearance is stale.
  Fixing it means changing how a completed run reaches the pinned copy, which is outside this
  change.
- `npm run lint:i18n` reports the generated catalogue is stale. This reproduces on main at the
  merge base, so it is inherited rather than introduced, and fixing it would widen this diff
  into an unrelated area.
- The generated TypeScript for a DAG node's status omits `| null` while the Python model
  declares it optional. That mismatch is what made the third defect above easy to write: the
  type said null was impossible. Correcting it means regenerating the schema and both clients,
  which belongs in its own change.

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

No new surface, no secret, no persisted artifact: the trace store is in memory and is cleared
when the session resets. Rollback is per commit - the stream line, the trace box, the height
terms and the command change are independent.

## Related Issues

N/A
## Summary

Removes the two packaged builtin playbooks (`deep-dive`, `topic-briefing`) from
`raven/playbook/builtin/`. The two-layer library stays exactly as it is: a
writable user root over a packaged read-only builtin root. What changes is that
the builtin layer now ships empty, so an install carries only what the user
wrote on that machine. The packaged layer remains the mechanism a release can
use, without demo content landing in every install and in the monthly public
release; `pyproject.toml` keeps the `raven/playbook/builtin/**/*.md` package
glob for that reason.

No production code changes. `load_playbook` was already gated on the library
offering something (`loop/main.py`: `if not self._playbooks.empty`), so a fresh
install simply does not register the loader until the first user playbook
exists; `create_playbook` still registers whenever the feature is on, because an
empty library is exactly when capturing the first workflow matters. Tests were
updated to pin that gate both ways: the old assertion that `load_playbook` is
always registered encoded the shipped-library premise and flipped, and a
companion test proves a user-layer playbook opens the loader.

Review round (chandler.zhang, blocking): the `PlaybookConfig` description
embedded in `model_json_schema()` claimed the builtin layer ships two playbooks
and that the loader is always offered. Fixed in `fix(config)` commit: the
contract now states the conditional loader and keeps the measured 848-token
figure as the populated-library shape.

## Type

- [ ] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [x] Other

## Verification

- `uv run pytest tests/ -q -p no:randomly` at the pre-rebase head:
  10821 passed, 51 skipped, 13 deselected, exit 0 (0 failures).
- Post-rebase (onto 7d239509) relevant suites:
  `tests/test_playbook_store.py tests/test_playbook_compose_prompt.py
  tests/test_agent_loop_playbook_entry.py tests/test_playbook_tool.py
  tests/test_playbook_executor.py tests/test_cli_playbook_commands.py` ->
  97 passed, exit 0.
- `ruff check` and `ruff format --check` on the three changed test files:
  passed.
- Load-bearing probe: with the `if not self._playbooks.empty` gate removed,
  the empty-library test fails and the populated-library test stays green
  (restored afterwards; `raven/agent/loop/main.py` is not part of this diff).
- `scripts/check_commit_messages.py origin/main..HEAD`: exit 0.
- Toolchain provenance: `raven.playbook.store` inside the test run resolved
  from the worktree checkout, not a shared install.
- After the review fix: `pytest tests/test_rpc_schema_match.py
  tests/test_playbook_store.py tests/test_playbook_compose_prompt.py
  tests/test_agent_loop_playbook_entry.py tests/test_config_raven_sections.py
  tests/test_cli_config_precedence.py tests/test_config_loader.py -q
  -p no:randomly` -> 374 passed, 1 skipped, exit 0.
- Contract check: `PlaybookConfig.model_json_schema()` embeds the new passage
  and no longer contains "ships two playbooks" or "always offered".

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [x] User-facing docs or screenshots are updated when needed

## Risk

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

Backward compatibility: the behavior change is that a fresh install no longer
offers `load_playbook` until the user layer has a playbook, and `raven playbook
run / list` simply report no builtins. An existing user layer is unaffected;
an existing playbook that was copied from a builtin under the same name keeps
its contents (the store has always resolved user-first).

Rollback: restoring the two deleted files returns the previous behavior; no
migration, no state, no schema.

## Related Issues

N/A

Co-authored-by: Claude (deepseek-v4-flash-vision-exp) <noreply@anthropic.com>
## Summary

Prevent a failed MCP transport from cancelling the main agent turn
during startup.

Keep each MCP transport and handshake in one lifecycle so SDK task-group
failures unwind into ordinary per-server connection errors. Raven can
then continue with the remaining MCP servers and enter the model loop.

## Type

- [x] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed

- `uv run pytest tests/test_sandbox_unit.py -x -q` - 70 passed.
- `uv run ruff check raven/agent/tools/mcp.py
tests/test_sandbox_unit.py` - passed.
- A real `raven agent` turn returned `RAVEN_FIXED_OK` after two
configured MCP servers failed authentication.
- Docs and screenshots are not needed because no user-facing contract
changed.

## Risk

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

The change only alters failure isolation during MCP initialization. Roll
back the single fix commit if transport lifecycle behavior regresses.

## Related Issues

N/A

---------

Co-authored-by: zhanghui <23442919+gloryfromca@users.noreply.github.com>
## Summary

Eleven defects across the session rail, the floating desk, the sub-agent panes
and the turn's product card, all reported from one round of use.

Rail and desk placement:

- A failed session reported from a leading dot while running and finished
  reported from the tail slot, so the row's state jumped ends on the one
  transition a reader watches for. Failed now uses the same slot.
- The desk's default geometry hung a 300px palette to the LEFT of its launcher,
  over the centred transcript. It now hangs under the launcher, flush with the
  same edge, at 250x260. The geometry key is bumped to v5 so a stored desk does
  not carry the old corner forward.

Panes opening on their own:

- `openInstance` handed the row to the workspace and returned without recording
  what it opened, so the promotion in `refreshInstances` matched again on every
  heartbeat: the pane reopened every couple of seconds, clearing the reader's
  fullscreen and stealing the active pane.
- A graph node promoted from its record view opened a second pane beside the
  first. The promoted view now takes the pane it was promoted from, and keeps
  fullscreen when that pane held it.

Sub-agent transcript and chrome:

- The agent stage withheld an answer until the record called itself settled, so
  a node whose list row had aged out never drew its answer at all. It is drawn
  as it arrives and redrawn in place on each poll.
- The pane's reading measure named three segment classes, leaving folds,
  notices and the products bar against the left edge; a record with no composer
  still reserved the composer's 150px; and a `:has()` specificity inversion
  floated the palette a panel-width in from the edge while a pane was
  fullscreen. Fullscreen now takes the chat's own frame.
- A retried model call left one thought row per attempt, which a reload then
  showed as one. A run of thought-only episodes merges into one row holding
  every thought and the summed clock.
- All desk dividers inherited `pointer-events: none` from the grid, so no seam
  could be grabbed, and the two-pane seam was half the width it was drawn at.

The turn's product card:

- The dag card stretched a node graph to 1.7x the size the same graph has in
  the composer sheet. It draws at its own size and may still shrink to fit.
- The single-delivery card carried empty caption to keep a square picture; the
  column is narrower and the caption tighter, taking the card from 182px to
  155px.
- A delivered file the workspace saw no write for (a playbook or a sub-agent
  wrote it on another lane) fell back to a grey icon. The tile now reads its
  own opening bytes from the URL it already probes.
- The miniature was called `.mini`, which is also the page's small button, so
  it wore that button's border, radius, type, hover background and
  `white-space: nowrap` -- and the nowrap was inherited by every paragraph in
  the rendered document, which therefore could not wrap at any width. Renamed
  to `.amini`, and the miniature is now inset as a page with a faded tail
  rather than bleeding into the tile's corner.

## Type

- [x] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

- `npm test --prefix ui` - passed, 57 files and 665 tests (15 added).
- `npm run type-check --prefix ui` - passed.
- `npm run gen:check --prefix ui` - passed, 132 methods.
- `node ui/scripts/check-css.mjs` - passed.
- `node ui/scripts/check-page.mjs` - passed.
- `make lint-tui` - passed (generated.ts and the i18n catalogue in sync).
- `make check-large-files` - passed.
- Each new assertion was mutation-checked: reverting the fix it covers turns it
  red.
- The product-card and dag-card items were additionally checked against real
  session data in a `raven serve` run over a copy of the author's workspace,
  not only against fixtures.

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed

## Risk

User-visible presentation changes only; no RPC, schema or persistence changes.
The one stored value that moves is the desk geometry key (v4 to v5), which
resets a customised desk position and size to the new default once. Rollback is
the squash commit revert.

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

## Related Issues

N/A
…ities visible (#312)

## Summary

A WeChat user asked for a recommendation and the bot answered "the web
search
tool is not configured here". That sentence was not the model's own
phrasing --
it was the tool's error text, relayed outward. `web_search` needs a
Serper key
and that deployment has never had one, but the tool was registered
unconditionally, so on every search-shaped question the model saw it,
reached
for it, and passed the setup error on to whoever was in the chat.

Withholding the tool fixes that and creates a second problem: an
unregistered
tool is invisible. Nothing in a running Raven then says the capability
exists at
all -- the model is never offered it, no document lists it, and `raven
doctor`
reports on providers and memory but has never mentioned tools. The only
way to
learn that web search is one account and one edit away was to read the
source.

This does both halves, because either alone leaves a real gap: the
deployer is
told what this install can and cannot do, and the model is still not
offered a
tool it cannot run.

### Withholding the unusable tool

`web_search` is gated on a resolved key, in the main loop and in the
sub-agent
surface (`subagent/manager.py`) -- a sub-agent that reaches for a search
it
cannot run reports the failure to its caller, and that text lands in the
parent
turn, the same leak one level down.

The gate asks the tool, not the config. `WebSearchTool.api_key` resolves
at call
time from the constructor value *or* `SERPER_API_KEY`, so reading
`tools.web.search.apiKey` alone would withdraw a working tool from any
deploy
that exports the variable and configures nothing.

The tool's error message also hard-coded `~/.raven/config.json` while
the
gateway runs with `--config` elsewhere, so following it meant editing a
file the
process never reads. It names the path actually in force now. That text
is
reachable only if the key disappears after registration, which is
exactly why it
should be right: it is the message for the case the gate cannot cover.

### Three rules, and nowhere to read them

Registration is decided per family, each a different shape:

| family | rule |
|---|---|
| `web_search` | a resolved key, asked of the built tool |
| `web_fetch` | nothing -- always registered, a key only improves
extraction |
| media x3 | an `api_key` *or* a `model`, either counting as configured
|

For media those are two questions, not one. A section naming only a
model is
registered, because a model alone counts as asking for the tool, and
then every
call returns a missing-key error. Whether a capability is *offered* and
whether
it *works* come apart there, and a report treating them as one fact
ticks a
capability that cannot run.

Each rule is defensible where it sits. What is missing is anywhere to
read them.
Providers had the same sprawl once and answered it with
`providers.auth`: a
declarative table plus `credential_status` as the single authority, with
an AST
invariant enforcing that authority, because six surfaces had answered
the same
question six ways and each looked reasonable alone. Tools never got the
equivalent.

### What this adds

**Each tool answers for itself,** twice where the questions differ.
`WebSearchTool.is_configured` reads the config value *or*
`SERPER_API_KEY`,
because those are two sources and only the tool consults both. The media
base
answers `is_configured` on a model *or* a key -- which is what stops an
OpenRouter credential set for chat from silently switching on three
tools that
bill per call -- and answers `has_key` separately on the chain it
actually
resolves at call time: its own section, the borrowed provider key, then
`OPENROUTER_API_KEY`. Both rules live with the credential they read.

**`capabilities.py` describes the five** for a human deciding what to
set up:
what each does in one line, how much work it is (nothing / reuse a
credential
you already have / obtain an account), where the key goes, where to get
one, and
what it costs. It rules on nothing -- `is_configured` and
`has_credential` both
ask the tools -- so it cannot become a second opinion.

**`raven doctor` grows a section**, listing every capability configured
or not,
ordered by how much the deployer has to do:

```
(markers render as a green check and a yellow bang; spelled [ok] and [!] here to
keep this description ASCII, since it becomes the squash commit body)

Tool capabilities
  web_fetch:       [ok] Read a web page the agent already has the URL for
  image_generate:  [ok] Generate an image  (borrowed: providers.openrouter.apiKey)
  text_to_speech:  [!] Generate speech from text
                   no key resolves; calls will fail
                   set: tools.media.speech.apiKey
                   or env: OPENROUTER_API_KEY
  video_generate:  -  Generate a video
                   switch on: tools.media.video.model
                   key: reusing providers.openrouter.apiKey
                   Billed per call; needs prepaid OpenRouter credit.
  web_search:      -  Search the web
                   set: tools.web.search.apiKey
                   or env: SERPER_API_KEY
                   key from: https://serper.dev
  2 capability(s) available but not set up; the agent is not offered them.
```

Three deliberate details.

Naming the credential is load-bearing in both directions. A row that
cannot
distinguish a reused credential from a missing one sends someone to
create an
account they already have -- and a row that claims a reuse with nothing
to reuse
is worse, because acting on it means setting a model, getting a
registered tool,
and watching every call fail on a credential they were told they had. So
the
reuse line prints only when a key is genuinely there to pick up, and a
capability already in that broken state says so outright instead of
showing a
satisfied tick.

The credential is named at the path that holds it. For the media family
`config_path` names the *model*, so reusing it as the key source pointed
the
deployer at a line with no credential in it.

And each fact is on its own line rather than in a sentence, because the
terminal
wraps a long line mid-path and a config key broken across two rows
cannot be
copied, which is the only thing that row is for.

Nothing here moves the exit code, including the warned row. An install
without
image generation is a choice, not a fault, and a doctor that fails on it
teaches
people to ignore doctor. The half-finished one is arguable -- the memory
section
does exit non-zero for a role the user configured that the server could
not
build -- but that failure is silent where it happens, recall just
returning
nothing, whereas this one returns an error string to the model on every
call.
Say the word and it becomes an exit code instead.

### Not in this change

Registration still lives in `AgentLoop`. Having it read the table is the
point
of this shape and removes the last duplicate reader, but it edits
`agent/loop/main.py`, which is under active change, and it is worth
doing on its
own once that settles. Until then the table is a description, and the
tests
below are what keep it honest.

`deep_research` is deliberately absent: it is moving to the sub-agent
surface
and its tool is going away.

## Type

- [ ] Fix
- [x] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

Mixed on purpose: the first commit is a fix and the rest are the feature
it
made necessary. Splitting them would land a change that hides a
capability
without landing the one that makes it discoverable.

## Verification

```
make lint                                            exit 0
make build                                           exit 0
npm run test --prefix ui-tui                         969 passed, 13 skipped
uv run --all-extras pytest -q                        6186 passed, 35 skipped, 1 failed
uv run pytest tests/test_tool_capabilities.py \
              tests/test_cli_doctor_commands.py \
              tests/test_agent_loop_web_tools.py     64 passed
uv run --extra dev ruff check raven tests scripts    All checks passed
uv run --extra dev ruff format --check               828 files already formatted
npx commitlint --from origin/main --to HEAD          exit 0
scripts/check_commit_messages.py origin/main..HEAD   exit 0
```

The one failure is
`test_read_file_image.py::test_an_attachment_that_cannot_be_
read_costs_a_note_not_the_turn`, and it is not from this branch: it
fails the
same way on `main` at `1cb604a` with these commits absent. The case
makes a file
unreadable with `chmod 000`, which does not block a root user, so it
fails for
anyone running the suite as root and passes in CI. This branch does not
touch
that file or the code under it.

The capability tests drive a real `AgentLoop` and compare what it
registered
against what the table predicts, rather than asserting the table against
itself.
The gated set is derived -- tools present once credentials are supplied,
absent
without -- so a sixth gated tool whose author forgets the table fails
here rather
than going unnoticed.

That last claim was false when first written, and the last commit is
what makes
it true. The fixture listing the media tools by hand never switched a
fourth one
on, so a new one never joined the gated set and the assertion held over
an
already-incomplete table. #305, which adds a MiniMax voice-clone tool,
is that
case: merged against this branch the assertion passed. Read from
`MediaGenConfig` instead, it fails and names the tool -- `gated but
undeclared:
['voice_clone']`.

Which means whichever of the two lands second turns this red,
deliberately. The
fix is one table entry, and it needs the rule that PR settles:
`voice_clone`
counts as configured on `api_key or api_base or model` in
`effective_media_config` while registration still gates on `api_key or
model`,
so an `apiBase`-only install with no MiniMax key is configured by one
rule and
withheld by the other. That is the divergence this table exists to make
visible,
and it is worth resolving there rather than papering over here.

Fifteen mutations, each caught:

```
the web_search gate removed from the main loop    3 failed
the same gate removed from the sub-agent surface  1 failed
media rule reads only the key                     7 failed
web_search rule reads only the config             1 failed
a tool removed from the table                     6 failed
the doctor section not rendered                   6 failed
doctor lists only configured tools                4 failed
unconfigured treated as a failure                17 failed
the reuse line is printed unconditionally         1 failed
the key source falls back to the model path       3 failed
a keyless registered capability is not flagged    1 failed
has_key always answers yes                        1 failed
has_credential collapses into is_configured       1 failed
the credential chain drops OPENROUTER_API_KEY     3 failed
the reuse check ignores the environment           3 failed
```

Four of those tests exist only because a mutation pass found the earlier
versions insufficient, and the last rounds are why the shape changed. A
media
case that sets both a model and an OpenRouter key proves nothing about
the "or
model" half: the borrow fills the key in, so a rule reading only the key
still
answers correctly. The case that pins it has nothing to borrow.

The sub-agent case is the same argument applied to the second call site:
with
the gate present only in the main loop, every test above still passes,
because
nothing was watching what the sub-agent surface registers.

More usefully, mutating `has_key` to always answer yes changed no test
at all in
the first version, which said the ruling was not load-bearing: the
doctor was
inferring "no credential" from an *empty source string* rather than from
the
tool's answer, so a fourth credential source would have been reported as
a
missing one. `has_credential` is now its own fact, asked of the tools,
and that
mutation fails.

Separately, autouse fixtures clear `SERPER_API_KEY` and
`OPENROUTER_API_KEY`,
without which several of these pass for the wrong reason on any machine
where a
developer exported one.

Rendering was checked against a real config in six states -- nothing
set, a
provider key present, a model with nothing to borrow, a tool with its
own key, a
model plus a borrowable key, and an exported variable as the only source
--
rather than only asserted on.

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed

## Risk

One behaviour change: `web_search` is no longer offered to the model
when no key
resolves. That is the fix. A deployment that has a key, in config or in
the
environment, is unaffected; one that has none was getting an error
string in
place of an answer.

The rest is additive. No other registration logic changed, so which
tools an
agent is offered is otherwise exactly what it was; `is_configured` moved
the
existing predicates onto the tools without altering them, and the
mutation
results above are what pins that. `has_key` and `_resolve_key` are new
names for
the chain `api_key` already resolved -- the property calls the extracted
one, so
callers see the same answers.

`raven doctor` gains a section and no new exit code. Its zero-network
guarantee
holds: the table reads config and environment only.

Two allowlist entries were added to
`test_only_the_auth_module_decides_configuredness_from_a_key`, argued in
place:
`subagent/manager.py`, which asks the built tool whether a key resolved
so an
unusable search is withheld, and `capabilities.py`, which reads keys to
*report*
-- which source supplied one, and whether one is there to reuse. Both
ruling
halves are delegated to the tools, whose files were already listed.

Rollback is a revert. Nothing is written and no configuration is read
differently.

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

## Related Issues

N/A

---------

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
## Summary

Audit every shell verb that survives, then retire the three whose indirection bought nothing.

The remaining-legacy document's Axis 2 asked for the shell verb count to be visible and stopped there. `docs/specs/2026-08-25-shell-verb-audit.md` gives each of the 21 verbs on `3b68ff72` a verdict: 3 retire mechanically, 4 stay by argument, 9 need real work, 4 are gated on a page-chrome migration nobody has scoped. Driving the count to zero is explicitly not what the audit recommends.

The code change takes the mechanical three. `openCron`, `openXa` and `openConn` each had a legacy half that was a single line calling straight back into another island, and no target was ever reassigned. The nav flyout rows, the rail's cron group and the settings page's connection links import the owning island instead; the three verbs, their `interface Shell` members and their legacy one-liners are gone. The ratchet falls from 21 to 18.

Two findings worth the reviewer's attention, both of which changed a verdict:

- `markNew` looks identical to the three and is deliberately kept. `features/rail/store.ts` already imports `shell/navfly`, so retiring the verb would turn a dependency the shell keeps one-way into a literal import cycle.
- `closeDetail` reads as a one-line `dataset` write, which made it look like the cheapest row on the page. It is reassigned twice more (`demo/152-skills.js:88`, `demo/153-plugins.js:120`), each wrapping the previous -- a decorator chain that grows a link per page sharing the drawer. It is filed under real work, not mechanical.

The nav flyout also gains a per-row test. The existing one picked a single row, so a row wired to the wrong opener, or to none, passed: a mutation making the xa row a no-op failed zero tests before this change and fails one after it.

No wire-protocol change, no server change. The served DOM, class names and styles are untouched.

## Type

- [ ] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [x] Refactor
- [ ] Other

## Verification

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [x] User-facing docs or screenshots are updated when needed

Run on the rebased head, after `glhttps/main` moved to `6fdf1d7b`:

- `npm run --prefix ui type-check`
- `npm test --prefix ui` (57 files, 656 tests)
- `npm run gen:check --prefix ui` (132 methods)
- `node ui/scripts/count-shared-globals.mjs` (0 / 0 / 18)
- `npm run --prefix ui build` plus `python3 ui/build.py`
- `node ui/scripts/check-page.mjs`
- `node ui/scripts/check-css.mjs`
- `uv run --frozen --python 3.12 --extra dev python scripts/check_large_files.py glhttps/main..HEAD`
- `git diff --check glhttps/main...HEAD`, plus a non-ASCII sweep of the added lines

Mutation checks on the new test, because it replaces shell fakes with module mocks and a rewritten test that cannot fail is worse than the one it replaced:

- xa row's opener replaced by a no-op: 0 tests failed before the new test, 1 after
- cron and conn openers swapped: 1 test failed

Both page modes were driven against the built page, since the change deletes legacy globals the offline layer used to define:

- live (`raven serve`, real RPC): the three rows open `xaPage`, `connPage`, `cronPage` respectively; no console errors from real interaction
- stub (`?stub=1` over the built `dist`): same three mappings, with `window.openCron` and `window.openXa` confirmed `undefined`

## Risk

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

User-visible surface is three navigation entries plus two settings links, all verified in both page modes. The deleted legacy globals had no remaining callers anywhere in `ui/src` or `page.html`. Rollback is reverting the two commits; the audit document stands on its own and carries no behaviour.

## Related Issues

N/A
…sage

## Summary

A session is named today by truncating its first user message to 40 characters, so the picker shows a raw opening line cut mid-word, often a pasted instruction. This asks a model to name it instead.

Key decisions:

- **The call runs beside the opening turn, never on its path.** Fired from `turn.send` after the submit, it reads only the first user message (waiting for the reply would name the answer rather than the request, and would leave the front end's placeholder spinning for a whole turn). Every refusal is silent, because the mechanical title `save()` derives is already in place: disabled by config, an opening message too short to name anything, a timeout, an answer that ignored the instruction, or a rename typed while the call was in flight.
- **No new metadata state.** A generated title carries the same `title_auto` marker the mechanical one does, so fork inheritance is unchanged. An earlier draft added a third marker distinguishing model-made from machine-derived; it was dropped because the human/not-human distinction is the only one any consumer reads.
- **Storage limits and display limits are separated.** The 24-codepoint generation budget is a runaway guard, not a layout rule (measured titles run 2-21 codepoints). `set_title` collapses a title to one line and refuses one past 200 characters rather than truncating it: a person typed that, and keeping the first 200 characters hands back a fragment they never wrote. Display truncation stays with each front end, which already ellipsises in both places.
- **The client-side 30-character cap is deleted, not aligned.** The GUI wrote a truncated first line on send so the rail would not sit on "New task"; the placeholder does that job now, so the constant that disagreed with the backend's 40 is gone rather than synchronised.
- **The GUI placeholder always ends, on a name and not on the default.** It gives up after 12s onto the opening line it captured when the wait began, preferring the stored title when the turn has already ended and written one. A placeholder outliving its call is the one state a reader cannot leave by waiting, and the stored title alone cannot be relied on here: `SessionManager.save` derives it at turn end, so a turn still working at 12s has none, and `refreshList` never re-reads the session the reader is looking at.
- **The TUI shows a resumed session's name** on the init bundle it already receives, and ignores the live event: the panel for that session has scrolled off above the turn that generated the name.

Two things a reviewer should know beyond the diff:

1. A refused rename in the GUI was silently swallowed (`.catch(() => {})`). Harmless while nothing could fail, but `set_title` now can, so the failure is reported and the row rolls back, mirroring `DS.sessions.pin`.
2. This project's pipeline runs two jobs, `tests` and `page`. `ruff`, `tsc`, `vitest` and the three codegen drift checks are not gated by CI at all, so the Verification list below is the only place they were run. Separately, `main` currently fails `make lint-python` on three files this branch does not touch (`raven/agent/tools/deliver.py`, `tests/test_channels_outlet.py`, `tests/test_rpc_files.py`) - real, but CI never sees it.

## Type

- [ ] Fix
- [x] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

Run on this branch after the review round, rebased onto `origin/main` at `01d39e58`:

- `uv run pytest -q --ignore=tests/test_start_webapp.py` - 10859 passed, 43 skipped, 0 failed
- `npm test --prefix ui` - 674 passed (58 files)
- `npm test --prefix ui-tui` - 1600 passed (110 files)
- `npm run type-check --prefix ui` and `--prefix ui-tui` - clean
- `npm run gen:check --prefix ui`, `npm run lint:rpc --prefix ui-tui`, `npm run lint:i18n --prefix ui-tui` - all three checked-in generated artifacts in sync
- `npm run lint --prefix ui-tui` - 23 warnings, none of them in a file this branch touches
- `uv run ruff check` and `ruff format --check` on every Python file in this diff - clean
- `make check-large-files` - clean

`tests/test_start_webapp.py` is excluded above because two of its cases fail on `main` in this environment: the test pins `PATH=/usr/bin:/bin`, so `bash` resolves to macOS 3.2 and `start_webapp.sh` fails to parse. Neither the script nor the test differs from `origin/main` on this branch.

Every new guard was mutation-checked - 17 mutations across both rounds, each confirmed to fail at least one test: the human-title race, the in-flight dedup, conversation-scoped emission, the discard-over-clamp rule, the minimum-input gate, the storage ceiling, both `_needs_naming` gates, resume carrying the title, the GUI timeout fallback, the settle path, the rail placeholder branch, and the TUI panel line. Writing them turned up a real defect: a model answering with an empty quoted string produced a title made of two quote characters.

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [x] User-facing docs or screenshots are updated when needed

## Risk

User-visible change: a new session gets a model-written name instead of its truncated first message, and the GUI shows a shimmer where the title goes until that name lands. Existing sessions are untouched; nothing rewrites a title already on disk.

Cost: one short model call per session, not per turn, against a first message already in context. `session_title.enabled` turns it off and `session_title.model` points it at a cheaper tier.

Rollback: set `session_title.enabled = false`. Sessions then keep the mechanical title exactly as they do today, and the GUI placeholder resolves through its 12s fallback. Reverting the commit is also clean - the only persisted change is the title string itself, written to a field that already existed.

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

## Related Issues

N/A
## Summary

The lexical half of RAVEN-20260823-001. Three read-only commands were refused
in one session because their comments said `there's`, `what's` and `raven's`.
`shell_policy.py` turns `#` off in its lexer, so English prose keeps taking
part in quote parsing: the apostrophe opened a quote that never closed, `shlex`
raised `No closing quotation`, and the fail-closed branch answered `hard_deny`.

Reproduced verbatim from the ledger's artifacts before touching anything --
all three `hard_deny`, `_command_segments` raising, and the same commands
without their comments allowed every time.

The user saw only `policy evaluation failed`, asked four times why, and the
model, given nothing to go on, guessed wrong twice.

### Both gates, not one

The same blindness runs the other way and reaches a second gate.
`ExecTool._guard_command` searches the deny list against raw text, so it never
hit the parse failure -- but a denied pattern written inside a comment blocked
the command there just the same:

```
ls -la  # unlike dd if=/dev/zero, this one only lists
```

Fixing `ShellCommandPolicy` alone would only have changed which message the
user got. A test pins each gate, and a mutation that reverts one of them is
caught.

### One lexical view

Comments are removed once, by a walk that shares the quote and escape rules
`_split_on_operators` already uses -- those rules are what decide whether a `#`
is a comment at all. Every safety check reads that view: the deny patterns, the
delete and power matchers, the approval families a surface registered, and the
second gate's own list. The raw command still goes to the executor, the audit
trail and the approval prompt, which are about what the user asked for rather
than about what it does.

A `#` opens a comment only outside quoting and at the start of a word.
`foo#bar`, `${#PATH}`, `$#`, `file#1` and `'#!/bin/sh'` are arguments. Cutting
at every `#` would quietly shorten commands, which is how a classifier stops
seeing the half that matters -- pinned with `echo a#b; rm -rf /tmp/tree`, which
must still deny.

### What does not change

Fail-closed. An unterminated quote in the executable region is still refused,
including when a well-formed comment precedes it. `rm -rf` or `shutdown` beside
a comment still denies, on either side of it. A `bash -c` script is still read
through to the command it carries, comments and all.

### Scope

This is deliberately half the ledger entry. The other half -- separating
"refuse this tool call" from "end the whole turn", which today share one
`abort_action` flag across the main loop, the sub-agent loop, tracing and the
UI -- is a change to the `ToolResult` control contract and belongs in its own
MR. This one stops the false refusals without touching that contract.

## Type

- [x] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

```
pytest -q                                  11056 passed, 83 skipped
pytest tests/test_shell_comments.py -q     26 passed
ruff check / ruff format --check           clean
```

Four mutations, each caught:

```
comments are never stripped                5 failed
a `#` anywhere starts a comment            1 failed
a `#` inside quotes starts one too         1 failed
only the policy gate learns the view       1 failed
```

The last is the one worth reading: it is what proves the second gate is
covered rather than assumed.

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed

## Risk

The change is narrowing what the classifier reads, so the direction that
matters is whether anything dangerous can now hide in a comment. It cannot:
the shell would not run it either, and the executable half is classified
exactly as before. Tests cover a denied pattern on both sides of a comment, a
recursive delete and a power command before and after one, and a wrapper
carrying its own script.

The opposite risk -- over-eager stripping cutting a command short -- is the
reason a `#` must start a word. Six shapes that are not comments are pinned.

No interface changes and no migration. `executable_text` is a new export;
`_guard_command` and `evaluate` keep their signatures. Rollback is a revert.

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

## Related Issues

RAVEN-20260823-001, lexical half.

One note for the ledger while this is open: the entry's header carries P1 while
its own impact section concludes P2, on the grounds that the trigger is
bounded by comment style and recoverable by removing the comment. The two
readings imply different things about whether there is a workaround, and the
priority field is what the queue is ordered by.
## Summary

Put tests under seven pieces of island-to-legacy wiring that nothing defended, and correct the count that found them.

`!268` audited the shell verbs by reading them. This asks a different question by running them: for each verb, `shell()` returns a proxy that no-ops one named member, and the suite runs. A verb whose removal leaves the suite green is one a migration could cut silently. The same trick one layer down, on `ds()`, asks it of the data sources.

Three verbs and four source members answered green. Each now has a test, and each test was checked by cutting the call it defends:

| wiring | what a silent cut looks like | pinned by |
|---|---|---|
| `closeSet` | the settings dialog stays up behind the connections page | both cards, separately -- cutting either one fails |
| `plugRedraw` | the caps page keeps the tab title and count from before an install | 4 of its 5 sites; `sync()` is not pinned |
| `workspaceSetOpen` | the desk's panel never opens, or stays open empty | the guard as well as the calls: closing a pane that is not the last must not shut it |
| `settings.model` | the card keeps showing the model that was there before the pick | the new model held outside the snapshot, so the assertion cannot pass without the re-read |
| `plugins.reload` | the shelf behind the progress sheet holds the pre-install answer | waits, since the re-read lands a microtask late |
| `transcript.okOf` | every tool row shows the wrong verdict | one clean result beside one error: forcing true fails, forcing false also fails |
| `transcript.branch` | the branch action never appears | the button and the text it hands over |

The last commit corrects the one before it rather than restating it. `agents.watch` was reported undefended and is not: with the tests exactly as they were, cutting the registration fails three of them. The sweep kills a member by proxying what `ds()` returns, and `hook()` reads `window.DS` directly, so the kill never reached the call site and green was read as undefended. Four call sites bypass `ds()` that way and are invisible to that measurement, named in the commit. So "5 of 61 undefended" should be read as **4 real holes out of 60 reachable calls, with four paths unmeasured**.

**One gap this branch does not close, recorded here because the squash drops commit bodies.** `branchOf` guards on `lane.main`, and that guard is not pinned: removing `if (!lane.main) return null` leaves all transcript tests green. Building a non-main lane means driving the sub-agent surface, which is a different fixture than any of these tests set up. The hole the sweep named -- the source's `branch` reaching the button -- is closed; the guard beside it is not.

The `plugRedraw` gap is the same shape: `sync()` is the fifth call site and is not pinned, only the four the table names.

No production behaviour changes. One assertion is added to three existing sub-agent tests; everything else is new tests and recording fakes that were empty functions.

## Type

- [ ] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [x] Other

Tests, plus one correction to a claim in this branch's own history.

## Verification

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed

On the rebased head, against `origin/main` at `9f9864b2`:

- `npm run --prefix ui type-check`
- `npm test --prefix ui` (58 files, 682 tests)
- `npm run gen:check --prefix ui` (132 methods)
- `node ui/scripts/count-shared-globals.mjs` (0 / 0 / 18)
- `npm run --prefix ui build` plus `python3 ui/build.py`
- `node ui/scripts/check-page.mjs`, `node ui/scripts/check-css.mjs`
- `uv run --frozen --python 3.12 --extra dev python scripts/check_large_files.py origin/main..HEAD`
- `git diff --check origin/main...HEAD`, plus a non-ASCII sweep of the added lines

Every added test was re-checked after the rebase by cutting the call it defends. Seven of eight mutations fail a test; the eighth is `plugRedraw` inside `sync()`, which this branch does not claim to pin. Mutating the four sites it does claim -- `toggleView`, `backToMarket`, `search`, and the settings pair -- each fails.

Re-running the verb sweep on this branch reports 0 of 18 verbs cuttable, against 3 before it.

## Risk

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

Test-only. The three sub-agent tests changed `poll?.()` to an assertion plus `poll!()`, which makes a missing callback fail where it happens rather than three assertions later. Rollback is dropping the branch.

## Related Issues

N/A
Absorbs the GitHub release into the downstream trunk. A merge rather than a
rebase so v0.1.13 becomes a real ancestor: the next sync starts from v0.1.13
instead of v0.1.12, and the eventual contribution back to GitHub can still tell
our commits from the upstream ones it already has.

Cost this round: 51 conflict hunks over 27 files, against 54 over 31 for
v0.1.12. The hunks were the cheap half. Upstream's #284 moves the model and
provider from process-wide state to a per-session binding, and the trunk had 230
commits standing on the old shape.

What git reported as a clean merge and was not
---------------------------------------------

Fourteen upstream-changed paths do not exist here, because we renamed or moved
them since v0.1.12. Rename detection found none of them, so their changes were
merged into paths nothing reads, silently:

* raven/tui_rpc/* -> raven/rpc/* (renamed in ce526ad). Nine files, including
  question_broker.py, whose whole #354 rewrite was lost, and methods/config.py,
  methods/model.py, methods/session.py, models.py and errors.py. Each was
  replayed by three-way merge against dd82f52. The three test modules were
  rebased function by function onto upstream's version instead, after a
  three-way merge spliced assertions into the wrong tests.
* raven/agent/tools/mcp.py -> raven/mcp/client.py. Carried the release-blocking
  fix, and is handled in the commit that follows this one.
* ui-tui/rpc-schema/openrpc.json -> rpc-schema/openrpc.json.

Decisions
---------

Follow upstream wherever the two disagree about provider and model:

* providers/pin.py is gone, so a bare model id is refused instead of derived.
  A model id does not name whose credential serves it, and the derivation
  walked a list -- with two vendors configured, which one pays came down to
  their order in it. rpc/methods/config.py now requires the provider.
* ModelSwitchInTurnError (-32009) is gone with it. A session switch lands on
  that session's next turn, so there is nothing to refuse mid-turn.
* AgentLoop.refresh_context_window and the LazyProvider on_built wiring are
  gone: the window rides on the binding now.
* The skill gate no longer forwards an unpaired pin.

Keep ours where the trunk is ahead, and graft upstream's behaviour into it:

* raven/agent/subagent/backends/raven_loop.py is the live sub-agent path, so
  manager.py's inline loop stays retired and the web_search gate from #312
  applies there.
* skill_hub/client.py keeps the public zip limits the skillhub RPC path imports,
  and takes upstream's query-string cap.
* ask_user keeps our JSON-string normalizers and feeds them into upstream's
  batch contract, so a batch validated by _prepare is one _normalize_questions
  has already coerced.
* The chat view composes both filters: hideIntroAfterFirstTurn drops the cover,
  visibleRows still hands the view to a direct chat.

Tests removed, not repaired
---------------------------

Eight tests covered mechanisms this merge deletes, and one of them hung rather
than failed:

* seven in test_agent_loop_model_switch.py over AgentLoop._pending_provider,
  the switch-parking mechanism #284 replaced with per-session bindings. Its
  replacement arrives with upstream as test_agent_loop_session_model.py.
* test_a_refusal_during_a_turn_keeps_its_own_code in test_acp_methods.py, whose
  error class no longer exists.

Baseline: the pipeline for 01d39e58, this merge's base, is green, so nothing
here can be charged to a pre-existing failure.

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
…turn

Ports the fix from upstream v0.1.13 (#365), which the merge could not deliver:
it changed raven/agent/tools/mcp.py, and this tree moved that module to
raven/mcp/client.py, so rename detection missed it and the change landed on a
path nothing reads. The trunk carried the release-blocking bug while claiming
the version that fixed it.

The SDK opens streamableHttp inside an anyio task group. Entered into the
caller's AsyncExitStack, a transport that dies takes the caller down when that
outer stack unwinds -- past every per-server except -- so one MCP server with an
expired credential cost the user the whole answer, with no reply at all.

_mcp_server_connection owns the transport, the session and the handshake as one
lifecycle over its own stack. The failure now unwinds inside that generator and
reaches the caller as an ordinary exception, which the per-server handler
already catches; the remaining servers still register and the turn reaches the
model loop. manager.py, the live path, goes through the same connect_mcp_server
and inherits it.

Verified in both directions: the new test in test_mcp_client.py fails on the
merge commit with the ExceptionGroup escaping, and passes here with the second
server registering its tool.

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
## Summary

Opening the tracing dashboard was unusable on a state dir with a few months of
history: the viewer rebuilt its entire snapshot on every request, and the page
re-asks every 5 seconds, so it was never idle. Measured on a 3.2 GB tree
(324k files, 102k spans, raven actively writing), a poll cost 7.6 s and the
response was 325 MB.

Four costs on the read path, each fixed here:

1. **The log reader recursed all of `logs/`** to find a few dozen `.log` files,
   walking the 324k-entry `audit-artifacts/` payload store on the way. Logs only
   ever land in the active file or `archive/<date>/` -- the only place either
   writer renames one to -- so the search stays inside those (600 ms -> 4 ms).
   Candidates are also stat'ed once each rather than inside a sort comparator.
2. **The deposit index walked the everos root per request**, which contains the
   engine's own virtualenv, and re-read every deposit. Dotted directories that
   hold no deposits are skipped and the index is reused while the tree is
   unchanged (433 ms -> 13 ms). The 54 `.md` files no longer visited are all
   pip-installed package readmes, which the entry parser already rejected.
3. **Nothing cached the snapshot.** The serialized body is now held, keyed on the
   inputs it was built from, and refreshed *behind* the response instead of
   making the reader wait -- a running raven appends spans continuously, so a
   poll that waits for the rebuild waits every single time. Only the body is
   retained, not the object graph it came from; retaining the graph measured at
   2.4 GB resident and grows with history, while the body is bounded by the
   payload. The refresh is deferred to after the response leaves the socket,
   because the rebuild is synchronous and would otherwise stall the write it was
   meant to skip.
4. **The tree nested a copy of every span** beside the spans array, so the
   payload carried the whole history twice. It now carries ids, depth and
   nesting, resolved against `trace.spans` in the page.

Search shared the disk cost and added its own: the result cap used an unlabelled
`break`, which caps nothing past the first trace. A broad term therefore matched
13k spans and read an artifact off disk for each -- 138k reads returning 802 MB
of text for one query. The cap now ends the scan, and artifact text is cached per
path under a byte ceiling.

Results, same tree and the UI's own polling cadence:

| | before | after |
|---|---|---|
| `/api/data`, first request | 7.6 s | 3.3 s |
| `/api/data`, subsequent polls | 4.8-7.6 s | 0.27 s |
| payload size | 325 MB | 171 MB |
| `/api/search` (`tool` / `memory` / `store`) | 19 s / 22 s / 72 s | 3.0 s / 2.6 s / 2.9 s |

Not addressed, and deliberately left out: the archives have no retention policy
at all, so the reader still reads the whole retained history on a rebuild. A time
window would cut this further but removes older sessions from the panel, which is
a product decision rather than a performance one. The remaining 2.6 s of a search
is the rebuild it triggers; removing that needs an index that survives rebuilds,
and two in-memory attempts were rejected here on measurement -- retaining the
searchable text of every span exhausted memory, and re-parsing the held body per
query was slower than rebuilding.

## Type

- [ ] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [x] Other (performance)

## Verification

Commands run, and what they reported:

- `uv run pytest -q` -- 11040 passed, 33 skipped, 2 failed. Both failures are in
  `tests/test_start_webapp.py` and reproduce unchanged on the pristine base
  commit, so they are not from this branch.
- `uv run pytest -q tests/integration/test_tracing_viewer_e2e.py -m integration`
  -- 6 passed. New file; spawns the real Node viewer against a synthetic state
  dir and drives it over HTTP.
- `uv run pytest -q tests/test_cli_tracing_commands.py tests/test_tracing_api.py
  tests/test_tracing_compact.py tests/test_no_otel_tracing.py` -- 86 passed.
- `uv run ruff check` and `ruff format --check` on the new test -- both clean.
- `make check-large-files` -- clean.
- Each of the six new assertions was mutation-checked: undoing the log scoping,
  dropping the archive subtree, removing the snapshot cache, pinning the
  fingerprint constant, nesting whole spans in the tree, flattening the tree,
  reversing the search sort, and removing the result cap each fail exactly the
  intended test and nothing else.
- The page itself was driven in a browser against the real tree: 181 session
  cards render, switching sessions redraws 267 span nodes nested to depth 3 with
  no missing fields, the content search returns 50 highlighted hits, and the
  console is clean. This matters because the tree change moves work into the
  page, which no automated gate covers -- `raven/tracing/viewer/*.js` sits
  outside every lint and type-check target in the Makefile.

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed

## Risk

Behaviour changes a reader can see:

- Data can be one refresh interval behind while raven is writing, by design. The
  page already polls, so it converges on the next tick; the e2e test pins that an
  appended span reaches a later poll.
- A log file placed under `logs/` outside the active file and `archive/` is no
  longer read. Neither writer produces one.
- A search for a broad term now returns the newest 50 of the first 200 matches
  scanned rather than of every match. The scan runs newest-first, so the result
  set is nearly identical, and the 200 cap was the existing intent.
- The viewer holds the serialized payload for as long as it runs -- roughly
  650 MB resident when idle on the tree measured here, against 60 MB before.
  `TRACE_ARTIFACT_CACHE_MAX_BYTES` bounds the artifact cache separately.

Rollback is a revert of this commit; the changes are confined to the viewer's
read path and add no state, schema or file format.

## Related Issues

N/A
@LivXue

LivXue commented Aug 29, 2026

Copy link
Copy Markdown
Member Author

Round 1 findings (three P1) are fixed in ad9bacc: argv-preserving launch so a TUI exit reopens correctly and no shell expansion occurs, plus file-level docs on the five new files. PR description updated with the round 1 note; all gates pass (23 tests).

Show the raven.openTui entry as an editor title button (menus.editor/title, navigation group) next to the active file tab, like the OpenCode and Claude Code extensions, and drop the status bar item, its facade surface, and the onStartupFinished activation.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Blocking: the code delta is sound, but the PR description must be corrected before merge because it contradicts this revision and becomes the squash commit body.

The description still says the extension uses whitelist-based POSIX/Windows shell quoting, reports 27 tests, records a real-shell quoting smoke test, and says the launch line is shell-quoted. This head removed buildSendText and its quoting tests in favor of direct shellPath/shellArgs; the current suite has 23 tests. The appended round-1 note states the new design, but it leaves the Summary, Verification, and Risk sections internally contradictory. AGENTS.md section 3.7 requires those sections to describe the current overall implementation and exact verification that will land in the squash commit.

Please replace the obsolete shell-quoting and 27-test claims with the direct-argv design and current 23-test/package results. No code change is required for this finding.

Coverage: I reviewed the delta from ad9bacccd803, the current full github/main...HEAD diff, repository rules and domain context, the command contribution and activation path, API facade callers, history, backward compatibility, and test changes. Moving the shortcut from the status bar to menus.editor/title is consistent across the manifest, implementation, facade, tests, and README. The removed test scaffolding only represented the deleted status-bar API; terminal behavior coverage remains, so tests were not weakened to obtain a green result.

Verification:

  • npm ci --prefix vscode-ext: passed with local EBADENGINE warnings because Node v23.10.0 is outside two dependencies' supported even-major ranges; CI uses Node 22.
  • Extension lint, type-check, test, build, and formatting checks: passed; Vitest reported 3 files and 23 tests passed.
  • npm audit --prefix vscode-ext --audit-level=critical: passed with 0 vulnerabilities.
  • VSIX packaging: passed; 11 files, 10.93 KB.
  • Large-file check, commitlint, repository commit-message checker, and git diff --check: passed.

make is unavailable in this environment, so I ran the underlying targets directly.

Create the Raven terminal with location: editor so the TUI opens next to the active file tab like the OpenCode extension, instead of the terminal panel. The adapter maps the facade's location flag to viewColumn Beside.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Blocking: the previously reported PR-description blocker remains unresolved; I found no new code finding in this revision.

The c5ca365 delta correctly maps the facade's editor location to ViewColumn.Beside, passes that placement from the terminal manager, and updates its tests and README consistently. I rechecked the affected callers, current full diff, repository rules and domain context, history, backward compatibility, and test changes. Existing lifecycle and argv coverage remains intact, so the tests were not weakened to obtain a green result.

Verification:

  • npm ci --prefix vscode-ext: passed with the same local EBADENGINE warnings from Node v23.10.0; CI uses Node 22.
  • Extension lint, type-check, test, build, and formatting checks: passed; Vitest reported 3 files and 23 tests passed.
  • npm audit --prefix vscode-ext --audit-level=critical: passed with 0 vulnerabilities.
  • VSIX packaging: passed; 11 files, 11.11 KB.
  • Large-file check, commitlint, repository commit-message checker, and git diff --check: passed.

make is unavailable in this environment, so I ran the underlying targets directly.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Blocking: the corrected PR description resolves the prior blocker, but the 0.1.1 bump leaves the committed lockfile on 0.1.0; see the inline note.

Coverage: I reviewed the delta from c5ca36550a4d, the live PR description, the current full github/main...HEAD diff, repository rules and domain context, package metadata and packaging output, affected callers, history, backward compatibility, and test changes. The PR description now accurately reflects the direct-argv implementation and 23-test suite. No tests were weakened.

Verification:

  • npm ci --prefix vscode-ext: passed with local EBADENGINE warnings because Node v23.10.0 is outside two dependencies' supported even-major ranges; CI uses Node 22.
  • Extension lint, type-check, test, build, and formatting checks: passed; Vitest reported 3 files and 23 tests passed.
  • npm audit --prefix vscode-ext --audit-level=critical: passed with 0 vulnerabilities.
  • VSIX packaging: passed and produced raven-vscode-0.1.1.vsix; 11 files, 11.12 KB.
  • Large-file check, commitlint, repository commit-message checker, and git diff --check: passed.

make is unavailable in this environment, so I ran the underlying targets directly.

Comment thread vscode-ext/package.json Outdated
"name": "raven-vscode",
"displayName": "Raven Agent",
"description": "Open the Raven TUI inside the VS Code integrated terminal.",
"version": "0.1.1",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P1] Keep the lockfile version in sync

This changes the package to 0.1.1, but both the top-level version and packages[""].version in package-lock.json remain 0.1.0. The packaged VSIX is therefore identified as 0.1.1 while the committed lock metadata still identifies the root package as 0.1.0. npm ci does not catch this root-version drift (it passed here), so please regenerate the lockfile through npm so all committed release metadata agrees on 0.1.1.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 48fd061: the lockfile was regenerated through npm, so the top-level version and packages[""].version now match 0.1.1 in package-lock.json.

Regenerate package-lock.json through npm so the committed root
version matches the package.json bump.

Co-authored-by: Claude <claude-sonnet-4-6> <noreply@anthropic.com>

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No blockers; this can merge as far as I am concerned.

The prior lockfile finding is resolved: package.json, package-lock.json's top-level version, and packages[""].version all read 0.1.1. The new commit changes only those two lockfile metadata fields, with no dependency or integrity churn. The live PR description also accurately records the fix and remains ASCII-only.

Coverage: I reviewed the delta from 176c5a6, rechecked the full current github/main...HEAD diff and live PR description, repository rules (AGENTS.md and CLAUDE.md) and domain context, package metadata, affected callers, branch history, backward compatibility, and test changes. The extension remains additive, and this revision changes no tests, so no tests were weakened to obtain a green result.

Verification:

  • npm ci, lint, type-check, test, build, and formatting checks: passed; Vitest reported 3 files and 23 tests passed. npm ci emitted local EBADENGINE warnings because Node v23.10.0 is outside two dependencies' supported even-major ranges; CI uses Node 22.
  • npm audit --audit-level=critical: passed with 0 vulnerabilities.
  • VSIX packaging: passed; raven-vscode-0.1.1.vsix contained 11 files and was 11.12 KB.
  • Large-file check, commitlint, repository commit-message checker, and git diff --check: passed.
  • Current GitHub checks are green for the extension, TUI, bridge, Windows self-upgrade, lint, repository files, commit messages, PR title, and pre-commit diff; the unrelated Python unit job was still pending when reviewed.

LivXue added 2 commits August 29, 2026 16:15
Probe the login shell environment (bash/zsh on posix, powershell.exe/pwsh on Windows) and merge it into the Raven terminal so proxy settings and API keys from the user's shell rc apply inside VS Code, fixing OpenRouter regional 403s that only appeared when launching raven through the extension. The probe is cached after the first open.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Blocking: the Windows environment probe skips the user's PowerShell profile, and the new commits have made the PR description stale again; see the inline note and summary below.

The live description still says version 0.1.1, 23 tests, and a 0.1.1 VSIX, while this revision is 0.1.2 with 31 tests. More importantly, its Risk section says the extension handles no credentials, but this head deliberately copies proxy settings and API keys into the Raven terminal. Because the description becomes the squash commit body, AGENTS.md section 3.7 requires the Summary, Verification, and Risk sections to describe this current behavior and exact results. Please update those claims before merge.

Coverage: I reviewed the delta from 48fd061, rechecked the full current github/main...HEAD diff and live PR description, AGENTS.md, CLAUDE.md, CONTEXT-MAP.md and the TUI glossary, the extension entry point, path probing, VS Code API adapter, terminal manager and tests, branch history, backward compatibility, package metadata, and whether tests were weakened. The eight new tests are additive and existing coverage remains, although the Windows test currently asserts the defective -NoProfile invocation rather than validating a profile-defined variable.

Verification:

  • npm ci, lint, type-check, test, build, and formatting checks: passed; Vitest reported 3 files and 31 tests passed. npm ci emitted local EBADENGINE warnings because Node v23.10.0 is outside two dependencies' supported even-major ranges; CI uses Node 22.
  • npm audit --audit-level=critical: passed with 0 vulnerabilities.
  • VSIX packaging: passed; raven-vscode-0.1.2.vsix contained 11 files and was 11.99 KB.
  • Large-file check, commitlint, repository commit-message checker, and git diff --check: passed.
  • Current GitHub checks are green for the extension, TUI, bridge, Windows self-upgrade, lint, repository files, commit messages, PR title, and pre-commit diff; the unrelated Python unit job was still pending when reviewed.

Comment thread vscode-ext/src/pathProbe.ts Outdated
const script =
'[Console]::OutputEncoding=[System.Text.Encoding]::UTF8; Get-ChildItem Env: | ForEach-Object { "$($_.Name)=$($_.Value)" }'
for (const shell of ['powershell.exe', 'pwsh']) {
const result = runner(shell, ['-NoProfile', '-NonInteractive', '-Command', script], {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P1] Load the PowerShell profile before reading its environment

-NoProfile explicitly prevents PowerShell from loading $PROFILE. For a GUI-launched VS Code where an API key or proxy variable is assigned in that profile, this subprocess only repeats the environment VS Code already had, so Raven still misses the value and the advertised Windows behavior is ineffective. The test currently locks in this flag instead of exercising a profile-defined variable. Please probe the user's actual profile-loading shell environment and cover that case.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 1e73eef: -NoProfile removed so the probe subprocess loads the user PowerShell profile, keeping -NonInteractive to avoid prompts; timeout raised to 15000 for profile startup cost. The test now asserts the profile-loading invocation.

Drop -NoProfile so the env probe subprocess loads the user's PowerShell profile, where proxy and API key variables live; keep -NonInteractive to avoid prompts and raise the timeout for profile startup cost.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Blocking: removing -NoProfile fixes the prior finding for Windows PowerShell, but the probe still bypasses PowerShell 7 profiles, and the previously reported PR-description blocker remains unresolved; see the inline note.

The live Summary, Verification, and Risk sections still contain the already-reported 0.1.1, 23-test, and no-credentials claims. Appending a round note does not correct the current-state sections that become the squash commit body, so that blocker remains.

Coverage: I reviewed the delta from f2a30d8, rechecked the full current github/main...HEAD diff and live PR description, AGENTS.md, CLAUDE.md, CONTEXT-MAP.md and the TUI glossary, path probing and its callers, the VS Code API adapter, terminal lifecycle, branch history, backward compatibility, package metadata, and test changes. The modified test accurately verifies removal of -NoProfile, and no existing test was weakened, but it does not cover selecting the user's actual PowerShell edition/profile.

Verification:

  • npm ci, lint, type-check, test, build, and formatting checks: passed; Vitest reported 3 files and 31 tests passed. npm ci emitted local EBADENGINE warnings because Node v23.10.0 is outside two dependencies' supported even-major ranges; CI uses Node 22.
  • npm audit --audit-level=critical: passed with 0 vulnerabilities.
  • VSIX packaging: passed; raven-vscode-0.1.2.vsix contained 11 files and was 11.97 KB.
  • Large-file check, commitlint, repository commit-message checker, and git diff --check: passed.
  • Current GitHub checks are green for the extension, TUI, bridge, Windows self-upgrade, lint, repository files, commit messages, PR title, and pre-commit diff; the unrelated Python unit job was still pending when reviewed.

Comment thread vscode-ext/src/pathProbe.ts Outdated
function probeWindowsEnv(runner: ProbeRunner): Record<string, string> | null {
const script =
'[Console]::OutputEncoding=[System.Text.Encoding]::UTF8; Get-ChildItem Env: | ForEach-Object { "$($_.Name)=$($_.Value)" }'
for (const shell of ['powershell.exe', 'pwsh']) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P1] Select the user's actual PowerShell profile

On a normal Windows host, powershell.exe is present and this command returns a nonempty environment, so the loop exits before ever trying pwsh. A user who has PowerShell 7 configured in VS Code and defines the proxy or API key in that edition's distinct $PROFILE still gets the Windows PowerShell profile instead, leaving Raven without the value. Please derive the configured/default shell (or otherwise select the correct edition) and cover the PowerShell 7 profile case.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 466e9aa: the Windows env probe now derives the VS Code-configured shell from terminal.integrated.defaultProfile.windows / profiles.windows, probes that edition first, and merges powershell.exe and pwsh environments with the preferred edition winning conflicts. Tests cover the PowerShell 7 profile case.

…ofiles

Derive the user's Windows shell from terminal.integrated.defaultProfile.windows and probe that edition first, then merge the powershell.exe and pwsh environments with the preferred edition winning conflicts, so profile-defined variables from PowerShell 7 reach raven even when Windows PowerShell also succeeds.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Blocking: explicit path-based PowerShell 7 profiles now win correctly, but source-based and auto-detected VS Code profiles still lose their edition hint, and the previously reported PR-description blocker remains unresolved; see the inline note.

The live Summary, Verification, and Risk sections still contain the already-reported 0.1.1, 23-test, and no-credentials claims. Appending another review-round note does not correct those current-state sections, which become the squash commit body.

Coverage: I reviewed the delta from 1e73eef, rechecked the full current github/main...HEAD diff and live PR description, AGENTS.md, CLAUDE.md, CONTEXT-MAP.md and the TUI glossary, VS Code's terminal-profile configuration contract, path probing and configuration callers, the API adapter, terminal lifecycle, branch history, backward compatibility, package metadata, and test changes. The merge-precedence tests are additive and existing tests were not weakened, but they inject a literal path directly and do not exercise shellHint's configuration boundary.

Verification:

  • npm ci, lint, type-check, test, build, and formatting checks: passed; Vitest reported 3 files and 32 tests passed. npm ci emitted local EBADENGINE warnings because Node v23.10.0 is outside two dependencies' supported even-major ranges; CI uses Node 22.
  • npm audit --audit-level=critical: passed with 0 vulnerabilities.
  • VSIX packaging: passed; raven-vscode-0.1.2.vsix contained 11 files and was 12.25 KB.
  • Large-file check, commitlint, repository commit-message checker, and git diff --check: passed.
  • Current GitHub checks are green for the extension, TUI, bridge, Windows self-upgrade, lint, repository files, commit messages, PR title, and pre-commit diff; the unrelated Python unit job was still pending when reviewed.

Comment thread vscode-ext/src/extension.ts Outdated
const config = api.workspace.getConfiguration('terminal.integrated')
const defaultProfile = config.get<string>('defaultProfile.windows')
const profiles = config.get<Record<string, { path?: string }>>('profiles.windows')
return (defaultProfile && profiles?.[defaultProfile]?.path) || undefined

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P1] Resolve source-based and auto-detected profiles

VS Code supports Windows profiles declared as { source: "PowerShell" } and auto-detects default profiles without requiring a literal path (official profile documentation). For those common PowerShell 7 configurations, this expression returns undefined; probeWindowsEnv then uses its generic order and lets powershell.exe win conflicts, so the PowerShell 7 $PROFILE value is still lost. The new test bypasses this boundary by passing a path directly. Please resolve the actual configured/default executable for source-based and auto-detected profiles and cover that configuration path.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 8463ac1: the shell hint now comes from vscode.env.shell, which VS Code itself resolves from terminal.integrated.defaultProfile including source-based and auto-detected profiles; the manual profiles.windows path lookup remains only as a fallback. resolveWindowsShellHint is a pure function with tests covering env.shell preference, path fallback, and source-based profiles without a path.

Use vscode.env.shell, which VS Code resolves from defaultProfile including source-based and auto-detected profiles, as the preferred Windows shell hint; the manual profiles.windows path lookup stays as a fallback. Covers PowerShell 7 setups declared without a literal path.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Blocking: the source-based/auto-detected profile defect is fixed and I found no new code issue, but this commit has made the PR description's exact verification stale again.

vscode.env.shell is the detected extension-host shell and is overridden by terminal.integrated.defaultProfile, so it supplies the resolved executable that the prior implementation lacked. The fallback remains available when that value is empty, and the new pure-function tests cover its precedence.

The live Verification and Summary still report 32 tests and a 12.25 KB VSIX, while this revision actually has 36 passing tests and packages 11 files at 12.33 KB. Because these current-state sections become the squash commit body and AGENTS.md section 3.7 requires exact commands and results, please update those two figures before merge.

Coverage: I reviewed the delta from 466e9aa, rechecked the full current github/main...HEAD diff and live PR description, AGENTS.md, CLAUDE.md, CONTEXT-MAP.md and the TUI glossary, the VS Code env.shell and terminal-profile contracts, API adapter and configuration callers, path probing, terminal lifecycle, branch history, backward compatibility, package metadata, and test changes. The four added tests are additive and no existing tests were weakened.

Verification:

  • npm ci, lint, type-check, test, build, and formatting checks: passed; Vitest reported 3 files and 36 tests passed. npm ci emitted local EBADENGINE warnings because Node v23.10.0 is outside two dependencies' supported even-major ranges; CI uses Node 22.
  • npm audit --audit-level=critical: passed with 0 vulnerabilities.
  • VSIX packaging: passed; raven-vscode-0.1.2.vsix contained 11 files and was 12.33 KB.
  • Large-file check, commitlint, repository commit-message checker, and git diff --check: passed.
  • Current GitHub checks are green for the extension, TUI, bridge, Windows self-upgrade, lint, and pre-commit diff; the unrelated Python unit job was still pending when reviewed.

@LivXue

LivXue commented Aug 29, 2026

Copy link
Copy Markdown
Member Author

Verified against the live description: the Verification and Summary sections now report the current figures (3 files / 36 tests, VSIX 11 files / 12.33 KB). The remaining mentions of 32 tests are inside the historical Round 7 note, which describes the state at that revision. No code change was needed for this round.

@gloryfromca

Copy link
Copy Markdown
Member

Confirmed. The live current-state Summary and Verification now report 3 files / 36 tests and an 11-file / 12.33 KB VSIX. The remaining 32-test text is clearly historical Round 7 context, so it does not contradict this revision. I reran npm test --prefix vscode-ext on the unchanged head (36/36 passed) and git diff --check github/main...HEAD also passed. This resolves my remaining blocker.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No blockers; this can merge as far as I am concerned.

The live PR description now resolves the only remaining blocker: its current-state Summary and Verification accurately report version 0.1.2, 3 files / 36 tests, and an 11-file / 12.33 KB VSIX, while the 32-test mention is correctly scoped to historical Round 7 context. No code changed since my review of this head, where the vscode.env.shell fix resolved source-based and auto-detected PowerShell profiles without introducing a new code issue.

Verification on this answer turn: npm test --prefix vscode-ext passed all 36 tests, and git diff --check github/main...HEAD passed.

Sanitize the extraArgs setting at the config boundary: non-array values become an empty list and only string entries survive, so a misconfigured string no longer spreads into single-character argv entries. Add sanitizeExtraArgs with unit tests.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No blockers; this can merge as far as I am concerned.

The new config-boundary sanitizer fixes the reported runtime type hazard: a scalar raven.extraArgs value now becomes an empty list, mixed arrays cannot pass non-string values into terminal argv, and valid string arrays preserve their order and contents. The live PR description accurately reflects this revision's 40-test and 12.41 KB package results.

Coverage: I reviewed the delta from 8463ac1, rechecked the full current github/main...HEAD diff and live PR description, AGENTS.md, CLAUDE.md, CONTEXT-MAP.md and the TUI glossary, the extension configuration boundary, Raven command construction and callers, package schema, branch history, backward compatibility, package metadata, and test changes. The four sanitizer tests are additive and no existing test was weakened.

Verification:

  • npm ci, lint, type-check, test, build, and formatting checks: passed; Vitest reported 3 files and 40 tests passed. npm ci emitted local EBADENGINE warnings because Node v23.10.0 is outside two dependencies' supported even-major ranges; CI uses Node 22.
  • npm audit --audit-level=critical: passed with 0 vulnerabilities.
  • VSIX packaging: passed; raven-vscode-0.1.2.vsix contained 11 files and was 12.41 KB.
  • Large-file check, commitlint, repository commit-message checker, and git diff --check: passed.
  • Current GitHub checks are green for the extension, TUI, bridge, Windows self-upgrade, lint, repository files, commit messages, PR title, and pre-commit diff; the unrelated Python unit job was still pending when reviewed.

LivXue and others added 3 commits August 31, 2026 10:45
…ify headroom (#372)

## Summary

Onboard model-default refreshes plus an EverOS startup fix:

1. Rerank default: Qwen/Qwen3-Reranker-4B has been retired from
OpenRouter, so the onboard wizard's rerank example and EN/ZH
recommendations now point to qwen/qwen3-reranker-8b, which OpenRouter
still serves.

2. Stale defaults: the memory-LLM capability floor (gpt-4.1-mini), the
multimodal recommendation (google/gemini-3-flash-preview), and the Azure
OpenAI provider default (gpt-5.2-chat) all point at generations behind
what OpenRouter/Azure serve today. They now read qwen/qwen3.8-flash (the
EverOS client always sends temperature, which the GPT-5.x reasoning
family rejects), google/gemini-3.7-flash, and gpt-5.6-sol. GPT-5.x Azure
deployments force reasoning_effort="none" on tool calls, matching
Microsoft's tool-calling requirement; configured effort still applies to
non-tool calls.

3. Startup failure: an exhausted fs.inotify.max_user_instances makes the
spawned EverOS server's watcher die at boot with "OSError: [Errno 24]
inotify instance limit reached", which surfaces only as a cryptic exit
buried in the dead-child log. ensure_everos_server now measures per-user
inotify headroom (counting same-uid inotify instances via /proc/<pid>/fd
links that read anon_inode:inotify against the kernel cap) before
spawning: it raises the cap when the process is privileged, otherwise it
fails fast with the exact sudo sysctl commands. A dead child whose log
blames inotify gets the same hint appended to its error.

## Type

- [x] Fix

## Verification

- make coverage (full default suite, uv run --frozen --python 3.12
--all-extras pytest -q): 7119 passed, 2 failed, 37 skipped, 13
deselected. The 2 failures are pre-existing on main and
environment-dependent (reproduced on main in a throwaway worktree): both
expect a chmod-based write to be refused, which cannot happen when the
suite runs as root (CAP_DAC_OVERRIDE):
-
tests/test_config_loader.py::test_migration_is_correct_even_when_the_file_cannot_be_written
-
tests/test_everos_server.py::TestAnUnwritableRootFailsAsAStartFailure::test_it_surfaces_as_runtime_error
- make coverage-diff COVERAGE_BASE_REF=origin/main: 100.00% (57/57
executable changed lines), passed the 90.00% threshold
- make coverage-ratchet: passed (line +2.11pp, branch +3.24pp over
baseline)
- uv run pytest tests/test_cli_onboard_commands.py: 251 passed
- uv run pytest tests/test_azure_openai_provider.py: 9 passed
- uv run ruff check (changed files): clean
- uv run ruff format --check (changed files): clean
- pre-commit commitlint hook (conventional commit message): passed on
all commits

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally

## Risk

- Security impact considered: the sysctl write runs only when the
process already holds the privilege to write /proc/sys; otherwise the
gate only returns the instructions.
- Backward compatibility considered: the gate is a no-op when headroom
exists or when procfs/sysctl cannot be read (non-Linux); the
model-default changes are wizard text and one provider default constant,
no config migration needed.
- Rollback path is clear: revert the merge; the check is isolated to
ensure_everos_server and three module-level helpers.

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

## Related Issues

N/A
## Summary

The Feishu adapter's reconnect loop retried on a fixed 5s sleep with no
backoff and no attempt cap. lark-oapi's ws Client.start() blocks while
the connection lives (the SDK reconnects transient drops internally with
its own 120s interval) and only returns by raising, almost always a
ClientException such as bad credentials or connection-limit exceeded.
The old loop therefore hammered Feishu's auth endpoint every 5s on
permanent failures.

Changes:

- Retries now use exponential backoff capped at 300s (5s initial, factor
  2): 5s, 10s, 20s, 40s, 80s, 160s, 300s.
- ClientException is logged as a permanent error instead of a generic
  warning.
- The backoff sleep runs in 1s slices so stop() can interrupt a long
  backoff promptly (no lingering thread).

Three tests added covering ladder growth/cap, ClientException handling,
and stop() interrupting the sleep.

## Type

- [x] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed

Commands and results:

- `uv run pytest tests/test_channels_feishu.py -q` -> 38 passed
  (1 warning is pre-existing, from test_stop_blocks_zombie_inbound).
- `uv run ruff check raven/channels/adapters/feishu/channel.py
  tests/test_channels_feishu.py` -> All checks passed.
- `uv run ruff format raven/channels/adapters/feishu/channel.py
  tests/test_channels_feishu.py` -> 2 files reformatted (applied).
- pre-commit hooks all passed, including the conventional commit message
  check.

No user-facing docs affected: this is an internal retry-cadence change
with no config or interface change.

## Risk

- [x] Security impact considered
- [x] Backward compatibility considered
- [x] Rollback path is clear for risky changes

No security surface change: only retry timing is affected. No config,
CLI, or API change; existing deployments behave the same apart from
slower retries after repeated connection failures. Rollback: revert the
single commit.

## Related Issues

N/A

---------

Co-authored-by: Claude (deepseek-v4-pro) <noreply@anthropic.com>

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No blockers; this can merge as far as I am concerned.

This head only merges the latest main into the already-reviewed feature branch. I compared the old and new branch ranges: all 15 feature commits are patch-identical, and the merge has no conflict-resolution delta. I also reread the current github/main...HEAD diff and the affected extension entry point, command resolution, environment probing, terminal lifecycle, repository wiring, and tests.

Covered: AGENTS.md / CLAUDE.md / CONTEXT-MAP.md and the TUI context rules, the full PR diff, affected callers and history, backward compatibility, and whether tests were weakened. I found no new issue.

Verification on 3b90f78: npm ci; lint; type-check; 40/40 tests; build; formatting; npm audit (0 vulnerabilities); VSIX packaging (11 files, 12.41 KB); large-file check; commitlint; repository commit-message check; and git diff --check all passed. Live CI has all reported checks green except the unrelated Python unit job, which is still pending; the VS Code extension job passed.

@gloryfromca
gloryfromca force-pushed the feat/vscode_tui_extension branch from 3b90f78 to 810ff21 Compare September 11, 2026 17:43
@gloryfromca

Copy link
Copy Markdown
Member

This PR was auto-closed by a history rewrite, not by a maintainer

On 2026-09-12 the main branch of this repository (and its GitLab mirror) was replaced with a linearized history: all merge commits were folded into a single line and author emails were unified. Every commit received a new SHA, so GitHub automatically closed every open pull request whose branch was touched. The GitHub API refuses to reopen them.

Your work is safe. The source branch of this PR was rewritten onto the new main; its file contents are byte-for-byte identical to what they were before, and it still shows the correct diff against main.

To continue: open a new PR from the same branch. Please reference this PR number so the discussion stays traceable.

The pre-rewrite history is preserved at the tag backup/pre-linearize-20260912.

Sorry for the noise.

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.