agent-roast 0.8.0 → 0.8.1

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
@@ -106,12 +106,12 @@ regressions) may appear in the report/summary rather than as an inline PR annota
106
106
  Use a separate SARIF category for introduced-findings review; it does not represent
107
107
  a full scan of all outstanding alerts.
108
108
 
109
- Install 0.8.0: `npx agent-roast@0.8.0 review --base origin/main`. For local development,
109
+ Install 0.8.1: `npx agent-roast@0.8.1 review --base origin/main`. For local development,
110
110
  run `npm run build` and use `node dist/cli.js`.
111
111
 
112
112
  ### Precise exceptions (since 0.8.0)
113
113
 
114
- Use `npx agent-roast@0.8.0 check --format json`, or build this checkout with
114
+ Use `npx agent-roast@0.8.1 check --format json`, or build this checkout with
115
115
  `npm run build` and run `node dist/cli.js`.
116
116
 
117
117
  Some skipped integrations or abstract stubs are intentional. In 0.8.0 you can accept
@@ -278,6 +278,11 @@ The JSON schema version for context reviews is now `1.1.0`.
278
278
  The generated hook uses the locally installed CLI (`--no-install`) and blocks on configured error rules instead of scoring
279
279
  old commits. It preserves an existing pre-commit hook.
280
280
 
281
+ `init` resolves the repository root even when run from a package/subdirectory, so
282
+ its policy is used by `check` and its workflow can be discovered by GitHub. Outside
283
+ a Git repository, initialization uses the supplied directory. Its output shows the
284
+ actual destination and existing policy/hooks are preserved.
285
+
281
286
  ### Configure what blocks a change
282
287
 
283
288
  Create `.agentroastrc.json` (or run `npx agent-roast init`):
@@ -342,7 +347,7 @@ jobs:
342
347
 
343
348
  To pilot the Action without blocking on findings, set `advisory: true`. It passes
344
349
  an explicit empty fail-on list; invalid configuration, Git errors and incomplete
345
- scans still fail. The `v1` Action installs 0.8.0. The CLI equivalent is `--fail-on ''`.
350
+ scans still fail. The `v1` Action installs 0.8.1. The CLI equivalent is `--fail-on ''`.
346
351
 
347
352
  > **Security Note:** `agent-roast` runs in an isolated runner directory (`$RUNNER_TEMP`) with `--ignore-scripts` to neutralize malicious `.npmrc` files in untrusted pull requests. `pull_request_target` is not supported.
348
353
 
@@ -393,6 +398,11 @@ Rust raw strings/lifetimes and Java/Kotlin text blocks. Contiguous comments, Pyt
393
398
  docstrings and multiline empty handlers are recognized. Template interpolation, complex language syntax and context outside
394
399
  the added diff in legacy audit modes are not fully parsed. Custom test API aliases,
395
400
  computed property calls and type/import resolution are not handled. Panic loops are an observational history heuristic.
401
+ Test registration options such as Node's `test('name', { skip: true }, fn)` and
402
+ `{ only: true }` are currently not inspected; use explicit `.skip`/`.only` APIs and
403
+ the runner's CI safeguards. Regexp masking is contextual, not a full JavaScript
404
+ parser. Formatting across contiguous lines is supported for casts, empty catches
405
+ and placeholder errors, but legacy added-line audits cannot recover missing hunks.
396
406
  The discipline score summarizes added history; it does not certify current code quality.
397
407
  `check` reports actionable findings without assigning a personality or quality score.
398
408
  History attribution is based on explicit text/identity heuristics; it does not prove
@@ -416,13 +426,16 @@ are exercised on Node 18/20/22 using `AGENT_ROAST_TEST_NODE`.
416
426
  npm ci
417
427
  npm run typecheck
418
428
  npm test
429
+
430
+ # Optional end-to-end CLI checks in disposable Git repositories
431
+ node scripts/verify-user-workflows.mjs dist/cli.js
419
432
  ```
420
433
 
421
434
  `npm test` builds the distribution first so CLI integration and bundle checks run
422
435
  against current code. Regression tests use disposable Git repositories. The programmatic
423
436
  `auditPullRequest({ staged: true })` API retains the legacy added-lines contract;
424
437
  use `reviewStagedChanges()` for the new context comparison and `ReviewResult`.
425
- The composite Action installs exactly 0.8.0. For local development, use
438
+ The composite Action installs exactly 0.8.1. For local development, use
426
439
  `node dist/cli.js --staged` after building.
427
440
 
428
441
  ### Author & Support
@@ -576,6 +576,8 @@ var SourceMasker = class {
576
576
  triple = false;
577
577
  verbatim = false;
578
578
  previousLine = 0;
579
+ tokenPrefix = "";
580
+ parentheses = [];
579
581
  python;
580
582
  javascript;
581
583
  rust;
@@ -602,12 +604,14 @@ var SourceMasker = class {
602
604
  this.rawEnd = "";
603
605
  this.triple = false;
604
606
  this.verbatim = false;
607
+ this.tokenPrefix = "";
608
+ this.parentheses.length = 0;
605
609
  }
606
610
  this.previousLine = number;
607
611
  const code = Array(line.length).fill(" ");
608
612
  const comments = Array(line.length).fill(" ");
609
613
  let i = 0;
610
- let prefix = "";
614
+ let prefix = this.tokenPrefix;
611
615
  const appendPrefix = (text) => {
612
616
  for (const char of text) {
613
617
  if (/\s/.test(char)) {
@@ -706,7 +710,7 @@ var SourceMasker = class {
706
710
  appendPrefix(" literal ");
707
711
  continue;
708
712
  }
709
- if (this.javascript && char === "/" && (/(?:[=(:,[!&|?{;]|\breturn|\byield)\s*$/.test(prefix) || !prefix.trim())) {
713
+ if (this.javascript && char === "/" && (/(?:=>|[=(:,[!&|?{;]|\breturn|\byield|\bthrow)\s*$/.test(prefix) || !prefix.trim())) {
710
714
  let j = i + 1;
711
715
  let inClass = false;
712
716
  for (; j < line.length; j++) {
@@ -727,10 +731,19 @@ var SourceMasker = class {
727
731
  }
728
732
  }
729
733
  code[i] = char;
734
+ if (this.javascript && char === "(") {
735
+ const control = /(?<![\w$])(?:if|while|for|with|switch|catch)\s*$/.exec(prefix);
736
+ this.parentheses.push(Boolean(control && !/\.\s*$/.test(prefix.slice(0, control.index))));
737
+ } else if (this.javascript && char === ")" && this.parentheses.pop()) {
738
+ prefix = "; ";
739
+ i++;
740
+ continue;
741
+ }
730
742
  appendPrefix(char);
731
743
  i++;
732
744
  }
733
745
  if (this.quote !== "`" && !this.triple && !this.verbatim && !continued) this.quote = "";
746
+ this.tokenPrefix = this.javascript ? (prefix + " ").slice(-64) : "";
734
747
  return { code: code.join(""), comments: comments.join(""), templateStarts, blockStarts };
735
748
  }
736
749
  };
@@ -1139,7 +1152,7 @@ var PYTHON_TRIVIAL_ASSERT_PATTERNS = [
1139
1152
  { regex: /\bself\s*\.\s*assertEqual\s*\(\s*(?:1\s*,\s*1|True\s*,\s*True)\s*\)/, name: "trivial assert (self.assertEqual(1, 1))" }
1140
1153
  ];
1141
1154
  var JS_STUB_PATTERNS = [
1142
- { regex: /\bthrow\s+new\s+Error\s*\(\s*["'](?:TODO\b|Not implemented\b|to be implemented\b|stub\b|implement me\b)/i, name: "unimplemented stub (throw new Error('Not implemented'))" }
1155
+ { regex: /\bthrow\s+new\s+Error\s*\(\s*["'`](?:TODO(?=\s*(?:["'`:]|implement\b))|Not implemented\b|to be implemented\b|stub\b|implement me\b)/i, name: "unimplemented stub (throw new Error('Not implemented'))" }
1143
1156
  ];
1144
1157
  var PYTHON_STUB_PATTERNS = [
1145
1158
  { regex: /\braise\s+NotImplementedError\b/, name: "unimplemented stub (raise NotImplementedError)" },
@@ -1251,22 +1264,6 @@ function auditFileDiffs(files, commitHash = "HEAD", commitDate = (/* @__PURE__ *
1251
1264
  infractions.push({ type: "type-escape", file: file.path, lineNumber: added.lineNumber, lineSnippet: rawLine.trim(), commitHash, commitDate, reason: "linter suppression (eslint-disable)" });
1252
1265
  foundEscape = true;
1253
1266
  }
1254
- if (typescript && !foundEscape && !isComment) {
1255
- for (const rule of CODE_TYPE_ESCAPE_PATTERNS) {
1256
- if (rule.regex.test(cleanedLine)) {
1257
- infractions.push({
1258
- type: "type-escape",
1259
- file: file.path,
1260
- lineNumber: added.lineNumber,
1261
- lineSnippet: rawLine.trim(),
1262
- commitHash,
1263
- commitDate,
1264
- reason: rule.name
1265
- });
1266
- break;
1267
- }
1268
- }
1269
- }
1270
1267
  if (python && !isComment && isTestFile(file.path)) {
1271
1268
  for (const rule of PYTHON_TRIVIAL_ASSERT_PATTERNS) {
1272
1269
  if (rule.regex.test(cleanedLine)) {
@@ -1283,40 +1280,55 @@ function auditFileDiffs(files, commitHash = "HEAD", commitDate = (/* @__PURE__ *
1283
1280
  }
1284
1281
  }
1285
1282
  }
1286
- if (!isComment) {
1287
- for (const rule of stubs) {
1288
- const matches = rawLine.matchAll(new RegExp(rule.regex.source, rule.regex.flags + "g"));
1289
- const match = [...matches].find((candidate) => cleanedLine[candidate.index] === rawLine[candidate.index]);
1290
- if (match) {
1291
- infractions.push({
1292
- type: "stub-abandonment",
1293
- file: file.path,
1294
- lineNumber: added.lineNumber,
1295
- lineSnippet: rawLine.trim(),
1296
- commitHash,
1297
- commitDate,
1298
- reason: rule.name
1299
- });
1300
- break;
1301
- }
1283
+ }
1284
+ let source = "";
1285
+ let code = "";
1286
+ let raw = "";
1287
+ const offsets = [];
1288
+ for (let i = 0; i < maskedLines.length; i++) {
1289
+ const separator = i ? maskedLines[i].number === maskedLines[i - 1].number + 1 ? "\n" : "\0" : "";
1290
+ source += separator;
1291
+ code += separator;
1292
+ raw += separator;
1293
+ offsets.push(source.length);
1294
+ source += maskedLines[i].testCode;
1295
+ code += maskedLines[i].code;
1296
+ raw += maskedLines[i].raw;
1297
+ }
1298
+ const evidence = (index, length) => {
1299
+ let low = 0;
1300
+ let high = offsets.length;
1301
+ while (low + 1 < high) {
1302
+ const middle = low + high >>> 1;
1303
+ if (offsets[middle] <= index) low = middle;
1304
+ else high = middle;
1305
+ }
1306
+ let end = low;
1307
+ while (end + 1 < offsets.length && offsets[end + 1] < index + length) end++;
1308
+ return { lineNumber: maskedLines[low].number, lineSnippet: maskedLines.slice(low, end + 1).map((line) => line.raw.trim()).join(" ") };
1309
+ };
1310
+ const reportedStubs = /* @__PURE__ */ new Set();
1311
+ for (const rule of stubs) {
1312
+ for (const match of raw.matchAll(new RegExp(rule.regex.source, rule.regex.flags + "g"))) {
1313
+ if (code[match.index] !== raw[match.index]) continue;
1314
+ const location = evidence(match.index, match[0].length);
1315
+ if (reportedStubs.has(location.lineNumber)) continue;
1316
+ reportedStubs.add(location.lineNumber);
1317
+ infractions.push({ type: "stub-abandonment", file: file.path, ...location, commitHash, commitDate, reason: rule.name });
1318
+ }
1319
+ }
1320
+ if (typescript) {
1321
+ const reported = new Set(infractions.slice(firstFinding).filter((f) => f.type === "type-escape").map((f) => f.lineNumber));
1322
+ for (const rule of CODE_TYPE_ESCAPE_PATTERNS) {
1323
+ for (const match of code.matchAll(new RegExp(rule.regex.source, "g"))) {
1324
+ const location = evidence(match.index, match[0].length);
1325
+ if (reported.has(location.lineNumber)) continue;
1326
+ reported.add(location.lineNumber);
1327
+ infractions.push({ type: "type-escape", file: file.path, ...location, commitHash, commitDate, reason: rule.name });
1302
1328
  }
1303
1329
  }
1304
1330
  }
1305
1331
  if (javascript) {
1306
- let source = "";
1307
- const offsets = [];
1308
- for (let i = 0; i < maskedLines.length; i++) {
1309
- if (i) source += maskedLines[i].number === maskedLines[i - 1].number + 1 ? "\n" : "\0";
1310
- offsets.push(source.length);
1311
- source += maskedLines[i].testCode;
1312
- }
1313
- let row = 0;
1314
- const evidence = (index, length) => {
1315
- while (row + 1 < offsets.length && offsets[row + 1] <= index) row++;
1316
- let end = row;
1317
- while (end + 1 < offsets.length && offsets[end + 1] < index + length) end++;
1318
- return { lineNumber: maskedLines[row].number, lineSnippet: maskedLines.slice(row, end + 1).map((line) => line.raw.trim()).join(" ") };
1319
- };
1320
1332
  for (const match of source.matchAll(JS_TEST_API)) {
1321
1333
  let previous = match.index - 1;
1322
1334
  while (previous >= 0 && /\s/.test(source[previous])) previous--;
@@ -1335,34 +1347,35 @@ function auditFileDiffs(files, commitHash = "HEAD", commitDate = (/* @__PURE__ *
1335
1347
  });
1336
1348
  }
1337
1349
  if (isTestFile(file.path)) {
1338
- row = 0;
1339
1350
  for (const rule of JS_TRIVIAL_ASSERT_PATTERNS) {
1340
1351
  for (const match of source.matchAll(new RegExp(rule.regex.source, "g"))) {
1341
1352
  infractions.push({ type: "trivial-assert", file: file.path, ...evidence(match.index, match[0].length), commitHash, commitDate, reason: rule.name });
1342
1353
  }
1343
- row = 0;
1344
1354
  }
1345
1355
  }
1346
1356
  }
1357
+ if (braceCatches) {
1358
+ for (const match of code.matchAll(/(?<![\w$.])catch\s*(?:\(\s*[^()\s\0][^()\0]*\))?\s*\{\s*\}/g)) {
1359
+ infractions.push({
1360
+ type: "swallowed-error",
1361
+ file: file.path,
1362
+ ...evidence(match.index, match[0].length),
1363
+ commitHash,
1364
+ commitDate,
1365
+ reason: match[0].includes("\n") ? "empty multi-line catch block" : "empty catch block (catch {})"
1366
+ });
1367
+ }
1368
+ }
1347
1369
  for (let index = 0; index < maskedLines.length; index++) {
1348
1370
  const start = maskedLines[index];
1349
- const catches = braceCatches ? [...start.code.matchAll(/(?<![\w$.])catch\s*(?:\(\s*[^)\s][^)]*\))?\s*\{/g)] : [];
1350
- for (const match of catches) {
1351
- if (/^\s*}/.test(start.code.slice(match.index + match[0].length))) {
1352
- infractions.push({ type: "swallowed-error", file: file.path, lineNumber: start.number, lineSnippet: start.raw.trim(), commitHash, commitDate, reason: "empty catch block (catch {})" });
1353
- }
1354
- }
1355
- const lastCatch = catches.at(-1);
1356
- const isCatch = lastCatch && /^\s*$/.test(start.code.slice(lastCatch.index + lastCatch[0].length));
1357
1371
  const inlineExcept = python && /^\s*except(?:\*?\s+[^:]+)?:\s*pass\s*;?\s*$/.test(start.code);
1358
1372
  if (inlineExcept) infractions.push({ type: "swallowed-error", file: file.path, lineNumber: start.number, lineSnippet: start.raw.trim(), commitHash, commitDate, reason: "silent python error swallow (except: pass)" });
1359
1373
  const isExcept = python && /^\s*except(?:\*?\s+[^:]+)?:\s*$/.test(start.code);
1360
- if (!isCatch && !isExcept) continue;
1374
+ if (!isExcept) continue;
1361
1375
  let next = index + 1;
1362
1376
  while (next < maskedLines.length && maskedLines[next].number === maskedLines[next - 1].number + 1 && !maskedLines[next].code.trim()) next++;
1363
1377
  const end = maskedLines[next];
1364
1378
  if (!end || end.number !== maskedLines[next - 1].number + 1) continue;
1365
- const closesCatch = isCatch && /^\s*}/.test(end.code);
1366
1379
  let closesExcept = isExcept && /^\s*pass\s*;?\s*$/.test(end.code);
1367
1380
  if (closesExcept) {
1368
1381
  const indent = start.raw.search(/\S/);
@@ -1371,14 +1384,14 @@ function auditFileDiffs(files, commitHash = "HEAD", commitDate = (/* @__PURE__ *
1371
1384
  while (after < maskedLines.length && maskedLines[after].number === maskedLines[after - 1].number + 1 && !maskedLines[after].code.trim()) after++;
1372
1385
  if (after < maskedLines.length && maskedLines[after].number === maskedLines[after - 1].number + 1 && maskedLines[after].raw.search(/\S/) > indent) closesExcept = false;
1373
1386
  }
1374
- if (closesCatch || closesExcept) infractions.push({
1387
+ if (closesExcept) infractions.push({
1375
1388
  type: "swallowed-error",
1376
1389
  file: file.path,
1377
1390
  lineNumber: start.number,
1378
1391
  lineSnippet: maskedLines.slice(index, next + 1).map((line) => line.raw.trim()).join(" "),
1379
1392
  commitHash,
1380
1393
  commitDate,
1381
- reason: closesCatch ? "empty multi-line catch block" : "silent multi-line python error swallow"
1394
+ reason: "silent multi-line python error swallow"
1382
1395
  });
1383
1396
  }
1384
1397
  const ordered = infractions.splice(firstFinding).sort((left, right) => (left.lineNumber || 0) - (right.lineNumber || 0));
@@ -2184,11 +2197,24 @@ var hasEntry = (filename) => {
2184
2197
  }
2185
2198
  };
2186
2199
  async function initProject(options = {}) {
2187
- const cwd = fs4.realpathSync(path3.resolve(options.cwd || process.cwd()));
2200
+ let cwd = fs4.realpathSync(path3.resolve(options.cwd || process.cwd()));
2201
+ const root = spawnSync2("git", [...HERMETIC_GIT_FLAGS, "rev-parse", "--show-toplevel"], {
2202
+ cwd,
2203
+ env: { ...getHermeticGitEnv(), LC_ALL: "C" },
2204
+ encoding: "utf8",
2205
+ timeout: 3e4,
2206
+ maxBuffer: 8192,
2207
+ windowsHide: true
2208
+ });
2209
+ if (root.error) throw new ConfigError("Cannot resolve the repository root for initialization.");
2210
+ if (root.status === 0) cwd = fs4.realpathSync(gitOutputPath(root.stdout));
2211
+ else if (root.status !== 128 || !root.stderr.includes("not a git repository")) {
2212
+ throw new ConfigError("Cannot resolve a working repository root for initialization.");
2213
+ }
2188
2214
  const created = [];
2189
2215
  const skipped = [];
2190
- const publish = (root, relative2, label, content, mode) => {
2191
- (createNewFile(root, relative2, content, mode) ? created : skipped).push(label);
2216
+ const publish = (root2, relative2, label, content, mode) => {
2217
+ (createNewFile(root2, relative2, content, mode) ? created : skipped).push(label);
2192
2218
  };
2193
2219
  if ([".agentroastrc.json", "agent-roast.config.json", ".agentroastrc"].some((name) => hasEntry(path3.join(cwd, name)))) {
2194
2220
  skipped.push(".agentroastrc.json");
@@ -2220,7 +2246,7 @@ async function initProject(options = {}) {
2220
2246
  } else publish(commonDir, relative2, ".git/hooks/pre-commit", PRE_COMMIT_HOOK_TEMPLATE, 493);
2221
2247
  }
2222
2248
  }
2223
- return { created, skipped };
2249
+ return { directory: cwd, created, skipped };
2224
2250
  }
2225
2251
 
2226
2252
  // src/badge.ts
package/dist/cli.js CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  roastRepository,
23
23
  terminalText,
24
24
  writeBaseline
25
- } from "./chunk-C423TSZF.js";
25
+ } from "./chunk-U7OCVA3D.js";
26
26
 
27
27
  // src/cli.ts
28
28
  import { Command } from "commander";
@@ -254,7 +254,7 @@ program.command("init").description("Initialize .agentroastrc.json and CI/hook c
254
254
  hook: opts.hook
255
255
  });
256
256
  if (res.created.length > 0) {
257
- console.log(`Initialized agent-roast in ${terminalText(targetDir)}:`);
257
+ console.log(`Initialized agent-roast in ${terminalText(res.directory)}:`);
258
258
  for (const file of res.created) {
259
259
  console.log(` + Created ${terminalText(file)}`);
260
260
  }
package/dist/index.d.ts CHANGED
@@ -366,6 +366,8 @@ interface InitOptions {
366
366
  hook?: boolean;
367
367
  }
368
368
  interface InitResult {
369
+ /** Actual project root containing the initialized files. */
370
+ directory: string;
369
371
  created: string[];
370
372
  skipped: string[];
371
373
  }
package/dist/index.js CHANGED
@@ -63,7 +63,7 @@ import {
63
63
  unquoteGitPath,
64
64
  validateExceptions,
65
65
  writeBaseline
66
- } from "./chunk-C423TSZF.js";
66
+ } from "./chunk-U7OCVA3D.js";
67
67
  export {
68
68
  CONFIG_FILENAMES,
69
69
  ConfigError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-roast",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Offline code checks for skipped tests, swallowed errors, unfinished stubs and AI coding shortcuts",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",