diff --git a/.claude/hooks/content-lint.sh b/.claude/hooks/content-lint.sh new file mode 100755 index 00000000..1c3405ef --- /dev/null +++ b/.claude/hooks/content-lint.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# 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 + +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 diff --git a/.claude/hooks/nextest-on-stop.sh b/.claude/hooks/nextest-on-stop.sh new file mode 100755 index 00000000..33837e78 --- /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 00000000..15927037 --- /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 00000000..c2574063 --- /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 00000000..6c3dbf42 --- /dev/null +++ b/.config/nextest.toml @@ -0,0 +1,3 @@ +# Fail a hung test fast instead of blocking CI: warn at 60s, kill after 5 periods. +[profile.default] +slow-timeout = { period = "60s", terminate-after = 5 } diff --git a/.gitignore b/.gitignore index 35532e08..e34478db 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. The shared Claude Code config (settings.json +# plus the repo hooks) is tracked; personal and per-session state 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 00000000..2683071b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,83 @@ +# AGENTS.md + +`CLAUDE.md` is a symlink to this file. + +## What shepherd is + +shepherd is the CoW Protocol composition root over the nexum WASM Component Model runtime and the videre intent and venue layer. +nexum-runtime supplies the venue-agnostic host: the Component Model runtime, the module supervisor, and the capability tables. +videre-nexum-module supplies the intent and venue platform on top of it. +This repository adds the CoW-specific parts: the `shepherd` engine binary, the CoW venue adapter, the ComposableCoW keeper machinery, and the production keeper modules. +Every module is a wasm32-wasip2 component, and the host grants each one only the capabilities its manifest declares. + +## Layout + +- `crates/shepherd-engine` builds the `shepherd` binary: the nexum runtime and the videre host wired as the CoW composition root. +- `crates/cow-venue` holds the CoW venue slices, and it is orderbook-only. + The default `body` slice carries the venue-neutral order intent body types and their borsh codec; the `client`, `assembly`, and `adapter` features layer the typed client, the chain-edge order assembly, and the wasm32-wasip2 venue adapter component on top. +- `crates/composable-cow` holds the ComposableCoW keeper machinery: the conditional-order body, the structured poll `Verdict`, and the `run` composition over the venue client. +- `modules/twap-monitor` and `modules/ethflow-watcher` are the production keeper modules. +- `tools/orderbook-mock` is the orderbook REST mock for load tests, and `tools/baseline-latency` is the Python latency baseline tooling. +- `wit/shepherd-cow` is this repository's WIT package. + `wit/deps/` vendors the cross-repo WIT packages, and `wit/deps.toml` pins their sources. +- `extensions.toml` is the client-capability registry that the module world synthesis reads. + It names the WIT import each `[capabilities]` declaration becomes, so it must stay next to `Cargo.toml`. +- The `engine*.toml` files are engine-side runtime configs, one per scenario. + `engine.example.toml` is the annotated template, `engine.m2.toml` and `engine.m3.toml` drive the smoke runbooks, `engine.e2e.toml` and `engine.load.toml` drive the e2e and load scenarios, `engine.soak.toml` and `engine.soak.docker.toml` drive the soak run, and `engine.docker.toml` matches the layout the `Dockerfile` bakes. + A real `engine.toml` carries paid RPC keys and is git-ignored; the committed files are placeholder templates that read secrets from `${VAR_NAME}` environment tokens. + +## Dependency pins + +This repository was carved out of the runtime monorepo, so the siblings are now external dependencies. +Each crate manifest pins `nexum-*` to a git rev of nullislabs/nexum-runtime and `videre-*` to a git rev of nullislabs/videre-nexum-module. +`wit/deps.toml` pins the same two revs for the vendored WIT packages. +To move to a newer sibling rev, change every occurrence of the old rev together: the crate manifests, `wit/deps.toml`, and the vendored `wit/deps/` copies. +A partial bump splits the graph and breaks the WIT resolve. +`Cargo.toml` also patches `cowprotocol` to a git rev of nullislabs/cow-rs until an upstream release carries the hash-only constructor. + +## Build, test, lint + +The workspace is edition 2024 on a pinned Rust 1.94 toolchain. +The flake devshell, the CI setup action, and the `Dockerfile` all pin 1.94; bump them in lockstep. +Enter the devshell with `nix develop`, or let `direnv allow` do it. +Every external dependency is hoisted into the `[workspace.dependencies]` table, and the core crates inherit with `dep.workspace = true`. +Guest modules under `modules/` do not inherit that table: they declare their own external dependencies, because a real module author has no access to it. + +Use the justfile recipes: + +``` +just build # build-modules + build-cow-venue + build-engine +just test # cargo nextest run, then cargo test --doc +just fmt # cargo fmt --all +just lint # cargo clippy --workspace --all-targets --all-features -- -D warnings +just ci # the full CI series locally +``` + +Run tests with `cargo nextest run` and doctests with `cargo test --doc`, because nextest does not run doctests. +Run `just ci` before you push: it mirrors `.github/workflows/ci.yml` one-to-one. +`cargo fmt --all --check` and the clippy `-D warnings` gate are the pre-commit gate, and both are blocking CI jobs. +CI also runs `cargo doc --workspace --no-deps` under `-D warnings` and the blocking `scripts/check-cow-orderbook-only.sh` gate, which holds `crates/cow-venue` to orderbook-only. +`just docker-build` builds the image, and `.github/workflows/docker.yml` publishes it to ghcr.io. + +The `.claude/hooks/` scripts support this loop. +`rustfmt-on-edit.sh` formats each edited `.rs` file with rustfmt. +`nextest-on-stop.sh` runs nextest for the crates with uncommitted `.rs` changes at the end of a turn. +Each hook exits without an error when its tool is absent, so nothing runs outside the dev shell. + +## House rules + +Do not use em-dashes (U+2014) anywhere. +Use ASCII hyphens, a colon, or split the sentence. +`.claude/hooks/content-lint.sh` blocks an edit that adds one to a `.rs` or `.md` file. +Write commit messages as Conventional Commits with an imperative subject. +Disclose AI assistance with an honest `AI Assistance: used for ` line in the commit message and the PR body. +Never add the `Co-Authored-By: Claude Code` or `Generated with Claude Code` boilerplate. +In a PR or issue body, keep one logical line per paragraph. + +## Documentation + +Write all documentation in ASD-STE100 Simplified Technical English. +Use short sentences, the active voice, and one idea per sentence. +In markdown files, put each sentence on its own line and do not wrap within a sentence; GitHub reflows the file when it displays it. +This keeps a diff to one changed line per changed sentence. +In PR and issue bodies, keep one line per paragraph, because GitHub renders a single newline in a comment as a line break. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 00000000..47dc3e3d --- /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 a861afc7..e6ef053e 100644 --- a/flake.nix +++ b/flake.nix @@ -47,6 +47,10 @@ wasm-tools wabt just + # Search tooling the agent hooks and CI gate scripts use: + # ripgrep for plain-text search, ast-grep for syntax-aware queries. + ripgrep + ast-grep pkg-config openssl ] ++ lib.optionals stdenv.isLinux [ mold ];