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
18 changes: 16 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ the only dev-dependency. Adding one is a deliberate, argued act — read the com
- **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/`, 75 files).**
**Module map (`src/`, 79 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 @@ -89,7 +89,8 @@ the only dev-dependency. Adding one is a deliberate, argued act — read the com
- 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`, `adapt.rs` (the three manifest formats io did not invent, read; and the
`plugin.toml` io generates for one, written and never parsed).
`plugin.toml` io generates for one, written and never parsed), `bundle_path.rs` (where a
declared `[[bin]]` goes, appended and creating no file — io-harness places nothing itself).
- 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 Expand Up @@ -130,6 +131,19 @@ tense, no diary (history goes in `CHANGELOG.md`; a version number in a sentence
not a story). Name the reason for a non-obvious decision where the decision is, once. No first
person, no "powerful"/"robust"/"simply".

**A bundle's name is translated where it is drawn, never renamed.**
`io_harness::NAMESPACE` is `__` and it is load-bearing on the wire — the system prompt, the tool
dispatch and every event `target` carry it — so `naming::display` is applied at the moment a name
reaches a person and `naming::wire` to a name they typed. `tests/namespacing.rs` walks the drawn
output of six operator-facing surfaces (the transcript, the status line, the pickers, and the
plugin, skill and marketplace panes) and fails when one of them draws a name still in the wire
spelling, so a new surface that draws a contributed name inherits the rule instead of rediscovering
it. **Not "the separator appears nowhere"** — that is a different and wrong property, and the
release that set this rule shipped the defect proving it. Translate the qualifier only: a path is
not a name, and `read src/__init__.py` drawn through `display` is a file that does not exist; and
only the FIRST separator is the join, so `bundle__deep__nested` is drawn `bundle:deep__nested` and
keeps one. A gate spelled as the blanket rule reads `display` written with `replace` as a fix.

**Comments carry the argument.** This codebase keeps long "why" comments — `Cargo.toml`'s
dependency notes, `tests/dependencies.rs`'s exemption rationale. Match that density; do not strip
them. A gate that reads prose can forbid a file from explaining itself: `tests/dependencies.rs`
Expand Down
101 changes: 100 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,104 @@ All notable changes to this project are documented here. The format follows

## [Unreleased]

## [0.34.0] - 2026-08-31

0.32.0 made io-harness's `__` a wire detail: a bundle's contribution is drawn with
a colon and translated back at the keyboard. It left three places where the
separator still reached a person — the sentence io-harness writes about a skill
read, the repetition guard that stopped recognising that sentence, and every MCP
tool call, drawn as the raw `mcp__github__create_issue`. This release closes all
three and puts a gate under the rule, so the next surface to draw a contributed
name cannot reintroduce the underscore quietly.

The pin moves to io-harness 0.73.0, which adds a seventh kind of contribution. A
bundle can ship an executable, and until now it contributed it to nothing: a model
could not tell *not installed* from *installed somewhere I may not look*. io-cli
makes it resolvable by appending the file's own directory to the run's `PATH` —
appended, so nothing a bundle ships can shadow a system command — and creates no
file to do it.

### Added

**A bundle's `[[bin]]` is invocable inside a run.** io-harness 0.73.0 accepts the
key, checks that the path stays inside the bundle, and places nothing itself; io
appends the directory holding the declared file to its own `PATH`, which every
command a run spawns inherits. Appended and never prepended: a prepended directory
lets anything a bundle ships answer to `git`, `cargo` or `ls` for every tool call
in the session, and the permission gate that would stop it matches a binary
*name*, which the wrong program under the right name satisfies. One entry per
directory, sorted, and none from a bundle switched off. No file is written and
nothing is linked, so the program resolves under the name it already has on disk —
a `[[bin]]` whose `name` is not that name is named at startup and does not
resolve, rather than being papered over with a wrapper io would then own inside
somebody else's directory. A bundle declared in the committed `io.toml` that
carries one is refused whole, exactly as one carrying a `[[hook]]` or an `[[mcp]]`
is. A bundle installed mid-session has its program placed at the next turn, with
everything else it contributes; on Windows a declared `review` shipped as
`review.exe` is correctly authored and is not reported, because `PATHEXT` is what
decides there.

**A skill can read the files beside it without a shell.** `read_skill` takes an
optional `path` on io-harness 0.73.0, so a bundle's `shared/*.md` and
`references/*` reach the model through the same tool and the same policy as the
skill body. Such a call is drawn with its path intact and untranslated: the
qualifier takes the colon and the path is left exactly as it was asked for.

**`tests/namespacing.rs` walks six operator-facing surfaces** — the transcript,
the status line, the pickers, and the plugin, skill and marketplace panes — and
fails when one of them draws a contributed name still in its wire spelling. Not
"the separator appears nowhere": a path carries one legitimately, and only the
first separator is the join, so `bundle__deep__nested` is drawn
`bundle:deep__nested` and keeps one. The rule had three exceptions and no gate,
which is how it had three exceptions.

### Changed

**A skill read is drawn as a skill loaded**, `Skill ultraship:plan · loaded`, and
io-harness's own decision sentence is not drawn for that tool. That sentence names
the skill on the wire, and the row above it already says what the call did — so
this removes the separator at its source rather than translating a sentence
io-cli did not write. A read that fails or is refused still says so, in
io-harness's words.

**An MCP tool call reads `Call github:create_issue`.** Its wire name is assembled
from a prefix, the server id and io-harness's separator, so
`mcp__github__create_issue` was a string nobody wrote and nobody reads.

**Invoking a bundle's skill echoes what you typed.** `/ultraship:brainstorm …`
goes into the scrollback as you wrote it, colon and all, while the prompt behind
it carries `ultraship__brainstorm` — the string the model's catalogue holds and
the only one `read_skill` resolves. The two differ deliberately: a transcript that
rewrote the line you typed would be the only place in the session that does.

**A bundle contributes seven kinds of thing, not six.** `Plugin::contributions()`
reports `bin` after `hooks` and before `policy`, and the pane, the guide and the
README say seven.

### Fixed

**The repetition guard stopped recognising io-harness's decision sentence.** A
pending call kept only the displayed target, so `trim_result` compared a sentence
naming the wire name against a call naming the colon form and printed both. The
pending call now keeps io-harness's own target beside the drawn one, which repairs
it at the cause rather than by matching two spellings at the comparison.

### Dependencies

- io-harness 0.72 → **0.73.0**. `Plugin::contributions()` gains `bin`, a manifest
gains `[[bin]]`, and `read_skill` gains an optional `path`. The direct dependency
set is unchanged at ten names.

### Upgrading

- **A `plugin.toml` carrying a `[[bin]]` cannot be read by an io-cli built against
io-harness 0.72.0 or earlier.** `Manifest` is `deny_unknown_fields`, so that
binary refuses the manifest **whole** and drops the bundle entirely — every
skill, agent, hook and layer in it, not just the `[[bin]]`. This matters where
two versions of io read one home: once io has written a `[[bin]]` into a manifest
it generates under `~/.io-cli`, the older binary sees the bundle as gone rather
than as shortened. Remove the `[[bin]]` tables before downgrading.

## [0.33.0] - 2026-08-31

io-harness 0.72.0 lets an agent ask several things at once and lets a choice carry
Expand Down Expand Up @@ -2977,7 +3075,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.33.0...HEAD
[Unreleased]: https://github.com/initorigin/io-cli/compare/v0.34.0...HEAD
[0.34.0]: https://github.com/initorigin/io-cli/compare/v0.33.0...v0.34.0
[0.33.0]: https://github.com/initorigin/io-cli/compare/v0.32.0...v0.33.0
[0.32.0]: https://github.com/initorigin/io-cli/compare/v0.31.0...v0.32.0
[0.31.0]: https://github.com/initorigin/io-cli/compare/v0.30.2...v0.31.0
Expand Down
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "io-cli"
version = "0.33.0"
version = "0.34.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 Expand Up @@ -63,7 +63,7 @@ path = "src/lib.rs"
# still correct about what this *renderer* can be handed; it is simply no longer
# load-bearing on the build. Turning `barcode` back off is what would restore
# it, and `documents` would then have to be spelled out as its other four.
io-harness = { version = "0.72", features = ["media", "browser", "documents"] }
io-harness = { version = "0.73", features = ["media", "browser", "documents"] }
# **`tui-textarea` is gone as of 0.18.0, and it is why this pair was frozen.**
# The comment that stood here said ratatui stayed at 0.29 and crossterm at 0.28
# because the composer was built out of that widget and the 0.30 line carried
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,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, installing a Claude Code or Codex plugin, 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 seven subsystems, the executable one of them ships and where a run finds it, 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, installing a Claude Code or Codex plugin, and the disclosure that happens with your configuration file untouched |
| [Capability bundles](guide/plugins.md) | A directory contributing to seven subsystems at once, marketplaces, installing a Claude Code or Codex plugin, the executable a bundle ships, 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
14 changes: 13 additions & 1 deletion docs/CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,19 @@ together.
**A bundle's contributions are displayed with a colon and addressed with io-harness's separator.**
`ultraship:brainstorm` is what you read; `ultraship__brainstorm` is what the model was shown and
what `read_skill` resolves. `io exec`, `io plugin` and `io skill` report the wire name, because a
script addresses the wire.
script addresses the wire. From 0.34.0 the separator reaches no operator-facing surface at all —
including an MCP tool call, drawn `Call github:create_issue` — and a gate walks six of those
surfaces and fails on `io_harness::NAMESPACE` appearing in any of them. Invoking a bundle's skill
as a command is the one place the two forms are side by side on purpose: the scrollback echoes the
line you typed, `/ultraship:brainstorm …`, and the prompt behind it carries `ultraship__brainstorm`.

**A bundle's `[[bin]]` is resolvable inside a run and nowhere else.** io-harness 0.73.0 accepts the
key, resolves the path against the bundle root and places nothing; io-cli **appends** the directory
holding that file to its own process `PATH`, which every command a run spawns inherits. Appended
and never prepended, so nothing a bundle ships can shadow a system command. No file is written and
nothing is linked, so the program resolves under the name it already has on disk — a `[[bin]]`
whose `name` is not that name is reported at startup and does not resolve. Your shell's `PATH` is
untouched, and a bundle switched off contributes no entry.

## What may change

Expand Down
11 changes: 8 additions & 3 deletions docs/config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -387,9 +387,14 @@ net = "deny"
# exactly the `[[plugin]]` entry below. The clone is a `git` invocation; there
# is no HTTP client here and no network path beside io-harness's.
#
# One directory can contribute six kinds of thing at once — skills, prompt
# templates, [[agent]] definitions, [[mcp]] servers, [[hook]] tables and policy
# layers — which is why there is a surface for it. `/plugin` lists THREE sets:
# One directory can contribute seven kinds of thing at once — skills, prompt
# templates, [[agent]] definitions, [[mcp]] servers, [[hook]] tables, [[bin]]
# executables and policy layers — which is why there is a surface for it. A
# [[bin]] is io-harness 0.73.0's, and io places it by appending the declaring
# file's own directory to the run's PATH: appended so nothing a bundle ships can
# shadow a system command, and no file is written, so the program answers to the
# name it already has rather than to the name the manifest gave it.
# `/plugin` lists THREE sets:
# what loaded and what each bundle contributed; what was declared `enabled =
# false`, under its own mark, with what switching it back on would bring; and
# every bundle that was declared and DROPPED with io-harness's own reason beside
Expand Down
Loading