Quirl0.1 RC
Contributing

AGENTS.md — working on Quirl

Canonical Quirl project documentation synced from AGENTS.md.

Quirl is a Rust workspace implementing a shell with typed data pipelines and an embedded, sandboxed Lua 5.4 extension runtime. This file captures the project-specific rules that generic Rust knowledge won't give you.

Engineering priorities: safety, performance, developer experience

In that order. This is Quirl's adaptation of TigerStyle, not a verbatim copy: rules written for a fixed-memory database must be translated to a general-purpose Rust shell.

  • Safety means preserving user data, terminal state, child-process lifecycle, capability boundaries, and deterministic resource limits.
  • Performance means predictable latency and memory use, especially for interactive paths, streams, untrusted input, and extension callbacks.
  • Developer experience means precise names, small APIs, actionable errors, reproducible tests, and one obvious source of truth.

Prefer the design that advances all three. Feature gaps are acceptable; known correctness, security, resource-leak, or terminal-corruption defects in the changed path are not "future cleanup." Fix them or keep the feature out.

For changes involving concurrency, process lifecycle, persistence, protocols, or untrusted execution, write down the failure model and invariants before implementation. Ask what can go wrong at every boundary, including partial success and cleanup failure.

Bound everything that can grow or wait

Every operation driven by user, filesystem, process, or extension input needs an explicit limit or a demonstrated streaming bound:

  • bytes retained and bytes scanned;
  • collection, queue, recursion, and nesting depth;
  • records, fields, files, processes, and pipeline stages;
  • instructions, wall time, retries, and callback duration.

Enforce bounds where work enters the owning crate, fail early with a ShellError carrying ErrorCode::ResourceLimit, and include the configured limit plus observed usage in error context when safe. A long-running event loop is allowed only when each turn has bounded work and cancellation is observable.

Avoid recursion on untrusted or attacker-controlled structure. Use an explicit stack with a depth limit. Recursion over a small compile-time-bounded domain is acceptable when the bound is obvious and tested.

Assertions, validation, and invariants

Distinguish programmer errors from expected operating errors:

  • Invalid source, configuration, protocol data, I/O, resource exhaustion, and extension behavior are operating errors. Validate them and return ShellError at shell-effect boundaries; a core-independent foundation parser may instead return an inert domain diagnostic for its consumer to map. Never panic on user-controlled input.
  • Impossible internal states are programmer errors. Encode them with types first, then use assert! or debug_assert! where an executable invariant materially improves review and testing. Do not use assertions as a substitute for handling a reachable error.
  • Assert or validate both the positive space and the negative boundary. If a writer guarantees a property, validate it again at the reader when crossing persistence, process, plugin, or protocol boundaries.
  • Split unrelated compound assertions so failures identify the violated invariant precisely.
  • Check compile-time relationships among protocol sizes, limits, and constants when Rust can express the check clearly.

Tests must exercise valid inputs, invalid inputs, and transitions from valid to invalid. Fault paths must prove cleanup as well as the returned error.

Explicit control flow and resource ownership

  • Prefer direct, structured control flow and a small number of domain-shaped abstractions. Every abstraction must make ownership, bounds, or invariants easier to see.
  • Keep variables in the smallest useful scope. Validate and transform values close to where they are consumed to reduce time-of-check/time-of-use gaps.
  • Centralize branching and state transitions in the owning function; push bounded iteration and pure calculation into focused helpers. Leaf helpers should not mutate distant state.
  • Aim for functions that fit on one screen, roughly 70 lines or fewer. Refactor longer functions unless keeping a state machine or transaction together is clearer and safer; document that reason when it is not obvious.
  • Prefer positive conditions and exhaustive match expressions. Break dense boolean expressions into named predicates or branches when a reviewer cannot readily enumerate the cases.
  • Acquire resources into RAII guards immediately. Partial initialization must unwind safely: close descriptors, reap children, restore terminal modes, remove temporary files, and preserve the original error.
  • Signal handlers and asynchronous observers record state only. Run Lua, adapters, hooks, and user callbacks at explicit safe points, never while a process graph, job entry, terminal handoff, or persisted record is only partially committed.

Use fixed-width integers for persisted, serialized, protocol, process, and resource-limit fields. Use usize for in-memory indexing and Rust collection APIs; convert at a checked boundary rather than with unchecked as casts.

Names, comments, and API shape

  • Choose precise nouns and verbs; avoid abbreviations unless they are standard domain terms. Do not overload one name with context-dependent meanings.
  • Put units and qualifiers in names when the type alone is ambiguous, for example timeout_ms, output_bytes_max, or process_count.
  • Use parameter structs when multiple same-typed arguments can be swapped or when boolean/optional arguments would make call sites unclear.
  • Pass correctness- and security-relevant options explicitly. Do not rely on a dependency's default timeout, capacity, permissions, standard library, or follow-symlink behavior.
  • Comments explain why an invariant, ordering, or unusual mechanism exists. Tests with non-obvious setup also explain their goal and method. Comments are complete, maintained sentences rather than restatements of the code.
  • Order files for a top-down first read: public contract and principal control flow before implementation details, with tests last.

Documentation is part of the interface

Documentation is a required correctness property, not optional follow-up work. Every public Rust crate, module, type, field, variant, constant, trait, and function must have //! or /// documentation that explains its contract. Document invariants, units, resource bounds, side effects, errors, and security assumptions where they matter; do not merely restate the identifier. The workspace denies missing_docs, and cargo xtask check builds all Rustdoc with warnings denied, so undocumented or broken public APIs cannot land.

Rustdoc describes the Rust API. User- and AI-facing command documentation has a separate runtime source of truth: Catalog::builtin() supplies help, completion, quirl describe, quirl doc, LSP, MCP catalog data, and quirl agent; HOST_API supplies the Lua SDK and AI capability catalog. Keep those records complete when behavior changes. Never copy command contracts into a parallel documentation table or assume Rust /// comments are available via runtime reflection. See docs/documentation-system.md.

Use rustfmt; do not hand-align code against the formatter. Workspace warnings and Clippy lints are part of the design contract, not optional polish.

Performance and dependencies

For hot paths or potentially large inputs, make a rough resource sketch before implementation: expected and maximum bytes, allocations, syscalls, processes, and latency. Optimize architecture and asymptotic behavior first; use benchmarks to justify micro-optimizations and retain regression evidence for important claims.

Keep control-plane work such as configuration, catalog construction, and extension discovery out of per-keystroke and per-row data paths. Batch I/O and cross-thread communication when it improves bounds and latency without delaying cancellation or interactive feedback.

Dependencies are a supply-chain, compile-time, binary-size, and maintenance cost. Prefer the standard library and existing workspace dependencies. A new dependency must have a concrete owner, justify why a small local implementation is less safe or maintainable, disable unnecessary features, and preserve the crate layering. Do not add a dependency for trivial convenience.

Architecture: respect the layering

Dependency direction is strict and one-way, codified in ADR 0016. The ADR contains the complete allowed edge table; in summary:

  • quirl-catalog, quirl-core, and quirl-syntax are foundation peers with no dependencies on other Quirl crates.
  • Contract, data, Lua, picker, plugin, process, LSP, and UI crates use only the inward Quirl edges listed in ADR 0016. In particular, quirl-picker may depend on core, and quirl-process may depend on core and syntax.
  • quirl-cli is the sole product composition root and the only product crate permitted to assemble every layer.
  • Native Quirl process execution and reusable child-lifecycle containment belong to quirl-process; core retains passive capability and outcome values.

When adding functionality, put it in the lowest crate that can own it, and never invert an arrow to make something compile.

One operating error type at effectful boundaries

Fallible cross-crate service and shell-effect paths return Result<T, quirl_core::ShellError>. ShellError carries an ErrorCode, message, labels, context, and help, and derives Serialize so --format json works for free.

Core-independent foundation parsers may return owned, inert diagnostics to avoid inverting the crate graph. Execution, I/O, and persistence consumers map those diagnostics to ShellError while preserving message, span, help, and source identity. Read-only presentation adapters may map them directly to their protocol's diagnostic value. Do not use this exception for operating failures.

  • Do not introduce anyhow, new error enums, or thiserror derives. Errors are hand-built for serialization control.
  • Map new failure domains onto an existing ErrorCode, or extend the enum in crates/quirl-core/src/error.rs if genuinely new.
  • Every error must render well both as JSON and through quirl_ui::render_error. Write the help text; a diagnostic without a suggested fix is half done.
  • clippy::unwrap_used and clippy::expect_used are denied by workspace lints (tests are exempt via clippy.toml). Rare true invariants such as mutex poisoning may carry a targeted #[allow] with a reason.

The Lua boundary is a security boundary

All Lua embedding lives in quirl-lua. Rules that must hold:

  • Every VM runs under a LuaPolicy (memory limit, instruction budget, wall deadline, cancellation). Never create an unrestricted mlua::Lua.
  • The stdlib is restricted (TABLE|STRING|MATH|UTF8); io, os, debug, require, and package stay removed. Do not re-expose them.
  • Values crossing Lua → Rust are deserialized into typed structs with #[serde(deny_unknown_fields)] and then validated. Never consume raw mlua::Value in higher crates; convert at the boundary.
  • A misbehaving script must fail with a ShellError (Lua, Validation, or ResourceLimit) — it must never panic the host or hang the session.

Generated artifacts: edit the source, not the output

  • The Lua SDK (LuaLS stubs, JSON schema, Markdown docs) is generated from the single HOST_API table in quirl-lua. To change the host API, edit HOST_API, then run cargo xtask sdk to regenerate docs/quirl.lua (a test asserts the checked-in file matches sdk_lua() exactly). Never hand-edit docs/quirl.lua.
  • Command metadata (help, completions, docs, AI export) comes from Catalog::builtin() in quirl-catalog. New commands and flags are added there once — never hardcode help strings or completion lists elsewhere.
  • Rust API documentation is generated directly from //! and /// comments by cargo xtask docs; never hand-edit files under target/doc.

Workspace hygiene

  • Toolchain is pinned to Rust 1.88 via rust-toolchain.toml; don't use features from newer compilers.
  • spikes/ directories are intentionally separate Cargo workspaces so that mutually exclusive engine features (Luau, QuickJS, …) never unify with the shell's Lua 5.4 build. Never add spikes as workspace members or import their dependencies into crates/.
  • quirl-bench is research tooling (publish = false), not product code.
  • Unsafe Rust is prohibited in crates/ except for the private cfg(windows) Job Object FFI wrapper in quirl-process sanctioned by ADR 0016. Every unsafe block there requires a local safety explanation; expanding that audited boundary requires a new ADR and security review.
  • No feature flags on the main crates; keep it that way unless an ADR says otherwise.

Testing

  • Tests live in-crate as #[cfg(test)] mod tests in the same file — no separate tests/ directories. Follow that pattern.
  • Name tests as behavior sentences in snake_case, e.g. instruction_budget_stops_runaway_code.
  • cargo xtask test runs workspace tests, bounded seeded C1 differential cases against available Bash/Zsh references, and guest-side Lua tests. Replay a generated failure with its reported --seed and --cases; never introduce a wall-clock seed inside a test.
  • Sandbox changes need adversarial tests: prove the budget, limit, or restriction actually trips.
  • Follow docs/testing-strategy.md for contract, fault, PTY, guest-runtime, and release evidence.

Process

  • cargo xtask check is the canonical local quality gate and must pass before every commit. It includes the workspace Rustdoc build and missing-public-docs enforcement. The only CI workflow is the bounded daily Bash/Zsh simulation swarm; other local Cargo tasks deliberately remain local while project traffic is low.
  • Conventional commits (feat, fix, docs, refactor, chore, bench), present tense, optionally scoped, e.g. feat(lua): add completion budgets.
  • Significant design choices go through an ADR in docs/decisions/. ADR 0001 is the standing contract: Lua is the only extension language, Rust validates everything at the boundary. ADR 0016 fixes the crate dependency and runtime-ownership graph. docs/language-design.md is the product specification; its §13 delivery sequence and acceptance gates define what each phase must prove.

xtask command conventions

  • Use the stable workspace xshell dependency, Shell, and cmd! for linear development-task orchestration. Set the workspace directory once on the Shell, interpolate arguments instead of formatting a command string, and let xshell reject nonzero statuses.
  • Keep std::process::Command for subprocess boundaries that require exact byte capture, custom stdin/stdout ownership, polling, deadlines, process-group containment, cancellation, or deliberate nonzero-status inspection. The compatibility simulator is the reference for this case.
  • Do not invoke a platform shell merely to join commands or expand arguments. Express sequencing in Rust and pass every dynamic argument through cmd! interpolation so it cannot become shell syntax.
  • Keep filesystem transactions in Rust. xshell improves command orchestration; it does not replace atomic writes, explicit bounds, RAII cleanup, or structured error context.
  • Quirl is a prototype moving fast: prefer extending the existing patterns (ShellError, HOST_API, Catalog::builtin, LuaPolicy) over inventing parallel mechanisms. Breaking changes are fine; drift is not.

On this page