Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .claude/hooks/content-lint.sh
Original file line number Diff line number Diff line change
@@ -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
31 changes: 31 additions & 0 deletions .claude/hooks/nextest-on-stop.sh
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions .claude/hooks/rustfmt-on-edit.sh
Original file line number Diff line number Diff line change
@@ -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
32 changes: 32 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -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
}
]
}
]
}
}
3 changes: 3 additions & 0 deletions .config/nextest.toml
Original file line number Diff line number Diff line change
@@ -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 }
9 changes: 7 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
83 changes: 83 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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: <tool> used for <what>` 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.
1 change: 1 addition & 0 deletions CLAUDE.md
4 changes: 4 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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 ];
Expand Down
Loading