promptwheel 0.7.13 โ†’ 0.7.14

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.
@@ -336,3 +336,6 @@ export declare function judgeGaming(
336
336
 
337
337
  /** Truncated SHA-256 of the verdict content (everything but `provenance`), the reproducible half of the receipt. */
338
338
  export declare function verdictFingerprint(report: Pick<Report, 'verdict'> & Partial<Pick<Report, 'metrics' | 'gate'>>): string;
339
+
340
+ /** The newest `sawbuck-allow: <reason>` comment from someone with write access, or ''. */
341
+ export declare function allowFromComments(comments: unknown[]): string;
@@ -774,6 +774,8 @@ function gate(repo, opts) {
774
774
  const metrics = cfg.metrics.map((m) => {
775
775
  const ev = evaluate(m, before[m.name], after[m.name], repeat);
776
776
  if (ev.status === 'unmeasurable' && ev.before == null && ev.after != null && adopted(m.name)) { ev.ok = true; ev.adopted = true; }
777
+ // a `report` guard tells you and does not fail: a contract can start by watching and tighten later
778
+ if (m.report && !ev.ok && ev.status === 'regressed') { ev.ok = true; ev.noted = true; }
777
779
  return { name: m.name, direction: m.direction || 'up', guard: !!m.guard, ...(m.gamingCheck === false ? { tripwire: true } : {}), ...ev };
778
780
  });
779
781
  // Section 3 (gaming detection), on by default: mutates `metrics` and yields the gate record
@@ -835,7 +837,9 @@ function gate(repo, opts) {
835
837
  }
836
838
  const mandated = mandateViolations.length > 0;
837
839
  const verdict = deriveVerdict(metrics, gateInfo, mandated);
840
+ const explained = explainMetrics(repo, metrics);
838
841
  const report = {
842
+ ...(explained ? { explained } : {}),
839
843
  base: short(repo, base), head: short(repo, head), repeat, mode: opts.working ? 'working' : 'refs',
840
844
  verdict, metrics,
841
845
  ...(mandateViolations.length ? { mandates: mandateViolations } : {}),
@@ -1226,6 +1230,34 @@ function printHuman(r) {
1226
1230
  }
1227
1231
 
1228
1232
  // PR-comment markdown (rendering lives in the tool so the GitHub Action stays thin)
1233
+ // What a person needs when a check speaks: what changed, in words, and how to accept it.
1234
+ const PLAIN = {
1235
+ lifecycle_scripts: 'a new install script', dependency_entries: 'a new dependency', lock_nonregistry_sources: 'a dependency from outside the registry',
1236
+ unpinned_execution: 'something that runs without a pinned version', install_policy_gaps: 'a weaker install policy', lock_drift: 'the shape of the repo changed',
1237
+ pins_changed: 'resolved dependency versions changed', tests_pass: 'tests stopped passing', test_count: 'fewer tests', skipped_tests: 'more skipped tests',
1238
+ suppressions: 'more silenced checks', assertions: 'fewer assertions',
1239
+ };
1240
+ function explainMetrics(repo, metrics) {
1241
+ const hit = metrics.filter((m) => m.noted || (m.guard && !m.ok));
1242
+ if (!hit.length) return null;
1243
+ let items = [];
1244
+ if (hit.some((m) => m.name === 'lock_drift')) {
1245
+ try { items = JSON.parse(execFileSync(process.execPath, ['.promptwheel/packs/supply-chain/lock.mjs', 'drift', '.', '--json'], { cwd: repo, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })).slice(0, 8); } catch { /* words without the list */ }
1246
+ }
1247
+ return hit.map((m) => ({ metric: m.name, noted: !!m.noted, what: PLAIN[m.name] || m.name, ...(m.name === 'lock_drift' && items.length ? { items } : {}), ...(m.allowable ? { allowable: true } : {}) }));
1248
+ }
1249
+ // A reason a person left on a pull request. The newest `sawbuck-allow: <reason>` comment from someone with write
1250
+ // access wins. A bot never counts. An agent signed in as that person can still post one: the record shows it.
1251
+ export function allowFromComments(comments) {
1252
+ const trusted = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']);
1253
+ for (const c of [...(comments || [])].reverse()) {
1254
+ if (!c || !c.user || c.user.type !== 'User' || !trusted.has(c.author_association)) continue;
1255
+ const m = String(c.body || '').match(/^\s*sawbuck-allow:[ \t]*(\S.*?)[ \t]*$/m);
1256
+ if (m) return `${m[1].slice(0, 280)} (approved by ${String(c.user.login).slice(0, 39)})`;
1257
+ }
1258
+ return '';
1259
+ }
1260
+
1229
1261
  function renderMarkdown(r) {
1230
1262
  const icon = r.verdict === 'pass' ? 'โœ…' : r.verdict === 'gamed' ? '๐Ÿšฉ' : r.verdict === 'inconclusive' ? '๐ŸŸก' : 'โŒ';
1231
1263
  const sIcon = { improved: '๐ŸŸข', regressed: '๐Ÿ”ด', unchanged: 'โšช', inconclusive: '๐ŸŸก', unmeasurable: 'โšซ' };
@@ -1240,6 +1272,7 @@ function renderMarkdown(r) {
1240
1272
  `\`${r.base} โ†’ ${r.head}\` ยท ${r.mode} mode${r.repeat > 1 ? ` ยท ร—${r.repeat}` : ''}`,
1241
1273
  '',
1242
1274
  ...(r.mandates ? [r.mandates.map((md) => `> โ›” **Mandate violated:** \`${md.name}\`${md.reason ? ': ' + md.reason : ''} (touched \`${md.files.join('`, `')}\`)${md.removed ? ' (deleted in this same change; it stays in force for the change that removes it)' : md.edited ? ' (edited in this same change; the version before the change stays in force for the change that edits it)' : ''}`).join('\n'), ''] : []),
1275
+ ...(r.explained ? [...r.explained.map((x) => `> ${x.noted ? 'โ„น๏ธ **Note:**' : 'โœ‹ **Needs a look:**'} ${x.what}${x.items ? ': `' + x.items.join('`, `') + '`' : ''}`), ...(r.explained.some((x) => !x.noted && x.allowable) ? ['>', '> To accept this, comment `sawbuck-allow: <your reason>` on this PR and re-run the check.'] : []), ''] : []),
1243
1276
  '| metric | before | after | ฮ” | status | confidence |',
1244
1277
  '|---|--:|--:|--:|---|---|',
1245
1278
  rows,
@@ -1514,6 +1547,7 @@ function dispatch() {
1514
1547
  if (cmd in COMMAND_FLAGS) checkFlags(cmd, rest);
1515
1548
  if (cmd === 'run') run(rest);
1516
1549
  else if (cmd === 'improve') improve(rest);
1550
+ else if (cmd === 'pr-allow') { let c = []; try { c = JSON.parse(readFileSync(0, 'utf8') || '[]'); } catch { /* no comments, no approval */ } process.stdout.write(allowFromComments(c)); }
1517
1551
  else if (cmd === 'insights') insights(rest);
1518
1552
  else if (cmd === 'init') init(rest);
1519
1553
  else if (cmd === 'guards') guards(rest);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "promptwheel",
3
- "version": "0.7.13",
3
+ "version": "0.7.14",
4
4
  "description": "A deterministic, zero-LLM check for your coding loop. A win must survive a re-run from the agent's source edits alone, so a test that went green by editing the test reads GAMED. Runs in CI or a Claude Code plugin.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,6 +30,8 @@ clean git worktrees, so a pack that is not in git is absent at both refs and the
30
30
  - **lock_drift**: structural items the tree has that `promptwheel.lock.json` does not. `promptwheel lock` reads the repo once and writes its shape: dependencies, install scripts, workflows, modules, import edges between modules, routes, public entry points, and the network hosts the source names. It also records each script's body, native binaries, and what a module can do (network, process spawning, eval), so a new capability shows even when the host is built at runtime. Ordinary edits never touch it. A new route is reported but not held by default; add `--routes` to hold it. On 1,403 commits across eight repositories this guard would have stopped 5.1%, about one in 20. JS and TS only for edges, routes, hosts and capabilities today.
31
31
  - **pins_changed**: a fingerprint of the resolved dependency tree, compared base to head, because a new version under an old name is how a worm arrives. It covers every package, version, source and integrity, not the file's bytes, so a release of your own project does not count. Any change needs a reason. Nothing is stored, so the lock does not change across updates and an approved update does not blind the next check. On the same commits this stopped 8.7%, dependency changes; use the CI path below for those.
32
32
 
33
+ **On a pull request.** Someone with write access comments `sawbuck-allow: <reason>` and re-runs the check. `sawbuck contract` marks everyday guards `report: true`, so they note a change without failing until you tighten them; this preset on its own holds them all.
34
+
33
35
  **Dependency updates in CI.** Give the reason from a step the agent cannot reach. Workflows are a frozen path, so an agent cannot edit this:
34
36
 
35
37
  ```yaml