Skip to content

release: prepare Codewhale v0.9.11 - #5542

Merged
Hmbown merged 172 commits into
mainfrom
codex/v0911-nonbenchmark-release-20260821
Aug 23, 2026
Merged

release: prepare Codewhale v0.9.11#5542
Hmbown merged 172 commits into
mainfrom
codex/v0911-nonbenchmark-release-20260821

Conversation

@Hmbown

@Hmbown Hmbown commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Summary

Prepare the non-benchmark Codewhale v0.9.11 release candidate on top of current main.

This branch intentionally excludes benchmarks/pi-agent-parity/** and its release-lane ancestry. PR head b38ecbfa332de285390a245d0de905091e861b86 is byte-for-byte identical to the fully gated local integration commit 98e3814e02f9f01d959c30580c3d07413e0cf4fa outside that path. The branch advanced 959dbe43…accfa93e5… (runtime-contract budget fix) → 491a7121… (SSRF redirect revalidation, duplicate-key refusal, idle MCP ping handling, app-server text) → b38ecbfa… (all Codex P1/P2 review threads).

Included

  • complete v0.9.11 version, lockfile, wrapper, SDK, VS Code, docs, generated-facts, installer, packaging, and changelog surface
  • exact deepseek-v4-flash-vision-exp catalog/aliases, official root/v1/beta endpoint behavior, custom-proxy separation, Runtime/Web capability truth, and Chat text + image_url serialization
  • Fleet member identity separated from Runtime authority, including verified legacy snapshots, deterministic friendly selectors, frozen retry identity, and distinct workflow/runtime depth contracts
  • bounded multi-file read_lints and portable command-contract facets
  • strict MCP initialize/tools/resources/JSON-RPC/schema/bounds/backpressure behavior
  • fail-closed provider completion handling
  • terminal raw-TTY/foreground ownership, control-sequence sanitation, and persistent fish/jellyfish/composer presentation
  • embedded Web UI desktop/mobile, keyboard/focus, truthful provider/vision, attention/recovery, accessibility, and loopback-only behavior
  • release/workflow/install/Windows/OHOS hardening

Fresh CI fix

The first GitHub run on 959dbe438... caught the runtime-contract budget gate: model-facing LSP/Fleet schema prose exceeded the established one-way ceiling. The follow-up commit compacts those descriptions without changing behavior and keeps their exact contract tests. Current measurements pass all 55 ceilings:

  • Plan: 37,796 bytes / 9,449 estimated tokens (111 bytes / 28 tokens below ceiling)
  • Act and Operate: 64,738 bytes / 16,185 estimated tokens (110 bytes / 27 tokens below ceiling)

The fix commit is signed off and changes only:

  • crates/tui/src/tools/lsp.rs
  • crates/tui/src/tools/workflow.rs

Community credit

This candidate preserves the integration ancestry and authorship for:

#5530 is recorded only as independently reviewed/superseded overlap; this PR does not claim it was merged.

Review follow-up

Fresh automated review (Claude full review + Codex review) on the first online candidates found one confirmed security blocker (bundle redirect hops were not revalidated) and nine portable-config/terminal/launch/localization defects. All are fixed on the branch with regression tests, each review thread carries a reply naming the function and tests, and the fixes are summarized in the PR comments.

Exact source candidate

  • PR candidate: b38ecbfa332de285390a245d0de905091e861b86
  • fully tested local integration commit: 98e3814e02f9f01d959c30580c3d07413e0cf4fa
  • PR candidate tree: 456b0e3408c3e2a7260a9e5a47c18263f85161a0
  • base main: 75dca2cfbe1b5e0e9fd58c9eed27dbc1b4705c48

The earlier 23fca25b..., 6824d78d..., 959dbe438..., accfa93e5..., and 491a7121... artifacts/screenshots/receipts are historical only. Final artifact identity, installation, and fresh terminal/Web captures will be recorded from the merged 40-character main SHA; no intermediate artifact is presented as final.

Local validation at the final source tree

PASS:

  • cargo fmt --all -- --check
  • git diff --check
  • version, OHOS dependency, changelog-sync, README translation/locale, product-vocabulary, public-facts, contributor-credit, Web, npm wrapper, Runtime SDK, VS Code, and release-helper checks
  • cargo check --workspace --all-targets --locked
  • cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
  • cargo test --workspace --all-features --locked (13,355 passed, 0 failed at the final tree)
  • scripts/release/publish-crates.sh dry-run (dry-run only; nothing uploaded)
  • runtime-contract budget script and its 17-test checker suite
  • LSP tool-contract tests: 13/13
  • DeepSeek provider-free loopback suite: 18/18 focused checks
  • MCP: 75 focused checks plus 3 CLI proxy checks
  • Fleet/workflow/config/TUI focused matrices, including omitted/zero step budgets remaining unbounded
  • shell completions for Bash, Zsh, Fish, PowerShell, and Elvish: codewhale and codew registered; no codewhale-tui
  • terminal and embedded-Web source/debug dogfood at 1440x900 and 390x844 with zero unexpected console/page/external-network errors

One loaded full-suite run transiently missed a persistent-service PID fixture. The failing scenario then passed alone, the complete three-scenario module passed three consecutive times, and a fresh exact full-workspace rerun passed. This receipt is retained rather than hidden.

No paid provider turn was sent. The GLM-5.3 continuation remains intentionally unsent because no explicit all-in USD cap and provider-side hard quota were supplied.

CI and merge gate

Fresh checks are running on b38ecbfa.... The protected-main ruleset requires Lint, Ubuntu, macOS, Windows, Version drift, and npm wrapper smoke. GitGuardian is informational/non-required here and is reporting the same historical synthetic redaction-test fixtures across the branch history, not a new credential or occurrence in the current two-file change; no GitGuardian incident state has been mutated.

The PR will not be merged until the fresh required checks and review are complete and every non-exempt failure has a precise disposition.

No tag, GitHub Release, crates/npm/container publication, deployment, DNS change, provider canary, or production mutation occurred.

No-Issue: maintainer-owned v0.9.11 release integration and packaging.

bistack and others added 30 commits August 20, 2026 15:36
…mand contract

- CommandPresentationContext: stable-key translation with named replacements (D3)
- CommandMediaContext + MediaAttachmentReceipt: atomic composer/media attach (D4)
- CommandWorkspaceContext::operation_digest: session-aware canonical digest (D5)
- Envelope: presentation + media optional slots with duplicate-slot asserts (D7)
- Contract tests: object safety, transport, translation failures, media atomicity, digest, duplicate-slot rejection (9 total)

Generated with Claude Code
…(D3)

Review fix: unknown translation key now fails with a generic safe error in the
contract test double and the assertion verifies the raw key is not echoed.

Generated with Claude Code
- PresentationAdapter: stable-key translation with named replacement validation and English fallback (D3)
- MediaAdapter: atomic media validation + composer insertion, portable receipt (D4)
- WorkspaceAdapter::operation_digest: session-aware canonical digest with no-active/failure semantics (D5)
- Bundle carries presentation + media adapters; construction performs no eager work (D7)
- 16 contract adapter tests green; boundary + migration gates PASS

Generated with Claude Code
Lib build flagged unused Locale import; moved to the cfg(test) module where it is used.

Generated with Claude Code
Expose bounded, workspace-relative LSP diagnostics for multiple existing files through the model-visible lsp tool. Reuse the shared transport pool, fail clearly when LSP is unavailable, and preserve the frozen tool catalog budget for #4070.
…ct registration

- /automation, /mcp: Contextual handlers consuming presentation facet for localized text (D3/D6)
- /attach: Contextual handler resolving paths via workspace facet + atomic media facet (D4/D6)
- /task: Contextual handler with workspace operation_digest for digest (D5/D6)
- /jobs, /network, /update: Pure argument-only handlers (D6)
- /network uses codewhale_config leaf APIs + reqwest::Url host parse; no TUI persistence/network_policy helpers
- mod.rs bridges all seven via ContextualCommand::from_contract; FunctionCommand removed from utility
- FEAT-015 transitional test updated: utility excluded from legacy-only assertion
- 35 utility tests + full TUI lib 10857/0 green

Generated with Claude Code
…ity dispatch

- utility removed from scripts/command-migration-topology.json frontier (topology scope immutable)
- utility removed from PENDING_GROUPS TUI projection
- Live migration gate PASS: frontier [config,core,debug,memory,plugins,project,session,skills] exact source correspondence
- Public dispatch tests: 7-entry portable inventory, pure /jobs+/update, contextual /automation+/task+/mcp+/attach+/network through public seam (Task 6.2)
- FEAT-015 transitional dead-code allowances removed (ContextualCommand legacy/command_handler/is_legacy); PENDING_GROUPS/parts get targeted test/gate allows
- Migration fixture updated to shrunk 8-group frontier; 54/54 fixtures + CI wiring 11/11 green

Generated with Claude Code
…work normalization

- MediaAdapter: video-path attach test (extension-gated, composer reference preserved)
- Network: wildcard/trailing-dot/case host normalization, URL-path rejection, hostless URL rejection, exact conflict removal tests

Generated with Claude Code
…d duration assertion)

run.metrics.duration.as_millis() > 0 flaked under saturated serial test runs:
the offline simulated loop performs real fs/tempdir work that can complete
sub-millisecond, so the millisecond threshold intermittently measured 0ms.
Aligned granularity with the per-tool assertion (as_nanos() > 0), which is
deterministic. Pre-existing on origin/main; repaired under the Boy Scout Rule.

Generated with Claude Code
Every nightly since 2026-08-16 failed the windows-arm64 leg while compiling
codewhale-tui, with `thread 'optimize module codewhale_tui.551c4fd52ebe22df
-cgu.13' has overflowed its stack` — the same codegen unit on all three build
attempts, so this was never flaky.

The stack belongs to the LLVM worker threads rustc spawns to run per-codegen-
unit optimization, not to rustc's main thread. `lto=off` is why that work runs
while the *library* is compiled instead of being deferred to the ThinLTO stage,
but it is not the root cause: removing the override would only relocate the
optimization and hide the failure.

Evidence (local, aarch64-apple-darwin, crate and every flag held fixed, only
RUST_MIN_STACK varied, CARGO_PROFILE_RELEASE_LTO=off):
  default (2 MiB) -> builds, 2m53s
  1 MiB           -> rustc dies, SIGBUS
  2 MiB           -> builds
  4 MiB           -> builds
So the requirement sits between 1 and 2 MiB. Unix std defaults to 2 MiB and
passes; the Windows ARM64 runner sat under it.

That requirement follows from the size of the crate — crates/tui is 788k lines
and the regression window added 24.7k of them — not from the operating system,
so RUST_MIN_STACK is set for the whole matrix rather than special-cased. The
value is reserved address space, not committed memory. Splitting the crate's
largest modules is the durable fix and is tracked separately.

Also drops CARGO_PROFILE_RELEASE_CODEGEN_UNITS: [profile.release] already sets
codegen-units = 16, so the override restated the existing value and never
provoked anything. Believing it did is what pointed the first diagnosis at the
wrong flag.

Shipped binaries were never affected: release-artifacts.yml builds
--profile dist with fat LTO and codegen-units = 1.

Verified locally: actionlint 1.7.12 clean, YAML parses, Build step env asserted.
The windows-arm64 nightly turning green is CI-only and is not proven here.

Diagnosis and patch produced with agent assistance.

Signed-off-by: Hunter Bown <hmbown@gmail.com>
…kspace

Ordinary workspace-write turns built their sandbox with
`network_access: true`, so agreeing to let a session edit this repository also
handed every shell command unrestricted outbound egress.

#273 introduced that grant, and justified it: the seatbelt default denies DNS,
which broke curl, yt-dlp, and package managers, and the comment argued the
application-level NetworkPolicy would remain "the only outbound boundary".
The second half was not true. NetworkPolicy governs fetch_url, web_search, and
MCP HTTP; it never saw a shell subprocess. So the layer meant to compensate for
the wide OS policy did not cover the thing the OS policy had opened, and
workspace-write sessions ran with no outbound boundary at any layer. This is
not tightening a working boundary — it is installing one that was missing.

Workspace-write is now created network-restricted. Egress comes from exactly
three explicit places:

  - `sandbox_network_access` in config (env: CODEWHALE_SANDBOX_NETWORK_ACCESS),
  - a `danger-full-access` posture, which applies no sandbox at all,
  - the existing post-denial elevation prompt, which grants network for one
    call after the user sees what was blocked.

Yolo and --yolo/Bypass are unchanged: they resolve to DangerFullAccess, so
their deliberate "no guardrails" contract still reports network. Plan stays
ReadOnly. Writable roots, tmpdir handling, and the git-worktree metadata roots
are untouched — only the network bit moved.

The decision is carried by a typed `SandboxNetworkAccess` rather than another
bool, so the default is stated once at the type instead of at each of the eight
resolver call sites, and "the user asked for network" cannot be transposed with
"some caller passed true".

Two surfaces were lying and now read the flag: `external-sandbox` hardcoded
`network_access: true` even when nothing granted it, and /status printed
"sandbox workspace-write, network on" for every workspace-write policy — true
only by accident of the old default.

Tests. The #273 regression test is retargeted rather than deleted: it now pins
that Agent mode still elevates *writes* while withholding network, which is the
property #273 actually needed. Added coverage for the full posture matrix
(Agent/Ask/Auto-Review/Never x configured overrides, plus Yolo and Plan), the
config key and its camelCase alias, and — the gap the audit surfaced — that the
generated seatbelt profile emits network rules if and only if the policy grants
them, so the OS layer and the application policy are verified to agree instead
of one being assumed to compensate for the other.

Verified: cargo test -p codewhale-tui --lib -> 10850 passed, 0 failed,
13 ignored (RUST_MIN_STACK=16777216, as scripts/dev-test.sh exports).
cargo fmt --all clean.

Platforms with no sandbox backend (default Linux without bubblewrap, and
Windows) still enforce nothing either way; /status and doctor continue to say
so, and that honesty gap is unchanged by this commit.

Implemented with agent assistance.

Signed-off-by: Hunter Bown <hmbown@gmail.com>
…nts' law

Two problems, one subsystem.

**Another tool's instruction file was standing authority here.** The canonical
list ranked `.claude/instructions.md` second and `CLAUDE.md` third — above
Codewhale's own `.codewhale/instructions.md` at fourth — `.claude/rules/` was an
auto-discovered rules directory, and `.cursorrules`, `.cursor/rules`,
`.clinerules`, `.windsurf/rules`, `.gemini`, `.github/copilot-instructions.md`
and `.github/muse-instructions.md` were imported into the system prompt with no
opt-in at all. Dropping a `CLAUDE.md` written as law for a different agent into
a repository silently made it law for this one, and outranked the file this
project actually owns. That is an injection surface, not a compatibility
feature.

Codewhale now reads `AGENTS.md`, the cross-agent `.agents/AGENTS.md`, and its
own instruction files by default. Every other agent's format is opt-in by name
through `project_instruction_imports` (env
`CODEWHALE_PROJECT_INSTRUCTION_IMPORTS`), imported files rank *below*
Codewhale's own rather than above them, and a workspace containing an
un-imported format produces a warning naming the exact setting — so this is
discoverable rather than a silent behavior loss. Unknown names in the key are
reported instead of dropped, because a typo otherwise means "import nothing"
and nobody would notice.

**A symlinked candidate directory could read outside the workspace.**
`collect_candidate_files` checked every *file* it found for symlinks but
reached them through `path.is_dir()`, which follows links. A symlinked
`.cursor/rules` pointing anywhere on disk was therefore traversed, and the real
files behind it passed every per-entry check. `project_context.rs` has refused
symlinked rules directories since it gained them, with a comment explaining
this precise escape; `fragments.rs` never got the same guard. Now both loaders
apply it. The regression test fails without the fix and passes with it — I
checked, rather than assuming.

**One budget instead of four.** The chain had 200 KiB, the rules block 500 KiB,
imported fragments 40 KiB, and the global fallback layer was merged in *after*
the chain budget had already closed, so it counted against nothing. No single
number described how much standing instruction text could precede the
conversation. All of it now shares one 48 KiB aggregate ceiling, applied once
after assembly. Instructions claim it before rules, and are trimmed from the
front — dropping the broadest scope first — so the nearest-scope file is the
last thing dropped instead of the first thing stranded, which is what the old
root-first per-segment accounting did. Truncation still leaves an explicit
marker.

The opt-in set is threaded as a parameter rather than read from a global inside
the loader, so callers and tests state which formats are in play instead of
racing on process-wide state.

Preserved: nearest-scope traversal, repository-root stopping, the $HOME clamp,
`O_NOFOLLOW` on unix, truncation markers, `<project_instructions source=…>`
provenance, and the existing duplicate-suppression between the two loaders.

Verified: cargo test -p codewhale-tui --lib -> 10854 passed, 0 failed,
13 ignored. cargo test -p codewhale-core -> 80 passed, 0 failed.
cargo fmt --all clean.

Note for the release notes: this is a behavior change. A repository whose only
instructions live in CLAUDE.md will stop contributing them until
`project_instruction_imports = ["claude"]` is set. The warning names the key.

Implemented with agent assistance.

Signed-off-by: Hunter Bown <hmbown@gmail.com>
Regenerate the committed website facts after exposing read_lints through the existing lsp tool.
At 80 columns the Japanese provider screen rendered

  モデルの実行先を選びます。ホステッドプロバイダーにはキーが必要ですが、ローカ

and stopped. The sentence continues "ルランタイムはキーなしで続行できます。" —
local runtimes continue without a key — which is the reassurance that screen
exists to deliver, on the one screen where the user is deciding whether they
need an API key. It was not wrapped to a second line; it was written past the
right edge and clipped by the terminal.

`wrap_words` iterates `text.split_whitespace()`. Japanese, Chinese, and Thai do
not delimit words, so the whole sentence is a single token. The wrap check is
`!current.is_empty() && needed > width`, which cannot fire for the first token
on a line, so the token was appended whole however wide it was. The function's
doc comment asserted the opposite: "no paragraph re-wrap can clip a locale with
longer sentences."

A token wider than the lane is now broken by display width on grapheme
clusters, which is the conventional wrap for those scripts. Light kinsoku comes
with it: a line may not begin with closing punctuation or a sentence-final mark
(`。 、 」 』 ) ! ? ー` and the Latin equivalents), so the break pulls one
cluster back rather than orphaning it. Grapheme clustering means Devanagari and
emoji sequences are never split mid-cluster either.

Languages that do use spaces keep their existing behaviour — the new branch only
runs for a token that cannot fit on any line.

Found by driving first-run onboarding through a PTY in all fifteen shipped
locales at 120x32 and 80x24 and reading the reconstructed frames. Korean,
Hindi, Russian, German, French, Spanish, Portuguese, Catalan, Indonesian,
Vietnamese and both Chinese packs already wrapped correctly; Japanese was the
locale whose translation was long enough to cross the lane at 80 columns.

Verified: cargo test -p codewhale-tui --lib -> 10858 passed, 0 failed,
13 ignored. The wrap regression test fails with the new branch disabled and
passes with it. Re-driving the real binary at 80x24 shows both lines.

Implemented with agent assistance.

Signed-off-by: Hunter Bown <hmbown@gmail.com>
…remains

`crates/core/src/engine/` was 526 lines describing a runtime that does not
exist. `Engine::run` accepted `Op::SendMessage`, appended the content to a
journal, and emitted `TurnComplete { status: "completed" }` — no provider
request, no tools, no streaming. `TurnExecutor`, which `docs/ARCHITECTURE.md`
pointed at as the owner of turn orchestration, was a field-copy constructor and
`step < self.max_steps`; its `exec_policy` field was stored and never read.

Nothing consumed it. The only occurrence of `codewhale_core::engine` in the
entire workspace was a doc comment inside the tree describing how the next
migration slice would use it, and `crates/core/src/lib.rs` declared the module
without re-exporting `executor`. `cargo check --workspace` passes unchanged
after removal.

Leaving it in place was not neutral. Its comments were the source of the claim
that `crates/core` owns the agent loop, which is false: the live loop is
`Engine::run_turn` in `crates/tui/src/core/engine/turn_loop.rs`, and
`crates/tui/src/core/` is a module inside the TUI crate, not this crate. That
confusion is exactly the kind a stalled migration leaves behind, and it made an
"is the runtime already migrated?" question un-answerable by reading.

Deleting it is also the part of the core-ownership migration that is honestly
achievable now, and it delivers the property that mattered: there is exactly one
turn loop in the workspace, enforced by a guard rather than asserted in prose.
`crates/core/tests/single_turn_loop.rs` scans the workspace, requires exactly
one `run_turn`, requires it to be the one in `turn_loop.rs`, and fails if
`crates/core/src/engine/` reappears. It is a source scan on purpose: a second
implementation would not be reachable from the first, so no type-level check
could see it.

The full crate hoist stays out of this release. Two hard blockers make it
unsafe to attempt now, and both should be recorded rather than rediscovered:
`crates/command-contract`'s only dependency is `codewhale-core`, so moving
TUI modules that need it into core is a cargo cycle; and `Event::AgentList`
declares `roster: Vec<crate::tui::agent_roster::AgentRosterRow>`, so core's
public event enum would name a TUI type the moment `events.rs` moved.

`docs/ARCHITECTURE.md` now describes what `crates/core` actually is, notes that
`app-server --http`/`--mobile` delegate to the TUI binary, and `AGENTS.md`
records the one-loop rule next to the other current contracts.

Verified: cargo check --workspace clean; cargo test -p codewhale-core ->
76 + 2 + 2 passed, 0 failed. cargo fmt --all clean.

Implemented with agent assistance.

Signed-off-by: Hunter Bown <hmbown@gmail.com>
…equest

`/prompt`, `prompt/request` and `prompt/run` returned HTTP 200 with a
`PromptResponse` for work that never happened. `Runtime::handle_prompt`
contacted no model: it resolved config, ran a local `ModelRegistry`
lookup, emitted three canned hook events (the `ResponseDelta` payload was
the literal string `model-selected`), and set `output` to a stringified
JSON echo of the caller's own routing metadata — provider, model,
telemetry flags, and the prompt itself. A client could not tell that
apart from a real answer, which is the whole problem: a benchmark
harness, an SDK, or a person reading the response all get a success and
a plausible-looking body for a model call that did not occur.

The durable damage was worse. With a `thread_id`, `handle_prompt` called
`touch_message` (appending a genuine user row and flipping the thread to
`Running`), then wrote that same echo into history as an **assistant**
message and saved a `prompt_response` checkpoint pointing at it. Nothing
marked the row synthetic, and nothing ever moved the thread out of
`Running`. Every prompt permanently poisoned the transcript it touched.

The real path already existed one file over: `RuntimeBridge::message_thread`
POSTs to `/v1/threads/{id}/turns` and streams the turn's SSE events, and
stdio `thread/message` has used it all along. So there was no engine to
build — only a fake to delete and a route to point at the engine. All
four prompt surfaces now go through one `run_bridged_turn`, and
`Runtime::handle_prompt` is gone along with `persist_latest_checkpoint`,
which existed only to record its fabrication.

HTTP `POST /thread` with a `Message` body had the same shape of lie in a
milder form: `status: "accepted"` and a `ResponseDelta("queued")` with no
worker started and no bridge call. The stdio path for the identical
request did real work, so the two transports disagreed about what
`accepted` meant. HTTP now runs the turn and replies `completed` with the
streamed frames. `Runtime::handle_thread` no longer accepts
`ThreadRequest::Message` at all — it owns thread bookkeeping, not the turn
engine — and says so, naming `POST /v1/threads/{id}/turns`.

Failure is now typed rather than success-shaped. `POST /prompt` returns
`{"error":{...}}` with 400/404/503/500 instead of HTTP 500 carrying a
`PromptResponse` whose `output` held the error text, and stdio gained
`-32005 runtime_unavailable` for "the turn engine could not be reached,
so nothing ran" — retryable, and distinct from `-32603`. There is no
configuration in which a prompt quietly echoes: if no runtime can be
reached, the caller is told.

`prompt_handler` also no longer holds the `Runtime` write lock across the
request. It held it for the entire body while doing no model work.

Verified: `cargo test -p codewhale-app-server --lib` (83 passed),
`cargo test -p codewhale-core --lib` (76 passed),
`RUSTFLAGS=-Dwarnings cargo clippy` clean on both crates, and
`cargo check --workspace --all-targets` clean. The four new app-server
tests and the rewritten core test were each confirmed to fail against a
temporarily restored pre-fix implementation.

Contract changes are declared in CHANGELOG.md.

Implemented with agent assistance.

Signed-off-by: Hunter Bown <hmbown@gmail.com>
…swers

`AppRequest::SubmitUserInput` returned `ok: true` with
`resolved: true` and stored the caller's answers in
`state.pending_user_input`. That map had no read site anywhere in the
crate — declared, initialised, written, never read. Every clarification
answer a client submitted was acknowledged as resolved and then dropped.
The doc comment called an in-flight resume "a follow-up", which read as
a partial implementation rather than what it was: a reply path that
cannot deliver anything.

It cannot be completed on this transport, and that is a structural fact,
not a missing feature. `handle_line_during_turn` executes exactly one
method while a turn is streaming — `thread/interrupt`. Everything else,
`app/request` included, hits the catch-all and queues until the turn
ends. So an answer sent over this transport waits for the turn, and a
turn blocked on that answer waits for the answer. Building the resume
awaiter would have produced a deadlock, not a feature.

The runtime API already has the surface that works:
`POST /v1/user-input/{thread_id}/{request_id}`, which owns the pending
request and can resume the turn that raised it. So this now returns
`ok: false` with `error: "user_input_reply_unsupported"` and a message
naming that endpoint and the reason. `pending_user_input` and its
type alias are deleted with it.

The minting side is untouched and was never fake: `Runtime::invoke_tool`
still emits a genuine `EventFrame::UserInputRequest` with the model's
real questions and returns `user_input_required`. Only its documented
reply path was a lie, and the comment there now points at the runtime
API instead of at this crate.

Verified: `cargo test -p codewhale-app-server --lib` (84 passed),
`cargo test -p codewhale-core --lib` (76 passed),
`RUSTFLAGS=-Dwarnings cargo clippy` clean on both crates,
`cargo check --workspace --all-targets` clean. The new test was confirmed
to fail against a temporarily restored `resolved: true` implementation.

Implemented with agent assistance.

Signed-off-by: Hunter Bown <hmbown@gmail.com>
`check-versions.sh` compares `crates/tui/CHANGELOG.md` against its slice of the
root `CHANGELOG.md` and fails on any divergence. The placeholder-engine removal
was recorded in the root file only, because the change lives in `crates/core`
rather than the tui crate — but the gate compares files, not subject matter.

Generated with ./scripts/sync-changelog.sh.

Verified: ./scripts/release/check-versions.sh exits 0 on this branch.

Signed-off-by: Hunter Bown <hmbown@gmail.com>
Mirrors the fix landed on main (75dca2c): the release-workflow contract test
asserts nightly.yml never matches /codewhale-tui/, and the stack-size comment
named the crate. Reworded to crates/tui.

Verified: full ci.yml versions job (check-versions.sh + 13 release-helper
contract tests) passes locally on this branch.

Signed-off-by: Hunter Bown <hmbown@gmail.com>
AGENTS.md says the model-facing sub-agent surface is `agent` only. It was
not. `agents/list`, `agents/message`, `agents/followup`, `agents/interrupt`,
`agents/coordinate`, and `agents/wait` never overrode `ToolSpec::
model_visible`, which defaults to true, so all six cleared the filter in
`ToolRegistry::build_api_tools` and entered the model catalog. Being deferred
did not hide them — deferral makes a tool discoverable via `tool_search`, and
both matchers read the same catalog. The `agent` description then named five
of them outright ("the narrow agents/list, agents/message, … tools expose the
same semantics directly"), so the surface actively taught the second
transport it claimed not to have.

The fix follows the precedent already in this tree: `rlm` and `exec_shell`
return false from `model_visible`, staying registered and executable by name
so a persisted transcript replays against the same implementation while never
being advertised. All six now do the same.

Five were pure duplicates of an `agent` action. `agents/coordinate` was not.
Its `claim` action is the only path to `SubAgentManager::expand_write_claim`,
and write-scope enforcement fails closed — so hiding it without a replacement
would have left the refusal message ("expand it first with agents/coordinate
action=claim") pointing at a tool the model can no longer call, with no way to
proceed. `agent` therefore gains exactly one action, `claim`, and no more:
propose/accept/supersede/reconcile/inspect stay off the model surface because
nothing fails closed without them.

`claim` reuses the write-scope vocabulary `action=start` already speaks —
`write_roots` advertised, `exact_files` and `coordination_contracts`
parse-accepted — so one set of names describes a child's scope whether it is
declared at launch or widened later. The translation to the coordinate wire is
the load-bearing part and has its own documented function, because both ways
of getting it wrong fail silently: the wire key is `roots`, and forwarding
`write_roots` hands `expand_write_claim` three empty lists, which returns the
unchanged claim with `Ok` — a success receipt for an expansion that never
happened. The same no-op-success is why a scopeless claim is refused outright
rather than passed through. Approval stays `Auto`, inheriting
`agents/coordinate`'s rationale: gating a coordination record deadlocks
autonomous fan-in, and `claim` can only widen the caller's own scope.

Per-role gating had to be solved explicitly, and this is the part worth
reviewing. Every capability gate here keys off a tool *name*, which is exactly
what breaks when six tools become one, and `agent` is deliberately exempt from
both: `posture_permits_tool` short-circuits it so delegation depth rather than
write posture governs spawning, and `execution_envelope::is_delegation_tool`
classifies it `Bounded` so a read-only member can still fan out read-only
work. A capability folded into `agent` therefore inherits no gate at all.
`agent_action_permitted` supplies one per action, reproducing the check the
retired tool actually had — `agents/coordinate` declared `WritesFiles` and was
kept off a read-only role's catalog by `envelope.write` — rather than
inventing a new policy. It is applied when shaping the catalog and again at
dispatch, because catalog shaping has never been the authority boundary here.
The other seven actions keep exactly today's visibility; narrowing
message/followup/interrupt for read-only roles would be an unrelated behavior
change smuggled in behind a catalog cleanup. Making `agent` a
`CANONICAL_ACTION_ALIASES` family would have reused the existing action-policy
seam, but `canonical_action_alias` feeds `execution_envelope`, where the
`agent` name is what earns the `Bounded` reclassification — so that route
would have made `claim` demand write *and* shell authority.

Contract changes, declared: the six tools leave the model catalog (wire
shapes, schemas, and dispatch-by-name are unchanged); `agent` gains the
`claim` action and its enum grows by one; the out-of-scope write refusal and
the child's write-scope briefing now name `agent action=claim`; the frozen
per-role surfaces drop the `agents/*` entries.

Tests: the catalog test that asserted `agent` appears once now also asserts
none of the six appear and that each stays registered and model-invisible;
`tool_search` is exercised on both the regex and bm25 paths with queries
aimed at the retired names and their descriptions; the `agent` description and
schema are asserted to name none of them; the claim path is proven end to end
by writing a file that was refused before the claim and admitted after it,
which is what makes the wrong wire key fail here instead of in production.
Every new test was confirmed to fail with its fix reverted.

Implemented with agent assistance.

Signed-off-by: Hunter Bown <hmbown@gmail.com>
`Message.role` was a `String`, so "assistant" and "assistnat" were both
valid transcripts and only a provider could tell them apart. Worse, the
four wire adapters each answered "what is an unfamiliar role?" differently
by accident: Chat Completions and Responses dropped it silently, Anthropic
forwarded it verbatim for the provider to 400 on, and cloud-code bailed.
There was no single place to state the answer because there was no type to
hang it on.

`crates/core/src/role.rs` adds that type. It is closed over the roles this
build actually mints — user, assistant, system, and the
`assistant_interrupted` sentinel — plus `Unrecognized(String)` for
everything else. There is deliberately no `Tool` variant: nothing on the
neutral side constructs one, and inventing a variant for a producer that
does not exist would just move the guessing into the type.

`Unrecognized` is not a shrug, it is the compatibility contract. `Role`
serializes through `as_str()` and deserializes from any string, so a
persisted transcript's bytes are identical before and after this change.
That matters concretely: `Message` derives Serialize/Deserialize and every
saved session holds these strings, while `session_manager` refuses a
session whose `schema_version` exceeds CURRENT and has no migration ladder
back down. A forward-incompatible role encoding would have stranded anyone
who downgraded. Two tests pin it — a per-role byte comparison against the
raw string encoding, and a whole-transcript load/re-save that must not
change a single byte, including an unknown "developer" role.

`assistant_interrupted` stays its own variant rather than a flag on
Assistant, so it keeps round-tripping as a distinct session item as
`models.rs` already asserted.

`PartialEq<&str>`/`PartialEq<String>` in both directions keep the ~40
read-only string comparisons across the tree compiling unchanged; only
construction sites moved. Fleet roles (`crates/tui/src/fleet/`,
`crates/workflow/`, the runtime worker-job APIs) are a different concept
and are untouched.

No behaviour change: this commit is types and construction sites only. The
adapters still each decide placement for themselves; that is the next
commit.

Implemented with agent assistance.
Signed-off-by: Hunter Bown <hmbown@gmail.com>
Avoid counting the private diagnostics helper as a second model-visible tool and refresh generated web facts back to the real 75-tool catalog.
`tools/subagent/coord.rs` was 3.8k lines holding two things that have almost
nothing to do with each other: the narrow `agents/*` tool wrappers, and the
durable coordination records — decisions, write-scope claims, contention and
reconciliation receipts, the projection types — that those wrappers happen to
read and write. The split is not arbitrary: the records have consumers outside
the tool layer entirely (`tui::coordination_detail`, `tui::work_surface`,
`tui::ui::tests`, `core::engine::tests` all name them), while the wrappers are
model-surface code with no consumer but the registry. Anyone reading either
half started by scrolling past the other.

`ledger.rs` now owns the records and their unit tests; `coord.rs` keeps the
tools. `coord` re-exports the ledger, so every existing
`crate::tools::subagent::coord::{…}` path still resolves and not one consumer
file was edited — which is the property that makes this reviewable as a move
rather than a refactor.

The re-export is a glob rather than an explicit list, deliberately. Several of
these types are named only from `cfg(test)` code in other modules, so an
explicit `pub use` of them reads as an unused import in a release build under
`-D warnings`; the glob also preserves each item's own visibility, keeping
`MAX_RECONCILIATION_RETRIES` reachable inside `coord` without promoting it to
the module's public surface.

Verified as a content-pure move: every chunk of the pre-split file is present
verbatim in exactly one of the two files, modulo three adjustments the move
itself forces — the `use` blocks split between the files, `coord`'s test module
relocating to the end of the file (an item may not follow a test module), and
one constant going from private to `pub(super)` because its only caller,
`AgentsCoordinateTool`, stayed behind. The lib suite reports the same 10864
passing tests before and after, so nothing was dropped in transit.

Implemented with agent assistance.

Signed-off-by: Hunter Bown <hmbown@gmail.com>
…e seam

Four adapters each answered "where does this role go, and what if I do not
recognise it?" and the four answers had drifted apart:

  * Chat Completions matched user/assistant/system in an if/else-if chain
    with no final else, so anything else fell off the end silently.
  * OpenAI Responses matched user/assistant/tool and swallowed system in a
    catch-all `_ => {}`. Its "tool" arm was dead: nothing on the neutral
    side has ever constructed that role, and the user arm already renders
    ToolResult blocks as function_call_output.
  * Anthropic Messages forwarded message.role verbatim, remapping only the
    interrupted sentinel. A system message — a compaction or branch summary,
    which session_tree mints routinely — therefore went out as
    {"role":"system"} for the provider to reject with a 400 naming neither
    the role nor the message.
  * Google cloud-code was the only one that failed closed.

`client/role_placement.rs` is now the single table. Given a Role and a
WireDialect it returns which channel carries the message, that the message
is omitted, or that the pair is rejected. Adapters still own the structural
rendering for their own dialect — Chat's tool_calls array, Responses'
function_call_output items, Anthropic's content blocks, cloud-code's parts —
but none of them chooses a channel any more.

`reject_unsupported_roles` runs at the top of prepare_outbound_request, the
one outbound seam, before any dialect builds a body. That is deliberate: a
pair that cannot be represented should die in this process with the message
index, the role, and the wire named, not as a provider error that arrives
with none of that.

Two behaviour changes, stated plainly rather than smuggled in:

1. On Anthropic Messages, an in-transcript system role and any unrecognised
   role are now rejected locally. Both were already broken — verbatim
   pass-through meant a guaranteed provider 400 — so nothing that used to
   succeed now fails; the failure just becomes legible. The alternative,
   dropping them like the OpenAI-shaped dialects do, was rejected: a
   dropped system message is a dropped compaction summary, i.e. silently
   sending a truncated history. Fail closed.
2. The dead "tool" arm in the Responses adapter is deleted, and cloud-code
   no longer special-cases a literal "model" role. Nothing constructs
   either.

Everything else is preserved on purpose. Responses keeps dropping
in-transcript system messages (live sessions are on that path today and
rejecting would break them), Chat and Responses keep dropping unknown
roles, and cloud-code keeps bailing on everything it cannot represent.
Placement grants no authority: it says which channel carries the bytes,
never how much the model should trust them.

Verified: the three tests that pin the changed behaviour
(anthropic_never_emits_a_role_outside_user_and_assistant,
seam_refuses_an_in_transcript_system_message_on_anthropic,
seam_refuses_the_interrupted_sentinel_on_cloud_code) were each confirmed to
fail with the Anthropic pass-through and the seam call temporarily
restored, and to pass with them in place. The remaining new tests are
characterization of preserved behaviour and pass either way, as intended.
Full `cargo test -p codewhale-tui --lib`: 10875 passed, 0 failed.

Implemented with agent assistance.
Signed-off-by: Hunter Bown <hmbown@gmail.com>
…ing them

The cloud-code arm caught every non-user, non-assistant placement with `_`,
which quietly re-opened the problem the closed enum exists to solve: adding
a placement variant would have made it a hard error on this wire without
anyone deciding that it should be. Naming the four rejected variants makes
the compiler ask.

No behaviour change — the same set of placements bails with the same
message.

Implemented with agent assistance.
Signed-off-by: Hunter Bown <hmbown@gmail.com>
# Conflicts:
#	CHANGELOG.md
#	crates/tui/CHANGELOG.md
# Conflicts:
#	CHANGELOG.md
#	crates/tui/CHANGELOG.md
The idle screen is a centred composition: whale, wordmark, then "What do you
want to accomplish?". Between the wordmark and the prompt sat the workspace
caption, and at 80 columns it rendered like this:

                                   Codewhale
  /private/tmp/claude-501/-Volumes-VIXinSSD-CW-codewhale/34267917-11f4-4d15-911a-…
                        What do you want to accomplish?

Full-bleed, flush-left, straight through the middle of the composition, ending
in an ellipsis mid-directory.

The centring was never wrong. The caption was built at full length, handed to
`truncate_to_width(&context, width)`, and only then measured:

    let context = truncate_to_width(&context, width);
    let inset = " ".repeat(width.saturating_sub(context.width()) / 2);

After truncation `context.width() == width`, so the inset is always zero. The
centring silently degraded into left-alignment exactly when the string was long
— which is always, for a real absolute path. Two lines apart, one defeats the
other.

Clipping also destroyed the only thing the line exists to say. A path cut at
`34267917-11f4-4d15-911a-` names no directory; the reader learns nothing while
the line consumes the full width and the strongest position on the screen.

So the caption now sheds detail rather than getting cut. In order: the MCP
count goes first, then the branch, then leading path components. The folder you
are standing in is the last thing to go, because it is the only part anyone
reads here. Elisions land on a separator, so `…/surface/ws2` reads as "somewhere
above here" instead of as a severed token. `shorten_workspace` declines to
elide when it would not actually save width — `~/code/app` stays as it is
rather than becoming `…/code/app`, which is the same length and throws away the
`~`.

The same line at 80 columns now:

                                   Codewhale
                             …/surface/ws2 · no git
                        What do you want to accomplish?

Found by driving the real binary in a PTY and reading the reconstructed frame,
not by reading the code.

Verified: cargo test -p codewhale-tui --lib -> 10863 passed, 0 failed,
13 ignored. Five new tests cover the centring invariant (the inset must be
non-zero at 60/80/100/120 columns), the shed order, that the current folder
survives every width, and that elision lands on a separator. cargo fmt clean;
the ci.yml versions job passes locally.

Implemented with agent assistance.

Signed-off-by: Hunter Bown <hmbown@gmail.com>
@Hmbown
Hmbown force-pushed the codex/v0911-nonbenchmark-release-20260821 branch from 5c1aabb to add169e Compare August 22, 2026 02:53
CodeWhale Bot added 4 commits August 21, 2026 20:11
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
@Hmbown

Hmbown commented Aug 22, 2026

Copy link
Copy Markdown
Owner Author

Maintainer release receipt — v0.9.11 candidate 23fca25bfbef3dd4db2c487b1e83c146b7d5013f

Source

  • Benchmark-free candidate: 23fca25bfbef3dd4db2c487b1e83c146b7d5013f.
  • Local integration commit: babbcce8f00b313f1fbd9809c92806d97fe97e90.
  • Complete non-benchmark trees were verified byte-identical. The candidate changes exactly four owned non-benchmark paths.

CI

  • All GitHub Actions gates are green.
  • Windows full job: PASS in 20m19s, covering the four portability/contention fixes.
  • macOS rerun: PASS in 39m43s. Full nextest suite PASS in 25m11s; doctests, RSS budget, lockfile drift, and offline evaluator all PASS.
  • The first macOS attempt was cancelled at the workflow ceiling after its sccache server exited and the job rebuilt locally; completed tests were green. The rerun cache receipt is healthy: 82 Rust hits, 1 miss, 0 cache errors, 0 compilation failures.

Local release gates and exact artifacts

  • PASS: format/diff, version surface, OHOS, changelog sync, translations, vocabulary, contributor credit, release-helper contracts, locked workspace check/clippy/test, all 55 runtime-contract metrics, crate publish dry-run, Web/npm/Runtime SDK/VS Code gates.
  • codewhale 0.9.11 (23fca25bfbef): 69,544,928 bytes; SHA-256 119e65145c48cd550bda78a3a3420b7c5d75fe39eba2d2bada5e8d11e0ab45ed.
  • codewhale-tui 0.9.11 (23fca25bfbef): 64,469,968 bytes; SHA-256 f141fa469f8449e3ef3d6be2eb8d071f50fce4c3fab0ee5c216963f67cc5d492.
  • PASS: app-server stdio, Runtime API and offline DeepSeek vision/alias discovery, MCP stdio, npm local-asset install, and network-disabled Docker completion checks for Bash, Zsh, Fish, PowerShell, and Elvish. No installed binary was replaced.

Dogfood

  • Fresh desktop 1440x900 and mobile 390x844 exact-artifact screenshots PASS; the bottom composer/overflow defect is no longer present.
  • Fresh deeper browser automation was BLOCKED by the local approval-service usage limit. Prior exact-artifact interaction evidence remains applicable because the Web source is byte-identical; this is explicitly prior evidence, not a fresh interaction claim.
  • Terminal PASS: first-run/trust/clean exit, offline catalog and flash-vision resolution, /attach, /network, /task, /mcp, /rc, /resume, /update, rejected workflow visibility, and 40x12/80x24/140x40 layouts.
  • Terminal BLOCKED: direct exact-artifact multi-file read_lints execution because loopback bind was sandbox-denied; populated sub-agent focus/scroll and omitted max_steps require a provider-backed turn. Source/local tests cover these paths.
  • A GLM-5.3 continuation prompt is loaded in a live exact-candidate terminal but remains unsent until an explicit all-in USD cap and confirmed provider-side hard quota exist.

Remaining blocker

  • GitGuardian remains red after scanning 168 historical commits: incidents 36436548 and 36436549, four occurrences of synthetic Bearer-token redaction fixtures in earlier commits. Current source assembles the flagged fixtures at runtime and contains no complete flagged credential. This needs authenticated GitGuardian false-positive/test-fixture classification; contributor history will not be rewritten merely to clear the check.

The PR remains draft and unmerged. Community credit for #5523, #5524, #5525, and #5526 is preserved; #5530 remains reviewed overlap only. benchmarks/pi-agent-parity/** was untouched. No provider canary, publish, deploy, merge, tag, release, production mutation, or installed-binary replacement occurred.

Promotes the fully gated non-benchmark candidate while preserving the benchmark tree exactly from the prior release-PR head.

Signed-off-by: CodeWhale Bot <bot@codewhale.net>
@Hmbown
Hmbown marked this pull request as ready for review August 22, 2026 11:18
@claude

claude Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 7m 39s —— View job


Code Review — v0.9.11 release integration (PR #5542)

Todo list

  • Gather context (diff vs origin/main, repo guidance)
  • Understand request (code review per custom instructions)
  • Review correctness bugs (core turn-loop refactor, app-server, cli, mcp)
  • Review provider/model/route safety (EPIC v0.8.65 EPIC: Separate provider facts, model facts, offerings, and route resolution #2608 invariant)
  • Review secrets/persistence/device-code handling (GitGuardian flagged persistence.rs)
  • Review reuse/simplification opportunities
  • Review test coverage
  • Post final review summary

This is a large release-integration PR (~39K/17K diff across ~412 files vs origin/main at 75dca2cf, 169 commits). I ran focused passes over the highest-risk areas (core/app-server, MCP, secrets/persistence/device-code, provider/route/fleet, CLI/config-bundles/command-contract, and the remaining smaller crates) and verified the notable claims against the current tree rather than trusting sub-review summaries at face value.


🔴 High: SSRF via unvalidated redirect hops in config_bundles.rs

crates/cli/src/config_bundles.rs:374-406 (fetch_bundle) validates scheme/host only on the original URLhttps is always allowed, http only for loopback hosts — but then hands the request to:

.redirect(reqwest::redirect::Policy::limited(MAX_REDIRECTS))

reqwest's built-in redirect policy does not re-run that scheme/host check on each hop. An attacker-controlled or compromised bundle host can respond with an https:// URL that 30x-redirects to http://169.254.169.254/... or any internal address, completely bypassing the "plain http only on loopback" contract the doc comment above the function promises.

The workspace already has the correct pattern for exactly this problem: crates/tui/src/tools/web/fetch.rs:219 uses redirect::Policy::none() and manually follows redirects, re-invoking validate_fetch_target (crates/tui/src/tools/web/guard.rs:72) on every new Location header. fetch_bundle should reuse or mirror that pattern instead of relying on reqwest's policy. No test exercises a redirect-to-internal-host case, consistent with the gap.

Fix this →

🟡 Medium

  • crates/cli/src/config_bundles.rs (plan_import/apply_entries) — bundle "sections" (preferences/global/etc.) are display labels only; the underlying config key space is flat. Two sections mapping the same real key silently collide at apply time with no reported conflict. Worth a duplicate-key check in the plan step.
  • crates/mcp/src/stdio_client.rs:789-811 — server-initiated JSON-RPC requests (e.g. ping) are only answered from inside the receive loop of an in-flight client request. During idle periods with no outstanding client call, an unsolicited ping from the child MCP server goes unanswered until the next unrelated request happens to drain the channel — a server with its own liveness timeout could wrongly conclude the client is unresponsive and exit. The doc comment frames this as fully handled, but only one window is actually serviced.
  • crates/app-server/src/lib.rs:2203 — the SubmitUserInput refusal message literal contains runs of ~30 raw spaces where line breaks were in source (e.g. "...cannot deliver clarification answers..."). This is real client-facing payload text, not a comment, so it will render garbled to API/JSON-RPC clients. Not caught by fmt/clippy since it's inside a string literal.

🟢 Low / nits

  • crates/release/src/launch.rs store() writes to a fixed *.json.tmp path before an atomic rename; two concurrent Codewhale processes racing here won't corrupt the file but one process's write can be clobbered mid-write by the other's, potentially losing/duplicating a launch-record hint. A PID/nonce suffix (already used for the hooks/src/lib.rs socket-path fix in this same PR) would close it.
  • crates/mcp/src/lib.rs read_resource_by_uri calls list_resources() (a full paginated fetch) against every registered server just to find a URI's owner — correct but expensive; could short-circuit on first match.
  • crates/mcp/src/lib.rs (stdio_tool_call_result/stdio_resource_read_result) unconditionally clones the full server result to populate a legacy field, doubling memory for large payloads bounded only by the 32 MB line cap — cuts against the PR's own resource-exhaustion framing.
  • crates/secrets/src/account.rsAccountSessionError::SecureStoreUnavailable is now dead (nothing constructs it since system_keyring() can no longer fail down that path); harmless but the stale error text is misleading if a reader assumes the path is reachable.
  • crates/config/src/device_code.rs doesn't itself cap a server-declared expires_in/lifetime Duration — no I/O happens on it directly today, but worth a sanity clamp at the point the caller derives it from the wire response.

✅ Verified as sound (not bugs)

  • Provider/model/route safety (EPIC v0.8.65 EPIC: Separate provider facts, model facts, offerings, and route resolution #2608 invariant): route/resolver.rs's deepseek_versioned_model_prefers_responses prefix-matches the model id, but only after provider_kind is already pinned from req.explicit_provider — this is endpoint selection within an explicit provider, not provider inference from a wire-id prefix. Existing test resolver_aggregator_preserves_prefixed_wire_id_without_inferring_deepseek plus new vision-exp/custom-endpoint tests cover the invariant. No violation found.
  • Fleet identity vs. Runtime authority separation: an initial pass flagged crates/protocol/src/fleet.rs's doc comments (new runs "reject" legacy security_policy/trust_level) as unenforced. On verification, the enforcement is real — it lives in crates/tui/src/fleet/task_spec.rs:117-119,161-163 (explicit rejection with a clear error message) and is exercised by fleet_security_policy_is_rejected_for_new_runs in crates/tui/src/fleet/manager.rs:4926, both added in this PR. Doc and code agree; the earlier flag was a scoping artifact of reviewing crates/protocol/ in isolation.
  • GitGuardian secret alerts (incidents 36436548/36436549): confirmed against the flagged historical commits (c14180738, etc.) — the literals were synthetic fixtures (abc123def456ghi, AIzaSyDeadBeefLeak), never working credentials. Current crates/config/src/persistence.rs assembles bearer/JWT-shaped test strings at runtime (["Bear","er"].concat(), etc.) specifically so the scanner no longer literal-matches them. No live secret in current source; the PR's characterization is accurate.
  • crates/config/src/device_code.rs (new, 460 lines): correct scheme/loopback validation before handing a verification URI to a browser opener, no logging of the token type, sound deadline/slow-down clamping. No issues.
  • crates/core engine deletion: grepped the whole repo for the deleted engine::/thread::{store,types,executor,events} symbols — zero external callers; it was dead scaffold, its own doc comment admitted the real turn loop was wired elsewhere. crates/core/tests/single_turn_loop.rs is a real, non-trivial guard (recursive scan + a second test forbidding recreation of core/src/engine/), matching the AGENTS.md single-turn-loop invariant.
  • Process lifecycle handling in crates/mcp/src/stdio_client.rs (Drop: close stdin → grace period → kill → wait) is solid and avoids zombies; bounds/pagination (ListBudget, read_bounded_line) are genuinely enforced, not just claimed, with real subprocess-based tests.
  • crates/cli/src/config_bundles.rs otherwise has strong test coverage for a large new file: oversize input, unknown fields, credential rejection, deterministic plan ordering, rollback-on-failure, idempotent re-import, and path-traversal/symlink-escape refusal.
  • fragments.rs's symlink-escape fix and config_document.rs's heal_extras_nesting guard are genuine, well-tested fixes.

Summary

One high-confidence security finding (SSRF in config_bundles.rs via unvalidated redirects) should be fixed before merge. The rest are medium/low correctness and efficiency notes. The provider/route-safety invariant and the Fleet identity/Runtime-authority separation — the two claims most load-bearing for this release — both check out against the current source, and the GitGuardian alerts are confirmed non-issues (synthetic fixtures, not live credentials).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 959dbe4382

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/config/src/persistence.rs
Comment thread crates/cli/src/lib.rs
Comment thread crates/cli/src/config_bundles.rs Outdated
Comment thread crates/cli/src/config_bundles.rs Outdated
Comment thread crates/cli/src/config_bundles.rs Outdated
Comment thread crates/cli/src/config_bundles.rs Outdated
Comment thread crates/tui/src/project_context.rs Outdated
Comment thread crates/tui/src/tui/ui/terminal_input.rs Outdated
Comment thread crates/release/src/launch.rs Outdated
Comment thread crates/tui/src/commands/groups/config/status.rs Outdated
@claude

claude Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Claude Code is working…

I'll analyze this and get back to you.

View job run

Signed-off-by: CodeWhale Bot <bot@codewhale.net>
@Hmbown
Hmbown force-pushed the codex/v0911-nonbenchmark-release-20260821 branch from 1e496ac to accfa93 Compare August 22, 2026 11:41
@claude

claude Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 1m 24s —— View job


Claude finished @Hmbown's task —— View job


Code Review — new commit since last review (PR #5542)

Todo list

  • Diff PR head vs. last-reviewed commit (959dbe43) to scope this pass
  • Review the new commit accfa93e ("fix(tui): keep runtime contract within budget")
  • Check for stale test assertions against the trimmed strings
  • Post review summary

The prior full review posted earlier on this PR already covered the bulk of this release-integration diff (SSRF finding in config_bundles.rs, plus several medium/low notes). Since then, exactly one new commit landed on the branch: accfa93e — no other files changed.

accfa93e — trims two tool-schema description strings

  • crates/tui/src/tools/lsp.rs: shortens LspTool::description() and the operation/path/line/character/query schema field descriptions.
  • crates/tui/src/tools/workflow.rs: shortens the fleet field description.

This is a mechanical trim to satisfy scripts/check-runtime-contract-budget.py, which enforces one-way (decrease-only) ceilings on the model-facing tool catalog's schema byte/token size (scripts/runtime-contract-budget.json) — consistent with this repo's existing pattern of trimming prompt/schema bytes across prior releases (see the v0.9.9/v0.9.10/v0.9.11 entries already recorded in that file's _comment).

Verified sound:

  • The unit test schema_documents_read_lints_contract_query_and_character_default (crates/tui/src/tools/lsp.rs:481) was updated in the same commit to match the new trimmed strings ("1-based line.", "1-based column (default 1).", "Workspace symbol query.") — no stale assertions left behind.
  • Grepped the rest of crates/tui/src for the old, longer wording (e.g. "freezes member identity, provider, model, and reasoning") — no other file references it, so nothing else needs updating in lockstep.
  • No behavior change: only doc/description text shrank; input_schema() structure, required fields, and enum values are untouched.

Minor (not blocking): the trimmed LspTool description drops the explicit "Reuses the session LSP manager and requires \[lsp] enabled = true` plus a configured language server"sentence entirely. That's still enforced in code (unaffected by this diff), just no longer surfaced to the model in the tool description — a model without prior context could calllspwithout realizing it needs[lsp] enabled = truefirst and get an unexplained failure. Given this is purely a budget-driven trim and the same trade-off already exists elsewhere in this file (e.g., "Optional workspace symbol query when operation=symbols" → "Workspace symbol query."), I wouldn't block on it, but it's worth keeping in mind ifread_lints/diagnostics` calls start failing silently against unconfigured LSP setups more often after this change.

No correctness bugs, provider/route-safety issues, security issues, or missing test coverage in this incremental commit.

@Hmbown

Hmbown commented Aug 22, 2026

Copy link
Copy Markdown
Owner Author

Fresh CI found one real release-gate regression on the previous candidate: the model-facing runtime-contract schema exceeded its established budget after the Fleet and bounded read_lints descriptions expanded.

I reproduced it locally and pushed the focused signed fix as accfa93e5a1a890661eb7e08ebc7e150b24e1aa9. The update changes only crates/tui/src/tools/lsp.rs and crates/tui/src/tools/workflow.rs; its diff against the prior PR head is empty under benchmarks/pi-agent-parity/**.

Post-fix local evidence:

  • runtime-contract budget: all 55 ceilings pass (Plan 111 bytes / 28 estimated tokens below; Act and Operate 110 bytes / 27 tokens below)
  • budget-checker tests: 17/17
  • LSP schema/tool tests: 13/13
  • full workspace all-features tests: PASS
  • formatting, workspace check, strict all-features clippy, diff check, and 21-crate publish dry-run: PASS

Fresh required checks are now running on the exact signed tip. No provider call, publication, deployment, or benchmark-lane mutation occurred.

@Hmbown

Hmbown commented Aug 22, 2026

Copy link
Copy Markdown
Owner Author

Maintainer triage of the full Claude review:

  • Confirmed / release-blocking: config_bundles::fetch_bundle validates only the initial URL while the default redirect policy can follow an HTTPS hop to a disallowed internal/plain-HTTP target. Merge is paused. I am replacing automatic redirects with bounded manual hops that revalidate every target and adding loopback redirect regression coverage.
  • Confirmed: the app-server user-input refusal contains source-formatting whitespace in client-visible text. A focused cleanup and regression assertion are in progress.
  • Confirmed for fail-closed hardening: duplicate config keys in multiple applicable bundle sections currently target one flat keyspace and can silently overwrite by section order. I am adding deterministic conflict rejection.
  • Under protocol review: idle server-initiated MCP requests/pings; I am checking the current reader/backpressure architecture before deciding whether this needs a v0.9.11 fix.

The PR will not merge until the confirmed blocker is fixed and the replacement candidate has fresh local and GitHub evidence. Contributor history and benchmarks/pi-agent-parity/** remain untouched.

Signed-off-by: CodeWhale Bot <bot@codewhale.net>
@claude

claude Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 2m 53s —— View job


Code Review — new commit 491a7121 (PR #5542)

Todo list

  • Identify the new commit since last review (491a7121, "fix(release): address v0.9.11 review blockers")
  • Verify SSRF fix in crates/cli/src/config_bundles.rs (bounded manual redirect revalidation)
  • Verify app-server whitespace/client-visible text fix
  • Verify MCP idle server-initiated request/ping handling change
  • Check for new correctness bugs, missing tests, or regressions introduced by the fix
  • Post final review summary

This commit is exactly what the maintainer's triage comment described: it addresses the SSRF finding, the app-server whitespace finding, and (further than stated — it says "under protocol review") the MCP idle-ping finding from the earlier full review, plus the "duplicate config keys silently collide" medium finding.

✅ SSRF fix (crates/cli/src/config_bundles.rs::fetch_bundle) — sound

  • Switches to redirect::Policy::none() and manually follows redirects, calling validate_bundle_url/validate_bundle_redirect on every hop — matches the pattern used by tools/web/fetch.rs/guard.rs that the original finding pointed to.
  • Goes further than the minimum fix: also rejects URL userinfo (user:pass@host) and forbids a redirect from changing scheme at all (not just downgrading), which is stricter than the original https-loopback-http contract but safe.
  • is_loopback_bundle_host now parses the host as an IpAddr and checks .is_loopback() instead of literal-string matching 127.0.0.1/::1, so it correctly covers the whole 127.0.0.0/8 range.
  • Error messages are deliberately URL/secret-free (bundle fetch request failed, not the URL) — verified by redirect_to_non_loopback_http_is_refused_without_leaking_location and bundle_url_credentials_are_rejected_without_echoing_them, which assert the secret/host never appears in the error text.
  • New tests spin up a real loopback TcpListener fixture and exercise the actual attack (redirect to a disallowed host, scheme-downgrade redirect, redirect-limit enforcement, relative-redirect success path) rather than only unit-testing the validator function in isolation. This is good regression coverage for exactly the bypass that was reported.
  • No remaining gap for the reported bypass. (Note, not a regression from this commit: HTTPS to an arbitrary host — including an internal service that happens to speak HTTPS — is still allowed by design, same as the original contract and the same threat-model boundary as tools/web/guard.rs; DNS-rebinding at connect time is likewise out of scope here, consistent with the rest of the codebase.)

✅ App-server whitespace fix — sound

crates/app-server/src/lib.rs:2200 now builds the refusal message with concat! instead of a literal containing the old line-wrap whitespace runs. New assertion (!message.contains(" ")) is a cheap, durable regression guard for exactly the reported defect.

✅ MCP idle server-request handling — sound, well beyond "under review"

crates/mcp/src/stdio_client.rs moves server-initiated request handling (ping, unsupported methods) from Connection::request's rendezvous loop (only serviced while a client call is in flight) into the reader thread itself, so it's answered immediately during idle periods — this is precisely the medium finding from the original review ("a server with its own liveness timeout could wrongly conclude the client is unresponsive").

  • stdin is now Arc<Mutex<ChildStdin>>; the reader thread holds only a Weak handle (response_stdin) and upgrades it transiently to write a response, explicitly dropping the strong ref right after. Connection (and its Drop) keeps the only strong Arc, so closing the connection still closes the child's stdin promptly — confirmed by the new idle_child_requests_are_answered_before_the_next_client_request test, which uses a real /bin/sh subprocess that kills itself if a ping/unsupported-method request goes unanswered for 1s while genuinely idle, then verifies drop(client) still delivers EOF (not a kill).
  • The old Connection::answer_server_request method and its call site in the rendezvous loop are cleanly removed — no dead code or duplicate handling left behind (verified: no remaining references).
  • write_jsonrpc_line is factored out and reused by both the reader thread and Connection::send, avoiding duplicated serialize/write/flush logic — nice cleanup, not just a bug fix.

✅ Duplicate-key collision fix (plan_import) — sound

Detects a bare config key present in more than one applicable bundle section (e.g. preferences.verbosity and global.verbosity both resolving to the same flat key) and fails the whole import closed via plan.conflicting before any write, rather than resolving by section iteration order. duplicate_flat_keys_across_applicable_sections_fail_before_apply confirms the collision is caught, the error text doesn't leak either value, and the config file on disk is byte-identical to before the attempted import.

Not touched by this commit — still open from earlier review passes

For completeness, since this is a release-gating PR: the following previously-flagged items are not addressed by 491a7121 and remain outstanding on the current tip (confirmed by re-checking the source, not just the diff):

  • Codex P1: apply_bundle (crates/cli/src/config_bundles.rs:679-680) still unconditionally std::fs::copy(&target, &backup_path)s before applying, which still fails NotFound on a fresh install where ConfigStore::load returns a default with no on-disk file yet — the first-machine import scenario the bundle feature is meant for.
  • Codex P1: crates/cli/src/lib.rs ConfigCommand::Import/Export dispatch still doesn't load the <cwd>/.codewhale/config.toml workspace store for --project, still relies solely on the unrelated global --config option.
  • Codex P1: export_bundle/value_for_export still export through ConfigToml::list_values()/get_value (stringly, non-recursive), so typed sections (harness_profiles, skills, snapshots) and nested-secret values under an otherwise benign key are still not handled per the docstring's contract.
  • Prior medium/low notes on crates/tui/src/project_context.rs, crates/tui/src/tui/ui/terminal_input.rs, crates/release/src/launch.rs, and crates/tui/src/commands/groups/config/status.rs are also untouched.

None of these are regressions introduced by 491a7121 — they're pre-existing gaps in code this same PR added — but worth tracking explicitly before the "release-blocking" label is cleared, since the maintainer's triage comment addressed the three findings from Claude's review plus the duplicate-key one, not the Codex-flagged items above.

Portable config bundles:
- import before a config file exists, removing the created document on
  rollback instead of requiring a backup copy
- `config import|export --project` loads the workspace document under the
  current (or legacy) app dir instead of the global store
- export serializes from the typed config model, preserving TOML value
  kinds and typed sections; import deep-merges without erasing omitted
  local authority (credentials, endpoints, hooks, LSP commands)
- recursive key/value secret policy on export and import, including
  camelCase and dotted credential keys, machine-local network proxy
  routes, cookies, and redaction placeholders
- compaction previews keep redacting camelCase/dotted credential keys

Terminal, launch, localization:
- fragment-backed foreign instruction formats participate in the opt-in
  warning only when the bounded loader would actually load them
- terminal input shutdown no longer joins a wedged TTY read on Unix
- launch records persist through a uniquely named temp file with
  replace-existing semantics on every platform
- the rewritten /status report is fully localized and preserves
  custom-provider identities that contain brace-like text

Signed-off-by: CodeWhale Bot <bot@codewhale.net>
@claude

claude Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 5m 18s —— View job


Code Review — new commit b38ecbfa (PR #5542)

Todo list

  • Diff PR head vs. last-reviewed commit (491a7121) to scope this pass
  • Review the new commit b38ecbfa ("fix(release): close v0.9.11 review threads")
  • Check whether it addresses the remaining Codex P1/P2 findings and prior Claude-review notes
  • Post review summary

Since the last review pass (491a7121), exactly one new commit landed: b38ecbfa. It's a large, deliberate cleanup that closes out essentially every open finding from the Codex P1/P2 comments and my own prior review notes. I verified each claim against the actual diff/source rather than trusting the commit message.

✅ Config bundles (crates/cli/src/config_bundles.rs, crates/cli/src/lib.rs) — all four Codex P1 findings fixed

  • Fresh-install import (NotFound on backup)apply_bundle_with now checks target.try_exists() before backing up; when the file is absent, backup_path = None instead of fs::copying a nonexistent path. Rollback removes the newly created file (tolerating NotFound) rather than restoring a backup that was never made. Covered by non_no_op_import_creates_a_missing_config_without_a_backup and failed_import_removes_a_config_created_during_the_transaction.
  • --project loading the workspace documentcrates/cli/src/lib.rs now resolves the config path from clap::ArgMatches before Cli::from_arg_matches, so config import|export --project targets <cwd>/.codewhale/config.toml (falling back to the legacy .deepseek/ app dir when only that exists) instead of the global --config store. An explicit --config still wins and is correctly refused for --project (--project requires a workspace config). Well tested, including the "both explicit and --project" conflict case.
  • Typed-field exportexport_bundle now walks config_document(config) (a round-tripped toml::map::Map of the typed ConfigToml) instead of list_values()'s rendered strings, so harness_profiles, skills, snapshots, and scalar TOML kinds (bool/int/float/datetime) survive export/import round-trips with their real types, not stringified. Verified by export_preserves_typed_structured_config_and_toml_value_kinds.
  • Recursive secret rejection on exportsanitize_export_value now recursively walks Table/Array values applying the same key-name (is_sensitive_bundle_key, including new camelCase/dotted normalization) and value-shape policy that import already used, at every nesting level — closing the "nested credential under a benign top-level key" bypass. Several adversarial tests cover this (nested access_token, Cookie/Set-Cookie, provider auth subtrees).
  • Deep-merge on importdeep_merge_toml_value recurses only through matching Table/Table pairs and replaces wholesale otherwise, so a bundle updating one field (e.g. providers.acme_gateway.model) leaves sibling local-only authority (api_key, base_url, auth, lsp.servers, hook_sinks.unix_socket_path) untouched — confirmed by structured_import_deep_merges_without_erasing_local_authority.
  • SSRF redirect revalidation from the prior fix (491a7121) is untouched and still intact.
  • One harmless leftover: export_section_for's key.starts_with("auth.") branch (config_bundles.rs) is now dead — export_bundle only calls it with top-level struct field names (no dots), and the real providers.*.auth subtree is dropped by the recursive scrubber instead. Not a bug, just stale code from the old dotted-key iteration; fine to leave or delete.

✅ Secret redaction — camelCase/dotted credential keys (crates/config/src/persistence.rs)

Fixes the Codex P1 on tool_args_preview/redact_json_secrets: adds normalize_sensitive_key, which folds case-transition and punctuation boundaries into _ before matching (accessToken, oauth.token, APIKey now match the same predicate as access_token). The token hint gets a deliberate carve-out so usage-metric keys (tokenBudget, tokenCount, maxTokens) stay visible rather than being redacted as false positives. New tests exercise both the redact-and-preserve-metrics cases for JSON and inline text.

✅ Terminal input shutdown (crates/tui/src/tui/ui/terminal_input.rs)

Drop no longer joins the pump thread on any platform — matches the doc comment's own claim that event::read can wedge forever after a tty disconnect. Also adds a stop-check right after poll returns Ok(true) so a cooperative shutdown doesn't proceed into a blocking read() unnecessarily.

✅ Launch record atomic replace (crates/release/src/launch.rs)

Replaces the old write + rename (which doesn't replace an existing file on Windows) with tempfile::NamedTempFile::persist, which uses MOVEFILE_REPLACE_EXISTING on Windows and atomic rename elsewhere. New test store_replaces_an_existing_record covers the actual regression (second store() call after a record already exists).

✅ Foreign-instruction opt-in warning (crates/tui/src/project_context.rs)

Fixes the P2: unimported_foreign_warnings now also checks fragment_candidates() (Cursor/Cline/Windsurf/Gemini/Copilot/Muse) via the actual bounded fragment loader, not just the Claude-specific context_files()/rules_dirs(). Goes further than the minimum fix — it only warns when content is genuinely loadable (non-empty, Markdown, non-symlink), with dedicated tests for empty files, non-Markdown rules dirs, and symlink escapes.

/status localization + brace-safe interpolation (crates/tui/src/commands/groups/config/status.rs, locale files)

All English literals in the rewritten /status report now route through typed MessageIds and tr()/localized(). The new localized() helper scans the template string for {placeholder} tokens and substitutes once per match without re-scanning substituted text, so a custom provider identity or session id containing literal {model}/{cells}-shaped text can't be misinterpreted as a placeholder or corrupt later substitutions — verified by status_report_interpolation_preserves_braces_in_runtime_values. All 15 locale files gained exactly the same 52 new Status* keys (checked via before/after key-count diff per locale), so no locale is left with missing translations. A full-surface Japanese-locale test iterates every new MessageId and asserts none renders as English.

Not addressed by this commit (pre-existing, non-blocking)

The MCP idle-ping protocol question the maintainer flagged as "under protocol review" was already resolved in 491a7121 per my prior pass. Nothing new is outstanding from the Codex review threads that I can find still open in the current tree.

No correctness bugs, panics, or provider/route-safety issues found in this commit. I could not run cargo check/clippy/tests in this sandboxed environment (build commands require approval not available here) — this review is based on full-diff static reading plus a focused sub-agent verification pass over config_bundles.rs, cross-checked against the actual current file contents, not just the diff or commit message.
· branch codex/v0911-nonbenchmark-release-20260821

@Hmbown

Hmbown commented Aug 23, 2026

Copy link
Copy Markdown
Owner Author

Replacement candidate pushed: PR head is now b38ecbfa332de285390a245d0de905091e861b86 (parent 491a7121…, 30 non-benchmark files; its tree is byte-identical outside benchmarks/pi-agent-parity/** to the fully gated local integration commit 98e3814e02f9f01d959c30580c3d07413e0cf4fa, and the benchmark subtree stays absent on the PR branch).

This commit closes every open Codex review thread (replies are posted per thread with the exact functions and regression tests) plus the earlier launch.rs temp-path note:

  • P1 fresh-install import — missing target is tracked as backup_path: None; rollback removes only a document the transaction created.
  • P1 --project dispatchconfig import|export --project now loads <cwd>/.codewhale/config.toml (falling back to a legacy .deepseek/ document if that is the one that exists), and scope is validated in both directions.
  • P1 typed export — bundles are serialized from the typed config model (harness_profiles, skills, snapshots, arrays/tables/ints/bools/datetimes keep their TOML kinds); import deep-merges so omitted local authority (API keys, base_url, credential commands, LSP executables, hook sockets) is never erased.
  • P1 recursive export secret policy — one recursive key/value predicate on both export and import: nested/camelCase/dotted credential keys, cookies, sk--shaped values, machine-local network.proxy routes, and [redacted] placeholders are scrubbed on export and rejected on import without echoing values.
  • P1 camelCase/dotted redactionredact_json_secrets/redact_secrets normalize accessToken, refreshToken, oauth.token, OAuthToken, x-api-key while keeping maxTokens/token_count metrics visible.
  • P2 fragment-backed opt-in warning — presence is decided by the same bounded loader that would load Cursor/Cline/Windsurf/Gemini/Copilot/Muse fragments (empty, non-Markdown, and symlinked inputs do not warn).
  • P2 Unix input-thread join — teardown detaches the pump thread on every platform; terminal-mode restore is independent of the join.
  • P2 Windows launch record — written through a uniquely named temp file with replace-existing persist, also closing the fixed-*.tmp race.
  • P2 /status localization — every label/value is MessageId-backed across all 15 locale packs (key-set parity 0/0), and custom-provider identities containing braces survive interpolation.

Local gates at 98e3814e… (all PASS): cargo fmt --check, git diff --check, version/OHOS/changelog-sync, README translation and locale parity, product vocabulary, cargo check --workspace --all-targets --locked, cargo clippy --workspace --all-targets --all-features --locked -D warnings, cargo test --workspace --all-features --locked (13,355 passed, 0 failed, all 21 doctest crates), 21-crate publish dry-run (nothing uploaded), Web lint/test/facts/docs/locales/build, embedded Runtime Web (36 node + 21 vitest), npm wrapper, Runtime SDK, VS Code, and all 14 release-helper suites.

Fresh required checks are running on b38ecbfa…. GitGuardian remains the known historical synthetic-fixture finding; no incident state was mutated. No merge, tag, publication, deployment, provider call, or benchmark-lane change occurred.

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.

3 participants