Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ the only dev-dependency. Adding one is a deliberate, argued act — read the com
- **Process spawns** are permitted in `src/shell.rs` (the `!` escape) and `src/fetch.rs` (one
`git clone` for a marketplace) and nowhere else.
- **TOML parsing** is permitted in `src/edit.rs` alone. Every other module that needs a value out
of a configuration file goes through `edit::value_at`, `edit::sections` or `edit::array`.
of a configuration file goes through `edit::value_at`, `edit::sections`, `edit::keys` or
`edit::array`.
`src/marketplace.rs` reads a stranger's `plugin.toml` that way, and `src/adapt.rs` *writes* one
without ever parsing it.
- **JSON deserialization** is permitted in `src/import.rs` and `src/adapt.rs`, and nowhere else.
Expand Down
142 changes: 141 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,145 @@ All notable changes to this project are documented here. The format follows

## [Unreleased]

## [0.33.0] - 2026-08-31

io-harness 0.72.0 lets an agent ask several things at once and lets a choice carry
a description and a preview. All of it reached io-cli the moment the pin moved and
none of it was drawn: a batched ask emitted an event the transcript had no
disposition for, a described choice was a label with its sentence thrown away, and
a question that accepted several answers accepted one. A resumed batch was worse
than blind — it came back as one row of numbered prose with an empty choices
column, so what an operator saw was a wall of text and nothing to pick.

This release makes the question surface the one surface. A batch arrives as one
overlay and is answered in place; an offer explains itself and can show what
taking it would do; `Space` marks a set where the question takes one.

The other half is a write nobody chose. `/config` was refused mid-turn because its
bare list carried a row that re-read the provider's catalogue and wrote a scope
file — and because `Left` and `Right` on a row wrote the file on the keystroke,
with no confirmation, which was the only such write in the product. Both are gone
rather than guarded, and the bare `/config` reports while a turn runs.

### Added

**An agent can ask several questions at once and they arrive as one overlay.** One
question is on the screen at a time, with the same offers, context and free-text
row a single question has. Deciding one moves to the next undecided one; deciding
the last delivers the whole batch. `PgUp` and `PgDn` walk it, and a decided
question re-opens with your own answer back in the composer. Two lines of the head
say which question of how many, and what this one was already decided as.

There is no review pane and no submit key, deliberately. The answers are already
on the screen they were typed into, one page-key apart; a second rendering of them
is a second thing that can disagree with the first, and a submit key that answers
nothing is the reflexive `Enter` this overlay has always been careful about.
Nothing is sent until every question is decided, because io-harness commits a batch
only when every entry has an answer — four of five parks the run as thoroughly as
none. `Esc` decides the question on the screen as *nobody here can answer this* and
moves on; the run still parks, which is all `Esc` has ever promised.

**A choice can explain itself.** A description is drawn on a row of its own under
the label and stays there, because comparing five offers needs all five sentences
at once. A preview unfolds beneath the offer under the marker, one at a time, and
folds when the marker moves — five blocks at once is a wall nobody reads. It is
marked the way this product already marks quoted words, since a preview is
somebody else's text. `Enter` on an offer whose preview is open still answers with
that offer.

**A question that takes several answers takes them.** `Space` marks and unmarks
the offer under the marker, the offers carry a box from the moment the list opens,
and `Enter` sends the marked set — or, with nothing marked, the offer you are
looking at, because an empty answer is information the agent did not have and
would now believe. The set is joined by io-harness's own speller, so two interfaces
answering one question produce the same text. Marks are held against the list you
were given and survive the query that hides a row: an operator narrows a list in
order to find each row to mark, and a filter that un-marked as it went would throw
away the marks made under the last query.

**`/config` runs while a turn is in flight, in its bare form**, taking the
mid-turn set from ten commands to eleven. `/config <key>` and `/config <key>
<value>` descend toward a write and keep their refusal — the first time the
run-state guard has read past a command's first word. The whole-command refusal
still covers `/plugin`, `/mcp`, `/provider`, `/skills`, `/memory` and `/store`.

**`io plugin remove` takes a bundle's name as well as its directory.** The path is
read first and against the disk, so an existing script means what it always meant.
Two declared bundles of one name are refused with both directories printed rather
than resolved by order.

### Changed

**No arrow key writes a configuration file.** `Left` and `Right` on a `/config`
row used to step a boolean or a closed set of words to its next value and write
that value into a scope file on the keystroke, unconfirmed. They now open that
setting's values with the marker on the value in force, and `Enter` is the
confirmation every other managed surface already uses.

**The price refresh moved off the bare `/config` list** to one descent below
`prices.as_of`, the date it writes, with *leave it* at row 0. It was the last row
of the list, which made a keystroke on a surface whose whole job is reporting read
the network, write a file and reassign the running turn's configuration.

**`io resume --list` says how many questions a parked row is waiting on.** A
batched ask is one row with one id and one `--answer` answers all of it — there is
no per-question flag, because io-harness parks a batch as a single row and records
a single reply against it. The refusal now says so and tells you to number your
answers to match. The questions themselves are counted rather than pasted into a
one-line detail, which read as if the first were the whole ask.

### Fixed

**`io skill add <dir>/SKILL.md` installed a skill that could never be removed.**
The destination was named from the source's file name, so the commonest layout on
disk wrote `~/.io-cli/skills/SKILL.md` — a shape io-cli read as a folder skill and
then refused both to remove and to disable, forever, with a sentence about a
directory that did not exist. The installed file is named from the skill's own
name now, and `io skill remove <name>` takes it back out.

**A skill `/import` wrote as a folder could not be removed or turned off.** The
product shipped a verb that created state its own management surface refused to
manage. Both levers work on it, and a disabled folder skill stays visible on the
surface that re-enables it. Disabling a loose `SKILL.md` sitting directly in the
skills directory is refused: the move keeps the name, so it would land as
`disabled/SKILL.md` and be re-offered as a skill called *disabled*, taking every
parked skill's hiding place with it.

**`io plugin add` printed `plugin remove <id> takes it back out` and the verb read
its argument as a path**, so the sentence was false on the door that printed it.

**A batched ask emitted an event nothing in the transcript could dispose of.**
io-harness 0.72.0 emits `QuestionsAsked` for a batch and does not also emit
`QuestionAsked`, so the surface this release exists for was the one ask the
transcript was blind to. The declared event kinds move from 51 to 52.

**A resumed batch came back as a wall of text with nothing to pick.** The parked
row's question column is numbered prose and its choices column is empty, so a
resume read only those and drew both. The questions are carried whole now.

**Thirty-four of the forty-six in-page links in the documentation went nowhere.**
0.30.2 split a 2,847-line README into nineteen guide pages and moved the text
faithfully, but every anchor kept pointing inside the file it had left. The link
gate skipped fragment-only links by design, so nothing caught it and nothing
stopped it growing. Every anchor now names the page that holds the heading, and
the gate resolves both spellings instead of skipping one. Found while bringing this
release's own prose back to true, and repaired here rather than left for a version
that would have had to find it again.

**A comment said io-harness substitutes two forms when it substitutes three.**
`${cmd:…}` is the third, and the same sentence went on to say a manifest refuses
all three. The correction had already been made once, in 0.21.0, in the other file
that carries the claim.

### Known limitations

**A single question that takes several answers loses that fact when it parks.**
io-harness's `PendingQuestion` has no column for it and the singular writer records
none, so a lone multi-select that parks and is resumed comes back as a pick-one. A
batched ask keeps it, because a batch carries its questions whole. This is
upstream, and it is stated rather than papered over with a default that would read
as a fact.

## [0.32.0] - 2026-08-30

The agent could ask you a question and you could not answer it. `Intent::render`
Expand Down Expand Up @@ -2838,7 +2977,8 @@ client, tool, sandbox, policy engine or session store of its own.
- There is no crates.io publish and `cargo install` is not an install path.
- No test in this release asserts on wall-clock time.

[Unreleased]: https://github.com/initorigin/io-cli/compare/v0.32.0...HEAD
[Unreleased]: https://github.com/initorigin/io-cli/compare/v0.33.0...HEAD
[0.33.0]: https://github.com/initorigin/io-cli/compare/v0.32.0...v0.33.0
[0.32.0]: https://github.com/initorigin/io-cli/compare/v0.31.0...v0.32.0
[0.31.0]: https://github.com/initorigin/io-cli/compare/v0.30.2...v0.31.0
[0.30.2]: https://github.com/initorigin/io-cli/compare/v0.30.1...v0.30.2
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.32.0"
version = "0.33.0"
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.71", features = ["media", "browser", "documents"] }
io-harness = { version = "0.72", 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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ posture on the next.](assets/screenshot.png)
| **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 |
| **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 |
Expand Down Expand Up @@ -138,7 +139,7 @@ the limits that capability actually has.

| Guide | What it covers |
| --- | --- |
| [While it works](docs/guide/the-session.md) | What a running turn shows you, how the agent's manner is drawn, answering without opening a run, how much reasoning a turn buys, and background jobs |
| [While it works](docs/guide/the-session.md) | What a running turn shows you, how the agent's manner is drawn, answering without opening a run, how much reasoning a turn buys, background jobs, and the one surface the agent asks everything through |
| [Keys](docs/guide/keys.md) | Every default binding, and moving one |
| [Commands](docs/guide/commands.md) | All thirty-six, grouped by what you are doing |
| [Bringing your setup across](docs/guide/import.md) | What `/import` finds on this machine, item by item, and the credential it never reads |
Expand Down Expand Up @@ -196,7 +197,7 @@ says so once at startup, naming the number that is no longer in force and
`[run] max_steps` as where the cap lives now. **A section that was ignored may
start being read**, which 0.14.0 did to eleven of them, and that is a behaviour
change for a file that already carried one; it is the migration note in
[Configuration](#configuration) and in the changelog rather than something to
[Configuration](docs/guide/configuration.md#configuration) and in the changelog rather than something to
find out from a turn.

## Security
Expand Down
30 changes: 17 additions & 13 deletions docs/CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,19 +147,23 @@ points an operator can enter the state.
**Every HTTP MCP server is refused by default**, because the default network effect is `Deny`. A
bare-host rule opens it.

**Ten commands run while a turn is in flight, and the rest are refused.** `/status`, `/context`,
`/cost`, `/stats`, `/help`, `/theme`, `/copy`, `/expand`, `/fleet` and `/image` report while the
agent works; `/` and `@` open the palette and path completion; `/compact` and `/steer` reach the
turn through their own arms as they always have. Every other command keeps its refusal, in the same
sentence, and the rule is what a command *does* rather than how harmless it looks: anything that
reassigns the session or the provider, writes the store or a configuration file, or submits a turn
of its own is refused.

**`/config` is refused mid-turn in every form, including the bare one.** Its picker offers a row
that re-reads the provider's catalogue, writes the prices into a scope file and reassigns the
configuration the running turn is holding. The run-state guard is on the whole command deliberately
— splitting a bare form from an argued one is where a mistake ships a write into a running turn —
and the same applies to `/plugin`, `/mcp`, `/provider`, `/skills`, `/memory` and `/store`.
**Eleven commands run while a turn is in flight, and the rest are refused.** `/status`,
`/context`, `/cost`, `/stats`, `/help`, `/theme`, `/copy`, `/expand`, `/fleet`, `/image` and
`/config` report while the agent works; `/` and `@` open the palette and path completion;
`/compact` and `/steer` reach the turn through their own arms as they always have. Every other
command keeps its refusal, in the same sentence, and the rule is what a command *does* rather than
how harmless it looks: anything that reassigns the session or the provider, writes the store or a
configuration file, or submits a turn of its own is refused.

**`/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 —
and because a horizontal arrow on a row wrote the scope file where it stood. 0.33.0 removed both
acts rather than guarding them: the refresh is one descent below `prices.as_of`, the arrows open a
setting's values, and the bare list reports and nothing more. `/config <key>` and
`/config <key> <value>` descend toward a write and keep their refusal, which is the first time the
run-state guard has read past a command's first word. The whole-command refusal still applies to
`/plugin`, `/mcp`, `/provider`, `/skills`, `/memory` and `/store`.

**A mid-stream token figure is an estimate and is drawn as one.** `EventKind::Token` carries text
and no count, because providers do not bill per chunk, so the figure that moves while a step is
Expand Down
4 changes: 2 additions & 2 deletions docs/guide/accounting.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ whose prices it read and threw away on the same row. The rates land in `[prices]
which is io-harness's own section: `as_of` for the date, and one line per model
under `[prices.models]`.

**Refreshing is a row on `/config`, and it shows every rate that would move before
it writes anything** — what each was and what it would become — and you can
**Refreshing is a row one descent below `prices.as_of` on `/config`, and it shows
every rate that would move before it writes anything** — what each was and what it would become — and you can
decline the lot. That is not courtesy. The file records what a rate *is* and never
where it came from, so io-cli cannot tell a correction you made by hand from a
value an older catalogue served; it does not guess, it shows you. A refetch that
Expand Down
Loading