aislop 0.5.0 → 0.6.0

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
@@ -12,6 +12,7 @@ import { fileURLToPath } from "node:url";
12
12
  import os from "node:os";
13
13
  import ts from "typescript";
14
14
  import wcwidth from "wcwidth";
15
+ import crypto from "node:crypto";
15
16
  import { isCancel, multiselect, select, text } from "@clack/prompts";
16
17
 
17
18
  //#region \0rolldown/runtime.js
@@ -982,7 +983,7 @@ const detectSwallowedExceptions = async (context) => {
982
983
  };
983
984
 
984
985
  //#endregion
985
- //#region src/engines/ai-slop/narrative-comments.ts
986
+ //#region src/engines/ai-slop/narrative-comments-patterns.ts
986
987
  const DECORATIVE_SEPARATOR = /^[-=─━~_*#]{6,}$/;
987
988
  const DECORATIVE_SECTION_HEADER = /^[-=─━~_*#]{3,}[\s\S]+?[-=─━~_*#]{3,}$/;
988
989
  const SECTION_HEADER = /^(Phase|Step|Section|Part)\s+\d+[:.-]/i;
@@ -1025,7 +1026,49 @@ const MEANINGFUL_JSDOC_TAGS = new Set([
1025
1026
  "todo",
1026
1027
  "link",
1027
1028
  "license",
1028
- "preserve"
1029
+ "preserve",
1030
+ "swagger",
1031
+ "openapi",
1032
+ "route",
1033
+ "group",
1034
+ "summary",
1035
+ "description",
1036
+ "operationid",
1037
+ "response",
1038
+ "responses",
1039
+ "request",
1040
+ "requestbody",
1041
+ "security",
1042
+ "tag",
1043
+ "tags",
1044
+ "path",
1045
+ "body",
1046
+ "query",
1047
+ "queryparam",
1048
+ "header",
1049
+ "headers",
1050
+ "produces",
1051
+ "accept",
1052
+ "middleware",
1053
+ "api",
1054
+ "apiname",
1055
+ "apidefine",
1056
+ "apigroup",
1057
+ "apiparam",
1058
+ "apiquery",
1059
+ "apibody",
1060
+ "apiheader",
1061
+ "apisuccess",
1062
+ "apierror",
1063
+ "apiexample",
1064
+ "apiversion",
1065
+ "apidescription",
1066
+ "apipermission",
1067
+ "apiuse",
1068
+ "apiignore",
1069
+ "apiprivate",
1070
+ "namespace",
1071
+ "category"
1029
1072
  ]);
1030
1073
  const SUPPORTED_EXTS = new Set([
1031
1074
  ".ts",
@@ -1041,6 +1084,19 @@ const SUPPORTED_EXTS = new Set([
1041
1084
  ".java",
1042
1085
  ".php"
1043
1086
  ]);
1087
+ const DECL_START = /^(\s*)(export\s+)?(async\s+)?(const|let|var|function|class|type|interface|enum|abstract\s+class)\s+/;
1088
+ const EXPORT_DEFAULT = /^\s*export\s+default\b/;
1089
+ const TS_MEMBER_DECL_START = /^\s*(?:readonly\s+|static\s+|public\s+|private\s+|protected\s+|abstract\s+|override\s+)*[\w$]+\??\s*:/;
1090
+ const PY_DECL_START = /^\s*(async\s+def|def|class)\s+/;
1091
+ const GO_DECL_START = /^\s*(func|type|var|const)\s+/;
1092
+ const RUST_DECL_START = /^\s*(pub\s+)?(async\s+)?(fn|struct|enum|trait|impl|const|static|type|mod)\s+/;
1093
+ const RUBY_DECL_START = /^\s*(class|module|def)\s+/;
1094
+ const JAVA_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|sealed|non-sealed|\s)+(?:class|interface|enum|record|@interface|\w[^(){};=]*\s+\w+\s*\()/;
1095
+ const JAVA_DECL_START_FALLBACK = /^\s*(class|interface|enum|record|@interface)\s+/;
1096
+ const PHP_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|readonly\s+)*(function|class|interface|trait|enum|const)\s+/;
1097
+
1098
+ //#endregion
1099
+ //#region src/engines/ai-slop/narrative-comments.ts
1044
1100
  const stripJsdocLine = (line) => line.replace(/^\s*\/\*\*+\s?/, "").replace(/\s*\*+\/\s*$/, "").replace(/^\s*\*\s?/, "").trim();
1045
1101
  const stripLineComment = (line) => line.replace(/^\s*(?:(?:\/\/)|#)\s?/, "");
1046
1102
  const getCommentSyntax = (ext) => {
@@ -1132,16 +1188,6 @@ const collectBlocks = (sourceLines, syntax) => {
1132
1188
  }
1133
1189
  return blocks;
1134
1190
  };
1135
- const DECL_START = /^(\s*)(export\s+)?(async\s+)?(const|let|var|function|class|type|interface|enum|abstract\s+class)\s+/;
1136
- const EXPORT_DEFAULT = /^\s*export\s+default\b/;
1137
- const TS_MEMBER_DECL_START = /^\s*(?:readonly\s+|static\s+|public\s+|private\s+|protected\s+|abstract\s+|override\s+)*[\w$]+\??\s*:/;
1138
- const PY_DECL_START = /^\s*(async\s+def|def|class)\s+/;
1139
- const GO_DECL_START = /^\s*(func|type|var|const)\s+/;
1140
- const RUST_DECL_START = /^\s*(pub\s+)?(async\s+)?(fn|struct|enum|trait|impl|const|static|type|mod)\s+/;
1141
- const RUBY_DECL_START = /^\s*(class|module|def)\s+/;
1142
- const JAVA_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|sealed|non-sealed|\s)+(?:class|interface|enum|record|@interface|\w[^(){};=]*\s+\w+\s*\()/;
1143
- const JAVA_DECL_START_FALLBACK = /^\s*(class|interface|enum|record|@interface)\s+/;
1144
- const PHP_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|readonly\s+)*(function|class|interface|trait|enum|const)\s+/;
1145
1191
  const looksLikeDeclarationPreamble = (nextLine, ext) => {
1146
1192
  if (nextLine === null) return false;
1147
1193
  if (DECL_START.test(nextLine) || EXPORT_DEFAULT.test(nextLine)) return true;
@@ -1682,72 +1728,12 @@ const architectureEngine = {
1682
1728
  };
1683
1729
 
1684
1730
  //#endregion
1685
- //#region src/engines/code-quality/complexity.ts
1686
- const FUNCTION_PATTERNS = [
1687
- {
1688
- regex: /^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/,
1689
- langFilter: [
1690
- ".js",
1691
- ".ts",
1692
- ".jsx",
1693
- ".tsx",
1694
- ".mjs",
1695
- ".cjs"
1696
- ]
1697
- },
1698
- {
1699
- regex: /^\s*(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?:=>|:\s*\w)/,
1700
- langFilter: [
1701
- ".js",
1702
- ".ts",
1703
- ".jsx",
1704
- ".tsx",
1705
- ".mjs",
1706
- ".cjs"
1707
- ]
1708
- },
1709
- {
1710
- regex: /^\s*def\s+(\w+)\s*\(([^)]*)\)/,
1711
- langFilter: [".py"]
1712
- },
1713
- {
1714
- regex: /^\s*func\s+(?:\([^)]*\)\s+)?(\w+)\s*\(([^)]*)\)/,
1715
- langFilter: [".go"]
1716
- },
1717
- {
1718
- regex: /^\s*fn\s+(\w+)\s*\(([^)]*)\)/,
1719
- langFilter: [".rs"]
1720
- },
1721
- {
1722
- regex: /^\s*(?:public|private|protected)?\s*(?:static\s+)?(?:\w+\s+)(\w+)\s*\(([^)]*)\)/,
1723
- langFilter: [
1724
- ".java",
1725
- ".cs",
1726
- ".cpp",
1727
- ".c",
1728
- ".php"
1729
- ]
1730
- }
1731
- ];
1732
- const countParams = (p) => p.trim() ? p.split(",").length : 0;
1733
- const matchFunctionOnLine = (line, ext) => {
1734
- for (let i = 0; i < FUNCTION_PATTERNS.length; i++) {
1735
- const pattern = FUNCTION_PATTERNS[i];
1736
- if (!pattern.langFilter.includes(ext)) continue;
1737
- const match = line.match(pattern.regex);
1738
- if (match) return {
1739
- name: match[1],
1740
- params: match[2] ?? "",
1741
- patternIndex: i
1742
- };
1743
- }
1744
- return null;
1745
- };
1731
+ //#region src/engines/code-quality/function-boundaries.ts
1746
1732
  const PYTHON_CONTROL_FLOW_RE = /^\s*(?:if|for|while|with|try|except|else|elif|finally|def|class)\b/;
1747
- const findFunctionEnd = (lines, startIndex, isPython) => {
1748
- if (isPython) return findPythonFunctionEnd(lines, startIndex);
1749
- return findBraceFunctionEnd(lines, startIndex);
1750
- };
1733
+ const ARROW_BLOCK_RE = /* @__PURE__ */ new RegExp("=>\\s*\\{");
1734
+ const ARROW_END_RE = /* @__PURE__ */ new RegExp("=>\\s*$");
1735
+ const BRACE_START_RE = /* @__PURE__ */ new RegExp("^\\s*\\{");
1736
+ const NEW_STATEMENT_RE = /* @__PURE__ */ new RegExp("^(?:export\\s+)?(?:const|let|var|function|class)\\s");
1751
1737
  const isControlFlowBrace = (lineText, braceIndex) => {
1752
1738
  const before = lineText.substring(0, braceIndex).trimEnd();
1753
1739
  if (before.endsWith(")")) return true;
@@ -1827,16 +1813,10 @@ const findPythonFunctionEnd = (lines, startIndex) => {
1827
1813
  maxNesting
1828
1814
  };
1829
1815
  };
1830
- const isDataFile = (content) => {
1831
- const nonEmpty = content.split("\n").filter((l) => l.trim().length > 0);
1832
- if (nonEmpty.length === 0) return false;
1833
- const dataLinePattern = /^\s*[{}[\]"']/;
1834
- return nonEmpty.filter((l) => dataLinePattern.test(l)).length / nonEmpty.length > .8;
1816
+ const findFunctionEnd = (lines, startIndex, isPython) => {
1817
+ if (isPython) return findPythonFunctionEnd(lines, startIndex);
1818
+ return findBraceFunctionEnd(lines, startIndex);
1835
1819
  };
1836
- const ARROW_BLOCK_RE = /* @__PURE__ */ new RegExp("=>\\s*\\{");
1837
- const ARROW_END_RE = /* @__PURE__ */ new RegExp("=>\\s*$");
1838
- const BRACE_START_RE = /* @__PURE__ */ new RegExp("^\\s*\\{");
1839
- const NEW_STATEMENT_RE = /* @__PURE__ */ new RegExp("^(?:export\\s+)?(?:const|let|var|function|class)\\s");
1840
1820
  const isBlockArrow = (lines, startIndex) => {
1841
1821
  if (ARROW_BLOCK_RE.test(lines[startIndex])) return true;
1842
1822
  if (ARROW_END_RE.test(lines[startIndex])) {
@@ -1872,6 +1852,75 @@ const countTemplateLines = (bodyLines) => {
1872
1852
  }
1873
1853
  return templateLineCount;
1874
1854
  };
1855
+
1856
+ //#endregion
1857
+ //#region src/engines/code-quality/complexity.ts
1858
+ const FUNCTION_PATTERNS = [
1859
+ {
1860
+ regex: /^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/,
1861
+ langFilter: [
1862
+ ".js",
1863
+ ".ts",
1864
+ ".jsx",
1865
+ ".tsx",
1866
+ ".mjs",
1867
+ ".cjs"
1868
+ ]
1869
+ },
1870
+ {
1871
+ regex: /^\s*(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?:=>|:\s*\w)/,
1872
+ langFilter: [
1873
+ ".js",
1874
+ ".ts",
1875
+ ".jsx",
1876
+ ".tsx",
1877
+ ".mjs",
1878
+ ".cjs"
1879
+ ]
1880
+ },
1881
+ {
1882
+ regex: /^\s*def\s+(\w+)\s*\(([^)]*)\)/,
1883
+ langFilter: [".py"]
1884
+ },
1885
+ {
1886
+ regex: /^\s*func\s+(?:\([^)]*\)\s+)?(\w+)\s*\(([^)]*)\)/,
1887
+ langFilter: [".go"]
1888
+ },
1889
+ {
1890
+ regex: /^\s*fn\s+(\w+)\s*\(([^)]*)\)/,
1891
+ langFilter: [".rs"]
1892
+ },
1893
+ {
1894
+ regex: /^\s*(?:public|private|protected)?\s*(?:static\s+)?(?:\w+\s+)(\w+)\s*\(([^)]*)\)/,
1895
+ langFilter: [
1896
+ ".java",
1897
+ ".cs",
1898
+ ".cpp",
1899
+ ".c",
1900
+ ".php"
1901
+ ]
1902
+ }
1903
+ ];
1904
+ const countParams = (p) => p.trim() ? p.split(",").length : 0;
1905
+ const matchFunctionOnLine = (line, ext) => {
1906
+ for (let i = 0; i < FUNCTION_PATTERNS.length; i++) {
1907
+ const pattern = FUNCTION_PATTERNS[i];
1908
+ if (!pattern.langFilter.includes(ext)) continue;
1909
+ const match = line.match(pattern.regex);
1910
+ if (match) return {
1911
+ name: match[1],
1912
+ params: match[2] ?? "",
1913
+ patternIndex: i
1914
+ };
1915
+ }
1916
+ return null;
1917
+ };
1918
+ const isDataFile = (content) => {
1919
+ const nonEmpty = content.split("\n").filter((l) => l.trim().length > 0);
1920
+ if (nonEmpty.length === 0) return false;
1921
+ const dataLinePattern = /^\s*[{}[\]"']/;
1922
+ return nonEmpty.filter((l) => dataLinePattern.test(l)).length / nonEmpty.length > .8;
1923
+ };
1875
1924
  const analyzeFunctions = (content, ext) => {
1876
1925
  const lines = content.split("\n");
1877
1926
  const functions = [];
@@ -1894,13 +1943,14 @@ const analyzeFunctions = (content, ext) => {
1894
1943
  }
1895
1944
  return functions;
1896
1945
  };
1946
+ const JSX_FILE_LOC_MULTIPLIER = 1.5;
1897
1947
  const checkFileDiagnostics = (relativePath, content, limits) => {
1898
1948
  const results = [];
1899
1949
  const lineCount = content.split("\n").length;
1900
1950
  const ext = path.extname(relativePath).toLowerCase();
1901
1951
  if (isDataFile(content)) return results;
1902
- const effectiveMax = ext === ".jsx" || ext === ".tsx" ? limits.maxFileLoc * 2 : limits.maxFileLoc;
1903
- if (lineCount > Math.ceil(effectiveMax * 1.1)) results.push({
1952
+ const effectiveMax = ext === ".jsx" || ext === ".tsx" ? Math.ceil(limits.maxFileLoc * JSX_FILE_LOC_MULTIPLIER) : limits.maxFileLoc;
1953
+ if (lineCount > effectiveMax) results.push({
1904
1954
  filePath: relativePath,
1905
1955
  engine: "code-quality",
1906
1956
  rule: "complexity/file-too-large",
@@ -3581,6 +3631,216 @@ const runCargoAudit = async (rootDir, timeout) => {
3581
3631
  }
3582
3632
  };
3583
3633
 
3634
+ //#endregion
3635
+ //#region src/utils/source-masker.ts
3636
+ const JS_EXTS = new Set([
3637
+ ".ts",
3638
+ ".tsx",
3639
+ ".js",
3640
+ ".jsx",
3641
+ ".mjs",
3642
+ ".cjs"
3643
+ ]);
3644
+ const PY_EXTS = new Set([".py"]);
3645
+ const RB_EXTS = new Set([".rb"]);
3646
+ const PHP_EXTS = new Set([".php"]);
3647
+ const familyForExt = (ext) => {
3648
+ if (JS_EXTS.has(ext)) return "js";
3649
+ if (PY_EXTS.has(ext)) return "py";
3650
+ if (RB_EXTS.has(ext)) return "rb";
3651
+ if (PHP_EXTS.has(ext)) return "php";
3652
+ return "none";
3653
+ };
3654
+ const maskStringsAndComments = (content, ext) => {
3655
+ const family = familyForExt(ext);
3656
+ if (family === "none") return content;
3657
+ if (family === "js") return maskJs(content);
3658
+ return maskSimple(content, family);
3659
+ };
3660
+ const maskJs = (content) => {
3661
+ const out = content.split("");
3662
+ const len = content.length;
3663
+ const tplStack = [];
3664
+ let i = 0;
3665
+ const mask = (start, end) => {
3666
+ for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
3667
+ };
3668
+ while (i < len) {
3669
+ const c = content[i];
3670
+ const next = content[i + 1];
3671
+ if (tplStack.length > 0) {
3672
+ if (c === "{") {
3673
+ tplStack[tplStack.length - 1]++;
3674
+ i++;
3675
+ continue;
3676
+ }
3677
+ if (c === "}") {
3678
+ if (tplStack[tplStack.length - 1] === 0) {
3679
+ tplStack.pop();
3680
+ const scan = consumeTemplateString(content, i + 1);
3681
+ mask(i + 1, scan.maskEnd);
3682
+ if (scan.openedInterp) tplStack.push(0);
3683
+ i = scan.resumeAt;
3684
+ continue;
3685
+ }
3686
+ tplStack[tplStack.length - 1]--;
3687
+ i++;
3688
+ continue;
3689
+ }
3690
+ if (c === "\"" || c === "'") {
3691
+ const strStart = i;
3692
+ i = consumeQuotedString(content, i, c);
3693
+ mask(strStart + 1, i - 1);
3694
+ continue;
3695
+ }
3696
+ if (c === "`") {
3697
+ const scan = consumeTemplateString(content, i + 1);
3698
+ mask(i + 1, scan.maskEnd);
3699
+ if (scan.openedInterp) tplStack.push(0);
3700
+ i = scan.resumeAt;
3701
+ continue;
3702
+ }
3703
+ if (c === "/" && next === "/") {
3704
+ const strStart = i;
3705
+ while (i < len && content[i] !== "\n") i++;
3706
+ mask(strStart, i);
3707
+ continue;
3708
+ }
3709
+ if (c === "/" && next === "*") {
3710
+ const strStart = i;
3711
+ i += 2;
3712
+ while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
3713
+ if (i < len - 1) i += 2;
3714
+ mask(strStart, i);
3715
+ continue;
3716
+ }
3717
+ i++;
3718
+ continue;
3719
+ }
3720
+ if (c === "\"" || c === "'") {
3721
+ const strStart = i;
3722
+ i = consumeQuotedString(content, i, c);
3723
+ mask(strStart + 1, i - 1);
3724
+ continue;
3725
+ }
3726
+ if (c === "`") {
3727
+ const scan = consumeTemplateString(content, i + 1);
3728
+ mask(i + 1, scan.maskEnd);
3729
+ if (scan.openedInterp) tplStack.push(0);
3730
+ i = scan.resumeAt;
3731
+ continue;
3732
+ }
3733
+ if (c === "/" && next === "/") {
3734
+ const strStart = i;
3735
+ while (i < len && content[i] !== "\n") i++;
3736
+ mask(strStart, i);
3737
+ continue;
3738
+ }
3739
+ if (c === "/" && next === "*") {
3740
+ const strStart = i;
3741
+ i += 2;
3742
+ while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
3743
+ if (i < len - 1) i += 2;
3744
+ mask(strStart, i);
3745
+ continue;
3746
+ }
3747
+ i++;
3748
+ }
3749
+ return out.join("");
3750
+ };
3751
+ const consumeQuotedString = (content, start, quote) => {
3752
+ const len = content.length;
3753
+ let i = start + 1;
3754
+ while (i < len) {
3755
+ const c = content[i];
3756
+ if (c === "\\" && i + 1 < len) {
3757
+ i += 2;
3758
+ continue;
3759
+ }
3760
+ if (c === quote) return i + 1;
3761
+ if (c === "\n") return i;
3762
+ i++;
3763
+ }
3764
+ return i;
3765
+ };
3766
+ const consumeTemplateString = (content, start) => {
3767
+ const len = content.length;
3768
+ let i = start;
3769
+ while (i < len) {
3770
+ const c = content[i];
3771
+ if (c === "\\" && i + 1 < len) {
3772
+ i += 2;
3773
+ continue;
3774
+ }
3775
+ if (c === "`") return {
3776
+ maskEnd: i,
3777
+ resumeAt: i + 1,
3778
+ openedInterp: false
3779
+ };
3780
+ if (c === "$" && content[i + 1] === "{") return {
3781
+ maskEnd: i,
3782
+ resumeAt: i + 2,
3783
+ openedInterp: true
3784
+ };
3785
+ i++;
3786
+ }
3787
+ return {
3788
+ maskEnd: i,
3789
+ resumeAt: i,
3790
+ openedInterp: false
3791
+ };
3792
+ };
3793
+ const maskSimple = (content, family) => {
3794
+ const out = content.split("");
3795
+ const len = content.length;
3796
+ let i = 0;
3797
+ const mask = (start, end) => {
3798
+ for (let k = start; k < end; k++) if (out[k] !== "\n") out[k] = " ";
3799
+ };
3800
+ while (i < len) {
3801
+ const c = content[i];
3802
+ const next = content[i + 1];
3803
+ if (family === "py" && (c === "\"" || c === "'")) {
3804
+ if (content[i + 1] === c && content[i + 2] === c) {
3805
+ const triple = c + c + c;
3806
+ const end = content.indexOf(triple, i + 3);
3807
+ const stop = end === -1 ? len : end + 3;
3808
+ mask(i + 3, stop - 3);
3809
+ i = stop;
3810
+ continue;
3811
+ }
3812
+ }
3813
+ if (c === "\"" || c === "'") {
3814
+ const strStart = i;
3815
+ i = consumeQuotedString(content, i, c);
3816
+ mask(strStart + 1, i - 1);
3817
+ continue;
3818
+ }
3819
+ if ((family === "py" || family === "rb" || family === "php") && c === "#") {
3820
+ const strStart = i;
3821
+ while (i < len && content[i] !== "\n") i++;
3822
+ mask(strStart, i);
3823
+ continue;
3824
+ }
3825
+ if (family === "php" && c === "/" && next === "/") {
3826
+ const strStart = i;
3827
+ while (i < len && content[i] !== "\n") i++;
3828
+ mask(strStart, i);
3829
+ continue;
3830
+ }
3831
+ if (family === "php" && c === "/" && next === "*") {
3832
+ const strStart = i;
3833
+ i += 2;
3834
+ while (i < len - 1 && !(content[i] === "*" && content[i + 1] === "/")) i++;
3835
+ if (i < len - 1) i += 2;
3836
+ mask(strStart, i);
3837
+ continue;
3838
+ }
3839
+ i++;
3840
+ }
3841
+ return out.join("");
3842
+ };
3843
+
3584
3844
  //#endregion
3585
3845
  //#region src/engines/security/risky.ts
3586
3846
  const ev = "eval";
@@ -3710,12 +3970,13 @@ const detectRiskyConstructs = async (context) => {
3710
3970
  const relativePath = path.relative(context.rootDirectory, filePath);
3711
3971
  const normalizedPath = relativePath.split(path.sep).join("/");
3712
3972
  const isMigrationOrSeeder = /(?:^|\/)(migrations|seeders|seeds|migrate)\//.test(normalizedPath);
3973
+ const masked = maskStringsAndComments(content, ext);
3713
3974
  for (const { pattern, extensions, name, message, help } of RISKY_PATTERNS) {
3714
3975
  if (!extensions.includes(ext)) continue;
3715
3976
  if (isMigrationOrSeeder && name === "sql-injection") continue;
3716
3977
  const regex = new RegExp(pattern.source, pattern.flags);
3717
3978
  let match;
3718
- while ((match = regex.exec(content)) !== null) {
3979
+ while ((match = regex.exec(masked)) !== null) {
3719
3980
  const line = content.slice(0, match.index).split("\n").length;
3720
3981
  if (name === "innerhtml") {
3721
3982
  const beforeMatch = content.slice(Math.max(0, match.index - 200), match.index);
@@ -4438,7 +4699,7 @@ const discoverProject = async (directory) => {
4438
4699
  //#region src/utils/git.ts
4439
4700
  const MAX_BUFFER = 50 * 1024 * 1024;
4440
4701
  const getChangedFiles = (cwd, base) => {
4441
- const result = spawnSync("git", [
4702
+ const diff = spawnSync("git", [
4442
4703
  "diff",
4443
4704
  "--name-only",
4444
4705
  "--diff-filter=ACMR",
@@ -4448,8 +4709,22 @@ const getChangedFiles = (cwd, base) => {
4448
4709
  encoding: "utf-8",
4449
4710
  maxBuffer: MAX_BUFFER
4450
4711
  });
4451
- if (result.error || result.status !== 0) return [];
4452
- return result.stdout.split("\n").filter((f) => f.length > 0).map((f) => path.resolve(cwd, f));
4712
+ if (diff.error || diff.status !== 0) return [];
4713
+ const untracked = spawnSync("git", [
4714
+ "ls-files",
4715
+ "--others",
4716
+ "--exclude-standard"
4717
+ ], {
4718
+ cwd,
4719
+ encoding: "utf-8",
4720
+ maxBuffer: MAX_BUFFER
4721
+ });
4722
+ const names = /* @__PURE__ */ new Set();
4723
+ for (const line of diff.stdout.split("\n")) if (line.length > 0) names.add(line);
4724
+ if (!untracked.error && untracked.status === 0) {
4725
+ for (const line of untracked.stdout.split("\n")) if (line.length > 0) names.add(line);
4726
+ }
4727
+ return Array.from(names).map((f) => path.resolve(cwd, f));
4453
4728
  };
4454
4729
  const getStagedFiles = (cwd) => {
4455
4730
  const result = spawnSync("git", [
@@ -4472,7 +4747,7 @@ const getStagedFiles = (cwd) => {
4472
4747
  * Application version — injected at build time by tsdown from package.json.
4473
4748
  * The fallback should always match the "version" field in package.json.
4474
4749
  */
4475
- const APP_VERSION = "0.5.0";
4750
+ const APP_VERSION = "0.6.0";
4476
4751
 
4477
4752
  //#endregion
4478
4753
  //#region src/utils/telemetry.ts
@@ -6071,42 +6346,46 @@ const runExpoDoctor = async (context) => {
6071
6346
 
6072
6347
  //#endregion
6073
6348
  //#region src/commands/fix-force.ts
6074
- const getJsAuditFixCommand = (rootDirectory) => {
6075
- if (fs.existsSync(path.join(rootDirectory, "pnpm-lock.yaml"))) return {
6076
- command: "pnpm",
6077
- args: ["audit", "--fix"]
6078
- };
6079
- if (fs.existsSync(path.join(rootDirectory, "package-lock.json")) || fs.existsSync(path.join(rootDirectory, "package.json"))) return {
6080
- command: "npm",
6081
- args: ["audit", "fix"]
6082
- };
6349
+ const INSTALL_TIMEOUT = 1800 * 1e3;
6350
+ const AUDIT_TIMEOUT = 60 * 1e3;
6351
+ const detectPackageManager = (rootDirectory) => {
6352
+ if (fs.existsSync(path.join(rootDirectory, "pnpm-lock.yaml"))) return "pnpm";
6353
+ if (fs.existsSync(path.join(rootDirectory, "package-lock.json")) || fs.existsSync(path.join(rootDirectory, "package.json"))) return "npm";
6083
6354
  return null;
6084
6355
  };
6085
- const INSTALL_TIMEOUT = 1800 * 1e3;
6086
6356
  const fixDependencyAudit = async (context, onProgress) => {
6087
- const auditFix = getJsAuditFixCommand(context.rootDirectory);
6088
- if (!auditFix) return;
6089
- onProgress?.(`Dependency audit fixes · running ${auditFix.command} audit fix (can take a few minutes)`);
6090
- const result = await runSubprocess(auditFix.command, auditFix.args, {
6091
- cwd: context.rootDirectory,
6357
+ const pm = detectPackageManager(context.rootDirectory);
6358
+ if (!pm) return;
6359
+ if (pm === "npm") {
6360
+ await runNpmAuditFix(context.rootDirectory, onProgress);
6361
+ await tryNpmOverrides(context.rootDirectory, onProgress);
6362
+ return;
6363
+ }
6364
+ if (await tryPnpmOverrides(context.rootDirectory, onProgress)) return;
6365
+ if (fs.existsSync(path.join(context.rootDirectory, "package-lock.json"))) {
6366
+ await runNpmAuditFix(context.rootDirectory, onProgress);
6367
+ await tryNpmOverrides(context.rootDirectory, onProgress);
6368
+ return;
6369
+ }
6370
+ onProgress?.("Dependency audit fixes · skipping (pnpm audit unavailable and no package-lock.json for npm fallback)");
6371
+ };
6372
+ const runNpmAuditFix = async (rootDir, onProgress) => {
6373
+ onProgress?.("Dependency audit fixes · running npm audit fix (can take a few minutes)");
6374
+ const result = await runSubprocess("npm", ["audit", "fix"], {
6375
+ cwd: rootDir,
6092
6376
  timeout: INSTALL_TIMEOUT
6093
6377
  });
6094
- if (result.exitCode !== 0 && !result.stdout && !result.stderr) throw new Error(`${auditFix.command} audit fix failed`);
6095
- onProgress?.(`Dependency audit fixes · running ${auditFix.command} install`);
6096
- const installResult = await runSubprocess(auditFix.command, ["install"], {
6097
- cwd: context.rootDirectory,
6378
+ if (result.exitCode !== 0 && !result.stdout && !result.stderr) throw new Error("npm audit fix failed");
6379
+ onProgress?.("Dependency audit fixes · running npm install");
6380
+ const installResult = await runSubprocess("npm", ["install"], {
6381
+ cwd: rootDir,
6098
6382
  timeout: INSTALL_TIMEOUT
6099
6383
  });
6100
- if (installResult.exitCode !== 0) throw new Error(installResult.stderr || installResult.stdout || `${auditFix.command} install failed after audit fix`);
6101
- if (auditFix.command === "npm") await tryNpmOverrides(context.rootDirectory, onProgress);
6384
+ if (installResult.exitCode !== 0) throw new Error(installResult.stderr || installResult.stdout || "npm install failed after audit fix");
6102
6385
  };
6103
- /**
6104
- * For unresolvable transitive vulnerabilities, attempt to add npm overrides
6105
- * in package.json. This forces a newer version of the vulnerable transitive dep.
6106
- */
6107
- const fetchLatestVersion = async (rootDir, pkgName) => {
6386
+ const fetchLatestVersion = async (rootDir, pkgName, pm) => {
6108
6387
  try {
6109
- const result = await runSubprocess("npm", [
6388
+ const result = await runSubprocess(pm, [
6110
6389
  "view",
6111
6390
  pkgName,
6112
6391
  "version",
@@ -6120,11 +6399,11 @@ const fetchLatestVersion = async (rootDir, pkgName) => {
6120
6399
  return null;
6121
6400
  }
6122
6401
  };
6123
- const collectOverrides = async (rootDir, vulnerabilities) => {
6402
+ const collectOverrides = async (rootDir, vulnerabilities, pm) => {
6124
6403
  const overrides = {};
6125
6404
  for (const [pkgName, vuln] of Object.entries(vulnerabilities)) {
6126
6405
  if (vuln.fixAvailable !== false || !vuln.range) continue;
6127
- const latest = await fetchLatestVersion(rootDir, pkgName);
6406
+ const latest = await fetchLatestVersion(rootDir, pkgName, pm);
6128
6407
  if (latest) overrides[pkgName] = latest;
6129
6408
  }
6130
6409
  return overrides;
@@ -6133,12 +6412,12 @@ const tryNpmOverrides = async (rootDir, onProgress) => {
6133
6412
  try {
6134
6413
  const auditResult = await runSubprocess("npm", ["audit", "--json"], {
6135
6414
  cwd: rootDir,
6136
- timeout: 3e4
6415
+ timeout: AUDIT_TIMEOUT
6137
6416
  });
6138
6417
  if (!auditResult.stdout) return;
6139
6418
  const vulnerabilities = JSON.parse(auditResult.stdout).vulnerabilities;
6140
6419
  if (!vulnerabilities) return;
6141
- const overrides = await collectOverrides(rootDir, vulnerabilities);
6420
+ const overrides = await collectOverrides(rootDir, vulnerabilities, "npm");
6142
6421
  if (Object.keys(overrides).length === 0) return;
6143
6422
  const pkgPath = path.join(rootDir, "package.json");
6144
6423
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
@@ -6146,7 +6425,7 @@ const tryNpmOverrides = async (rootDir, onProgress) => {
6146
6425
  ...pkg.overrides || {},
6147
6426
  ...overrides
6148
6427
  };
6149
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
6428
+ fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
6150
6429
  onProgress?.("Dependency audit fixes · applying npm overrides (npm install)");
6151
6430
  await runSubprocess("npm", ["install"], {
6152
6431
  cwd: rootDir,
@@ -6154,6 +6433,70 @@ const tryNpmOverrides = async (rootDir, onProgress) => {
6154
6433
  });
6155
6434
  } catch {}
6156
6435
  };
6436
+ const patchedRangeToVersion = (patched) => {
6437
+ const match = patched.match(/^\s*>=?\s*([0-9]+\.[0-9]+\.[0-9]+[^\s]*)/);
6438
+ return match ? `^${match[1]}` : null;
6439
+ };
6440
+ const overrideKey = (name, vulnerable, patched) => {
6441
+ if (vulnerable && vulnerable.trim().length > 0 && !/^\*$/.test(vulnerable.trim())) return `${name}@${vulnerable.trim()}`;
6442
+ const first = patched.match(/([0-9]+\.[0-9]+\.[0-9]+)/)?.[1];
6443
+ return first ? `${name}@<${first}` : name;
6444
+ };
6445
+ const collectPnpmOverrides = (advisories) => {
6446
+ const overrides = {};
6447
+ for (const adv of Object.values(advisories)) {
6448
+ if (!adv.module_name || !adv.patched_versions) continue;
6449
+ const target = patchedRangeToVersion(adv.patched_versions);
6450
+ if (!target) continue;
6451
+ const key = overrideKey(adv.module_name, adv.vulnerable_versions, adv.patched_versions);
6452
+ overrides[key] = target;
6453
+ }
6454
+ return overrides;
6455
+ };
6456
+ const isPnpmAuditRetired = (stdout, stderr) => {
6457
+ const haystack = `${stdout}\n${stderr}`.toLowerCase();
6458
+ return haystack.includes("410") || haystack.includes("gone") || haystack.includes("retired") || haystack.includes("endpoint") || haystack.includes("err_pnpm_audit") || haystack.includes("audit endpoint");
6459
+ };
6460
+ const tryPnpmOverrides = async (rootDir, onProgress) => {
6461
+ onProgress?.("Dependency audit fixes · running pnpm audit");
6462
+ const auditResult = await runSubprocess("pnpm", ["audit", "--json"], {
6463
+ cwd: rootDir,
6464
+ timeout: AUDIT_TIMEOUT
6465
+ });
6466
+ if (!auditResult.stdout) {
6467
+ if (isPnpmAuditRetired(auditResult.stdout ?? "", auditResult.stderr ?? "")) return false;
6468
+ return auditResult.exitCode === 0;
6469
+ }
6470
+ let parsed;
6471
+ try {
6472
+ parsed = JSON.parse(auditResult.stdout);
6473
+ } catch {
6474
+ if (auditResult.exitCode !== 0 || isPnpmAuditRetired(auditResult.stdout, auditResult.stderr ?? "")) return false;
6475
+ return true;
6476
+ }
6477
+ const advisories = parsed.advisories;
6478
+ if (!advisories || Object.keys(advisories).length === 0) return true;
6479
+ const overrides = collectPnpmOverrides(advisories);
6480
+ if (Object.keys(overrides).length === 0) return true;
6481
+ const pkgPath = path.join(rootDir, "package.json");
6482
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
6483
+ const pnpmBlock = pkg.pnpm ?? {};
6484
+ const existing = pnpmBlock.overrides ?? {};
6485
+ pkg.pnpm = {
6486
+ ...pnpmBlock,
6487
+ overrides: {
6488
+ ...existing,
6489
+ ...overrides
6490
+ }
6491
+ };
6492
+ fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
6493
+ onProgress?.("Dependency audit fixes · applying pnpm overrides (pnpm install)");
6494
+ await runSubprocess("pnpm", ["install"], {
6495
+ cwd: rootDir,
6496
+ timeout: INSTALL_TIMEOUT
6497
+ });
6498
+ return true;
6499
+ };
6157
6500
  const fixExpoDependencies = async (context, onProgress) => {
6158
6501
  await removeDisallowedExpoPackages(context.rootDirectory, onProgress);
6159
6502
  onProgress?.("Expo dependency alignment · running expo install --fix (can take a few minutes)");
@@ -6268,8 +6611,9 @@ const runFormattingStep = async (deps) => {
6268
6611
  const runForceSteps = async (deps) => {
6269
6612
  if (!deps.force) return;
6270
6613
  if (deps.config.engines["code-quality"] && hasJsOrTs(deps.projectInfo)) await deps.runStep("Remove unused files", () => runKnipUnusedFiles(deps.resolvedDir), () => fixUnusedFiles(deps.resolvedDir));
6271
- if (deps.config.engines.security) await deps.runStep("Dependency audit fixes", () => runDependencyAudit(deps.context), () => fixDependencyAudit(deps.context, deps.rail.setActiveLabel));
6272
- if (deps.projectInfo.frameworks.includes("expo")) await deps.runStep("Expo dependency alignment", () => runExpoDoctor(deps.context), () => fixExpoDependencies(deps.context, deps.rail.setActiveLabel));
6614
+ const railUpdate = (label) => deps.rail.setActiveLabel(label);
6615
+ if (deps.config.engines.security) await deps.runStep("Dependency audit fixes", () => runDependencyAudit(deps.context), () => fixDependencyAudit(deps.context, railUpdate));
6616
+ if (deps.projectInfo.frameworks.includes("expo")) await deps.runStep("Expo dependency alignment", () => runExpoDoctor(deps.context), () => fixExpoDependencies(deps.context, railUpdate));
6273
6617
  };
6274
6618
 
6275
6619
  //#endregion
@@ -6433,6 +6777,1244 @@ const fixCommand = async (directory, config, options = {
6433
6777
  }
6434
6778
  };
6435
6779
 
6780
+ //#endregion
6781
+ //#region src/hooks/feedback.ts
6782
+ const MAX_FINDINGS = 20;
6783
+ const toFinding = (d, rootDirectory) => {
6784
+ if (d.severity !== "error" && d.severity !== "warning") return null;
6785
+ const file = path.isAbsolute(d.filePath) ? path.relative(rootDirectory, d.filePath) : d.filePath;
6786
+ return {
6787
+ ruleId: d.rule,
6788
+ severity: d.severity,
6789
+ category: d.category,
6790
+ file,
6791
+ line: d.line,
6792
+ col: d.column || void 0,
6793
+ message: d.message
6794
+ };
6795
+ };
6796
+ const buildNextSteps = (findings) => {
6797
+ const steps = [];
6798
+ const errorCount = findings.filter((f) => f.severity === "error").length;
6799
+ if (errorCount > 0) steps.push(`Fix ${errorCount} error${errorCount === 1 ? "" : "s"} before the next turn.`);
6800
+ const byFile = /* @__PURE__ */ new Map();
6801
+ for (const f of findings) {
6802
+ const list = byFile.get(f.file) ?? [];
6803
+ list.push(f);
6804
+ byFile.set(f.file, list);
6805
+ }
6806
+ for (const [file, list] of Array.from(byFile.entries()).slice(0, 3)) {
6807
+ const lines = list.map((f) => f.line).slice(0, 3).join(", ");
6808
+ steps.push(`Address ${list.length} finding${list.length === 1 ? "" : "s"} in ${file} (line${list.length === 1 ? "" : "s"} ${lines}).`);
6809
+ }
6810
+ return steps;
6811
+ };
6812
+ const buildFeedback = (diagnostics, score, rootDirectory, baseline) => {
6813
+ const all = diagnostics.map((d) => toFinding(d, rootDirectory)).filter((x) => x !== null);
6814
+ const capped = all.slice(0, MAX_FINDINGS);
6815
+ const elided = all.length > MAX_FINDINGS ? all.length - MAX_FINDINGS : void 0;
6816
+ const counts = {
6817
+ error: diagnostics.filter((d) => d.severity === "error").length,
6818
+ warning: diagnostics.filter((d) => d.severity === "warning").length,
6819
+ fixable: diagnostics.filter((d) => d.fixable).length,
6820
+ total: all.length
6821
+ };
6822
+ return {
6823
+ schema: "aislop.hook.v1",
6824
+ score,
6825
+ baseline,
6826
+ regressed: typeof baseline === "number" ? score < baseline : false,
6827
+ counts,
6828
+ findings: capped,
6829
+ elided,
6830
+ nextSteps: buildNextSteps(capped)
6831
+ };
6832
+ };
6833
+
6834
+ //#endregion
6835
+ //#region src/hooks/io/scan-lock.ts
6836
+ const LOCK_DIR = ".aislop";
6837
+ const LOCK_FILE = "hook.lock";
6838
+ const STALE_MS = 3e4;
6839
+ const lockPath = (cwd) => path.join(cwd, LOCK_DIR, LOCK_FILE);
6840
+ const readLock = (target) => {
6841
+ try {
6842
+ const raw = fs.readFileSync(target, "utf-8");
6843
+ const parsed = JSON.parse(raw);
6844
+ if (typeof parsed.pid !== "number" || typeof parsed.ts !== "number") return null;
6845
+ return parsed;
6846
+ } catch {
6847
+ return null;
6848
+ }
6849
+ };
6850
+ const acquireHookLock = (cwd) => {
6851
+ const target = lockPath(cwd);
6852
+ const existing = readLock(target);
6853
+ if (existing && Date.now() - existing.ts < STALE_MS) return null;
6854
+ try {
6855
+ fs.mkdirSync(path.dirname(target), { recursive: true });
6856
+ fs.writeFileSync(target, JSON.stringify({
6857
+ pid: process.pid,
6858
+ ts: Date.now()
6859
+ }));
6860
+ } catch {
6861
+ return null;
6862
+ }
6863
+ return () => {
6864
+ try {
6865
+ if (readLock(target)?.pid === process.pid) fs.unlinkSync(target);
6866
+ } catch {}
6867
+ };
6868
+ };
6869
+
6870
+ //#endregion
6871
+ //#region src/hooks/io/scoped-scan.ts
6872
+ const existingAbsolutePaths = (cwd, files) => files.map((f) => path.isAbsolute(f) ? f : path.join(cwd, f)).filter((p) => {
6873
+ try {
6874
+ return fs.statSync(p).isFile();
6875
+ } catch {
6876
+ return false;
6877
+ }
6878
+ });
6879
+ const resolveHookFiles = (cwd, files) => {
6880
+ const direct = existingAbsolutePaths(cwd, files);
6881
+ if (direct.length > 0) return direct;
6882
+ return existingAbsolutePaths(cwd, getChangedFiles(cwd));
6883
+ };
6884
+ const runScopedScan = async (cwd, filePaths) => {
6885
+ const project = await discoverProject(cwd);
6886
+ const config = loadConfig(cwd);
6887
+ const configDir = findConfigDir(project.rootDirectory);
6888
+ const rulesPath = configDir ? path.join(configDir, RULES_FILE) : void 0;
6889
+ const diagnostics = (await runEngines({
6890
+ rootDirectory: project.rootDirectory,
6891
+ languages: project.languages,
6892
+ frameworks: project.frameworks,
6893
+ files: filterProjectFiles(project.rootDirectory, filePaths),
6894
+ installedTools: project.installedTools,
6895
+ config: {
6896
+ quality: config.quality,
6897
+ security: {
6898
+ audit: false,
6899
+ auditTimeout: 0
6900
+ },
6901
+ architectureRulesPath: config.engines.architecture ? rulesPath : void 0
6902
+ }
6903
+ }, {
6904
+ format: config.engines.format,
6905
+ lint: config.engines.lint,
6906
+ "code-quality": config.engines["code-quality"],
6907
+ "ai-slop": config.engines["ai-slop"],
6908
+ architecture: config.engines.architecture,
6909
+ security: false
6910
+ })).flatMap((r) => r.diagnostics);
6911
+ const { score } = calculateScore(diagnostics, config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing);
6912
+ return {
6913
+ diagnostics,
6914
+ score,
6915
+ rootDirectory: project.rootDirectory
6916
+ };
6917
+ };
6918
+
6919
+ //#endregion
6920
+ //#region src/hooks/io/atomic-write.ts
6921
+ const atomicWrite = (targetPath, content) => {
6922
+ const dir = path.dirname(targetPath);
6923
+ fs.mkdirSync(dir, { recursive: true });
6924
+ const rand = Math.random().toString(36).slice(2, 10);
6925
+ const tmp = path.join(dir, `.aislop-tmp-${process.pid}-${rand}`);
6926
+ fs.writeFileSync(tmp, content, "utf-8");
6927
+ fs.renameSync(tmp, targetPath);
6928
+ };
6929
+ const readIfExists = (targetPath) => {
6930
+ try {
6931
+ return fs.readFileSync(targetPath, "utf-8");
6932
+ } catch {
6933
+ return null;
6934
+ }
6935
+ };
6936
+
6937
+ //#endregion
6938
+ //#region src/hooks/quality-gate/baseline.ts
6939
+ const BASELINE_REL = path.join(".aislop", "baseline.json");
6940
+ const baselinePath = (cwd) => path.join(cwd, BASELINE_REL);
6941
+ const readBaseline = (cwd) => {
6942
+ const raw = readIfExists(baselinePath(cwd));
6943
+ if (!raw) return null;
6944
+ try {
6945
+ const parsed = JSON.parse(raw);
6946
+ if (parsed.schema !== "aislop.baseline.v1") return null;
6947
+ return parsed;
6948
+ } catch {
6949
+ return null;
6950
+ }
6951
+ };
6952
+ const writeBaseline = (cwd, baseline) => {
6953
+ const target = baselinePath(cwd);
6954
+ atomicWrite(target, `${JSON.stringify(baseline, null, 2)}\n`);
6955
+ return target;
6956
+ };
6957
+ const captureBaseline = async (cwd) => {
6958
+ const project = await discoverProject(cwd);
6959
+ const config = loadConfig(cwd);
6960
+ const results = await runEngines({
6961
+ rootDirectory: project.rootDirectory,
6962
+ languages: project.languages,
6963
+ frameworks: project.frameworks,
6964
+ files: [],
6965
+ installedTools: project.installedTools,
6966
+ config: {
6967
+ quality: config.quality,
6968
+ security: {
6969
+ audit: false,
6970
+ auditTimeout: 0
6971
+ }
6972
+ }
6973
+ }, {
6974
+ format: config.engines.format,
6975
+ lint: config.engines.lint,
6976
+ "code-quality": config.engines["code-quality"],
6977
+ "ai-slop": config.engines["ai-slop"],
6978
+ architecture: config.engines.architecture,
6979
+ security: false
6980
+ });
6981
+ const diagnostics = results.flatMap((r) => r.diagnostics);
6982
+ const { score } = calculateScore(diagnostics, config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing);
6983
+ const byEngine = {};
6984
+ for (const r of results) {
6985
+ const { score: engineScore } = calculateScore(diagnostics.filter((d) => r.diagnostics.includes(d)), config.scoring.weights, config.scoring.thresholds, project.sourceFileCount, config.scoring.smoothing);
6986
+ byEngine[r.engine] = engineScore;
6987
+ }
6988
+ const target = writeBaseline(cwd, {
6989
+ schema: "aislop.baseline.v1",
6990
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
6991
+ score,
6992
+ byEngine,
6993
+ fileCount: project.sourceFileCount
6994
+ });
6995
+ return {
6996
+ score,
6997
+ fileCount: project.sourceFileCount,
6998
+ path: target
6999
+ };
7000
+ };
7001
+ const appendSessionFiles = (cwd, files) => {
7002
+ if (files.length === 0) return;
7003
+ const target = path.join(cwd, ".aislop", "session.jsonl");
7004
+ try {
7005
+ fs.mkdirSync(path.dirname(target), { recursive: true });
7006
+ const line = `${JSON.stringify({
7007
+ ts: Date.now(),
7008
+ files
7009
+ })}\n`;
7010
+ fs.appendFileSync(target, line);
7011
+ } catch {}
7012
+ };
7013
+ const readSessionFiles = (cwd) => {
7014
+ const raw = readIfExists(path.join(cwd, ".aislop", "session.jsonl"));
7015
+ if (!raw) return [];
7016
+ const files = /* @__PURE__ */ new Set();
7017
+ for (const line of raw.split("\n")) {
7018
+ if (!line.trim()) continue;
7019
+ try {
7020
+ const entry = JSON.parse(line);
7021
+ for (const f of entry.files ?? []) files.add(f);
7022
+ } catch {}
7023
+ }
7024
+ return Array.from(files);
7025
+ };
7026
+ const clearSessionFiles = (cwd) => {
7027
+ const target = path.join(cwd, ".aislop", "session.jsonl");
7028
+ try {
7029
+ fs.unlinkSync(target);
7030
+ } catch {}
7031
+ };
7032
+
7033
+ //#endregion
7034
+ //#region src/hooks/adapters/claude.ts
7035
+ const extractFiles$2 = (stdin) => {
7036
+ const files = /* @__PURE__ */ new Set();
7037
+ const input = stdin.tool_input ?? {};
7038
+ if (typeof input.file_path === "string" && input.file_path.length > 0) files.add(input.file_path);
7039
+ if (Array.isArray(input.edits)) {
7040
+ for (const e of input.edits) if (e && typeof e.file_path === "string" && e.file_path.length > 0) files.add(e.file_path);
7041
+ }
7042
+ return Array.from(files);
7043
+ };
7044
+ const parseClaudeStdin = (raw) => {
7045
+ if (!raw.trim()) return {};
7046
+ try {
7047
+ return JSON.parse(raw);
7048
+ } catch {
7049
+ return {};
7050
+ }
7051
+ };
7052
+ const readStdin$2 = async () => {
7053
+ if (process.stdin.isTTY) return "";
7054
+ const chunks = [];
7055
+ for await (const chunk of process.stdin) chunks.push(chunk);
7056
+ return Buffer.concat(chunks).toString("utf-8");
7057
+ };
7058
+ const renderClaudeOutput = (additional, block) => {
7059
+ const out = { hookSpecificOutput: {
7060
+ hookEventName: "PostToolUse",
7061
+ additionalContext: additional
7062
+ } };
7063
+ if (block) {
7064
+ out.decision = "block";
7065
+ out.reason = block.reason;
7066
+ }
7067
+ return out;
7068
+ };
7069
+ const runClaudeHook = async (deps = {}) => {
7070
+ const getStdin = deps.stdin ?? readStdin$2;
7071
+ const write = deps.write ?? ((s) => process.stdout.write(s));
7072
+ const input = parseClaudeStdin(await getStdin());
7073
+ const cwd = input.cwd && path.isAbsolute(input.cwd) ? input.cwd : process.cwd();
7074
+ const files = resolveHookFiles(cwd, extractFiles$2(input));
7075
+ if (files.length === 0) return 0;
7076
+ const release = acquireHookLock(cwd);
7077
+ if (!release) return 0;
7078
+ try {
7079
+ const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, files);
7080
+ const baseline = readBaseline(cwd);
7081
+ appendSessionFiles(cwd, files);
7082
+ const feedback = buildFeedback(diagnostics, score, rootDirectory, baseline?.score);
7083
+ const envelope = renderClaudeOutput(JSON.stringify(feedback));
7084
+ write(JSON.stringify(envelope));
7085
+ return 0;
7086
+ } catch {
7087
+ return 0;
7088
+ } finally {
7089
+ release();
7090
+ }
7091
+ };
7092
+ const parseClaudeStopStdin = (raw) => {
7093
+ if (!raw.trim()) return {};
7094
+ try {
7095
+ return JSON.parse(raw);
7096
+ } catch {
7097
+ return {};
7098
+ }
7099
+ };
7100
+ const runClaudeStopHook = async (deps = {}) => {
7101
+ const getStdin = deps.stdin ?? readStdin$2;
7102
+ const write = deps.write ?? ((s) => process.stdout.write(s));
7103
+ const input = parseClaudeStopStdin(await getStdin());
7104
+ const cwd = input.cwd && path.isAbsolute(input.cwd) ? input.cwd : process.cwd();
7105
+ if (input.stop_hook_active) return 0;
7106
+ const baseline = readBaseline(cwd);
7107
+ if (!baseline) return 0;
7108
+ const sessionFiles = readSessionFiles(cwd);
7109
+ if (sessionFiles.length === 0) return 0;
7110
+ const release = acquireHookLock(cwd);
7111
+ if (!release) return 0;
7112
+ try {
7113
+ const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, sessionFiles);
7114
+ const feedback = buildFeedback(diagnostics, score, rootDirectory, baseline.score);
7115
+ if (!feedback.regressed) {
7116
+ clearSessionFiles(cwd);
7117
+ return 0;
7118
+ }
7119
+ const envelope = renderClaudeOutput(JSON.stringify(feedback), { reason: `aislop: score dropped from ${baseline.score} to ${score}. Fix the ${feedback.counts.total} finding${feedback.counts.total === 1 ? "" : "s"} before finishing.` });
7120
+ write(JSON.stringify(envelope));
7121
+ return 0;
7122
+ } catch {
7123
+ return 0;
7124
+ } finally {
7125
+ release();
7126
+ }
7127
+ };
7128
+
7129
+ //#endregion
7130
+ //#region src/hooks/adapters/cursor.ts
7131
+ const extractFiles$1 = (stdin) => {
7132
+ const files = /* @__PURE__ */ new Set();
7133
+ if (typeof stdin.file_path === "string" && stdin.file_path.length > 0) files.add(stdin.file_path);
7134
+ if (Array.isArray(stdin.edits)) {
7135
+ for (const e of stdin.edits) if (e && typeof e.file_path === "string" && e.file_path.length > 0) files.add(e.file_path);
7136
+ }
7137
+ const input = stdin.tool_input ?? {};
7138
+ if (typeof input.file_path === "string" && input.file_path.length > 0) files.add(input.file_path);
7139
+ if (Array.isArray(input.edits)) {
7140
+ for (const e of input.edits) if (e && typeof e.file_path === "string" && e.file_path.length > 0) files.add(e.file_path);
7141
+ }
7142
+ return Array.from(files);
7143
+ };
7144
+ const parseCursorStdin = (raw) => {
7145
+ if (!raw.trim()) return {};
7146
+ try {
7147
+ return JSON.parse(raw);
7148
+ } catch {
7149
+ return {};
7150
+ }
7151
+ };
7152
+ const renderCursorOutput = (additional, event = "afterFileEdit") => ({ hookSpecificOutput: {
7153
+ hookEventName: event,
7154
+ additionalContext: additional
7155
+ } });
7156
+ const readStdin$1 = async () => {
7157
+ if (process.stdin.isTTY) return "";
7158
+ const chunks = [];
7159
+ for await (const chunk of process.stdin) chunks.push(chunk);
7160
+ return Buffer.concat(chunks).toString("utf-8");
7161
+ };
7162
+ const runCursorHook = async (deps = {}) => {
7163
+ const getStdin = deps.stdin ?? readStdin$1;
7164
+ const write = deps.write ?? ((s) => process.stdout.write(s));
7165
+ const writeErr = deps.writeErr ?? ((s) => process.stderr.write(s));
7166
+ const input = parseCursorStdin(await getStdin());
7167
+ const cwd = input.cwd && path.isAbsolute(input.cwd) ? input.cwd : process.cwd();
7168
+ const files = resolveHookFiles(cwd, extractFiles$1(input));
7169
+ if (files.length === 0) return 0;
7170
+ const release = acquireHookLock(cwd);
7171
+ if (!release) return 0;
7172
+ try {
7173
+ const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, files);
7174
+ const feedback = buildFeedback(diagnostics, score, rootDirectory);
7175
+ const serialized = JSON.stringify(feedback);
7176
+ write(JSON.stringify(renderCursorOutput(serialized)));
7177
+ writeErr(`${serialized}\n`);
7178
+ return 0;
7179
+ } catch {
7180
+ return 0;
7181
+ } finally {
7182
+ release();
7183
+ }
7184
+ };
7185
+
7186
+ //#endregion
7187
+ //#region src/hooks/adapters/gemini.ts
7188
+ const extractFiles = (stdin) => {
7189
+ const files = /* @__PURE__ */ new Set();
7190
+ const input = stdin.tool_input ?? {};
7191
+ if (typeof input.file_path === "string" && input.file_path.length > 0) files.add(input.file_path);
7192
+ if (typeof input.path === "string" && input.path.length > 0) files.add(input.path);
7193
+ return Array.from(files);
7194
+ };
7195
+ const parseGeminiStdin = (raw) => {
7196
+ if (!raw.trim()) return {};
7197
+ try {
7198
+ return JSON.parse(raw);
7199
+ } catch {
7200
+ return {};
7201
+ }
7202
+ };
7203
+ const renderGeminiOutput = (additional) => ({ hookSpecificOutput: {
7204
+ hookEventName: "AfterTool",
7205
+ additionalContext: additional
7206
+ } });
7207
+ const readStdin = async () => {
7208
+ if (process.stdin.isTTY) return "";
7209
+ const chunks = [];
7210
+ for await (const chunk of process.stdin) chunks.push(chunk);
7211
+ return Buffer.concat(chunks).toString("utf-8");
7212
+ };
7213
+ const runGeminiHook = async (deps = {}) => {
7214
+ const getStdin = deps.stdin ?? readStdin;
7215
+ const write = deps.write ?? ((s) => process.stdout.write(s));
7216
+ const input = parseGeminiStdin(await getStdin());
7217
+ const cwd = input.cwd && path.isAbsolute(input.cwd) ? input.cwd : process.cwd();
7218
+ const files = resolveHookFiles(cwd, extractFiles(input));
7219
+ if (files.length === 0) return 0;
7220
+ const release = acquireHookLock(cwd);
7221
+ if (!release) return 0;
7222
+ try {
7223
+ const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, files);
7224
+ const feedback = buildFeedback(diagnostics, score, rootDirectory);
7225
+ write(JSON.stringify(renderGeminiOutput(JSON.stringify(feedback))));
7226
+ return 0;
7227
+ } catch {
7228
+ return 0;
7229
+ } finally {
7230
+ release();
7231
+ }
7232
+ };
7233
+
7234
+ //#endregion
7235
+ //#region src/hooks/assets.ts
7236
+ const AISLOP_MD_BODY = `# aislop — agent instructions
7237
+
7238
+ [aislop](https://github.com/scanaislop/aislop) 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, 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.
7239
+
7240
+ ## On every edit
7241
+
7242
+ A PostToolUse hook runs \`aislop hook claude\` after every Edit, Write, or MultiEdit. It scans the touched files and returns findings as JSON \`additionalContext\` shaped like \`AislopFeedback\` (schema \`aislop.hook.v1\`). Act on them the same turn.
7243
+
7244
+ ## Severity ladder
7245
+
7246
+ - \`error\` — MUST fix this turn.
7247
+ - \`warning\` + \`fixable: true\` — MUST fix this turn.
7248
+ - \`warning\` + \`fixable: false\` — fix if trivially mechanical, otherwise surface in your reply.
7249
+
7250
+ ## Rules
7251
+
7252
+ - \`.aislop/config.yaml\` — thresholds and engine toggles. Treat as authoritative; don't edit without user consent.
7253
+ - \`.aislop/rules.yaml\` — project-specific architecture rules (may be absent). When a finding cites \`architecture/*\`, open this file and follow it.
7254
+ - Custom rules can change between sessions. Trust what the scan returns, not a cached understanding of what the rules are.
7255
+
7256
+ ## Principles
7257
+
7258
+ - Do not disable rules to pass the scan. Fix the underlying issue.
7259
+ - If a finding is a false positive, leave it and explain in your reply — do not delete the rule config.
7260
+ - The findings payload includes \`nextSteps[]\` — treat those as your plan for the turn.
7261
+ `;
7262
+
7263
+ //#endregion
7264
+ //#region src/hooks/io/sentinel.ts
7265
+ const sentinelHash = (content) => `sha256:${crypto.createHash("sha256").update(content).digest("hex").slice(0, 32)}`;
7266
+ const BEGIN_RE = /<!--\s*aislop:begin\s+v(\d+)(?:\s+hash=([^\s>]+))?\s*-->/;
7267
+ const END_RE = /<!--\s*aislop:end\s+v\d+\s*-->/;
7268
+ const renderFence = (body, hash) => [
7269
+ `<!-- aislop:begin v1 hash=${hash} -->`,
7270
+ body.trimEnd(),
7271
+ "<!-- aislop:end v1 -->"
7272
+ ].join("\n");
7273
+ const upsertMarkdownFence = (existing, body, hash) => {
7274
+ const fenced = renderFence(body, hash);
7275
+ if (existing == null || existing.length === 0) return {
7276
+ nextContent: `${fenced}\n`,
7277
+ replaced: false
7278
+ };
7279
+ const begin = existing.match(BEGIN_RE);
7280
+ const end = existing.match(END_RE);
7281
+ if (begin && end && (end.index ?? 0) > (begin.index ?? 0)) return {
7282
+ nextContent: `${existing.slice(0, begin.index)}${fenced}${existing.slice((end.index ?? 0) + end[0].length)}`,
7283
+ replaced: true
7284
+ };
7285
+ return {
7286
+ nextContent: `${existing}${existing.endsWith("\n") ? "" : "\n"}\n${fenced}\n`,
7287
+ replaced: false
7288
+ };
7289
+ };
7290
+
7291
+ //#endregion
7292
+ //#region src/hooks/install/types.ts
7293
+ const emptyResult = () => ({
7294
+ wrote: [],
7295
+ skipped: [],
7296
+ planned: []
7297
+ });
7298
+ const applyContent = (result, opts, target, nextContent, summary) => {
7299
+ if (readIfExists(target) === nextContent) {
7300
+ result.skipped.push(target);
7301
+ return;
7302
+ }
7303
+ if (opts.dryRun) {
7304
+ result.planned.push({
7305
+ path: target,
7306
+ summary
7307
+ });
7308
+ return;
7309
+ }
7310
+ atomicWrite(target, nextContent);
7311
+ result.wrote.push(target);
7312
+ };
7313
+ const applyRemoval = (result, opts, target, nextContent) => {
7314
+ const existing = readIfExists(target);
7315
+ if (existing == null) {
7316
+ result.skipped.push(target);
7317
+ return;
7318
+ }
7319
+ if (existing === (nextContent ?? "")) {
7320
+ result.skipped.push(target);
7321
+ return;
7322
+ }
7323
+ if (opts.dryRun) {
7324
+ result.removed.push(target);
7325
+ return;
7326
+ }
7327
+ if (nextContent == null) try {
7328
+ fs.unlinkSync(target);
7329
+ } catch {}
7330
+ else atomicWrite(target, nextContent);
7331
+ result.removed.push(target);
7332
+ };
7333
+
7334
+ //#endregion
7335
+ //#region src/hooks/install/rules-only.ts
7336
+ const installRulesOnly = (opts, paths, summary) => {
7337
+ const result = emptyResult();
7338
+ const next = upsertMarkdownFence(readIfExists(paths.rules), AISLOP_MD_BODY, sentinelHash(AISLOP_MD_BODY)).nextContent;
7339
+ applyContent(result, opts, paths.rules, next, summary);
7340
+ if (paths.host && paths.marker) {
7341
+ const host = readIfExists(paths.host) ?? "";
7342
+ if (!host.includes(paths.marker)) {
7343
+ const joiner = host.endsWith("\n") || host.length === 0 ? "" : "\n";
7344
+ const prefix = host.length === 0 ? "" : `${host}${joiner}\n`;
7345
+ applyContent(result, opts, paths.host, `${prefix}${paths.marker}\n`, `append ${paths.marker} reference`);
7346
+ } else result.skipped.push(paths.host);
7347
+ }
7348
+ return result;
7349
+ };
7350
+ const uninstallRulesOnly = (opts, paths) => {
7351
+ const result = {
7352
+ removed: [],
7353
+ skipped: []
7354
+ };
7355
+ if (readIfExists(paths.rules) != null) applyRemoval(result, opts, paths.rules, null);
7356
+ else result.skipped.push(paths.rules);
7357
+ if (paths.host && paths.marker) {
7358
+ const host = readIfExists(paths.host);
7359
+ if (host != null && host.includes(paths.marker)) {
7360
+ const stripped = host.split("\n").filter((l) => l.trim() !== paths.marker).join("\n").replace(/\n{3,}/g, "\n\n").trim();
7361
+ applyRemoval(result, opts, paths.host, stripped.length === 0 ? null : `${stripped}\n`);
7362
+ } else result.skipped.push(paths.host);
7363
+ }
7364
+ return result;
7365
+ };
7366
+
7367
+ //#endregion
7368
+ //#region src/hooks/install/antigravity.ts
7369
+ const resolveAntigravityPaths = (opts) => ({ rules: path.join(opts.cwd, ".agents", "rules", "antigravity-aislop-rules.md") });
7370
+ const installAntigravity = (opts) => {
7371
+ if (opts.scope !== "project") return {
7372
+ wrote: [],
7373
+ skipped: [],
7374
+ planned: [{
7375
+ path: ".agents/rules/antigravity-aislop-rules.md",
7376
+ summary: "Antigravity is project-scope only; pass --project"
7377
+ }]
7378
+ };
7379
+ return installRulesOnly(opts, resolveAntigravityPaths(opts), "write .agents/rules/antigravity-aislop-rules.md");
7380
+ };
7381
+ const uninstallAntigravity = (opts) => {
7382
+ if (opts.scope !== "project") return {
7383
+ removed: [],
7384
+ skipped: []
7385
+ };
7386
+ return uninstallRulesOnly(opts, resolveAntigravityPaths(opts));
7387
+ };
7388
+
7389
+ //#endregion
7390
+ //#region src/hooks/io/json-patch.ts
7391
+ const AISLOP_SENTINEL_KEY = "__aislop";
7392
+ const isAislopManaged = (x) => typeof x === "object" && x !== null && AISLOP_SENTINEL_KEY in x && x[AISLOP_SENTINEL_KEY] != null;
7393
+ const groupIsAislop = (group) => {
7394
+ if (typeof group !== "object" || group === null) return false;
7395
+ const hooks = group.hooks;
7396
+ if (!Array.isArray(hooks)) return false;
7397
+ return hooks.some((h) => isAislopManaged(h));
7398
+ };
7399
+ const upsertHookGroup = (config, event, group) => {
7400
+ const next = { ...config };
7401
+ const hooks = next.hooks && typeof next.hooks === "object" ? next.hooks : {};
7402
+ const cleaned = (Array.isArray(hooks[event]) ? hooks[event] : []).filter((g) => !groupIsAislop(g));
7403
+ next.hooks = {
7404
+ ...hooks,
7405
+ [event]: [...cleaned, group]
7406
+ };
7407
+ return next;
7408
+ };
7409
+ const upsertFlatHook = (config, event, entry) => {
7410
+ const next = { ...config };
7411
+ const hooks = next.hooks && typeof next.hooks === "object" ? next.hooks : {};
7412
+ const cleaned = (Array.isArray(hooks[event]) ? hooks[event] : []).filter((e) => !isAislopManaged(e));
7413
+ next.hooks = {
7414
+ ...hooks,
7415
+ [event]: [...cleaned, entry]
7416
+ };
7417
+ return next;
7418
+ };
7419
+ const removeAislopEntries = (config, event) => {
7420
+ const next = { ...config };
7421
+ const hooks = next.hooks && typeof next.hooks === "object" ? next.hooks : {};
7422
+ const existing = Array.isArray(hooks[event]) ? hooks[event] : [];
7423
+ const cleaned = existing.filter((e) => !isAislopManaged(e) && !groupIsAislop(e));
7424
+ const removed = existing.length - cleaned.length;
7425
+ const nextHooks = { ...hooks };
7426
+ if (cleaned.length === 0) delete nextHooks[event];
7427
+ else nextHooks[event] = cleaned;
7428
+ if (Object.keys(nextHooks).length === 0) delete next.hooks;
7429
+ else next.hooks = nextHooks;
7430
+ return {
7431
+ next,
7432
+ removed
7433
+ };
7434
+ };
7435
+
7436
+ //#endregion
7437
+ //#region src/hooks/install/claude.ts
7438
+ const resolveClaudePaths = (opts) => {
7439
+ const root = opts.scope === "project" ? path.join(opts.cwd, ".claude") : path.join(opts.home, ".claude");
7440
+ return {
7441
+ settings: path.join(root, "settings.json"),
7442
+ aislopMd: path.join(root, "AISLOP.md"),
7443
+ claudeMd: path.join(root, "CLAUDE.md")
7444
+ };
7445
+ };
7446
+ const buildHookGroup$1 = () => {
7447
+ const hashBody = JSON.stringify({
7448
+ command: "aislop hook claude",
7449
+ matcher: "Edit|Write|MultiEdit"
7450
+ });
7451
+ return {
7452
+ matcher: "Edit|Write|MultiEdit",
7453
+ hooks: [{
7454
+ type: "command",
7455
+ command: "aislop hook claude",
7456
+ [AISLOP_SENTINEL_KEY]: {
7457
+ v: 1,
7458
+ managed: true,
7459
+ hash: sentinelHash(hashBody)
7460
+ }
7461
+ }]
7462
+ };
7463
+ };
7464
+ const buildStopHookGroup = () => {
7465
+ const hashBody = JSON.stringify({ command: "aislop hook claude --stop" });
7466
+ return {
7467
+ matcher: "",
7468
+ hooks: [{
7469
+ type: "command",
7470
+ command: "aislop hook claude --stop",
7471
+ [AISLOP_SENTINEL_KEY]: {
7472
+ v: 1,
7473
+ managed: true,
7474
+ hash: sentinelHash(hashBody)
7475
+ }
7476
+ }]
7477
+ };
7478
+ };
7479
+ const renderSettings$1 = (existingRaw, qualityGate) => {
7480
+ let obj = {};
7481
+ if (existingRaw) try {
7482
+ obj = JSON.parse(existingRaw);
7483
+ } catch {
7484
+ obj = {};
7485
+ }
7486
+ let next = upsertHookGroup(obj, "PostToolUse", buildHookGroup$1());
7487
+ if (qualityGate) next = upsertHookGroup(next, "Stop", buildStopHookGroup());
7488
+ else next = removeAislopEntries(next, "Stop").next;
7489
+ return `${JSON.stringify(next, null, 2)}\n`;
7490
+ };
7491
+ const installClaude = (opts) => {
7492
+ const paths = resolveClaudePaths(opts);
7493
+ const result = emptyResult();
7494
+ const nextSettings = renderSettings$1(readIfExists(paths.settings), Boolean(opts.qualityGate));
7495
+ applyContent(result, opts, paths.settings, nextSettings, "register PostToolUse hook");
7496
+ const mdHash = sentinelHash(AISLOP_MD_BODY);
7497
+ const fenced = upsertMarkdownFence(readIfExists(paths.aislopMd), AISLOP_MD_BODY, mdHash);
7498
+ applyContent(result, opts, paths.aislopMd, fenced.nextContent, "write AISLOP.md rules");
7499
+ const existingClaudeMd = readIfExists(paths.claudeMd) ?? "";
7500
+ const marker = "@AISLOP.md";
7501
+ if (!existingClaudeMd.includes(marker)) {
7502
+ const joiner = existingClaudeMd.endsWith("\n") || existingClaudeMd.length === 0 ? "" : "\n";
7503
+ const prefix = existingClaudeMd.length === 0 ? "" : `${existingClaudeMd}${joiner}\n`;
7504
+ applyContent(result, opts, paths.claudeMd, `${prefix}${marker}\n`, "append @AISLOP.md reference");
7505
+ } else result.skipped.push(paths.claudeMd);
7506
+ return result;
7507
+ };
7508
+ const uninstallClaude = (opts) => {
7509
+ const paths = resolveClaudePaths({
7510
+ ...opts,
7511
+ qualityGate: false
7512
+ });
7513
+ const result = {
7514
+ removed: [],
7515
+ skipped: []
7516
+ };
7517
+ const settingsRaw = readIfExists(paths.settings);
7518
+ if (settingsRaw) {
7519
+ let obj = {};
7520
+ try {
7521
+ obj = JSON.parse(settingsRaw);
7522
+ } catch {
7523
+ obj = {};
7524
+ }
7525
+ const stripped = removeAislopEntries(removeAislopEntries(obj, "PostToolUse").next, "Stop").next;
7526
+ const stillHasHooks = stripped.hooks && typeof stripped.hooks === "object" && Object.keys(stripped.hooks).length > 0;
7527
+ const otherKeys = Object.keys(stripped).filter((k) => k !== "hooks");
7528
+ if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.settings, null);
7529
+ else applyRemoval(result, opts, paths.settings, `${JSON.stringify(stripped, null, 2)}\n`);
7530
+ } else result.skipped.push(paths.settings);
7531
+ if (readIfExists(paths.aislopMd) != null) applyRemoval(result, opts, paths.aislopMd, null);
7532
+ else result.skipped.push(paths.aislopMd);
7533
+ const claudeMd = readIfExists(paths.claudeMd);
7534
+ if (claudeMd != null && claudeMd.includes("@AISLOP.md")) {
7535
+ const stripped = claudeMd.split("\n").filter((line) => line.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
7536
+ applyRemoval(result, opts, paths.claudeMd, stripped.length === 0 ? null : `${stripped}\n`);
7537
+ } else result.skipped.push(paths.claudeMd);
7538
+ return result;
7539
+ };
7540
+
7541
+ //#endregion
7542
+ //#region src/hooks/install/cline.ts
7543
+ const resolveClinePaths = (opts) => ({ rules: path.join(opts.cwd, ".clinerules") });
7544
+ const resolveRooPaths = (opts) => ({ rules: path.join(opts.cwd, ".roo", "rules", "aislop.md") });
7545
+ const installCline = (opts) => {
7546
+ if (opts.scope !== "project") return {
7547
+ wrote: [],
7548
+ skipped: [],
7549
+ planned: [{
7550
+ path: ".clinerules",
7551
+ summary: "Cline is project-scope only; pass --project"
7552
+ }]
7553
+ };
7554
+ const cline = installRulesOnly(opts, resolveClinePaths(opts), "write .clinerules");
7555
+ const roo = installRulesOnly(opts, resolveRooPaths(opts), "write .roo/rules/aislop.md");
7556
+ return {
7557
+ wrote: [...cline.wrote, ...roo.wrote],
7558
+ skipped: [...cline.skipped, ...roo.skipped],
7559
+ planned: [...cline.planned, ...roo.planned]
7560
+ };
7561
+ };
7562
+ const uninstallCline = (opts) => {
7563
+ if (opts.scope !== "project") return {
7564
+ removed: [],
7565
+ skipped: []
7566
+ };
7567
+ const a = uninstallRulesOnly(opts, resolveClinePaths(opts));
7568
+ const b = uninstallRulesOnly(opts, resolveRooPaths(opts));
7569
+ return {
7570
+ removed: [...a.removed, ...b.removed],
7571
+ skipped: [...a.skipped, ...b.skipped]
7572
+ };
7573
+ };
7574
+
7575
+ //#endregion
7576
+ //#region src/hooks/install/codex.ts
7577
+ const resolveCodexPaths = (opts) => ({ rules: opts.scope === "project" ? path.join(opts.cwd, "AGENTS.md") : path.join(opts.home, ".codex", "AGENTS.md") });
7578
+ const installCodex = (opts) => installRulesOnly(opts, resolveCodexPaths(opts), "write AGENTS.md rules for Codex");
7579
+ const uninstallCodex = (opts) => uninstallRulesOnly(opts, resolveCodexPaths(opts));
7580
+
7581
+ //#endregion
7582
+ //#region src/hooks/install/copilot.ts
7583
+ const resolveCopilotPaths = (opts) => ({ rules: path.join(opts.cwd, ".github", "copilot-instructions.md") });
7584
+ const installCopilot = (opts) => {
7585
+ if (opts.scope !== "project") return {
7586
+ wrote: [],
7587
+ skipped: [],
7588
+ planned: [{
7589
+ path: ".github/copilot-instructions.md",
7590
+ summary: "Copilot is project-scope only; pass --project"
7591
+ }]
7592
+ };
7593
+ return installRulesOnly(opts, resolveCopilotPaths(opts), "write .github/copilot-instructions.md");
7594
+ };
7595
+ const uninstallCopilot = (opts) => {
7596
+ if (opts.scope !== "project") return {
7597
+ removed: [],
7598
+ skipped: []
7599
+ };
7600
+ return uninstallRulesOnly(opts, resolveCopilotPaths(opts));
7601
+ };
7602
+
7603
+ //#endregion
7604
+ //#region src/hooks/install/cursor.ts
7605
+ const resolveCursorPaths = (opts) => {
7606
+ const root = opts.scope === "project" ? path.join(opts.cwd, ".cursor") : path.join(opts.home, ".cursor");
7607
+ return {
7608
+ hooks: path.join(root, "hooks.json"),
7609
+ rules: path.join(opts.cwd, ".cursor", "rules", "aislop.mdc")
7610
+ };
7611
+ };
7612
+ const buildHookEntry = () => {
7613
+ const hashBody = JSON.stringify({
7614
+ command: "aislop hook cursor",
7615
+ timeout: 5e3
7616
+ });
7617
+ return {
7618
+ command: "aislop hook cursor",
7619
+ type: "command",
7620
+ timeout: 5e3,
7621
+ [AISLOP_SENTINEL_KEY]: {
7622
+ v: 1,
7623
+ managed: true,
7624
+ hash: sentinelHash(hashBody)
7625
+ }
7626
+ };
7627
+ };
7628
+ const renderHooksJson = (existingRaw) => {
7629
+ let obj = { version: 1 };
7630
+ if (existingRaw) try {
7631
+ obj = JSON.parse(existingRaw);
7632
+ } catch {
7633
+ obj = { version: 1 };
7634
+ }
7635
+ if (typeof obj.version !== "number") obj.version = 1;
7636
+ const next = upsertFlatHook(obj, "afterFileEdit", buildHookEntry());
7637
+ return `${JSON.stringify(next, null, 2)}\n`;
7638
+ };
7639
+ const installCursor = (opts) => {
7640
+ const paths = resolveCursorPaths(opts);
7641
+ const result = emptyResult();
7642
+ const nextHooks = renderHooksJson(readIfExists(paths.hooks));
7643
+ applyContent(result, opts, paths.hooks, nextHooks, "register afterFileEdit hook");
7644
+ if (opts.scope === "project") {
7645
+ const rules = upsertMarkdownFence(readIfExists(paths.rules), AISLOP_MD_BODY, sentinelHash(AISLOP_MD_BODY)).nextContent;
7646
+ applyContent(result, opts, paths.rules, rules, "write .cursor/rules/aislop.mdc");
7647
+ }
7648
+ return result;
7649
+ };
7650
+ const uninstallCursor = (opts) => {
7651
+ const paths = resolveCursorPaths(opts);
7652
+ const result = {
7653
+ removed: [],
7654
+ skipped: []
7655
+ };
7656
+ const raw = readIfExists(paths.hooks);
7657
+ if (raw) {
7658
+ let obj = {};
7659
+ try {
7660
+ obj = JSON.parse(raw);
7661
+ } catch {
7662
+ obj = {};
7663
+ }
7664
+ const stripped = removeAislopEntries(obj, "afterFileEdit").next;
7665
+ const stillHasHooks = stripped.hooks && typeof stripped.hooks === "object" && Object.keys(stripped.hooks).length > 0;
7666
+ const otherKeys = Object.keys(stripped).filter((k) => k !== "hooks" && k !== "version");
7667
+ if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.hooks, null);
7668
+ else applyRemoval(result, opts, paths.hooks, `${JSON.stringify(stripped, null, 2)}\n`);
7669
+ } else result.skipped.push(paths.hooks);
7670
+ if (opts.scope === "project") applyRemoval(result, opts, paths.rules, null);
7671
+ return result;
7672
+ };
7673
+
7674
+ //#endregion
7675
+ //#region src/hooks/install/gemini.ts
7676
+ const resolveGeminiPaths = (opts) => {
7677
+ const root = opts.scope === "project" ? path.join(opts.cwd, ".gemini") : path.join(opts.home, ".gemini");
7678
+ return {
7679
+ settings: path.join(root, "settings.json"),
7680
+ aislopMd: path.join(root, "AISLOP.md"),
7681
+ geminiMd: path.join(root, "GEMINI.md")
7682
+ };
7683
+ };
7684
+ const buildHookGroup = () => {
7685
+ const hashBody = JSON.stringify({
7686
+ command: "aislop hook gemini",
7687
+ matcher: "write_file|replace"
7688
+ });
7689
+ return {
7690
+ matcher: "write_file|replace",
7691
+ hooks: [{
7692
+ name: "aislop",
7693
+ type: "command",
7694
+ command: "aislop hook gemini",
7695
+ timeout: 5e3,
7696
+ [AISLOP_SENTINEL_KEY]: {
7697
+ v: 1,
7698
+ managed: true,
7699
+ hash: sentinelHash(hashBody)
7700
+ }
7701
+ }]
7702
+ };
7703
+ };
7704
+ const renderSettings = (existingRaw) => {
7705
+ let obj = {};
7706
+ if (existingRaw) try {
7707
+ obj = JSON.parse(existingRaw);
7708
+ } catch {
7709
+ obj = {};
7710
+ }
7711
+ const next = upsertHookGroup(obj, "AfterTool", buildHookGroup());
7712
+ return `${JSON.stringify(next, null, 2)}\n`;
7713
+ };
7714
+ const installGemini = (opts) => {
7715
+ const paths = resolveGeminiPaths(opts);
7716
+ const result = emptyResult();
7717
+ const next = renderSettings(readIfExists(paths.settings));
7718
+ applyContent(result, opts, paths.settings, next, "register AfterTool hook");
7719
+ const fenced = upsertMarkdownFence(readIfExists(paths.aislopMd), AISLOP_MD_BODY, sentinelHash(AISLOP_MD_BODY)).nextContent;
7720
+ applyContent(result, opts, paths.aislopMd, fenced, "write AISLOP.md rules");
7721
+ const existingGeminiMd = readIfExists(paths.geminiMd) ?? "";
7722
+ const marker = "@AISLOP.md";
7723
+ if (!existingGeminiMd.includes(marker)) {
7724
+ const joiner = existingGeminiMd.endsWith("\n") || existingGeminiMd.length === 0 ? "" : "\n";
7725
+ const prefix = existingGeminiMd.length === 0 ? "" : `${existingGeminiMd}${joiner}\n`;
7726
+ applyContent(result, opts, paths.geminiMd, `${prefix}${marker}\n`, "append @AISLOP.md reference");
7727
+ } else result.skipped.push(paths.geminiMd);
7728
+ return result;
7729
+ };
7730
+ const uninstallGemini = (opts) => {
7731
+ const paths = resolveGeminiPaths(opts);
7732
+ const result = {
7733
+ removed: [],
7734
+ skipped: []
7735
+ };
7736
+ const raw = readIfExists(paths.settings);
7737
+ if (raw) {
7738
+ let obj = {};
7739
+ try {
7740
+ obj = JSON.parse(raw);
7741
+ } catch {
7742
+ obj = {};
7743
+ }
7744
+ const stripped = removeAislopEntries(obj, "AfterTool").next;
7745
+ const stillHasHooks = stripped.hooks && typeof stripped.hooks === "object" && Object.keys(stripped.hooks).length > 0;
7746
+ const otherKeys = Object.keys(stripped).filter((k) => k !== "hooks");
7747
+ if (!stillHasHooks && otherKeys.length === 0) applyRemoval(result, opts, paths.settings, null);
7748
+ else applyRemoval(result, opts, paths.settings, `${JSON.stringify(stripped, null, 2)}\n`);
7749
+ } else result.skipped.push(paths.settings);
7750
+ if (readIfExists(paths.aislopMd) != null) applyRemoval(result, opts, paths.aislopMd, null);
7751
+ else result.skipped.push(paths.aislopMd);
7752
+ const geminiMd = readIfExists(paths.geminiMd);
7753
+ if (geminiMd != null && geminiMd.includes("@AISLOP.md")) {
7754
+ const stripped = geminiMd.split("\n").filter((l) => l.trim() !== "@AISLOP.md").join("\n").replace(/\n{3,}/g, "\n\n").trim();
7755
+ applyRemoval(result, opts, paths.geminiMd, stripped.length === 0 ? null : `${stripped}\n`);
7756
+ } else result.skipped.push(paths.geminiMd);
7757
+ return result;
7758
+ };
7759
+
7760
+ //#endregion
7761
+ //#region src/hooks/install/kilocode.ts
7762
+ const resolveKilocodePaths = (opts) => ({ rules: path.join(opts.cwd, ".kilocode", "rules", "aislop-rules.md") });
7763
+ const installKilocode = (opts) => {
7764
+ if (opts.scope !== "project") return {
7765
+ wrote: [],
7766
+ skipped: [],
7767
+ planned: [{
7768
+ path: ".kilocode/rules/aislop-rules.md",
7769
+ summary: "Kilo Code is project-scope only; pass --project"
7770
+ }]
7771
+ };
7772
+ return installRulesOnly(opts, resolveKilocodePaths(opts), "write .kilocode/rules/aislop-rules.md");
7773
+ };
7774
+ const uninstallKilocode = (opts) => {
7775
+ if (opts.scope !== "project") return {
7776
+ removed: [],
7777
+ skipped: []
7778
+ };
7779
+ return uninstallRulesOnly(opts, resolveKilocodePaths(opts));
7780
+ };
7781
+
7782
+ //#endregion
7783
+ //#region src/hooks/install/windsurf.ts
7784
+ const resolveWindsurfPaths = (opts) => ({ rules: path.join(opts.cwd, ".windsurfrules") });
7785
+ const installWindsurf = (opts) => {
7786
+ if (opts.scope !== "project") return {
7787
+ wrote: [],
7788
+ skipped: [],
7789
+ planned: [{
7790
+ path: ".windsurfrules",
7791
+ summary: "Windsurf is project-scope only; pass --project"
7792
+ }]
7793
+ };
7794
+ return installRulesOnly(opts, resolveWindsurfPaths(opts), "write .windsurfrules");
7795
+ };
7796
+ const uninstallWindsurf = (opts) => {
7797
+ if (opts.scope !== "project") return {
7798
+ removed: [],
7799
+ skipped: []
7800
+ };
7801
+ return uninstallRulesOnly(opts, resolveWindsurfPaths(opts));
7802
+ };
7803
+
7804
+ //#endregion
7805
+ //#region src/hooks/install/registry.ts
7806
+ const ALL_AGENTS = [
7807
+ "claude",
7808
+ "cursor",
7809
+ "gemini",
7810
+ "codex",
7811
+ "windsurf",
7812
+ "cline",
7813
+ "kilocode",
7814
+ "antigravity",
7815
+ "copilot"
7816
+ ];
7817
+ const AGENTS_PROJECT_ONLY = [
7818
+ "windsurf",
7819
+ "cline",
7820
+ "kilocode",
7821
+ "antigravity",
7822
+ "copilot"
7823
+ ];
7824
+ const AGENTS_SUPPORTING_BOTH_SCOPES = [
7825
+ "claude",
7826
+ "cursor",
7827
+ "gemini",
7828
+ "codex"
7829
+ ];
7830
+ const paths = {
7831
+ claude: (opts) => {
7832
+ const p = resolveClaudePaths(opts);
7833
+ return [
7834
+ p.settings,
7835
+ p.aislopMd,
7836
+ p.claudeMd
7837
+ ];
7838
+ },
7839
+ cursor: (opts) => {
7840
+ const p = resolveCursorPaths(opts);
7841
+ return opts.scope === "project" ? [p.hooks, p.rules] : [p.hooks];
7842
+ },
7843
+ gemini: (opts) => {
7844
+ const p = resolveGeminiPaths(opts);
7845
+ return [
7846
+ p.settings,
7847
+ p.aislopMd,
7848
+ p.geminiMd
7849
+ ];
7850
+ },
7851
+ codex: (opts) => [resolveCodexPaths(opts).rules],
7852
+ windsurf: (opts) => [resolveWindsurfPaths(opts).rules],
7853
+ cline: (opts) => [resolveClinePaths(opts).rules, resolveRooPaths(opts).rules],
7854
+ kilocode: (opts) => [resolveKilocodePaths(opts).rules],
7855
+ antigravity: (opts) => [resolveAntigravityPaths(opts).rules],
7856
+ copilot: (opts) => [resolveCopilotPaths(opts).rules]
7857
+ };
7858
+ const REGISTRY = {
7859
+ claude: {
7860
+ install: installClaude,
7861
+ uninstall: uninstallClaude,
7862
+ paths: paths.claude
7863
+ },
7864
+ cursor: {
7865
+ install: installCursor,
7866
+ uninstall: uninstallCursor,
7867
+ paths: paths.cursor
7868
+ },
7869
+ gemini: {
7870
+ install: installGemini,
7871
+ uninstall: uninstallGemini,
7872
+ paths: paths.gemini
7873
+ },
7874
+ codex: {
7875
+ install: installCodex,
7876
+ uninstall: uninstallCodex,
7877
+ paths: paths.codex
7878
+ },
7879
+ windsurf: {
7880
+ install: installWindsurf,
7881
+ uninstall: uninstallWindsurf,
7882
+ paths: paths.windsurf
7883
+ },
7884
+ cline: {
7885
+ install: installCline,
7886
+ uninstall: uninstallCline,
7887
+ paths: paths.cline
7888
+ },
7889
+ kilocode: {
7890
+ install: installKilocode,
7891
+ uninstall: uninstallKilocode,
7892
+ paths: paths.kilocode
7893
+ },
7894
+ antigravity: {
7895
+ install: installAntigravity,
7896
+ uninstall: uninstallAntigravity,
7897
+ paths: paths.antigravity
7898
+ },
7899
+ copilot: {
7900
+ install: installCopilot,
7901
+ uninstall: uninstallCopilot,
7902
+ paths: paths.copilot
7903
+ }
7904
+ };
7905
+ const defaultScopeFor = (agent) => AGENTS_PROJECT_ONLY.includes(agent) ? "project" : "global";
7906
+ const detectInstalledAgents = (opts) => {
7907
+ const hits = [];
7908
+ for (const agent of ALL_AGENTS) {
7909
+ const scope = defaultScopeFor(agent);
7910
+ if (REGISTRY[agent].paths({
7911
+ home: opts.home,
7912
+ cwd: opts.cwd,
7913
+ scope
7914
+ }).some((p) => fs.existsSync(p))) hits.push(agent);
7915
+ }
7916
+ return hits;
7917
+ };
7918
+
7919
+ //#endregion
7920
+ //#region src/commands/hook.ts
7921
+ const resolveOpts = (agent, flags) => {
7922
+ const scope = AGENTS_PROJECT_ONLY.includes(agent) ? "project" : flags.scope;
7923
+ return {
7924
+ home: os.homedir(),
7925
+ cwd: process.cwd(),
7926
+ scope,
7927
+ dryRun: flags.dryRun,
7928
+ qualityGate: flags.qualityGate
7929
+ };
7930
+ };
7931
+ const printPlan = (agent, result) => {
7932
+ if (result.planned.length === 0) {
7933
+ process.stdout.write(` ${agent}: already up to date\n`);
7934
+ return;
7935
+ }
7936
+ process.stdout.write(` ${agent}:\n`);
7937
+ for (const op of result.planned) process.stdout.write(` ${style(theme, "dim", "+")} ${op.path} — ${op.summary}\n`);
7938
+ };
7939
+ const hookInstall = async (flags) => {
7940
+ if (flags.dryRun) process.stdout.write("aislop hook install (dry-run)\n\n");
7941
+ for (const agent of flags.agents) {
7942
+ const opts = resolveOpts(agent, flags);
7943
+ const result = REGISTRY[agent].install(opts);
7944
+ if (flags.dryRun) {
7945
+ printPlan(agent, result);
7946
+ continue;
7947
+ }
7948
+ if (result.wrote.length === 0) {
7949
+ process.stdout.write(`${agent}: nothing to do (already up to date)\n`);
7950
+ continue;
7951
+ }
7952
+ for (const f of result.wrote) process.stdout.write(` wrote ${f}\n`);
7953
+ for (const f of result.skipped) process.stdout.write(` skip ${f}\n`);
7954
+ }
7955
+ if (flags.dryRun) process.stdout.write("\nNo files touched. Re-run without --dry-run to apply.\n");
7956
+ };
7957
+ const hookUninstall = async (flags) => {
7958
+ if (flags.dryRun) process.stdout.write("aislop hook uninstall (dry-run)\n\n");
7959
+ for (const agent of flags.agents) {
7960
+ const opts = resolveOpts(agent, flags);
7961
+ const result = REGISTRY[agent].uninstall(opts);
7962
+ if (result.removed.length === 0) {
7963
+ process.stdout.write(`${agent}: nothing installed\n`);
7964
+ continue;
7965
+ }
7966
+ for (const f of result.removed) process.stdout.write(` remove ${f}\n`);
7967
+ for (const f of result.skipped) process.stdout.write(` skip ${f}\n`);
7968
+ }
7969
+ };
7970
+ const hookStatus = async () => {
7971
+ const home = os.homedir();
7972
+ const cwd = process.cwd();
7973
+ process.stdout.write("aislop hook status\n\n");
7974
+ const installed = new Set(detectInstalledAgents({
7975
+ home,
7976
+ cwd
7977
+ }));
7978
+ for (const agent of ALL_AGENTS) {
7979
+ const scope = defaultScopeFor(agent);
7980
+ const hits = REGISTRY[agent].paths({
7981
+ home,
7982
+ cwd,
7983
+ scope
7984
+ }).filter((p) => fs.existsSync(p));
7985
+ const status = installed.has(agent) ? "installed" : "not installed";
7986
+ const marker = installed.has(agent) ? "✓" : "·";
7987
+ process.stdout.write(` ${marker} ${agent.padEnd(12)} ${scope.padEnd(8)} ${status}\n`);
7988
+ for (const p of hits) process.stdout.write(` ${p}\n`);
7989
+ }
7990
+ };
7991
+ const hookRun = async (agent, flags) => {
7992
+ let exitCode = 0;
7993
+ if (agent === "claude") exitCode = flags?.stop ? await runClaudeStopHook() : await runClaudeHook();
7994
+ else if (agent === "cursor") exitCode = await runCursorHook();
7995
+ else if (agent === "gemini") exitCode = await runGeminiHook();
7996
+ else {
7997
+ process.stderr.write(`hook: agent "${agent}" has no runtime adapter (rules-file-only)\n`);
7998
+ process.exit(0);
7999
+ }
8000
+ process.exit(exitCode);
8001
+ };
8002
+ const hookBaseline = async () => {
8003
+ const result = await captureBaseline(process.cwd());
8004
+ process.stdout.write(`baseline captured: score=${result.score} files=${result.fileCount}\n`);
8005
+ process.stdout.write(` -> ${result.path}\n`);
8006
+ };
8007
+ const parseAgentFlag = (raw, fallback) => {
8008
+ if (!raw) return fallback;
8009
+ const parts = raw.split(",").map((s) => s.trim()).filter(Boolean);
8010
+ const unknown = parts.filter((p) => !ALL_AGENTS.includes(p));
8011
+ if (unknown.length > 0) throw new Error(`Unknown agent(s): ${unknown.join(", ")}. Valid: ${ALL_AGENTS.join(", ")}`);
8012
+ return parts;
8013
+ };
8014
+ const defaultInstallTargets = () => {
8015
+ return AGENTS_SUPPORTING_BOTH_SCOPES;
8016
+ };
8017
+
6436
8018
  //#endregion
6437
8019
  //#region src/commands/init.ts
6438
8020
  const buildInitSuccessRender = (input) => {
@@ -7004,6 +8586,45 @@ program.command("ci [directory]").description("CI-friendly JSON output with exit
7004
8586
  program.command("rules [directory]").description("List all available rules").action(async (directory = ".") => {
7005
8587
  await rulesCommand(directory);
7006
8588
  });
8589
+ const hook = program.command("hook").description("Install or invoke AI-agent integration hooks");
8590
+ const resolveScope = (flags) => {
8591
+ if (flags.project) return "project";
8592
+ if (flags.global) return "global";
8593
+ return "global";
8594
+ };
8595
+ hook.command("install").description("Install aislop hooks for one or more coding agents").option("--agent <names>", "comma-separated agent list (claude,cursor,gemini,codex,windsurf,cline,kilocode,antigravity,copilot). default: all non-project-only agents").option("-g, --global", "install to the user-scope config (default for agents that support it)").option("--project", "install to the project-scope config").option("--dry-run", "print the planned diff without writing").option("--yes", "skip the confirmation prompt (reserved)").option("--quality-gate", "add a Stop hook that blocks when score regresses below baseline (Claude only)").action(async (opts) => {
8596
+ await hookInstall({
8597
+ agents: parseAgentFlag(opts.agent, defaultInstallTargets()),
8598
+ scope: resolveScope(opts),
8599
+ dryRun: Boolean(opts.dryRun),
8600
+ yes: Boolean(opts.yes),
8601
+ qualityGate: Boolean(opts.qualityGate)
8602
+ });
8603
+ });
8604
+ hook.command("uninstall").description("Uninstall aislop hooks for one or more agents").option("--agent <names>", "comma-separated agent list. default: all agents with installed hooks").option("-g, --global", "uninstall from user-scope config").option("--project", "uninstall from project-scope config").option("--dry-run", "print the planned removal without writing").action(async (opts) => {
8605
+ await hookUninstall({
8606
+ agents: parseAgentFlag(opts.agent, defaultInstallTargets()),
8607
+ scope: resolveScope(opts),
8608
+ dryRun: Boolean(opts.dryRun),
8609
+ yes: true,
8610
+ qualityGate: false
8611
+ });
8612
+ });
8613
+ hook.command("status").description("Show which agent hooks are installed").action(async () => {
8614
+ await hookStatus();
8615
+ });
8616
+ hook.command("baseline").description("Capture the current project score as the quality-gate baseline").action(async () => {
8617
+ await hookBaseline();
8618
+ });
8619
+ hook.command("claude").description("Internal: Claude Code PostToolUse / Stop callback (reads stdin)").option("--stop", "run in Stop-hook mode for the quality gate").action(async (opts) => {
8620
+ await hookRun("claude", { stop: Boolean(opts.stop) });
8621
+ });
8622
+ hook.command("cursor").description("Internal: Cursor afterFileEdit callback (reads stdin)").action(async () => {
8623
+ await hookRun("cursor");
8624
+ });
8625
+ hook.command("gemini").description("Internal: Gemini CLI AfterTool callback (reads stdin)").action(async () => {
8626
+ await hookRun("gemini");
8627
+ });
7007
8628
  const main = async () => {
7008
8629
  await program.parseAsync();
7009
8630
  await flushTelemetry();