From ddbbb669358fda6bec8681cf94daae4f33df2aaa Mon Sep 17 00:00:00 2001 From: mfw78 Date: Wed, 29 Jul 2026 06:42:50 +0000 Subject: [PATCH 1/2] chore(dev): add Claude Code hooks, AGENTS.md, and dev-shell search tooling Add the shared agent tooling this repo lacked after the carve. .claude/hooks/ carries three tested hooks: rustfmt-on-edit.sh formats each edited .rs file at edition 2024 (the workspace edition), content-lint.sh blocks an edit that adds an em-dash to a .rs or .md file, and nextest-on-stop.sh runs nextest for the crates with uncommitted .rs changes at the end of a turn. Each hook is a no-op when its tool is absent, so nothing runs outside the dev shell. .claude/settings.json wires them to the PostToolUse and Stop events. AGENTS.md states the repo contract: what shepherd is, the crate and config layout, the nexum-runtime and videre-nexum-module rev pins, the build and test commands, and the house rules. CLAUDE.md is a symlink to it. The file follows ASD-STE100 Simplified Technical English with one sentence per line. flake.nix gains ripgrep and ast-grep in the devShell; cargo-nextest was already there. .config/nextest.toml adds a 60s slow-timeout that terminates a hung test after 5 periods. The justfile and CI are unchanged: they already run cargo nextest run plus a separate cargo test --doc. .gitignore previously ignored all of .claude/. It now tracks settings.json and hooks/ and keeps settings.local.json and worktrees/ ignored. 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 | 83 ++++++++++++++++++++++++++++++++ CLAUDE.md | 1 + flake.nix | 4 ++ 9 files changed, 183 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 00000000..fcd9731a --- /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). +# shepherd 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 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 ]; From 3bf6e2ee1c89f664534a5d4205ecb142418bac4f Mon Sep 17 00:00:00 2001 From: mfw78 Date: Wed, 29 Jul 2026 06:59:37 +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 fcd9731a..1c3405ef 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). -# shepherd 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