Skip to content
Merged
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
22 changes: 22 additions & 0 deletions .agents/skills/add-gtest/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
name: add-gtest
description: Add and register a gtest file for a join class. Use when creating tests for new or untested code.
---

# Add a gtest

Input: module and class.

1. Read the header and a neighbouring test (e.g. `core/tests/tcp_acceptor_test.cpp`) for the exact layout and license header.
2. List the class's public methods; create `<module>/tests/<class_snake>_test.cpp` with one `TEST (<Class>, <method>)` each (plus `create` (`construct` if the class has a `create` method)/`move`/`copy`), mimicking the neighbouring file for fixture naming and the `@brief Test <method> method.` form, each covering success, errors, boundaries and overloads. Concurrent code gets a multi-threaded section for TSan.
3. Register it in `<module>/tests/CMakeLists.txt` next to its neighbours:

```cmake
add_executable(<name>.gtest <name>_test.cpp)
target_link_libraries(<name>.gtest ${JOIN_<MODULE>} GTest::gtest_main)
add_test(NAME <name>.gtest COMMAND <name>.gtest)
install(TARGETS <name>.gtest RUNTIME DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME}/test)
```
4. Build and run it in dev-loop mode (`gcc-debug`, that target only) with skills `compile` and `test`.

If a test reveals a bug, report the failing assertion and propose a diff first; do not change the code under test unprompted.
46 changes: 46 additions & 0 deletions .agents/skills/benchmark/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
name: benchmark
description: Check join performance versus main with json-benchmark, pack-benchmark and queue-benchmark. Use at closing when benchmarked code changed.
---

# Benchmark a change

Required at closing when the change touches benchmarked code: `data/` parsers and writers (`json-benchmark` for JSON, `pack-benchmark` for MessagePack `pack.hpp`, both for shared code such as `value*`, `sax*`), `core/` queues and memory backends (`queue.hpp`, `memory.hpp`: `queue-benchmark`). Bump/slab allocators have no benchmark: report **not verified** or measure ad hoc. Done means no degradation versus `main` beyond the run-to-run spread. If no suitable machine is available, report the item as **not verified** (indicative WSL2 numbers may be given but do not satisfy it) and ask the user how to proceed.

## 1. Ask first

Before running anything, ask the user: **is a real machine available, ideally both aarch64 and x86_64?** Memory ordering and SIMD behave differently on each, and numbers from a hypervisor are not trustworthy: `queue-benchmark` pins threads (`pthread_setaffinity_np`), which cannot control physical core placement under WSL2, and the `run_bench.sh` of `json-benchmark` and `pack-benchmark` refuse to start unless the governor is `performance` and boost is off, then wait for the CPU to settle, `ALLOW_NOISY=1` overriding it. If only WSL2 is available, say that results are indicative only, relative, and not publishable. The repos are expected next to join (`../<repo>`); if one is missing, clone `https://github.com/joinframework/<repo>.git` there, never into the join tree.

## 1b. Put the machine in performance mode

Read the current state first: `cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor` and `cat /sys/devices/system/cpu/cpufreq/boost` (Intel with `intel_pstate` uses `/sys/devices/system/cpu/intel_pstate/no_turbo` instead; not verified here). Expected: governor `performance`, boost off (`0`; `no_turbo` = `1`). Note the original values.

```bash
echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
echo 0 | sudo tee /sys/devices/system/cpu/cpufreq/boost # AMD / acpi-cpufreq
echo 1 | sudo tee /sys/devices/system/cpu/intel_pstate/no_turbo # Intel intel_pstate
```

Run the governor command and the boost command for this CPU with `sudo` only when justified, and say why. If `sudo` cannot run here (password prompt, no rights), give them exactly and ask the user to run them with the `! <command>` prefix so they execute in this session. Do not use `ALLOW_NOISY=1` to skip the check unless the user agrees to noisy numbers. When the campaign is over, remind the user of the original values so they can restore them. If the machine has no cpufreq interface (WSL2, some VMs), say the mode cannot be set and the numbers are indicative only.

## 2. Compare, same machine, same session

The repos fetch join from `main` by default (`FetchContent`, `GIT_TAG main`). To measure the branch, point them at the local checkout (`FETCHCONTENT_SOURCE_DIR_JOIN`, honoured by all three):

```bash
cmake -S . -B <scratchpad>/<repo>-branch -DCMAKE_BUILD_TYPE=Release -DFETCHCONTENT_SOURCE_DIR_JOIN=<path-to-join>
```

Check each tree measures the intended join: `grep FETCHCONTENT_SOURCE_DIR_JOIN <build>/CMakeCache.txt` must show its path, otherwise the numbers are GitHub `main`. json-benchmark and pack-benchmark pin a dconv tag: if the change needs an unreleased dconv, also pass `-DFETCHCONTENT_SOURCE_DIR_DCONV=<path-to-dconv>` to both trees.

Baseline: the same with a join `origin/main` worktree (`git -C <path-to-join> fetch origin main && git -C <path-to-join> worktree add --detach <scratchpad>/base origin/main`) in `<scratchpad>/<repo>-main`. Build both, then run the **binaries of each tree directly**, baseline and branch back to back, several times, alternating, with results written to the scratchpad. Do not use `run_bench.sh` for the comparison: each rebuilds `build/` against GitHub `main` and regenerates the committed `plots/`. Leave no changes in the benchmark repos; remove the worktree at the end.

- **json-benchmark** (its `run_bench.sh`: conformance, then 3 repetitions, min reported, `taskset -c 2`, `GLIBC_TUNABLES=glibc.malloc.mmap_threshold=33554432:glibc.malloc.trim_threshold=4294967296`): `<build>/benchmarks/performance/bench_<parse|stringify|prettify|statistics>_join` with the same `taskset` and `GLIBC_TUNABLES`, keep the minimum; datasets `canada`, `citm_catalog`, `twitter`. Also run `<build>/benchmarks/conformance/conformance --output <scratchpad>/<main|branch>-conformance.json`: a conformance drop is a regression. `sonic-cpp` is built unconditionally with `-march=native`; building on aarch64 is not verified.
- **pack-benchmark** (same `run_bench.sh` protocol as json-benchmark): `<build>/benchmarks/performance/bench_<parse|serialize|statistics>_join`, same `taskset`, `GLIBC_TUNABLES` and conformance; datasets `canada`, `citm_catalog`, `twitter` (`.msgpack`).
- **queue-benchmark** (its `run_bench.sh`: 10 repetitions, means): e.g. `<build>/benchmarks/mpmc/bench_mpmc_throughput --benchmark_filter="Join.*4/4/" --benchmark_repetitions=10 --benchmark_display_aggregates_only=true --benchmark_out=<scratchpad>/<main|branch>-mpmc.json`. Throughput, bulk and latency for `Spsc`, `Mpsc`, `Mpmc`, `LocalMem` and `ShmMem`.

Only the join entries matter for a regression check; the other libraries are context.

## 3. Report

Give: machine (CPU model, architecture, governor/boost, kernel, native or WSL2), repetitions, and a before/after table for the affected benchmarks with the relative change. Call a change real only if it exceeds the run-to-run spread; otherwise say it is within noise. When a delta is close to the spread, rerun with 5 repetitions per side and decide with google benchmark's `compare.py` (`<build>/_deps/googlebenchmark-src/tools/compare.py benchmarks <main>.json <branch>.json`, needs `scipy`; merge each side's runs into one JSON first): with 3 per side its Mann-Whitney test cannot reach p < 0.05. A mechanism read from the diff (fewer atomics, false sharing removed) is a hypothesis until `perf stat -e cycles,instructions,cache-misses,branch-misses` shows it (needs `kernel.perf_event_paranoid` <= 1). Never claim a gain you did not measure, and say plainly when the machine made the numbers unreliable.
25 changes: 25 additions & 0 deletions .agents/skills/compile/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
name: compile
description: Configure and build join with the CMake presets, as CI does. Use to compile a change or fix build errors.
---

# Compile

Dev loop vs closing: see `AGENTS.md` "Definition of done". Dev loop: `gcc-debug` only, one target. Closing: gcc and clang, `debug` + `release` + `tsan` + `asan`; announce it, it is slow.

```bash
cmake --preset <preset>
cmake --build --preset <preset> -j [--target <name>.gtest]
```

Presets: `gcc-` / `clang-` + `debug`, `release`, `tsan`, `asan`; trees in `build/<compiler>/<kind>`. Fix warnings (`-Werror`), never silence them. `JOIN_ENABLE_IO_URING` differs per preset (see `CMakePresets.json`), so a path can be compiled by one compiler and not the other.

## What CI builds on every PR

1. `clang-format` at the version pinned in `build.yml` over the tree (gates everything; `exclude-regex: 'build'` also skips paths containing `build`, e.g. `value_builder*`, so format those locally). Check `clang-format --version`: if it differs, ask the user to install it; a format diff seen only in CI points to the version gap.
2. gcc and clang x amd64 and arm64, in `joinframework/join-ci`: `release` then `debug`; `tsan` and `asan` on amd64.
3. CodeQL on `gcc-debug`.

## Report

Presets built, with the first error or warning in full on failure. Missing dependencies: see the README, and ask the user to install them.
32 changes: 32 additions & 0 deletions .agents/skills/coverage/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
name: coverage
description: Check join coverage is not degraded versus main (gcc and clang). Use at closing.
---

# Coverage

Coverage must not drop on **gcc and clang**. Use the `debug` presets (only ones with coverage on) and CI's `gcovr` options. `gcc-debug` has io_uring and `clang-debug` does not: compare each compiler only with itself.

```bash
C=gcc # repeat with C=clang
GCOV=gcov; [ "$C" = clang ] && GCOV='llvm-cov gcov'
cmake --preset $C-debug && cmake --build --preset $C-debug
find build/$C/debug -name '*.gcda' -delete
ctest --preset $C-debug
gcovr --root . --gcov-executable "$GCOV" --exclude-pattern-prefix LCOV --exclude-branches-by-pattern '.*' \
--exclude '.*/tests/.*' --exclude '.*/samples/.*' --json-summary <scratchpad>/$C-${SIDE:-after}.json --print-summary build/$C/debug
```

Baseline: do the same on `origin/main` in a worktree (`git fetch origin main && git worktree add --detach <scratchpad>/base origin/main`; configure, build, ctest, gcovr, same preset) with `SIDE=before`, into `$C-before.json`. Remove the worktree (`git worktree remove --force <scratchpad>/base`) at the end, also when a step failed.

Compare total line coverage and, above all, every changed file. Any drop fails: add tests (or `LCOV_EXCL_*` for deliberately unreachable code, sparingly) and re-measure.

Needs `gcovr` and `llvm-cov` matching clang, and root for the privileged tests (skipped or failing otherwise, which hides their coverage). When a tool is missing or the change touches privileged code, save the steps above, after `set -e`, to `<scratchpad>/coverage.sh` and run it in CI's image (`docker pull joinframework/join-ci:latest` first if it lacks `gcovr`), on copies so the host `build/` is untouched:

```bash
rsync -a --exclude build ./ <scratchpad>/head/
docker run --rm --privileged --sysctl net.ipv6.conf.all.disable_ipv6=0 -v <scratchpad>:<scratchpad> -w <scratchpad>/head \
joinframework/join-ci:latest bash -c "bash <scratchpad>/coverage.sh; rc=\$?; chown -R $(id -u):$(id -g) <scratchpad>; exit \$rc"
```

Same for the baseline with `-e SIDE=before -w <scratchpad>/base`. `--privileged` is root-equivalent (skill `test`). If neither works, report coverage as **not verified**, never "not degraded".
22 changes: 22 additions & 0 deletions .agents/skills/create-pr/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
name: create-pr
description: Branch, commit, push and open a PR with the join conventions. Use when asked to commit, push or open a PR.
---

# Branch, commit, push

**Ask the user before each of: creating the branch (propose the name), committing (show files and message), pushing (branch and remote), opening the PR (title and base).** One approval covers one action. Never merge, force-push or push to `main`.

1. **Inspect**: `git status`, `git diff`, current branch. Stage nothing unrelated, nothing under `build/`.
2. **Validate**: the dev loop of `AGENTS.md` "Definition of done" passed; the closing checks run only when the user says the change is ready to merge. Say what was not verified when asking.
3. **Branch**: on `main`, `git switch -c <prefix><kebab-name>`. Stay on a suitable branch; never commit on `main`.
- `feature/`: additions (class, API, option, tool).
- `bugfix/`: real bug, root cause fixed, regression test added.
- `hotfix/`: small quick fix, minimal diff.

Ask once if bugfix vs hotfix is unclear.
4. **Version**: bump `VERSION` in every PR, unless it is already ahead of the latest tag at an equal or higher level: minor for a feature or an API change, patch otherwise (bugfix, hotfix, tools, CI, docs, agent config), major only on request. Minor or major also sets `PROJECT_NUMBER` = `<major>.<minor>` in `doxyfile`.
5. **Commit**: explicit paths (no `git add -A`), one commit. Message: one capitalized imperative line, about 50 chars, no period, no body, no PR number, no trailer: `Fix netlink dump race`, or `Msgpack binary value type` for a new feature.
6. **Push**: `git push -u origin <branch>`. Never rewrite pushed history; report a rejected push.
7. **PR**: title = commit line, no body. Ask first; with `gh`: `gh pr create --base main --title "<Title>" --body ""`. Otherwise give `https://github.com/joinframework/join/compare/main...<branch>?expand=1`.
8. **Report** branch, commit, presets run and the link; do not claim CI is green.
19 changes: 19 additions & 0 deletions .agents/skills/release/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
name: release
description: Tag and publish a join release from main. Use when asked to release or tag a version.
---

# Release

**Ask the user before creating the tag and again before pushing it.** Never move, delete or re-push an existing tag; never push anything else.

1. **Check**:
- On `main`, clean tree, up to date: `git fetch origin --tags && git status` and `git rev-parse HEAD origin/main` equal.
- `VERSION` was bumped by a merged PR (skill `create-pr`), is greater than the latest tag (`git tag -l --sort=-v:refname | head -1`) and `v$(cat VERSION)` does not exist locally or on `origin`.
- `PROJECT_NUMBER` in `doxyfile` = `<major>.<minor>` of `VERSION`.
- CI (`cd` workflow) is green on this commit: ask the user, or check with `gh run list --workflow cd.yml --commit $(git rev-parse HEAD)` if available.

If a check fails, stop and say why.
2. **Tag**: lightweight tag on `HEAD` (= `origin/main`, the squash-merged PR commit), like the existing ones: `git tag v$(cat VERSION)`.
3. **Push**: `git push origin v$(cat VERSION)`. `release.yml` checks the tag against `VERSION`, builds (scan, tsan, asan, pack) and publishes the GitHub release with the `.deb` packages and generated notes.
4. **Report** the tag, commit and `https://github.com/joinframework/join/actions/workflows/release.yml`; do not claim the release is published until the user confirms the workflow passed.
35 changes: 35 additions & 0 deletions .agents/skills/test/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
name: test
description: Run join tests (ctest or one gtest), including sanitizers and root-only tests. Use to validate a change or reproduce a failure.
---

# Test

Build first with skill `compile`. Dev loop vs closing: see `AGENTS.md` "Definition of done". At closing, the `coverage` skill follows, and `benchmark` when the change touches benchmarked code.

```bash
ctest --preset <preset> --output-on-failure [-R '^<name>\.gtest$']
build/<compiler>/<kind>/<module>/tests/<name>.gtest --gtest_filter='Suite.test' --gtest_repeat=50
```

`tsan` for races, atomics, reactor/proactor; `asan` for memory, UB, lifetimes. Repeat flaky or concurrent tests under `tsan` before calling them fixed. The `tsan`/`asan` test presets set `TSAN_OPTIONS`/`ASAN_OPTIONS`/`UBSAN_OPTIONS` (`CMakePresets.json`): export the same values when running a binary directly.

## What CI tests on every PR

gcc and clang x amd64 and arm64, in `joinframework/join-ci` (`--privileged`, IPv6): `release` then `debug` ctest; on amd64 `gcovr` coverage, then `tsan` and `asan`. Coverage is generated for gcc and clang, but only the gcc report is published (`deploy-coverage`, which can fail the run).

Privileged tests (`CAP_NET_RAW`, veth, netns) run for real in CI; without root older tests fail locally and newer ones skip (new privileged tests use a fixture that probes the privilege and calls `GTEST_SKIP`, e.g. `PingTest`): ask the user to run them as root, see `AGENTS.md`. A skip is not a pass. `JOIN_ENABLE_IO_URING` differs per preset, so a path can be exercised by one compiler and not the other.

## Run in the CI image (Docker)

On a machine with Docker, the privileged tests can run in the CI image `joinframework/join-ci:latest` (same flags as CI: `--privileged`, IPv6 enabled). `--privileged` is root-equivalent: run it yourself only when justified, otherwise give the command to the user with the `! ` prefix; either way, analyze the log. The repo is copied into the container so the host `build/` is untouched. Example (not verified; change the preset as needed):

```bash
docker run --rm --privileged --sysctl net.ipv6.conf.all.disable_ipv6=0 -v "$PWD":/src:ro joinframework/join-ci:latest bash -c 'mkdir /work && tar --exclude=./build -C /src -cf - . | tar -C /work -xf - && cd /work && git config --global --add safe.directory /work && cmake --preset gcc-debug && cmake --build --preset gcc-debug && ctest --preset gcc-debug --output-on-failure' 2>&1 | tee <scratchpad>/gcc-debug.log
```

Run the same steps for each preset required at closing. Without Docker, ask the user to run the root tests directly (see `AGENTS.md`).

## Report

Presets run and pass/fail counts. Report environmental failures or skips with the output; never claim success you did not observe.
29 changes: 29 additions & 0 deletions .claude/agents/explorer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
name: explorer
description: "Read-only: map where a new join feature fits before coding. Use before implementing a feature."
tools: Read, Grep, Glob, Bash
---

You explore **join** before a feature is developed. Strictly read-only: never edit, write, commit or push. Bash is for inspection only (`git log`, `git show`, `grep`, `ls`), never builds. Read `AGENTS.md` and the `.claude/rules/` that match the area first.

## Method

1. Restate the feature in one sentence and name the module it belongs to (`core`, `crypto`, `data`, `fabric`, `services`; dependencies only flow `services` -> `fabric` -> `crypto` -> `core` and `services` -> `data` -> `core`).
2. Find the **closest existing analogues** (e.g. `Dhcp` for a new fabric protocol, `BasicBumpAllocator` for an allocator) and read them: public API shape, policy parameters, ownership, error reporting, sync/async forms.
3. Locate everything the change touches: headers, sources, `CMakeLists.txt`, the matching `tests/` files, samples, README sections, and the callers.
4. Classify the code as **hot path or not** (`hot-path.md`): allocation, locking, atomics and cache-line layout, progress guarantees, what could be a policy or computed at build time.
5. Check history with `git log` / `git show` on the analogues for earlier decisions, and the constraints in `.claude/rules/` (e.g. `fabric-api.md`).
6. List what would have to be tested (one file per class, one test per public method) and what the closing verification will exercise (privileges needed, io_uring, sanitizers).

Stay narrow: read what answers the question, do not survey the whole tree. Ground every claim in a `path:line`; say "not verified" for anything you did not read.

## Output (keep it under about 60 lines)

- **Fit**: module, files to add or change, 1-2 sentences.
- **Analogues**: the classes to imitate, with `path:line` and what to copy.
- **Design options**: 2-3 at most, with a recommendation and why (performance, progress guarantee, policy vs runtime, API shape).
- **Impact**: callers, CMake, tests to create, README or docs, build options or dependencies.
- **Risks**: concurrency, lifetime, ABI/API breakage, privileged tests, platform limits.
- **Open questions** for the user, only those that change the design.

Do not propose a diff and do not start implementing; the caller decides and develops.
Loading
Loading