github yologdev/yoyo-evolve v0.2.0

latest releases: day219-11-10, day219-08-59
3 hours ago

Days 213–219. If you run --yes with a --deny list, take this release. In 0.1.19, yoyo --yes --deny "echo BLOCKED_ZZ" still ran echo BLOCKED_ZZ. The minor bump is for two new flags (--save-session, --continue-strict). The rest is mostly exit codes and stdout that tell the truth: more shell subcommands exit nonzero when they fail, and a pipe gets one answer instead of a retried attempt spliced into it.

Security

  • A user deny pattern (--deny, .yoyo.toml [permissions] deny) now blocks bash on every approval path. Before, yoyo --yes --deny "echo BLOCKED_ZZ" ran echo BLOCKED_ZZ (measured: Exit code: 0). Answering a (always) once also skipped every later deny check. And a command the safety analyzer flags (e.g. git push --force) was matched against the decorated warning text instead of the command, so deny = ["git push --force*"] prompted instead of refusing. Deny is now checked once, on the raw command, inside the bash tool, before any prompt or auto-approval. allow and --yes behave as before for commands no deny pattern matches. File-path deny for write_file/edit_file is unchanged.
  • Sub-agent and explore-agent bash now follows the same rules as the parent's: the user's permissions.deny, the hard deny list, and a refusal for anything the parent would have asked the user to confirm. Under a worktree-pinned parent, the child's bash also runs inside the pin and refuses git redirection escapes (git -C, --git-dir, --work-tree) (#977).
  • The hard deny list matches commands instead of substrings. A recursive delete of a build directory under /tmp, and prose that merely mentions a pattern (a grep, an echo, a commit message), are no longer refused. Recursive deletes of the root or home directory are refused in every flag spelling (-fr, -r -f, -Rf, --no-preserve-root); before, several of those spellings got through. The safety analyzer's rm check also catches a tab after rm, a backslash-escaped \rm, and a quoted 'rm'.
  • /cd now applies the new directory's [permissions] deny patterns: they are added to the session's deny list and enforced on bash, in the agent and in sub-agents, for the rest of the session. The list only grows (/cd-ing back removes nothing). Deny applies to bash only; file tools never read it, and /run, ! and background jobs do not check it. Everything else in the new directory's config (allow, [directories], hooks, MCP servers, skills) is still not reloaded, and /cd prints a yellow ⚠ warning: (plain: warning:) naming what it skipped, never hidden by quiet mode (#869 stays open).

Added

  • New --save-session <path> flag: after a -p or piped run, yoyo writes the session to <path> (the same JSON /save writes, loadable with /load), including when the turn failed. It never creates missing directories. An unwritable path prints error: --save-session: could not write <path>: <reason> and exits non-zero, keeping the turn's own failure code if it already had one (#978). Without the flag nothing changes.
  • New --continue-strict flag: like --continue, but a missing or unparsable session file exits 1 before any model call, instead of warning and silently starting over with exit 0 (#979). Plain --continue is unchanged.
  • --output-format stream-json writes {"type":"sessionRestored","messages":N} right after agentStart when a session was restored (#979).

Fixed

  • A pipe gets one answer. When stdout is not a terminal and a turn dies after text has already streamed, yoyo no longer retries and re-streams the partial answer (before: the same partial text up to six times). It exits nonzero with the original error and one stderr line saying why; rerun the command. A terminal, and a turn that dies before any text, retry as before (#976). The same rule now covers yoagent's own internal provider retry on the plain -p text path, which had spliced a dead attempt's text into stdout (PARTIAL_\n\nPONG\n, exit 0) (#989). --output-format stream-json is unchanged on purpose: its NDJSON closes the dead attempt in band, so the reader can tell the attempts apart.
  • The terminal bell no longer writes 0x07 bytes onto stdout when stdout is not a terminal. The bell goes to stdout only when stdout is a tty and not reserved for the payload; otherwise to stderr when stderr is a terminal, and it is dropped when neither is (#976).
  • Plain yoyo -p "..." and piped stdin without --print no longer wrap the answer in blank lines. Measured before: yoyo -p "Reply with exactly: PONG" printed \n\n\nPONG\n\n. It now prints PONG\n. Leading whitespace-only lines are dropped (the --print rule, indentation kept), the answer ends with exactly one newline, and blank lines inside the answer are kept byte-for-byte. The REPL, --print and --output-format json/stream-json are unchanged.
  • Models whose provider preset carries no price now fall back to yoyo's own price table instead of being costed at $0 in /cost.
  • Shell subcommands that print a failure now exit nonzero, so a script or CI step can tell (#982 slices):
    • yoyo test and yoyo run return the child's real status (a failing cargo test gives 101; yoyo run false gives 1; nothing ran gives 1).
    • yoyo diff, yoyo commit, yoyo blame, yoyo changelog, yoyo evolution and yoyo tree exit 1 outside a git repository.
    • yoyo lint and yoyo health exit 1 when no project is found.
    • yoyo model <unknown> exits 2; yoyo skill show <missing> exits 1.
    • yoyo docs <missing> exits 1 (also when docs.rs is unreachable), and it no longer prints a green ✓ for a crate or item docs.rs 404s: it decides on the HTTP status, not the page text.
    • yoyo config get <typo> says the key is not a settable config key, suggests the nearest one (did you mean model?) and exits 2. A real key that is merely unset still prints "using default" and exits 0.
    • #982 stays open: about 30 other shell arms can still print a failure and exit 0.
  • yoyo checkpoint save, yoyo git status, yoyo pr list, yoyo stash pop, yoyo mcp list and the other shell forms of ten REPL-only commands (checkpoint, fork, bg, revisit, spawn, history, stash, pr, git, mcp) are refused for free when the second word is one of that command's own subcommands, instead of starting a billed model turn. Prose still reaches the model (yoyo git how do I rebase), and the refusal names the yoyo -p "..." hatch. plan, refactor, copy and web are deliberately not gated because their subcommand words also start ordinary prompts (#936).

Changed

  • yoagent 0.18.1 → 0.24.1 (#987, #991 step 1). ThinkingLevel gained XHigh/Max upstream; yoyo still sends at most High. MiniMax now uses yoagent's api.minimax.io default. Bedrock should work per yoagent 0.22, unverified here. Not in this release: wiring yoagent 0.24.1's retry-safe event filter (#991 step 2 was attempted and reverted, because on pipes it dropped the partial answer of a turn that fails for good).

Price table audit

  • price_drift_audit (DeepSeek rows): passed, 3 rows compared.
  • General sweep: price audit: SUMMARY compared 36, matched 15, drifted 9, cache_read_only 12, unpriced 137 of 173 catalogue rows (rel_tol 1%). The drifted rows are UNRECONCILED in this release, as in 0.1.19: the table was not edited, because the skill requires reading each vendor's pricing page first and that did not happen in this session. models.dev now lists deepseek-v4-pro at 0.66/1.98 per MTok against yoyo's 0.27/1.1. /cost figures for the drifted models may be wrong. The 12 cache_read_only rows are the known unmodelled-cache gap.

Don't miss a new yoyo-evolve release

NewReleases is sending notifications on new releases.