Skip to content

io-cli 0.24.0 - #62

Merged
aakashpawar1999 merged 17 commits into
mainfrom
develop
Aug 28, 2026
Merged

aakashpawar1999 merged 17 commits into
mainfrom
develop

Conversation

@aakashpawar1999

Copy link
Copy Markdown
Contributor

The release PR for 0.24.0 — a turn proves its work instead of asserting it.

[app.io-cli.gates] takes one criterion of three kinds: a command that must exit a status, a file that must exist and optionally say something, or a rubric a second model answers. It is attached in contract::configured, the half io exec and a session share, so CI is held to the same standard as the terminal. The verdict reaches the status line and the scrollback with what the command printed; a failing gate buys a follow-up turn carrying the failure text; and io exec gains exit 6.

Until this release every contract io-cli built carried Verification::None — which is why RunOutcome::Success was unreachable from this interface, and finished only ever meant the agent stopped.

/mcp gains an edit verb, closing a limitation stated in the record since 0.21.0.

Verified on the merged tree

  • cargo test --all-targets — 86 binaries, 1,255 passed, 0 failed, 28 ignored
  • Live against a real model — 28 passed, 0 failed, 236.65s
  • clippy -D warnings, cargo fmt --check, nightly rustdoc — all clean
  • 8/8 CI checks green on macOS, Linux and Windows

Three adversarial reviews found four HIGH defects behind a fully green suite — including one that would have made io exec exit 6 for every operator who never configured a gate. All fixed; the record carries the detail.

The sealed record rides this merge, pinned in .ultraship/releases.lock.

src/gates.rs holds what "done" means for a repository: the
[app.io-cli.gates] section as written, the one criterion it resolves to,
and the two refusals the file itself cannot express.

The module is gates and not verify — src/verify.rs is already the
wizard's live credential check, and two modules named for the same word
would be a reader trap for the life of the product.
…mmand

Settings::criterion resolves [app.io-cli.gates] to one Criterion or to a
Refusal, and the refusals are the point: a rubric with no reviewer, or a
reviewer that is the model doing the work, is Error::Config at run start
in io-harness, so catching them at the file is the difference between a
config edit and a session that will not start.

The proposed command comes from io_harness::toolchain::detect layered
with Config::toolchain. io-cli holds no marker table and no command
table, and a test sweeps this module's comment-stripped source to keep it
that way.

File existence maps to Verification::None and is answered here through
read_bytes. WorkspaceFileContains reads with unwrap_or_default, so an
empty needle passes on a file nobody wrote; checked_here() exists so a
caller reading verification() alone cannot report an ungated run.
The field, its catalogue keys, and a worked example of all three kinds.

settings::render writes gates: None, following the containment
precedent. A file arriving from the wizard with a gate already in it
would hold back every turn of an operator who never said what done means
here, and a review criterion would spend a second model's completion
doing it.

The example ships commented out, because the docs gate writes that file
to disk and discovers it.
Drawn by fields() and by the hand-built footer alike, absent when no
criterion is configured, and carrying the attempt number only from the
second attempt.

Cleared by start_run as well as forget_run. forget_run is reached by
/resume, /fork, /clear and rewind but not by the operator typing a second
prompt, so without this turn one's verdict is drawn over the whole of
turn two.
Routes gate_phase_failed and gate_output, which src/triage.rs has named
since 0.20.0 as belonging to a gate no session had yet.

A failing review no longer draws in Tone::Refused. That tone's word is
refused, which is the permission boundary's word, and it conflated the
policy declining to run a gate with the work not meeting the bar. Those
need opposite responses, and a test now asserts the two vocabularies stay
disjoint.
In contract::configured, the half io exec and a session share. A gate
applied in the session's half would hold a terminal to a standard and let
CI run to green with nothing checked.

The criterion and its reviewer are decided together: a Review reaching a
contract with no reviewer is Error::Config at run start, so either both
go on or neither does, and a refusal leaves the run ungated with a notice
rather than dead.

Printable wraps a provider so it can satisfy ModelReviewer's Debug bound.
None of io-harness's own provider types implements Debug, which makes its
only shipped Reviewer unusable from any crate that uses them. The Debug
impl prints the vendor name and never the credential.
A wrapper around exec::code rather than an arm inside it. That function's
body is extracted by a test splitting on its exact signature line, and
its table is a statement about RunOutcome, which carries no verification
verdict at all.

Only a gate that Failed earns 6. One that Errored never answered, and
reporting that as the work not holding up would claim a judgement nobody
made.
The 31st command, in Configure because it writes the configuration file.
Its description names all three kinds, because two cost a process and one
is a billed completion on every gated turn.

README gains the gates section, the exit 6 row, the [app.io-cli.gates]
key, and corrections to two claims this release falsifies.
…ure in hand

/gates shows the criterion in force, what the repository's own toolchain
proposes, and the last run's verdict, and writes a criterion to a chosen
scope. A review criterion with no reviewer, or one whose reviewer is the
working model, is refused before the write rather than at run start.

A failing gate composes a follow-up turn carrying the criterion and what
it reported, fed through the queue the driver already has. gate_retry
takes no goal, so a retry that re-sends the original prompt cannot be
written.

The retry budget is accumulated across the chain. Store::gate_attempts
is keyed on the run, and every session turn is its own run, so counting
per run restarts at one on each retry and would retry forever against a
real model.

/mcp gains an edit verb, closing the limitation stated since 0.21.0. The
entry is re-resolved by id before the write, never addressed by the
picker's row index.
tests/provider.rs requires every vendor constructor to be written exactly
once outside the wizard handshake and the /provider panel, so the
interactive and headless entry points cannot drift apart. It refused this
release twice: once for constructing in gates.rs, once for constructing
in provider.rs beside the real site.

A reviewer is a third construction, like the wizard's ping. It never
drives a turn and deliberately runs a different model from the one doing
the work, so merging it into the session's maker would make the second
opinion change model whenever /model did.

src/reviewer.rs is excluded by name like the other two, and held to that
boundary by a new test: it must build a ModelReviewer, may name nothing
that drives a turn, and may be called from the contract and nowhere
else.
GateOutcome is not in scope in this module, so the intra-doc link could
not resolve. Written in plain backticks like every other io-harness item
named in this crate's docs.

Seventh release in eight that this gate has caught something cargo doc,
clippy and the suite all pass over. It is not in ci.yml; it is run by
hand at the seal.
Three arms. A command criterion that passes returns Success rather than
Finished, which is the whole release in one assertion: until now every
contract this crate built carried Verification::None, so Success was
unreachable from this interface.

A criterion that cannot pass is recorded as failed and exits 6, whatever
the run itself reported. And a rubric is judged by a second model, with
EventKind::Reviewed carrying the verdict and a refusal carrying reasons,
because the reasons are the only thing the operator can act on and the
only thing the retry can carry.

The criterion is asserted on the contract before the run as well as after
it. A run that succeeded with no criterion attached would look identical
from the outcome alone, which is the confusion this release ends.
The worst of it: io-harness evaluates the contract's criterion after
every step on which the agent called a tool, and for Verification::None
that evaluation is false — so it records phase=none, Failed on every
ungated run this product has ever driven. Read back naively that says
the gate failed. io exec would have exited 6 for every operator who has
never configured a gate, and every ordinary session turn would have
drawn 'gate failed'. Those rows are now dropped unconditionally.

The retry counted those same rows, so its attempt number was the step
count and may_retry compared nine against a budget of one. The headline
feature was dead for every criterion except bare existence. The driver
now keeps one row per turn.

A parked, denied or cancelled run was given a verdict and retried: the
operator was told '/resume opens it' and then had that run buried under
a fresh billed turn. Only a run that reached its own end is judged.

Configuring any gate silently switched off conversational classification,
so 'hello' started a full agent run and then ran the test suite after
every step of it. Set explicitly where a criterion is attached.

Also: the fold moved to gates.rs because io exec may not reach the
interface; a failing gate no longer borrows the permission boundary's
word; the reviewer refusal no longer formats a ProviderSpec, which holds
the api_key; an unknown gates key is refused rather than silently
disabling the gate; an empty command argv is refused here rather than
erroring at run start on every turn; /gates named the wrong io.toml and
re-judged past turns against today's filesystem; and io exec now says
when a configured section is not gating the run.

The documentation claimed the gate runs once per turn. It runs after
every step, which for a rubric is a billed completion per step. That was
the one disclosure the design existed to make.
… item

criterion_of is public and criterion_for is not, so the intra-doc link
could not resolve under --document-private-items-less rustdoc. Written
in plain backticks.

Second catch by that gate in this release, and it is not in ci.yml.
Sealed during completion, on the feature branch, before the merge and
the tag. The Release URL is deterministic so it is written now; the
artifact byte sizes and the smoke on the published binary are not
knowable yet and belong in the final report.

Suite 86/1,255/0. Live 28/28 against a real model. clippy, fmt and the
nightly rustdoc gate clean. Three adversarial reviews, four HIGH defects
found behind a green suite and fixed.

transition RELEASED did not pin the lock, for the thirty-third release
running; pinned by hand.
drive.py is copied to the repository root from the gitignored evidence
directory to run the live suite, and a git add -A during 0.24.0 caught a
copy that was sitting there. It was removed in the next commit and the
net effect on the tree is nil, but the ignore entry is what stops it
happening a second time.
io-cli 0.24.0 — a turn proves its work instead of asserting it
@aakashpawar1999
aakashpawar1999 merged commit 568939c into main Aug 28, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant