Skip to content

Add the std Pack, state the gateway's API principle, and clear three crates' vibelight debt - #178

Merged
vinniefalco merged 26 commits into
cppalliance:masterfrom
vinniefalco:master
Oct 30, 2026
Merged

vinniefalco merged 26 commits into
cppalliance:masterfrom
vinniefalco:master

Conversation

@vinniefalco

@vinniefalco vinniefalco commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Summary

master carries five changes that landed in sequence. They ship in one pull request because each was read closely while the next was planned, and the later ones are the debt that reading the earlier ones raised.

Part Commits Files What it is
the std Pack 7357dd23e, 08801150f 26 a new plugin crate holding Cursor's six file tools as stubs
the gateway's API principle c73db8f7f 1 crates/gateway/README.md states the rule its wire API follows
the Engine cleanup beb5a43ff through 4eb444630 90 ten steps from a vibelight pass over runvil-internal/engine
the Lua cleanup 0c2477ab2 through 9b4c21a39 136 eight steps from a vibelight pass over runvil-internal/lua
the model-client cleanup 8e763fef2 through 157bc77e9 28 three steps from a vibelight pass over runvil-internal/model-client

24 commits, 238 files, 7203 insertions, 3455 deletions.

The three cleanups are one method applied to three crates: outline the crate with vibelight, raise slop candidates from the outline alone, verify every candidate against the repository and the docs, keep only the ones with a consequence, then deliver a plan whose steps each land as one commit against a green suite. Each plan stays in vibe/ with its findings, its operator answers, and its steps marked completed.

Part 1: the std Pack

This adds plugin-std, the Pack that gives a Runvil agent the file tools every coding agent has, and it copies Cursor's surface rather than inventing one. A model already trained on Cursor's tools then needs no new instructions: it calls Read, Write, StrReplace, Delete, Glob, and Grep, with Cursor's parameter names and Cursor's own descriptions.

Every tool is a stub. A well-formed call answers a text saying the implementation is missing; a call whose arguments miss the schema fails with the parameter named. The stubs exist so the crate, the definitions, the ids, the wire names, and the Lua path are all proven before any file logic lands.

  • crates/plugin-std installs runvil/std, so its Pack id is std and every tool id sits under it: std/read, std/write, std/replace, std/rm, std/glob, std/grep.
  • Lua calls a tool by its id, tools.call("std/read", { path = "/notes.md" }), and offers the Pack with tools.offer("std"). The model calls the same tool by its wire name, Read, which ToolInfo::wire_name sets. The two layers are deliberately separate: the Lua ids stay short and prefixed, and the wire names copy Cursor exactly.
  • Each tool's description and parameter schema are checked in under crates/plugin-std/schemas/ and embedded with include_str!. schemas/EDITS.md records every departure from Cursor's text, per file, each with its reason: Read loses its image and PDF promises because this build reads text only, and three of its sentences stop saying "local filesystem", "the machine", and "the User"; Glob and Grep defaults that said "Cursor workspace root" say the run's filesystem root instead; Grep stops pointing at rg and at the Shell tool, because this Pack offers no shell; Write and StrReplace keep Cursor's parameters, types, and required lists with wording of ours; Delete is kept verbatim.
  • src/check.rs validates a call against the stored schema, honoring type, properties, required, enum, and items. It runs before anything else, so a malformed call cannot reach an operation.
  • A refusal is an InvalidArguments ToolError that names the parameter and, where it helps, the value it received: std_Grep: 'pattern' is required, and the call left it out.
  • A schema keyword the check does not understand is not a reason to refuse a call, and an undeclared parameter passes through, so the tool's own operation decides what it can do with the value.
  • A well-formed call returns ToolOutput::trusted rather than a ToolError, so the success path and the failure path are both exercised end to end.
  • crates/workshop/server/src/agents.rs installs the Pack in every agent run's Host, beside web, user-input, and mcp. The existing test that lists every installed Pack now expects std too.
  • crates/workshop/ui/test/docs-claims.mjs gains the crate to its Plugin doc list, so the wording scan reads its comments for the defined terms and the Pack-activation ban.
  • No new outside dependency: serde and serde_json are already in the workspace.

The stub text names the tool twice and says a repeat call is safe but identical, because a model that reads only "not implemented" may retry and conclude its arguments were wrong.

Nothing here touches the filesystem, so the VFS Mode policy gates nothing yet.

What is deliberately not here

  • Shell. An in-process shell over the same filesystem is a separate Pack, so a run that wants one declares it.
  • Images and PDFs. Read is text only.
  • User approval for mutations. The VFS already answers an approval-required error under Ask mode; wiring it to an operator flow is later work.
  • ListDir. Cursor gates it behind enableDeprecatedLsTool, which defaults to false, and its replacement exists only as proto, so there is no model-facing definition worth copying.

Part 2: the gateway's API principle

crates/gateway/README.md now states the rule that the gateway's wire API follows. The rule says to follow the standard shape, to add the smallest opt-in extension, and to keep working when the extension is absent.

  • The file orders the principle as: the standard rule, extensions, capability discovery, one worked example, the fallback, exclusions, and a check to run before a route or a wire type changes.
  • Each addition must be named in the standard's own namespace, documented beside the standard shape that it extends, and opt-in wherever the protocol offers a way to ask.
  • POST /v1/chat/completions, POST /v1/embeddings, GET /v1/models, POST /v1/audio/speech, POST /v1/audio/transcriptions, and /v1/realtime follow OpenAI shapes, and /v1/rerank follows the Jina and Cohere shape because OpenAI publishes none.
  • The worked extension is a replacement snapshot for realtime transcription, requested through session.include with item.input_audio_transcription.hypothesis, because delta events only append and cannot retract a word.
  • The capability endpoint stays a rule only: the file states that no such endpoint exists yet, and this commit changes no route, wire type, or test.

Part 3: the Engine cleanup

vibe/2026-10/2026-10-28-1-engine-vibelight-cleanup.md is a vibelight pass over crates/runvil-internal/engine. 116 findings were raised, 86 accepted and 30 rejected; each accepted one lands as a step.

Two of the ten steps hold the changes that alter what the code can express. The rest are documentation, plus one visibility narrowing.

The two structural changes

One map for the Pack roster. The run context and the shared tool set each kept two maps keyed by Pack name: one recorded whether a Pack could serve the run, and the other the vendor of every Pack that could. Nothing held them in step, so a Pack could carry a vendor while also being refused, and the second map was only ever the usable subset of the first. Both now hold a single map whose entry says either that a Pack can serve, with its vendor, or that it cannot, with the reason. RosterEntry replaces the bare reason, so the disagreement has no shape left to take. vendors is gone with nothing in its place: no field, no constructor argument, no accessor.

Infallible VM access. SectionContext::vm was an Option whose None no live frame could reach, so five error branches promised a failure that could not happen. Only a frame's own destruction clears the slot, so live_vm is now the single read and the accessor hands the VM back directly. Twelve scheduler call sites drop a question mark. Section setup also receives the argument table already marked writable or frozen, in place of a separate optional value and a writable flag, so the mode cannot travel apart from the value it qualifies and nothing can turn a frozen table writable after construction.

The remaining steps

  • Root starts. start_root_chain fills the root lineage and root call depth itself and enqueues the chain before returning, so start_h1, end_h1, and start_root_walk lose their own pushes and no caller can set the two root constants. The admission records that track a chain giving its slots back are named for what the slots do rather than for the event that produced them, and ChainOps drops its copy of the run context.
  • One producer for the Pack order. A section's render is now asked about exactly the Packs the section stored. SetEvidence splits the render's asked and answered Packs away from the rules that name tools, so a tool-shaped rule can never be built with an empty tool list.
  • One owner for the empty-reply message. The Engine's scripted model and the gateway reader kept private copies under a "change together" note, and the copies had already diverged on whitespace-only reasoning. CompletionError::empty_reply is now the one builder, and it settles the disagreement: a reasoning channel holding only whitespace counts as absent.
  • Chat effects. Effect::Chat carried a second copy of the completion settings the binding beside it already fixes, so a hand-built effect could send one request while its record logged another. The effect keeps only the binding, and every site that performs a round asks the binding for its settings as it sends.
  • Documentation. The Engine's doc comments now describe what its error surface, its run bindings, and its effect records actually do. Three modules that re-exported their dependencies state that as a rule and name the groups that still reach those dependencies directly, and the re-export sets gain the two items callers were reaching past them for. README.md gains the fifth effect kind and scopes the file case to outside a prelude's install.

Part 4: the Lua cleanup

vibe/2026-10-29-1-lua-slop-cleanup.md is the same method applied to crates/runvil-internal/lua, the largest Engine crate at 135 files and 28,852 lines. 130 findings were raised, 15 rejected, 11 duplicates and 6 same-cause findings merged, and 98 kept: 86 cheap and 12 substantial. Both operator decisions were answered in favor.

The goal was one crate that names each fact once, in the file that owns it, with no doc contradicting its own body, and no author-visible behavior change. Every Lua global, table shape, and message the guide documents stays as documented.

What the twelve substantial findings changed

  • The instruction quota is gone. HOOK_BUDGET was u64::MAX, so the quota could never trip, and the guide states three times that no instruction limit exists. InstructionBudget became CancelHook, named for the one job the hook still performs, and the firing counter went with it.
  • A refusal cannot skip its event. prepare_dispatch took an Option publicly and its None arm fired no ToolResult at all, so a caller could report nothing. It now takes the report it must produce and keeps an Option body private, which makes the silent path unrepresentable.
  • Answer carries one failure side. Every variant wrapped its own Result while the failure was identical for all of them. The failure side moved off the variants.
  • TaskStatus.ok is gone. It restated what the phase already decided for every phase but Done.
  • The compacting flag and its snapshot became two phases. They were hand-paired across five sites with one arm unreachable, and the snapshot outlives the flag by design, so merging them into one would have dropped the retry state.
  • sys_live is not an Option. The typestate already guarantees injection before every reader, so the mirror is built at injection and every dead None arm is gone.
  • The counts API cannot be misused. increment used to error on an unseeded id, so every dispatch site had to seed first and the error arm was unreachable; counts now seed on increment.
  • Offers and ModelSelection name what their locks hold. SectionOffers was the working name, but the Engine already owns a type by that name at the crate seam.
  • Request::ToolCall lost its third state. It paired two Options whose only other combination existed for a test hook.
  • attach_rendered_tools returns the tools it attached, so its Engine caller stops re-reading a slot it just filled.
  • ModelReport names its turn turn, not script.

The rest

The remaining steps are renaming, placement, and documentation: splitting handles and sys, one walkable read-only proxy, one coroutine shim installer, the parse-module names, the protocol docs, the store and key-order modules, and the Error::Lua doc rule stated once across 29 # Errors sections.

Part 5: the model-client cleanup

vibe/2026-10-29-2-model-client-slop.md covers crates/runvil-internal/model-client. 21 findings were accepted, 2 rejected, 18 cheap and 3 substantial, and no operator decision was needed.

  • Tool arguments are a map, not a value. ToolCall::arguments was a serde_json::Value documented as always a JSON object, yet the view type modeled a null payload with an Option and a Value::Null arm reachable only from a hand-built literal in a test. The field now holds a Map, and the object check returns the map it proved, so no constructor has to unwrap and no site parses twice.
  • CompletionOptions holds one ModelInvocation. It kept its own copies of the three request settings, with its own getters and setters, and a binding copied the three across by hand, so a missed copy would compile and silently send nothing.
  • A binding's provider comes from its catalog. provider() read only its own field, so a binding filled from a catalog reported no provider while render_info() reported the catalog's. One expression now answers for both readers, and the Engine's now-redundant copy call is gone.
  • Five surface members are gone. ModelSet::from_parts had eleven callers and every one was a test; ModelView::bindings was the only read that cloned a whole list and no Engine path called it; TryFrom<f64> for Temperature was a second spelling of new that nothing used; ModelView::default shadowed Default::default where ModelSet calls the same fact default_alias; and five of six runvil_types re-exports kept paths nothing names resolving.

Facade impact

crates/runvil/public-api.txt changes by six lines:

+pub fn runvil::model::CompletionError::empty_reply(reasoning_present: bool) -> runvil::model::CompletionError
+pub fn runvil::model::CompletionOptions::invocation(&self) -> &runvil::model::ModelInvocation
+pub fn runvil::model::CompletionOptions::with_invocation(self, invocation: runvil::model::ModelInvocation) -> runvil::model::CompletionOptions
-pub runvil::effect::Effect::Chat::options: runvil::model::CompletionOptions
-impl core::convert::TryFrom<f64> for runvil::model::Temperature
-impl core::convert::TryFrom<f64> for runvil::model::Temperature { type Error = runvil::model::TemperatureError }

Effect::Chat loses a field, Temperature loses a conversion, and CompletionError and CompletionOptions each gain published members. Every crate here is publish = false, so none of it is a semver commitment.

Breaking changes

None outside crates/runvil/public-api.txt, which is checked against the code and updated in this pull request. A Plugin crate is not part of the Engine's facade, the std Pack touches no existing type, and the Engine cleanup's one cross-crate interface move is RosterEntry, which replaces a bare String inside an unpublished crate. The Lua and model-client cleanups change no author-visible behavior.

Test plan

Results as the gates report them at 157bc77e9:

  • workspace nextest: 5615 tests run, 5615 passed, 76 skipped
  • the Workshop partition: 308 tests run, 308 passed, 4 skipped
  • the Engine and Lua components: 1624 tests run, 1624 passed
  • harness-gateway-client: 431 tests run, 431 passed, 2 skipped
  • plugin-std: 32 tests, covering the definitions, the argument check, the PackInfo and install contract, and every stub answer
  • the facade gate: build-xtask api --check reports 0 violations and the listing matches public-api.txt
  • both clippy runs with warnings denied, plus the headless cargo check -p gateway --no-default-features
  • cargo fmt --all --check
  • the workspace rustdoc gate with warnings denied, and the facade docs build
  • cargo test -p build-xtask for the boundary and structural checks
  • the docs-claims wording scan
  • cargo deny check and cargo audit

The plans

Five plans ship with the change and use the same contract format as the rest of vibe/:

  • vibe/2026-10-27-2-plugin-std-file-tools.md records the std Pack's shipped work as its first step, then specifies what is left: one schema correction, the six real operations over the call's VFS access, approval, and the shell.
  • vibe/2026-10/2026-10-28-1-engine-vibelight-cleanup.md carries the Engine findings, the operator answers, and all ten steps, each marked completed.
  • vibe/2026-10-29-1-lua-slop-cleanup.md carries the Lua findings, the operator answers, and all eight steps, each marked completed.
  • vibe/2026-10-29-2-model-client-slop.md carries the model-client findings and all three steps, each marked completed.
  • vibe/2026-10-27-1-pr172-debt-removal.md, from the earlier pass this one builds on.

A note on the runvil-docs steps

Three of the cleanup steps fix guide sentences, and they live in runvil-docs because that checkout is a separate repository. Its commits, 4f537d5, b6c0d90, and faacbf9, are pushed to its master and need their own pull request. They are not part of this one.

Run vibelight from the repo root with cargo vibelight. The alias runs the sibling vibelight crate in release mode, so cargo builds it on first use and rebuilds it when its source changes.

- The path ../vibelight/Cargo.toml is relative to the cwd. The alias works only from the repo root, and only with vibelight checked out next to this repo.

- Nothing is tested. The alias was not run.
Every tool answers a stub that says the implementation is missing, and a call whose arguments miss the schema fails with the parameter named.

- `crates/plugin-std` offers `std/read`, `std/write`, `std/replace`, `std/rm`, `std/glob`, and `std/grep` under the wire names `Read`, `Write`, `StrReplace`, `Delete`, `Glob`, and `Grep`.
- Each tool's description and parameter schema are checked in under `schemas/` and embedded with `include_str!`, so the model-facing text is Cursor's own; `schemas/EDITS.md` records every line changed from it.
- The stub answer is a successful `ToolOutput::trusted` rather than a `ToolError`, so a well-formed call and a malformed one exercise different paths.
- `crates/workshop/server/src/agents.rs` installs the Pack in every agent run's Host beside `web`, `user-input`, and `mcp`.
- No operation touches the filesystem yet, so the VFS Mode policy gates nothing until the tools are real.
`crates/gateway/README.md` now states the rule that the gateway's wire API follows. The rule says to follow the standard shape, to add the smallest opt-in extension, and to keep working when the extension is absent.

- The file orders the principle as: the standard rule, extensions, capability discovery, one worked example, the fallback, exclusions, and a check to run before a route or a wire type changes.
- Each addition must be named in the standard's own namespace, documented beside the standard shape that it extends, and opt-in wherever the protocol offers a way to ask.
- `POST /v1/chat/completions`, `POST /v1/embeddings`, `GET /v1/models`, `POST /v1/audio/speech`, `POST /v1/audio/transcriptions`, and `/v1/realtime` follow OpenAI shapes, and `/v1/rerank` follows the Jina and Cohere shape because OpenAI publishes none.
- The worked extension is a replacement snapshot for realtime transcription, requested through `session.include` with `item.input_audio_transcription.hypothesis`, because `delta` events only append and cannot retract a word.
- The capability endpoint stays a rule only: the file states that no such endpoint exists yet, and this commit changes no route, wire type, or test.
The run context and the shared tool set each kept two maps keyed by Pack name: one recorded whether a Pack could serve the run, and the other the vendor of every Pack that could. Nothing held them in step, so a Pack could carry a vendor while also being refused, and the second map was only ever the usable subset of the first. Both now hold a single map whose entry says either that a Pack can serve, with its vendor, or that it cannot, with the reason, so the disagreement has no shape left to take. A Pack named in both the usable and the unavailable lists still counts as unavailable.

- `RosterEntry` replaces the bare reason in the roster value with two variants, one carrying the vendor and one the reason, so a Pack can no longer be usable and refused at once. It is exported from the Lua crate and re-exported by the engine's import surface, which is this change's one cross-crate interface move.
- `roster_for_test` is a public accessor added beside the test-support helpers so a test in another crate can read the one map, which itself stays visible only inside the crate that declares it.
- `packs` replaces the pair on the run context with one entry per roster Pack, filled so that a name in both lists keeps the unusable entry: that Pack records no vendor and reports the reason it was handed, and the setter keeps its signature.
- `vendor_for` reads the vendor off the Pack's entry, so an unavailable Pack answers nothing because its entry holds no vendor rather than because a second map left it out.
- `vendors` is gone with nothing in its place: no field, no constructor argument, no accessor, and the run context's debug rendering lists the one map where it listed two.

Design: new value-object @ crates/runvil-internal/lua/src/handles.rs::RosterEntry
  boundary: pub
Plan: vibe/2026-10/2026-10-28-1-engine-vibelight-cleanup.md
Chat effects carried a second copy of the completion settings that the binding beside them already fixes, so a hand-built effect could send one request while its record logged another. The effect now keeps only the binding, and every site that performs a round asks the binding for the settings as it sends. The request and the record therefore come from one source, and the mismatch can no longer be written down.

- `Effect::Chat` drops its completion-settings field, which leaves the binding as the one place a round's settings live for both the sender and the record.
- `CompletionOptions` leaves the engine model module's re-export list, so the test that still names it imports it from the model client directly.
- `crates/runvil/public-api.txt` loses the field's published line, so the effect's exposed shape shrinks by that one member.
- `binding.completion_options()` is called at each performing site in place of the field, so the settings reach the chat call rebuilt from the same binding the round records.
- `crates/runvil-internal/engine/src/execute/run/effect-tests.rs` changes only by dropping its helper's field line, and no record assertion moved, which is what shows the logged shape stayed put.

Plan: vibe/2026-10/2026-10-28-1-engine-vibelight-cleanup.md
Section accessors no longer report a missing virtual machine that live code cannot observe: only a frame's own destruction clears the slot, so they hand it back directly and the scheduler loses error checks that could never fire. Section setup now receives the argument table already marked writable or frozen, so the single pass allowed to rewrite it picks that privilege where the frame is built rather than flipping a flag on a finished structure afterward.

- `live_vm` is the only read of the frame's VM slot, so the invariant that only the frame's own drop clears it is stated once instead of restated at each accessor.
- `argv` becomes one typed field on the setup struct in place of a separate optional value and a writable flag, so the mode cannot travel apart from the value it qualifies.
- `vm_setup` builds that mode itself from a new flag parameter, so the privileged construction passes true where the frame is built rather than mutating the struct after construction.
- `vm()` no longer returns a result, so twelve scheduler call sites drop the question mark and the internal-error arm a live frame could never raise is gone from those paths.
- `vm_setup_hands_the_vm_the_runs_argv_in_the_mode_the_caller_asks_for` pins both modes against the run's own derived argument table, so a frozen section and the privileged one are each observable at the boundary.
- `fully_bound_vm` takes the typed mode, so the global-surface tests cover frozen and writable by naming each and their failure messages report the mode rather than a boolean.
- `argv_writable` no longer exists as a setup field, so no caller can turn a frozen argument table writable after construction.

Design: new pure-function @ crates/runvil-internal/engine/src/execute/section_context.rs::live_vm [deps: Option<&SectionVm>]
Design: extends parameter-object @ crates/runvil-internal/engine/src/execute/section_vm.rs::SectionVmSetup
Design: new flag-parameter @ crates/runvil-internal/engine/src/execute/context.rs::vm_setup
Plan: vibe/2026-10/2026-10-28-1-engine-vibelight-cleanup.md
A section's render is now asked about exactly the Packs the section stored, so that order has one producer instead of two. A broken merge rule now carries evidence shaped to the rule itself: asked and answered Packs for the render rule, and at least one tool for the rules that name tools. Scope errors call the list what it is, the section's own aliases, and mention the discovery route only when the section actually offers one.

- `SetEvidence` splits the render's asked and answered Packs away from the rules that name tools, so a tool-shaped rule can never be built with an empty tool list. `SetRule` keeps only the rules that name tools.
- `offered_packs` takes one slot per stored Pack up front and fills the sets while walking the tools, which deletes the local list and the sort that produced the same order twice.
- `NonEmpty::new` is now public and compiled outside test builds, because the engine crate supplies the rule and the first tool that broke it.
- `out_of_scope_message` renders the scope text and receives the section's Dynamic Set as a passed fact rather than reading it back out of the alias list, since a catalog tool can wear a discovery wire name while no Dynamic Set holds a tool.
- `static_aliases` replaces the old in-scope field name, and the message tail now reads "aliases this section offers", so the text no longer claims the list is complete.

Design: new pure-function @ crates/runvil-internal/engine/src/error.rs::out_of_scope_message deps: &[String],&str,bool
Design: new parameter-object @ crates/runvil-internal/engine/src/error/sets.rs::SetEvidence
Design: new surface-growth @ crates/runvil-internal/lua/src/protocol/non_empty.rs::NonEmpty::new
  boundary: pub
Repairs: a render is asked about exactly the stored Pack order @ crates/runvil-internal/engine/src/execute/scope.rs::offered_packs - a render was asked for a Pack outside that order, sorted ahead of the stored Packs
Plan: vibe/2026-10/2026-10-28-1-engine-vibelight-cleanup.md
Every place that started a chain at the root of a run set the same two identity values by hand and then had to remember to put that chain on the ready queue, so three separate sites carried the same obligation. One constructor now owns both, and a root start is already on the queue when it returns. The admission records that track a chain giving its slots back are now named for what the slots do rather than for the event that produced it, so a reader no longer meets three names for one transition and a field whose name says the opposite. The one-chain view stopped holding a second reference to the run context, so the prompt it hands out is the one its own chain reads. The rest of the change is the scheduler saying out loud rules it already enforced, including which teardown paths stay silent and where the first walked entry is defined.

- `start_root_chain` fills the root lineage and root call depth itself and enqueues the chain before returning, so `start_h1`, `end_h1`, and `start_root_walk` lose their own pushes and no caller can set the two root constants.
- `take_back`, `drop_aborted`, and the `gave_back` map name what happens to a chain's slots rather than the event that caused it, and every reader, abort path, and admission test moves with the new names.
- `ChainOps` drops its copy of the run context, so the prompt the view hands out is read from the chain being viewed rather than from the scheduler beside it.
- `live_author_tasks` and `live_all` name the two task listings, one without internal timers and one with them, so a caller no longer reads a listing whose filter it has to guess.
- `end_h1` passes the pass's concurrency into the root start, so the walk still inherits a limit that a section lowered through `tasks.concurrency`.
- `Chain::counters` is the one normative statement of the first walked entry, and the pass, the walk, and the chain record defer to that field instead of restating it.
- `start_h1` returns no chain index, so a caller cannot enqueue a chain the constructor already queued.

Design: new encapsulated-invariant @ crates/runvil-internal/engine/src/execute/scheduler/chain.rs::Scheduler::start_root_chain
  instead-of: temporal-coupling: a root start that is only ready after its caller pushes it
Plan: vibe/2026-10/2026-10-28-1-engine-vibelight-cleanup.md
The message for a turn that carries no text and no tool calls is written in one place now, so the engine's scripted model and the gateway reader no longer keep private copies that had to change together. The shared builder also settles a disagreement between them: a reasoning channel holding only whitespace counts as absent, so the specific the gateway would never have written no longer appears. That builder is a new method on a type the runvil facade publishes, which a prior pass declined for that reason alone before the operator approved it. Test-support helpers that only in-crate suites use are now crate-private, which leaves the bench to prove by compilation that nothing it names was narrowed.

- `CompletionError::empty_reply` builds the empty-reply failure from the kind's phrase and appends the reasoning-ignored specific only when a reasoning channel was dropped, leaving the backend's stop label to the caller that knows it. `public-api.txt` gains the line, so the facade records the addition as published surface.
- `crates/harness-gateway-client/src/wire/parse.rs` deletes its own wording constant and local builder and calls the shared one instead, so the two readers have no pair left to keep in step.
- `crates/runvil-internal/engine/src/test_support/scripted-chat.rs` narrows its reply builders, request accessors, and recorded-call type to crate-private, since only the in-crate suites reach them. The chat type, the reply type with its variant fields, its constructor, and the entry points and counters around it stay public, because the bench names exactly those.
- `crates/runvil-internal/engine/src/test_support.rs` expects the dead-code lint for that module outside test builds and spells out in the reason which items the bench reaches, so the narrowing is not reported as unused in the build the bench compiles against.
- `ScriptedReply` treats a whitespace-only reasoning channel as absent when it answers an empty text reply, so the message carries the plain phrase rather than a specific that the gateway reader, which already trimmed, would never write. Its module also loses the private constant, the local builder, and the doc sentence promising that the two copies change together.
- `ScriptedReply::Failure` loses its `finish_reason` field together with the block that copied it, and the constructor here stops setting it, so the failure variant keeps only what a scripted failure actually names.

Design: new surface-growth @ crates/runvil-internal/model-client/src/model/error.rs::CompletionError::empty_reply
  boundary: pub
Repairs: an empty reply names a reasoning channel only when it carried text @ crates/runvil-internal/engine/src/test_support/scripted-chat.rs::ScriptedReply::answer - a whitespace-only reasoning channel appended the reasoning-ignored specific
Plan: vibe/2026-10/2026-10-28-1-engine-vibelight-cleanup.md
The engine's doc comments now describe what its error surface, its run bindings, and its effect records actually do. Several sentences named a cause, a model map, or a quota that the code beside them does not have, so each was rewritten from the implementation it sits on. One accessor also narrowed to its parent module. Run behavior is unchanged.

- `limits` narrows from `pub(crate)` to `pub(super)`, the only visibility token this change touches.
- `RunResult` now says why it alone is non-exhaustive: the caller consumes an outcome as a result, so a wildcard arm that treats an unfamiliar outcome as a failure is a sound default, while the closed `Step`, `Effect`, and `EffectAnswer` must break the caller's build when a kind is added.
- `originating_error` documents the eight variants that can answer a cause, and records that the transparent `Completion` and `Substitution` answer none.
- `model` documents that an absent entry means no role is bound, so a catalog model a UI snapshot reaches by raw id is absent from the map too.
- `Parse` drops its compiled-Lua claim, which `Lua` now names among the six authoring failures that land there.

Plan: vibe/2026-10/2026-10-28-1-engine-vibelight-cleanup.md
Three modules that re-exported their dependencies now state that as a rule and name the groups that still reach those dependencies directly. The re-export sets gain the two items callers were reaching past them for, and the touched modules now import through the surfaces, so a forgotten re-export becomes a compile error rather than a silent divergence. Section documentation loses claims the code contradicts: a derive note that named two blocked traits where only one is, a fallback promise a debug assertion rules out, a setup sequence called fixed that omitted a step it performs, and an error list that restated the variants it summarized. A codec sentence that sent a transport to a crate without one now names the crate that has it.

- `crate::model`, `crate::lua`, and `crate::parser` each state that production code names the surface and not the dependency, and each names its own exceptions by group instead of asserting completeness.
- `runvil_model_client::detail` and `runvil_parser::detail` are re-exported whole, since their helpers are the dependencies' own algorithms, called where the rule each implements is documented.
- `ModelSetError` and `ModelKeyword` join the re-export sets, so a module naming either through its surface now finds it there.
- `error::convert` and `error::value` are named as the error bridge, which is the one production exception the `lua` and `parser` surfaces carry beside the test-only ones.
- `debug_assert!` is now the stated contract on the visible-set constructor, which drops the fallback promise, and the two error paragraphs point at `not_found`, which owns the not-found rule.

Design: extends facade @ crates/runvil-internal/engine/src/model.rs
Design: extends facade @ crates/runvil-internal/engine/src/lua.rs
Design: extends facade @ crates/runvil-internal/engine/src/parser.rs
Plan: vibe/2026-10/2026-10-28-1-engine-vibelight-cleanup.md
Two documentation sentences now agree with the behavior they describe. The Engine request list gains the Pack tool render and the catalog tool result in place of the single tool result it carried, and it scopes the file case to outside a prelude's install, so the list names five kinds where it named four. The store-access rule now records the section install derivation as a second decision site, so a reader sees both places the executor fixes store scope.

- `README.md`: the effect list goes from four entries to five, and the tool result it named is split into a Pack's render of the tools a section offers and a catalog tool's result.
- `crates/runvil-internal/engine/AGENTS.md`: the store-scope sentence names section-VM install for the section's store table, so the executor's derivation is stated for dispatch and for install rather than for dispatch alone.

Plan: vibe/2026-10/2026-10-28-1-engine-vibelight-cleanup.md
Plan: vibe/2026-10/2026-10-28-1-engine-vibelight-cleanup.md
@vinniefalco vinniefalco changed the title Add the std Pack with Cursor's six file tools Add the std Pack, state the gateway's API principle, and clear the Engine's vibelight debt Oct 29, 2026
Error documentation in the Lua crate now matches what the code produces: failures wrapped from the Lua runtime are documented as runtime errors, and the error module, error kind, and store error field docs now say what those types carry, with the store field list written once. The instruction quota is deleted because its trip point was effectively unreachable and no instruction ceiling applies to a block. The remaining hook only polls for cancellation, so its names now say that and its firing counter goes. Two identical author refusal helpers become one shared function, the writer of the conversation slot gets a name distinct from the installer that calls it, and the JSON encoder calls its message prefix a prefix. Log quota messages and author refusal text read exactly as before.

- `CancelHook` replaces `InstructionBudget` and holds only the run's set-once cancel flag, which the main-state hook, every block coroutine's hook, and the shim's cancel checks share. `install_cancel_hook`, `cancel_poll`, and the `cancel_hook` field take over from the budget-named function, hook builder, and field.
- `QuotaResource::Instruction` leaves the public enum along with its display arm, so the engine and protocol tests that sampled it now use `QuotaResource::LogEvent`.
- `author_refusal` is one free function in the error module, replacing the identical private `refusal` copies in the JSON and messages modules; every call in those modules now goes through it.
- `set_messages` is the new name of the guard-slot writer in the globals module, so it no longer shares a name with the `install_messages` that calls it.
- `Error::LuaRuntime` now appears in 26 `# Errors` sections that named `Error::Lua`, the variant `Error::lua` and `Error::shared` actually build. `argv_json`, `var_snapshot_table`, and `var_to_json` now name each variant by the failure behind it.
- `QuotaExceeded` displays the same two log budget messages as before, now written inline in its match rather than read from `lua_quota`.
- `author_refusal` raises an external `Error::Lua`, the same value both old copies raised, so `pcall` still reports kind `lua`.
- `HOOK_BUDGET` and the hook's trip branch are gone, so the hook ends a block only when the cancel flag is set.
- `hook_firings` and the firing counter behind it are gone, and the two cancellation tests drop the assertions that a cancelled firing goes uncounted.

Design: replaces shared-mutable-state @ crates/runvil-internal/lua/src/hardening.rs::CancelHook was: crates/runvil-internal/lua/src/hardening.rs::InstructionBudget
Design: removes surface-growth @ crates/runvil-internal/lua/src/error.rs::QuotaResource::Instruction boundary: pub
Design: replaces pure-function @ crates/runvil-internal/lua/src/error.rs::author_refusal deps: String was: crates/runvil-internal/lua/src/json.rs::refusal
Design: replaces pure-function @ crates/runvil-internal/lua/src/error.rs::author_refusal deps: String was: crates/runvil-internal/lua/src/messages.rs::refusal
Deferred: Renaming `Error::lua` to `lua_runtime` stays out of this change.
Plan: vibe/2026-10-29-1-lua-slop-cleanup.md
Tool call counting now seeds an unseen tool on its first increment, so dispatch no longer seeds it in a separate call and takes the lock once. The section's offers and model selection, the static set reader, the tool id decoder, the run's tool set, and the store conflict slot get names that say what they hold. Attaching the frozen tool sets now hands them back, which removes an error path that could never fire. The docs now say the sets freeze at the section's first loop round rather than its first model call, since a one-shot inference freezes nothing.

- `Offers` replaces `ToolRuntime` and `ModelSelection` replaces `ModelRuntime`, with the `SectionVm` fields, accessors, and lock poison messages renamed to match what each lock holds. The name `SectionOffers` was passed over because the engine already owns a type by that name.
- `pub(crate) offers` takes the offer list out of the public surface, so code outside the crate cannot break the rule that each offer is held once.
- `attach_rendered_tools` now returns the sets the list holds and is `#[must_use]`, so the first loop round uses the returned sets instead of re-reading them through an internal error that could not fire. Callers that drop the result bind it with `let _ =`.
- `current_static_set` replaces `current_tool_bindings`, since it returns the static offers rather than bindings, and `tool_id` replaces `tool_name`, since it returns a tool id rather than a wire name.
- `store_conflict` replaces `store_conflicts`, because the slot holds one conflict that a later one overwrites, and `tool_set` replaces `bound_tools` for the run's shared tool set.
- `increment` now seeds a missing id at 0 and counts it under one lock, so the three engine dispatch paths no longer call `ensure` first. `ensure` stays for pre-seeding ids that `tools.stats` should list before any call.
- `increment_seeds_a_missing_id_and_counts_it` pins the new seeding, and the attach test now checks with `Arc::ptr_eq` that a second attach hands back the sets the first one left in place.
- `models.infer` is now documented as freezing nothing, and every doc that said the first model call freezes the sets now says the first loop round does.
- `rendered` is deleted, and its callers read `rendered_tools().is_some()` instead, so one accessor reads the frozen sets.

Design: removes temporal-coupling @ crates/runvil-internal/lua/src/scope.rs::ToolCallCounts::increment boundary: pub
Design: removes temporal-coupling @ crates/runvil-internal/lua/src/messages.rs::MessageList::attach_rendered_tools boundary: pub
Design: removes surface-growth @ crates/runvil-internal/lua/src/scope.rs::Offers::offers boundary: pub
Design: replaces shared-mutable-state @ crates/runvil-internal/lua/src/scope.rs::Offers boundary: pub was: crates/runvil-internal/lua/src/scope.rs::ToolRuntime
Design: replaces shared-mutable-state @ crates/runvil-internal/lua/src/models.rs::ModelSelection boundary: pub was: crates/runvil-internal/lua/src/models.rs::ModelRuntime
Design: replaces surface-growth @ crates/runvil-internal/lua/src/vm.rs::SectionVm::offers boundary: pub was: crates/runvil-internal/lua/src/vm.rs::SectionVm::tool_runtime
Design: replaces surface-growth @ crates/runvil-internal/lua/src/vm.rs::SectionVm::model_selection boundary: pub was: crates/runvil-internal/lua/src/vm.rs::SectionVm::model_runtime
Design: replaces shared-mutable-state @ crates/runvil-internal/lua/src/vm.rs::SectionVm::store_conflict was: crates/runvil-internal/lua/src/vm.rs::SectionVm::store_conflicts
Design: replaces shared-parameter-cluster @ crates/runvil-internal/lua/src/vm/state.rs::current_static_set deps: AdhocTools,Mutex<Offers>,ToolSet boundary: pub was: crates/runvil-internal/lua/src/vm/state.rs::current_tool_bindings
Design: replaces pure-function @ crates/runvil-internal/lua/src/tools/decode.rs::tool_id deps: Value was: crates/runvil-internal/lua/src/tools/decode.rs::tool_name
Deferred: The engine's `bound_tool_set` keeps its name.
Plan: vibe/2026-10-29-1-lua-slop-cleanup.md
The module that held tool bindings also held block results, section targets, prompt-read text, and library sections, and the module for the sealed system table also held the guarded variable proxy. Each now splits into modules named for what they hold, and the crate root re-exports every public item under its old path. One walkable read-only proxy builder now serves both the tool stats tables and the Pack list, so a hardening change reaches both. The public tool dispatch entry point now requires a script report, so no caller outside the crate can reach the path that fires no tool result event, and the model report holds its turn directly. The dispatch docs and the crate's agent notes now say that the executor counts each call when it issues it.

- `crate::bindings` takes `ToolOutputKind`, `ToolBinding`, `wire_name_of`, `RosterEntry`, and `ToolSet` from `handles`, and its tests move with it as `bindings-tests.rs`. It still imports `DiscoveryTool` from `scope` while `scope` imports `ToolBinding` from it, so the module cycle moves with the file.
- `crate::section` takes `LuaBlockResult`, `resolve_section_target`, `SectionText`, `LibraryKey`, `Library`, and `LibraryBlock`, the items the VM exchanges with its caller about sections.
- `crate::var` takes the guarded `var` proxy, its three registry keys, and its builders and readers unchanged, leaving `crate::sys` with `seal_sys`.
- `pub use crate::section` and `pub use crate::bindings` replace the crate root's `handles` re-export under the same names, so no public path changes.
- `read_only_walk` moves from a private helper in the stats module into `crate::proxy`, and `tools.stats`, its views, and `tools.packs` all build through it. It keeps the `mlua::Error::external` refusal, so `tools.packs` refuses writes and labels its metatable with the same text as before.
- `prepare_dispatch` takes a `ScriptReport` instead of an `Option` and forwards to the crate-private `dispatch_body` with `Some(script)`. Only `prepare_model_dispatch` and one test in each of `dispatch-tests.rs` and `tests-parameters.rs` pass `None`, so no public caller can reach the path that fires no `ToolResult`.
- `dispatch_body` repeats the six leading parameters of `prepare_dispatch` and `prepare_model_dispatch`, so that tuple now spans three signatures in the module.
- `ModelReport` holds `turn: u32` directly instead of a nested `ScriptReport`, and the engine builds it from `call.report.turn`.
- `install_pack_list` sets `__len` on the metatable `read_only_walk` returns, and returns `Error::Internal` if the proxy comes back without one.
- `ToolBinding` documents `id` as the tool's id rather than a selected live identity, since `from_info` copies the catalog id.
- `AGENTS.md` now says `prepare_dispatch` applies the succeeded/failed observation rather than counts, that the executor counts each call when it issues it, and that adhoc and discovery tools report in the executor.
- `PACKS_LABEL`, the stats module's private `proxy`, and the inline read-only metatable code in `install_pack_list` are gone.

Design: replaces cyclic-dependency @ crates/runvil-internal/lua/src/bindings.rs was: crates/runvil-internal/lua/src/handles.rs
Design: replaces value-object @ crates/runvil-internal/lua/src/bindings.rs::ToolBinding boundary: pub was: crates/runvil-internal/lua/src/handles.rs::ToolBinding
Design: replaces value-object @ crates/runvil-internal/lua/src/bindings.rs::RosterEntry boundary: pub was: crates/runvil-internal/lua/src/handles.rs::RosterEntry
Design: replaces pure-function @ crates/runvil-internal/lua/src/bindings.rs::wire_name_of deps: ToolInfo boundary: pub was: crates/runvil-internal/lua/src/handles.rs::wire_name_of
Design: replaces value-object @ crates/runvil-internal/lua/src/section.rs::LuaBlockResult boundary: pub was: crates/runvil-internal/lua/src/handles.rs::LuaBlockResult
Design: replaces pure-function @ crates/runvil-internal/lua/src/section.rs::resolve_section_target deps: Value was: crates/runvil-internal/lua/src/handles.rs::resolve_section_target
Design: replaces value-object @ crates/runvil-internal/lua/src/section.rs::SectionText boundary: pub was: crates/runvil-internal/lua/src/handles.rs::SectionText
Design: replaces newtype @ crates/runvil-internal/lua/src/section.rs::LibraryKey boundary: pub was: crates/runvil-internal/lua/src/handles.rs::LibraryKey
Design: replaces value-object @ crates/runvil-internal/lua/src/section.rs::LibraryBlock boundary: pub was: crates/runvil-internal/lua/src/handles.rs::LibraryBlock
Design: extends shared-parameter-cluster @ crates/runvil-internal/lua/src/dispatch.rs::dispatch_body deps: Emitter,GuardNonce,Option<&str>,Option<ScriptReport>,Result<ToolOutput,ToolError>,ToolBinding,str
Design: extends facade @ crates/runvil-internal/lua/src/lib.rs boundary: pub
Plan: vibe/2026-10-29-1-lua-slop-cleanup.md
A dispatched request's answer now holds only its success value, and a failure travels as the error side of a result around it, so failure sites no longer pick a request-specific variant just to wrap an error. A task's status phase now carries whether a finished task succeeded, so the reported outcome cannot disagree with the phase. The yield parser's refusal case gets a name that no longer collides with the call request, its read helpers give way to direct raw reads, and per-record validation moves beside the record types it builds. Scripts see no change: every failure still resumes as false plus an error table, every success resumes with the same values, and the task status table reads as before, while the protocol docs are corrected to match the code.

- `Answer<E>` holds only success values, one per request: `Note` and `Cancel` become unit variants, and only `JoinAny` still carries an `E`, inside `TaskDelivery<E>`, because a member's failure is part of a successful wait. The per-variant results and `map_error` are gone.
- `render_envelope` replaces the `render` method as a crate-internal free function over `Result<Answer<E>, E>`: an `Err` renders as `(false, table)` with the typed error retained, and the success arms match the old ones. `resume_block_coro_answer`, `answer_inline`, `wake_from_wait`, `deliver`, and `fill_batch_slot` take the same result shape.
- `YieldParse::Refused` replaces `YieldParse::Call` and holds a bare `Error`, and `FieldFailure::Argument` replaces `FieldFailure::Call`, so neither name collides with the `call` request. `classify` loses its per-op closure, and the enum takes a `clippy::large_enum_variant` expectation, since a parse lives only until the driver matches it.
- `TaskPhase::Done` now holds `ok`, and `TaskStatus` loses its separate `ok` field, so the outcome cannot disagree with the phase. The engine's `task_phase` carries the flag from `TaskState`, and the now-unused `ok` method on `TaskState` is deleted.
- `parse_record` and the other record validators move from the chat request parser into `record.rs` beside `MessageRecord`, absorbing the old `parse_message` body and dropping its one-line wrapper. The `crate::protocol` re-export keeps the `parse_record` path, and the refusal texts are unchanged.
- `raw_field`, `json_field`, and `direct_yield` are deleted: `op` and `var` are read with `raw_get` directly, each failure still a malformed yield, and the raw-read rule moves to the parse module doc. The store parser builds its argument failures through `call_error`, as the other parsers do.
- `answer_envelope` replaces the test-support `into_envelope` method with a free function over the same result shape, and the tests in both crates call it.
- `dropped_answer` is deleted: a dropped effect now answers every continuation with `Err(Error::Interrupted)` rather than picking a variant per continuation, and a timer's drop still goes to `drop_timer`.
- `fill_batch_slot` passes every failure through to its slot; only a success other than plain text is still an internal error.
- `task_status_table` sets the same Lua `ok` as before: absent while running, the task's outcome once done, and `false` when cancelled or abandoned. A new test pins all five phases.
- `VfsOp`, a `pub` re-export, now documents that the parse checks argument shapes and the operation checks line ranges when it runs.
- `ContentPart::ImageUrl` is documented as any image URL with no scheme restriction, and `TaskDelivery` as never delivering an abandoned task. The `Request::Store` doc says Pack preludes call the direct store closures before the yield shims install.
- `crates/runvil-internal/lua/src/protocol/tests/answer_chat.rs` is deleted, since a chat failure no longer has its own variant; the cancelled-answer test in `answer.rs` pins the same `cancelled` kind and message.

Design: removes pure-function @ crates/runvil-internal/engine/src/execute/scheduler/apply.rs::dropped_answer deps: Continuation
Design: extends pure-function @ crates/runvil-internal/engine/src/execute/scheduler/waits.rs::task_phase deps: TaskState
Design: removes pure-function @ crates/runvil-internal/lua/src/protocol/parse.rs::direct_yield
Design: replaces pure-function @ crates/runvil-internal/lua/src/protocol/record.rs::parse_record deps: Value,usize was: crates/runvil-internal/lua/src/protocol/parse/chat.rs::parse_message
Design: replaces pure-function @ crates/runvil-internal/lua/src/protocol/record.rs::chat_roles was: crates/runvil-internal/lua/src/protocol/parse/chat.rs::chat_roles
Design: replaces pure-function @ crates/runvil-internal/lua/src/protocol/record.rs::content_refusal deps: usize was: crates/runvil-internal/lua/src/protocol/parse/chat.rs::content_refusal
Design: replaces pure-function @ crates/runvil-internal/lua/src/protocol/record.rs::parse_content_parts deps: Value,usize was: crates/runvil-internal/lua/src/protocol/parse/chat.rs::parse_content_parts
Design: replaces pure-function @ crates/runvil-internal/lua/src/protocol/record.rs::parse_content_part deps: Value,usize was: crates/runvil-internal/lua/src/protocol/parse/chat.rs::parse_content_part
Design: replaces pure-function @ crates/runvil-internal/lua/src/protocol/record.rs::image_url_refusal deps: usize was: crates/runvil-internal/lua/src/protocol/parse/chat.rs::image_url_refusal
Design: replaces pure-function @ crates/runvil-internal/lua/src/protocol/record.rs::parse_tool_call_record deps: Value,usize was: crates/runvil-internal/lua/src/protocol/parse/chat.rs::parse_tool_call_record
Plan: vibe/2026-10-29-1-lua-slop-cleanup.md
Section setup now installs every coroutine shim in one step, including the loop and compact shims, so it no longer depends on a second install made in the right order. A model-issued tool call carries its call id and turn together as one value, and a request that supplies only one of them is refused as malformed. The live mirror of the sealed system values is built when injection succeeds, which removes the optional state and its unreachable failure. Hooks and constructors that only tests use now compile only into test builds, and comments that described a separate scheduler mode now describe the one path.

- `install_shim_prelude` puts `models.loop` and `models.compact` on `models` itself, which retires `install_section_loop_shim`, its re-exports, and the `LOOP_REGISTRY` and `COMPACT_REGISTRY` stashes. Section setup calls `install_coro_shims` alone, and the body still runs 85 lines after `fanout_shim` takes over the `tasks.fanout` install.
- `ModelCall` replaces the separate optional `call_id` and `turn` on `Request::ToolCall`, and the engine threads it through `dispatch_tool_call` into `CallSite::Model`, whose `turn` is now a plain `u32`. It is a public type re-exported beside `Request`.
- `ToolCallFields` keeps optional `call_id` and `turn` because `parse_tool_calls` reads its batch entries through the same `read_tool_call`, and a batch entry carries no turn of its own.
- `Injected` now owns the live `sys` mirror beside `access`, so `SectionVm` drops its `Option` field, the `NOT_INJECTED` failure goes, and the prose and `require` readers take `&Arc<Mutex<Json>>`.
- `install_prose` goes away and `prose::install` delegates to `swap`, dropping the render it replaced, so one function fills the `prose` slot.
- `stash_shims` stashes `model_tool_call` under `MODEL_TOOL_CALL_REGISTRY` only in test and `test-support` builds. `resume_block_coro` takes the same gate, and `inject_values` moves into the test-support module.
- `parse_tool_call` now refuses a `call_id` without a `turn`, or a `turn` without a `call_id`, as a malformed yield, so each updated `tools.call_as_model` test call now passes a turn.
- `CallSite::Script` no longer carries a turn, so a script call always reports the turn counter at dispatch.
- `the_yield_shim_install_puts_models_loop_and_models_compact_on_models` pins both functions on `models` after the shim install alone, and `every_entry_of_a_tool_calls_batch_parses_under_the_batch_turn` pins one turn across a batch.
- `tools_call_as_model` still compiles into every VM's shim chunk; only its registry stash is test-only.

Design: removes temporal-coupling @ crates/runvil-internal/lua/src/coro.rs::install_section_loop_shim deps: Lua boundary: pub
Design: extends oversized-unit @ crates/runvil-internal/lua/src/coro.rs::install_shim_prelude deps: Arc<AtomicBool>,Arc<AtomicU32>,CancelHook,Lua,usize
Design: new shared-parameter-cluster @ crates/runvil-internal/lua/src/coro.rs::fanout_shim deps: Function,Lua,Table
Design: new feature-flag @ crates/runvil-internal/lua/src/coro.rs::stash_shims deps: Lua,Table
Design: new parameter-object @ crates/runvil-internal/lua/src/protocol/request.rs::ModelCall boundary: pub
Design: removes temporal-coupling @ crates/runvil-internal/lua/src/vm.rs::SectionVm
Deferred: Production VMs still compile tools_call_as_model in the shim chunk.
Plan: vibe/2026-10-29-1-lua-slop-cleanup.md
The models loop now keeps the records saved before a compaction inside the phases that need them, instead of in a separate machine field. The compacting phase always holds them, and the chatting phase holds them only during a compacted round's retry, so a second overflow can still put them back and report the context as exhausted. This removes an internal error branch for a compacting phase with no saved records, a state the old code never built and the new types cannot express. The model handle loses its duplicate label getter while its label field returns the same name as before, and several docs are rewritten: the snapshot wording, the suspending shims the installers name, the raw model id fallback, and the loop entry's check order.

- `Phase::Compacting` carries the snapshot taken at overflow, and `Phase::Chatting` carries it as `Some` through a compacted round's retry. The chat phase needs it too, because the retry must still restore it and raise `context_exhausted` on a second overflow.
- `chat` takes the snapshot it holds: a new round passes `None`, and a kept compaction passes the snapshot `settle` hands back.
- `step` drops the internal-error arm for a compacting phase with no snapshot, since that phase can no longer be built without one.
- `label` on a model handle now reads `this.name()`, so Lua sees the same alias `name` returns, and the duplicate getter is deleted.
- `loop_begin`'s doc states the entry checks as receiver then arity for a handle's `loop`, handle-first refusal then arity for `models.loop`, then the compactor's type, and no longer quotes a closure body.
- `set_compacting` is still paired by hand at each site; no single value owns the compacting flag from set to clear.

Design: removes temporal-coupling @ crates/runvil-internal/lua/src/models_loop/machine.rs::Machine
Deferred: no Compaction value owns the compacting flag end to end yet.
Plan: vibe/2026-10-29-1-lua-slop-cleanup.md
The store's read helpers now live in a private child of the store module, and the store operation dispatcher sits beside the table it backs, so the parent globals module no longer holds bodies its child imports back. The shared table key order becomes its own crate-level module rather than a child of the fanout collection code, since key iteration and JSON encoding use it too. Docs that narrated a scoping alternative, named retired pieces, or called every trailing iteration key non-scalar are corrected. The program type drops its non-exhaustive marker, the compactor policy enum drops its default, a token accessor now compiles only in builds with testing enabled, and the log length refusal builds its text from the limit constant so its test follows the constant. Behavior visible to Lua code is unchanged.

- `engine_globals-store-read.rs` is a private child of the store module holding the read bodies and their line-range helpers. Only `read_store` and `read_store_numbered` are `pub(super)`.
- `run_store_op` now sits in the store module beside the table it backs. The parent's `pub use` re-export keeps its crate path unchanged.
- `read_store_bounded` picks its renderer once and always calls `with_read_range`, so the `read_range` and `read_range_numbered` wrappers are gone. The whole-file plain read still returns `view.read_string` unchanged.
- `crate::order` is now a crate-level module that fanout's collection module no longer re-exports. Iteration and `json.encode` key ordering import it directly.
- `Compactor` no longer derives `Default`, and its doc states that the loop reads `Fail` when the compactor argument is omitted. The old default test becomes `fail_raises_typed_exhaustion_for_a_provider_overflow` and keeps only its exhaustion assertion.
- `LuaProgram` drops `#[non_exhaustive]`, which its private fields already made redundant. `compile_internal` names its parameter `chunk_name`, the value it uses as both chunk name and display location.
- `LUA_LOG_CHARACTER_LIMIT` feeds both the log length refusal and the logging test's expected text, so the test follows the constant. The message still reads "at most 256 characters".
- `UsageAnchor` exposes `tokens` only under `test` or the `test-support` feature, so production builds cannot read the anchored count.
- `sort_key` refuses non-UTF-8 strings and non-finite numbers along with non-scalar keys. The iteration docs now call that whole trailing segment unordered.
- `mlua::Scope` no longer appears in the install docs, which had narrated it as the alternative to owned captures.

Design: replaces shared-parameter-cluster @ crates/runvil-internal/lua/src/engine_globals-store-read.rs::read_store_bounded deps: Access,LineNumbers,Option<i64>,str was: crates/runvil-internal/lua/src/engine_globals.rs::read_store_bounded
Design: replaces shared-parameter-cluster @ crates/runvil-internal/lua/src/engine_globals-store-read.rs::read_store deps: Access,Option<i64>,str was: crates/runvil-internal/lua/src/engine_globals.rs::read_store
Design: replaces shared-parameter-cluster @ crates/runvil-internal/lua/src/engine_globals-store-read.rs::read_store_numbered deps: Access,Option<i64>,str was: crates/runvil-internal/lua/src/engine_globals.rs::read_store_numbered
Design: replaces shared-parameter-cluster @ crates/runvil-internal/lua/src/engine_globals-store-read.rs::with_read_range deps: Access,FnOnce,Option<usize>,str,usize was: crates/runvil-internal/lua/src/engine_globals.rs::with_read_range
Design: replaces pure-function @ crates/runvil-internal/lua/src/engine_globals-store-read.rs::resolve_line_range deps: Option<usize>,str,usize was: crates/runvil-internal/lua/src/engine_globals.rs::resolve_line_range
Design: replaces pure-function @ crates/runvil-internal/lua/src/engine_globals-store-read.rs::number_lines_from deps: [&str],usize was: crates/runvil-internal/lua/src/engine_globals.rs::number_lines_from
Design: removes shared-parameter-cluster @ crates/runvil-internal/lua/src/engine_globals.rs::read_range deps: Access,Option<usize>,str,usize
Design: removes shared-parameter-cluster @ crates/runvil-internal/lua/src/engine_globals.rs::read_range_numbered deps: Access,Option<usize>,str,usize
Design: replaces dispatch-on-tag @ crates/runvil-internal/lua/src/engine_globals-store.rs::run_store_op deps: Access,VfsOp boundary: pub was: crates/runvil-internal/lua/src/engine_globals.rs::run_store_op
Design: replaces pure-function @ crates/runvil-internal/lua/src/order.rs::compare_integer_float deps: f64,i64 was: crates/runvil-internal/lua/src/collection-order.rs::compare_integer_float
Design: replaces pure-function @ crates/runvil-internal/lua/src/order.rs::sort_key deps: Value was: crates/runvil-internal/lua/src/collection-order.rs::sort_key
Design: removes surface-growth @ crates/runvil-internal/lua/src/compactors.rs::UsageAnchor::tokens boundary: pub
Deferred: The cleared non-scalar key wording in the iteration tests waits for a later scan.
Plan: vibe/2026-10-29-1-lua-slop-cleanup.md
Plan: vibe/2026-10-29-1-lua-slop-cleanup.md
Tool call arguments are now stored as a JSON map rather than a JSON value, so the shape a tool receives is the shape the constructor proved and the null case cannot be constructed. The object check hands back the map it validated, which removes a second parse at every construction site. Three fields that sibling modules could write directly are now private, and three doc comments that described behavior the code does not have were corrected.

- `checked_call_arguments` takes the value by value and returns the map it proved, and the refusal message for a non-object payload is unchanged word for word.
- `ToolCall` keeps its id and name private, so a sibling module can no longer write past the validation that `from_parts` performs; arguments stays crate visible because the detail module reads it.
- `metadata_diagnostics` is private rather than crate visible, so only the module that declares it can write it and callers reach it through the getter.
- `tool_call_arguments` returns an owned value, so the dispatcher moves it into the event rather than cloning the map a second time at the call site.
- `to_json_string` serializes the map directly and falls back to an empty object, because the default fallback would publish a blank string that a tool cannot parse.
- `Value::Null` is no longer a reachable arguments state, so the view tests drop the null half and the constructor still refuses a null payload through the non-object arm.

Design: extends newtype @ crates/runvil-internal/model-client/src/client/wire.rs::ToolArguments
Design: extends newtype @ crates/runvil-internal/model-client/src/client/wire.rs::ArgumentNames
Design: new encapsulated-invariant @ crates/runvil-internal/model-client/src/client/wire.rs::ToolCall
Plan: vibe/2026-10-29-2-model-client-slop.md
The options type kept its own copies of the three request settings, so its getters, its setters, and the hand copy a binding made could disagree about what a request would carry; it now holds those frozen settings as one value and reads through it. A binding filled from a catalog reported no provider unless its own setter had run, so the accessor contradicted the catalog the binding was filled from; one expression now answers for both readers. The module prose described how roles are declared and resolved rather than a bind call that no code performs, and the rule for what a failure message may hold is stated once instead of three times in slightly different wording. A set constructor, a view method, and a temperature conversion are gone, leaving each of those surfaces one way in.

- `CompletionOptions` holds one `ModelInvocation` in place of three fields, so its getters and setters touch the same value and a binding hands its frozen settings over in a single call.
- `crates/runvil-internal/engine/src/execute/context-bound.rs` stops copying the catalog's provider into the binding's own field, because the accessor now reads the catalog directly and the reassignment that existed only for that copy is gone.
- `crates/runvil-internal/model-client/src/model.rs` narrows its catalog re-exports to `ModelId`, the one name reached through this crate, and its module doc now describes the frontmatter role declarations instead of a bind call that no code performs.
- `crates/runvil-internal/model-client/src/model/error.rs` carries the message rule on the module page, reduces three doc blocks to pointers, and names all three public builders in the one it keeps.
- `crates/runvil/public-api.txt` gains the two new options methods and loses the two temperature conversion lines, with no other line moving.
- `crate::detail::tool_schema_new` is now named without brackets in `ToolSchema::name`'s documentation, because the api check rejects a doc link from a facade-visible item into a crate the facade does not re-export, and that method is facade-visible.
- `provider()` answers from the held catalog first and from the binding's own field second, so a binding built with catalog info reports that catalog's provider even when its own setter never ran.
- `from_parts` is gone, so every former caller builds its set with `push` and `set_default_alias` and a set has one construction path.
- `bindings()` is gone from the view trait and its mutex implementation, and `TryFrom<f64> for Temperature` is gone as well, leaving one temperature constructor.

Design: removes parallel-abstraction @ crates/runvil-internal/model-client/src/model/options.rs::CompletionOptions
  boundary: pub
Repairs: a binding's provider is its catalog's provider, with its own field as fallback @ crates/runvil-internal/model-client/src/model/options.rs::ModelBinding::provider - a binding filled from a catalog without calling the provider setter reported no provider while the catalog named one
Plan: vibe/2026-10-29-2-model-client-slop.md
Plan: vibe/2026-10-29-2-model-client-slop.md
@vinniefalco vinniefalco changed the title Add the std Pack, state the gateway's API principle, and clear the Engine's vibelight debt Add the std Pack, state the gateway's API principle, and clear three crates' vibelight debt Oct 29, 2026
Each engine rule now has one statement, at its owner, and three rules move from docs into types and checks. A chat effect cannot hold a prefix count without an earlier round, a suspended block refuses a second answer, and a Lua-crate internal fault keeps its source line. The rest removes vestiges, narrows visibility, renames items with misleading names, and shortens or rewrites doc comments.

- `Effect::Chat` holds `after: Option<(RoundId, u64)>` in place of `after` and `keep`, and `commit` on `MessageList` returns the same `Option`, so a prefix count cannot exist without an earlier round. `EffectRecord::Chat` still records `after` and `keep` apart, and the record assertions in `effect-tests.rs` keep their values, so the run log format does not change.
- `LuaError::Internal` now holds the `location` that `Error::internal` captures with `#[track_caller]`, and the new `Error::internal_at` copies it into the engine error. So `RunError::location` reports the site that built a Lua-crate internal fault, not a line in `convert.rs`, and `an_owned_lua_internal_message_reaches_the_engine_error_whole` checks that path and line.
- `SectionContext::new` and `new_h1` share the VM build, cancel flag, and Lua limits through `limited_vm`, and the frame literal through `assemble`.
- The discovery texts take their tool names from `DiscoveryTool::Search.wire_name()` and `DiscoveryTool::Call.wire_name()`, and the `pattern` description reads `PATTERN_LIMIT`, which moves into `execute/scope.rs`. The H1 gate notice starts with `NOTICE_HEADER`, which `Requirements` also uses, and keeps the text of the removed `H1_GATE_FAILED`.
- The `cancel` module, the four `SourceLocation` builders, and the engine's `From<ModelSetError>` bridge are gone, so `CancelHandle` comes from `runvil_types::cancel` and a poisoned model set reaches the engine through `runvil_lua::Error`. `advance_turn` calls `update` and drops the `unwrap_or(u32::MAX)` fallback.
- `RunContext`, `RunError`, and `ModelBindings` lose `#[non_exhaustive]`, and `public-api.txt` records this beside the `Effect::Chat` field change. Their fields stay private, so other crates still cannot build or destructure them.
- `from_state` becomes private, so code outside its module cannot build a `Run` from a state that skipped `prepare_state`. The `limits` field of `RunContext` becomes `pub(super)`, six admission items become private, and the test-only `recording` module and its items become `pub(crate)`.
- `bound_tool_set` becomes `run_tool_set`, because it binds nothing, and `prepare_effective_scope` becomes `freeze_tool_sets`. `take_back` on `Admission` becomes `release`, because the admission docs use "take back" for a chain that regains its slots.
- `deliver` on `Chain` returns `Error::Internal` for a second answer before resume and keeps the first, and `answered` in `ChainOps` becomes a `bool`, so `with_chain` requeues a chain at most once. The new test `a_second_delivery_before_resume_is_an_internal_error` makes both deliveries through the `deliver_twice_for_test` hook.
- `take_delivery` matches the entry state in place and writes `Delivered` only from `Done`, so no refusal arm puts a state back by hand. The new test `a_refused_delivery_leaves_the_entry_unchanged` checks that a `Cancelled`, `Abandoned`, or `Running` entry keeps its state after a refusal.
- The nil `argv` substitution error now reads `is nil (argv holds no value)` in place of `is nil (the args string is not JSON)`, because a JSON null or an H1 pass that leaves `argv` nil also causes it. `nil_argv_is_an_error` checks the new text.
- The store round-trip test now compares the whole rebuilt error for each variant whose fields a table carries, not only the `NotUtf8` offset, over the list from `one_failure_per_store_variant`.
- The `RunErrorKind` docs now put a Lua compile failure under `Parse` and a broken `step` or `resume` contract under `Internal`, and they drop the instruction-hook budget from `Quota`.
- `AGENTS.md`, `README.md`, and the engine crate docs say "Pack tool" where they said "catalog tool", because a tool that only the model sees also leaves the run as `Effect::ToolCall`. The engine `AGENTS.md` rule now says the Engine depends on no tool, search, or model implementation, and the engine README ties determinism to answers in the same order at the same steps.
- The diff adds no test for the shared `limited_vm` path, the discovery texts built from `wire_name()`, or the `update` call in `advance_turn`.
Plan: vibe/2026-10-30-1-engine-vibelight-cleanup.md
@vinniefalco
vinniefalco merged commit 8064ff7 into cppalliance:master Oct 30, 2026
19 checks passed

This branch was successfully deployed

1 active deployment
github-pages — 8064ff75 Deployed Oct 30, 2026 by vinniefalco via deploy #148
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