altimate-receipts 0.16.0 → 0.18.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.
@@ -1,4 +1,9 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ destructiveOutsideRepo,
4
+ regenerableBuildArtifact,
5
+ regenerableReceiptRm
6
+ } from "./chunk-IDAIDLZT.js";
2
7
 
3
8
  // src/findings/gitParse.ts
4
9
  var WRAPPER = /* @__PURE__ */ new Set(["sudo", "env", "nohup", "command", "stdbuf", "time", "timeout", "xargs"]);
@@ -177,8 +182,8 @@ function shellExecutorArg(clause) {
177
182
  }
178
183
  return void 0;
179
184
  }
180
- function cwdAtFirstGit(command, base5) {
181
- let cur = base5;
185
+ function cwdAtFirstGit(command, base6) {
186
+ let cur = base6;
182
187
  let moved = false;
183
188
  let unknown = false;
184
189
  for (const clause of splitClauses(stripHeredocs(command))) {
@@ -1061,8 +1066,8 @@ function isInScope(path2, promptLc, readSet) {
1061
1066
  if (promptLc.includes(lc)) {
1062
1067
  return true;
1063
1068
  }
1064
- const base5 = lc.split("/").pop();
1065
- if (base5 && base5.length > 2 && promptLc.includes(base5)) {
1069
+ const base6 = lc.split("/").pop();
1070
+ if (base6 && base6.length > 2 && promptLc.includes(base6)) {
1066
1071
  return true;
1067
1072
  }
1068
1073
  const dirs = lc.split("/").filter((d) => d.length > 2);
@@ -1362,10 +1367,74 @@ var CODE_EXT = /\.(?:py|js|ts|tsx|jsx|go|rs|rb|java|kt|sh|bash|toml|cfg|ini|mk|g
1362
1367
  var TEST_FOCUS = /\b(?:it|describe|test|context)\.only\s*\(|\bfdescribe\s*\(|\bfit\s*\(/;
1363
1368
  var CONFIG_FILE = /(?:^|\/)(?:tsconfig[^/]*\.json|\.eslintrc[^/]*|eslint\.config\.[a-z]+|\.flake8|setup\.cfg|pyproject\.toml|jest\.config\.[a-z]+|vitest\.config\.[a-z]+|\.pre-commit-config\.ya?ml)$/i;
1364
1369
  var CONFIG_WEAKEN = /"strict"\s*:\s*false|"noImplicitAny"\s*:\s*false|"strictNullChecks"\s*:\s*false|"skipLibCheck"\s*:\s*true|:\s*["']off["']|coverageThreshold|--passWithNoTests/i;
1370
+ var STEERING_FILE = /(?:^|\/)(?:CLAUDE|AGENTS|GEMINI|MEMORY|\.cursorrules|\.windsurfrules|\.clinerules|copilot-instructions)(?:\.md)?$/i;
1371
+ var SELF_PERMISSION_BYPASS = /\b(?:skip|disable|bypass|don'?t\s+(?:run|bother(?:\s+with)?|worry\s+about)|no\s+need\s+to\s+(?:run|add|write)|you\s+(?:may|can|don'?t\s+(?:have|need)\s+to)\s+(?:skip|disable|bypass|run))\b[^.\n]{0,40}\b(?:tests?|test\s+suite|checks?|lint(?:er|ing)?|hooks?|pre-?commit|ci\b|verif\w+|coverage|type[\s-]?check\w*)\b/i;
1365
1372
  var CICD_FILE = /(?:^|\/)(?:\.github\/workflows\/[^/]+\.ya?ml|\.gitlab-ci\.yml|Jenkinsfile(?:\.[\w.]+)?|\.circleci\/config\.yml|azure-pipelines\.yml|bitbucket-pipelines\.yml|\.drone\.yml)$/i;
1366
1373
  function base2(p) {
1367
1374
  return p.split("/").pop() || p;
1368
1375
  }
1376
+ var CONTROL_FILE = /(?:^|\/)\.claude\/settings(?:\.[A-Za-z0-9_-]+)*\.json$|(?:^|\/)\.claude\/agents\/[^/]+\.md$|(?:^|\/)\.claude\/hooks\/|(?:^|\/)\.githooks\/[^/]+$|(?:^|\/)\.git\/hooks\/[^/]+$|(?:^|\/)policy\/guardrails\.policy\.json$/i;
1377
+ function bashWritesControlFile(cmd) {
1378
+ const redir = cmd.match(/(?:>>?|\btee\b)\s+(?:-\S+\s+)*["']?([^\s"'|;&<>]+)/);
1379
+ if (redir && CONTROL_FILE.test(redir[1])) {
1380
+ return redir[1];
1381
+ }
1382
+ const tokens = cmd.split(/[\s|;&]+/).map((t) => t.replace(/^["']|["']$/g, "")).filter(Boolean);
1383
+ if (/\bsed\b[^|]*\s-i\b/.test(cmd)) {
1384
+ const t = tokens.find((x) => !x.startsWith("-") && CONTROL_FILE.test(x));
1385
+ if (t) {
1386
+ return t;
1387
+ }
1388
+ }
1389
+ if (/\b(?:cp|mv|install|rsync)\b/.test(cmd)) {
1390
+ const ops = tokens.filter((t) => !t.startsWith("-") && !/^(?:cp|mv|install|rsync)$/.test(t));
1391
+ const dest = ops[ops.length - 1];
1392
+ if (dest && CONTROL_FILE.test(dest)) {
1393
+ return dest;
1394
+ }
1395
+ }
1396
+ return void 0;
1397
+ }
1398
+ function deriveSelfPermission(sum) {
1399
+ const out = [];
1400
+ const seen = /* @__PURE__ */ new Set();
1401
+ const push = (spanId, path2, how) => {
1402
+ if (seen.has(path2)) {
1403
+ return;
1404
+ }
1405
+ seen.add(path2);
1406
+ out.push({
1407
+ id: `self-permission-${spanId}`,
1408
+ severity: "critical",
1409
+ title: `Edited its own agent controls: ${base2(path2)}`,
1410
+ detail: `\`${path2}\` is one of the agent's own Claude Code control files (permission allow-list, subagent tool allow-list, or a hook script) \u2014 it was written ${how} this session. Changing it loosens the very fence the agent runs inside. Confirm the agent was meant to change its own permissions/hooks; revert if not.`,
1411
+ impactLabel: "self-permissioning",
1412
+ confidence: 0.9,
1413
+ score: 1e3 * 0.9 * 0.9,
1414
+ evidenceSpanId: spanId,
1415
+ filePath: path2,
1416
+ guardrailRule: "Don't let an agent edit its own .claude/settings, agent definitions, or hooks \u2014 change those yourself, in review."
1417
+ });
1418
+ };
1419
+ for (const s of sum.spans) {
1420
+ if (s.kind !== "tool") {
1421
+ continue;
1422
+ }
1423
+ if (isEditTool(s.name)) {
1424
+ const fp = filePathOf(s.input);
1425
+ if (fp && CONTROL_FILE.test(fp)) {
1426
+ push(s.spanId, fp, "via an edit");
1427
+ }
1428
+ } else if (isCommandTool(s.name)) {
1429
+ const cmd = commandOf(s.input);
1430
+ const target = cmd ? bashWritesControlFile(cmd) : void 0;
1431
+ if (target) {
1432
+ push(s.spanId, target, `via the command \`${cmd.trim().slice(0, 120)}\``);
1433
+ }
1434
+ }
1435
+ }
1436
+ return out;
1437
+ }
1369
1438
  function deriveBypassFindings(sum) {
1370
1439
  const out = [];
1371
1440
  const tools = sum.spans.filter((s) => s.kind === "tool").sort((a, b) => a.startTime - b.startTime || a.spanId.localeCompare(b.spanId));
@@ -1374,6 +1443,11 @@ function deriveBypassFindings(sum) {
1374
1443
  let focus;
1375
1444
  let configWeaken;
1376
1445
  let ciCdTouch;
1446
+ let specTamper;
1447
+ const promptLc = (sum.prompt ?? "").toLowerCase();
1448
+ const editingInstructionsIsTask = /\b(?:updat|edit|chang|add(?:ing)?|writ|revis|amend|fix)\w*\b[^.\n]{0,40}\b(?:instruction|rule|guideline|guardrail|memory|steering|claude\.?md|agents\.?md|context\s+file|prompt)/i.test(
1449
+ promptLc
1450
+ );
1377
1451
  for (const s of tools) {
1378
1452
  if (isCommandTool(s.name)) {
1379
1453
  const invs = parseGitInvocations(commandOf(s.input));
@@ -1441,6 +1515,20 @@ function deriveBypassFindings(sum) {
1441
1515
  guardrailRule: `Don't disable type-strictness, lint rules, or coverage thresholds to pass; fix the code the check flags.`
1442
1516
  };
1443
1517
  }
1518
+ if (!specTamper && STEERING_FILE.test(fp) && !editingInstructionsIsTask && SELF_PERMISSION_BYPASS.test(newStr) && !SELF_PERMISSION_BYPASS.test(oldStr)) {
1519
+ specTamper = {
1520
+ id: `spec-tamper-${s.spanId}`,
1521
+ severity: "high",
1522
+ title: `Granted itself permission to bypass a check in ${base2(fp)}`,
1523
+ detail: `An edit to \`${base2(fp)}\` \u2014 the agent's own steering/instructions file \u2014 added language permitting it to skip or disable an oversight mechanism (tests, checks, lint, hooks, or CI). An agent rewriting the rules it operates under to license skipping verification is a spec-gaming signal (Specification Self-Correction, arXiv:2507.18742). Review what oversight it exempted itself from.`,
1524
+ impactLabel: "rewrote its own rules",
1525
+ confidence: 0.8,
1526
+ score: 100 * 0.9 * 0.8,
1527
+ evidenceSpanId: s.spanId,
1528
+ filePath: fp,
1529
+ guardrailRule: "Never edit your own instructions/steering file to permit skipping tests, checks, or hooks; satisfy the oversight instead."
1530
+ };
1531
+ }
1444
1532
  if (!ciCdTouch && CICD_FILE.test(fp)) {
1445
1533
  ciCdTouch = {
1446
1534
  id: `ci-cd-touch-${s.spanId}`,
@@ -1457,7 +1545,7 @@ function deriveBypassFindings(sum) {
1457
1545
  }
1458
1546
  }
1459
1547
  }
1460
- for (const f of [hookBypass, forcePush, focus, configWeaken, ciCdTouch]) {
1548
+ for (const f of [hookBypass, forcePush, focus, configWeaken, ciCdTouch, specTamper]) {
1461
1549
  if (f) {
1462
1550
  out.push(f);
1463
1551
  }
@@ -1465,6 +1553,112 @@ function deriveBypassFindings(sum) {
1465
1553
  return out;
1466
1554
  }
1467
1555
 
1556
+ // src/findings/commandSecurityFindings.ts
1557
+ var isPrintLead = (cmd) => /^\s*(echo|printf|:|#|cat\s*<<)/.test(cmd);
1558
+ var deQuote = (cmd) => cmd.replace(/'[^']*'/g, "''").replace(/"[^"]*"/g, '""');
1559
+ var METADATA = /\b(?:169\.254\.169\.254|169\.254\.170\.2|metadata\.google\.internal|metadata\.goog)\b|\[fd00:ec2::254\]/i;
1560
+ var TLS_DISABLE = /\bcurl\b[^|;&]*\s(?:-k|--insecure)\b|--no-check-certificate\b|NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*0|PYTHONHTTPSVERIFY\s*=\s*0|\bgit\b[^|;&]*-c\s+http\.sslVerify=false|--strict-ssl[ =]false|\bcurl\b[^|;&]*\s--cacert\s+\/dev\/null/i;
1561
+ var SETUID = /\bchmod\b[^|;&]*\s(?:[ug]\+s|\+s)\b|\bchmod\b[^|;&]*\s(?:[2467])[0-7]{3}\b/i;
1562
+ var OBFUSCATED_PIPE = /\bbase64\s+(?:-d|--decode)\b[^|]*\|\s*(?:sudo\s+)?(?:ba|z)?sh\b|\bxxd\s+-r\b[^|]*\|\s*(?:ba)?sh\b|\bbase64\s+(?:-d|--decode)\b[^|]*\|\s*python/i;
1563
+ var OBFUSCATED_INLINE = /python[0-9.]*\s+-c\s*["'][^"']*\b(?:exec|eval)\(|node\s+-e\s*["'][^"']*\b(?:eval|Function|atob)\(|\beval\s+"\$\(/i;
1564
+ var EVAL_ALLOW = /\beval\s+"\$\(\s*(?:ssh-agent|direnv\s+hook|rbenv\s+init|pyenv\s+init|nodenv\s+init|brew\s+shellenv|zoxide\s+init|starship\s+init|fnm\s+env|mise\s+activate|conda\s+shell)/i;
1565
+ var PIP_INSTALL = /\b(?:pip3?|uv\s+pip)\s+install\b/i;
1566
+ var NPM_INSTALL = /\b(?:npm\s+(?:i|install|add)|yarn\s+add|pnpm\s+add)\b/i;
1567
+ var NONREGISTRY = /\b(?:git\+[a-z]+:\/\/|hg\+|svn\+|bzr\+|https?:\/\/\S+\.(?:whl|tar\.gz|tgz)|git\+ssh|github:|gitlab:|bitbucket:)/i;
1568
+ var VERIFY_CMD = /\b(?:pytest|go\s+test|cargo\s+(?:test|clippy)|(?:npm|yarn|pnpm)\s+(?:run\s+)?(?:test|lint|typecheck)|jest|vitest|mocha|rspec|phpunit|dotnet\s+test|gradle\s+test|mvn\s+test|tox|nox|ruff|flake8|pylint|mypy|pyright|eslint|tsc|golangci-lint|(?:black|prettier)\b[^|;&]*--check)\b/i;
1569
+ var MASK = /\|\|\s*(?:true\b|exit\s+0\b|:(?![\w-]))|;\s*(?:true|exit\s+0)\s*$/i;
1570
+ function classify(rawCmd) {
1571
+ const raw = stripHeredocs(rawCmd);
1572
+ const dq = deQuote(raw);
1573
+ const print = isPrintLead(raw);
1574
+ const clip = (c) => c.trim().slice(0, 140);
1575
+ if (!print && METADATA.test(dq)) {
1576
+ return {
1577
+ id: "metadata-egress",
1578
+ severity: "high",
1579
+ title: "Contacted the cloud metadata endpoint",
1580
+ detail: `A command reached a cloud instance-metadata / link-local endpoint (\`${clip(rawCmd)}\`) \u2014 the SSRF / cloud-credential surface. Confirm this access was intended; rotate instance credentials if it wasn't.`,
1581
+ guardrail: "Agents should never contact 169.254.169.254 / metadata.google.internal; it exposes cloud IAM credentials."
1582
+ };
1583
+ }
1584
+ if (!print && TLS_DISABLE.test(dq)) {
1585
+ return {
1586
+ id: "tls-verify-disabled",
1587
+ severity: "high",
1588
+ title: "Disabled TLS / certificate verification",
1589
+ detail: `A command turned off TLS/cert verification (\`${clip(rawCmd)}\`), exposing whatever it fetched to man-in-the-middle. Re-run with verification; confirm nothing sensitive crossed an unverified channel.`,
1590
+ guardrail: "Never disable TLS verification (curl -k, NODE_TLS_REJECT_UNAUTHORIZED=0, verify=False); fix the cert instead."
1591
+ };
1592
+ }
1593
+ if (!print && SETUID.test(dq)) {
1594
+ return {
1595
+ id: "setuid-create",
1596
+ severity: "high",
1597
+ title: "Set the setuid/setgid bit on a file",
1598
+ detail: `A command set the setuid/setgid bit (\`${clip(rawCmd)}\`) \u2014 a binary that runs with the owner's privileges regardless of caller, the canonical privilege-escalation backdoor. Confirm a setuid binary was intended; remove the bit if not.`,
1599
+ guardrail: "Don't create setuid/setgid binaries (chmod u+s / 4755) from an agent session."
1600
+ };
1601
+ }
1602
+ if (OBFUSCATED_PIPE.test(dq) || !print && !EVAL_ALLOW.test(raw) && OBFUSCATED_INLINE.test(raw)) {
1603
+ return {
1604
+ id: "obfuscated-exec",
1605
+ severity: "high",
1606
+ title: "Decoded and executed an obfuscated payload",
1607
+ detail: `A command decoded an encoded blob and executed it (\`${clip(rawCmd)}\`) \u2014 the executed code is hidden from the diff. Inspect the decoded payload before trusting this change.`,
1608
+ guardrail: `Don't decode-then-execute (base64 -d | sh, eval "$(\u2026)"); download, inspect, then run.`
1609
+ };
1610
+ }
1611
+ if (!print && (PIP_INSTALL.test(dq) || NPM_INSTALL.test(dq)) && NONREGISTRY.test(dq)) {
1612
+ return {
1613
+ id: "untrusted-dep-source",
1614
+ severity: "high",
1615
+ title: "Installed a dependency from a non-registry source",
1616
+ detail: `An install pulled a package from a URL / VCS / tarball rather than the package registry (\`${clip(rawCmd)}\`) \u2014 unvetted code in the build. Confirm the source is trusted and pinned.`,
1617
+ guardrail: "Install dependencies from the registry with a pinned version; review any git+/URL/tarball source."
1618
+ };
1619
+ }
1620
+ if (!print && VERIFY_CMD.test(dq) && MASK.test(dq)) {
1621
+ return {
1622
+ id: "masked-failure",
1623
+ severity: "high",
1624
+ title: "Suppressed a verification command's failure",
1625
+ detail: `A test/lint/typecheck command was forced to succeed (\`${clip(rawCmd)}\`) \u2014 its non-zero exit was discarded with \`|| true\`/\`; true\`, so a failing gate reads as passed. Re-run it without the success-forcer and fix the real failure before trusting this as verified.`,
1626
+ guardrail: "Don't mask a verification command with `|| true` / `; true`; a forced-green gate proves nothing."
1627
+ };
1628
+ }
1629
+ return void 0;
1630
+ }
1631
+ function deriveCommandSecurityFindings(sum) {
1632
+ const out = [];
1633
+ const seen = /* @__PURE__ */ new Set();
1634
+ for (const s of sum.spans) {
1635
+ if (s.kind !== "tool" || !isCommandTool(s.name)) {
1636
+ continue;
1637
+ }
1638
+ const cmd = commandOf(s.input);
1639
+ if (!cmd) {
1640
+ continue;
1641
+ }
1642
+ const hit = classify(cmd);
1643
+ if (!hit || seen.has(hit.id)) {
1644
+ continue;
1645
+ }
1646
+ seen.add(hit.id);
1647
+ out.push({
1648
+ id: `${hit.id}-${s.spanId}`,
1649
+ severity: hit.severity,
1650
+ title: hit.title,
1651
+ detail: hit.detail,
1652
+ impactLabel: hit.id,
1653
+ confidence: 0.9,
1654
+ score: 100 * 0.9,
1655
+ evidenceSpanId: s.spanId,
1656
+ guardrailRule: hit.guardrail
1657
+ });
1658
+ }
1659
+ return out;
1660
+ }
1661
+
1468
1662
  // src/findings/compositeFindings.ts
1469
1663
  var COMPLETION = /\b(all tests? (?:now )?pass(?:ing|ed)?|tests? (?:now )?pass(?:ing|ed)?|all green|everything passes|works now|done|fixed|resolved)\b/i;
1470
1664
  var HEDGE = /\b(should|probably|might|maybe|may|i think|hopefully|in theory|ought to|expects? to|expected to|seems? to|presumably)\b/i;
@@ -2525,6 +2719,261 @@ function deriveInjectionFindings(sum) {
2525
2719
  return [];
2526
2720
  }
2527
2721
 
2722
+ // src/findings/mcpFindings.ts
2723
+ var MUTATING_VERBS = /* @__PURE__ */ new Set([
2724
+ "create",
2725
+ "update",
2726
+ "delete",
2727
+ "remove",
2728
+ "post",
2729
+ "send",
2730
+ "write",
2731
+ "insert",
2732
+ "upsert",
2733
+ "merge",
2734
+ "deploy",
2735
+ "publish",
2736
+ "set",
2737
+ "add",
2738
+ "move",
2739
+ "rename",
2740
+ "archive",
2741
+ "close",
2742
+ "comment",
2743
+ "approve",
2744
+ "revoke",
2745
+ "trigger",
2746
+ "run",
2747
+ "execute",
2748
+ "invoke",
2749
+ "edit",
2750
+ "patch",
2751
+ "put",
2752
+ "upload",
2753
+ "push",
2754
+ "schedule",
2755
+ "share",
2756
+ "duplicate"
2757
+ ]);
2758
+ var DURABLE_SERVER = /github|gitlab|bitbucket|linear|jira|asana|slack|discord|notion|confluence|hubspot|salesforce|stripe|postgres|mysql|sql|mongo|database|s3|gcs|sheets|gmail|calendar|drive|fireflies|clay|apollo/i;
2759
+ var EPHEMERAL_SERVER = /chrome|playwright|puppeteer|browser|selenium|webdriver/i;
2760
+ function parseMcp(name) {
2761
+ if (!name.startsWith("mcp__")) {
2762
+ return void 0;
2763
+ }
2764
+ const rest = name.slice("mcp__".length);
2765
+ const i = rest.lastIndexOf("__");
2766
+ if (i < 0) {
2767
+ return { server: rest, action: "" };
2768
+ }
2769
+ return { server: rest.slice(0, i), action: rest.slice(i + 2) };
2770
+ }
2771
+ function isMutatingAction(action) {
2772
+ return action.toLowerCase().split(/[-_]+/).some((tok) => MUTATING_VERBS.has(tok));
2773
+ }
2774
+ function deriveMcpFindings(sum) {
2775
+ const calls = /* @__PURE__ */ new Map();
2776
+ let firstSpanId;
2777
+ for (const s of sum.spans) {
2778
+ if (s.kind !== "tool" || typeof s.name !== "string") {
2779
+ continue;
2780
+ }
2781
+ const p = parseMcp(s.name);
2782
+ if (!p || !p.action || !isMutatingAction(p.action) || EPHEMERAL_SERVER.test(p.server)) {
2783
+ continue;
2784
+ }
2785
+ const key = `${p.server}::${p.action}`;
2786
+ const prev = calls.get(key);
2787
+ if (prev) {
2788
+ prev.count++;
2789
+ } else {
2790
+ calls.set(key, { server: p.server, action: p.action, count: 1 });
2791
+ }
2792
+ if (!firstSpanId) {
2793
+ firstSpanId = s.spanId;
2794
+ }
2795
+ }
2796
+ if (calls.size === 0) {
2797
+ return [];
2798
+ }
2799
+ const list = [...calls.values()];
2800
+ const total = list.reduce((n, c) => n + c.count, 0);
2801
+ const durable = list.some((c) => DURABLE_SERVER.test(c.server));
2802
+ const label = list.slice(0, 8).map((c) => `\`${c.server}::${c.action}\`${c.count > 1 ? ` \xD7${c.count}` : ""}`).join(", ");
2803
+ return [
2804
+ {
2805
+ id: `mcp-write-${firstSpanId ?? "0"}`,
2806
+ severity: durable ? "high" : "medium",
2807
+ title: `${total} external write${total === 1 ? "" : "s"} via MCP tools`,
2808
+ detail: `The session made ${total} mutating MCP tool call${total === 1 ? "" : "s"} \u2014 external side-effects on systems the PR diff does not show: ${label}. Confirm these external effects were intended; undo any that weren't.`,
2809
+ impactLabel: "external side-effects",
2810
+ confidence: 0.9,
2811
+ score: (durable ? 100 : 10) * 0.9,
2812
+ evidenceSpanId: firstSpanId,
2813
+ guardrailRule: "An agent's MCP writes (issues, messages, DB rows, files in external systems) aren't in the diff \u2014 review them as part of the change."
2814
+ }
2815
+ ];
2816
+ }
2817
+
2818
+ // src/findings/persistenceFindings.ts
2819
+ var AUTORUN_PATH = /(?:^|\/)\.(?:bashrc|zshrc|bash_profile|zprofile|profile|bash_login|zshenv)$|(?:^|\/)\.config\/fish\/config\.fish$|^\/etc\/profile(?:\.d\/.+)?$|(?:^|\/)Library\/LaunchAgents\/[^/]+|^\/Library\/Launch(?:Daemons|Agents)\/[^/]+|^\/etc\/systemd\/system\/[^/]+|(?:^|\/)\.config\/systemd\/user\/[^/]+|^\/etc\/cron[^/]*|^\/var\/spool\/cron\//i;
2820
+ var PERSIST_CMD = /\bcrontab\b(?!\s+(?:-l|-r)\b)|\blaunchctl\s+(?:load|bootstrap|enable)\b|\bsystemctl\s+(?:--user\s+)?enable\b|\bgit\b[^|;&]*\bconfig\b[^|;&]*--global[^|;&]*core\.hooksPath/i;
2821
+ var isPrintLead2 = (cmd) => /^\s*(echo|printf|:|#|cat\s*<<)/.test(cmd);
2822
+ var deQuote2 = (cmd) => cmd.replace(/'[^']*'/g, "''").replace(/"[^"]*"/g, '""');
2823
+ function commandWritesAutorun(cmd) {
2824
+ const m = cmd.match(/(?:>>?|\btee\b)\s+(?:-\S+\s+)*["']?([^\s"'|;&<>]+)/);
2825
+ if (m && AUTORUN_PATH.test(m[1])) {
2826
+ return m[1];
2827
+ }
2828
+ return void 0;
2829
+ }
2830
+ function derivePersistenceFindings(sum) {
2831
+ const out = [];
2832
+ const seen = /* @__PURE__ */ new Set();
2833
+ const push = (spanId, what, how) => {
2834
+ if (seen.has(what)) {
2835
+ return;
2836
+ }
2837
+ seen.add(what);
2838
+ out.push({
2839
+ id: `persistence-write-${spanId}`,
2840
+ severity: "high",
2841
+ title: `Established auto-run persistence: ${what}`,
2842
+ detail: `\`${what}\` is an OS auto-run surface (shell startup / cron / launchd / systemd) \u2014 code placed here runs on the next shell, login, or boot. It was written ${how} this session, outside the PR diff. Confirm the auto-run was intended; remove it if not.`,
2843
+ impactLabel: "persistence",
2844
+ confidence: 0.9,
2845
+ score: 100 * 0.9,
2846
+ evidenceSpanId: spanId,
2847
+ guardrailRule: "Don't let an agent write shell-rc / cron / launchd / systemd auto-run surfaces; make persistence changes yourself, in review."
2848
+ });
2849
+ };
2850
+ for (const s of sum.spans) {
2851
+ if (s.kind !== "tool") {
2852
+ continue;
2853
+ }
2854
+ if (isEditTool(s.name)) {
2855
+ const fp = filePathOf(s.input);
2856
+ if (fp && AUTORUN_PATH.test(fp)) {
2857
+ push(s.spanId, fp, "via an edit");
2858
+ }
2859
+ } else if (isCommandTool(s.name)) {
2860
+ const raw = stripHeredocs(commandOf(s.input));
2861
+ if (!raw) {
2862
+ continue;
2863
+ }
2864
+ const dq = deQuote2(raw);
2865
+ const written = commandWritesAutorun(dq);
2866
+ if (written) {
2867
+ push(s.spanId, written, `via the command \`${raw.trim().slice(0, 120)}\``);
2868
+ } else if (!isPrintLead2(raw) && PERSIST_CMD.test(dq)) {
2869
+ push(s.spanId, raw.trim().slice(0, 60), `via the command \`${raw.trim().slice(0, 120)}\``);
2870
+ }
2871
+ }
2872
+ }
2873
+ return out;
2874
+ }
2875
+
2876
+ // src/findings/sensitiveAccess.ts
2877
+ var CRITICAL = [
2878
+ /(?:^|\/)\.ssh(?:\/|$)/i,
2879
+ /(?:^|\/)\.aws(?:\/|$)/i,
2880
+ /(?:^|\/)\.gnupg(?:\/|$)/i,
2881
+ /(?:^|\/)\.kube(?:\/|$)/i,
2882
+ /(?:^|\/)id_rsa\b/i,
2883
+ /(?:^|\/)id_ed25519\b/i,
2884
+ /(?:^|\/)id_ecdsa\b/i,
2885
+ /\.pem$/i,
2886
+ /\.key$/i,
2887
+ /\.p12$/i,
2888
+ /\.keychain$/i,
2889
+ /(?:^|\/)\.git-credentials$/i,
2890
+ /^\/etc\/shadow\b/i,
2891
+ /^\/etc\/gshadow\b/i,
2892
+ /^\/etc\/passwd\b/i,
2893
+ /^\/etc\/sudoers(?:\.d)?(?:\/|$)/i,
2894
+ /^\/etc\/ssh(?:\/|$)/i,
2895
+ /^\/root(?:\/|$)/i,
2896
+ /^\/(?:private\/)?etc\/master\.passwd\b/i
2897
+ ];
2898
+ var HIGH = [
2899
+ /(?:^|\/)\.env(?:\.[A-Za-z0-9_-]+)?$/i,
2900
+ /(?:^|\/)\.netrc$/i,
2901
+ /(?:^|\/)\.npmrc$/i,
2902
+ /(?:^|\/)\.pypirc$/i,
2903
+ /(?:^|\/)\.bash_history$/i,
2904
+ /(?:^|\/)\.zsh_history$/i
2905
+ ];
2906
+ var EXCLUDE = /\.(?:example|sample|template|dist)$/i;
2907
+ function sensitivity(path2) {
2908
+ const p = path2.replace(/^["']|["']$/g, "").trim();
2909
+ if (!p || EXCLUDE.test(p)) {
2910
+ return void 0;
2911
+ }
2912
+ if (CRITICAL.some((re) => re.test(p))) {
2913
+ return "critical";
2914
+ }
2915
+ if (HIGH.some((re) => re.test(p))) {
2916
+ return "high";
2917
+ }
2918
+ return void 0;
2919
+ }
2920
+ function sensitiveInCommand(cmd) {
2921
+ for (const raw of cmd.split(/[\s|<>;&=()]+/)) {
2922
+ const tok = raw.replace(/^["']|["']$/g, "");
2923
+ if (!tok || tok.startsWith("-")) {
2924
+ continue;
2925
+ }
2926
+ const tier = sensitivity(tok);
2927
+ if (tier) {
2928
+ return { path: tok, tier };
2929
+ }
2930
+ }
2931
+ return void 0;
2932
+ }
2933
+ var base5 = (p) => p.replace(/\/+$/, "").split("/").pop() || p;
2934
+ function deriveSensitiveAccess(sum) {
2935
+ const findings = [];
2936
+ const seen = /* @__PURE__ */ new Set();
2937
+ for (const s of sum.spans) {
2938
+ let hit;
2939
+ if (isReadTool(s.name)) {
2940
+ const fp = filePathOf(s.input);
2941
+ const tier = fp ? sensitivity(fp) : void 0;
2942
+ if (fp && tier) {
2943
+ hit = { path: fp, tier, how: "read" };
2944
+ }
2945
+ } else if (isCommandTool(s.name)) {
2946
+ const cmd = commandOf(s.input);
2947
+ const m = cmd ? sensitiveInCommand(cmd) : void 0;
2948
+ if (m) {
2949
+ hit = { path: m.path, tier: m.tier, how: `the command \`${cmd.trim().slice(0, 120)}\`` };
2950
+ }
2951
+ }
2952
+ if (!hit) {
2953
+ continue;
2954
+ }
2955
+ const key = `${hit.path}|${hit.how === "read" ? "read" : "cmd"}`;
2956
+ if (seen.has(key)) {
2957
+ continue;
2958
+ }
2959
+ seen.add(key);
2960
+ const verb = hit.how === "read" ? "Read" : "Accessed";
2961
+ const via = hit.how === "read" ? "via a file read" : `via ${hit.how}`;
2962
+ findings.push({
2963
+ id: `sensitive-access-${s.spanId}`,
2964
+ severity: hit.tier,
2965
+ title: `${verb} a credential file: ${base5(hit.path)}`,
2966
+ detail: `\`${hit.path}\` was accessed this session ${via} \u2014 a credential / identity file outside the change under review. Confirm the access was intended; rotate the credential if it wasn't meant to be exposed to the agent.`,
2967
+ confidence: 0.9,
2968
+ score: 0,
2969
+ evidenceSpanId: s.spanId,
2970
+ filePath: hit.path,
2971
+ guardrailRule: "Don't read secret or identity files (~/.ssh, ~/.aws, .env, *.pem) from an agent session; inject what's needed through the environment instead."
2972
+ });
2973
+ }
2974
+ return findings;
2975
+ }
2976
+
2528
2977
  // src/findings/toolUseFindings.ts
2529
2978
  var WRAP = /<tool_use_error>([\s\S]*?)<\/tool_use_error>/i;
2530
2979
  var FABRICATED = /no such tool|unknown tool|tool .{0,30}not found|not a valid tool|no tool named/i;
@@ -2595,6 +3044,63 @@ var SEVERITY_WEIGHT = {
2595
3044
  function scoreOf(sev, impact, confidence) {
2596
3045
  return SEVERITY_WEIGHT[sev] * (0.2 + Math.min(1, impact)) * confidence;
2597
3046
  }
3047
+ function deriveTruncatedTurn(sum) {
3048
+ const truncated = sum.spans.filter(
3049
+ (s) => s.kind === "generation" && (s.finishReason === "length" || s.finishReason === "content_filter" || s.finishReason === "error")
3050
+ );
3051
+ if (truncated.length === 0) {
3052
+ return [];
3053
+ }
3054
+ const t = truncated[0];
3055
+ const more = truncated.length > 1 ? ` (${truncated.length} turns)` : "";
3056
+ const why = t.finishReason === "length" ? "hit the output-token ceiling (max_tokens)" : t.finishReason === "content_filter" ? "was stopped by a content filter" : "ended in an error";
3057
+ return [
3058
+ {
3059
+ id: `truncated-turn-${t.spanId}`,
3060
+ severity: "high",
3061
+ title: `Turn cut off mid-output${more}`,
3062
+ detail: `A generation ${why}, so the agent's last edit/command/tool-call in that turn may be truncated. Re-check the work produced right after it.`,
3063
+ impactLabel: "turn truncated",
3064
+ confidence: 0.8,
3065
+ score: scoreOf("high", 0.8, 0.8),
3066
+ evidenceSpanId: t.spanId,
3067
+ guardrailRule: "A length-stopped turn likely left work half-done; raise max_tokens or continue the turn, then re-check the last edit/command."
3068
+ }
3069
+ ];
3070
+ }
3071
+ var DETECTORS = [
3072
+ // correctness & trust findings
3073
+ { name: "correctness", fn: (s) => deriveCorrectnessFindings(s) },
3074
+ // edit-body scan: reward-hacking against the test suite
3075
+ { name: "edit-scan", fn: (s) => deriveEditScanFindings(s) },
3076
+ // composite (M13): correlate a green claim with a same-session test suppression never re-greened.
3077
+ // Pure correlation over the rows already pushed above — reads `prior`.
3078
+ { name: "composite", fn: (s, prior) => deriveCompositeFindings(s, prior) },
3079
+ // audit composites (M80, experimental): gated until validated on the live fleet.
3080
+ {
3081
+ name: "audit",
3082
+ fn: (s) => process.env.RECEIPTS_EXPERIMENTAL_DETECTORS === "1" ? deriveAuditFindings(s) : []
3083
+ },
3084
+ // tool-use validity: fabricated tools / malformed arguments
3085
+ { name: "tool-use", fn: (s) => deriveToolUseFindings(s) },
3086
+ // check / guardrail bypass: agent circumvented its own oversight
3087
+ { name: "bypass", fn: (s) => deriveBypassFindings(s) },
3088
+ // self-permissioning (M101): agent edited its own .claude controls / hooks
3089
+ { name: "self-permission", fn: (s) => deriveSelfPermission(s) },
3090
+ // prompt injection: external content the agent ingested tried to hijack it
3091
+ { name: "injection", fn: (s) => deriveInjectionFindings(s) },
3092
+ // sensitive-path access (M100): the agent read a credential / identity file
3093
+ { name: "sensitive-access", fn: (s) => deriveSensitiveAccess(s) },
3094
+ // MCP external side-effects (M105): mutating MCP tool calls the diff can't show
3095
+ { name: "mcp", fn: (s) => deriveMcpFindings(s) },
3096
+ // command-pattern security (M106/108/109/110/111/112): metadata egress, untrusted dep source,
3097
+ // TLS-disable, setuid, obfuscated exec, masked verification failure
3098
+ { name: "command-security", fn: (s) => deriveCommandSecurityFindings(s) },
3099
+ // persistence (M107): agent wrote an OS auto-run surface (shell-rc/cron/launchd/systemd)
3100
+ { name: "persistence", fn: (s) => derivePersistenceFindings(s) },
3101
+ // truncated turn (M35): the provider cut a turn off; the last edit/command may be half-written
3102
+ { name: "truncated-turn", fn: (s) => deriveTruncatedTurn(s) }
3103
+ ];
2598
3104
  function deriveFindings(sum) {
2599
3105
  const findings = [];
2600
3106
  const total = sum.totalCost || 0;
@@ -2809,33 +3315,8 @@ function deriveFindings(sum) {
2809
3315
  fixPrompt: `In a recent session, ${simpleExpensive.length} short steps ran on a frontier model when a cheaper tier would have sufficed (~${formatCostAlways(saveable)} of avoidable spend). Suggest which steps to route to a cheaper model and how.`
2810
3316
  });
2811
3317
  }
2812
- findings.push(...deriveCorrectnessFindings(sum));
2813
- findings.push(...deriveEditScanFindings(sum));
2814
- findings.push(...deriveCompositeFindings(sum, findings));
2815
- if (process.env.RECEIPTS_EXPERIMENTAL_DETECTORS === "1") {
2816
- findings.push(...deriveAuditFindings(sum));
2817
- }
2818
- findings.push(...deriveToolUseFindings(sum));
2819
- findings.push(...deriveBypassFindings(sum));
2820
- findings.push(...deriveInjectionFindings(sum));
2821
- const truncated = sum.spans.filter(
2822
- (s) => s.kind === "generation" && (s.finishReason === "length" || s.finishReason === "content_filter" || s.finishReason === "error")
2823
- );
2824
- if (truncated.length > 0) {
2825
- const t = truncated[0];
2826
- const more = truncated.length > 1 ? ` (${truncated.length} turns)` : "";
2827
- const why = t.finishReason === "length" ? "hit the output-token ceiling (max_tokens)" : t.finishReason === "content_filter" ? "was stopped by a content filter" : "ended in an error";
2828
- findings.push({
2829
- id: `truncated-turn-${t.spanId}`,
2830
- severity: "high",
2831
- title: `Turn cut off mid-output${more}`,
2832
- detail: `A generation ${why}, so the agent's last edit/command/tool-call in that turn may be truncated. Re-check the work produced right after it.`,
2833
- impactLabel: "turn truncated",
2834
- confidence: 0.8,
2835
- score: scoreOf("high", 0.8, 0.8),
2836
- evidenceSpanId: t.spanId,
2837
- guardrailRule: "A length-stopped turn likely left work half-done; raise max_tokens or continue the turn, then re-check the last edit/command."
2838
- });
3318
+ for (const detector of DETECTORS) {
3319
+ findings.push(...detector.fn(sum, findings));
2839
3320
  }
2840
3321
  const rewriteSpans = new Set(
2841
3322
  findings.filter((f) => f.id.startsWith("history-rewrite")).map((f) => f.evidenceSpanId)
@@ -2888,74 +3369,6 @@ function gradeLetter(main) {
2888
3369
 
2889
3370
  // src/trace/diffScope.ts
2890
3371
  import { spawnSync } from "child_process";
2891
-
2892
- // src/findings/surface.ts
2893
- var OPERATOR_KINDS = /* @__PURE__ */ new Set([
2894
- "loop",
2895
- "bottleneck",
2896
- "cost-concentration",
2897
- "cache-opportunity",
2898
- "model-downgrade"
2899
- ]);
2900
- var PRIVILEGED_PREFIXES = [
2901
- "ci-cd-touch",
2902
- "lockfile-edit",
2903
- "hook-bypass",
2904
- "config-weaken",
2905
- "grader-edit",
2906
- "eval-override",
2907
- "test-focus",
2908
- "test-skipped",
2909
- "test-trivialised",
2910
- "green-by-suppression",
2911
- "untested-test",
2912
- "history-rewrite",
2913
- "force-push"
2914
- ];
2915
- var TEST_PATH3 = /(?:^|\/)(?:tests?|specs?|__tests__)(?:\/|$)|\.(?:test|spec)\.|_test\./;
2916
- function privileged(id, filePath) {
2917
- if (PRIVILEGED_PREFIXES.some((p) => id.startsWith(p))) {
2918
- return true;
2919
- }
2920
- return id.startsWith("file-shrink") && !!filePath && TEST_PATH3.test(filePath);
2921
- }
2922
- function findingSurface(id) {
2923
- if (OPERATOR_KINDS.has(id) || id.startsWith("errcluster-")) {
2924
- return "operator";
2925
- }
2926
- return "merge";
2927
- }
2928
- function destructiveOutsideRepo(title) {
2929
- const clause = title.replace(/^Destructive op:\s*/i, "").replace(/\s*×\d+\s*$/, "");
2930
- const m = clause.match(/^\s*(rm|rmdir)\b(.*)$/i);
2931
- if (!m) {
2932
- return false;
2933
- }
2934
- const operands = m[2].split(/\s+/).filter((t) => t && !t.startsWith("-"));
2935
- if (operands.length === 0) {
2936
- return false;
2937
- }
2938
- const scratch = (t) => {
2939
- const p = t.replace(/^["']|["']$/g, "");
2940
- return /\$/.test(p) || p.startsWith("/tmp") || p.startsWith("/var/folders") || p.startsWith("/private/var/folders") || p.startsWith("~");
2941
- };
2942
- return operands.every(scratch);
2943
- }
2944
- function regenerableReceiptRm(title) {
2945
- const clause = title.replace(/^Destructive op:\s*/i, "").replace(/\s*×\d+\s*$/, "");
2946
- const m = clause.match(/^\s*(rm|rmdir)\b(.*)$/i);
2947
- if (!m) {
2948
- return false;
2949
- }
2950
- const operands = m[2].split(/\s+/).filter((t) => t && !t.startsWith("-"));
2951
- if (operands.length === 0) {
2952
- return false;
2953
- }
2954
- const receiptJson = (t) => /(?:^|\/)\.receipts\/[^/]+\.json$/.test(t.replace(/^["']|["']$/g, ""));
2955
- return operands.every(receiptJson);
2956
- }
2957
-
2958
- // src/trace/diffScope.ts
2959
3372
  function git(args, cwd) {
2960
3373
  const r = spawnSync("git", args, { encoding: "utf8", cwd });
2961
3374
  return r.status === 0 ? r.stdout : null;
@@ -2966,22 +3379,22 @@ function changedFiles(baseOverride, opts) {
2966
3379
  if (!root) {
2967
3380
  return null;
2968
3381
  }
2969
- let base5 = baseOverride;
2970
- if (!base5) {
3382
+ let base6 = baseOverride;
3383
+ if (!base6) {
2971
3384
  const sym = git(["symbolic-ref", "refs/remotes/origin/HEAD"], root)?.trim();
2972
3385
  if (sym) {
2973
- base5 = sym.replace("refs/remotes/", "");
3386
+ base6 = sym.replace("refs/remotes/", "");
2974
3387
  } else if (git(["rev-parse", "--verify", "--quiet", "origin/main"], root) !== null) {
2975
- base5 = "origin/main";
3388
+ base6 = "origin/main";
2976
3389
  } else {
2977
- base5 = "main";
3390
+ base6 = "main";
2978
3391
  }
2979
3392
  }
2980
- let out = git(["diff", "--name-only", `${base5}...HEAD`], root);
3393
+ let out = git(["diff", "--name-only", `${base6}...HEAD`], root);
2981
3394
  if (out === null && !baseOverride) {
2982
3395
  out = git(["diff", "--name-only", "main...HEAD"], root);
2983
3396
  if (out !== null) {
2984
- base5 = "main";
3397
+ base6 = "main";
2985
3398
  }
2986
3399
  }
2987
3400
  const pending = opts?.includeWorkingTree ? workingTreeFiles(root) : [];
@@ -2992,7 +3405,7 @@ function changedFiles(baseOverride, opts) {
2992
3405
  if (files.length === 0) {
2993
3406
  return null;
2994
3407
  }
2995
- return { base: base5, files, repoRoot: root };
3408
+ return { base: base6, files, repoRoot: root };
2996
3409
  }
2997
3410
  function workingTreeFiles(root) {
2998
3411
  const out = git(["status", "--porcelain", "-z", "--untracked-files=all"], root);
@@ -3014,12 +3427,12 @@ function workingTreeFiles(root) {
3014
3427
  return files;
3015
3428
  }
3016
3429
  function inDiff(filePath, files) {
3017
- const base5 = filePath.split("/").pop();
3430
+ const base6 = filePath.split("/").pop();
3018
3431
  for (const d of files) {
3019
3432
  if (d === filePath || d.endsWith(`/${filePath}`) || filePath.endsWith(`/${d}`)) {
3020
3433
  return true;
3021
3434
  }
3022
- if (base5 && d.split("/").pop() === base5) {
3435
+ if (base6 && d.split("/").pop() === base6) {
3023
3436
  return true;
3024
3437
  }
3025
3438
  }
@@ -3060,7 +3473,7 @@ function keepUnderDiff(f, files, commandFor, locusFor) {
3060
3473
  return cmd ? gitTouchesDiff(cmd, files) : false;
3061
3474
  }
3062
3475
  if (isDestructive(f.id)) {
3063
- if (destructiveOutsideRepo(f.title) || regenerableReceiptRm(f.title)) {
3476
+ if (destructiveOutsideRepo(f.title) || regenerableReceiptRm(f.title) || regenerableBuildArtifact(f.title)) {
3064
3477
  return false;
3065
3478
  }
3066
3479
  const cmd = commandFor?.(f) ?? f.title.replace(/^Destructive op:\s*/i, "").replace(/\s*×\d+\s*$/, "");
@@ -3136,12 +3549,12 @@ function applyDiffScope(derived, findings, files, projectPath) {
3136
3549
  if (!projectPath || !f.evidenceSpanId) {
3137
3550
  return void 0;
3138
3551
  }
3139
- const base5 = spanCwd.get(f.evidenceSpanId);
3140
- const state = cwdAtFirstGit(spanCmd.get(f.evidenceSpanId) ?? "", base5);
3552
+ const base6 = spanCwd.get(f.evidenceSpanId);
3553
+ const state = cwdAtFirstGit(spanCmd.get(f.evidenceSpanId) ?? "", base6);
3141
3554
  if (state.kind === "unknown") {
3142
3555
  return "elsewhere";
3143
3556
  }
3144
- const at = state.kind === "known" ? state.path : base5;
3557
+ const at = state.kind === "known" ? state.path : base6;
3145
3558
  if (!at) {
3146
3559
  return void 0;
3147
3560
  }
@@ -3156,7 +3569,7 @@ function applyDiffScope(derived, findings, files, projectPath) {
3156
3569
  const destructiveCount = derived.spans.filter((s) => s.destructive).filter((s) => {
3157
3570
  const cmd = commandOf(s.input);
3158
3571
  const clause = destructiveMatch(cmd) ?? cmd;
3159
- if (destructiveOutsideRepo(clause) || regenerableReceiptRm(clause)) {
3572
+ if (destructiveOutsideRepo(clause) || regenerableReceiptRm(clause) || regenerableBuildArtifact(clause)) {
3160
3573
  return false;
3161
3574
  }
3162
3575
  if (/(^|[\s;&|])git\s/.test(cmd)) {
@@ -3253,8 +3666,9 @@ var intAfter = (line, re) => {
3253
3666
  const m = line.match(re);
3254
3667
  return m ? Number.parseInt(m[1], 10) : void 0;
3255
3668
  };
3669
+ var PYTEST_SUMMARY = /\b\d+\s+(?:passed|failed|skipped|errors?)\b[^\n]*?\bin\s+\d+(?:\.\d+)?s\b/;
3256
3670
  function parsePytest(o) {
3257
- const line = o.split("\n").reverse().find((l) => /={2,}[^\n]*\b\d+\s+(?:passed|failed|skipped|errors?)\b/.test(l));
3671
+ const line = o.split("\n").reverse().find((l) => PYTEST_SUMMARY.test(l));
3258
3672
  if (!line) {
3259
3673
  return void 0;
3260
3674
  }
@@ -3274,6 +3688,30 @@ function parsePytest(o) {
3274
3688
  durationMs: dur2 ? Math.round(Number.parseFloat(dur2[1]) * 1e3) : void 0
3275
3689
  };
3276
3690
  }
3691
+ function parseUnittest(o) {
3692
+ const ran = o.match(/^Ran\s+(\d+)\s+tests?\s+in\s+([\d.]+)s/m);
3693
+ if (!ran) {
3694
+ return void 0;
3695
+ }
3696
+ const passedVerdict = /^OK\b/m.test(o);
3697
+ const failedVerdict = o.match(/^FAILED\s*\(([^)]*)\)/m);
3698
+ if (!passedVerdict && !failedVerdict) {
3699
+ return void 0;
3700
+ }
3701
+ const detail = failedVerdict ? failedVerdict[1] : o.match(/^OK\s*\(([^)]*)\)/m)?.[1] ?? "";
3702
+ const failures = intAfter(detail, /failures=(\d+)/) ?? 0;
3703
+ const errors = intAfter(detail, /errors=(\d+)/) ?? 0;
3704
+ const skipped = intAfter(detail, /skipp?ed=(\d+)/);
3705
+ const total = Number.parseInt(ran[1], 10);
3706
+ const failed = failures + errors;
3707
+ return {
3708
+ runner: "unittest",
3709
+ passed: Math.max(0, total - failed - (skipped ?? 0)),
3710
+ failed,
3711
+ skipped,
3712
+ durationMs: Math.round(Number.parseFloat(ran[2]) * 1e3)
3713
+ };
3714
+ }
3277
3715
  function parseJestVitest(o) {
3278
3716
  const line = o.split("\n").find((l) => /^\s*Tests?\b/.test(l) && /\b\d+\s+(passed|failed)/.test(l));
3279
3717
  if (!line) {
@@ -3313,8 +3751,21 @@ function parseGo(o) {
3313
3751
  skipped: skipped || void 0
3314
3752
  };
3315
3753
  }
3754
+ var PARSERS = [
3755
+ { name: "pytest", fn: parsePytest },
3756
+ { name: "unittest", fn: parseUnittest },
3757
+ { name: "jest/vitest", fn: parseJestVitest },
3758
+ { name: "cargo", fn: parseCargo },
3759
+ { name: "go", fn: parseGo }
3760
+ ];
3316
3761
  function parseTestMetrics(output) {
3317
- const p = parsePytest(output) ?? parseJestVitest(output) ?? parseCargo(output) ?? parseGo(output);
3762
+ let p;
3763
+ for (const parser of PARSERS) {
3764
+ p = parser.fn(output);
3765
+ if (p) {
3766
+ break;
3767
+ }
3768
+ }
3318
3769
  if (!p) {
3319
3770
  return void 0;
3320
3771
  }
@@ -3378,7 +3829,7 @@ function renderLedger(rows) {
3378
3829
  // src/receipt/build.ts
3379
3830
  var PREDICATE_TYPE = "https://receipts.dev/agent-execution/v1";
3380
3831
  var STATEMENT_TYPE = "https://in-toto.io/Statement/v1";
3381
- var TEST_CMD = /\b(pytest|jest|vitest|npm (run )?test|yarn test|go test|cargo test|tsc|eslint|dbt (test|build)|mocha|rspec|phpunit|gradle test|mvn test)\b/i;
3832
+ var TEST_CMD = /\b(pytest|unittest|manage\.py test|jest|vitest|npm (run )?test|yarn test|go test|cargo test|tsc|eslint|dbt (test|build)|mocha|rspec|phpunit|gradle test|mvn test)\b/i;
3382
3833
  var round = (n, places) => {
3383
3834
  const f = 10 ** places;
3384
3835
  return Math.round((n || 0) * f) / f;
@@ -3529,6 +3980,12 @@ async function buildReceipt(session, derived, findings, opts = {}) {
3529
3980
  if (opts.scope) {
3530
3981
  predicate.scope = opts.scope;
3531
3982
  }
3983
+ if (opts.guardEvents?.length) {
3984
+ predicate.guardEvents = opts.guardEvents;
3985
+ }
3986
+ if (opts.subagents && (opts.subagents.surfaced.length > 0 || opts.subagents.total > 0)) {
3987
+ predicate.subagents = opts.subagents;
3988
+ }
3532
3989
  const claims = deriveClaims(
3533
3990
  finalAssistantText(derived),
3534
3991
  predicate.evidence.testsRan,
@@ -5184,9 +5641,6 @@ export {
5184
5641
  deriveFindings,
5185
5642
  gradeLetter,
5186
5643
  renderLedger,
5187
- privileged,
5188
- findingSurface,
5189
- destructiveOutsideRepo,
5190
5644
  changedFiles,
5191
5645
  inDiff,
5192
5646
  narrowEffort,
@@ -5221,4 +5675,4 @@ export {
5221
5675
  redact,
5222
5676
  redactReceipt
5223
5677
  };
5224
- //# sourceMappingURL=chunk-XLL4N2XR.js.map
5678
+ //# sourceMappingURL=chunk-FPDXOAYN.js.map