promptwheel 0.5.1 → 0.6.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.
Files changed (2) hide show
  1. package/bin/promptwheel.mjs +30 -10
  2. package/package.json +1 -1
@@ -75,6 +75,18 @@ function loadConfig(repo) {
75
75
  return cfg;
76
76
  }
77
77
 
78
+ // does the repo have a committed gate config?
79
+ const hasConfigFile = (repo) => ['promptwheel.config.json', 'outcome-gate.config.json'].some((f) => existsSync(join(repo, f)));
80
+
81
+ // the zero-config default: the gate `init` would write, built in memory from stack detection.
82
+ // It gates the detected test command (gaming detection on) with the stack's env/linkDirs so the
83
+ // measurement actually runs in a throwaway worktree. Lets `run` and `check()` work with no config
84
+ // file; `init` is for customizing. A repo with no test command gets a sentinel that reads inert ->
85
+ // INCONCLUSIVE (never a fake green), with a message pointing at the fix.
86
+ function autoConfig(repo) {
87
+ return { ...detectEnv(repo), metrics: [{ name: 'tests_pass', cmd: detectTestCmd(repo), extract: 'exit', direction: 'pass', guard: true }] };
88
+ }
89
+
78
90
  function resolveBase(repo, base) {
79
91
  if (base) return base;
80
92
  for (const b of ['origin/main', 'origin/master', 'main', 'master']) {
@@ -546,7 +558,12 @@ function gate(repo, opts) {
546
558
  function run(argv) {
547
559
  const args = parseArgs(argv);
548
560
  const repo = git(['rev-parse', '--show-toplevel'], process.cwd());
549
- 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 });
561
+ let config;
562
+ if (!hasConfigFile(repo) && !args.noAuto) {
563
+ config = autoConfig(repo);
564
+ console.error(`no promptwheel.config.json, auto-gating the detected test command: ${config.metrics[0].cmd} (promptwheel init to customize; --no-auto to require a config)`);
565
+ }
566
+ 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, config });
550
567
  if (args.json) console.log(JSON.stringify(report, null, 2));
551
568
  else if (args.markdown) console.log(renderMarkdown(report));
552
569
  else printHuman(report);
@@ -951,7 +968,7 @@ function printHuman(r) {
951
968
  const tag = m.guard ? (m.ok ? 'guard✓' : 'GUARD✗') : 'info';
952
969
  const d = m.delta == null ? 'n/a' : (m.delta > 0 ? `+${m.delta}` : `${m.delta}`);
953
970
  console.log(` ${arrowFor(m)} ${m.name.padEnd(18)} ${String(m.before).padStart(8)} → ${String(m.after).padStart(8)} (${d}, ${m.status}) [${tag}, ${m.confidence}]`);
954
- if (m.inert) console.log(' ⚠ never passed at either ref, this guard is protecting nothing (check its command)');
971
+ if (m.inert) console.log(` ⚠ "${m.name}" never ran (same value at both refs): its command needs a build/deps first, or is not your real test command. Fix: promptwheel init, or set setup/linkDirs in promptwheel.config.json`);
955
972
  if (m.gamed === true) console.log(` 🚩 GAMED, ${m.gamingReason}`);
956
973
  }
957
974
  if (r.gate && r.gate.weakened) for (const w of r.gate.weakened) console.log(` 🚩 GATE WEAKENED, the guard "${w.name}" was ${w.change === 'guard-removed' ? 'removed' : 'disabled'} in this change and it would have ${w.status} at head (goalpost moved, not earned)`);
@@ -993,6 +1010,7 @@ function parseArgs(argv) {
993
1010
  else if (argv[i] === '--repeat') a.repeat = parseInt(argv[++i], 10);
994
1011
  else if (argv[i] === '--working') a.working = true;
995
1012
  else if (argv[i] === '--no-record') a.noRecord = true;
1013
+ else if (argv[i] === '--no-auto') a.noAuto = true;
996
1014
  else if (argv[i] === '--detect-gaming' || argv[i] === '--antihack') { a.detectGaming = true; a.dgExplicit = true; }
997
1015
  else if (argv[i] === '--no-detect-gaming' || argv[i] === '--no-antihack') { a.detectGaming = false; a.dgExplicit = true; }
998
1016
  else if (argv[i] === '--since') a.since = argv[++i];
@@ -1046,7 +1064,8 @@ export function verdictExitCode(verdict) {
1046
1064
  // head string head ref (default: HEAD)
1047
1065
  // working boolean gate uncommitted changes instead of base..head
1048
1066
  // config object a resolved config { metrics:[...], ... }; omit to load
1049
- // promptwheel.config.json from the repo
1067
+ // promptwheel.config.json, or auto-detect a default when none exists
1068
+ // auto boolean allow the zero-config auto-detect fallback (default true; false throws if no config)
1050
1069
  // repeat number measurement repeats for the noise band (default: config.repeat ?? 1)
1051
1070
  // detectGaming boolean run the Section 3 source-only re-proof (default: true)
1052
1071
  // cohort string cohort tag for the record (default: CI ? 'ci' : 'local')
@@ -1059,19 +1078,20 @@ export function check(options = {}) {
1059
1078
  : git(['rev-parse', '--show-toplevel'], process.cwd());
1060
1079
  } catch { throw new Error(`promptwheel check: not inside a git repository (${options.repo || process.cwd()})`); }
1061
1080
 
1062
- if (options.config !== undefined) {
1063
- const c = options.config;
1064
- if (!c || typeof c !== 'object' || !Array.isArray(c.metrics) || c.metrics.length === 0)
1081
+ let config = options.config;
1082
+ if (config !== undefined) {
1083
+ if (!config || typeof config !== 'object' || !Array.isArray(config.metrics) || config.metrics.length === 0)
1065
1084
  throw new Error('promptwheel check: options.config.metrics must be a non-empty array');
1066
- } else if (!['promptwheel.config.json', 'outcome-gate.config.json'].some((f) => existsSync(join(repo, f)))) {
1067
- throw new Error(`promptwheel check: no promptwheel.config.json in ${repo} (pass options.config to supply one in process)`);
1085
+ } else if (!hasConfigFile(repo)) {
1086
+ if (options.auto === false) throw new Error(`promptwheel check: no promptwheel.config.json in ${repo} (pass options.config, or allow auto-detect)`);
1087
+ config = autoConfig(repo); // zero-config: gate the detected test command in process
1068
1088
  }
1069
1089
 
1070
1090
  return gate(repo, {
1071
1091
  base: options.base,
1072
1092
  head: options.head,
1073
1093
  working: options.working,
1074
- config: options.config,
1094
+ config,
1075
1095
  repeat: options.repeat,
1076
1096
  detectGaming: options.detectGaming ?? true,
1077
1097
  cohort: options.cohort,
@@ -1212,7 +1232,7 @@ function main() {
1212
1232
  console.log([
1213
1233
  '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.',
1214
1234
  '',
1215
- ' promptwheel init [--preset <name> | --list] write a starter config for your stack',
1235
+ ' promptwheel init [--preset <name> | --list] write a starter config (OPTIONAL: run works zero-config, auto-detecting your test command)',
1216
1236
  ' promptwheel run [--base R] [--head R] [--repeat N] [--json|--markdown]',
1217
1237
  ' catches reward-hacking BY DEFAULT, re-proves each win from SOURCE edits alone (verdict GAMED,',
1218
1238
  ' 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.5.1",
3
+ "version": "0.6.0",
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": {