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,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())