sawbuck 0.1.20 → 0.1.21
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/GLOSSARY.md +7 -6
- package/README.md +11 -0
- package/init.mjs +26 -2
- package/package.json +1 -1
- package/reflect.mjs +1 -1
- package/run.mjs +1 -1
- package/sawbuck.mjs +11 -5
package/GLOSSARY.md
CHANGED
|
@@ -14,7 +14,7 @@ it is plain and scientific. The saddle is the brand and the architecture spine
|
|
|
14
14
|
(the name, `attach`, `fit`); it is NOT a vocabulary for every concept.
|
|
15
15
|
|
|
16
16
|
Why plain wins everywhere: an LLM has strong trained priors on words like
|
|
17
|
-
`regression`, `verified`, `
|
|
17
|
+
`regression`, `verified`, `gamed`. Metaphor jargon in the model's context forces
|
|
18
18
|
a gloss every session (token cost) and risks a misread (trajectory drift). Plain,
|
|
19
19
|
high-prior words serve the human reader and the agent equally, so we do not keep
|
|
20
20
|
two vocabularies.
|
|
@@ -28,7 +28,7 @@ two vocabularies.
|
|
|
28
28
|
| the verifier | **the check** (verb: *verify*) | runs the metrics on a change |
|
|
29
29
|
| a change that improved and holds | **verified** | |
|
|
30
30
|
| a change that made a guarded metric worse | **regression** | do-not-retry |
|
|
31
|
-
| a change that passed by
|
|
31
|
+
| a change that passed by faking the metric | **gamed** | the win came from editing the test, config, or grader, not the source (the engine verdict, and the word every Sawbuck surface uses) |
|
|
32
32
|
| the accumulated record of outcomes | **results** | `results.json` |
|
|
33
33
|
| what Sawbuck leaves you | **verified history** | the findings you can trust |
|
|
34
34
|
| a surfaced past outcome | **finding** | |
|
|
@@ -61,12 +61,13 @@ Findings are **leads, not conclusions**, verify against the diff before acting.
|
|
|
61
61
|
## Dispatch template
|
|
62
62
|
|
|
63
63
|
```
|
|
64
|
-
Sawbuck · {N} regressions charted · {N}
|
|
64
|
+
Sawbuck · {N} regressions charted · {N} gamed changes caught
|
|
65
65
|
🆕 just learned: {reason} ({file})
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
## Note on engine verdicts
|
|
69
69
|
|
|
70
|
-
The check engine
|
|
71
|
-
|
|
72
|
-
**
|
|
70
|
+
The check engine emits verdicts (`gamed`, `fail`, `pass`); the surface keeps **gamed**
|
|
71
|
+
as-is (it is vivid and used on every surface) and maps `fail` → **regression** and
|
|
72
|
+
`pass` → **verified**. ("Invalid" was an earlier softening of `gamed` that no surface
|
|
73
|
+
adopted; `gamed` is canonical.)
|
package/README.md
CHANGED
|
@@ -48,6 +48,17 @@ sawbuck: gamed recorded on test.js (the win came from editing the test, no
|
|
|
48
48
|
| `sawbuck recall` | related notes for your current change from extra memory providers you compose in (bring your own) |
|
|
49
49
|
| `sawbuck run --attempt '<agent cmd>'` | thin harness: load context, run one attempt through the keep-if-verified gate |
|
|
50
50
|
| `sawbuck trace` · `sawbuck doctor` · `detach --verify` | show the footprint; check the attachment resolves; prove a clean removal |
|
|
51
|
+
| `sawbuck where` · `sawbuck statusline` | list every repo it is attached to; a one-line status-line segment (🐴 watching / N caught) |
|
|
52
|
+
|
|
53
|
+
## Give your agent the contract
|
|
54
|
+
|
|
55
|
+
`sawbuck init` writes this block into your `AGENTS.md` for you (and `CLAUDE.md` / `.cursorrules` if present; `--no-agent-block` to skip), and `sawbuck attach --claude` enforces the same thing every turn via the hook. Here it is to paste yourself if you want:
|
|
56
|
+
|
|
57
|
+
This repo is under a Sawbuck contract: a change is done only when the win survives verification
|
|
58
|
+
from the source alone. Before finishing, run `sawbuck check`. If it reads `gamed`, the green came
|
|
59
|
+
from editing a test, config, or grader, not the source, so undo that and earn it in the source.
|
|
60
|
+
Run `sawbuck context` first to see what broke in these files before (reverts, weakened tests,
|
|
61
|
+
fragile spots). Never skip, weaken, or delete a test to go green.
|
|
51
62
|
|
|
52
63
|
Each repo needs a `promptwheel.config.json` saying what to measure (your tests, lint, coverage, or an eval).
|
|
53
64
|
|
package/init.mjs
CHANGED
|
@@ -32,7 +32,7 @@ const engineBin = (envVar, pkgSubpath, devPath) => {
|
|
|
32
32
|
for (const spec of [pkgSubpath, bare]) { try { return require.resolve(spec); } catch {} }
|
|
33
33
|
return devPath;
|
|
34
34
|
};
|
|
35
|
-
const PW = engineBin('PW_BIN', 'promptwheel/bin/promptwheel.mjs', join(HERE, '..', '
|
|
35
|
+
const PW = engineBin('PW_BIN', 'promptwheel/bin/promptwheel.mjs', join(HERE, '..', 'promptwheel', 'bin', 'promptwheel.mjs'));
|
|
36
36
|
const LB = engineBin('LB_BIN', '@promptwheel/logbook/bin/logbook.mjs', join(HERE, '..', 'logbook', 'bin', 'logbook.mjs'));
|
|
37
37
|
|
|
38
38
|
const argv = process.argv.slice(2);
|
|
@@ -44,6 +44,7 @@ const yes = has('--yes') || has('-y');
|
|
|
44
44
|
const forceWizard = has('--wizard');
|
|
45
45
|
const investigate = !has('--no-investigate');
|
|
46
46
|
const force = has('--force');
|
|
47
|
+
const noBlock = has('--no-agent-block');
|
|
47
48
|
const presetIdx = argv.indexOf('--preset');
|
|
48
49
|
const preset = presetIdx > -1 ? argv[presetIdx + 1] : null;
|
|
49
50
|
|
|
@@ -54,7 +55,9 @@ const prior = priorExisted ? readFileSync(cfgPath, 'utf8') : null;
|
|
|
54
55
|
const restore = () => { try { if (prior != null) writeFileSync(cfgPath, prior); else if (existsSync(cfgPath)) unlinkSync(cfgPath); } catch {} };
|
|
55
56
|
|
|
56
57
|
// 1) base contract from promptwheel init (stack-detected tests + antihack tripwires)
|
|
57
|
-
|
|
58
|
+
// --no-agent-block: suppress promptwheel's own AGENTS.md gate block here; sawbuck writes its own
|
|
59
|
+
// sawbuck-flavored contract block below, so the user does not get two overlapping agent guides.
|
|
60
|
+
const pwArgs = ['init', '--force', '--no-agent-block'];
|
|
58
61
|
if (preset) pwArgs.push('--preset', preset);
|
|
59
62
|
try { execFileSync(process.execPath, [PW, ...pwArgs], { cwd: repo, stdio: 'ignore' }); }
|
|
60
63
|
catch { restore(); out('sawbuck init: could not generate the base contract (promptwheel init failed).'); process.exit(1); }
|
|
@@ -115,8 +118,29 @@ function describeMetric(m) {
|
|
|
115
118
|
|
|
116
119
|
function writeCfg() { writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n'); }
|
|
117
120
|
|
|
121
|
+
// Write (or refresh) the agent-orienting contract block so a coding agent (or a loop not wired to the
|
|
122
|
+
// `attach --claude` hook) knows the rules. AGENTS.md (the cross-tool convention; created if absent) +
|
|
123
|
+
// CLAUDE.md / .cursorrules when present. Idempotent + safe: only the sentinel-delimited block is
|
|
124
|
+
// touched. `--no-agent-block` skips it.
|
|
125
|
+
function writeAgentBlock() {
|
|
126
|
+
const START = '<!-- sawbuck:contract -->', END = '<!-- /sawbuck:contract -->';
|
|
127
|
+
const block = `${START}\n## Sawbuck contract\n\nThis repo is under a Sawbuck contract: a change is done only when the win survives verification from the source alone. Before finishing, run \`sawbuck check\`. If it reads \`gamed\`, the green came from editing a test, config, or grader, not the source, so undo that and earn it in the source. Run \`sawbuck context\` first to see what broke in these files before (reverts, weakened tests, fragile spots). Never skip, weaken, or delete a test to go green.\n${END}\n`;
|
|
128
|
+
const targets = ['AGENTS.md', ...['CLAUDE.md', '.cursorrules'].filter((f) => existsSync(join(repo, f)))];
|
|
129
|
+
const done = [];
|
|
130
|
+
for (const f of targets) {
|
|
131
|
+
const p = join(repo, f);
|
|
132
|
+
let text = existsSync(p) ? readFileSync(p, 'utf8') : '';
|
|
133
|
+
const s = text.indexOf(START);
|
|
134
|
+
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); }
|
|
135
|
+
else { text = (text.trim() ? text.replace(/\n*$/, '\n\n') : '') + block; }
|
|
136
|
+
try { writeFileSync(p, text); done.push(f); } catch { /* best-effort */ }
|
|
137
|
+
}
|
|
138
|
+
return done;
|
|
139
|
+
}
|
|
140
|
+
|
|
118
141
|
function report() {
|
|
119
142
|
out('sawbuck: wrote promptwheel.config.json');
|
|
143
|
+
if (!noBlock) { const w = writeAgentBlock(); if (w.length) out(`sawbuck: wrote the agent guide (the contract rules) to ${w.join(', ')}`); }
|
|
120
144
|
out("\n the contract (what a win must satisfy):");
|
|
121
145
|
for (const m of cfg.metrics) out(' - ' + describeMetric(m));
|
|
122
146
|
if (evidence) { out('\n investigated this repo:'); for (const l of evidenceLines()) out(l); }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sawbuck",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"description": "Sign your agent to a contract so it does what you want: a deterministic, zero-LLM check that re-proves every win from the agent's source edits alone, so a green that came from editing the test is caught and reverted. A saddle for your coding harness.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/reflect.mjs
CHANGED
|
@@ -34,7 +34,7 @@ const engineBin = (envVar, pkgSubpath, devPath) => {
|
|
|
34
34
|
for (const spec of [pkgSubpath, bare]) { try { return require.resolve(spec); } catch {} }
|
|
35
35
|
return devPath;
|
|
36
36
|
};
|
|
37
|
-
const PW = engineBin('PW_BIN', 'promptwheel/bin/promptwheel.mjs', join(HERE, '..', '
|
|
37
|
+
const PW = engineBin('PW_BIN', 'promptwheel/bin/promptwheel.mjs', join(HERE, '..', 'promptwheel', 'bin', 'promptwheel.mjs'));
|
|
38
38
|
|
|
39
39
|
const flag = (name, def) => { const i = process.argv.indexOf(name); return i > -1 ? process.argv[i + 1] : def; };
|
|
40
40
|
const repo = flag('--repo', process.cwd());
|
package/run.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
16
16
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
17
17
|
const require = createRequire(import.meta.url);
|
|
18
18
|
const bin = (envVar, pkg, dev) => { if (process.env[envVar]) return process.env[envVar]; const bare = pkg.replace(/\/bin\/[^/]*$/, ''); for (const s of [pkg, bare]) { try { return require.resolve(s); } catch {} } return dev; };
|
|
19
|
-
const PW = bin('PW_BIN', 'promptwheel/bin/promptwheel.mjs', join(HERE, '..', '
|
|
19
|
+
const PW = bin('PW_BIN', 'promptwheel/bin/promptwheel.mjs', join(HERE, '..', 'promptwheel', 'bin', 'promptwheel.mjs'));
|
|
20
20
|
|
|
21
21
|
const arg = (n, d) => { const i = process.argv.indexOf(n); return i > -1 ? process.argv[i + 1] : d; };
|
|
22
22
|
const repo = arg('--repo', process.cwd());
|
package/sawbuck.mjs
CHANGED
|
@@ -27,7 +27,7 @@ const require = createRequire(import.meta.url);
|
|
|
27
27
|
// resolve the promptwheel engine bin: env override, installed package, then the dev sibling
|
|
28
28
|
const PW = process.env.PW_BIN
|
|
29
29
|
|| ['promptwheel/bin/promptwheel.mjs', 'promptwheel'].reduce((r, s) => { try { return r || require.resolve(s); } catch { return r; } }, '')
|
|
30
|
-
|| join(HERE, '..', '
|
|
30
|
+
|| join(HERE, '..', 'promptwheel', 'bin', 'promptwheel.mjs');
|
|
31
31
|
// run the engine and parse its JSON verdict (it prints JSON even when it exits nonzero)
|
|
32
32
|
function gateReport(repo, a) {
|
|
33
33
|
let o = '', code = 0;
|
|
@@ -63,10 +63,11 @@ try { version = JSON.parse(readFileSync(join(HERE, 'package.json'), 'utf8')).ver
|
|
|
63
63
|
const help = `sawbuck ${version}: a saddle for your repo
|
|
64
64
|
|
|
65
65
|
Usage:
|
|
66
|
-
sawbuck init [--yes] [--wizard] [--no-investigate]
|
|
66
|
+
sawbuck init [--yes] [--wizard] [--no-investigate] [--no-agent-block]
|
|
67
67
|
Generate the contract (promptwheel.config.json): a stack starter, tuned and
|
|
68
|
-
explained by what your git history shows actually breaks.
|
|
69
|
-
|
|
68
|
+
explained by what your git history shows actually breaks. Also writes a short
|
|
69
|
+
contract guide into AGENTS.md so your agent knows the rules (--no-agent-block
|
|
70
|
+
to skip). Wizard in a terminal, non-interactive in a pipe/CI/agent (or --yes).
|
|
70
71
|
sawbuck attach [repo] Wire Sawbuck into a repo (default: a git post-commit hook)
|
|
71
72
|
--claude instead wire project Claude Code hooks (writes .claude/settings.local.json)
|
|
72
73
|
--shared with --claude, write the tracked .claude/settings.json (team-shared, visible in git)
|
|
@@ -125,8 +126,13 @@ switch (cmd) {
|
|
|
125
126
|
// zero-config (the engine auto-detects your test command), and say plainly whether the win was
|
|
126
127
|
// earned. No attach, no hook, no record. The try-it-first on-ramp.
|
|
127
128
|
const repo = repoArg();
|
|
128
|
-
|
|
129
|
+
// stderr ignored so a benign failing probe (e.g. rev-parse HEAD~1 on a one-commit repo) does not
|
|
130
|
+
// leak a scary "fatal:" line before sawbuck's own message.
|
|
131
|
+
const g = (...a) => { try { return execFileSync('git', ['-C', repo, ...a], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); } catch { return ''; } };
|
|
129
132
|
const dirty = g('status', '--porcelain').length > 0;
|
|
133
|
+
// nothing to verify yet: a clean tree with no previous commit to diff against (a fresh repo). Say so
|
|
134
|
+
// plainly instead of the generic "could not run" below, so the first run is not a dead end.
|
|
135
|
+
if (!dirty && !g('rev-parse', '--verify', 'HEAD~1')) { out('🐴 sawbuck: nothing to verify yet, the working tree is clean and this is the only commit. Make a change (or add one more commit), then `sawbuck check` re-proves the win from your source edits alone.'); process.exit(2); }
|
|
130
136
|
const where = dirty ? 'working' : 'last';
|
|
131
137
|
const { rep, code } = gateReport(repo, dirty ? ['run', '--working'] : ['run', '--base', 'HEAD~1', '--head', 'HEAD']);
|
|
132
138
|
if (!rep || !rep.verdict) { out('🐴 sawbuck: could not run the check here. Make sure this is a git repo with a commit, and a test command (or run `sawbuck init`).'); process.exit(2); }
|