Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,33 @@ All notable changes to this project are documented here. The format follows

## [Unreleased]

## [0.25.0] - 2026-08-28

The work a turn does ends as something somebody can review. io-harness has
offered seven git built-ins — `git_status`, `git_diff`, `git_log`, `git_add`,
`git_commit`, `git_branch`, `git_worktree` — on every workspace run since long
before this interface existed, with no feature gate and a fixed argv that can
reach no other subcommand. io-cli had never surfaced one of them.

The branch the working tree is on is on the status line, read from `.git/HEAD`
rather than from a subprocess, and it follows the agent when it switches. A
commit the agent makes is committed to the scrollback with the message it wrote
and the branch it landed on. `/commit` hands the turn's work to the agent to
describe and stage, under the identity `[run.commit_identity]` names — the
identity 0.14.0 made reach a turn and which nothing had yet read.

**And it repairs what made all of that unreachable.** Under `ask-writes` — the
posture the wizard recommends — io-harness's git spawn treats an asking `exec`
policy as a hard refusal rather than raising an approval, so every one of the
seven tools was refused before it ran and the operator was never asked. io-cli
now names that refusal, before a turn is spent and whenever one arrives, and
offers the single rule that lifts it: `exec` allowed for `git`, one binary,
for the session.

A sub-agent can also work in a checkout of its own. `worktree = true` on an
`[[agent]]` entry roots that child under `.worktrees/` on its own branch before
its first step, and the fleet says which children have one.

## [0.24.0] - 2026-08-28

A turn proves its work instead of asserting it. io-harness has carried a
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.24.0"
version = "0.25.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
128 changes: 127 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,7 @@ above a row that ranked there for reasons having nothing to do with it.
| `/image` | draw an attached image again: /image 1 |
| `/copy` | put the last answer on the system clipboard |
| `/copy diff` | put the whole run's patch on the system clipboard |
| `/commit` | ask the agent to describe this turn's work and commit it; allow to permit git |

**inspect**

Expand Down Expand Up @@ -496,6 +497,13 @@ above a row that ranked there for reasons having nothing to do with it.
| `/gates` | the check a turn must pass before it is done: a command, a file, or a rubric |
| `/import` | bring instructions, MCP servers, skills and a model across from another agent |

`/commit` is new in 0.25.0 and sits under **this turn** beside `/copy` and `/copy
diff` because it is the third thing you do with work that has just finished: one
puts the answer somewhere, one puts the patch somewhere, and this one makes the
patch permanent. Its description says *ask the agent* because that is literally
what the word does — it sends a prompt, and the agent reviews, stages and writes
the message. See [Git](#git).

`/plugin` is new in 0.20.0 and sits beside those two because it is the third
surface of the same kind: something a configuration file declares by name, whose
effect on the session is otherwise invisible. See [Capability
Expand Down Expand Up @@ -1209,6 +1217,112 @@ back with.
`io exec` gains exit `6` for this — the agent finished and the work does not hold
up. See [Exit status](#exit-status).

## Git

A gate says the work holds up. This says what becomes of it.

The agent has had seven git built-ins on every workspace run since long before
this interface existed: `git_status`, `git_diff`, `git_log`, `git_add`,
`git_commit`, `git_branch` and `git_worktree`. They are io-harness's, and each is
a fixed argv that can reach no other subcommand — there is no `push`, no `remote`
and no `reset` among them. What was missing until 0.25.0 was any of it reaching
you.

**The branch the working tree is on is on the status line.** It is read out of
`.git/HEAD` as text, because git writes it there in a format that has not changed
in the lifetime of the tool and this program starts no process to ask — so it
costs a file read, and it follows the agent when the agent switches branch. A
detached head is drawn as a short object id rather than as nothing, and a
directory that is not a repository draws no field at all: `io` runs in plenty of
them and must not get worse there.

**A commit the agent makes is committed into the scrollback** — the branch it
landed on and the message the model wrote. The
diff is not drawn a second time. It is already on screen immediately above, from
the step that wrote it, and drawing it twice would cost you the reason the block
is there at all.

**`/commit` hands this turn's work to the agent, and the agent writes the
message.** io-cli runs no git and composes no subject line: the command sends a
prompt asking it to review what changed with those tools, stage what belongs to
this turn, and say what the change does and why. That is a billed turn against a
real model, and it is why the row says *ask the agent* rather than promising a
deterministic act.

**`[run.commit_identity]` decides who the commit is authored as**, and io-cli
reads that value rather than picking one. io-harness hands the name and email
from that section to git on the commit invocation itself, and the section always
resolves to something, so a repository with no identity of its own is told which
default io-harness will use. You are told before the turn is spent, because the
author of a commit is the one thing about it that cannot be corrected afterwards
without rewriting history.

### The refusal this repairs

**Before 0.25.0 all seven tools were refused before they ran, for most operators,
and nobody was ever asked.** io-harness's git spawn checks the `exec` policy
itself and accepts only an outright allow. Every other gated act turns an *ask*
into a question on your screen and waits for it; this one returns a refusal
instead, so `ask` behaves exactly as `deny` does. `ask before writes` — the
posture the wizard recommends, and the one this README recommends with it — sets
`exec` to ask. That is the whole of the defect, and it is filed upstream as
io-harness#214.

What io-cli does about it is name the refusal and offer one rule: `exec` allowed
for `git`, one binary, for this session. `/commit` asks that question *before* it
spends the turn, because a commit the policy was always going to refuse still
costs a real completion to discover. The rule goes through the same remembered
layer as anything else you allow for a session, so it is exactly as strong as
those and no stronger: it widens an asking default and cannot take back a deny
from a layer beneath it. One binary name is also the narrowest grant that works —
an `exec` pattern has no notion of a subcommand, so `git` says *this program may
be spawned* and nothing about any other.

**Under a posture that denies rather than asks, no rule is offered.** A rule is
matched before a default, so the same allowance would work under `read only` too,
and a keystroke that quietly defeats the one posture whose name is a promise is
not a convenience. What you are told there is to change posture — a decision, and
not a shortcut.

### A checkout of its own

Every agent in a tree shares one working directory, so two children editing the
same file are one overwriting the other. `worktree = true` on an `[[agent]]` entry
gives that child its own checkout instead:

```toml
[[agent]]
name = "reviewer"
worktree = true
```

io-harness roots it under `.worktrees/` in your repository, on a new branch
created before the child's first step. **The branch is not named after the
entry** — it is the entry's name, the parent run, the step and a digest of the
child's goal, so `reviewer` becomes something like `reviewer-12-3-a1b2c3d4`.
That is what makes two children of one entry, spawned in the same step, land in
two checkouts rather than one; and since nothing here removes either, it is also
the shape to look for when you go and find the work afterwards. If the worktree
cannot be
made — no git, not a repository, the boundary refusing that path — the spawn
fails with the reason and **no child starts**, rather than quietly sharing the
parent's tree and reintroducing the collision the switch exists to remove.
`/fleet` marks the rows whose roster entry asked for one. See [The
fleet](#the-fleet).

That mark is a property of the roster entry and not a directory. io-harness
records a child's actual worktree path and hands it back to nobody, so a path
drawn on that row could only be reconstructed — and a reconstruction is an
address that is wrong the moment either side changes, which matters here because
you would `cd` into it.

**What none of this does.** Nothing removes a worktree and nothing deletes a
branch: that is yours to do, because removing one throws away the work the child
was spawned to produce. io-cli does not open a pull request, and the seven tools
reach no remote at all. The work ends as commits on a branch in your own
checkout, and what happens to it after that is a decision this program does not
make for you.

## Pictures

**Drag a picture onto the prompt, or copy it and paste.** That is the whole of
Expand Down Expand Up @@ -1771,7 +1885,10 @@ from 0.24.0**, which was the last of these to be named here and was named with i
reason: it needed a surface of its own, and `/gates` is that surface. There is
still no `[verify]` section, because there is none in io-harness's schema to
apply — what a session carries comes from `[app.io-cli.gates]`, which is io-cli's
own. See [Verification gates](#verification-gates).
own. See [Verification gates](#verification-gates). **`[run.commit_identity]` is
*read* from 0.25.0**: it has reached a turn's contract since 0.14.0 like every
other section, and until there was a commit to author it was a value nothing in
this interface had cause to look at. See [Git](#git).

That leaves one key still unapplied, and it has a reason too: `run.templates` is
the thirteenth `[run]` key, reachable only through its own accessor. It is not a
Expand All @@ -1790,6 +1907,15 @@ allowlist it found and writes no rule at all. Setting the boundary here is
can see what you are granting. See [Bringing your setup
across](#bringing-your-setup-across).

**Git stops at your own checkout.** The seven built-ins the agent has are fixed
argvs with no remote among them, so nothing here pushes, fetches or opens a pull
request, and 0.25.0 adds no surface that does. Nothing removes a worktree or
deletes a branch either — both throw away work, and both are yours to decide. And
io-cli starts no git process of its own: the branch on the status line is a read
of `.git/HEAD`, and `/commit` is a prompt. What lifts the tools when your posture
refuses them is a permission rule, not a code path that runs git behind the
policy. See [Git](#git).

**Sixel is still absent**, because encoding it means palette quantisation and
another dependency, for terminals that either speak one of the two protocols
already here or draw half blocks correctly. The Kitty path covers PNG rather than
Expand Down
45 changes: 44 additions & 1 deletion docs/config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ net = "deny"
# [instructions] AGENTS.md and whatever else you point it at
# [[mcp]] MCP servers. Merged with [[app.io-cli.mcp]] below.
# [[lsp]] language servers. Merged with [[app.io-cli.lsp]].
# [[agent]] the roster a fan-out draws its children from
# [[agent]] the roster a fan-out draws its children from. 0.25.0
# adds `worktree`; see the block below
# [web] provider-executed search and fetch
# [memory] how much durable memory a run may keep
# [[plugin]] capability bundles, declared by path. 0.20.0; see the
Expand Down Expand Up @@ -207,6 +208,48 @@ net = "deny"
# transport = "http"
# url = "https://mcp.example.com/v1"

# --- The agent roster (0.25.0) ----------------------------------------------
#
# io-harness's own section: the roster a contained turn's fan-out draws its
# children from. `name` is what the parent asks for, and every other key narrows
# that child against its parent — a `role`, a `model`, a `max_steps`, an
# `effort`, and the two refusals `deny_write` and `deny_net`. An entry with
# nothing but a name is exactly the child a bare spawn produced before the
# roster existed.
#
# `worktree = true` GIVES ONE CHILD ITS OWN CHECKOUT. Every agent in a tree
# shares one working directory otherwise, so two children editing the same file
# are one overwriting the other. Set this and io-harness roots that child under
# `.worktrees/` in the repository, on a new branch created before its first
# step. THE BRANCH IS NOT NAMED AFTER THIS ENTRY: it is the entry's name, the
# parent run, the step and a digest of the child's goal — `reviewer` becomes
# something like `reviewer-12-3-a1b2c3d4`. That is what makes the line below
# true, and it is the shape to look for when you go and find the work.
# `/fleet` marks the rows whose definition asked for one.
#
# WHAT IT COSTS. Another checkout of the repository on disk, and a branch per
# CHILD rather than per definition — two children of the same entry spawned in
# the same step get one each, which is the collision this key exists to remove.
# It also gives the spawn a new way to fail: no git, not a repository, or the
# boundary above refusing that path, and then NO CHILD STARTS. Quietly sharing
# the parent's tree would be the exact overwrite the key was set to prevent.
#
# AND NOTHING REMOVES ONE. Not io-harness, not io-cli, not the end of the run:
# the worktree and its branch are still there when the tree finishes, because
# removing either throws away the work the child was spawned to produce. So
# `.worktrees/` accumulates until you clear it out yourself.
#
# [[agent]]
# name = "reviewer"
# role = "You read a diff and say what is wrong with it."
# model = "anthropic/claude-haiku-4.5"
# max_steps = 20
# deny_write = true
#
# [[agent]]
# name = "author"
# worktree = true

# --- Capability bundles (0.20.0) --------------------------------------------
#
# A bundle is a directory with a `plugin.toml` in it, and it is loaded because
Expand Down
Loading