From e473791d85ca4cec7b8c6e054a87d57a73c15af0 Mon Sep 17 00:00:00 2001 From: mfw78 Date: Wed, 29 Jul 2026 06:42:56 +0000 Subject: [PATCH 1/2] chore(dev): add Claude hooks, AGENTS.md, and dev-shell search tooling Add the shared Claude Code configuration this repo had no equivalent of, plus the two dev-shell tools the agent workflow depends on. .claude/settings.json wires three hooks: - rustfmt-on-edit.sh formats each edited .rs file with edition 2024, matching the workspace edition. - content-lint.sh blocks an edit that adds an em-dash to a .rs or .md file, which is the house rule the repo already follows by hand. - nextest-on-stop.sh runs cargo nextest for the crates with uncommitted .rs changes at the end of a turn. Each hook is a no-op when its tool is missing, so nothing changes outside the Nix dev shell. AGENTS.md is the agent contract, and CLAUDE.md is a symlink to it. It is written in ASD-STE100 Simplified Technical English with one sentence per line. The content is grounded in README.md, Cargo.toml, the justfile, and .github/workflows/ci.yml: no new roadmap or architecture claims. flake.nix gains ripgrep and ast-grep in the devShell. cargo-nextest was already there. Nothing is removed. .config/nextest.toml sets a slow-timeout so a hung test fails by name after five minutes instead of consuming the CI job timeout. The justfile and the CI workflow are unchanged: they already run cargo nextest run and a separate cargo test --doc. .gitignore previously ignored all of .claude/. It now tracks settings.json and hooks/ while keeping settings.local.json and worktrees/ out of the repository. AI Assistance: Claude Opus used for the hooks, AGENTS.md, and flake change --- .claude/hooks/content-lint.sh | 12 +++++ .claude/hooks/nextest-on-stop.sh | 31 +++++++++++++ .claude/hooks/rustfmt-on-edit.sh | 10 +++++ .claude/settings.json | 32 ++++++++++++++ .config/nextest.toml | 3 ++ .gitignore | 9 +++- AGENTS.md | 75 ++++++++++++++++++++++++++++++++ CLAUDE.md | 1 + flake.nix | 4 ++ 9 files changed, 175 insertions(+), 2 deletions(-) create mode 100755 .claude/hooks/content-lint.sh create mode 100755 .claude/hooks/nextest-on-stop.sh create mode 100755 .claude/hooks/rustfmt-on-edit.sh create mode 100644 .claude/settings.json create mode 100644 .config/nextest.toml create mode 100644 AGENTS.md create mode 120000 CLAUDE.md diff --git a/.claude/hooks/content-lint.sh b/.claude/hooks/content-lint.sh new file mode 100755 index 0000000..6062335 --- /dev/null +++ b/.claude/hooks/content-lint.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# PostToolUse(Write|Edit): flag edits that introduce banned tokens (house style). +# nexum-runtime bans em-dashes in source, rustdoc, and markdown (AGENTS.md). +set -u +f=$(jq -r '.tool_input.file_path // .tool_response.filePath // empty' 2>/dev/null) || exit 0 +case "$f" in *.rs|*.md) ;; *) exit 0 ;; esac +[ -f "$f" ] || exit 0 +if rg -qF $'\xe2\x80\x94' "$f"; then + printf '{"decision":"block","reason":%s}\n' \ + "$(jq -Rn --arg r "Em-dash found in $f. This repo bans em-dashes (AGENTS.md): use ASCII hyphens or split the sentence." '$r')" +fi +exit 0 diff --git a/.claude/hooks/nextest-on-stop.sh b/.claude/hooks/nextest-on-stop.sh new file mode 100755 index 0000000..33837e7 --- /dev/null +++ b/.claude/hooks/nextest-on-stop.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Stop hook: run `cargo nextest run` for the workspace crates that have +# uncommitted .rs changes. Non-blocking; reports a pass/fail summary. No-op when +# nothing relevant changed or cargo/nextest are unavailable. +set -u +root=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0 +cd "$root" || exit 0 +command -v cargo >/dev/null 2>&1 || exit 0 +cargo nextest --version >/dev/null 2>&1 || exit 0 + +changed=$(git status --porcelain=v1 2>/dev/null | awk '{print $NF}' | rg '\.rs$' || true) +[ -z "$changed" ] && exit 0 + +meta=$(cargo metadata --no-deps --format-version 1 2>/dev/null) || exit 0 +pkgs=$(printf '%s\n' "$changed" | while IFS= read -r f; do + [ -n "$f" ] || continue + printf '%s' "$meta" | jq -r --arg f "$root/$f" \ + '.packages[] | (.manifest_path | rtrimstr("Cargo.toml")) as $d | select($f | startswith($d)) | .name' +done | sort -u) +[ -z "$pkgs" ] && exit 0 + +args=(); while IFS= read -r p; do [ -n "$p" ] && args+=(-p "$p"); done <<< "$pkgs" +list=$(printf '%s' "$pkgs" | tr '\n' ' ') +if out=$(cargo nextest run --no-tests=warn "${args[@]}" 2>&1); then + printf '{"systemMessage":%s,"suppressOutput":true}\n' "$(jq -Rn --arg m "nextest OK for touched crates: $list" '$m')" +else + fails=$(printf '%s' "$out" | rg -N 'FAIL|error\[|test result: FAILED|panicked' | tail -15) + printf '{"systemMessage":%s}\n' "$(jq -Rn --arg m "nextest FAILED for touched crates ($list): +$fails" '$m')" +fi +exit 0 diff --git a/.claude/hooks/rustfmt-on-edit.sh b/.claude/hooks/rustfmt-on-edit.sh new file mode 100755 index 0000000..1592703 --- /dev/null +++ b/.claude/hooks/rustfmt-on-edit.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# PostToolUse(Write|Edit): format the edited Rust file with rustfmt (edition 2024). +# Fast per-file format; no-op for non-.rs paths or when rustfmt is unavailable. +set -u +f=$(jq -r '.tool_input.file_path // .tool_response.filePath // empty' 2>/dev/null) || exit 0 +case "$f" in *.rs) ;; *) exit 0 ;; esac +[ -f "$f" ] || exit 0 +command -v rustfmt >/dev/null 2>&1 || exit 0 +rustfmt --edition 2024 "$f" 2>/dev/null || true +exit 0 diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..c257406 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,32 @@ +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Write|Edit", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/rustfmt-on-edit.sh", + "timeout": 30 + }, + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/content-lint.sh", + "timeout": 15 + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/nextest-on-stop.sh", + "timeout": 600 + } + ] + } + ] + } +} diff --git a/.config/nextest.toml b/.config/nextest.toml new file mode 100644 index 0000000..18cb55d --- /dev/null +++ b/.config/nextest.toml @@ -0,0 +1,3 @@ +[profile.default] +# Fail a hung test by name after 5 minutes instead of letting it eat the CI timeout. +slow-timeout = { period = "60s", terminate-after = 5 } diff --git a/.gitignore b/.gitignore index 35532e0..d330db8 100644 --- a/.gitignore +++ b/.gitignore @@ -25,9 +25,14 @@ Thumbs.db # which is then caught by the rule above). !.env.example -# Agent skills / AI tooling — installed locally, never committed. +# Agent skills / AI tooling. Shared repo config is tracked; anything personal +# or per-session is not. .agents/ -.claude/ +.claude/* +!.claude/settings.json +!.claude/hooks/ +.claude/settings.local.json +.claude/worktrees/ skills-lock.json # Engine runtime state (default state_dir from engine.toml). diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..453efea --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,75 @@ +`CLAUDE.md` is a symlink to this file. + +## What nexum-runtime is + +nexum-runtime is a WASM Component Model host runtime for web3 modules, and it supervises guest components built against the `nexum:host` WIT world. +Each module gets a capability-gated view of the host: chain access over JSON-RPC, an allowlisted `wasi:http` outbound gate, a local key-value store, clocks, and structured logging. +The host enforces fuel, memory, and epoch limits per module. + +The runtime is generic and venue-agnostic: it ships no venue and no domain payload, and `crates/nexum-cli` composes the core lattice and nothing else. +A downstream layer adds its own capabilities through the extension seam in `crates/nexum-world`. +A composition root declares `[extensions.]` rows in an `extensions.toml`, and `synthesize` emits those rows after the core capability rows in the module's WIT world. + +This repository is the leaf of the Nullis runtime stack and carries no cross-repo dependencies. +The videre and shepherd repositories build on the SDK and the runtime published here. + +## Layout + +- `crates/nexum-runtime` - the engine host: wasmtime embedding, supervisor, capability providers, and metrics. +- `crates/nexum-cli` - the bare `nexum` engine binary, composed over `nexum-launch` with the `CoreRuntime` preset. +- `crates/nexum-launch` - the generic launcher: CLI parsing, config loading, tracing setup, and the run loop for a preset. +- `crates/nexum-sdk` - the guest-side SDK that modules build against, host-neutral and domain-free. +- `crates/nexum-sdk-test` - in-memory host mocks and assertion helpers for module unit tests. +- `crates/nexum-module-macros` - the `#[module]` proc-macro, reached through `nexum_sdk::module`. +- `crates/nexum-tasks` - task lifecycle and graceful shutdown, and the only crate that spawns raw `tokio` tasks. +- `crates/nexum-world` - per-module WIT world synthesis, the core capability table, and the extension registry. +- `modules/example` - the minimal reference module, with balance-tracker, http-probe, and price-alert under `modules/examples/`. +- `modules/fixtures/` - adversarial fixtures: clock-reader, flaky-bomb, fuel-bomb, memory-bomb, panic-bomb, and slow-host. +- `tools/load-gen` - the load generator for soak runs. +- `wit/nexum-host` - the `nexum:host` WIT package. + +## Build, test, lint + +The workspace uses Rust edition 2024. +The flake pins the toolchain to Rust 1.94.0, which matches the toolchain CI installs. +Run `nix develop` to enter the dev shell, or run `direnv allow` once. +The dev shell supplies the toolchain, the `wasm32-wasip2` target, `cargo-nextest`, `just`, `ripgrep`, and `ast-grep`. + +Use the justfile recipes: + +```sh +just build # the engine plus every guest wasm +just test # host engine unit tests through nextest +just fmt # cargo fmt --all +just lint # cargo clippy --workspace --all-targets --all-features -- -D warnings +just ci # the full CI series: fmt, clippy, doc, wasms, nextest, doctests +``` + +Run tests with `cargo nextest run`, not `cargo test`. +nextest does not run doctests, so run `cargo test --doc --workspace --all-features` as well. +Run `just fmt` and `just lint` before each commit, because CI fails on any rustfmt or clippy warning. + +The hooks in `.claude/hooks/` support this loop. +`rustfmt-on-edit.sh` formats each edited `.rs` file. +`nextest-on-stop.sh` runs nextest for the crates with uncommitted `.rs` changes at the end of a turn. +Each hook exits without work when `rustfmt`, `cargo`, or `cargo-nextest` is absent, so both stay silent outside the dev shell. + +## House rules + +Do not use em-dashes in source, rustdoc, markdown, commit messages, or PR and issue bodies. +Use an ASCII hyphen, use a colon, or write two sentences. +`.claude/hooks/content-lint.sh` blocks an edit that adds an em-dash to a `.rs` or `.md` file. + +Write each commit message as a Conventional Commit with an imperative subject. +Disclose AI assistance with an honest `AI Assistance: used for ` line in the commit message and in the PR body. +Never add a `Co-Authored-By: Claude Code` footer or a `Generated with Claude Code` line. + +Keep one logical line per paragraph in a PR or issue body. +GitHub renders a single newline in a comment as a line break, so a sentence-per-line body wraps too early. + +## Documentation + +Write all documentation in ASD-STE100 Simplified Technical English. +Use short sentences, the active voice, and one idea per sentence. +In a markdown file, put each sentence on its own line and do not wrap within a sentence. +GitHub reflows the file when it displays it, and the diff then shows one changed line per changed sentence. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/flake.nix b/flake.nix index a861afc..a4a5a6b 100644 --- a/flake.nix +++ b/flake.nix @@ -47,6 +47,10 @@ wasm-tools wabt just + # Search tooling the agent workflow leans on: ripgrep for text, + # ast-grep for syntax-aware Rust queries and rewrites. + ripgrep + ast-grep pkg-config openssl ] ++ lib.optionals stdenv.isLinux [ mold ]; From aca4e496ee9e8bdfed8941402d3dbba38fbbc95d Mon Sep 17 00:00:00 2001 From: mfw78 Date: Wed, 29 Jul 2026 06:59:32 +0000 Subject: [PATCH 2/2] fix(hooks): block only newly added em-dashes content-lint scanned the whole file, so any edit to a file that already carried an em-dash was blocked even when the edit added none. That made an existing README or comment effectively uneditable. Compare the count against the committed version of the same file and block only on a net increase. A file that already carries the token stays editable, and adding one is still blocked. AI Assistance: Claude Opus used for this fix. --- .claude/hooks/content-lint.sh | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/.claude/hooks/content-lint.sh b/.claude/hooks/content-lint.sh index 6062335..1c3405e 100755 --- a/.claude/hooks/content-lint.sh +++ b/.claude/hooks/content-lint.sh @@ -1,12 +1,28 @@ #!/usr/bin/env bash -# PostToolUse(Write|Edit): flag edits that introduce banned tokens (house style). -# nexum-runtime bans em-dashes in source, rustdoc, and markdown (AGENTS.md). +# PostToolUse(Write|Edit): block an edit that ADDS a banned token to a .rs or +# .md file. Counts are compared against the committed version, so a file that +# already carries a banned token stays editable and only a net increase blocks. set -u f=$(jq -r '.tool_input.file_path // .tool_response.filePath // empty' 2>/dev/null) || exit 0 case "$f" in *.rs|*.md) ;; *) exit 0 ;; esac [ -f "$f" ] || exit 0 -if rg -qF $'\xe2\x80\x94' "$f"; then - printf '{"decision":"block","reason":%s}\n' \ - "$(jq -Rn --arg r "Em-dash found in $f. This repo bans em-dashes (AGENTS.md): use ASCII hyphens or split the sentence." '$r')" -fi + +block() { printf '{"decision":"block","reason":%s}\n' "$(jq -Rn --arg r "$1" '$r')"; exit 0; } + +# $1 = rg flags, $2 = pattern, $3 = message +check() { + local now head root rel + now=$(rg -o $1 -- "$2" "$f" 2>/dev/null | wc -l | tr -d ' '); now=${now:-0} + [ "$now" -eq 0 ] && return 0 + head=0 + if root=$(git -C "$(dirname "$f")" rev-parse --show-toplevel 2>/dev/null); then + rel=${f#"$root"/} + head=$(git -C "$root" show "HEAD:$rel" 2>/dev/null | rg -o $1 -- "$2" 2>/dev/null | wc -l | tr -d ' ') + head=${head:-0} + fi + [ "$now" -gt "$head" ] && block "$3 File: $f (was $head, now $now)." + return 0 +} + +check -F $'\xe2\x80\x94' "This edit adds an em-dash. House style bans em-dashes: use an ASCII hyphen, a colon, or split the sentence." exit 0