promptwheel 0.2.3 → 0.4.0
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 +9 -2
- package/bin/promptwheel.mjs +181 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,8 +72,15 @@ npx promptwheel improve --attempt "claude -p 'reduce lint errors'"
|
|
|
72
72
|
|
|
73
73
|
# what's actually responding in this repo? (aggregates .promptwheel/outcomes.jsonl)
|
|
74
74
|
npx promptwheel insights
|
|
75
|
+
|
|
76
|
+
# EXPERIMENTAL (Phase 5): the earned playbook + where the next attempt should go
|
|
77
|
+
npx promptwheel playbook # decayed, evidence-gated claims distilled from the record
|
|
78
|
+
npx promptwheel suggest # UCB over the lever scores — proven levers vs under-explored arms
|
|
79
|
+
npx promptwheel backfill -n 30 # cold start: seed the record from git history (cohort-tagged, commit types → labels)
|
|
75
80
|
```
|
|
76
81
|
|
|
82
|
+
**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.
|
|
83
|
+
|
|
77
84
|
**Footprint:** it never touches your working tree — every measurement runs in a throwaway git worktree **in your system temp dir** (one at a time; `node_modules` is 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 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).
|
|
78
85
|
|
|
79
86
|
## Loop patterns
|
|
@@ -187,7 +194,7 @@ The accumulated record of **which change-types move which metrics** is the asset
|
|
|
187
194
|
- [docs/VISION.md](docs/VISION.md) — why we pivoted from orchestrator to outcome gate, the thesis, the moat, the open-core model.
|
|
188
195
|
- [docs/ROADMAP.md](docs/ROADMAP.md) — the phased plan and the ship-now/stay-thin guardrails.
|
|
189
196
|
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) — how the engine works: schemas, extract modes, the trust/noise model.
|
|
190
|
-
- [docs/LEARNING.md](docs/LEARNING.md) — the (research-gated) Phase-5 design:
|
|
197
|
+
- [docs/LEARNING.md](docs/LEARNING.md) — the (research-gated) Phase-5 design: outcome-curated playbook (Agentic Context Engineering, Stanford 2510.04618) + UCB work-discovery.
|
|
191
198
|
- [CLAUDE.md](CLAUDE.md) — the constitution for anyone (human or agent) working in this repo.
|
|
192
199
|
|
|
193
200
|
## Develop
|
|
@@ -211,6 +218,6 @@ The engine is one importable file; pure helpers are exported for unit tests, the
|
|
|
211
218
|
- [x] `insights` — reward-stream aggregation (Phase-5 seed)
|
|
212
219
|
- [x] `--detect-gaming` — reward-hack detection: re-prove the win from source edits alone + `antihack` preset
|
|
213
220
|
- [x] npm publish — `promptwheel@0.1.0` (the lead magnet, shipped 2026-06)
|
|
214
|
-
- [
|
|
221
|
+
- [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))
|
|
215
222
|
|
|
216
223
|
> Status: **published** (npm `promptwheel`, v0.2.0) — all core phases built. Lineage: CommandLayer → BlockSpool → PromptWheel (orchestrator, archived) → **PromptWheel (outcome gate)**.
|
package/bin/promptwheel.mjs
CHANGED
|
@@ -188,6 +188,18 @@ function changedSourcePaths(repo, base, head) {
|
|
|
188
188
|
return { source: all.filter((p) => !isNonSource(p)), nonSource: all.filter(isNonSource) };
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
// coarse change-location fingerprint for the outcome record: top source dirs of the diff
|
|
192
|
+
function subsystemsOf(repo, base, head) {
|
|
193
|
+
try {
|
|
194
|
+
const counts = {};
|
|
195
|
+
for (const p of changedSourcePaths(repo, base, head).source) {
|
|
196
|
+
const d = p.includes('/') ? p.slice(0, p.indexOf('/')) : '(root)';
|
|
197
|
+
counts[d] = (counts[d] || 0) + 1;
|
|
198
|
+
}
|
|
199
|
+
return Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 3).map(([d]) => d);
|
|
200
|
+
} catch { return []; }
|
|
201
|
+
}
|
|
202
|
+
|
|
191
203
|
// measure `metric` in a worktree at `base` with ONLY the source slice of base→head applied.
|
|
192
204
|
function measureSourceOnly(repo, base, head, metric, linkNM, repeat) {
|
|
193
205
|
const { source } = changedSourcePaths(repo, base, head);
|
|
@@ -349,7 +361,15 @@ function gate(repo, opts) {
|
|
|
349
361
|
const failed = metrics.some((m) => m.guard && !m.ok);
|
|
350
362
|
const gamed = metrics.some((m) => m.gamed === true);
|
|
351
363
|
const verdict = failed ? 'fail' : gamed ? 'gamed' : 'pass';
|
|
352
|
-
const report = {
|
|
364
|
+
const report = {
|
|
365
|
+
base: short(repo, base), head: short(repo, head), repeat, mode: opts.working ? 'working' : 'refs',
|
|
366
|
+
// learning-substrate fields (Phase 5): cohort segments reliability by environment,
|
|
367
|
+
// label attributes a change-type, subsystems fingerprint WHERE the change landed.
|
|
368
|
+
cohort: opts.cohort ?? (process.env.CI ? 'ci' : 'local'),
|
|
369
|
+
...(opts.label ? { label: opts.label } : {}),
|
|
370
|
+
subsystems: subsystemsOf(repo, base, head),
|
|
371
|
+
verdict, metrics,
|
|
372
|
+
};
|
|
353
373
|
if (!opts.noRecord && cfg.record !== false) recordOutcome(repo, report);
|
|
354
374
|
return report;
|
|
355
375
|
}
|
|
@@ -357,7 +377,7 @@ function gate(repo, opts) {
|
|
|
357
377
|
function run(argv) {
|
|
358
378
|
const args = parseArgs(argv);
|
|
359
379
|
const repo = git(['rev-parse', '--show-toplevel'], process.cwd());
|
|
360
|
-
const report = gate(repo, { base: args.base, head: args.head, working: args.working, repeat: args.repeat, noRecord: args.noRecord, detectGaming: args.detectGaming });
|
|
380
|
+
const report = gate(repo, { base: args.base, head: args.head, working: args.working, repeat: args.repeat, noRecord: args.noRecord, detectGaming: args.detectGaming, label: args.label });
|
|
361
381
|
if (args.json) console.log(JSON.stringify(report, null, 2));
|
|
362
382
|
else if (args.markdown) console.log(renderMarkdown(report));
|
|
363
383
|
else printHuman(report);
|
|
@@ -385,7 +405,7 @@ function improve(argv) {
|
|
|
385
405
|
try { execSync(args.attempt, { cwd: repo, stdio: 'inherit' }); }
|
|
386
406
|
catch (e) { console.error(` (attempt exited ${e.status ?? 1} — gating whatever it changed)`); }
|
|
387
407
|
|
|
388
|
-
const report = gate(repo, { working: true, repeat: args.repeat, noRecord: args.noRecord, detectGaming: args.detectGaming });
|
|
408
|
+
const report = gate(repo, { working: true, repeat: args.repeat, noRecord: args.noRecord, detectGaming: args.detectGaming, label: args.label ?? args.attempt });
|
|
389
409
|
const noChange = report.metrics.every((m) => m.delta === 0 || m.delta == null);
|
|
390
410
|
const improvedNames = report.metrics.filter((m) => m.status === 'improved').map((m) => m.name);
|
|
391
411
|
|
|
@@ -419,7 +439,7 @@ function revert(repo) {
|
|
|
419
439
|
}
|
|
420
440
|
|
|
421
441
|
// Phase-5 seed: turn the accumulated reward stream into signal. Thin on purpose —
|
|
422
|
-
// this is the substrate a future
|
|
442
|
+
// this is the substrate a future outcome-curated playbook / UCB work-discovery loop trains on,
|
|
423
443
|
// NOT that loop itself. Just honest aggregation, no model.
|
|
424
444
|
function insights(argv) {
|
|
425
445
|
const args = parseArgs(argv);
|
|
@@ -447,6 +467,149 @@ function insights(argv) {
|
|
|
447
467
|
console.log(' highest-lever metrics are where an agent loop should spend its attempts.\n');
|
|
448
468
|
}
|
|
449
469
|
|
|
470
|
+
// ---------------------------------------------------------------------------
|
|
471
|
+
// Phase 5 — outcome-curated playbook + UCB work-discovery.
|
|
472
|
+
// Unfrozen 2026-07-02 by explicit founder decision overriding D7 (see docs/LEARNING.md).
|
|
473
|
+
// Design constraints that survive the unfreeze:
|
|
474
|
+
// - PURE VIEW: the append-only ledger (.promptwheel/outcomes.jsonl) is the only store;
|
|
475
|
+
// the playbook is re-derived on every read (decay at read time — no curator state to rot).
|
|
476
|
+
// - EVIDENCE-GATED: a key renders a claim only past MIN_MOVED weighted observations;
|
|
477
|
+
// everything below that is counted, not asserted.
|
|
478
|
+
// - CLAIM-GATED: no public compounding claim until bench/compounding-ab.mjs passes on
|
|
479
|
+
// real usage data. The gate on the CLAIM outlived the gate on the CODE.
|
|
480
|
+
// ---------------------------------------------------------------------------
|
|
481
|
+
const HALF_LIFE = 20; // runs — an entry's weight halves every HALF_LIFE runs unless re-earned
|
|
482
|
+
const MIN_MOVED = 3; // weighted moved-observations before a key earns a rendered claim
|
|
483
|
+
const EXPLORE_C = 1.0; // UCB exploration constant
|
|
484
|
+
|
|
485
|
+
function readLedger(repo) {
|
|
486
|
+
const f = join(repo, '.promptwheel', 'outcomes.jsonl');
|
|
487
|
+
if (!existsSync(f)) return [];
|
|
488
|
+
return readFileSync(f, 'utf8').split('\n').filter(Boolean)
|
|
489
|
+
.map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// fold the ledger into decayed per-key stats. keys: `metric` · `metric @ subsystem` · `metric # label`
|
|
493
|
+
function foldOutcomes(runs) {
|
|
494
|
+
const keys = new Map();
|
|
495
|
+
const N = runs.length;
|
|
496
|
+
runs.forEach((r, idx) => {
|
|
497
|
+
const w = Math.pow(0.5, (N - 1 - idx) / HALF_LIFE); // idx N-1 = newest → weight 1
|
|
498
|
+
for (const m of (r.metrics || [])) {
|
|
499
|
+
const tags = [m.name];
|
|
500
|
+
for (const s of (r.subsystems || [])) tags.push(`${m.name} @ ${s}`);
|
|
501
|
+
if (r.label) tags.push(`${m.name} # ${r.label}`);
|
|
502
|
+
for (const key of tags) {
|
|
503
|
+
const k = keys.get(key) ?? { key, metric: m.name, w: 0, n: 0, imp: 0, reg: 0, moved: 0, effects: [], cohorts: {}, last: null };
|
|
504
|
+
k.n += 1; k.w += w;
|
|
505
|
+
const coh = (k.cohorts[r.cohort || 'unknown'] ??= { imp: 0, reg: 0 });
|
|
506
|
+
if (m.status === 'improved') {
|
|
507
|
+
k.imp += w; k.moved += w; coh.imp += 1;
|
|
508
|
+
if (typeof m.delta === 'number' && m.delta !== 0) k.effects.push(Math.abs(m.delta));
|
|
509
|
+
} else if (m.status === 'regressed') { k.reg += w; k.moved += w; coh.reg += 1; }
|
|
510
|
+
k.last = r.ts || k.last;
|
|
511
|
+
keys.set(key, k);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
return [...keys.values()];
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const betaMean = (imp, reg) => (1 + imp) / (2 + imp + reg); // Beta(1,1)-smoothed helpfulness
|
|
519
|
+
|
|
520
|
+
// composite lever = smoothed helpfulness × responsiveness; UCB adds an exploration bonus
|
|
521
|
+
function scoreKey(k, totalW) {
|
|
522
|
+
const p = betaMean(k.imp, k.reg);
|
|
523
|
+
const move = k.w ? k.moved / k.w : 0;
|
|
524
|
+
const lever = p * move;
|
|
525
|
+
const ucb = lever + EXPLORE_C * Math.sqrt(Math.log(Math.max(Math.E, totalW)) / Math.max(1e-6, k.w));
|
|
526
|
+
return { p: +p.toFixed(3), move: +move.toFixed(3), lever: +lever.toFixed(3), ucb: +ucb.toFixed(3), effect: +(median(k.effects) ?? 0).toFixed(6) };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// cohorts that disagree in sign are flagged, never averaged into a lie
|
|
530
|
+
function cohortNote(k) {
|
|
531
|
+
const cs = Object.entries(k.cohorts).filter(([, c]) => c.imp + c.reg > 0);
|
|
532
|
+
if (cs.length < 2) return '';
|
|
533
|
+
const signs = new Set(cs.map(([, c]) => Math.sign(c.imp - c.reg)));
|
|
534
|
+
return signs.size > 1 ? ' · ⚠ cohort-dependent (ci vs local disagree)' : '';
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function loadEntries(repo) {
|
|
538
|
+
const runs = readLedger(repo);
|
|
539
|
+
if (!runs.length) { console.error('no outcome record yet — run the gate a few times first (.promptwheel/outcomes.jsonl)'); process.exit(2); }
|
|
540
|
+
const keys = foldOutcomes(runs);
|
|
541
|
+
const totalW = keys.filter((k) => k.key === k.metric).reduce((s, k) => s + k.w, 0);
|
|
542
|
+
const entries = keys.map((k) => ({ ...k, ...scoreKey(k, totalW), sufficient: k.moved >= MIN_MOVED }))
|
|
543
|
+
.sort((a, b) => b.lever - a.lever || b.w - a.w);
|
|
544
|
+
return { runs, entries };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// the distilled, execution-earned context an agent (or CLAUDE.md) can consume
|
|
548
|
+
function playbook(argv) {
|
|
549
|
+
const args = parseArgs(argv);
|
|
550
|
+
const repo = git(['rev-parse', '--show-toplevel'], process.cwd());
|
|
551
|
+
const { runs, entries } = loadEntries(repo);
|
|
552
|
+
if (args.json) { console.log(JSON.stringify({ runs: runs.length, halfLife: HALF_LIFE, minMoved: MIN_MOVED, entries }, null, 2)); return; }
|
|
553
|
+
const suff = entries.filter((e) => e.sufficient);
|
|
554
|
+
const pct = (x) => `${Math.round(x * 100)}%`;
|
|
555
|
+
const out = [];
|
|
556
|
+
out.push(`## Earned playbook — ${runs.length} gated runs, decay half-life ${HALF_LIFE} runs`);
|
|
557
|
+
out.push('*(every line below was measured by the gate, not asserted; entries decay unless re-earned)*', '');
|
|
558
|
+
if (!suff.length) {
|
|
559
|
+
out.push(`_Nothing has earned a claim yet — a key needs ≥${MIN_MOVED} weighted moved-observations. Keep gating; ${entries.length} keys are accumulating._`);
|
|
560
|
+
} else {
|
|
561
|
+
for (const e of suff.slice(0, 20)) {
|
|
562
|
+
out.push(`- **${e.key}** — helpful ${pct(e.p)} when it moves (${e.imp.toFixed(1)}✓/${e.reg.toFixed(1)}✗ weighted), responds in ${pct(e.move)} of runs, median effect ${e.effect}${cohortNote(e)}`);
|
|
563
|
+
}
|
|
564
|
+
const hidden = entries.length - suff.length;
|
|
565
|
+
if (hidden > 0) out.push('', `_${hidden} more keys below the evidence threshold — counted, not asserted._`);
|
|
566
|
+
}
|
|
567
|
+
console.log(out.join('\n'));
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// UCB work-discovery: where should the loop spend its NEXT attempt? Proposes measured
|
|
571
|
+
// targets (metric/subsystem arms) — never code advice; the measurement stays the message.
|
|
572
|
+
function suggest(argv) {
|
|
573
|
+
const args = parseArgs(argv);
|
|
574
|
+
const repo = git(['rev-parse', '--show-toplevel'], process.cwd());
|
|
575
|
+
const { runs, entries } = loadEntries(repo);
|
|
576
|
+
const arms = entries.filter((e) => e.key === e.metric || e.key.includes(' @ ')).sort((a, b) => b.ucb - a.ucb);
|
|
577
|
+
if (args.json) { console.log(JSON.stringify({ runs: runs.length, thin: runs.length < 10, arms: arms.slice(0, 10) }, null, 2)); return; }
|
|
578
|
+
console.log(`\nPromptWheel suggest — UCB over ${runs.length} gated runs${runs.length < 10 ? ' (thin record: exploration dominates — treat as a coin with opinions)' : ''}\n`);
|
|
579
|
+
for (const a of arms.slice(0, 5)) {
|
|
580
|
+
console.log(` ${a.ucb.toFixed(3)} ${a.key.padEnd(28)} lever ${a.lever.toFixed(3)} (helpful ${a.p}, responds ${a.move}) · evidence ${a.moved.toFixed(1)} moved / ${a.n} runs${a.sufficient ? '' : ' · below claim threshold'}`);
|
|
581
|
+
}
|
|
582
|
+
console.log('\n score = lever + exploration bonus — high scores are either proven levers or under-explored arms.\n');
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// backfill — seed the ledger from git history: replay past commits through the CURRENT
|
|
586
|
+
// metrics (LEARNING.md harvest path 1). Deterministic, no LLM. Rows are cohort-tagged
|
|
587
|
+
// 'backfill' — historical human commits are NOT live agent-loop evidence; the cohort
|
|
588
|
+
// machinery segments them and flags disagreement rather than averaging it away. The
|
|
589
|
+
// conventional-commit type (fix/feat/refactor/…) becomes the change-type label for free.
|
|
590
|
+
function backfill(argv) {
|
|
591
|
+
const args = parseArgs(argv);
|
|
592
|
+
const repo = git(['rev-parse', '--show-toplevel'], process.cwd());
|
|
593
|
+
const listArgs = ['rev-list', '--no-merges'];
|
|
594
|
+
if (args.since) listArgs.push(`${args.since}..HEAD`); else listArgs.push('-n', String(args.n ?? 30), 'HEAD');
|
|
595
|
+
const commits = git(listArgs, repo).split('\n').filter(Boolean).reverse(); // oldest first → ledger stays chronological, decay stays honest
|
|
596
|
+
if (!commits.length) { console.error('no commits to backfill'); process.exit(2); }
|
|
597
|
+
const seen = new Set(readLedger(repo).filter((r) => r.cohort === 'backfill').map((r) => r.head));
|
|
598
|
+
let done = 0, skipped = 0;
|
|
599
|
+
console.log(`\nbackfilling ${commits.length} commits through the current metrics (gaming detection ${args.dgExplicit && args.detectGaming ? 'ON' : 'off — pass --detect-gaming to audit history too'})\n`);
|
|
600
|
+
for (const c of commits) {
|
|
601
|
+
try { git(['rev-parse', `${c}~1`], repo); } catch { skipped++; continue; } // root commit
|
|
602
|
+
if (seen.has(short(repo, c))) { skipped++; continue; } // already recorded
|
|
603
|
+
const subj = git(['log', '-1', '--format=%s', c], repo);
|
|
604
|
+
const type = /^(feat|fix|docs|test|chore|refactor|perf|ci|build|style)\b/i.exec(subj)?.[1]?.toLowerCase();
|
|
605
|
+
const report = gate(repo, { base: `${c}~1`, head: c, repeat: args.repeat, detectGaming: args.dgExplicit ? args.detectGaming : false, label: type, cohort: 'backfill' });
|
|
606
|
+
console.log(` ${report.head} ${report.verdict.toUpperCase().padEnd(6)} ${type ? `#${type}`.padEnd(10) : ''.padEnd(10)} ${subj.slice(0, 56)}`);
|
|
607
|
+
done++;
|
|
608
|
+
}
|
|
609
|
+
console.log(`\n backfilled ${done}${skipped ? ` (skipped ${skipped}: root or already recorded)` : ''} — old commits that no longer build record as unmeasurable, never faked.`);
|
|
610
|
+
console.log(' next: promptwheel playbook · promptwheel suggest\n');
|
|
611
|
+
}
|
|
612
|
+
|
|
450
613
|
// ---------------------------------------------------------------------------
|
|
451
614
|
// init — write a starter config so a newcomer isn't staring at a blank page
|
|
452
615
|
// ---------------------------------------------------------------------------
|
|
@@ -610,24 +773,30 @@ function parseArgs(argv) {
|
|
|
610
773
|
else if (argv[i] === '--repeat') a.repeat = parseInt(argv[++i], 10);
|
|
611
774
|
else if (argv[i] === '--working') a.working = true;
|
|
612
775
|
else if (argv[i] === '--no-record') a.noRecord = true;
|
|
613
|
-
else if (argv[i] === '--detect-gaming' || argv[i] === '--antihack') a.detectGaming = true;
|
|
614
|
-
else if (argv[i] === '--no-detect-gaming' || argv[i] === '--no-antihack') a.detectGaming = false;
|
|
776
|
+
else if (argv[i] === '--detect-gaming' || argv[i] === '--antihack') { a.detectGaming = true; a.dgExplicit = true; }
|
|
777
|
+
else if (argv[i] === '--no-detect-gaming' || argv[i] === '--no-antihack') { a.detectGaming = false; a.dgExplicit = true; }
|
|
778
|
+
else if (argv[i] === '--since') a.since = argv[++i];
|
|
779
|
+
else if (argv[i] === '-n' || argv[i] === '--limit') a.n = parseInt(argv[++i], 10);
|
|
615
780
|
else if (argv[i] === '--attempt') a.attempt = argv[++i];
|
|
781
|
+
else if (argv[i] === '--label') a.label = argv[++i];
|
|
616
782
|
else if (argv[i] === '--json') a.json = true;
|
|
617
|
-
else if (argv[i] === '--markdown') a.markdown = true;
|
|
783
|
+
else if (argv[i] === '--markdown' || argv[i] === '--md') a.markdown = true;
|
|
618
784
|
}
|
|
619
785
|
return a;
|
|
620
786
|
}
|
|
621
787
|
|
|
622
788
|
// pure, side-effect-free helpers are exported for unit testing; the CLI below only
|
|
623
789
|
// runs when this file is executed directly (not when imported by the test suite).
|
|
624
|
-
export { extract, evaluate, median, spread, renderMarkdown, isNonSource, gain, judgeGaming };
|
|
790
|
+
export { extract, evaluate, median, spread, renderMarkdown, isNonSource, gain, judgeGaming, foldOutcomes, betaMean, scoreKey };
|
|
625
791
|
|
|
626
792
|
function main() {
|
|
627
793
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
628
794
|
if (cmd === 'run') run(rest);
|
|
629
795
|
else if (cmd === 'improve') improve(rest);
|
|
630
796
|
else if (cmd === 'insights') insights(rest);
|
|
797
|
+
else if (cmd === 'playbook') playbook(rest);
|
|
798
|
+
else if (cmd === 'suggest') suggest(rest);
|
|
799
|
+
else if (cmd === 'backfill') backfill(rest);
|
|
631
800
|
else if (cmd === 'init') init(rest);
|
|
632
801
|
else if (cmd === 'guards') guards(rest);
|
|
633
802
|
else {
|
|
@@ -641,7 +810,10 @@ function main() {
|
|
|
641
810
|
' promptwheel run --working gate uncommitted changes (incl. newly added files)',
|
|
642
811
|
' promptwheel improve --attempt "<cmd>" run an agent/script; keep only if a metric improved',
|
|
643
812
|
' exit 0=kept · 1=regression · 3=plateau · add --json',
|
|
644
|
-
' promptwheel insights which metrics actually respond (
|
|
813
|
+
' promptwheel insights which metrics actually respond (raw counts)',
|
|
814
|
+
' promptwheel playbook [--json] the earned playbook: decayed, evidence-gated claims distilled from the record',
|
|
815
|
+
' promptwheel suggest [--json] UCB work-discovery: where the next attempt should go (experimental)',
|
|
816
|
+
' promptwheel backfill [-n N | --since <ref>] seed the ledger from git history (cohort-tagged; commit types become labels)',
|
|
645
817
|
' promptwheel guards show the effective guardrails (incl. inherited) + flag record',
|
|
646
818
|
'',
|
|
647
819
|
'Loop it: while promptwheel improve --attempt "$AGENT"; do :; done # stops on plateau/regression',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "promptwheel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
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": {
|