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
78 changes: 77 additions & 1 deletion 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.37.0] - 2026-09-04

The release that says where your context went. `/context` has read the real outgoing request
since 0.21.0, but it drew the tool catalogue as one number — so an operator who wanted to know
which of their bundles was expensive had no way to ask. It is now attributed: a row per MCP
server, named with the bundle that contributed it where a bundle did, and a `skill catalogue`
row for the block io-harness writes into the system prompt. That last row is the one that was
missing entirely; before this release a session's skills were counted inside `system block`, so
they appeared to cost nothing at all. `/plugin` and `/mcp` draw the same figure on the row where
you switch the thing on, from the same snapshot, so the two surfaces cannot disagree.

The tiers are now stated rather than implied. Of the seven kinds of thing a bundle contributes,
three reach the wire — MCP servers, skills and agents — and four are free: hooks, templates, a
declared binary, and policy layers. A bundle that ships only hooks costs nothing on any request,
forever, and nothing told you that before.

And `/context withhold <tool>` takes a tool away and keeps it away until you say otherwise;
`/context allow <tool>` puts that one back, and bare `/context allow` clears the whole mask.
This answers a limitation this product already published in its own words: a model that
reaches for `docx_write` in a session where you never meant a document to be written was stopped
by the write gate rather than by the absence of the tool. Now it can be stopped by name, before
anything starts.

**A mask does not make a turn cheaper, and this release says so everywhere it could be
misread.** The roadmap entry this version was planned from assumed the opposite — that masking
would be the lever for cutting what a turn costs. io-harness 0.76.0's `ToolMask` is
*availability, never membership*: a masked run is offered a byte-identical catalogue, on purpose,
because the tool array sits ahead of the provider's cache breakpoint and dropping a definition
would save its tokens once and pay a cache *write* on every later turn. Withholding in fact makes
a request marginally larger, by one sentence naming the withheld tools, placed after the
observations where it costs no cache entry. So the two halves of this release do not compose:
attribution is the cost feature, masking is a scoping and safety feature, and no surface here
draws the second as the first.

### Added

- `/context withhold <tool>`, `/context allow <tool>` and `/context allow` to clear the mask.
Verbs under an existing command rather than a thirty-seventh: `COMMANDS.len()` is still **36**
and the group occupancy is still `[7, 9, 10, 10]`, so the product's last free slot is still
free. Same shape as 0.29.0's marketplace verbs inside `/plugin`.
- A `skill catalogue` section on `/context`, located by the sentence io-harness writes above it.
The constant is gated against the pinned `run/prompts.rs` itself, so a pin that copy-edits that
sentence fails the build rather than silently reporting zero — which is what the two memory
heads beside it still risk, and which reads exactly like a session with no skills.
- A per-request token figure on `/plugin` and `/mcp` rows, and a statement of which contribution
kinds are free.

### Changed

- `/context`'s sections are seven rather than six, and MCP rows name the bundle that contributed
the server. A bundle's server is drawn in the spelling you read (`docs:search`) rather than the
wire spelling — the translation is applied only to a bundle's server, because a plugin id
cannot carry the separator while a bare `[[mcp]]` id from `io.toml` is validated by nobody and
may carry one legitimately.

### Known limitations

- **An agent roster is not attributed to the bundle that contributed it.** io-harness composes it
into the system block with no marker io-cli can locate. It is counted, inside `system block`;
it is not counted *against the bundle*. A number invented for it would be worse than none.
- **A mask does not reach a spawned child.** io-harness builds a child's contract fresh and
carries no mask into it, deliberately — the mask is a request about the operator's own turn.
So with `[app.io-cli.containment]` configured, a withheld tool can still be called inside the
fleet. This is upstream's boundary, not a gap here.
- **A server that has not been on a wire is drawn as unmeasured, not as zero.** The two mean
different things and a zero would report an unmeasured server as free.
- `/mcp` now carries two counts of different things — what the server announced at connect time
and what actually reached the wire on the last request. They can legitimately differ, and the
row says which is which rather than putting two bare numbers side by side.

### Dependencies

- io-harness stays at **0.76.0**, the newest published. The first release in eighteen where the
pin does not move.

## [0.36.0] - 2026-09-03

The release that makes io reachable from an editor. The Agent Client Protocol is the field's
Expand Down Expand Up @@ -3322,7 +3397,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.36.0...HEAD
[Unreleased]: https://github.com/initorigin/io-cli/compare/v0.37.0...HEAD
[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
[0.35.0]: https://github.com/initorigin/io-cli/compare/v0.34.1...v0.35.0
[0.34.1]: https://github.com/initorigin/io-cli/compare/v0.34.0...v0.34.1
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "io-cli"
version = "0.36.0"
version = "0.37.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
8 changes: 7 additions & 1 deletion docs/CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,12 +188,18 @@ bare-host rule opens it.

**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;
`/config` are admitted 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.

Ten of the eleven only report. **`/context withhold <tool>` is the exception and it changes
something** — the session's tool mask — which is admitted because the moment you want a tool
withheld is the moment you watch the agent reach for it. It passes the rule above rather than
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.

**`/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
43 changes: 43 additions & 0 deletions docs/guide/accounting.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,49 @@ quotes USD and every provider it can connect to bills in it. Four decimals below
unit and two above, in integer arithmetic — a turn costs a fraction of a cent and
a month costs dollars, and one precision cannot show both.

### What your extensions cost, per request

`/cost` is what a run spent. `/context` is what it is *carrying* — and since
0.37.0 it says where that came from rather than giving you one number for the
lot. The tool catalogue is split by what put each tool on the wire: a row per MCP
server, named with the bundle that contributed it where a bundle did, plus a
`skill catalogue` row for the block io-harness writes into the system prompt. That
last one is new; before 0.37.0 a session's skills were counted inside the system
block, so they appeared to cost nothing.

`/plugin` and `/mcp` put the same figure on the row where you switch the thing on,
drawn from the same snapshot, so the two surfaces cannot tell you different
things about one server.

**Not everything a bundle contributes costs anything, and the difference is
large.** Of the seven kinds of thing a bundle can contribute, three reach the
wire and four do not:

| Contribution | Cost per request |
| --- | --- |
| MCP servers | every tool's name, description and JSON schema, every request |
| Skills | one line each — name and description. A body is read on demand by `read_skill` and only when the model asks |
| Agents | a roster in the system block |
| Hooks | nothing |
| Templates | nothing — io-cli expands them into your prompt |
| A declared binary | nothing |
| Policy layers | nothing |

So a bundle that ships only hooks is free forever, and one that ships an MCP
server with forty tools is not. That is the decision `/plugin`'s cost column
exists to inform.

Two honest gaps in the number. An **agent roster** is not counted against the
bundle that contributed it: io-harness composes it into the system block with no
marker io-cli can locate, and a number invented for it would be worse than none.
And a server that has not been on a wire yet is drawn as unmeasured rather than as
zero — the two mean different things, and only one of them is something you could
act on.

**Withholding a tool does not reduce any of this.** See
[What this release is not](limits.md) — the catalogue sent is the same either way,
by design. The lever these numbers inform is turning a bundle or a server off.

---

[README](../../README.md) · [All guides](../CAPABILITIES.md) · [What you may depend on](../CONTRACT.md)
2 changes: 1 addition & 1 deletion docs/guide/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ above a row that ranked there for reasons having nothing to do with it.
| --- | --- |
| `/help` | this table |
| `/status` | commit the whole session state into the scrollback |
| `/context` | what is in the model's window, read from the request that carried the turn |
| `/context` | what is in the model's window, read from the request that carried the turn, and withhold or allow a tool by name |
| `/expand` | commit the last step's full detail into the scrollback |
| `/fleet` | show the children this turn has spawned |
| `/skills` | every skill, shipped or yours: what it is for, whether it is on, and its file; add and remove one |
Expand Down
8 changes: 7 additions & 1 deletion docs/guide/keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,16 @@ the defaults that shipped, and marks `Ctrl+C` as fixed.

## While a turn is running

**Eleven commands report while the agent works**, and they are `/status`,
**Eleven commands run while the agent works**, and they are `/status`,
`/context`, `/cost`, `/stats`, `/help`, `/theme`, `/copy`, `/expand`, `/fleet`,
`/image` and `/config`.

Ten of them only report. `/context withhold <tool>` is the one that changes
something, and it is admitted because watching the agent reach for a tool is
exactly when you know you want it withheld. It takes effect on the **next** turn
— this one's contract was built before you typed — and it writes no file, which
is the test every command on this list has to pass.

The palette opens mid-turn too, and path completion with it — the `/` and the `@`
in the table above. `/compact` and `/steer` reach the running turn through their
own arms, as they always have, and are not part of this set.
Expand Down
30 changes: 29 additions & 1 deletion docs/guide/limits.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,42 @@ and the agent may decide to use it on a model that cannot see — and io-cli can
take a tool out of io-harness's own workspace tool set. Checking the model rather
than the provider would mean reading the live catalogue on every attach, and it
would still not close the door the agent opens. **If you work with images, choose
a model that accepts them.**
a model that accepts them.** From 0.37.0 you can also close that particular door
for a turn with `/context withhold view_image`, which refuses the call rather
than removing the tool — see below.

**The twelve document tools cannot be taken out of that tool set either**, and
six of them write. A model that reaches for `docx_write` in a session where you
never meant a document to be written is stopped by the write gate rather than by
the absence of the tool — which is what the gate is for, and why the writers are
named one by one in [Documents](media.md#documents) rather than counted.

**Since 0.37.0 you can stop it earlier, by name.** `/context withhold docx_write`
refuses that tool before anything is started, and it stays refused until you say
otherwise — `/context withhold` is a posture, not a one-shot. `/context allow
docx_write` puts that one back; **bare `/context allow` clears the whole mask**,
so name the tool unless dropping every one of them is what you meant. Both
sentences above stay true: the tool is not taken out of the
set, the catalogue the model is offered is byte for byte the one it would have
been offered anyway, and the definition still costs its tokens on every request.
What a mask changes is what may be *called*.

**A mask does not make a turn cheaper, and it is worth saying so plainly because
it is the obvious thing to assume.** io-harness sends the identical catalogue
either way on purpose — the tool array sits ahead of the provider's cache
breakpoint, so dropping a definition would save its tokens once and pay a cache
*write* on every later turn of the run. Withholding in fact makes a request
marginally larger: one sentence naming the withheld tools, placed where it costs
no cache entry. Use it to bound what the agent may do, and use `/context` and
`/plugin` to see what your extensions cost — those are two different questions
and only the second one is about money.

**A mask does not reach a spawned child.** With `[app.io-cli.containment]`
configured, a contained turn's children get contracts io-harness builds fresh,
and it carries no mask into them deliberately — the mask is a request about your
turn, and a child's work is not your turn. So a tool you withheld can still be
called inside the fleet. See [The fleet](fleet.md).

**An image the agent was *given* rather than asked for is not shown.** A picture
returned by an MCP tool, and a browser screenshot, both become images inside
io-harness — but through private plumbing and with no event of any kind, so
Expand Down
8 changes: 8 additions & 0 deletions docs/guide/media.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ twelve more of them: **io-cli cannot take a tool out of io-harness's workspace
tool set**, so a feature this crate turns on is a tool the agent has, and the
only honest thing to do with that is say so. See [Documents](#documents).

That is still true in 0.37.0, and it is worth being exact about what changed
beside it. `/context withhold <tool>` does not remove a tool from the set — the
catalogue the model is offered is byte for byte the same one — it withholds
*permission to call it* for the turn, and a call is refused before anything is
started. So the tool is still there, still costs its definition's tokens, and the
agent is told in the same request that asking for it will do nothing. See
[What this release is not](limits.md).

A picture is drawn from half blocks — `▀` splits a cell into two halves that are
each about square — fitted to your terminal's width and bounded in height. On
kitty, ghostty, WezTerm and Konsole a PNG is drawn as the **real image** instead,
Expand Down
Loading