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 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.4.2), all core phases built. Lineage: CommandLayer → BlockSpool → PromptWheel (orchestrator, archived) → **PromptWheel (outcome gate)**.
268
+ > Status: **published** (npm `promptwheel`, v0.7.1), all core phases built. Lineage: CommandLayer → BlockSpool → PromptWheel (orchestrator, archived) → **PromptWheel (outcome gate)**.
@@ -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.0",
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
+ }
@@ -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 — id — message)
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} — ${f.id} — ${f.msg}`);
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