0.9.12 integration: must-fix + UX fixes (work-in-progress) - #5576
Draft
Hmbown wants to merge 232 commits into
Draft
0.9.12 integration: must-fix + UX fixes (work-in-progress)#5576Hmbown wants to merge 232 commits into
Hmbown wants to merge 232 commits into
Conversation
…iled events Add an opt-in, machine-readable lifecycle event outbox for supervisors and automation harnesses. Unset/empty config = feature OFF = behavior unchanged. Config ([lifecycle_outbox]): - path — JSONL outbox file (unset/empty disables the feature) - webhook_url — optional webhook endpoint; POSTs only when set - webhook_token — optional bearer token for webhook_url Writer (crates/hooks/src/lifecycle_outbox.rs): - One JSONL line per event in the existing RuntimeEventEnvelope shape (schema_version, seq, event, kind, thread_id, turn_id, item_id, timestamp, created_at, payload); append + flush per event. - seq monotonic per file; recovers from the last complete line on open via a bounded 64 KiB tail scan (torn trailing lines ignored). - Single non-blocking writer task: emit() enqueues; no tokio runtime available => drop with warning. - Payloads only from bounded, pre-redacted fields (headline ≤ 80, detail ≤ 120, preview ≤ 200 chars; control bytes stripped). Signed-off-by: M-Maciej <130112810+M-Maciej@users.noreply.github.com>
A TUI turn killed mid-flight by a disconnected engine (stream idle/error, crash) never receives a TurnComplete, so its turn_start stayed orphaned in the outbox — the TUI had no analogue of the exec channel-closed guarantee. recover_engine_event_disconnect now captures the in-progress turn identity before the state reset and emits the folded turn_end (kind turn.failed, status failed, wall-clock duration, bounded error, workspace) for exactly the state turn_start is emitted for; a disconnect with no in-progress turn fabricates nothing. Tests cover both branches. Signed-off-by: M-Maciej <130112810+M-Maciej@users.noreply.github.com>
… site The consumer resolves the project from payload.workspace, so events lacking it were dropped fail-closed. Every emit site now carries the resolved workspace path — TUI turn_start, turn_end, session_end, turn_stalled, both subagent events (which additionally carry subagent alongside agent_id), and both exec turn_end sites (terminal receipt and channel-closed) — matching the session_start and exec turn_start sites that already had it. Tests: a hooks round-trip asserts workspace on every event type and subagent on the subagent events; the stall emit-site test asserts the workspace; the exec integration asserts payload.workspace equals the --workspace directory for turn_start and turn_end. Signed-off-by: M-Maciej <130112810+M-Maciej@users.noreply.github.com>
… turn_end CONFIGURATION.md documents the routing fields (workspace on every payload, subagent on subagent events), the new TUI failure-path folded turn_end, and tightens the webhook wording to bounded retries inside the sink, failures logged and dropped, never fed back into the agent loop. The lane changelog marks the two closed gaps (routing fields, TUI orphan turn_end) done and keeps the genuinely remaining follow-ups. Signed-off-by: M-Maciej <130112810+M-Maciej@users.noreply.github.com>
codewhale doctor now reports the resolved outbox state — off (default) when [lifecycle_outbox].path is unset/empty, on with the sink path otherwise — matching the truth-and-resilience theme of the other posture rows. Tested for both states. Signed-off-by: M-Maciej <130112810+M-Maciej@users.noreply.github.com>
- docs/rfcs/1365-lifecycle-outbox.md: converts the build log into the review artifact modeled on 1364-hooks-lifecycle.md (problem, scope table, design/contract, structure, limitations, test plan, review checkpoints). Issue number provisional until the upstream issue is filed. - docs/changelog-lifecycle-outbox.md: status line pointing at the RFC. Signed-off-by: M-Maciej <130112810+M-Maciej@users.noreply.github.com>
Goal-continuation turns are engine-originated: the synthetic ContinueGoal token never passes through the UI's user-message dispatch, so the emit sites hooked there produced no outbox turn pair for them. Move the TUI's turn-boundary emits into the engine. The interactive TUI wires its [lifecycle_outbox] handle at engine spawn (spawn_tui_engine, including every engine-replacement site), and handle_send_message emits turn_start right after TurnStarted and turn_end at the single terminal outcome, with the same envelope shape (workspace + thread/turn ids) and the same seq discipline (same writer). Exec and hosted engines keep their existing emit sites and pass no handle, so nothing changes there. Side effects, both emit-only fixes: the engine-side turn_end is projected exhaustively from the terminal status (no more undocumented turn.ended fallback), and completion events without a preceding turn (compaction/ purge, bang commands) no longer mint a phantom turn_end under a stale turn id. Regression: goal_continuation_turn_emits_turn_start_and_turn_end_pair_to_the_outbox runs a real user turn plus its synthetic continuation against the engine harness and asserts both turn pairs land in the outbox file with distinct, correlated turn ids. Signed-off-by: M-Maciej <130112810+M-Maciej@users.noreply.github.com>
Signed-off-by: M-Maciej <130112810+M-Maciej@users.noreply.github.com>
The in-file round-trip test still constructed next_seq/recovered after the atomic seq change landed; drop the removed fields. cargo test -p codewhale-hooks 22/22. Signed-off-by: M-Maciej <130112810+M-Maciej@users.noreply.github.com>
was already removed when turn-boundary outbox events moved engine-side
(08e684550): turn_start after TurnStarted and turn_end only at the single
terminal outcome of handle_send_message, so compaction/purge completions
with no preceding TurnStarted can no longer mint a turn_end under a stale
turn id. Re-adding a TUI-side emit here would double every turn_end.
Add the regression test the audit asked for at the interactive TUI's
engine wiring: a real user turn produces exactly one turn_start/turn_end
pair (one turn_end, status completed), and a cancel-before-start
compaction TurnComplete { Interrupted } with no in-progress turn writes
nothing — the outbox still holds exactly the one pair, no turn.interrupted
line, no duplicate.
Gate: ZIG=/opt/zig-0.15.2/zig cargo test -p codewhale-tui --lib event_loop
→ 5 passed, 0 failed.
Signed-off-by: M-Maciej <130112810+M-Maciej@users.noreply.github.com>
- expand ~/env vars in [lifecycle_outbox].path at both TUI construction sites (tui/app/init.rs and lib.rs exec) via config::expand_path, so the documented ~/.codewhale/... example lands under $HOME instead of a literal ~ directory. Regression test builds the App with a tilde path and asserts the line lands in the expanded home and no literal ~ directory appears. - webhook delivery no longer blocks the local append path. The writer drain loop hands each POST to a detached task bounded by a WEBHOOK_MAX_IN_FLIGHT (4) semaphore; a full backlog drops the newest delivery rather than queueing unbounded. Regression tests: a slow endpoint cannot delay local appends; a full backlog drops webhook deliveries but never the local append. - bounded_text strips full ANSI escape sequences (CSI, OSC, DCS/SOS/PM/APC, two-char escapes) instead of only the ESC byte; leaks like '[31m' no longer survive. - module header reworded to the RFC-mandated phrasing: bounded retries inside the sink; failures logged and dropped, never fed back into the agent loop. - CONFIGURATION.md documents the exhaustive turn_end kind projection (no turn.ended fallback kind); config.example.toml comments match. Signed-off-by: M-Maciej <130112810+M-Maciej@users.noreply.github.com>
…on + signal flush The shared lifecycle outbox is written by many concurrent session processes. Two failure modes remain from that ownership gap: - A session killed mid-turn (SIGKILL, closed pane) dies between its turn_start and turn_end appends and can run no code, leaving the turn_start unpaired in the file (verifier turn-pairing/G1 FAIL). - A SIGTERM/SIGHUP/SIGINT exit previously restored the terminal and exited without closing the open turn. The session now owns its turn events end to end: - LifecycleOutbox::reconcile_interrupted_turns(thread_id, reason) scans the outbox under the cross-process exclusive lock for this thread's turn_start lines lacking a matching turn_end and appends one synthetic turn_end each (kind turn.interrupted, payload status=interrupted + reconciled=true + reason, inheriting the start's workspace). One lock acquisition across scan+appends keeps the reconciliation idempotent across concurrent sessions. The open turn is derived from file truth, never in-memory state, so no duplicate turn_end can be fabricated. - The TUI reconciles at boot (before the first emit) and registers its outbox identity for the terminating-signal cleanup task, which runs the same reconciliation for SIGTERM/SIGINT/SIGHUP before exiting. Both paths wait (bounded) for the process's own queued events to drain first, so a still-queued turn_start is visible to the scan. - LifecycleOutbox::emit_blocking adds the synchronous, runtime-free append primitive for shutdown paths and deterministic fixture writers. - New unit tests: reconciliation pairing/idempotence/torn-tail/foreign thread untouched, blocking emit, signal flush pairing + no-op; the existing N-writer monotonic-seq test still covers locked appends. - New example interleaved_outbox_fixture generates a cross-process interleaved fixture; the verifier script reports PASS for turn pairing, and B2 on it (the WP gate). - RFC 1365 documents the session-ownership contract and the SIGKILL / boot-reconciliation / graceful-shutdown relationship. Signed-off-by: M-Maciej <130112810+M-Maciej@users.noreply.github.com>
…ovisional number Signed-off-by: M-Maciej <130112810+M-Maciej@users.noreply.github.com>
…-writers poll emit() enqueues without awaiting, so on a busy runner the first poll can race ahead of the first append (which creates the outbox file). The lenient reader treated the missing file as a panic; on the Windows CI job the race lost and the test failed with NotFound. A missing file is now an empty poll — the writers' appends are asserted by the final line count, so a genuinely broken writer still fails the test, just with a proper assertion instead of an early read panic. Signed-off-by: M-Maciej <130112810+M-Maciej@users.noreply.github.com>
Regenerated crates/tui/CHANGELOG.md via scripts/sync-changelog.sh. Signed-off-by: M-Maciej <130112810+M-Maciej@users.noreply.github.com>
…the same session /relaunch reuses the /exit teardown path (engine shutdown, persistence flush, terminal restore) and then replaces the process image with the current executable run as `resume <session-id>`, so the resumed session owns the same terminal with no orphan process. - commands/groups/core/relaunch.rs: command; refuses unsaved sessions and in-flight runtime work, records the session id in App.pending_relaunch, and quits through the ordinary exit path. - relaunch.rs: argv builder (<exe> resume <id>), the process-wide handoff, and the Unix CommandExt::exec that runs after telemetry close-out; Windows consumes the handoff as a no-op and the quit-time resume hint is the instruction. - event_loop.rs: hands the pending id over after the persistence flush and suppresses the redundant resume hint; lib.rs execs after finish_telemetry so the old session's telemetry is recorded first. - localization: CmdRelaunchDescription in all 15 complete packs plus the command contract bridge; update.rs no longer claims there is no self-exec pattern. Signed-off-by: M-Maciej <130112810+M-Maciej@users.noreply.github.com>
current_executable() trusted std::env::current_exe() whenever it returned Ok, but on Linux a binary replaced by rename resolves through /proc/self/exe to a path with a literal " (deleted)" suffix that no longer exists. exec_relaunch then exec'd the dead path and /relaunch failed with "could not relaunch (... (deleted)): No such file or directory". Resolve the image path, then keep it only when it is non-empty, exists, and its file name is not marked " (deleted)"; otherwise fall back to argv[0] (the PATH-resolvable invocation name), preserving the existing error-path fallback. relaunch_argv's "<exe> resume <session-id>" contract is unchanged. Signed-off-by: M-Maciej <130112810+M-Maciej@users.noreply.github.com>
The Windows CI gate (cargo test --no-run) denies dead code under -D warnings. relaunch_argv is only called from the unix-only self-exec path, so the plain Windows lib build flagged it. Gate the item itself with #[cfg(any(unix, test))] — the portable argv-construction tests keep exercising it on every platform — instead of adding a dead-code lint allowance, so the dead-code budget is untouched. Signed-off-by: M-Maciej <130112810+M-Maciej@users.noreply.github.com>
Regenerated crates/tui/CHANGELOG.md via scripts/sync-changelog.sh. Signed-off-by: M-Maciej <130112810+M-Maciej@users.noreply.github.com>
…/status verbs Config-gated [control_socket] table (off by default) binds <sessions-dir>/<session-id>/control.sock (0600) per running session, speaking a newline-framed JSON-RPC. Verbs: message (structured user message through the composer dispatch path; queued under load), interrupt (the extracted Esc cancel body, shared with the Esc key path), relaunch (seam: dispatches the /relaunch slash-command path — no mechanics duplicated here), status (turn/goal snapshot answered by the socket thread). Wiring: run_event_loop constructs SessionControl and reconciles/updates/drains once per iteration; the socket runs on background threads with bounded reads (1 MiB) and 5 s dispatch timeouts. Unix-only; non-unix parses the key but refuses to bind. Signed-off-by: M-Maciej <130112810+M-Maciej@users.noreply.github.com>
A second live process holding a session's socket made the per-frame reconcile retry the connect-probe and warn-log every iteration. Retries now back off (5 s in prod, 200 ms under test) keyed on the session id, so switching sessions is never delayed by another session's refusal. Signed-off-by: M-Maciej <130112810+M-Maciej@users.noreply.github.com>
…atforms The listener is nonblocking, and on macOS/FreeBSD an accepted socket inherits O_NONBLOCK from the listener (Linux accepted sockets are blocking). The connection handler assumes blocking reads, so on macOS a large request hit EAGAIN mid-frame, the handler dropped the connection, and the client's in-flight write failed with BrokenPipe — the oversized-request test failed exactly this way on macOS CI. Setting the accepted stream back to blocking (a no-op on Linux) makes the handler's bounded-read model hold on every platform. Signed-off-by: M-Maciej <130112810+M-Maciej@users.noreply.github.com>
The Windows CI gate (cargo test --no-run) denies unused imports and dead code under -D warnings. On non-unix targets the socket transport does not exist, so its imports, timing constants, and request/response types are unreachable there. Split the io/atomic imports and gate the five socket timing constants with cfg(unix), and mark the six protocol types (Request, Method, MessageParams, EmptyParams, ControlCommand, ResponseResult) with a scoped allow: they stay reachable in the portable protocol/parsing tests and on unix builds, and are only unreachable in the plain Windows lib build. The dead-code budget file is untouched. Signed-off-by: M-Maciej <130112810+M-Maciej@users.noreply.github.com>
…5533) Regenerated crates/tui/CHANGELOG.md via scripts/sync-changelog.sh. Signed-off-by: M-Maciej <130112810+M-Maciej@users.noreply.github.com>
docs/changelog-lifecycle-outbox.md is the ecosystem-internal build log the RFC was mined from; upstream-facing notes live in the RFC itself and in the root CHANGELOG.md. The RFC no longer references it. Signed-off-by: M-Maciej <130112810+M-Maciej@users.noreply.github.com>
…elease (#5562) Stale write-claims persistently locked sub-agents out of command execution: a claim from a finished/crashed/prior-session child kept looking like a live writer forever, so every later builder failed with 'write-scope contention' or was denied all command tools with 'another child is writing in this shared checkout' — even with no concurrency (issue #5562, ops A9). - Unify liveness: one is_live_coordination_owner predicate (Running agent with the current session boot id, or a non-terminal current- session worker) now drives both claim admission and the shared- checkout peer gate. The old gate used a different definition whose fail-closed 'unknown owner' branch handed prior-session claims an eternal veto, surviving restarts. - Add ledger.release_stale_claims: drops claims whose owner is not a live claimant, optionally scoped to one owner (live claims are never removed), and advances the ledger sequence like any other mutation. - Add the coordinate 'release' action to the model-facing tool, returning released owners + sequence, persisted synchronously. Tests: ledger release sweeps (idempotent, live-safe, owner filter) and an end-to-end coordinate release whose removal survives a state reload. Refs: #5562 #5529
The verifier role description said 'Runs targeted validation' while the enforced envelope actually grants the bounded built-in verification surface (tests/checks) with a read-only write ceiling and refused unbounded shell forms, and the shared-checkout gate could previously deny it every command. The roster description, verifier intro, and SUBAGENTS posture row now say what is true: bounded validation, no writes, unbounded shell refused. The profile itself stays on the deliberate Full-shell ceiling clamped by ChildAuthority (#5186).
…le tool (R2) 'Approve for session' on a shell command stored the bare tool name in the session-approval set, so any later invocation of the same shell tool (tool_name clause in is_session_approved_for_tool) was auto-approved for the whole session — a one-command grant with whole-tool reach. - Store only the lossy grouping key (shell:<command family>, net:<host>, patch paths) in the session-approval set. - Bare tool names are never session-wide; the tool name is recorded as audit evidence under tool.approval.session_grant. - Flip the test that encoded the escalation: a tool-name grant no longer covers future calls of the same tool; a grouping-key grant does. Refs: codewhale-ops IMPROVEMENT-PLAN-0912 R2, #5123
#5610) Windows canonicalizes operands to \\?\-prefixed verbatim paths; that prefix is Win32 syntax, not a glob, but its '?' tripped both readonly classifiers' metacharacter gates and the POSIX tokenizer ate the backslashes. normalize_windows_command_paths strips the prefix for CLASSIFICATION ONLY (drive-letter and UNC forms both handled, quoting preserved, mid-token '?' left alone so wildcards still reject); the executed argv is never rewritten — real splitting stays in split_command_windows_style (#5595). Adapted from PR #5610, whose tools/shell.rs hunks are superseded on this branch. Co-Authored-By: Grok 4.6 <noreply@anthropic.com>
…uild with_mcp_tools captured the MCP catalog with a non-blocking try_lock at build time, so any in-flight MCP operation holding the pool mutex silently dropped EVERY MCP tool from the sub-agent registry — the child then ran its whole turn with no MCP tools and no error. The async caller (run_subagent) now captures an owned snapshot under a real .await (McpPool::all_tools_owned) and threads it through new_with_owner, so the builder never locks. Regression test holds the pool mutex across an await point and asserts all adapters still register. Co-Authored-By: Grok 4.6 <noreply@anthropic.com>
Three write paths bypassed the discipline every other writer follows: - legacy File write/edit actions skipped acquire_file_mutation, so they could interleave with contract-path writers on the same file; - review receipts wrote with plain fs::write — a crash mid-write could tear the receipt and make a completed review look unproven; - pandoc's --output wrote the requested path in place, so a failed or killed conversion left a torn half-written document there; it now stages to a .pandoc-partial sibling, renames into place on success, and removes the staging file on failure. Co-Authored-By: Grok 4.6 <noreply@anthropic.com>
The per-manager sequence restarts at zero, so a persisted agent resumed under a new manager reissued approval ids from an earlier lifecycle. Durable approval receipts (#5584) turn that collision into a live hazard: a stale receipt could auto-answer the new prompt. Ids now carry the manager's construction-time boot id (#405), which is fresh per manager and needs no new lifecycle source. is_child_approval_id routing is unchanged (substring contract), and ids stay unique within one manager and across two managers over the same state file. Closes #5615 Co-Authored-By: Grok 4.6 <noreply@anthropic.com>
The MCP server's chat tool hardcoded deepseek-v4-pro as its default model — wrong for every non-DeepSeek configuration and invisible until the call landed on a provider that doesn't serve that id. The default is now the runtime's configured model (explicit model argument still wins), and the schema description stops naming a provider-specific id. Co-Authored-By: Grok 4.6 <noreply@anthropic.com> Entire-Checkpoint: 01M0WKKGM9JQWTVE7D5RFKTTAA
The #5613 cherry-pick sequence landed docs/zh_hans/INSTALL.md in 5e558a5 and then deleted it again in 7b11cb5 ("add zh_hans translation for KEYBINDINGS"). Upstream's own KEYBINDINGS commit (582915f) only adds docs/zh_hans/KEYBINDINGS.md; the deletion was an artifact of how the pick was staged here, not anything the contributor did. The file survived untracked in the working tree and is byte-identical to 5e558a5, so this restores it verbatim. Shizuku's translation is now fully present on the integration branch, as #5613 intended. Co-Authored-By: Grok 4.6 <noreply@anthropic.com>
Takes dependabot #5537 by re-pinning the three call sites in release.yml and release-republish.yml. The action stays SHA-pinned, as this repo's supply-chain policy requires; only the pinned commit moves. The SHA was verified against upstream's own v4.3.0 tag via the GitHub API rather than taken on the PR's word: repos/docker/setup-buildx-action/git/ref/tags/v4.3.0 -> 37fe631027851001ddb9b187196cc803df7f5f0e v4.3.0 is a dependency-refresh release for the action itself (@docker/actions-toolkit 0.92.0 -> 0.95.0, brace-expansion, js-yaml, postcss, undici); no input or behavior change for how we call it. Co-Authored-By: Grok 4.6 <noreply@anthropic.com>
Takes dependabot #5540 and #5539 via `cargo update --precise`; both manifests already carry compatible caret ranges (`similar = "3"`, `rio-vt = "0.5.1"`), so only the lockfile moves. The rio-vt bump carries its sibling crates forward (corcovado, rio-grapheme-width, rio-graphics, teletypewriter 0.5.19 -> 0.5.26) and drops five stale transitive dependencies outright: fuchsia-zircon, fuchsia-zircon-sys, iovec, cfg-if 0.1, unicode-width-16, and windows 0.42. rio-unicode 0.5.26 replaces them. Net -122/+70 lockfile lines. Gated on a quiet tree at 04ebb13: cargo clippy -p codewhale-tui --lib --tests --locked clean cargo test -p codewhale-tui --lib --locked 11389 passed, 0 failed, 13 ignored #5387 (tower-http 0.6 -> 0.7) is deliberately left for 0.10: it is a breaking bump, not a patch refresh. Co-Authored-By: Grok 4.6 <noreply@anthropic.com> Entire-Checkpoint: 01M0WZHVVMQRX7AE4TCA2HDBR8
@wuisabel-gif found this while diagnosing the CI failure on PR #5608: the `r` in `/plugin t-r-ust` opened a raw-detail pager mid-command, which is what broke plugin_toml_binary_lifecycle_skill_and_stdio_mcp_acceptance on ubuntu and macOS. Their PR gated on `detail_target_cell_index().is_some()`, and the review of that PR treated the selection gate shipped in 32baa17 as the answer. It was not the whole answer. The selection gate refuses their exact case, but the same keystroke is still stolen by a different route: a transcript selection made with the mouse is never cleared by typing — only by resize, click-away, or an explicit command — so with a selection standing, the first `y`/`Y`/`r` of a typed message is consumed as a block action while `app.input` still reads empty. Typing "review this" left "eview this" in the composer and opened a pager. `transcript_block_actions_available` now also defers to an in-flight paste/typing burst. A keystroke inside a burst belongs to the composer; a standing selection still claims the key once the burst settles, so the feature is unchanged for its intended use. Two regression tests, both theirs in substance: - typing_burst_keeps_its_first_char_out_of_block_actions — pins the mechanism, and pins that the selection still wins after the burst ends. - typed_plugin_command_survives_a_standing_selection — walks the literal `/plugin trust demo` string they reproduced from CI. The first fails on 32baa17 as shipped, which is how we know their report described a real defect in our own implementation and not only in theirs. rustfmt --edition 2024 clean cargo clippy -p codewhale-tui --lib --tests --locked clean cargo test -p codewhale-tui --lib --locked 11391 passed, 0 failed, 13 ignored Co-Authored-By: wuisabel-gif <wuisabel-gif@users.noreply.github.com> Co-Authored-By: Grok 4.6 <noreply@anthropic.com> Entire-Checkpoint: 01M0X0C2TGGHSKV8PZS37K775N
…repo (#5617) Reported by @LmeSzinc with a complete and correct diagnosis: their own `git commit` intermittently failed with "Unable to create '.../.git/index.lock': File exists" while codewhale sat idle in the same repository. `git status` and `git diff` opportunistically refresh the index, and that refresh takes `.git/index.lock`. The agent's read-only shells already set GIT_OPTIONAL_LOCKS=0 (tools/shell.rs:4173, :5048) but none of the internal probes did — and the repository chrome probe runs `status --porcelain` against the user's repo every two seconds (event_loop.rs:3546 -> git_status.rs:104). That is the window a hand-run `git commit` collides with. Measured on git 2.51.0, 200 stat-dirty files: plain status: .git/index mtime 1787680138 -> 1787680148 REWRITTEN no-locks status: .git/index mtime 1787680148 -> 1787680148 UNCHANGED Rewriting the index is what takes the lock; suppressing optional locks removes the write, and with it our contribution to the contention. Two layers: - `impl ExternalTool for Git` now overrides `command()` to set GIT_OPTIONAL_LOCKS=0. Because `ExternalTool::output` and `::status` both build on `Self::command()`, this covers all 13 helpers that route through `Git::` — @git/@diff mentions, the model-callable git tools, review, verify, tasks, undo/diff commands, github cli prechecks, workspace context, runtime API, snapshot side-repo. Deliberately NOT on the trait default, so Gh/Cargo/Node/Python/RustC do not inherit a git-specific var. - The four sites that spawn `Command::new("git")` directly and therefore bypass the executor get it individually: the 2-second chrome probe (git_status.rs:159), the fleet profile drafter, subagent claim detection, and the `/init` project report. Every `git status`/`git diff` the product runs against a user repository is now lock-free. Genuine writes are unaffected: GIT_OPTIONAL_LOCKS suppresses only *optional* lock-taking, so add/commit/stash/update-ref still work, and `git diff --quiet` exit-code semantics are preserved — snapshot::repo depends on those for `/undo` cursoring. Two tests, because nothing pinned the empty env before: `git_command_never_takes_optional_locks` and `optional_lock_suppression_does_not_leak_to_other_tools`. Deliberately NOT taken from the report: trimming the periodic probe from 6 commands to 3. Of the six, only `status --porcelain` touches the index — the one that proposal keeps — so it removes 2 of 6 spawns and 0 of 1 lock-takers. Moving `rev-list --left-right --count` off the periodic probe would also freeze the header's ahead/behind counts, since `chrome_label` renders them every frame and only two call sites force a refresh. The file-watcher proposal needs a new dependency and an operator call; the non-git fast-fail lands separately. rustfmt --edition 2024 clean cargo clippy -p codewhale-tui --lib --tests --locked clean cargo test -p codewhale-tui --lib --locked 11393 passed, 0 failed, 13 ignored Co-Authored-By: Grok 4.6 <noreply@anthropic.com> Entire-Checkpoint: 01M0X0WVCF38NRTET3HS9AAYJ2
The two-second repository-chrome probe was re-running its full command set
on every single tick, because the staleness test compared the wrong two
paths:
g.root.as_deref() != Some(workspace)
`root` is the result of `rev-parse --show-toplevel` — the repository top
level — while `workspace` is where the session was launched. Those are
equal only when the session starts at the repo root. Launch codewhale from
any subdirectory (the workspace defaults to `current_dir()`) and that
clause is permanently true, so `CACHE_TTL` never applied and the probe ran
unconditionally, `git status --porcelain` included. That is a direct
amplifier of the index-lock contention fixed in b9fd283.
Snapshots now record `probed_workspace` — the probe's own input — and
staleness compares that. Extracted as a pure `snapshot_is_stale` so the
cache contract is testable without spawning git; nothing pinned it before.
Also fast-fails outside a repository. `probe_status` previously spawned a
doomed `rev-parse` on every tick forever in a non-git workspace, since the
negative result could never satisfy the broken predicate above. It now
checks `project_context::find_git_root` first, which walks parents and
understands the `gitdir:` pointer file — so linked worktrees and submodules
are still recognised, which a naive `.git` directory test would break. The
`rev-parse` fallback stays for what that cannot see, such as bare repos.
A later `git init` is still picked up: the negative snapshot expires on the
normal TTL.
This half was the reporter's part 4. They had the symptom right and the
cause slightly off — they read it as "the negative result is never cached",
where in fact the snapshot is cached and the predicate ignores it. They
also did not spot that the same one-line bug fires inside repositories, not
only outside them, which is the larger half.
Four tests: fresh-from-subdirectory is not stale, a different workspace is
stale, an unprobed snapshot is stale, and a non-git workspace fast-fails
while caching its own workspace.
rustfmt --edition 2024 clean
cargo clippy -p codewhale-tui --lib --tests --locked clean
cargo test -p codewhale-tui --lib --locked 11397 passed,
0 failed,
13 ignored
Co-Authored-By: Grok 4.6 <noreply@anthropic.com>
The module header claimed three things that were all false: that it prefers `gix` when available at build time, that it falls back to *a single* short-lived `git` invocation, and that the invocation has a hard timeout. There is no gix dependency anywhere in the graph, a probe runs up to six git invocations, and `git_output` sets no timeout at all. This is not idle tidying. @LmeSzinc reasoned about probe cost in #5617 from what this file says about itself, and what it said was wrong in the direction that understates the cost. Documentation that lies to a contributor doing careful work is a defect. Replaced with the actual command set, and a note that all of them now run with GIT_OPTIONAL_LOCKS=0. Co-Authored-By: Grok 4.6 <noreply@anthropic.com>
Reported by @rafaelcavalheri: both tools called the blocking run_git_command directly inside async execute(), parking the tokio worker for the duration of a git subprocess. Both declare supports_parallel() == true, and the engine drains an advertised-parallel batch from a single task (core/engine/tool_execution.rs), so an inline blocking spawn does not just stall this one tool - it serializes the entire batch. The first-call Git::command() resolution (one-time synchronous git --version probe, OnceLock-cached after) ran on the worker too. Fix is the contributor's: a run_git_command_async spawn_blocking wrapper, character-identical to the one git_history.rs has shipped since 346bfe3. Adaptation on landing: extended doc comment and two regression tests. Composition with b9fd283 (#5617) re-verified: the wrapper routes through run_git_command -> Git::command(), so GIT_OPTIONAL_LOCKS=0 stays attached. Pinned end to end by readonly_tools_do_not_rewrite_the_users_index: on a 200-file stat-dirty fixture a raw unlocked `git status` demonstrably rewrites .git/index (verified empirically on git 2.51.0, matching b9fd283's measurement) while both tools through the offload leave it byte-identical. The test skips rather than false-fails if a future git stops opportunistic index writes. async_offload_matches_the_sync_ path_byte_for_byte pins the wrapper as a pure offload (same status, stdout, stderr as the sync path). Honest limit, same as the 346bfe3 precedent: executor occupancy cannot be observed deterministically without timing asserts, which flake under the suite's parallel load, so the offload property itself is carried by the doc comment and review, not a test. #5595 check: touches only tools/git.rs; args are a Vec<String> handed to Command::args, no shell-splitting path is involved, and #5595's operand rules live in command_safety.rs/shell.rs, untouched. rustfmt --edition 2024 clean cargo clippy -p codewhale-tui --lib --tests --locked clean cargo test -p codewhale-tui --lib --locked 11399 passed, 0 failed, 13 ignored Co-Authored-By: rafaelcavalheri <rafaelcavalheri@users.noreply.github.com> Co-Authored-By: Grok 4.6 <noreply@anthropic.com>
…iscovery path
Operator report (2026-08-25): bare /mcp with 17 configured servers took
tens of seconds and then opened a read-only pager with no actions.
Cause, verified in code: /mcp mapped to McpUiAction::Show, which built a
second throwaway McpPool and ran connect_all() - a sequential for-loop
over every enabled server with a default 10s connect timeout each -
before the screen could open. The engine's live pool was already
connected; McpPool::manager_snapshot exists precisely to snapshot it
("Snapshot the live pool rather than starting a second discovery pool").
The resulting pager (PagerView::from_text) offered zero interactivity;
every mutation was a typed subcommand.
The Extensions modal already is the interactive management surface:
Hooks/Plugins/Marketplace/Skills/MCP tabs, instant open from config plus
the last live-pool snapshot, per-row [reload]/[enable] actions that
round-trip through these same /mcp verbs.
Changes:
- /mcp, /mcp status, /mcp list -> AppAction::OpenExtensions{tab: Mcp},
identical to /plugin's routing onto its tab. Instant.
- McpUiAction::Show removed (no remaining producer or consumer).
- Validate/doctor now route through the engine-owned live pool (the
reload path) instead of the UI-side discovery pool: the diagnosis is
the real pool reconnecting, which is what a doctor should be.
- discover_manager_snapshot_with_workspace_and_plugins deleted (its only
caller was the Show path). The cfg(test) discover_manager_snapshot
helper stays for spawn-error-chain tests.
- format_mcp_manager + open_mcp_manager_pager deleted (233-line module
reduced to the add_mcp_message helper; three format-only tests went
with them).
- handle_mcp_ui_action still refreshes app.mcp_snapshot /
mcp_configured_count / hotbar MCP actions after mutations, and now
opens the Extensions MCP tab for verb-driven flows.
Known follow-ups (next commits on this lane, not this one): parallel
connect_all (boot/doctor/runtime-api all still serialize), and a
non-mutating engine live-pool snapshot accessor so /mcp opens with real
statuses without waiting for or forcing a reconnect.
rustfmt --edition 2024 clean
cargo clippy -p codewhale-tui --lib --tests --locked clean
cargo test -p codewhale-tui --lib --locked 11396 passed,
0 failed,
13 ignored
(-3 vs 868fb7f: exactly the three deleted format-only tests)
Co-Authored-By: Grok 4.6 <noreply@anthropic.com>
connect_all was a sequential for-loop over get_or_connect, so every server paid the slowest server's spawn+handshake from its own budget. With the default 10s connect timeout and a large config (a 17-server setup reported 2026-08-25), the worst case was N x 10s before the pool was usable - at boot (engine mcp_tools), at /mcp reload, in doctor, and in the runtime API, all of which share this path. Now a two-phase connect: a cheap sequential pass decides who needs a connection (ready-checks, plugin-authority gates - both read live pool state, so they stay out of the spawned tasks), then the slow McpConnection::connect_with_policy calls run in a JoinSet, bounded by a semaphore of 8 so a config full of npx servers does not start one node runtime per server at the same instant (peak-memory regression the sequential loop never had; wall-clock is unaffected because the connect timeout dominates). Per-server connect timeouts are unchanged (enforced inside the handshake); one wedged server can no longer serialize the rest. Semantics deliberately preserved from the sequential loop: - config reloaded before the name snapshot (added-server-connects-on- first-call test still passes), - plugin-authority revocation drops + reports instead of reconnecting, - required-server sweep reports at most one error per name, never burying a real spawn diagnosis with the generic string, - a config edit landing mid-batch is reconciled by one retry pass: a content change drops every connection the pass inserted, so the second pass reconnects against the new config. - a panicked connect task is now contained (JoinError attributed to "connection task") where the sequential loop propagated the panic and took the whole pool down. Concurrency property carried by structure + doc comment (JoinSet vs for-loop), same as the 346bfe3 spawn_blocking precedent; the suite's existing connect_all behavior tests (reload-before-snapshot, one-error-per-required-server, spawn-error chains) pin the semantics. rustfmt --edition 2024 clean cargo clippy -p codewhale-tui --lib --tests --locked clean cargo test -p codewhale-tui --lib --locked 11396 passed, 0 failed, 13 ignored Co-Authored-By: Grok 4.6 <noreply@anthropic.com>
…es to command contract - CommandPluginContext: object-safe synchronous facet covering registry reads/mutations, async-bridged install/update/uninstall with sync receipts (D11), export, legacy scan, kimi managed import, and marketplace - Portable DTOs: PluginSummary/Detail/Diagnostic/McpServerDetail, mutation outcome+receipt, export receipt, legacy tool+scan, managed candidate+scan, marketplace catalog/candidate/add/state, suggestion - PLUGIN = 1 << 10 capability bit and one plugin envelope slot with with_plugin builder - Contract tests: object safety, field/variant closure, sync receipt outcomes, exact-hash mismatch, managed/marketplace portability, envelope transport, duplicate-slot rejection, bit stability - Contract boundary gate green; workspace compiles; fmt clean; 23/23 contract tests pass Generated with Claude Code
…misleading receipts Code-review finding: trust/enable/disable/revoke_trust returned a PluginMutationReceipt with outcome always NoChange, which is semantically wrong (NoChange means 'already up to date' in the install/update path). The host registry returns Result<(), String>; the handler renders the action word from its own dispatch arm and re-reads detail for post-mutation state. Return Result<(), String> — the exact-minimum typed surface. Generated with Claude Code
…tricted exposure - PluginAdapter implements CommandPluginContext against App: registry reads (summaries/detail/diagnostics/validation/suggest), registry mutations (trust/enable/disable/revoke with skill-cache + active-skill side effects), async-bridged install/update/uninstall with synchronous receipts (D11), export, legacy scan, kimi managed scan/install, marketplace state/add/remove/install (incl. builtin official catalog) - CommandContextBundle grows to eleven slots with plugin; contexts() exposes plugin only for PLUGIN capability - Portable conversion helpers: summary/detail/mcp server/diagnostic/marketplace diagnostic/mutation receipt/export receipt/legacy tool/marketplace candidate/catalog - kimi_import: scan_managed_plugins_portable wrapper; group modules made pub(crate); plugin_network_policy/run_async exposed - Adapter tests: host-data projection, registry mutation + suggest behavior, restricted exposure (3 tests) - Full TUI lib suite 11395/0; boundary gate green; fmt clean Generated with Claude Code
…acet parity - mod.rs: portable plugins() dispatch consuming workspace/presentation/plugin facets; legacy shell builds bundle and delegates (Phase 6 replaces with from_contract) - render.rs: render_bundle_detail/escape helpers consume portable PluginDetail + presentation facet - legacy.rs: consumes PluginLegacyScan; kimi_import.rs: consumes PluginManagedScan; marketplace.rs: consumes PluginMarketplaceState with localized plan text - Presentation facet: key_to_plugin_message_id maps all 52 plugin keys; source_path carried for marketplace provenance - Contract: PluginSuggestion.state_label, PluginDetail.inventory_summary, PluginMarketplaceCatalog.source_path, reload() facet method - Tests: 18 plugin tests converted to the portable shell path; full parity preserved - Full TUI lib 11394/0; contract 23/23; boundary gates green Generated with Claude Code
…nk both frontiers - PluginsCmd implements contract RegisterCommand<CommandResult> with exact WORKSPACE | PRESENTATION | PLUGIN; PluginsCommands group registers via ContextualCommand::from_contract - plugins_contextual destructures facets with safe missing-facet errors; transitional App shell now test-only - Public dispatch tests: exact capability set, undeclared facets absent, public seam dispatch, no-panic matrix (3 tests) - Remove plugins from PENDING_GROUPS and scripts/command-migration-topology.json frontier (same commit) - Migration fixture updated for six-group frontier; feat015 legacy-assertion test adds plugin to MIGRATED - All gates green: contract 23/23, TUI lib 11397/0, migration/boundary/CI fixtures + live gates, fmt, diff hygiene Generated with Claude Code
- Fix clippy findings in FEAT-020 plugin files: identical if blocks (contract.rs), useless as_ref/map (marketplace.rs), useless format + redundant closure (render.rs), manual unwrap_or_default (mod.rs), collapsible if (contract tests) - Boy Scout: repair pre-existing lints outside FEAT-020 scope (computer-use linux.rs &PathBuf->&Path, config catalog tests contains()/type_complexity) - cargo clippy --workspace --all-targets --locked -- -D warnings exits 0 with zero warnings Generated with Claude Code
rollback_hash_mismatch called crate::plugins::install::uninstall directly from the portable handler, a TUI-owned executable dependency that violates the D1 boundary and would break the FEAT-040 physical move. Add CommandPluginContext::uninstall_path(name, plugins_dir) - a file-level rollback removal with no registry resolution or skill side effects - and route the content-hash-mismatch rollback through it. The host adapter owns the crate::plugins call. Verified: contract 23/23, plugins group 18/18, plugin-scoped TUI suite 205/0, clippy -D warnings clean.
Merge Paulo Aboim Pinto's FEAT-020 plugins-group command-shape adoption into the v0.9.12 integration branch. Keeps the original seven commits (not squash) so authorship stays with Paulo. Not retargeted to main: this stack depends on FEAT-019 (MEMORY facet, PLUGIN = 1<<10).
| .await?; | ||
| loaded_session_id = Some(saved_id.clone()); | ||
| if output_format == ExecOutputFormat::Text && !json_output { | ||
| eprintln!("{}", exec_resumed_session_line(&saved_id)); |
| ) { | ||
| Ok(id) => { | ||
| if output_format == ExecOutputFormat::Text && !json_output { | ||
| eprintln!("{}", exec_saved_session_line(&id)); |
| .await | ||
| .map_err(StreamableSendError::Other)?; | ||
| let mut request = apply_safe_custom_headers( | ||
| with_default_mcp_http_headers(self.client.post(&self.url), true), |
| }; | ||
| let path = self.events_path(thread_id)?; | ||
| let mut base_seq = since_seq.unwrap_or(0); | ||
| let mut tail = VecDeque::with_capacity(tail_limit.min(RUNTIME_EVENT_REPLAY_BATCH_SIZE)); |
This was referenced Aug 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Status
Integration branch for the v0.9.12 cycle — now gated and code-complete for the release blockers; remaining pre-review work is version bump + changelog/RC gates (tracker: #5573). Not to be merged until those gates are green.
On this branch (72 commits)
Release blockers finished here:
/costtotals (owner-scoped runtime lease, sealed-mailbox preserved, exactly-once across worker/session/reload) —8343c89d5journal/usage/reportextraction, semantic-free moves verified by normalized-content SHA (3e788d6b5,be3a201f2,a4a788cd3,64545aeb9) — the broader lib.rs/config.rs/client.rs/runtime_threads.rs checklist stays openSafety & money: R2 approval grants match the command family · R3 Chat-Completions SSE error frames · R4 non-streaming read timeouts · R5 fleet per-task wall-clock · R7 worker self-reap on parent death · R9 typed task errors + fail-fast parallel/pipeline · S1 opt-in read deny-list (#5568) · S2 hard-link write guard (#5569) · fleet run-wide usage ceiling R6 (#5567) · per-step TurnUsage pricing (#5578) · cost-unknown footer chip
Subagents/coordination: #5562 stale write-claim liveness release +
coordinate release+ honest verifier description · in-workspacegit -Cread-only gate fix (#5595) · resumable turn-owned parking (#5596) · detached schema repair within runtime budgetCache/context: C1 wire-order tool fingerprint · C3 real cache-hit rate in
/context· C5 undeclared-drift assert + namedchange:tool_surfacedeclarations for mid-turn tool-surface mutations (#5571) · T1 meter/trigger alignment · #5577 named compaction refusals + the exact 842k/1M billed-window regression · honest reasoning-only length-stop failure · step-budget soft landing + report-on-exhaustion (A1/A2)Workflow: #5583 receipts/partial-success · R9 fail-fast ·
/workflow confirmgate fixUX: underwater state-reading surface (steady tints, activity shape, seam fix) · #5550 @path:START-END ranges · #5555 clipboard last-copy backup · #5549 wait_any/wait_all · double/triple-click composer selection · #5548 web dispatch-rejection coverage · #5564 REBRAND docs · #5579 plugin load-failure hints · OpenRouter attribution headers
Packaging: verified Omarchy/AUR path +
codewhale-tuicompatibility alias + updater respects package ownershipInfra/credit: T4 MCP 401/403 reactive OAuth refresh (#5572 — loopback QA path still tracked before close) · credit gate recognizes agent contributors · CLI diagnostics reworded to avoid secret-scanner false positives
Verification
Per-commit focused gates; fmt/
cargo check --workspace/clippy 0-warning at every landing. Current-head evidence: workflow wall 218/218 (isolated worktree) and 187/187 TUI slice at the merged head; #5597 multi-route acceptance (DeepSeek/Anthropic/Custom) 1/1 + adjacent 5/5 + cost sweep 90/90; the four previously failing engine tests (C5 tool-surface drift, reasoning-only length stop, zero-step host drain) fixed and green; cache_guard 9/9; adaptive-evidence integration 1/1; cucumber plugin e2e 16/16.Closes #5548
Closes #5562
Closes #5564
Closes #5567
Closes #5568
Closes #5569
Closes #5571
Closes #5577
Closes #5578
Closes #5579
Closes #5582
Closes #5583
Closes #5595
Closes #5596
Closes #5597
No-Issue: v0.9.12 integration and release train; #5572 stays open pending the loopback 401→refresh→retry QA proof, #5586 keeps its mega-file checklist, #5588 keeps its neutrality audit.