feat(providers): add MiniMax-M3 to the Minimax provider#652
feat(providers): add MiniMax-M3 to the Minimax provider#652octo-patch wants to merge 822 commits into
Conversation
…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>
…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>
|
Pricing looks off for MiniMax-M3. The PR hardcodes |
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
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). |
|
I am still seeing 1529 files changed. I suggest you close this PR. |
Summary
Add
MiniMax-M3to the Minimax (Anthropic-compatible) provider configuration.Changes
MiniMax-M3to the model selection list inMinimaxProvider.get_available_models()andPROVIDER_INFO["minimax"](listed first as the new flagship; existing M2.x models retained).MiniMax-M3insrc/models/configs.pywith a 1M context window, mirroring thedeepseek-v4/glm-5.21M entries. Also register an explicitMiniMax-M2anchor:get_model_config's prefix fallback bases a key onrsplit("-", 1)[0], so anMiniMax-M3-only entry would base on the bareMiniMaxand wrongly promote everyMiniMax-M2*id to 1M — the anchor keeps the M2 line on its 200K default (same "both exact keys" guard theglm-4vsglm-5.2comment documents).minimax-m3window to the display table incontext_analyzer.py, ahead of the bareminimaxfallback, so the status bar and the canonical registry agree at 1M (they must, pertest_context_window_1m.py).MiniMax-M3pricing tier insrc/services/pricing.py(input $0.60 / output $2.40 / cache-read $0.12 per 1M tokens; no separate cache-write charge, socache_creationmirrorsinput, as for DeepSeek).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 passedpytest tests/test_minimax_abort_signal.py tests/test_providers.py tests/test_model_command.py— 65 passed