sawbuck 0.1.19 → 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/CONTRACT.md CHANGED
@@ -8,6 +8,8 @@ This is an open interface for making an AI coding harness trustworthy. It define
8
8
 
9
9
  The contract has one property that holds all of it together: a verdict is **deterministic and reproducible from `(base, head, config)` alone**. No LLM is in the path, and nothing is read from the agent's trajectory, so the result cannot be obfuscated against and does not change when you ask it twice.
10
10
 
11
+ A corollary worth stating as an invariant: **the gate is defined only by in-repo, versioned config.** External systems (a dashboard, an issue tracker, a memory provider) may *report on* a verdict, but they must never *define* the guardrail. A guardrail that lives outside the repo is one the check cannot see in the diff, so it cannot be fingerprinted, so loosening it leaves no trace to catch as gamed. Memory (the input) is a pluggable socket; the gate (the output) stays in-repo and fingerprinted.
12
+
11
13
  > This contract covers the check. Sawbuck pairs it with the other half, **memory** (the relevant history for what you touch, plus the live debt), as the mirror idea: where the check is a standard others *conform to*, memory is a socket others *fill*. The default memory provider is @promptwheel/logbook, carried along so it works out of the box, but it is swappable; logbook is a default, not a requirement of the idea.
12
14
  >
13
15
  > Both halves nest with your folders, so policy and memory flow top-down and isolate across siblings. A parent `promptwheel.config.json` passes its guardrails down via `extends` (repos inherit the base gate; local metrics override by name), and memory inherits a parent folder's shared scope the same way (a repo sees itself and its ancestors, never a sibling). One model for both: the directory tree is the scope.
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`, `invalid`. Metaphor jargon in the model's context forces
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 weakening its test | **invalid** | the test was weakened, not the metric improved (engine verdict: `gamed`) |
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} invalid changes caught
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 still emits its own verdicts (`gamed`, `fail`, `pass`); Sawbuck
71
- maps them to the plain outcome words above (`gamed` → **invalid**, `fail` →
72
- **regression**). Engine-internal names are not part of this surface.
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
 
@@ -77,7 +88,7 @@ Debt also rises on its own: once attached, the per-turn `context` hook surfaces
77
88
  Sawbuck is thin glue over two roles, each behind an open interface that points in opposite directions:
78
89
 
79
90
  - **the check** : the deterministic outcome gate, a published standard (the [Verified-Outcome Contract](CONTRACT.md)) that others conform to. Reference implementation: **promptwheel**. You plug in *what* to measure (your tests, lint, coverage, an eval); the un-gameable source re-proof is the fixed part. Embed it with `import { check }`, or check any tool's verdict with `promptwheel conformance`.
80
- - **the memory** : the relevant history for the files you touch, plus the live debt. A socket you can fill. Default provider: **@promptwheel/logbook** (git history as agent memory; also ships an MCP server). Sawbuck reads it through one adapter, so you can *compose in your own* memory: `SAWBUCK_MEMORY=logbook,obsidian` keeps logbook as the git-truth + debt provider and adds your notes, then `sawbuck recall`. Two reference providers ship in `providers/`: **obsidian** (local notes, offline; set `OBSIDIAN_VAULT`) and **mem0** (semantic memory; the one that reaches the network, so it needs a `MEM0_API_KEY` or a `~/.mem0` key file, unlike the offline default; verified against Mem0's v3 API). Any bin that speaks the three contract verbs works; `sawbuck memory-conformance <bin>` checks one. Composed providers are **folder-scoped and isolated by default**: each repo sees only its own memory, never a sibling's. Drop a `.sawbuck-root` marker (or set `SAWBUCK_MEMORY_ROOT`) in a parent folder to let repos under it inherit that folder's shared memory, top down. logbook is the default, not a hard requirement of the idea.
91
+ - **the memory** : the relevant history for the files you touch, plus the live debt. A socket you can fill. Default provider: **@promptwheel/logbook** (git history as agent memory; also ships an MCP server). Sawbuck reads it through one adapter, so you can *compose in your own* memory: `SAWBUCK_MEMORY=logbook,obsidian` keeps logbook as the git-truth + debt provider and adds your notes, then `sawbuck recall`. Four reference providers ship in `providers/`: **obsidian** (local notes vault; set `OBSIDIAN_VAULT`), **local** (a plain JSON file, zero setup), **claude** (read-only: your project's `CLAUDE.md` / `.claude` memory, already there, nothing to configure), and **mem0** (semantic memory; the one that reaches the network, so it needs a `MEM0_API_KEY` or a `~/.mem0` key file, unlike the offline default; verified against Mem0's v3 API). Any bin that speaks the three contract verbs works; `sawbuck memory-conformance <bin>` checks one. Composed providers are **folder-scoped and isolated by default**: each repo sees only its own memory, never a sibling's. Drop a `.sawbuck-root` marker (or set `SAWBUCK_MEMORY_ROOT`) in a parent folder to let repos under it inherit that folder's shared memory, top down. logbook is the default, not a hard requirement of the idea.
81
92
 
82
93
  In one line: the check is a standard we publish for others to meet; the memory is a socket we expose for others to fill. Sawbuck composes the two.
83
94
 
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, '..', 'referee', 'bin', 'promptwheel.mjs'));
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
- const pwArgs = ['init', '--force'];
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/memory.mjs CHANGED
@@ -31,7 +31,7 @@ function logbookBin() {
31
31
  }
32
32
  const envKey = (name) => 'SAWBUCK_MEMORY_' + name.toUpperCase().replace(/[^A-Z0-9]/g, '_') + '_BIN';
33
33
  // the ordered provider list. Each: { name, bin, dialect: 'logbook' | 'contract' }.
34
- const SHIPPED = new Set(['obsidian', 'mem0']); // reference contract providers that ship in providers/
34
+ const SHIPPED = new Set(['obsidian', 'mem0', 'local', 'claude']); // reference contract providers that ship in providers/
35
35
  export function providers() {
36
36
  const spec = (process.env.SAWBUCK_MEMORY || 'logbook').split(',').map((s) => s.trim()).filter(Boolean);
37
37
  return spec.map((name) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sawbuck",
3
- "version": "0.1.19",
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",
@@ -30,6 +30,8 @@
30
30
  "memory.mjs",
31
31
  "providers/obsidian.mjs",
32
32
  "providers/mem0.mjs",
33
+ "providers/local.mjs",
34
+ "providers/claude.mjs",
33
35
  "run.mjs",
34
36
  "README.md",
35
37
  "GLOSSARY.md",
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ // claude.mjs, a REFERENCE read-only memory provider: your project's Claude Code memory (CLAUDE.md,
3
+ // AGENTS.md, and top-level .claude/*.md) as a recall source. It is the most UBIQUITOUS local memory -
4
+ // a Claude Code repo already has it - so it is zero-setup. READ-ONLY by design: recall only; debt and
5
+ // record EXIT NONZERO (unsupported). This is a view of memory you already keep, not a write target
6
+ // (writing findings into CLAUDE.md would bloat the always-loaded project context), and the contract
7
+ // lets a provider implement a subset. Inherently leaf-scoped (reads only this repo's files, so no
8
+ // cross-repo leak). Zero-dep, offline, local-notes axis.
9
+ //
10
+ // recall --file P [--file Q] --json relevant lines from your project memory -> Finding[] (kind note)
11
+ // debt | record EXIT NONZERO (unsupported)
12
+
13
+ import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
14
+ import { join, basename, extname } from 'node:path';
15
+
16
+ const argv = process.argv.slice(2);
17
+ const cmd = argv[0];
18
+ const flagAll = (n) => argv.reduce((a, v, i) => (argv[i - 1] === n ? [...a, v] : a), []);
19
+ const die = (m, c = 2) => { process.stderr.write(`claude provider: ${m}\n`); process.exit(c); };
20
+ const CWD = process.cwd();
21
+
22
+ if (cmd === 'debt' || cmd === 'record') die(`${cmd} is unsupported (claude memory is a read-only recall source)`, 2);
23
+
24
+ if (cmd === 'recall') {
25
+ const paths = flagAll('--file');
26
+ if (!paths.length) { console.log('[]'); process.exit(0); }
27
+ const matches = (l) => paths.find((p) => l.includes(p) || (basename(p).length > 2 && l.includes(basename(p)))) || null;
28
+ // the project's Claude Code memory: CLAUDE.md / AGENTS.md at the root, plus top-level .claude/*.md.
29
+ const sources = [];
30
+ for (const f of ['CLAUDE.md', 'AGENTS.md']) { const p = join(CWD, f); if (existsSync(p)) sources.push(p); }
31
+ try { for (const e of readdirSync(join(CWD, '.claude'))) if (extname(e).toLowerCase() === '.md') sources.push(join(CWD, '.claude', e)); } catch {}
32
+ const out = [];
33
+ for (const src of sources) {
34
+ let text = ''; try { if (statSync(src).size > 512 * 1024) continue; text = readFileSync(src, 'utf8'); } catch { continue; }
35
+ const lines = text.split('\n');
36
+ let heading = basename(src);
37
+ for (let i = 0; i < lines.length; i++) {
38
+ const h = lines[i].match(/^#{1,6}\s+(.+)/);
39
+ if (h) { heading = h[1].trim(); continue; }
40
+ const hit = matches(lines[i]);
41
+ if (!hit) continue;
42
+ out.push({ kind: 'note', path: hit, ref: `${basename(src)}:${i + 1}`, date: null, summary: `${heading}: ${lines[i].trim()}`.slice(0, 280), score: 0.4 });
43
+ }
44
+ }
45
+ console.log(JSON.stringify(out.slice(0, 8)));
46
+ process.exit(0);
47
+ }
48
+
49
+ die(`unknown command '${cmd || ''}' (recall | debt | record)`, 2);
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ // local.mjs, a REFERENCE minimal local-file memory provider: the simplest way to fill the socket with
3
+ // no external tool. Findings live as JSON in <repo>/.sawbuck/memory-notes.json (git-excluded by attach).
4
+ // It is the cleanest conformance target and a "bring your own, just a file" option. Zero-dep, offline,
5
+ // in the local-notes axis alongside obsidian. Folder-scoped like the others.
6
+ //
7
+ // recall --file P [--scope ID ...] --json -> Finding[] (kind note)
8
+ // record --ref SHA --summary S [--scope ID] [--side X] [--evidence-file F] --by WHO
9
+ // debt --json EXITS NONZERO (a note store is not git-aware)
10
+ //
11
+ // Store: SAWBUCK_LOCAL_MEMORY, else <cwd>/.sawbuck/memory-notes.json.
12
+
13
+ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
14
+ import { join, basename, dirname } from 'node:path';
15
+
16
+ const argv = process.argv.slice(2);
17
+ const cmd = argv[0];
18
+ const flagAll = (n) => argv.reduce((a, v, i) => (argv[i - 1] === n ? [...a, v] : a), []);
19
+ const flag = (n, d) => { const i = argv.indexOf(n); return i > -1 ? (argv[i + 1] ?? true) : d; };
20
+ const die = (m, c = 2) => { process.stderr.write(`local provider: ${m}\n`); process.exit(c); };
21
+
22
+ const STORE = process.env.SAWBUCK_LOCAL_MEMORY || join(process.cwd(), '.sawbuck', 'memory-notes.json');
23
+ const load = () => { try { const v = JSON.parse(readFileSync(STORE, 'utf8')); return Array.isArray(v) ? v : []; } catch { return []; } };
24
+
25
+ if (cmd === 'debt') die('debt is unsupported by a note store (git-truth lives in a git provider like logbook)', 2);
26
+
27
+ if (cmd === 'recall') {
28
+ const paths = flagAll('--file');
29
+ if (!paths.length) { console.log('[]'); process.exit(0); }
30
+ const scopes = new Set(flagAll('--scope')); // self + ancestors; empty (direct call) = match any
31
+ const bases = paths.map((p) => basename(p));
32
+ const out = [];
33
+ for (const f of load()) {
34
+ if (scopes.size && f.scope != null && !scopes.has(f.scope)) continue; // isolation: a sibling scope is excluded
35
+ const hit = (f.path && paths.includes(f.path)) || (f.summary && bases.some((b) => f.summary.includes(b)));
36
+ if (!hit) continue;
37
+ out.push({ kind: 'note', path: f.path ?? null, ref: f.ref ?? null, date: f.date ?? null, summary: String(f.summary || '').slice(0, 280), scope: f.scope ?? '', score: 0.5 });
38
+ }
39
+ out.sort((a, b) => String(b.date).localeCompare(String(a.date)));
40
+ console.log(JSON.stringify(out.slice(0, 8)));
41
+ process.exit(0);
42
+ }
43
+
44
+ if (cmd === 'record') {
45
+ const ref = String(flag('--ref', '')), summary = String(flag('--summary', '')), by = String(flag('--by', 'sawbuck'));
46
+ const scope = flagAll('--scope')[0] ?? '';
47
+ const side = flag('--side', ''), evidence = flag('--evidence-file', '');
48
+ const notes = load();
49
+ if (notes.some((f) => f.ref === ref && f.summary === summary && f.scope === scope)) { console.log('recorded (already present)'); process.exit(0); }
50
+ notes.push({ ref, summary, path: evidence || null, scope, by, ...(side ? { side: String(side) } : {}), date: new Date().toISOString().slice(0, 10) });
51
+ try { mkdirSync(dirname(STORE), { recursive: true }); writeFileSync(STORE, JSON.stringify(notes, null, 2) + '\n'); console.log('recorded'); process.exit(0); }
52
+ catch (e) { die(`write failed: ${String(e.message).split('\n')[0]}`, 1); }
53
+ }
54
+
55
+ die(`unknown command '${cmd || ''}' (recall | debt | record)`, 2);
@@ -103,7 +103,16 @@ if (cmd === 'record') {
103
103
  // a [[wikilink]] to the evidence path so Obsidian's graph connects the finding to the code note.
104
104
  const link = evidence ? `\n- evidence: [[${String(evidence).replace(/[[\]]/g, '')}]]` : '';
105
105
  const body = `---\nsawbuck: finding\nref: ${ref}\nby: ${by}\ndate: ${date}\nscope: ${leaf}\n---\n\n# ${summary.slice(0, 80)}\n\n${summary}\n\n- ref: \`${ref}\`${side ? `\n- side: ${side}` : ''}${link}\n`;
106
- try { writeFileSync(join(dir, `${safeRef}.md`), body); console.log('recorded'); process.exit(0); }
106
+ // no-clobber + dedupe: if this exact finding is already noted, skip; if a DIFFERENT note shares the
107
+ // ref, write a suffixed sibling instead of overwriting the earlier one.
108
+ let target = join(dir, `${safeRef}.md`);
109
+ if (existsSync(target)) {
110
+ let prev = ''; try { prev = readFileSync(target, 'utf8'); } catch {}
111
+ if (prev.includes(summary)) { console.log('recorded (already present)'); process.exit(0); }
112
+ let i = 2; while (existsSync(join(dir, `${safeRef}-${i}.md`))) i++;
113
+ target = join(dir, `${safeRef}-${i}.md`);
114
+ }
115
+ try { writeFileSync(target, body); console.log('recorded'); process.exit(0); }
107
116
  catch (e) { die(`write failed: ${String(e.message).split('\n')[0]}`, 1); }
108
117
  }
109
118
 
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, '..', 'referee', 'bin', 'promptwheel.mjs'));
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, '..', 'referee', 'bin', 'promptwheel.mjs'));
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, '..', 'referee', 'bin', 'promptwheel.mjs');
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. Wizard in a terminal,
69
- non-interactive in a pipe/CI/agent (or with --yes).
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
- const g = (...a) => { try { return execFileSync('git', ['-C', repo, ...a], { encoding: 'utf8' }).trim(); } catch { return ''; } };
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); }