feat(eval): add agent-eval harness and /audit + /publish Claude skills

Replaces the old interactive publish.js script with two Claude skills and
a full agent-evaluation harness:

- `.claude/skills/audit/` — `/audit` skill drives `scripts/agent-eval/audit.sh`
  to benchmark retrieval quality (with vs. without codegraph) on a chosen
  real-world repo from the new `corpus.json` (17 repos across 14 languages).
- `.claude/skills/publish/` — `/publish` skill orchestrates the full release
  workflow (preflight → changelog → confirmation gate → bump/build → npm
  publish → GitHub release), replacing `publish.js`.
- `scripts/agent-eval/` — headless (`run-agent.sh`, `run-all.sh`) and
  interactive tmux (`itrun.sh`) harnesses with stream-json parsers
  (`parse-run.mjs`, `parse-session.mjs`) that report tool calls, token
  usage, and a VERDICT line summarising codegraph_explore vs. Read/Grep counts.
- `run-interactive-test.md` — documents the two harnesses, idle-detection
  approach, and what "good" agent behavior looks like after explore-first
  guidance.
This commit is contained in:
Colby McHenry
2026-05-20 09:39:17 -05:00
parent 1cbca5a51e
commit 7fe64b32be
11 changed files with 818 additions and 65 deletions
+74
View File
@@ -0,0 +1,74 @@
---
name: audit
description: Benchmark CodeGraph retrieval quality on a real codebase by comparing agent behavior with vs without CodeGraph. Use when the user runs /audit or asks to test, benchmark, audit, or validate a codegraph version (the local dev build or a published npm version) against a language's repo.
---
# CodeGraph Quality Audit
Measures how much CodeGraph helps an agent versus plain grep/read, for a chosen
codegraph version on a chosen real-world repo. Drives the harness in
`scripts/agent-eval/`.
## Prerequisites
- `tmux` 3+, a logged-in `claude` CLI, `node`, `git` (macOS/Linux).
- Run from the codegraph repo root.
## Workflow
Copy this checklist:
```
- [ ] 1. Pick version (local or npm)
- [ ] 2. Pick language
- [ ] 3. Pick repo by size
- [ ] 4. Pick harness (headless / tmux / both)
- [ ] 5. Run audit.sh in the background
- [ ] 6. Report results
```
**Step 1 — version.** Ask with `AskUserQuestion`: which codegraph version to test.
Offer "Local dev build" and "Latest published"; the free-text "Other" lets the
user type a specific version (e.g. `0.7.10`). Map the answer to a VERSION token:
- "Local dev build" → `local`
- "Latest published" → `latest`
- a typed version → that string (e.g. `0.7.10`)
**Step 2 — language.** Read `.claude/skills/audit/corpus.json`. Ask with
`AskUserQuestion` which language to test, listing the languages that have entries.
**Step 3 — repo.** From the chosen language's entries, ask which repo. Label each
option with its size and file count, e.g. `excalidraw — Medium (~600 files)`.
Each entry carries the `repo` URL and a representative `question`.
**Step 4 — harness.** Ask with `AskUserQuestion` which harness to run, and map
the answer to a MODE token:
- "Headless" → `headless``claude -p` with stream-json: exact tokens/cost and a
clean tool sequence (2 runs, fast, no TTY).
- "Interactive (tmux)" → `tmux` — drives the real Claude TUI in tmux: faithful
Explore-subagent behavior, metrics from session logs (2 runs, slower).
- "Both" → `all` — headless + interactive (4 runs).
**Step 5 — run.** Launch in the background (sets the version, clones if missing,
wipes + re-indexes, runs the chosen arms — several minutes):
```bash
scripts/agent-eval/audit.sh <VERSION> <repo-name> <repo-url> "<question>" <MODE>
```
**Step 6 — report.** When the job finishes, read the log and report per arm:
- Headless (`parse-run.mjs`): total tool calls, file `Read`s, Grep/Bash,
codegraph-tool calls, duration, **total cost**.
- Interactive (`parse-session.mjs`): the `VERDICT: codegraph_explore used Nx |
Read N | Grep/Bash N` and `TOKENS:` lines.
Lead with cost + tool/Read counts — they are the reliable signals; raw token
in/out are confounded by subagent delegation and prompt caching. State whether
codegraph reduced effort and whether both arms reached a correct answer.
## Notes
- The index is rebuilt every run (`audit.sh` wipes `.codegraph`) — different
versions extract differently, so an index must be served by the same binary
that built it.
- `audit.sh` temporarily mutates the global `codegraph` install for the test,
then restores your dev link via `local-install.sh`.
- Corpus repos are cloned to `/tmp/codegraph-corpus` (reused if already present).
- Add or edit repos in `corpus.json` (fields: `name`, `repo`, `size`, `files`,
`question`).
+63
View File
@@ -0,0 +1,63 @@
{
"_comment": "Test corpus for /audit. Add entries freely. size: Small (<~150 files), Medium (~150-1500), Large (>~1500). 'question' is a representative architectural question that exercises cross-file understanding.",
"TypeScript": [
{ "name": "ky", "repo": "https://github.com/sindresorhus/ky", "size": "Small", "files": "~25", "question": "How does ky implement request retries and timeouts?" },
{ "name": "excalidraw", "repo": "https://github.com/excalidraw/excalidraw", "size": "Medium", "files": "~600", "question": "How does Excalidraw render and update canvas elements?" },
{ "name": "vscode", "repo": "https://github.com/microsoft/vscode", "size": "Large", "files": "~10000", "question": "How does the extension host communicate with the main process?" }
],
"JavaScript": [
{ "name": "express", "repo": "https://github.com/expressjs/express", "size": "Small", "files": "~50", "question": "How does Express route a request through its middleware stack?" }
],
"Go": [
{ "name": "cobra", "repo": "https://github.com/spf13/cobra", "size": "Small", "files": "~50", "question": "How does cobra parse commands and flags?" },
{ "name": "gin", "repo": "https://github.com/gin-gonic/gin", "size": "Medium", "files": "~150", "question": "How does gin route requests through its middleware chain?" },
{ "name": "terraform", "repo": "https://github.com/hashicorp/terraform", "size": "Large", "files": "~4000", "question": "How does Terraform build and walk the resource dependency graph?" }
],
"Python": [
{ "name": "click", "repo": "https://github.com/pallets/click", "size": "Small", "files": "~60", "question": "How does click parse command-line arguments into commands?" },
{ "name": "flask", "repo": "https://github.com/pallets/flask", "size": "Medium", "files": "~90", "question": "How does Flask dispatch a request to a view function?" },
{ "name": "django", "repo": "https://github.com/django/django", "size": "Large", "files": "~2700", "question": "How does Django's ORM build and execute a query from a QuerySet?" }
],
"Rust": [
{ "name": "clap", "repo": "https://github.com/clap-rs/clap", "size": "Medium", "files": "~200", "question": "How does clap parse arguments against a derived command definition?" },
{ "name": "tokio", "repo": "https://github.com/tokio-rs/tokio", "size": "Large", "files": "~700", "question": "How does tokio schedule and run async tasks on its runtime?" },
{ "name": "deno", "repo": "https://github.com/denoland/deno", "size": "Large", "files": "~1500", "question": "How does Deno load and execute a TypeScript module?" }
],
"Java": [
{ "name": "gson", "repo": "https://github.com/google/gson", "size": "Medium", "files": "~200", "question": "How does Gson serialize an object to JSON?" },
{ "name": "okhttp", "repo": "https://github.com/square/okhttp", "size": "Medium", "files": "~640", "question": "How does OkHttp process a request through its interceptor chain?" },
{ "name": "guava", "repo": "https://github.com/google/guava", "size": "Large", "files": "~3000", "question": "How does Guava's CacheBuilder build and configure a cache?" }
],
"Kotlin": [
{ "name": "koin", "repo": "https://github.com/InsertKoinIO/koin", "size": "Medium", "files": "~300", "question": "How does Koin resolve and inject dependencies?" },
{ "name": "leakcanary", "repo": "https://github.com/square/leakcanary", "size": "Medium", "files": "~250", "question": "How does LeakCanary detect and analyze a memory leak?" }
],
"Swift": [
{ "name": "alamofire", "repo": "https://github.com/Alamofire/Alamofire", "size": "Small", "files": "~100", "question": "How does Alamofire build, send, and validate a request?" }
],
"C#": [
{ "name": "serilog", "repo": "https://github.com/serilog/serilog", "size": "Medium", "files": "~250", "question": "How does Serilog route a log event to its sinks?" },
{ "name": "jellyfin", "repo": "https://github.com/jellyfin/jellyfin", "size": "Large", "files": "~2500", "question": "How does Jellyfin scan and identify items in a media library?" }
],
"Ruby": [
{ "name": "sinatra", "repo": "https://github.com/sinatra/sinatra", "size": "Small", "files": "~60", "question": "How does Sinatra match a request to a route handler?" },
{ "name": "discourse", "repo": "https://github.com/discourse/discourse", "size": "Large", "files": "~3000", "question": "How does Discourse create and render a new post?" }
],
"PHP": [
{ "name": "slim", "repo": "https://github.com/slimphp/Slim", "size": "Small", "files": "~80", "question": "How does Slim handle a request through its middleware?" },
{ "name": "laravel", "repo": "https://github.com/laravel/framework", "size": "Large", "files": "~3000", "question": "How does Laravel resolve and dispatch a route to a controller?" }
],
"C": [
{ "name": "redis", "repo": "https://github.com/redis/redis", "size": "Large", "files": "~600", "question": "How does Redis parse and dispatch a client command?" }
],
"C++": [
{ "name": "json", "repo": "https://github.com/nlohmann/json", "size": "Small", "files": "~100", "question": "How does nlohmann::json parse a JSON string into a value?" },
{ "name": "grpc", "repo": "https://github.com/grpc/grpc", "size": "Large", "files": "~3000", "question": "How does gRPC dispatch an incoming RPC to its handler?" }
],
"Dart": [
{ "name": "flutter", "repo": "https://github.com/flutter/flutter", "size": "Large", "files": "~6000", "question": "How does Flutter build and lay out a widget tree?" }
],
"Svelte": [
{ "name": "shadcn-svelte", "repo": "https://github.com/huntabyte/shadcn-svelte", "size": "Medium", "files": "~600", "question": "How do shadcn-svelte components compose and apply their styling?" }
]
}
+136
View File
@@ -0,0 +1,136 @@
---
name: publish
description: Publishes a new minor or major release of this npm package (codegraph). Reads the latest version from npm, generates a user-perspective CHANGELOG entry from commits since the last tag, bumps package.json, publishes to npm, and creates the matching GitHub release. Use when the user runs /publish or asks to cut, ship, or publish a release / new version.
---
# Publish a release
Cut a **minor or major** release: generate the changelog, bump, publish to npm, and create the GitHub release. Patch releases are intentionally not offered here.
This skill performs the actual publish (npm publish, git push, GitHub release) — that is the whole point of invoking it, so the general "hand the user the commands" rule does **not** apply inside `/publish`. The **confirmation gate in Step 5 is the safeguard**: never run a step past it without explicit approval.
Run from the repo root.
## Workflow
Copy this checklist and work through it in order:
```
- [ ] 1. Preflight: branch, sync, auth
- [ ] 2. Read base version from npm, compute candidates
- [ ] 3. Ask the user: minor or major
- [ ] 4. Generate the CHANGELOG entry from commits since the last tag
- [ ] 5. CONFIRMATION GATE — show changelog + plan, get explicit approval
- [ ] 6. Write CHANGELOG.md, bump, build
- [ ] 7. Commit + push
- [ ] 8. npm publish
- [ ] 9. scripts/release.sh (GitHub release)
- [ ] 10. Verify on the npm registry
```
### Step 1 — Preflight
```bash
git rev-parse --abbrev-ref HEAD # expect: main
git fetch origin
git status --porcelain # working tree should be clean
git rev-list --left-right --count origin/main...HEAD # "<behind> <ahead>"
npm whoami # npm auth (publish will fail without it)
gh auth status # gh auth (release.sh needs it)
```
- If not on `main`, stop and ask the user to confirm releasing from this branch.
- If behind origin, `git pull --ff-only` so the final push is a fast-forward.
- If the tree has **unrelated** uncommitted changes, stop and ask — the release commit only stages 3 files, but a dirty tree usually means something's mid-flight.
- If `npm whoami` or `gh auth status` fails, stop and tell the user to authenticate.
### Step 2 — Base version + candidates
The latest **published** version is the source of truth, not local `package.json`.
```bash
PKG=$(node -p "require('./package.json').name")
BASE=$(npm view "$PKG" version)
node -e "const [a,b]=process.argv[1].split('.').map(Number);console.log('minor ->',a+'.'+(b+1)+'.0');console.log('major ->',(a+1)+'.0.0')" "$BASE"
```
Note if local `package.json` differs from `BASE` (an unpublished bump) — surface it, but still base the new version on npm.
### Step 3 — Ask minor or major
Use the **AskUserQuestion** tool with the two computed candidates as options (show the resulting version in each label, e.g. "minor → 0.8.0"). Set the new version from the answer.
### Step 4 — Generate the changelog entry
```bash
LAST=$(git describe --tags --abbrev=0 --match 'v*' 2>/dev/null)
git log --no-merges "${LAST}..HEAD" --pretty=format:'%h %s'
```
Read the commit subjects; for any whose user impact is unclear, inspect the diff (`git show <hash>` or `git diff "${LAST}..HEAD" -- <path>`). Then **write the entry yourself** following the repo's conventions in `CLAUDE.md` → "Writing changelog entries":
- Header: `## [X.Y.Z] - YYYY-MM-DD` (get the date with `date +%F`).
- Group under `### Added`, `### Changed`, `### Fixed`, `### Removed`, `### Deprecated`, `### Security`**omit empty sections**.
- Write from the **user's perspective** (observable capability/symptom), not the implementation. Collapse noisy commits ("fix typo", "address review") into the feature they belong to or drop them.
- Plan the bottom link reference: `[X.Y.Z]: https://github.com/colbymchenry/codegraph/releases/tag/vX.Y.Z`.
Do not write to any file yet — draft it for review first.
### Step 5 — CONFIRMATION GATE
Show the user, in chat:
1. The new version (`BASE``X.Y.Z`, minor/major).
2. The full drafted changelog entry.
3. The exact actions Steps 69 will take (commit + push + npm publish + GitHub release).
Then **STOP**. Proceed only on explicit approval ("yes" / "proceed"). If the user requests prose changes, revise the draft and re-show. Do not run any command below until approved.
### Step 6 — Write changelog, bump, build
1. Use the **Edit** tool to insert the drafted `## [X.Y.Z]` block at the **top** of `CHANGELOG.md` (under the intro, above the previous version), and add the link reference with the other `[x.y.z]:` links at the bottom.
2. Bump (also updates `package-lock.json`; `--allow-same-version` keeps re-runs safe):
```bash
npm version X.Y.Z --no-git-tag-version --allow-same-version
```
3. Build (fail fast before any push/publish):
```bash
npm run build
```
### Step 7 — Commit + push
`release.sh` tags HEAD, so the bump must be committed first.
```bash
git add package.json package-lock.json CHANGELOG.md
git commit -m "release: X.Y.Z"
git push
```
### Step 8 — Publish to npm
```bash
npm publish --access public
```
### Step 9 — GitHub release
`scripts/release.sh` reads the `## [X.Y.Z]` block from CHANGELOG.md, tags `vX.Y.Z`, pushes the tag, and creates the GitHub release. It is idempotent.
```bash
./scripts/release.sh
```
### Step 10 — Verify
Confirm against the **registry**, not the website (the website caches):
```bash
npm view "$PKG" version # must equal X.Y.Z
```
Report the release URL (`scripts/release.sh` prints it) and the published version.
## If something fails midway
Re-running is safe: `npm version --allow-same-version` no-ops if already bumped, `git commit` skips if nothing's staged (check `git diff --cached --quiet`), `git push` no-ops if up to date, and `scripts/release.sh` skips tag/release steps already done. Re-run from the failed step.