altimate-receipts 0.3.2 → 0.4.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.
package/dist/cli.js CHANGED
@@ -45,14 +45,291 @@ import {
45
45
  } from "./chunk-UHI6BGLE.js";
46
46
 
47
47
  // src/cli.ts
48
- import { spawnSync } from "child_process";
49
- import { existsSync, mkdirSync, readFileSync as readFileSync4, realpathSync, writeFileSync } from "fs";
50
- import { join as join4, relative } from "path";
48
+ import { spawnSync as spawnSync3 } from "child_process";
49
+ import {
50
+ existsSync as existsSync4,
51
+ mkdirSync as mkdirSync2,
52
+ readFileSync as readFileSync6,
53
+ readdirSync as readdirSync3,
54
+ realpathSync,
55
+ unlinkSync,
56
+ writeFileSync as writeFileSync3
57
+ } from "fs";
58
+ import { homedir } from "os";
59
+ import { join as join6, relative } from "path";
51
60
  import { pathToFileURL } from "url";
52
61
 
62
+ // src/hook/installGitHook.ts
63
+ import { spawnSync } from "child_process";
64
+ import { chmodSync, existsSync, readFileSync, renameSync, writeFileSync } from "fs";
65
+ import { isAbsolute, join } from "path";
66
+ var MARKER = "altimate-receipts";
67
+ var HOOK_SCRIPT = `#!/bin/sh
68
+ # Verified-by-Receipts pre-push hook \u2014 generated by \`receipts install-hook\` (altimate-receipts).
69
+ # Attaches this branch's agent receipt before pushing. Best-effort: only the
70
+ # deliberate "receipt attached, push again" signal (exit 42) ever stops a push.
71
+ hookdir=$(dirname "$0")
72
+ if [ -x "$hookdir/pre-push.local" ]; then
73
+ "$hookdir/pre-push.local" "$@" </dev/null || exit $?
74
+ fi
75
+ command -v npx >/dev/null 2>&1 || exit 0
76
+ npx -y altimate-receipts@latest hook pre-push --agent git
77
+ [ "$?" -eq 42 ] && exit 1
78
+ exit 0
79
+ `;
80
+ function hooksDir(cwd) {
81
+ const r = spawnSync("git", ["rev-parse", "--git-path", "hooks"], { encoding: "utf8", cwd });
82
+ if (r.status !== 0) {
83
+ return null;
84
+ }
85
+ const p = r.stdout.trim();
86
+ return isAbsolute(p) ? p : join(cwd ?? process.cwd(), p);
87
+ }
88
+ function installGitHook(cwd) {
89
+ const dir = hooksDir(cwd);
90
+ if (!dir) {
91
+ return { ok: false, reason: "not a git repository" };
92
+ }
93
+ const target = join(dir, "pre-push");
94
+ const local = join(dir, "pre-push.local");
95
+ if (existsSync(target)) {
96
+ const current = readFileSync(target, "utf8");
97
+ if (current.includes(MARKER)) {
98
+ if (current === HOOK_SCRIPT) {
99
+ return { ok: true, action: "unchanged" };
100
+ }
101
+ writeFileSync(target, HOOK_SCRIPT);
102
+ chmodSync(target, 493);
103
+ return { ok: true, action: "installed" };
104
+ }
105
+ if (existsSync(local)) {
106
+ return {
107
+ ok: false,
108
+ reason: `${target} and ${local} both exist and aren't receipts hooks \u2014 resolve manually`
109
+ };
110
+ }
111
+ renameSync(target, local);
112
+ writeFileSync(target, HOOK_SCRIPT);
113
+ chmodSync(target, 493);
114
+ return { ok: true, action: "chained" };
115
+ }
116
+ writeFileSync(target, HOOK_SCRIPT);
117
+ chmodSync(target, 493);
118
+ return { ok: true, action: "installed" };
119
+ }
120
+ var PREPARE_CMD = "npx -y altimate-receipts@latest install-hook";
121
+ function wirePrepareScript(pkgPath) {
122
+ if (!existsSync(pkgPath)) {
123
+ return { ok: false, reason: `${pkgPath} not found \u2014 \`--prepare\` needs a package.json` };
124
+ }
125
+ const raw = readFileSync(pkgPath, "utf8");
126
+ let pkg;
127
+ try {
128
+ pkg = JSON.parse(raw);
129
+ } catch {
130
+ return { ok: false, reason: `${pkgPath} is not valid JSON \u2014 leaving it untouched` };
131
+ }
132
+ if (pkg.scripts === void 0) {
133
+ pkg.scripts = {};
134
+ }
135
+ const scripts = pkg.scripts;
136
+ const prepare = scripts.prepare;
137
+ if (prepare?.includes(PREPARE_CMD)) {
138
+ return { ok: true, changed: false };
139
+ }
140
+ scripts.prepare = prepare ? `${prepare} && ${PREPARE_CMD}` : PREPARE_CMD;
141
+ const indent = raw.match(/^(\s+)"/m)?.[1] ?? " ";
142
+ writeFileSync(pkgPath, `${JSON.stringify(pkg, null, indent)}
143
+ `);
144
+ return { ok: true, changed: true };
145
+ }
146
+
147
+ // src/hook/prePush.ts
148
+ import { spawnSync as spawnSync2 } from "child_process";
149
+ import { existsSync as existsSync2 } from "fs";
150
+ import { join as join2 } from "path";
151
+ var REPUSH_EXIT = 42;
152
+ var ATTACH_SUBJECT = (branch) => `chore(receipts): attach agent receipt for ${branch}`;
153
+ function git(args, cwd) {
154
+ const r = spawnSync2("git", args, { encoding: "utf8", cwd });
155
+ return r.status === 0 ? r.stdout.trim() : "";
156
+ }
157
+ var WRAPPERS = /* @__PURE__ */ new Set(["command", "exec", "nohup", "time", "env"]);
158
+ var GIT_VALUE_FLAGS = /* @__PURE__ */ new Set(["-C", "-c", "--git-dir", "--work-tree", "--exec-path"]);
159
+ var TAG_REFSPEC = /^(refs\/tags\/|v\d+(\.\d+)*$)/;
160
+ function isGitPush(command) {
161
+ const blanked = command.replace(/\\["']/g, " ").replace(/'[^']*'/g, " ").replace(/"[^"]*"/g, " ");
162
+ for (const simple of blanked.split(/(?:&&|\|\||[;|\n])/)) {
163
+ const tokens = simple.trim().split(/\s+/).filter(Boolean);
164
+ let i = 0;
165
+ while (i < tokens.length && (/^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[i]) || WRAPPERS.has(tokens[i]))) {
166
+ i++;
167
+ }
168
+ if (tokens[i] !== "git") {
169
+ continue;
170
+ }
171
+ i++;
172
+ while (i < tokens.length && tokens[i].startsWith("-")) {
173
+ const flag = tokens[i];
174
+ i++;
175
+ if (GIT_VALUE_FLAGS.has(flag)) {
176
+ i++;
177
+ }
178
+ }
179
+ if (tokens[i] !== "push") {
180
+ continue;
181
+ }
182
+ const rest = tokens.slice(i + 1);
183
+ if (rest.includes("--dry-run") || rest.includes("-n") || rest.includes("--tags")) {
184
+ continue;
185
+ }
186
+ const positionals = rest.filter((t) => !t.startsWith("-"));
187
+ const refspecs = positionals.slice(1);
188
+ if (refspecs.length > 0 && refspecs.every((r) => TAG_REFSPEC.test(r))) {
189
+ continue;
190
+ }
191
+ return true;
192
+ }
193
+ return false;
194
+ }
195
+ function gitStdinPushesBranch(stdin) {
196
+ return stdin.split("\n").some((line) => line.trim().startsWith("refs/heads/"));
197
+ }
198
+ function repoOptedIn(repoRoot) {
199
+ return existsSync2(join2(repoRoot, ".github", "workflows", "receipts.yml")) || existsSync2(join2(repoRoot, ".receipts"));
200
+ }
201
+ function headIsAttachCommit(branch, cwd) {
202
+ return git(["log", "-1", "--format=%s"], cwd) === ATTACH_SUBJECT(branch);
203
+ }
204
+ async function readStdin(stream = process.stdin) {
205
+ let data = "";
206
+ for await (const chunk of stream) {
207
+ data += chunk;
208
+ }
209
+ return data;
210
+ }
211
+ async function runHookPrePush(dialect, stdin, generate) {
212
+ try {
213
+ if (process.env.RECEIPTS_HOOK === "0") {
214
+ return { exit: 0 };
215
+ }
216
+ if ((process.env.RECEIPTS_STORE || "commit").toLowerCase() === "none") {
217
+ return { exit: 0 };
218
+ }
219
+ if (dialect === "claude") {
220
+ let payload;
221
+ try {
222
+ payload = JSON.parse(stdin);
223
+ } catch {
224
+ return { exit: 0 };
225
+ }
226
+ if (payload.tool_name !== "Bash" || !payload.tool_input?.command) {
227
+ return { exit: 0 };
228
+ }
229
+ if (payload.cwd && existsSync2(payload.cwd)) {
230
+ process.chdir(payload.cwd);
231
+ }
232
+ if (!isGitPush(payload.tool_input.command)) {
233
+ return { exit: 0 };
234
+ }
235
+ } else if (!gitStdinPushesBranch(stdin)) {
236
+ return { exit: 0 };
237
+ }
238
+ const repoRoot = git(["rev-parse", "--show-toplevel"]);
239
+ if (!repoRoot || !repoOptedIn(repoRoot)) {
240
+ return { exit: 0 };
241
+ }
242
+ const branch = git(["rev-parse", "--abbrev-ref", "HEAD"]);
243
+ if (!branch || branch === "HEAD") {
244
+ return { exit: 0 };
245
+ }
246
+ if (dialect === "git" && headIsAttachCommit(branch)) {
247
+ return { exit: 0 };
248
+ }
249
+ const write = process.stderr.write.bind(process.stderr);
250
+ process.stderr.write = (() => true);
251
+ let generated;
252
+ try {
253
+ generated = await generate();
254
+ } finally {
255
+ process.stderr.write = write;
256
+ }
257
+ if (generated !== 0) {
258
+ return { exit: 0 };
259
+ }
260
+ if (!git(["status", "--porcelain", ".receipts/"])) {
261
+ return { exit: 0 };
262
+ }
263
+ spawnSync2("git", ["add", ".receipts/"], { encoding: "utf8" });
264
+ const commit = spawnSync2(
265
+ "git",
266
+ ["commit", "--no-verify", "-m", ATTACH_SUBJECT(branch), "--", ".receipts/"],
267
+ { encoding: "utf8" }
268
+ );
269
+ if (commit.status !== 0) {
270
+ return { exit: 0 };
271
+ }
272
+ if (dialect === "git") {
273
+ return {
274
+ exit: REPUSH_EXIT,
275
+ message: `\u{1F4CE} Receipts: attached an agent receipt for '${branch}' \u2014 run \`git push\` again to include it.`
276
+ };
277
+ }
278
+ return {
279
+ exit: 0,
280
+ message: `receipts: attached an agent receipt for '${branch}' \u2014 it rides this push.`
281
+ };
282
+ } catch {
283
+ return { exit: 0 };
284
+ }
285
+ }
286
+
287
+ // src/hook/settingsMerge.ts
288
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
289
+ import { dirname } from "path";
290
+ var HOOK_COMMAND = "npx -y altimate-receipts@latest hook pre-push";
291
+ function mergeHookIntoSettings(path) {
292
+ let settings = {};
293
+ if (existsSync3(path)) {
294
+ let parsed;
295
+ try {
296
+ parsed = JSON.parse(readFileSync2(path, "utf8"));
297
+ } catch {
298
+ return { ok: false, reason: `${path} is not valid JSON \u2014 leaving it untouched` };
299
+ }
300
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
301
+ return { ok: false, reason: `${path} is not a JSON object \u2014 leaving it untouched` };
302
+ }
303
+ settings = parsed;
304
+ }
305
+ if (settings.hooks === void 0) {
306
+ settings.hooks = {};
307
+ }
308
+ const hooks = settings.hooks;
309
+ if (typeof hooks !== "object" || hooks === null || Array.isArray(hooks)) {
310
+ return { ok: false, reason: `${path} has a non-object "hooks" key \u2014 leaving it untouched` };
311
+ }
312
+ if (hooks.PreToolUse === void 0) {
313
+ hooks.PreToolUse = [];
314
+ }
315
+ const pre = hooks.PreToolUse;
316
+ if (!Array.isArray(pre)) {
317
+ return { ok: false, reason: `${path} has a non-array hooks.PreToolUse \u2014 leaving it untouched` };
318
+ }
319
+ const present = pre.some((e) => e?.hooks?.some((h) => h?.command === HOOK_COMMAND));
320
+ if (present) {
321
+ return { ok: true, changed: false };
322
+ }
323
+ pre.push({ matcher: "Bash", hooks: [{ type: "command", command: HOOK_COMMAND }] });
324
+ mkdirSync(dirname(path), { recursive: true });
325
+ writeFileSync2(path, `${JSON.stringify(settings, null, 2)}
326
+ `);
327
+ return { ok: true, changed: true };
328
+ }
329
+
53
330
  // src/receipt/assert.ts
54
- import { readFileSync } from "fs";
55
- import { join } from "path";
331
+ import { readFileSync as readFileSync3 } from "fs";
332
+ import { join as join3 } from "path";
56
333
  var OPS = /* @__PURE__ */ new Set([
57
334
  "eq",
58
335
  "ne",
@@ -147,10 +424,10 @@ function validateAssertion(raw) {
147
424
  };
148
425
  }
149
426
  function loadAsserts(repoRoot) {
150
- const path = join(repoRoot, ".receipts", "asserts.json");
427
+ const path = join3(repoRoot, ".receipts", "asserts.json");
151
428
  let text;
152
429
  try {
153
- text = readFileSync(path, "utf8");
430
+ text = readFileSync3(path, "utf8");
154
431
  } catch {
155
432
  return [];
156
433
  }
@@ -374,8 +651,8 @@ function renderFieldScan(s) {
374
651
  }
375
652
 
376
653
  // src/report/log.ts
377
- import { readFileSync as readFileSync2, readdirSync } from "fs";
378
- import { join as join2 } from "path";
654
+ import { readFileSync as readFileSync4, readdirSync } from "fs";
655
+ import { join as join4 } from "path";
379
656
  var SEV_ICON2 = { critical: "\u26D4", high: "\u26A0\uFE0F", medium: "\u{1F50D}", low: "\xB7" };
380
657
  var SEV_RANK = { critical: 0, high: 1, medium: 2, low: 3 };
381
658
  var NON_RECEIPT = /(?:^|\/)(?:asserts(?:\.example)?|sample)\.json$/i;
@@ -399,7 +676,7 @@ function loadReceiptHistory(dir) {
399
676
  for (const f of files) {
400
677
  let input;
401
678
  try {
402
- input = JSON.parse(readFileSync2(join2(dir, f), "utf8"));
679
+ input = JSON.parse(readFileSync4(join4(dir, f), "utf8"));
403
680
  } catch {
404
681
  continue;
405
682
  }
@@ -457,6 +734,26 @@ ${lines.join("\n")}
457
734
  `;
458
735
  }
459
736
 
737
+ // src/report/prune.ts
738
+ var PROTECTED = /(?:^|\/)(?:asserts(?:\.example)?|sample)\.json$/i;
739
+ function branchSlug(branch) {
740
+ return branch.replace(/[/\\]/g, "-");
741
+ }
742
+ function planPrune(files, liveSlugs) {
743
+ const keep = [];
744
+ const remove = [];
745
+ for (const f of files) {
746
+ const base = f.split("/").pop() ?? f;
747
+ const slug = base.replace(/\.json$/i, "");
748
+ if (PROTECTED.test(base) || liveSlugs.has(slug)) {
749
+ keep.push(f);
750
+ } else {
751
+ remove.push(f);
752
+ }
753
+ }
754
+ return { keep, remove };
755
+ }
756
+
460
757
  // src/report/sarif.ts
461
758
  var INFO_URI = "https://github.com/AltimateAI/altimate-receipts";
462
759
  function levelOf(severity) {
@@ -542,8 +839,8 @@ function toSarif(receipt) {
542
839
  }
543
840
 
544
841
  // src/report/stats.ts
545
- import { readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
546
- import { join as join3 } from "path";
842
+ import { readFileSync as readFileSync5, readdirSync as readdirSync2 } from "fs";
843
+ import { join as join5 } from "path";
547
844
  var NON_RECEIPT2 = /(?:^|\/)(?:asserts(?:\.example)?|sample)\.json$/i;
548
845
  function computeStats(dir) {
549
846
  let files;
@@ -561,7 +858,7 @@ function computeStats(dir) {
561
858
  for (const f of files) {
562
859
  let input;
563
860
  try {
564
- input = JSON.parse(readFileSync3(join3(dir, f), "utf8"));
861
+ input = JSON.parse(readFileSync5(join5(dir, f), "utf8"));
565
862
  } catch {
566
863
  skipped++;
567
864
  continue;
@@ -704,7 +1001,14 @@ Usage
704
1001
  receipts eval Flag-rate of the detectors over your real local sessions (--last N, --json)
705
1002
  receipts badge [receipt] shields.io endpoint JSON for a README/PR badge (--out f)
706
1003
  receipts sarif [receipt] SARIF 2.1.0 for GitHub code-scanning (inline + Security tab; --out f)
707
- receipts init Scaffold the PR-check workflow into this repo (1-command adopt)
1004
+ receipts prune [dir] Remove committed receipts for merged/deleted branches (--dry-run)
1005
+ receipts init One-command adopt: PR-check workflow + the repo-committed
1006
+ agent hook (--prepare also wires the git-hook floor)
1007
+ receipts hook pre-push (called by hooks, not humans) attach the receipt on push
1008
+ (--agent claude | git selects the payload dialect)
1009
+ receipts install-hook Write the self-updating pre-push hook into .git/hooks
1010
+ receipts setup-local Add the hook to YOUR ~/.claude/settings.json (only fires
1011
+ in repos that have adopted the Receipts workflow)
708
1012
  receipts rederive <file> Reproduce the canonical Receipt from a transcript
709
1013
  receipts assert [selector] Check the receipt against committed .receipts/asserts.json (CI gate)
710
1014
  receipts mcp Start the MCP server (stdio) for IDEs/agents
@@ -756,9 +1060,13 @@ var COMMANDS = /* @__PURE__ */ new Set([
756
1060
  "stats",
757
1061
  "eval",
758
1062
  "badge",
1063
+ "prune",
759
1064
  "sarif",
760
1065
  "init",
761
- "assert"
1066
+ "assert",
1067
+ "hook",
1068
+ "install-hook",
1069
+ "setup-local"
762
1070
  ]);
763
1071
  function parseArgs(argv) {
764
1072
  const args = argv.slice(2);
@@ -772,7 +1080,9 @@ function parseArgs(argv) {
772
1080
  handoff: false,
773
1081
  redact: false,
774
1082
  copy: false,
1083
+ dryRun: false,
775
1084
  wholeSession: false,
1085
+ prepare: false,
776
1086
  color: !process.env.NO_COLOR && process.stdout.isTTY === true
777
1087
  };
778
1088
  const positionals = [];
@@ -811,13 +1121,20 @@ function parseArgs(argv) {
811
1121
  if (Number.isFinite(n) && n > 0) {
812
1122
  parsed.last = Math.floor(n);
813
1123
  }
1124
+ } else if (a === "--dry-run") {
1125
+ parsed.dryRun = true;
814
1126
  } else if (a === "--whole-session") {
815
1127
  parsed.wholeSession = true;
1128
+ } else if (a === "--prepare") {
1129
+ parsed.prepare = true;
816
1130
  } else if (a === "--agent") {
817
1131
  const next = args[i + 1];
818
1132
  if (next && agentIds().includes(next)) {
819
1133
  parsed.agent = next;
820
1134
  i++;
1135
+ } else if (next === "claude" || next === "git") {
1136
+ parsed.hookDialect = next;
1137
+ i++;
821
1138
  }
822
1139
  } else if (a === "--no-color") {
823
1140
  parsed.color = false;
@@ -872,8 +1189,20 @@ async function run(argv) {
872
1189
  if (args.command === "sarif") {
873
1190
  return runSarif(args.file, { out: args.out });
874
1191
  }
1192
+ if (args.command === "prune") {
1193
+ return runPrune(args.file, { dryRun: args.dryRun });
1194
+ }
875
1195
  if (args.command === "init") {
876
- return runInit();
1196
+ return runInit({ prepare: args.prepare });
1197
+ }
1198
+ if (args.command === "hook") {
1199
+ return runHook(args.file, args.hookDialect ?? "claude");
1200
+ }
1201
+ if (args.command === "install-hook") {
1202
+ return runInstallHook();
1203
+ }
1204
+ if (args.command === "setup-local") {
1205
+ return runSetupLocal();
877
1206
  }
878
1207
  if (args.command === "rederive") {
879
1208
  return runRederive(args.file, {
@@ -960,7 +1289,7 @@ Run a coding-agent session first, then try again.
960
1289
  }
961
1290
  const out = args.compact ? canonicalize(receipt) : JSON.stringify(receipt, null, 2);
962
1291
  if (args.out) {
963
- writeFileSync(args.out, `${out}
1292
+ writeFileSync3(args.out, `${out}
964
1293
  `);
965
1294
  process.stderr.write(`Receipt written to ${args.out}
966
1295
  `);
@@ -983,8 +1312,8 @@ Run a coding-agent session first, then try again.
983
1312
  process.stdout.write(renderCard({ summary, derived, findings }, { color: args.color }));
984
1313
  return 0;
985
1314
  }
986
- function git(args) {
987
- const r = spawnSync("git", args, { encoding: "utf8" });
1315
+ function git2(args) {
1316
+ const r = spawnSync3("git", args, { encoding: "utf8" });
988
1317
  return r.status === 0 ? r.stdout.trim() : "";
989
1318
  }
990
1319
  var PR_SELECT_SCAN = 150;
@@ -1021,8 +1350,8 @@ async function pickForDiff(all, branch, repoRoot, files) {
1021
1350
  return best ?? primary;
1022
1351
  }
1023
1352
  async function runPr(opts) {
1024
- const branch = opts.branch || git(["rev-parse", "--abbrev-ref", "HEAD"]);
1025
- const repoRoot = git(["rev-parse", "--show-toplevel"]);
1353
+ const branch = opts.branch || git2(["rev-parse", "--abbrev-ref", "HEAD"]);
1354
+ const repoRoot = git2(["rev-parse", "--show-toplevel"]);
1026
1355
  if (!branch || branch === "HEAD") {
1027
1356
  process.stderr.write("receipts pr: not on a git branch (use --branch <name>).\n");
1028
1357
  return 1;
@@ -1087,11 +1416,11 @@ Build the branch with a coding agent first, or run \`receipts --list\`.
1087
1416
  `);
1088
1417
  }
1089
1418
  const safe = branch.replace(/[/\\]/g, "-");
1090
- const dir = join4(repoRoot || ".", ".receipts");
1091
- const out = opts.out ?? join4(dir, `${safe}.json`);
1092
- mkdirSync(dir, { recursive: true });
1093
- writeFileSync(out, json);
1094
- git(["add", out]);
1419
+ const dir = join6(repoRoot || ".", ".receipts");
1420
+ const out = opts.out ?? join6(dir, `${safe}.json`);
1421
+ mkdirSync2(dir, { recursive: true });
1422
+ writeFileSync3(out, json);
1423
+ git2(["add", out]);
1095
1424
  const rel = repoRoot ? relative(repoRoot, out) : out;
1096
1425
  process.stderr.write(
1097
1426
  `receipts pr: wrote ${rel} (Grade ${receipt.predicate.grade}, ${scopeNote}) from "${summary.title ?? "untitled"}".
@@ -1118,8 +1447,8 @@ async function runGuardrails(opts) {
1118
1447
  const rules = collectGuardrails(findingSets);
1119
1448
  const block = renderGuardrailsBlock(rules, opts.json ? "json" : "md");
1120
1449
  if (opts.out) {
1121
- const existing = existsSync(opts.out) ? readFileSync4(opts.out, "utf8") : "";
1122
- writeFileSync(opts.out, upsertGuardrailsSection(existing, block));
1450
+ const existing = existsSync4(opts.out) ? readFileSync6(opts.out, "utf8") : "";
1451
+ writeFileSync3(opts.out, upsertGuardrailsSection(existing, block));
1123
1452
  process.stderr.write(
1124
1453
  `guardrails: wrote ${rules.length} rule(s) to ${opts.out} (from ${findingSets.length} session${findingSets.length === 1 ? "" : "s"}).
1125
1454
  `
@@ -1151,8 +1480,8 @@ async function runTrends(opts) {
1151
1480
  const trends = computeTrends(inputs, requested);
1152
1481
  if (opts.out) {
1153
1482
  const block = renderTrends(trends, "md");
1154
- const existing = existsSync(opts.out) ? readFileSync4(opts.out, "utf8") : "";
1155
- writeFileSync(opts.out, upsertTrendsSection(existing, block));
1483
+ const existing = existsSync4(opts.out) ? readFileSync6(opts.out, "utf8") : "";
1484
+ writeFileSync3(opts.out, upsertTrendsSection(existing, block));
1156
1485
  process.stderr.write(
1157
1486
  `trends: wrote section to ${opts.out} (from ${trends.window.used} sessions).
1158
1487
  `
@@ -1178,7 +1507,7 @@ function runEnvelope(file) {
1178
1507
  return 1;
1179
1508
  }
1180
1509
  try {
1181
- const receipt = JSON.parse(readFileSync4(file, "utf8"));
1510
+ const receipt = JSON.parse(readFileSync6(file, "utf8"));
1182
1511
  process.stdout.write(`${JSON.stringify(toDsseEnvelope(receipt), null, 2)}
1183
1512
  `);
1184
1513
  return 0;
@@ -1195,7 +1524,7 @@ async function runVerify(file, opts = {}) {
1195
1524
  }
1196
1525
  let input;
1197
1526
  try {
1198
- input = JSON.parse(readFileSync4(file, "utf8"));
1527
+ input = JSON.parse(readFileSync6(file, "utf8"));
1199
1528
  } catch (err) {
1200
1529
  process.stderr.write(`Could not read ${file}: ${err instanceof Error ? err.message : err}
1201
1530
  `);
@@ -1249,8 +1578,8 @@ function runDiff(fileA, fileB, opts = {}) {
1249
1578
  );
1250
1579
  return 1;
1251
1580
  }
1252
- pathA = join4(".receipts", `${hist[1].name}.json`);
1253
- pathB = join4(".receipts", `${hist[0].name}.json`);
1581
+ pathA = join6(".receipts", `${hist[1].name}.json`);
1582
+ pathB = join6(".receipts", `${hist[0].name}.json`);
1254
1583
  process.stdout.write(`receipts diff: ${hist[1].name} \u2192 ${hist[0].name} (most recent two)
1255
1584
 
1256
1585
  `);
@@ -1264,7 +1593,7 @@ function runDiff(fileA, fileB, opts = {}) {
1264
1593
  const read = (f) => {
1265
1594
  let input;
1266
1595
  try {
1267
- input = JSON.parse(readFileSync4(f, "utf8"));
1596
+ input = JSON.parse(readFileSync6(f, "utf8"));
1268
1597
  } catch (err) {
1269
1598
  process.stderr.write(`Could not read ${f}: ${err instanceof Error ? err.message : err}
1270
1599
  `);
@@ -1287,7 +1616,7 @@ function runDiff(fileA, fileB, opts = {}) {
1287
1616
  const output = opts.json ? `${JSON.stringify(delta, null, 2)}
1288
1617
  ` : renderDiff(delta);
1289
1618
  if (opts.out) {
1290
- writeFileSync(opts.out, output);
1619
+ writeFileSync3(opts.out, output);
1291
1620
  process.stdout.write(`receipts diff: wrote ${opts.out}
1292
1621
  `);
1293
1622
  } else {
@@ -1301,7 +1630,7 @@ function runLog(dir, opts = {}) {
1301
1630
  const output = opts.json ? `${JSON.stringify(shown, null, 2)}
1302
1631
  ` : renderLog(shown, all.length);
1303
1632
  if (opts.out) {
1304
- writeFileSync(opts.out, output);
1633
+ writeFileSync3(opts.out, output);
1305
1634
  process.stdout.write(`receipts log: wrote ${opts.out}
1306
1635
  `);
1307
1636
  } else {
@@ -1314,7 +1643,7 @@ function runStats(dir, opts = {}) {
1314
1643
  const output = opts.json ? `${JSON.stringify(stats, null, 2)}
1315
1644
  ` : renderStats(stats);
1316
1645
  if (opts.out) {
1317
- writeFileSync(opts.out, output);
1646
+ writeFileSync3(opts.out, output);
1318
1647
  process.stdout.write(`receipts stats: wrote ${opts.out}
1319
1648
  `);
1320
1649
  } else {
@@ -1323,10 +1652,10 @@ function runStats(dir, opts = {}) {
1323
1652
  return 0;
1324
1653
  }
1325
1654
  function runBadge(file, opts = {}) {
1326
- const repoRoot = git(["rev-parse", "--show-toplevel"]) || ".";
1327
- const branch = git(["rev-parse", "--abbrev-ref", "HEAD"]);
1328
- const path = file ?? (branch && branch !== "HEAD" ? join4(repoRoot, ".receipts", `${branch.replace(/[/\\]/g, "-")}.json`) : void 0);
1329
- if (!path || !existsSync(path)) {
1655
+ const repoRoot = git2(["rev-parse", "--show-toplevel"]) || ".";
1656
+ const branch = git2(["rev-parse", "--abbrev-ref", "HEAD"]);
1657
+ const path = file ?? (branch && branch !== "HEAD" ? join6(repoRoot, ".receipts", `${branch.replace(/[/\\]/g, "-")}.json`) : void 0);
1658
+ if (!path || !existsSync4(path)) {
1330
1659
  process.stderr.write(
1331
1660
  `receipts badge: no receipt found${path ? ` at ${path}` : ""}. Run \`receipts pr\` first, or pass a receipt path.
1332
1661
  `
@@ -1335,7 +1664,7 @@ function runBadge(file, opts = {}) {
1335
1664
  }
1336
1665
  let input;
1337
1666
  try {
1338
- input = JSON.parse(readFileSync4(path, "utf8"));
1667
+ input = JSON.parse(readFileSync6(path, "utf8"));
1339
1668
  } catch {
1340
1669
  process.stderr.write(`receipts badge: could not parse ${path}
1341
1670
  `);
@@ -1350,7 +1679,7 @@ function runBadge(file, opts = {}) {
1350
1679
  const output = `${JSON.stringify(badgeEndpoint(res.receipt.predicate), null, 2)}
1351
1680
  `;
1352
1681
  if (opts.out) {
1353
- writeFileSync(opts.out, output);
1682
+ writeFileSync3(opts.out, output);
1354
1683
  process.stdout.write(`receipts badge: wrote ${opts.out}
1355
1684
  `);
1356
1685
  } else {
@@ -1359,10 +1688,10 @@ function runBadge(file, opts = {}) {
1359
1688
  return 0;
1360
1689
  }
1361
1690
  function runSarif(file, opts = {}) {
1362
- const repoRoot = git(["rev-parse", "--show-toplevel"]) || ".";
1363
- const branch = git(["rev-parse", "--abbrev-ref", "HEAD"]);
1364
- const path = file ?? (branch && branch !== "HEAD" ? join4(repoRoot, ".receipts", `${branch.replace(/[/\\]/g, "-")}.json`) : void 0);
1365
- if (!path || !existsSync(path)) {
1691
+ const repoRoot = git2(["rev-parse", "--show-toplevel"]) || ".";
1692
+ const branch = git2(["rev-parse", "--abbrev-ref", "HEAD"]);
1693
+ const path = file ?? (branch && branch !== "HEAD" ? join6(repoRoot, ".receipts", `${branch.replace(/[/\\]/g, "-")}.json`) : void 0);
1694
+ if (!path || !existsSync4(path)) {
1366
1695
  process.stderr.write(
1367
1696
  `receipts sarif: no receipt found${path ? ` at ${path}` : ""}. Run \`receipts pr\` first, or pass a receipt path.
1368
1697
  `
@@ -1371,7 +1700,7 @@ function runSarif(file, opts = {}) {
1371
1700
  }
1372
1701
  let input;
1373
1702
  try {
1374
- input = JSON.parse(readFileSync4(path, "utf8"));
1703
+ input = JSON.parse(readFileSync6(path, "utf8"));
1375
1704
  } catch {
1376
1705
  process.stderr.write(`receipts sarif: could not parse ${path}
1377
1706
  `);
@@ -1386,7 +1715,7 @@ function runSarif(file, opts = {}) {
1386
1715
  const output = `${JSON.stringify(toSarif(res.receipt), null, 2)}
1387
1716
  `;
1388
1717
  if (opts.out) {
1389
- writeFileSync(opts.out, output);
1718
+ writeFileSync3(opts.out, output);
1390
1719
  process.stdout.write(`receipts sarif: wrote ${opts.out}
1391
1720
  `);
1392
1721
  } else {
@@ -1394,6 +1723,59 @@ function runSarif(file, opts = {}) {
1394
1723
  }
1395
1724
  return 0;
1396
1725
  }
1726
+ function runPrune(dir, opts = {}) {
1727
+ const repoRoot = git2(["rev-parse", "--show-toplevel"]) || ".";
1728
+ const dpath = dir ?? join6(repoRoot, ".receipts");
1729
+ let files;
1730
+ try {
1731
+ files = readdirSync3(dpath).filter((f) => f.endsWith(".json"));
1732
+ } catch {
1733
+ process.stderr.write(`receipts prune: no ${dpath} directory.
1734
+ `);
1735
+ return 0;
1736
+ }
1737
+ const ls = git2(["ls-remote", "--heads", "origin"]);
1738
+ if (ls === null) {
1739
+ process.stderr.write(
1740
+ "receipts prune: could not list remote branches (offline / no 'origin'?) \u2014 aborting, nothing removed.\n"
1741
+ );
1742
+ return 1;
1743
+ }
1744
+ const liveSlugs = new Set(
1745
+ ls.split("\n").map((l) => l.split("refs/heads/")[1]).filter(Boolean).map((b) => branchSlug(b.trim()))
1746
+ );
1747
+ const { keep, remove } = planPrune(files, liveSlugs);
1748
+ if (remove.length === 0) {
1749
+ process.stdout.write(`receipts prune: nothing to prune (${keep.length} receipt(s) kept).
1750
+ `);
1751
+ return 0;
1752
+ }
1753
+ if (opts.dryRun) {
1754
+ process.stdout.write(
1755
+ `receipts prune (dry-run): would remove ${remove.length}/${files.length} receipt(s), keep ${keep.length}:
1756
+ `
1757
+ );
1758
+ for (const f of remove) {
1759
+ process.stdout.write(` - ${f}
1760
+ `);
1761
+ }
1762
+ return 0;
1763
+ }
1764
+ for (const f of remove) {
1765
+ const p = join6(dpath, f);
1766
+ if (git2(["rm", "-f", "--", p]) === null) {
1767
+ try {
1768
+ unlinkSync(p);
1769
+ } catch {
1770
+ }
1771
+ }
1772
+ }
1773
+ process.stdout.write(
1774
+ `receipts prune: removed ${remove.length} receipt(s) for merged/deleted branches; kept ${keep.length}.
1775
+ `
1776
+ );
1777
+ return 0;
1778
+ }
1397
1779
  async function runEval(opts = {}) {
1398
1780
  const limit = opts.limit && opts.limit > 0 ? opts.limit : 200;
1399
1781
  const summaries = (await listSessions(opts.agent)).slice(0, limit);
@@ -1414,17 +1796,53 @@ async function runEval(opts = {}) {
1414
1796
  ` : renderFieldScan(scan));
1415
1797
  return 0;
1416
1798
  }
1417
- function runInit() {
1799
+ function runInit(opts = {}) {
1800
+ const lines = [];
1418
1801
  const v = getVersion();
1419
- const tag = /^\d+\.\d+\.\d+$/.test(v) ? `v${v}` : "v0.2.2";
1802
+ const major = /^(\d+)\.\d+\.\d+/.exec(v)?.[1];
1803
+ const tag = major ? `v${major}` : "v0";
1420
1804
  const dir = ".github/workflows";
1421
1805
  const path = `${dir}/receipts.yml`;
1422
- if (existsSync(path)) {
1423
- process.stdout.write(`receipts init: ${path} already exists \u2014 leaving it untouched.
1424
- `);
1425
- return 0;
1806
+ if (existsSync4(path)) {
1807
+ lines.push(`receipts init: ${path} already exists \u2014 leaving it untouched.`);
1808
+ } else {
1809
+ mkdirSync2(dir, { recursive: true });
1810
+ writeFileSync3(path, workflowContent(tag, v));
1811
+ lines.push(`receipts init: wrote ${path} (tracking ${tag}, quiet + non-blocking).`);
1812
+ }
1813
+ const settingsPath = join6(".claude", "settings.json");
1814
+ const merged = mergeHookIntoSettings(settingsPath);
1815
+ if (!merged.ok) {
1816
+ lines.push(`receipts init: ${merged.reason}.`);
1817
+ } else if (merged.changed) {
1818
+ lines.push(`receipts init: added the receipts pre-push hook to ${settingsPath}.`);
1819
+ } else {
1820
+ lines.push(`receipts init: ${settingsPath} already has the receipts hook.`);
1821
+ }
1822
+ const ignorePath = join6(".claude", ".gitignore");
1823
+ if (!existsSync4(ignorePath)) {
1824
+ writeFileSync3(ignorePath, "*\n!settings.json\n!.gitignore\n");
1825
+ lines.push(`receipts init: wrote ${ignorePath} (commit settings.json, ignore the rest).`);
1826
+ }
1827
+ if (opts.prepare) {
1828
+ const wired = wirePrepareScript("package.json");
1829
+ if (!wired.ok) {
1830
+ lines.push(`receipts init: ${wired.reason}.`);
1831
+ } else if (wired.changed) {
1832
+ lines.push(
1833
+ "receipts init: wired `prepare` in package.json \u2014 `npm install` now self-installs the git hook."
1834
+ );
1835
+ } else {
1836
+ lines.push("receipts init: package.json `prepare` already wired.");
1837
+ }
1426
1838
  }
1427
- const content = `name: Verified by Receipts
1839
+ lines.push(" Commit these, open a PR, and receipts attach themselves from then on.");
1840
+ process.stdout.write(`${lines.join("\n")}
1841
+ `);
1842
+ return 0;
1843
+ }
1844
+ function workflowContent(tag, v) {
1845
+ return `name: Verified by Receipts
1428
1846
 
1429
1847
  # Deterministic "what did the coding agent actually do?" check on PRs. Quiet + non-blocking
1430
1848
  # pilot: acts only when a branch commits an agent Receipt (.receipts/<branch>.json);
@@ -1443,21 +1861,56 @@ permissions:
1443
1861
 
1444
1862
  jobs:
1445
1863
  receipts:
1864
+ # Tracks the ${tag} major tag (auto-gets minor/patch features). Pin a full version
1865
+ # (e.g. @v${v}) or a commit SHA for immutability.
1446
1866
  uses: AltimateAI/altimate-receipts/.github/workflows/receipts.reusable.yml@${tag}
1447
1867
  with:
1448
1868
  require-receipt: false # never fail a PR that has no receipt (soft pilot)
1449
1869
  notify-when-missing: false # stay silent unless a receipt is present
1450
1870
  # block-on: "" # informational check; never blocks a merge
1451
1871
  `;
1452
- mkdirSync(dir, { recursive: true });
1453
- writeFileSync(path, content);
1872
+ }
1873
+ async function runHook(kind, dialect) {
1874
+ if (kind !== "pre-push") {
1875
+ process.stderr.write("Usage: receipts hook pre-push [--agent claude|git]\n");
1876
+ return 1;
1877
+ }
1878
+ const stdin = await readStdin();
1879
+ const res = await runHookPrePush(dialect, stdin, async () => runPr({}));
1880
+ if (res.message) {
1881
+ process.stderr.write(`${res.message}
1882
+ `);
1883
+ }
1884
+ return res.exit;
1885
+ }
1886
+ function runInstallHook() {
1887
+ const res = installGitHook();
1888
+ if (!res.ok) {
1889
+ process.stderr.write(`receipts install-hook: ${res.reason}.
1890
+ `);
1891
+ return 1;
1892
+ }
1893
+ const note = {
1894
+ installed: "installed .git/hooks/pre-push",
1895
+ unchanged: ".git/hooks/pre-push already installed",
1896
+ chained: "installed .git/hooks/pre-push (existing hook preserved as pre-push.local, runs first)"
1897
+ }[res.action];
1898
+ process.stdout.write(`receipts install-hook: ${note}.
1899
+ `);
1900
+ return 0;
1901
+ }
1902
+ function runSetupLocal() {
1903
+ const path = join6(homedir(), ".claude", "settings.json");
1904
+ const res = mergeHookIntoSettings(path);
1905
+ if (!res.ok) {
1906
+ process.stderr.write(`receipts setup-local: ${res.reason}.
1907
+ `);
1908
+ return 1;
1909
+ }
1454
1910
  process.stdout.write(
1455
- [
1456
- `receipts init: wrote ${path} (pinned to ${tag}, quiet + non-blocking).`,
1457
- " Commit it, open a PR, and the Receipts check runs on every PR.",
1458
- " Generate receipts locally with the pre-push hook \u2014 see docs/onboarding-internal.md.",
1459
- ""
1460
- ].join("\n")
1911
+ res.changed ? `receipts setup-local: added the receipts hook to ${path} \u2014 active only in repos with the Receipts workflow.
1912
+ ` : `receipts setup-local: ${path} already has the receipts hook.
1913
+ `
1461
1914
  );
1462
1915
  return 0;
1463
1916
  }
@@ -1478,7 +1931,7 @@ async function runRederive(file, opts = {}) {
1478
1931
  return 0;
1479
1932
  }
1480
1933
  async function runAssert(opts) {
1481
- const repoRoot = git(["rev-parse", "--show-toplevel"]) || ".";
1934
+ const repoRoot = git2(["rev-parse", "--show-toplevel"]) || ".";
1482
1935
  let asserts;
1483
1936
  try {
1484
1937
  asserts = loadAsserts(repoRoot);