altimate-receipts 0.3.2 → 0.5.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
@@ -8,17 +8,18 @@ import {
8
8
  compareToTranscript,
9
9
  copyToClipboard,
10
10
  inDiff,
11
+ narrowEffort,
11
12
  rederiveFromTranscript,
12
13
  renderShareMarkdown,
13
14
  sliceByBranch,
14
15
  toDsseEnvelope
15
- } from "./chunk-SUAQDKUV.js";
16
+ } from "./chunk-WISVSYA7.js";
16
17
  import {
17
18
  computeTrends,
18
19
  deriveTargets,
19
20
  renderTrends,
20
21
  upsertTrendsSection
21
- } from "./chunk-RQLUZ6FQ.js";
22
+ } from "./chunk-QGUQOQXO.js";
22
23
  import {
23
24
  agentIds,
24
25
  anyDetected,
@@ -42,17 +43,322 @@ import {
42
43
  selectSummary,
43
44
  upsertGuardrailsSection,
44
45
  verifyBundle
45
- } from "./chunk-UHI6BGLE.js";
46
+ } from "./chunk-2QTR3AF4.js";
46
47
 
47
48
  // 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";
49
+ import { spawnSync as spawnSync3 } from "child_process";
50
+ import {
51
+ existsSync as existsSync4,
52
+ mkdirSync as mkdirSync3,
53
+ readFileSync as readFileSync6,
54
+ readdirSync as readdirSync3,
55
+ realpathSync,
56
+ unlinkSync,
57
+ writeFileSync as writeFileSync3
58
+ } from "fs";
59
+ import { homedir } from "os";
60
+ import { join as join6, relative } from "path";
51
61
  import { pathToFileURL } from "url";
52
62
 
63
+ // src/hook/installGitHook.ts
64
+ import { spawnSync } from "child_process";
65
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
66
+ import { isAbsolute, join } from "path";
67
+ var MARKER = "altimate-receipts";
68
+ var SHIM_BODY = `command -v npx >/dev/null 2>&1 || exit 0
69
+ code=0
70
+ npx -y altimate-receipts@latest hook pre-push --agent git || code=$?
71
+ if [ "$code" -eq 42 ]; then exit 1; fi
72
+ exit 0
73
+ `;
74
+ var HOOK_SCRIPT = `#!/bin/sh
75
+ # Verified-by-Receipts pre-push hook \u2014 generated by \`receipts install-hook\` (altimate-receipts).
76
+ # Attaches this branch's agent receipt before pushing. Best-effort: only the
77
+ # deliberate "receipt attached, push again" signal (exit 42) ever stops a push.
78
+ hookdir=$(dirname "$0")
79
+ if [ -x "$hookdir/pre-push.local" ]; then
80
+ "$hookdir/pre-push.local" "$@" </dev/null || exit $?
81
+ fi
82
+ ${SHIM_BODY}`;
83
+ var HUSKY_SCRIPT = `# Verified-by-Receipts pre-push hook \u2014 generated by \`receipts install-hook\` (altimate-receipts).
84
+ # Attaches this branch's agent receipt before pushing. Best-effort: only the
85
+ # deliberate "receipt attached, push again" signal (exit 42) ever stops a push.
86
+ ${SHIM_BODY}`;
87
+ function git(args, cwd) {
88
+ const r = spawnSync("git", args, { encoding: "utf8", cwd });
89
+ return r.status === 0 ? r.stdout.trim() : null;
90
+ }
91
+ var abs = (p, cwd) => isAbsolute(p) ? p : join(cwd ?? process.cwd(), p);
92
+ function resolveHookTarget(cwd) {
93
+ if (git(["rev-parse", "--git-dir"], cwd) === null) {
94
+ return null;
95
+ }
96
+ const hooksPath = git(["config", "core.hooksPath"], cwd);
97
+ if (hooksPath) {
98
+ const parts = hooksPath.split(/[\\/]/);
99
+ if (parts.includes(".husky")) {
100
+ const root = git(["rev-parse", "--show-toplevel"], cwd) ?? cwd ?? process.cwd();
101
+ return { kind: "husky", dir: join(root, ".husky") };
102
+ }
103
+ return { kind: "custom", dir: abs(hooksPath, cwd) };
104
+ }
105
+ const p = git(["rev-parse", "--git-path", "hooks"], cwd);
106
+ return p ? { kind: "git", dir: abs(p, cwd) } : null;
107
+ }
108
+ function installGitHook(cwd) {
109
+ const target = resolveHookTarget(cwd);
110
+ if (!target) {
111
+ return { ok: false, reason: "not a git repository" };
112
+ }
113
+ const script = target.kind === "husky" ? HUSKY_SCRIPT : HOOK_SCRIPT;
114
+ mkdirSync(target.dir, { recursive: true });
115
+ const hookPath = join(target.dir, "pre-push");
116
+ const local = join(target.dir, "pre-push.local");
117
+ if (existsSync(hookPath)) {
118
+ const current = readFileSync(hookPath, "utf8");
119
+ if (current.includes(MARKER)) {
120
+ if (current === script) {
121
+ return { ok: true, action: "unchanged", path: hookPath };
122
+ }
123
+ writeFileSync(hookPath, script);
124
+ chmodSync(hookPath, 493);
125
+ return { ok: true, action: "installed", path: hookPath };
126
+ }
127
+ if (target.kind === "husky") {
128
+ return {
129
+ ok: false,
130
+ reason: `${hookPath} already exists \u2014 append this to it:
131
+ ${SHIM_BODY}`
132
+ };
133
+ }
134
+ if (existsSync(local)) {
135
+ return {
136
+ ok: false,
137
+ reason: `${hookPath} and ${local} both exist and aren't receipts hooks \u2014 resolve manually`
138
+ };
139
+ }
140
+ renameSync(hookPath, local);
141
+ writeFileSync(hookPath, script);
142
+ chmodSync(hookPath, 493);
143
+ return { ok: true, action: "chained", path: hookPath };
144
+ }
145
+ writeFileSync(hookPath, script);
146
+ chmodSync(hookPath, 493);
147
+ return { ok: true, action: "installed", path: hookPath };
148
+ }
149
+ var PREPARE_CMD = "npx -y altimate-receipts@latest install-hook";
150
+ function wirePrepareScript(pkgPath) {
151
+ if (!existsSync(pkgPath)) {
152
+ return { ok: false, reason: `${pkgPath} not found \u2014 \`--prepare\` needs a package.json` };
153
+ }
154
+ const raw = readFileSync(pkgPath, "utf8");
155
+ let pkg;
156
+ try {
157
+ pkg = JSON.parse(raw);
158
+ } catch {
159
+ return { ok: false, reason: `${pkgPath} is not valid JSON \u2014 leaving it untouched` };
160
+ }
161
+ if (pkg.scripts === void 0) {
162
+ pkg.scripts = {};
163
+ }
164
+ const scripts = pkg.scripts;
165
+ const prepare = scripts.prepare;
166
+ if (prepare?.includes(PREPARE_CMD)) {
167
+ return { ok: true, changed: false };
168
+ }
169
+ scripts.prepare = prepare ? `${prepare} && ${PREPARE_CMD}` : PREPARE_CMD;
170
+ const indent = raw.match(/^(\s+)"/m)?.[1] ?? " ";
171
+ writeFileSync(pkgPath, `${JSON.stringify(pkg, null, indent)}
172
+ `);
173
+ return { ok: true, changed: true };
174
+ }
175
+
176
+ // src/hook/prePush.ts
177
+ import { spawnSync as spawnSync2 } from "child_process";
178
+ import { existsSync as existsSync2 } from "fs";
179
+ import { join as join2 } from "path";
180
+ var REPUSH_EXIT = 42;
181
+ var ATTACH_SUBJECT = (branch) => `chore(receipts): attach agent receipt for ${branch}`;
182
+ function git2(args, cwd) {
183
+ const r = spawnSync2("git", args, { encoding: "utf8", cwd });
184
+ return r.status === 0 ? r.stdout.trim() : "";
185
+ }
186
+ var WRAPPERS = /* @__PURE__ */ new Set(["command", "exec", "nohup", "time", "env"]);
187
+ var GIT_VALUE_FLAGS = /* @__PURE__ */ new Set(["-C", "-c", "--git-dir", "--work-tree", "--exec-path"]);
188
+ var TAG_REFSPEC = /^(refs\/tags\/|v\d+(\.\d+)*$)/;
189
+ function isGitPush(command) {
190
+ const blanked = command.replace(/\\["']/g, " ").replace(/'[^']*'/g, " ").replace(/"[^"]*"/g, " ");
191
+ for (const simple of blanked.split(/(?:&&|\|\||[;|\n])/)) {
192
+ const tokens = simple.trim().split(/\s+/).filter(Boolean);
193
+ let i = 0;
194
+ while (i < tokens.length && (/^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[i]) || WRAPPERS.has(tokens[i]))) {
195
+ i++;
196
+ }
197
+ if (tokens[i] !== "git") {
198
+ continue;
199
+ }
200
+ i++;
201
+ while (i < tokens.length && tokens[i].startsWith("-")) {
202
+ const flag = tokens[i];
203
+ i++;
204
+ if (GIT_VALUE_FLAGS.has(flag)) {
205
+ i++;
206
+ }
207
+ }
208
+ if (tokens[i] !== "push") {
209
+ continue;
210
+ }
211
+ const rest = tokens.slice(i + 1);
212
+ if (rest.includes("--dry-run") || rest.includes("-n") || rest.includes("--tags")) {
213
+ continue;
214
+ }
215
+ const positionals = rest.filter((t) => !t.startsWith("-"));
216
+ const refspecs = positionals.slice(1);
217
+ if (refspecs.length > 0 && refspecs.every((r) => TAG_REFSPEC.test(r))) {
218
+ continue;
219
+ }
220
+ return true;
221
+ }
222
+ return false;
223
+ }
224
+ function gitStdinPushesBranch(stdin) {
225
+ return stdin.split("\n").some((line) => line.trim().startsWith("refs/heads/"));
226
+ }
227
+ function repoOptedIn(repoRoot) {
228
+ return existsSync2(join2(repoRoot, ".github", "workflows", "receipts.yml")) || existsSync2(join2(repoRoot, ".receipts"));
229
+ }
230
+ function headIsAttachCommit(branch, cwd) {
231
+ return git2(["log", "-1", "--format=%s"], cwd) === ATTACH_SUBJECT(branch);
232
+ }
233
+ async function readStdin(stream = process.stdin) {
234
+ let data = "";
235
+ for await (const chunk of stream) {
236
+ data += chunk;
237
+ }
238
+ return data;
239
+ }
240
+ async function runHookPrePush(dialect, stdin, generate) {
241
+ try {
242
+ if (process.env.RECEIPTS_HOOK === "0") {
243
+ return { exit: 0 };
244
+ }
245
+ if ((process.env.RECEIPTS_STORE || "commit").toLowerCase() === "none") {
246
+ return { exit: 0 };
247
+ }
248
+ if (dialect === "claude") {
249
+ let payload;
250
+ try {
251
+ payload = JSON.parse(stdin);
252
+ } catch {
253
+ return { exit: 0 };
254
+ }
255
+ if (payload.tool_name !== "Bash" || !payload.tool_input?.command) {
256
+ return { exit: 0 };
257
+ }
258
+ if (payload.cwd && existsSync2(payload.cwd)) {
259
+ process.chdir(payload.cwd);
260
+ }
261
+ if (!isGitPush(payload.tool_input.command)) {
262
+ return { exit: 0 };
263
+ }
264
+ } else if (!gitStdinPushesBranch(stdin)) {
265
+ return { exit: 0 };
266
+ }
267
+ const repoRoot = git2(["rev-parse", "--show-toplevel"]);
268
+ if (!repoRoot || !repoOptedIn(repoRoot)) {
269
+ return { exit: 0 };
270
+ }
271
+ const branch = git2(["rev-parse", "--abbrev-ref", "HEAD"]);
272
+ if (!branch || branch === "HEAD") {
273
+ return { exit: 0 };
274
+ }
275
+ if (dialect === "git" && headIsAttachCommit(branch)) {
276
+ return { exit: 0 };
277
+ }
278
+ const write = process.stderr.write.bind(process.stderr);
279
+ process.stderr.write = (() => true);
280
+ let generated;
281
+ try {
282
+ generated = await generate();
283
+ } finally {
284
+ process.stderr.write = write;
285
+ }
286
+ if (generated !== 0) {
287
+ return { exit: 0 };
288
+ }
289
+ if (!git2(["status", "--porcelain", ".receipts/"])) {
290
+ return { exit: 0 };
291
+ }
292
+ spawnSync2("git", ["add", ".receipts/"], { encoding: "utf8" });
293
+ const commit = spawnSync2(
294
+ "git",
295
+ ["commit", "--no-verify", "-m", ATTACH_SUBJECT(branch), "--", ".receipts/"],
296
+ { encoding: "utf8" }
297
+ );
298
+ if (commit.status !== 0) {
299
+ return { exit: 0 };
300
+ }
301
+ if (dialect === "git") {
302
+ return {
303
+ exit: REPUSH_EXIT,
304
+ message: `\u{1F4CE} Receipts: attached an agent receipt for '${branch}' \u2014 run \`git push\` again to include it.`
305
+ };
306
+ }
307
+ return {
308
+ exit: 0,
309
+ message: `receipts: attached an agent receipt for '${branch}' \u2014 it rides this push.`
310
+ };
311
+ } catch {
312
+ return { exit: 0 };
313
+ }
314
+ }
315
+
316
+ // src/hook/settingsMerge.ts
317
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
318
+ import { dirname } from "path";
319
+ var HOOK_COMMAND = "npx -y altimate-receipts@latest hook pre-push";
320
+ function mergeHookIntoSettings(path) {
321
+ let settings = {};
322
+ if (existsSync3(path)) {
323
+ let parsed;
324
+ try {
325
+ parsed = JSON.parse(readFileSync2(path, "utf8"));
326
+ } catch {
327
+ return { ok: false, reason: `${path} is not valid JSON \u2014 leaving it untouched` };
328
+ }
329
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
330
+ return { ok: false, reason: `${path} is not a JSON object \u2014 leaving it untouched` };
331
+ }
332
+ settings = parsed;
333
+ }
334
+ if (settings.hooks === void 0) {
335
+ settings.hooks = {};
336
+ }
337
+ const hooks = settings.hooks;
338
+ if (typeof hooks !== "object" || hooks === null || Array.isArray(hooks)) {
339
+ return { ok: false, reason: `${path} has a non-object "hooks" key \u2014 leaving it untouched` };
340
+ }
341
+ if (hooks.PreToolUse === void 0) {
342
+ hooks.PreToolUse = [];
343
+ }
344
+ const pre = hooks.PreToolUse;
345
+ if (!Array.isArray(pre)) {
346
+ return { ok: false, reason: `${path} has a non-array hooks.PreToolUse \u2014 leaving it untouched` };
347
+ }
348
+ const present = pre.some((e) => e?.hooks?.some((h) => h?.command === HOOK_COMMAND));
349
+ if (present) {
350
+ return { ok: true, changed: false };
351
+ }
352
+ pre.push({ matcher: "Bash", hooks: [{ type: "command", command: HOOK_COMMAND }] });
353
+ mkdirSync2(dirname(path), { recursive: true });
354
+ writeFileSync2(path, `${JSON.stringify(settings, null, 2)}
355
+ `);
356
+ return { ok: true, changed: true };
357
+ }
358
+
53
359
  // src/receipt/assert.ts
54
- import { readFileSync } from "fs";
55
- import { join } from "path";
360
+ import { readFileSync as readFileSync3 } from "fs";
361
+ import { join as join3 } from "path";
56
362
  var OPS = /* @__PURE__ */ new Set([
57
363
  "eq",
58
364
  "ne",
@@ -147,10 +453,10 @@ function validateAssertion(raw) {
147
453
  };
148
454
  }
149
455
  function loadAsserts(repoRoot) {
150
- const path = join(repoRoot, ".receipts", "asserts.json");
456
+ const path = join3(repoRoot, ".receipts", "asserts.json");
151
457
  let text;
152
458
  try {
153
- text = readFileSync(path, "utf8");
459
+ text = readFileSync3(path, "utf8");
154
460
  } catch {
155
461
  return [];
156
462
  }
@@ -374,8 +680,8 @@ function renderFieldScan(s) {
374
680
  }
375
681
 
376
682
  // src/report/log.ts
377
- import { readFileSync as readFileSync2, readdirSync } from "fs";
378
- import { join as join2 } from "path";
683
+ import { readFileSync as readFileSync4, readdirSync } from "fs";
684
+ import { join as join4 } from "path";
379
685
  var SEV_ICON2 = { critical: "\u26D4", high: "\u26A0\uFE0F", medium: "\u{1F50D}", low: "\xB7" };
380
686
  var SEV_RANK = { critical: 0, high: 1, medium: 2, low: 3 };
381
687
  var NON_RECEIPT = /(?:^|\/)(?:asserts(?:\.example)?|sample)\.json$/i;
@@ -399,7 +705,7 @@ function loadReceiptHistory(dir) {
399
705
  for (const f of files) {
400
706
  let input;
401
707
  try {
402
- input = JSON.parse(readFileSync2(join2(dir, f), "utf8"));
708
+ input = JSON.parse(readFileSync4(join4(dir, f), "utf8"));
403
709
  } catch {
404
710
  continue;
405
711
  }
@@ -457,6 +763,26 @@ ${lines.join("\n")}
457
763
  `;
458
764
  }
459
765
 
766
+ // src/report/prune.ts
767
+ var PROTECTED = /(?:^|\/)(?:asserts(?:\.example)?|sample)\.json$/i;
768
+ function branchSlug(branch) {
769
+ return branch.replace(/[/\\]/g, "-");
770
+ }
771
+ function planPrune(files, liveSlugs) {
772
+ const keep = [];
773
+ const remove = [];
774
+ for (const f of files) {
775
+ const base = f.split("/").pop() ?? f;
776
+ const slug = base.replace(/\.json$/i, "");
777
+ if (PROTECTED.test(base) || liveSlugs.has(slug)) {
778
+ keep.push(f);
779
+ } else {
780
+ remove.push(f);
781
+ }
782
+ }
783
+ return { keep, remove };
784
+ }
785
+
460
786
  // src/report/sarif.ts
461
787
  var INFO_URI = "https://github.com/AltimateAI/altimate-receipts";
462
788
  function levelOf(severity) {
@@ -542,8 +868,8 @@ function toSarif(receipt) {
542
868
  }
543
869
 
544
870
  // src/report/stats.ts
545
- import { readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
546
- import { join as join3 } from "path";
871
+ import { readFileSync as readFileSync5, readdirSync as readdirSync2 } from "fs";
872
+ import { join as join5 } from "path";
547
873
  var NON_RECEIPT2 = /(?:^|\/)(?:asserts(?:\.example)?|sample)\.json$/i;
548
874
  function computeStats(dir) {
549
875
  let files;
@@ -561,7 +887,7 @@ function computeStats(dir) {
561
887
  for (const f of files) {
562
888
  let input;
563
889
  try {
564
- input = JSON.parse(readFileSync3(join3(dir, f), "utf8"));
890
+ input = JSON.parse(readFileSync5(join5(dir, f), "utf8"));
565
891
  } catch {
566
892
  skipped++;
567
893
  continue;
@@ -704,7 +1030,14 @@ Usage
704
1030
  receipts eval Flag-rate of the detectors over your real local sessions (--last N, --json)
705
1031
  receipts badge [receipt] shields.io endpoint JSON for a README/PR badge (--out f)
706
1032
  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)
1033
+ receipts prune [dir] Remove committed receipts for merged/deleted branches (--dry-run)
1034
+ receipts init One-command adopt: PR-check workflow + the repo-committed
1035
+ agent hook (--prepare also wires the git-hook floor)
1036
+ receipts hook pre-push (called by hooks, not humans) attach the receipt on push
1037
+ (--agent claude | git selects the payload dialect)
1038
+ receipts install-hook Write the self-updating pre-push hook into .git/hooks
1039
+ receipts setup-local Add the hook to YOUR ~/.claude/settings.json (only fires
1040
+ in repos that have adopted the Receipts workflow)
708
1041
  receipts rederive <file> Reproduce the canonical Receipt from a transcript
709
1042
  receipts assert [selector] Check the receipt against committed .receipts/asserts.json (CI gate)
710
1043
  receipts mcp Start the MCP server (stdio) for IDEs/agents
@@ -756,9 +1089,13 @@ var COMMANDS = /* @__PURE__ */ new Set([
756
1089
  "stats",
757
1090
  "eval",
758
1091
  "badge",
1092
+ "prune",
759
1093
  "sarif",
760
1094
  "init",
761
- "assert"
1095
+ "assert",
1096
+ "hook",
1097
+ "install-hook",
1098
+ "setup-local"
762
1099
  ]);
763
1100
  function parseArgs(argv) {
764
1101
  const args = argv.slice(2);
@@ -772,7 +1109,9 @@ function parseArgs(argv) {
772
1109
  handoff: false,
773
1110
  redact: false,
774
1111
  copy: false,
1112
+ dryRun: false,
775
1113
  wholeSession: false,
1114
+ prepare: false,
776
1115
  color: !process.env.NO_COLOR && process.stdout.isTTY === true
777
1116
  };
778
1117
  const positionals = [];
@@ -811,13 +1150,20 @@ function parseArgs(argv) {
811
1150
  if (Number.isFinite(n) && n > 0) {
812
1151
  parsed.last = Math.floor(n);
813
1152
  }
1153
+ } else if (a === "--dry-run") {
1154
+ parsed.dryRun = true;
814
1155
  } else if (a === "--whole-session") {
815
1156
  parsed.wholeSession = true;
1157
+ } else if (a === "--prepare") {
1158
+ parsed.prepare = true;
816
1159
  } else if (a === "--agent") {
817
1160
  const next = args[i + 1];
818
1161
  if (next && agentIds().includes(next)) {
819
1162
  parsed.agent = next;
820
1163
  i++;
1164
+ } else if (next === "claude" || next === "git") {
1165
+ parsed.hookDialect = next;
1166
+ i++;
821
1167
  }
822
1168
  } else if (a === "--no-color") {
823
1169
  parsed.color = false;
@@ -872,8 +1218,20 @@ async function run(argv) {
872
1218
  if (args.command === "sarif") {
873
1219
  return runSarif(args.file, { out: args.out });
874
1220
  }
1221
+ if (args.command === "prune") {
1222
+ return runPrune(args.file, { dryRun: args.dryRun });
1223
+ }
875
1224
  if (args.command === "init") {
876
- return runInit();
1225
+ return runInit({ prepare: args.prepare });
1226
+ }
1227
+ if (args.command === "hook") {
1228
+ return runHook(args.file, args.hookDialect ?? "claude");
1229
+ }
1230
+ if (args.command === "install-hook") {
1231
+ return runInstallHook();
1232
+ }
1233
+ if (args.command === "setup-local") {
1234
+ return runSetupLocal();
877
1235
  }
878
1236
  if (args.command === "rederive") {
879
1237
  return runRederive(args.file, {
@@ -960,7 +1318,7 @@ Run a coding-agent session first, then try again.
960
1318
  }
961
1319
  const out = args.compact ? canonicalize(receipt) : JSON.stringify(receipt, null, 2);
962
1320
  if (args.out) {
963
- writeFileSync(args.out, `${out}
1321
+ writeFileSync3(args.out, `${out}
964
1322
  `);
965
1323
  process.stderr.write(`Receipt written to ${args.out}
966
1324
  `);
@@ -983,15 +1341,26 @@ Run a coding-agent session first, then try again.
983
1341
  process.stdout.write(renderCard({ summary, derived, findings }, { color: args.color }));
984
1342
  return 0;
985
1343
  }
986
- function git(args) {
987
- const r = spawnSync("git", args, { encoding: "utf8" });
1344
+ function git3(args) {
1345
+ const r = spawnSync3("git", args, { encoding: "utf8" });
988
1346
  return r.status === 0 ? r.stdout.trim() : "";
989
1347
  }
990
1348
  var PR_SELECT_SCAN = 150;
991
1349
  function diffOverlap(derived, files) {
992
1350
  return derived.filesChanged.filter((f) => inDiff(f.path, files)).length;
993
1351
  }
994
- async function pickForDiff(all, branch, repoRoot, files) {
1352
+ function branchBirthMs(base) {
1353
+ if (!base) {
1354
+ return null;
1355
+ }
1356
+ const out = git3(["log", "--reverse", "--format=%at", `${base}..HEAD`]);
1357
+ const first = Number(out.split("\n")[0]?.trim());
1358
+ return Number.isFinite(first) && first > 0 ? first * 1e3 : null;
1359
+ }
1360
+ function staleForBranch(endedAt, birthMs) {
1361
+ return birthMs != null && endedAt != null && endedAt < birthMs;
1362
+ }
1363
+ async function pickForDiff(all, branch, repoRoot, files, birthMs = null) {
995
1364
  const load = async (sum) => {
996
1365
  const session = await loadSession(sum);
997
1366
  if (!session) {
@@ -1006,7 +1375,7 @@ async function pickForDiff(all, branch, repoRoot, files) {
1006
1375
  }
1007
1376
  let best = null;
1008
1377
  let bestScore = 0;
1009
- const recent = all.filter((s) => inRepo(s.projectPath, repoRoot)).slice(0, PR_SELECT_SCAN);
1378
+ const recent = all.filter((s) => inRepo(s.projectPath, repoRoot)).filter((s) => !staleForBranch(s.endedAt, birthMs)).slice(0, PR_SELECT_SCAN);
1010
1379
  for (const sum of recent) {
1011
1380
  const cand = await load(sum);
1012
1381
  if (!cand) {
@@ -1021,15 +1390,15 @@ async function pickForDiff(all, branch, repoRoot, files) {
1021
1390
  return best ?? primary;
1022
1391
  }
1023
1392
  async function runPr(opts) {
1024
- const branch = opts.branch || git(["rev-parse", "--abbrev-ref", "HEAD"]);
1025
- const repoRoot = git(["rev-parse", "--show-toplevel"]);
1393
+ const branch = opts.branch || git3(["rev-parse", "--abbrev-ref", "HEAD"]);
1394
+ const repoRoot = git3(["rev-parse", "--show-toplevel"]);
1026
1395
  if (!branch || branch === "HEAD") {
1027
1396
  process.stderr.write("receipts pr: not on a git branch (use --branch <name>).\n");
1028
1397
  return 1;
1029
1398
  }
1030
1399
  const diff = opts.wholeSession ? null : changedFiles(opts.base);
1031
1400
  const all = await listSessions();
1032
- const picked = diff ? await pickForDiff(all, branch, repoRoot || void 0, diff.files) : await (async () => {
1401
+ const picked = diff ? await pickForDiff(all, branch, repoRoot || void 0, diff.files, branchBirthMs(diff.base)) : await (async () => {
1033
1402
  const sum = selectForBranch(all, branch, repoRoot || void 0);
1034
1403
  return sum ? { summary: sum, session: await loadSession(sum), derived: null } : null;
1035
1404
  })();
@@ -1053,6 +1422,12 @@ Build the branch with a coding agent first, or run \`receipts --list\`.
1053
1422
  findings = sd.findings;
1054
1423
  scope = { kind: "diff", base: diff.base, files: diff.files };
1055
1424
  scopeNote = `diff vs ${diff.base} (${diff.files.length} file${diff.files.length === 1 ? "" : "s"})`;
1425
+ const slice = sliceByBranch(session, branch);
1426
+ const eff = narrowEffort(slice ? deriveSpans(slice) : null, diff.files);
1427
+ if (eff) {
1428
+ derived = { ...derived, diffCostUsd: eff.cost, diffTokens: eff.tokens, diffTurns: eff.turns };
1429
+ scope.branch = branch;
1430
+ }
1056
1431
  } else if (!opts.wholeSession) {
1057
1432
  const slice = sliceByBranch(session, branch);
1058
1433
  if (slice) {
@@ -1087,11 +1462,11 @@ Build the branch with a coding agent first, or run \`receipts --list\`.
1087
1462
  `);
1088
1463
  }
1089
1464
  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]);
1465
+ const dir = join6(repoRoot || ".", ".receipts");
1466
+ const out = opts.out ?? join6(dir, `${safe}.json`);
1467
+ mkdirSync3(dir, { recursive: true });
1468
+ writeFileSync3(out, json);
1469
+ git3(["add", out]);
1095
1470
  const rel = repoRoot ? relative(repoRoot, out) : out;
1096
1471
  process.stderr.write(
1097
1472
  `receipts pr: wrote ${rel} (Grade ${receipt.predicate.grade}, ${scopeNote}) from "${summary.title ?? "untitled"}".
@@ -1118,8 +1493,8 @@ async function runGuardrails(opts) {
1118
1493
  const rules = collectGuardrails(findingSets);
1119
1494
  const block = renderGuardrailsBlock(rules, opts.json ? "json" : "md");
1120
1495
  if (opts.out) {
1121
- const existing = existsSync(opts.out) ? readFileSync4(opts.out, "utf8") : "";
1122
- writeFileSync(opts.out, upsertGuardrailsSection(existing, block));
1496
+ const existing = existsSync4(opts.out) ? readFileSync6(opts.out, "utf8") : "";
1497
+ writeFileSync3(opts.out, upsertGuardrailsSection(existing, block));
1123
1498
  process.stderr.write(
1124
1499
  `guardrails: wrote ${rules.length} rule(s) to ${opts.out} (from ${findingSets.length} session${findingSets.length === 1 ? "" : "s"}).
1125
1500
  `
@@ -1151,8 +1526,8 @@ async function runTrends(opts) {
1151
1526
  const trends = computeTrends(inputs, requested);
1152
1527
  if (opts.out) {
1153
1528
  const block = renderTrends(trends, "md");
1154
- const existing = existsSync(opts.out) ? readFileSync4(opts.out, "utf8") : "";
1155
- writeFileSync(opts.out, upsertTrendsSection(existing, block));
1529
+ const existing = existsSync4(opts.out) ? readFileSync6(opts.out, "utf8") : "";
1530
+ writeFileSync3(opts.out, upsertTrendsSection(existing, block));
1156
1531
  process.stderr.write(
1157
1532
  `trends: wrote section to ${opts.out} (from ${trends.window.used} sessions).
1158
1533
  `
@@ -1178,7 +1553,7 @@ function runEnvelope(file) {
1178
1553
  return 1;
1179
1554
  }
1180
1555
  try {
1181
- const receipt = JSON.parse(readFileSync4(file, "utf8"));
1556
+ const receipt = JSON.parse(readFileSync6(file, "utf8"));
1182
1557
  process.stdout.write(`${JSON.stringify(toDsseEnvelope(receipt), null, 2)}
1183
1558
  `);
1184
1559
  return 0;
@@ -1195,7 +1570,7 @@ async function runVerify(file, opts = {}) {
1195
1570
  }
1196
1571
  let input;
1197
1572
  try {
1198
- input = JSON.parse(readFileSync4(file, "utf8"));
1573
+ input = JSON.parse(readFileSync6(file, "utf8"));
1199
1574
  } catch (err) {
1200
1575
  process.stderr.write(`Could not read ${file}: ${err instanceof Error ? err.message : err}
1201
1576
  `);
@@ -1249,8 +1624,8 @@ function runDiff(fileA, fileB, opts = {}) {
1249
1624
  );
1250
1625
  return 1;
1251
1626
  }
1252
- pathA = join4(".receipts", `${hist[1].name}.json`);
1253
- pathB = join4(".receipts", `${hist[0].name}.json`);
1627
+ pathA = join6(".receipts", `${hist[1].name}.json`);
1628
+ pathB = join6(".receipts", `${hist[0].name}.json`);
1254
1629
  process.stdout.write(`receipts diff: ${hist[1].name} \u2192 ${hist[0].name} (most recent two)
1255
1630
 
1256
1631
  `);
@@ -1264,7 +1639,7 @@ function runDiff(fileA, fileB, opts = {}) {
1264
1639
  const read = (f) => {
1265
1640
  let input;
1266
1641
  try {
1267
- input = JSON.parse(readFileSync4(f, "utf8"));
1642
+ input = JSON.parse(readFileSync6(f, "utf8"));
1268
1643
  } catch (err) {
1269
1644
  process.stderr.write(`Could not read ${f}: ${err instanceof Error ? err.message : err}
1270
1645
  `);
@@ -1287,7 +1662,7 @@ function runDiff(fileA, fileB, opts = {}) {
1287
1662
  const output = opts.json ? `${JSON.stringify(delta, null, 2)}
1288
1663
  ` : renderDiff(delta);
1289
1664
  if (opts.out) {
1290
- writeFileSync(opts.out, output);
1665
+ writeFileSync3(opts.out, output);
1291
1666
  process.stdout.write(`receipts diff: wrote ${opts.out}
1292
1667
  `);
1293
1668
  } else {
@@ -1301,7 +1676,7 @@ function runLog(dir, opts = {}) {
1301
1676
  const output = opts.json ? `${JSON.stringify(shown, null, 2)}
1302
1677
  ` : renderLog(shown, all.length);
1303
1678
  if (opts.out) {
1304
- writeFileSync(opts.out, output);
1679
+ writeFileSync3(opts.out, output);
1305
1680
  process.stdout.write(`receipts log: wrote ${opts.out}
1306
1681
  `);
1307
1682
  } else {
@@ -1314,7 +1689,7 @@ function runStats(dir, opts = {}) {
1314
1689
  const output = opts.json ? `${JSON.stringify(stats, null, 2)}
1315
1690
  ` : renderStats(stats);
1316
1691
  if (opts.out) {
1317
- writeFileSync(opts.out, output);
1692
+ writeFileSync3(opts.out, output);
1318
1693
  process.stdout.write(`receipts stats: wrote ${opts.out}
1319
1694
  `);
1320
1695
  } else {
@@ -1323,10 +1698,10 @@ function runStats(dir, opts = {}) {
1323
1698
  return 0;
1324
1699
  }
1325
1700
  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)) {
1701
+ const repoRoot = git3(["rev-parse", "--show-toplevel"]) || ".";
1702
+ const branch = git3(["rev-parse", "--abbrev-ref", "HEAD"]);
1703
+ const path = file ?? (branch && branch !== "HEAD" ? join6(repoRoot, ".receipts", `${branch.replace(/[/\\]/g, "-")}.json`) : void 0);
1704
+ if (!path || !existsSync4(path)) {
1330
1705
  process.stderr.write(
1331
1706
  `receipts badge: no receipt found${path ? ` at ${path}` : ""}. Run \`receipts pr\` first, or pass a receipt path.
1332
1707
  `
@@ -1335,7 +1710,7 @@ function runBadge(file, opts = {}) {
1335
1710
  }
1336
1711
  let input;
1337
1712
  try {
1338
- input = JSON.parse(readFileSync4(path, "utf8"));
1713
+ input = JSON.parse(readFileSync6(path, "utf8"));
1339
1714
  } catch {
1340
1715
  process.stderr.write(`receipts badge: could not parse ${path}
1341
1716
  `);
@@ -1350,7 +1725,7 @@ function runBadge(file, opts = {}) {
1350
1725
  const output = `${JSON.stringify(badgeEndpoint(res.receipt.predicate), null, 2)}
1351
1726
  `;
1352
1727
  if (opts.out) {
1353
- writeFileSync(opts.out, output);
1728
+ writeFileSync3(opts.out, output);
1354
1729
  process.stdout.write(`receipts badge: wrote ${opts.out}
1355
1730
  `);
1356
1731
  } else {
@@ -1359,10 +1734,10 @@ function runBadge(file, opts = {}) {
1359
1734
  return 0;
1360
1735
  }
1361
1736
  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)) {
1737
+ const repoRoot = git3(["rev-parse", "--show-toplevel"]) || ".";
1738
+ const branch = git3(["rev-parse", "--abbrev-ref", "HEAD"]);
1739
+ const path = file ?? (branch && branch !== "HEAD" ? join6(repoRoot, ".receipts", `${branch.replace(/[/\\]/g, "-")}.json`) : void 0);
1740
+ if (!path || !existsSync4(path)) {
1366
1741
  process.stderr.write(
1367
1742
  `receipts sarif: no receipt found${path ? ` at ${path}` : ""}. Run \`receipts pr\` first, or pass a receipt path.
1368
1743
  `
@@ -1371,7 +1746,7 @@ function runSarif(file, opts = {}) {
1371
1746
  }
1372
1747
  let input;
1373
1748
  try {
1374
- input = JSON.parse(readFileSync4(path, "utf8"));
1749
+ input = JSON.parse(readFileSync6(path, "utf8"));
1375
1750
  } catch {
1376
1751
  process.stderr.write(`receipts sarif: could not parse ${path}
1377
1752
  `);
@@ -1386,7 +1761,7 @@ function runSarif(file, opts = {}) {
1386
1761
  const output = `${JSON.stringify(toSarif(res.receipt), null, 2)}
1387
1762
  `;
1388
1763
  if (opts.out) {
1389
- writeFileSync(opts.out, output);
1764
+ writeFileSync3(opts.out, output);
1390
1765
  process.stdout.write(`receipts sarif: wrote ${opts.out}
1391
1766
  `);
1392
1767
  } else {
@@ -1394,6 +1769,59 @@ function runSarif(file, opts = {}) {
1394
1769
  }
1395
1770
  return 0;
1396
1771
  }
1772
+ function runPrune(dir, opts = {}) {
1773
+ const repoRoot = git3(["rev-parse", "--show-toplevel"]) || ".";
1774
+ const dpath = dir ?? join6(repoRoot, ".receipts");
1775
+ let files;
1776
+ try {
1777
+ files = readdirSync3(dpath).filter((f) => f.endsWith(".json"));
1778
+ } catch {
1779
+ process.stderr.write(`receipts prune: no ${dpath} directory.
1780
+ `);
1781
+ return 0;
1782
+ }
1783
+ const ls = git3(["ls-remote", "--heads", "origin"]);
1784
+ if (ls === null) {
1785
+ process.stderr.write(
1786
+ "receipts prune: could not list remote branches (offline / no 'origin'?) \u2014 aborting, nothing removed.\n"
1787
+ );
1788
+ return 1;
1789
+ }
1790
+ const liveSlugs = new Set(
1791
+ ls.split("\n").map((l) => l.split("refs/heads/")[1]).filter(Boolean).map((b) => branchSlug(b.trim()))
1792
+ );
1793
+ const { keep, remove } = planPrune(files, liveSlugs);
1794
+ if (remove.length === 0) {
1795
+ process.stdout.write(`receipts prune: nothing to prune (${keep.length} receipt(s) kept).
1796
+ `);
1797
+ return 0;
1798
+ }
1799
+ if (opts.dryRun) {
1800
+ process.stdout.write(
1801
+ `receipts prune (dry-run): would remove ${remove.length}/${files.length} receipt(s), keep ${keep.length}:
1802
+ `
1803
+ );
1804
+ for (const f of remove) {
1805
+ process.stdout.write(` - ${f}
1806
+ `);
1807
+ }
1808
+ return 0;
1809
+ }
1810
+ for (const f of remove) {
1811
+ const p = join6(dpath, f);
1812
+ if (git3(["rm", "-f", "--", p]) === null) {
1813
+ try {
1814
+ unlinkSync(p);
1815
+ } catch {
1816
+ }
1817
+ }
1818
+ }
1819
+ process.stdout.write(
1820
+ `receipts prune: removed ${remove.length} receipt(s) for merged/deleted branches; kept ${keep.length}.
1821
+ `
1822
+ );
1823
+ return 0;
1824
+ }
1397
1825
  async function runEval(opts = {}) {
1398
1826
  const limit = opts.limit && opts.limit > 0 ? opts.limit : 200;
1399
1827
  const summaries = (await listSessions(opts.agent)).slice(0, limit);
@@ -1414,17 +1842,53 @@ async function runEval(opts = {}) {
1414
1842
  ` : renderFieldScan(scan));
1415
1843
  return 0;
1416
1844
  }
1417
- function runInit() {
1845
+ function runInit(opts = {}) {
1846
+ const lines = [];
1418
1847
  const v = getVersion();
1419
- const tag = /^\d+\.\d+\.\d+$/.test(v) ? `v${v}` : "v0.2.2";
1848
+ const major = /^(\d+)\.\d+\.\d+/.exec(v)?.[1];
1849
+ const tag = major ? `v${major}` : "v0";
1420
1850
  const dir = ".github/workflows";
1421
1851
  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;
1852
+ if (existsSync4(path)) {
1853
+ lines.push(`receipts init: ${path} already exists \u2014 leaving it untouched.`);
1854
+ } else {
1855
+ mkdirSync3(dir, { recursive: true });
1856
+ writeFileSync3(path, workflowContent(tag, v));
1857
+ lines.push(`receipts init: wrote ${path} (tracking ${tag}, quiet + non-blocking).`);
1858
+ }
1859
+ const settingsPath = join6(".claude", "settings.json");
1860
+ const merged = mergeHookIntoSettings(settingsPath);
1861
+ if (!merged.ok) {
1862
+ lines.push(`receipts init: ${merged.reason}.`);
1863
+ } else if (merged.changed) {
1864
+ lines.push(`receipts init: added the receipts pre-push hook to ${settingsPath}.`);
1865
+ } else {
1866
+ lines.push(`receipts init: ${settingsPath} already has the receipts hook.`);
1867
+ }
1868
+ const ignorePath = join6(".claude", ".gitignore");
1869
+ if (!existsSync4(ignorePath)) {
1870
+ writeFileSync3(ignorePath, "*\n!settings.json\n!.gitignore\n");
1871
+ lines.push(`receipts init: wrote ${ignorePath} (commit settings.json, ignore the rest).`);
1872
+ }
1873
+ if (opts.prepare) {
1874
+ const wired = wirePrepareScript("package.json");
1875
+ if (!wired.ok) {
1876
+ lines.push(`receipts init: ${wired.reason}.`);
1877
+ } else if (wired.changed) {
1878
+ lines.push(
1879
+ "receipts init: wired `prepare` in package.json \u2014 `npm install` now self-installs the git hook."
1880
+ );
1881
+ } else {
1882
+ lines.push("receipts init: package.json `prepare` already wired.");
1883
+ }
1426
1884
  }
1427
- const content = `name: Verified by Receipts
1885
+ lines.push(" Commit these, open a PR, and receipts attach themselves from then on.");
1886
+ process.stdout.write(`${lines.join("\n")}
1887
+ `);
1888
+ return 0;
1889
+ }
1890
+ function workflowContent(tag, v) {
1891
+ return `name: Verified by Receipts
1428
1892
 
1429
1893
  # Deterministic "what did the coding agent actually do?" check on PRs. Quiet + non-blocking
1430
1894
  # pilot: acts only when a branch commits an agent Receipt (.receipts/<branch>.json);
@@ -1443,21 +1907,56 @@ permissions:
1443
1907
 
1444
1908
  jobs:
1445
1909
  receipts:
1910
+ # Tracks the ${tag} major tag (auto-gets minor/patch features). Pin a full version
1911
+ # (e.g. @v${v}) or a commit SHA for immutability.
1446
1912
  uses: AltimateAI/altimate-receipts/.github/workflows/receipts.reusable.yml@${tag}
1447
1913
  with:
1448
1914
  require-receipt: false # never fail a PR that has no receipt (soft pilot)
1449
1915
  notify-when-missing: false # stay silent unless a receipt is present
1450
1916
  # block-on: "" # informational check; never blocks a merge
1451
1917
  `;
1452
- mkdirSync(dir, { recursive: true });
1453
- writeFileSync(path, content);
1918
+ }
1919
+ async function runHook(kind, dialect) {
1920
+ if (kind !== "pre-push") {
1921
+ process.stderr.write("Usage: receipts hook pre-push [--agent claude|git]\n");
1922
+ return 1;
1923
+ }
1924
+ const stdin = await readStdin();
1925
+ const res = await runHookPrePush(dialect, stdin, async () => runPr({}));
1926
+ if (res.message) {
1927
+ process.stderr.write(`${res.message}
1928
+ `);
1929
+ }
1930
+ return res.exit;
1931
+ }
1932
+ function runInstallHook() {
1933
+ const res = installGitHook();
1934
+ if (!res.ok) {
1935
+ process.stderr.write(`receipts install-hook: ${res.reason}.
1936
+ `);
1937
+ return 1;
1938
+ }
1939
+ const note = {
1940
+ installed: `installed ${res.path}`,
1941
+ unchanged: `${res.path} already installed`,
1942
+ chained: `installed ${res.path} (existing hook preserved as pre-push.local, runs first)`
1943
+ }[res.action];
1944
+ process.stdout.write(`receipts install-hook: ${note}.
1945
+ `);
1946
+ return 0;
1947
+ }
1948
+ function runSetupLocal() {
1949
+ const path = join6(homedir(), ".claude", "settings.json");
1950
+ const res = mergeHookIntoSettings(path);
1951
+ if (!res.ok) {
1952
+ process.stderr.write(`receipts setup-local: ${res.reason}.
1953
+ `);
1954
+ return 1;
1955
+ }
1454
1956
  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")
1957
+ res.changed ? `receipts setup-local: added the receipts hook to ${path} \u2014 active only in repos with the Receipts workflow.
1958
+ ` : `receipts setup-local: ${path} already has the receipts hook.
1959
+ `
1461
1960
  );
1462
1961
  return 0;
1463
1962
  }
@@ -1478,7 +1977,7 @@ async function runRederive(file, opts = {}) {
1478
1977
  return 0;
1479
1978
  }
1480
1979
  async function runAssert(opts) {
1481
- const repoRoot = git(["rev-parse", "--show-toplevel"]) || ".";
1980
+ const repoRoot = git3(["rev-parse", "--show-toplevel"]) || ".";
1482
1981
  let asserts;
1483
1982
  try {
1484
1983
  asserts = loadAsserts(repoRoot);
@@ -1550,8 +2049,10 @@ if (isMain) {
1550
2049
  });
1551
2050
  }
1552
2051
  export {
2052
+ branchBirthMs,
1553
2053
  diffOverlap,
1554
2054
  parseArgs,
1555
- run
2055
+ run,
2056
+ staleForBranch
1556
2057
  };
1557
2058
  //# sourceMappingURL=cli.js.map