Skip to content

Workshop crates cleanup - #71

Merged
vinniefalco merged 44 commits into
cppalliance:masterfrom
vinniefalco:master
Sep 25, 2026
Merged

vinniefalco merged 44 commits into
cppalliance:masterfrom
vinniefalco:master

Conversation

@vinniefalco

Copy link
Copy Markdown
Member

Cleans up the workshop crates so they are easier for a human to explore and maintain. "shell" now means only a command shell in a terminal, the test suite no longer skips silently or leans on fixed sleeps, a loopback bind gap and a save-timeout bug are fixed, dead code and copied helpers are gone, and the densest files are split along their existing seams. The workshop's human docs are deleted rather than fixed, pending a rewrite at beta.

The full plan, with the design and decision record, is vibe/2026-09-24-2-workshop-crates-cleanup.md.

Behavior changes

Only four things change for anyone outside the codebase:

  • The standalone workshop-server refuses a non-loopback server.bind (including 0.0.0.0 and ::) at startup with InvalidInput. The desktop app already forces 127.0.0.1:0, so it is unaffected.
  • A request that hits the route deadline gets a JSON ErrorEnvelope body with a timeout code instead of an empty 408. This covers every route wrapped by with_deadline, and both UIs render it as a readable message instead of a JSON parse failure.
  • When a save times out, the editor treats its conflict token as unknown, tells the user the save may or may not have landed, and re-reads before the next save instead of sending a stale token. The worst case is the existing conflict dialog.
  • /prompts/contract is served by the server. Path, method, bodies, status codes, and wire error codes are unchanged.

The published user guide also loses its Workshop part. The Gateway, Language, and Agent parts are unchanged.

What changed

  • Vocabulary. The desktop app moved from crates/workshop/shell to crates/workshop/desktop, and the build check's tier, the shared status bar, the workshop UI frame, and config-ui's post-login frame (now "desk") all have their own words. Config-ui's views are now pages. The Tauri package and binary names are unchanged.
  • Tests. Symlink tests fail under CI instead of skipping, and new tests cover UNC, verbatim, case-only, and junction paths in the path jail. Fixed sleeps are replaced with event-driven waits, the realtime relay's origin and subprotocol refusals are pinned, and the /ws frames are pinned in a fixture shared by Rust and TypeScript.
  • Dead code. WorkshopObserver, the StatusBus helpers, stale re-exports, and the server's pre-decomposition module aliases are deleted. The gateway app has its own copies of the icons.
  • Structure. app.rs's children moved into an app/ directory, compose is broken into per-subsystem register helpers that return named registration structs, the desktop supervisor and the agent socket's framing helpers are split out, the heartbeat and progress loops have named phases, the hyphenated groups became directories, and the /ws socket lives in a workshop_socket module.
  • Shared helpers. Error rendering, the state-bucket validator, and the mock HTTP server test helper live in workshop-support. The workshop UI's backoff and service tokens moved into services/.
  • Docs. The workshop's guide chapters, their export, and the workshop READMEs are deleted and dropped from the guide build. Code-level docs (//! crate docs, AGENTS.md rules, comments, Cargo descriptions) are corrected where the plan touched them.

Verification

Every canonical gate passes locally, each at least as green as the baseline recorded before the first change: both nextest partitions, the headless server run, doctests, both clippy partitions, fmt, the headless gateway check, the workspace and facade docs gates, cargo xtask api --check, mdbook build guide, cargo run -p build-user-guide, cargo test -p build-xtask, both UIs' build, test, and typecheck, and cargo workshop.

Suite Before After
Main tests 3,899 3,915
Workshop tests 234 247
Headless server tests 130 143
Workshop UI tests 136 139
Config UI tests 178 178

A retired-name grep over the scope is clean. The only remaining "shell" uses are the terminal command shell and third-party NSIS keywords.

Known

  • The save-timeout bug was inferred from reading the code. crates/workshop/ui/test/editor-save-timeout.mjs drives the timeout path end to end, but it was not reproduced against a live slow disk.
  • A late write can still land after the editor's re-read. The re-read narrows the race but does not remove it.
  • Deferred by the plan: the protocol crate's dependency on the full facade, cross-part imports in the workshop UI (STATUS_BAR, openInZone), and the workshop user documentation.

Record the plan's seed and its baseline. Copy the plan into the vibe dated record, point vibe/ACTIVE at it, and record the full canonical gate results so every later step compares against a known-green baseline.

- `vibe/ACTIVE` names the plan copy as the active plan for this run.
- `vibe/2026-09-24-2-workshop-crates-cleanup.md` records the baseline: every canonical gate passes, with no failing or intermittent tests.

Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
Remove the workshop user guide chapters, their SUMMARY.md entries, the single-file export, and the five workshop READMEs. The guide generator loses the workshop set, the doc tool loses its workshop lens, and the guide introduction stops routing to the deleted part.

- `guide/src/workshop/` and `guide/promptforge-workshop-guide.md` are removed; the Gateway, Language, and Agent parts stay byte-for-byte.
- `crates/build-user-guide/src/main.rs` drops the workshop set and repoints two tests at the gateway part.
- `crates/workshop/ui/test/docs-claims.mjs` drops its workshop-export check; the guide-sources check stays.
- `tools/document.md` loses the workshop lens so it cannot regenerate the deleted guide.

Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
Replace the two silent symlink skips in the workspace tests with a CI-aware helper that panics when CI is set, so a runner that cannot create a symlink turns the job red instead of skipping silently. Pin the confinement behavior for the path-spelling tricks a request can arrive in.

- `crates/workshop/workspace/src/workspace-tests-jail.rs` adds `symlink_unavailable(ci, reason)` and jail tests for verbatim `\\?\` spellings, UNC spellings, case-only respellings, and a directory junction.
- The two skip sites in `workspace-tests.rs` now call `jail::symlink_unavailable` with `std::env::var_os("CI").is_some()`.

Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
Remove the wall-clock delays from four workshop tests so the suite no longer depends on fixed timing. A stalled-browser relay test drops a redundant sleep and lets its timeout-bounded read wait for the cleanup, a recovery fixture parks on a channel gate instead of a five-second sleep, and two quiet-window assertions advance the paused clock or read after the end-of-stream signal instead of sleeping.

- `realtime_relay/overload.rs` drops the 750 ms sleep; the `RECV_TIMEOUT` read already waits for the relay to release the stalled browser.
- `shell/src/gateway/tests/recovery.rs` parks the hanging child on an mpsc gate the test drops, instead of a five-second thread sleep.
- `heartbeat_loop/startup_convergence.rs` advances the paused clock across the quiet window instead of sleeping four intervals.
- `chat_gate.rs` turns the quiet-window helper into a zero-deadline read after the models frame, the catalog's end-of-stream signal.

Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
Pin the realtime relay's two refusals with tests against a live server. Each test asserts the exact status and empty body of a rejected upgrade, one for an origin outside the allowlist and one for a requested subprotocol, so the refusals are recorded as the browser sees them without changing production behavior.

- `request_with` builds the upgrade request with an optional origin and subprotocol, so both refusal tests share one handshake path and differ only in the rejected header.
- `refused_response` runs the handshake and returns the HTTP response that refused it, turning the socket error into the status and body each test asserts.
- `a_foreign_origin_is_refused_with_an_empty_forbidden_body` asserts a foreign origin is refused with status 403 and an empty body.
- `a_requested_subprotocol_is_refused_with_an_empty_bad_request_body` asserts a requested subprotocol is refused with status 400 and an empty body.
- `realtime.rs` gains only the `#[cfg(test)]` `#[path = "realtime-tests.rs"] mod tests;` block, so the relay's production behavior is pinned without being changed.

Design: new pure-function @ crates/workshop/server/src/routes/realtime-tests.rs::request_with deps: &str,Option<&str>,Option<&str>
Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
The workshop socket's wire frames are now pinned by one shared fixture asserted on both the Rust and TypeScript sides, so a drift on either side fails that side's test. The model-selection event gains a typed frame beside the profile-selection event, and the server parses it the same typed way rather than reading the model field out of the raw JSON. Both suites assert the same case list, so a frame added on one side no longer goes unnoticed on the other.

- `SelectModelFrame` — a typed inbound frame beside `SwitchProfileFrame`: equality over its single `model` field, no methods, no id, re-exported from the crate root.
- `workshop-frames.json` — one fixture keyed by case name holds the six `/ws` frames, so both suites pin the same wire contract.
- `workshop_frames` — the Rust test serializes each server-to-client frame and deserializes each client-to-server frame against the fixture, and pins the exact case list.
- `workshop-wire-fixtures.mjs` — the TypeScript test drives the socket through every fixture frame and checks the client sends match their entries.
- `select_model` — no behavior change: the refusal text is unchanged and no existing frame gains or loses a field.

Design: new value-object @ crates/workshop/protocol/src/menu.rs::SelectModelFrame
  boundary: wire
Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
The workshop server may only bind to loopback, since a wildcard or LAN address would expose it to other hosts. The bind helper now refuses a non-loopback address with an invalid-input error before it creates a socket.

Repairs: workshop server binds only to loopback @ crates/workshop/server/src/serve.rs::reuse_bind - non-loopback addresses were accepted, exposing the workshop server to other hosts
Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
Requests that run past their route deadline now answer a JSON error envelope instead of an empty 408 body. The envelope carries a machine-readable code and a message naming the elapsed deadline, so clients can distinguish a deadline failure from a raw timeout. A test-only stall makes a slow write deterministic, so the timeout and the write that still lands on disk are both reachable without waiting out the ten-second production deadline.

- `DEADLINE_ELAPSED_CODE` — the machine-readable code `deadline_elapsed` is a public constant exported from support, so the middleware and the shape test share one source instead of a repeated string.
- `deadline_elapsed_message` — the user-visible message is a pure function of the elapsed `Duration`, exported beside the code.
- `routes_with_deadline` — a `test-fixtures`-gated builder binds the workspace router on a test deadline, so no production knob is added.
- `workspace-stall.rs` — a test-only stall holds the next write on the blocking pool until the test releases it, making the timeout deterministic.
- `save_timeout` — the end-to-end test asserts a `PUT /workspace/file` that outlasts its deadline answers a 408 whose content type is JSON and whose body equals `workshop_protocol::ErrorEnvelope::new(deadline_elapsed_message(TEST_DEADLINE), DEADLINE_ELAPSED_CODE)`.
- `stall_wait` — the deadline abandons but does not cancel the blocking write; releasing the stall lets it still land on disk, which the test asserts.
- `serde_json::Value` — the shape test compares parsed JSON values, so `ErrorEnvelope` gains no `Deserialize` and the protocol crate is untouched.

Design: new surface-growth @ crates/workshop/support/src/deadline.rs [boundary: wire]
Design: new pure-function @ crates/workshop/support/src/deadline.rs::deadline_elapsed_message [deps: Duration] [boundary: pub]
Repairs: route-deadline 408 answers a JSON error envelope @ crates/workshop/support/src/deadline.rs - a write outlasting its deadline answered an empty 408 body
Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
When a request runs past the server's deadline, the answer may carry an empty body, which the transport floor misread as a JSON shape failure. The floor now recognizes the timeout and renders a readable timed-out message, while a deadline that carries the error envelope still shows the server's message. A new regression test covers both shapes through the write boundary.

- `readJson` now returns `null` for a 408 whose body is empty or non-JSON, so the non-OK handler renders a readable timeout instead of a shape failure.
- `errorMessage` now answers a 408 with the route timed out and still returns the envelope's message when one is present.
- `json-request-timeout.mjs` pins both 408 shapes, including a write through the boundary.

Repairs: route timeout renders readably @ crates/workshop/ui/src/services/json-request.ts::readJson - an empty 408 body surfaced a non-JSON shape failure
Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
A save that times out no longer leaves the editor assuming its conflict token is still valid. The editor marks the token unknown after a deadline and tells the user the write may or may not have landed. The next save re-reads the file, adopts the fresh token when the disk still holds what was last sent, and otherwise falls back to the conflict dialog, so a stale token never reaches the write.

- `tokenUnknown` records an unknown-token state beside the known token, paired with `lastSentText` as the last write attempt; both reset whenever a read or write establishes a fresh token.
- `DeadlineElapsed` adds a deadline_elapsed code to the error catalog, which `isDeadlineElapsed` narrows from a caught error.
- `httpFailure` maps a 408 response to the deadline code rather than the generic HTTP status, and `save` re-reads the file while the token is unknown, adopting the fresh token on a disk match and showing the conflict dialog otherwise.
- `token` never travels stale: no save path forwards a token the editor does not currently know.

Design: extends dispatch-on-tag @ crates/workshop/ui/src/services/workspace-api.ts::httpFailure deps: number,string,unknown
Design: new pure-function @ crates/workshop/ui/src/services/workspace-api.ts::isDeadlineElapsed deps: unknown
Repairs: the editor never sends a stale token @ crates/workshop/ui/src/parts/editor/editor-panel.ts::save - the next save after a timed-out write re-sent the stale token
Repairs: a 408 maps to the DeadlineElapsed catalog code @ crates/workshop/ui/src/services/workspace-api.ts::httpFailure - a timed-out write was reported as a generic HttpStatus error
Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
The workshop's append-only run-event log is deleted, and the gateway drops the engine dependency that the log imported. The status bus loses the convenience helpers that fixed a severity and activity, so its tests now push a fixed update through the bus directly. The server stops re-exporting the stale cache types and the alias to the deleted log.

- `WorkshopObserver`: the shared, append-only run-event log and its broadcast fan-out are removed, and the gateway drops the `promptforge` dependency the log imported.
- `StatusBus`: the `report`, `info`, `debug`, `error`, and `idle` helpers are removed; their tests now call `emit` with an explicit update.
- `CacheEvent`, `CacheResponse`, and `SsePayloadStream`: the server stops re-exporting these cache types and the `observer` module alias.

Design: removes shared-mutable-state @ crates/workshop/gateway/src/observer.rs::WorkshopObserver
  boundary: pub
Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
The gateway app now keeps its own byte-identical icon set instead of reading the workshop's icons across the crate boundary. The build script and the embedding test point at the local copies, so the icons travel with the crate's own files. The rule that keeps icon copies synchronized with their masters is extended to cover these new copies.

- `assets/icon.ico`: the byte-identical copy now feeds the Windows exe embedding, and `assets/32x32.png` and `assets/64x64.png` bring the brand glyph sources into the crate beside the existing tray rgba assets.
- `ICON`: the build-script constant now names the local copy, and the embedding test reads the same `assets/icon.ico`, so the Windows exe embeds the crate's own file rather than the workshop's.
- `../../workshop/shell/icons/icon.ico`: the out-of-crate reference is gone from every gateway app file.

Design: removes hidden-dependency @ crates/gateway/app/build.rs::ICON
Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
Relocate the Workshop desktop application to a directory named desktop, retiring shell as a name for anything but terminal command shells. Every reference to the old location is corrected in the same commit so the build, release workflows, and sidecar staging keep working. The package and binary names are unchanged, and the moved files are byte-for-byte identical, so nothing observable changes.

- `crates/workshop/desktop` — the desktop app directory, moved verbatim from `crates/workshop/shell`; every file is a 100%-similarity rename, and the `workshop` package and `promptforge-workshop` binary names are unchanged.
- `tiered_crate_dir` — the build check's fallback directory flips from `shell` to `desktop`, so the tier graph still resolves the Tauri crate.
- `crates/workshop/desktop/binaries` — the sidecar staging path, corrected in the nightly and release workflows, the staging tool, its test, and the interruption test so CI still locates the gateway executable.
- `crates/workshop/shell` — nothing remains at the old path, and no moved file changes content, so the desktop app behaves exactly as before.

Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
The Workshop vocabulary now reserves shell for terminal command shells, and the Rust sources, build checks, and workflow comments rename every other use. Where shell meant the Tauri application it now reads the desktop app, and where it meant the server or its build-check tier it now reads the server. The change is names and prose only, so no public API, wire shape, or persisted format moves.

- `SHELL` — in `crates/build-xtask/src/tidy.rs` the tier constant becomes `SERVER`, and in `crates/build-xtask/src/product.rs` the boundary constant becomes `DESKTOP`, still valued `"workshop"`, so the tier reads "server" and the boundary reads "desktop app".
- `shape_for_shell` — the desktop config's listener-shaping function becomes `shape_for_desktop`, and its `SHELL_BIND` constant becomes `DESKTOP_BIND`.
- `the_shell_re_adding_workshop_server_is_reported` — renamed to `the_desktop_app_re_adding_workshop_server_is_reported`; the tidy test, the server-api surface test, and the desktop sibling-probe test drop shell from their names the same way.
- `tao/wry shell` — becomes `tao/wry runtime`, the windowing-framework sense that names neither the desktop app nor the server.
- `thin shell` — becomes `thin entry point` in the server's crate and binary docs, so the server stops describing itself as a shell.
- `shell` — no longer names the desktop app or the server in the touched files; the only remaining uses are terminal command shells and third-party names.

Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
Retire the word "shell" across the shared status bar and the workshop UI so that it names only a terminal command shell. The shared status bar component becomes the status bar view, the workshop frame becomes the desk, the lazy panel stand-in becomes the placeholder, and the boot shell becomes the entry bundle. The Tauri application is now called the desktop app throughout. No behavior changes.

- `createStatusBarView`: the shared status bar export `createStatusBarShell` and its `StatusBarShell` type become `createStatusBarView` and `StatusBarView`, and each consumer's local `shell` variable becomes `view`.
- `ws-desk`: the workshop frame's `.ws-shell` class becomes `.ws-desk` in the layout stylesheet, the entry markup, and the layout test.
- `placeholder`: the lazy panel stand-in's "shell" wording and its `shell` local become `placeholder` in the panel type, the zones stylesheet, and the sizing regression test.

Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
Rename config-ui's views directory to pages so that page names a routed screen behind a tab and view names a DOM component. The six screen modules and their tests drop the view suffix as they move, and the six imports that pull them in follow. The moved files are byte-for-byte identical and identifiers keep their names, so nothing observable changes.

- `crates/gateway/config-ui/ui/src/pages` — the directory, moved from `views`; the cloud-models, discover, models, profiles, secrets, and settings modules and their tests rename from `*-view` to `*-page`, while three test files move without a new name.
- `createDiscoverView` — the six factories keep their names, and their imports change only the module path.
- `crates/gateway/config-ui/ui/src/views` — nothing remains at the old path, and no moved file changes content, so the interface behaves exactly as before.

Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
Renames the gateway config UI so its post-login frame is the desk and its routed-screen identifiers are pages, retiring shell and view from those roles. The change is naming only: no behavior, state, or wire format is touched, and the test descriptions and a test name follow the new terms.

- `mountLiveShell` is renamed `mountLiveDesk` and `showShell` becomes `showDesk`; the frame's `main.className` and the `.shell` CSS rules become `.desk`.
- `ViewId` becomes `PageId`, and the `createModelsView`, `createCloudModelsView`, `createDiscoverView`, `createProfilesView`, `createSecretsView`, and `createSettingsView` factories become their `createXPage` counterparts, with `viewRoot` renamed `pageRoot` and `setActiveView`, `tabByView`, `PendingView`, and `disposeView` renamed to their page equivalents.
- `.view-empty` becomes `.page-empty` at every empty, loading, and missing state.

Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
Retire the word shell from the Workshop documentation, keeping it only for command shells in a terminal, and record the agreed vocabulary in the root agent rules. The Tauri crate becomes the desktop app, the top dependency tier becomes the server, and the eagerly loaded front-end composition becomes the entry bundle.

- `## Vocabulary`: A new section in the root agent rules records ten agreed words and their current meanings, so the structural rules can use them without re-defining each one.
- `server -> features -> services -> vocabulary`: The dependency graph's top tier is renamed from shell to server, named for its only crate.
- `desktop app`: The Tauri `workshop` crate and its product lifecycle are now described as the desktop app, replacing shell in the crate rules and the architecture rules.
- `entry bundle`: The eagerly loaded SPA composition is now the entry bundle, replacing boot shell in the SPA rules and the agent rules.

Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
The server's application module moves its two conditional child modules out of sibling files and into a dedicated subdirectory, and drops the explicit path attributes that located them. Standard module layout now resolves the children from the directory, and the relocation is a pure move that changes no module behavior.

- `app/fixtures.rs` and `app/tests.rs` replace `app-fixtures.rs` and `app-tests.rs` as pure renames into a new `app` directory.
- `app.rs` drops the `#[path = "app-fixtures.rs"]` and `#[path = "app-tests.rs"]` attributes so standard layout resolves the `fixtures` and `tests` modules.

Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
The server's composition root had grown too large to read as a single function. It moves into its own module and breaks into one helper per subsystem, each registering one subsystem and holding its guards. The orchestrator now reads as a list of those registrations in composition order, and the boot contract stays a single assertion after every subsystem has registered.

- `compose::compose` moves the composition root into its own module and now delegates to per-subsystem register helpers; the three call sites change only to qualify the name.
- `register_status`, `register_menu`, `register_gateway`, `register_workspace`, `register_user_state`, and `register_sessions` split the single composition body into one helper per subsystem, each holding its own registration guards.
- `registry.require` keeps the same required-handle assertions in the same order, so a missing contribution still fails composition at startup.

Design: removes oversized-unit @ crates/workshop/server/src/app.rs::compose
Design: new shared-parameter-cluster @ crates/workshop/server/src/app/compose.rs deps: Omit, Registrations, Registry
Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
Split the agent socket's framing helpers out of the socket module into a sibling framing module so each stays under the five-hundred-line file ceiling. The two pure render functions that map the harness wait and delta vocabulary onto Workshop's wire shapes gain table-driven tests, while the durable-event helpers that need a live session and socket stay covered by the existing integration tests. Behavior is unchanged.

- `socket_frames` is the new sibling module holding the four framing helpers moved out of `socket`, keeping each file under the five-hundred-line ceiling.
- `Attached` and its three fields widen to `pub(crate)` so the framing module can read the session and advance the per-client `cursor` and `framed` index.
- `input_frame` and `delta_frame` stay pure render functions mapping the harness wait and delta vocabulary onto the protocol's wire shapes, and their table-driven tests pin each mapping.
- `drain_events` and `frame_entry` gain no unit tests: they need a live session and socket, so the existing integration socket tests cover them end to end.

Design: replaces pure-function @ crates/workshop/server/src/agents/socket_frames.rs::input_frame deps: WaitFrame was: crates/workshop/server/src/agents/socket.rs::input_frame
Design: replaces pure-function @ crates/workshop/server/src/agents/socket_frames.rs::delta_frame deps: Delta was: crates/workshop/server/src/agents/socket.rs::delta_frame
Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
The desktop gateway supervisor is split from a single large module into a directory of four child modules plus a coordinating root. Each child owns one seam: shutdown and completion signals, thread lifecycle, recovery-candidate ownership, and cancellable launch and wait. The supervision state machine and its injected helpers stay in the root with unchanged signatures, so the gateway tests are untouched. The split is purely structural and preserves behavior.

- `supervisor.rs` is deleted and replaced by a `supervisor/` directory: `signals.rs`, `lifecycle.rs`, `recovery.rs`, and `launch.rs` each own one seam, with `mod.rs` as the coordinating root.
- `RecoveryOwnership` and `RecoveryIdentity` widen from `pub(super)` to `pub(crate)`, the spawn and launch helpers widen the same way, and `StopSignal` and `Completion` become `pub(super)`, so each child reaches the pieces it needs.
- `run_supervision`, `supervise`, and `validate_recovery` keep their signatures, and the gateway tests change no code.

Design: removes oversized-unit @ crates/workshop/desktop/src/gateway/supervisor.rs
Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
The gateway heartbeat and progress loops are each rewritten from one long function into a set of named phase helpers, so each phase is a separately named step rather than a flat sequence inside a single body. The stop and rebind control flow is preserved exactly, keeping the same branches, their order, and their cancellation points, so the loops behave as before. The change is purely structural and leaves the tests untouched.

- `crates/workshop/gateway/src/heartbeat.rs` — `run` is reduced to a coordinator that calls `await_probe_interval`, `probe`, `announce_transition`, and `refresh_sources`, with a `Phase` enum distinguishing `Stop` from `Rebind`.
- `crates/workshop/gateway/src/gateway_progress.rs` — `run` is reduced to a coordinator that calls `idle_until_reachable`, `subscribe`, `drive_stream`, and `recover`.
- `select!` — every `select!` keeps its branches, branch order, and cancellation points, so the loops behave as they did before.

Design: removes oversized-unit @ crates/workshop/gateway/src/heartbeat.rs::run
Design: removes oversized-unit @ crates/workshop/gateway/src/gateway_progress.rs::run
Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
Move the prompt contract endpoint out of the workspace crate into the server, which already depends on the engine, and drop the engine dependency that only that endpoint used. The parse-failure mapping moves into the server's error type so the handler file stays a near-verbatim rename, and the endpoint's wire contract is unchanged.

- `routes::prompts::routes()` now returns a stateless router that the server mounts under its default deadline, so the endpoint carries no shared state.
- `AppError::prompt_parse` renders each parse-failure kind into its wire code and message, keeping the mapping in the error type rather than the handler.
- `UNPROCESSABLE_ENTITY` and the parse-failure wire codes are unchanged, so the endpoint's status and error codes stay the same.
- `promptforge` leaves the workspace manifest, so the workspace crate no longer depends on the engine.

Design: replaces pure-function @ crates/workshop/server/src/error.rs::AppError::prompt_parse deps: ParseError
  was: crates/workshop/workspace/src/handlers-prompts.rs::parse_failure
Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
The workshop support crate gains the error-message rendering and the state-bucket validator that were duplicated across the workshop server and its feature crates. The server and its feature crates now call the shared helpers, delete their local copies, and map the shared validator's refusals onto their existing wire errors, so rendered messages and wire codes stay exactly the same.

- `render_message` is now a generic free function in workshop support over any type that displays and reports a source, replacing the three crate-local copies in the server, user-state, and workspace error modules.
- `validate_bucket_body` becomes the single validator for a bucket put, running the allow-list, size-cap, and JSON checks in cheapest-refusal-first order, with `resolve_bucket_key`, `check_bucket_cap`, and `check_bucket_text` as its steps.
- `StateBucketError` carries one variant per refusal; the user-state and workspace `From` conversions map it onto each crate's existing wire error, keeping the wire codes unchanged.
- `LEAK_DETAIL` still tracks the debug-assertions flag, so production bodies keep the error's own message and debug bodies still append the source chain, exactly as before.
- `user_state_key` and `check_text_cap` in user-state, and `ui_state_key` and `check_ui_state_cap` in workspace, are deleted, as are the copied `render_message` and `LEAK_DETAIL` definitions.

Design: new pure-function @ crates/workshop/support/src/error_message.rs::render_message deps: E,bool
  boundary: pub
Design: new pure-function @ crates/workshop/support/src/state_bucket.rs::resolve_bucket_key deps: str
  boundary: pub
Design: new pure-function @ crates/workshop/support/src/state_bucket.rs::check_bucket_cap deps: usize
  boundary: pub
Design: new pure-function @ crates/workshop/support/src/state_bucket.rs::check_bucket_text deps: str
  boundary: pub
Design: new pure-function @ crates/workshop/support/src/state_bucket.rs::validate_bucket_body deps: str,u8,usize
  boundary: pub
Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
This change introduces one shared mock HTTP server helper and routes nine near-identical test binders through it. Each binder previously opened a loopback listener and served the caller's router in a spawned task, a block duplicated across the gateway and server test suites. The helper returns the bound address and the spawned task handle, so a caller can leave the server running for the test's lifetime or abort it early. The consolidation removes the duplication and adds tests that the served router answers on the reported address and that aborting the handle stops the server.

- `workshop_support::fixtures::serve` is the shared helper: it binds a free loopback port, serves the caller's router in a spawned task, and returns the bound address plus the task handle.
- `test-fixtures` gates the `fixtures` module, and the gateway now enables `workshop-support/test-fixtures` in both its feature set and its dev-dependency so its tests compile without `--all-features`.
- `a_mock_server_serves_its_router_on_the_reported_address` and `the_returned_handle_stops_the_server_when_aborted` assert the served router answers on the reported address and that aborting the handle cancels the task.
- `tokio::net::TcpListener::bind("127.0.0.1:0")` no longer appears in the nine changed test helpers; the shared helper now owns the bind-and-serve block.

Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
This commit rehydrates the flat hyphenated sibling modules into directories in standard module layout, removing the path attributes that had pinned each module to a kebab-case filename. Groups of three or more siblings become a directory beside their parent, and the modules that shared one ambiguous name take distinct names so each reflects the file it now occupies. Behavior and the persisted workspace format are unchanged.

- `#[path = "workspace-backing.rs"]` — the path attributes pinning each moved sibling to a flat kebab filename are dropped, and the modules now resolve through standard directory layout beside their parent.
- `ui_state_kv` — the four modules that shared the name `ui_state` take distinct names (`ui_state_kv`, `ui_state_memory`, and their `ui_state_kv_tests` and `ui_state_memory_tests` test modules) so each matches the file it now occupies.
- `pub(crate) mod ui_state_kv` — the renamed modules stay crate-internal and no test changes its assertions, so behavior and the public surface are unchanged.

Design: replaces shotgun-surgery @ crates/workshop/workspace/src/workspace_file/ui_state_kv.rs was: crates/workshop/workspace/src/workspace_file-ui-state.rs
Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
The server drops the compatibility aliases that re-exported its extracted subsystem crates under their pre-decomposition module paths, so every internal reference now names the real crates directly. The public re-exports keep their names and only their backing paths move.

- `pub use workshop_gateway::{gateway, gateway_binding, gateway_progress, heartbeat, resolve}` — The server deletes this alias block and the matching `workshop_menu` and `workshop_status` aliases, so internal call sites reach the subsystem crates directly.
- `GatewayClient` — Every named re-export keeps its name and only its backing path moves to the real crate, so the server's public surface is unchanged.
- `workshop_gateway::gateway::GatewayClient` — The change is a pure rename: no behavior changes, and the only test edits re-point this import and the `REQUEST_TIMEOUT` constant rather than adding new coverage.

Design: removes shim @ crates/workshop/server/src/lib.rs
  boundary: pub
Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
The workshop socket moves out of the agent-session subsystem into a top-level module of its own, renamed so the server side pairs with the workshop-socket name its client already uses. Its menu submodule moves with it, and the agent-session socket keeps sharing the frame-sending helpers from the new location. The socket keeps its route, behavior, and tests.

- `mod workshop_socket;` — The /ws socket module is declared at the server crate root, so it no longer nests inside the agent-session subsystem and its name pairs with the client's workshop-socket service.
- `#[path = "workshop_socket-menu.rs"]` — The menu submodule follows the rename and stays a path-wired sibling, so the module name stays clean.
- `.route("/ws", get(crate::workshop_socket::upgrade))` — The route keeps its mount point and now names the moved module, so the wire behavior is unchanged.
- `pub(crate) mod session;` — The old declaration is dropped and no public item moves, so the crate's public surface is unchanged and no test changes its assertions.

Design: replaces shotgun-surgery @ crates/workshop/server/src/workshop_socket.rs was: crates/workshop/server/src/agents/session.rs
Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
This commit renames three things so each name keeps one meaning. The server task that derives status-bar frames becomes the status reporter, leaving relay to name only the model-catalog passthrough. The gateway's switch-profile answer body is renamed so it stops colliding with the menu's unrelated outcome type. The workbench-socket wording becomes workshop socket, matching the vocabulary.

- `SwitchProfileBody` — The gateway's switch-profile answer type takes this name in its definition, the client decode, and both crate-root re-exports, so it no longer collides with workshop-menu's unrelated `SwitchOutcome`.
- `spawn_reporter` — The status task's spawner and its `report` loop take these names, so "relay" names only the model-catalog passthrough in `agents/relay.rs`.
- `workshop_socket` — The `/ws` socket becomes the "workshop socket" in the crate docs and the AGENTS.md vocabulary, replacing the "workbench socket" wording.
- `SwitchOutcome` — The renames are mechanical: the switch tests change only the type name in their two assertions, and no route, frame shape, or behavior changes.

Design: replaces value-object @ crates/workshop/gateway/src/gateway/events.rs::SwitchProfileBody
  boundary: wire
  was: crates/workshop/gateway/src/gateway/events.rs::SwitchOutcome
Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
Every workshop subsystem now returns its registration guards as a named struct with one named field per guard, instead of an anonymous tuple. Call sites read the guards by field name rather than unpacking by position, so each guard's role stays explicit as the subsystem set grows. The user-state subsystem gains a dedicated registration module, bringing it in line with the others.

`GatewayTaskRegistrations` — `register_tasks` returns this struct with `heartbeat` and `subscriber` fields in place of the two-tuple it returned before.

`MenuRegistrations`, `StatusRegistrations`, and `WorkspaceRegistrations` — each `register` now returns a named struct of guards in place of its three-tuple, and each crate re-exports the struct.

`UserStateRegistrations` — `register` moves from `user-state/src/lib.rs` into a new `handles.rs` and returns this struct with `routes` and `state` in place of its two-tuple.

`compose.rs` — the composition root reads the guards through the named fields, and the heartbeat loop test follows; the registrations they hold are unchanged.

Design: new parameter-object @ crates/workshop/gateway/src/handles.rs::GatewayTaskRegistrations
  boundary: pub
Design: new parameter-object @ crates/workshop/menu/src/handles.rs::MenuRegistrations
  boundary: pub
Design: new parameter-object @ crates/workshop/status/src/handles.rs::StatusRegistrations
  boundary: pub
Design: new parameter-object @ crates/workshop/user-state/src/handles.rs::UserStateRegistrations
  boundary: pub
Design: new parameter-object @ crates/workshop/workspace/src/handles.rs::WorkspaceRegistrations
  boundary: pub
Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
The workshop UI's reconnect backoff is deduplicated and its service tokens move into the DOM-free services layer. The exponential backoff that two sockets each carried is extracted into one shared class that both now call. The four service tokens and their interface types leave the feature directories for the services layer, while the DOM-bound widgets stay behind and register against those tokens. This enforces the layering rule that imports flow through services, and lets consumers name a service contract without pulling its widget code.

- `ReconnectBackoff`: The duplicated exponential backoff in `WorkshopSocket` and `AgentSocket` collapses into this one class, whose initial and maximum delays are options; both sockets delegate reset, cancel, and schedule to it.
- `crates/workshop/ui/src/services/`: Four service tokens and their interface types move into new modules here — `STATUS_BAR`, `CLOSED_EDITORS`, `EDITOR_SETTINGS_SERVICE`, and `QUICK_INPUT_SERVICE` — leaving the DOM-bound widgets in parts to register against the tokens.
- `AGENTS.md`: The layering rule now states that a service token and its interface types always live in services, even when the implementing widget is DOM-bound and stays in parts.
- `crates/workshop/desktop/src/gateway/supervisor/mod.rs`: The launch-helper re-exports become test-only behind `#[cfg(test)]`.
- `test/reconnect-backoff.mjs`: A new unit test drives the class against scripted timers, pinning the one-second start, the doubling growth, the cap, and the reset a successful open triggers.
- `parts/`: No file still imports one of the four tokens from parts; every consumer now resolves them from services.

Design: removes clone-block @ crates/workshop/ui/src/services/workshop-socket.ts::WorkshopSocket
Design: removes clone-block @ crates/workshop/ui/src/services/agent-socket.ts::AgentSocket
Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
Correct code-level documentation in the workshop crates so it matches the code. The subsystem registry's description and invariants stop claiming it never names a subsystem, and its crate docs now record the three runtime links its sealed traits carry. Stale comments are corrected to name the modules and callers that exist now, and the import-guidance sentence now names the repository-root rules file explicitly.

Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
Record the exit gate results beside the baseline and mark the cleanup complete. Every canonical gate and the additional headless and doctest runs pass at least as green as their baseline entries. The two remaining frame-sense uses of shell are corrected to desk, leaving shell to mean only a terminal command shell.

- `crates/shared-ui/toast.ts`: the composition-root comment now reads "desk and view" instead of "shell and view".
- `crates/shared-ui/tokens.css`: the theme-surface comment now reads "for desk chrome" instead of "for shell chrome".
- `vibe/2026-09-24-2-workshop-crates-cleanup.md`: records the exit gate results beside the baseline.

Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
Plan: vibe/2026-09-24-2-workshop-crates-cleanup.md
close cppalliance#69

A section can now pin the sampling temperature and the generation token cap for the model it selects, so a prompt can hold them steady across runs. The section passes them as an optional table alongside the selection; model rounds on that selection send them, the handle the selection returns shows them, and rounds on the prompt-wide default keep the model's own values. A later selection replaces the options, and a field left out keeps the model's default. Invalid options fail the call with an error that names the option and states what was required and what was given, and the call selects nothing. The language and agent guides now document the table, its rules, and which rounds it affects.

- `UseOptions` holds the validated `temperature` and `max_tokens` and travels with the label in `ModelRuntime`. Its `apply` starts from the binding's own invocation and overrides only the fields that are set.
- `resolve_model_binding` applies the options only when the section has a selection; the prompt-wide default path returns the frozen binding unchanged.
- `ModelBinding::with_invocation` is a new public builder that replaces a binding's frozen per-request fields, and `public-api.txt` lists it.
- `models.use` returns a handle over the adjusted binding, so its `temperature` and `max_tokens` fields show the options. A later plain `models.use` clears them, and a `models.get` handle for the same label keeps the role defaults.
- `parse_use_options` rejects a non-table second argument, a third argument, a non-string key, an unknown key, or an invalid value, naming the option and required versus actual. It runs before `select`, so a rejected call records no selection.
- `option_check_order` fixes the checking order, non-string keys first and then names bytewise, so the reported error does not depend on the Lua state's hash seed. A test repeats each case on 32 fresh states.
- `decode_temperature` accepts a Lua integer such as 0, and `decode_max_tokens` accepts an integral float as well as an integer.
- `models_use_forwards_binding_completion_options_to_the_gateway` now pins both options on the request body and checks that a following section on the prompt-wide default sends neither.
- `models.loop` has no test with options set, and no test runs a round on the handle `models.use` returns; the handle tests read its fields only.

Design: new surface-growth @ crates/promptforge-internal/model-client/src/model/options.rs::ModelBinding::with_invocation boundary: pub
Design: extends surface-growth @ crates/promptforge-internal/lua/src/models.rs::install_models boundary: pub
Design: extends shared-mutable-state @ crates/promptforge-internal/lua/src/models.rs::ModelRuntime
Design: new value-object @ crates/promptforge-internal/lua/src/models.rs::UseOptions
Design: new pure-function @ crates/promptforge-internal/lua/src/models.rs::invalid_option deps: &str,&str,std::fmt::Display
Design: new pure-function @ crates/promptforge-internal/lua/src/models.rs::decode_temperature deps: &Value
Design: new pure-function @ crates/promptforge-internal/lua/src/models.rs::decode_max_tokens deps: &Value
Design: new pure-function @ crates/promptforge-internal/lua/src/models.rs::option_check_order deps: &Value
Design: new pure-function @ crates/promptforge-internal/lua/src/models.rs::parse_use_options deps: &MultiValue,Value
Design: new pure-function @ crates/promptforge-internal/lua/src/models-tests.rs::used deps: &ModelRuntime
Design: extends oversized-unit @ crates/promptforge-internal/lua/src/models.rs::install_models
Plan: vibe/2026-09-24-2-issues-69-59-70.md
close cppalliance#59

Only the Windows and Linux tray backends build the grayed and error icon variants, so macOS builds compiled the tint helpers as unused code and warned about them. The helpers now compile only for Windows, for Linux, and for tests. The helper bodies do not change.

- `tint` gets the same gate as its two callers, so the shared helper does not become the next unused item on macOS.
- `test` in the gate keeps the helpers compiled under test on every platform, so the existing luma and red-tint tests run on macOS too.
- `grayed` and `error_tint` do not exist in a macOS non-test build, so a future macOS caller fails to compile. Their doc comments name the Windows and Linux backends as the callers the gate follows.
- `crates/gateway/app/src/tray/logic.rs` gains no test. Only a build for a macOS target checks the excluded side of the gate.

Deferred: the aarch64-apple-darwin clippy check needs a Mac host
Plan: vibe/2026-09-24-2-issues-69-59-70.md
close cppalliance#70

A role that asks for no thinking now binds a model whose thinking can be switched, and every round under that role asks the model to turn thinking off; only a model that always thinks is refused. The model request timeout no longer caps the whole reply. It bounds the wait for the response headers and then for each next piece of the stream, restarting whenever data arrives, so a long reply that keeps streaming completes while a stalled one still fails as a retryable timeout. The default stays 120 seconds and no setting is added.

- `.timeout(self.request_timeout)` is gone from the request builder because it capped the body read as well. The send is wrapped in `tokio::time::timeout` instead, which bounds only the wait for headers.
- `ResponseChunks` now carries the timeout beside the response, and `next_chunk` bounds each read with it. The non-success path reads its error body through the same chunks, so that read is bounded too.
- `elapsed` wraps an expired deadline in `ClientTimeout` under `Error::Http`, so it reports exactly as a reqwest timeout does and `is_timeout` and `is_retryable` still hold.
- `fill_model_bindings` refuses `no-thinking` only when the model's thinking is `ThinkingMode::Always`. A switchable model now prepares, and its request carries `enable_thinking` set to false.
- `request_timeout` keeps its signature but now means the longest wait for the next receive. Its docs and both user guides say so, and the harness manifest gains the tokio `time` feature for it.
- `a_hard_keyword_the_current_model_fails_is_reported` now uses an Always model for its `no-thinking` refusal case, since a switchable model no longer fails.
- `spawn_paced_gateway` writes a chunked SSE body piece by piece with real sleeps of 30 to 50 ms against a 100 ms budget, so the steady and trickle tests depend on wall-clock pacing.

Design: new pure-function @ crates/harness/models/src/transport.rs::elapsed deps: tokio::time::error::Elapsed
Design: removes newtype @ crates/harness/models/src/transport.rs::ResponseChunks
Design: new flag-parameter @ crates/harness/models/src/transport/tests/limits.rs::spawn_paced_gateway deps: Vec<(Duration, String)>,bool
Design: new pure-function @ crates/harness/models/src/transport/tests/limits.rs::stream_close
Design: new pure-function @ crates/harness/models/src/transport/tests/limits.rs::budgeted_client deps: &str,Duration
Repairs: no-thinking is satisfied by a switchable model @ crates/promptforge-internal/engine/src/execute/fill.rs::fill_model_bindings - a no-thinking role on a switchable model was refused as an unmet requirement
Repairs: a stream that keeps arriving is never cut off @ crates/harness/models/src/transport.rs::GatewayClient::complete - a streamed reply longer than the request timeout failed as a timeout
Plan: vibe/2026-09-24-2-issues-69-59-70.md
Plan: vibe/2026-09-24-2-issues-69-59-70.md
In the workspace crate's path-confinement tests, every helper and test that reads from the parent module compiles only on Windows, so on other platforms the glob import had no users. The Linux lint job denies warnings and failed on that unused import. Gating the import to Windows removes the warning at its source rather than suppressing the lint, and the platform-independent skip helper and its two tests still build everywhere.

- `use super::*;` now carries `#[cfg(windows)]`, the same gate as every item that uses it. The ungated skip helper and its two tests use nothing from the parent module, so they compile unchanged on all platforms.

Deferred: Linux clippy confirmation waits for the next CI run of vibe2.
Plan: vibe/2026-09-25-1-vibe2-debt-removal.md
The editor's save and the conflict dialog's Overwrite now write the file through one shared helper, so both record a timed-out or conflicted write the same way. Before, an Overwrite that timed out showed the raw timeout error, did not mark the conflict token unknown, and did not record the text it sent. The next save could then send a write without first re-reading the file, and when the timed-out Overwrite had in fact landed, the next save reopened the conflict dialog instead of adopting the file's new token.

- `writeCurrent` is a new private method that performs the write for the panel's own file and alone records its outcome: the text sent, the new token, the unknown-token flag, the saved baseline, the timeout message, and the conflict dialog. It rethrows any other error, and its doc comment bars Save As from it because a timeout marks the open file's token unknown.
- `overwrite()` now answers a timeout with the "may or may not have been written" message and marks the token unknown, so the next save re-reads the file before sending any token. It also records the text it sent, so when a timed-out Overwrite did land, the next save adopts the fresh token instead of reopening the conflict dialog.
- `save()` keeps its reconcile read and its observable behavior; its write, token update, saved baseline, and timeout and conflict handling now run inside the helper with the same results.
- `crates/workshop/ui/test/editor-save-timeout.mjs` gains two cases that click the dialog's Overwrite button: one where the Overwrite times out without writing, and one where it times out after its write landed. Their assertions fail against the previous Overwrite and pass now, and the header comment lists six cases.
- `isModifiedConflict` still guards the outer handlers in `save()` and `overwrite()`, but write conflicts no longer reach them; only an error from the file read can.

Design: new pure-function @ crates/workshop/ui/test/editor-save-timeout.mjs::overwriteButton deps: panel
Deferred: relabeling the late-write case in the save-timeout test header
Repairs: a timed-out write leaves the token unknown @ crates/workshop/ui/src/parts/editor/editor-panel.ts::EditorPanel::overwrite - the save after a timed-out Overwrite wrote without re-reading the file
Repairs: reconciliation compares the disk with the last text sent @ crates/workshop/ui/src/parts/editor/editor-panel.ts::EditorPanel::overwrite - a save after a timed-out Overwrite that landed reopened the conflict dialog
Plan: vibe/2026-09-25-1-vibe2-debt-removal.md
The heartbeat startup test checks that refresh retries stop once a model selection is restored by confirming the request count stays flat across a short quiet window. That window advanced a paused clock, but the refresh probes are real loopback HTTP requests that a paused-clock advance cannot drive, so the count could not grow and the check passed whether or not retries had stopped. The window is now a real sleep of four test intervals, so retries that continue after restoration show up as extra requests and fail the check.

- `tokio::time::sleep(TEST_INTERVAL * 4)` replaces the pause, advance, and resume sequence for the quiet window. The test now spends that time on the wall clock so the loopback probes can run during it.
- `requests_after_restore` is still captured before the window and compared after it by the same equality assertion. Only the wait between them changed.

Plan: vibe/2026-09-25-1-vibe2-debt-removal.md
The server crate's module documentation listed the workshop socket as part of the sessions subsystem, which no longer owns it. This change moves that socket out of the sessions inventory in both places it is described and writes the sessions subsystem's children as checked documentation links. A new CI step builds the server documentation with private items and treats every warning as an error, so renaming or removing a linked module breaks the build instead of leaving the docs stale.

- `Docs (workshop-server, private items)` builds the `workshop-server` docs with `--document-private-items` under `RUSTDOCFLAGS: -D warnings`, directly after the workshop doctests. Any doc warning on a crate-private item now fails CI.
- `crate::workshop_socket` is now the documented home of the `/ws` workshop socket, outside the sessions subsystem, and the `session` child is gone from the sessions inventory.
- `socket`, `relay`, `state`, and `bindings` are intra-doc links in the sessions inventory, so renaming or removing one of those modules breaks the docs step.
- `NoopAssets` is a plain code span in the `AssetServer` docs because it is compiled only under `headless` or `test`, so a default docs build cannot resolve a link to it.
- `app::compose`, `app`, `agents`, and `workshop_socket` stay unlinked code spans in the crate-root inventory, so the docs step does not catch those names going stale.
- `workshop-server` changes only in doc comments; no signature or runtime code changes.

Deferred: The crate-root inventory in `crates/workshop/server/src/lib.rs` keeps `agents`, `workshop_socket`, `app`, and `app::compose` as unlinked code spans.
Plan: vibe/2026-09-25-1-vibe2-debt-removal.md
Plan: vibe/2026-09-25-1-vibe2-debt-removal.md
@vinniefalco
vinniefalco merged commit 890ae2f into cppalliance:master Sep 25, 2026
19 checks passed

This branch was successfully deployed

1 active deployment
github-pages — 890ae2f6 Deployed Sep 25, 2026 by vinniefalco via deploy #80
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