Skip to content

Changelog

v0.38.0 - 2026-09-29

  • Mode-aware prompt color - the REPL prompt marker and its continuation variant are colored by the active mode, cyan for a read-only mode and orange otherwise, and the startup banner's mode label matches. A mode may set an explicit color (cyan/orange) in its spec to override the derived default. Docs (config.md). Tests: tests/test_modes.py, tests/test_focus.py
  • Team stage error reporting - run_team synthesizes a stage error from the stage status and its sub-session id when the stage result carried none, so the team tool reports stage "<role>" ended with status "<status>" (session <id>) - see the sub-session log instead of unknown error. A permission-clamped stage failure also points at the caller ceiling and how to widen it. Docs (teams.md). Tests: tests/test_team_tool.py
  • Team stage self-reentry guard - a team stage can no longer call the team tool for the team it belongs to: the per-invocation resolver returns deny (disabled by tool policy) when the requested name is already on the _team_stack, each stage brief tells the stage to do its own work directly rather than orchestrate, and the team_cycle error carries the same guidance. This fixes a stage (for example a Developer whose brief read Team: DevTestPipeline) that tried to re-run its own pipeline and either tripped a confusing cycle error or stalled. Docs (teams.md). Tests: tests/test_team_tool.py
  • Sub-agent permission feedback - an ask-gated tool inside a sub-agent now returns an explicit permission denied result that names the category and points at ask(kind="permission"), instead of the interactive [cancelled] User declined marker. The sub-agent prompt names the auto-denied categories and tells it not to retry, and the agent loop stops a sub-agent after repeated consecutive denials. This fixes a team stage (for example a Tester that could not run commands) that retried a denied call dozens of times and then reported a false success. Docs (security.md, swarm.md, teams.md). Tests: tests/test_subagent.py
  • Ollama null-content fix - an assistant turn that only made tool calls, or only produced reasoning, is now serialized with an empty content instead of null (turn_to_provider), and OpenAICompatibleProvider._prepare_messages coerces a null assistant content to an empty string. A session with any tool-using turn no longer 400s on Ollama's OpenAI-compatible endpoint (invalid message content type: <nil>), which had made every later request in the session fail. Tests: tests/test_turns.py, tests/test_providers.py

v0.37.0 - 2026-09-27

  • Renamed the project from Replio to Polyglav - the Python package (src/polyglav), the polyglav CLI and python -m polyglav, the worktree directory (.polyglav/), the global config directory (~/.config/polyglav/), the POLYGLAV_* environment variables, the bundled polyglav-core-* plugins with their polyglav_version manifest field, and all repository, PyPI, and documentation URLs. Existing .replio/ configs and sessions are not migrated.
  • Rewrote the README around outcomes with the "One agent, many heads" slogan and moved the technical quick start, CLI, API, swarm, jobs, fleet, MCP, Docker, and plugin material into a new INSTALL.md.

v0.36.0 - 2026-09-24

  • Fixed scoped code_test no-match detection on Python 3.11 - the dev wrapper now also treats Ran 0 tests in the output as no tests, because the NO TESTS RAN message and exit code 5 for a target with no tests were only added in Python 3.12. A scoped run on 3.11 now reports no tests matched "<target>" instead of looking like a passing check. The test no longer asserts the 3.12-only message or exit code, which also unblocks the all-plugins run on 3.11. Tests: plugins/replio-core-dev/tests/test_tools.py
  • Tool identity, error color, compact status params - the git/git_commit tools now print as ⎇ Git with their own glyph instead of ← Read, and the dev wrappers print $ Test/$ Lint/$ Format. A new error registration predicate marks a result as failed (run_command and the dev wrappers read the exit N line), and the agent loop renders such an echoed result red through ReplUI.tool_error_result rather than dim. ToolRegistry.params_str renders an oversized or multiline argument as <N chars> instead of dumping a body into the glyph status line or the confirm label. Docs (tools.md). Tests: tests/test_ui.py, tests/test_tool_registry.py, plus the git, dev, and exec plugin suites
  • Optional output log - output_log (default false) tees everything the REPL prints into .replio/output/<session>.txt (directory set by output_log_dir), ANSI colors included, so a mis-rendered line can be inspected later. A new TeeStream in ui.py wraps sys.stdout for the run and skips spinner repaints (writes with \r and no \n) and readline markers. Docs (config.md). Tests: tests/test_repl_input.py
  • Renamed types to roles - the agent catalog now uses one vocabulary: AgentType is Role, TypeRegistry is RoleRegistry, src/replio/types.py is roles.py, bundled_types.json is bundled_roles.json, docs/types.md is docs/roles.md, and the catalog files are roles.json. The /types command is /roles, a new /role command reports the active run's role and session, --type is --role (replio run/replio fleet config, jobs), the plugin hook is register_roles, and the delegate tool argument, the catalog tool kind, and the team stage field are all role. The assistant_type config key is assistant_role. No compatibility aliases, so existing .replio/types.json and teams.json files with the old keys are adapted by hand. Docs synced across docs/, AGENTS.md, README.md, and VISION.md. Tests renamed (tests/test_roles.py)
  • Visible sub-run stages and stats - a delegated agent or team stage now leaves a dimmed ✓ <stage> (Ns, N tokens) line when it finishes instead of erasing the animated status line, so a multi-stage run shows its progression rather than only the last stage. ReplUI.status_end takes an optional note, Engine._run_stats derives the duration and token total from the turn's usage, and the NullUI/BufferUI/HeadlessUI sinks take the note too. A new subrun_verbosity config (quiet default, summary, full) controls how much of a sub-run reaches the terminal: summary uses the new SubRunUI to forward only the sub-run's write and edit activity lines while its full output still buffers for /focus log. Docs (config.md). Tests: tests/test_ui.py, tests/test_team_run.py
  • Reliable REPL prompts and multi-line input - the tool confirm and ask are line-based with validation instead of raw single-key reads, so an answer is always taken and a redirected stdin never hangs. The confirm accepts y/yes/empty as yes and n/no as no, re-prompting on anything else, and its hidden path only applies to the confirm (a free-text ask answer stays visible). The ask tool registers confirm=False, a new registry metadata flag, so a question is never gated by its own confirm prompt and the loop proceeds straight to it. Multi-line input closes on a delimiter at the start of a line or on a blank line instead of the odd-count quote heuristic, so a backspace or a pasted delimiter no longer submits mid-typing, and a trailing \ continues the message on the next line. Docs (tools.md, README.md). Tests: tests/test_ui.py, tests/test_ask.py, tests/test_unattended.py, tests/test_repl_input.py
  • vcs permission category - git_commit moves off the edit category onto its own vcs category (default ask), so the commit tool is gated by its own carve rather than by general file edits. Its per-invocation resolver now returns ask only when all is true and otherwise returns None, which defers to the category action, so a role whose carve sets vcs: allow commits without a prompt while every other role confirms (ask auto-denies in a sub-run). Staging everything with all=true always asks, so an unattended commit never sweeps unrelated work. Docs (AGENTS.md, config.md, security.md, tools.md, writing-tools.md). Tests: plugins/replio-core-git/tests/test_tools.py, tests/test_config.py
  • Scoped code_test runs - code_test with a target now runs that target instead of appending it to a unittest discover command, which previously matched nothing, exited 5, and printed NO TESTS RAN, so a scoped run verified nothing while looking like a completed check. When the configured dev.test_cmd is the unittest discover form, the wrapper builds python -m unittest <target>, and any other command still appends its target. A target run that reports no tests appends Error: no tests matched "<target>" (unittest exit 5) to the tool result, and the dev wrapper's DEFAULT_TIMEOUT rises from 120 to 300 because the full suite takes about 144s. Tests: plugins/replio-core-dev/tests/test_tools.py
  • Config merge - nested config objects now merge per key across the built-in defaults, the global config, and the local config (tool_permission, modes, ask_policy, memory_scopes, grant_permission), so a local object overrides only the keys it names. Lists and scalars still replace wholesale. The default ask, catalog, handoff, and team permissions are no longer dropped when a local tool_permission is set, which had confirm-gated those tools because ToolPolicy falls back to ask for a missing category, and Config.unset recomputes the key from the remaining layers. Docs (config.md). Tests: tests/test_config.py, tests/test_modes.py
  • Dimmed thought summary - the (Thought N.Ns) line printed after streamed reasoning is dimmed again, so only the - Thinking header (reasoning shown) and the + Thought N.Ns line (reasoning hidden) stay blue. Docs (config.md). Tests: tests/test_ui.py

v0.35.0 - 2026-09-20

  • REPL UI colors and prompts - the terminal surface gets one color language and quicker prompts. The user prompt marker is bold cyan, activity/tool status lines and the confirm/ask prompts are orange (\033[38;5;208m), a failed tool call prints its ! Error: line red, and the - Thinking/+ Thought/(Thought N.Ns) headers are blue while the reasoning body and tool output stay dim. Every status line now starts on a new line instead of trailing streamed text (_ensure_newline). The tool confirm defaults to yes on an empty answer ([Y/n]), and /connect, /config, and the compaction prompt follow the same [Y/n] convention. New hide_confirm_input config hides the typed input for the tool confirm while free-text ask answers stay visible. Docs (config.md, architecture.md, commands.md, security.md, tools.md, writing-tools.md). Tests: tests/test_ui.py
  • opencode reasoning echo - the opencode and opencode-go providers now send captured reasoning back as reasoning_content, which Console Go requires in thinking mode when a turn continues after tool calls (it rejected the tool-loop continuation with HTTP 400 The reasoning_content in the thinking mode must be passed back to the API). OpenAICompatibleProvider gains an ECHO_REASONING class attribute (default false) and _prepare_messages, which always drops the internal thinking key and, when enabled, copies it to reasoning_content on assistant messages. OpenCodeProviderBase sets the flag for both providers, and the auto-continue assistant message carries its thinking so a length continuation echoes it too. Other providers are unchanged, since some endpoints (the official DeepSeek reasoner) reject reasoning_content in the input. Docs (providers.md). Tests: tests/test_providers.py, plugins/replio-core-opencode/tests/test_opencode_providers.py
  • Session log version - every session records the Replio version that created it (Session.version, stamped in SessionManager.create via get_version). It is written to the session JSON next to role, round-trips through to_dict/from_dict, and is never refreshed, so a log says which build wrote it. Files written before the field load with an empty version (no backfill). Docs (session.md). Tests: tests/test_session_log.py
  • Per-run output buffers - every run records a plain-text buffer, and a sub-engine renders through a new BufferUI sink instead of NullUI, so its streamed output, tool lines, and footer land in the run (a focused run rebuilt for the REPL keeps the terminal ReplUI). Run gains append_buffer/buffer_text/clear_buffer plus a buffer_lock and a max_buffer_lines cap, and the new run_buffer_max_lines config (default 2000, 0 = unlimited) trims the oldest lines. /focus log [n] prints the focused run's buffer (all lines, or the last n). Docs (commands.md, architecture.md, config.md, testing.md). Tests: tests/test_run_buffer.py, plus test_subagent.py
  • Run status animation - a Braille status line (status_spinner, default true) animates while a delegated agent or team stage runs. The REPL is blocked during run_subagent, so the spinner runs on its own thread. ReplUI gains status_begin(label)/status_end() (the thinking spinner is generalized to a labeled spinner), NullUI/HeadlessUI no-op, and Engine.run_subagent wraps each run with a label that names the team and stage when nested. Docs (config.md, testing.md). Tests: test_ui.py

v0.34.0 - 2026-09-18

  • Provider session binding - providers carry a session_id (BaseProvider/OpenAICompatibleProvider) bound to the run's own session. The engine passes the session id when it builds a provider and load_or_create_session rebinds it, so a resumed or focused run reuses the provider-side session, while a shared sub-agent provider keeps the caller's binding. The OpenCode providers fix their session_id override so the passed run session id (or a minted uuid) survives construction and is sent as x-opencode-session. Docs (providers.md, testing.md). Tests: test_provider_session.py plus the OpenCode plugin suite
  • Memory scopes - a shared src/replio/memory.py owns bounded memory under .replio/memory/, one Markdown summary per scope at roles/<type>.md, teams/<name>.md, and jobs/<name>.md. Role memory is injected into a sub-agent's system prompt and refreshed after its run, and team and job memory move onto the same module (reads fall back to the older .replio/teams/.../memory.md and .replio/jobs/*.memory.md locations, writes use the new path). New config memory (default true), memory_scopes (per scope), and memory_max_chars (default 2000), plus a /memorize [scope] [name] command and Engine.memorize. Docs: new docs/memory.md, plus config.md, commands.md, architecture.md, teams.md, jobs.md, swarm.md, index.md, testing.md. Tests: tests/test_memory.py
  • Run continuation and context control - delegate/team accept resume (a run id like #3, a session id like #ab12cd, a session name, or session:<name>) and context (continue, compact, or new) in place of warm session_key/warm_sessions sessions, so a caller resumes a specific run or session and chooses whether to continue, compact, or start clean. Engine.resolve_target resolves the handle against live runs, saved sessions, and session ids, and reactivates a resumed run. Team resume applies to the first stage. The sub_<key> warm member sessions, _warm_session_name, Team.warm_sessions, TeamStage.session_key, the team tool's warm argument, and the matching catalog fields are removed, and the review loop starts a fresh producer run per iteration seeded by the previous findings. Docs synced (teams.md, swarm.md, session.md, testing.md). Tests: test_run_resume.py (was test_warm_sessions.py) plus a test_team_loop.py update
  • Run-owned focus - the REPL focus stack is keyed by run id (FocusManager), and focus only navigates: a run with a retained engine is attached directly, a past run is rebuilt by Engine.run_engine, which resumes the run's own session and role. agent_<role> sessions, Engine.focused_engine, and _agent_session_name are removed, and RunRegistry keeps a run's engine (set_engine/engine_for) with reactivate. /focus attaches by #run, #<session_id>, session:<name>, root, or a session name and navigates by parent/child/sibling/next/prev/back, rendering a ↔ Switch to <role> marker on non-current runs. The handoff tool targets runs (parent/child/sibling/root/#run/#session_id/session name), and focus_on_delegate records the child run (TurnResult.run_id) instead of a role. A focused run adopts the root UI so its turns render at the terminal. Docs synced (architecture.md, commands.md, config.md, tools.md, session.md, testing.md). Tests: test_focus.py, test_handoff.py, test_focus_on_delegate.py, plus /history and /print run-target updates
  • Session log field rename - the session's two identity fields are now session_name and session_id (was name and code), so the log states what each value is. Session.session_name/Session.session_id replace Session.name/Session.code, Run.session_id replaces Run.code (the run's session still holds the session name), and the helpers are session_id_hash, coded_session_name, session_id_from_name, SESSION_ID_LEN, and find_by_session_id. Generated names keep the six-character id as the trailing component (ses_<ts>_<id>, job_<ts>_<id>, sub_<ts>_<id>), and the replio serve /chat body key session_id (the session-name selector) is renamed to session to avoid the collision. Files written before the rename no longer load (left on disk and still listed). Docs synced (session.md, commands.md, tools.md, architecture.md, testing.md, api.md, fleet.md, jobs.md, swarm.md, security.md, teams.md, usage/programming.md, AGENTS.md). Tests: tests/test_session_id.py (was test_run_code.py) plus field renames across the suite

v0.33.0 - 2026-09-16

  • Job and delegation session naming - job runs and delegation sub-agents now embed the same six-character run code, so generated runs are job_<ts>_<code> and sub_<ts>_<code>. The job name and the parent session id are dropped from the filename: the job name stays in the job registry and each run's JobRun.session, and the calling session stays in parent_id. coded_name/name_code in sessions/manager.py mint and parse the code (_sub_session_name, job_session_name), a name collision re-mints instead of appending _2, and SessionManager.create derives a coded name's code automatically. Warm sub_<key> member sessions and agent_<role> role sessions keep their stable deterministic names, their code naming is a follow-up task. Docs synced (session.md, jobs.md, swarm.md, teams.md, security.md, testing.md, usage/programming.md, AGENTS.md). Tests: tests/test_run_code.py, tests/test_jobs.py, tests/test_subagent.py
  • Stable run code and ses_<ts>_<code> naming - every session now carries a six-character base36 code, minted at creation and stored on the session (Session.code, Run.code). Auto sessions are named ses_<ts>_<code> from the start, so the prompt-slug rename (_auto_name_session) and its transliteration logic are gone and a session name never changes. The code is a durable handle across restarts: commands accept it as #code (/focus #ab12cd, handoff target #ab12cd, /history --run #ab12cd, /print --run #ab12cd) and it is shown by /focus, /history, and the run log. Resolution checks live runs first, then SessionManager.find_by_code globs the sessions directory for *_<code>.json - no session files are read to find it. Engine.chat loses the now-redundant autoname parameter (the name is final at creation), and load_or_create_session syncs run.session/run.code. Explicit names (/session new, --session-id) carry no code and are referenced by session:<name>, and legacy files resolve by name only. Docs synced (session.md, architecture.md, commands.md, tools.md, testing.md). Tests: tests/test_run_code.py, plus code assertions across the suite
  • /print command - reprints a listed turn in full or one part of it. /print <n> shows the turn's full metadata block (#index, status, duration, tool count, started/ended, model, provider, mode, reasoning - empty values as -, so a command turn is self-describing), then each part in order: the operator prompt, thinking (dim), every tool call with input JSON, output, is_error, and analysis, the answer, command records (compaction summary included), and system notes. /print <n>.<m> prints only the m-th part. Each part's text is capped per part by the new print_max_chars config (default 4000, 0 = unlimited) with ... (N more chars, use --full) appended, and --full drops it for one call. --run <#id|role|session:name|name> reuses the /history target resolver to read another run without switching focus or the current session. The line renderer render_turn lives in sessions/render.py. Docs synced (commands.md, session.md, config.md, testing.md). Tests: tests/test_print.py, tests/test_session_render.py
  • /history command - lists the active session's turns as a numbered, printable index, one line per turn: #<index> [<status>] <duration> <tool count> <prompt>. The index is the turn's absolute index, so a turn can be named later regardless of the listing limit. /history 3 shows the last three turns (default 10), /history all every turn, and --thoughts adds a dim first-line excerpt of the turn's thinking (--thoughts all prints the full text). /history --run <#id|role|session:name|name> reads another run's session without switching focus or the current session: an id resolves to the live focused run when present, otherwise the saved session, a role resolves to its live agent_<role> engine or the saved session, and a bare name resolves to the saved session (legacy flat-messages files read as not found). The line renderer turn_summary lives in sessions/render.py and the target resolver _target_session in commands/builtins.py, shared with the upcoming /print. Docs synced (commands.md, session.md, testing.md). Tests: tests/test_history.py, tests/test_session_render.py
  • Session turn cutover - session logs now store turns only, and the flat messages format is removed everywhere. A turn is one operator prompt (or one slash command) and the agent's response: {index, started_at, ended_at, status, model, provider, mode, reasoning, parts}. Parts are typed (user, text, thinking, tool, command, system), and a tool part co-locates a call with its result (name, input, output, is_error, optional analysis). Session exposes an explicit turn/part API (start_turn/end_turn/add_part and add_user/add_system/add_thinking/add_text/add_tool/add_command) that replaces add_message. The engine opens a turn with the user part, co-locates each tool call with its result, closes the turn with its status, and rebuilds the provider context from turns through turns.provider_messages (assistant text plus consecutive tool parts grouped into one tool_calls message with its results, thinking attached, command parts dropped, compaction summary plus turn-index boundary). The renderer, Markdown export, /sessions preview, and the secondary readers (delegate result summary, ask task preview, run and team memory summarization, eval error count) all read turns. compact_keep now counts turns. Existing .replio/sessions/*.json files with messages are left untouched and no longer load (read() returns nothing, the name stays listed). Docs synced (session.md, architecture.md, config.md, security.md, testing.md, writing-tools.md, AGENTS.md). Tests: tests/test_session_log.py, tests/test_session_render.py, plus turn-model migrations across the suite
  • Session turn model - a new src/replio/sessions/turns.py defines the turn/part shape for the session rewrite. A turn holds index, started_at, ended_at, status, the turn-level model/provider/mode/reasoning, and an ordered parts list. Builders cover the six part types (user, text, thinking, tool, command, system), a tool part co-locates name/input/output/is_error/analysis, and provider_messages rebuilds the OpenAI-compatible context by grouping a turn's text and consecutive tool parts back into one assistant tool_calls message plus its tool results, dropping command parts and applying the compaction summary and turn-index boundary. The module is wired into Session and the engine by the turn cutover above. Docs synced (testing.md). Tests: tests/test_turns.py

v0.32.0 - 2026-09-15

  • focus_on_delegate config - off (default), ask, or on moves the REPL focus to the delegated role's engine after a delegate call. With on the tool records a pending focus that ChatLoop applies after the turn, so the caller's answer still finishes and the next input goes to the specialist. With ask the operator is prompted first (skipped under unattended mode), and an unrecognized value falls back to off. Focus is REPL-only, so a sub-agent that delegates is a no-op, and a focused role engine that delegates follows too. Docs synced (config.md, architecture.md, swarm.md, testing.md). Tests: tests/test_focus_on_delegate.py
  • handoff tool - a core tool (category and permission handoff, default allow) lets an agent pause or finish its run and hand control to another run, with the operator's focus following. target resolves against the run registry as parent, child, sibling, a role name, root, or a run id (3 or #3), and done (default false) finishes the current run instead of leaving it paused. The tool records a pending handoff and the agent loop stops after the tool batch, so no further provider round runs. ChatLoop applies the pending handoff once the turn returns, focusing the resolved role's engine (a root-role target resets to the assistant). It is REPL-only: a delegated sub-agent has no focus manager and gets a clear error. Docs synced (tools.md, config.md, architecture.md, testing.md). Tests: tests/test_handoff.py
  • /focus command - shows the current run, the run tree (indented, current marked), and the flat call log, then attaches or moves through the run registry. Attach by run id (<id>, #id), role (the stable agent_<role> engine), or session:<name>, and navigate with parent, child [n], sibling [n], next, prev, and back. Focusing a run attaches to the role engine that owns it, and a root-role run resets to the assistant. Focus is REPL-only. Docs synced (commands.md, architecture.md). Tests: tests/test_focus.py
  • Focus manager and role engines - the REPL keeps a focus stack (FocusManager, src/replio/focus.py): the root assistant plus a role engine per focused role. ChatLoop.active() returns the focused engine and ChatLoop.run() routes turns and slash commands to it, so one window drives whichever agent is in focus. Engine.focused_engine(role) builds a role engine with the same type resolution, skills, and permission carve as a delegation sub-agent, but with the REPL UI and a stable agent_<role> session that resumes on re-focus, and it records a run. back() and reset() walk the stack and repoint ReplUI._loop so rendering and confirms follow the active engine. A new prompt_role config (default false) prefixes the prompt with the active role (e.g. Assistant >>>). The per-engine run attribute is current_run so it no longer shadows ChatLoop.run. Docs synced (architecture.md, config.md). Tests: tests/test_focus.py
  • Run registry and call tree - every engine instance is a run in a process-local RunRegistry (src/replio/runs.py) shared across the delegation tree: numeric id, role, session, parent and ordered children, status, the task brief, and timestamps. Delegated and team-stage engines register as children of the caller and are finished when their chat returns, RunRegistry.runs() is the flat creation-order call log, and children(id) walks the tree. This is the data model the focus and handoff surface builds on. Docs synced (architecture.md). Tests: tests/test_runs.py
  • Session role metadata - every session records the agent type that owns it as role, stamped at creation: the bound root type (Engine.bind_root_agent), the delegated and team-stage type (_new_sub_engine), and the job type (scheduler._build_engine). /session new carries the current role. A plain root or a headless run with no --type leaves it empty, and legacy files without the key load with an empty role (no backfill), so a run is reconstructable from its log. Docs synced (session.md). Tests: tests/test_session_log.py, tests/test_assistant.py, tests/test_subagent.py

v0.31.0 - 2026-09-12

  • Assistant root role - the REPL root now binds to a bundled assistant type on startup, so a plain replio session is the operator's single point of contact: it answers small tasks inline and delegates bigger work (use composer to design a team, then run it with team, or leader to supervise). New config keys assistant (default true, REPL only) and assistant_type (default assistant) rebind the root to any type without code changes, and a non-empty system_prompt or a local override of the type always wins. Engine.bind_root_agent resolves the type and merges its prompt and skills, plus its model/permissions/ask policy only when the user has not set them. Headless run/serve/jobs are unchanged and keep using --type. Docs synced (config.md, types.md). Tests: tests/test_assistant.py
  • Team permission category - the team tool gains its own team permission category (default allow), separate from delegate, so a type can be a delegation target yet be barred from running pipelines. The composer now sets team: deny instead of delegate: deny, which had made _delegate_action refuse delegation to the composer entirely (the delegate category on a target also gates delegation to it). Docs synced (config.md, tools.md, teams.md, types.md). Tests: tests/test_team_tool.py, tests/test_composer.py
  • Composer role - a bundled agent type that designs and persists a complete team for a task without running it. The composer reads the task and the existing catalog, decides the stages and the specialist types and skills each needs, saves the team (including per-stage skills and an optional review loop) through the catalog tool, and reports the team so the caller can run it. It carries catalog allow with edit, bash, and team denied, so it can shape the catalog but cannot write project files, run shell, or start the team itself. To make that carve effective, the default tool_permission.catalog is now allow (it writes only to .replio/, like the default edit), and setting it to ask confirm-gates catalog changes. management joins the bundled type tag vocabulary. Docs synced (config.md, types.md). Tests: tests/test_composer.py, plus bundled-type and catalog updates
  • Team review loop - a team's loop config (from, until, max_iterations, verdict) iterates a producer/reviewer block until the reviewer's result contains the verdict marker followed by PASS or the iteration cap is reached, the generate > check > correct pattern. Stages before from and after until run once. The producer's brief carries the previous review and its warm session, so it revises with its own context, and its session is forced warm for the run even when the team does not set warm_sessions. from/until accept a stage type name or index, the marker defaults to VERDICT:, and reaching the cap without a pass is still a successful run. The catalog tool saves loop. Docs synced (swarm.md, teams.md). Tests: tests/test_team_loop.py
  • Warm member sessions - delegate and team stages can resume a persistent sub-agent session so a role keeps its context across calls while per-run skills extend it. run_subagent/_new_sub_engine take a session_key that loads or creates sub_<key> and appends the new task, and the delegate tool exposes it. Teams gain warm_sessions (each stage keyed <team>__<stage-type>) and a per-stage session_key, and the team tool's warm argument overrides the team setting for one run. Without a key the fresh sub_<ts>_<parent-session> behavior is unchanged. The catalog tool saves warm_sessions and stage session_key. Docs synced (session.md, swarm.md, teams.md). Tests: tests/test_warm_sessions.py
  • Agent catalog management - a new core catalog tool (category and permission catalog) lets a model list, show, save, and remove agent types, teams, and skills, and reload the catalog, so a composing agent can build a team and use it in the same run. Saves write to the local project catalog (.replio/types.json, .replio/teams.json, .replio/skills/<name>.md). Catalog changes are versioned on the shared plugin manager, and every engine reloads its registries at the start of the next agent-loop round through Engine.reload_catalogs/touch_catalogs, so a team composed by a sub-agent is visible to the root engine that delegated it. Because it writes only to .replio/, a composing agent can be granted catalog management without general edit power. The default tool_permission.catalog is allow, and setting it to ask confirm-gates catalog changes. Docs synced (config.md, tools.md). Tests: tests/test_catalog.py
  • Per-invocation skills - a caller can extend an agent type for one run instead of defining a new type. delegate and the team tool accept a skills array, and a team stage may set skills, all layered over the type's standing AgentType.skills. Resolution order is standing skills first, then invocation skills (task-wide before stage-specific), deduplicated by name, with unknown names skipped silently. Layering never changes a type's tool carve, so a reusable agent keeps its identity while a task adds its own technology, stack, or framework instructions. Docs synced (skills.md, swarm.md, teams.md, types.md). Tests: test_delegate.py, test_subagent.py, test_team_run.py, test_teams.py

v0.30.0 - 2026-09-10

  • Autonomous supervisor end-to-end verification - the last work package of the autonomous supervisor track. A deterministic governance-loop test (tests/test_autonomous_supervisor.py) drives a leader-typed supervisor job through the whole loop under a mock provider: it runs a team, a stage's ask parks under unattended mode, the run verifies, the job.run.completed report payload carries the parked ask, and answering via the asks store injects into the origin session so a resumed turn sees the decision. The report payload and replio jobs status/replio jobs run output now flag parked asks from the run (parked_asks_for_run walks the run session's sub_sessions). New replio jobs add-supervisor (and /jobs add-supervisor) scaffolds a leader-typed supervisor job from a supervisor task template, approved by default, one command away from the daemon. Docs synced (jobs.md Supervisor overnight run runbook, testing.md coverage + live run, PLAN.md, TODO.md). The bundled leader type gains grant_permission/ask_policy and a team-aware prompt, so it delegates bash/web it denies itself to team stages. Tests: tests/test_autonomous_supervisor.py + test_jobs.py parked-ask report + render_status assertions
  • Report-backs - the core dispatch + in-band surfaces behind the replio-core-webhook connector. The JobScheduler fires one job.run.completed event per completed run (final status after retries, both the normal and early build-failure paths) to a registered report service via a new optional plugin_manager and _dispatch_report(job, run). The payload carries job/status/duration/reason/session/started_at/finished_at/content/worktree/memory, service exceptions are logged and never fail the run, and a missing service is a no-op. In-band: render_status gains a last run: line (status, duration, reason, session) for replio jobs status//jobs status. replio jobs run//jobs run add a summary: line from the run memory, and the delegate/team REPL sub-footers now append status session via a new note arg on UI.footer (ReplUI/NullUI/HeadlessUI). Docs synced (config.md, jobs.md Report-back, plugins.md services, testing.md, AGENTS.md). Tests: tests/test_jobs.py TestSchedulerReport + render_status last-run assertion
  • replio-core-webhook (bundled plugin) - a report-back connector that delivers completed job-run reports out-of-band. It registers services['report'] and POSTs a completed run's JSON report (job, status, duration, reason, session, timestamps, content, worktree, memory summary) to the report.webhook config URL with a short timeout. An empty URL is a no-op and a failed POST is logged without raising. Stdlib urllib only. Tests: plugins/replio-core-webhook/tests/
  • Pending-ask inbox - an unattended run's ask target='human' now parks as a pending request in .replio/asks.json (returned to the agent as [parked] Ask #<id> ...) instead of erroring or silently routing to the lead, so an overnight supervisor can finish and still ask. New AskStore (src/replio/asks.py): id/question/context/options/origin/kind/permission/status/answer + timestamps, thread-locked atomic writes, add/find/list(status)/answer. The park hook lives in the ask tool handler (tools/ask.py): under unattended mode a human-routed direction ask parks, a lead-less target='lead' parks, and a permission ask routed human (or forced by an ask ceiling) parks while route auto still grants once to the lead. Answering (/asks answer <id> <text> or the serve API) marks the ask answered and injects a user message [answer to parked ask #<id>] <answer> into the origin session, so the next turn on that session resumes with the operator's decision in context (full in-place mid-loop resume stays a TODO). Surfaces: /asks command (list [all|pending|answered], show <id>, answer <id> <text>, answer reloads a current origin session), serve GET /asks + POST /asks/<id>/answer (200/404/400), and job engines (scheduler._build_engine) now run unattended: true so scheduled/durable runs park human asks the same way. Docs synced (config.md Unattended mode, security.md, commands.md, api.md, jobs.md, session.md, testing.md, AGENTS.md). Tests: tests/test_asks.py, plus park-behavior updates in tests/test_unattended.py
  • Unattended mode - unattended: true guarantees nothing in a turn reads stdin at any depth, so an overnight REPL run cannot freeze on a prompt. The root engine drops its terminal UI (_ask_ui) in __init__ and never propagates it to sub-agents (_new_sub_engine carries the flag into each sub-config), confirm-gated tools auto-deny in _confirm_tool ([cancelled] User declined ...), and ask target='human' at the root returns the non-blocking "no one to answer" error while a sub-agent's human ask routes to its lead agent instead. An unapproved type/team model is no longer prompted under unattended mode - it is denied unless the headless approve_models flag is set. Enabled per run with replio --unattended (transient config.apply, not persisted), in config, or live via /unattended (status/on/off, sets config + Engine.set_unattended). New confirm_timeout (seconds, default 0 = forever) makes attended confirm/ask prompts self-limiting: ReplUI.confirm/ask now read via a select-based _timed_input and auto-deny/return None when the timeout elapses. Docs synced (config.md schema + Unattended mode section, security.md, commands.md, testing.md). Tests: tests/test_unattended.py
  • team tool - a core, model-invokable team(name, task) runs Engine.run_team and returns the final stage's answer, so a lead agent can orchestrate a whole pipeline instead of delegating each stage itself. Permission is category delegate, resolved per invocation like delegate: a stage type with delegate: "deny" disables the team, "ask" confirms it, otherwise it runs. Each stage's carve is still capped by the caller's grant_permission, and a stage whose requested permissions get clamped adds a (reduced permissions for: <type>) note so a silently degraded run is visible. run_team now refuses a team already on the current stack (team_cycle) and stops at max_team_depth nested team runs (default 2, team_depth), with _team_depth/_team_stack propagated into sub-engines, so a supervisor stage that itself runs teams cannot recurse forever, and config gains max_team_depth. The tool registers loop=True, so /tool team runs a persisted agent-loop turn. Docs synced (teams.md, swarm.md, tools.md, config.md, testing.md, AGENTS.md). Tests: tests/test_team_tool.py
  • Delegation permission ceiling + in-run permission grants - agent types gain grant_permission (the maximum category actions an engine may hand down) and ask_policy (routing for the ask tool by kind). A sub-agent's effective carve is now the caller's tool_permission narrowed by the type carve and capped by the caller's grant_permission (default: the caller's own carve), so a type can no longer widen a category above the caller - the documented no-escalation invariant is enforced by resolve_permissions/resolve_grant_ceiling in types.py (previously dict.update let a type carve raise a denied category). A type that sets grant_permission may delegate categories it does not use itself (a supervisor with edit/bash denied for itself can still hand them to an implementer while never running them). The ask tool gains kind (permission/direction, default direction) and permission (tool or category). A kind='permission' request routes by ask_policy.permission (auto = lead decides, human = operator, deny = disabled), is always capped by the ceiling, and on approval adds a one-shot grant on the asking sub-agent - consumed by the next matching call, with the operator able to grant always for the rest of that sub-agent's run (the lead only ever grants once). Grants never propagate to grandchildren and are recorded in the session permissions audit array (scope, granted_by), and the granted tool is re-added to the provider schema for the next loop round. ToolPolicy gains grant/grant_for/consume, and _init_tooling splits policy construction from a per-round _tool_schema() refresh. Config defaults grant_permission: {} and ask_policy: {permission: auto, direction: human}, and job --type engines get the same clamp. Docs synced (types.md, security.md, tools.md, swarm.md, config.md). Tests: tests/test_delegation_permissions.py

v0.29.0 - 2026-09-09

  • Realigned the headless CLI to the internal slash commands - replio models now mirrors /models: bare replio models shows the configured/approved models.json history grouped by provider (> active, (key) marker, No models configured yet when empty) and replio models list [provider] probes a provider's advertised models live (default: current provider, 1 exit on a failed probe). replio plugins enable|disable <name> joins the existing CLI plugin subcommands (toggles the plugins config list, applies on next start, mirroring /plugins). replio config reload re-reads the config files from disk (mirrors /config reload). replio jobs and replio export were verified aligned. Docs synced (commands.md, testing.md). Tests: test_cli.py TestCliModels rewritten + TestCliPlugins enable/disable cases, test_config.py reload case
  • Pluralized the registry commands - /type -> /types, /team -> /teams, /skill -> /skills, matching the /model vs /models convention that catalog commands take plural names (no aliases - breaking). Usage strings, tab completion, and docs updated (commands.md, types.md, teams.md, skills.md, swarm.md, plugins.md, providers.md, testing.md, AGENTS.md, README.md, VISION.md, use-cases/developer.md). Tests: test_types.py/test_teams.py/test_skills.py/test_team_run.py dispatch on the plural names
  • Split /session into /session (active session) and /sessions (saved-session catalog) - the active session is shown or switched with /session (bare shows the current session, new/load <name>/save), while the catalog lives under /sessions (bare lists saved sessions with sub_ children annotated, plus preview/delete/export), mirroring the /model vs /models split. Removed /session list/preview/delete/export (no aliases - breaking). Tab completion follows (/session load, /sessions preview|delete|export). Docs synced (commands.md, session.md, jobs.md, testing.md, AGENTS.md, architecture.md). Tests: session command/completion/export tests moved to the new surfaces
  • Consolidated model commands - /model now shows or switches only the active model (/model <name> sets it on the current provider, /model <provider>/<model> switches provider and model together), and /models owns listing: bare /models shows the approved/configured models.json history grouped by provider (> marks the active one), /models list [provider] probes a provider's advertised models live (default: current provider). Removed /model list, /model list --online, the /model-list alias, and the --online flag. The "(configured model not in the list)" note moved into the shared online renderer, and /connect's pick-a-model hint points at /models list <provider>. Docs synced (commands.md, providers.md, config.md, testing.md). Tests: test_commands.py model suites rewritten (bare /model current, /models configured listing + active marker, /models list default/<provider>/error/empty/note, /models usage)
  • Loop-enabled tool turns - a tool registered with loop=True (starting with delegate) invoked via /tool now runs a real persisted agent-loop turn instead of a one-shot _run_tool + print. New Engine.chat_tool(name, args) seeds _agent_loop(seed_tool=...), which executes and persists the tool call and result, then streams the model's final answer in the same turn, so the plan, results, and answer land in the session and a later prompt can reference the work. ToolRegistry gains the loop registration metadata and loop_for(), and /tool routes loop tools through chat_tool while other tools keep the direct-print path. Docs synced (tools.md metadata table, AGENTS.md adding-a-tool metadata list). Tests: test_delegate.py (test_tool_command_delegate_runs_loop_turn) and test_tool_registry.py
  • Sub-agent ask reaches the operator - a config whose tool_permission override replaced the whole dict dropped the ask: allow default, so sub-agent ask calls auto-declined on the NullUI confirm gate and never reached the root terminal. The ask tool registration stays permission='ask' (the operator channel is the handler's own routing, not the confirm prompt). With ask: allow restored, target='human' routes to the root ReplUI via the inherited _ask_ui and target='lead' to the delegating engine. Regression test in test_ask.py (test_subagent_human_ask_reaches_operator)

v0.28.0 - 2026-09-07

  • Externalized the vendor providers to bundled plugins - ollama, openai, groq, anthropic (single-provider plugins replio-core-ollama/-openai/-groq/-anthropic) and opencode/opencode-go (shared replio-core-opencode, keeps the x-opencode-session + replio/<ver> UA base). Core keeps BaseProvider/OpenAICompatibleProvider (providers/base.py), the PROVIDERS registry (now only the openai-compatible fallback), detect_provider, merged_providers(config) (loads plugin providers for standalone paths like fleet model-ref unfolding), and engine resolution. New HOST_PATTERNS class attribute replaces the hardcoded detect_provider host list - plugin providers auto-detect in /connect <url>, with the longest pattern winning (so opencode.ai/zen/go beats opencode.ai/zen). Core PROVIDERS wins on name conflicts with plugin registrations, which also keeps the mock-provider test pattern (patch('replio.providers.PROVIDERS', ...)) working. Default plugins config gains the 5 provider plugins, and a backfill migration (Config._migrate_plugins) appends them to existing non-empty plugins lists so vendor providers stay available after the upgrade - the migration is marked for removal in a future version (TODO). Vendor unit tests moved into the plugin suites (defaults, headers, endpoints, reasoning payloads, opencode session/UA, ollama streaming). Core test_providers.py now covers the substrate (host-pattern detection, merged registry). Docs synced (providers.md bundled table + auto-detection + adding-a-provider, plugins.md HOST_PATTERNS + name-conflict precedence, config.md provider list, testing.md coverage map, AGENTS.md, architecture.md). Tests: test_providers.py rewritten, test_providers_registry.py resolves through merged providers, test_bundled_plugins.py includes the provider plugins, plugin suites for the 5 provider plugins
  • Streaming delta null-safety - opencode-go (DeepSeek-style) chunks carry explicit nulls (content, reasoning_content, tool_calls), and the base provider iterated delta.get('tool_calls', []), raising 'NoneType' object is not iterable on the first turn. Streaming and non-streaming paths now treat null fields as absent (delta/choice fallbacks, tool_calls or [], content or ''), and a missing choices array returns an error event instead of crashing. Regression test in tests/test_ollama_provider.py (test_null_fields_in_delta_are_ignored)
  • OpenCode providers send x-opencode-session - Go rejects chat requests without it (MissingSessionID). New shared OpenCodeProviderBase (providers/opencode_base.py) generates a stable per-conversation session id (a UUID per provider instance, overridable via a session_id kwarg) and adds it to every request header, and switches the opencode providers' User-Agent to an identifying replio/<version> per the OpenCode docs (passes the Cloudflare edge check, unlike Python-urllib/*). Zen and Go both use the header. Other providers keep the browser-like user agent. Docs in docs/providers.md. Tests in tests/test_providers.py (TestOpenCodeHeaders: present/stability, override, identifying UA)
  • Browser-like User-Agent on provider requests - _headers() now sends Mozilla/5.0 (...) instead of urllib's default Python-urllib/<ver>, which Cloudflare bot protection rejects with HTTP 403: error code: 1010. This fixes /connect (the GET /v1/models probe) and /model list --online for the Cloudflare-fronted opencode.ai endpoints, and prevents the same block on chat streaming/non-streaming. One header covers the model probe, _post, and stream_sse since they all go through _headers(). Docs in docs/providers.md. Test in tests/test_providers.py (TestProviderHeaders.test_browser_user_agent)

v0.27.0 - 2026-09-05

  • ask tool (core) - a model-facing tool that pauses the run and routes a decision or permission request to an answerer, so sub-agents and team stages get decisions mid-run instead of returning open questions. New src/replio/tools/ask.py (register_ask_tool, registered in Engine._init_tooling like delegate), schema question (required) plus context/options/target. target='human' (default) prompts the operator at the terminal via a new ReplUI.ask free-text prompt (? Answer:). A sub-agent's ask is prefixed with its sub_<...> session name because delegation and team stages run synchronously in-process while the terminal is free. target='lead' answers through a bounded non-streaming consultation with the delegating engine's model (_lead reference plus _ask_ui propagation in _new_sub_engine. The prompt includes the sub-agent's delegated task). A root engine has no lead and falls back to human. Headless roots with neither (run/serve/jobs) get an Error: ask has no one to answer ... result and the run continues without pausing - the async "pause a running job and resume on reply" variant stays tracked separately (TODO). The ask itself is not additionally gated: tool_permission.ask defaults to allow (operators can tools.deny: ["ask"]). The question (tool args) and answer (tool result) persist in the asking session and the call lands in the session permissions audit array. NullUI/HeadlessUI gain an ask stub returning None. Docs synced (tools.md Asking the human or the lead, config.md tool_permission, security.md, swarm.md, jobs.md, AGENTS.md). tests/test_ask.py (9: schema/registration, human answer/empty-cancelled/context-options, no-channel error, lead consultation + fallback-to-human + root-fallback, sub-engine _lead/_ask_ui inheritance, full-loop persistence) and the schema-names test in test_engine.py
  • /connect is provider-only now - it connects a provider and stores its API key (and any custom base URL) in the global providers.json registry, and never touches the model (picked separately with /model). Bare /connect shows an interactive picker of known providers (core + plugin, (key) markers) accepting a number, a name, or a URL. /connect <name> presets a known provider's default base URL and prompts only the API key (API key [<stored>]: re-enters a missing or stale key, Enter keeps it). /connect <url> detects a known host or a plugin provider's default URL, otherwise creates a named custom provider with the name derived from the host (https://llm.acme.example/v1 -> acme-example) - /connect <url> <name> overrides the name. Stored custom providers appear in the picker for reconnecting. All forms keep the connect_check probe (GET /v1/models) with the Save anyway? path, write provider/base_url into config (model untouched, no model approval here anymore), re-init the provider, and print the Connected to <provider> (<base_url>) result plus a /model hint. _resolve_provider_factory now resolves a registry-named custom provider as OpenAICompatibleProvider so its stored key is found on reload. Docs synced (providers.md Setting up/Auto-detection, commands.md, config.md registry base_url semantics, plugins.md provider default-URL matching, testing.md). Tests: connect suite rewritten for the provider flow (18 cases: picker incl. stored-custom reconnect, named preset + stored-key keep/re-enter + unknown-name, URL known-host/plugin default match/name derivation/override/bare hostname, probe accept/decline, connect_check off, config model untouched)
  • Model refs unfold, gated on approval - a provider/model ref (e.g. opencode-go/deepseek-v4-flash) resolves to the provider, its default base URL, and the bare model wherever a model is set: config.model, /model <ref>, --model <ref>, and an agent type's model field (a type now pins provider and model together). resolve_model_ref in providers/registry.py unfolds only known providers with a default base URL. The resolved model must be approved (present in models.json): the REPL asks on load and in /model, /team run pre-checks the stages' type models and asks once, and headless runs auto-approve an explicit --model but deny a type/team-referenced model unless --approve-model is passed (replio run, replio jobs add, replio fleet config. Jobs gain an approve_model field and the scheduler grants it, fleet config pre-approves into models.json). A denied ref sets an engine _provider_error that makes chat() return an error result, and a ref naming a provider without a key still switches but prints run /connect <provider>. Engine gains approve_models, unfold_ref, and _ensure_model_approved. _new_sub_engine/run_subagent/run_team gate type models. Docs synced (providers.md Model refs and approval, config.md, commands.md, types.md, jobs.md, fleet.md, testing.md). Tests: resolve_model_ref (7), engine unfold/gate/deny/grant/chat-short-circuit/subagent-gate/type-unfold/team-precheck, /model ref + prompt, CLI approval wiring, jobs approve_model round-trip
  • models.json reshaped to approved-model history - ModelEntry is now {provider, model, added_at, last_used} with no API keys or base URLs (those live in providers.json). ModelRegistry keys by (provider, model) (find/put/touch/remove, grouped kept, api_key_for dropped, no 0600), and old per-model-key entries are read for provider/model only - no migration, keys are re-entered via /connect. /model list groups approved models by provider with > for the active one and a (key) marker sourced from the provider registry, /model list --online takes base_url/api_key from the provider registry, /model <name> touches (provider, model), and the /connect picker shows [provider] model reusing the stored provider key. Docs synced (config.md, providers.md, commands.md, testing.md). tests/test_models.py rewritten (approved-history shape, old-shape dropped without migration) and test_commands.py connect/picker/model tests updated to assert keys on the provider registry
  • Provider registry - ~/.config/replio/providers.json stores active connections keyed by provider name: one API key per provider, and a custom base_url only when it differs from the provider's default (the class default applies otherwise). The engine now resolves the API key and a fallback base_url from this registry (Engine.providers, _reinit_provider/check_connection/list_models) instead of models.json, and /connect writes the provider entry alongside the model entry. File written 0600 when it holds keys, atomic replace, corrupt-file tolerant. ProviderRegistry/ProviderEntry in providers/registry.py. Docs synced (providers.md, config.md, security.md, commands.md, testing.md). tests/test_providers_registry.py (15 tests) plus test_engine.py registry-key and base_url-fallback coverage
  • Sequential team runs - Engine.run_team(team, task) executes a team stage-by-stage through the same in-process sub-engine as delegate. Each stage builds a per-run brief (team name + original task, a ## Stage N result block per prior stage capped at 4000 chars with a truncation marker, the previous stage's handoff_note, the shared team memory, and the stage's task_hint), runs via run_subagent into a fresh sub_<ts>_<parent> session, and records the result. A failing stage stops the run. After the run the whole team run is summarized (seeded with the prior memory, via the same compaction summarizer as /compact. A per-stage fallback stores when the summarizer fails) and written atomically to .replio/teams/<name>/memory.md (new team_memory_path/read_team_memory/write_team_memory helpers in teams.py, mirroring the jobs .memory.md pattern), which the next run's briefs read back. _new_sub_engine/run_subagent accept a mode override so a stage's mode applies to its sub-engine while an empty stage mode inherits the caller (previously sub-agents always ran build). delegate keeps the build default. New TeamRunResult dataclass. /team run <name> <task> runs a team from the REPL (one line per stage, final result, memory path), registered in the /team subcommands. Docs in docs/teams.md (Running a team section) and docs/swarm.md. tests/test_team_run.py (brief builder, sequential execution + exact brief persistence + parent linkage, stage mode override/inheritance, stop-on-failure, unknown stage type, zero stages, memory write/seed/fallback, /team run command). Completed PLAN M1 Engine.run_team. The remaining sequential-runs TODO scope (persistent member sessions for recurring teams) moves to M3
  • Renamed persona to agent type across the product - the concept is a named agent definition bundling identity (persona), function, authority, capability, expertise, and archetype into one reusable profile, and type is the neutral umbrella that covers all of them without colliding with role (chat message roles in the session/API), function (OpenAI function calling), or profile (the existing permission-profile language). Breaking rename with no legacy aliases: personas.py -> types.py (AgentType / TypeRegistry), bundled_personas.json -> bundled_types.json, /persona -> /type, --persona -> --type, personas.json -> types.json, the persona field in jobs and team stages and the delegate tool argument -> type, and the plugin hook register_personas -> register_types. Existing personas.json files, --persona flags, /persona calls, and plugins using the old hook must be updated. docs/personas.md -> docs/types.md documents the six axes and the naming rationale. Tests updated across the suite (test_types.py, delegate, subagent, teams, jobs, fleet, plugins, tool-policy). Docs, README, VISION, PLAN, TODO, AGENTS, and the site synced

v0.26.0 - 2026-09-03

  • Bundled web plugin renamed - replio-core-websearch -> replio-core-web (it ships both search and page fetching, so the old name understated it). Manifest bumped to 0.14.0, default plugins config, tool/plugin docs, and the web-search-unavailable message updated. Existing plugins configs that named the old plugin must switch to replio-core-web
  • GitHub Pages website - site/ mkdocs project (mkdocs-material theme) plus a deploy-site.yml workflow that assembles the site from repo sources on push to main (Overview, Vision, Roadmap, Backlog, Changelog, and the reference docs minus use-cases/vs) and publishes the static build to a gh-pages branch, so the site is regenerated every release with no drift. The app stays zero-dependency, mkdocs is CI-only. README gains a site badge and link. TODO/PLAN "Docs site" items trimmed to the remaining ReadTheDocs part
  • code_test interpreter resolution - replio-core-dev now resolves a leading python/python3 in the test command (dev.test_cmd, default python -m unittest discover) to sys.executable, so the default suite runs on machines with only python3 (no python on PATH). Custom commands are untouched. Docs in docs/tools.md, tests in plugins/replio-core-dev/tests/test_tools.py (default + explicit-config resolution)
  • OpenCode Zen + Go providers - opencode (https://opencode.ai/zen/v1, default kimi-k3) and opencode-go (https://opencode.ai/zen/go/v1, default deepseek-v4-flash) as OpenAI-compatible providers sharing the OpenCode API key (registered via /connect like any provider, no env var). detect_provider() recognizes opencode.ai (/zen/go path picks opencode-go, otherwise opencode) so /connect auto-switches. Model refs accept the opencode/<model-id> / opencode-go/<model-id> conventions as well as bare ids - the prefix is stripped from the payload (_payload override). The model-list endpoints are an inventory, not an entitlement check: inference still needs the matching paid subscription. Docs in docs/providers.md (built-in table, detection, reasoning row), docs/config.md, README.md. Tests in tests/test_providers.py (defaults, detection, registry, endpoints, ref-prefix stripping). Provider externalization to bundled plugins is tracked in TODO/PLAN
  • Tool-use evaluation harness - replio eval runs task fixtures through the headless agent loop and reports tool-use metrics. New src/replio/eval.py defines the JSON fixture model (task prompt, provisioned worktree files, expected tool-name trace, declarative verifier with exact/must_include/avoid/max_calls/min_calls/args), the runner (each fixture gets an isolated temp worktree with its own .replio/, a throwaway engine with HeadlessUI(auto='allow'), and a cwd switch so relative tool paths resolve there), and per-fixture + aggregate metrics (tool-call accuracy, redundant identical calls, engine + tool errors, provider tokens). replio eval list / run (with --fixture, --provider/--model/--base-url, --compare, --output table|json) are wired through main.py + cli.py. Fixtures are discovered from the new bundled replio-core-eval plugin (via a new register_fixtures plugin entry hook, PluginManager.register_fixtures), ~/.config/replio/eval/*.json, and .replio/eval/*.json, merged by id with local winning. The bundled catalog ships five fs fixtures (read-file-lines, find-then-read, list-directory, grep-symbol, page-large-file). Eval defaults to read/list/web allow and edit/bash/mcp deny, overridable per fixture. Docs in docs/eval.md (linked from docs/index.md, docs/commands.md, docs/plugins.md, AGENTS.md), and docs/writing-tools.md evaluating-tools section now points at the harness. Tests: tests/test_eval.py (model, verifier, metrics, discovery, cwd isolation), tests/test_cli.py eval coverage, tests/test_plugins.py register_fixtures hook + failure marking, plugins/replio-core-eval/tests/test_fixtures.py

v0.25.0 - 2026-09-02

  • Project instructions file - per-worktree AGENTS.md (or any project_instructions name, "" disables) is auto-loaded into the system prompt as a leading system message, capped at 20000 chars, present files only. It composes with system_prompt and the mode instruction and applies to sub-agents too (they share the worktree). Docs in config.md and tools.md. Tests: test_modes.py (instructions_file_section loading/absent/disabled/default/truncation) and test_engine.py (injection, absent skip, unset skip)
  • run_command command allowlist - tool_permission.bash_allow (default []) restricts run_command to commands whose every chained segment (over &&/||/;/|/&) starts with an allowed prefix. Heredocs and multi-line commands are rejected outright. Matching commands fall through to the normal bash action, others are denied. Implemented as a per-invocation policy resolver on run_command: the engine now injects the merged tool_permission into resolvers that declare a _permissions argument (a general capability, not a run_command special case), so it composes with modes, tools.deny/tools.allow, and worktree escalation. Docs in config.md (new bash_allow section) and tools.md. Tests: exec-suite allowlist cases (prefix, chain, pipe, heredoc, multi-line, empty) plus engine-level allow/deny
  • code_test / code_lint / code_format wrappers - new bundled replio-core-dev plugin. Each runs the project command from a config key (dev.test_cmd default python -m unittest discover, dev.lint_cmd default ruff check ., dev.format_cmd default ruff format .), appends an optional target, and reports exit code + output. bash permission, cwd scoping, hard timeout clamp. Added to the default plugin list. Tests: plugin suite (unittest run, config commands, target append, failure/unknown/timeout/truncation, metadata)
  • git tool - new bundled replio-core-git plugin. Read-only git (status/diff/log/branch/show/rev_parse, read permission, worktree-scoped cwd) and git_commit (add/commit, edit permission, always ask-gated via its resolver). Both run git via subprocess argv (never a shell string) with a hard timeout. No push/merge/checkout/history rewrite. Added to the default plugin list. Tests: plugin suite against a temp repo (status/diff/log/branch/show/rev_parse, add/commit/all, missing cwd, permissions, aliases)
  • file_edit tool - new bundled replio-core-edit plugin. Targeted search-and-replace at a path (old -> new, optional count, 0 = all), with a difflib diff preview in the status line. edit permission, path scoping, aliases edit, param aliases file/old_text/new_text. Complements file_write for surgical single-hunk edits. Added to the default plugin list. Docs in tools.md bundled-tools table. Tests: plugin suite (first/replace-all/count/delete/missing/directory/not-found/empty/aliases/metadata/status)
  • New bundled plugins ship in the default plugins list (replio-core-edit, replio-core-git, replio-core-dev) - test_bundled_plugins.py asserts discovery, origin, tools, and the default config set

v0.24.0 - 2026-09-01

  • Default tool-result cap - tool_max_result_chars now defaults to 100000 chars (Anthropic ~25k-token guide, 0 = unlimited), so every tool result is bounded by default: the fs tools (file_read/list_dir/glob/grep), run_command, and MCP tool results are each cut at a line boundary with a trailing ... (truncated) marker. list_dir gains an entry cap via the new list_dir_max_entries config (default 200, 0 = unlimited) with a ... (showing first N of M entries) marker, so a large tree stays bounded regardless of name lengths and the model is told how many entries there are. Docs synced (config.md, tools.md, writing-tools.md). Tests: fs suite (entry cap + default-boundary truncation) and test_config.py defaults
  • Default tool names namespaced, old names kept as aliases - web_fetch merges the former open and fetch_page web tools into one canonical fetch tool (url or a web_search result id, offset paging), file_read/file_write replace read_file/write_file, and web_search stays canonical. All old spellings still resolve at call time (open, fetch_page, read_file, write_file, read, write, view, search, web, ls, find, bash, exec), so existing configs, /tool calls, and session logs keep working. Only the advertised provider schema changes (13 -> 12 canonical tools). The fs tool error hints now point at the canonical names, and the noise_tools default trims all three fetch spellings. Docs synced across tools.md, writing-tools.md, config.md, session.md, plugins.md, fleet.md, security.md, usage/programming.md, use-cases, and AGENTS.md. Tests updated for the new canonical set (test_engine.py, test_commands.py, test_cli.py) plus websearch-suite coverage for the merged web_fetch
  • Tool schema advertises canonical names only - ToolRegistry.schema_filtered() now returns one schema per registered tool and drops the alias entries (bash/exec, read/view, ls, find, search) from what is sent to the provider (20 -> 13 tool definitions per request). Aliases still resolve at call time, so /tool, /help, policy, confirm, glyphs, and the unknown-tool hint are unchanged. A model that still calls a dialect name gets it absorbed. Docs synced (tools.md registration table). Tests: test_tool_registry.py (canonical-only advertisement, denied canonical stays hidden even when an alias is allowed) and test_engine.py (build-mode schema is exactly the 13 canonical names)
  • Agent-loop cancellation and tool-dialect hardening - Ctrl-C now cancels the running turn instead of declining one tool or killing the REPL. ReplUI.confirm re-raises KeyboardInterrupt (EOF still returns False), and Engine._agent_loop catches it during streaming, tool execution, and retry sleeps, merging partial output, printing (cancelled), persisting state, and returning TurnResult(status='cancelled') so the REPL returns to the prompt (headless replio run exits non-zero). Unknown tool calls now return Error: unknown tool "X". Available tools: ... via new ToolRegistry.is_registered()/primary_names(), so the model can self-correct instead of retrying a hallucinated name. Web-search and fs dialect absorption: web_search gains the search alias, grep gains the find alias (its query -> pattern alias already maps the model's args), and open treats a URL string passed in id as the URL. Tests: test_agent_loop.py (cancel mid-stream/after-tool, unknown-tool hint), test_ui.py (confirm Ctrl-C/EOF), websearch suite (search alias, URL-as-id), fs suite (find alias)

v0.23.0 - 2026-08-31

  • Skills registry - Skill (name, markdown content, optional description/tags) and SkillRegistry in new src/replio/skills.py: skills are flat <name>.md files in the local .replio/skills/ and global ~/.config/replio/skills/ dirs plus the in-memory plugin layer (register_skills hook, registry.add_plugin({...})), precedence plugin < global < local. Engine.skills wires the hook on first access, /plugins install/update/uninstall refresh all three registries live, and /skill manages the catalog (list/show/new/remove, plugin skills not removable). Persona skills (previously inert) are resolved and injected into the sub-agent system prompt as a ## Skills section (_new_sub_engine) - missing skills skip silently, empty-prompt personas keep the section as their whole prompt - and the same section is appended for jobs with --persona (scheduler._build_engine, via the shared skills_section helper). Docs in docs/skills.md, linked from docs/index.md. personas.md skills field now points at the live registry, and plugins.md hook contract updated. tests/test_skills.py (registry, section, command), test_subagent.py injection cases, test_jobs.py _build_engine injection, test_plugins.py skills hook against a real registry
  • PluginManager._bundled_dir() hardens its fallback for source checkouts - when the replio.plugins.bundled subpackage cannot be imported (e.g. running with PYTHONPATH=src instead of an installed package), it now resolves the repo-root plugins/ directory before the stale in-package path, so bundled plugins load without pip install -e .. test_plugin_suites inherits the fix. Docs note in docs/testing.md (absolute PYTHONPATH for the detached-fleet daemon)
  • Teams registry - shape-only named team pipelines: Team (name, ordered stages, description, tags) + TeamStage (persona, optional mode/task_hint/handoff_note, plain-string shorthand) in new src/replio/teams.py, and TeamRegistry with the same four-layer merge as personas (bundled < plugin < global < local, per-field with stages replaced wholesale). src/replio/bundled_teams.json ships the two pre-carved pipelines referencing the bundled personas - writing (researcher > writer > referencer > editor) and programming (planner > programmer > tester > code-reviewer) - with concrete task hints and handoff notes. Plugins contribute teams via the existing register_teams entry hook (registry.add_plugin(...), in-memory layer, reload(plugin_manager=None) re-applies like personas), and Engine.teams wires it on first access. /plugins install/update/uninstall refresh both registries live (_refresh_registries). /team manages the catalog (list with tag filter + stage chain, show, new/override, remove, bundled remove rejected, origins marked bundled/plugin/global/local/merged). run lands with Engine.run_team (PLAN M1). Subsumes the TODO "Jobs registry - named team configurations" item. Docs in docs/teams.md (schema, layers, roster), linked from docs/index.md + docs/swarm.md. tests/test_teams.py (registry + command coverage) and test_plugins.py integration (real TeamRegistry hook dispatch, engine.teams)
  • Plugin contribution hooks - entry modules may now define register_personas(registry) (personas contributed via registry.add_plugin(entry), same shape as personas.json), register_teams(teams), and register_skills(skills). PluginManager gains the three matching dispatch methods on a shared _run_hook helper (extracted from register_tools/register_commands, identical semantics: loaded modules only, a hook exception marks the plugin error with f'<hook> failed: ...'). PersonaRegistry gains a fourth in-memory scope between bundled and global, so precedence is bundled < plugin < global < local and a personas.json entry always overrides a plugin-provided persona. origin() reports plugin/merged and /persona lists plugin personas as (plugin). New PersonaRegistry.reload(plugin_manager=None) re-reads the three files, clears the plugin scope, and re-applies register_personas when a plugin manager is passed. Engine.personas wires the hook on first access (sub-agents inherit it via the shared plugin manager), and /plugins install/update/uninstall refresh the running REPL's personas registry live (tools/commands still activate on next start). Plugin personas never touch disk - add_plugin writes nothing. Docs synced (plugins.md entry contract + precedence, personas.md layers, testing.md). tests/test_plugins.py (persona/team/skill hooks + failure marking + engine integration) and tests/test_personas.py (plugin scope merge, origins, reload re-read + re-apply) grow to 81 combined

v0.22.0 - 2026-08-27

  • Fleet orchestration supervisor - replio fleet runs many scoped replio serve agents as supervised children. src/replio/fleet.py ships the declarative roster .replio/fleet.json (AgentDef: name/dir/enabled/prefer_port/max_restarts/command test seam) and runtime state .replio/fleet.state.json (pids, ports, statuses, restart counts), plus FleetController: bind-probe port allocation (find_free_port(preferred, lo=8780, hi=8890)), urllib GET /health probing (2s timeout), spawn as [sys.executable, -m, replio, serve, --host, 127.0.0.1, --port, <n>, --path, <dir>] with per-agent stdout/stderr to <dir>/.replio/logs/<name>.log and REPLIO_FLEET_PORT/DIR/AGENT exported to children, and a supervisor loop with restart policy (5s backoff doubling to 60s, max_restarts budget pausing an agent as crashed, enabled gate), health-threshold failures, and graceful SIGINT down. replio fleet up runs the loop in the foreground (Ctrl-C = graceful down), up --detach re-execs a background daemon that signals SIGINT/SIGTERM to stop. CLI: init (scan subdirs holding .replio/config.json), add, remove, up, down, status (agent/enabled/port/pid/state/restarts/last-error table), restart [name|all] (stop + reset backoff + relaunch), logs <name> [n] [--follow], and config <name> per-agent config generation (--provider/--model/--persona/--system-prompt/--mode/--tools-deny/--tool-permission, selected keys only, persona prompt/model/permissions inlined, unknown persona errors). Docs in docs/fleet.md (Supervisor section), docs/commands.md, docs/testing.md. tests/test_fleet.py covers allocation, loopback probes, manifest/state round-trip and corruption tolerance, the full spawn > health > restart > down cycle, max_restarts give-up, disabled gate, env seams, the config CLI, and a detached-daemon end-to-end (25 tests, mock-only)
  • replio fleet CLI wiring - main.py fleet subcommand parser + dispatch, cli.py cmd_fleet with --path accepted before the subcommand like jobs
  • Config immutability is documented, not enforced: replio fleet config is the operator surface and a serve process has no config-write CLI path today. An engine-level guard stays an open TODO
  • Bundled plugins restructured to plugins/<name>/{src,tests} - plugin code lives in src/ (manifest entry points there, the loader puts the entry module's directory on sys.path, so sibling imports resolve from src/), and each bundled plugin ships its own unit suite in tests/. replio-core-fs + replio-core-exec suites moved out of test_machine_tools.py, replio-core-mcp out of test_mcp.py (now in-repo src/), and replio-core-websearch gains coverage for the DDG parser, display formatters, text extractor, and open target resolution. The core suite discovers the plugin suites via tests/test_plugin_suites.py. replio plugins test [name] runs them headless, and a missing plugins dispatch in replio main was fixed. test_tool_registry.py decoupled onto synthetic tools (plugin-specific registration metadata lives in the plugin suites), and the redundant mcp_* presence check was dropped from test_bundled_plugins.py. Packaging ships src/ + tests/. Docs synced (plugins.md, testing.md)
  • Test cleanup - dropped the redundant mcp_* tool-presence assertion from test_bundled_plugins.py (owned by the test_mcp.py registration suite) and single-sourced the fs/exec cap-config setup

v0.21.0 - 2026-08-26

  • Legacy support removal (breaking, dev-stage) - dropped the delegate_* session prefix: /session list annotates sub_* children only and old delegate_* files get no special handling. Dropped --max-context (per-run fresh session files plus the rolling run-memory summary make history capping unnecessary). Dropped the config migrations (plugins.enabled/plugins.deny -> plugins, and local api_key -> global). Old keys are simply ignored. Removed config.api_key entirely - API keys live only in the global model registry (~/.config/replio/models.json, via /connect), the engine no longer falls back to config.api_key (_reinit_provider/check_connection/list_models resolve from the registry), and /config + replio config treat api_key as an ordinary key (no forced-global write, masking, or 0600). Tests updated. Docs synced (config.md, providers.md, security.md, deploy.md, usage/programming.md, session.md, testing.md)
  • Per-run job session files and unified session naming - by default every job run gets a fresh session .replio/sessions/job_<YYYYMMDD>_<HHMMSS>_<name>.json (same-second collisions get a _2 suffix), so no single file grows unbounded. Retries within a run share that run's file, and --session opts into a stable growing session. The session-naming scheme is now type-prefixed across the board: interactive sessions auto-name as ses_<ts>_<slug>, delegation sub-agents write sub_<ts>_<parent-session> (the suffix is the calling session id, sanitized and capped), and jobs use job_*. /session list annotates sub_* children with their parent. run_subagent no longer auto-renames the sub-agent's session. Docs synced (docs/session.md naming scheme table + docs/jobs.md per-run session section, security.md/swarm.md/usage/programming.md sub_*). Tests updated (test_subagent.py/test_delegate.py filter by parent id) and tests/test_jobs.py grows to 88 (per-run naming, collision dedupe, --session stability)
  • Job run memory - every run (successful or failed) is summarized and written to a rolling .replio/jobs/<name>.memory.md via the same compaction summarizer as /compact, seeded with the previous memory so context carries. A fallback (Run <ts>: verified|failed + first part of the output/error) is stored when the summarize call fails. The memory file is injected into the next run as a ## Run memory system prompt block, so the model knows what previous runs did without the session file growing. replio jobs show <name> prints the memory path + preview. The file is human-editable like the task file and never breaks a run. Docs in docs/jobs.md (Run memory section). tests/test_jobs.py grows to 82
  • Job task definition as a linked Markdown file - replio jobs add <name> --file <path> --cron ... links the job to a task file (default .replio/jobs/<name>.md) that is re-read at the start of every run, so editing the .md is how you change the job - no re-adding. A missing file is created from a template (# <name> / ## Task / ## Done when / ## Notes) at add or edit time, and a task file deleted afterwards fails the run with a clear task file not found reason instead of silently using stale instructions. --prompt is now optional (at least one of --prompt / --file required). With both, the prompt is the short per-run trigger. New replio jobs edit <name> (and /jobs edit) opens the task file in $EDITOR, creating the template first. The scheduler composes the run's system prompt as persona prompt + task-file contents (## Job task) + --system-prompt, with the generic recurring-job prompt as the fallback. Job.task_file is persisted (worktree-relative when under the worktree). Docs in docs/jobs.md (task-file section) and docs/commands.md. tests/test_jobs.py grows to 77
  • Job runtime operations - replio jobs status is the journalctl-style view (state, fired count with ok/failed, last error, next run, uptime since creation, per-run approval state). replio jobs stop <name> (alias of disable) makes stopping discoverable. replio jobs show prints the last run's output. replio jobs run --verbose streams the live turn (tokens stdout, tool activity stderr) for Tmux/REPL demos, and headless run now prints the final answer. Tests grow to 68 in tests/test_jobs.py
  • Job memory across runs - the rolling run-memory summary (.memory.md, see the run-memory bullet) plus a fresh per-run session file replace the earlier stable-session design. The register history is capped at the last 100 runs. Without --persona or --system-prompt, runs inject a default recurring-job system prompt so the model understands the recurring task and past runs. Docs in docs/jobs.md
  • Human-in-the-loop per-run approval - --require-approval arms exactly one run at a time: after every run the job parks in a new waiting_approval state, the daemon refuses to fire it, replio jobs status//jobs status shows WAITING for approve, and replio jobs approve <name> (or /jobs approve) arms only the next run before it parks again. reject clears the grant. Manual run still overrides. This is the per-run gate on top of arm/disarm. Mid-run blocking (an ask tool pausing a running job) is documented as planned in docs/jobs.md and tracked in TODO
  • Richer job run spec - --persona (system prompt, model override, tool carve), --system-prompt, --mode, --provider/--model, --tools-deny, and --tool-permission were already present. The scheduler now falls back to a generic recurring-job system prompt and exposes Job.ready_to_run(). A job can already orchestrate a team by delegating to personas (delegate category defaults to allow on the job's tool carve)
  • docs/jobs.md rewritten - status model with waiting_approval, the three HITL gates (arm/disarm, per-run approval, planned mid-run blocking), per-run session + rolling memory semantics, --verbose run output. docs/commands.md gains status/stop/--verbose and the new add flags
  • Scheduled / durable jobs - a job is a named prompt plus a schedule (cron 5-field expression, interval seconds, or a one-shot at datetime), a tool carve, and durability settings (retries with exponential backoff, per-attempt timeout). Jobs live in .replio/jobs.json, one register per worktree. replio jobs list/show/add/approve/reject/enable/disable/remove/run/daemon and an equivalent /jobs REPL command manage them. A job is a human-gated workflow: add starts proposed and only approve (or --approval auto, or a manual run) activates it. The daemon never approves. Running sets executing, then verified (ok/truncated) or failed after retries. Every attempt is recorded in an append-only history. Jobs run on a stable job.<name> session (cross-run context, resumable trail) through a fresh headless Engine per attempt with HeadlessUI(auto='deny') - ask-gated tools are denied, a job reaches only its allow tools inside its worktree. Scheduling is deterministic: next_run_at is the single source of truth, recomputed strictly after each run, so a stopped daemon never replays missed windows, and a one-shot at job disables itself after running. The scheduler is single-threaded (jobs run sequentially in name order). Concurrency is future work
  • Cron scheduling engine (stdlib, in jobs.py) - the 5-field parser supports *, */step, a-b, a-b/step, and a,b,c lists on all fields with dow 0-7 (both 0 and 7 are Sunday). Day-of-month and day-of-week are restrictive (both must match). replio jobs daemon polls on --tick (default 15s) and runs due jobs, replio jobs run <name> [--no-retry] runs one now and exits 0/1. Per-job overrides (--mode, --provider/--model, --persona, --system-prompt, --tools-deny, --tool-permission) rebuild the engine from the same merge run_subagent uses. Docs in docs/jobs.md
  • Tests (tests/test_jobs.py, 57 tests) - cron parser field expansion and edge cases (leap day, month roll, restrictive day rule), next_run/compute_next_run/parse_dt, job model round-trip and the runnable gate, registry store (round-trip, remove, corrupt-file tolerance), scheduler run/tick under a mocked engine (verified/failed, retries with per-attempt history, unknown persona, one-shot at disables itself, approval gates), replio jobs CLI (add/approve/list cycle, auto-approval, bad cron, duplicates, run exit codes). Docs synced (commands.md CLI + /jobs tables, testing.md, use-cases/homelab, enterprise.md durable-workflows status)
  • Enterprise use-case note - the durable-workflows gap now points at what landed: scheduled jobs, retries with backoff, timeouts, resumability, and the proposed > approved > executing > verified > failed status model shipped as replio jobs. Dead-letter queues remain planned

v0.20.0 - 2026-08-25

  • Delegation workflow fixes - delegate now defaults to allow (runs without a confirm, DEFAULT_CONFIG.tool_permission.delegate and the per-persona resolver default, and a persona can still set delegate: "ask"), so /tool delegate no longer prompts. Sub-agent sessions link to their parent: Session gains parent_id and sub_sessions (persisted), run_subagent records both (/session preview shows parent/sub-sessions, /session list annotates delegate_* with (child of ...)). A sub-agent that finishes with no prose now returns a log-summary result instead of (no content) - files written (write_file), tool round counts, and the last bash line are read back from its session - keeping the delegate result informative for the lead model and the REPL. Direct /tool calls no longer double-print the delegate result (ToolRegistry.execute()/Engine._run_tool take an echo flag injected as a hidden _echo kwarg, and /tool passes echo=False). Docs synced (config.md, personas.md, swarm.md, commands.md, security.md, session.md, testing.md, AGENTS.md)
  • delegate tool (core) - run a task under a persona as a sub-agent and get its final answer. Permission resolves per invocation through a new permission_fn registration hook: ToolPolicy.action() now accepts the tool arguments and a per-tool resolvers map (policy stays the single resolution point, resolvers only refine a non-deny base and are skipped without args, so schema filtering is unaffected). The delegate tool's resolver follows the persona rule - a configured persona uses its own tool_permission (category delegate defaults to ask, new DEFAULT_CONFIG entry), a persona outside the registry is deny. Display is gated by the new delegate_echo config (default true): on, the final answer plus a sub footer (duration + completion tokens) print in the REPL. Off hides the result. Testable from the REPL via /tool delegate {"persona": ..., "task": ...}. Registered as a core tool in Engine._init_tooling, which also builds the resolver map. tests/test_delegate.py (8 tests) + resolver coverage in tests/test_tool_policy.py. Docs in docs/tools.md, docs/config.md, docs/swarm.md
  • In-process sub-agent engine - Engine.run_subagent(persona, task) executes a task under a selected persona and returns its TurnResult. A sub-engine is a fresh Engine built on a per-persona config: the persona's system_prompt becomes the system prompt, its model overrides the caller's model when set (else the caller's provider is reused), its tool_permission is merged over the caller's categories, and mode is forced to build so the carve alone decides edit/bash. It shares the caller's plugin manager and worktree, runs with a quiet NullUI (ask-gated tools auto-deny, so there are no interactive prompts), and persists a namespaced delegate_<persona>_<ts> session into the shared sessions/ dir. Engine.__init__ gains optional plugin_manager/provider injection knobs. Covers tests/test_subagent.py (9 tests). Docs expanded in docs/swarm.md
  • Persona tags - Persona gains a tags list for job-based grouping and filtering, using a controlled vocabulary (research, writing, programming, review) on the bundled roster (researcher = research+writing, editor = writing+review, code-reviewer = programming+review, etc.). /persona shows tags in list/show, and /persona list <tag> filters (unknown tags print the known vocabulary). List merge keeps the "local replaces lower layer" rule per field. Docs updated (docs/personas.md), tests grow to 27, and a Jobs registry (team configurations built on tags/personas) is noted as a future TODO item
  • Bundled default personas - the registry gains a third read-only bundled layer (src/replio/bundled_personas.json, shipped via package-data) with precedence bundled < global < local, mirroring bundled plugins. Ships two pre-carved teams: document (researcher, writer, referencer, editor) and programming (planner, programmer, tester, code-reviewer), each with a system prompt and per-persona tool_permission (researchers/editors are read-only, programmer/tester may run bash). Personas stay model-agnostic (model empty = inherit the caller). /persona shows (bundled)/(merged) origins, new <name> now overrides any existing persona (bundled included), and remove rejects bundled-only personas with override guidance. Docs updated (docs/personas.md roster table, docs/swarm.md). tests/test_personas.py grows to 21 tests
  • Personas registry - personas defined as a named catalog (system prompt, optional model override, optional skills and per-persona tool_permission), stored in single JSON files merged global (~/.config/replio/personas.json) then local (.replio/personas.json), field-by-field with local winning and empty local fields inheriting from global. PersonaRegistry (ModelRegistry-style, personas.py), /persona command (list / show / new / remove), Engine.personas accessor, docs/personas.md and a Personas and delegation section in docs/swarm.md (per-persona delegate permission rule and the document-pipeline use case). Covered by tests/test_personas.py (11 registry + 5 command tests)

v0.19.0 - 2026-08-24

  • Soft tool results now render as dimmed info lines - bundled tools declare a note predicate (read-only callable on the raw result) via registration metadata, and the shared dispatch point _run_tool echoes the note line under the activity line when it fires. (empty file), (empty directory), (no matches for "x"), (end of content)/(empty content), and No search results found. were previously invisible in the REPL (only persisted + fed to the model), while only Error: results rendered. Gated by the new show_notes config (default true). Error/note checks sit side by side so a result cannot double-render, and the echo path excludes note results
  • Session audit trail - every tool permission resolution and its outcome (allow/ask/deny -> granted/declined/denied, with the tool's path when present) is recorded to a new append-only session permissions array. Always-on with no config switch. Replaces the old "confirm prompts are ephemeral" guarantee in docs (AGENTS.md, tools.md, security.md, use-cases/index.md, session.md)
  • Stable message identifiers - every session message now carries an id (msg_<hex>) auto-assigned on creation, for cross-referencing and future delegation (sub_sessions). Legacy messages without an id remain readable
  • Configurable footer token counts - the (7.4s, 8 tokens) footer now builds from the new footer_tokens config (default ["context"], reproducing the old output). Segments render in order joined by /: context = <n> tokens (input/context size), in/out/thinking = <n>t pulled from provider usage (prompt_tokens, completion_tokens, completion_tokens_details.reasoning_tokens), with unavailable counts skipped and an empty list hiding the token section. show_context_size remains the master on/off. Engine._usage_counts() resolves the counts. footer(duration, counts) replaced the old (duration, usage, tokens) signature across ReplUI/HeadlessUI/NullUI
  • Thought-duration line for streamed thinking - with /thinking on, each thinking block now ends with a dimmed (Thought N.Ns) footer-style line (the duration was previously dropped for visible thinking, and only the show_thinking: off spinner path showed it). Gated by the new show_thought_duration config (default true). Mirrored in HeadlessUI (stderr) for replio run/serve, documented in docs/config.md, and covered in tests/test_ui.py
  • Global model registry (~/.config/replio/models.json, separate from config) - /connect appends configured models (provider/base_url/model) and now stores API keys there (0600), no longer in config.api_key (kept as a read-only legacy fallback). /model list shows configured models grouped by provider with the active one marked > and (key) presence. /model list --online [provider] probes a provider's advertised models. On a fresh project /connect offers a numbered picker (#N) that reuses a known model and its stored key, so deleting a project config can no longer lose connection credentials. The engine resolves the API key from the matching registry entry before falling back to config.api_key
  • /config handles single config lines in either scope - leading --global / --local flags (Git-like default stays local, matching replio config set --global), threaded through set, unset, and -a/-r list ops. Output prints the resolved scope (e.g. /config --global api_key <key> is the one-line global key set, masked in output). Config.apply() replaces the old persist=False flag - in-memory overrides are now an explicit, separate method used by one-shot CLI flags and Engine._reinit_provider normalization (which can no longer write config files at startup)
  • Config writes are now scoped and secure - the project-local file holds only explicitly selected keys (a save never re-writes the merged config, so secrets can't be copied down). replio config get/set/unset CLI (with --global/--local, --show-origin, JSON values) and /config unset + origin display in the listing. One-shot CLI overrides (replio run --provider/--model/--base-url/--mode) are no longer persisted. api_key was global-only (written 0600) and is superseded by the model registry above
  • Turn recovery for autonomous agents - on truncation (finish_reason=length) with a partial answer, auto_continue (default true) re-requests the turn with a "continue exactly where you stopped" instruction and stitches the parts into one message (capped by auto_continue_max, default 2). A stream that completes with no content is retried (stream_retries) before being flagged empty. Reasoning-only turns (thinking present, no content) are no longer errors. Partial output is persisted whenever the turn produced content or thinking
  • Thinking is captured from reasoning as well as reasoning_content deltas - ollama.com streams chain-of-thought under reasoning, so reasoning shows in the REPL and sessions for those endpoints instead of being silently dropped (tokens still counted against max_tokens)
  • Diagnosed a real truncation+empty-response pairing: a low explicit max_tokens, the dropped reasoning delta key, and no continuation. The mechanics are documented in docs/config.md and docs/providers.md (migration landed live: local max_tokens dropped, global max_tokens 0, api_key moved to global)
  • Confirm prompts align with activity glyphs - the ? prefix starts at the beginning of the line (no leading indent), so a confirm like ? read_file x.txt - approve? [y/N] sits flush under its % Search / ← Read activity line

v0.18.0 - 2026-08-23

  • Word-level streaming buffering - the REPL now buffers streamed tokens to word boundaries and prints whole words as they complete, so responses render smoothly without mid-word pauses or breaks. The <<< prefix appears on the first printed word, indentation and newlines flush correctly, and the trailing partial word is written before any status line, confirm prompt, or the turn footer. HeadlessUI and NullUI stream unchanged, and the content persisted to sessions is untouched. Gated by the new word_streaming config (default true), off restores character-by-character streaming
  • Multi-line REPL input - opening """ or ''' switches the prompt to ... until the block closes. The composed prompt is sent as one turn with the framing quotes stripped (both """...""" and task: """...""" work, indentation inside the block is kept, and the block is a single history entry). Balancing is overlap-aware, so """" reads as a matched pair. Ctrl-C or EOF on an open block exits the REPL cleanly instead of swallowing the interrupt
  • Available models are now discoverable - /models (alias /model-list) lists the connected provider's models. replio models [--path] does the same headlessly and exits 1 on a failed probe. When /connect saves a config whose model is missing from the advertised list, it offers Show available models? [y/N]. Engine.check_connection() now returns (ok, message, models) from a single _fetch_models() call, and Engine.list_models() proxies the same shared fetch. list_models() dropped its inline [Error] print (pure, returns [] on failure)
  • Multi-line input tests - tests/test_repl_input.py (22 tests: overlap-aware open-delim detection, framing strip variants incl. indentation and ''', and run-loop flows for single-line, block composition, EOF exit inside a block, and untouched slash commands)
  • Word-streaming tests - tests/test_ui.py (9 tests: boundary and tail flush, multi-word chunks hold only the partial word, newline flush, footer flush with separator newline, off-mode immediate writes, markdown bold across a flush boundary, flush before status and confirm prompts)
  • /connect tests the provider connection before saving - a GET <base_url>/v1/models probe via OpenAICompatibleProvider.check_connection() over the shared _fetch_models() helper. Broken values print the error and need an explicit Save anyway? [y/N] to persist. A success message notes when the configured model is missing from the model list
  • /provider <name> switches then probes the new connection and prints a warning when it fails (Run /connect to fix). Both probes are gated by the new connect_check config (default true). Set it to false to skip them for offline or flaky networks
  • Engine.check_connection() probes a throwaway provider built from optional overrides (never mutates config/self.provider), with provider resolution extracted into _resolve_provider_factory() (also cleans up _reinit_provider's unknown-provider handling)
  • Tests - check_connection and list_models coverage in tests/test_providers.py (loopback success/empty/model-note/HTTP, network error, silent-on-error) and tests/test_engine.py (resolution, override precedence, no state mutation, base-URL detection, list_models). /connect probe-before-commit (decline keeps config, accept saves, connect_check: false skips, model-mismatch show offer) plus /models and /provider warn in tests/test_commands.py. replio models in tests/test_cli.py. Docs in docs/config.md (connect_check), docs/providers.md, docs/commands.md, docs/testing.md
  • replio export <name> [--out <file>] - headless Markdown export for scripts and CI, reusing the same renderer and defaulting to the same .replio/exports/<name>.md target (--out - prints to stdout, exit 1 on an unknown session)
  • Session export to Markdown - /session export <name> [out] renders any saved session as a Markdown transcript (default .replio/exports/<name>.md, custom path via the second arg, - for stdout). The pure renderer in sessions/render.py covers the full persisted log: thinking, tool calls and results as fenced code blocks, tool analysis, command records, compaction summaries with the trimmed boundary, and the ## Errors section. It reads via read() (never switches the current session) and carries serialization-time transforms (noise_tools, session_tool_max_chars) through as persisted
  • /session export completes session names in the REPL, and the export format is documented in docs/session.md
  • Tests - tests/test_session_render.py (17 tests: renderer output per role, fence escaping for backtick-heavy tool results, persisted noise transforms, and /session export dispatch covering default/custom/stdout targets and the non-destructive read) plus tests/test_cli.py for replio export (default/custom/stdout targets, unknown session, main dispatch)

v0.17.0 - 2026-08-21

  • Failed tool calls now render a dimmed ! Error: ... line (first line of the result) under the activity line, for every tool through the shared dispatch point (agent loop, /tool, policy-denied calls), gated by the new show_errors config (default true)
  • list_dir and grep get the * glyph with distinct verbs (* List, * Grep), matching glob's * Glob - they no longer render as ← Read, which made directory listings and greps look like read_file calls
  • run_command now validates cwd (friendly Error: cwd not found instead of an OSError) and clamps timeout to 1..600 seconds, so a nonsense value like 10000 no longer executes
  • The confirm prompt prefix changed from ↳ to ? so it no longer collides with the reserved delegate-category glyph
  • Glyph activity lines and confirm prompts now show the parameters the model passed (← Read engine.py [offset=299, limit=85], $ Run pytest [cwd=/workspace, timeout=600]), gated by the new glyph_params config (default true). The label's own argument is not repeated
  • max_tokens now defaults to 8192 (sent to the provider, overriding low provider-side defaults like Ollama's 2048 cap). Setting it to 0 omits it from the payload so the provider's own default applies
  • Truncation messages now distinguish a configured max_tokens cap from the provider's own default limit (no more misleading (0) in the session log when the limit is unset)
  • Session auto-names are now ASCII-only: non-ASCII letters are transliterated (NFKD, e.g. prüfe becomes prufe) instead of being stored in filenames
  • /help now lists tools as indented subcommand-style rows under /tool (replacing the separate "Available tools:" section), filtered by policy and mode like the /tool listing. Bare /tool shows the same rows with short descriptions
  • Fixed streaming and non-streaming provider requests failing with 405 Method Not Allowed on endpoints that redirect - urllib converts POST to GET on 301/302/303 redirects, so api.ollama.com/v1/chat/completions (which 301-redirects to ollama.com) ended up GETting the chat endpoint and getting 405 back. A PostRedirectHandler now preserves the POST method and body across redirects (utils/http.py, providers/base.py), with loopback-server tests for both the SSE stream and non-streaming _post
  • Fixed OpenAI/Groq/Anthropic provider defaults posting to a doubled path - DEFAULT_BASE_URL already includes /v1, and _endpoint() appended /v1/chat/completions again, producing /v1/v1/chat/completions (404). _endpoint() and list_models() now normalize a trailing /v1 instead of doubling it, so default and custom base URLs both resolve correctly
  • Plan/Build (and custom) agent modes - a mode is a named posture combining a system instruction with tool-policy overrides. New mode (default build) and modes config keys ship the built-ins: build (no overrides, current behavior) and plan (read-only - denies the edit and bash categories and instructs the model to investigate and propose rather than modify). Custom modes can set system_prompt, tool_permission (merged over the base, mode wins per key), tools.deny (appended), and tools.allow (replaces when non-empty). An unknown mode falls back to build with no error
  • /mode command - no args lists the current mode and all defined modes, /mode <name> switches live (the next turn uses the new posture, mode switches are recorded as command messages), unknown names print the valid list. Mode names tab-complete in the REPL. The REPL banner and the replio serve stderr line show the active mode when it is not build
  • --mode <name> CLI flag on replio run and replio serve - headless agents start in the given posture (e.g. replio run --mode plan for a read-only review run)
  • Mode mechanics reuse the existing ToolPolicy - no new machinery: ToolPolicy.allowed() is now permission-aware, so a category-level deny (tool_permission.edit: deny and tool_permission.bash: deny) filters the tool from the provider schema and from /tool//help listings, not just direct calls. The MCP server's tool listing honors the same filtering
  • Mode instructions and system_prompt are injected at the engine level (_provider_messages) as a virtual system message, so the REPL, replio run, replio serve, and MCP all apply them. Headless modes now receive system_prompt (previously REPL-only, persisted as a session message - that block is removed)
  • Sessions record the active mode on every assistant message (mode field, parity with reasoning), so the posture in effect per turn is auditable from the append-only log
  • Tests - tests/test_modes.py (12 tests: resolve/merge rules, unknown fallback, instruction composition), plus plan-mode schema filtering, instruction injection, per-message mode, /mode command and completer, --mode CLI, and permission-aware allowed() coverage in engine/commands/cli/policy tests
  • Docs - docs/config.md (mode/modes schema + semantics), docs/commands.md (/mode, --mode), docs/tools.md (category-deny schema filtering, mode layering), docs/security.md (Modes section), docs/session.md (mode field), README feature line, developer use-case guide updated

v0.16.0 - 2026-08-20

  • Reasoning persisted in session logs - each assistant message records the reasoning config value in effect, alongside the existing thinking text. Reasoning text is always logged regardless of show_thinking (display hides it but never drops it from the log)
  • Thinking/reasoning toggle - two orthogonal knobs: show_thinking (default false) controls display (stream reasoning dimmed when on, else an animated spinner + + Thought N.Ns summary), and reasoning (default "auto") requests reasoning from the model and controls its token budget. reasoning maps false/"off" (no request), true/"on"/"auto" (provider default), and "low"/"medium"/"high" (budget hint) to provider params - OpenAI reasoning_effort, Anthropic thinking.budget_tokens (1024/2048/4096), Qwen/Ollama enable_thinking - with a generic reasoning_effort pass-through for other OpenAI-compatible endpoints. The /thinking on|off|? command toggles show_thinking live
  • Thinking spinner - when show_thinking: false the REPL now shows an animated ⠋⠙⠹... spinner (stdlib threading daemon, \r\033[K cleared) in place of the silent wait, then falls back to the + Thought N.Ns summary. No new config key - folded into show_thinking. Interactive ReplUI only, headless stderr output unchanged
  • Activity lines and tool status are ephemeral UI - emitted through the UI sink only and never persisted to session files (tool calls/results are recorded there). Regression test locks this in

v0.15.0 - 2026-08-18

  • MCP (Model Context Protocol) support via the bundled replio-core-mcp plugin (stdlib-only - JSON-RPC 2.0 over newline-delimited stdio and SSE over urllib, no third-party mcp library)
  • MCP client - connect to external servers over stdio or streamable HTTP and import their tools into the ToolRegistry, named <prefix>.<tool> and registered under the new mcp permission category (default ask, so each remote call confirms in the REPL). Management tools mcp_connect/mcp_list/mcp_disconnect and a /mcp command wrap the same registry dispatch. mcp.servers config defines server name, transport, command/url, prefix, headers, and timeout
  • Dual-era negotiation - the client probes server/discover (modern 2026-07-28 per-request _meta protocol) and falls back to the legacy initialize handshake (2025-11-25 and earlier), accepting a mutually supported version on unsupported-version errors
  • MCP server - exposes Replio's policy-filtered tools and its sessions as resources (replio://session/<name>) to external agents, over stdio (replio mcp) and HTTP (POST /mcp on replio serve). The server is dual-era too, serving server/discover/modern requests or the legacy initialize handshake depending on how the client opens. ask-policy tools run by default when serving (mcp_server.allow_ask, deferred to the external client as the human-in-the-loop)
  • Core stays MCP-agnostic - replio mcp and the /mcp HTTP route delegate to the plugin's mcp_server service (same generic register_services pattern as the web-search service), erroring cleanly if the plugin is not loaded
  • Tests - tests/test_mcp.py (27 tests): JSON-RPC framing, stdio/HTTP transports over real subprocesses and a loopback server, modern/legacy negotiation, tool import + prefixing, server dispatch (initialize/discover/tools/call/resources), _meta validation, and policy integration
  • Docs - docs/mcp.md (config schema, client + server usage, interop note and security), bundled-plugin and config/tools/testing references updated

v0.14.0 - 2026-08-17

  • Alias layer for tools and params - tool names can register aliases (read/view > read_file, ls > list_dir, bash/exec > run_command) and param_aliases (file > path, query > pattern, cmd > command, q > query, cursor > offset). The registry resolves aliases to the canonical tool and normalizes args, so the advertised schema stays in the project's own vocabulary while model-dialect tool and argument names are absorbed. /tool, /help, tool policy, confirm prompts, and glyph activity lines work through aliases unchanged
  • open web tool (replio-core-websearch) - fetch a web page by id (1-based result from the most recent web_search) or url, with offset (also accepted as cursor) to resume reading. web_search now retains its last results for open to resolve, and a shared _fetch_text helper gives both fetch_page and open offset-with-continuation paging, appending [offset N of M chars - continue with cursor=N] when content continues past the cap
  • Configurable stream retry - a provider stream that ends before a completion event with no streamed content is re-requested up to 1 + stream_retries times (default 3 total attempts, configurable) with stream_retry_delay (default 0.5s) between attempts. The retrying note shows the attempt count, and when tool calls have already run the warning notes the results are saved so the answer can be retried with a follow-up message
  • tool_max_result_chars config (default 0 = unlimited) - replaces the bundled plugins' hardcoded 8000-char tool-result cap with a configurable one (set via /config tool_max_result_chars N). With the default nothing is truncated, read_file headers now report the total line and character count so the model can page large files with offset/limit, and read_file(path, limit=0) returns just the header as a size probe before committing to a read. Handlers can read the config via a _config kwarg the registry passes only when declared
  • Glyph activity lines - typed <glyph> <verb> <key_arg> status (dimmed) replaces the [tool: key_arg] oneliner for mapped categories, gated by the new glyph_lines config (default true). ToolRegistry.activity() resolves the glyph from category defaults (read ← Read, write → Write, search % Search, exec $ Run, ask ~ Ask, todo - Todo, delegate ↳ Call) or per-tool glyph/verb overrides (glob * Glob, fetch_page ↓ Fetch). Disabling glyph_lines or an unmapped category keeps the [tool: key_arg] oneliner + detail lines. ReplUI/HeadlessUI render the glyph line, ephemeral UI only (never persisted to session files)
  • write_file reports the resolved absolute path to the model - the tool result now returns Created|Overwritten|Appended <resolved abs path> (<n> lines, <n> chars) instead of echoing the raw path arg, so when a relative path resolves to an unexpected directory (e.g. launched from ~) the model sees where the file actually landed, the tool description notes that relative paths resolve against the current working directory. Terminal status preview unchanged. Tests: create/overwrite/append result strings and relative-path resolution
  • Tab completion extended - filesystem path completion after command arguments (directories get a trailing / for continued descent), tool-name completion for /tool <prefix>, and subcommand completion for commands declaring subcommands (/session lo > load, /plugins dis > disable)
  • Fixed tab completion - the completer no longer fires on macOS libedit (readline.parse_and_bind('tab: complete') is GNU-only and silently inserts a literal tab, libedit needs bind ^I rl_complete). / is removed from the completer delimiters so the model-facing word keeps its slash and paths complete correctly

v0.13.0 - 2026-08-16

  • Per-turn stats on their own line - the (Ns, N tokens) footer no longer lands at the end of the last content line, ReplUI tracks whether streamed output ended with a newline and emits a separating one before the footer
  • One-shot retry for empty/truncated streams - a provider stream that ends without a completion event and with no streamed content is requested once more (with a "retrying" note) before the "Stream ended before a completion event" error is surfaced, masks transient provider drops such as Ollama cloud returning an empty follow-up stream after a tool call
  • write_file status preview ends with a dimmed parenthesized summary - resolved absolute path, line count, char count, and created/overwritten/appended action (tool result string stays minimal)
  • Thinking announce - + Thinking on its own line before streamed reasoning when show_thinking: true, when hidden, + Thought 12.3s (thinking duration) is printed instead and the reasoning text stays out of the terminal. The engine times the thinking window (thinking_begin/thinking_end UI hooks), HeadlessUI mirrors it in --verbose mode, and a spinner (⠧ Thinking) can replace the static markers later
  • Human-readable tool status - default oneliner is now [tool: key_arg] (e.g. [write_file: test.md], [run_command: echo hi]) instead of the raw args dump (content='...\n...', mode='w'), detail lines follow dimmed: write_file shows the written text via a registered status callback (new-file + line preview, or a difflib unified diff of existing files - works for append too), and run_command echoes its output (echo registration metadata)
  • list_dir gains depth (default 1) - higher values render an indented recursive tree (tree -L style), skipping SKIP_DIRS during descent, depth=1 output is unchanged
  • ToolRegistry.execute() drops arguments not declared in a tool's schema and null-valued arguments (e.g. a hallucinated recursive, or depth: null) - tools run with their valid args instead of erroring, worktree semantics documented (worktree = launch directory / --path, launching from ~ makes home the worktree, so subdirectories don't escalate)
  • Built-in features ship as bundled plugins - web_search/fetch_page and the machine tools (read_file, list_dir, write_file, glob, grep, run_command) moved out of the core (src/replio/web/, tools/builtins.py, tools/machine.py deleted) into three first-party bundled plugins under plugins/ (shipped as replio.plugins.bundled): replio-core-websearch, replio-core-fs, replio-core-exec. Packaged via package-dir mapping, so the repo-root plugins/ directory is the canonical source, discovery adds a bundled root with precedence bundled < global < local (local/global overrides win)
  • plugins config list - replaces plugins.enabled/plugins.deny (migrated automatically). The default config lists the bundled plugins so they are active out of the box, empty list = all discovered plugins load, /plugins enable/disable and install/uninstall maintain the list
  • register_services entry hook - plugins can power core non-tool features. replio-core-websearch registers the search service backing the web_search: true search-then-answer mode (engine._perform_search now routes through it and degrades gracefully when the plugin is absent)
  • PluginInfo.origin (bundled/global/local) shown in /plugins and replio plugins list, bundled plugins cannot be updated or uninstalled (disable instead)
  • tests/test_bundled_plugins.py (10 tests) - bundled discovery, tool registration, search service, bundled uninstall/update blocking, local override, config disable, default-config membership, test_tool_registry.py/test_machine_tools.py/test_commands.py/test_tool_calling.py/test_session_log.py reworked to load bundled plugins and patch the search service instead of replio.web.search

v0.12.0 - 2026-08-15

  • Plugin system (src/replio/plugins/) - external repositories register tools, providers, and slash commands without changing the core, PluginManager discovers plugins in ~/.config/replio/plugins/ and .replio/plugins/ (local wins), validates manifest.json (replio_version/python semver ranges), imports entry modules once, and hooks them into the live registries
  • Plugin entry contract - optional register_tools(registry) / register_providers(providers) / register_commands(commands) hooks on the entry module, matching the core's register_* conventions, plugin tools automatically inherit tool policy, /tool, /help, refinement, and session logging
  • Lazy plugin dependencies - third-party packages are imported inside plugin functions (never by the core), so the stdlib-only guarantee holds and a missing dep surfaces as a tool error with pip install guidance
  • /plugins command (/plugins list, /plugins <name> detail with per-dep status, enable/disable, install/update/uninstall) plus tab completion for plugin names, /connect now offers plugin providers
  • replio plugins CLI (list/install/update/uninstall) - headless plugin management so replio run/CI can use freshly installed plugins, install clones a git URL or copies a local path, records source, and --deps pip-installs the declared requires
  • Config keys plugins.enabled (allowlist, empty = all) and plugins.deny (always excluded) - activation is explicit and applies on the next start
  • docs/plugins.md - plugin layout, manifest schema, compatibility contract, entry hooks, management, security, and future paths (per-plugin venv isolation, PyPI entry-point source, migrating core web tools out)
  • tests/test_plugins.py (30 tests) - manifest parsing, replio_version/python compat skipping, enable/deny filtering, global/local precedence, entry load-error isolation, all three registration hooks, lazy-dep pip guidance, tool-policy filtering, install/update/uninstall from a local path, and Engine integration (tools + commands + /plugins)
  • REPL banner shows the version - Replio v0.12.0 (provider: model) on startup, gated by show_version config (default true), toggleable via /config show_version false
  • GET /version endpoint - the serve API exposes the installed version as {"version": ...}, alongside /health and /sessions
  • /version slash command (alias /v) - prints Replio <version> in the REPL, version lookup centralized in replio.get_version() and shared with the CLI --version flag
  • --version / -v CLI flag - replio --version prints the installed version (from package metadata) and exits
  • Clear screen on REPL start - the interactive REPL wipes scrollback + visible screen (\033[3J\033[2J\033[H) before printing the banner, gated by clear_screen config (default true), toggleable via /config clear_screen false. Headless run/serve modes are unaffected.

v0.11.0 - 2026-08-14

  • Engine headless agent core (engine.py) + TurnResult - the agent loop extracted from ChatLoop returns a structured {content, thinking, tool_calls, errors, duration, usage, model, provider, session, status} turn result, Engine.chat(text, autoname=True) and Engine.load_or_create_session(name) make sessions addressable from any front-end
  • UISink abstraction (ui.py) - the loop renders through ReplUI (terminal ANSI streaming, confirm prompts, footer), HeadlessUI (stderr diagnostics, auto-approve/deny confirm policy, never blocks on stdin), or NullUI (silent)
  • replio run - one-shot CLI mode (JSON or text output) over the same agent loop + session manager, flags --prompt, --provider, --model, --base-url, --output, --verbose, --session-id, --yes/--no, exits 0 on ok/truncated and 1 on error/empty
  • replio serve - stdlib ThreadingHTTPServer JSON API (POST /chat, GET /sessions, GET /health) with a shared engine serialized behind a lock, ask-gated tools are denied (fed back as [cancelled]), the server never blocks on stdin
  • Headless ask policy - tools gated by ask are denied by default in headless mode, --yes/--no override to approve or deny
  • Tests for headless entry points - test_engine.py (turn result, thinking split, session addressing, confirm policy), test_cli.py (mock provider, JSON/text output, session-id persistence, exit codes), test_server.py (ephemeral-port server against a mocked provider)
  • ChatLoop is now ChatLoop(Engine) - a thin REPL shell (readline, banner, prompt loop) inheriting the headless core, commands and session logic are unchanged
  • _handle_message() replaced by Engine.chat(), _StreamRenderer moved into ReplUI, and the <thinking> marker split is owned by the engine so thinking stays separate from content in JSON results
  • The input() confirm prompt moved from chat.py to ui.py (tests patch replio.ui.input)

v0.10.0 - 2026-08-13

  • OpenAI, Groq, and Anthropic providers (providers/openai.py, providers/groq.py, providers/anthropic.py) - OpenAI-compatible /v1/chat/completions subclasses of the shared OpenAICompatibleProvider, each with its own default base_url and default model (gpt-4o-mini, llama-3.3-70b-versatile, claude-sonnet-4-20250514)
  • Provider auto-detection - detect_provider(base_url) in providers/__init__.py matches well-known hosts (openai.com, groq.com, anthropic.com, ollama.com/ollama.ai) and falls back to the generic openai-compatible provider for any other OpenAI-compatible endpoint
  • PROVIDERS registry - _reinit_provider() resolves the configured provider name through the dict (ollama, openai, groq, anthropic, openai-compatible), unknown names are auto-detected from base_url with a printed note instead of silently falling back to ollama
  • /connect detects the provider from the entered base URL and switches automatically when it matches a known host
  • Agent-loop hardening - streamed content is persisted when the SSE stream ends without a done event, and session errors entries are recorded for silent failures: stream EOF before a completion event, empty/thinking-only done, and unexpected exceptions escaping the agent loop, unexpected exceptions in run() are caught, logged to the session errors, and the REPL stays alive instead of crashing
  • tests/test_providers.py - provider defaults, auth header, detect_provider, and registry coverage
  • OllamaProvider now subclasses the shared OpenAICompatibleProvider - the streaming/tool-call/error logic it previously owned moved to providers/base.py, behavior unchanged
  • Switching providers swaps in the new provider's default base_url/model when the configured values still match another provider's default (i.e. were never customized), explicit custom values are preserved
  • tests/test_ollama_provider.py - stream_sse patches retargeted to the base module where the streaming logic now lives

v0.9.0 - 2026-08-12

  • /session preview <name> - read-only structural preview (name, created/updated, message counts by role, tools used) without switching the active session, shown on /session load too
  • noise_tools config (default ["fetch_page"]) - results of these tools are replaced by a [<tool> result excluded from log, see tool call above for parameters] marker at persistence time only, the live turn still feeds the full result to the model, and the assistant tool_calls message keeps the query/URL so the log stays reproducible
  • Session-name tab completion - /session load and /session delete complete against saved session names (bash-style common-prefix completion, double-tab lists candidates)
  • Compaction summary visibility - /compact and /session load (when the compaction offer is accepted) print the generated summary text, stored as the result of the triggering command message (with a compact_from boundary index)
  • Context-size visibility - dimmed (Ns, N tokens) line after each response, using the provider's usage.prompt_tokens when reported (fallback: char-based estimate), gated by show_context_size (default true)
  • /session load compaction offer - always prompts Summarize & trim history before continuing? [y/N]
  • tests/test_http.py - stream_sse survives a multi-byte UTF-8 character split across 4096-byte read chunks, normal data-line flow, and [DONE]/error passthrough
  • Sessions are append-only logs - compaction no longer removes or rewrites session entries, it only trims the provider context via the summary record and boundary, so the full history always stays in the file
  • Provider payload is prepared from the log - role: command messages are never sent as-is, compaction summary records convert to system messages, and dangling tool messages are skipped
  • /session load and /session preview print a structural preview, /session load records the load as a command message in the loaded session
  • max_tokens default is now 0 (unset) - the value is omitted from the provider payload entirely, so the provider's own default applies and long answers are no longer cut at the old 2048-token default, set a cap explicitly via /config max_tokens N
  • /session new and /session load now reassign ChatLoop.current_session - previously they only updated SessionManager.current, so loaded/new sessions were never actually used for prompts, rendering, or autosave
  • If a configured max_tokens cap is hit (finish_reason: length), the loop now prints a visible truncation warning and records a session errors entry instead of stopping silently
  • OllamaProvider.chat() streaming captures usage from the final chunk and attaches it to the done event
  • config.py - new keys show_context_size, compact_keep (default 4), noise_tools (default ["fetch_page"]), removed compact_prompt_chars
  • Compaction failed with sessions containing command/tool messages - the summarizer now sanitizes the batch (drops command, drops tool_calls declarations, converts tool > user [tool result]), and provider errors are printed instead of swallowed as a generic "Compaction failed"
  • Tab completion for / commands never matched - readline passes the current word, and command names have no leading slash, so the prefix test always failed, the completer now strips the / when comparing and re-adds it on completion
  • stream_sse decoded each 4096-byte chunk with strict UTF-8, so a multi-byte character split across a chunk boundary raised UnicodeDecodeError - caught as a generic error that aborted the stream mid-output and killed the tool loop during long web research, buffering is now byte-based and each complete line is decoded with errors='replace'

v0.8.0 - 2026-08-05

  • Session metadata - created_at/updated_at (bumped on every message) and a top-level errors array with add_error(), to_dict() now persists every message, including role: tool
  • tool_analysis config (default false) - optional model-generated one-line insight summary stored as analysis on each tool message, so a session log can be reconstructed without re-running the tool, skipped for [cancelled]/error results
  • session_tool_max_chars config (default 0 = unlimited) - caps persisted tool-result content at serialization time only, the in-memory provider context always keeps full results
  • _StreamRenderer captures thinking_text, _agent_loop persists per-round reasoning as thinking metadata on tool-call and final assistant messages (still excluded from content)
  • Provider error events now append to the session errors array before the loop bails, instead of being dropped
  • tests/test_session_log.py - session model, save/load round-trip, legacy-file loading, tool_max_chars truncation, thinking metadata, and tool_analysis behavior
  • SessionManager.save() accepts tool_max_chars, session_auto_save() forwards the config value
  • _execute_tool_calls() appends assistant tool-call and tool-result messages via Session.add_message() (single timestamp/updated_at path instead of direct list appends)
  • Docs updated to match: session files are now complete logs (tool results, thinking, errors), confirm prompts and tool status remain ephemeral UI

v0.7.0 - 2026-08-03

  • Tool short= registration metadata - human-readable one-line labels for the /help tools table (falls back to a truncated description)
  • CommandRegistry.canonical() and ToolRegistry.info() accessors backing the /help <name> detail views
  • glob tool - recursive pattern file discovery (**/*.py, src/**/chat.py), skips noise dirs (.git, .venv, __pycache__...), marks dirs with /, caps at 200 matches
  • grep tool - regex content search returning file:line: text matches with an optional file-filter glob, skips noise dirs, caps at 100 matches, friendly invalid-regex error
  • /config structured values - JSON auto-parse (["run_command"] > list, 0.3/2048/true > number/bool), -a/-r list add/remove, reload re-reads config from disk (warns when provider/model changed), unknown keys prompt y/N before storing
  • Command registration metadata - description and subcommands on CommandRegistry.register() (self.meta), canonical names tracked so aliases can't shadow real command names
  • Machine access tools (tools/machine.py): read_file (numbered lines, offset/limit, truncation), list_dir (sorted entries, dir markers, sizes), write_file (parent-dir creation, write/append), run_command (subprocess exec with timeout, stdout/stderr capture, exit code, 8k output cap)
  • Tool permission model (tools/policy.py) - ToolPolicy resolves each tool call to allow/ask/deny from configurable permission keys (tool_permission: read/list/edit/bash/web) plus name-level tools.allow/tools.deny (deny and allow-whitelist take precedence)
  • Path-scoped confirmation - read/write/list calls targeting paths outside the project worktree escalate to ask (opencode-style external_directory), so in-worktree file ops run unattended while external access prompts
  • Confirm prompts in the agent loop (_confirm_tool) - bash: ask by default so every run_command confirms, cancelled calls feed [cancelled] tool results back to the model, denied tools are filtered from the provider schema entirely
  • Registration metadata - category, permission, path_arg, key_arg on ToolRegistry.register() plus permission_for()/path_arg_for()/key_arg_for()/schema_filtered(), retrofitted web_search/fetch_page (forward-compat for the deferred activity-lines glyph system)
  • tests/test_tool_policy.py (allow/deny, permission keys, external-directory escalation, precedence) and tests/test_machine_tools.py (read/list/write/exec against a temp dir, timeout, metadata)
  • /tool policy tests - deny rejection, ask accept/decline, schema filtering of denied tools
  • /help Available tools section renders a clean two-column table using new short= registration labels (Run a shell command, Search the web, ...), category/permission/action now show only in /help <tool> details
  • /help is now the central help system - shows Available commands (aliases inline, subcommands at a standard 4-space indent) followed by Available tools (one per line with [category / permission-key: action], policy-filtered)
  • /help <name> shows details for a specific command (aliases + subcommands) or tool (category, permission: action, full parameter schema with required/optional), commands take precedence
  • /tool (no args) lists allowed tool names one per line and points to /help <tool> for details
  • read_file always emits a # <path> - N lines header (with the shown range when truncated) so the model knows the file's total size without extra reads
  • /help compacts to one line per command with aliases inline (/help, /h) and subcommand rows beneath (/session > new/list/load/delete/save), all aligned to a computed description column
  • /session (no args) reuses the shared subcommand metadata instead of a hardcoded usage block
  • /tool command respects tool policy - listing shows only allowed tools, execution routes through _run_tool() (deny rejection + confirm prompts)
  • Session.to_dict() and the agent loop unchanged - confirm prompts and tool status remain ephemeral REPL UI, never persisted
  • Config shallow-copied DEFAULT_CONFIG, sharing the nested tools.deny/tool_permission lists/dicts across instances - mutations (e.g. /config tools.deny -a ...) leaked into later sessions, now deep-copied on load and reload

v0.6.0 - 2026-08-02

  • /tool <name> <json-args> slash command - generic thin wrapper executing any registered tool via the same ToolRegistry the model uses, no args lists registered tools
  • tests/test_tool_registry.py - registry metadata tests: refine_required() for web_search vs fetch_page/unknown
  • tests/test_commands.py - /tool dispatch tests: registry execution, name listing, disabled path, invalid JSON
  • Unified streaming agent loop (_agent_loop() in chat.py) - a single SSE stream per turn replaces the two-phase non-streaming decision + streaming call, tool_calls events are handled mid-stream, eliminating the double-call cost when no tools are used
  • _StreamRenderer in chat.py - stateful streaming display (thinking markers, markdown, <<< prefix) extracted from the old _stream_response and reused across loop rounds
  • OllamaProvider.chat() now accepts tools, forwards them in the stream payload, and accumulates fragmented delta.tool_calls into a tool_calls event
  • tests/helpers.py - shared make_chat() builder for ChatLoop tests
  • tests/test_agent_loop.py - loop-level tests: no-tools single round trip, thinking excluded from persisted content, error bail, empty stream
  • tests/test_ollama_provider.py - provider streaming tests: fragmented tool-call accumulation, done/thinking events, tools in payload, error passthrough
  • ToolRegistry.register() accepts refine metadata, refine_required() lookup added - web_search registered with refine=True, and _execute_tool_calls() now refines via metadata instead of special-casing the web_search name
  • BaseProvider.chat() signature gains tools: list[dict] | None = None
  • _handle_message() branches collapse onto _agent_loop(), chat_nonstreaming() is now used only by _refine_query()
  • tests/test_tool_calling.py rewritten against the streaming event generator
  • _chat_with_tools(), _stream_response(), _output_content() and _response_start from chat.py
  • /search and /web slash commands - model tool-calling and web_search auto mode cover searching

v0.5.0 - 2026-07-28

  • _TextExtractor class in tools/builtins.py - stdlib HTMLParser-based text extraction for fetch_page
  • Test test_empty_stream_falls_back_to_nonstreaming_content - verifies fallback saves non-streaming content when streaming returns empty
  • Session.to_dict() filters out role: tool messages - session files only contain REPL-visible messages (user, assistant, command, system)
  • Final assistant response missing from session when streaming returned no tokens - fall back to non-streaming result content when _stream_response() returns empty
  • fetch_page returned raw HTML/JS/CSS noise - replaced regex tag-stripping with _TextExtractor (HTMLParser) that drops <script>, <style>, <svg>, <noscript> and extracts clean visible text
  • Session files stored raw tool-result content that was never displayed in the REPL - filtered role: tool messages from serialized output, assistant tool_calls (web_search, fetch_page declarations) still documented

v0.4.0 - 2026-07-28

  • 3 new test cases: empty-content, multiple tool calls, and API error path (now 7 tests total)
  • Tool-call messages (assistant tool_calls + tool results) lost when streaming produced empty content or an exception occurred - wrapped _chat_with_tools() and _stream_response() in try/finally so session is always persisted
  • _chat_with_tools() no longer skips _stream_response() when chat_nonstreaming returns empty/null content - final response is always streamed
  • Session file rename left JSON name field stale - added session_auto_save() after rename so the file always has the correct session name

v0.3.0 - 2026-07-28

  • Auto-session naming: first user message auto-names the session (sanitized, truncated to 40 chars). Renames the .json file on disk
  • Markdown-aware streaming: code blocks in cyan, inline code in green, bold text rendered with ANSI bold. Disabled by default (markdown_streaming: false). Enable via config.
  • Thinking/reasoning token detection: provider-level reasoning_content field (DeepSeek R1, o1, o3). Configurable via show_thinking (default true)
  • Error handling: _post(), chat_nonstreaming(), chat(), _chat_with_tools(), and list_models() now catch HTTPError (auth 401, server 500) and URLError (network, timeout) gracefully - errors print in red, REPL continues
  • _chat_with_tools(): when no tools are used, now calls _stream_response() for live token output instead of dumping the full response at once
  • query_refine config system: when enabled, short web_search queries of up to query_refine_min_words words are auto-refined via a lightweight model call with query_refine_context recent messages as context. Configurable via query_refine, query_refine_min_words (default 3), query_refine_context (default 4)
  • tool_status_visible config option (default true): when false, hides dimmed [tool: args] status lines during tool execution
  • Mock test suite for tool calling (tests/test_tool_calling.py): 4 offline tests covering no-tools, single-tool, unknown-tool error, and force-search paths
  • Markdown streaming now off by default, the state machine was unreliable on real streaming output
  • Removed ... as thinking marker/closer (too many false positives in normal text)

v0.2.0 - 2026-07-27

  • Auto-search mode: set web_search: true in config to search on every message
  • Search results displayed as compact list, injected as AI context for grounded responses
  • /search <query> and /web <query> commands
  • web/display.py - format_results() for terminal, format_context() for AI context injection
  • web/search.py - DDGResultParser (HTMLParser), search endpoint at lite.duckduckgo.com
  • Web search: DuckDuckGo Lite via html.parser - /search <query> and /web <query> commands
  • Config default: tool_calling: true
  • /search command integrated with tool calling (uses _chat_with_tools(force_search=...))
  • _show_tool_status() - dimmed [web_search: "query"] status during tool execution
  • _chat_with_tools() - decision loop that executes tool calls and injects results
  • Ollama provider: refactored payload generation, chat_nonstreaming() with tools support
  • BaseProvider.chat_nonstreaming() - returns {role, content, tool_calls, finish_reason}
  • tools/builtins.py - web_search and fetch_page tools
  • tools/registry.py - decorator-based tool registration (OpenAI function calling format)
  • Tool calling system: two-phase chat (non-streaming tool decision > stream final content)

v0.1.0 - 2026-07-27

  • <<< prefix on streaming responses for clear user/AI separation
  • Command logging: all slash commands stored as command-role messages in session history
  • Model tracking: per-message model + provider on each assistant response
  • Elapsed time display after each response
  • Timestamps on every message and duration tracking on assistant responses
  • Streaming token display with ANSI-colored prompt
  • REPL loop with readline input history and tab completion
  • Built-in slash commands: /help, /connect, /model, /provider, /session, /config, /exit
  • Command registry with decorator-based registration
  • Session manager: create, list, load, delete, auto-save (JSON format)
  • Ollama cloud provider (defaults to https://api.ollama.com)
  • Provider abstraction layer (OpenAI-compatible /v1/chat/completions)
  • HTTP SSE streaming via urllib for live token output
  • Config module: global + local JSON merge (~/.config/replio/ + .replio/)
  • Project scaffold (pyproject.toml, .venv, directory structure)