@methiu/raigal 0.2.0 → 0.2.3

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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { t as APP_VERSION } from "./version-v4OAaBLZ.js";
2
- import { a as getEngineLabel, i as ENGINE_INFO, r as scoreImpactForRule, t as summarizeFindingAssessments } from "./finding-assessment-M5fUTE-C.js";
1
+ import { t as APP_VERSION } from "./version-B4eYJn3o.js";
2
+ import { a as getEngineLabel, i as ENGINE_INFO, r as scoreImpactForRule, t as summarizeFindingAssessments } from "./finding-assessment-CGpt5YUR.js";
3
3
  import { n as isSecretClassRule, r as maskSecrets, t as FIXED_SECRET_DIAGNOSTIC_MESSAGE } from "./mask-secrets-BXn-ZSEh.js";
4
4
  import { createRequire, isBuiltin } from "node:module";
5
5
  import path from "node:path";
@@ -8,14 +8,14 @@ import YAML, { parse } from "yaml";
8
8
  import { z } from "zod/v4";
9
9
  import pc from "picocolors";
10
10
  import wcwidth from "wcwidth";
11
- import micromatch from "micromatch";
11
+ import picomatch from "picomatch";
12
12
  import { spawn, spawnSync } from "node:child_process";
13
13
  import { fileURLToPath, pathToFileURL } from "node:url";
14
14
  import os, { tmpdir } from "node:os";
15
15
  import { performance } from "node:perf_hooks";
16
16
  import ts from "typescript";
17
17
  import { parse as parse$1 } from "smol-toml";
18
- import { createHash, createPublicKey, randomUUID, verify } from "node:crypto";
18
+ import crypto, { createHash, createPublicKey, randomUUID, verify } from "node:crypto";
19
19
  import { isCancel, multiselect, select, text } from "@clack/prompts";
20
20
  import * as readline from "node:readline";
21
21
  import { Writable } from "node:stream";
@@ -88,7 +88,8 @@ const DEFAULT_CONFIG = {
88
88
  format: "json"
89
89
  },
90
90
  telemetry: { enabled: true },
91
- rules: {}
91
+ rules: {},
92
+ overrides: []
92
93
  };
93
94
  const GITHUB_WORKFLOW_DIR = ".github/workflows";
94
95
  const GITHUB_WORKFLOW_FILE = "raigal.yml";
@@ -131,7 +132,7 @@ const DEFAULT_RULES_YAML = `# Architecture rules (BYO)
131
132
  //#endregion
132
133
  //#region src/config/extends.ts
133
134
  const MAX_DEPTH$1 = 5;
134
- const MAX_CONFIG_BYTES = 1024 * 1024;
135
+ const MAX_CONFIG_BYTES = 1048576;
135
136
  const isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
136
137
  const deepMerge = (...sources) => {
137
138
  const result = {};
@@ -312,12 +313,25 @@ const CiSchema = z.object({
312
313
  format: z.enum(["json"]).default("json")
313
314
  }).strict();
314
315
  const TelemetrySchema = z.object({ enabled: z.boolean().default(true) }).strict();
316
+ const BaselineConfigSchema = z.object({ mode: z.enum(["strict", "relaxed"]).default("relaxed") }).strict();
317
+ const LicensingConfigSchema = z.object({ onLapse: z.enum(["fail-open", "fail-closed"]).default("fail-open") }).strict();
318
+ const WorkspaceConfigSchema = z.object({ minimumFloor: z.number().min(0).max(100).default(50) }).strict();
315
319
  const RuleSeverityOverride = z.enum([
316
320
  "error",
317
321
  "warning",
318
322
  "off"
319
323
  ]);
320
324
  const RulesSchema = z.record(z.string(), RuleSeverityOverride).default(() => ({}));
325
+ const FileOverrideSchema = z.object({
326
+ files: z.array(z.string().min(1)).min(1),
327
+ quality: z.object({
328
+ maxFunctionLoc: QualitySchema.shape.maxFunctionLoc.unwrap().optional(),
329
+ maxFileLoc: QualitySchema.shape.maxFileLoc.unwrap().optional(),
330
+ maxNesting: QualitySchema.shape.maxNesting.unwrap().optional(),
331
+ maxParams: QualitySchema.shape.maxParams.unwrap().optional()
332
+ }).optional(),
333
+ rules: z.record(z.string(), RuleSeverityOverride).optional()
334
+ });
321
335
  const AislopConfigSchema = z.object({
322
336
  version: z.number().default(1),
323
337
  extends: z.union([z.string(), z.array(z.string())]).optional(),
@@ -372,7 +386,11 @@ const AislopConfigSchema = z.object({
372
386
  format: "json"
373
387
  })),
374
388
  telemetry: TelemetrySchema.default(() => ({ enabled: true })),
389
+ baseline: BaselineConfigSchema.optional(),
390
+ licensing: LicensingConfigSchema.optional(),
391
+ workspace: WorkspaceConfigSchema.optional(),
375
392
  rules: RulesSchema,
393
+ overrides: z.array(FileOverrideSchema).default(() => []),
376
394
  exclude: z.array(z.string()).default(() => [
377
395
  "node_modules",
378
396
  ".git",
@@ -405,6 +423,9 @@ const KNOWN_KEYS_BY_PATH = {
405
423
  "scoring",
406
424
  "ci",
407
425
  "telemetry",
426
+ "baseline",
427
+ "licensing",
428
+ "workspace",
408
429
  "rules",
409
430
  "exclude",
410
431
  "include"
@@ -455,7 +476,10 @@ const KNOWN_KEYS_BY_PATH = {
455
476
  ],
456
477
  "scoring.thresholds": ["good", "ok"],
457
478
  ci: ["failBelow", "format"],
458
- telemetry: ["enabled"]
479
+ telemetry: ["enabled"],
480
+ baseline: ["mode"],
481
+ licensing: ["onLapse"],
482
+ workspace: ["minimumFloor"]
459
483
  };
460
484
  const levenshtein = (a, b) => {
461
485
  const m = a.length;
@@ -587,7 +611,8 @@ const loadConfig = (directory) => {
587
611
  const configPath = findConfigFile(directory);
588
612
  if (!configPath) return DEFAULT_CONFIG;
589
613
  try {
590
- return validateConfig(loadConfigChain(configPath), configPath);
614
+ const merged = loadConfigChain(configPath);
615
+ return validateConfig(merged, configPath);
591
616
  } catch (error) {
592
617
  if (error instanceof ConfigError) throw error;
593
618
  throw new ConfigError(configPath, `Failed to parse ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
@@ -674,7 +699,7 @@ const highlightRaigal = (text, theme, baseToken) => {
674
699
  const tail = text.slice(lastIndex);
675
700
  return out + (baseToken ? style(theme, baseToken, tail) : tail);
676
701
  };
677
- const highlightAislop = highlightRaigal;
702
+ const highlightAislop = (...args) => highlightRaigal(...args);
678
703
  //#endregion
679
704
  //#region src/ui/width.ts
680
705
  const ANSI_RE$1 = new RegExp(`\\[[0-9;]*m`, "g");
@@ -683,7 +708,8 @@ const stringWidth = (s) => {
683
708
  const bare = stripAnsi$1(s);
684
709
  let total = 0;
685
710
  for (const ch of bare) {
686
- const w = wcwidth(ch.codePointAt(0) ?? 0);
711
+ const cp = ch.codePointAt(0) ?? 0;
712
+ const w = wcwidth(cp);
687
713
  total += w > 0 ? w : 1;
688
714
  }
689
715
  return total;
@@ -699,7 +725,8 @@ const truncate = (s, max, ellipsis = "…") => {
699
725
  let out = "";
700
726
  let w = 0;
701
727
  for (const ch of s) {
702
- const cw = wcwidth(ch.codePointAt(0) ?? 0);
728
+ const cp = ch.codePointAt(0) ?? 0;
729
+ const cw = wcwidth(cp);
703
730
  if (w + cw > limit) break;
704
731
  out += ch;
705
732
  w += cw;
@@ -709,7 +736,8 @@ const truncate = (s, max, ellipsis = "…") => {
709
736
  //#endregion
710
737
  //#region src/ui/display.ts
711
738
  const renderDisplaySection = (label, deps = {}) => {
712
- return style(deps.theme ?? theme, "section", label);
739
+ const t = deps.theme ?? theme;
740
+ return style(t, "section", label);
713
741
  };
714
742
  const renderDisplayRows = (rows, options = {}) => {
715
743
  if (rows.length === 0) return [];
@@ -856,8 +884,10 @@ const detectSourceLanguages = (sourceFiles) => {
856
884
  const detectLanguages = (directory, sourceFiles) => {
857
885
  const languages = new Set(detectSourceLanguages(sourceFiles));
858
886
  for (const [file, lang] of Object.entries(LANGUAGE_SIGNALS)) if (fs.existsSync(path.join(directory, file))) languages.add(lang);
859
- if (readPackageJson(path.join(directory, "package.json"))) if (fs.existsSync(path.join(directory, "tsconfig.json"))) languages.add("typescript");
860
- else languages.add("javascript");
887
+ if (readPackageJson(path.join(directory, "package.json"))) {
888
+ if (fs.existsSync(path.join(directory, "tsconfig.json"))) languages.add("typescript");
889
+ else languages.add("javascript");
890
+ }
861
891
  for (const signal of PYTHON_SIGNALS) if (fs.existsSync(path.join(directory, signal))) {
862
892
  languages.add("python");
863
893
  break;
@@ -938,13 +968,13 @@ const normalizeExcludePatterns = (patterns) => patterns.flatMap((pattern) => {
938
968
  const normalized = withoutProjectPrefix.slice(0, end);
939
969
  if (normalized.length === 0) return [];
940
970
  if (normalized.length > 256) return [];
941
- if (micromatch.scan(normalized).isGlob) return [normalized];
971
+ if (picomatch.scan(normalized).isGlob) return [normalized];
942
972
  if (normalized.startsWith(".") && !normalized.includes("/")) return supportedGlobPatterns([`**/${normalized}`, `**/${normalized}/**`]);
943
973
  return supportedGlobPatterns([normalized, `${normalized}/**`]);
944
974
  });
945
975
  const isPathExcluded = (relativePath, normalizedPatterns) => {
946
976
  if (normalizedPatterns.length === 0) return false;
947
- return micromatch.isMatch(relativePath, normalizedPatterns, { dot: true });
977
+ return picomatch.isMatch(relativePath, normalizedPatterns, { dot: true });
948
978
  };
949
979
  const filterExcludedDiagnostics = (diagnostics, rootDirectory, excludePatterns) => {
950
980
  if (!excludePatterns || excludePatterns.length === 0) return diagnostics;
@@ -990,7 +1020,7 @@ const isAutoGenerated = (filePath) => {
990
1020
  };
991
1021
  //#endregion
992
1022
  //#region src/utils/git-ignore.ts
993
- const MAX_BUFFER$2 = 50 * 1024 * 1024;
1023
+ const MAX_BUFFER$2 = 52428800;
994
1024
  const NOT_IGNORED_ARGUMENTS = [
995
1025
  "ls-files",
996
1026
  "--cached",
@@ -1101,7 +1131,7 @@ const dropGitIgnoredPaths = (rootDirectory, absolutePaths) => {
1101
1131
  };
1102
1132
  //#endregion
1103
1133
  //#region src/utils/project-file-list.ts
1104
- const MAX_BUFFER$1 = 50 * 1024 * 1024;
1134
+ const MAX_BUFFER$1 = 52428800;
1105
1135
  const normalizePruneDirectories = (directories) => new Set([...directories].map((directory) => directory.toLowerCase()));
1106
1136
  const isInPrunedDirectory = (filePath, pruneDirectories) => {
1107
1137
  const pathSegments = filePath.split("/");
@@ -1312,12 +1342,13 @@ const normalizeIncludePatterns = (patterns) => patterns.flatMap((pattern) => {
1312
1342
  const normalized = pattern.trim().replace(/^\.\//, "").replace(/\/$/, "");
1313
1343
  if (normalized === "" || normalized === ".") return ["**"];
1314
1344
  if (normalized.length > 256) return [];
1315
- if (micromatch.scan(normalized).isGlob) return [normalized];
1345
+ if (picomatch.scan(normalized).isGlob) return [normalized];
1316
1346
  return supportedGlobPatterns([normalized, `${normalized}/**`]);
1317
1347
  });
1318
1348
  const createPathMatcher = (patterns) => {
1319
- const matchers = patterns.map((pattern) => micromatch.matcher(pattern, { dot: true }));
1320
- return (filePath) => matchers.some((matches) => matches(filePath));
1349
+ if (patterns.length === 0) return () => false;
1350
+ const isMatch = picomatch(patterns, { dot: true });
1351
+ return (filePath) => isMatch(filePath);
1321
1352
  };
1322
1353
  const filterFiles = (rootDirectory, files, options) => {
1323
1354
  const extraExtensions = new Set(options.extraExtensions);
@@ -1651,7 +1682,8 @@ const resolvePackageRoot = (startFile) => {
1651
1682
  while (true) {
1652
1683
  const packageJsonPath = path.join(current, "package.json");
1653
1684
  if (fs.existsSync(packageJsonPath)) try {
1654
- if (JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")).name === "aislop") return current;
1685
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
1686
+ if (packageJson.name === "aislop" || packageJson.name === "@methiu/raigal" || packageJson.name === "raigal") return current;
1655
1687
  } catch {}
1656
1688
  const parent = path.dirname(current);
1657
1689
  if (parent === current) break;
@@ -1746,7 +1778,7 @@ const checkInstalledTools = async () => {
1746
1778
  };
1747
1779
  //#endregion
1748
1780
  //#region src/utils/project-path-safety.ts
1749
- const MAX_PROJECT_CONFIG_BYTES = 1024 * 1024;
1781
+ const MAX_PROJECT_CONFIG_BYTES = 1048576;
1750
1782
  const isWithinDirectory$2 = (rootDirectory, candidate) => {
1751
1783
  const relative = path.relative(rootDirectory, candidate);
1752
1784
  return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
@@ -1800,7 +1832,7 @@ const isRootBoundedTarget = (candidate, rootDirectory) => {
1800
1832
  };
1801
1833
  //#endregion
1802
1834
  //#region src/utils/aislop-ignore.ts
1803
- const readRaigalIgnorePatterns = (rootDirectory) => {
1835
+ const readAislopIgnorePatterns = (rootDirectory) => {
1804
1836
  const patterns = [];
1805
1837
  for (const fileName of [".raigalignore", ".aislopignore"]) {
1806
1838
  const targetPath = safeProjectFilePath(path.join(rootDirectory, fileName), rootDirectory);
@@ -1811,7 +1843,6 @@ const readRaigalIgnorePatterns = (rootDirectory) => {
1811
1843
  }
1812
1844
  return patterns;
1813
1845
  };
1814
- const readAislopIgnorePatterns = readRaigalIgnorePatterns;
1815
1846
  //#endregion
1816
1847
  //#region src/utils/source-file-context.ts
1817
1848
  const getSourceFilesForRoot = (rootDirectory) => filterProjectFiles(rootDirectory, listProjectFiles(rootDirectory));
@@ -2072,7 +2103,8 @@ const resolveTrustedTscPath = () => {
2072
2103
  };
2073
2104
  const isReferenceOnlyConfig = (tsconfigPath) => {
2074
2105
  try {
2075
- const parsed = parseJsonc(fs.readFileSync(tsconfigPath, "utf-8"));
2106
+ const raw = fs.readFileSync(tsconfigPath, "utf-8");
2107
+ const parsed = parseJsonc(raw);
2076
2108
  if (!parsed) return false;
2077
2109
  return Array.isArray(parsed.references) && !parsed.files && !parsed.include && !parsed.extends;
2078
2110
  } catch {
@@ -2391,7 +2423,7 @@ const changeContextOrder = (context) => {
2391
2423
  };
2392
2424
  //#endregion
2393
2425
  //#region src/utils/git.ts
2394
- const MAX_BUFFER = 50 * 1024 * 1024;
2426
+ const MAX_BUFFER = 52428800;
2395
2427
  const baseRefExists = (cwd, ref) => {
2396
2428
  const result = spawnSync("git", [
2397
2429
  "rev-parse",
@@ -2639,6 +2671,132 @@ const doctorCommand = async (directory, options = {}) => {
2639
2671
  }));
2640
2672
  };
2641
2673
  //#endregion
2674
+ //#region src/utils/globs.ts
2675
+ const MAX_BRACE_EXPANSIONS = 256;
2676
+ const braceListAt = (pattern, start) => {
2677
+ let depth = 0;
2678
+ const commas = [];
2679
+ for (let i = start; i < pattern.length; i += 1) {
2680
+ const char = pattern[i];
2681
+ if (char === "\\") i += 1;
2682
+ else if (char === "{") depth += 1;
2683
+ else if (char === ",") {
2684
+ if (depth === 1) commas.push(i);
2685
+ } else if (char === "}") {
2686
+ depth -= 1;
2687
+ if (depth === 0) return commas.length > 0 ? {
2688
+ start,
2689
+ end: i,
2690
+ commas
2691
+ } : null;
2692
+ }
2693
+ }
2694
+ return null;
2695
+ };
2696
+ const findBraceList = (pattern) => {
2697
+ for (let start = pattern.indexOf("{"); start !== -1; start = pattern.indexOf("{", start + 1)) {
2698
+ if (start > 0 && pattern[start - 1] === "\\") continue;
2699
+ const list = braceListAt(pattern, start);
2700
+ if (list) return list;
2701
+ }
2702
+ return null;
2703
+ };
2704
+ const expandInto = (pattern, results) => {
2705
+ const list = findBraceList(pattern);
2706
+ if (!list) {
2707
+ results.push(pattern);
2708
+ return results.length <= MAX_BRACE_EXPANSIONS;
2709
+ }
2710
+ const bounds = [
2711
+ list.start,
2712
+ ...list.commas,
2713
+ list.end
2714
+ ];
2715
+ const prefix = pattern.slice(0, list.start);
2716
+ const suffix = pattern.slice(list.end + 1);
2717
+ for (let i = 0; i < bounds.length - 1; i += 1) {
2718
+ const option = pattern.slice(bounds[i] + 1, bounds[i + 1]);
2719
+ if (!expandInto(`${prefix}${option}${suffix}`, results)) return false;
2720
+ }
2721
+ return true;
2722
+ };
2723
+ const expandBraceLists = (pattern) => {
2724
+ const results = [];
2725
+ return expandInto(pattern, results) ? results : [pattern];
2726
+ };
2727
+ const isNegated = (pattern) => picomatch.scan(pattern).negated;
2728
+ const matchesGlobList = (value, patterns, options = {}) => {
2729
+ const expanded = patterns.flatMap(expandBraceLists);
2730
+ const positives = expanded.filter((pattern) => !isNegated(pattern));
2731
+ const negatives = expanded.filter(isNegated).map((pattern) => pattern.slice(1));
2732
+ const included = positives.length === 0 || picomatch.isMatch(value, positives, options);
2733
+ const excluded = negatives.length > 0 && picomatch.isMatch(value, negatives, options);
2734
+ return included && !excluded;
2735
+ };
2736
+ //#endregion
2737
+ //#region src/scoring/rule-severity.ts
2738
+ /**
2739
+ * Apply per-rule severity overrides from config: "off" drops the diagnostic,
2740
+ * "error"/"warning" rewrite its severity before scoring and rendering.
2741
+ */
2742
+ const applyRuleSeverities = (diagnostics, overrides) => {
2743
+ if (Object.keys(overrides).length === 0) return diagnostics;
2744
+ const result = [];
2745
+ for (const diagnostic of diagnostics) {
2746
+ const override = overrides[diagnostic.rule];
2747
+ if (!override) {
2748
+ result.push(diagnostic);
2749
+ continue;
2750
+ }
2751
+ if (override === "off") continue;
2752
+ result.push(override === diagnostic.severity ? diagnostic : {
2753
+ ...diagnostic,
2754
+ severity: override
2755
+ });
2756
+ }
2757
+ return result;
2758
+ };
2759
+ //#endregion
2760
+ //#region src/config/overrides.ts
2761
+ const overrideRelativePath = (rootDirectory, filePath) => {
2762
+ const paths = path.win32.isAbsolute(rootDirectory) && !path.posix.isAbsolute(rootDirectory) ? path.win32 : path.posix;
2763
+ const root = rootDirectory.replaceAll("\\", "/");
2764
+ const file = filePath.replaceAll("\\", "/");
2765
+ return paths.normalize(paths.isAbsolute(file) ? paths.relative(root, file) : file).replaceAll("\\", "/");
2766
+ };
2767
+ const matchesEntry = (relativePath, patterns) => matchesGlobList(relativePath, patterns, { dot: true });
2768
+ const resolveFilePolicy = (config, relativePath) => {
2769
+ let quality = config.quality;
2770
+ let rules = config.rules ?? {};
2771
+ if (relativePath === ".." || relativePath.startsWith("../") || path.win32.isAbsolute(relativePath)) return {
2772
+ quality,
2773
+ rules
2774
+ };
2775
+ for (const entry of config.overrides ?? []) {
2776
+ if (!matchesEntry(relativePath, entry.files)) continue;
2777
+ if (entry.quality) quality = {
2778
+ ...quality,
2779
+ ...entry.quality
2780
+ };
2781
+ if (entry.rules) rules = {
2782
+ ...rules,
2783
+ ...entry.rules
2784
+ };
2785
+ }
2786
+ return {
2787
+ quality,
2788
+ rules
2789
+ };
2790
+ };
2791
+ const applyFileOverrides = (diagnostics, context) => {
2792
+ if (!context.config.overrides?.length) return diagnostics;
2793
+ return diagnostics.flatMap((diagnostic) => {
2794
+ const relativePath = overrideRelativePath(context.rootDirectory, diagnostic.filePath);
2795
+ const { rules } = resolveFilePolicy(context.config, relativePath);
2796
+ return applyRuleSeverities([diagnostic], rules);
2797
+ });
2798
+ };
2799
+ //#endregion
2642
2800
  //#region src/utils/string-literals.ts
2643
2801
  const consumeQuotedString = (content, start, quote) => {
2644
2802
  const len = content.length;
@@ -3247,7 +3405,7 @@ const tokenizeJs = (content, ext = ".ts") => {
3247
3405
  };
3248
3406
  //#endregion
3249
3407
  //#region src/utils/source-masker.ts
3250
- const JS_EXTS$2 = /* @__PURE__ */ new Set([
3408
+ const JS_EXTS$3 = /* @__PURE__ */ new Set([
3251
3409
  ".ts",
3252
3410
  ".tsx",
3253
3411
  ".js",
@@ -3255,7 +3413,7 @@ const JS_EXTS$2 = /* @__PURE__ */ new Set([
3255
3413
  ".mjs",
3256
3414
  ".cjs"
3257
3415
  ]);
3258
- const PY_EXTS = /* @__PURE__ */ new Set([".py"]);
3416
+ const PY_EXTS$1 = /* @__PURE__ */ new Set([".py"]);
3259
3417
  const RB_EXTS = /* @__PURE__ */ new Set([".rb"]);
3260
3418
  const PHP_EXTS = /* @__PURE__ */ new Set([".php"]);
3261
3419
  const CSHARP_EXTS = /* @__PURE__ */ new Set([".cs"]);
@@ -3279,8 +3437,8 @@ const C_STYLE_COMMENT_EXTS = /* @__PURE__ */ new Set([
3279
3437
  ".zig"
3280
3438
  ]);
3281
3439
  const familyForExt = (ext) => {
3282
- if (JS_EXTS$2.has(ext)) return "js";
3283
- if (PY_EXTS.has(ext)) return "py";
3440
+ if (JS_EXTS$3.has(ext)) return "js";
3441
+ if (PY_EXTS$1.has(ext)) return "py";
3284
3442
  if (RB_EXTS.has(ext)) return "rb";
3285
3443
  if (PHP_EXTS.has(ext)) return "php";
3286
3444
  if (CSHARP_EXTS.has(ext)) return "csharp";
@@ -3550,7 +3708,7 @@ const areLiteralMultisetsEqual = (a, b) => {
3550
3708
  };
3551
3709
  //#endregion
3552
3710
  //#region src/engines/ai-slop/abstractions.ts
3553
- const JS_EXTS$1 = /* @__PURE__ */ new Set([
3711
+ const JS_EXTS$2 = /* @__PURE__ */ new Set([
3554
3712
  ".ts",
3555
3713
  ".tsx",
3556
3714
  ".js",
@@ -3561,11 +3719,11 @@ const JS_EXTS$1 = /* @__PURE__ */ new Set([
3561
3719
  const THIN_WRAPPER_PATTERNS = [
3562
3720
  {
3563
3721
  pattern: /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\{\s*\n?\s*return\s+\w+\([^)]*\);\s*\n?\s*\}/g,
3564
- extensions: JS_EXTS$1
3722
+ extensions: JS_EXTS$2
3565
3723
  },
3566
3724
  {
3567
3725
  pattern: /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
3568
- extensions: JS_EXTS$1
3726
+ extensions: JS_EXTS$2
3569
3727
  },
3570
3728
  {
3571
3729
  pattern: /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm,
@@ -4813,7 +4971,7 @@ const scriptKindFor$1 = (ext) => {
4813
4971
  }
4814
4972
  };
4815
4973
  const lineFor$1 = (sourceFile, node) => sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
4816
- const makeDiagnostic = (filePath, line, rule, message, help) => ({
4974
+ const makeDiagnostic$1 = (filePath, line, rule, message, help) => ({
4817
4975
  filePath,
4818
4976
  engine: "ai-slop",
4819
4977
  rule,
@@ -4863,7 +5021,7 @@ const detectRedundantTryCatch = (sourceFile, relativePath) => {
4863
5021
  if (ts.isTryStatement(node) && node.catchClause && !node.finallyBlock) {
4864
5022
  const catchNameNode = node.catchClause.variableDeclaration?.name;
4865
5023
  const [onlyStatement] = node.catchClause.block.statements;
4866
- if (catchNameNode && ts.isIdentifier(catchNameNode) && node.catchClause.block.statements.length === 1 && onlyStatement && (isRethrowStatement(onlyStatement, catchNameNode.text) || isPromiseRejectRethrow(onlyStatement, catchNameNode.text))) diagnostics.push(makeDiagnostic(relativePath, lineFor$1(sourceFile, node.catchClause), "ai-slop/redundant-try-catch", "Catch block only rethrows the same error", "Remove the try/catch or add useful context, cleanup, or recovery. Rethrowing unchanged errors is usually defensive agent noise."));
5024
+ if (catchNameNode && ts.isIdentifier(catchNameNode) && node.catchClause.block.statements.length === 1 && onlyStatement && (isRethrowStatement(onlyStatement, catchNameNode.text) || isPromiseRejectRethrow(onlyStatement, catchNameNode.text))) diagnostics.push(makeDiagnostic$1(relativePath, lineFor$1(sourceFile, node.catchClause), "ai-slop/redundant-try-catch", "Catch block only rethrows the same error", "Remove the try/catch or add useful context, cleanup, or recovery. Rethrowing unchanged errors is usually defensive agent noise."));
4867
5025
  }
4868
5026
  ts.forEachChild(node, visit);
4869
5027
  };
@@ -4881,7 +5039,7 @@ const detectPrimitiveCoercions = (sourceFile, relativePath) => {
4881
5039
  const [arg] = child.arguments;
4882
5040
  if (arg && ts.isIdentifier(arg)) {
4883
5041
  const primitive = params.get(arg.text);
4884
- if (primitive && child.expression.text === COERCION_CTORS[primitive]) diagnostics.push(makeDiagnostic(relativePath, lineFor$1(sourceFile, child), "ai-slop/redundant-type-coercion", `Parameter '${arg.text}' is already typed as ${primitive} but is coerced again`, "Trust the typed boundary or validate unknown input before this function. Re-coercing already typed parameters is usually defensive agent noise."));
5042
+ if (primitive && child.expression.text === COERCION_CTORS[primitive]) diagnostics.push(makeDiagnostic$1(relativePath, lineFor$1(sourceFile, child), "ai-slop/redundant-type-coercion", `Parameter '${arg.text}' is already typed as ${primitive} but is coerced again`, "Trust the typed boundary or validate unknown input before this function. Re-coercing already typed parameters is usually defensive agent noise."));
4885
5043
  }
4886
5044
  }
4887
5045
  ts.forEachChild(child, visitBody);
@@ -4924,7 +5082,7 @@ const detectDuplicateExportedTypes = (parsedSources) => {
4924
5082
  continue;
4925
5083
  }
4926
5084
  if (previous.filePath === declaration.filePath) continue;
4927
- diagnostics.push(makeDiagnostic(declaration.filePath, declaration.line, "ai-slop/duplicate-type-declaration", `Exported type '${declaration.name}' duplicates an existing declaration`, `Reuse or import the existing type from ${previous.filePath} instead of re-declaring the same shape in another file.`));
5085
+ diagnostics.push(makeDiagnostic$1(declaration.filePath, declaration.line, "ai-slop/duplicate-type-declaration", `Exported type '${declaration.name}' duplicates an existing declaration`, `Reuse or import the existing type from ${previous.filePath} instead of re-declaring the same shape in another file.`));
4928
5086
  }
4929
5087
  }
4930
5088
  return diagnostics;
@@ -5026,7 +5184,7 @@ const detectDuplicateImports = async (context) => {
5026
5184
  rule: "ai-slop/duplicate-import",
5027
5185
  severity: "warning",
5028
5186
  message: `"${spec}" is also imported on line ${firstLine}. Merge into a single import statement.`,
5029
- help: "Two imports from the same module split readers' attention and grow the import block. Run aislop fix to merge them automatically.",
5187
+ help: "Two imports from the same module split readers' attention and grow the import block. Run raigal fix to merge them automatically.",
5030
5188
  line: dup.line,
5031
5189
  column: 1,
5032
5190
  category: "AI Slop",
@@ -6648,7 +6806,8 @@ const isJsRelativeOrAbsolute = (spec) => spec.startsWith(".") || spec.startsWith
6648
6806
  const RUNTIME_BUILTINS = /* @__PURE__ */ new Set(["bun"]);
6649
6807
  const isJsBuiltin = (spec) => {
6650
6808
  if (RUNTIME_BUILTINS.has(spec)) return true;
6651
- return isBuiltin(spec.startsWith("node:") ? spec.slice(5) : spec) || isBuiltin(spec);
6809
+ const stripped = spec.startsWith("node:") ? spec.slice(5) : spec;
6810
+ return isBuiltin(stripped) || isBuiltin(spec);
6652
6811
  };
6653
6812
  const VIRTUAL_MODULE_PREFIXES = [
6654
6813
  "astro:",
@@ -6860,7 +7019,8 @@ const isLocalAliasReplacement = (rawValue, configDirectory, rootDirectory) => {
6860
7019
  if (!target) return false;
6861
7020
  if (!target.startsWith(".") && !target.startsWith("/") && !target.startsWith("~/") && !target.startsWith("@/")) return false;
6862
7021
  const normalized = target.startsWith("~/") || target.startsWith("@/") ? target.slice(2) : target;
6863
- return isRootBoundedTarget(path.isAbsolute(normalized) ? normalized : path.resolve(configDirectory, normalized), rootDirectory);
7022
+ const candidate = path.isAbsolute(normalized) ? normalized : path.resolve(configDirectory, normalized);
7023
+ return isRootBoundedTarget(candidate, rootDirectory);
6864
7024
  };
6865
7025
  const hasValidPackageImportTarget = (value, packageDirectory, rootDirectory) => {
6866
7026
  if (!isRootBoundedTarget(packageDirectory, rootDirectory)) return false;
@@ -8054,7 +8214,9 @@ const isRustDocCommentLine = (line) => {
8054
8214
  };
8055
8215
  const collectBlocks = (sourceLines, syntax, content, ext) => {
8056
8216
  const blocks = [];
8057
- const commentLines = getCommentLines(content ?? sourceLines.join("\n"), ext ?? (syntax.linePrefixes.includes("//") ? ".ts" : ".py"));
8217
+ const actualContent = content ?? sourceLines.join("\n");
8218
+ const actualExt = ext ?? (syntax.linePrefixes.includes("//") ? ".ts" : ".py");
8219
+ const commentLines = getCommentLines(actualContent, actualExt);
8058
8220
  let i = 0;
8059
8221
  while (i < sourceLines.length) {
8060
8222
  const line = sourceLines[i];
@@ -8382,7 +8544,8 @@ const detectNarrativeComments = async (context) => {
8382
8544
  } catch {
8383
8545
  continue;
8384
8546
  }
8385
- const blocks = collectBlocks(content.split("\n"), syntax, content, ext);
8547
+ const lines = content.split("\n");
8548
+ const blocks = collectBlocks(lines, syntax, content, ext);
8386
8549
  for (const block of blocks) {
8387
8550
  const { matched, reason } = detectNarrativeInBlock(block, ext);
8388
8551
  if (!matched) continue;
@@ -8854,7 +9017,7 @@ const detectRustPatterns = async (context) => {
8854
9017
  };
8855
9018
  //#endregion
8856
9019
  //#region src/engines/ai-slop/silent-recovery.ts
8857
- const JS_EXTS = /* @__PURE__ */ new Set([
9020
+ const JS_EXTS$1 = /* @__PURE__ */ new Set([
8858
9021
  ".ts",
8859
9022
  ".tsx",
8860
9023
  ".js",
@@ -9049,7 +9212,7 @@ const detectSilentRecovery = async (context) => {
9049
9212
  for (const filePath of files) {
9050
9213
  if (isAutoGenerated(filePath)) continue;
9051
9214
  const ext = path.extname(filePath);
9052
- const isJs = JS_EXTS.has(ext);
9215
+ const isJs = JS_EXTS$1.has(ext);
9053
9216
  if (!isJs && !(ext === ".py")) continue;
9054
9217
  const relPath = relativePosix(context.rootDirectory, filePath);
9055
9218
  if (isNonProductionPath(relPath)) continue;
@@ -9334,7 +9497,7 @@ const detectUnusedCss = async (context) => {
9334
9497
  rule: "ai-slop/unused-css",
9335
9498
  severity: "warning",
9336
9499
  message: `CSS class '.${def.name}' is defined but never referenced in the codebase`,
9337
- help: "Remove the dead CSS rule, or confirm the class is used dynamically (and add an aislop-ignore comment if so)",
9500
+ help: "Remove the dead CSS rule, or confirm the class is used dynamically (and add a raigal-ignore comment if so)",
9338
9501
  line: def.line,
9339
9502
  column: 0,
9340
9503
  category: "AI Slop",
@@ -10324,7 +10487,8 @@ const checkFunctionDiagnostics = (relativePath, fn, limits, ext) => {
10324
10487
  const results = [];
10325
10488
  const multiplier = functionLocMultiplier(fn, ext);
10326
10489
  const fnMax = functionLocBudget(fn, ext, limits.maxFunctionLoc);
10327
- if (exceedsLimit(fn.lineCount - fn.templateLines, limits.maxFunctionLoc, multiplier)) results.push({
10490
+ const effectiveLineCount = fn.lineCount - fn.templateLines;
10491
+ if (exceedsLimit(effectiveLineCount, limits.maxFunctionLoc, multiplier)) results.push({
10328
10492
  filePath: relativePath,
10329
10493
  engine: "code-quality",
10330
10494
  rule: "complexity/function-too-long",
@@ -10381,10 +10545,10 @@ const checkFileComplexity = (filePath, rootDirectory, limits) => {
10381
10545
  };
10382
10546
  const checkComplexity = async (context) => {
10383
10547
  const files = getSourceFiles(context);
10384
- const limits = context.config.quality;
10385
10548
  const diagnostics = [];
10386
10549
  for (const filePath of files) {
10387
10550
  if (isAutoGenerated(filePath)) continue;
10551
+ const { quality: limits } = resolveFilePolicy(context.config, overrideRelativePath(context.rootDirectory, filePath));
10388
10552
  diagnostics.push(...checkFileComplexity(filePath, context.rootDirectory, limits));
10389
10553
  }
10390
10554
  return diagnostics;
@@ -10414,14 +10578,14 @@ const isTrivialLine = (line) => {
10414
10578
  };
10415
10579
  const SVG_MARKUP_RE = /<\/?(?:svg|path|polyline|line|circle|rect|g)\b|(?:xmlns|viewBox|stroke(?:-width|-linecap|-linejoin)?|fill|fill-opacity|d|points|x1|x2|y1|y2)=/;
10416
10580
  const DATA_LITERAL_RE = /^\s*(?:[A-Za-z_$][\w$-]*:\s*(?:["'`[{]|\d|true\b|false\b|null\b)|["'`][^"'`]*["'`],?\s*$|[{}\]],?\s*$|\),?\s*$)/;
10417
- const SUPPRESS_RE = /aislop[- ]ignore(?:-next-block|-file)?\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
10418
- const FILE_SUPPRESS_RE = /aislop[- ]ignore-file\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
10581
+ const SUPPRESS_RE = /(?:raigal|aislop)[- ]ignore(?:-next-block|-file)?\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
10582
+ const FILE_SUPPRESS_RE = /(?:raigal|aislop)[- ]ignore-file\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
10419
10583
  const fileHasSuppression = (content) => FILE_SUPPRESS_RE.test(content);
10420
10584
  const isLowSignalMarkupWindow = (lines) => {
10421
10585
  return lines.filter((line) => SVG_MARKUP_RE.test(line)).length >= Math.ceil(WINDOW_SIZE / 2);
10422
10586
  };
10423
10587
  const isLowSignalDataWindow = (lines) => {
10424
- return lines.filter((line) => DATA_LITERAL_RE.test(line)).length >= WINDOW_SIZE - 1;
10588
+ return lines.filter((line) => DATA_LITERAL_RE.test(line)).length >= 9;
10425
10589
  };
10426
10590
  const findSuppressedLines = (lines) => {
10427
10591
  const suppressed = /* @__PURE__ */ new Set();
@@ -10453,7 +10617,7 @@ const collectMeaningfulLines = (content) => {
10453
10617
  if (isLowSignalDataWindow(window)) continue;
10454
10618
  if (window.every(isTrivialLine)) continue;
10455
10619
  const normalised = window.map(normaliseLine);
10456
- if (normalised.filter((n) => n.length > 0 && n !== "}" && n !== "{").length < WINDOW_SIZE - 1) continue;
10620
+ if (normalised.filter((n) => n.length > 0 && n !== "}" && n !== "{").length < 9) continue;
10457
10621
  if (new Set(normalised).size < MIN_DISTINCT_LINES) continue;
10458
10622
  hits.push({
10459
10623
  startLine: i + 1,
@@ -10557,8 +10721,8 @@ const shouldIncludeIssue = (issueType, filePath) => {
10557
10721
  return !filePath.replace(/\\/g, "/").includes(".github/workflows/");
10558
10722
  };
10559
10723
  const DEPENDENCY_HELP = {
10560
- dependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
10561
- devDependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
10724
+ dependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `raigal fix`.",
10725
+ devDependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `raigal fix`.",
10562
10726
  unlisted: "This package is imported in code but not declared in package.json. Run `npm install` to add it.",
10563
10727
  unresolved: "This import cannot be resolved. Check for typos or missing packages.",
10564
10728
  binaries: "This binary is used but its package is not in package.json."
@@ -11276,13 +11440,23 @@ const withReportPath = async (run) => {
11276
11440
  };
11277
11441
  const UNSUPPORTED_GLOB_SYNTAX = /[?[\]{}!|()\\]/;
11278
11442
  const isDotnetFormatPath = (pattern) => !pattern.startsWith("-") && !UNSUPPORTED_GLOB_SYNTAX.test(pattern);
11443
+ const expandBraces = (pattern, depth = 0) => {
11444
+ if (depth > 10) return [pattern];
11445
+ const match = /\{([^{}]+)\}/.exec(pattern);
11446
+ if (!match) return [pattern];
11447
+ const [fullMatch, options] = match;
11448
+ const parts = options.split(",");
11449
+ const prefix = pattern.slice(0, match.index);
11450
+ const suffix = pattern.slice(match.index + fullMatch.length);
11451
+ return parts.flatMap((part) => expandBraces(`${prefix}${part}${suffix}`, depth + 1));
11452
+ };
11279
11453
  const buildDotnetFormatExcludeScope = (excludePatterns) => {
11280
11454
  const scope = {
11281
11455
  excludeArguments: [],
11282
11456
  unsupportedPatterns: []
11283
11457
  };
11284
11458
  for (const pattern of normalizeExcludePatterns(excludePatterns ?? [])) {
11285
- const expanded = micromatch.braces(pattern, { expand: true });
11459
+ const expanded = expandBraces(pattern);
11286
11460
  if (expanded.every(isDotnetFormatPath)) scope.excludeArguments.push(...expanded);
11287
11461
  else scope.unsupportedPatterns.push(pattern);
11288
11462
  }
@@ -11507,7 +11681,7 @@ const runGofmt = async (context) => {
11507
11681
  rule: "go-formatting",
11508
11682
  severity: "warning",
11509
11683
  message: "Go file is not formatted correctly",
11510
- help: "Run `aislop fix` to auto-format with gofmt",
11684
+ help: "Run `raigal fix` to auto-format with gofmt",
11511
11685
  line: 0,
11512
11686
  column: 0,
11513
11687
  category: "Format",
@@ -11577,7 +11751,7 @@ const parseRuffFormatOutput = (output, rootDir) => {
11577
11751
  rule: "python-formatting",
11578
11752
  severity: "warning",
11579
11753
  message: "Python file is not formatted correctly",
11580
- help: "Run `aislop fix` to auto-format with ruff",
11754
+ help: "Run `raigal fix` to auto-format with ruff",
11581
11755
  line: 0,
11582
11756
  column: 0,
11583
11757
  category: "Format",
@@ -11589,7 +11763,8 @@ const parseRuffFormatOutput = (output, rootDir) => {
11589
11763
  const fixRuffFormat = async (context) => {
11590
11764
  const targets = context.files ? getPythonTargets(context) : [context.rootDirectory];
11591
11765
  if (context.files && targets.length === 0) return;
11592
- const result = await runSubprocess(resolveToolBinary("ruff"), ["format", ...targets], {
11766
+ const ruffBinary = resolveToolBinary("ruff");
11767
+ const result = await runSubprocess(ruffBinary, ["format", ...targets], {
11593
11768
  cwd: context.rootDirectory,
11594
11769
  timeout: 6e4
11595
11770
  });
@@ -11659,7 +11834,8 @@ const parseClangTidyOutput = (output, rootDirectory) => {
11659
11834
  const runClangTidy = async (context) => {
11660
11835
  const compileCommandsDir = findCompileCommandsDir(context);
11661
11836
  if (!compileCommandsDir) return [];
11662
- const sources = filterSourcesInDatabase(findCppSources(context).filter((f) => CPP_IMPL_EXTENSIONS.has(path.extname(f).toLowerCase())), readCompileCommandsFiles(compileCommandsDir));
11837
+ const implSources = findCppSources(context).filter((f) => CPP_IMPL_EXTENSIONS.has(path.extname(f).toLowerCase()));
11838
+ const sources = filterSourcesInDatabase(implSources, readCompileCommandsFiles(compileCommandsDir));
11663
11839
  if (sources.length === 0) return [];
11664
11840
  const diagnostics = [];
11665
11841
  for (const chunk of chunkFilePaths(sources, CLANG_TIDY_CHUNK_MAX_FILES)) try {
@@ -12029,7 +12205,7 @@ const runExpoDoctor = async (context) => {
12029
12205
  rule: "expo-doctor/config-error",
12030
12206
  severity: "warning",
12031
12207
  message: configError,
12032
- help: "Install project dependencies, then re-run `aislop scan`.",
12208
+ help: "Install project dependencies, then re-run `raigal scan`.",
12033
12209
  line: 0,
12034
12210
  column: 0,
12035
12211
  category: "Expo",
@@ -12495,7 +12671,8 @@ const processFile = (filePath, fileTargets, result) => {
12495
12671
  });
12496
12672
  continue;
12497
12673
  }
12498
- const { edit, skipReason } = computeEdit(sourceFile, classifyIdentifier(best));
12674
+ const shape = classifyIdentifier(best);
12675
+ const { edit, skipReason } = computeEdit(sourceFile, shape);
12499
12676
  if (!edit) {
12500
12677
  pendingSkips.push({
12501
12678
  target,
@@ -13076,7 +13253,7 @@ const normalizeEslintIgnorePatterns = (patterns) => patterns.map((pattern) => pa
13076
13253
  if (!pattern.includes("*") && !path.extname(pattern)) return [`${pattern}/**`];
13077
13254
  return [pattern];
13078
13255
  });
13079
- const isEslintIgnored = (relativePath, patterns) => patterns.length > 0 && micromatch.isMatch(relativePath, patterns, { dot: true });
13256
+ const isEslintIgnored = (relativePath, patterns) => patterns.length > 0 && picomatch.isMatch(relativePath, patterns, { dot: true });
13080
13257
  const getOxlintTargets = (context) => {
13081
13258
  const eslintIgnorePatterns = normalizeEslintIgnorePatterns(collectEslintIgnorePatterns(context.rootDirectory));
13082
13259
  return getSourceFiles(context).filter((filePath) => OXLINT_EXTENSIONS.has(path.extname(filePath).toLowerCase())).filter((filePath) => !isAutoGenerated(filePath)).map((filePath) => path.relative(context.rootDirectory, filePath).split(path.sep).join("/")).filter((filePath) => !isEslintIgnored(filePath, eslintIgnorePatterns));
@@ -13310,7 +13487,8 @@ const ruffLintFixArgs = (context, unsafe) => {
13310
13487
  };
13311
13488
  const fixRuffLint = async (context) => {
13312
13489
  if (context.files && getPythonTargets(context).length === 0) return;
13313
- const result = await runSubprocess(resolveToolBinary("ruff"), ruffLintFixArgs(context, false), {
13490
+ const ruffBinary = resolveToolBinary("ruff");
13491
+ const result = await runSubprocess(ruffBinary, ruffLintFixArgs(context, false), {
13314
13492
  cwd: context.rootDirectory,
13315
13493
  timeout: 6e4
13316
13494
  });
@@ -13318,7 +13496,8 @@ const fixRuffLint = async (context) => {
13318
13496
  };
13319
13497
  const fixRuffLintForce = async (context) => {
13320
13498
  if (context.files && getPythonTargets(context).length === 0) return;
13321
- const result = await runSubprocess(resolveToolBinary("ruff"), ruffLintFixArgs(context, true), {
13499
+ const ruffBinary = resolveToolBinary("ruff");
13500
+ const result = await runSubprocess(ruffBinary, ruffLintFixArgs(context, true), {
13322
13501
  cwd: context.rootDirectory,
13323
13502
  timeout: 6e4
13324
13503
  });
@@ -13407,6 +13586,174 @@ const lintEngine = {
13407
13586
  }
13408
13587
  };
13409
13588
  //#endregion
13589
+ //#region src/engines/security/ai-security.ts
13590
+ const JS_EXTS = /* @__PURE__ */ new Set([
13591
+ ".ts",
13592
+ ".tsx",
13593
+ ".js",
13594
+ ".jsx",
13595
+ ".mjs",
13596
+ ".cjs"
13597
+ ]);
13598
+ const PY_EXTS = /* @__PURE__ */ new Set([".py"]);
13599
+ const UNICODE_TAG_RE = /[\u{E0000}-\u{E007F}]/gu;
13600
+ const hasRuleIgnore = (lines, lineIndex, ruleSuffix) => {
13601
+ const start = Math.max(0, lineIndex - 2);
13602
+ const re = new RegExp(`(?:raigal|aislop)[- ]ignore(?:-file)?\\b.*(?:${ruleSuffix}|security\\/${ruleSuffix})`, "i");
13603
+ return lines.slice(start, lineIndex + 1).some((line) => re.test(line));
13604
+ };
13605
+ const hasFileIgnore = (lines, ruleSuffix) => {
13606
+ const re = new RegExp(`(?:raigal|aislop)[- ]ignore-file\\b.*(?:${ruleSuffix}|security\\/${ruleSuffix})`, "i");
13607
+ return lines.slice(0, 10).some((line) => re.test(line));
13608
+ };
13609
+ const makeDiagnostic = (filePath, rule, message, help, line, column) => ({
13610
+ filePath,
13611
+ engine: "security",
13612
+ rule,
13613
+ severity: "error",
13614
+ message,
13615
+ help,
13616
+ line,
13617
+ column,
13618
+ category: "Security",
13619
+ fixable: false
13620
+ });
13621
+ const P_BYPASS = "bypassPermissions";
13622
+ const P_EXEC_TYPE = "executor_type";
13623
+ const P_LOCAL = "local";
13624
+ const P_USE_DOCKER = "use_docker";
13625
+ const P_AUTO_RUN = "auto_run";
13626
+ const P_UNSAFE_MODE = "unsafe_mode";
13627
+ const P_PYTHON_REPL = "PythonREPLTool";
13628
+ const UNSANDBOXED_TOOL_PATTERNS = [
13629
+ {
13630
+ pattern: new RegExp(`\\bpermission_?mode["']?\\s*[:=]\\s*["']${P_BYPASS}["']`, "i"),
13631
+ message: `permission_mode '${P_BYPASS}' bypasses agent sandboxing and human confirmation`,
13632
+ help: `Do not use ${P_BYPASS} mode in production; use standard interactive approval`
13633
+ },
13634
+ {
13635
+ pattern: new RegExp(`\\b${P_EXEC_TYPE}["']?\\s*[:=]\\s*["']${P_LOCAL}["']`, "i"),
13636
+ message: `${P_EXEC_TYPE}='${P_LOCAL}' executes LLM-generated code directly on host machine without container isolation`,
13637
+ help: "Configure agent to use DockerCommandLineCodeExecutor or a containerized sandbox environment"
13638
+ },
13639
+ {
13640
+ pattern: new RegExp(`\\b(?:LocalCommandLineCodeExecutor|LocalCodeExecutor)\\s*\\(`),
13641
+ message: "Local code executor executes LLM-generated code on host machine without container isolation",
13642
+ help: "Use DockerCommandLineCodeExecutor or an isolated execution sandbox"
13643
+ },
13644
+ {
13645
+ pattern: new RegExp(`\\b${P_USE_DOCKER}\\s*[:=]\\s*(?:False|false)\\b|["']${P_USE_DOCKER}["']\\s*:\\s*(?:False|false)\\b`),
13646
+ message: `${P_USE_DOCKER}=False disables container sandboxing for agent code execution`,
13647
+ help: `Set ${P_USE_DOCKER}=True to isolate agent execution inside Docker containers`
13648
+ },
13649
+ {
13650
+ pattern: new RegExp(`\\b(?:interpreter\\.)?${P_AUTO_RUN}\\s*[:=]\\s*(?:True|true)\\b`),
13651
+ message: `${P_AUTO_RUN}=True allows autonomous shell execution without human confirmation`,
13652
+ help: `Keep ${P_AUTO_RUN}=False so dangerous commands require explicit human review`
13653
+ },
13654
+ {
13655
+ pattern: new RegExp(`\\bCodeInterpreterTool\\s*\\([^)]*${P_UNSAFE_MODE}\\s*=\\s*(?:True|true)\\b|\\b${P_UNSAFE_MODE}\\s*[:=]\\s*(?:True|true)\\b`),
13656
+ message: `${P_UNSAFE_MODE}=True allows code interpreter tool to execute without Docker isolation`,
13657
+ help: `Disable ${P_UNSAFE_MODE} and isolate code execution in containerized environments`
13658
+ },
13659
+ {
13660
+ pattern: new RegExp(`\\b(?:${P_PYTHON_REPL}|PythonAstREPLTool)\\s*\\(`),
13661
+ message: `${P_PYTHON_REPL} executes arbitrary Python code in host process without container isolation`,
13662
+ help: `Use containerized sandbox execution rather than unconfined ${P_PYTHON_REPL} in production`
13663
+ }
13664
+ ];
13665
+ const P_SYS = "system";
13666
+ const P_DEV = "developer";
13667
+ const P_SM = "SystemMessage";
13668
+ const PROMPT_INJECTION_JS_PATTERNS = [
13669
+ new RegExp(`(?:role\\s*:\\s*["'](?:${P_SYS}|${P_DEV})["'][^}{;]{0,120}?content\\s*:\\s*\`[^\`]*\\$\\{[^}]+\\}[^\`]*\`)`, "g"),
13670
+ new RegExp(`(?:content\\s*:\\s*\`[^\`]*\\$\\{[^}]+\\}[^\`]*\`[^}{;]{0,120}?role\\s*:\\s*["'](?:${P_SYS}|${P_DEV})["'])`, "g"),
13671
+ new RegExp(`(?:role\\s*:\\s*["'](?:${P_SYS}|${P_DEV})["'][^}{;]{0,120}?content\\s*:\\s*(?:["'][^"'\\n]*["']\\s*\\+\\s*[a-zA-Z_$]|[a-zA-Z_$][\\w$]*\\s*\\+\\s*["']))`, "g"),
13672
+ new RegExp(`(?:content\\s*:\\s*(?:["'][^"'\\n]*["']\\s*\\+\\s*[a-zA-Z_$]|[a-zA-Z_$][\\w$]*\\s*\\+\\s*["'])[^}{;]{0,120}?role\\s*:\\s*["'](?:${P_SYS}|${P_DEV})["'])`, "g"),
13673
+ new RegExp(`\\b(?:${P_SYS}|systemPrompt|system_prompt|system_instruction)\\s*:\\s*\`[^\`]*\\$\\{[^}]+\\}[^\`]*\``, "g"),
13674
+ new RegExp(`\\b(?:${P_SYS}|systemPrompt|system_prompt|system_instruction)\\s*:\\s*(?:["'][^"'\\n]*["']\\s*\\+\\s*[a-zA-Z_$]|[a-zA-Z_$][\\w$]*\\s*\\+\\s*["'])`, "g"),
13675
+ new RegExp(`\\b(?:new\\s+)?${P_SM}\\s*\\(\\s*(?:\`[^\`]*\\$\\{[^}]+\\}[^\`]*\`|["'][^"'\\n]*["']\\s*\\+\\s*[a-zA-Z_$]|[a-zA-Z_$][\\w$]*\\s*\\+\\s*["']|\\{\\s*content\\s*:\\s*\`[^\`]*\\$\\{[^}]+\\}[^\`]*\`)`, "g"),
13676
+ new RegExp(`\\[\\s*["']${P_SYS}["']\\s*,\\s*\`[^\`]*\\$\\{[^}]+\\}[^\`]*\`\\s*\\]`, "g")
13677
+ ];
13678
+ const PROMPT_INJECTION_PY_PATTERNS = [
13679
+ new RegExp(`(?:["']role["']\\s*:\\s*["'](?:${P_SYS}|${P_DEV})["'][^}{;]{0,120}?["']content["']\\s*:\\s*f(?:"""[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?"""|\'\'\'[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?\'\'\'|["'][^"'\\n]*?\\{[^}]+\\}[^"'\\n]*?["']))`, "g"),
13680
+ new RegExp(`(?:["']content["']\\s*:\\s*f(?:"""[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?"""|\'\'\'[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?\'\'\'|["'][^"'\\n]*?\\{[^}]+\\}[^"'\\n]*?["'])[^}{;]{0,120}?["']role["']\\s*:\\s*["'](?:${P_SYS}|${P_DEV})["'])`, "g"),
13681
+ new RegExp(`(?:["']role["']\\s*:\\s*["'](?:${P_SYS}|${P_DEV})["'][^}{;]{0,120}?["']content["']\\s*:\\s*(?:["'][^"'\\n]*["']\\s*\\+\\s*[a-zA-Z_]|[a-zA-Z_]\\w*\\s*\\+\\s*["']|["'][^"'\\n]*["']\\.format\\())`, "g"),
13682
+ new RegExp(`(?:["']content["']\\s*:\\s*(?:["'][^"'\\n]*["']\\s*\\+\\s*[a-zA-Z_]|[a-zA-Z_]\\w*\\s*\\+\\s*["']|["'][^"'\\n]*["']\\.format\\()[^}{;]{0,120}?["']role["']\\s*:\\s*["'](?:${P_SYS}|${P_DEV})["'])`, "g"),
13683
+ new RegExp(`\\b(?:${P_SYS}|system_prompt|system_message|system_instruction)\\s*=\\s*f(?:"""[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?"""|\'\'\'[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?\'\'\'|["'][^"'\\n]*?\\{[^}]+\\}[^"'\\n]*?["'])`, "g"),
13684
+ new RegExp(`\\b(?:${P_SYS}|system_prompt|system_message|system_instruction)\\s*=\\s*(?:["'][^"'\\n]*["']\\s*\\+\\s*[a-zA-Z_]|[a-zA-Z_]\\w*\\s*\\+\\s*["']|["'][^"'\\n]*["']\\.format\\()`, "g"),
13685
+ new RegExp(`\\b${P_SM}\\s*\\(\\s*(?:content\\s*=\\s*)?f(?:"""[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?"""|\'\'\'[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?\'\'\'|["'][^"'\\n]*?\\{[^}]+\\}[^"'\\n]*?["'])`, "g"),
13686
+ new RegExp(`\\b${P_SM}\\s*\\(\\s*(?:content\\s*=\\s*)?(?:["'][^"'\\n]*["']\\s*\\+\\s*[a-zA-Z_]|[a-zA-Z_]\\w*\\s*\\+\\s*["']|["'][^"'\\n]*["']\\.format\\()`, "g"),
13687
+ new RegExp(`\\(\\s*["']${P_SYS}["']\\s*,\\s*f(?:"""[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?"""|\'\'\'[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?\'\'\'|["'][^"'\\n]*?\\{[^}]+\\}[^"'\\n]*?["'])\\s*\\)`, "g")
13688
+ ];
13689
+ const checkInvisibleUnicode = (relPath, content, lines) => {
13690
+ if (hasFileIgnore(lines, "invisible-unicode-tags")) return [];
13691
+ const results = [];
13692
+ const tagRegex = new RegExp(UNICODE_TAG_RE.source, UNICODE_TAG_RE.flags);
13693
+ for (const match of content.matchAll(tagRegex)) {
13694
+ const matchIndex = match.index ?? 0;
13695
+ const line = content.slice(0, matchIndex).split("\n").length;
13696
+ const col = matchIndex - content.lastIndexOf("\n", matchIndex - 1);
13697
+ if (hasRuleIgnore(lines, line - 1, "invisible-unicode-tags")) continue;
13698
+ results.push(makeDiagnostic(relPath, "security/invisible-unicode-tags", "Invisible Unicode Tag character (U+E0000-U+E007F) detected: potential stealth prompt injection or ASCII smuggling", "Remove invisible Unicode tag characters. Unicode Tag block (U+E0000-U+E007F) has no legitimate printable presentation in source code", line, col));
13699
+ }
13700
+ return results;
13701
+ };
13702
+ const checkUnsandboxedTools = (relPath, content, lines) => {
13703
+ if (hasFileIgnore(lines, "unsandboxed-agent-tool")) return [];
13704
+ const results = [];
13705
+ for (const { pattern, message, help } of UNSANDBOXED_TOOL_PATTERNS) {
13706
+ const regex = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
13707
+ for (const match of content.matchAll(regex)) {
13708
+ const matchIndex = match.index ?? 0;
13709
+ const line = content.slice(0, matchIndex).split("\n").length;
13710
+ const col = matchIndex - content.lastIndexOf("\n", matchIndex - 1);
13711
+ if (hasRuleIgnore(lines, line - 1, "unsandboxed-agent-tool")) continue;
13712
+ results.push(makeDiagnostic(relPath, "security/unsandboxed-agent-tool", message, help, line, col));
13713
+ }
13714
+ }
13715
+ return results;
13716
+ };
13717
+ const checkPromptInjection = (relPath, content, lines, isJs) => {
13718
+ if (hasFileIgnore(lines, "prompt-injection")) return [];
13719
+ const results = [];
13720
+ const patterns = isJs ? PROMPT_INJECTION_JS_PATTERNS : PROMPT_INJECTION_PY_PATTERNS;
13721
+ for (const pattern of patterns) {
13722
+ const regex = new RegExp(pattern.source, pattern.flags);
13723
+ for (const match of content.matchAll(regex)) {
13724
+ const matchIndex = match.index ?? 0;
13725
+ const line = content.slice(0, matchIndex).split("\n").length;
13726
+ const col = matchIndex - content.lastIndexOf("\n", matchIndex - 1);
13727
+ if (hasRuleIgnore(lines, line - 1, "prompt-injection")) continue;
13728
+ results.push(makeDiagnostic(relPath, "security/prompt-injection", "Untrusted variable interpolation into system prompt creates prompt injection vulnerability", "Keep system prompts static. Pass dynamic user inputs via user messages or input parameters with strict delimiters", line, col));
13729
+ }
13730
+ }
13731
+ return results;
13732
+ };
13733
+ const scanAiSecurity = async (context) => {
13734
+ const files = getSourceFiles(context);
13735
+ const diagnostics = [];
13736
+ for (const filePath of files) {
13737
+ const ext = path.extname(filePath);
13738
+ let content;
13739
+ try {
13740
+ content = fs.readFileSync(filePath, "utf-8");
13741
+ } catch {
13742
+ continue;
13743
+ }
13744
+ const relativePath = relativePosix(context.rootDirectory, filePath);
13745
+ const lines = content.split("\n");
13746
+ diagnostics.push(...checkInvisibleUnicode(relativePath, content, lines));
13747
+ const isJs = JS_EXTS.has(ext);
13748
+ const isPy = PY_EXTS.has(ext);
13749
+ if (isJs || isPy) {
13750
+ diagnostics.push(...checkUnsandboxedTools(relativePath, content, lines));
13751
+ diagnostics.push(...checkPromptInjection(relativePath, content, lines, isJs));
13752
+ }
13753
+ }
13754
+ return diagnostics;
13755
+ };
13756
+ //#endregion
13410
13757
  //#region src/engines/security/audit-value.ts
13411
13758
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
13412
13759
  const readString = (record, key) => {
@@ -13786,7 +14133,7 @@ const parseJsAudit = (output, source, manifest) => {
13786
14133
  rule: "security/dependency-audit-skipped",
13787
14134
  severity: "info",
13788
14135
  message: `Dependency audit skipped (${source}): lockfile is missing`,
13789
- help: errorDetail ?? "Generate a lockfile, then re-run `aislop scan` for dependency vulnerability checks.",
14136
+ help: errorDetail ?? "Generate a lockfile, then re-run `raigal scan` for dependency vulnerability checks.",
13790
14137
  line: 0,
13791
14138
  column: 0,
13792
14139
  category: "Security",
@@ -13897,7 +14244,8 @@ const readRuntimeDependencies = (rootDir) => {
13897
14244
  collectRuntimeNames(rootManifest, names);
13898
14245
  const globs = [...npmWorkspaceGlobs(rootManifest), ...pnpmWorkspaceGlobs(rootDir)];
13899
14246
  if (globs.length === 0) return { runtimeDependencies: names };
13900
- for (const relativeDir of micromatch(listPackageDirectories(rootDir), globs)) {
14247
+ const isMatch = picomatch(globs, { dot: true });
14248
+ for (const relativeDir of listPackageDirectories(rootDir).filter((dir) => isMatch(dir))) {
13901
14249
  const memberManifest = readJsonFile(path.join(rootDir, relativeDir, "package.json"));
13902
14250
  if (memberManifest) collectRuntimeNames(memberManifest, names);
13903
14251
  }
@@ -13959,10 +14307,11 @@ const auditSkippedDiagnostic = (source, help) => ({
13959
14307
  });
13960
14308
  const runNpmAudit = async (rootDir, timeout) => {
13961
14309
  try {
13962
- return parseJsAudit((await runSubprocess("npm", ["audit", "--json"], {
14310
+ const result = await runSubprocess("npm", ["audit", "--json"], {
13963
14311
  cwd: rootDir,
13964
14312
  timeout
13965
- })).stdout, "npm audit", readRuntimeDependencies(rootDir));
14313
+ });
14314
+ return parseJsAudit(result.stdout, "npm audit", readRuntimeDependencies(rootDir));
13966
14315
  } catch (error) {
13967
14316
  return [auditSkippedDiagnostic("npm audit", `Failed to run npm audit: ${errorMessageOf(error)}`)];
13968
14317
  }
@@ -13983,10 +14332,11 @@ const runBunAudit = async (rootDir, timeout) => {
13983
14332
  const runPnpmAuditWithFallback = async (rootDir, timeout) => {
13984
14333
  const canFallbackToNpm = fs.existsSync(path.join(rootDir, "package-lock.json"));
13985
14334
  try {
13986
- const diagnostics = parseJsAudit((await runSubprocess("pnpm", ["audit", "--json"], {
14335
+ const result = await runSubprocess("pnpm", ["audit", "--json"], {
13987
14336
  cwd: rootDir,
13988
14337
  timeout
13989
- })).stdout, "pnpm audit", readRuntimeDependencies(rootDir));
14338
+ });
14339
+ const diagnostics = parseJsAudit(result.stdout, "pnpm audit", readRuntimeDependencies(rootDir));
13990
14340
  if (diagnostics.some((d) => d.rule === "security/dependency-audit-skipped")) {
13991
14341
  if (canFallbackToNpm) return runNpmAudit(rootDir, timeout);
13992
14342
  return diagnostics;
@@ -14343,7 +14693,7 @@ const RISKY_PATTERNS = [
14343
14693
  //#region src/engines/security/risky.ts
14344
14694
  const hasDangerouslySetInnerHtmlIgnore = (lines, lineIndex) => {
14345
14695
  const start = Math.max(0, lineIndex - 2);
14346
- return lines.slice(start, lineIndex + 1).some((line) => /(?:biome-ignore|eslint-disable|aislop-ignore).*(?:noDangerouslySetInnerHtml|dangerouslySetInnerHTML|dangerously-set-innerhtml)/i.test(line));
14696
+ return lines.slice(start, lineIndex + 1).some((line) => /(?:biome-ignore|eslint-disable|raigal-ignore|aislop-ignore).*(?:noDangerouslySetInnerHtml|dangerouslySetInnerHTML|dangerously-set-innerhtml)/i.test(line));
14347
14697
  };
14348
14698
  const isStructuredDataScript = (content, matchIndex) => {
14349
14699
  const before = content.slice(Math.max(0, matchIndex - 300), matchIndex);
@@ -14671,7 +15021,11 @@ const ALL_ENGINES = [
14671
15021
  name: "security",
14672
15022
  async run(context) {
14673
15023
  const diagnostics = [];
14674
- const promises = [scanSecrets(context), detectRiskyConstructs(context)];
15024
+ const promises = [
15025
+ scanSecrets(context),
15026
+ detectRiskyConstructs(context),
15027
+ scanAiSecurity(context)
15028
+ ];
14675
15029
  if (context.config.security.audit) promises.push(runDependencyAudit(context));
14676
15030
  const results = await Promise.allSettled(promises);
14677
15031
  for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
@@ -14693,6 +15047,7 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
14693
15047
  const result = await engine.run(context);
14694
15048
  result.diagnostics = filterExcludedDiagnostics(result.diagnostics, context.rootDirectory, context.excludePatterns);
14695
15049
  result.elapsed = performance.now() - start;
15050
+ result.diagnostics = applyFileOverrides(result.diagnostics, context);
14696
15051
  onComplete?.(result);
14697
15052
  return result;
14698
15053
  } catch (error) {
@@ -14701,6 +15056,7 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
14701
15056
  diagnostics: [],
14702
15057
  elapsed: performance.now() - start,
14703
15058
  skipped: true,
15059
+ failed: true,
14704
15060
  skipReason: error instanceof Error ? error.message : String(error)
14705
15061
  };
14706
15062
  onComplete?.(result);
@@ -14711,9 +15067,13 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
14711
15067
  diagnostics: [],
14712
15068
  elapsed: 0,
14713
15069
  skipped: true,
15070
+ failed: true,
14714
15071
  skipReason: r.reason instanceof Error ? r.reason.message : String(r.reason)
14715
15072
  });
14716
- return dedupeEquivalenceDiagnostics(dedupeOverlappingComments(dedupeCSharpAsync(stripSecretDiagnostics(finalResults))));
15073
+ const stripped = stripSecretDiagnostics(finalResults);
15074
+ const csharp = dedupeCSharpAsync(stripped);
15075
+ const comments = dedupeOverlappingComments(csharp);
15076
+ return dedupeEquivalenceDiagnostics(comments);
14717
15077
  };
14718
15078
  const stripSecretDiagnostics = (results) => {
14719
15079
  for (const result of results) for (const d of result.diagnostics) if (d.redactSource || isSecretClassRule(d.rule)) {
@@ -14762,7 +15122,8 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
14762
15122
  const capScale = Math.max(1, fileCount.effectiveSourceFileCount / CAP_REFERENCE_FILES);
14763
15123
  const defaultRuleCap = typeof maxPerRule === "number" && maxPerRule > 0 ? maxPerRule : null;
14764
15124
  const capForRule = (key) => {
14765
- const ruleCap = scoreImpactForRule(key.slice(key.indexOf(":") + 1)).cap;
15125
+ const rule = key.slice(key.indexOf(":") + 1);
15126
+ const ruleCap = scoreImpactForRule(rule).cap;
14766
15127
  if (typeof ruleCap === "number") return defaultRuleCap ? Math.min(defaultRuleCap, ruleCap) : ruleCap;
14767
15128
  return defaultRuleCap;
14768
15129
  };
@@ -14929,6 +15290,10 @@ const SAFE_PROPERTY_NAMES = /* @__PURE__ */ new Set([
14929
15290
  "exit_code",
14930
15291
  "duration_ms",
14931
15292
  "error_kind",
15293
+ "error_name",
15294
+ "error_code",
15295
+ "failed_stage",
15296
+ "engines_failed",
14932
15297
  "score",
14933
15298
  "score_bucket",
14934
15299
  "finding_count",
@@ -15042,10 +15407,11 @@ const track = (input) => {
15042
15407
  cachedInstallId = ensured.installId;
15043
15408
  installCreated = ensured.created;
15044
15409
  }
15045
- const { clean, dropped } = redactProperties({
15410
+ const merged = {
15046
15411
  ...baseProperties(cachedInstallId),
15047
15412
  ...input.properties
15048
- });
15413
+ };
15414
+ const { clean, dropped } = redactProperties(merged);
15049
15415
  if (isDebug()) {
15050
15416
  const compact = JSON.stringify({
15051
15417
  event: input.event,
@@ -15157,6 +15523,8 @@ const buildCommandCompletedProps = (input) => {
15157
15523
  if (typeof input.fixSteps === "number") props.fix_steps = input.fixSteps;
15158
15524
  if (typeof input.fixResolved === "number") props.fix_resolved = input.fixResolved;
15159
15525
  if (typeof input.fixScoreDelta === "number") props.fix_score_delta = input.fixScoreDelta;
15526
+ if (input.errorName) props.error_name = input.errorName;
15527
+ if (input.errorCode) props.error_code = input.errorCode;
15160
15528
  return props;
15161
15529
  };
15162
15530
  const errorKindFromException = (error) => {
@@ -15166,6 +15534,26 @@ const errorKindFromException = (error) => {
15166
15534
  if (message.includes("engine") && message.includes("crash")) return "engine_crash";
15167
15535
  return "unknown";
15168
15536
  };
15537
+ const SAFE_ERROR_NAME = /^[A-Za-z_$][A-Za-z0-9_$]{0,39}$/;
15538
+ const SAFE_ERROR_CODE = /^[A-Za-z][A-Za-z0-9_]{0,39}$/;
15539
+ const errorIdentity = (error) => {
15540
+ let name;
15541
+ if (error instanceof Error && typeof error.name === "string" && error.name.length > 0) name = error.name;
15542
+ else if (error === null) name = "null";
15543
+ else if (typeof error === "object") name = "object";
15544
+ else name = typeof error;
15545
+ const identity = { error_name: SAFE_ERROR_NAME.test(name) ? name : "Error" };
15546
+ const code = error?.code;
15547
+ if (typeof code === "string" && SAFE_ERROR_CODE.test(code)) identity.error_code = code;
15548
+ else if (typeof code === "number" && Number.isFinite(code)) identity.error_code = String(code);
15549
+ return identity;
15550
+ };
15551
+ //#endregion
15552
+ //#region src/telemetry/fatal.ts
15553
+ const reportedErrors = /* @__PURE__ */ new Set();
15554
+ const markErrorReported = (error) => {
15555
+ reportedErrors.add(error);
15556
+ };
15169
15557
  //#endregion
15170
15558
  //#region src/telemetry/lifecycle.ts
15171
15559
  const STARTED_FLUSH_TIMEOUT_MS = 100;
@@ -15176,6 +15564,7 @@ const withCommandLifecycle = async (start, run) => {
15176
15564
  fileCount: start.fileCount,
15177
15565
  properties: start.properties
15178
15566
  });
15567
+ start.config;
15179
15568
  track({
15180
15569
  event: "cli_command_started",
15181
15570
  properties: startProps,
@@ -15192,6 +15581,7 @@ const withCommandLifecycle = async (start, run) => {
15192
15581
  startProps,
15193
15582
  exitCode: result.exitCode,
15194
15583
  durationMs,
15584
+ errorKind: result.errorKind,
15195
15585
  score: result.score ?? void 0,
15196
15586
  findingCount: result.findingCount,
15197
15587
  errorCount: result.errorCount,
@@ -15209,16 +15599,21 @@ const withCommandLifecycle = async (start, run) => {
15209
15599
  await flushTelemetry();
15210
15600
  return result;
15211
15601
  } catch (error) {
15602
+ const durationMs = performance.now() - startedAt;
15603
+ const identity = errorIdentity(error);
15212
15604
  track({
15213
15605
  event: "cli_command_completed",
15214
15606
  properties: buildCommandCompletedProps({
15215
15607
  startProps,
15216
15608
  exitCode: 1,
15217
- durationMs: performance.now() - startedAt,
15218
- errorKind: errorKindFromException(error)
15609
+ durationMs,
15610
+ errorKind: errorKindFromException(error),
15611
+ errorName: identity.error_name,
15612
+ errorCode: identity.error_code
15219
15613
  }),
15220
15614
  config: start.config
15221
15615
  });
15616
+ markErrorReported(error);
15222
15617
  await flushTelemetry();
15223
15618
  throw error;
15224
15619
  }
@@ -15296,14 +15691,17 @@ const renderRailStep = (step, deps = {}) => {
15296
15691
  * between the last step and the footer.
15297
15692
  */
15298
15693
  const renderRailConnector = (deps = {}) => {
15299
- return ` ${style(deps.theme ?? theme, "accentDim", (deps.symbols ?? symbols).rail)}\n`;
15694
+ const t = deps.theme ?? theme;
15695
+ const s = deps.symbols ?? symbols;
15696
+ return ` ${style(t, "accentDim", s.rail)}\n`;
15300
15697
  };
15301
15698
  /**
15302
15699
  * Render the rail-end footer line ("└ <footer text>").
15303
15700
  */
15304
15701
  const renderRailFooter = (footer, deps = {}) => {
15305
15702
  const t = deps.theme ?? theme;
15306
- return ` ${style(t, "accentDim", (deps.symbols ?? symbols).railEnd)} ${highlightAislop(footer, t)}\n`;
15703
+ const s = deps.symbols ?? symbols;
15704
+ return ` ${style(t, "accentDim", s.railEnd)} ${highlightAislop(footer, t)}\n`;
15307
15705
  };
15308
15706
  const renderRail = (input, deps = {}) => {
15309
15707
  const t = deps.theme ?? theme;
@@ -15430,7 +15828,8 @@ var LiveRail = class {
15430
15828
  */
15431
15829
  const renderHintLine = (hint, deps = {}) => {
15432
15830
  const t = deps.theme ?? theme;
15433
- return ` ${style(t, "accent", (deps.symbols ?? symbols).hint)} ${highlightAislop(maskSecrets(hint), t)}\n`;
15831
+ const s = deps.symbols ?? symbols;
15832
+ return ` ${style(t, "accent", s.hint)} ${highlightAislop(maskSecrets(hint), t)}\n`;
15434
15833
  };
15435
15834
  const createLogger = (deps = {}) => {
15436
15835
  const t = deps.theme ?? theme;
@@ -15575,7 +15974,8 @@ const groupByFile = (diagnostics) => {
15575
15974
  });
15576
15975
  };
15577
15976
  const isInstalled = (bin) => {
15578
- return spawnSync(process.platform === "win32" ? "where" : "which", [bin], { encoding: "utf-8" }).status === 0;
15977
+ const cmd = process.platform === "win32" ? "where" : "which";
15978
+ return spawnSync(cmd, [bin], { encoding: "utf-8" }).status === 0;
15579
15979
  };
15580
15980
  const copyToClipboard = (text) => {
15581
15981
  const args = {
@@ -15714,7 +16114,9 @@ const createEngineContext = (rootDirectory, projectInfo, config, options = {}) =
15714
16114
  quality: config.quality,
15715
16115
  security: config.security,
15716
16116
  lint: config.lint,
15717
- engines: config.engines
16117
+ engines: config.engines,
16118
+ overrides: config.overrides,
16119
+ rules: config.rules
15718
16120
  },
15719
16121
  ...options.scope ? {
15720
16122
  files: options.scope.files,
@@ -16056,7 +16458,9 @@ const verifyEntitlementJwt = (jwt, options = {}) => {
16056
16458
  if (!header.kid) throw new JwtVerificationError("JWT header missing 'kid'", "unknown_kid");
16057
16459
  const publicKey = getPublicKey(header.kid);
16058
16460
  if (!publicKey) throw new JwtVerificationError(`Unknown or untrusted signing key id (kid: ${header.kid})`, "unknown_kid");
16059
- if (!verify(null, Buffer.from(`${headerB64}.${payloadB64}`, "utf-8"), publicKey, Buffer.from(signatureB64, "base64url"))) throw new JwtVerificationError("Invalid JWT cryptographic signature", "invalid_signature");
16461
+ const signedData = Buffer.from(`${headerB64}.${payloadB64}`, "utf-8");
16462
+ const signature = Buffer.from(signatureB64, "base64url");
16463
+ if (!verify(null, signedData, publicKey, signature)) throw new JwtVerificationError("Invalid JWT cryptographic signature", "invalid_signature");
16060
16464
  let rawPayload;
16061
16465
  try {
16062
16466
  rawPayload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf-8"));
@@ -16068,7 +16472,7 @@ const verifyEntitlementJwt = (jwt, options = {}) => {
16068
16472
  const claims = parsed.data;
16069
16473
  const expectedIssuer = process.env.RAIGAL_ISSUER?.trim() || "https://app.raigal.dev";
16070
16474
  if (claims.iss !== expectedIssuer) throw new JwtVerificationError(`Untrusted issuer: ${claims.iss}`, "invalid_issuer");
16071
- if (!isVersionAtLeast("0.2.0", claims.min_cli_version)) throw new JwtVerificationError(`CLI version ${APP_VERSION} is below required minimum ${claims.min_cli_version}. Run "raigal update" or "npm install -g @methiu/raigal@latest".`, "cli_version_unsupported");
16475
+ if (!isVersionAtLeast("0.2.3", claims.min_cli_version)) throw new JwtVerificationError(`CLI version ${APP_VERSION} is below required minimum ${claims.min_cli_version}. Run "raigal update" or "npm install -g @methiu/raigal@latest".`, "cli_version_unsupported");
16072
16476
  const now = options.nowSeconds ?? Math.floor(Date.now() / 1e3);
16073
16477
  const grace = options.allowGraceSeconds ?? 0;
16074
16478
  if (now > claims.exp + grace) {
@@ -16102,7 +16506,7 @@ const fetchGitHubOidcToken = async () => {
16102
16506
  };
16103
16507
  //#endregion
16104
16508
  //#region src/cloud/entitlement.ts
16105
- const GRACE_PERIOD_SECONDS = 4320 * 60;
16509
+ const GRACE_PERIOD_SECONDS = 259200;
16106
16510
  var MissingCredentialError = class extends Error {
16107
16511
  constructor(message = "No organization credential found") {
16108
16512
  super(message);
@@ -16182,6 +16586,10 @@ const handleEntitlementFetchError = (err, cached, cachedClaims, now) => {
16182
16586
  nowSeconds: now,
16183
16587
  allowGraceSeconds: GRACE_PERIOD_SECONDS
16184
16588
  });
16589
+ if (graceClaims.plan === "trial" && graceClaims.trial_ends_at && now > Math.floor(new Date(graceClaims.trial_ends_at).getTime() / 1e3)) {
16590
+ clearCachedEntitlement();
16591
+ throw new LicenceDeniedError("Organization trial has expired", "trial_expired");
16592
+ }
16185
16593
  return {
16186
16594
  jwt: cached.entitlement,
16187
16595
  claims: graceClaims,
@@ -16189,6 +16597,7 @@ const handleEntitlementFetchError = (err, cached, cachedClaims, now) => {
16189
16597
  grace: now > graceClaims.exp
16190
16598
  };
16191
16599
  } catch (jwtErr) {
16600
+ if (jwtErr instanceof LicenceDeniedError) throw jwtErr;
16192
16601
  if (jwtErr instanceof JwtVerificationError && jwtErr.code === "expired_beyond_grace") throw new LicenceUnverifiableError(`Raigal license could not be verified online and the offline grace period expired on ${(/* @__PURE__ */ new Date((cachedClaims?.exp ?? 0) * 1e3)).toISOString()}. Check network connectivity or Raigal Cloud status.`);
16193
16602
  }
16194
16603
  throw new LicenceUnverifiableError(`Raigal license could not be verified online (${err instanceof Error ? err.message : String(err)}). Check network connectivity or Raigal Cloud status.`);
@@ -16203,7 +16612,8 @@ const getEntitlement = async () => {
16203
16612
  } catch {
16204
16613
  cachedClaims = null;
16205
16614
  }
16206
- if (cached && cachedClaims && now < cached.refresh_after) return {
16615
+ const isTrialExpired = cachedClaims?.plan === "trial" && cachedClaims.trial_ends_at && now > Math.floor(new Date(cachedClaims.trial_ends_at).getTime() / 1e3);
16616
+ if (cached && cachedClaims && !isTrialExpired && now < cached.refresh_after) return {
16207
16617
  jwt: cached.entitlement,
16208
16618
  claims: cachedClaims,
16209
16619
  fromCache: true
@@ -16212,7 +16622,7 @@ const getEntitlement = async () => {
16212
16622
  if (!activeToken) {
16213
16623
  oidcToken = await fetchGitHubOidcToken();
16214
16624
  if (!oidcToken) {
16215
- if (cached && cachedClaims) return {
16625
+ if (cached && cachedClaims && !isTrialExpired) return {
16216
16626
  jwt: cached.entitlement,
16217
16627
  claims: cachedClaims,
16218
16628
  fromCache: true
@@ -16221,7 +16631,8 @@ const getEntitlement = async () => {
16221
16631
  }
16222
16632
  } else oidcToken = await fetchGitHubOidcToken();
16223
16633
  try {
16224
- const response = await fetchEntitlementFromApi(activeToken?.token ?? "", oidcToken);
16634
+ const tokenToPass = activeToken?.token ?? "";
16635
+ const response = await fetchEntitlementFromApi(tokenToPass, oidcToken);
16225
16636
  const claims = verifyEntitlementJwt(response.entitlement, { nowSeconds: now });
16226
16637
  saveCachedEntitlement(response.entitlement, response.refresh_after);
16227
16638
  return {
@@ -16235,7 +16646,7 @@ const getEntitlement = async () => {
16235
16646
  };
16236
16647
  //#endregion
16237
16648
  //#region src/cloud/owner.ts
16238
- const GITHUB_REMOTE_RE = /^(?:(?:git@|ssh:\/\/(?:git@)?)github\.com(?::(?:\d+\/)?|[:/])|https:\/\/(?:[^@]+@)?github\.com\/)([^/]+)\/([^/.\s]+?)(?:\.git)?\s*$/;
16649
+ const GITHUB_REMOTE_RE = /^(?:(?:git@|ssh:\/\/(?:git@)?)github\.com(?::(?:\d+\/)?|[:/])|https:\/\/(?:[^@]+@)?github\.com\/)([^/]+)\/([^/\s]+?)(?:\.git)?\s*$/;
16239
16650
  const parseGitRemote = (rawUrl) => {
16240
16651
  if (!rawUrl) return {
16241
16652
  owner: null,
@@ -16373,6 +16784,23 @@ const requireEntitlement = async (options = {}) => {
16373
16784
  try {
16374
16785
  const entitlement = await getEntitlement();
16375
16786
  const tokenKind = getActiveToken()?.kind ?? "session";
16787
+ if (entitlement.claims.plan === "trial" && entitlement.claims.trial_ends_at) {
16788
+ const trialEnd = new Date(entitlement.claims.trial_ends_at).getTime();
16789
+ if (Date.now() > trialEnd) {
16790
+ const message = "Raigal license denied: Organization trial has expired";
16791
+ if (mode === "throw") throw new LicenceDeniedError(message, "trial_expired");
16792
+ if (mode === "fatal") {
16793
+ process.stderr.write(`${message}\n`);
16794
+ process.exit(30);
16795
+ }
16796
+ process.stderr.write(`[raigal] ${message}\n`);
16797
+ return {
16798
+ ok: false,
16799
+ exitCode: 30,
16800
+ message
16801
+ };
16802
+ }
16803
+ }
16376
16804
  const detectedOwner = detectLocalRepoOwner(directory);
16377
16805
  if (options.repoOwner) detectedOwner.owner = options.repoOwner.toLowerCase();
16378
16806
  const ownerCheck = checkOwnerAgainstAllowlist(detectedOwner, entitlement.claims.allowed_owners, tokenKind);
@@ -16747,7 +17175,8 @@ const fixNarrativeComments = async (context) => {
16747
17175
  for (let i = 0; i < lines.length; i += 1) if (!toRemove.has(i + 1)) kept.push(lines[i]);
16748
17176
  const newContent = kept.join("\n");
16749
17177
  if (newContent !== content) {
16750
- if (!areLiteralMultisetsEqual(beforeLiterals, extractStringAndTemplateLiterals(newContent, ext))) continue;
17178
+ const afterLiterals = extractStringAndTemplateLiterals(newContent, ext);
17179
+ if (!areLiteralMultisetsEqual(beforeLiterals, afterLiterals)) continue;
16751
17180
  fs.writeFileSync(filePath, newContent);
16752
17181
  }
16753
17182
  }
@@ -16929,6 +17358,56 @@ const computeRemovalRange = (sourceFile, node, content) => {
16929
17358
  end: finalEnd
16930
17359
  };
16931
17360
  };
17361
+ const isReferencedOutside = (sourceFile, statement, name) => {
17362
+ const start = statement.getStart(sourceFile);
17363
+ const end = statement.getEnd();
17364
+ let found = false;
17365
+ const visit = (node) => {
17366
+ if (found) return;
17367
+ if (ts.isIdentifier(node) && node.text === name) {
17368
+ const pos = node.getStart(sourceFile);
17369
+ if (pos < start || pos >= end) found = true;
17370
+ return;
17371
+ }
17372
+ ts.forEachChild(node, visit);
17373
+ };
17374
+ visit(sourceFile);
17375
+ return found;
17376
+ };
17377
+ const exportKeywordRange = (sourceFile, statement, content) => {
17378
+ const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) ?? [] : [];
17379
+ if (modifiers.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword)) return null;
17380
+ const exportKeyword = modifiers.find((m) => m.kind === ts.SyntaxKind.ExportKeyword);
17381
+ if (!exportKeyword) return null;
17382
+ let end = exportKeyword.getEnd();
17383
+ while (end < content.length && (content[end] === " " || content[end] === " ")) end++;
17384
+ return {
17385
+ start: exportKeyword.getStart(sourceFile),
17386
+ end
17387
+ };
17388
+ };
17389
+ const removalFor = (sourceFile, statement, content, decl) => {
17390
+ if (!isReferencedOutside(sourceFile, statement, decl.name)) return null;
17391
+ const range = exportKeywordRange(sourceFile, statement, content);
17392
+ if (!range) return {
17393
+ type: "skip",
17394
+ reason: "still referenced in its own file",
17395
+ declaration: decl
17396
+ };
17397
+ return {
17398
+ type: "match",
17399
+ removal: {
17400
+ ...range,
17401
+ declaration: decl
17402
+ }
17403
+ };
17404
+ };
17405
+ const declaredNames = (statement) => {
17406
+ if (ts.isVariableStatement(statement)) return statement.declarationList.declarations.map((d) => ts.isIdentifier(d.name) ? d.name.text : null).filter((name) => name !== null);
17407
+ if (ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement) || ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement) || ts.isEnumDeclaration(statement) || ts.isModuleDeclaration(statement)) return statement.name && ts.isIdentifier(statement.name) ? [statement.name.text] : [];
17408
+ return [];
17409
+ };
17410
+ const isDeclaredMoreThanOnce = (sourceFile, name) => sourceFile.statements.filter((s) => declaredNames(s).includes(name)).length > 1;
16932
17411
  const kindOfStatement = (node) => {
16933
17412
  if (ts.isVariableStatement(node)) return "variable";
16934
17413
  if (ts.isFunctionDeclaration(node)) return "function";
@@ -16940,6 +17419,11 @@ const kindOfStatement = (node) => {
16940
17419
  };
16941
17420
  const matchStatement = (sourceFile, statement, content, decl) => {
16942
17421
  if (!kindOfStatement(statement)) return { type: "none" };
17422
+ if (declaredNames(statement).includes(decl.name) && isDeclaredMoreThanOnce(sourceFile, decl.name)) return {
17423
+ type: "skip",
17424
+ reason: "merged declaration",
17425
+ declaration: decl
17426
+ };
16943
17427
  if (ts.isVariableStatement(statement)) {
16944
17428
  const varDecls = statement.declarationList.declarations;
16945
17429
  if (varDecls.length === 0) return { type: "none" };
@@ -16955,6 +17439,8 @@ const matchStatement = (sourceFile, statement, content, decl) => {
16955
17439
  reason: "multi-declaration variable statement",
16956
17440
  declaration: decl
16957
17441
  };
17442
+ const referenced = removalFor(sourceFile, statement, content, decl);
17443
+ if (referenced) return referenced;
16958
17444
  if (initializerHasSideEffects(match.initializer)) return {
16959
17445
  type: "skip",
16960
17446
  reason: "initializer may have side effects",
@@ -16972,6 +17458,8 @@ const matchStatement = (sourceFile, statement, content, decl) => {
16972
17458
  if (!statement.name) return { type: "none" };
16973
17459
  if (statement.name.text !== decl.name) return { type: "none" };
16974
17460
  if (!nodeContainsLine(sourceFile, statement, decl.line)) return { type: "none" };
17461
+ const referenced = removalFor(sourceFile, statement, content, decl);
17462
+ if (referenced) return referenced;
16975
17463
  return {
16976
17464
  type: "match",
16977
17465
  removal: {
@@ -16983,6 +17471,8 @@ const matchStatement = (sourceFile, statement, content, decl) => {
16983
17471
  if (ts.isTypeAliasDeclaration(statement) || ts.isInterfaceDeclaration(statement) || ts.isEnumDeclaration(statement)) {
16984
17472
  if (statement.name.text !== decl.name) return { type: "none" };
16985
17473
  if (!nodeContainsLine(sourceFile, statement, decl.line)) return { type: "none" };
17474
+ const referenced = removalFor(sourceFile, statement, content, decl);
17475
+ if (referenced) return referenced;
16986
17476
  return {
16987
17477
  type: "match",
16988
17478
  removal: {
@@ -17056,7 +17546,7 @@ const detectUnusedDeclarations = async (context) => {
17056
17546
  rule: "code-quality/unused-declaration",
17057
17547
  severity: "warning",
17058
17548
  message: `Unused ${extracted.kind}: ${extracted.name}`,
17059
- help: "This top-level declaration is never used; aislop will remove it.",
17549
+ help: "This top-level declaration is never used; raigal will remove it.",
17060
17550
  line: d.line,
17061
17551
  column: d.column,
17062
17552
  category: "Dead Code",
@@ -17074,7 +17564,7 @@ const detectUnusedDeclarations = async (context) => {
17074
17564
  rule: "code-quality/unused-declaration",
17075
17565
  severity: "warning",
17076
17566
  message: `Unused ${kind}: ${name}`,
17077
- help: "This top-level declaration is never imported; aislop will remove it.",
17567
+ help: "This top-level declaration is never imported; raigal will remove it.",
17078
17568
  line: d.line,
17079
17569
  column: d.column,
17080
17570
  category: "Dead Code",
@@ -17180,7 +17670,7 @@ const removeUnusedDeclarations = (rootDirectory, declarations) => {
17180
17670
  };
17181
17671
  //#endregion
17182
17672
  //#region src/commands/fix-expo.ts
17183
- const INSTALL_TIMEOUT$1 = 1800 * 1e3;
17673
+ const INSTALL_TIMEOUT$1 = 18e5;
17184
17674
  const fixExpoDependencies = async (context, onProgress) => {
17185
17675
  await removeDisallowedExpoPackages(context.rootDirectory, onProgress);
17186
17676
  onProgress?.("Expo dependency alignment · running expo install --fix (can take a few minutes)");
@@ -17234,8 +17724,8 @@ const removeDisallowedExpoPackages = async (rootDir, onProgress) => {
17234
17724
  };
17235
17725
  //#endregion
17236
17726
  //#region src/commands/fix-force.ts
17237
- const INSTALL_TIMEOUT = 1800 * 1e3;
17238
- const AUDIT_TIMEOUT = 60 * 1e3;
17727
+ const INSTALL_TIMEOUT = 18e5;
17728
+ const AUDIT_TIMEOUT = 6e4;
17239
17729
  const detectPackageManager = (rootDirectory) => {
17240
17730
  if (fs.existsSync(path.join(rootDirectory, "pnpm-lock.yaml"))) return "pnpm";
17241
17731
  if (fs.existsSync(path.join(rootDirectory, "package-lock.json")) || fs.existsSync(path.join(rootDirectory, "package.json"))) return "npm";
@@ -17554,13 +18044,14 @@ const CANNOT_SCOPE_REASON = "cannot honour an explicit file list under --changes
17554
18044
  const MISSING_MANIFEST_REASON = "no manifest or lockfile in the selected scope";
17555
18045
  const fixScopeError = (resolvedDir, flags) => {
17556
18046
  if (flags.changes && flags.staged) return "--changes and --staged cannot be used together.";
17557
- return scanTargetError(resolvedDir, {
18047
+ const options = {
17558
18048
  changes: Boolean(flags.changes),
17559
18049
  staged: Boolean(flags.staged),
17560
18050
  base: flags.base,
17561
18051
  verbose: false,
17562
18052
  json: false
17563
- });
18053
+ };
18054
+ return scanTargetError(resolvedDir, options);
17564
18055
  };
17565
18056
  const collectFixFileScope = (rootDirectory, excludePatterns, includePatterns, flags) => {
17566
18057
  const mode = resolveScanScopeMode({
@@ -17619,9 +18110,10 @@ const runFormattingStep = async (deps) => {
17619
18110
  else if (deps.projectInfo.languages.includes("python")) log.warn("Python detected but ruff is not installed; skipping Python formatting fixes.");
17620
18111
  if (deps.projectInfo.languages.includes("go") && deps.projectInfo.installedTools.gofmt) await deps.runStep("Formatting (go)", () => runGofmt(deps.context), () => fixGofmt(deps.context));
17621
18112
  else if (deps.projectInfo.languages.includes("go")) log.warn("Go detected but gofmt is not installed; skipping Go formatting fixes.");
17622
- if (deps.projectInfo.languages.includes("rust") && deps.projectInfo.installedTools.rustfmt) if (isScopedFix(deps.context)) deps.skipStep?.("Formatting (rust)", CANNOT_SCOPE_REASON);
17623
- else await deps.runStep("Formatting (rust)", () => runGenericFormatter(deps.context, "rust"), () => fixGenericFormatter(deps.context, "rust"));
17624
- else if (deps.projectInfo.languages.includes("rust")) log.warn("Rust detected but rustfmt is not installed; skipping Rust formatting fixes.");
18113
+ if (deps.projectInfo.languages.includes("rust") && deps.projectInfo.installedTools.rustfmt) {
18114
+ if (isScopedFix(deps.context)) deps.skipStep?.("Formatting (rust)", CANNOT_SCOPE_REASON);
18115
+ else await deps.runStep("Formatting (rust)", () => runGenericFormatter(deps.context, "rust"), () => fixGenericFormatter(deps.context, "rust"));
18116
+ } else if (deps.projectInfo.languages.includes("rust")) log.warn("Rust detected but rustfmt is not installed; skipping Rust formatting fixes.");
17625
18117
  if (deps.projectInfo.languages.includes("ruby") && deps.projectInfo.installedTools.rubocop) {
17626
18118
  if (!skipUnsafeSafeFormatter(deps, "ruby")) await deps.runStep("Formatting (ruby)", () => runGenericFormatter(deps.context, "ruby"), () => fixGenericFormatter(deps.context, "ruby"));
17627
18119
  } else if (deps.projectInfo.languages.includes("ruby")) log.warn("Ruby detected but rubocop is not installed; skipping Ruby formatting fixes.");
@@ -17670,7 +18162,8 @@ const runDeclarationStep = async (deps) => {
17670
18162
  if (!deps.config.engines["code-quality"]) return;
17671
18163
  if (!hasJsOrTs(deps.projectInfo)) return;
17672
18164
  await deps.runStep("Unused declarations", () => detectUnusedDeclarations(deps.context), async () => {
17673
- const declarations = diagnosticsToDeclarations(await detectUnusedDeclarations(deps.context));
18165
+ const diagnostics = await detectUnusedDeclarations(deps.context);
18166
+ const declarations = diagnosticsToDeclarations(diagnostics);
17674
18167
  removeUnusedDeclarations(deps.resolvedDir, declarations);
17675
18168
  });
17676
18169
  };
@@ -17701,10 +18194,14 @@ const runForceSteps = async (deps) => {
17701
18194
  return (await runKnipUnusedFiles(deps.resolvedDir)).filter((diagnostic) => isPathInFixScope(deps.context, diagnostic.filePath));
17702
18195
  }, () => fixUnusedFiles(deps.resolvedDir, (filePath) => isPathInFixScope(deps.context, filePath)));
17703
18196
  const railUpdate = (label) => deps.rail.setActiveLabel(label);
17704
- if (deps.config.engines.security) if (!scopeIncludesManifestWrites(deps.context)) deps.skipStep?.("Dependency audit fixes", MISSING_MANIFEST_REASON);
17705
- else await deps.runStep("Dependency audit fixes", () => runDependencyAudit(deps.context), () => fixDependencyAudit(deps.context, railUpdate));
17706
- if (deps.projectInfo.frameworks.includes("expo") && deps.config.lint.expoDoctor) if (!scopeIncludesManifestWrites(deps.context)) deps.skipStep?.("Expo dependency alignment", MISSING_MANIFEST_REASON);
17707
- else await deps.runStep("Expo dependency alignment", () => runExpoDoctor(deps.context), () => fixExpoDependencies(deps.context, railUpdate));
18197
+ if (deps.config.engines.security) {
18198
+ if (!scopeIncludesManifestWrites(deps.context)) deps.skipStep?.("Dependency audit fixes", MISSING_MANIFEST_REASON);
18199
+ else await deps.runStep("Dependency audit fixes", () => runDependencyAudit(deps.context), () => fixDependencyAudit(deps.context, railUpdate));
18200
+ }
18201
+ if (deps.projectInfo.frameworks.includes("expo") && deps.config.lint.expoDoctor) {
18202
+ if (!scopeIncludesManifestWrites(deps.context)) deps.skipStep?.("Expo dependency alignment", MISSING_MANIFEST_REASON);
18203
+ else await deps.runStep("Expo dependency alignment", () => runExpoDoctor(deps.context), () => fixExpoDependencies(deps.context, railUpdate));
18204
+ }
17708
18205
  };
17709
18206
  //#endregion
17710
18207
  //#region src/commands/fix-plan.ts
@@ -18026,7 +18523,8 @@ const saveCachedPolicy = (repoSlug, policy) => {
18026
18523
  };
18027
18524
  const fetchRemotePolicy = async (repoSlug, token) => {
18028
18525
  try {
18029
- const data = await requestApi(`/v1/policy?repo=${encodeURIComponent(repoSlug.toLowerCase())}`, {
18526
+ const endpoint = `/v1/policy?repo=${encodeURIComponent(repoSlug.toLowerCase())}`;
18527
+ const data = await requestApi(endpoint, {
18030
18528
  method: "GET",
18031
18529
  token,
18032
18530
  timeoutMs: 4e3
@@ -18086,6 +18584,8 @@ const resolveHistoryDir = (directory) => {
18086
18584
  const resolved = path.resolve(directory);
18087
18585
  const primary = path.join(resolved, CONFIG_DIR);
18088
18586
  if (fs.existsSync(primary)) return primary;
18587
+ const legacy = path.join(resolved, ".aislop");
18588
+ if (fs.existsSync(legacy)) return legacy;
18089
18589
  return null;
18090
18590
  };
18091
18591
  const historyPath = (directory) => {
@@ -18198,6 +18698,842 @@ const recordFullScanActivity = (history, allowPilotInvitation, telemetryConfig)
18198
18698
  appendHistory(history);
18199
18699
  return allowPilotInvitation && !isHistoryDisabled() && !isTelemetryDisabled(telemetryConfig) && recordCompletedFullScan();
18200
18700
  };
18701
+ const AISLOP_MD_BODY = `# Raigal - agent instructions
18702
+
18703
+ [Raigal](https://github.com/meethiu/raigal) is your guardrail for shipping clean, maintainable, production-grade code. It catches the patterns AI coding tools leave behind: dead imports, redundant code, duplicate logic, unsafe any-typed casts, narrative comments, swallowed errors, hidden fallbacks, oversized functions, risky security constructs - and grades the project out of 100 so regressions are visible. Treat its findings as blocking, not advisory: the goal is code a human maintainer would happily inherit.
18704
+
18705
+ ## On every edit
18706
+
18707
+ A PostToolUse hook runs \`raigal hook claude\` after every Edit, Write, or MultiEdit. It scans the touched files and returns findings as JSON \`additionalContext\` shaped like \`RaigalFeedback\` (score, baseline, delta, regressed, counts, findings, newSinceBaseline, suggestedActions). Act on them the same turn; the \`suggestedActions\` field tells you exactly what to run next.
18708
+
18709
+ ## Severity ladder
18710
+
18711
+ - \`error\` - MUST fix this turn.
18712
+ - \`warning\` + \`fixable: true\` - MUST fix this turn.
18713
+ - \`warning\` + \`fixable: false\` - fix if trivially mechanical, otherwise surface in your reply.
18714
+
18715
+ ## Rules
18716
+
18717
+ - \`.raigal/config.yaml\` (or \`.aislop/config.yaml\`) - thresholds and engine toggles. Treat as authoritative; don't edit without user consent.
18718
+ - \`.raigal/rules.yaml\` (or \`.aislop/rules.yaml\`) - project-specific architecture rules (may be absent). When a finding cites \`architecture/*\`, open this file and follow it.
18719
+ - Custom rules can change between sessions. Trust what the scan returns, not a cached understanding of what the rules are.
18720
+
18721
+ ## Principles
18722
+
18723
+ - Do not disable rules to pass the scan. Fix the underlying issue.
18724
+ - If a finding is a false positive, leave it and explain in your reply - do not delete the rule config.
18725
+ - The findings payload includes \`nextSteps[]\` - treat those as your plan for the turn.
18726
+ `;
18727
+ //#endregion
18728
+ //#region src/hooks/io/atomic-write.ts
18729
+ const atomicWrite = (targetPath, content) => {
18730
+ const dir = path.dirname(targetPath);
18731
+ fs.mkdirSync(dir, { recursive: true });
18732
+ const rand = Math.random().toString(36).slice(2, 10);
18733
+ const tmp = path.join(dir, `.aislop-tmp-${process.pid}-${rand}`);
18734
+ fs.writeFileSync(tmp, content, "utf-8");
18735
+ try {
18736
+ fs.renameSync(tmp, targetPath);
18737
+ } catch (err) {
18738
+ const code = err?.code;
18739
+ if (code === "EPERM" || code === "EBUSY") {
18740
+ fs.copyFileSync(tmp, targetPath);
18741
+ try {
18742
+ fs.unlinkSync(tmp);
18743
+ } catch {}
18744
+ } else throw err;
18745
+ }
18746
+ };
18747
+ const readIfExists = (targetPath) => {
18748
+ try {
18749
+ return fs.readFileSync(targetPath, "utf-8");
18750
+ } catch {
18751
+ return null;
18752
+ }
18753
+ };
18754
+ //#endregion
18755
+ //#region src/hooks/io/sentinel.ts
18756
+ const sentinelHash = (content) => `sha256:${crypto.createHash("sha256").update(content).digest("hex").slice(0, 32)}`;
18757
+ const BEGIN_RE = /<!--\s*aislop:begin\s+v(\d+)(?:\s+hash=([^\s>]+))?\s*-->/;
18758
+ const END_RE = /<!--\s*aislop:end\s+v\d+\s*-->/;
18759
+ const renderFence = (body, hash) => [
18760
+ `<!-- aislop:begin v1 hash=${hash} -->`,
18761
+ body.trimEnd(),
18762
+ "<!-- aislop:end v1 -->"
18763
+ ].join("\n");
18764
+ const upsertMarkdownFence = (existing, body, hash) => {
18765
+ const fenced = renderFence(body, hash);
18766
+ if (existing == null || existing.length === 0) return {
18767
+ nextContent: `${fenced}\n`,
18768
+ replaced: false
18769
+ };
18770
+ const begin = existing.match(BEGIN_RE);
18771
+ const end = existing.match(END_RE);
18772
+ if (begin && end && (end.index ?? 0) > (begin.index ?? 0)) return {
18773
+ nextContent: `${existing.slice(0, begin.index)}${fenced}${existing.slice((end.index ?? 0) + end[0].length)}`,
18774
+ replaced: true
18775
+ };
18776
+ return {
18777
+ nextContent: `${existing}${existing.endsWith("\n") ? "" : "\n"}\n${fenced}\n`,
18778
+ replaced: false
18779
+ };
18780
+ };
18781
+ //#endregion
18782
+ //#region src/hooks/install/types.ts
18783
+ const emptyResult = () => ({
18784
+ wrote: [],
18785
+ skipped: [],
18786
+ planned: []
18787
+ });
18788
+ const applyContent = (result, opts, target, nextContent, summary) => {
18789
+ if (readIfExists(target) === nextContent) {
18790
+ result.skipped.push(target);
18791
+ return;
18792
+ }
18793
+ if (opts.dryRun) {
18794
+ result.planned.push({
18795
+ path: target,
18796
+ summary
18797
+ });
18798
+ return;
18799
+ }
18800
+ atomicWrite(target, nextContent);
18801
+ result.wrote.push(target);
18802
+ };
18803
+ const applyRemoval = (result, opts, target, nextContent) => {
18804
+ const existing = readIfExists(target);
18805
+ if (existing == null) {
18806
+ result.skipped.push(target);
18807
+ return;
18808
+ }
18809
+ if (existing === (nextContent ?? "")) {
18810
+ result.skipped.push(target);
18811
+ return;
18812
+ }
18813
+ if (opts.dryRun) {
18814
+ result.removed.push(target);
18815
+ return;
18816
+ }
18817
+ if (nextContent == null) try {
18818
+ fs.unlinkSync(target);
18819
+ } catch {}
18820
+ else atomicWrite(target, nextContent);
18821
+ result.removed.push(target);
18822
+ };
18823
+ //#endregion
18824
+ //#region src/hooks/install/rules-only.ts
18825
+ const installRulesOnly = (opts, paths, summary) => {
18826
+ const result = emptyResult();
18827
+ const existing = readIfExists(paths.rules);
18828
+ const hash = sentinelHash(AISLOP_MD_BODY);
18829
+ const next = upsertMarkdownFence(existing, AISLOP_MD_BODY, hash).nextContent;
18830
+ applyContent(result, opts, paths.rules, next, summary);
18831
+ if (paths.host && paths.marker) {
18832
+ const host = readIfExists(paths.host) ?? "";
18833
+ if (!host.includes(paths.marker)) {
18834
+ const joiner = host.endsWith("\n") || host.length === 0 ? "" : "\n";
18835
+ const prefix = host.length === 0 ? "" : `${host}${joiner}\n`;
18836
+ applyContent(result, opts, paths.host, `${prefix}${paths.marker}\n`, `append ${paths.marker} reference`);
18837
+ } else result.skipped.push(paths.host);
18838
+ }
18839
+ return result;
18840
+ };
18841
+ const uninstallRulesOnly = (opts, paths) => {
18842
+ const result = {
18843
+ removed: [],
18844
+ skipped: []
18845
+ };
18846
+ if (readIfExists(paths.rules) != null) applyRemoval(result, opts, paths.rules, null);
18847
+ else result.skipped.push(paths.rules);
18848
+ if (paths.host && paths.marker) {
18849
+ const host = readIfExists(paths.host);
18850
+ if (host?.includes(paths.marker)) {
18851
+ const stripped = host.split("\n").filter((l) => l.trim() !== paths.marker).join("\n").replace(/\n{3,}/g, "\n\n").trim();
18852
+ applyRemoval(result, opts, paths.host, stripped.length === 0 ? null : `${stripped}\n`);
18853
+ } else result.skipped.push(paths.host);
18854
+ }
18855
+ return result;
18856
+ };
18857
+ //#endregion
18858
+ //#region src/hooks/install/antigravity.ts
18859
+ const resolveAntigravityPaths = (opts) => ({ rules: path.join(opts.cwd, ".agents", "rules", "antigravity-aislop-rules.md") });
18860
+ const installAntigravity = (opts) => {
18861
+ if (opts.scope !== "project") return {
18862
+ wrote: [],
18863
+ skipped: [],
18864
+ planned: [{
18865
+ path: ".agents/rules/antigravity-aislop-rules.md",
18866
+ summary: "Antigravity is project-scope only; pass --project"
18867
+ }]
18868
+ };
18869
+ return installRulesOnly(opts, resolveAntigravityPaths(opts), "write .agents/rules/antigravity-aislop-rules.md");
18870
+ };
18871
+ const uninstallAntigravity = (opts) => {
18872
+ if (opts.scope !== "project") return {
18873
+ removed: [],
18874
+ skipped: []
18875
+ };
18876
+ return uninstallRulesOnly(opts, resolveAntigravityPaths(opts));
18877
+ };
18878
+ //#endregion
18879
+ //#region src/hooks/io/json-patch.ts
18880
+ const AISLOP_SENTINEL_KEY = "__aislop";
18881
+ const isAislopManaged = (x) => typeof x === "object" && x !== null && ("__aislop" in x && x["__aislop"] != null || "__raigal" in x && x["__raigal"] != null);
18882
+ const groupIsAislop = (group) => {
18883
+ if (typeof group !== "object" || group === null) return false;
18884
+ const hooks = group.hooks;
18885
+ if (!Array.isArray(hooks)) return false;
18886
+ return hooks.some((h) => isAislopManaged(h));
18887
+ };
18888
+ const upsertHookGroup = (config, event, group) => {
18889
+ const next = { ...config };
18890
+ const hooks = next.hooks && typeof next.hooks === "object" ? next.hooks : {};
18891
+ const cleaned = (Array.isArray(hooks[event]) ? hooks[event] : []).filter((g) => !groupIsAislop(g));
18892
+ next.hooks = {
18893
+ ...hooks,
18894
+ [event]: [...cleaned, group]
18895
+ };
18896
+ return next;
18897
+ };
18898
+ const upsertFlatHook = (config, event, entry) => {
18899
+ const next = { ...config };
18900
+ const hooks = next.hooks && typeof next.hooks === "object" ? next.hooks : {};
18901
+ const cleaned = (Array.isArray(hooks[event]) ? hooks[event] : []).filter((e) => !isAislopManaged(e));
18902
+ next.hooks = {
18903
+ ...hooks,
18904
+ [event]: [...cleaned, entry]
18905
+ };
18906
+ return next;
18907
+ };
18908
+ const removeAislopEntries = (config, event) => {
18909
+ const next = { ...config };
18910
+ const hooks = next.hooks && typeof next.hooks === "object" ? next.hooks : {};
18911
+ const existing = Array.isArray(hooks[event]) ? hooks[event] : [];
18912
+ const cleaned = existing.filter((e) => !isAislopManaged(e) && !groupIsAislop(e));
18913
+ const removed = existing.length - cleaned.length;
18914
+ const nextHooks = { ...hooks };
18915
+ if (cleaned.length === 0) delete nextHooks[event];
18916
+ else nextHooks[event] = cleaned;
18917
+ if (Object.keys(nextHooks).length === 0) delete next.hooks;
18918
+ else next.hooks = nextHooks;
18919
+ return {
18920
+ next,
18921
+ removed
18922
+ };
18923
+ };
18924
+ //#endregion
18925
+ //#region src/hooks/install/claude.ts
18926
+ const resolveClaudePaths = (opts) => {
18927
+ const root = opts.scope === "project" ? path.join(opts.cwd, ".claude") : path.join(opts.home, ".claude");
18928
+ return {
18929
+ settings: path.join(root, "settings.json"),
18930
+ aislopMd: path.join(root, "AISLOP.md"),
18931
+ claudeMd: path.join(root, "CLAUDE.md")
18932
+ };
18933
+ };
18934
+ const buildHookGroup$1 = () => {
18935
+ const hashBody = JSON.stringify({
18936
+ command: "raigal hook claude",
18937
+ matcher: "Edit|Write|MultiEdit"
18938
+ });
18939
+ return {
18940
+ matcher: "Edit|Write|MultiEdit",
18941
+ hooks: [{
18942
+ type: "command",
18943
+ command: "raigal hook claude",
18944
+ [AISLOP_SENTINEL_KEY]: {
18945
+ v: 1,
18946
+ managed: true,
18947
+ hash: sentinelHash(hashBody)
18948
+ }
18949
+ }]
18950
+ };
18951
+ };
18952
+ const buildStopHookGroup = () => {
18953
+ const hashBody = JSON.stringify({ command: "raigal hook claude --stop" });
18954
+ return {
18955
+ matcher: "",
18956
+ hooks: [{
18957
+ type: "command",
18958
+ command: "raigal hook claude --stop",
18959
+ [AISLOP_SENTINEL_KEY]: {
18960
+ v: 1,
18961
+ managed: true,
18962
+ hash: sentinelHash(hashBody)
18963
+ }
18964
+ }]
18965
+ };
18966
+ };
18967
+ const FILE_CHANGED_MATCHER = ".raigal/config.yml|.raigal/rules.yml|.aislop/config.yml|.aislop/rules.yml|package.json";
18968
+ const buildFileChangedHookGroup = () => {
18969
+ const hashBody = JSON.stringify({
18970
+ command: "raigal hook claude --on-file-changed",
18971
+ matcher: FILE_CHANGED_MATCHER
18972
+ });
18973
+ return {
18974
+ matcher: FILE_CHANGED_MATCHER,
18975
+ hooks: [{
18976
+ type: "command",
18977
+ command: "raigal hook claude --on-file-changed",
18978
+ [AISLOP_SENTINEL_KEY]: {
18979
+ v: 1,
18980
+ managed: true,
18981
+ hash: sentinelHash(hashBody)
18982
+ }
18983
+ }]
18984
+ };
18985
+ };
18986
+ const renderSettings$1 = (existingRaw, qualityGate) => {
18987
+ let obj = {};
18988
+ if (existingRaw) try {
18989
+ obj = JSON.parse(existingRaw);
18990
+ } catch {
18991
+ obj = {};
18992
+ }
18993
+ let next = upsertHookGroup(obj, "PostToolUse", buildHookGroup$1());
18994
+ next = upsertHookGroup(next, "FileChanged", buildFileChangedHookGroup());
18995
+ if (qualityGate) next = upsertHookGroup(next, "Stop", buildStopHookGroup());
18996
+ else next = removeAislopEntries(next, "Stop").next;
18997
+ return `${JSON.stringify(next, null, 2)}\n`;
18998
+ };
18999
+ const installClaude = (opts) => {
19000
+ const paths = resolveClaudePaths(opts);
19001
+ const result = emptyResult();
19002
+ const nextSettings = renderSettings$1(readIfExists(paths.settings), Boolean(opts.qualityGate));
19003
+ applyContent(result, opts, paths.settings, nextSettings, "register PostToolUse + FileChanged hooks");
19004
+ const mdHash = sentinelHash(AISLOP_MD_BODY);
19005
+ const existingMd = readIfExists(paths.aislopMd);
19006
+ const fenced = upsertMarkdownFence(existingMd, AISLOP_MD_BODY, mdHash);
19007
+ applyContent(result, opts, paths.aislopMd, fenced.nextContent, "write AISLOP.md rules");
19008
+ const existingClaudeMd = readIfExists(paths.claudeMd) ?? "";
19009
+ const marker = "@AISLOP.md";
19010
+ if (!existingClaudeMd.includes(marker)) {
19011
+ const joiner = existingClaudeMd.endsWith("\n") || existingClaudeMd.length === 0 ? "" : "\n";
19012
+ const prefix = existingClaudeMd.length === 0 ? "" : `${existingClaudeMd}${joiner}\n`;
19013
+ applyContent(result, opts, paths.claudeMd, `${prefix}${marker}\n`, "append @AISLOP.md reference");
19014
+ } else result.skipped.push(paths.claudeMd);
19015
+ return result;
19016
+ };
19017
+ const uninstallClaude = (opts) => {
19018
+ const paths = resolveClaudePaths({
19019
+ ...opts,
19020
+ qualityGate: false
19021
+ });
19022
+ const result = {
19023
+ removed: [],
19024
+ skipped: []
19025
+ };
19026
+ const settingsRaw = readIfExists(paths.settings);
19027
+ if (settingsRaw) {
19028
+ let obj = {};
19029
+ try {
19030
+ obj = JSON.parse(settingsRaw);
19031
+ } catch {
19032
+ obj = {};
19033
+ }
19034
+ const afterPostToolUse = removeAislopEntries(obj, "PostToolUse").next;
19035
+ const afterFileChanged = removeAislopEntries(afterPostToolUse, "FileChanged").next;
19036
+ const stripped = removeAislopEntries(afterFileChanged, "Stop").next;
19037
+ const stillHasHooks = stripped.hooks && typeof stripped.hooks === "object" && Object.keys(stripped.hooks).length > 0;
19038
+ const otherKeys = Object.keys(stripped).filter((k) => k !== "hooks");
19039
+ if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.settings, null);
19040
+ else applyRemoval(result, opts, paths.settings, `${JSON.stringify(stripped, null, 2)}\n`);
19041
+ } else result.skipped.push(paths.settings);
19042
+ if (readIfExists(paths.aislopMd) != null) applyRemoval(result, opts, paths.aislopMd, null);
19043
+ else result.skipped.push(paths.aislopMd);
19044
+ const claudeMd = readIfExists(paths.claudeMd);
19045
+ if (claudeMd?.includes("@AISLOP.md")) {
19046
+ const stripped = claudeMd.split("\n").filter((line) => line.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
19047
+ applyRemoval(result, opts, paths.claudeMd, stripped.length === 0 ? null : `${stripped}\n`);
19048
+ } else result.skipped.push(paths.claudeMd);
19049
+ return result;
19050
+ };
19051
+ //#endregion
19052
+ //#region src/hooks/install/cline.ts
19053
+ const resolveClinePaths = (opts) => ({ rules: path.join(opts.cwd, ".clinerules") });
19054
+ const resolveRooPaths = (opts) => ({ rules: path.join(opts.cwd, ".roo", "rules", "aislop.md") });
19055
+ const installCline = (opts) => {
19056
+ if (opts.scope !== "project") return {
19057
+ wrote: [],
19058
+ skipped: [],
19059
+ planned: [{
19060
+ path: ".clinerules",
19061
+ summary: "Cline is project-scope only; pass --project"
19062
+ }]
19063
+ };
19064
+ const cline = installRulesOnly(opts, resolveClinePaths(opts), "write .clinerules");
19065
+ const roo = installRulesOnly(opts, resolveRooPaths(opts), "write .roo/rules/aislop.md");
19066
+ return {
19067
+ wrote: [...cline.wrote, ...roo.wrote],
19068
+ skipped: [...cline.skipped, ...roo.skipped],
19069
+ planned: [...cline.planned, ...roo.planned]
19070
+ };
19071
+ };
19072
+ const uninstallCline = (opts) => {
19073
+ if (opts.scope !== "project") return {
19074
+ removed: [],
19075
+ skipped: []
19076
+ };
19077
+ const a = uninstallRulesOnly(opts, resolveClinePaths(opts));
19078
+ const b = uninstallRulesOnly(opts, resolveRooPaths(opts));
19079
+ return {
19080
+ removed: [...a.removed, ...b.removed],
19081
+ skipped: [...a.skipped, ...b.skipped]
19082
+ };
19083
+ };
19084
+ //#endregion
19085
+ //#region src/hooks/install/codex.ts
19086
+ const resolveCodexPaths = (opts) => ({ rules: opts.scope === "project" ? path.join(opts.cwd, "AGENTS.md") : path.join(opts.home, ".codex", "AGENTS.md") });
19087
+ const installCodex = (opts) => installRulesOnly(opts, resolveCodexPaths(opts), "write AGENTS.md rules for Codex");
19088
+ const uninstallCodex = (opts) => uninstallRulesOnly(opts, resolveCodexPaths(opts));
19089
+ //#endregion
19090
+ //#region src/hooks/install/copilot.ts
19091
+ const resolveCopilotPaths = (opts) => ({ rules: path.join(opts.cwd, ".github", "copilot-instructions.md") });
19092
+ const installCopilot = (opts) => {
19093
+ if (opts.scope !== "project") return {
19094
+ wrote: [],
19095
+ skipped: [],
19096
+ planned: [{
19097
+ path: ".github/copilot-instructions.md",
19098
+ summary: "Copilot is project-scope only; pass --project"
19099
+ }]
19100
+ };
19101
+ return installRulesOnly(opts, resolveCopilotPaths(opts), "write .github/copilot-instructions.md");
19102
+ };
19103
+ const uninstallCopilot = (opts) => {
19104
+ if (opts.scope !== "project") return {
19105
+ removed: [],
19106
+ skipped: []
19107
+ };
19108
+ return uninstallRulesOnly(opts, resolveCopilotPaths(opts));
19109
+ };
19110
+ //#endregion
19111
+ //#region src/hooks/install/cursor.ts
19112
+ const resolveCursorPaths = (opts) => {
19113
+ const root = opts.scope === "project" ? path.join(opts.cwd, ".cursor") : path.join(opts.home, ".cursor");
19114
+ return {
19115
+ hooks: path.join(root, "hooks.json"),
19116
+ rules: path.join(opts.cwd, ".cursor", "rules", "aislop.mdc")
19117
+ };
19118
+ };
19119
+ const buildHookEntry = () => {
19120
+ const hashBody = JSON.stringify({
19121
+ command: "raigal hook cursor",
19122
+ timeout: 5e3
19123
+ });
19124
+ return {
19125
+ command: "raigal hook cursor",
19126
+ type: "command",
19127
+ timeout: 5e3,
19128
+ [AISLOP_SENTINEL_KEY]: {
19129
+ v: 1,
19130
+ managed: true,
19131
+ hash: sentinelHash(hashBody)
19132
+ }
19133
+ };
19134
+ };
19135
+ const renderHooksJson = (existingRaw) => {
19136
+ let obj = { version: 1 };
19137
+ if (existingRaw) try {
19138
+ obj = JSON.parse(existingRaw);
19139
+ } catch {
19140
+ obj = { version: 1 };
19141
+ }
19142
+ if (typeof obj.version !== "number") obj.version = 1;
19143
+ const next = upsertFlatHook(obj, "afterFileEdit", buildHookEntry());
19144
+ return `${JSON.stringify(next, null, 2)}\n`;
19145
+ };
19146
+ const installCursor = (opts) => {
19147
+ const paths = resolveCursorPaths(opts);
19148
+ const result = emptyResult();
19149
+ const nextHooks = renderHooksJson(readIfExists(paths.hooks));
19150
+ applyContent(result, opts, paths.hooks, nextHooks, "register afterFileEdit hook");
19151
+ if (opts.scope === "project") {
19152
+ const existingRules = readIfExists(paths.rules);
19153
+ const hash = sentinelHash(AISLOP_MD_BODY);
19154
+ const rules = upsertMarkdownFence(existingRules, AISLOP_MD_BODY, hash).nextContent;
19155
+ applyContent(result, opts, paths.rules, rules, "write .cursor/rules/aislop.mdc");
19156
+ }
19157
+ return result;
19158
+ };
19159
+ const uninstallCursor = (opts) => {
19160
+ const paths = resolveCursorPaths(opts);
19161
+ const result = {
19162
+ removed: [],
19163
+ skipped: []
19164
+ };
19165
+ const raw = readIfExists(paths.hooks);
19166
+ if (raw) {
19167
+ let obj = {};
19168
+ try {
19169
+ obj = JSON.parse(raw);
19170
+ } catch {
19171
+ obj = {};
19172
+ }
19173
+ const stripped = removeAislopEntries(obj, "afterFileEdit").next;
19174
+ const stillHasHooks = stripped.hooks && typeof stripped.hooks === "object" && Object.keys(stripped.hooks).length > 0;
19175
+ const otherKeys = Object.keys(stripped).filter((k) => k !== "hooks" && k !== "version");
19176
+ if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.hooks, null);
19177
+ else applyRemoval(result, opts, paths.hooks, `${JSON.stringify(stripped, null, 2)}\n`);
19178
+ } else result.skipped.push(paths.hooks);
19179
+ if (opts.scope === "project") applyRemoval(result, opts, paths.rules, null);
19180
+ return result;
19181
+ };
19182
+ //#endregion
19183
+ //#region src/hooks/install/gemini.ts
19184
+ const resolveGeminiPaths = (opts) => {
19185
+ const root = opts.scope === "project" ? path.join(opts.cwd, ".gemini") : path.join(opts.home, ".gemini");
19186
+ return {
19187
+ settings: path.join(root, "settings.json"),
19188
+ aislopMd: path.join(root, "AISLOP.md"),
19189
+ geminiMd: path.join(root, "GEMINI.md")
19190
+ };
19191
+ };
19192
+ const buildHookGroup = () => {
19193
+ const hashBody = JSON.stringify({
19194
+ command: "raigal hook gemini",
19195
+ matcher: "write_file|replace"
19196
+ });
19197
+ return {
19198
+ matcher: "write_file|replace",
19199
+ hooks: [{
19200
+ name: "raigal",
19201
+ type: "command",
19202
+ command: "raigal hook gemini",
19203
+ timeout: 5e3,
19204
+ [AISLOP_SENTINEL_KEY]: {
19205
+ v: 1,
19206
+ managed: true,
19207
+ hash: sentinelHash(hashBody)
19208
+ }
19209
+ }]
19210
+ };
19211
+ };
19212
+ const renderSettings = (existingRaw) => {
19213
+ let obj = {};
19214
+ if (existingRaw) try {
19215
+ obj = JSON.parse(existingRaw);
19216
+ } catch {
19217
+ obj = {};
19218
+ }
19219
+ const next = upsertHookGroup(obj, "AfterTool", buildHookGroup());
19220
+ return `${JSON.stringify(next, null, 2)}\n`;
19221
+ };
19222
+ const installGemini = (opts) => {
19223
+ const paths = resolveGeminiPaths(opts);
19224
+ const result = emptyResult();
19225
+ const next = renderSettings(readIfExists(paths.settings));
19226
+ applyContent(result, opts, paths.settings, next, "register AfterTool hook");
19227
+ const existingMd = readIfExists(paths.aislopMd);
19228
+ const hash = sentinelHash(AISLOP_MD_BODY);
19229
+ const fenced = upsertMarkdownFence(existingMd, AISLOP_MD_BODY, hash).nextContent;
19230
+ applyContent(result, opts, paths.aislopMd, fenced, "write AISLOP.md rules");
19231
+ const existingGeminiMd = readIfExists(paths.geminiMd) ?? "";
19232
+ const marker = "@AISLOP.md";
19233
+ if (!existingGeminiMd.includes(marker)) {
19234
+ const joiner = existingGeminiMd.endsWith("\n") || existingGeminiMd.length === 0 ? "" : "\n";
19235
+ const prefix = existingGeminiMd.length === 0 ? "" : `${existingGeminiMd}${joiner}\n`;
19236
+ applyContent(result, opts, paths.geminiMd, `${prefix}${marker}\n`, "append @AISLOP.md reference");
19237
+ } else result.skipped.push(paths.geminiMd);
19238
+ return result;
19239
+ };
19240
+ const uninstallGemini = (opts) => {
19241
+ const paths = resolveGeminiPaths(opts);
19242
+ const result = {
19243
+ removed: [],
19244
+ skipped: []
19245
+ };
19246
+ const raw = readIfExists(paths.settings);
19247
+ if (raw) {
19248
+ let obj = {};
19249
+ try {
19250
+ obj = JSON.parse(raw);
19251
+ } catch {
19252
+ obj = {};
19253
+ }
19254
+ const stripped = removeAislopEntries(obj, "AfterTool").next;
19255
+ const stillHasHooks = stripped.hooks && typeof stripped.hooks === "object" && Object.keys(stripped.hooks).length > 0;
19256
+ const otherKeys = Object.keys(stripped).filter((k) => k !== "hooks");
19257
+ if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.settings, null);
19258
+ else applyRemoval(result, opts, paths.settings, `${JSON.stringify(stripped, null, 2)}\n`);
19259
+ } else result.skipped.push(paths.settings);
19260
+ if (readIfExists(paths.aislopMd) != null) applyRemoval(result, opts, paths.aislopMd, null);
19261
+ else result.skipped.push(paths.aislopMd);
19262
+ const geminiMd = readIfExists(paths.geminiMd);
19263
+ if (geminiMd?.includes("@AISLOP.md")) {
19264
+ const stripped = geminiMd.split("\n").filter((l) => l.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
19265
+ applyRemoval(result, opts, paths.geminiMd, stripped.length === 0 ? null : `${stripped}\n`);
19266
+ } else result.skipped.push(paths.geminiMd);
19267
+ return result;
19268
+ };
19269
+ //#endregion
19270
+ //#region src/hooks/install/kilocode.ts
19271
+ const resolveKilocodePaths = (opts) => ({ rules: path.join(opts.cwd, ".kilocode", "rules", "aislop-rules.md") });
19272
+ const installKilocode = (opts) => {
19273
+ if (opts.scope !== "project") return {
19274
+ wrote: [],
19275
+ skipped: [],
19276
+ planned: [{
19277
+ path: ".kilocode/rules/aislop-rules.md",
19278
+ summary: "Kilo Code is project-scope only; pass --project"
19279
+ }]
19280
+ };
19281
+ return installRulesOnly(opts, resolveKilocodePaths(opts), "write .kilocode/rules/aislop-rules.md");
19282
+ };
19283
+ const uninstallKilocode = (opts) => {
19284
+ if (opts.scope !== "project") return {
19285
+ removed: [],
19286
+ skipped: []
19287
+ };
19288
+ return uninstallRulesOnly(opts, resolveKilocodePaths(opts));
19289
+ };
19290
+ //#endregion
19291
+ //#region src/hooks/install/pi.ts
19292
+ const resolvePiPaths = (opts) => {
19293
+ return { extension: opts.scope === "project" ? path.join(opts.cwd, ".pi", "extensions", "aislop.js") : path.join(opts.home, ".pi", "agent", "extensions", "aislop.js") };
19294
+ };
19295
+ const PI_EXTENSION_SOURCE = `// raigal — auto-generated pi extension. Do not edit by hand.
19296
+ // Reinstall with: raigal hook install --pi
19297
+ import { spawn } from "node:child_process";
19298
+
19299
+ // Runs the CLI async so a slow scan never blocks pi's event loop, and forwards
19300
+ // ctx.signal so Esc actually kills the child instead of it running to completion.
19301
+ const runHook = (bin, args, input, signal) =>
19302
+ new Promise((resolve) => {
19303
+ const child = spawn(bin, args, { stdio: ["pipe", "pipe", "ignore"] });
19304
+ const chunks = [];
19305
+ let settled = false;
19306
+ let timer;
19307
+ const onAbort = () => {
19308
+ child.kill();
19309
+ finish(null);
19310
+ };
19311
+ const cleanup = () => {
19312
+ if (timer !== undefined) clearTimeout(timer);
19313
+ signal?.removeEventListener("abort", onAbort);
19314
+ };
19315
+ const finish = (value) => {
19316
+ if (settled) return;
19317
+ settled = true;
19318
+ cleanup();
19319
+ resolve(value);
19320
+ };
19321
+ signal?.addEventListener("abort", onAbort, { once: true });
19322
+ timer = setTimeout(() => {
19323
+ child.kill();
19324
+ finish(null);
19325
+ }, 15000);
19326
+ child.stdout.on("data", (c) => chunks.push(c));
19327
+ child.on("error", () => finish(null));
19328
+ child.on("close", (code) => {
19329
+ finish(code === 0 && chunks.length > 0 ? Buffer.concat(chunks).toString("utf-8") : null);
19330
+ });
19331
+ child.stdin.end(input);
19332
+ });
19333
+
19334
+ export default function (pi) {
19335
+ pi.on("tool_result", async (event, ctx) => {
19336
+ if (event.toolName !== "edit" && event.toolName !== "write") return;
19337
+ if (event.isError) return;
19338
+ const filePath = event.input && event.input.path;
19339
+ if (typeof filePath !== "string" || filePath.length === 0) return;
19340
+
19341
+ const bin = process.env.RAIGAL_BIN || process.env.AISLOP_BIN || "raigal";
19342
+ const payload = JSON.stringify({
19343
+ cwd: ctx.cwd,
19344
+ file_path: filePath,
19345
+ tool_name: event.toolName,
19346
+ });
19347
+
19348
+ let out;
19349
+ try {
19350
+ const stdout = await runHook(bin, ["hook", "pi"], payload, ctx.signal);
19351
+ if (!stdout) return;
19352
+ out = JSON.parse(stdout);
19353
+ } catch {
19354
+ return;
19355
+ }
19356
+ if (!out || !out.message) return;
19357
+
19358
+ return {
19359
+ content: [...event.content, { type: "text", text: out.message }],
19360
+ isError: event.isError,
19361
+ };
19362
+ });
19363
+ }
19364
+ `;
19365
+ const installPi = (opts) => {
19366
+ const paths = resolvePiPaths(opts);
19367
+ const result = emptyResult();
19368
+ applyContent(result, opts, paths.extension, PI_EXTENSION_SOURCE, "write pi raigal extension");
19369
+ return result;
19370
+ };
19371
+ const uninstallPi = (opts) => {
19372
+ const paths = resolvePiPaths(opts);
19373
+ const result = {
19374
+ removed: [],
19375
+ skipped: []
19376
+ };
19377
+ if (readIfExists(paths.extension) != null) applyRemoval(result, opts, paths.extension, null);
19378
+ else result.skipped.push(paths.extension);
19379
+ return result;
19380
+ };
19381
+ //#endregion
19382
+ //#region src/hooks/install/windsurf.ts
19383
+ const resolveWindsurfPaths = (opts) => ({ rules: path.join(opts.cwd, ".windsurfrules") });
19384
+ const installWindsurf = (opts) => {
19385
+ if (opts.scope !== "project") return {
19386
+ wrote: [],
19387
+ skipped: [],
19388
+ planned: [{
19389
+ path: ".windsurfrules",
19390
+ summary: "Windsurf is project-scope only; pass --project"
19391
+ }]
19392
+ };
19393
+ return installRulesOnly(opts, resolveWindsurfPaths(opts), "write .windsurfrules");
19394
+ };
19395
+ const uninstallWindsurf = (opts) => {
19396
+ if (opts.scope !== "project") return {
19397
+ removed: [],
19398
+ skipped: []
19399
+ };
19400
+ return uninstallRulesOnly(opts, resolveWindsurfPaths(opts));
19401
+ };
19402
+ //#endregion
19403
+ //#region src/hooks/install/registry.ts
19404
+ const ALL_AGENTS = [
19405
+ "claude",
19406
+ "cursor",
19407
+ "gemini",
19408
+ "pi",
19409
+ "codex",
19410
+ "windsurf",
19411
+ "cline",
19412
+ "kilocode",
19413
+ "antigravity",
19414
+ "copilot"
19415
+ ];
19416
+ const AGENTS_PROJECT_ONLY = [
19417
+ "windsurf",
19418
+ "cline",
19419
+ "kilocode",
19420
+ "antigravity",
19421
+ "copilot"
19422
+ ];
19423
+ const AGENTS_SUPPORTING_BOTH_SCOPES = [
19424
+ "claude",
19425
+ "cursor",
19426
+ "gemini",
19427
+ "pi",
19428
+ "codex"
19429
+ ];
19430
+ const paths = {
19431
+ claude: (opts) => {
19432
+ const p = resolveClaudePaths(opts);
19433
+ return [
19434
+ p.settings,
19435
+ p.aislopMd,
19436
+ p.claudeMd
19437
+ ];
19438
+ },
19439
+ cursor: (opts) => {
19440
+ const p = resolveCursorPaths(opts);
19441
+ return opts.scope === "project" ? [p.hooks, p.rules] : [p.hooks];
19442
+ },
19443
+ gemini: (opts) => {
19444
+ const p = resolveGeminiPaths(opts);
19445
+ return [
19446
+ p.settings,
19447
+ p.aislopMd,
19448
+ p.geminiMd
19449
+ ];
19450
+ },
19451
+ pi: (opts) => [resolvePiPaths(opts).extension],
19452
+ codex: (opts) => [resolveCodexPaths(opts).rules],
19453
+ windsurf: (opts) => [resolveWindsurfPaths(opts).rules],
19454
+ cline: (opts) => [resolveClinePaths(opts).rules, resolveRooPaths(opts).rules],
19455
+ kilocode: (opts) => [resolveKilocodePaths(opts).rules],
19456
+ antigravity: (opts) => [resolveAntigravityPaths(opts).rules],
19457
+ copilot: (opts) => [resolveCopilotPaths(opts).rules]
19458
+ };
19459
+ const REGISTRY = {
19460
+ claude: {
19461
+ install: installClaude,
19462
+ uninstall: uninstallClaude,
19463
+ paths: paths.claude
19464
+ },
19465
+ cursor: {
19466
+ install: installCursor,
19467
+ uninstall: uninstallCursor,
19468
+ paths: paths.cursor
19469
+ },
19470
+ gemini: {
19471
+ install: installGemini,
19472
+ uninstall: uninstallGemini,
19473
+ paths: paths.gemini
19474
+ },
19475
+ pi: {
19476
+ install: installPi,
19477
+ uninstall: uninstallPi,
19478
+ paths: paths.pi
19479
+ },
19480
+ codex: {
19481
+ install: installCodex,
19482
+ uninstall: uninstallCodex,
19483
+ paths: paths.codex
19484
+ },
19485
+ windsurf: {
19486
+ install: installWindsurf,
19487
+ uninstall: uninstallWindsurf,
19488
+ paths: paths.windsurf
19489
+ },
19490
+ cline: {
19491
+ install: installCline,
19492
+ uninstall: uninstallCline,
19493
+ paths: paths.cline
19494
+ },
19495
+ kilocode: {
19496
+ install: installKilocode,
19497
+ uninstall: uninstallKilocode,
19498
+ paths: paths.kilocode
19499
+ },
19500
+ antigravity: {
19501
+ install: installAntigravity,
19502
+ uninstall: uninstallAntigravity,
19503
+ paths: paths.antigravity
19504
+ },
19505
+ copilot: {
19506
+ install: installCopilot,
19507
+ uninstall: uninstallCopilot,
19508
+ paths: paths.copilot
19509
+ }
19510
+ };
19511
+ const defaultScopeFor = (agent) => AGENTS_PROJECT_ONLY.includes(agent) ? "project" : "global";
19512
+ const HOOK_MARKERS = [
19513
+ "__raigal",
19514
+ "raigal:begin",
19515
+ "@RAIGAL.md",
19516
+ "__aislop",
19517
+ "aislop:begin",
19518
+ "@AISLOP.md"
19519
+ ];
19520
+ const hasHook = (target) => {
19521
+ try {
19522
+ if (!fs.statSync(target).isFile()) return false;
19523
+ const base = path.basename(target).toLowerCase();
19524
+ if (base.startsWith("raigal") || base.startsWith("aislop")) return true;
19525
+ const content = fs.readFileSync(target, "utf8");
19526
+ return HOOK_MARKERS.some((marker) => content.includes(marker));
19527
+ } catch {
19528
+ return false;
19529
+ }
19530
+ };
19531
+ const detectionScopes = (agent) => AGENTS_SUPPORTING_BOTH_SCOPES.includes(agent) ? ["global", "project"] : [defaultScopeFor(agent)];
19532
+ const detectRaigalHooks = (opts) => ALL_AGENTS.filter((agent) => detectionScopes(agent).some((scope) => REGISTRY[agent].paths({
19533
+ home: opts.home,
19534
+ cwd: opts.cwd,
19535
+ scope
19536
+ }).some(hasHook)));
18201
19537
  //#endregion
18202
19538
  //#region src/output/terminal.ts
18203
19539
  const groupBy = (items, key) => {
@@ -18365,27 +19701,16 @@ const printEngineStatus = (result) => {
18365
19701
  }
18366
19702
  };
18367
19703
  //#endregion
18368
- //#region src/scoring/rule-severity.ts
18369
- /**
18370
- * Apply per-rule severity overrides from config: "off" drops the diagnostic,
18371
- * "error"/"warning" rewrite its severity before scoring and rendering.
18372
- */
18373
- const applyRuleSeverities = (diagnostics, overrides) => {
18374
- if (Object.keys(overrides).length === 0) return diagnostics;
18375
- const result = [];
18376
- for (const diagnostic of diagnostics) {
18377
- const override = overrides[diagnostic.rule];
18378
- if (!override) {
18379
- result.push(diagnostic);
18380
- continue;
18381
- }
18382
- if (override === "off") continue;
18383
- result.push(override === diagnostic.severity ? diagnostic : {
18384
- ...diagnostic,
18385
- severity: override
18386
- });
18387
- }
18388
- return result;
19704
+ //#region src/commands/scan-hook-nudge.ts
19705
+ const shouldShowHookNudge = (ctx) => ctx.isTty && !ctx.isCi && ctx.installedAgentCount === 0;
19706
+ const buildHookNudge = (ctx) => {
19707
+ if (!shouldShowHookNudge(ctx)) return null;
19708
+ return [
19709
+ "",
19710
+ "Next: install the per-edit hook so your coding agent fixes slop before it lands.",
19711
+ ctx.invocation === "raigal" ? " raigal hook install" : " npm install -g @methiu/raigal && raigal hook install",
19712
+ ""
19713
+ ].join("\n");
18389
19714
  };
18390
19715
  //#endregion
18391
19716
  //#region src/commands/scan-coverage.ts
@@ -18679,6 +20004,9 @@ const RULE_LABELS = {
18679
20004
  "security/shell-injection": "Possible shell injection",
18680
20005
  "security/unsafe-deserialization": "Unsafe deserialization",
18681
20006
  "security/unsafe-c-call": "Memory-unsafe C call",
20007
+ "security/prompt-injection": "Prompt injection (untrusted system prompt)",
20008
+ "security/unsandboxed-agent-tool": "Unsandboxed agent code execution tool",
20009
+ "security/invisible-unicode-tags": "Invisible Unicode tag character (stealth injection)",
18682
20010
  "dotnet/projects-skipped": "C# projects skipped by build-backed lint",
18683
20011
  "cppcheck/chunks-skipped": "C/C++ files skipped by cppcheck",
18684
20012
  "eslint/no-undef": "Undefined identifier",
@@ -18767,6 +20095,9 @@ const RULE_DESCRIPTIONS = {
18767
20095
  "security/shell-injection": "Shell command is built from unsanitized input.",
18768
20096
  "security/unsafe-deserialization": "Legacy .NET formatter can deserialize arbitrary types (RCE).",
18769
20097
  "security/unsafe-c-call": "Unbounded C string/exec function with buffer-overflow or shell risk.",
20098
+ "security/prompt-injection": "Untrusted dynamic variable interpolation into system or developer prompt.",
20099
+ "security/unsandboxed-agent-tool": "Agent executes code or commands on host without container or interactive gate.",
20100
+ "security/invisible-unicode-tags": "Invisible Unicode Tag block (U+E0000-U+E007F) used for prompt injection or ASCII smuggling.",
18770
20101
  "dotnet/projects-skipped": "Build-backed C# lint only ran on restored projects; the rest were skipped.",
18771
20102
  "cppcheck/chunks-skipped": "A cppcheck invocation timed out or failed on one or more chunks; their files were not analyzed.",
18772
20103
  "oxlint/*": "JavaScript/TypeScript lint finding from oxlint.",
@@ -18841,7 +20172,8 @@ const renderActionRows = (steps, t) => {
18841
20172
  const renderTextSteps = (steps, t, s) => {
18842
20173
  return steps.filter((step) => step.text).map((step) => {
18843
20174
  const glyph = step.emphasis === "primary" ? s.hint : s.bullet;
18844
- return ` ${style(t, step.emphasis === "primary" ? "accent" : "muted", glyph)} ${highlightAislop(step.text ?? "", t)}`;
20175
+ const tokenFor = step.emphasis === "primary" ? "accent" : "muted";
20176
+ return ` ${style(t, tokenFor, glyph)} ${highlightAislop(step.text ?? "", t)}`;
18845
20177
  });
18846
20178
  };
18847
20179
  const renderSummary = (input, deps = {}) => {
@@ -18898,10 +20230,14 @@ const renderSummary = (input, deps = {}) => {
18898
20230
  return lines.join("\n");
18899
20231
  };
18900
20232
  const renderStarCta = (deps = {}) => {
18901
- return `\n ${style(deps.theme ?? theme, "muted", `★ Found this useful? Consider purchasing this software at ${terminalLink("https://raigal.dev")}`)}\n`;
20233
+ const t = deps.theme ?? theme;
20234
+ const repository = terminalLink("https://raigal.dev");
20235
+ return `\n ${style(t, "muted", `★ Found this useful? Consider purchasing this software at ${repository}`)}\n`;
18902
20236
  };
18903
20237
  const renderTeamCta = (deps = {}) => {
18904
- return `\n ${style(deps.theme ?? theme, "muted", `→ Using raigal with a team? Get a 14-day team baseline at ${terminalLink("https://raigal.dev/contact?intent=team-baseline")}`)}\n`;
20238
+ const t = deps.theme ?? theme;
20239
+ const href = terminalLink("https://raigal.dev/contact?intent=team-baseline");
20240
+ return `\n ${style(t, "muted", `→ Using raigal with a team? Get a 14-day team baseline at ${href}`)}\n`;
18905
20241
  };
18906
20242
  const renderCleanRun = (input, deps = {}) => {
18907
20243
  const t = deps.theme ?? theme;
@@ -18960,7 +20296,7 @@ const buildScanNextSteps = (input) => {
18960
20296
  emphasis: "primary",
18961
20297
  label: "Provider",
18962
20298
  command: `${input.invocation} agent --provider codex`,
18963
- detail: "or use --provider claude/opencode"
20299
+ detail: "or use --provider claude/opencode/pi"
18964
20300
  }, {
18965
20301
  emphasis: "muted",
18966
20302
  label: "Preview",
@@ -19119,13 +20455,15 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
19119
20455
  const configDir = findConfigDir(resolvedDir);
19120
20456
  const rulesPath = configDir ? path.join(configDir, RULES_FILE) : void 0;
19121
20457
  const engineConfig = {
20458
+ overrides: config.overrides,
20459
+ rules: config.rules,
19122
20460
  quality: config.quality,
19123
20461
  security: config.security,
19124
20462
  lint: config.lint,
19125
20463
  architectureRulesPath: config.engines.architecture ? rulesPath : void 0,
19126
20464
  engines: config.engines
19127
20465
  };
19128
- const { results: unannotated, suppressedCount } = applySuppressions((await runEnginesWithProgress({
20466
+ const severityAdjusted = (await runEnginesWithProgress({
19129
20467
  rootDirectory: resolvedDir,
19130
20468
  languages: projectInfo.languages,
19131
20469
  frameworks: projectInfo.frameworks,
@@ -19140,8 +20478,9 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
19140
20478
  config: engineConfig
19141
20479
  }, config.engines, machineOutput)).map((result) => ({
19142
20480
  ...result,
19143
- diagnostics: applyRuleSeverities(result.diagnostics, config.rules)
19144
- })), resolvedDir);
20481
+ diagnostics: config.overrides.length ? result.diagnostics : applyRuleSeverities(result.diagnostics, config.rules)
20482
+ }));
20483
+ const { results: unannotated, suppressedCount } = applySuppressions(severityAdjusted, resolvedDir);
19145
20484
  if (suppressedCount > 0 && !machineOutput) log.muted(`Suppressed ${suppressedCount} finding(s) via raigal-ignore directives`);
19146
20485
  const changeMap = options.changes && !options.staged ? getChangedLineMap(resolvedDir, options.base) : null;
19147
20486
  const results = changeMap ? unannotated.map((result) => ({
@@ -19178,6 +20517,7 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
19178
20517
  engineIssues[r.engine] = r.diagnostics.length;
19179
20518
  engineTimings[r.engine] = Math.round(r.elapsed);
19180
20519
  }
20520
+ const enginesFailed = results.filter((r) => r.failed).map((r) => r.engine);
19181
20521
  const completion = {
19182
20522
  exitCode,
19183
20523
  score: scoreable ? scoreResult.score : null,
@@ -19187,7 +20527,8 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
19187
20527
  warningCount: allDiagnostics.filter((d) => d.severity === "warning").length,
19188
20528
  fixableCount: allDiagnostics.filter((d) => d.fixable).length,
19189
20529
  engineIssues,
19190
- engineTimings
20530
+ engineTimings,
20531
+ ...enginesFailed.length > 0 ? { properties: { engines_failed: enginesFailed } } : {}
19191
20532
  };
19192
20533
  recordScanToCloud({
19193
20534
  results,
@@ -19200,12 +20541,12 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
19200
20541
  scoreFileCount
19201
20542
  });
19202
20543
  if (options.sarif) {
19203
- const { buildSarifLog } = await import("./sarif-Do8E1xhZ.js");
20544
+ const { buildSarifLog } = await import("./sarif-CpuYM3aK.js");
19204
20545
  console.log(JSON.stringify(buildSarifLog(results), null, 2));
19205
20546
  return completion;
19206
20547
  }
19207
20548
  if (options.json) {
19208
- const { buildJsonOutput } = await import("./json-C2IwKr9o.js");
20549
+ const { buildJsonOutput } = await import("./json-BflWt4dX.js");
19209
20550
  const jsonOut = buildJsonOutput(results, scoreResult, scoreFileCount, elapsedMs, scanCoverage);
19210
20551
  console.log(JSON.stringify(jsonOut, null, 2));
19211
20552
  return completion;
@@ -19238,6 +20579,18 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
19238
20579
  printBrand: options.printBrand,
19239
20580
  showPilotInvitation
19240
20581
  }));
20582
+ if (options.command !== "ci" && options.printBrand !== false) {
20583
+ const nudge = buildHookNudge({
20584
+ installedAgentCount: detectRaigalHooks({
20585
+ home: os.homedir(),
20586
+ cwd: resolvedDir
20587
+ }).length,
20588
+ isTty: Boolean(process.stdout.isTTY),
20589
+ isCi: isCiEnv(),
20590
+ invocation: detectInvocation()
20591
+ });
20592
+ if (nudge) process.stdout.write(nudge);
20593
+ }
19241
20594
  return completion;
19242
20595
  };
19243
20596
  //#endregion
@@ -19347,7 +20700,8 @@ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
19347
20700
  brand: options.printBrand !== false
19348
20701
  }));
19349
20702
  const safe = Boolean(options.safe);
19350
- const scope = collectFixFileScope(resolvedDir, [...config.exclude, ...readAislopIgnorePatterns(resolvedDir)], config.include, options);
20703
+ const excludePatterns = [...config.exclude, ...readAislopIgnorePatterns(resolvedDir)];
20704
+ const scope = collectFixFileScope(resolvedDir, excludePatterns, config.include, options);
19351
20705
  const scopedProjectInfo = scope ? {
19352
20706
  ...projectInfo,
19353
20707
  languages: detectSourceLanguages([...scope.files, ...scope.testFiles])
@@ -19402,6 +20756,8 @@ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
19402
20756
  const configDir = findConfigDir(resolvedDir);
19403
20757
  const rulesPath = configDir ? path.join(configDir, RULES_FILE) : void 0;
19404
20758
  const engineConfig = {
20759
+ overrides: config.overrides,
20760
+ rules: config.rules,
19405
20761
  quality: config.quality,
19406
20762
  security: config.security,
19407
20763
  lint: config.lint,
@@ -19427,7 +20783,7 @@ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
19427
20783
  status: "done",
19428
20784
  label: "Verification complete"
19429
20785
  });
19430
- const scanResults = appendPostFixLintResult(verificationResults, collectPostFixLintDiagnostics(steps));
20786
+ const scanResults = appendPostFixLintResult(verificationResults, applyFileOverrides(collectPostFixLintDiagnostics(steps), context));
19431
20787
  const allDiagnostics = scanResults.flatMap((r) => r.diagnostics);
19432
20788
  const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, scope ? scope.scoreFileCount : projectInfo.sourceFileCount, config.scoring.smoothing, config.scoring.maxPerRule);
19433
20789
  const errors = allDiagnostics.filter((d) => d.severity === "error").length;
@@ -19489,6 +20845,13 @@ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
19489
20845
  };
19490
20846
  };
19491
20847
  //#endregion
20848
+ //#region src/ui/prompts.ts
20849
+ const runCancellable = async (fn) => {
20850
+ const value = await fn();
20851
+ if (isCancel(value)) return void 0;
20852
+ return value;
20853
+ };
20854
+ //#endregion
19492
20855
  //#region src/commands/init.ts
19493
20856
  const buildInitSuccessRender = (input) => {
19494
20857
  const deps = {
@@ -19558,13 +20921,13 @@ const writeGithubWorkflow = (rootDirectory, enabled) => {
19558
20921
  };
19559
20922
  };
19560
20923
  const promptForConfigChoices = async () => {
19561
- const enginesSelection = await multiselect({
20924
+ const enginesSelection = await runCancellable(() => multiselect({
19562
20925
  message: "Which engines should run?",
19563
20926
  options: ENGINE_CHOICES,
19564
20927
  initialValues: DEFAULT_ENGINE_SELECTION,
19565
20928
  required: false
19566
- });
19567
- if (isCancel(enginesSelection)) return null;
20929
+ }));
20930
+ if (enginesSelection === void 0 || isCancel(enginesSelection)) return null;
19568
20931
  const failBelowRaw = await text({
19569
20932
  message: "CI quality gate — fail the build when the score drops below (0-100)",
19570
20933
  initialValue: "70",
@@ -19695,16 +21058,18 @@ const initCommand = async (directory, options = {}) => {
19695
21058
  status: "done",
19696
21059
  label: `Wrote ${CONFIG_DIR}/${CONFIG_FILE}`
19697
21060
  }];
19698
- if (choices.engines.includes("architecture")) if (!fs.existsSync(rulesPath)) {
19699
- fs.writeFileSync(rulesPath, DEFAULT_RULES_YAML);
19700
- steps.push({
19701
- status: "done",
19702
- label: `Wrote ${CONFIG_DIR}/${RULES_FILE}`
21061
+ if (choices.engines.includes("architecture")) {
21062
+ if (!fs.existsSync(rulesPath)) {
21063
+ fs.writeFileSync(rulesPath, DEFAULT_RULES_YAML);
21064
+ steps.push({
21065
+ status: "done",
21066
+ label: `Wrote ${CONFIG_DIR}/${RULES_FILE}`
21067
+ });
21068
+ } else steps.push({
21069
+ status: "skipped",
21070
+ label: `${CONFIG_DIR}/${RULES_FILE} already exists`
19703
21071
  });
19704
- } else steps.push({
19705
- status: "skipped",
19706
- label: `${CONFIG_DIR}/${RULES_FILE} already exists`
19707
- });
21072
+ }
19708
21073
  const workflowResult = writeGithubWorkflow(resolvedDir, choices.writeGithubWorkflow);
19709
21074
  if (workflowResult.status === "written") steps.push({
19710
21075
  status: "done",
@@ -19898,8 +21263,10 @@ const runSearchPrompt = async (options) => {
19898
21263
  }
19899
21264
  if (key.name === "space" && options.mode === "multi") {
19900
21265
  const item = filtered[cursor];
19901
- if (item) if (selected.has(item.value)) selected.delete(item.value);
19902
- else selected.add(item.value);
21266
+ if (item) {
21267
+ if (selected.has(item.value)) selected.delete(item.value);
21268
+ else selected.add(item.value);
21269
+ }
19903
21270
  render();
19904
21271
  return;
19905
21272
  }
@@ -20095,7 +21462,8 @@ const BUILTIN_RULES = [
20095
21462
  "typescript/*",
20096
21463
  "dotnet/*",
20097
21464
  "cppcheck/*",
20098
- "clang-tidy/*"
21465
+ "clang-tidy/*",
21466
+ "expo-doctor/*"
20099
21467
  ]
20100
21468
  },
20101
21469
  {
@@ -20194,7 +21562,10 @@ const BUILTIN_RULES = [
20194
21562
  "security/shell-injection",
20195
21563
  "security/unsafe-deserialization",
20196
21564
  "security/unsafe-c-call",
20197
- "security/dependency-audit-skipped"
21565
+ "security/dependency-audit-skipped",
21566
+ "security/prompt-injection",
21567
+ "security/unsandboxed-agent-tool",
21568
+ "security/invisible-unicode-tags"
20198
21569
  ]
20199
21570
  }
20200
21571
  ];
@@ -20204,7 +21575,8 @@ const collectRuleEntries = (directory) => {
20204
21575
  for (const { engine, rules } of BUILTIN_RULES) for (const rule of rules) entries.push(toRuleEntry(engine, rule));
20205
21576
  const configDir = findConfigDir(resolvedDir);
20206
21577
  if (configDir) {
20207
- const archRules = loadArchitectureRules(path.join(configDir, RULES_FILE));
21578
+ const rulesPath = path.join(configDir, RULES_FILE);
21579
+ const archRules = loadArchitectureRules(rulesPath);
20208
21580
  for (const rule of archRules) entries.push({
20209
21581
  id: `arch/${rule.name}`,
20210
21582
  engine: "architecture",