agent-roast 0.23.0 → 0.24.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 CHANGED
@@ -112,7 +112,7 @@ Measured, with the denominators and the caveats in [docs/benchmarks.md](docs/ben
112
112
  deliberate, and not specific to AI. agent-roast is a tripwire that asks about them. On repositories it had not seen, between a quarter and
113
113
  two thirds of its blocks were questions with a harmless answer, depending on the sample.**
114
114
 
115
- - **164 of 164** hand-labeled cheating edits are caught and **153 of 153** harmless look-alikes (reformats, moves, raised
115
+ - **165 of 165** hand-labeled cheating edits are caught and **154 of 154** harmless look-alikes (reformats, moves, raised
116
116
  thresholds, environment-gated skips) stay silent. The cases were written with the detector, so they guard against regressions;
117
117
  they do not prove accuracy. One gap is documented and still open.
118
118
  - **A catalog of cheats that someone else wrote.** Of the nine hacks in [reward-hack-zoo](https://github.com/MatthewWaller/reward-hack-zoo) (hand-written specimens; none from a model) four are reported (a skip, a patched target, a `conftest.py` that rewrites reports,
@@ -4538,6 +4538,32 @@ function gatingCalls(file, text) {
4538
4538
  if (!(/\.gitlab-ci/.test(file) || CI_GATE.test(gatingTriggers(text) ?? text))) return [];
4539
4539
  return [...text.matchAll(/^\s*-?\s*uses:\s*["']?\.\/(\.github\/workflows\/[^\s"'#@]+\.ya?ml)/gm)].map((match) => match[1]);
4540
4540
  }
4541
+ function lostCalls(file, oldText, newText) {
4542
+ if (!CI_FILE.test(file)) return [];
4543
+ const before = gatingCalls(file, oldText ?? "");
4544
+ if (!before.length) return [];
4545
+ const after = new Set(gatingCalls(file, newText ?? ""));
4546
+ return before.filter((workflow) => !after.has(workflow));
4547
+ }
4548
+ function calledTestCommands(text) {
4549
+ const directories = [...new Set([...text.matchAll(/^\s*(?:-\s+)?working-directory\s*:\s*["']?([^\s"'#]+)/gm)].map((match) => match[1]))].sort().join(",");
4550
+ return ciTestEntries(text).map((entry) => `${entry.command.replace(/\s#.*$/, "").replace(/^\s*-?\s*(?:run\s*:\s*)?/, "").replace(/\s+/g, " ").trim()} @${directories}`);
4551
+ }
4552
+ function resolveLostCallers(lost, commit) {
4553
+ const out = [];
4554
+ for (const [file, { content, stillCalled }] of lost) {
4555
+ if (stillCalled || gatingTestCommands(file, content).length > 0 || calledTestCommands(content).length === 0) continue;
4556
+ out.push(finding(
4557
+ "guard-weakened",
4558
+ file,
4559
+ 1,
4560
+ "nothing calls this workflow on a push or pull request any more",
4561
+ "Workflow keeps its tests, but the workflow that called it is gone",
4562
+ commit
4563
+ ));
4564
+ }
4565
+ return out;
4566
+ }
4541
4567
  function resolveCiTestSteps(counts, commit, elsewhere = /* @__PURE__ */ new Set(), called = /* @__PURE__ */ new Set()) {
4542
4568
  const before = counts.reduce((sum, item) => sum + item.before, 0);
4543
4569
  const after = counts.reduce((sum, item) => sum + item.after + item.reusableAfter, 0);
@@ -4800,6 +4826,8 @@ async function compareSnapshot(options, cwd, snapshot) {
4800
4826
  const guardFindings = [];
4801
4827
  const ciCounts = [];
4802
4828
  const calledInChange = /* @__PURE__ */ new Set();
4829
+ const lostCalledWorkflows = /* @__PURE__ */ new Set();
4830
+ const touchedCiContent = /* @__PURE__ */ new Map();
4803
4831
  let productionChanged = false;
4804
4832
  const decode = (content) => {
4805
4833
  totalBytes += content.length;
@@ -4837,8 +4865,12 @@ async function compareSnapshot(options, cwd, snapshot) {
4837
4865
  const newText = await readNew(change);
4838
4866
  guardFindings.push(...analyzeGuardFile(change.newPath, oldText, newText, refs.headSha));
4839
4867
  const ci = ciTestSteps(change.newPath, oldText, newText);
4840
- if (ci) ciCounts.push(ci);
4841
- if (ci && newText !== null) for (const workflow of gatingCalls(change.newPath, newText)) calledInChange.add(workflow);
4868
+ if (ci) {
4869
+ ciCounts.push(ci);
4870
+ touchedCiContent.set(change.newPath, newText);
4871
+ for (const workflow of lostCalls(change.newPath, oldText, newText)) lostCalledWorkflows.add(workflow);
4872
+ if (newText !== null) for (const workflow of gatingCalls(change.newPath, newText)) calledInChange.add(workflow);
4873
+ }
4842
4874
  result.filesInspected++;
4843
4875
  } catch (error) {
4844
4876
  assertNotAborted(options.signal);
@@ -4897,7 +4929,8 @@ async function compareSnapshot(options, cwd, snapshot) {
4897
4929
  result.complete = result.skippedFiles.length === 0;
4898
4930
  const rules = policy.config.rules ?? {};
4899
4931
  const elsewhere = /* @__PURE__ */ new Set(), called = new Set(calledInChange);
4900
- if (ciCounts.some((item) => item.before > 0 && item.after === 0 && item.reusableAfter === 0)) {
4932
+ const untouchedLostCallerContent = /* @__PURE__ */ new Map();
4933
+ if (ciCounts.some((item) => item.before > 0 && item.after === 0 && item.reusableAfter === 0) || lostCalledWorkflows.size > 0) {
4901
4934
  const touched = new Set(changes.flatMap((change) => [change.oldPath, change.newPath]));
4902
4935
  const tree = /^[a-f0-9]{40,64}$/.test(refs.headSha) ? refs.headSha : refs.baseSha;
4903
4936
  try {
@@ -4906,6 +4939,7 @@ async function compareSnapshot(options, cwd, snapshot) {
4906
4939
  const text = new TextDecoder().decode(await readBlob(cwd, file.oid, options.signal, 512 * 1024));
4907
4940
  for (const command of enforcedTestCommands(file.path, text)) elsewhere.add(command);
4908
4941
  for (const workflow of gatingCalls(file.path, text)) called.add(workflow);
4942
+ if (lostCalledWorkflows.has(file.path)) untouchedLostCallerContent.set(file.path, text);
4909
4943
  } catch (error) {
4910
4944
  assertNotAborted(options.signal);
4911
4945
  }
@@ -4914,7 +4948,16 @@ async function compareSnapshot(options, cwd, snapshot) {
4914
4948
  assertNotAborted(options.signal);
4915
4949
  }
4916
4950
  }
4917
- result.infractions.push(...[...resolveTampering(tamperingFiles, productionChanged), ...guardFindings, ...resolveCiTestSteps(ciCounts, refs.headSha, elsewhere, called)].filter((finding2) => rules[finding2.type] !== "off"));
4951
+ const ciFindings = resolveCiTestSteps(ciCounts, refs.headSha, elsewhere, called);
4952
+ const alreadyFlagged = new Set(ciFindings.map((found) => found.file));
4953
+ const lostCallers = /* @__PURE__ */ new Map();
4954
+ for (const workflow of lostCalledWorkflows) {
4955
+ if (alreadyFlagged.has(workflow)) continue;
4956
+ const content = touchedCiContent.has(workflow) ? touchedCiContent.get(workflow) ?? null : untouchedLostCallerContent.get(workflow) ?? null;
4957
+ if (content === null) continue;
4958
+ lostCallers.set(workflow, { content, stillCalled: called.has(workflow) });
4959
+ }
4960
+ result.infractions.push(...[...resolveTampering(tamperingFiles, productionChanged), ...guardFindings, ...ciFindings, ...resolveLostCallers(lostCallers, refs.headSha)].filter((finding2) => rules[finding2.type] !== "off"));
4918
4961
  const exceptions = applyExceptions(result.infractions, options.ignoreExceptions ? [] : policy.config.exceptions, /* @__PURE__ */ new Date(), existingCounts);
4919
4962
  result.infractions = exceptions.findings;
4920
4963
  result.suppressedFindings = exceptions.suppressedFindings;
package/dist/cli.js CHANGED
@@ -38,7 +38,7 @@ import {
38
38
  terminalText,
39
39
  uninstallHooks,
40
40
  writeBaseline
41
- } from "./chunk-L4XLCXC2.js";
41
+ } from "./chunk-U5MFYERX.js";
42
42
 
43
43
  // src/cli.ts
44
44
  import { Command } from "commander";
package/dist/index.js CHANGED
@@ -92,7 +92,7 @@ import {
92
92
  unquoteGitPath,
93
93
  validateExceptions,
94
94
  writeBaseline
95
- } from "./chunk-L4XLCXC2.js";
95
+ } from "./chunk-U5MFYERX.js";
96
96
  export {
97
97
  AGENT_LABELS,
98
98
  CONFIG_FILENAMES,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-roast",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Stops AI agents (and rushed humans) weakening tests, coverage gates or CI to get a green build. Hook for Claude Code, Codex, Cursor, Gemini CLI, Copilot, Droid; pre-commit; GitHub Action. Offline.",
5
5
  "mcpName": "io.github.Nari-Ab/agent-roast",
6
6
  "type": "module",