@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/cli.js CHANGED
@@ -13,9 +13,8 @@ import { z } from "zod/v4";
13
13
  import crypto, { createHash, createPublicKey, randomUUID, verify } from "node:crypto";
14
14
  import YAML, { parse } from "yaml";
15
15
  import { confirm, isCancel, multiselect, select, text } from "@clack/prompts";
16
- import * as readline$1 from "node:readline";
17
- import readline from "node:readline";
18
- import micromatch from "micromatch";
16
+ import readline, * as readline$1 from "node:readline";
17
+ import picomatch from "picomatch";
19
18
  import ts from "typescript";
20
19
  import { parse as parse$1 } from "smol-toml";
21
20
  import { Writable } from "node:stream";
@@ -113,7 +112,8 @@ const commandExists = (bin) => {
113
112
  }
114
113
  }
115
114
  }
116
- const result = spawnSync(process.platform === "win32" ? "where.exe" : "which", [bin], {
115
+ const command = process.platform === "win32" ? "where.exe" : "which";
116
+ const result = spawnSync(command, [bin], {
117
117
  encoding: "utf-8",
118
118
  timeout: PROVIDER_PROBE_TIMEOUT_MS
119
119
  });
@@ -702,7 +702,7 @@ const highlightRaigal = (text, theme, baseToken) => {
702
702
  const tail = text.slice(lastIndex);
703
703
  return out + (baseToken ? style$1(theme, baseToken, tail) : tail);
704
704
  };
705
- const highlightAislop = highlightRaigal;
705
+ const highlightAislop = (...args) => highlightRaigal(...args);
706
706
  //#endregion
707
707
  //#region src/ui/width.ts
708
708
  const ANSI_RE$1 = new RegExp(`\\[[0-9;]*m`, "g");
@@ -711,7 +711,8 @@ const stringWidth = (s) => {
711
711
  const bare = stripAnsi$1(s);
712
712
  let total = 0;
713
713
  for (const ch of bare) {
714
- const w = wcwidth(ch.codePointAt(0) ?? 0);
714
+ const cp = ch.codePointAt(0) ?? 0;
715
+ const w = wcwidth(cp);
715
716
  total += w > 0 ? w : 1;
716
717
  }
717
718
  return total;
@@ -727,7 +728,8 @@ const truncate = (s, max, ellipsis = "…") => {
727
728
  let out = "";
728
729
  let w = 0;
729
730
  for (const ch of s) {
730
- const cw = wcwidth(ch.codePointAt(0) ?? 0);
731
+ const cp = ch.codePointAt(0) ?? 0;
732
+ const cw = wcwidth(cp);
731
733
  if (w + cw > limit) break;
732
734
  out += ch;
733
735
  w += cw;
@@ -737,7 +739,8 @@ const truncate = (s, max, ellipsis = "…") => {
737
739
  //#endregion
738
740
  //#region src/ui/display.ts
739
741
  const renderDisplaySection = (label, deps = {}) => {
740
- return style$1(deps.theme ?? theme, "section", label);
742
+ const t = deps.theme ?? theme;
743
+ return style$1(t, "section", label);
741
744
  };
742
745
  const renderDisplayRows = (rows, options = {}) => {
743
746
  if (rows.length === 0) return [];
@@ -898,7 +901,8 @@ const symbols = createSymbols({ plain: isPlain() });
898
901
  */
899
902
  const renderHintLine = (hint, deps = {}) => {
900
903
  const t = deps.theme ?? theme;
901
- return ` ${style$1(t, "accent", (deps.symbols ?? symbols).hint)} ${highlightAislop(maskSecrets(hint), t)}\n`;
904
+ const s = deps.symbols ?? symbols;
905
+ return ` ${style$1(t, "accent", s.hint)} ${highlightAislop(maskSecrets(hint), t)}\n`;
902
906
  };
903
907
  const createLogger = (deps = {}) => {
904
908
  const t = deps.theme ?? theme;
@@ -923,7 +927,7 @@ const createLogger = (deps = {}) => {
923
927
  const log = createLogger();
924
928
  //#endregion
925
929
  //#region src/version.ts
926
- const APP_VERSION = "0.2.0";
930
+ const APP_VERSION = "0.2.3";
927
931
  //#endregion
928
932
  //#region src/commands/agent-connect.ts
929
933
  const commandText = (status) => [status.provider.loginCommand.command, ...status.provider.loginCommand.args].join(" ");
@@ -1410,7 +1414,9 @@ const verifyEntitlementJwt = (jwt, options = {}) => {
1410
1414
  if (!header.kid) throw new JwtVerificationError("JWT header missing 'kid'", "unknown_kid");
1411
1415
  const publicKey = getPublicKey(header.kid);
1412
1416
  if (!publicKey) throw new JwtVerificationError(`Unknown or untrusted signing key id (kid: ${header.kid})`, "unknown_kid");
1413
- if (!verify(null, Buffer.from(`${headerB64}.${payloadB64}`, "utf-8"), publicKey, Buffer.from(signatureB64, "base64url"))) throw new JwtVerificationError("Invalid JWT cryptographic signature", "invalid_signature");
1417
+ const signedData = Buffer.from(`${headerB64}.${payloadB64}`, "utf-8");
1418
+ const signature = Buffer.from(signatureB64, "base64url");
1419
+ if (!verify(null, signedData, publicKey, signature)) throw new JwtVerificationError("Invalid JWT cryptographic signature", "invalid_signature");
1414
1420
  let rawPayload;
1415
1421
  try {
1416
1422
  rawPayload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf-8"));
@@ -1422,7 +1428,7 @@ const verifyEntitlementJwt = (jwt, options = {}) => {
1422
1428
  const claims = parsed.data;
1423
1429
  const expectedIssuer = process.env.RAIGAL_ISSUER?.trim() || "https://app.raigal.dev";
1424
1430
  if (claims.iss !== expectedIssuer) throw new JwtVerificationError(`Untrusted issuer: ${claims.iss}`, "invalid_issuer");
1425
- 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");
1431
+ 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");
1426
1432
  const now = options.nowSeconds ?? Math.floor(Date.now() / 1e3);
1427
1433
  const grace = options.allowGraceSeconds ?? 0;
1428
1434
  if (now > claims.exp + grace) {
@@ -1456,7 +1462,7 @@ const fetchGitHubOidcToken = async () => {
1456
1462
  };
1457
1463
  //#endregion
1458
1464
  //#region src/cloud/entitlement.ts
1459
- const GRACE_PERIOD_SECONDS = 4320 * 60;
1465
+ const GRACE_PERIOD_SECONDS = 259200;
1460
1466
  var MissingCredentialError = class extends Error {
1461
1467
  constructor(message = "No organization credential found") {
1462
1468
  super(message);
@@ -1536,6 +1542,10 @@ const handleEntitlementFetchError = (err, cached, cachedClaims, now) => {
1536
1542
  nowSeconds: now,
1537
1543
  allowGraceSeconds: GRACE_PERIOD_SECONDS
1538
1544
  });
1545
+ if (graceClaims.plan === "trial" && graceClaims.trial_ends_at && now > Math.floor(new Date(graceClaims.trial_ends_at).getTime() / 1e3)) {
1546
+ clearCachedEntitlement();
1547
+ throw new LicenceDeniedError("Organization trial has expired", "trial_expired");
1548
+ }
1539
1549
  return {
1540
1550
  jwt: cached.entitlement,
1541
1551
  claims: graceClaims,
@@ -1543,6 +1553,7 @@ const handleEntitlementFetchError = (err, cached, cachedClaims, now) => {
1543
1553
  grace: now > graceClaims.exp
1544
1554
  };
1545
1555
  } catch (jwtErr) {
1556
+ if (jwtErr instanceof LicenceDeniedError) throw jwtErr;
1546
1557
  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.`);
1547
1558
  }
1548
1559
  throw new LicenceUnverifiableError(`Raigal license could not be verified online (${err instanceof Error ? err.message : String(err)}). Check network connectivity or Raigal Cloud status.`);
@@ -1557,7 +1568,8 @@ const getEntitlement = async () => {
1557
1568
  } catch {
1558
1569
  cachedClaims = null;
1559
1570
  }
1560
- if (cached && cachedClaims && now < cached.refresh_after) return {
1571
+ const isTrialExpired = cachedClaims?.plan === "trial" && cachedClaims.trial_ends_at && now > Math.floor(new Date(cachedClaims.trial_ends_at).getTime() / 1e3);
1572
+ if (cached && cachedClaims && !isTrialExpired && now < cached.refresh_after) return {
1561
1573
  jwt: cached.entitlement,
1562
1574
  claims: cachedClaims,
1563
1575
  fromCache: true
@@ -1566,7 +1578,7 @@ const getEntitlement = async () => {
1566
1578
  if (!activeToken) {
1567
1579
  oidcToken = await fetchGitHubOidcToken();
1568
1580
  if (!oidcToken) {
1569
- if (cached && cachedClaims) return {
1581
+ if (cached && cachedClaims && !isTrialExpired) return {
1570
1582
  jwt: cached.entitlement,
1571
1583
  claims: cachedClaims,
1572
1584
  fromCache: true
@@ -1575,7 +1587,8 @@ const getEntitlement = async () => {
1575
1587
  }
1576
1588
  } else oidcToken = await fetchGitHubOidcToken();
1577
1589
  try {
1578
- const response = await fetchEntitlementFromApi(activeToken?.token ?? "", oidcToken);
1590
+ const tokenToPass = activeToken?.token ?? "";
1591
+ const response = await fetchEntitlementFromApi(tokenToPass, oidcToken);
1579
1592
  const claims = verifyEntitlementJwt(response.entitlement, { nowSeconds: now });
1580
1593
  saveCachedEntitlement(response.entitlement, response.refresh_after);
1581
1594
  return {
@@ -1589,7 +1602,7 @@ const getEntitlement = async () => {
1589
1602
  };
1590
1603
  //#endregion
1591
1604
  //#region src/cloud/owner.ts
1592
- const GITHUB_REMOTE_RE$1 = /^(?:(?:git@|ssh:\/\/(?:git@)?)github\.com(?::(?:\d+\/)?|[:/])|https:\/\/(?:[^@]+@)?github\.com\/)([^/]+)\/([^/.\s]+?)(?:\.git)?\s*$/;
1605
+ const GITHUB_REMOTE_RE$1 = /^(?:(?:git@|ssh:\/\/(?:git@)?)github\.com(?::(?:\d+\/)?|[:/])|https:\/\/(?:[^@]+@)?github\.com\/)([^/]+)\/([^/\s]+?)(?:\.git)?\s*$/;
1593
1606
  const parseGitRemote = (rawUrl) => {
1594
1607
  if (!rawUrl) return {
1595
1608
  owner: null,
@@ -1727,6 +1740,23 @@ const requireEntitlement = async (options = {}) => {
1727
1740
  try {
1728
1741
  const entitlement = await getEntitlement();
1729
1742
  const tokenKind = getActiveToken()?.kind ?? "session";
1743
+ if (entitlement.claims.plan === "trial" && entitlement.claims.trial_ends_at) {
1744
+ const trialEnd = new Date(entitlement.claims.trial_ends_at).getTime();
1745
+ if (Date.now() > trialEnd) {
1746
+ const message = "Raigal license denied: Organization trial has expired";
1747
+ if (mode === "throw") throw new LicenceDeniedError(message, "trial_expired");
1748
+ if (mode === "fatal") {
1749
+ process.stderr.write(`${message}\n`);
1750
+ process.exit(30);
1751
+ }
1752
+ process.stderr.write(`[raigal] ${message}\n`);
1753
+ return {
1754
+ ok: false,
1755
+ exitCode: 30,
1756
+ message
1757
+ };
1758
+ }
1759
+ }
1730
1760
  const detectedOwner = detectLocalRepoOwner(directory);
1731
1761
  if (options.repoOwner) detectedOwner.owner = options.repoOwner.toLowerCase();
1732
1762
  const ownerCheck = checkOwnerAgainstAllowlist(detectedOwner, entitlement.claims.allowed_owners, tokenKind);
@@ -2026,415 +2056,154 @@ const renderBackgroundLaunch = (launch) => {
2026
2056
  ].join("\n")}`);
2027
2057
  };
2028
2058
  //#endregion
2029
- //#region src/agents/prompt.ts
2030
- const CONTEXT_LINES$1 = 3;
2031
- const MAX_DIAGNOSTICS_PER_FILE$1 = 6;
2032
- const MAX_FILES$1 = 12;
2033
- const priority = (diagnostic) => {
2034
- if (diagnostic.severity === "error") return 0;
2035
- if (diagnostic.severity === "warning") return 1;
2036
- return 2;
2037
- };
2038
- const selectAgentFindings = (diagnostics, limit) => diagnostics.filter((diagnostic) => diagnostic.severity !== "info").sort((a, b) => {
2039
- const severityDelta = priority(a) - priority(b);
2040
- if (severityDelta !== 0) return severityDelta;
2041
- if (a.fixable !== b.fixable) return a.fixable ? 1 : -1;
2042
- return a.filePath.localeCompare(b.filePath);
2043
- }).slice(0, Math.max(1, limit));
2044
- const groupByFile$1 = (diagnostics) => {
2045
- const groups = /* @__PURE__ */ new Map();
2046
- for (const diagnostic of diagnostics) {
2047
- const list = groups.get(diagnostic.filePath) ?? [];
2048
- list.push(diagnostic);
2049
- groups.set(diagnostic.filePath, list);
2050
- }
2051
- return [...groups.entries()].map(([filePath, items]) => ({
2052
- filePath,
2053
- diagnostics: items
2054
- })).slice(0, MAX_FILES$1);
2059
+ //#region src/agents/session-activity.ts
2060
+ const createUsageTotals = () => ({
2061
+ inputTokens: 0,
2062
+ cachedInputTokens: 0,
2063
+ outputTokens: 0,
2064
+ totalTokens: 0
2065
+ });
2066
+ const createSessionStats = () => ({
2067
+ providerPasses: 0,
2068
+ toolCalls: 0,
2069
+ outputEvents: 0
2070
+ });
2071
+ const mergeProviderUsage = (totals, usage) => {
2072
+ const inputTokens = Math.max(totals.inputTokens, usage.inputTokens ?? 0);
2073
+ const cachedInputTokens = Math.max(totals.cachedInputTokens, usage.cachedInputTokens ?? 0);
2074
+ const outputTokens = Math.max(totals.outputTokens, usage.outputTokens ?? 0);
2075
+ const totalTokens = Math.max(totals.totalTokens, usage.totalTokens ?? inputTokens + cachedInputTokens + outputTokens);
2076
+ const costUsd = usage.costUsd === void 0 ? totals.costUsd : Math.max(totals.costUsd ?? 0, usage.costUsd);
2077
+ return {
2078
+ inputTokens,
2079
+ cachedInputTokens,
2080
+ outputTokens,
2081
+ totalTokens,
2082
+ ...costUsd !== void 0 ? { costUsd } : {}
2083
+ };
2055
2084
  };
2056
- const snippetFor = (rootDirectory, diagnostic) => {
2057
- if (diagnostic.line <= 0 || diagnostic.redactSource || isSecretClassRule(diagnostic.rule)) return null;
2058
- const absolutePath = path.resolve(rootDirectory, diagnostic.filePath);
2059
- let content;
2060
- try {
2061
- content = fs.readFileSync(absolutePath, "utf-8");
2062
- } catch {
2063
- return null;
2064
- }
2065
- const lines = content.split("\n");
2066
- const start = Math.max(0, diagnostic.line - 1 - CONTEXT_LINES$1);
2067
- const end = Math.min(lines.length, diagnostic.line + CONTEXT_LINES$1);
2068
- const out = [];
2069
- for (let index = start; index < end; index += 1) {
2070
- const lineNumber = index + 1;
2071
- const marker = lineNumber === diagnostic.line ? ">" : " ";
2072
- out.push(`${marker} ${String(lineNumber).padStart(4)} | ${lines[index]}`);
2073
- }
2074
- return out.join("\n");
2085
+ const abbreviateTokens = (n) => {
2086
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
2087
+ if (n >= 1e3) return `${Math.round(n / 1e3)}k`;
2088
+ return String(n);
2075
2089
  };
2076
- const buildRepairPrompt = (input) => {
2077
- const groups = groupByFile$1(input.findings);
2078
- const lines = [
2079
- "You are repairing AI slop findings in a local git worktree.",
2080
- "",
2081
- `Current raigal score: ${input.score == null ? "not scored" : `${input.score}/100`}`,
2082
- `Target score: ${input.targetScore}/100`,
2083
- `Turn budget: ${input.maxTurns}`,
2084
- "",
2085
- "Hard constraints:",
2086
- "- Make the smallest code changes that improve the findings below.",
2087
- "- Do not change public APIs, exports, database schemas, or test expectations unless a finding directly requires it.",
2088
- "- Do not delete tests.",
2089
- "- Do not add dependencies unless the existing project already clearly expects them.",
2090
- "- Never print, copy, commit, or echo secret values or credentials. Replace all hardcoded credentials and secrets with environment-variable references (e.g. process.env.KEY).",
2091
- "- If a finding looks like a false positive, leave the code alone and mention it in your final summary.",
2092
- "- Run the relevant local verification command if it is obvious and cheap.",
2093
- "",
2094
- "raigal findings to repair:",
2095
- ""
2090
+ const formatUsageTotals = (usage) => {
2091
+ if (usage.totalTokens === 0 && usage.costUsd === void 0) return "unknown";
2092
+ const tokens = [
2093
+ `${abbreviateTokens(usage.totalTokens)} total`,
2094
+ `${abbreviateTokens(usage.inputTokens)} in`,
2095
+ `${abbreviateTokens(usage.outputTokens)} out`
2096
2096
  ];
2097
- for (const group of groups) {
2098
- lines.push(`## ${group.filePath}`);
2099
- for (const diagnostic of group.diagnostics.slice(0, MAX_DIAGNOSTICS_PER_FILE$1)) {
2100
- const location = diagnostic.line > 0 ? `:${diagnostic.line}` : "";
2101
- lines.push(`- ${diagnostic.severity.toUpperCase()} ${diagnostic.rule}${location}: ${diagnostic.message}`);
2102
- if (diagnostic.help) lines.push(` Help: ${diagnostic.help}`);
2103
- const snippet = snippetFor(input.rootDirectory, diagnostic);
2104
- if (snippet) {
2105
- lines.push("```");
2106
- lines.push(snippet);
2107
- lines.push("```");
2108
- }
2097
+ if (usage.cachedInputTokens > 0) tokens.push(`${abbreviateTokens(usage.cachedInputTokens)} cached`);
2098
+ if (usage.costUsd !== void 0) tokens.push(`$${usage.costUsd.toFixed(2)}`);
2099
+ return tokens.join(" / ");
2100
+ };
2101
+ const formatToolCalls = (count) => `${count.toLocaleString()} tool call${count === 1 ? "" : "s"}`;
2102
+ const isProviderToolLine = (line) => Boolean(line?.startsWith("exec: ") || line?.startsWith("tool: "));
2103
+ const formatDiffStat = (file) => {
2104
+ if (file.binary) return "binary";
2105
+ if (typeof file.additions === "number" || typeof file.deletions === "number") return `+${file.additions ?? 0} -${file.deletions ?? 0}`;
2106
+ return "changed";
2107
+ };
2108
+ const createChangedFileTracker = (input) => {
2109
+ const files = /* @__PURE__ */ new Map();
2110
+ let timer;
2111
+ let pending = false;
2112
+ const noteFile = (filePath, source, stat) => {
2113
+ const absolute = path.join(input.cwd, filePath);
2114
+ const updatedAt = ((fs.existsSync(absolute) ? fs.statSync(absolute) : null)?.mtime ?? /* @__PURE__ */ new Date()).toISOString();
2115
+ const prior = files.get(filePath);
2116
+ const additions = stat?.additions ?? prior?.additions;
2117
+ const deletions = stat?.deletions ?? prior?.deletions;
2118
+ const binary = stat?.binary ?? prior?.binary;
2119
+ if (prior?.updatedAt === updatedAt && prior.source === source && prior.additions === additions && prior.deletions === deletions && prior.binary === binary) return;
2120
+ const next = {
2121
+ filePath,
2122
+ updatedAt,
2123
+ source,
2124
+ ...additions !== void 0 ? { additions } : {},
2125
+ ...deletions !== void 0 ? { deletions } : {},
2126
+ ...binary !== void 0 ? { binary } : {}
2127
+ };
2128
+ files.set(filePath, next);
2129
+ input.session?.append("file.changed", next);
2130
+ input.onChange([...files.values()]);
2131
+ };
2132
+ const refresh = async (source = "git diff") => {
2133
+ if (pending) return [...files.values()];
2134
+ pending = true;
2135
+ try {
2136
+ const stats = await diffNumstat(input.cwd);
2137
+ for (const filePath of await diffNameOnly(input.cwd)) noteFile(filePath, source, stats.get(filePath));
2138
+ return [...files.values()];
2139
+ } finally {
2140
+ pending = false;
2109
2141
  }
2110
- lines.push("");
2111
- }
2112
- if (input.findings.length === 0) lines.push("No findings were supplied. Inspect the codebase for obvious AI slop only if needed.");
2113
- lines.push("After editing, stop and summarize what changed and what you intentionally skipped.");
2114
- return maskSecrets(lines.join("\n"));
2142
+ };
2143
+ const start = () => {
2144
+ if (timer) return;
2145
+ timer = setInterval(() => {
2146
+ refresh("git diff");
2147
+ }, 1e3);
2148
+ timer.unref();
2149
+ };
2150
+ const stop = async () => {
2151
+ if (timer) {
2152
+ clearInterval(timer);
2153
+ timer = void 0;
2154
+ }
2155
+ return refresh("git diff");
2156
+ };
2157
+ return {
2158
+ noteFile,
2159
+ refresh,
2160
+ start,
2161
+ stop,
2162
+ files: () => [...files.values()]
2163
+ };
2115
2164
  };
2116
2165
  //#endregion
2117
- //#region src/agents/publish.ts
2118
- const defaultBranchSuffix = () => (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\..+$/, "").replace("T", "-");
2119
- const normalizeBranchName = (raw) => {
2120
- const cleaned = raw.trim().replace(/[^A-Za-z0-9._/-]+/g, "-").replace(/\/+/g, "/").replace(/^-+|-+$/g, "").replace(/^\/+|\/+$/g, "");
2121
- return cleaned.length > 0 ? cleaned.slice(0, 120) : `raigal/agent-${defaultBranchSuffix()}`;
2122
- };
2123
- const defaultBranchName = () => `raigal/agent-${defaultBranchSuffix()}`;
2124
- const runGit = async (cwd, args) => {
2125
- const result = await runSubprocess("git", args, {
2126
- cwd,
2127
- timeout: 9e4
2128
- });
2129
- if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `git ${args.join(" ")} failed`);
2130
- return result.stdout;
2166
+ //#region src/agents/session-state.ts
2167
+ const ACTIVITY_CAP = 200;
2168
+ const EDITS_CAP = 8;
2169
+ const buildInitialState = (init) => ({
2170
+ model: null,
2171
+ scoreStart: null,
2172
+ score: null,
2173
+ findingsRemaining: null,
2174
+ filesChanged: /* @__PURE__ */ new Set(),
2175
+ filesEdited: /* @__PURE__ */ new Set(),
2176
+ passes: 0,
2177
+ toolCalls: 0,
2178
+ tokens: {
2179
+ in: 0,
2180
+ out: 0,
2181
+ cached: 0,
2182
+ total: 0
2183
+ },
2184
+ startedAt: Date.now(),
2185
+ worktree: null,
2186
+ branch: null,
2187
+ activity: [],
2188
+ recentEdits: [],
2189
+ steps: [],
2190
+ files: [],
2191
+ actions: [],
2192
+ usage: null,
2193
+ estimatedTokens: 0,
2194
+ phase: "starting",
2195
+ pendingDecision: null,
2196
+ summary: null,
2197
+ ...init
2198
+ });
2199
+ const activeStepIndex = (steps) => {
2200
+ for (let i = steps.length - 1; i >= 0; i -= 1) if (steps[i].status === "running") return i;
2201
+ return -1;
2131
2202
  };
2132
- const currentBranch = async (cwd) => {
2133
- const result = await runSubprocess("git", ["branch", "--show-current"], { cwd });
2134
- if (result.exitCode !== 0) return null;
2135
- return result.stdout.trim() || null;
2136
- };
2137
- const resolveBaseBranch = async (cwd, fallback) => {
2138
- if (fallback && fallback !== "HEAD") return fallback;
2139
- const cached = await runSubprocess("git", [
2140
- "symbolic-ref",
2141
- "refs/remotes/origin/HEAD",
2142
- "--short"
2143
- ], { cwd });
2144
- if (cached.exitCode === 0 && cached.stdout.trim()) return cached.stdout.trim().replace(/^origin\//, "");
2145
- const remote = await runSubprocess("git", [
2146
- "remote",
2147
- "show",
2148
- "origin"
2149
- ], {
2150
- cwd,
2151
- timeout: 3e4
2152
- });
2153
- if (remote.exitCode === 0) {
2154
- const branch = remote.stdout.split("\n").find((entry) => entry.trim().startsWith("HEAD branch:"))?.split(":").slice(1).join(":").trim();
2155
- if (branch) return branch;
2156
- }
2157
- return "main";
2158
- };
2159
- const ensureBranch = async (cwd, branch) => {
2160
- if (await currentBranch(cwd) === branch) return;
2161
- await runGit(cwd, [
2162
- "switch",
2163
- "-c",
2164
- branch
2165
- ]);
2166
- };
2167
- const ensureGh = async () => {
2168
- if ((await runSubprocess("gh", ["--version"], { timeout: 1e4 })).exitCode !== 0) throw new Error("Creating a PR requires the GitHub CLI. Install and authenticate `gh`, then retry.");
2169
- };
2170
- const buildPrBody = (input) => {
2171
- const delta = input.beforeScore != null && input.afterScore != null ? input.afterScore - input.beforeScore : null;
2172
- const deltaStr = delta != null ? ` (${delta >= 0 ? `+${delta}` : delta})` : "";
2173
- const scoreStr = `${input.beforeScore ?? "not scored"} -> ${input.afterScore ?? "not scored"}${deltaStr}`;
2174
- let diffStatLine = `- Files changed: ${input.changedFiles.length}`;
2175
- if (typeof input.diffSummary === "string" && input.diffSummary.trim()) diffStatLine += ` (${input.diffSummary.trim()})`;
2176
- else if (input.diffSummary && typeof input.diffSummary === "object") {
2177
- const adds = input.diffSummary.additions ?? 0;
2178
- const dels = input.diffSummary.deletions ?? 0;
2179
- diffStatLine += ` (+${adds} -${dels})`;
2180
- }
2181
- const fixedFindingsSection = input.fixedFindings && input.fixedFindings.length > 0 ? [
2182
- "### Fixed findings",
2183
- "",
2184
- "| Rule | File | Message |",
2185
- "|---|---|---|",
2186
- ...input.fixedFindings.slice(0, 10).map((f) => {
2187
- const loc = f.line ? `${f.path}:${f.line}` : f.path;
2188
- const msg = (f.message || "").replace(/\|/g, "\\|");
2189
- return `| \`${f.ruleId}\` | \`${loc}\` | ${msg} |`;
2190
- }),
2191
- input.fixedFindings.length > 10 ? `\n*...and ${input.fixedFindings.length - 10} more findings fixed.*` : "",
2192
- ""
2193
- ] : [];
2194
- return [
2195
- "This PR was generated locally by `raigal agent`.",
2196
- "",
2197
- "## Summary",
2198
- "",
2199
- `- Provider: ${input.providerId}`,
2200
- `- Score: ${scoreStr}`,
2201
- diffStatLine,
2202
- "",
2203
- ...fixedFindingsSection,
2204
- "## Changed files",
2205
- "",
2206
- ...input.changedFiles.map((file) => `- ${file}`),
2207
- "",
2208
- "## Verification",
2209
- "",
2210
- "- `raigal scan --json`",
2211
- "",
2212
- "Review before merging. The deterministic scan is the gate; the provider only proposed the patch."
2213
- ].filter(Boolean).join("\n");
2214
- };
2215
- const publishAgentDiff = async (input) => {
2216
- if (!input.options.commit && !input.options.pr) return null;
2217
- if (input.changedFiles.length === 0) return null;
2218
- const branch = normalizeBranchName(input.options.branch ?? defaultBranchName());
2219
- await ensureBranch(input.cwd, branch);
2220
- await runGit(input.cwd, ["add", "-A"]);
2221
- if ((await runSubprocess("git", [
2222
- "diff",
2223
- "--cached",
2224
- "--quiet"
2225
- ], { cwd: input.cwd })).exitCode === 0) return null;
2226
- await runGit(input.cwd, [
2227
- "commit",
2228
- "-m",
2229
- input.options.commitMessage,
2230
- "-m",
2231
- "Generated by raigal agent."
2232
- ]);
2233
- const commitSha = (await runGit(input.cwd, [
2234
- "rev-parse",
2235
- "--short",
2236
- "HEAD"
2237
- ])).trim();
2238
- if (!input.options.pr) return {
2239
- branch,
2240
- commitSha,
2241
- pushed: false,
2242
- prUrl: null
2243
- };
2244
- await ensureGh();
2245
- const base = input.options.base ?? await resolveBaseBranch(input.cwd, input.originalBranch);
2246
- await runGit(input.cwd, [
2247
- "push",
2248
- "-u",
2249
- "origin",
2250
- branch
2251
- ]);
2252
- let diffSummary = input.diffSummary;
2253
- if (!diffSummary) {
2254
- const stat = await runSubprocess("git", [
2255
- "diff",
2256
- "--shortstat",
2257
- "HEAD~1"
2258
- ], { cwd: input.cwd });
2259
- if (stat.exitCode === 0 && stat.stdout.trim()) diffSummary = stat.stdout.trim();
2260
- }
2261
- const args = [
2262
- "pr",
2263
- "create",
2264
- "--base",
2265
- base,
2266
- "--head",
2267
- branch,
2268
- "--title",
2269
- input.options.prTitle ?? input.options.commitMessage,
2270
- "--body",
2271
- buildPrBody({
2272
- ...input,
2273
- diffSummary
2274
- })
2275
- ];
2276
- if (!input.options.ready) args.push("--draft");
2277
- const pr = await runSubprocess("gh", args, {
2278
- cwd: input.cwd,
2279
- timeout: 9e4
2280
- });
2281
- if (pr.exitCode !== 0) throw new Error(pr.stderr || pr.stdout || "Failed to create pull request with gh.");
2282
- return {
2283
- branch,
2284
- commitSha,
2285
- pushed: true,
2286
- prUrl: pr.stdout.trim() || null
2287
- };
2288
- };
2289
- //#endregion
2290
- //#region src/agents/session-activity.ts
2291
- const createUsageTotals = () => ({
2292
- inputTokens: 0,
2293
- cachedInputTokens: 0,
2294
- outputTokens: 0,
2295
- totalTokens: 0
2296
- });
2297
- const createSessionStats = () => ({
2298
- providerPasses: 0,
2299
- toolCalls: 0,
2300
- outputEvents: 0
2301
- });
2302
- const mergeProviderUsage = (totals, usage) => {
2303
- const inputTokens = Math.max(totals.inputTokens, usage.inputTokens ?? 0);
2304
- const cachedInputTokens = Math.max(totals.cachedInputTokens, usage.cachedInputTokens ?? 0);
2305
- const outputTokens = Math.max(totals.outputTokens, usage.outputTokens ?? 0);
2306
- const totalTokens = Math.max(totals.totalTokens, usage.totalTokens ?? inputTokens + cachedInputTokens + outputTokens);
2307
- const costUsd = usage.costUsd === void 0 ? totals.costUsd : Math.max(totals.costUsd ?? 0, usage.costUsd);
2308
- return {
2309
- inputTokens,
2310
- cachedInputTokens,
2311
- outputTokens,
2312
- totalTokens,
2313
- ...costUsd !== void 0 ? { costUsd } : {}
2314
- };
2315
- };
2316
- const abbreviateTokens = (n) => {
2317
- if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
2318
- if (n >= 1e3) return `${Math.round(n / 1e3)}k`;
2319
- return String(n);
2320
- };
2321
- const formatUsageTotals = (usage) => {
2322
- if (usage.totalTokens === 0 && usage.costUsd === void 0) return "unknown";
2323
- const tokens = [
2324
- `${abbreviateTokens(usage.totalTokens)} total`,
2325
- `${abbreviateTokens(usage.inputTokens)} in`,
2326
- `${abbreviateTokens(usage.outputTokens)} out`
2327
- ];
2328
- if (usage.cachedInputTokens > 0) tokens.push(`${abbreviateTokens(usage.cachedInputTokens)} cached`);
2329
- if (usage.costUsd !== void 0) tokens.push(`$${usage.costUsd.toFixed(2)}`);
2330
- return tokens.join(" / ");
2331
- };
2332
- const formatToolCalls = (count) => `${count.toLocaleString()} tool call${count === 1 ? "" : "s"}`;
2333
- const isProviderToolLine = (line) => Boolean(line?.startsWith("exec: ") || line?.startsWith("tool: "));
2334
- const formatDiffStat = (file) => {
2335
- if (file.binary) return "binary";
2336
- if (typeof file.additions === "number" || typeof file.deletions === "number") return `+${file.additions ?? 0} -${file.deletions ?? 0}`;
2337
- return "changed";
2338
- };
2339
- const createChangedFileTracker = (input) => {
2340
- const files = /* @__PURE__ */ new Map();
2341
- let timer;
2342
- let pending = false;
2343
- const noteFile = (filePath, source, stat) => {
2344
- const absolute = path.join(input.cwd, filePath);
2345
- const updatedAt = ((fs.existsSync(absolute) ? fs.statSync(absolute) : null)?.mtime ?? /* @__PURE__ */ new Date()).toISOString();
2346
- const prior = files.get(filePath);
2347
- const additions = stat?.additions ?? prior?.additions;
2348
- const deletions = stat?.deletions ?? prior?.deletions;
2349
- const binary = stat?.binary ?? prior?.binary;
2350
- if (prior?.updatedAt === updatedAt && prior.source === source && prior.additions === additions && prior.deletions === deletions && prior.binary === binary) return;
2351
- const next = {
2352
- filePath,
2353
- updatedAt,
2354
- source,
2355
- ...additions !== void 0 ? { additions } : {},
2356
- ...deletions !== void 0 ? { deletions } : {},
2357
- ...binary !== void 0 ? { binary } : {}
2358
- };
2359
- files.set(filePath, next);
2360
- input.session?.append("file.changed", next);
2361
- input.onChange([...files.values()]);
2362
- };
2363
- const refresh = async (source = "git diff") => {
2364
- if (pending) return [...files.values()];
2365
- pending = true;
2366
- try {
2367
- const stats = await diffNumstat(input.cwd);
2368
- for (const filePath of await diffNameOnly(input.cwd)) noteFile(filePath, source, stats.get(filePath));
2369
- return [...files.values()];
2370
- } finally {
2371
- pending = false;
2372
- }
2373
- };
2374
- const start = () => {
2375
- if (timer) return;
2376
- timer = setInterval(() => {
2377
- refresh("git diff");
2378
- }, 1e3);
2379
- timer.unref();
2380
- };
2381
- const stop = async () => {
2382
- if (timer) {
2383
- clearInterval(timer);
2384
- timer = void 0;
2385
- }
2386
- return refresh("git diff");
2387
- };
2388
- return {
2389
- noteFile,
2390
- refresh,
2391
- start,
2392
- stop,
2393
- files: () => [...files.values()]
2394
- };
2395
- };
2396
- //#endregion
2397
- //#region src/agents/session-state.ts
2398
- const ACTIVITY_CAP = 200;
2399
- const EDITS_CAP = 8;
2400
- const buildInitialState = (init) => ({
2401
- model: null,
2402
- scoreStart: null,
2403
- score: null,
2404
- findingsRemaining: null,
2405
- filesChanged: /* @__PURE__ */ new Set(),
2406
- filesEdited: /* @__PURE__ */ new Set(),
2407
- passes: 0,
2408
- toolCalls: 0,
2409
- tokens: {
2410
- in: 0,
2411
- out: 0,
2412
- cached: 0,
2413
- total: 0
2414
- },
2415
- startedAt: Date.now(),
2416
- worktree: null,
2417
- branch: null,
2418
- activity: [],
2419
- recentEdits: [],
2420
- steps: [],
2421
- files: [],
2422
- actions: [],
2423
- usage: null,
2424
- estimatedTokens: 0,
2425
- phase: "starting",
2426
- pendingDecision: null,
2427
- summary: null,
2428
- ...init
2429
- });
2430
- const activeStepIndex = (steps) => {
2431
- for (let i = steps.length - 1; i >= 0; i -= 1) if (steps[i].status === "running") return i;
2432
- return -1;
2433
- };
2434
- const createEmitter = (subscribers) => {
2435
- return () => {
2436
- for (const fn of subscribers) fn();
2437
- };
2203
+ const createEmitter = (subscribers) => {
2204
+ return () => {
2205
+ for (const fn of subscribers) fn();
2206
+ };
2438
2207
  };
2439
2208
  const createCoreMethods = ({ state, subscribers, emit }) => ({
2440
2209
  getState: () => state,
@@ -2614,7 +2383,7 @@ var AgentTui = class {
2614
2383
  }
2615
2384
  ensureMounted() {
2616
2385
  if (!this.tty || this.handle) return;
2617
- this.handle = import("./mount-DTc8WaIt.js").then((m) => m.mountAgentTui(this.store));
2386
+ this.handle = import("./mount-CHGc3gB6.js").then((m) => m.mountAgentTui(this.store));
2618
2387
  }
2619
2388
  setActions(actions) {
2620
2389
  this.store.update({ actions: actions.filter((action) => action.trim().length > 0) });
@@ -2681,16 +2450,249 @@ var AgentTui = class {
2681
2450
  await this.close();
2682
2451
  if (!this.tty) this.write(` ${opts.footer}\n`);
2683
2452
  }
2684
- async abort() {
2685
- this.store.update({ phase: "error" });
2686
- await this.close();
2453
+ async abort() {
2454
+ this.store.update({ phase: "error" });
2455
+ await this.close();
2456
+ }
2457
+ async close() {
2458
+ const pending = this.handle;
2459
+ this.handle = null;
2460
+ if (!pending) return;
2461
+ await (await pending).close();
2462
+ }
2463
+ };
2464
+ //#endregion
2465
+ //#region src/commands/agent-local-cli.ts
2466
+ const cliPath$1 = () => {
2467
+ const arg = process.argv[1];
2468
+ if (arg && fs.existsSync(arg)) return arg;
2469
+ return path.resolve("dist/cli.js");
2470
+ };
2471
+ const runSelf = (args, cwd) => {
2472
+ const result = spawnSync(process.execPath, [cliPath$1(), ...args], {
2473
+ cwd,
2474
+ encoding: "utf-8",
2475
+ env: {
2476
+ ...process.env,
2477
+ AISLOP_NO_TELEMETRY: "1",
2478
+ AISLOP_NO_UPDATE_NOTIFIER: "1",
2479
+ NO_COLOR: "1"
2480
+ },
2481
+ maxBuffer: 52428800
2482
+ });
2483
+ return {
2484
+ exitCode: result.status,
2485
+ stdout: result.stdout,
2486
+ stderr: result.stderr
2487
+ };
2488
+ };
2489
+ const scanJson = (cwd) => {
2490
+ const result = runSelf([
2491
+ "scan",
2492
+ ".",
2493
+ "--json"
2494
+ ], cwd);
2495
+ if (!result.stdout.trim()) throw new Error(result.stderr || "raigal scan did not produce JSON output.");
2496
+ try {
2497
+ return JSON.parse(result.stdout);
2498
+ } catch (error) {
2499
+ throw new Error(`Failed to parse raigal scan output: ${error instanceof Error ? error.message : String(error)}`);
2500
+ }
2501
+ };
2502
+ const runSafeFix = (cwd) => {
2503
+ const result = runSelf([
2504
+ "fix",
2505
+ ".",
2506
+ "--safe"
2507
+ ], cwd);
2508
+ if (result.exitCode !== 0 && result.stderr.trim()) log.warn(result.stderr.trim().split("\n")[0] ?? "Safe fix reported a non-zero exit.");
2509
+ };
2510
+ const applyDiff = async (root, patch) => {
2511
+ const child = spawnSync("git", [
2512
+ "apply",
2513
+ "--index",
2514
+ "--whitespace=nowarn",
2515
+ "-"
2516
+ ], {
2517
+ cwd: root,
2518
+ input: patch,
2519
+ encoding: "utf-8",
2520
+ maxBuffer: 52428800
2521
+ });
2522
+ if (child.status !== 0) throw new Error(child.stderr || child.stdout || "Failed to apply agent diff.");
2523
+ };
2524
+ //#endregion
2525
+ //#region src/agents/publish.ts
2526
+ const defaultBranchSuffix = () => (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\..+$/, "").replace("T", "-");
2527
+ const normalizeBranchName = (raw) => {
2528
+ const cleaned = raw.trim().replace(/[^A-Za-z0-9._/-]+/g, "-").replace(/\/+/g, "/").replace(/^-+|-+$/g, "").replace(/^\/+|\/+$/g, "");
2529
+ return cleaned.length > 0 ? cleaned.slice(0, 120) : `raigal/agent-${defaultBranchSuffix()}`;
2530
+ };
2531
+ const defaultBranchName = () => `raigal/agent-${defaultBranchSuffix()}`;
2532
+ const runGit = async (cwd, args) => {
2533
+ const result = await runSubprocess("git", args, {
2534
+ cwd,
2535
+ timeout: 9e4
2536
+ });
2537
+ if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `git ${args.join(" ")} failed`);
2538
+ return result.stdout;
2539
+ };
2540
+ const currentBranch = async (cwd) => {
2541
+ const result = await runSubprocess("git", ["branch", "--show-current"], { cwd });
2542
+ if (result.exitCode !== 0) return null;
2543
+ return result.stdout.trim() || null;
2544
+ };
2545
+ const resolveBaseBranch = async (cwd, fallback) => {
2546
+ if (fallback && fallback !== "HEAD") return fallback;
2547
+ const cached = await runSubprocess("git", [
2548
+ "symbolic-ref",
2549
+ "refs/remotes/origin/HEAD",
2550
+ "--short"
2551
+ ], { cwd });
2552
+ if (cached.exitCode === 0 && cached.stdout.trim()) return cached.stdout.trim().replace(/^origin\//, "");
2553
+ const remote = await runSubprocess("git", [
2554
+ "remote",
2555
+ "show",
2556
+ "origin"
2557
+ ], {
2558
+ cwd,
2559
+ timeout: 3e4
2560
+ });
2561
+ if (remote.exitCode === 0) {
2562
+ const branch = remote.stdout.split("\n").find((entry) => entry.trim().startsWith("HEAD branch:"))?.split(":").slice(1).join(":").trim();
2563
+ if (branch) return branch;
2564
+ }
2565
+ return "main";
2566
+ };
2567
+ const ensureBranch = async (cwd, branch) => {
2568
+ if (await currentBranch(cwd) === branch) return;
2569
+ await runGit(cwd, [
2570
+ "switch",
2571
+ "-c",
2572
+ branch
2573
+ ]);
2574
+ };
2575
+ const ensureGh = async () => {
2576
+ if ((await runSubprocess("gh", ["--version"], { timeout: 1e4 })).exitCode !== 0) throw new Error("Creating a PR requires the GitHub CLI. Install and authenticate `gh`, then retry.");
2577
+ };
2578
+ const buildPrBody = (input) => {
2579
+ const delta = input.beforeScore != null && input.afterScore != null ? input.afterScore - input.beforeScore : null;
2580
+ const deltaStr = delta != null ? ` (${delta >= 0 ? `+${delta}` : delta})` : "";
2581
+ const scoreStr = `${input.beforeScore ?? "not scored"} -> ${input.afterScore ?? "not scored"}${deltaStr}`;
2582
+ let diffStatLine = `- Files changed: ${input.changedFiles.length}`;
2583
+ if (typeof input.diffSummary === "string" && input.diffSummary.trim()) diffStatLine += ` (${input.diffSummary.trim()})`;
2584
+ else if (input.diffSummary && typeof input.diffSummary === "object") {
2585
+ const adds = input.diffSummary.additions ?? 0;
2586
+ const dels = input.diffSummary.deletions ?? 0;
2587
+ diffStatLine += ` (+${adds} -${dels})`;
2687
2588
  }
2688
- async close() {
2689
- const pending = this.handle;
2690
- this.handle = null;
2691
- if (!pending) return;
2692
- await (await pending).close();
2589
+ const fixedFindingsSection = input.fixedFindings && input.fixedFindings.length > 0 ? [
2590
+ "### Fixed findings",
2591
+ "",
2592
+ "| Rule | File | Message |",
2593
+ "|---|---|---|",
2594
+ ...input.fixedFindings.slice(0, 10).map((f) => {
2595
+ const loc = f.line ? `${f.path}:${f.line}` : f.path;
2596
+ const msg = (f.message || "").replace(/\|/g, "\\|");
2597
+ return `| \`${f.ruleId}\` | \`${loc}\` | ${msg} |`;
2598
+ }),
2599
+ input.fixedFindings.length > 10 ? `\n*...and ${input.fixedFindings.length - 10} more findings fixed.*` : "",
2600
+ ""
2601
+ ] : [];
2602
+ return [
2603
+ "This PR was generated locally by `raigal agent`.",
2604
+ "",
2605
+ "## Summary",
2606
+ "",
2607
+ `- Provider: ${input.providerId}`,
2608
+ `- Score: ${scoreStr}`,
2609
+ diffStatLine,
2610
+ "",
2611
+ ...fixedFindingsSection,
2612
+ "## Changed files",
2613
+ "",
2614
+ ...input.changedFiles.map((file) => `- ${file}`),
2615
+ "",
2616
+ "## Verification",
2617
+ "",
2618
+ "- `raigal scan --json`",
2619
+ "",
2620
+ "Review before merging. The deterministic scan is the gate; the provider only proposed the patch."
2621
+ ].filter(Boolean).join("\n");
2622
+ };
2623
+ const publishAgentDiff = async (input) => {
2624
+ if (!input.options.commit && !input.options.pr) return null;
2625
+ if (input.changedFiles.length === 0) return null;
2626
+ const branch = normalizeBranchName(input.options.branch ?? defaultBranchName());
2627
+ await ensureBranch(input.cwd, branch);
2628
+ await runGit(input.cwd, ["add", "-A"]);
2629
+ if ((await runSubprocess("git", [
2630
+ "diff",
2631
+ "--cached",
2632
+ "--quiet"
2633
+ ], { cwd: input.cwd })).exitCode === 0) return null;
2634
+ await runGit(input.cwd, [
2635
+ "commit",
2636
+ "-m",
2637
+ input.options.commitMessage,
2638
+ "-m",
2639
+ "Generated by raigal agent."
2640
+ ]);
2641
+ const commitSha = (await runGit(input.cwd, [
2642
+ "rev-parse",
2643
+ "--short",
2644
+ "HEAD"
2645
+ ])).trim();
2646
+ if (!input.options.pr) return {
2647
+ branch,
2648
+ commitSha,
2649
+ pushed: false,
2650
+ prUrl: null
2651
+ };
2652
+ await ensureGh();
2653
+ const base = input.options.base ?? await resolveBaseBranch(input.cwd, input.originalBranch);
2654
+ await runGit(input.cwd, [
2655
+ "push",
2656
+ "-u",
2657
+ "origin",
2658
+ branch
2659
+ ]);
2660
+ let diffSummary = input.diffSummary;
2661
+ if (!diffSummary) {
2662
+ const stat = await runSubprocess("git", [
2663
+ "diff",
2664
+ "--shortstat",
2665
+ "HEAD~1"
2666
+ ], { cwd: input.cwd });
2667
+ if (stat.exitCode === 0 && stat.stdout.trim()) diffSummary = stat.stdout.trim();
2693
2668
  }
2669
+ const args = [
2670
+ "pr",
2671
+ "create",
2672
+ "--base",
2673
+ base,
2674
+ "--head",
2675
+ branch,
2676
+ "--title",
2677
+ input.options.prTitle ?? input.options.commitMessage,
2678
+ "--body",
2679
+ buildPrBody({
2680
+ ...input,
2681
+ diffSummary
2682
+ })
2683
+ ];
2684
+ if (!input.options.ready) args.push("--draft");
2685
+ const pr = await runSubprocess("gh", args, {
2686
+ cwd: input.cwd,
2687
+ timeout: 9e4
2688
+ });
2689
+ if (pr.exitCode !== 0) throw new Error(pr.stderr || pr.stdout || "Failed to create pull request with gh.");
2690
+ return {
2691
+ branch,
2692
+ commitSha,
2693
+ pushed: true,
2694
+ prUrl: pr.stdout.trim() || null
2695
+ };
2694
2696
  };
2695
2697
  //#endregion
2696
2698
  //#region src/cloud/context.ts
@@ -2766,7 +2768,8 @@ const detectGitDiff = (directory, baseRef) => {
2766
2768
  "HEAD~1"
2767
2769
  ], directory);
2768
2770
  let diffPreview;
2769
- if (preview) diffPreview = preview.split("\n").slice(0, 40).join("\n").slice(0, 1e4);
2771
+ const allowSnippets = process.env.RAIGAL_NO_SNIPPETS !== "true" && process.env.RAIGAL_PRIVACY_MODE !== "true";
2772
+ if (preview && allowSnippets) diffPreview = preview.split("\n").slice(0, 40).join("\n").slice(0, 1e4);
2770
2773
  return {
2771
2774
  diffSummary,
2772
2775
  diffPreview
@@ -2887,7 +2890,7 @@ const detectCiContext = () => {
2887
2890
  };
2888
2891
  //#endregion
2889
2892
  //#region src/cloud/outbox.ts
2890
- const MAX_OUTBOX_BYTES = 5 * 1024 * 1024;
2893
+ const MAX_OUTBOX_BYTES = 5242880;
2891
2894
  const getMaxOutboxBytes = () => {
2892
2895
  const override = process.env.RAIGAL_MAX_OUTBOX_BYTES;
2893
2896
  if (override) {
@@ -2919,7 +2922,7 @@ const writeRunsToOutbox = (runs) => {
2919
2922
  recursive: true,
2920
2923
  mode: 448
2921
2924
  });
2922
- let lines = runs.map((r) => JSON.stringify(r));
2925
+ const lines = runs.map((r) => JSON.stringify(r));
2923
2926
  let text = lines.length > 0 ? `${lines.join("\n")}\n` : "";
2924
2927
  const maxBytes = getMaxOutboxBytes();
2925
2928
  while (Buffer.byteLength(text, "utf-8") > maxBytes && lines.length > 1) {
@@ -2944,7 +2947,9 @@ const getLastRecordedRun = () => {
2944
2947
  const runs = loadOutboxRuns();
2945
2948
  return runs.length > 0 ? runs[runs.length - 1] ?? null : null;
2946
2949
  };
2947
- const flushOutbox = async (token) => {
2950
+ const flushOutbox = async (optionsOrToken) => {
2951
+ const token = typeof optionsOrToken === "string" ? optionsOrToken : optionsOrToken?.token;
2952
+ const oidcToken = typeof optionsOrToken === "object" ? optionsOrToken?.oidcToken : void 0;
2948
2953
  const runs = loadOutboxRuns();
2949
2954
  if (runs.length === 0) return {
2950
2955
  sent: 0,
@@ -2960,6 +2965,7 @@ const flushOutbox = async (token) => {
2960
2965
  await requestApi("/v1/runs", {
2961
2966
  method: "POST",
2962
2967
  token,
2968
+ oidcToken,
2963
2969
  body: run,
2964
2970
  timeoutMs: 4e3
2965
2971
  });
@@ -3079,9 +3085,15 @@ var CloudRunRecorder = class {
3079
3085
  if (this.finished) return;
3080
3086
  this.finished = true;
3081
3087
  try {
3082
- enqueueRun(this.buildRunPayload(exitCode));
3088
+ const run = this.buildRunPayload(exitCode);
3089
+ enqueueRun(run);
3083
3090
  const activeToken = getActiveToken();
3084
- if (activeToken) await flushOutbox(activeToken.token);
3091
+ let oidcToken;
3092
+ if (!activeToken) oidcToken = await fetchGitHubOidcToken();
3093
+ if (activeToken || oidcToken) await flushOutbox({
3094
+ token: activeToken?.token,
3095
+ oidcToken
3096
+ });
3085
3097
  } catch {}
3086
3098
  }
3087
3099
  };
@@ -3091,7 +3103,10 @@ const startRunRecorder = (command, directory, flags = []) => {
3091
3103
  const recorder = new CloudRunRecorder(command, directory, flags);
3092
3104
  activeRecorder = recorder;
3093
3105
  const activeToken = getActiveToken();
3094
- if (activeToken) flushOutbox(activeToken.token).catch(() => {});
3106
+ if (activeToken) flushOutbox({ token: activeToken.token }).catch(() => {});
3107
+ else fetchGitHubOidcToken().then((oidcToken) => {
3108
+ if (oidcToken) return flushOutbox({ oidcToken });
3109
+ }).catch(() => {});
3095
3110
  return recorder;
3096
3111
  };
3097
3112
  const finishActiveRecorder = async (exitCode = 0) => {
@@ -3102,64 +3117,186 @@ const finishActiveRecorder = async (exitCode = 0) => {
3102
3117
  }
3103
3118
  };
3104
3119
  //#endregion
3105
- //#region src/commands/agent-local-cli.ts
3106
- const cliPath$1 = () => {
3107
- const arg = process.argv[1];
3108
- if (arg && fs.existsSync(arg)) return arg;
3109
- return path.resolve("dist/cli.js");
3120
+ //#region src/commands/agent-session-publish.ts
3121
+ const maybePublishAgentDiff = async ({ tui, session, options, changedFiles, worktreePath, originalBranch, providerId, before, after }) => {
3122
+ if (changedFiles.length > 0 && (options.commit || options.pr)) {
3123
+ tui.start(options.pr ? "Creating local branch and PR" : "Creating local commit");
3124
+ session.append("publish.started", {
3125
+ commit: options.commit,
3126
+ pr: options.pr,
3127
+ branch: options.branch,
3128
+ base: options.base,
3129
+ ready: options.ready
3130
+ });
3131
+ const afterKeys = new Set(after.diagnostics.map((d) => `${d.rule}:${d.filePath}:${d.line}`));
3132
+ const fixedFindings = before.diagnostics.filter((d) => !afterKeys.has(`${d.rule}:${d.filePath}:${d.line}`)).map((d) => ({
3133
+ ruleId: d.rule,
3134
+ path: d.filePath,
3135
+ line: d.line,
3136
+ message: d.message
3137
+ }));
3138
+ const published = await publishAgentDiff({
3139
+ cwd: worktreePath,
3140
+ originalBranch,
3141
+ providerId,
3142
+ beforeScore: before.score,
3143
+ afterScore: after.score,
3144
+ changedFiles,
3145
+ fixedFindings,
3146
+ options: {
3147
+ commit: options.commit,
3148
+ pr: options.pr,
3149
+ branch: options.branch,
3150
+ base: options.base,
3151
+ commitMessage: options.commitMessage,
3152
+ prTitle: options.prTitle,
3153
+ ready: options.ready
3154
+ }
3155
+ });
3156
+ session.append(published ? "publish.finished" : "publish.skipped", { result: published });
3157
+ tui.complete({
3158
+ status: published ? "done" : "skipped",
3159
+ label: published?.prUrl ? `Opened PR ${published.prUrl}` : published ? `Committed ${published.commitSha}` : "No commit created"
3160
+ });
3161
+ return published;
3162
+ }
3163
+ if (options.commit || options.pr) session.append("publish.skipped", {
3164
+ reason: "no_changed_files",
3165
+ commit: options.commit,
3166
+ pr: options.pr
3167
+ });
3168
+ return null;
3110
3169
  };
3111
- const runSelf = (args, cwd) => {
3112
- const result = spawnSync(process.execPath, [cliPath$1(), ...args], {
3113
- cwd,
3114
- encoding: "utf-8",
3115
- env: {
3116
- ...process.env,
3117
- AISLOP_NO_TELEMETRY: "1",
3118
- AISLOP_NO_UPDATE_NOTIFIER: "1",
3119
- NO_COLOR: "1"
3120
- },
3121
- maxBuffer: 50 * 1024 * 1024
3170
+ const recordAgentSessionTelemetry = ({ worktreePath, published, branchOption, providerId, beforeScore, afterScore, targetScore, durationMs, changedFilesCount }) => {
3171
+ const recorder = getActiveRecorder();
3172
+ if (!recorder) return;
3173
+ let patchDiff;
3174
+ if (worktreePath) try {
3175
+ const rawDiff = spawnSync("git", [
3176
+ "diff",
3177
+ "-U2",
3178
+ "--no-color",
3179
+ "HEAD"
3180
+ ], {
3181
+ cwd: worktreePath,
3182
+ encoding: "utf-8"
3183
+ }).stdout || "";
3184
+ patchDiff = rawDiff ? maskSecrets(rawDiff).slice(0, 2e4) : void 0;
3185
+ } catch {}
3186
+ if (published) {
3187
+ const prMatch = published.prUrl?.match(/\/pull\/(\d+)/);
3188
+ const prNumber = prMatch ? Number.parseInt(prMatch[1], 10) : void 0;
3189
+ recorder.setGitContext({
3190
+ branch: published.branch,
3191
+ pr_number: prNumber
3192
+ });
3193
+ } else if (branchOption) recorder.setGitContext({ branch: branchOption });
3194
+ const agentTelemetry = {
3195
+ provider: providerId,
3196
+ before_score: beforeScore,
3197
+ after_score: afterScore,
3198
+ pr_url: published?.prUrl || void 0,
3199
+ patch_diff: patchDiff,
3200
+ duration_ms: durationMs
3201
+ };
3202
+ recorder.setAgentTelemetry(agentTelemetry);
3203
+ recorder.recordStep("agent:baseline", 50, true);
3204
+ recorder.recordStep("agent:verify", durationMs, afterScore !== null && afterScore >= targetScore);
3205
+ recorder.setReport({
3206
+ score: afterScore,
3207
+ scoreable: true,
3208
+ files_scanned: changedFilesCount,
3209
+ findings: [],
3210
+ agent_telemetry: agentTelemetry
3122
3211
  });
3123
- return {
3124
- exitCode: result.status,
3125
- stdout: result.stdout,
3126
- stderr: result.stderr
3127
- };
3128
3212
  };
3129
- const scanJson = (cwd) => {
3130
- const result = runSelf([
3131
- "scan",
3132
- ".",
3133
- "--json"
3134
- ], cwd);
3135
- if (!result.stdout.trim()) throw new Error(result.stderr || "raigal scan did not produce JSON output.");
3136
- try {
3137
- return JSON.parse(result.stdout);
3138
- } catch (error) {
3139
- throw new Error(`Failed to parse raigal scan output: ${error instanceof Error ? error.message : String(error)}`);
3213
+ //#endregion
3214
+ //#region src/agents/prompt.ts
3215
+ const CONTEXT_LINES$1 = 3;
3216
+ const MAX_DIAGNOSTICS_PER_FILE$1 = 6;
3217
+ const MAX_FILES$1 = 12;
3218
+ const priority = (diagnostic) => {
3219
+ if (diagnostic.severity === "error") return 0;
3220
+ if (diagnostic.severity === "warning") return 1;
3221
+ return 2;
3222
+ };
3223
+ const selectAgentFindings = (diagnostics, limit) => diagnostics.filter((diagnostic) => diagnostic.severity !== "info").sort((a, b) => {
3224
+ const severityDelta = priority(a) - priority(b);
3225
+ if (severityDelta !== 0) return severityDelta;
3226
+ if (a.fixable !== b.fixable) return a.fixable ? 1 : -1;
3227
+ return a.filePath.localeCompare(b.filePath);
3228
+ }).slice(0, Math.max(1, limit));
3229
+ const groupByFile$1 = (diagnostics) => {
3230
+ const groups = /* @__PURE__ */ new Map();
3231
+ for (const diagnostic of diagnostics) {
3232
+ const list = groups.get(diagnostic.filePath) ?? [];
3233
+ list.push(diagnostic);
3234
+ groups.set(diagnostic.filePath, list);
3140
3235
  }
3236
+ return [...groups.entries()].map(([filePath, items]) => ({
3237
+ filePath,
3238
+ diagnostics: items
3239
+ })).slice(0, MAX_FILES$1);
3141
3240
  };
3142
- const runSafeFix = (cwd) => {
3143
- const result = runSelf([
3144
- "fix",
3145
- ".",
3146
- "--safe"
3147
- ], cwd);
3148
- if (result.exitCode !== 0 && result.stderr.trim()) log.warn(result.stderr.trim().split("\n")[0] ?? "Safe fix reported a non-zero exit.");
3241
+ const snippetFor = (rootDirectory, diagnostic) => {
3242
+ if (diagnostic.line <= 0 || diagnostic.redactSource || isSecretClassRule(diagnostic.rule)) return null;
3243
+ const absolutePath = path.resolve(rootDirectory, diagnostic.filePath);
3244
+ let content;
3245
+ try {
3246
+ content = fs.readFileSync(absolutePath, "utf-8");
3247
+ } catch {
3248
+ return null;
3249
+ }
3250
+ const lines = content.split("\n");
3251
+ const start = Math.max(0, diagnostic.line - 1 - CONTEXT_LINES$1);
3252
+ const end = Math.min(lines.length, diagnostic.line + CONTEXT_LINES$1);
3253
+ const out = [];
3254
+ for (let index = start; index < end; index += 1) {
3255
+ const lineNumber = index + 1;
3256
+ const marker = lineNumber === diagnostic.line ? ">" : " ";
3257
+ out.push(`${marker} ${String(lineNumber).padStart(4)} | ${lines[index]}`);
3258
+ }
3259
+ return out.join("\n");
3149
3260
  };
3150
- const applyDiff = async (root, patch) => {
3151
- const child = spawnSync("git", [
3152
- "apply",
3153
- "--index",
3154
- "--whitespace=nowarn",
3155
- "-"
3156
- ], {
3157
- cwd: root,
3158
- input: patch,
3159
- encoding: "utf-8",
3160
- maxBuffer: 50 * 1024 * 1024
3161
- });
3162
- if (child.status !== 0) throw new Error(child.stderr || child.stdout || "Failed to apply agent diff.");
3261
+ const buildRepairPrompt = (input) => {
3262
+ const groups = groupByFile$1(input.findings);
3263
+ const lines = [
3264
+ "You are repairing AI slop findings in a local git worktree.",
3265
+ "",
3266
+ `Current raigal score: ${input.score == null ? "not scored" : `${input.score}/100`}`,
3267
+ `Target score: ${input.targetScore}/100`,
3268
+ `Turn budget: ${input.maxTurns}`,
3269
+ "",
3270
+ "Hard constraints:",
3271
+ "- Make the smallest code changes that improve the findings below.",
3272
+ "- Do not change public APIs, exports, database schemas, or test expectations unless a finding directly requires it.",
3273
+ "- Do not delete tests.",
3274
+ "- Do not add dependencies unless the existing project already clearly expects them.",
3275
+ "- Never print, copy, commit, or echo secret values or credentials. Replace all hardcoded credentials and secrets with environment-variable references (e.g. process.env.KEY).",
3276
+ "- If a finding looks like a false positive, leave the code alone and mention it in your final summary.",
3277
+ "- Run the relevant local verification command if it is obvious and cheap.",
3278
+ "",
3279
+ "raigal findings to repair:",
3280
+ ""
3281
+ ];
3282
+ for (const group of groups) {
3283
+ lines.push(`## ${group.filePath}`);
3284
+ for (const diagnostic of group.diagnostics.slice(0, MAX_DIAGNOSTICS_PER_FILE$1)) {
3285
+ const location = diagnostic.line > 0 ? `:${diagnostic.line}` : "";
3286
+ lines.push(`- ${diagnostic.severity.toUpperCase()} ${diagnostic.rule}${location}: ${diagnostic.message}`);
3287
+ if (diagnostic.help) lines.push(` Help: ${diagnostic.help}`);
3288
+ const snippet = snippetFor(input.rootDirectory, diagnostic);
3289
+ if (snippet) {
3290
+ lines.push("```");
3291
+ lines.push(snippet);
3292
+ lines.push("```");
3293
+ }
3294
+ }
3295
+ lines.push("");
3296
+ }
3297
+ if (input.findings.length === 0) lines.push("No findings were supplied. Inspect the codebase for obvious AI slop only if needed.");
3298
+ lines.push("After editing, stop and summarize what changed and what you intentionally skipped.");
3299
+ return maskSecrets(lines.join("\n"));
3163
3300
  };
3164
3301
  //#endregion
3165
3302
  //#region src/agents/provider-metadata.ts
@@ -3614,6 +3751,53 @@ const maybeApplyDiff = async (input) => {
3614
3751
  await applyDiff(input.originalRoot, await readBinaryDiff(input.worktreePath));
3615
3752
  return true;
3616
3753
  };
3754
+ const resolveAgentSessionCwd = (created, requestedDirectory) => {
3755
+ const relative = path.relative(created.state.root, requestedDirectory);
3756
+ const outsideRoot = relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative);
3757
+ if (relative.length === 0 || outsideRoot) return created.worktree.path;
3758
+ return path.resolve(created.worktree.path, relative);
3759
+ };
3760
+ const prepareWorktree = async (tui, resolvedDir, options) => {
3761
+ tui.start("Preparing local session");
3762
+ const created = await createAgentWorktree(resolvedDir, { inPlace: options.inPlace });
3763
+ tui.setMetric("Worktree", created.worktree.created ? path.relative(created.state.root, created.worktree.path) : "current");
3764
+ tui.complete({
3765
+ status: "done",
3766
+ label: created.worktree.created ? `Created worktree ${path.relative(created.state.root, created.worktree.path)}` : "Using current worktree"
3767
+ });
3768
+ return created;
3769
+ };
3770
+ const scanBaseline = (tui, cwd) => {
3771
+ tui.start("Scanning baseline");
3772
+ const scan = scanJson(cwd);
3773
+ tui.setMetric("Score", `${scan.score ?? "not scored"} -> ...`);
3774
+ tui.setMetric("Findings", scan.diagnostics.length);
3775
+ tui.complete({
3776
+ status: scan.summary.errors > 0 ? "warn" : "done",
3777
+ label: `Baseline ${scan.score ?? "not scored"} / 100 · ${scan.diagnostics.length} findings`
3778
+ });
3779
+ return scan;
3780
+ };
3781
+ const runSafeFixStep = (tui, cwd, options) => {
3782
+ if (options.noFix) return;
3783
+ tui.start("Applying deterministic safe fixes");
3784
+ runSafeFix(cwd);
3785
+ tui.complete({
3786
+ status: "done",
3787
+ label: "Safe fixer finished"
3788
+ });
3789
+ };
3790
+ const selectFindings = (tui, cwd, limit, pass) => {
3791
+ tui.start(`Pass ${pass}: selecting findings`);
3792
+ const scan = scanJson(cwd);
3793
+ const findings = selectAgentFindings(scan.diagnostics, limit);
3794
+ tui.setMetric("Selected", findings.length);
3795
+ tui.complete({
3796
+ status: findings.length > 0 ? "done" : "skipped",
3797
+ label: findings.length > 0 ? `Pass ${pass}: selected ${findings.length} finding${findings.length === 1 ? "" : "s"}` : "No remaining agent findings"
3798
+ });
3799
+ return findings;
3800
+ };
3617
3801
  //#endregion
3618
3802
  //#region src/utils/paths.ts
3619
3803
  /** Normalize an OS path to forward-slash (POSIX) separators. */
@@ -3749,52 +3933,6 @@ const printAgentSessionSummary = (input) => {
3749
3933
  //#endregion
3750
3934
  //#region src/commands/agent-session.ts
3751
3935
  const scoreDelta = (before, after) => typeof before === "number" && typeof after === "number" ? after - before : void 0;
3752
- const resolveAgentSessionCwd = (created, requestedDirectory) => {
3753
- const relative = path.relative(created.state.root, requestedDirectory);
3754
- const outsideRoot = relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative);
3755
- if (relative.length === 0 || outsideRoot) return created.worktree.path;
3756
- return path.resolve(created.worktree.path, relative);
3757
- };
3758
- const prepareWorktree = async (tui, resolvedDir, options) => {
3759
- tui.start("Preparing local session");
3760
- const created = await createAgentWorktree(resolvedDir, { inPlace: options.inPlace });
3761
- tui.setMetric("Worktree", created.worktree.created ? path.relative(created.state.root, created.worktree.path) : "current");
3762
- tui.complete({
3763
- status: "done",
3764
- label: created.worktree.created ? `Created worktree ${path.relative(created.state.root, created.worktree.path)}` : "Using current worktree"
3765
- });
3766
- return created;
3767
- };
3768
- const scanBaseline = (tui, cwd) => {
3769
- tui.start("Scanning baseline");
3770
- const scan = scanJson(cwd);
3771
- tui.setMetric("Score", `${scan.score ?? "not scored"} -> ...`);
3772
- tui.setMetric("Findings", scan.diagnostics.length);
3773
- tui.complete({
3774
- status: scan.summary.errors > 0 ? "warn" : "done",
3775
- label: `Baseline ${scan.score ?? "not scored"} / 100 · ${scan.diagnostics.length} findings`
3776
- });
3777
- return scan;
3778
- };
3779
- const runSafeFixStep = (tui, cwd, options) => {
3780
- if (options.noFix) return;
3781
- tui.start("Applying deterministic safe fixes");
3782
- runSafeFix(cwd);
3783
- tui.complete({
3784
- status: "done",
3785
- label: "Safe fixer finished"
3786
- });
3787
- };
3788
- const selectFindings = (tui, cwd, limit, pass) => {
3789
- tui.start(`Pass ${pass}: selecting findings`);
3790
- const findings = selectAgentFindings(scanJson(cwd).diagnostics, limit);
3791
- tui.setMetric("Selected", findings.length);
3792
- tui.complete({
3793
- status: findings.length > 0 ? "done" : "skipped",
3794
- label: findings.length > 0 ? `Pass ${pass}: selected ${findings.length} finding${findings.length === 1 ? "" : "s"}` : "No remaining agent findings"
3795
- });
3796
- return findings;
3797
- };
3798
3936
  const runAgentSession = async (selected, resolvedDir, options, started) => {
3799
3937
  const tui = new AgentTui({
3800
3938
  provider: selected.provider.label,
@@ -3959,49 +4097,16 @@ const runAgentSession = async (selected, resolvedDir, options, started) => {
3959
4097
  applyRequested: options.apply,
3960
4098
  changedFiles: changedFiles.length
3961
4099
  });
3962
- if (changedFiles.length > 0 && (options.commit || options.pr)) {
3963
- tui.start(options.pr ? "Creating local branch and PR" : "Creating local commit");
3964
- session.append("publish.started", {
3965
- commit: options.commit,
3966
- pr: options.pr,
3967
- branch: options.branch,
3968
- base: options.base,
3969
- ready: options.ready
3970
- });
3971
- const afterKeys = new Set(verified.after.diagnostics.map((d) => `${d.rule}:${d.filePath}:${d.line}`));
3972
- const fixedFindings = before.diagnostics.filter((d) => !afterKeys.has(`${d.rule}:${d.filePath}:${d.line}`)).map((d) => ({
3973
- ruleId: d.rule,
3974
- path: d.filePath,
3975
- line: d.line,
3976
- message: d.message
3977
- }));
3978
- published = await publishAgentDiff({
3979
- cwd: created.worktree.path,
3980
- originalBranch: created.state.branch,
3981
- providerId: selected.provider.id,
3982
- beforeScore: before.score,
3983
- afterScore: verified.after.score,
3984
- changedFiles,
3985
- fixedFindings,
3986
- options: {
3987
- commit: options.commit,
3988
- pr: options.pr,
3989
- branch: options.branch,
3990
- base: options.base,
3991
- commitMessage: options.commitMessage,
3992
- prTitle: options.prTitle,
3993
- ready: options.ready
3994
- }
3995
- });
3996
- session.append(published ? "publish.finished" : "publish.skipped", { result: published });
3997
- tui.complete({
3998
- status: published ? "done" : "skipped",
3999
- label: published?.prUrl ? `Opened PR ${published.prUrl}` : published ? `Committed ${published.commitSha}` : "No commit created"
4000
- });
4001
- } else if (options.commit || options.pr) session.append("publish.skipped", {
4002
- reason: "no_changed_files",
4003
- commit: options.commit,
4004
- pr: options.pr
4100
+ published = await maybePublishAgentDiff({
4101
+ tui,
4102
+ session,
4103
+ options,
4104
+ changedFiles,
4105
+ worktreePath: created.worktree.path,
4106
+ originalBranch: created.state.branch,
4107
+ providerId: selected.provider.id,
4108
+ before,
4109
+ after: verified.after
4005
4110
  });
4006
4111
  session.append("session.completed", {
4007
4112
  durationMs: Math.round(performance.now() - started),
@@ -4017,47 +4122,17 @@ const runAgentSession = async (selected, resolvedDir, options, started) => {
4017
4122
  published: Boolean(published)
4018
4123
  });
4019
4124
  await tui.finish({ footer: `Done · ${selected.provider.id} · ${Math.round(performance.now() - started)}ms` });
4020
- const recorder = getActiveRecorder();
4021
- if (recorder) {
4022
- let patchDiff;
4023
- if (created?.worktree?.path) try {
4024
- patchDiff = (spawnSync("git", [
4025
- "diff",
4026
- "-U2",
4027
- "--no-color",
4028
- "HEAD"
4029
- ], {
4030
- cwd: created.worktree.path,
4031
- encoding: "utf-8"
4032
- }).stdout || "").slice(0, 2e4) || void 0;
4033
- } catch {}
4034
- if (published) {
4035
- const prMatch = published.prUrl?.match(/\/pull\/(\d+)/);
4036
- const prNumber = prMatch ? Number.parseInt(prMatch[1], 10) : void 0;
4037
- recorder.setGitContext({
4038
- branch: published.branch,
4039
- pr_number: prNumber
4040
- });
4041
- } else if (options.branch) recorder.setGitContext({ branch: options.branch });
4042
- const agentTelemetry = {
4043
- provider: selected.provider.id,
4044
- before_score: before.score,
4045
- after_score: verified.after.score,
4046
- pr_url: published?.prUrl || void 0,
4047
- patch_diff: patchDiff,
4048
- duration_ms: Math.round(performance.now() - started)
4049
- };
4050
- recorder.setAgentTelemetry(agentTelemetry);
4051
- recorder.recordStep("agent:baseline", 50, true);
4052
- recorder.recordStep("agent:verify", Math.round(performance.now() - started), verified.after.score !== null && verified.after.score >= options.targetScore);
4053
- recorder.setReport({
4054
- score: verified.after.score,
4055
- scoreable: true,
4056
- files_scanned: changedFiles.length,
4057
- findings: [],
4058
- agent_telemetry: agentTelemetry
4059
- });
4060
- }
4125
+ recordAgentSessionTelemetry({
4126
+ worktreePath: created?.worktree?.path,
4127
+ published,
4128
+ branchOption: options.branch,
4129
+ providerId: selected.provider.id,
4130
+ beforeScore: before.score,
4131
+ afterScore: verified.after.score,
4132
+ targetScore: options.targetScore,
4133
+ durationMs: Math.round(performance.now() - started),
4134
+ changedFilesCount: changedFiles.length
4135
+ });
4061
4136
  printAgentSessionSummary({
4062
4137
  before,
4063
4138
  after: verified.after,
@@ -4704,7 +4779,8 @@ const DEFAULT_CONFIG = {
4704
4779
  format: "json"
4705
4780
  },
4706
4781
  telemetry: { enabled: true },
4707
- rules: {}
4782
+ rules: {},
4783
+ overrides: []
4708
4784
  };
4709
4785
  const GITHUB_WORKFLOW_DIR = ".github/workflows";
4710
4786
  const GITHUB_WORKFLOW_FILE = "raigal.yml";
@@ -4747,7 +4823,7 @@ const DEFAULT_RULES_YAML = `# Architecture rules (BYO)
4747
4823
  //#endregion
4748
4824
  //#region src/config/extends.ts
4749
4825
  const MAX_DEPTH$1 = 5;
4750
- const MAX_CONFIG_BYTES = 1024 * 1024;
4826
+ const MAX_CONFIG_BYTES = 1048576;
4751
4827
  const isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4752
4828
  const deepMerge = (...sources) => {
4753
4829
  const result = {};
@@ -4928,12 +5004,25 @@ const CiSchema = z.object({
4928
5004
  format: z.enum(["json"]).default("json")
4929
5005
  }).strict();
4930
5006
  const TelemetrySchema = z.object({ enabled: z.boolean().default(true) }).strict();
5007
+ const BaselineConfigSchema = z.object({ mode: z.enum(["strict", "relaxed"]).default("relaxed") }).strict();
5008
+ const LicensingConfigSchema = z.object({ onLapse: z.enum(["fail-open", "fail-closed"]).default("fail-open") }).strict();
5009
+ const WorkspaceConfigSchema = z.object({ minimumFloor: z.number().min(0).max(100).default(50) }).strict();
4931
5010
  const RuleSeverityOverride = z.enum([
4932
5011
  "error",
4933
5012
  "warning",
4934
5013
  "off"
4935
5014
  ]);
4936
5015
  const RulesSchema = z.record(z.string(), RuleSeverityOverride).default(() => ({}));
5016
+ const FileOverrideSchema = z.object({
5017
+ files: z.array(z.string().min(1)).min(1),
5018
+ quality: z.object({
5019
+ maxFunctionLoc: QualitySchema.shape.maxFunctionLoc.unwrap().optional(),
5020
+ maxFileLoc: QualitySchema.shape.maxFileLoc.unwrap().optional(),
5021
+ maxNesting: QualitySchema.shape.maxNesting.unwrap().optional(),
5022
+ maxParams: QualitySchema.shape.maxParams.unwrap().optional()
5023
+ }).optional(),
5024
+ rules: z.record(z.string(), RuleSeverityOverride).optional()
5025
+ });
4937
5026
  const AislopConfigSchema = z.object({
4938
5027
  version: z.number().default(1),
4939
5028
  extends: z.union([z.string(), z.array(z.string())]).optional(),
@@ -4988,7 +5077,11 @@ const AislopConfigSchema = z.object({
4988
5077
  format: "json"
4989
5078
  })),
4990
5079
  telemetry: TelemetrySchema.default(() => ({ enabled: true })),
5080
+ baseline: BaselineConfigSchema.optional(),
5081
+ licensing: LicensingConfigSchema.optional(),
5082
+ workspace: WorkspaceConfigSchema.optional(),
4991
5083
  rules: RulesSchema,
5084
+ overrides: z.array(FileOverrideSchema).default(() => []),
4992
5085
  exclude: z.array(z.string()).default(() => [
4993
5086
  "node_modules",
4994
5087
  ".git",
@@ -5020,6 +5113,9 @@ const KNOWN_KEYS_BY_PATH = {
5020
5113
  "scoring",
5021
5114
  "ci",
5022
5115
  "telemetry",
5116
+ "baseline",
5117
+ "licensing",
5118
+ "workspace",
5023
5119
  "rules",
5024
5120
  "exclude",
5025
5121
  "include"
@@ -5070,7 +5166,10 @@ const KNOWN_KEYS_BY_PATH = {
5070
5166
  ],
5071
5167
  "scoring.thresholds": ["good", "ok"],
5072
5168
  ci: ["failBelow", "format"],
5073
- telemetry: ["enabled"]
5169
+ telemetry: ["enabled"],
5170
+ baseline: ["mode"],
5171
+ licensing: ["onLapse"],
5172
+ workspace: ["minimumFloor"]
5074
5173
  };
5075
5174
  const levenshtein = (a, b) => {
5076
5175
  const m = a.length;
@@ -5202,7 +5301,8 @@ const loadConfig = (directory) => {
5202
5301
  const configPath = findConfigFile(directory);
5203
5302
  if (!configPath) return DEFAULT_CONFIG;
5204
5303
  try {
5205
- return validateConfig(loadConfigChain(configPath), configPath);
5304
+ const merged = loadConfigChain(configPath);
5305
+ return validateConfig(merged, configPath);
5206
5306
  } catch (error) {
5207
5307
  if (error instanceof ConfigError) throw error;
5208
5308
  throw new ConfigError(configPath, `Failed to parse ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
@@ -5360,6 +5460,10 @@ const SAFE_PROPERTY_NAMES = /* @__PURE__ */ new Set([
5360
5460
  "exit_code",
5361
5461
  "duration_ms",
5362
5462
  "error_kind",
5463
+ "error_name",
5464
+ "error_code",
5465
+ "failed_stage",
5466
+ "engines_failed",
5363
5467
  "score",
5364
5468
  "score_bucket",
5365
5469
  "finding_count",
@@ -5473,10 +5577,11 @@ const track = (input) => {
5473
5577
  cachedInstallId = ensured.installId;
5474
5578
  installCreated = ensured.created;
5475
5579
  }
5476
- const { clean, dropped } = redactProperties({
5580
+ const merged = {
5477
5581
  ...baseProperties(cachedInstallId),
5478
5582
  ...input.properties
5479
- });
5583
+ };
5584
+ const { clean, dropped } = redactProperties(merged);
5480
5585
  if (isDebug()) {
5481
5586
  const compact = JSON.stringify({
5482
5587
  event: input.event,
@@ -5615,6 +5720,8 @@ const buildCommandCompletedProps = (input) => {
5615
5720
  if (typeof input.fixSteps === "number") props.fix_steps = input.fixSteps;
5616
5721
  if (typeof input.fixResolved === "number") props.fix_resolved = input.fixResolved;
5617
5722
  if (typeof input.fixScoreDelta === "number") props.fix_score_delta = input.fixScoreDelta;
5723
+ if (input.errorName) props.error_name = input.errorName;
5724
+ if (input.errorCode) props.error_code = input.errorCode;
5618
5725
  return props;
5619
5726
  };
5620
5727
  const buildHookScanCompletedProps = (input) => {
@@ -5635,6 +5742,50 @@ const errorKindFromException = (error) => {
5635
5742
  if (message.includes("engine") && message.includes("crash")) return "engine_crash";
5636
5743
  return "unknown";
5637
5744
  };
5745
+ const SAFE_ERROR_NAME = /^[A-Za-z_$][A-Za-z0-9_$]{0,39}$/;
5746
+ const SAFE_ERROR_CODE = /^[A-Za-z][A-Za-z0-9_]{0,39}$/;
5747
+ const errorIdentity = (error) => {
5748
+ let name;
5749
+ if (error instanceof Error && typeof error.name === "string" && error.name.length > 0) name = error.name;
5750
+ else if (error === null) name = "null";
5751
+ else if (typeof error === "object") name = "object";
5752
+ else name = typeof error;
5753
+ const identity = { error_name: SAFE_ERROR_NAME.test(name) ? name : "Error" };
5754
+ const code = error?.code;
5755
+ if (typeof code === "string" && SAFE_ERROR_CODE.test(code)) identity.error_code = code;
5756
+ else if (typeof code === "number" && Number.isFinite(code)) identity.error_code = String(code);
5757
+ return identity;
5758
+ };
5759
+ const buildCommandFailedProps = (input) => {
5760
+ const props = {
5761
+ failed_stage: input.stage,
5762
+ ...errorIdentity(input.error)
5763
+ };
5764
+ if (input.command) props.command = input.command;
5765
+ return props;
5766
+ };
5767
+ //#endregion
5768
+ //#region src/telemetry/fatal.ts
5769
+ const reportedErrors = /* @__PURE__ */ new Set();
5770
+ let activeTelemetryConfig;
5771
+ const markErrorReported = (error) => {
5772
+ reportedErrors.add(error);
5773
+ };
5774
+ const rememberTelemetryConfig = (config) => {
5775
+ activeTelemetryConfig = config;
5776
+ };
5777
+ const reportFatalError = async (error, stage, config) => {
5778
+ if (reportedErrors.has(error)) return;
5779
+ track({
5780
+ event: "cli_command_failed",
5781
+ properties: buildCommandFailedProps({
5782
+ error,
5783
+ stage
5784
+ }),
5785
+ config: activeTelemetryConfig ?? config
5786
+ });
5787
+ await flushTelemetry(2e3);
5788
+ };
5638
5789
  //#endregion
5639
5790
  //#region src/telemetry/lifecycle.ts
5640
5791
  const STARTED_FLUSH_TIMEOUT_MS = 100;
@@ -5645,6 +5796,7 @@ const withCommandLifecycle = async (start, run) => {
5645
5796
  fileCount: start.fileCount,
5646
5797
  properties: start.properties
5647
5798
  });
5799
+ rememberTelemetryConfig(start.config);
5648
5800
  track({
5649
5801
  event: "cli_command_started",
5650
5802
  properties: startProps,
@@ -5661,6 +5813,7 @@ const withCommandLifecycle = async (start, run) => {
5661
5813
  startProps,
5662
5814
  exitCode: result.exitCode,
5663
5815
  durationMs,
5816
+ errorKind: result.errorKind,
5664
5817
  score: result.score ?? void 0,
5665
5818
  findingCount: result.findingCount,
5666
5819
  errorCount: result.errorCount,
@@ -5678,16 +5831,21 @@ const withCommandLifecycle = async (start, run) => {
5678
5831
  await flushTelemetry();
5679
5832
  return result;
5680
5833
  } catch (error) {
5834
+ const durationMs = performance.now() - startedAt;
5835
+ const identity = errorIdentity(error);
5681
5836
  track({
5682
5837
  event: "cli_command_completed",
5683
5838
  properties: buildCommandCompletedProps({
5684
5839
  startProps,
5685
5840
  exitCode: 1,
5686
- durationMs: performance.now() - startedAt,
5687
- errorKind: errorKindFromException(error)
5841
+ durationMs,
5842
+ errorKind: errorKindFromException(error),
5843
+ errorName: identity.error_name,
5844
+ errorCode: identity.error_code
5688
5845
  }),
5689
5846
  config: start.config
5690
5847
  });
5848
+ markErrorReported(error);
5691
5849
  await flushTelemetry();
5692
5850
  throw error;
5693
5851
  }
@@ -6263,14 +6421,17 @@ const renderRailStep = (step, deps = {}) => {
6263
6421
  * between the last step and the footer.
6264
6422
  */
6265
6423
  const renderRailConnector = (deps = {}) => {
6266
- return ` ${style$1(deps.theme ?? theme, "accentDim", (deps.symbols ?? symbols).rail)}\n`;
6424
+ const t = deps.theme ?? theme;
6425
+ const s = deps.symbols ?? symbols;
6426
+ return ` ${style$1(t, "accentDim", s.rail)}\n`;
6267
6427
  };
6268
6428
  /**
6269
6429
  * Render the rail-end footer line ("└ <footer text>").
6270
6430
  */
6271
6431
  const renderRailFooter = (footer, deps = {}) => {
6272
6432
  const t = deps.theme ?? theme;
6273
- return ` ${style$1(t, "accentDim", (deps.symbols ?? symbols).railEnd)} ${highlightAislop(footer, t)}\n`;
6433
+ const s = deps.symbols ?? symbols;
6434
+ return ` ${style$1(t, "accentDim", s.railEnd)} ${highlightAislop(footer, t)}\n`;
6274
6435
  };
6275
6436
  const renderRail = (input, deps = {}) => {
6276
6437
  const t = deps.theme ?? theme;
@@ -6427,7 +6588,7 @@ const launchMonitorInBackground = async (directory, options) => {
6427
6588
  fs.mkdirSync(logDir, { recursive: true });
6428
6589
  const logPath = path.join(logDir, `${monitorId}.log`);
6429
6590
  const logFd = fs.openSync(logPath, "a");
6430
- writeAgentMonitorRecord(root, {
6591
+ const record = {
6431
6592
  id: monitorId,
6432
6593
  root,
6433
6594
  requestedDirectory: path.resolve(directory),
@@ -6445,7 +6606,8 @@ const launchMonitorInBackground = async (directory, options) => {
6445
6606
  noFix: options.noFix,
6446
6607
  logPath,
6447
6608
  recentCycles: []
6448
- });
6609
+ };
6610
+ writeAgentMonitorRecord(root, record);
6449
6611
  const child = spawn(process.execPath, [cliPath(), ...buildBackgroundMonitorArgs(directory, options)], {
6450
6612
  cwd: root,
6451
6613
  detached: true,
@@ -6849,6 +7011,13 @@ const registerAgentMonitorSubcommands = (agent) => {
6849
7011
  registerMonitorStopCommand(monitor);
6850
7012
  };
6851
7013
  //#endregion
7014
+ //#region src/ui/prompts.ts
7015
+ const runCancellable = async (fn) => {
7016
+ const value = await fn();
7017
+ if (isCancel(value)) return void 0;
7018
+ return value;
7019
+ };
7020
+ //#endregion
6852
7021
  //#region src/commands/agent-apply.ts
6853
7022
  const asString = (value) => typeof value === "string" && value.length > 0 ? value : null;
6854
7023
  const isInside = (base, target) => {
@@ -7687,7 +7856,8 @@ const registerPlanSubcommand = (agent) => {
7687
7856
  const plan = agent.command("plan [directory]").description("Preview provider, worktree, findings, and publish actions without editing");
7688
7857
  addAgentOptions(plan);
7689
7858
  plan.action(async (directory = ".", _flags, command) => {
7690
- const options = agentOptionsFromFlags(command.optsWithGlobals(), command);
7859
+ const flags = command.optsWithGlobals();
7860
+ const options = agentOptionsFromFlags(flags, command);
7691
7861
  await withAgentLifecycle("agent_plan", directory, agentTelemetryProperties(options), async () => {
7692
7862
  await agentPlanCommand(directory, options);
7693
7863
  });
@@ -7697,7 +7867,8 @@ const registerAgentCommand = (program) => {
7697
7867
  const agent = program.command("agent [directory]").description("Run a local AI slop repair session with Codex, Claude Code, or OpenCode");
7698
7868
  addAgentOptions(agent);
7699
7869
  agent.action(async (directory = ".", _flags, command) => {
7700
- const options = agentOptionsFromFlags(command.optsWithGlobals(), command);
7870
+ const flags = command.optsWithGlobals();
7871
+ const options = agentOptionsFromFlags(flags, command);
7701
7872
  await withAgentLifecycle("agent", directory, agentTelemetryProperties(options), async () => {
7702
7873
  return await agentCommand(directory, options);
7703
7874
  });
@@ -7909,7 +8080,11 @@ const whoamiCommand = async () => {
7909
8080
  process.stdout.write(` Organization: ${orgName} (${orgId})\n`);
7910
8081
  if (userEmail) process.stdout.write(` User: ${userEmail}\n`);
7911
8082
  process.stdout.write(` Credential: ${active.kind === "api_key" ? "Org API Key" : "User Session"} (via ${active.source})\n`);
7912
- process.stdout.write(` Plan: ${plan}${trialEnd ? ` (trial ends ${trialEnd})` : ""}\n`);
8083
+ let planDisplay = plan;
8084
+ if (plan === "active") planDisplay = "active (enterprise license)";
8085
+ else if (plan === "expired") planDisplay = `expired${trialEnd ? ` (trial ended ${trialEnd})` : ""}`;
8086
+ else if (trialEnd) planDisplay = `trial (trial ends ${trialEnd})`;
8087
+ process.stdout.write(` Plan: ${planDisplay}\n`);
7913
8088
  process.stdout.write(` Allowed Owners: ${owners || "none"}\n`);
7914
8089
  if (claims) {
7915
8090
  const expDate = (/* @__PURE__ */ new Date(claims.exp * 1e3)).toISOString();
@@ -7985,9 +8160,9 @@ const buildSuggestedActions = (diagnostics, findings, regressed, delta) => {
7985
8160
  if (fixableDiags.length > 0) {
7986
8161
  const ruleIds = Array.from(new Set(fixableDiags.map((d) => d.rule)));
7987
8162
  actions.push({
7988
- id: "run_aislop_fix",
7989
- label: `Run aislop fix to clear ${fixableDiags.length} mechanical finding${fixableDiags.length === 1 ? "" : "s"}.`,
7990
- command: "aislop fix",
8163
+ id: "run_raigal_fix",
8164
+ label: `Run raigal fix to clear ${fixableDiags.length} mechanical finding${fixableDiags.length === 1 ? "" : "s"}.`,
8165
+ command: "raigal fix",
7991
8166
  rationale: "These findings have deterministic fixes (formatting, unused imports, trivial comments). Running this before any manual work avoids burning agent tokens on what the CLI handles for free.",
7992
8167
  ruleIds
7993
8168
  });
@@ -8048,8 +8223,40 @@ const buildFeedback = (diagnostics, score, rootDirectory, baseline, meta) => {
8048
8223
  const regressed = typeof delta === "number" ? delta < 0 : false;
8049
8224
  let newSinceBaseline;
8050
8225
  if (baselineSnapshot && baselineSnapshot.findingFingerprints.length > 0) {
8051
- const known = new Set(baselineSnapshot.findingFingerprints);
8052
- newSinceBaseline = all.filter((f) => !known.has(fingerprintFinding(f))).slice(0, MAX_NEW_SINCE_BASELINE);
8226
+ const remainingExact = new Set(baselineSnapshot.findingFingerprints);
8227
+ const unmatchedFindings = [];
8228
+ for (const f of all) {
8229
+ const exactFp = fingerprintFinding(f);
8230
+ if (remainingExact.has(exactFp)) remainingExact.delete(exactFp);
8231
+ else unmatchedFindings.push(f);
8232
+ }
8233
+ const remainingPool = [];
8234
+ for (const fp of remainingExact) {
8235
+ const parts = fp.split(":");
8236
+ if (parts.length >= 3) {
8237
+ const file = parts[0];
8238
+ const line = Number.parseInt(parts[1], 10);
8239
+ const rule = parts.slice(2).join(":");
8240
+ remainingPool.push({
8241
+ file,
8242
+ line: Number.isNaN(line) ? 0 : line,
8243
+ rule
8244
+ });
8245
+ }
8246
+ }
8247
+ const stillUnmatched = [];
8248
+ for (const f of unmatchedFindings) {
8249
+ const candidateIdx = remainingPool.findIndex((b) => b.file === f.file && b.rule === f.ruleId && Math.abs(b.line - f.line) <= 5);
8250
+ if (candidateIdx >= 0) remainingPool.splice(candidateIdx, 1);
8251
+ else stillUnmatched.push(f);
8252
+ }
8253
+ const fresh = [];
8254
+ for (const f of stillUnmatched) {
8255
+ const candidateIdx = remainingPool.findIndex((b) => b.file === f.file && b.rule === f.ruleId);
8256
+ if (candidateIdx >= 0) remainingPool.splice(candidateIdx, 1);
8257
+ else fresh.push(f);
8258
+ }
8259
+ newSinceBaseline = fresh.slice(0, MAX_NEW_SINCE_BASELINE);
8053
8260
  }
8054
8261
  return {
8055
8262
  schema: "aislop.hook.v2",
@@ -8088,25 +8295,151 @@ const readLock = (target) => {
8088
8295
  return null;
8089
8296
  }
8090
8297
  };
8091
- const acquireHookLock = (cwd) => {
8092
- const target = lockPath(cwd);
8093
- const existing = readLock(target);
8094
- if (existing && Date.now() - existing.ts < STALE_MS) return null;
8095
- try {
8096
- fs.mkdirSync(path.dirname(target), { recursive: true });
8097
- fs.writeFileSync(target, JSON.stringify({
8098
- pid: process.pid,
8099
- ts: Date.now()
8100
- }));
8101
- } catch {
8102
- return null;
8298
+ const acquireHookLock = (cwd) => {
8299
+ const target = lockPath(cwd);
8300
+ const existing = readLock(target);
8301
+ if (existing && Date.now() - existing.ts < STALE_MS) return null;
8302
+ try {
8303
+ fs.mkdirSync(path.dirname(target), { recursive: true });
8304
+ fs.writeFileSync(target, JSON.stringify({
8305
+ pid: process.pid,
8306
+ ts: Date.now()
8307
+ }));
8308
+ } catch {
8309
+ return null;
8310
+ }
8311
+ return () => {
8312
+ try {
8313
+ if (readLock(target)?.pid === process.pid) fs.unlinkSync(target);
8314
+ } catch {}
8315
+ };
8316
+ };
8317
+ //#endregion
8318
+ //#region src/utils/globs.ts
8319
+ const MAX_BRACE_EXPANSIONS = 256;
8320
+ const braceListAt = (pattern, start) => {
8321
+ let depth = 0;
8322
+ const commas = [];
8323
+ for (let i = start; i < pattern.length; i += 1) {
8324
+ const char = pattern[i];
8325
+ if (char === "\\") i += 1;
8326
+ else if (char === "{") depth += 1;
8327
+ else if (char === ",") {
8328
+ if (depth === 1) commas.push(i);
8329
+ } else if (char === "}") {
8330
+ depth -= 1;
8331
+ if (depth === 0) return commas.length > 0 ? {
8332
+ start,
8333
+ end: i,
8334
+ commas
8335
+ } : null;
8336
+ }
8337
+ }
8338
+ return null;
8339
+ };
8340
+ const findBraceList = (pattern) => {
8341
+ for (let start = pattern.indexOf("{"); start !== -1; start = pattern.indexOf("{", start + 1)) {
8342
+ if (start > 0 && pattern[start - 1] === "\\") continue;
8343
+ const list = braceListAt(pattern, start);
8344
+ if (list) return list;
8345
+ }
8346
+ return null;
8347
+ };
8348
+ const expandInto = (pattern, results) => {
8349
+ const list = findBraceList(pattern);
8350
+ if (!list) {
8351
+ results.push(pattern);
8352
+ return results.length <= MAX_BRACE_EXPANSIONS;
8353
+ }
8354
+ const bounds = [
8355
+ list.start,
8356
+ ...list.commas,
8357
+ list.end
8358
+ ];
8359
+ const prefix = pattern.slice(0, list.start);
8360
+ const suffix = pattern.slice(list.end + 1);
8361
+ for (let i = 0; i < bounds.length - 1; i += 1) {
8362
+ const option = pattern.slice(bounds[i] + 1, bounds[i + 1]);
8363
+ if (!expandInto(`${prefix}${option}${suffix}`, results)) return false;
8364
+ }
8365
+ return true;
8366
+ };
8367
+ const expandBraceLists = (pattern) => {
8368
+ const results = [];
8369
+ return expandInto(pattern, results) ? results : [pattern];
8370
+ };
8371
+ const isNegated = (pattern) => picomatch.scan(pattern).negated;
8372
+ const matchesGlobList = (value, patterns, options = {}) => {
8373
+ const expanded = patterns.flatMap(expandBraceLists);
8374
+ const positives = expanded.filter((pattern) => !isNegated(pattern));
8375
+ const negatives = expanded.filter(isNegated).map((pattern) => pattern.slice(1));
8376
+ const included = positives.length === 0 || picomatch.isMatch(value, positives, options);
8377
+ const excluded = negatives.length > 0 && picomatch.isMatch(value, negatives, options);
8378
+ return included && !excluded;
8379
+ };
8380
+ //#endregion
8381
+ //#region src/scoring/rule-severity.ts
8382
+ /**
8383
+ * Apply per-rule severity overrides from config: "off" drops the diagnostic,
8384
+ * "error"/"warning" rewrite its severity before scoring and rendering.
8385
+ */
8386
+ const applyRuleSeverities = (diagnostics, overrides) => {
8387
+ if (Object.keys(overrides).length === 0) return diagnostics;
8388
+ const result = [];
8389
+ for (const diagnostic of diagnostics) {
8390
+ const override = overrides[diagnostic.rule];
8391
+ if (!override) {
8392
+ result.push(diagnostic);
8393
+ continue;
8394
+ }
8395
+ if (override === "off") continue;
8396
+ result.push(override === diagnostic.severity ? diagnostic : {
8397
+ ...diagnostic,
8398
+ severity: override
8399
+ });
8400
+ }
8401
+ return result;
8402
+ };
8403
+ //#endregion
8404
+ //#region src/config/overrides.ts
8405
+ const overrideRelativePath = (rootDirectory, filePath) => {
8406
+ const paths = path.win32.isAbsolute(rootDirectory) && !path.posix.isAbsolute(rootDirectory) ? path.win32 : path.posix;
8407
+ const root = rootDirectory.replaceAll("\\", "/");
8408
+ const file = filePath.replaceAll("\\", "/");
8409
+ return paths.normalize(paths.isAbsolute(file) ? paths.relative(root, file) : file).replaceAll("\\", "/");
8410
+ };
8411
+ const matchesEntry = (relativePath, patterns) => matchesGlobList(relativePath, patterns, { dot: true });
8412
+ const resolveFilePolicy = (config, relativePath) => {
8413
+ let quality = config.quality;
8414
+ let rules = config.rules ?? {};
8415
+ if (relativePath === ".." || relativePath.startsWith("../") || path.win32.isAbsolute(relativePath)) return {
8416
+ quality,
8417
+ rules
8418
+ };
8419
+ for (const entry of config.overrides ?? []) {
8420
+ if (!matchesEntry(relativePath, entry.files)) continue;
8421
+ if (entry.quality) quality = {
8422
+ ...quality,
8423
+ ...entry.quality
8424
+ };
8425
+ if (entry.rules) rules = {
8426
+ ...rules,
8427
+ ...entry.rules
8428
+ };
8103
8429
  }
8104
- return () => {
8105
- try {
8106
- if (readLock(target)?.pid === process.pid) fs.unlinkSync(target);
8107
- } catch {}
8430
+ return {
8431
+ quality,
8432
+ rules
8108
8433
  };
8109
8434
  };
8435
+ const applyFileOverrides = (diagnostics, context) => {
8436
+ if (!context.config.overrides?.length) return diagnostics;
8437
+ return diagnostics.flatMap((diagnostic) => {
8438
+ const relativePath = overrideRelativePath(context.rootDirectory, diagnostic.filePath);
8439
+ const { rules } = resolveFilePolicy(context.config, relativePath);
8440
+ return applyRuleSeverities([diagnostic], rules);
8441
+ });
8442
+ };
8110
8443
  const supportedGlobPatterns = (patterns) => patterns.filter((pattern) => pattern.length <= 256);
8111
8444
  const normalizeExcludePatterns = (patterns) => patterns.flatMap((pattern) => {
8112
8445
  const trimmed = pattern.trim();
@@ -8116,13 +8449,13 @@ const normalizeExcludePatterns = (patterns) => patterns.flatMap((pattern) => {
8116
8449
  const normalized = withoutProjectPrefix.slice(0, end);
8117
8450
  if (normalized.length === 0) return [];
8118
8451
  if (normalized.length > 256) return [];
8119
- if (micromatch.scan(normalized).isGlob) return [normalized];
8452
+ if (picomatch.scan(normalized).isGlob) return [normalized];
8120
8453
  if (normalized.startsWith(".") && !normalized.includes("/")) return supportedGlobPatterns([`**/${normalized}`, `**/${normalized}/**`]);
8121
8454
  return supportedGlobPatterns([normalized, `${normalized}/**`]);
8122
8455
  });
8123
8456
  const isPathExcluded = (relativePath, normalizedPatterns) => {
8124
8457
  if (normalizedPatterns.length === 0) return false;
8125
- return micromatch.isMatch(relativePath, normalizedPatterns, { dot: true });
8458
+ return picomatch.isMatch(relativePath, normalizedPatterns, { dot: true });
8126
8459
  };
8127
8460
  const filterExcludedDiagnostics = (diagnostics, rootDirectory, excludePatterns) => {
8128
8461
  if (!excludePatterns || excludePatterns.length === 0) return diagnostics;
@@ -8135,7 +8468,7 @@ const filterExcludedDiagnostics = (diagnostics, rootDirectory, excludePatterns)
8135
8468
  };
8136
8469
  //#endregion
8137
8470
  //#region src/utils/project-path-safety.ts
8138
- const MAX_PROJECT_CONFIG_BYTES = 1024 * 1024;
8471
+ const MAX_PROJECT_CONFIG_BYTES = 1048576;
8139
8472
  const isWithinDirectory$3 = (rootDirectory, candidate) => {
8140
8473
  const relative = path.relative(rootDirectory, candidate);
8141
8474
  return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
@@ -8189,7 +8522,7 @@ const isRootBoundedTarget = (candidate, rootDirectory) => {
8189
8522
  };
8190
8523
  //#endregion
8191
8524
  //#region src/utils/aislop-ignore.ts
8192
- const readRaigalIgnorePatterns = (rootDirectory) => {
8525
+ const readAislopIgnorePatterns = (rootDirectory) => {
8193
8526
  const patterns = [];
8194
8527
  for (const fileName of [".raigalignore", ".aislopignore"]) {
8195
8528
  const targetPath = safeProjectFilePath(path.join(rootDirectory, fileName), rootDirectory);
@@ -8200,7 +8533,6 @@ const readRaigalIgnorePatterns = (rootDirectory) => {
8200
8533
  }
8201
8534
  return patterns;
8202
8535
  };
8203
- const readAislopIgnorePatterns = readRaigalIgnorePatterns;
8204
8536
  //#endregion
8205
8537
  //#region src/utils/generated-files.ts
8206
8538
  const AUTO_GENERATED_PATTERNS = [
@@ -8236,7 +8568,7 @@ const isAutoGenerated = (filePath) => {
8236
8568
  };
8237
8569
  //#endregion
8238
8570
  //#region src/utils/git-ignore.ts
8239
- const MAX_BUFFER$2 = 50 * 1024 * 1024;
8571
+ const MAX_BUFFER$2 = 52428800;
8240
8572
  const NOT_IGNORED_ARGUMENTS = [
8241
8573
  "ls-files",
8242
8574
  "--cached",
@@ -8347,7 +8679,7 @@ const dropGitIgnoredPaths = (rootDirectory, absolutePaths) => {
8347
8679
  };
8348
8680
  //#endregion
8349
8681
  //#region src/utils/project-file-list.ts
8350
- const MAX_BUFFER$1 = 50 * 1024 * 1024;
8682
+ const MAX_BUFFER$1 = 52428800;
8351
8683
  const normalizePruneDirectories = (directories) => new Set([...directories].map((directory) => directory.toLowerCase()));
8352
8684
  const isInPrunedDirectory = (filePath, pruneDirectories) => {
8353
8685
  const pathSegments = filePath.split("/");
@@ -8559,12 +8891,13 @@ const normalizeIncludePatterns = (patterns) => patterns.flatMap((pattern) => {
8559
8891
  const normalized = pattern.trim().replace(/^\.\//, "").replace(/\/$/, "");
8560
8892
  if (normalized === "" || normalized === ".") return ["**"];
8561
8893
  if (normalized.length > 256) return [];
8562
- if (micromatch.scan(normalized).isGlob) return [normalized];
8894
+ if (picomatch.scan(normalized).isGlob) return [normalized];
8563
8895
  return supportedGlobPatterns([normalized, `${normalized}/**`]);
8564
8896
  });
8565
8897
  const createPathMatcher = (patterns) => {
8566
- const matchers = patterns.map((pattern) => micromatch.matcher(pattern, { dot: true }));
8567
- return (filePath) => matchers.some((matches) => matches(filePath));
8898
+ if (patterns.length === 0) return () => false;
8899
+ const isMatch = picomatch(patterns, { dot: true });
8900
+ return (filePath) => isMatch(filePath);
8568
8901
  };
8569
8902
  const filterFiles = (rootDirectory, files, options) => {
8570
8903
  const extraExtensions = new Set(options.extraExtensions);
@@ -9278,7 +9611,7 @@ const tokenizeJs = (content, ext = ".ts") => {
9278
9611
  };
9279
9612
  //#endregion
9280
9613
  //#region src/utils/source-masker.ts
9281
- const JS_EXTS$2 = /* @__PURE__ */ new Set([
9614
+ const JS_EXTS$3 = /* @__PURE__ */ new Set([
9282
9615
  ".ts",
9283
9616
  ".tsx",
9284
9617
  ".js",
@@ -9286,7 +9619,7 @@ const JS_EXTS$2 = /* @__PURE__ */ new Set([
9286
9619
  ".mjs",
9287
9620
  ".cjs"
9288
9621
  ]);
9289
- const PY_EXTS = /* @__PURE__ */ new Set([".py"]);
9622
+ const PY_EXTS$1 = /* @__PURE__ */ new Set([".py"]);
9290
9623
  const RB_EXTS = /* @__PURE__ */ new Set([".rb"]);
9291
9624
  const PHP_EXTS = /* @__PURE__ */ new Set([".php"]);
9292
9625
  const CSHARP_EXTS = /* @__PURE__ */ new Set([".cs"]);
@@ -9310,8 +9643,8 @@ const C_STYLE_COMMENT_EXTS = /* @__PURE__ */ new Set([
9310
9643
  ".zig"
9311
9644
  ]);
9312
9645
  const familyForExt = (ext) => {
9313
- if (JS_EXTS$2.has(ext)) return "js";
9314
- if (PY_EXTS.has(ext)) return "py";
9646
+ if (JS_EXTS$3.has(ext)) return "js";
9647
+ if (PY_EXTS$1.has(ext)) return "py";
9315
9648
  if (RB_EXTS.has(ext)) return "rb";
9316
9649
  if (PHP_EXTS.has(ext)) return "php";
9317
9650
  if (CSHARP_EXTS.has(ext)) return "csharp";
@@ -9581,7 +9914,7 @@ const areLiteralMultisetsEqual = (a, b) => {
9581
9914
  };
9582
9915
  //#endregion
9583
9916
  //#region src/engines/ai-slop/abstractions.ts
9584
- const JS_EXTS$1 = /* @__PURE__ */ new Set([
9917
+ const JS_EXTS$2 = /* @__PURE__ */ new Set([
9585
9918
  ".ts",
9586
9919
  ".tsx",
9587
9920
  ".js",
@@ -9592,11 +9925,11 @@ const JS_EXTS$1 = /* @__PURE__ */ new Set([
9592
9925
  const THIN_WRAPPER_PATTERNS = [
9593
9926
  {
9594
9927
  pattern: /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*(?::\s*\w[^{]*)?\{\s*\n?\s*return\s+\w+\([^)]*\);\s*\n?\s*\}/g,
9595
- extensions: JS_EXTS$1
9928
+ extensions: JS_EXTS$2
9596
9929
  },
9597
9930
  {
9598
9931
  pattern: /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w[^=]*)?\s*=>\s*\w+\([^)]*\);/g,
9599
- extensions: JS_EXTS$1
9932
+ extensions: JS_EXTS$2
9600
9933
  },
9601
9934
  {
9602
9935
  pattern: /def\s+(\w+)\s*\([^)]*\)(?:\s*->[^:]*)?:\s*\n\s+return\s+\w+\([^)]*\)\s*$/gm,
@@ -10844,7 +11177,7 @@ const scriptKindFor$1 = (ext) => {
10844
11177
  }
10845
11178
  };
10846
11179
  const lineFor$1 = (sourceFile, node) => sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
10847
- const makeDiagnostic = (filePath, line, rule, message, help) => ({
11180
+ const makeDiagnostic$1 = (filePath, line, rule, message, help) => ({
10848
11181
  filePath,
10849
11182
  engine: "ai-slop",
10850
11183
  rule,
@@ -10894,7 +11227,7 @@ const detectRedundantTryCatch = (sourceFile, relativePath) => {
10894
11227
  if (ts.isTryStatement(node) && node.catchClause && !node.finallyBlock) {
10895
11228
  const catchNameNode = node.catchClause.variableDeclaration?.name;
10896
11229
  const [onlyStatement] = node.catchClause.block.statements;
10897
- 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."));
11230
+ 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."));
10898
11231
  }
10899
11232
  ts.forEachChild(node, visit);
10900
11233
  };
@@ -10912,7 +11245,7 @@ const detectPrimitiveCoercions = (sourceFile, relativePath) => {
10912
11245
  const [arg] = child.arguments;
10913
11246
  if (arg && ts.isIdentifier(arg)) {
10914
11247
  const primitive = params.get(arg.text);
10915
- 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."));
11248
+ 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."));
10916
11249
  }
10917
11250
  }
10918
11251
  ts.forEachChild(child, visitBody);
@@ -10955,7 +11288,7 @@ const detectDuplicateExportedTypes = (parsedSources) => {
10955
11288
  continue;
10956
11289
  }
10957
11290
  if (previous.filePath === declaration.filePath) continue;
10958
- 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.`));
11291
+ 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.`));
10959
11292
  }
10960
11293
  }
10961
11294
  return diagnostics;
@@ -11057,7 +11390,7 @@ const detectDuplicateImports = async (context) => {
11057
11390
  rule: "ai-slop/duplicate-import",
11058
11391
  severity: "warning",
11059
11392
  message: `"${spec}" is also imported on line ${firstLine}. Merge into a single import statement.`,
11060
- help: "Two imports from the same module split readers' attention and grow the import block. Run aislop fix to merge them automatically.",
11393
+ help: "Two imports from the same module split readers' attention and grow the import block. Run raigal fix to merge them automatically.",
11061
11394
  line: dup.line,
11062
11395
  column: 1,
11063
11396
  category: "AI Slop",
@@ -12765,7 +13098,8 @@ const isJsRelativeOrAbsolute = (spec) => spec.startsWith(".") || spec.startsWith
12765
13098
  const RUNTIME_BUILTINS = /* @__PURE__ */ new Set(["bun"]);
12766
13099
  const isJsBuiltin = (spec) => {
12767
13100
  if (RUNTIME_BUILTINS.has(spec)) return true;
12768
- return isBuiltin(spec.startsWith("node:") ? spec.slice(5) : spec) || isBuiltin(spec);
13101
+ const stripped = spec.startsWith("node:") ? spec.slice(5) : spec;
13102
+ return isBuiltin(stripped) || isBuiltin(spec);
12769
13103
  };
12770
13104
  const VIRTUAL_MODULE_PREFIXES = [
12771
13105
  "astro:",
@@ -12977,7 +13311,8 @@ const isLocalAliasReplacement = (rawValue, configDirectory, rootDirectory) => {
12977
13311
  if (!target) return false;
12978
13312
  if (!target.startsWith(".") && !target.startsWith("/") && !target.startsWith("~/") && !target.startsWith("@/")) return false;
12979
13313
  const normalized = target.startsWith("~/") || target.startsWith("@/") ? target.slice(2) : target;
12980
- return isRootBoundedTarget(path.isAbsolute(normalized) ? normalized : path.resolve(configDirectory, normalized), rootDirectory);
13314
+ const candidate = path.isAbsolute(normalized) ? normalized : path.resolve(configDirectory, normalized);
13315
+ return isRootBoundedTarget(candidate, rootDirectory);
12981
13316
  };
12982
13317
  const hasValidPackageImportTarget = (value, packageDirectory, rootDirectory) => {
12983
13318
  if (!isRootBoundedTarget(packageDirectory, rootDirectory)) return false;
@@ -14171,7 +14506,9 @@ const isRustDocCommentLine = (line) => {
14171
14506
  };
14172
14507
  const collectBlocks = (sourceLines, syntax, content, ext) => {
14173
14508
  const blocks = [];
14174
- const commentLines = getCommentLines(content ?? sourceLines.join("\n"), ext ?? (syntax.linePrefixes.includes("//") ? ".ts" : ".py"));
14509
+ const actualContent = content ?? sourceLines.join("\n");
14510
+ const actualExt = ext ?? (syntax.linePrefixes.includes("//") ? ".ts" : ".py");
14511
+ const commentLines = getCommentLines(actualContent, actualExt);
14175
14512
  let i = 0;
14176
14513
  while (i < sourceLines.length) {
14177
14514
  const line = sourceLines[i];
@@ -14499,7 +14836,8 @@ const detectNarrativeComments = async (context) => {
14499
14836
  } catch {
14500
14837
  continue;
14501
14838
  }
14502
- const blocks = collectBlocks(content.split("\n"), syntax, content, ext);
14839
+ const lines = content.split("\n");
14840
+ const blocks = collectBlocks(lines, syntax, content, ext);
14503
14841
  for (const block of blocks) {
14504
14842
  const { matched, reason } = detectNarrativeInBlock(block, ext);
14505
14843
  if (!matched) continue;
@@ -14971,7 +15309,7 @@ const detectRustPatterns = async (context) => {
14971
15309
  };
14972
15310
  //#endregion
14973
15311
  //#region src/engines/ai-slop/silent-recovery.ts
14974
- const JS_EXTS = /* @__PURE__ */ new Set([
15312
+ const JS_EXTS$1 = /* @__PURE__ */ new Set([
14975
15313
  ".ts",
14976
15314
  ".tsx",
14977
15315
  ".js",
@@ -15166,7 +15504,7 @@ const detectSilentRecovery = async (context) => {
15166
15504
  for (const filePath of files) {
15167
15505
  if (isAutoGenerated(filePath)) continue;
15168
15506
  const ext = path.extname(filePath);
15169
- const isJs = JS_EXTS.has(ext);
15507
+ const isJs = JS_EXTS$1.has(ext);
15170
15508
  if (!isJs && !(ext === ".py")) continue;
15171
15509
  const relPath = relativePosix(context.rootDirectory, filePath);
15172
15510
  if (isNonProductionPath(relPath)) continue;
@@ -15451,7 +15789,7 @@ const detectUnusedCss = async (context) => {
15451
15789
  rule: "ai-slop/unused-css",
15452
15790
  severity: "warning",
15453
15791
  message: `CSS class '.${def.name}' is defined but never referenced in the codebase`,
15454
- help: "Remove the dead CSS rule, or confirm the class is used dynamically (and add an aislop-ignore comment if so)",
15792
+ help: "Remove the dead CSS rule, or confirm the class is used dynamically (and add a raigal-ignore comment if so)",
15455
15793
  line: def.line,
15456
15794
  column: 0,
15457
15795
  category: "AI Slop",
@@ -16452,7 +16790,8 @@ const checkFunctionDiagnostics = (relativePath, fn, limits, ext) => {
16452
16790
  const results = [];
16453
16791
  const multiplier = functionLocMultiplier(fn, ext);
16454
16792
  const fnMax = functionLocBudget(fn, ext, limits.maxFunctionLoc);
16455
- if (exceedsLimit(fn.lineCount - fn.templateLines, limits.maxFunctionLoc, multiplier)) results.push({
16793
+ const effectiveLineCount = fn.lineCount - fn.templateLines;
16794
+ if (exceedsLimit(effectiveLineCount, limits.maxFunctionLoc, multiplier)) results.push({
16456
16795
  filePath: relativePath,
16457
16796
  engine: "code-quality",
16458
16797
  rule: "complexity/function-too-long",
@@ -16509,10 +16848,10 @@ const checkFileComplexity = (filePath, rootDirectory, limits) => {
16509
16848
  };
16510
16849
  const checkComplexity = async (context) => {
16511
16850
  const files = getSourceFiles(context);
16512
- const limits = context.config.quality;
16513
16851
  const diagnostics = [];
16514
16852
  for (const filePath of files) {
16515
16853
  if (isAutoGenerated(filePath)) continue;
16854
+ const { quality: limits } = resolveFilePolicy(context.config, overrideRelativePath(context.rootDirectory, filePath));
16516
16855
  diagnostics.push(...checkFileComplexity(filePath, context.rootDirectory, limits));
16517
16856
  }
16518
16857
  return diagnostics;
@@ -16542,14 +16881,14 @@ const isTrivialLine = (line) => {
16542
16881
  };
16543
16882
  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)=/;
16544
16883
  const DATA_LITERAL_RE = /^\s*(?:[A-Za-z_$][\w$-]*:\s*(?:["'`[{]|\d|true\b|false\b|null\b)|["'`][^"'`]*["'`],?\s*$|[{}\]],?\s*$|\),?\s*$)/;
16545
- const SUPPRESS_RE = /aislop[- ]ignore(?:-next-block|-file)?\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
16546
- const FILE_SUPPRESS_RE = /aislop[- ]ignore-file\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
16884
+ const SUPPRESS_RE = /(?:raigal|aislop)[- ]ignore(?:-next-block|-file)?\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
16885
+ const FILE_SUPPRESS_RE = /(?:raigal|aislop)[- ]ignore-file\b.*\b(?:duplicate-block|code-quality\/duplicate-block)\b/;
16547
16886
  const fileHasSuppression = (content) => FILE_SUPPRESS_RE.test(content);
16548
16887
  const isLowSignalMarkupWindow = (lines) => {
16549
16888
  return lines.filter((line) => SVG_MARKUP_RE.test(line)).length >= Math.ceil(WINDOW_SIZE / 2);
16550
16889
  };
16551
16890
  const isLowSignalDataWindow = (lines) => {
16552
- return lines.filter((line) => DATA_LITERAL_RE.test(line)).length >= WINDOW_SIZE - 1;
16891
+ return lines.filter((line) => DATA_LITERAL_RE.test(line)).length >= 9;
16553
16892
  };
16554
16893
  const findSuppressedLines = (lines) => {
16555
16894
  const suppressed = /* @__PURE__ */ new Set();
@@ -16581,7 +16920,7 @@ const collectMeaningfulLines = (content) => {
16581
16920
  if (isLowSignalDataWindow(window)) continue;
16582
16921
  if (window.every(isTrivialLine)) continue;
16583
16922
  const normalised = window.map(normaliseLine);
16584
- if (normalised.filter((n) => n.length > 0 && n !== "}" && n !== "{").length < WINDOW_SIZE - 1) continue;
16923
+ if (normalised.filter((n) => n.length > 0 && n !== "}" && n !== "{").length < 9) continue;
16585
16924
  if (new Set(normalised).size < MIN_DISTINCT_LINES) continue;
16586
16925
  hits.push({
16587
16926
  startLine: i + 1,
@@ -16685,8 +17024,8 @@ const shouldIncludeIssue = (issueType, filePath) => {
16685
17024
  return !filePath.replace(/\\/g, "/").includes(".github/workflows/");
16686
17025
  };
16687
17026
  const DEPENDENCY_HELP = {
16688
- dependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
16689
- devDependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
17027
+ dependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `raigal fix`.",
17028
+ devDependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `raigal fix`.",
16690
17029
  unlisted: "This package is imported in code but not declared in package.json. Run `npm install` to add it.",
16691
17030
  unresolved: "This import cannot be resolved. Check for typos or missing packages.",
16692
17031
  binaries: "This binary is used but its package is not in package.json."
@@ -17404,13 +17743,23 @@ const withReportPath = async (run) => {
17404
17743
  };
17405
17744
  const UNSUPPORTED_GLOB_SYNTAX = /[?[\]{}!|()\\]/;
17406
17745
  const isDotnetFormatPath = (pattern) => !pattern.startsWith("-") && !UNSUPPORTED_GLOB_SYNTAX.test(pattern);
17746
+ const expandBraces = (pattern, depth = 0) => {
17747
+ if (depth > 10) return [pattern];
17748
+ const match = /\{([^{}]+)\}/.exec(pattern);
17749
+ if (!match) return [pattern];
17750
+ const [fullMatch, options] = match;
17751
+ const parts = options.split(",");
17752
+ const prefix = pattern.slice(0, match.index);
17753
+ const suffix = pattern.slice(match.index + fullMatch.length);
17754
+ return parts.flatMap((part) => expandBraces(`${prefix}${part}${suffix}`, depth + 1));
17755
+ };
17407
17756
  const buildDotnetFormatExcludeScope = (excludePatterns) => {
17408
17757
  const scope = {
17409
17758
  excludeArguments: [],
17410
17759
  unsupportedPatterns: []
17411
17760
  };
17412
17761
  for (const pattern of normalizeExcludePatterns(excludePatterns ?? [])) {
17413
- const expanded = micromatch.braces(pattern, { expand: true });
17762
+ const expanded = expandBraces(pattern);
17414
17763
  if (expanded.every(isDotnetFormatPath)) scope.excludeArguments.push(...expanded);
17415
17764
  else scope.unsupportedPatterns.push(pattern);
17416
17765
  }
@@ -17635,7 +17984,7 @@ const runGofmt = async (context) => {
17635
17984
  rule: "go-formatting",
17636
17985
  severity: "warning",
17637
17986
  message: "Go file is not formatted correctly",
17638
- help: "Run `aislop fix` to auto-format with gofmt",
17987
+ help: "Run `raigal fix` to auto-format with gofmt",
17639
17988
  line: 0,
17640
17989
  column: 0,
17641
17990
  category: "Format",
@@ -17663,7 +18012,8 @@ const resolvePackageRoot = (startFile) => {
17663
18012
  while (true) {
17664
18013
  const packageJsonPath = path.join(current, "package.json");
17665
18014
  if (fs.existsSync(packageJsonPath)) try {
17666
- if (JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")).name === "aislop") return current;
18015
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
18016
+ if (packageJson.name === "aislop" || packageJson.name === "@methiu/raigal" || packageJson.name === "raigal") return current;
17667
18017
  } catch {}
17668
18018
  const parent = path.dirname(current);
17669
18019
  if (parent === current) break;
@@ -17774,7 +18124,7 @@ const parseRuffFormatOutput = (output, rootDir) => {
17774
18124
  rule: "python-formatting",
17775
18125
  severity: "warning",
17776
18126
  message: "Python file is not formatted correctly",
17777
- help: "Run `aislop fix` to auto-format with ruff",
18127
+ help: "Run `raigal fix` to auto-format with ruff",
17778
18128
  line: 0,
17779
18129
  column: 0,
17780
18130
  category: "Format",
@@ -17786,7 +18136,8 @@ const parseRuffFormatOutput = (output, rootDir) => {
17786
18136
  const fixRuffFormat = async (context) => {
17787
18137
  const targets = context.files ? getPythonTargets(context) : [context.rootDirectory];
17788
18138
  if (context.files && targets.length === 0) return;
17789
- const result = await runSubprocess(resolveToolBinary("ruff"), ["format", ...targets], {
18139
+ const ruffBinary = resolveToolBinary("ruff");
18140
+ const result = await runSubprocess(ruffBinary, ["format", ...targets], {
17790
18141
  cwd: context.rootDirectory,
17791
18142
  timeout: 6e4
17792
18143
  });
@@ -17856,7 +18207,8 @@ const parseClangTidyOutput = (output, rootDirectory) => {
17856
18207
  const runClangTidy = async (context) => {
17857
18208
  const compileCommandsDir = findCompileCommandsDir(context);
17858
18209
  if (!compileCommandsDir) return [];
17859
- const sources = filterSourcesInDatabase(findCppSources(context).filter((f) => CPP_IMPL_EXTENSIONS.has(path.extname(f).toLowerCase())), readCompileCommandsFiles(compileCommandsDir));
18210
+ const implSources = findCppSources(context).filter((f) => CPP_IMPL_EXTENSIONS.has(path.extname(f).toLowerCase()));
18211
+ const sources = filterSourcesInDatabase(implSources, readCompileCommandsFiles(compileCommandsDir));
17860
18212
  if (sources.length === 0) return [];
17861
18213
  const diagnostics = [];
17862
18214
  for (const chunk of chunkFilePaths(sources, CLANG_TIDY_CHUNK_MAX_FILES)) try {
@@ -18226,7 +18578,7 @@ const runExpoDoctor = async (context) => {
18226
18578
  rule: "expo-doctor/config-error",
18227
18579
  severity: "warning",
18228
18580
  message: configError,
18229
- help: "Install project dependencies, then re-run `aislop scan`.",
18581
+ help: "Install project dependencies, then re-run `raigal scan`.",
18230
18582
  line: 0,
18231
18583
  column: 0,
18232
18584
  category: "Expo",
@@ -18692,7 +19044,8 @@ const processFile = (filePath, fileTargets, result) => {
18692
19044
  });
18693
19045
  continue;
18694
19046
  }
18695
- const { edit, skipReason } = computeEdit(sourceFile, classifyIdentifier(best));
19047
+ const shape = classifyIdentifier(best);
19048
+ const { edit, skipReason } = computeEdit(sourceFile, shape);
18696
19049
  if (!edit) {
18697
19050
  pendingSkips.push({
18698
19051
  target,
@@ -19273,7 +19626,7 @@ const normalizeEslintIgnorePatterns = (patterns) => patterns.map((pattern) => pa
19273
19626
  if (!pattern.includes("*") && !path.extname(pattern)) return [`${pattern}/**`];
19274
19627
  return [pattern];
19275
19628
  });
19276
- const isEslintIgnored = (relativePath, patterns) => patterns.length > 0 && micromatch.isMatch(relativePath, patterns, { dot: true });
19629
+ const isEslintIgnored = (relativePath, patterns) => patterns.length > 0 && picomatch.isMatch(relativePath, patterns, { dot: true });
19277
19630
  const getOxlintTargets = (context) => {
19278
19631
  const eslintIgnorePatterns = normalizeEslintIgnorePatterns(collectEslintIgnorePatterns(context.rootDirectory));
19279
19632
  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));
@@ -19507,7 +19860,8 @@ const ruffLintFixArgs = (context, unsafe) => {
19507
19860
  };
19508
19861
  const fixRuffLint = async (context) => {
19509
19862
  if (context.files && getPythonTargets(context).length === 0) return;
19510
- const result = await runSubprocess(resolveToolBinary("ruff"), ruffLintFixArgs(context, false), {
19863
+ const ruffBinary = resolveToolBinary("ruff");
19864
+ const result = await runSubprocess(ruffBinary, ruffLintFixArgs(context, false), {
19511
19865
  cwd: context.rootDirectory,
19512
19866
  timeout: 6e4
19513
19867
  });
@@ -19515,7 +19869,8 @@ const fixRuffLint = async (context) => {
19515
19869
  };
19516
19870
  const fixRuffLintForce = async (context) => {
19517
19871
  if (context.files && getPythonTargets(context).length === 0) return;
19518
- const result = await runSubprocess(resolveToolBinary("ruff"), ruffLintFixArgs(context, true), {
19872
+ const ruffBinary = resolveToolBinary("ruff");
19873
+ const result = await runSubprocess(ruffBinary, ruffLintFixArgs(context, true), {
19519
19874
  cwd: context.rootDirectory,
19520
19875
  timeout: 6e4
19521
19876
  });
@@ -19556,7 +19911,8 @@ const resolveTrustedTscPath = () => {
19556
19911
  };
19557
19912
  const isReferenceOnlyConfig = (tsconfigPath) => {
19558
19913
  try {
19559
- const parsed = parseJsonc(fs.readFileSync(tsconfigPath, "utf-8"));
19914
+ const raw = fs.readFileSync(tsconfigPath, "utf-8");
19915
+ const parsed = parseJsonc(raw);
19560
19916
  if (!parsed) return false;
19561
19917
  return Array.isArray(parsed.references) && !parsed.files && !parsed.include && !parsed.extends;
19562
19918
  } catch {
@@ -19699,6 +20055,174 @@ const lintEngine = {
19699
20055
  }
19700
20056
  };
19701
20057
  //#endregion
20058
+ //#region src/engines/security/ai-security.ts
20059
+ const JS_EXTS = /* @__PURE__ */ new Set([
20060
+ ".ts",
20061
+ ".tsx",
20062
+ ".js",
20063
+ ".jsx",
20064
+ ".mjs",
20065
+ ".cjs"
20066
+ ]);
20067
+ const PY_EXTS = /* @__PURE__ */ new Set([".py"]);
20068
+ const UNICODE_TAG_RE = /[\u{E0000}-\u{E007F}]/gu;
20069
+ const hasRuleIgnore = (lines, lineIndex, ruleSuffix) => {
20070
+ const start = Math.max(0, lineIndex - 2);
20071
+ const re = new RegExp(`(?:raigal|aislop)[- ]ignore(?:-file)?\\b.*(?:${ruleSuffix}|security\\/${ruleSuffix})`, "i");
20072
+ return lines.slice(start, lineIndex + 1).some((line) => re.test(line));
20073
+ };
20074
+ const hasFileIgnore = (lines, ruleSuffix) => {
20075
+ const re = new RegExp(`(?:raigal|aislop)[- ]ignore-file\\b.*(?:${ruleSuffix}|security\\/${ruleSuffix})`, "i");
20076
+ return lines.slice(0, 10).some((line) => re.test(line));
20077
+ };
20078
+ const makeDiagnostic = (filePath, rule, message, help, line, column) => ({
20079
+ filePath,
20080
+ engine: "security",
20081
+ rule,
20082
+ severity: "error",
20083
+ message,
20084
+ help,
20085
+ line,
20086
+ column,
20087
+ category: "Security",
20088
+ fixable: false
20089
+ });
20090
+ const P_BYPASS = "bypassPermissions";
20091
+ const P_EXEC_TYPE = "executor_type";
20092
+ const P_LOCAL = "local";
20093
+ const P_USE_DOCKER = "use_docker";
20094
+ const P_AUTO_RUN = "auto_run";
20095
+ const P_UNSAFE_MODE = "unsafe_mode";
20096
+ const P_PYTHON_REPL = "PythonREPLTool";
20097
+ const UNSANDBOXED_TOOL_PATTERNS = [
20098
+ {
20099
+ pattern: new RegExp(`\\bpermission_?mode["']?\\s*[:=]\\s*["']${P_BYPASS}["']`, "i"),
20100
+ message: `permission_mode '${P_BYPASS}' bypasses agent sandboxing and human confirmation`,
20101
+ help: `Do not use ${P_BYPASS} mode in production; use standard interactive approval`
20102
+ },
20103
+ {
20104
+ pattern: new RegExp(`\\b${P_EXEC_TYPE}["']?\\s*[:=]\\s*["']${P_LOCAL}["']`, "i"),
20105
+ message: `${P_EXEC_TYPE}='${P_LOCAL}' executes LLM-generated code directly on host machine without container isolation`,
20106
+ help: "Configure agent to use DockerCommandLineCodeExecutor or a containerized sandbox environment"
20107
+ },
20108
+ {
20109
+ pattern: new RegExp(`\\b(?:LocalCommandLineCodeExecutor|LocalCodeExecutor)\\s*\\(`),
20110
+ message: "Local code executor executes LLM-generated code on host machine without container isolation",
20111
+ help: "Use DockerCommandLineCodeExecutor or an isolated execution sandbox"
20112
+ },
20113
+ {
20114
+ pattern: new RegExp(`\\b${P_USE_DOCKER}\\s*[:=]\\s*(?:False|false)\\b|["']${P_USE_DOCKER}["']\\s*:\\s*(?:False|false)\\b`),
20115
+ message: `${P_USE_DOCKER}=False disables container sandboxing for agent code execution`,
20116
+ help: `Set ${P_USE_DOCKER}=True to isolate agent execution inside Docker containers`
20117
+ },
20118
+ {
20119
+ pattern: new RegExp(`\\b(?:interpreter\\.)?${P_AUTO_RUN}\\s*[:=]\\s*(?:True|true)\\b`),
20120
+ message: `${P_AUTO_RUN}=True allows autonomous shell execution without human confirmation`,
20121
+ help: `Keep ${P_AUTO_RUN}=False so dangerous commands require explicit human review`
20122
+ },
20123
+ {
20124
+ pattern: new RegExp(`\\bCodeInterpreterTool\\s*\\([^)]*${P_UNSAFE_MODE}\\s*=\\s*(?:True|true)\\b|\\b${P_UNSAFE_MODE}\\s*[:=]\\s*(?:True|true)\\b`),
20125
+ message: `${P_UNSAFE_MODE}=True allows code interpreter tool to execute without Docker isolation`,
20126
+ help: `Disable ${P_UNSAFE_MODE} and isolate code execution in containerized environments`
20127
+ },
20128
+ {
20129
+ pattern: new RegExp(`\\b(?:${P_PYTHON_REPL}|PythonAstREPLTool)\\s*\\(`),
20130
+ message: `${P_PYTHON_REPL} executes arbitrary Python code in host process without container isolation`,
20131
+ help: `Use containerized sandbox execution rather than unconfined ${P_PYTHON_REPL} in production`
20132
+ }
20133
+ ];
20134
+ const P_SYS = "system";
20135
+ const P_DEV = "developer";
20136
+ const P_SM = "SystemMessage";
20137
+ const PROMPT_INJECTION_JS_PATTERNS = [
20138
+ new RegExp(`(?:role\\s*:\\s*["'](?:${P_SYS}|${P_DEV})["'][^}{;]{0,120}?content\\s*:\\s*\`[^\`]*\\$\\{[^}]+\\}[^\`]*\`)`, "g"),
20139
+ new RegExp(`(?:content\\s*:\\s*\`[^\`]*\\$\\{[^}]+\\}[^\`]*\`[^}{;]{0,120}?role\\s*:\\s*["'](?:${P_SYS}|${P_DEV})["'])`, "g"),
20140
+ 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"),
20141
+ 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"),
20142
+ new RegExp(`\\b(?:${P_SYS}|systemPrompt|system_prompt|system_instruction)\\s*:\\s*\`[^\`]*\\$\\{[^}]+\\}[^\`]*\``, "g"),
20143
+ 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"),
20144
+ 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"),
20145
+ new RegExp(`\\[\\s*["']${P_SYS}["']\\s*,\\s*\`[^\`]*\\$\\{[^}]+\\}[^\`]*\`\\s*\\]`, "g")
20146
+ ];
20147
+ const PROMPT_INJECTION_PY_PATTERNS = [
20148
+ 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"),
20149
+ 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"),
20150
+ 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"),
20151
+ 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"),
20152
+ 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"),
20153
+ 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"),
20154
+ new RegExp(`\\b${P_SM}\\s*\\(\\s*(?:content\\s*=\\s*)?f(?:"""[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?"""|\'\'\'[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?\'\'\'|["'][^"'\\n]*?\\{[^}]+\\}[^"'\\n]*?["'])`, "g"),
20155
+ 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"),
20156
+ new RegExp(`\\(\\s*["']${P_SYS}["']\\s*,\\s*f(?:"""[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?"""|\'\'\'[\\s\\S]*?\\{[^}]+\\}[\\s\\S]*?\'\'\'|["'][^"'\\n]*?\\{[^}]+\\}[^"'\\n]*?["'])\\s*\\)`, "g")
20157
+ ];
20158
+ const checkInvisibleUnicode = (relPath, content, lines) => {
20159
+ if (hasFileIgnore(lines, "invisible-unicode-tags")) return [];
20160
+ const results = [];
20161
+ const tagRegex = new RegExp(UNICODE_TAG_RE.source, UNICODE_TAG_RE.flags);
20162
+ for (const match of content.matchAll(tagRegex)) {
20163
+ const matchIndex = match.index ?? 0;
20164
+ const line = content.slice(0, matchIndex).split("\n").length;
20165
+ const col = matchIndex - content.lastIndexOf("\n", matchIndex - 1);
20166
+ if (hasRuleIgnore(lines, line - 1, "invisible-unicode-tags")) continue;
20167
+ 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));
20168
+ }
20169
+ return results;
20170
+ };
20171
+ const checkUnsandboxedTools = (relPath, content, lines) => {
20172
+ if (hasFileIgnore(lines, "unsandboxed-agent-tool")) return [];
20173
+ const results = [];
20174
+ for (const { pattern, message, help } of UNSANDBOXED_TOOL_PATTERNS) {
20175
+ const regex = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
20176
+ for (const match of content.matchAll(regex)) {
20177
+ const matchIndex = match.index ?? 0;
20178
+ const line = content.slice(0, matchIndex).split("\n").length;
20179
+ const col = matchIndex - content.lastIndexOf("\n", matchIndex - 1);
20180
+ if (hasRuleIgnore(lines, line - 1, "unsandboxed-agent-tool")) continue;
20181
+ results.push(makeDiagnostic(relPath, "security/unsandboxed-agent-tool", message, help, line, col));
20182
+ }
20183
+ }
20184
+ return results;
20185
+ };
20186
+ const checkPromptInjection = (relPath, content, lines, isJs) => {
20187
+ if (hasFileIgnore(lines, "prompt-injection")) return [];
20188
+ const results = [];
20189
+ const patterns = isJs ? PROMPT_INJECTION_JS_PATTERNS : PROMPT_INJECTION_PY_PATTERNS;
20190
+ for (const pattern of patterns) {
20191
+ const regex = new RegExp(pattern.source, pattern.flags);
20192
+ for (const match of content.matchAll(regex)) {
20193
+ const matchIndex = match.index ?? 0;
20194
+ const line = content.slice(0, matchIndex).split("\n").length;
20195
+ const col = matchIndex - content.lastIndexOf("\n", matchIndex - 1);
20196
+ if (hasRuleIgnore(lines, line - 1, "prompt-injection")) continue;
20197
+ 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));
20198
+ }
20199
+ }
20200
+ return results;
20201
+ };
20202
+ const scanAiSecurity = async (context) => {
20203
+ const files = getSourceFiles(context);
20204
+ const diagnostics = [];
20205
+ for (const filePath of files) {
20206
+ const ext = path.extname(filePath);
20207
+ let content;
20208
+ try {
20209
+ content = fs.readFileSync(filePath, "utf-8");
20210
+ } catch {
20211
+ continue;
20212
+ }
20213
+ const relativePath = relativePosix(context.rootDirectory, filePath);
20214
+ const lines = content.split("\n");
20215
+ diagnostics.push(...checkInvisibleUnicode(relativePath, content, lines));
20216
+ const isJs = JS_EXTS.has(ext);
20217
+ const isPy = PY_EXTS.has(ext);
20218
+ if (isJs || isPy) {
20219
+ diagnostics.push(...checkUnsandboxedTools(relativePath, content, lines));
20220
+ diagnostics.push(...checkPromptInjection(relativePath, content, lines, isJs));
20221
+ }
20222
+ }
20223
+ return diagnostics;
20224
+ };
20225
+ //#endregion
19702
20226
  //#region src/ui/invocation.ts
19703
20227
  const detectInvocation = () => "raigal";
19704
20228
  //#endregion
@@ -20081,7 +20605,7 @@ const parseJsAudit = (output, source, manifest) => {
20081
20605
  rule: "security/dependency-audit-skipped",
20082
20606
  severity: "info",
20083
20607
  message: `Dependency audit skipped (${source}): lockfile is missing`,
20084
- help: errorDetail ?? "Generate a lockfile, then re-run `aislop scan` for dependency vulnerability checks.",
20608
+ help: errorDetail ?? "Generate a lockfile, then re-run `raigal scan` for dependency vulnerability checks.",
20085
20609
  line: 0,
20086
20610
  column: 0,
20087
20611
  category: "Security",
@@ -20192,7 +20716,8 @@ const readRuntimeDependencies = (rootDir) => {
20192
20716
  collectRuntimeNames(rootManifest, names);
20193
20717
  const globs = [...npmWorkspaceGlobs(rootManifest), ...pnpmWorkspaceGlobs(rootDir)];
20194
20718
  if (globs.length === 0) return { runtimeDependencies: names };
20195
- for (const relativeDir of micromatch(listPackageDirectories(rootDir), globs)) {
20719
+ const isMatch = picomatch(globs, { dot: true });
20720
+ for (const relativeDir of listPackageDirectories(rootDir).filter((dir) => isMatch(dir))) {
20196
20721
  const memberManifest = readJsonFile(path.join(rootDir, relativeDir, "package.json"));
20197
20722
  if (memberManifest) collectRuntimeNames(memberManifest, names);
20198
20723
  }
@@ -20254,10 +20779,11 @@ const auditSkippedDiagnostic = (source, help) => ({
20254
20779
  });
20255
20780
  const runNpmAudit = async (rootDir, timeout) => {
20256
20781
  try {
20257
- return parseJsAudit((await runSubprocess("npm", ["audit", "--json"], {
20782
+ const result = await runSubprocess("npm", ["audit", "--json"], {
20258
20783
  cwd: rootDir,
20259
20784
  timeout
20260
- })).stdout, "npm audit", readRuntimeDependencies(rootDir));
20785
+ });
20786
+ return parseJsAudit(result.stdout, "npm audit", readRuntimeDependencies(rootDir));
20261
20787
  } catch (error) {
20262
20788
  return [auditSkippedDiagnostic("npm audit", `Failed to run npm audit: ${errorMessageOf(error)}`)];
20263
20789
  }
@@ -20278,10 +20804,11 @@ const runBunAudit = async (rootDir, timeout) => {
20278
20804
  const runPnpmAuditWithFallback = async (rootDir, timeout) => {
20279
20805
  const canFallbackToNpm = fs.existsSync(path.join(rootDir, "package-lock.json"));
20280
20806
  try {
20281
- const diagnostics = parseJsAudit((await runSubprocess("pnpm", ["audit", "--json"], {
20807
+ const result = await runSubprocess("pnpm", ["audit", "--json"], {
20282
20808
  cwd: rootDir,
20283
20809
  timeout
20284
- })).stdout, "pnpm audit", readRuntimeDependencies(rootDir));
20810
+ });
20811
+ const diagnostics = parseJsAudit(result.stdout, "pnpm audit", readRuntimeDependencies(rootDir));
20285
20812
  if (diagnostics.some((d) => d.rule === "security/dependency-audit-skipped")) {
20286
20813
  if (canFallbackToNpm) return runNpmAudit(rootDir, timeout);
20287
20814
  return diagnostics;
@@ -20638,7 +21165,7 @@ const RISKY_PATTERNS = [
20638
21165
  //#region src/engines/security/risky.ts
20639
21166
  const hasDangerouslySetInnerHtmlIgnore = (lines, lineIndex) => {
20640
21167
  const start = Math.max(0, lineIndex - 2);
20641
- return lines.slice(start, lineIndex + 1).some((line) => /(?:biome-ignore|eslint-disable|aislop-ignore).*(?:noDangerouslySetInnerHtml|dangerouslySetInnerHTML|dangerously-set-innerhtml)/i.test(line));
21168
+ return lines.slice(start, lineIndex + 1).some((line) => /(?:biome-ignore|eslint-disable|raigal-ignore|aislop-ignore).*(?:noDangerouslySetInnerHtml|dangerouslySetInnerHTML|dangerously-set-innerhtml)/i.test(line));
20642
21169
  };
20643
21170
  const isStructuredDataScript = (content, matchIndex) => {
20644
21171
  const before = content.slice(Math.max(0, matchIndex - 300), matchIndex);
@@ -20966,7 +21493,11 @@ const ALL_ENGINES = [
20966
21493
  name: "security",
20967
21494
  async run(context) {
20968
21495
  const diagnostics = [];
20969
- const promises = [scanSecrets(context), detectRiskyConstructs(context)];
21496
+ const promises = [
21497
+ scanSecrets(context),
21498
+ detectRiskyConstructs(context),
21499
+ scanAiSecurity(context)
21500
+ ];
20970
21501
  if (context.config.security.audit) promises.push(runDependencyAudit(context));
20971
21502
  const results = await Promise.allSettled(promises);
20972
21503
  for (const result of results) if (result.status === "fulfilled") diagnostics.push(...result.value);
@@ -20988,6 +21519,7 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
20988
21519
  const result = await engine.run(context);
20989
21520
  result.diagnostics = filterExcludedDiagnostics(result.diagnostics, context.rootDirectory, context.excludePatterns);
20990
21521
  result.elapsed = performance.now() - start;
21522
+ result.diagnostics = applyFileOverrides(result.diagnostics, context);
20991
21523
  onComplete?.(result);
20992
21524
  return result;
20993
21525
  } catch (error) {
@@ -20996,6 +21528,7 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
20996
21528
  diagnostics: [],
20997
21529
  elapsed: performance.now() - start,
20998
21530
  skipped: true,
21531
+ failed: true,
20999
21532
  skipReason: error instanceof Error ? error.message : String(error)
21000
21533
  };
21001
21534
  onComplete?.(result);
@@ -21006,9 +21539,13 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
21006
21539
  diagnostics: [],
21007
21540
  elapsed: 0,
21008
21541
  skipped: true,
21542
+ failed: true,
21009
21543
  skipReason: r.reason instanceof Error ? r.reason.message : String(r.reason)
21010
21544
  });
21011
- return dedupeEquivalenceDiagnostics(dedupeOverlappingComments(dedupeCSharpAsync(stripSecretDiagnostics(finalResults))));
21545
+ const stripped = stripSecretDiagnostics(finalResults);
21546
+ const csharp = dedupeCSharpAsync(stripped);
21547
+ const comments = dedupeOverlappingComments(csharp);
21548
+ return dedupeEquivalenceDiagnostics(comments);
21012
21549
  };
21013
21550
  const stripSecretDiagnostics = (results) => {
21014
21551
  for (const result of results) for (const d of result.diagnostics) if (d.redactSource || isSecretClassRule(d.rule)) {
@@ -21154,6 +21691,9 @@ const RULE_SCORE_IMPACTS = {
21154
21691
  "security/shell-injection": strict("Interpolated shell commands can become command execution."),
21155
21692
  "security/unsafe-deserialization": strict("Deserializing untrusted data with a legacy .NET formatter can become remote code execution."),
21156
21693
  "security/unsafe-c-call": strict("Unbounded C string and process calls can become buffer overflows or command execution."),
21694
+ "security/prompt-injection": strict("Dynamic user interpolation in system/developer prompts enables prompt injection attacks."),
21695
+ "security/unsandboxed-agent-tool": strict("Unsandboxed agent code execution or tool permission bypass allows arbitrary host commands."),
21696
+ "security/invisible-unicode-tags": strict("Invisible Unicode Tag characters (U+E0000-U+E007F) hide stealth prompt injection payloads."),
21157
21697
  "security/dependency-audit-skipped": advisory("An unavailable audit is visibility loss, not evidence of a vulnerability."),
21158
21698
  "dotnet/projects-skipped": advisory("Skipping unrestored C# projects is visibility loss, not evidence of a defect."),
21159
21699
  "cppcheck/chunks-skipped": advisory("A skipped cppcheck chunk is visibility loss, not evidence that the skipped files have a defect."),
@@ -21230,7 +21770,8 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
21230
21770
  const capScale = Math.max(1, fileCount.effectiveSourceFileCount / CAP_REFERENCE_FILES);
21231
21771
  const defaultRuleCap = typeof maxPerRule === "number" && maxPerRule > 0 ? maxPerRule : null;
21232
21772
  const capForRule = (key) => {
21233
- const ruleCap = scoreImpactForRule(key.slice(key.indexOf(":") + 1)).cap;
21773
+ const rule = key.slice(key.indexOf(":") + 1);
21774
+ const ruleCap = scoreImpactForRule(rule).cap;
21234
21775
  if (typeof ruleCap === "number") return defaultRuleCap ? Math.min(defaultRuleCap, ruleCap) : ruleCap;
21235
21776
  return defaultRuleCap;
21236
21777
  };
@@ -21246,29 +21787,6 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
21246
21787
  };
21247
21788
  };
21248
21789
  //#endregion
21249
- //#region src/scoring/rule-severity.ts
21250
- /**
21251
- * Apply per-rule severity overrides from config: "off" drops the diagnostic,
21252
- * "error"/"warning" rewrite its severity before scoring and rendering.
21253
- */
21254
- const applyRuleSeverities = (diagnostics, overrides) => {
21255
- if (Object.keys(overrides).length === 0) return diagnostics;
21256
- const result = [];
21257
- for (const diagnostic of diagnostics) {
21258
- const override = overrides[diagnostic.rule];
21259
- if (!override) {
21260
- result.push(diagnostic);
21261
- continue;
21262
- }
21263
- if (override === "off") continue;
21264
- result.push(override === diagnostic.severity ? diagnostic : {
21265
- ...diagnostic,
21266
- severity: override
21267
- });
21268
- }
21269
- return result;
21270
- };
21271
- //#endregion
21272
21790
  //#region src/utils/discovery-classifiers.ts
21273
21791
  const LANGUAGE_SIGNALS = {
21274
21792
  "tsconfig.json": "typescript",
@@ -21366,8 +21884,10 @@ const detectSourceLanguages = (sourceFiles) => {
21366
21884
  const detectLanguages = (directory, sourceFiles) => {
21367
21885
  const languages = new Set(detectSourceLanguages(sourceFiles));
21368
21886
  for (const [file, lang] of Object.entries(LANGUAGE_SIGNALS)) if (fs.existsSync(path.join(directory, file))) languages.add(lang);
21369
- if (readPackageJson(path.join(directory, "package.json"))) if (fs.existsSync(path.join(directory, "tsconfig.json"))) languages.add("typescript");
21370
- else languages.add("javascript");
21887
+ if (readPackageJson(path.join(directory, "package.json"))) {
21888
+ if (fs.existsSync(path.join(directory, "tsconfig.json"))) languages.add("typescript");
21889
+ else languages.add("javascript");
21890
+ }
21371
21891
  for (const signal of PYTHON_SIGNALS) if (fs.existsSync(path.join(directory, signal))) {
21372
21892
  languages.add("python");
21373
21893
  break;
@@ -21594,7 +22114,7 @@ const runScopedScan = async (cwd, filePaths) => {
21594
22114
  const rulesPath = configDir ? path.join(configDir, RULES_FILE) : void 0;
21595
22115
  const enumeratedPaths = new Set(projectCandidates.map((filePath) => path.resolve(rootDirectory, filePath)));
21596
22116
  const scopedCandidates = filePaths.filter((filePath) => enumeratedPaths.has(path.resolve(rootDirectory, filePath)));
21597
- const diagnostics = applyRuleSeverities((await runEngines({
22117
+ const context = {
21598
22118
  rootDirectory: project.rootDirectory,
21599
22119
  languages: project.languages,
21600
22120
  frameworks: project.frameworks,
@@ -21603,6 +22123,8 @@ const runScopedScan = async (cwd, filePaths) => {
21603
22123
  projectFiles: [.../* @__PURE__ */ new Set([...projectSourceFiles, ...filterProjectDeclarationFiles(project.rootDirectory, projectCandidates, excludePatterns, config.include)])],
21604
22124
  installedTools: project.installedTools,
21605
22125
  config: {
22126
+ overrides: config.overrides,
22127
+ rules: config.rules,
21606
22128
  quality: config.quality,
21607
22129
  security: {
21608
22130
  audit: false,
@@ -21615,14 +22137,17 @@ const runScopedScan = async (cwd, filePaths) => {
21615
22137
  allowProjectLocalTools: false,
21616
22138
  architectureRulesPath: config.engines.architecture ? rulesPath : void 0
21617
22139
  }
21618
- }, {
22140
+ };
22141
+ const enabled = {
21619
22142
  format: false,
21620
22143
  lint: false,
21621
22144
  "code-quality": config.engines["code-quality"],
21622
22145
  "ai-slop": config.engines["ai-slop"],
21623
22146
  architecture: config.engines.architecture,
21624
22147
  security: false
21625
- })).flatMap((result) => result.diagnostics), config.rules);
22148
+ };
22149
+ const results = await runEngines(context, enabled);
22150
+ const diagnostics = applyRuleSeverities(results.flatMap((result) => result.diagnostics), config.overrides.length ? {} : config.rules);
21626
22151
  const { score } = calculateScore(diagnostics, config.scoring.weights, config.scoring.thresholds, projectSourceFiles.length + projectTestFiles.length, config.scoring.smoothing, config.scoring.maxPerRule);
21627
22152
  return {
21628
22153
  diagnostics,
@@ -21680,14 +22205,26 @@ const hasExistingLiteralBackslashPath = (cwd, fingerprint) => {
21680
22205
  }
21681
22206
  };
21682
22207
  const migrateMarkerlessFingerprint = (cwd, fingerprint) => hasExistingLiteralBackslashPath(cwd, fingerprint) ? fingerprint : normalizeLegacyFingerprint(fingerprint);
21683
- const BASELINE_REL = path.join(".aislop", "baseline.json");
21684
- const baselinePath = (cwd) => path.join(cwd, BASELINE_REL);
22208
+ const baselinePath = (cwd) => {
22209
+ const raigalCandidate = path.join(cwd, ".raigal", "baseline.json");
22210
+ if (fs.existsSync(raigalCandidate)) return raigalCandidate;
22211
+ const raigalDir = path.join(cwd, ".raigal");
22212
+ if (fs.existsSync(raigalDir)) return raigalCandidate;
22213
+ return path.join(cwd, ".aislop", "baseline.json");
22214
+ };
22215
+ const sessionPath = (cwd) => {
22216
+ const raigalCandidate = path.join(cwd, ".raigal", "session.jsonl");
22217
+ if (fs.existsSync(raigalCandidate)) return raigalCandidate;
22218
+ const raigalDir = path.join(cwd, ".raigal");
22219
+ if (fs.existsSync(raigalDir)) return raigalCandidate;
22220
+ return path.join(cwd, ".aislop", "session.jsonl");
22221
+ };
21685
22222
  const readBaseline = (cwd) => {
21686
22223
  const raw = readIfExists(baselinePath(cwd));
21687
22224
  if (!raw) return null;
21688
22225
  try {
21689
22226
  const parsed = JSON.parse(raw);
21690
- if (parsed.schema !== "aislop.baseline.v2" && parsed.schema !== "aislop.baseline.v1") return null;
22227
+ if (parsed.schema !== "aislop.baseline.v2" && parsed.schema !== "aislop.baseline.v1" && parsed.schema !== "raigal.baseline.v2" && parsed.schema !== "raigal.baseline.v1") return null;
21691
22228
  const findingFingerprints = parsed.findingFingerprints ?? [];
21692
22229
  const needsWindowsMigration = parsed.pathFormat !== "posix";
21693
22230
  return {
@@ -21719,13 +22256,14 @@ const captureBaseline = async (cwd) => {
21719
22256
  });
21720
22257
  const project = await discoverProject(cwd);
21721
22258
  const config = loadConfig(cwd);
21722
- const results = await runEngines({
22259
+ const context = {
21723
22260
  rootDirectory: project.rootDirectory,
21724
22261
  languages: project.languages,
21725
22262
  frameworks: project.frameworks,
21726
- files: [],
21727
22263
  installedTools: project.installedTools,
21728
22264
  config: {
22265
+ overrides: config.overrides,
22266
+ rules: config.rules,
21729
22267
  quality: config.quality,
21730
22268
  security: {
21731
22269
  audit: false,
@@ -21737,19 +22275,22 @@ const captureBaseline = async (cwd) => {
21737
22275
  },
21738
22276
  allowProjectLocalTools: false
21739
22277
  }
21740
- }, {
22278
+ };
22279
+ const enabled = {
21741
22280
  format: false,
21742
22281
  lint: false,
21743
22282
  "code-quality": config.engines["code-quality"],
21744
22283
  "ai-slop": config.engines["ai-slop"],
21745
22284
  architecture: config.engines.architecture,
21746
22285
  security: false
21747
- });
21748
- const diagnostics = results.flatMap((r) => r.diagnostics);
22286
+ };
22287
+ const results = await runEngines(context, enabled);
22288
+ const diagnostics = applyRuleSeverities(results.flatMap((r) => r.diagnostics), config.overrides.length ? {} : config.rules);
21749
22289
  const { score } = calculateScore(diagnostics, config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing, config.scoring.maxPerRule);
21750
22290
  const byEngine = {};
21751
22291
  for (const r of results) {
21752
- const { score: engineScore } = calculateScore(diagnostics.filter((d) => r.diagnostics.includes(d)), config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing, config.scoring.maxPerRule);
22292
+ const engineDiags = diagnostics.filter((d) => d.engine === r.engine);
22293
+ const { score: engineScore } = calculateScore(engineDiags, config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing, config.scoring.maxPerRule);
21753
22294
  byEngine[r.engine] = engineScore;
21754
22295
  }
21755
22296
  const findingFingerprints = diagnostics.filter((d) => d.severity === "error" || d.severity === "warning").map((d) => fingerprintDiagnostic(d, project.rootDirectory));
@@ -21771,7 +22312,7 @@ const captureBaseline = async (cwd) => {
21771
22312
  };
21772
22313
  const appendSessionFiles = (cwd, files) => {
21773
22314
  if (files.length === 0) return;
21774
- const target = path.join(cwd, ".aislop", "session.jsonl");
22315
+ const target = sessionPath(cwd);
21775
22316
  try {
21776
22317
  fs.mkdirSync(path.dirname(target), { recursive: true });
21777
22318
  const line = `${JSON.stringify({
@@ -21782,7 +22323,8 @@ const appendSessionFiles = (cwd, files) => {
21782
22323
  } catch {}
21783
22324
  };
21784
22325
  const readSessionFiles = (cwd) => {
21785
- const raw = readIfExists(path.join(cwd, ".aislop", "session.jsonl"));
22326
+ const target = sessionPath(cwd);
22327
+ const raw = readIfExists(target);
21786
22328
  if (!raw) return [];
21787
22329
  const files = /* @__PURE__ */ new Set();
21788
22330
  for (const line of raw.split("\n")) {
@@ -21795,7 +22337,7 @@ const readSessionFiles = (cwd) => {
21795
22337
  return Array.from(files);
21796
22338
  };
21797
22339
  const clearSessionFiles = (cwd) => {
21798
- const target = path.join(cwd, ".aislop", "session.jsonl");
22340
+ const target = sessionPath(cwd);
21799
22341
  try {
21800
22342
  fs.unlinkSync(target);
21801
22343
  } catch {}
@@ -21899,7 +22441,7 @@ const runClaudeFileChangedHook = async (deps = {}) => {
21899
22441
  schema: "aislop.hook.v2",
21900
22442
  event: "file_changed",
21901
22443
  file: changed,
21902
- message: `Watched file changed (${changed}). aislop refreshed the baseline — score: ${result.score}.`,
22444
+ message: `Watched file changed (${changed}). raigal refreshed the baseline - score: ${result.score}.`,
21903
22445
  baseline: {
21904
22446
  score: result.score,
21905
22447
  fileCount: result.fileCount
@@ -22255,7 +22797,9 @@ const applyRemoval = (result, opts, target, nextContent) => {
22255
22797
  //#region src/hooks/install/rules-only.ts
22256
22798
  const installRulesOnly = (opts, paths, summary) => {
22257
22799
  const result = emptyResult();
22258
- const next = upsertMarkdownFence(readIfExists(paths.rules), AISLOP_MD_BODY, sentinelHash(AISLOP_MD_BODY)).nextContent;
22800
+ const existing = readIfExists(paths.rules);
22801
+ const hash = sentinelHash(AISLOP_MD_BODY);
22802
+ const next = upsertMarkdownFence(existing, AISLOP_MD_BODY, hash).nextContent;
22259
22803
  applyContent(result, opts, paths.rules, next, summary);
22260
22804
  if (paths.host && paths.marker) {
22261
22805
  const host = readIfExists(paths.host) ?? "";
@@ -22307,7 +22851,7 @@ const uninstallAntigravity = (opts) => {
22307
22851
  //#endregion
22308
22852
  //#region src/hooks/io/json-patch.ts
22309
22853
  const AISLOP_SENTINEL_KEY = "__aislop";
22310
- const isAislopManaged = (x) => typeof x === "object" && x !== null && "__aislop" in x && x["__aislop"] != null;
22854
+ const isAislopManaged = (x) => typeof x === "object" && x !== null && ("__aislop" in x && x["__aislop"] != null || "__raigal" in x && x["__raigal"] != null);
22311
22855
  const groupIsAislop = (group) => {
22312
22856
  if (typeof group !== "object" || group === null) return false;
22313
22857
  const hooks = group.hooks;
@@ -22362,14 +22906,14 @@ const resolveClaudePaths = (opts) => {
22362
22906
  };
22363
22907
  const buildHookGroup$1 = () => {
22364
22908
  const hashBody = JSON.stringify({
22365
- command: "aislop hook claude",
22909
+ command: "raigal hook claude",
22366
22910
  matcher: "Edit|Write|MultiEdit"
22367
22911
  });
22368
22912
  return {
22369
22913
  matcher: "Edit|Write|MultiEdit",
22370
22914
  hooks: [{
22371
22915
  type: "command",
22372
- command: "aislop hook claude",
22916
+ command: "raigal hook claude",
22373
22917
  [AISLOP_SENTINEL_KEY]: {
22374
22918
  v: 1,
22375
22919
  managed: true,
@@ -22379,12 +22923,12 @@ const buildHookGroup$1 = () => {
22379
22923
  };
22380
22924
  };
22381
22925
  const buildStopHookGroup = () => {
22382
- const hashBody = JSON.stringify({ command: "aislop hook claude --stop" });
22926
+ const hashBody = JSON.stringify({ command: "raigal hook claude --stop" });
22383
22927
  return {
22384
22928
  matcher: "",
22385
22929
  hooks: [{
22386
22930
  type: "command",
22387
- command: "aislop hook claude --stop",
22931
+ command: "raigal hook claude --stop",
22388
22932
  [AISLOP_SENTINEL_KEY]: {
22389
22933
  v: 1,
22390
22934
  managed: true,
@@ -22396,14 +22940,14 @@ const buildStopHookGroup = () => {
22396
22940
  const FILE_CHANGED_MATCHER = ".raigal/config.yml|.raigal/rules.yml|.aislop/config.yml|.aislop/rules.yml|package.json";
22397
22941
  const buildFileChangedHookGroup = () => {
22398
22942
  const hashBody = JSON.stringify({
22399
- command: "aislop hook claude --on-file-changed",
22943
+ command: "raigal hook claude --on-file-changed",
22400
22944
  matcher: FILE_CHANGED_MATCHER
22401
22945
  });
22402
22946
  return {
22403
22947
  matcher: FILE_CHANGED_MATCHER,
22404
22948
  hooks: [{
22405
22949
  type: "command",
22406
- command: "aislop hook claude --on-file-changed",
22950
+ command: "raigal hook claude --on-file-changed",
22407
22951
  [AISLOP_SENTINEL_KEY]: {
22408
22952
  v: 1,
22409
22953
  managed: true,
@@ -22431,7 +22975,8 @@ const installClaude = (opts) => {
22431
22975
  const nextSettings = renderSettings$1(readIfExists(paths.settings), Boolean(opts.qualityGate));
22432
22976
  applyContent(result, opts, paths.settings, nextSettings, "register PostToolUse + FileChanged hooks");
22433
22977
  const mdHash = sentinelHash(AISLOP_MD_BODY);
22434
- const fenced = upsertMarkdownFence(readIfExists(paths.aislopMd), AISLOP_MD_BODY, mdHash);
22978
+ const existingMd = readIfExists(paths.aislopMd);
22979
+ const fenced = upsertMarkdownFence(existingMd, AISLOP_MD_BODY, mdHash);
22435
22980
  applyContent(result, opts, paths.aislopMd, fenced.nextContent, "write AISLOP.md rules");
22436
22981
  const existingClaudeMd = readIfExists(paths.claudeMd) ?? "";
22437
22982
  const marker = "@AISLOP.md";
@@ -22546,11 +23091,11 @@ const resolveCursorPaths = (opts) => {
22546
23091
  };
22547
23092
  const buildHookEntry = () => {
22548
23093
  const hashBody = JSON.stringify({
22549
- command: "aislop hook cursor",
23094
+ command: "raigal hook cursor",
22550
23095
  timeout: 5e3
22551
23096
  });
22552
23097
  return {
22553
- command: "aislop hook cursor",
23098
+ command: "raigal hook cursor",
22554
23099
  type: "command",
22555
23100
  timeout: 5e3,
22556
23101
  [AISLOP_SENTINEL_KEY]: {
@@ -22577,7 +23122,9 @@ const installCursor = (opts) => {
22577
23122
  const nextHooks = renderHooksJson(readIfExists(paths.hooks));
22578
23123
  applyContent(result, opts, paths.hooks, nextHooks, "register afterFileEdit hook");
22579
23124
  if (opts.scope === "project") {
22580
- const rules = upsertMarkdownFence(readIfExists(paths.rules), AISLOP_MD_BODY, sentinelHash(AISLOP_MD_BODY)).nextContent;
23125
+ const existingRules = readIfExists(paths.rules);
23126
+ const hash = sentinelHash(AISLOP_MD_BODY);
23127
+ const rules = upsertMarkdownFence(existingRules, AISLOP_MD_BODY, hash).nextContent;
22581
23128
  applyContent(result, opts, paths.rules, rules, "write .cursor/rules/aislop.mdc");
22582
23129
  }
22583
23130
  return result;
@@ -22617,15 +23164,15 @@ const resolveGeminiPaths = (opts) => {
22617
23164
  };
22618
23165
  const buildHookGroup = () => {
22619
23166
  const hashBody = JSON.stringify({
22620
- command: "aislop hook gemini",
23167
+ command: "raigal hook gemini",
22621
23168
  matcher: "write_file|replace"
22622
23169
  });
22623
23170
  return {
22624
23171
  matcher: "write_file|replace",
22625
23172
  hooks: [{
22626
- name: "aislop",
23173
+ name: "raigal",
22627
23174
  type: "command",
22628
- command: "aislop hook gemini",
23175
+ command: "raigal hook gemini",
22629
23176
  timeout: 5e3,
22630
23177
  [AISLOP_SENTINEL_KEY]: {
22631
23178
  v: 1,
@@ -22650,7 +23197,9 @@ const installGemini = (opts) => {
22650
23197
  const result = emptyResult();
22651
23198
  const next = renderSettings(readIfExists(paths.settings));
22652
23199
  applyContent(result, opts, paths.settings, next, "register AfterTool hook");
22653
- const fenced = upsertMarkdownFence(readIfExists(paths.aislopMd), AISLOP_MD_BODY, sentinelHash(AISLOP_MD_BODY)).nextContent;
23200
+ const existingMd = readIfExists(paths.aislopMd);
23201
+ const hash = sentinelHash(AISLOP_MD_BODY);
23202
+ const fenced = upsertMarkdownFence(existingMd, AISLOP_MD_BODY, hash).nextContent;
22654
23203
  applyContent(result, opts, paths.aislopMd, fenced, "write AISLOP.md rules");
22655
23204
  const existingGeminiMd = readIfExists(paths.geminiMd) ?? "";
22656
23205
  const marker = "@AISLOP.md";
@@ -22716,8 +23265,8 @@ const uninstallKilocode = (opts) => {
22716
23265
  const resolvePiPaths = (opts) => {
22717
23266
  return { extension: opts.scope === "project" ? path.join(opts.cwd, ".pi", "extensions", "aislop.js") : path.join(opts.home, ".pi", "agent", "extensions", "aislop.js") };
22718
23267
  };
22719
- const PI_EXTENSION_SOURCE = `// aislop — auto-generated pi extension. Do not edit by hand.
22720
- // Reinstall with: aislop hook install --pi
23268
+ const PI_EXTENSION_SOURCE = `// raigal — auto-generated pi extension. Do not edit by hand.
23269
+ // Reinstall with: raigal hook install --pi
22721
23270
  import { spawn } from "node:child_process";
22722
23271
 
22723
23272
  // Runs the CLI async so a slow scan never blocks pi's event loop, and forwards
@@ -22762,7 +23311,7 @@ export default function (pi) {
22762
23311
  const filePath = event.input && event.input.path;
22763
23312
  if (typeof filePath !== "string" || filePath.length === 0) return;
22764
23313
 
22765
- const bin = process.env.AISLOP_BIN || "aislop";
23314
+ const bin = process.env.RAIGAL_BIN || process.env.AISLOP_BIN || "raigal";
22766
23315
  const payload = JSON.stringify({
22767
23316
  cwd: ctx.cwd,
22768
23317
  file_path: filePath,
@@ -22789,7 +23338,7 @@ export default function (pi) {
22789
23338
  const installPi = (opts) => {
22790
23339
  const paths = resolvePiPaths(opts);
22791
23340
  const result = emptyResult();
22792
- applyContent(result, opts, paths.extension, PI_EXTENSION_SOURCE, "write pi aislop extension");
23341
+ applyContent(result, opts, paths.extension, PI_EXTENSION_SOURCE, "write pi raigal extension");
22793
23342
  return result;
22794
23343
  };
22795
23344
  const uninstallPi = (opts) => {
@@ -22945,6 +23494,31 @@ const detectInstalledAgents = (opts) => {
22945
23494
  }
22946
23495
  return hits;
22947
23496
  };
23497
+ const HOOK_MARKERS = [
23498
+ "__raigal",
23499
+ "raigal:begin",
23500
+ "@RAIGAL.md",
23501
+ "__aislop",
23502
+ "aislop:begin",
23503
+ "@AISLOP.md"
23504
+ ];
23505
+ const hasHook = (target) => {
23506
+ try {
23507
+ if (!fs.statSync(target).isFile()) return false;
23508
+ const base = path.basename(target).toLowerCase();
23509
+ if (base.startsWith("raigal") || base.startsWith("aislop")) return true;
23510
+ const content = fs.readFileSync(target, "utf8");
23511
+ return HOOK_MARKERS.some((marker) => content.includes(marker));
23512
+ } catch {
23513
+ return false;
23514
+ }
23515
+ };
23516
+ const detectionScopes = (agent) => AGENTS_SUPPORTING_BOTH_SCOPES.includes(agent) ? ["global", "project"] : [defaultScopeFor(agent)];
23517
+ const detectRaigalHooks = (opts) => ALL_AGENTS.filter((agent) => detectionScopes(agent).some((scope) => REGISTRY[agent].paths({
23518
+ home: opts.home,
23519
+ cwd: opts.cwd,
23520
+ scope
23521
+ }).some(hasHook)));
22948
23522
  //#endregion
22949
23523
  //#region src/ui/search-select.ts
22950
23524
  const silentOutput = new Writable({ write(_chunk, _encoding, callback) {
@@ -23123,8 +23697,10 @@ const runSearchPrompt = async (options) => {
23123
23697
  }
23124
23698
  if (key.name === "space" && options.mode === "multi") {
23125
23699
  const item = filtered[cursor];
23126
- if (item) if (selected.has(item.value)) selected.delete(item.value);
23127
- else selected.add(item.value);
23700
+ if (item) {
23701
+ if (selected.has(item.value)) selected.delete(item.value);
23702
+ else selected.add(item.value);
23703
+ }
23128
23704
  render();
23129
23705
  return;
23130
23706
  }
@@ -23388,10 +23964,11 @@ const hookRun = async (agent, flags) => {
23388
23964
  process.exit(0);
23389
23965
  }
23390
23966
  let exitCode = 0;
23391
- if (agent === "claude") if (flags?.onFileChanged) exitCode = await runClaudeFileChangedHook();
23392
- else if (flags?.stop) exitCode = await runClaudeStopHook();
23393
- else exitCode = await runClaudeHook();
23394
- else if (agent === "cursor") exitCode = await runCursorHook();
23967
+ if (agent === "claude") {
23968
+ if (flags?.onFileChanged) exitCode = await runClaudeFileChangedHook();
23969
+ else if (flags?.stop) exitCode = await runClaudeStopHook();
23970
+ else exitCode = await runClaudeHook();
23971
+ } else if (agent === "cursor") exitCode = await runCursorHook();
23395
23972
  else if (agent === "gemini") exitCode = await runGeminiHook();
23396
23973
  else if (agent === "pi") exitCode = await runPiHook();
23397
23974
  else {
@@ -23402,7 +23979,8 @@ const hookRun = async (agent, flags) => {
23402
23979
  process.exit(exitCode);
23403
23980
  };
23404
23981
  const hookBaseline = async () => {
23405
- const result = await captureBaseline(process.cwd());
23982
+ const cwd = process.cwd();
23983
+ const result = await captureBaseline(cwd);
23406
23984
  process.stdout.write(renderHookBaseline({
23407
23985
  score: result.score,
23408
23986
  fileCount: result.fileCount,
@@ -23632,8 +24210,38 @@ const registerHookAliases = (program) => {
23632
24210
  });
23633
24211
  };
23634
24212
  //#endregion
24213
+ //#region src/utils/github-slug.ts
24214
+ const GITHUB_REMOTE_RE = /^(?:(?:git@|ssh:\/\/(?:git@)?)github\.com(?::(?:\d+\/)?|[:/])|https:\/\/(?:[^@]+@)?github\.com\/)([^/]+)\/([^/\s]+?)(?:\.git)?\s*$/;
24215
+ const parseGithubSlug = (remoteUrl) => {
24216
+ const match = remoteUrl.trim().match(GITHUB_REMOTE_RE);
24217
+ if (!match) return null;
24218
+ const owner = match[1];
24219
+ const repo = match[2];
24220
+ if (!owner || !repo) return null;
24221
+ return {
24222
+ owner,
24223
+ repo
24224
+ };
24225
+ };
24226
+ const detectGithubSlug = (directory) => {
24227
+ let raw;
24228
+ try {
24229
+ raw = execSync("git remote get-url origin", {
24230
+ cwd: path.resolve(directory),
24231
+ encoding: "utf-8",
24232
+ stdio: [
24233
+ "ignore",
24234
+ "pipe",
24235
+ "ignore"
24236
+ ]
24237
+ });
24238
+ } catch {
24239
+ return null;
24240
+ }
24241
+ return parseGithubSlug(raw);
24242
+ };
24243
+ //#endregion
23635
24244
  //#region src/commands/badge.ts
23636
- const GITHUB_REMOTE_RE = /^(?:git@github\.com:|https:\/\/(?:[^@]+@)?github\.com\/)([^/]+)\/([^/.\s]+?)(?:\.git)?\s*$/;
23637
24245
  const renderBadgeOutput = ({ owner, repo, svgUrl, pageUrl }) => {
23638
24246
  const slug = `${owner}/${repo}`;
23639
24247
  const markdown = `[![raigal](${svgUrl})](${pageUrl})`;
@@ -23677,31 +24285,6 @@ const renderBadgeOutput = ({ owner, repo, svgUrl, pageUrl }) => {
23677
24285
  ""
23678
24286
  ].join("\n");
23679
24287
  };
23680
- const detectGithubSlugFromGit = (directory) => {
23681
- let raw;
23682
- try {
23683
- raw = execSync("git remote get-url origin", {
23684
- cwd: path.resolve(directory),
23685
- encoding: "utf-8",
23686
- stdio: [
23687
- "ignore",
23688
- "pipe",
23689
- "ignore"
23690
- ]
23691
- });
23692
- } catch {
23693
- return null;
23694
- }
23695
- const match = raw.trim().match(GITHUB_REMOTE_RE);
23696
- if (!match) return null;
23697
- const owner = match[1];
23698
- const repo = match[2];
23699
- if (!owner || !repo) return null;
23700
- return {
23701
- owner,
23702
- repo
23703
- };
23704
- };
23705
24288
  const badgeCommand = async (options = {}) => {
23706
24289
  await requireEntitlement({
23707
24290
  directory: options.directory,
@@ -23710,7 +24293,7 @@ const badgeCommand = async (options = {}) => {
23710
24293
  let owner = options.owner?.trim();
23711
24294
  let repo = options.repo?.trim();
23712
24295
  if (!owner || !repo) {
23713
- const detected = detectGithubSlugFromGit(options.directory ?? ".");
24296
+ const detected = detectGithubSlug(options.directory ?? ".");
23714
24297
  if (!detected) throw new Error("Could not detect a GitHub remote. Run from a repo with `git remote get-url origin` set, or pass --owner and --repo.");
23715
24298
  owner ??= detected.owner;
23716
24299
  repo ??= detected.repo;
@@ -23826,6 +24409,9 @@ const RULE_LABELS = {
23826
24409
  "security/shell-injection": "Possible shell injection",
23827
24410
  "security/unsafe-deserialization": "Unsafe deserialization",
23828
24411
  "security/unsafe-c-call": "Memory-unsafe C call",
24412
+ "security/prompt-injection": "Prompt injection (untrusted system prompt)",
24413
+ "security/unsandboxed-agent-tool": "Unsandboxed agent code execution tool",
24414
+ "security/invisible-unicode-tags": "Invisible Unicode tag character (stealth injection)",
23829
24415
  "dotnet/projects-skipped": "C# projects skipped by build-backed lint",
23830
24416
  "cppcheck/chunks-skipped": "C/C++ files skipped by cppcheck",
23831
24417
  "eslint/no-undef": "Undefined identifier",
@@ -23914,6 +24500,9 @@ const RULE_DESCRIPTIONS = {
23914
24500
  "security/shell-injection": "Shell command is built from unsanitized input.",
23915
24501
  "security/unsafe-deserialization": "Legacy .NET formatter can deserialize arbitrary types (RCE).",
23916
24502
  "security/unsafe-c-call": "Unbounded C string/exec function with buffer-overflow or shell risk.",
24503
+ "security/prompt-injection": "Untrusted dynamic variable interpolation into system or developer prompt.",
24504
+ "security/unsandboxed-agent-tool": "Agent executes code or commands on host without container or interactive gate.",
24505
+ "security/invisible-unicode-tags": "Invisible Unicode Tag block (U+E0000-U+E007F) used for prompt injection or ASCII smuggling.",
23917
24506
  "dotnet/projects-skipped": "Build-backed C# lint only ran on restored projects; the rest were skipped.",
23918
24507
  "cppcheck/chunks-skipped": "A cppcheck invocation timed out or failed on one or more chunks; their files were not analyzed.",
23919
24508
  "oxlint/*": "JavaScript/TypeScript lint finding from oxlint.",
@@ -24108,7 +24697,8 @@ const BUILTIN_RULES = [
24108
24697
  "typescript/*",
24109
24698
  "dotnet/*",
24110
24699
  "cppcheck/*",
24111
- "clang-tidy/*"
24700
+ "clang-tidy/*",
24701
+ "expo-doctor/*"
24112
24702
  ]
24113
24703
  },
24114
24704
  {
@@ -24207,7 +24797,10 @@ const BUILTIN_RULES = [
24207
24797
  "security/shell-injection",
24208
24798
  "security/unsafe-deserialization",
24209
24799
  "security/unsafe-c-call",
24210
- "security/dependency-audit-skipped"
24800
+ "security/dependency-audit-skipped",
24801
+ "security/prompt-injection",
24802
+ "security/unsandboxed-agent-tool",
24803
+ "security/invisible-unicode-tags"
24211
24804
  ]
24212
24805
  }
24213
24806
  ];
@@ -24217,7 +24810,8 @@ const collectRuleEntries = (directory) => {
24217
24810
  for (const { engine, rules } of BUILTIN_RULES) for (const rule of rules) entries.push(toRuleEntry(engine, rule));
24218
24811
  const configDir = findConfigDir(resolvedDir);
24219
24812
  if (configDir) {
24220
- const archRules = loadArchitectureRules(path.join(configDir, RULES_FILE));
24813
+ const rulesPath = path.join(configDir, RULES_FILE);
24814
+ const archRules = loadArchitectureRules(rulesPath);
24221
24815
  for (const rule of archRules) entries.push({
24222
24816
  id: `arch/${rule.name}`,
24223
24817
  engine: "architecture",
@@ -24274,6 +24868,8 @@ const resolveHistoryDir = (directory) => {
24274
24868
  const resolved = path.resolve(directory);
24275
24869
  const primary = path.join(resolved, CONFIG_DIR);
24276
24870
  if (fs.existsSync(primary)) return primary;
24871
+ const legacy = path.join(resolved, ".aislop");
24872
+ if (fs.existsSync(legacy)) return legacy;
24277
24873
  return null;
24278
24874
  };
24279
24875
  const historyPath = (directory) => {
@@ -24413,7 +25009,7 @@ const trendCommand = async (directory, limit) => {
24413
25009
  //#endregion
24414
25010
  //#region src/update-notifier.ts
24415
25011
  const REGISTRY_URL = "https://registry.npmjs.org/@methiu/raigal/latest";
24416
- const CHECK_INTERVAL_MS = 1440 * 60 * 1e3;
25012
+ const CHECK_INTERVAL_MS = 864e5;
24417
25013
  const REQUEST_TIMEOUT_MS = 2e3;
24418
25014
  const CACHE_BASENAME = "update_check.json";
24419
25015
  const isUpdateNotifierDisabled = (env = process.env) => {
@@ -24486,7 +25082,7 @@ const maybeNotifyUpdate = async (now = Date.now()) => {
24486
25082
  if (!process.stderr.isTTY) return;
24487
25083
  const cachePath = resolveUpdateCachePath();
24488
25084
  const cache = readCache(cachePath);
24489
- if (cache && isOutdated("0.2.0", cache.latest)) process.stderr.write(formatUpdateNotice(APP_VERSION, cache.latest));
25085
+ if (cache && isOutdated("0.2.3", cache.latest)) process.stderr.write(formatUpdateNotice(APP_VERSION, cache.latest));
24490
25086
  if (!cache || now - cache.checkedAt > CHECK_INTERVAL_MS) {
24491
25087
  const latest = await fetchLatestVersion$1();
24492
25088
  if (latest) writeCache(cachePath, {
@@ -25006,7 +25602,7 @@ const renderCommandGroup = (group, commandWidth) => {
25006
25602
  return lines;
25007
25603
  };
25008
25604
  const renderCommandReference = (input = {}) => {
25009
- const version = input.version ?? "0.2.0";
25605
+ const version = input.version ?? "0.2.3";
25010
25606
  const commandWidth = Math.max(...COMMAND_REFERENCE.map((item) => item.command.length));
25011
25607
  const lines = [renderHeader({
25012
25608
  version,
@@ -25228,8 +25824,9 @@ const renderHelpDetails = () => [
25228
25824
  ""
25229
25825
  ].map((line) => highlightRaigal(line, theme)).join("\n");
25230
25826
  const renderHome = (input = {}) => {
25827
+ const version = input.version ?? "0.2.3";
25231
25828
  let out = renderHeader({
25232
- version: input.version ?? "0.2.0",
25829
+ version,
25233
25830
  command: "--bare",
25234
25831
  context: []
25235
25832
  });
@@ -25289,10 +25886,10 @@ const registerExtraCommands = (program) => {
25289
25886
  return { exitCode: 0 };
25290
25887
  });
25291
25888
  });
25292
- program.command("update").alias("upgrade").description("Check npm for the latest aislop version").action(async () => {
25889
+ program.command("update").alias("upgrade").description("Check npm for the latest raigal version").action(async () => {
25293
25890
  await updateCommand();
25294
25891
  });
25295
- program.command("version").description("Print the installed aislop version").action(() => {
25892
+ program.command("version").description("Print the installed raigal version").action(() => {
25296
25893
  process.stdout.write(`${APP_VERSION}\n`);
25297
25894
  });
25298
25895
  program.command("commands").description("List all commands and major flags").action(() => {
@@ -25480,7 +26077,8 @@ const saveCachedPolicy = (repoSlug, policy) => {
25480
26077
  };
25481
26078
  const fetchRemotePolicy = async (repoSlug, token) => {
25482
26079
  try {
25483
- const data = await requestApi(`/v1/policy?repo=${encodeURIComponent(repoSlug.toLowerCase())}`, {
26080
+ const endpoint = `/v1/policy?repo=${encodeURIComponent(repoSlug.toLowerCase())}`;
26081
+ const data = await requestApi(endpoint, {
25484
26082
  method: "GET",
25485
26083
  token,
25486
26084
  timeoutMs: 4e3
@@ -25885,7 +26483,7 @@ const printEngineStatus = (result) => {
25885
26483
  };
25886
26484
  //#endregion
25887
26485
  //#region src/utils/git.ts
25888
- const MAX_BUFFER = 50 * 1024 * 1024;
26486
+ const MAX_BUFFER = 52428800;
25889
26487
  const baseRefExists = (cwd, ref) => {
25890
26488
  const result = spawnSync("git", [
25891
26489
  "rev-parse",
@@ -25974,6 +26572,18 @@ const getChangedLineMap = (cwd, base) => {
25974
26572
  return map;
25975
26573
  };
25976
26574
  //#endregion
26575
+ //#region src/commands/scan-hook-nudge.ts
26576
+ const shouldShowHookNudge = (ctx) => ctx.isTty && !ctx.isCi && ctx.installedAgentCount === 0;
26577
+ const buildHookNudge = (ctx) => {
26578
+ if (!shouldShowHookNudge(ctx)) return null;
26579
+ return [
26580
+ "",
26581
+ "Next: install the per-edit hook so your coding agent fixes slop before it lands.",
26582
+ ctx.invocation === "raigal" ? " raigal hook install" : " npm install -g @methiu/raigal && raigal hook install",
26583
+ ""
26584
+ ].join("\n");
26585
+ };
26586
+ //#endregion
25977
26587
  //#region src/commands/scan-coverage.ts
25978
26588
  const coverageReason = (c) => {
25979
26589
  if (c.supportedFiles === 0 && c.dominantUnsupported) return `This repository is ${c.dominantUnsupported} (${c.unsupportedFiles} files), which Raigal does not analyze. No score — it would not reflect this code.`;
@@ -26396,7 +27006,8 @@ const renderActionRows = (steps, t) => {
26396
27006
  const renderTextSteps = (steps, t, s) => {
26397
27007
  return steps.filter((step) => step.text).map((step) => {
26398
27008
  const glyph = step.emphasis === "primary" ? s.hint : s.bullet;
26399
- return ` ${style$1(t, step.emphasis === "primary" ? "accent" : "muted", glyph)} ${highlightAislop(step.text ?? "", t)}`;
27009
+ const tokenFor = step.emphasis === "primary" ? "accent" : "muted";
27010
+ return ` ${style$1(t, tokenFor, glyph)} ${highlightAislop(step.text ?? "", t)}`;
26400
27011
  });
26401
27012
  };
26402
27013
  const renderSummary = (input, deps = {}) => {
@@ -26453,10 +27064,14 @@ const renderSummary = (input, deps = {}) => {
26453
27064
  return lines.join("\n");
26454
27065
  };
26455
27066
  const renderStarCta = (deps = {}) => {
26456
- return `\n ${style$1(deps.theme ?? theme, "muted", `★ Found this useful? Consider purchasing this software at ${terminalLink("https://raigal.dev")}`)}\n`;
27067
+ const t = deps.theme ?? theme;
27068
+ const repository = terminalLink("https://raigal.dev");
27069
+ return `\n ${style$1(t, "muted", `★ Found this useful? Consider purchasing this software at ${repository}`)}\n`;
26457
27070
  };
26458
27071
  const renderTeamCta = (deps = {}) => {
26459
- return `\n ${style$1(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`;
27072
+ const t = deps.theme ?? theme;
27073
+ const href = terminalLink("https://raigal.dev/contact?intent=team-baseline");
27074
+ return `\n ${style$1(t, "muted", `→ Using raigal with a team? Get a 14-day team baseline at ${href}`)}\n`;
26460
27075
  };
26461
27076
  const renderCleanRun = (input, deps = {}) => {
26462
27077
  const t = deps.theme ?? theme;
@@ -26515,7 +27130,7 @@ const buildScanNextSteps = (input) => {
26515
27130
  emphasis: "primary",
26516
27131
  label: "Provider",
26517
27132
  command: `${input.invocation} agent --provider codex`,
26518
- detail: "or use --provider claude/opencode"
27133
+ detail: "or use --provider claude/opencode/pi"
26519
27134
  }, {
26520
27135
  emphasis: "muted",
26521
27136
  label: "Preview",
@@ -26682,13 +27297,15 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
26682
27297
  const configDir = findConfigDir(resolvedDir);
26683
27298
  const rulesPath = configDir ? path.join(configDir, RULES_FILE) : void 0;
26684
27299
  const engineConfig = {
27300
+ overrides: config.overrides,
27301
+ rules: config.rules,
26685
27302
  quality: config.quality,
26686
27303
  security: config.security,
26687
27304
  lint: config.lint,
26688
27305
  architectureRulesPath: config.engines.architecture ? rulesPath : void 0,
26689
27306
  engines: config.engines
26690
27307
  };
26691
- const { results: unannotated, suppressedCount } = applySuppressions((await runEnginesWithProgress({
27308
+ const severityAdjusted = (await runEnginesWithProgress({
26692
27309
  rootDirectory: resolvedDir,
26693
27310
  languages: projectInfo.languages,
26694
27311
  frameworks: projectInfo.frameworks,
@@ -26703,8 +27320,9 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
26703
27320
  config: engineConfig
26704
27321
  }, config.engines, machineOutput)).map((result) => ({
26705
27322
  ...result,
26706
- diagnostics: applyRuleSeverities(result.diagnostics, config.rules)
26707
- })), resolvedDir);
27323
+ diagnostics: config.overrides.length ? result.diagnostics : applyRuleSeverities(result.diagnostics, config.rules)
27324
+ }));
27325
+ const { results: unannotated, suppressedCount } = applySuppressions(severityAdjusted, resolvedDir);
26708
27326
  if (suppressedCount > 0 && !machineOutput) log.muted(`Suppressed ${suppressedCount} finding(s) via raigal-ignore directives`);
26709
27327
  const changeMap = options.changes && !options.staged ? getChangedLineMap(resolvedDir, options.base) : null;
26710
27328
  const results = changeMap ? unannotated.map((result) => ({
@@ -26741,6 +27359,7 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
26741
27359
  engineIssues[r.engine] = r.diagnostics.length;
26742
27360
  engineTimings[r.engine] = Math.round(r.elapsed);
26743
27361
  }
27362
+ const enginesFailed = results.filter((r) => r.failed).map((r) => r.engine);
26744
27363
  const completion = {
26745
27364
  exitCode,
26746
27365
  score: scoreable ? scoreResult.score : null,
@@ -26750,7 +27369,8 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
26750
27369
  warningCount: allDiagnostics.filter((d) => d.severity === "warning").length,
26751
27370
  fixableCount: allDiagnostics.filter((d) => d.fixable).length,
26752
27371
  engineIssues,
26753
- engineTimings
27372
+ engineTimings,
27373
+ ...enginesFailed.length > 0 ? { properties: { engines_failed: enginesFailed } } : {}
26754
27374
  };
26755
27375
  recordScanToCloud({
26756
27376
  results,
@@ -26763,12 +27383,12 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
26763
27383
  scoreFileCount
26764
27384
  });
26765
27385
  if (options.sarif) {
26766
- const { buildSarifLog } = await import("./sarif--DI_2p2r.js");
27386
+ const { buildSarifLog } = await import("./sarif-CdtsAfJ1.js");
26767
27387
  console.log(JSON.stringify(buildSarifLog(results), null, 2));
26768
27388
  return completion;
26769
27389
  }
26770
27390
  if (options.json) {
26771
- const { buildJsonOutput } = await import("./json-C9fBWj3V.js");
27391
+ const { buildJsonOutput } = await import("./json-BaycSoJP.js");
26772
27392
  const jsonOut = buildJsonOutput(results, scoreResult, scoreFileCount, elapsedMs, scanCoverage);
26773
27393
  console.log(JSON.stringify(jsonOut, null, 2));
26774
27394
  return completion;
@@ -26801,6 +27421,18 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
26801
27421
  printBrand: options.printBrand,
26802
27422
  showPilotInvitation
26803
27423
  }));
27424
+ if (options.command !== "ci" && options.printBrand !== false) {
27425
+ const nudge = buildHookNudge({
27426
+ installedAgentCount: detectRaigalHooks({
27427
+ home: os.homedir(),
27428
+ cwd: resolvedDir
27429
+ }).length,
27430
+ isTty: Boolean(process.stdout.isTTY),
27431
+ isCi: isCiEnv(),
27432
+ invocation: detectInvocation()
27433
+ });
27434
+ if (nudge) process.stdout.write(nudge);
27435
+ }
26804
27436
  return completion;
26805
27437
  };
26806
27438
  //#endregion
@@ -27401,7 +28033,8 @@ const groupByFile = (diagnostics) => {
27401
28033
  });
27402
28034
  };
27403
28035
  const isInstalled = (bin) => {
27404
- return spawnSync(process.platform === "win32" ? "where" : "which", [bin], { encoding: "utf-8" }).status === 0;
28036
+ const cmd = process.platform === "win32" ? "where" : "which";
28037
+ return spawnSync(cmd, [bin], { encoding: "utf-8" }).status === 0;
27405
28038
  };
27406
28039
  const copyToClipboard = (text) => {
27407
28040
  const args = {
@@ -27540,7 +28173,9 @@ const createEngineContext = (rootDirectory, projectInfo, config, options = {}) =
27540
28173
  quality: config.quality,
27541
28174
  security: config.security,
27542
28175
  lint: config.lint,
27543
- engines: config.engines
28176
+ engines: config.engines,
28177
+ overrides: config.overrides,
28178
+ rules: config.rules
27544
28179
  },
27545
28180
  ...options.scope ? {
27546
28181
  files: options.scope.files,
@@ -27875,7 +28510,8 @@ const fixNarrativeComments = async (context) => {
27875
28510
  for (let i = 0; i < lines.length; i += 1) if (!toRemove.has(i + 1)) kept.push(lines[i]);
27876
28511
  const newContent = kept.join("\n");
27877
28512
  if (newContent !== content) {
27878
- if (!areLiteralMultisetsEqual(beforeLiterals, extractStringAndTemplateLiterals(newContent, ext))) continue;
28513
+ const afterLiterals = extractStringAndTemplateLiterals(newContent, ext);
28514
+ if (!areLiteralMultisetsEqual(beforeLiterals, afterLiterals)) continue;
27879
28515
  fs.writeFileSync(filePath, newContent);
27880
28516
  }
27881
28517
  }
@@ -28057,6 +28693,56 @@ const computeRemovalRange = (sourceFile, node, content) => {
28057
28693
  end: finalEnd
28058
28694
  };
28059
28695
  };
28696
+ const isReferencedOutside = (sourceFile, statement, name) => {
28697
+ const start = statement.getStart(sourceFile);
28698
+ const end = statement.getEnd();
28699
+ let found = false;
28700
+ const visit = (node) => {
28701
+ if (found) return;
28702
+ if (ts.isIdentifier(node) && node.text === name) {
28703
+ const pos = node.getStart(sourceFile);
28704
+ if (pos < start || pos >= end) found = true;
28705
+ return;
28706
+ }
28707
+ ts.forEachChild(node, visit);
28708
+ };
28709
+ visit(sourceFile);
28710
+ return found;
28711
+ };
28712
+ const exportKeywordRange = (sourceFile, statement, content) => {
28713
+ const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) ?? [] : [];
28714
+ if (modifiers.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword)) return null;
28715
+ const exportKeyword = modifiers.find((m) => m.kind === ts.SyntaxKind.ExportKeyword);
28716
+ if (!exportKeyword) return null;
28717
+ let end = exportKeyword.getEnd();
28718
+ while (end < content.length && (content[end] === " " || content[end] === " ")) end++;
28719
+ return {
28720
+ start: exportKeyword.getStart(sourceFile),
28721
+ end
28722
+ };
28723
+ };
28724
+ const removalFor = (sourceFile, statement, content, decl) => {
28725
+ if (!isReferencedOutside(sourceFile, statement, decl.name)) return null;
28726
+ const range = exportKeywordRange(sourceFile, statement, content);
28727
+ if (!range) return {
28728
+ type: "skip",
28729
+ reason: "still referenced in its own file",
28730
+ declaration: decl
28731
+ };
28732
+ return {
28733
+ type: "match",
28734
+ removal: {
28735
+ ...range,
28736
+ declaration: decl
28737
+ }
28738
+ };
28739
+ };
28740
+ const declaredNames = (statement) => {
28741
+ if (ts.isVariableStatement(statement)) return statement.declarationList.declarations.map((d) => ts.isIdentifier(d.name) ? d.name.text : null).filter((name) => name !== null);
28742
+ 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] : [];
28743
+ return [];
28744
+ };
28745
+ const isDeclaredMoreThanOnce = (sourceFile, name) => sourceFile.statements.filter((s) => declaredNames(s).includes(name)).length > 1;
28060
28746
  const kindOfStatement = (node) => {
28061
28747
  if (ts.isVariableStatement(node)) return "variable";
28062
28748
  if (ts.isFunctionDeclaration(node)) return "function";
@@ -28068,6 +28754,11 @@ const kindOfStatement = (node) => {
28068
28754
  };
28069
28755
  const matchStatement = (sourceFile, statement, content, decl) => {
28070
28756
  if (!kindOfStatement(statement)) return { type: "none" };
28757
+ if (declaredNames(statement).includes(decl.name) && isDeclaredMoreThanOnce(sourceFile, decl.name)) return {
28758
+ type: "skip",
28759
+ reason: "merged declaration",
28760
+ declaration: decl
28761
+ };
28071
28762
  if (ts.isVariableStatement(statement)) {
28072
28763
  const varDecls = statement.declarationList.declarations;
28073
28764
  if (varDecls.length === 0) return { type: "none" };
@@ -28083,6 +28774,8 @@ const matchStatement = (sourceFile, statement, content, decl) => {
28083
28774
  reason: "multi-declaration variable statement",
28084
28775
  declaration: decl
28085
28776
  };
28777
+ const referenced = removalFor(sourceFile, statement, content, decl);
28778
+ if (referenced) return referenced;
28086
28779
  if (initializerHasSideEffects(match.initializer)) return {
28087
28780
  type: "skip",
28088
28781
  reason: "initializer may have side effects",
@@ -28100,6 +28793,8 @@ const matchStatement = (sourceFile, statement, content, decl) => {
28100
28793
  if (!statement.name) return { type: "none" };
28101
28794
  if (statement.name.text !== decl.name) return { type: "none" };
28102
28795
  if (!nodeContainsLine(sourceFile, statement, decl.line)) return { type: "none" };
28796
+ const referenced = removalFor(sourceFile, statement, content, decl);
28797
+ if (referenced) return referenced;
28103
28798
  return {
28104
28799
  type: "match",
28105
28800
  removal: {
@@ -28111,6 +28806,8 @@ const matchStatement = (sourceFile, statement, content, decl) => {
28111
28806
  if (ts.isTypeAliasDeclaration(statement) || ts.isInterfaceDeclaration(statement) || ts.isEnumDeclaration(statement)) {
28112
28807
  if (statement.name.text !== decl.name) return { type: "none" };
28113
28808
  if (!nodeContainsLine(sourceFile, statement, decl.line)) return { type: "none" };
28809
+ const referenced = removalFor(sourceFile, statement, content, decl);
28810
+ if (referenced) return referenced;
28114
28811
  return {
28115
28812
  type: "match",
28116
28813
  removal: {
@@ -28184,7 +28881,7 @@ const detectUnusedDeclarations = async (context) => {
28184
28881
  rule: "code-quality/unused-declaration",
28185
28882
  severity: "warning",
28186
28883
  message: `Unused ${extracted.kind}: ${extracted.name}`,
28187
- help: "This top-level declaration is never used; aislop will remove it.",
28884
+ help: "This top-level declaration is never used; raigal will remove it.",
28188
28885
  line: d.line,
28189
28886
  column: d.column,
28190
28887
  category: "Dead Code",
@@ -28202,7 +28899,7 @@ const detectUnusedDeclarations = async (context) => {
28202
28899
  rule: "code-quality/unused-declaration",
28203
28900
  severity: "warning",
28204
28901
  message: `Unused ${kind}: ${name}`,
28205
- help: "This top-level declaration is never imported; aislop will remove it.",
28902
+ help: "This top-level declaration is never imported; raigal will remove it.",
28206
28903
  line: d.line,
28207
28904
  column: d.column,
28208
28905
  category: "Dead Code",
@@ -28308,7 +29005,7 @@ const removeUnusedDeclarations = (rootDirectory, declarations) => {
28308
29005
  };
28309
29006
  //#endregion
28310
29007
  //#region src/commands/fix-expo.ts
28311
- const INSTALL_TIMEOUT$1 = 1800 * 1e3;
29008
+ const INSTALL_TIMEOUT$1 = 18e5;
28312
29009
  const fixExpoDependencies = async (context, onProgress) => {
28313
29010
  await removeDisallowedExpoPackages(context.rootDirectory, onProgress);
28314
29011
  onProgress?.("Expo dependency alignment · running expo install --fix (can take a few minutes)");
@@ -28362,8 +29059,8 @@ const removeDisallowedExpoPackages = async (rootDir, onProgress) => {
28362
29059
  };
28363
29060
  //#endregion
28364
29061
  //#region src/commands/fix-force.ts
28365
- const INSTALL_TIMEOUT = 1800 * 1e3;
28366
- const AUDIT_TIMEOUT = 60 * 1e3;
29062
+ const INSTALL_TIMEOUT = 18e5;
29063
+ const AUDIT_TIMEOUT = 6e4;
28367
29064
  const detectPackageManager = (rootDirectory) => {
28368
29065
  if (fs.existsSync(path.join(rootDirectory, "pnpm-lock.yaml"))) return "pnpm";
28369
29066
  if (fs.existsSync(path.join(rootDirectory, "package-lock.json")) || fs.existsSync(path.join(rootDirectory, "package.json"))) return "npm";
@@ -28661,13 +29358,14 @@ const CANNOT_SCOPE_REASON = "cannot honour an explicit file list under --changes
28661
29358
  const MISSING_MANIFEST_REASON = "no manifest or lockfile in the selected scope";
28662
29359
  const fixScopeError = (resolvedDir, flags) => {
28663
29360
  if (flags.changes && flags.staged) return "--changes and --staged cannot be used together.";
28664
- return scanTargetError(resolvedDir, {
29361
+ const options = {
28665
29362
  changes: Boolean(flags.changes),
28666
29363
  staged: Boolean(flags.staged),
28667
29364
  base: flags.base,
28668
29365
  verbose: false,
28669
29366
  json: false
28670
- });
29367
+ };
29368
+ return scanTargetError(resolvedDir, options);
28671
29369
  };
28672
29370
  const collectFixFileScope = (rootDirectory, excludePatterns, includePatterns, flags) => {
28673
29371
  const mode = resolveScanScopeMode({
@@ -28726,9 +29424,10 @@ const runFormattingStep = async (deps) => {
28726
29424
  else if (deps.projectInfo.languages.includes("python")) log.warn("Python detected but ruff is not installed; skipping Python formatting fixes.");
28727
29425
  if (deps.projectInfo.languages.includes("go") && deps.projectInfo.installedTools.gofmt) await deps.runStep("Formatting (go)", () => runGofmt(deps.context), () => fixGofmt(deps.context));
28728
29426
  else if (deps.projectInfo.languages.includes("go")) log.warn("Go detected but gofmt is not installed; skipping Go formatting fixes.");
28729
- if (deps.projectInfo.languages.includes("rust") && deps.projectInfo.installedTools.rustfmt) if (isScopedFix(deps.context)) deps.skipStep?.("Formatting (rust)", CANNOT_SCOPE_REASON);
28730
- else await deps.runStep("Formatting (rust)", () => runGenericFormatter(deps.context, "rust"), () => fixGenericFormatter(deps.context, "rust"));
28731
- else if (deps.projectInfo.languages.includes("rust")) log.warn("Rust detected but rustfmt is not installed; skipping Rust formatting fixes.");
29427
+ if (deps.projectInfo.languages.includes("rust") && deps.projectInfo.installedTools.rustfmt) {
29428
+ if (isScopedFix(deps.context)) deps.skipStep?.("Formatting (rust)", CANNOT_SCOPE_REASON);
29429
+ else await deps.runStep("Formatting (rust)", () => runGenericFormatter(deps.context, "rust"), () => fixGenericFormatter(deps.context, "rust"));
29430
+ } else if (deps.projectInfo.languages.includes("rust")) log.warn("Rust detected but rustfmt is not installed; skipping Rust formatting fixes.");
28732
29431
  if (deps.projectInfo.languages.includes("ruby") && deps.projectInfo.installedTools.rubocop) {
28733
29432
  if (!skipUnsafeSafeFormatter(deps, "ruby")) await deps.runStep("Formatting (ruby)", () => runGenericFormatter(deps.context, "ruby"), () => fixGenericFormatter(deps.context, "ruby"));
28734
29433
  } else if (deps.projectInfo.languages.includes("ruby")) log.warn("Ruby detected but rubocop is not installed; skipping Ruby formatting fixes.");
@@ -28777,7 +29476,8 @@ const runDeclarationStep = async (deps) => {
28777
29476
  if (!deps.config.engines["code-quality"]) return;
28778
29477
  if (!hasJsOrTs(deps.projectInfo)) return;
28779
29478
  await deps.runStep("Unused declarations", () => detectUnusedDeclarations(deps.context), async () => {
28780
- const declarations = diagnosticsToDeclarations(await detectUnusedDeclarations(deps.context));
29479
+ const diagnostics = await detectUnusedDeclarations(deps.context);
29480
+ const declarations = diagnosticsToDeclarations(diagnostics);
28781
29481
  removeUnusedDeclarations(deps.resolvedDir, declarations);
28782
29482
  });
28783
29483
  };
@@ -28808,10 +29508,14 @@ const runForceSteps = async (deps) => {
28808
29508
  return (await runKnipUnusedFiles(deps.resolvedDir)).filter((diagnostic) => isPathInFixScope(deps.context, diagnostic.filePath));
28809
29509
  }, () => fixUnusedFiles(deps.resolvedDir, (filePath) => isPathInFixScope(deps.context, filePath)));
28810
29510
  const railUpdate = (label) => deps.rail.setActiveLabel(label);
28811
- if (deps.config.engines.security) if (!scopeIncludesManifestWrites(deps.context)) deps.skipStep?.("Dependency audit fixes", MISSING_MANIFEST_REASON);
28812
- else await deps.runStep("Dependency audit fixes", () => runDependencyAudit(deps.context), () => fixDependencyAudit(deps.context, railUpdate));
28813
- if (deps.projectInfo.frameworks.includes("expo") && deps.config.lint.expoDoctor) if (!scopeIncludesManifestWrites(deps.context)) deps.skipStep?.("Expo dependency alignment", MISSING_MANIFEST_REASON);
28814
- else await deps.runStep("Expo dependency alignment", () => runExpoDoctor(deps.context), () => fixExpoDependencies(deps.context, railUpdate));
29511
+ if (deps.config.engines.security) {
29512
+ if (!scopeIncludesManifestWrites(deps.context)) deps.skipStep?.("Dependency audit fixes", MISSING_MANIFEST_REASON);
29513
+ else await deps.runStep("Dependency audit fixes", () => runDependencyAudit(deps.context), () => fixDependencyAudit(deps.context, railUpdate));
29514
+ }
29515
+ if (deps.projectInfo.frameworks.includes("expo") && deps.config.lint.expoDoctor) {
29516
+ if (!scopeIncludesManifestWrites(deps.context)) deps.skipStep?.("Expo dependency alignment", MISSING_MANIFEST_REASON);
29517
+ else await deps.runStep("Expo dependency alignment", () => runExpoDoctor(deps.context), () => fixExpoDependencies(deps.context, railUpdate));
29518
+ }
28815
29519
  };
28816
29520
  //#endregion
28817
29521
  //#region src/commands/fix-plan.ts
@@ -29107,7 +29811,8 @@ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
29107
29811
  brand: options.printBrand !== false
29108
29812
  }));
29109
29813
  const safe = Boolean(options.safe);
29110
- const scope = collectFixFileScope(resolvedDir, [...config.exclude, ...readAislopIgnorePatterns(resolvedDir)], config.include, options);
29814
+ const excludePatterns = [...config.exclude, ...readAislopIgnorePatterns(resolvedDir)];
29815
+ const scope = collectFixFileScope(resolvedDir, excludePatterns, config.include, options);
29111
29816
  const scopedProjectInfo = scope ? {
29112
29817
  ...projectInfo,
29113
29818
  languages: detectSourceLanguages([...scope.files, ...scope.testFiles])
@@ -29162,6 +29867,8 @@ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
29162
29867
  const configDir = findConfigDir(resolvedDir);
29163
29868
  const rulesPath = configDir ? path.join(configDir, RULES_FILE) : void 0;
29164
29869
  const engineConfig = {
29870
+ overrides: config.overrides,
29871
+ rules: config.rules,
29165
29872
  quality: config.quality,
29166
29873
  security: config.security,
29167
29874
  lint: config.lint,
@@ -29187,7 +29894,7 @@ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
29187
29894
  status: "done",
29188
29895
  label: "Verification complete"
29189
29896
  });
29190
- const scanResults = appendPostFixLintResult(verificationResults, collectPostFixLintDiagnostics(steps));
29897
+ const scanResults = appendPostFixLintResult(verificationResults, applyFileOverrides(collectPostFixLintDiagnostics(steps), context));
29191
29898
  const allDiagnostics = scanResults.flatMap((r) => r.diagnostics);
29192
29899
  const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, scope ? scope.scoreFileCount : projectInfo.sourceFileCount, config.scoring.smoothing, config.scoring.maxPerRule);
29193
29900
  const errors = allDiagnostics.filter((d) => d.severity === "error").length;
@@ -29318,13 +30025,13 @@ const writeGithubWorkflow = (rootDirectory, enabled) => {
29318
30025
  };
29319
30026
  };
29320
30027
  const promptForConfigChoices = async () => {
29321
- const enginesSelection = await multiselect({
30028
+ const enginesSelection = await runCancellable(() => multiselect({
29322
30029
  message: "Which engines should run?",
29323
30030
  options: ENGINE_CHOICES,
29324
30031
  initialValues: DEFAULT_ENGINE_SELECTION,
29325
30032
  required: false
29326
- });
29327
- if (isCancel(enginesSelection)) return null;
30033
+ }));
30034
+ if (enginesSelection === void 0 || isCancel(enginesSelection)) return null;
29328
30035
  const failBelowRaw = await text({
29329
30036
  message: "CI quality gate — fail the build when the score drops below (0-100)",
29330
30037
  initialValue: "70",
@@ -29455,16 +30162,18 @@ const initCommand = async (directory, options = {}) => {
29455
30162
  status: "done",
29456
30163
  label: `Wrote ${CONFIG_DIR}/${CONFIG_FILE}`
29457
30164
  }];
29458
- if (choices.engines.includes("architecture")) if (!fs.existsSync(rulesPath)) {
29459
- fs.writeFileSync(rulesPath, DEFAULT_RULES_YAML);
29460
- steps.push({
29461
- status: "done",
29462
- label: `Wrote ${CONFIG_DIR}/${RULES_FILE}`
30165
+ if (choices.engines.includes("architecture")) {
30166
+ if (!fs.existsSync(rulesPath)) {
30167
+ fs.writeFileSync(rulesPath, DEFAULT_RULES_YAML);
30168
+ steps.push({
30169
+ status: "done",
30170
+ label: `Wrote ${CONFIG_DIR}/${RULES_FILE}`
30171
+ });
30172
+ } else steps.push({
30173
+ status: "skipped",
30174
+ label: `${CONFIG_DIR}/${RULES_FILE} already exists`
29463
30175
  });
29464
- } else steps.push({
29465
- status: "skipped",
29466
- label: `${CONFIG_DIR}/${RULES_FILE} already exists`
29467
- });
30176
+ }
29468
30177
  const workflowResult = writeGithubWorkflow(resolvedDir, choices.writeGithubWorkflow);
29469
30178
  if (workflowResult.status === "written") steps.push({
29470
30179
  status: "done",
@@ -29679,13 +30388,19 @@ const suggestClosest = (token, candidates) => {
29679
30388
  };
29680
30389
  //#endregion
29681
30390
  //#region src/cli.ts
29682
- const safeTelemetryConfig = (dir) => {
30391
+ const safeTelemetryConfig = (dir = process.cwd()) => {
29683
30392
  try {
29684
30393
  return loadConfig(dir).telemetry;
29685
30394
  } catch {
29686
30395
  return;
29687
30396
  }
29688
30397
  };
30398
+ const exitOnFatal = (error, stage) => {
30399
+ console.error(error);
30400
+ reportFatalError(error, stage, safeTelemetryConfig()).finally(() => process.exit(1));
30401
+ };
30402
+ process.on("uncaughtException", (error) => exitOnFatal(error, "uncaught_exception"));
30403
+ process.on("unhandledRejection", (reason) => exitOnFatal(reason, "unhandled_rejection"));
29689
30404
  const handleTerminationSignal = async (signal) => {
29690
30405
  killActiveChildren();
29691
30406
  const exitCode = signal === "SIGINT" ? 130 : 143;
@@ -29784,7 +30499,7 @@ fixProgram.action(async (directory = ".", _flags, command) => {
29784
30499
  }
29785
30500
  });
29786
30501
  registerAgentCommand(program);
29787
- program.command("init [directory]").description("Create aislop config and optional CI workflow").option("--strict", "write an enterprise-grade default config: all engines, typecheck on, CI failBelow 85, workflow included").action(async (directory = ".", _flags, command) => {
30502
+ program.command("init [directory]").description("Create raigal config and optional CI workflow").option("--strict", "write an enterprise-grade default config: all engines, typecheck on, CI failBelow 85, workflow included").action(async (directory = ".", _flags, command) => {
29788
30503
  const flags = command.optsWithGlobals();
29789
30504
  await withCommandLifecycle({
29790
30505
  command: "init",
@@ -29850,11 +30565,13 @@ program.hook("preAction", (_thisCommand, actionCommand) => {
29850
30565
  "hook"
29851
30566
  ].includes(cmdName)) {
29852
30567
  const flagNames = process.argv.slice(2).filter((arg) => arg.startsWith("-")).map((arg) => arg.replace(/^--?/, "").split("=")[0] ?? "");
29853
- startRunRecorder(cmdName, actionCommand.args?.[0] || process.cwd(), flagNames);
30568
+ const dir = actionCommand.args?.[0] || process.cwd();
30569
+ startRunRecorder(cmdName, dir, flagNames);
29854
30570
  }
29855
30571
  });
29856
30572
  program.hook("postAction", async () => {
29857
- await finishActiveRecorder(typeof process.exitCode === "number" ? process.exitCode : 0);
30573
+ const code = typeof process.exitCode === "number" ? process.exitCode : 0;
30574
+ await finishActiveRecorder(code);
29858
30575
  });
29859
30576
  const main = async () => {
29860
30577
  fireInstalledOnce();
@@ -29882,6 +30599,6 @@ const main = async () => {
29882
30599
  await flushTelemetry();
29883
30600
  await maybeNotifyUpdate();
29884
30601
  };
29885
- main();
30602
+ main().catch((error) => exitOnFatal(error, "main"));
29886
30603
  //#endregion
29887
30604
  export { requestApi as a, log as c, detectRepoRef as i, FIXED_SECRET_DIAGNOSTIC_MESSAGE as l, withFindingAssessments as n, getActiveToken as o, ENGINE_INFO as r, APP_VERSION as s, summarizeFindingAssessments as t, isSecretClassRule as u };