Skip to content

Releases: initorigin/io-cli

io-cli 0.16.0

Choose a tag to compare

@github-actions github-actions released this 25 Aug 16:07
04fad6a

Nobody edits io.toml by hand.

/config is the whole configuration file as a surface in the session. Every
key io-harness validates, with the value in force and the file that decided it —
user, project, local, or default where no file decided it at all. A key
no file named names no file: io-harness returns an empty origin for it, and
attributing that to the lowest-precedence file would credit you for a value you
never wrote. Choose a row to put its key in the prompt; /config <key> <value>
asks which of the three files to write it to, and only that choice writes.

Your file survives the write. The comments, the blank lines, the key order
you chose, and every section io-cli has no type for — [[agent]], [[hook]],
[instructions], [toolchain], [prices], [[plugin]] — come back byte for
byte. One value's bytes are replaced and everything else is copied through. The
new bytes go to a temporary file and are renamed over the original, so a failed
write cannot truncate a configuration, and the file's mode is preserved.

A project-scoped change that would widen the boundary is refused, in
io-harness's own words.
All seven cases: a [[hook]] array, a [browser]
table, and policy.defaults.exec = "allow", policy.defaults.net = "allow",
sandbox.allow_network = true, sandbox.force_floor = false and
sandbox.mode = "full-access". The same values are accepted in io.local.toml,
because the rule is about the scope rather than the value. io-cli holds no copy
of those rules: it writes, asks io-harness to read it back, and puts the file
back exactly as it was when the answer is no.

/mcp shows what is configured, which servers answered this session, how
many distinct tools each has answered, and the last failure. A server the session
has not reached says so and is not drawn as a failure — that is the state
every server is in before the first turn runs. Servers are added, edited and
removed from the file through the same writer.

/provider is where [[provider]] stops being a single entry. Several are
configured, each with its own credential, model and endpoint, and the order they
are listed in is the order a turn tries them — the fallback io-harness has
supported since its 0.27.0, which this interface has drawn an event for without
ever being able to cause. Entries are added, reordered and removed, and an entry
moved keeps its own comments. The twenty-one vendor presets io-harness reaches
through one Compatible provider are offered by name, with the endpoint each
resolves to.

/profile selects a named [profile.<name>] for the session, and
--profile <name> selects one for a single run without writing anything.
Profiles have been in io-harness since its 0.27.0 and no io-cli release had ever
selected one.

Three ceilings gain a home: max_parallel_reads, spawn_background_after_secs
and detached_spawns, under [app.io-cli], because io-harness has no
configuration key for any of them. They apply to an interactive turn and to
io exec alike.

The command surface is grouped, because this release took it to twenty and a
flat list of twenty is a list nobody reads: the session, this turn, inspect,
configure, none longer than ten. The / palette shows the groups while you
browse and drops them the moment you type. /help is the same grouping, written
into the terminal's own scrollback. Each palette row now carries a mark saying
whether it runs a command or fills the prompt — beside the name rather than in
the description, because the description is what a narrow terminal drops first.
/usage answers what /status answers and is listed nowhere.

Removed

[app.io-cli] max_steps was removed, as 0.14.0 said it would be. It was
deprecated in that release with a notice naming this one, in the terminal, in the
README and in this file. Use [run] max_steps, which bounds a session turn and an
io exec run alike.

A file that still carries the key loads exactly as before — the key is
ignored rather than rejected — and the session tells you once at startup, naming
the number that is no longer in force. That notice is deliberate: [app.io-cli]
is not schema-checked, so without it the key would simply stop working and your
step cap would change with nothing on screen to say why.

Changed

The status line's mcp N/M tools now reads mcp N/M calls. The second number
has counted calls since 0.10.0: EventKind::Mcp carries no tool count and
io-harness exposes no catalogue accessor, so the number that field wanted was
never available and it counted the one that was. /mcp now draws a per-server
count beside it, and two numbers disagreeing about one word is worse than one
number with an honest label.

Known limitations

/mcp's tool count is how many distinct tools a server has answered this
session, which is a lower bound on what it offers. There is no channel for the
real number on io-harness 0.67. Disabling a configured server without removing it
is not offered either: McpServer has no key for it, and because the type is
#[serde(flatten)]-based an invented one would be accepted by the file and
ignored by the harness — so the server would start anyway. Both are io-cli
0.17.0's, behind an io-harness release.

Install

macOS and Linux:

curl -fsSL https://raw.githubusercontent.com/initorigin/io-cli/main/install.sh | sh

Windows, in PowerShell:

irm https://raw.githubusercontent.com/initorigin/io-cli/main/install.ps1 | iex

Both scripts verify the artifact against SHA256SUMS before unpacking
it and install into a per-user directory with no administrator rights.
The checksum defends against a truncated download and a tampered asset;
curl | sh is a trust-the-publisher model regardless.

io-cli 0.15.0

Choose a tag to compare

@github-actions github-actions released this 25 Aug 13:46
dac2cc7

io keeps its things in one place.

~/.io-cli is now the answer to "where does it live", on every platform, and an
existing install moves into it the first time you run this version.
Before this
release there were three answers and none of them carried the product's name:
~/.config/io on a Linux box, $XDG_CONFIG_HOME/io where that was set,
%APPDATA%\io on Windows — with the run store sitting beside whichever one
applied, undocumented and untested. One directory to back up, to copy to another
machine, to delete when you are finished with it, and to name in a bug report.

The move is the part to read before upgrading. On the first 0.15.0 run,
io.toml and the run store are moved from wherever they were into ~/.io-cli,
and each file that moved is named on screen — in the scrollback in a session, on
stderr under io exec, never on stdout, because --json writes NDJSON there. If
the run cannot start at all — a configuration file that will not parse, a store
that will not open — the report is still said, ahead of the error. That is the
moment it matters most: an error naming a path you have never seen, one keystroke
after your old directory emptied, is the reading this release exists to prevent.
Nothing is overwritten: where the home already holds a file, the one already there
is the one that stays and the other is left exactly where it was. Nothing is
deleted, and a file copied across filesystems has its copy checked before the
original goes. If you would rather keep the old location, set IO_CONFIG_HOME to
it before you first run 0.15.0 — and if you have already set IO_CONFIG or
IO_CONFIG_HOME, nothing here happens to you at all: no variable is set, no file
is moved, and you are not told about a migration that did not occur.

The store moves with the file because it has to. The run store's path is
derived from the configuration file's own directory, so moving one without the
other would empty /resume on upgrade. Its write-ahead log moves with it for the
same reason one level down: SQLite opens a runs.db whose -wal was left behind
without complaining and simply does not contain the last session, which is a loss
that arrives as a session that vanished rather than as an error. The durable
memory the agent writes for itself is rows in that store rather than a file, so it
travels with it.

This is a product choosing its home, not a second configuration system.
io-harness has resolved $IO_CONFIG, then $IO_CONFIG_HOME, then the platform's
own place since the 0.19.0 that introduced io.toml at all, and it reads them at
the moment it is asked. So io-cli names the second one for you when you have named
neither, once, before the first read — and the resolution order itself is
unchanged. One consequence stated rather than left to be found: the variable is
set in io-cli's own process, so every child a session starts inherits it — a !
shell line, a spawned agent, a nested io. For a nested io that is the right
answer; for anything else it is one more variable in the environment.

A tilde is a home directory now, in a skills path. io-harness substitutes
${env:…} and ${file:…} and nothing else, so a ~ written in a skills key
reached the directory walk verbatim and named a directory literally called ~
which the configuration example shipped as its own suggestion. io-cli expands a
leading ~ before handing the path over, and where no skills key is set at all
the default is ~/.io-cli/skills, which is created with the home so that it is a
real place to drop a file into. Where you have chosen your own location and io-cli
made no home, a default naming a directory that does not exist is not used at all
— io-harness refuses such a directory outright rather than finding nothing in it,
which would be every turn failing rather than an empty catalogue.

And / lists the skills the model was given. The palette walked
[app.io-cli] skills alone, so a [run] skills reached the agent while the
command list showed nothing from it. It asks for the same directory the turn is
built with now, which closes that gap rather than widening it with a new default.

/status gained a home row, naming the directory your configuration file and
run store are actually in and the word that decided it — default, IO_CONFIG or
IO_CONFIG_HOME. It reports the directory in force rather than the one io-cli
would have chosen, which are the same thing until you have chosen otherwise, and
that is exactly when a row like this earns its place.

Rolling back is a variable or a move, because nothing here destroys anything.
Reinstall 0.14.0 and set IO_CONFIG_HOME=~/.io-cli and it reads the moved files
where they now are, with no history lost; without that variable it looks in the
old location, finds it empty, and your files are still on disk in the home.

Install

macOS and Linux:

curl -fsSL https://raw.githubusercontent.com/initorigin/io-cli/main/install.sh | sh

Windows, in PowerShell:

irm https://raw.githubusercontent.com/initorigin/io-cli/main/install.ps1 | iex

Both scripts verify the artifact against SHA256SUMS before unpacking
it and install into a per-user directory with no administrator rights.
The checksum defends against a truncated download and a tampered asset;
curl | sh is a trust-the-publisher model regardless.

io-cli 0.14.0

Choose a tag to compare

@github-actions github-actions released this 25 Aug 04:42
1fd0f77

The configuration file reaches your terminal.

Eleven sections of io.toml that an interactive session read past are now
applied to every turn.
This is a behaviour change for any file that already
carries one, so it is the first thing said: [sandbox], [run],
[run.commit_identity], [instructions], [[mcp]], [[lsp]], [[agent]],
[web], [memory] and [browser] were read by io-harness, validated by
io-harness, documented in this product's own README and configuration example,
and then discarded by every session turn — a developer who wrote
[run] max_tokens = 200000 and watched a turn spend past it was reading a file
that did nothing, with nothing on screen saying so. The reason the documentation
gave had been stale for three releases: the harness's steerable turn built its
own contract, and the flat turn stopped taking that path in 0.11.0. What was left
was an omission and not a constraint. A session turn and an io exec run now
build the config-derived half of their contract from one call, so what the file
says has the same effect in a terminal as it has in CI.

A [run] block written for CI now bounds a conversation. max_steps = 20 is
a reasonable thing to have set for an unattended run and an unreasonable cap on a
session, and if that is what your file says, that is now what your terminal does.
The session names what the file turned on when it starts, the status line carries
each budget in force, and /status lists them all — so a turn that will stop at a
ceiling says which one before it gets there rather than after. An operator who
wants [run] for CI only moves it to a project file or narrows it by scope.

[web] is a capability and not a preference, and it deserves its own
sentence.
Reaching a session turn it gives the model the provider's own search
and fetch, and it is the vendor that dials the URL — so the net rule in your
permission policy is not what governs it. That rule decides what this machine may
reach. A [web] table that did nothing in your terminal yesterday turns something
on in it today, from a file you may have written for something else, which is why
the session says so in its own words at start rather than folding it into a list
of what was applied.

/status commits the whole session state into the scrollback. One fact per
row: the workspace and the session id with the turn its head is at, the provider
and model, every policy layer by name with the acts it governs, the containment
caps and the draw against them, the sandbox mode asked for beside the backend that
actually answered on this host, every budget with what is left, how full the
context is, and what is connected — MCP servers and language servers as answered
of configured, the browser, the skills directory. Every field on it is a value
io-harness supplied. It commits upward rather than opening a pane, the same answer
Ctrl+T and /expand already give, and it is not a table: a table has a column
width, the widest cell here is a workspace path, and a row too long for the
terminal is folded rather than cut. It is a command and not a key, deliberately —
a key is cheap to add later and expensive to take back once it is in anybody's
fingers.

The ceilings in force are on the status line, beside what has been drawn against
them.
left 17/20 steps, left 12.4k/200.0k tok, left 4m30s/10m00s, for each
of the step, token and duration budgets that exists and for no others: a budget
you did not set draws no field, so a session that configured nothing looks exactly
as it did. They are read off the contract the turn was built from rather than
composed a second time out of the file, which is the only place the order of
precedence is already resolved.

A turn that ends on a budget says which budget. step_cap_reached,
time_budget_exceeded and cost_budget_exceeded were reported through the error
path, so what an operator met under a half-finished answer was
error: step_cap_reached — a ceiling drawn as a crash. All four outcomes are
successful calls in io-harness and always have been. The word stays the harness's,
because this interface reports what the harness decided and never relabels it;
what changed is the weight it is said in.

Three event kinds reach the transcript that never have. All three have been
emitted into every ordinary session and dropped here. Every outbound connection
a contained command dialled
is now a line carrying the host as the command asked
for it, the port and whether the policy permitted it — never a resolved address,
because the policy's patterns are written against names and a row showing
140.82.121.4 would not match the rule that decided it. A refusal is drawn as a
refusal and not as an error: nothing broke, the boundary worked. An absent dial
line is not evidence of no egress — a permissive or all-or-nothing policy
names no host and emits none of these ever. Each sandbox created, capped or
destroyed
says so, with the backend that isolated it where io-harness carries
one and no invented name where it does not; a cap reached is drawn as a limit
reached, because the sandbox did exactly what its configuration told it to.
A stalled agent is on screen while it is stalling, naming the step it stopped
on and how long it has been there, rather than reaching you as a session that had
gone quiet and then, once the run was over, as one word on the outcome line.

io exec takes io-cli's own step floor of a thousand, where it used to take
io-harness's twelve. Twelve steps is not a turn, so an unattended job ended
error: step_cap_reached over half-finished work with nobody watching — the same
defect the floor exists to fix in a session, made worse rather than better by the
run being unattended. A [run] max_steps in the file still beats the floor, in
either direction.

Servers named in both scopes are merged rather than one list replacing the
other.
io-harness's with_mcp and with_lsp assign the whole collection, so
applying [[mcp]] and then [[app.io-cli.mcp]] in sequence silently discarded
the first list — an operator with servers in both would have lost one set with no
message. They are concatenated and deduplicated by id, the [app.io-cli] entry
winning a collision because it is the more specific scope, and the session names
the id that lost.

[app.io-cli] max_steps is deprecated, still honoured, and removed in 0.16.0.
It exists because the flat turn once had no way to raise io-harness's cap of
twelve, and [run] max_steps now does that job — two spellings for one number in
one file, where the less discoverable of the two wins. Nothing about it changes
here: a file carrying it gets exactly the cap it asks for, and still beats
[run] max_steps. What is new is one line at session start naming the key, the
value it took and where the number moves to. A file carrying only [run] max_steps
says nothing, and neither does a file carrying neither — a deprecation notice on a
session that is not using the deprecated key teaches operators to stop reading the
start-up lines.

A startup notice is committed rather than said. Six things can put a sentence
in that list — a section io-harness could not read, a keybinding naming no action,
a templates directory that would not walk, a skills directory that would not
either, a server named in both scopes and this release's max_steps deprecation —
and they were written to the footer, which replaces: a file with several things
wrong with it showed the last one and silently dropped every earlier one. Each
takes a row of its own in the scrollback now.

The documentation said the opposite of the code, and in five places it had been
wrong since 0.11.0.
docs/config.example.toml carried a block headed "Not read
by an interactive session" naming eight tables; the README marked skills,
[[app.io-cli.mcp]], [[app.io-cli.lsp]] and [app.io-cli.browser] "contained
turns only", said the capabilities and the fan-out were one switch, and said an
uncontained session could not be given a responder or a plan gate. All four
capabilities have been applied unconditionally, and the responder with them, since
0.11.0 gave the ordinary turn a contract. Those claims are gone. Nothing rides
[app.io-cli.containment] but the fan-out.

No key is added, removed or renamed, and a 0.13.1 configuration file is a valid
0.14.0 configuration file; what changes is what an existing one does, which is the
migration note above. An older binary reading a file that has moved to
[run] max_steps falls back to io-cli's own floor, which is what it did before
the key existed. Nothing is asked of io-harness; the pin stays at 0.66.

Install

macOS and Linux:

curl -fsSL https://raw.githubusercontent.com/initorigin/io-cli/main/install.sh | sh

Windows, in PowerShell:

irm https://raw.githubusercontent.com/initorigin/io-cli/main/install.ps1 | iex

Both scripts verify the artifact against SHA256SUMS before unpacking
it and install into a per-user directory with no administrator rights.
The checksum defends against a truncated download and a tampered asset;
curl | sh is a trust-the-publisher model regardless.

io-cli 0.13.1

Choose a tag to compare

@github-actions github-actions released this 24 Aug 17:47
2e2bb08

The session answers every keystroke.

The prompt froze when it grew past two rows, and it is fixed. Pressing the
newline key a second time — or running /clear, or expanding a large pasted block
back to its full text — could stop the session dead for seconds, and on a measured
run it stopped for 5.7 seconds and then answered nothing at all. All three do the
same thing underneath: they re-place the inline viewport, which needs the terminal
to itself for a moment, and the keyboard reader was taking it straight back every
time it let go. A reader now stands aside while a placement wants the terminal.
The same keystroke, measured on the same script against the same binary: 5.7
seconds of silence before, 11 milliseconds after.

A prompt written on more than one line is read back as more than one line. A
two-line prompt was echoed as one run-together row, because a rendered line is one
row and a newline inside it draws as nothing.

/attach takes the path you actually have. Three things were wrong with it at
once. A path dragged in from Finder arrived quoted and the quotes were never taken
off, so the extension read as png" and io said your screenshot was not an image.
The quoting escaped every non-ASCII character, which includes the narrow no-break
space macOS puts in every screenshot's name — so the path named no file even
unquoted. And a path outside the workspace was refused outright, which is where
screenshots live. /attach ~/Pictures/shot.png now works. A path inside the
workspace still goes through the session's policy, unchanged; a path outside it is
read directly, because that is the operator's own file and the same boundary !
already crosses.

The prompt wraps, and there is one cursor. tui-textarea scrolls sideways
rather than wrapping and paints its own block cursor, while everything io-cli
measures assumes a wrap — so a long prompt was drawn clipped at the left, with
two cursor blocks on it in two different places, and the viewport had grown for
rows nothing used. The composer draws its own wrapped rows now: text that reaches
the right edge continues on the next line, the window follows the insertion
point, and the only cursor on screen is the terminal's own.

Pasting the same block again toggles it both ways. Expanding a collapsed
paste used to leave the block in the prompt with its placeholder gone, so the
next paste of the same clipboard appended a fresh one — [pasted text #2], then
#3, then #4, piling up after text that was already there.

A pasted block deletes as one thing on every backwards deletion.
Option+Backspace and Ctrl+W used to eat [pasted text #8, 464 characters]
one word at a time, and a placeholder is matched by its exact text — so the first
press had already stopped it standing for the block it named.

The composer is one row at rest and grows to what a prompt needs. The second
row was there for a paste too big to read in one and was empty for every prompt
anybody types.

/attach is gone; drag a picture onto the prompt or paste it. A command was
something you had to be told about before you could use the feature. Pasting the
same file again toggles between the marker and the path it stands for. The word
is not kept as a hidden alias: /attach is answered the way any other word that
is not a command is — there is no /attach. The commands are: — and the list
under it is the truth.

A marker deletes with the space written for it, so one press removes one
thing under every backspace — Option+Backspace used to eat 1] off the end and
leave [Image # on the prompt. The path a repeat paste toggles to is quoted, the
way any pasted path is. And /clear resets the numbering: [Image #1] and
[pasted text #1] count from one again, because the ones before them belonged to
the conversation that ended.

An attachment is [Image #1]. A picture is no longer drawn when you attach
it or when you send the turn: the marker is what the prompt carries, what the
agent is told and what the transcript keeps, and it deletes as one thing exactly
as a pasted block does. /image 1 draws it when you want to see it — a fresh
copy at the bottom, because a committed row belongs to the terminal's scrollback
and nothing here can reach back into it. /image is no longer a second spelling
of /attach.

Notices moved to the footer. Stopping one turn used to leave three
warning-coloured rows in your scrollback — stopping at the next step boundary,
stopping now, stopped — sitting between two answers forever. None of them is
part of the conversation. A notice now takes the footer's last row, replaces the
one before it, and is gone at your next keystroke. What still reaches the
transcript is what belongs to the record: what was authorised, what was answered,
and why a turn failed.

A drop of several files is several pictures. A terminal writes a multiple
selection on one line, separated by spaces with any space inside a name escaped —
or one per line. Read as a single string none of that was a path, so dropping
three pictures at once did nothing at all.

A turn stopped before it did anything is taken back whole. No step, nothing
streamed, nothing on screen but the echo of your prompt: esc abandons it at
once, the rows come off the screen, and the prompt goes back in the composer
ready to edit or send again. Nothing is said, because nothing happened.

A rule over the composer, matching the one under it. The prompt had a
boundary on one side only, so it read as the tail of whatever the turn had last
written rather than as the field it is.

A picture no longer lands on top of what was there. The rows a committed
image occupies were the viewport a moment ago, and nothing erased them, so an
image that did not fill its box was drawn into a stale prompt and status line.

A failed turn says what it means before it quotes the provider. Attaching a
screenshot to a model that cannot look at one used to end with
error: escalated_terminal and a routing layer's JSON about HTTP 404. Six
conditions now get a sentence in front of the provider's own line — no image
support, no credit, a rate limit, a rejected credential, an unroutable model, and
a conversation past the context length. The provider's text is never replaced,
only prefaced.

The work is now above the line that says it is working, with a row of air
between them. Through 0.13.0 the streaming row was drawn under the activity line,
so the newest words the agent had written read as a footnote to a spinner rather
than as the transcript continuing. If you have a screenshot or a recording of an
older release, this is what looks different.

Install

macOS and Linux:

curl -fsSL https://raw.githubusercontent.com/initorigin/io-cli/main/install.sh | sh

Windows, in PowerShell:

irm https://raw.githubusercontent.com/initorigin/io-cli/main/install.ps1 | iex

Both scripts verify the artifact against SHA256SUMS before unpacking
it and install into a per-user directory with no administrator rights.
The checksum defends against a truncated download and a tampered asset;
curl | sh is a trust-the-publisher model regardless.

io-cli 0.13.0

Choose a tag to compare

@github-actions github-actions released this 24 Aug 01:42
fbc5243

Five defaults that were never set.

Every turn now carries a system prompt io-cli wrote, and a model will answer
differently under it.
This is the change with the widest reach, so it is the
first thing said: through 0.12.0 every turn ran io-harness's built-in
description, which names the tools and says nothing about tone, format or length.
The new prompt sets what io is, that the reader is at a terminal, that the
answer comes first and briefly, that work is reported once done rather than
narrated in advance, and that the output is monospaced text about eighty columns
wide. It is appended to io-harness's own prompt, not substituted for it, so
the harness keeps its framing, its tool catalogue, the repository's own
instructions and the sentence that decides how a turn ends. It names no model and
no vendor and claims no tool. There is no configuration key for it: per-repository
voice belongs in the file io-harness's [instructions] discovers.

The palette no longer grows the viewport, and no longer shows every command at
once.
If you are used to seeing the whole list on /, you will notice. What you
get instead is the rows the session's viewport already has, and the rest by typing
or arrowing — the behaviour /model has always had. What that buys is the
keystroke: opening the palette used to re-place the viewport, which asks the
terminal where its cursor is and takes the stdin lock to read the answer, and did
it again on the way out. On a terminal that does not answer that query, / cost
two seconds. It now costs a repaint.

A blank row between a designed block and your next prompt. A thought footer, a
tool cell or a harness warning followed by the line used to read as one block
in one voice. One row of air, never two.

io names the newline key that works on your terminal. Shift+Enter is
unreportable unless the terminal speaks the Kitty keyboard protocol — it sends the
same byte as Enter — so on a terminal that does not, /help's key reference and
the wizard's closing screen now name Alt+Enter and the trailing backslash and
say the key is unreportable here rather than listing it. The README's table keeps
the advertised spelling, because a README is read somewhere else. Nothing about
the composer changed: all four spellings work exactly as they did.

The installers say what they are doing. install.sh and install.ps1 now
narrate every step on stdout: the target they resolved, where the version came
from, each URL as it is fetched, the expected and the computed checksum both
before comparing them, the destination and whether it is on PATH, and the
installed binary's own --version. Every failure path is unchanged, message and
exit status, and stays on stderr.

No configuration change: a 0.12.0 configuration file is a valid 0.13.0
configuration file, and no key is added, removed or reinterpreted.

Install

macOS and Linux:

curl -fsSL https://raw.githubusercontent.com/initorigin/io-cli/main/install.sh | sh

Windows, in PowerShell:

irm https://raw.githubusercontent.com/initorigin/io-cli/main/install.ps1 | iex

Both scripts verify the artifact against SHA256SUMS before unpacking
it and install into a per-user directory with no administrator rights.
The checksum defends against a truncated download and a tampered asset;
curl | sh is a trust-the-publisher model regardless.

io-cli 0.12.0

Choose a tag to compare

@github-actions github-actions released this 23 Aug 17:18
666d039

The capabilities stopped being a mode.

A contained turn no longer proposes a plan unless you asked for one. This is
the one thing in this release that takes something away, so it is the first thing
said: through 0.10.0 and 0.11.0, configuring [app.io-cli.containment] also
registered io-cli's plan gate, and registering a gate is the entire condition for
io-harness's planning phase — so every turn of a fan-out session stopped and
proposed before it did anything. If that is the behaviour you were relying on,
/plan on is where it lives now, and nothing else changed about it.

/plan on | off, off by default. It takes effect from the next turn, and bare
/plan reports which phase you are in without switching it, the same rule
/contain follows. While the phase is on the status line says planning — it is
not cleared when a run ends, because the phase outlives the turn it was set on and
an operator watching an agent that will not write needs the reason on screen.

A question is answered on any turn. The responder was on the contained turn
only, so on an ordinary turn the agent asking what you meant paused the run with
nobody offered the question. io-harness resolves a contract's responder inside the
tool dispatch on any run, so there was never a reason for that; there is now an
overlay wherever the question is asked.

The contained-mode notice stopped overstating what the mode gives. It offered
skills, MCP servers, language servers and a browser as things containment grants,
and named a lost mid-turn steer as its price. Neither has been true since 0.11.0
gave the ordinary turn a contract: all four are on every turn, and no turn takes a
steer inbox. The notice now names the caps and one difference — this is the only
turn that can fan out — and /contain off says "not contained" rather than
promising a steering this product no longer has.

Nothing is asked of io-harness; the pin stays at 0.66. A 0.11.0 configuration file
is a valid 0.12.0 configuration file.

Install

macOS and Linux:

curl -fsSL https://raw.githubusercontent.com/initorigin/io-cli/main/install.sh | sh

Windows, in PowerShell:

irm https://raw.githubusercontent.com/initorigin/io-cli/main/install.ps1 | iex

Both scripts verify the artifact against SHA256SUMS before unpacking
it and install into a per-user directory with no administrator rights.
The checksum defends against a truncated download and a tampered asset;
curl | sh is a trust-the-publisher model regardless.

io-cli 0.11.0

Choose a tag to compare

@github-actions github-actions released this 20 Aug 18:36
c4c272f

The transcript's vocabulary changed.

Four tags stopped appearing: prompt_composed, contained, reasoning and
answered.
They were never designed lines. io-harness declares fifty-one event
kinds and thirty-seven of them fell through to a placeholder that committed the
variant's own snake-cased name, which is what put Rust identifiers in front of
whoever was reading a session. Every kind now has a disposition chosen by hand —
a line, a status-line field, or nothing — and a kind io-cli has never seen
commits nothing at all and is counted instead.

Nothing about the permission boundary, the approval overlay, the containment
seam, the scrollback contract or the io-harness pin changes. This release asked
io-harness for nothing.

Added

  • The activity line, a new top row of the viewport present for exactly as
    long as a turn is in flight: a word for the turn, the elapsed clock and the
    live token count. The word is chosen once per step from a fixed list, so it
    moves when the work does and not on a timer of its own. On a narrow terminal it
    drops the token count and then the clock, which is the rule the status line
    already follows.
  • A live row that says what is happening, in one order: waiting on you, then
    an open tool call and its target, then the model thinking, then the streaming
    tail. Waiting on a person outranks everything else, because every other thing
    that row can say is about work going on without you.
  • The model's reasoning, committed as a thought — one row: the word, how long
    the step had been going, and what it cost. The text is kept for /expand and
    not committed: a thought is usually longer than the answer it precedes, and a
    transcript carrying every one buries the work in the deliberation. /expand is
    the only place it can be read, because io-harness neither stores reasoning nor
    folds it into the next prompt.
  • Two status-line fields: the provider and the step count. Both are set from
    the events that carry them and both are cleared when a run is forgotten. They
    are where the two removed rows' facts went.
  • /clear — a new conversation without leaving the binary: a new session id,
    no prior turn sent to the model, and the run-scoped status fields back to zero.
    It clears the screen and nothing else; the conversation it ends is in
    io-harness's store and is still listed by /resume. Refused while a turn is
    running.
  • /exit is listed, and /quit is gone. The parser has accepted exit
    since 0.1.0 and nothing ever advertised it; two commands doing one thing, with
    a row each in the palette, was the other half of that defect.
  • The model's markdown is rendered rather than printed. Headings, bullets,
    quotes, rules, fenced code and inline bold, italic and code — a line at a time,
    because that is how the transcript commits. Anything unrecognised is left
    exactly as the model wrote it: a renderer that guessed would eat characters out
    of an answer.
  • The composer. Pasting the same block twice expands the placeholder into the
    block; backspace over a placeholder removes the whole placeholder and the block
    it stands for; a pasted path that names a file is quoted and resolved, so a
    path with a space survives as one word.

Changed

  • A tool cell reads as a verb and a path: Read src/lib.rs rather than
    read_file and an absolute one. The mapping is a table of io-harness's own
    built-in tool names; a tool that is not in it keeps the name io-harness sent,
    because a verb invented for a tool this release has never seen would mean
    nothing. A target inside the workspace is shown relative to it and one outside
    is shown whole.
  • A turn ends on its answer. The finished · N steps · N tok row is gone. An
    outcome that stopped short still commits its own line, because a run that
    stalled or hit a ceiling has to say so; a plain finish commits a blank line.
  • via {provider} is gone from under every prompt. The provider is a
    status-line field now, spelled the way the posture is.
  • The viewport is eight rows, not four: a blank, the activity line, the live
    row, two rows of composer, and a three-row footer. It is still clamped to the
    terminal, so 80x24 is a supported size — the rows go in the order they can be
    given up, and the composer keeps its two at every size.
  • The command palette shows the whole list. Opening / re-places a taller
    viewport for as long as the palette is open and gives the rows back on close —
    by a choice, by Esc, or by the terminal resizing under it. It is done only at
    an empty prompt, where nothing is streaming.
  • --plain still commits the provider and the run's numbers. The two rows
    this release removed moved to a line a plain session does not have, and a fact
    that lives only in a repainting row is a fact taken from exactly the reader who
    cannot follow one. It is committed in the status line's own spelling, so a
    number has one form wherever you meet it.
  • A step commits a line only when it says something its tool cells did not.
    Through 0.10.0 every call was printed twice — once as a cell and once in the
    step line under it, in a different order and a different vocabulary. What is
    left for that line is what the cells cannot carry: files changed, or a decision
    that could not be paired to a call.
  • A cell's result column carries what io-harness added, not what the cell has
    already said.
    Read io.toml · read io.toml is now Read io.toml, and
    List · list_dir (4 entries) is List · (4 entries).
  • An outcome that stopped short says what it means. step_cap_reached,
    stalled, the three budgets, plan_rejected, cancelled,
    awaiting_recovery and escalated each get a sentence under io-harness's own
    word. A run used to end on error: step_cap_reached and nothing else.
  • Spacing. One blank row between a block of tool cells and whatever follows,
    one between a designed line and the model's prose, one between turns rather
    than two, and a paragraph break inside a thought is one row rather than two.
  • A viewport erases its own rows before handing them back, so the palette
    leaves nothing painted behind the session it returns to.
  • A turn is no longer capped at twelve steps. Every turn now carries a
    contract io-cli built — the ordinary one through turn_bounded_observed, the
    contained one as before — so the step cap is this product's rather than
    io-harness's default. It is a thousand, which is not a number anybody reaches
    on purpose: what ends a turn should be the work finishing, a stall, a budget or
    you, never an arithmetic ceiling reported as error: step_cap_reached under a
    half-written file. [app.io-cli] max_steps sets your own.
  • Esc stops a running turn, which is what it is for in every other agent,
    and a second press of Esc or Ctrl+C stops it now rather than at the next
    step boundary. The first press is still the clean stop: the run closes itself
    and the store records how it ended.
  • Shift+Enter has two fallbacks that always work. It needs the Kitty
    keyboard protocol, and a terminal without it sends the same byte for Enter.
    Alt+Enter and Ctrl+J insert a newline everywhere, alongside the trailing
    backslash.
  • The footer is three rows: a rule, then two lines. One long dot-separated
    run of eight fields is a sentence with the punctuation removed. Now the state
    and the model sit on one row with the clock at the right edge, the counts and
    the posture on the row under it, and exactly one thing is bold and one is
    coloured — which is what makes either mean anything.
  • The prompt takes the rows it needs, up to ten, and gives them back.
  • /clear opens the session again, banner and all, rather than leaving a
    cleared screen with one grey line on it.
  • The banner is a card with room in it: the mark, the version, and the model,
    policy and workspace, one blank row inside each edge and two columns inside
    each side.
  • Shift+Tab cycles the posture silently. The footer repaints on the same
    keystroke, so the line it used to commit said in the scrollback what the screen
    was already showing — and cycling through three postures to reach one left
    three of them behind, permanently, in the transcript of a session that ran
    under one.
  • The clock and the activity line's token count belong to the turn. Both
    start at zero when a turn starts: a clock counting since the terminal opened
    said 22m12s about a turn six seconds old. The footer keeps the session's
    token total, because that is what a spend is judged on.
  • A diff carries line numbers, in each side's own file, with a blank row
    above it. A change you can see but cannot go to is half a diff.
  • An approval is said once. The overlay carries the request, so the
    transcript no longer commits the same sentence directly above it. In plain
    mode, which draws no overlay, the line is still committed.
  • The footer says working only when nothing above it does. The activity
    line already carries a spinner and a word; a second spinner under it turning at
    the same rate said one thing twice.
  • A step commits no line when its cells already said it, changed files
    included — the diff underneath is what says a file changed.
  • The keyboard-protocol probe is asked once per process. It costs two seconds
    on a terminal that never answers, and the palette re-places the viewport twice
    per open and close.

Install

macOS and Linux:

curl -fsSL https://raw.githubusercontent.com/initorigin/io-cli/main/install.sh | sh

Windows, in PowerShell:

irm https://raw.githubusercontent.com/initorigin/io-cli/main/install.ps1 | iex

Both scripts verify the artifact against SHA256SUMS before unpacking
it and install into a per-user directory with no administrator rights.
The checksum defends against a truncated download and a ...

Read more

io-cli 0.10.0

Choose a tag to compare

@github-actions github-actions released this 19 Aug 16:51
8eb3a57

A contained session answers.

The two places a run stops and waits for a person are answered where they
happened, the skills you gave the agent are in the palette, and the line says
what the session is connected to.

All of it rides [app.io-cli.containment], and that is worth reading before
you configure any of it.
io-harness offers exactly one session entry point that
takes a caller's TaskContract — the contained one — and a responder, a plan
gate, MCP servers, language servers, a browser and a skills directory are all
fields of that contract. So the capabilities and the fan-out are one switch. What
it costs is nothing that turn ever had: a contained turn has never taken a steer
inbox. A session without the table is the session 0.9.0 shipped, mid-turn
Ctrl+C included.

Added

  • The agent's question about intent, answered in the session it was asked in.
    Not an approval — an approval asks whether an act is permitted, this asks what
    you meant, and its answer authorizes nothing. So it is prose you type rather
    than one of three keys. Esc leaves it unanswered, which pauses the run with
    the question kept rather than sending the agent back with nothing.
  • A plan, decided before any of it runs. Registering a gate turns
    io-harness's planning phase on, and while it is on the run's own policy denies
    every write and every exec — so cancelling is not an undo, there is nothing yet
    to undo. Enter on an empty prompt approves, a correction and Enter sends it
    back for another plan, Esc cancels and nothing runs.
  • Harness skills in the / palette, after the commands and the templates,
    discovered by io-harness from the skills directory. Choosing one puts use the <name> skill: in your prompt; the file is the model's to read, under the
    run's own policy. io-cli parses no skill file.
  • The line says what the session is connected to — an MCP server and how many
    tools it offered, a language server that came up for this workspace, and the
    browser with the last host it was allowed or refused, drawn differently
    because a block that reads like a visit is worse than no field at all. Every
    one comes off the event stream, so a server that was configured and never
    answered leaves the line silent, which is the honest answer.
  • [[app.io-cli.mcp]], [[app.io-cli.lsp]], [app.io-cli.browser] and
    skills
    , deserialized straight into io-harness's own types. io-cli defines
    no schema for any of them.
  • The real image on iTerm2. Its escape has no equivalent of Kitty's C=1, so
    the placement is bracketed by a cursor save and restore — which is what that
    flag was doing — and states its width and height in cells, so the rows it
    costs are known before it is written. Terminals that speak neither protocol
    still get half blocks and no escape at all.

Changed

  • A contained turn is driven through Session::turn_contained_bounded_observed
    and carries a contract this crate built. A session that configures nothing
    builds a contract identical, field for field, to the one io-harness built for
    it before.
  • A contained turn now stops for a plan before it acts, because registering
    a plan gate is what turns io-harness's planning phase on and a contained turn
    carries one. That is a round trip 0.9.0 did not have; /contain off gives back
    a turn that starts working immediately.

Install

macOS and Linux:

curl -fsSL https://raw.githubusercontent.com/initorigin/io-cli/main/install.sh | sh

Windows, in PowerShell:

irm https://raw.githubusercontent.com/initorigin/io-cli/main/install.ps1 | iex

Both scripts verify the artifact against SHA256SUMS before unpacking
it and install into a per-user directory with no administrator rights.
The checksum defends against a truncated download and a tampered asset;
curl | sh is a trust-the-publisher model regardless.

io-cli 0.9.0

Choose a tag to compare

@github-actions github-actions released this 19 Aug 14:35
b17b9ed

The session gains sight, in both directions.

You can show the agent a picture, and you can see the picture the agent looked
at, in the terminal you are already in rather than by going and opening a file.

Added

  • /attach, which puts an image in front of the agent for the next turn and
    only the next turn.
    The path is read through io-harness's Workspace, which
    documents that as the same policy gate a source read passes rather than a
    second one — so an image the session may not read is refused exactly the way a
    file it may not read already is. The argument can be @-completed, because the
    path picker opens on @ after any whitespace and not only at an empty prompt.
  • The picture the agent looked at, committed where it looked. Enabling
    io-harness's media feature puts its own view_image tool into the workspace
    tool set, so the agent gains the ability to look at images in this release.
    It is governed by the same policy as any other read, and when it does look, the
    same picture goes into your scrollback at that point in the conversation.
  • Half-block rendering, which works on every terminal. splits a cell into
    two halves that are each about square, so a picture is drawn from the cells the
    terminal already has, fitted to its width and bounded in height.
  • The real image where the terminal speaks the Kitty graphics protocol
    kitty, ghostty, WezTerm and Konsole. Placed with C=1, which is what lets it
    sit inside a renderer that draws the cells around it.
  • Background shell handles are named, counted and accounted for. A
    shell_start outlives the step that launched it, which is the whole point of
    it and the whole problem: a run waiting on a dev server looks exactly like a
    run that has hung. The command is named when it starts, a bg N field counts
    what is still alive, and each job says how it ended — exited with a status,
    killed, or left running by a run that finished first.

Changed

  • io-harness is taken with features = ["media"]. The pin does not move; 0.9.0
    is still built against 0.65.
  • image is the eleventh direct dependency, with default-features = false and
    exactly the nine formats io-harness will accept from a file. Its defaults would
    pull an AV1 encoder and rayon into a crate that only ever decodes a file the
    harness has already accepted.

Not in this release

  • iTerm2's own inline-image protocol. Its escape has no equivalent of Kitty's
    C=1: it advances the cursor and may scroll, and a scroll changes what every
    later absolute cursor move in the same draw means. It probably lines up against
    a region of exactly the right height — but "probably" is not good enough when
    the failure lands in scrollback that no later redraw can clean. iTerm2 gets the
    cell form, which is a picture. Deferred to 0.10.0.
  • Sixel. Encoding it means palette quantisation, which means another
    dependency, for terminals that either also speak Kitty or render half blocks
    correctly.
  • The graphics path for jpeg, gif and webp. Kitty's f=100 is PNG, and the
    only base64 in reach is the one io-harness already computed — this crate takes
    no base64 dependency. Media::attach transcodes bmp, tiff, ico, tga and pnm to
    PNG on the way in, so those reach the graphics path along with png itself,
    while jpeg, gif and webp take the cell form.
  • Any check that the chosen model accepts images, as opposed to the
    provider.
    Provider::accepts_images is asked before an attachment is
    accepted, but with OpenRouter in front of four hundred models that answer is
    yes while the model you picked may be text-only — and the failure then is the
    provider's own HTTP 404: No endpoints found that support image input, after
    the step and its tokens are spent. Found by running the built binary. It cannot
    be closed from here anyway: enabling images gave the agent view_image, and a
    tool in io-harness's workspace set is not io-cli's to remove.
  • Anything the agent was given rather than asked for. An image 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 — there is no media
    variant among its fifty-one — so nothing reaches this program to draw.
  • Live indicators for MCP servers, language servers and the browser. All
    three are fields of a task contract supplied by the caller, and no io-harness
    session entry point takes one, so those events cannot fire in a session at all.
    They already work in io exec, whose contract carries them, and --json
    already emits them. Moved to 0.10.0, which waits on the same change.

Install

macOS and Linux:

curl -fsSL https://raw.githubusercontent.com/initorigin/io-cli/main/install.sh | sh

Windows, in PowerShell:

irm https://raw.githubusercontent.com/initorigin/io-cli/main/install.ps1 | iex

Both scripts verify the artifact against SHA256SUMS before unpacking
it and install into a per-user directory with no administrator rights.
The checksum defends against a truncated download and a tampered asset;
curl | sh is a trust-the-publisher model regardless.

io-cli 0.8.0

Choose a tag to compare

@github-actions github-actions released this 19 Aug 04:23
ad2dba9

A decomposed task becomes visible while it runs.

An agent can break work into sub-agents and run them over the same workspace.
io-harness has been able to do that since 0.39.0 and nothing has ever shown it.
This release does: the children, the tiers, the refusals and what the fan-out is
costing are on screen while it happens, and every one of those is a fact only
this core emits.

Added

  • [app.io-cli.containment], and the contained turns it selects. Four caps —
    agents, agents at once per tier, depth, and a token ceiling the whole tree
    draws down together — read as io-harness's own type, so there is one spelling
    of them. With the table present a session's turns go through the one entry
    point that reaches io-harness's spawn loop; with it absent, every turn is the
    turn 0.7.0 shipped. /contain on|off switches, and /contain on its own
    reports rather than guessing.
  • A live fleet view, over the prompt, opened by Ctrl+F or /fleet. One row
    per admitted child with its state and its own draw, indented by its depth, and
    a per-tier line counting what is working, waiting and done. A waiting child is
    a count and never a row: until a concurrency slot frees it has no run of its
    own to name, and a placeholder for one would put an agent on screen that does
    not exist yet.
  • Spawns, refusals, collected reports and detached children in the
    transcript
    , where they happen. A refusal says which cap refused it in words
    and that the agent carries on with what it has, because a refusal is not an
    error. A collected report names no child — the event carries none, and with
    several in flight the order they arrive in is not identity.
  • The spend field on the status line, six releases after it was named. What
    this turn has drawn and what the tree has left, in tokens; a tree reporting no
    ceiling gets none stated rather than a zero. It was unreachable until now
    because io-harness emits the draw only from its contained loop.
  • A sixth rebindable action, fleet, defaulting to Ctrl+F.

Changed

  • io-harness moves from 0.64 to 0.65, which makes RunOutcome
    #[non_exhaustive] and adds AwaitingRecovery. io exec maps the pause to
    its existing "paused" exit code and describes it; an outcome a later harness
    adds now exits as unfinished rather than breaking the build, so the property
    the old exhaustive match carried moved to a test that reads the variants out of
    the locked source and fails naming the one the table missed.
  • EventKind::RecoveryPaused renders with the tool and the attempt id a recovery
    decision has to name, rather than as the muted word.

Known limitations

  • A contained turn cannot be steered. io-harness has no session entry point
    that takes a caller's containment and a steer inbox together, so a turn that
    fans out cannot be redirected while it runs. Ctrl+C still ends it, through
    the observer, at the next point where no child is in flight — the interface
    says that is what it is waiting for rather than appearing to have missed the
    key.
  • A contained turn applies no agent roster, no [run] budget and no
    [sandbox].
    It is built from the session's own default contract, the same
    reason a steered turn does not apply them. The containment table's own token
    ceiling is what bounds it.
  • A collected report is attributed to the tree and not to a child, because
    ChildCollected carries no run id.
  • The view closes when the turn ends. The tree is kept — /fleet reopens it,
    and every spawn, refusal and report is in the transcript — but the prompt comes
    back on its own rather than staying hidden behind a tree that has stopped
    moving.
  • The fleet view is four rows. The viewport's height is fixed for the life of
    the terminal, and rebuilding it while a run is committing into scrollback is
    not a trade this release takes.

Install

macOS and Linux:

curl -fsSL https://raw.githubusercontent.com/initorigin/io-cli/main/install.sh | sh

Windows, in PowerShell:

irm https://raw.githubusercontent.com/initorigin/io-cli/main/install.ps1 | iex

Both scripts verify the artifact against SHA256SUMS before unpacking
it and install into a per-user directory with no administrator rights.
The checksum defends against a truncated download and a tampered asset;
curl | sh is a trust-the-publisher model regardless.