@methiu/raigal 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire, isBuiltin } from "node:module";
3
+ import { execSync, spawn, spawnSync } from "node:child_process";
3
4
  import fs, { closeSync, existsSync, mkdtempSync, openSync, readFileSync, rmSync } from "node:fs";
4
- import { Command, Option } from "commander";
5
5
  import path from "node:path";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
+ import { Command, Option } from "commander";
6
8
  import { performance } from "node:perf_hooks";
7
- import { execSync, spawn, spawnSync } from "node:child_process";
8
9
  import os, { tmpdir } from "node:os";
9
10
  import pc from "picocolors";
10
11
  import wcwidth from "wcwidth";
11
- import { fileURLToPath, pathToFileURL } from "node:url";
12
12
  import { z } from "zod/v4";
13
13
  import crypto, { createHash, createPublicKey, randomUUID, verify } from "node:crypto";
14
14
  import YAML, { parse } from "yaml";
@@ -923,7 +923,7 @@ const createLogger = (deps = {}) => {
923
923
  const log = createLogger();
924
924
  //#endregion
925
925
  //#region src/version.ts
926
- const APP_VERSION = "0.2.4";
926
+ const APP_VERSION = "0.2.6";
927
927
  //#endregion
928
928
  //#region src/commands/agent-connect.ts
929
929
  const commandText = (status) => [status.provider.loginCommand.command, ...status.provider.loginCommand.args].join(" ");
@@ -1266,6 +1266,8 @@ const requestApi = async (endpoint, options = {}) => {
1266
1266
  };
1267
1267
  if (options.token) headers.Authorization = `Bearer ${options.token}`;
1268
1268
  if (options.oidcToken) headers["X-GitHub-OIDC"] = options.oidcToken;
1269
+ const orgId = options.orgId || process.env.RAIGAL_ORG_ID;
1270
+ if (orgId) headers["X-Raigal-Org-Id"] = orgId;
1269
1271
  let body;
1270
1272
  if (options.body !== void 0) {
1271
1273
  headers["Content-Type"] = "application/json";
@@ -1327,11 +1329,12 @@ const pollDeviceToken = async (deviceCode) => {
1327
1329
  throw err;
1328
1330
  }
1329
1331
  };
1330
- const fetchEntitlementFromApi = async (token, oidcToken) => {
1332
+ const fetchEntitlementFromApi = async (token, oidcToken, orgId) => {
1331
1333
  const data = await requestApi("/v1/entitlement", {
1332
1334
  method: "POST",
1333
1335
  token: token || void 0,
1334
- oidcToken
1336
+ oidcToken,
1337
+ orgId
1335
1338
  });
1336
1339
  return EntitlementResponse.parse(data);
1337
1340
  };
@@ -1422,7 +1425,7 @@ const verifyEntitlementJwt = (jwt, options = {}) => {
1422
1425
  const claims = parsed.data;
1423
1426
  const expectedIssuer = process.env.RAIGAL_ISSUER?.trim() || "https://app.raigal.dev";
1424
1427
  if (claims.iss !== expectedIssuer) throw new JwtVerificationError(`Untrusted issuer: ${claims.iss}`, "invalid_issuer");
1425
- if (!isVersionAtLeast("0.2.4", 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");
1428
+ if (!isVersionAtLeast("0.2.6", 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
1429
  const now = options.nowSeconds ?? Math.floor(Date.now() / 1e3);
1427
1430
  const grace = options.allowGraceSeconds ?? 0;
1428
1431
  if (now > claims.exp + grace) {
@@ -1581,7 +1584,9 @@ const getEntitlement = async () => {
1581
1584
  }
1582
1585
  } else oidcToken = await fetchGitHubOidcToken();
1583
1586
  try {
1584
- const response = await fetchEntitlementFromApi(activeToken?.token ?? "", oidcToken);
1587
+ const tokenToPass = activeToken?.token ?? "";
1588
+ const orgId = process.env.RAIGAL_ORG_ID;
1589
+ const response = await fetchEntitlementFromApi(tokenToPass, oidcToken, orgId);
1585
1590
  const claims = verifyEntitlementJwt(response.entitlement, { nowSeconds: now });
1586
1591
  saveCachedEntitlement(response.entitlement, response.refresh_after);
1587
1592
  return {
@@ -1595,7 +1600,7 @@ const getEntitlement = async () => {
1595
1600
  };
1596
1601
  //#endregion
1597
1602
  //#region src/cloud/owner.ts
1598
- const GITHUB_REMOTE_RE$1 = /^(?:(?:git@|ssh:\/\/(?:git@)?)github\.com(?::(?:\d+\/)?|[:/])|https:\/\/(?:[^@]+@)?github\.com\/)([^/]+)\/([^/\s]+?)(?:\.git)?\s*$/;
1603
+ const GENERIC_GIT_REMOTE_RE = /^(?:(?:git@|ssh:\/\/(?:git@)?)[^:/]+(?::(?:\d+\/)?|[:/])|https?:\/\/(?:[^@]+@)?[^/]+\/)([^/]+)\/([^/\s]+?)(?:\.git)?\s*$/;
1599
1604
  const parseGitRemote = (rawUrl) => {
1600
1605
  if (!rawUrl) return {
1601
1606
  owner: null,
@@ -1603,18 +1608,19 @@ const parseGitRemote = (rawUrl) => {
1603
1608
  hasRemote: false,
1604
1609
  isGitHubRemote: false
1605
1610
  };
1606
- const match = rawUrl.match(GITHUB_REMOTE_RE$1);
1611
+ const match = rawUrl.match(GENERIC_GIT_REMOTE_RE);
1607
1612
  if (!match || !match[1] || !match[2]) return {
1608
1613
  owner: null,
1609
1614
  repo: null,
1610
1615
  hasRemote: true,
1611
1616
  isGitHubRemote: false
1612
1617
  };
1618
+ const isGitHub = rawUrl.includes("github.com");
1613
1619
  return {
1614
1620
  owner: match[1].toLowerCase(),
1615
1621
  repo: match[2].toLowerCase(),
1616
1622
  hasRemote: true,
1617
- isGitHubRemote: true
1623
+ isGitHubRemote: isGitHub
1618
1624
  };
1619
1625
  };
1620
1626
  const readGitConfigRemote = (dir) => {
@@ -1679,6 +1685,27 @@ const detectLocalRepoOwner = (directory) => {
1679
1685
  isGitHubRemote: true
1680
1686
  };
1681
1687
  }
1688
+ const gitlabRepo = process.env.CI_PROJECT_PATH?.trim();
1689
+ if (gitlabRepo && gitlabRepo.includes("/")) {
1690
+ const parts = gitlabRepo.split("/");
1691
+ const repo = parts.pop() ?? "";
1692
+ return {
1693
+ owner: (parts[0] ?? "").toLowerCase(),
1694
+ repo: repo.toLowerCase(),
1695
+ hasRemote: true,
1696
+ isGitHubRemote: false
1697
+ };
1698
+ }
1699
+ const bitbucketRepo = (process.env.BITBUCKET_REPO_FULL_NAME || process.env.BITBUCKET_REPO_SLUG)?.trim();
1700
+ if (bitbucketRepo) {
1701
+ const [owner = process.env.BITBUCKET_WORKSPACE || "", repo = ""] = bitbucketRepo.includes("/") ? bitbucketRepo.split("/") : [process.env.BITBUCKET_WORKSPACE || "", bitbucketRepo];
1702
+ return {
1703
+ owner: owner.toLowerCase(),
1704
+ repo: repo.toLowerCase(),
1705
+ hasRemote: true,
1706
+ isGitHubRemote: false
1707
+ };
1708
+ }
1682
1709
  const gitRoot = findGitRoot$1(directory);
1683
1710
  if (!gitRoot) return {
1684
1711
  owner: null,
@@ -1690,26 +1717,17 @@ const detectLocalRepoOwner = (directory) => {
1690
1717
  if (rawUrl !== null) return parseGitRemote(rawUrl);
1691
1718
  return parseGitRemote(spawnGitRemote(gitRoot));
1692
1719
  };
1693
- const isNonGitHubCi = () => {
1694
- const env = process.env;
1695
- if (!(env.CI === "true" || env.CI === "1")) return false;
1696
- return Boolean(env.GITLAB_CI || env.BITBUCKET_BUILD_NUMBER || env.CIRCLECI && !env.CIRCLE_PROJECT_REPONAME?.includes("/") || env.BUILDKITE || env.TRAVIS || env.JENKINS_URL);
1697
- };
1698
1720
  const checkOwnerAgainstAllowlist = (detected, allowedOwners, tokenKind) => {
1699
- if (isNonGitHubCi()) return {
1700
- allowed: false,
1701
- reason: "Non-GitHub CI environments are unsupported under organization allowlists."
1702
- };
1703
1721
  if (!detected.hasRemote) {
1704
1722
  if (tokenKind === "session") return { allowed: true };
1705
1723
  return {
1706
1724
  allowed: false,
1707
- reason: "Repository has no GitHub remote. CI and API key runs require an origin remote matching an allowed organization."
1725
+ reason: "Repository has no origin remote. CI and API key runs require an origin remote matching an allowed organization."
1708
1726
  };
1709
1727
  }
1710
- if (!detected.isGitHubRemote || !detected.owner) return {
1728
+ if (!detected.owner) return {
1711
1729
  allowed: false,
1712
- reason: "Repository remote is not hosted on GitHub or could not be determined."
1730
+ reason: "Repository remote owner could not be determined."
1713
1731
  };
1714
1732
  const lowerOwner = detected.owner.toLowerCase();
1715
1733
  if (!allowedOwners.some((allowed) => allowed.login.toLowerCase() === lowerOwner)) {
@@ -1717,7 +1735,7 @@ const checkOwnerAgainstAllowlist = (detected, allowedOwners, tokenKind) => {
1717
1735
  return {
1718
1736
  allowed: false,
1719
1737
  owner: lowerOwner,
1720
- reason: `GitHub owner '${detected.owner}' is not on the organization allowlist (${allowedLogins || "none"}).`
1738
+ reason: `Repository owner '${detected.owner}' is not on the organization allowlist (${allowedLogins || "none"}).`
1721
1739
  };
1722
1740
  }
1723
1741
  return {
@@ -2805,8 +2823,8 @@ const detectGitContext = (directory) => {
2805
2823
  const eventPayload = readGitHubEventPayload();
2806
2824
  const pr = eventPayload?.pull_request;
2807
2825
  let headSha = pr?.head?.sha;
2808
- if (!headSha) headSha = process.env.GITHUB_SHA || execGit(["rev-parse", "HEAD"], directory);
2809
- let branch = process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME;
2826
+ if (!headSha) headSha = process.env.GITHUB_SHA || process.env.CI_COMMIT_SHA || process.env.BITBUCKET_COMMIT || execGit(["rev-parse", "HEAD"], directory);
2827
+ let branch = process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || process.env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME || process.env.CI_COMMIT_REF_NAME || process.env.BITBUCKET_BRANCH;
2810
2828
  if (!branch) {
2811
2829
  branch = execGit([
2812
2830
  "rev-parse",
@@ -2815,7 +2833,7 @@ const detectGitContext = (directory) => {
2815
2833
  ], directory);
2816
2834
  if (branch === "HEAD") branch = void 0;
2817
2835
  }
2818
- let baseRef = pr?.base?.ref || process.env.GITHUB_BASE_REF;
2836
+ let baseRef = pr?.base?.ref || process.env.GITHUB_BASE_REF || process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME || process.env.BITBUCKET_PR_DESTINATION_BRANCH;
2819
2837
  if (!baseRef) baseRef = void 0;
2820
2838
  let prNumber;
2821
2839
  if (typeof pr?.number === "number") prNumber = pr.number;
@@ -2823,6 +2841,12 @@ const detectGitContext = (directory) => {
2823
2841
  else if (process.env.RAIGAL_PR_NUMBER || process.env.PR_NUMBER) {
2824
2842
  const parsed = Number.parseInt(process.env.RAIGAL_PR_NUMBER || process.env.PR_NUMBER || "", 10);
2825
2843
  if (!Number.isNaN(parsed) && parsed > 0) prNumber = parsed;
2844
+ } else if (process.env.CI_MERGE_REQUEST_IID) {
2845
+ const parsed = Number.parseInt(process.env.CI_MERGE_REQUEST_IID, 10);
2846
+ if (!Number.isNaN(parsed) && parsed > 0) prNumber = parsed;
2847
+ } else if (process.env.BITBUCKET_PR_ID) {
2848
+ const parsed = Number.parseInt(process.env.BITBUCKET_PR_ID, 10);
2849
+ if (!Number.isNaN(parsed) && parsed > 0) prNumber = parsed;
2826
2850
  } else if (process.env.GITHUB_REF) {
2827
2851
  const match = process.env.GITHUB_REF.match(/refs\/pull\/(\d+)(?:\/|$)/);
2828
2852
  if (match && match[1]) prNumber = Number.parseInt(match[1], 10);
@@ -2879,7 +2903,31 @@ const detectCiContext = () => {
2879
2903
  ...runUrl ? { run_url: runUrl } : {}
2880
2904
  };
2881
2905
  }
2882
- if (process.env.CI === "true" || process.env.CI === "1") return { provider: process.env.GITLAB_CI && "gitlab-ci" || process.env.BITBUCKET_BUILD_NUMBER && "bitbucket-pipelines" || process.env.CIRCLECI && "circleci" || process.env.BUILDKITE && "buildkite" || process.env.TRAVIS && "travis-ci" || "generic-ci" };
2906
+ if (process.env.GITLAB_CI) {
2907
+ const runUrl = process.env.CI_JOB_URL || process.env.CI_PIPELINE_URL;
2908
+ return {
2909
+ provider: "gitlab-ci",
2910
+ ...runUrl ? { run_url: runUrl } : {}
2911
+ };
2912
+ }
2913
+ if (process.env.BITBUCKET_BUILD_NUMBER) {
2914
+ const workspace = process.env.BITBUCKET_WORKSPACE;
2915
+ const repoSlug = process.env.BITBUCKET_REPO_SLUG;
2916
+ const buildNumber = process.env.BITBUCKET_BUILD_NUMBER;
2917
+ const runUrl = workspace && repoSlug && buildNumber ? `https://bitbucket.org/${workspace}/${repoSlug}/pipelines/results/${buildNumber}` : void 0;
2918
+ return {
2919
+ provider: "bitbucket-pipelines",
2920
+ ...runUrl ? { run_url: runUrl } : {}
2921
+ };
2922
+ }
2923
+ if (process.env.CI === "true" || process.env.CI === "1") {
2924
+ const provider = process.env.CIRCLECI && "circleci" || process.env.BUILDKITE && "buildkite" || process.env.TRAVIS && "travis-ci" || "generic-ci";
2925
+ const runUrl = process.env.CIRCLE_BUILD_URL || process.env.BUILDKITE_BUILD_URL || process.env.TRAVIS_BUILD_WEB_URL;
2926
+ return {
2927
+ provider,
2928
+ ...runUrl ? { run_url: runUrl } : {}
2929
+ };
2930
+ }
2883
2931
  };
2884
2932
  //#endregion
2885
2933
  //#region src/cloud/outbox.ts
@@ -4791,10 +4839,53 @@ jobs:
4791
4839
  runs-on: ubuntu-latest
4792
4840
  steps:
4793
4841
  - uses: actions/checkout@v4
4842
+ with:
4843
+ fetch-depth: 0
4794
4844
  - uses: actions/setup-node@v4
4795
4845
  with:
4796
4846
  node-version: 22
4797
4847
  - run: npx --yes @methiu/raigal@latest ci .
4848
+ env:
4849
+ RAIGAL_TOKEN: \${{ secrets.RAIGAL_TOKEN }}
4850
+ # RAIGAL_ORG_ID: \${{ vars.RAIGAL_ORG_ID }} # Optional: required if repository owner belongs to multiple Raigal organizations
4851
+ `;
4852
+ const DEFAULT_GITLAB_CI_FILE = ".gitlab-ci.yml";
4853
+ const DEFAULT_GITLAB_CI_YAML = `raigal_quality_gate:
4854
+ image: node:24
4855
+ stage: test
4856
+ script:
4857
+ - if [ -n "$CI_MERGE_REQUEST_IID" ]; then npx --yes @methiu/raigal@latest ci --changes --base "$CI_MERGE_REQUEST_DIFF_BASE_SHA"; else npx --yes @methiu/raigal@latest ci .; fi
4858
+ variables:
4859
+ RAIGAL_TOKEN: $RAIGAL_TOKEN
4860
+ # RAIGAL_ORG_ID: $RAIGAL_ORG_ID # Optional: required if repository owner belongs to multiple Raigal organizations
4861
+ rules:
4862
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
4863
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
4864
+ `;
4865
+ const DEFAULT_BITBUCKET_FILE = "bitbucket-pipelines.yml";
4866
+ const DEFAULT_BITBUCKET_PIPELINES_YAML = `pipelines:
4867
+ default:
4868
+ - step:
4869
+ name: raigal quality gate
4870
+ image: node:24
4871
+ script:
4872
+ - npx --yes @methiu/raigal@latest ci .
4873
+ secrets:
4874
+ - RAIGAL_TOKEN
4875
+ # - RAIGAL_ORG_ID
4876
+ pull-requests:
4877
+ "**":
4878
+ - step:
4879
+ name: raigal quality gate
4880
+ image: node:24
4881
+ clone:
4882
+ depth: full
4883
+ script:
4884
+ - git fetch origin "$BITBUCKET_PR_DESTINATION_BRANCH"
4885
+ - npx --yes @methiu/raigal@latest ci --changes --base FETCH_HEAD
4886
+ secrets:
4887
+ - RAIGAL_TOKEN
4888
+ # - RAIGAL_ORG_ID
4798
4889
  `;
4799
4890
  const DEFAULT_RULES_YAML = `# Architecture rules (BYO)
4800
4891
  # Uncomment and customize to enforce your project's conventions.
@@ -22658,13 +22749,21 @@ const runPiHook = async (deps = {}) => {
22658
22749
  release();
22659
22750
  }
22660
22751
  };
22661
- const AISLOP_MD_BODY = `# Raigal - agent instructions
22752
+ //#endregion
22753
+ //#region src/hooks/assets.ts
22754
+ const buildRaigalMdBody = (agent) => {
22755
+ let onEditSection;
22756
+ if (agent === "claude") onEditSection = `A PostToolUse hook runs \`raigal hook claude\` after every Edit, Write, or MultiEdit. It scans the touched files and returns findings as JSON \`additionalContext\` shaped like \`RaigalFeedback\` (score, baseline, delta, regressed, counts, findings, newSinceBaseline, suggestedActions). Act on them the same turn; the \`suggestedActions\` field tells you exactly what to run next.`;
22757
+ else if (agent === "gemini") onEditSection = `An AfterTool hook runs \`raigal hook gemini\` after file modifications. It scans the touched files and returns findings. Act on them the same turn; fix reported issues before completing.`;
22758
+ else if (agent === "cursor") onEditSection = `An afterFileEdit hook runs \`raigal hook cursor\` after file modifications. It scans the touched files and reports quality findings. Fix reported errors and fixable warnings before completing your turn.`;
22759
+ else onEditSection = `Raigal scans changed files and reports code quality, security, and AI pattern findings. Act on reported findings during your turn; fix errors and fixable warnings before completing your response.`;
22760
+ return `# Raigal - agent instructions
22662
22761
 
22663
22762
  [Raigal](https://github.com/meethiu/raigal) is your guardrail for shipping clean, maintainable, production-grade code. It catches the patterns AI coding tools leave behind: dead imports, redundant code, duplicate logic, unsafe any-typed casts, narrative comments, swallowed errors, hidden fallbacks, oversized functions, risky security constructs - and grades the project out of 100 so regressions are visible. Treat its findings as blocking, not advisory: the goal is code a human maintainer would happily inherit.
22664
22763
 
22665
22764
  ## On every edit
22666
22765
 
22667
- A PostToolUse hook runs \`raigal hook claude\` after every Edit, Write, or MultiEdit. It scans the touched files and returns findings as JSON \`additionalContext\` shaped like \`RaigalFeedback\` (score, baseline, delta, regressed, counts, findings, newSinceBaseline, suggestedActions). Act on them the same turn; the \`suggestedActions\` field tells you exactly what to run next.
22766
+ ${onEditSection}
22668
22767
 
22669
22768
  ## Severity ladder
22670
22769
 
@@ -22684,15 +22783,17 @@ A PostToolUse hook runs \`raigal hook claude\` after every Edit, Write, or Multi
22684
22783
  - If a finding is a false positive, leave it and explain in your reply - do not delete the rule config.
22685
22784
  - The findings payload includes \`nextSteps[]\` - treat those as your plan for the turn.
22686
22785
  `;
22786
+ };
22787
+ buildRaigalMdBody("claude");
22687
22788
  //#endregion
22688
22789
  //#region src/hooks/io/sentinel.ts
22689
22790
  const sentinelHash = (content) => `sha256:${crypto.createHash("sha256").update(content).digest("hex").slice(0, 32)}`;
22690
- const BEGIN_RE = /<!--\s*aislop:begin\s+v(\d+)(?:\s+hash=([^\s>]+))?\s*-->/;
22691
- const END_RE = /<!--\s*aislop:end\s+v\d+\s*-->/;
22791
+ const BEGIN_RE = /<!--\s*(?:raigal|aislop):begin\s+v(\d+)(?:\s+hash=([^\s>]+))?\s*-->/;
22792
+ const END_RE = /<!--\s*(?:raigal|aislop):end\s+v\d+\s*-->/;
22692
22793
  const renderFence = (body, hash) => [
22693
- `<!-- aislop:begin v1 hash=${hash} -->`,
22794
+ `<!-- raigal:begin v1 hash=${hash} -->`,
22694
22795
  body.trimEnd(),
22695
- "<!-- aislop:end v1 -->"
22796
+ "<!-- raigal:end v1 -->"
22696
22797
  ].join("\n");
22697
22798
  const upsertMarkdownFence = (existing, body, hash) => {
22698
22799
  const fenced = renderFence(body, hash);
@@ -22757,11 +22858,20 @@ const applyRemoval = (result, opts, target, nextContent) => {
22757
22858
  //#region src/hooks/install/rules-only.ts
22758
22859
  const installRulesOnly = (opts, paths, summary) => {
22759
22860
  const result = emptyResult();
22760
- const next = upsertMarkdownFence(readIfExists(paths.rules), AISLOP_MD_BODY, sentinelHash(AISLOP_MD_BODY)).nextContent;
22861
+ const body = paths.body ?? buildRaigalMdBody();
22862
+ const next = upsertMarkdownFence(readIfExists(paths.rules) ?? (paths.legacyRules ? readIfExists(paths.legacyRules) : null), body, sentinelHash(body)).nextContent;
22761
22863
  applyContent(result, opts, paths.rules, next, summary);
22864
+ if (paths.legacyRules && readIfExists(paths.legacyRules) != null) {
22865
+ if (!opts.dryRun) try {
22866
+ fs.rmSync(paths.legacyRules, { force: true });
22867
+ } catch {}
22868
+ }
22762
22869
  if (paths.host && paths.marker) {
22763
22870
  const host = readIfExists(paths.host) ?? "";
22764
- if (!host.includes(paths.marker)) {
22871
+ if (paths.legacyMarker && host.includes(paths.legacyMarker)) {
22872
+ const migrated = host.split("\n").map((l) => l.trim() === paths.legacyMarker ? paths.marker : l).join("\n");
22873
+ applyContent(result, opts, paths.host, migrated, `update ${paths.marker} reference`);
22874
+ } else if (!host.includes(paths.marker)) {
22765
22875
  const joiner = host.endsWith("\n") || host.length === 0 ? "" : "\n";
22766
22876
  const prefix = host.length === 0 ? "" : `${host}${joiner}\n`;
22767
22877
  applyContent(result, opts, paths.host, `${prefix}${paths.marker}\n`, `append ${paths.marker} reference`);
@@ -22774,30 +22884,41 @@ const uninstallRulesOnly = (opts, paths) => {
22774
22884
  removed: [],
22775
22885
  skipped: []
22776
22886
  };
22777
- if (readIfExists(paths.rules) != null) applyRemoval(result, opts, paths.rules, null);
22778
- else result.skipped.push(paths.rules);
22779
- if (paths.host && paths.marker) {
22887
+ let removedRule = false;
22888
+ if (readIfExists(paths.rules) != null) {
22889
+ applyRemoval(result, opts, paths.rules, null);
22890
+ removedRule = true;
22891
+ }
22892
+ if (paths.legacyRules && readIfExists(paths.legacyRules) != null) {
22893
+ applyRemoval(result, opts, paths.legacyRules, null);
22894
+ removedRule = true;
22895
+ }
22896
+ if (!removedRule) result.skipped.push(paths.rules);
22897
+ if (paths.host && (paths.marker || paths.legacyMarker)) {
22780
22898
  const host = readIfExists(paths.host);
22781
- if (host?.includes(paths.marker)) {
22782
- const stripped = host.split("\n").filter((l) => l.trim() !== paths.marker).join("\n").replace(/\n{3,}/g, "\n\n").trim();
22899
+ if (host && (paths.marker && host.includes(paths.marker) || paths.legacyMarker && host.includes(paths.legacyMarker))) {
22900
+ const stripped = host.split("\n").filter((l) => l.trim() !== paths.marker && l.trim() !== paths.legacyMarker).join("\n").replace(/\n{3,}/g, "\n\n").trim();
22783
22901
  applyRemoval(result, opts, paths.host, stripped.length === 0 ? null : `${stripped}\n`);
22784
- } else result.skipped.push(paths.host);
22902
+ } else if (paths.host) result.skipped.push(paths.host);
22785
22903
  }
22786
22904
  return result;
22787
22905
  };
22788
22906
  //#endregion
22789
22907
  //#region src/hooks/install/antigravity.ts
22790
- const resolveAntigravityPaths = (opts) => ({ rules: path.join(opts.cwd, ".agents", "rules", "antigravity-aislop-rules.md") });
22908
+ const resolveAntigravityPaths = (opts) => ({
22909
+ rules: path.join(opts.cwd, ".agents", "rules", "antigravity-raigal-rules.md"),
22910
+ legacyRules: path.join(opts.cwd, ".agents", "rules", "antigravity-aislop-rules.md")
22911
+ });
22791
22912
  const installAntigravity = (opts) => {
22792
22913
  if (opts.scope !== "project") return {
22793
22914
  wrote: [],
22794
22915
  skipped: [],
22795
22916
  planned: [{
22796
- path: ".agents/rules/antigravity-aislop-rules.md",
22917
+ path: ".agents/rules/antigravity-raigal-rules.md",
22797
22918
  summary: "Antigravity is project-scope only; pass --project"
22798
22919
  }]
22799
22920
  };
22800
- return installRulesOnly(opts, resolveAntigravityPaths(opts), "write .agents/rules/antigravity-aislop-rules.md");
22921
+ return installRulesOnly(opts, resolveAntigravityPaths(opts), "write .agents/rules/antigravity-raigal-rules.md");
22801
22922
  };
22802
22923
  const uninstallAntigravity = (opts) => {
22803
22924
  if (opts.scope !== "project") return {
@@ -22806,9 +22927,7 @@ const uninstallAntigravity = (opts) => {
22806
22927
  };
22807
22928
  return uninstallRulesOnly(opts, resolveAntigravityPaths(opts));
22808
22929
  };
22809
- //#endregion
22810
- //#region src/hooks/io/json-patch.ts
22811
- const AISLOP_SENTINEL_KEY = "__aislop";
22930
+ const RAIGAL_SENTINEL_KEY = "__raigal";
22812
22931
  const isAislopManaged = (x) => typeof x === "object" && x !== null && ("__aislop" in x && x["__aislop"] != null || "__raigal" in x && x["__raigal"] != null);
22813
22932
  const groupIsAislop = (group) => {
22814
22933
  if (typeof group !== "object" || group === null) return false;
@@ -22858,6 +22977,7 @@ const resolveClaudePaths = (opts) => {
22858
22977
  const root = opts.scope === "project" ? path.join(opts.cwd, ".claude") : path.join(opts.home, ".claude");
22859
22978
  return {
22860
22979
  settings: path.join(root, "settings.json"),
22980
+ raigalMd: path.join(root, "RAIGAL.md"),
22861
22981
  aislopMd: path.join(root, "AISLOP.md"),
22862
22982
  claudeMd: path.join(root, "CLAUDE.md")
22863
22983
  };
@@ -22872,7 +22992,7 @@ const buildHookGroup$1 = () => {
22872
22992
  hooks: [{
22873
22993
  type: "command",
22874
22994
  command: "raigal hook claude",
22875
- [AISLOP_SENTINEL_KEY]: {
22995
+ [RAIGAL_SENTINEL_KEY]: {
22876
22996
  v: 1,
22877
22997
  managed: true,
22878
22998
  hash: sentinelHash(hashBody)
@@ -22887,7 +23007,7 @@ const buildStopHookGroup = () => {
22887
23007
  hooks: [{
22888
23008
  type: "command",
22889
23009
  command: "raigal hook claude --stop",
22890
- [AISLOP_SENTINEL_KEY]: {
23010
+ [RAIGAL_SENTINEL_KEY]: {
22891
23011
  v: 1,
22892
23012
  managed: true,
22893
23013
  hash: sentinelHash(hashBody)
@@ -22906,7 +23026,7 @@ const buildFileChangedHookGroup = () => {
22906
23026
  hooks: [{
22907
23027
  type: "command",
22908
23028
  command: "raigal hook claude --on-file-changed",
22909
- [AISLOP_SENTINEL_KEY]: {
23029
+ [RAIGAL_SENTINEL_KEY]: {
22910
23030
  v: 1,
22911
23031
  managed: true,
22912
23032
  hash: sentinelHash(hashBody)
@@ -22932,15 +23052,25 @@ const installClaude = (opts) => {
22932
23052
  const result = emptyResult();
22933
23053
  const nextSettings = renderSettings$1(readIfExists(paths.settings), Boolean(opts.qualityGate));
22934
23054
  applyContent(result, opts, paths.settings, nextSettings, "register PostToolUse + FileChanged hooks");
22935
- const mdHash = sentinelHash(AISLOP_MD_BODY);
22936
- const fenced = upsertMarkdownFence(readIfExists(paths.aislopMd), AISLOP_MD_BODY, mdHash);
22937
- applyContent(result, opts, paths.aislopMd, fenced.nextContent, "write AISLOP.md rules");
23055
+ const body = buildRaigalMdBody("claude");
23056
+ const mdHash = sentinelHash(body);
23057
+ const fenced = upsertMarkdownFence(readIfExists(paths.raigalMd) ?? readIfExists(paths.aislopMd), body, mdHash);
23058
+ applyContent(result, opts, paths.raigalMd, fenced.nextContent, "write RAIGAL.md rules");
23059
+ if (paths.raigalMd !== paths.aislopMd && readIfExists(paths.aislopMd) != null) {
23060
+ if (!opts.dryRun) try {
23061
+ fs.rmSync(paths.aislopMd, { force: true });
23062
+ } catch {}
23063
+ }
22938
23064
  const existingClaudeMd = readIfExists(paths.claudeMd) ?? "";
22939
- const marker = "@AISLOP.md";
22940
- if (!existingClaudeMd.includes(marker)) {
23065
+ const marker = "@RAIGAL.md";
23066
+ const legacyMarker = "@AISLOP.md";
23067
+ if (existingClaudeMd.includes(legacyMarker)) {
23068
+ const migrated = existingClaudeMd.split("\n").map((line) => line.trim() === legacyMarker ? marker : line).join("\n");
23069
+ applyContent(result, opts, paths.claudeMd, migrated, "update @RAIGAL.md reference");
23070
+ } else if (!existingClaudeMd.includes(marker)) {
22941
23071
  const joiner = existingClaudeMd.endsWith("\n") || existingClaudeMd.length === 0 ? "" : "\n";
22942
23072
  const prefix = existingClaudeMd.length === 0 ? "" : `${existingClaudeMd}${joiner}\n`;
22943
- applyContent(result, opts, paths.claudeMd, `${prefix}${marker}\n`, "append @AISLOP.md reference");
23073
+ applyContent(result, opts, paths.claudeMd, `${prefix}${marker}\n`, "append @RAIGAL.md reference");
22944
23074
  } else result.skipped.push(paths.claudeMd);
22945
23075
  return result;
22946
23076
  };
@@ -22969,11 +23099,19 @@ const uninstallClaude = (opts) => {
22969
23099
  if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.settings, null);
22970
23100
  else applyRemoval(result, opts, paths.settings, `${JSON.stringify(stripped, null, 2)}\n`);
22971
23101
  } else result.skipped.push(paths.settings);
22972
- if (readIfExists(paths.aislopMd) != null) applyRemoval(result, opts, paths.aislopMd, null);
22973
- else result.skipped.push(paths.aislopMd);
23102
+ let removedMd = false;
23103
+ if (readIfExists(paths.raigalMd) != null) {
23104
+ applyRemoval(result, opts, paths.raigalMd, null);
23105
+ removedMd = true;
23106
+ }
23107
+ if (readIfExists(paths.aislopMd) != null) {
23108
+ applyRemoval(result, opts, paths.aislopMd, null);
23109
+ removedMd = true;
23110
+ }
23111
+ if (!removedMd) result.skipped.push(paths.raigalMd);
22974
23112
  const claudeMd = readIfExists(paths.claudeMd);
22975
- if (claudeMd?.includes("@AISLOP.md")) {
22976
- const stripped = claudeMd.split("\n").filter((line) => line.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
23113
+ if (claudeMd?.includes("@RAIGAL.md") || claudeMd?.includes("@AISLOP.md")) {
23114
+ const stripped = claudeMd.split("\n").filter((line) => line.trim() !== "@RAIGAL.md" && line.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
22977
23115
  applyRemoval(result, opts, paths.claudeMd, stripped.length === 0 ? null : `${stripped}\n`);
22978
23116
  } else result.skipped.push(paths.claudeMd);
22979
23117
  return result;
@@ -22981,7 +23119,10 @@ const uninstallClaude = (opts) => {
22981
23119
  //#endregion
22982
23120
  //#region src/hooks/install/cline.ts
22983
23121
  const resolveClinePaths = (opts) => ({ rules: path.join(opts.cwd, ".clinerules") });
22984
- const resolveRooPaths = (opts) => ({ rules: path.join(opts.cwd, ".roo", "rules", "aislop.md") });
23122
+ const resolveRooPaths = (opts) => ({
23123
+ rules: path.join(opts.cwd, ".roo", "rules", "raigal.md"),
23124
+ legacyRules: path.join(opts.cwd, ".roo", "rules", "aislop.md")
23125
+ });
22985
23126
  const installCline = (opts) => {
22986
23127
  if (opts.scope !== "project") return {
22987
23128
  wrote: [],
@@ -22992,7 +23133,7 @@ const installCline = (opts) => {
22992
23133
  }]
22993
23134
  };
22994
23135
  const cline = installRulesOnly(opts, resolveClinePaths(opts), "write .clinerules");
22995
- const roo = installRulesOnly(opts, resolveRooPaths(opts), "write .roo/rules/aislop.md");
23136
+ const roo = installRulesOnly(opts, resolveRooPaths(opts), "write .roo/rules/raigal.md");
22996
23137
  return {
22997
23138
  wrote: [...cline.wrote, ...roo.wrote],
22998
23139
  skipped: [...cline.skipped, ...roo.skipped],
@@ -23043,7 +23184,8 @@ const resolveCursorPaths = (opts) => {
23043
23184
  const root = opts.scope === "project" ? path.join(opts.cwd, ".cursor") : path.join(opts.home, ".cursor");
23044
23185
  return {
23045
23186
  hooks: path.join(root, "hooks.json"),
23046
- rules: path.join(opts.cwd, ".cursor", "rules", "aislop.mdc")
23187
+ rules: path.join(opts.cwd, ".cursor", "rules", "raigal.mdc"),
23188
+ legacyRules: path.join(opts.cwd, ".cursor", "rules", "aislop.mdc")
23047
23189
  };
23048
23190
  };
23049
23191
  const buildHookEntry = () => {
@@ -23055,7 +23197,7 @@ const buildHookEntry = () => {
23055
23197
  command: "raigal hook cursor",
23056
23198
  type: "command",
23057
23199
  timeout: 5e3,
23058
- [AISLOP_SENTINEL_KEY]: {
23200
+ [RAIGAL_SENTINEL_KEY]: {
23059
23201
  v: 1,
23060
23202
  managed: true,
23061
23203
  hash: sentinelHash(hashBody)
@@ -23079,8 +23221,14 @@ const installCursor = (opts) => {
23079
23221
  const nextHooks = renderHooksJson(readIfExists(paths.hooks));
23080
23222
  applyContent(result, opts, paths.hooks, nextHooks, "register afterFileEdit hook");
23081
23223
  if (opts.scope === "project") {
23082
- const rules = upsertMarkdownFence(readIfExists(paths.rules), AISLOP_MD_BODY, sentinelHash(AISLOP_MD_BODY)).nextContent;
23083
- applyContent(result, opts, paths.rules, rules, "write .cursor/rules/aislop.mdc");
23224
+ const body = buildRaigalMdBody("cursor");
23225
+ const rules = upsertMarkdownFence(readIfExists(paths.rules) ?? (paths.legacyRules ? readIfExists(paths.legacyRules) : null), body, sentinelHash(body)).nextContent;
23226
+ applyContent(result, opts, paths.rules, rules, "write .cursor/rules/raigal.mdc");
23227
+ if (paths.legacyRules && readIfExists(paths.legacyRules) != null) {
23228
+ if (!opts.dryRun) try {
23229
+ fs.rmSync(paths.legacyRules, { force: true });
23230
+ } catch {}
23231
+ }
23084
23232
  }
23085
23233
  return result;
23086
23234
  };
@@ -23104,7 +23252,18 @@ const uninstallCursor = (opts) => {
23104
23252
  if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.hooks, null);
23105
23253
  else applyRemoval(result, opts, paths.hooks, `${JSON.stringify(stripped, null, 2)}\n`);
23106
23254
  } else result.skipped.push(paths.hooks);
23107
- if (opts.scope === "project") applyRemoval(result, opts, paths.rules, null);
23255
+ if (opts.scope === "project") {
23256
+ let removedRule = false;
23257
+ if (readIfExists(paths.rules) != null) {
23258
+ applyRemoval(result, opts, paths.rules, null);
23259
+ removedRule = true;
23260
+ }
23261
+ if (paths.legacyRules && readIfExists(paths.legacyRules) != null) {
23262
+ applyRemoval(result, opts, paths.legacyRules, null);
23263
+ removedRule = true;
23264
+ }
23265
+ if (!removedRule) result.skipped.push(paths.rules);
23266
+ }
23108
23267
  return result;
23109
23268
  };
23110
23269
  //#endregion
@@ -23113,6 +23272,7 @@ const resolveGeminiPaths = (opts) => {
23113
23272
  const root = opts.scope === "project" ? path.join(opts.cwd, ".gemini") : path.join(opts.home, ".gemini");
23114
23273
  return {
23115
23274
  settings: path.join(root, "settings.json"),
23275
+ raigalMd: path.join(root, "RAIGAL.md"),
23116
23276
  aislopMd: path.join(root, "AISLOP.md"),
23117
23277
  geminiMd: path.join(root, "GEMINI.md")
23118
23278
  };
@@ -23129,7 +23289,7 @@ const buildHookGroup = () => {
23129
23289
  type: "command",
23130
23290
  command: "raigal hook gemini",
23131
23291
  timeout: 5e3,
23132
- [AISLOP_SENTINEL_KEY]: {
23292
+ [RAIGAL_SENTINEL_KEY]: {
23133
23293
  v: 1,
23134
23294
  managed: true,
23135
23295
  hash: sentinelHash(hashBody)
@@ -23152,14 +23312,25 @@ const installGemini = (opts) => {
23152
23312
  const result = emptyResult();
23153
23313
  const next = renderSettings(readIfExists(paths.settings));
23154
23314
  applyContent(result, opts, paths.settings, next, "register AfterTool hook");
23155
- const fenced = upsertMarkdownFence(readIfExists(paths.aislopMd), AISLOP_MD_BODY, sentinelHash(AISLOP_MD_BODY)).nextContent;
23156
- applyContent(result, opts, paths.aislopMd, fenced, "write AISLOP.md rules");
23315
+ const body = buildRaigalMdBody("gemini");
23316
+ const hash = sentinelHash(body);
23317
+ const fenced = upsertMarkdownFence(readIfExists(paths.raigalMd) ?? readIfExists(paths.aislopMd), body, hash).nextContent;
23318
+ applyContent(result, opts, paths.raigalMd, fenced, "write RAIGAL.md rules");
23319
+ if (paths.raigalMd !== paths.aislopMd && readIfExists(paths.aislopMd) != null) {
23320
+ if (!opts.dryRun) try {
23321
+ fs.rmSync(paths.aislopMd, { force: true });
23322
+ } catch {}
23323
+ }
23157
23324
  const existingGeminiMd = readIfExists(paths.geminiMd) ?? "";
23158
- const marker = "@AISLOP.md";
23159
- if (!existingGeminiMd.includes(marker)) {
23325
+ const marker = "@RAIGAL.md";
23326
+ const legacyMarker = "@AISLOP.md";
23327
+ if (existingGeminiMd.includes(legacyMarker)) {
23328
+ const migrated = existingGeminiMd.split("\n").map((l) => l.trim() === legacyMarker ? marker : l).join("\n");
23329
+ applyContent(result, opts, paths.geminiMd, migrated, "update @RAIGAL.md reference");
23330
+ } else if (!existingGeminiMd.includes(marker)) {
23160
23331
  const joiner = existingGeminiMd.endsWith("\n") || existingGeminiMd.length === 0 ? "" : "\n";
23161
23332
  const prefix = existingGeminiMd.length === 0 ? "" : `${existingGeminiMd}${joiner}\n`;
23162
- applyContent(result, opts, paths.geminiMd, `${prefix}${marker}\n`, "append @AISLOP.md reference");
23333
+ applyContent(result, opts, paths.geminiMd, `${prefix}${marker}\n`, "append @RAIGAL.md reference");
23163
23334
  } else result.skipped.push(paths.geminiMd);
23164
23335
  return result;
23165
23336
  };
@@ -23183,28 +23354,39 @@ const uninstallGemini = (opts) => {
23183
23354
  if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.settings, null);
23184
23355
  else applyRemoval(result, opts, paths.settings, `${JSON.stringify(stripped, null, 2)}\n`);
23185
23356
  } else result.skipped.push(paths.settings);
23186
- if (readIfExists(paths.aislopMd) != null) applyRemoval(result, opts, paths.aislopMd, null);
23187
- else result.skipped.push(paths.aislopMd);
23357
+ let removedMd = false;
23358
+ if (readIfExists(paths.raigalMd) != null) {
23359
+ applyRemoval(result, opts, paths.raigalMd, null);
23360
+ removedMd = true;
23361
+ }
23362
+ if (readIfExists(paths.aislopMd) != null) {
23363
+ applyRemoval(result, opts, paths.aislopMd, null);
23364
+ removedMd = true;
23365
+ }
23366
+ if (!removedMd) result.skipped.push(paths.raigalMd);
23188
23367
  const geminiMd = readIfExists(paths.geminiMd);
23189
- if (geminiMd?.includes("@AISLOP.md")) {
23190
- const stripped = geminiMd.split("\n").filter((l) => l.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
23368
+ if (geminiMd?.includes("@RAIGAL.md") || geminiMd?.includes("@AISLOP.md")) {
23369
+ const stripped = geminiMd.split("\n").filter((l) => l.trim() !== "@RAIGAL.md" && l.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
23191
23370
  applyRemoval(result, opts, paths.geminiMd, stripped.length === 0 ? null : `${stripped}\n`);
23192
23371
  } else result.skipped.push(paths.geminiMd);
23193
23372
  return result;
23194
23373
  };
23195
23374
  //#endregion
23196
23375
  //#region src/hooks/install/kilocode.ts
23197
- const resolveKilocodePaths = (opts) => ({ rules: path.join(opts.cwd, ".kilocode", "rules", "aislop-rules.md") });
23376
+ const resolveKilocodePaths = (opts) => ({
23377
+ rules: path.join(opts.cwd, ".kilocode", "rules", "raigal-rules.md"),
23378
+ legacyRules: path.join(opts.cwd, ".kilocode", "rules", "aislop-rules.md")
23379
+ });
23198
23380
  const installKilocode = (opts) => {
23199
23381
  if (opts.scope !== "project") return {
23200
23382
  wrote: [],
23201
23383
  skipped: [],
23202
23384
  planned: [{
23203
- path: ".kilocode/rules/aislop-rules.md",
23385
+ path: ".kilocode/rules/raigal-rules.md",
23204
23386
  summary: "Kilo Code is project-scope only; pass --project"
23205
23387
  }]
23206
23388
  };
23207
- return installRulesOnly(opts, resolveKilocodePaths(opts), "write .kilocode/rules/aislop-rules.md");
23389
+ return installRulesOnly(opts, resolveKilocodePaths(opts), "write .kilocode/rules/raigal-rules.md");
23208
23390
  };
23209
23391
  const uninstallKilocode = (opts) => {
23210
23392
  if (opts.scope !== "project") return {
@@ -23358,18 +23540,24 @@ const paths = {
23358
23540
  const p = resolveClaudePaths(opts);
23359
23541
  return [
23360
23542
  p.settings,
23543
+ p.raigalMd,
23361
23544
  p.aislopMd,
23362
23545
  p.claudeMd
23363
23546
  ];
23364
23547
  },
23365
23548
  cursor: (opts) => {
23366
23549
  const p = resolveCursorPaths(opts);
23367
- return opts.scope === "project" ? [p.hooks, p.rules] : [p.hooks];
23550
+ return opts.scope === "project" ? [
23551
+ p.hooks,
23552
+ p.rules,
23553
+ ...p.legacyRules ? [p.legacyRules] : []
23554
+ ] : [p.hooks];
23368
23555
  },
23369
23556
  gemini: (opts) => {
23370
23557
  const p = resolveGeminiPaths(opts);
23371
23558
  return [
23372
23559
  p.settings,
23560
+ p.raigalMd,
23373
23561
  p.aislopMd,
23374
23562
  p.geminiMd
23375
23563
  ];
@@ -23377,9 +23565,22 @@ const paths = {
23377
23565
  pi: (opts) => [resolvePiPaths(opts).extension],
23378
23566
  codex: (opts) => [resolveCodexPaths(opts).rules],
23379
23567
  windsurf: (opts) => [resolveWindsurfPaths(opts).rules],
23380
- cline: (opts) => [resolveClinePaths(opts).rules, resolveRooPaths(opts).rules],
23381
- kilocode: (opts) => [resolveKilocodePaths(opts).rules],
23382
- antigravity: (opts) => [resolveAntigravityPaths(opts).rules],
23568
+ cline: (opts) => {
23569
+ const roo = resolveRooPaths(opts);
23570
+ return [
23571
+ resolveClinePaths(opts).rules,
23572
+ roo.rules,
23573
+ ...roo.legacyRules ? [roo.legacyRules] : []
23574
+ ];
23575
+ },
23576
+ kilocode: (opts) => {
23577
+ const k = resolveKilocodePaths(opts);
23578
+ return [k.rules, ...k.legacyRules ? [k.legacyRules] : []];
23579
+ },
23580
+ antigravity: (opts) => {
23581
+ const a = resolveAntigravityPaths(opts);
23582
+ return [a.rules, ...a.legacyRules ? [a.legacyRules] : []];
23583
+ },
23383
23584
  copilot: (opts) => [resolveCopilotPaths(opts).rules]
23384
23585
  };
23385
23586
  const REGISTRY = {
@@ -25030,7 +25231,7 @@ const maybeNotifyUpdate = async (now = Date.now()) => {
25030
25231
  if (!process.stderr.isTTY) return;
25031
25232
  const cachePath = resolveUpdateCachePath();
25032
25233
  const cache = readCache(cachePath);
25033
- if (cache && isOutdated("0.2.4", cache.latest)) process.stderr.write(formatUpdateNotice(APP_VERSION, cache.latest));
25234
+ if (cache && isOutdated("0.2.6", cache.latest)) process.stderr.write(formatUpdateNotice(APP_VERSION, cache.latest));
25034
25235
  if (!cache || now - cache.checkedAt > CHECK_INTERVAL_MS) {
25035
25236
  const latest = await fetchLatestVersion$1();
25036
25237
  if (latest) writeCache(cachePath, {
@@ -25304,6 +25505,10 @@ const COMMAND_REFERENCE = [
25304
25505
  summary: "Stop a running background agent session",
25305
25506
  flags: ["--root <directory>", "--force"]
25306
25507
  },
25508
+ {
25509
+ command: "raigal mcp",
25510
+ summary: "Run the Model Context Protocol (MCP) server over stdio"
25511
+ },
25307
25512
  {
25308
25513
  command: "raigal ci [directory]",
25309
25514
  summary: "Run the CI quality gate with thresholded exit codes",
@@ -25483,6 +25688,7 @@ const commandGroupLabel = (command) => {
25483
25688
  if (command.startsWith("raigal scan")) return "Core Workflow";
25484
25689
  if (command.startsWith("raigal fix")) return "Core Workflow";
25485
25690
  if (command.startsWith("raigal ci")) return "Core Workflow";
25691
+ if (command.startsWith("raigal mcp")) return "Local Agent";
25486
25692
  if (command.startsWith("raigal hook")) return "Hooks";
25487
25693
  if (command === "raigal hooks") return "Hooks";
25488
25694
  if (command.startsWith("raigal install")) return "Hooks";
@@ -25550,7 +25756,7 @@ const renderCommandGroup = (group, commandWidth) => {
25550
25756
  return lines;
25551
25757
  };
25552
25758
  const renderCommandReference = (input = {}) => {
25553
- const version = input.version ?? "0.2.4";
25759
+ const version = input.version ?? "0.2.6";
25554
25760
  const commandWidth = Math.max(...COMMAND_REFERENCE.map((item) => item.command.length));
25555
25761
  const lines = [renderHeader({
25556
25762
  version,
@@ -25773,7 +25979,7 @@ const renderHelpDetails = () => [
25773
25979
  ].map((line) => highlightRaigal(line, theme)).join("\n");
25774
25980
  const renderHome = (input = {}) => {
25775
25981
  let out = renderHeader({
25776
- version: input.version ?? "0.2.4",
25982
+ version: input.version ?? "0.2.6",
25777
25983
  command: "--bare",
25778
25984
  context: []
25779
25985
  });
@@ -26444,12 +26650,13 @@ const baseRefExists = (cwd, ref) => {
26444
26650
  return !result.error && result.status === 0;
26445
26651
  };
26446
26652
  const getChangedFiles = (cwd, base) => {
26653
+ const trimmedBase = base?.trim();
26447
26654
  const diff = spawnSync("git", [
26448
26655
  "diff",
26449
26656
  "--no-color",
26450
26657
  "--name-only",
26451
26658
  "--diff-filter=ACMR",
26452
- base ?? "HEAD"
26659
+ trimmedBase && trimmedBase.length > 0 ? trimmedBase : "HEAD"
26453
26660
  ], {
26454
26661
  cwd,
26455
26662
  encoding: "utf-8",
@@ -26501,13 +26708,14 @@ const listUntrackedFiles = (cwd) => {
26501
26708
  return untracked.stdout.split("\n").filter((line) => line.length > 0);
26502
26709
  };
26503
26710
  const getChangedLineMap = (cwd, base) => {
26711
+ const trimmedBase = base?.trim();
26504
26712
  const diff = spawnSync("git", [
26505
26713
  "diff",
26506
26714
  "--no-color",
26507
26715
  "--no-ext-diff",
26508
26716
  "-U0",
26509
26717
  "--diff-filter=ACMR",
26510
- base ?? "HEAD"
26718
+ trimmedBase && trimmedBase.length > 0 ? trimmedBase : "HEAD"
26511
26719
  ], {
26512
26720
  cwd,
26513
26721
  encoding: "utf-8",
@@ -26782,10 +26990,13 @@ const isHistoryComparableScan = (options) => !options.staged && !options.changes
26782
26990
  const isFullProjectScan = (options) => isHistoryComparableScan(options) && !options.include?.length && !options.exclude?.length;
26783
26991
  const resolveScanScopeMode = (options) => {
26784
26992
  if (options.staged) return { kind: "staged" };
26785
- if (options.changes) return options.base ? {
26786
- kind: "changes",
26787
- base: options.base
26788
- } : { kind: "changes" };
26993
+ if (options.changes) {
26994
+ const base = options.base?.trim();
26995
+ return base ? {
26996
+ kind: "changes",
26997
+ base
26998
+ } : { kind: "changes" };
26999
+ }
26789
27000
  return { kind: "full" };
26790
27001
  };
26791
27002
  //#endregion
@@ -27144,7 +27355,8 @@ const buildScanRender = (input) => {
27144
27355
  const scanTargetError = (resolvedDir, options) => {
27145
27356
  if (!fs.existsSync(resolvedDir)) return `Path does not exist: ${resolvedDir}`;
27146
27357
  if (!fs.statSync(resolvedDir).isDirectory()) return `Not a directory: ${resolvedDir}`;
27147
- if (options.changes && options.base && !baseRefExists(resolvedDir, options.base)) return `Could not resolve base ref "${options.base}". Make sure it exists and was fetched (e.g. \`git fetch origin ${options.base}\`).`;
27358
+ const base = options.base?.trim();
27359
+ if (options.changes && base && !baseRefExists(resolvedDir, base)) return `Could not resolve base ref "${base}". Make sure it exists and was fetched (e.g. \`git fetch origin ${base}\`).`;
27148
27360
  return null;
27149
27361
  };
27150
27362
  //#endregion
@@ -29900,10 +30112,12 @@ const buildInitSuccessRender = (input) => {
29900
30112
  }, deps);
29901
30113
  const writtenCount = input.steps.filter((s) => s.status === "done").length;
29902
30114
  const footer = writtenCount === 0 ? "Nothing to write" : `Done · wrote ${writtenCount} file${writtenCount === 1 ? "" : "s"}`;
29903
- return `${header}${renderRail({
30115
+ const rail = renderRail({
29904
30116
  steps: input.steps,
29905
30117
  footer
29906
- }, deps)}\n${renderHintLine(`Try ${input.nextCommand}`, deps)}`;
30118
+ }, deps);
30119
+ const extraHint = input.hint ? `\n${renderHintLine(input.hint, deps)}` : "";
30120
+ return `${header}${rail}\n${renderHintLine(`Try ${input.nextCommand}`, deps)}${extraHint}`;
29907
30121
  };
29908
30122
  const ENGINE_CHOICES = [
29909
30123
  {
@@ -29954,7 +30168,45 @@ const writeGithubWorkflow = (rootDirectory, enabled) => {
29954
30168
  relativePath
29955
30169
  };
29956
30170
  };
29957
- const promptForConfigChoices = async () => {
30171
+ const writeGitlabWorkflow = (rootDirectory, enabled) => {
30172
+ const relativePath = DEFAULT_GITLAB_CI_FILE;
30173
+ if (!enabled) return { status: "declined" };
30174
+ const workflowPath = path.join(rootDirectory, DEFAULT_GITLAB_CI_FILE);
30175
+ if (fs.existsSync(workflowPath)) return {
30176
+ status: "skipped-exists",
30177
+ relativePath
30178
+ };
30179
+ fs.writeFileSync(workflowPath, DEFAULT_GITLAB_CI_YAML);
30180
+ return {
30181
+ status: "written",
30182
+ relativePath
30183
+ };
30184
+ };
30185
+ const writeBitbucketWorkflow = (rootDirectory, enabled) => {
30186
+ const relativePath = DEFAULT_BITBUCKET_FILE;
30187
+ if (!enabled) return { status: "declined" };
30188
+ const workflowPath = path.join(rootDirectory, DEFAULT_BITBUCKET_FILE);
30189
+ if (fs.existsSync(workflowPath)) return {
30190
+ status: "skipped-exists",
30191
+ relativePath
30192
+ };
30193
+ fs.writeFileSync(workflowPath, DEFAULT_BITBUCKET_PIPELINES_YAML);
30194
+ return {
30195
+ status: "written",
30196
+ relativePath
30197
+ };
30198
+ };
30199
+ const detectDefaultCiProvider = (directory) => {
30200
+ if (fs.existsSync(path.join(directory, ".gitlab-ci.yml"))) return "gitlab";
30201
+ if (fs.existsSync(path.join(directory, "bitbucket-pipelines.yml"))) return "bitbucket";
30202
+ if (fs.existsSync(path.join(directory, ".github/workflows"))) return "github";
30203
+ const detectedOwner = detectLocalRepoOwner(directory);
30204
+ if (detectedOwner.hasRemote) {
30205
+ if (detectedOwner.isGitHubRemote) return "github";
30206
+ }
30207
+ return "github";
30208
+ };
30209
+ const promptForConfigChoices = async (directory) => {
29958
30210
  const enginesSelection = await runCancellable(() => multiselect({
29959
30211
  message: "Which engines should run?",
29960
30212
  options: ENGINE_CHOICES,
@@ -29984,15 +30236,26 @@ const promptForConfigChoices = async () => {
29984
30236
  });
29985
30237
  if (isCancel(telemetryChoice)) return null;
29986
30238
  const workflowChoice = await select({
29987
- message: "Add a GitHub Actions workflow to run Raigal on every push and PR?",
29988
- options: [{
29989
- value: "yes",
29990
- label: "Yes — writes .github/workflows/raigal.yml"
29991
- }, {
29992
- value: "no",
29993
- label: "No, I'll wire CI myself"
29994
- }],
29995
- initialValue: "yes"
30239
+ message: "Add automated CI quality gate workflow?",
30240
+ options: [
30241
+ {
30242
+ value: "github",
30243
+ label: "GitHub Actions — writes .github/workflows/raigal.yml"
30244
+ },
30245
+ {
30246
+ value: "gitlab",
30247
+ label: "GitLab CI — writes .gitlab-ci.yml"
30248
+ },
30249
+ {
30250
+ value: "bitbucket",
30251
+ label: "Bitbucket Pipelines — writes bitbucket-pipelines.yml"
30252
+ },
30253
+ {
30254
+ value: "none",
30255
+ label: "No, I'll wire CI myself"
30256
+ }
30257
+ ],
30258
+ initialValue: detectDefaultCiProvider(directory)
29996
30259
  });
29997
30260
  if (isCancel(workflowChoice)) return null;
29998
30261
  return {
@@ -30000,7 +30263,8 @@ const promptForConfigChoices = async () => {
30000
30263
  failBelow: Number(failBelowRaw),
30001
30264
  typecheck: DEFAULT_CONFIG.lint.typecheck,
30002
30265
  telemetryEnabled: telemetryChoice === "enabled",
30003
- writeGithubWorkflow: workflowChoice === "yes"
30266
+ writeGithubWorkflow: workflowChoice === "github",
30267
+ ciProvider: workflowChoice
30004
30268
  };
30005
30269
  };
30006
30270
  const strictChoices = () => ({
@@ -30008,7 +30272,8 @@ const strictChoices = () => ({
30008
30272
  failBelow: 85,
30009
30273
  typecheck: true,
30010
30274
  telemetryEnabled: DEFAULT_CONFIG.telemetry.enabled,
30011
- writeGithubWorkflow: true
30275
+ writeGithubWorkflow: true,
30276
+ ciProvider: "github"
30012
30277
  });
30013
30278
  const writeAislopConfig = (configDir, configPath, choices) => {
30014
30279
  const selected = new Set(choices.engines);
@@ -30085,7 +30350,7 @@ const initCommand = async (directory, options = {}) => {
30085
30350
  return;
30086
30351
  }
30087
30352
  }
30088
- const choices = options.strict ? strictChoices() : await promptForConfigChoices();
30353
+ const choices = options.strict ? strictChoices() : await promptForConfigChoices(resolvedDir);
30089
30354
  if (!choices) return;
30090
30355
  writeAislopConfig(configDir, configPath, choices);
30091
30356
  const steps = [{
@@ -30102,19 +30367,49 @@ const initCommand = async (directory, options = {}) => {
30102
30367
  status: "skipped",
30103
30368
  label: `${CONFIG_DIR}/${RULES_FILE} already exists`
30104
30369
  });
30105
- const workflowResult = writeGithubWorkflow(resolvedDir, choices.writeGithubWorkflow);
30106
- if (workflowResult.status === "written") steps.push({
30107
- status: "done",
30108
- label: `Wrote ${workflowResult.relativePath}`
30109
- });
30110
- else if (workflowResult.status === "skipped-exists") steps.push({
30111
- status: "skipped",
30112
- label: `${workflowResult.relativePath} already exists`
30113
- });
30370
+ let ciHint;
30371
+ if (choices.ciProvider === "gitlab") {
30372
+ const gitlabResult = writeGitlabWorkflow(resolvedDir, true);
30373
+ if (gitlabResult.status === "written") {
30374
+ steps.push({
30375
+ status: "done",
30376
+ label: `Wrote ${gitlabResult.relativePath}`
30377
+ });
30378
+ ciHint = "Set RAIGAL_TOKEN in GitLab CI/CD Variables (Settings > CI/CD > Variables).";
30379
+ } else if (gitlabResult.status === "skipped-exists") steps.push({
30380
+ status: "skipped",
30381
+ label: `${gitlabResult.relativePath} already exists`
30382
+ });
30383
+ } else if (choices.ciProvider === "bitbucket") {
30384
+ const bbResult = writeBitbucketWorkflow(resolvedDir, true);
30385
+ if (bbResult.status === "written") {
30386
+ steps.push({
30387
+ status: "done",
30388
+ label: `Wrote ${bbResult.relativePath}`
30389
+ });
30390
+ ciHint = "Set RAIGAL_TOKEN in Bitbucket Repository Variables (Repository settings > Repository variables).";
30391
+ } else if (bbResult.status === "skipped-exists") steps.push({
30392
+ status: "skipped",
30393
+ label: `${bbResult.relativePath} already exists`
30394
+ });
30395
+ } else if (choices.writeGithubWorkflow || choices.ciProvider === "github") {
30396
+ const workflowResult = writeGithubWorkflow(resolvedDir, true);
30397
+ if (workflowResult.status === "written") {
30398
+ steps.push({
30399
+ status: "done",
30400
+ label: `Wrote ${workflowResult.relativePath}`
30401
+ });
30402
+ ciHint = "GitHub Actions zero-token OIDC enabled (permissions: id-token: write).";
30403
+ } else if (workflowResult.status === "skipped-exists") steps.push({
30404
+ status: "skipped",
30405
+ label: `${workflowResult.relativePath} already exists`
30406
+ });
30407
+ }
30114
30408
  process.stdout.write(buildInitSuccessRender({
30115
30409
  steps,
30116
30410
  nextCommand: `${invocation} scan`,
30117
- includeHeader: false
30411
+ includeHeader: false,
30412
+ hint: ciHint
30118
30413
  }));
30119
30414
  };
30120
30415
  //#endregion
@@ -30446,6 +30741,15 @@ program.command("doctor [directory]").description("Check toolchain coverage for
30446
30741
  return { exitCode: 0 };
30447
30742
  });
30448
30743
  });
30744
+ program.command("mcp").description("Run the Model Context Protocol (MCP) server over stdio").action(async () => {
30745
+ const mcpPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "mcp.js");
30746
+ spawn(process.execPath, [mcpPath], {
30747
+ stdio: "inherit",
30748
+ env: process.env
30749
+ }).on("exit", (code) => {
30750
+ process.exit(code ?? 0);
30751
+ });
30752
+ });
30449
30753
  const ciProgram = program.command("ci [directory]").description("Run the quality gate for CI");
30450
30754
  for (const [flag, description] of [
30451
30755
  ["--changes", "only gate files changed vs --base (or HEAD)"],