Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,60 @@ All notable changes to this project are documented here. The format follows

## [Unreleased]

## [0.38.1] - 2026-09-05

Two defects that switched a feature off without saying so, found by driving the released 0.38.0
the way a user drives it rather than by running its test suite.

`io config set` wrote a quoted string for every key it did not know by name, and
`[app.io-cli.containment]` and `[run.context]` are two such sections — so `max_total_agents 4`
reached the file as `"4"`, io-harness refused to read the section back, and fan-out was simply
off behind one warning line. Meanwhile the in-session `/config` never went through that code at
all and wrote the operator's raw text as TOML source, so the same key typed the same way got two
different answers depending on which door it came through. There is now one speller and both
doors call it.

A failing verification gate under `io exec` never ended the run. io-harness evaluates the
criterion after every step and hands the failure back, which is a reason for the agent to keep
going, so the only thing that stopped it was this crate's own step cap of a thousand — and that
constant is documented as safe precisely because three other bounds exist, every one of which a
gate removes. A gated headless run now ends, and exit `6` arrives.

### Changed

- **Behaviour change for scripts.** `io config get` on a key io-cli's catalogue does not name now
reads back an unquoted number or boolean where it read back a quoted string. A script that
matched on `"4"` will see `4`. This is the fix, not a side effect: the quoted form is what
io-harness refused to deserialize.
- **Behaviour change for CI.** A gated `io exec` that previously ran to the step cap now ends and
exits `6` (`UNVERIFIED`) once its criterion has failed and nothing else bounds the run. A job
that relied on a timeout to stop it will now fail fast instead, which is the documented
behaviour it should always have had. A gated run that genuinely needs more steps sets
`[run] max_steps`, `max_duration_secs` or `max_tokens` — any of the three wins over the new
bound.
- `/config` refuses a value the key does not admit at the moment it is typed, rather than opening
a picker asking which file to write a value into that will not be written.

### Fixed

- `io config set` types a bare value as it was written for keys outside io-cli's catalogue: an
integer as an integer, a float as a float, `true` and `false` as booleans, everything else as a
quoted string. `app.io-cli.containment.*` and `run.context.*` can be set from the shell again.
- `/config <key> <value>` in a session goes through the same speller as `io config set`, so
`policy.defaults.net allow` is written as a string instead of being refused as invalid TOML.
- `io exec` refuses before the first step when `[app.io-cli]` will not deserialize while naming a
gate, instead of running with no criterion and reporting success. When the broken section names
no gate, the notice is printed rather than swallowed — headless never printed it at all.

### Dependencies

- io-harness `0.78` → `0.79`. No breaking change is declared and no line of its `src/` was
deleted. It adds `EventKind::Program` and the `run_program` tool name **unconditionally** rather
than behind its `codeact` feature, which this crate does not enable, so `triage::TRIAGE`,
`acp_map::MAPPING` and `acp_map::TOOL_KINDS` each take a row for something no run here can
currently emit. Three test assertions that counted event kinds by literal now derive the count
instead, so the next such addition names the new kind rather than a number.

## [0.38.0] - 2026-09-04

The release that lets you install io the way you install everything else. Distribution was a
Expand Down Expand Up @@ -3461,6 +3515,7 @@ client, tool, sandbox, policy engine or session store of its own.
- No test in this release asserts on wall-clock time.

[Unreleased]: https://github.com/initorigin/io-cli/compare/v0.38.0...HEAD
[0.38.1]: https://github.com/initorigin/io-cli/compare/v0.38.0...v0.38.1
[0.38.0]: https://github.com/initorigin/io-cli/compare/v0.37.0...v0.38.0
[0.37.0]: https://github.com/initorigin/io-cli/compare/v0.36.0...v0.37.0
[0.36.0]: https://github.com/initorigin/io-cli/compare/v0.35.0...v0.36.0
Expand Down
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "io-cli"
version = "0.38.0"
version = "0.38.1"
edition = "2021"
# io-harness declares 1.95, set by `libsqlite3-sys` calling the std `cfg_select!`
# macro from its build script. Nothing here can be lower than what the harness
Expand Down Expand Up @@ -63,7 +63,7 @@ path = "src/lib.rs"
# still correct about what this *renderer* can be handed; it is simply no longer
# load-bearing on the build. Turning `barcode` back off is what would restore
# it, and `documents` would then have to be spelled out as its other four.
io-harness = { version = "0.78", features = ["media", "browser", "documents"] }
io-harness = { version = "0.79", features = ["media", "browser", "documents"] }
# **`tui-textarea` is gone as of 0.18.0, and it is why this pair was frozen.**
# The comment that stood here said ratatui stayed at 0.29 and crossterm at 0.28
# because the composer was built out of that widget and the 0.30 line carried
Expand Down
12 changes: 6 additions & 6 deletions Formula/io.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ class Io < Formula

on_macos do
if Hardware::CPU.arm?
url "https://github.com/initorigin/io-cli/releases/download/v0.37.0/io-0.37.0-aarch64-apple-darwin.tar.gz"
sha256 "93fa6ea5a85535457056207cf8ddde44c3a2b1f58dd644e78b26853f4169069f"
url "https://github.com/initorigin/io-cli/releases/download/v0.38.0/io-0.38.0-aarch64-apple-darwin.tar.gz"
sha256 "e38a9a4ac49b6149049d19b2d1398d42da6ed33f3b5d3c5f9f4f5933b725dc79"
end

if Hardware::CPU.intel?
url "https://github.com/initorigin/io-cli/releases/download/v0.37.0/io-0.37.0-x86_64-apple-darwin.tar.gz"
sha256 "f070a8d2afed9498a8a08bbf49683744b5741dda90319291430b99bcb98e08f8"
url "https://github.com/initorigin/io-cli/releases/download/v0.38.0/io-0.38.0-x86_64-apple-darwin.tar.gz"
sha256 "2f7f8923a6c918a4f8bae9fc33f6c31888a91456a2184daf2eb86978a6777b68"
end
end

Expand All @@ -47,8 +47,8 @@ class Io < Formula
# rather than a URL that 404s halfway through an install.
on_linux do
if Hardware::CPU.intel?
url "https://github.com/initorigin/io-cli/releases/download/v0.37.0/io-0.37.0-x86_64-unknown-linux-musl.tar.gz"
sha256 "1196630b787c343e96953a4091e78908d812c39a10876655c4afe6c3dbc2faf3"
url "https://github.com/initorigin/io-cli/releases/download/v0.38.0/io-0.38.0-x86_64-unknown-linux-musl.tar.gz"
sha256 "26a6e918526800047ada4050b5e6e296ee45c1a4ab5881d46741fc520b015a0c"
end
end

Expand Down
8 changes: 4 additions & 4 deletions bucket/io.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"version": "0.37.0",
"version": "0.38.0",
"description": "Terminal agent that shows what it may do, what it spends and what it refused",
"homepage": "https://github.com/initorigin/io-cli",
"license": "Apache-2.0",
"architecture": {
"64bit": {
"url": "https://github.com/initorigin/io-cli/releases/download/v0.37.0/io-0.37.0-x86_64-pc-windows-msvc.zip",
"hash": "a6011df7775bdcc4d3a8b5e026690c0b010ee42c7d018c595196cb323d21ce57",
"extract_dir": "io-0.37.0-x86_64-pc-windows-msvc"
"url": "https://github.com/initorigin/io-cli/releases/download/v0.38.0/io-0.38.0-x86_64-pc-windows-msvc.zip",
"hash": "6ec1b0db26a25188e49c5c7d20084ef3c5538b50be18ab74692f06eaaf4859c3",
"extract_dir": "io-0.38.0-x86_64-pc-windows-msvc"
}
},
"bin": "io.exe",
Expand Down
19 changes: 19 additions & 0 deletions src/acp_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,20 @@ pub const MAPPING: &[(&str, Update, &str)] = &[
Update::None,
"as `sandbox` — the boundary is io-cli's status line",
),
// 0.79.0, and placed where `EventKind` declares it — between `contained` and
// `dialed` — because this table's ordering rule is that it can be read down
// the side of `observe.rs` when the pin moves. Declared unconditionally by
// the harness and emitted only behind its `codeact` feature, which this crate
// does not enable, so the row is here because the table is total over what
// the harness declares.
(
"program",
Update::None,
"the acts a program took are not on this event — each re-enters dispatch \
and arrives as its own `tool_call`, which is already translated; \
announcing the program itself would put a tool call in the editor that \
has no `call_id` and never completes",
),
(
"dialed",
Update::None,
Expand Down Expand Up @@ -396,6 +410,11 @@ pub const TOOL_KINDS: &[(&str, &str)] = &[
("git_branch", "execute"),
("git_worktree", "execute"),
("spawn_agent", "execute"),
// 0.79.0, and `execute` is the only honest kind: it runs a program the model
// wrote, under the run's own exec mode. Declared unconditionally by
// `tools/mod.rs`, so this row is needed whether or not this crate enables
// io-harness's `codeact` feature — which today it does not.
("run_program", "execute"),
// Fetches — the browser reaches something outside the workspace.
("browser_navigate", "fetch"),
("browser_screenshot", "fetch"),
Expand Down
12 changes: 6 additions & 6 deletions src/adapt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
//! `self.root.join(d)` and `Path::join` of an absolute argument discards the base.
//! io-harness 0.74.0 refuses that spelling for `skills` and for `templates` in
//! **every** scope, `Scope::User` included
//! (`io-harness-0.78.0/src/plugin.rs:1097`), and the reason is the one this
//! (`io-harness-0.79.0/src/plugin.rs:1097`), and the reason is the one this
//! product would give: every `*.md` under such a directory is read into the
//! model's system prompt on every turn, so a manifest contributes a directory it
//! *ships* rather than one it points at somewhere else on the machine. Copying is
Expand Down Expand Up @@ -377,7 +377,7 @@ pub const SKILLS_DIR: &str = "skills";
/// io-harness calls the same thing a **template**, and the two formats are the
/// same file: markdown, optional frontmatter, `$ARGUMENTS` for what the operator
/// typed. `Templates::discover` takes every `*.md` in a directory
/// (`io-harness-0.78.0/src/template.rs:279`), so what is translated is the
/// (`io-harness-0.79.0/src/template.rs:279`), so what is translated is the
/// directory and never the files in it — the bytes are copied verbatim into
/// [`TEMPLATES_DIR`] and nothing in them is rewritten.
pub const COMMANDS_DIR: &str = "commands";
Expand Down Expand Up @@ -578,7 +578,7 @@ pub fn at(root: &Path, owner: &str, repo: &str, name: &str) -> PathBuf {
///
/// **The two directories are copied into `into` and named relative to it.**
/// io-harness 0.74.0 refuses an absolute or a `..`-carrying `skills` or
/// `templates` in every scope (`io-harness-0.78.0/src/plugin.rs:1097`), because
/// `templates` in every scope (`io-harness-0.79.0/src/plugin.rs:1097`), because
/// every `*.md` under one is read into the model's system prompt — so a bundle
/// contributes a directory it ships. An adapter comes to ship one by copying it,
/// and [`Adapter::copied`] reports which moved. The copy refuses a symbolic link
Expand Down Expand Up @@ -714,7 +714,7 @@ pub fn generate(bundle: &Path, name: &str, into: &Path) -> Result<Adapter, Strin
// `expanded`, which refuses a substitution by the field it was written in —
// this catches the case that check has not been wired to yet. io-harness
// refuses a `${` anywhere in a manifest, in every scope
// (`io-harness-0.78.0/src/plugin.rs:977`), and the refusal takes the whole
// (`io-harness-0.79.0/src/plugin.rs:977`), and the refusal takes the whole
// bundle rather than the one key: a single missed value would cost the
// operator every capability the bundle carries, reported against a file they
// did not write. Nothing is written when this fires.
Expand Down Expand Up @@ -1036,7 +1036,7 @@ fn substitution(text: &str) -> Option<String> {
/// adapted, so expanding it reads nothing of the host — it writes down a path io
/// already knew. `${env:…}`, `${file:…}`, `${cmd:…}` and every shell variable a
/// bundle writes are the opposite: io-harness refuses each of them inside a
/// manifest in every scope (`io-harness-0.78.0/src/plugin.rs:977`), because a
/// manifest in every scope (`io-harness-0.79.0/src/plugin.rs:977`), because a
/// bundle is a third party's directory even when the file naming it is the
/// operator's own.
///
Expand Down Expand Up @@ -1252,7 +1252,7 @@ fn unquoted(said: &str) -> &str {
///
/// **A transport io-harness does not speak is a refusal naming the server**, not
/// a server quietly left out. io-harness speaks stdio and streamable HTTP
/// (`io-harness-0.78.0/src/mcp.rs:301`), so a bundle declaring `sse` is declaring
/// (`io-harness-0.79.0/src/mcp.rs:301`), so a bundle declaring `sse` is declaring
/// a capability no adapter can deliver — and a manifest carrying one fewer server
/// than the bundle does is exactly the silent absence this module's docs exist to
/// end. The whole generation stops, because a bundle is installed for what it
Expand Down
6 changes: 3 additions & 3 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1489,7 +1489,7 @@ pub const READ_MARK: &str = "+";
/// The case this page exists for. A file that is there and is not read looks,
/// from everywhere else in this product, exactly like one that is read: nothing
/// warns, and a missing or skipped instruction file is passed over in silence
/// (`read_instructions`, `io-harness-0.78.0/src/config.rs:2911`).
/// (`read_instructions`, `io-harness-0.79.0/src/config.rs:2997`).
pub const UNREAD_MARK: &str = "-";

/// What one row of the memory page stands for.
Expand Down Expand Up @@ -1705,8 +1705,8 @@ pub fn memory_notes(view: &crate::recall::View, glyphs: &crate::glyphs::Glyphs)
/// writing: a file that exists and is not read. It names what would make it
/// read rather than only reporting the fact, because `[instructions] files`
/// **replaces** the default list rather than adding to it
/// (`DEFAULT_INSTRUCTIONS`, `io-harness-0.78.0/src/config.rs:190`, read at
/// `:2808`) and there is nothing on any
/// (`DEFAULT_INSTRUCTIONS`, `io-harness-0.79.0/src/config.rs:190`, read at
/// `:3006`) and there is nothing on any
/// other surface to pull on.
pub fn instruction_said(
file: &crate::memory::Instruction,
Expand Down
Loading