Add repository-local tutorial agent skills

This commit is contained in:
LinuxDev9002
2026-07-18 15:27:12 -07:00
parent c054586a39
commit b48a2381d7
11 changed files with 906 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
---
name: write-bpf-production-tutorial
description: Create or revise bilingual bpf-developer-tutorial lessons as small, production-realistic eBPF tools with host builds, benchmark-kernel KVM tests, reproducible evidence, and prose authored only by the exact Claude model claude-opus-4-6[1m]. Use when proposing, implementing, documenting, reviewing, or preparing a PR for a new eBPF tutorial, especially when deciding between numbered lessons, features, networking, security, or xpu/gpu placement.
---
# Write BPF Production Tutorials
Build a useful tool around a kernel capability. Do not publish a wrapper around an upstream selftest or a feature-only API tour.
## Enforce the author boundary
- Let the implementation agent design, code, test, and collect technical evidence.
- Require Claude Code with the exact model string `claude-opus-4-6[1m]` to author or revise `README.md` and `README.zh.md`.
- Invoke Claude only through `scripts/run-claude-writer.sh`. Do not expose a model override and do not configure a fallback model.
- Do not write tutorial prose with Codex or another model. Send factual corrections back through the same wrapper instead of rewriting paragraphs manually.
- Permit other models to review code or prose, but never to author replacement tutorial text.
- Preserve every wrapper trace and manifest under `~/.local/state/bpf-tutorial-writer/runs/`. Never delete, truncate, rewrite, or clean that directory.
## Establish the tutorial contract
Before coding, write one sentence naming:
1. the production failure or operational need;
2. the user-visible command and result;
3. the kernel capability that makes it possible;
4. the positive and negative paths that prove it works.
Reject proposals whose only result is “the program loaded,” “the helper returned success,” or “the upstream selftest passed.”
Read [references/tutorial-acceptance.md](references/tutorial-acceptance.md) before selecting scope or placement. Read [references/writing-guide.md](references/writing-guide.md) before invoking the writer.
## Implement and verify
1. Read the repository contract, nearby lesson code, Makefiles, documentation generators, and CI workflows.
2. Keep one production scenario per PR. Start from the remote default branch for every PR.
3. Follow existing libbpf and CO-RE patterns. Add a deterministic workload, observable result, cleanup, and a relevant failure path.
4. Build on the host without loading BPF there.
5. Use `$test-bpf-tutorial-kvm` for every load, attach, and runtime test. Record guest kernel provenance and real output.
6. Add a compile CI step. Add a runtime feature probe or an explicit skip when ordinary CI lacks the required kernel.
7. Run documentation generators and link/unit tests.
## Delegate the prose
Create a task file outside the lesson with:
- the exact README paths Claude may edit;
- the production scenario and intended audience;
- the final code paths and CLI;
- verified build and guest commands;
- real captured output with private paths and identities removed;
- minimum kernel, libbpf, config, privilege, and hardware requirements;
- primary upstream references;
- facts or claims Claude must not add.
Then run:
```bash
TUTORIAL_ROOT="$(git rev-parse --show-toplevel)"
"$TUTORIAL_ROOT/.agents/skills/write-bpf-production-tutorial/scripts/run-claude-writer.sh" \
--repo "$TUTORIAL_ROOT" \
--task /absolute/path/to/writer-task.md
```
The wrapper must finish with a verified exact-model trace. Inspect both READMEs for matching structure and technical meaning, then rerun build and documentation commands exactly as written.
## Prepare the PR
- Include only one lesson and the minimal generated index/compatibility changes.
- Explain the production problem before the kernel feature in the PR body.
- Report the host build, KVM positive test, negative/cleanup test, guest kernel provenance, documentation tests, and model-trace manifest.
- Follow the repository's review, Copilot, and monitored CI gates. Do not merge unless the user asks.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "BPF Production Tutorial Writer"
short_description: "Build tested eBPF lessons with Claude Opus prose"
default_prompt: "Use $write-bpf-production-tutorial to turn this production eBPF scenario into a tested bilingual tutorial."

View File

@@ -0,0 +1,38 @@
# Tutorial acceptance and placement
## Acceptance gate
A numbered lesson must ship a small tool that a developer could plausibly run while operating or debugging a real system. Require all of the following:
- A concrete actor and problem, such as an SRE diagnosing queueing, a security agent quarantining a live connection, or an accelerator operator finding leaked shared buffers.
- A user-space CLI with useful output, filters, and actionable errors.
- A deterministic workload or fixture that recreates the scenario.
- An assertion connecting the workload to BPF output or behavior.
- A relevant negative path, unsupported-feature probe, or policy miss.
- Automatic cleanup of links, qdiscs, cgroups, maps, sockets, and test processes.
- A documented minimum kernel and required config or hardware.
- Host compile coverage and guest runtime evidence.
An upstream selftest may guide correctness, but do not copy its test harness as the lesson's product story.
## Placement
- Use `src/<number>-<name>/` for an end-to-end tracing, networking, security, scheduling, or operations tool.
- Use `src/features/<name>/` only when the reusable kernel primitive itself is the teaching goal and no honest production tool can be kept small.
- Use `src/xpu/<name>/` when the workload is specifically about GPU, NPU, DMA-BUF, accelerator drivers, or accelerator profiling. Do not use xpu merely because the host has a GPU.
- Keep cross-cutting deployment mechanisms such as program signing under `src/features/` unless the lesson implements a complete operational policy loader.
## Scenario guidance
- `fsession`: build function-latency/SLO diagnosis, not a cookie unit test.
- BPF qdisc: build a two-class latency-sensitive packet scheduler with queue/drop counters, not a bare `Qdisc_ops` registration test.
- Socket destroy: build a live connection quarantine CLI with an allowlisted control case.
- File dynptr plus task work: build deferred executable/header inspection with a direct-read failure demonstration.
- DMA-BUF iterator: build an accelerator/shared-buffer inventory and leak check under `src/xpu/`; avoid claiming per-process ownership without fd correlation.
- XDP multi-buffer: build a fragmented/jumbo-packet L4 parser with a truncated-packet failure case.
- Signed programs: build a signed policy deployment workflow with an untrusted/tampered rejection path.
- BPF crypto: defer unless the example has a defensible operational use and clear key-lifecycle boundaries.
## PR boundary
Keep each scenario in its own branch and PR. Generate README/SUMMARY/compatibility changes on that branch only. Rebase the next branch on current upstream `main`, not on another unmerged tutorial branch.

View File

@@ -0,0 +1,40 @@
# BPF tutorial writing guide
Write for developers who want to reproduce a small, useful eBPF tool. Use oral, direct English and natural Chinese. Prefer short, connected sentences over academic prose or marketing claims.
## Required narrative
1. Open with a production incident or operational task the reader recognizes. Explain the consequence and the signal the tool will expose or control.
2. Show the command and verified result early. Do not begin with a generic definition of eBPF.
3. Explain the end-to-end flow from workload to hook, maps or kfuncs, user-space processing, and final output.
4. Present complete source when it stays readable. Otherwise use copyable 15–40 line excerpts and link to the complete lesson.
5. Explain only the kernel mechanisms needed to understand the tool. State minimum kernel/libbpf versions, configs, privileges, limitations, and cleanup.
6. Provide exact build and run commands. Use only output captured from a real run. Never invent benchmark numbers, events, PIDs, paths, or errors.
7. Include the positive path and one meaningful miss, failure, or unsupported path.
8. Close with what the operator can now learn or control, plus the repository and tutorial-site links.
## Style rules
- Use a specific title such as `# eBPF Tutorial by Example: Quarantine an Established TCP Connection`.
- Build a progressive explanation around one running example instead of listing features.
- Use active voice and define technical terms through the example.
- Avoid throat clearing, repeated summaries, vague claims, paper-like language, em dashes, and stacked one-line paragraphs.
- Do not describe a demo as production-ready. State scope and missing hardening plainly.
- Preserve exact code, command, version, and output facts supplied in the task file.
- Link kernel claims to primary sources such as kernel commits, documentation, or selftests.
## Bilingual rules
- Keep English and Chinese headings, examples, commands, limitations, and claims aligned.
- Translate naturally rather than sentence by sentence.
- Keep code and verified console output identical.
- Do not use Chinese em dashes (`——`) or spaced English em dashes (` — `).
## Required ending links
Include direct links to:
- <https://github.com/eunomia-bpf/bpf-developer-tutorial>
- <https://eunomia.dev/tutorials/>
Do not add frontmatter unless nearby lessons use it.

View File

@@ -0,0 +1,121 @@
#!/usr/bin/env bash
set -euo pipefail
readonly MODEL='claude-opus-4-6[1m]'
readonly SKILL_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
readonly VERIFY="$SKILL_DIR/scripts/verify_claude_trace.py"
readonly GUIDE="$SKILL_DIR/references/writing-guide.md"
usage() {
echo "usage: $0 --repo ABSOLUTE_REPO_PATH --task ABSOLUTE_TASK_FILE" >&2
}
repo=
task=
while (($#)); do
case "$1" in
--repo)
repo=${2:-}
shift 2
;;
--task)
task=${2:-}
shift 2
;;
--model|--fallback-model)
echo "model overrides and fallbacks are forbidden; required model: $MODEL" >&2
exit 2
;;
-h|--help)
usage
exit 0
;;
*)
usage
exit 2
;;
esac
done
[[ $repo = /* && -d $repo/.git ]] || { echo "--repo must be an absolute Git checkout" >&2; exit 2; }
[[ $task = /* && -f $task ]] || { echo "--task must be an absolute readable file" >&2; exit 2; }
command -v claude >/dev/null || { echo "Claude Code is required" >&2; exit 127; }
command -v python3 >/dev/null || { echo "python3 is required" >&2; exit 127; }
state_root=${XDG_STATE_HOME:-$HOME/.local/state}/bpf-tutorial-writer/runs
mkdir -p "$state_root"
chmod 700 "$state_root"
run_id="$(date -u +%Y%m%dT%H%M%SZ)-$$"
run_dir="$state_root/$run_id"
mkdir -m 700 "$run_dir"
trace="$run_dir/claude-trace.jsonl"
manifest="$run_dir/manifest.json"
head_before=$(git -C "$repo" rev-parse HEAD)
status_before=$(git -C "$repo" status --porcelain=v1)
task_sha=$(sha256sum "$task" | awk '{print $1}')
prompt=$(printf '%s\n\n%s\n\n%s\n' \
'You are the sole prose author for an eBPF production tutorial. Read the repository code and evidence named in the task. Edit only the README.md and README.zh.md paths explicitly allowed by the task. Do not edit code, build files, generated indexes, or tests. Do not invent output or claims. Finish by stating which README files you edited.' \
"$(<"$GUIDE")" \
"$(<"$task")")
set +e
(
cd "$repo"
claude -p \
--model "$MODEL" \
--effort high \
--permission-mode acceptEdits \
--allowedTools Read Edit Write \
--output-format stream-json \
--verbose \
"$prompt"
) >"$trace" 2>&1
claude_status=$?
set -e
trace_sha=$(sha256sum "$trace" | awk '{print $1}')
head_after=$(git -C "$repo" rev-parse HEAD)
status_after=$(git -C "$repo" status --porcelain=v1)
MODEL_VALUE="$MODEL" \
REPO_VALUE="$repo" \
TASK_VALUE="$task" \
TASK_SHA_VALUE="$task_sha" \
TRACE_VALUE="$trace" \
TRACE_SHA_VALUE="$trace_sha" \
CLAUDE_STATUS_VALUE="$claude_status" \
HEAD_BEFORE_VALUE="$head_before" \
HEAD_AFTER_VALUE="$head_after" \
STATUS_BEFORE_VALUE="$status_before" \
STATUS_AFTER_VALUE="$status_after" \
python3 - "$manifest" <<'PY'
import json
import os
import pathlib
path = pathlib.Path(__import__("sys").argv[1])
path.write_text(json.dumps({
"model": os.environ["MODEL_VALUE"],
"repo": os.environ["REPO_VALUE"],
"task": os.environ["TASK_VALUE"],
"task_sha256": os.environ["TASK_SHA_VALUE"],
"trace": os.environ["TRACE_VALUE"],
"trace_sha256": os.environ["TRACE_SHA_VALUE"],
"claude_exit_status": int(os.environ["CLAUDE_STATUS_VALUE"]),
"head_before": os.environ["HEAD_BEFORE_VALUE"],
"head_after": os.environ["HEAD_AFTER_VALUE"],
"status_before": os.environ["STATUS_BEFORE_VALUE"],
"status_after": os.environ["STATUS_AFTER_VALUE"],
}, indent=2, sort_keys=True) + "\n", encoding="utf-8")
PY
chmod 600 "$trace" "$manifest"
if ((claude_status != 0)); then
echo "Claude exited with status $claude_status; preserved trace: $trace" >&2
exit "$claude_status"
fi
python3 "$VERIFY" --trace "$trace" --model "$MODEL" --require-write
echo "manifest=$manifest"

View File

@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""Verify that a persisted Claude trace used the required authoring model."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any
def load_events(path: Path) -> list[dict[str, Any]]:
raw = path.read_text(encoding="utf-8")
try:
value = json.loads(raw)
except json.JSONDecodeError:
events = []
for number, line in enumerate(raw.splitlines(), 1):
if not line.strip():
continue
try:
value = json.loads(line)
except json.JSONDecodeError as error:
raise ValueError(f"invalid JSON on trace line {number}: {error}") from error
events.extend(value if isinstance(value, list) else [value])
return [event for event in events if isinstance(event, dict)]
values = value if isinstance(value, list) else [value]
return [event for event in values if isinstance(event, dict)]
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--trace", required=True, type=Path)
parser.add_argument("--model", required=True)
parser.add_argument("--require-write", action="store_true")
args = parser.parse_args()
events = load_events(args.trace)
init_models = {
event.get("model")
for event in events
if event.get("type") == "system" and event.get("subtype") == "init"
}
if init_models != {args.model}:
raise SystemExit(f"expected init model {args.model!r}, found {sorted(init_models)!r}")
base_model = args.model.split("[", 1)[0]
authored_text = 0
write_calls = 0
assistant_models = set()
for event in events:
if event.get("type") != "assistant":
continue
message = event.get("message") or {}
model = message.get("model")
content = message.get("content") or []
if any(item.get("type") == "text" and item.get("text") for item in content):
authored_text += 1
assistant_models.add(model)
write_calls += sum(
item.get("type") == "tool_use" and item.get("name") in {"Edit", "Write"}
for item in content
)
if not authored_text:
raise SystemExit("trace contains no authored assistant text")
if assistant_models != {base_model}:
raise SystemExit(
f"expected authored text from {base_model!r}, found {sorted(assistant_models)!r}"
)
if args.require_write and write_calls == 0:
raise SystemExit("trace contains no Claude Edit or Write call")
results = [event for event in events if event.get("type") == "result"]
if not results:
raise SystemExit("trace contains no terminal result")
result = results[-1]
if result.get("is_error"):
raise SystemExit(f"Claude run failed: {result.get('subtype', 'unknown error')}")
usage = result.get("modelUsage") or {}
exact_usage = usage.get(args.model) or {}
if exact_usage.get("outputTokens", 0) <= 0:
raise SystemExit(f"terminal usage does not prove output from {args.model}")
print(
json.dumps(
{
"model": args.model,
"authored_text_events": authored_text,
"write_calls": write_calls,
"trace": str(args.trace),
},
sort_keys=True,
)
)
return 0
if __name__ == "__main__":
raise SystemExit(main())