altimate-receipts 0.19.0 → 0.20.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 +32 -0
- package/dist/{chunk-C7NAN2LL.js → chunk-7LROPRL2.js} +83 -16
- package/dist/chunk-7LROPRL2.js.map +1 -0
- package/dist/{chunk-MJ3ZJNPX.js → chunk-7QTTBFY3.js} +3 -3
- package/dist/{chunk-COMWJSBT.js → chunk-IRUM4XOY.js} +2 -2
- package/dist/chunk-RYOBPBGP.js +232 -0
- package/dist/chunk-RYOBPBGP.js.map +1 -0
- package/dist/cli.js +377 -109
- package/dist/cli.js.map +1 -1
- package/dist/gen-catalog.js +1 -1
- package/dist/index.js +3 -3
- package/dist/mcp/server.js +3 -3
- package/package.json +1 -1
- package/dist/chunk-44V3DNK5.js +0 -107
- package/dist/chunk-44V3DNK5.js.map +0 -1
- package/dist/chunk-C7NAN2LL.js.map +0 -1
- /package/dist/{chunk-MJ3ZJNPX.js.map → chunk-7QTTBFY3.js.map} +0 -0
- /package/dist/{chunk-COMWJSBT.js.map → chunk-IRUM4XOY.js.map} +0 -0
package/README.md
CHANGED
|
@@ -177,6 +177,38 @@ Humans pushing from a terminal, other agents, or repos that won't commit `.claud
|
|
|
177
177
|
config → **[the onboarding guide](./docs/onboarding.md)** has a one-command fallback for
|
|
178
178
|
each.
|
|
179
179
|
|
|
180
|
+
### Live guard (optional — prevention, not just record)
|
|
181
|
+
|
|
182
|
+
The Work Record is *post-hoc* — it documents what the agent did. The **live guard** is the
|
|
183
|
+
*pre-execution* arm: a PreToolUse hook that classifies a tool call **before it runs** and
|
|
184
|
+
either blocks it, asks you to confirm, or just records it.
|
|
185
|
+
|
|
186
|
+
```sh
|
|
187
|
+
npx altimate-receipts init --guard
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
It uses the **same FP-aware policy** as the Work Record (`policy/guardrails.policy.json`), in
|
|
191
|
+
three tiers:
|
|
192
|
+
|
|
193
|
+
- **deny** — never-legitimate catastrophes only (`rm -rf /` / `~` / `.git` / `.receipts`,
|
|
194
|
+
force-push to a *protected* branch, `mkfs`, `DROP DATABASE`). Hard-blocked.
|
|
195
|
+
- **ask** — ambiguous-but-dangerous (`rm -rf` of an unknown dir, `git reset --hard`,
|
|
196
|
+
`--no-verify`, pipe-a-script-from-the-internet, force-push to a *feature* branch). Routed to
|
|
197
|
+
your normal confirmation prompt — never silently blocked.
|
|
198
|
+
- **warn** — surfaced + recorded, never blocked (editing a CI workflow / a grader / an `.env`).
|
|
199
|
+
|
|
200
|
+
Blocked calls never reach the transcript, so the guard **records each deny/ask/warn into the
|
|
201
|
+
Receipt** — the highest-signal line a record can carry: *what the agent was stopped from doing.*
|
|
202
|
+
|
|
203
|
+
**It's off by default, and opt-out is one env var.** Active gating is your call:
|
|
204
|
+
|
|
205
|
+
```sh
|
|
206
|
+
RECEIPTS_GUARD=0 # turn the guard off without un-installing (fail-open, records nothing)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
The guard is **fail-open by contract** — any error, or the opt-out, allows the call. The
|
|
210
|
+
post-hoc Record is the backstop, so live enforcement favors letting work through.
|
|
211
|
+
|
|
180
212
|
## Going deeper (optional)
|
|
181
213
|
|
|
182
214
|
- **`receipts --json`** — a portable, vendor-neutral [in-toto](https://in-toto.io)
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
destructiveOutsideRepo,
|
|
4
|
+
isProtectedBranch,
|
|
4
5
|
regenerableBuildArtifact,
|
|
5
|
-
regenerableReceiptRm
|
|
6
|
-
|
|
6
|
+
regenerableReceiptRm,
|
|
7
|
+
stripHeredocs
|
|
8
|
+
} from "./chunk-RYOBPBGP.js";
|
|
7
9
|
|
|
8
10
|
// src/version.ts
|
|
9
11
|
import { readFileSync } from "fs";
|
|
@@ -247,6 +249,12 @@ var has = (inv, ...names) => [...inv.flags, ...inv.globalFlags].some((f) => name
|
|
|
247
249
|
function isHardForcePush(inv) {
|
|
248
250
|
return inv.subcommand === "push" && has(inv, "force", "f") && !has(inv, "force-with-lease", "force-if-includes");
|
|
249
251
|
}
|
|
252
|
+
function forcePushTargetProtected(inv) {
|
|
253
|
+
return inv.positionals.some((p) => {
|
|
254
|
+
const dst = p.includes(":") ? p.split(":").pop() ?? "" : p;
|
|
255
|
+
return isProtectedBranch(dst);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
250
258
|
function isNoVerify(inv) {
|
|
251
259
|
if (inv.subcommand === "commit") {
|
|
252
260
|
return has(inv, "no-verify", "n");
|
|
@@ -681,6 +689,25 @@ function uncachedCost(usage, model) {
|
|
|
681
689
|
return (usage.input * price.input + usage.output * price.output + (usage.cacheRead + usage.cacheWrite) * price.input) / m;
|
|
682
690
|
}
|
|
683
691
|
|
|
692
|
+
// src/findings/paths.ts
|
|
693
|
+
var WORKTREE_PREFIX = /^.*\/\.claude\/worktrees\/[^/]+\//;
|
|
694
|
+
var AGENT_RUNTIME = /(?:^|\/)\.claude\/(?:projects|todos|statsig|shell-snapshots|history)\//;
|
|
695
|
+
function isAgentRuntimePath(p) {
|
|
696
|
+
return AGENT_RUNTIME.test(p);
|
|
697
|
+
}
|
|
698
|
+
function repoRelativePath(p) {
|
|
699
|
+
return p.replace(WORKTREE_PREFIX, "").replace(/^\.\//, "");
|
|
700
|
+
}
|
|
701
|
+
function repoFilePath(raw) {
|
|
702
|
+
if (!raw) {
|
|
703
|
+
return void 0;
|
|
704
|
+
}
|
|
705
|
+
if (isAgentRuntimePath(raw)) {
|
|
706
|
+
return void 0;
|
|
707
|
+
}
|
|
708
|
+
return repoRelativePath(raw);
|
|
709
|
+
}
|
|
710
|
+
|
|
684
711
|
// src/findings/toolRoles.ts
|
|
685
712
|
var EDIT_NAMES = /^(edit|multiedit|write|notebookedit|str_replace_editor|str_replace|apply_patch|search_replace|create_file|edit_file|insert_edit_into_file|replace_string_in_file|patch|write_file|create)$/i;
|
|
686
713
|
var CREATE_NAMES = /^(write|create_file|write_file|create|notebookedit)$/i;
|
|
@@ -723,7 +750,7 @@ function filePathOf(input) {
|
|
|
723
750
|
return void 0;
|
|
724
751
|
}
|
|
725
752
|
const v = r.file_path ?? r.filePath ?? r.path ?? r.target_file ?? r.targetFile ?? r.filename ?? r.fileName ?? r.uri ?? r.fsPath;
|
|
726
|
-
return typeof v === "string" ? v : void 0;
|
|
753
|
+
return typeof v === "string" ? repoFilePath(v) : void 0;
|
|
727
754
|
}
|
|
728
755
|
function commandOf(input) {
|
|
729
756
|
if (typeof input === "string") {
|
|
@@ -809,6 +836,27 @@ function toolInputField(input, keys) {
|
|
|
809
836
|
}
|
|
810
837
|
return void 0;
|
|
811
838
|
}
|
|
839
|
+
var READ_ONLY_FILE_CMD = /^(?:cat|bat|head|tail|less|more|nl)$/i;
|
|
840
|
+
function extractReadPaths(command) {
|
|
841
|
+
const out = [];
|
|
842
|
+
for (const stmt of command.split(/\n|;|&&|\|\||\|/)) {
|
|
843
|
+
const toks = stmt.trim().split(/\s+/).filter(Boolean);
|
|
844
|
+
const head = toks[0]?.replace(/.*\//, "");
|
|
845
|
+
if (!head || !READ_ONLY_FILE_CMD.test(head)) {
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
for (const t of toks.slice(1)) {
|
|
849
|
+
if (/^\d*&?[<>]/.test(t)) {
|
|
850
|
+
break;
|
|
851
|
+
}
|
|
852
|
+
if (t.startsWith("-") || /^\d+$/.test(t) || /[*?]/.test(t)) {
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
out.push(t.replace(/^["']|["']$/g, ""));
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return out;
|
|
859
|
+
}
|
|
812
860
|
var DESTRUCTIVE_PATTERNS = [
|
|
813
861
|
/(?<!git )\brm\s+(-[a-z]*\s+)*-?[a-z]*[rf]/i,
|
|
814
862
|
// rm -rf, rm -fr, rm -r -f (NOT `git rm`)
|
|
@@ -823,11 +871,6 @@ var DESTRUCTIVE_PATTERNS = [
|
|
|
823
871
|
// writing to a raw disk
|
|
824
872
|
/\bmkfs\b|\bdd\s+if=/i
|
|
825
873
|
];
|
|
826
|
-
function stripHeredocs(command) {
|
|
827
|
-
const closed = /<<-?\s*(['"]?)([A-Za-z_]\w*)\1[\s\S]*?\n[ \t]*\2(?![A-Za-z0-9_])/g;
|
|
828
|
-
const stripped = command.replace(closed, "<<heredoc");
|
|
829
|
-
return stripped.replace(/<<-?\s*(['"]?)[A-Za-z_]\w*\1[\s\S]*$/, "<<heredoc");
|
|
830
|
-
}
|
|
831
874
|
function stripCodeStringsAndComments(code) {
|
|
832
875
|
let out = "";
|
|
833
876
|
let state = "code";
|
|
@@ -1038,7 +1081,9 @@ function classifyDestructive(name, input) {
|
|
|
1038
1081
|
if (DESTRUCTIVE_PATTERNS.some((re) => re.test(scannable))) {
|
|
1039
1082
|
return true;
|
|
1040
1083
|
}
|
|
1041
|
-
return parseGitInvocations(scannable).some(
|
|
1084
|
+
return parseGitInvocations(scannable).some(
|
|
1085
|
+
(inv) => destroysGitData(inv) && !(isHardForcePush(inv) && !forcePushTargetProtected(inv))
|
|
1086
|
+
);
|
|
1042
1087
|
}
|
|
1043
1088
|
function destructiveMatch(command) {
|
|
1044
1089
|
const scannable = destructiveScannable(command);
|
|
@@ -1130,10 +1175,22 @@ function deriveSpans(session) {
|
|
|
1130
1175
|
for (const m of session.messages) {
|
|
1131
1176
|
for (const c of m.toolCalls ?? []) {
|
|
1132
1177
|
if (FILE_READ_TOOLS.has(c.name)) {
|
|
1133
|
-
const fp =
|
|
1178
|
+
const fp = repoFilePath(
|
|
1179
|
+
toolInputField(c.input, ["file_path", "path", "filename", "target_file"])
|
|
1180
|
+
);
|
|
1134
1181
|
if (fp) {
|
|
1135
1182
|
filesReadSet.add(fp);
|
|
1136
1183
|
}
|
|
1184
|
+
} else if (CMD_TOOLS.has(c.name)) {
|
|
1185
|
+
const cmd = toolInputField(c.input, ["command", "cmd"]);
|
|
1186
|
+
if (cmd) {
|
|
1187
|
+
for (const p of extractReadPaths(cmd)) {
|
|
1188
|
+
const fp = repoFilePath(p);
|
|
1189
|
+
if (fp) {
|
|
1190
|
+
filesReadSet.add(fp);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1137
1194
|
}
|
|
1138
1195
|
}
|
|
1139
1196
|
}
|
|
@@ -1244,12 +1301,16 @@ function deriveSpans(session) {
|
|
|
1244
1301
|
loopCounts.set(sig, { count: 1, name: call.name, preview: String(preview).slice(0, 80) });
|
|
1245
1302
|
}
|
|
1246
1303
|
if (FILE_WRITE_TOOLS.has(call.name)) {
|
|
1247
|
-
const fp =
|
|
1304
|
+
const fp = repoFilePath(
|
|
1305
|
+
toolInputField(call.input, ["file_path", "path", "filename", "target_file"])
|
|
1306
|
+
);
|
|
1248
1307
|
if (fp) {
|
|
1249
1308
|
filesChangedMap.set(fp, "write");
|
|
1250
1309
|
}
|
|
1251
1310
|
} else if (FILE_EDIT_TOOLS.has(call.name)) {
|
|
1252
|
-
const fp =
|
|
1311
|
+
const fp = repoFilePath(
|
|
1312
|
+
toolInputField(call.input, ["file_path", "path", "filename", "target_file"])
|
|
1313
|
+
);
|
|
1253
1314
|
if (fp && !filesChangedMap.has(fp)) {
|
|
1254
1315
|
filesChangedMap.set(fp, "edit");
|
|
1255
1316
|
}
|
|
@@ -3574,8 +3635,9 @@ function diffEffort(derived, files, window) {
|
|
|
3574
3635
|
let tokens = 0;
|
|
3575
3636
|
for (const s of derived.spans) {
|
|
3576
3637
|
if (s.kind === "generation" && editedGens.has(s.spanId)) {
|
|
3577
|
-
|
|
3578
|
-
tokens +=
|
|
3638
|
+
const t = s.tokens;
|
|
3639
|
+
tokens += (t?.input ?? 0) + (t?.output ?? 0) + (t?.cacheWrite ?? 0);
|
|
3640
|
+
cost += t ? estimateCost({ ...t, cacheRead: 0 }, s.name) : s.cost || 0;
|
|
3579
3641
|
}
|
|
3580
3642
|
}
|
|
3581
3643
|
return { cost, tokens, turns: editedGens.size };
|
|
@@ -4006,7 +4068,12 @@ async function buildReceipt(session, derived, findings, opts = {}) {
|
|
|
4006
4068
|
session: {
|
|
4007
4069
|
agent: session.source,
|
|
4008
4070
|
model: session.model,
|
|
4009
|
-
|
|
4071
|
+
// Prefer the BRANCH name for a diff-scoped receipt. `session.title` is the session's first
|
|
4072
|
+
// user message, which is shared across every branch a long multi-branch session produces (and
|
|
4073
|
+
// contaminated across resumed sessions) — so 5 unrelated branches all read "Plan SaaS layer…".
|
|
4074
|
+
// The branch is recorded in `scope` (so L1 re-derivation reproduces this exact title) and
|
|
4075
|
+
// actually describes the branch. Fall back to the session title only when there's no branch.
|
|
4076
|
+
title: opts.scope?.branch || session.title,
|
|
4010
4077
|
startedAt: session.startedAt,
|
|
4011
4078
|
endedAt: session.endedAt,
|
|
4012
4079
|
durationMs: session.totals.durationMs
|
|
@@ -5719,4 +5786,4 @@ export {
|
|
|
5719
5786
|
redact,
|
|
5720
5787
|
redactReceipt
|
|
5721
5788
|
};
|
|
5722
|
-
//# sourceMappingURL=chunk-
|
|
5789
|
+
//# sourceMappingURL=chunk-7LROPRL2.js.map
|