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