@methiu/raigal 0.2.0 → 0.2.2

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
@@ -15,7 +15,7 @@ import YAML, { parse } from "yaml";
15
15
  import { confirm, isCancel, multiselect, select, text } from "@clack/prompts";
16
16
  import * as readline$1 from "node:readline";
17
17
  import readline from "node:readline";
18
- import micromatch from "micromatch";
18
+ import picomatch from "picomatch";
19
19
  import ts from "typescript";
20
20
  import { parse as parse$1 } from "smol-toml";
21
21
  import { Writable } from "node:stream";
@@ -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");
@@ -923,7 +923,7 @@ const createLogger = (deps = {}) => {
923
923
  const log = createLogger();
924
924
  //#endregion
925
925
  //#region src/version.ts
926
- const APP_VERSION = "0.2.0";
926
+ const APP_VERSION = "0.2.2";
927
927
  //#endregion
928
928
  //#region src/commands/agent-connect.ts
929
929
  const commandText = (status) => [status.provider.loginCommand.command, ...status.provider.loginCommand.args].join(" ");
@@ -1422,7 +1422,7 @@ const verifyEntitlementJwt = (jwt, options = {}) => {
1422
1422
  const claims = parsed.data;
1423
1423
  const expectedIssuer = process.env.RAIGAL_ISSUER?.trim() || "https://app.raigal.dev";
1424
1424
  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");
1425
+ if (!isVersionAtLeast("0.2.2", 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
1426
  const now = options.nowSeconds ?? Math.floor(Date.now() / 1e3);
1427
1427
  const grace = options.allowGraceSeconds ?? 0;
1428
1428
  if (now > claims.exp + grace) {
@@ -1536,6 +1536,10 @@ const handleEntitlementFetchError = (err, cached, cachedClaims, now) => {
1536
1536
  nowSeconds: now,
1537
1537
  allowGraceSeconds: GRACE_PERIOD_SECONDS
1538
1538
  });
1539
+ if (graceClaims.plan === "trial" && graceClaims.trial_ends_at && now > Math.floor(new Date(graceClaims.trial_ends_at).getTime() / 1e3)) {
1540
+ clearCachedEntitlement();
1541
+ throw new LicenceDeniedError("Organization trial has expired", "trial_expired");
1542
+ }
1539
1543
  return {
1540
1544
  jwt: cached.entitlement,
1541
1545
  claims: graceClaims,
@@ -1543,6 +1547,7 @@ const handleEntitlementFetchError = (err, cached, cachedClaims, now) => {
1543
1547
  grace: now > graceClaims.exp
1544
1548
  };
1545
1549
  } catch (jwtErr) {
1550
+ if (jwtErr instanceof LicenceDeniedError) throw jwtErr;
1546
1551
  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
1552
  }
1548
1553
  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 +1562,8 @@ const getEntitlement = async () => {
1557
1562
  } catch {
1558
1563
  cachedClaims = null;
1559
1564
  }
1560
- if (cached && cachedClaims && now < cached.refresh_after) return {
1565
+ const isTrialExpired = cachedClaims?.plan === "trial" && cachedClaims.trial_ends_at && now > Math.floor(new Date(cachedClaims.trial_ends_at).getTime() / 1e3);
1566
+ if (cached && cachedClaims && !isTrialExpired && now < cached.refresh_after) return {
1561
1567
  jwt: cached.entitlement,
1562
1568
  claims: cachedClaims,
1563
1569
  fromCache: true
@@ -1566,7 +1572,7 @@ const getEntitlement = async () => {
1566
1572
  if (!activeToken) {
1567
1573
  oidcToken = await fetchGitHubOidcToken();
1568
1574
  if (!oidcToken) {
1569
- if (cached && cachedClaims) return {
1575
+ if (cached && cachedClaims && !isTrialExpired) return {
1570
1576
  jwt: cached.entitlement,
1571
1577
  claims: cachedClaims,
1572
1578
  fromCache: true
@@ -1727,6 +1733,23 @@ const requireEntitlement = async (options = {}) => {
1727
1733
  try {
1728
1734
  const entitlement = await getEntitlement();
1729
1735
  const tokenKind = getActiveToken()?.kind ?? "session";
1736
+ if (entitlement.claims.plan === "trial" && entitlement.claims.trial_ends_at) {
1737
+ const trialEnd = new Date(entitlement.claims.trial_ends_at).getTime();
1738
+ if (Date.now() > trialEnd) {
1739
+ const message = "Raigal license denied: Organization trial has expired";
1740
+ if (mode === "throw") throw new LicenceDeniedError(message, "trial_expired");
1741
+ if (mode === "fatal") {
1742
+ process.stderr.write(`${message}\n`);
1743
+ process.exit(30);
1744
+ }
1745
+ process.stderr.write(`[raigal] ${message}\n`);
1746
+ return {
1747
+ ok: false,
1748
+ exitCode: 30,
1749
+ message
1750
+ };
1751
+ }
1752
+ }
1730
1753
  const detectedOwner = detectLocalRepoOwner(directory);
1731
1754
  if (options.repoOwner) detectedOwner.owner = options.repoOwner.toLowerCase();
1732
1755
  const ownerCheck = checkOwnerAgainstAllowlist(detectedOwner, entitlement.claims.allowed_owners, tokenKind);
@@ -2026,267 +2049,6 @@ const renderBackgroundLaunch = (launch) => {
2026
2049
  ].join("\n")}`);
2027
2050
  };
2028
2051
  //#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);
2055
- };
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");
2075
- };
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
- ""
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
- }
2109
- }
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"));
2115
- };
2116
- //#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;
2131
- };
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
2052
  //#region src/agents/session-activity.ts
2291
2053
  const createUsageTotals = () => ({
2292
2054
  inputTokens: 0,
@@ -2693,6 +2455,239 @@ var AgentTui = class {
2693
2455
  }
2694
2456
  };
2695
2457
  //#endregion
2458
+ //#region src/commands/agent-local-cli.ts
2459
+ const cliPath$1 = () => {
2460
+ const arg = process.argv[1];
2461
+ if (arg && fs.existsSync(arg)) return arg;
2462
+ return path.resolve("dist/cli.js");
2463
+ };
2464
+ const runSelf = (args, cwd) => {
2465
+ const result = spawnSync(process.execPath, [cliPath$1(), ...args], {
2466
+ cwd,
2467
+ encoding: "utf-8",
2468
+ env: {
2469
+ ...process.env,
2470
+ AISLOP_NO_TELEMETRY: "1",
2471
+ AISLOP_NO_UPDATE_NOTIFIER: "1",
2472
+ NO_COLOR: "1"
2473
+ },
2474
+ maxBuffer: 50 * 1024 * 1024
2475
+ });
2476
+ return {
2477
+ exitCode: result.status,
2478
+ stdout: result.stdout,
2479
+ stderr: result.stderr
2480
+ };
2481
+ };
2482
+ const scanJson = (cwd) => {
2483
+ const result = runSelf([
2484
+ "scan",
2485
+ ".",
2486
+ "--json"
2487
+ ], cwd);
2488
+ if (!result.stdout.trim()) throw new Error(result.stderr || "raigal scan did not produce JSON output.");
2489
+ try {
2490
+ return JSON.parse(result.stdout);
2491
+ } catch (error) {
2492
+ throw new Error(`Failed to parse raigal scan output: ${error instanceof Error ? error.message : String(error)}`);
2493
+ }
2494
+ };
2495
+ const runSafeFix = (cwd) => {
2496
+ const result = runSelf([
2497
+ "fix",
2498
+ ".",
2499
+ "--safe"
2500
+ ], cwd);
2501
+ if (result.exitCode !== 0 && result.stderr.trim()) log.warn(result.stderr.trim().split("\n")[0] ?? "Safe fix reported a non-zero exit.");
2502
+ };
2503
+ const applyDiff = async (root, patch) => {
2504
+ const child = spawnSync("git", [
2505
+ "apply",
2506
+ "--index",
2507
+ "--whitespace=nowarn",
2508
+ "-"
2509
+ ], {
2510
+ cwd: root,
2511
+ input: patch,
2512
+ encoding: "utf-8",
2513
+ maxBuffer: 50 * 1024 * 1024
2514
+ });
2515
+ if (child.status !== 0) throw new Error(child.stderr || child.stdout || "Failed to apply agent diff.");
2516
+ };
2517
+ //#endregion
2518
+ //#region src/agents/publish.ts
2519
+ const defaultBranchSuffix = () => (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\..+$/, "").replace("T", "-");
2520
+ const normalizeBranchName = (raw) => {
2521
+ const cleaned = raw.trim().replace(/[^A-Za-z0-9._/-]+/g, "-").replace(/\/+/g, "/").replace(/^-+|-+$/g, "").replace(/^\/+|\/+$/g, "");
2522
+ return cleaned.length > 0 ? cleaned.slice(0, 120) : `raigal/agent-${defaultBranchSuffix()}`;
2523
+ };
2524
+ const defaultBranchName = () => `raigal/agent-${defaultBranchSuffix()}`;
2525
+ const runGit = async (cwd, args) => {
2526
+ const result = await runSubprocess("git", args, {
2527
+ cwd,
2528
+ timeout: 9e4
2529
+ });
2530
+ if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `git ${args.join(" ")} failed`);
2531
+ return result.stdout;
2532
+ };
2533
+ const currentBranch = async (cwd) => {
2534
+ const result = await runSubprocess("git", ["branch", "--show-current"], { cwd });
2535
+ if (result.exitCode !== 0) return null;
2536
+ return result.stdout.trim() || null;
2537
+ };
2538
+ const resolveBaseBranch = async (cwd, fallback) => {
2539
+ if (fallback && fallback !== "HEAD") return fallback;
2540
+ const cached = await runSubprocess("git", [
2541
+ "symbolic-ref",
2542
+ "refs/remotes/origin/HEAD",
2543
+ "--short"
2544
+ ], { cwd });
2545
+ if (cached.exitCode === 0 && cached.stdout.trim()) return cached.stdout.trim().replace(/^origin\//, "");
2546
+ const remote = await runSubprocess("git", [
2547
+ "remote",
2548
+ "show",
2549
+ "origin"
2550
+ ], {
2551
+ cwd,
2552
+ timeout: 3e4
2553
+ });
2554
+ if (remote.exitCode === 0) {
2555
+ const branch = remote.stdout.split("\n").find((entry) => entry.trim().startsWith("HEAD branch:"))?.split(":").slice(1).join(":").trim();
2556
+ if (branch) return branch;
2557
+ }
2558
+ return "main";
2559
+ };
2560
+ const ensureBranch = async (cwd, branch) => {
2561
+ if (await currentBranch(cwd) === branch) return;
2562
+ await runGit(cwd, [
2563
+ "switch",
2564
+ "-c",
2565
+ branch
2566
+ ]);
2567
+ };
2568
+ const ensureGh = async () => {
2569
+ 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.");
2570
+ };
2571
+ const buildPrBody = (input) => {
2572
+ const delta = input.beforeScore != null && input.afterScore != null ? input.afterScore - input.beforeScore : null;
2573
+ const deltaStr = delta != null ? ` (${delta >= 0 ? `+${delta}` : delta})` : "";
2574
+ const scoreStr = `${input.beforeScore ?? "not scored"} -> ${input.afterScore ?? "not scored"}${deltaStr}`;
2575
+ let diffStatLine = `- Files changed: ${input.changedFiles.length}`;
2576
+ if (typeof input.diffSummary === "string" && input.diffSummary.trim()) diffStatLine += ` (${input.diffSummary.trim()})`;
2577
+ else if (input.diffSummary && typeof input.diffSummary === "object") {
2578
+ const adds = input.diffSummary.additions ?? 0;
2579
+ const dels = input.diffSummary.deletions ?? 0;
2580
+ diffStatLine += ` (+${adds} -${dels})`;
2581
+ }
2582
+ const fixedFindingsSection = input.fixedFindings && input.fixedFindings.length > 0 ? [
2583
+ "### Fixed findings",
2584
+ "",
2585
+ "| Rule | File | Message |",
2586
+ "|---|---|---|",
2587
+ ...input.fixedFindings.slice(0, 10).map((f) => {
2588
+ const loc = f.line ? `${f.path}:${f.line}` : f.path;
2589
+ const msg = (f.message || "").replace(/\|/g, "\\|");
2590
+ return `| \`${f.ruleId}\` | \`${loc}\` | ${msg} |`;
2591
+ }),
2592
+ input.fixedFindings.length > 10 ? `\n*...and ${input.fixedFindings.length - 10} more findings fixed.*` : "",
2593
+ ""
2594
+ ] : [];
2595
+ return [
2596
+ "This PR was generated locally by `raigal agent`.",
2597
+ "",
2598
+ "## Summary",
2599
+ "",
2600
+ `- Provider: ${input.providerId}`,
2601
+ `- Score: ${scoreStr}`,
2602
+ diffStatLine,
2603
+ "",
2604
+ ...fixedFindingsSection,
2605
+ "## Changed files",
2606
+ "",
2607
+ ...input.changedFiles.map((file) => `- ${file}`),
2608
+ "",
2609
+ "## Verification",
2610
+ "",
2611
+ "- `raigal scan --json`",
2612
+ "",
2613
+ "Review before merging. The deterministic scan is the gate; the provider only proposed the patch."
2614
+ ].filter(Boolean).join("\n");
2615
+ };
2616
+ const publishAgentDiff = async (input) => {
2617
+ if (!input.options.commit && !input.options.pr) return null;
2618
+ if (input.changedFiles.length === 0) return null;
2619
+ const branch = normalizeBranchName(input.options.branch ?? defaultBranchName());
2620
+ await ensureBranch(input.cwd, branch);
2621
+ await runGit(input.cwd, ["add", "-A"]);
2622
+ if ((await runSubprocess("git", [
2623
+ "diff",
2624
+ "--cached",
2625
+ "--quiet"
2626
+ ], { cwd: input.cwd })).exitCode === 0) return null;
2627
+ await runGit(input.cwd, [
2628
+ "commit",
2629
+ "-m",
2630
+ input.options.commitMessage,
2631
+ "-m",
2632
+ "Generated by raigal agent."
2633
+ ]);
2634
+ const commitSha = (await runGit(input.cwd, [
2635
+ "rev-parse",
2636
+ "--short",
2637
+ "HEAD"
2638
+ ])).trim();
2639
+ if (!input.options.pr) return {
2640
+ branch,
2641
+ commitSha,
2642
+ pushed: false,
2643
+ prUrl: null
2644
+ };
2645
+ await ensureGh();
2646
+ const base = input.options.base ?? await resolveBaseBranch(input.cwd, input.originalBranch);
2647
+ await runGit(input.cwd, [
2648
+ "push",
2649
+ "-u",
2650
+ "origin",
2651
+ branch
2652
+ ]);
2653
+ let diffSummary = input.diffSummary;
2654
+ if (!diffSummary) {
2655
+ const stat = await runSubprocess("git", [
2656
+ "diff",
2657
+ "--shortstat",
2658
+ "HEAD~1"
2659
+ ], { cwd: input.cwd });
2660
+ if (stat.exitCode === 0 && stat.stdout.trim()) diffSummary = stat.stdout.trim();
2661
+ }
2662
+ const args = [
2663
+ "pr",
2664
+ "create",
2665
+ "--base",
2666
+ base,
2667
+ "--head",
2668
+ branch,
2669
+ "--title",
2670
+ input.options.prTitle ?? input.options.commitMessage,
2671
+ "--body",
2672
+ buildPrBody({
2673
+ ...input,
2674
+ diffSummary
2675
+ })
2676
+ ];
2677
+ if (!input.options.ready) args.push("--draft");
2678
+ const pr = await runSubprocess("gh", args, {
2679
+ cwd: input.cwd,
2680
+ timeout: 9e4
2681
+ });
2682
+ if (pr.exitCode !== 0) throw new Error(pr.stderr || pr.stdout || "Failed to create pull request with gh.");
2683
+ return {
2684
+ branch,
2685
+ commitSha,
2686
+ pushed: true,
2687
+ prUrl: pr.stdout.trim() || null
2688
+ };
2689
+ };
2690
+ //#endregion
2696
2691
  //#region src/cloud/context.ts
2697
2692
  const execGit = (args, cwd) => {
2698
2693
  try {
@@ -2766,7 +2761,8 @@ const detectGitDiff = (directory, baseRef) => {
2766
2761
  "HEAD~1"
2767
2762
  ], directory);
2768
2763
  let diffPreview;
2769
- if (preview) diffPreview = preview.split("\n").slice(0, 40).join("\n").slice(0, 1e4);
2764
+ const allowSnippets = process.env.RAIGAL_NO_SNIPPETS !== "true" && process.env.RAIGAL_PRIVACY_MODE !== "true";
2765
+ if (preview && allowSnippets) diffPreview = preview.split("\n").slice(0, 40).join("\n").slice(0, 1e4);
2770
2766
  return {
2771
2767
  diffSummary,
2772
2768
  diffPreview
@@ -2919,7 +2915,7 @@ const writeRunsToOutbox = (runs) => {
2919
2915
  recursive: true,
2920
2916
  mode: 448
2921
2917
  });
2922
- let lines = runs.map((r) => JSON.stringify(r));
2918
+ const lines = runs.map((r) => JSON.stringify(r));
2923
2919
  let text = lines.length > 0 ? `${lines.join("\n")}\n` : "";
2924
2920
  const maxBytes = getMaxOutboxBytes();
2925
2921
  while (Buffer.byteLength(text, "utf-8") > maxBytes && lines.length > 1) {
@@ -2944,7 +2940,9 @@ const getLastRecordedRun = () => {
2944
2940
  const runs = loadOutboxRuns();
2945
2941
  return runs.length > 0 ? runs[runs.length - 1] ?? null : null;
2946
2942
  };
2947
- const flushOutbox = async (token) => {
2943
+ const flushOutbox = async (optionsOrToken) => {
2944
+ const token = typeof optionsOrToken === "string" ? optionsOrToken : optionsOrToken?.token;
2945
+ const oidcToken = typeof optionsOrToken === "object" ? optionsOrToken?.oidcToken : void 0;
2948
2946
  const runs = loadOutboxRuns();
2949
2947
  if (runs.length === 0) return {
2950
2948
  sent: 0,
@@ -2960,6 +2958,7 @@ const flushOutbox = async (token) => {
2960
2958
  await requestApi("/v1/runs", {
2961
2959
  method: "POST",
2962
2960
  token,
2961
+ oidcToken,
2963
2962
  body: run,
2964
2963
  timeoutMs: 4e3
2965
2964
  });
@@ -3081,7 +3080,12 @@ var CloudRunRecorder = class {
3081
3080
  try {
3082
3081
  enqueueRun(this.buildRunPayload(exitCode));
3083
3082
  const activeToken = getActiveToken();
3084
- if (activeToken) await flushOutbox(activeToken.token);
3083
+ let oidcToken;
3084
+ if (!activeToken) oidcToken = await fetchGitHubOidcToken();
3085
+ if (activeToken || oidcToken) await flushOutbox({
3086
+ token: activeToken?.token,
3087
+ oidcToken
3088
+ });
3085
3089
  } catch {}
3086
3090
  }
3087
3091
  };
@@ -3091,7 +3095,10 @@ const startRunRecorder = (command, directory, flags = []) => {
3091
3095
  const recorder = new CloudRunRecorder(command, directory, flags);
3092
3096
  activeRecorder = recorder;
3093
3097
  const activeToken = getActiveToken();
3094
- if (activeToken) flushOutbox(activeToken.token).catch(() => {});
3098
+ if (activeToken) flushOutbox({ token: activeToken.token }).catch(() => {});
3099
+ else fetchGitHubOidcToken().then((oidcToken) => {
3100
+ if (oidcToken) return flushOutbox({ oidcToken });
3101
+ }).catch(() => {});
3095
3102
  return recorder;
3096
3103
  };
3097
3104
  const finishActiveRecorder = async (exitCode = 0) => {
@@ -3102,64 +3109,185 @@ const finishActiveRecorder = async (exitCode = 0) => {
3102
3109
  }
3103
3110
  };
3104
3111
  //#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");
3112
+ //#region src/commands/agent-session-publish.ts
3113
+ const maybePublishAgentDiff = async ({ tui, session, options, changedFiles, worktreePath, originalBranch, providerId, before, after }) => {
3114
+ if (changedFiles.length > 0 && (options.commit || options.pr)) {
3115
+ tui.start(options.pr ? "Creating local branch and PR" : "Creating local commit");
3116
+ session.append("publish.started", {
3117
+ commit: options.commit,
3118
+ pr: options.pr,
3119
+ branch: options.branch,
3120
+ base: options.base,
3121
+ ready: options.ready
3122
+ });
3123
+ const afterKeys = new Set(after.diagnostics.map((d) => `${d.rule}:${d.filePath}:${d.line}`));
3124
+ const fixedFindings = before.diagnostics.filter((d) => !afterKeys.has(`${d.rule}:${d.filePath}:${d.line}`)).map((d) => ({
3125
+ ruleId: d.rule,
3126
+ path: d.filePath,
3127
+ line: d.line,
3128
+ message: d.message
3129
+ }));
3130
+ const published = await publishAgentDiff({
3131
+ cwd: worktreePath,
3132
+ originalBranch,
3133
+ providerId,
3134
+ beforeScore: before.score,
3135
+ afterScore: after.score,
3136
+ changedFiles,
3137
+ fixedFindings,
3138
+ options: {
3139
+ commit: options.commit,
3140
+ pr: options.pr,
3141
+ branch: options.branch,
3142
+ base: options.base,
3143
+ commitMessage: options.commitMessage,
3144
+ prTitle: options.prTitle,
3145
+ ready: options.ready
3146
+ }
3147
+ });
3148
+ session.append(published ? "publish.finished" : "publish.skipped", { result: published });
3149
+ tui.complete({
3150
+ status: published ? "done" : "skipped",
3151
+ label: published?.prUrl ? `Opened PR ${published.prUrl}` : published ? `Committed ${published.commitSha}` : "No commit created"
3152
+ });
3153
+ return published;
3154
+ }
3155
+ if (options.commit || options.pr) session.append("publish.skipped", {
3156
+ reason: "no_changed_files",
3157
+ commit: options.commit,
3158
+ pr: options.pr
3159
+ });
3160
+ return null;
3110
3161
  };
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
3162
+ const recordAgentSessionTelemetry = ({ worktreePath, published, branchOption, providerId, beforeScore, afterScore, targetScore, durationMs, changedFilesCount }) => {
3163
+ const recorder = getActiveRecorder();
3164
+ if (!recorder) return;
3165
+ let patchDiff;
3166
+ if (worktreePath) try {
3167
+ patchDiff = (spawnSync("git", [
3168
+ "diff",
3169
+ "-U2",
3170
+ "--no-color",
3171
+ "HEAD"
3172
+ ], {
3173
+ cwd: worktreePath,
3174
+ encoding: "utf-8"
3175
+ }).stdout || "").slice(0, 2e4) || void 0;
3176
+ } catch {}
3177
+ if (published) {
3178
+ const prMatch = published.prUrl?.match(/\/pull\/(\d+)/);
3179
+ const prNumber = prMatch ? Number.parseInt(prMatch[1], 10) : void 0;
3180
+ recorder.setGitContext({
3181
+ branch: published.branch,
3182
+ pr_number: prNumber
3183
+ });
3184
+ } else if (branchOption) recorder.setGitContext({ branch: branchOption });
3185
+ const agentTelemetry = {
3186
+ provider: providerId,
3187
+ before_score: beforeScore,
3188
+ after_score: afterScore,
3189
+ pr_url: published?.prUrl || void 0,
3190
+ patch_diff: patchDiff,
3191
+ duration_ms: durationMs
3192
+ };
3193
+ recorder.setAgentTelemetry(agentTelemetry);
3194
+ recorder.recordStep("agent:baseline", 50, true);
3195
+ recorder.recordStep("agent:verify", durationMs, afterScore !== null && afterScore >= targetScore);
3196
+ recorder.setReport({
3197
+ score: afterScore,
3198
+ scoreable: true,
3199
+ files_scanned: changedFilesCount,
3200
+ findings: [],
3201
+ agent_telemetry: agentTelemetry
3122
3202
  });
3123
- return {
3124
- exitCode: result.status,
3125
- stdout: result.stdout,
3126
- stderr: result.stderr
3127
- };
3128
3203
  };
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)}`);
3204
+ //#endregion
3205
+ //#region src/agents/prompt.ts
3206
+ const CONTEXT_LINES$1 = 3;
3207
+ const MAX_DIAGNOSTICS_PER_FILE$1 = 6;
3208
+ const MAX_FILES$1 = 12;
3209
+ const priority = (diagnostic) => {
3210
+ if (diagnostic.severity === "error") return 0;
3211
+ if (diagnostic.severity === "warning") return 1;
3212
+ return 2;
3213
+ };
3214
+ const selectAgentFindings = (diagnostics, limit) => diagnostics.filter((diagnostic) => diagnostic.severity !== "info").sort((a, b) => {
3215
+ const severityDelta = priority(a) - priority(b);
3216
+ if (severityDelta !== 0) return severityDelta;
3217
+ if (a.fixable !== b.fixable) return a.fixable ? 1 : -1;
3218
+ return a.filePath.localeCompare(b.filePath);
3219
+ }).slice(0, Math.max(1, limit));
3220
+ const groupByFile$1 = (diagnostics) => {
3221
+ const groups = /* @__PURE__ */ new Map();
3222
+ for (const diagnostic of diagnostics) {
3223
+ const list = groups.get(diagnostic.filePath) ?? [];
3224
+ list.push(diagnostic);
3225
+ groups.set(diagnostic.filePath, list);
3140
3226
  }
3227
+ return [...groups.entries()].map(([filePath, items]) => ({
3228
+ filePath,
3229
+ diagnostics: items
3230
+ })).slice(0, MAX_FILES$1);
3141
3231
  };
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.");
3232
+ const snippetFor = (rootDirectory, diagnostic) => {
3233
+ if (diagnostic.line <= 0 || diagnostic.redactSource || isSecretClassRule(diagnostic.rule)) return null;
3234
+ const absolutePath = path.resolve(rootDirectory, diagnostic.filePath);
3235
+ let content;
3236
+ try {
3237
+ content = fs.readFileSync(absolutePath, "utf-8");
3238
+ } catch {
3239
+ return null;
3240
+ }
3241
+ const lines = content.split("\n");
3242
+ const start = Math.max(0, diagnostic.line - 1 - CONTEXT_LINES$1);
3243
+ const end = Math.min(lines.length, diagnostic.line + CONTEXT_LINES$1);
3244
+ const out = [];
3245
+ for (let index = start; index < end; index += 1) {
3246
+ const lineNumber = index + 1;
3247
+ const marker = lineNumber === diagnostic.line ? ">" : " ";
3248
+ out.push(`${marker} ${String(lineNumber).padStart(4)} | ${lines[index]}`);
3249
+ }
3250
+ return out.join("\n");
3149
3251
  };
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.");
3252
+ const buildRepairPrompt = (input) => {
3253
+ const groups = groupByFile$1(input.findings);
3254
+ const lines = [
3255
+ "You are repairing AI slop findings in a local git worktree.",
3256
+ "",
3257
+ `Current raigal score: ${input.score == null ? "not scored" : `${input.score}/100`}`,
3258
+ `Target score: ${input.targetScore}/100`,
3259
+ `Turn budget: ${input.maxTurns}`,
3260
+ "",
3261
+ "Hard constraints:",
3262
+ "- Make the smallest code changes that improve the findings below.",
3263
+ "- Do not change public APIs, exports, database schemas, or test expectations unless a finding directly requires it.",
3264
+ "- Do not delete tests.",
3265
+ "- Do not add dependencies unless the existing project already clearly expects them.",
3266
+ "- 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).",
3267
+ "- If a finding looks like a false positive, leave the code alone and mention it in your final summary.",
3268
+ "- Run the relevant local verification command if it is obvious and cheap.",
3269
+ "",
3270
+ "raigal findings to repair:",
3271
+ ""
3272
+ ];
3273
+ for (const group of groups) {
3274
+ lines.push(`## ${group.filePath}`);
3275
+ for (const diagnostic of group.diagnostics.slice(0, MAX_DIAGNOSTICS_PER_FILE$1)) {
3276
+ const location = diagnostic.line > 0 ? `:${diagnostic.line}` : "";
3277
+ lines.push(`- ${diagnostic.severity.toUpperCase()} ${diagnostic.rule}${location}: ${diagnostic.message}`);
3278
+ if (diagnostic.help) lines.push(` Help: ${diagnostic.help}`);
3279
+ const snippet = snippetFor(input.rootDirectory, diagnostic);
3280
+ if (snippet) {
3281
+ lines.push("```");
3282
+ lines.push(snippet);
3283
+ lines.push("```");
3284
+ }
3285
+ }
3286
+ lines.push("");
3287
+ }
3288
+ if (input.findings.length === 0) lines.push("No findings were supplied. Inspect the codebase for obvious AI slop only if needed.");
3289
+ lines.push("After editing, stop and summarize what changed and what you intentionally skipped.");
3290
+ return maskSecrets(lines.join("\n"));
3163
3291
  };
3164
3292
  //#endregion
3165
3293
  //#region src/agents/provider-metadata.ts
@@ -3614,6 +3742,52 @@ const maybeApplyDiff = async (input) => {
3614
3742
  await applyDiff(input.originalRoot, await readBinaryDiff(input.worktreePath));
3615
3743
  return true;
3616
3744
  };
3745
+ const resolveAgentSessionCwd = (created, requestedDirectory) => {
3746
+ const relative = path.relative(created.state.root, requestedDirectory);
3747
+ const outsideRoot = relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative);
3748
+ if (relative.length === 0 || outsideRoot) return created.worktree.path;
3749
+ return path.resolve(created.worktree.path, relative);
3750
+ };
3751
+ const prepareWorktree = async (tui, resolvedDir, options) => {
3752
+ tui.start("Preparing local session");
3753
+ const created = await createAgentWorktree(resolvedDir, { inPlace: options.inPlace });
3754
+ tui.setMetric("Worktree", created.worktree.created ? path.relative(created.state.root, created.worktree.path) : "current");
3755
+ tui.complete({
3756
+ status: "done",
3757
+ label: created.worktree.created ? `Created worktree ${path.relative(created.state.root, created.worktree.path)}` : "Using current worktree"
3758
+ });
3759
+ return created;
3760
+ };
3761
+ const scanBaseline = (tui, cwd) => {
3762
+ tui.start("Scanning baseline");
3763
+ const scan = scanJson(cwd);
3764
+ tui.setMetric("Score", `${scan.score ?? "not scored"} -> ...`);
3765
+ tui.setMetric("Findings", scan.diagnostics.length);
3766
+ tui.complete({
3767
+ status: scan.summary.errors > 0 ? "warn" : "done",
3768
+ label: `Baseline ${scan.score ?? "not scored"} / 100 · ${scan.diagnostics.length} findings`
3769
+ });
3770
+ return scan;
3771
+ };
3772
+ const runSafeFixStep = (tui, cwd, options) => {
3773
+ if (options.noFix) return;
3774
+ tui.start("Applying deterministic safe fixes");
3775
+ runSafeFix(cwd);
3776
+ tui.complete({
3777
+ status: "done",
3778
+ label: "Safe fixer finished"
3779
+ });
3780
+ };
3781
+ const selectFindings = (tui, cwd, limit, pass) => {
3782
+ tui.start(`Pass ${pass}: selecting findings`);
3783
+ const findings = selectAgentFindings(scanJson(cwd).diagnostics, limit);
3784
+ tui.setMetric("Selected", findings.length);
3785
+ tui.complete({
3786
+ status: findings.length > 0 ? "done" : "skipped",
3787
+ label: findings.length > 0 ? `Pass ${pass}: selected ${findings.length} finding${findings.length === 1 ? "" : "s"}` : "No remaining agent findings"
3788
+ });
3789
+ return findings;
3790
+ };
3617
3791
  //#endregion
3618
3792
  //#region src/utils/paths.ts
3619
3793
  /** Normalize an OS path to forward-slash (POSIX) separators. */
@@ -3749,52 +3923,6 @@ const printAgentSessionSummary = (input) => {
3749
3923
  //#endregion
3750
3924
  //#region src/commands/agent-session.ts
3751
3925
  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
3926
  const runAgentSession = async (selected, resolvedDir, options, started) => {
3799
3927
  const tui = new AgentTui({
3800
3928
  provider: selected.provider.label,
@@ -3959,49 +4087,16 @@ const runAgentSession = async (selected, resolvedDir, options, started) => {
3959
4087
  applyRequested: options.apply,
3960
4088
  changedFiles: changedFiles.length
3961
4089
  });
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
4090
+ published = await maybePublishAgentDiff({
4091
+ tui,
4092
+ session,
4093
+ options,
4094
+ changedFiles,
4095
+ worktreePath: created.worktree.path,
4096
+ originalBranch: created.state.branch,
4097
+ providerId: selected.provider.id,
4098
+ before,
4099
+ after: verified.after
4005
4100
  });
4006
4101
  session.append("session.completed", {
4007
4102
  durationMs: Math.round(performance.now() - started),
@@ -4017,47 +4112,17 @@ const runAgentSession = async (selected, resolvedDir, options, started) => {
4017
4112
  published: Boolean(published)
4018
4113
  });
4019
4114
  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
- }
4115
+ recordAgentSessionTelemetry({
4116
+ worktreePath: created?.worktree?.path,
4117
+ published,
4118
+ branchOption: options.branch,
4119
+ providerId: selected.provider.id,
4120
+ beforeScore: before.score,
4121
+ afterScore: verified.after.score,
4122
+ targetScore: options.targetScore,
4123
+ durationMs: Math.round(performance.now() - started),
4124
+ changedFilesCount: changedFiles.length
4125
+ });
4061
4126
  printAgentSessionSummary({
4062
4127
  before,
4063
4128
  after: verified.after,
@@ -4704,7 +4769,8 @@ const DEFAULT_CONFIG = {
4704
4769
  format: "json"
4705
4770
  },
4706
4771
  telemetry: { enabled: true },
4707
- rules: {}
4772
+ rules: {},
4773
+ overrides: []
4708
4774
  };
4709
4775
  const GITHUB_WORKFLOW_DIR = ".github/workflows";
4710
4776
  const GITHUB_WORKFLOW_FILE = "raigal.yml";
@@ -4934,6 +5000,16 @@ const RuleSeverityOverride = z.enum([
4934
5000
  "off"
4935
5001
  ]);
4936
5002
  const RulesSchema = z.record(z.string(), RuleSeverityOverride).default(() => ({}));
5003
+ const FileOverrideSchema = z.object({
5004
+ files: z.array(z.string().min(1)).min(1),
5005
+ quality: z.object({
5006
+ maxFunctionLoc: QualitySchema.shape.maxFunctionLoc.unwrap().optional(),
5007
+ maxFileLoc: QualitySchema.shape.maxFileLoc.unwrap().optional(),
5008
+ maxNesting: QualitySchema.shape.maxNesting.unwrap().optional(),
5009
+ maxParams: QualitySchema.shape.maxParams.unwrap().optional()
5010
+ }).optional(),
5011
+ rules: z.record(z.string(), RuleSeverityOverride).optional()
5012
+ });
4937
5013
  const AislopConfigSchema = z.object({
4938
5014
  version: z.number().default(1),
4939
5015
  extends: z.union([z.string(), z.array(z.string())]).optional(),
@@ -4989,6 +5065,7 @@ const AislopConfigSchema = z.object({
4989
5065
  })),
4990
5066
  telemetry: TelemetrySchema.default(() => ({ enabled: true })),
4991
5067
  rules: RulesSchema,
5068
+ overrides: z.array(FileOverrideSchema).default(() => []),
4992
5069
  exclude: z.array(z.string()).default(() => [
4993
5070
  "node_modules",
4994
5071
  ".git",
@@ -5360,6 +5437,10 @@ const SAFE_PROPERTY_NAMES = /* @__PURE__ */ new Set([
5360
5437
  "exit_code",
5361
5438
  "duration_ms",
5362
5439
  "error_kind",
5440
+ "error_name",
5441
+ "error_code",
5442
+ "failed_stage",
5443
+ "engines_failed",
5363
5444
  "score",
5364
5445
  "score_bucket",
5365
5446
  "finding_count",
@@ -5615,6 +5696,8 @@ const buildCommandCompletedProps = (input) => {
5615
5696
  if (typeof input.fixSteps === "number") props.fix_steps = input.fixSteps;
5616
5697
  if (typeof input.fixResolved === "number") props.fix_resolved = input.fixResolved;
5617
5698
  if (typeof input.fixScoreDelta === "number") props.fix_score_delta = input.fixScoreDelta;
5699
+ if (input.errorName) props.error_name = input.errorName;
5700
+ if (input.errorCode) props.error_code = input.errorCode;
5618
5701
  return props;
5619
5702
  };
5620
5703
  const buildHookScanCompletedProps = (input) => {
@@ -5635,6 +5718,50 @@ const errorKindFromException = (error) => {
5635
5718
  if (message.includes("engine") && message.includes("crash")) return "engine_crash";
5636
5719
  return "unknown";
5637
5720
  };
5721
+ const SAFE_ERROR_NAME = /^[A-Za-z_$][A-Za-z0-9_$]{0,39}$/;
5722
+ const SAFE_ERROR_CODE = /^[A-Za-z][A-Za-z0-9_]{0,39}$/;
5723
+ const errorIdentity = (error) => {
5724
+ let name;
5725
+ if (error instanceof Error && typeof error.name === "string" && error.name.length > 0) name = error.name;
5726
+ else if (error === null) name = "null";
5727
+ else if (typeof error === "object") name = "object";
5728
+ else name = typeof error;
5729
+ const identity = { error_name: SAFE_ERROR_NAME.test(name) ? name : "Error" };
5730
+ const code = error?.code;
5731
+ if (typeof code === "string" && SAFE_ERROR_CODE.test(code)) identity.error_code = code;
5732
+ else if (typeof code === "number" && Number.isFinite(code)) identity.error_code = String(code);
5733
+ return identity;
5734
+ };
5735
+ const buildCommandFailedProps = (input) => {
5736
+ const props = {
5737
+ failed_stage: input.stage,
5738
+ ...errorIdentity(input.error)
5739
+ };
5740
+ if (input.command) props.command = input.command;
5741
+ return props;
5742
+ };
5743
+ //#endregion
5744
+ //#region src/telemetry/fatal.ts
5745
+ const reportedErrors = /* @__PURE__ */ new Set();
5746
+ let activeTelemetryConfig;
5747
+ const markErrorReported = (error) => {
5748
+ reportedErrors.add(error);
5749
+ };
5750
+ const rememberTelemetryConfig = (config) => {
5751
+ activeTelemetryConfig = config;
5752
+ };
5753
+ const reportFatalError = async (error, stage, config) => {
5754
+ if (reportedErrors.has(error)) return;
5755
+ track({
5756
+ event: "cli_command_failed",
5757
+ properties: buildCommandFailedProps({
5758
+ error,
5759
+ stage
5760
+ }),
5761
+ config: activeTelemetryConfig ?? config
5762
+ });
5763
+ await flushTelemetry(2e3);
5764
+ };
5638
5765
  //#endregion
5639
5766
  //#region src/telemetry/lifecycle.ts
5640
5767
  const STARTED_FLUSH_TIMEOUT_MS = 100;
@@ -5645,6 +5772,7 @@ const withCommandLifecycle = async (start, run) => {
5645
5772
  fileCount: start.fileCount,
5646
5773
  properties: start.properties
5647
5774
  });
5775
+ rememberTelemetryConfig(start.config);
5648
5776
  track({
5649
5777
  event: "cli_command_started",
5650
5778
  properties: startProps,
@@ -5661,6 +5789,7 @@ const withCommandLifecycle = async (start, run) => {
5661
5789
  startProps,
5662
5790
  exitCode: result.exitCode,
5663
5791
  durationMs,
5792
+ errorKind: result.errorKind,
5664
5793
  score: result.score ?? void 0,
5665
5794
  findingCount: result.findingCount,
5666
5795
  errorCount: result.errorCount,
@@ -5678,16 +5807,21 @@ const withCommandLifecycle = async (start, run) => {
5678
5807
  await flushTelemetry();
5679
5808
  return result;
5680
5809
  } catch (error) {
5810
+ const durationMs = performance.now() - startedAt;
5811
+ const identity = errorIdentity(error);
5681
5812
  track({
5682
5813
  event: "cli_command_completed",
5683
5814
  properties: buildCommandCompletedProps({
5684
5815
  startProps,
5685
5816
  exitCode: 1,
5686
- durationMs: performance.now() - startedAt,
5687
- errorKind: errorKindFromException(error)
5817
+ durationMs,
5818
+ errorKind: errorKindFromException(error),
5819
+ errorName: identity.error_name,
5820
+ errorCode: identity.error_code
5688
5821
  }),
5689
5822
  config: start.config
5690
5823
  });
5824
+ markErrorReported(error);
5691
5825
  await flushTelemetry();
5692
5826
  throw error;
5693
5827
  }
@@ -6849,6 +6983,13 @@ const registerAgentMonitorSubcommands = (agent) => {
6849
6983
  registerMonitorStopCommand(monitor);
6850
6984
  };
6851
6985
  //#endregion
6986
+ //#region src/ui/prompts.ts
6987
+ const runCancellable = async (fn) => {
6988
+ const value = await fn();
6989
+ if (isCancel(value)) return void 0;
6990
+ return value;
6991
+ };
6992
+ //#endregion
6852
6993
  //#region src/commands/agent-apply.ts
6853
6994
  const asString = (value) => typeof value === "string" && value.length > 0 ? value : null;
6854
6995
  const isInside = (base, target) => {
@@ -7909,7 +8050,11 @@ const whoamiCommand = async () => {
7909
8050
  process.stdout.write(` Organization: ${orgName} (${orgId})\n`);
7910
8051
  if (userEmail) process.stdout.write(` User: ${userEmail}\n`);
7911
8052
  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`);
8053
+ let planDisplay = plan;
8054
+ if (plan === "active") planDisplay = "active (enterprise license)";
8055
+ else if (plan === "expired") planDisplay = `expired${trialEnd ? ` (trial ended ${trialEnd})` : ""}`;
8056
+ else if (trialEnd) planDisplay = `trial (trial ends ${trialEnd})`;
8057
+ process.stdout.write(` Plan: ${planDisplay}\n`);
7913
8058
  process.stdout.write(` Allowed Owners: ${owners || "none"}\n`);
7914
8059
  if (claims) {
7915
8060
  const expDate = (/* @__PURE__ */ new Date(claims.exp * 1e3)).toISOString();
@@ -8107,6 +8252,132 @@ const acquireHookLock = (cwd) => {
8107
8252
  } catch {}
8108
8253
  };
8109
8254
  };
8255
+ //#endregion
8256
+ //#region src/utils/globs.ts
8257
+ const MAX_BRACE_EXPANSIONS = 256;
8258
+ const braceListAt = (pattern, start) => {
8259
+ let depth = 0;
8260
+ const commas = [];
8261
+ for (let i = start; i < pattern.length; i += 1) {
8262
+ const char = pattern[i];
8263
+ if (char === "\\") i += 1;
8264
+ else if (char === "{") depth += 1;
8265
+ else if (char === ",") {
8266
+ if (depth === 1) commas.push(i);
8267
+ } else if (char === "}") {
8268
+ depth -= 1;
8269
+ if (depth === 0) return commas.length > 0 ? {
8270
+ start,
8271
+ end: i,
8272
+ commas
8273
+ } : null;
8274
+ }
8275
+ }
8276
+ return null;
8277
+ };
8278
+ const findBraceList = (pattern) => {
8279
+ for (let start = pattern.indexOf("{"); start !== -1; start = pattern.indexOf("{", start + 1)) {
8280
+ if (start > 0 && pattern[start - 1] === "\\") continue;
8281
+ const list = braceListAt(pattern, start);
8282
+ if (list) return list;
8283
+ }
8284
+ return null;
8285
+ };
8286
+ const expandInto = (pattern, results) => {
8287
+ const list = findBraceList(pattern);
8288
+ if (!list) {
8289
+ results.push(pattern);
8290
+ return results.length <= MAX_BRACE_EXPANSIONS;
8291
+ }
8292
+ const bounds = [
8293
+ list.start,
8294
+ ...list.commas,
8295
+ list.end
8296
+ ];
8297
+ const prefix = pattern.slice(0, list.start);
8298
+ const suffix = pattern.slice(list.end + 1);
8299
+ for (let i = 0; i < bounds.length - 1; i += 1) {
8300
+ const option = pattern.slice(bounds[i] + 1, bounds[i + 1]);
8301
+ if (!expandInto(`${prefix}${option}${suffix}`, results)) return false;
8302
+ }
8303
+ return true;
8304
+ };
8305
+ const expandBraceLists = (pattern) => {
8306
+ const results = [];
8307
+ return expandInto(pattern, results) ? results : [pattern];
8308
+ };
8309
+ const isNegated = (pattern) => picomatch.scan(pattern).negated;
8310
+ const matchesGlobList = (value, patterns, options = {}) => {
8311
+ const expanded = patterns.flatMap(expandBraceLists);
8312
+ const positives = expanded.filter((pattern) => !isNegated(pattern));
8313
+ const negatives = expanded.filter(isNegated).map((pattern) => pattern.slice(1));
8314
+ const included = positives.length === 0 || picomatch.isMatch(value, positives, options);
8315
+ const excluded = negatives.length > 0 && picomatch.isMatch(value, negatives, options);
8316
+ return included && !excluded;
8317
+ };
8318
+ //#endregion
8319
+ //#region src/scoring/rule-severity.ts
8320
+ /**
8321
+ * Apply per-rule severity overrides from config: "off" drops the diagnostic,
8322
+ * "error"/"warning" rewrite its severity before scoring and rendering.
8323
+ */
8324
+ const applyRuleSeverities = (diagnostics, overrides) => {
8325
+ if (Object.keys(overrides).length === 0) return diagnostics;
8326
+ const result = [];
8327
+ for (const diagnostic of diagnostics) {
8328
+ const override = overrides[diagnostic.rule];
8329
+ if (!override) {
8330
+ result.push(diagnostic);
8331
+ continue;
8332
+ }
8333
+ if (override === "off") continue;
8334
+ result.push(override === diagnostic.severity ? diagnostic : {
8335
+ ...diagnostic,
8336
+ severity: override
8337
+ });
8338
+ }
8339
+ return result;
8340
+ };
8341
+ //#endregion
8342
+ //#region src/config/overrides.ts
8343
+ const overrideRelativePath = (rootDirectory, filePath) => {
8344
+ const paths = path.win32.isAbsolute(rootDirectory) && !path.posix.isAbsolute(rootDirectory) ? path.win32 : path.posix;
8345
+ const root = rootDirectory.replaceAll("\\", "/");
8346
+ const file = filePath.replaceAll("\\", "/");
8347
+ return paths.normalize(paths.isAbsolute(file) ? paths.relative(root, file) : file).replaceAll("\\", "/");
8348
+ };
8349
+ const matchesEntry = (relativePath, patterns) => matchesGlobList(relativePath, patterns, { dot: true });
8350
+ const resolveFilePolicy = (config, relativePath) => {
8351
+ let quality = config.quality;
8352
+ let rules = config.rules ?? {};
8353
+ if (relativePath === ".." || relativePath.startsWith("../") || path.win32.isAbsolute(relativePath)) return {
8354
+ quality,
8355
+ rules
8356
+ };
8357
+ for (const entry of config.overrides ?? []) {
8358
+ if (!matchesEntry(relativePath, entry.files)) continue;
8359
+ if (entry.quality) quality = {
8360
+ ...quality,
8361
+ ...entry.quality
8362
+ };
8363
+ if (entry.rules) rules = {
8364
+ ...rules,
8365
+ ...entry.rules
8366
+ };
8367
+ }
8368
+ return {
8369
+ quality,
8370
+ rules
8371
+ };
8372
+ };
8373
+ const applyFileOverrides = (diagnostics, context) => {
8374
+ if (!context.config.overrides?.length) return diagnostics;
8375
+ return diagnostics.flatMap((diagnostic) => {
8376
+ const relativePath = overrideRelativePath(context.rootDirectory, diagnostic.filePath);
8377
+ const { rules } = resolveFilePolicy(context.config, relativePath);
8378
+ return applyRuleSeverities([diagnostic], rules);
8379
+ });
8380
+ };
8110
8381
  const supportedGlobPatterns = (patterns) => patterns.filter((pattern) => pattern.length <= 256);
8111
8382
  const normalizeExcludePatterns = (patterns) => patterns.flatMap((pattern) => {
8112
8383
  const trimmed = pattern.trim();
@@ -8116,13 +8387,13 @@ const normalizeExcludePatterns = (patterns) => patterns.flatMap((pattern) => {
8116
8387
  const normalized = withoutProjectPrefix.slice(0, end);
8117
8388
  if (normalized.length === 0) return [];
8118
8389
  if (normalized.length > 256) return [];
8119
- if (micromatch.scan(normalized).isGlob) return [normalized];
8390
+ if (picomatch.scan(normalized).isGlob) return [normalized];
8120
8391
  if (normalized.startsWith(".") && !normalized.includes("/")) return supportedGlobPatterns([`**/${normalized}`, `**/${normalized}/**`]);
8121
8392
  return supportedGlobPatterns([normalized, `${normalized}/**`]);
8122
8393
  });
8123
8394
  const isPathExcluded = (relativePath, normalizedPatterns) => {
8124
8395
  if (normalizedPatterns.length === 0) return false;
8125
- return micromatch.isMatch(relativePath, normalizedPatterns, { dot: true });
8396
+ return picomatch.isMatch(relativePath, normalizedPatterns, { dot: true });
8126
8397
  };
8127
8398
  const filterExcludedDiagnostics = (diagnostics, rootDirectory, excludePatterns) => {
8128
8399
  if (!excludePatterns || excludePatterns.length === 0) return diagnostics;
@@ -8189,7 +8460,7 @@ const isRootBoundedTarget = (candidate, rootDirectory) => {
8189
8460
  };
8190
8461
  //#endregion
8191
8462
  //#region src/utils/aislop-ignore.ts
8192
- const readRaigalIgnorePatterns = (rootDirectory) => {
8463
+ const readAislopIgnorePatterns = (rootDirectory) => {
8193
8464
  const patterns = [];
8194
8465
  for (const fileName of [".raigalignore", ".aislopignore"]) {
8195
8466
  const targetPath = safeProjectFilePath(path.join(rootDirectory, fileName), rootDirectory);
@@ -8200,7 +8471,6 @@ const readRaigalIgnorePatterns = (rootDirectory) => {
8200
8471
  }
8201
8472
  return patterns;
8202
8473
  };
8203
- const readAislopIgnorePatterns = readRaigalIgnorePatterns;
8204
8474
  //#endregion
8205
8475
  //#region src/utils/generated-files.ts
8206
8476
  const AUTO_GENERATED_PATTERNS = [
@@ -8559,12 +8829,13 @@ const normalizeIncludePatterns = (patterns) => patterns.flatMap((pattern) => {
8559
8829
  const normalized = pattern.trim().replace(/^\.\//, "").replace(/\/$/, "");
8560
8830
  if (normalized === "" || normalized === ".") return ["**"];
8561
8831
  if (normalized.length > 256) return [];
8562
- if (micromatch.scan(normalized).isGlob) return [normalized];
8832
+ if (picomatch.scan(normalized).isGlob) return [normalized];
8563
8833
  return supportedGlobPatterns([normalized, `${normalized}/**`]);
8564
8834
  });
8565
8835
  const createPathMatcher = (patterns) => {
8566
- const matchers = patterns.map((pattern) => micromatch.matcher(pattern, { dot: true }));
8567
- return (filePath) => matchers.some((matches) => matches(filePath));
8836
+ if (patterns.length === 0) return () => false;
8837
+ const isMatch = picomatch(patterns, { dot: true });
8838
+ return (filePath) => isMatch(filePath);
8568
8839
  };
8569
8840
  const filterFiles = (rootDirectory, files, options) => {
8570
8841
  const extraExtensions = new Set(options.extraExtensions);
@@ -16509,10 +16780,10 @@ const checkFileComplexity = (filePath, rootDirectory, limits) => {
16509
16780
  };
16510
16781
  const checkComplexity = async (context) => {
16511
16782
  const files = getSourceFiles(context);
16512
- const limits = context.config.quality;
16513
16783
  const diagnostics = [];
16514
16784
  for (const filePath of files) {
16515
16785
  if (isAutoGenerated(filePath)) continue;
16786
+ const { quality: limits } = resolveFilePolicy(context.config, overrideRelativePath(context.rootDirectory, filePath));
16516
16787
  diagnostics.push(...checkFileComplexity(filePath, context.rootDirectory, limits));
16517
16788
  }
16518
16789
  return diagnostics;
@@ -17404,13 +17675,23 @@ const withReportPath = async (run) => {
17404
17675
  };
17405
17676
  const UNSUPPORTED_GLOB_SYNTAX = /[?[\]{}!|()\\]/;
17406
17677
  const isDotnetFormatPath = (pattern) => !pattern.startsWith("-") && !UNSUPPORTED_GLOB_SYNTAX.test(pattern);
17678
+ const expandBraces = (pattern, depth = 0) => {
17679
+ if (depth > 10) return [pattern];
17680
+ const match = /\{([^{}]+)\}/.exec(pattern);
17681
+ if (!match) return [pattern];
17682
+ const [fullMatch, options] = match;
17683
+ const parts = options.split(",");
17684
+ const prefix = pattern.slice(0, match.index);
17685
+ const suffix = pattern.slice(match.index + fullMatch.length);
17686
+ return parts.flatMap((part) => expandBraces(`${prefix}${part}${suffix}`, depth + 1));
17687
+ };
17407
17688
  const buildDotnetFormatExcludeScope = (excludePatterns) => {
17408
17689
  const scope = {
17409
17690
  excludeArguments: [],
17410
17691
  unsupportedPatterns: []
17411
17692
  };
17412
17693
  for (const pattern of normalizeExcludePatterns(excludePatterns ?? [])) {
17413
- const expanded = micromatch.braces(pattern, { expand: true });
17694
+ const expanded = expandBraces(pattern);
17414
17695
  if (expanded.every(isDotnetFormatPath)) scope.excludeArguments.push(...expanded);
17415
17696
  else scope.unsupportedPatterns.push(pattern);
17416
17697
  }
@@ -19273,7 +19554,7 @@ const normalizeEslintIgnorePatterns = (patterns) => patterns.map((pattern) => pa
19273
19554
  if (!pattern.includes("*") && !path.extname(pattern)) return [`${pattern}/**`];
19274
19555
  return [pattern];
19275
19556
  });
19276
- const isEslintIgnored = (relativePath, patterns) => patterns.length > 0 && micromatch.isMatch(relativePath, patterns, { dot: true });
19557
+ const isEslintIgnored = (relativePath, patterns) => patterns.length > 0 && picomatch.isMatch(relativePath, patterns, { dot: true });
19277
19558
  const getOxlintTargets = (context) => {
19278
19559
  const eslintIgnorePatterns = normalizeEslintIgnorePatterns(collectEslintIgnorePatterns(context.rootDirectory));
19279
19560
  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));
@@ -20192,7 +20473,8 @@ const readRuntimeDependencies = (rootDir) => {
20192
20473
  collectRuntimeNames(rootManifest, names);
20193
20474
  const globs = [...npmWorkspaceGlobs(rootManifest), ...pnpmWorkspaceGlobs(rootDir)];
20194
20475
  if (globs.length === 0) return { runtimeDependencies: names };
20195
- for (const relativeDir of micromatch(listPackageDirectories(rootDir), globs)) {
20476
+ const isMatch = picomatch(globs, { dot: true });
20477
+ for (const relativeDir of listPackageDirectories(rootDir).filter((dir) => isMatch(dir))) {
20196
20478
  const memberManifest = readJsonFile(path.join(rootDir, relativeDir, "package.json"));
20197
20479
  if (memberManifest) collectRuntimeNames(memberManifest, names);
20198
20480
  }
@@ -20988,6 +21270,7 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
20988
21270
  const result = await engine.run(context);
20989
21271
  result.diagnostics = filterExcludedDiagnostics(result.diagnostics, context.rootDirectory, context.excludePatterns);
20990
21272
  result.elapsed = performance.now() - start;
21273
+ result.diagnostics = applyFileOverrides(result.diagnostics, context);
20991
21274
  onComplete?.(result);
20992
21275
  return result;
20993
21276
  } catch (error) {
@@ -20996,6 +21279,7 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
20996
21279
  diagnostics: [],
20997
21280
  elapsed: performance.now() - start,
20998
21281
  skipped: true,
21282
+ failed: true,
20999
21283
  skipReason: error instanceof Error ? error.message : String(error)
21000
21284
  };
21001
21285
  onComplete?.(result);
@@ -21006,6 +21290,7 @@ const runEngines = async (context, enabledEngines, onStart, onComplete) => {
21006
21290
  diagnostics: [],
21007
21291
  elapsed: 0,
21008
21292
  skipped: true,
21293
+ failed: true,
21009
21294
  skipReason: r.reason instanceof Error ? r.reason.message : String(r.reason)
21010
21295
  });
21011
21296
  return dedupeEquivalenceDiagnostics(dedupeOverlappingComments(dedupeCSharpAsync(stripSecretDiagnostics(finalResults))));
@@ -21246,29 +21531,6 @@ const calculateScore = (diagnostics, weights, thresholds, sourceFileCount, smoot
21246
21531
  };
21247
21532
  };
21248
21533
  //#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
21534
  //#region src/utils/discovery-classifiers.ts
21273
21535
  const LANGUAGE_SIGNALS = {
21274
21536
  "tsconfig.json": "typescript",
@@ -21603,6 +21865,8 @@ const runScopedScan = async (cwd, filePaths) => {
21603
21865
  projectFiles: [.../* @__PURE__ */ new Set([...projectSourceFiles, ...filterProjectDeclarationFiles(project.rootDirectory, projectCandidates, excludePatterns, config.include)])],
21604
21866
  installedTools: project.installedTools,
21605
21867
  config: {
21868
+ overrides: config.overrides,
21869
+ rules: config.rules,
21606
21870
  quality: config.quality,
21607
21871
  security: {
21608
21872
  audit: false,
@@ -21622,7 +21886,7 @@ const runScopedScan = async (cwd, filePaths) => {
21622
21886
  "ai-slop": config.engines["ai-slop"],
21623
21887
  architecture: config.engines.architecture,
21624
21888
  security: false
21625
- })).flatMap((result) => result.diagnostics), config.rules);
21889
+ })).flatMap((result) => result.diagnostics), config.overrides.length ? {} : config.rules);
21626
21890
  const { score } = calculateScore(diagnostics, config.scoring.weights, config.scoring.thresholds, projectSourceFiles.length + projectTestFiles.length, config.scoring.smoothing, config.scoring.maxPerRule);
21627
21891
  return {
21628
21892
  diagnostics,
@@ -21726,6 +21990,8 @@ const captureBaseline = async (cwd) => {
21726
21990
  files: [],
21727
21991
  installedTools: project.installedTools,
21728
21992
  config: {
21993
+ overrides: config.overrides,
21994
+ rules: config.rules,
21729
21995
  quality: config.quality,
21730
21996
  security: {
21731
21997
  audit: false,
@@ -22945,6 +23211,31 @@ const detectInstalledAgents = (opts) => {
22945
23211
  }
22946
23212
  return hits;
22947
23213
  };
23214
+ const HOOK_MARKERS = [
23215
+ "__raigal",
23216
+ "raigal:begin",
23217
+ "@RAIGAL.md",
23218
+ "__aislop",
23219
+ "aislop:begin",
23220
+ "@AISLOP.md"
23221
+ ];
23222
+ const hasHook = (target) => {
23223
+ try {
23224
+ if (!fs.statSync(target).isFile()) return false;
23225
+ const base = path.basename(target).toLowerCase();
23226
+ if (base.startsWith("raigal") || base.startsWith("aislop")) return true;
23227
+ const content = fs.readFileSync(target, "utf8");
23228
+ return HOOK_MARKERS.some((marker) => content.includes(marker));
23229
+ } catch {
23230
+ return false;
23231
+ }
23232
+ };
23233
+ const detectionScopes = (agent) => AGENTS_SUPPORTING_BOTH_SCOPES.includes(agent) ? ["global", "project"] : [defaultScopeFor(agent)];
23234
+ const detectRaigalHooks = (opts) => ALL_AGENTS.filter((agent) => detectionScopes(agent).some((scope) => REGISTRY[agent].paths({
23235
+ home: opts.home,
23236
+ cwd: opts.cwd,
23237
+ scope
23238
+ }).some(hasHook)));
22948
23239
  //#endregion
22949
23240
  //#region src/ui/search-select.ts
22950
23241
  const silentOutput = new Writable({ write(_chunk, _encoding, callback) {
@@ -23632,8 +23923,38 @@ const registerHookAliases = (program) => {
23632
23923
  });
23633
23924
  };
23634
23925
  //#endregion
23926
+ //#region src/utils/github-slug.ts
23927
+ const GITHUB_REMOTE_RE = /^(?:git@github\.com:|https:\/\/(?:[^@]+@)?github\.com\/)([^/]+)\/([^/\s]+?)(?:\.git)?\s*$/;
23928
+ const parseGithubSlug = (remoteUrl) => {
23929
+ const match = remoteUrl.trim().match(GITHUB_REMOTE_RE);
23930
+ if (!match) return null;
23931
+ const owner = match[1];
23932
+ const repo = match[2];
23933
+ if (!owner || !repo) return null;
23934
+ return {
23935
+ owner,
23936
+ repo
23937
+ };
23938
+ };
23939
+ const detectGithubSlug = (directory) => {
23940
+ let raw;
23941
+ try {
23942
+ raw = execSync("git remote get-url origin", {
23943
+ cwd: path.resolve(directory),
23944
+ encoding: "utf-8",
23945
+ stdio: [
23946
+ "ignore",
23947
+ "pipe",
23948
+ "ignore"
23949
+ ]
23950
+ });
23951
+ } catch {
23952
+ return null;
23953
+ }
23954
+ return parseGithubSlug(raw);
23955
+ };
23956
+ //#endregion
23635
23957
  //#region src/commands/badge.ts
23636
- const GITHUB_REMOTE_RE = /^(?:git@github\.com:|https:\/\/(?:[^@]+@)?github\.com\/)([^/]+)\/([^/.\s]+?)(?:\.git)?\s*$/;
23637
23958
  const renderBadgeOutput = ({ owner, repo, svgUrl, pageUrl }) => {
23638
23959
  const slug = `${owner}/${repo}`;
23639
23960
  const markdown = `[![raigal](${svgUrl})](${pageUrl})`;
@@ -23677,31 +23998,6 @@ const renderBadgeOutput = ({ owner, repo, svgUrl, pageUrl }) => {
23677
23998
  ""
23678
23999
  ].join("\n");
23679
24000
  };
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
24001
  const badgeCommand = async (options = {}) => {
23706
24002
  await requireEntitlement({
23707
24003
  directory: options.directory,
@@ -23710,7 +24006,7 @@ const badgeCommand = async (options = {}) => {
23710
24006
  let owner = options.owner?.trim();
23711
24007
  let repo = options.repo?.trim();
23712
24008
  if (!owner || !repo) {
23713
- const detected = detectGithubSlugFromGit(options.directory ?? ".");
24009
+ const detected = detectGithubSlug(options.directory ?? ".");
23714
24010
  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
24011
  owner ??= detected.owner;
23716
24012
  repo ??= detected.repo;
@@ -24486,7 +24782,7 @@ const maybeNotifyUpdate = async (now = Date.now()) => {
24486
24782
  if (!process.stderr.isTTY) return;
24487
24783
  const cachePath = resolveUpdateCachePath();
24488
24784
  const cache = readCache(cachePath);
24489
- if (cache && isOutdated("0.2.0", cache.latest)) process.stderr.write(formatUpdateNotice(APP_VERSION, cache.latest));
24785
+ if (cache && isOutdated("0.2.2", cache.latest)) process.stderr.write(formatUpdateNotice(APP_VERSION, cache.latest));
24490
24786
  if (!cache || now - cache.checkedAt > CHECK_INTERVAL_MS) {
24491
24787
  const latest = await fetchLatestVersion$1();
24492
24788
  if (latest) writeCache(cachePath, {
@@ -25006,7 +25302,7 @@ const renderCommandGroup = (group, commandWidth) => {
25006
25302
  return lines;
25007
25303
  };
25008
25304
  const renderCommandReference = (input = {}) => {
25009
- const version = input.version ?? "0.2.0";
25305
+ const version = input.version ?? "0.2.2";
25010
25306
  const commandWidth = Math.max(...COMMAND_REFERENCE.map((item) => item.command.length));
25011
25307
  const lines = [renderHeader({
25012
25308
  version,
@@ -25229,7 +25525,7 @@ const renderHelpDetails = () => [
25229
25525
  ].map((line) => highlightRaigal(line, theme)).join("\n");
25230
25526
  const renderHome = (input = {}) => {
25231
25527
  let out = renderHeader({
25232
- version: input.version ?? "0.2.0",
25528
+ version: input.version ?? "0.2.2",
25233
25529
  command: "--bare",
25234
25530
  context: []
25235
25531
  });
@@ -25974,6 +26270,18 @@ const getChangedLineMap = (cwd, base) => {
25974
26270
  return map;
25975
26271
  };
25976
26272
  //#endregion
26273
+ //#region src/commands/scan-hook-nudge.ts
26274
+ const shouldShowHookNudge = (ctx) => ctx.isTty && !ctx.isCi && ctx.installedAgentCount === 0;
26275
+ const buildHookNudge = (ctx) => {
26276
+ if (!shouldShowHookNudge(ctx)) return null;
26277
+ return [
26278
+ "",
26279
+ "Next: install the per-edit hook so your coding agent fixes slop before it lands.",
26280
+ ctx.invocation === "raigal" ? " raigal hook install" : " npm install -g @methiu/raigal && raigal hook install",
26281
+ ""
26282
+ ].join("\n");
26283
+ };
26284
+ //#endregion
25977
26285
  //#region src/commands/scan-coverage.ts
25978
26286
  const coverageReason = (c) => {
25979
26287
  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.`;
@@ -26515,7 +26823,7 @@ const buildScanNextSteps = (input) => {
26515
26823
  emphasis: "primary",
26516
26824
  label: "Provider",
26517
26825
  command: `${input.invocation} agent --provider codex`,
26518
- detail: "or use --provider claude/opencode"
26826
+ detail: "or use --provider claude/opencode/pi"
26519
26827
  }, {
26520
26828
  emphasis: "muted",
26521
26829
  label: "Preview",
@@ -26682,6 +26990,8 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
26682
26990
  const configDir = findConfigDir(resolvedDir);
26683
26991
  const rulesPath = configDir ? path.join(configDir, RULES_FILE) : void 0;
26684
26992
  const engineConfig = {
26993
+ overrides: config.overrides,
26994
+ rules: config.rules,
26685
26995
  quality: config.quality,
26686
26996
  security: config.security,
26687
26997
  lint: config.lint,
@@ -26703,7 +27013,7 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
26703
27013
  config: engineConfig
26704
27014
  }, config.engines, machineOutput)).map((result) => ({
26705
27015
  ...result,
26706
- diagnostics: applyRuleSeverities(result.diagnostics, config.rules)
27016
+ diagnostics: config.overrides.length ? result.diagnostics : applyRuleSeverities(result.diagnostics, config.rules)
26707
27017
  })), resolvedDir);
26708
27018
  if (suppressedCount > 0 && !machineOutput) log.muted(`Suppressed ${suppressedCount} finding(s) via raigal-ignore directives`);
26709
27019
  const changeMap = options.changes && !options.staged ? getChangedLineMap(resolvedDir, options.base) : null;
@@ -26741,6 +27051,7 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
26741
27051
  engineIssues[r.engine] = r.diagnostics.length;
26742
27052
  engineTimings[r.engine] = Math.round(r.elapsed);
26743
27053
  }
27054
+ const enginesFailed = results.filter((r) => r.failed).map((r) => r.engine);
26744
27055
  const completion = {
26745
27056
  exitCode,
26746
27057
  score: scoreable ? scoreResult.score : null,
@@ -26750,7 +27061,8 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
26750
27061
  warningCount: allDiagnostics.filter((d) => d.severity === "warning").length,
26751
27062
  fixableCount: allDiagnostics.filter((d) => d.fixable).length,
26752
27063
  engineIssues,
26753
- engineTimings
27064
+ engineTimings,
27065
+ ...enginesFailed.length > 0 ? { properties: { engines_failed: enginesFailed } } : {}
26754
27066
  };
26755
27067
  recordScanToCloud({
26756
27068
  results,
@@ -26768,7 +27080,7 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
26768
27080
  return completion;
26769
27081
  }
26770
27082
  if (options.json) {
26771
- const { buildJsonOutput } = await import("./json-C9fBWj3V.js");
27083
+ const { buildJsonOutput } = await import("./json-BaycSoJP.js");
26772
27084
  const jsonOut = buildJsonOutput(results, scoreResult, scoreFileCount, elapsedMs, scanCoverage);
26773
27085
  console.log(JSON.stringify(jsonOut, null, 2));
26774
27086
  return completion;
@@ -26801,6 +27113,18 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
26801
27113
  printBrand: options.printBrand,
26802
27114
  showPilotInvitation
26803
27115
  }));
27116
+ if (options.command !== "ci" && options.printBrand !== false) {
27117
+ const nudge = buildHookNudge({
27118
+ installedAgentCount: detectRaigalHooks({
27119
+ home: os.homedir(),
27120
+ cwd: resolvedDir
27121
+ }).length,
27122
+ isTty: Boolean(process.stdout.isTTY),
27123
+ isCi: isCiEnv(),
27124
+ invocation: detectInvocation()
27125
+ });
27126
+ if (nudge) process.stdout.write(nudge);
27127
+ }
26804
27128
  return completion;
26805
27129
  };
26806
27130
  //#endregion
@@ -27540,7 +27864,9 @@ const createEngineContext = (rootDirectory, projectInfo, config, options = {}) =
27540
27864
  quality: config.quality,
27541
27865
  security: config.security,
27542
27866
  lint: config.lint,
27543
- engines: config.engines
27867
+ engines: config.engines,
27868
+ overrides: config.overrides,
27869
+ rules: config.rules
27544
27870
  },
27545
27871
  ...options.scope ? {
27546
27872
  files: options.scope.files,
@@ -28057,6 +28383,56 @@ const computeRemovalRange = (sourceFile, node, content) => {
28057
28383
  end: finalEnd
28058
28384
  };
28059
28385
  };
28386
+ const isReferencedOutside = (sourceFile, statement, name) => {
28387
+ const start = statement.getStart(sourceFile);
28388
+ const end = statement.getEnd();
28389
+ let found = false;
28390
+ const visit = (node) => {
28391
+ if (found) return;
28392
+ if (ts.isIdentifier(node) && node.text === name) {
28393
+ const pos = node.getStart(sourceFile);
28394
+ if (pos < start || pos >= end) found = true;
28395
+ return;
28396
+ }
28397
+ ts.forEachChild(node, visit);
28398
+ };
28399
+ visit(sourceFile);
28400
+ return found;
28401
+ };
28402
+ const exportKeywordRange = (sourceFile, statement, content) => {
28403
+ const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) ?? [] : [];
28404
+ if (modifiers.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword)) return null;
28405
+ const exportKeyword = modifiers.find((m) => m.kind === ts.SyntaxKind.ExportKeyword);
28406
+ if (!exportKeyword) return null;
28407
+ let end = exportKeyword.getEnd();
28408
+ while (end < content.length && (content[end] === " " || content[end] === " ")) end++;
28409
+ return {
28410
+ start: exportKeyword.getStart(sourceFile),
28411
+ end
28412
+ };
28413
+ };
28414
+ const removalFor = (sourceFile, statement, content, decl) => {
28415
+ if (!isReferencedOutside(sourceFile, statement, decl.name)) return null;
28416
+ const range = exportKeywordRange(sourceFile, statement, content);
28417
+ if (!range) return {
28418
+ type: "skip",
28419
+ reason: "still referenced in its own file",
28420
+ declaration: decl
28421
+ };
28422
+ return {
28423
+ type: "match",
28424
+ removal: {
28425
+ ...range,
28426
+ declaration: decl
28427
+ }
28428
+ };
28429
+ };
28430
+ const declaredNames = (statement) => {
28431
+ if (ts.isVariableStatement(statement)) return statement.declarationList.declarations.map((d) => ts.isIdentifier(d.name) ? d.name.text : null).filter((name) => name !== null);
28432
+ 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] : [];
28433
+ return [];
28434
+ };
28435
+ const isDeclaredMoreThanOnce = (sourceFile, name) => sourceFile.statements.filter((s) => declaredNames(s).includes(name)).length > 1;
28060
28436
  const kindOfStatement = (node) => {
28061
28437
  if (ts.isVariableStatement(node)) return "variable";
28062
28438
  if (ts.isFunctionDeclaration(node)) return "function";
@@ -28068,6 +28444,11 @@ const kindOfStatement = (node) => {
28068
28444
  };
28069
28445
  const matchStatement = (sourceFile, statement, content, decl) => {
28070
28446
  if (!kindOfStatement(statement)) return { type: "none" };
28447
+ if (declaredNames(statement).includes(decl.name) && isDeclaredMoreThanOnce(sourceFile, decl.name)) return {
28448
+ type: "skip",
28449
+ reason: "merged declaration",
28450
+ declaration: decl
28451
+ };
28071
28452
  if (ts.isVariableStatement(statement)) {
28072
28453
  const varDecls = statement.declarationList.declarations;
28073
28454
  if (varDecls.length === 0) return { type: "none" };
@@ -28083,6 +28464,8 @@ const matchStatement = (sourceFile, statement, content, decl) => {
28083
28464
  reason: "multi-declaration variable statement",
28084
28465
  declaration: decl
28085
28466
  };
28467
+ const referenced = removalFor(sourceFile, statement, content, decl);
28468
+ if (referenced) return referenced;
28086
28469
  if (initializerHasSideEffects(match.initializer)) return {
28087
28470
  type: "skip",
28088
28471
  reason: "initializer may have side effects",
@@ -28100,6 +28483,8 @@ const matchStatement = (sourceFile, statement, content, decl) => {
28100
28483
  if (!statement.name) return { type: "none" };
28101
28484
  if (statement.name.text !== decl.name) return { type: "none" };
28102
28485
  if (!nodeContainsLine(sourceFile, statement, decl.line)) return { type: "none" };
28486
+ const referenced = removalFor(sourceFile, statement, content, decl);
28487
+ if (referenced) return referenced;
28103
28488
  return {
28104
28489
  type: "match",
28105
28490
  removal: {
@@ -28111,6 +28496,8 @@ const matchStatement = (sourceFile, statement, content, decl) => {
28111
28496
  if (ts.isTypeAliasDeclaration(statement) || ts.isInterfaceDeclaration(statement) || ts.isEnumDeclaration(statement)) {
28112
28497
  if (statement.name.text !== decl.name) return { type: "none" };
28113
28498
  if (!nodeContainsLine(sourceFile, statement, decl.line)) return { type: "none" };
28499
+ const referenced = removalFor(sourceFile, statement, content, decl);
28500
+ if (referenced) return referenced;
28114
28501
  return {
28115
28502
  type: "match",
28116
28503
  removal: {
@@ -29162,6 +29549,8 @@ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
29162
29549
  const configDir = findConfigDir(resolvedDir);
29163
29550
  const rulesPath = configDir ? path.join(configDir, RULES_FILE) : void 0;
29164
29551
  const engineConfig = {
29552
+ overrides: config.overrides,
29553
+ rules: config.rules,
29165
29554
  quality: config.quality,
29166
29555
  security: config.security,
29167
29556
  lint: config.lint,
@@ -29187,7 +29576,7 @@ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
29187
29576
  status: "done",
29188
29577
  label: "Verification complete"
29189
29578
  });
29190
- const scanResults = appendPostFixLintResult(verificationResults, collectPostFixLintDiagnostics(steps));
29579
+ const scanResults = appendPostFixLintResult(verificationResults, applyFileOverrides(collectPostFixLintDiagnostics(steps), context));
29191
29580
  const allDiagnostics = scanResults.flatMap((r) => r.diagnostics);
29192
29581
  const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, scope ? scope.scoreFileCount : projectInfo.sourceFileCount, config.scoring.smoothing, config.scoring.maxPerRule);
29193
29582
  const errors = allDiagnostics.filter((d) => d.severity === "error").length;
@@ -29318,13 +29707,13 @@ const writeGithubWorkflow = (rootDirectory, enabled) => {
29318
29707
  };
29319
29708
  };
29320
29709
  const promptForConfigChoices = async () => {
29321
- const enginesSelection = await multiselect({
29710
+ const enginesSelection = await runCancellable(() => multiselect({
29322
29711
  message: "Which engines should run?",
29323
29712
  options: ENGINE_CHOICES,
29324
29713
  initialValues: DEFAULT_ENGINE_SELECTION,
29325
29714
  required: false
29326
- });
29327
- if (isCancel(enginesSelection)) return null;
29715
+ }));
29716
+ if (enginesSelection === void 0) return null;
29328
29717
  const failBelowRaw = await text({
29329
29718
  message: "CI quality gate — fail the build when the score drops below (0-100)",
29330
29719
  initialValue: "70",
@@ -29679,13 +30068,19 @@ const suggestClosest = (token, candidates) => {
29679
30068
  };
29680
30069
  //#endregion
29681
30070
  //#region src/cli.ts
29682
- const safeTelemetryConfig = (dir) => {
30071
+ const safeTelemetryConfig = (dir = process.cwd()) => {
29683
30072
  try {
29684
30073
  return loadConfig(dir).telemetry;
29685
30074
  } catch {
29686
30075
  return;
29687
30076
  }
29688
30077
  };
30078
+ const exitOnFatal = (error, stage) => {
30079
+ console.error(error);
30080
+ reportFatalError(error, stage, safeTelemetryConfig()).finally(() => process.exit(1));
30081
+ };
30082
+ process.on("uncaughtException", (error) => exitOnFatal(error, "uncaught_exception"));
30083
+ process.on("unhandledRejection", (reason) => exitOnFatal(reason, "unhandled_rejection"));
29689
30084
  const handleTerminationSignal = async (signal) => {
29690
30085
  killActiveChildren();
29691
30086
  const exitCode = signal === "SIGINT" ? 130 : 143;
@@ -29784,7 +30179,7 @@ fixProgram.action(async (directory = ".", _flags, command) => {
29784
30179
  }
29785
30180
  });
29786
30181
  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) => {
30182
+ 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
30183
  const flags = command.optsWithGlobals();
29789
30184
  await withCommandLifecycle({
29790
30185
  command: "init",
@@ -29882,6 +30277,6 @@ const main = async () => {
29882
30277
  await flushTelemetry();
29883
30278
  await maybeNotifyUpdate();
29884
30279
  };
29885
- main();
30280
+ main().catch((error) => exitOnFatal(error, "main"));
29886
30281
  //#endregion
29887
30282
  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 };