promptwheel 0.7.3 → 0.7.5
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/INTEGRATE.md +29 -41
- package/README.md +102 -170
- package/bin/promptwheel.mjs +185 -286
- package/package.json +3 -5
- package/packs/security/README.md +2 -2
package/INTEGRATE.md
CHANGED
|
@@ -17,10 +17,13 @@ The strongest guarantee and the loosest coupling: emit or consume the two artifa
|
|
|
17
17
|
Contract defines, and any harness interoperates with any other, in any language, without
|
|
18
18
|
importing this package.
|
|
19
19
|
|
|
20
|
-
- **Section 1, exit codes.** `0` pass, `1` fail (a guarded metric regressed beyond noise
|
|
21
|
-
`2` gamed (a win that did not survive a source-only re-run), `3`
|
|
22
|
-
measured nothing, so a pass cannot be certified). An agent reads the
|
|
23
|
-
reward it cannot inflate; a merge gate reads the same code as a wall it
|
|
20
|
+
- **Section 1, exit codes.** `0` pass, `1` fail (a guarded metric regressed beyond noise, or
|
|
21
|
+
a mandate was violated), `2` gamed (a win that did not survive a source-only re-run), `3`
|
|
22
|
+
inconclusive (a guard measured nothing, so a pass cannot be certified). An agent reads the
|
|
23
|
+
code as a per-turn reward it cannot inflate; a merge gate reads the same code as a wall it
|
|
24
|
+
cannot self-clear. A usage or config error (a malformed config, a missing `extends` target,
|
|
25
|
+
a bad flag) exits `64` (`EX_USAGE`), which is not a verdict: a CI step must never read a
|
|
26
|
+
typo as `GAMED`.
|
|
24
27
|
- **Section 2, the verdict JSON.** The object below, reproducible from `(base, head, config)`
|
|
25
28
|
alone.
|
|
26
29
|
|
|
@@ -30,9 +33,7 @@ importing this package.
|
|
|
30
33
|
"head": "e4f5g6h",
|
|
31
34
|
"mode": "refs",
|
|
32
35
|
"repeat": 3,
|
|
33
|
-
"cohort": "ci",
|
|
34
36
|
"verdict": "gamed",
|
|
35
|
-
"subsystems": ["api", "auth"],
|
|
36
37
|
"metrics": [
|
|
37
38
|
{
|
|
38
39
|
"name": "tests_pass",
|
|
@@ -65,9 +66,10 @@ delta inside it is `inconclusive`, never a regression. `retained` is the fractio
|
|
|
65
66
|
metric's gain that survived the source-only re-run (Section 3). `confidence`
|
|
66
67
|
(`unverified | low | medium | high`) and `sourceOnly` are informational fields this
|
|
67
68
|
implementation adds; a minimal conforming report may omit them. `gate` (optional) records a
|
|
68
|
-
truncated SHA-256 fingerprint of the gate definition
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
truncated SHA-256 fingerprint of the gate definition (metrics, guards, `evaluatorPaths`,
|
|
70
|
+
`gamingThreshold`, `mandates`) at base and head, sets `gate.changed: true` when they differ,
|
|
71
|
+
and lists any guard the change dropped to hide a regression; a non-empty `gate.weakened`
|
|
72
|
+
forces `gamed`, so an agent cannot pass by disabling the guard it is judged by.
|
|
71
73
|
|
|
72
74
|
Self-check your implementation against this schema (see Section 4 below):
|
|
73
75
|
|
|
@@ -84,7 +86,7 @@ Node. Run the gate, read the exit code (Section 1), parse stdout (Section 2).
|
|
|
84
86
|
|
|
85
87
|
```bash
|
|
86
88
|
promptwheel run --base "$BASE" --head "$HEAD" --json
|
|
87
|
-
echo $? # 0 pass | 1 fail | 2 gamed | 3 inconclusive
|
|
89
|
+
echo $? # 0 pass | 1 fail | 2 gamed | 3 inconclusive | 64 usage/config error (not a verdict)
|
|
88
90
|
```
|
|
89
91
|
|
|
90
92
|
```bash
|
|
@@ -125,16 +127,18 @@ Returns the Section 2 verdict object (identical to `run --json`). Reproducible f
|
|
|
125
127
|
| `base` | string | merge-base vs `origin/main`..., else `HEAD~1` | base ref |
|
|
126
128
|
| `head` | string | `HEAD` | head ref |
|
|
127
129
|
| `working` | boolean | `false` | gate uncommitted changes instead of `base..head` |
|
|
128
|
-
| `config` | object | load `promptwheel.config.json
|
|
130
|
+
| `config` | object | load `promptwheel.config.json`, or auto-detect | a resolved config `{ metrics: [...], ... }`, supplied in process |
|
|
131
|
+
| `auto` | boolean | `true` | with no config file and no `config` option, gate the auto-detected test command (zero-config); `false` throws instead |
|
|
129
132
|
| `repeat` | number | `config.repeat ?? 1` | measurement repeats for the noise band |
|
|
130
133
|
| `detectGaming` | boolean | `true` | run the Section 3 source-only re-proof |
|
|
131
|
-
| `
|
|
132
|
-
| `label` | string | none | change-type label |
|
|
134
|
+
| `allow` | string | none | a declared override reason (the same as `--allow` / a `Promptwheel-Allow:` trailer): a would-be `gamed` reads `pass` with the reason recorded in `report.allowed` |
|
|
133
135
|
| `record` | boolean | `false` | append the run to `.promptwheel/outcomes.jsonl` |
|
|
134
136
|
|
|
135
|
-
Preconditions throw (they do not exit the host): not inside a git repository;
|
|
136
|
-
`promptwheel.config.json` and no `config` option; a `config` with no non-empty
|
|
137
|
-
array.
|
|
137
|
+
Preconditions throw (they do not exit the host): not inside a git repository; `auto: false`
|
|
138
|
+
with no `promptwheel.config.json` and no `config` option; a `config` with no non-empty
|
|
139
|
+
`metrics` array. With neither a file nor `config`, the default is zero-config: the detected
|
|
140
|
+
test command is gated (an inert command reads `inconclusive`, never a fake pass). Supplying
|
|
141
|
+
`config` needs no file on disk:
|
|
138
142
|
|
|
139
143
|
```js
|
|
140
144
|
const report = check({
|
|
@@ -185,29 +189,13 @@ implementation earns the right to say "Verified-Outcome Contract compatible."
|
|
|
185
189
|
|
|
186
190
|
---
|
|
187
191
|
|
|
188
|
-
##
|
|
192
|
+
## Related
|
|
189
193
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
- **Import.** The package is importable (`@promptwheel/logbook`) and importing it never runs the
|
|
199
|
-
CLI; `queryEvents`, `analyze`, `auditHead`, and `hotspots` are exported for advanced in-process
|
|
200
|
-
use. The scan and cache entry points take explicit options, so the CLI and MCP are the
|
|
201
|
-
first-class surfaces for most integrations. The reference harness (Sawbuck) consumes the
|
|
202
|
-
logbook by shelling `logbook query --json`, not by importing it, which keeps the same process
|
|
203
|
-
boundary the check engine uses.
|
|
204
|
-
|
|
205
|
-
---
|
|
206
|
-
|
|
207
|
-
## The layer boundary (why this stays thin)
|
|
208
|
-
|
|
209
|
-
The engines are deterministic and model-blind: no LLM call, no network, no runtime
|
|
210
|
-
dependency, no build step. A harness that orchestrates models (Sawbuck is the reference one)
|
|
211
|
-
consumes these engines across a process boundary, by shelling the CLI or resolving the bin,
|
|
212
|
-
and never imports their internals. Keep that boundary and the check stays something a
|
|
213
|
-
skeptic can re-run and get the same verdict, which is the entire point.
|
|
194
|
+
PromptWheel is the check: it verifies what holds now. The history side is served by
|
|
195
|
+
[`@promptwheel/logbook`](https://www.npmjs.com/package/@promptwheel/logbook), a separate
|
|
196
|
+
zero-dependency package that turns a repo's git history into agent-readable memory
|
|
197
|
+
(hotspots, reverts, suppressions), shelled the same way (`logbook query --json`). Both are
|
|
198
|
+
deterministic and model-blind: no LLM call, no network, no build step. A harness that
|
|
199
|
+
orchestrates models should consume them across a process boundary (shell the CLI, parse
|
|
200
|
+
`--json`) rather than importing their internals, so a skeptic can always re-run the check and
|
|
201
|
+
get the same verdict.
|
package/README.md
CHANGED
|
@@ -2,51 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/promptwheel-ai/promptwheel/actions/workflows/ci.yml) [](LICENSE)  
|
|
4
4
|
|
|
5
|
-
**A deterministic check for your coding loop.** A green test should mean your agent *fixed the code*, not that it edited the test to match the bug. PromptWheel
|
|
5
|
+
**A deterministic, zero-LLM check for your coding loop.** A green test should mean your agent *fixed the code*, not that it edited the test to match the bug. PromptWheel re-proves every win from the agent's source edits alone, so a green that came from editing the test, grader or config reads `GAMED`. The code being checked never gets to grade itself.
|
|
6
6
|
|
|
7
7
|

|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Catch your agent cheating, in 60 seconds
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
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.)
|
|
14
|
-
|
|
15
|
-
PromptWheel is the reference check for the [Verified-Outcome Contract](https://github.com/sawbuck-ai/sawbuck/blob/master/CONTRACT.md), the open interface for a verifiable coding loop. Use it from CI, a Claude Code hook, or in process (`import { check } from 'promptwheel'`); the four integration paths (conform, shell, import, MCP) are in [INTEGRATE.md](INTEGRATE.md).
|
|
16
|
-
|
|
17
|
-
> 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.
|
|
18
|
-
|
|
19
|
-
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.
|
|
20
|
-
|
|
21
|
-
For any change (`base` → `head`), it measures each configured metric in an **isolated git worktree** before and after, **refuses to trust a delta inside the measurement noise band**, enforces **regression guards**, and emits a structured verdict:
|
|
22
|
-
|
|
23
|
-
```
|
|
24
|
-
PromptWheel a1b2c3d → e4f5g6h (×5)
|
|
25
|
-
|
|
26
|
-
= tests_pass 1 → 1 (0, unchanged) [guard✓, high]
|
|
27
|
-
▼ lint_errors 12 → 7 (-5, improved) [guard✓, high]
|
|
28
|
-
▲ bundle_kb 340 → 352 (+12, regressed) [info, medium]
|
|
29
|
-
▼ p95_ms 210 → 208 (-2, inconclusive) [info, low]
|
|
30
|
-
|
|
31
|
-
VERDICT: PASS
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
Exit `0` pass · `1` fail · `2` gamed · `3` inconclusive (a guard measured nothing), CI-friendly. No build step, zero dependencies, Node 18+.
|
|
35
|
-
|
|
36
|
-
## Catch your agent cheating, on by default
|
|
37
|
-
|
|
38
|
-
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)**:
|
|
39
|
-
|
|
40
|
-
```
|
|
41
|
-
$ promptwheel run
|
|
42
|
-
|
|
43
|
-
PromptWheel base → head
|
|
44
|
-
▲ tests_pass 0 → 1 (+1, improved) [guard✓, high]
|
|
45
|
-
🚩 GAMED, the "win" changed zero production-source files, only test/config/grader/golden
|
|
46
|
-
VERDICT: GAMED, a metric "improved" by editing the goalposts, not the source
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
**See it fire yourself**, 60 seconds, illustrative (a hand-built cheat is a *mechanism* demo, not evidence):
|
|
11
|
+
A hand-built cheat, so you can watch the mechanism fire (illustrative, not evidence):
|
|
50
12
|
|
|
51
13
|
```bash
|
|
52
14
|
mkdir pw-cheat-demo && cd pw-cheat-demo && git init -q
|
|
@@ -56,7 +18,7 @@ printf "import {test} from 'node:test'; import assert from 'node:assert'; import
|
|
|
56
18
|
git add -A && git commit -qm 'buggy code + an honest failing test'
|
|
57
19
|
npx -y promptwheel@latest init && git add -A && git commit -qm 'add the gate config'
|
|
58
20
|
|
|
59
|
-
# an agent "greens" the suite by editing the TEST to expect the bug
|
|
21
|
+
# an agent "greens" the suite by editing the TEST to expect the bug; the code stays broken:
|
|
60
22
|
printf "import {test} from 'node:test'; import assert from 'node:assert'; import {add} from './add.js';\ntest('add', () => assert.equal(add(2, 3), -1));\n" > add.test.js
|
|
61
23
|
git commit -qam 'make the suite green'
|
|
62
24
|
|
|
@@ -65,90 +27,115 @@ npx -y promptwheel@latest run --base HEAD~1 --head HEAD
|
|
|
65
27
|
# VERDICT: GAMED (exit 2)
|
|
66
28
|
```
|
|
67
29
|
|
|
68
|
-
|
|
30
|
+
What happened: PromptWheel measured `tests_pass` at both commits in throwaway git worktrees (0 then 1, a win), then split the diff into production source versus `{test, config, grader, golden}` files, re-applied **only the source slice** on a clean worktree at the base, and re-ran the metric. The win did not survive, so it is `GAMED`. No LLM, no network: a diff partition plus a re-run, reproducible in seconds with a human-readable reason.
|
|
31
|
+
|
|
32
|
+
The same runs on any change in any stack. For a change `base` to `head` it measures each configured metric before and after, refuses to trust a delta inside the measurement noise band, enforces regression guards, and emits one verdict:
|
|
69
33
|
|
|
70
|
-
```bash
|
|
71
|
-
npx promptwheel init # guarded tests + antihack tripwires by default
|
|
72
|
-
npx promptwheel run # detection ON by default · exit 0 win · 1 regression · 2 GAMED · 3 inconclusive
|
|
73
|
-
# (add --no-detect-gaming for just the outcome gate)
|
|
74
34
|
```
|
|
35
|
+
PromptWheel a1b2c3d → e4f5g6h (×5)
|
|
75
36
|
|
|
76
|
-
|
|
37
|
+
= tests_pass 1 → 1 (0, unchanged) [guard✓, high]
|
|
38
|
+
▼ lint_errors 12 → 7 (-5, improved) [guard✓, high]
|
|
39
|
+
▲ bundle_kb 340 → 352 (+12, regressed) [info, medium]
|
|
40
|
+
▼ p95_ms 210 → 208 (-2, inconclusive) [info, low]
|
|
77
41
|
|
|
78
|
-
|
|
42
|
+
VERDICT: PASS
|
|
43
|
+
```
|
|
79
44
|
|
|
80
|
-
|
|
45
|
+
Exit `0` pass · `1` fail (a guarded metric regressed, or a mandate was violated) · `2` gamed · `3` inconclusive (a guard measured nothing) · `64` usage or config error (never a verdict). No build step, zero dependencies, Node 18+. The same verdict runs as a PR check in CI, as a per-turn signal inside a coding loop, or in process (`import { check } from 'promptwheel'`); it is the reference check for the [Verified-Outcome Contract](https://github.com/sawbuck-ai/sawbuck/blob/master/CONTRACT.md), and the integration paths are in [INTEGRATE.md](INTEGRATE.md).
|
|
81
46
|
|
|
82
|
-
##
|
|
47
|
+
## Install and use
|
|
83
48
|
|
|
84
49
|
```bash
|
|
85
|
-
|
|
86
|
-
npx promptwheel init # detects stack → guarded tests + antihack tripwires (+ lint if eslint is set up)
|
|
87
|
-
npx promptwheel init --list # presets: tests-pass · lint · bundle-size · llm-eval · antihack
|
|
50
|
+
npm i -g promptwheel # or run everything through: npx promptwheel ...
|
|
88
51
|
|
|
89
|
-
#
|
|
90
|
-
npx promptwheel run
|
|
91
|
-
npx promptwheel run --working # measure UNCOMMITTED changes (incl. newly added files)
|
|
52
|
+
npx promptwheel run # zero-config: gates your detected test command; base = merge-base with main, head = HEAD
|
|
53
|
+
npx promptwheel run --working # gate UNCOMMITTED changes (incl. newly added files)
|
|
92
54
|
npx promptwheel run --repeat 5 --json # measure 5× to establish a noise band, emit JSON
|
|
93
55
|
|
|
94
|
-
#
|
|
95
|
-
npx promptwheel
|
|
96
|
-
# exit 0 = kept a real win · 1 = guarded regression (reverted) · 3 = plateau / inconclusive (reverted) · add --json
|
|
56
|
+
npx promptwheel init # optional: write promptwheel.config.json (guarded tests + antihack tripwires, + lint if eslint is set up)
|
|
57
|
+
npx promptwheel init --list # presets: tests-pass · lint · bundle-size · security · llm-eval · antihack · no-ai-tells · security-antihack
|
|
97
58
|
|
|
98
|
-
#
|
|
99
|
-
npx promptwheel
|
|
59
|
+
# the loop: run any agent/script, keep the change ONLY if a metric improved, else revert
|
|
60
|
+
npx promptwheel improve --attempt "claude -p 'reduce lint errors'"
|
|
61
|
+
# exit 0 = kept a real win · 1 = guarded regression (reverted) · 2 = gamed (reverted) · 3 = plateau / inconclusive (reverted) · add --json
|
|
62
|
+
while npx promptwheel improve --attempt "$AGENT"; do :; done # converges: stops on the first non-zero
|
|
100
63
|
|
|
101
|
-
|
|
102
|
-
npx promptwheel
|
|
103
|
-
npx promptwheel suggest # UCB over the lever scores, proven levers vs under-explored arms
|
|
104
|
-
npx promptwheel backfill -n 30 # cold start: seed the record from git history (cohort-tagged, commit types → labels)
|
|
64
|
+
npx promptwheel insights # which metrics actually respond in this repo (counts from .promptwheel/outcomes.jsonl)
|
|
65
|
+
npx promptwheel guards # the effective guardrails, including ones inherited through extends
|
|
105
66
|
```
|
|
106
67
|
|
|
107
|
-
|
|
68
|
+
Gaming detection is on by default in `run` and `improve`; `--no-detect-gaming` gives you the bare outcome gate.
|
|
108
69
|
|
|
109
|
-
`promptwheel init` writes this into your `AGENTS.md`
|
|
70
|
+
**Give your agent the gate.** `promptwheel init` writes this into your `AGENTS.md` (and `CLAUDE.md` / `.cursorrules` if present; `--no-agent-block` to skip), so the coding agent runs the gate itself. To paste it yourself:
|
|
110
71
|
|
|
111
72
|
After a change that should move a measurable metric (tests, lint, coverage, an eval), run
|
|
112
73
|
`npx promptwheel run --working`. Exit 2 (verdict GAMED) means the win came from editing a test,
|
|
113
74
|
config, or grader, not the source, so revert and earn it in the source. Exit 1 means a guarded
|
|
114
75
|
metric regressed, revert that too. Never weaken, skip, or delete a test to go green.
|
|
115
76
|
|
|
116
|
-
**
|
|
117
|
-
|
|
118
|
-
**Footprint:** it never touches your working tree, every measurement runs in a throwaway git worktree **in your system temp dir** (one at a time; configured `linkDirs` are symlinked not copied, `node_modules` by default, `.venv` for Python, etc.), removed when the run finishes. The only thing PromptWheel writes to your repo is the optional `.promptwheel/outcomes.jsonl` record, commit it to build the per-repo "what moves what" history, or `.gitignore` it (`--no-record` to skip entirely). A hard-killed run can't leave clutter behind: the next run **self-heals** any orphaned worktree (stale registry entry + abandoned temp checkout).
|
|
77
|
+
**Two callers, by design.** Your *agent* consumes the verdict as a per-turn signal (`improve`, or `run --working --json`). The *harness* (a Stop-hook, or CI) runs the same check as an audit the agent cannot self-clear, because the check re-proves from the source alone and never asks the code to grade itself. See [docs/ENFORCEMENT.md](docs/ENFORCEMENT.md) for the three places to enforce it.
|
|
119
78
|
|
|
120
|
-
|
|
79
|
+
## Config, `promptwheel.config.json`
|
|
121
80
|
|
|
122
|
-
|
|
81
|
+
`run` works with no config (it gates your detected test command). A config says *what* to measure:
|
|
123
82
|
|
|
124
|
-
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"repeat": 1,
|
|
86
|
+
"metrics": [
|
|
87
|
+
{ "name": "tests_pass", "cmd": "npm test --silent", "extract": "exit", "direction": "pass", "guard": true },
|
|
88
|
+
{ "name": "lint_errors", "cmd": "npx eslint . | grep -c error", "extract": "number", "direction": "down", "guard": true },
|
|
89
|
+
{ "name": "bundle_kb", "cmd": "du -sk dist | cut -f1", "extract": "number", "direction": "down", "guard": false }
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
```
|
|
125
93
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
94
|
+
- **cmd**, any shell command, run inside the worktree.
|
|
95
|
+
- **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).
|
|
96
|
+
- **direction**, `up` (higher better) · `down` (lower better) · `pass` (boolean 0/1).
|
|
97
|
+
- **guard**, `true` = a *trusted* regression **fails** the gate; `false` = informational.
|
|
98
|
+
- **gamingCheck**, `false` exempts a metric from the 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.
|
|
99
|
+
- **gamingThreshold**, the fraction of a win that must survive the source-only re-run to count as earned (default `0.5`). Config-level scalar, overridable per metric; inherited through `extends` like `repeat`.
|
|
100
|
+
- **evaluatorPaths**, globs for files that *define* your gate (a threshold file, a budget, a CI limit). Editing one in the same diff that improves a metric is `GAMED` by declaration, deterministically. Opt-in, because only you know which files are the goalposts.
|
|
129
101
|
|
|
130
|
-
|
|
131
|
-
npx promptwheel run --working --json # branch on .verdict / per-metric .status
|
|
132
|
-
```
|
|
102
|
+
Dependency and environment keys (config-level; `init` writes stack defaults):
|
|
133
103
|
|
|
134
|
-
|
|
104
|
+
- **linkDirs**, dirs to symlink from your checkout into each measuring worktree, for deps that live outside git. Default `["node_modules"]`; `init` writes `[".venv"]` for Python and `[]` for Rust (`cargo test` writes to `target/`, so it is built fresh in the worktree rather than shared across refs). `"linkNodeModules": false` still works as shorthand for "link nothing".
|
|
105
|
+
- **env**, environment variables for every metric command; `{wt}` is substituted with the worktree path. Python configs get `"PYTHONPATH": "{wt}/src:{wt}"` so the *measured ref* is imported, not an editable install of your original checkout.
|
|
106
|
+
- **setup**, an optional command run once per ref before metrics (for example `npm run build` or `pip install -e .`), after any source patch, so a build-gated suite measures the ref it is actually on.
|
|
135
107
|
|
|
136
|
-
|
|
108
|
+
Copy a config for your stack (pytest, tsc, coverage, bundle size, LLM eval) from [examples/](examples/).
|
|
137
109
|
|
|
138
|
-
|
|
110
|
+
### Guardrails and inheritance
|
|
139
111
|
|
|
140
|
-
|
|
112
|
+
Teams keep one shared base config and have every repo inherit it via `extends`:
|
|
141
113
|
|
|
114
|
+
```json
|
|
115
|
+
// promptwheel.config.json
|
|
116
|
+
{ "extends": "./promptwheel.base.json",
|
|
117
|
+
"metrics": [ { "name": "cost_per_run_usd", "guard": false } ] } // loosen one inherited guard, locally
|
|
142
118
|
```
|
|
143
|
-
|
|
144
|
-
|
|
119
|
+
|
|
120
|
+
`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 record. Read this way, `extends` is the set of shared invariants your agents are held to, enforced as measured guards rather than advisory text. Natural-language conventions belong in `AGENTS.md`; only deterministic, measurable guards belong here.
|
|
121
|
+
|
|
122
|
+
### Mandates (protected paths)
|
|
123
|
+
|
|
124
|
+
Some rules are not a metric at all, they are a hard line: *never touch `src/auth/**`*, *the migration files are frozen*. Declare those as `mandates`, and any change that touches a protected path **fails the gate before a single metric is read**, however green everything else is:
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
// promptwheel.config.json
|
|
128
|
+
{ "mandates": [
|
|
129
|
+
{ "name": "no-auth-edits", "paths": ["src/auth/**"], "reason": "auth changes go through a dedicated, reviewed PR" }
|
|
130
|
+
],
|
|
131
|
+
"metrics": [ /* ... */ ] }
|
|
145
132
|
```
|
|
146
133
|
|
|
147
|
-
|
|
134
|
+
A mandate is deterministic (a path match on the diff, zero-LLM), rides at the top of the report, and **inherits through `extends`** so a parent scope's hard rule holds for every repo beneath it. It is folded into the gate fingerprint at both refs, so quietly deleting a mandate changes the fingerprint: the report records `gate.changed: true` and prints a "gate definition changed" line, a tracked, tamper-evident change. And deleting it does not clear it: the mandates in force are the union of the base ref's and the head's, so a change that removes a mandate and touches its path in the same diff still fails (reported with `removed: true`). Retire a mandate in its own change, with the protected path untouched. (Before 0.7.5 the base-ref fingerprint did not include mandates, so this was recorded backwards: an unchanged mandate read as a changed gate and a deleted one did not, and the deletion cleared the rule.) This is the line between a contract and a request: `AGENTS.md` can *ask* an agent not to touch auth; a mandate will not let the change past the gate.
|
|
148
135
|
|
|
149
|
-
## In CI
|
|
136
|
+
## In CI and in Claude Code
|
|
150
137
|
|
|
151
|
-
Drop this in your repo
|
|
138
|
+
**GitHub Action.** Drop this in your repo; it posts a verdict comment on every PR and fails the check on a guarded regression, a gamed win, or an inconclusive gate:
|
|
152
139
|
|
|
153
140
|
```yaml
|
|
154
141
|
# .github/workflows/promptwheel.yml
|
|
@@ -165,104 +152,49 @@ jobs:
|
|
|
165
152
|
with: { repeat: '3' }
|
|
166
153
|
```
|
|
167
154
|
|
|
168
|
-
The Action runs straight from its own checkout, no npm install, no build. See [`action.yml`](action.yml).
|
|
155
|
+
The Action runs straight from its own checkout, no npm install, no build. See [`action.yml`](action.yml); a copy-paste workflow file lives at [`examples/github-action.yml`](examples/github-action.yml). Make the check a branch-protection requirement and the agent cannot merge past it.
|
|
169
156
|
|
|
170
|
-
|
|
157
|
+
**Claude Code plugin.** Bring the gate in as slash commands (the plugin wraps the CLI, so install that too: `npm i -g promptwheel`):
|
|
171
158
|
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
"metrics": [
|
|
176
|
-
{ "name": "tests_pass", "cmd": "npm test --silent", "extract": "exit", "direction": "pass", "guard": true },
|
|
177
|
-
{ "name": "lint_errors", "cmd": "npx eslint . | grep -c error", "extract": "number", "direction": "down", "guard": true },
|
|
178
|
-
{ "name": "bundle_kb", "cmd": "du -sk dist | cut -f1", "extract": "number", "direction": "down", "guard": false }
|
|
179
|
-
]
|
|
180
|
-
}
|
|
159
|
+
```
|
|
160
|
+
/plugin marketplace add promptwheel-ai/promptwheel
|
|
161
|
+
/plugin install promptwheel@promptwheel-ai
|
|
181
162
|
```
|
|
182
163
|
|
|
183
|
-
|
|
184
|
-
- **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).
|
|
185
|
-
- **direction**, `up` (higher better) · `down` (lower better) · `pass` (boolean 0/1).
|
|
186
|
-
- **guard**, `true` = a *trusted* regression **fails** the gate; `false` = informational.
|
|
187
|
-
- **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.
|
|
188
|
-
- **gamingThreshold**, the fraction of a win that must survive the source-only re-run to count as earned (default `0.5`). Config-level scalar, overridable per metric; inherited through `extends` like `repeat`.
|
|
164
|
+
Then `/promptwheel:setup` (write a config) · `/promptwheel:gate` (gate uncommitted changes) · `/promptwheel:improve <cmd>` (keep-if-improved) · `/promptwheel:insights` · `/promptwheel:logbook`. See [`plugins/promptwheel/`](plugins/promptwheel/). A Stop-hook that refuses to let an agent end a turn on a `GAMED` win ships at [`examples/detect-gaming-stop.sh`](examples/detect-gaming-stop.sh).
|
|
189
165
|
|
|
190
|
-
|
|
166
|
+
## Trust model
|
|
191
167
|
|
|
192
|
-
-
|
|
193
|
-
- **env**, environment variables for every metric command; `{wt}` is substituted with the worktree path. Python configs get `"PYTHONPATH": "{wt}/src:{wt}"` so the *measured ref* is imported, not an editable install of your original checkout.
|
|
194
|
-
- **setup**, an optional command run once per ref before metrics (e.g. `npm run build`, `pip install -e .`), after any source patch, so a build-gated suite measures the ref it's actually on.
|
|
168
|
+
**What it catches.** After the normal gate, every *win* is re-proven using the agent's source edits alone. If the win only survived by skipping or deleting a test, mocking the grader, editing a golden, relaxing a config, or touching zero source files, the verdict is `GAMED` (exit 2). On top of that:
|
|
195
169
|
|
|
196
|
-
|
|
170
|
+
- **Tripwire guards** (`test_count`, `skipped_tests`, `suppressions`, `assertions`, written by the plain `init` default) catch the quieter shapes: an inline suppression (`@ts-nocheck`, `eslint-disable`, `# noqa`) or a suite gutted while the metric stays flat.
|
|
171
|
+
- **Gate integrity.** The gate is resolved as of the base commit too. A change that disables a guard or deletes a guarded metric in the same diff it passes is re-measured under the base definition and reads `GAMED` if that guard would have failed. The gate definition (metrics, guards, `evaluatorPaths`, `gamingThreshold`, `mandates`) is fingerprinted at both refs; a difference is recorded as `gate.changed` and printed.
|
|
172
|
+
- **A sanctioned override.** When you *do* mean to change a test, config, or grader, declare it: a `Promptwheel-Allow: <reason>` trailer on the commit (`Sawbuck-Allow:` is accepted as an alias), `--allow "<reason>"` on the CLI, or the `allow` option on `check()`, turns what would read `GAMED` into a `pass` with the reason recorded in `report.allowed`. An undeclared test-only win is still `gamed`, and a reasonless declaration does not override. The check never judges intent; it forces intent to be explicit and auditable.
|
|
173
|
+
- **A provenance receipt** on every verdict (`report.provenance`: base and head SHAs, a config hash, a verdict hash), so a skeptic can re-run `(base, head, config)` and get the same hash.
|
|
197
174
|
|
|
198
|
-
|
|
175
|
+
**The honest limit.** This is one layer, not a silver bullet. It catches the evaluator-tampering class (test, grader, golden and config edits) deterministically and for free. It is blind, by construction, to a cheat that lives in the source file itself (a hardcoded answer, behavior that differs under test): that needs held-out tests, an LLM judge, or a human, and those expensive layers should be reserved for the calls only they can make. The coverage matrix and the in/out-of-scope boundary are in [docs/DETECTION-LAYERS.md](docs/DETECTION-LAYERS.md); the measured numbers are in [bench/RESULTS.md](bench/RESULTS.md) (`node bench/gaming-bench.mjs` to reproduce). Another gameable thing, an LLM judge asking "did you cheat?", is deliberately not part of the check.
|
|
199
176
|
|
|
200
|
-
|
|
201
|
-
promptwheel guards
|
|
202
|
-
```
|
|
177
|
+
**Noise.** A number that jumps around between runs is worthless as a signal, and PromptWheel does not pretend otherwise: `--repeat N` measures each metric N times at both refs and uses the median; the noise band is the observed spread; a delta inside the band is `inconclusive` with `low` confidence and does not fail a guard (no flaky CI failures). Confidence reads `high` (deterministic extract, or zero observed noise), `medium` (delta clears the noise band), `low` (delta inside noise), or `unverified` (single read; run `--repeat` to earn trust). A guard whose command never ran (same value at both refs) makes the verdict `inconclusive`, never a fake pass.
|
|
203
178
|
|
|
204
|
-
|
|
179
|
+
**When not to use it.** If you only care that tests pass and nobody could be editing the tests, your plain CI already has you covered. PromptWheel earns its place when the thing writing the code can also touch the thing judging it, or when you have a graded metric beyond pass/fail (an eval score, cost per run, latency, size) that a change could quietly move. Single-metric CI gates (benchmark trackers, size limits) go deeper on one number; wrap them as a metric `cmd` and PromptWheel composes them into one verdict.
|
|
205
180
|
|
|
206
|
-
|
|
207
|
-
// promptwheel.config.json
|
|
208
|
-
{ "extends": "./promptwheel.base.json",
|
|
209
|
-
"metrics": [ { "name": "cost_per_run_usd", "guard": false } ] } // loosen one inherited guard, locally
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
`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.
|
|
213
|
-
|
|
214
|
-
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.)
|
|
181
|
+
## Per-turn cost
|
|
215
182
|
|
|
216
|
-
|
|
183
|
+
A check runs your metric commands in two throwaway worktrees (before and after) plus a source-only re-run when a metric improved, so one check takes roughly 2 to 3× your metric command's runtime plus about 40ms of worktree overhead. Point the per-turn gate at a *fast* metric (a lint count, a quick test subset, a build-size check) and leave the slow full suite for CI: a 50ms check is invisible in a loop, a two-minute one is not. A deterministic metric (a test exit, a count) is accurate at the default `repeat: 1`; only a noisy metric (latency, timing) needs `repeat: 3` or more, which multiplies the cost.
|
|
217
184
|
|
|
218
|
-
|
|
185
|
+
**Footprint.** It never touches your working tree: every measurement runs in a throwaway git worktree in your system temp dir (one at a time; configured `linkDirs` are symlinked, not copied), removed when the run finishes. The only thing PromptWheel writes to your repo is the optional `.promptwheel/outcomes.jsonl` record; commit it, `.gitignore` it, or pass `--no-record`. A hard-killed run cannot leave clutter behind: the next run self-heals any orphaned worktree.
|
|
219
186
|
|
|
220
|
-
|
|
221
|
-
- A delta **inside the noise band** is reported `inconclusive` with `low` confidence and **does not fail a guard** (no flaky CI failures).
|
|
222
|
-
- **Confidence:** `high` (deterministic extract, or zero observed noise) · `medium` (delta clears the noise band) · `low` (delta inside noise) · `unverified` (single read, run `--repeat` to earn trust).
|
|
187
|
+
## Portability
|
|
223
188
|
|
|
224
|
-
The
|
|
225
|
-
|
|
226
|
-
## Where PromptWheel fits (and where it doesn't)
|
|
227
|
-
|
|
228
|
-
- **vs single-axis CI gates** (Codspeed, Bencher, size-limit, Lighthouse-CI): they own deep statistics on *one* metric; PromptWheel is the **cross-metric gate that composes them**, "did `eval_pass_rate` **and** cost improve without regressing the guards?" in one verdict. Wrap any of them as a metric `cmd` and let `--repeat` handle the noise.
|
|
229
|
-
- **vs loop/agent frameworks** (Ralph, GEPA, reward models): PromptWheel is the **execution-grounded reward they lack**, it runs your real suite with zero deps; it does not drive the loop or do test-time search.
|
|
230
|
-
- **When NOT to use it:** if you only care that tests pass, your base verifier already has you covered. PromptWheel earns its place when you have a **graded numeric metric** beyond pass/fail (eval score, $/run, latency, size) that a change could quietly move.
|
|
189
|
+
The core gamed-win check is git only and runs anywhere git does: it classifies the files a win touched (`git diff --name-only`) to separate production source from test, config, and grader files, so a green that came only from editing the goalposts is caught the same on Linux, macOS, and Windows (the `gamed-catch` CI job proves this on all three). The starter tripwire metrics that `init` writes are `grep` one-liners that assume a GNU-grep-compatible shell, which PromptWheel runs through the system shell (`sh` on Linux/macOS, `cmd` on native Windows). Where grep is absent, those metrics read unmeasurable, and an unmeasurable guard fails closed (verdict `FAIL`), so a gamed green is still caught, never passed; it just lands as `FAIL` rather than the dedicated `GAMED`. Run under WSL or git-bash, or replace those tripwire `cmd`s with your toolchain's own counters, for a clean verdict on Windows.
|
|
231
190
|
|
|
232
191
|
## Docs
|
|
233
192
|
|
|
234
|
-
- [
|
|
235
|
-
- [docs/
|
|
236
|
-
- [docs/
|
|
237
|
-
- [docs/ROADMAP.md](docs/ROADMAP.md), the phased plan and the ship-now/stay-thin guardrails.
|
|
193
|
+
- [INTEGRATE.md](INTEGRATE.md), the integration surface: the exit-code and verdict-JSON contract, shelling `--json`, `import { check }`, and `promptwheel conformance` for self-checking a third-party implementation.
|
|
194
|
+
- [docs/DETECTION-LAYERS.md](docs/DETECTION-LAYERS.md), how the source-only re-run fits as the deterministic layer alongside held-out tests, LLM judges, and human review: the coverage matrix and the honest in/out-of-scope boundary.
|
|
195
|
+
- [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 Claude Code Stop-hook), the exact wiring, and the protect-the-gate's-own-config caveat.
|
|
238
196
|
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md), how the engine works: schemas, extract modes, the trust/noise model.
|
|
239
|
-
- [
|
|
240
|
-
- [CLAUDE.md](CLAUDE.md), the
|
|
241
|
-
|
|
242
|
-
## Develop
|
|
243
|
-
|
|
244
|
-
```bash
|
|
245
|
-
npm test # the dep-free suite (node:test), unit + integration, no dependencies
|
|
246
|
-
```
|
|
197
|
+
- [CHANGELOG.md](CHANGELOG.md), what shipped in each version.
|
|
198
|
+
- [CLAUDE.md](CLAUDE.md), the working notes for anyone (human or agent) changing this repo.
|
|
247
199
|
|
|
248
|
-
The engine is one importable file; pure helpers are exported for unit tests
|
|
249
|
-
|
|
250
|
-
## Roadmap
|
|
251
|
-
|
|
252
|
-
- [x] before/after worktree measurement + regression guards
|
|
253
|
-
- [x] noise band + confidence (don't trust a delta inside the jitter)
|
|
254
|
-
- [x] `--working` mode, measure uncommitted changes (tracked **and** untracked)
|
|
255
|
-
- [x] persisted reward stream (`.promptwheel/outcomes.jsonl`), the compounding "what moves what" record
|
|
256
|
-
- [x] GitHub Action / PR-comment wrapper (open-core distribution surface)
|
|
257
|
-
- [x] agent loop: `improve`, propose → gate → keep only if a metric improved
|
|
258
|
-
- [x] loop-consumable `improve`: exit `0` kept / `1` regression / `3` plateau + `--json result`
|
|
259
|
-
- [x] `promptwheel init` + presets, zero-config onboarding
|
|
260
|
-
- [x] `insights`, reward-stream aggregation (Phase-5 seed)
|
|
261
|
-
- [x] `--detect-gaming`, reward-hack detection: re-prove the win from source edits alone + `antihack` preset
|
|
262
|
-
- [x] npm publish, `promptwheel@0.4.2` (the lead magnet)
|
|
263
|
-
- [x] outcome-curated learning + UCB work-discovery, `playbook` + `suggest` + the compounding A/B harness (**experimental**; compounding *claims* stay gated on real-data proof, see [docs/LEARNING.md](docs/LEARNING.md))
|
|
264
|
-
- [x] gate integrity: a win that disables a guard or deletes a guarded metric in the same diff reads `GAMED` (SHA-256 gate fingerprint; 0.5.0)
|
|
265
|
-
- [x] zero-config: `run` and `check()` auto-detect the gate with no committed config, `init` becomes optional (0.6.0)
|
|
266
|
-
- [x] declared override (`Sawbuck-Allow:` trailer / `--allow`) + `init` build-setup detection, so a build-gated test measures instead of reading inert (0.7.0)
|
|
267
|
-
|
|
268
|
-
> Status: **published** (npm `promptwheel`, v0.7.1), all core phases built. Lineage: CommandLayer → BlockSpool → PromptWheel (orchestrator, archived) → **PromptWheel (outcome gate)**.
|
|
200
|
+
Develop: `npm test` runs the dep-free suite (`node:test`, unit + integration). The engine is one importable file; pure helpers are exported for unit tests and the CLI runs only when invoked directly. Add a test with every behavior change.
|