Repository navigation
io-cli 0.26.0 - #66
Merged
Merged
Conversation
`config.fallback_specs()` has been read, rendered by `/provider` and then thrown away: `provider::build` took the head spec and nothing else. An operator with three providers in their file was shown a promise the product did not keep. `Vendor` is one enum over the four vendor types, so a chain of arbitrary length has the single concrete type a non-dyn-compatible `Provider` requires. It wraps and never constructs — the four constructors stay written exactly once, in `maker_for`, which is what the one-construction-site gates assert. `Chain` is flat rather than nested over `io_harness::Fallback`. The nested shape does not compile: a `Fallback<Vendor, Chain>` makes the RPITIT future's auto-trait inference depend on itself, and rustc reports a type cycle that boxing the recursive arm does not break. The decision to fall over is still io-harness's — `ProviderErrorKind::is_retryable`, the same predicate its own `Fallback` and its own in-run retry ask. `last_served()` answers `None` when the head served, which is a deliberate difference from `Fallback`. io-harness emits `EventKind::FellBackTo` for any `Some` (`run/step.rs:503`) while `Fallback::last_served` answers `Some` for its own primary too, so a chain built on that type would have told the operator "the provider fell over" on every step of every run. `--provider` replaces the whole chain rather than heading it: a run scoped to one vendor on the command line must not spend at another.
…e to make room `TaskContract::with_effort` has been public and unused since io-harness gained it. `/effort low|medium|high` puts a level on every later turn, `/effort off` returns to sending no reasoning field at all, and a bare `/effort` reports. The absent case is not a fourth level. `TaskContract::effort` is an `Option<Effort>` and `None` sends the pre-0.31.0 request body byte for byte, so `contract::buying` leaves the contract Debug-identical when nothing was asked for — asserted through the same instrument that holds a configured-nothing session to the contract it built before. `Turn` stood at ten of ten, and `src/commands.rs` pre-committed the answer for the release that met the bound: re-file what is in the wrong group, do not widen it. `/profile` was misfiled by that group's own rule — it changes the overlay every later turn is built from rather than acting on the work the turn just finished — so it moves to `Session` and `/effort` takes the slot. The level is a posture and not a one-shot: it is read by each turn rather than taken, so it survives to the next one, and it survives `Status::forget_run` beside `policy` and `planning`. It is drawn by both renderers, because a field added to `Status::fields` alone is green in a unit test and nowhere the binary draws. The decisions live in `contract::buying`, `app::reasoning_of` and `app::reasoning_said` rather than in the driver, because nothing under `tests/` links `src/main.rs` and a conditional written there could be neither asserted nor sabotaged.
…re they do not fire `[app.io-cli.routing]` carries two rules — escalate to a stronger model after a number of consecutive failed gate attempts, downshift to a cheaper one while the run has written little. The decision stays io-harness's: `Routing::model_for` is its own pure function and the only implementation of it, and this crate never re-derives the rules or their precedence. The section is io-cli's own `Deserialize` type rather than `io_harness::Routing` directly, which is the one place it differs from `[app.io-cli.containment]`. `Routing` derives no serde at all and is `#[non_exhaustive]`, so it can neither be deserialized into nor written as a literal; the three builders are the way in and they keep working when the harness grows a fourth field. `require_primary` is not offered. It gates on `Provider::reachable`, a defaulted trait method returning `Ok(true)` that no provider in io-harness 0.69 overrides, so a key for it would be advertised, accepted and permanently inert. And routing does not reach a contained turn. io-harness applies it only in the flat workspace loop; the tree loop takes each agent's model from its own roster entry. So a session with containment configured is told, once, at startup — and a session without it is told nothing, because warning every operator with a routing section about a limitation most of them cannot hit is worse than warning none. Alongside it, two smaller repairs to what a turn says about itself. `[app.io-cli] conversational` is the first way to say that a question which is only a question should still open a run; absent, the decision stays io-harness's own, which is what it has always been. And `TurnResult::kind` is read at last: a turn answered in one completion, with no steps and no tools, has always arrived as silence, because every line this product draws about a turn comes from events such a turn never emits.
… assembly Three of this release's criteria name a sabotage with no site a test could reach. Two are in `src/main.rs`, which nothing under `tests/` links: whether the effort level is read by each turn or taken from the session — the difference between a posture and a one-shot, and the whole of what `/effort` promises — and whether the driver asks `TurnResult::kind` or infers an answered turn from a step count, which would call a turn cancelled before its first step a conversation. Both are gated by reading the driver as text, which is a weak instrument and the only one available; four gates of the same kind already exist for the same reason. The third was in `provider::build`. Every existing test drives `Chain` directly, and a `build` that folded the chain from the fallback specs alone would produce a perfectly working chain of the operator's second choice. `Probe` implements `WithProvider` so the assembly itself can be asserted: which model the chain asks first, and that every link's host reaches the egress policy. All three were confirmed by executing the sabotage each names.
…e adds The gate refused `src/provider.rs`, and it was right to. It also could not have seen the loop that matters: its three needles were anchored at four spaces, so they caught a loop written at the top level of a function and were blind to one written inside an `impl` — which is where an agent loop would actually be written. The release that introduces this crate's first loop around provider calls is the release that has to fix that, so the needles are unanchored and every loop in a provider-calling file is now seen. Unanchoring immediately refused `src/verify.rs` for three doc comments containing the word "while". A gate that reads prose forbids a file from explaining itself — 0.16.0, 0.19.0, and twice in one wave in 0.25.0 — and the lesson recorded there is that the stripping belongs in the sweep and not only in the exception. It is in the sweep. The chain is exempted by path and held to being a boundary rather than a hole: its loops may iterate nothing but `self.links`, the decision to try another vendor must be io-harness's own `is_retryable`, it still may not read a response, and it must still forward every completion method the trait declares. The count moved rather than being weakened. Subtracting the module's calls one by one is how this held through 0.17.0, and it stops being honest once the file holds a delegating enum and a chain: the subtraction would grow until it cancelled everything, which is a gate going vacuous without going red. So the count is asserted everywhere else — still exactly one, the wizard's handshake — and inside the module every call must be on a decorator's inner provider, a vendor arm or a chain link, counted on squashed text because rustfmt decides where a long call breaks. Both halves were sabotaged: a call on an unnamed receiver, and a predicate of this crate's own instead of the harness's. Each was refused.
… settle The README gains a section on which model a run asks, with the containment disclosure as its own paragraph rather than a footnote: the rules parse, are listed by /config, reach the contract and never fire under containment, and io-cli says so at start only when both are configured. `/effort` and the moved `/profile` are in the commands table and the notes; the provider paragraph now says the chain runs, which failures fall through and which do not. `docs/config.example.toml` gains the routing sub-tables and `conversational`, commented at the density the gates block already uses, and both `tests/docs.rs` and `tests/configure.rs` hold the example and the README to the catalogue. Two live arms, for the two claims a fixture cannot reach. A turn carrying a reasoning level has to complete against a real endpoint, because a field the vendor does not accept is an HTTP 400 rather than a failing comparison. And a real fall-through needs a real transport failure: the head is a `compatible` endpoint pointed at a port nothing is listening on, which spends no credential because the connection never opens, with the real provider underneath it.
Tracing callers before the review reported found this release about to ship `routing::describe` with none. `tests/routing.rs` covered it thoroughly — three properties, the singular/plural, the empty case — and no test can see that nothing calls a function. That is what 0.20.0 shipped seven of behind 1,077 passing tests, and what this release's own risks section calls this codebase's proven blind spot. The disclosure had one call site where it needed three. An operator meets this state at session start, when they open `/config` to edit the rules, and when they type `/contain on` — and the last is the worst of the three, because they begin uncontained, are told nothing because nothing applied, and then move into the one mode where their rules do not fire without a word. `/config` was also promised the sentence by a comment in `src/configure.rs` and by F4 itself, and never printed it. All three now key on `contained` rather than on `containment.is_some()`. The two are equal at startup and stop being equal the moment `/contain off` is typed, which is precisely when the rules start working again — a warning keyed on the configuration would go on being shown to an operator it no longer applies to. A driver-text gate holds all four call sites, since nothing under `tests/` links the binary.
Asserted at two widths, which is 0.25.0's lesson rather than a flourish: at eighty columns a crowded row may not fit a field either way, and a gate that asserts only there is equally true of a status line that never drew the field at all — that release shipped one and found it by sabotage. The wide arm proves it is drawn; the narrow arm proves the row still fits. The absent case is asserted as an absence on both renderers, because a default that drew `effort medium` would put a field on every operator's status line for a release that changed nothing about their turns. Sabotaged by drawing the level with an em dash: the ASCII arm fails, which is what a terminal that cannot render it actually pays for.
Two reviewers, split by area, against a suite that was fully green. Both found the first one independently, which is the strongest signal this gate gives. **A turn refused at the token ceiling was reported as answered.** `TurnKind::Reply` carries a second meaning io-harness documents on the variant itself: a completion that crossed `max_tokens` and was refused rather than served is also a `Reply`, because no run was opened either way. Reading the kind alone told an operator with `[run] max_tokens` set that their refused question had been answered — no answer on screen, no mention of the budget. The guard is io-harness's own, which emits its `Answered` event only for a `Reply` whose outcome is `Finished`. **The resumed run never bought the reasoning the session asked for.** `contract::buying`'s own note counted three `contract::session` callers that build a contract nothing runs; there are five sites, and `resume_pending` drives real completions. `/effort high` applied to every turn except the half of the work an operator came back to finish, while the status line went on saying `effort high`. The gate that should have caught it asked `contains` rather than a count, so one call site satisfied it forever; it counts now. **Typing `/effort high` took the posture off an eighty-column footer.** The level went into the group `row` fits all or nothing, so it displaced the policy and the containment word together — the failure `branch` was moved out of that group for one release ago. It is in the counters now, pushed last, where narrowing takes one field at a time. The startup disclosure used `say`, a one-slot footer notice: it overwrote the containment line it was meant to qualify and died on the first keystroke. Half a routing rule failed to deserialize `[app.io-cli]` **entirely**, which silently reverted the theme, the keys, the ceilings, the capabilities and the verification gate — and `/config` writes one key at a time, so every path through that surface to a routing rule passed through it. Both keys in each rule are optional now and the half rule is refused by name, the shape `gates::Settings::criterion` already uses. The same refusal covers the three values that are writable and disastrous: `failures = 0`, which io-harness reads as "escalate before anything has failed"; `bytes = 0`, which can never be true; and a model written as the empty string. `/effort <typo>` reported instead of refusing, so an operator on `high` trying to spend less was told "every turn asks for high reasoning" and went on paying for it. `/effort OFF` did the same, because the literal match sat in front of a `FromStr` that lowercases for itself. Two tests were the defect rather than the code: the image conjunction asserted `!all` over two links that both took the trait's default of `false`, which every possible implementation satisfies; and the catalogue gate matched a key's last segment, which for two routing keys is `model` and occurs dozens of times in the file it checks.
The first version of this gate picked eighty columns and asserted the standing facts were present there. Which width a field starts costing something depends on the posture string, the glyph set and every counter on the row, so that is arithmetic a test will get wrong — and it did: with the fixture trimmed so the control arm passed, the sabotage that put the level back in the all-or-nothing group sailed through it. The property has no number in it. Setting a level must never remove a standing fact that was on the row without it, at any width. Checked at six, with a guard that at least one of them held a standing fact to begin with — without which the whole loop is satisfied by a row that never carried one.
io-cli 0.26.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 pull request for io-cli 0.26.0.
develop→main, carrying PR #65 and nothing else —git diff --quiet feat/0.26.0 developis clean, so the tree every gate ran on is the tree that ships.What ships
/effort low|medium|high|off— how much reasoning a turn buys, held as a posture across turns and across a/resume, with its absence sending no reasoning field at all./profilemoves to the session to make room, a re-file rather than a widened bound.[app.io-cli.routing]— escalate after consecutive failed gates, downshift while a run has written little. io-harness evaluates the rules; a rule that could only misfire is refused by name. Inert under containment, disclosed at three surfaces, filed upstream./providerhas drawn since 0.21.0. A retryable failure falls through; a bad credential does not. Behaviour change for a file with more than one[[provider]]entry.[app.io-cli] conversationalasks that every prompt open a run.Gates, on this tree
The review
Two adversarial reviewers, split by area, against a fully green suite — ninth consecutive release it has found real defects. Three HIGH, five MEDIUM, one LOW, all fixed; both reviewers independently found the same HIGH. A fourth defect was found by tracing callers: a public function with sixteen tests and no caller anywhere.
io-harness pin unmoved at 0.69 — newest published, satisfied by doing nothing, the twelfth time.
Record sealed and pinned at
sha256:b7d20acc…,ultraship validateexit 0 over 67 checks.