altimate-receipts 0.18.1 → 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-JWDZLJDG.js → chunk-7LROPRL2.js} +98 -31
- package/dist/chunk-7LROPRL2.js.map +1 -0
- package/dist/{chunk-3R25IDH5.js → chunk-7QTTBFY3.js} +223 -7
- package/dist/chunk-7QTTBFY3.js.map +1 -0
- package/dist/{chunk-UADWOMMH.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 +439 -195
- package/dist/cli.js.map +1 -1
- package/dist/gen-catalog.js +1 -1
- package/dist/index.js +9 -3
- package/dist/mcp/server.js +3 -3
- package/package.json +1 -1
- package/dist/chunk-3R25IDH5.js.map +0 -1
- package/dist/chunk-JWDZLJDG.js.map +0 -1
- package/dist/chunk-LYLHXQAU.js +0 -106
- package/dist/chunk-LYLHXQAU.js.map +0 -1
- /package/dist/{chunk-UADWOMMH.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,25 @@
|
|
|
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";
|
|
9
|
+
|
|
10
|
+
// src/version.ts
|
|
11
|
+
import { readFileSync } from "fs";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
function getVersion() {
|
|
14
|
+
try {
|
|
15
|
+
const pkgUrl = new URL("../package.json", import.meta.url);
|
|
16
|
+
const raw = readFileSync(fileURLToPath(pkgUrl), "utf8");
|
|
17
|
+
const pkg = JSON.parse(raw);
|
|
18
|
+
return pkg.version ?? "0.0.0";
|
|
19
|
+
} catch {
|
|
20
|
+
return "0.0.0";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
7
23
|
|
|
8
24
|
// src/findings/gitParse.ts
|
|
9
25
|
var WRAPPER = /* @__PURE__ */ new Set(["sudo", "env", "nohup", "command", "stdbuf", "time", "timeout", "xargs"]);
|
|
@@ -233,6 +249,12 @@ var has = (inv, ...names) => [...inv.flags, ...inv.globalFlags].some((f) => name
|
|
|
233
249
|
function isHardForcePush(inv) {
|
|
234
250
|
return inv.subcommand === "push" && has(inv, "force", "f") && !has(inv, "force-with-lease", "force-if-includes");
|
|
235
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
|
+
}
|
|
236
258
|
function isNoVerify(inv) {
|
|
237
259
|
if (inv.subcommand === "commit") {
|
|
238
260
|
return has(inv, "no-verify", "n");
|
|
@@ -667,6 +689,25 @@ function uncachedCost(usage, model) {
|
|
|
667
689
|
return (usage.input * price.input + usage.output * price.output + (usage.cacheRead + usage.cacheWrite) * price.input) / m;
|
|
668
690
|
}
|
|
669
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
|
+
|
|
670
711
|
// src/findings/toolRoles.ts
|
|
671
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;
|
|
672
713
|
var CREATE_NAMES = /^(write|create_file|write_file|create|notebookedit)$/i;
|
|
@@ -709,7 +750,7 @@ function filePathOf(input) {
|
|
|
709
750
|
return void 0;
|
|
710
751
|
}
|
|
711
752
|
const v = r.file_path ?? r.filePath ?? r.path ?? r.target_file ?? r.targetFile ?? r.filename ?? r.fileName ?? r.uri ?? r.fsPath;
|
|
712
|
-
return typeof v === "string" ? v : void 0;
|
|
753
|
+
return typeof v === "string" ? repoFilePath(v) : void 0;
|
|
713
754
|
}
|
|
714
755
|
function commandOf(input) {
|
|
715
756
|
if (typeof input === "string") {
|
|
@@ -795,6 +836,27 @@ function toolInputField(input, keys) {
|
|
|
795
836
|
}
|
|
796
837
|
return void 0;
|
|
797
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
|
+
}
|
|
798
860
|
var DESTRUCTIVE_PATTERNS = [
|
|
799
861
|
/(?<!git )\brm\s+(-[a-z]*\s+)*-?[a-z]*[rf]/i,
|
|
800
862
|
// rm -rf, rm -fr, rm -r -f (NOT `git rm`)
|
|
@@ -809,11 +871,6 @@ var DESTRUCTIVE_PATTERNS = [
|
|
|
809
871
|
// writing to a raw disk
|
|
810
872
|
/\bmkfs\b|\bdd\s+if=/i
|
|
811
873
|
];
|
|
812
|
-
function stripHeredocs(command) {
|
|
813
|
-
const closed = /<<-?\s*(['"]?)([A-Za-z_]\w*)\1[\s\S]*?\n[ \t]*\2(?![A-Za-z0-9_])/g;
|
|
814
|
-
const stripped = command.replace(closed, "<<heredoc");
|
|
815
|
-
return stripped.replace(/<<-?\s*(['"]?)[A-Za-z_]\w*\1[\s\S]*$/, "<<heredoc");
|
|
816
|
-
}
|
|
817
874
|
function stripCodeStringsAndComments(code) {
|
|
818
875
|
let out = "";
|
|
819
876
|
let state = "code";
|
|
@@ -1024,7 +1081,9 @@ function classifyDestructive(name, input) {
|
|
|
1024
1081
|
if (DESTRUCTIVE_PATTERNS.some((re) => re.test(scannable))) {
|
|
1025
1082
|
return true;
|
|
1026
1083
|
}
|
|
1027
|
-
return parseGitInvocations(scannable).some(
|
|
1084
|
+
return parseGitInvocations(scannable).some(
|
|
1085
|
+
(inv) => destroysGitData(inv) && !(isHardForcePush(inv) && !forcePushTargetProtected(inv))
|
|
1086
|
+
);
|
|
1028
1087
|
}
|
|
1029
1088
|
function destructiveMatch(command) {
|
|
1030
1089
|
const scannable = destructiveScannable(command);
|
|
@@ -1116,10 +1175,22 @@ function deriveSpans(session) {
|
|
|
1116
1175
|
for (const m of session.messages) {
|
|
1117
1176
|
for (const c of m.toolCalls ?? []) {
|
|
1118
1177
|
if (FILE_READ_TOOLS.has(c.name)) {
|
|
1119
|
-
const fp =
|
|
1178
|
+
const fp = repoFilePath(
|
|
1179
|
+
toolInputField(c.input, ["file_path", "path", "filename", "target_file"])
|
|
1180
|
+
);
|
|
1120
1181
|
if (fp) {
|
|
1121
1182
|
filesReadSet.add(fp);
|
|
1122
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
|
+
}
|
|
1123
1194
|
}
|
|
1124
1195
|
}
|
|
1125
1196
|
}
|
|
@@ -1230,12 +1301,16 @@ function deriveSpans(session) {
|
|
|
1230
1301
|
loopCounts.set(sig, { count: 1, name: call.name, preview: String(preview).slice(0, 80) });
|
|
1231
1302
|
}
|
|
1232
1303
|
if (FILE_WRITE_TOOLS.has(call.name)) {
|
|
1233
|
-
const fp =
|
|
1304
|
+
const fp = repoFilePath(
|
|
1305
|
+
toolInputField(call.input, ["file_path", "path", "filename", "target_file"])
|
|
1306
|
+
);
|
|
1234
1307
|
if (fp) {
|
|
1235
1308
|
filesChangedMap.set(fp, "write");
|
|
1236
1309
|
}
|
|
1237
1310
|
} else if (FILE_EDIT_TOOLS.has(call.name)) {
|
|
1238
|
-
const fp =
|
|
1311
|
+
const fp = repoFilePath(
|
|
1312
|
+
toolInputField(call.input, ["file_path", "path", "filename", "target_file"])
|
|
1313
|
+
);
|
|
1239
1314
|
if (fp && !filesChangedMap.has(fp)) {
|
|
1240
1315
|
filesChangedMap.set(fp, "edit");
|
|
1241
1316
|
}
|
|
@@ -3560,8 +3635,9 @@ function diffEffort(derived, files, window) {
|
|
|
3560
3635
|
let tokens = 0;
|
|
3561
3636
|
for (const s of derived.spans) {
|
|
3562
3637
|
if (s.kind === "generation" && editedGens.has(s.spanId)) {
|
|
3563
|
-
|
|
3564
|
-
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;
|
|
3565
3641
|
}
|
|
3566
3642
|
}
|
|
3567
3643
|
return { cost, tokens, turns: editedGens.size };
|
|
@@ -3635,20 +3711,6 @@ function applyDiffScope(derived, findings, files, projectPath) {
|
|
|
3635
3711
|
};
|
|
3636
3712
|
}
|
|
3637
3713
|
|
|
3638
|
-
// src/version.ts
|
|
3639
|
-
import { readFileSync } from "fs";
|
|
3640
|
-
import { fileURLToPath } from "url";
|
|
3641
|
-
function getVersion() {
|
|
3642
|
-
try {
|
|
3643
|
-
const pkgUrl = new URL("../package.json", import.meta.url);
|
|
3644
|
-
const raw = readFileSync(fileURLToPath(pkgUrl), "utf8");
|
|
3645
|
-
const pkg = JSON.parse(raw);
|
|
3646
|
-
return pkg.version ?? "0.0.0";
|
|
3647
|
-
} catch {
|
|
3648
|
-
return "0.0.0";
|
|
3649
|
-
}
|
|
3650
|
-
}
|
|
3651
|
-
|
|
3652
3714
|
// src/receipt/hash.ts
|
|
3653
3715
|
import { createHash } from "crypto";
|
|
3654
3716
|
import { promises as fs } from "fs";
|
|
@@ -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
|
|
@@ -5678,6 +5745,7 @@ function redactReceipt(receipt) {
|
|
|
5678
5745
|
}
|
|
5679
5746
|
|
|
5680
5747
|
export {
|
|
5748
|
+
getVersion,
|
|
5681
5749
|
deriveSpans,
|
|
5682
5750
|
cwdAtFirstGit,
|
|
5683
5751
|
formatTokens,
|
|
@@ -5690,7 +5758,6 @@ export {
|
|
|
5690
5758
|
narrowEffort,
|
|
5691
5759
|
windowedEffort,
|
|
5692
5760
|
applyDiffScope,
|
|
5693
|
-
getVersion,
|
|
5694
5761
|
hashTranscriptFile,
|
|
5695
5762
|
sha256Hex,
|
|
5696
5763
|
PREDICATE_TYPE,
|
|
@@ -5719,4 +5786,4 @@ export {
|
|
|
5719
5786
|
redact,
|
|
5720
5787
|
redactReceipt
|
|
5721
5788
|
};
|
|
5722
|
-
//# sourceMappingURL=chunk-
|
|
5789
|
+
//# sourceMappingURL=chunk-7LROPRL2.js.map
|