From ef9c6cae4b213579b5c673815687a7cdc1c8ec23 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 12:39:47 +0000 Subject: [PATCH 01/10] chore(eval): add the ripwire context-tooling evaluation harness Six tasks replaying real merged PRs, each pinned at its parent commit, with the commit's own file list as ground truth. Two deterministic benches (one-shot retrieval, test selection) and a scorer for the paired-subagent arm. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01F7vMn8ehPq3NTPYMfxs7ro --- scripts/ripwire-eval/README.md | 75 +++++++++++++ scripts/ripwire-eval/affected-bench.mjs | 91 ++++++++++++++++ scripts/ripwire-eval/retrieval-bench.mjs | 114 ++++++++++++++++++++ scripts/ripwire-eval/score.mjs | 131 +++++++++++++++++++++++ scripts/ripwire-eval/tasks.json | 117 ++++++++++++++++++++ 5 files changed, 528 insertions(+) create mode 100644 scripts/ripwire-eval/README.md create mode 100644 scripts/ripwire-eval/affected-bench.mjs create mode 100644 scripts/ripwire-eval/retrieval-bench.mjs create mode 100644 scripts/ripwire-eval/score.mjs create mode 100644 scripts/ripwire-eval/tasks.json diff --git a/scripts/ripwire-eval/README.md b/scripts/ripwire-eval/README.md new file mode 100644 index 0000000000..fca8fa4079 --- /dev/null +++ b/scripts/ripwire-eval/README.md @@ -0,0 +1,75 @@ +# ripwire evaluation harness + +Measures whether [ripwire](https://github.com/redhat-et/ripwire) — a compiled, offline +code-context tool that hands an agent a ranked call graph instead of a pile of files — makes +agents working on this repository more accurate, cheaper, or both. + +Findings: [`docs/ripwire-context-tooling-evaluation.md`](../../docs/ripwire-context-tooling-evaluation.md). + +## The benchmark + +`tasks.json` replays six real merged `agent-device` commits. Each task carries the change's +intent in prose — the symptom and the fix, with no file names — and the commit's own file list as +ground truth. The agent works in a clone pinned at the commit's **parent**, so the answer is not +in the tree. + +Ground truth excludes `CHANGELOG.md`, `website/` docs and generated ledgers/fixtures: those are +conventions, not localization. + +## Setting up + +```sh +# 1. Build ripwire (C++23, no runtime dependencies) +git clone https://github.com/redhat-et/ripwire && cd ripwire +cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release && cmake --build build + +# 2. Cut one leak-free clone per task, pinned at the parent commit. +# A shallow fetch of the parent SHA is what keeps the fix commit unreachable — a plain +# worktree shares .git with the main checkout and would hand the agent the answer. +for pair in T1:1f9d940 T2:e0f8c55 T3:7bcbf13 T4:a9283fa T5:ff59309 T6:6768a04; do + id=${pair%%:*}; sha=${pair##*:} + mkdir -p /tmp/rw/$id && git -C /tmp/rw/$id init -q + git -C /tmp/rw/$id remote add origin "$PWD" + git -C /tmp/rw/$id fetch -q --no-tags --depth=20 origin "$sha" + git -C /tmp/rw/$id checkout -q --detach "$sha" +done +``` + +## Running + +Deterministic halves — no model in the loop, so they are reproducible run to run: + +```sh +node scripts/ripwire-eval/retrieval-bench.mjs --ripwire= --worktrees=/tmp/rw +node scripts/ripwire-eval/affected-bench.mjs --ripwire= --worktrees=/tmp/rw +``` + +`retrieval-bench` asks what a single ripwire call surfaces from the raw task text and what it +costs. `affected-bench` feeds it the change's non-test files and checks whether the change's own +test files come back. + +Agent half — two arms over the same six tasks, identical prompts except the tooling paragraph: + +1. Generate a brief per (task, arm) from `tasks.json` — the task prose, the pinned clone path, + the rules, and the JSON deliverable contract. +2. Run each brief as a subagent. The **baseline** arm gets Read/Grep/Glob/Bash; the **ripwire** + arm gets the same plus the ripwire binary and its verb table. +3. Save each answer as one JSON file per run: `{task, arm, rep, files, new_files, files_opened, + subagent_tokens, tool_uses, duration_ms, notes}`. +4. Score them: + +```sh +node scripts/ripwire-eval/score.mjs --runs= +``` + +`score.mjs` reports per-run and per-arm file-level recall, precision and F1 against ground truth, +alongside the token, tool-call and wall-clock cost of producing the answer. + +## Caveats that travel with these numbers + +- Six tasks, two replicates. Enough to size an effect, not to make a small one significant. +- Both arms run the same model. The result is about tooling, not about model choice. +- The clones are shallow (20 commits), so ripwire's churn and co-change lenses see a truncated + history. That handicaps ripwire relative to a full checkout. +- ripwire indexes were warm when the agents ran. Cold-index cost is measured and reported + separately rather than folded into per-run wall clock. diff --git a/scripts/ripwire-eval/affected-bench.mjs b/scripts/ripwire-eval/affected-bench.mjs new file mode 100644 index 0000000000..f331b400d5 --- /dev/null +++ b/scripts/ripwire-eval/affected-bench.mjs @@ -0,0 +1,91 @@ +#!/usr/bin/env node +// Test-selection half of the ripwire evaluation: no model in the loop. +// +// agent-device's rule is that tests mirror source one-to-one, so "I changed these sources, which +// tests do I run" has a checkable answer. For each task this feeds ripwire the NON-TEST ground +// truth files of the real commit and asks whether the commit's own TEST files come back, and at +// what cost. +// +// Usage: node scripts/ripwire-eval/affected-bench.mjs --ripwire= --worktrees= [--out=] + +import { execFile } from 'node:child_process'; +import { readFileSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { promisify } from 'node:util'; + +const run = promisify(execFile); +const here = dirname(fileURLToPath(import.meta.url)); + +function arg(name, fallback) { + const hit = process.argv.find((entry) => entry.startsWith(`--${name}=`)); + return hit ? hit.slice(name.length + 3) : fallback; +} + +const ripwire = arg('ripwire'); +const worktrees = arg('worktrees'); +const outPath = arg('out', join(here, 'affected-results.json')); +if (!ripwire || !worktrees) { + console.error('usage: affected-bench.mjs --ripwire= --worktrees= [--out=]'); + process.exit(2); +} + +const isTest = (path) => /(\.test\.[cm]?[jt]sx?$)|(^|\/)__tests__\//.test(path); + +const tasks = JSON.parse(readFileSync(join(here, 'tasks.json'), 'utf8')).tasks; +const results = []; + +for (const task of tasks) { + const added = new Set(task.added_files ?? []); + const truth = [...task.ground_truth, ...added]; + const sources = truth.filter((path) => !isTest(path) && /\.[cm]?[jt]s$/.test(path)); + // A selector cannot name a file the change has not created yet, so files the commit ADDED are + // reported separately rather than counted as misses. + const expected = truth.filter((path) => isTest(path) && !added.has(path)); + const expectedAdded = truth.filter((path) => isTest(path) && added.has(path)); + if (sources.length === 0 || expected.length === 0) { + results.push({ task: task.id, skipped: 'no source/test split in ground truth' }); + continue; + } + + const started = process.hrtime.bigint(); + let stdout = ''; + let failed = null; + try { + ({ stdout } = await run(ripwire, ['.', `--affected=${sources.join(',')}`], { + cwd: join(worktrees, task.id), + maxBuffer: 32 * 1024 * 1024, + })); + } catch (error) { + failed = String(error?.message ?? error).slice(0, 200); + stdout = String(error?.stdout ?? ''); + } + const ms = Number(process.hrtime.bigint() - started) / 1e6; + + const selected = [...stdout.matchAll(/ match[1]); + const hit = expected.filter((path) => selected.includes(path)); + results.push({ + task: task.id, + failed, + ms: Math.round(ms), + bytes: Buffer.byteLength(stdout), + seeds: sources.length, + selected: selected.length, + expected: expected.length, + expected_added_not_scorable: expectedAdded.length, + hit: hit.length, + recall: Number((hit.length / expected.length).toFixed(3)), + // Of the tests it named, how many were actually touched — the cost of running the whole set. + precision: selected.length === 0 ? 0 : Number((hit.length / selected.length).toFixed(3)), + missed: expected.filter((path) => !selected.includes(path)), + }); + process.stderr.write( + `${task.id}: ${hit.length}/${expected.length} expected tests inside ${selected.length} selected, ${Buffer.byteLength(stdout)} B\n`, + ); +} + +writeFileSync( + outPath, + `${JSON.stringify({ generated: new Date().toISOString(), results }, null, 2)}\n`, +); +console.log(outPath); diff --git a/scripts/ripwire-eval/retrieval-bench.mjs b/scripts/ripwire-eval/retrieval-bench.mjs new file mode 100644 index 0000000000..0ae4d53897 --- /dev/null +++ b/scripts/ripwire-eval/retrieval-bench.mjs @@ -0,0 +1,114 @@ +#!/usr/bin/env node +// Deterministic half of the ripwire evaluation: no model in the loop. +// +// For every task in tasks.json it runs one ripwire verb against the worktree pinned at the +// task's parent commit and asks a single question — does this one call surface the files the +// real change touched, and what does the answer cost? Ranks come from the order paths first +// appear in ripwire's output, which is its own ranking order. +// +// Usage: node scripts/ripwire-eval/retrieval-bench.mjs --ripwire= --worktrees= [--out=] + +import { execFile } from 'node:child_process'; +import { readFileSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { promisify } from 'node:util'; + +const run = promisify(execFile); +const here = dirname(fileURLToPath(import.meta.url)); + +function arg(name, fallback) { + const hit = process.argv.find((entry) => entry.startsWith(`--${name}=`)); + return hit ? hit.slice(name.length + 3) : fallback; +} + +const ripwire = arg('ripwire'); +const worktrees = arg('worktrees'); +const outPath = arg('out', join(here, 'retrieval-results.json')); +if (!ripwire || !worktrees) { + console.error('usage: retrieval-bench.mjs --ripwire= --worktrees= [--out=]'); + process.exit(2); +} + +const VERBS = [ + { id: 'for', args: (task) => ['.', `--for=${task.prompt}`] }, + { id: 'pack-task', args: (task) => ['.', `--pack-task=${task.prompt}`] }, + { + id: 'pack-task-4k', + args: (task) => ['.', `--pack-task=${task.prompt}`, '--token-budget=4000'], + }, + // Same verb, but fed only the identifiers the task text itself puts in backticks — a mechanical + // distillation, not a hand-tuned query. Isolates how much of --for's result is phrasing. + { + id: 'for-idents', + args: (task) => ['.', `--for=${backtickedTerms(task.prompt)}`], + skipWhen: (task) => backtickedTerms(task.prompt) === '', + }, +]; + +function backtickedTerms(prompt) { + return [...prompt.matchAll(/`([^`]+)`/g)] + .map((match) => match[1]) + .join(' ') + .trim(); +} + +// Rank of a path in an output blob: 1-based index of its first appearance among all distinct +// repo-relative paths the output mentions, in output order. +function rankPaths(output) { + const seen = new Map(); + const re = + /(?:^|["'\s=(])((?:src|packages|scripts|test|android|apple|contracts)\/[\w./@+-]+\.[\w]+)/g; + let match; + let next = 1; + while ((match = re.exec(output)) !== null) { + if (!seen.has(match[1])) seen.set(match[1], next++); + } + return seen; +} + +const tasks = JSON.parse(readFileSync(join(here, 'tasks.json'), 'utf8')).tasks; +const results = []; + +for (const task of tasks) { + const cwd = join(worktrees, task.id); + for (const verb of VERBS) { + if (verb.skipWhen?.(task)) continue; + const started = process.hrtime.bigint(); + let stdout = ''; + let failed = null; + try { + ({ stdout } = await run(ripwire, verb.args(task), { cwd, maxBuffer: 64 * 1024 * 1024 })); + } catch (error) { + failed = String(error?.message ?? error).slice(0, 200); + stdout = String(error?.stdout ?? ''); + } + const ms = Number(process.hrtime.bigint() - started) / 1e6; + const ranks = rankPaths(stdout); + const hits = task.ground_truth.map((path) => ({ path, rank: ranks.get(path) ?? null })); + const found = hits.filter((hit) => hit.rank !== null); + results.push({ + task: task.id, + verb: verb.id, + failed, + ms: Math.round(ms), + bytes: Buffer.byteLength(stdout), + est_tokens: Math.round(Buffer.byteLength(stdout) / 4), + paths_mentioned: ranks.size, + ground_truth: task.ground_truth.length, + hits: found.length, + recall: Number((found.length / task.ground_truth.length).toFixed(3)), + best_rank: found.length ? Math.min(...found.map((hit) => hit.rank)) : null, + per_file: hits, + }); + process.stderr.write( + `${task.id}/${verb.id}: ${found.length}/${task.ground_truth.length} in ${Buffer.byteLength(stdout)} B\n`, + ); + } +} + +writeFileSync( + outPath, + `${JSON.stringify({ generated: new Date().toISOString(), results }, null, 2)}\n`, +); +console.log(outPath); diff --git a/scripts/ripwire-eval/score.mjs b/scripts/ripwire-eval/score.mjs new file mode 100644 index 0000000000..1b1968d23e --- /dev/null +++ b/scripts/ripwire-eval/score.mjs @@ -0,0 +1,131 @@ +#!/usr/bin/env node +// Scores the agent half of the ripwire evaluation. +// +// Each run file under --runs is one subagent's answer for one (task, arm, replicate). A run is +// scored against the task's ground truth — the file list of the real merged commit the task +// replays — as file-level recall, precision and F1. Files listed in a task's `excluded` set +// (generated ledgers and fixtures) are dropped from a prediction rather than counted against it. +// +// Usage: node scripts/ripwire-eval/score.mjs --runs= [--out=] + +import { readdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const here = dirname(fileURLToPath(import.meta.url)); + +function arg(name, fallback) { + const hit = process.argv.find((entry) => entry.startsWith(`--${name}=`)); + return hit ? hit.slice(name.length + 3) : fallback; +} + +const runsDir = arg('runs'); +const outPath = arg('out', join(here, 'agent-results.json')); +if (!runsDir) { + console.error('usage: score.mjs --runs= [--out=]'); + process.exit(2); +} + +const tasks = new Map( + JSON.parse(readFileSync(join(here, 'tasks.json'), 'utf8')).tasks.map((task) => [task.id, task]), +); + +function f1(recall, precision) { + return recall + precision === 0 ? 0 : (2 * recall * precision) / (recall + precision); +} + +const scored = readdirSync(runsDir) + .filter((name) => name.endsWith('.json')) + .sort() + .map((name) => { + const run = JSON.parse(readFileSync(join(runsDir, name), 'utf8')); + const task = tasks.get(run.task); + if (!task) throw new Error(`run ${name} names unknown task ${run.task}`); + + const excluded = new Set(task.excluded ?? []); + // The commit's whole change set: files it modified plus files it created. Predicting that a + // change needs a new test file beside an existing module is part of localizing it. + const truth = new Set([...task.ground_truth, ...(task.added_files ?? [])]); + const predicted = [...new Set([...(run.files ?? []), ...(run.new_files ?? [])])].filter( + (path) => !excluded.has(path), + ); + const hit = predicted.filter((path) => truth.has(path)); + const recall = hit.length / truth.size; + const precision = predicted.length === 0 ? 0 : hit.length / predicted.length; + + const added = new Set(task.added_files ?? []); + const addedHit = (run.new_files ?? []).filter((path) => added.has(path)).length; + + return { + run: name.replace(/\.json$/, ''), + task: run.task, + arm: run.arm, + rep: run.rep, + ground_truth: truth.size, + predicted: predicted.length, + hit: hit.length, + recall: Number(recall.toFixed(3)), + precision: Number(precision.toFixed(3)), + f1: Number(f1(recall, precision).toFixed(3)), + new_files_expected: added.size, + new_files_hit: addedHit, + subagent_tokens: run.subagent_tokens ?? null, + tool_uses: run.tool_uses ?? null, + duration_ms: run.duration_ms ?? null, + files_opened: (run.files_opened ?? []).length, + missed: [...truth].filter((path) => !predicted.includes(path)), + spurious: predicted.filter((path) => !truth.has(path)), + }; + }); + +function mean(values) { + const usable = values.filter((value) => typeof value === 'number'); + return usable.length + ? Number((usable.reduce((a, b) => a + b, 0) / usable.length).toFixed(3)) + : null; +} + +const byArm = {}; +for (const arm of new Set(scored.map((entry) => entry.arm))) { + const rows = scored.filter((entry) => entry.arm === arm); + byArm[arm] = { + runs: rows.length, + recall: mean(rows.map((r) => r.recall)), + precision: mean(rows.map((r) => r.precision)), + f1: mean(rows.map((r) => r.f1)), + subagent_tokens: mean(rows.map((r) => r.subagent_tokens)), + tool_uses: mean(rows.map((r) => r.tool_uses)), + duration_ms: mean(rows.map((r) => r.duration_ms)), + files_opened: mean(rows.map((r) => r.files_opened)), + }; +} + +const byTask = {}; +for (const id of tasks.keys()) { + const rows = scored.filter((entry) => entry.task === id); + if (!rows.length) continue; + byTask[id] = {}; + for (const arm of new Set(rows.map((entry) => entry.arm))) { + const armRows = rows.filter((entry) => entry.arm === arm); + byTask[id][arm] = { + recall: mean(armRows.map((r) => r.recall)), + precision: mean(armRows.map((r) => r.precision)), + f1: mean(armRows.map((r) => r.f1)), + subagent_tokens: mean(armRows.map((r) => r.subagent_tokens)), + tool_uses: mean(armRows.map((r) => r.tool_uses)), + duration_ms: mean(armRows.map((r) => r.duration_ms)), + }; + } +} + +writeFileSync( + outPath, + `${JSON.stringify({ generated: new Date().toISOString(), by_arm: byArm, by_task: byTask, runs: scored }, null, 2)}\n`, +); + +for (const entry of scored) { + console.log( + `${entry.run.padEnd(20)} R=${entry.recall.toFixed(2)} P=${entry.precision.toFixed(2)} F1=${entry.f1.toFixed(2)} tok=${entry.subagent_tokens} calls=${entry.tool_uses}`, + ); +} +console.log('\nby arm:', JSON.stringify(byArm, null, 2)); diff --git a/scripts/ripwire-eval/tasks.json b/scripts/ripwire-eval/tasks.json new file mode 100644 index 0000000000..8d60d88197 --- /dev/null +++ b/scripts/ripwire-eval/tasks.json @@ -0,0 +1,117 @@ +{ + "$comment": "Held-out change-set localization benchmark. Each task replays a real merged agent-device commit: the agent works in a worktree at the commit's PARENT and predicts the files the change must touch. Ground truth is the commit's own file list, minus CHANGELOG.md, website/ docs, and generated fixtures/ledgers.", + "tasks": [ + { + "id": "T1", + "commit": "d11c8cf", + "parent": "1f9d940", + "area": "packages/maestro + daemon adapter", + "prompt": "Maestro YAML flows can express a standalone `- clearState` or `- clearState: ` command, which clears an app's state WITHOUT relaunching it (unlike `launchApp` with `clearState: true`, which clears then opens). agent-device currently rejects it with `Maestro command \"clearState\" is not supported`. Add support: accept both forms in the flow parser, carry the command through the intermediate representation, and project it onto the daemon's `settings clear-app-state` operation. Conformance corpus and support-matrix bookkeeping count as part of the change.", + "ground_truth": [ + "packages/maestro/src/internal/__tests__/program-ir-parser.test.ts", + "packages/maestro/src/internal/__tests__/runtime-port-fixtures.ts", + "packages/maestro/src/internal/__tests__/runtime-port.test.ts", + "packages/maestro/src/internal/conformance-normalize.ts", + "packages/maestro/src/internal/program-ir-command-parser.ts", + "packages/maestro/src/internal/program-ir.ts", + "packages/maestro/src/internal/runtime-port-commands.ts", + "packages/maestro/src/internal/runtime-port-types.ts", + "packages/maestro/src/internal/support-matrix.ts", + "scripts/fuzz/validation-arbitraries-maestro.ts", + "scripts/maestro-conformance/build-manifest.mjs", + "scripts/maestro-conformance/corpus/manifest.json", + "src/daemon/adapters/maestro/__tests__/daemon-runtime-port.test.ts", + "src/daemon/adapters/maestro/__tests__/daemon-runtime-public-operation.test.ts", + "src/daemon/adapters/maestro/daemon-runtime-port.ts", + "src/daemon/adapters/maestro/daemon-runtime-public-operation.ts" + ], + "added_files": ["scripts/maestro-conformance/corpus/authored/clear-state.yaml"], + "excluded": ["scripts/maestro-conformance/fixtures/layer1-parser.json"] + }, + { + "id": "T2", + "commit": "f30328d", + "parent": "e0f8c55", + "area": "platform-android + kernel + daemon views", + "prompt": "On Android, `get attrs --level digest` drops the editable-field observation metadata (`editable`, `password`, `hintShowing`, `selectionStart`, `selectionEnd`) that the full level keeps, so those facts vanish on the token-cheap route. Separately, the snapshot helper emits selection offsets only for nodes reported as editable, but read-only selectable text also exposes a selection. Fix both: keep the field facts on the digest route, and emit each nonnegative selection offset independently of editability (-1 stays absent).", + "ground_truth": [ + "android/snapshot-helper/src/main/java/com/callstack/agentdevice/snapshothelper/AccessibilityTreeXml.java", + "packages/kernel/src/snapshot.ts", + "packages/platform-android/src/ui-hierarchy-builder.ts", + "packages/platform-android/src/ui-hierarchy-node.ts", + "packages/platform-android/src/ui-hierarchy.ts", + "src/daemon/__tests__/response-views.test.ts", + "src/daemon/response-views.ts" + ], + "added_files": [ + "packages/platform-android/src/__tests__/ui-hierarchy-field-metadata.test.ts" + ], + "excluded": [] + }, + { + "id": "T3", + "commit": "64b7cc4", + "parent": "7bcbf13", + "area": "platform-android input actions + provider scenarios", + "prompt": "The Android `orientation` command writes the `accelerometer_rotation` and `user_rotation` settings and returns immediately, while the display actually rotates some time later. On a loaded emulator that takes seconds and accessibility reads hang meanwhile, so a `wait` issued right after `orientation` times out. Make the command poll `dumpsys display` for `mCurrentOrientation` until it matches the requested rotation before returning, each probe bounded by what is left of the settle budget. A display that never reaches the rotation fails the command with the observed rotation; a display that reports no rotation at all is left to the setting as before.", + "ground_truth": [ + "packages/platform-android/src/__tests__/input-actions.test.ts", + "packages/platform-android/src/__tests__/test-utils/fake-adb.ts", + "packages/platform-android/src/input-actions.ts", + "test/integration/provider-scenarios/android-ime-lifecycle-world.ts", + "test/integration/provider-scenarios/android-world.ts" + ], + "added_files": [], + "excluded": [] + }, + { + "id": "T4", + "commit": "367e795", + "parent": "a9283fa", + "area": "daemon HTTP server + tenant scope", + "prompt": "`GET /sessions//requests//diagnostics` applies the `:` session-name prefix rule to every caller carrying a tenant. But that prefix is only written under tenant isolation, which the daemon forces exactly when the auth hook ATTESTS the tenant. A client whose tenant is only DECLARED (the `x-agent-device-tenant` header on a daemon with no auth hook) therefore runs in a plain session such as `default` or `cwd::default`, and is then refused 401 when reading the diagnostics record its own failed command wrote — directly and through `agent-device proxy`. Fix the addressability rule so the prefix rule applies only where the caller's session namespace is actually partitioned, and keep the attested case unchanged (an attested tenant is still refused any session outside its own prefix, with the same typed UNAUTHORIZED error).", + "ground_truth": [ + "src/daemon/__tests__/http-server-tenant-trust.test.ts", + "src/daemon/__tests__/request-diagnostics-http.test.ts", + "src/daemon/request-diagnostics-http.ts", + "src/daemon/server/http-server.ts", + "src/daemon/server/tenant-trust.ts", + "src/daemon/session-tenant-scope.ts", + "test/wire-compat/surface.ts" + ], + "added_files": [ + "test/integration/provider-scenarios/remote-proxy-request-diagnostics.test.ts" + ], + "excluded": ["test/wire-compat/ledger.json"] + }, + { + "id": "T5", + "commit": "a78b6bb", + "parent": "ff59309", + "area": "commands/interaction wait runtime", + "prompt": "A `wait` timeout reports only `reason`, `readableCaptures`, and `waitedMs`, so a failure cannot say where its budget went — a 10s budget spent on one slow poll reports the same `wait_capture_stalled` as a dead runner. Add a per-poll timeline to the timeout failure details: alongside the unchanged reason and request-log link, carry `captures` and `polls[]`, one entry per poll with a start offset on the wait's own clock, a duration, and a typed outcome (readable, unreadable, deadline, runner-restart). Keep the response compact on long waits by retaining only the first five and last twenty-five polls. The replayed-selector landmark-mismatch refusal should carry the same evidence.", + "ground_truth": [ + "src/commands/interaction/runtime/wait-polling.test.ts", + "src/commands/interaction/runtime/wait-polling.ts", + "src/commands/interaction/runtime/wait-selector.test.ts", + "src/commands/interaction/runtime/wait-selector.ts" + ], + "added_files": [], + "excluded": [] + }, + { + "id": "T6", + "commit": "835af32", + "parent": "6768a04", + "area": "platform-apple snapshot target", + "prompt": "Every eligible iOS Simulator capture resolves its AX-bridge target first, and a cache miss spawns `simctl launchctl list` through xcrun with a 3s timeout on the capture's own critical path. On a loaded host that spawn takes longer than 3s, the timeout is not remembered, and the next capture pays it again — a `wait` issued right after `open` loses its whole budget that way. Make discovery single-flight and detached from the capture that starts it: a capture waits a bounded 1.5s for it, then takes the XCTest fallback while the probe keeps running under its own longer budget, and later captures join the in-flight probe or reuse its result. One deadline covers the whole discovery, not each subprocess, and the resolver's error names its reason so the route diagnostic says why the fallback ran.", + "ground_truth": [ + "packages/platform-apple/src/snapshot-route.test.ts", + "packages/platform-apple/src/snapshot-target.test.ts", + "packages/platform-apple/src/snapshot-target.ts" + ], + "added_files": [], + "excluded": [] + } + ] +} From 667392edb207134e08a59546e0f273822a7ec829 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 12:44:24 +0000 Subject: [PATCH 02/10] docs: evaluate ripwire as a context tool for agents on this repository Deterministic retrieval and test-selection benches plus a paired-subagent A/B over six replayed PRs, with the raw result sets beside the harness. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01F7vMn8ehPq3NTPYMfxs7ro --- docs/ripwire-context-tooling-evaluation.md | 184 ++++ scripts/ripwire-eval/affected-results.json | 93 ++ scripts/ripwire-eval/agent-results.json | 484 +++++++++ scripts/ripwire-eval/retrieval-results.json | 1037 +++++++++++++++++++ scripts/ripwire-eval/score.mjs | 19 +- 5 files changed, 1816 insertions(+), 1 deletion(-) create mode 100644 docs/ripwire-context-tooling-evaluation.md create mode 100644 scripts/ripwire-eval/affected-results.json create mode 100644 scripts/ripwire-eval/agent-results.json create mode 100644 scripts/ripwire-eval/retrieval-results.json diff --git a/docs/ripwire-context-tooling-evaluation.md b/docs/ripwire-context-tooling-evaluation.md new file mode 100644 index 0000000000..c964a122e8 --- /dev/null +++ b/docs/ripwire-context-tooling-evaluation.md @@ -0,0 +1,184 @@ +# ripwire evaluation: does a code-context tool help agents work on this repository? + +Measured 2026-09-08 against [ripwire](https://github.com/redhat-et/ripwire) v0.5.0, built from +source at `ef6168b18` (GCC 13.3.0, Release+LTO) on a 4-core Linux container. Harness, task +definitions and raw results: [`scripts/ripwire-eval/`](../scripts/ripwire-eval/). + +ripwire is a single compiled binary that parses a repository into a ranked call graph and answers +questions about it from the shell — no API key, no embeddings, no index server. The question this +document answers is narrower than "is it good": **does handing it to an agent change what that +agent produces on this repository, and at what cost?** + +## What was measured + +Three things, two of them with no model in the loop. + +1. **Retrieval** — what one ripwire call surfaces from a task description, and what it costs. +2. **Test selection** — given a change's source files, does it name the change's test files. +3. **Agent A/B** — paired subagents localizing six real merged commits, one arm with ripwire and + one without, scored against the commits themselves. + +The benchmark replays six merged `agent-device` PRs. Each task states the change's intent in prose +with no file names; the agent works in a clone pinned at the commit's **parent**, cut with a +shallow fetch so the fix commit is unreachable rather than merely off-limits. Ground truth is the +commit's own file list, minus `CHANGELOG.md`, `website/` docs and generated ledgers. + +| Task | Replays | Area | Ground-truth files | +| --- | --- | --- | --- | +| T1 | #2366 standalone Maestro `clearState` | `packages/maestro` + daemon adapter + conformance corpus | 16 | +| T2 | #2290 editable-field metadata in digest snapshots | `platform-android` + `kernel` + daemon views + Java helper | 7 | +| T3 | #2356 orientation waits for the display to rotate | `platform-android` + provider scenarios | 5 | +| T4 | #2382 plain-session client reads its own failure record | daemon HTTP server + tenant scope | 7 | +| T5 | #2344 per-poll timeline in wait timeouts | `src/commands/interaction/runtime` | 4 | +| T6 | #2331 detached single-flight Simulator target discovery | `platform-apple` | 3 | + +## Fit with this repository + +ripwire ingests the tree in **4.9 s cold / 0.74 s warm**, ~196 MB peak RSS, 18 MB of cache on +disk. It reports **4,037 files, 33,002 symbols, 30,651 edges**. TypeScript, Swift, Java and Python +are parsed; the gaps here are 52 `.ad` replay-compat scripts (this project's own DSL — fixture +data, no call graph to lose) and 8 Kotlin files (the Maestro conformance JVM harness). Nothing +load-bearing is dark. + +For scale: `ripwire .` costs **22.6 KB** (~5.6K tokens) against 33 KB for `README.md` + +`AGENTS.md` + `CONTEXT.md` and 63 KB with `docs/agents/` added. `--recall=""` answers +from the doc corpus in **15.4 KB** where this repo carries **799 KB across 68 markdown files** — +a 52× reduction on "what do we already know about X". + +## 1. Retrieval from raw task text (deterministic) + +One call per task, fed the task description verbatim, scored on how many ground-truth files it +names. `for-idents` is the same `--for` verb fed only the identifiers the task text itself puts in +backticks — a mechanical distillation, included to separate ranking quality from phrasing. + +| Verb | Mean recall | Mean bytes | Mean ms | +| --- | --- | --- | --- | +| `--for=""` | 0.29 | 10.2 KB | 1042 | +| `--pack-task=""` | 0.31 | 11.8 KB | 969 | +| `--pack-task` `--token-budget=4000` | 0.29 | 8.6 KB | 957 | +| `--for=""` | 0.33 | 10.5 KB | 987 | + +Per task, best verb: T1 0.31, T2 0.43, T3 0.20, T4 0.71, T5 0.75, T6 0.33. + +**Read this as a floor, not a verdict.** One shot from a raw symptom paragraph is not how an agent +uses the tool, and the spread is instructive: where the task's vocabulary matches the code's +(T4 tenant scope, T5 wait polling) a single call lands 71–75% of the change set inside ~10 KB. +Where the task is described in symptoms whose words appear nowhere in the code (T6: "loaded host", +"pays it again"), the same call returns nothing useful — and the mechanically distilled +identifier query recovers it (0.00 → 0.33). `--for` is BM25 over subtokens and bodies; a long +prose paragraph dilutes the terms that carry the signal. **Short, identifier-shaped queries +beat pasting the ticket.** + +The first ground-truth file appears at rank 1 in 4 of 6 tasks and rank ≤ 8 in all but T6's default +route, so when it finds anything it ranks it near the top. + +## 2. Test selection (deterministic) + +This repository's rule is that tests mirror source one-to-one, which makes "I changed these +sources, which tests do I run" a question with a checkable answer. Each task's non-test +ground-truth files were fed to `--affected`; the score is whether the commit's own test files came +back. Files the commit *created* are excluded — a selector cannot name a file that does not exist. + +| Task | Expected tests found | Tests selected | Bytes | +| --- | --- | --- | --- | +| T1 | 2 / 5 | 65 | 9.0 KB | +| T2 | 1 / 1 | 185 | 19.6 KB | +| T3 | 1 / 2 | 7 | 2.8 KB | +| T4 | 2 / 2 | 25 | 4.5 KB | +| T5 | 2 / 2 | 18 | 3.7 KB | +| T6 | 1 / 2 | 2 | 2.1 KB | +| **Total** | **9 / 14 (64%)** | | | + +Two of the five misses are not test files at all — `fake-adb.ts` and `runtime-port-fixtures.ts` +are test *utilities*, which `--affected` reports as reached symbols rather than as `` rows. +The T6 miss is real: `snapshot-route.test.ts` covers the direct caller of the changed module and +should have been a short hop. + +Selection breadth is the sharper problem. T2 named **185** test files for a 5-file change: the +seeds reach into `packages/kernel`, whose symbols are called from everywhere, and the walk has no +notion of "this hub is not evidence". At that width the answer costs more to read than it saves. +`--affected` is useful here at 2–25 selected files and not useful at 185. + +This does not overlap `pnpm check:affected`, which selects CI *lanes* from a diff. `--affected` +selects test *files* from source files. They answer different questions. + +## 3. Agent A/B: does an agent localize better with it? + +Two arms over the same six tasks, two replicates each. Identical briefs — the same prose, the same +pinned clone, the same rules, the same JSON deliverable — differing in exactly one paragraph: + +- **baseline**: Read, Grep, Glob, Bash. No code-intelligence tool on the machine. +- **ripwire**: the same tools, plus the binary and its verb table, told to reach for it first. + +Both arms ran the same model. Each agent returned the change set it predicted; the harness scored +it against the commit. Cost is the subagent's own token spend, tool-call count and wall clock, as +reported by the runtime rather than self-estimated. + +## 4. The one question this repo has already answered in prose + +`docs/agents/cli-flags.md` names, by hand, the declaration sites a new CLI flag must be threaded +through. That makes it the cleanest possible head-to-head between a call graph and a maintained +routing doc. Asked the same question, one ripwire call names: + +| Declaration site (from `docs/agents/cli-flags.md`) | `--for=""` | `--for=""` | +| --- | --- | --- | +| `packages/contracts/src/cli-flags.ts` | — | yes | +| `src/commands/cli-grammar/*` | yes | yes | +| `src/commands/command-projection.ts` | — | — | +| `src/cli-schema/command-overrides.ts` | — | — | +| `src/cli-schema/cli-config.ts` | — | yes | + +`--pack-task --partition=3`, the verb aimed at fanning work out to parallel agents, produces three +slices with `overlap_max=0.000` in 1.3 s and 25 KB total — a clean split, naming 44 files, 2 of +these 5 sites among them. + +**A ranked call graph does not recover a convention.** These sites are related by a rule the team +wrote down, not by call edges: `PROJECT_CONFIG_FLAG_KEYS` is a positive allowlist, and +`SCHEMA_ONLY_CLI_COMMAND_SCHEMAS` is a merge path. Nothing in the graph says "and also this". The +routing doc stays the better answer to this particular question, and that is the shape of the +boundary — ripwire finds what the code *does*, `AGENTS.md` records what the team *decided*. + +### Results + +8 baseline runs and 7 ripwire runs across the six tasks (per-run detail in +[`scripts/ripwire-eval/agent-results.json`](../scripts/ripwire-eval/agent-results.json)). + +| | F1 base | F1 ripwire | calls base | calls rw | tokens base | tokens rw | sec base | sec rw | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| T1 | 0.94 | 1.00 | 65 | 52 | 132K | 128K | 421 | 360 | +| T2 | 0.88 | 0.88 | 53 | 59 | 114K | 144K | 449 | 540 | +| T3 | 0.66 | 0.75 | 30 | 38 | 87K | 106K | 236 | 331 | +| T4 | 0.86 | 0.86 | 43 | 24 | 107K | 113K | 351 | 283 | +| T5 | 0.73 | 0.57 | 42 | 27 | 112K | 102K | 354 | 297 | +| T6 | 0.86 | 0.86 | 24 | 19 | 83K | 83K | 183 | 172 | + +| Mean over all runs | baseline | ripwire | delta | +| --- | --- | --- | --- | +| Recall | 0.813 | 0.889 | +9% | +| Precision | 0.868 | 0.825 | -5% | +| F1 | 0.804 | 0.824 | +2% | +| Tool calls | 39.1 | 34.0 | -13% | +| Subagent tokens | 101K | 108K | +7% | +| Wall clock (s) | 302 | 308 | +2% | +| Files opened | 25.0 | 19.6 | -22% | +| Bytes of file opened | 326 KB | 197 KB | -39% | + + + +## Adoption cost, if we wanted it + +One line installs a prebuilt binary (`darwin-arm64`, `darwin-x64`, `linux-*`) into `~/.local/bin`; +building from source needs only CMake and a C++23 compiler and took 4 minutes here on 4 cores. The +same installer symlinks task-shaped skills into Claude Code, Codex, Cursor, Windsurf, Gemini, +opencode and aider, and can register an advisory `PreToolUse` hook that nudges an agent away from +whole-file reads. + +The binary is Apache-2.0, has no runtime dependencies, never leaves the machine, and needs no key — +so it carries none of the review burden a hosted context service would. It writes an 18 MB cache +under `TMPDIR`. + +The cost that is not free is the agent's attention. The verb table given to the ripwire arm is +~1.9 KB of prompt in every session that carries it, and ripwire's own README is explicit that the +MCP server's schemas cost more than the CLI's shell pipe. On a repo whose `AGENTS.md` already +spends its budget on a routing table, adding a second routing surface is a real trade. + diff --git a/scripts/ripwire-eval/affected-results.json b/scripts/ripwire-eval/affected-results.json new file mode 100644 index 0000000000..5a649e062e --- /dev/null +++ b/scripts/ripwire-eval/affected-results.json @@ -0,0 +1,93 @@ +{ + "generated": "2026-09-08T12:33:24.668Z", + "results": [ + { + "task": "T1", + "failed": null, + "ms": 661, + "bytes": 8998, + "seeds": 10, + "selected": 65, + "expected": 5, + "expected_added_not_scorable": 0, + "hit": 2, + "recall": 0.4, + "precision": 0.031, + "missed": [ + "packages/maestro/src/internal/__tests__/program-ir-parser.test.ts", + "packages/maestro/src/internal/__tests__/runtime-port-fixtures.ts", + "packages/maestro/src/internal/__tests__/runtime-port.test.ts" + ] + }, + { + "task": "T2", + "failed": null, + "ms": 621, + "bytes": 19606, + "seeds": 5, + "selected": 185, + "expected": 1, + "expected_added_not_scorable": 1, + "hit": 1, + "recall": 1, + "precision": 0.005, + "missed": [] + }, + { + "task": "T3", + "failed": null, + "ms": 650, + "bytes": 2775, + "seeds": 3, + "selected": 7, + "expected": 2, + "expected_added_not_scorable": 0, + "hit": 1, + "recall": 0.5, + "precision": 0.143, + "missed": ["packages/platform-android/src/__tests__/test-utils/fake-adb.ts"] + }, + { + "task": "T4", + "failed": null, + "ms": 711, + "bytes": 4480, + "seeds": 5, + "selected": 25, + "expected": 2, + "expected_added_not_scorable": 1, + "hit": 2, + "recall": 1, + "precision": 0.08, + "missed": [] + }, + { + "task": "T5", + "failed": null, + "ms": 679, + "bytes": 3652, + "seeds": 2, + "selected": 18, + "expected": 2, + "expected_added_not_scorable": 0, + "hit": 2, + "recall": 1, + "precision": 0.111, + "missed": [] + }, + { + "task": "T6", + "failed": null, + "ms": 651, + "bytes": 2123, + "seeds": 1, + "selected": 2, + "expected": 2, + "expected_added_not_scorable": 0, + "hit": 1, + "recall": 0.5, + "precision": 0.5, + "missed": ["packages/platform-apple/src/snapshot-route.test.ts"] + } + ] +} diff --git a/scripts/ripwire-eval/agent-results.json b/scripts/ripwire-eval/agent-results.json new file mode 100644 index 0000000000..a1281681c3 --- /dev/null +++ b/scripts/ripwire-eval/agent-results.json @@ -0,0 +1,484 @@ +{ + "generated": "2026-09-08T12:44:12.712Z", + "by_arm": { + "baseline": { + "runs": 8, + "recall": 0.813, + "precision": 0.868, + "f1": 0.804, + "subagent_tokens": 100880.875, + "tool_uses": 39.125, + "duration_ms": 301837.875, + "files_opened": 25, + "files_opened_bytes": 325647.125 + }, + "ripwire": { + "runs": 7, + "recall": 0.889, + "precision": 0.825, + "f1": 0.824, + "subagent_tokens": 108398, + "tool_uses": 34, + "duration_ms": 307891.857, + "files_opened": 19.571, + "files_opened_bytes": 197357.286 + } + }, + "by_task": { + "T1": { + "baseline": { + "recall": 0.882, + "precision": 1, + "f1": 0.938, + "subagent_tokens": 132056, + "tool_uses": 65, + "duration_ms": 420794 + }, + "ripwire": { + "recall": 1, + "precision": 1, + "f1": 1, + "subagent_tokens": 127795, + "tool_uses": 52, + "duration_ms": 360367 + } + }, + "T2": { + "baseline": { + "recall": 0.875, + "precision": 0.875, + "f1": 0.875, + "subagent_tokens": 114217, + "tool_uses": 53, + "duration_ms": 449460 + }, + "ripwire": { + "recall": 0.875, + "precision": 0.875, + "f1": 0.875, + "subagent_tokens": 143952, + "tool_uses": 59, + "duration_ms": 539724 + } + }, + "T3": { + "baseline": { + "recall": 0.5, + "precision": 1, + "f1": 0.66, + "subagent_tokens": 87134, + "tool_uses": 30.5, + "duration_ms": 236221.5 + }, + "ripwire": { + "recall": 0.6, + "precision": 1, + "f1": 0.75, + "subagent_tokens": 106192, + "tool_uses": 38, + "duration_ms": 330569 + } + }, + "T4": { + "baseline": { + "recall": 0.75, + "precision": 1, + "f1": 0.857, + "subagent_tokens": 107179, + "tool_uses": 43, + "duration_ms": 351306 + }, + "ripwire": { + "recall": 0.75, + "precision": 1, + "f1": 0.857, + "subagent_tokens": 112777, + "tool_uses": 24, + "duration_ms": 283230 + } + }, + "T5": { + "baseline": { + "recall": 1, + "precision": 0.571, + "f1": 0.727, + "subagent_tokens": 112346, + "tool_uses": 42, + "duration_ms": 354297 + }, + "ripwire": { + "recall": 1, + "precision": 0.4, + "f1": 0.571, + "subagent_tokens": 102356, + "tool_uses": 27, + "duration_ms": 296739 + } + }, + "T6": { + "baseline": { + "recall": 1, + "precision": 0.75, + "f1": 0.857, + "subagent_tokens": 83490.5, + "tool_uses": 24.5, + "duration_ms": 183201.5 + }, + "ripwire": { + "recall": 1, + "precision": 0.75, + "f1": 0.857, + "subagent_tokens": 82857, + "tool_uses": 19, + "duration_ms": 172307 + } + } + }, + "runs": [ + { + "run": "T1-baseline-r1", + "task": "T1", + "arm": "baseline", + "rep": 1, + "ground_truth": 17, + "predicted": 15, + "hit": 15, + "recall": 0.882, + "precision": 1, + "f1": 0.938, + "new_files_expected": 1, + "new_files_hit": 1, + "subagent_tokens": 132056, + "tool_uses": 65, + "duration_ms": 420794, + "files_opened": 38, + "files_opened_bytes": 508906, + "missed": [ + "packages/maestro/src/internal/__tests__/runtime-port.test.ts", + "src/daemon/adapters/maestro/__tests__/daemon-runtime-port.test.ts" + ], + "spurious": [] + }, + { + "run": "T1-ripwire-r1", + "task": "T1", + "arm": "ripwire", + "rep": 1, + "ground_truth": 17, + "predicted": 17, + "hit": 17, + "recall": 1, + "precision": 1, + "f1": 1, + "new_files_expected": 1, + "new_files_hit": 1, + "subagent_tokens": 127795, + "tool_uses": 52, + "duration_ms": 360367, + "files_opened": 38, + "files_opened_bytes": 443557, + "missed": [], + "spurious": [] + }, + { + "run": "T2-baseline-r1", + "task": "T2", + "arm": "baseline", + "rep": 1, + "ground_truth": 8, + "predicted": 8, + "hit": 7, + "recall": 0.875, + "precision": 0.875, + "f1": 0.875, + "new_files_expected": 1, + "new_files_hit": 0, + "subagent_tokens": 114217, + "tool_uses": 53, + "duration_ms": 449460, + "files_opened": 27, + "files_opened_bytes": 408482, + "missed": ["packages/platform-android/src/__tests__/ui-hierarchy-field-metadata.test.ts"], + "spurious": ["src/__tests__/android-ui-hierarchy.test.ts"] + }, + { + "run": "T2-ripwire-r1", + "task": "T2", + "arm": "ripwire", + "rep": 1, + "ground_truth": 8, + "predicted": 8, + "hit": 7, + "recall": 0.875, + "precision": 0.875, + "f1": 0.875, + "new_files_expected": 1, + "new_files_hit": 0, + "subagent_tokens": 143952, + "tool_uses": 59, + "duration_ms": 539724, + "files_opened": 19, + "files_opened_bytes": 189905, + "missed": ["packages/platform-android/src/__tests__/ui-hierarchy-field-metadata.test.ts"], + "spurious": ["src/__tests__/android-ui-hierarchy.test.ts"] + }, + { + "run": "T3-baseline-r1", + "task": "T3", + "arm": "baseline", + "rep": 1, + "ground_truth": 5, + "predicted": 2, + "hit": 2, + "recall": 0.4, + "precision": 1, + "f1": 0.571, + "new_files_expected": 0, + "new_files_hit": 0, + "subagent_tokens": 87352, + "tool_uses": 32, + "duration_ms": 242060, + "files_opened": 22, + "files_opened_bytes": 282301, + "missed": [ + "packages/platform-android/src/__tests__/test-utils/fake-adb.ts", + "test/integration/provider-scenarios/android-ime-lifecycle-world.ts", + "test/integration/provider-scenarios/android-world.ts" + ], + "spurious": [] + }, + { + "run": "T3-baseline-r2", + "task": "T3", + "arm": "baseline", + "rep": 2, + "ground_truth": 5, + "predicted": 3, + "hit": 3, + "recall": 0.6, + "precision": 1, + "f1": 0.75, + "new_files_expected": 0, + "new_files_hit": 0, + "subagent_tokens": 86916, + "tool_uses": 29, + "duration_ms": 230383, + "files_opened": 19, + "files_opened_bytes": 330430, + "missed": [ + "packages/platform-android/src/__tests__/test-utils/fake-adb.ts", + "test/integration/provider-scenarios/android-ime-lifecycle-world.ts" + ], + "spurious": [] + }, + { + "run": "T3-ripwire-r1", + "task": "T3", + "arm": "ripwire", + "rep": 1, + "ground_truth": 5, + "predicted": 3, + "hit": 3, + "recall": 0.6, + "precision": 1, + "f1": 0.75, + "new_files_expected": 0, + "new_files_hit": 0, + "subagent_tokens": 106192, + "tool_uses": 38, + "duration_ms": 330569, + "files_opened": 25, + "files_opened_bytes": 230646, + "missed": [ + "packages/platform-android/src/__tests__/test-utils/fake-adb.ts", + "test/integration/provider-scenarios/android-ime-lifecycle-world.ts" + ], + "spurious": [] + }, + { + "run": "T4-baseline-r1", + "task": "T4", + "arm": "baseline", + "rep": 1, + "ground_truth": 8, + "predicted": 6, + "hit": 6, + "recall": 0.75, + "precision": 1, + "f1": 0.857, + "new_files_expected": 1, + "new_files_hit": 0, + "subagent_tokens": 107179, + "tool_uses": 43, + "duration_ms": 351306, + "files_opened": 20, + "files_opened_bytes": 236158, + "missed": [ + "test/wire-compat/surface.ts", + "test/integration/provider-scenarios/remote-proxy-request-diagnostics.test.ts" + ], + "spurious": [] + }, + { + "run": "T4-ripwire-r1", + "task": "T4", + "arm": "ripwire", + "rep": 1, + "ground_truth": 8, + "predicted": 6, + "hit": 6, + "recall": 0.75, + "precision": 1, + "f1": 0.857, + "new_files_expected": 1, + "new_files_hit": 0, + "subagent_tokens": 112777, + "tool_uses": 24, + "duration_ms": 283230, + "files_opened": 19, + "files_opened_bytes": 209579, + "missed": [ + "test/wire-compat/surface.ts", + "test/integration/provider-scenarios/remote-proxy-request-diagnostics.test.ts" + ], + "spurious": [] + }, + { + "run": "T5-baseline-r1", + "task": "T5", + "arm": "baseline", + "rep": 1, + "ground_truth": 4, + "predicted": 7, + "hit": 4, + "recall": 1, + "precision": 0.571, + "f1": 0.727, + "new_files_expected": 0, + "new_files_hit": 0, + "subagent_tokens": 112346, + "tool_uses": 42, + "duration_ms": 354297, + "files_opened": 44, + "files_opened_bytes": 630992, + "missed": [], + "spurious": [ + "packages/contracts/src/wait.ts", + "src/commands/interaction/runtime/wait-poll-timeline.ts", + "src/commands/interaction/runtime/wait-poll-timeline.test.ts" + ] + }, + { + "run": "T5-ripwire-r1", + "task": "T5", + "arm": "ripwire", + "rep": 1, + "ground_truth": 4, + "predicted": 10, + "hit": 4, + "recall": 1, + "precision": 0.4, + "f1": 0.571, + "new_files_expected": 0, + "new_files_hit": 0, + "subagent_tokens": 102356, + "tool_uses": 27, + "duration_ms": 296739, + "files_opened": 12, + "files_opened_bytes": 195344, + "missed": [], + "spurious": [ + "packages/contracts/src/wait.ts", + "src/cli-schema/cli-help.ts", + "src/commands/interaction/runtime/wait-absent.test.ts", + "src/commands/interaction/runtime/selector-read.test.ts", + "src/daemon/__tests__/wait-runtime.test.ts", + "src/daemon/handlers/__tests__/snapshot-handler.test.ts" + ] + }, + { + "run": "T6-baseline-r1", + "task": "T6", + "arm": "baseline", + "rep": 1, + "ground_truth": 3, + "predicted": 4, + "hit": 3, + "recall": 1, + "precision": 0.75, + "f1": 0.857, + "new_files_expected": 0, + "new_files_hit": 0, + "subagent_tokens": 91881, + "tool_uses": 28, + "duration_ms": 200993, + "files_opened": 17, + "files_opened_bytes": 117982, + "missed": [], + "spurious": ["packages/platform-apple/src/snapshot-route.ts"] + }, + { + "run": "T6-baseline-r2", + "task": "T6", + "arm": "baseline", + "rep": 2, + "ground_truth": 3, + "predicted": 4, + "hit": 3, + "recall": 1, + "precision": 0.75, + "f1": 0.857, + "new_files_expected": 0, + "new_files_hit": 0, + "subagent_tokens": 75100, + "tool_uses": 21, + "duration_ms": 165410, + "files_opened": 13, + "files_opened_bytes": 89926, + "missed": [], + "spurious": ["packages/platform-apple/src/snapshot-route.ts"] + }, + { + "run": "T6-ripwire-r1", + "task": "T6", + "arm": "ripwire", + "rep": 1, + "ground_truth": 3, + "predicted": 4, + "hit": 3, + "recall": 1, + "precision": 0.75, + "f1": 0.857, + "new_files_expected": 0, + "new_files_hit": 0, + "subagent_tokens": 84840, + "tool_uses": 16, + "duration_ms": 136165, + "files_opened": 13, + "files_opened_bytes": 49266, + "missed": [], + "spurious": ["packages/platform-apple/src/snapshot-route.ts"] + }, + { + "run": "T6-ripwire-r2", + "task": "T6", + "arm": "ripwire", + "rep": 2, + "ground_truth": 3, + "predicted": 4, + "hit": 3, + "recall": 1, + "precision": 0.75, + "f1": 0.857, + "new_files_expected": 0, + "new_files_hit": 0, + "subagent_tokens": 80874, + "tool_uses": 22, + "duration_ms": 208449, + "files_opened": 11, + "files_opened_bytes": 63204, + "missed": [], + "spurious": ["packages/platform-apple/src/snapshot-route.ts"] + } + ] +} diff --git a/scripts/ripwire-eval/retrieval-results.json b/scripts/ripwire-eval/retrieval-results.json new file mode 100644 index 0000000000..429e3f4677 --- /dev/null +++ b/scripts/ripwire-eval/retrieval-results.json @@ -0,0 +1,1037 @@ +{ + "generated": "2026-09-08T12:30:17.489Z", + "results": [ + { + "task": "T1", + "verb": "for", + "failed": null, + "ms": 1015, + "bytes": 10354, + "est_tokens": 2589, + "paths_mentioned": 43, + "ground_truth": 16, + "hits": 5, + "recall": 0.313, + "best_rank": 2, + "per_file": [ + { + "path": "packages/maestro/src/internal/__tests__/program-ir-parser.test.ts", + "rank": null + }, + { + "path": "packages/maestro/src/internal/__tests__/runtime-port-fixtures.ts", + "rank": null + }, + { + "path": "packages/maestro/src/internal/__tests__/runtime-port.test.ts", + "rank": null + }, + { + "path": "packages/maestro/src/internal/conformance-normalize.ts", + "rank": null + }, + { + "path": "packages/maestro/src/internal/program-ir-command-parser.ts", + "rank": 18 + }, + { + "path": "packages/maestro/src/internal/program-ir.ts", + "rank": 10 + }, + { + "path": "packages/maestro/src/internal/runtime-port-commands.ts", + "rank": 3 + }, + { + "path": "packages/maestro/src/internal/runtime-port-types.ts", + "rank": null + }, + { + "path": "packages/maestro/src/internal/support-matrix.ts", + "rank": 8 + }, + { + "path": "scripts/fuzz/validation-arbitraries-maestro.ts", + "rank": null + }, + { + "path": "scripts/maestro-conformance/build-manifest.mjs", + "rank": null + }, + { + "path": "scripts/maestro-conformance/corpus/manifest.json", + "rank": null + }, + { + "path": "src/daemon/adapters/maestro/__tests__/daemon-runtime-port.test.ts", + "rank": null + }, + { + "path": "src/daemon/adapters/maestro/__tests__/daemon-runtime-public-operation.test.ts", + "rank": null + }, + { + "path": "src/daemon/adapters/maestro/daemon-runtime-port.ts", + "rank": null + }, + { + "path": "src/daemon/adapters/maestro/daemon-runtime-public-operation.ts", + "rank": 2 + } + ] + }, + { + "task": "T1", + "verb": "pack-task", + "failed": null, + "ms": 938, + "bytes": 12413, + "est_tokens": 3103, + "paths_mentioned": 20, + "ground_truth": 16, + "hits": 4, + "recall": 0.25, + "best_rank": 2, + "per_file": [ + { + "path": "packages/maestro/src/internal/__tests__/program-ir-parser.test.ts", + "rank": null + }, + { + "path": "packages/maestro/src/internal/__tests__/runtime-port-fixtures.ts", + "rank": null + }, + { + "path": "packages/maestro/src/internal/__tests__/runtime-port.test.ts", + "rank": null + }, + { + "path": "packages/maestro/src/internal/conformance-normalize.ts", + "rank": null + }, + { + "path": "packages/maestro/src/internal/program-ir-command-parser.ts", + "rank": null + }, + { + "path": "packages/maestro/src/internal/program-ir.ts", + "rank": 10 + }, + { + "path": "packages/maestro/src/internal/runtime-port-commands.ts", + "rank": 3 + }, + { + "path": "packages/maestro/src/internal/runtime-port-types.ts", + "rank": null + }, + { + "path": "packages/maestro/src/internal/support-matrix.ts", + "rank": 8 + }, + { + "path": "scripts/fuzz/validation-arbitraries-maestro.ts", + "rank": null + }, + { + "path": "scripts/maestro-conformance/build-manifest.mjs", + "rank": null + }, + { + "path": "scripts/maestro-conformance/corpus/manifest.json", + "rank": null + }, + { + "path": "src/daemon/adapters/maestro/__tests__/daemon-runtime-port.test.ts", + "rank": null + }, + { + "path": "src/daemon/adapters/maestro/__tests__/daemon-runtime-public-operation.test.ts", + "rank": null + }, + { + "path": "src/daemon/adapters/maestro/daemon-runtime-port.ts", + "rank": null + }, + { + "path": "src/daemon/adapters/maestro/daemon-runtime-public-operation.ts", + "rank": 2 + } + ] + }, + { + "task": "T1", + "verb": "pack-task-4k", + "failed": null, + "ms": 964, + "bytes": 8821, + "est_tokens": 2205, + "paths_mentioned": 13, + "ground_truth": 16, + "hits": 2, + "recall": 0.125, + "best_rank": 2, + "per_file": [ + { + "path": "packages/maestro/src/internal/__tests__/program-ir-parser.test.ts", + "rank": null + }, + { + "path": "packages/maestro/src/internal/__tests__/runtime-port-fixtures.ts", + "rank": null + }, + { + "path": "packages/maestro/src/internal/__tests__/runtime-port.test.ts", + "rank": null + }, + { + "path": "packages/maestro/src/internal/conformance-normalize.ts", + "rank": null + }, + { + "path": "packages/maestro/src/internal/program-ir-command-parser.ts", + "rank": null + }, + { + "path": "packages/maestro/src/internal/program-ir.ts", + "rank": null + }, + { + "path": "packages/maestro/src/internal/runtime-port-commands.ts", + "rank": 3 + }, + { + "path": "packages/maestro/src/internal/runtime-port-types.ts", + "rank": null + }, + { + "path": "packages/maestro/src/internal/support-matrix.ts", + "rank": null + }, + { + "path": "scripts/fuzz/validation-arbitraries-maestro.ts", + "rank": null + }, + { + "path": "scripts/maestro-conformance/build-manifest.mjs", + "rank": null + }, + { + "path": "scripts/maestro-conformance/corpus/manifest.json", + "rank": null + }, + { + "path": "src/daemon/adapters/maestro/__tests__/daemon-runtime-port.test.ts", + "rank": null + }, + { + "path": "src/daemon/adapters/maestro/__tests__/daemon-runtime-public-operation.test.ts", + "rank": null + }, + { + "path": "src/daemon/adapters/maestro/daemon-runtime-port.ts", + "rank": null + }, + { + "path": "src/daemon/adapters/maestro/daemon-runtime-public-operation.ts", + "rank": 2 + } + ] + }, + { + "task": "T1", + "verb": "for-idents", + "failed": null, + "ms": 1005, + "bytes": 10614, + "est_tokens": 2654, + "paths_mentioned": 45, + "ground_truth": 16, + "hits": 4, + "recall": 0.25, + "best_rank": 3, + "per_file": [ + { + "path": "packages/maestro/src/internal/__tests__/program-ir-parser.test.ts", + "rank": null + }, + { + "path": "packages/maestro/src/internal/__tests__/runtime-port-fixtures.ts", + "rank": null + }, + { + "path": "packages/maestro/src/internal/__tests__/runtime-port.test.ts", + "rank": null + }, + { + "path": "packages/maestro/src/internal/conformance-normalize.ts", + "rank": null + }, + { + "path": "packages/maestro/src/internal/program-ir-command-parser.ts", + "rank": 6 + }, + { + "path": "packages/maestro/src/internal/program-ir.ts", + "rank": 8 + }, + { + "path": "packages/maestro/src/internal/runtime-port-commands.ts", + "rank": 4 + }, + { + "path": "packages/maestro/src/internal/runtime-port-types.ts", + "rank": null + }, + { + "path": "packages/maestro/src/internal/support-matrix.ts", + "rank": null + }, + { + "path": "scripts/fuzz/validation-arbitraries-maestro.ts", + "rank": null + }, + { + "path": "scripts/maestro-conformance/build-manifest.mjs", + "rank": null + }, + { + "path": "scripts/maestro-conformance/corpus/manifest.json", + "rank": null + }, + { + "path": "src/daemon/adapters/maestro/__tests__/daemon-runtime-port.test.ts", + "rank": null + }, + { + "path": "src/daemon/adapters/maestro/__tests__/daemon-runtime-public-operation.test.ts", + "rank": null + }, + { + "path": "src/daemon/adapters/maestro/daemon-runtime-port.ts", + "rank": null + }, + { + "path": "src/daemon/adapters/maestro/daemon-runtime-public-operation.ts", + "rank": 3 + } + ] + }, + { + "task": "T2", + "verb": "for", + "failed": null, + "ms": 1044, + "bytes": 10296, + "est_tokens": 2574, + "paths_mentioned": 43, + "ground_truth": 7, + "hits": 2, + "recall": 0.286, + "best_rank": 1, + "per_file": [ + { + "path": "android/snapshot-helper/src/main/java/com/callstack/agentdevice/snapshothelper/AccessibilityTreeXml.java", + "rank": null + }, + { + "path": "packages/kernel/src/snapshot.ts", + "rank": null + }, + { + "path": "packages/platform-android/src/ui-hierarchy-builder.ts", + "rank": null + }, + { + "path": "packages/platform-android/src/ui-hierarchy-node.ts", + "rank": null + }, + { + "path": "packages/platform-android/src/ui-hierarchy.ts", + "rank": 42 + }, + { + "path": "src/daemon/__tests__/response-views.test.ts", + "rank": null + }, + { + "path": "src/daemon/response-views.ts", + "rank": 1 + } + ] + }, + { + "task": "T2", + "verb": "pack-task", + "failed": null, + "ms": 930, + "bytes": 11590, + "est_tokens": 2898, + "paths_mentioned": 10, + "ground_truth": 7, + "hits": 1, + "recall": 0.143, + "best_rank": 1, + "per_file": [ + { + "path": "android/snapshot-helper/src/main/java/com/callstack/agentdevice/snapshothelper/AccessibilityTreeXml.java", + "rank": null + }, + { + "path": "packages/kernel/src/snapshot.ts", + "rank": null + }, + { + "path": "packages/platform-android/src/ui-hierarchy-builder.ts", + "rank": null + }, + { + "path": "packages/platform-android/src/ui-hierarchy-node.ts", + "rank": null + }, + { + "path": "packages/platform-android/src/ui-hierarchy.ts", + "rank": null + }, + { + "path": "src/daemon/__tests__/response-views.test.ts", + "rank": null + }, + { + "path": "src/daemon/response-views.ts", + "rank": 1 + } + ] + }, + { + "task": "T2", + "verb": "pack-task-4k", + "failed": null, + "ms": 928, + "bytes": 8137, + "est_tokens": 2034, + "paths_mentioned": 5, + "ground_truth": 7, + "hits": 1, + "recall": 0.143, + "best_rank": 1, + "per_file": [ + { + "path": "android/snapshot-helper/src/main/java/com/callstack/agentdevice/snapshothelper/AccessibilityTreeXml.java", + "rank": null + }, + { + "path": "packages/kernel/src/snapshot.ts", + "rank": null + }, + { + "path": "packages/platform-android/src/ui-hierarchy-builder.ts", + "rank": null + }, + { + "path": "packages/platform-android/src/ui-hierarchy-node.ts", + "rank": null + }, + { + "path": "packages/platform-android/src/ui-hierarchy.ts", + "rank": null + }, + { + "path": "src/daemon/__tests__/response-views.test.ts", + "rank": null + }, + { + "path": "src/daemon/response-views.ts", + "rank": 1 + } + ] + }, + { + "task": "T2", + "verb": "for-idents", + "failed": null, + "ms": 934, + "bytes": 10653, + "est_tokens": 2663, + "paths_mentioned": 37, + "ground_truth": 7, + "hits": 3, + "recall": 0.429, + "best_rank": 1, + "per_file": [ + { + "path": "android/snapshot-helper/src/main/java/com/callstack/agentdevice/snapshothelper/AccessibilityTreeXml.java", + "rank": 31 + }, + { + "path": "packages/kernel/src/snapshot.ts", + "rank": null + }, + { + "path": "packages/platform-android/src/ui-hierarchy-builder.ts", + "rank": null + }, + { + "path": "packages/platform-android/src/ui-hierarchy-node.ts", + "rank": null + }, + { + "path": "packages/platform-android/src/ui-hierarchy.ts", + "rank": 1 + }, + { + "path": "src/daemon/__tests__/response-views.test.ts", + "rank": null + }, + { + "path": "src/daemon/response-views.ts", + "rank": 25 + } + ] + }, + { + "task": "T3", + "verb": "for", + "failed": null, + "ms": 1038, + "bytes": 9748, + "est_tokens": 2437, + "paths_mentioned": 34, + "ground_truth": 5, + "hits": 1, + "recall": 0.2, + "best_rank": 8, + "per_file": [ + { + "path": "packages/platform-android/src/__tests__/input-actions.test.ts", + "rank": null + }, + { + "path": "packages/platform-android/src/__tests__/test-utils/fake-adb.ts", + "rank": null + }, + { + "path": "packages/platform-android/src/input-actions.ts", + "rank": 8 + }, + { + "path": "test/integration/provider-scenarios/android-ime-lifecycle-world.ts", + "rank": null + }, + { + "path": "test/integration/provider-scenarios/android-world.ts", + "rank": null + } + ] + }, + { + "task": "T3", + "verb": "pack-task", + "failed": null, + "ms": 945, + "bytes": 9695, + "est_tokens": 2424, + "paths_mentioned": 8, + "ground_truth": 5, + "hits": 0, + "recall": 0, + "best_rank": null, + "per_file": [ + { + "path": "packages/platform-android/src/__tests__/input-actions.test.ts", + "rank": null + }, + { + "path": "packages/platform-android/src/__tests__/test-utils/fake-adb.ts", + "rank": null + }, + { + "path": "packages/platform-android/src/input-actions.ts", + "rank": null + }, + { + "path": "test/integration/provider-scenarios/android-ime-lifecycle-world.ts", + "rank": null + }, + { + "path": "test/integration/provider-scenarios/android-world.ts", + "rank": null + } + ] + }, + { + "task": "T3", + "verb": "pack-task-4k", + "failed": null, + "ms": 909, + "bytes": 8476, + "est_tokens": 2119, + "paths_mentioned": 7, + "ground_truth": 5, + "hits": 0, + "recall": 0, + "best_rank": null, + "per_file": [ + { + "path": "packages/platform-android/src/__tests__/input-actions.test.ts", + "rank": null + }, + { + "path": "packages/platform-android/src/__tests__/test-utils/fake-adb.ts", + "rank": null + }, + { + "path": "packages/platform-android/src/input-actions.ts", + "rank": null + }, + { + "path": "test/integration/provider-scenarios/android-ime-lifecycle-world.ts", + "rank": null + }, + { + "path": "test/integration/provider-scenarios/android-world.ts", + "rank": null + } + ] + }, + { + "task": "T3", + "verb": "for-idents", + "failed": null, + "ms": 1002, + "bytes": 10718, + "est_tokens": 2680, + "paths_mentioned": 41, + "ground_truth": 5, + "hits": 1, + "recall": 0.2, + "best_rank": 1, + "per_file": [ + { + "path": "packages/platform-android/src/__tests__/input-actions.test.ts", + "rank": null + }, + { + "path": "packages/platform-android/src/__tests__/test-utils/fake-adb.ts", + "rank": null + }, + { + "path": "packages/platform-android/src/input-actions.ts", + "rank": 1 + }, + { + "path": "test/integration/provider-scenarios/android-ime-lifecycle-world.ts", + "rank": null + }, + { + "path": "test/integration/provider-scenarios/android-world.ts", + "rank": null + } + ] + }, + { + "task": "T4", + "verb": "for", + "failed": null, + "ms": 988, + "bytes": 10118, + "est_tokens": 2530, + "paths_mentioned": 41, + "ground_truth": 7, + "hits": 3, + "recall": 0.429, + "best_rank": 1, + "per_file": [ + { + "path": "src/daemon/__tests__/http-server-tenant-trust.test.ts", + "rank": null + }, + { + "path": "src/daemon/__tests__/request-diagnostics-http.test.ts", + "rank": null + }, + { + "path": "src/daemon/request-diagnostics-http.ts", + "rank": 1 + }, + { + "path": "src/daemon/server/http-server.ts", + "rank": 5 + }, + { + "path": "src/daemon/server/tenant-trust.ts", + "rank": 4 + }, + { + "path": "src/daemon/session-tenant-scope.ts", + "rank": null + }, + { + "path": "test/wire-compat/surface.ts", + "rank": null + } + ] + }, + { + "task": "T4", + "verb": "pack-task", + "failed": null, + "ms": 986, + "bytes": 12724, + "est_tokens": 3181, + "paths_mentioned": 19, + "ground_truth": 7, + "hits": 5, + "recall": 0.714, + "best_rank": 1, + "per_file": [ + { + "path": "src/daemon/__tests__/http-server-tenant-trust.test.ts", + "rank": 16 + }, + { + "path": "src/daemon/__tests__/request-diagnostics-http.test.ts", + "rank": 17 + }, + { + "path": "src/daemon/request-diagnostics-http.ts", + "rank": 1 + }, + { + "path": "src/daemon/server/http-server.ts", + "rank": 5 + }, + { + "path": "src/daemon/server/tenant-trust.ts", + "rank": 4 + }, + { + "path": "src/daemon/session-tenant-scope.ts", + "rank": null + }, + { + "path": "test/wire-compat/surface.ts", + "rank": null + } + ] + }, + { + "task": "T4", + "verb": "pack-task-4k", + "failed": null, + "ms": 994, + "bytes": 9044, + "est_tokens": 2261, + "paths_mentioned": 15, + "ground_truth": 7, + "hits": 5, + "recall": 0.714, + "best_rank": 1, + "per_file": [ + { + "path": "src/daemon/__tests__/http-server-tenant-trust.test.ts", + "rank": 12 + }, + { + "path": "src/daemon/__tests__/request-diagnostics-http.test.ts", + "rank": 13 + }, + { + "path": "src/daemon/request-diagnostics-http.ts", + "rank": 1 + }, + { + "path": "src/daemon/server/http-server.ts", + "rank": 5 + }, + { + "path": "src/daemon/server/tenant-trust.ts", + "rank": 4 + }, + { + "path": "src/daemon/session-tenant-scope.ts", + "rank": null + }, + { + "path": "test/wire-compat/surface.ts", + "rank": null + } + ] + }, + { + "task": "T4", + "verb": "for-idents", + "failed": null, + "ms": 952, + "bytes": 9641, + "est_tokens": 2410, + "paths_mentioned": 44, + "ground_truth": 7, + "hits": 2, + "recall": 0.286, + "best_rank": 13, + "per_file": [ + { + "path": "src/daemon/__tests__/http-server-tenant-trust.test.ts", + "rank": null + }, + { + "path": "src/daemon/__tests__/request-diagnostics-http.test.ts", + "rank": null + }, + { + "path": "src/daemon/request-diagnostics-http.ts", + "rank": 17 + }, + { + "path": "src/daemon/server/http-server.ts", + "rank": 13 + }, + { + "path": "src/daemon/server/tenant-trust.ts", + "rank": null + }, + { + "path": "src/daemon/session-tenant-scope.ts", + "rank": null + }, + { + "path": "test/wire-compat/surface.ts", + "rank": null + } + ] + }, + { + "task": "T5", + "verb": "for", + "failed": null, + "ms": 1036, + "bytes": 10059, + "est_tokens": 2515, + "paths_mentioned": 35, + "ground_truth": 4, + "hits": 2, + "recall": 0.5, + "best_rank": 1, + "per_file": [ + { + "path": "src/commands/interaction/runtime/wait-polling.test.ts", + "rank": null + }, + { + "path": "src/commands/interaction/runtime/wait-polling.ts", + "rank": 1 + }, + { + "path": "src/commands/interaction/runtime/wait-selector.test.ts", + "rank": null + }, + { + "path": "src/commands/interaction/runtime/wait-selector.ts", + "rank": 5 + } + ] + }, + { + "task": "T5", + "verb": "pack-task", + "failed": null, + "ms": 997, + "bytes": 13150, + "est_tokens": 3288, + "paths_mentioned": 28, + "ground_truth": 4, + "hits": 3, + "recall": 0.75, + "best_rank": 1, + "per_file": [ + { + "path": "src/commands/interaction/runtime/wait-polling.test.ts", + "rank": null + }, + { + "path": "src/commands/interaction/runtime/wait-polling.ts", + "rank": 1 + }, + { + "path": "src/commands/interaction/runtime/wait-selector.test.ts", + "rank": 18 + }, + { + "path": "src/commands/interaction/runtime/wait-selector.ts", + "rank": 4 + } + ] + }, + { + "task": "T5", + "verb": "pack-task-4k", + "failed": null, + "ms": 946, + "bytes": 9018, + "est_tokens": 2255, + "paths_mentioned": 14, + "ground_truth": 4, + "hits": 3, + "recall": 0.75, + "best_rank": 1, + "per_file": [ + { + "path": "src/commands/interaction/runtime/wait-polling.test.ts", + "rank": null + }, + { + "path": "src/commands/interaction/runtime/wait-polling.ts", + "rank": 1 + }, + { + "path": "src/commands/interaction/runtime/wait-selector.test.ts", + "rank": 12 + }, + { + "path": "src/commands/interaction/runtime/wait-selector.ts", + "rank": 4 + } + ] + }, + { + "task": "T5", + "verb": "for-idents", + "failed": null, + "ms": 1006, + "bytes": 10785, + "est_tokens": 2696, + "paths_mentioned": 39, + "ground_truth": 4, + "hits": 2, + "recall": 0.5, + "best_rank": 3, + "per_file": [ + { + "path": "src/commands/interaction/runtime/wait-polling.test.ts", + "rank": null + }, + { + "path": "src/commands/interaction/runtime/wait-polling.ts", + "rank": 3 + }, + { + "path": "src/commands/interaction/runtime/wait-selector.test.ts", + "rank": null + }, + { + "path": "src/commands/interaction/runtime/wait-selector.ts", + "rank": 11 + } + ] + }, + { + "task": "T6", + "verb": "for", + "failed": null, + "ms": 1136, + "bytes": 10390, + "est_tokens": 2598, + "paths_mentioned": 33, + "ground_truth": 3, + "hits": 0, + "recall": 0, + "best_rank": null, + "per_file": [ + { + "path": "packages/platform-apple/src/snapshot-route.test.ts", + "rank": null + }, + { + "path": "packages/platform-apple/src/snapshot-target.test.ts", + "rank": null + }, + { + "path": "packages/platform-apple/src/snapshot-target.ts", + "rank": null + } + ] + }, + { + "task": "T6", + "verb": "pack-task", + "failed": null, + "ms": 1021, + "bytes": 11137, + "est_tokens": 2784, + "paths_mentioned": 7, + "ground_truth": 3, + "hits": 0, + "recall": 0, + "best_rank": null, + "per_file": [ + { + "path": "packages/platform-apple/src/snapshot-route.test.ts", + "rank": null + }, + { + "path": "packages/platform-apple/src/snapshot-target.test.ts", + "rank": null + }, + { + "path": "packages/platform-apple/src/snapshot-target.ts", + "rank": null + } + ] + }, + { + "task": "T6", + "verb": "pack-task-4k", + "failed": null, + "ms": 1002, + "bytes": 8256, + "est_tokens": 2064, + "paths_mentioned": 3, + "ground_truth": 3, + "hits": 0, + "recall": 0, + "best_rank": null, + "per_file": [ + { + "path": "packages/platform-apple/src/snapshot-route.test.ts", + "rank": null + }, + { + "path": "packages/platform-apple/src/snapshot-target.test.ts", + "rank": null + }, + { + "path": "packages/platform-apple/src/snapshot-target.ts", + "rank": null + } + ] + }, + { + "task": "T6", + "verb": "for-idents", + "failed": null, + "ms": 1024, + "bytes": 10545, + "est_tokens": 2636, + "paths_mentioned": 43, + "ground_truth": 3, + "hits": 1, + "recall": 0.333, + "best_rank": 8, + "per_file": [ + { + "path": "packages/platform-apple/src/snapshot-route.test.ts", + "rank": null + }, + { + "path": "packages/platform-apple/src/snapshot-target.test.ts", + "rank": null + }, + { + "path": "packages/platform-apple/src/snapshot-target.ts", + "rank": 8 + } + ] + } + ] +} diff --git a/scripts/ripwire-eval/score.mjs b/scripts/ripwire-eval/score.mjs index 1b1968d23e..83a9230334 100644 --- a/scripts/ripwire-eval/score.mjs +++ b/scripts/ripwire-eval/score.mjs @@ -8,7 +8,7 @@ // // Usage: node scripts/ripwire-eval/score.mjs --runs= [--out=] -import { readdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -20,6 +20,9 @@ function arg(name, fallback) { } const runsDir = arg('runs'); +// Optional: the directory holding the per-task clones. Given it, each run also reports the byte +// size of the files it opened — a context-cost proxy that does not depend on self-reporting. +const worktrees = arg('worktrees'); const outPath = arg('out', join(here, 'agent-results.json')); if (!runsDir) { console.error('usage: score.mjs --runs= [--out=]'); @@ -73,11 +76,24 @@ const scored = readdirSync(runsDir) tool_uses: run.tool_uses ?? null, duration_ms: run.duration_ms ?? null, files_opened: (run.files_opened ?? []).length, + files_opened_bytes: worktrees ? openedBytes(worktrees, run) : null, missed: [...truth].filter((path) => !predicted.includes(path)), spurious: predicted.filter((path) => !truth.has(path)), }; }); +function openedBytes(root, run) { + let total = 0; + for (const path of run.files_opened ?? []) { + try { + total += statSync(join(root, run.task, path)).size; + } catch { + // A path the agent named that does not resolve in the pinned clone contributes nothing. + } + } + return total; +} + function mean(values) { const usable = values.filter((value) => typeof value === 'number'); return usable.length @@ -97,6 +113,7 @@ for (const arm of new Set(scored.map((entry) => entry.arm))) { tool_uses: mean(rows.map((r) => r.tool_uses)), duration_ms: mean(rows.map((r) => r.duration_ms)), files_opened: mean(rows.map((r) => r.files_opened)), + files_opened_bytes: mean(rows.map((r) => r.files_opened_bytes)), }; } From 614ec109e99adf34b17f60bcc69aaf855c7f9005 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 12:53:13 +0000 Subject: [PATCH 03/10] docs: record the ripwire evaluation result and check in its raw runs 24 paired subagent runs over six replayed PRs, plus two deterministic benches. The arms produce identical change sets in 8 of 12 pairs; ripwire reads 30% fewer source bytes but spends 10% more tokens, because its fixed per-call preamble costs more than the file reads it replaces. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01F7vMn8ehPq3NTPYMfxs7ro --- docs/ripwire-context-tooling-evaluation.md | 175 ++++++++-- scripts/ripwire-eval/README.md | 8 + scripts/ripwire-eval/agent-results.json | 320 +++++++++++++++--- scripts/ripwire-eval/runs/T1-baseline-r1.json | 67 ++++ scripts/ripwire-eval/runs/T1-baseline-r2.json | 76 +++++ scripts/ripwire-eval/runs/T1-ripwire-r1.json | 69 ++++ scripts/ripwire-eval/runs/T1-ripwire-r2.json | 63 ++++ scripts/ripwire-eval/runs/T2-baseline-r1.json | 49 +++ scripts/ripwire-eval/runs/T2-baseline-r2.json | 43 +++ scripts/ripwire-eval/runs/T2-ripwire-r1.json | 41 +++ scripts/ripwire-eval/runs/T2-ripwire-r2.json | 39 +++ scripts/ripwire-eval/runs/T3-baseline-r1.json | 38 +++ scripts/ripwire-eval/runs/T3-baseline-r2.json | 36 ++ scripts/ripwire-eval/runs/T3-ripwire-r1.json | 42 +++ scripts/ripwire-eval/runs/T3-ripwire-r2.json | 42 +++ scripts/ripwire-eval/runs/T4-baseline-r1.json | 41 +++ scripts/ripwire-eval/runs/T4-baseline-r2.json | 42 +++ scripts/ripwire-eval/runs/T4-ripwire-r1.json | 40 +++ scripts/ripwire-eval/runs/T4-ripwire-r2.json | 43 +++ scripts/ripwire-eval/runs/T5-baseline-r1.json | 66 ++++ scripts/ripwire-eval/runs/T5-baseline-r2.json | 50 +++ scripts/ripwire-eval/runs/T5-ripwire-r1.json | 36 ++ scripts/ripwire-eval/runs/T5-ripwire-r2.json | 37 ++ scripts/ripwire-eval/runs/T6-baseline-r1.json | 35 ++ scripts/ripwire-eval/runs/T6-baseline-r2.json | 31 ++ scripts/ripwire-eval/runs/T6-ripwire-r1.json | 31 ++ scripts/ripwire-eval/runs/T6-ripwire-r2.json | 29 ++ 27 files changed, 1501 insertions(+), 88 deletions(-) create mode 100644 scripts/ripwire-eval/runs/T1-baseline-r1.json create mode 100644 scripts/ripwire-eval/runs/T1-baseline-r2.json create mode 100644 scripts/ripwire-eval/runs/T1-ripwire-r1.json create mode 100644 scripts/ripwire-eval/runs/T1-ripwire-r2.json create mode 100644 scripts/ripwire-eval/runs/T2-baseline-r1.json create mode 100644 scripts/ripwire-eval/runs/T2-baseline-r2.json create mode 100644 scripts/ripwire-eval/runs/T2-ripwire-r1.json create mode 100644 scripts/ripwire-eval/runs/T2-ripwire-r2.json create mode 100644 scripts/ripwire-eval/runs/T3-baseline-r1.json create mode 100644 scripts/ripwire-eval/runs/T3-baseline-r2.json create mode 100644 scripts/ripwire-eval/runs/T3-ripwire-r1.json create mode 100644 scripts/ripwire-eval/runs/T3-ripwire-r2.json create mode 100644 scripts/ripwire-eval/runs/T4-baseline-r1.json create mode 100644 scripts/ripwire-eval/runs/T4-baseline-r2.json create mode 100644 scripts/ripwire-eval/runs/T4-ripwire-r1.json create mode 100644 scripts/ripwire-eval/runs/T4-ripwire-r2.json create mode 100644 scripts/ripwire-eval/runs/T5-baseline-r1.json create mode 100644 scripts/ripwire-eval/runs/T5-baseline-r2.json create mode 100644 scripts/ripwire-eval/runs/T5-ripwire-r1.json create mode 100644 scripts/ripwire-eval/runs/T5-ripwire-r2.json create mode 100644 scripts/ripwire-eval/runs/T6-baseline-r1.json create mode 100644 scripts/ripwire-eval/runs/T6-baseline-r2.json create mode 100644 scripts/ripwire-eval/runs/T6-ripwire-r1.json create mode 100644 scripts/ripwire-eval/runs/T6-ripwire-r2.json diff --git a/docs/ripwire-context-tooling-evaluation.md b/docs/ripwire-context-tooling-evaluation.md index c964a122e8..c055636085 100644 --- a/docs/ripwire-context-tooling-evaluation.md +++ b/docs/ripwire-context-tooling-evaluation.md @@ -9,6 +9,29 @@ questions about it from the shell — no API key, no embeddings, no index server document answers is narrower than "is it good": **does handing it to an agent change what that agent produces on this repository, and at what cost?** +## Verdict + +**Not worth adopting repo-wide today. Worth revisiting if its output gets cheaper.** + +Across 24 paired subagent runs on six replayed PRs, an agent given ripwire produced the same change +set as one without it in **8 of 12 pairs**, and the aggregate accuracy difference (+3% F1) is +smaller than the run-to-run variance inside either arm. It did read **30% fewer bytes of source**, +which is the mechanism doing exactly what it claims — but it spent **10% more tokens** doing it, +because the tool's own output is verbose enough to more than repay the file reads it replaces. On +the one change this repository has already documented the answer for (threading a CLI flag), a +maintained routing doc beat the call graph. + +What did show up, and is worth keeping in view: + +1. **It is consistent where grep is lucky.** On the one task with a non-obvious touch point — a + scripted provider fake that throws on unscripted calls — both ripwire runs found it and only one + of two baseline runs did. +2. **The token cost is a fixable implementation detail, not a design limit.** ripwire's + self-documenting preamble is a *fixed* 1.6–3.1 KB per invocation, up to 62% of a small verb's + whole response, re-sent on every call. A terse mode would likely flip the token column. +3. **The cheap deterministic verbs stand on their own.** `--recall` answers from 799 KB of markdown + in 15 KB; `--affected` names the right test files at 2–5 KB when the seed set is narrow. + ## What was measured Three things, two of them with no model in the loop. @@ -21,14 +44,15 @@ Three things, two of them with no model in the loop. The benchmark replays six merged `agent-device` PRs. Each task states the change's intent in prose with no file names; the agent works in a clone pinned at the commit's **parent**, cut with a shallow fetch so the fix commit is unreachable rather than merely off-limits. Ground truth is the -commit's own file list, minus `CHANGELOG.md`, `website/` docs and generated ledgers. +commit's own file list — files it modified plus files it created — minus `CHANGELOG.md`, +`website/` docs and generated ledgers. | Task | Replays | Area | Ground-truth files | | --- | --- | --- | --- | -| T1 | #2366 standalone Maestro `clearState` | `packages/maestro` + daemon adapter + conformance corpus | 16 | -| T2 | #2290 editable-field metadata in digest snapshots | `platform-android` + `kernel` + daemon views + Java helper | 7 | +| T1 | #2366 standalone Maestro `clearState` | `packages/maestro` + daemon adapter + conformance corpus | 17 | +| T2 | #2290 editable-field metadata in digest snapshots | `platform-android` + `kernel` + daemon views + Java helper | 8 | | T3 | #2356 orientation waits for the display to rotate | `platform-android` + provider scenarios | 5 | -| T4 | #2382 plain-session client reads its own failure record | daemon HTTP server + tenant scope | 7 | +| T4 | #2382 plain-session client reads its own failure record | daemon HTTP server + tenant scope | 8 | | T5 | #2344 per-poll timeline in wait timeouts | `src/commands/interaction/runtime` | 4 | | T6 | #2331 detached single-flight Simulator target discovery | `platform-apple` | 3 | @@ -40,6 +64,11 @@ are parsed; the gaps here are 52 `.ad` replay-compat scripts (this project's own data, no call graph to lose) and 8 Kotlin files (the Maestro conformance JVM harness). Nothing load-bearing is dark. +Its one-command quality lens, `--quality-panel`, ranks 105 of this tree's 16,097 function bodies +in 16 KB and 6.3 s — but the head of that list is Swift and Objective-C runner *test* code, not the +TypeScript the 300-line module rule is aimed at. Useful as a lens, not as a gate, which is what its +own documentation says. + For scale: `ripwire .` costs **22.6 KB** (~5.6K tokens) against 33 KB for `README.md` + `AGENTS.md` + `CONTEXT.md` and 63 KB with `docs/agents/` added. `--recall=""` answers from the doc corpus in **15.4 KB** where this repo carries **799 KB across 68 markdown files** — @@ -104,15 +133,88 @@ selects test *files* from source files. They answer different questions. ## 3. Agent A/B: does an agent localize better with it? -Two arms over the same six tasks, two replicates each. Identical briefs — the same prose, the same -pinned clone, the same rules, the same JSON deliverable — differing in exactly one paragraph: +Two arms over the same six tasks, two replicates each — 24 runs. Identical briefs (the same prose, +the same pinned clone, the same rules, the same JSON deliverable) differing in exactly one +paragraph: - **baseline**: Read, Grep, Glob, Bash. No code-intelligence tool on the machine. - **ripwire**: the same tools, plus the binary and its verb table, told to reach for it first. Both arms ran the same model. Each agent returned the change set it predicted; the harness scored -it against the commit. Cost is the subagent's own token spend, tool-call count and wall clock, as -reported by the runtime rather than self-estimated. +it against the commit. Cost is the subagent's own token spend, tool-call count and wall clock as +reported by the runtime, not self-estimated. + +### Results + +Per task, mean of two runs, shown as `baseline → ripwire`: + +| Task | F1 | tool calls | tokens | seconds | +| --- | --- | --- | --- | --- | +| T1 | 0.92 → 0.95 | 59 → 52 | 135K → 146K | 422 → 407 | +| T2 | 0.88 → 0.88 | 52 → 53 | 116K → 137K | 447 → 513 | +| T3 | 0.66 → 0.75 | 30 → 35 | 87K → 109K | 236 → 316 | +| T4 | 0.86 → 0.86 | 40 → 26 | 106K → 110K | 327 → 316 | +| T5 | 0.67 → 0.69 | 36 → 28 | 103K → 108K | 324 → 314 | +| T6 | 0.86 → 0.86 | 24 → 19 | 83K → 83K | 183 → 172 | + +Means over all 12 runs per arm: + +| | baseline | ripwire | delta | +| --- | --- | --- | --- | +| Recall | 0.834 | 0.861 | **+3%** | +| Precision | 0.850 | 0.855 | +1% | +| F1 | 0.807 | 0.830 | **+3%** | +| Source bytes opened | 334 KB | 234 KB | **-30%** | +| Files opened | 26.2 | 20.9 | -20% | +| Tool calls | 40.3 | 35.7 | -12% | +| **Subagent tokens** | 105.2K | 115.4K | **+10%** | +| Wall clock | 323 s | 340 s | +5% | + +Because the runs are paired (same task, same replicate index), the sign counts matter more than the +means at this sample size: + +| Metric | ripwire lower | ripwire higher | identical | +| --- | --- | --- | --- | +| F1 | 1 | 3 | 8 | +| Source bytes opened | 10 | 2 | 0 | +| Tool calls | 7 | 4 | 1 | +| Subagent tokens | 3 | 9 | 0 | +| Wall clock | 4 | 8 | 0 | + +### Reading the result + +**Accuracy is a wash.** F1 moved +3%, and 8 of 12 pairs produced *identical* file sets. On T2, T4 +and T6 all four runs returned exactly the same answer — with and without the tool, twice each. The +differences sit in two places: + +- **T3** is the one task where the arms genuinely separated, and it separated on *consistency*, not + on a ceiling. The change needs `test/integration/provider-scenarios/android-world.ts` edited, + because that scripted fake throws on any unscripted adb call and the fix adds a `dumpsys display` + probe. Both ripwire runs found it. Of the baseline runs, one found it and one did not (2/5 vs + 3/5) — it read `fake-adb.ts` and `android-world.ts` and concluded neither needed a change. +- **T1**, the 17-file Maestro change, produced the matrix's only perfect run — ripwire, 17/17, + including the conformance-corpus and fuzz-arbitrary bookkeeping. Its other three runs, both arms, + all landed 15/17. + +T5 is noise, not signal: recall was 1.00 in all four runs and precision swung 0.40–0.67 *within* +both arms, because every run over-predicted a different set of contract and help files. + +**It does substitute for reading.** 10 of 12 pairs opened less source with ripwire — ~100 KB less +per run on average, 30% by bytes across 20% fewer files. That is the mechanism working as +advertised. + +**And it still cost more tokens.** 9 of 12 pairs spent *more* with ripwire, +10.2K on +average. The saved file bytes did not pay for the tool's own output. Measured directly on this +repository, ripwire's self-documenting XML comment preamble is **1.6 KB on `--for`, 1.7 KB on +`--affected`, and 3.1 KB on `--callers` — 62% of that verb's entire 5.1 KB response**. It is a +*fixed* cost per invocation, so an agent that calls six verbs pays it six times, and it lands +hardest on exactly the cheap, narrow verbs that should be the tool's best value. Whole-file reads +went down; total context did not. + +This is the single most actionable finding here, and it is a fixable one: the preamble is +documentation aimed at a first-time reader, re-sent to an agent that has already read it. A +`--terse` mode that emits the header once per session — or not at all — would likely flip the token +column without touching the ranking. ## 4. The one question this repo has already answered in prose @@ -138,32 +240,7 @@ wrote down, not by call edges: `PROJECT_CONFIG_FLAG_KEYS` is a positive allowlis routing doc stays the better answer to this particular question, and that is the shape of the boundary — ripwire finds what the code *does*, `AGENTS.md` records what the team *decided*. -### Results - -8 baseline runs and 7 ripwire runs across the six tasks (per-run detail in -[`scripts/ripwire-eval/agent-results.json`](../scripts/ripwire-eval/agent-results.json)). - -| | F1 base | F1 ripwire | calls base | calls rw | tokens base | tokens rw | sec base | sec rw | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | -| T1 | 0.94 | 1.00 | 65 | 52 | 132K | 128K | 421 | 360 | -| T2 | 0.88 | 0.88 | 53 | 59 | 114K | 144K | 449 | 540 | -| T3 | 0.66 | 0.75 | 30 | 38 | 87K | 106K | 236 | 331 | -| T4 | 0.86 | 0.86 | 43 | 24 | 107K | 113K | 351 | 283 | -| T5 | 0.73 | 0.57 | 42 | 27 | 112K | 102K | 354 | 297 | -| T6 | 0.86 | 0.86 | 24 | 19 | 83K | 83K | 183 | 172 | - -| Mean over all runs | baseline | ripwire | delta | -| --- | --- | --- | --- | -| Recall | 0.813 | 0.889 | +9% | -| Precision | 0.868 | 0.825 | -5% | -| F1 | 0.804 | 0.824 | +2% | -| Tool calls | 39.1 | 34.0 | -13% | -| Subagent tokens | 101K | 108K | +7% | -| Wall clock (s) | 302 | 308 | +2% | -| Files opened | 25.0 | 19.6 | -22% | -| Bytes of file opened | 326 KB | 197 KB | -39% | - ## Adoption cost, if we wanted it @@ -182,3 +259,35 @@ The cost that is not free is the agent's attention. The verb table given to the MCP server's schemas cost more than the CLI's shell pipe. On a repo whose `AGENTS.md` already spends its budget on a routing table, adding a second routing surface is a real trade. +## Recommendation + +**Do not add ripwire to the repository's agent setup as a default.** The evidence does not support +spending `AGENTS.md` budget on a second routing surface, and the token column is currently +negative. `AGENTS.md` plus `rg` is not the weak baseline this kind of tool is usually measured +against — the declaration-site table, the one-to-one test topology and the typed registries already +do much of the work a call graph would otherwise supply. + +**Do keep it as an individual, opt-in tool.** It is a single Apache-2.0 binary, installs in one +line, sends nothing anywhere and needs no key, so the cost of one engineer trying it is a minute. +The verbs worth trying first here are `--recall` (52× cheaper than the doc corpus it searches) and +`--affected` on a narrow seed set. + +**Re-run this harness if ripwire ships a terse output mode.** `scripts/ripwire-eval/` is written to +be re-run against a new binary with two commands; the token result is the one number most likely to +move, and it is the one currently deciding the verdict. + +**Two findings are worth sending upstream**, since both are measured rather than impressionistic: +the fixed preamble cost per invocation, and `--affected` selecting 185 test files for a 5-file +change once its seeds reach a hub module in `packages/kernel`. + +## Caveats + +- Six tasks, two replicates. Enough to size an effect, not to make a small one significant. The + arms are indistinguishable on half the tasks, which is itself the main result. +- Both arms ran the same model; this measures tooling, not model choice. +- The pinned clones are shallow (20 commits), so ripwire's churn and co-change lenses see a + truncated history. That handicaps ripwire. +- ripwire indexes were warm when the agents ran; the 4.9 s cold index is reported separately rather + than folded into per-run wall clock. +- Change-set localization is one job among many. This says nothing about ripwire's refactoring, + security or quality lenses beyond the single `--quality-panel` run noted above. diff --git a/scripts/ripwire-eval/README.md b/scripts/ripwire-eval/README.md index fca8fa4079..c61d732053 100644 --- a/scripts/ripwire-eval/README.md +++ b/scripts/ripwire-eval/README.md @@ -73,3 +73,11 @@ alongside the token, tool-call and wall-clock cost of producing the answer. history. That handicaps ripwire relative to a full checkout. - ripwire indexes were warm when the agents ran. Cold-index cost is measured and reported separately rather than folded into per-run wall clock. + +## What is checked in here + +- `tasks.json` — the six tasks and their ground truth. +- `runs/` — the 24 raw subagent answers, one file per (task, arm, replicate). +- `agent-results.json`, `retrieval-results.json`, `affected-results.json` — scored output of the + three benches, regenerated by the commands above. Run `pnpm format` after regenerating; the + scripts write plain `JSON.stringify` output and oxfmt owns the checked-in shape. diff --git a/scripts/ripwire-eval/agent-results.json b/scripts/ripwire-eval/agent-results.json index a1281681c3..6a8fa33423 100644 --- a/scripts/ripwire-eval/agent-results.json +++ b/scripts/ripwire-eval/agent-results.json @@ -1,46 +1,46 @@ { - "generated": "2026-09-08T12:44:12.712Z", + "generated": "2026-09-08T12:52:18.925Z", "by_arm": { "baseline": { - "runs": 8, - "recall": 0.813, - "precision": 0.868, - "f1": 0.804, - "subagent_tokens": 100880.875, - "tool_uses": 39.125, - "duration_ms": 301837.875, - "files_opened": 25, - "files_opened_bytes": 325647.125 + "runs": 12, + "recall": 0.834, + "precision": 0.85, + "f1": 0.807, + "subagent_tokens": 105232.833, + "tool_uses": 40.333, + "duration_ms": 323198.25, + "files_opened": 26.167, + "files_opened_bytes": 334431.083 }, "ripwire": { - "runs": 7, - "recall": 0.889, - "precision": 0.825, - "f1": 0.824, - "subagent_tokens": 108398, - "tool_uses": 34, - "duration_ms": 307891.857, - "files_opened": 19.571, - "files_opened_bytes": 197357.286 + "runs": 12, + "recall": 0.861, + "precision": 0.855, + "f1": 0.83, + "subagent_tokens": 115447.917, + "tool_uses": 35.667, + "duration_ms": 339821.833, + "files_opened": 20.917, + "files_opened_bytes": 234439.75 } }, "by_task": { "T1": { "baseline": { "recall": 0.882, - "precision": 1, - "f1": 0.938, - "subagent_tokens": 132056, - "tool_uses": 65, - "duration_ms": 420794 + "precision": 0.969, + "f1": 0.923, + "subagent_tokens": 135275, + "tool_uses": 59, + "duration_ms": 422489.5 }, "ripwire": { - "recall": 1, - "precision": 1, - "f1": 1, - "subagent_tokens": 127795, - "tool_uses": 52, - "duration_ms": 360367 + "recall": 0.941, + "precision": 0.969, + "f1": 0.955, + "subagent_tokens": 145767, + "tool_uses": 52.5, + "duration_ms": 407472 } }, "T2": { @@ -48,17 +48,17 @@ "recall": 0.875, "precision": 0.875, "f1": 0.875, - "subagent_tokens": 114217, - "tool_uses": 53, - "duration_ms": 449460 + "subagent_tokens": 115953, + "tool_uses": 52, + "duration_ms": 446680.5 }, "ripwire": { "recall": 0.875, "precision": 0.875, "f1": 0.875, - "subagent_tokens": 143952, - "tool_uses": 59, - "duration_ms": 539724 + "subagent_tokens": 137133.5, + "tool_uses": 53, + "duration_ms": 512938.5 } }, "T3": { @@ -74,9 +74,9 @@ "recall": 0.6, "precision": 1, "f1": 0.75, - "subagent_tokens": 106192, - "tool_uses": 38, - "duration_ms": 330569 + "subagent_tokens": 109296, + "tool_uses": 35, + "duration_ms": 316329 } }, "T4": { @@ -84,35 +84,35 @@ "recall": 0.75, "precision": 1, "f1": 0.857, - "subagent_tokens": 107179, - "tool_uses": 43, - "duration_ms": 351306 + "subagent_tokens": 106299, + "tool_uses": 39.5, + "duration_ms": 326635.5 }, "ripwire": { "recall": 0.75, "precision": 1, "f1": 0.857, - "subagent_tokens": 112777, - "tool_uses": 24, - "duration_ms": 283230 + "subagent_tokens": 109735.5, + "tool_uses": 26, + "duration_ms": 316027 } }, "T5": { "baseline": { "recall": 1, - "precision": 0.571, - "f1": 0.727, - "subagent_tokens": 112346, - "tool_uses": 42, - "duration_ms": 354297 + "precision": 0.507, + "f1": 0.671, + "subagent_tokens": 103245.5, + "tool_uses": 36.5, + "duration_ms": 323961 }, "ripwire": { "recall": 1, - "precision": 0.4, - "f1": 0.571, - "subagent_tokens": 102356, - "tool_uses": 27, - "duration_ms": 296739 + "precision": 0.534, + "f1": 0.685, + "subagent_tokens": 107898.5, + "tool_uses": 28.5, + "duration_ms": 313857.5 } }, "T6": { @@ -159,6 +159,30 @@ ], "spurious": [] }, + { + "run": "T1-baseline-r2", + "task": "T1", + "arm": "baseline", + "rep": 2, + "ground_truth": 17, + "predicted": 16, + "hit": 15, + "recall": 0.882, + "precision": 0.938, + "f1": 0.909, + "new_files_expected": 1, + "new_files_hit": 0, + "subagent_tokens": 138494, + "tool_uses": 53, + "duration_ms": 424185, + "files_opened": 46, + "files_opened_bytes": 518343, + "missed": [ + "scripts/maestro-conformance/build-manifest.mjs", + "scripts/maestro-conformance/corpus/authored/clear-state.yaml" + ], + "spurious": ["scripts/maestro-conformance/corpus/upstream/044_clear_state.yaml"] + }, { "run": "T1-ripwire-r1", "task": "T1", @@ -180,6 +204,30 @@ "missed": [], "spurious": [] }, + { + "run": "T1-ripwire-r2", + "task": "T1", + "arm": "ripwire", + "rep": 2, + "ground_truth": 17, + "predicted": 16, + "hit": 15, + "recall": 0.882, + "precision": 0.938, + "f1": 0.909, + "new_files_expected": 1, + "new_files_hit": 0, + "subagent_tokens": 163739, + "tool_uses": 53, + "duration_ms": 454577, + "files_opened": 33, + "files_opened_bytes": 332650, + "missed": [ + "scripts/maestro-conformance/build-manifest.mjs", + "scripts/maestro-conformance/corpus/authored/clear-state.yaml" + ], + "spurious": ["scripts/maestro-conformance/corpus/upstream/044_clear_state.yaml"] + }, { "run": "T2-baseline-r1", "task": "T2", @@ -201,6 +249,27 @@ "missed": ["packages/platform-android/src/__tests__/ui-hierarchy-field-metadata.test.ts"], "spurious": ["src/__tests__/android-ui-hierarchy.test.ts"] }, + { + "run": "T2-baseline-r2", + "task": "T2", + "arm": "baseline", + "rep": 2, + "ground_truth": 8, + "predicted": 8, + "hit": 7, + "recall": 0.875, + "precision": 0.875, + "f1": 0.875, + "new_files_expected": 1, + "new_files_hit": 0, + "subagent_tokens": 117689, + "tool_uses": 51, + "duration_ms": 443901, + "files_opened": 21, + "files_opened_bytes": 201881, + "missed": ["packages/platform-android/src/__tests__/ui-hierarchy-field-metadata.test.ts"], + "spurious": ["src/__tests__/android-ui-hierarchy.test.ts"] + }, { "run": "T2-ripwire-r1", "task": "T2", @@ -222,6 +291,27 @@ "missed": ["packages/platform-android/src/__tests__/ui-hierarchy-field-metadata.test.ts"], "spurious": ["src/__tests__/android-ui-hierarchy.test.ts"] }, + { + "run": "T2-ripwire-r2", + "task": "T2", + "arm": "ripwire", + "rep": 2, + "ground_truth": 8, + "predicted": 8, + "hit": 7, + "recall": 0.875, + "precision": 0.875, + "f1": 0.875, + "new_files_expected": 1, + "new_files_hit": 0, + "subagent_tokens": 130315, + "tool_uses": 47, + "duration_ms": 486153, + "files_opened": 17, + "files_opened_bytes": 217570, + "missed": ["packages/platform-android/src/__tests__/ui-hierarchy-field-metadata.test.ts"], + "spurious": ["src/__tests__/android-ui-hierarchy.test.ts"] + }, { "run": "T3-baseline-r1", "task": "T3", @@ -295,6 +385,30 @@ ], "spurious": [] }, + { + "run": "T3-ripwire-r2", + "task": "T3", + "arm": "ripwire", + "rep": 2, + "ground_truth": 5, + "predicted": 3, + "hit": 3, + "recall": 0.6, + "precision": 1, + "f1": 0.75, + "new_files_expected": 0, + "new_files_hit": 0, + "subagent_tokens": 112400, + "tool_uses": 32, + "duration_ms": 302089, + "files_opened": 25, + "files_opened_bytes": 364634, + "missed": [ + "packages/platform-android/src/__tests__/test-utils/fake-adb.ts", + "test/integration/provider-scenarios/android-ime-lifecycle-world.ts" + ], + "spurious": [] + }, { "run": "T4-baseline-r1", "task": "T4", @@ -319,6 +433,30 @@ ], "spurious": [] }, + { + "run": "T4-baseline-r2", + "task": "T4", + "arm": "baseline", + "rep": 2, + "ground_truth": 8, + "predicted": 6, + "hit": 6, + "recall": 0.75, + "precision": 1, + "f1": 0.857, + "new_files_expected": 1, + "new_files_hit": 0, + "subagent_tokens": 105419, + "tool_uses": 36, + "duration_ms": 301965, + "files_opened": 21, + "files_opened_bytes": 263820, + "missed": [ + "test/wire-compat/surface.ts", + "test/integration/provider-scenarios/remote-proxy-request-diagnostics.test.ts" + ], + "spurious": [] + }, { "run": "T4-ripwire-r1", "task": "T4", @@ -343,6 +481,30 @@ ], "spurious": [] }, + { + "run": "T4-ripwire-r2", + "task": "T4", + "arm": "ripwire", + "rep": 2, + "ground_truth": 8, + "predicted": 6, + "hit": 6, + "recall": 0.75, + "precision": 1, + "f1": 0.857, + "new_files_expected": 1, + "new_files_hit": 0, + "subagent_tokens": 106694, + "tool_uses": 28, + "duration_ms": 348824, + "files_opened": 22, + "files_opened_bytes": 232569, + "missed": [ + "test/wire-compat/surface.ts", + "test/integration/provider-scenarios/remote-proxy-request-diagnostics.test.ts" + ], + "spurious": [] + }, { "run": "T5-baseline-r1", "task": "T5", @@ -368,6 +530,33 @@ "src/commands/interaction/runtime/wait-poll-timeline.test.ts" ] }, + { + "run": "T5-baseline-r2", + "task": "T5", + "arm": "baseline", + "rep": 2, + "ground_truth": 4, + "predicted": 9, + "hit": 4, + "recall": 1, + "precision": 0.444, + "f1": 0.615, + "new_files_expected": 0, + "new_files_hit": 0, + "subagent_tokens": 94145, + "tool_uses": 31, + "duration_ms": 293625, + "files_opened": 26, + "files_opened_bytes": 423952, + "missed": [], + "spurious": [ + "packages/contracts/src/wait.ts", + "src/daemon/__tests__/wait-runtime.test.ts", + "src/cli-schema/cli-help.ts", + "src/commands/interaction/runtime/wait-poll-timeline.ts", + "src/commands/interaction/runtime/wait-poll-timeline.test.ts" + ] + }, { "run": "T5-ripwire-r1", "task": "T5", @@ -396,6 +585,27 @@ "src/daemon/handlers/__tests__/snapshot-handler.test.ts" ] }, + { + "run": "T5-ripwire-r2", + "task": "T5", + "arm": "ripwire", + "rep": 2, + "ground_truth": 4, + "predicted": 6, + "hit": 4, + "recall": 1, + "precision": 0.667, + "f1": 0.8, + "new_files_expected": 0, + "new_files_hit": 0, + "subagent_tokens": 113441, + "tool_uses": 30, + "duration_ms": 330976, + "files_opened": 17, + "files_opened_bytes": 284353, + "missed": [], + "spurious": ["packages/contracts/src/wait.ts", "src/daemon/__tests__/wait-runtime.test.ts"] + }, { "run": "T6-baseline-r1", "task": "T6", diff --git a/scripts/ripwire-eval/runs/T1-baseline-r1.json b/scripts/ripwire-eval/runs/T1-baseline-r1.json new file mode 100644 index 0000000000..941aaad602 --- /dev/null +++ b/scripts/ripwire-eval/runs/T1-baseline-r1.json @@ -0,0 +1,67 @@ +{ + "task": "T1", + "arm": "baseline", + "rep": 1, + "files": [ + "packages/maestro/src/internal/program-ir.ts", + "packages/maestro/src/internal/program-ir-command-parser.ts", + "packages/maestro/src/internal/runtime-port-types.ts", + "packages/maestro/src/internal/runtime-port-commands.ts", + "packages/maestro/src/internal/conformance-normalize.ts", + "packages/maestro/src/internal/support-matrix.ts", + "packages/maestro/src/internal/__tests__/runtime-port-fixtures.ts", + "packages/maestro/src/internal/__tests__/program-ir-parser.test.ts", + "src/daemon/adapters/maestro/daemon-runtime-public-operation.ts", + "src/daemon/adapters/maestro/daemon-runtime-port.ts", + "src/daemon/adapters/maestro/__tests__/daemon-runtime-public-operation.test.ts", + "scripts/fuzz/validation-arbitraries-maestro.ts", + "scripts/maestro-conformance/build-manifest.mjs", + "scripts/maestro-conformance/corpus/manifest.json", + "scripts/maestro-conformance/fixtures/layer1-parser.json" + ], + "new_files": ["scripts/maestro-conformance/corpus/authored/clear-state.yaml"], + "files_opened": [ + "AGENTS.md", + "packages/maestro/src/internal/program-ir.ts", + "packages/maestro/src/internal/program-ir-command-parser.ts", + "packages/maestro/src/internal/program-ir-values.ts", + "packages/maestro/src/internal/runtime-port-types.ts", + "packages/maestro/src/internal/runtime-port-commands.ts", + "packages/maestro/src/internal/conformance-normalize.ts", + "packages/maestro/src/internal/support-matrix.ts", + "packages/maestro/src/internal/engine-types.ts", + "packages/maestro/src/internal/export-flow.ts", + "packages/maestro/src/internal/facade-runtime-port.ts", + "packages/maestro/src/internal/progress.ts", + "packages/maestro/src/internal/replay-plan-steps.ts", + "packages/maestro/src/internal/__tests__/runtime-port-fixtures.ts", + "packages/maestro/src/internal/__tests__/runtime-port.test.ts", + "packages/maestro/src/internal/__tests__/program-ir-parser.test.ts", + "packages/maestro/test/conformance/verify.ts", + "packages/maestro/test/conformance/verify.test.ts", + "packages/maestro/test/conformance/expected-divergence.ts", + "packages/maestro/test/conformance/fixture-seal.ts", + "packages/maestro/test/conformance/harness.ts", + "src/daemon/adapters/maestro/daemon-runtime-public-operation.ts", + "src/daemon/adapters/maestro/daemon-runtime-port.ts", + "src/daemon/adapters/maestro/daemon-runtime-port-support.ts", + "src/daemon/adapters/maestro/__tests__/daemon-runtime-public-operation.test.ts", + "src/daemon/adapters/maestro/__tests__/daemon-runtime-port.test.ts", + "src/commands/capture/settings.ts", + "src/cli/parser/__tests__/maestro-support-matrix.test.ts", + "scripts/fuzz/validation-arbitraries-maestro.ts", + "scripts/fuzz/validation-arbitraries-maestro.test.ts", + "scripts/help-conformance-cases.mjs", + "scripts/maestro-conformance/build-manifest.mjs", + "scripts/maestro-conformance/pinned-upstream.json", + "scripts/maestro-conformance/corpus/manifest.json", + "scripts/maestro-conformance/corpus/upstream/043_stop_app.yaml", + "scripts/maestro-conformance/fixtures/layer1-parser.json", + "scripts/maestro-conformance/fixtures/layer2-semantics.json", + "scripts/maestro-conformance/jvm-harness/src/main/kotlin/dev/agentdevice/conformance/Layer1.kt" + ], + "subagent_tokens": 132056, + "tool_uses": 65, + "duration_ms": 420794, + "notes": "Treated stopApp as the structural twin and followed every site naming it, then followed the enforcement chain to the corpus and support-matrix bookkeeping." +} diff --git a/scripts/ripwire-eval/runs/T1-baseline-r2.json b/scripts/ripwire-eval/runs/T1-baseline-r2.json new file mode 100644 index 0000000000..9f63b4cf17 --- /dev/null +++ b/scripts/ripwire-eval/runs/T1-baseline-r2.json @@ -0,0 +1,76 @@ +{ + "task": "T1", + "arm": "baseline", + "rep": 2, + "files": [ + "packages/maestro/src/internal/program-ir.ts", + "packages/maestro/src/internal/program-ir-command-parser.ts", + "packages/maestro/src/internal/runtime-port-types.ts", + "packages/maestro/src/internal/runtime-port-commands.ts", + "packages/maestro/src/internal/conformance-normalize.ts", + "packages/maestro/src/internal/support-matrix.ts", + "packages/maestro/src/internal/__tests__/program-ir-parser.test.ts", + "packages/maestro/src/internal/__tests__/runtime-port-fixtures.ts", + "packages/maestro/src/internal/__tests__/runtime-port.test.ts", + "src/daemon/adapters/maestro/daemon-runtime-public-operation.ts", + "src/daemon/adapters/maestro/daemon-runtime-port.ts", + "src/daemon/adapters/maestro/__tests__/daemon-runtime-public-operation.test.ts", + "src/daemon/adapters/maestro/__tests__/daemon-runtime-port.test.ts", + "scripts/fuzz/validation-arbitraries-maestro.ts", + "scripts/maestro-conformance/corpus/manifest.json", + "scripts/maestro-conformance/fixtures/layer1-parser.json" + ], + "new_files": ["scripts/maestro-conformance/corpus/upstream/044_clear_state.yaml"], + "files_opened": [ + "AGENTS.md", + "packages/maestro/src/index.ts", + "packages/maestro/src/index.test.ts", + "packages/maestro/src/internal/program-ir.ts", + "packages/maestro/src/internal/program-ir-command-parser.ts", + "packages/maestro/src/internal/program-ir-values.ts", + "packages/maestro/src/internal/runtime-port-types.ts", + "packages/maestro/src/internal/runtime-port-commands.ts", + "packages/maestro/src/internal/conformance-normalize.ts", + "packages/maestro/src/internal/export-flow.ts", + "packages/maestro/src/internal/support-matrix.ts", + "packages/maestro/src/internal/engine-types.ts", + "packages/maestro/src/internal/facade-runtime-port.ts", + "packages/maestro/src/internal/compatibility-policy.ts", + "packages/maestro/src/internal/replay-plan-steps.ts", + "packages/maestro/src/internal/__tests__/program-ir-parser.test.ts", + "packages/maestro/src/internal/__tests__/runtime-port-fixtures.ts", + "packages/maestro/src/internal/__tests__/runtime-port.test.ts", + "packages/maestro/test/conformance/verify.ts", + "packages/maestro/test/conformance/expected-divergence.ts", + "packages/maestro/test/conformance/fixture-seal.ts", + "packages/maestro/test/conformance/harness.ts", + "packages/ad-replay/src/internal/runtime-port-types.ts", + "src/daemon/adapters/maestro/daemon-runtime-public-operation.ts", + "src/daemon/adapters/maestro/daemon-runtime-port.ts", + "src/daemon/adapters/maestro/daemon-runtime-port-support.ts", + "src/daemon/adapters/maestro/__tests__/daemon-runtime-public-operation.test.ts", + "src/daemon/adapters/maestro/__tests__/daemon-runtime-port.test.ts", + "src/daemon/adapters/maestro/__tests__/daemon-runtime-port-mutations.test.ts", + "src/daemon/adapters/maestro/__tests__/daemon-runtime-port-fixtures.ts", + "src/daemon/replay/internal/session-replay-maestro-failure.ts", + "src/daemon/handlers/snapshot-settings.ts", + "src/commands/capture/settings.ts", + "src/cli/parser/__tests__/maestro-support-matrix.test.ts", + "scripts/fuzz/validation-arbitraries-maestro.ts", + "scripts/fuzz/validation-arbitraries-maestro.test.ts", + "scripts/check-replay-compat-provenance.ts", + "scripts/maestro-conformance/README.md", + "scripts/maestro-conformance/pinned-upstream.json", + "scripts/maestro-conformance/build-manifest.mjs", + "scripts/maestro-conformance/corpus/manifest.json", + "scripts/maestro-conformance/corpus/upstream/021_launch_app_with_clear_state.yaml", + "scripts/maestro-conformance/corpus/upstream/043_stop_app.yaml", + "scripts/maestro-conformance/corpus/upstream/045_clear_keychain.yaml", + "scripts/maestro-conformance/fixtures/layer1-parser.json", + "scripts/maestro-conformance/fixtures/layer2-semantics.json" + ], + "subagent_tokens": 138494, + "tool_uses": 53, + "duration_ms": 424185, + "notes": "Followed the rejection message to the parser table, used stopApp as a tracer, then chased the gates keyed off SUPPORTED_MAESTRO_COMMAND_NAMES." +} diff --git a/scripts/ripwire-eval/runs/T1-ripwire-r1.json b/scripts/ripwire-eval/runs/T1-ripwire-r1.json new file mode 100644 index 0000000000..5ef9423686 --- /dev/null +++ b/scripts/ripwire-eval/runs/T1-ripwire-r1.json @@ -0,0 +1,69 @@ +{ + "task": "T1", + "arm": "ripwire", + "rep": 1, + "files": [ + "packages/maestro/src/internal/program-ir.ts", + "packages/maestro/src/internal/program-ir-command-parser.ts", + "packages/maestro/src/internal/runtime-port-types.ts", + "packages/maestro/src/internal/runtime-port-commands.ts", + "packages/maestro/src/internal/conformance-normalize.ts", + "packages/maestro/src/internal/support-matrix.ts", + "packages/maestro/src/internal/__tests__/program-ir-parser.test.ts", + "packages/maestro/src/internal/__tests__/runtime-port-fixtures.ts", + "packages/maestro/src/internal/__tests__/runtime-port.test.ts", + "src/daemon/adapters/maestro/daemon-runtime-public-operation.ts", + "src/daemon/adapters/maestro/daemon-runtime-port.ts", + "src/daemon/adapters/maestro/__tests__/daemon-runtime-public-operation.test.ts", + "src/daemon/adapters/maestro/__tests__/daemon-runtime-port.test.ts", + "scripts/fuzz/validation-arbitraries-maestro.ts", + "scripts/maestro-conformance/build-manifest.mjs", + "scripts/maestro-conformance/corpus/manifest.json", + "scripts/maestro-conformance/fixtures/layer1-parser.json" + ], + "new_files": ["scripts/maestro-conformance/corpus/authored/clear-state.yaml"], + "files_opened": [ + "AGENTS.md", + "packages/maestro/src/internal/program-ir.ts", + "packages/maestro/src/internal/program-ir-command-parser.ts", + "packages/maestro/src/internal/program-ir-values.ts", + "packages/maestro/src/internal/runtime-port-types.ts", + "packages/maestro/src/internal/runtime-port-commands.ts", + "packages/maestro/src/internal/engine-types.ts", + "packages/maestro/src/internal/conformance-normalize.ts", + "packages/maestro/src/internal/support-matrix.ts", + "packages/maestro/src/internal/export-flow.ts", + "packages/maestro/src/internal/progress.ts", + "packages/maestro/src/internal/facade-runtime-port.ts", + "packages/maestro/src/internal/compatibility-policy.ts", + "packages/maestro/src/index.ts", + "packages/maestro/src/internal/__tests__/runtime-port-fixtures.ts", + "packages/maestro/src/internal/__tests__/runtime-port.test.ts", + "packages/maestro/src/internal/__tests__/program-ir-parser.test.ts", + "packages/maestro/test/conformance/expected-divergence.ts", + "packages/maestro/test/conformance/verify.ts", + "src/daemon/adapters/maestro/daemon-runtime-port.ts", + "src/daemon/adapters/maestro/daemon-runtime-public-operation.ts", + "src/daemon/adapters/maestro/daemon-runtime-port-support.ts", + "src/daemon/adapters/maestro/__tests__/daemon-runtime-public-operation.test.ts", + "src/daemon/adapters/maestro/__tests__/daemon-runtime-port.test.ts", + "src/cli/parser/__tests__/maestro-support-matrix.test.ts", + "scripts/fuzz/validation-arbitraries-maestro.ts", + "scripts/fuzz/validation-arbitraries-maestro.test.ts", + "scripts/maestro-conformance/README.md", + "scripts/maestro-conformance/build-manifest.mjs", + "scripts/maestro-conformance/fixtures/layer1-parser.json", + "scripts/maestro-conformance/corpus/upstream/021_launch_app_with_clear_state.yaml", + "scripts/maestro-conformance/corpus/upstream/043_stop_app.yaml", + "scripts/maestro-conformance/corpus/upstream/116_kill_app.yaml", + "scripts/maestro-conformance/corpus/authored/repeat.yaml", + "scripts/maestro-conformance/corpus/invalid/unknown-command.yaml", + "test/integration/ios-simulator-e2e/behavior-coverage.ts", + "test/integration/ios-simulator-e2e/live-fixtures.ts", + ".fallowrc.json" + ], + "subagent_tokens": 127795, + "tool_uses": 52, + "duration_ms": 360367, + "notes": "Grepped the rejection string to packages/maestro, then traced stopApp end to end through parser table, IR union, runtime-port tables, daemon projection, and the conformance coverage check." +} diff --git a/scripts/ripwire-eval/runs/T1-ripwire-r2.json b/scripts/ripwire-eval/runs/T1-ripwire-r2.json new file mode 100644 index 0000000000..52528ebfe4 --- /dev/null +++ b/scripts/ripwire-eval/runs/T1-ripwire-r2.json @@ -0,0 +1,63 @@ +{ + "task": "T1", + "arm": "ripwire", + "rep": 2, + "files": [ + "packages/maestro/src/internal/program-ir.ts", + "packages/maestro/src/internal/program-ir-command-parser.ts", + "packages/maestro/src/internal/runtime-port-types.ts", + "packages/maestro/src/internal/runtime-port-commands.ts", + "packages/maestro/src/internal/conformance-normalize.ts", + "packages/maestro/src/internal/support-matrix.ts", + "packages/maestro/src/internal/__tests__/runtime-port-fixtures.ts", + "packages/maestro/src/internal/__tests__/runtime-port.test.ts", + "packages/maestro/src/internal/__tests__/program-ir-parser.test.ts", + "src/daemon/adapters/maestro/daemon-runtime-public-operation.ts", + "src/daemon/adapters/maestro/daemon-runtime-port.ts", + "src/daemon/adapters/maestro/__tests__/daemon-runtime-public-operation.test.ts", + "src/daemon/adapters/maestro/__tests__/daemon-runtime-port.test.ts", + "scripts/fuzz/validation-arbitraries-maestro.ts", + "scripts/maestro-conformance/corpus/manifest.json", + "scripts/maestro-conformance/fixtures/layer1-parser.json" + ], + "new_files": ["scripts/maestro-conformance/corpus/upstream/044_clear_state.yaml"], + "files_opened": [ + "AGENTS.md", + "packages/maestro/src/index.ts", + "packages/maestro/src/index.test.ts", + "packages/maestro/src/internal/support-matrix.ts", + "packages/maestro/src/internal/compatibility-policy.ts", + "packages/maestro/src/internal/program-ir.ts", + "packages/maestro/src/internal/program-ir-command-parser.ts", + "packages/maestro/src/internal/runtime-port-commands.ts", + "packages/maestro/src/internal/runtime-port-types.ts", + "packages/maestro/src/internal/conformance-normalize.ts", + "packages/maestro/src/internal/export-flow.ts", + "packages/maestro/src/internal/progress.ts", + "packages/maestro/src/internal/engine-types.ts", + "packages/maestro/src/internal/__tests__/runtime-port-fixtures.ts", + "packages/maestro/src/internal/__tests__/runtime-port.test.ts", + "packages/maestro/test/conformance/harness.ts", + "packages/maestro/test/conformance/verify.ts", + "packages/maestro/test/conformance/verify.test.ts", + "packages/maestro/test/conformance/expected-divergence.ts", + "scripts/maestro-conformance/README.md", + "scripts/maestro-conformance/build-manifest.mjs", + "scripts/maestro-conformance/corpus/manifest.json", + "scripts/fuzz/validation-arbitraries-maestro.ts", + "scripts/fuzz/validation-arbitraries-maestro.test.ts", + "scripts/fuzz/targets.ts", + "src/daemon/adapters/maestro/daemon-runtime-port.ts", + "src/daemon/adapters/maestro/daemon-runtime-port-support.ts", + "src/daemon/adapters/maestro/daemon-runtime-public-operation.ts", + "src/daemon/adapters/maestro/__tests__/daemon-runtime-port.test.ts", + "src/daemon/adapters/maestro/__tests__/daemon-runtime-port-mutations.test.ts", + "src/daemon/adapters/maestro/__tests__/daemon-runtime-public-operation.test.ts", + "src/daemon/replay/internal/__tests__/session-replay-runtime-maestro.test.ts", + "src/cli/parser/__tests__/maestro-support-matrix.test.ts" + ], + "subagent_tokens": 163739, + "tool_uses": 53, + "duration_ms": 454577, + "notes": "Traced stopApp with ripwire greps from the parser table through IR, dispatch tables and daemon projection, then followed the conformance and fuzz coverage gates." +} diff --git a/scripts/ripwire-eval/runs/T2-baseline-r1.json b/scripts/ripwire-eval/runs/T2-baseline-r1.json new file mode 100644 index 0000000000..f710b197af --- /dev/null +++ b/scripts/ripwire-eval/runs/T2-baseline-r1.json @@ -0,0 +1,49 @@ +{ + "task": "T2", + "arm": "baseline", + "rep": 1, + "files": [ + "android/snapshot-helper/src/main/java/com/callstack/agentdevice/snapshothelper/AccessibilityTreeXml.java", + "packages/platform-android/src/ui-hierarchy.ts", + "packages/platform-android/src/ui-hierarchy-node.ts", + "packages/platform-android/src/ui-hierarchy-builder.ts", + "packages/kernel/src/snapshot.ts", + "src/daemon/response-views.ts", + "src/daemon/__tests__/response-views.test.ts", + "src/__tests__/android-ui-hierarchy.test.ts" + ], + "new_files": [], + "files_opened": [ + "AGENTS.md", + "android/snapshot-helper/src/main/java/com/callstack/agentdevice/snapshothelper/AccessibilityTreeXml.java", + "android/snapshot-helper/src/main/java/com/callstack/agentdevice/snapshothelper/HelperProtocol.java", + "android/snapshot-helper/src/test/java/com/callstack/agentdevice/snapshothelper/SnapshotHelperTestSuite.java", + "packages/kernel/src/snapshot.ts", + "packages/contracts/src/is-predicate.ts", + "packages/contracts/src/snapshot-presentation.ts", + "packages/platform-android/src/ui-hierarchy.ts", + "packages/platform-android/src/ui-hierarchy-node.ts", + "packages/platform-android/src/ui-hierarchy-builder.ts", + "packages/platform-android/src/observation.ts", + "packages/platform-android/src/fill-verification.ts", + "packages/platform-android/src/snapshot-presentation.ts", + "packages/platform-android/src/snapshot-helper-artifact.ts", + "packages/platform-android/src/snapshot-helper-session-protocol.ts", + "packages/platform-android/src/inventory.ts", + "packages/platform-android/src/__tests__/snapshot-helper.test.ts", + "src/daemon/response-views.ts", + "src/daemon/__tests__/response-views.test.ts", + "src/__tests__/android-ui-hierarchy.test.ts", + "src/snapshot/snapshot-lines.ts", + "src/mcp/command-output-schemas.ts", + "src/cli-schema/cli-help.ts", + "test/wire-compat/ledger.json", + "test/output-economy/render-fixtures.ts", + "test/output-economy/selection-fixtures.ts", + "test/output-economy/output-economy.baseline.test.ts" + ], + "subagent_tokens": 114217, + "tool_uses": 53, + "duration_ms": 449460, + "notes": "Grepped the digest response level to the field allowlist, then followed the Android field facts backwards from XML emission through the parse/normalize boundary to the kernel node type." +} diff --git a/scripts/ripwire-eval/runs/T2-baseline-r2.json b/scripts/ripwire-eval/runs/T2-baseline-r2.json new file mode 100644 index 0000000000..618bbc5a2d --- /dev/null +++ b/scripts/ripwire-eval/runs/T2-baseline-r2.json @@ -0,0 +1,43 @@ +{ + "task": "T2", + "arm": "baseline", + "rep": 2, + "files": [ + "android/snapshot-helper/src/main/java/com/callstack/agentdevice/snapshothelper/AccessibilityTreeXml.java", + "packages/kernel/src/snapshot.ts", + "packages/platform-android/src/ui-hierarchy.ts", + "packages/platform-android/src/ui-hierarchy-node.ts", + "packages/platform-android/src/ui-hierarchy-builder.ts", + "src/daemon/response-views.ts", + "src/daemon/__tests__/response-views.test.ts", + "src/__tests__/android-ui-hierarchy.test.ts" + ], + "new_files": [], + "files_opened": [ + "AGENTS.md", + "src/daemon/response-views.ts", + "android/snapshot-helper/src/main/java/com/callstack/agentdevice/snapshothelper/AccessibilityTreeXml.java", + "android/snapshot-helper/src/main/java/com/callstack/agentdevice/snapshothelper/AccessibilityTreeCapture.java", + "android/snapshot-helper/src/test/java/com/callstack/agentdevice/snapshothelper/SnapshotHelperTestSuite.java", + "android/snapshot-helper/src/test/java/com/callstack/agentdevice/snapshothelper/AccessibilityCaptureStabilizerTest.java", + "packages/platform-android/src/ui-hierarchy.ts", + "packages/platform-android/src/ui-hierarchy-node.ts", + "packages/platform-android/src/ui-hierarchy-builder.ts", + "packages/platform-android/src/fill-verification.ts", + "packages/platform-android/src/observation.ts", + "packages/platform-android/src/snapshot-helper-types.ts", + "packages/platform-android/src/__tests__/ui-hierarchy-builder.test.ts", + "packages/kernel/src/snapshot.ts", + "packages/contracts/src/snapshot-presentation.ts", + "src/snapshot/snapshot-presentation/android/predicates.ts", + "src/commands/interaction/output.ts", + "src/mcp/command-output-schemas.ts", + "src/__tests__/android-ui-hierarchy.test.ts", + "test/output-economy/fixtures.ts", + "test/output-economy/render-fixtures.ts" + ], + "subagent_tokens": 117689, + "tool_uses": 51, + "duration_ms": 443901, + "notes": "Grepped the digest level to the single node-field allowlist, then followed the Android facts backwards through the parse/normalize/build boundary to the helper's XML serializer." +} diff --git a/scripts/ripwire-eval/runs/T2-ripwire-r1.json b/scripts/ripwire-eval/runs/T2-ripwire-r1.json new file mode 100644 index 0000000000..2ba4a69e0d --- /dev/null +++ b/scripts/ripwire-eval/runs/T2-ripwire-r1.json @@ -0,0 +1,41 @@ +{ + "task": "T2", + "arm": "ripwire", + "rep": 1, + "files": [ + "android/snapshot-helper/src/main/java/com/callstack/agentdevice/snapshothelper/AccessibilityTreeXml.java", + "packages/platform-android/src/ui-hierarchy.ts", + "packages/platform-android/src/ui-hierarchy-node.ts", + "packages/platform-android/src/ui-hierarchy-builder.ts", + "packages/kernel/src/snapshot.ts", + "src/daemon/response-views.ts", + "src/daemon/__tests__/response-views.test.ts", + "src/__tests__/android-ui-hierarchy.test.ts" + ], + "new_files": [], + "files_opened": [ + "AGENTS.md", + "android/snapshot-helper/README.md", + "android/snapshot-helper/src/main/java/com/callstack/agentdevice/snapshothelper/AccessibilityTreeXml.java", + "android/snapshot-helper/src/test/java/com/callstack/agentdevice/snapshothelper/SnapshotHelperTestSuite.java", + "android/snapshot-helper/src/test/java/com/callstack/agentdevice/snapshothelper/BoundedUiAutomationConnectionTest.java", + "packages/contracts/src/snapshot-presentation.ts", + "packages/kernel/src/snapshot.ts", + "packages/platform-android/src/fill-diagnostics.ts", + "packages/platform-android/src/fill-verification.ts", + "packages/platform-android/src/ui-hierarchy.ts", + "packages/platform-android/src/ui-hierarchy-node.ts", + "packages/platform-android/src/ui-hierarchy-builder.ts", + "packages/platform-android/src/__tests__/ui-hierarchy-builder.test.ts", + "src/__tests__/android-ui-hierarchy.test.ts", + "src/commands/interaction/output.ts", + "src/commands/interaction/runtime/selector-read.ts", + "src/daemon/response-views.ts", + "src/daemon/__tests__/response-views.test.ts", + "src/snapshot/snapshot-presentation/android/helper.ts" + ], + "subagent_tokens": 143952, + "tool_uses": 59, + "duration_ms": 539724, + "notes": "ripwire's ranked lens and literal greps pinned the digest allowlist and the helper XML writer; following the metadata through the normalize boundary showed where the facts are dropped." +} diff --git a/scripts/ripwire-eval/runs/T2-ripwire-r2.json b/scripts/ripwire-eval/runs/T2-ripwire-r2.json new file mode 100644 index 0000000000..6aa4f942c7 --- /dev/null +++ b/scripts/ripwire-eval/runs/T2-ripwire-r2.json @@ -0,0 +1,39 @@ +{ + "task": "T2", + "arm": "ripwire", + "rep": 2, + "files": [ + "android/snapshot-helper/src/main/java/com/callstack/agentdevice/snapshothelper/AccessibilityTreeXml.java", + "packages/platform-android/src/ui-hierarchy.ts", + "packages/platform-android/src/ui-hierarchy-node.ts", + "packages/platform-android/src/ui-hierarchy-builder.ts", + "packages/kernel/src/snapshot.ts", + "src/daemon/response-views.ts", + "src/daemon/__tests__/response-views.test.ts", + "src/__tests__/android-ui-hierarchy.test.ts" + ], + "new_files": [], + "files_opened": [ + "src/daemon/response-views.ts", + "src/daemon/__tests__/response-views.test.ts", + "packages/kernel/src/snapshot.ts", + "packages/platform-android/src/ui-hierarchy.ts", + "packages/platform-android/src/ui-hierarchy-node.ts", + "packages/platform-android/src/ui-hierarchy-builder.ts", + "packages/platform-android/src/snapshot-types.ts", + "packages/platform-android/src/fill-verification.ts", + "packages/platform-android/src/device-input-state.ts", + "packages/platform-android/src/observation.ts", + "android/snapshot-helper/src/main/java/com/callstack/agentdevice/snapshothelper/AccessibilityTreeXml.java", + "android/snapshot-helper/src/test/java/com/callstack/agentdevice/snapshothelper/SnapshotHelperTestSuite.java", + "src/commands/interaction/runtime/selector-read.ts", + "packages/contracts/src/snapshot-presentation.ts", + "src/mcp/command-output-schemas.ts", + "src/snapshot/snapshot-lines.ts", + "src/__tests__/android-ui-hierarchy.test.ts" + ], + "subagent_tokens": 130315, + "tool_uses": 47, + "duration_ms": 486153, + "notes": "--grep on the literal field names showed where they live; --for on the digest concept surfaced selectorReadView; followed the typed field list back to the Java serializer." +} diff --git a/scripts/ripwire-eval/runs/T3-baseline-r1.json b/scripts/ripwire-eval/runs/T3-baseline-r1.json new file mode 100644 index 0000000000..f6ab0c597d --- /dev/null +++ b/scripts/ripwire-eval/runs/T3-baseline-r1.json @@ -0,0 +1,38 @@ +{ + "task": "T3", + "arm": "baseline", + "rep": 1, + "files": [ + "packages/platform-android/src/input-actions.ts", + "packages/platform-android/src/__tests__/input-actions.test.ts" + ], + "new_files": [], + "files_opened": [ + "AGENTS.md", + "packages/platform-android/src/input-actions.ts", + "packages/platform-android/src/__tests__/input-actions.test.ts", + "packages/platform-android/src/__tests__/test-utils/fake-adb.ts", + "packages/platform-android/src/adb.ts", + "packages/platform-android/src/adb-executor.ts", + "packages/platform-android/src/adb-transport.ts", + "packages/platform-android/src/mechanics.ts", + "packages/platform-android/src/window-state.ts", + "packages/contracts/src/device-rotation.ts", + "packages/contracts/src/platform-runtime-operations.ts", + "packages/command-registry/src/registry.ts", + "packages/host-kit/src/retry.ts", + "packages/host-kit/src/internal/retry.ts", + "packages/provider-webdriver/src/webdriver-orientation.ts", + "src/daemon/orientation-runtime.ts", + "src/core/interactors/android.ts", + "src/commands/system/index.ts", + "test/integration/provider-scenarios/android-lifecycle.test.ts", + "test/integration/provider-scenarios/android-world.ts", + "test/integration/android-emulator-e2e/live-harness.ts", + "test/integration/live-device-e2e-failed-step-evidence.test.ts" + ], + "subagent_tokens": 87352, + "tool_uses": 32, + "duration_ms": 242060, + "notes": "Grepped rotation settings to setAndroidOrientation, confirmed via callers and adb executor options that the poll is contained in input-actions.ts." +} diff --git a/scripts/ripwire-eval/runs/T3-baseline-r2.json b/scripts/ripwire-eval/runs/T3-baseline-r2.json new file mode 100644 index 0000000000..7e96ef3cab --- /dev/null +++ b/scripts/ripwire-eval/runs/T3-baseline-r2.json @@ -0,0 +1,36 @@ +{ + "task": "T3", + "arm": "baseline", + "rep": 2, + "files": [ + "packages/platform-android/src/input-actions.ts", + "packages/platform-android/src/__tests__/input-actions.test.ts", + "test/integration/provider-scenarios/android-world.ts" + ], + "new_files": [], + "files_opened": [ + "AGENTS.md", + "packages/platform-android/src/input-actions.ts", + "packages/platform-android/src/__tests__/input-actions.test.ts", + "packages/platform-android/src/__tests__/test-utils/fake-adb.ts", + "packages/platform-android/src/adb.ts", + "packages/platform-android/src/adb-transport.ts", + "packages/platform-android/src/window-state.ts", + "packages/platform-android/src/screenshot.ts", + "packages/platform-android/src/mechanics.ts", + "packages/contracts/src/device-rotation.ts", + "src/core/interactors/android.ts", + "src/daemon/orientation-runtime.ts", + "src/cli-schema/cli-help.ts", + "packages/command-registry/src/registry.ts", + "test/integration/provider-scenarios/android-world.ts", + "test/integration/provider-scenarios/android-lifecycle.test.ts", + "test/integration/provider-scenarios/providers.ts", + "test/integration/live-device-e2e-failed-step-evidence.test.ts", + "test/integration/android-emulator-e2e/live-harness.ts" + ], + "subagent_tokens": 86916, + "tool_uses": 29, + "duration_ms": 230383, + "notes": "Grepped rotation settings to setAndroidOrientation, traced its caller chain, checked which fake-adb harnesses reject an added dumpsys call." +} diff --git a/scripts/ripwire-eval/runs/T3-ripwire-r1.json b/scripts/ripwire-eval/runs/T3-ripwire-r1.json new file mode 100644 index 0000000000..074006f2c7 --- /dev/null +++ b/scripts/ripwire-eval/runs/T3-ripwire-r1.json @@ -0,0 +1,42 @@ +{ + "task": "T3", + "arm": "ripwire", + "rep": 1, + "files": [ + "packages/platform-android/src/input-actions.ts", + "packages/platform-android/src/__tests__/input-actions.test.ts", + "test/integration/provider-scenarios/android-world.ts" + ], + "new_files": [], + "files_opened": [ + "AGENTS.md", + "packages/platform-android/src/input-actions.ts", + "packages/platform-android/src/adb.ts", + "packages/platform-android/src/adb-transport.ts", + "packages/platform-android/src/adb-executor.ts", + "packages/platform-android/src/window-state.ts", + "packages/platform-android/src/screenshot.ts", + "packages/platform-android/src/runtime.ts", + "packages/platform-android/src/runtime.test.ts", + "packages/platform-android/src/__tests__/input-actions.test.ts", + "packages/platform-android/src/__tests__/test-utils/fake-adb.ts", + "packages/contracts/src/orientation-runtime.ts", + "packages/contracts/src/device-rotation.ts", + "packages/host-kit/src/retry.ts", + "packages/host-kit/src/internal/retry.ts", + "src/core/interactors/android.ts", + "src/daemon/orientation-runtime.ts", + "src/commands/system/index.ts", + "test/integration/provider-scenarios/android-world.ts", + "test/integration/provider-scenarios/android-lifecycle.test.ts", + "test/integration/provider-scenarios/providers.ts", + "test/integration/android-emulator-e2e/live-automation-scenario.ts", + "test/integration/android-emulator-e2e/live-harness.ts", + "test/integration/live-device-e2e-failed-step-evidence.test.ts", + "test/integration/replays/android/fixture/02-selector-routes-covered-diagnosis.ad" + ], + "subagent_tokens": 106192, + "tool_uses": 38, + "duration_ms": 330569, + "notes": "ripwire put setAndroidOrientation at rank 1, its callers/affected walk gave the owning unit test, greps exposed the provider-scenario fake that must script the new probe." +} diff --git a/scripts/ripwire-eval/runs/T3-ripwire-r2.json b/scripts/ripwire-eval/runs/T3-ripwire-r2.json new file mode 100644 index 0000000000..3ddef6dd07 --- /dev/null +++ b/scripts/ripwire-eval/runs/T3-ripwire-r2.json @@ -0,0 +1,42 @@ +{ + "task": "T3", + "arm": "ripwire", + "rep": 2, + "files": [ + "packages/platform-android/src/input-actions.ts", + "packages/platform-android/src/__tests__/input-actions.test.ts", + "test/integration/provider-scenarios/android-world.ts" + ], + "new_files": [], + "files_opened": [ + "AGENTS.md", + "packages/platform-android/src/input-actions.ts", + "packages/platform-android/src/adb.ts", + "packages/platform-android/src/adb-transport.ts", + "packages/platform-android/src/settings-airplane.ts", + "packages/platform-android/src/device-input-state.ts", + "packages/platform-android/src/emulator-lifecycle.ts", + "packages/platform-android/src/mechanics.ts", + "packages/platform-android/src/runtime.ts", + "packages/platform-android/src/index.ts", + "packages/platform-android/src/permission-grant-state.ts", + "packages/platform-android/src/__tests__/input-actions.test.ts", + "packages/platform-android/src/__tests__/test-utils/fake-adb.ts", + "packages/platform-android/src/__tests__/settings-permission.test.ts", + "packages/platform-android/src/__tests__/window-state.test.ts", + "packages/contracts/src/orientation-runtime.ts", + "src/daemon/orientation-runtime.ts", + "src/core/interactors/android.ts", + "src/cli-schema/cli-help.ts", + "src/cli-schema/cli-help-topics.test.ts", + "src/__tests__/client-system-commands.test.ts", + "test/integration/provider-scenarios/android-world.ts", + "test/integration/provider-scenarios/android-lifecycle.test.ts", + "test/integration/provider-scenarios/providers.ts", + "test/integration/android-emulator-e2e/live-harness.ts" + ], + "subagent_tokens": 112400, + "tool_uses": 32, + "duration_ms": 302089, + "notes": "ripwire put setAndroidOrientation at rank 1, callers/expand confirmed the single call path, greps showed which unit test and integration world must change." +} diff --git a/scripts/ripwire-eval/runs/T4-baseline-r1.json b/scripts/ripwire-eval/runs/T4-baseline-r1.json new file mode 100644 index 0000000000..a59457ca87 --- /dev/null +++ b/scripts/ripwire-eval/runs/T4-baseline-r1.json @@ -0,0 +1,41 @@ +{ + "task": "T4", + "arm": "baseline", + "rep": 1, + "files": [ + "src/daemon/request-diagnostics-http.ts", + "src/daemon/server/tenant-trust.ts", + "src/daemon/server/http-server.ts", + "src/daemon/session-tenant-scope.ts", + "src/daemon/__tests__/request-diagnostics-http.test.ts", + "src/daemon/__tests__/http-server-tenant-trust.test.ts", + "test/wire-compat/ledger.json" + ], + "new_files": [], + "files_opened": [ + "AGENTS.md", + "src/daemon/request-diagnostics-http.ts", + "src/daemon/session-tenant-scope.ts", + "src/daemon/request-admission.ts", + "src/daemon/server/http-server.ts", + "src/daemon/server/tenant-trust.ts", + "src/daemon/tenant-owned-entry.ts", + "src/daemon/upload-http.ts", + "src/daemon/downloadable-artifact-http.ts", + "src/daemon/__tests__/request-diagnostics-http.test.ts", + "src/daemon/__tests__/http-server-tenant-trust.test.ts", + "src/remote/daemon-proxy.ts", + "src/remote/remote-request-diagnostics.ts", + "src/__tests__/daemon-proxy.test.ts", + "packages/contracts/src/daemon-http.ts", + "test/wire-compat/surface.ts", + "test/wire-compat/ledger.json", + "test/wire-compat/README.md", + "test/wire-compat/declaration-digest.ts", + "test/integration/provider-scenarios/remote-proxy-parity.test.ts" + ], + "subagent_tokens": 107179, + "tool_uses": 43, + "duration_ms": 351306, + "notes": "Grepped from the route path to the diagnostics module, followed the tenant guard back to scopeRequestSession, checked which tests and ledger digests pin current behavior." +} diff --git a/scripts/ripwire-eval/runs/T4-baseline-r2.json b/scripts/ripwire-eval/runs/T4-baseline-r2.json new file mode 100644 index 0000000000..cd76d12481 --- /dev/null +++ b/scripts/ripwire-eval/runs/T4-baseline-r2.json @@ -0,0 +1,42 @@ +{ + "task": "T4", + "arm": "baseline", + "rep": 2, + "files": [ + "src/daemon/request-diagnostics-http.ts", + "src/daemon/server/http-server.ts", + "src/daemon/server/tenant-trust.ts", + "src/daemon/session-tenant-scope.ts", + "src/daemon/__tests__/request-diagnostics-http.test.ts", + "src/daemon/__tests__/http-server-tenant-trust.test.ts", + "test/wire-compat/ledger.json" + ], + "new_files": [], + "files_opened": [ + "AGENTS.md", + "src/daemon/request-diagnostics-http.ts", + "src/daemon/session-tenant-scope.ts", + "src/daemon/request-admission.ts", + "src/daemon/server/tenant-trust.ts", + "src/daemon/server/http-server.ts", + "src/daemon/resumable-upload.ts", + "src/daemon/artifact-tracking.ts", + "src/daemon/__tests__/request-diagnostics-http.test.ts", + "src/daemon/__tests__/http-server-tenant-trust.test.ts", + "src/remote/daemon-proxy.ts", + "src/remote/remote-request-diagnostics.ts", + "packages/contracts/src/daemon-http.ts", + "test/wire-compat/README.md", + "test/wire-compat/surface.ts", + "test/wire-compat/ledger.json", + "test/wire-compat/declaration-digest.ts", + "test/wire-compat/wire-compat.test.ts", + "test/wire-compat/wire-mutations.test.ts", + "test/integration/provider-scenarios/remote-proxy-parity.test.ts", + "test/integration/provider-scenarios/daemon-http-server.test.ts" + ], + "subagent_tokens": 105419, + "tool_uses": 36, + "duration_ms": 301965, + "notes": "Followed isTenantOwnedSessionName from its owning module to the route guard, back up to resolveTrustedTenant, then checked the tests and wire ledger that pin those declarations." +} diff --git a/scripts/ripwire-eval/runs/T4-ripwire-r1.json b/scripts/ripwire-eval/runs/T4-ripwire-r1.json new file mode 100644 index 0000000000..7b7022de0a --- /dev/null +++ b/scripts/ripwire-eval/runs/T4-ripwire-r1.json @@ -0,0 +1,40 @@ +{ + "task": "T4", + "arm": "ripwire", + "rep": 1, + "files": [ + "src/daemon/request-diagnostics-http.ts", + "src/daemon/server/http-server.ts", + "src/daemon/server/tenant-trust.ts", + "src/daemon/session-tenant-scope.ts", + "src/daemon/__tests__/request-diagnostics-http.test.ts", + "src/daemon/__tests__/http-server-tenant-trust.test.ts", + "test/wire-compat/ledger.json" + ], + "new_files": [], + "files_opened": [ + "AGENTS.md", + "src/daemon/request-admission.ts", + "src/daemon/session-tenant-scope.ts", + "src/daemon/request-diagnostics-http.ts", + "src/daemon/server/http-server.ts", + "src/daemon/server/tenant-trust.ts", + "src/daemon/upload-http.ts", + "src/daemon/downloadable-artifact-http.ts", + "src/daemon/__tests__/request-diagnostics-http.test.ts", + "src/daemon/__tests__/http-server-tenant-trust.test.ts", + "src/remote/daemon-proxy.ts", + "src/remote/remote-request-diagnostics.ts", + "src/remote/__tests__/remote-request-diagnostics.test.ts", + "src/__tests__/cli-remote-diagnostics.test.ts", + "test/wire-compat/surface.ts", + "test/wire-compat/ledger.json", + "test/wire-compat/README.md", + "test/wire-compat/declaration-digest.ts", + "test/wire-compat/wire-mutations.test.ts" + ], + "subagent_tokens": 112777, + "tool_uses": 24, + "duration_ms": 283230, + "notes": "--for on the tenant/session-prefix concept surfaced the scope module and route; --uses bounded the blast radius to the aux-auth path and its two test files." +} diff --git a/scripts/ripwire-eval/runs/T4-ripwire-r2.json b/scripts/ripwire-eval/runs/T4-ripwire-r2.json new file mode 100644 index 0000000000..69f54fa190 --- /dev/null +++ b/scripts/ripwire-eval/runs/T4-ripwire-r2.json @@ -0,0 +1,43 @@ +{ + "task": "T4", + "arm": "ripwire", + "rep": 2, + "files": [ + "src/daemon/request-diagnostics-http.ts", + "src/daemon/server/tenant-trust.ts", + "src/daemon/server/http-server.ts", + "src/daemon/session-tenant-scope.ts", + "src/daemon/__tests__/request-diagnostics-http.test.ts", + "src/daemon/__tests__/http-server-tenant-trust.test.ts", + "test/wire-compat/ledger.json" + ], + "new_files": [], + "files_opened": [ + "AGENTS.md", + "src/daemon/request-diagnostics-http.ts", + "src/daemon/server/tenant-trust.ts", + "src/daemon/server/http-server.ts", + "src/daemon/session-tenant-scope.ts", + "src/daemon/session-paths.ts", + "src/daemon/config.ts", + "src/daemon/request-admission.ts", + "src/daemon/upload-http.ts", + "src/daemon/downloadable-artifact-http.ts", + "src/daemon/__tests__/request-diagnostics-http.test.ts", + "src/daemon/__tests__/http-server-tenant-trust.test.ts", + "src/remote/daemon-proxy.ts", + "src/__tests__/daemon-proxy.test.ts", + "src/__tests__/cli-remote-diagnostics.test.ts", + "packages/contracts/src/daemon-http.ts", + "test/wire-compat/surface.ts", + "test/wire-compat/ledger.json", + "test/wire-compat/README.md", + "test/wire-compat/closure-policy.ts", + "test/wire-compat/wire-mutations.test.ts", + "test/integration/provider-scenarios/remote-proxy-artifact-tenant.test.ts" + ], + "subagent_tokens": 106694, + "tool_uses": 28, + "duration_ms": 348824, + "notes": "--for/--pack-task put the route, its authorizer and the tenant-trust decision on one page; traced auth.tenantId back to the attested-vs-declared collapse." +} diff --git a/scripts/ripwire-eval/runs/T5-baseline-r1.json b/scripts/ripwire-eval/runs/T5-baseline-r1.json new file mode 100644 index 0000000000..27d0ecf548 --- /dev/null +++ b/scripts/ripwire-eval/runs/T5-baseline-r1.json @@ -0,0 +1,66 @@ +{ + "task": "T5", + "arm": "baseline", + "rep": 1, + "files": [ + "packages/contracts/src/wait.ts", + "src/commands/interaction/runtime/wait-polling.ts", + "src/commands/interaction/runtime/wait-polling.test.ts", + "src/commands/interaction/runtime/wait-selector.ts", + "src/commands/interaction/runtime/wait-selector.test.ts" + ], + "new_files": [ + "src/commands/interaction/runtime/wait-poll-timeline.ts", + "src/commands/interaction/runtime/wait-poll-timeline.test.ts" + ], + "files_opened": [ + "AGENTS.md", + "CHANGELOG.md", + "docs/agents/selector-capture.md", + "packages/contracts/package.json", + "packages/contracts/src/wait.ts", + "packages/contracts/src/replay.ts", + "packages/contracts/src/facades/replay.ts", + "packages/kernel/src/errors.ts", + "packages/kernel/src/redaction.ts", + "src/cli-schema/cli-help.ts", + "src/cli-schema/cli-help-topics.test.ts", + "src/commands/interaction/runtime/wait-polling.ts", + "src/commands/interaction/runtime/wait-polling.test.ts", + "src/commands/interaction/runtime/wait-selector.ts", + "src/commands/interaction/runtime/wait-selector.test.ts", + "src/commands/interaction/runtime/wait-absent.ts", + "src/commands/interaction/runtime/wait-text.ts", + "src/commands/interaction/runtime/wait-stable.ts", + "src/commands/interaction/runtime/wait-stable.test.ts", + "src/commands/interaction/runtime/selector-read.ts", + "src/commands/interaction/runtime/selector-read.test.ts", + "src/commands/interaction/runtime/selector-wait.ts", + "src/commands/output/error.ts", + "src/core/selector-pipeline.ts", + "src/core/selector-pipeline-policy.ts", + "src/daemon/wait-runtime.ts", + "src/daemon/wait-current-surface.ts", + "src/daemon/wait-current-surface.test.ts", + "src/daemon/response.ts", + "src/daemon/daemon-request.ts", + "src/daemon/__tests__/wait-runtime.test.ts", + "src/daemon/handlers/__tests__/wait-landmark-recording.test.ts", + "src/daemon/replay/internal/session-replay-target-verification.ts", + "src/daemon/replay/internal/__tests__/session-replay-target-verification-runtime.test.ts", + "test/wire-compat/closure-policy.ts", + "test/output-economy/fixtures.ts", + "test/output-economy/selection-fixtures.ts", + "test/output-economy/render-fixtures.ts", + "test/output-economy/routine-workflow.ts", + "test/output-economy/output-economy.baseline.json", + "test/output-economy/output-economy.baseline.test.ts", + "test/integration/ios-simulator-e2e/live-assertions.ts", + "test/integration/ios-simulator-e2e-visibility-scroll.test.ts", + "website/docs/docs/commands.md" + ], + "subagent_tokens": 112346, + "tool_uses": 42, + "duration_ms": 354297, + "notes": "Grepped wait_capture_stalled/readableCaptures to the contract and polling module, followed callers to the one refusal that bypasses waitTimeoutError." +} diff --git a/scripts/ripwire-eval/runs/T5-baseline-r2.json b/scripts/ripwire-eval/runs/T5-baseline-r2.json new file mode 100644 index 0000000000..cbe395382f --- /dev/null +++ b/scripts/ripwire-eval/runs/T5-baseline-r2.json @@ -0,0 +1,50 @@ +{ + "task": "T5", + "arm": "baseline", + "rep": 2, + "files": [ + "src/commands/interaction/runtime/wait-polling.ts", + "src/commands/interaction/runtime/wait-selector.ts", + "packages/contracts/src/wait.ts", + "src/commands/interaction/runtime/wait-polling.test.ts", + "src/commands/interaction/runtime/wait-selector.test.ts", + "src/daemon/__tests__/wait-runtime.test.ts", + "src/cli-schema/cli-help.ts" + ], + "new_files": [ + "src/commands/interaction/runtime/wait-poll-timeline.ts", + "src/commands/interaction/runtime/wait-poll-timeline.test.ts" + ], + "files_opened": [ + "AGENTS.md", + "docs/agents/selector-capture.md", + "packages/contracts/src/wait.ts", + "packages/contracts/src/replay.ts", + "packages/contracts/src/snapshot-timeout-evidence.ts", + "packages/contracts/src/wait-runtime-plan.ts", + "packages/kernel/src/errors.ts", + "src/commands/interaction/runtime/wait-polling.ts", + "src/commands/interaction/runtime/wait-polling.test.ts", + "src/commands/interaction/runtime/wait-selector.ts", + "src/commands/interaction/runtime/wait-selector.test.ts", + "src/commands/interaction/runtime/wait-absent.ts", + "src/commands/interaction/runtime/wait-text.ts", + "src/commands/interaction/runtime/wait-stable.ts", + "src/commands/interaction/runtime/wait-deadline.ts", + "src/commands/interaction/runtime/selector-wait.ts", + "src/commands/interaction/runtime/selector-read.ts", + "src/core/selector-pipeline.ts", + "src/core/selector-pipeline-policy.ts", + "src/daemon/wait-runtime.ts", + "src/daemon/wait-current-surface.ts", + "src/daemon/__tests__/wait-runtime.test.ts", + "src/cli-schema/cli-help.ts", + "src/cli-schema/cli-help-topics.test.ts", + "fallow-baselines/health.json", + "website/docs/docs/commands.md" + ], + "subagent_tokens": 94145, + "tool_uses": 31, + "duration_ms": 293625, + "notes": "Grepped the wait_capture_stalled reason to the contract, then followed createWaitPolling to the single failure-evidence assembly point and the one throw that skips it." +} diff --git a/scripts/ripwire-eval/runs/T5-ripwire-r1.json b/scripts/ripwire-eval/runs/T5-ripwire-r1.json new file mode 100644 index 0000000000..1d3b9c1e12 --- /dev/null +++ b/scripts/ripwire-eval/runs/T5-ripwire-r1.json @@ -0,0 +1,36 @@ +{ + "task": "T5", + "arm": "ripwire", + "rep": 1, + "files": [ + "src/commands/interaction/runtime/wait-polling.ts", + "src/commands/interaction/runtime/wait-selector.ts", + "packages/contracts/src/wait.ts", + "src/cli-schema/cli-help.ts", + "src/commands/interaction/runtime/wait-polling.test.ts", + "src/commands/interaction/runtime/wait-selector.test.ts", + "src/commands/interaction/runtime/wait-absent.test.ts", + "src/commands/interaction/runtime/selector-read.test.ts", + "src/daemon/__tests__/wait-runtime.test.ts", + "src/daemon/handlers/__tests__/snapshot-handler.test.ts" + ], + "new_files": [], + "files_opened": [ + "AGENTS.md", + "src/commands/interaction/runtime/wait-polling.ts", + "src/commands/interaction/runtime/wait-selector.ts", + "src/commands/interaction/runtime/wait-absent.ts", + "src/commands/interaction/runtime/wait-polling.test.ts", + "packages/contracts/src/wait.ts", + "packages/kernel/src/errors.ts", + "src/daemon/wait-current-surface.ts", + "src/daemon/selector-recording.ts", + "src/daemon/replay/internal/session-replay-dispatch-narrowing.ts", + "src/cli-schema/cli-help.ts", + "src/mcp/command-output-schemas.ts" + ], + "subagent_tokens": 102356, + "tool_uses": 27, + "duration_ms": 296739, + "notes": "--for landed on wait-polling.ts/wait-selector.ts, --callers bounded the fan-out to four wait loops, greps pinned contract/help/tests." +} diff --git a/scripts/ripwire-eval/runs/T5-ripwire-r2.json b/scripts/ripwire-eval/runs/T5-ripwire-r2.json new file mode 100644 index 0000000000..cc112cd77c --- /dev/null +++ b/scripts/ripwire-eval/runs/T5-ripwire-r2.json @@ -0,0 +1,37 @@ +{ + "task": "T5", + "arm": "ripwire", + "rep": 2, + "files": [ + "src/commands/interaction/runtime/wait-polling.ts", + "src/commands/interaction/runtime/wait-selector.ts", + "packages/contracts/src/wait.ts", + "src/commands/interaction/runtime/wait-polling.test.ts", + "src/commands/interaction/runtime/wait-selector.test.ts", + "src/daemon/__tests__/wait-runtime.test.ts" + ], + "new_files": [], + "files_opened": [ + "AGENTS.md", + "src/commands/interaction/runtime/wait-polling.ts", + "src/commands/interaction/runtime/wait-selector.ts", + "src/commands/interaction/runtime/wait-absent.ts", + "src/commands/interaction/runtime/wait-text.ts", + "src/commands/interaction/runtime/wait-polling.test.ts", + "packages/contracts/src/wait.ts", + "packages/contracts/src/replay.ts", + "packages/contracts/src/snapshot-timeout-evidence.ts", + "packages/contracts/package.json", + "packages/kernel/src/errors.ts", + "src/cli-schema/cli-help.ts", + "src/cli-schema/cli-help-topics.test.ts", + "src/daemon/wait-runtime.ts", + "src/daemon/wait-current-surface.ts", + "src/daemon/replay/internal/session-replay-target-verification.ts", + "src/daemon/__tests__/wait-runtime.test.ts" + ], + "subagent_tokens": 113441, + "tool_uses": 30, + "duration_ms": 330976, + "notes": "--grep on the literal detail keys pinned createWaitPolling as the sole owner of failure evidence; --uses proved every wait loop funnels through it and found the one throw that bypasses it." +} diff --git a/scripts/ripwire-eval/runs/T6-baseline-r1.json b/scripts/ripwire-eval/runs/T6-baseline-r1.json new file mode 100644 index 0000000000..ae30ae5afe --- /dev/null +++ b/scripts/ripwire-eval/runs/T6-baseline-r1.json @@ -0,0 +1,35 @@ +{ + "task": "T6", + "arm": "baseline", + "rep": 1, + "files": [ + "packages/platform-apple/src/snapshot-target.ts", + "packages/platform-apple/src/snapshot-target.test.ts", + "packages/platform-apple/src/snapshot-route.ts", + "packages/platform-apple/src/snapshot-route.test.ts" + ], + "new_files": [], + "files_opened": [ + "AGENTS.md", + "packages/platform-apple/src/snapshot-target.ts", + "packages/platform-apple/src/snapshot-target.test.ts", + "packages/platform-apple/src/snapshot-route.ts", + "packages/platform-apple/src/snapshot-route.test.ts", + "packages/platform-apple/src/snapshot-process.ts", + "packages/platform-apple/src/core/app-resolution.ts", + "packages/platform-apple/src/core/apps-simctl.ts", + "packages/platform-apple/src/runtime.ts", + "packages/platform-apple/src/runtime-snapshot.ts", + "packages/platform-apple/src/snapshot-source/adapter.ts", + "packages/platform-apple/src/snapshot-source/deadline.ts", + "packages/platform-apple/src/snapshot-source/limits.ts", + "packages/host-kit/src/retry.ts", + "packages/host-kit/src/internal/retry.ts", + "packages/host-kit/package.json", + "packages/kernel/src/errors.ts" + ], + "subagent_tokens": 91881, + "tool_uses": 28, + "duration_ms": 200993, + "notes": "Grepped for the launchctl spawn, followed createSimulatorSnapshotTargetResolver to its single consumer, confirmed by grep nothing else references the reason strings." +} diff --git a/scripts/ripwire-eval/runs/T6-baseline-r2.json b/scripts/ripwire-eval/runs/T6-baseline-r2.json new file mode 100644 index 0000000000..d89b49c9df --- /dev/null +++ b/scripts/ripwire-eval/runs/T6-baseline-r2.json @@ -0,0 +1,31 @@ +{ + "task": "T6", + "arm": "baseline", + "rep": 2, + "files": [ + "packages/platform-apple/src/snapshot-target.ts", + "packages/platform-apple/src/snapshot-target.test.ts", + "packages/platform-apple/src/snapshot-route.ts", + "packages/platform-apple/src/snapshot-route.test.ts" + ], + "new_files": [], + "files_opened": [ + "AGENTS.md", + "packages/platform-apple/src/core/app-resolution.ts", + "packages/platform-apple/src/snapshot-target.ts", + "packages/platform-apple/src/snapshot-route.ts", + "packages/platform-apple/src/snapshot-target.test.ts", + "packages/platform-apple/src/snapshot-process.ts", + "packages/platform-apple/src/snapshot-route.test.ts", + "packages/platform-apple/src/core/apps-simctl.ts", + "packages/platform-apple/src/core/tool-provider.ts", + "packages/kernel/src/keyed-lock.ts", + "packages/kernel/src/errors.ts", + "scripts/ios-snapshot-benchmark/cell-admission.ts", + "scripts/ios-snapshot-benchmark/lifecycle.ts" + ], + "subagent_tokens": 75100, + "tool_uses": 21, + "duration_ms": 165410, + "notes": "Grepped launchctl to the resolver and its single production caller, confirmed by repo-wide grep the reason strings have no other consumers." +} diff --git a/scripts/ripwire-eval/runs/T6-ripwire-r1.json b/scripts/ripwire-eval/runs/T6-ripwire-r1.json new file mode 100644 index 0000000000..514d4418b8 --- /dev/null +++ b/scripts/ripwire-eval/runs/T6-ripwire-r1.json @@ -0,0 +1,31 @@ +{ + "task": "T6", + "arm": "ripwire", + "rep": 1, + "files": [ + "packages/platform-apple/src/snapshot-target.ts", + "packages/platform-apple/src/snapshot-route.ts", + "packages/platform-apple/src/snapshot-target.test.ts", + "packages/platform-apple/src/snapshot-route.test.ts" + ], + "new_files": [], + "files_opened": [ + "AGENTS.md", + "packages/platform-apple/src/snapshot-target.ts", + "packages/platform-apple/src/snapshot-route.ts", + "packages/platform-apple/src/snapshot-process.ts", + "packages/platform-apple/src/snapshot-source/deadline.ts", + "packages/platform-apple/src/snapshot-source/limits.ts", + "packages/platform-apple/src/snapshot-target.test.ts", + "packages/platform-apple/src/snapshot-route.test.ts", + "packages/platform-apple/src/core/config.ts", + "packages/platform-apple/src/index.ts", + "packages/host-kit/src/retry.ts", + "docs/agents/selector-capture.md", + "oxlint.config.ts" + ], + "subagent_tokens": 84840, + "tool_uses": 16, + "duration_ms": 136165, + "notes": "ripwire's conceptual lens surfaced resolveSimulatorSnapshotTarget in snapshot-target.ts, --impact showed its only non-test reach is snapshot-route.ts, greps confirmed nothing else observes the behavior." +} diff --git a/scripts/ripwire-eval/runs/T6-ripwire-r2.json b/scripts/ripwire-eval/runs/T6-ripwire-r2.json new file mode 100644 index 0000000000..cc62b6458c --- /dev/null +++ b/scripts/ripwire-eval/runs/T6-ripwire-r2.json @@ -0,0 +1,29 @@ +{ + "task": "T6", + "arm": "ripwire", + "rep": 2, + "files": [ + "packages/platform-apple/src/snapshot-target.ts", + "packages/platform-apple/src/snapshot-target.test.ts", + "packages/platform-apple/src/snapshot-route.ts", + "packages/platform-apple/src/snapshot-route.test.ts" + ], + "new_files": [], + "files_opened": [ + "AGENTS.md", + "packages/platform-apple/src/snapshot-target.ts", + "packages/platform-apple/src/snapshot-target.test.ts", + "packages/platform-apple/src/snapshot-route.ts", + "packages/platform-apple/src/snapshot-route.test.ts", + "packages/platform-apple/src/snapshot-process.ts", + "packages/platform-apple/src/snapshot-source/deadline.ts", + "packages/platform-apple/src/snapshot-source/limits.ts", + "packages/platform-apple/src/core/apps-simctl.ts", + "packages/platform-apple/src/runtime.ts", + "packages/host-kit/src/retry.ts" + ], + "subagent_tokens": 80874, + "tool_uses": 22, + "duration_ms": 208449, + "notes": "--grep on 'launchctl list' plus a conceptual --for led to the two modules; --uses proved a single consumer and --affected confirmed the two mirrored test files." +} From 3ce4c05b36bfc26235f8c879561c83d683047bcb Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 13:00:32 +0000 Subject: [PATCH 04/10] refactor(eval): give the ripwire benches one shared CLI and runner The fallow audit found the three benches carrying three copies of the flag reader and the ripwire invocation, and score.mjs assembling accuracy and cost in one 41-line arrow. Extract bench-cli.mjs for the flags, the tasks file and the timed run, and split scoring into accuracy and cost. Byte-for-byte the same results; only measured wall clock moves. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01F7vMn8ehPq3NTPYMfxs7ro --- scripts/ripwire-eval/README.md | 5 +- scripts/ripwire-eval/affected-bench.mjs | 64 ++---- scripts/ripwire-eval/affected-results.json | 14 +- scripts/ripwire-eval/agent-results.json | 50 +++-- scripts/ripwire-eval/bench-cli.mjs | 77 +++++++ scripts/ripwire-eval/retrieval-bench.mjs | 59 ++---- scripts/ripwire-eval/retrieval-results.json | 50 ++--- scripts/ripwire-eval/score.mjs | 213 +++++++++++--------- 8 files changed, 309 insertions(+), 223 deletions(-) create mode 100644 scripts/ripwire-eval/bench-cli.mjs diff --git a/scripts/ripwire-eval/README.md b/scripts/ripwire-eval/README.md index c61d732053..6506a52d41 100644 --- a/scripts/ripwire-eval/README.md +++ b/scripts/ripwire-eval/README.md @@ -59,9 +59,12 @@ Agent half — two arms over the same six tasks, identical prompts except the to 4. Score them: ```sh -node scripts/ripwire-eval/score.mjs --runs= +node scripts/ripwire-eval/score.mjs --runs= --worktrees=/tmp/rw ``` +`--worktrees` is optional; with it, each run also reports the byte size of the files it opened, +measured from the pinned clone rather than taken from the agent's own account. + `score.mjs` reports per-run and per-arm file-level recall, precision and F1 against ground truth, alongside the token, tool-call and wall-clock cost of producing the answer. diff --git a/scripts/ripwire-eval/affected-bench.mjs b/scripts/ripwire-eval/affected-bench.mjs index f331b400d5..a671913a3f 100644 --- a/scripts/ripwire-eval/affected-bench.mjs +++ b/scripts/ripwire-eval/affected-bench.mjs @@ -8,34 +8,21 @@ // // Usage: node scripts/ripwire-eval/affected-bench.mjs --ripwire= --worktrees= [--out=] -import { execFile } from 'node:child_process'; -import { readFileSync, writeFileSync } from 'node:fs'; -import { dirname, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { promisify } from 'node:util'; +import { writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { harnessDir, loadTasks, readArgs, runRipwire } from './bench-cli.mjs'; -const run = promisify(execFile); -const here = dirname(fileURLToPath(import.meta.url)); - -function arg(name, fallback) { - const hit = process.argv.find((entry) => entry.startsWith(`--${name}=`)); - return hit ? hit.slice(name.length + 3) : fallback; -} - -const ripwire = arg('ripwire'); -const worktrees = arg('worktrees'); -const outPath = arg('out', join(here, 'affected-results.json')); -if (!ripwire || !worktrees) { - console.error('usage: affected-bench.mjs --ripwire= --worktrees= [--out=]'); - process.exit(2); -} +const { ripwire, worktrees, out } = readArgs({ + usage: 'affected-bench.mjs --ripwire= --worktrees= [--out=]', + required: ['ripwire', 'worktrees'], + optional: { out: join(harnessDir, 'affected-results.json') }, +}); const isTest = (path) => /(\.test\.[cm]?[jt]sx?$)|(^|\/)__tests__\//.test(path); -const tasks = JSON.parse(readFileSync(join(here, 'tasks.json'), 'utf8')).tasks; const results = []; -for (const task of tasks) { +for (const task of loadTasks()) { const added = new Set(task.added_files ?? []); const truth = [...task.ground_truth, ...added]; const sources = truth.filter((path) => !isTest(path) && /\.[cm]?[jt]s$/.test(path)); @@ -48,27 +35,18 @@ for (const task of tasks) { continue; } - const started = process.hrtime.bigint(); - let stdout = ''; - let failed = null; - try { - ({ stdout } = await run(ripwire, ['.', `--affected=${sources.join(',')}`], { - cwd: join(worktrees, task.id), - maxBuffer: 32 * 1024 * 1024, - })); - } catch (error) { - failed = String(error?.message ?? error).slice(0, 200); - stdout = String(error?.stdout ?? ''); - } - const ms = Number(process.hrtime.bigint() - started) / 1e6; - - const selected = [...stdout.matchAll(/ match[1]); + const call = await runRipwire( + ripwire, + ['.', `--affected=${sources.join(',')}`], + join(worktrees, task.id), + ); + const selected = [...call.stdout.matchAll(/ match[1]); const hit = expected.filter((path) => selected.includes(path)); results.push({ task: task.id, - failed, - ms: Math.round(ms), - bytes: Buffer.byteLength(stdout), + failed: call.failed, + ms: call.ms, + bytes: call.bytes, seeds: sources.length, selected: selected.length, expected: expected.length, @@ -80,12 +58,12 @@ for (const task of tasks) { missed: expected.filter((path) => !selected.includes(path)), }); process.stderr.write( - `${task.id}: ${hit.length}/${expected.length} expected tests inside ${selected.length} selected, ${Buffer.byteLength(stdout)} B\n`, + `${task.id}: ${hit.length}/${expected.length} expected tests inside ${selected.length} selected, ${call.bytes} B\n`, ); } writeFileSync( - outPath, + out, `${JSON.stringify({ generated: new Date().toISOString(), results }, null, 2)}\n`, ); -console.log(outPath); +console.log(out); diff --git a/scripts/ripwire-eval/affected-results.json b/scripts/ripwire-eval/affected-results.json index 5a649e062e..1c72638971 100644 --- a/scripts/ripwire-eval/affected-results.json +++ b/scripts/ripwire-eval/affected-results.json @@ -1,10 +1,10 @@ { - "generated": "2026-09-08T12:33:24.668Z", + "generated": "2026-09-08T12:57:43.643Z", "results": [ { "task": "T1", "failed": null, - "ms": 661, + "ms": 690, "bytes": 8998, "seeds": 10, "selected": 65, @@ -22,7 +22,7 @@ { "task": "T2", "failed": null, - "ms": 621, + "ms": 697, "bytes": 19606, "seeds": 5, "selected": 185, @@ -36,7 +36,7 @@ { "task": "T3", "failed": null, - "ms": 650, + "ms": 740, "bytes": 2775, "seeds": 3, "selected": 7, @@ -50,7 +50,7 @@ { "task": "T4", "failed": null, - "ms": 711, + "ms": 774, "bytes": 4480, "seeds": 5, "selected": 25, @@ -64,7 +64,7 @@ { "task": "T5", "failed": null, - "ms": 679, + "ms": 686, "bytes": 3652, "seeds": 2, "selected": 18, @@ -78,7 +78,7 @@ { "task": "T6", "failed": null, - "ms": 651, + "ms": 703, "bytes": 2123, "seeds": 1, "selected": 2, diff --git a/scripts/ripwire-eval/agent-results.json b/scripts/ripwire-eval/agent-results.json index 6a8fa33423..378be46998 100644 --- a/scripts/ripwire-eval/agent-results.json +++ b/scripts/ripwire-eval/agent-results.json @@ -1,5 +1,5 @@ { - "generated": "2026-09-08T12:52:18.925Z", + "generated": "2026-09-08T12:57:15.919Z", "by_arm": { "baseline": { "runs": 12, @@ -32,7 +32,9 @@ "f1": 0.923, "subagent_tokens": 135275, "tool_uses": 59, - "duration_ms": 422489.5 + "duration_ms": 422489.5, + "files_opened": 42, + "files_opened_bytes": 513624.5 }, "ripwire": { "recall": 0.941, @@ -40,7 +42,9 @@ "f1": 0.955, "subagent_tokens": 145767, "tool_uses": 52.5, - "duration_ms": 407472 + "duration_ms": 407472, + "files_opened": 35.5, + "files_opened_bytes": 388103.5 } }, "T2": { @@ -50,7 +54,9 @@ "f1": 0.875, "subagent_tokens": 115953, "tool_uses": 52, - "duration_ms": 446680.5 + "duration_ms": 446680.5, + "files_opened": 24, + "files_opened_bytes": 305181.5 }, "ripwire": { "recall": 0.875, @@ -58,7 +64,9 @@ "f1": 0.875, "subagent_tokens": 137133.5, "tool_uses": 53, - "duration_ms": 512938.5 + "duration_ms": 512938.5, + "files_opened": 18, + "files_opened_bytes": 203737.5 } }, "T3": { @@ -68,7 +76,9 @@ "f1": 0.66, "subagent_tokens": 87134, "tool_uses": 30.5, - "duration_ms": 236221.5 + "duration_ms": 236221.5, + "files_opened": 20.5, + "files_opened_bytes": 306365.5 }, "ripwire": { "recall": 0.6, @@ -76,7 +86,9 @@ "f1": 0.75, "subagent_tokens": 109296, "tool_uses": 35, - "duration_ms": 316329 + "duration_ms": 316329, + "files_opened": 25, + "files_opened_bytes": 297640 } }, "T4": { @@ -86,7 +98,9 @@ "f1": 0.857, "subagent_tokens": 106299, "tool_uses": 39.5, - "duration_ms": 326635.5 + "duration_ms": 326635.5, + "files_opened": 20.5, + "files_opened_bytes": 249989 }, "ripwire": { "recall": 0.75, @@ -94,7 +108,9 @@ "f1": 0.857, "subagent_tokens": 109735.5, "tool_uses": 26, - "duration_ms": 316027 + "duration_ms": 316027, + "files_opened": 20.5, + "files_opened_bytes": 221074 } }, "T5": { @@ -104,7 +120,9 @@ "f1": 0.671, "subagent_tokens": 103245.5, "tool_uses": 36.5, - "duration_ms": 323961 + "duration_ms": 323961, + "files_opened": 35, + "files_opened_bytes": 527472 }, "ripwire": { "recall": 1, @@ -112,7 +130,9 @@ "f1": 0.685, "subagent_tokens": 107898.5, "tool_uses": 28.5, - "duration_ms": 313857.5 + "duration_ms": 313857.5, + "files_opened": 14.5, + "files_opened_bytes": 239848.5 } }, "T6": { @@ -122,7 +142,9 @@ "f1": 0.857, "subagent_tokens": 83490.5, "tool_uses": 24.5, - "duration_ms": 183201.5 + "duration_ms": 183201.5, + "files_opened": 15, + "files_opened_bytes": 103954 }, "ripwire": { "recall": 1, @@ -130,7 +152,9 @@ "f1": 0.857, "subagent_tokens": 82857, "tool_uses": 19, - "duration_ms": 172307 + "duration_ms": 172307, + "files_opened": 12, + "files_opened_bytes": 56235 } } }, diff --git a/scripts/ripwire-eval/bench-cli.mjs b/scripts/ripwire-eval/bench-cli.mjs new file mode 100644 index 0000000000..83eb2ff2c0 --- /dev/null +++ b/scripts/ripwire-eval/bench-cli.mjs @@ -0,0 +1,77 @@ +// Shared plumbing for the ripwire-eval scripts: flag reading, the tasks file, and the one way +// they all invoke the ripwire binary. Every bench needs the same three, and a bench that grows +// its own copy is how the three drift apart. + +import { execFile } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { promisify } from 'node:util'; + +const execFileAsync = promisify(execFile); + +/** The directory this harness lives in — where `tasks.json` and the result files sit. */ +export const harnessDir = dirname(fileURLToPath(import.meta.url)); + +/** Reads `--name=value` off argv. */ +function arg(name, fallback) { + const hit = process.argv.find((entry) => entry.startsWith(`--${name}=`)); + return hit ? hit.slice(name.length + 3) : fallback; +} + +/** + * Reads the named flags, exiting with the given usage line if any of `required` is missing, so a + * bench states its contract once instead of repeating the check and the message. + */ +export function readArgs({ usage, required, optional = {} }) { + const values = {}; + for (const name of required) values[name] = arg(name); + for (const [name, fallback] of Object.entries(optional)) values[name] = arg(name, fallback); + const missing = required.filter((name) => !values[name]); + if (missing.length > 0) { + console.error(`usage: ${usage}`); + process.exit(2); + } + return values; +} + +export function loadTasks() { + return JSON.parse(readFileSync(join(harnessDir, 'tasks.json'), 'utf8')).tasks; +} + +/** + * One ripwire invocation, timed. A non-zero exit is data, not an abort: ripwire writes its answer + * to stdout even on the exit codes that report a refusal, and a bench row that says which verb + * failed is worth more than a dead run. + */ +export async function runRipwire(ripwire, args, cwd) { + const started = process.hrtime.bigint(); + const { stdout, failed } = await capture(ripwire, args, cwd); + return { + stdout, + failed, + ms: Math.round(Number(process.hrtime.bigint() - started) / 1e6), + bytes: Buffer.byteLength(stdout), + }; +} + +async function capture(ripwire, args, cwd) { + try { + const { stdout } = await execFileAsync(ripwire, args, { cwd, maxBuffer: 64 * 1024 * 1024 }); + return { stdout, failed: null }; + } catch (error) { + return failureOf(error); + } +} + +// A refusal still writes its answer to stdout, so keep whatever came back and record why. +function failureOf(error) { + return { + stdout: textOf(error?.stdout, ''), + failed: textOf(error?.message, error).slice(0, 200), + }; +} + +function textOf(value, fallback) { + return String(value === undefined ? fallback : value); +} diff --git a/scripts/ripwire-eval/retrieval-bench.mjs b/scripts/ripwire-eval/retrieval-bench.mjs index 0ae4d53897..218824caa0 100644 --- a/scripts/ripwire-eval/retrieval-bench.mjs +++ b/scripts/ripwire-eval/retrieval-bench.mjs @@ -8,27 +8,15 @@ // // Usage: node scripts/ripwire-eval/retrieval-bench.mjs --ripwire= --worktrees= [--out=] -import { execFile } from 'node:child_process'; -import { readFileSync, writeFileSync } from 'node:fs'; -import { dirname, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { promisify } from 'node:util'; +import { writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { harnessDir, loadTasks, readArgs, runRipwire } from './bench-cli.mjs'; -const run = promisify(execFile); -const here = dirname(fileURLToPath(import.meta.url)); - -function arg(name, fallback) { - const hit = process.argv.find((entry) => entry.startsWith(`--${name}=`)); - return hit ? hit.slice(name.length + 3) : fallback; -} - -const ripwire = arg('ripwire'); -const worktrees = arg('worktrees'); -const outPath = arg('out', join(here, 'retrieval-results.json')); -if (!ripwire || !worktrees) { - console.error('usage: retrieval-bench.mjs --ripwire= --worktrees= [--out=]'); - process.exit(2); -} +const { ripwire, worktrees, out } = readArgs({ + usage: 'retrieval-bench.mjs --ripwire= --worktrees= [--out=]', + required: ['ripwire', 'worktrees'], + optional: { out: join(harnessDir, 'retrieval-results.json') }, +}); const VERBS = [ { id: 'for', args: (task) => ['.', `--for=${task.prompt}`] }, @@ -67,33 +55,22 @@ function rankPaths(output) { return seen; } -const tasks = JSON.parse(readFileSync(join(here, 'tasks.json'), 'utf8')).tasks; const results = []; -for (const task of tasks) { - const cwd = join(worktrees, task.id); +for (const task of loadTasks()) { for (const verb of VERBS) { if (verb.skipWhen?.(task)) continue; - const started = process.hrtime.bigint(); - let stdout = ''; - let failed = null; - try { - ({ stdout } = await run(ripwire, verb.args(task), { cwd, maxBuffer: 64 * 1024 * 1024 })); - } catch (error) { - failed = String(error?.message ?? error).slice(0, 200); - stdout = String(error?.stdout ?? ''); - } - const ms = Number(process.hrtime.bigint() - started) / 1e6; - const ranks = rankPaths(stdout); + const call = await runRipwire(ripwire, verb.args(task), join(worktrees, task.id)); + const ranks = rankPaths(call.stdout); const hits = task.ground_truth.map((path) => ({ path, rank: ranks.get(path) ?? null })); const found = hits.filter((hit) => hit.rank !== null); results.push({ task: task.id, verb: verb.id, - failed, - ms: Math.round(ms), - bytes: Buffer.byteLength(stdout), - est_tokens: Math.round(Buffer.byteLength(stdout) / 4), + failed: call.failed, + ms: call.ms, + bytes: call.bytes, + est_tokens: Math.round(call.bytes / 4), paths_mentioned: ranks.size, ground_truth: task.ground_truth.length, hits: found.length, @@ -102,13 +79,13 @@ for (const task of tasks) { per_file: hits, }); process.stderr.write( - `${task.id}/${verb.id}: ${found.length}/${task.ground_truth.length} in ${Buffer.byteLength(stdout)} B\n`, + `${task.id}/${verb.id}: ${found.length}/${task.ground_truth.length} in ${call.bytes} B\n`, ); } } writeFileSync( - outPath, + out, `${JSON.stringify({ generated: new Date().toISOString(), results }, null, 2)}\n`, ); -console.log(outPath); +console.log(out); diff --git a/scripts/ripwire-eval/retrieval-results.json b/scripts/ripwire-eval/retrieval-results.json index 429e3f4677..17ddad7ebd 100644 --- a/scripts/ripwire-eval/retrieval-results.json +++ b/scripts/ripwire-eval/retrieval-results.json @@ -1,11 +1,11 @@ { - "generated": "2026-09-08T12:30:17.489Z", + "generated": "2026-09-08T12:57:39.274Z", "results": [ { "task": "T1", "verb": "for", "failed": null, - "ms": 1015, + "ms": 1056, "bytes": 10354, "est_tokens": 2589, "paths_mentioned": 43, @@ -84,7 +84,7 @@ "task": "T1", "verb": "pack-task", "failed": null, - "ms": 938, + "ms": 931, "bytes": 12413, "est_tokens": 3103, "paths_mentioned": 20, @@ -163,7 +163,7 @@ "task": "T1", "verb": "pack-task-4k", "failed": null, - "ms": 964, + "ms": 916, "bytes": 8821, "est_tokens": 2205, "paths_mentioned": 13, @@ -242,7 +242,7 @@ "task": "T1", "verb": "for-idents", "failed": null, - "ms": 1005, + "ms": 974, "bytes": 10614, "est_tokens": 2654, "paths_mentioned": 45, @@ -321,7 +321,7 @@ "task": "T2", "verb": "for", "failed": null, - "ms": 1044, + "ms": 996, "bytes": 10296, "est_tokens": 2574, "paths_mentioned": 43, @@ -364,7 +364,7 @@ "task": "T2", "verb": "pack-task", "failed": null, - "ms": 930, + "ms": 873, "bytes": 11590, "est_tokens": 2898, "paths_mentioned": 10, @@ -407,7 +407,7 @@ "task": "T2", "verb": "pack-task-4k", "failed": null, - "ms": 928, + "ms": 913, "bytes": 8137, "est_tokens": 2034, "paths_mentioned": 5, @@ -450,7 +450,7 @@ "task": "T2", "verb": "for-idents", "failed": null, - "ms": 934, + "ms": 949, "bytes": 10653, "est_tokens": 2663, "paths_mentioned": 37, @@ -493,7 +493,7 @@ "task": "T3", "verb": "for", "failed": null, - "ms": 1038, + "ms": 1011, "bytes": 9748, "est_tokens": 2437, "paths_mentioned": 34, @@ -528,7 +528,7 @@ "task": "T3", "verb": "pack-task", "failed": null, - "ms": 945, + "ms": 988, "bytes": 9695, "est_tokens": 2424, "paths_mentioned": 8, @@ -563,7 +563,7 @@ "task": "T3", "verb": "pack-task-4k", "failed": null, - "ms": 909, + "ms": 949, "bytes": 8476, "est_tokens": 2119, "paths_mentioned": 7, @@ -598,7 +598,7 @@ "task": "T3", "verb": "for-idents", "failed": null, - "ms": 1002, + "ms": 978, "bytes": 10718, "est_tokens": 2680, "paths_mentioned": 41, @@ -633,7 +633,7 @@ "task": "T4", "verb": "for", "failed": null, - "ms": 988, + "ms": 973, "bytes": 10118, "est_tokens": 2530, "paths_mentioned": 41, @@ -676,7 +676,7 @@ "task": "T4", "verb": "pack-task", "failed": null, - "ms": 986, + "ms": 958, "bytes": 12724, "est_tokens": 3181, "paths_mentioned": 19, @@ -719,7 +719,7 @@ "task": "T4", "verb": "pack-task-4k", "failed": null, - "ms": 994, + "ms": 1009, "bytes": 9044, "est_tokens": 2261, "paths_mentioned": 15, @@ -762,7 +762,7 @@ "task": "T4", "verb": "for-idents", "failed": null, - "ms": 952, + "ms": 1020, "bytes": 9641, "est_tokens": 2410, "paths_mentioned": 44, @@ -805,7 +805,7 @@ "task": "T5", "verb": "for", "failed": null, - "ms": 1036, + "ms": 1041, "bytes": 10059, "est_tokens": 2515, "paths_mentioned": 35, @@ -836,7 +836,7 @@ "task": "T5", "verb": "pack-task", "failed": null, - "ms": 997, + "ms": 939, "bytes": 13150, "est_tokens": 3288, "paths_mentioned": 28, @@ -867,7 +867,7 @@ "task": "T5", "verb": "pack-task-4k", "failed": null, - "ms": 946, + "ms": 888, "bytes": 9018, "est_tokens": 2255, "paths_mentioned": 14, @@ -898,7 +898,7 @@ "task": "T5", "verb": "for-idents", "failed": null, - "ms": 1006, + "ms": 922, "bytes": 10785, "est_tokens": 2696, "paths_mentioned": 39, @@ -929,7 +929,7 @@ "task": "T6", "verb": "for", "failed": null, - "ms": 1136, + "ms": 1016, "bytes": 10390, "est_tokens": 2598, "paths_mentioned": 33, @@ -956,7 +956,7 @@ "task": "T6", "verb": "pack-task", "failed": null, - "ms": 1021, + "ms": 965, "bytes": 11137, "est_tokens": 2784, "paths_mentioned": 7, @@ -983,7 +983,7 @@ "task": "T6", "verb": "pack-task-4k", "failed": null, - "ms": 1002, + "ms": 994, "bytes": 8256, "est_tokens": 2064, "paths_mentioned": 3, @@ -1010,7 +1010,7 @@ "task": "T6", "verb": "for-idents", "failed": null, - "ms": 1024, + "ms": 1013, "bytes": 10545, "est_tokens": 2636, "paths_mentioned": 43, diff --git a/scripts/ripwire-eval/score.mjs b/scripts/ripwire-eval/score.mjs index 83a9230334..c8cd292acd 100644 --- a/scripts/ripwire-eval/score.mjs +++ b/scripts/ripwire-eval/score.mjs @@ -6,35 +6,110 @@ // replays — as file-level recall, precision and F1. Files listed in a task's `excluded` set // (generated ledgers and fixtures) are dropped from a prediction rather than counted against it. // -// Usage: node scripts/ripwire-eval/score.mjs --runs= [--out=] +// Usage: node scripts/ripwire-eval/score.mjs --runs= [--worktrees=] [--out=] import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'; -import { dirname, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { join } from 'node:path'; +import { harnessDir, loadTasks, readArgs } from './bench-cli.mjs'; + +const { + runs: runsDir, + worktrees, + out, +} = readArgs({ + usage: 'score.mjs --runs= [--worktrees=] [--out=]', + required: ['runs'], + // Given the per-task clones, each run also reports the byte size of the files it opened — a + // context-cost measure that does not depend on the agent self-reporting one. + optional: { worktrees: '', out: join(harnessDir, 'agent-results.json') }, +}); + +const tasks = new Map(loadTasks().map((task) => [task.id, task])); -const here = dirname(fileURLToPath(import.meta.url)); +function f1(recall, precision) { + return recall + precision === 0 ? 0 : (2 * recall * precision) / (recall + precision); +} -function arg(name, fallback) { - const hit = process.argv.find((entry) => entry.startsWith(`--${name}=`)); - return hit ? hit.slice(name.length + 3) : fallback; +function openedPaths(run) { + return Array.isArray(run.files_opened) ? run.files_opened : []; } -const runsDir = arg('runs'); -// Optional: the directory holding the per-task clones. Given it, each run also reports the byte -// size of the files it opened — a context-cost proxy that does not depend on self-reporting. -const worktrees = arg('worktrees'); -const outPath = arg('out', join(here, 'agent-results.json')); -if (!runsDir) { - console.error('usage: score.mjs --runs= [--out=]'); - process.exit(2); +function openedBytes(root, run) { + let total = 0; + for (const path of openedPaths(run)) { + try { + total += statSync(join(root, run.task, path)).size; + } catch { + // A path the agent named that does not resolve in the pinned clone contributes nothing. + } + } + return total; } -const tasks = new Map( - JSON.parse(readFileSync(join(here, 'tasks.json'), 'utf8')).tasks.map((task) => [task.id, task]), -); +/** The commit's whole change set: files it modified plus files it created. */ +function truthOf(task) { + return new Set([...task.ground_truth, ...(task.added_files ?? [])]); +} -function f1(recall, precision) { - return recall + precision === 0 ? 0 : (2 * recall * precision) / (recall + precision); +/** What the run claims, minus the generated files the task excludes from scoring either way. */ +function predictionOf(run, task) { + const excluded = new Set(task.excluded ?? []); + return [...new Set([...(run.files ?? []), ...(run.new_files ?? [])])].filter( + (path) => !excluded.has(path), + ); +} + +// The runtime-reported cost fields, carried through as numbers or as null when a run predates one. +const REPORTED_COST = ['subagent_tokens', 'tool_uses', 'duration_ms']; + +function numberOrNull(value) { + return typeof value === 'number' ? value : null; +} + +/** What the answer cost to produce, as the runtime reported it — never the agent's own estimate. */ +function costOf(run) { + return { + ...Object.fromEntries(REPORTED_COST.map((key) => [key, numberOrNull(run[key])])), + files_opened: openedPaths(run).length, + files_opened_bytes: worktrees ? openedBytes(worktrees, run) : null, + }; +} + +/** How close the answer came: the three rates plus the sets behind them. */ +function accuracyOf(run, task) { + const truth = truthOf(task); + const predicted = predictionOf(run, task); + const hit = predicted.filter((path) => truth.has(path)); + const recall = hit.length / truth.size; + const precision = predicted.length === 0 ? 0 : hit.length / predicted.length; + const added = new Set(task.added_files ?? []); + + return { + ground_truth: truth.size, + predicted: predicted.length, + hit: hit.length, + recall: Number(recall.toFixed(3)), + precision: Number(precision.toFixed(3)), + f1: Number(f1(recall, precision).toFixed(3)), + new_files_expected: added.size, + new_files_hit: (run.new_files ?? []).filter((path) => added.has(path)).length, + missed: [...truth].filter((path) => !predicted.includes(path)), + spurious: predicted.filter((path) => !truth.has(path)), + }; +} + +function scoreRun(name, run, task) { + const { missed, spurious, ...rates } = accuracyOf(run, task); + return { + run: name.replace(/\.json$/, ''), + task: run.task, + arm: run.arm, + rep: run.rep, + ...rates, + ...costOf(run), + missed, + spurious, + }; } const scored = readdirSync(runsDir) @@ -44,56 +119,9 @@ const scored = readdirSync(runsDir) const run = JSON.parse(readFileSync(join(runsDir, name), 'utf8')); const task = tasks.get(run.task); if (!task) throw new Error(`run ${name} names unknown task ${run.task}`); - - const excluded = new Set(task.excluded ?? []); - // The commit's whole change set: files it modified plus files it created. Predicting that a - // change needs a new test file beside an existing module is part of localizing it. - const truth = new Set([...task.ground_truth, ...(task.added_files ?? [])]); - const predicted = [...new Set([...(run.files ?? []), ...(run.new_files ?? [])])].filter( - (path) => !excluded.has(path), - ); - const hit = predicted.filter((path) => truth.has(path)); - const recall = hit.length / truth.size; - const precision = predicted.length === 0 ? 0 : hit.length / predicted.length; - - const added = new Set(task.added_files ?? []); - const addedHit = (run.new_files ?? []).filter((path) => added.has(path)).length; - - return { - run: name.replace(/\.json$/, ''), - task: run.task, - arm: run.arm, - rep: run.rep, - ground_truth: truth.size, - predicted: predicted.length, - hit: hit.length, - recall: Number(recall.toFixed(3)), - precision: Number(precision.toFixed(3)), - f1: Number(f1(recall, precision).toFixed(3)), - new_files_expected: added.size, - new_files_hit: addedHit, - subagent_tokens: run.subagent_tokens ?? null, - tool_uses: run.tool_uses ?? null, - duration_ms: run.duration_ms ?? null, - files_opened: (run.files_opened ?? []).length, - files_opened_bytes: worktrees ? openedBytes(worktrees, run) : null, - missed: [...truth].filter((path) => !predicted.includes(path)), - spurious: predicted.filter((path) => !truth.has(path)), - }; + return scoreRun(name, run, task); }); -function openedBytes(root, run) { - let total = 0; - for (const path of run.files_opened ?? []) { - try { - total += statSync(join(root, run.task, path)).size; - } catch { - // A path the agent named that does not resolve in the pinned clone contributes nothing. - } - } - return total; -} - function mean(values) { const usable = values.filter((value) => typeof value === 'number'); return usable.length @@ -101,42 +129,41 @@ function mean(values) { : null; } +const AGGREGATED = [ + 'recall', + 'precision', + 'f1', + 'subagent_tokens', + 'tool_uses', + 'duration_ms', + 'files_opened', + 'files_opened_bytes', +]; + +function summarize(rows) { + return Object.fromEntries(AGGREGATED.map((key) => [key, mean(rows.map((row) => row[key]))])); +} + const byArm = {}; for (const arm of new Set(scored.map((entry) => entry.arm))) { const rows = scored.filter((entry) => entry.arm === arm); - byArm[arm] = { - runs: rows.length, - recall: mean(rows.map((r) => r.recall)), - precision: mean(rows.map((r) => r.precision)), - f1: mean(rows.map((r) => r.f1)), - subagent_tokens: mean(rows.map((r) => r.subagent_tokens)), - tool_uses: mean(rows.map((r) => r.tool_uses)), - duration_ms: mean(rows.map((r) => r.duration_ms)), - files_opened: mean(rows.map((r) => r.files_opened)), - files_opened_bytes: mean(rows.map((r) => r.files_opened_bytes)), - }; + byArm[arm] = { runs: rows.length, ...summarize(rows) }; } const byTask = {}; for (const id of tasks.keys()) { const rows = scored.filter((entry) => entry.task === id); - if (!rows.length) continue; - byTask[id] = {}; - for (const arm of new Set(rows.map((entry) => entry.arm))) { - const armRows = rows.filter((entry) => entry.arm === arm); - byTask[id][arm] = { - recall: mean(armRows.map((r) => r.recall)), - precision: mean(armRows.map((r) => r.precision)), - f1: mean(armRows.map((r) => r.f1)), - subagent_tokens: mean(armRows.map((r) => r.subagent_tokens)), - tool_uses: mean(armRows.map((r) => r.tool_uses)), - duration_ms: mean(armRows.map((r) => r.duration_ms)), - }; - } + if (rows.length === 0) continue; + byTask[id] = Object.fromEntries( + [...new Set(rows.map((entry) => entry.arm))].map((arm) => [ + arm, + summarize(rows.filter((entry) => entry.arm === arm)), + ]), + ); } writeFileSync( - outPath, + out, `${JSON.stringify({ generated: new Date().toISOString(), by_arm: byArm, by_task: byTask, runs: scored }, null, 2)}\n`, ); From 162db73826b9bd812a2db4496ca7dfcbd224262b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 13:00:32 +0000 Subject: [PATCH 05/10] chore(gates): declare the ripwire-eval benches as fallow entry points MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit They are operator-run CLIs invoked by hand, so nothing in the tree references them; without these roots their shared bench-cli.mjs reads as dead too, and the CRAP estimate — which is derived from export references — flags functions at 6 cyclomatic. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01F7vMn8ehPq3NTPYMfxs7ro --- .fallowrc.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.fallowrc.json b/.fallowrc.json index 22975c7e2f..70937334b5 100644 --- a/.fallowrc.json +++ b/.fallowrc.json @@ -20,6 +20,12 @@ "packages/capture-kit/src/png-worker.ts", "scripts/patch-xcuitest-runner-icon.ts", "scripts/runner-request-count/run.ts", + // The ripwire-eval benches are operator-run CLIs (docs/ripwire-context-tooling-evaluation.md), + // invoked by hand rather than imported, so nothing in the tree references them; without these + // roots their shared bench-cli.mjs reads as dead too. + "scripts/ripwire-eval/retrieval-bench.mjs", + "scripts/ripwire-eval/affected-bench.mjs", + "scripts/ripwire-eval/score.mjs", "packages/capture-kit/src/ios-snapshot-engine/replay.ts", // #1596 regression fixture: runs as a real `node --experimental-strip-types` // subprocess (test/integration/daemon-replace-exit-flush.test.ts), so From 8498a76e9ba1c4ed5ef40e080e07727d2549fcd0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 13:08:40 +0000 Subject: [PATCH 06/10] fix(eval): correct the test-selection denominator and make the setup reproducible Review follow-up on #2400. - --affected scored anything under __tests__/ as an expected test, counting runtime-port-fixtures.ts and fake-adb.ts as harnesses it should have named. Only *.test.* files count now; files that merely live in a test location are helpers, listed per task under helpers_not_scored and counted in neither column. The result is 9/12, not 9/14. - Retrieval recall is labelled existing-files-only, the basis it always used: a retrieval verb ranks what the tree contains, so a file the commit created is not a hit it could have scored. The agent A/B scores the whole set. - The documented setup could not work: it made the task clones' origin the ripwire checkout, and git refuses to fetch an abbreviated SHA. Replaced with setup-clones.sh, which resolves origin from the checkout it runs in, fetches the full parent SHA from tasks.json, and fails if the fix commit is reachable. - The A/B was described as if the two bench commands re-ran it. They never did. It is now stated as an archived observation, with the two arm briefs and the generator that renders them checked in: make-briefs.mjs reproduces the briefs the recorded runs were given byte-for-byte. All results regenerated from freshly cut clones via the documented commands. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01F7vMn8ehPq3NTPYMfxs7ro --- docs/ripwire-context-tooling-evaluation.md | 56 ++++++++---- scripts/ripwire-eval/README.md | 96 ++++++++++++--------- scripts/ripwire-eval/affected-bench.mjs | 17 +++- scripts/ripwire-eval/affected-results.json | 51 ++++++----- scripts/ripwire-eval/agent-results.json | 2 +- scripts/ripwire-eval/arms/baseline.md | 1 + scripts/ripwire-eval/arms/ripwire.md | 24 ++++++ scripts/ripwire-eval/make-briefs.mjs | 86 ++++++++++++++++++ scripts/ripwire-eval/retrieval-bench.mjs | 6 ++ scripts/ripwire-eval/retrieval-results.json | 78 +++++++++++------ scripts/ripwire-eval/setup-clones.sh | 38 ++++++++ scripts/ripwire-eval/tasks.json | 24 +++--- 12 files changed, 358 insertions(+), 121 deletions(-) create mode 100644 scripts/ripwire-eval/arms/baseline.md create mode 100644 scripts/ripwire-eval/arms/ripwire.md create mode 100644 scripts/ripwire-eval/make-briefs.mjs create mode 100755 scripts/ripwire-eval/setup-clones.sh diff --git a/docs/ripwire-context-tooling-evaluation.md b/docs/ripwire-context-tooling-evaluation.md index c055636085..1ae06b3efc 100644 --- a/docs/ripwire-context-tooling-evaluation.md +++ b/docs/ripwire-context-tooling-evaluation.md @@ -77,7 +77,10 @@ a 52× reduction on "what do we already know about X". ## 1. Retrieval from raw task text (deterministic) One call per task, fed the task description verbatim, scored on how many ground-truth files it -names. `for-idents` is the same `--for` verb fed only the identifiers the task text itself puts in +names. **Recall here is over the change's existing files only** — a retrieval verb ranks what the +tree contains, so a file the commit created is not a hit it could have scored. The agent A/B in +§3 scores the whole change set, added files included, so the two denominators differ on purpose +(the task table above lists the whole set). `for-idents` is the same `--for` verb fed only the identifiers the task text itself puts in backticks — a mechanical distillation, included to separate ranking quality from phrasing. | Verb | Mean recall | Mean bytes | Mean ms | @@ -106,27 +109,33 @@ route, so when it finds anything it ranks it near the top. This repository's rule is that tests mirror source one-to-one, which makes "I changed these sources, which tests do I run" a question with a checkable answer. Each task's non-test ground-truth files were fed to `--affected`; the score is whether the commit's own test files came -back. Files the commit *created* are excluded — a selector cannot name a file that does not exist. +back. -| Task | Expected tests found | Tests selected | Bytes | -| --- | --- | --- | --- | -| T1 | 2 / 5 | 65 | 9.0 KB | -| T2 | 1 / 1 | 185 | 19.6 KB | -| T3 | 1 / 2 | 7 | 2.8 KB | -| T4 | 2 / 2 | 25 | 4.5 KB | -| T5 | 2 / 2 | 18 | 3.7 KB | -| T6 | 1 / 2 | 2 | 2.1 KB | -| **Total** | **9 / 14 (64%)** | | | - -Two of the five misses are not test files at all — `fake-adb.ts` and `runtime-port-fixtures.ts` -are test *utilities*, which `--affected` reports as reached symbols rather than as `` rows. -The T6 miss is real: `snapshot-route.test.ts` covers the direct caller of the changed module and -should have been a short hop. +Two exclusions keep the denominator honest. Files the commit *created* are out — a selector cannot +name a file that does not exist. And only **test files** (`*.test.ts`) count: a file that merely +lives in a test location — `__tests__/test-utils/fake-adb.ts`, `__tests__/runtime-port-fixtures.ts`, +a provider-scenario world — is a helper, neither a source the change starts from nor a harness +`--affected` could name. Five such helpers appear across the six changes; each task's are listed +under `helpers_not_scored`. + +| Task | Expected tests found | Tests selected | Helpers not scored | Bytes | +| --- | --- | --- | --- | --- | +| T1 | 2 / 4 | 65 | 1 | 9.0 KB | +| T2 | 1 / 1 | 185 | 0 | 19.6 KB | +| T3 | 1 / 1 | 5 | 3 | 2.4 KB | +| T4 | 2 / 2 | 24 | 1 | 4.4 KB | +| T5 | 2 / 2 | 18 | 0 | 3.7 KB | +| T6 | 1 / 2 | 2 | 0 | 2.1 KB | +| **Total** | **9 / 12 (75%)** | | 5 | | + +Both misses are real. T1's are two `packages/maestro` harnesses the walk did not reach; T6's is +`snapshot-route.test.ts`, which covers the direct caller of the changed module and should have +been a short hop. Selection breadth is the sharper problem. T2 named **185** test files for a 5-file change: the seeds reach into `packages/kernel`, whose symbols are called from everywhere, and the walk has no notion of "this hub is not evidence". At that width the answer costs more to read than it saves. -`--affected` is useful here at 2–25 selected files and not useful at 185. +`--affected` is useful here at 2–24 selected files and not useful at 185. This does not overlap `pnpm check:affected`, which selects CI *lanes* from a diff. `--affected` selects test *files* from source files. They answer different questions. @@ -144,6 +153,16 @@ Both arms ran the same model. Each agent returned the change set it predicted; t it against the commit. Cost is the subagent's own token spend, tool-call count and wall clock as reported by the runtime, not self-estimated. +**Read this half as an archived observation, not as a scripted experiment.** The two deterministic +benches above re-run from one command each; this one does not. The arms were driven by subagents +inside a Claude Code session rather than by a runner in this repository, so repeating it depends on +an agent runtime the harness does not own and on a model that is not pinned here. What is preserved +is everything that made the comparison fair and the recorded runs auditable: the two tooling +paragraphs verbatim (`scripts/ripwire-eval/arms/`), the generator that renders the 12 briefs from +them (`make-briefs.mjs`, which reproduces the briefs the recorded runs were given byte-for-byte), +and the 24 answers as returned (`scripts/ripwire-eval/runs/`). `score.mjs --runs=… --worktrees=…` +re-derives every number below from those files. + ### Results Per task, mean of two runs, shown as `baseline → ripwire`: @@ -284,6 +303,9 @@ change once its seeds reach a hub module in `packages/kernel`. - Six tasks, two replicates. Enough to size an effect, not to make a small one significant. The arms are indistinguishable on half the tasks, which is itself the main result. +- The agent A/B is an archived observation: its runs are checked in and re-scorable, but they are + not re-runnable from this repository (see §3). The retrieval and test-selection benches are, and + reproduce from the documented commands on freshly cut clones. - Both arms ran the same model; this measures tooling, not model choice. - The pinned clones are shallow (20 commits), so ripwire's churn and co-change lenses see a truncated history. That handicaps ripwire. diff --git a/scripts/ripwire-eval/README.md b/scripts/ripwire-eval/README.md index 6506a52d41..39c4cc7f89 100644 --- a/scripts/ripwire-eval/README.md +++ b/scripts/ripwire-eval/README.md @@ -19,67 +19,85 @@ conventions, not localization. ## Setting up ```sh -# 1. Build ripwire (C++23, no runtime dependencies) -git clone https://github.com/redhat-et/ripwire && cd ripwire -cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release && cmake --build build - -# 2. Cut one leak-free clone per task, pinned at the parent commit. -# A shallow fetch of the parent SHA is what keeps the fix commit unreachable — a plain -# worktree shares .git with the main checkout and would hand the agent the answer. -for pair in T1:1f9d940 T2:e0f8c55 T3:7bcbf13 T4:a9283fa T5:ff59309 T6:6768a04; do - id=${pair%%:*}; sha=${pair##*:} - mkdir -p /tmp/rw/$id && git -C /tmp/rw/$id init -q - git -C /tmp/rw/$id remote add origin "$PWD" - git -C /tmp/rw/$id fetch -q --no-tags --depth=20 origin "$sha" - git -C /tmp/rw/$id checkout -q --detach "$sha" -done +# 1. Build ripwire (C++23, no runtime dependencies) somewhere OUTSIDE this repository. +git clone https://github.com/redhat-et/ripwire /tmp/ripwire +cmake -S /tmp/ripwire -B /tmp/ripwire/build -G Ninja -DCMAKE_BUILD_TYPE=Release +cmake --build /tmp/ripwire/build # the binary lands at /tmp/ripwire/build/ripwire + +# 2. Cut one leak-free clone per task, pinned at its commit's parent. Run it from an +# agent-device checkout — `origin` for the task clones is THIS repository. +scripts/ripwire-eval/setup-clones.sh /tmp/rw ``` +`setup-clones.sh` shallow-fetches each parent SHA into its own repository and then *proves* the +fix commit is unreachable from it, failing rather than handing an agent the answer. A `git +worktree` would not do: it shares `.git` with the main checkout, so the commit under test would be +one `git log --all` away. The fetch names the full SHA because git refuses to fetch an abbreviated +one, which is why `tasks.json` carries full SHAs. + ## Running -Deterministic halves — no model in the loop, so they are reproducible run to run: +### The deterministic halves — reproducible run to run, no model in the loop ```sh -node scripts/ripwire-eval/retrieval-bench.mjs --ripwire= --worktrees=/tmp/rw -node scripts/ripwire-eval/affected-bench.mjs --ripwire= --worktrees=/tmp/rw +node scripts/ripwire-eval/retrieval-bench.mjs --ripwire=/tmp/ripwire/build/ripwire --worktrees=/tmp/rw +node scripts/ripwire-eval/affected-bench.mjs --ripwire=/tmp/ripwire/build/ripwire --worktrees=/tmp/rw ``` `retrieval-bench` asks what a single ripwire call surfaces from the raw task text and what it -costs. `affected-bench` feeds it the change's non-test files and checks whether the change's own -test files come back. +costs. Its recall is over the change's **existing** files only: a retrieval verb ranks what the +tree contains, so a file the commit created is not a hit it could have scored. + +`affected-bench` feeds it the change's non-test sources and checks whether the change's own test +files come back. It scores **test files** (`*.test.ts`) only. Files that merely live in a test +location — `__tests__/test-utils/fake-adb.ts`, `__tests__/runtime-port-fixtures.ts`, a +provider-scenario world — are helpers: neither a source the change starts from nor a harness +`--affected` could name. They are listed per task under `helpers_not_scored` and counted in +neither column. + +Both write their result JSON next to this README. Run `pnpm format` afterwards; the scripts emit +plain `JSON.stringify` output and oxfmt owns the checked-in shape. + +### The agent A/B — an archived observation, not a scripted experiment + +**Read the A/B numbers as a recorded result, not as something these commands will reproduce.** +The two arms were driven by subagents inside a Claude Code session, not by a runner in this +repository, so re-running them depends on an agent runtime this harness does not own and on a +model that is not pinned here. What *is* preserved is everything that made the comparison fair, +and it is enough to repeat the design or to audit the one that ran: -Agent half — two arms over the same six tasks, identical prompts except the tooling paragraph: +- `arms/baseline.md` and `arms/ripwire.md` — the two tooling paragraphs, verbatim. They are the + **only** difference between the arms. +- `make-briefs.mjs` — renders the 12 briefs from those two files and `tasks.json`. It reproduces + the briefs the recorded runs were given byte-for-byte: -1. Generate a brief per (task, arm) from `tasks.json` — the task prose, the pinned clone path, - the rules, and the JSON deliverable contract. -2. Run each brief as a subagent. The **baseline** arm gets Read/Grep/Glob/Bash; the **ripwire** - arm gets the same plus the ripwire binary and its verb table. -3. Save each answer as one JSON file per run: `{task, arm, rep, files, new_files, files_opened, - subagent_tokens, tool_uses, duration_ms, notes}`. -4. Score them: + ```sh + node scripts/ripwire-eval/make-briefs.mjs \ + --worktrees=/tmp/rw --out=/tmp/rw-briefs --ripwire=/tmp/ripwire/build/ripwire + ``` + +- `runs/` — the 24 answers as returned, one file per (task, arm, replicate), each carrying the + runtime's own `subagent_tokens`, `tool_uses` and `duration_ms` rather than a self-estimate. + +To repeat it: generate the briefs, run each as one agent with the tools its arm names, save each +answer as `runs/--r.json` in the shape those files already use, and score: ```sh -node scripts/ripwire-eval/score.mjs --runs= --worktrees=/tmp/rw +node scripts/ripwire-eval/score.mjs --runs=scripts/ripwire-eval/runs --worktrees=/tmp/rw ``` `--worktrees` is optional; with it, each run also reports the byte size of the files it opened, measured from the pinned clone rather than taken from the agent's own account. -`score.mjs` reports per-run and per-arm file-level recall, precision and F1 against ground truth, -alongside the token, tool-call and wall-clock cost of producing the answer. - -## Caveats that travel with these numbers - -- Six tasks, two replicates. Enough to size an effect, not to make a small one significant. -- Both arms run the same model. The result is about tooling, not about model choice. -- The clones are shallow (20 commits), so ripwire's churn and co-change lenses see a truncated - history. That handicaps ripwire relative to a full checkout. -- ripwire indexes were warm when the agents ran. Cold-index cost is measured and reported - separately rather than folded into per-run wall clock. +Execution configuration behind the recorded runs: 24 runs (6 tasks x 2 arms x 2 replicates), one +subagent per run with a fresh context, both arms on the same model, ripwire 0.5.0 built from +`ef6168b18` with its index already warm for each clone. ## What is checked in here - `tasks.json` — the six tasks and their ground truth. +- `arms/` — the two tooling paragraphs that define the A/B; `make-briefs.mjs` renders the briefs. +- `bench-cli.mjs` — the flag reader, the tasks file and the one timed ripwire invocation. - `runs/` — the 24 raw subagent answers, one file per (task, arm, replicate). - `agent-results.json`, `retrieval-results.json`, `affected-results.json` — scored output of the three benches, regenerated by the commands above. Run `pnpm format` after regenerating; the diff --git a/scripts/ripwire-eval/affected-bench.mjs b/scripts/ripwire-eval/affected-bench.mjs index a671913a3f..4d0ae3d714 100644 --- a/scripts/ripwire-eval/affected-bench.mjs +++ b/scripts/ripwire-eval/affected-bench.mjs @@ -18,18 +18,26 @@ const { ripwire, worktrees, out } = readArgs({ optional: { out: join(harnessDir, 'affected-results.json') }, }); -const isTest = (path) => /(\.test\.[cm]?[jt]sx?$)|(^|\/)__tests__\//.test(path); +// A test FILE is a harness the runner executes. A file that merely lives in a test location — +// `__tests__/test-utils/fake-adb.ts`, `__tests__/runtime-port-fixtures.ts`, a provider-scenario +// world — is a helper: it is neither a source the change starts from nor a harness `--affected` +// could name, so it is counted in neither column. +const isTestFile = (path) => /\.test\.[cm]?[jt]sx?$/.test(path); +const inTestLocation = (path) => /(^|\/)(__tests__|test)\//.test(path); +const isHelper = (path) => !isTestFile(path) && inTestLocation(path); +const isSource = (path) => !isTestFile(path) && !isHelper(path) && /\.[cm]?[jt]s$/.test(path); const results = []; for (const task of loadTasks()) { const added = new Set(task.added_files ?? []); const truth = [...task.ground_truth, ...added]; - const sources = truth.filter((path) => !isTest(path) && /\.[cm]?[jt]s$/.test(path)); + const sources = truth.filter(isSource); // A selector cannot name a file the change has not created yet, so files the commit ADDED are // reported separately rather than counted as misses. - const expected = truth.filter((path) => isTest(path) && !added.has(path)); - const expectedAdded = truth.filter((path) => isTest(path) && added.has(path)); + const expected = truth.filter((path) => isTestFile(path) && !added.has(path)); + const expectedAdded = truth.filter((path) => isTestFile(path) && added.has(path)); + const helpers = truth.filter(isHelper); if (sources.length === 0 || expected.length === 0) { results.push({ task: task.id, skipped: 'no source/test split in ground truth' }); continue; @@ -51,6 +59,7 @@ for (const task of loadTasks()) { selected: selected.length, expected: expected.length, expected_added_not_scorable: expectedAdded.length, + helpers_not_scored: helpers, hit: hit.length, recall: Number((hit.length / expected.length).toFixed(3)), // Of the tests it named, how many were actually touched — the cost of running the whole set. diff --git a/scripts/ripwire-eval/affected-results.json b/scripts/ripwire-eval/affected-results.json index 1c72638971..a399c70335 100644 --- a/scripts/ripwire-eval/affected-results.json +++ b/scripts/ripwire-eval/affected-results.json @@ -1,33 +1,34 @@ { - "generated": "2026-09-08T12:57:43.643Z", + "generated": "2026-09-08T13:06:07.464Z", "results": [ { "task": "T1", "failed": null, - "ms": 690, + "ms": 717, "bytes": 8998, "seeds": 10, "selected": 65, - "expected": 5, + "expected": 4, "expected_added_not_scorable": 0, + "helpers_not_scored": ["packages/maestro/src/internal/__tests__/runtime-port-fixtures.ts"], "hit": 2, - "recall": 0.4, + "recall": 0.5, "precision": 0.031, "missed": [ "packages/maestro/src/internal/__tests__/program-ir-parser.test.ts", - "packages/maestro/src/internal/__tests__/runtime-port-fixtures.ts", "packages/maestro/src/internal/__tests__/runtime-port.test.ts" ] }, { "task": "T2", "failed": null, - "ms": 697, + "ms": 649, "bytes": 19606, "seeds": 5, "selected": 185, "expected": 1, "expected_added_not_scorable": 1, + "helpers_not_scored": [], "hit": 1, "recall": 1, "precision": 0.005, @@ -36,40 +37,47 @@ { "task": "T3", "failed": null, - "ms": 740, - "bytes": 2775, - "seeds": 3, - "selected": 7, - "expected": 2, + "ms": 655, + "bytes": 2447, + "seeds": 1, + "selected": 5, + "expected": 1, "expected_added_not_scorable": 0, + "helpers_not_scored": [ + "packages/platform-android/src/__tests__/test-utils/fake-adb.ts", + "test/integration/provider-scenarios/android-ime-lifecycle-world.ts", + "test/integration/provider-scenarios/android-world.ts" + ], "hit": 1, - "recall": 0.5, - "precision": 0.143, - "missed": ["packages/platform-android/src/__tests__/test-utils/fake-adb.ts"] + "recall": 1, + "precision": 0.2, + "missed": [] }, { "task": "T4", "failed": null, - "ms": 774, - "bytes": 4480, - "seeds": 5, - "selected": 25, + "ms": 642, + "bytes": 4380, + "seeds": 4, + "selected": 24, "expected": 2, "expected_added_not_scorable": 1, + "helpers_not_scored": ["test/wire-compat/surface.ts"], "hit": 2, "recall": 1, - "precision": 0.08, + "precision": 0.083, "missed": [] }, { "task": "T5", "failed": null, - "ms": 686, + "ms": 667, "bytes": 3652, "seeds": 2, "selected": 18, "expected": 2, "expected_added_not_scorable": 0, + "helpers_not_scored": [], "hit": 2, "recall": 1, "precision": 0.111, @@ -78,12 +86,13 @@ { "task": "T6", "failed": null, - "ms": 703, + "ms": 650, "bytes": 2123, "seeds": 1, "selected": 2, "expected": 2, "expected_added_not_scorable": 0, + "helpers_not_scored": [], "hit": 1, "recall": 0.5, "precision": 0.5, diff --git a/scripts/ripwire-eval/agent-results.json b/scripts/ripwire-eval/agent-results.json index 378be46998..a6ca1c5142 100644 --- a/scripts/ripwire-eval/agent-results.json +++ b/scripts/ripwire-eval/agent-results.json @@ -1,5 +1,5 @@ { - "generated": "2026-09-08T12:57:15.919Z", + "generated": "2026-09-08T13:06:07.561Z", "by_arm": { "baseline": { "runs": 12, diff --git a/scripts/ripwire-eval/arms/baseline.md b/scripts/ripwire-eval/arms/baseline.md new file mode 100644 index 0000000000..9d754e88b7 --- /dev/null +++ b/scripts/ripwire-eval/arms/baseline.md @@ -0,0 +1 @@ +TOOLING — this arm has the standard agent toolbox only: Read, Grep, Glob, and Bash (`cat`, `rg`, `find`, `ls`). There is no code-intelligence or code-graph tool on this machine. Use them as you normally would. diff --git a/scripts/ripwire-eval/arms/ripwire.md b/scripts/ripwire-eval/arms/ripwire.md new file mode 100644 index 0000000000..6a5c45aa12 --- /dev/null +++ b/scripts/ripwire-eval/arms/ripwire.md @@ -0,0 +1,24 @@ +TOOLING — this machine has `ripwire`, a code-context tool, installed at: + + RIPWIRE={{RIPWIRE}} + +It builds a ranked, deterministic call graph of the repository and answers questions about it from the shell. Run it as `$RIPWIRE `. The first call indexes the tree (a few seconds); later calls are warm (<1s). + +REACH FOR IT FIRST — it is meant to replace the Read/Grep/Glob reflex, not sit beside it: + +| About to… | Run instead | +|---|---| +| orient yourself in an unfamiliar repo | `$RIPWIRE . ` (ranked map of what matters) | +| grep a concept ("where do we retry") | `$RIPWIRE . --for=""` | +| grep a symbol name | `$RIPWIRE . --for="theExactName"` or `--uses=SYM` | +| read a whole file to understand one function | `$RIPWIRE . --expand=SYM` | +| read several files to learn how something works | `$RIPWIRE . --pack-task=""` (ranking + bodies + callers + tests in ONE budgeted call; add `--token-budget=N`) | +| ask "who calls this / what breaks if I change it" | `$RIPWIRE . --callers=SYM` · `--callees=SYM` · `--impact=SYM` · `--uses=SYM` | +| ask "which tests cover this" | `$RIPWIRE . --affected=F1,F2` · `--exercises=TESTFILE` | +| find an exact literal (error text, config key) | `$RIPWIRE . --grep='literal' --grep-context=2` | +| check a closed claim | `$RIPWIRE . --verify='calls(A,B)'` (also uses/unused/contains/defines/reaches) | +| see how several task symbols relate | `$RIPWIRE . --connect=A,B,C` | +| not sure which verb fits | `$RIPWIRE . --help-task=""` (recommends one command) | +| a symbol you expected is missing from the map | `$RIPWIRE . --skipped` then `--doctor` | + +`$RIPWIRE . --help` lists every verb. You still have Read, Grep, Glob and Bash and may use them, but use ripwire for orientation and localization first — that is what it is for. diff --git a/scripts/ripwire-eval/make-briefs.mjs b/scripts/ripwire-eval/make-briefs.mjs new file mode 100644 index 0000000000..4cafee2a27 --- /dev/null +++ b/scripts/ripwire-eval/make-briefs.mjs @@ -0,0 +1,86 @@ +#!/usr/bin/env node +// Regenerates the exact per-(task, arm) briefs the agent A/B was run from. +// +// The briefs are the experiment's only variable: both arms get the same prose, the same pinned +// clone, the same rules and the same deliverable contract, and differ solely in the tooling +// paragraph read from arms/. Checking the generator in — rather than the 12 rendered files — +// keeps that invariant checkable: a brief that drifts is a diff here, not a silent one. +// +// Usage: node scripts/ripwire-eval/make-briefs.mjs --worktrees= --out= [--ripwire=] + +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { harnessDir, loadTasks, readArgs } from './bench-cli.mjs'; + +const { worktrees, out, ripwire } = readArgs({ + usage: 'make-briefs.mjs --worktrees= --out= [--ripwire=]', + required: ['worktrees', 'out'], + optional: { ripwire: '' }, +}); + +const ARMS = ['baseline', 'ripwire']; + +const BRIEF = `# Change-set localization brief + +You are localizing a change in \`agent-device\`, a large TypeScript monorepo (~4,000 files, ~33,000 +symbols) checked out READ-ONLY at: + + {{WORKTREE}} + +## Rules + +- Work only inside that directory. Do not modify any file. +- Do NOT use git history in any form (\`git log\`, \`git show\`, \`git blame\`, \`git diff\`, \`git grep\` + over refs). The answer is not in this repository's history and using it is cheating. +- Do NOT implement the change. Your deliverable is the CHANGE SET only. +- \`AGENTS.md\` at the repo root documents the project's own routing conventions if you want it. +- Work efficiently: a real agent pays for every byte it reads. Stop when you are confident, not + when you are exhaustive. + +## Tooling + +{{ARM}} + +## Task + +{{PROMPT}} + +## Deliverable + +End your final message with exactly one fenced \`\`\`json block and nothing after it: + +{"files": ["repo-relative/path.ts", "..."], + "new_files": ["repo-relative/path.ts", "..."], + "files_opened": ["every file you read any part of, repo-relative"], + "tool_calls": 0, + "notes": "one sentence on how you found them"} + +- \`files\` = existing files that must be MODIFIED. \`new_files\` = files that must be CREATED. +- Include only source, test, fixture and script files. Do NOT include CHANGELOG.md or anything + under \`website/\`. +- Precision counts as much as recall. Do not pad the list with plausible-but-untouched files. +- \`tool_calls\` must be your honest total count of tool invocations. +`; + +const armText = Object.fromEntries( + ARMS.map((arm) => [ + arm, + readFileSync(join(harnessDir, 'arms', `${arm}.md`), 'utf8') + .trim() + .replaceAll('{{RIPWIRE}}', ripwire), + ]), +); + +mkdirSync(out, { recursive: true }); +for (const task of loadTasks()) { + for (const arm of ARMS) { + const path = join(out, `${task.id}-${arm}.md`); + writeFileSync( + path, + BRIEF.replace('{{WORKTREE}}', join(worktrees, task.id)) + .replace('{{ARM}}', armText[arm]) + .replace('{{PROMPT}}', task.prompt), + ); + console.log(path); + } +} diff --git a/scripts/ripwire-eval/retrieval-bench.mjs b/scripts/ripwire-eval/retrieval-bench.mjs index 218824caa0..3e626fb68c 100644 --- a/scripts/ripwire-eval/retrieval-bench.mjs +++ b/scripts/ripwire-eval/retrieval-bench.mjs @@ -6,6 +6,11 @@ // real change touched, and what does the answer cost? Ranks come from the order paths first // appear in ripwire's output, which is its own ranking order. // +// Recall here is over the change's EXISTING files only (`ground_truth`), not the whole change set: +// a retrieval verb ranks what the tree contains, so a file the commit created is not a hit it +// could have scored. The agent A/B scores the whole set, added files included, and the two +// denominators are therefore different on purpose. +// // Usage: node scripts/ripwire-eval/retrieval-bench.mjs --ripwire= --worktrees= [--out=] import { writeFileSync } from 'node:fs'; @@ -73,6 +78,7 @@ for (const task of loadTasks()) { est_tokens: Math.round(call.bytes / 4), paths_mentioned: ranks.size, ground_truth: task.ground_truth.length, + ground_truth_basis: 'existing-files-only', hits: found.length, recall: Number((found.length / task.ground_truth.length).toFixed(3)), best_rank: found.length ? Math.min(...found.map((hit) => hit.rank)) : null, diff --git a/scripts/ripwire-eval/retrieval-results.json b/scripts/ripwire-eval/retrieval-results.json index 17ddad7ebd..55e283bcc1 100644 --- a/scripts/ripwire-eval/retrieval-results.json +++ b/scripts/ripwire-eval/retrieval-results.json @@ -1,15 +1,16 @@ { - "generated": "2026-09-08T12:57:39.274Z", + "generated": "2026-09-08T13:06:03.406Z", "results": [ { "task": "T1", "verb": "for", "failed": null, - "ms": 1056, + "ms": 5252, "bytes": 10354, "est_tokens": 2589, "paths_mentioned": 43, "ground_truth": 16, + "ground_truth_basis": "existing-files-only", "hits": 5, "recall": 0.313, "best_rank": 2, @@ -84,11 +85,12 @@ "task": "T1", "verb": "pack-task", "failed": null, - "ms": 931, + "ms": 4661, "bytes": 12413, "est_tokens": 3103, "paths_mentioned": 20, "ground_truth": 16, + "ground_truth_basis": "existing-files-only", "hits": 4, "recall": 0.25, "best_rank": 2, @@ -163,11 +165,12 @@ "task": "T1", "verb": "pack-task-4k", "failed": null, - "ms": 916, + "ms": 869, "bytes": 8821, "est_tokens": 2205, "paths_mentioned": 13, "ground_truth": 16, + "ground_truth_basis": "existing-files-only", "hits": 2, "recall": 0.125, "best_rank": 2, @@ -242,11 +245,12 @@ "task": "T1", "verb": "for-idents", "failed": null, - "ms": 974, - "bytes": 10614, - "est_tokens": 2654, + "ms": 947, + "bytes": 10613, + "est_tokens": 2653, "paths_mentioned": 45, "ground_truth": 16, + "ground_truth_basis": "existing-files-only", "hits": 4, "recall": 0.25, "best_rank": 3, @@ -321,11 +325,12 @@ "task": "T2", "verb": "for", "failed": null, - "ms": 996, + "ms": 5186, "bytes": 10296, "est_tokens": 2574, "paths_mentioned": 43, "ground_truth": 7, + "ground_truth_basis": "existing-files-only", "hits": 2, "recall": 0.286, "best_rank": 1, @@ -364,11 +369,12 @@ "task": "T2", "verb": "pack-task", "failed": null, - "ms": 873, + "ms": 4583, "bytes": 11590, "est_tokens": 2898, "paths_mentioned": 10, "ground_truth": 7, + "ground_truth_basis": "existing-files-only", "hits": 1, "recall": 0.143, "best_rank": 1, @@ -407,11 +413,12 @@ "task": "T2", "verb": "pack-task-4k", "failed": null, - "ms": 913, + "ms": 951, "bytes": 8137, "est_tokens": 2034, "paths_mentioned": 5, "ground_truth": 7, + "ground_truth_basis": "existing-files-only", "hits": 1, "recall": 0.143, "best_rank": 1, @@ -450,11 +457,12 @@ "task": "T2", "verb": "for-idents", "failed": null, - "ms": 949, + "ms": 875, "bytes": 10653, "est_tokens": 2663, "paths_mentioned": 37, "ground_truth": 7, + "ground_truth_basis": "existing-files-only", "hits": 3, "recall": 0.429, "best_rank": 1, @@ -493,11 +501,12 @@ "task": "T3", "verb": "for", "failed": null, - "ms": 1011, + "ms": 4943, "bytes": 9748, "est_tokens": 2437, "paths_mentioned": 34, "ground_truth": 5, + "ground_truth_basis": "existing-files-only", "hits": 1, "recall": 0.2, "best_rank": 8, @@ -528,11 +537,12 @@ "task": "T3", "verb": "pack-task", "failed": null, - "ms": 988, + "ms": 4737, "bytes": 9695, "est_tokens": 2424, "paths_mentioned": 8, "ground_truth": 5, + "ground_truth_basis": "existing-files-only", "hits": 0, "recall": 0, "best_rank": null, @@ -563,11 +573,12 @@ "task": "T3", "verb": "pack-task-4k", "failed": null, - "ms": 949, + "ms": 981, "bytes": 8476, "est_tokens": 2119, "paths_mentioned": 7, "ground_truth": 5, + "ground_truth_basis": "existing-files-only", "hits": 0, "recall": 0, "best_rank": null, @@ -598,11 +609,12 @@ "task": "T3", "verb": "for-idents", "failed": null, - "ms": 978, + "ms": 1016, "bytes": 10718, "est_tokens": 2680, "paths_mentioned": 41, "ground_truth": 5, + "ground_truth_basis": "existing-files-only", "hits": 1, "recall": 0.2, "best_rank": 1, @@ -633,11 +645,12 @@ "task": "T4", "verb": "for", "failed": null, - "ms": 973, + "ms": 5223, "bytes": 10118, "est_tokens": 2530, "paths_mentioned": 41, "ground_truth": 7, + "ground_truth_basis": "existing-files-only", "hits": 3, "recall": 0.429, "best_rank": 1, @@ -676,11 +689,12 @@ "task": "T4", "verb": "pack-task", "failed": null, - "ms": 958, + "ms": 4522, "bytes": 12724, "est_tokens": 3181, "paths_mentioned": 19, "ground_truth": 7, + "ground_truth_basis": "existing-files-only", "hits": 5, "recall": 0.714, "best_rank": 1, @@ -719,11 +733,12 @@ "task": "T4", "verb": "pack-task-4k", "failed": null, - "ms": 1009, + "ms": 960, "bytes": 9044, "est_tokens": 2261, "paths_mentioned": 15, "ground_truth": 7, + "ground_truth_basis": "existing-files-only", "hits": 5, "recall": 0.714, "best_rank": 1, @@ -762,11 +777,12 @@ "task": "T4", "verb": "for-idents", "failed": null, - "ms": 1020, + "ms": 914, "bytes": 9641, "est_tokens": 2410, "paths_mentioned": 44, "ground_truth": 7, + "ground_truth_basis": "existing-files-only", "hits": 2, "recall": 0.286, "best_rank": 13, @@ -805,11 +821,12 @@ "task": "T5", "verb": "for", "failed": null, - "ms": 1041, + "ms": 4826, "bytes": 10059, "est_tokens": 2515, "paths_mentioned": 35, "ground_truth": 4, + "ground_truth_basis": "existing-files-only", "hits": 2, "recall": 0.5, "best_rank": 1, @@ -836,11 +853,12 @@ "task": "T5", "verb": "pack-task", "failed": null, - "ms": 939, + "ms": 4562, "bytes": 13150, "est_tokens": 3288, "paths_mentioned": 28, "ground_truth": 4, + "ground_truth_basis": "existing-files-only", "hits": 3, "recall": 0.75, "best_rank": 1, @@ -867,11 +885,12 @@ "task": "T5", "verb": "pack-task-4k", "failed": null, - "ms": 888, + "ms": 948, "bytes": 9018, "est_tokens": 2255, "paths_mentioned": 14, "ground_truth": 4, + "ground_truth_basis": "existing-files-only", "hits": 3, "recall": 0.75, "best_rank": 1, @@ -898,11 +917,12 @@ "task": "T5", "verb": "for-idents", "failed": null, - "ms": 922, + "ms": 959, "bytes": 10785, "est_tokens": 2696, "paths_mentioned": 39, "ground_truth": 4, + "ground_truth_basis": "existing-files-only", "hits": 2, "recall": 0.5, "best_rank": 3, @@ -929,11 +949,12 @@ "task": "T6", "verb": "for", "failed": null, - "ms": 1016, + "ms": 5362, "bytes": 10390, "est_tokens": 2598, "paths_mentioned": 33, "ground_truth": 3, + "ground_truth_basis": "existing-files-only", "hits": 0, "recall": 0, "best_rank": null, @@ -956,11 +977,12 @@ "task": "T6", "verb": "pack-task", "failed": null, - "ms": 965, + "ms": 4830, "bytes": 11137, "est_tokens": 2784, "paths_mentioned": 7, "ground_truth": 3, + "ground_truth_basis": "existing-files-only", "hits": 0, "recall": 0, "best_rank": null, @@ -983,11 +1005,12 @@ "task": "T6", "verb": "pack-task-4k", "failed": null, - "ms": 994, + "ms": 996, "bytes": 8256, "est_tokens": 2064, "paths_mentioned": 3, "ground_truth": 3, + "ground_truth_basis": "existing-files-only", "hits": 0, "recall": 0, "best_rank": null, @@ -1010,11 +1033,12 @@ "task": "T6", "verb": "for-idents", "failed": null, - "ms": 1013, + "ms": 1026, "bytes": 10545, "est_tokens": 2636, "paths_mentioned": 43, "ground_truth": 3, + "ground_truth_basis": "existing-files-only", "hits": 1, "recall": 0.333, "best_rank": 8, diff --git a/scripts/ripwire-eval/setup-clones.sh b/scripts/ripwire-eval/setup-clones.sh new file mode 100755 index 0000000000..ba753fd4bc --- /dev/null +++ b/scripts/ripwire-eval/setup-clones.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Cuts one leak-free clone per task, pinned at the commit's PARENT, and proves the fix commit is +# unreachable from each. +# +# A shallow fetch of the parent SHA is what makes the clone leak-free: a `git worktree` shares +# .git with the main checkout, so the commit the agent is asked to predict would be one +# `git log --all` away. The fetch must name the FULL sha — git refuses to fetch an abbreviated +# one — which is why tasks.json carries full SHAs. +# +# Usage: scripts/ripwire-eval/setup-clones.sh [] (default: /tmp/rw) + +set -euo pipefail + +dest="${1:-/tmp/rw}" +here="$(cd "$(dirname "$0")" && pwd)" +repo="$(git -C "$here" rev-parse --show-toplevel)" + +read_tasks() { + node -e ' + const tasks = require(process.argv[1]).tasks; + for (const t of tasks) console.log([t.id, t.parent, t.commit].join(" ")); + ' "$here/tasks.json" +} + +while read -r id parent commit; do + rm -rf "${dest:?}/$id" + mkdir -p "$dest/$id" + git -C "$dest/$id" init -q + git -C "$dest/$id" remote add origin "$repo" + git -C "$dest/$id" fetch -q --no-tags --depth=20 origin "$parent" + git -C "$dest/$id" checkout -q --detach "$parent" + + if git -C "$dest/$id" cat-file -e "$commit^{commit}" 2>/dev/null; then + echo "$id LEAKS ${commit:0:9} — refusing to hand an agent the answer" >&2 + exit 1 + fi + echo "$id pinned at ${parent:0:9}, ${commit:0:9} unreachable" +done < <(read_tasks) diff --git a/scripts/ripwire-eval/tasks.json b/scripts/ripwire-eval/tasks.json index 8d60d88197..3da10de2e7 100644 --- a/scripts/ripwire-eval/tasks.json +++ b/scripts/ripwire-eval/tasks.json @@ -3,8 +3,8 @@ "tasks": [ { "id": "T1", - "commit": "d11c8cf", - "parent": "1f9d940", + "commit": "d11c8cf9d66917d34aece8b3f75d64beae0c107a", + "parent": "1f9d940bff4a8d28c83f5c5cf55f99e4402b5f0d", "area": "packages/maestro + daemon adapter", "prompt": "Maestro YAML flows can express a standalone `- clearState` or `- clearState: ` command, which clears an app's state WITHOUT relaunching it (unlike `launchApp` with `clearState: true`, which clears then opens). agent-device currently rejects it with `Maestro command \"clearState\" is not supported`. Add support: accept both forms in the flow parser, carry the command through the intermediate representation, and project it onto the daemon's `settings clear-app-state` operation. Conformance corpus and support-matrix bookkeeping count as part of the change.", "ground_truth": [ @@ -30,8 +30,8 @@ }, { "id": "T2", - "commit": "f30328d", - "parent": "e0f8c55", + "commit": "f30328d086f43c4e51ec04a2487e0a5e343c81f1", + "parent": "e0f8c55f6e091b1757e54600fed71fdef015a12e", "area": "platform-android + kernel + daemon views", "prompt": "On Android, `get attrs --level digest` drops the editable-field observation metadata (`editable`, `password`, `hintShowing`, `selectionStart`, `selectionEnd`) that the full level keeps, so those facts vanish on the token-cheap route. Separately, the snapshot helper emits selection offsets only for nodes reported as editable, but read-only selectable text also exposes a selection. Fix both: keep the field facts on the digest route, and emit each nonnegative selection offset independently of editability (-1 stays absent).", "ground_truth": [ @@ -50,8 +50,8 @@ }, { "id": "T3", - "commit": "64b7cc4", - "parent": "7bcbf13", + "commit": "64b7cc45d45bc8044913534318a02efdcf264763", + "parent": "7bcbf1350b5a7c4b4c89ec06d56693c66b236707", "area": "platform-android input actions + provider scenarios", "prompt": "The Android `orientation` command writes the `accelerometer_rotation` and `user_rotation` settings and returns immediately, while the display actually rotates some time later. On a loaded emulator that takes seconds and accessibility reads hang meanwhile, so a `wait` issued right after `orientation` times out. Make the command poll `dumpsys display` for `mCurrentOrientation` until it matches the requested rotation before returning, each probe bounded by what is left of the settle budget. A display that never reaches the rotation fails the command with the observed rotation; a display that reports no rotation at all is left to the setting as before.", "ground_truth": [ @@ -66,8 +66,8 @@ }, { "id": "T4", - "commit": "367e795", - "parent": "a9283fa", + "commit": "367e795ee72458ef2a340a627e5e9a1680c03712", + "parent": "a9283fabc7daaec8de41d275cb01d1b3903f4e88", "area": "daemon HTTP server + tenant scope", "prompt": "`GET /sessions//requests//diagnostics` applies the `:` session-name prefix rule to every caller carrying a tenant. But that prefix is only written under tenant isolation, which the daemon forces exactly when the auth hook ATTESTS the tenant. A client whose tenant is only DECLARED (the `x-agent-device-tenant` header on a daemon with no auth hook) therefore runs in a plain session such as `default` or `cwd::default`, and is then refused 401 when reading the diagnostics record its own failed command wrote — directly and through `agent-device proxy`. Fix the addressability rule so the prefix rule applies only where the caller's session namespace is actually partitioned, and keep the attested case unchanged (an attested tenant is still refused any session outside its own prefix, with the same typed UNAUTHORIZED error).", "ground_truth": [ @@ -86,8 +86,8 @@ }, { "id": "T5", - "commit": "a78b6bb", - "parent": "ff59309", + "commit": "a78b6bb9a3495b727dbdebd0c004d855a7408b54", + "parent": "ff59309415e7a2c0dcc33247d6818bdc6e708477", "area": "commands/interaction wait runtime", "prompt": "A `wait` timeout reports only `reason`, `readableCaptures`, and `waitedMs`, so a failure cannot say where its budget went — a 10s budget spent on one slow poll reports the same `wait_capture_stalled` as a dead runner. Add a per-poll timeline to the timeout failure details: alongside the unchanged reason and request-log link, carry `captures` and `polls[]`, one entry per poll with a start offset on the wait's own clock, a duration, and a typed outcome (readable, unreadable, deadline, runner-restart). Keep the response compact on long waits by retaining only the first five and last twenty-five polls. The replayed-selector landmark-mismatch refusal should carry the same evidence.", "ground_truth": [ @@ -101,8 +101,8 @@ }, { "id": "T6", - "commit": "835af32", - "parent": "6768a04", + "commit": "835af32577c93869dba96a46df8e7e5d1b64fef1", + "parent": "6768a045a2d06434df51b62fb5ef5d18b0335d49", "area": "platform-apple snapshot target", "prompt": "Every eligible iOS Simulator capture resolves its AX-bridge target first, and a cache miss spawns `simctl launchctl list` through xcrun with a 3s timeout on the capture's own critical path. On a loaded host that spawn takes longer than 3s, the timeout is not remembered, and the next capture pays it again — a `wait` issued right after `open` loses its whole budget that way. Make discovery single-flight and detached from the capture that starts it: a capture waits a bounded 1.5s for it, then takes the XCTest fallback while the probe keeps running under its own longer budget, and later captures join the in-flight probe or reuse its result. One deadline covers the whole discovery, not each subprocess, and the resolver's error names its reason so the route diagnostic says why the fallback ran.", "ground_truth": [ From 56d10df036032dff1ac01f87907ddb1708d8636c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 13:08:40 +0000 Subject: [PATCH 07/10] chore(gates): declare make-briefs.mjs as a fallow entry point Same reason as its sibling benches: an operator-run CLI nothing imports. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01F7vMn8ehPq3NTPYMfxs7ro --- .fallowrc.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.fallowrc.json b/.fallowrc.json index 70937334b5..d1cf6c6041 100644 --- a/.fallowrc.json +++ b/.fallowrc.json @@ -26,6 +26,7 @@ "scripts/ripwire-eval/retrieval-bench.mjs", "scripts/ripwire-eval/affected-bench.mjs", "scripts/ripwire-eval/score.mjs", + "scripts/ripwire-eval/make-briefs.mjs", "packages/capture-kit/src/ios-snapshot-engine/replay.ts", // #1596 regression fixture: runs as a real `node --experimental-strip-types` // subprocess (test/integration/daemon-replace-exit-flush.test.ts), so From 6ed849f6aecd98030f38b5d44cf32ae5eb81dcd7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 15:01:48 +0000 Subject: [PATCH 08/10] fix(eval): seed --affected from every source language, not just JS/TS Review follow-up on #2400. The seed predicate required a .ts/.mjs extension, so T2's AccessibilityTreeXml.java was dropped and the walk started from five of the change's six sources while the report claimed all non-test sources. ripwire builds a call graph for Java here, so the fix is to seed from it rather than relabel the benchmark: the seed set is now the code languages ripwire parses, with data files still excluded because they carry no call edges. T2 goes from 5 to 6 seeds; its selected set and every task's score are unchanged, so the 9/12 result stands. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01F7vMn8ehPq3NTPYMfxs7ro --- docs/ripwire-context-tooling-evaluation.md | 7 ++++--- scripts/ripwire-eval/affected-bench.mjs | 8 +++++++- scripts/ripwire-eval/affected-results.json | 18 +++++++++--------- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/docs/ripwire-context-tooling-evaluation.md b/docs/ripwire-context-tooling-evaluation.md index 1ae06b3efc..20168f4b58 100644 --- a/docs/ripwire-context-tooling-evaluation.md +++ b/docs/ripwire-context-tooling-evaluation.md @@ -108,8 +108,9 @@ route, so when it finds anything it ranks it near the top. This repository's rule is that tests mirror source one-to-one, which makes "I changed these sources, which tests do I run" a question with a checkable answer. Each task's non-test -ground-truth files were fed to `--affected`; the score is whether the commit's own test files came -back. +ground-truth files were fed to `--affected` — every one ripwire builds a call graph for, this +change set's Android helper Java included, not just its TypeScript; the score is whether the +commit's own test files came back. Two exclusions keep the denominator honest. Files the commit *created* are out — a selector cannot name a file that does not exist. And only **test files** (`*.test.ts`) count: a file that merely @@ -121,7 +122,7 @@ under `helpers_not_scored`. | Task | Expected tests found | Tests selected | Helpers not scored | Bytes | | --- | --- | --- | --- | --- | | T1 | 2 / 4 | 65 | 1 | 9.0 KB | -| T2 | 1 / 1 | 185 | 0 | 19.6 KB | +| T2 | 1 / 1 | 185 | 0 | 19.7 KB | | T3 | 1 / 1 | 5 | 3 | 2.4 KB | | T4 | 2 / 2 | 24 | 1 | 4.4 KB | | T5 | 2 / 2 | 18 | 0 | 3.7 KB | diff --git a/scripts/ripwire-eval/affected-bench.mjs b/scripts/ripwire-eval/affected-bench.mjs index 4d0ae3d714..e02a973442 100644 --- a/scripts/ripwire-eval/affected-bench.mjs +++ b/scripts/ripwire-eval/affected-bench.mjs @@ -25,7 +25,13 @@ const { ripwire, worktrees, out } = readArgs({ const isTestFile = (path) => /\.test\.[cm]?[jt]sx?$/.test(path); const inTestLocation = (path) => /(^|\/)(__tests__|test)\//.test(path); const isHelper = (path) => !isTestFile(path) && inTestLocation(path); -const isSource = (path) => !isTestFile(path) && !isHelper(path) && /\.[cm]?[jt]s$/.test(path); +// Seeds are code files in a language ripwire builds a call graph for — this repository's ground +// truth reaches TypeScript, .mjs and the Android helper's Java, and a walk seeded from only some +// of a change's sources measures less than the change. Data files (.json, .yaml) carry no call +// edges and are never seeds. +const SEED_EXTENSIONS = + /\.(?:[cm]?[jt]sx?|java|swift|kt|mm?|c|cc|cpp|h|hpp|py|go|rs|rb|php|cs|sh)$/; +const isSource = (path) => !isTestFile(path) && !isHelper(path) && SEED_EXTENSIONS.test(path); const results = []; diff --git a/scripts/ripwire-eval/affected-results.json b/scripts/ripwire-eval/affected-results.json index a399c70335..6101be4c1d 100644 --- a/scripts/ripwire-eval/affected-results.json +++ b/scripts/ripwire-eval/affected-results.json @@ -1,10 +1,10 @@ { - "generated": "2026-09-08T13:06:07.464Z", + "generated": "2026-09-08T15:01:05.415Z", "results": [ { "task": "T1", "failed": null, - "ms": 717, + "ms": 5302, "bytes": 8998, "seeds": 10, "selected": 65, @@ -22,9 +22,9 @@ { "task": "T2", "failed": null, - "ms": 649, - "bytes": 19606, - "seeds": 5, + "ms": 2560, + "bytes": 19711, + "seeds": 6, "selected": 185, "expected": 1, "expected_added_not_scorable": 1, @@ -37,7 +37,7 @@ { "task": "T3", "failed": null, - "ms": 655, + "ms": 2807, "bytes": 2447, "seeds": 1, "selected": 5, @@ -56,7 +56,7 @@ { "task": "T4", "failed": null, - "ms": 642, + "ms": 3161, "bytes": 4380, "seeds": 4, "selected": 24, @@ -71,7 +71,7 @@ { "task": "T5", "failed": null, - "ms": 667, + "ms": 3278, "bytes": 3652, "seeds": 2, "selected": 18, @@ -86,7 +86,7 @@ { "task": "T6", "failed": null, - "ms": 650, + "ms": 2886, "bytes": 2123, "seeds": 1, "selected": 2, From 0e86a2dc98586145af21eb58644ebdbf51378cb8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 16:05:08 +0000 Subject: [PATCH 09/10] fix(eval): drop Kotlin from the --affected seed allowlist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-up on #2400. The allowlist claimed Kotlin as a language ripwire builds a call graph for, but it is one of the two documented parser gaps in this tree — the Maestro conformance JVM harness is dark to the graph, as the report itself says. Seeding from a file ripwire cannot parse would have measured an empty walk as a real one. No task in the set carries a Kotlin source, so every published number is unchanged; the results file is regenerated to match the script that produced it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01F7vMn8ehPq3NTPYMfxs7ro --- scripts/ripwire-eval/affected-bench.mjs | 6 +++--- scripts/ripwire-eval/affected-results.json | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/scripts/ripwire-eval/affected-bench.mjs b/scripts/ripwire-eval/affected-bench.mjs index e02a973442..8e607d10e2 100644 --- a/scripts/ripwire-eval/affected-bench.mjs +++ b/scripts/ripwire-eval/affected-bench.mjs @@ -28,9 +28,9 @@ const isHelper = (path) => !isTestFile(path) && inTestLocation(path); // Seeds are code files in a language ripwire builds a call graph for — this repository's ground // truth reaches TypeScript, .mjs and the Android helper's Java, and a walk seeded from only some // of a change's sources measures less than the change. Data files (.json, .yaml) carry no call -// edges and are never seeds. -const SEED_EXTENSIONS = - /\.(?:[cm]?[jt]sx?|java|swift|kt|mm?|c|cc|cpp|h|hpp|py|go|rs|rb|php|cs|sh)$/; +// edges and are never seeds, and neither is Kotlin: ripwire does not parse it, so the Maestro +// conformance JVM harness is dark to the graph (docs/ripwire-context-tooling-evaluation.md). +const SEED_EXTENSIONS = /\.(?:[cm]?[jt]sx?|java|swift|mm?|c|cc|cpp|h|hpp|py|go|rs|rb|php|cs|sh)$/; const isSource = (path) => !isTestFile(path) && !isHelper(path) && SEED_EXTENSIONS.test(path); const results = []; diff --git a/scripts/ripwire-eval/affected-results.json b/scripts/ripwire-eval/affected-results.json index 6101be4c1d..6503cabef3 100644 --- a/scripts/ripwire-eval/affected-results.json +++ b/scripts/ripwire-eval/affected-results.json @@ -1,10 +1,10 @@ { - "generated": "2026-09-08T15:01:05.415Z", + "generated": "2026-09-08T16:04:40.961Z", "results": [ { "task": "T1", "failed": null, - "ms": 5302, + "ms": 3972, "bytes": 8998, "seeds": 10, "selected": 65, @@ -22,7 +22,7 @@ { "task": "T2", "failed": null, - "ms": 2560, + "ms": 2757, "bytes": 19711, "seeds": 6, "selected": 185, @@ -37,7 +37,7 @@ { "task": "T3", "failed": null, - "ms": 2807, + "ms": 2484, "bytes": 2447, "seeds": 1, "selected": 5, @@ -56,7 +56,7 @@ { "task": "T4", "failed": null, - "ms": 3161, + "ms": 2524, "bytes": 4380, "seeds": 4, "selected": 24, @@ -71,7 +71,7 @@ { "task": "T5", "failed": null, - "ms": 3278, + "ms": 2840, "bytes": 3652, "seeds": 2, "selected": 18, @@ -86,7 +86,7 @@ { "task": "T6", "failed": null, - "ms": 2886, + "ms": 2179, "bytes": 2123, "seeds": 1, "selected": 2, From f868f0c0c70521ecfceafd30874e02dbfc3b946d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 06:25:49 +0000 Subject: [PATCH 10/10] fix(eval): narrow the conclusions to the invocations actually tested MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-up on #2400. Three claims went further than the evidence. - The report recommended waiting for a terse output mode. --legend=compact is already documented at the tested revision. Measured: it costs no recall and saves 55% on --callers, 10% on --affected, 1% on --for. The arms ran on default output, and their calls were mostly --for-shaped, so compact narrows the +10% token gap rather than closing it — stated as the open question it is rather than a reason to defer. retrieval-bench gains a for-compact variant so the comparison is reproducible. - The CLI-flag head-to-head tested --for and --pack-task, which reach 1-3 of the five documented declaration sites, and concluded a call graph cannot recover a convention. --recall, the verb built for document questions, finds all five. That conclusion was an artifact of the verb chosen and is withdrawn. - --affected scored a precision figure against the historical commit's file list. It answers transitive reach, so a test it names that the commit left alone is not a false positive and that list is not a precision oracle. The field is removed; recall stands, and selection breadth is reported as read cost rather than a defect. Recall and every A/B number are unchanged; the corrections are to framing, one withdrawn conclusion, and one removed metric. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01F7vMn8ehPq3NTPYMfxs7ro --- docs/ripwire-context-tooling-evaluation.md | 96 +++--- scripts/ripwire-eval/affected-bench.mjs | 8 +- scripts/ripwire-eval/affected-results.json | 20 +- scripts/ripwire-eval/retrieval-bench.mjs | 4 + scripts/ripwire-eval/retrieval-results.json | 314 ++++++++++++++++++-- 5 files changed, 362 insertions(+), 80 deletions(-) diff --git a/docs/ripwire-context-tooling-evaluation.md b/docs/ripwire-context-tooling-evaluation.md index 20168f4b58..3dedc01578 100644 --- a/docs/ripwire-context-tooling-evaluation.md +++ b/docs/ripwire-context-tooling-evaluation.md @@ -26,11 +26,15 @@ What did show up, and is worth keeping in view: 1. **It is consistent where grep is lucky.** On the one task with a non-obvious touch point — a scripted provider fake that throws on unscripted calls — both ripwire runs found it and only one of two baseline runs did. -2. **The token cost is a fixable implementation detail, not a design limit.** ripwire's - self-documenting preamble is a *fixed* 1.6–3.1 KB per invocation, up to 62% of a small verb's - whole response, re-sent on every call. A terse mode would likely flip the token column. -3. **The cheap deterministic verbs stand on their own.** `--recall` answers from 799 KB of markdown - in 15 KB; `--affected` names the right test files at 2–5 KB when the seed set is narrow. +2. **Part of the token cost is already avoidable today.** ripwire's self-documenting preamble is a + *fixed* 1.6–3.1 KB per invocation, up to 62% of a small verb's whole response. The documented + `--legend=compact` removes most of it — but by verb: **−55% on `--callers`, −10% on `--affected`, + −1% on `--for`**. The A/B arm's calls were mostly `--for`-shaped, so compact would not have + closed the +10% gap; it would have helped an agent that leaned on the narrow verbs. +3. **The document verbs are the strongest thing here.** `--recall` answers from 799 KB of markdown + in 15 KB, and it finds all five CLI-flag declaration sites this repo documents by hand — where + the graph verbs find one to three. `--affected` recalls 9 of 12 touched test files at 2–5 KB + when the seed set is narrow. ## What was measured @@ -133,10 +137,16 @@ Both misses are real. T1's are two `packages/maestro` harnesses the walk did not `snapshot-route.test.ts`, which covers the direct caller of the changed module and should have been a short hop. -Selection breadth is the sharper problem. T2 named **185** test files for a 5-file change: the -seeds reach into `packages/kernel`, whose symbols are called from everywhere, and the walk has no -notion of "this hub is not evidence". At that width the answer costs more to read than it saves. -`--affected` is useful here at 2–24 selected files and not useful at 185. +**Recall only, deliberately.** `--affected` answers "which tests can transitively reach this +change" — a strictly larger set than "which tests this commit happened to edit". A test it names +that the commit left alone is not a false positive, so the commit's file list cannot score +precision, and an earlier draft of this report was wrong to treat the extra names as excess. +`selected` below is breadth — what it would cost to read or run the set — not a quality score. + +Breadth still varies enough to matter in practice. T2 named **185** test files for a 5-file change, +because its seeds reach into `packages/kernel`, whose symbols are called from everywhere. That is a +correct transitive answer and an expensive one; at 2–24 files the same verb is cheap to act on. +Whether the wide answer is *useful* is a judgement about read cost, not a measured defect. This does not overlap `pnpm check:affected`, which selects CI *lanes* from a diff. `--affected` selects test *files* from source files. They answer different questions. @@ -231,36 +241,38 @@ repository, ripwire's self-documenting XML comment preamble is **1.6 KB on `--fo hardest on exactly the cheap, narrow verbs that should be the tool's best value. Whole-file reads went down; total context did not. -This is the single most actionable finding here, and it is a fixable one: the preamble is -documentation aimed at a first-time reader, re-sent to an agent that has already read it. A -`--terse` mode that emits the header once per session — or not at all — would likely flip the token -column without touching the ranking. +**How much of this is avoidable today.** ripwire already ships `--legend=compact` (documented in +its `docs/COMMANDS.md`); the arms were run on the default legend, so the +10% above is a +default-output number. Measured on this repository, compact costs nothing in recall and saves +**55% on `--callers`, 10% on `--affected`, and 1% on `--for`** — it strips the whole preamble from +the narrow verbs and only part of it from `--for`, whose bulk is ranked rows, not header. Since the +ripwire arm's calls were mostly `--for`/`--pack-task`-shaped, compact would have trimmed the gap +rather than closed it. Re-running the A/B under `--legend=compact` is the open question this +evaluation does not answer. ## 4. The one question this repo has already answered in prose `docs/agents/cli-flags.md` names, by hand, the declaration sites a new CLI flag must be threaded -through. That makes it the cleanest possible head-to-head between a call graph and a maintained -routing doc. Asked the same question, one ripwire call names: - -| Declaration site (from `docs/agents/cli-flags.md`) | `--for=""` | `--for=""` | -| --- | --- | --- | -| `packages/contracts/src/cli-flags.ts` | — | yes | -| `src/commands/cli-grammar/*` | yes | yes | -| `src/commands/command-projection.ts` | — | — | -| `src/cli-schema/command-overrides.ts` | — | — | -| `src/cli-schema/cli-config.ts` | — | yes | - -`--pack-task --partition=3`, the verb aimed at fanning work out to parallel agents, produces three -slices with `overlap_max=0.000` in 1.3 s and 25 KB total — a clean split, naming 44 files, 2 of -these 5 sites among them. - -**A ranked call graph does not recover a convention.** These sites are related by a rule the team -wrote down, not by call edges: `PROJECT_CONFIG_FLAG_KEYS` is a positive allowlist, and -`SCHEMA_ONLY_CLI_COMMAND_SCHEMAS` is a merge path. Nothing in the graph says "and also this". The -routing doc stays the better answer to this particular question, and that is the shape of the -boundary — ripwire finds what the code *does*, `AGENTS.md` records what the team *decided*. - +through — the cleanest available head-to-head between a maintained routing doc and the tool. +| Declaration site (from `docs/agents/cli-flags.md`) | `--for` (prose) | `--for` (identifiers) | `--recall` | +| --- | --- | --- | --- | +| `packages/contracts/src/cli-flags.ts` | — | yes | **yes** | +| `src/commands/cli-grammar/*` | yes | yes | **yes** | +| `src/commands/command-projection.ts` | — | — | **yes** | +| `src/cli-schema/command-overrides.ts` | — | — | **yes** | +| `src/cli-schema/cli-config.ts` | — | yes | **yes** | + +**`--recall` finds all five, in 15.7 KB.** That is the verb built for this question: it searches the +written corpus, and the answer to "where does a CLI flag get threaded" lives in a document, not in +call edges. An earlier draft of this report tested only `--for` and `--pack-task`, which reach 1–3 +of the five, and concluded from that "a ranked call graph does not recover a convention". That +conclusion was an artifact of the verb chosen, and is withdrawn. + +What survives is narrower and less interesting: **the graph verbs are the wrong tool for a +convention, and ripwire knows it** — the routing table in its own skills sends this question to +`--recall`. `--pack-task --partition=3`, the fan-out verb, produces three slices with +`overlap_max=0.000` in 1.3 s and 25 KB total, naming 44 files, 2 of these 5 sites among them. ## Adoption cost, if we wanted it @@ -292,13 +304,14 @@ line, sends nothing anywhere and needs no key, so the cost of one engineer tryin The verbs worth trying first here are `--recall` (52× cheaper than the doc corpus it searches) and `--affected` on a narrow seed set. -**Re-run this harness if ripwire ships a terse output mode.** `scripts/ripwire-eval/` is written to -be re-run against a new binary with two commands; the token result is the one number most likely to -move, and it is the one currently deciding the verdict. +**Re-run the A/B under `--legend=compact` before treating the token result as settled.** The arms +ran on default output. Compact is measured above and is worth 55% on `--callers` but only 1% on +`--for`, so it should narrow rather than close the gap — but that is an inference from the +deterministic benches, not a measurement of the agent arm, and the harness exists to settle it. -**Two findings are worth sending upstream**, since both are measured rather than impressionistic: -the fixed preamble cost per invocation, and `--affected` selecting 185 test files for a 5-file -change once its seeds reach a hub module in `packages/kernel`. +**One finding is worth sending upstream:** the fixed preamble is 15% of a `--for` response and 62% +of a `--callers` one, and `--legend=compact` clears it from the latter but not the former — the +default is the expensive one, on the verb agents reach for most. ## Caveats @@ -314,3 +327,6 @@ change once its seeds reach a hub module in `packages/kernel`. than folded into per-run wall clock. - Change-set localization is one job among many. This says nothing about ripwire's refactoring, security or quality lenses beyond the single `--quality-panel` run noted above. +- **Results are bounded by the invocations tested, and the choice of verb changed conclusions.** + The A/B ran on default-legend output; §4's original finding reversed once `--recall` was tried + instead of `--for`. Read every number here as "this verb, this flag set", not "the tool". diff --git a/scripts/ripwire-eval/affected-bench.mjs b/scripts/ripwire-eval/affected-bench.mjs index 8e607d10e2..87854242a5 100644 --- a/scripts/ripwire-eval/affected-bench.mjs +++ b/scripts/ripwire-eval/affected-bench.mjs @@ -6,6 +6,12 @@ // truth files of the real commit and asks whether the commit's own TEST files come back, and at // what cost. // +// RECALL ONLY, deliberately. `--affected` answers "which tests can transitively reach this change", +// which is a strictly larger set than "which tests the commit happened to edit". A test it names +// that the commit left alone is not a false positive — the commit's file list is not a precision +// oracle for a reach query. `selected` is therefore reported as breadth (what it would cost to run +// or read the set), never scored against the ground truth. +// // Usage: node scripts/ripwire-eval/affected-bench.mjs --ripwire= --worktrees= [--out=] import { writeFileSync } from 'node:fs'; @@ -68,8 +74,6 @@ for (const task of loadTasks()) { helpers_not_scored: helpers, hit: hit.length, recall: Number((hit.length / expected.length).toFixed(3)), - // Of the tests it named, how many were actually touched — the cost of running the whole set. - precision: selected.length === 0 ? 0 : Number((hit.length / selected.length).toFixed(3)), missed: expected.filter((path) => !selected.includes(path)), }); process.stderr.write( diff --git a/scripts/ripwire-eval/affected-results.json b/scripts/ripwire-eval/affected-results.json index 6503cabef3..2ab5d43eb1 100644 --- a/scripts/ripwire-eval/affected-results.json +++ b/scripts/ripwire-eval/affected-results.json @@ -1,10 +1,10 @@ { - "generated": "2026-09-08T16:04:40.961Z", + "generated": "2026-09-09T06:24:35.118Z", "results": [ { "task": "T1", "failed": null, - "ms": 3972, + "ms": 543, "bytes": 8998, "seeds": 10, "selected": 65, @@ -13,7 +13,6 @@ "helpers_not_scored": ["packages/maestro/src/internal/__tests__/runtime-port-fixtures.ts"], "hit": 2, "recall": 0.5, - "precision": 0.031, "missed": [ "packages/maestro/src/internal/__tests__/program-ir-parser.test.ts", "packages/maestro/src/internal/__tests__/runtime-port.test.ts" @@ -22,7 +21,7 @@ { "task": "T2", "failed": null, - "ms": 2757, + "ms": 486, "bytes": 19711, "seeds": 6, "selected": 185, @@ -31,13 +30,12 @@ "helpers_not_scored": [], "hit": 1, "recall": 1, - "precision": 0.005, "missed": [] }, { "task": "T3", "failed": null, - "ms": 2484, + "ms": 925, "bytes": 2447, "seeds": 1, "selected": 5, @@ -50,13 +48,12 @@ ], "hit": 1, "recall": 1, - "precision": 0.2, "missed": [] }, { "task": "T4", "failed": null, - "ms": 2524, + "ms": 493, "bytes": 4380, "seeds": 4, "selected": 24, @@ -65,13 +62,12 @@ "helpers_not_scored": ["test/wire-compat/surface.ts"], "hit": 2, "recall": 1, - "precision": 0.083, "missed": [] }, { "task": "T5", "failed": null, - "ms": 2840, + "ms": 475, "bytes": 3652, "seeds": 2, "selected": 18, @@ -80,13 +76,12 @@ "helpers_not_scored": [], "hit": 2, "recall": 1, - "precision": 0.111, "missed": [] }, { "task": "T6", "failed": null, - "ms": 2179, + "ms": 477, "bytes": 2123, "seeds": 1, "selected": 2, @@ -95,7 +90,6 @@ "helpers_not_scored": [], "hit": 1, "recall": 0.5, - "precision": 0.5, "missed": ["packages/platform-apple/src/snapshot-route.test.ts"] } ] diff --git a/scripts/ripwire-eval/retrieval-bench.mjs b/scripts/ripwire-eval/retrieval-bench.mjs index 3e626fb68c..994e40bce1 100644 --- a/scripts/ripwire-eval/retrieval-bench.mjs +++ b/scripts/ripwire-eval/retrieval-bench.mjs @@ -32,6 +32,10 @@ const VERBS = [ }, // Same verb, but fed only the identifiers the task text itself puts in backticks — a mechanical // distillation, not a hand-tuned query. Isolates how much of --for's result is phrasing. + // Same query, with the documented compact legend. Isolates how much of the default output is + // the self-documenting preamble — the answer differs sharply by verb, so it is measured, not + // assumed (docs/COMMANDS.md `--legend=full|compact`). + { id: 'for-compact', args: (task) => ['.', `--for=${task.prompt}`, '--legend=compact'] }, { id: 'for-idents', args: (task) => ['.', `--for=${backtickedTerms(task.prompt)}`], diff --git a/scripts/ripwire-eval/retrieval-results.json b/scripts/ripwire-eval/retrieval-results.json index 55e283bcc1..59efe7e83a 100644 --- a/scripts/ripwire-eval/retrieval-results.json +++ b/scripts/ripwire-eval/retrieval-results.json @@ -1,11 +1,11 @@ { - "generated": "2026-09-08T13:06:03.406Z", + "generated": "2026-09-09T06:24:31.660Z", "results": [ { "task": "T1", "verb": "for", "failed": null, - "ms": 5252, + "ms": 1464, "bytes": 10354, "est_tokens": 2589, "paths_mentioned": 43, @@ -85,7 +85,7 @@ "task": "T1", "verb": "pack-task", "failed": null, - "ms": 4661, + "ms": 1273, "bytes": 12413, "est_tokens": 3103, "paths_mentioned": 20, @@ -165,7 +165,7 @@ "task": "T1", "verb": "pack-task-4k", "failed": null, - "ms": 869, + "ms": 747, "bytes": 8821, "est_tokens": 2205, "paths_mentioned": 13, @@ -241,11 +241,91 @@ } ] }, + { + "task": "T1", + "verb": "for-compact", + "failed": null, + "ms": 1341, + "bytes": 10173, + "est_tokens": 2543, + "paths_mentioned": 47, + "ground_truth": 16, + "ground_truth_basis": "existing-files-only", + "hits": 5, + "recall": 0.313, + "best_rank": 2, + "per_file": [ + { + "path": "packages/maestro/src/internal/__tests__/program-ir-parser.test.ts", + "rank": null + }, + { + "path": "packages/maestro/src/internal/__tests__/runtime-port-fixtures.ts", + "rank": null + }, + { + "path": "packages/maestro/src/internal/__tests__/runtime-port.test.ts", + "rank": null + }, + { + "path": "packages/maestro/src/internal/conformance-normalize.ts", + "rank": null + }, + { + "path": "packages/maestro/src/internal/program-ir-command-parser.ts", + "rank": 18 + }, + { + "path": "packages/maestro/src/internal/program-ir.ts", + "rank": 10 + }, + { + "path": "packages/maestro/src/internal/runtime-port-commands.ts", + "rank": 3 + }, + { + "path": "packages/maestro/src/internal/runtime-port-types.ts", + "rank": null + }, + { + "path": "packages/maestro/src/internal/support-matrix.ts", + "rank": 8 + }, + { + "path": "scripts/fuzz/validation-arbitraries-maestro.ts", + "rank": null + }, + { + "path": "scripts/maestro-conformance/build-manifest.mjs", + "rank": null + }, + { + "path": "scripts/maestro-conformance/corpus/manifest.json", + "rank": null + }, + { + "path": "src/daemon/adapters/maestro/__tests__/daemon-runtime-port.test.ts", + "rank": null + }, + { + "path": "src/daemon/adapters/maestro/__tests__/daemon-runtime-public-operation.test.ts", + "rank": null + }, + { + "path": "src/daemon/adapters/maestro/daemon-runtime-port.ts", + "rank": null + }, + { + "path": "src/daemon/adapters/maestro/daemon-runtime-public-operation.ts", + "rank": 2 + } + ] + }, { "task": "T1", "verb": "for-idents", "failed": null, - "ms": 947, + "ms": 1007, "bytes": 10613, "est_tokens": 2653, "paths_mentioned": 45, @@ -325,7 +405,7 @@ "task": "T2", "verb": "for", "failed": null, - "ms": 5186, + "ms": 695, "bytes": 10296, "est_tokens": 2574, "paths_mentioned": 43, @@ -369,7 +449,7 @@ "task": "T2", "verb": "pack-task", "failed": null, - "ms": 4583, + "ms": 1076, "bytes": 11590, "est_tokens": 2898, "paths_mentioned": 10, @@ -413,7 +493,7 @@ "task": "T2", "verb": "pack-task-4k", "failed": null, - "ms": 951, + "ms": 658, "bytes": 8137, "est_tokens": 2034, "paths_mentioned": 5, @@ -453,11 +533,55 @@ } ] }, + { + "task": "T2", + "verb": "for-compact", + "failed": null, + "ms": 696, + "bytes": 10131, + "est_tokens": 2533, + "paths_mentioned": 46, + "ground_truth": 7, + "ground_truth_basis": "existing-files-only", + "hits": 2, + "recall": 0.286, + "best_rank": 1, + "per_file": [ + { + "path": "android/snapshot-helper/src/main/java/com/callstack/agentdevice/snapshothelper/AccessibilityTreeXml.java", + "rank": null + }, + { + "path": "packages/kernel/src/snapshot.ts", + "rank": null + }, + { + "path": "packages/platform-android/src/ui-hierarchy-builder.ts", + "rank": null + }, + { + "path": "packages/platform-android/src/ui-hierarchy-node.ts", + "rank": null + }, + { + "path": "packages/platform-android/src/ui-hierarchy.ts", + "rank": 42 + }, + { + "path": "src/daemon/__tests__/response-views.test.ts", + "rank": null + }, + { + "path": "src/daemon/response-views.ts", + "rank": 1 + } + ] + }, { "task": "T2", "verb": "for-idents", "failed": null, - "ms": 875, + "ms": 646, "bytes": 10653, "est_tokens": 2663, "paths_mentioned": 37, @@ -501,7 +625,7 @@ "task": "T3", "verb": "for", "failed": null, - "ms": 4943, + "ms": 806, "bytes": 9748, "est_tokens": 2437, "paths_mentioned": 34, @@ -537,7 +661,7 @@ "task": "T3", "verb": "pack-task", "failed": null, - "ms": 4737, + "ms": 1457, "bytes": 9695, "est_tokens": 2424, "paths_mentioned": 8, @@ -573,7 +697,7 @@ "task": "T3", "verb": "pack-task-4k", "failed": null, - "ms": 981, + "ms": 776, "bytes": 8476, "est_tokens": 2119, "paths_mentioned": 7, @@ -605,11 +729,47 @@ } ] }, + { + "task": "T3", + "verb": "for-compact", + "failed": null, + "ms": 690, + "bytes": 9607, + "est_tokens": 2402, + "paths_mentioned": 37, + "ground_truth": 5, + "ground_truth_basis": "existing-files-only", + "hits": 1, + "recall": 0.2, + "best_rank": 8, + "per_file": [ + { + "path": "packages/platform-android/src/__tests__/input-actions.test.ts", + "rank": null + }, + { + "path": "packages/platform-android/src/__tests__/test-utils/fake-adb.ts", + "rank": null + }, + { + "path": "packages/platform-android/src/input-actions.ts", + "rank": 8 + }, + { + "path": "test/integration/provider-scenarios/android-ime-lifecycle-world.ts", + "rank": null + }, + { + "path": "test/integration/provider-scenarios/android-world.ts", + "rank": null + } + ] + }, { "task": "T3", "verb": "for-idents", "failed": null, - "ms": 1016, + "ms": 1044, "bytes": 10718, "est_tokens": 2680, "paths_mentioned": 41, @@ -645,7 +805,7 @@ "task": "T4", "verb": "for", "failed": null, - "ms": 5223, + "ms": 740, "bytes": 10118, "est_tokens": 2530, "paths_mentioned": 41, @@ -689,7 +849,7 @@ "task": "T4", "verb": "pack-task", "failed": null, - "ms": 4522, + "ms": 1428, "bytes": 12724, "est_tokens": 3181, "paths_mentioned": 19, @@ -733,7 +893,7 @@ "task": "T4", "verb": "pack-task-4k", "failed": null, - "ms": 960, + "ms": 730, "bytes": 9044, "est_tokens": 2261, "paths_mentioned": 15, @@ -773,11 +933,55 @@ } ] }, + { + "task": "T4", + "verb": "for-compact", + "failed": null, + "ms": 731, + "bytes": 10059, + "est_tokens": 2515, + "paths_mentioned": 45, + "ground_truth": 7, + "ground_truth_basis": "existing-files-only", + "hits": 3, + "recall": 0.429, + "best_rank": 1, + "per_file": [ + { + "path": "src/daemon/__tests__/http-server-tenant-trust.test.ts", + "rank": null + }, + { + "path": "src/daemon/__tests__/request-diagnostics-http.test.ts", + "rank": null + }, + { + "path": "src/daemon/request-diagnostics-http.ts", + "rank": 1 + }, + { + "path": "src/daemon/server/http-server.ts", + "rank": 5 + }, + { + "path": "src/daemon/server/tenant-trust.ts", + "rank": 4 + }, + { + "path": "src/daemon/session-tenant-scope.ts", + "rank": null + }, + { + "path": "test/wire-compat/surface.ts", + "rank": null + } + ] + }, { "task": "T4", "verb": "for-idents", "failed": null, - "ms": 914, + "ms": 692, "bytes": 9641, "est_tokens": 2410, "paths_mentioned": 44, @@ -821,7 +1025,7 @@ "task": "T5", "verb": "for", "failed": null, - "ms": 4826, + "ms": 746, "bytes": 10059, "est_tokens": 2515, "paths_mentioned": 35, @@ -853,7 +1057,7 @@ "task": "T5", "verb": "pack-task", "failed": null, - "ms": 4562, + "ms": 1010, "bytes": 13150, "est_tokens": 3288, "paths_mentioned": 28, @@ -885,7 +1089,7 @@ "task": "T5", "verb": "pack-task-4k", "failed": null, - "ms": 948, + "ms": 657, "bytes": 9018, "est_tokens": 2255, "paths_mentioned": 14, @@ -913,11 +1117,43 @@ } ] }, + { + "task": "T5", + "verb": "for-compact", + "failed": null, + "ms": 713, + "bytes": 9868, + "est_tokens": 2467, + "paths_mentioned": 39, + "ground_truth": 4, + "ground_truth_basis": "existing-files-only", + "hits": 2, + "recall": 0.5, + "best_rank": 1, + "per_file": [ + { + "path": "src/commands/interaction/runtime/wait-polling.test.ts", + "rank": null + }, + { + "path": "src/commands/interaction/runtime/wait-polling.ts", + "rank": 1 + }, + { + "path": "src/commands/interaction/runtime/wait-selector.test.ts", + "rank": null + }, + { + "path": "src/commands/interaction/runtime/wait-selector.ts", + "rank": 5 + } + ] + }, { "task": "T5", "verb": "for-idents", "failed": null, - "ms": 959, + "ms": 684, "bytes": 10785, "est_tokens": 2696, "paths_mentioned": 39, @@ -949,7 +1185,7 @@ "task": "T6", "verb": "for", "failed": null, - "ms": 5362, + "ms": 716, "bytes": 10390, "est_tokens": 2598, "paths_mentioned": 33, @@ -977,7 +1213,7 @@ "task": "T6", "verb": "pack-task", "failed": null, - "ms": 4830, + "ms": 1369, "bytes": 11137, "est_tokens": 2784, "paths_mentioned": 7, @@ -1005,7 +1241,7 @@ "task": "T6", "verb": "pack-task-4k", "failed": null, - "ms": 996, + "ms": 1087, "bytes": 8256, "est_tokens": 2064, "paths_mentioned": 3, @@ -1029,11 +1265,39 @@ } ] }, + { + "task": "T6", + "verb": "for-compact", + "failed": null, + "ms": 999, + "bytes": 10487, + "est_tokens": 2622, + "paths_mentioned": 38, + "ground_truth": 3, + "ground_truth_basis": "existing-files-only", + "hits": 0, + "recall": 0, + "best_rank": null, + "per_file": [ + { + "path": "packages/platform-apple/src/snapshot-route.test.ts", + "rank": null + }, + { + "path": "packages/platform-apple/src/snapshot-target.test.ts", + "rank": null + }, + { + "path": "packages/platform-apple/src/snapshot-target.ts", + "rank": null + } + ] + }, { "task": "T6", "verb": "for-idents", "failed": null, - "ms": 1026, + "ms": 693, "bytes": 10545, "est_tokens": 2636, "paths_mentioned": 43,