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
13 changes: 10 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,17 @@ the only dev-dependency. Adding one is a deliberate, argued act — read the com
`git clone` for a marketplace) and nowhere else.
- **TOML parsing** is permitted in `src/edit.rs` alone. Every other module that needs a value out
of a configuration file goes through `edit::value_at`, `edit::sections` or `edit::array`.
`src/marketplace.rs` reads a stranger's `plugin.toml` that way.
`src/marketplace.rs` reads a stranger's `plugin.toml` that way, and `src/adapt.rs` *writes* one
without ever parsing it.
- **JSON deserialization** is permitted in `src/import.rs` and `src/adapt.rs`, and nowhere else.
The rule is over the parse rather than over the crate name: `src/exec.rs` serializes its own
`--json` event lines and is not a second opinion about anybody's file. The set is compared with
`==` and has its own near-miss test, because a widening makes a gate more permissive and so goes
vacuous without going red.
- **No agent loop.** The sweep strips comments first, so a doc comment containing "while" is not
a finding; `src/provider.rs` is exempted by path and held to four properties of its own.

**Module map (`src/`, 74 files).**
**Module map (`src/`, 75 files).**

- Session surface — `app.rs` (state), `term.rs` (an inline viewport, never the alternate
screen), `events.rs` + `triage.rs` (what each io-harness event does; a kind with no
Expand All @@ -81,7 +87,8 @@ the only dev-dependency. Adding one is a deliberate, argued act — read the com
`provider.rs`.
- Store — `store.rs`, `export.rs`, `undo.rs`, `rewind.rs`, `sessions.rs`.
- Bundles — `skills.rs`, `skillview.rs`, `plugin`-facing `pluginview`, `marketplace.rs`,
`fetch.rs`, `import.rs`.
`fetch.rs`, `import.rs`, `adapt.rs` (the three manifest formats io did not invent, read; and the
`plugin.toml` io generates for one, written and never parsed).
- Media and git — `picture.rs`, `attach.rs`, `repo.rs`, `commit.rs`.

**Nothing under `tests/` links `src/main.rs`.** A guard written in the driver cannot be tested
Expand Down
76 changes: 75 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,79 @@ All notable changes to this project are documented here. The format follows

## [Unreleased]

## [0.31.0] - 2026-08-30

io-cli has operated a marketplace nobody could stock. Every capability bundle
published in the field is a Claude Code plugin or a Codex plugin, and
`plugin.toml` is a format only this project writes — so the surface 0.29.0 and
0.30.0 built, disclosed and gated was reachable only by a bundle you had written
yourself, which is the one case that never needed a marketplace. A survey of the
marketplaces on the author's own machine found five repositories publishing 304
plugins between them and zero `.toml` files among them.

From this release io reads the three formats a bundle is actually published in.
The bet is that a plugin's value is in its skills, its templates and its agents
rather than in the syntax of its manifest, and that a tool which reads three
formats is better placed than one that asks 291 strangers to adopt a fourth.

### Added

**`.claude-plugin/marketplace.json` is read as a repository's index**, and
`.claude-plugin/plugin.json` and `.codex-plugin/plugin.json` as bundle manifests
wherever the existing walk already looks. The precedence is stated: a
`plugin.toml` at a repository's root suppresses the index and nothing else; where
there is no root manifest the index is the answer and the walk does not also run,
because a union would list bundles the author never published beside the ones
they did; where there is neither, the walk reads each directory natively first and
foreign second. An entry in a shape io does not read is listed with its reason
rather than dropped.

**An index may place a plugin in another repository**, which 238 of the official
marketplace's 291 entries do, and installing one fetches it at the commit the
index names. `git clone --revision` is not available below git 2.49, so a
commit-pinned shallow fetch is four invocations rather than one.

**An adapter manifest is generated** under
`~/.io-cli/adapters/<owner>/<repo>/<name>/plugin.toml`, with absolute paths into
the clone, so io-harness loads the bundle with no change to that crate. The
stranger's checkout is never written to. `/plugin` draws an adapted bundle under
its own mark with the generated manifest's directory on the row, so the
difference between what an author wrote and what io generated is not something
you have to infer, and the file to open when io-harness drops a bundle is named.

### Changed

**Skills follow `$IO_CONFIG` and `$IO_CONFIG_HOME`, on both the read and the
write.** They resolved through io-cli's default home while your memory file
resolved through the home actually in force, so pointing either variable
elsewhere moved one and not the other. A skill is something you wrote, and it
belongs where the rest of what you wrote is. The limitation this removes was
recorded in `docs/CONTRACT.md` since 0.30.1.

**io does not move skills you already have**, and it says so rather than leaving
you to notice. If you have set either variable and your old `~/.io-cli/skills`
still holds anything, io names both directories at startup — a release that moved
only the read would have had `/skills add` writing where nothing looks.

### Not included, deliberately

**A Claude Code or Codex plugin's hooks are not carried across.** io-harness's
hooks are argv against its own event tags and deliberately never a shell string,
and it refuses `${env:}`, `${file:}` and `${cmd:}` inside a manifest in every
scope — so a hook in either foreign format is a shell line, an unknown event and a
refused substitution at once. No adapter closes that, and an approximated hook is
a program running on your machine that nobody described accurately.

So every hook a bundle declares is **shown** before you install it, with its
event, its command unshortened, and the reason it will not run. If you want hooks
under io, the repository's author adds a `plugin.toml`, which then wins its own
root.

io also does not become compatible with another tool's runtime — the variables it
sets, the directory it runs a hook in, its permission model or its session
lifecycle. io reads what a manifest says and nothing about how another program
would have executed it.

## [0.30.2] - 2026-08-30

A documentation release. Thirty-one claims across the README, the changelog, the
Expand Down Expand Up @@ -2667,7 +2740,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.30.2...HEAD
[Unreleased]: https://github.com/initorigin/io-cli/compare/v0.31.0...HEAD
[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
[0.30.0]: https://github.com/initorigin/io-cli/compare/v0.29.0...v0.30.0
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.30.2"
version = "0.31.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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ the limits that capability actually has.
| [When a run stops for you](docs/guide/resume.md) | A question, a plan or an interrupted call answered where it was left — and one `io` at a time on one conversation |
| [What it costs](docs/guide/accounting.md) | The money and the token split, where a price comes from, and why an unpriced model makes a total a floor |
| [Skills](docs/guide/skills.md) | The five shipped, yours, and what a duplicate name costs |
| [Capability bundles](docs/guide/plugins.md) | A directory contributing to six subsystems, marketplaces, and what a bundle is allowed to do being shown before it may do it |
| [Capability bundles](docs/guide/plugins.md) | A directory contributing to six subsystems, marketplaces, installing a Claude Code or Codex plugin, and what a bundle is allowed to do being shown before it may do it |
| [Hooks](docs/guide/hooks.md) | Reacting to a run from `io.toml`, and the failure that is quiet |
| [The fleet](docs/guide/fleet.md) | Contained turns, the tree `Ctrl+F` shows, and the plan a contained turn proposes |
| [Verification gates](docs/guide/verification.md) | The criterion, what it costs per step, and exit `6` |
Expand Down
2 changes: 1 addition & 1 deletion docs/CAPABILITIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ the limits that capability actually has.
| [When a run stops for you](guide/resume.md) | A question, a plan or an interrupted call decided where it was left; the turn an interrupt makes unresumable; and one `io` at a time on one conversation |
| [What it costs](guide/accounting.md) | Money and the token split by run, session, model and day; where a price comes from; and why an unpriced model makes a total a floor |
| [Skills](guide/skills.md) | The five this crate ships, adding and removing your own, the claimed name being the frontmatter name, and what a duplicate costs |
| [Capability bundles](guide/plugins.md) | A directory contributing to six subsystems at once, marketplaces, and the disclosure that happens with your configuration file untouched |
| [Capability bundles](guide/plugins.md) | A directory contributing to six subsystems at once, marketplaces, installing a Claude Code or Codex plugin, and the disclosure that happens with your configuration file untouched |
| [Hooks](guide/hooks.md) | An audit log, a notification, a formatter or a check that stops the run, declared in `io.toml` — and the failure that is quiet |
| [The fleet](guide/fleet.md) | Contained turns spawning children under one shared ceiling, the live tree, and the plan a contained turn proposes before it acts |
| [Verification gates](guide/verification.md) | The criterion, the fact that it is evaluated after every step rather than once per turn, per-gate retry, and exit `6` |
Expand Down
20 changes: 14 additions & 6 deletions docs/CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,9 @@ One directory: `~/.io-cli` on Unix, `%USERPROFILE%\.io-cli` on Windows, created
| --- | --- |
| `io.toml` | The user-scope configuration |
| `runs.db` (+ `-wal`, `-shm`) | The session store |
| `skills/` | Skills, with `skills/disabled/` for the ones switched off |
| `marketplaces/` | Cloned marketplaces |
| `skills/` | Skills, with `skills/disabled/` for the ones switched off. Read from and written to the home in force, so `$IO_CONFIG`/`$IO_CONFIG_HOME` moves it with `io.toml` and `IO.md`. io does not move files you already own; it says so at startup if the old directory still holds any |
| `marketplaces/` | Cloned marketplaces, with `marketplaces/.entries/` for a repository an index pointed at |
| `adapters/<owner>/<repo>/<name>/` | The `plugin.toml` io generates for a Claude Code or Codex bundle |
| `.fetching/` | Staging for a clone in flight, renamed into place on success |
| `IO.md` | The user-scope memory file |
| `.import-offered` | Marks that the import offer was made, so declining is remembered |
Expand All @@ -116,10 +117,17 @@ deleted that was not already copied. This does nothing at all when `$IO_CONFIG`

## Limits that hold today

**`$IO_CONFIG_HOME` does not move the skills directory.** The skills surfaces resolve through
io-cli's *default* home (`home::path()`) where the memory file resolves through the home actually
in force (`home::in_force()`), so an operator who points `$IO_CONFIG_HOME` elsewhere gets their
memory file there and their skills in `~/.io-cli/skills/`. Known since 0.30.1.
**A marketplace, and an entry inside one, resolve against `github.com` and no other host.** A
marketplace is named `<owner>/<repo>`; an index entry naming a repository elsewhere has its url
re-derived to that shape, and a url that is not two ordinary path segments on that host is refused
and listed with its reason. The only string io hands `git` is one io built — a url somebody else
wrote reaching a clone is how `ext::sh -c …` becomes a remote shell. All 238 remote entries of
`anthropics/claude-plugins-official` are on that host.

**A Claude Code or Codex plugin's hooks are read, shown, and translated into nothing.**
io-harness's hooks are argv against its own event tags and never a shell string, and it refuses
`${env:}`, `${file:}` and `${cmd:}` inside a manifest in every scope. This is an impossibility, not
a deferral: the author adds a `plugin.toml` if they want hooks under io.

**A turn the operator interrupted cannot be resumed.** It is reported as ended, with `/fork`
offered instead.
Expand Down
5 changes: 4 additions & 1 deletion docs/guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ Nine keys live there, and eight tables:
| `diff` | `unified` — the default, and what an absent key means — or `minimal`, the changed lines and the `@@` header without the context, for reviewing by file rather than by hunk. |
| `glyphs` | `unicode` or `ascii`. Absent asks the locale. |
| `plain` | `true` runs every session in plain mode. The same switch as `--plain`, which wins over it. |
| `skills` | a directory of skills for the agent. They appear in the `/` palette by name, and the agent reads them itself. Absent, it is `~/.io-cli/skills`. A leading `~` is your home directory — io-cli expands it before io-harness sees the path, because io-harness substitutes `${env:…}`, `${file:…}` and `${cmd:…}`, and a tilde is none of the three. |
| `skills` | a directory of skills for the agent. They appear in the `/` palette by name, and the agent reads them itself. Absent, it is `skills/` under the home in force — `~/.io-cli/skills` unless `$IO_CONFIG` or `$IO_CONFIG_HOME` moved it. A leading `~` is your home directory — io-cli expands it before io-harness sees the path, because io-harness substitutes `${env:…}`, `${file:…}` and `${cmd:…}`, and a tilde is none of the three. |
| `max_parallel_reads` | how many read-only tool calls one turn may run at once. Absent, it is io-harness's own 10; `0` is clamped to 1 rather than meaning none. A `TaskContract` field with no io-harness configuration key of its own, which is why it is named here. |
| `spawn_background_after_secs` | how long a spawned child may run before it is backgrounded. Absent, a child is waited for however long it takes. |
| `detached_spawns` | whether a spawn may detach at all. Absent, it may. `false` buys a trace with every child's whole life in it, which a detached child gives up. |
Expand Down Expand Up @@ -289,6 +289,9 @@ configuration file is in it, and so is the run store `runs.db` with the `-wal`
and `-shm` SQLite keeps beside it — which is where the agent's durable memory
lives too, because that is rows inside the store rather than a file of its own —
and the skills directory, which is `~/.io-cli/skills` when `skills` names none.
That directory is the `skills/` of the home in force — the same home your
`io.toml` and `IO.md` are in — so `$IO_CONFIG` or `$IO_CONFIG_HOME` moves all
three together, and io reads and writes skills in whichever one is in force.
`~/.io-cli/IO.md` is in it as well: the guidance you want in every project, which
`/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 — as long as you have named no
Expand Down
65 changes: 64 additions & 1 deletion docs/guide/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ the hand-written reader could not: an inline `hook = [{…}]` array, and a
## Marketplaces

**A marketplace is a git repository you name.** It is cloned into your own home
and walked for directories carrying a `plugin.toml`:
and read for the bundles it publishes:

```
/plugin marketplace add zeroonething/ultraship
Expand Down Expand Up @@ -123,6 +123,69 @@ Installing is the verb you already had:
spellings. They are two strangers' repositories, and installing whichever the
walk reached first is installing code you did not choose.

### Three manifest formats, and which one wins

A capability bundle in the field is a Claude Code plugin or a Codex plugin.
`plugin.toml` is a format this project writes and nobody else does, so io reads
all three rather than asking anyone to adopt a fourth:

| File | Read as |
| --- | --- |
| `plugin.toml` | A bundle, natively |
| `.claude-plugin/marketplace.json` | The repository's own index of what it publishes |
| `.claude-plugin/plugin.json`, `.codex-plugin/plugin.json` | A bundle manifest |

The precedence is stated rather than discovered:

- **A `plugin.toml` at the repository's root suppresses the index.** An author who
writes io's own manifest has said what they publish in the format io owns, and a
foreign index does not speak over it. It suppresses the index and nothing else —
a repository with a root manifest and bundles beneath it still lists all of them.
- **Where there is no root `plugin.toml`, the index is the answer** and the
directory walk does not also run. A union would list bundles the author did not
publish beside the ones they did, and you would have no way to tell which was
which.
- **Where there is neither, the walk runs**, reading each directory as a
`plugin.toml` first and a foreign manifest only where it carries none.

An entry in a shape io does not read is **listed with its reason**, never dropped.

**An index may place a plugin in another repository**, and 238 of the 291 entries
in `anthropics/claude-plugins-official` do. Those are fetched when you install
one, at the commit the index names where it names one. io re-derives the
repository from the url and rebuilds it: a url that is not `<owner>/<repo>` on
GitHub is refused, because the only string io hands `git` is one io built.

**An adapted bundle is marked as adapted in `/plugin`**, under its own mark, with
the generated manifest's directory on the row — that file is what you open when
io-harness drops the bundle, and nothing else names it. io writes it under
`~/.io-cli/adapters/<owner>/<repo>/<name>/plugin.toml`, never inside the clone.
The paths in it are absolute and point into the clone, so io-harness loads it as
an ordinary bundle. The author's checkout is not written to.

**A plugin name that is not a usable id is refused rather than mangled.** An id is
what you type at `plugin add` and what prefixes every name the bundle
contributes, so io accepts a name that is already an id, folds one that becomes an
id by lowercasing, and refuses everything else. Two entries in one index reaching
the same id are refused naming both.

### Hooks do not cross, and that is not a gap

**A Claude Code or Codex plugin's hooks are not carried across.** io-harness's
hooks are argv against its own events, deliberately never a shell string; a hook
in those formats is a shell line, an event io does not know, and a `${…}`
substitution io-harness refuses inside a manifest in every scope. All three at
once, and no adapter closes any of them.

So io **shows** them instead. Every hook a bundle declares is drawn before you
install it, with its event, its command **unshortened**, and the reason it will
not run. An approximated hook is a program running on your machine that nobody
described accurately, and a shortened one is a command you consented to without
reading.

If you want hooks under io, the repository's author adds a `plugin.toml` — one
file, which then wins its own root.

**Removing a marketplace removes the clone and nothing else.** A bundle you
declared out of it keeps its `[[plugin]]` entry — a cache being emptied is not a
reason to undo a decision you made about your configuration. What io owes you
Expand Down
Loading