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 +21 -10
- package/bin/promptwheel.mjs +12 -10
- package/package.json +3 -3
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
|
|
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")
|
|
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 —
|
|
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
|
|
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
|
|
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 #
|
|
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
|
-
- [
|
|
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
|
|
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)**.
|
package/bin/promptwheel.mjs
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
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 —
|
|
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.
|
|
4
|
-
"description": "
|
|
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
|
}
|