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