run_r/run_r_script semantics, and that
the bash tool makes corteza a general-purpose agent rather than R-only.PR #96 already redirected R_USER_CACHE_DIR for the test run (so
saber's transitive briefs writes landed in tempdir), but corteza
itself writes to all three tools::R_user_dir() roots — cache
(saber briefs, /last response), data (session transcripts via
R/paths.R, matrix state), and config (matrix.json). Session tests
were still leaking files under
~/.local/share/R/corteza/agents/…/sessions/*.jsonl, which would
have tripped CRAN's "checking for new files in some other
directories" NOTE on BDR's donttest box even when local
tinypkgr::check() reports Status: OK. The suite-level redirect in
tests/tinytest.R now sets all three (R_USER_CACHE_DIR,
R_USER_DATA_DIR, R_USER_CONFIG_DIR) to tempfiles for the check
run; end-user behavior is unchanged.
Patch release batching the 0.6.7.1 through 0.6.7.12 dev cycles. Per-PR detail is preserved in the dev-marker sections below.
chat(provider = "ollama") followed by an LLM-triggered
spawn_subagent was silently producing children on Anthropic: the
default tool_executor in .make_tool_handler() called call_skill()
with no ctx, so tool_spawn_subagent() saw ctx$session = NULL and
subagent_spawn() fell through to getOption("corteza.provider"). The
executor now passes ctx = list(session = session), so provider, model,
cwd, plan_mode, and archival_depth all flow through to the child.
A single unbounded tool result (e.g. a bash command that printed 57k
lines) was flattened straight to the model and mirrored into history,
blowing the next call past the provider token limit; /compact then
couldn't recover the session. Tool results now pass through a universal
guard (admit_tool_result()) that caps oversized output to a marker plus
a recoverable handle, and /compact elides giant message bodies so it can
rescue an already-wedged session.
A chat() started without an explicit model no longer crashes after
the first turn. The per-turn context meter called
context_limit_for_model() with a NULL model and errored on the table
subscript; it now falls back to the default limit (and resolves a
model-less session to the provider default for display).
Packaging-only pass ahead of the 0.6.8 release: applied a deferred
rformat reflow, generated the user_interrupt_marker man page,
refreshed cran-comments.md, and turned the skills vignette's clone
example into a <your-skills-repo-url> placeholder so it carries no
dead link. No code behavior changes.
A turn interrupted with Ctrl+C / Esc now leaves the same "stop and ask the user what to do instead" instruction in history as an explicit deny, so the LLM checks in on the next turn rather than quietly continuing. Previously the interrupt marker was terser than the deny marker even though the approval prompt's Esc/Ctrl+C hint routes through the interrupt path.
default_provider_model() now delegates to
llm.api::provider_default_model() instead of corteza's own
provider-to-model table, which had drifted. corteza tracks llm.api's
picks going forward (e.g. openai and moonshot defaults shift to
llm.api's current choices). No change when you set a model explicitly.
serve() no longer hands the subagent tools (spawn_subagent,
query_subagent, collect_subagent, list_subagents, kill_subagent)
to MCP clients by default. A spawned subagent runs its own agent loop
and spends autonomously on the host's LLM credentials, so an unattended
MCP client could otherwise trigger unbounded cost it never sees.
Enable with subagents.expose_over_mcp (config, default FALSE) or
serve(expose_subagents = TRUE). When enabled, cumulative subagent
spend over MCP is capped: spawn_subagent/query_subagent are refused
once spend crosses subagents.mcp_spend_cap_usd (default $5.00; <= 0
disables) or an optional subagents.mcp_spend_cap_tokens for providers
that don't report cost. The cap reads the same meter as /spent; the
in-process chat()/CLI loop is unaffected.
/spent (alias /cost) reports the approximate USD spent this process
run, in both chat() and the CLI. Each turn accumulates the cost
scalar that llm.api 0.1.4 returns, plus token counts. When a model is
absent from llm.api's price snapshot its cost is unknown, so totals are
shown as a floor rather than a precise figure.
Spend is process-lifetime: /clear no longer zeroes the tally, it
closes the current conversation and opens a new one, so /spent
itemizes each conversation between clears with a grand total. /clear
now also kills any live subagents (a fresh conversation leaves none
behind); their spend is retired into the run total, shown as a separate
process-level subagents line that keeps counting an agent after it is
killed. Resumed prior-run spend is not loaded from disk.
The CLI no longer runs tools in a separate callr worker. chat() and
the CLI now drive one shared loop (run_repl_loop) in a single R
process, executing tools in-process via corteza::turn().
.subagent_registry per session: a model-spawned subagent shows up in
/agents, and a /spawn-ed one is reachable by query_subagent./flush
moved into the shared loop. Per-surface differences (rendering, help
text, input reader) ride through injected hooks, not forked logic.chat() gains the per-turn context meter and auto-compaction the CLI
already had./r, !) and the foreground bash tool moved off
blocking system2() onto interruptible processx::run(), so an
interrupt returns to the prompt instead of killing the session.worker_dispatch, worker_tool_list, cli_worker_spawn,
cli_worker_drain_events). worker_init() stays exported: subagents
call it across the callr::r_session boundary to set cwd and register
skills.Hitting Ctrl+C (CLI) or Esc (RStudio chat) after a multi-tool turn no longer loses the tool calls that completed before the interrupt. Previously the next turn / next CLI invocation saw the original user prompt, an interrupt marker, and nothing else.
Imports: llm.api (>= 0.1.4) so agent() exposes the
history_callback parameter. R/turn.R uses it to mirror the
in-progress provider-native history into the live session env as
each assistant message and each tool result lands. With the
callback wired, turn_session$history reflects the latest snapshot
even when an interrupt unwinds the turn mid-flight.repair_interrupted_tool_history() (with the
surrounding apply_exit_marker()) synthesizes provider-correct
tool_result blocks for any tool_use issued during the current
turn that never got matching results. Wired into chat()'s
interrupt and corteza_user_deny condition handlers. Anthropic's
"every tool_use needs a tool_result" requirement no longer 400s
the next chat() API call after an interrupted multi-tool turn.dump_completed_tools_summary() walks the per-turn
history slice and appends a text summary of completed tool calls
to the persistent flat-text session$messages before the
interrupt / denial marker. Wired into both CLI exit handlers
(inst/bin/corteza). The next CLI turn's api_history rebuild
now carries the work the LLM accomplished before the interrupt
("[ran tool_name(...) -> result]" entries instead of nothing).on.exit guard around the archival ownership transfer.
An inner guard inside archival_archive_turn() kills the holder
subagent if anything between spawn and the explicit
transferred <- TRUE unwinds; an outer guard in
maybe_archive_turn() covers the parent-history-collapse window.
Either failure cleans the orphan instead of leaving it sitting in
.subagent_registry with no parent reference.subagent_query() / the query_subagent tool gain a return_name
argument. When set to a name (or .h_NNN handle) the child left its
result bound under, the child resolves it after the turn and ships
the value back; the parent stashes it in the handle store and
appends a [stored as .h_NNN] block to the reply. A large
structured result is then referenced by name in a later run_r
instead of being inlined into the parent's context, mirroring how
run_r already returns large values. The async (wait = FALSE)
path captures the name at query time and applies it on collect.skill_install() clones GitHub repos via processx::run()
(already an import) instead of system2(), with a nonzero git
exit surfaced as a clear error. The clone is extracted into an
internal git_clone() helper so its status handling is
unit-testable against a local repository, no network required.find_break_point() (text chunking) replaces two backward
character-scan loops with a two-tier regex: last newline, else
last whitespace (tabs included). Behavior preserved; added
regression tests for newline-beats-later-space and tab handling.Patch release batching the 0.6.6.1 through 0.6.6.20 dev cycles plus the out-of-band "Deny aborts the whole turn" change. Per-PR detail is preserved in the dev-marker sections below.
corteza_execute_in_chat() / _retain() now expand the current
line to the full top-level R statement before sending, matching
RStudio's built-in Ctrl+Enter behavior. Before, hitting Ctrl+Enter
on the first line of lm(y ~ x,\n data = df) sent only the
unclosed first line -- annoying outside chat(), and broken
inside chat() where /r lm(y ~ x, failed to parse. Buffer is
parsed with keep.source = TRUE; falls back to single-line when
the buffer has a syntax error elsewhere or the cursor sits on a
blank / comment line outside any expression.chat() and the CLI /r handlers now read continuation lines
with a + prompt until the expression parses, mirroring R's
built-in REPL continuation. Capped at 100 continuation lines to
keep a stuck parse from blocking the REPL.handle_eval_env() used to skip
reassignment when the .h_NNN symbol already existed in
globalenv, so rebinding a handle id in the store left the old
globalenv copy in place (codex repro'd: tool_run_r('.h_001')
returned the previous snapshot after the store was rewritten).
Now handle_eval_env() assigns unconditionally and removes
globalenv bindings the package previously created that are no
longer in the store, via a new .handle_managed registry.
clear_handles() also sweeps the managed bindings out of
globalenv.session_id() ignored its caller's agent and only collision-
checked the default agent's transcript dir. With the
docker-style ~6000-name pool, a non-main agent could mint a
name that already existed in its own store and silently reuse
the transcript. session_id() now takes agent_id, threads
it to .session_name_exists(), and the collision check now
consults both the transcript file and the in-store metadata
for that agent.ctx$path, so
tools::file_ext() returned "" and the addin routed the
line as "other" -- inside chat() that meant the LLM saw raw
R code instead of a /r ... invocation. Untitled buffers are
now treated as R, matching RStudio's built-in assumption.corteza_execute_in_chat() (plus
corteza_execute_in_chat_retain() for Alt+Enter) reads the
current line / selection from RStudio's source editor and sends
it to the console, auto-prefixing /r for .R files and
! for .sh / .bash files when corteza::chat() is the
active REPL. When chat() is not running, no prefix is added --
the addin is a superset of RStudio's default execute-line
behavior, so binding it to Ctrl+Enter doesn't break normal R
scripting outside chat.chat() now sets options(corteza.chat_active = TRUE) on entry
and clears it on exit; the addin reads this flag to decide
whether to prefix.rstudioapi added to Suggests. Addin file lives at
inst/rstudio/addins.dcf so RStudio picks it up after install.Setup: in RStudio, open Tools -> Modify Keyboard Shortcuts, pick "Addins" in the dropdown, and bind Ctrl+Enter (and optionally Alt+Enter) to the two corteza addins. Override the built-in "Run current line/selection" mappings.
tool_run_r() was advertised as "Execute R code in the session's
global environment", but PR #36 (Phase 5 of CLI/worker split,
2026-04-21) silently changed it to evaluate in a fresh child env
of globalenv. Ordinary <- and = assignments landed in the
child env and disappeared when the call returned; only <<-
walked up the scope chain and persisted. Reported 2026-05-20.handle_eval_env() now copies handles INTO globalenv (under
their hidden .h_NNN names that ls() filters out by default)
and returns globalenv itself. tool_run_r() evals in globalenv,
so <- writes the the right place and survives between calls.test_tool_impl.R and test_handles.R
exercise multi-call persistence with both <- and =.U+1F7E8)
laid out in a brick pattern: adjacent rows offset by an odd
number of cells so the kernels stagger like masonry instead of
stacking in straight columns. No ANSI escapes -- the emoji is
its own colour, supported across every modern terminal.kimi-k2.6 for gpt-4o shifted the right edge and broke the
brick offset.0.6.6.17 -> v0.6.6.adjective_surname
(e.g. boring_wozniak) instead of UUIDs. Wordlists are the
Docker moby/moby adjectives plus ~60 scientist surnames.
Collisions retry up to 10 times before falling back to a hex
suffix.corteza::chat() now prints a session adjective_surname
line below the banner, matching the CLI's layout.chat() and the ~/bin/corteza CLI now open with a gold
brain-corn silhouette rendered in 256-color ANSI, with the
corteza version, active model, provider, tool count, and
/help / /quit hints embedded directly inside the kernels.
Replaces the prior single-line corteza chat | model @ provider
header. Uses 256-color index 220 (gold) for compatibility with
terminals that don't advertise true-color via COLORTERM.corteza_startup_banner() in R/banner.R --
template-driven, so future tweaks to the silhouette only touch
one constant.! <cmd> prompt prefix runs a shell command locally (bash on
unix, cmd.exe on Windows), prints output, and stages it for the
next LLM message. Matches the Claude Code / codex ! convention.
The space after ! is required so prompts that legitimately
start with ! aren't captured. Output is capped at 4000
characters in the staged version (with a truncation note); the
on-screen output is full./r <expr> is now available in the CLI too -- previously chat()
only. Same staging behavior: visible result prints inline, gets
queued for the next LLM message, with an oversized printed
result swapped for str() of the value.run_bang_shell() and run_r_eval() live in
R/chat-slash.R so both surfaces use one source of truth for
the local-eval + cap logic.task_create(tasks) / task_update(index, status)
LLM tools (Claude-Code-style TaskCreate pattern). The list lives
on the session, is injected into each turn's system prompt as
numbered [ ] / [>] / [x] / [-] lines, and is shown to the user
as a compact summary whenever it changes during a turn. Status
is pending, in_progress, completed, or cancelled.
Promoting a task to in_progress auto-demotes any other
in_progress task to pending so the "one active at a time"
invariant holds without rejecting valid edits..make_tool_handler() (R/turn.R) rather than the normal skill
executor, so the CLI's callr-worker dispatch can't strand
mutations in the wrong process. The intercept fires before
dry-run, policy, and approval -- task-list updates never prompt
the user./tasks (and /tasks clear) slash commands in both chat()
and the terminal CLI. /clear wipes the task list along with
the conversation, matching the "fresh start" intent.session_new, session_save, session_load) so it survives
CLI restarts and chat(session = ...) resumes./plan mode, which is a one-shot
research-and-propose flow; the task list tracks ongoing
progress for multi-step work without changing the policy gate.model is NULL, corteza::chat()'s startup banner and
per-turn "Thinking with ..." line route through
resolve_provider_model(provider, model) instead of falling
straight to the literal "(provider default)" placeholder. With
config$provider = "moonshot" and no explicit model, the
banner now reads kimi-k2.6 @ moonshot, matching what the
terminal CLI already showed. Unknown providers still degrade to
the "(provider default)" string.cli_approval_lines() takes a new deny_label argument (default
"Deny"). corteza::chat() passes "Deny (Esc)"; the terminal
CLI passes "Deny (Ctrl+C)". The key doesn't literally cancel
the readline prompt itself, but it cancels the in-flight turn,
which is the user-facing escape hatch users want to know about.
Deny still raises the corteza_user_deny marker; Esc/Ctrl+C
during a turn raises an interrupt marker. The LLM treats both as
"stop and check with the user" -- the wording of the interrupt
marker is less directive than the deny marker, worth tightening
in a follow-up.R/render-md-ansi.R adds render_md_ansi(text) (internal):
strip markdown syntax markers and apply ANSI styling for terminal
display, leaving the raw markdown source intact when the output
isn't a TTY (NO_COLOR, piped to file) or when the user opts out
via options(corteza.markdown = FALSE). Both corteza::chat()
and inst/bin/corteza now route assistant responses through it,
so the two surfaces stay in sync. Replaces the smaller bespoke
renderer that previously lived in inst/bin/corteza. Handles
H1/H2/H3 headings, **bold**, *italic* / _italic_ (modern
terminals interpret as italic), `inline code`, fenced code
blocks (dim, 2-space indent), - / * bullets, > blockquote
lines, and [text](url) links.[user declined: ...] tool result that
the LLM saw and treated as feedback, planning the next call --
which forced users to mash "3" through cascades of dependent tool
calls. The next turn now starts with a history marker that names
the denied tool and tells the LLM to stop and ask the user what to
do instead, rather than retrying or planning a workaround./copy slash command copies the most recent assistant
response to the system clipboard via the optional clipr
package. Silent no-op until the first assistant reply lands;
prints a hint if clipr is not installed or the clipboard
isn't available (e.g., headless Linux without xclip /
wl-clipboard).tool_run_r_script() switches its callr::rscript() call from
stdout = "|" to stdout = NULL, with stderr = "2>&1"
unchanged. The old combination hangs indefinitely on Windows with
CRAN callr 3.7.6 when the child script errors via stop() — the
internal timeout never fires (r-lib/callr#313). res$stdout is
still populated by the 2>&1 redirect, so the return value and
LLM-facing text are identical. Can be reverted once we depend on a
fixed callr (>= the post-e93efd1 release).test_subagent_callr.R gate rationale corrected: that gate is
about per-test budget, not the callr bug; r_session uses a
separate code path and was empirically verified not affected.skills_as_api_tools() (the chat() path) now applies the same
available() predicate filter that schema_from_registry() (the
CLI path) has always used. Conditional tools — git_* when not
inside a repo, platform-specific shell tools — no longer show up
in chat() while being hidden in the CLI. Counts agree.turn_footer_line() defaults to the detected terminal width
(COLUMNS env, options("width")) instead of a fixed 60-char
line, so the ─ Worked for 3m 18s ──── separator reaches the
right edge./status is now an alias of /context. Both render the same
block: a Codex-style header (corteza version, model+provider,
cwd, session id) followed by the context meter.system in bright blue,
tools in bright magenta, history in cyan — so the bar maps
visually to the breakdown rows below.New compact horizontal /context display answers the two questions
a user actually has: how full is context, and what's using it. Same
in both corteza::chat() and the CLI:
Context 24.7K / 128.0K 19% compact 90%
[██████████..................................│.....]
system 22.0K 89%
tools 2.7K 11%
history 56
│ tick at its
cell position in the empty part of the bar.Dropped the "Project context still comes from saber::briefing() and saber::agent_context()..." paragraph — the bar already shows where the system budget is going, and the prose was longer than the data.
corteza::chat() or ~/bin/corteza turn, a dim
footer line shows wall-clock duration: ─ Worked for 3m 18s ────.inst/bin/corteza only, so
corteza::chat() couldn't render the same /status, /doctor,
/config, /diff, or /review output. They now live in
R/cli-helpers.R as internal package functions; both surfaces call
them. chat() gets all five commands plus /last and /outputs./compact in chat() now uses the same do_compact() the CLI does,
instead of a separate inline implementation. Both routes share one
prompt and one chat-call shape./last, /outputs) is now session-scoped via
session$sessionId rather than a CLI-process global. Subagents and
parents have isolated buffers; /clear drops the outgoing
session's buffer./remember and /recall in the CLI are gone. They called
memory_store / memory_search / strip_tags / parse_tags —
none of which exist in the package — and would error on use.
/flush is alive (rebuilt earlier this cycle as a real memory
flush via .run_memory_flush()) and stays.Backspace past the start of your typing no longer eats the >
prompt character. The bash hack used for line editing now passes
the prompt to read -e -p so readline owns the cursor instead of
the prompt being a cat() that readline can't see. ANSI color
escapes in the prompt are wrapped in \001 \002 so readline's
column math stays correct.
The approval prompt's Choice [1]: dropped the [1] shell
convention; the (Enter) hint already lives on choice 1 itself,
so the bracket was redundant.
Multi-line input in both corteza::chat() and the CLI. Two
entries with two contracts:
/paste [optional text] — explicit "paste anything" mode.
Collects every line verbatim (logs, code, paths with literal
trailing \, etc.) until /end on its own line or Ctrl+D.\ — drops into
bash-heredoc-with-continuation mode mid-line, seeded with what
you already typed. Keep ending lines with \ to continue;
the first line without a trailing \ is final and gets
included. \\ at end of a line stays literal. /end and EOF
also terminate.Paste content that happens to start with / is not reinterpreted
as a corteza command. No "Paste mode..." banner — IYKYK.
Reason section
(gate text + Policy: + Model route:) is gone; Access collapses
to a single line that names the path or command (e.g. Write to CLAUDE.md, Run command in /home/troy/corteza); the redundant
Path: detail line above Access is suppressed when it would just
repeat the same path. Choices 1 and 3 carry key hints: (Enter) and
(Esc).● User replied: summary paraphrasing the chosen action (e.g.
"Allow writing to CLAUDE.md once").User replied: summary via ANSI cursor-up + clear-down. In
corteza::chat() running under RStudio (whose console doesn't
honor cursor-position escapes) the block stays in scrollback with
the summary appended below.replace_in_file and write_file now attach a unified-diff payload
to their MCP result. The CLI and corteza::chat() render it inline
in the tool-call output as ⎿ Added N, removed M followed by one
row per kept line (NNNN +|-| content with red/green color) instead
of the prior N lines in Xms summary. The LLM-facing result text is
unchanged — the diff is for the human reading the terminal.replace_in_file → "Update",
write_file → "Write" (matches the inline-diff phrasing).diff -u. If diff isn't
on PATH the tool degrades to a one-line size summary rather than
failing. Diff payload is capped at 200 lines / 20000 chars with a
[diff truncated: N more lines] marker so big writes don't dump
thousands of lines into chat scrollback./diff slash command's output is also ANSI-colored.ansi_supported() / ansi_colors() in the package are now the
single source of truth for both corteza::chat() and the
~/bin/corteza CLI. RStudio's R console (which is not a tty) is now
correctly detected as ANSI-capable, and NO_COLOR / FORCE_COLOR
overrides work in both surfaces.corteza::chat() and the
~/bin/corteza CLI catch the R-level interrupt.callr worker subprocess, the worker is sent SIGINT so
the in-flight tool (e.g. a long bash or run_r call) actually
stops. The worker is recycled only if it doesn't return to idle.[Interrupted by user before completing.] marker so the next turn's
model sees that the prior turn ended early.corteza::chat() is interrupted by Esc (RStudio's console
intercepts Ctrl+C for copy). In the terminal ~/bin/corteza CLI it's
Ctrl+C — terminals send raw ^[ for Esc, which is not a signal.load_saber_briefing() now wraps saber::briefing() in
suppressMessages() so the briefing text no longer leaks to the
user's terminal every time a subagent calls session_setup().subagent_query(id, prompt, wait = FALSE) fires a prompt and
returns the canonical id immediately; the parent collects the
reply later with subagent_collect(id). A subagent can carry
only one in-flight async query at a time — both wait paths
refuse to stack on top of a pending call./queue <id> <prompt> (fire) and /collect <id>
(drain). /agents distinguishes idle vs busy.collect_subagent mirrors the CLI surface.agents/subagent-<id>/sessions/<id>.jsonl, matching the
shape archival holders already use. Disk space is cheap; context
is expensive. Compaction (below) can rewrite the in-memory
history without losing anything on disk.inst/bin/corteza into
package code so chat, the CLI loop, and subagents share the
same budget math: context_limit_for_model(), format_tokens(),
estimate_text_tokens(), estimate_history_tokens(),
estimate_tool_tokens(), estimate_live_context_tokens(),
context_usage_pct(). Also default_provider_model() for
resolving the model identity a subagent will actually run with.subagents.context_compaction config block. Defaults to
mode: inherit_strict with compact_pct: 75. Working subagents
compact their own in-memory history after each turn when usage
passes the threshold; the on-disk transcript stays intact.
Archive holders are skipped via a kind marker stamped by
subagent_seed_history()./agents now shows model, age, live context (tokens / limit),
cumulative input/output tokens, and cumulative cost per
subagent. Live tokens are computed via a child-side
r_session$run() call per /agents invocation; busy children
show ctx ?. Cost is captured when the provider returns it;
shown as ? otherwise (most non-Anthropic providers).plan_mode flag. When on, the LLM is told to
research and propose rather than act: the policy engine denies
write/exec tool calls (write_file, replace_in_file, bash,
run_r, run_r_script), and an exit_plan_mode tool is injected
into the tool list. A successful exit_plan_mode call flips the
flag back off so the LLM proceeds with the work./plan slash command in chat() and the corteza CLI: bare
toggles, /plan <task> enables and submits the task as the next
prompt.plan_mode from parent_session so spawning a
child can't launder a write through plan mode.archival config block. Default off — CRAN users see no behavior
change. When enabled, finished turns collapse into a fresh subagent
that holds the full transcript, while the parent's history keeps a
compact {summary, subagent_id} block. The LLM sees live subagents
in its system prompt and picks query_subagent vs spawn_subagent
as a normal tool decision.[Max turns reached] is no longer a dead-end string: with archival
on, the full transcript persists in a subagent for follow-up via
query_subagent.archival.trigger.depth_cap).transcript_append infra under
agents/subagent-<id>/sessions/<id>.jsonl.subagent_seed_history, subagent_turn_set_id.archival.enabled requires subagents.enabled.
No silent overrides.vignette("retroactive-extraction") for the full opt-in
surface, design notes, and known limitations./spawn now parses --model, --preset, and --tools in any
order. Matches the MCP tool_spawn_subagent surface.investigate, work, minimal).
Default is investigate (read/search only).subagent_spawn(tools = character(0)) is now a documented
configuration: spawns a holder with no active tools. Used by the
archival runtime to create transcript-only subagents.resolve_subagent_tools() honors config$subagents$default_tools
when neither preset nor tools is supplied (was silently bypassed
before).file("stdin") rather than stdin() (which reads from the script
source under Rscript -e), echo the client's protocolVersion in
the initialize response instead of hardcoding it, and serialize
empty capabilities.tools as a JSON object ({}) rather than an
array ([]). Thanks to Grant McDermott (@grantmcdermott, #62).configuration vignette covering config files and precedence,
CLI flags, the full JSON config-key surface (core, context, safety,
skills, subagents, channels, etc.), slash commands, MCP server setup
(stdio and socket transports), session tuning, systemd service, and
environment variables. Thanks to Bob Rudis (@hrbrmstr, #54).Live context indicator now reflects the actual size of the next
prompt (system + tools + message history) rather than cumulative
billed API tokens. Old behavior counted up forever — /clear and
/compact had no visible effect on the indicator. Status line label
updated from Usage to Live context./context now prints live usage and the auto-compact threshold
alongside the loaded context files.resolve_provider_model().
Legacy kimi-k2 now resolves to kimi-k2.6 for moonshot.
Moonshot's chat temperature is forced to 1 (their API rejects other
values on kimi).session$compactions ->
session$compactionCount, matching memoryFlushCompactionCount.
Existing on-disk sessions show 0 compactions until the next compact;
display-only._read_prompt_via_bash now prints the prompt from R and captures
input through a tempfile. Previously relied on bash -p plus
system2(stdout = TRUE), which was fragile on terminals that mixed
the prompt into stdout.First CRAN submission.
Eight-phase refactor of the command-line interface so its subprocess no longer speaks MCP internally.
callr::r_session worker. Tool
dispatch goes through corteza::worker_dispatch() directly; no
JSON-RPC, no tools/list handshake, no per-call envelope on the
CLI-to-worker path.serve() remains a spec-compliant MCP server for external clients
(Claude Desktop, VS Code, mcptools). Public MCP behavior is
unchanged.R/registry.R. chat(), serve(),
and the CLI all read from .skill_registry. No state duplication.cli_worker_spawn(), worker_init(), worker_dispatch(),
worker_tool_list(), cli_worker_drain_events() exposed with
@keywords internal so the callr session can reach them as
corteza::*.corteza_tool_error condition class
carries tool name, args, original class, and message across the
worker pipe.R/subagent.R) also use callr::r_session instead of
spawning corteza::serve() children. Same architecture: one
persistent worker per subagent, direct tool dispatch, no MCP.R/schema.R with schema_from_fn() and register_skill_from_fn().
Tool definitions are derived at runtime from formals() and the
package's .Rd files via tools::Rd_db(). Replaces 20+
hand-written skill_spec(params = list(...)) blocks with one-line
registrations.(type) parenthetical in @param
docs ((character), (integer), (logical), (character vector),
(character; one of: a, b, c) for enums).schema_from_registry() produces the Anthropic-API-shaped tools
payload the CLI sends to the model — in the CLI's own process, not
round-tripped through the worker.inst/tinytest/test_tool_schemas.R asserts every formal maps to a
@param entry and vice versa. Drift between doc and signature
fails the test suite.register_skill_from_fn() accepts an available predicate;
schema_from_registry() filters tools whose predicate returns
FALSE. Git tools gate on .git; web search gates on
TAVILY_API_KEY. 18-20% fewer tokens in the system prompt for a
bare environment.tool_run_r() wraps non-scalar or over-threshold values with
with_handle(). The LLM gets a str() summary plus an opaque
.h_NNN handle instead of a flood of printed output.tool_read_handle(handle, op) for subsequent inspection
(str, head, summary, print). Handles are addressable by
name in later run_r calls.tool_call, tool_result,
timings) to stderr. CLI drains them between calls.--trace flag (and options(corteza.trace = TRUE))
pretty-prints the events inline via printify::print_step() /
printify::print_message().NO_COLOR honored, FORCE_COLOR overrides,
classic Windows consoles fall back to plain text.bash to an absolute path (Rtools first,
Git Bash fallback) so C:\Windows\System32\bash.exe (the WSL
launcher stub) cannot intercept commands.cmd shell tool when no real bash is found.normalizePath(winslash = "/") consistently.callr (for the worker transport), printify (for --trace
rendering).codetools, curl, jsonlite, llm.api, processx,
saber.mx.api, tinytest.