Files
claude-plugin-inference-arb…/design/spec.md
T
Oleks 88fc58b2fd
ci/woodpecker/push/test Pipeline was successful
FR-4.5.1: a narrow window may not overturn a wide measured reading
The stability gate treated every evidence flip as symmetric, and that
produced a false downgrade on the candidate it was written for
(kotkan/claude-plugin-inference-arbitrage#15). Re-measuring
anxious/agent-wip/release-policy-derivation over four real windows with
the same script gave 0 -> 2 -> 12 -> 15 invocations at 7/14/27/60 days:
monotonic accumulation of a step that runs about once every two days, not
a label flipping about. The 7-day window had not caught an unstable
candidate, it had failed to observe a real one.

So a thin/unmeasured reading now only overturns a `measured` one when its
window is at least 90% as wide (observed since->until span). Below that
the comparison takes a third verdict, `insufficient-window`: not filed,
not downgraded to a boundary question, no FR-6.5 self-report, and carried
forward with the wider snapshot left as the standing comparison point.
The parity is relative rather than an absolute day count because the
adequate width is a property of the candidate's invocation rate, which
the auditor does not know in advance.

Unchanged, and tested: the reverse direction (thin prior -> measured now)
still downgrades, a flip between comparably wide windows still downgrades,
and a threshold crossing with `measured` on both sides is never excused by
narrowness. An unreadable window is not an exemption either — the flip
stands and window_parity records that the check could not run.

- bin/stability-classify: window_days/window_parity/deferrable, the
  insufficient-window verdict, stability_deferrals, window_parity on
  findings too, insufficient_window + window_parity_ratio in the gate block
- bin/audit-snapshot: record deferrals in the snapshot notes so the row is
  visible on the wiki page instead of silently dropped
- design/spec.md: FR-4.5.1 as a testable requirement; S4 restated
- design/rubric.md: the third boundary in §5b
- skills/offload-audit/SKILL.md: do not file and do not self-report a deferral
- tests: real 60-day anxious fixture; cases (f)/(f2) on real data, (j)-(n)
  synthetic — 7 test files pass

Verified against the live store (~/.cache/inference-arbitrage/wiki): the
real 7.0d-vs-26.49d comparison now reports insufficient-window with zero
findings, while the real 33.4d-vs-26.49d one reports stable at 13.87% and
files normally.
2026-07-30 15:57:02 +03:00

815 lines
45 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# spec.md — `inference-arbitrage`
**One-line description.** Audits a Claude Code plugin — its definition *and* its
measured transcript usage — for work that is currently paid for as LLM inference
but is really a deterministic algorithm, and proposes where to cut the boundary
between script and judgment.
**Status.** Implemented through TASKS Phase 7 (acceptance). Phase 8 (publish) is
outstanding. This document remains the requirements record; where an FR describes
behavior, that behavior ships — see the `bin/`, `skills/`, `agents/`, and
`commands/` trees, and `tests/run-all.sh` for the assertions.
**Naming note.** The proposed name **`inference-arbitrage`** (rationale in
`plan.md` §1) was accepted, and the directory renamed to match the manifest
`name` per this workspace's convention (TASKS 1.1). Nothing in the design depends
on the name.
**Target repo:** `kotkan/claude-plugin-inference-arbitrage`. The org was chosen
deliberately rather than derived — `kotkan` ("Kotkan Workload Development")
already holds the two plugins this one is most closely related to
(`kotkan/claude-plugin-token-budget`, `kotkan/claude-plugin-worktree-discipline`)
as well as `anti-patterns`. Not yet published to `claude-plugins.oleks.space`;
that is TASKS 8.2, per the `plugin-publishing` skill.
---
## 1. Why
### 1.1 The economics that make this worth building
`token-budget` established the governing fact of Claude Code cost, and this spec
inherits it rather than re-deriving it:
> ~95%+ of raw tokens are `cache_read`; well under 1% is generated output. Cost
> scales with **(session length × context size)**, largely independent of work
> produced.
The consequence for *this* plugin is sharp and non-obvious:
**Every turn the model spends emitting a mechanical tool call costs approximately
one full context re-read.** A step like "run `git worktree list`, parse it, and
decide which entries are stale" does not cost "a bit of reasoning" — it costs
~200k tokens of re-read, at 0.1× input rate, *per turn*, and it costs the same
whether the step was hard or trivial. A 6-turn deterministic loop inside a skill
is 6 full context re-reads to produce an answer a 40-line script produces for
zero tokens.
So the offload lever is not "the model writes too much." It is: **how many turns
does the model have to stay in the loop for, on steps that produce no judgment.**
That is a measurable quantity, and it is what this plugin measures.
### 1.2 The precedent — this already worked, twice, by hand
Two plugins in this environment already made the cut correctly, and both are
usable as calibration fixtures:
- **`token-budget`.** The entire transcript-parsing algorithm — JSONL streaming,
`(message.id, requestId)` de-duplication, streaming-snapshot `output_tokens`
maxing, the pricing table, per-session aggregation — lives in `bin/cc-tokens`
(606 lines of stdlib Python). The skills carry only what is irreducibly
judgment: *which* subcommand this question needs, how to frame the dollar
caveat, which of two remediation skills a finding routes to, and whether the
answer is "nothing is structurally wrong here." The boundary is drawn at
exactly the right place, and nobody has to say so out loud because the split
is structural.
- **`worktree-discipline`.** The rule that a worktree is only safe to remove if
its commits have a *remote* upstream was being got wrong by inference: a bare
`@{u}` check resolves even in a repo with no remote, because `git worktree
add -b` sets up local tracking. The global `CLAUDE.md` records this explicitly
— *"Use `worktree-audit`, which makes that call correctly; a bare `@{u}` check
does not."* The classification moved into `bin/worktree-audit`; the skill kept
the judgment (is this branch someone's in-progress work? is this worth
reconciling or rescuing?).
Both were discovered by an incident. The point of this plugin is to find the
third one **before** the incident, and to find it with a number attached.
### 1.3 The user's actual framing
> "searching for the possibility points to offload algorithms into scripts from
> bare inference … not only as a one-time optimization, but also as ongoing
> efficiency monitoring with usage analysis … the problem here is to correctly
> understand a points where we need intelligence between script entities
> correctly positioned"
Three requirements fall out, and they are the three functional pillars below:
find the candidates (static + dynamic), **place the boundary correctly** (the
crux — see `rubric.md`), and keep watching over time.
---
## 2. Scope
### 2.1 In scope
- Auditing **any** Claude Code plugin, addressed by directory path or by name
resolved against `~/.claude/plugins/cache/<source>/<name>/<version>/`.
- A **static pass** over the plugin definition (skills, agents, commands, hooks,
references, existing scripts).
- A **dynamic pass** over local session transcripts (`~/.claude/projects/**/*.jsonl`),
attributing measured token spend to the target plugin's skills and agents.
- A **boundary classification** of each candidate against a published rubric.
- **On-demand runs** that each persist a snapshot, so history accumulates and
successive runs can be diffed.
- Output paths per run: tracked issues on the **target** plugin's repo (FR-6), a
durable summary on **this** plugin's own wiki (FR-7), and durable context in
mempalace so a later run does not re-derive what an earlier one concluded
(FR-9). Findings about the *audit itself* go to this plugin's own tracker
instead of the target's (FR-6.5).
### 2.2 Out of scope for v1
- **Implementing** the offload. The plugin proposes and measures; a human or a
separate work session writes the script. Rationale: an auditor that also
writes the code it recommends loses the ability to say "no offload here."
- Real-time hooks or a mandatory cron. Cadence is on-demand by explicit
instruction (user or another agent). A user may wrap it in the `loop` or
`schedule` skills if they want recurrence — that is their choice, not the
plugin's default.
- Auditing non-plugin agent code, MCP servers, or arbitrary repos.
- Any network-dependent token accounting. The dynamic pass is offline, like
`cc-tokens`.
### 2.3 Non-goals worth naming
- **Not a linter.** It does not enforce style on SKILL.md. It reasons about cost
and determinism only.
- **Not a benchmark.** It never re-runs a plugin to measure it. It reads what
already happened.
- **Not an optimizer of prose length.** Trimming a skill body is a `context-diet`
concern and belongs to `token-budget`. This plugin is about *turn count on
mechanical work*, which is a different and larger lever.
---
## 3. Functional requirements
### FR-1 — Target resolution
**FR-1.1** Accept a target as a filesystem path, or as a plugin name resolved
through `~/.claude/plugins/cache/*/<name>/` (picking the highest semver
directory, excluding any marked `.orphaned_at`).
**FR-1.2** Record, in every snapshot, the resolved absolute path, the plugin
`name` and `version` from `.claude-plugin/plugin.json`, and the source
marketplace. Audits of different versions must be comparable but distinguishable.
**FR-1.3** Refuse to audit a directory with no `.claude-plugin/plugin.json`, with
a clear error naming what was expected. Do not guess at a plugin-shaped layout.
### FR-2 — Static pass
**FR-2.1** Enumerate the target's surface deterministically, via a script, not by
having a model read files: `skills/*/SKILL.md` (name, description, trigger
phrases, `allowed-tools`, body word count, heading structure), `agents/*.md`
(name, `model`, `tools` allowlist, body word count), `commands/*`, `hooks/*` and
`hooks.json`, `bin/*`, `references/*`, `tests/*`.
**FR-2.2** Emit per-skill and per-agent **static signals** (defined in §5.1) as
structured JSON — never as prose. The model consumes the JSON.
**FR-2.3** Compute a **script-coverage ratio** per plugin: prose-instruction
volume vs. shipped executable volume, plus the count of *literal command blocks
appearing in more than one skill* (a duplicated command block is a missing
shared script).
**FR-2.4** The static pass must run to completion on a plugin with zero
transcript history. A brand-new plugin is auditable on definition alone; the
report must then state that no measured evidence exists and mark every candidate
`unmeasured`.
### FR-3 — Dynamic (usage) pass
**FR-3.1** Attribute measured token spend to the target plugin's skills using the
`attributionPlugin` / `attributionSkill` fields present on assistant lines in the
transcripts, and to its agents via `agentName` / `isSidechain`.
*Verified during spec research:* these fields exist and are populated
(`attributionPlugin: "memory"`, `attributionSkill: "memory:save"`, etc.), and
**every** attributed assistant line sampled carried a full `message.usage` block
(1129/1129 in a 60-transcript sample). Per-skill cost attribution is therefore
directly computable, not an estimate.
**FR-3.2** Reuse `token-budget`'s accounting rather than reimplementing it. The
two documented transcript traps — one message spanning many JSONL lines all
repeating the same `usage`, and `output_tokens` being streaming snapshots that
must be maxed per `(message.id, requestId)` — are exactly the kind of thing that
is silently got wrong on a reimplementation. See PLAN §4.3 for the mechanism and
its risk.
**FR-3.3** Compute the **offload signals** defined in §5.2 — Mechanical Turn
Ratio, repetition signature, read amplification, retry density, judgment density,
fan-out multiplier — per skill and per agent.
**FR-3.4** Report **attribution coverage** for every audit: what fraction of the
window's turns plausibly belonging to this plugin were actually attributable.
Work driven by a plugin's hooks, or by an agent invoked without a Skill call, may
carry no attribution. An audit that silently understates coverage is worse than
no audit, so coverage is a required headline field, not a footnote.
**FR-3.5** Emit only **shapes and counts** — normalized tool-call signatures,
counts, token sums. Never copy raw transcript content, file contents, command
arguments, or user prose into a snapshot, wiki page, or issue. Transcripts
contain secrets and private work. Argument literals are masked (see §5.2.2).
### FR-4 — Boundary classification
**FR-4.1** Every candidate is classified into exactly one of four positions —
`pure-script`, `script-with-compiled-judgment`, `llm-over-script-digest`,
`pure-inference` — by the rubric in `rubric.md`.
**FR-4.2** No candidate may be filed as an issue unless the agent can produce the
**falsifiability triple** (rubric §5): the proposed function signature, three
input→expected-output pairs including one edge case, and *the case where a human
would overrule the script*. Failure to produce the third element downgrades the
candidate to a **boundary question** reported to the user, never filed.
**FR-4.3** Each candidate carries a `boundary_confidence` (`high` / `medium` /
`low`) derived from which of the five determinism tests it passes, and an
`offload_value` in weighted tokens and as a share of the plugin's audited spend.
**FR-4.4** The audit must be able to conclude **"nothing to offload here"** and
say so plainly. `token-budget` is expected to produce exactly this result; if a
run against `token-budget` produces a list of confident offload candidates, the
rubric is miscalibrated and that is a bug in this plugin.
**FR-4.5** No candidate may be filed unless its **evidence is stable across
measurement windows**. Before filing, each candidate whose verdict is `file` is
matched by FR-5.2 identity against the most recent prior snapshot recording it,
and the two windows' evidence is compared. A candidate whose
`measurement_strength` differs between the windows, or whose
`share_of_audited_spend` falls on opposite sides of the §5.2.7 filing threshold,
**is downgraded to a boundary question and never filed** — and the instability is
itself reported as a finding against *this* plugin's repo (FR-6.5), because a
confidence label that moves with the calendar is a defect in the auditor, not a
fact about the target. A candidate with no prior window to compare against is
marked `unchecked` and files normally; an absence of history is not evidence of
instability, and manufacturing one from it would contradict FR-2.4.
**FR-4.5.1 — Minimum window width for a downgrade.** A weak reading may only
overturn a `measured` one when it was taken over a comparably wide window. Let
`width(w) = w.until - w.since` in days, read off the window each reading was
taken over (the observed session span the scan records, not the requested
range). A comparison is **not read as instability**, and takes a third verdict
`insufficient-window`, when all of the following hold:
1. the prior reading's `measurement_strength` is `measured`;
2. the current reading's `measurement_strength` is `thin` or `unmeasured`;
3. `width(current) < 0.9 × width(prior)`.
An `insufficient-window` candidate is **neither filed nor downgraded to a
boundary question**, emits **no FR-6.5 self-report**, and is carried forward: the
wider prior snapshot remains the standing comparison point, so the next audit
over an adequately wide window compares against it and not against this
window's under-sample. The run records the widths it decided on, the parity
floor, and the flips it suppressed.
Three cases remain downgrades, and must not be excused by narrowness: the
reverse direction (`thin`/`unmeasured` prior, `measured` current — the earlier
reading is then the weak one); any flip between windows that satisfy the 0.9
parity; and a threshold crossing in which **both** readings are `measured`,
since a window that reached `measured` observed the candidate often enough for
the share to be a claim about relative spend rather than a sampling failure.
The rule is stated as a ratio against the prior window rather than an absolute
number of days because the adequate width is a property of the candidate's
invocation rate, which the auditor does not know in advance:
`anxious/agent-wip/release-policy-derivation` runs roughly once every two days
and needs ~27 days; a once-per-hour step is fully measured in one. The ratio is
0.9 rather than 1.0 because window bounds are observed session spans, so two
nominally identical windows differ by hours. When either window does not carry
both bounds, width is not computable, the flip is **not** excused, and the run
records that the check could not be made — an unreadable window must be visible,
never a silent exemption.
### FR-5 — Snapshot accumulation and diffing
**FR-5.1** Every run appends one machine-readable snapshot record to an
append-only store on this plugin's own Gitea wiki (schema in PLAN §6).
**FR-5.2** Candidates carry a **stable identity** across runs so that "3 of 5
prior recommendations are still unaddressed" is computable. Identity resolution
order: (1) an open/closed Gitea issue number, once filed — the strongest and
preferred identity; (2) the normalized tool-sequence signature; (3) the
`(skill, slug)` pair assigned at first sighting.
**FR-5.3** A run compares against the previous snapshot for the same target and
reports per-candidate status: `new`, `persisting`, `grown`, `shrunk`, `resolved`,
`signature-drifted`, `stale` (target version changed such that the skill no
longer exists).
**FR-5.4** Trend comparison must normalize for usage volume. Absolute token
deltas across windows of different activity are meaningless — a quiet week reads
as an improvement. Primary trend metrics are **cost per invocation** and **share
of the plugin's audited spend**; absolute tokens are reported as context only.
**FR-5.5** A candidate is `resolved` only when its issue is closed **and** the
signature's measured cost has dropped below threshold in a later window with at
least N invocations. Issue-closed-without-measurement is reported as
`claimed-fixed-unconfirmed`. Committed ≠ verified, in the same spirit as this
environment's `nixos-deploy-pending` discipline.
### FR-6 — Output path A: issues on the target repo
**FR-6.1** Each `high`-confidence candidate above the value threshold is filed as
a tracked issue **on the target plugin's own repo**, by delegating to
`anxious:issuer` (which shapes and dispatches; `cluster:gitea-agent` performs the
write). This plugin never writes to Gitea directly.
**FR-6.2** Follow the existing environment conventions rather than inventing a
scheme: full `owner/repo#num` references everywhere; labels from the `anxious`
four-axis taxonomy (`kind/chore` or `kind/capability-gap`,
`area/agent-behavior`, `domain/agents`, `activity/automate`), plus one new
cross-cutting label `token-offload`. Filing is implicit — the audit does not ask
permission to file, consistent with the global bug-handling rule.
**FR-6.3** **Idempotency is mandatory.** Each issue body carries a marker line
`<!-- ia-candidate: <candidate_id> -->`. Before filing, search the target repo
for that marker; if found, comment the updated measurement on the existing issue
instead of opening a duplicate. An on-demand audit that is re-run must not spam
the tracker — this is the single most likely way for the plugin to become
hated.
**FR-6.4** If the target plugin has no writable repo (a third-party or official
marketplace plugin), skip path A entirely, record `filing: unavailable` with the
reason, and report the candidate in the summary only.
**FR-6.5** A finding **about the audit itself** — currently only the FR-4.5
instability downgrade — is filed against **this plugin's own repo**
(`kotkan/claude-plugin-inference-arbitrage`), never the target's. The target did
not change; the auditor described it two ways, so the target's tracker is the
wrong place to put it and filing it there would blame the audited plugin for the
auditor's defect. These findings obey FR-6.3's idempotency rule with their own
marker, `<!-- ia-stability: <candidate_id> -->`, kept distinct from
`ia-candidate` so that a self-finding and a target finding for the same
`candidate_id` cannot collide in a marker search. Delegation is unchanged: this
plugin never writes to Gitea directly.
### FR-7 — Output path B: the run summary
**FR-7.1** Each run writes a human-readable summary page to **this plugin's own**
Gitea wiki at `Audits/<target-plugin>/<YYYY-MM-DD>`, and updates
`Audits/<target-plugin>/Latest`.
**FR-7.2** Rationale for wiki-over-artifact, since the brief asks for one: the
summary must be durable, addressable across sessions, diffable by git, and
readable by a future agent that was not present for the run. A Gitea wiki page is
all four — the wiki is a git repo, so the snapshot JSONL and the prose report
version together and a `git log` shows the audit history for free. An Artifact is
a private, session-produced web page with no cross-run addressing and no
relationship to the repo; it cannot be the system of record. An Artifact **is**
offered as an optional rendered view — a trend chart across snapshots — when a
human explicitly asks to look at the history, generated from the same snapshot
data. Canonical store: wiki. Optional presentation: artifact.
**FR-7.3** The summary states, in this order: attribution coverage and window;
the composition headline (how much of the plugin's spend was mechanical); the
ranked candidate table with boundary classification and value; the diff against
the previous snapshot; the boundary questions posed to the user — both the
low-confidence candidates and any downgraded by the FR-4.5 stability gate, the
latter marked as a defect in the auditor rather than a fact about the
target; any candidate the gate could not judge either way (`insufficient-window`,
FR-4.5.1), stated as a window too narrow to compare rather than as a finding
about the target or the auditor; and the issues filed or updated, in full
`owner/repo#num` form,
separating those on the target's tracker (FR-6.1) from those on this plugin's
own (FR-6.5).
*Shipped as of v0.5.0, with one gap named rather than papered over:* the
analyst's spoken report (`skills/offload-audit` §7) makes both distinctions, but
the **rendered wiki page does not yet.** `audit-snapshot`'s summary renderer
lists all boundary questions uniformly and shows only each candidate's own
issue, because `snapshot_candidate` does not carry the `stability` field and
stability findings never enter the snapshot at all — `stability-classify` emits
them alongside it, which is why `memory-notes` takes `--stable` as a separate
input (FR-9.1). Closing this means widening the snapshot schema, not editing a
renderer; until then a self-filed finding reaches the page only via `--note`.
### FR-8 — Self-application
**FR-8.1** The plugin must pass its own audit. Its static pass is a script, its
transcript scan is a script, its snapshot diff is a script; the agent's job is
classification, judgment, and reporting. If any of these migrate into skill prose
executed by inference, the plugin is violating its own thesis.
**FR-8.2** Auditing `inference-arbitrage` with `inference-arbitrage` is an
acceptance test (S7, TASKS 7.7).
**FR-8.3** **The audit's delegation topology is bounded and fixed.** One audit is
one unit of work: the whole audit may be handed to a single `offload-analyst`
instance, and that hand-off happens **at most once**, from the top-level command
or skill. The analyst is the delegation target, not a further decision point — it
never spawns another `offload-analyst`, and never one agent per skill or per
candidate. Its only outbound `Agent` calls are the fixed filing chain of FR-6.1
(`anxious:issuer``cluster:gitea-agent`).
This is a hard requirement and not a style note, because it failed in exactly the
predictable way. The command instructs a caller to delegate a *heavy* target to
the analyst; nothing told the analyst that it *was* that delegation target, so on
a large plugin it re-applied the same heaviness criterion to itself and spawned
another instance, three to four levels deep, each restarting the same measurement
steps with no forward progress. Size is a reason to work through a target
methodically, never a reason to hand it to a fresh copy of yourself with no
memory of what has already been measured.
The rule is load-bearing twice over: it is the S8 runtime constraint (PLAN §7 —
no subagent fan-out, on a 16 GiB laptop), and it is FR-8.1 self-application, since
unbounded self-delegation is precisely the shape of waste this plugin exists to
find. A token-optimization plugin recursing on itself is the most expensive bug
it could possibly have.
### FR-9 — Memory integration
The wiki (FR-7) is this plugin's system of record; **mempalace is where an audit
becomes context for the next one.** Two distinct purposes, and they need
different placement.
**FR-9.1 — The bridge is a script, exactly like FR-7's.** `audit-snapshot
memory-queries` emits the searches to run before grading; `audit-snapshot
memory-notes` emits the `{wing, room, content, source_file, added_by}` payloads
to write after the run, plus the knowledge-graph triples. Neither makes an MCP
call: filesystem in, JSON out. The caller passes both **verbatim** to `search`
and `add_drawer` / `kg_add`.
This is FR-8.1 applied to this feature. *Which* candidates get written about is
already decided by the classify and stability verdicts, both deterministic;
*what text gets written* is a total function of the snapshot. A plugin that
narrated its own bookkeeping at inference time would be filing an issue against
itself.
**FR-9.2 — Reading is advisory, never a gate.** Prior drawers are context the
analyst may cite. They may **never** override `bin/boundary-classify` (FR-4.2)
or `bin/stability-classify` (FR-4.5), and are never a reason to file a candidate
that did not clear the gates, to skip the FR-6.3 marker search, or to re-grade a
verdict already returned. Memory says what was thought before; the gates decide
what happens now. A remembered "we declined this last time" is a reason to look
harder at the reasoning, not to short-circuit it — and a remembered "we filed
this last time" is not a filing decision at all, because the tracker, not
memory, is the FR-5.2 identity of record.
Note honestly what kind of requirement this is, clause by clause — the mix is
not uniform, and calling the whole of FR-9.2 unenforced would be as wrong as
calling it a gate.
Three of its four prohibitions **are** mechanical, and recall cannot reach them:
- *file something the gates rejected* — `bin/filing-plan` emits `action: file`
only for `verdict == "file"`, and skips everything else with the verdict named
in the reason;
- *skip the FR-6.3 marker search* — `filing-plan plan` makes `--existing`
required precisely so that a search which never ran cannot default to
"nothing exists";
- *re-grade a returned verdict* — the verdict is `boundary-classify`'s output,
and `stability-classify` rewrites rather than re-asks it.
What is left unenforced is the one clause that matters most and is the hardest
to watch: **what the analyst writes into the judgments JSON that those gates
then consume.** The gates are pure functions of their input, so recall cannot
corrupt a verdict directly — it can only corrupt the evidence handed to them, by
softening a determinism test or supplying an overrule case remembered rather
than reasoned. No script can see that, because the whole value of the read is
that it influences judgment; a read that could not move the judgments JSON would
be a read with no purpose.
An audit trail is therefore the only available control, and it is a real
obligation, not a hope: **a run that leans on a recalled conclusion says so in
its report**, so a human can see the lean and discount it. That is why FR-9.2 is
written as prohibitions rather than thresholds.
**FR-9.3 — Two placements, for two different readers.**
| | Wing | Room | Read | Write |
| --- | --- | --- | --- | --- |
| Per-target findings | the audited plugin's manifest `name` | `inference-arbitrage-audits` | yes | yes |
| Cross-target judgment | `claude-plugins` | `inference-arbitrage-lessons` | yes | yes |
*Target-scoped* mirrors FR-6.1's philosophy — the finding belongs on the
target's own surface, not this plugin's — so a human or agent later working on
that plugin meets the offload findings while searching their own project's
memory. Two drawers per run: the per-run summary, and the qualitative judgment
calls (declined candidates and why, stability downgrades).
*Cross-target* is this plugin's own accumulated judgment — calibration
outcomes, coverage-dilution norms, the shapes that keep turning out to be false
positives — in the wing this plugin's own development history already lives in.
It is written on **every** run regardless of target and regardless of outcome; a
"nothing to offload here" result is exactly the calibration evidence worth
keeping. It is read at the start of every audit, independent of which plugin is
being audited.
**Room names are namespaced by this plugin's name, deliberately.** A wing named
for a plugin can collide with an unrelated wing that already means something
else: `cluster` (the k3s cluster) and `imagex` (corp Jira/Slack/Bitbucket work)
both exist today and mean neither the `cluster` plugin nor the `imagex` plugin.
Namespacing the *room* turns such a collision into a shelving overlap rather
than a content collision — a room-filtered search returns only audit drawers,
and every drawer opens with a line naming what wrote it. Dumping into a generic
`decisions` / `gotchas` room would have made the collision real.
**FR-9.4 — Redaction applies unchanged.** FR-3.5 governs a drawer exactly as it
governs an issue body or a wiki page: shapes, counts and prose *about* the audit
only. Never raw transcript content, file contents, command arguments, or user
prose.
The memory renderers therefore do **not** reuse the wiki renderers. A wiki page
lives in this plugin's own repo; a drawer lands in a wing shared with unrelated
projects, and `snapshot.notes` is caller-supplied free text (`--note`) that
reaches the wiki page legitimately. The memory renderers read from a fixed
**allowlist** of structured snapshot fields — ids, positions, booleans, counts,
ratios — so no free-text field crosses into a drawer at all. Allowlist, never
blocklist: a blocklist is one new snapshot field away from leaking.
**FR-9.5 — The untestable seam is one call wide.** The MCP calls cannot be
exercised by the shell test harness, and are not faked there. That is precisely
why all the judgment lives in the script: the caller's remaining job is a
verbatim tool call, so the part that cannot be tested is the part with nothing
in it to get wrong. `tests/memory.test.sh` covers wing/room resolution, query
packing, drawer selection, rendered content and redaction; the live round trip
is verified by hand (TASKS 7.9.5).
---
## 4. Success criteria
| # | Criterion | How it is verified |
| --- | --- | --- |
| S1 | Audits any plugin by path or name, with no target-specific code | Run against `token-budget`, `worktree-discipline`, `anxious`, `memory` |
| S2 | Correctly returns "nothing to offload" for a well-cut plugin | Run against `token-budget` → no `high`-confidence candidates |
| S3 | Rediscovers a known-correct historical cut | Run against `worktree-discipline` with `bin/worktree-audit` removed from the inventory → must flag the classification step as `high` confidence |
| S4 | Mechanically refuses a candidate whose evidence is a window artifact | The two-window `anxious` fixture → the FR-4.5 gate refuses `agent-wip/release-policy-derivation` (as `insufficient-window`, per FR-4.5.1, since the 7-day current window cannot fairly contradict the 26-day `measured` one), with no human in the loop; a flip between comparably wide windows is still downgraded to a boundary question |
| S5 | Re-running does not duplicate issues | Run twice; second run comments, does not file |
| S6 | Trend across ≥2 snapshots is computable and volume-normalized | Two runs on different windows; diff reports per-invocation deltas |
| S7 | Passes its own audit | FR-8.2 |
| S8 | Runs within emmett's constraints | Single stdlib process, streaming, no subagent fan-out and no self-delegation (FR-8.3), no toolchain build |
S3 is the important one. It is the only criterion that tests the rubric against a
cut *known* to be correct, made by a human, for a reason that is written down.
S4 was originally *"finds a real candidate in `anxious` that the user agrees is
genuine."* That is retired: a human triaging one candidate and pronouncing it
good is not a repeatable test, and it puts the plugin's central safety property
behind whether someone was feeling skeptical that day. What actually happened is
that a human review caught a candidate whose evidence flipped between windows —
so the criterion is now that gate, run mechanically (FR-4.5), against the real
two-window evidence that produced the original finding.
The *reason* S4's candidate is refused changed in v0.7.0, and the change is
itself instructive. Two further real windows (14 and 60 days, same script, same
candidate) showed 0 → 2 → 12 → 15 invocations as the window widened: monotonic
accumulation of a low-frequency step, not a label flipping about. The 7-day
window had not caught an unstable candidate, it had failed to observe a real one,
and calling that instability blamed the candidate for the auditor's window. So
FR-4.5.1 makes the refusal honest — `insufficient-window`, cannot tell either
way, compare again over a wider window — while keeping the property S4 exists to
test: nothing is filed off a window artifact, with no human in the loop.
(kotkan/claude-plugin-inference-arbitrage#15.)
---
## 5. Signal catalog
### 5.1 Static signals — what the definition says
These are computed by `bin/plugin-inventory` and consumed as JSON.
| Signal | Definition | Reads as |
| --- | --- | --- |
| **Procedural density** | Fraction of a skill body that is numbered/imperative steps containing literal commands, vs. discursive prose | A recipe the model re-derives every invocation |
| **Script-verb count** | Occurrences of `count`, `sum`, `rank`, `sort`, `diff`, `parse`, `validate`, `enumerate`, `dedupe`, `format`, `check exists`, `compare` | Mechanical intent stated in prose |
| **Judgment-verb count** | Occurrences of `decide`, `judge`, `explain`, `prioritize`, `name`, `write`, `weigh`, `interpret`, `for this user` | Irreducible intent stated in prose |
| **Verb ratio** | script-verbs / (script-verbs + judgment-verbs) per skill | >0.6 with no `bin/` script is the loudest static smell |
| **Duplicated command blocks** | Identical or near-identical fenced command blocks appearing in ≥2 skills | A shared script that was never written |
| **Rule tables** | Markdown tables that are pure lookup (input → output, finding → action) | A dispatch table being narrated at inference time |
| **Script coverage** | executable LOC in `bin/` + `hooks/` vs. total skill+agent body words | Structural balance; low ratio + high verb ratio is the target profile |
| **Hook/prose drift** | Logic present both in a hook script and restated in skill prose | Two sources of truth; the prose will rot |
| **Tool allowlist shape** | An agent whose `tools` are dominated by read-only mechanical tools | The agent was built to fetch, not to judge |
**Worked example — `token-budget` (expected: clean).** `skills/token-audit` has
high procedural density (it prescribes three exact commands in order) and a rule
table (`finding → remediation skill`). But script coverage is high (606 LOC of
`cc-tokens` against ~3 skill bodies), the verb ratio in the *body* is
judgment-dominated ("report", "interpret", "state the caveat", "rank the fixes by
effect"), and — decisively — the prescribed commands are invocations of a script
that already exists. The static pass should score this as **already cut**, with
the residual candidate being at most "the fixed three-command opening sequence
could be one `cc-tokens audit` subcommand," classified `low` value.
**Worked example — `anxious` (expected: real candidates).** `agents/steward.md`
sweeps *every* `oleks/*` repo and reconciles labels, milestones, and board
placement. The taxonomy it reconciles against is a **pure lookup table** with a
documented deterministic ruleset (`references/taxonomy.md` axis 3 is explicitly
described as *"steward-owned, repo-derived (deterministic)"*, and axis 1's
derivation is a four-step priority list ending in "defer to human"). A
deterministic ruleset, written down, applied per repo by an LLM turn, over N
repos, is the canonical shape: the *derivation* is a script, the *"defer to
human"* branch and the axis-2/axis-4 weak priors are judgment. Expected
classification: `llm-over-script-digest` — a script computes proposed labels and
emits only the disagreements and the no-signal cases for the model to rule on.
### 5.2 Dynamic signals — what the transcripts show
Computed by `bin/offload-scan` over `~/.claude/projects/**/*.jsonl`, filtered to
turns attributed to the target.
#### 5.2.1 The primary metric — Mechanical Turn Ratio
A turn is **mechanical** if all of:
- it contains ≥1 `tool_use` block, and every one is drawn from the mechanical
tool set (read-only `Bash`, `Read`, `Grep`, `Glob`, `list_*` / `get_*` /
`*_read` MCP tools, `ToolSearch`), and
- its `output_tokens` are below a threshold (default 400) — the model emitted a
tool call and little else, and
- it is not immediately preceded by a tool error (that is a retry, counted
separately under 5.2.4).
```
MTR(skill) = mechanical_turns / attributed_turns
mechanical_tokens = Σ over mechanical turns of (cache_read + cache_write + input + output)
offload_waste(skill) = mechanical_tokens # weighted, via cc-tokens' tier normalization
```
`offload_waste` is the honest number: **tokens spent purely to keep the model in
the loop on steps that produced no judgment.** Because `cache_read` dominates, it
is very nearly `mechanical_turns × mean_context_size` — which is exactly why turn
count, not verbosity, is the lever.
An MTR above ~0.5 on a skill carrying meaningful spend is the primary flag. A
high MTR on a skill with trivial spend is noise and is not reported.
#### 5.2.2 Repetition signature — finding the algorithm in the wild
Normalize every tool call to a signature: tool name plus argument *shape*, with
volatile literals masked — paths → `<path>`, integers → `<n>`, hex/SHA → `<sha>`,
URLs → `<url>`, quoted free text → `<str>`. (This masking is also what satisfies
the privacy requirement FR-3.5.)
Within each invocation of a skill, extract the ordered signature sequence. Across
invocations, find n-grams of length ≥3 that recur in ≥3 distinct invocations.
**A frequently-recurring identical tool sequence is an algorithm, empirically
discovered.** This is the most direct possible answer to the user's ask, because
it does not rely on reading intent out of prose — it observes the procedure being
executed the same way repeatedly and prices it. Each recurring n-gram is reported
with its occurrence count and its measured `offload_waste`, and becomes a
candidate with a ready-made proposed script boundary.
#### 5.2.3 Read amplification
```
read_amplification = tokens_pulled_into_context_by_read_tools
/ tokens_of_that_material_referenced_in_subsequent_output
```
Approximated by comparing tool-result sizes to the model's next-turn output
length and its literal overlap with the result. High amplification means the
model is reading a haystack to find a needle — a filter/digest script belongs
upstream. This is the signature of the `llm-over-script-digest` position.
#### 5.2.4 Retry / self-correction density
Fraction of attributed turns that follow a tool error, or that repeat a
near-identical signature with adjusted arguments. Every retry is a full context
re-read. High density means the invocation is fiddly — the correct offload is
often a thin wrapper script that gets the invocation right once, rather than a
wholesale algorithm move. Cheap to build, immediately effective.
#### 5.2.5 Judgment density — the brake
Fraction of attributed turns with substantive `output_tokens` (above threshold)
and *no* `tool_use`, or containing `AskUserQuestion`. This is where the model was
actually thinking.
**Judgment density is a brake, not an accelerator.** High judgment density with
high spend means the plugin is doing what it should and must be reported as
healthy. The offload target is specifically **low judgment density × high spend**.
#### 5.2.6 Fan-out multiplier
Sidechain turns attributed to the target. Each subagent carries its own full
context, so a mechanical step performed inside a subagent costs a multiple of the
same step inline. A mechanical n-gram executing inside a fan-out is the
highest-value candidate class there is — and this environment already has a
recorded finding against one-subagent-per-tiny-step fan-out.
#### 5.2.7 Composite ranking
```
offload_value = offload_waste × (1 judgment_density) × repetition_factor
```
where `repetition_factor` = 1 + log₂(recurrences of the dominant n-gram).
Reported in weighted tokens and as a share of the plugin's audited spend.
**Measurement strength.** Before the value threshold can be applied, the evidence
must be strong enough for the share to mean anything. Each candidate carries a
`measurement_strength`:
| Value | When |
| --- | --- |
| `measured` | ≥3 invocations **and** ≥30 attributed turns |
| `thin` | attributed, but too few invocations — the share is arithmetically real and evidentially worthless |
| `unmeasured` | no attributed usage at all (the FR-2.4 zero-history case) |
**Filing threshold.** `boundary_confidence == high` **and** the falsifiability
triple was produced (including the overrule case) **and** the FR-4.5 stability
gate passed. Given those, the value test depends on measurement strength:
- `measured``offload_value` must also be ≥2% of the audited window's total
spend. Below it, the candidate is a boundary question.
- `thin` / `unmeasured` → the value test **cannot** be applied and is not
applied. The candidate files on its **static case** alone, and the issue body
must state that the value claim is unproven. FR-2.4 requires a plugin with no
transcript history to stay auditable, and silently withholding a
correctness-complete candidate because it happens to be unmeasured would make
every brand-new plugin audit return nothing.
That asymmetry is deliberate and is the one place a candidate reaches the tracker
without a cost number behind it. It is safe only because the correctness gates —
the five determinism tests, the overrule case — are unconditional; what varies is
whether the plugin can also say *how much it is worth*. Conversely the value
threshold is deliberately conservative where it does apply: the cost of a missed
candidate is a slightly expensive plugin; the cost of a wrong candidate is a
plugin that confidently does the wrong thing, forever, silently.
Two verdicts sit outside this ladder entirely. A candidate classified
`pure-inference` (position 4) is reported as **`no-offload`** — an explicit
finding that the step is correctly done by inference, per FR-4.4, not a silent
omission. A hook/prose drift signal (§5.1) is reported as a **`drift-note`**: it
is a restated configuration contract with two sources of truth, and its fix is to
point the prose at the hook file, not to write a new script.
---
## 6. Risks and open questions
These want the user's input before or during the build.
**Q1 — Cross-plugin dependency on `cc-tokens`.** The dynamic pass should not
reimplement `token-budget`'s accounting (FR-3.2), but a plugin cannot rely on
another plugin's `${CLAUDE_PLUGIN_ROOT}`. Options: (a) require `cc-tokens` on
`PATH` and fail loudly with an install hint; (b) glob
`~/.claude/plugins/cache/*/token-budget/*/bin/cc-tokens` and pick the newest;
(c) vendor a copy. Recommendation: (a) with (b) as fallback, and **never** (c) —
a vendored copy silently diverges on exactly the two traps that are hardest to
notice. Better still: contribute `cc-tokens attribute --by skill|plugin --json`
upstream to `token-budget`, so the money math has one home. **Resolved (TASKS
0.1): (a) with (b) as fallback**, never (c). `bin/offload-scan` tries `PATH`,
then the cache glob, then fails loudly with an install hint. The upstream
contribution remains optional (TASKS 8.3).
**Q2 — Attribution coverage may be thin.** `attributionSkill` appears only when a
Skill was formally invoked. Hook-driven work, agent work started without a Skill
call, and plugin logic that runs as part of a larger session may carry no
attribution at all. A 60-transcript sample surfaced ~1100 attributed lines across
a dozen skills — real, but not obviously complete. If coverage for a given target
is low, the audit's dynamic pass is weak and must say so loudly (FR-3.4). *Open:
is there a better attribution path for agent-driven work than `agentName`?*
**Q3 — Read access to a target's repo.** The audit reads the local plugin cache,
which needs no repo access. But FR-6 files issues on the target's repo, which
does. For `oleks/*` plugins this is fine. For third-party or official-marketplace
plugins there is no writable tracker, and the audit degrades to summary-only
(FR-6.4). Confirm that degradation is acceptable rather than an error.
**Resolved (TASKS 0.2): yes**`filing: unavailable` with the reason recorded,
and the run summary still written.
**Q4 — False positives are the real failure mode.** Recommending a script where
judgment is needed produces something worse than the status quo: a fast,
confident, wrong answer with no one watching. The rubric's five determinism
tests, the mandatory "when would a human overrule this" element, and the
conservative filing threshold are the mitigations, and the plugin never
implements its own recommendations. The residual risk is that a plausible-looking
candidate gets built by a later session that does not re-check the boundary —
which is why the issue body must carry the overrule case, not just the proposal.
**Q5 — Goodhart.** A plugin that scores plugins on token cost will, if followed
blindly, push toward brittle over-scripting. Proposed counter-metric tracked on
the trend page: the rate of `token-offload` issues later closed as `wontfix`, and
of offload scripts subsequently reverted. If that rate climbs, the rubric is too
loose. *Open: is this worth building in v1, or noted as a manual check?*
**Q6 — Privacy of the summary surface.** Snapshots and wiki pages are derived
from transcripts that contain secrets, private prose, and corp work. FR-3.5's
masking is the control. Worth confirming the masking list is sufficient, and
whether audits of `imagex:*` (corp) plugins should be excluded from the shared
wiki entirely.
**Q7 — Local compute budget.** Scanning a multi-gigabyte transcript history on
emmett must be a single streaming stdlib process, never a fan-out of one subagent
per skill. Stated as a hard constraint in PLAN §7, flagged here so it is a
conscious decision rather than an accident.
**Q8 — Rubric versioning.** Snapshots record `rubric_version`. When the rubric
changes, old candidate classifications are not directly comparable. Proposal:
diffs across a rubric-version boundary are annotated, not suppressed. Confirm.
---
## 7. Incidental finding
While surveying the plugin cache for this spec, two leaked git worktrees were
found inside the installed plugin cache itself:
```
~/.claude/plugins/cache/oleks-local/worktree-discipline/1.12.0/.claude/worktrees/wf_fdb5df0f-811-2/
~/.claude/plugins/cache/oleks-local/worktree-discipline/1.12.0/.claude/worktrees/wf_fdb5df0f-811-3/
```
Each carries a full copy of the plugin tree. These are unnamed-Workflow-subagent
worktrees (`wf_*`) that leaked into a *cache* directory — a location `sweep-worktrees`
is unlikely to be pointed at, and one that a plugin reinstall would silently
orphan. Filed as **kotkan/claude-plugin-worktree-discipline#9**; not in scope for
this spec.