promptwheel 0.7.0 → 0.7.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 +15 -1
- package/bin/promptwheel.mjs +25 -1
- package/package.json +2 -2
- package/packs/security/scan.mjs +2 -2
package/README.md
CHANGED
|
@@ -75,6 +75,8 @@ npx promptwheel run # detection ON by default · exit 0 wi
|
|
|
75
75
|
|
|
76
76
|
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 via `gamingThreshold` (config-level, or per-metric).
|
|
77
77
|
|
|
78
|
+
**An intentional goalpost change has a sanctioned path.** When you *do* mean to change a test, config, or grader, declare it: a `Sawbuck-Allow: <reason>` trailer on the commit (or `--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 just forces intent to be explicit and auditable (the reason lives in the commit, so a reviewer sees it instead of a silent green).
|
|
79
|
+
|
|
78
80
|
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, it includes a cross-stack table with a real pytest run + a numeric eval-pass-rate metric). **Gate your own stack**, pytest · tsc · coverage · bundle · llm-eval, copy a config from **[examples/](examples/)**.
|
|
79
81
|
|
|
80
82
|
## Use
|
|
@@ -102,6 +104,15 @@ npx promptwheel suggest # UCB over the lever scores, proven le
|
|
|
102
104
|
npx promptwheel backfill -n 30 # cold start: seed the record from git history (cohort-tagged, commit types → labels)
|
|
103
105
|
```
|
|
104
106
|
|
|
107
|
+
## Give your agent the gate
|
|
108
|
+
|
|
109
|
+
`promptwheel init` writes this into your `AGENTS.md` for you (and `CLAUDE.md` / `.cursorrules` if present; `--no-agent-block` to skip), so a coding agent (or a loop) runs the gate itself, not just you. To paste it yourself:
|
|
110
|
+
|
|
111
|
+
After a change that should move a measurable metric (tests, lint, coverage, an eval), run
|
|
112
|
+
`npx promptwheel run --working`. Exit 2 (verdict GAMED) means the win came from editing a test,
|
|
113
|
+
config, or grader, not the source, so revert and earn it in the source. Exit 1 means a guarded
|
|
114
|
+
metric regressed, revert that too. Never weaken, skip, or delete a test to go green.
|
|
115
|
+
|
|
105
116
|
**The consequence ledger.** git records *what changed*; PromptWheel records *what the change did*, same trust model (local, deterministic, append-only, no server, no LLM in the verdict). `playbook` and `suggest` are pure re-derivations over that ledger: every rendered line was measured by the gate, decays unless re-earned, and stays hidden below an evidence threshold. No compounding claim is made for them until the A/B acceptance test (`bench/compounding-ab.mjs`) passes on real usage data.
|
|
106
117
|
|
|
107
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).
|
|
@@ -250,5 +261,8 @@ The engine is one importable file; pure helpers are exported for unit tests, the
|
|
|
250
261
|
- [x] `--detect-gaming`, reward-hack detection: re-prove the win from source edits alone + `antihack` preset
|
|
251
262
|
- [x] npm publish, `promptwheel@0.4.2` (the lead magnet)
|
|
252
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)
|
|
253
267
|
|
|
254
|
-
> Status: **published** (npm `promptwheel`, v0.
|
|
268
|
+
> Status: **published** (npm `promptwheel`, v0.7.1), all core phases built. Lineage: CommandLayer → BlockSpool → PromptWheel (orchestrator, archived) → **PromptWheel (outcome gate)**.
|
package/bin/promptwheel.mjs
CHANGED
|
@@ -970,10 +970,34 @@ function init(argv) {
|
|
|
970
970
|
if (envCfg.linkDirs) note += ` · linking ${envCfg.linkDirs.join(', ')}`;
|
|
971
971
|
writeFileSync(out, JSON.stringify({ repeat: 1, ...envCfg, ...(evaluatorPaths ? { evaluatorPaths } : {}), metrics }, null, 2) + '\n');
|
|
972
972
|
console.log(`✓ wrote promptwheel.config.json, ${note}`);
|
|
973
|
+
if (!argv.includes('--no-agent-block')) {
|
|
974
|
+
const wrote = writeAgentBlock(repo);
|
|
975
|
+
if (wrote.length) console.log(`✓ wrote the agent guide (when to run the gate) to ${wrote.join(', ')}`);
|
|
976
|
+
}
|
|
973
977
|
console.log('\n next: promptwheel run --working # gate your uncommitted changes');
|
|
974
978
|
console.log(' promptwheel init --list # other presets (llm-eval, bundle-size, …)');
|
|
975
979
|
}
|
|
976
980
|
|
|
981
|
+
// Write (or refresh) a short agent-orienting block so the coding AGENT runs the gate itself, not just
|
|
982
|
+
// the human. Goes into AGENTS.md (the cross-tool convention; created if absent) and into CLAUDE.md /
|
|
983
|
+
// .cursorrules when those already exist. Idempotent + safe: only the sentinel-delimited block is
|
|
984
|
+
// touched, everything else in the file is left exactly as-is. `--no-agent-block` skips it.
|
|
985
|
+
function writeAgentBlock(repo) {
|
|
986
|
+
const START = '<!-- promptwheel:gate -->', END = '<!-- /promptwheel:gate -->';
|
|
987
|
+
const block = `${START}\n## PromptWheel gate\n\nAfter a change that should move a measurable metric (tests, lint, coverage, an eval), run \`npx promptwheel run --working\`. Exit 2 (verdict GAMED) means the win came from editing a test, config, or grader, not the source, so revert and earn it in the source. Exit 1 means a guarded metric regressed, revert that too. Never weaken, skip, or delete a test to go green.\n${END}\n`;
|
|
988
|
+
const targets = ['AGENTS.md', ...['CLAUDE.md', '.cursorrules'].filter((f) => existsSync(join(repo, f)))];
|
|
989
|
+
const done = [];
|
|
990
|
+
for (const f of targets) {
|
|
991
|
+
const p = join(repo, f);
|
|
992
|
+
let text = existsSync(p) ? readFileSync(p, 'utf8') : '';
|
|
993
|
+
const s = text.indexOf(START);
|
|
994
|
+
if (s > -1) { const e = text.indexOf(END, s); text = (e > -1 ? text.slice(0, s) + block + text.slice(e + END.length).replace(/^\n+/, '\n') : text.slice(0, s) + block); }
|
|
995
|
+
else { text = (text.trim() ? text.replace(/\n*$/, '\n\n') : '') + block; }
|
|
996
|
+
try { writeFileSync(p, text); done.push(f); } catch { /* best-effort */ }
|
|
997
|
+
}
|
|
998
|
+
return done;
|
|
999
|
+
}
|
|
1000
|
+
|
|
977
1001
|
// observability: list the EFFECTIVE guardrails (incl. inherited) + each one's flag record
|
|
978
1002
|
function guards(argv) {
|
|
979
1003
|
const args = parseArgs(argv);
|
|
@@ -1288,7 +1312,7 @@ function main() {
|
|
|
1288
1312
|
console.log([
|
|
1289
1313
|
'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.',
|
|
1290
1314
|
'',
|
|
1291
|
-
' promptwheel init [--preset <name> | --list] write a starter config (OPTIONAL: run works zero-config, auto-detecting your test command)',
|
|
1315
|
+
' promptwheel init [--preset <name> | --list] [--no-agent-block] write a starter config + an AGENTS.md guide so your agent runs the gate (OPTIONAL: run works zero-config, auto-detecting your test command)',
|
|
1292
1316
|
' promptwheel run [--base R] [--head R] [--repeat N] [--json|--markdown]',
|
|
1293
1317
|
' catches reward-hacking BY DEFAULT, re-proves each win from SOURCE edits alone (verdict GAMED,',
|
|
1294
1318
|
' exit 2, when a metric only moved by editing tests/config/grader). Use --no-detect-gaming for the bare gate.',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "promptwheel",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "A deterministic, zero-LLM check for your coding loop. It proves every change earned its win by re-running the result from the agent's source edits alone, so a green that came from editing the test reads GAMED. Runs in CI or a Claude Code hook, or import { check } in process.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -50,4 +50,4 @@
|
|
|
50
50
|
"detect-gaming",
|
|
51
51
|
"reward-hack-detection"
|
|
52
52
|
]
|
|
53
|
-
}
|
|
53
|
+
}
|
package/packs/security/scan.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// The full AST+dataflow engine (the other 75 recommended patterns) is the follow-up.
|
|
6
6
|
//
|
|
7
7
|
// Usage: node scan.mjs [path] → prints the finding count (for `promptwheel` metrics)
|
|
8
|
-
// node scan.mjs [path] --list → prints each finding (file:line
|
|
8
|
+
// node scan.mjs [path] --list → prints each finding (file:line · id · message)
|
|
9
9
|
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
10
10
|
import { join, extname } from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
@@ -50,7 +50,7 @@ for (const file of walk(ROOT)) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
if (LIST) {
|
|
53
|
-
for (const f of findings) console.error(` [${f.sev}] ${f.file}:${f.line}
|
|
53
|
+
for (const f of findings) console.error(` [${f.sev}] ${f.file}:${f.line} · ${f.id} · ${f.msg}`);
|
|
54
54
|
console.error(`\n${findings.length} security-invariant violation(s)`);
|
|
55
55
|
}
|
|
56
56
|
// the metric value: lower is better, 0 is clean
|