A round that asked for several calls printed the first one as an abandoned
half-line: the next prefix closed the still-open row (a bare newline), the
result then printed whole and repeated that prefix, and the rest of the batch
was suppressed. The model edits files in batches, so the duplicate row showed
up there most. Queue the later prefixes instead: each one opens its own row
once the row before it has taken its detail, which keeps one finished row per
call in call order and leaves the bytes identical to the non-interactive path.
Only output that genuinely takes the line — a prompt box, streamed text, an
error — still ends the open prefix as its own line. The cursor is still never
moved.
The default catalog and the sample config still named `deepseek-v4-flash`,
which DeepSeek keeps only as a compatibility alias. Spell out the mapping
Claude-* ids get server-side (`deepseek-v4-pro` / `deepseek-flash`) instead
of the `deepseek-v4-*` glob the flash rename made stale.
DeepSeek dropped the `v4` prefix from its flash model, and the old id stays
valid only as a compatibility alias. Seed the default catalog with the current
name so a provider that omits `models` no longer offers a deprecated id, and
follow the rename in the wire fixtures and the minimal config template. An
explicit `deepseek-v4-flash` in TOML still passes through unchanged.
A round that updated its todo list printed one header plus a full stack
render per call, so the same list appeared several times. When a batch only
touches todo tools, print one `* Todo x<N>: Push, Update` line and the last
result, whose text already ends with the final stack. A batch holding any
other tool keeps the per-call lines, and a todo call never prints a prefix
at call time (it would dangle when the batch is merged).
A caller that goes away mid-batch (cancel, display error) left the results of
tools that had already run out of history, so the retry ran them again. Append
the collected results first, then emit the events.
A failed or cancelled turn drops unfinished assistant text, and tool calls
whose result already arrived stay in history (and in the store), so /retry
continues from real results instead of running those tools again.
A tool batch counted as committed only once every call had its result, so a
batch cut short was dropped whole and the retry ran tools that had already
had effects. Keep the calls that did report a result: their assistant row
stays as the pairing carrier (a request must answer every tool_calls id),
without the text of the unfinished turn, and the calls without a result are
absent so the retry re-issues them instead of assuming they ran. The kept
rows go to the store right away, and a failed rollback write is not allowed
to mask the failure being rolled back.
A reasoning trace was appended after the body it produced, which read as if the
answer had come first and the thinking had followed it. It now leads the row,
under its own `reasoning:` label with a blank line before the answer - the order
the live stream uses. The trace only prints where it already did: a turn's ends
(the default view) and -vv; previews stay silent.
A skill root can live inside the workspace (the project-scope .claude/skills),
and grep reports those hits relative to the workspace root while outside ones
come back absolute. The attribution only stripped an absolute skill prefix, so
an in-workspace root produced alpha/alpha/SKILL.md - and, worse, a path that no
longer started with the skill name when the root itself was the workspace.
Normalizing the hit to an absolute path before relativizing to the skill covers
both, and the hit line is now matched with a pattern instead of a colon split so
a Windows drive letter cannot split it wrongly.
Found by running the REPL against a workspace-local skill root; the tool tests
only covered roots outside the workspace, so they get that case now.
README and the example config cover the [skills] section, the five tools, and
the read/write split; AGENTS.md gains the skills/ package, the tool family, the
config fields, and the /skills + /status surface; architecture.md lists the
package. doc/skills.md is the author guide: layout, the frontmatter subset and
how a broken one degrades, roots and shadowing, what the agent sees, the
grant story for reads and writes, and the warning that a skill is data —
third-party skill directories are untrusted input.
ReplState builds the store from [skills] against the current workspace and
publishes its roots as static read-only roots, so read_file/grep_files see a
skill directory while writing stays denied; the roots are rebuilt when the
workspace changes, since the project-scope directories hang off it. Discovered
skills are folded into the system prompt the way MCP instructions are, with the
catalog cap and its 'use skill_search' hint, and only when tools are on and
injection is not switched off; [skills].enabled = false drops the tools from the
registry entirely.
The write policy is installed with the CLI confirm: once, session, or no, timed
with a default of no and independent of YOLO, because a skill directory is not a
grant. A library host that installs no hook keeps the deny default. /skills
lists, searches, reads, and reloads; /status counts what was found.
skill_create writes a SKILL.md (name, description, body) plus whatever bundled
files the model lists, and skill_edit changes one file of an existing skill
either by literal replacement, by appending, or by rewriting it. Both resolve
every path inside the skill directory first, so a bad argument is answered
before anything is written and before the human is asked anything; a rewritten
SKILL.md is re-parsed so a skill that lost its description (or its frontmatter)
says so instead of failing quietly. The store is reloaded after a write, which
makes a new skill visible to the very next skill_list.
Writing is a host policy, not a directory-access grant: the denylist wins
first, then a session grant, then [skills].allow_write, then the host's confirm
hook — asked once per skill directory, so one answer covers its files. A host
without a hook denies, which keeps the library default safe; the confirm can
also answer 'session' to cover the rest of the chat.
skill_list shows what was discovered (name, source root, directory, description)
together with the copies a name collision shadowed and the notes from repaired
frontmatter, so a third-party skill never disappears quietly. skill_read
addresses a skill by name and defaults to its SKILL.md, reusing read_file for
ranges, line numbers, and the continuation token; a bundled file is resolved
inside the skill directory only, symlinks included. skill_search greps every
visible skill (or one) and rebases each hit on its skill, which makes a match
attributable without absolute paths.
Reads need no grant: the host registers the roots as static read-only roots on
the workspace policy, so read_file/grep_files see skill directories too while
write access stays denied — skill writes will go through their own confirm.
Grep used to drop every file outside the primary workspace root, which also hid
hits under a granted directory; those now come back as absolute paths.
A skill is a directory with a SKILL.md; roots are ordered by priority (an
explicit path, then our own user directory, then the directories other
harnesses install into, with the project-scope one behind its own switch). The
first copy of a name wins and the ones it shadows stay listed for reporting
instead of vanishing.
The frontmatter reader covers the subset those files actually use — scalars,
booleans, ints, inline and block lists, | and > blocks, folded plain scalars —
so no YAML dependency is needed; a nested mapping is skipped and a structurally
broken block degrades to "no metadata" with a note, keeping a third-party skill
listable rather than dropping it. Metadata is cached against the directory list
and its SKILL.md timestamps, so a listing is cheap yet notices an edit.
Skills are discovered from user-level directories by default: an explicit
``paths`` list wins, then the built-in sources (our own user config dir, and
other harnesses' skill dirs) with the project-scope dirs behind their own
switch. A name found twice shadows the lower-priority copy without hiding it.
``inject_catalog`` / ``max_catalog_skills`` bound what reaches the system
prompt, ``enabled = false`` registers nothing, and ``allow_write`` is the
standing grant for editing skills (default: ask every time). A malformed
section degrades to the defaults, like [agent] and [mcp].
Every builtin tool now prints the in-progress prefix (previously only the ones
with a renderer were named), MCP tools share one generic line, and plugin tools
keep the [tool] fallback. The parallel-batch fallback erases only its own line,
so the streamed line above it survives.
PTY, human-prompt, temp-dir, truncate, and the rest of the VCS/todo family still
printed raw "[tool] name(args)" lines while every other tool had a syntax. Each
one now has a prefix that appears as the call starts and an outcome parsed from
its own result shape: session ids and alive/exit state for PTY, answered/
selected/count for the ask family, granted or denied for access requests, the
new path for a temp dir, changed files with line counts for vcs_diff, and the
rendered stack for the remaining todo commands. ask_into_pty reports only that
the answer was written.
Resolution is no longer a dict lookup: namespaced MCP tools are built from
their ``mcp__<server>__<tool>`` name so a newly connected server gets the same
line without registration, and the resolved renderer is carried from call to
result instead of being looked up twice. Plugin tools and custom calls keep the
whole-line fallback. The shared key=value parser and the ranged-read detail are
reused by run_argv, fetch, read_file, and get_truncated rather than copied.
Tests cover every added line (including the error paths), the MCP naming rule,
and an invariant that each builtin tool resolves to a renderer.
Erasing the in-flight prefix reused a helper that cleared one row more than
requested, so a parallel tool batch also erased the line above the prefix -
the streamed reasoning text that sits there when a model reasons and then
calls tools. The helper now clears exactly the requested rows and takes
explicit control of where the cursor lands; the parallel fallback erases its
own line and lets the leading newline of the whole-line write reuse it.
Row counts are computed from physical terminal rows instead of logical
newlines, so an assistant segment wider than the terminal is fully erased
before the markdown re-render rather than leaving a stray copy behind.
Tests: a byte-level regression for reasoning before a parallel batch, unit
coverage for the row-counting and erase primitives, and a wrapped-line flush.
The mixed policy previewed the user row, so a long prompt came back truncated
even though it is half of what the turn is about. Both ends - the human's
message and the model's answer - now print in full at every verbosity level;
--preview remains the explicit one-line skimmer that collapses them too.
Tests cover the ends staying full in the default and -v views (and the rounds
between them staying one-liners) plus a long-prompt regression.
/history now works in turns (default: the last turn's user message and final
answer), with -v/-vv for intermediate rows and full bodies, and display-number
addressing (N / last [N] / --message N) that never exposes database rows.
/history counted raw message rows, so /history 1 printed whatever row happened
to be last (usually a tool result in a tool-heavy session) and "one row means
full body" was a proxy for "show me that one thing".
A turn is what a human reviews: /history now prints the last turn's two ends
(the user message and the model's answer, markdown for the latter), -v prints
every row of the selected turn(s), -vv prints full bodies plus the local system
row, and --preview collapses every row to one line. N addresses turn N,
last [N] the last N turns, --message N a single row.
Numbering is a display ordinal over conversation rows (local-only rows print as
local) and deliberately never a database row number - seq is an implementation
detail that shifts with /clear. Grouping and rendering live in the new
cli/transcript.py.
Ctrl+C now cancels the turn (or the auto-retry countdown), cancels a poll-based
prompt read such as wait's, and is ignored with a hint for off-loop readline
prompts; it never exits the REPL and never reaches MCP servers, which spawn in
their own session. Also drop ask_into_pty's stale "Ctrl+C cancels" claim: that
prompt is readline-based and empty input is its cancel.
The wait line rendered every non-error/non-slept outcome as "(disturbed)", so a
Ctrl+C cancellation of the wait prompt looked like the human disturbed it. Print
"(cancelled)" for the tool's "cancelled by user" result.
The wait tool's prompt runs off-loop on a worker thread, but its read polls
select/msvcrt, so it can act on a cancel: the SIGINT target now sets a flag the
reader checks between polls and raises PromptCancelledError with, which the tool
reports as "cancelled by user" while the turn continues. run_prompt_async /
run_cancellable_prompt_async hand the matching hook to the CLI guard; a stale
cancel never leaks into the next prompt read.