Skip to content

chore(*): catch the ui-web rebuild up to main - #529

Merged
gloryfromca merged 50 commits into
refactor/ui_web_architecturefrom
chore/merge_main_into_ui_web
Sep 20, 2026
Merged

gloryfromca merged 50 commits into
refactor/ui_web_architecturefrom
chore/merge_main_into_ui_web

Conversation

@gloryfromca

@gloryfromca gloryfromca commented Sep 20, 2026 •

Copy link
Copy Markdown
Member

Summary

refactor/ui_web_architecture last shared a commit with main at 5a29ea22
(#451). This merges main (dbe259f6) into it so the conflicts get settled
once, now, instead of all at once under #475, and so CI runs on the merged tree
rather than on a branch GitHub already reports as CONFLICTING.

Seven paths conflicted. How each was settled:

  • ui-web/src/features/settings/SettingsPage.tsx, its test, and
    ui-web/src/live/120-settings.js -- deleted on this branch, edited on main
    (modify/delete, so git leaves main's copy in the tree). The rebuilt settings
    surface replaces all three; the deletions stand.
  • i18n/messages.json and ui-tui/src/i18n/messages.generated.ts -- main's
    only change to either since the merge base is fix(*): let the web settings timezone control write, and drop the retired one #506 dropping
    gui.set.chn.cron_to and cron_ph (8 and 4 lines, no additions). This branch
    already drops the whole gui.set.chn.* group, so its side is the superset and
    wins.
  • raven/skill_hub/hub.py -- each side added one import from
    skill_hub.audit. write_install_meta is called at line 621,
    INSTALL_META at line 699, and both exist in the merged audit.py, so both
    are imported.
  • ui-web/build.py -- main rewrote the newline="" comment, this branch
    added flush=True to the print under it. Both kept.

What came in quietly and is worth knowing: #506's cron timezone fix auto-merged
into raven/rpc/methods/console.py and brought its five cases in
tests/test_rpc_settings.py with it. The rebuilt settings pages have no cron
section at all, so on this branch the backend can now write
cron.defaultTimezone correctly and nothing on the page does it. Whether that
control gets ported, and in which PR, is a separate decision.

Type

  • Fix
  • Feature
  • Docs
  • CI / tooling
  • Refactor
  • Other (catch-up merge)

Verification

Run in a worktree on the merged commit:

  • uv run pytest tests/test_rpc_settings.py tests/test_skill_hub_client.py tests/test_skill_hub_policy.py tests/test_skill_hub_source.py tests/test_skill_hub_tools.py tests/test_i18n_boundary.py -q -- 174 passed.
  • npm test in ui-web -- 181 files, 2513 tests, all passed.
  • node ui-tui/scripts/gen-i18n.mjs -- regenerating the TUI catalogue leaves
    messages.generated.ts byte-identical to the resolved file, so the two i18n
    artifacts agree.
  • make lint-python -- 1986 files already formatted, clean.
  • make lint-imports -- 10 contracts kept, 0 broken.
  • make build-ui -- page built, boot-snapshot: OK on both snapshots.
  • scripts/check_source_language.py and scripts/check_large_files.py over
    bc74bf73..HEAD -- both exit 0.

The full suite was left to CI.

commit messages is red, and will be red on any PR that merges main into a
branch. Commitlint runs over <base>..HEAD, which here includes every commit
main gained since the merge base. One of them, b01534d0 (#471, merged
09-18), carries a Co-authored-by trailer with a non-ASCII name, which
scripts/check_commit_messages.py rejects. That is history on main; this
branch neither introduced it nor can rewrite it. Nothing in the ruleset makes
that check blocking.

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

Risk

This branch is squash-only, so merging this PR flattens the merge commit and
the merge base stays at 5a29ea22. The contents line up either way, but git
will not know main was taken in, and the next catch-up will re-raise the
content conflicts in the two large i18n files. Landing it as a real merge
commit instead would move the merge base and avoid that; that needs someone who
can bypass the ruleset.

Rollback is resetting the branch to bc74bf73; nothing here is a source change
of its own beyond the four resolutions above.

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

Related Issues

N/A. Unblocks #475.

xfng-sd and others added 30 commits September 17, 2026 11:56
…ads (#463)

## Summary

`main` is red on three jobs (`python lint`, `unit 2/4`, `unit 3/4`), and
has been
since #451 merged. All of it is fallout from one arrival: `image_search`
came with a
second switch beside its credential.

`tools.web.search.images` decides whether the tool is registered at all
(`raven/agent/loop/wiring.py`), so a lane that never places a picture
keeps the tool
face it had. The capability table was not told. `is_configured` answers
on the vendor
key alone, so with a Serper key and the switch left off the table ticked
a capability
the agent does not hold, and the two tests that drive a real `AgentLoop`
caught exactly
the disagreement that module exists to prevent.

The switch belongs on `is_disabled`, not `is_configured`. That
function's own docstring
already names the reason: a switched-off tool usually has its credential
set, and
calling it unconfigured sends the deployer to set a key that is already
there.
`tools.disabledTools` and this flag are the same kind of answer, so
`is_offered` needs
no change and the doctor row reads "off" rather than "missing a key".

The test configs that mean "everything supplied" now turn the switch on,
the way
`raven/core/runtime.py` passes it from the config, and a new case pins
the combination
that was wrong: key present, pictures off, nothing offered, table saying
off.

Two more pieces of the same arrival:

- `ImageSearchTool` copied `WebSearchTool`'s key resolution without its
two attribute
annotations, so the untyped lambda `wiring.py` hands it left the
`api_key` property
returning `~AlwaysFalsy | str` against a declared `str`. The type
checker pinned in
  #416 reports it; the annotations are the fix.
- `WebSearchConfig` gained the `images` field, so the writer's validated
subtree now
carries it and two `test_config_update_tools` expectations that pin the
written
  section by value went stale.

Not changed here: `get_web_search` still reports `{provider, api_key,
max_results}` and
does not surface `images`, though `set_web_search` can write it. That is
a gap in the
read side of the config API, not a CI failure, and it wants its own
change.

## Type

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

## Verification

Run on Windows 11 in a clean worktree cut from `main` (`5a29ea22`):

- `uv run pytest tests/test_tool_capabilities.py
tests/test_config_update_tools.py -q`:
71 passed. All four tests failing on `main` now pass, plus the new case.
- `uv run --frozen --python 3.12 --all-extras ty check raven evolver
agents plugins-dist scripts`:
the `web.py:516` diagnostic is gone. The diagnostics that remain locally
are
unresolved imports for optional packages absent from this machine's
environment
(`boxlite`, `nio`); CI reported exactly one diagnostic before this
change, the one
  fixed here.
- `uv run --frozen --python 3.12 --extra dev ruff check raven tests`:
all checks passed.
- `ruff format --check` on the four changed files: already formatted.
- `uv run pytest tests/test_cli_doctor_commands.py
tests/test_rpc_console.py -q`, which
consume `is_disabled`: the failing-test set is byte-identical before and
after this
change (8 pre-existing Windows-only failures around file modes and an
absent browser
  package, none of them in the set CI reports).

## Risk

`is_disabled` gains a second reason to answer true, so a deployment with
a search key
and `tools.web.search.images` off now reads as "switched off" in `raven
doctor` and the
console capability report, where it previously read as offered. That is
the state the
loop was already in; only the description changes. Nothing about
registration, gating,
or the tool's runtime behaviour moves.

The other two changes are an annotation and two test expectations, with
no runtime
effect.

Rollback is reverting this commit; it touches nothing that other work
builds on.

## Related Issues

N/A

---------

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
… busy runner (#466)

## Summary

Since the idle ceiling became a gate (#428), a unit shard has failed on
`main` and on most pull requests with every test passing: the ceiling
reads wall clock minus CPU and calls the difference waiting, and a
thread that is runnable and not on a CPU spends it the same way. With
four workers, the controller and coverage on a four-vCPU runner that
queueing reached three to four seconds inside one long test, so the
shard died on whichever test was executing when contention peaked, a
different one each run. Warming the catalogue (#452) did not stop it.
#449 halves the workers per shard, which pays for the same measurement
with every shard's wall clock.

This changes the measurement instead. Idle now subtracts the time the
test's threads spent queued for a CPU, read from the scheduler's own
account in `/proc/self/task/<tid>/schedstat`:

- every thread of the process is read, not only the one running the
test, because the rpc model handlers do their catalogue reads on
`asyncio.to_thread`;
- a thread writes its own last reading on the way out, through a wrapper
on `Thread._bootstrap_inner`, because the per-test event loop is closed
at the end of the test and takes its executor threads with it, and
`/proc` forgets a thread the moment it ends;
- CPU stays on `os.times`, which keeps the CPU of ended threads and of
reaped children; the scheduler's per-thread run time would have been
lost the same way the queue time was.

A sleeping thread is not on a run queue, so a test that waits on purpose
is still charged the whole wait. Without `schedstat` (macOS, Windows)
the arithmetic is the old one; the strict gate only runs on Linux, and a
Linux-only test fails loudly if the runner's kernel stops keeping the
statistics rather than letting the gate degrade quietly. `-n 4`, the 3 s
ceiling and the four shards are unchanged. If this lands, #449 is not
needed.

## Type

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

## Verification

Unit tests for the clocks (12 on macOS, 13 on Linux, the last one reads
the live kernel):

```
uv run --frozen --all-extras pytest -q tests/test_conftest_idle_ceiling.py
  macOS: 12 passed, 1 skipped
  Linux container: 13 passed
```

Full suite on macOS, where the fallback arithmetic runs:

```
uv run --frozen --all-extras pytest -q -p no:cacheprovider
  23137 passed, 109 skipped in 291.30s
```

Contrast runs in a Linux container (Docker, linux/arm64, kernel 6.12),
the checkout and venv on container-local storage, `main` at 2bdc4bd
against this branch. Four workers pinned to two CPUs stands in for the
four-vCPU runner; the ceiling is lowered to 0.05 s so the effect shows
on one file:

```
docker run --cpuset-cpus=0-1 ... pytest tests/test_rpc_model.py --idle-ceiling 0.05 -n 4
  main:   70 unmarked tests over the ceiling, worst 1.67s idle of 2.89s
  branch:  2 unmarked tests over the ceiling, worst 0.12s idle of 0.50s (0.19s queued)

docker run --cpuset-cpus=0-3 ... pytest tests/test_rpc_model.py --idle-ceiling 0.05 -n 16
  main:   81 over, worst 6.11s idle of 8.16s
  branch:  1 over, 0.13s idle of 1.11s (0.73s queued)

an unmarked test that does time.sleep(0.5), on the branch:
  0.53s idle of 1.70s (0.00s queued)   # an honest wait is still charged in full
```

The CI shard command, same container, four workers pinned to two CPUs:

```
pytest -q --shard 3/4 --durations=25 --idle-ceiling-strict -n 4 --cov=raven --cov=raven_everos --cov-branch --cov-report=
  main:   5131 passed, 10 skipped; "1 unmarked test(s) waited more than 3s (failing the session)"
          3.11s idle of 6.04s tests/test_ppt_engine_template_tool.py::test_binding_a_template_by_path; exit 1
  branch: 5378 passed, 11 skipped; no idle report; exit 0
  branch, shards 1/4, 2/4, 4/4: no idle report in any (three failures, all `git ls-files` exit 128 because
          the copied worktree's gitdir is not inside the container)
```

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [ ] User-facing docs or screenshots are updated when needed (none; the
option help text and the summary line say what is now subtracted)

## Risk

- Behaviour change is confined to what the ceiling charges a test: less
on a busy runner, the same for a real wait. Nothing under `raven/`
changes.
- `Thread._bootstrap_inner` is private CPython API (present from 3.8
through 3.13); the wrapper is installed once, guarded against a second
import of the module, and pinned by a test. If a future interpreter
drops it the module fails to import at collection, which is loud.
- Rollback: revert the commit; the gate goes back to wall clock minus
CPU.

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

## Related Issues

#428 made the ceiling a gate; #452 warmed the catalogue; #449 halves the
workers instead.

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

## Summary

`os.kill(pid, 0)` is the POSIX existence probe. On Windows, CPython
implements
`os.kill` as `TerminateProcess`, so signal 0 is not a probe there at
all, and it fails
in two directions depending on what the caller may do to the target:

- a process the caller can terminate, which is any child of it, is
killed and the call
returns cleanly, so the probe reports "alive" about a process it has
just destroyed;
- a process it cannot, such as the detached `raven web` supervisor,
raises `OSError`
with `ERROR_INVALID_PARAMETER`, so the probe reports "dead" about a
process that is
  running.

The second is why `raven web --stop` did nothing on Windows.
`_read_web_state` and
`_read_serve_pid` both answered `None`, so `_stop_resident` signalled
neither the
supervisor nor the gateway, and printed "nothing to stop" while the
engine went on
serving the page.

`raven/utils/pid.py` holds the platform question now. Windows asks
`OpenProcess` plus
a zero-timeout `WaitForSingleObject`, the pair
`raven/updates/upgrade.py` already uses
to watch a parent exit: a handle that fails to open for any reason but
`ERROR_ACCESS_DENIED` is a dead process, and one that opens and then
times out is a
live one. The wait rather than `GetExitCodeProcess`, because a process
whose real exit
code is 259 cannot be told apart from `STILL_ACTIVE`.

`_pid_alive` keeps its name in `serve_commands`, since `_gateway_page`
imports it and
the stop tests patch it; only the answer moves.

Four other call sites carry the same probe and are left for their own
change. The
sharpest is `raven/agent/tools/background_exec.py`, which reaches it on
the Windows
branch specifically, against the agent's own background tasks.

## Type

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

## Verification

Run on Windows 11, in a clean worktree cut from `main`:

- `uv run pytest tests/test_utils_pid.py
tests/test_cli_serve_commands.py -q`:
  77 passed, 3 failed.
- the same 3 failures reproduce on `main` without this change
(`test_the_state_file_stays_owner_only`,
`test_sigterm_removes_the_state_file`,
`test_sigterm_during_the_prologue_still_removes_the_state_file`): they
are
pre-existing Windows gaps in the supervisor tests, unrelated to this PR,
and this
branch adds no new failure. Baseline on `main` is 73 passed, 3 failed;
the 4 extra
  passes are the new `tests/test_utils_pid.py`.

The new test drives real subprocesses. Survival is asserted by a wait
that must time
out, not by a poll taken straight after the probe: termination is
asynchronous, and
the immediate read still says "running" for a process already on its way
down. Forcing
the POSIX branch on every platform fails two of the four cases.

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

## Risk

POSIX behaviour is unchanged: the helper keeps calling `os.kill(pid, 0)`
there. On
Windows, `raven web --stop` starts working and stops silently
terminating child
processes it only meant to look at, which is the point of the change.

Rollback is reverting to the inline `os.kill(pid, 0)` in
`raven/cli/serve_commands.py`
and dropping `raven/utils/pid.py`.

- [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>
…es (#457)

## Summary

A Windows checkout of this repo with the default `core.autocrlf=true`
rewrites every
text file to CRLF, and two byte-exact gates in `ui-web` then fail on a
tree that is
byte-identical to the one CI passes on Linux:

- the boot-order and open-conversation node tests slice their source on
`"\n}\n"`,
which never matches a CRLF file, so the sliced function comes out empty
and 9 tests
  fail with `ReferenceError`;
- `gen-rpc-client --check` compares its own LF output against the file
on disk and
  reports permanent drift in `src/rpc/generated.ts`.

`.gitattributes` pins the checkout filter so the working tree matches
what the
repository already stores. The repository stores LF today, so this adds
no
renormalization diff: `git add --renormalize .` over the whole tree
stages nothing
beyond the two files in this PR. Only the checkout filter changes.

`ui-web/build.py` is the other half of the same defect. It assembled the
page under
Python's default newline translation, so `dist/index.html` came out CRLF
on Windows
even from an LF source tree, and `check-page.mjs` could no longer
extract the script
payloads (its `<script>\n` anchor needs the LF to be the next byte).
That artifact
ships inside the wheel, so it must not depend on which platform
assembled it.

## Type

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

## Verification

Run on Windows 11 with `core.autocrlf=true`, in a clean worktree cut
from `main`:

- `git add --renormalize .` after the change stages nothing (confirms no
  renormalization diff);
- `file ui-web/src/rpc/generated.ts` reports CRLF on `main` and LF after
a forced
  re-checkout on this branch;
- `npm run gen:check` (ui-web): `generated.ts matches the contract (178
methods)`;
- `npx vitest run` (ui-web): 108 files, 1836 tests passed;
- `npm run --prefix ui-web build && python ui-web/build.py`:
`dist/index.html` is LF;
- `node ui-web/scripts/check-page.mjs`: `OK (1,482,975 bytes, 3 scripts
parse)`;
- `node ui-web/scripts/count-shared-globals.mjs`, `check-css.mjs`,
  `check-class-namespace.mjs`: all OK.

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

## Risk

No runtime behaviour changes for users on Linux or macOS, where the
checkout already
produced LF. On Windows the working tree stops being rewritten to CRLF,
so a
contributor with existing local checkouts may see one large
renormalization touch on
the next checkout; the committed bytes do not move. The built
`dist/index.html` is now
LF on every platform, which is what the Linux-built wheel already
shipped.

Rollback is deleting `.gitattributes` and dropping the `newline=""`
argument in
`ui-web/build.py`.

- [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>
…here (#464)

## What

A skill installed through the market had no way to be removed from the
page, because `ext.list` decided "was this installed from the hub" by
looking for one marker file, and the installer most skills arrive by
writes a different one. Closes #456.

Two installers put hub skills on disk:

| | market module (`raven/skill_hub/hub.py`) | context engine +
`use_skill` (`raven/agent/tools/skill_hub.py`,
`raven/context_engine/segments/skills.py`) |
| --- | --- | --- |
| lands at | `<skills>/<name>/` |
`<skills>/hub/<slug>@<version>/<name>/` |
| stamps | `.skillhub.json` | `.install-meta.json` |

`ext.list` tested only for the first, so the second -- the ordinary path
-- reported `hub: false`. The page gates its removal control on that
flag, so it appeared for the exception and never for the rule. On the
machine this was found on, three of four installed skills were the
second kind; the one that showed a remove button was the one that came
through the market module.

## How

Backend only; nothing under `ui-web/` or `ui-tui/` changes. The page
already renders the control when `hub` is true, and already skips the
market-detail fetch when `hub_id` is empty, so both halves light up on
their own once the flag is right.

- `raven/skill_hub/audit.py` names the stamp it writes (`INSTALL_META`)
so the two readers test for the file the installer wrote rather than a
spelling of their own.
- `raven/rpc/methods/console.py` reports `hub: true` when either stamp
is present. The bundle stamp carries a slug, not a market id, so that
skill has an empty `hub_id` -- a state the page already handles.
- `raven/skill_hub/hub.py` lets `skillhub.remove` find a bundle by the
skill name inside it, resolving the wrapper folder the same way
`SkillHubClient._bundle_root` did at install time, and removes the
bundle whole: the bundle is the install unit, and the CLI's `skill
remove` deletes the same folder. Only a stamped directory is eligible --
a folder placed under `hub/` by hand is left alone, which is the rule
`MARKER` already enforced on the other layout.

The second change is not optional given the first: reporting `hub: true`
without it would put a remove button on a skill the handler then
refuses, which is worse than no button.

## Verification

-
`test_ext_list_calls_a_skill_hub_installed_whichever_installer_stamped_it`:
three skills, one per stamp and one with neither, asserting `(hub,
hub_id)` for each -- `(True, "uuid-market")`, `(True, "")`, `(False,
"")`. The marker names are pinned as literals in the test so a drift
between writer and reader is a failure, not a pass.
- `test_remove_reaches_a_bundle_the_other_installer_cached`: a stamped
bundle is removed whole by its skill name; an unstamped folder under
`hub/` is refused and left in place, and so is the neighbouring bundle.
- Mutation-checked: dropping the second stamp from `ext.list` fails
exactly the first test; dropping the bundle lookup from `remove` fails
exactly the second. 139 passed in the two suites on the unmutated tree.
- Dry-run against the real workspace: all four installed skills now
report `hub: true`; the three bundles resolve to their `<slug>@v0`
folders by skill name; the market-module skill and an unknown name
resolve to none.
- Ruff check and format clean.

## Not done here, on purpose

Removal does not stop the context engine re-installing the skill on its
next catalogue hit; the CLI documents the same limit and points at
`skill block`. Whether the page should offer that beside removal is a UI
question and out of scope while `ui-web/` is frozen. Also left as they
were: the two installers keeping two layouts and two stamps -- this
makes the readers agree with both writers rather than deciding which
writer should win.

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

---------

Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
Co-authored-by: KT <74288668+0xKT@users.noreply.github.com>
… group (#455)

## Summary

A conversation started in the TUI was listed by the served page and then
could not be opened: clicking it produced an empty transcript.

`SessionManager.session_path` looks for a transcript in this process's
own group, and falls back to searching the other groups when it is not
there. That fallback was gated on `project_slug`, so only a surface that
groups by launch directory ever ran it. The gateway groups by channel
and never did -- yet `list_sessions` scans every group, so the page
offered a conversation filed under a launch directory's slug and then
looked for it under `sessions/tui/`. `session.resume` missed the file,
fell through to its fresh-mint branch, and handed the reader an empty
transcript under a new id.

The write verbs resolve through the same function, so the miss was not
only a failed read. A rename, a pin or a turn from the page filed a
second transcript under the channel group, splitting one conversation
across two files:

```
sessions/-srv-alpha/abc.jsonl -> 2 messages
sessions/tui/abc.jsonl        -> 1 messages   (written by the gateway)
```

Ungating the search is necessary but not sufficient, and the second half
is the part worth reviewing. The glob matches on the chat_id, and a
chat_id names no channel: `cli:<id>` and `tui:<id>` are two
conversations, so an ungated search let a `tui:` save adopt the `cli:`
file and append to it. A candidate is now taken only once its own
metadata key says it is this session. A transcript carrying no key
predates that field and is still adopted on the stem alone, which is the
case the search was originally added for.

Matching on the stored key rather than on the directory name is also
what makes this correct on Windows. `key_from_path` tells a slug from a
channel by a leading `-`, which holds only for POSIX paths; a Windows
launch directory slugs to `C--Windows-system32`, with no leading
separator to read.

Root cause was confirmed against a control rather than by inspection:
two `tui:` sessions, same handler, same manager, differing only in which
group holds the file. The one under `sessions/tui/` resumed with its 26
messages; the one under a slug minted a fresh empty session.

## Type

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

## Verification

```
uv run pytest tests/test_session_project_grouping.py -q      2 failed, 32 passed
uv run pytest tests/test_cli_session_commands.py -q          43 passed
uv run ruff check raven/session/manager.py tests/test_session_project_grouping.py
                                                             All checks passed!
uv run ruff format --check <same two files>                  2 files already formatted
uv run ty check raven/session/manager.py                     All checks passed!
```

The 2 failures in `test_session_project_grouping.py` are present on
unmodified `main` in this Windows checkout and are not touched by this
change: `test_project_dir_is_not_rewritten_by_a_later_run_elsewhere` and
`test_slug_collides_for_a_separator_and_an_underscore` both build
fixtures from POSIX paths such as `/srv/alpha`, which `Path` renders as
`\srv\alpha` on Windows.

Regression scope was measured rather than asserted. Running `pytest
tests/ -k "session or transcript or workdir or resume"` (about 1085
tests) with and without the change gives **372 failures before and 372
after, with identical failure sets** -- no test changes state in either
direction. The same comparison over the session-adjacent files alone
gives 156 before and 156 after.

Five tests were added to `tests/test_session_project_grouping.py`,
extending the existing file rather than adding a new one. Two of them
fail without the fix and pass with it:

- `test_the_gateway_opens_a_session_filed_under_a_project_slug` -- the
reported bug;
- `test_the_gateway_appends_to_the_transcript_it_opened` -- the
split-file half;
- `test_two_channels_sharing_a_chat_id_keep_their_own_transcripts` --
the guard against over-correcting, and the case that caught the first
attempt at this fix;
- `test_a_keyless_transcript_is_still_adopted_across_groups` -- pre-key
transcripts stay reachable;
- `test_the_gateway_still_files_its_own_sessions_by_channel` -- reaching
across groups is a fallback for an existing transcript, not a change of
where this process writes.

The first attempt, which ungated the search without the key check, broke
`test_cross_channel_ambiguous_same_id_two_channels` in
`tests/test_cli_session_commands.py`. That failure is what produced the
key check.

Fix verified against the real workspace that produced the report: the
affected session now opens with both of its messages, resolving to its
actual file under the launch directory's slug.

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

No docs change: this corrects the behaviour the surrounding docstrings
already described, and coins no domain term.

## Risk

User-visible change: a surface can now open a transcript filed under
another group. On the gateway this closes a gap rather than widening
reach -- `list_sessions` already scans every group, so those sessions
were already listed and offered; only opening them was broken. For a
slug-grouped surface the search is unchanged apart from the new key
check, which makes it strictly stricter: a candidate that would
previously have been adopted on its stem alone is now refused when its
stored key names a different session.

Transcripts written before the metadata `key` field are unaffected --
they answer no key and are still adopted on the stem.

`session_path` now reads the first line of each candidate when the
direct path is absent. The read is one `readline` on a file the caller
is deciding whether to open, and the candidate set for an exact chat_id
is normally empty or a single file.

Rollback is a revert of the single commit; the change is confined to one
function and one new module-private helper, and writes nothing to disk
that a revert would strand.

- [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[1m]) <noreply@anthropic.com>
Co-authored-by: KT <74288668+0xKT@users.noreply.github.com>
…t moved on (#476)

## Summary

The installers stop telling you what to run and run it. `install.sh`
ends with
`raven web --stop && raven web --foreground`, so a finished install is
the page
open in a browser rather than a hint to go and type something; first-run
setup
is walked through by the page's own onboarding. The capability summary,
the
upgrade-versus-first-run wording and the config.json probe that fed it
go with
it. `install.ps1` mirrors all of this, with one deliberate divergence: a
non-zero page exit only warns there, because under `irm | iex` that is
the
reader's own interactive PowerShell and Ctrl-C, the ordinary way to end
a
foreground page, returns non-zero.

Two gaps that only bite a source install are closed on the way:

- `build_web_assets` compares each artifact against its sources instead
of only
testing that it exists. An editable install relinks Python and nothing
else,
so `ui-tui/dist/entry.js` and `ui-web/dist/index.html` kept serving
whatever
the tree held at first install while every `.py` beside them moved on.
The
page watches `i18n/` too, since `ui-web/build.py` inlines the catalogue.
`node_modules`, `dist` and `.modern` are pruned: `npm ci` rewrites the
first
  on every build, and the other two hold the artifact being judged.
- A node packaged without npm now fetches a private runtime that carries
one.
Debian and Ubuntu split the two packages, so a system node >= 22
satisfied
`ensure_node` and the build then found no npm with no private runtime
ever
provisioned. The download body is split out as `provision_private_node`
and
called from the build probe as well, subshelled so a failure there is a
skipped build rather than a failed install. It stays lazy rather than
moving
into `ensure_node`, so a wheel install that never runs npm does not pay
for a
  50 MB download it has no use for.

Both READMEs are reordered and the clone install is documented under
Quick
Start, where it was missing: `./install.sh` and a piped run do different
things
from inside a checkout, because local mode requires `$0` to be a real
file, and
`RAVEN_LOCAL_SRC` is the opt-out. The Chinese README also picks up the
EverMind
ecosystem table the English one had already moved to.

One test changed rather than being deleted.
`test_readme_quickstart_matches_the_installer_hint`
required an `Onboard and run` heading in both READMEs; the installers no
longer
close with a first-run hint and neither README carries that section, so
the pin
had no subject left. It is repointed at what the two must still agree
on, which
is the clone install.

`install.ps1` has not been parsed or executed. There is no pwsh on the
machine
this was written on and no PowerShell step in CI, so every Windows
change here
is reviewed by eye and pinned by text tripwires only. A real Windows run
before
release would be worth it.

## Type

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

## Verification

`uv run python -m pytest tests/ --ignore=tests/integration` on the
merged tree:
23161 passed, 109 skipped.

`uv run pytest tests/test_install_script.py`: 27 passed, including a
functional
test that extracts the real `is_stale` and `resolve_node_dir` from
`install.sh`
and runs them under `sh`. `is_stale` is checked across missing artifact,
fresh
artifact, touched page source, touched catalogue, and build output
touched;
`resolve_node_dir` across system node with npm, node without npm, node
without
npm and a failed fetch, and no node at all. Both were mutation-checked:
removing
the fallback line from `install.sh` turns the new tests red.

`sh -n install.sh`, `make check-source-language`, `make
check-large-files`,
`ruff check`, `ruff format --check`: all clean.

- [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: a finished install now holds the terminal on a running
page
instead of returning to the prompt. That also means the installer's exit
code is
the gateway's, so on a clone where the page build only warned,
`install.sh` now
exits 1 with "No page is built" where it used to exit 0 with a working
TUI. A
re-run over a checkout that has moved on will rebuild the frontend,
which costs
an `npm ci` it used to skip.

Rollback is per commit: the launch, the rebuild-on-change and the npm
fallback
are independent edits to `install.sh` and `install.ps1` and can be
reverted
separately. No runtime code outside the installers changed.

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

## Related Issues

N/A

---------

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

Turn "bug fix to regression case" from a manual chore into a gated
pipeline, and give trajectory regressions their own CI gate.

- `raven trajectory replay --json`: stable machine-readable report
(schema_version 1) with defensive JSON coercion; on a halted replay the
full JSON is still emitted before exit code 2.
- Case metadata contract: `case.yaml` requires non-blank
issue/owner/why/re_record; residual-scan findings pass only with an
explicit per-token human sign-off (`reviewed_residuals`: full-token
sha256 plus a verbatim reason, nothing exempted automatically -- a
machine-produced redaction report is not approval).
- `raven trajectory regression validate [CASE_DIR|--all]`: static commit
gate -- whole-directory discovery so a broken case cannot hide, both
schemas, cassette completeness down to the replay contract (valid JSON
is not a usable payload: missing payloads via the extracted
replayability check, corrupt inner fields via `validate_recording`),
rejection of files the residual scanner would silently skip, and a 256
KiB / 1 MiB size budget; every parse boundary collects problems instead
of raising, so one malformed case cannot abort the --all sweep.
- `raven trajectory regression init <source> --name <case>`: scaffold
from a bundle directory, attempt id, trajectory report tarball, or bug
report package (two-layer extraction; both passes accept only regular
files/directories with root-contained names). Minimize lands in staging;
residual findings are reviewed interactively (the full token is shown
locally, files receive digest and reason); publication copies onto the
destination filesystem under a `.init-` staging name, re-checks the case
name, then renames on one device -- failures clean up only what the run
created.
- Vacuous-pass guard (`MIN_COMMITTED_CASES`, raised by hand) and a
`trajectory` CI job: replay the committed cases, run `validate --all`
even after a pytest failure so both gates report, upload per-case
failure reports (replay --json schema plus check failures, mode from
each case's expect.yaml) as a 14-day artifact.
- Docs: the Trajectory Regression Case entry in CONTEXT.md extended in
place; `tests/trajectories/README.md` documents the per-case layout and
workflow.

Also: `config_secrets_loaded` (a field name of redaction.json itself, a
guaranteed self-referential false positive in every cassette) joins the
residual scanner's benign literals, and both sample cases gain real
`case.yaml` metadata.

## Type

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

## Verification

- `uv run pytest tests/ -q` (after rebasing onto latest main): 23149
passed; the 37 failures and 20 errors are environment-bound and
reproduced identically on a pristine origin/main worktree without these
changes.
- `uv run pytest tests/test_trajectory_regressions.py
tests/test_cli_trajectory_regression_commands.py
tests/test_cli_trajectory_commands.py tests/test_trajectory_replay.py
tests/test_trajectory_cassette.py tests/test_trajectory_redact.py -q`:
282 passed (about 90 new tests across the five steps).
- `uv run ruff check .`, `uv run ruff format --check .`, `make
lint-imports`, `make lint-deps`, `make check-large-files`, `make
check-source-language`: pass. `make lint-types` reports one diagnostic
in `raven/agent/tools/web.py`, a file this branch does not touch,
pre-existing on main.
- CI workflow YAML parses (`yaml.safe_load`); the first real run of the
new `trajectory` job happens on this PR (see this PR's checks).
- CLI assertion width-independence re-verified with `COLUMNS=60`.

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

## Risk

- All additive: the default `replay` output path is unchanged;
`regression init/validate` are new subcommands; the one scanner change
adds a fixed benign literal (any variation still flags).
- The new `trajectory` CI job becomes a required-passing gate for PRs;
to lift it in an emergency, revert the CI commit (`ci: add trajectory
regression gate job and case-count guard`) -- the remaining commits are
independent.
- Tar extraction accepts only regular files and directories with
root-contained relative names on both layers, with `filter="data"` kept
as defense in depth.

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

## Related Issues

N/A

---------

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

Present Raven's orchestration capabilities and four specialized agents
with benchmark figures, and keep the English and Chinese READMEs
aligned.

- Add the Multi-Agent Orchestration Benchmark at the end of the
introduction. Compare Hermes, Claude Code, and Raven with shared 0-1
scales for Node F1, Edge F1, Partial Order Accuracy, and Exact Match
Rate.
- Move Raven Agents before Benchmarks and add concise Research, Code,
Design, and Oncall descriptions with their figures.
- Replace the third-party agent table with a centered card image and
explain how Raven connects to and orchestrates these agents. Host image
files as GitHub attachments.
- Standardize primary heading markers and align section order, captions,
model settings paths, and the EverMind ecosystem catalog across both
languages.

## Type

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

## Verification

Commands run after rebasing onto the latest main:

```text
uv run --no-sync pytest tests/test_readme_scope_canon.py tests/test_living_docs.py -q -o addopts=''
# 4 passed
uv run --no-sync pre-commit run --files README.md README.zh-CN.md
# All applicable checks passed
make check-large-files
# Passed
make check-source-language
# Passed
env PYTHONPATH=. uv run --no-sync python scripts/check_commit_messages.py origin/main..HEAD
# Passed
git diff --check
# Passed
```

Compared all 27 headings, 12 code blocks, image URLs and sizing,
commands, paths, numeric values, and architecture graph structure across
both languages. Verified all 24 orchestration scores against the
supplied results and confirmed that the uploaded chart matches the local
PNG byte for byte.

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

## Risk

This changes documentation structure, wording, and linked figures.
Runtime behavior is unchanged. Figures depend on GitHub attachment
availability. Roll back by reverting the changes to README.md and
README.zh-CN.md.

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

## Related Issues

#462, #465, #474, #477, #479

---------

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
…the roles (#468)

## Summary

The agent loop already names four strategy roles (Memory, Planning,
Capability, Action), but two judgements a turn makes still lived in the
shell around them. This moves both onto the roles, in two dependent
commits, with no behaviour change intended.

**1. The charter reads through Memory and Action.** A dispatched
sub-agent's playbook (`_meta["raven.playbook"]`) was read from four
places inside the loop and the tool registry. `prompt` and `stopWhen`
are now read by `DefaultMemory.assemble()`, which fills `task_brief` and
`task_done_when` on the `TurnContext` the identity segment renders.
`checks` and `code` are read by `DefaultAction.judge()`, which
`ToolRegistry.execute` asks once per proposed call, the way it already
asks the permission gate. `tools` stays in `withheld_names()` and the
registry still decides what a refusal does, so a replaced role withholds
nothing it could not already withhold.

**2. Mid-turn window shrinking seats on Memory.** The five recoveries
the loop ran inline go through one method,
`MemoryModule.shrink(messages, pressure, state, model)`, with
`WindowPressure`, `WindowState` and `ShrinkResult` as the carriers:

- proactive compaction, once the last billed reading crosses the trigger
- the standing image window
- a provider's overflow error
- a picture refused inside a tool result
- pictures refused for size

The retry mechanism stays in the shell. The six `continue` sites and the
iteration rollback did not move, so a hook sees a retried call exactly
as it saw the first. `WindowState` is a per-turn object the shell owns
and hands to each call, because the role outlives the turn;
`image_window` has no default, since 0 is a real value meaning "no
picture stays".

The pure pieces both sides need move to a new package,
`raven/agent/window/`, which the import contracts treat as cargo that
may not import the loop shell. `REASONING_EFFORT_LADDER` moves to
`contracts/llm_provider.py` because the loop's empty-response descent
and the window's head summary both read it and neither package may
import the other.

Contract papers: `CONTRACTS_VERSION` 26 to 28, `CONTRACTS_LINE_CEILING`
3,040 to 3,200, each bump with its own review paragraph in
`tests/test_kernel_budget.py`.

A follow-up PR carries the third step, which gives a sub-agent nine
verbs in place of the six hook phases. It is kept separate because it
touches every bundled plugin.

## Type

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

## Verification

Gates, run at each of the two commits:

```
uv run ruff check --no-cache raven evolver agents plugins-dist tests scripts
uv run ruff format --check --no-cache raven evolver agents plugins-dist tests scripts
uv run lint-imports
uv run pytest -q
npx commitlint --from origin/main --to HEAD --config commitlint.config.cjs
uv run python scripts/check_commit_messages.py origin/main..HEAD
uv run python scripts/check_source_language.py origin/main..HEAD
uv run python scripts/check_large_files.py origin/main..HEAD
```

Import contracts: 10 kept, 0 broken at both commits. The suite is green
at both; the failures this machine shows are the same ones it shows on
an unmodified `main` (missing browser and optional packages), and
neither commit adds one. The loop's own e2e files, which the default
selection leaves out, were run too (charter, playbook, truncation,
direct chat, fallback chain, ACP stdio): no failure that `main` does not
also show.

Behaviour preservation. Each commit was run against a worktree of its
own parent under the same scripted provider, outputs normalised for
nonces and temp paths, then byte-compared. Four scenarios, eight
comparisons, all identical:

- a whole turn with no playbook: host turn, spawned sub-agent, tool
calls and refusals
- a playbook-on worker lane in process, including two charter refusals
- six window-shrink scenarios: proactive compaction, the standing image
window, overflow, a refused picture, oversized pictures, the effort
ladder
- the five-plugin hook seam, every phase's decision and metadata per
iteration

Mutation check on the second commit: breaking each of the five moves
inside `DefaultMemory.shrink` turns exactly its own scenario red, so the
comparison is not vacuous.

Real model, Sonnet 4.5 through OpenRouter, at both commits:

- playbook on, in-process lane: both workers receive the charter, write
their files and finish
- playbook on, ACP fork lane: the charter prompt crosses the process
boundary, and a charter check blocks a `write_file` outside the fence

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

## Risk

No user-visible behaviour change is intended, and the comparisons above
are byte-identical on every path they cover.

For anyone replacing a harness role: `MemoryModule` gains `shrink`, so a
replacement written against the old protocol needs it.
`DefaultMemory.bind()` names the missing member at assembly rather than
failing inside a turn.

Rollback is a revert of the merge commit. Nothing persisted on disk
changes shape.

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

## Related Issues

N/A

---------

Co-authored-by: yao pengfei <yaopengfei@shanda.com>
Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
Co-authored-by: KT <74288668+0xKT@users.noreply.github.com>
## Summary

Synchronize the Chinese README with the updated English overview. The
change aligns the Raven introduction and built-in agent section,
including the Host Agent positioning, Raven Evolver description,
built-in agent wording, and orchestration readiness statement.

The existing benchmark charts, links, captions, and the remaining
bilingual sections are preserved.

## Type

- [ ] Fix
- [ ] Feature
- [x] 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

Commands run:

- `uv run --no-sync pytest tests/test_readme_scope_canon.py
tests/test_cli_onboard_commands.py -k readme_quickstart -q` (1 passed)
- `uv run --no-sync pytest tests/test_docker_runtime.py -k
container_exposes_no_engine_selector -q` (1 passed)
- `UV_NO_SYNC=1 make check-large-files check-source-language
PYTHON_VERSION=.venv/bin/python COMMIT_RANGE=HEAD` (passed)
- `git diff --check` (passed)

## Risk

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

This is a documentation-only change. Reverting the commit restores the
previous README wording.

## Related Issues

N/A

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

Five upgrade-path and reporting defects in the embedding pin that #414
landed. Three were named as bounded follow-ups in that PR's final
review; two came out of driving the acceptance environment afterwards,
and both of those were invisible to the unit suite because every fixture
already held the new shape.

The two found by acceptance:

- **A config still holding the retired endpoint shape does not load at
all.** The block used to carry the address and the key;
`EmbeddingConfig` forbids extras, so `raven doctor` ends in a traceback
rather than in a degraded feature, and so does every other command that
reads the extension blocks. The migration takes those keys now: the
address is adopted onto whichever configured provider answers there, and
when nothing answers the whole block goes rather than half of it, with
the dropped address named in the log. A model left with no provider is a
pin every reader resolves to nothing while every screen reads it as
configured.
- **A pin could not name a provider this config holds.** The provider
half was checked against the registry alone, and Raven carries no spec
for every vendor LiteLLM can reach. A pin the wizard had stored and
every reader resolved came back from the settings page as a provider
that does not exist, leaving the endpoint uneditable on the page that
exists to edit it. A section the operator wrote counts as proof the
vendor exists; a name nothing holds is still refused as a typo.

The three from review:

- **Clearing the pin was a no-op.** The picker's inherit option sends
both halves empty and the writer dropped empty values before deciding
anything, so the call returned applied while the file kept the old pair.
- **Doctor reported a move it had not made.** The endpoint only moves
when a configured provider answers at the retired address, and the
return value was discarded. The unmade move stays in the remaining list
now, with a line saying why.
- **The canonical SkillForge glossary entry still said
`skillForge.everos`** and attributed the local extraction pipeline to an
embedded copy of the memory backend.

## Type

- [x] Fix

## Verification

- `uv run --all-extras pytest -q` -- 23058 passed, 110 skipped, 4
failed; the four are the same pre-existing failures main carries
(`test_agent_loop_token_budget`, `test_agents_code_launcher`,
`test_agents_oncall_launcher`, `test_ppt_engine_image_search`), matched
by test id against a run on the merge base.
- `uv run ruff check .` and `uv run ruff format --check .` -- clean.
- `make check-commits`, `make check-source-language`, `make
check-large-files` -- clean.
- Every fix was reproduced before and after against a real config or a
real gateway, not only through its test: the unloadable config came from
the acceptance home itself and `raven doctor` was run on it before and
after; the provider refusal was reproduced over a real WebSocket to a
running gateway; clearing and the doctor report were each run end to
end.
- Each new test was checked by removing its fix and confirming it fails.
- Wider acceptance on the running product, with the memory plugin and
without it: a gateway with no memory plugin installed built a knowledge
base whose width (4096) was measured from the live endpoint and answered
a query sharing almost no keywords with the source (score 0.728); the
real TUI recalled a fact stored in an earlier turn, with the recall
payload in the audit artifact as evidence rather than the model's
answer.

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

## Risk

Two changes widen what is accepted rather than narrowing it: a provider
section counts alongside the registry, and a migration edits a block it
previously left alone. The migration only runs for a config carrying the
retired keys and is stamped, so it runs once; a config already in the
current shape is untouched. Rolling back any single commit is safe --
they do not depend on each other.

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

## Related Issues

N/A

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ith (#483)

## Summary

An EverOS agent case carries three fields: `task_intent`, `approach` and
`key_insight`. Recall built `Memory.text` from the first and the last
only, so the method a case was solved with never reached the prompt. The
reader saw what was attempted and what was concluded, with the steps
between them missing.

The other two consumers of the same row already keep all three fields:
the sub-agent memory record joins intent, approach and insight, and the
memory page maps `approach` to a card's body. Only the recall path that
feeds the model dropped it.

This joins the three fields in field order. Two constraints shaped how:

- Intent stays on the first line. The skill router takes this text's
first line as a hit's display name and as its line in the search
listing, so moving intent off line one would rename every recalled case.
- Empty fields drop out, so an EverOS that answers without an `approach`
still reads as prose instead of carrying a blank paragraph.

## Type

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

## Verification

```
uv run --frozen pytest tests/test_everos_backend.py tests/test_skill_router_everos_source.py -q
182 passed in 17.69s

make lint-python lint-types
ruff check: All checks passed!
ruff format: 1960 files already formatted
ty check: All checks passed!

make check-source-language
clean, no findings
```

The case test asserts the exact joined text rather than substring
membership, and it was run against the pre-change conversion first to
confirm it fails there (AssertionError on the missing approach). A
second test covers a row that arrives without an `approach`, which is
the shape that would otherwise produce a blank paragraph.

No user-facing doc states the recall text shape. The CONTEXT.md passage
that lists all three case fields describes the sub-agent memory record,
which is a different code path and is unchanged.

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

## Risk

Each recalled case now carries one more paragraph, so the agent track's
share of a prompt grows by roughly the length of one `approach` per case
hit. Nothing downstream parses this text: the skill router reads the
first line for a display name and passes the remainder through
untouched, and that first line does not move.

Rollback is a revert of the single commit.

- [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

Synchronize the Chinese README with the updated English README. The
change aligns the Raven introduction, benchmark captions, built-in agent
copy, third-party agent positioning, and the removal of the obsolete
benchmark summary section.

The change also adds the updated wording for data analysis, PowerPoint
slide generation, and unattended AI4S benchmark captions while
preserving all chart links and remaining bilingual sections.

## Type

- [ ] Fix
- [ ] Feature
- [x] 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

Commands run:

- `uv run --no-sync pytest tests/test_readme_scope_canon.py
tests/test_cli_onboard_commands.py -k readme_quickstart -q` (1 passed)
- `uv run --no-sync pytest tests/test_docker_runtime.py -k
container_exposes_no_engine_selector -q` (1 passed)
- `UV_NO_SYNC=1 make check-large-files check-source-language
PYTHON_VERSION=.venv/bin/python COMMIT_RANGE=HEAD` (passed)
- `git diff --check` (passed)

## Risk

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

This is a documentation-only change. Reverting the commit restores the
previous README wording.

## Related Issues

N/A

---------

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
…ch (#485)

## Summary

Enabling an entrance from the page (weixin, for one) builds and starts
the adapter on the spot through `ChannelManager.start_one`. The gateway
command hands every channel its spine dispatch in one loop at launch,
and that loop has already run by then, so a channel started this way has
no `intake._submit`: it logs `no spine dispatch wired` once per message
and drops every one, while the page draws it connected and the adapter's
own login has succeeded.

The outlet half of the same hazard was fixed earlier:
`channels.on_started` registers an outlet so a hot-started channel can
be replied to. The intake half stayed a launch-time loop, and
`start_one` fires `on_started` and nothing else, so nothing after launch
ever calls `set_submit` for a channel born later.

The fix composes the intake onto the hook the outlet already uses. A
module-level `_wire_channel_intake(channels, dispatch)` sets the submit
on each channel the manager holds and wraps `channels.on_started` so a
channel started later gets the outlet it already got plus the intake;
the command calls it at the point where `_inbound_dispatch` is born.
Channels present at launch are wired exactly as before. The helper is
module-level rather than a closure in the command body so that it can be
executed by tests, which the command body cannot be. Nothing under
`ui-web/` or `ui-tui/` changes.

## Type

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

## Verification

- `.venv/bin/python -m pytest tests/test_cli_gateway_commands.py -q` on
this branch rebased onto current `main`: 50 passed. Four new cases drive
`_wire_channel_intake` through fake channels and a fake manager: every
channel present at launch gets the dispatch; a channel the manager
starts later gets it through `on_started` while the outlet hook that was
already there still runs; a manager with no outlet hook still wires the
intake; and the command body is pinned by source to call the helper with
`_inbound_dispatch` and to have lost the launch-only loop.
- Mutation-checked both ways: dropping `wire(ch)` from the composed hook
fails exactly the two cases that start a channel late; dropping the
outlet call fails exactly the outlet assertion of the hot-started case.
- `python scripts/coverage_gate.py diff --base-ref upstream/main
--threshold 90` after the file ran under `--cov=raven --cov-branch`:
91.67% (11/12 executable changed lines). The one uncovered line is the
call inside the `gateway` command body, which no unit test executes, the
same as every other line of that body.
- `.venv/bin/ruff check` and `.venv/bin/ruff format --check` on
`raven/cli/gateway_commands.py` and
`tests/test_cli_gateway_commands.py`: clean.
- The drop was reproduced on a live gateway before the fix (2026-09-14):
enable, disable and re-enable weixin through the page's own RPC puts the
adapter on the `start_one` path, and every inbound message was logged as
`no spine dispatch wired` and dropped.
- No user-facing docs change; the behaviour was always the documented
one.

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

## Risk

- Security: none. No new input path; the dispatch a hot-started channel
now receives is the same one launch-time channels already had.
- Backward compatibility: channels present at launch are wired by the
same call as before; the only change is that a channel started later is
wired too.
- Rollback: revert the squash commit; the hook falls back to the
outlet-only lambda.

- [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>
…es (#469)

## Summary

Follows #468 (merged as `869ad78a`), which moved the charter reads and
the window's mid-turn shrinking onto the harness roles. This branch is
rebased onto `main` and carries only its own three commits.

A sub-agent's product logic was an `AgentHook` subclass: six phase
methods over a fourteen-field context it could write, with the turn's
own state kept in the context's free-form `metadata` dict. Surveyed
before this change, the five bundled plugins had 71 phase methods, 42 of
them doing real work, reading fourteen context fields and using six
decision fields. Every one of them fits a small set of verbs. This PR
makes those verbs the contract, in three commits.

**1. A sub-agent writes a conduct instead of six hook phases.** New
factory-loop-tier paper `contracts/agent_conduct.py`: `AgentConduct`
with nine optional verbs (`intake`, `select_tools`, `advise`,
`system_addendum`, `review`, `salvage`, `outbound`, `archive`,
`observe`), answered against a read-only `StepView` of fourteen fields,
with `Intake` and `Verdict` (Accept / Resample / End) as the answers and
a `ConductFactory` because the host builds one conduct per turn.

It sits beside `loop_hooks` rather than replacing it. The six phases
remain the loop's *timing* contract: when the loop asks, in what order,
what a refusal does. This is the *judgement* contract: given one step,
what does this agent say about it. `ConductHook` seats one in the other,
so a plugin that still ships a plain `AgentHook` keeps working and the
composite's ordering, merging and rollback are untouched.

All five bundled plugins are ported: design-engine, ppt-engine,
code-flow, oncall-flow and research-flow. Research keeps its six-phase
gate chain: the gates now subclass a plugin-private `Gate` and read a
`GateCtx` the conduct builds from its `StepView` and its own
turn-private facts, so six thousand lines of gate bodies did not move
and its wrappers still see the cross-gate state they read.

The turn's conduct and the system-message addendum it splices live in
the turn's own `metadata` dict, not on the hook: one hook instance
serves every turn a process runs, and a system turn can overlap a user
one on the same chain.

**2. The conducts seat on the roles.** `MemoryModule.intake`,
`PlanningModule.advise`, `ActionModule.review` and
`ActionModule.salvage` each take this turn's conducts, so a replaced
role decides what a plugin's judgement does rather than the seat
deciding it. The composition rules live in
`raven/agent/harness/conducts.py`: the first non-Accept verdict wins,
advice joins in order, the first salvage wins, an intake threads the
text through each conduct. `run_turn` binds the harness for the turn the
way it binds the model, and the seat falls back to the same rules when
nothing is bound, which is how a plugin test drives a hook directly.

Contract papers: the conduct paper is factory-loop tier, so the
contract-tier digest and `CONTRACTS_VERSION` do not move with it.
`CONTRACTS_LINE_CEILING` 3,200 to 3,420, each bump with its own review
paragraph in `tests/test_kernel_budget.py`.

**3. What the review found.** Eleven threads, all resolved in the third
commit. The ones that changed behaviour rather than prose:
`CodeFlowHook` kept its `rolls_back_iterations = False` (inheriting
`True` from the new base had it hold every reply behind the draft gate
instead of streaming); `StepView` now publishes deep-frozen nested
values, since freezing only the outer tuple left a conduct able to write
through to `ctx.messages`; a turn's conducts are collected at the role
boundary, so a replaced role sees one batch of the turn's judgements
rather than one call per seat; an addendum is located by its own text
rather than a saved offset, which two conducts appending system text
used to shift out from under each other; `Planning` and `Action` gained
binders, so a replacement role missing a new member is named at assembly
rather than swallowed by the composite's `except Exception`.
`CONTEXT.md` carries the Agent Conduct term and the Agent Hook entry no
longer describes what this PR replaces.

## Type

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

## Verification

Gates. CI runs these at the branch tip (17 pass, 1 skipped -- the
skipped row is the disabled Claude Mention workflow). Locally, at each
of the three commits:

```
uv run ruff check --no-cache raven evolver agents plugins-dist tests scripts
uv run ruff format --check --no-cache raven evolver agents plugins-dist tests scripts
uv run lint-imports
uv run pytest -q
npx commitlint --from origin/main --to HEAD --config commitlint.config.cjs
uv run python scripts/check_commit_messages.py origin/main..HEAD
uv run python scripts/check_source_language.py origin/main..HEAD
uv run python scripts/check_large_files.py origin/main..HEAD
```

Import contracts: 10 kept, 0 broken at each of the three commits. The
suite is green at each, and no commit adds a failure that `main` does
not already show on this machine (this checkout has the heavy extras
uninstalled, so the local run carries failures `main` carries too -- CI
is the authority, and it is green). The loop's own e2e files, which the
default selection leaves out, were run too (charter, playbook,
truncation, direct chat, fallback chain, ACP stdio): no new failure.

Behaviour preservation. The oracle for this change is the hook seam
itself: all five plugins driven through every phase of a multi-iteration
turn under the same scripted provider, recording each phase's decision
and the observers it files. Outputs normalised for nonces and temp
paths, then byte-compared.

Re-run after the review fixes at the branch tip `81e6f423` (all three
commits), against `main` at `c65c2059`:

| scenario | bytes |
| --- | --- |
| five-plugin hook seam | 23,840 == 23,840 |
| a whole turn with no playbook (host turn, spawned sub-agent, tool
calls and refusals) | 12,821 == 12,821 |
| six window-shrink scenarios | 133,371 == 133,371 |

These cover the tip, not each commit separately. The seam comparison is
the one that matters most for the third commit: its first fix changed
`rolls_back_iterations` for code-flow, which decides whether replies
stream or are held, and the seam oracle records the decision at every
phase, so an unintended change there shows as a diff rather than as
silence.

A unit test proves the seat is a real decision point rather than a
pass-through: with a lenient Action role bound, a conduct's Resample is
not applied, and Planning's advice and Memory's intake are what the loop
receives.

Real model, Sonnet 4.5 through OpenRouter, at the branch tip:

- playbook on, in-process lane: two workers, each receives the charter
(3 checks), writes its file, finishes; 26 recorded model calls
- playbook on, ACP fork lane: the charter prompt crosses the process
boundary (the fork answers ZEBRA where the task text says PONG, so the
brief reached the forked model through Memory), and a charter check
blocks `write_file` to `notes/hi.txt` against a `pathPrefix: out/` rule
- Raven-Research, a fork running the full gate chain: `plain-first:
escalated_tool`, then `sufficiency-gate: released on the pages at
iteration 4 after 2 searches / 2 pages`

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

## Risk

No user-visible behaviour change is intended, and the hook-seam
comparison is byte-identical on every phase it covers.

For plugin authors: the six-phase `AgentHook` is unchanged and a plugin
shipping one keeps working. `ConductHook` is an adapter that seats a
conduct in that same chain.

For anyone replacing a harness role: the role protocols gain `intake`,
`advise`, `review` and `salvage`, so a replacement written against the
old protocol needs them.

Rollback is a revert of the merge commit. Nothing persisted on disk
changes shape.

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

## Related Issues

N/A

---------

Co-authored-by: yao pengfei <yaopengfei@shanda.com>
Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
…res (#410)

## Summary

Absorbs the first of two remaining commits from the research fork before
it stopped
iterating. This one lands instruments only: nothing here changes what a
shipped run does,
and the research flow label does not move because no model-visible text
changed.

`raven/agent/loop/dead_end.py` (new) asks whether a finished turn ended
on the model's own
prose. The test is structural, with a textual sub-label for the tail
shape. Three decisions
are worth review:

- It lives under `raven/agent/loop/` rather than in the research plugin,
because the loop
is what would re-run a dead turn, and a host module cannot import a
plugin without
  breaking `make lint-imports`.
- The ask labeller is injected (`ask_kind`) rather than imported,
because only the product
that writes the harness asks knows how they are worded. With no labeller
the failure mode
is still recognised structurally and reported as `harness_ask_unknown`,
so the boolean
  never depends on wording.
- The fork marked its injected turns with a key of its own. This loop
already had an
equivalent in `_HOOK_INJECTED_KEY`, so the predicate reads that rather
than adding a
  second marker for the same fact.

The conditional rerun that reads the predicate arrived on this branch
afterwards, in
#418, along with the turn's wall clock. Both are supplied as data a hook
writes onto the
turn's metadata rather than as config the loop reads, so an agent whose
hooks leave no
budget runs exactly as it did before either existed: the rerun is
unreachable for it and
the clock is never consulted. The rerun starts from the original
question rather than
salvaging the failed attempt, because salvage was measured on 25 real
duds to produce
content 23 times and the right answer none of them, turning a detectable
zero into a
confident wrong answer.

Three defects in that work were found by review and fixed on this
branch, each with a
reproduction that is red on the commit before it. The loop numbered
episodes from an
iteration count that restarts, so a rerun emitted indices [0, 1, 2, 0]
and the TUI, which
keys episode rows and their fold state by that index, had the rerun's
first step collide
with the first attempt's. Each attempt took its own clock reading, so a
first attempt
ending just under the limit handed the rerun a fresh full budget and a
ten-second turn ran
to eighteen. And the research hook opens its per-turn ledger at
iteration 1, which a rerun
reaches again, so the second open replaced the path the close reads and
left the first
attempt's file unreachable on disk.

Reading that back afterwards turned up three more, none of them raised
in review. The
turn's end record carried the attempt's iteration count beside the
turn's elapsed time
with nothing saying which was which, so it gained ``attempt``. The hook
contract described
that record as "status and iteration count" while it carried five
fields, which hid
``stopped_by`` from anyone writing a terminal gate against the contract.
And the rerun's
progress line named research in a loop that serves every agent, and
called itself the
first attempt when the budget allows more than one.

A verbatim body sink (`RAVEN_VERBATIM_SINK`) records search and fetch
results at three
chokepoints. It is a second file rather than more columns on the ledger
because a 250 KB
row would leave no complete line in the 64 KB tail window
`_resolve_session_seq` reads,
which would silently reset the re-run split. Off unless the environment
names a file, and a
write failure is logged rather than raised.

`DigestOutput` lets a digest return a write-only ledger annotation
beside its text. The
annotation is stripped from the tool result and merged into the fetch
row, so a digest can
never label its own output where the model can read the label. Nothing
emits one yet; this
is the pipe a later digest sidecar uses.

`is_hard_tool_failure` was blind to the whole reader path. `web_fetch`
never returns a bare
string, it returns a JSON envelope, so a spent quota, a rejected URL and
a failed
validation all read as success and reset the streak `web_search` had
accumulated.
Transient markers are still tested first, so a 429 inside an envelope
stays retryable.

Forced finalize now books its own salvage spend. The phase fires on
every run that produced
no answer and burns up to `max_tokens` doing it, and it was counted
nowhere.

The Chinese refusal opener lives in `raven/i18n/zh_lexicon.py`, which is
the exemption zone
this repo keeps Chinese forms in, and the predicate imports it from
there rather than
carrying a literal outside the zone.

Two defects found in the first review round are fixed on this head, both
reproduced before
being changed.

The verbatim sink borrowed the ledger's `_resolve_session_seq`, which
recovers the re-run
split by reading the last 64 KB of the file and parsing the last
complete line. That works
for the ledger because its rows are small; this sink stores the large
ones, so the first
record wider than the window leaves nothing parseable and the sequence
resets to 1, making
two runs indistinguishable. Runs are now told apart by a tag minted in
memory once per
process and never recovered from disk, which removes the read a large
row defeats. The
argument for keeping this file separate from the ledger was already in
its docstring; it
applies to the sequence too, and now says so.

`is_hard_tool_failure` started counting JSON envelopes while
`failure_class` still read
them as undifferentiated text. The streak key is `(tool, failure_class)`
and the break
threshold is two, so a blocked URL followed by a reader's HTTP refusal
fired the
stop-repeating nudge at a model that had changed both its cause and its
approach. Both
predicates now share one envelope parse, and an envelope is classified
by its own `error`
string rather than by a textual ladder that cannot see into one.
Coarseness is for model
prose, which varies without meaning anything; a tool writes its error
from a small fixed
vocabulary, so equal strings are the same failure and different ones are
different. The
parts that vary with the page travel in the envelope's other keys, so
they cannot split a
streak.

The last commit removes an `isinstance` guard in the envelope reader
that the diff-coverage
report flagged as the only uncovered changed line. JSON has one shape
that opens with a
brace, so the branch was unreachable; removed rather than covered.

## Type

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

## Verification

Run at this head (95c975b), on `origin/main` with nothing to rebase
onto.

```
uv run --frozen --python 3.12 --all-extras pytest -q tests/
  22132 passed, 43 skipped, 109 warnings in 388.67s
```

Integration and e2e are deselected by default (`-m "not integration and
not e2e"`), so the
run above does not include them. This change adds no integration
surface.

```
make coverage
make coverage-ratchet
  line   current=88.66% baseline=87.30% delta=+1.36pp
  branch current=81.59% baseline=79.43% delta=+2.15pp
  Coverage ratchet passed with 0.05pp tolerance.
make coverage-baseline-check
  Coverage baseline update is monotonic.
COVERAGE_BASE_REF=origin/main make coverage-diff
  Diff coverage: 100.00% (126/126 executable changed lines)
```

Diff coverage reported 99.21% on the commit before last, with one
uncovered changed line:
the sentence the rerun says to the reader, which no test drove because
none passed an
`on_progress` into a retrying turn. The last commit pins it rather than
waiving it.

```
make lint-python
  All checks passed! / 1946 files already formatted
make lint-imports
  Contracts: 10 kept, 0 broken.
make lint-deps
  Success! No dependency issues found.
make check-source-language
  clean
make check-large-files
  clean
make check-commits
  clean
```

The kernel budget gate caught one thing worth naming: documenting the
turn-end record in
the hook contract put `raven/contracts` at 2808 lines against a ceiling
of 2791. The
per-field detail moved to the write site in `turn_path`, which is not
budgeted, and the
paper now names the fields and says where the rest is. `raven/contracts`
is at 2788.

Each of the three review fixes was proved against the commit it fixes
rather than
reasoned about: a detached worktree at that commit, the new tests copied
in, red there and
green here. They reproduce the reviewer's own numbers -- the episode
list `[0, 1, 2, 0]`,
the eighteen seconds, and the stranded ledger file. The strengthened
comparison in
`test_the_retry_starts_from_the_question_not_from_the_wreckage` is a
tightened guarantee
rather than a regression test and passes either way, which is why it is
called out here.

`agents/raven-research/.env` was moved aside for every suite run: it is
gitignored and
absent in CI, and two launcher tests that delete a key from the
environment otherwise find
one in the file.

This machine has no search-provider key, so no research brief can
actually be run here.
The retrieval-path changes are verified by unit tests only, which is why
the three
chokepoints are pinned by tests that assert both the presence and the
absence of a row
rather than by an observed run. The same limit applies to the rerun: the
clock and the
dead-end trigger are driven by a deterministic clock in tests, never by
a real turn.

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

No user-facing surface changed, so there is nothing to document. The one
operator-visible
knob, `RAVEN_VERBATIM_SINK`, is an instrument that is off unless set and
is documented in
the module that reads it.

## Risk

The `is_hard_tool_failure` change is the only unconditional one, and it
reaches every agent
rather than just research: any tool returning a JSON envelope with a
non-empty top-level
`error` now counts toward the tool-failure streak that triggers the
change-approach nudge.
That is the intended correction, and the direction is conservative
because transient
markers are still tested first. Reverting it is a single branch.

Everything else is inert by default. The body sink writes nothing unless
the environment
names a file. `DigestOutput` has no emitter in this change, and a digest
returning a plain
string produces the same envelope it always did, which
`test_a_bare_string_digest_still_works` pins.

The dead-end predicate now has a caller, and it reaches only an agent
whose hooks write a
budget onto the turn's metadata. No agent on this branch does:
`agents/raven-research`
ships `wallClockSeconds` and inherits the rerun from its plugin's class
default, and every
other agent leaves the key unwritten and is bounded exactly as before.
The turn's overrun
is one budget plus at most one iteration, because the clock is read
between iterations and
never mid-generation: cancelling a call in flight would discard a
finished generation and
leave no answer at all. Rolling the rerun back is the budget key;
rolling the clock back is
the same.

Rollback is the whole commit; nothing here is depended on by anything
already on main.

- [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>
Co-authored-by: KT <74288668+0xKT@users.noreply.github.com>
…es (#472)

## Summary

After a conversation switches model from the page, the agent keeps
introducing itself as the model it left. The switch itself works -- the
session record, the request and the usage ledger all carry the new id --
but the system prompt's `You are running on model: ...` line was read
from `agents.defaults.model`, so the agent was told it still runs on the
configured default and repeated that back. From the page it is
indistinguishable from a switch that never happened.

The fix reads the id from the turn's active `ModelBinding` instead. The
loop already opens `use_binding(binding)` around every turn
(`raven/agent/loop/main.py`, around `_run_turn`), and that binding is
the session's own `/model` pick when it has one (`binding_for_session`
in `raven/agent/loop/wiring.py`), else the default.
`render._resolved_model_id` now takes its model id from
`active_binding()` when one is set and from config outside a turn, and
runs either through the same storage-to-wire conversion
(`providers.wire`) it always ran the default through. One source file
changes; nothing is threaded through the assembly path.

This is the approach the review suggested over the earlier revision of
this PR, which carried the id down as a new contract field, and it is
better on every point that revision was faulted for:

- **Right spelling.** The earlier revision forwarded
`session.metadata["model"]`, the storage spelling, and rendered it
as-is; on Codex, Azure and every gateway install that is not the id the
request carries. Here the bound id goes through `wire_model` exactly as
the default does, so `openai-codex/gpt-5.1-codex` renders as
`gpt-5.1-codex` and a gateway install keeps its `openrouter/` prefix, in
both branches. The conversion stays on the one path that owns it, and a
lazily built provider's identity `wire_model_id` is not relied on.
- **The estimation path agrees by construction.**
`ContextBuilder._get_identity` (`raven/agent/context/builder.py`) sizes
the system prompt for the token budget and calls the same renderer with
no arguments; it is invoked from `_assemble_context_messages`, inside
the binding, so it now names the same model the assembled prompt does. A
field handed down the assembly path reached the segment and not this
caller.
- **No contract change.** `AssemblyContext` and `TurnContext` are
untouched, so there is no field position to argue about, no producer
(the curator's `TurnContext` rebuild) that can forget to copy it, and
`CONTRACTS_VERSION` stays where `main` has it.
- **`stable = True` stays true.** A binding is per session, constant
within it; the line changes once per switch, which is what a switch is.
A router's per-message pick is not reflected in the line (the router is
opt-in and off by default); that is the one case a contract field would
have covered, and it is also the case that made the segment's stability
claim false.
- **No empty-string hole.** `ModelBinding` refuses an empty model id at
construction, so the line cannot be deleted by a blank pick.

A fallback hop further down a model chain still reads a prompt naming
the primary: context is assembled once, before the call. Not changed
here and not a regression -- the sibling `can_see_images` field
documents the same caveat.

Nothing under `ui-web/` or `ui-tui/` changes.

## Type

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

## Verification

Five new tests in
`tests/test_segments.py::TestIdentityNamesTheBoundModel`:

- `test_a_switched_conversation_is_told_the_model_it_is_bound_to` --
inside `use_binding` on a gateway config, `_resolved_model_id()` names
the bound model with the gateway prefix and never the configured
default.
- `test_the_bound_id_goes_through_the_same_storage_to_wire_conversion`
-- a codex-form id bound to the turn renders as the wire spelling the
client sends, not the stored one; this is the assertion the earlier
revision could not fail.
- `test_outside_a_turn_the_configured_default_still_answers` -- no
binding, config default, as before.
- `test_the_segment_names_the_bound_model` -- the whole segment, through
`IdentitySegmentBuilder.build`.
-
`test_the_estimation_prompt_and_the_turn_prompt_agree_on_a_switched_model`
-- `ContextBuilder._get_identity()` equals the segment's text inside a
binding and names the switched model.

Commands and results:

- Mutation-checked: replacing the binding read with the config read
fails exactly the four tests that bind a model (4 failed, 46 passed in
`tests/test_segments.py`); the outside-a-turn test and the three
pre-existing wire-form tests stay green.
- `.venv/bin/python -m pytest` over every test file that names
`identity_text`, `_resolved_model_id`, `IdentitySegmentBuilder` or
`ContextBuilder(`, plus `tests/test_contracts_two_tier_ledger.py`,
`tests/test_kernel_budget.py` and `tests/test_context_stable_prefix.py`
(11 files): 375 passed.
- `python scripts/coverage_gate.py diff --base-ref upstream/main
--threshold 90` after those suites ran under `--cov=raven --cov-branch`:
100% (3/3 executable changed lines).
- `.venv/bin/ruff check`, `.venv/bin/ruff format --check` and
`.venv/bin/ty check` on `raven/context_engine/segments/render.py`:
clean.
- The contracts gates pass unchanged: this revision touches nothing
under `raven/contracts/`.
- No user-facing docs change: the prompt line already documents itself
as naming the model the turn runs on.

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

## Risk

- Security: none. The binding's model id is already in every request and
in the usage ledger; nothing new is read or sent.
- Backward compatibility: outside a turn (no binding) the renderer
behaves exactly as before. Inside a turn the line names the bound model
instead of the configured default; for a conversation that never
switched they are the same id, so the rendered prompt is byte-identical
there. No wire, config or contract change.
- User-visible change: after a `/model` switch the agent names the
switched model in its identity line from the next turn on. The identity
prefix changes once per switch, so a prompt-cache prefix keyed on it is
invalidated once per switch, not per turn.
- Rollback: revert the squash commit; one file, nothing else to
reconcile.

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

## Related Issues

Fixes #470

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

Update the hosted AI4AI (Nanochat 50M Pretraining) chart reference in
both bilingual READMEs.

- Replace the old image attachment URL in README.md and README.zh-CN.md.
- Use the updated chart with the Bits Per Byte (BPB) label and the Lower
Is Better annotation above the BPB values.
- Keep the chart hosted as a GitHub attachment; no image binary is added
to the repository.

## Type

- [ ] Fix
- [ ] Feature
- [x] 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

Commands:

- `git diff --check`
- `UV_NO_SYNC=1 make check-large-files check-source-language
PYTHON_VERSION=.venv/bin/python COMMIT_RANGE=HEAD`
- `UV_NO_SYNC=1 uv run --no-sync pytest tests/test_readme_scope_canon.py
tests/test_cli_onboard_commands.py -k readme_quickstart -q`
- Downloaded attachment SHA-256 matches the local PNG.

## Risk

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

This is a documentation-only URL update. Reverting the commit restores
the previous chart.

## Related Issues

#465

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
…ber like the fleet (#493)

## What

Two defects in the shipped `agents/raven-code` profile, both inherited
from the retired vendored twin's comparison hygiene and both silent at
runtime.

**The skill route was advertised but dead.** Trunk's skills are
pull-discovery: a name+description menu rides the user envelope, and its
footer tells the model, verbatim, *"Read one with read_skill(id); search
differently with find_skill"* (`raven/context_engine/scent.py`).
raven-code kept `read_skill`, `use_skill` and `find_skill` in its
disable rows, so the menu taught a route that answered "unknown tool" --
while the router still ran, retrieval still happened, and every body it
fetched was dropped unread, with nothing logged to say so. All three
tools register off the skill registry alone
(`raven/agent/loop/wiring.py`); a Hub endpoint only adds their `hub/`
branch, and `hub` itself stays disabled.

**Memory was double-off.** design, oncall, ppt and research all ship
`memory.backend: "everos"`; raven-code alone shipped backend `null`
**plus** the `everos-memory` plugin opt-out -- the one shipped agent
that could not remember. The identity was already `raven-code` in all
three places (`memory.userId/agentId`, the plugin slice,
`subagent.json`); only the two off-switches go.

The redundant `skillForge` block goes too: its two knobs
(`rewriteEnabled`/`llmGateEnabled`) are only built under push discovery
(`factory.py`), which no shipped agent runs, and the other four agents
carry no such block.

**Review follow-up (second commit):** enabling everos-memory also offers
the plugin's `understand_media` tool through the entry-point lane, which
the hermetic face fixture cannot see -- an unledgered 14th tool in
production. Same remedy as the playbook rows that board past the fixture
the same way: the disable row is the pin, and `PLUGIN_LANE_WITHHELD`
puts the lane on the ledger. raven-research holds the same line; design,
oncall and ppt serve the tool deliberately.

## Fleet posture after this change

| | everos | read/use/find_skill | skillForge block |
|---|---|---|---|
| design / oncall / ppt / research | on | design serves all three | none
|
| **raven-code (before)** | **double-off** | **all three disabled** |
explicit false/false |
| **raven-code (after)** | on | all three served | none |

## Tests

The tool face moves on its ledger, not by luck
(`tests/test_agents_code_launcher.py`): `SKILL_LANE_TOOLS` joins the
face arithmetic, `find_skill` leaves `TRUNK_NEW_WITHHELD` (five stay
withheld), `PLUGIN_LANE_WITHHELD` pins the plugin lane, and the everos
posture test is rewritten from *stays-factory-off* to *ships-on*. The
face itself is hermetically rebuilt from the render: 13 tools = the
previous 10 + the three skill tools, verified by name and schema.

- `tests/test_agents_code_launcher.py`: 70 passed
- reviewer's set (`test_agents_code_launcher` +
`test_agent_loop_skill_tools` + `test_context_scent` +
`test_everos_plugin_discovery`): 110 passed
- ruff check + format: clean

---------

Co-authored-by: litong <238663200+TongLi31@users.noreply.github.com>
## Summary

Add `.github/CODEOWNERS` with a single entry mapping `/raven/agent/` to
`@LivXue`, so a
pull request that changes a file under that directory automatically gets
a review request
from the directory's owner.

This only requests a review. The branch ruleset keeps
`require_code_owner_review` off, so
no approval requirement changes and no contributor's merge path is
affected. Making it a
merge gate would be a separate ruleset change, deliberately not made
here.

Two limits are inherent to the mechanism rather than gaps to close
later:

- GitHub never requests a review from the author of a pull request, so
one opened by the
  owner does not assign them. The API answers that case with HTTP 422.
- A draft pull request gets no automatic request. It is sent when the
draft is marked
  ready for review.

CODEOWNERS is read from the base branch, so this governs pull requests
opened after it
lands and does not apply retroactively. Ten of the sixteen pull requests
open at the time
of writing touch `raven/agent/`. Those were assigned manually, apart
from the one the
owner authored, which cannot take the assignment for the reason given
above.

## Type

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

## Verification

Gates run in a clean worktree branched from `origin/main`, each exit
code read rather than
inferred from empty output:

```
make check-large-files      exit 0
make check-source-language  exit 0
make check-commits          exit 0
```

Behaviour checked against GitHub rather than by reading the file. The
CODEOWNERS validator
on the pushed branch returns no errors, which is what proves both that
the pattern parses
and that the named owner resolves to an account holding write access:

```
gh api "repos/EverMind-AI/Raven/codeowners/errors?ref=..."
{"errors":[]}
```

Not run, and why: `make lint` and `make test-python` analyse Python,
TypeScript and
JavaScript, and this diff adds one ten-line text file containing no
code, so neither
measures anything about the change. No test in the repository exercises
CODEOWNERS
resolution, which is why the validator above is cited instead of a test.
Separately
confirmed that nothing reads the directory this file lands in as a set:
of the three tests
mentioning `.github/`, one validates backticked documentation paths
carrying a known file
extension, one names a single workflow file, and one globs
`.github/workflows` only.

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

## Risk

Nothing changes for any contributor: an added review request does not
gate a merge, and
CODEOWNERS confers no permission of its own. The owner handle named here
is already public
on every commit and pull request in this repository, so the file
discloses nothing new.

Rollback is deleting the file, or the one entry in it. Nothing depends
on it.

- [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[1m]) <noreply@anthropic.com>
## Summary

Replace the first image in README.md and README.zh-CN.md with the
updated Raven banner, "One raven, a whole flock of specialists." Both
versions use the same GitHub-hosted attachment.

## Type

- [ ] Fix
- [ ] Feature
- [x] 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 --frozen --extra dev pytest tests/test_readme_scope_canon.py
-x -q`: 2 passed.
- `uv run --frozen --extra dev pre-commit run --files README.md
README.zh-CN.md`: all applicable hooks passed.
- `git diff --check origin/main...HEAD`: passed.
- `make check-commits check-large-files check-source-language`: passed.

GitHub confirmed the attachment upload. Download timeouts prevented a
complete comparison with the supplied PNG. Browser rendering at desktop
and mobile widths has not been checked.

## Risk

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

This changes the README banner only. The image is hosted as a GitHub
attachment. Reverting the two URL edits restores the previous banner.

## Related Issues

Fixes #126

Co-authored-by: Codex <noreply@openai.com>
#507)

## Summary

Stands up a bilingual MkDocs site under `docs-site/`, published to
GitHub Pages
by a workflow that builds on every pull request and deploys only from
`main`.
Fourteen topics ship in English and Chinese: the quick start,
self-hosting,
Docker, the WebUI, the sandbox manual, the runtime architecture, the
command
reference, the repository layout, the developer workflow, the
proactivity design
and reference pair, the self-evolution map and the tracing API.

This pull request is additive. Both READMEs keep their prose and gain
one link
to the site in the header row; trimming them is deliberately a second
pull
request, so the site can be confirmed live before anything depends on
it.

Key decisions:

- **`docs-site/`, not `docs/`.** `docs/README.md` defines `docs/` as
design
notes and dated records, and `docs/plans/` is an archive explicitly not
held
  to today's layout. A user manual has the opposite lifecycle.
- **Full bilingual parity**, via the `mkdocs-static-i18n` suffix
structure
(`page.md` beside `page.zh.md`). Parity decays immediately without a
guard, so
  a test fails when a page has no twin.
- **Anchors are pinned on the Chinese side.** A CJK heading slugifies to
  nothing, so Markdown numbers it `_1`, `_2`, and inserting one heading
renumbers every anchor below it, silently breaking links readers already
hold.
Each Chinese heading therefore states the anchor its English twin gets
for
  free: 133 heading anchors, identical across the two languages, none
  positional.
- **A left rail, not a top bar.** The header carries the brand, the
search box
and the navigation tree down the left; the table of contents on the
right is
  one continuous polyline clipped to the headings currently on screen.
- **No template overrides.** Material customises through
`theme.custom_dir` with
Jinja partials, those partials are `.html`, and
`scripts/check_large_files.py`
  holds `.html` in `BLOCKED_ASSET_EXTENSIONS`. The skin is therefore one
stylesheet remapping Material's own custom properties, plus one script
for the
table-of-contents rail. The brand mark is a `data:` URI for the same
reason.
- **An owner-signed `docs-site/` exemption zone** in AGENTS.md section
1.3. The
i18n config carries the Chinese navigation labels, and `mkdocs.yml` is
not
`*.md`, so the blanket suffix exemption does not reach it. A contract
test
holds the AGENTS.md list and `EXEMPT_PREFIXES` equal, so both moved
together.
- **The site never links out to a file it does not host.** Documents
that stay
in the repository are named as plain paths rather than linked, and a
guard
fails a page that links to an external webpage or a repository-relative
path.

Two defects found and closed here rather than after publication. The
Chinese
site rendered an English sidebar: navigation labels live in
`mkdocs.yml`, not in
any page, so comparing the two languages' Markdown finds nothing and
only the
rendered output shows it. And a control could be present, sized,
animated and
answer `querySelector` while being unclickable, because a popup Material
sized
for a top bar landed outside the viewport once the header became a rail.

## Type

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

## Verification

All commands run at the pushed head, rebased onto `origin/main`.

```
make check-large-files                                         exit 0
make check-source-language                                     exit 0
make docs-build                                                exit 0, 0 errors
  docs-site/site/index.html and docs-site/site/zh/index.html   both present
  mkdocs_static_i18n: Translated 17 navigation elements to zh
uv run pytest tests/test_docs_site.py tests/test_source_language_check.py \
  tests/test_readme_scope_canon.py tests/test_docker_runtime.py \
  tests/test_living_docs.py                                    44 passed
uv run pytest tests/integration/test_docs_site_controls_e2e.py 13 passed
pre-commit run --files <changed files>                         all hooks pass
git diff --check                                               clean
```

Anchor parity was measured on the built HTML rather than assumed: 14
page pairs,
133 heading anchors compared at h2 through h6, every one identical
between the
two languages and none positional.

The controls are hit-tested rather than queried.
`document.elementFromPoint` at
the exact point a reader clicks must return the control itself, which is
the
only check that catches an off-viewport popup; the language switcher,
the search
panel, the repository link and the table-of-contents rail each pass it.

The brand lockup is measured, not eyeballed. A failing test was written
first
and reported the wordmark sitting 4.00px below the mark's centre line;
it is
0.00px after the fix, and a 4x screenshot puts the residual optical
offset at
0.25px.

Every guard this branch adds was confirmed to fail first. The
brand-class guard
was mutated from both sides in turn: renaming `.em-eyebrow` in the
stylesheet
turned it red, renaming `class="em-standfirst"` in `index.md` turned it
red, and
it is green again with both restored. The anchor guard was shown to
catch both
failure modes, a missing anchor and a wrong one.

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

## Risk

No behaviour change to any shipped code path. Nothing imports
`docs-site/`, the
new Makefile targets are additive, and the only change outside
`docs-site/` and
its tests is one link added to each README's header row.

Rollback is deleting the branch: the site does not exist until Pages is
enabled
with `Source: GitHub Actions`, and every other change is inert without
it.

Disclosed residue, checked and deliberately not changed:

- The README header now links to `https://evermind-ai.github.io/Raven/`,
which
404s until Pages is enabled. It is the same URL the Documentation
section
  already carries.
- The `deploy` job is skipped on pull requests by design, so a green
check here
does not prove deployment works. The first real evidence is the `deploy`
job's
  own conclusion on the `main` run after merge.
- GitHub Pages is not enabled on this repository. Measured: `GET
/repos/.../pages`
returns 404. Under `Deploy from a branch` the uploaded artifact is
silently
ignored while the workflow still reports success, so the source must be
set to
  `GitHub Actions`.
- `tests/integration/` is excluded by `norecursedirs`, so the browser
suite is a
  local guard and never runs in CI.
- The self-hosting page's Makefile target, docker path and port claims
are
unguarded. They were equally unguarded in the README they came from, and
the
  owner ruled against adding a guard for them.
- The self-evolution SOP and the benchmark contract stay in the
repository and
are named as plain paths from the pages that reference them. The SOP is
a
translation of an upstream project's internal document and carries
redaction
placeholders, which is a publication decision rather than a formatting
one.
- `docs/plans/` still shows the pre-restyle `mkdocs.yml`. CONTEXT.md
defines that
directory as an archive of the tree as it stood, with an explicit rule
against
  updating an old plan to match a later change.
- The Chinese pages drop the brand's italic serif accent word. Cormorant
Garamond
carries no CJK glyphs, so it would fall back to another face without any
sign.
- The code font is Material's default Roboto Mono. The brand names four
  typefaces and no monospace, so none was invented for it.

## Related Issues

N/A

---------

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
… states (#503)

## Summary

The overview says the four built-in agents are "Powered by the **Raven
Evolver** engine". The architecture section of the same file says
`evolver/`
is a separate tool that consumes Raven as a library and that the runtime
does
not import Evolver, and `pyproject.toml` enforces that with an
import-linter
contract literally named "the runtime does not import the evolver". A
reader
gets two incompatible answers to whether Evolver is a runtime engine or
an
external development tool, and the contract settles which one is true.

This was gloryfromca's blocking review finding on #486. The thread was
resolved when that PR merged, but the text was never changed, so the
contradiction is live on `main` today in both language mirrors.

Both mirrors now put Evolver where it actually runs: refining the shared
harness from outside, against benchmarks, developing the agents rather
than
running inside them. That is the reading the review offered for the case
where
the intended claim was that Evolver was used to develop the agents. The
overview also takes the canonical spelling of harness self-evolution
from the
architecture section, as the review asked.

Scope note: only the Evolver relationship changes. The SOTA sentence in
the
same paragraph is untouched - it was not part of the finding, and what
it
should say is a product call rather than a consistency fix.

## Type

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

## Verification

Prose only: two lines, one per language mirror, no code and no new
files.

- `git diff --stat` -> `README.md | 2 +-`, `README.zh-CN.md | 2 +-`
- The two gates that apply here are satisfied by construction rather
than by a
command: `check-source-language` exempts `*.md` by suffix (AGENTS.md
1.3),
  and `check-large-files` sees no added or resized file.
- Read back against the two places the claim has to agree with: the
Evolver
  row of the agent table, and the harness self-evolution bullet in the
  architecture section. Both mirrors now match both.

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

## Risk

Documentation wording only. No runtime behaviour, no interface, no
build.
Rollback is a plain revert. The claim removed was the one the
import-linter
contract forbids being true, so nothing downstream depends on it.

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

## Related Issues

N/A. Lands the finding from #486 that was resolved without a change.

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

## Summary

`failure_class` keys the loop's tool-failure streak on an envelope's
`error`
string alone. That works only while the string comes from the small
fixed
vocabulary its producers are meant to keep, and it is a convention the
module
cannot enforce - the docstring added in #410 names exactly this failure
mode:
one cause counting as many.

Every media tool broke it. `_format_http_error` interpolated up to 400
characters of vendor body (which carries a request id) into `error`; the
unreadable-reference handlers interpolated the path; the four catch-alls
put
`str(e)` there, which spells the host. So a dead endpoint produced a
class per
call, `(tool, failure_class)` never repeated, the streak never reached
`_LOOP_BREAK_THRESHOLD`, and the stop-repeating nudge was unreachable
for
`image_generate`, `text_to_speech` and `video_generate` alike.

The URL gate in front of `web_fetch` had it too, in both copies of the
tool:
most reasons `validate_url_target` composes name the address they
refused, so
a model walking an internal range got one class per address. #410 fixed
the
handlers behind that gate and left the gate itself.

`error` now carries only what a reader could enumerate. The variable
part moves
to `detail`, which keeps it in front of the model and inside
`is_hard_tool_failure`'s transient-marker scan. Where the split gave the
single
call a further key, the batch reducer forwards that too: `hint`, which
names the
proxy a 403 can be routed through, and `url`, which names the reference
a
guarded fetch refused. A batched picture is told what the single call is
told,
which is the property the split has to preserve. Distinct causes stay
distinct,
which is the failure mode this trades against: two HTTP statuses, two
exception
types and the four vendor refusals each keep their own class, and the
tests
assert that direction too.

Found by sweeping for the shape rather than by reading the code: every
`"error":` whose value is not a literal is a candidate, and that grep is
the
cheapest guard against this recurring.

## Type

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

## Verification

- `uv run pytest tests/ -q -p no:randomly` -> 23443 passed, 109 skipped
- `make lint-python` -> ruff check and format clean
- `make lint-types` -> all checks passed
- `make lint-imports` -> 10 contracts kept, 0 broken
- `make check-source-language`, `make check-large-files` -> clean
- Revert-to-red, per fix, restoring the source from a backup afterwards:
reverting `media_gen.py` alone turns the four new class tests red and
leaves
`test_two_different_statuses_stay_two_classes` green; reverting either
copy
of `web.py` turns only that copy's gate test red and leaves the two #410
  streak tests green. For the batch fold, each key is pinned on its own:
reverting it to `detail` alone turns both new batch tests red, restoring
`hint` alone leaves the reference test red, restoring `url` alone leaves
the
  403 test red.
- After rebase onto 45203a4: `uv run pytest
tests/test_media_gen_tool.py
  tests/test_security_web_ssrf.py -q -p no:randomly` -> 100 passed;
  `make lint-python`, `make lint-types`, `make check-source-language`,
  `make check-large-files` -> clean.

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

## Risk

Tool output shape changes: a caller reading the reason out of `error`
now
finds it in `detail`. The readers in this repo are the model itself and
the
tests updated here; `tests/test_security_web_ssrf.py` is the one
behavioural
assertion that moved, and it still pins that a private address is
refused for
being private. Rollback is a plain revert of this commit - no state, no
migration, no config.

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

## Related Issues

N/A. Finishes the envelope work started in #410, which fixed the
reader's
handlers and left the gate in front of them and every media tool.

---------

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

## Summary

`raven gateway` exited with SIGSEGV (-11, or 139 in a shell) on every
SIGTERM,
so a systemd or docker stop recorded a crash instead of the clean stop
it was.
`raven tui` and `raven agent` had the same defect.

Root cause: `LocalSkillCatalog` auto-starts `SkillFileWatcher`, a daemon
thread
that parks inside the Rust `watch()` of watchfiles. Daemon status does
not make
process exit safe while the thread sits in native code -- CPython runs
`Py_FinalizeEx` under that call. Every Python shutdown step succeeds
first, so
the crash lands after the graceful chain is complete and the real exit
code is
masked. With faulthandler armed all three faults report `<no Python
frame>`,
i.e. no Python code is on the stack.

Every owner now retires it:

- the gateway's shutdown chain, for the generation still bound at exit;
- the TUI's teardown;
- the one-shot agent's teardown;
- `RavenRuntime.dispose`, for a generation retired at a swap.
`build_runtime`
mints a fresh `AgentLoop` per generation, so each swap starts a new
watcher
  and abandons the previous one; without this a reloaded gateway still
segfaults on stop. The generation-organ roster grows the same entry, so
the
  two halves of that contract stay in step;
- the gateway's shutdown again, for a candidate staged by a reload that
the
serving loop never consumed. That generation is not the bound `agent`,
so
  nothing else reaches it;
- the gateway's shutdown once more, for a candidate `take()` handed out
whose
binding a cancelled unbind never finished. `_serve_generations` holds
that
candidate in a local across `await _unbind_generation()`, so a shutdown
cancelling the await unwinds the coroutine and drops the local, leaving
the
generation staged nowhere and bound nowhere. `SwapCoordinator` now keeps
it:
`take()` records the candidate it hands out and `release()` clears it,
which
spans exactly handover to the moment the caller's own binding points at
the
new loop. The serving loop needed no new line, because it already calls
both
  ends of that span.

The replay runner already stopped the same watcher for the same reason;
these
were the other owners. The stop sits in the generation contract and in
each
command's teardown rather than in `AgentLoop.stop`, whose other caller
is the
generation swap -- that path keeps the process running and must keep
skill
auto-refresh with it.

The gateway's sweep now lives in a module-level
`_retire_generation_watchers`
rather than inline in the serve closure. `run()` is a 550-line closure
with no
import seam, so the inline form could only be pinned by reading its
source text
-- which never executes it, and so earned no coverage on the lines that
matter.
The helper drains all three seats and keeps going when one stop raises,
since a
sweep that stops early still leaves a watcher parked in native code.

Two comments describing a removed guard go with it: one in
`agent_commands`
pointing at an exit chokepoint in `raven.cli.commands.run`, and the
test-side
scaffolding that patched `os._exit` for an exit the command no longer
makes.

## Type

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

## Verification

Process-level, faulthandler armed, throwaway home and a dummy key
nothing calls:

```
                                      before   after
raven gateway   SIGTERM                 -11      0     (-11 on 5 of 5 runs)
raven gateway   SIGHUP then SIGTERM     -11      0     (2 reloads after)
raven tui       PTY, then signal        -11      0
raven agent     ordinary one-shot exit  139      1     (1 is the turn's own
                                                        auth failure, which
                                                        139 had been masking)
```

Controlled pair isolating the cause, on the loop factory the TUI uses --
build
the loop, let the interpreter finalize:

```
watcher left running   exit 139
stop_file_watcher()    exit 0
```

Suites, from the branch tree:

```
uv run pytest tests/integration/test_gateway_sigterm_smoke.py -q -n 0 -m integration
  1 passed          (this is the guard test that was failing)

pytest tests/test_cli_agent_loop_parity.py tests/test_cli_gateway_commands.py \
  tests/test_cli_gateway_health.py tests/test_cli_tui_commands.py \
  tests/test_cli_agent_commands.py tests/test_gateway_spine.py \
  tests/test_generation_swap.py tests/test_plugin_services_lifecycle.py \
  tests/test_rpc_control.py tests/test_rpc_system.py \
  tests/test_skill_watcher_cleanup.py tests/test_trajectory_replay.py \
  tests/test_utils_asyncio_runner.py -q
  307 passed, exit 0

ruff check / ruff format --check over the changed files   clean
ty check over the changed files                           no diagnostics on the changed lines
scripts/check_large_files.py origin/main..HEAD            exit 0
scripts/check_source_language.py origin/main..HEAD        exit 0
commitlint --from origin/main                             exit 0
```

Second round, after review found the in-transition window; rebased onto
`origin/main` at 6d8c363:

```
pytest tests/test_cli_gateway_commands.py tests/test_core_runtime_swap.py \
  tests/test_generation_swap.py tests/test_cli_tui_commands.py \
  tests/test_cli_agent_commands.py -q
  171 passed, exit 0

every test file importing raven.core.runtime          131 passed, exit 0
real gateway: boot, SIGTERM, exit code                0 on 3 of 3, no fault text
ruff check / ruff format --check                      clean
make check-source-language / make check-large-files   exit 0
coverage_gate.py diff --threshold 90                  95.83% (23/24), passed
```

Revert check on that round: removing the `_in_transition` record, the
in-transition seat in the sweep, or the sweep's `try/except` each fails
its own
test and nothing else. The cancellation window itself is reproduced
against real
`SkillFileWatcher` threads, counted by thread name, and is red without
the fix.

Diff coverage on changed lines was 50% (4 of 8) before that round,
because the
gateway-side tests read the chain's source instead of running it. The
one line
still uncovered is the call site inside `run()`, which cannot execute
without
booting a real gateway; the wiring to the seam is pinned by source
inspection.

Each of the four regression tests was confirmed red before its fix. The
TUI and
agent stops were additionally re-confirmed afterwards by removing both
again and
watching those two tests go red, then restoring them.

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

## Risk

User-visible change: these three commands now exit with their real
status
instead of a segfault. Anything reading the exit code sees a different
value --
0 where it used to see 139, and for a failed one-shot the turn's own
non-zero
code rather than 139.

Rollback: revert the commits. The watcher is a cache-invalidation
helper, so
removing the stops restores the previous behaviour exactly, crash
included.

Checked and deliberately not changed:

- The stops are not wrapped in try/except. `ContextBuilder.__init__`
assigns
`skills` unconditionally, `stop_file_watcher` is documented safe when no
watcher was started, and the watcher's own contract is that neither
`start`
nor `stop` raises. This matches the existing production caller in the
replay
  runner, which is also unwrapped.
- `raven acp` reaches finalization the same way but calls `os._exit(0)`
after a
real session, so the fault is masked there rather than fixed. Left as
is;
  making it consistent is a separate change.
- No central gate was reinstated. A future command that builds a loop
and exits
normally would reintroduce this, and the source-inspection tests here
only
  guard the retirement seats that exist today.
- The watcher starts in `ContextBuilder.__init__`, which is
construction-time
work, and `RavenRuntime.discard` is contract-bound to stay call-free
because
a call there would mean an organ was started before FREEZE. So a staged
or
in-transition candidate is retired by the gateway's sweep rather than by
  `discard`, and the docstring records why. Moving the watcher out of
  construction would let `discard` own it; that is a separate change.
- The shutdown-before-consumption window was not reproduced by timing:
three
runs sending SIGTERM 0.2s, 1.0s and 2.0s after a reload all completed
the
  swap first and exited 0. The staged-candidate retirement is driven by
  forcing the state in a test rather than by racing for it.

Known gap in the evidence: the two `raven tui` runs were ended with
SIGTERM
after Ctrl+C went unanswered, because the TUI never finished loading
against a
throwaway home. The ordinary-exit case is evidenced by the agent
one-shot,
which exits by itself with no signal involved.

- [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[1m]) <noreply@anthropic.com>
…ked again (#467)

## Summary

Two regressions in the hand-built deck lanes (medium and high on
Raven-Design), and the stream failure that made every run of them a coin
toss.

**The deck lane draws its own pages again.** PR #451 pointed the
hand-built lane at the engine's helper modules (`ppt_layout`,
`ppt_theme`, `ppt_shapes`), which were written for the max lane's script
backend. Their defaults became the deck: `heading()` put a white surface
band bled across every page, `card()` became the way anything was
placed, text frames were top-anchored with the lower third empty, and a
build script named three colours. Measured on the same request, the
accent colour appeared 8 times across 20 pages. The skill, route note
and assets reference go back to the pre-#451 text: the lane draws with
python-pptx and takes only the icon set, `add_formula` and `math_runs`
from the engine. On the reverted lane the accent appears 54 times, the
deck carries 12 real pictures and the layouts differ page to page.

**Colour is named beside the type sizes.** The skill listed sizes for
every role and said nothing about colour, so a lane with the right
palette still drew in one colour. One paragraph now says what colour is
for; the layouts note no longer reads as one accent per page, which a
lane took as a ceiling. Where `image_search` is not offered, the
fallback no longer tells the lane to give the pictures up: `web_search`
for the page, `web_fetch` it, and download where the fetch backend keeps
image links.

**A stall in a silent think is asked again.** Across two measured runs
the lane's turn failed 28 times on `Upstream idle timeout exceeded`,
every time in the one round that writes the whole build script into a
tool argument. `stream_llm_call` refuses to retry after output a watcher
has seen, and counted reasoning deltas and half-built tool-call
fragments as that output, though neither is ever shown as the reply. So
the configured retry ladder was never reached. `rendered()` now asks
what a watcher could have seen; every retry path resets the buffers
first (a slot left standing merged with the next attempt into a call the
model never made); a stall handed back as an error response carries the
usage it spent. The in-process builtin backend, whose comment claimed a
retry ladder and passed none, now receives the deployment's
`llmErrorRetryDelays` and `llmRetryAfterOutput` through the
SubagentManager. No packaged product takes that path; all five run as
ACP agents on the loop's own call site.

**Two bundled templates edited in place**, by the maintainer's word.
`beige_geometric_general_report` loses its page 20, a dark photograph
cut by a circle under pale rounded cards; 23 pages become 22 and the two
reference pages the lane borrows from it (17, 19) keep their numbers.
`black_circuit_tech_launch` loses the 27 percent circuit texture on its
master and the photograph on its Section Header layout, so its body and
section pages sit on the master's plain `#000000`; cover and closing are
unchanged. `templates.manifest.json` is repinned; `fetch_templates.py
--verify` holds all ten pins. The registry archive was not re-cut.

No configuration default changes. The only behaviour every lane sees: a
stream that fails before any reply text reached a watcher is retried on
the configured ladder instead of failing the turn.

## Type

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

## Verification

```
uv run --frozen pytest tests/test_agent_loop_llm_error_patience.py tests/test_agent_loop_stream.py \
  tests/test_subagent_manager.py tests/test_subagent_routing_backend.py -q
265 passed in 6.85s   (after rebase onto main)

uv run --frozen pytest tests/test_litellm_provider_stream.py tests/test_litellm_provider_stream_end.py \
  tests/test_provider_stream_fallback.py tests/test_providers_stream_idle_budget.py -q
102 passed

uv run --frozen ruff check raven/agent/loop/main.py raven/agent/subagent/backends/raven_loop.py \
  raven/agent/subagent/manager.py raven/providers/streaming.py
All checks passed!

grep -nP "[^\x00-\x7F]" agents/raven-design/route-raven-ppt.md   -> no matches (the note is pinned ASCII)
```

Both new tests were checked against the unfixed code:
`test_a_stall_during_a_silent_think_is_asked_again` fails with the old
`emitted` test, and
`test_what_the_failed_attempt_left_behind_is_dropped` fails with the
buffer resets removed.

The failure itself was reproduced outside the runtime: the same
37-message history, six pictures and tool table, sent through litellm
1.101.0 to the same vendor, succeeds on a short answer every time and
fails mid-stream one time in three when asked to write the full build
script; healthy long streams show a maximum inter-chunk gap of 1.9s over
234s, so the cut is a dropped connection, not a slow model.

Decks built on the changed lane, same request as the #451 acceptance run
(glm-5.3-flash via OpenRouter): medium 21 pages with 12 pictures and 7
text colours, 48 gate findings (4 `excessive_whitespace`, down from 18);
high 13 pages with 13 pictures and 9 text colours, 37 findings. The high
run's page count came from the host agent's forwarded brief, which
stated 10-14 pages the user never asked for; that is a separate issue.

Template edits: `tests/test_ppt_engine_templates.py
tests/test_ppt_engine_default_templates.py
tests/test_ppt_engine_template_bands.py
tests/test_ppt_engine_template_theme.py
tests/test_ppt_engine_measure_geometry.py` -> 152 passed, 1 skipped;
`make check-large-files` -> clean; both templates rendered and read page
by page.

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

## Risk

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

User-visible: on every lane, a streamed call that fails during reasoning
or mid tool-call, with no reply text yet shown, waits out the configured
ladder (default 15/30/60s) and asks again instead of ending the turn. A
call that already streamed reply text behaves as before. Reasoning
tokens of the abandoned attempt are spent again and are now recorded in
usage. Rollback: revert the `providers` commit alone to restore the old
failure; the skill text and the builtin-backend plumbing are independent
of it.

## Related Issues

N/A
## Summary

Space on an attempt row in the `raven trajectory` browser previously
showed two truncated per-turn preview lines. This PR replaces it with an
interactive full-conversation viewer. It re-lands the content of PR
#382,
which was auto-closed by the 2026-09-12 history linearization without
being merged; the branch is rebased onto the linearized main, with one
extra commit expressing the CJK display-width test fixtures as unicode
escapes to satisfy the source-language gate that main gained since.

- Data layer `raven/trajectory/conversation.py`: rebuilds an attempt's
  span snapshot into a causally ordered stream of labeled records (User
  input, LLM input/output, Tool input/output, skill, memory, and
  subagent events). Event-time semantics (inputs at span start, outputs
  at span end) plus nesting depth from parentSpanId keep the causal
  order. Full content comes from artifact files (paths restricted to
  the trace store, streamed 512 KiB cap per file); span previews are
  only a visible fallback. ERROR spans and expected-but-unreadable
  payloads always yield a placeholder record. LLM inputs omit only the
  item-by-item verified common message prefix against the previous call
  (omissions and rewrites are announced); tool_calls of unexecuted
  tools keep their full arguments.
- Rendering: kind-colored labels aligned to one column, hanging-indent
  bodies with word-aware CJK-safe wrapping, degraded/error/meta note
  lines, single-pass turn separators (interleaved traces are never
  regrouped), and a stacked layout on narrow terminals that never drops
  characters.
- Interactive viewer (prompt_toolkit full-screen, entered for every
  preview on a terminal): scrolling, a muted bottom bar with an
  'h for help' hint, filter/collapsed state and the scroll range (laid
  out by display width, never overflowing any terminal), a scrollable
  help page reachable in full on small screens, a global collapse (s:
  bodies over five display lines fold to five plus an ellipsis; error
  and degradation notes never fold), and a label filter (%: real input
  line, case-insensitive, empty clears; filtered-out turn groups lose
  their separators). Resizes re-wrap and re-judge folding live; the
  content caret is hidden (only the filter input shows one). q/Esc
  returns to the list; Ctrl+C cancels the whole browser from any viewer
  state. A rebuild failure falls back to the legacy per-turn preview;
  without a terminal the preview degrades to direct printing.
- Every dynamic field passes the sanitization gates (control
  characters, rich markup, and ANSI never reach the terminal). Adds the
  Conversation Record term to CONTEXT.md.

## Type

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

## Verification

- `uv run pytest tests/test_trajectory_conversation.py
  tests/test_cli_preview_viewer.py tests/test_cli_trajectory_browse.py
  tests/test_trajectory_store.py -q` -> 298 passed (viewer assertions
  run against really rendered screens)
- `uv run pytest -q` (full, on the rebased branch) -> 23463 passed,
  125 skipped; 33 failed + 20 errors reproduce with identical node ids
  on a clean origin/main worktree - environment baseline, not
  introduced here (passed-count delta of 96 equals the tests this
  branch adds)
- `make check-source-language`, `make check-large-files`,
  `make lint-types`, `make lint-python`, `make lint-imports` -> all pass
- Manual: rendered 6 real local attempts at widths 72/120/30 with zero
  over-width lines (largest attempt 7060 lines in 0.35s); drove the
  real TUI in a pty at normal and 44x12 sizes (viewer, help paging,
  collapse, label filtering, Ctrl+C cancel)

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

## Risk

- Read/display side only; no persisted format changes. The untrusted
  input surface (artifact content, typed filter input) is contained by
  the store-prefix path check, the 512 KiB streamed cap, and per-field
  sanitization.
- User-visible change: the Space preview is now a full-screen viewer;
  the legacy rendering remains as the automatic fallback when the
  rebuild fails, and non-terminal environments print directly.
- Rollback: revert this PR, or restore the previous `_preview_screen`
  call site to get the old preview back.

- [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-fable-5) <noreply@anthropic.com>
…e shell (#516)

## Summary

The documentation site built one search index for both languages, so a
reader on
the English pages was offered Chinese ones: results they cannot read,
behind
links that leave the language they chose. A build hook now splits the
merged
index in two and points the Chinese pages at their own copy, which the
theme
also resolves every result link against.

Where that split runs is the whole fix. The merge lands in a plugin's
own
`on_post_build`, so the split has to be the last thing that event does.
Running
it on `on_shutdown` looks equivalent and is not: `mkdocs serve` never
shuts
down, so the preview went on serving the merged index however often it
rebuilt.
The new guard makes the same assertion against `mkdocs build` and
against
`mkdocs serve`, because passing one says nothing about the other.

The rest settles the shell against the reference layout:

- the rail's foot is its own box the navigation cannot show through, and
the tab
  icon is the EverMind mark;
- the footer keeps to the content column instead of covering the fixed
columns
  once the reader reaches the page foot;
- the contents column reaches the screen foot with its scrollbar hidden
behind a
  fade, and follows the reader by centring the first lit entry;
- search opens as a centred dialog over the page. The form itself
travels into
that dialog, so what stays in the rail is drawn from the field's own
metrics
and its shape does not change on a click. Neither state animates: the
theme
transitions the two shapes into each other, and leaving search played
that out
  in the corner of the page after the reader had moved on.

## Type

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

## Verification

- `uv run pytest tests/integration/test_docs_site_search_e2e.py` -> 2
passed.
Without this change the preview case fails: the Chinese index is HTTP
404 and
  the English index carries 146 Chinese pages.
- `uv run pytest tests/integration/test_docs_site_controls_e2e.py` -> 25
passed
- `uv run pytest tests/test_docs_site.py
tests/test_source_language_check.py` ->
  31 passed
- `make check-commits`, `make check-source-language`, `make
check-large-files` ->
  exit 0; `ruff check` on the added files -> no findings
- `make docs-build` -> the root index holds 149 entries and no Chinese
page; the
  Chinese index holds 146, none of them addressed from the site root

Driven in a real browser at 1560x820 in both languages: a word that
appears in
both trees returns 10 results and 0 from the other language, and the
corner of
the page is pixel-identical to its never-opened state at every frame
after
leaving search. Every page of the built site was checked at the page
foot (28
pages, 0 faults) for a footer covering a column, for a column whose last
entry
cannot be reached, and for the rail's foot icons being clickable.

No page content changed, so nothing else needed updating with it.

- [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: search no longer returns pages from the other language,
and the
search control, the footer and the two fixed columns change shape. The
field
left in the rail while the dialog is open is a drawn replica, and every
rule
behind it sits above the theme's desktop breakpoint, so narrow viewports
keep
the theme's own full-screen search untouched.

Rollback: revert the commit. The hook is one file, and dropping it from
the
`hooks:` list in `docs-site/mkdocs.yml` restores the previous merged
index on
its own.

- [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[1m]) <noreply@anthropic.com>
)

## Summary

An EverOS role points a section at a provider: the settings card writes
the
model and asks `lend_provider_credentials` for that provider's key and
address to go beside it. The address was copied only when there was one
to
copy -- the reasoning being that a section with no lender address may be
holding one the reader typed by hand.

A section being pointed at a new lender is holding the OLD lender's
address,
though, and leaving the field out of the answer keeps it. Moving the
memory
role from an OpenRouter model to a DeepSeek one left DeepSeek's key at
OpenRouter's address, which the far end refuses. DeepSeek is not a
corner
case here: every vendor LiteLLM routes carries no `default_api_base`, so
the
lend answers with no address for all of them.

The lender answers about the address either way now, empty included. An
empty
value reads where it lands the way an unset one does: the EverOS writer
sets
the field, and the section stops carrying somebody else's endpoint.

Found while running the settings page's acceptance case for the EverOS
roles
against a real gateway. The lend rule is from #414, not from the
settings
work that surfaced it.

## Type

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

## Verification

- `uv run pytest tests/test_config_update_providers.py
tests/test_rpc_settings.py
tests/test_cli_onboard_commands.py tests/test_rpc_console.py -q` -- 641
passed
  on the branch this was written on; 191 on the two closest suites here.
- The new case fails without the change: the lender answers `{"api_key":
...}`
  where the section needs `{"api_key": ..., "base_url": ""}`.
- `uv run ruff check` and `ruff format --check` over both files --
clean.
- Real gateway, temp `RAVEN_HOME`, EverOS plugin present:
`settings.everosSet`
  with `borrow_from: "deepseek"` writes the model and DeepSeek's key and
leaves no `base_url` behind; the same call with `borrow_from:
"openrouter"`
writes OpenRouter's address back. Before the change the first of those
left
  `base_url = "https://openrouter.ai/api/v1"` under DeepSeek's key.

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

## Risk

- Two callers, both of which point a section at a provider: the EverOS
card
(`settings.everosSet`) and the CLI onboarding reuse step. For a lender
that
  has an address, nothing changes. For one that has none, the borrowing
  section now stops carrying the previous lender's -- which is the fix.
- A reader who typed an address by hand into an EverOS section and then
borrowed from an addressless provider loses that typed value. That is
the
same event as the bug, read the other way; the section is being pointed
  somewhere else, and the card shows the resolved address.
- Rollback: revert the commit.

## Related Issues

N/A

---------

Co-authored-by: gloryfromca <23442919+gloryfromca@users.noreply.github.com>
Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
ypflll and others added 2 commits September 20, 2026 11:36
…write (#495)

## Summary

#469 gave a bundled sub-agent nine verbs in place of six hook phases.
Reviewing it turned up a
list of things the contract said but did not yet do, and this PR closes
that list. No behaviour
change is intended: the point is that the same seam becomes something a
role can replace and
something a generated judgement can enter.

**1. The roles are named apart from the verbs, and `observe` is gone.**
`ActionModule.review`
and the participant's `review` were two different things a line apart.
The four delegating role
methods now say what they do -- they ask -- and the verbs keep their own
names, so `ask_review`
asks `review`. `observe` is removed: a verb with no return value decides
nothing and can only
keep state a participant is forbidden to keep. `outbound` says on its
own docstring that it is
the one verb with no role seat, and `StepView` gains `phase`, so a verb
asked at two moments
reads which one it is rather than inferring it from whichever other
field happens to be set
(`tools_ran` becomes a property derived from it, so the two cannot
disagree).

**2. The three verbs the seat was applying itself get a seat.**
`system_addendum`, `archive` and
`select_tools` were rendered straight onto the hook decision, so each
was the one judgement in
the set with nowhere to compose two participants, nowhere to vet what
came back, and nothing a
replacement role could decide. They join the other four:
`MemoryModule.ask_system_addendum` / `ask_archive` and
`CapabilityModule.ask_select_tools`. The
seat is not the point where narrowing takes effect -- `select` still is,
and the registry still
adjudicates every call -- so a participant narrows after the product has
spoken and never instead
of it.

**3. A participant answers with plain data.** `review`, `intake` and
`system_addendum` returned
host classes (`Verdict`, `Intake`) and `salvage` returned `Any`.
`Accept`, `Resample`, `End` and
`Intake` are now builders returning plain mappings, the host reads one
back with `read_verdict` /
`read_intake`, and an answer it cannot read is silence rather than a
refusal. That is what makes
the third invariant real and what lets a judgement compiled from source
answer the same verbs a
plugin does -- it can build a `dict`; it cannot build a host class.

**4. A dispatch's judgements are a participant.** `judge` was the one
verb a role answered by
reading the dispatch's own `checks` and `code` for itself. A Charter now
enters as a participant
like any other, `ActionModule.ask_judge` takes the list, and the merge
is a veto so the first
refusal decides. The participant list becomes the only route an external
judgement takes, which
is the thing step four needs: a judgement generated for one dispatch
joins the same list without
a second way in.

**5. The rename.** `AgentConduct` is `AgentParticipant`,
`contracts/agent_conduct.py` is
`contracts/participant.py`, `agent/hook/conduct.py` is
`agent/hook/participant.py`, and
`agent/harness/conducts.py` is `agent/harness/participants.py`.
"Conduct" was already taken in
this tree -- raven-code's Coding Conduct prompts are a different thing
-- and a reader hitting both
had no way to tell them apart. The rename was done symbol by symbol, not
by search and replace,
for exactly that reason.

**7. Review (LivXue, at 3a1bf07).** The `judge` disclosure had reached
two of its four homes;
the other two are `ActionModule.ask_judge` on the paper and
`DefaultAction.ask_judge`, and they
survived because they phrase the claim without naming any symbol the
earlier fix touched. The test
meant to pin that claim could not fail for the reason it was written --
it asserted the refusal
rather than the argument, and built a `ParticipantHook` it dropped --
and now asserts what the
registry hands the role, mutation-checked by wiring a participant
through. `compose_judge`
exploded a one-sentence `str` refusal into one refusal per character,
because `str` satisfies
`Sequence[str]` structurally; `charter_code.run_judge` coerced it where
this composition used to
live and the handling did not move with it. Two records corrected: the
type-only
`raven.contracts` -> `raven.agent` edge, the first in the tree and the
price of moving `Verdict`
and `Intake` off the paper, is now written beside the import contract it
bends rather than left to
be discovered; and the ledger entry for the 3,200 -> 3,410 bump is
restored to the names and count
it was written with. Not taken: reading `step.phase` at
`research_flow/flow.py:1000` -- the gate
chain's phase vocabulary and `StepView.phase` coincide on
`after_iteration` only, and that call
site wants `before_execute_tools`, which is the key `_phase` dispatches
on.

**6. What the paper was claiming and not doing.** Read back over the
seam afterwards rather than
over the diff. ``judge`` read as though a plugin's override were asked
ahead of a dispatch's own
rules; it is not, and cannot be as written, because the party that asks
the verb is
``ToolRegistry.execute``, which is not the hook chain and holds no
handle on this turn's
participants. The merge order is real; nothing puts a plugin in the
list. Wiring one through is a
turn-scoped register this change does not add, and it would start
refusing calls that run today,
so it is named on the paper and pinned by a test instead.
``max_iterations`` had lost its
docstring to the property inserted between the field and the string
documenting it. No executable
line moves in this commit: the module's AST, docstrings stripped, is
identical to its parent's.

CONTEXT.md's entry is rewritten to the end state, including the two
kinds of role method, the two
verbs that are exceptions, and the note that the Coding Conduct prompts
are unrelated.

One item from the same list is deliberately not here: a `planning.*`
block on the dispatch
parameters. It is not a contract closeout but a new model-facing field,
and it needs the
generator's schema and prompt to move with it.

Contract papers: `participant.py` and `harness.py` are both factory-loop
tier, so the
contract-tier digest and `CONTRACTS_VERSION` do not move.
`CONTRACTS_LINE_CEILING` 3,440 to
3,470 to 3,500 to 3,540 to 3,550 to 3,570, each bump with its own review
paragraph in
`tests/test_kernel_budget.py`. The chain was recomputed after rebasing
onto #410, which added
seven lines to `loop_hooks.py` inside the old headroom and left one
commit here with one line of
it; every "Measured at" figure in that file is the count at that commit.

## Type

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

## Verification

Gates, run at each of the seven commits:

```
uv run ruff check --no-cache raven evolver agents plugins-dist tests scripts
uv run ruff format --check --no-cache raven evolver agents plugins-dist tests scripts
uv run lint-imports
uv run pytest -q
npx commitlint --from origin/main --to HEAD --config commitlint.config.cjs
uv run python scripts/check_commit_messages.py origin/main..HEAD
uv run python scripts/check_source_language.py origin/main..HEAD
uv run python scripts/check_large_files.py origin/main..HEAD
```

Import contracts: 10 kept, 0 broken. The suite is green: the failures
this machine shows are the
same ones it shows on an unmodified `main` in the same worktree layout
(missing browser and
optional packages), and this branch adds none.

Behaviour preservation. The oracle is the hook seam itself. `main` and
this branch were run under
the same scripted provider, outputs normalised for nonces and temp
paths, then byte-compared.
Four scenarios, all identical:

- the five-plugin hook seam, every phase's decision and metadata per
iteration (23,840 bytes)
- a whole turn with no playbook: host turn, spawned sub-agent, tool
calls and refusals
- a playbook-on worker lane in process, including the charter refusals
- six window-shrink scenarios

This is the seam the change is about, and identical output across it is
what "no behaviour
change" means here -- it is not a claim about paths the four scenarios
do not reach.

Two claims had no assertion anywhere and now do. ``phase``, and the
``tools_ran`` derived from
it, are what three of the five bundled plugins branch on -- ppt on its
close, oncall on its
counter, research on which gate runs -- and a mistyped phase string at
one of the six call sites
would have changed what they do with a green suite. The read-only test
now also covers
``mode_overlay``, the one frozen field it skipped. All three assertions
were mutation-checked:
mislabelling a phase, deriving ``tools_ran`` from the wrong one, and
dropping the freeze each
turn exactly their own test red.

New tests, each written against a claim the contract now makes rather
than against the code:
a participant cannot write through the `StepView` it is shown; an answer
the host cannot read is
silence, not a refusal; a participant that answers with a mapping it
built itself is understood;
every verb with a seat goes through its role rather than around it,
asserted as a set so a new
verb that bypasses its role fails; two seats in one chain each keep one
addendum; two turns in
flight keep their own participants; a bound role decides what a
participant's verdict does; a
plugin's own judgement speaks before the dispatch's, and a silent
participant lets the next one
speak.

Real model, Sonnet 4.5 through OpenRouter, on this branch: a query
generates a playbook, two
sub-agents are instantiated and run, each writes its `summary.md` under
its own fence, and both
results are announced back to the originating session.

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

## Risk

For plugin authors: the six-phase `AgentHook` is untouched and a plugin
shipping one keeps
working. A plugin written against #469's `AgentConduct` needs the new
names and, if it answered
with `Verdict` or `Intake`, the builders instead -- the five bundled
plugins are ported here and
are the worked examples.

One verb narrowed, and silently for a plugin that used the width.
`salvage` was annotated
`Any | None` and `compose_salvage` returned whatever was not None; it is
now `str | None` and only
a non-empty string is taken. A third-party plugin that salvages with a
reply object rather than
text stops being heard rather than raising -- which is the third
invariant working as written, and
worth knowing before upgrading. The narrowing is deliberate: `salvage`
answers the text a turn
sends when it produced none, and `Any` was not a contract.

For anyone replacing a harness role: the four delegating methods are
renamed and four more are
added, so a replacement written against #469 needs them. `bind()` names
the missing member at
assembly rather than failing inside a turn.

Rollback is a revert of the merge commit. Nothing persisted on disk
changes shape.

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

## Related Issues

Follows #468 and #469.

Generated with Claude Code (https://claude.com/claude-code)

---------

Co-authored-by: yao pengfei <yaopengfei@shanda.com>
Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
…tecture

Conflicts and how they were settled:

- ui-web/src/features/settings/SettingsPage.tsx, its test, and
  ui-web/src/live/120-settings.js: deleted on this branch, edited on main. The
  rebuilt settings surface replaces all three, so the deletions stand.
- i18n/messages.json and ui-tui/src/i18n/messages.generated.ts: main's only
  change to either since the merge base is #506 dropping gui.set.chn.cron_to
  and cron_ph. This branch already drops the whole gui.set.chn.* group, so its
  side is the superset and wins.
- raven/skill_hub/hub.py: each side added an import from skill_hub.audit. Both
  symbols are used and both exist after the merge, so both are imported.
- ui-web/build.py: main rewrote the newline="" comment, this branch added
  flush=True to the print beneath it. Kept both.

The cron timezone fix from #506 auto-merged into raven/rpc/methods/console.py
and brings its tests with it. The rebuilt settings pages have no cron section,
so that control has no UI on this branch.

Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
@gloryfromca
gloryfromca requested a review from 0xKT September 20, 2026 05:08

@gloryfromca gloryfromca left a comment

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.

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

Reviewed the full target-to-head diff and merge topology, then checked all seven explicit conflict resolutions plus the paths changed on both sides that Git merged automatically. I also covered AGENTS.md/CLAUDE.md, the Web UI architecture vocabulary and boundaries, relevant callers and source-commit history, backward compatibility, and whether tests were removed or weakened. The deleted legacy settings paths do not lose the embedding-model warning fix: the rebuilt settings source already carries that behavior. The cron-timezone control remains absent, as disclosed, but that absence predates this merge on the target branch.

Verification: npm test passed 181 files / 2,513 tests; the targeted Python suite passed 174 tests after installing the repository's documented all-extras test environment; npm run type-check, npm run lint, and npm run build exited 0 (lint reported five existing hook warnings); and uv run python ui-web/build.py built the page with both boot snapshots matching. The first Python attempt in a fresh default environment produced 85 setup errors because raven_everos was not installed; rerunning after uv sync --locked --all-extras --dev passed. The worktree remained clean.

The currently failed commit-message check is the inherited-main-history case already documented in the PR, not a defect introduced by this revision.

@gloryfromca gloryfromca left a comment

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.

Blocking: the merged ext.list test suite must be restored by updating the stale tool-registry stub.

Reviewed the delta from the previously assessed revision, the full target-to-head diff, the merge topology and source-commit history, AGENTS.md/CLAUDE.md, the Web UI context and architecture boundaries, affected callers, backward compatibility, and whether tests were weakened. The merge has no manual tree deviation from Git's clean recursive result, but the inline test-contract incompatibility is a semantic merge failure.

Verification on this exact head: npm test passed 181 files / 2,518 tests. uv run pytest tests/test_provider_rates.py tests/test_rpc_config.py tests/test_rpc_console.py tests/test_rpc_settings.py -q produced 325 passes and the one inline failure; the isolated failing test reproduced it again. The worktree remains clean.

Comment thread tests/test_rpc_console.py Outdated
LivXue and others added 4 commits September 20, 2026 14:53
… row share (#535)

## Summary

Replaces the drawing in `ui-web/icon/raven.svg`. Nothing else moves: no
path,
no stylesheet rule and no caller changes.

That one file has two consumers, because `ui-web/build.py` copies it
over
`dist/assets/raven.svg` after the rest of the asset tree is in place:

- the browser tab icon, through `ui-web/src/page.html`;
- the mark on the first row of the file-processing list in the knowledge
base
  settings dialog, through `ravenIconPath()` in
  `ui-web/src/shell/provider-mark.tsx` and its one call site in
  `ui-web/src/features/knowledge/KnowledgePage.tsx`.

The second one is easy to miss, so it was checked rather than assumed.
The row
sizes the mark at 17px through `.kbprocr .provider-icon` in
`ui-web/src/styles/page.css`, not at the 20px the base `.provider-icon`
rule
gives, and it is the first row, so it renders at full opacity. The image
carries no `data-tone` attribute, so none of the six `invert(1)` rules
in that
stylesheet matches it and the theme switch leaves it alone. Both were
confirmed
by rendering the built asset at 17px on the light and the dark surface.

The new file keeps a `viewBox`, which is what lets one drawing serve a
17px row
and a 32px high-dpi tab.

This does not touch `ui-web/src/assets/agents/raven.svg`, the separate
mark the
agent rows draw. Those two files were already different drawings and
still are.

## Type

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

## Verification

Gates, against this branch's own range rather than a stale one:

- `scripts/check_large_files.py origin/main..HEAD` exit 0. `ui-web/` is
exempt
from the extension list by `scripts/check_large_files.py`, and the file
is
  1199 bytes, down from 2524.
- `scripts/check_source_language.py origin/main..HEAD` exit 0.
- `scripts/check_commit_messages.py origin/main..HEAD` exit 0.
- `npx commitlint --from origin/main --to HEAD` exit 0.
- `npm run gen:check --prefix ui-web` exit 0, 178 methods matched.
- `npm run type-check --prefix ui-web` exit 0.

Tests. CI does not run `make test-ui`, so the one test file that names
this
asset was run directly:

- `npm test --prefix ui-web -- --run
src/features/knowledge/KnowledgePage.test.tsx`
  gives 5 failed, 104 passed, 0 unhandled errors, exit 1.
- All 5 are the `the recall test` cases, and node names the cause
itself:
`localStorage is not available because --localstorage-file was not
provided`.
- They are inherited, not introduced. `git diff origin/main..HEAD
--name-only --
ui-web` lists this SVG and nothing else, so the JavaScript that run
exercised
is byte-identical to `origin/main`; the run was the baseline. No test
imports
  or reads the file, so its bytes cannot reach an assertion.

Rendering. The built asset was rendered in headless Chrome at 16, 32, 48
and 64
pixels on a light and a dark tab strip, and at the row's real 17px box
on both
themes.

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

The first box is left unchecked deliberately. The relevant file reports
5 red,
and while the evidence above shows all 5 are inherited and unreachable
from this
change, the suite does not come back green on this machine and saying it
does
would be false. On the third box: `grep` over the tracked tree finds no
prose
describing this drawing. The favicon sentences in
`ui-web/src/styles/page.css`
and `ui-web/src/shell/agent-mark.tsx` describe `miromind.svg` and the
agent
mark, both of which this leaves alone.

## Risk

User-visible in two places, both cosmetic. Rollback is reverting the
commit and
rebuilding; `dist/` is generated, so nothing downstream holds a copy.

Checked and deliberately not changed:

- At 16px the halo ring largely disappears. Its stroke is 1.6 units on a
64
unit canvas, which is 0.4 of a physical pixel there, and a count of ring
pixels falls from 245 at 64px to 17 at 16px. A high-dpi tab draws 32px,
where
it holds. Widening the stroke would change a drawing that was chosen as
it is,
  so it is reported rather than edited.
- No test pins the `viewBox` of this file, and none is added here. No
icon in
the tree has such a guard, and adding the first one is a wider decision
than
  this change.

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

The security note: a favicon is parsed by the browser directly, so the
file was
checked for active content. It has no `script`, `foreignObject`,
`image`,
`xlink:href`, `href`, `url(`, `@import` or event-handler attribute; the
only
external token is the SVG namespace declaration.

## Related Issues

N/A

Co-authored-by: Claude (claude-opus-5[1m]) <noreply@anthropic.com>
…534)

## Summary

A presentation pass over both READMEs. No prose was rewritten and no
benchmark
number changed; all four changes are about how the Built-in Agents and
EverMind
Ecosystem sections read.

1. The four agent subheadings (Raven-Research, Raven-Code, Raven-Design,
Raven-Oncall) now carry a single chevron, one step lighter than the
double
chevron the section headings already use. That gives depth by weight
without
   introducing a second symbol family.
2. The six Raven-Code, Raven-Design and Raven-Oncall benchmark charts
render at
95% of the line instead of full width. Both READMEs reference the same
six
assets, so the width is set in each file or the two language pages
disagree
   about the same picture.
3. The EverMind Ecosystem section now opens with a brand banner, cropped
to
2172x511 (4.25:1) from the 3.00:1 original. The empty field above and
below
the orbital composition is gone and the remaining margin is equal on
both
sides, which the original was not. The composition itself is untouched.
4. The two Raven-Oncall captions state the result their chart shows
rather than
   naming the comparison.

Checked and deliberately not changed:

- The two Raven-Oncall charts label the same model differently:
DS-V4.1-Flash
  on AI4AI, DS-V4-Flash on AI4S. Pre-existing, and correcting it means
  regenerating a chart rather than editing text.
- The Raven-Research chart keeps its full width, and is now the only
chart in
  that section which does.

## Type

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

## Verification

Gates, each against a non-empty branch range:

    make check-large-files              exit 0
    make check-source-language          exit 0
    scripts/check_commit_messages.py    exit 0

Tests:

    uv run --frozen --all-extras pytest tests/test_living_docs.py \
      tests/test_scope_canon.py tests/test_commit_lint.py -q
    15 passed

Assets: for each uploaded image the published URL was fetched with GET
and its
sha256 compared against the local file. Both matched. HEAD is not usable
for
this check, because user-attachments answers 403 to it whether or not
the
upload succeeded.

Greps:

- The superseded banner asset id has zero occurrences repo-wide.
- The four agent heading anchors have zero references in either README,
in the
documentation site, or in the workflows, so the slug change the chevron
  prefix causes breaks nothing that links to them.

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

The first two boxes are blank on purpose. No test reads the two root
READMEs:
test_living_docs globs the package READMEs, test_scope_canon reads a
documentation-site page, and test_external_consumer_surface reads a
different
README. The three suites above ran because they are the nearest
documentation
guards, not because they cover this change. Repository lint does not
cover
Markdown.

## Risk

User-visible: the four agent subheadings gain a symbol, so their
generated
anchors change; six charts render slightly narrower; the ecosystem
section
gains a banner; two captions assert a result. Nothing outside the two
README
files changes.

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

Security: no credential, endpoint or permission is touched. The uploaded
image
is a brand banner, public by intent.

Backward compatibility: the heading anchors are the one break. Nothing
in the
repository links to them, but an external bookmark to one of the four
would
stop resolving.

Rollback: revert the commit. Images are attachments rather than
committed
files, and superseded assets stay published, so any link already shared
keeps
resolving.

## Related Issues

#530 hosts the ecosystem banner assets and stays open for that purpose.

---------

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

Raven speaks the Agent2Agent protocol, version 1.0, in both directions.
The host
agent is the peer: nothing here makes a sub-agent addressable from
outside, and a
raven launched as a sub-agent refuses to serve the face at all, because
it is
already reached over ACP by the host that started it.

Inbound is an aiohttp face carrying the two cards and a JSON-RPC
endpoint. It
mounts on the gateway when `a2a.server.enabled` is set, and `raven a2a
serve` runs
it standalone for a headless deployment, where running the command is
itself the
opt-in. Outbound is a single tool, `a2a_send`, that takes a card URL and
a message;
it enters the tool list only when `a2a.peers` is non-empty, so a host
with nowhere
to send a message does not carry the schema. The two switches are
deliberately
independent: calling out and being called are separate deployments.

Onboarding mints `a2a.server.token` and stops there. The face ships
closed:
finishing an unrelated install is not consent to open a second network
surface, so
`raven a2a enable` is what opens it, and it is the only writer of that
flag.
Enabling mints a missing token in the same command, because a face
switched on with
an empty token answers every caller with a 401 and reads as a broken
deployment
rather than the closed door it is. `raven a2a disable` closes it again
and keeps the
token, so reopening does not re-issue a credential callers already hold.
The wizard
never writes the flag at all, not even as false, so a repeat onboard
leaves a face
the operator opened by hand alone. A config nobody onboarded, and any
`A2aConfig()`
assembled in process, stays inert.

What that opens is narrower than a network face. The app carrying it
binds loopback
at both call sites, hardcoded rather than read from config, and every
request is
checked against a freshly minted 256-bit bearer token. The minting write
is also
what fixes `config.json` to owner-only, from the temporary file's first
byte rather
than with a chmod afterwards; a later enable or disable adds no secret
material, so
it leaves whatever mode the operator chose. Nothing else narrows that
file and it
already held provider API keys, so the obligation predates this work and
is merely
discharged here.

There are two cards because the protocol splits one description across
two
authentication states. The public card answers an unauthenticated GET,
and has to:
a caller reads it to learn which scheme to authenticate with, so
requiring the
credential first would be circular. It therefore names no installed
agent. The
extended card is a `GetExtendedAgentCard` RPC riding the same bearer
check as every
other method, and is the only one of the two that may say what this host
can
actually do.

On that extended card the sub-agent roster rides
`capabilities.extensions`, not
`skills`. A skill is the protocol's word for something an agent can be
asked to do,
and a peer cannot ask for one named sub-agent: it can only send a
message to this
host, which then decides what to dispatch. One skill per sub-agent would
advertise
call targets that do not exist. What a peer can genuinely request is the
orchestration, so that is a single skill, and the roster moves to the
protocol's
own extension point under a versioned URI. That is the only place a
conformant card
may carry a payload the spec does not define, because `AgentCard` is a
closed set
of fourteen protobuf fields: a custom top-level key is rejected by a
strict parser
and silently dropped by a lenient one. The entry declares `required`
false, so a
reader that has never heard of the URI ignores it and still talks to
this host.

Both cards derive `supportedInterfaces[].url` from the origin the caller
arrived
on. One face answers under every name that routes to it, and a URL fixed
when the
face is mounted is right for at most one of them. This matters more than
it looks:
a caller that finds an interface off the card's own origin must refuse
it, since a
card fetched from a trusted origin would otherwise redirect the
credential
elsewhere, and a relative URL reads as off-origin to that check.

## Type

- [x] Feature

## Verification

- `make test-python` at this head: 23729 passed, 112 skipped, 1 failed,
in 288 s.
  The one failure is

`tests/test_install_script.py::test_resolve_node_dir_answers_each_case_it_exists_for`,
which fails with the same assertion on `origin/main` by itself; this
branch changes
  no install or node file.
- `uv run --frozen --all-extras pytest
tests/integration/test_a2a_protocol_e2e.py -q`:
6 passed. That directory sits outside the default pytest scope, so it is
run by
  hand rather than reached by the line above.
- `make lint-imports`: 10 contracts kept, 0 broken. `make lint-types`:
all checks
  passed.
- `uv run ruff check raven/ tests/`: clean. `ruff format --check`:
clean.
- `make check-commits` over all 68 commits, `make
check-source-language`,
  `make check-large-files`: all exit 0.
- Live, over a real network boundary rather than in process: a raven in
a Docker
container and a raven on the host, each running this branch, called each
other in
both directions. Nineteen assertions per direction covering both cards,
the four
refusal paths, and a completed turn. The container publishes its face on
a
different port than it binds, so the card has to name the port the
caller used and
not the socket the server opened; both cards did, in both directions.
That run
predates the default-off change described above, which touches no file
under
`raven/a2a/` or `raven/a2a_client/`: both packages are byte-identical to
the head
  it ran against.
- Reverting each fix turns its own tests red, checked one fix at a time
rather than
all together, including the interface-URL fix mutated out inside the
running
  container.

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

## Risk

Behaviour changes for an operator who runs `raven onboard` after this
lands: a
token is minted into `a2a.server.token`, and `config.json` is narrowed
to
owner-only. No listening face is opened. Opening one takes `raven a2a
enable` and a
gateway restart, so the second network surface only ever exists because
somebody
typed for it.

Rolling an opened face back is `raven a2a disable` and a restart. The
token stays in
place and is never rotated, so callers already holding it keep working
when the face
is reopened, and a later onboard will not reopen it by itself. Nothing
else in the
install changes.

Costs and limits, stated rather than left to be discovered:

- A gateway that serves A2A imports `a2a-sdk`, measured at roughly 570
ms and 391
modules on top of the gateway's own stack, sqlalchemy and the
OpenTelemetry api
among them. It is paid once at mount, not per request, and an operator
who
switches the face off pays none of it. Moving it to the first request is
recorded
  as deferred in the design note rather than done here.
- `a2a_send` adds 157 tokens to the tool array of every turn, measured
off a
  captured provider call, and only on a host with a peer configured.
- The task store is in memory and unbounded, and inbound concurrency is
not capped.
A local face behind a bearer token is the deployment this is written
for; a
  host exposed more widely should bound both first.
- Pulling in `a2a-sdk` brings the OpenTelemetry api into the gateway's
import
graph. This is a trade accepted for the SDK's protocol types and
handler, not an
  adoption of OpenTelemetry by this project.

Checked during review and deliberately not fixed here: config validation
accepts an
agent row with an empty or whitespace name, which would reach the
extended card's
roster as an empty entry. It is not introduced by this work, the
previous shape
carried the same value, and closing it belongs with the agent schema
rather than
with the card.

- [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>
#513 added schema_hidden_names() to the tool registry and updated the
stand-ins that existed on this branch; ext_list calls it to decide which
tools are fixed. #464 landed a case on main with a stand-in of its own. The
two sit in different places in the same file, so merging the branches raised
no textual conflict and left the new call beside the old fake.

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

Copy link
Copy Markdown
Member Author

Fixed in 0a064daaa, same diagnosis. tests/test_rpc_console.py:249 built the ext_list stand-in before the registry had the method, and now reads

tools=SimpleNamespace(tool_names=[], get=lambda _name: None, schema_hidden_names=frozenset),

which is what the other stand-in in the same file (line 189) already carried.

Where it came from: #513 added schema_hidden_names() to the tool registry and updated the stand-ins that existed on refactor/ui_web_architecture at the time. #464 landed the failing case on main with a stand-in of its own. The two sit in different places in one file, so the merge raised no textual conflict and left the new call beside the old fake.

Bisected, so the arrival point is not a guess: tests/test_rpc_console.py is 98 passed at a0666244c (the catch-up merge) and 1 failed / 98 passed at 3abc4e14f (the later merge of the refreshed branch into it). It comes in with the second merge.

Verified after the fix: tests/test_rpc_console.py 99 passed, and 457 passed across test_rpc_session.py, test_rpc_contract_shapes.py, test_rpc_settings.py, test_rpc_console.py, test_agent_loop_tool_search.py, test_tool_search.py, test_tool_registry_withheld.py. The other registry stand-ins live in the first two of those and do not reach ext_list, so that run covers them too.

@gloryfromca gloryfromca left a comment

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.

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

The one-line delta supplies the registry method required by the merged ext_list() contract, matches the neighboring fake, and restores the prior finding without weakening its assertions. I checked the delta against the full target diff, affected caller and history, repository rules and Web UI architecture constraints, backward compatibility, and test integrity; no new issue remains.

Verification: the formerly failing test passes in isolation, and uv run pytest tests/test_provider_rates.py tests/test_rpc_config.py tests/test_rpc_console.py tests/test_rpc_settings.py -q passes 326/326. The prior review thread was replied to and resolved, and the worktree is clean.

gloryfromca and others added 2 commits September 20, 2026 15:21
## Summary

A pull request whose base is not the default branch carries, in its
`BASE..HEAD` range, every commit the default branch has gained since the
two
diverged. Both commit linters read that range, so they re-read history
that
already passed this gate when it landed and that nobody can rewrite now.

#529 is the case that surfaced it: merging `main` into
`refactor/ui_web_architecture` puts 38 `main` commits in range, one of
which
(`b01534d0`, #471) carries a `Co-authored-by` trailer with a non-ASCII
name.
The gate reports that PR red for a message it did not write. The same
will
happen to every future catch-up merge.

`scripts/check_commit_messages.py` now takes several revision arguments
and
passes them to `git log`, so a caret exclusion works. Commitlint has no
equivalent flag, so the workflow iterates the same revision list and
lints one
message at a time. On a pull request the workflow excludes the
repository's
default branch; a push to the default branch is unchanged and still
lints what
it pushed.

This does not touch the offending commit on `main` and does not relax a
rule:
a commit that a PR actually introduces is linted exactly as before.

## Type

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

## Verification

`uv run pytest tests/test_check_commit_messages.py
tests/test_commit_lint.py -q`
-- 15 passed. Three of those cases are new and cover a repo where the
default
branch carries a rejected message and the feature branch merges it in:
the
range fails without an exclusion, passes with one, and still fails when
the
bad commit is the branch's own.

Mutation check on the new cases: replacing `_commit_messages(args)` with
`_commit_messages(args[:1])`, which is the behaviour before this change,
turns
`test_excluding_the_default_branch_skips_the_history_it_carries` red and
leaves
the other three green. The pin fails when the fix is removed.

Against the real history behind #529, with the checker run directly:

- `check_commit_messages.py "bc74bf73..a066624"` -- exit 1, naming
  `b01534d02` exactly as CI does.
- the same range plus `^github/main` -- exit 0.

The workflow's new loop was run as written against both ranges: 0
commits to
lint on the catch-up range, and a planted `Update stuff.` commit on a
feature
branch still sets the failure flag.

`make lint-python` -- clean. `scripts/check_source_language.py` over the
branch
-- exit 0; the test's non-ASCII message is built from an escape, so the
file
itself stays ASCII.

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

## Risk

Commitlint now starts once per commit instead of once per range, so a PR
with
many commits spends a second or so more in this job. The exclusion only
ever
removes commits that are already on the default branch, so nothing a PR
introduces escapes either linter.

Rollback is reverting this PR; the previous behaviour is one range
argument and
one commitlint invocation.

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

## Related Issues

N/A. Unblocks the `commit messages` check on #529.

---------

Co-authored-by: gloryfromca <23442919+gloryfromca@users.noreply.github.com>
Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>

@gloryfromca gloryfromca left a comment

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.

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

Reviewed the new target-branch merge, its exact recursive-merge tree, the four files changed on both sides, relevant callers and source history, AGENTS.md/CLAUDE.md and Web UI context constraints, backward compatibility, and test integrity. The onboarding/import and DAG follow-up changes integrate without a manual merge deviation or a new cross-history failure; the test count increases despite the intentional retirement of the old DAG sheet tests.

Verification on this head: the affected Python set passes 377 tests; npm test passes 184 files / 2,530 tests; type-check, lint, and production build exit 0 (lint reports five unrelated existing warnings); and uv run python ui-web/build.py matches both boot snapshots. The worktree is clean.

ZuyiZhou and others added 2 commits September 20, 2026 16:08
…533)

## Summary

Raven-Research capped its replies at 30000 characters; every other lane
caps at
60000. The low number was vestigial, carried over from the vendored fork
rather
than chosen, and research writes the longest replies of the five. Raised
to 60000.

The branch briefly carried a run-once config migration (floor 10) that
lifted a
stored 30000 row to the new value on an upgraded install, added in
response to the
review finding that a stored row wins over the discovered manifest. It
was reverted
at the maintainers' request: no release has shipped, so there is no
upgraded install
with such a row, and the manifest change on its own is the whole fix.
The branch
diff against main is the one manifest line. A development config that
registered
the lane before this change keeps its stored `maxOutputChars` until that
row is
edited or removed (`merge_product_seeds` takes the whole stored row over
the
discovered one, by design).

## Type

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

## Verification

Head 5e00464: the manifest change, a merge of main, the migration and
its revert.

```
uv run --frozen --python 3.12 --all-extras pytest -q tests/test_config_loader.py tests/test_agent_schemas.py tests/test_subagent_vendored_agents.py tests/test_subagent_acp.py tests/test_acp_modes.py tests/test_agents_research_flow_ask_user_surface.py
  410 passed in 13.32s

uv run --frozen --python 3.12 --extra dev ruff check raven/config/loader.py tests/test_config_loader.py
  All checks passed!
uv run --frozen --python 3.12 --extra dev ruff format --check raven/config/loader.py tests/test_config_loader.py
  2 files already formatted

PYTHONPATH=. uv run --frozen --python 3.12 --extra dev python scripts/check_source_language.py origin/main...HEAD
  exit 0
make check-commits
  exit 0

git diff --stat origin/main...HEAD
  agents/raven-research/subagent.json | 2 +-
git diff --stat dd8b58d HEAD -- raven/config/loader.py tests/test_config_loader.py
  (empty: the revert leaves both files byte-identical to the pre-migration commit)
```

The type checker was not rerun locally: the revert removes code only,
and CI's
python lint job runs it on the head.

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

## Risk

A research reply between 30000 and 60000 characters now reaches the
caller whole,
costing that turn up to 30000 more characters of context. Rollback is
the one value.
A config that stored the lane's row before this change (development
installs only,
since nothing has shipped) keeps its stored cap until the row is edited
or removed.

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

## Related Issues

N/A

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

@gloryfromca gloryfromca left a comment

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.

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

Reviewed the new 57bb5d886d22..f2c4e5c20519 catch-up, including the seven files changed on both sides of the merge. The result is byte-for-byte Git’s automatic merge tree. I traced the first-run stack rebuild through dispatcher/emitter/deliverable callers and the live-setting changes through the turn boundary, provider pool, curator, skill gate, usage reporting, and settings fixtures; the two lines of work preserve their respective contracts when combined. I also checked the repository rules and Runtime/Web context documents, relevant history and callers, backward compatibility, and test changes; the tests add coverage for the new behavior rather than weakening existing assertions.

Verification: uv run pytest -q over the 14 affected Python test modules passed 641/641; npm test -- --run passed 184 files and 2,530/2,530 tests. The completed GitHub code, lint, page, RPC-contract, trajectory, TUI, bridge, repository, docs, Windows-upgrade, and three completed unit-shard checks are green. CI separately reports the commit-message gate failing on pre-existing commit b01534d02ae1 for a non-ASCII message body, while unit shard 1/4 is still pending; that machine-detected metadata issue is outside this code-review verdict.

@gloryfromca gloryfromca left a comment

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.

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

Reviewed the new f2c4e5c20519..8e1032ff076e catch-up and the two files changed on both sides of the merge. The result is byte-for-byte Git’s automatic merge tree. I traced the ext.list built-in classification through the registry withholding rules, generated wire contract, fixtures, and settings-row mapping; the prior test-stub repair remains compatible now that this path no longer consults schema_hidden_names(). I also followed the anchored model picker and two-step update flow through their callers and retained state.

Covered the repository rules and Runtime/Web context constraints, the revision delta, callers and history, backward compatibility, and test integrity. Existing assertions were updated to the corrected contracts and supplemented with focused regression coverage rather than weakened.

Verification: the affected RPC/schema suite passed 546/546; the web suite passed 186 files and 2,544/2,544 tests; npm run type-check passed; npm run lint passed with the same five existing warnings.

@gloryfromca gloryfromca left a comment

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.

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

Reviewed the new 8e1032f..dd1e8ae merge delta and the full target...HEAD diff under AGENTS.md, CLAUDE.md, and the relevant CONTEXT.md architecture rules. I checked the three files modified on both merge parents, their callers and history, backward compatibility of the A2A/config/loop wiring, the surfaces-law boundary, and whether tests were weakened. The merge tree is byte-identical to Git's clean automatic merge result, and the added tests expand rather than relax coverage.

Verification: the focused A2A/config/loop/integration set passed 692/692; ui-web passed 2,544/2,544; Ruff passed; git diff --check passed; and every hosted PR check is green, including all four Python 3.12 shards. The repository-wide local Python 3.13 run completed with 23,949 passed, 120 skipped, and one failure in tests/test_cli_theme.py::test_bold_accent_renders_styled_not_bare. That test, its theme source, and Rich 14.3.4 are identical on the target, so this is not introduced by this revision. mypy was unavailable locally, while the hosted Python lint job passed. No new findings.

@gloryfromca
gloryfromca merged commit 80ecae6 into refactor/ui_web_architecture Sep 20, 2026
21 checks passed
@gloryfromca
gloryfromca deleted the chore/merge_main_into_ui_web branch September 20, 2026 09:46
gloryfromca added a commit that referenced this pull request Sep 21, 2026
## Summary

Second catch-up of `refactor/ui_web_architecture` with `main`, this time
to
`6ebc2baa` (#592): the 31 commits `main` gained since #529 took it to
`dbe259f6`. Synced main to `6ebc2baa`; the next round starts from that
SHA.

#529 landed as a squash, so git still saw the two branches diverge at
`5a29ea22` and re-raised every conflict #529 had already settled (68
paths
instead of 33). The first commit here merges `dbe259f6` with `-s ours`
--
tree unchanged, ancestry recorded -- so the second commit merges only
what
is new; a third takes #528 and a fifth takes #592 (READMEs only, no
conflict), both of which landed on `main` while the earlier commits were
being verified. The same steps work for every later round.

Thirty-three paths conflicted in the second commit, one in the third;
the
fourth commit is what an independent read of the merge turned up (the
sheet's Connect, `capabilities_wanted`'s fourth case, the dead styles);
the
sixth merges this branch's own tip (#584, #591, #586), where only the
fixture-shape gate's UNSENT list met (both sides kept). How each
conflict
was settled:

- 17 legacy ui-web files (`live/`, `demo/`, `features/xa`,
  `features/knowledge`, `scripts/boot-order.test.mjs`,
  `composer/open-conversation.test.ts`), edited on `main` for #542, #549
  and #554: deleted on this branch, and the deletions stand. `main`'s
  `shell/workdir.ts` and its test, which git relocated into `lib/`, are
  dropped for the same reason -- see the follow-up below.
- `main.tsx`, `page.html`, `state/session/registry.test.ts`: this
branch's
version. `main`'s edits there were the legacy chip wiring; the registry
conflict was a rename git guessed from
`scripts/page-switch-live.test.mjs`.
- `raven/agent/subagent/probe.py`: both imports; Test records a snapshot
  for a preset too (`main`, #554) through `record_capabilities` (this
  branch, #559); a kept record carries `needs_auth`; the boot backfill
  re-measures an outdated model menu (this branch) and a recorded
  credential refusal (`main`).
- `raven/rpc/methods/subagents.py`: `needs_auth` reads the snapshot this
  branch already loads per row.
- `raven/agent/subagent/manager.py` (#528, third commit): both sides
kept
at both hunks -- `main`'s two shutdown refusals beside
`SPAWN_REFUSED_PREFIX`
  with this branch's `_row_pin` after them, and the spawn announce that
types its mark and carries `node_id` (this branch) then returns early
when
  `_inject` was refused (`main`).
- `tests/test_subagent_probe.py`, `test_subagent_acp.py`,
`test_rpc_subagents.py`, `test_rpc_console.py`, `test_config_update.py`:
  both sides' new tests, in order. This branch's two backfill tests pin
  `_unconfigured_acp_preset_rows` the way `main`'s do, and the
`_agent_home` helper both sides defined is one function passing a
`Path`,
  as the config property does.
- `README.md`, `README.zh-CN.md`: `main`'s figure and showcase section.
- `ui-web/src/rpc/generated.ts`, `ui-tui/src/rpc/generated.ts`,
  `ui-tui/src/i18n/messages.generated.ts`: regenerated from the merged
  schema and catalogue.

Carried onto the rebuilt tree, since the backend halves merged cleanly:

- #554: the agent hub's rows, its sheet and the wizard step name a
refused
  agent `Unauthorized` (disabled) instead of offering Connect; the sheet
offers Test beside it, the press that re-measures the handshake and lets
the row return to Connect after a sign-in. `capabilities_wanted` treats
a
recorded refusal as wanting a re-measure, the same fourth case the boot
  backfill gained from `main`. Four tests.
- #542: rail rows carry the session's `workdir`, so the folder group
`RailPage.tsx` gained in the merge can fill. The tag's class moves under
  the rail namespace (`rail-wdt`) with its rule in the new
  `features/rail/styles.css`, as check-class-namespace requires.
- #549: the page-side upload ceiling follows `raven/rpc/files.py` to 100
MB.
- The offline fixtures send `needs_auth`;
`session.list.sessions[].workdir`
is pinned unsent in fixture-shape until the chip lands. The folder
chip's
thirty lines of popover rules that `main` added to `styles/page.css` are
  dropped with the chip; the follow-up brings its own.

Follow-up, not in this PR: the folder chip itself (#542's
`shell/workdir.ts`: the composer chip, the `fs.dirs` browser, `workdir`
on
`session.create`) has to be rebuilt on this tree's chip pattern
(`state/perm.ts` + `chrome/PermChip.tsx`). The backend, the i18n keys
and
the rail half are already here.

## Type

- [ ] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [x] Other (catch-up merge)

## Verification

Run in the worktree on the merged tree:

- `uv run pytest tests/test_subagent_probe.py tests/test_subagent_acp.py
tests/test_rpc_subagents.py tests/test_rpc_console.py
tests/test_config_update.py tests/test_rpc_session.py
tests/test_rpc_contract_shapes.py tests/test_rpc_schema_match.py
tests/test_rpc_registration.py tests/test_i18n_boundary.py
tests/test_subagent_manager.py tests/test_subagent_dag_runner.py
tests/test_rpc_bootstrap.py tests/test_cli_tui_commands.py
tests/test_cli_gateway_commands.py tests/test_rpc_transport.py
tests/test_cli_serve_commands.py tests/test_importer_orchestrator.py
tests/test_cli_gateway_page.py -q` on the final head -- 1893 passed (the
same files, run per commit as the branch grew: 1141, 590, 501).
- `make lint-python` -- ruff check clean; ruff format clean after
formatting the three spliced test files.
- `npm test` in `ui-web` on the final head -- 189 files, 2468 tests
passed (before the sync merge: 193 files, 2609; the first run had failed
two gates, check-class-namespace and fixture-shape, on the merged `.wdt`
class and the three new optional wire fields, both fixed as described
above).
- `npm run type-check`, `npm run lint` (0 errors, 5 pre-existing
warnings), `npm run gen:check` (193 methods) in `ui-web`.
- `npm run lint:i18n`, `npm run lint:rpc`, `npm run type-check`, `npm
test` in `ui-tui` -- all clean.
- `make build-ui` on the final head -- page built, `boot-snapshot: OK`
on both snapshots (235 nodes, the golden #591 set).
- `scripts/check_source_language.py
origin/refactor/ui_web_architecture..HEAD` and
`scripts/check_large_files.py origin/refactor/ui_web_architecture..HEAD`
-- both exit 0; `git merge-tree --write-tree origin/main HEAD` -- clean.

The full Python suite is left to CI.

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

## Risk

No source change of its own beyond the resolutions and the three carries
above. On the page: a refused external agent now shows a disabled
`Unauthorized` button where it showed Connect; conversations pinned to a
folder group under their own rail heading with the folder's name on the
row; uploads up to 100 MB are sent instead of refused at 25 MB. All
three
match what `main` ships today.

This branch is squash-only, so the merge base stays at `5a29ea22` after
this lands; the next round repeats the `-s ours 6ebc2ba` step first.
Rollback is reverting the squash commit; the branch is back at
`99c37e74`.

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

## Related Issues

N/A. Unblocks #475.

---------

Co-authored-by: xfng-sd <xufang@shanda.com>
Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Co-authored-by: Handsome-wzw <68996445+Handsome-wzw@users.noreply.github.com>
Co-authored-by: Dizhan Xue <dizhan.xue@evermind.ai>
Co-authored-by: Kevin Hu <kevinhu.sh@gmail.com>
Co-authored-by: admin <admin@SH-HuKai.local>
Co-authored-by: 江国庆/Forrest <mr.jianggq@163.com>
Co-authored-by: 江国庆 <guoqingjiang@deepglint.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: ypflll <ypflll@163.com>
Co-authored-by: yao pengfei <yaopengfei@shanda.com>
Co-authored-by: zhanghui <huizhang1995@gmail.com>
Co-authored-by: Zuyi Zhou <144661423+ZuyiZhou@users.noreply.github.com>
Co-authored-by: Tong Li <litong02@shanda.com>
Co-authored-by: litong <238663200+TongLi31@users.noreply.github.com>
Co-authored-by: Tchen-data <176354753+Tchen-data@users.noreply.github.com>
Co-authored-by: gloryfromca <23442919+gloryfromca@users.noreply.github.com>
Co-authored-by: Dizhan Xue <xuedizhan17@mails.ucas.ac.cn>
Co-authored-by: silverLXT <25195409+silverLXT@users.noreply.github.com>
Co-authored-by: xiaotian.luo <xiaotian.luo@thetahealth.ai>
Co-authored-by: userName20260323 <zhao.wang@evermind.ai>
Co-authored-by: zhao.wang <270284818+userName20260323@users.noreply.github.com>
Co-authored-by: arelchan <1239372199@qq.com>
Co-authored-by: arelchan <204152633+arelchan@users.noreply.github.com>
LivXue pushed a commit that referenced this pull request Oct 12, 2026
## Summary

A pull request whose base is not the default branch carries, in its
`BASE..HEAD` range, every commit the default branch has gained since the
two
diverged. Both commit linters read that range, so they re-read history
that
already passed this gate when it landed and that nobody can rewrite now.

#529 is the case that surfaced it: merging `main` into
`refactor/ui_web_architecture` puts 38 `main` commits in range, one of
which
(`b01534d0`, #471) carries a `Co-authored-by` trailer with a non-ASCII
name.
The gate reports that PR red for a message it did not write. The same
will
happen to every future catch-up merge.

`scripts/check_commit_messages.py` now takes several revision arguments
and
passes them to `git log`, so a caret exclusion works. Commitlint has no
equivalent flag, so the workflow iterates the same revision list and
lints one
message at a time. On a pull request the workflow excludes the
repository's
default branch; a push to the default branch is unchanged and still
lints what
it pushed.

This does not touch the offending commit on `main` and does not relax a
rule:
a commit that a PR actually introduces is linted exactly as before.

## Type

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

## Verification

`uv run pytest tests/test_check_commit_messages.py
tests/test_commit_lint.py -q`
-- 15 passed. Three of those cases are new and cover a repo where the
default
branch carries a rejected message and the feature branch merges it in:
the
range fails without an exclusion, passes with one, and still fails when
the
bad commit is the branch's own.

Mutation check on the new cases: replacing `_commit_messages(args)` with
`_commit_messages(args[:1])`, which is the behaviour before this change,
turns
`test_excluding_the_default_branch_skips_the_history_it_carries` red and
leaves
the other three green. The pin fails when the fix is removed.

Against the real history behind #529, with the checker run directly:

- `check_commit_messages.py "bc74bf73..a066624"` -- exit 1, naming
  `b01534d02` exactly as CI does.
- the same range plus `^github/main` -- exit 0.

The workflow's new loop was run as written against both ranges: 0
commits to
lint on the catch-up range, and a planted `Update stuff.` commit on a
feature
branch still sets the failure flag.

`make lint-python` -- clean. `scripts/check_source_language.py` over the
branch
-- exit 0; the test's non-ASCII message is built from an escape, so the
file
itself stays ASCII.

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

## Risk

Commitlint now starts once per commit instead of once per range, so a PR
with
many commits spends a second or so more in this job. The exclusion only
ever
removes commits that are already on the default branch, so nothing a PR
introduces escapes either linter.

Rollback is reverting this PR; the previous behaviour is one range
argument and
one commitlint invocation.

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

## Related Issues

N/A. Unblocks the `commit messages` check on #529.

---------

Co-authored-by: gloryfromca <23442919+gloryfromca@users.noreply.github.com>
Co-authored-by: Claude (claude-opus-5) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.