altimate-receipts 0.3.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/LICENSE +202 -0
- package/README.md +251 -0
- package/dist/chunk-RQLUZ6FQ.js +262 -0
- package/dist/chunk-RQLUZ6FQ.js.map +1 -0
- package/dist/chunk-SUAQDKUV.js +529 -0
- package/dist/chunk-SUAQDKUV.js.map +1 -0
- package/dist/chunk-UHI6BGLE.js +4569 -0
- package/dist/chunk-UHI6BGLE.js.map +1 -0
- package/dist/cli.js +1523 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.js +293 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/server.js +235 -0
- package/dist/mcp/server.js.map +1 -0
- package/package.json +67 -0
- package/schema/agent-execution-receipt-v1.json +248 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1523 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
CHECK_CATALOG,
|
|
4
|
+
applyDiffScope,
|
|
5
|
+
canonicalize,
|
|
6
|
+
changedFiles,
|
|
7
|
+
checkForId,
|
|
8
|
+
compareToTranscript,
|
|
9
|
+
copyToClipboard,
|
|
10
|
+
inDiff,
|
|
11
|
+
rederiveFromTranscript,
|
|
12
|
+
renderShareMarkdown,
|
|
13
|
+
sliceByBranch,
|
|
14
|
+
toDsseEnvelope
|
|
15
|
+
} from "./chunk-SUAQDKUV.js";
|
|
16
|
+
import {
|
|
17
|
+
computeTrends,
|
|
18
|
+
deriveTargets,
|
|
19
|
+
renderTrends,
|
|
20
|
+
upsertTrendsSection
|
|
21
|
+
} from "./chunk-RQLUZ6FQ.js";
|
|
22
|
+
import {
|
|
23
|
+
agentIds,
|
|
24
|
+
anyDetected,
|
|
25
|
+
buildReceipt,
|
|
26
|
+
collectGuardrails,
|
|
27
|
+
deriveFindings,
|
|
28
|
+
deriveSpans,
|
|
29
|
+
formatCostAlways,
|
|
30
|
+
formatTokens,
|
|
31
|
+
getVersion,
|
|
32
|
+
inRepo,
|
|
33
|
+
listSessions,
|
|
34
|
+
loadSession,
|
|
35
|
+
redact,
|
|
36
|
+
redactReceipt,
|
|
37
|
+
renderCard,
|
|
38
|
+
renderGuardrailsBlock,
|
|
39
|
+
renderList,
|
|
40
|
+
rootsHint,
|
|
41
|
+
selectForBranch,
|
|
42
|
+
selectSummary,
|
|
43
|
+
upsertGuardrailsSection,
|
|
44
|
+
verifyBundle
|
|
45
|
+
} from "./chunk-UHI6BGLE.js";
|
|
46
|
+
|
|
47
|
+
// src/cli.ts
|
|
48
|
+
import { spawnSync } from "child_process";
|
|
49
|
+
import { existsSync, mkdirSync, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
50
|
+
import { join as join4, relative } from "path";
|
|
51
|
+
import { pathToFileURL } from "url";
|
|
52
|
+
|
|
53
|
+
// src/receipt/assert.ts
|
|
54
|
+
import { readFileSync } from "fs";
|
|
55
|
+
import { join } from "path";
|
|
56
|
+
var OPS = /* @__PURE__ */ new Set([
|
|
57
|
+
"eq",
|
|
58
|
+
"ne",
|
|
59
|
+
"lt",
|
|
60
|
+
"lte",
|
|
61
|
+
"gt",
|
|
62
|
+
"gte",
|
|
63
|
+
"matches",
|
|
64
|
+
"in",
|
|
65
|
+
"empty",
|
|
66
|
+
"exists"
|
|
67
|
+
]);
|
|
68
|
+
function resolvePath(predicate, path) {
|
|
69
|
+
if (path === "findings" || path.startsWith("findings.")) {
|
|
70
|
+
const rest = path === "findings" ? "" : path.slice("findings.".length);
|
|
71
|
+
if (rest === "") {
|
|
72
|
+
return predicate.findings.length;
|
|
73
|
+
}
|
|
74
|
+
if (rest.startsWith("severity.")) {
|
|
75
|
+
const level = rest.slice("severity.".length);
|
|
76
|
+
return predicate.findings.filter((f) => f.severity === level).length;
|
|
77
|
+
}
|
|
78
|
+
return predicate.findings.filter((f) => f.id === rest || f.id.startsWith(`${rest}-`)).length;
|
|
79
|
+
}
|
|
80
|
+
let cur = predicate;
|
|
81
|
+
for (const key of path.split(".")) {
|
|
82
|
+
if (cur == null || typeof cur !== "object") {
|
|
83
|
+
return void 0;
|
|
84
|
+
}
|
|
85
|
+
cur = cur[key];
|
|
86
|
+
}
|
|
87
|
+
return cur;
|
|
88
|
+
}
|
|
89
|
+
var isNum = (v) => typeof v === "number" && Number.isFinite(v);
|
|
90
|
+
var isEmpty = (v) => v == null || v === "" || v === 0 || Array.isArray(v) && v.length === 0 || typeof v === "object" && Object.keys(v).length === 0;
|
|
91
|
+
function applyOp(op, actual, value) {
|
|
92
|
+
const needNum = () => isNum(actual) && isNum(value) ? null : { error: `op '${op}' needs a numeric field and value (got ${JSON.stringify(actual)})` };
|
|
93
|
+
switch (op) {
|
|
94
|
+
case "exists":
|
|
95
|
+
return { pass: actual !== void 0 && actual !== null };
|
|
96
|
+
case "empty":
|
|
97
|
+
return { pass: isEmpty(actual) };
|
|
98
|
+
case "eq":
|
|
99
|
+
return { pass: actual === value };
|
|
100
|
+
case "ne":
|
|
101
|
+
return { pass: actual !== value };
|
|
102
|
+
case "lt":
|
|
103
|
+
return needNum() ?? { pass: actual < value };
|
|
104
|
+
case "lte":
|
|
105
|
+
return needNum() ?? { pass: actual <= value };
|
|
106
|
+
case "gt":
|
|
107
|
+
return needNum() ?? { pass: actual > value };
|
|
108
|
+
case "gte":
|
|
109
|
+
return needNum() ?? { pass: actual >= value };
|
|
110
|
+
case "matches": {
|
|
111
|
+
if (typeof actual !== "string" || typeof value !== "string") {
|
|
112
|
+
return { error: `op 'matches' needs a string field and a string pattern` };
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
return { pass: new RegExp(value).test(actual) };
|
|
116
|
+
} catch (e) {
|
|
117
|
+
return { error: `invalid regex '${value}': ${e.message}` };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
case "in":
|
|
121
|
+
return Array.isArray(value) ? { pass: value.includes(actual) } : { error: `op 'in' needs an array value` };
|
|
122
|
+
default:
|
|
123
|
+
return { error: `unknown op '${op}'` };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function validateAssertion(raw) {
|
|
127
|
+
if (!raw || typeof raw !== "object") {
|
|
128
|
+
return { error: "assertion must be an object" };
|
|
129
|
+
}
|
|
130
|
+
const r = raw;
|
|
131
|
+
if (typeof r.path !== "string" || !r.path) {
|
|
132
|
+
return { error: "assertion.path must be a non-empty string" };
|
|
133
|
+
}
|
|
134
|
+
if (typeof r.op !== "string" || !OPS.has(r.op)) {
|
|
135
|
+
return { error: `assertion.op '${String(r.op)}' is not one of ${[...OPS].join("|")}` };
|
|
136
|
+
}
|
|
137
|
+
if (r.severity !== void 0 && r.severity !== "error" && r.severity !== "warn") {
|
|
138
|
+
return { error: "assertion.severity must be 'error' or 'warn'" };
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
assertion: {
|
|
142
|
+
path: r.path,
|
|
143
|
+
op: r.op,
|
|
144
|
+
value: r.value,
|
|
145
|
+
severity: r.severity ?? "error"
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function loadAsserts(repoRoot) {
|
|
150
|
+
const path = join(repoRoot, ".receipts", "asserts.json");
|
|
151
|
+
let text;
|
|
152
|
+
try {
|
|
153
|
+
text = readFileSync(path, "utf8");
|
|
154
|
+
} catch {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
let parsed;
|
|
158
|
+
try {
|
|
159
|
+
parsed = JSON.parse(text);
|
|
160
|
+
} catch (e) {
|
|
161
|
+
throw new Error(`.receipts/asserts.json is not valid JSON: ${e.message}`);
|
|
162
|
+
}
|
|
163
|
+
const list = Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" && Array.isArray(parsed.asserts) ? parsed.asserts : null;
|
|
164
|
+
if (!list) {
|
|
165
|
+
throw new Error(".receipts/asserts.json must be an array or { asserts: [...] }");
|
|
166
|
+
}
|
|
167
|
+
const out = [];
|
|
168
|
+
for (const raw of list) {
|
|
169
|
+
const v = validateAssertion(raw);
|
|
170
|
+
if ("error" in v) {
|
|
171
|
+
throw new Error(`invalid assertion: ${v.error} (${JSON.stringify(raw)})`);
|
|
172
|
+
}
|
|
173
|
+
out.push(v.assertion);
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
function evaluateAsserts(receipt, asserts) {
|
|
178
|
+
return asserts.map((assertion) => {
|
|
179
|
+
const actual = resolvePath(receipt.predicate, assertion.path);
|
|
180
|
+
if (actual === void 0 && assertion.op !== "exists" && assertion.op !== "empty" && assertion.op !== "ne") {
|
|
181
|
+
return { assertion, pass: false, actual, error: `path '${assertion.path}' not found` };
|
|
182
|
+
}
|
|
183
|
+
const res = applyOp(assertion.op, actual, assertion.value);
|
|
184
|
+
return "error" in res ? { assertion, pass: false, actual, error: res.error } : { assertion, pass: res.pass, actual };
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
function assertExitCode(results) {
|
|
188
|
+
return results.some((r) => (!r.pass || r.error) && (r.assertion.severity ?? "error") === "error") ? 1 : 0;
|
|
189
|
+
}
|
|
190
|
+
function renderAssertResults(results) {
|
|
191
|
+
if (results.length === 0) {
|
|
192
|
+
return "receipts assert: no assertions (.receipts/asserts.json absent) \u2014 nothing to check.";
|
|
193
|
+
}
|
|
194
|
+
const lines = results.map((r) => {
|
|
195
|
+
const { path, op, value, severity } = r.assertion;
|
|
196
|
+
const expr = `${path} ${op}${value === void 0 ? "" : ` ${JSON.stringify(value)}`}`;
|
|
197
|
+
if (r.error) {
|
|
198
|
+
return ` \u26A0\uFE0F ERROR ${expr} \u2014 ${r.error}`;
|
|
199
|
+
}
|
|
200
|
+
const tag = r.pass ? " \u2705 PASS " : severity === "warn" ? " \u{1F7E1} WARN " : " \u274C FAIL ";
|
|
201
|
+
return `${tag} ${expr} (actual: ${JSON.stringify(r.actual)})`;
|
|
202
|
+
});
|
|
203
|
+
const failed = results.filter(
|
|
204
|
+
(r) => !r.pass && (r.assertion.severity ?? "error") === "error"
|
|
205
|
+
).length;
|
|
206
|
+
const warned = results.filter((r) => !r.pass && r.assertion.severity === "warn").length;
|
|
207
|
+
const passed = results.filter((r) => r.pass).length;
|
|
208
|
+
lines.push(
|
|
209
|
+
` ${passed} passed \xB7 ${failed} failed${warned ? ` \xB7 ${warned} warned` : ""} of ${results.length}`
|
|
210
|
+
);
|
|
211
|
+
return lines.join("\n");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/report/badge.ts
|
|
215
|
+
var SEV_ORDER = ["critical", "high", "medium", "low"];
|
|
216
|
+
function worstSeverity(findings) {
|
|
217
|
+
return SEV_ORDER.find((s) => findings.some((f) => f.severity === s));
|
|
218
|
+
}
|
|
219
|
+
function badgeEndpoint(predicate) {
|
|
220
|
+
const findings = predicate.findings ?? [];
|
|
221
|
+
const n = findings.length;
|
|
222
|
+
const worst = worstSeverity(findings);
|
|
223
|
+
const color = n === 0 ? "brightgreen" : worst === "critical" || worst === "high" ? "red" : worst === "medium" ? "orange" : "yellow";
|
|
224
|
+
const message = n === 0 ? "no findings" : `${n} finding${n === 1 ? "" : "s"}`;
|
|
225
|
+
return { schemaVersion: 1, label: "receipts", message, color };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/report/diff.ts
|
|
229
|
+
function findingKey(f) {
|
|
230
|
+
return `${f.title}\0${f.filePath ?? ""}`;
|
|
231
|
+
}
|
|
232
|
+
function scopeLabel(r) {
|
|
233
|
+
const s = r.predicate.scope;
|
|
234
|
+
if (!s || s.kind === "session") return "whole session";
|
|
235
|
+
if (s.kind === "branch") return `branch ${s.branch ?? "?"}`;
|
|
236
|
+
return `diff (${(s.files ?? []).length} files${s.base ? ` vs ${s.base}` : ""})`;
|
|
237
|
+
}
|
|
238
|
+
function diffReceipts(a, b) {
|
|
239
|
+
const ea = a.predicate.evidence;
|
|
240
|
+
const eb = b.predicate.evidence;
|
|
241
|
+
const count = (key, label, va, vb) => ({
|
|
242
|
+
key,
|
|
243
|
+
label,
|
|
244
|
+
a: va,
|
|
245
|
+
b: vb,
|
|
246
|
+
delta: vb - va
|
|
247
|
+
});
|
|
248
|
+
const counts = [
|
|
249
|
+
count("filesChanged", "files changed", ea.filesChanged, eb.filesChanged),
|
|
250
|
+
count("edits", "edits", ea.edits, eb.edits),
|
|
251
|
+
count("commands", "commands", ea.commands, eb.commands),
|
|
252
|
+
count("reads", "reads", ea.reads, eb.reads),
|
|
253
|
+
count("destructiveOps", "destructive ops", ea.destructiveOps, eb.destructiveOps),
|
|
254
|
+
count("tokens", "tokens", ea.tokens.total, eb.tokens.total)
|
|
255
|
+
];
|
|
256
|
+
const aKeys = new Set(a.predicate.findings.map(findingKey));
|
|
257
|
+
const bKeys = new Set(b.predicate.findings.map(findingKey));
|
|
258
|
+
const findingsAdded = b.predicate.findings.filter((f) => !aKeys.has(findingKey(f)));
|
|
259
|
+
const findingsRemoved = a.predicate.findings.filter((f) => !bKeys.has(findingKey(f)));
|
|
260
|
+
const findingsCommon = a.predicate.findings.filter((f) => bKeys.has(findingKey(f))).length;
|
|
261
|
+
return {
|
|
262
|
+
grade: { a: a.predicate.grade, b: b.predicate.grade },
|
|
263
|
+
counts,
|
|
264
|
+
cost: { a: ea.costUsd, b: eb.costUsd, delta: eb.costUsd - ea.costUsd },
|
|
265
|
+
testsObserved: { a: ea.testsRan, b: eb.testsRan },
|
|
266
|
+
findingsAdded,
|
|
267
|
+
findingsRemoved,
|
|
268
|
+
findingsCommon,
|
|
269
|
+
scope: { a: scopeLabel(a), b: scopeLabel(b) }
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
var SEV_ICON = { critical: "\u26D4", high: "\u26A0\uFE0F", medium: "\u{1F50D}", low: "\xB7" };
|
|
273
|
+
function signed(n) {
|
|
274
|
+
if (n === 0) return "\xB10";
|
|
275
|
+
return n > 0 ? `+${n}` : `${n}`;
|
|
276
|
+
}
|
|
277
|
+
function findingLines(fs) {
|
|
278
|
+
return fs.map((f) => {
|
|
279
|
+
const loc = f.filePath ? ` (\`${f.filePath}\`)` : "";
|
|
280
|
+
return ` ${SEV_ICON[f.severity] ?? "\xB7"} ${f.title}${loc}`;
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
function renderDiff(d) {
|
|
284
|
+
const out = [];
|
|
285
|
+
out.push("Receipt diff \u2014 A (baseline) \u2192 B (new)");
|
|
286
|
+
out.push("");
|
|
287
|
+
out.push(`Grade: ${d.grade.a} \u2192 ${d.grade.b}`);
|
|
288
|
+
out.push(`Scope: A ${d.scope.a} \xB7 B ${d.scope.b}`);
|
|
289
|
+
out.push("");
|
|
290
|
+
out.push("Evidence:");
|
|
291
|
+
for (const c of d.counts) {
|
|
292
|
+
out.push(` ${c.label}: ${c.a} \u2192 ${c.b} (${signed(c.delta)})`);
|
|
293
|
+
}
|
|
294
|
+
const cd = d.cost;
|
|
295
|
+
const costDelta = cd.delta === 0 ? "\xB10" : (cd.delta > 0 ? "+" : "-") + formatCostAlways(Math.abs(cd.delta));
|
|
296
|
+
out.push(` cost: ${formatCostAlways(cd.a)} \u2192 ${formatCostAlways(cd.b)} (${costDelta})`);
|
|
297
|
+
out.push(
|
|
298
|
+
` tokens (total): ${formatTokens(d.counts.find((c) => c.key === "tokens")?.a ?? 0)} \u2192 ${formatTokens(d.counts.find((c) => c.key === "tokens")?.b ?? 0)}`
|
|
299
|
+
);
|
|
300
|
+
const to = d.testsObserved;
|
|
301
|
+
const toStr = (v) => v ? "observed" : "not observed";
|
|
302
|
+
out.push(` test command: ${toStr(to.a)} \u2192 ${toStr(to.b)}${to.a !== to.b ? " (changed)" : ""}`);
|
|
303
|
+
out.push("");
|
|
304
|
+
out.push(
|
|
305
|
+
`Findings: ${d.findingsAdded.length} only in B \xB7 ${d.findingsRemoved.length} only in A \xB7 ${d.findingsCommon} in both`
|
|
306
|
+
);
|
|
307
|
+
if (d.findingsAdded.length) {
|
|
308
|
+
out.push("");
|
|
309
|
+
out.push("Only in B (new):");
|
|
310
|
+
out.push(...findingLines(d.findingsAdded));
|
|
311
|
+
}
|
|
312
|
+
if (d.findingsRemoved.length) {
|
|
313
|
+
out.push("");
|
|
314
|
+
out.push("Only in A (absent from B):");
|
|
315
|
+
out.push(...findingLines(d.findingsRemoved));
|
|
316
|
+
}
|
|
317
|
+
out.push("");
|
|
318
|
+
out.push("Deterministic \xB7 0 model calls \xB7 evidence, not judgement.");
|
|
319
|
+
return `${out.join("\n")}
|
|
320
|
+
`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/report/eval.ts
|
|
324
|
+
function firedCategories(findings) {
|
|
325
|
+
const keys = /* @__PURE__ */ new Set();
|
|
326
|
+
for (const f of findings) {
|
|
327
|
+
const c = checkForId(f.id);
|
|
328
|
+
if (c) {
|
|
329
|
+
keys.add(c.key);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return keys;
|
|
333
|
+
}
|
|
334
|
+
function summarizeFieldScan(rows) {
|
|
335
|
+
const label = new Map(CHECK_CATALOG.map((c) => [c.key, c.label]));
|
|
336
|
+
const cat = /* @__PURE__ */ new Map();
|
|
337
|
+
let flagged = 0;
|
|
338
|
+
for (const r of rows) {
|
|
339
|
+
if (r.categories.length) {
|
|
340
|
+
flagged++;
|
|
341
|
+
}
|
|
342
|
+
for (const key of new Set(r.categories)) {
|
|
343
|
+
cat.set(key, (cat.get(key) ?? 0) + 1);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const byCategory = [...cat.entries()].map(([key, sessions]) => ({ key, label: label.get(key) ?? key, sessions })).sort((a, b) => b.sessions - a.sessions || a.label.localeCompare(b.label));
|
|
347
|
+
return { scanned: rows.length, flagged, byCategory, rows: [...rows] };
|
|
348
|
+
}
|
|
349
|
+
function renderFieldScan(s) {
|
|
350
|
+
if (s.scanned === 0) {
|
|
351
|
+
return "No local agent sessions found to scan.\n";
|
|
352
|
+
}
|
|
353
|
+
const out = [];
|
|
354
|
+
out.push(`Receipts field scan \u2014 ${s.scanned} real local session${s.scanned === 1 ? "" : "s"}`);
|
|
355
|
+
out.push("");
|
|
356
|
+
const rate = Math.round(100 * s.flagged / s.scanned);
|
|
357
|
+
out.push(`Flagged: ${s.flagged}/${s.scanned} sessions (${rate}%) carried \u22651 merge-surface flag`);
|
|
358
|
+
out.push("");
|
|
359
|
+
if (s.byCategory.length === 0) {
|
|
360
|
+
out.push("No merge-surface category fired on any scanned session.");
|
|
361
|
+
} else {
|
|
362
|
+
out.push("By category (sessions flagged):");
|
|
363
|
+
for (const c of s.byCategory) {
|
|
364
|
+
out.push(` ${c.label.padEnd(26)} ${c.sessions}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
out.push(
|
|
368
|
+
"",
|
|
369
|
+
"A flag may be a true finding or a false positive \u2014 this is a flag rate you interpret,",
|
|
370
|
+
"not a verdict. Deterministic \xB7 0 model calls \xB7 reads only local sessions."
|
|
371
|
+
);
|
|
372
|
+
return `${out.join("\n")}
|
|
373
|
+
`;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/report/log.ts
|
|
377
|
+
import { readFileSync as readFileSync2, readdirSync } from "fs";
|
|
378
|
+
import { join as join2 } from "path";
|
|
379
|
+
var SEV_ICON2 = { critical: "\u26D4", high: "\u26A0\uFE0F", medium: "\u{1F50D}", low: "\xB7" };
|
|
380
|
+
var SEV_RANK = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
381
|
+
var NON_RECEIPT = /(?:^|\/)(?:asserts(?:\.example)?|sample)\.json$/i;
|
|
382
|
+
function scopeLabel2(scope) {
|
|
383
|
+
if (!scope || scope.kind === "session") return "session";
|
|
384
|
+
if (scope.kind === "branch") return `branch ${scope.branch ?? "?"}`;
|
|
385
|
+
return `diff ${(scope.files ?? []).length}f`;
|
|
386
|
+
}
|
|
387
|
+
function isoDate(ms) {
|
|
388
|
+
if (!ms || !Number.isFinite(ms)) return "\u2014".padEnd(10);
|
|
389
|
+
return new Date(ms).toISOString().slice(0, 10);
|
|
390
|
+
}
|
|
391
|
+
function loadReceiptHistory(dir) {
|
|
392
|
+
let files;
|
|
393
|
+
try {
|
|
394
|
+
files = readdirSync(dir).filter((f) => f.endsWith(".json") && !NON_RECEIPT.test(f));
|
|
395
|
+
} catch {
|
|
396
|
+
return [];
|
|
397
|
+
}
|
|
398
|
+
const entries = [];
|
|
399
|
+
for (const f of files) {
|
|
400
|
+
let input;
|
|
401
|
+
try {
|
|
402
|
+
input = JSON.parse(readFileSync2(join2(dir, f), "utf8"));
|
|
403
|
+
} catch {
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
const res = verifyBundle(input);
|
|
407
|
+
if (!res.ok || !res.receipt) continue;
|
|
408
|
+
const p = res.receipt.predicate;
|
|
409
|
+
const fs = p.findings ?? [];
|
|
410
|
+
let topSeverity;
|
|
411
|
+
for (const x of fs) {
|
|
412
|
+
if (!topSeverity || SEV_RANK[x.severity] < SEV_RANK[topSeverity]) topSeverity = x.severity;
|
|
413
|
+
}
|
|
414
|
+
entries.push({
|
|
415
|
+
name: f.replace(/\.json$/i, ""),
|
|
416
|
+
grade: p.grade,
|
|
417
|
+
costUsd: p.evidence.diffCostUsd ?? p.evidence.costUsd,
|
|
418
|
+
findings: fs.length,
|
|
419
|
+
topSeverity,
|
|
420
|
+
scope: scopeLabel2(p.scope),
|
|
421
|
+
endedAt: p.session?.endedAt
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
entries.sort((a, b) => (b.endedAt ?? 0) - (a.endedAt ?? 0) || a.name.localeCompare(b.name));
|
|
425
|
+
return entries;
|
|
426
|
+
}
|
|
427
|
+
function renderLog(entries, total = entries.length) {
|
|
428
|
+
if (!entries.length) return "No committed receipts found in .receipts/.\n";
|
|
429
|
+
const rows = entries.map((e) => {
|
|
430
|
+
const findings = e.findings === 0 ? "0" : `${e.findings} ${e.topSeverity ? SEV_ICON2[e.topSeverity] : ""}`.trim();
|
|
431
|
+
return {
|
|
432
|
+
date: isoDate(e.endedAt),
|
|
433
|
+
grade: e.grade,
|
|
434
|
+
cost: formatCostAlways(e.costUsd),
|
|
435
|
+
findings,
|
|
436
|
+
scope: e.scope,
|
|
437
|
+
name: e.name
|
|
438
|
+
};
|
|
439
|
+
});
|
|
440
|
+
const w = (key, head2) => Math.max(head2.length, ...rows.map((r) => r[key].length));
|
|
441
|
+
const wDate = w("date", "DATE");
|
|
442
|
+
const wGrade = Math.max(5, ...rows.map((r) => r.grade.length));
|
|
443
|
+
const wCost = w("cost", "COST");
|
|
444
|
+
const wFind = w("findings", "FINDINGS");
|
|
445
|
+
const wScope = w("scope", "SCOPE");
|
|
446
|
+
const head = `${"DATE".padEnd(wDate)} ${"GRADE".padEnd(wGrade)} ${"COST".padEnd(wCost)} ${"FINDINGS".padEnd(wFind)} ${"SCOPE".padEnd(wScope)} RECEIPT`;
|
|
447
|
+
const lines = rows.map(
|
|
448
|
+
(r) => `${r.date.padEnd(wDate)} ${r.grade.padEnd(wGrade)} ${r.cost.padEnd(wCost)} ${r.findings.padEnd(wFind)} ${r.scope.padEnd(wScope)} ${r.name}`
|
|
449
|
+
);
|
|
450
|
+
const header = `Receipt history \u2014 .receipts/ (${total} receipt${total === 1 ? "" : "s"}${entries.length < total ? `, showing ${entries.length}` : ""})`;
|
|
451
|
+
return `${header}
|
|
452
|
+
|
|
453
|
+
${head}
|
|
454
|
+
${lines.join("\n")}
|
|
455
|
+
|
|
456
|
+
<cost = this change's cost (diff-scoped where available) \xB7 grade is each receipt's own recorded grade \xB7 evidence, not judgement>
|
|
457
|
+
`;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/report/sarif.ts
|
|
461
|
+
var INFO_URI = "https://github.com/AltimateAI/altimate-receipts";
|
|
462
|
+
function levelOf(severity) {
|
|
463
|
+
if (severity === "critical" || severity === "high") return "error";
|
|
464
|
+
if (severity === "medium") return "warning";
|
|
465
|
+
return "note";
|
|
466
|
+
}
|
|
467
|
+
function ruleIdOf(id) {
|
|
468
|
+
const check = checkForId(id);
|
|
469
|
+
if (check) return check.key;
|
|
470
|
+
return id.replace(/-(?:tool|gen|span|s)-?\d.*$/i, "") || id;
|
|
471
|
+
}
|
|
472
|
+
function repoRelative(filePath, scopeFiles) {
|
|
473
|
+
const base = filePath.split("/").pop() ?? filePath;
|
|
474
|
+
for (const sf of scopeFiles) {
|
|
475
|
+
if (sf === filePath || sf.endsWith(`/${filePath}`) || filePath.endsWith(`/${sf}`)) return sf;
|
|
476
|
+
if ((sf.split("/").pop() ?? sf) === base) return sf;
|
|
477
|
+
}
|
|
478
|
+
return base;
|
|
479
|
+
}
|
|
480
|
+
function toSarif(receipt) {
|
|
481
|
+
const p = receipt.predicate;
|
|
482
|
+
const scopeFiles = p.scope?.files ?? [];
|
|
483
|
+
const findings = p.findings ?? [];
|
|
484
|
+
const ruleOrder = [];
|
|
485
|
+
const rules = /* @__PURE__ */ new Map();
|
|
486
|
+
const sevRank = { error: 0, warning: 1, note: 2 };
|
|
487
|
+
const results = findings.map((f) => {
|
|
488
|
+
const ruleId = ruleIdOf(f.id);
|
|
489
|
+
const level = levelOf(f.severity);
|
|
490
|
+
const existing = rules.get(ruleId);
|
|
491
|
+
if (!existing) {
|
|
492
|
+
ruleOrder.push(ruleId);
|
|
493
|
+
rules.set(ruleId, {
|
|
494
|
+
id: ruleId,
|
|
495
|
+
name: checkForId(f.id)?.label ?? ruleId,
|
|
496
|
+
shortDescription: { text: checkForId(f.id)?.label ?? ruleId },
|
|
497
|
+
defaultConfiguration: { level },
|
|
498
|
+
helpUri: INFO_URI
|
|
499
|
+
});
|
|
500
|
+
} else if (sevRank[level] < sevRank[existing.defaultConfiguration.level]) {
|
|
501
|
+
existing.defaultConfiguration.level = level;
|
|
502
|
+
}
|
|
503
|
+
const uri = f.filePath ? repoRelative(f.filePath, scopeFiles) : void 0;
|
|
504
|
+
const result = {
|
|
505
|
+
ruleId,
|
|
506
|
+
ruleIndex: ruleOrder.indexOf(ruleId),
|
|
507
|
+
level,
|
|
508
|
+
message: { text: f.detail ? `${f.title} \u2014 ${f.detail}` : f.title },
|
|
509
|
+
// Stable across commits: rule + path, never the volatile span id.
|
|
510
|
+
partialFingerprints: { receiptsId: `${ruleId}::${uri ?? "session"}` }
|
|
511
|
+
};
|
|
512
|
+
if (uri) {
|
|
513
|
+
result.locations = [
|
|
514
|
+
{
|
|
515
|
+
physicalLocation: {
|
|
516
|
+
artifactLocation: { uri },
|
|
517
|
+
...f.line ? { region: { startLine: f.line } } : {}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
];
|
|
521
|
+
}
|
|
522
|
+
return result;
|
|
523
|
+
});
|
|
524
|
+
const ruleList = ruleOrder.map((id) => rules.get(id));
|
|
525
|
+
return {
|
|
526
|
+
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
527
|
+
version: "2.1.0",
|
|
528
|
+
runs: [
|
|
529
|
+
{
|
|
530
|
+
tool: {
|
|
531
|
+
driver: {
|
|
532
|
+
name: "Receipts",
|
|
533
|
+
informationUri: INFO_URI,
|
|
534
|
+
version: p.generator?.version,
|
|
535
|
+
rules: ruleList
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
results
|
|
539
|
+
}
|
|
540
|
+
]
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// src/report/stats.ts
|
|
545
|
+
import { readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
|
|
546
|
+
import { join as join3 } from "path";
|
|
547
|
+
var NON_RECEIPT2 = /(?:^|\/)(?:asserts(?:\.example)?|sample)\.json$/i;
|
|
548
|
+
function computeStats(dir) {
|
|
549
|
+
let files;
|
|
550
|
+
try {
|
|
551
|
+
files = readdirSync2(dir).filter((f) => f.endsWith(".json") && !NON_RECEIPT2.test(f));
|
|
552
|
+
} catch {
|
|
553
|
+
files = [];
|
|
554
|
+
}
|
|
555
|
+
const grades = { A: 0, B: 0, C: 0, F: 0 };
|
|
556
|
+
const cat = /* @__PURE__ */ new Map();
|
|
557
|
+
let receipts = 0;
|
|
558
|
+
let skipped = 0;
|
|
559
|
+
let flagged = 0;
|
|
560
|
+
let totalFindings = 0;
|
|
561
|
+
for (const f of files) {
|
|
562
|
+
let input;
|
|
563
|
+
try {
|
|
564
|
+
input = JSON.parse(readFileSync3(join3(dir, f), "utf8"));
|
|
565
|
+
} catch {
|
|
566
|
+
skipped++;
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
const res = verifyBundle(input);
|
|
570
|
+
if (!res.ok || !res.receipt) {
|
|
571
|
+
skipped++;
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
receipts++;
|
|
575
|
+
const p = res.receipt.predicate;
|
|
576
|
+
grades[p.grade] = (grades[p.grade] ?? 0) + 1;
|
|
577
|
+
const findings = p.findings ?? [];
|
|
578
|
+
if (findings.length) flagged++;
|
|
579
|
+
totalFindings += findings.length;
|
|
580
|
+
const seen = /* @__PURE__ */ new Set();
|
|
581
|
+
for (const finding of findings) {
|
|
582
|
+
const c = checkForId(finding.id);
|
|
583
|
+
if (!c) continue;
|
|
584
|
+
const row = cat.get(c.key) ?? { icon: c.icon, label: c.label, findings: 0, receipts: 0 };
|
|
585
|
+
row.findings++;
|
|
586
|
+
if (!seen.has(c.key)) {
|
|
587
|
+
row.receipts++;
|
|
588
|
+
seen.add(c.key);
|
|
589
|
+
}
|
|
590
|
+
cat.set(c.key, row);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const byCategory = [...cat.entries()].map(([key, v]) => ({ key, ...v })).sort((a, b) => b.findings - a.findings || a.label.localeCompare(b.label));
|
|
594
|
+
return { receipts, skipped, flagged, grades, byCategory, totalFindings };
|
|
595
|
+
}
|
|
596
|
+
function renderStats(s) {
|
|
597
|
+
if (s.receipts === 0) return "No committed receipts found in .receipts/.\n";
|
|
598
|
+
const out = [];
|
|
599
|
+
out.push(
|
|
600
|
+
`Receipts scoreboard \u2014 ${s.receipts} receipt${s.receipts === 1 ? "" : "s"} in .receipts/`
|
|
601
|
+
);
|
|
602
|
+
out.push("");
|
|
603
|
+
const pct = (n) => `${Math.round(100 * n / s.receipts)}%`;
|
|
604
|
+
out.push(
|
|
605
|
+
`Grades: \u{1F7E2} A ${s.grades.A} \xB7 \u{1F7E1} B ${s.grades.B} \xB7 \u{1F7E0} C ${s.grades.C} \xB7 \u{1F534} F ${s.grades.F}`
|
|
606
|
+
);
|
|
607
|
+
out.push(`Flagged: ${s.flagged}/${s.receipts} receipts (${pct(s.flagged)}) carried \u22651 finding`);
|
|
608
|
+
out.push("");
|
|
609
|
+
if (s.byCategory.length === 0) {
|
|
610
|
+
out.push("Problems caught: none \u2014 every committed receipt was clean.");
|
|
611
|
+
} else {
|
|
612
|
+
out.push(
|
|
613
|
+
`Problems caught (${s.totalFindings} findings across ${s.byCategory.length} categories):`
|
|
614
|
+
);
|
|
615
|
+
for (const c of s.byCategory) {
|
|
616
|
+
out.push(
|
|
617
|
+
` ${c.icon} ${c.label.padEnd(26)} ${c.findings} finding${c.findings === 1 ? "" : "s"} in ${c.receipts} receipt${c.receipts === 1 ? "" : "s"}`
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (s.skipped) {
|
|
622
|
+
out.push("", `<${s.skipped} non-receipt/invalid file${s.skipped === 1 ? "" : "s"} skipped>`);
|
|
623
|
+
}
|
|
624
|
+
out.push("", "Deterministic \xB7 0 model calls \xB7 counts of recorded receipts, not a verdict.");
|
|
625
|
+
return `${out.join("\n")}
|
|
626
|
+
`;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// src/share/handoff.ts
|
|
630
|
+
var OPEN_IDS = /* @__PURE__ */ new Set(["unverified-change", "claimed-commit-none", "fake-green"]);
|
|
631
|
+
function buildHandoff(receipt, findings) {
|
|
632
|
+
const all = [...findings.main, ...findings.minor];
|
|
633
|
+
const ev = receipt.predicate.evidence;
|
|
634
|
+
return {
|
|
635
|
+
subject: receipt.subject[0]?.digest.sha256 ?? "",
|
|
636
|
+
grade: receipt.predicate.grade,
|
|
637
|
+
done: {
|
|
638
|
+
filesChanged: ev.filesChanged,
|
|
639
|
+
edits: ev.edits,
|
|
640
|
+
commands: ev.commands,
|
|
641
|
+
reads: ev.reads,
|
|
642
|
+
testsRan: ev.testsRan
|
|
643
|
+
},
|
|
644
|
+
open: all.filter((f) => OPEN_IDS.has(f.id)).map((f) => redact(f.title)),
|
|
645
|
+
risk: all.filter((f) => f.severity === "high" || f.severity === "critical").map((f) => ({ severity: f.severity, title: redact(f.title) })),
|
|
646
|
+
next: all.map((f) => f.fixPrompt).filter((p) => !!p).map(redact),
|
|
647
|
+
prevent: all.map((f) => f.guardrailRule).filter((g) => !!g).map(redact),
|
|
648
|
+
verify: `receipts rederive <transcript> # reproduces subject ${(receipt.subject[0]?.digest.sha256 ?? "").slice(0, 12)}\u2026`
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
function renderHandoffMarkdown(h) {
|
|
652
|
+
const lines = [];
|
|
653
|
+
lines.push(`# Handoff \u2014 Grade ${h.grade}`, "");
|
|
654
|
+
lines.push(
|
|
655
|
+
"## \u2705 Done",
|
|
656
|
+
`${h.done.filesChanged} files changed \xB7 ${h.done.edits} edits \xB7 ${h.done.commands} commands \xB7 ${h.done.reads} reads \xB7 tests ran: ${h.done.testsRan ? "yes" : "no"}`,
|
|
657
|
+
""
|
|
658
|
+
);
|
|
659
|
+
if (h.open.length) {
|
|
660
|
+
lines.push("## \u{1F6A7} Open", ...h.open.map((o) => `- ${o}`), "");
|
|
661
|
+
}
|
|
662
|
+
if (h.risk.length) {
|
|
663
|
+
lines.push(
|
|
664
|
+
"## \u{1F6E1}\uFE0F Risk",
|
|
665
|
+
...h.risk.map((r) => `- **${r.severity.toUpperCase()}** ${r.title}`),
|
|
666
|
+
""
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
if (h.next.length) {
|
|
670
|
+
lines.push("## \u23ED\uFE0F Next", ...h.next.map((n) => `- ${n}`), "");
|
|
671
|
+
}
|
|
672
|
+
if (h.prevent.length) {
|
|
673
|
+
lines.push("## \u{1F9F7} Prevent", ...h.prevent.map((p) => `- ${p}`), "");
|
|
674
|
+
}
|
|
675
|
+
lines.push("## \u{1F50E} Verify", `\`${h.verify}\``, "");
|
|
676
|
+
return `${lines.join("\n").trimEnd()}
|
|
677
|
+
`;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// src/cli.ts
|
|
681
|
+
var HELP = `
|
|
682
|
+
\u{1F9FE} receipts \u2014 proof, not vibes
|
|
683
|
+
|
|
684
|
+
A deterministic, cross-agent Report Card of what your coding agent actually did.
|
|
685
|
+
Reads the agent's own transcript locally. No account, no upload, no model calls.
|
|
686
|
+
|
|
687
|
+
Usage
|
|
688
|
+
receipts [selector] Print the Agent Report Card for a session
|
|
689
|
+
receipts --list List recent sessions (then: receipts <n>)
|
|
690
|
+
receipts --json [selector] Emit the Receipt object (in-toto Statement)
|
|
691
|
+
receipts --share [selector] Print a redacted, paste-ready Markdown summary
|
|
692
|
+
receipts --handoff [selector] Print a verifiable handoff brief (done/open/risk/next/verify)
|
|
693
|
+
receipts guardrails [sel] Paste-ready prevention rules (AGENTS.md / CLAUDE.md)
|
|
694
|
+
from the findings (--last N to aggregate sessions)
|
|
695
|
+
receipts trends Cross-session digest: grades, recurring findings,
|
|
696
|
+
and evidence over your last N sessions (default 10)
|
|
697
|
+
receipts pr Write this PR's redacted Receipt to .receipts/
|
|
698
|
+
(diff-scoped: only this change's findings)
|
|
699
|
+
receipts envelope <receipt> Wrap a Receipt as an unsigned DSSE envelope
|
|
700
|
+
receipts verify <bundle> Verify a Receipt (+ --transcript to prove fidelity)
|
|
701
|
+
receipts diff [<a> <b>] What changed between two Receipts (no args: last two; --json)
|
|
702
|
+
receipts log [dir] List the committed receipts in .receipts/ (--last N)
|
|
703
|
+
receipts stats [dir] Dogfooding scoreboard: how often it ran + what it caught
|
|
704
|
+
receipts eval Flag-rate of the detectors over your real local sessions (--last N, --json)
|
|
705
|
+
receipts badge [receipt] shields.io endpoint JSON for a README/PR badge (--out f)
|
|
706
|
+
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)
|
|
708
|
+
receipts rederive <file> Reproduce the canonical Receipt from a transcript
|
|
709
|
+
receipts assert [selector] Check the receipt against committed .receipts/asserts.json (CI gate)
|
|
710
|
+
receipts mcp Start the MCP server (stdio) for IDEs/agents
|
|
711
|
+
|
|
712
|
+
selector: a list number (e.g. 3), a session id, or part of the title.
|
|
713
|
+
With no selector, the most recent session is used.
|
|
714
|
+
|
|
715
|
+
Options
|
|
716
|
+
--list List recent sessions and exit
|
|
717
|
+
--agent <name> Limit to one agent: claude-code | codex | cursor | openclaw
|
|
718
|
+
--json Emit the Receipt as JSON (no card)
|
|
719
|
+
--compact With --json, emit canonical (sorted, minified) JSON
|
|
720
|
+
--share Emit a redacted Markdown summary for sharing
|
|
721
|
+
--redact With --json, redact secrets from the Receipt
|
|
722
|
+
--out <path> With --json, write to a file instead of stdout
|
|
723
|
+
--base <ref> With pr, the diff base to scope against (default: main)
|
|
724
|
+
--branch <name> With pr, fall back to a branch slice (default: current)
|
|
725
|
+
--whole-session With pr, use the whole session (no scoping)
|
|
726
|
+
--last <n> With guardrails / trends, span the last n sessions
|
|
727
|
+
--copy Also copy --share / --json output to the clipboard
|
|
728
|
+
--no-color Disable ANSI color (also honors NO_COLOR)
|
|
729
|
+
-v, --version Print the version and exit
|
|
730
|
+
-h, --help Print this help and exit
|
|
731
|
+
|
|
732
|
+
Signing is done in CI by the "Verified-by: Receipts" GitHub Action (Sigstore
|
|
733
|
+
keyless via GitHub Artifact Attestations). See docs/verified-by.md.
|
|
734
|
+
|
|
735
|
+
Receipts reports what an agent DID \u2014 not whether it was correct. Your tests are
|
|
736
|
+
the oracle for success. Evidence, not judgement.
|
|
737
|
+
|
|
738
|
+
Note: a Receipt can contain titles, file paths, and command snippets from the
|
|
739
|
+
transcript (possibly secrets). Plain --json is local only; use --share or
|
|
740
|
+
--json --redact to mask secrets before the output leaves your machine.
|
|
741
|
+
|
|
742
|
+
Docs: https://github.com/AltimateAI/altimate-receipts
|
|
743
|
+
`;
|
|
744
|
+
var COMMANDS = /* @__PURE__ */ new Set([
|
|
745
|
+
"envelope",
|
|
746
|
+
"verify",
|
|
747
|
+
"pr",
|
|
748
|
+
"mcp",
|
|
749
|
+
"rederive",
|
|
750
|
+
"guardrails",
|
|
751
|
+
"trends",
|
|
752
|
+
"diff",
|
|
753
|
+
"log",
|
|
754
|
+
"stats",
|
|
755
|
+
"eval",
|
|
756
|
+
"badge",
|
|
757
|
+
"sarif",
|
|
758
|
+
"init",
|
|
759
|
+
"assert"
|
|
760
|
+
]);
|
|
761
|
+
function parseArgs(argv) {
|
|
762
|
+
const args = argv.slice(2);
|
|
763
|
+
const parsed = {
|
|
764
|
+
help: false,
|
|
765
|
+
version: false,
|
|
766
|
+
list: false,
|
|
767
|
+
json: false,
|
|
768
|
+
compact: false,
|
|
769
|
+
share: false,
|
|
770
|
+
handoff: false,
|
|
771
|
+
redact: false,
|
|
772
|
+
copy: false,
|
|
773
|
+
wholeSession: false,
|
|
774
|
+
color: !process.env.NO_COLOR && process.stdout.isTTY === true
|
|
775
|
+
};
|
|
776
|
+
const positionals = [];
|
|
777
|
+
for (let i = 0; i < args.length; i++) {
|
|
778
|
+
const a = args[i];
|
|
779
|
+
if (a === "-h" || a === "--help") {
|
|
780
|
+
parsed.help = true;
|
|
781
|
+
} else if (a === "-v" || a === "--version") {
|
|
782
|
+
parsed.version = true;
|
|
783
|
+
} else if (a === "--list") {
|
|
784
|
+
parsed.list = true;
|
|
785
|
+
} else if (a === "--json") {
|
|
786
|
+
parsed.json = true;
|
|
787
|
+
} else if (a === "--compact") {
|
|
788
|
+
parsed.compact = true;
|
|
789
|
+
} else if (a === "--share") {
|
|
790
|
+
parsed.share = true;
|
|
791
|
+
} else if (a === "--handoff") {
|
|
792
|
+
parsed.handoff = true;
|
|
793
|
+
} else if (a === "--redact") {
|
|
794
|
+
parsed.redact = true;
|
|
795
|
+
} else if (a === "--copy") {
|
|
796
|
+
parsed.copy = true;
|
|
797
|
+
} else if (a === "--out") {
|
|
798
|
+
parsed.out = args[++i];
|
|
799
|
+
} else if (a === "--branch") {
|
|
800
|
+
parsed.branchScope = args[++i];
|
|
801
|
+
} else if (a === "--base") {
|
|
802
|
+
parsed.base = args[++i];
|
|
803
|
+
} else if (a === "--transcript") {
|
|
804
|
+
parsed.transcript = args[++i];
|
|
805
|
+
} else if (a === "--last") {
|
|
806
|
+
const n = Number(args[++i]);
|
|
807
|
+
if (Number.isFinite(n) && n > 0) {
|
|
808
|
+
parsed.last = Math.floor(n);
|
|
809
|
+
}
|
|
810
|
+
} else if (a === "--whole-session") {
|
|
811
|
+
parsed.wholeSession = true;
|
|
812
|
+
} else if (a === "--agent") {
|
|
813
|
+
const next = args[i + 1];
|
|
814
|
+
if (next && agentIds().includes(next)) {
|
|
815
|
+
parsed.agent = next;
|
|
816
|
+
i++;
|
|
817
|
+
}
|
|
818
|
+
} else if (a === "--no-color") {
|
|
819
|
+
parsed.color = false;
|
|
820
|
+
} else if (a === "--color") {
|
|
821
|
+
parsed.color = true;
|
|
822
|
+
} else if (!a.startsWith("-")) {
|
|
823
|
+
positionals.push(a);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
if (positionals[0] && COMMANDS.has(positionals[0])) {
|
|
827
|
+
parsed.command = positionals[0];
|
|
828
|
+
parsed.file = positionals[1];
|
|
829
|
+
parsed.second = positionals[2];
|
|
830
|
+
} else {
|
|
831
|
+
parsed.selector = positionals[0];
|
|
832
|
+
}
|
|
833
|
+
return parsed;
|
|
834
|
+
}
|
|
835
|
+
async function run(argv) {
|
|
836
|
+
const args = parseArgs(argv);
|
|
837
|
+
if (args.version) {
|
|
838
|
+
process.stdout.write(`${getVersion()}
|
|
839
|
+
`);
|
|
840
|
+
return 0;
|
|
841
|
+
}
|
|
842
|
+
if (args.help) {
|
|
843
|
+
process.stdout.write(`${HELP}
|
|
844
|
+
`);
|
|
845
|
+
return 0;
|
|
846
|
+
}
|
|
847
|
+
if (args.command === "envelope") {
|
|
848
|
+
return runEnvelope(args.file);
|
|
849
|
+
}
|
|
850
|
+
if (args.command === "verify") {
|
|
851
|
+
return runVerify(args.file, { transcript: args.transcript, branch: args.branchScope });
|
|
852
|
+
}
|
|
853
|
+
if (args.command === "diff") {
|
|
854
|
+
return runDiff(args.file, args.second, { json: args.json, out: args.out });
|
|
855
|
+
}
|
|
856
|
+
if (args.command === "log") {
|
|
857
|
+
return runLog(args.file, { json: args.json, last: args.last, out: args.out });
|
|
858
|
+
}
|
|
859
|
+
if (args.command === "stats") {
|
|
860
|
+
return runStats(args.file, { json: args.json, out: args.out });
|
|
861
|
+
}
|
|
862
|
+
if (args.command === "eval") {
|
|
863
|
+
return runEval({ json: args.json, limit: args.last, agent: args.agent });
|
|
864
|
+
}
|
|
865
|
+
if (args.command === "badge") {
|
|
866
|
+
return runBadge(args.file, { out: args.out });
|
|
867
|
+
}
|
|
868
|
+
if (args.command === "sarif") {
|
|
869
|
+
return runSarif(args.file, { out: args.out });
|
|
870
|
+
}
|
|
871
|
+
if (args.command === "init") {
|
|
872
|
+
return runInit();
|
|
873
|
+
}
|
|
874
|
+
if (args.command === "rederive") {
|
|
875
|
+
return runRederive(args.file, {
|
|
876
|
+
branch: args.branchScope,
|
|
877
|
+
redact: args.redact,
|
|
878
|
+
compact: args.compact
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
if (args.command === "guardrails") {
|
|
882
|
+
return runGuardrails({
|
|
883
|
+
selector: args.file,
|
|
884
|
+
agent: args.agent,
|
|
885
|
+
last: args.last,
|
|
886
|
+
out: args.out,
|
|
887
|
+
copy: args.copy,
|
|
888
|
+
json: args.json
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
if (args.command === "trends") {
|
|
892
|
+
return runTrends({
|
|
893
|
+
selector: args.file,
|
|
894
|
+
agent: args.agent,
|
|
895
|
+
last: args.last,
|
|
896
|
+
out: args.out,
|
|
897
|
+
copy: args.copy,
|
|
898
|
+
json: args.json,
|
|
899
|
+
color: args.color
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
if (args.command === "pr") {
|
|
903
|
+
return runPr({
|
|
904
|
+
out: args.out,
|
|
905
|
+
branch: args.branchScope,
|
|
906
|
+
base: args.base,
|
|
907
|
+
wholeSession: args.wholeSession
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
if (args.command === "assert") {
|
|
911
|
+
return runAssert({ selector: args.file, agent: args.agent, json: args.json });
|
|
912
|
+
}
|
|
913
|
+
if (args.command === "mcp") {
|
|
914
|
+
const { startStdio } = await import("./mcp/server.js");
|
|
915
|
+
await startStdio();
|
|
916
|
+
await new Promise((resolve) => {
|
|
917
|
+
process.stdin.once("close", resolve);
|
|
918
|
+
process.stdin.once("end", resolve);
|
|
919
|
+
});
|
|
920
|
+
return 0;
|
|
921
|
+
}
|
|
922
|
+
if (!await anyDetected()) {
|
|
923
|
+
process.stderr.write(
|
|
924
|
+
`No agent transcripts found under ${rootsHint()}.
|
|
925
|
+
Run a coding-agent session first, then try again.
|
|
926
|
+
`
|
|
927
|
+
);
|
|
928
|
+
return 1;
|
|
929
|
+
}
|
|
930
|
+
const sessions = await listSessions(args.agent);
|
|
931
|
+
if (args.list) {
|
|
932
|
+
process.stdout.write(renderList(sessions, { color: args.color }));
|
|
933
|
+
return 0;
|
|
934
|
+
}
|
|
935
|
+
const summary = selectSummary(sessions, args.selector);
|
|
936
|
+
if (!summary) {
|
|
937
|
+
process.stderr.write(
|
|
938
|
+
args.selector ? `No session matched "${args.selector}". Try \`receipts --list\`.
|
|
939
|
+
` : "No sessions found.\n"
|
|
940
|
+
);
|
|
941
|
+
return 1;
|
|
942
|
+
}
|
|
943
|
+
const session = await loadSession(summary);
|
|
944
|
+
if (!session) {
|
|
945
|
+
process.stderr.write(`Could not read session: ${summary.id}
|
|
946
|
+
`);
|
|
947
|
+
return 1;
|
|
948
|
+
}
|
|
949
|
+
const derived = deriveSpans(session);
|
|
950
|
+
const findings = deriveFindings(derived);
|
|
951
|
+
if (args.json) {
|
|
952
|
+
let receipt = await buildReceipt(session, derived, findings);
|
|
953
|
+
if (args.redact) {
|
|
954
|
+
receipt = redactReceipt(receipt);
|
|
955
|
+
}
|
|
956
|
+
const out = args.compact ? canonicalize(receipt) : JSON.stringify(receipt, null, 2);
|
|
957
|
+
if (args.out) {
|
|
958
|
+
writeFileSync(args.out, `${out}
|
|
959
|
+
`);
|
|
960
|
+
process.stderr.write(`Receipt written to ${args.out}
|
|
961
|
+
`);
|
|
962
|
+
return 0;
|
|
963
|
+
}
|
|
964
|
+
emit(out, args.copy, "Receipt");
|
|
965
|
+
return 0;
|
|
966
|
+
}
|
|
967
|
+
if (args.share) {
|
|
968
|
+
const md = renderShareMarkdown({ summary, session, derived, findings });
|
|
969
|
+
emit(md.trimEnd(), args.copy, "Shareable summary");
|
|
970
|
+
return 0;
|
|
971
|
+
}
|
|
972
|
+
if (args.handoff) {
|
|
973
|
+
const receipt = redactReceipt(await buildReceipt(session, derived, findings));
|
|
974
|
+
const md = renderHandoffMarkdown(buildHandoff(receipt, findings));
|
|
975
|
+
emit(md.trimEnd(), args.copy, "Handoff");
|
|
976
|
+
return 0;
|
|
977
|
+
}
|
|
978
|
+
process.stdout.write(renderCard({ summary, derived, findings }, { color: args.color }));
|
|
979
|
+
return 0;
|
|
980
|
+
}
|
|
981
|
+
function git(args) {
|
|
982
|
+
const r = spawnSync("git", args, { encoding: "utf8" });
|
|
983
|
+
return r.status === 0 ? r.stdout.trim() : "";
|
|
984
|
+
}
|
|
985
|
+
var PR_SELECT_SCAN = 150;
|
|
986
|
+
function diffOverlap(derived, files) {
|
|
987
|
+
return derived.filesChanged.filter((f) => inDiff(f.path, files)).length;
|
|
988
|
+
}
|
|
989
|
+
async function pickForDiff(all, branch, repoRoot, files) {
|
|
990
|
+
const load = async (sum) => {
|
|
991
|
+
const session = await loadSession(sum);
|
|
992
|
+
if (!session) {
|
|
993
|
+
return null;
|
|
994
|
+
}
|
|
995
|
+
return { summary: sum, session, derived: deriveSpans(session) };
|
|
996
|
+
};
|
|
997
|
+
const primarySum = selectForBranch(all, branch, repoRoot);
|
|
998
|
+
const primary = primarySum ? await load(primarySum) : null;
|
|
999
|
+
if (primary && diffOverlap(primary.derived, files) > 0) {
|
|
1000
|
+
return primary;
|
|
1001
|
+
}
|
|
1002
|
+
let best = null;
|
|
1003
|
+
let bestScore = 0;
|
|
1004
|
+
const recent = all.filter((s) => inRepo(s.projectPath, repoRoot)).slice(0, PR_SELECT_SCAN);
|
|
1005
|
+
for (const sum of recent) {
|
|
1006
|
+
const cand = await load(sum);
|
|
1007
|
+
if (!cand) {
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
const score = diffOverlap(cand.derived, files);
|
|
1011
|
+
if (score > bestScore) {
|
|
1012
|
+
best = cand;
|
|
1013
|
+
bestScore = score;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return best ?? primary;
|
|
1017
|
+
}
|
|
1018
|
+
async function runPr(opts) {
|
|
1019
|
+
const branch = opts.branch || git(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
1020
|
+
const repoRoot = git(["rev-parse", "--show-toplevel"]);
|
|
1021
|
+
if (!branch || branch === "HEAD") {
|
|
1022
|
+
process.stderr.write("receipts pr: not on a git branch (use --branch <name>).\n");
|
|
1023
|
+
return 1;
|
|
1024
|
+
}
|
|
1025
|
+
const diff = opts.wholeSession ? null : changedFiles(opts.base);
|
|
1026
|
+
const all = await listSessions();
|
|
1027
|
+
const picked = diff ? await pickForDiff(all, branch, repoRoot || void 0, diff.files) : await (async () => {
|
|
1028
|
+
const sum = selectForBranch(all, branch, repoRoot || void 0);
|
|
1029
|
+
return sum ? { summary: sum, session: await loadSession(sum), derived: null } : null;
|
|
1030
|
+
})();
|
|
1031
|
+
if (!picked || !picked.session) {
|
|
1032
|
+
process.stderr.write(
|
|
1033
|
+
`receipts pr: no agent session found for branch "${branch}" in this repo.
|
|
1034
|
+
Build the branch with a coding agent first, or run \`receipts --list\`.
|
|
1035
|
+
`
|
|
1036
|
+
);
|
|
1037
|
+
return 1;
|
|
1038
|
+
}
|
|
1039
|
+
const { summary, session } = picked;
|
|
1040
|
+
let scopedSession = session;
|
|
1041
|
+
let derived = picked.derived ?? deriveSpans(session);
|
|
1042
|
+
let findings = deriveFindings(derived);
|
|
1043
|
+
let scope = { kind: "session" };
|
|
1044
|
+
let scopeNote = "whole session";
|
|
1045
|
+
if (diff) {
|
|
1046
|
+
const sd = applyDiffScope(derived, findings, diff.files);
|
|
1047
|
+
derived = sd.derived;
|
|
1048
|
+
findings = sd.findings;
|
|
1049
|
+
scope = { kind: "diff", base: diff.base, files: diff.files };
|
|
1050
|
+
scopeNote = `diff vs ${diff.base} (${diff.files.length} file${diff.files.length === 1 ? "" : "s"})`;
|
|
1051
|
+
} else if (!opts.wholeSession) {
|
|
1052
|
+
const slice = sliceByBranch(session, branch);
|
|
1053
|
+
if (slice) {
|
|
1054
|
+
scopedSession = slice;
|
|
1055
|
+
derived = deriveSpans(slice);
|
|
1056
|
+
findings = deriveFindings(derived);
|
|
1057
|
+
scope = { kind: "branch", branch };
|
|
1058
|
+
scopeNote = `branch ${branch} (no git diff)`;
|
|
1059
|
+
} else {
|
|
1060
|
+
scopeNote = "whole session (no diff / branch tags)";
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
const receipt = redactReceipt(await buildReceipt(scopedSession, derived, findings, { scope }));
|
|
1064
|
+
const safe = branch.replace(/[/\\]/g, "-");
|
|
1065
|
+
const dir = join4(repoRoot || ".", ".receipts");
|
|
1066
|
+
const out = opts.out ?? join4(dir, `${safe}.json`);
|
|
1067
|
+
mkdirSync(dir, { recursive: true });
|
|
1068
|
+
writeFileSync(out, `${JSON.stringify(receipt, null, 2)}
|
|
1069
|
+
`);
|
|
1070
|
+
git(["add", out]);
|
|
1071
|
+
const rel = repoRoot ? relative(repoRoot, out) : out;
|
|
1072
|
+
process.stderr.write(
|
|
1073
|
+
`receipts pr: wrote ${rel} (Grade ${receipt.predicate.grade}, ${scopeNote}) from "${summary.title ?? "untitled"}".
|
|
1074
|
+
`
|
|
1075
|
+
);
|
|
1076
|
+
return 0;
|
|
1077
|
+
}
|
|
1078
|
+
async function runGuardrails(opts) {
|
|
1079
|
+
if (!await anyDetected()) {
|
|
1080
|
+
process.stderr.write(`No agent transcripts found under ${rootsHint()}.
|
|
1081
|
+
`);
|
|
1082
|
+
return 1;
|
|
1083
|
+
}
|
|
1084
|
+
const derivations = await deriveTargets({
|
|
1085
|
+
agent: opts.agent,
|
|
1086
|
+
last: opts.last,
|
|
1087
|
+
selector: opts.selector
|
|
1088
|
+
});
|
|
1089
|
+
if (derivations.length === 0) {
|
|
1090
|
+
process.stderr.write("guardrails: no matching session.\n");
|
|
1091
|
+
return 1;
|
|
1092
|
+
}
|
|
1093
|
+
const findingSets = derivations.map((d) => d.findings);
|
|
1094
|
+
const rules = collectGuardrails(findingSets);
|
|
1095
|
+
const block = renderGuardrailsBlock(rules, opts.json ? "json" : "md");
|
|
1096
|
+
if (opts.out) {
|
|
1097
|
+
const existing = existsSync(opts.out) ? readFileSync4(opts.out, "utf8") : "";
|
|
1098
|
+
writeFileSync(opts.out, upsertGuardrailsSection(existing, block));
|
|
1099
|
+
process.stderr.write(
|
|
1100
|
+
`guardrails: wrote ${rules.length} rule(s) to ${opts.out} (from ${findingSets.length} session${findingSets.length === 1 ? "" : "s"}).
|
|
1101
|
+
`
|
|
1102
|
+
);
|
|
1103
|
+
return 0;
|
|
1104
|
+
}
|
|
1105
|
+
emit(block, opts.copy ?? false, "Guardrails");
|
|
1106
|
+
if (opts.last) {
|
|
1107
|
+
process.stderr.write(`(from the last ${findingSets.length} sessions)
|
|
1108
|
+
`);
|
|
1109
|
+
}
|
|
1110
|
+
return 0;
|
|
1111
|
+
}
|
|
1112
|
+
async function runTrends(opts) {
|
|
1113
|
+
if (!await anyDetected()) {
|
|
1114
|
+
process.stderr.write(`No agent transcripts found under ${rootsHint()}.
|
|
1115
|
+
`);
|
|
1116
|
+
return 1;
|
|
1117
|
+
}
|
|
1118
|
+
const requested = opts.last && opts.last > 0 ? opts.last : opts.selector ? 1 : 10;
|
|
1119
|
+
const derivations = await deriveTargets(
|
|
1120
|
+
opts.last && opts.last > 0 ? { agent: opts.agent, last: opts.last } : opts.selector ? { agent: opts.agent, selector: opts.selector } : { agent: opts.agent, last: 10 }
|
|
1121
|
+
);
|
|
1122
|
+
if (derivations.length === 0) {
|
|
1123
|
+
process.stderr.write("trends: no matching sessions.\n");
|
|
1124
|
+
return 1;
|
|
1125
|
+
}
|
|
1126
|
+
const inputs = derivations.slice().reverse().map((d) => ({ derived: d.derived, findings: d.findings }));
|
|
1127
|
+
const trends = computeTrends(inputs, requested);
|
|
1128
|
+
if (opts.out) {
|
|
1129
|
+
const block = renderTrends(trends, "md");
|
|
1130
|
+
const existing = existsSync(opts.out) ? readFileSync4(opts.out, "utf8") : "";
|
|
1131
|
+
writeFileSync(opts.out, upsertTrendsSection(existing, block));
|
|
1132
|
+
process.stderr.write(
|
|
1133
|
+
`trends: wrote section to ${opts.out} (from ${trends.window.used} sessions).
|
|
1134
|
+
`
|
|
1135
|
+
);
|
|
1136
|
+
return 0;
|
|
1137
|
+
}
|
|
1138
|
+
if (opts.json) {
|
|
1139
|
+
emit(renderTrends(trends, "json"), opts.copy ?? false, "Trends");
|
|
1140
|
+
return 0;
|
|
1141
|
+
}
|
|
1142
|
+
process.stdout.write(renderTrends(trends, "card", { color: opts.color }));
|
|
1143
|
+
if (opts.copy) {
|
|
1144
|
+
const ok = copyToClipboard(renderTrends(trends, "card", { color: false }));
|
|
1145
|
+
process.stderr.write(
|
|
1146
|
+
ok ? "Trends copied to clipboard.\n" : "Clipboard unavailable \u2014 copy the output above.\n"
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
return 0;
|
|
1150
|
+
}
|
|
1151
|
+
function runEnvelope(file) {
|
|
1152
|
+
if (!file) {
|
|
1153
|
+
process.stderr.write("Usage: receipts envelope <receipt.json>\n");
|
|
1154
|
+
return 1;
|
|
1155
|
+
}
|
|
1156
|
+
try {
|
|
1157
|
+
const receipt = JSON.parse(readFileSync4(file, "utf8"));
|
|
1158
|
+
process.stdout.write(`${JSON.stringify(toDsseEnvelope(receipt), null, 2)}
|
|
1159
|
+
`);
|
|
1160
|
+
return 0;
|
|
1161
|
+
} catch (err) {
|
|
1162
|
+
process.stderr.write(`Could not read ${file}: ${err instanceof Error ? err.message : err}
|
|
1163
|
+
`);
|
|
1164
|
+
return 1;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
async function runVerify(file, opts = {}) {
|
|
1168
|
+
if (!file) {
|
|
1169
|
+
process.stderr.write("Usage: receipts verify <bundle.json> [--transcript <t> [--branch b]]\n");
|
|
1170
|
+
return 1;
|
|
1171
|
+
}
|
|
1172
|
+
let input;
|
|
1173
|
+
try {
|
|
1174
|
+
input = JSON.parse(readFileSync4(file, "utf8"));
|
|
1175
|
+
} catch (err) {
|
|
1176
|
+
process.stderr.write(`Could not read ${file}: ${err instanceof Error ? err.message : err}
|
|
1177
|
+
`);
|
|
1178
|
+
return 1;
|
|
1179
|
+
}
|
|
1180
|
+
const result = verifyBundle(input);
|
|
1181
|
+
if (!result.ok) {
|
|
1182
|
+
process.stderr.write("\u2717 Invalid Receipt:\n");
|
|
1183
|
+
for (const e of result.errors) {
|
|
1184
|
+
process.stderr.write(` - ${e}
|
|
1185
|
+
`);
|
|
1186
|
+
}
|
|
1187
|
+
return 1;
|
|
1188
|
+
}
|
|
1189
|
+
const sig = result.signed ? `signed${result.rekorLogIndex !== void 0 ? ` \xB7 Rekor #${result.rekorLogIndex}` : ""}` : "unsigned";
|
|
1190
|
+
process.stdout.write(`\u2713 Valid Receipt \u2014 Grade ${result.grade} \xB7 ${sig}
|
|
1191
|
+
`);
|
|
1192
|
+
if (result.signer) {
|
|
1193
|
+
process.stdout.write(` signer: ${result.signer}
|
|
1194
|
+
`);
|
|
1195
|
+
}
|
|
1196
|
+
if (opts.transcript && result.receipt) {
|
|
1197
|
+
const cmp = await compareToTranscript(result.receipt, opts.transcript, { branch: opts.branch });
|
|
1198
|
+
if (!cmp.rederived) {
|
|
1199
|
+
process.stderr.write(" \u2717 could not re-derive from the supplied transcript\n");
|
|
1200
|
+
return 1;
|
|
1201
|
+
}
|
|
1202
|
+
if (!cmp.matches) {
|
|
1203
|
+
process.stderr.write(
|
|
1204
|
+
" \u2717 re-derivation MISMATCH \u2014 the Receipt does not match its transcript (possibly hand-edited)\n"
|
|
1205
|
+
);
|
|
1206
|
+
return 1;
|
|
1207
|
+
}
|
|
1208
|
+
process.stdout.write(" \u2713 re-derived \u2014 faithful to the transcript (L1)\n");
|
|
1209
|
+
return 0;
|
|
1210
|
+
}
|
|
1211
|
+
process.stdout.write(
|
|
1212
|
+
" Note: structural check only. Pass --transcript to prove fidelity (L1); `gh attestation verify` for full Sigstore/Rekor.\n"
|
|
1213
|
+
);
|
|
1214
|
+
return 0;
|
|
1215
|
+
}
|
|
1216
|
+
function runDiff(fileA, fileB, opts = {}) {
|
|
1217
|
+
let pathA = fileA;
|
|
1218
|
+
let pathB = fileB;
|
|
1219
|
+
if (!pathA && !pathB) {
|
|
1220
|
+
const hist = loadReceiptHistory(".receipts");
|
|
1221
|
+
if (hist.length < 2) {
|
|
1222
|
+
process.stderr.write(
|
|
1223
|
+
`receipts diff: need two receipts. Found ${hist.length} in .receipts/ \u2014 pass two explicitly: receipts diff <a.json> <b.json>.
|
|
1224
|
+
`
|
|
1225
|
+
);
|
|
1226
|
+
return 1;
|
|
1227
|
+
}
|
|
1228
|
+
pathA = join4(".receipts", `${hist[1].name}.json`);
|
|
1229
|
+
pathB = join4(".receipts", `${hist[0].name}.json`);
|
|
1230
|
+
process.stdout.write(`receipts diff: ${hist[1].name} \u2192 ${hist[0].name} (most recent two)
|
|
1231
|
+
|
|
1232
|
+
`);
|
|
1233
|
+
}
|
|
1234
|
+
if (!pathA || !pathB) {
|
|
1235
|
+
process.stderr.write(
|
|
1236
|
+
"Usage: receipts diff [<receiptA.json> <receiptB.json>] [--json] [--out <f>]\n with no args, diffs the two most recent receipts in .receipts/\n"
|
|
1237
|
+
);
|
|
1238
|
+
return 1;
|
|
1239
|
+
}
|
|
1240
|
+
const read = (f) => {
|
|
1241
|
+
let input;
|
|
1242
|
+
try {
|
|
1243
|
+
input = JSON.parse(readFileSync4(f, "utf8"));
|
|
1244
|
+
} catch (err) {
|
|
1245
|
+
process.stderr.write(`Could not read ${f}: ${err instanceof Error ? err.message : err}
|
|
1246
|
+
`);
|
|
1247
|
+
return null;
|
|
1248
|
+
}
|
|
1249
|
+
const res = verifyBundle(input);
|
|
1250
|
+
if (!res.ok || !res.receipt) {
|
|
1251
|
+
process.stderr.write(`\u2717 ${f} is not a valid Receipt:
|
|
1252
|
+
`);
|
|
1253
|
+
for (const e of res.errors) process.stderr.write(` - ${e}
|
|
1254
|
+
`);
|
|
1255
|
+
return null;
|
|
1256
|
+
}
|
|
1257
|
+
return res.receipt;
|
|
1258
|
+
};
|
|
1259
|
+
const a = read(pathA);
|
|
1260
|
+
const b = read(pathB);
|
|
1261
|
+
if (!a || !b) return 1;
|
|
1262
|
+
const delta = diffReceipts(a, b);
|
|
1263
|
+
const output = opts.json ? `${JSON.stringify(delta, null, 2)}
|
|
1264
|
+
` : renderDiff(delta);
|
|
1265
|
+
if (opts.out) {
|
|
1266
|
+
writeFileSync(opts.out, output);
|
|
1267
|
+
process.stdout.write(`receipts diff: wrote ${opts.out}
|
|
1268
|
+
`);
|
|
1269
|
+
} else {
|
|
1270
|
+
process.stdout.write(output);
|
|
1271
|
+
}
|
|
1272
|
+
return 0;
|
|
1273
|
+
}
|
|
1274
|
+
function runLog(dir, opts = {}) {
|
|
1275
|
+
const all = loadReceiptHistory(dir ?? ".receipts");
|
|
1276
|
+
const shown = opts.last ? all.slice(0, opts.last) : all;
|
|
1277
|
+
const output = opts.json ? `${JSON.stringify(shown, null, 2)}
|
|
1278
|
+
` : renderLog(shown, all.length);
|
|
1279
|
+
if (opts.out) {
|
|
1280
|
+
writeFileSync(opts.out, output);
|
|
1281
|
+
process.stdout.write(`receipts log: wrote ${opts.out}
|
|
1282
|
+
`);
|
|
1283
|
+
} else {
|
|
1284
|
+
process.stdout.write(output);
|
|
1285
|
+
}
|
|
1286
|
+
return 0;
|
|
1287
|
+
}
|
|
1288
|
+
function runStats(dir, opts = {}) {
|
|
1289
|
+
const stats = computeStats(dir ?? ".receipts");
|
|
1290
|
+
const output = opts.json ? `${JSON.stringify(stats, null, 2)}
|
|
1291
|
+
` : renderStats(stats);
|
|
1292
|
+
if (opts.out) {
|
|
1293
|
+
writeFileSync(opts.out, output);
|
|
1294
|
+
process.stdout.write(`receipts stats: wrote ${opts.out}
|
|
1295
|
+
`);
|
|
1296
|
+
} else {
|
|
1297
|
+
process.stdout.write(output);
|
|
1298
|
+
}
|
|
1299
|
+
return 0;
|
|
1300
|
+
}
|
|
1301
|
+
function runBadge(file, opts = {}) {
|
|
1302
|
+
const repoRoot = git(["rev-parse", "--show-toplevel"]) || ".";
|
|
1303
|
+
const branch = git(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
1304
|
+
const path = file ?? (branch && branch !== "HEAD" ? join4(repoRoot, ".receipts", `${branch.replace(/[/\\]/g, "-")}.json`) : void 0);
|
|
1305
|
+
if (!path || !existsSync(path)) {
|
|
1306
|
+
process.stderr.write(
|
|
1307
|
+
`receipts badge: no receipt found${path ? ` at ${path}` : ""}. Run \`receipts pr\` first, or pass a receipt path.
|
|
1308
|
+
`
|
|
1309
|
+
);
|
|
1310
|
+
return 1;
|
|
1311
|
+
}
|
|
1312
|
+
let input;
|
|
1313
|
+
try {
|
|
1314
|
+
input = JSON.parse(readFileSync4(path, "utf8"));
|
|
1315
|
+
} catch {
|
|
1316
|
+
process.stderr.write(`receipts badge: could not parse ${path}
|
|
1317
|
+
`);
|
|
1318
|
+
return 1;
|
|
1319
|
+
}
|
|
1320
|
+
const res = verifyBundle(input);
|
|
1321
|
+
if (!res.ok || !res.receipt) {
|
|
1322
|
+
process.stderr.write(`receipts badge: ${path} is not a valid Receipt.
|
|
1323
|
+
`);
|
|
1324
|
+
return 1;
|
|
1325
|
+
}
|
|
1326
|
+
const output = `${JSON.stringify(badgeEndpoint(res.receipt.predicate), null, 2)}
|
|
1327
|
+
`;
|
|
1328
|
+
if (opts.out) {
|
|
1329
|
+
writeFileSync(opts.out, output);
|
|
1330
|
+
process.stdout.write(`receipts badge: wrote ${opts.out}
|
|
1331
|
+
`);
|
|
1332
|
+
} else {
|
|
1333
|
+
process.stdout.write(output);
|
|
1334
|
+
}
|
|
1335
|
+
return 0;
|
|
1336
|
+
}
|
|
1337
|
+
function runSarif(file, opts = {}) {
|
|
1338
|
+
const repoRoot = git(["rev-parse", "--show-toplevel"]) || ".";
|
|
1339
|
+
const branch = git(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
1340
|
+
const path = file ?? (branch && branch !== "HEAD" ? join4(repoRoot, ".receipts", `${branch.replace(/[/\\]/g, "-")}.json`) : void 0);
|
|
1341
|
+
if (!path || !existsSync(path)) {
|
|
1342
|
+
process.stderr.write(
|
|
1343
|
+
`receipts sarif: no receipt found${path ? ` at ${path}` : ""}. Run \`receipts pr\` first, or pass a receipt path.
|
|
1344
|
+
`
|
|
1345
|
+
);
|
|
1346
|
+
return 1;
|
|
1347
|
+
}
|
|
1348
|
+
let input;
|
|
1349
|
+
try {
|
|
1350
|
+
input = JSON.parse(readFileSync4(path, "utf8"));
|
|
1351
|
+
} catch {
|
|
1352
|
+
process.stderr.write(`receipts sarif: could not parse ${path}
|
|
1353
|
+
`);
|
|
1354
|
+
return 1;
|
|
1355
|
+
}
|
|
1356
|
+
const res = verifyBundle(input);
|
|
1357
|
+
if (!res.ok || !res.receipt) {
|
|
1358
|
+
process.stderr.write(`receipts sarif: ${path} is not a valid Receipt.
|
|
1359
|
+
`);
|
|
1360
|
+
return 1;
|
|
1361
|
+
}
|
|
1362
|
+
const output = `${JSON.stringify(toSarif(res.receipt), null, 2)}
|
|
1363
|
+
`;
|
|
1364
|
+
if (opts.out) {
|
|
1365
|
+
writeFileSync(opts.out, output);
|
|
1366
|
+
process.stdout.write(`receipts sarif: wrote ${opts.out}
|
|
1367
|
+
`);
|
|
1368
|
+
} else {
|
|
1369
|
+
process.stdout.write(output);
|
|
1370
|
+
}
|
|
1371
|
+
return 0;
|
|
1372
|
+
}
|
|
1373
|
+
async function runEval(opts = {}) {
|
|
1374
|
+
const limit = opts.limit && opts.limit > 0 ? opts.limit : 200;
|
|
1375
|
+
const summaries = (await listSessions(opts.agent)).slice(0, limit);
|
|
1376
|
+
const rows = [];
|
|
1377
|
+
for (const summary of summaries) {
|
|
1378
|
+
const session = await loadSession(summary);
|
|
1379
|
+
if (!session) {
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
const { main, minor } = deriveFindings(deriveSpans(session));
|
|
1383
|
+
rows.push({
|
|
1384
|
+
title: summary.title ?? summary.id,
|
|
1385
|
+
categories: [...firedCategories([...main, ...minor])]
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
const scan = summarizeFieldScan(rows);
|
|
1389
|
+
process.stdout.write(opts.json ? `${JSON.stringify(scan, null, 2)}
|
|
1390
|
+
` : renderFieldScan(scan));
|
|
1391
|
+
return 0;
|
|
1392
|
+
}
|
|
1393
|
+
function runInit() {
|
|
1394
|
+
const v = getVersion();
|
|
1395
|
+
const tag = /^\d+\.\d+\.\d+$/.test(v) ? `v${v}` : "v0.2.2";
|
|
1396
|
+
const dir = ".github/workflows";
|
|
1397
|
+
const path = `${dir}/receipts.yml`;
|
|
1398
|
+
if (existsSync(path)) {
|
|
1399
|
+
process.stdout.write(`receipts init: ${path} already exists \u2014 leaving it untouched.
|
|
1400
|
+
`);
|
|
1401
|
+
return 0;
|
|
1402
|
+
}
|
|
1403
|
+
const content = `name: Verified by Receipts
|
|
1404
|
+
|
|
1405
|
+
# Deterministic "what did the coding agent actually do?" check on PRs. Quiet + non-blocking
|
|
1406
|
+
# pilot: acts only when a branch commits an agent Receipt (.receipts/<branch>.json);
|
|
1407
|
+
# otherwise silent. Adds a new "Receipts" check only \u2014 touches no existing workflow.
|
|
1408
|
+
# Docs: https://github.com/AltimateAI/altimate-receipts/blob/main/docs/onboarding-internal.md
|
|
1409
|
+
|
|
1410
|
+
on:
|
|
1411
|
+
pull_request:
|
|
1412
|
+
|
|
1413
|
+
permissions:
|
|
1414
|
+
contents: read
|
|
1415
|
+
id-token: write # Sigstore keyless signing of the receipt
|
|
1416
|
+
attestations: write # record the attestation
|
|
1417
|
+
pull-requests: write # post the Receipts comment
|
|
1418
|
+
checks: write # post the Receipts check
|
|
1419
|
+
|
|
1420
|
+
jobs:
|
|
1421
|
+
receipts:
|
|
1422
|
+
uses: AltimateAI/altimate-receipts/.github/workflows/receipts.reusable.yml@${tag}
|
|
1423
|
+
with:
|
|
1424
|
+
require-receipt: false # never fail a PR that has no receipt (soft pilot)
|
|
1425
|
+
notify-when-missing: false # stay silent unless a receipt is present
|
|
1426
|
+
# block-on: "" # informational check; never blocks a merge
|
|
1427
|
+
`;
|
|
1428
|
+
mkdirSync(dir, { recursive: true });
|
|
1429
|
+
writeFileSync(path, content);
|
|
1430
|
+
process.stdout.write(
|
|
1431
|
+
[
|
|
1432
|
+
`receipts init: wrote ${path} (pinned to ${tag}, quiet + non-blocking).`,
|
|
1433
|
+
" Commit it, open a PR, and the Receipts check runs on every PR.",
|
|
1434
|
+
" Generate receipts locally with the pre-push hook \u2014 see docs/onboarding-internal.md.",
|
|
1435
|
+
""
|
|
1436
|
+
].join("\n")
|
|
1437
|
+
);
|
|
1438
|
+
return 0;
|
|
1439
|
+
}
|
|
1440
|
+
async function runRederive(file, opts = {}) {
|
|
1441
|
+
if (!file) {
|
|
1442
|
+
process.stderr.write("Usage: receipts rederive <transcript.jsonl> [--branch b] [--redact]\n");
|
|
1443
|
+
return 1;
|
|
1444
|
+
}
|
|
1445
|
+
const receipt = await rederiveFromTranscript(file, { branch: opts.branch, redact: opts.redact });
|
|
1446
|
+
if (!receipt) {
|
|
1447
|
+
process.stderr.write(`receipts rederive: could not read a session from ${file}
|
|
1448
|
+
`);
|
|
1449
|
+
return 1;
|
|
1450
|
+
}
|
|
1451
|
+
const out = opts.compact ? canonicalize(receipt) : JSON.stringify(receipt, null, 2);
|
|
1452
|
+
process.stdout.write(`${out}
|
|
1453
|
+
`);
|
|
1454
|
+
return 0;
|
|
1455
|
+
}
|
|
1456
|
+
async function runAssert(opts) {
|
|
1457
|
+
const repoRoot = git(["rev-parse", "--show-toplevel"]) || ".";
|
|
1458
|
+
let asserts;
|
|
1459
|
+
try {
|
|
1460
|
+
asserts = loadAsserts(repoRoot);
|
|
1461
|
+
} catch (e) {
|
|
1462
|
+
process.stderr.write(`receipts assert: ${e.message}
|
|
1463
|
+
`);
|
|
1464
|
+
return 2;
|
|
1465
|
+
}
|
|
1466
|
+
if (asserts.length === 0) {
|
|
1467
|
+
process.stdout.write(`${renderAssertResults([])}
|
|
1468
|
+
`);
|
|
1469
|
+
return 0;
|
|
1470
|
+
}
|
|
1471
|
+
const summary = selectSummary(await listSessions(opts.agent), opts.selector);
|
|
1472
|
+
if (!summary) {
|
|
1473
|
+
process.stderr.write(
|
|
1474
|
+
opts.selector ? `No session matched "${opts.selector}". Try \`receipts --list\`.
|
|
1475
|
+
` : "No sessions found.\n"
|
|
1476
|
+
);
|
|
1477
|
+
return 1;
|
|
1478
|
+
}
|
|
1479
|
+
const session = await loadSession(summary);
|
|
1480
|
+
if (!session) {
|
|
1481
|
+
process.stderr.write(`Could not read session: ${summary.id}
|
|
1482
|
+
`);
|
|
1483
|
+
return 1;
|
|
1484
|
+
}
|
|
1485
|
+
const derived = deriveSpans(session);
|
|
1486
|
+
const receipt = await buildReceipt(session, derived, deriveFindings(derived));
|
|
1487
|
+
const results = evaluateAsserts(receipt, asserts);
|
|
1488
|
+
if (opts.json) {
|
|
1489
|
+
process.stdout.write(`${JSON.stringify(results, null, 2)}
|
|
1490
|
+
`);
|
|
1491
|
+
} else {
|
|
1492
|
+
process.stdout.write(`${renderAssertResults(results)}
|
|
1493
|
+
`);
|
|
1494
|
+
}
|
|
1495
|
+
return assertExitCode(results);
|
|
1496
|
+
}
|
|
1497
|
+
function emit(text, copy, label) {
|
|
1498
|
+
process.stdout.write(`${text}
|
|
1499
|
+
`);
|
|
1500
|
+
if (copy) {
|
|
1501
|
+
const ok = copyToClipboard(text);
|
|
1502
|
+
process.stderr.write(
|
|
1503
|
+
ok ? `
|
|
1504
|
+
${label} copied to clipboard.
|
|
1505
|
+
` : "\nClipboard unavailable \u2014 copy the output above.\n"
|
|
1506
|
+
);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
var entry = process.argv[1];
|
|
1510
|
+
var isMain = entry !== void 0 && import.meta.url === pathToFileURL(entry).href;
|
|
1511
|
+
if (isMain) {
|
|
1512
|
+
run(process.argv).then((code) => process.exit(code)).catch((err) => {
|
|
1513
|
+
process.stderr.write(`receipts: ${err instanceof Error ? err.message : String(err)}
|
|
1514
|
+
`);
|
|
1515
|
+
process.exit(1);
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
export {
|
|
1519
|
+
diffOverlap,
|
|
1520
|
+
parseArgs,
|
|
1521
|
+
run
|
|
1522
|
+
};
|
|
1523
|
+
//# sourceMappingURL=cli.js.map
|