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/{chunk-EKMFU3ES.js → chunk-543NGQTN.js} +2 -2
- package/dist/{chunk-GOLRNSZT.js → chunk-ASPLZPMQ.js} +11 -2
- package/dist/{chunk-GOLRNSZT.js.map → chunk-ASPLZPMQ.js.map} +1 -1
- package/dist/{chunk-EYM5WETZ.js → chunk-DBQWQVZZ.js} +139 -19
- package/dist/chunk-DBQWQVZZ.js.map +1 -0
- package/dist/cli.js +267 -118
- package/dist/cli.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/mcp/server.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-EYM5WETZ.js.map +0 -1
- /package/dist/{chunk-EKMFU3ES.js.map → chunk-543NGQTN.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -13,13 +13,13 @@ import {
|
|
|
13
13
|
renderShareMarkdown,
|
|
14
14
|
sliceByBranch,
|
|
15
15
|
toDsseEnvelope
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-ASPLZPMQ.js";
|
|
17
17
|
import {
|
|
18
18
|
computeTrends,
|
|
19
19
|
deriveTargets,
|
|
20
20
|
renderTrends,
|
|
21
21
|
upsertTrendsSection
|
|
22
|
-
} from "./chunk-
|
|
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-
|
|
46
|
+
} from "./chunk-DBQWQVZZ.js";
|
|
47
47
|
|
|
48
48
|
// src/cli.ts
|
|
49
|
-
import { spawnSync as
|
|
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
|
|
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
|
-
|
|
179
|
-
|
|
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
|
|
216
|
-
const r =
|
|
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) =>
|
|
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
|
|
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
|
|
359
|
+
return primary;
|
|
249
360
|
}
|
|
250
361
|
}
|
|
251
|
-
return
|
|
362
|
+
return null;
|
|
252
363
|
}
|
|
253
364
|
function headIsAttachCommit(branch, cwd) {
|
|
254
|
-
return
|
|
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 &&
|
|
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 =
|
|
408
|
+
const repoRoot = git3(["rev-parse", "--show-toplevel"]);
|
|
291
409
|
if (!repoRoot || !repoOptedIn(repoRoot)) {
|
|
292
410
|
return { exit: 0 };
|
|
293
411
|
}
|
|
294
|
-
const branch =
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
316
|
-
const commit =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
1440
|
-
const r =
|
|
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 =
|
|
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 ||
|
|
1497
|
-
const repoRoot =
|
|
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
|
-
|
|
1576
|
-
|
|
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 =
|
|
1696
|
+
const attrs = join7(dir, ".gitattributes");
|
|
1579
1697
|
if (!existsSync4(attrs)) {
|
|
1580
1698
|
writeFileSync3(attrs, "* linguist-generated\n");
|
|
1581
|
-
|
|
1699
|
+
git4(["add", attrs]);
|
|
1582
1700
|
}
|
|
1583
1701
|
writeFileSync3(out, json);
|
|
1584
|
-
|
|
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
|
-
|
|
1743
|
-
|
|
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
|
-
|
|
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 =
|
|
1817
|
-
const branch =
|
|
1818
|
-
const path = file ?? (branch && branch !== "HEAD" ?
|
|
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 =
|
|
1853
|
-
const branch =
|
|
1854
|
-
const path = file ?? (branch && branch !== "HEAD" ?
|
|
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 =
|
|
1889
|
-
const dpath = dir ??
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
1927
|
-
if (
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 (!
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
2061
|
-
const commit =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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, {
|
|
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 =
|
|
2342
|
+
const repoRoot = git4(["rev-parse", "--show-toplevel"]) || ".";
|
|
2194
2343
|
let asserts;
|
|
2195
2344
|
try {
|
|
2196
2345
|
asserts = loadAsserts(repoRoot);
|