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
76 changes: 76 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,81 @@ All notable changes to this project are documented here. The format follows

## [Unreleased]

## [0.38.2] - 2026-09-06

The rest of what the 2026-09-05 field test found, and the io-harness pin that turned out to carry
two of its answers.

`io resume --list` showed thirteen parked runs as thirteen rows differing only by a run id and a
step number, which is a listing nobody can choose from. It carries each run's start time and its
goal now. io-harness published a reader for neither until 0.81.0; both arrived there, which is why
this was named as carried when 0.38.2 was outlined and is delivered here instead.

`io plugin search` for something that is not there printed nothing at all, which reads exactly like
a broken command. `io mcp disable` warned about io-harness versions the operator never installed
and could not check. `io setup` over an existing 36 KB configuration announced a first run — and
the comment beside that fix named a test holding the driver to it that had never been written, so
the feature was one deleted line from reverting in silence.

The pin moves 0.79 → 0.81.0. Its two additions to the `Provider` trait default to `None`, so every
wrapper in this crate compiled clean, passed every test, and answered for itself instead of for the
provider it wraps. Nothing reported it and nothing could have; there is a gate now that reads the
method list out of the locked harness rather than out of a list somebody wrote down.

Delegating them does not move the number yet, and the release says so rather than implying
otherwise. In io-harness 0.81.0 only `Compatible` implements those two methods, and only from a
catalogue that has to be fetched on the same instance first — so on OpenRouter, Anthropic and
OpenAI the run still assembles under the 24,000-token fallback. That is upstream's to answer and it
has been filed. What this release fixes is that the wrapper is no longer the thing losing it.

### Added

- `io resume --list` carries each run's start time and goal on the plain row, and `goal`,
`started_at` and `root` on the `--json` object. A run started before io-harness 0.7.0 has no
stamp in the store and renders `unknown` rather than a date.
- `io plugin search <term>` with no match says so — in the session, and on **stderr** from the argv
door, so a pipeline still reads exactly the hits and nothing else.

### Changed

- `ctx N%`, the `/context` page and `/status` all divide by the ceiling the run announces
(`EventKind::ContextCeiling`, new in io-harness 0.81.0) rather than by the budget on the contract
io-cli built. **On the providers this release can construct, that is the same 24,000 it was**:
in io-harness 0.81.0 only `Compatible` answers `context_window`, and only from a catalogue
`models()` has already fetched on that same instance, so the harness reports `source: "fallback"`
and the number does not move. The plumbing is connected end to end and gated; what it waits on is
a provider that answers. Nothing you see changes yet, and that is stated rather than implied.
- **Behaviour change for scripts.** The plain rows of `io resume --list` are wider — a script
slicing them by column position sees new fields. The `--json` shape is the one this product asks
scripts to parse, and its three new keys are nullable, so an existing reader is unaffected. They
are not appended: `serde_json` emits an object in key order, so `goal` now precedes `run_id`.
Anything parsing JSON is fine; anything matching the line by prefix is not.
- **Behaviour change for scripts.** `io config get` on a key that no file sets and that io-cli's
catalogue does not name answers `no such key` where it answered `default`. A misspelt key used to
be indistinguishable from an unset one, which is the whole reason to ask.
- The two "older binary" warnings on `[[mcp]]` and `[[plugin]]` `enabled` keys name io-cli 0.29.0 —
the release that first understood the key — instead of io-harness 0.69.0 and 0.70.0. Same
boundary, stated in the version an operator can actually check, and it no longer goes stale every
time the pin moves.

### Fixed

- All four of this crate's `Provider` wrappers delegate `context_window` and `max_output_tokens`.
Both are new in io-harness 0.81.0 with `None` defaults, so without this the whole tree compiled
and tested green while discarding the window the provider catalogue already held.
- `io setup` over an existing configuration names the file it is about to write over, and the
driver's call is now held to it by a test — the one `src/wizard.rs` had been citing by name.
- A guide page said io-harness emits no event of any kind when the agent is handed an image. Since
io-harness 0.81.0 it emits one; the page said otherwise the moment the pin moved.
- A session row's timestamp carries its UTC marker without breaking the assertion that the stored
`T` separator was replaced by a space — `UTC` contains a `T`, and the check was being made over
the whole string.
- Finished sessions' lock files are swept at startup, a fenced block draws no bare language line, a
thought is committed above the prose it produced, a spawned child's outcome is drawn as words
rather than as a Rust `Debug`, the home line is drawn only on a run that created or moved
something, and no `--help` page narrates release history or shares one example across four
subcommands.

## [0.38.1] - 2026-09-05

Two defects that switched a feature off without saying so, found by driving the released 0.38.0
Expand Down Expand Up @@ -3515,6 +3590,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.2]: https://github.com/initorigin/io-cli/compare/v0.38.1...v0.38.2
[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
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.1"
version = "0.38.2"
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.79", features = ["media", "browser", "documents"] }
io-harness = { version = "0.81", 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.38.0/io-0.38.0-aarch64-apple-darwin.tar.gz"
sha256 "e38a9a4ac49b6149049d19b2d1398d42da6ed33f3b5d3c5f9f4f5933b725dc79"
url "https://github.com/initorigin/io-cli/releases/download/v0.38.1/io-0.38.1-aarch64-apple-darwin.tar.gz"
sha256 "ba2cc2b821c238686c835a886b5eb4eb392365b87873ffcd5960ef6560aa73d0"
end

if Hardware::CPU.intel?
url "https://github.com/initorigin/io-cli/releases/download/v0.38.0/io-0.38.0-x86_64-apple-darwin.tar.gz"
sha256 "2f7f8923a6c918a4f8bae9fc33f6c31888a91456a2184daf2eb86978a6777b68"
url "https://github.com/initorigin/io-cli/releases/download/v0.38.1/io-0.38.1-x86_64-apple-darwin.tar.gz"
sha256 "2d273e01d430de2d6f4d70f44c393d282646f421724fa78b5f4d023008d5efe7"
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.38.0/io-0.38.0-x86_64-unknown-linux-musl.tar.gz"
sha256 "26a6e918526800047ada4050b5e6e296ee45c1a4ab5881d46741fc520b015a0c"
url "https://github.com/initorigin/io-cli/releases/download/v0.38.1/io-0.38.1-x86_64-unknown-linux-musl.tar.gz"
sha256 "2e99e143d1c0803d7e07e005146395e82d7ed20a4d883fc4bbb27be634d4040e"
end
end

Expand Down
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,17 @@ posture on the next.](assets/screenshot.png)
| **A session you can read** | Every finished line in the terminal's own scrollback, designed rather than defaulted: a tool call as a verb and a path, a thought as a thought, an answer that ends the turn |
| **A working view** | Two sticky rows while a turn runs — a word for the turn with its clock and spend, and a line under it saying what is happening *now* |
| **The boundary, visible** | The posture on the footer, a refusal that names the act, the target, the rule and the layer, and `Shift+Tab` to change it from the next turn |
| **A tool held back** | `/context withhold <tool>` builds the session's tool mask and keeps that tool refused until `/context allow`; io-harness sends a byte-identical catalogue either way, so the request grows by the one sentence naming what is withheld |
| **Approvals in place** | A write stops the run and shows the diff it proposes; `y`, `a` or `n`, answered where it was asked |
| **A fan-out you can watch** | Contained turns spawn children under one shared ceiling; `Ctrl+F` shows the tree and what it is costing |
| **Your file, in force** | Every section of `io.toml` bounds a session turn as it bounds `io exec`; the budgets in force are on the status line with what is left of them, and `/status` commits the whole state — policy layers, sandbox backend, caps, budgets, connections — into the scrollback |
| **Undo** | `Esc Esc` at an empty prompt rewinds the last turn — its files, its memory and the conversation head |
| **Conversations that survive** | `/resume` reopens an earlier session and answers whatever its last run stopped on, `/fork` continues from an earlier turn, `/clear` starts fresh without leaving |
| **A paused run, answered** | A question, a plan or an interrupted call is decided where it was left and the run carries on from the step it stopped at — from a session, or from a script with `io resume` |
| **Everything it asks, in one place** | Several questions asked at once arrive as one overlay, answered one at a time and sent together; an offer may explain itself and preview what taking it would do; `Space` marks where a question takes more than one answer |
| **Headless** | `io exec` runs one goal to completion with documented exit codes and `--json`, and `io resume` carries a parked one on |
| **Headless** | `io exec` runs one goal to completion with documented exit codes and `--json`, and `io resume` carries a parked one on. Exit `6` is a run that was judged and did not hold up: a verification gate that failed, or io-harness's `RunOutcome::SchemaUnsatisfied` — the schema contract, a run that never produced the shape `[run] output_schema` asked for |
| **From your editor** | `io acp` serves the Agent Client Protocol (ACP) as newline-delimited JSON-RPC 2.0 over stdio, so Zed or a JetBrains IDE runs io against this machine's own configuration; an action in the grey tier arrives as a `session/request_permission` you answer in the editor |
| **Bundles from a marketplace you named** | `/plugin marketplace add <owner>/<repo>` clones an index into your own home and `/plugin add <name>` installs a bundle out of it — the same words from a shell, as `io plugin marketplace add` and `io plugin add` — and what a bundle is allowed to do is shown before it may do it |
| **Readable without seeing it** | `--plain` animates nothing and commits every state change as text, for a screen reader, a braille display or a log |
| **Markdown, rendered** | Headings, bullets, code and emphasis drawn as themselves rather than printed as notation |
| **Documents, read and written** | Spreadsheets, Word, slide decks, PDFs and barcodes through io-harness's own tools — twelve of them, six of which write, every one under the same gate as any other read or write |
Expand Down Expand Up @@ -109,9 +112,11 @@ scoop bucket add io-cli https://github.com/initorigin/io-cli
scoop install io
```

The tap and the bucket live in this repository rather than in two of their own,
which is why `brew tap` and `scoop bucket add` name a URL. They install the same
checksum-verified artifacts the scripts do.
The tap and the bucket live in this repository rather than in two of their own —
[`Formula/io.rb`](Formula/io.rb) and [`bucket/io.json`](bucket/io.json) — which is
why `brew tap` and `scoop bucket add` name a URL: this repository is not named
`homebrew-io-cli`, so neither tool can derive the URL from the name. They install
the same checksum-verified artifacts the scripts do.

Both scripts pick the right build for your machine, **verify it against the
published `SHA256SUMS` before unpacking it**, and install into a directory you
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.38.0",
"version": "0.38.1",
"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.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"
"url": "https://github.com/initorigin/io-cli/releases/download/v0.38.1/io-0.38.1-x86_64-pc-windows-msvc.zip",
"hash": "f3cb3c0d6b31a8d37d9603e9faf1b38a8056c1558f0299cf2b6ac4eab689cbbf",
"extract_dir": "io-0.38.1-x86_64-pc-windows-msvc"
}
},
"bin": "io.exe",
Expand Down
28 changes: 24 additions & 4 deletions docs/CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ With no subcommand, `io` opens an interactive session.
| `io exec "<goal>"` | Runs one goal to completion with no terminal interaction |
| `io resume` | Carries on a run that stopped for a question, a plan or an interrupted call; `--list` shows what is waiting |
| `io mcp …` | Manage MCP servers without opening a session |
| `io plugin …` | Manage capability bundles and marketplaces |
| `io plugin …` | Manage capability bundles and marketplaces — the verbs are `add`, `install`, `list`, `search`, `remove` and `marketplace`. `io plugin marketplace add <owner>/<repo>` clones an index, `io plugin marketplace list` and `remove` are its other two, and `io plugin add <name>` installs a bundle out of one |
| `io config …` | Read and write configuration keys |
| `io skill …` | Add, list and remove skills |
| `io acp` | Serves the Agent Client Protocol on stdin and stdout, for an editor |
| `io upgrade` | Prints the command that updates this binary, for the way it was installed |
| `io acp` | Serves the Agent Client Protocol (ACP) on stdin and stdout, for an editor — an ACP client such as Zed or a JetBrains IDE spawns it; see [From an editor](guide/editors.md) |
| `io upgrade` | Prints the command that updates this binary, for the way it was installed: `brew upgrade io` from the tap this repository is, `scoop update io` from the bucket, and the installer line from a script install. It prints and does not run |

`--plain` reaches an interactive session and stops there. `io exec` builds no theme, draws
nothing and animates nothing already, so there is no second thing for the flag to switch off.
Expand Down Expand Up @@ -62,7 +62,20 @@ to the existing `3` would have moved exactly the runs `6` was invented for.
| `3` | CEILING | A step, time, cost or budget ceiling was reached, and nothing judged the work |
| `4` | PAUSED | The run is waiting on a question, a plan, an approval or an interrupted call — resumable with `io resume`, except an approval |
| `5` | UNFINISHED | The run ended in a state this table does not name |
| `6` | UNVERIFIED | The work was judged by a verification gate and did not hold up |
| `6` | UNVERIFIED | The work was judged and did not hold up — a verification gate that failed, or a run that never satisfied its output schema |

**Two different failures reach `6`, and a caller that has to tell them apart reads stderr rather
than the code.** A verification gate is `[app.io-cli.gates]`, this crate's own key, and a failing
one is io-harness's `VerificationFailed`. **The schema contract is io-harness's `[run]
output_schema`** — io-cli defines none and sets none, and `Config::apply_to` applies the operator's
to a session turn and to both headless doors alike — and a run that never produces the shape it
asks for is `RunOutcome::SchemaUnsatisfied`. Both mean the same thing, which is why they share a
code: the work produced an answer, something checked it, and it did not hold up. The line
`io exec` writes to stderr is what separates them, and the schema one says the run *never produced
the shape its output schema asked for*. Neither is a ceiling — reporting either as `3` would send
an operator to raise `[run] max_steps`, which buys more attempts at the same failure. Over ACP
both are `max_turn_requests`, because such a run exhausted the attempts it was allowed rather than
refusing anything.

Exit `4` names the `question_id`, `plan_id` or `attempt_id` that `io resume` needs — for three of
the four pauses. **An approval is the fourth and it names no invocation**, because there is none:
Expand Down Expand Up @@ -210,6 +223,13 @@ withheld is the moment you watch the agent reach for it. It passes the rule abov
bending it: no file is written, nothing is reassigned, and the mask reaches the **next** turn
because this one's contract was built before the keystroke.

**The mask changes what the agent may call and not what a turn costs.** io-harness sends the same
catalogue either way and io adds one sentence naming what is withheld, so a masked request is the
longer of the two. `/context allow <tool>` takes one name back and `/context allow` on its own
clears the mask; a name that matches nothing the last request carried is kept, because io-harness
keeps an unknown name deliberately so a mask stays portable between builds, and io says so on the
line.

**`/config` is admitted bare and refused the moment it carries a word.** Through 0.32.0 it was
refused in every form, because the bare list carried a row that re-read the provider's catalogue,
wrote the prices into a scope file and reassigned the configuration the running turn was holding —
Expand Down
Loading