altimate-receipts 0.19.0 → 0.21.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-COMWJSBT.js → chunk-3QI3VEWL.js} +2 -2
- package/dist/chunk-KOPNMBLZ.js +233 -0
- package/dist/chunk-KOPNMBLZ.js.map +1 -0
- package/dist/{chunk-MJ3ZJNPX.js → chunk-OW3O53BD.js} +187 -8
- package/dist/chunk-OW3O53BD.js.map +1 -0
- package/dist/{chunk-VKT2ZUCT.js → chunk-SVTKMARA.js} +31 -3
- package/dist/chunk-SVTKMARA.js.map +1 -0
- package/dist/{chunk-C7NAN2LL.js → chunk-Y53BB6HS.js} +1843 -1302
- package/dist/chunk-Y53BB6HS.js.map +1 -0
- package/dist/cli.js +1151 -132
- package/dist/cli.js.map +1 -1
- package/dist/gen-catalog.js +2 -2
- package/dist/index.js +4 -4
- package/dist/mcp/server.js +3 -3
- package/package.json +1 -1
- package/schema/agent-execution-receipt-v1.json +95 -0
- 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 +0 -1
- package/dist/chunk-VKT2ZUCT.js.map +0 -1
- /package/dist/{chunk-COMWJSBT.js.map → chunk-3QI3VEWL.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)
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
loadSession,
|
|
8
8
|
selectSummary,
|
|
9
9
|
upsertSection
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-Y53BB6HS.js";
|
|
11
11
|
|
|
12
12
|
// src/report/sessions.ts
|
|
13
13
|
async function deriveTargets(opts) {
|
|
@@ -259,4 +259,4 @@ export {
|
|
|
259
259
|
renderTrends,
|
|
260
260
|
upsertTrendsSection
|
|
261
261
|
};
|
|
262
|
-
//# sourceMappingURL=chunk-
|
|
262
|
+
//# sourceMappingURL=chunk-3QI3VEWL.js.map
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// policy/guardrails.policy.json
|
|
4
|
+
var guardrails_policy_default = {
|
|
5
|
+
version: 1,
|
|
6
|
+
_doc: "CANONICAL guardrail policy \u2014 the single source of truth for the trust layer's surfaces: live hook (hooks/guard-policy.mjs), post-hoc detector/render (src/findings/surface.ts + action/verified-by.mjs), and fleet (receipts-cloud/app/guardrails.py). DATA only \u2014 each surface has a thin language-specific evaluator. Lists/messages/patterns live HERE ONLY so they cannot drift. See policy/README.md for the migration checklist.",
|
|
7
|
+
buildDirs: [
|
|
8
|
+
"target",
|
|
9
|
+
"dist",
|
|
10
|
+
"build",
|
|
11
|
+
"out",
|
|
12
|
+
"node_modules",
|
|
13
|
+
".next",
|
|
14
|
+
".nuxt",
|
|
15
|
+
".turbo",
|
|
16
|
+
".parcel-cache",
|
|
17
|
+
"__pycache__",
|
|
18
|
+
".pytest_cache",
|
|
19
|
+
".mypy_cache",
|
|
20
|
+
".ruff_cache",
|
|
21
|
+
"coverage",
|
|
22
|
+
".gradle"
|
|
23
|
+
],
|
|
24
|
+
buildDirsExcluded: {
|
|
25
|
+
vendor: "committed in Go/PHP",
|
|
26
|
+
".cache": "stateful / app-specific",
|
|
27
|
+
".terraform": "stateful (provider plugins + lock)"
|
|
28
|
+
},
|
|
29
|
+
protectedBranches: "main|master|release/|prod|production",
|
|
30
|
+
ephemeral: "(^|/)(tmp|\\.venv|venv)(/|$)|^/(private/)?tmp/|^/var/folders/|\\.(bak|tmp|temp|pyc|pyo|class|o|log)$",
|
|
31
|
+
guardrails: {
|
|
32
|
+
"destructive-op": {
|
|
33
|
+
severity: "critical",
|
|
34
|
+
guardrail: "Require explicit confirmation before destructive ops (rm -rf, DROP/TRUNCATE, git reset --hard) outside a sandbox.",
|
|
35
|
+
fix: "Review the destructive op; confirm nothing needed was lost before merging."
|
|
36
|
+
},
|
|
37
|
+
"force-push": {
|
|
38
|
+
severity: "critical",
|
|
39
|
+
guardrail: "Block force-push to shared/protected branches; confirm on feature branches.",
|
|
40
|
+
fix: "Verify no teammate work was overwritten by the force-push."
|
|
41
|
+
},
|
|
42
|
+
"hook-bypass": {
|
|
43
|
+
severity: "high",
|
|
44
|
+
action: "ask",
|
|
45
|
+
guardrail: "Disallow --no-verify / git-hook bypass on protected branches; let the hooks run.",
|
|
46
|
+
fix: "Re-run the pre-commit/pre-push hooks that were skipped and address any failures."
|
|
47
|
+
},
|
|
48
|
+
"pipe-from-internet": {
|
|
49
|
+
severity: "high",
|
|
50
|
+
action: "ask",
|
|
51
|
+
guardrail: "Never pipe a script straight from the internet into a shell; download, read, then run.",
|
|
52
|
+
fix: "Fetch the script to a file, review it, and run it explicitly if safe."
|
|
53
|
+
},
|
|
54
|
+
"force-with-lease": {
|
|
55
|
+
severity: "medium",
|
|
56
|
+
action: "warn",
|
|
57
|
+
guardrail: "Force-with-lease is safer than --force but still rewrites shared history \u2014 confirm intent.",
|
|
58
|
+
fix: "Confirm the rewrite is intended and coordinated with anyone sharing the branch."
|
|
59
|
+
},
|
|
60
|
+
"grader-harness-edit": {
|
|
61
|
+
severity: "high",
|
|
62
|
+
action: "warn",
|
|
63
|
+
guardrail: "Never edit the grader/test/harness/config to make a check pass.",
|
|
64
|
+
fix: "Revert the grader/test edit and fix the underlying issue instead."
|
|
65
|
+
},
|
|
66
|
+
"cicd-edit": {
|
|
67
|
+
severity: "medium",
|
|
68
|
+
action: "warn",
|
|
69
|
+
guardrail: "Flag edits to CI/CD pipeline files for human review before merge.",
|
|
70
|
+
fix: "Review the CI/CD change; confirm it was intended and doesn't weaken checks."
|
|
71
|
+
},
|
|
72
|
+
"secret-exposure": {
|
|
73
|
+
severity: "high",
|
|
74
|
+
action: "warn",
|
|
75
|
+
guardrail: "Never echo or commit secrets; redact tokens/keys in output and files.",
|
|
76
|
+
fix: "Rotate any exposed secret and remove it from history."
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// src/policy/load.ts
|
|
82
|
+
var policy = guardrails_policy_default;
|
|
83
|
+
var BUILD_DIRS = new Set(policy.buildDirs);
|
|
84
|
+
var PROTECTED_BRANCHES = new RegExp(
|
|
85
|
+
`\\b(origin\\s+)?(${policy.protectedBranches})\\b`,
|
|
86
|
+
"i"
|
|
87
|
+
);
|
|
88
|
+
var EPHEMERAL = new RegExp(policy.ephemeral, "i");
|
|
89
|
+
var G = policy.guardrails;
|
|
90
|
+
|
|
91
|
+
// src/policy/classify.ts
|
|
92
|
+
function stripHeredocs(s) {
|
|
93
|
+
const closed = /<<-?\s*(['"]?)([A-Za-z_]\w*)\1[\s\S]*?\n[ \t]*\2(?![A-Za-z0-9_])/g;
|
|
94
|
+
const stripped = s.replace(closed, "<<heredoc");
|
|
95
|
+
return stripped.replace(/<<-?\s*(['"]?)[A-Za-z_]\w*\1[\s\S]*$/, "<<heredoc");
|
|
96
|
+
}
|
|
97
|
+
function splitStatements(cmd) {
|
|
98
|
+
const out = [];
|
|
99
|
+
let cur = "";
|
|
100
|
+
let q = null;
|
|
101
|
+
for (let i = 0; i < cmd.length; i++) {
|
|
102
|
+
const c = cmd[i];
|
|
103
|
+
if (q) {
|
|
104
|
+
cur += c;
|
|
105
|
+
if (c === q) q = null;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (c === "'" || c === '"') {
|
|
109
|
+
q = c;
|
|
110
|
+
cur += c;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (c === "\n" || c === ";") {
|
|
114
|
+
out.push(cur);
|
|
115
|
+
cur = "";
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (c === "&" && cmd[i + 1] === "&" || c === "|" && cmd[i + 1] === "|") {
|
|
119
|
+
out.push(cur);
|
|
120
|
+
cur = "";
|
|
121
|
+
i++;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
cur += c;
|
|
125
|
+
}
|
|
126
|
+
out.push(cur);
|
|
127
|
+
return out.map((s) => s.trim()).filter(Boolean);
|
|
128
|
+
}
|
|
129
|
+
function isRegenerableBuildDir(path) {
|
|
130
|
+
return path.replace(/^["']|["']$/g, "").split("/").some((seg) => BUILD_DIRS.has(seg));
|
|
131
|
+
}
|
|
132
|
+
function isEphemeralTarget(path) {
|
|
133
|
+
return EPHEMERAL.test(path.replace(/^["']|["']$/g, ""));
|
|
134
|
+
}
|
|
135
|
+
function isProtectedBranch(ref) {
|
|
136
|
+
return PROTECTED_BRANCHES.test(ref);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/findings/surface.ts
|
|
140
|
+
var OPERATOR_KINDS = /* @__PURE__ */ new Set([
|
|
141
|
+
"loop",
|
|
142
|
+
"bottleneck",
|
|
143
|
+
"cost-concentration",
|
|
144
|
+
"cache-opportunity",
|
|
145
|
+
"model-downgrade"
|
|
146
|
+
]);
|
|
147
|
+
var PRIVILEGED_PREFIXES = [
|
|
148
|
+
"ci-cd-touch",
|
|
149
|
+
"lockfile-edit",
|
|
150
|
+
"hook-bypass",
|
|
151
|
+
"config-weaken",
|
|
152
|
+
"spec-tamper",
|
|
153
|
+
"grader-edit",
|
|
154
|
+
"eval-override",
|
|
155
|
+
"test-focus",
|
|
156
|
+
"test-skipped",
|
|
157
|
+
"test-trivialised",
|
|
158
|
+
"green-by-suppression",
|
|
159
|
+
"untested-test",
|
|
160
|
+
"history-rewrite",
|
|
161
|
+
"force-push",
|
|
162
|
+
"self-permission",
|
|
163
|
+
"sensitive-access",
|
|
164
|
+
"guard-blocked",
|
|
165
|
+
"mcp-write",
|
|
166
|
+
"mcp-config-change",
|
|
167
|
+
"metadata-egress",
|
|
168
|
+
"tls-verify-disabled",
|
|
169
|
+
"setuid-create",
|
|
170
|
+
"obfuscated-exec",
|
|
171
|
+
"untrusted-dep-source",
|
|
172
|
+
"persistence-write",
|
|
173
|
+
"masked-failure",
|
|
174
|
+
"external-state-mismatch"
|
|
175
|
+
];
|
|
176
|
+
var TEST_PATH = /(?:^|\/)(?:tests?|specs?|__tests__)(?:\/|$)|\.(?:test|spec)\.|_test\./;
|
|
177
|
+
function privileged(id, filePath) {
|
|
178
|
+
if (PRIVILEGED_PREFIXES.some((p) => id.startsWith(p))) {
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
return id.startsWith("file-shrink") && !!filePath && TEST_PATH.test(filePath);
|
|
182
|
+
}
|
|
183
|
+
function findingSurface(id) {
|
|
184
|
+
const base = id.replace(/-sa-[0-9a-f]+$/i, "");
|
|
185
|
+
if (OPERATOR_KINDS.has(base) || base.startsWith("errcluster-")) {
|
|
186
|
+
return "operator";
|
|
187
|
+
}
|
|
188
|
+
return "merge";
|
|
189
|
+
}
|
|
190
|
+
function everyRemovalOperand(title, safe) {
|
|
191
|
+
const clause = title.replace(/^Destructive op:\s*/i, "").replace(/\s*×\d+\s*$/, "");
|
|
192
|
+
const m = clause.match(/^\s*(rm|rmdir)\b(.*)$/i);
|
|
193
|
+
if (!m) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
const args = m[2].replace(/\s+(?:\d*>>?|\d*<|&>|>&)\S*.*$/, "").replace(/\s+—\s.*$/, "");
|
|
197
|
+
const operands = args.split(/\s+/).filter((t) => t && !t.startsWith("-"));
|
|
198
|
+
return operands.length > 0 && operands.every(safe);
|
|
199
|
+
}
|
|
200
|
+
var unquote = (operand) => operand.replace(/^["']|["']$/g, "");
|
|
201
|
+
function destructiveOutsideRepo(title) {
|
|
202
|
+
return everyRemovalOperand(title, (operand) => {
|
|
203
|
+
const p = unquote(operand);
|
|
204
|
+
return /\$/.test(p) || p.startsWith("/tmp") || p.startsWith("/var/folders") || p.startsWith("/private/var/folders") || p.startsWith("~");
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
function regenerableReceiptRm(title) {
|
|
208
|
+
return everyRemovalOperand(
|
|
209
|
+
title,
|
|
210
|
+
(operand) => /(?:^|\/)\.receipts\/[^/]+\.json$/.test(unquote(operand))
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
function regenerableBuildArtifact(title) {
|
|
214
|
+
return everyRemovalOperand(title, isRegenerableBuildDir);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export {
|
|
218
|
+
PROTECTED_BRANCHES,
|
|
219
|
+
EPHEMERAL,
|
|
220
|
+
G,
|
|
221
|
+
stripHeredocs,
|
|
222
|
+
splitStatements,
|
|
223
|
+
isRegenerableBuildDir,
|
|
224
|
+
isEphemeralTarget,
|
|
225
|
+
isProtectedBranch,
|
|
226
|
+
PRIVILEGED_PREFIXES,
|
|
227
|
+
privileged,
|
|
228
|
+
findingSurface,
|
|
229
|
+
destructiveOutsideRepo,
|
|
230
|
+
regenerableReceiptRm,
|
|
231
|
+
regenerableBuildArtifact
|
|
232
|
+
};
|
|
233
|
+
//# sourceMappingURL=chunk-KOPNMBLZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../policy/guardrails.policy.json","../src/policy/load.ts","../src/policy/classify.ts","../src/findings/surface.ts"],"sourcesContent":["{\n \"version\": 1,\n \"_doc\": \"CANONICAL guardrail policy — the single source of truth for the trust layer's surfaces: live hook (hooks/guard-policy.mjs), post-hoc detector/render (src/findings/surface.ts + action/verified-by.mjs), and fleet (receipts-cloud/app/guardrails.py). DATA only — each surface has a thin language-specific evaluator. Lists/messages/patterns live HERE ONLY so they cannot drift. See policy/README.md for the migration checklist.\",\n \"buildDirs\": [\n \"target\",\n \"dist\",\n \"build\",\n \"out\",\n \"node_modules\",\n \".next\",\n \".nuxt\",\n \".turbo\",\n \".parcel-cache\",\n \"__pycache__\",\n \".pytest_cache\",\n \".mypy_cache\",\n \".ruff_cache\",\n \"coverage\",\n \".gradle\"\n ],\n \"buildDirsExcluded\": {\n \"vendor\": \"committed in Go/PHP\",\n \".cache\": \"stateful / app-specific\",\n \".terraform\": \"stateful (provider plugins + lock)\"\n },\n \"protectedBranches\": \"main|master|release/|prod|production\",\n \"ephemeral\": \"(^|/)(tmp|\\\\.venv|venv)(/|$)|^/(private/)?tmp/|^/var/folders/|\\\\.(bak|tmp|temp|pyc|pyo|class|o|log)$\",\n \"guardrails\": {\n \"destructive-op\": {\n \"severity\": \"critical\",\n \"guardrail\": \"Require explicit confirmation before destructive ops (rm -rf, DROP/TRUNCATE, git reset --hard) outside a sandbox.\",\n \"fix\": \"Review the destructive op; confirm nothing needed was lost before merging.\"\n },\n \"force-push\": {\n \"severity\": \"critical\",\n \"guardrail\": \"Block force-push to shared/protected branches; confirm on feature branches.\",\n \"fix\": \"Verify no teammate work was overwritten by the force-push.\"\n },\n \"hook-bypass\": {\n \"severity\": \"high\",\n \"action\": \"ask\",\n \"guardrail\": \"Disallow --no-verify / git-hook bypass on protected branches; let the hooks run.\",\n \"fix\": \"Re-run the pre-commit/pre-push hooks that were skipped and address any failures.\"\n },\n \"pipe-from-internet\": {\n \"severity\": \"high\",\n \"action\": \"ask\",\n \"guardrail\": \"Never pipe a script straight from the internet into a shell; download, read, then run.\",\n \"fix\": \"Fetch the script to a file, review it, and run it explicitly if safe.\"\n },\n \"force-with-lease\": {\n \"severity\": \"medium\",\n \"action\": \"warn\",\n \"guardrail\": \"Force-with-lease is safer than --force but still rewrites shared history — confirm intent.\",\n \"fix\": \"Confirm the rewrite is intended and coordinated with anyone sharing the branch.\"\n },\n \"grader-harness-edit\": {\n \"severity\": \"high\",\n \"action\": \"warn\",\n \"guardrail\": \"Never edit the grader/test/harness/config to make a check pass.\",\n \"fix\": \"Revert the grader/test edit and fix the underlying issue instead.\"\n },\n \"cicd-edit\": {\n \"severity\": \"medium\",\n \"action\": \"warn\",\n \"guardrail\": \"Flag edits to CI/CD pipeline files for human review before merge.\",\n \"fix\": \"Review the CI/CD change; confirm it was intended and doesn't weaken checks.\"\n },\n \"secret-exposure\": {\n \"severity\": \"high\",\n \"action\": \"warn\",\n \"guardrail\": \"Never echo or commit secrets; redact tokens/keys in output and files.\",\n \"fix\": \"Rotate any exposed secret and remove it from history.\"\n }\n }\n}\n","// Canonical guardrails policy for the TS surfaces (post-hoc detector + the live-guard CLI command).\n//\n// The policy JSON (policy/guardrails.policy.json) is the CROSS-LANGUAGE canon — also read by the\n// live-guard .mjs (via policy/load.mjs) and receipts-cloud (Python). Here it is *imported*, not\n// file-read, so esbuild/tsup inlines it into `dist` and it ships in the published package (which\n// includes `dist` only). This file mirrors policy/load.mjs so the two stay byte-equivalent.\n//\n// This is THE single source of the guardrail data for TS: BUILD_DIRS, the ephemeral/protected\n// patterns, and the per-rule message metadata. The post-hoc detector imports from here instead of\n// re-declaring (it previously had its own BUILD_DIRS in surface.ts, a PROTECTED_REF in gitParse.ts,\n// and an inline ephemeral check) — eliminating the drift the dogfood audit found.\nimport policyJson from \"../../policy/guardrails.policy.json\";\n\ninterface GuardrailMeta {\n severity: string;\n guardrail: string;\n fix: string;\n action?: \"deny\" | \"ask\" | \"warn\";\n}\n\ninterface Policy {\n buildDirs: string[];\n protectedBranches: string;\n ephemeral: string;\n guardrails: Record<string, GuardrailMeta>;\n}\n\n/** The raw canonical policy object. */\nexport const policy: Policy = policyJson as Policy;\n\n/** Frozen regenerable build-output dir set (the formerly-duplicated list, now sourced here). */\nexport const BUILD_DIRS: ReadonlySet<string> = new Set(policy.buildDirs);\n\n/** Protected-branch matcher for force-push scoping (deny on protected, else confirm). */\nexport const PROTECTED_BRANCHES = new RegExp(\n `\\\\b(origin\\\\s+)?(${policy.protectedBranches})\\\\b`,\n \"i\",\n);\n\n/** Ephemeral scratch path/file matcher (`/tmp`, `.venv`, `*.bak`, …). */\nexport const EPHEMERAL = new RegExp(policy.ephemeral, \"i\");\n\n/** id → { severity, guardrail, fix, action? } — the shared message + tier metadata. */\nexport const G: Record<string, GuardrailMeta> = policy.guardrails;\n","// Canonical FP-aware classification PRIMITIVES, shared by the post-hoc detector (src/findings/*)\n// and the live-guard CLI command (src/hook/guard.ts). The \"ONE policy, three surfaces\" promise was\n// only true for the policy DATA (policy/guardrails.policy.json); the LOGIC was duplicated between\n// the live guard (hooks/guard-policy.mjs) and the post-hoc TS, and drifted — the dogfood audit\n// found the SAME force-push / scratch FP fixed twice (PR #190 live, PR #191 post-hoc). These are the\n// pure primitives both surfaces compose; each surface keeps its own input adapter (the live guard\n// works on a raw command string; the post-hoc on parsed spans + finding titles).\nimport { BUILD_DIRS, EPHEMERAL, PROTECTED_BRANCHES } from \"./load.js\";\n\n/**\n * Strip heredoc bodies (`<<TAG … TAG`) before classifying — they are DATA (commit messages, review\n * prompts, file content), not executed commands. A commit message that *describes* `DROP DATABASE`\n * or `rm -rf /` must not be read as if it ran one. Handles `<<-`, quoted/unquoted markers, multiple\n * heredocs, and an unclosed (truncated) heredoc (stripped to end). (Rare exception: `bash <<EOF`\n * executes the body; the post-hoc detector is the backstop there.)\n */\nexport function stripHeredocs(s: string): string {\n const closed = /<<-?\\s*(['\"]?)([A-Za-z_]\\w*)\\1[\\s\\S]*?\\n[ \\t]*\\2(?![A-Za-z0-9_])/g;\n const stripped = s.replace(closed, \"<<heredoc\");\n // An opener with no matching close (e.g. a truncated capture) → drop to end.\n return stripped.replace(/<<-?\\s*(['\"]?)[A-Za-z_]\\w*\\1[\\s\\S]*$/, \"<<heredoc\");\n}\n\n/**\n * Split a command into statements on `\\n ; && ||` — but NOT on a single `|` (a pipeline is one\n * statement). Quote-aware so separators inside strings don't fragment it. The core FP fix: each\n * statement is classified independently, so a `git push -f` to a feature branch is not denied\n * because `origin/main` appears in a *different* statement of the same merge-loop script.\n */\nexport function splitStatements(cmd: string): string[] {\n const out: string[] = [];\n let cur = \"\";\n let q: string | null = null;\n for (let i = 0; i < cmd.length; i++) {\n const c = cmd[i];\n if (q) {\n cur += c;\n if (c === q) q = null;\n continue;\n }\n if (c === \"'\" || c === '\"') {\n q = c;\n cur += c;\n continue;\n }\n if (c === \"\\n\" || c === \";\") {\n out.push(cur);\n cur = \"\";\n continue;\n }\n if ((c === \"&\" && cmd[i + 1] === \"&\") || (c === \"|\" && cmd[i + 1] === \"|\")) {\n out.push(cur);\n cur = \"\";\n i++;\n continue;\n }\n cur += c;\n }\n out.push(cur);\n return out.map((s) => s.trim()).filter(Boolean);\n}\n\n/** A path operand is a regenerable build artifact iff a path segment is a known build dir\n * (`node_modules`, `dist`, `target`, …). Deliberately EXCLUDES vendor/.cache/.terraform — see\n * guardrails.policy.json `buildDirsExcluded`. */\nexport function isRegenerableBuildDir(path: string): boolean {\n return path\n .replace(/^[\"']|[\"']$/g, \"\")\n .split(\"/\")\n .some((seg) => BUILD_DIRS.has(seg));\n}\n\n/** A path operand is ephemeral scratch (`/tmp`, `/var/folders`, `.venv`, `*.bak`, …) per the\n * canonical EPHEMERAL pattern — removing it is housekeeping, not a merge risk. */\nexport function isEphemeralTarget(path: string): boolean {\n return EPHEMERAL.test(path.replace(/^[\"']|[\"']$/g, \"\"));\n}\n\n/** A git ref (a force-push target) names a protected/shared branch (`main`, `master`, `release/`,\n * `prod`, `production`) — force-pushing one is a real merge risk; a feature branch is routine. */\nexport function isProtectedBranch(ref: string): boolean {\n return PROTECTED_BRANCHES.test(ref);\n}\n","/**\n * Which surface a finding belongs to. The **merge gate** (the PR comment) is about\n * the *merged artifact* — what a reviewer must check before merging. The **operator**\n * view (the `receipts` card and `receipts trends`) is about *how the agent worked* —\n * efficiency, cost, loops, retries. One source of truth, shared by the PR-comment\n * renderer and its standalone Action mirror.\n */\n\nimport { isRegenerableBuildDir } from \"../policy/classify.js\";\n\n/** Kinds that describe agent behaviour/efficiency, not the merged change. */\nconst OPERATOR_KINDS = new Set([\n \"loop\",\n \"bottleneck\",\n \"cost-concentration\",\n \"cache-opportunity\",\n \"model-downgrade\",\n]);\n\nexport type Surface = \"merge\" | \"operator\";\n\n/** Finding-id prefixes that touch a PRIVILEGED surface — CI/CD, lockfiles, hooks,\n * checker config, graders/evals, test integrity, git history (SPEC-0073 R5). These\n * render first in the Agent Work Record, ▲-marked, never collapsed: an edit here runs\n * with repository secrets or moves the ground truth reviewers stand on. */\nexport const PRIVILEGED_PREFIXES = [\n \"ci-cd-touch\",\n \"lockfile-edit\",\n \"hook-bypass\",\n \"config-weaken\",\n \"spec-tamper\",\n \"grader-edit\",\n \"eval-override\",\n \"test-focus\",\n \"test-skipped\",\n \"test-trivialised\",\n \"green-by-suppression\",\n \"untested-test\",\n \"history-rewrite\",\n \"force-push\",\n \"self-permission\",\n \"sensitive-access\",\n \"guard-blocked\",\n \"mcp-write\",\n \"mcp-config-change\",\n \"metadata-egress\",\n \"tls-verify-disabled\",\n \"setuid-create\",\n \"obfuscated-exec\",\n \"untrusted-dep-source\",\n \"persistence-write\",\n \"masked-failure\",\n \"external-state-mismatch\",\n];\n\n/** `file-shrink` is privileged only when the shrunk file is a test (assertion loss). */\nconst TEST_PATH = /(?:^|\\/)(?:tests?|specs?|__tests__)(?:\\/|$)|\\.(?:test|spec)\\.|_test\\./;\n\nexport function privileged(id: string, filePath?: string): boolean {\n if (PRIVILEGED_PREFIXES.some((p) => id.startsWith(p))) {\n return true;\n }\n return id.startsWith(\"file-shrink\") && !!filePath && TEST_PATH.test(filePath);\n}\n\nexport function findingSurface(id: string): Surface {\n // Classify on the BASE kind: strip a subagent suffix (`-sa-<agentId>`, M104) so a wrapped operator\n // id like `bottleneck-sa-1a2b3c4d` is still recognized as operator and can't leak onto the merge\n // surface. (`errcluster-<tool>` — \"X failed N× before succeeding\" — is also an efficiency signal.)\n const base = id.replace(/-sa-[0-9a-f]+$/i, \"\");\n if (OPERATOR_KINDS.has(base) || base.startsWith(\"errcluster-\")) {\n return \"operator\";\n }\n return \"merge\";\n}\n\n/**\n * Shared shape for the destructive-op carve-outs below. Parses the `rm`/`rmdir` clause from a\n * `Destructive op:` finding title and returns true only when EVERY removal operand satisfies\n * `safe`. A non-rm destructive (git/SQL/infra acts on the repo or an external system) or an empty\n * operand list returns false — i.e. the finding keeps flagging. Each carve-out below just supplies\n * the per-operand `safe` predicate.\n */\nfunction everyRemovalOperand(title: string, safe: (operand: string) => boolean): boolean {\n const clause = title.replace(/^Destructive op:\\s*/i, \"\").replace(/\\s*×\\d+\\s*$/, \"\");\n const m = clause.match(/^\\s*(rm|rmdir)\\b(.*)$/i);\n if (!m) {\n return false;\n }\n // Drop shell redirects and anything after them (`2>/dev/null`, `> log`, `&>f`, `2>&1`) and a\n // trailing `— via subagent …` attribution, so they aren't read as removal operands. Without this,\n // `rm -rf /tmp/x 2>/dev/null` keeps the `/tmp` carve-out from recognizing the scratch target (the\n // redirect token isn't a path), and the op false-flags as a merge risk.\n const args = m[2].replace(/\\s+(?:\\d*>>?|\\d*<|&>|>&)\\S*.*$/, \"\").replace(/\\s+—\\s.*$/, \"\");\n const operands = args.split(/\\s+/).filter((t) => t && !t.startsWith(\"-\"));\n return operands.length > 0 && operands.every(safe);\n}\n\n/** Strip surrounding quotes from a parsed operand before matching. */\nconst unquote = (operand: string): string => operand.replace(/^[\"']|[\"']$/g, \"\");\n\n/**\n * True when a destructive finding's target is a **scratch path** (a `$VAR`, `/tmp`,\n * `/var/folders`, or `~`), so it can't be a merge risk and shouldn't gate the PR. Conservative:\n * `rm -rf ./src`, `rm -rf build`, `git reset --hard`, `DROP TABLE` are all kept (return false).\n */\nexport function destructiveOutsideRepo(title: string): boolean {\n return everyRemovalOperand(title, (operand) => {\n const p = unquote(operand);\n return (\n /\\$/.test(p) ||\n p.startsWith(\"/tmp\") ||\n p.startsWith(\"/var/folders\") ||\n p.startsWith(\"/private/var/folders\") ||\n p.startsWith(\"~\")\n );\n });\n}\n\n/**\n * True when a destructive finding deletes only the tool's **own regenerable Receipt artifact**\n * (`rm .receipts/<branch>.json`) — a scope-layer carve-out: a `.receipts/*.json` is inside the repo\n * but is rewritten by `receipts pr`, so it isn't a merge risk. Only a flat `.receipts/<name>.json`\n * file matches; a mixed command or the whole dir (`rm -rf .receipts`) keeps flagging.\n */\nexport function regenerableReceiptRm(title: string): boolean {\n return everyRemovalOperand(title, (operand) =>\n /(?:^|\\/)\\.receipts\\/[^/]+\\.json$/.test(unquote(operand)),\n );\n}\n\n// BUILD_DIRS (well-known regenerable build/dependency output dirs — deleting them is housekeeping,\n// not data loss) now comes from the canonical policy via ../policy/load.js — single source shared\n// with the live guard. Deliberately excludes vendor/.cache/.terraform (see guardrails.policy.json).\n\n/**\n * True when a destructive op deletes only **regenerable build artifacts** (`rm -rf dist`, `rm -rf\n * node_modules`). Scope-layer carve-out like {@link destructiveOutsideRepo}: build output is inside\n * the repo but regenerable in one command. Conservative: every operand must have a path segment in\n * the frozen build-dir set — a mixed command (`rm -rf dist src`) or a real target keeps flagging.\n */\nexport function regenerableBuildArtifact(title: string): boolean {\n return everyRemovalOperand(title, isRegenerableBuildDir);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
agentIds,
|
|
4
4
|
applyDiffScope,
|
|
5
5
|
buildReceipt,
|
|
6
|
+
computeCompletion,
|
|
6
7
|
deriveEvidence,
|
|
7
8
|
deriveFindings,
|
|
8
9
|
deriveSpans,
|
|
@@ -16,18 +17,18 @@ import {
|
|
|
16
17
|
redactReceipt,
|
|
17
18
|
renderLedger,
|
|
18
19
|
windowedEffort
|
|
19
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-Y53BB6HS.js";
|
|
20
21
|
import {
|
|
21
22
|
FIX_FOR,
|
|
22
23
|
categorize,
|
|
23
24
|
checkForId
|
|
24
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-SVTKMARA.js";
|
|
25
26
|
import {
|
|
26
27
|
destructiveOutsideRepo,
|
|
27
28
|
findingSurface,
|
|
28
29
|
privileged,
|
|
29
30
|
regenerableBuildArtifact
|
|
30
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-KOPNMBLZ.js";
|
|
31
32
|
|
|
32
33
|
// src/report/sarif.ts
|
|
33
34
|
var INFO_URI = "https://github.com/AltimateAI/altimate-receipts";
|
|
@@ -93,6 +94,44 @@ function toSarif(receipt) {
|
|
|
93
94
|
}
|
|
94
95
|
return result;
|
|
95
96
|
});
|
|
97
|
+
for (const scanner of p.scanners ?? []) {
|
|
98
|
+
for (const sr of scanner.results ?? []) {
|
|
99
|
+
const ruleId = sr.ruleId || scanner.tool;
|
|
100
|
+
const level = sr.level;
|
|
101
|
+
const existingRule = rules.get(ruleId);
|
|
102
|
+
if (!existingRule) {
|
|
103
|
+
ruleOrder.push(ruleId);
|
|
104
|
+
rules.set(ruleId, {
|
|
105
|
+
id: ruleId,
|
|
106
|
+
name: `${scanner.tool}:${ruleId}`,
|
|
107
|
+
shortDescription: { text: `${scanner.category} (${scanner.tool})` },
|
|
108
|
+
defaultConfiguration: { level },
|
|
109
|
+
helpUri: INFO_URI
|
|
110
|
+
});
|
|
111
|
+
} else if (sevRank[level] < sevRank[existingRule.defaultConfiguration.level]) {
|
|
112
|
+
existingRule.defaultConfiguration.level = level;
|
|
113
|
+
}
|
|
114
|
+
const uri = sr.file;
|
|
115
|
+
const result = {
|
|
116
|
+
ruleId,
|
|
117
|
+
ruleIndex: ruleOrder.indexOf(ruleId),
|
|
118
|
+
level,
|
|
119
|
+
message: { text: sr.message ?? `${scanner.tool} ${ruleId}` },
|
|
120
|
+
partialFingerprints: { receiptsId: `${ruleId}::${uri ?? "scanner"}` }
|
|
121
|
+
};
|
|
122
|
+
if (uri) {
|
|
123
|
+
result.locations = [
|
|
124
|
+
{
|
|
125
|
+
physicalLocation: {
|
|
126
|
+
artifactLocation: { uri },
|
|
127
|
+
...sr.line ? { region: { startLine: sr.line } } : {}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
results.push(result);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
96
135
|
const ruleList = ruleOrder.map((id) => rules.get(id));
|
|
97
136
|
return {
|
|
98
137
|
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
@@ -263,6 +302,85 @@ function serialize(value) {
|
|
|
263
302
|
// src/report/prComment.ts
|
|
264
303
|
import { createHash as createHash2 } from "crypto";
|
|
265
304
|
|
|
305
|
+
// src/findings/externalState.ts
|
|
306
|
+
var TARGET = /\b([A-Za-z0-9][\w.-]*\/[\w.-]+)#(\d+)\b|(?:^|[\s(])#(\d+)\b/;
|
|
307
|
+
var PR_REF = /(?:[A-Za-z0-9][\w.-]*\/[\w.-]+)?#\d+/;
|
|
308
|
+
var MERGED_OBJECT = new RegExp(
|
|
309
|
+
`\\b(?:merged|landed)\\s+(?:(?:the\\s+)?(?:pr|pull\\s*request)\\s+)?${PR_REF.source}`,
|
|
310
|
+
"i"
|
|
311
|
+
);
|
|
312
|
+
var MERGED_SUBJECT = new RegExp(
|
|
313
|
+
`${PR_REF.source}\\s+(?:(?:has|have|is|was|been|got|now|successfully|finally)\\s+){0,4}(?:merged|landed)\\b`,
|
|
314
|
+
"i"
|
|
315
|
+
);
|
|
316
|
+
var isMergedClaim = (s) => (MERGED_OBJECT.test(s) || MERGED_SUBJECT.test(s)) && !LOCAL_MERGE.test(s);
|
|
317
|
+
var CI_GREEN = /\b(?:ci|checks?|pipeline|the build|all checks?)\b[^.!?\n]*\b(?:is|are|all)?\s*(?:green|passing|passed|pass)\b/i;
|
|
318
|
+
var HEDGE = /\b(should|will|going to|once|after|when|if|need to|needs to|ready to|about to|pending|waiting)\b/i;
|
|
319
|
+
var NEGATION = /\b(not|isn'?t|aren'?t|wasn'?t|weren'?t|never|no longer|failed to|couldn'?t|can'?t|cannot|won'?t|without)\b/i;
|
|
320
|
+
var LOCAL_MERGE = /\bmerged\s+\S+\s+into\b|\bmerged\s+(?:main|master|develop|trunk|origin|upstream|HEAD)\b/i;
|
|
321
|
+
var OTHER_PR_BACKDROP = /\b(?:from|in|like|see|per|via)\s+(?:pr\s+)?#\s*\d|\bapproach\s+(?:from|in)\b|\bbase\s+branch\b|\bduplicat\w*/i;
|
|
322
|
+
function targetOf(sentence) {
|
|
323
|
+
const m = TARGET.exec(sentence);
|
|
324
|
+
if (!m) {
|
|
325
|
+
return void 0;
|
|
326
|
+
}
|
|
327
|
+
if (m[1] && m[2]) {
|
|
328
|
+
return `${m[1]}#${m[2]}`;
|
|
329
|
+
}
|
|
330
|
+
return m[3] ? `#${m[3]}` : void 0;
|
|
331
|
+
}
|
|
332
|
+
function parseExternalClaims(finalText) {
|
|
333
|
+
const out = [];
|
|
334
|
+
const seen = /* @__PURE__ */ new Set();
|
|
335
|
+
for (const sentence of finalText.split(/[.!?\n]+/)) {
|
|
336
|
+
if (HEDGE.test(sentence) || NEGATION.test(sentence) || OTHER_PR_BACKDROP.test(sentence)) {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const target = targetOf(sentence);
|
|
340
|
+
if (!target) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const kind = isMergedClaim(sentence) ? "merged" : CI_GREEN.test(sentence) ? "ci-green" : void 0;
|
|
344
|
+
if (!kind) {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const key = `${kind}|${target}`;
|
|
348
|
+
if (seen.has(key)) {
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
seen.add(key);
|
|
352
|
+
out.push({ kind, target, raw: sentence.trim().slice(0, 200) });
|
|
353
|
+
}
|
|
354
|
+
return out;
|
|
355
|
+
}
|
|
356
|
+
function label(kind) {
|
|
357
|
+
return kind === "merged" ? "merged" : "CI green";
|
|
358
|
+
}
|
|
359
|
+
function deriveExternalStateRows(rows) {
|
|
360
|
+
const out = [];
|
|
361
|
+
let i = 0;
|
|
362
|
+
for (const r of rows) {
|
|
363
|
+
if (r.match) {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
const who = r.actor ? ` by \`${r.actor}\`` : "";
|
|
367
|
+
const when = r.ts ? ` (${r.ts})` : "";
|
|
368
|
+
const at = r.fetchedAt ? ` fetched ${r.fetchedAt}` : "";
|
|
369
|
+
out.push({
|
|
370
|
+
id: `external-state-mismatch-${i}`,
|
|
371
|
+
severity: "high",
|
|
372
|
+
title: `Claimed ${r.target} ${label(r.kind)}, API reports ${r.actual}`,
|
|
373
|
+
detail: `The session claimed ${r.target} was ${r.claimed}, but the authoritative API reports \`${r.actual}\`${who}${when}${at}. This is a recorded input \u2014 the reconciler fetched it in CI; the deterministic core never makes the call. Confirm the claim, or correct the summary. ${r.url ?? ""}`.trim(),
|
|
374
|
+
impactLabel: "external state",
|
|
375
|
+
confidence: 1,
|
|
376
|
+
score: 100,
|
|
377
|
+
guardrailRule: "Don't claim a PR is merged / CI is green until the authoritative API confirms it; reconcile claims about external state before reporting done."
|
|
378
|
+
});
|
|
379
|
+
i++;
|
|
380
|
+
}
|
|
381
|
+
return out;
|
|
382
|
+
}
|
|
383
|
+
|
|
266
384
|
// src/findings/guardEvents.ts
|
|
267
385
|
var SEV = {
|
|
268
386
|
deny: "critical",
|
|
@@ -398,6 +516,32 @@ function subagentRows(p) {
|
|
|
398
516
|
...s.filePath ? { filePath: s.filePath } : {}
|
|
399
517
|
}));
|
|
400
518
|
}
|
|
519
|
+
function externalStateRows(p) {
|
|
520
|
+
if (!p.externalState?.length) {
|
|
521
|
+
return [];
|
|
522
|
+
}
|
|
523
|
+
return deriveExternalStateRows(p.externalState).map((f) => ({
|
|
524
|
+
id: f.id,
|
|
525
|
+
severity: f.severity,
|
|
526
|
+
title: f.title,
|
|
527
|
+
detail: f.detail,
|
|
528
|
+
confidence: f.confidence,
|
|
529
|
+
score: f.score,
|
|
530
|
+
impactLabel: f.impactLabel
|
|
531
|
+
}));
|
|
532
|
+
}
|
|
533
|
+
function externalStateCorroborations(p) {
|
|
534
|
+
const ok = (p.externalState ?? []).filter((r) => r.match);
|
|
535
|
+
if (ok.length === 0) {
|
|
536
|
+
return void 0;
|
|
537
|
+
}
|
|
538
|
+
const chips = ok.map((r) => {
|
|
539
|
+
const who = r.actor ? ` by \`${r.actor}\`` : "";
|
|
540
|
+
const when = r.ts ? ` \xB7 ${r.ts}` : "";
|
|
541
|
+
return `\u2714 ${r.target} ${r.kind === "merged" ? "merged" : "CI green"}${who}${when}`;
|
|
542
|
+
}).join(" \xB7 ");
|
|
543
|
+
return `<sub>\u{1F310} external state reconciled: ${chips} (authoritative API, attested).</sub>`;
|
|
544
|
+
}
|
|
401
545
|
function subagentCountLine(p) {
|
|
402
546
|
const sa = p.subagents;
|
|
403
547
|
if (!sa || sa.total === 0) {
|
|
@@ -407,11 +551,27 @@ function subagentCountLine(p) {
|
|
|
407
551
|
const tail = shown > 0 ? `, ${shown} surfaced above` : ", none merge-relevant";
|
|
408
552
|
return `<sub>\u{1F9F7} ${sa.total} subagent${sa.total === 1 ? "" : "s"} ran (${sa.considered} did file/command work${tail}) \u2014 separate transcripts, attributed.</sub>`;
|
|
409
553
|
}
|
|
554
|
+
function witnessLine(p) {
|
|
555
|
+
const w = p.witness;
|
|
556
|
+
if (!w) {
|
|
557
|
+
return void 0;
|
|
558
|
+
}
|
|
559
|
+
if (w.tier === "witness-divergence") {
|
|
560
|
+
return `<sub>\u{1F6F0}\uFE0F model-side: <b>diverges from altimate-router</b> \u2014 ${w.present}/${w.witnessed} model decisions match the independent record; the rest are missing from this transcript.</sub>`;
|
|
561
|
+
}
|
|
562
|
+
const sig = w.signed === "verified" ? ", signature verified" : w.signed === "unverified" ? ", signature unverified" : "";
|
|
563
|
+
return `<sub>\u{1F6F0}\uFE0F model-side: witnessed by altimate-router \u2014 ${w.present}/${w.witnessed} model decisions independently recorded${sig}.</sub>`;
|
|
564
|
+
}
|
|
410
565
|
function renderPrComment(receipt, opts = {}) {
|
|
411
566
|
const p = receipt.predicate;
|
|
412
567
|
const ev = p.evidence;
|
|
413
568
|
const operatorCount = p.findings.filter((f) => findingSurface(f.id) === "operator").length;
|
|
414
|
-
const main = visibleMergeFindings([
|
|
569
|
+
const main = visibleMergeFindings([
|
|
570
|
+
...guardRows(p),
|
|
571
|
+
...subagentRows(p),
|
|
572
|
+
...externalStateRows(p),
|
|
573
|
+
...p.findings
|
|
574
|
+
]);
|
|
415
575
|
const cats = categorize(main);
|
|
416
576
|
const files = p.scope?.kind === "diff" ? p.scope.files ?? [] : [];
|
|
417
577
|
const priv = main.filter((f) => privileged(f.id, f.filePath));
|
|
@@ -675,6 +835,14 @@ function recordDetails(p, ev, cats, operatorCount, opts, feedback) {
|
|
|
675
835
|
if (saLine) {
|
|
676
836
|
inner.push(saLine);
|
|
677
837
|
}
|
|
838
|
+
const wLine = witnessLine(p);
|
|
839
|
+
if (wLine) {
|
|
840
|
+
inner.push(wLine);
|
|
841
|
+
}
|
|
842
|
+
const xLine = externalStateCorroborations(p);
|
|
843
|
+
if (xLine) {
|
|
844
|
+
inner.push(xLine);
|
|
845
|
+
}
|
|
678
846
|
const signed = opts.signed ? `Signed (Sigstore \u2192 Rekor)${opts.attestationUrl ? ` \xB7 [attestation](${opts.attestationUrl})` : ""} \xB7 ` : "";
|
|
679
847
|
inner.push(
|
|
680
848
|
`<sub>${signed}Re-derivable (L1: \`receipts verify <receipt> --transcript <t>\`) \xB7 pull any event's tape: \`npx altimate-receipts log --ref <evidence-id>\` \xB7 deterministic \xB7 0 model calls \xB7 evidence, not judgement \xB7 [trust model](${TRUST_DOC_URL}).</sub>`
|
|
@@ -1127,7 +1295,11 @@ async function rederiveFromTranscript(path, opts = {}) {
|
|
|
1127
1295
|
const branch = scope?.kind === "branch" ? scope.branch : opts.branch;
|
|
1128
1296
|
const session = branch ? sliceByBranch(loaded, branch) ?? loaded : loaded;
|
|
1129
1297
|
let derived = deriveSpans(session);
|
|
1298
|
+
if (opts.criteria?.length) {
|
|
1299
|
+
derived.criteria = opts.criteria;
|
|
1300
|
+
}
|
|
1130
1301
|
let findings = deriveFindings(derived);
|
|
1302
|
+
const completion = opts.criteria?.length ? computeCompletion(derived, [...findings.main, ...findings.minor]) : void 0;
|
|
1131
1303
|
if (scope?.kind === "diff" && scope.files) {
|
|
1132
1304
|
const scoped = applyDiffScope(derived, findings, scope.files, session.projectPath);
|
|
1133
1305
|
derived = scoped.derived;
|
|
@@ -1151,7 +1323,7 @@ async function rederiveFromTranscript(path, opts = {}) {
|
|
|
1151
1323
|
}
|
|
1152
1324
|
}
|
|
1153
1325
|
}
|
|
1154
|
-
const receipt = await buildReceipt(session, derived, findings, { scope });
|
|
1326
|
+
const receipt = await buildReceipt(session, derived, findings, { scope, completion });
|
|
1155
1327
|
return opts.redact ? redactReceipt(receipt) : receipt;
|
|
1156
1328
|
}
|
|
1157
1329
|
async function compareToTranscript(committed, transcriptPath, opts = {}) {
|
|
@@ -1162,13 +1334,16 @@ async function compareToTranscript(committed, transcriptPath, opts = {}) {
|
|
|
1162
1334
|
// codex/cursor receipt isn't re-run through the Claude adapter. An explicit
|
|
1163
1335
|
// opts.source still wins.
|
|
1164
1336
|
source: opts.source ?? sourceFromReceipt(committed),
|
|
1165
|
-
scope: committed.predicate.scope
|
|
1337
|
+
scope: committed.predicate.scope,
|
|
1338
|
+
// SPEC-0118 — re-apply the recorded criteria basis (like scope) so a receipt
|
|
1339
|
+
// carrying completion findings re-derives byte-identically with no asserts.json.
|
|
1340
|
+
criteria: committed.predicate.evidence.completion?.criteria
|
|
1166
1341
|
});
|
|
1167
1342
|
if (!fresh) {
|
|
1168
1343
|
return { rederived: false, matches: false };
|
|
1169
1344
|
}
|
|
1170
1345
|
const strip = (r) => {
|
|
1171
|
-
if (!r.predicate.prEffort && !r.predicate.prHistory && !r.predicate.prCleared && !r.predicate.guardEvents && !r.predicate.subagents) {
|
|
1346
|
+
if (!r.predicate.prEffort && !r.predicate.prHistory && !r.predicate.prCleared && !r.predicate.guardEvents && !r.predicate.subagents && !r.predicate.scanners && !r.predicate.externalState && !r.predicate.witness) {
|
|
1172
1347
|
return r;
|
|
1173
1348
|
}
|
|
1174
1349
|
const {
|
|
@@ -1177,6 +1352,9 @@ async function compareToTranscript(committed, transcriptPath, opts = {}) {
|
|
|
1177
1352
|
prCleared: _z,
|
|
1178
1353
|
guardEvents: _g,
|
|
1179
1354
|
subagents: _s,
|
|
1355
|
+
scanners: _sc,
|
|
1356
|
+
externalState: _e,
|
|
1357
|
+
witness: _w,
|
|
1180
1358
|
...rest
|
|
1181
1359
|
} = r.predicate;
|
|
1182
1360
|
return { ...r, predicate: rest };
|
|
@@ -1194,6 +1372,7 @@ export {
|
|
|
1194
1372
|
exporterIds,
|
|
1195
1373
|
gitInvocations,
|
|
1196
1374
|
canonicalize,
|
|
1375
|
+
parseExternalClaims,
|
|
1197
1376
|
PR_COMMENT_MARKER,
|
|
1198
1377
|
visibleMergeFindings,
|
|
1199
1378
|
eventKey,
|
|
@@ -1212,4 +1391,4 @@ export {
|
|
|
1212
1391
|
rederiveFromTranscript,
|
|
1213
1392
|
compareToTranscript
|
|
1214
1393
|
};
|
|
1215
|
-
//# sourceMappingURL=chunk-
|
|
1394
|
+
//# sourceMappingURL=chunk-OW3O53BD.js.map
|