Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# The five skills io ships are `include_str!`'d into the binary and written to
# disk verbatim, so their bytes are a product artifact rather than a source file
# a developer reads. Without this, a Windows checkout produces CRLF, the same
# release ships two different sets of bytes, and the manifest digest of a file io
# wrote differs by the platform it was built on.
#
# io-harness reads either fence — `split_front_matter` strips `---\n` and
# `---\r\n` alike and trims `\r` off every key — so this is about the artifact
# being one artifact, not about the parser coping.
skills/*.md text eol=lf
22 changes: 16 additions & 6 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,22 @@ updates:
# graph and closed nothing. So the dependency was removed rather than
# bumped: the editing model is `src/editor.rs` now, and ratatui is on 0.30.
#
# Alert #1 was dismissed as not-reachable rather than fixed by a version,
# and the evidence is this: in ratatui 0.29 `lru` backed the layout cache
# only (`src/layout/layout.rs:9` and `:31`), and `iter_mut` appears nowhere
# under `ratatui-0.29.0/src/layout/` — so `lru::IterMut`, which is the
# entire subject of the advisory, was never constructed on any path this
# binary could reach. The tree is clean by version now in any case:
# **Alert #1 closed as `fixed`, not dismissed**, at 2026-08-26T14:57:27Z —
# the moment the merge put `lru 0.18.2` in `Cargo.lock`. The plan had been
# to dismiss it as not-reachable, and the plan was overtaken: a dependency
# change that moves the version out of range closes the alert on its own,
# and a dismissal on top would have recorded a waiver for something that was
# actually repaired. Check an alert's state after the merge before
# dismissing anything.
#
# The unreachability evidence is kept because it is why this was never
# urgent, and why the release could be planned around the migration rather
# than rushed: in ratatui 0.29 `lru` backed the layout cache only
# (`src/layout/layout.rs:9` and `:31`), and `iter_mut` appears nowhere under
# `ratatui-0.29.0/src/layout/` — so `lru::IterMut`, the entire subject of the
# advisory, was never constructed on any path this binary could reach.
#
# What holds the tree clean from here is a gate rather than a judgement:
# `ratatui-core` 0.30 takes a current `lru`, and
# `tests/dependencies.rs::n1_the_composer_is_free_and_no_lru_is_in_the_advisorys_range`
# fails the build if any `lru` in the lockfile falls inside the range.
Expand Down
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,50 @@ All notable changes to this project are documented here. The format follows

## [Unreleased]

## [0.19.0] - 2026-08-27

Ask for a thing io can do, in your own words, and have it happen.

**Five skills ship with io and answer the asks nobody should have to learn a
command for.** "Stop asking me before every write in this repository" is
`io-permissions`; "add the GitHub MCP server" is `io-mcp`; "point this at a
local model instead" is `io-provider`; "remember that we use pnpm here" is
`io-remember`; "update io" is `io-update`, which checks the published Release
and proposes the exact `install.sh` line rather than replacing the binary behind
you. Every one of them ends in a change you see before it lands — a proposed
edit, shown as a diff, gated by the same policy as anything else.

**They are ordinary files in `~/.io-cli/skills`, beside your own.** `SKILL.md`
bodies and nothing more clever than that: open one, edit it, fork it, delete it.
The model is offered each skill's name and description on every turn and reads
the body through io-harness's own `read_skill` tool, under this session's
policy. An upgrade brings forward the ones nobody has touched, leaves a file you
edited exactly as it is and says so, and never resurrects one you turned off.

**`/skills` is where all of it is visible**, and turning a skill off is moving
its file into `skills/disabled/` — a directory io-harness's discovery walks past
because it holds no `SKILL.md`. `/skills` does the move with a keystroke, it
takes effect on the next turn, and it works on your own skills too. There is no
`enabled` flag anywhere, because a second list disagreeing with the filesystem
is how a product grows two sources of truth.

**`/mcp` and `/provider` moved from the inspect group to the configure group**,
which matters to anyone who finds a command by scanning `/help` rather than by
typing it. Both open with a list and both go on to add, edit, disable and remove
entries in the configuration file, which is what configure means and what
inspect promises it does not do. The commands, their keys and their screens are
unchanged; only where they are listed is. Inspect goes to nine and configure to
six, with `/skills` the twenty-sixth command.

**Two guards you can watch work.** A skill of yours already answering to a
shipped name is never overwritten: io-cli installs four files instead of five
and names the one it withheld and the file that claimed the name — because
io-harness addresses a skill by name and two of one name ends every turn of the
session before the first completion. And io-harness accepts at most 64 skills in
a directory, rejecting the whole set rather than trimming it, so io-cli counts
before it writes, stops short of the ceiling and says how many it withheld. An
install that cannot write at all is reported and the session runs anyway.

## [0.18.0] - 2026-08-26

Tell it to remember something, and it is still remembered tomorrow.
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.18.0"
version = "0.19.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
111 changes: 106 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,8 @@ above a row that ranked there for reasons having nothing to do with it.
| `/contain` | run turns contained, so the agent can fan out: on, off, or ask |
| `/plan` | make turns propose a plan before they work: on, off, or ask |
| `/profile` | switch to a named profile from the configuration, for this session |
| `/steer` | send what is queued into the turn that is already running |
| `/compact` | fold this conversation into a summary, at the next step |

**inspect**

Expand All @@ -371,12 +373,9 @@ above a row that ranked there for reasons having nothing to do with it.
| `/help` | this table |
| `/status` | commit the whole session state into the scrollback |
| `/context` | what is in the model's window, read from the request that carried the turn |
| `/steer` | send what is queued into the turn that is already running |
| `/compact` | fold this conversation into a summary, at the next step |
| `/expand` | commit the last step's full detail into the scrollback |
| `/fleet` | show the children this turn has spawned |
| `/mcp` | the MCP servers configured, and what this session has seen of each |
| `/provider` | the providers configured, in the order a turn tries them |
| `/skills` | every skill, shipped or yours: what it is for, whether it is on, and its file |
| `/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 |
Expand All @@ -389,6 +388,16 @@ above a row that ranked there for reasons having nothing to do with it.
| `/theme` | change the theme for this session |
| `/remember` | remember a line of guidance, in the scope you choose |
| `/memory` | what io remembers: the instruction files, and the agent's own notes |
| `/mcp` | the MCP servers configured, and what this session has seen of each |
| `/provider` | the providers configured, in the order a turn tries them |

`/mcp` and `/provider` sit under **configure** from 0.19.0, and it is a
correction rather than a promotion: both open with a list, and both go on from
that list to add, edit, disable and remove entries in the configuration file,
which is the one thing **inspect** promises it never does. `/steer` and
`/compact` are listed under **this turn**, which is the group the code has
always filed them in — the table above said otherwise until this release, and
nothing but a reader was misled by it.

`/usage` answers what `/status` answers and is deliberately not listed above: an
alias earns no row of its own, because a second row for one screen reads as a
Expand Down Expand Up @@ -445,6 +454,88 @@ refuse a large payload without saying so.
`/theme` and `/model` change this session only and say so. Making a choice
permanent is `io setup`.

## Skills

**Five of the things io can do have a plain-language door.** Say what you want
in your own words — "stop asking me before every write in this repository", "add
the GitHub MCP server", "point this at a local model instead", "remember that we
use pnpm here", "update io" — and the model reaches for the skill that answers
it, instead of you reaching for the command that does.

| Skill | For |
| --- | --- |
| `io-permissions` | changing what io asks about before it acts |
| `io-mcp` | adding, changing or removing an MCP server |
| `io-provider` | switching provider, or pointing the session at a local model |
| `io-remember` | writing something down where the next session reads it |
| `io-update` | finding out whether a newer io has been released, and proposing the installer line for it |

**They are files, and nothing more clever than that.** Five ordinary `SKILL.md`
bodies written into `~/.io-cli/skills`, beside whatever skills you keep there
yourself. Open one, read it, edit it, copy it into a skill of your own, delete
it — the same things you would do to any other markdown file in a directory you
own. There is no registry, no index and no remote source; the five are carried
in the binary and written out the first time io has a home to put them in.

**Delete one and it stays deleted.** `rm ~/.io-cli/skills/io-mcp.md` is the way
to be rid of a shipped skill for good: io remembers that it wrote that name, so
it does not put the file back on the next start. A skill added in a *later*
version has no such record, so upgrading still brings you the new ones. If you
only want one out of the way for now, turn it off instead — that is reversible
and `/skills` does it for you.

**Each of them ends in a change you see before it lands.** A skill instructs the
model in what io can already do and which surface does it, so what comes back is
a proposed edit to `io.toml`, or to a memory file, or a command to run — shown
as a diff or as a command, gated by exactly the policy everything else is gated
by, and refusable. A skill can no more move the permission boundary behind your
back than the agent can, because moving it *is* a write, and a write is a thing
you approve.

**The model is offered a name and a description, and reads the rest only when it
matters.** Every turn's system prompt carries the catalogue — five names and
five short descriptions — and the body of a skill reaches the model through
io-harness's own `read_skill` tool, under this session's policy, like any other
read. So a skill costs the prompt one line until it is relevant, and it is
subject to the same boundary as everything else in the session.

**An upgrade refreshes what nobody touched and leaves the rest alone.** io-cli
records the bytes it last wrote for each shipped skill; a file that still
matches gets the new text, and a file you have edited is kept exactly as it is
and named on screen as kept. A skill with no record at all is treated as yours.
The bias is deliberate: there is no restore point behind these files, so the
failure mode of a lost or unreadable record is that io-cli stops refreshing,
never that it writes over something you wrote.

**Turning a skill off is moving its file into `~/.io-cli/skills/disabled/`.**
io-harness admits a subdirectory only when it holds a `SKILL.md`, so a folder of
loose `.md` files is invisible to discovery, to the catalogue and to
`read_skill` — which makes a directory the whole mechanism, with no second list
to disagree with the filesystem. It works on your own skills too, and it
survives an upgrade: a shipped skill sitting in `disabled/` is not written back
into `skills/` on the next launch, because a switch that turns itself on again
every morning is not a switch. `/skills` does the move for you and shows, for
every skill in both directories, what it is for, whether it came from io-cli or
from you, whether it is on, and the file it lives in.

**Two names resolving to one skill is fatal, which is why io-cli withholds
rather than overwrites.** io-harness addresses a skill by name, and a directory
holding two of the same name fails discovery outright — not as a listing quirk,
but as an error raised at the start of a run, so *every turn of that session*
dies before the first completion. The resolved name is the `name:` in a file's
frontmatter where there is one, not the filename, so a file of yours called
anything at all can claim `io-mcp`. io-cli therefore reads the directory before
it writes to it, and never installs a shipped skill over a name your own files
already claim: it installs four instead of five, and says which one it withheld
and which file claimed the name. Rename yours, or leave it — the choice stays
with the file you wrote.

**And there is a ceiling: io-harness accepts at most 64 skills in a directory.**
It rejects the whole set rather than trimming it, so an operator sitting near
the limit who gains five more would otherwise get no skills at all as their
upgrade. io-cli counts first, installs up to the ceiling and stops, and says how
many it installed and how many it withheld.

## The fleet

An agent can break a task into sub-agents and run them over the same workspace.
Expand Down Expand Up @@ -774,6 +865,14 @@ and the skills directory, which is `~/.io-cli/skills` when `skills` names none.
`/remember` writes when you pick that scope. That is one directory to copy to a
new machine, and one path to put in a bug report.

Two more paths arrive with the shipped skills. `~/.io-cli/skills/disabled/` holds
the ones that are turned off, which is a directory rather than a setting — see
[Skills](#skills). And `~/.io-cli/.skills-manifest` is where io-cli records the
bytes it last wrote for each shipped skill, so an upgrade can tell an untouched
file from one you edited. It sits in the home and deliberately *not* in the
skills directory, because every markdown file in there is offered to the model
and a state file is not a skill.

The file is found in this order, which is io-harness's and is unchanged:
`$IO_CONFIG`, else `$IO_CONFIG_HOME/io.toml`, else `$XDG_CONFIG_HOME/io/io.toml`
or `~/.config/io/io.toml`, and `%APPDATA%\io\io.toml` on Windows. What 0.15.0
Expand Down Expand Up @@ -917,7 +1016,9 @@ nothing reaches this program to draw.
into prompt text, so nothing but this program is involved; a skill is read by the
*model*, through a tool, under the run's own policy. Choosing one from the
palette puts `use the <name> skill: ` in your prompt and stops there. io-cli
parses no skill file and keeps no copy of one.
parses no skill file: the five it ships from 0.19.0 are bodies it carries and
writes to disk, and after that they are read the same way yours are — by the
model, through the tool, under the policy.

**`io exec` runs one goal and stops, and a run that pauses stays paused.** An
agent that asks a question about what you meant, or proposes a plan, ends the run
Expand Down
62 changes: 62 additions & 0 deletions skills/io-mcp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
name: io-mcp
description: Add, change or remove an MCP server for io by proposing an edit to the [[mcp]] array in io.toml, in the scope that suits it.
---

Use this when the operator wants a tool server: "add the GitHub MCP server",
"point io at our internal docs server", "drop the one that keeps failing",
"why is the issues server not answering".

## What one looks like

`[[mcp]]` is an array of tables. A stdio server is a program io launches and
talks to over a pipe; an HTTP server is a URL it dials.

```toml
[[mcp]]
id = "docs"
transport = "stdio"
command = "mcp-server-docs"
args = []

[[mcp]]
id = "issues"
transport = "http"
url = "https://mcp.example.com/v1"
```

`id` is how everything else refers to the server, so it has to be unique. There
is a second array, `[[app.io-cli.mcp]]`, which is merged with the top-level one
and wins a collision of ids — use it only when the operator wants a server for
the terminal session and not for `io exec`, and say so if you do.

## Which file

The user file `~/.io-cli/io.toml` for a server that belongs to this machine; the
project's `io.toml` for one the whole team should have. **A credential never goes
into a committed file**: write `${env:GITHUB_TOKEN}` and let the environment
carry the secret. io-harness substitutes `${env:…}` and `${file:…}` and nothing
else.

## The surface

`/mcp` shows what is configured, which servers this session actually reached, how
many distinct tools each answered with, and the last failure. A server the
session has not tried yet says so rather than showing as broken — worth checking
before proposing to remove one. The same surface adds, edits and removes entries,
so the operator may prefer to be sent there.

A server's tools reach a turn through io-harness. Its network traffic is the
server's own and is not what the `net` rule in `[policy.defaults]` governs, so do
not tell the operator that a policy change will contain it.

## What to do

Name the server, the transport, the id, and the file you would put it in. Then
either send the operator to `/mcp`, or propose the edit — writing `io.toml` is an
ordinary write, shown as a diff and gated by the session's policy, and it is the
operator who approves it.

The server is not configured because you described it. Say what you are
proposing; do not say it is connected, and do not say it works until `/mcp` shows
it answering.
Loading