Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Tooling and Git Hooks

The repository uses mise.toml to install the project hook runner and hk.pkl to define the hook behavior. criv.toml also declares project-level tooling files, including Hawk’s hawk.toml, as source roots. The decision record is ADR-0013.

Run the initial setup with:

mise install

The mise postinstall hook runs hk install --mise, so Git hook execution goes through mise x and uses the pinned tool versions, including hk, Rust 1.97.1, and actionlint. GitHub Actions security analysis is pinned here too: zizmor checks workflow, composite action, and Dependabot definitions for risky CI/CD patterns. Hawk uses the same Rust toolchain to check whether public Rust APIs are required by the shipped criv binary. Release tooling is also pinned here: Cocogitto calculates the next SemVer version from conventional commits, and cargo-release updates Cargo workspace versions. hk 2 evaluates hk.pkl with its bundled Pkl evaluator, so no separate pkl CLI is needed.

The top-level steps block in hk.pkl runs in the check, fix, and pre-commit hooks. It holds the gatekeepers (merge-conflict markers, files over 500 KB outside brag-output/, and private keys), the formatters, and the workflow and shell linters. pre-commit adds gitleaks on the staged changes, and check adds gitleaks on the Git history. hk fix does not stage its changes; pass --stage to stage them.

Workflow YAML under .github/workflows/ is checked with actionlint in pre-commit and the full check hook. zizmor runs in offline mode in the same hooks so local validation does not require a GitHub token or network access. This follow-up hook decision is ADR-0018.

Prose lint

Vale checks the prose in docs/. .vale.ini applies the built-in Vale style, so no vale sync step is necessary. The pre-commit hook runs Vale on changed files under docs/. The hosted vale CI job runs vale-cli/vale-action on all of docs/. To run it locally:

mise exec -- vale docs

If Vale flags a correct project word, add it to .vale/styles/config/vocabularies/criv/accept.txt. Add the (?i) prefix to a common word. Do not add it to a proper noun or an acronym, so Vale enforces the case. ADR-0143 records this decision.

Local Rust cache

Mise 2026.8.16 or later routes this project’s cargo commands through Mr. Boxington mbx 1.9.0. Run these commands after pulling the configuration:

mise install
mise reshim
mise exec -- cargo test --locked --workspace

An active mise shell also wraps plain cargo commands. Git hooks use the same wrapper through hk install --mise. Do not run mbx setup --global for this project. The checked-in configuration supplies the wrapper.

mbx shares compiler results between local worktrees. Its default cache policy controls incremental compilation. Worktrunk still copies target/ with shared filesystem blocks, and its removal hook still deletes each worktree’s copy. The wrapper sets MBX_TARGET_VIEWS=0 to keep these independent directories. Do not migrate a criv worktree to an mbx-managed target while this copy policy is in use. Give simultaneous Cargo builds separate output directories, for example --target-dir target/clippy for Clippy beside workspace tests.

The wrapper bypasses mbx when CI or GITHUB_ACTIONS is nonempty. Hosted builds keep the compiler cache policy from ADR-0108. Hawk also sets MBX_DISABLE=1, clears RUSTC_WRAPPER, and keeps target/hawk for its build files.

Inspect the shared cache and preview its cleanup with:

mise exec -- mbx cache stats
mise exec -- mbx gc --dry-run

Deleting a worktree does not delete the shared compiler cache. mbx collects eligible cache entries under its own retention policy. Cache byte counts do not measure physical space saved by filesystem block sharing.

To bypass mbx for one command, including a local release or debugger build:

MBX_DISABLE=1 mise exec -- cargo build --release

To remove automatic wrapping, remove [wrappers.cargo] and [wrappers.cargo.env] and the [env] entry for MBX_DISABLE from mise.toml. Then run mise reshim. Existing target/ directories remain valid Cargo output directories. The mbx tool pin and shared cache can be removed separately.

ADR-0142 records this integration. Issue #196 holds the proposed performance comparison. A local trial on 8 September 2026 used three runs per case on macOS/APFS, Rust 1.97.1, and mbx 1.9.0, before integration with PR #197. The values below are medians. Estimated disk allocation fell from 2.47 GiB to 2.04 GiB, including the mbx cache. Empty-cache tests took 275 s with mbx versus 78 s with plain Cargo; simultaneous tests and Clippy took 238 s versus 46 s. Warm and small-edit test times showed no clear gain. Disk estimates used free-space changes after deleting isolated trial outputs; they do not predict long-term cache growth. Use MBX_DISABLE=1 when build speed is the priority for these workloads.

Running criv from a hook runner

criv does not install Git hooks and does not set core.hooksPath, per ADR-0054. Setting that config replaces the hook directory wholesale, which silently disables whichever runner the repository already uses. Wire criv into your own runner instead.

The commands are stable:

StageCommands
commitcriv watch --once, criv check --changed, criv enforce --stage commit
pushcriv enforce --stage push

watch --once refreshes .criv/state.json before the changed check. The changed check is a staged, read-only partial fast path; plain criv check remains the full manual and hosted authority per ADR-0067. Run commit commands in the listed order.

With hk, as this repository does in hk.pkl:

local criv_check_changed = new Step {
  glob = List("**/*.md", "criv.toml")
  check_first = true
  check = "criv check --changed"
  fix = "criv check --fix"
}

local criv_enforce_commit = new Step {
  check = "criv enforce --stage commit"
}

hooks {
  ["pre-commit"] {
    fix = true
    steps {
      ["criv-check"] = criv_check_changed
      ["criv-enforce"] = criv_enforce_commit
    }
  }
}

With lefthook, in lefthook.yml:

pre-commit:
  parallel: false
  commands:
    criv-watch:
      run: criv watch --once
    criv-check:
      run: criv check --changed
    criv-enforce:
      run: criv enforce --stage commit

pre-push:
  commands:
    criv-enforce:
      run: criv enforce --stage push

Keep parallel: false for the commit stage: check reads the state watch writes, so they must not overlap.

Manual task entry points are:

mise run commit-msg -- .git/COMMIT_EDITMSG
mise run pre-commit
mise run pre-push
mise run fix
mise run perf
mise run vscode-build
mise run vscode-package
mise run release-plan
mise run release-auto

The hosted release workflow calls release-auto after CI passes. The command uses Cocogitto to prepare and push the version commit. Hosted jobs build, package, verify, attest, and publish that exact commit. Normal local development needs only release-plan; release-auto remains the workflow helper and a diagnostic entry point. See releasing for the full sequence.

Pre-commit and pre-push are the automatic local validation boundary. Agents and contributors do not need to replay them with an aggregate command after each commit. The former local aggregate task has been removed.

Hosted CI invokes the complete hk check profile directly. Every step hk runs is defined in hk.pkl with its command inline, per ADR-0049. To inspect or debug one core step explicitly:

hk check --all --plan                        # list every step
hk check --all --step hawk

Pre-push runs Clippy, workspace tests, Hawk, and criv push enforcement. Hawk is kept in both pre-push and the hosted CI profile so unnecessary public Rust APIs fail before hosted CI during normal local development.

mise run perf generates isolated vaults from the checked-in barrs-small and criv-medium manifests, then measures an explicit release binary. Build that binary first and identify its profile at the command line:

cargo build --release
mise run perf -- --binary target/release/criv --profile release

The script records repeated samples for cold, warm, and changed watch --once, validation, CI enforcement, docs-only next-adr-id, orphan-docs, and nodes --kind doc queries, the source-backed nodes --kind code --without-docs query, and a no-op snapshot diff. Five samples are collected by default; use --samples to change the count or repeat --case to select cases:

mise run perf -- --binary target/release/criv --profile release --samples 9
mise run perf -- --binary target/release/criv --profile release --case check

Every recorded sample receives a fresh generated vault. Warm cases perform an untimed state-building run inside that vault before the recorded command, and the changed case mutates the manifest’s declared number of supported source files without changing their sizes. A unique result directory preserves the run identity, exact manifest copies, raw JSONL samples, stdout/stderr, and a JSON summary with min/median/max and median absolute deviation. Failed samples remain in the raw evidence and make the harness fail.

mise run perf-container runs one ignored smoke case through Testcontainers. It builds criv and the harness inside a digest-pinned Rust Linux image. Docker is only an optional execution environment: the same checked-in manifests and generator define the vaults, results have their own container machine identity, and this task is not part of hooks or hosted CI. A Docker-API-compatible runtime is required. Large-workload measurement remains deferred.

Obsidian and VS Code validation are hosted-CI suites rather than automatic hk steps. Contributors changing a companion should run its package scripts directly. Run npm ci from the repository root first. The Obsidian companion commands are:

npm audit --workspace criv-obsidian-plugin --audit-level=high
npm --prefix .obsidian/plugins/criv run format:check
npm --prefix .obsidian/plugins/criv run lint
npm --prefix .obsidian/plugins/criv test
npm --prefix .obsidian/plugins/criv run build:wasm
npm --prefix .obsidian/plugins/criv run build:plugin
npm --prefix .obsidian/plugins/criv run build

The combined build preserves the release and manual entry point by running the Wasm build followed by the plugin build. The Wasm command invokes wasm-pack through mise exec cargo:wasm-pack@0.15.0, so the repository’s pinned tools are used rather than whichever Rust or wasm-pack appears first in the shell. It writes criv_wasm.js, criv_wasm_bg.js, and criv_wasm_bg.wasm under .obsidian/plugins/criv/pkg/. The plugin bundle keeps that runtime import external, so a distributable plugin must carry the generated pkg/ directory beside main.js.

If Obsidian reports that the criv Wasm runtime is unavailable, run the combined build, confirm those three runtime files exist, and reload the plugin. A state schema or parse error is separate: regenerate .criv/state.json with criv watch --once instead of rebuilding Wasm.

The VS Code-compatible extension lives in extensions/vscode-criv. Its local tasks are exposed through mise and npm:

npm audit --workspace vscode-criv --audit-level=high
npm --prefix extensions/vscode-criv run build
npm --prefix extensions/vscode-criv run test
npm --prefix extensions/vscode-criv run test:integration
npm --prefix extensions/vscode-criv run lint
npm --prefix extensions/vscode-criv run format:check
npm --prefix extensions/vscode-criv run package

The integration command launches the VS Code extension test host and fails on JSON diagnostics for extension manifest and language configuration files. Its short temporary user-data and extension-installation paths avoid macOS IPC path limits. Hosted CI runs this command under Xvfb; it is not part of local hooks.

The extension uses a read-only LikeC4 preview as the default .c4 editor. It selects the named view owned by the opened file. There is no fallback view: a file that owns none states so and points at a file that declares a view. Navigating inside the diagram opens the file that owns the target view in the same editor group, so the tab tracks the diagram. Use Reopen Editor With → Text Editor to edit the DSL. The preview webview uses packaged extension resources and does not use a CDN or a global LikeC4 command. Run criv watch --once after architecture or extension source changes. This keeps architecture state current. The coding agent authors the repository Code model and views under docs/architecture/code/, so watch does not replace them.

npm --prefix extensions/vscode-criv run package and mise run vscode-package build a local vscode-criv.vsix without publishing. mise run vscode-install builds that VSIX and installs it into the local editor; reload the window afterwards. The prepublish hook builds the Node.js Wasm target into extensions/vscode-criv/pkg/; the VSIX includes criv_wasm.js and criv_wasm_bg.wasm from that directory. If the extension reports an unavailable Wasm runtime, rebuild build:wasm, recreate the VSIX, confirm those files are present in the package, and reload the editor window. Regenerate invalid state with criv watch --once; do not treat a state-validation error as a runtime packaging failure.

The optional viewer is local-only under ADR-0087. The project does not publish it to the VS Code Marketplace or Open VSX. Release archives put the one viewer package next to the criv executable. Install it with an explicit editor selection:

criv install-editor --editor code
criv install-editor --editor cursor

Use --dry-run to validate the bundled viewer and local editor command without changing editor state.

Use hk validate after editing hk.pkl. Prefer hk built-ins when one exists; the commit message hook uses Builtins.check_conventional_commit instead of a shell wrapper around hk util check-conventional-commit.

The hawk hk step runs Hawk with warnings denied against the shipped criv CLI. The criv-wasm crate is excluded because its exported functions are consumed by the separately built Wasm artifacts, outside the CLI binary’s Cargo graph. The pre-push and full check hooks run Hawk whenever Rust sources, Cargo metadata, or its configuration change. Hawk uses target/hawk for its instrumented Cargo artifacts so its compiler work stays isolated from the other parallel checks. This policy is captured in ADR-0043 and the local/hosted validation boundary is ADR-0061.