altimate-receipts 0.5.2 → 0.6.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
@@ -13,13 +13,13 @@ import {
13
13
  renderShareMarkdown,
14
14
  sliceByBranch,
15
15
  toDsseEnvelope
16
- } from "./chunk-GOLRNSZT.js";
16
+ } from "./chunk-ASPLZPMQ.js";
17
17
  import {
18
18
  computeTrends,
19
19
  deriveTargets,
20
20
  renderTrends,
21
21
  upsertTrendsSection
22
- } from "./chunk-EKMFU3ES.js";
22
+ } from "./chunk-543NGQTN.js";
23
23
  import {
24
24
  agentIds,
25
25
  anyDetected,
@@ -43,10 +43,10 @@ import {
43
43
  selectSummary,
44
44
  upsertGuardrailsSection,
45
45
  verifyBundle
46
- } from "./chunk-EYM5WETZ.js";
46
+ } from "./chunk-DBQWQVZZ.js";
47
47
 
48
48
  // src/cli.ts
49
- import { spawnSync as spawnSync4 } from "child_process";
49
+ import { spawnSync as spawnSync5 } from "child_process";
50
50
  import {
51
51
  existsSync as existsSync4,
52
52
  mkdirSync as mkdirSync3,
@@ -57,7 +57,7 @@ import {
57
57
  writeFileSync as writeFileSync3
58
58
  } from "fs";
59
59
  import { homedir } from "os";
60
- import { join as join6, relative } from "path";
60
+ import { join as join7, relative } from "path";
61
61
  import { pathToFileURL } from "url";
62
62
 
63
63
  // src/hook/installGitHook.ts
@@ -174,9 +174,64 @@ function wirePrepareScript(pkgPath) {
174
174
  }
175
175
 
176
176
  // src/hook/prePush.ts
177
+ import { spawnSync as spawnSync3 } from "child_process";
178
+ import { existsSync as existsSync3 } from "fs";
179
+ import { join as join3 } from "path";
180
+
181
+ // src/report/store.ts
177
182
  import { spawnSync as spawnSync2 } from "child_process";
178
- import { existsSync as existsSync2 } from "fs";
179
- import { join as join2 } from "path";
183
+ var IDENT = ["-c", "user.name=receipts", "-c", "user.email=receipts@altimate.ai"];
184
+ var RECEIPT_REF_PREFIX = "refs/receipts/";
185
+ var receiptRef = (slug) => `${RECEIPT_REF_PREFIX}${slug}`;
186
+ function git2(args, opts = {}) {
187
+ const r = spawnSync2("git", args, {
188
+ encoding: "utf8",
189
+ cwd: opts.cwd,
190
+ input: opts.input,
191
+ env: opts.env ? { ...process.env, ...opts.env } : process.env
192
+ });
193
+ return { out: (r.stdout ?? "").trim(), ok: r.status === 0, err: (r.stderr ?? "").trim() };
194
+ }
195
+ function writeReceiptRef(slug, branch, json, endedAtMs, cwd) {
196
+ const blob = git2(["hash-object", "-w", "--stdin"], { cwd, input: json });
197
+ if (!blob.ok) {
198
+ return { ok: false, reason: "hash-object failed (not a git repository?)" };
199
+ }
200
+ const tree = git2(["mktree"], { cwd, input: `100644 blob ${blob.out} receipt.json
201
+ ` });
202
+ if (!tree.ok) {
203
+ return { ok: false, reason: "mktree failed" };
204
+ }
205
+ const when = `@${Math.max(1, Math.floor((endedAtMs ?? 0) / 1e3))} +0000`;
206
+ const commit = git2([...IDENT, "commit-tree", tree.out, "-m", `receipt: ${branch}`], {
207
+ cwd,
208
+ env: { GIT_AUTHOR_DATE: when, GIT_COMMITTER_DATE: when }
209
+ });
210
+ if (!commit.ok) {
211
+ return { ok: false, reason: `commit-tree failed: ${commit.err}` };
212
+ }
213
+ const ref = receiptRef(slug);
214
+ const upd = git2(["update-ref", ref, commit.out], { cwd });
215
+ if (!upd.ok) {
216
+ return { ok: false, reason: `update-ref ${ref} failed` };
217
+ }
218
+ return { ok: true, ref, commit: commit.out };
219
+ }
220
+ function readReceiptRef(slug, cwd) {
221
+ const r = git2(["cat-file", "blob", `${receiptRef(slug)}:receipt.json`], { cwd });
222
+ return r.ok ? r.out : null;
223
+ }
224
+ function listReceiptRefs(cwd) {
225
+ const r = git2(["for-each-ref", RECEIPT_REF_PREFIX, "--format=%(refname)"], { cwd });
226
+ if (!r.ok || !r.out) {
227
+ return [];
228
+ }
229
+ return r.out.split("\n").filter(Boolean).map((ref) => ({ ref, slug: ref.slice(RECEIPT_REF_PREFIX.length) }));
230
+ }
231
+ function pushReceiptRef(slug, remote = "origin", cwd) {
232
+ const ref = receiptRef(slug);
233
+ return git2(["push", remote, `+${ref}:${ref}`], { cwd }).ok;
234
+ }
180
235
 
181
236
  // src/trace/gitCommand.ts
182
237
  var WRAPPERS = /* @__PURE__ */ new Set(["command", "exec", "nohup", "time", "env"]);
@@ -209,11 +264,63 @@ function gitInvocations(command) {
209
264
  return out;
210
265
  }
211
266
 
267
+ // src/hook/settingsMerge.ts
268
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
269
+ import { dirname, join as join2 } from "path";
270
+ var HOOK_COMMAND = "npx -y altimate-receipts@latest hook pre-push";
271
+ function settingsStore(root) {
272
+ try {
273
+ const s = JSON.parse(readFileSync2(join2(root, ".claude", "settings.json"), "utf8"));
274
+ const v = s?.env?.RECEIPTS_STORE;
275
+ return typeof v === "string" && v ? v : void 0;
276
+ } catch {
277
+ return void 0;
278
+ }
279
+ }
280
+ function mergeHookIntoSettings(path, command = HOOK_COMMAND) {
281
+ let settings = {};
282
+ if (existsSync2(path)) {
283
+ let parsed;
284
+ try {
285
+ parsed = JSON.parse(readFileSync2(path, "utf8"));
286
+ } catch {
287
+ return { ok: false, reason: `${path} is not valid JSON \u2014 leaving it untouched` };
288
+ }
289
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
290
+ return { ok: false, reason: `${path} is not a JSON object \u2014 leaving it untouched` };
291
+ }
292
+ settings = parsed;
293
+ }
294
+ if (settings.hooks === void 0) {
295
+ settings.hooks = {};
296
+ }
297
+ const hooks = settings.hooks;
298
+ if (typeof hooks !== "object" || hooks === null || Array.isArray(hooks)) {
299
+ return { ok: false, reason: `${path} has a non-object "hooks" key \u2014 leaving it untouched` };
300
+ }
301
+ if (hooks.PreToolUse === void 0) {
302
+ hooks.PreToolUse = [];
303
+ }
304
+ const pre = hooks.PreToolUse;
305
+ if (!Array.isArray(pre)) {
306
+ return { ok: false, reason: `${path} has a non-array hooks.PreToolUse \u2014 leaving it untouched` };
307
+ }
308
+ const present = pre.some((e) => e?.hooks?.some((h) => h?.command === command));
309
+ if (present) {
310
+ return { ok: true, changed: false };
311
+ }
312
+ pre.push({ matcher: "Bash", hooks: [{ type: "command", command }] });
313
+ mkdirSync2(dirname(path), { recursive: true });
314
+ writeFileSync2(path, `${JSON.stringify(settings, null, 2)}
315
+ `);
316
+ return { ok: true, changed: true };
317
+ }
318
+
212
319
  // src/hook/prePush.ts
213
320
  var REPUSH_EXIT = 42;
214
321
  var ATTACH_SUBJECT = (branch) => `chore(receipts): attach agent receipt for ${branch}`;
215
- function git2(args, cwd) {
216
- const r = spawnSync2("git", args, { encoding: "utf8", cwd });
322
+ function git3(args, cwd) {
323
+ const r = spawnSync3("git", args, { encoding: "utf8", cwd });
217
324
  return r.status === 0 ? r.stdout.trim() : "";
218
325
  }
219
326
  var TAG_REFSPEC = /^(refs\/tags\/|v\d+(\.\d+)*$)/;
@@ -236,22 +343,33 @@ function isGitPush(command) {
236
343
  function gitStdinPushesBranch(stdin) {
237
344
  return stdin.split("\n").some((line) => line.trim().startsWith("refs/heads/"));
238
345
  }
239
- var hasMarker = (root) => existsSync2(join2(root, ".github", "workflows", "receipts.yml")) || existsSync2(join2(root, ".receipts"));
346
+ var hasMarker = (root) => existsSync3(join3(root, ".github", "workflows", "receipts.yml")) || existsSync3(join3(root, ".receipts"));
240
347
  function repoOptedIn(repoRoot) {
241
348
  if (hasMarker(repoRoot)) {
242
349
  return true;
243
350
  }
244
- const common = git2(["rev-parse", "--git-common-dir"], repoRoot);
351
+ const primary = primaryCheckout(repoRoot);
352
+ return primary ? hasMarker(primary) : false;
353
+ }
354
+ function primaryCheckout(repoRoot) {
355
+ const common = git3(["rev-parse", "--git-common-dir"], repoRoot);
245
356
  if (common.endsWith("/.git")) {
246
357
  const primary = common.slice(0, -"/.git".length);
247
358
  if (primary && primary !== repoRoot) {
248
- return hasMarker(primary);
359
+ return primary;
249
360
  }
250
361
  }
251
- return false;
362
+ return null;
252
363
  }
253
364
  function headIsAttachCommit(branch, cwd) {
254
- return git2(["log", "-1", "--format=%s"], cwd) === ATTACH_SUBJECT(branch);
365
+ return git3(["log", "-1", "--format=%s"], cwd) === ATTACH_SUBJECT(branch);
366
+ }
367
+ function isDefaultBranch(branch, cwd) {
368
+ const head = git3(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], cwd);
369
+ if (head) {
370
+ return head === `origin/${branch}`;
371
+ }
372
+ return branch === "main" || branch === "master";
255
373
  }
256
374
  async function readStdin(stream = process.stdin) {
257
375
  let data = "";
@@ -278,7 +396,7 @@ async function runHookPrePush(dialect, stdin, generate) {
278
396
  if (payload.tool_name !== "Bash" || !payload.tool_input?.command) {
279
397
  return { exit: 0 };
280
398
  }
281
- if (payload.cwd && existsSync2(payload.cwd)) {
399
+ if (payload.cwd && existsSync3(payload.cwd)) {
282
400
  process.chdir(payload.cwd);
283
401
  }
284
402
  if (!isGitPush(payload.tool_input.command)) {
@@ -287,15 +405,20 @@ async function runHookPrePush(dialect, stdin, generate) {
287
405
  } else if (!gitStdinPushesBranch(stdin)) {
288
406
  return { exit: 0 };
289
407
  }
290
- const repoRoot = git2(["rev-parse", "--show-toplevel"]);
408
+ const repoRoot = git3(["rev-parse", "--show-toplevel"]);
291
409
  if (!repoRoot || !repoOptedIn(repoRoot)) {
292
410
  return { exit: 0 };
293
411
  }
294
- const branch = git2(["rev-parse", "--abbrev-ref", "HEAD"]);
295
- if (!branch || branch === "HEAD") {
412
+ const branch = git3(["rev-parse", "--abbrev-ref", "HEAD"]);
413
+ if (!branch || branch === "HEAD" || isDefaultBranch(branch)) {
296
414
  return { exit: 0 };
297
415
  }
298
- if (dialect === "git" && headIsAttachCommit(branch)) {
416
+ const store = (process.env.RECEIPTS_STORE || settingsStore(repoRoot) || settingsStore(primaryCheckout(repoRoot) ?? "") || "commit").toLowerCase();
417
+ if (store === "none") {
418
+ return { exit: 0 };
419
+ }
420
+ process.env.RECEIPTS_STORE = store;
421
+ if (store !== "ref" && dialect === "git" && headIsAttachCommit(branch)) {
299
422
  return { exit: 0 };
300
423
  }
301
424
  const write = process.stderr.write.bind(process.stderr);
@@ -309,11 +432,22 @@ async function runHookPrePush(dialect, stdin, generate) {
309
432
  if (generated !== 0) {
310
433
  return { exit: 0 };
311
434
  }
312
- if (!git2(["status", "--porcelain", ".receipts/"])) {
435
+ if (store === "ref") {
436
+ const slug = branch.replace(/[/\\]/g, "-");
437
+ if (readReceiptRef(slug) == null) {
438
+ return { exit: 0 };
439
+ }
440
+ const pushed = pushReceiptRef(slug);
441
+ return {
442
+ exit: 0,
443
+ message: pushed ? `receipts: receipt at refs/receipts/${slug} \u2014 travels alongside this push.` : `receipts: wrote refs/receipts/${slug}; pushing it failed \u2014 push the ref manually.`
444
+ };
445
+ }
446
+ if (!git3(["status", "--porcelain", ".receipts/"])) {
313
447
  return { exit: 0 };
314
448
  }
315
- spawnSync2("git", ["add", ".receipts/"], { encoding: "utf8" });
316
- const commit = spawnSync2(
449
+ spawnSync3("git", ["add", ".receipts/"], { encoding: "utf8" });
450
+ const commit = spawnSync3(
317
451
  "git",
318
452
  ["commit", "--no-verify", "-m", ATTACH_SUBJECT(branch), "--", ".receipts/"],
319
453
  { encoding: "utf8" }
@@ -336,52 +470,9 @@ async function runHookPrePush(dialect, stdin, generate) {
336
470
  }
337
471
  }
338
472
 
339
- // src/hook/settingsMerge.ts
340
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
341
- import { dirname } from "path";
342
- var HOOK_COMMAND = "npx -y altimate-receipts@latest hook pre-push";
343
- function mergeHookIntoSettings(path, command = HOOK_COMMAND) {
344
- let settings = {};
345
- if (existsSync3(path)) {
346
- let parsed;
347
- try {
348
- parsed = JSON.parse(readFileSync2(path, "utf8"));
349
- } catch {
350
- return { ok: false, reason: `${path} is not valid JSON \u2014 leaving it untouched` };
351
- }
352
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
353
- return { ok: false, reason: `${path} is not a JSON object \u2014 leaving it untouched` };
354
- }
355
- settings = parsed;
356
- }
357
- if (settings.hooks === void 0) {
358
- settings.hooks = {};
359
- }
360
- const hooks = settings.hooks;
361
- if (typeof hooks !== "object" || hooks === null || Array.isArray(hooks)) {
362
- return { ok: false, reason: `${path} has a non-object "hooks" key \u2014 leaving it untouched` };
363
- }
364
- if (hooks.PreToolUse === void 0) {
365
- hooks.PreToolUse = [];
366
- }
367
- const pre = hooks.PreToolUse;
368
- if (!Array.isArray(pre)) {
369
- return { ok: false, reason: `${path} has a non-array hooks.PreToolUse \u2014 leaving it untouched` };
370
- }
371
- const present = pre.some((e) => e?.hooks?.some((h) => h?.command === command));
372
- if (present) {
373
- return { ok: true, changed: false };
374
- }
375
- pre.push({ matcher: "Bash", hooks: [{ type: "command", command }] });
376
- mkdirSync2(dirname(path), { recursive: true });
377
- writeFileSync2(path, `${JSON.stringify(settings, null, 2)}
378
- `);
379
- return { ok: true, changed: true };
380
- }
381
-
382
473
  // src/receipt/assert.ts
383
474
  import { readFileSync as readFileSync3 } from "fs";
384
- import { join as join3 } from "path";
475
+ import { join as join4 } from "path";
385
476
  var OPS = /* @__PURE__ */ new Set([
386
477
  "eq",
387
478
  "ne",
@@ -476,7 +567,7 @@ function validateAssertion(raw) {
476
567
  };
477
568
  }
478
569
  function loadAsserts(repoRoot) {
479
- const path = join3(repoRoot, ".receipts", "asserts.json");
570
+ const path = join4(repoRoot, ".receipts", "asserts.json");
480
571
  let text;
481
572
  try {
482
573
  text = readFileSync3(path, "utf8");
@@ -704,7 +795,7 @@ function renderFieldScan(s) {
704
795
 
705
796
  // src/report/log.ts
706
797
  import { readFileSync as readFileSync4, readdirSync } from "fs";
707
- import { join as join4 } from "path";
798
+ import { dirname as dirname2, join as join5 } from "path";
708
799
  var SEV_ICON2 = { critical: "\u26D4", high: "\u26A0\uFE0F", medium: "\u{1F50D}", low: "\xB7" };
709
800
  var SEV_RANK = { critical: 0, high: 1, medium: 2, low: 3 };
710
801
  var NON_RECEIPT = /(?:^|\/)(?:asserts(?:\.example)?|sample)\.json$/i;
@@ -722,13 +813,27 @@ function loadReceiptHistory(dir) {
722
813
  try {
723
814
  files = readdirSync(dir).filter((f) => f.endsWith(".json") && !NON_RECEIPT.test(f));
724
815
  } catch {
725
- return [];
816
+ files = [];
817
+ }
818
+ const sources = files.map((f) => {
819
+ try {
820
+ return { name: f, raw: readFileSync4(join5(dir, f), "utf8") };
821
+ } catch {
822
+ return { name: f, raw: null };
823
+ }
824
+ });
825
+ const repo = dirname2(dir);
826
+ for (const { slug } of listReceiptRefs(repo)) {
827
+ sources.push({ name: `${slug}@ref`, raw: readReceiptRef(slug, repo) });
726
828
  }
727
829
  const entries = [];
728
- for (const f of files) {
830
+ for (const { name: f, raw } of sources) {
831
+ if (raw == null) {
832
+ continue;
833
+ }
729
834
  let input;
730
835
  try {
731
- input = JSON.parse(readFileSync4(join4(dir, f), "utf8"));
836
+ input = JSON.parse(raw);
732
837
  } catch {
733
838
  continue;
734
839
  }
@@ -892,7 +997,7 @@ function toSarif(receipt) {
892
997
 
893
998
  // src/report/stats.ts
894
999
  import { readFileSync as readFileSync5, readdirSync as readdirSync2 } from "fs";
895
- import { join as join5 } from "path";
1000
+ import { join as join6 } from "path";
896
1001
  var NON_RECEIPT2 = /(?:^|\/)(?:asserts(?:\.example)?|sample)\.json$/i;
897
1002
  function computeStats(dir) {
898
1003
  let files;
@@ -910,7 +1015,7 @@ function computeStats(dir) {
910
1015
  for (const f of files) {
911
1016
  let input;
912
1017
  try {
913
- input = JSON.parse(readFileSync5(join5(dir, f), "utf8"));
1018
+ input = JSON.parse(readFileSync5(join6(dir, f), "utf8"));
914
1019
  } catch {
915
1020
  skipped++;
916
1021
  continue;
@@ -1027,13 +1132,13 @@ function renderHandoffMarkdown(h) {
1027
1132
  }
1028
1133
 
1029
1134
  // src/trace/commitMatch.ts
1030
- import { spawnSync as spawnSync3 } from "child_process";
1135
+ import { spawnSync as spawnSync4 } from "child_process";
1031
1136
  var SHA_CAP = 200;
1032
1137
  function branchShas(base, cwd) {
1033
1138
  if (!base) {
1034
1139
  return [];
1035
1140
  }
1036
- const r = spawnSync3("git", ["log", `--max-count=${SHA_CAP}`, "--format=%H", `${base}..HEAD`], {
1141
+ const r = spawnSync4("git", ["log", `--max-count=${SHA_CAP}`, "--format=%H", `${base}..HEAD`], {
1037
1142
  encoding: "utf8",
1038
1143
  cwd
1039
1144
  });
@@ -1330,6 +1435,7 @@ async function run(argv) {
1330
1435
  }
1331
1436
  if (args.command === "rederive") {
1332
1437
  return runRederive(args.file, {
1438
+ source: args.agent,
1333
1439
  branch: args.branchScope,
1334
1440
  redact: args.redact,
1335
1441
  compact: args.compact
@@ -1436,8 +1542,8 @@ Run a coding-agent session first, then try again.
1436
1542
  process.stdout.write(renderCard({ summary, derived, findings }, { color: args.color }));
1437
1543
  return 0;
1438
1544
  }
1439
- function git3(args) {
1440
- const r = spawnSync4("git", args, { encoding: "utf8" });
1545
+ function git4(args) {
1546
+ const r = spawnSync5("git", args, { encoding: "utf8" });
1441
1547
  return r.status === 0 ? r.stdout.trim() : "";
1442
1548
  }
1443
1549
  var PR_SELECT_SCAN = 150;
@@ -1448,7 +1554,7 @@ function branchBirthMs(base) {
1448
1554
  if (!base) {
1449
1555
  return null;
1450
1556
  }
1451
- const out = git3(["log", "--reverse", "--format=%at", `${base}..HEAD`]);
1557
+ const out = git4(["log", "--reverse", "--format=%at", `${base}..HEAD`]);
1452
1558
  const first = Number(out.split("\n")[0]?.trim());
1453
1559
  return Number.isFinite(first) && first > 0 ? first * 1e3 : null;
1454
1560
  }
@@ -1493,8 +1599,8 @@ async function pickForDiff(all, branch, repoRoot, files, birthMs = null, shas =
1493
1599
  return best ?? primary;
1494
1600
  }
1495
1601
  async function runPr(opts) {
1496
- const branch = opts.branch || git3(["rev-parse", "--abbrev-ref", "HEAD"]);
1497
- const repoRoot = git3(["rev-parse", "--show-toplevel"]);
1602
+ const branch = opts.branch || git4(["rev-parse", "--abbrev-ref", "HEAD"]);
1603
+ const repoRoot = git4(["rev-parse", "--show-toplevel"]);
1498
1604
  if (!branch || branch === "HEAD") {
1499
1605
  process.stderr.write("receipts pr: not on a git branch (use --branch <name>).\n");
1500
1606
  return 1;
@@ -1553,7 +1659,7 @@ Build the branch with a coding agent first, or run \`receipts --list\`.
1553
1659
  const receipt = redactReceipt(await buildReceipt(scopedSession, derived, findings, { scope }));
1554
1660
  const json = `${JSON.stringify(receipt, null, 2)}
1555
1661
  `;
1556
- const store = (opts.store || process.env.RECEIPTS_STORE || "commit").toLowerCase();
1662
+ const store = (opts.store || process.env.RECEIPTS_STORE || (repoRoot ? settingsStore(repoRoot) ?? settingsStore(primaryCheckout(repoRoot) ?? "") : void 0) || "commit").toLowerCase();
1557
1663
  if (store === "none") {
1558
1664
  process.stdout.write(json);
1559
1665
  process.stderr.write(
@@ -1567,21 +1673,33 @@ Build the branch with a coding agent first, or run \`receipts --list\`.
1567
1673
  `receipts pr: store=${store} not yet implemented (SPEC-0064) \u2014 using \`commit\`.
1568
1674
  `
1569
1675
  );
1570
- } else if (store !== "commit") {
1676
+ } else if (store !== "commit" && store !== "ref") {
1571
1677
  process.stderr.write(`receipts pr: unknown store=${store} \u2014 using \`commit\`.
1572
1678
  `);
1573
1679
  }
1574
1680
  const safe = branch.replace(/[/\\]/g, "-");
1575
- const dir = join6(repoRoot || ".", ".receipts");
1576
- const out = opts.out ?? join6(dir, `${safe}.json`);
1681
+ if (store === "ref") {
1682
+ const w = writeReceiptRef(safe, branch, json, summary.endedAt, repoRoot || void 0);
1683
+ if (w.ok) {
1684
+ process.stderr.write(
1685
+ `receipts pr: wrote ${w.ref} (Grade ${receipt.predicate.grade}, ${scopeNote}) from "${summary.title ?? "untitled"}" \u2014 no tree files.
1686
+ `
1687
+ );
1688
+ return 0;
1689
+ }
1690
+ process.stderr.write(`receipts pr: store=ref failed (${w.reason}) \u2014 using \`commit\`.
1691
+ `);
1692
+ }
1693
+ const dir = join7(repoRoot || ".", ".receipts");
1694
+ const out = opts.out ?? join7(dir, `${safe}.json`);
1577
1695
  mkdirSync3(dir, { recursive: true });
1578
- const attrs = join6(dir, ".gitattributes");
1696
+ const attrs = join7(dir, ".gitattributes");
1579
1697
  if (!existsSync4(attrs)) {
1580
1698
  writeFileSync3(attrs, "* linguist-generated\n");
1581
- git3(["add", attrs]);
1699
+ git4(["add", attrs]);
1582
1700
  }
1583
1701
  writeFileSync3(out, json);
1584
- git3(["add", out]);
1702
+ git4(["add", out]);
1585
1703
  const rel = repoRoot ? relative(repoRoot, out) : out;
1586
1704
  process.stderr.write(
1587
1705
  `receipts pr: wrote ${rel} (Grade ${receipt.predicate.grade}, ${scopeNote}) from "${summary.title ?? "untitled"}".
@@ -1739,8 +1857,9 @@ function runDiff(fileA, fileB, opts = {}) {
1739
1857
  );
1740
1858
  return 1;
1741
1859
  }
1742
- pathA = join6(".receipts", `${hist[1].name}.json`);
1743
- pathB = join6(".receipts", `${hist[0].name}.json`);
1860
+ const sourceOf = (name) => name.endsWith("@ref") ? name : join7(".receipts", `${name}.json`);
1861
+ pathA = sourceOf(hist[1].name);
1862
+ pathB = sourceOf(hist[0].name);
1744
1863
  process.stdout.write(`receipts diff: ${hist[1].name} \u2192 ${hist[0].name} (most recent two)
1745
1864
 
1746
1865
  `);
@@ -1754,7 +1873,8 @@ function runDiff(fileA, fileB, opts = {}) {
1754
1873
  const read = (f) => {
1755
1874
  let input;
1756
1875
  try {
1757
- input = JSON.parse(readFileSync6(f, "utf8"));
1876
+ const raw = f.endsWith("@ref") ? readReceiptRef(f.slice(0, -"@ref".length)) : null;
1877
+ input = JSON.parse(raw ?? readFileSync6(f, "utf8"));
1758
1878
  } catch (err) {
1759
1879
  process.stderr.write(`Could not read ${f}: ${err instanceof Error ? err.message : err}
1760
1880
  `);
@@ -1813,9 +1933,9 @@ function runStats(dir, opts = {}) {
1813
1933
  return 0;
1814
1934
  }
1815
1935
  function runBadge(file, opts = {}) {
1816
- const repoRoot = git3(["rev-parse", "--show-toplevel"]) || ".";
1817
- const branch = git3(["rev-parse", "--abbrev-ref", "HEAD"]);
1818
- const path = file ?? (branch && branch !== "HEAD" ? join6(repoRoot, ".receipts", `${branch.replace(/[/\\]/g, "-")}.json`) : void 0);
1936
+ const repoRoot = git4(["rev-parse", "--show-toplevel"]) || ".";
1937
+ const branch = git4(["rev-parse", "--abbrev-ref", "HEAD"]);
1938
+ const path = file ?? (branch && branch !== "HEAD" ? join7(repoRoot, ".receipts", `${branch.replace(/[/\\]/g, "-")}.json`) : void 0);
1819
1939
  if (!path || !existsSync4(path)) {
1820
1940
  process.stderr.write(
1821
1941
  `receipts badge: no receipt found${path ? ` at ${path}` : ""}. Run \`receipts pr\` first, or pass a receipt path.
@@ -1849,9 +1969,9 @@ function runBadge(file, opts = {}) {
1849
1969
  return 0;
1850
1970
  }
1851
1971
  function runSarif(file, opts = {}) {
1852
- const repoRoot = git3(["rev-parse", "--show-toplevel"]) || ".";
1853
- const branch = git3(["rev-parse", "--abbrev-ref", "HEAD"]);
1854
- const path = file ?? (branch && branch !== "HEAD" ? join6(repoRoot, ".receipts", `${branch.replace(/[/\\]/g, "-")}.json`) : void 0);
1972
+ const repoRoot = git4(["rev-parse", "--show-toplevel"]) || ".";
1973
+ const branch = git4(["rev-parse", "--abbrev-ref", "HEAD"]);
1974
+ const path = file ?? (branch && branch !== "HEAD" ? join7(repoRoot, ".receipts", `${branch.replace(/[/\\]/g, "-")}.json`) : void 0);
1855
1975
  if (!path || !existsSync4(path)) {
1856
1976
  process.stderr.write(
1857
1977
  `receipts sarif: no receipt found${path ? ` at ${path}` : ""}. Run \`receipts pr\` first, or pass a receipt path.
@@ -1885,8 +2005,8 @@ function runSarif(file, opts = {}) {
1885
2005
  return 0;
1886
2006
  }
1887
2007
  function runPrune(dir, opts = {}) {
1888
- const repoRoot = git3(["rev-parse", "--show-toplevel"]) || ".";
1889
- const dpath = dir ?? join6(repoRoot, ".receipts");
2008
+ const repoRoot = git4(["rev-parse", "--show-toplevel"]) || ".";
2009
+ const dpath = dir ?? join7(repoRoot, ".receipts");
1890
2010
  let files;
1891
2011
  try {
1892
2012
  files = readdirSync3(dpath).filter((f) => f.endsWith(".json"));
@@ -1895,7 +2015,7 @@ function runPrune(dir, opts = {}) {
1895
2015
  `);
1896
2016
  return 0;
1897
2017
  }
1898
- const ls = git3(["ls-remote", "--heads", "origin"]);
2018
+ const ls = git4(["ls-remote", "--heads", "origin"]);
1899
2019
  if (ls === null) {
1900
2020
  process.stderr.write(
1901
2021
  "receipts prune: could not list remote branches (offline / no 'origin'?) \u2014 aborting, nothing removed.\n"
@@ -1907,8 +2027,11 @@ function runPrune(dir, opts = {}) {
1907
2027
  );
1908
2028
  const { keep, remove } = planPrune(files, liveSlugs);
1909
2029
  if (remove.length === 0) {
1910
- process.stdout.write(`receipts prune: nothing to prune (${keep.length} receipt(s) kept).
2030
+ const prunedRefs = pruneReceiptRefs(liveSlugs, opts.dryRun ?? false);
2031
+ if (prunedRefs === 0) {
2032
+ process.stdout.write(`receipts prune: nothing to prune (${keep.length} receipt(s) kept).
1911
2033
  `);
2034
+ }
1912
2035
  return 0;
1913
2036
  }
1914
2037
  if (opts.dryRun) {
@@ -1920,11 +2043,13 @@ function runPrune(dir, opts = {}) {
1920
2043
  process.stdout.write(` - ${f}
1921
2044
  `);
1922
2045
  }
2046
+ pruneReceiptRefs(liveSlugs, true);
1923
2047
  return 0;
1924
2048
  }
2049
+ pruneReceiptRefs(liveSlugs, false);
1925
2050
  for (const f of remove) {
1926
- const p = join6(dpath, f);
1927
- if (git3(["rm", "-f", "--", p]) === null) {
2051
+ const p = join7(dpath, f);
2052
+ if (git4(["rm", "-f", "--", p]) === null) {
1928
2053
  try {
1929
2054
  unlinkSync(p);
1930
2055
  } catch {
@@ -1937,6 +2062,24 @@ function runPrune(dir, opts = {}) {
1937
2062
  );
1938
2063
  return 0;
1939
2064
  }
2065
+ function pruneReceiptRefs(liveSlugs, dryRun) {
2066
+ const refs = listReceiptRefs().filter(({ slug }) => !liveSlugs.has(slug));
2067
+ if (refs.length === 0) {
2068
+ return 0;
2069
+ }
2070
+ for (const { ref, slug } of refs) {
2071
+ if (dryRun) {
2072
+ process.stdout.write(` - ${ref}
2073
+ `);
2074
+ continue;
2075
+ }
2076
+ git4(["update-ref", "-d", ref]);
2077
+ git4(["push", "origin", `:${ref}`]);
2078
+ process.stdout.write(`receipts prune: removed ${ref} (branch ${slug} gone).
2079
+ `);
2080
+ }
2081
+ return refs.length;
2082
+ }
1940
2083
  async function runEval(opts = {}) {
1941
2084
  const limit = opts.limit && opts.limit > 0 ? opts.limit : 200;
1942
2085
  const summaries = (await listSessions(opts.agent)).slice(0, limit);
@@ -1973,7 +2116,7 @@ function runInit(opts = {}) {
1973
2116
  written.push(path);
1974
2117
  lines.push(`receipts init: wrote ${path} (tracking ${tag}, quiet + non-blocking).`);
1975
2118
  }
1976
- const settingsPath = join6(".claude", "settings.json");
2119
+ const settingsPath = join7(".claude", "settings.json");
1977
2120
  const merged = mergeHookIntoSettings(settingsPath);
1978
2121
  if (!merged.ok) {
1979
2122
  lines.push(`receipts init: ${merged.reason}.`);
@@ -1983,14 +2126,14 @@ function runInit(opts = {}) {
1983
2126
  } else {
1984
2127
  lines.push(`receipts init: ${settingsPath} already has the receipts hook.`);
1985
2128
  }
1986
- const ignorePath = join6(".claude", ".gitignore");
2129
+ const ignorePath = join7(".claude", ".gitignore");
1987
2130
  if (!existsSync4(ignorePath)) {
1988
2131
  writeFileSync3(ignorePath, "*\n!settings.json\n!.gitignore\n");
1989
2132
  written.push(ignorePath);
1990
2133
  lines.push(`receipts init: wrote ${ignorePath} (commit settings.json, ignore the rest).`);
1991
2134
  }
1992
2135
  if (opts.agents?.includes("codex") || existsSync4(".codex")) {
1993
- const codexPath = join6(".codex", "hooks.json");
2136
+ const codexPath = join7(".codex", "hooks.json");
1994
2137
  const m = mergeHookIntoSettings(
1995
2138
  codexPath,
1996
2139
  "npx -y altimate-receipts@latest hook pre-push --agent codex"
@@ -2004,7 +2147,7 @@ function runInit(opts = {}) {
2004
2147
  lines.push(`receipts init: ${codexPath} already has the receipts hook.`);
2005
2148
  }
2006
2149
  }
2007
- const attrsPath = join6(".receipts", ".gitattributes");
2150
+ const attrsPath = join7(".receipts", ".gitattributes");
2008
2151
  if (!existsSync4(attrsPath)) {
2009
2152
  mkdirSync3(".receipts", { recursive: true });
2010
2153
  writeFileSync3(attrsPath, "* linguist-generated\n");
@@ -2043,22 +2186,22 @@ function openAdoptionPr(written, lines) {
2043
2186
  lines.push("receipts init: nothing new to commit \u2014 the repo is already integrated.");
2044
2187
  return;
2045
2188
  }
2046
- if (!git3(["rev-parse", "--git-dir"])) {
2189
+ if (!git4(["rev-parse", "--git-dir"])) {
2047
2190
  lines.push("receipts init: not a git repository \u2014 commit the files above manually.");
2048
2191
  return;
2049
2192
  }
2050
- if (git3(["rev-parse", "--verify", "--quiet", ADOPT_BRANCH]) !== "") {
2193
+ if (git4(["rev-parse", "--verify", "--quiet", ADOPT_BRANCH]) !== "") {
2051
2194
  lines.push(
2052
2195
  `receipts init: branch ${ADOPT_BRANCH} already exists \u2014 commit the files above to it manually.`
2053
2196
  );
2054
2197
  return;
2055
2198
  }
2056
- if (spawnSync4("git", ["checkout", "-b", ADOPT_BRANCH], { encoding: "utf8" }).status !== 0) {
2199
+ if (spawnSync5("git", ["checkout", "-b", ADOPT_BRANCH], { encoding: "utf8" }).status !== 0) {
2057
2200
  lines.push(`receipts init: could not create branch ${ADOPT_BRANCH} \u2014 commit manually.`);
2058
2201
  return;
2059
2202
  }
2060
- spawnSync4("git", ["add", "--", ...written], { encoding: "utf8" });
2061
- const commit = spawnSync4(
2203
+ spawnSync5("git", ["add", "--", ...written], { encoding: "utf8" });
2204
+ const commit = spawnSync5(
2062
2205
  "git",
2063
2206
  ["commit", "-m", "chore: adopt receipts \u2014 zero-install agent-work verification"],
2064
2207
  { encoding: "utf8" }
@@ -2070,7 +2213,7 @@ function openAdoptionPr(written, lines) {
2070
2213
  return;
2071
2214
  }
2072
2215
  lines.push(`receipts init: committed ${written.length} file(s) on ${ADOPT_BRANCH}.`);
2073
- const push = spawnSync4("git", ["push", "-u", "origin", ADOPT_BRANCH], { encoding: "utf8" });
2216
+ const push = spawnSync5("git", ["push", "-u", "origin", ADOPT_BRANCH], { encoding: "utf8" });
2074
2217
  if (push.status !== 0) {
2075
2218
  lines.push(
2076
2219
  "receipts init: push failed (no remote / auth?) \u2014 push the branch and open a PR manually."
@@ -2078,7 +2221,7 @@ function openAdoptionPr(written, lines) {
2078
2221
  return;
2079
2222
  }
2080
2223
  lines.push(`receipts init: pushed ${ADOPT_BRANCH}.`);
2081
- const gh = spawnSync4(
2224
+ const gh = spawnSync5(
2082
2225
  "gh",
2083
2226
  [
2084
2227
  "pr",
@@ -2094,7 +2237,7 @@ function openAdoptionPr(written, lines) {
2094
2237
  lines.push(`receipts init: opened the PR \u2014 ${gh.stdout.trim().split("\n").pop()}`);
2095
2238
  return;
2096
2239
  }
2097
- const remote = git3(["remote", "get-url", "origin"]);
2240
+ const remote = git4(["remote", "get-url", "origin"]);
2098
2241
  const slug = remote ? /github\.com[:/]([^/]+\/[^/.]+)/.exec(remote)?.[1] : void 0;
2099
2242
  lines.push(
2100
2243
  slug ? `receipts init: open the PR here \u2014 https://github.com/${slug}/pull/new/${ADOPT_BRANCH}` : "receipts init: branch pushed \u2014 open a PR from it in your forge."
@@ -2159,7 +2302,7 @@ function runInstallHook() {
2159
2302
  return 0;
2160
2303
  }
2161
2304
  function runSetupLocal() {
2162
- const path = join6(homedir(), ".claude", "settings.json");
2305
+ const path = join7(homedir(), ".claude", "settings.json");
2163
2306
  const res = mergeHookIntoSettings(path);
2164
2307
  if (!res.ok) {
2165
2308
  process.stderr.write(`receipts setup-local: ${res.reason}.
@@ -2175,10 +2318,16 @@ function runSetupLocal() {
2175
2318
  }
2176
2319
  async function runRederive(file, opts = {}) {
2177
2320
  if (!file) {
2178
- process.stderr.write("Usage: receipts rederive <transcript.jsonl> [--branch b] [--redact]\n");
2321
+ process.stderr.write(
2322
+ "Usage: receipts rederive <transcript.jsonl> [--agent a] [--branch b] [--redact]\n"
2323
+ );
2179
2324
  return 1;
2180
2325
  }
2181
- const receipt = await rederiveFromTranscript(file, { branch: opts.branch, redact: opts.redact });
2326
+ const receipt = await rederiveFromTranscript(file, {
2327
+ source: opts.source,
2328
+ branch: opts.branch,
2329
+ redact: opts.redact
2330
+ });
2182
2331
  if (!receipt) {
2183
2332
  process.stderr.write(`receipts rederive: could not read a session from ${file}
2184
2333
  `);
@@ -2190,7 +2339,7 @@ async function runRederive(file, opts = {}) {
2190
2339
  return 0;
2191
2340
  }
2192
2341
  async function runAssert(opts) {
2193
- const repoRoot = git3(["rev-parse", "--show-toplevel"]) || ".";
2342
+ const repoRoot = git4(["rev-parse", "--show-toplevel"]) || ".";
2194
2343
  let asserts;
2195
2344
  try {
2196
2345
  asserts = loadAsserts(repoRoot);