@7n/rules 1.4.1 → 1.5.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.
Files changed (102) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/bin/n-rules.js +65 -17
  3. package/package.json +11 -1
  4. package/rules/abie/clean_merged_ignore_branches/concern.json +3 -0
  5. package/rules/docker/lint_docker_yml/concern.json +3 -0
  6. package/rules/js/lint_js_yml/concern.json +3 -0
  7. package/rules/k8s/lint_k8s_yml/concern.json +3 -0
  8. package/rules/npm-module/npm_publish_yml/concern.json +3 -0
  9. package/rules/php/lint_php_yml/concern.json +3 -0
  10. package/rules/python/lint_python_yml/concern.json +3 -0
  11. package/rules/rust/lint_rust_yml/concern.json +3 -0
  12. package/rules/rust/toolchain_cache/concern.json +10 -1
  13. package/rules/security/lint_security_yml/concern.json +3 -0
  14. package/rules/style/lint_style_yml/concern.json +3 -0
  15. package/rules/tauri/main.mdc +1 -1
  16. package/rules/tauri/release/release.mdc +15 -0
  17. package/rules/text/lint_text/concern.json +3 -0
  18. package/schemas/concern.json +86 -16
  19. package/schemas/n-rules.json +11 -1
  20. package/schemas/v8r-catalog.json +123 -27
  21. package/scripts/auto-rules.mjs +47 -6
  22. package/scripts/docs/auto-rules.md +3 -1
  23. package/scripts/docs/skills-cli.md +3 -2
  24. package/scripts/docs/upgrade-n-rules-and-install.md +3 -1
  25. package/scripts/lib/acp-runner.mjs +174 -0
  26. package/scripts/lib/concern-meta.mjs +8 -1
  27. package/scripts/lib/docs/acp-runner.md +58 -0
  28. package/scripts/lib/docs/concern-meta.md +1 -1
  29. package/scripts/lib/docs/read-n-rules-config-lite.md +3 -1
  30. package/scripts/lib/docs/resolve-plugins.md +17 -0
  31. package/scripts/lib/docs/run-conftest-batch.md +3 -1
  32. package/scripts/lib/lint-surface/docs/policy-lint-adapter.md +3 -1
  33. package/scripts/lib/lint-surface/docs/run-detectors.md +5 -1
  34. package/scripts/lib/lint-surface/policy-lint-adapter.mjs +2 -0
  35. package/scripts/lib/lint-surface/run-detectors.mjs +72 -5
  36. package/scripts/lib/read-n-rules-config-lite.mjs +5 -3
  37. package/scripts/lib/resolve-plugins.mjs +248 -0
  38. package/scripts/lib/run-conftest-batch.mjs +3 -1
  39. package/scripts/skills-cli.mjs +30 -52
  40. package/scripts/upgrade-n-rules-and-install.mjs +5 -4
  41. package/rules/ga/clean_ga_workflows/clean_ga_workflows.mdc +0 -20
  42. package/rules/ga/clean_ga_workflows/clean_ga_workflows.rego +0 -112
  43. package/rules/ga/clean_ga_workflows/concern.json +0 -10
  44. package/rules/ga/clean_ga_workflows/docs/fix-clean_ga_workflows.md +0 -33
  45. package/rules/ga/clean_ga_workflows/docs/index.md +0 -9
  46. package/rules/ga/clean_ga_workflows/fix-clean_ga_workflows.mjs +0 -8
  47. package/rules/ga/clean_ga_workflows/template/clean-ga-workflows.yml.snippet.yml +0 -26
  48. package/rules/ga/clean_merged_branch/clean_merged_branch.mdc +0 -24
  49. package/rules/ga/clean_merged_branch/clean_merged_branch.rego +0 -163
  50. package/rules/ga/clean_merged_branch/concern.json +0 -10
  51. package/rules/ga/clean_merged_branch/docs/fix-clean_merged_branch.md +0 -27
  52. package/rules/ga/clean_merged_branch/docs/index.md +0 -9
  53. package/rules/ga/clean_merged_branch/fix-clean_merged_branch.mjs +0 -8
  54. package/rules/ga/clean_merged_branch/template/clean-merged-branch.yml.snippet.yml +0 -37
  55. package/rules/ga/docs/index.md +0 -11
  56. package/rules/ga/git_ai/concern.json +0 -11
  57. package/rules/ga/git_ai/docs/fix-git_ai.md +0 -30
  58. package/rules/ga/git_ai/docs/index.md +0 -9
  59. package/rules/ga/git_ai/fix-git_ai.mjs +0 -5
  60. package/rules/ga/git_ai/git_ai.mdc +0 -19
  61. package/rules/ga/git_ai/git_ai.rego +0 -76
  62. package/rules/ga/git_ai/template/git-ai.yml.snippet.yml +0 -30
  63. package/rules/ga/lint_ga/concern.json +0 -10
  64. package/rules/ga/lint_ga/docs/fix-lint_ga.md +0 -32
  65. package/rules/ga/lint_ga/docs/index.md +0 -9
  66. package/rules/ga/lint_ga/fix-lint_ga.mjs +0 -5
  67. package/rules/ga/lint_ga/lint_ga.mdc +0 -21
  68. package/rules/ga/lint_ga/lint_ga.rego +0 -132
  69. package/rules/ga/lint_ga/template/lint-ga.yml.snippet.yml +0 -41
  70. package/rules/ga/main.json +0 -1
  71. package/rules/ga/main.mdc +0 -17
  72. package/rules/ga/vscode_extensions/concern.json +0 -11
  73. package/rules/ga/vscode_extensions/docs/fix-vscode_extensions.md +0 -25
  74. package/rules/ga/vscode_extensions/docs/index.md +0 -11
  75. package/rules/ga/vscode_extensions/fix-vscode_extensions.mjs +0 -1
  76. package/rules/ga/vscode_extensions/template/extensions.json.snippet.json +0 -1
  77. package/rules/ga/vscode_extensions/vscode_extensions.mdc +0 -9
  78. package/rules/ga/vscode_extensions/vscode_extensions.rego +0 -18
  79. package/rules/ga/vscode_settings/concern.json +0 -11
  80. package/rules/ga/vscode_settings/docs/fix-vscode_settings.md +0 -26
  81. package/rules/ga/vscode_settings/docs/index.md +0 -9
  82. package/rules/ga/vscode_settings/fix-vscode_settings.mjs +0 -5
  83. package/rules/ga/vscode_settings/template/settings.json.snippet.json +0 -1
  84. package/rules/ga/vscode_settings/vscode_settings.mdc +0 -9
  85. package/rules/ga/vscode_settings/vscode_settings.rego +0 -22
  86. package/rules/ga/workflow_common/concern.json +0 -9
  87. package/rules/ga/workflow_common/template/uses-min-versions.snippet.json +0 -4
  88. package/rules/ga/workflow_common/workflow_common.mdc +0 -18
  89. package/rules/ga/workflow_common/workflow_common.rego +0 -288
  90. package/rules/ga/workflows/concern.json +0 -8
  91. package/rules/ga/workflows/docs/fix-workflows.md +0 -37
  92. package/rules/ga/workflows/docs/main.md +0 -38
  93. package/rules/ga/workflows/fix-workflows.mjs +0 -212
  94. package/rules/ga/workflows/main.mjs +0 -433
  95. package/rules/ga/workflows/workflows.mdc +0 -33
  96. package/rules/ga/zizmor_yml/concern.json +0 -11
  97. package/rules/ga/zizmor_yml/docs/fix-zizmor_yml.md +0 -28
  98. package/rules/ga/zizmor_yml/docs/index.md +0 -9
  99. package/rules/ga/zizmor_yml/fix-zizmor_yml.mjs +0 -3
  100. package/rules/ga/zizmor_yml/template/zizmor.yml.snippet.yml +0 -5
  101. package/rules/ga/zizmor_yml/zizmor_yml.mdc +0 -9
  102. package/rules/ga/zizmor_yml/zizmor_yml.rego +0 -27
@@ -6,139 +6,220 @@
6
6
  "name": "n-rules.json",
7
7
  "description": "Конфігурація @7n/rules (.n-rules.json у корені репозиторію)",
8
8
  "url": "n-rules.json",
9
- "fileMatch": [".n-rules.json", "**/.n-rules.json", ".n-rules.example.json", "**/.n-rules.example.json"]
9
+ "fileMatch": [
10
+ ".n-rules.json",
11
+ "**/.n-rules.json",
12
+ ".n-rules.example.json",
13
+ "**/.n-rules.example.json"
14
+ ]
10
15
  },
11
16
  {
12
17
  "name": "any.json",
13
18
  "description": "Заглушка-схема (приймає будь-який JSON) — bespoke tool-конфіги й one-off артефакти без офіційної схеми",
14
19
  "url": "any.json",
15
- "fileMatch": [".licensee.json", "**/.licensee.json", "docs/specs/**/*.json", "docs/plans/**/*.json", "any.json"]
20
+ "fileMatch": [
21
+ ".licensee.json",
22
+ "**/.licensee.json",
23
+ "docs/specs/**/*.json",
24
+ "docs/plans/**/*.json",
25
+ "any.json"
26
+ ]
16
27
  },
17
28
  {
18
29
  "name": "rego-target.json",
19
30
  "description": "Маніфест rego-полісі (npm/rules/<id>/policy/<name>/target.json) — які файли проєкту фідити в conftest",
20
31
  "url": "target.json",
21
- "fileMatch": ["npm/rules/*/policy/*/target.json", "rules/*/policy/*/target.json"]
32
+ "fileMatch": [
33
+ "npm/rules/*/policy/*/target.json",
34
+ "rules/*/policy/*/target.json",
35
+ "plugins/*/rules/*/policy/*/target.json"
36
+ ]
22
37
  },
23
38
  {
24
39
  "name": "tsconfig",
25
40
  "description": "TypeScript config (tsconfig.json та tsconfig.*.json) у всьому проєкті",
26
41
  "url": "vendor/tsconfig.json",
27
- "fileMatch": ["tsconfig.json", "**/tsconfig.json", "tsconfig.*.json", "**/tsconfig.*.json"]
42
+ "fileMatch": [
43
+ "tsconfig.json",
44
+ "**/tsconfig.json",
45
+ "tsconfig.*.json",
46
+ "**/tsconfig.*.json"
47
+ ]
28
48
  },
29
49
  {
30
50
  "name": "jsconfig",
31
51
  "description": "jsconfig.json у всьому проєкті",
32
52
  "url": "vendor/jsconfig.json",
33
- "fileMatch": ["jsconfig.json", "**/jsconfig.json"]
53
+ "fileMatch": [
54
+ "jsconfig.json",
55
+ "**/jsconfig.json"
56
+ ]
34
57
  },
35
58
  {
36
59
  "name": "n-rules skill meta",
37
60
  "description": "Маніфест skill-пакета n-rules (npm/skills/<id>/main.json) — назва, тригери, дозволи",
38
- "fileMatch": ["npm/skills/*/main.json", "skills/*/main.json"],
61
+ "fileMatch": [
62
+ "npm/skills/*/main.json",
63
+ "skills/*/main.json"
64
+ ],
39
65
  "url": "skill-meta.json"
40
66
  },
41
67
  {
42
68
  "name": "n-rules rule meta",
43
69
  "description": "Маніфест rule-пакета n-rules (npm/rules/<id>/main.json) — назва, опис, версія правила",
44
- "fileMatch": ["npm/rules/*/main.json", "rules/*/main.json"],
70
+ "fileMatch": [
71
+ "npm/rules/*/main.json",
72
+ "rules/*/main.json",
73
+ "plugins/*/rules/*/main.json"
74
+ ],
45
75
  "url": "rule-meta.json"
46
76
  },
47
77
  {
48
78
  "name": "n-rules concern meta",
49
79
  "description": "Маніфест concern-а n-rules (npm/rules/<id>/<concern>/concern.json) — flat-concern layout: check/policy/lint-поверхні",
50
- "fileMatch": ["npm/rules/*/*/concern.json", "rules/*/*/concern.json"],
80
+ "fileMatch": [
81
+ "npm/rules/*/*/concern.json",
82
+ "rules/*/*/concern.json",
83
+ "plugins/*/rules/*/*/concern.json"
84
+ ],
51
85
  "url": "concern.json"
52
86
  },
53
87
  {
54
88
  "name": "package.json",
55
89
  "description": "npm package.json у всьому проєкті",
56
90
  "url": "vendor/package.json",
57
- "fileMatch": ["package.json", "**/package.json"]
91
+ "fileMatch": [
92
+ "package.json",
93
+ "**/package.json"
94
+ ]
58
95
  },
59
96
  {
60
97
  "name": "knip",
61
98
  "description": "knip.json (unused-code detector config)",
62
99
  "url": "vendor/knip.json",
63
- "fileMatch": ["knip.json", "**/knip.json"]
100
+ "fileMatch": [
101
+ "knip.json",
102
+ "**/knip.json"
103
+ ]
64
104
  },
65
105
  {
66
106
  "name": "jscpd",
67
107
  "description": ".jscpd.json (copy-paste detector config)",
68
108
  "url": "vendor/jscpd.json",
69
- "fileMatch": [".jscpd.json", "**/.jscpd.json"]
109
+ "fileMatch": [
110
+ ".jscpd.json",
111
+ "**/.jscpd.json"
112
+ ]
70
113
  },
71
114
  {
72
115
  "name": "cspell",
73
116
  "description": ".cspell.json (spell-checker config)",
74
117
  "url": "vendor/cspell.json",
75
- "fileMatch": [".cspell.json", "**/.cspell.json"]
118
+ "fileMatch": [
119
+ ".cspell.json",
120
+ "**/.cspell.json"
121
+ ]
76
122
  },
77
123
  {
78
124
  "name": "oxlintrc",
79
125
  "description": ".oxlintrc.json (oxlint config)",
80
126
  "url": "vendor/oxlintrc.json",
81
- "fileMatch": [".oxlintrc.json", "**/.oxlintrc.json"]
127
+ "fileMatch": [
128
+ ".oxlintrc.json",
129
+ "**/.oxlintrc.json"
130
+ ]
82
131
  },
83
132
  {
84
133
  "name": "oxfmtrc",
85
134
  "description": ".oxfmtrc.json (oxfmt config)",
86
135
  "url": "vendor/oxfmtrc.json",
87
- "fileMatch": [".oxfmtrc.json", "**/.oxfmtrc.json"]
136
+ "fileMatch": [
137
+ ".oxfmtrc.json",
138
+ "**/.oxfmtrc.json"
139
+ ]
88
140
  },
89
141
  {
90
142
  "name": "claude-code-settings",
91
143
  "description": ".claude/settings.json (Claude Code hooks/permissions)",
92
144
  "url": "vendor/claude-code-settings.json",
93
- "fileMatch": [".claude/settings.json", "**/.claude/settings.json"]
145
+ "fileMatch": [
146
+ ".claude/settings.json",
147
+ "**/.claude/settings.json"
148
+ ]
94
149
  },
95
150
  {
96
151
  "name": "codex-hooks",
97
152
  "description": ".codex/hooks.json (Codex CLI hooks)",
98
153
  "url": "vendor/codex-hooks.json",
99
- "fileMatch": [".codex/hooks.json", "**/.codex/hooks.json"]
154
+ "fileMatch": [
155
+ ".codex/hooks.json",
156
+ "**/.codex/hooks.json"
157
+ ]
100
158
  },
101
159
  {
102
160
  "name": "mcp",
103
161
  "description": ".mcp.json (MCP-сервери проєкту: Claude Code/Cursor)",
104
162
  "url": "vendor/mcp.json",
105
- "fileMatch": [".mcp.json", "**/.mcp.json"]
163
+ "fileMatch": [
164
+ ".mcp.json",
165
+ "**/.mcp.json"
166
+ ]
106
167
  },
107
168
  {
108
169
  "name": "marksman",
109
170
  "description": ".marksman.toml — офіційної схеми нема (permissive any.json)",
110
171
  "url": "any.json",
111
- "fileMatch": [".marksman.toml", "**/.marksman.toml"]
172
+ "fileMatch": [
173
+ ".marksman.toml",
174
+ "**/.marksman.toml"
175
+ ]
112
176
  },
113
177
  {
114
178
  "name": "cargo",
115
179
  "description": "Cargo.toml (маніфест Rust-крейта/воркспейса)",
116
180
  "url": "vendor/cargo.json",
117
- "fileMatch": ["Cargo.toml", "**/Cargo.toml"]
181
+ "fileMatch": [
182
+ "Cargo.toml",
183
+ "**/Cargo.toml"
184
+ ]
118
185
  },
119
186
  {
120
187
  "name": "cargo-mutants",
121
188
  "description": ".cargo/mutants.toml (конфігурація cargo-mutants)",
122
189
  "url": "vendor/cargo-mutants-config.json",
123
- "fileMatch": [".cargo/mutants.toml", "**/.cargo/mutants.toml"]
190
+ "fileMatch": [
191
+ ".cargo/mutants.toml",
192
+ "**/.cargo/mutants.toml"
193
+ ]
124
194
  },
125
195
  {
126
196
  "name": "cargo-deny",
127
197
  "description": "deny.toml (cargo-deny) — у schemastore схеми нема (permissive any.json)",
128
198
  "url": "any.json",
129
- "fileMatch": ["deny.toml", "**/deny.toml"]
199
+ "fileMatch": [
200
+ "deny.toml",
201
+ "**/deny.toml"
202
+ ]
130
203
  },
131
204
  {
132
205
  "name": "tauri-conf-overlay",
133
206
  "description": "tauri.conf.<variant>.json — частковий merge-overlay базового tauri.conf.json (повна tauri-схема відхиляє partial), тому permissive any.json",
134
207
  "url": "any.json",
135
- "fileMatch": ["tauri.conf.*.json", "**/tauri.conf.*.json"]
208
+ "fileMatch": [
209
+ "tauri.conf.*.json",
210
+ "**/tauri.conf.*.json"
211
+ ]
136
212
  },
137
213
  {
138
214
  "name": "github-action",
139
215
  "description": "action.yml/action.yaml (GitHub composite/JS actions)",
140
216
  "url": "vendor/github-action.json",
141
- "fileMatch": ["action.yml", "**/action.yml", "action.yaml", "**/action.yaml"]
217
+ "fileMatch": [
218
+ "action.yml",
219
+ "**/action.yml",
220
+ "action.yaml",
221
+ "**/action.yaml"
222
+ ]
142
223
  },
143
224
  {
144
225
  "name": "github-workflow",
@@ -155,25 +236,40 @@
155
236
  "name": "zizmor",
156
237
  "description": ".github/zizmor.yml (конфігурація zizmor — статичний аналізатор GitHub Actions)",
157
238
  "url": "vendor/zizmor.json",
158
- "fileMatch": [".github/zizmor.yml", "**/.github/zizmor.yml", ".github/zizmor.yaml", "**/.github/zizmor.yaml"]
239
+ "fileMatch": [
240
+ ".github/zizmor.yml",
241
+ "**/.github/zizmor.yml",
242
+ ".github/zizmor.yaml",
243
+ "**/.github/zizmor.yaml"
244
+ ]
159
245
  },
160
246
  {
161
247
  "name": "bunfig",
162
248
  "description": "bunfig.toml (конфігурація Bun runtime)",
163
249
  "url": "vendor/bunfig.json",
164
- "fileMatch": ["bunfig.toml", "**/bunfig.toml"]
250
+ "fileMatch": [
251
+ "bunfig.toml",
252
+ "**/bunfig.toml"
253
+ ]
165
254
  },
166
255
  {
167
256
  "name": "mt-config",
168
257
  "description": ".mt.json / .mt-override.json — конфігурація @7n/mt (Nitra MT)",
169
258
  "url": "vendor/mt-config.json",
170
- "fileMatch": [".mt.json", "**/.mt.json", "**/.mt-override.json"]
259
+ "fileMatch": [
260
+ ".mt.json",
261
+ "**/.mt.json",
262
+ "**/.mt-override.json"
263
+ ]
171
264
  },
172
265
  {
173
266
  "name": "layers-config",
174
267
  "description": "Конфіг рушія шарової документації @7n/layers (<docsDir>/layers.json, напр. npm/docs/layers.json у репо mt)",
175
268
  "url": "layers-config.json",
176
- "fileMatch": ["**/docs/layers.json", "layers.json"]
269
+ "fileMatch": [
270
+ "**/docs/layers.json",
271
+ "layers.json"
272
+ ]
177
273
  }
178
274
  ]
179
275
  }
@@ -73,6 +73,43 @@ export function discoverRuleAutoActivation(rulesDir = RULES_DIR) {
73
73
 
74
74
  const RULE_AUTO_ACTIVATION = discoverRuleAutoActivation()
75
75
 
76
+ /** Кеш агрегованої активації для multi-dir прогонів: ключ — join(dirs). */
77
+ const AGGREGATED_ACTIVATION_CACHE = new Map()
78
+
79
+ /**
80
+ * Агрегована мапа активації по кількох rules-каталогах (ядро + плагіни): правила
81
+ * зливаються за id, перший власник виграє (порядок каталогів = пріоритет).
82
+ * Без `rulesDirs` — вбудовані module-level константи (шлях без плагінів).
83
+ * @param {string[]} [rulesDirs] упорядковані rules-каталоги
84
+ * @returns {{ activation: Record<string, import('./lib/rule-meta.mjs').RuleAutoSpec>, order: string[], dependencies: Record<string, readonly string[]> }} активація, порядок, залежності
85
+ */
86
+ export function getRuleAutoActivation(rulesDirs) {
87
+ if (!Array.isArray(rulesDirs) || rulesDirs.length === 0) {
88
+ return { activation: RULE_AUTO_ACTIVATION, order: AUTO_RULE_ORDER, dependencies: AUTO_RULE_DEPENDENCIES }
89
+ }
90
+ const key = rulesDirs.join('\u{0}')
91
+ const cached = AGGREGATED_ACTIVATION_CACHE.get(key)
92
+ if (cached) return cached
93
+ /** @type {Record<string, import('./lib/rule-meta.mjs').RuleAutoSpec>} */
94
+ const activation = {}
95
+ for (const dir of rulesDirs) {
96
+ for (const [id, spec] of Object.entries(discoverRuleAutoActivation(dir))) {
97
+ if (!(id in activation)) activation[id] = spec
98
+ }
99
+ }
100
+ const order = Object.freeze(Object.keys(activation).toSorted((a, b) => a.localeCompare(b)))
101
+ const dependencies = Object.freeze(
102
+ Object.fromEntries(
103
+ Object.entries(activation)
104
+ .filter(([, s]) => 'rules' in s)
105
+ .map(([id, s]) => [id, Object.freeze(/** @type {{rules:string[]}} */ (s).rules)])
106
+ )
107
+ )
108
+ const result = { activation, order, dependencies }
109
+ AGGREGATED_ACTIVATION_CACHE.set(key, result)
110
+ return result
111
+ }
112
+
76
113
  /** Стабільний алфавітний порядок (замість хардкод-масиву). */
77
114
  export const AUTO_RULE_ORDER = Object.freeze(Object.keys(RULE_AUTO_ACTIVATION).toSorted((a, b) => a.localeCompare(b)))
78
115
 
@@ -279,13 +316,14 @@ async function collectRepoPaths(root) {
279
316
  * стежити за порядком викликів `addRule`.
280
317
  * @param {string[]} detectedRules уже зібрані id правил (мутується через addRule)
281
318
  * @param {(ruleId: string) => void} addRule callback із спільної фабрики (поважає `disable-rules` і дублі)
319
+ * @param {Record<string, readonly string[]>} [dependencies] граф залежностей (дефолт — вбудований)
282
320
  * @returns {void}
283
321
  */
284
- function resolveRuleDependencies(detectedRules, addRule) {
322
+ function resolveRuleDependencies(detectedRules, addRule, dependencies = AUTO_RULE_DEPENDENCIES) {
285
323
  let changed = true
286
324
  while (changed) {
287
325
  changed = false
288
- for (const [ruleId, deps] of Object.entries(AUTO_RULE_DEPENDENCIES)) {
326
+ for (const [ruleId, deps] of Object.entries(dependencies)) {
289
327
  if (detectedRules.includes(ruleId)) continue
290
328
  if (deps.every(d => detectedRules.includes(d))) {
291
329
  const before = detectedRules.length
@@ -332,14 +370,17 @@ function specMatches(spec, ctx) {
332
370
  * @param {string[]} params.availableRules перелік доступних правил з пакету
333
371
  * @param {unknown} params.packageJsonParsed кореневий package.json (розпарсений) або null
334
372
  * @param {string[]} [params.disableRules] список `disable-rules` з конфігу
373
+ * @param {string[]} [params.rulesDirs] rules-каталоги (ядро + плагіни); без них — вбудований
335
374
  * @returns {Promise<{ rules: string[] }>} список id у стабільному порядку (за `AUTO_RULE_ORDER`)
336
375
  */
337
376
  export async function detectAutoRules({
338
377
  root,
339
378
  availableRules,
340
379
  packageJsonParsed,
341
- disableRules = DEFAULT_DISABLED_LIST
380
+ disableRules = DEFAULT_DISABLED_LIST,
381
+ rulesDirs
342
382
  }) {
383
+ const { activation, order, dependencies } = getRuleAutoActivation(rulesDirs)
343
384
  const facts = await collectAutoRuleFacts(root)
344
385
  const paths = await collectRepoPaths(root)
345
386
  const normalizedRules = new Set(availableRules.map(r => r.trim().toLowerCase()))
@@ -357,13 +398,13 @@ export async function detectAutoRules({
357
398
  detectedRules.push(ruleId)
358
399
  }
359
400
 
360
- for (const [ruleId, spec] of Object.entries(RULE_AUTO_ACTIVATION)) {
401
+ for (const [ruleId, spec] of Object.entries(activation)) {
361
402
  if ('rules' in spec) continue
362
403
  if (await specMatches(spec, { root, facts, paths, packageJsonParsed })) addRule(ruleId)
363
404
  }
364
- resolveRuleDependencies(detectedRules, addRule)
405
+ resolveRuleDependencies(detectedRules, addRule, dependencies)
365
406
 
366
- const rules = AUTO_RULE_ORDER.filter(r => detectedRules.includes(r))
407
+ const rules = order.filter(r => detectedRules.includes(r))
367
408
  return { rules }
368
409
  }
369
410
 
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: auto-rules.mjs
4
4
  resource: npm/scripts/auto-rules.mjs
5
5
  docgen:
6
- crc: 6ce82609
6
+ crc: d4a1b2da
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 90
9
9
  ---
@@ -32,3 +32,5 @@ mergeConfigWithAutoDetected — Обновлює конфігурацію, до
32
32
 
33
33
  - Перехоплює помилки і не пропускає винятків назовні (fail-safe).
34
34
  - Свідомо пропускає шляхи: `.git`, `node_modules`.
35
+
36
+ **Multi-dir (плагіни):** `getRuleAutoActivation(rulesDirs)` агрегує активацію по кількох rules-каталогах (ядро перше, перший власник виграє, кеш на процес); `detectAutoRules` приймає опційний `rulesDirs`.
@@ -9,7 +9,7 @@ docgen:
9
9
 
10
10
  ## Огляд
11
11
 
12
- Цей модуль забезпечує керування функціоналом скілів пакета `@7n/rules`. Він каталогізує доступні скіли на основі наявності файлу `SKILL.md` у відповідному пакеті. Для виконання скілу збирається контекст, що включає інструкції скілу та інформацію з конфігураційних файлів проєкту: `package.json`, `tsconfig.json`, `.n-rules.json` та `main.json`. Система дозволяє або вивести список доступних скілів (`npx @7n/rules skill list`), або ініціювати виконання обраного скілу (наприклад, `npx @7n/rules skill pi taze`), з можливістю передачі додаткових аргументів. Пріоритетним механізмом виконання є інтеграція з вбудованим pi-агентом; поряд з ним підтримуються повноцінні зовнішні CLI-раннери (`cursor`, `codex`) і deprecated `claude`.
12
+ Цей модуль забезпечує керування функціоналом скілів пакета `@7n/rules`. Він каталогізує доступні скіли на основі наявності файлу `SKILL.md` у відповідному пакеті. Для виконання скілу збирається контекст, що включає інструкції скілу та інформацію з конфігураційних файлів проєкту: `package.json`, `tsconfig.json`, `.n-rules.json` та `main.json`. Система дозволяє або вивести список доступних скілів (`npx @7n/rules skill list`), або ініціювати виконання обраного скілу (наприклад, `npx @7n/rules skill pi taze`), з можливістю передачі додаткових аргументів. Пріоритетним механізмом виконання є інтеграція з вбудованим pi-агентом; поряд з ним підтримуються повноцінні зовнішні ACP-агенти (`cursor`, `codex`) і deprecated `claude`.
13
13
 
14
14
  ## Поведінка
15
15
 
@@ -30,6 +30,7 @@ runSkillsCli виконує логіку командного інтерфейс
30
30
 
31
31
  ## Гарантії поведінки
32
32
 
33
- - Сам модуль лише читає файли й збирає промпт; **виконання** делегується агенту: `pi` (вбудований, мутує дерево, запускає bash) або зовнішнім `cursor`/`codex`/`claude` CLI (промпт передається через stdin: `cursor-agent -p`, `codex exec -`, `claude -p`).
33
+ - Сам модуль лише читає файли й збирає промпт; **виконання** делегується агенту: `pi` (вбудований, мутує дерево, запускає bash) або зовнішньому ACP-агенту `cursor`/`codex`/`claude` через `runAcpRunner` (`./lib/acp-runner.mjs`) JSON-RPC поверх stdio (`cursor-agent acp`, бандловані адаптери `@agentclientprotocol/codex-acp` і `@agentclientprotocol/claude-agent-acp`), а не сирий `stdin`/`stdout`-піпінг.
34
34
  - Тира моделі для `pi`-runner береться з `main.json.tier` скіла (дефолт `max`).
35
+ - ACP-раннер автоапрувляє `session/request_permission` (без інтерактивних питань) — паритет із колишнім non-interactive режимом; `Client`-реалізація читає/пише файли напряму через `node:fs`.
35
36
  - Лише `claude` — deprecated: друкує попередження й лишається як fallback, доки не налаштовано pi-модель. `cursor`/`codex` — повноцінні раннери без попередження.
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: upgrade-n-rules-and-install.mjs
4
4
  resource: npm/scripts/upgrade-n-rules-and-install.mjs
5
5
  docgen:
6
- crc: 715ef559
6
+ crc: f33841b9
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.98
@@ -34,3 +34,5 @@ upgradeNRulesToLatestAndBunInstall — Оновлює `@7n/rules` до оста
34
34
  - Свідомо пропускає шляхи: `node_modules`.
35
35
 
36
36
  **Міграція перейменування:** `migrateLegacyDependencyName` переносить legacy-ключ `@nitra/cursor` → `@7n/rules` у тій самій секції package.json перед оновленням версії; якщо registry ще не має `@7n/rules` (404 до першої публікації), версія береться з package.json пакету поточного процесу (fallback).
37
+
38
+ **packageName:** `resolveInstalledPackageRoot(projectRoot, fallbackPackageRoot, packageName)` — третій параметр (дефолт `@7n/rules`) дозволяє резолвити встановлені плагіни.
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Виконання скіла через зовнішнього ACP-агента (Agent Client Protocol,
3
+ * agentclientprotocol.com) — JSON-RPC поверх stdio замість сирого
4
+ * `stdin`/`stdout`-піпінгу CLI.
5
+ *
6
+ * Заміняє колишній `runLlmCli` (spawnSync + `-p`/`exec -`) для `cursor`/`codex`/`claude`:
7
+ * підʼєднання йде через офіційний TS SDK `@zed-industries/agent-client-protocol`,
8
+ * а дозволи на tool-calls (`session/request_permission`) автоапрувляться без участі
9
+ * людини — паритет із сьогоднішнім non-interactive `-p`-режимом (full user-trust,
10
+ * без write-guard, як задокументовано в `@7n/llm-lib/agent-skill`).
11
+ *
12
+ * `cursor` — нативний ACP-режим самого `cursor-agent` (`cursor-agent acp`).
13
+ * `codex`/`claude` — офіційні адаптери (`@agentclientprotocol/codex-acp`,
14
+ * `@agentclientprotocol/claude-agent-acp`), що бандлять свій рушій — зовнішній
15
+ * бінарник `codex`/`claude` у PATH більше не потрібен.
16
+ */
17
+
18
+ import { spawn } from 'node:child_process'
19
+ import { readFileSync, writeFileSync } from 'node:fs'
20
+ import { createRequire } from 'node:module'
21
+ import { dirname, join } from 'node:path'
22
+ import { stdout } from 'node:process'
23
+ import { Readable, Writable } from 'node:stream'
24
+
25
+ const require = createRequire(import.meta.url)
26
+
27
+ /**
28
+ * Команда запуску ACP-агента на провайдер. `cursor` — зовнішній бінарник у PATH;
29
+ * `codex`/`claude` — резолвляться з `bin`-запису бандлованого npm-пакета-адаптера.
30
+ * @type {Record<'cursor' | 'codex' | 'claude', { bin: string, args: string[] } | { adapterPackage: string }>}
31
+ */
32
+ export const ACP_AGENT_COMMANDS = {
33
+ cursor: { bin: 'cursor-agent', args: ['acp'] },
34
+ codex: { adapterPackage: '@agentclientprotocol/codex-acp' },
35
+ claude: { adapterPackage: '@agentclientprotocol/claude-agent-acp' }
36
+ }
37
+
38
+ /**
39
+ * `StopReason` ACP-прогону → exit code CLI-скіла.
40
+ * @param {string} stopReason `end_turn`|`max_tokens`|`max_turn_requests`|`refusal`|`cancelled`
41
+ * @returns {0 | 1} 0 — успішне завершення (`end_turn`), 1 — усе інше
42
+ */
43
+ export function stopReasonToExitCode(stopReason) {
44
+ return stopReason === 'end_turn' ? 0 : 1
45
+ }
46
+
47
+ /**
48
+ * Резолвить абсолютний шлях до bin-файлу бандлованого ACP-адаптера з його `package.json`.
49
+ * @param {string} adapterPackage назва npm-пакета адаптера (`@agentclientprotocol/codex-acp`, …)
50
+ * @returns {string} абсолютний шлях до виконуваного файлу адаптера
51
+ */
52
+ export function resolveAdapterBin(adapterPackage) {
53
+ const pkgJsonPath = require.resolve(`${adapterPackage}/package.json`)
54
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8'))
55
+ const binField = pkg.bin
56
+ const binRelPath = typeof binField === 'string' ? binField : Object.values(binField)[0]
57
+ return join(dirname(pkgJsonPath), /** @type {string} */ (binRelPath))
58
+ }
59
+
60
+ /**
61
+ * Обирає `PermissionOption` без участі людини: `allow_always` > `allow_once` > перша опція.
62
+ * Паритет із non-interactive `-p`/`exec -` — скіл є явною user-invocation, тож дозволи
63
+ * не питаються інтерактивно (див. `@7n/llm-lib/agent-skill` — той самий full-trust режим).
64
+ * @param {{ optionId: string, kind: string }[]} options варіанти дозволу з `RequestPermissionRequest`
65
+ * @returns {string} `optionId` обраного варіанту
66
+ */
67
+ export function pickAutoPermissionOptionId(options) {
68
+ const byKind = kind => options.find(option => option.kind === kind)
69
+ const chosen = byKind('allow_always') ?? byKind('allow_once') ?? options[0]
70
+ return chosen.optionId
71
+ }
72
+
73
+ /**
74
+ * ACP `Client` для скіл-раннера: автоапрув дозволів, стрім тексту в stdout,
75
+ * пряма реалізація файлового вводу-виводу поверх `node:fs` (full-trust режим).
76
+ */
77
+ class AcpSkillClient {
78
+ /**
79
+ * @param {{ out: (chunk: string) => void }} deps вивід тексту (парність зі streaming `-p`)
80
+ */
81
+ constructor({ out }) {
82
+ this.out = out
83
+ }
84
+
85
+ /**
86
+ * @param {{ options: { optionId: string, kind: string }[] }} params запит дозволу від агента
87
+ * @returns {Promise<{ outcome: { outcome: 'selected', optionId: string } }>} автообраний варіант
88
+ */
89
+ requestPermission(params) {
90
+ return Promise.resolve({ outcome: { outcome: 'selected', optionId: pickAutoPermissionOptionId(params.options) } })
91
+ }
92
+
93
+ /**
94
+ * @param {{ update: { sessionUpdate: string, content?: { type: string, text?: string } } }} params подія прогресу сесії
95
+ * @returns {Promise<void>} стрімить текстові дельти в stdout; інші типи подій ігноруються
96
+ */
97
+ sessionUpdate(params) {
98
+ const { update } = params
99
+ if (update.sessionUpdate === 'agent_message_chunk' && update.content?.type === 'text') {
100
+ this.out(update.content.text ?? '')
101
+ }
102
+ return Promise.resolve()
103
+ }
104
+
105
+ /**
106
+ * @param {{ path: string }} params шлях до файлу
107
+ * @returns {Promise<{ content: string }>} вміст файлу
108
+ */
109
+ readTextFile(params) {
110
+ return Promise.resolve({ content: readFileSync(params.path, 'utf8') })
111
+ }
112
+
113
+ /**
114
+ * @param {{ path: string, content: string }} params шлях і новий вміст файлу
115
+ * @returns {Promise<Record<string, never>>} порожня відповідь (успіх)
116
+ */
117
+ writeTextFile(params) {
118
+ writeFileSync(params.path, params.content, 'utf8')
119
+ return Promise.resolve({})
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Виконує скіл через зовнішнього ACP-агента (`cursor`/`codex`/`claude`).
125
+ * @param {'cursor' | 'codex' | 'claude'} kind провайдер
126
+ * @param {string} prompt готовий промпт скіла
127
+ * @param {string} projectDir робочий каталог сесії агента
128
+ * @param {(line: string) => void} logError вивід помилок
129
+ * @param {{ acp?: object, spawnFn?: typeof spawn, out?: (chunk: string) => void, resolveAdapterBin?: (pkg: string) => string, isBinaryInPath?: (bin: string) => boolean }} [deps] інжекти для тестів
130
+ * @returns {Promise<number>} exit code (0 — `end_turn`, 1 — інакше)
131
+ */
132
+ export async function runAcpRunner(kind, prompt, projectDir, logError, deps = {}) {
133
+ const spawnFn = deps.spawnFn ?? spawn
134
+ const out = deps.out ?? (chunk => stdout.write(chunk))
135
+ const resolveBin = deps.resolveAdapterBin ?? resolveAdapterBin
136
+
137
+ const command = ACP_AGENT_COMMANDS[kind]
138
+ const { bin, args } =
139
+ 'adapterPackage' in command
140
+ ? { bin: process.execPath, args: [resolveBin(command.adapterPackage)] }
141
+ : command
142
+
143
+ if ('bin' in command && deps.isBinaryInPath && !deps.isBinaryInPath(command.bin)) {
144
+ throw new Error(`\`${command.bin}\` not found in PATH. Install ${kind} CLI (ACP mode) or use \`skill pi\`.`)
145
+ }
146
+
147
+ const acp = deps.acp ?? (await import('@zed-industries/agent-client-protocol'))
148
+ const child = spawnFn(bin, args, { cwd: projectDir, stdio: ['pipe', 'pipe', 'inherit'], env: process.env })
149
+
150
+ try {
151
+ const input = Writable.toWeb(child.stdin)
152
+ const output = /** @type {ReadableStream<Uint8Array>} */ (Readable.toWeb(child.stdout))
153
+ const client = new AcpSkillClient({ out })
154
+ const stream = acp.ndJsonStream(input, output)
155
+ const connection = new acp.ClientSideConnection(() => client, stream)
156
+
157
+ await connection.initialize({
158
+ protocolVersion: acp.PROTOCOL_VERSION,
159
+ clientCapabilities: { fs: { readTextFile: true, writeTextFile: true } }
160
+ })
161
+ const session = await connection.newSession({ cwd: projectDir, mcpServers: [] })
162
+ const result = await connection.prompt({
163
+ sessionId: session.sessionId,
164
+ prompt: [{ type: 'text', text: prompt }]
165
+ })
166
+
167
+ if (result.stopReason !== 'end_turn') {
168
+ logError(`acp ${kind}: stopReason=${result.stopReason}`)
169
+ }
170
+ return stopReasonToExitCode(result.stopReason)
171
+ } finally {
172
+ child.kill()
173
+ }
174
+ }
@@ -27,6 +27,8 @@ import { join } from 'node:path'
27
27
  /**
28
28
  * @typedef {object} ConcernMeta
29
29
  * @property {string} name ім'я concern-а (= basename каталогу)
30
+ * @property {string|undefined} requiresCapability capability з `requires.capability` — концерн
31
+ * активний лише коли якийсь плагін декларує її у своєму маніфесті (напр. `ci:github`)
30
32
  * @property {string} dir абсолютний шлях до каталогу concern-а
31
33
  * @property {boolean} check чи є JS check поверхня
32
34
  * @property {PolicySurface|undefined} policy policy-поверхня concern-а (rego/template) або undefined.
@@ -112,7 +114,12 @@ export async function readConcernMeta(concernDir, name) {
112
114
 
113
115
  if (!check && !policy && !lint) return null
114
116
 
115
- return { name, dir: concernDir, check, policy, lint, fixability: parseFixability(raw.fixability) }
117
+ const requiresCapability =
118
+ raw.requires && typeof raw.requires === 'object' && typeof raw.requires.capability === 'string'
119
+ ? raw.requires.capability
120
+ : undefined
121
+
122
+ return { name, dir: concernDir, check, policy, lint, requiresCapability, fixability: parseFixability(raw.fixability) }
116
123
  }
117
124
 
118
125
  /**