Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d405683
feat(term): make a viewport re-placement something a test can execute
aakashpawar1999 Aug 30, 2026
67f16cc
feat(rows): measure wrapped rows, and say how many were dropped
aakashpawar1999 Aug 30, 2026
c81f5a2
feat(app): the viewport is the size of what it has to show
aakashpawar1999 Aug 30, 2026
b3959a4
feat(intent): the question becomes answerable, as one list with no modes
aakashpawar1999 Aug 30, 2026
57e6ea1
fix(triage): a question is drawn once, and never as a warning
aakashpawar1999 Aug 30, 2026
777e944
fix(tone): say refused only where a boundary refused
aakashpawar1999 Aug 30, 2026
1cc25f4
feat(naming): read a bundle's name, keep io-harness's
aakashpawar1999 Aug 30, 2026
13a0bac
feat(queue): a message reaches the turn it was typed into
aakashpawar1999 Aug 30, 2026
58ee9f1
feat(main): the command surface works while the agent does
aakashpawar1999 Aug 30, 2026
ab8e730
feat(status): the token figure moves while the turn is spending
aakashpawar1999 Aug 30, 2026
72e006b
fix(truncation): say what was not drawn
aakashpawar1999 Aug 30, 2026
ecafc3f
perf(resolved): resolve the plugin set once, not twice a turn
aakashpawar1999 Aug 30, 2026
1ea6dd2
test: prove the headless path untouched and the narrow forms intact
aakashpawar1999 Aug 30, 2026
b602081
docs: move the documentation with the code, and bump to 0.32.0
aakashpawar1999 Aug 30, 2026
2c9d925
test: gate the two criteria the sabotage pass found ungated
aakashpawar1999 Aug 30, 2026
48667e5
style: rustfmt, and the lockfile's version
aakashpawar1999 Aug 30, 2026
f5b8a5a
fix: the adversarial review's three HIGH and four MEDIUM
aakashpawar1999 Aug 30, 2026
b21d6e6
fix(main): a picker does not re-place the viewport
aakashpawar1999 Aug 30, 2026
8cc4901
fix: three lesser findings from the review
aakashpawar1999 Aug 30, 2026
9ca9c67
Merge pull request #81 from initorigin/feat/0.32.0
aakashpawar1999 Aug 30, 2026
f384cb7
docs: the README no longer says the viewport is eight rows
aakashpawar1999 Aug 30, 2026
e2bd3cb
docs: two more claims the display translation made false
aakashpawar1999 Aug 30, 2026
e1a1e65
Merge pull request #83 from initorigin/feat/0.32.0-readme
aakashpawar1999 Aug 30, 2026
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
101 changes: 100 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,104 @@ All notable changes to this project are documented here. The format follows

## [Unreleased]

## [0.32.0] - 2026-08-30

The agent could ask you a question and you could not answer it. `Intent::render`
drew the answer composer only where there was room left over, and a question with
a context line and five choices filled all eight rows of the viewport — so what
you actually saw was a question, some inert bullets, and nothing to type into.
The same question reached the screen twice, and the second copy was labelled
`warning:`.

That fixed eight-row viewport is one cause behind four defects, so this release
makes the viewport the size of what it has to show. The rest of what is here
follows from it: a plan overlay that keeps its own footer, a queue you can read,
pickers that say how much they are not showing.

Alongside it, the interface stops withholding things it already does. Ten
commands run while a turn is in flight. A message typed mid-turn reaches that
turn. A bundle's skill is drawn under a name you can read and type. The token
figure moves while the tokens are being spent. And the plugin set is resolved once
for the session rather than twice for every message.

### Added

**The viewport grows to what a surface asks for, and gives the rows back.** A
question overlay asks for its offers and its composer, a plan overlay for its
steps and its footer, the queue for one row per message, a picker for its list.
The ceiling is your terminal's height less four rows — not a ration, but the
exchange the surface is about, kept visible. On an 80×24 that is a twenty-row
viewport. `VIEWPORT_HEIGHT` is a floor now rather than a fixed size.

**The question overlay is answerable, as one list with no modes.** The agent's
offers and a row for an answer nobody offered are peers in the same list. `Enter`
on an offer sends it verbatim; the last row unfolds a composer directly beneath
it and typing there sends prose. Typing anywhere moves the marker to that row, so
you can simply start writing. The marker opens there too, never on the agent's
first suggestion — a reflexive `Enter` must not become silent agreement with
something you have not read.

**`Tab` takes the row under the marker, in every list in the product**, and
`Shift+Tab` steps back. Both were unbound and did nothing.

**Ten commands run while a turn is in flight**: `/status`, `/context`, `/cost`,
`/stats`, `/help`, `/theme`, `/copy`, `/expand`, `/fleet` and `/image`. `/` opens
the palette mid-turn and `@` completes a path. Everything that reassigns the
session or the provider, writes the store or a configuration file, or submits a
turn keeps its refusal.

**A message typed during a turn is delivered to that turn**, at its next step
boundary, and recorded in the transcript as it goes. `/steer` is unchanged. A
message that reaches no further step boundary still runs as its own turn
afterwards.

**A live token figure**, estimated from the streamed text and written `~1.2k tok`
so it cannot be read as settled. The provider's own number replaces it when the
step commits.

### Changed

**A bundle's contributions are drawn `bundle:name` rather than `bundle__name`** —
on `/skills`, in the palette, in the `Read skill` line, in `/plugin`'s inventory
and on `/status`. io-harness's separator is unchanged and still what the model
sees; io translates at the two edges. Choosing a skill now writes the command
`/bundle:name` instead of the sentence `use the <name> skill: `, which submitted
as an ordinary prompt and left it to the model to interpret.

**The plugin set is resolved once per session** rather than twice on the build
path of every turn, and re-read only when a declared manifest's stamp changes.
`/plugin` and `/skills` cost nothing to open when nothing has moved.

**Tones say what they mean.** `refused:` is reserved for an act the permission
boundary refused. A mistyped `/effort` argument, a run that is simply not in the
store, three watch errors and two `/memory` bookkeeping lines were all wearing it;
a plan proposal and a question were drawn as warnings. All corrected.

### Fixed

**A question is drawn once, and never as a warning.** It was committed to the
transcript and redrawn by the overlay, with the overlay's copy prefixed
`warning:`. The transcript line survives wherever nothing else will draw it —
under `--plain`, and on a resumed run, which has no overlay in this process at
all.

**Overlays measured themselves by counting lines while rendering through a
wrapping paragraph**, so a long question or plan step consumed more rows than the
count admitted: the offers and the footer fell off the bottom, and the composer
was then drawn over rows already painted. Everything measures wrapped rows now.

**The plan overlay keeps its `Enter approves / Esc cancels` footer.** It was the
last line pushed and so the first line lost, on the one overlay whose own
documentation forbids exactly that.

**Truncation says what it dropped.** A picker over four hundred rows, a fleet view
with more children than rows, and `/stats`' per-table breakdown all cut silently;
`/stats` was capping at five on a page with unlimited rows.

**`outcome_help` told you a parked question or plan could not be answered by this
release and to say it in your next prompt.** `/resume` has answered both since
0.23.0.

## [0.31.0] - 2026-08-30

io-cli has operated a marketplace nobody could stock. Every capability bundle
Expand Down Expand Up @@ -2740,7 +2838,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.31.0...HEAD
[Unreleased]: https://github.com/initorigin/io-cli/compare/v0.32.0...HEAD
[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
[0.30.1]: https://github.com/initorigin/io-cli/compare/v0.30.0...v0.30.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.31.0"
version = "0.32.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
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,12 @@ posture on the next.](assets/screenshot.png)

`io` does not enter the alternate screen and does not capture the mouse, in any
mode, behind any flag. Every finished message, tool call and system line is
committed into the terminal's own scrollback; eight rows at the bottom hold the
committed into the terminal's own scrollback; a few rows at the bottom hold the
unfinished tail of a streaming answer, a blank row, the activity line, a rule,
one row of composer and a three-row footer, and only those repaint.
one row of composer and a three-row footer, and only those repaint. Eight rows is
the floor: a surface that needs more — a question with its offers, a plan with its
steps, the messages you have queued — takes what it needs and gives it back,
stopping four rows short of the top so the exchange it is about stays visible.

So when the session ends the whole conversation is still there. Your terminal's
search finds it, tmux copy-mode scrolls it, and a mouse drag selects it — none of
Expand Down
25 changes: 25 additions & 0 deletions docs/CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,31 @@ 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`.

**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
streaming is derived from the delta text and written `~1.2k tok`. It is replaced by the provider's
own number the moment the step commits, and the settled form has no tilde. Nothing adds the two
together.

**A bundle's contributions are displayed with a colon and addressed with io-harness's separator.**
`ultraship:brainstorm` is what you read; `ultraship__brainstorm` is what the model was shown and
what `read_skill` resolves. `io exec`, `io plugin` and `io skill` report the wire name, because a
script addresses the wire.

## What may change

Everything not named above. In particular: the layout of what is drawn on the terminal, the
Expand Down
30 changes: 29 additions & 1 deletion docs/guide/keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
| `Ctrl+F` | show the fleet: the children this turn has spawned |
| `y / a / n` | answer an approval: allow once, allow this session, deny |
| `Esc` | stop the running turn, or close a picker without choosing |
| `/` | at an empty prompt, open the command palette |
| `Tab` | in any list, take the row under the marker; `Shift+Tab` steps back |
| `/` | open the command palette — at the prompt or while a turn runs |
| `@` | after a space, complete a path from the workspace |
| `!` | run the rest of the line in your shell; the agent never sees it |

Expand Down Expand Up @@ -134,3 +135,30 @@ the defaults that shipped, and marks `Ctrl+C` as fixed.
---

[README](../../README.md) · [All guides](../CAPABILITIES.md) · [What you may depend on](../CONTRACT.md)

## While a turn is running

**Ten commands report while the agent works.** `/status`, `/context`, `/cost`,
`/stats`, `/help`, `/theme`, `/copy`, `/expand`, `/fleet` and `/image` all answer
mid-turn, `/` opens the palette and `@` completes a path, and `/compact` and
`/steer` reach the running turn as they always have.

Until 0.32.0 every slash but those last two was refused with *not while a turn is
running — Ctrl+C interrupts it first*, so reading a status page meant stopping
your own work to do it. None of these was a missing capability; each was one the
product had and withheld.

**Everything else keeps that refusal, and the rule is what a command does rather
than how harmless it looks.** A command that reassigns the session or the
provider, writes the store or a configuration file, or submits a turn of its own
is refused: `/clear`, `/resume`, `/fork`, `/model`, `/undo`, `/setup`, `/import`,
`/profile`, `/effort`, `/contain`, `/plan`, `/gates`, `/remember`, `/commit`,
`/config`, `/plugin`, `/mcp`, `/provider`, `/skills`, `/memory`, `/store`, and a
`!` line.

**`/config` is refused even bare**, and that is worth saying because it looks like
a report. Its picker offers a row that re-reads your provider's catalogue, writes
the prices into a scope file and reassigns the configuration the running turn is
holding. The guard is on the whole command rather than on the verb, deliberately:
splitting a reading form from a writing one is where a mistake ships a write into
a turn that is already running.
39 changes: 35 additions & 4 deletions docs/guide/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,15 @@ misjudged can do to your permission boundary is take something out of it.

**A bundle id must match `[a-z0-9][a-z0-9-]{0,31}`**, and every name it
contributes is rewritten by io-harness to `<bundle>__<name>` at load — an agent's
name, an MCP server's id, a policy layer's name. `/plugin` draws that namespaced
string unchanged rather than a prettier short form, because it is what a refusal
will name, what a tool call will name, and what you will type to spawn the agent.
A shorter name here would be a third spelling of the same thing.
name, an MCP server's id, a policy layer's name. `/plugin` draws it **qualified**
rather than shortened, because the qualifier is what a refusal will name, what a
tool call will name, and what you will type to spawn the agent; a bare `reviewer`
would say nothing about where it came from.

Since 0.32.0 the qualifier is drawn with a colon — `rust-review:reviewer` — while
io-harness keeps its own separator on the wire. That is a translation at the edge
and not a third spelling: `io plugin list` and `io exec` report the underscore
form, because a script addresses the wire.

**Hooks are named, everywhere, from 0.30.0.** Each one gets a row: the event it
fires on, or the tool call it sits in front of, and the command it runs. This is
Expand Down Expand Up @@ -220,3 +225,29 @@ rather than ignoring it. Remove the `enabled` keys before downgrading.
---

[README](../../README.md) · [All guides](../CAPABILITIES.md) · [What you may depend on](../CONTRACT.md)

## When io re-reads your bundles

Reading a bundle is not free: every declared `plugin.toml` is opened, parsed,
validated and trust-checked, and every skill file inside it is read in full to
recover its name and description. Until 0.32.0 io did all of that **twice for every
message you sent**, and again each time you opened `/plugin` or `/skills`. With a
few bundles installed that is a pause before every turn.

**It is now read once for the session**, at startup, and again only when something
on disk has moved.

**How io decides it moved.** It stats each declared bundle's `plugin.toml` and
compares the modified time and the length against what it recorded. That is a
handful of `metadata` calls, not a re-read — which is why `/plugin` and `/skills`
open instantly when nothing has changed.

A bundle installed or removed is **always** seen, because the declared set itself is
compared rather than each entry in it.

**The limit, stated rather than implied.** On a filesystem whose modified-time
granularity cannot separate two writes inside one second, a second edit that leaves
the file exactly the same length is not distinguishable from the first, and io will
not notice it until something else about the bundle changes. Restart the session,
or touch the file, if you have hit that. io says this rather than claiming a
freshness it cannot prove.
33 changes: 30 additions & 3 deletions docs/guide/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,10 @@ that is why, and `/skills` is where you see which directory each name came from.
**A bundle's skills are listed too, under the name the model actually uses.**
Until 0.21.0 they reached the model and appeared on no surface that lists a skill,
so `/skills` and the `/` palette were lists that disagreed with the catalogue the
turn was handed. They are in both now, spelled `<bundle>__<name>` — io-harness's
own namespacing, and the string a refusal or a tool call will name — with the
bundle named as where the row came from.
turn was handed. They are in both now, qualified by the bundle they came from —
drawn `<bundle>:<name>` since 0.32.0, and addressed as io-harness's own
`<bundle>__<name>`, which is what a refusal or a tool call will name. See *The
name you read, and the name the model was shown* below.

**Turning a bundle's skill on or off is refused, and the refusal is the honest
answer.** Turning a skill off is moving its file into a `disabled/` directory
Expand All @@ -130,3 +131,29 @@ as well.
---

[README](../../README.md) · [All guides](../CAPABILITIES.md) · [What you may depend on](../CONTRACT.md)

## The name you read, and the name the model was shown

A bundle's skill is qualified with the bundle it came from, so two bundles can
each contribute a `review` and both are addressable. io-harness joins the two
halves with a double underscore — `ultraship__brainstorm` — and that string is
load-bearing: it is what goes into the model's own catalogue, and `read_skill`
resolves it by exact match, so the model can only ask for the string it was shown.

**Since 0.32.0 you read `ultraship:brainstorm` instead.** The `/skills` list, the
`/` palette, the `Read skill` line in your scrollback, `/plugin`'s inventory and
`/status`'s policy rows all draw the colon form, and typing `/ultraship:brainstorm`
runs it. Nothing about the wire changed: io translates at the two edges — once on
the way to your screen, once on the way back from your keyboard — and io-harness
never sees a colon.

`io exec`, `io plugin` and `io skill` report the underscore form, because a script
reads their output and a script addresses the wire.

**Choosing a skill from the palette now writes a command rather than a sentence.**
It used to put `use the <name> skill: ` into the composer, which submits as an
ordinary prompt — so whether the skill actually ran depended on how the model read
an English request. It writes `/ultraship:brainstorm` now, and that dispatches.

A skill whose name collides with a built-in command resolves to the command. No
command contains a colon, so the two can always be told apart.
Loading