altimate-receipts 0.5.2 → 0.6.1

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-54J2FEKI.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+)*$)/;
@@ -233,25 +340,44 @@ function isGitPush(command) {
233
340
  }
234
341
  return false;
235
342
  }
343
+ function isGitCommit(command) {
344
+ for (const { sub, rest } of gitInvocations(command)) {
345
+ if (sub === "commit" && !rest.includes("--dry-run")) {
346
+ return true;
347
+ }
348
+ }
349
+ return false;
350
+ }
236
351
  function gitStdinPushesBranch(stdin) {
237
352
  return stdin.split("\n").some((line) => line.trim().startsWith("refs/heads/"));
238
353
  }
239
- var hasMarker = (root) => existsSync2(join2(root, ".github", "workflows", "receipts.yml")) || existsSync2(join2(root, ".receipts"));
354
+ var hasMarker = (root) => existsSync3(join3(root, ".github", "workflows", "receipts.yml")) || existsSync3(join3(root, ".receipts"));
240
355
  function repoOptedIn(repoRoot) {
241
356
  if (hasMarker(repoRoot)) {
242
357
  return true;
243
358
  }
244
- const common = git2(["rev-parse", "--git-common-dir"], repoRoot);
359
+ const primary = primaryCheckout(repoRoot);
360
+ return primary ? hasMarker(primary) : false;
361
+ }
362
+ function primaryCheckout(repoRoot) {
363
+ const common = git3(["rev-parse", "--git-common-dir"], repoRoot);
245
364
  if (common.endsWith("/.git")) {
246
365
  const primary = common.slice(0, -"/.git".length);
247
366
  if (primary && primary !== repoRoot) {
248
- return hasMarker(primary);
367
+ return primary;
249
368
  }
250
369
  }
251
- return false;
370
+ return null;
252
371
  }
253
372
  function headIsAttachCommit(branch, cwd) {
254
- return git2(["log", "-1", "--format=%s"], cwd) === ATTACH_SUBJECT(branch);
373
+ return git3(["log", "-1", "--format=%s"], cwd) === ATTACH_SUBJECT(branch);
374
+ }
375
+ function isDefaultBranch(branch, cwd) {
376
+ const head = git3(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], cwd);
377
+ if (head) {
378
+ return head === `origin/${branch}`;
379
+ }
380
+ return branch === "main" || branch === "master";
255
381
  }
256
382
  async function readStdin(stream = process.stdin) {
257
383
  let data = "";
@@ -261,6 +387,7 @@ async function readStdin(stream = process.stdin) {
261
387
  return data;
262
388
  }
263
389
  async function runHookPrePush(dialect, stdin, generate) {
390
+ let pendingCommit = false;
264
391
  try {
265
392
  if (process.env.RECEIPTS_HOOK === "0") {
266
393
  return { exit: 0 };
@@ -278,42 +405,59 @@ async function runHookPrePush(dialect, stdin, generate) {
278
405
  if (payload.tool_name !== "Bash" || !payload.tool_input?.command) {
279
406
  return { exit: 0 };
280
407
  }
281
- if (payload.cwd && existsSync2(payload.cwd)) {
408
+ if (payload.cwd && existsSync3(payload.cwd)) {
282
409
  process.chdir(payload.cwd);
283
410
  }
284
411
  if (!isGitPush(payload.tool_input.command)) {
285
412
  return { exit: 0 };
286
413
  }
414
+ pendingCommit = isGitCommit(payload.tool_input.command);
287
415
  } else if (!gitStdinPushesBranch(stdin)) {
288
416
  return { exit: 0 };
289
417
  }
290
- const repoRoot = git2(["rev-parse", "--show-toplevel"]);
418
+ const repoRoot = git3(["rev-parse", "--show-toplevel"]);
291
419
  if (!repoRoot || !repoOptedIn(repoRoot)) {
292
420
  return { exit: 0 };
293
421
  }
294
- const branch = git2(["rev-parse", "--abbrev-ref", "HEAD"]);
295
- if (!branch || branch === "HEAD") {
422
+ const branch = git3(["rev-parse", "--abbrev-ref", "HEAD"]);
423
+ if (!branch || branch === "HEAD" || isDefaultBranch(branch)) {
296
424
  return { exit: 0 };
297
425
  }
298
- if (dialect === "git" && headIsAttachCommit(branch)) {
426
+ const store = (process.env.RECEIPTS_STORE || settingsStore(repoRoot) || settingsStore(primaryCheckout(repoRoot) ?? "") || "commit").toLowerCase();
427
+ if (store === "none") {
428
+ return { exit: 0 };
429
+ }
430
+ process.env.RECEIPTS_STORE = store;
431
+ if (store !== "ref" && dialect === "git" && headIsAttachCommit(branch)) {
299
432
  return { exit: 0 };
300
433
  }
301
434
  const write = process.stderr.write.bind(process.stderr);
302
435
  process.stderr.write = (() => true);
303
436
  let generated;
304
437
  try {
305
- generated = await generate();
438
+ generated = await generate({ pendingCommit });
306
439
  } finally {
307
440
  process.stderr.write = write;
308
441
  }
309
442
  if (generated !== 0) {
310
443
  return { exit: 0 };
311
444
  }
312
- if (!git2(["status", "--porcelain", ".receipts/"])) {
445
+ if (store === "ref") {
446
+ const slug = branch.replace(/[/\\]/g, "-");
447
+ if (readReceiptRef(slug) == null) {
448
+ return { exit: 0 };
449
+ }
450
+ const pushed = pushReceiptRef(slug);
451
+ return {
452
+ exit: 0,
453
+ 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.`
454
+ };
455
+ }
456
+ if (!git3(["status", "--porcelain", ".receipts/"])) {
313
457
  return { exit: 0 };
314
458
  }
315
- spawnSync2("git", ["add", ".receipts/"], { encoding: "utf8" });
316
- const commit = spawnSync2(
459
+ spawnSync3("git", ["add", ".receipts/"], { encoding: "utf8" });
460
+ const commit = spawnSync3(
317
461
  "git",
318
462
  ["commit", "--no-verify", "-m", ATTACH_SUBJECT(branch), "--", ".receipts/"],
319
463
  { encoding: "utf8" }
@@ -336,52 +480,9 @@ async function runHookPrePush(dialect, stdin, generate) {
336
480
  }
337
481
  }
338
482
 
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
483
  // src/receipt/assert.ts
383
484
  import { readFileSync as readFileSync3 } from "fs";
384
- import { join as join3 } from "path";
485
+ import { join as join4 } from "path";
385
486
  var OPS = /* @__PURE__ */ new Set([
386
487
  "eq",
387
488
  "ne",
@@ -476,7 +577,7 @@ function validateAssertion(raw) {
476
577
  };
477
578
  }
478
579
  function loadAsserts(repoRoot) {
479
- const path = join3(repoRoot, ".receipts", "asserts.json");
580
+ const path = join4(repoRoot, ".receipts", "asserts.json");
480
581
  let text;
481
582
  try {
482
583
  text = readFileSync3(path, "utf8");
@@ -704,7 +805,7 @@ function renderFieldScan(s) {
704
805
 
705
806
  // src/report/log.ts
706
807
  import { readFileSync as readFileSync4, readdirSync } from "fs";
707
- import { join as join4 } from "path";
808
+ import { dirname as dirname2, join as join5 } from "path";
708
809
  var SEV_ICON2 = { critical: "\u26D4", high: "\u26A0\uFE0F", medium: "\u{1F50D}", low: "\xB7" };
709
810
  var SEV_RANK = { critical: 0, high: 1, medium: 2, low: 3 };
710
811
  var NON_RECEIPT = /(?:^|\/)(?:asserts(?:\.example)?|sample)\.json$/i;
@@ -722,13 +823,27 @@ function loadReceiptHistory(dir) {
722
823
  try {
723
824
  files = readdirSync(dir).filter((f) => f.endsWith(".json") && !NON_RECEIPT.test(f));
724
825
  } catch {
725
- return [];
826
+ files = [];
827
+ }
828
+ const sources = files.map((f) => {
829
+ try {
830
+ return { name: f, raw: readFileSync4(join5(dir, f), "utf8") };
831
+ } catch {
832
+ return { name: f, raw: null };
833
+ }
834
+ });
835
+ const repo = dirname2(dir);
836
+ for (const { slug } of listReceiptRefs(repo)) {
837
+ sources.push({ name: `${slug}@ref`, raw: readReceiptRef(slug, repo) });
726
838
  }
727
839
  const entries = [];
728
- for (const f of files) {
840
+ for (const { name: f, raw } of sources) {
841
+ if (raw == null) {
842
+ continue;
843
+ }
729
844
  let input;
730
845
  try {
731
- input = JSON.parse(readFileSync4(join4(dir, f), "utf8"));
846
+ input = JSON.parse(raw);
732
847
  } catch {
733
848
  continue;
734
849
  }
@@ -892,7 +1007,7 @@ function toSarif(receipt) {
892
1007
 
893
1008
  // src/report/stats.ts
894
1009
  import { readFileSync as readFileSync5, readdirSync as readdirSync2 } from "fs";
895
- import { join as join5 } from "path";
1010
+ import { join as join6 } from "path";
896
1011
  var NON_RECEIPT2 = /(?:^|\/)(?:asserts(?:\.example)?|sample)\.json$/i;
897
1012
  function computeStats(dir) {
898
1013
  let files;
@@ -910,7 +1025,7 @@ function computeStats(dir) {
910
1025
  for (const f of files) {
911
1026
  let input;
912
1027
  try {
913
- input = JSON.parse(readFileSync5(join5(dir, f), "utf8"));
1028
+ input = JSON.parse(readFileSync5(join6(dir, f), "utf8"));
914
1029
  } catch {
915
1030
  skipped++;
916
1031
  continue;
@@ -1027,13 +1142,13 @@ function renderHandoffMarkdown(h) {
1027
1142
  }
1028
1143
 
1029
1144
  // src/trace/commitMatch.ts
1030
- import { spawnSync as spawnSync3 } from "child_process";
1145
+ import { spawnSync as spawnSync4 } from "child_process";
1031
1146
  var SHA_CAP = 200;
1032
1147
  function branchShas(base, cwd) {
1033
1148
  if (!base) {
1034
1149
  return [];
1035
1150
  }
1036
- const r = spawnSync3("git", ["log", `--max-count=${SHA_CAP}`, "--format=%H", `${base}..HEAD`], {
1151
+ const r = spawnSync4("git", ["log", `--max-count=${SHA_CAP}`, "--format=%H", `${base}..HEAD`], {
1037
1152
  encoding: "utf8",
1038
1153
  cwd
1039
1154
  });
@@ -1330,6 +1445,7 @@ async function run(argv) {
1330
1445
  }
1331
1446
  if (args.command === "rederive") {
1332
1447
  return runRederive(args.file, {
1448
+ source: args.agent,
1333
1449
  branch: args.branchScope,
1334
1450
  redact: args.redact,
1335
1451
  compact: args.compact
@@ -1436,8 +1552,8 @@ Run a coding-agent session first, then try again.
1436
1552
  process.stdout.write(renderCard({ summary, derived, findings }, { color: args.color }));
1437
1553
  return 0;
1438
1554
  }
1439
- function git3(args) {
1440
- const r = spawnSync4("git", args, { encoding: "utf8" });
1555
+ function git4(args) {
1556
+ const r = spawnSync5("git", args, { encoding: "utf8" });
1441
1557
  return r.status === 0 ? r.stdout.trim() : "";
1442
1558
  }
1443
1559
  var PR_SELECT_SCAN = 150;
@@ -1448,7 +1564,7 @@ function branchBirthMs(base) {
1448
1564
  if (!base) {
1449
1565
  return null;
1450
1566
  }
1451
- const out = git3(["log", "--reverse", "--format=%at", `${base}..HEAD`]);
1567
+ const out = git4(["log", "--reverse", "--format=%at", `${base}..HEAD`]);
1452
1568
  const first = Number(out.split("\n")[0]?.trim());
1453
1569
  return Number.isFinite(first) && first > 0 ? first * 1e3 : null;
1454
1570
  }
@@ -1493,13 +1609,13 @@ async function pickForDiff(all, branch, repoRoot, files, birthMs = null, shas =
1493
1609
  return best ?? primary;
1494
1610
  }
1495
1611
  async function runPr(opts) {
1496
- const branch = opts.branch || git3(["rev-parse", "--abbrev-ref", "HEAD"]);
1497
- const repoRoot = git3(["rev-parse", "--show-toplevel"]);
1612
+ const branch = opts.branch || git4(["rev-parse", "--abbrev-ref", "HEAD"]);
1613
+ const repoRoot = git4(["rev-parse", "--show-toplevel"]);
1498
1614
  if (!branch || branch === "HEAD") {
1499
1615
  process.stderr.write("receipts pr: not on a git branch (use --branch <name>).\n");
1500
1616
  return 1;
1501
1617
  }
1502
- const diff = opts.wholeSession ? null : changedFiles(opts.base);
1618
+ const diff = opts.wholeSession ? null : changedFiles(opts.base, { includeWorkingTree: opts.pendingCommit });
1503
1619
  const all = await listSessions();
1504
1620
  const picked = diff ? await pickForDiff(
1505
1621
  all,
@@ -1553,7 +1669,7 @@ Build the branch with a coding agent first, or run \`receipts --list\`.
1553
1669
  const receipt = redactReceipt(await buildReceipt(scopedSession, derived, findings, { scope }));
1554
1670
  const json = `${JSON.stringify(receipt, null, 2)}
1555
1671
  `;
1556
- const store = (opts.store || process.env.RECEIPTS_STORE || "commit").toLowerCase();
1672
+ const store = (opts.store || process.env.RECEIPTS_STORE || (repoRoot ? settingsStore(repoRoot) ?? settingsStore(primaryCheckout(repoRoot) ?? "") : void 0) || "commit").toLowerCase();
1557
1673
  if (store === "none") {
1558
1674
  process.stdout.write(json);
1559
1675
  process.stderr.write(
@@ -1567,21 +1683,33 @@ Build the branch with a coding agent first, or run \`receipts --list\`.
1567
1683
  `receipts pr: store=${store} not yet implemented (SPEC-0064) \u2014 using \`commit\`.
1568
1684
  `
1569
1685
  );
1570
- } else if (store !== "commit") {
1686
+ } else if (store !== "commit" && store !== "ref") {
1571
1687
  process.stderr.write(`receipts pr: unknown store=${store} \u2014 using \`commit\`.
1572
1688
  `);
1573
1689
  }
1574
1690
  const safe = branch.replace(/[/\\]/g, "-");
1575
- const dir = join6(repoRoot || ".", ".receipts");
1576
- const out = opts.out ?? join6(dir, `${safe}.json`);
1691
+ if (store === "ref") {
1692
+ const w = writeReceiptRef(safe, branch, json, summary.endedAt, repoRoot || void 0);
1693
+ if (w.ok) {
1694
+ process.stderr.write(
1695
+ `receipts pr: wrote ${w.ref} (Grade ${receipt.predicate.grade}, ${scopeNote}) from "${summary.title ?? "untitled"}" \u2014 no tree files.
1696
+ `
1697
+ );
1698
+ return 0;
1699
+ }
1700
+ process.stderr.write(`receipts pr: store=ref failed (${w.reason}) \u2014 using \`commit\`.
1701
+ `);
1702
+ }
1703
+ const dir = join7(repoRoot || ".", ".receipts");
1704
+ const out = opts.out ?? join7(dir, `${safe}.json`);
1577
1705
  mkdirSync3(dir, { recursive: true });
1578
- const attrs = join6(dir, ".gitattributes");
1706
+ const attrs = join7(dir, ".gitattributes");
1579
1707
  if (!existsSync4(attrs)) {
1580
1708
  writeFileSync3(attrs, "* linguist-generated\n");
1581
- git3(["add", attrs]);
1709
+ git4(["add", attrs]);
1582
1710
  }
1583
1711
  writeFileSync3(out, json);
1584
- git3(["add", out]);
1712
+ git4(["add", out]);
1585
1713
  const rel = repoRoot ? relative(repoRoot, out) : out;
1586
1714
  process.stderr.write(
1587
1715
  `receipts pr: wrote ${rel} (Grade ${receipt.predicate.grade}, ${scopeNote}) from "${summary.title ?? "untitled"}".
@@ -1739,8 +1867,9 @@ function runDiff(fileA, fileB, opts = {}) {
1739
1867
  );
1740
1868
  return 1;
1741
1869
  }
1742
- pathA = join6(".receipts", `${hist[1].name}.json`);
1743
- pathB = join6(".receipts", `${hist[0].name}.json`);
1870
+ const sourceOf = (name) => name.endsWith("@ref") ? name : join7(".receipts", `${name}.json`);
1871
+ pathA = sourceOf(hist[1].name);
1872
+ pathB = sourceOf(hist[0].name);
1744
1873
  process.stdout.write(`receipts diff: ${hist[1].name} \u2192 ${hist[0].name} (most recent two)
1745
1874
 
1746
1875
  `);
@@ -1754,7 +1883,8 @@ function runDiff(fileA, fileB, opts = {}) {
1754
1883
  const read = (f) => {
1755
1884
  let input;
1756
1885
  try {
1757
- input = JSON.parse(readFileSync6(f, "utf8"));
1886
+ const raw = f.endsWith("@ref") ? readReceiptRef(f.slice(0, -"@ref".length)) : null;
1887
+ input = JSON.parse(raw ?? readFileSync6(f, "utf8"));
1758
1888
  } catch (err) {
1759
1889
  process.stderr.write(`Could not read ${f}: ${err instanceof Error ? err.message : err}
1760
1890
  `);
@@ -1813,9 +1943,9 @@ function runStats(dir, opts = {}) {
1813
1943
  return 0;
1814
1944
  }
1815
1945
  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);
1946
+ const repoRoot = git4(["rev-parse", "--show-toplevel"]) || ".";
1947
+ const branch = git4(["rev-parse", "--abbrev-ref", "HEAD"]);
1948
+ const path = file ?? (branch && branch !== "HEAD" ? join7(repoRoot, ".receipts", `${branch.replace(/[/\\]/g, "-")}.json`) : void 0);
1819
1949
  if (!path || !existsSync4(path)) {
1820
1950
  process.stderr.write(
1821
1951
  `receipts badge: no receipt found${path ? ` at ${path}` : ""}. Run \`receipts pr\` first, or pass a receipt path.
@@ -1849,9 +1979,9 @@ function runBadge(file, opts = {}) {
1849
1979
  return 0;
1850
1980
  }
1851
1981
  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);
1982
+ const repoRoot = git4(["rev-parse", "--show-toplevel"]) || ".";
1983
+ const branch = git4(["rev-parse", "--abbrev-ref", "HEAD"]);
1984
+ const path = file ?? (branch && branch !== "HEAD" ? join7(repoRoot, ".receipts", `${branch.replace(/[/\\]/g, "-")}.json`) : void 0);
1855
1985
  if (!path || !existsSync4(path)) {
1856
1986
  process.stderr.write(
1857
1987
  `receipts sarif: no receipt found${path ? ` at ${path}` : ""}. Run \`receipts pr\` first, or pass a receipt path.
@@ -1885,8 +2015,8 @@ function runSarif(file, opts = {}) {
1885
2015
  return 0;
1886
2016
  }
1887
2017
  function runPrune(dir, opts = {}) {
1888
- const repoRoot = git3(["rev-parse", "--show-toplevel"]) || ".";
1889
- const dpath = dir ?? join6(repoRoot, ".receipts");
2018
+ const repoRoot = git4(["rev-parse", "--show-toplevel"]) || ".";
2019
+ const dpath = dir ?? join7(repoRoot, ".receipts");
1890
2020
  let files;
1891
2021
  try {
1892
2022
  files = readdirSync3(dpath).filter((f) => f.endsWith(".json"));
@@ -1895,7 +2025,7 @@ function runPrune(dir, opts = {}) {
1895
2025
  `);
1896
2026
  return 0;
1897
2027
  }
1898
- const ls = git3(["ls-remote", "--heads", "origin"]);
2028
+ const ls = git4(["ls-remote", "--heads", "origin"]);
1899
2029
  if (ls === null) {
1900
2030
  process.stderr.write(
1901
2031
  "receipts prune: could not list remote branches (offline / no 'origin'?) \u2014 aborting, nothing removed.\n"
@@ -1907,8 +2037,11 @@ function runPrune(dir, opts = {}) {
1907
2037
  );
1908
2038
  const { keep, remove } = planPrune(files, liveSlugs);
1909
2039
  if (remove.length === 0) {
1910
- process.stdout.write(`receipts prune: nothing to prune (${keep.length} receipt(s) kept).
2040
+ const prunedRefs = pruneReceiptRefs(liveSlugs, opts.dryRun ?? false);
2041
+ if (prunedRefs === 0) {
2042
+ process.stdout.write(`receipts prune: nothing to prune (${keep.length} receipt(s) kept).
1911
2043
  `);
2044
+ }
1912
2045
  return 0;
1913
2046
  }
1914
2047
  if (opts.dryRun) {
@@ -1920,11 +2053,13 @@ function runPrune(dir, opts = {}) {
1920
2053
  process.stdout.write(` - ${f}
1921
2054
  `);
1922
2055
  }
2056
+ pruneReceiptRefs(liveSlugs, true);
1923
2057
  return 0;
1924
2058
  }
2059
+ pruneReceiptRefs(liveSlugs, false);
1925
2060
  for (const f of remove) {
1926
- const p = join6(dpath, f);
1927
- if (git3(["rm", "-f", "--", p]) === null) {
2061
+ const p = join7(dpath, f);
2062
+ if (git4(["rm", "-f", "--", p]) === null) {
1928
2063
  try {
1929
2064
  unlinkSync(p);
1930
2065
  } catch {
@@ -1937,6 +2072,24 @@ function runPrune(dir, opts = {}) {
1937
2072
  );
1938
2073
  return 0;
1939
2074
  }
2075
+ function pruneReceiptRefs(liveSlugs, dryRun) {
2076
+ const refs = listReceiptRefs().filter(({ slug }) => !liveSlugs.has(slug));
2077
+ if (refs.length === 0) {
2078
+ return 0;
2079
+ }
2080
+ for (const { ref, slug } of refs) {
2081
+ if (dryRun) {
2082
+ process.stdout.write(` - ${ref}
2083
+ `);
2084
+ continue;
2085
+ }
2086
+ git4(["update-ref", "-d", ref]);
2087
+ git4(["push", "origin", `:${ref}`]);
2088
+ process.stdout.write(`receipts prune: removed ${ref} (branch ${slug} gone).
2089
+ `);
2090
+ }
2091
+ return refs.length;
2092
+ }
1940
2093
  async function runEval(opts = {}) {
1941
2094
  const limit = opts.limit && opts.limit > 0 ? opts.limit : 200;
1942
2095
  const summaries = (await listSessions(opts.agent)).slice(0, limit);
@@ -1973,7 +2126,7 @@ function runInit(opts = {}) {
1973
2126
  written.push(path);
1974
2127
  lines.push(`receipts init: wrote ${path} (tracking ${tag}, quiet + non-blocking).`);
1975
2128
  }
1976
- const settingsPath = join6(".claude", "settings.json");
2129
+ const settingsPath = join7(".claude", "settings.json");
1977
2130
  const merged = mergeHookIntoSettings(settingsPath);
1978
2131
  if (!merged.ok) {
1979
2132
  lines.push(`receipts init: ${merged.reason}.`);
@@ -1983,14 +2136,14 @@ function runInit(opts = {}) {
1983
2136
  } else {
1984
2137
  lines.push(`receipts init: ${settingsPath} already has the receipts hook.`);
1985
2138
  }
1986
- const ignorePath = join6(".claude", ".gitignore");
2139
+ const ignorePath = join7(".claude", ".gitignore");
1987
2140
  if (!existsSync4(ignorePath)) {
1988
2141
  writeFileSync3(ignorePath, "*\n!settings.json\n!.gitignore\n");
1989
2142
  written.push(ignorePath);
1990
2143
  lines.push(`receipts init: wrote ${ignorePath} (commit settings.json, ignore the rest).`);
1991
2144
  }
1992
2145
  if (opts.agents?.includes("codex") || existsSync4(".codex")) {
1993
- const codexPath = join6(".codex", "hooks.json");
2146
+ const codexPath = join7(".codex", "hooks.json");
1994
2147
  const m = mergeHookIntoSettings(
1995
2148
  codexPath,
1996
2149
  "npx -y altimate-receipts@latest hook pre-push --agent codex"
@@ -2004,7 +2157,7 @@ function runInit(opts = {}) {
2004
2157
  lines.push(`receipts init: ${codexPath} already has the receipts hook.`);
2005
2158
  }
2006
2159
  }
2007
- const attrsPath = join6(".receipts", ".gitattributes");
2160
+ const attrsPath = join7(".receipts", ".gitattributes");
2008
2161
  if (!existsSync4(attrsPath)) {
2009
2162
  mkdirSync3(".receipts", { recursive: true });
2010
2163
  writeFileSync3(attrsPath, "* linguist-generated\n");
@@ -2043,22 +2196,22 @@ function openAdoptionPr(written, lines) {
2043
2196
  lines.push("receipts init: nothing new to commit \u2014 the repo is already integrated.");
2044
2197
  return;
2045
2198
  }
2046
- if (!git3(["rev-parse", "--git-dir"])) {
2199
+ if (!git4(["rev-parse", "--git-dir"])) {
2047
2200
  lines.push("receipts init: not a git repository \u2014 commit the files above manually.");
2048
2201
  return;
2049
2202
  }
2050
- if (git3(["rev-parse", "--verify", "--quiet", ADOPT_BRANCH]) !== "") {
2203
+ if (git4(["rev-parse", "--verify", "--quiet", ADOPT_BRANCH]) !== "") {
2051
2204
  lines.push(
2052
2205
  `receipts init: branch ${ADOPT_BRANCH} already exists \u2014 commit the files above to it manually.`
2053
2206
  );
2054
2207
  return;
2055
2208
  }
2056
- if (spawnSync4("git", ["checkout", "-b", ADOPT_BRANCH], { encoding: "utf8" }).status !== 0) {
2209
+ if (spawnSync5("git", ["checkout", "-b", ADOPT_BRANCH], { encoding: "utf8" }).status !== 0) {
2057
2210
  lines.push(`receipts init: could not create branch ${ADOPT_BRANCH} \u2014 commit manually.`);
2058
2211
  return;
2059
2212
  }
2060
- spawnSync4("git", ["add", "--", ...written], { encoding: "utf8" });
2061
- const commit = spawnSync4(
2213
+ spawnSync5("git", ["add", "--", ...written], { encoding: "utf8" });
2214
+ const commit = spawnSync5(
2062
2215
  "git",
2063
2216
  ["commit", "-m", "chore: adopt receipts \u2014 zero-install agent-work verification"],
2064
2217
  { encoding: "utf8" }
@@ -2070,7 +2223,7 @@ function openAdoptionPr(written, lines) {
2070
2223
  return;
2071
2224
  }
2072
2225
  lines.push(`receipts init: committed ${written.length} file(s) on ${ADOPT_BRANCH}.`);
2073
- const push = spawnSync4("git", ["push", "-u", "origin", ADOPT_BRANCH], { encoding: "utf8" });
2226
+ const push = spawnSync5("git", ["push", "-u", "origin", ADOPT_BRANCH], { encoding: "utf8" });
2074
2227
  if (push.status !== 0) {
2075
2228
  lines.push(
2076
2229
  "receipts init: push failed (no remote / auth?) \u2014 push the branch and open a PR manually."
@@ -2078,7 +2231,7 @@ function openAdoptionPr(written, lines) {
2078
2231
  return;
2079
2232
  }
2080
2233
  lines.push(`receipts init: pushed ${ADOPT_BRANCH}.`);
2081
- const gh = spawnSync4(
2234
+ const gh = spawnSync5(
2082
2235
  "gh",
2083
2236
  [
2084
2237
  "pr",
@@ -2094,7 +2247,7 @@ function openAdoptionPr(written, lines) {
2094
2247
  lines.push(`receipts init: opened the PR \u2014 ${gh.stdout.trim().split("\n").pop()}`);
2095
2248
  return;
2096
2249
  }
2097
- const remote = git3(["remote", "get-url", "origin"]);
2250
+ const remote = git4(["remote", "get-url", "origin"]);
2098
2251
  const slug = remote ? /github\.com[:/]([^/]+\/[^/.]+)/.exec(remote)?.[1] : void 0;
2099
2252
  lines.push(
2100
2253
  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."
@@ -2135,7 +2288,11 @@ async function runHook(kind, dialect) {
2135
2288
  return 1;
2136
2289
  }
2137
2290
  const stdin = await readStdin();
2138
- const res = await runHookPrePush(dialect, stdin, async () => runPr({}));
2291
+ const res = await runHookPrePush(
2292
+ dialect,
2293
+ stdin,
2294
+ async (o) => runPr({ pendingCommit: o?.pendingCommit })
2295
+ );
2139
2296
  if (res.message) {
2140
2297
  process.stderr.write(`${res.message}
2141
2298
  `);
@@ -2159,7 +2316,7 @@ function runInstallHook() {
2159
2316
  return 0;
2160
2317
  }
2161
2318
  function runSetupLocal() {
2162
- const path = join6(homedir(), ".claude", "settings.json");
2319
+ const path = join7(homedir(), ".claude", "settings.json");
2163
2320
  const res = mergeHookIntoSettings(path);
2164
2321
  if (!res.ok) {
2165
2322
  process.stderr.write(`receipts setup-local: ${res.reason}.
@@ -2175,10 +2332,16 @@ function runSetupLocal() {
2175
2332
  }
2176
2333
  async function runRederive(file, opts = {}) {
2177
2334
  if (!file) {
2178
- process.stderr.write("Usage: receipts rederive <transcript.jsonl> [--branch b] [--redact]\n");
2335
+ process.stderr.write(
2336
+ "Usage: receipts rederive <transcript.jsonl> [--agent a] [--branch b] [--redact]\n"
2337
+ );
2179
2338
  return 1;
2180
2339
  }
2181
- const receipt = await rederiveFromTranscript(file, { branch: opts.branch, redact: opts.redact });
2340
+ const receipt = await rederiveFromTranscript(file, {
2341
+ source: opts.source,
2342
+ branch: opts.branch,
2343
+ redact: opts.redact
2344
+ });
2182
2345
  if (!receipt) {
2183
2346
  process.stderr.write(`receipts rederive: could not read a session from ${file}
2184
2347
  `);
@@ -2190,7 +2353,7 @@ async function runRederive(file, opts = {}) {
2190
2353
  return 0;
2191
2354
  }
2192
2355
  async function runAssert(opts) {
2193
- const repoRoot = git3(["rev-parse", "--show-toplevel"]) || ".";
2356
+ const repoRoot = git4(["rev-parse", "--show-toplevel"]) || ".";
2194
2357
  let asserts;
2195
2358
  try {
2196
2359
  asserts = loadAsserts(repoRoot);