Skip to content

Development

Provider-plugin authoring uses the three GitTPL repositories directly. The parent template composes reviewed source and integration modules, including an initially empty plugin. Each generated provider owns its descriptor, module builds, tests and CI/release workflows. Core CI uses plugins/example as a packaging-tooling fixture. Move that project to a standalone repository without changing the stable plugin and module IDs.

Working on a standalone provider

Build the provider's own source and integration modules with Cargo and Bun before packaging their declared artifacts. From the provider root, run memcastle plugin inspect --json, memcastle plugin package, then memcastle plugin test dist/ID-VERSION.tar.gz against the exact resulting archive. The descriptor plugin.toml stays unchanged; the archive alone carries generated versioned manifest.toml metadata and an external .sha256sum. Use a tagged Git commit, an SCM development version, or an explicit --version override. Review the archive inventory and checksum and commit source Cargo.lock files with source changes. The smoke test uses a disposable palace and validates preview, consent, installation and source activation; it never uses the configured palace or agent installation. Agent registration and legacy adoption remain provider-specific checks. Standalone provider CI checks source conformance and archive installation against the successful main CI prerelease and selected plugin-capable release trees (latest stable patch), recording the exact Core revision or resolved tag. Core CI builds and checks only the plugins/example fixture on each pull request and main push. See Plugin packages for the release assets, SHA verification, reusable workflow inputs and compatibility policy.

Prerequisites

  • Node and bun, only for web/ and integrations/ (mise run web:check and integrations:check install them for the task; the Rust build needs neither).
  • Rust, via rustup, which installs the channel pinned in rust-toolchain.toml (mise does not manage Rust itself).
  • The remaining tools (nextest, prek, ast-grep, typos, ...) via mise — run mise install once.

That's it — SurrealKV, the embedded storage engine, is pure Rust, so the storage engine itself needs no C/C++ toolchain. (A transitive TLS dependency, aws-lc-sys, may use cmake on some targets; that is unrelated to storage.)

Everyday tasks

mise run build      # cargo build
mise run test       # the basic suite: cargo nextest run, without the WebAssembly tests (accepts nextest selectors)
mise run test:wasm  # the WebAssembly suite: every tests/wasm_*.rs binary (slow; needs the wasm32-wasip2 target)
mise run sources:check      # build and conformance-test every source under sources/
mise run sources:test -- directory   # the same for one source, which is what each CI leg runs
mise run lint       # cargo clippy --all-targets --all-features -- -D warnings
mise run format     # cargo fmt --all
mise run guards     # the architecture guard, including its rule fixtures, described below
mise run integrations:check # test the Core-shipped integration and install its bundle
mise run integrations:build # bundle integrations/*/src into integrations/*/dist, for `--assets-dir "$PWD"`
mise run integrations:package # lay out what a release ships in target/bundled-integrations
mise run web:check  # typecheck, test, build and package the web UI, then serve it from a real daemon (needs bun and node)
mise run web:build  # build web/ into web/dist, for `--assets-dir "$PWD"` with `web.enable`
mise run web:dev    # the dashboard's dev server, proxying /api to the running daemon
mise run web:package # lay out what a release ships in target/bundled-web
mise run eval       # measure retrieval quality and latency on the bundled dataset (docs/retrieval-evaluation.md)
mise run eval:baseline # rewrite the committed retrieval baseline, after a change meant to move quality
mise run eval:compare -- a.json b.json # what changed between two retrieval reports
mise run eval:longmemeval -- file.json # session retrieval on a downloaded LongMemEval file
mise run check      # every lint, the guards, both test suites, the sources, the integrations and the dashboard, without modifying the tree
mise run ci         # check plus the docs build: the local equivalent of CI's lint,
                    # test and docs steps
mise cli <args>      # run memcastle from source, e.g. `mise cli status`
mise run docs       # serve this documentation locally
mise run docs:build # build it into site/

The project-owned web:build, web:package, integrations:build and integrations:package tasks skip their build when their inputs have not changed and their required outputs still exist. Changing a source or removing an output makes the corresponding task run again; use mise run --force web:build (or the other task name) to rebuild regardless of freshness. The test and check tasks still run on every invocation, including when used through mise run check or mise run ci.

prek install (once) wires formatting, spelling, actionlint, markdownlint, commitlint, basic file hygiene, and the architecture guard described below into git commit as hooks. Full-project Clippy runs in CI and with mise run lint, rather than on every local commit.

Running the daemon locally

mise cli serve
# in another terminal:
mise cli status
mise cli mine ./src
mise cli job list
mise cli job demo --steps 5   # exercise the scheduler without mining anything
mise cli search "job scheduler"
mise cli daemon stop

In this repository mise.toml sets development defaults in its [env] table: MEMCASTLE_ASSETS_DIR is the checkout and MEMCASTLE_WEB_ENABLE is true. They apply to mise run, to mise cli and to any shell with mise activated once you are in the project directory, so mise cli serve already serves the dashboard from web/dist (after mise run web:build) and finds the integrations and sources of the checkout, with no flag. Because the variables follow the shell, any memcastle you run from the project directory uses them, including an installed one: memcastle integration install codex there installs the assets prepared in the checkout (mise run integrations:build) and not the package's. Run it from another directory, or env -u MEMCASTLE_ASSETS_DIR memcastle ..., to use the installed assets. A flag still wins over the variable (--assets-dir), and your own mise.local.toml can override or drop either.

The suites that start a daemon or run the installer are not affected: mise run integrations:check and mise run web:check unset every MEMCASTLE_* variable before they start, and the Rust and bun harnesses build their own environment, so a test never depends on where it was run from.

By default the palace lives under ~/.local/share/memcastle/default and the daemon listens on 127.0.0.1 port 8420. Every path, environment variable, flag and the precedence between them is in Configuration. config::Config is the source of truth if that page ever drifts.

Testing

  • Unit tests live next to the code they test (domain::job's state machine, config's validation, store's migrations/persistence — the storage tests use SurrealDB's in-memory engine for speed, plus one test against a real SurrealKV directory; survival across a restart is proven at the process boundary in tests/persistence.rs, because SurrealKV's file lock is not released within one process).
  • Three suites. Core test binaries that build or run a WebAssembly component are named tests/wasm_*.rs, and that prefix is the whole definition of the WebAssembly suite: .config/nextest.toml selects it with binary(/^wasm_/), and mise run test:wasm and CI's wasm job pass the same prefix to Cargo as --test 'wasm_*', so that the other test binaries are not compiled for nothing. A new wasm_ file therefore joins the suite with no configuration. Most other tests are the basic suite, which is what mise run test:fast and the default and ci nextest profiles run; the wasm and ci-wasm profiles run Core's component tests. mise run test uses the same execution filter but may still compile excluded component test binaries; use test:fast to avoid linking them. Process-boundary durability, MCP and auth tests and the committed retrieval baseline form the Linux-only slow suite, selected with the slow-tests Cargo feature by mise run test:slow and CI's ci-slow profile. mise run check:all combines the template's check task with this project-specific slow gate. The basic suite runs in CI on Linux, macOS and Windows and needs no WebAssembly target. The WebAssembly suite runs in CI on Linux only, in its own wasm job with its own Codecov flag, and its sources job explicitly tests the directory reference source with mise run sources:test -- directory. That job runs conformance, plus any source-owned core-tests.rs cases not expressible as conformance.
  • Integration tests (tests/) run against a tempdir palace and an OS-assigned port. Those that start the daemon inside the test process are modules of one binary, tests/in_process/main.rs: each tests/ binary links the whole server, about 600 MB with debug info, so sixteen of them were most of what compiling the tests took, and one links it once. To add one, create tests/in_process/<name>.rs and list it in main.rs. nextest still runs every test in a process of its own, and a test is named in_process <module>::<test>, so mise run test -- -E 'binary(in_process) & test(auth::)' selects one file's tests. A test that needs a process boundary (a daemon that is killed, a CLI with no daemon) stays a binary of its own. Most start the daemon in-process (tests/common's TestDaemon); the ones that need a real process boundary (SurrealKV's file lock is not released within one process) spawn the memcastle binary instead:
  • tests/in_process/server.rs — health, status, graceful shutdown, retry (in-process).
  • tests/in_process/concurrency.rs — many simulated clients submitting jobs and reading status at once, proving the shared store stays consistent (in-process).
  • tests/in_process/memory_mode.rs, tests/in_process/mcp_memory_mode.rs — per-request and per-MCP-session memory modes (in-process).
  • tests/in_process/audit.rs, tests/in_process/repair.rs — the audit and repair job kinds end to end (in-process).
  • tests/persistence.rs — data and job state survive a daemon restart, including a SIGKILL mid-job and a pause or cancel requested just before it, memcastle daemon start and memcastle daemon restart --bind --port (subprocess).
  • tests/mcp_smoke.rs — the MCP surface end to end: a real MCP client over streamable HTTP against a memcastle serve subprocess, covering read and write tools, migrations before serving, a second session, and persistence across a restart with a reconnecting session (subprocess).
  • tests/in_process/auth.rs — optional bearer-token authentication over real HTTP: every route guarded, the open health probe, a configured secret, generate, rotate and revoke, and MCP with a token and with no credential-management tool (in-process).
  • tests/auth_lifecycle.rs — the authentication lifecycle through the real CLI: generate, restart, enable, a rotated and a revoked token, the daemon refusing to start with nothing to check against, and no token ever reaching the log, the database or any file (subprocess).
  • tests/credential_isolation.rs — OAuth credentials stay in one module that reaches no palace, no jobs and no WebAssembly runtime, that only app and the daemon's root name, and that never logs or reads a token outside where it is stored or sent to its provider.
  • tests/wasm_oauth.rs — a source that signs in with OAuth against a real daemon and the real CLI, with a fake provider: consent to the sign-in, source auth through the device flow, the token a run is given, renewal, a revoked credential, a miner that needs the sign-in, and no refresh token anywhere but the credentials file (needs the wasm32-wasip2 target).
  • tests/config_assets.rs — the assets override is honoured and checked, and a standalone binary starts with no assets (subprocess).
  • tests/dependencies.rs — the lockfile never pulls a second storage engine into the binary (ADR-001), nor a source-specific SDK or native database binding (ADR-026), and the WebAssembly runtime stays built without the features a source host never uses.
  • tests/config_bind.rs — the listener itself: bind address and port precedence, and real bind failures (subprocess).
  • tests/config_paths.rs — the XDG config, data and state locations, resolved through the real binary (subprocess).
  • tests/migrate.rs — memcastle migrate and its --check/--status modes (subprocess).
  • tests/cli.rs — the binary's argument parsing and its behaviour with no daemon reachable (subprocess), including colour, completions and the absence of a help subcommand.
  • tests/in_process/retrieval_eval/ — the retrieval evaluation harness (Retrieval evaluation): the bundled dataset against its committed baseline, no result outside its scope or time, and the metric, report and LongMemEval converter units (in-process). Its explicit runs (mise run eval*) are #[ignore]d.
  • tests/in_process/palace.rs — the /api/wings/... hierarchy routes: lifecycle, error contract and memory-mode gates (in-process).
  • tests/shutdown.rs — a stopping process lets the embedded datastore finish stopping before it exits (subprocess).
  • tests/in_process/db_endpoint.rs — the database admin endpoint: opt-in, loopback by default, refused origins, and sharing the daemon's data (in-process).
  • tests/in_process/miners.rs — persistent miner configuration end to end: several miners with their own options, creation and changes validated before anything is written, a change never broadening silently, hand edits noticed and an invalid one keeping the last good miners, a cursor surviving every change that keeps the source, a credential never shown, and REST, the CLI and MCP agreeing (in-process, with the CLI as a subprocess client).
  • tests/in_process/triggers.rs — source triggers end to end: disabled by default and nothing started by defining a trigger, a miner or a webhook; enabling refused with what is missing; poll, watch and webhook firing; a webhook's listener opening only while one is enabled and refusing every unauthentic delivery alike; repeated deliveries running once; a burst of firings and deliveries becoming one waiting run; a restart keeping the user's choice and the remembered timetable; a vanished watch path reported and recovered; hand edits; and REST and the CLI agreeing (in-process, with the CLI as a subprocess client).
  • tests/in_process/cli_daemon.rs — CLI flags that change what the daemon is asked: --mode, and relative mine paths against an in-process daemon (subprocess client).
  • tests/in_process/integration_contract.rs — the daemon half of the integration contract: a real MCP session replays the language-neutral fixtures in tests/fixtures/integration/ (modes, checkpoint payloads, failure classes), and fails when the contract page, the capability manifest and the test names disagree (in-process).
  • src/project.rs — the project file and the MEMCASTLE_WING/MEMCASTLE_ROOM overrides, replaying the cases in tests/fixtures/project-config/ (unit tests): discovery, nesting, the git and home boundaries, the environment, and what is refused. The integrations replay the same cases in TypeScript, see Project configuration.
  • The external Pi and OpenCode plugins run their own agent-client suites against a real daemon in their provider repositories, replaying the integration contract's language-neutral fixtures.
  • tests/integration_cli.rs — memcastle integration list|install|update|remove through the real binary, with fake fake pi and opencode programs and a throwaway home: the lifecycle, idempotence, compatibility refusals, preservation of the user's other plugins and settings, and resolution from --assets-dir, the environment and the binary's own prefix. The installer's unit tests are in src/integration/ (manifest, discovery, adapters, install, update, remove, rollback).
  • tests/integration_bundle.rs — checks the bundles built by integrations:check with packaging/integrations/build.sh: the package tree (manifests accepted by this MemCastle, no sources or node_modules, the skills) and the same lifecycle from the packaged tree and from the checkout. It is #[ignore]d in the basic suite so that mise run integrations:check can build the checkout assets without a second suite racing it.
  • tests/in_process/events.rs — GET /api/events against a real daemon: a note, a wing and a job's progress arrive as identifiers and never as content, a disabled session is refused and a read_only one served, the token is required, and an open stream does not keep the daemon from shutting down.
  • tests/in_process/web.rs — the dashboard's routes against a real daemon: opt-in, the static files and their headers, the refusal of any path that leaves web/dist, the page for a build that is missing, the public shell with authentication on, /api/config, and the jobs listing's kind and limit (in-process). src/assets unit-tests that a packaged layout and a worktree resolve web/dist/index.html through the same lookup.
  • tests/web_bundle.rs — builds the dashboard with packaging/web/build.sh, checks the package tree (no sources, maps or node_modules) and serves it from an installed prefix and from the checkout. It needs bun and node, so it is #[ignore]d in the basic suite and mise run web:check runs it.
  • web/test/ — the dashboard's client, login, route guard, event-stream parser and live loader (vitest, no daemon), and the same client, login and event stream against a real memcastle serve (web/test/daemon, bun), see Web UI.
  • tests/in_process/native_conformance.rs — conformance for Core's built-in directory adapter in the basic suite. WebAssembly provider conformance runs in each source's own CI leg, including sources/directory/.
  • The Pi history source and lifecycle extension have their own tests and fixtures in memcastle-pi, run against a released or locally built Core binary.
  • The OpenCode history source's fixtures, conformance and installation/mining smoke checks live in memcastle-opencode.
  • The GitHub repository source's fixtures, conformance and installation/mining smoke checks live in memcastle-github.
  • The Codex and ChatGPT modules' fixtures, conformance and independent plugin-install/adoption smokes live in memcastle-openai.
  • tests/wasm_registry.rs — registries against a real daemon (a directory registry written with the same calls source index makes): search, install by name with consent, an archive that is not the one published, a package that is not the one the index says, version choice, the trust policy, update with and without new permissions, a bundled source that a registry never installs, updates or replaces, a source registered by GitHub repository (against a stand-in for the API, with a digest that does not match and a repository that cannot be read), an unreadable registry, and the CLI publishing, signing, searching, installing and updating (in-process and subprocess, WebAssembly suite, using the synthetic Core mock source).
  • tests/wasm_bundle.rs — packaging/sources/build.sh removes stale bundled components and archives from the now-empty Core source package tree (WebAssembly suite, Unix only).
  • tests/wasm_runtime.rs, tests/wasm_plugin.rs — installable mock sources against a real daemon: consent, independent module lifecycles, mining through the shared pipeline, retained data, and refusing a component altered on disk (in-process, WebAssembly suite).
  • tests/wasm_projects.rs — memcastle source init, build, test and package for real, the sandbox (a source cannot read outside its grant, see the environment, outrun its time limit or exceed its memory, or run an unlisted program) and installing through the CLI. It builds components with Cargo for wasm32-wasip2 (subprocess, WebAssembly suite); see Building components in tests.
  • tests/in_process/skills.rs — the shared agent skills: every skill is discoverable, and every tool, CLI command and REST route it names exists in this release (in-process daemon, plus the real binary's help).

Run a subset with nextest's filter syntax, e.g.:

mise run test -- --filter-expr 'test(job)'

A filter on mise run test still excludes the wasm_ binaries; use mise run test:wasm -- <filter> for those. mise run cover runs the basic suite with coverage. The web TypeScript suite reports coverage too: with COVERAGE=1, mise run web:check runs its test:cover script, which writes lcov under web/coverage/; CI uploads it to Codecov under the web flag. Standalone agent plugins run their own client suites in their provider repositories. .github/codecov.yml also groups every report into components (core, surfaces, app, memory, sources, web, integrations) by path, so the Codecov page shows each area whatever flag covered it. Each component reflects the merged execution coverage from every reporting suite. Its required status permits the same 0.5% fluctuation as the project status, so platform and timing variation does not fail a build.

Core and provider test boundaries

Generic Core WebAssembly lifecycle and contract tests use tests/fixtures/mock-source/, a synthetic WIT component. Its two fixed candidates and locator-derived text exercise installation, consent, cursors, mining, registry updates and independent plugin modules without testing any provider's discovery rules or calling an external service. The mock is a test fixture, not a releasable source: it lives outside sources/ and is built only by the generic tests. tests/source_isolation.rs guards against reintroducing a concrete provider fixture into those tests.

Each real WebAssembly source's own mise run sources:test -- <name> CI leg runs its conformance fixtures and source-owned behavior tests; sources/directory/ retains the directory conformance cases. Core's built-in directory adapter has its separate native conformance test. Scaffolding and sandbox tests (tests/wasm_projects.rs), OAuth tests (tests/wasm_oauth.rs) and the release bundle test use purpose-built components because their behavior is what those tests verify.

Where the CI time goes

Measured on GitHub's runners after the suites were split, so that a change aimed at speed has a baseline to beat. Compiling is the test binaries built instrumented for coverage; running is nextest's own summary line. The figures are from runs that restored their caches; the first run of a new job, or one whose cache was evicted, compiles wasmtime and Cranelift from scratch and takes several minutes longer.

Job Compiling Running Whole job
Tests, Linux (basic, 1119 tests) 46 s 224 s 5.4 min
Tests, macOS (basic) 25 s 67 s 2.5 min
Tests, Windows (basic) 75 s 215 s 7.3 min
WebAssembly tests, Linux (38 tests) 10 s with warm caches, 2.8 min cold 38 s 1.5 to 4.3 min
Source, one leg per sources/<name>/ 10 s seconds about 1 min

The WebAssembly tests run in under a minute in CI, and their job finishes well before the basic Linux one. Both suites build only the test binaries they run: the wasm job passes --test 'wasm_*' to Cargo, and the daemon tests that start a server in the test process are one binary, tests/in_process, rather than sixteen. The time left in the basic suite is its roughly 1100 tests, not compiling or WebAssembly.

Building components in tests

Under nextest every test is its own process, so a component built by one test is invisible to the next, and the cost is how many times Cargo has to compile what. The helpers in tests/common/wasm.rs keep that to one small crate per build:

  • Every wasm_* test builds into one shared debug target directory, so wit-bindgen and serde_json compile once.
  • Every scaffolded project is seeded with the mock source's Cargo.lock. A scaffold ships none, and without it each build updated the crates.io index and re-resolved 35 packages while holding Cargo's package-cache lock, which every other build then waited for.
  • The mock source is built in place, in debug, into that shared directory, so generic tests never need a provider build or wait for the release build with LTO that source-owned CI legs make.
  • A test that drives memcastle source init must call share_target_of afterwards. Without it the project builds in release into a target/ of its own and recompiles every dependency, which used to be the two slowest tests in the suite.

Locally this took the WebAssembly suite from about 225 s to 95 s on a cold target directory and from about 92 s to 38 s on a warm one; in CI, on a warm cache, the tests went from 49 s to 31 s and the slowest from 25 s to 14 s. What is left of the job is compiling the instrumented test binaries.

The architecture guard

The non-negotiable invariant in AGENTS.md — "the CLI has no business logic MCP/HTTP can't reuse" — is enforced by the architecture prek hook's ast-grep store-isolation rules. They inspect Rust paths in src/main.rs, src/cli.rs, src/client/, src/mcp/ and src/api/ for direct store or jobs use (including multiline grouped use crate::{store::..} imports). The one allowed exception is main.rs's SurrealStore import, which memcastle migrate needs. A diagnostic-code string such as memcastle::jobs::not_found still trips the rule, but a comment does not: tests should read a code from the error (Error::body().code) rather than spell it out in these directories. If you find yourself wanting to import store from one of those, the fix is almost always to add a method to app::AppServices instead, so the same capability becomes available to every interface at once.

The single-writer rule enforces the companion invariant, one daemon and one writer per embedded palace: SurrealStore::connect may only be called from src/server/, src/store/ and src/main.rs (for migrate).

The integrations-http-only-* and shared clients-http-only-* rules keep an integration to MCP and HTTP: they fail on surrealdb, surrealkv, SurrealStore, a store or jobs path, or /api/db in supported source files under integrations/ (Markdown and node_modules are skipped), see ADR-019. Comments and unsupported file types do not trip these rules.

The authentication invariant has no hook, because a route or an MCP tool is not something a grep can recognise. It is enforced by tests instead: tests/in_process/auth.rs walks a list of the REST routes (and a route that does not exist) without a token, and fails if an MCP tool's name mentions credentials.

The database admin endpoint (ADR-015) is guarded the same way: a daemon that was only started has no second listener, a non-loopback bind needs the opt-in and authentication, a page from another origin is refused, and no MCP tool mentions the database. Those are tests/in_process/db_endpoint.rs, the /api/db entries in tests/in_process/auth.rs and a unit test that serve has no flag that starts it. surrealdb-core and surrealdb-rpc are pinned to the exact surrealdb version: they are SurrealDB internal API, so bump all three together.

Invariant 9 (source-specific code stays out of the pipeline) and invariant 10 (a source package has no ambient authority) have no hook, because what they forbid is not something a grep over the whole tree can recognise. tests/source_isolation.rs reads the source text instead: the pipeline, the chunker and the adapter contract name no adapter, no file access and no WebAssembly runtime; adapters, the WebAssembly host and src/source/ never reach the store or the jobs (at any depth); only src/mining/wasm/ names the engine; and the host calls nothing that hands a guest the daemon's environment, standard streams, arguments or a writable directory. Building the reference WebAssembly sources needs the wasm32-wasip2 target (rustup target add wasm32-wasip2, which rust-toolchain.toml requests); mise run sources:check builds and tests every source under sources/. tests/source_isolation.rs builds nothing and runs in the basic suite on every OS.

Invariant 6 and 10's rule that MCP never touches credentials covers OAuth sign-ins too (ADR-039): tests/in_process/auth.rs guards both /api/source-packages/{name}/auth routes and fails on any MCP tool that mentions auth, credential, oauth or sign, and tests/credential_isolation.rs reads the source text so that the credential module reaches neither the palace nor the WebAssembly runtime, and nothing outside app and the daemon's root can name it.

Invariant 12 (changing a miner is administrative, and the daemon is the only writer of the [[miners]] section) has no hook either. tests/in_process/auth.rs guards every /api/miners route and fails when any MCP tool other than memcastle_miner_list and memcastle_miner_get mentions miners; tests/in_process/miners.rs holds the rest, and config::miners_file's unit tests hold that an edit keeps the file's comments and other tables (ADR-037).

Invariant 13 (triggers are opt-in, administrative, and only ask for a run) has no hook either. tests/trigger_isolation.rs reads the source text so that src/trigger reaches neither the palace, the jobs nor any source, only app and the daemon's root can name it, MCP calls nothing that defines, enables or fires a trigger, the main router serves no /hooks route, the webhook listener never logs or passes on a body or a secret, and a trigger queues work only through the request miner run makes. tests/in_process/auth.rs guards every /api/triggers route and fails when any MCP tool other than memcastle_trigger_list and memcastle_trigger_get mentions triggers (ADR-043).

The integration installer is local tooling like the source tooling, and is held to the same rule in the same way. tests/integration_isolation.rs fails when src/integration/ mentions the store, the jobs, the daemon client, the network or an agent's settings file or a credential, and when anything under src/mcp, src/api, src/app or src/server (or any module but the binary) calls it. tests/integration_cli.rs exercises the installer with synthetic integrations; tests/integration_bundle.rs checks the Core-shipped package tree on Linux.

The web UI is held to HTTP as an integration is: the web-http-only-* and shared clients-http-only-* rules fail on surrealdb, surrealkv, SurrealStore, a store or jobs path, or /api/db in supported source files under web/ (Markdown, node_modules, dist and bun.lock are skipped), see ADR-035. That the dashboard's static files are the only thing besides the liveness probe that needs no token is held by tests/in_process/auth.rs and tests/in_process/web.rs, which walk the routes with the dashboard off and on.

Two more rules guard the remaining invariants. job-status-only-via-apply fails on any .status = assignment outside src/domain/job.rs, so a job's status only ever changes through Job::apply. no-hand-rolled-ddl fails on schema DDL in Rust code, because the schema is SurrealKit's (database/schema/*.surql).

The pinned ast-grep rules live under ast-grep/rules/, with discovery and Vue-as-HTML parsing in sgconfig.yml. The architecture hook scans the whole checkout even if only one relevant file was staged; architecture-rule-tests runs the native YAML rule fixtures, and mise run guards runs both prek hooks. To inspect diagnostics directly, run ast-grep scan --report-style short; to iterate on a rule, run ast-grep test --skip-snapshot-tests. Vue <script> blocks use HTML's built-in JavaScript injection, including imports from lang="ts" blocks; HTML rules check attributes and interpolations separately. The rules target Rust, TypeScript, TSX, JavaScript, Vue, HTML and JSON as specified by each rule's file globs; other file types are outside their scope. The web guard skips Markdown, node_modules, dist and bun.lock, and the integration guard skips Markdown and node_modules. These syntax rules cannot resolve renamed imports, computed strings or runtime behavior.

Documentation

The documentation site is built by Zensical from docs/, configured in zensical.toml. mise run docs serves it with live reload, and mise run docs:build (part of mise run ci) builds it. The documentation workflow builds pull requests without deployment and publishes merged main builds into the root of memcastle/memcastle.github.io's gh-pages branch, where Pages serves https://memcastle.github.io/. After the first publish creates the branch (if the repository is empty), configure the destination repository's Pages source as Deploy from a branch (gh-pages, /). The source repository's github-pages environment needs APP_CLIENT_ID (variable) and APP_PRIVATE_KEY (secret) for a GitHub App installed on memcastle/memcastle.github.io with Contents: write; its token is scoped to that repository. The first publish creates the branch when the destination is empty, and subsequent identical builds make no commit. Documentation is part of a change, not a follow-up: a pull request that changes a flag, a setting, a tool or a user-visible behaviour updates the page that describes it, in the same commit series.

  • Where things go. Task-oriented pages (installation, quickstart, guides) and references (configuration, CLI, MCP and REST) are for users; architecture.md, this page and adr/ are for contributors. Say a thing once and link to it from elsewhere instead of repeating it.
  • New pages must be added to the nav in zensical.toml, or they are built but not reachable.
  • Describe what exists. Document the implementation as it is, and put anything not built yet under a "not implemented" or "non-goals" heading rather than describing it as available. Run the command and paste real output when a page shows some.
  • Diagrams use Mermaid in fenced mermaid blocks, next to the text they explain. GitHub and the site both render them; stick to flowcharts, sequence, state, class and entity-relationship diagrams, and keep them small enough to read on a phone.
  • Prose style. One sentence per line, wrapped at a clause boundary if a sentence would pass 120 columns (mise run lint:md enforces the width on AGENTS.md, CONTRIBUTING.md, README.md, docs/ and skills/*/SKILL.md). mise run spell runs typos; a legitimate new term goes into typos.toml's extend-words.

This repository is generated from a template

See CONTRIBUTING.md for the git tpl workflow (mise run tpl:diff, mise run tpl:update), and AGENTS.md for where project-specific content goes so template updates keep merging cleanly.