promptwheel 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,9 +8,9 @@ PromptWheel re-proves every "win" using the agent's **source edits alone**. If t
8
8
 
9
9
  It's built on an **outcome gate**: for any change it re-runs your metric commands (tests, lint, `tsc`, coverage, bundle, eval) in throwaway git worktrees before and after, and refuses to trust a delta inside the measurement noise. The gate everyone ships asks *"did the number move?"* — PromptWheel also asks *"did the agent **earn** it?"*
10
10
 
11
- PromptWheel is the **signal, not the loop driver**: wire it as the verifier inside Claude Code `/loop`, a Ralph `while`-loop, or a Beads pull-loop. Each turn it measures your real repo metrics in throwaway worktrees, refuses to trust a delta inside the noise, and answers one question — *did this turn earn its keep?* — so the loop improves instead of confidently degrading. (In CI it's the **outcome gate for AI code**: the same verdict, as a PR check.)
11
+ PromptWheel is the **signal, not the loop driver**: wire it as the verifier inside Claude Code `/loop`, a Ralph `while`-loop, or a Beads pull-loop. Each turn it answers one question *did this turn earn its keep, and did the agent earn it honestly?* — so the loop improves instead of confidently degrading. (In CI it's the **outcome gate for AI code**: the same verdict, as a PR check.)
12
12
 
13
- > Same name, new meaning. The "wheel" is the **improvement flywheel**: every turn only counts if it **provably moved a metric without regressing another.** Orchestration (the old "wheel of prompts") is a solved, commoditized problem; the trustworthy reward signal is the open one.
13
+ > Same name, new meaning. The "wheel" is the **improvement flywheel**: every turn only counts if it **provably moved a metric without regressing another.** Orchestration (the old "wheel of prompts") *and the outcome gate itself* are now solved, commoditized problems; the open one is **catching when the agent games that gate** — making the reward signal one you can't cheat.
14
14
 
15
15
  AI coding agents (and humans) declare success when a change *compiles and tests pass*. They almost never verify that a change **improved a real measurable thing without regressing another** — and outcome data rots in dashboards (DORA, Swarmia) disconnected from the change that caused it. PromptWheel closes that loop.
16
16
 
@@ -29,12 +29,12 @@ PromptWheel a1b2c3d → e4f5g6h (×5)
29
29
 
30
30
  Exit `0` on pass, `1` on fail (CI-friendly). No build step, zero dependencies, Node 18+.
31
31
 
32
- ## Catch your agent cheating — `--detect-gaming`
32
+ ## Catch your agent cheating — on by default
33
33
 
34
- The headline feature. After the normal gate, every *win* is re-proven using the agent's source edits **alone**: PromptWheel splits the diff into production source vs. `{test, config, grader, golden}` files, rebuilds a clean worktree at the base with **only the source slice** applied, and re-runs the gate. If the win doesn't survive — because it only passed by skipping/deleting a test, mocking the grader, editing a golden, relaxing a config, or the "win" touched zero source files — the verdict is **`GAMED` (exit 2)**:
34
+ The headline feature, and it runs **by default** (in both `run` and `improve`; pass `--no-detect-gaming` for the bare outcome gate). After the normal gate, every *win* is re-proven using the agent's source edits **alone**: PromptWheel splits the diff into production source vs. `{test, config, grader, golden}` files, rebuilds a clean worktree at the base with **only the source slice** applied, and re-runs the gate. If the win doesn't survive — because it only passed by skipping/deleting a test, mocking the grader, editing a golden, relaxing a config, or the "win" touched zero source files — the verdict is **`GAMED` (exit 2)**:
35
35
 
36
36
  ```
37
- $ promptwheel run --detect-gaming
37
+ $ promptwheel run
38
38
 
39
39
  PromptWheel base → head
40
40
  ▲ tests_pass 0 → 1 (+1, improved) [guard✓, high]
@@ -46,11 +46,14 @@ Inline source-file suppressions (`@ts-nocheck`, `eslint-disable`, `# noqa`) are
46
46
 
47
47
  ```bash
48
48
  npx promptwheel init --preset antihack # target + tripwire guards
49
- npx promptwheel run --detect-gaming # exit 0 = real win · 1 = regression · 2 = GAMED
49
+ npx promptwheel run # detection ON by default · exit 0 win · 1 regression · 2 GAMED
50
+ # (add --no-detect-gaming for just the outcome gate)
50
51
  ```
51
52
 
52
53
  Deterministic, zero-LLM, zero-network: an LLM judge asking "did you cheat?" is itself gameable; this is a diff partition plus a re-run, so a flag is trustworthy without a human in the loop. The 50%-of-gain-survives threshold is the default and is tunable.
53
54
 
55
+ This is **one layer**, not a silver bullet: it catches the evaluator-tampering class (test/grader/golden/config edits) deterministically and for free, so the expensive layers — held-out tests for semantically-weak wins, an LLM judge or a human for intent and leakage — are reserved for the calls only they can make. See **[docs/DETECTION-LAYERS.md](docs/DETECTION-LAYERS.md)** for the coverage matrix and honest scope, and **[bench/RESULTS.md](bench/RESULTS.md)** for the measured numbers (`node bench/gaming-bench.mjs` to reproduce).
56
+
54
57
  ## Use
55
58
 
56
59
  ```bash
@@ -59,7 +62,7 @@ npx promptwheel init # detects stack → guarded test metri
59
62
  npx promptwheel init --list # presets: tests-pass · lint · bundle-size · llm-eval · antihack
60
63
 
61
64
  # measure a change
62
- npx promptwheel run # base = merge-base with main, head = HEAD
65
+ npx promptwheel run # base = merge-base with main, head = HEAD · reward-hack detection ON
63
66
  npx promptwheel run --working # measure UNCOMMITTED changes (incl. newly added files)
64
67
  npx promptwheel run --repeat 5 --json # measure 5× to establish a noise band, emit JSON
65
68
 
@@ -87,6 +90,8 @@ npx promptwheel run --working --json # branch on .verdict / per-metric .statu
87
90
 
88
91
  The exit code is the contract — `0` kept · `1` regression · `3` plateau — so any driver (`/loop`, a Ralph `while`, a Beads pull-loop) converges without parsing anything. PromptWheel never drives the loop; it only says whether the turn counted.
89
92
 
93
+ **Two callers, by design.** Your *agent* consumes the verdict — it's the loop's per-turn reward (`improve` / `run --working --json`; exit `0` kept · `1`/`2` reverted · `3` plateau). The *harness* (a Stop-hook or CI) runs `--detect-gaming` — the audit the agent **can't self-clear**, because a contestant can't referee itself (see [docs/ENFORCEMENT.md](docs/ENFORCEMENT.md)). Same tool; *who calls it* is the difference between a reward and an audit.
94
+
90
95
  ## In Claude Code — plugin
91
96
 
92
97
  Bring the gate into Claude Code as slash commands:
@@ -136,6 +141,7 @@ The Action runs straight from its own checkout — no npm install, no build. See
136
141
  - **extract** — reduce its output to a number: `number` (last number, default) · `lines` (count non-empty lines) · `exit` (1 if exit 0 else 0) · `{ "regex": "coverage: (\\d+)" }` (first capture).
137
142
  - **direction** — `up` (higher better) · `down` (lower better) · `pass` (boolean 0/1).
138
143
  - **guard** — `true` = a *trusted* regression **fails** the gate; `false` = informational.
144
+ - **gamingCheck** — `false` exempts a metric from `--detect-gaming`'s source-only re-run. Use it for tripwire / test-side guards (assertion counts, test counts) whose gains legitimately live in test files — otherwise adding real tests would be flagged as gaming. The `antihack` preset sets this on its tripwires.
139
145
 
140
146
  ## Guardrails & inheritance
141
147
 
@@ -155,6 +161,8 @@ Teams keep one shared base config and have every repo inherit it via `extends`:
155
161
 
156
162
  `extends` takes a path (or array of paths) to base configs: a repo **inherits** their guardrails, and a local metric of the same name **overrides** the inherited one (tighten, loosen, or disable). `promptwheel guards` shows the effective set with provenance — `inherited ← base.json`, `local`, or `local override` — plus each guard's flag record from the outcome stream.
157
163
 
164
+ Read this way, `extends` is **the shared invariants — the "business rules" — your agents inherit and are held to**: enforced as *measured guards* (a trusted regression fails the gate / reverts the commit), not advisory text an agent can quietly ignore. (Natural-language conventions belong in `AGENTS.md`; only deterministic, measurable guards belong here.)
165
+
158
166
  ## Trust model — the point of the whole thing
159
167
 
160
168
  A number that jumps around between runs is worthless as a signal. PromptWheel won't pretend otherwise:
@@ -173,6 +181,8 @@ The accumulated record of **which change-types move which metrics** is the asset
173
181
 
174
182
  ## Docs
175
183
 
184
+ - [docs/DETECTION-LAYERS.md](docs/DETECTION-LAYERS.md) — how `--detect-gaming` fits as the **deterministic layer** alongside held-out tests, LLM judges, and human review: the coverage matrix, the compose-as-a-pipeline model, and the honest in/out-of-scope boundary.
185
+ - [docs/ENFORCEMENT.md](docs/ENFORCEMENT.md) — making "the agent can't skip it" real: the three places to enforce (loop-revert · CI + branch protection · a tested Claude Code Stop-hook), the exact wiring, and the *protect-the-gate's-own-config* caveat.
176
186
  - [docs/VISION.md](docs/VISION.md) — why we pivoted from orchestrator to outcome gate, the thesis, the moat, the open-core model.
177
187
  - [docs/ROADMAP.md](docs/ROADMAP.md) — the phased plan and the ship-now/stay-thin guardrails.
178
188
  - [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — how the engine works: schemas, extract modes, the trust/noise model.
@@ -182,7 +192,7 @@ The accumulated record of **which change-types move which metrics** is the asset
182
192
  ## Develop
183
193
 
184
194
  ```bash
185
- npm test # 36 dep-free tests (node:test) — unit + integration, no dependencies
195
+ npm test # 37 dep-free tests (node:test) — unit + integration, no dependencies
186
196
  ```
187
197
 
188
198
  The engine is one importable file; pure helpers are exported for unit tests, the CLI runs only when invoked directly. Add a test with every behavior change.
@@ -199,6 +209,7 @@ The engine is one importable file; pure helpers are exported for unit tests, the
199
209
  - [x] `promptwheel init` + presets — zero-config onboarding
200
210
  - [x] `insights` — reward-stream aggregation (Phase-5 seed)
201
211
  - [x] `--detect-gaming` — reward-hack detection: re-prove the win from source edits alone + `antihack` preset
202
- - [ ] npm publish (the lead magnet) · ACE-style learning + UCB work-discovery (**frozen** — gated on data + ≥1 paid engagement; see [docs/LEARNING.md](docs/LEARNING.md))
212
+ - [x] npm publish — `promptwheel@0.1.0` (the lead magnet, shipped 2026-06)
213
+ - [ ] ACE-style learning + UCB work-discovery (**frozen** — gated on data + ≥1 paid engagement; see [docs/LEARNING.md](docs/LEARNING.md))
203
214
 
204
- > Status: v0, runnable, all core phases built. Lineage: CommandLayer → BlockSpool → PromptWheel (orchestrator, archived) → **PromptWheel (outcome gate)**.
215
+ > Status: **published v0.1.0** — the consulting lead magnet (npm `promptwheel@0.1.0`), all core phases built. Lineage: CommandLayer → BlockSpool → PromptWheel (orchestrator, archived) → **PromptWheel (outcome gate)**.
@@ -311,6 +311,7 @@ function gate(repo, opts) {
311
311
  for (const m of metrics) {
312
312
  if (m.status !== 'improved') continue;
313
313
  const cm = cfg.metrics.find((c) => c.name === m.name);
314
+ if (cm.gamingCheck === false) continue; // tripwire / test-side guards aren't re-proven from source (their gain legitimately lives in test files)
314
315
  const j = judgeGaming(m, measureSourceOnly(repo, base, head, cm, linkNM, repeat));
315
316
  m.gamed = j.gamed; m.sourceOnly = j.sourceOnly; m.retained = j.retained; m.gamingReason = j.reason;
316
317
  }
@@ -333,7 +334,7 @@ function run(argv) {
333
334
  process.exit(report.verdict === 'pass' ? 0 : report.verdict === 'gamed' ? 2 : 1);
334
335
  }
335
336
 
336
- // the moat: append every gated run to a per-repo outcome record (best-effort, never fails the gate)
337
+ // persisted record: append every gated run to a per-repo outcome record (best-effort, never fails the gate)
337
338
  function recordOutcome(repo, report) {
338
339
  try {
339
340
  const dir = join(repo, '.promptwheel');
@@ -342,7 +343,7 @@ function recordOutcome(repo, report) {
342
343
  } catch { /* recording must never break the gate */ }
343
344
  }
344
345
 
345
- // the flywheel: run any agent/script, gate the result, keep the change ONLY if a metric improved
346
+ // keep-if-improved: run any agent/script, gate the result, keep the change ONLY if a metric improved
346
347
  function improve(argv) {
347
348
  const args = parseArgs(argv);
348
349
  if (!args.attempt) { console.error('improve requires --attempt "<command that changes the repo>"'); process.exit(2); }
@@ -434,10 +435,10 @@ const PRESETS = {
434
435
  'antihack': { desc: 'catch reward-hacking: a target + tripwires; pairs with `run --detect-gaming` (source-only re-run)',
435
436
  metrics: [
436
437
  { name: 'tests_pass', cmd: '__TESTCMD__', extract: 'exit', direction: 'pass', guard: true },
437
- { name: 'test_count', cmd: 'grep -rIoE "\\b(it|test|describe) ?\\(|def test_" --exclude-dir=node_modules --exclude-dir=.git . 2>/dev/null | wc -l | tr -d " "', extract: 'number', direction: 'up', guard: true },
438
- { name: 'skipped_tests', cmd: 'grep -rIoE "\\.(skip|only) ?\\(|xit ?\\(|@pytest\\.mark\\.skip" --exclude-dir=node_modules --exclude-dir=.git . 2>/dev/null | wc -l | tr -d " "', extract: 'number', direction: 'down', guard: true },
439
- { name: 'suppressions', cmd: 'grep -rIoE "eslint-disable|@ts-(ignore|nocheck)|# ?type: ?ignore|# ?noqa" --exclude-dir=node_modules --exclude-dir=.git . 2>/dev/null | wc -l | tr -d " "', extract: 'number', direction: 'down', guard: true },
440
- { name: 'assertions', cmd: 'grep -rIoE "expect ?\\(|\\bassert" --exclude-dir=node_modules --exclude-dir=.git . 2>/dev/null | wc -l | tr -d " "', extract: 'number', direction: 'up', guard: true },
438
+ { name: 'test_count', cmd: 'grep -rIoE "\\b(it|test|describe) ?\\(|def test_" --exclude-dir=node_modules --exclude-dir=.git . 2>/dev/null | wc -l | tr -d " "', extract: 'number', direction: 'up', guard: true, gamingCheck: false },
439
+ { name: 'skipped_tests', cmd: 'grep -rIoE "\\.(skip|only) ?\\(|xit ?\\(|@pytest\\.mark\\.skip" --exclude-dir=node_modules --exclude-dir=.git . 2>/dev/null | wc -l | tr -d " "', extract: 'number', direction: 'down', guard: true, gamingCheck: false },
440
+ { name: 'suppressions', cmd: 'grep -rIoE "eslint-disable|@ts-(ignore|nocheck)|# ?type: ?ignore|# ?noqa" --exclude-dir=node_modules --exclude-dir=.git . 2>/dev/null | wc -l | tr -d " "', extract: 'number', direction: 'down', guard: true, gamingCheck: false },
441
+ { name: 'assertions', cmd: 'grep -rIoE "expect ?\\(|\\bassert" --exclude-dir=node_modules --exclude-dir=.git . 2>/dev/null | wc -l | tr -d " "', extract: 'number', direction: 'up', guard: true, gamingCheck: false },
441
442
  ] },
442
443
  };
443
444
 
@@ -552,7 +553,7 @@ function renderMarkdown(r) {
552
553
  }
553
554
 
554
555
  function parseArgs(argv) {
555
- const a = { json: false };
556
+ const a = { json: false, detectGaming: true }; // reward-hack detection is ON by default; --no-detect-gaming opts out
556
557
  for (let i = 0; i < argv.length; i++) {
557
558
  if (argv[i] === '--base') a.base = argv[++i];
558
559
  else if (argv[i] === '--head') a.head = argv[++i];
@@ -560,6 +561,7 @@ function parseArgs(argv) {
560
561
  else if (argv[i] === '--working') a.working = true;
561
562
  else if (argv[i] === '--no-record') a.noRecord = true;
562
563
  else if (argv[i] === '--detect-gaming' || argv[i] === '--antihack') a.detectGaming = true;
564
+ else if (argv[i] === '--no-detect-gaming' || argv[i] === '--no-antihack') a.detectGaming = false;
563
565
  else if (argv[i] === '--attempt') a.attempt = argv[++i];
564
566
  else if (argv[i] === '--json') a.json = true;
565
567
  else if (argv[i] === '--markdown') a.markdown = true;
@@ -580,13 +582,13 @@ function main() {
580
582
  else if (cmd === 'guards') guards(rest);
581
583
  else {
582
584
  console.log([
583
- 'PromptWheel — the per-turn reward for AI coding loops. Prove a change moved a metric.',
585
+ 'PromptWheel — catch your agent cheating. Prove a change moved a real metric (and that the agent earned it, not gamed it). The per-turn reward + source-only audit for AI coding loops.',
584
586
  '',
585
587
  ' promptwheel init [--preset <name> | --list] write a starter config for your stack',
586
588
  ' promptwheel run [--base R] [--head R] [--repeat N] [--json|--markdown]',
589
+ ' catches reward-hacking BY DEFAULT — re-proves each win from SOURCE edits alone (verdict GAMED,',
590
+ ' exit 2, when a metric only moved by editing tests/config/grader). Use --no-detect-gaming for the bare gate.',
587
591
  ' promptwheel run --working gate uncommitted changes (incl. newly added files)',
588
- ' promptwheel run --detect-gaming re-prove each win with SOURCE edits alone — catch reward-hacking',
589
- ' (verdict GAMED, exit 2, when a metric only moved by editing tests/config/grader)',
590
592
  ' promptwheel improve --attempt "<cmd>" run an agent/script; keep only if a metric improved',
591
593
  ' exit 0=kept · 1=regression · 3=plateau · add --json',
592
594
  ' promptwheel insights which metrics actually respond (loop memory)',
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "promptwheel",
3
- "version": "0.1.0",
4
- "description": "The trustworthy per-turn reward for AI coding loops prove a change moved a metric without regressing another. Before/after in an isolated worktree, regression-guarded, noise-aware.",
3
+ "version": "0.1.2",
4
+ "description": "Catch your agent cheating — a deterministic, zero-LLM CLI that flags when an AI coding agent gamed its own metric (re-proves the win from the agent's source edits alone). Built on an outcome gate; runs in CI or a Claude Code hook.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "promptwheel": "bin/promptwheel.mjs",
@@ -18,5 +18,5 @@
18
18
  "bugs": { "url": "https://github.com/promptwheel-ai/promptwheel/issues" },
19
19
  "author": "Matthew Owens",
20
20
  "license": "MIT",
21
- "keywords": ["agent", "ci", "regression", "metrics", "verification", "ai-coding", "outcome", "evals", "loop", "promptwheel"]
21
+ "keywords": ["agent", "ci", "regression", "metrics", "verification", "ai-coding", "outcome", "evals", "loop", "promptwheel", "reward-hacking", "detect-gaming", "reward-hack-detection"]
22
22
  }