Skip to content

feat(providers): add MiniMax-M3 to the Minimax provider#652

Open
octo-patch wants to merge 822 commits into
agentforce314:mainfrom
octo-patch:octo/minimax-m3
Open

feat(providers): add MiniMax-M3 to the Minimax provider#652
octo-patch wants to merge 822 commits into
agentforce314:mainfrom
octo-patch:octo/minimax-m3

Conversation

@octo-patch

Copy link
Copy Markdown

Summary

Add MiniMax-M3 to the Minimax (Anthropic-compatible) provider configuration.

Changes

  • Add MiniMax-M3 to the model selection list in MinimaxProvider.get_available_models() and PROVIDER_INFO["minimax"] (listed first as the new flagship; existing M2.x models retained).
  • Register MiniMax-M3 in src/models/configs.py with a 1M context window, mirroring the deepseek-v4 / glm-5.2 1M entries. Also register an explicit MiniMax-M2 anchor: get_model_config's prefix fallback bases a key on rsplit("-", 1)[0], so an MiniMax-M3-only entry would base on the bare MiniMax and wrongly promote every MiniMax-M2* id to 1M — the anchor keeps the M2 line on its 200K default (same "both exact keys" guard the glm-4 vs glm-5.2 comment documents).
  • Add the minimax-m3 window to the display table in context_analyzer.py, ahead of the bare minimax fallback, so the status bar and the canonical registry agree at 1M (they must, per test_context_window_1m.py).
  • Add an MiniMax-M3 pricing tier in src/services/pricing.py (input $0.60 / output $2.40 / cache-read $0.12 per 1M tokens; no separate cache-write charge, so cache_creation mirrors input, as for DeepSeek).
  • Add unit tests for the M3 context window (display + canonical, plus a regression guard that the M2 line is not promoted) and M3 pricing.

Why

MiniMax-M3 is the new flagship model with a 1M context window; registering it lets context-window-aware logic (compaction triggers, token warnings) and the cost display use the real values instead of the 200K/unknown defaults.

Testing

  • pytest tests/test_context_window_1m.py tests/test_pricing_status_bar.py tests/test_provider_registry.py — 60 passed
  • pytest tests/test_minimax_abort_signal.py tests/test_providers.py tests/test_model_command.py — 65 passed

ericleepi314 and others added 30 commits June 26, 2026 22:57
…us bar (agentforce314#402)

* feat(ui-tui): match slash menu + footer to Claude Code

- Slash menu (SlashMenu.tsx): selected row is now a full-width highlighted bar
  (suggestion-blue bg, dark bold text, › prefix) with aligned name/description
  columns — matches PromptInputFooterSuggestions. Fixed a double-slash bug.
- Footer (StatusBar.tsx): left "? for shortcuts" hint + right "● model · mode"
  status (mode colored when non-default), matching PromptInputFooter's
  left-hints / right-status layout. Dropped the redundant key-hint line and the
  internal cc:// label from the footer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(ui-tui): match permission prompt to Claude Code

- PermissionDialog: top-rule-only frame in permission blue-purple (was a full
  amber box), "<tool> wants to run" + indented args preview + a numbered option
  list ("❯ 1. Yes (y)" / "2. No, and tell the agent what to do differently
  (n/esc)") — mirrors components/permissions/PermissionDialog.tsx. App accepts
  1/2 keys alongside y/n.
- Tool-call dot is now green (success status), matching the original.
- Hide the working spinner while a permission prompt is open.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(ui-tui): render Edit/Write tool diffs (green added / red removed)

- diff.ts: minimal line diff (trim common prefix/suffix, show changed middle)
  + toolDiff() for Edit (old→new), Write (all-added), MultiEdit.
- DiffView.tsx: line-number gutter + marker, added lines on diffAdded green
  bg rgb(34,92,43), removed on diffRemoved red bg rgb(122,41,54), context dim —
  mirrors the original StructuredDiff.
- Adapter carries the raw tool input; Message renders DiffView under Edit/Write
  tool calls. Capped at 40 lines with "+N more".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(ui-tui): WebFetch/WebSearch show the URL/query in link color

Render the URL (WebFetch) / query (WebSearch) argument in the blue-purple link
color so it reads as a link, matching the original's URL-aware tool display.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…gentforce314#403)

* fix(ui-tui): bound live stream to viewport (dup lines) + flush-left diffs

Two issues from terminal testing:

1. Duplicated lines. The live (non-Static) streaming region is the only
   unbounded part of the dynamic tree. When a streamed response overflows the
   viewport, Ink can't erase the scrolled-off rows, so every re-render — the
   spinner ticks ~10×/s — leaves a stale copy in scrollback, and a long message
   appears dozens of times. Fix: cap the live stream to a viewport-fitting tail
   (streamTail, plain text); the full syntax-highlighted markdown still commits
   to <Static> when the assistant message lands. Verified via a pyte
   HistoryScreen A/B (pre-fix: 2+ copies in scrollback; post-fix: exactly one).

2. Edit/Write diffs rendered too far right. Dropped DiffView's marginLeft and the
   leading gutter space so deeply-indented code isn't pushed off the right edge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ui-tui): keep the welcome banner in scrollback (was vanishing on first task)

The banner was a conditional render (entries.length === 0 ? <Banner/> : null),
so it disappeared the moment the first message landed. The original Claude Code
prints its logo once and lets it scroll into history.

Fix: commit the banner as the FIRST <Static> entry (a new 'banner' entry kind
carrying a session-info snapshot), so it's written to scrollback once and stays.
Because <Static> is append-only and indexed, the banner must be APPENDED before
any other entry — so submit is now gated on a `ready` flag set when system/init
arrives. This also fixes a race where a message sent during the (~20s) cold
start beat the banner into the list, which prepended the banner and duplicated
the first row. Input shows "starting agent-server…" until ready.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…tter (agentforce314#404)

* fix(ui-tui): wrap long diff lines instead of truncating

The diff renderer used wrap="truncate-end", so long edited lines (e.g. minified
config, SVG paths) were cut off the right edge. The original StructuredDiff wraps
them: a non-colored line-number gutter column + a colored content column where
long lines wrap across visual rows — each wrapped row keeps the +/- marker and
the green/red background, and the line number shows only on the first row. Also
added the "⎿ Added N, removed M lines" summary. Rows now fill the full width.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ui-tui): true file line numbers in diffs + marker hugs the code

Two diff-rendering corrections, grounded in the original StructuredDiff /
FileEditToolDiff:

- True file line numbers. The original computes the patch from the on-disk file
  (loadDiffData reads file_path, structuredPatch gives oldStart/newStart). We do
  the same: the adapter reads the edited file (best-effort, local in spawn mode)
  and offsets the diff line numbers to where old_string/new_string actually sits,
  instead of region-relative 1-based numbers. Falls back to relative if the file
  can't be read (e.g. attach mode). Verified: editing line 4 now shows "4", not 1.
- Marker hugs the content. Dropped the extra space after +/- so the code's own
  indentation lines up (e.g. "4 -const target", "+  if (...)") — matches the
  original gutter exactly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…isconnect) (agentforce314#405)

Bug: leaving `clawcodex tui` idle for a while printed `disconnected` and then
ignored all input. Root cause: the TUI↔backend link was a loopback WebSocket;
the Python `websockets` server enforces ping/pong idle timeouts and closes idle
connections, and the client had no keepalive/reconnect — after the drop, submits
silently wrote to a dead socket.

Fix: adopt hermes-agent's strategy — the local link is the spawned child's
stdin/stdout (NDJSON). A pipe can't idle-time-out. The agent dispatch was already
transport-agnostic, so this is a sibling transport, not a rewrite:

- Server (`agent_server_cli.py`): `--stdio` mode runs one session pumping
  stdin→`agent.send_to_agent` / `agent.messages_from_agent`→stdout, reusing
  `make_spawn_agent`/`AgentHandle` exactly as the WS pump does. stdout is
  reserved for JSON frames (banner prints suppressed); stdin EOF ends the session.
- Client: new `transport.ts` with a `Transport` interface — `StdioTransport`
  (spawn child `--stdio`, readline stdout→onData, stdin←send) and `WsTransport`
  (attach-to-remote, unchanged). `DirectConnectClient` delegates framing to a
  transport (`send` appends `\n`). `cli.tsx` uses stdio for local spawn, WS only
  for an explicit `cc://` URL. `App` takes a `transport`; banner cwd comes from
  `system/init`; on disconnect the input is gated (no silent dead-link submits).
- `tui_launcher.py` comments updated; orphaned `spawnBackend.ts` removed.

Verified: 45s-idle then a prompt still responds (WS would have dropped); default
stdio spawn + full `clawcodex tui` path work e2e; attach/WS preserved; server
suite 80 passed (incl. a new stdio lifecycle test); ui-tui typecheck+build green.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ct reads (agentforce314#406)

Studied the original Claude Code via a multi-tool filmstrip: it collapses
repeated tool calls into a live "● Reading N files…" summary (count updates in
place) and keeps file reads collapsed. clawcodex showed each Read as a separate
block dumping content, with only a generic "Working…" spinner. Two changes:

- Live tool-progress in the spinner: the working indicator now shows the current
  tool activity — "Reading README.md" for one, "Reading 4 files" (collapsed
  count) for several — updating as each tool call streams in, instead of a static
  random verb. App tracks per-turn tool counts by verb; cleared on result / new
  turn (Spinner gains an `activity` prop).
- Compact tool results: a long line-numbered file dump (Read / cat -n) collapses
  to "⎿ Read N lines" instead of dumping 8 lines per call — several reads no
  longer bury the transcript (the original keeps these collapsed).

Verified via filmstrip captures: spinner goes Working… → Reading 4 files…; a
single big read shows "⎿ Read 651 lines" + "Reading README.md…".

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…reads) (agentforce314#407)

Follow-up to agentforce314#406 — the exact original behavior. Repeated Read-like calls
(Read/Glob/Grep/LS) now collapse into a single in-place block that updates as
the turn runs — "● Reading 4 files… └ <current>" — and freezes into a collapsed
"● Read 4 files" summary when the round ends, instead of N verbose blocks each
dumping file content. The spinner stays a generic verb beside it (matching the
original's "Reading N files" block + separate working spinner).

Implementation (no double-render — the deferred-commit the note called for):
- Adapter exposes tool_use ids: tool entries carry `toolUseId`, tool-result
  entries carry `forToolUseIds`; collapsed-summary entries carry `count`.
- App keeps a live read-group ref (NOT Static); Read-like calls accumulate
  there, their results are dropped, and the group is frozen into a committed
  summary the moment a non-read entry (e.g. the assistant's reply text) arrives —
  preserving order (tools before the summary).
- New LiveTools.tsx renders the block; toolMeta.ts holds the shared verb/noun
  map used by the block, spinner, and committed summary. Non-read tools
  (Bash/Edit/…) still commit individually with their output/diffs.

Verified via filmstrip: live "● Reading 4 files… └ package.json" during the
turn → committed "● Read 4 files" + the summary, in order.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…gentforce314#408)

`clawcodex tui` failed with "Cannot find package 'react' from
…/.bun/install/cache/…/react-jsx-dev-runtime.development.js" — the launcher ran
`bun run src/cli.tsx`, and bun mis-resolves react/jsx-dev-runtime from its global
auto-install cache. Since the install path builds the dist, prefer
`node ui-tui/dist/cli.js` (standard node resolution, no JSX-runtime quirk);
`bun run src` stays as the no-build fallback when no dist exists.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…bash (agentforce314#409)

The installer set up only the Python side; `clawcodex tui` needs node + the
built ui-tui dist + node_modules, so it failed on a fresh install. Added an 8th
step:
- provision_node(): reuse an existing node/npm, else fetch the official Node
  binary (no sudo) into ~/.clawcodex/node and link node/npm/npx into ~/.local/bin
  (already on PATH). Platform/arch detected (darwin/linux × arm64/x64).
- build_tui(): `npm install && npm run build` in the cloned ui-tui (non-fatal —
  the REPL works without it). The launcher already prefers node+dist (agentforce314#408).
- `clawcodex tui` added to the post-install next-steps; `update` rebuilds too.

Verified: Node tarball downloads+runs (v22.12.0); `--dry-run` shows step 8/8 +
the npm build; `npm install && npm run build` produces ui-tui/dist.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
agentforce314#410)

Matched the original Claude Code diff renderer (verified by driving the same
edit through openclaude and comparing). Three fixes to DiffView:

- Syntax highlighting: each diff line is highlighted via cli-highlight (language
  from the file extension). The add/del background is baked into the ANSI and
  re-applied after every syntax reset (\x1b[0m → reset+bg), so a mid-line reset
  no longer punches a hole in the tint. Rendered as a raw-ANSI string in <Text>
  (same trick markdown.tsx uses), not Ink's backgroundColor (which the resets
  would clear).
- Tight tint: the background extends only to the LONGEST line in the hunk
  (capped at the terminal width), not the full terminal — short hunks get a
  compact block instead of a full-width bar. Long lines wrap ANSI-aware
  (wrap-ansi), keeping marker + tint on each row.
- Context lines: Edit diffs now show ~3 unchanged file lines above/below the
  change (diff.ts withFileContext), for orientation — the original shows these.

DiffView takes the file path (for language detection) from the tool input.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…pdates (agentforce314#411)

A prior install's `uv sync` re-pins the tracked uv.lock in place. That local
change makes `git pull --ff-only` fail, so on every subsequent
`curl … | install.sh | bash` the updater warned "continuing with existing code"
and silently kept the old version — re-running never picked up new commits.

clone_or_update_repo now restores the installer-managed uv.lock before pulling
(the dir is a managed mirror, not a working copy), and falls back to
`fetch --depth 1 + reset --hard origin/<ref>` when even that can't fast-forward
(shallow clones can't always FF). Verified: a checkout pinned at the prior
release with a dirty uv.lock updates cleanly to latest.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
… terminals (agentforce314#412)

Two issues in the real TUI that my pyte captures masked:

1. No syntax highlighting. cli-highlight (chalk) emits color only when it detects
   color support; in the launcher-spawned TUI that resolves to "no color", so the
   highlighted code dropped to plain text — flat, unreadable on the diff's
   hardcoded rgb tint. (A PTY always reports a TTY, so captures still colored.)
   Fix: new forceColor.ts sets FORCE_COLOR=3 before chalk/Ink load (imported
   first in cli.tsx), respecting an explicit NO_COLOR / FORCE_COLOR. The TUI
   always renders to a terminal and Ink already emits truecolor.

2. Full-width tint on wide terminals. The block is padded to the longest line in
   the hunk; one long line (or a 200-col terminal) stretched the tint across the
   whole screen, giving short lines giant bars. Fix: cap the content column at
   MAX_DIFF_WIDTH=120 — a no-op on normal terminals, it bounds the worst case so
   long lines wrap and there's a real right margin.

Verified at COLS=200 with a long-line edit: highlighted code, blocks bounded to
~120 with a right margin, long body wraps.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
… word-level) (agentforce314#413)

Replace the hand-rolled diff tinting with a verbatim port of openclaude's
pure-TS ColorDiff/ColorFile — the SAME module the original Claude Code TUI uses
(typescript/src/native-ts/color-diff) — so the diff body is faithful by
construction instead of an approximation that drifts: dimmed line-number gutter,
+/-/space markers, red/green line tints, highlight.js Monokai syntax colors, and
word-level diff highlighting.

- colorDiff.ts: verbatim port (string-width, ESM createRequire for highlight.js,
  tolerant of hljs 10 `emitter` / hljs 11 `_emitter`).
- patch.ts: port of getPatchForDisplay/getPatchFromContents (structuredPatch,
  &/$ escaping, 3 context lines).
- diff.ts: buildToolDiff -> Edit/MultiEdit render as Update/Create diff; Write
  renders highlighted new content (ColorFile), not an all-green diff.
- DiffView.tsx: width = cols-12, summary "Added N lines, removed M lines",
  no dashed frame -- matches FileEditToolUpdatedMessage exactly.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…mer (agentforce314#414)

Replace the braille spinner with the original Claude Code spinner look:
- oscillating glyph cycle `· ✢ ✳ ✶ ✻ ✽` (then reversed) in Claude orange,
  advancing every 120ms (SpinnerGlyph's SPINNER_FRAMES), per-platform chars.
- a random whimsical verb per turn from the full SPINNER_VERBS list (186
  verbs, ported verbatim from constants/spinnerVerbs.ts).
- a glimmer: a brighter highlight sweeps across the verb (GlimmerMessage).
- dim `(elapsed · esc to interrupt)` status.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…agentforce314#415)

Make the permission prompt faithful to the original (components/permissions):
a top-rule box in the permission blue-purple, a bold tool title + dim subtitle,
and a TOOL-SPECIFIC preview instead of raw key:value text:
- Edit/MultiEdit/Write -> the actual colored diff (reuses ColorDiff via DiffView;
  permission runs pre-apply so the on-disk file gives a real diff).
- Bash -> the highlighted command + description subtitle.
- Fetch/Search/Read/Grep/Glob -> titled with the url/query/path/pattern.

Two options (Yes / No) since the wire protocol is allow/deny; App maps the keys.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…gentforce314#416)

GFM tables previously fell through to paragraphs and rendered as raw `| a | b |`
pipes. Add a focused table renderer matching the original Claude Code look
(components/MarkdownTable): box-drawing borders, bold header, per-column
alignment (:--/--:/:-:), proportional column shrink to fit the terminal, and
cell wrapping. Inline markers are stripped inside cells.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Add the original's @-typeahead: typing `@<partial>` at the end of the input
opens a file-suggestion dropdown.

- fileIndex.ts: bounded, cached recursive walk of the cwd (skips vendor/heavy
  dirs, capped); searchFiles ranks basename-prefix > basename-substring >
  path-substring, then shortest path.
- FileMenu.tsx: dropdown mirroring SlashMenu — dir dimmed, basename emphasized,
  selected row a full-width highlighted bar.
- App.tsx: detect the @token, ↑/↓ to navigate, Tab/Enter to complete to
  `@path `. Mutually exclusive with the slash menu.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…gentforce314#418)

The diff library emits a literal `\ No newline at end of file` line in
hunk.lines for non-newline-terminated content (common for inputs-only edit
diffs from old_string/new_string snippets). ColorDiff rendered these as context
lines, which both looked wrong and threw off line numbering. Filter them in
getPatchForDisplay/getPatchFromContents — real code lines are prefixed
+/-/space, so only the markers start with a literal backslash.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The original distinguishes success/error tool-result variants; the client
rendered every result dim. Thread tool_result `is_error` through the adapter and
render errored results in the error color (not collapsed — the message is what
the user needs to see).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…orce314#420)

End-to-end port of the original's StatusLine context %.

Backend (agent_server.py): implement the get_context_usage control request
(was a stub) — compute real usage via context_system.analyze_context over the
session's conversation + system prompt + tool schemas; reply with
total_tokens / max_tokens / percentage / categories. Best-effort: any failure
degrades to just the protocol version so a pull can never break the session.

Client:
- client.ts: request/response correlation (requestControl) so the client can
  PULL control responses (control_response was previously filtered).
- App.tsx: pull get_context_usage on init and after each turn; store usage.
- StatusBar.tsx: show `NN% (used/max)` colored by headroom (dim <70, amber
  70-90, red >=90), before model · mode.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…tforce314#421)

The agent's TodoWrite tool_use already flows to the client (TodoWrite is in the
agent-server registry); render it as the original's checklist instead of a
generic tool call:
- ⏺ Update Todos header
- ✔ completed (green, struck through, dim), ◼ in-progress (orange, bold),
  ◻ pending — matching TaskListV2's getTaskIcon + styling.
- drop the noisy "Todos have been modified" tool result (the list is the output).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…314#422)

Add the original's /context (ContextVisualization). Uses the get_context_usage
control pull (implemented server-side in the prior PR), so it's end-to-end:
- /context pulls a fresh usage snapshot and renders a breakdown entry.
- ContextView: a total bar colored by headroom (green/amber/red) with
  `NN% · used/max tokens`, then per-category proportional bars + token counts.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…314#423)

When the agent spawns a subagent (the Agent tool, legacy "Task"; registered in
the agent-server by default), render it as a distinct Task card instead of a
generic tool call:
- ⏺ Task(<description>) in the permission/subagent blue-purple, with the
  subagent_type as a dim badge and an optional @name.
- the subagent's final output flows as the normal tool result below it.

Live subagent progress (the original's AgentProgressLine) needs the agent-server
to stream nested-loop progress events (a ToolContext progress hook + a new
message type) — a follow-up backend chapter.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…rce314#424)

End-to-end port of the original's live subagent progress.

Backend (additive, defensive — no hook means no behavior change):
- ToolContext.agent_progress_emit hook (optional).
- agent-server wires it (_emit_agent_progress) to forward an `agent_progress`
  message to the client.
- the Agent tool sets run_params.on_message (run_agent calls it per subagent
  message; covers sync + background paths) → folds each message into a
  ProgressTracker and emits {agent_id, name, description, subagent_type,
  activity, tool_use_count, tokens, status}.

Client:
- App handles `agent_progress`: upsert a live line per agent_id, cleared at
  turn end / new prompt.
- AgentProgressLine: ⏺ task · activity · N tools · K tokens (subagent
  blue-purple, @name for named agents), rendered in the dynamic region.

Tests: 80 server + 170 agent/tool-system pass (backend is purely additive).
Client rendering screenshot-verified with synthetic events; live end-to-end
runs in a model-backed session.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Port the original's prompt history navigation (inventory §1): when no
slash/@ menu is open, ↑ walks back through submitted prompts and ↓ forward,
preserving the in-progress draft at the bottom (readline-style). Consecutive
duplicates are de-duped; typing exits history browsing.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…314#426)

Port the original's queued commands (inventory §1). Previously a prompt
submitted mid-turn was dropped; now it's queued, shown above the input
(dim "⏎ …"), and sent automatically when the turn ends — one per turn, in
order. Implemented with a dispatchPrompt helper + a drain effect on (busy,
ready, permissions).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Port the original's incremental reverse-search (inventory §1). Ctrl+R opens a
`(reverse-i-search)` prompt; typing filters submitted history to the newest
match, Ctrl+R cycles to older matches, Enter accepts into the input, Esc
cancels. Builds on the ↑/↓ history added previously.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
End-to-end port of the original's /compact.

Backend (agent_server.py): a new `compact` control request → _do_compact runs
compact_service.compact_conversation (summarizes older messages, mutates the
conversation in place) and replies with tokens_saved / pre / post counts.
Idle-guarded: refuses mid-turn (the worker mutates the conversation during a
turn), so no race. Defensive try/except.

Client: /compact [instructions] slash command → requestControl('compact')
(120s timeout for the summary call) → renders "Compacted N → M messages ·
saved Xk tokens" and refreshes the context-usage indicator; errors surface.

Server tests: 80 pass (additive). Live summary runs in a model-backed session.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…#429)

End-to-end port of the original's cost display.

Backend (agent_server.py): compute per-turn USD via src.services.pricing
.compute_cost(model, usage) and include total_cost_usd on the result message
(best-effort; 0.0 for unpriced models — no guessing).

Client: result line shows "· $X.XX" when cost > 0; App accumulates a session
total and StatusBar shows it ("$X.XX · NN% (used/max) · model · mode").
Hidden for unpriced models so it never shows a misleading $0.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Port the original's AssistantThinkingMessage. The agent-server already passes
thinking/redacted_thinking content blocks through (message_to_dict → _sdk_envelope),
so this is client rendering: the adapter extracts thinking blocks (before the
spoken text) into a 'thinking' entry; Message renders "∴ Thinking" (dim italic)
+ the reasoning (dim italic, indented, capped with a "+N more lines" note).
Redacted thinking shows a placeholder. Renders whenever the provider emits
thinking (extended-thinking models with thinking enabled).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Port the original's theming (dark + light palettes from utils/theme.ts). `theme`
becomes a mutable live object; applyTheme(name) swaps the palette in place and a
top-level re-render repaints the dynamic UI + new output (printed scrollback
keeps its colors, like a real terminal).

- /theme <dark|light> switches live; $CLAWCODEX_THEME sets it at startup.
- DiffView passes currentThemeName() to ColorDiff/ColorFile, so diffs follow the
  theme (light → GitHub syntax + light tints; dark → Monokai + dark tints).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
ericleepi314 and others added 25 commits July 3, 2026 10:34
…blob (agentforce314#628)

WebSearch fell through formatToolResult's per-tool branches to raw
passthrough, so the transcript printed the entire model-facing snippet
blob (dozens of wrapped rows per search). The original renders ONE line
(WebSearchTool/UI.tsx renderToolResultMessage): "Did N searches in Xs".

- agent-server: _display_tool_result also recognizes the WebSearch
  output shape (query/results/duration_seconds, no "type" key) and
  forwards {type:"web_search", durationSeconds, searchCount} on the
  tool_use_result envelope — searchCount per getSearchSummary
  (non-string results entries), bool-duration lookalikes rejected.
- ui-tui: formatToolResult WebSearch branch renders the exact original
  string from the envelope (whole seconds >=1s, else ms); without an
  envelope (older backend) the count is recovered from the blob's
  "Links: [" / "No links found." marker lines and duration is omitted.
  Errors keep the error path; the full blob stays behind ctrl+o via
  unchanged result_raw retention.

Critic-reviewed: APPROVE (shape detection surveyed against all other
dict-producing tools; four wire-compat cells verified).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… model id (agentforce314#629)

The /model picker (and typed /model) always printed "error: invalid
response: model switch": the gatewayClient adapter fire-and-forgot the
set_model control and resolved {ok:true} while both callers require
ConfigSetResponse.value. Worse, the hermes /model grammar
("<model> --provider <slug> --global|--tui-session") was forwarded
verbatim, so the backend set and PERSISTED a garbage model id like
"deepseek-v4-pro --provider deepseek" (re-seeded at every launch).

- gatewayClient.setModel: parse the grammar (--provider consumes the
  next token; scope flags dropped — persistence is unconditional),
  round-trip set_model, map {ok,model,warning?} -> {value,warning?},
  throw the backend error into the catching rpc() wrapper on refusal.
- agent_server._do_set_model: validate missing model / not-ready /
  cross-provider mismatch, error instead of swallow+ack on setter
  failure, echo the resulting model, warn when it's outside the
  provider's get_available_models() list.
- startPromptLiveSession: don't double-print on a null rpc result.

Verified by 5 new adapter tests, a new server control test, and a live
pyte e2e (picker + typed + mismatch paths, real deepseek backend).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…agentforce314#630)

User report: 'there is no slash command "/skills". Please add it back.'

The hermes TUI port (agentforce314#572) shipped a full /skills implementation (ops.ts
+ SkillsHub overlay) but it was invisible and inert against clawcodex:

- the completion menu + /help catalog are fed by the hardcoded SLASHES
  list in gatewayClient, which lacked /skills (same class as /exit agentforce314#625)
- its skills.manage / skills.reload RPCs hit the adapter's default case
  and resolved {}, so even typed blind the hub said "no skills available"
- /skills <unknown-sub> fell through to the workflow fallback ("isn't
  wired into the clawcodex backend yet")

Fix: SLASHES entry; skills.manage list/inspect/search served from a
TTL-cached list_skills control (install/browse reject honestly — no
community-skill backend in clawcodex); skills.reload busts the cache and
reports the count; catalog skill_count fills lazily from the warm cache
so startup doesn't pay a disk scan; dispatchSlash 'skills' returns a
usage hint instead of the workflow fallback. Server: list_skills payload
gains category (settings scope beats loader bucket) + path, entry cap
120→1000, description cap 80→400.

Verified live (PTY drive, real backend + built dist): /sk completion
entry, hub with bundled·5/user·535 categories, list/inspect/search
panels, reload "Re-scanned skills: 540 available.", and the
unknown-name / install / unknown-sub error paths. tsc clean;
gatewayClient suite 47/47 (7 new); 3 new server tests pass; full-suite
failures confirmed pre-existing via stashed-tree run.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ghost text (agentforce314#631)

User report (/goal): slash commands had no value suggestion — the original
shows e.g. "/goal [<condition> | clear]" as you type.

Ported the original's model (typescript/: Command.argumentHint →
useTypeahead's exactly-one-trailing-space gate → BaseTextInput dim ghost):

- SlashCommand.argumentHint on 34 TUI-local commands (grammars derived from
  each run()'s actual parsing + usage strings), hint on 10 gateway SLASHES
  entries (values validated against agent_server handlers: PERMISSION_MODES,
  set_effort, set_thinking, knowledge), and workflow argument_hint passed
  through from list_workflow_commands.
- complete.slash items + commands.catalog gain hint data; the catalog's
  hints map is the ghost-text lookup source for gateway/workflow commands.
- Completion menu rows render the grammar dim between name and description:
  "/effort [minimal|low|medium|high|auto|ultracode] Set reasoning effort…".
- TextInput gains an argumentHint prop appended dim after the value (never
  part of it); shown only while the input is exactly "/name " — appears on
  the space keystroke, clears on the first real argument.
- Precedence rule the original doesn't need: the TUI-local registry's hint
  wins over the gateway's (dispatch consults the local registry first), so
  shadowed names (/compact, /model, /bg, /resume) show the grammar that
  actually runs; those SLASHES entries deliberately carry no hint.

Deliberate deviation: clawcodex's applyCompletion inserts "/name" without a
trailing space (original inserts "/name "), so the ghost appears one
keystroke later — at the user's space — rather than changing completion
semantics.

Verified live (PTY drive, built dist): menu row hint for /eff, ghost at
"/effort " on the composer line, cleared at "/effort m", local /skills +
/pet ghosts, workflow "/deep-research <question>" row, no ghost for unknown
commands, hidden at two trailing spaces. tsc clean; eslint clean on touched
files (--fix also normalized pre-existing padding warnings in
gatewayClient.ts/appOverlays.tsx); 1218 tests pass (12 new), same 8
pre-existing env-baseline failures.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ule) (agentforce314#632)

PR agentforce314#631 accidentally swept in an unrelated in-progress hunk from the
working tree: complete.path was rewritten to import
./lib/fileSuggestions.js — a file that exists only as untracked WIP and
was never committed. Merged main fails typecheck/build:

  src/gatewayClient.ts(24): error TS2307: Cannot find module
  './lib/fileSuggestions.js'

(The pre-merge verification passed because the untracked file satisfied
the import in the working tree — the commit itself wasn't self-contained.)

Restore the pre-agentforce314#631 complete.path exactly: the readdirSync-based
completePath method, its node:fs / node:path imports, and the simple
case body. The file-mention feature can land later WITH its module.

Verified in a clean worktree (no WIP files): tsc clean (fails with
TS2307 before this fix), esbuild build succeeds, gatewayClient +
argumentHints suites 59/59, eslint clean. Diff vs pre-agentforce314#631 main now
contains only the argument-hints feature.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…input box (agentforce314#633)

After a turn whose transcript scrolled past the viewport, typing the
next query rendered on the footer row ("? for shortcuts" line) instead
of the composer input row, with the stale footer left on screen.

Root cause: inline sessions start below pre-existing shell output, so
frame rows scroll into scrollback earlier than LogUpdate's
height-vs-viewport arithmetic predicts. A repaint of a row in that
phantom band emits a cursor-up that clamps at the viewport top; every
later relative write — and the parked displayCursor that seeds all
future frames' relative moves — lands one row low, permanently (main
screen has no per-frame CSI H self-heal).

Fix: track the frame-end cursor's physical viewport row across frames
(LogUpdate.physCursorRow; LF and auto-wrap pin at the bottom margin)
and derive scrollback reachability from it. The 0-seed only ever
under-estimates, so the guard is conservative (a relative move can
never clamp) and converges to exact at the first bottom-margin pin.
Also: never park the declared cursor above the physical viewport top,
and re-anchor on clearTerminal / forceRedraw / SIGCONT.

Verified: new regression suite drives the real pipeline (React →
renderer → log-update → optimizer → terminal writer) against a strict
VT emulator across a full turn lifecycle — 8 scenarios including the
exact reported repro (fails before, passes after) and a ctrl+L
re-anchor pass. Fork suite 118 tests green; tsc + eslint clean; real
PTY drive with a live model turn confirms typing lands on the prompt
row after an overflowing turn.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…agentforce314#634)

The src/coordinator/ module (chapter-10 port) was inert — 2 of its 15 TS
wiring seams were connected; CLAUDE_CODE_COORDINATOR_MODE=1 changed nothing
the model saw. Now, mirroring the TS seams:

- system prompt: the orchestration prompt REPLACES the base blocks
  (systemPrompt.ts:63-75); style still appends; trailing workspace/git/
  CLAUDE.md context block kept and extended with workerToolsContext
  (QueryEngine.ts:300-306), incl. MCP server names + scratchpad note
- tools: non-mutating main-loop registry view narrows to Agent/SendMessage/
  TaskStop/StructuredOutput + PR-activity MCP tools (toolPool.ts) at all 5
  consumption points (server emit_init/_context_usage/turn; headless init/
  turn); workers keep the Agent tool's captured FULL registry
  (AgentTool.tsx:568-575)
- agents: get_built_in_agents() swaps to [worker, general-purpose, Explore,
  Plan] so subagent_type "worker" resolves (builtInAgents.ts:35-43)
- Agent tool: model param ignored, spawns forced async, slim tool prompt
  (AgentTool.tsx:252/562, prompt.ts:206-211)
- sessions: _save_session stamps "mode"; _do_resume match_session_mode
  (single_session-gated — the env flip is process-global and must not
  contaminate sibling --http sessions) + banner + cached-prompt rebuild +
  init re-emit; gatewayClient shows the banner
- mode.py set fix (+StructuredOutput, PR-activity suffix exemption) and
  "via the Agent tool" byte-parity; ASYNC_AGENT_ALLOWED_TOOLS +=
  NotebookEdit, ToolSearch (constants/tools.ts:53-69) w/ snapshot refreshes

Off-path (env unset) unchanged — pinned by a reference-composition test.
tests/coordinator/ 76/76; live NDJSON smoke green both ways; critic loop:
gap+plan 2 rounds each, implementation APPROVE after 1 revision round.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…dio server (agentforce314#635)

* feat(entrypoints): mcp serve — re-expose clawcodex tools as an MCP stdio server

Port of typescript/src/entrypoints/mcp.ts (startMCPServer), the headline gap
of the entrypoints/ parity phase (G1): Python's `mcp` handler only had
`list`; there was no way to expose this port's tools to another MCP host.

- src/entrypoints/mcp_serve.py: mcp.server.Server over stdio; ListTools from
  the registry (tool.prompt() descriptions, JSON-schema inputs, no output
  schemas — none declared); CallTool via registry.dispatch (schema
  validation + validate_input + the full permission pipeline); result
  mapping text/image/json per mcp.ts:199-227; configured MCP servers
  re-exposed via get_mcp_tools_commands_and_resources with MCP-wins name
  dedup (mcp.ts:46-54, registry evict-and-replace).
- SECURITY (plan P1, critic-caught): the synthetic serve context pins
  ToolPermissionContext() — mode "default", empty rules, bypass UNAVAILABLE
  — because ToolContext's default factory is bypassPermissions, which would
  have run the server with all gating off. No permission handler → ask-
  requiring tools fail CLOSED (mirrors TS getEmptyToolPermissionContext,
  Tool.ts:142). Live-verified: in-workspace Glob executes, Write denied,
  out-of-workspace reads denied (read-permission port composes).
- `serve` verb in entrypoints/mcp.py (engine imported only in its branch —
  `list` lean-contract preserved and now pinned by test).
- G12: CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS defaults to "true" at both
  per-process entries (cli.main, agent_server_cli) mirroring cli.tsx:44 —
  contract for the ch04 betas docket; gates only the unwired tool-search
  mode selector today.

tests/entrypoints/test_mcp_serve.py (14): posture pins, dedup, content
mapping, memory-transport round-trips (list/call/deny/validation/unknown/
out-of-workspace), verb usage, lean contract, betas defaults both entries.
Live stdio smoke: real subprocess + real MCP client — initialize handshake,
39 tools, in-workspace Glob, fail-closed Write.

Gap analysis + plan: my-docs/get-parity-by-folder/entrypoints-* (critic
rounds: gap REVISE→revised; plan REVISE w/ 2 BLOCKING → posture pinned +
teams-cleanup deferred + two-PR split).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mcp-serve): critic round — event-loop freedom, re-exposure grant + test

- dispatch via asyncio.to_thread (critic M1): sync dispatch inline froze the
  MCP server loop for the tool's duration; the worker thread has no running
  loop so async tools take the clean asyncio.run path (TS's await is
  cooperative — this is the Python equivalent).
- re-exposure grant (found writing critic M2's test): mcp__* tools are
  ask-gated in this port (PR agentforce314#347), so the fail-closed posture denied them —
  dead on arrival where TS serves them ungated. Content-less session allow
  rules for exactly the re-exposed names (configured servers are the user's
  grant; C7 approval gate already filtered unapproved) — builtins stay
  fail-closed. Integration test drives the REAL chain: loader → registry
  evict-and-replace on builtin collision → list surfacing → dispatch of
  both, Write still denied.
- betas setdefault to module scope in agent_server_cli (critic m4 —
  cli.tsx:44 placement, beats import-time env capture); test rewritten as
  subprocess import checks.
- documented: disabled-tools hidden from ListTools (intentional divergence;
  TS lists + rejects at call), alias-collision fallback, plain-list mapping.

tests/entrypoints: 15/15.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(mcp-serve): honor settings deny rules; serialize dispatch (critic minors)

- _build_serve_context grafts the settings tiers' DENY rules onto the
  pinned posture (deny precedes allow → beats the re-exposure grant; a
  C7-approved server with `deny: mcp__x__dangerous` in settings is honored).
  ONLY deny rules — importing allow rules or mode would widen the serve
  surface. Best-effort: broken settings leave the fail-closed posture.
- dispatch_lock (asyncio.Lock) around the to_thread dispatch: the shared
  ToolContext is mutable and not thread-safe; sequential execution restores
  the pre-to_thread profile while the loop stays free for pings (the two
  concerns M1 separated).
- test: deny-beats-grant end-to-end (denied tool errors + never runs;
  sibling benign tool from the same server still executes). 16/16.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… paths (agentforce314#636)

ENTRY-2 (gap G2, port of validateProviderEnvForStartupOrExit — cli.tsx:149,
providerValidation.ts:479-528): surface a broken provider configuration at
startup instead of deep inside the first API call. Function-at-altitude port
against this port's own provider registry (TS's validator is built on the
integrations descriptor subsystem, which has no Python analog).

- src/entrypoints/provider_validation.py: side-effect-free
  get_provider_validation_error (unknown provider; key-requiring provider
  with no key — byte-compatible with the messages headless printed inline)
  + validate_provider_at_startup with the TS exit split: non-interactive →
  print + exit(2), interactive TTY → WARN AND CONTINUE (TS does not kick
  interactive users out; the TUI surfaces the repair path).
- THREE call sites, ONE implementation (critic P3/P6):
  cli.main() after permission resolution (covers bare interactive + -p);
  _run_tui_subcommand (never reaches main()'s dispatch region; eager-parses
  --provider, the eagerParseCliFlag idiom); headless.py's inline check
  REPLACED by the helper (exit-code-2 contract and message preserved —
  live-verified). Fast paths never invoke it (pinned by test).
- Test harnesses that fake headless/tui provider wiring at module-alias
  level now stub the validation seam (it reads the real registry and has
  its own dedicated tests): test_headless_{cli,sigint},
  test_dangerous_skip_permissions, test_permission_ask_flow,
  test_init_integration.

tests/entrypoints/test_startup_validation.py (11): helper matrix (keyless /
unknown / missing-key / present-key), exit-vs-warn split, silence on valid,
and call-site routing pins for all three paths + fast-path non-invocation.
Full suite at the 6-failure baseline (7724 passed).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ction texts (agentforce314#637)

* feat(hooks): execute PermissionRequest hooks at the ask seam; TS rejection texts

HOOKS-1 (hooks-folder parity, G1+G2): the "PermissionRequest" hook event was
registered (hook_types.py:30,:75) but had NO execution site — the same
registered-but-inert class round-4 ch01 fixed for the other events.

G1 — PermissionRequest hooks now fire at the single ask choke point,
handle_permission_ask (both live seams funnel through it: the query-loop
can_use_tool_adapter and registry.dispatch), BEFORE any interactive prompt
and BEFORE the no-handler fail-closed branch (hook decisions work headless).
Port of PermissionContext.runHooks (PermissionContext.ts:216-263) +
executePermissionRequestHooks (utils/hooks.ts:4392-4427): first decisive
hook wins — allow (optional updatedInput + updatedPermissions →
chosen_updates via the promoted deserialize_permission_update) or deny
(message; optional interrupt → context.abort_controller.abort). Hook
failures contained (logged; normal flow continues). Accepts BOTH output
forms: the flat schema (Python's documented convention — decision/reason/
updatedInput + new updatedPermissions/interrupt fields) and the TS wire
envelope hookSpecificOutput.decision (utils/hooks.ts:833-840) so hooks
written for the reference CLI work unchanged.

G2 — user denials now send the model the TS-verbatim instructive texts
(utils/messages.ts:214-221) with the main-vs-subagent split
(cancelAndAbort, PermissionContext.ts:154-173) keyed on ToolContext.agent_id
(the toolUseContext.agentId analog): REJECT_MESSAGE / SUBAGENT_REJECT_MESSAGE
+ with-reason prefix variants. withMemoryCorrectionHint not ported
(GrowthBook tengu_amber_prism default-off — a no-op upstream too). The
no-handler fail-closed message stays distinct (not a user rejection).

Supporting refactors: run_coroutine_blocking extracted to utils/async_bridge
(registry._invoke_tool_call's run-or-thread bridge, now shared by the ask
seam); _deserialize_permission_update promoted from the agent-server to
permissions/updates.py (the wire shape is shared by can_use_tool replies and
hook updatedPermissions); adapter's unused _tool_use_id param now threaded.

tests/test_permission_request_hooks.py (19): decision matrix (allow/deny/
updatedInput/updatedPermissions/interrupt/envelope-form), matcher scoping,
failure containment, headless hook-allow, no-hooks fast path, backward-compat
signature, BOTH choke-point seams end-to-end, the four rejection-text pins +
subagent split + no-handler distinctness, and a real settings-file →
HookConfigManager.load → deny end-to-end. Two pre-existing deny-text pins
updated to the new constants.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(hooks): critic round — canonical suggestion JSON, first-decisive parity, typed reason

- MAJOR: permission_suggestions handed to hooks were __dict__-shaped with
  nested PermissionRuleValue objects repr-stringified by json.dumps
  (default=str) — unusable for hook authors, divergent from TS's
  PermissionUpdate JSON. serialize_permission_update promoted to
  permissions/updates.py (agent-server delegates — symmetric with the
  deserializer promotion) and used for hook stdin; read-back test proves a
  hook receives {"type","destination","behavior","rules":[{"tool_name",
  "rule_content"}]} canonical form.
- first-decisive short-circuit: _collect now BREAKS on the first decisive
  yield — later hooks never execute (TS runHooks abandons the generator
  mid-iteration); multi-hook side-effect test pins the abandonment.
- decision_reason is the typed frozen HookDecisionReason (types.py:181),
  not a raw camelCase dict (latent AttributeError on attribute access).
- test file's __main__ block moved to EOF (TestConfigLoaderEndToEnd was
  defined after it — python-direct runs silently skipped it).
- registry.py dead asyncio/threading imports removed (F401 after the
  async_bridge extraction).
- docs: gap §4 stale rows reconciled (useHistorySearch out of PORTED —
  reverse-i-search absent in the hermes client; useAwaySummary docket
  bullet fixed to absent-both), G3 cite corrected (check.py:225/:240-311),
  memory-hint co-gate noted; plan W2 rewritten (generator→_collect driver
  coroutine, first-decisive abandonment, the documented sequential-vs-race
  divergence + timeout bound, unconditional abort channel with loop-gate
  cites, serializer/deserializer pair promotion), G2 turn-abort-on-bare-
  reject scope note added.

tests/test_permission_request_hooks.py: 21/21. Full suite at the 6-failure
baseline (7745 passed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…INTEG-1) (agentforce314#638)

* feat(providers): live model discovery for dynamic-catalog providers (INTEG-1)

integrations/ parity phase 1 — port of typescript/src/integrations/
discoveryService.ts + discoveryCache.ts. Every Python get_available_models()
returned a static tuple; for dynamic-catalog providers that is wrong by
construction (the ollama spec shipped available_models=("deepseek-coder:1.3b",)
— installed ollama models are machine-local; vLLM/SGLang serve whatever was
launched; OpenRouter's hosted list churns weekly), and the static stub flowed
all the way to the /model picker (_available_models → ui-tui).

src/providers/model_discovery.py: openai-compatible {base}/models (data[].id)
+ ollama {root}/api/tags (models[].name; the /v1 OpenAI-compat surface is
stripped for tags) fetchers; persistent TTL cache at
~/.clawcodex/model-discovery-cache.json (versioned, corrupt/mismatch → empty,
atomic tempfile+os.replace writes, 1-day TTL matching TS discoveryCacheTtl);
merge = discovered ∪ static, discovered first, deduped, never empty when the
static list has entries. NON-BLOCKING CONTRACT (documented divergence from
TS's blocking fetch-with-TTL): discovered_models returns cache-or-static
immediately and refreshes stale entries on a single-flight daemon thread —
get_available_models sits on agent-server control paths (init/list replies)
where a blocking fetch would stall the client when a local endpoint is down;
cost is one static-only response on a cold cache. Errors never propagate
(stale-or-static fallback, the TS error path).

Wiring: ProviderSpec.dynamic_catalog ("ollama"|"openai-compatible", default
None) on the ollama/vllm/sglang rows (lmstudio has no row — verified);
_SpecOpenAICompatibleProvider.get_available_models branches to discovery only
when set; the hand-written OpenRouter provider merges live discovery over its
curated list. No agent-server change needed (_available_models already calls
get_available_models; latency profile preserved).

G4 spot-check (per plan W3) ran clean: no Python request path sends `store`
and none uses max_completion_tokens (grep zero over src/providers/) — the TS
per-vendor shim knobs are not needed for this port's provider set; recorded
in the gap doc.

tests/providers/test_model_discovery.py (15): parsers (incl. the /v1-strip
pin + malformed-JSON tolerance), cache (fresh-no-fetch, stale→refresh→fresh,
corrupt/version tolerance, atomic write with no stray temps), merge
(order/dedup/never-empty/static fallback), single-flight coalescing, wiring
(ollama spec discovery, static specs never touch discovery, openrouter
override). No local ollama on this machine — live smoke was fixture-only,
stated honestly. Full suite at the 6-failure baseline (7760 passed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(providers): critic round — TS two-mode merge semantics, warm-on-switch, canonical cache home

- MAJOR-1: replaced the single discovered-first union with the TS two-mode
  design (discoveryService.ts:194-212 + gateway source fields): dynamic
  (ollama/vllm/sglang) = discovered REPLACES static when non-empty — the
  bogus deepseek-coder:1.3b stub is now actually gone once a real list
  arrives (the union kept it selectable forever); hybrid (openrouter) =
  STATIC-first merge with case-insensitive dedup — curation keeps its order
  instead of being buried under 300+ discovered ids.
- MAJOR-2: warm-on-activation in _do_set_provider (the
  refreshStartupDiscoveryForActiveRoute analog) — one best-effort
  non-blocking get_available_models() after the switch kicks the
  single-flight refresh so the picker's later read sees discovered models;
  init warms the initial provider emergently via get_settings.
- MINOR-3: _cache_path uses config.GLOBAL_CONFIG_DIR (test fixtures
  re-point it) instead of a hardcoded ~/.clawcodex.
- MINOR-4: cache read-modify-write in _refresh under a module write-lock
  (withDiscoveryCacheLock analog) — the in-flight guard alone left a
  cross-KEY lost-update window.
- MINOR-5: openrouter test constructs OpenRouterProvider(api_key="")
  normally (__init__ is lazy — the __new__ dance was unnecessary).
- docs: plan W1/W2 rewritten to the two-mode design + warm + cache-home +
  RMW lock; gap doc's TS cache home corrected to ~/.openclaude (legacy
  ~/.claude fallback) and the two source semantics named where merge order
  was under-specified.

tests: 17 (order pins re-pinned to the mode semantics — the ollama wiring
test now asserts the stub is ABSENT after discovery, the headline; hybrid
curation-first pin; cache-path-honors-config-home; concurrent cross-key
refreshes both persist). Full suite at the 6-failure baseline (7762).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ings placeholder truth (agentforce314#639)

Small-folder closes from the get-parity-by-folder sweep (memdir/ +
keybindings/), critic round.

MEMDIR-1 — the critic caught a live gap behind a flag misread: my close
verified GrowthBook call-site DEFAULT ARGS, but the vendored stub's
_openBuildDefaults table (growthbook.ts:19-24) sets tengu_coral_fern TRUE,
so the reference build emits buildSearchingPastContextSection
(memdir.ts:375-407) into every user's memory prompt — and Python's builders
omitted it, with load_memory_prompt live in prompt_assembly. Ported as
always-emit (no flag system here; open-build behavior is the reference):
build_searching_past_context_section in src/memdir/memdir.py — Grep-tool
invocation forms (this port always ships the Grep tool; TS's shell-grep
branch covers ant-native/REPL modes it doesn't have), memory dir *.md +
the port's saved-session store (~/.clawcodex/sessions/ *.json) as the
transcript-search target. Wired at the two live TS call sites:
build_memory_lines tail (memdir.ts:263) and the combined team prompt after
extra guidelines (teamMemPrompts.ts:96). The third TS site (:366) is the
KAIROS daily-log builder — deferred with its mode (Slice D), correctly not
wired. isExtractModeActive (also _openBuildDefaults-ON) reclassified in the
gap doc as a deferred feature with owner (query/stop-hooks docket), not a
memdir residual.

keybindings/ — placeholder docstring (src/keybindings/__init__.py) restated:
it pointed to src/tui/keybindings.py, deleted with the Textual TUI in the
UI-consolidation (PR agentforce314#566), and a superseded ch13 plan. Now states the
current truth (defaults live in the kept ui-tui client; customization gate
resolves false in open builds — key absent from _openBuildDefaults).
Comment-only; the placeholder stays load-bearing for reference_data
snapshots (tests/test_porting_workspace.py).

tests: 4 new pins (section content incl. both Grep forms; build_memory_lines
ends with the section; build_memory_prompt keeps it BEFORE the MEMORY.md
block, matching TS :293 composition; combined prompt ends with it exactly
once). memdir suites 43 green; full suite at the 6-failure baseline (7766).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…e, client command, canon dir (agentforce314#640)

* feat(outputStyles): wire the feature end-to-end — startup, persistence, client command, canon dir

outputStyles/ parity phase OS-1: the folder's file was already ported
(loader.py, ch13 phase-9 = loadOutputStylesDir.ts), but the FEATURE around
it was unwired — memory's standing "output styles producer unwired" item,
now closed end-to-end:

G1 startup producer: settings.output_style.style now initializes
tool_context.output_style_name at agent-server context construction and the
headless entry (output_style_from_settings — never raises), so a configured
style applies from the FIRST prompt. TS reads settings?.outputStyle at
prompt-build time (constants/outputStyles.ts:207); this port carries the
style on the context (the established set_output_style pattern).

G3 persistence: _do_set_output_style now persists the choice via the new
settings.update_local_settings — the updateSettingsForSource('localSettings')
analog (Settings/Config.tsx:1600): atomic merge into the local tier
(.claude/config.local.json settings block), global-config fallback outside a
git root, cache invalidation; best-effort (in-memory switch still applies).

W3 client + availability: the kept ui-tui client gains /output-style
(SLASHES + arg grammar; with a name → set_output_style; bare → current +
available list from get_settings, which now carries output_style +
available_output_styles). The set_output_style control also replies
available_styles, and — a real bug found while wiring — validation now
follows the LOADER'S truth (available_output_styles = builtins ∪ user
styles): the old fixed VALID_OUTPUT_STYLES tuple rejected the real builtin
"explanatory" and accepted three styles that never existed
("concise"/"verbose"/"markdown" resolved silently to default).

G4 loader canon dir: user styles live at GLOBAL_CONFIG_DIR/outputStyles
(the INTEG-1 canon rule) with ~/.claude/outputStyles kept as a documented
legacy read-fallback; canon wins name collisions. resolve_output_style and
available_output_styles share one resolution (listed == resolvable).

tests: tests/test_output_style_wiring.py (13) — startup producer trio,
persistence roundtrip/merge/global-fallback, availability incl. the
listing==resolution invariant, canon/legacy dir semantics, and direct
handler tests (explanatory accepted+persisted+replied; "concise" rejected
WITH availability). One e2e pin updated from the old invented list.
Existing output-style suites 70 green; ui-tui builds; full suite at the
6-failure baseline (7774 passed, one updated pin verified green).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(settings): remove the invented VALID_OUTPUT_STYLES enum (OS-1 self-review)

Consistency hole found while OS-1 was in critic review: validation.py:46
still enforced the invented tuple, so a settings file configuring the REAL
builtin "explanatory" — which the new G1 startup producer now reads and
applies — would flag invalid, while nonexistent "concise" passed. TS's
settings schema is a plain z.string() (custom user styles make a fixed enum
unvalidatable); the check and the constant are removed, with the loader's
available_output_styles() as the runtime truth where a listing is needed.
Unknown names keep falling back to "default" at resolve time. The old
validation pin is re-pinned to free-form semantics (real builtin + custom
name both clean).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(outputStyles): G1 startup-seam e2e pin (critic MAJOR)

A settings-configured style must reach tool_context.output_style_name
during _build_runtime — asserted at the protocol level (git-init'd
workspace + .claude/config.local.json with output_style.style=explanatory
→ spawn → get_settings reply carries output_style=explanatory, the field
that reads the producer's exact output). Guards against reordering the
producer after the prompt build or breaking the assignment — the leaf/unit
tests alone stay green on those regressions. (The gap doc's TS-persistence
framing MAJOR is fixed in my-docs: the TS /output-style command is a
deprecated stub; the picker+persistence live in /config
(Settings/Config.tsx:791/:1600) — this port deliberately REVIVES the
command surface since it ships no /config picker.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…-to-end (PLUGINS-1) (agentforce314#641)

* wip(plugins): PLUGINS-1 — karpathy-guidelines bundled plugin + init (registry wiring next)

Port of typescript/src/plugins/bundled/karpathyGuidelines.ts: the prompt is
VERBATIM (mechanically extracted from the TS template literal, 2351 chars)
in src/plugins/karpathy_guidelines.py; BuiltinPluginDefinition registration
(default_enabled=False, one user-invocable skill, args→"## User Focus"
suffix per getPromptForCommand); src/plugins/init_builtin.py =
initBuiltinPlugins analog (idempotent — registration replaces by name).
Verified: idempotent registration lands in get_builtin_plugins()["disabled"]
(default-off), focus suffix works.

REMAINING (next commits): startup call at the main.tsx:1926 analogs
(agent-server _build_runtime + headless), command-aggregator wiring
(commands.ts:401 analog in command_system/aggregator.py) with the
settings enabled-gate (note: get_builtin_plugins currently uses
default_enabled only — the settings overlay is part of the wiring),
tests (enabled-only exposure + verbatim pin + focus suffix), suite at
the 6-failure baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(plugins): init builtin plugins at startup + PLUGINS-1 tests

Startup calls at both main.tsx:1926 analogs (agent-server _build_runtime +
headless entry; guarded — plugins must not block startup). Tests: idempotent
init lands karpathy-guidelines in the disabled bucket (default_enabled
False), the verbatim-prompt pin (2351 chars + all four section headers),
and the args→"## User Focus" suffix. Aggregator wiring (commands.ts:401
analog) + settings enabled-gate remain for the next commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(plugins): wire builtin-plugin skills into the command aggregator (PLUGINS-1 complete)

The commands.ts:401 analog: enabled builtin-plugin skills now become slash
commands via _builtin_plugin_skill_commands() in get_commands — deliberately
UNCACHED so the enabled gate stays fresh (a placement bug during wiring
briefly stole _load_skill_commands_cached's @lru_cache — caught by the
enabled-gate test, decorator restored to its owner). The karpathy skill is
now a real Skill instance (get_builtin_plugin_skill_commands filters with
isinstance(skill, Skill) — dict defs were silently skipped): content = the
verbatim prompt, loaded_from="plugin", user_invocable. get_builtin_plugins
gains the user enable/disable overlay (settings.extra["enabledPlugins"]
{plugin_id: bool} over default_enabled — the TS /plugin-toggle persistence
shape; never raises).

tests 4/4: idempotent default-disabled registration, verbatim pin
(2351 chars + 4 headers), Skill-shape pin, and the enabled-gate e2e
(command absent by default; present with the settings override — distinct
cwds because get_commands caches per cwd).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(plugins): critic round — the enabled skill now renders and resolves (B1+B2)

The critic EXECUTED the command and proved the feature rendered EMPTY:
- B1: only Skill.content was set; both render paths read markdown_content
  (skill_to_prompt_command copies it; the headless fallback reads it) →
  set BOTH per the canonical loader contract.
- B2: the model was advertised the enabled skill but invocation failed
  "Unknown skill" — get_registered_skill (the Skill tool's resolution) was
  plugin-blind where TS resolves through the plugin-aware getCommands()
  (SkillTool.ts:91) → enable-gated plugin fallback added to
  get_registered_skill (only ENABLED plugins' skills are yielded; disabled
  never leak).
- minor: enabled-override requires literal True (TS userSetting === true;
  string[] values disable) + the camelCase writer-contract note.
- tests: three execution pins added (render via get_prompt_for_command
  asserting the guidelines in the OUTPUT; Skill-tool resolution
  enabled-only + default-off; string-list override disabled) — the original
  four were green while the feature was broken because they asserted fields
  the runtime never reads. Plan doc records the round.

plugin+aggregator+command_system+skills slice: 484 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…1) (agentforce314#642)

* feat(query): teammate TaskCompleted + TeammateIdle stop hooks (QUERY-1)

query/ parity phase 1 — port of stopHooks.ts:335-453 + the executors
(utils/hooks.ts:3920 executeTeammateIdleHooks / :4000
executeTaskCompletedHooks). The two events existed in hook_types.py
(:61-62/:105-106) with ZERO executors or call sites — the
registered-but-inert class, 4th instance this sweep.

- executors: the HOOKS-1 trio pattern; stdin carries teammate identity
  (+ task fields for TaskCompleted); matcher-less.
- stop_hooks: the teammate block runs AFTER the core Stop/SubagentStop
  loop (TS ordering — core prevent-continuation returns first). Per
  in-progress task OWNED by the teammate → TaskCompleted; then
  TeammateIdle always. blockingError → meta user message with the
  VERBATIM prefixes (utils/hooks.ts:2091/:2113 "TeammateIdle hook
  feedback:\n" / "TaskCompleted hook feedback:\n");
  preventContinuation → hook_stopped_continuation attachment + stop;
  abort early-out. The early gate is SPLIT (core_gate | teammate_gate):
  previously the generator fast-exited when no Stop/SubagentStop hooks
  were configured, which would have silently skipped teammate-only
  configurations.
- identity threading: Agent-tool `name` → RunAgentParams.agent_name →
  SubagentContextOverrides.teammate_name → ToolContext.teammate_name;
  team_name from the parent's team file. BOTH required by the gate
  (teammate.ts:125-131 — a named agent outside a team is not a teammate).
- task sweep reads context.tasks (the TasksV2 store). NOTE for the tasks
  docket: subagent contexts get fresh tasks={} (subagent_context ch10
  comment), so a teammate does not yet SEE the leader's shared list —
  TaskCompleted fires only on tasks in the teammate's own store today;
  the shared-store threading is that docket's item (recorded in the gap
  doc's stop-line).

tests (9, execution-style per the plugins lesson): both events fire
(marker files via real command hooks), non-owned/completed skipped,
non-teammate gate, teammate-only-config split-gate pin, verbatim
blocking prefix, preventContinuation attachment, identity threading
(named+team / anonymous / named-without-team), and both stdin contracts
(the port's uniform hook_event key — the TS hook_event_name naming
divergence is pre-existing and uniform, hooks-docket owned).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(query): critic round — board sharing for teammates + real-path liveness proof

The docs critic proved the first draft's liveness premise FALSE
(in_process_teammate is Phase-7 scaffolding — constructs no context,
drives no loop; the planned seam was fictional; tasks_core was the wrong
board). The implementation had independently landed on the correct model
(the critic's own named alternative): TeamCreate populates context.team
(team.py:40) + a NAMED Agent spawn threads identity through the ONE real
seam (RunAgentParams.agent_name → SubagentContextOverrides →
create_subagent_context), sweeping the tasks_v2 board (context.tasks).

This round closes the remaining dead-in-practice facet the critic nailed:
subagent contexts get fresh tasks={} (ch10 isolation), so a teammate could
never see leader-assigned tasks and TaskCompleted would never fire. The
BOARD IS NOW SHARED for teammate spawns (named + parent team — TS's
single-shared-board semantics, utils/tasks.ts:443); anonymous subagents
keep fresh isolation (pinned).

New tests (11 total): TestRealPathLiveness drives the block through REAL
context construction — TeamCreate tool → named create_subagent_context
spawn → shared board (identity + `tasks is leader.tasks` pinned) → BOTH
hooks fire via marker files; anonymous isolation pinned. Docs revised per
all seven findings (liveness reframe, real seam, tasks_v2 board, executor
faithfulness notes incl. the port's absent hook-chain runtime and the
no-agentInfo stdin, the _get_stop_hook_message divergence trap, flag
attribution precision — only passport_quail is IN _openBuildDefaults).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(query): critic minors — permission_mode threading, board-snapshot + invariant, hardening pins

- M2: both teammate executors carry permission_mode in stdin (TS
  createBaseHookInput parity; threaded from stop_hooks' computed mode;
  pinned by test).
- M1: the teammate task-board sweep snapshots (list()) before filtering,
  with the sync-only invariant documented at the sharing site — the shared
  board is a plain dict, unlike the RLock-guarded sibling stores; guard it
  like them if teammate fan-out ever mutates from worker threads.
- N1: context.py identity comment corrected (Agent-tool named spawn, not
  the Phase-7 in-process scaffolding).
- D1: docs' baseline wording made environment-durable (zero regressions,
  every failure reproduced on the branch base; count 6-8 by environment).
- tests +4 (15 total): TaskCompleted-before-TeammateIdle ordering; core
  Stop preventContinuation returns BEFORE the teammate block (the indent
  refactor's key invariant); the TaskCompleted verbatim prefix (symmetry
  with the TeammateIdle pin); permission_mode stdin pin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ert (SCHEMAS-1) (agentforce314#643)

* feat(hooks): enforce the `if` hook pre-filter — was registered-but-inert (SCHEMAS-1)

schemas/ parity: typescript/src/schemas/hooks.ts is a TS import-cycle-breaker
(no Python analog needed — the hook schema lives in src/hooks/). But
checking its fields surfaced a live gap: `HookConfig.if_condition` (the
`if: "Bash(git *)"` permission-rule pre-filter) round-tripped through
config but was NEVER evaluated — _run_hooks_for_event filtered only by
matcher, so an `if`-scoped hook ran for EVERY command of its tool. The
registered-but-inert pattern (self-caught while dispositioning the folder).

Port of prepareIfConditionMatcher (utils/hooks.ts:1571-1610): only tool
events carry an `if` matcher; the rule is parsed via the port's
permission_rule_value_from_string; a differing tool-name skips the hook;
no rule-content runs it; rule-content is matched with the port's
prepare_permission_matcher against the tool's matchable value
(Bash → command, the documented case). A non-Bash tool with rule content
whose value isn't extractable skips like TS (patternMatcher undefined →
false) but LOGS it (debug) rather than dropping silently — the general
per-tool matcher is a bounded follow-up (recorded in the gap doc).

tests/test_hook_if_condition.py (10): matcher unit cases (match / no-match
/ tool-mismatch / no-rule-content / non-tool-event / no-condition /
unmatchable-tool) + execution enforcement (real command hooks + marker
files: an `if: "Bash(git *)"` hook does NOT spawn on `ls`, DOES on
`git commit`, and no-`if` still runs). Existing executor suite green; full
suite at the 6-failure baseline (7812 passed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(hooks): SCHEMAS-1 critic round — fix the stale if_condition docstring

The if_condition field's own docstring (hook_types.py:241-242) claimed it
was "Evaluated by matches_hook_condition (Phase 4 / WI-4.2)" — an evaluator
that exists NOWHERE in the repo (a stale forward-reference to unbuilt code
that made the field look wired when it was inert). Now points to the real
evaluator this phase added (_matches_if_condition) and records that the
field was inert before SCHEMAS-1.

Docs (same critic round): schemas field-diff COMPLETED — the "field-by-field"
claim now covers the full zod-key union; the four un-surfaced fields
headers/allowedEnvVars/model/statusMessage (http-hook headers +
env-interpolation allowlist are real functional gaps — an auth'd webhook
can't send Authorization) are recorded as a bounded hooks-subsystem
follow-up (SCHEMAS-2 candidate), out of SCHEMAS-1's if-enforcement scope.
remote doc: 2 majors fixed (remotePermissionBridge is shared not CCR-only,
solved by direct_connect_manager forwarding raw can_use_tool;
sdk_message_adapter is 1/4 divergent not name-for-name — drops
convertSDKMessage) + consumer-list corrections.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(hooks): SCHEMAS-1 round 2 — file-tool `if` matching + non-tool-event skip (were bugs)

The critic caught two behavioral divergences from prepareIfConditionMatcher:

MAJOR-1 (safety regression, fail-closed): the first cut extracted a
matchable value only for Bash → every OTHER tool returned None → the hook
was SKIPPED. But seven TS tools implement preparePermissionMatcher
(Bash/Read/Edit/Write/Glob/Grep/Monitor), so `if:"Read(*.ts)"` (the `if`
schema's OWN documented example) and safety guards like
`if:"Edit(*.env)"` were silently disabled — and worse than inert: pre-PR
an unmatched `if` ran the hook, post-PR it never ran, so a PreToolUse
guard blocking secret writes silently stopped firing. Fixed by porting
matchWildcardPattern (shellRuleMatching.ts:90 — the SAME matcher every TS
tool uses) and extracting per-tool values: file-path tools → file_path/
notebook_path, pattern tools → pattern, Bash → command + each chained
sub-command (any-subcommand match, so `Bash(git *)` fires on
`git push && npm test` — also resolves the critic's chaining-strictness
minor, since matchWildcardPattern carries none). A tool with NO matcher
analog now FAILS OPEN (runs + warns), never silently disabled.

MAJOR-2 (parity inversion): a present `if` on a NON-tool event now SKIPS
the hook (TS's caller sees an undefined matcher → return false,
hooks.ts:2023-2027) instead of ignore-and-run.

MINOR: the current tool name is legacy-normalized too (symmetric with the
parsed side).

tests: 15 (was 10) — file-tool evaluated (match/no-match), the Edit(*.env)
guard-fires safety pin, Bash chaining any-subcommand, non-tool-event skip,
unsupported-tool fail-open, + execution pins (marker files) for Read(*.ts)
run/skip. Docs: SCHEMAS-1 scope corrected (both faithfulness points no
longer misattributed to TS); remote revisit-trigger nit (two faithful +
one divergent). Full suite at the 6-failure baseline (7817).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(hooks): SCHEMAS-1 round 3 — colon-syntax Bash `if` (the last fail-closed footgun)

The critic's runtime probe found the safety hole surviving in the colon
corner: colon is the CANONICAL permission-rule form, but `Bash(rm:*)` was
fed raw to the wildcard matcher (→ `^rm:.*$`, no match on a colon-free
command) so a `Bash(rm:*)` PreToolUse guard silently never fired on
`rm -rf /`. Ported the prefix branch to full parity
(BashTool.preparePermissionMatcher + permissionRuleExtractPrefix,
shellRuleMatching.ts:43-48): Bash/Monitor rule content is matched by
_command_rule_matches — colon form (`git:*` → exact-or-prefix+space) else
the wildcard matcher — applied per env-stripped sub-command.

Also from the round-3 review:
- MINOR: leading `VAR=val` assignments stripped (`FOO=bar git push` matches
  `Bash(git *)`; TS matches on argv).
- MINOR/latent: Monitor recategorized to the command class (TS
  MonitorTool matcher uses `command`, Bash-style) — unreachable today (no
  Monitor tool in the port) but faithful for when it lands; doc line fixed.

tests 17 (was 15): the `Bash(rm:*)` guard-fires + `git:*` exact/prefix/bare
+ VAR-strip pins. Self-caught during this fix: a heredoc over-escaped the
prefix/env regexes (`\\*`→`\*`, `\\S`→`\S`) — repaired via the Edit tool
and re-verified from the worktree cwd (LESSON reaffirmed: verify the
written regex, and run verification from the worktree, not the primary
checkout — uv resolves to whichever src is on the path). Full suite at the
6-failure baseline (7819 passed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ed) (agentforce314#644)

* feat(services): port the tool-use summary generator (SERVICES-1; wiring deferred)

services/ round, iteration 1. toolUseSummaryGenerator.ts (the Haiku ~30-char
git-commit-style label for a completed tool round) was ABSENT; the state
(QueryState.pending_tool_use_summary) + config were carried but dead.

src/services/tool_use_summary.py — faithful port: TOOL_USE_SUMMARY_SYSTEM_PROMPT
verbatim (mechanically extracted, 444 chars), _truncate_json BYTE-EXACT to
truncateJson (slice[:max-3] + "..." ASCII, result-len == max_len — NOT the
U+2026 the plan first drafted), the exact user-prompt shape
(contextPrefix + "Tools completed:\n\n" + Tool/Input/Output + "\n\nLabel:"),
the small-fast-model side query via chat_async DIRECTLY (free text, reusing
only memdir's _resolve_recall_model pin — NOT _select_with_provider, which
forces JSON mode), never-raises.

Query-loop WIRING (W2/W3) is DEFERRED with a recorded design (critic round
B1/B2): unlike the sweep's other inert-plumbing fixes (whose consumers
existed once wired), the Haiku-label consumer is a mobile/external SDK app
that does NOT exist in this port. TS emits it SDK-only
(createToolUseSummaryMessage → {type:'tool_use_summary', summary,
precedingToolUseIds}); Python's sdk_types carries only the UNRELATED
drop-filtered streamlined_tool_use_summary (a tool-COUNT feature — the plan
first mis-cited it as a renderer). Wiring a per-tool-round small-model call
now = cost-without-benefit; the tested generator is ready to wire (the
design is in the plan doc) the moment a consumer lands.

Docs: the folder gap analysis now dispositions the subsystems the first
draft omitted (critic A1/A2/A3) — autoFix (a REAL live settings-opt-in,
unowned gap → SERVICES-2, the next to IMPLEMENT), AgentSummary
(→coordinator docket), tips/github-deviceFlow/rateLimitMocking, and the
voice/ placeholder reconcile.

tests/services/test_tool_use_summary.py (12): verbatim prompt, byte-exact
truncate, exact user-prompt shape, label strip, never-raises (raise/None/
no-chat_async/empty), small-model pin reuse.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(services): SERVICES-1 critic follow-up — true byte-exact JSON + doc-sync

- _truncate_json now serializes with separators=(",",":") (JSON.stringify's
  no-space form) so it is byte-exact to TS truncateJson, not just on the
  "..." marker (the prior spaced json.dumps diverged: '{"a": 1}' vs
  '{"a":1}'). Test pins updated to the no-space form.
- doc-sync nits: plan's stale "…" truncate line + the W1 signature (provider,
  not tool_use_context); gap doc's build.ts path prefixed scripts/.

12 tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…S-2) (agentforce314#645)

* feat(services): autoFix config + hook + runner (SERVICES-2 W1-W3; wiring next)

Port of typescript/src/services/autoFix/ — the settings-opt-in feature that
runs the user's lint/test command after a file edit and injects
<auto_fix_feedback> so the model self-fixes. Python had only the /autofix
slash shell; the runtime was absent.

- config.py (autoFixConfig.ts): get_auto_fix_config(raw) → AutoFixConfig|None
  — dict + enabled + at-least-one-of-lint/test (the zod .refine) + numeric
  bounds (maxRetries 0..10 default 3, timeout 1000..300000 default 30000);
  None (safeParse-null posture) on any failure. load_auto_fix_config reads
  settings.extra["autoFix"] (verified: no typed field — lands in extra, like
  the plugins round's enabledPlugins).
- hook.py (autoFixHook.ts): AUTO_FIX_TOOLS = the port's file-mutation
  registry names (Write/Edit/MultiEdit/NotebookEdit, the file_edit/file_write
  analog); should_run_auto_fix; build_auto_fix_context (the <auto_fix_feedback>
  block VERBATIM) + build_max_retries_context (verbatim).
- runner.py (autoFixRunner.ts): run_auto_fix_check — lint first, test only if
  lint passed; each an asyncio.create_subprocess_shell bounded by timeout,
  killing the process GROUP on timeout (start_new_session + os.killpg, the
  detached/killTree analog) and reaping to avoid zombies; _build_error_summary
  verbatim; never-raises (spawn failure → has_errors=False).

Verified: config parse/refine/bounds, hook fire + verbatim context, runner
lint-fail-skips-test / lint-pass-test-fail / both-clean / timeout-kill
(sleep 30 @ 800ms → returns 0.81s, group killed, timed_out) / abort.

REMAINING: W4 wiring — a NEW sibling run_auto_fix_step at the orchestrator
(tool_execution.py, after run_post_tool_use_hooks), NOT inside that function
(it early-returns when no user PostToolUse hooks configured — the
teammate-hooks split-gate class; autoFix must run regardless). Retry cap by
query_tracking.chain_id. Then tests + suite. Awaiting autofix-critic on the
plan's W4 placement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(services): autoFix wiring + all critic findings (SERVICES-2 complete)

The autofix-critic caught that TS autoFix is DEAD CODE — AUTO_FIX_TOOLS =
{'file_edit','file_write'} never matches the real tool names (Edit/Write,
FileEditTool/constants.ts:2 FILE_EDIT_TOOL_NAME='Edit'), verified
first-hand. So this port activates the author's INTENT ({Edit,Write}) as a
DOCUMENTED, opt-in-gated DIVERGENCE (fixes the reference's dead tool-name
set; inert unless a user writes settings.autoFix.enabled).

All findings:
- B1 (split-gate): run_auto_fix_step is a NEW SIBLING at the orchestrator
  (tool_execution.py, after run_post_tool_use_hooks) — NOT inside that
  function, which early-returns when no user PostToolUse hook is configured
  (the common autoFix case). Pinned by test_fires_with_zero_posttool_hooks.
- B2 (accessor): config via get_settings().extra.get("autoFix") (ToolContext
  has no settings; global read = the per-process equivalent).
- M1 (reject-not-clamp): out-of-range/non-int → whole config None (zod
  .min/.max rejects); .default only when key absent.
- M2 (camelCase): reads raw["maxRetries"]/raw["timeout"].
- M3 (reset-on-success): the retry counter is DELETED on a clean run
  (toolHooks.ts:247), not just incremented on error.
- M4 (tool set): {"Edit","Write"} — the author's intent, not the 4-element
  _FILE_EDIT_TOOLS (NotebookEdit excluded, MultiEdit is a phantom).
- M5 (None-guard): chain key handles query_tracking=None → "default".
- M6 (10k cap): each stream sliced to 10 000 before combine.
- minors: SIGTERM (not SIGKILL) + reap + ProcessLookupError race; mid-flight
  abort (kills the group, degrades to no-errors); cwd from tool_use_context;
  the post-hook attachment yield shape (list-wrapped content).

tests/services/test_autofix.py (26): config parse/refine/reject/camelCase,
hook fire + verbatim contexts + {Edit,Write} intent, runner (lint-skips-test
/ test-fail / clean / timeout-SIGTERM-kill / 10k cap / pre-set abort /
mid-flight abort / no-commands), step (ZERO-hooks regression, non-file skip,
retry cap, reset-on-clean, None-query-tracking). Full suite at the 6-failure
baseline (7857 passed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(services): autoFix config zod-strictness (critic minor agentforce314#1)

enabled must be a strict bool (a string "false" is truthy in Python — must
not turn autoFix ON against intent); a bad-type lint/test rejects the whole
config → None, matching zod z.boolean()/z.string() safeParse (not the prior
silent coerce/drop). 3 new tests; consistent with the already-strict int
path. (Minors agentforce314#2 retry-map-lifecycle and agentforce314#3 proxy-coverage accepted as
faithful-to-TS / adequate.) Full suite at the 6-failure baseline (7860).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…SERVICES-4) (agentforce314#646)

* feat(services): wiki completion — structured ingest + index rebuild (SERVICES-4)

The /wiki command is wired live (agent_server.py _do_wiki), so the on-disk
wiki is a real consumer. Port of typescript/src/services/wiki/{utils,ingest,
indexBuilder}.ts, replacing the copy-only ingest:

- src/wiki/utils.py: sanitize_wiki_slug, summarize_text (NON-LLM 280-char
  truncate + U+2026, verbatim), extract_title_from_text.
- src/wiki/wiki.py ingest_source: now writes a STRUCTURED source note
  (title/summary/excerpt=first-20-lines) at sources/{slug}.md via the
  verbatim buildSourceNote template + a verbatim log entry, then rebuilds
  the index — vs the prior shutil.copyfile raw copy. Returns
  {ok,dest,source_note,summary,title}.
- src/wiki/index_builder.py: rebuild_wiki_index (port of rebuildWikiIndex) —
  lists pages+sources markdown, extracts page titles (first '# ' heading or
  filename), writes the browsable index; called by ingest + exposed for a
  future /wiki reindex.

tests/test_wiki_completion.py (11) + updated the legacy copy-only test to the
structured-note behavior. 16 wiki tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(services): wiki completion critic minors (MINOR-1/2/3/4)

- MINOR-1: _index_template (init) now matches rebuild_wiki_index's structure
  (## Core Pages / ## Sources / ## Recent Updates + maintainer line) so the
  first ingest's rebuild no longer flips the index headers user-visibly.
- MINOR-2: ingested_at is ISO-8601 with milliseconds + Z (Date.toISOString()
  parity), not second-resolution.
- MINOR-4: rel_source via os.path.relpath (lexical, TS path.relative) so
  out-of-cwd sources get a ../-style path, not a bare basename.
- MINOR-3: byte-exact test pins for _build_source_note + the log-line format
  (the highest-risk artifacts, previously substring-only).

18 wiki tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…#647)

* feat(skills): port the /batch bundled skill (SKILLS-1)

bundled/batch.ts is registered UNCONDITIONALLY in initBundledSkills but was
absent in the port. Verbatim prompt-port mirroring the port's simplify.py:

- src/skills/bundled/batch.py: /batch is a user-invocable slash
  (disable_model_invocation) whose prompt orchestrates a large parallel
  change — research+plan in plan mode, then 5–30 isolated-worktree Agent
  runs each opening a PR. get_prompt_for_command guards empty-instruction
  (MISSING_INSTRUCTION) and non-git (NOT_A_GIT_REPO via the sync get_is_git,
  since the port's builder signature is sync unlike TS's async). Tool-name
  literals (EnterPlanMode/ExitPlanMode/Agent/AskUserQuestion/Skill) verified
  against the port registry; WORKER_INSTRUCTIONS + both guard messages
  verbatim.
- wired into bundled/__init__.py init_bundled_skills alongside
  debug/loop/simplify — the live slash-menu consumer.

tests/skills/test_batch_skill.py (8): registration fields, missing-instruction
+ not-a-git-repo guards, built prompt (tool names, worktree isolation, 5–30,
verbatim WORKER_INSTRUCTIONS, instruction interpolation). Broader skills
suites green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(skills): batch golden-length pin + guard-order (critic NITs)

- test_golden_length_and_anchors: length + boundary snapshot of _build_prompt
  as a drift canary (byte-identity to TS verified externally by the critic).
- test_missing_instruction_wins_over_git_check: pins the guard ORDER
  (empty-args short-circuits BEFORE the git check, parity with TS 112→116) by
  spying that get_is_git is never reached.

10/10 batch tests. The critic's MINOR (SkillPromptCommand headless-fallback
stub when tool_context is absent) is a pre-existing SHARED property of all
bundled skills (both live surfaces thread tool_context), noted in the parity
map — not a defect in this PR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…d) (agentforce314#648)

* feat(skills): port the /update-config bundled skill (SKILLS-2, adapted)

updateConfig.ts is a settings-editing skill. Ported ADAPTED (not verbatim):
a verbatim port would teach the model to write settings the port CAN'T PARSE.
Per the scope review + an empirical file-topology spike, the prompt is
HAND-AUTHORED (like TS, which deliberately hand-wrote examples) and grounded
in the port's REAL on-disk loaders. The TS generateSettingsSchema()
introspector is dropped (it was aimed at the config "settings" block, not the
harness-settings the skill teaches); the [hooks-only] mode is deferred (the
port's /init doesn't invoke it).

Topology (spike-confirmed): permissions + env + hooks → .clawcodex/settings.json
(user/project/local); model/provider block → ~/.clawcodex/config.json; MCP →
.mcp.json + approval. NOT .claude/ (harness-owned — settings_paths.py:5).

Keep/cut (grep-verified): KEEP permissions {allow,deny,ask} string rules + 5
real modes (default/plan/acceptEdits/bypassPermissions/dontAsk) +
additionalDirectories, env, hooks (event/matcher/command + agent/http/prompt),
.mcp.json+enableAllProjectMcpServers. CUT absent keys (cleanupPeriodDays/
respectGitignore/spinnerTipsEnabled/alwaysThinkingEnabled). DON'T teach the
internal SettingsSchema fields (advisor_*/auto_mode_*). allowed_tools=["Read"]
mirrors TS's read-before-write auto-approve.

tests/skills/test_update_config_skill.py (15): registration, port-correct
topology (.clawcodex not .claude, 3 scopes, config block), shapes (5 modes,
{allow,deny,ask} strings that ACTUALLY PARSE in the real loader, env, hooks,
.mcp.json), no-wrong-keys (absent + internal fields), valid-JSON blocks.
23 skills tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(skills): update-config hook/permission examples the port can run (critic MA1-3/N1-2)

The examples shipped config the port can't execute — the exact failure mode:

- MA1: command-hook example used $CLAUDE_FILE_PATHS (the port's hook executor
  never sets it — only CLAUDE_HOOK_EVENT/PROJECT_DIR/ENV_FILE/PLUGIN_ROOT/
  CONFIG_DIR). Swapped to the real stdin-JSON contract: the payload arrives on
  STDIN as {session_id,tool_name,tool_input,tool_response}; example now uses
  `jq -r '.tool_input.file_path // empty' | xargs -r eslint --fix`. Added a
  one-line stdin-contract note (N2).
- MA2: agent-hook example used the TS `prompt` key, which the port's parser
  has no alias for → the hook is dropped at load (config_manager.py:65-66,173).
  Fixed to `agentInstructions` (agent) / `promptText` (prompt) — the keys the
  port actually reads.
- MA3: defaultMode + permissions.additionalDirectories are write-only today
  (setup_permissions reads only allow/deny/ask; updates.py:365-367 confirms
  defaultMode). Caveated the mode (set via --permission-mode / /mode) and
  replaced additionalDirectories with the top-level additionalWorkingDirectories
  key the port DOES read at startup (setup.py:85).
- N1: strengthened the vacuous `rule_value is not None` test to assert
  semantics (Bash→'npm:*', Read→None tool-only, etc.).

New tests pin the runtime contract (no $CLAUDE_FILE_PATHS in the hooks block;
agentInstructions/promptText; the write-only-mode caveat; additionalWorking-
Directories not additionalDirectories). 25 skills tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* style(skills): update-config eslint hook 2>/dev/null || true (critic micro-nit)

Match TS's originals so a non-zero eslint exit on a PostToolUse hook doesn't
surface an error. Prompt-string only; 15 update-config tests green (JSON
blocks still valid).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…entforce314#649)

* feat(tasks): reap a sub-agent's background bash on exit (TASKS-1)

The tasks-critic caught a live gap I'd hidden under the "local_shell PORTED"
label: TS's killShellTasksForAgent (killShellTasks.ts:53), called
UNCONDITIONALLY from the agent-exit finally (runAgent.ts:849), reaps every
run_in_background bash a (sub)agent spawned so a shell loop doesn't outlive
the agent as a PPID=1 zombie. The port had no equivalent — LocalShellTaskState
had no owner field, background bash spawned fully detached (start_new_session),
and the async-agent lifecycle only closed the transcript. A non-terminating
background command leaked on both agent AND session exit.

- local_shell.py: LocalShellTaskState gains `agent_id` (owner; None = main
  session). New kill_shell_tasks_for_agent(agent_id, registry) SIGTERMs the
  process group of every running local_bash task owned by that agent; never
  raises.
- bash/background.py: spawn stamps agent_id=getattr(context, "agent_id", None)
  (verified run_agent.py:328 builds the sub-agent's ToolContext with its own
  agent_id; the main session's is None).
- agent.py: the async-agent lifecycle `finally` now awaits the reap before
  closing the transcript.

tests/tasks/test_kill_shell_for_agent.py (6, real subprocesses): reaps the
owning agent's non-terminating bash; leaves other agents' + main-session
(agent_id=None) tasks untouched; skips completed; never raises; spawn stamps
the owner. 58 task/bash/agent tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(tasks): reconcile stale task_registry comment (local_workflow IS ported)

The '_REGISTERED_TASKS' comment listed Workflow as out-of-scope, but
local_workflow IS ported + registered (tasks/__init__.py). Updated to reflect
the real registered set + the genuinely-out-of-scope gated/remote types
(critic nit). Comment-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(tasks): move background-bash reap to the CORE run_agent finally (critic round 2)

The first cut wired the reap into agent.py's async-only wrapper finally, so
SYNC (inline) sub-agents (_run_sync_agent → run_agent) and WORKFLOW sub-agents
(workflow/runner.py → run_agent) still leaked their run_in_background bash —
the exact zombie the fix claims to close. Relocated to the CORE run_agent
generator's finally (run_agent.py:424) — the single path every agent (async +
sync + workflow) traverses, matching TS's killShellTasksForAgent placement in
the core runAgent finally (runAgent.ts:849). Dropped the now-redundant async
wrapper reap.

New test drives run_agent end-to-end on a plain (sync) invocation and asserts
the core finally calls the reap with the sub-agent's own agent_id — covering
the paths the async-only wiring missed. 7 tests green.

Deferred nits (pre-existing LocalShellTask.kill): reaped task lands `failed`
not TS's synchronous `killed`; no SIGKILL ladder.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…dering (UTILS-1, wiring deferred) (agentforce314#650)

* feat(utils): surface MCP server instructions in the system prompt (UTILS-1)

The utils/ a-m explorer found a live correctness gap: the MCP client captures
each connected server's InitializeResult `instructions`
(services/mcp/client.py:279-306 → ConnectedMCPServer.instructions), but
_build_mcp_section (context_system/prompt_assembly.py) listed only server
NAMES — nothing in the port read `.instructions`, so server-authored guidance
("authenticate before calling tools", usage conventions) was silently dropped
and the model saw only the tool schemas.

Port of getMcpInstructions (constants/prompts.ts:572-596): after the server
list, _build_mcp_section now appends a "# MCP Server Instructions" block with
`## <name>\n<instructions>` per connected server that provided them
(whitespace-only ignored; no block when none provided). Both call sites
(cached + uncached) share the function, so one fix covers both.

tests/test_mcp_instructions.py (4): instructions surfaced for servers that
have them, none-section-when-absent, whitespace-only ignored, multiple blocks
joined. 30 prompt-assembly tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(utils): subprocess env secret-scrub (UTILS-2, port of subprocessEnv.ts)

The utils/ n-z explorer found a systemic security gap: TS's subprocessEnv()
strips ~23 secret env vars (ANTHROPIC_API_KEY/CLAUDE_CODE_OAUTH_TOKEN/
AWS_SECRET_ACCESS_KEY/ACTIONS_*_TOKEN/OTEL_*_HEADERS/…) + their INPUT_ GHA
twins from a child process's env when CLAUDE_CODE_SUBPROCESS_ENV_SCRUB is
truthy — an anti-exfiltration control so a prompt-injected Bash command can't
read a credential via ${ANTHROPIC_API_KEY}. The port spawned every child with
the full os.environ (bash fg/bg Popen inherit; hook _build_hook_env **os.environ).

src/utils/subprocess_env.py: subprocess_env(base=None) — copy of os.environ
(or base); flag-off = pass-through (parity), flag-on = strip the 23 vars +
INPUT_ twins, keep everything else. Wired at the 3 subprocess sites: bash
foreground (bash_tool.py) + background (background.py) Popen env=, and hook
_build_hook_env base. (The proxy-env-merge half of TS subprocessEnv is left
for the deferred CCR-remote-proxy chapter — noted in the module docstring.)

tests/test_subprocess_env_scrub.py (5): flag-off pass-through, flag-on strips
secrets + INPUT_ twins (keeps PATH), truthy variants, fresh-dict (no os.environ
mutation), explicit base. 253 bash tests green (env= adds no regression).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs+test(utils): UTILS-1 wiring-deferred (critic B1) + UTILS-2 all-23 test

Critic caught UTILS-1 inert on the live path: _build_mcp_section renders
getMcpInstructions faithfully, but McpRuntime discards the connect()
instructions (mcp_runtime.py:101) and every live prompt-build site passes
mcp_servers=None — only the dead engine.py/QueryEngine path threads it. My
unit tests passed by calling the renderer directly, bypassing the dead wiring.
So UTILS-1 is RENDERING-PORTED, LIVE-WIRING-DEFERRED (not closed) — commented
in-code + re-dispositioned to the MCP-instructions-live-wiring chapter (retain
in McpRuntime + thread + REQUEST-scoped per-turn section per critic M1).

UTILS-2 (subprocess scrub) APPROVED as-is; added test_all_23_scrub_vars_stripped
(all 23 vars + INPUT_ twins stripped, PATH kept, the flag preserved for the
child). LESSON: a passing unit test on a renderer proves rendering, not live
delivery — test through the live entry point.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(server): raise default max_turns from 20 to 50 across all entry points

The 20-turn default was cutting off legitimate work mid-task. Bumps the
default in both the interactive agent-server (clawcodex tui) and the
headless/-p entrypoint for consistency, and hoists DEFAULT_MAX_TURNS as a
shared constant between the agent-server dataclass and its CLI parser so
the two literals can no longer drift apart silently. Adds a regression
test pinning the CLI default to the constant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: add TODOS.md tracking pre-existing failures found while shipping max_turns bump

Logs 2 pre-existing test-failure clusters (advisor tool default
properties, workspace-boundary e2e enforcement) discovered — and
confirmed unrelated, via stash-and-rerun against main — while shipping
the max_turns default change. Also logs the dead max_cost_usd/
settings.max_turns enforcement gap and missing TUI --max-turns override
that two independent adversarial reviews flagged as a P2 follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@ericleepi314

Copy link
Copy Markdown
Collaborator

Pricing looks off for MiniMax-M3. The PR hardcodes /bin/zsh.60 input / .40 output / /bin/zsh.12 cache-read per 1M while the comment says this is the standard ≤512K input tier. OpenRouter currently lists minimax/minimax-m3 at /bin/zsh.30 / .20 per 1M with 1M context: https://openrouter.ai/minimax/minimax-m3. MiniMax's own Pay as You Go API pricing also shows the current standard ≤512k input tokens tier as /bin/zsh.30 input / .20 output / /bin/zsh.06 prompt-cache read; /bin/zsh.60 / .40 / /bin/zsh.12 is the >512k input tokens tier: https://platform.minimax.io/docs/guides/pricing-paygo.\n\nCan you fix this before merge? Either use the standard API/OpenRouter estimate as the flat pricing entry, or make pricing tier-aware based on input length so long-context calls use the higher tier. The tests in tests/test_pricing_status_bar.py should be updated accordingly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@octo-patch

Copy link
Copy Markdown
Author

Thanks for the review. I updated the MiniMax-M3 flat pricing entry to the standard API/OpenRouter tier and adjusted tests/test_pricing_status_bar.py accordingly, then pushed a new commit. I ran python3 -m pytest tests/test_pricing_status_bar.py in a temporary venv (27 passed).

@ericleepi314

Copy link
Copy Markdown
Collaborator

I am still seeing 1529 files changed.

I suggest you close this PR.
Do a git pull from main. And re open a new PR with clean delta.

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