io-cli 0.14.0 - #40
Merged
Merged
Conversation
Eleven sections of io.toml were read and validated by io-harness, documented in this product's README and configuration example, and silently discarded by an interactive session. A developer who wrote `[run] max_tokens` and watched a turn spend past it was reading a file that did nothing. `contract::configured` is the config-derived half of a contract and both arms now call it. The order of precedence is stated and tested, weakest to strongest: io-harness's defaults, io-cli's step floor, `Config::apply_to`, `[sandbox]`, then `[app.io-cli]`. The floor is applied before `apply_to` so a `[run] max_steps` the operator actually wrote beats it, rather than a file that lowers the cap being ignored while one that raises it works. `[sandbox]` is attached by hand, because `Config::apply_to` does not apply it, and only when the file has one: a default `SandboxConfig` carries real ceilings where `TaskContract::workspace` starts from `SandboxLimits::none()`, so attaching one unconditionally would cap a session whose operator asked for nothing. The two server scopes are merged rather than replaced. `with_mcp` and `with_lsp` assign the whole collection, so applying `[[mcp]]` and then `[[app.io-cli.mcp]]` in sequence would leave a contract holding only the second list. They are concatenated and deduplicated by id, the `[app.io-cli]` entry taking the wider entry's position, and a dropped duplicate is named once at session start. `io exec` gains the step floor too. It set no cap at all, so a headless run took io-harness's twelve and ended `error: step_cap_reached` under half-finished work with nobody watching. What stays arm-specific is the responder, the plan gate and the system prompt: `PROMPT` tells the model it is rendered in an eighty-column pane whose output has scrolled, which is false of `io exec --json`. The live arms are handed an empty configuration, so every one of them keeps the contract it was written against.
Three facts the pinned harness emits on an ordinary session turn were reaching the renderer and being dropped. All three were in the triage table already, deliberately silent, so `Status::unknown` never moved for any of them and nothing on screen said they had been discarded. A dial carries the host as the command asked for it, the port, and whether the policy permitted it. It is never a resolved address: the policy's patterns are written against names, so a row showing an address would not match the rule that decided it and its reader could not tell which rule to change. A refusal draws in the refusal tone, whose word survives with no colour at all. A sandbox draws four of the seven kinds that reach the channel. `cap_hit` is a limit reached and not a failure — the sandbox did what its configuration told it to, and reporting that through the error path would tell an operator their run broke at the moment their cap held. The backend is carried where the event has one and never invented where it does not: io-harness sets it on `create` and `exec` alone. The `dial` kind draws nothing, because io-harness builds it as a `destroy` event with the kind overwritten and emits it beside the dial itself. Drawing both would put every dial in the transcript twice, and the copy here is the poorer one — it carries the word without the host, the port or the verdict. A stall is composed from the run state around it, since the variant carries nothing at all: the step it stopped on, and how long it has been there. No clock is read in the renderer, so the line is arithmetic on two ages the driver handed in rather than a timing, which is also what makes it assertable. An absent dial line is not evidence of no egress. The event has one emit site behind three conjoined preconditions, so a policy that names no hosts emits none of these ever.
…g as a ceiling A session that now obeys `[run]` can be bounded by a number the operator wrote and never see it. The status line carries each budget that exists with what is left of it — steps, tokens and duration — and omits the ones that do not, so a session with no `[run]` looks exactly as it did. Both renderers draw them, from one composer. `Status` has two — the three-row footer the binary draws at an idle prompt, and the one-row form a short terminal falls back to — and in 0.12.0 a field added to the second while the test asserted the second was green everywhere and invisible on screen. `Status::budgets_left` composes each remainder once and both renderers extend from it, so they cannot disagree. A turn ended by a budget now says which budget rather than reporting a failure. `step_cap_reached`, `time_budget_exceeded` and `cost_budget_exceeded` join `budget_ceiling_reached` in the ceiling tone: all four are ordinary `Ok` outcomes, and a ceiling is the operator's own instruction being carried out. Reporting one through the error path tells them their run broke at the moment their limit held, which is the opposite of what happened. The budgets are read off the contract the turn is about to run and never recomposed from the configuration, because the only thing that knows the whole order of precedence is that contract. The cost of not duplicating it is that the fields appear once a turn has been built rather than at the very first idle prompt; they persist afterwards, since the file does not change while a session runs. Whether a step budget exists is a question the contract cannot answer on its own — `max_steps` is a plain `u32` and every turn carries io-cli's floor — so it is answered by comparing against that floor. An operator who writes `[run] max_steps = 1000` therefore gets no step field, which is preferred to drawing a floor nobody asked for on every session.
…up notice be seen `[app.io-cli] max_steps` exists because the flat turn once had no way to raise io-harness's cap of twelve. This release gives `[run] max_steps` that job, so there are two spellings for one number in one file and the less discoverable one wins. It keeps working and keeps winning; what changes is that a file carrying it says so once, naming the key, the value it took, and where the number moves to. It is removed in 0.16.0, with the release that rewrites the configuration surface — a deprecation that removes in the same breath is not a deprecation. The decision reads the field rather than the effective cap. Keying off the step cap would fire on every session, and keying off `[app.io-cli]` being present would fire on the file `io setup` writes — a notice on a session not using the deprecated key is noise that teaches operators to stop reading the start-up lines. The startup notices are committed rather than said, which is what the comment above that loop has always claimed they were. `App::say` sets the footer's notice, and setting it replaces it, so a session with several things wrong with its configuration file showed only the last of them — and the file with the most wrong with it was the one losing the most. Six senders can put a line in that list and two of them are new in this release, so what was survivable is not any more. The doc beside the key is corrected in the same release. It claimed io-cli could not raise the cap on the steered path and that the key was honoured only on a contained session; both have been false since 0.11.0 moved the flat arm onto an entry point that takes a caller's contract.
… scrollback A status line a few cells wide can carry the numbers and cannot carry the reasons. `/status` writes the rest into the terminal's own scrollback, where search and selection already work, as `Ctrl+T` and `/expand` do — the viewport is a few rows and this product has no alternate screen. Every field is a value io-harness supplied. The policy layers come off `Policy::layers` by name with the acts each governs; the backend that actually applied comes off `EventKind::Contained`, because the resolved containment is not public and a live turn cannot be asked what contains it; the draw comes off the `SpendDraw` stream, because the ledger is built inside the run and never handed back; and what is connected is the configured roster confirmed against the events that said a server came up, because neither session type is public either. Where a fact is not yet known the page says so in words rather than defaulting to a number that would read as measured. The budgets are `Status::budgets_left()` rather than a third spelling of the same remainder. A number has one spelling wherever it is met. One fact per row and nothing aligned across rows, so there is no column for anything to be squeezed out of. A row too long for the terminal folds with a hanging indent and a very long path is split rather than allowed to overflow — nothing is ever fitted, so no ellipsis appears. A status page that shortened the thing its reader opened it to read would be the one surface here that cannot be trusted. It is a command and not a key: the keys are nearly all spoken for, nothing yet shows this is checked often enough to spend one on, and a key is cheap to add later and expensive to take back once it is in anybody's fingers. The one-contract-per-turn gate now names its two call sites instead of counting them. `/status` reads a contract to report the configured rosters and builds no turn, and raising the count would have admitted a real second arm just as readily as this reader.
…ng change nothing Two defects a capture of the real binary found, neither of which any test in the suite could see. The budgets reached the status line only where a turn was built, so a session whose io.toml sets three ceilings showed none of them until it had already spent against one. The moment to learn a conversation is capped at forty steps is before typing into it. They are read once at startup now, from the same builder every turn uses — recomposing them from the file would be a second answer to the precedence question, and it would drift the first time a layer moved. `/status` reported them by assigning that field before composing its page, which answered the question and made a read-only command change what the status line said the moment it was opened. It composes against the contract it was handed instead. What is drawn against a ceiling is still the session's own, because that is a fact about the session however the ceiling was arrived at. The one-contract-per-turn gate names all three call sites rather than counting them: the turn's, `/status`'s reading, and the opening one. Counting to three would admit a genuine second arm just as readily, which is the failure that test exists to make unrepresentable.
…is release ended Eleven sections of io.toml stopped being ignored by an interactive session, so every surface describing the old shape is now wrong. Five of those claims were already wrong before this release and are corrected here for the first time. The configuration example loses the block headed "Not read by an interactive session" and the reason it gave, which went stale in 0.11.0. In its place is what bounds a turn in a session and in `io exec` alike, with the precedence order and the two migration notes: a `[run]` block written for CI now bounds a conversation, and `[web]` is a capability rather than a preference — reaching a session turn it grants provider-executed search and fetch, and the vendor dials the URL, so the local `net` rule is not what governs it. That one is stated in its own words rather than folded into a list. The README loses four "Contained turns only" marks, the claim that the capabilities and the fan-out are one switch, and the claim that an uncontained session cannot be given a responder or a plan gate. The two causes are kept apart on purpose: that half has been false since 0.11.0, while the `[sandbox]`/`[run]`/`[[agent]]` half becomes false because of this release. `[app.io-cli] max_steps` has existed since 0.10.0 and had never been documented at all. It is in the table now, marked deprecated. The new gate is what found it: the README's key table is asserted against a fully populated settings value, so a key that exists and is undocumented fails by name rather than waiting for somebody to notice. Three more gates hold what the prose now claims — that the README quotes the budget strings the status line actually draws, that no surface still carries the stale phrases, and that the deprecation notice, the README and the CHANGELOG agree on the replacement key and the release that removes it. Also corrected: a doc comment dated the end of the capability coupling to 0.12.0, which is a different change. It was 0.11.0.
The sabotage pass deleted the `Dialed` arm from the renderer and this gate stayed green. It asserted that `src/events.rs` contains the string `EventKind::Dialed` anywhere in the file, and the `Sandbox` arm's own prose names that variant to explain why it draws nothing itself — so the mention answered the question and the deleted arm went unnoticed. That is the defect this test was written in 0.11.0 to close, arriving through the door the test itself left open: a name with no arm behind it. It now requires the variant at the twelve spaces every match arm in that file sits at, which is how `tests/glyphs.rs` has always read the same file, and no `use`, doc line or expression is indented that way. Verified both ways: green on the real tree, and red by name with the arm removed.
io-cli 0.14.0
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.
Release PR for io-cli 0.14.0.
developis byte-identical tofeat/0.14.0,which is the tree every gate ran against —
git diff --quiet feat/0.14.0 origin/developis clean, so nothing landed above the feature merge.The configuration file the operator wrote is the session they get. Eleven
sections of
io.tomlwere read and validated by io-harness, documented in thisproduct's README, and discarded by every interactive turn; they now reach a
session as they have reached
io execsince 0.2.0, through one builder with astated and tested order of precedence. The budgets in force stand on the status
line with what is left of each, a turn ended by one says which ceiling it
reached rather than reporting a failure, three event kinds the harness has
always emitted arrive on screen, and
/statuscommits the whole state into thescrollback.
io execgains io-cli's step floor.[app.io-cli] max_stepsis deprecated,still wins, and goes in 0.16.0.
Verification
io execruns against a realendpoint; the built binary driven through a pty.
sha256:725a5564…,ultraship validateexit 0 over 44 checks.What the process caught
The pre-implementation source audit falsified the stated mechanism of six of the
twelve acceptance criteria before a line was written. Running the binary then
found two defects no test could see. The sabotage pass found a third — a gate
written in 0.11.0 to catch "a name with no arm behind it" had itself been
satisfied by the name appearing in a comment, so a deleted renderer arm passed
it. All three are fixed here and recorded in the release record and its two
iteration records.