io-cli 0.16.0 - #44
Merged
Merged
Conversation
io-cli's only writer serialises a whole private File struct and puts it to disk, which is right for the wizard creating a file and destructive for 0.16.0, where every surface writes into a file an operator wrote by hand. A re-serialisation drops their comments, their key order and every section this crate has no type for, and the result still parses and still carries the value that was asked for — which is what makes the mistake invisible to any test that re-parses. So the tests assert a byte property. The mechanism is toml's re-exported Spanned. Three things about it were measured against toml 1.1.4 first, and each decided the design: a flat key/value region yields exact value spans for every scalar kind; a table's own span is its header rather than its body, so the document is cut into regions before anything is parsed; and a recursive untagged node type silently loses every nested span instead of erroring. A dotted key reports the key's span rather than the value's, so those are refused with a sentence rather than mis-spliced. No dependency is added — toml_edit is not in the tree and is not wanted. tests/dependencies.rs forbids toml::from_str anywhere in src/, and caught this module before review did. Amended the way the spawn ban was amended for src/shell.rs: permitted by path, plus an assertion that edit.rs names no io-harness configuration type, so the exception cannot widen into a second reader of what a setting means.
io-harness has recorded the origin of every key since its 0.30.0 and no io-cli release has ever asked. This is that question asked. Three scopes, not four — user, project, local — and a fourth answer that is not a file: a key no file named has an EMPTY origin, which is the crate's own default speaking. Attributing that to the lowest-precedence source file would credit an operator for a value they never wrote, which is a lie a reader cannot detect. Decided::Default names no path, and the sabotage arm that makes it name one kills exactly one test. MemorySection is private in io-harness 0.66 and there is no Config::memory, so a surface built only on typed accessors would have a hole exactly where an operator had written something. Those keys are quoted from the file the origin names, through edit::value_at — the writer's own span machinery, reading bytes and interpreting nothing. The catalogue is io-cli's own list because the harness exposes no enumeration of its schema, so a gate holds it to docs/config.example.toml. It caught four invented keys on its first run: policy.defaults separates read from write, and [memory] is max_entries, max_chars and max_entry_chars. The example documented those three sections in prose and named none of their keys; it now carries every one the catalogue offers. A key outside the catalogue is still shown. A surface that listed only what it already knew would hide exactly the keys a reader went looking for.
…he refusal The write is verified by io-harness reading it back, and rolled back when it refuses. That is what keeps io-cli from holding a second copy of the harness's rules: refuse_widening fires on the scope of the FILE, and only Config::discover knows that — Config::from_toml has no path and therefore no scope. So the only honest way to ask "may this file say this" is to write it and re-discover, and when the answer is no the original bytes go back and the harness's own sentence comes out, re-worded by nobody. All seven refusal cases are covered: the five PROJECT_WIDENING pairs and the two sections refused wholesale. The same five values are accepted in io.local.toml, which is what proves the rule is about the scope rather than the value — a test asserting only the refusals would pass against a build that refused them everywhere. reload returns the Config AND io-cli's own CliSettings, because main derives the second from the first once at startup; refreshing only the Config would leave the theme, the glyph set and every capability as they were at session start while the surface claimed otherwise. edit gains append and remove: set reaches a key inside an entry that already exists, and an array of tables grows by gaining a whole block. A new entry goes last, because for [[provider]] the order is the fallback chain and inserting into the middle would rearrange which provider runs.
/config alone opens a picker over every setting with the value in force and the file that decided it. A chosen row fills the composer with the key rather than writing anything — the palette's own idiom, and the right one here: a picker that wrote on a keystroke would change a file on the way past. /config <key> answers what is in force. /config <key> <value> asks which of the three scopes it goes in, and only that choice writes. config and capabilities are both mutable now and both re-derived after a write. That is F3's sabotage made structural: main derives io-cli's own settings from the Config once at startup, so refreshing only the Config would leave the theme, the glyph set and every capability as they were at session start while the surface claimed the value had changed. Adding one command row failed four gates in three files, which is the blast radius this release's risks predicted: the written-out COMMANDS list, the README table, and the palette's narrowing counts, which are stated rather than derived so a count cannot recompute itself into passing while the narrowing it describes stops happening. All three updated. The fourth was this branch's own gate catching its own prose — edit.rs may not reach for a configuration type, and its doc comment explains why, which means naming several. The gate now strips comments: it is a property of what the code does, and one that read prose would forbid the file from explaining itself.
…t ceilings [app.io-cli] max_steps was deprecated in 0.14.0 with a notice naming this release, in the terminal, the README and the CHANGELOG. It is gone. The notice is not, and the pairing is the point. CliSettings carries no deny_unknown_fields, so a file still holding the key parses fine and the key is simply ignored: no error, no warning, and a step cap that quietly changed. So the notice reads the raw section now, through Config::app with a shape naming only the dead key. Its sabotage is deleting the notice along with the field, under which the contract is still right and an operator's turns quietly start ending at a different number with nothing to say why. Two existing tests asserted the behaviour being removed. Both rewritten to assert what is true now: [run] max_steps is the only spelling left. In its place, the three TaskContract ceilings io-harness gives no key at all — max_parallel_reads, spawn_background_after_secs, detached_spawns. RunSection carries thirteen fields and none of them is these, so io-cli names them in its own section. They are applied in contract::configured rather than beside the other [app.io-cli] keys, and the placement is the criterion: io exec calls configured and nothing else, so a ceiling applied in session would bound a terminal and leave CI running on the defaults. That is the asymmetry 0.14.0 deleted, and the sabotage that reintroduces it fails only the headless arm. The README key-table gate failed by design and caught all three of a missing row, a stale row, and the prose count above the table.
The registry moved under this release. 0.66.0 was the newest published version when the contract was written; 0.67.0 is now, and the standing rule is that every release pins the newest published io-harness. The whole source audit was re-verified against 0.67.0 after the bump and every load-bearing fact holds unchanged: Scope still has three variants, PRESETS is still 21 and still pub(crate), Config still derives only Debug, Clone and Default, MemorySection is still private, the eight Config accessors this release uses are still public, and EventKind::Mcp still carries exactly server, tool, ok and millis. The suite is unmoved at 752. 0.67.0 also closes the wall io-cli 0.17.0 was declared to be blocked on: turn_bounded_steered and turn_contained_bounded_steered take the caller's contract, an observer and a steer inbox on one call. Nothing in this release uses them; the roadmap entry for 0.17.0 is corrected to say the dependency is satisfied rather than pending.
Configured and reached are different sets, and the second is the one an operator is asking about. The status line's aggregate counts what the run reached and carries no server names at all, so a server that is in the file and never answered looks exactly like one that is not in the file. Three states, and "not reached this session" is its own. It is the state every server is in before the first turn runs, and drawing it as a failure would tell an operator their configuration is broken at the moment it is most likely to be fine. The sabotage that does exactly that kills two tests. The count is distinct tools asked for, not calls, and the panel says it is a lower bound. EventKind::Mcp carries server, tool, ok and millis and no tool count, and io_harness::mcp exposes no catalogue accessor, so how many tools a server OFFERED is not on any channel this product can reach. It becomes reachable in 0.17.0, behind io-harness 0.68.0. Which is also why the status line's own field is renamed here. `mcp N/M tools` counted calls from 0.10.0 to 0.16.0, for exactly this reason: the number it wanted was never on the wire, so it counted the one that was. Renamed in both renderers, because Status has two and a field added to one is a field missing from the other. Add, edit and remove write through the release's own writer. The byte property is proved on the one table where the loss would be silent: an [[mcp]] entry carrying env and args, neither of which io-cli models, and which serde does not reject because McpServer is flatten-based. Disable and reconnect are not offered. There is no key for disable, and an invented one would be accepted by the file and ignored by the harness, so the server would start anyway. There is nothing to reconnect: servers attach per turn, so the panel says a change takes effect on the next turn.
… been
io-harness has read [[provider]] as a fallback chain since its 0.27.0 — the
first entry is what a run uses and each later one is the next link — and no
io-cli release has offered a way to arrange it. This interface has drawn an
event for a fallback happening without ever being able to cause one.
Reordering needed a new writer primitive. Order is meaning here, so a move
carries the entry's own bytes: its comments, and every key io-cli does not
model. The test asserts the moved entry arrives whole, that a later section
does not shift, and that moving it back is the identity.
The twenty-one presets are offered, and io-cli's list is proved against
io-harness's rather than trusted. PRESETS, preset_names and preset_list are
all pub(crate), so the real list is read out of Compatible::preset's own
refusal, which its documentation says names the presets that do exist. The
gate checks the refusal still carries them before comparing, because two
empty lists compare equal and that is a control that cannot fail. Where a
preset points, and whether it is local, come from Compatible::base rather
than a second table here.
The credential column reads the file's own bytes, because io-harness
substitutes ${env:…} while parsing and refuses an unset variable outright —
so by the time a Config exists the indirection is already gone, and showing
an operator which variable they named means quoting what they wrote.
Verification is the wizard's, reused. The panel adds none of its own, which
an existing gate now enforces: it may name Compatible::preset to ask a
preset a question, and may not keep the answer.
io-harness has carried [profile.<name>] since its 0.27.0 and no io-cli release has ever selected one, so a set of choices has never had a name in this product. It has no accessor for them either. with_profile applies one by name and says so when the name is wrong, but nothing lists them: the merged table is private and profile keys do not appear in Config::origins, which a probe confirmed prints nothing for a file declaring two. So the names come from the file, through a new edit::sections that exposes the scan the writer already cuts a document with — header paths only, nothing about what is inside them. Two doors. --profile is global like -C and -m, because a set of choices you want for one run is exactly what you type on either side of a subcommand, and it is applied above both arms so a session and an io exec run get the same overlay from one decision. /profile lists and switches in a keystroke, re-deriving io-cli's own settings alongside the Config, because a profile body is the file format again and can carry [app.io-cli] keys too. Neither writes anything, and a wrong name gets the harness's own sentence.
…w is Twenty commands in one flat list is a list nobody reads, and this release is what made it twenty. Four groups by what the operator is doing — the session, this turn, inspect, configure — none longer than ten. The palette shows those groups while it is browsed and drops them the moment a character is typed, because a ranked list with headings interleaved puts a heading above a row that ranked there for unrelated reasons. A heading can never be chosen; every movement steps past one, and Enter declines anyway. That found a real bug: Picker::new sets its match set directly and never calls refilter, so a grouped list opened with the marker on a heading and Enter answered nothing. The kind mark is a third field on Row, and both exclusions are the point. Not in the label, because the matcher ranks the label and a shared first character makes exact and prefix matching unreachable — the defect the stripped slash exists to avoid, coming back as the price of marking rows. Not in the detail, because the detail is dropped first on a narrow terminal, which is exactly where the 0.15.0 marking failed. The roadmap said the kinds were drawn identically; they were not, and the real defect was next to it. palette and palette_pick now read one construction. They walked the inventories separately and agreed because they were written next to each other; headings make that agreement impossible to keep by inspection. /help renders the same grouping, and its order is asserted to be the palette's rather than a second arrangement. /usage answers what /status answers and appears in no inventory, no group and no palette row.
The README's [app.io-cli] key table is asserted against a fully-populated CliSettings, so removing max_steps and adding three ceilings failed it three ways: a missing row, a stale row, and the prose count above the table, which the gate reads because a count written in prose is the first half of a table to go stale. The command table is grouped now, matching the palette and /help, so three surfaces cannot describe three different arrangements. The palette's marks and the unlisted /usage alias are documented beside it. The CHANGELOG states the removal as a removal, under its own heading, with why the notice outlives the field: [app.io-cli] is not schema-checked, so without it the key would simply stop working. A Changed heading records the mcp label, which counted calls under the word "tools" since 0.10.0, and a Known limitations heading records the tool count and the absent disable verb with the reason for each and where they go. config.example.toml gained every [run], [sandbox] and [memory] key the /config catalogue offers; it documented those sections in prose and named none of their keys, which is what the catalogue gate caught.
The version bump, and the two findings clippy raised on the release gates: an unnecessary_sort_by in the writer's splice ordering, and a single_match in the scope-picker arm. The sort keeps its stability, which a move depends on — it pushes a removal and an insertion that can share a start. Gates: fmt clean, clippy clean, 61 binaries / 792 passed / 0 failed / 24 ignored, release build green and the binary reports io 0.16.0. The doc gates and the release binary both came after the bump, which is the rule 0.57.0, 0.60.2, 0.65.0 and 0.14.0 each paid for once.
io-cli 0.16.0 — the configuration file as a surface
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The release PR for 0.16.0 — the configuration file as a surface in the session.
/config,/mcp,/providerand/profile; a writer that replaces one value'sbytes and copies every other byte through; the three
TaskContractceilingsio-harness gives no configuration key; the removal of
[app.io-cli] max_stepsthat 0.14.0 promised for this release; and the command surface grouped, marked
and committed.
Everything in PR #43, which merged green on three platforms with no flake. The
sealed record, its lock pin and the evidence are on disk under
.ultraship/,which this repository gitignores whole.
cargo fmt --check,cargo clippy --all-targets -- -D warnings— exit 0cargo test --no-fail-fast— 61 binaries, 792 passed, 0 failed, 24 ignoredcargo build --release— exit 0, binary reportsio 0.16.0live session showing the removal notice and the grouped palette, and a live
write through
/configthat changed exactly one line of a hand-written file