Skip to content

io-cli 0.27.0 - #68

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

aakashpawar1999 merged 9 commits into
mainfrom
develop

Conversation

@aakashpawar1999

Copy link
Copy Markdown
Contributor

The release pull request for 0.27.0. Everything in it merged into develop through #67; git diff --quiet feat/0.27.0 develop is clean, so this ships exactly the tree the record describes.

The store stops being something you cannot see, undo stops being all-or-nothing, and the work stops ending when the terminal closes.

  • /store — the file's own page arithmetic, per session and in total; rm, sweep and compact, each behind a confirmation whose first row declines. A deletion does not shrink the file, and io says so.
  • /undo — one file, one step, or the whole turn.
  • /export — the conversation as markdown, a run's canonical trace verbatim.
  • One event that reached nobody now has a line; the other eight silences keep their routes.
  • /contain moves to Session, freeing the Turn slot /undo takes without widening the bound.

Gates

Suite 93 binaries, 1,417 passed, 0 failed (0.26.0: 90 / 1,362)
Live, real provider 36 passed, 238s (0.26.0: 33)
Matrix green on all three platforms
clippy / rustdoc clean; the nightly rustdoc gate caught one first
Sabotage one per criterion, all executed, each killing only its own arms
Adversarial review four HIGH behind a fully green suite — the tenth consecutive release
io-harness pin 0.69, unmoved — newest published, the thirteenth time

Six CodeQL alerts were raised and dismissed as false positives with evidence: all six are the rust/cleartext-logging heuristic firing on the name session_id, which here is a SQLite last_insert_rowid() integer with no entropy that authenticates nothing, in assertion messages in test binaries that never ship.

The record is sealed and pinned at sha256:b9b8e449…, ultraship validate exits 0 over 72 checks.

…hange it

`/store` commits a page carrying the file's own page arithmetic — bytes on
disk, bytes already free inside it, the session and run counts and the
per-table breakdown — with each session's own size beside it. `/store rm <id>`,
`/store sweep <date>` and `/store compact` change it, each behind a
confirmation whose row 0 declines.

Every figure is io-harness's. `Store::store_size`, `session_size`,
`delete_session`, `sweep_sessions` and `compact` have all been public since
before this product had a home to keep a store in, and none of them was called
anywhere in this crate.

The distinction the module exists to make is that a deletion does not shrink
the file: SQLite frees pages into the database rather than out of it, so a
removal moves bytes from `file_bytes` into `free_bytes` and the file on disk
stays the size it was. A `VACUUM` is the only reclamation available, because
every store this crate has created was created without `auto_vacuum`. The
report says so, and the gate asserts `file_bytes` is unchanged by a removal
rather than merely no larger.

The sweep asks for the rule rather than for a count. There is no public reader
for `sessions.created_at`, the column it filters on, so the set a date selects
cannot be known in advance — and the one available substitute, a session's
earliest turn, is strictly later than the session itself and would under-state
a deletion. Filed as io-harness#216; the figures are reported from `Pruned`
once the sweep has run, refused ids included.

`store::acts` puts the confirmation's one decision in the library, because
`src/main.rs` is a binary nothing under `tests/` can link.

Also corrects a stale comment: `src/main.rs` claimed `/skills` confirms with
"leave it" at row 0. It does the opposite, and it is the only confirmation in
the product that does.
… verbatim

`/export` writes this session's conversation as markdown for a person to read,
and `/export trace` writes one run's canonical trace exactly as io-harness
produced it. Both go through `Workspace::write_file`, so the path policy the
session runs under is the one that applies, and an existing file is refused
rather than overwritten: an export is a snapshot, and the next one is a
different snapshot.

The trace is passed through untouched. Its whole value is that io-harness
excludes wall-clock stamps, measured durations, an ephemeral tempdir path and
autoincrement ids from it so two runs of one case can be compared; a trace this
crate reformatted would compare against nothing. The gate is a byte comparison
for that reason, not a parse-and-compare, which would pass on exactly the
defect that matters.

It is not JSON. `canonical_trace` builds pipe-delimited lines by hand, there is
no JSON alternative in io-harness, and `StepRecord` does not derive `Serialize`.
The export takes a `.txt` path: a file named `.json` that is not JSON breaks for
anyone who pipes it to a JSON tool.

An existence check through `read_bytes` was wrong and would have refused every
first export — a missing file is `Error::Config("no such file")`, not
`Error::Io`. It asks `resolve(path)?.exists()` now, which separates whether a
path may be addressed from whether it may be written.

`Inspect` reaches ten commands, which is the bound, so its gate is an equality
now rather than "there is room left".
`/undo <path>` restores one file from the snapshot taken before this run first
wrote it. `/undo step <n>` reverse-applies one step's recorded diff. A bare
`/undo` undoes the whole turn, which is what the rewind chord has always done —
and both doors now reach one `undo_whole_turn`, so the word and the keystroke
cannot drift apart.

Two events fire for the first time in this product's history. `Rewound` is
emitted only inside `rewind_run_observed` and `Reverted` only inside
`rewind_step_observed`; `src/rewind.rs` called the plain `rewind_run` from 0.4.0
until now, so the arm rendering one of them in `src/events.rs` was unreachable
from the day it was written. The gates assert on what an observer was handed,
because the rendered sentence is identical either way.

Reverting a step is order-sensitive: a later step still standing on the same
lines makes the revert stale and io-harness leaves the file alone rather than
fuzzy-matching it. That is said on screen, and only when something actually came
back stale.

The four answers to a file restore stay four answers. `NotKept` and
`NotRecorded` mean different things, and collapsing them tells an operator
"nothing to undo" about a file whose previous contents were merely over the
snapshot cap.

`/contain` moves from `Turn` to `Session` to make room, on that group's own
rule: `Turn` means a command acting on the work the turn just finished, and
`/contain on|off` is a posture that changes how every later turn is driven. The
bound stays at ten, as it did in 0.19.0, 0.22.0 and 0.26.0.
The nine events this release set out to draw were already in src/triage.rs with
a disposition of Silent and a written route — the surface by which the fact
reaches the operator instead. Eight of those routes are better arguments than
drawing would be: `reverted` and `rewound` route to io-cli's own rewind summary,
so drawing them would double-report the undo this release just wired up, and
`plan_proposed` routes to the overlay showing that same plan.

One route was wrong. `speculated` named `io exec --json` and the durable trace,
and both are places a person goes deliberately, afterwards, already suspecting
something. A route is meant to name where a fact reaches somebody who was not
looking for it. It draws a line now, carrying what reading ahead bought and what
it cost — and only when something was discarded, because io-harness emits the
event whenever anything was started at all, and a line in every transcript
saying nothing went wrong is a line nobody reads.

The set gate this release planned to build already existed: tests/triage.rs
reads `pub enum EventKind` out of the pinned io-harness source and fails by name
when the two sets differ. What it lacked was any check on the route column, so a
silence could name a surface that does not exist. It cannot now.

tests/glyphs.rs caught the new arm by itself and by name, which is what it was
written for in 0.14.0.
…odel can reach them

`delete_session`, `sweep_sessions` and `compact()` are each named in exactly
one file, and it is the one whose whole subject is the store. The roadmap's words
for /store are that no model can call any of it; "nothing was added" is not
assertable, so this asserts its reachable form — a second module naming one of
these is the first step towards a caller that is not a keystroke.

The other half is that no source file calls `with_tools`, which is how a model
is handed reach at all.

Both sweeps strip comments, and that was verified by writing the forbidden name
into one and watching nothing fail.
Every confirmation this release adds is drawn through the real render buffer at
six widths in both glyph sets, with a workspace path chosen not to fit. The
property names no number: no row overflows, and row 0 is still the declining
row — a confirmation that dropped its "leave it" on a narrow terminal would
leave an operator with only the destructive choice on screen.

That is 0.26.0's correction applied. A gate that picks a width is asserting
arithmetic it will get wrong, and that release's own narrow fixture passed at
eighty because the field could not fit either way.

N3 is asserted as the absence of a configuration key, because that is the only
way this release could have changed what a turn is built from: every surface it
adds is a command, none is configured, and none reaches contract::session. The
contract's existing Debug-equality gates pass unamended, which is the rest of
the same claim made by instruments this release did not touch.
Three README sections, each stating the fact that surprises: a deletion does not
shrink the file, a removal takes the session's restore points but not its memory,
a step revert is order-sensitive, a restore overwrites an edit made afterwards,
an existing export is refused rather than overwritten, and the canonical trace is
not JSON.

docs/config.example.toml is untouched, which is the point: this release adds no
configuration key, and a change to that file would contradict the gate that says
so.

The nightly rustdoc gate caught a redundant explicit link target in src/undo.rs
— the eighth time in ten releases it has found something stable cargo doc,
clippy and cargo test --doc all miss.
Two reviewers, split by area, over a fully green suite. The tenth consecutive
release in which that gate has paid for itself, and the two most severe were
found by both independently.

**A sweep date was never validated, and the comparison is lexical.**
`sweep_sessions` filters a text column, so `/store sweep 2026-8-1` — an unpadded
month — sorts above every real timestamp, as does `/store sweep yesterday`.
Either would have removed every session in the store, across every workspace,
while the confirmation echoed back exactly what was typed and read as correct.
The boundary is now parsed to a shape and normalised, or refused by name.

**The session io is running could be deleted from inside itself.** The /store
page lists every session with its root, which is what makes the operator's own
the recognisable one; io-harness refuses nothing and enforces no foreign keys,
so the process would have carried on writing turns into a session that no longer
existed and every later /undo and /export would have reported it empty.

**The export was the whole turn tree, not the conversation.** session_turns is
documented as the tree; a rewind moves the head without deleting the turn, so an
undone turn was being exported, unmarked, into the one artifact whose purpose is
being read by somebody who was not there. It walks Session::history now.

**The whole-turn undo reported through the one-slot footer**, so every "left as
the turn left it" warning was overwritten and an operator was told a restore had
happened that had not. It records into the scrollback, like its two siblings.

Also: /export wrote under the file's policy rather than the session's effective
posture, so it wrote while the footer said read only; the step revert did the
same, against a contract io-harness documents; a dangling symlink read as an
empty path and a write would have followed it out of the workspace; export::write
now refuses an existing file itself rather than trusting a check taken before the
confirmation; Reverted::NoHunk printed a Rust debug string; a sweep never said
how many restore points it took; and the /store page's session total counted
sessions its own list could not show.

A gate that could not fail is fixed: the sweep confirmation's "carries no count"
assertion had a loop-invariant `&&` that made it hold for any title at all.
The three new commands' argued forms had no test whatsoever — including the two
refusal families whose entire purpose is that a mistyped verb never reaches
something destructive.

undo::whole_run and Grain::label are gone. Both were public, tested, and called
by nothing — the shape this codebase shipped in 0.20.0, 0.21.0 and 0.26.0.

And several comments claimed src/events.rs had an unreachable arm for Rewound
and Reverted. It has no arm for either: both are Silent by decision and route to
the rewind summary. What the observed forms buy is that the events exist at all.
@aakashpawar1999
aakashpawar1999 merged commit 7a6fcf5 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