Skip to content

io-cli 0.33.0 - #85

Merged
aakashpawar1999 merged 15 commits into
mainfrom
develop
Aug 30, 2026
Merged

aakashpawar1999 merged 15 commits into
mainfrom
develop

Conversation

@aakashpawar1999

Copy link
Copy Markdown
Contributor

The agent could ask you five questions and you answered them five times. Question carried one question with bare-string choices and the harness blocked inside Responder::answer before a second could be asked, so an interface downstream could only render what reached it — one at a time, with nothing saying what any offer cost.

io-harness 0.72.0 is the upstream that changes it, and this is the release that meets it.

What you get

Several questions arrive as one overlay, answered in place. One question on screen at a time on the identical surface a single question has; deciding one moves to the next undecided one, PgUp/PgDn walk them, and a decided question re-opens with its answer back in the composer. There is no review pane and no new submit key — a pane would be a second rendering of state already on screen, and it would need a submit key that answers nothing, which is the reflexive-Enter hazard the free-text marker rule exists for.

An offer can explain itself. A description draws under its label; a preview unfolds beneath the focused offer in the blockquote this product already uses for somebody else's words, measured through rows::wrapped and growing the viewport to hold it. One is open at a time.

A question that takes several answers is answered with several. The spacebar marks and unmarks; the set is submitted through Question::answer_of, io-harness's own spelling, so two interfaces answering one question produce one text.

A parked batch resumes as the batch it was. And /config runs while a turn is in flight.

Three defects found by running the shipped binary

  • io skill add <dir>/SKILL.md — the dominant skill layout — wrote ~/.io-cli/skills/SKILL.md and the folder guard then refused to remove it forever, about a folder that did not exist.
  • Every skill /import wrote was a folder skill, and folder skills could not be removed or disabled at all. The product shipped a verb that created state its own management surface refused to manage.
  • io plugin add printed "plugin remove <id> takes it back out" and the verb read its argument as a path, so the sentence was false on the door that printed it.

/config mid-turn took removing a write, not guarding one

The iteration that deferred this named one offending row. The bare picker in fact had no report-only rows: ←/→ called cycle_setting, which wrote the scope file on the keystroke with no confirmation — the only unconfirmed write in the product reachable from a bare arrow key. The act moved one descent below prices.as_of, the date it writes; the arrows now open that descent focused on the value in force; cycle_setting is gone. Eleven commands run mid-turn, and the excluded set drops from seven to six.

What the gates found behind a green suite

Every offline gate was green — 1,790 tests, clippy, fmt, doctests — over all of this.

  • A HIGH, found independently by an adversarial review and a sabotage pass. Intent::resumed never read PendingQuestion::questions, so a resumed batch drew one run-on line with no offers: verbatim the failure this release exists to stop, and the failure src/resume.rs cites to argue the field must exist.
  • Three acceptance criteria were gated by nothing. F14 named a call-site count that did not exist; F17 was one empty slice from silent reversion, the widening shape where nothing fails and a gate goes vacuous without going red; N6 named a prose sweep nobody had written.
  • An invariant this crate states in several places was false. src/edit.rs was not the only file parsing TOML, and the gate enforcing it named two spellings out of eight.
  • The planning audit falsified four plan claims before any code, and a citation audit found a comment claiming io-harness substitutes two forms when it substitutes three — a correction already made once, in 0.21.0, in the other file that carries the claim.

Evidence

cargo build --all-targets                    exit 0
cargo fmt --all -- --check                   exit 0
cargo clippy --all-targets -- -D warnings    exit 0
cargo test --all-targets                     1,790 passed, 0 failed, 37 ignored
cargo test --doc                             1 passed
cargo test --test live -- --ignored          37 passed, 0 failed, 245.60s

io-harness pinned to 0.72.0, the newest published, with its breaking changes read against this crate's callers first — including two the upstream changelog omits.

The release record is sealed and pinned before this merge, at sha256:3bf0f74d227a297a2b3f3caa2ad6043be6ad2bd2ae2f357b757aae8b11b34fd3; ultraship validate exits 0 over 97 checks.

Question::choices is Vec<Choice> and Question is non_exhaustive, so the pin
moves with its callers rather than ahead of them.
io skill add <dir>/SKILL.md wrote ~/.io-cli/skills/SKILL.md, and the folder
guard then refused to remove it forever — about a folder that did not exist.
The destination now comes from the resolved name, describe gains the
parent-directory fallback io-harness itself applies, and is_bundle requires the
parent to be the skill's own.

A skill /import wrote as a folder inside io-cli's own skills directory is now
removable and disable-able too; it never was, so the product shipped a verb
creating state its management surface refused to manage.

Three defects the repair would otherwise have introduced are closed with it:
disabled_files now walks one level, so a disabled folder skill does not vanish
from the surface that re-enables it; install's disabled-set check can see a
parked folder; and disabling a loose SKILL.md is refused, because relocate keeps
the name and it would have landed as disabled/SKILL.md — re-offered as a skill
called disabled, taking every other parked skill's hiding place with it.
plugin add printed "plugin remove <id> takes it back out" and the verb resolved
its argument as a path, so the sentence was false on the door that printed it.

The path reading stays first and unchanged. Only when no configuration file
declares that directory does the word match declared bundle ids, across loaded,
disabled and refused entries. Hits are collected rather than taken first: an id
is unique among loaded bundles only, and deleting the wrong [[plugin]] entry is
silent.

The declared set is passed in rather than read here, because Config::plugins is
a full re-parse of every manifest and is confined to src/resolved.rs.
accepting_several opts a picker in; marked and chosen read the set as indices
into the caller's unfiltered rows. Outcome is untouched — it is Copy and has six
exhaustive matches — so a picker that never marks anything cannot observe this.

Marks survive the query, because the operator narrows the list in order to find
each row to mark. set_rows clears them, since a different list means the indices
name different rows.

set_unfold now stores one height per row, and the demand reserves the largest
configured height, so neither the marker nor the query can move it.
io-harness 0.72.0 emits QuestionsAsked for several questions asked together and
does not also emit QuestionAsked, so the transcript was blind to the exact ask
this release exists to serve.

The disposition is Line rather than Silent: an overlay draws a question, never an
ask, and "these three arrived together" is the only fact the event carries.
Routing it to a surface that does not hold it would have routed it to nothing.

The count of declared event kinds moves 51 to 52, and its assertion now runs
after the name sets — it stood first, so a harness bump failed saying a number
moved and stopped before the list naming the new kind ever ran.
It sat on the bare /config list, which made a keystroke there read the network,
write a scope file and reassign the running turn's configuration — and that is
why /config could not run while a turn was in flight.

configure::descent answers for prices.as_of and nothing else, keyed on the key
rather than on Kind::Machine so a machine-written key added later inherits no
act. The do-nothing row is at index 0, as it is in every other confirmation.

configure::said is one speller for the two doors that now both report a setting.
CATALOGUE is unchanged at 37 keys.
A batched ask parks as one row whose question column is numbered prose and whose
choices column is empty, so a resumed batch showed a wall of text with nothing to
pick.

Pending::Question gains a questions field rather than a second variant: the
harness parks one row with one question_id and the same four resume signatures,
so a variant would assert a second driver that does not exist. It is empty for a
singular ask, which is what the store actually holds.

The headless door answers a batch with one --answer, because record_answer does
one compare-and-swap — the fix was to the sentence, not the mechanism. It now
says the row is N questions, that one text answers all of them, and to number
them to match.

sessions::note counts a batch instead of quoting it; the old arm pasted the
numbered rendering into a one-line detail, which reads as if the first question
were the whole ask.

Known limitation, stated in the rustdoc: a singular multi-select's flag is
destroyed before io-cli sees the row — PendingQuestion has no such column and
put_question writes no questions value.
Eleven commands now run mid-turn. /config is admitted bare and refused argued,
which is the first time runs_mid_turn has had a reason to read past the first
word — it has always taken the whole line.

This discharges US-IO-CLI-0.32.0-I11 by removing the write rather than guarding
it. That iteration named one offending row; the bare list in fact had no
report-only rows at all, because Left/Right called cycle_setting, which wrote the
scope file on the keystroke with no confirmation — the only such write in the
product. The arrows now open the value descent focused on the value in force, and
Enter is the confirmation every other managed surface uses. cycle_setting is gone.

The act was appended in two places; removing only the bare-list push would have
looked right and been undone by the next arrow press, because the value-cycle
redraw appended it again.

Pick::Config gains a leave-it arm above the key arm, or row 0 of the new descent
opens a scope picker for a setting called "leave it".

The declared bundles reach manage::plan from the session's already-resolved set,
and the argv door builds one only for the removal verb, so editing an MCP server
does not pay for a plugin re-parse.
io-harness does not emit QuestionAsked for a batch, so an arm filtering only on
it stops matching the moment the model picks ask_questions — and a live arm that
stops matching goes quiet rather than red, which is indistinguishable from
passing.

Both arms now match either variant, and an ordinary offline gate reads this
file's own source and requires the plural to appear as the other half of an
alternation with a specific singular pattern position. A comment cannot satisfy
it, and a watching count closes the hole where a rename leaves the loop finding
nothing and passing.

One new arm answers a three-question batch with a distinct answer per question
and compares the recorded sequence against the typed one, so answering only the
first, answering out of order, or one answer for all each fail rather than park
quietly. 37 ignored, 1 offline gate passing.
…elves

Responder::answer_all is overridden, so a batch crosses the channel as one unit
rather than as N serial calls. The container lives in the library: src/main.rs is
linked by no test binary, and a queue written there could be neither asserted nor
sabotaged.

A batch is reviewed in place as it is answered — one question on screen at a time,
deciding one moves to the next undecided one, PgUp/PgDn walk them, a decided
question re-opens with its answer back in the composer. No review pane, because
one is a second rendering of state already on screen that can disagree with it,
and it needs a submit key that answers nothing — the reflexive-Enter hazard the
free-text marker rule exists for. On a single question none of this is drawn.

A second question arriving while one was open used to REPLACE it, dropping the
first question's reply channel and turning it silently into a pause. The guard is
in App::open_intent rather than the driver, so a test can reach it.

An offer draws its description under its label and unfolds its preview beneath
itself, one at a time, in the blockquote the markdown renderer already uses — the
diff gutter is private and shaped around an Edit. Heights are measured through
rows::wrapped and reach set_unfold before the demand is read, so a gate written
against lines.len() fails.

Two defects caught while building it. Enter was routed on Picker::unfolded_now,
which is true over any unfolding row — with previews that sent Enter over an open
preview to the empty composer; it now asks whether the marker is on the row that
takes typing. And a description is a heading row, so picker indices stopped being
choice indices: indexing choices with a row would have answered the agent with a
different offer of the same question, silently. One pure translation now does it.

Multi-select marks with the spacebar and submits through Question::answer_of, the
harness's own spelling, so two interfaces answer one question with one text.

Stated cost: an offer with both a description and a preview draws the preview
between the label and its description, because a picker opens a block under the
row that owns it.
TurnFacts carried two summary sentences and last_run had none — an insertion put
the struct between a sentence and the function it described. mid_turn_picker
opened with the truncated tail of commit_image's doc, which has its own. And
Picker::unfolded_now told the next caller to route keystrokes on it, which is the
defect this release just fixed in the one caller that did.

Nothing in the suite can see a rustdoc summary attached to the wrong item, so all
three survived every gate. mid_turn_picker's count is corrected too: four picker
kinds reach it now, not three.
Eleven commands run while a turn is in flight, not ten, and the excluded set is
six rather than seven. The refused list in the keys guide named twenty-one where
the code refuses twenty-two — /exit and /export had never been listed, and that
was already false before this release.

The keys guide argued the spacebar was unavailable on a picker because every
printable character filters. This release put the spacebar arm above the printable
one on a plural picker, so that reasoning was about to become false; it is
rewritten, along with the arrow-cycling paragraph for a cycle that no longer
exists.

Thirty-four of the forty-six in-page links went nowhere, left over from 0.30.2's
README split moving sections into pages while the anchors kept pointing inside the
file they left. The link gate skipped fragment-only links by design, so nothing
caught it. Every anchor now names its page, and the gate resolves both spellings.

Every io-harness source citation was re-verified against 0.72.0 rather than
version-bumped: nine of the ten cited files are byte-identical between the pins
and run.rs gained one line above line 102, so four citations shift by exactly one
and the rest stand. One claim was false — io-harness substitutes three forms, not
two — and the correction already existed in the other file that carries it.
…ed nothing

Found by an adversarial review and a sabotage pass over a fully green suite.

Intent::resumed never read PendingQuestion::questions. A batch parks as one row
whose question column is numbered prose and whose choices column is empty, so a
resumed batch drew one run-on line with no offers, the first question's context
presented as everyone's, and multiple defaulted to false — verbatim the failure
this release exists to stop, and the failure src/resume.rs cites to argue the
field must exist. Reachable through the release's own flow: decline one question,
the run parks, resume. io exec reaches it on the first ask, since nothing there
implements Responder.

Both construction paths now end in one private constructor, so the struct literal
that let them diverge is gone. Delivery stays singular — several answers are
assembled into the one text the one parked row takes.

F14 had no gate. Its criterion describes a count of configure::write call sites
reachable from a key handler; no such test existed, only a prose assertion
satisfiable by editing markdown. The gate now counts nineteen writes in the
driver, names each by door, and slices the arrow arm to catch a write moved into
it. Its cycle_setting assertion doubles as the comment-stripper's self-check.

F17 was one empty slice from silent reversion. Passing one at either manage::plan
call site returned plugin remove to path-only behaviour with the whole suite
green — the widening shape, where nothing fails and the gate goes vacuous without
going red.

N6 named a prose sweep that did not exist, and the TOML invariant this crate
states in several places was false: src/prices.rs parsed a whole document by
turbofish while the gate banned two spellings out of eight. prices never needed a
document, only a count of one table's keys, so edit gained the accessor rather
than the gate gaining an exemption.

Also: an arrow on a key whose values cannot be offered speaks again, restoring
two sentences deleted with cycle_setting; the decline row's key is a sentinel
prefixed like the act beside it, because a configuration file may hold a quoted
bare key called "leave it"; both plugin doors answer the same question, through
one staleness refresh rather than two, which keeps the once-per-session
resolution the gate pins at two call sites; /settings is admitted mid-turn with
/config and refused argued with it, since parse reads them as one command; and
four vacuous assertions now fail on what they claim to gate.
The release matrix caught this with macOS and Linux green. Git checks these pages
out with CRLF on Windows, and every helper in the file reasons about a bare
newline — sharpest in paragraph(), which ends a paragraph at a blank line, found
no boundary at all on a CRLF checkout, took the rest of the document, and then
reported that a page naming eleven commands named all thirty-eight.

Normalised once where the files are read rather than in each helper, so the rule
is one line at the boundary instead of a guard every future reader has to
remember. Proven against a genuine CRLF checkout of docs/guide/keys.md rather
than only against the LF one this machine has.
@aakashpawar1999
aakashpawar1999 merged commit bc13deb into main Aug 30, 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