promptwheel 0.7.13 โ 0.7.15
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/bin/promptwheel.d.ts +3 -0
- package/bin/promptwheel.mjs +34 -0
- package/package.json +1 -1
- package/packs/supply-chain/README.md +2 -0
package/bin/promptwheel.d.ts
CHANGED
|
@@ -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;
|
package/bin/promptwheel.mjs
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.7.15",
|
|
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
|