Multi-crate Rust workspace for the PromptForge pipeline engine, the harness that hosts it, the inference gateway, and the Workshop desktop product.
- Do more with less. Prefer simple, foundational primitives over specific solutions: a primitive that naturally enables today's functionality and also generalizes beats a custom mechanism specified as a laundry list of requirements. Generality is the payoff, not a goal.
- When evaluating how to implement a capability, check whether the existing facilities subsume the work before building new machinery. Prioritize in this order:
- Reuse an existing facility
- Make the smallest improvement to an existing facility which enables the capability.
- Add a new facility. New machinery must have a material benefit beyond tidiness.
- When improving an existing facility, prefer an improvement that serves a problem class beyond the current case over one that solves only the case at hand, when the general shape costs no more.
- Error and status messages are designed assuming model consumption: concise, factual, and self-contained, naming what is missing or unmet with required versus actual, because a message may arrive as tool output that a model reasons about.
- Workshop is a user-facing agentic development environment: a Tauri desktop application with an HTML/CSS/TypeScript UI
- PromptForge is the runtime execution engine for the PromptForge Prompting Language: structured Markdown files with live Lua code fences
- Gateway is an independent service that proxies local and remote inference through one OpenAI-compatible HTTP and WebSocket endpoint
- Harness is the engine's only production host: it owns the tokio runtime, the performers that execute the engine's effects, agent sessions, and the run log; Workshop and other clients drive runs through it
- shell: a command shell in a terminal, and nothing else.
- desktop app: the Tauri crate (package
workshop), undercrates/workshop/desktop/. - server: the build check's tier that holds only
workshop-server. - desk: a UI's main frame.
- workbench: the VS Code-style UI architecture, and the Model menu snapshot frame on the
/wssocket. - platform:
@workshop/platform, the family's browser-side UI mechanics (commands, menus, keybindings, context keys, services, panels, lifecycle). - workshop socket: the
/wssocket, served by the server'sworkshop_socketmodule. - page: a routed screen behind a tab.
- view: a DOM component.
- placeholder: what a lazy panel shows while its code chunk loads.
- entry bundle: the eagerly loaded composition:
crates/workshop/ui/src/main.tsand the*.contribution.tsmodules it imports. Lazy panels never import a module inside it, directly or through another import. Everything else that eager and lazy code both import, such as@workshop/platform,services/,base/, and shared parts modules likeparts/layout/zones.ts, is shared code. - take: one push-to-talk dictation recording and the transcript region it owns in the target text.
- zone: a named Dockview tab bank in the Workshop layout:
left,main, andright, withbottomreserved. A zone survives its last panel closing. The default layout is product policy set at the composition root (LAYOUT_POLICY, registered inmain.ts): the workspace tree anchorsleftand the agent session anchorsright. Document editors open inmainthrough the editor type's default zone. - chip: an inline pill in the chat box: a mention, a typeahead row, or an attachment. The host owns its kind and payload.
- contribution: an item a subsystem self-registers into
workshop-registry: a route, background task, state handle, or push sink. In the SPA, an eager*.contribution.tsmodule that registers a feature's command, menu, tab-menu, and quick-access rows and its panel types at module scope, before any service exists. - status bus: the broadcast channel in
workshop-statusthat carries status bar updates from every subsystem to every connected/wssession and retains the newest update for sessions that connect later. - sidecar: the local
promptforge-gatewayprocess the desktop app attaches to through its discovery file or launches beside itself. An explicitly configured gateway endpoint is not a sidecar. - publication: installing a validated gateway endpoint into the gateway binding as its next generation. The whole snapshot of client, base URL, and bearer is built first and stored atomically, then long-lived tasks are woken to reconnect. Teardown closes publication for good.
- The four main products are PromptForge, Gateway, Workshop, and Harness
- Workshop crates are named workshop-* and must not depend on gateway crates; workshop crates may name the gateway public pair, the promptforge public API, and
harness - Gateway's public surface is two root crates,
gateway-api-typesandgateway-api-discovery; everything else lives under crates/gateway/, a manifestless container private to the family - no outside crate may depend into it, and workshop crates may name only the public pair. Gateway crates must not depend on promptforge or workshop crates - Workshop crates live under crates/workshop/, a manifestless container private to the family - no outside crate may depend into it; the desktop app is crates/workshop/desktop (package
workshop), and the server and its subsystems sit beside it with short directory names - Harness crates are named
harnessand harness-. Their public surface is one root crate,harnessat crates/harness/: a facade of single-item re-exports, laid out likepromptforge. Everything else lives under crates/harness-internal/, a fourth manifestless container private to the family, andharnessis the only outside crate permitted to depend into it. harness- crates may depend only onpromptforge, never on gateway, shared, or workshop crates; crates outside the family, build-* included, reach the harness only throughharness; promptforge-* and gateway-* crates must not depend on harness crates - The composed topology rule: a crate in a family container (crates/promptforge-internal/, crates/gateway/, crates/workshop/, crates/harness-internal/) may depend only on crates at the crates/ root and its own siblings; the root is the public layer. Crates named build-* are meta tooling, exempt from container privacy
- PromptForge crates are named
promptforgeand promptforge-* and must not depend on gateway, workshop, or harness crates - PromptForge has one public crate,
promptforgeat crates/promptforge/: a facade of single-item re-exports grouped into documented role modules. Crates outside the family may depend only onpromptforge, never on a promptforge-* crate. Everything else lives under crates/promptforge-internal/, a manifestless container private to the family that holds the engine (promptforge-engine), the types crate (promptforge-types), the virtual filesystem (promptforge-vfs), and the lua, parser, and model-client crates;promptforgeis the only outside crate permitted to depend into it - The desktop app (the
workshopcrate) depends onworkshop-server-apiand never onworkshop-server; the facade is the desktop app's entire view of the server - Shared crates are named shared-, contain the public API surface across products and downstream crates, and must not depend on any product crates. PromptForge's own public surface is the
promptforgefacade, and its types crate (promptforge-types) has left shared- for the private container; Gateway's is gateway-api-types and gateway-api-discovery, named gateway-* now that both have left shared-*;promptforge-typesholds the shared vocabulary together with host-support code (the untrusted guards, the cancellation tree, and the event emitter) - Crates named build-* are for building specific outputs
- Dependency rules bind all kinds: normal, dev, build, and target-specific dependencies. One exception: a crate under crates/promptforge-internal/ may list
promptforgein[dev-dependencies]only so its doc examples compile against the facade paths hosts see. No unit test, integration test, or bench imports it. This edge is exempt from the one-way flow rule under Structural Rules.
- Prefer types and compiler checks, then behavior tests and deterministic fault injection. Add a structural check only with explicit user approval for a stable product or security boundary that has no ordinary equivalent.
- Repository policy binds plans. A plan cannot introduce a source parser, snapshot, allowlist, count, ceiling, topology check, import walker, or other structural enforcement unless the user explicitly approves that exception.
- Behavior changes ship with tests in the same change. Preserve product and behavior tests during refactors. Structural tests that an approved plan identifies as unsupported may be removed without replacement by another structural proxy.
- A Cargo feature gates a real constraint such as a toolchain requirement or heavy native build. It does not describe product shape. Feature-disabled builds must not leak optional types into core paths.
- Runtime and serve paths never compile native dependencies or invoke build tools. Library and serve paths return failures instead of exiting the process or installing process-global state.
- Long-running gateway work reports through
gateway-progress, a private gateway family crate: a producer begins an activity with a text, replaces the text as work moves, and drops the guard when done. Consumers outside the family read only theProgresswire type fromgateway-api-types. - Unsafe code stays in its explicitly owned boundary. Every unsafe block documents its safety invariants immediately before the block.
- Comments explain a non-obvious constraint, ordering requirement, or workaround. Every platform or external-bug workaround cites its upstream issue URL in the explanatory comment.
- JSON that reaches the run log or a replay comparison round-trips exactly -
to_value,to_string,from_stryield an identical value, object keys stay canonical (sorted), numbers must be finite, and serde_jsonpreserve_orderis never enabled. Exact parsing (float_roundtrip) carries that guarantee; a value derived and then logged is additionally rounded to its meaningful precision at the source.
- Full suite:
cargo nextest run --locked --workspace --exclude workshop --exclude workshop-server --exclude workshop-server-api --all-features, then doctests viacargo test --workspace --exclude workshop --exclude workshop-server --exclude workshop-server-api --all-features --doc; workshop crates separately:cargo nextest run --locked -p workshop -p workshop-server -p workshop-server-api. - Linter:
cargo clippy --workspace --exclude workshop --exclude workshop-server --exclude workshop-server-api --all-targets --all-features -- -D warnings(workshop:cargo clippy -p workshop -p workshop-server -p workshop-server-api --all-targets -- -D warnings). Clippy is a superset ofcargo checkand shares no artifacts with it, so never run a standalonecargo check --workspacebeside the clippy runs; the one exception is the headless feature-combination gatecargo check -p gateway --no-default-features, which checks a build shape clippy --all-features does not cover. - Formatter:
cargo fmt --all --check. - Docs:
cargo doc --workspace --no-deps --all-features --exclude workshop --exclude workshop-server --exclude workshop-server-apiwithRUSTDOCFLAGS="-D warnings"; user guide:cargo xtask site --books-only. Rustdoc lints are not covered by clippy; never skip the docs gate. - Facade docs:
RUSTDOCFLAGS="-D warnings" cargo doc -p promptforge --no-deps, without--all-features, so the facade's docs build with default features. - Facade surface:
cargo +<pinned nightly> xtask api --check, where the pinned nightly is the one named incrates/build-xtask/src/api/toolchain.rs; on any other toolchain it fails at once, naming the nightly it needs. It checks that every path a surface item's signature, fields, bounds, impls, or doc links name is a facade re-export (or std, core, alloc, or an allowlisted crate), that no surface doc text names an internal crate, and that the surface listing matches the committedcrates/promptforge/public-api.txt. - Boundary and structural harness:
cargo test -p build-xtask.
- Dependencies flow one way: server -> features -> services -> vocabulary. Never add a dependency from a lower tier to a higher one. If Cargo rejects a cycle, the design is wrong, not the graph. On the SPA side, lazy panels never import a module inside the entry bundle, directly or through another import. Everything else that eager and lazy code both import, such as
@workshop/platform,services/,base/, and shared parts modules likeparts/layout/zones.ts, is shared code. - Every workshop-* and harness-* crate's lib.rs opens with a //! doc containing a
## Invariantsmarker that lists what the crate may depend on and what it may not. The marker is mandatory for those families by package name; a family crate without it failscargo test -p build-xtask. The desktop app (theworkshopcrate) is exempt, and theharnessfacade has no marker, likepromptforge. Read the marker before adding an import. - No file in a crate with the marker exceeds 500 lines. If an edit would push a file past 500, split first, then edit.
cargo test -p build-xtaskenforces the tier graph, the mandatory marker, the lint inheritance, the ceiling over the Rust files in every workshop-* and harness-* crate plus any other crate with the marker, and the product-boundary matrix above (including the single-public-crate rules for promptforge and harness and the container privacy rules for crates/promptforge-internal/, crates/gateway/, crates/workshop/, crates/harness-internal/, and the nested crates/gateway/stt/ subsystem, whose only family-visible crate is gateway-stt) across every workspace manifest; the desktop app (theworkshopcrate) is exempt from the marker and the ceiling until the headless agent mode plan. - Source directories are flat by default. A subdirectory of source files must contain at least three files, and its count includes the files in its nested subdirectories; one or two files belong beside the parent module as
foo-bar.rs(parent stem, dash, kebab label), wired with an explicit path attribute so the module name stays clean:#[path = "foo-bar.rs"] mod bar;. The two forms are convertible in both directions: when afoo-*.rssibling group grows to three files, rehydrate it into afoo/subdirectory in standard module layout (foo/bar.rsbesidefoo.rs) and drop the path attributes; when a subdirectory shrinks below three files, flatten it back to kebab siblings. Both conversions apply at each level of nesting. Apply whichever conversion applies when you touch files in a group on the wrong side of the line. Top-leveltests/andbenches/trees are exempt; they follow Cargo target conventions.
- CSS lives beside its TypeScript, never in a separate styles/ tree. A designer finds the styles for the agent chat at parts/agent/agent-session.css, not by grepping a flat directory. Every feature directory is self-contained: its .ts and .css sit together. A part that the panel registry loads lazily has an
index.tsas its chunk entry: each feature registers its own panel type from its*.contribution.tswithload: () => import("./index"), and other parts need none. - No raw color, size, or spacing values in Workshop UI component CSS. Use --ws-* tokens. The sizes and semantic tiers live in
@workshop/look: primitives in look/sizes.css, intent aliases in look/semantic.css. Per-component overrides live in ui/src/tokens/component.css. Sizes and elevation are themed in look/semantic.css, and colors in look/tokens.css, whichlook's own component sheets read directly. - No
localStorage. The SPA never reads or writes browser storage; every persisted value goes through theui-storageadapter to the server. UI state has two homes by scope: account state (preferences and ephemera alike, such as editor toggles, zoom, recent files, and command history) goes toui-state.jsonin the state directory (theworkshop-user-statecrate,/user/state), and workspace-scoped state (anything that should travel with the.pfworkdocument) goes to the workspace file through the server (/workspace/file/state). A new persisted value is a new allow-listed key in one of those two buckets, added on the server first.