@7n/rules 1.5.1 → 1.7.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 (110) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/bin/n-rules.js +83 -21
  3. package/package.json +1 -2
  4. package/rules/abie/main.mdc +0 -6
  5. package/rules/js/check/docs/main.md +1 -1
  6. package/rules/js/check/main.mjs +6 -10
  7. package/rules/js/main.mdc +0 -1
  8. package/rules/js/vscode_extensions/template/extensions.json.snippet.json +4 -1
  9. package/rules/npm-module/main.mdc +0 -2
  10. package/rules/php/tooling/docs/main.md +1 -1
  11. package/rules/php/tooling/main.mjs +2 -7
  12. package/rules/python/tooling/docs/main.md +1 -1
  13. package/rules/python/tooling/main.mjs +2 -7
  14. package/rules/rego/lib/docs/run-external-tool.md +1 -2
  15. package/rules/rego/lib/run-external-tool.mjs +4 -1
  16. package/rules/security/main.mdc +0 -1
  17. package/rules/style/main.mdc +0 -1
  18. package/rules/style/tooling/docs/main.md +1 -1
  19. package/rules/style/tooling/main.mjs +2 -7
  20. package/rules/tauri/release/release.mdc +1 -1
  21. package/rules/text/formatting/docs/main.md +1 -1
  22. package/rules/text/formatting/main.mjs +11 -11
  23. package/rules/text/main.mdc +0 -1
  24. package/scripts/docs/skills-cli.md +1 -1
  25. package/scripts/lib/acp-runner.mjs +26 -33
  26. package/scripts/lib/docs/acp-runner.md +5 -5
  27. package/scripts/lib/docs/mirror-parity.md +1 -1
  28. package/scripts/lib/mirror-parity.mjs +49 -16
  29. package/scripts/skills-cli.mjs +35 -26
  30. package/rules/abie/clean_merged_ignore_branches/clean_merged_ignore_branches.mdc +0 -19
  31. package/rules/abie/clean_merged_ignore_branches/clean_merged_ignore_branches.rego +0 -81
  32. package/rules/abie/clean_merged_ignore_branches/concern.json +0 -11
  33. package/rules/abie/clean_merged_ignore_branches/docs/fix-clean_merged_ignore_branches.md +0 -31
  34. package/rules/abie/clean_merged_ignore_branches/docs/index.md +0 -9
  35. package/rules/abie/clean_merged_ignore_branches/fix-clean_merged_ignore_branches.mjs +0 -8
  36. package/rules/abie/clean_merged_ignore_branches/template/clean-merged-branch.yml.snippet.yml +0 -9
  37. package/rules/docker/lint_docker_yml/concern.json +0 -12
  38. package/rules/docker/lint_docker_yml/docs/fix-lint_docker_yml.md +0 -31
  39. package/rules/docker/lint_docker_yml/docs/index.md +0 -9
  40. package/rules/docker/lint_docker_yml/fix-lint_docker_yml.mjs +0 -5
  41. package/rules/docker/lint_docker_yml/lint_docker_yml.mdc +0 -14
  42. package/rules/docker/lint_docker_yml/lint_docker_yml.rego +0 -79
  43. package/rules/docker/lint_docker_yml/template/lint-docker.yml.snippet.yml +0 -41
  44. package/rules/js/lint_js_yml/concern.json +0 -12
  45. package/rules/js/lint_js_yml/docs/fix-lint_js_yml.md +0 -28
  46. package/rules/js/lint_js_yml/docs/index.md +0 -9
  47. package/rules/js/lint_js_yml/fix-lint_js_yml.mjs +0 -5
  48. package/rules/js/lint_js_yml/lint_js_yml.mdc +0 -14
  49. package/rules/js/lint_js_yml/lint_js_yml.rego +0 -117
  50. package/rules/js/lint_js_yml/template/lint-js.yml.snippet.yml +0 -40
  51. package/rules/k8s/lint_k8s_yml/concern.json +0 -12
  52. package/rules/k8s/lint_k8s_yml/docs/fix-lint_k8s_yml.md +0 -30
  53. package/rules/k8s/lint_k8s_yml/docs/index.md +0 -9
  54. package/rules/k8s/lint_k8s_yml/fix-lint_k8s_yml.mjs +0 -5
  55. package/rules/k8s/lint_k8s_yml/lint_k8s_yml.rego +0 -57
  56. package/rules/k8s/lint_k8s_yml/template/lint-k8s.yml.snippet.yml +0 -43
  57. package/rules/npm-module/npm_publish_yml/concern.json +0 -13
  58. package/rules/npm-module/npm_publish_yml/docs/fix-npm_publish_yml.md +0 -28
  59. package/rules/npm-module/npm_publish_yml/docs/index.md +0 -9
  60. package/rules/npm-module/npm_publish_yml/fix-npm_publish_yml.mjs +0 -8
  61. package/rules/npm-module/npm_publish_yml/template/npm-publish.yml.snippet.yml +0 -49
  62. package/rules/php/lint_php_yml/concern.json +0 -12
  63. package/rules/php/lint_php_yml/docs/fix-lint_php_yml.md +0 -28
  64. package/rules/php/lint_php_yml/docs/index.md +0 -9
  65. package/rules/php/lint_php_yml/fix-lint_php_yml.mjs +0 -5
  66. package/rules/php/lint_php_yml/lint_php_yml.mdc +0 -23
  67. package/rules/php/lint_php_yml/lint_php_yml.rego +0 -34
  68. package/rules/php/lint_php_yml/template/lint-php.yml.snippet.yml +0 -47
  69. package/rules/python/lint_python_yml/concern.json +0 -12
  70. package/rules/python/lint_python_yml/docs/fix-lint_python_yml.md +0 -27
  71. package/rules/python/lint_python_yml/docs/index.md +0 -9
  72. package/rules/python/lint_python_yml/fix-lint_python_yml.mjs +0 -5
  73. package/rules/python/lint_python_yml/lint_python_yml.mdc +0 -12
  74. package/rules/python/lint_python_yml/lint_python_yml.rego +0 -68
  75. package/rules/python/lint_python_yml/template/lint-python.yml.snippet.yml +0 -41
  76. package/rules/rust/lint_rust_yml/concern.json +0 -14
  77. package/rules/rust/lint_rust_yml/docs/fix-lint_rust_yml.md +0 -28
  78. package/rules/rust/lint_rust_yml/docs/index.md +0 -9
  79. package/rules/rust/lint_rust_yml/fix-lint_rust_yml.mjs +0 -5
  80. package/rules/rust/lint_rust_yml/lint_rust_yml.mdc +0 -17
  81. package/rules/rust/lint_rust_yml/lint_rust_yml.rego +0 -79
  82. package/rules/rust/lint_rust_yml/template/lint-rust.yml.snippet.yml +0 -44
  83. package/rules/rust/toolchain_cache/concern.json +0 -14
  84. package/rules/rust/toolchain_cache/docs/fix-toolchain_cache.md +0 -35
  85. package/rules/rust/toolchain_cache/docs/index.md +0 -12
  86. package/rules/rust/toolchain_cache/docs/main.md +0 -57
  87. package/rules/rust/toolchain_cache/fix-toolchain_cache.mjs +0 -117
  88. package/rules/rust/toolchain_cache/main.mjs +0 -177
  89. package/rules/rust/toolchain_cache/toolchain_cache.mdc +0 -23
  90. package/rules/security/lint_security_yml/concern.json +0 -14
  91. package/rules/security/lint_security_yml/docs/fix-lint_security_yml.md +0 -31
  92. package/rules/security/lint_security_yml/docs/index.md +0 -9
  93. package/rules/security/lint_security_yml/fix-lint_security_yml.mjs +0 -8
  94. package/rules/security/lint_security_yml/lint_security_yml.mdc +0 -7
  95. package/rules/security/lint_security_yml/lint_security_yml.rego +0 -33
  96. package/rules/security/lint_security_yml/template/lint-security.yml.snippet.yml +0 -31
  97. package/rules/style/lint_style_yml/concern.json +0 -12
  98. package/rules/style/lint_style_yml/docs/fix-lint_style_yml.md +0 -31
  99. package/rules/style/lint_style_yml/docs/index.md +0 -9
  100. package/rules/style/lint_style_yml/fix-lint_style_yml.mjs +0 -5
  101. package/rules/style/lint_style_yml/lint_style_yml.mdc +0 -13
  102. package/rules/style/lint_style_yml/lint_style_yml.rego +0 -38
  103. package/rules/style/lint_style_yml/template/lint-style.yml.snippet.yml +0 -39
  104. package/rules/text/lint_text/concern.json +0 -11
  105. package/rules/text/lint_text/docs/fix-lint_text.md +0 -28
  106. package/rules/text/lint_text/docs/index.md +0 -9
  107. package/rules/text/lint_text/fix-lint_text.mjs +0 -5
  108. package/rules/text/lint_text/lint_text.mdc +0 -19
  109. package/rules/text/lint_text/lint_text.rego +0 -104
  110. package/rules/text/lint_text/template/lint-text.yml.snippet.yml +0 -61
@@ -12,7 +12,7 @@ agentclientprotocol.com) — JSON-RPC поверх stdio замість сиро
12
12
  для раннерів `cursor`/`codex`/`claude` у `skills-cli.mjs`.
13
13
 
14
14
  `cursor` запускається через нативний ACP-режим `cursor-agent acp` (зовнішній
15
- бінарник у PATH). `codex`/`claude` — через офіційні бандловані адаптери
15
+ бінарник у PATH). `codex`/`claude` — через офіційні вбудовані адаптери
16
16
  (`@agentclientprotocol/codex-acp`, `@agentclientprotocol/claude-agent-acp`), що
17
17
  самі керують своїм рушієм — зовнішній бінарник `codex`/`claude` у PATH більше
18
18
  не потрібен.
@@ -20,7 +20,7 @@ agentclientprotocol.com) — JSON-RPC поверх stdio замість сиро
20
20
  ## Поведінка
21
21
 
22
22
  - `ACP_AGENT_COMMANDS` — команда запуску агента на провайдер: `cursor` — бінарник
23
- у PATH; `codex`/`claude` — резолвляться з `bin`-запису бандлованого пакета-адаптера.
23
+ у PATH; `codex`/`claude` — резолвляться з `bin`-запису вбудованого пакета-адаптера.
24
24
  - `stopReasonToExitCode` — мапить ACP `StopReason` прогону на exit code: `end_turn` → `0`,
25
25
  усе інше (`max_tokens`, `max_turn_requests`, `refusal`, `cancelled`) → `1`.
26
26
  - `resolveAdapterBin` — резолвить абсолютний шлях до bin-файлу адаптера з його `package.json`.
@@ -29,7 +29,7 @@ agentclientprotocol.com) — JSON-RPC поверх stdio замість сиро
29
29
  режимом — скіл є явною user-invocation, тож дозволи на tool-calls не питаються
30
30
  інтерактивно.
31
31
  - `AcpSkillClient` (internal) — ACP `Client` для скіл-раннера: `requestPermission`
32
- автоапрувляє через `pickAutoPermissionOptionId`, `sessionUpdate` стрімить текстові
32
+ автоматично схвалює через `pickAutoPermissionOptionId`, `sessionUpdate` стрімить текстові
33
33
  дельти (`agent_message_chunk`) у переданий `out`, `readTextFile`/`writeTextFile`
34
34
  реалізовані напряму через `node:fs` (full-trust режим, без write-guard).
35
35
  - `runAcpRunner` — підʼєднується до агента (`initialize` → `newSession` → `prompt`),
@@ -41,14 +41,14 @@ agentclientprotocol.com) — JSON-RPC поверх stdio замість сиро
41
41
  - `ACP_AGENT_COMMANDS` — таблиця команд запуску на провайдер.
42
42
  - `stopReasonToExitCode(stopReason)` — `StopReason` → `0 | 1`.
43
43
  - `resolveAdapterBin(adapterPackage)` — шлях до bin-файлу адаптера.
44
- - `pickAutoPermissionOptionId(options)` — `optionId` автообраного варіанту дозволу.
44
+ - `pickAutoPermissionOptionId(options)` — `optionId` автоматично обраного варіанту дозволу.
45
45
  - `runAcpRunner(kind, prompt, projectDir, logError, deps?)` — виконує скіл через
46
46
  ACP-агента `kind` (`cursor`|`codex`|`claude`); `deps` — інжекти для тестів
47
47
  (`acp`, `spawnFn`, `out`, `resolveAdapterBin`, `isBinaryInPath`).
48
48
 
49
49
  ## Гарантії поведінки
50
50
 
51
- - Permission-реквести від агента автоапрувляться без інтерактивного проміту
51
+ - Permission-запити від агента автоматично схвалюються без інтерактивного запиту
52
52
  парність із колишнім non-interactive `-p`/`exec -` режимом (full user-trust,
53
53
  без write-guard, як і в `@7n/llm-lib/agent-skill`).
54
54
  - `cursor` кидає, якщо `cursor-agent` відсутній у PATH (перевіряється до спавну
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: mirror-parity.mjs
4
4
  resource: npm/scripts/lib/mirror-parity.mjs
5
5
  docgen:
6
- crc: 91a0c9f2
6
+ crc: 1e89b486
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 90
9
9
  ---
@@ -1,8 +1,12 @@
1
1
  /**
2
2
  * Parity дзеркала правил: `.cursor/rules/n-<id>.mdc` має дорівнювати канонічному
3
- * `npm/rules/<id>/<id>.mdc` з inlined-шаблонами — тим самим трансформом, що його
4
- * застосовує синк (`readBundledRuleContent` → `inlineTemplateLinks`). Дрейф виникає,
5
- * коли канонічний `.mdc` змінюють, не регенерувавши дзеркало (беклог адаптації flow #10).
3
+ * `main.mdc` правила з inlined-шаблонами — тим самим трансформом, що його
4
+ * застосовує синк (`readBundledRuleContent` → `inlineTemplateLinks` → mixin-extras).
5
+ * Дрейф виникає, коли канонічний `.mdc` змінюють, не регенерувавши дзеркало.
6
+ *
7
+ * Multi-dir: власник правила шукається у `npm/rules/<id>` і `plugins/<p>/rules/<id>`
8
+ * (перший з `main.mdc`); решта тек `rules/<id>` інших джерел — mixin-extras, їхні
9
+ * concern-mdc доінлайнюються після концернів власника (як у синку).
6
10
  *
7
11
  * Використовується і тестом-гардом (drift === []), і разовою регенерацією.
8
12
  */
@@ -10,41 +14,70 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs'
10
14
  import { dirname, join } from 'node:path'
11
15
 
12
16
  import { appendDiscoveredMdcFiles, inlineTemplateLinks } from './inline-template-links.mjs'
17
+ import { readNRulesConfigLite } from './read-n-rules-config-lite.mjs'
18
+ import { resolvePlugins } from './resolve-plugins.mjs'
13
19
 
14
20
  const MIRROR_PREFIX = 'n-'
15
21
  const MDC_EXT = '.mdc'
16
22
 
17
23
  /**
18
- * Керовані дзеркала `.cursor/rules/n-<id>.mdc`, що мають канонічне джерело
19
- * `npm/rules/<id>/<id>.mdc`. Дзеркала без канону (зовнішні) пропускаються.
24
+ * Rules-джерела репо у порядку пріоритету: ядро, потім АКТИВНІ плагіни (з `.n-rules.json`,
25
+ * резолв через node_modules той самий шлях, що у синку; неактивні plugins/* монорепо
26
+ * не враховуються, бо їх нема і в дзеркалах).
27
+ * @param {string} repoRoot корінь репо
28
+ * @returns {Promise<string[]>} абсолютні шляхи rules-каталогів
29
+ */
30
+ async function repoRulesDirs(repoRoot) {
31
+ const dirs = [join(repoRoot, 'npm/rules')]
32
+ const config = await readNRulesConfigLite(repoRoot)
33
+ for (const plugin of resolvePlugins(repoRoot, { plugins: config.plugins }, { allowInstall: false, quiet: true })) {
34
+ dirs.push(plugin.rulesDir)
35
+ }
36
+ return dirs
37
+ }
38
+
39
+ /**
40
+ * Керовані дзеркала `.cursor/rules/n-<id>.mdc` з канонічним джерелом у ядрі або плагіні.
41
+ * Дзеркала без канону (зовнішні) пропускаються.
20
42
  * @param {string} repoRoot корінь репо
21
- * @returns {{ id: string, mirrorPath: string, canonicalPath: string }[]} список
43
+ * @returns {Promise<{ id: string, mirrorPath: string, canonicalPath: string, extraDirs: string[] }[]>} список
22
44
  */
23
- export function listManagedMirrors(repoRoot) {
45
+ export async function listManagedMirrors(repoRoot) {
24
46
  const rulesDir = join(repoRoot, '.cursor/rules')
25
47
  if (!existsSync(rulesDir)) return []
48
+ const sources = await repoRulesDirs(repoRoot)
26
49
  return readdirSync(rulesDir)
27
50
  .filter(f => f.startsWith(MIRROR_PREFIX) && f.endsWith(MDC_EXT))
28
51
  .map(f => {
29
52
  const id = f.slice(MIRROR_PREFIX.length, -MDC_EXT.length)
53
+ const candidates = sources.map(s => join(s, id))
54
+ const owner = candidates.find(dir => existsSync(join(dir, `main${MDC_EXT}`)))
55
+ const extraDirs = owner === undefined ? [] : candidates.filter(dir => dir !== owner && existsSync(dir))
30
56
  return {
31
57
  id,
32
58
  mirrorPath: join(rulesDir, f),
33
- canonicalPath: join(repoRoot, 'npm/rules', id, `main${MDC_EXT}`)
59
+ canonicalPath: owner === undefined ? '' : join(owner, `main${MDC_EXT}`),
60
+ extraDirs
34
61
  }
35
62
  })
36
- .filter(m => existsSync(m.canonicalPath))
63
+ .filter(m => m.canonicalPath !== '')
37
64
  }
38
65
 
39
66
  /**
40
- * Очікуваний вміст дзеркала = канон з inlined-шаблонами (трансформ синку).
41
- * @param {string} canonicalPath абсолютний шлях `npm/rules/<id>/<id>.mdc`
67
+ * Очікуваний вміст дзеркала = канон з inlined-шаблонами + concern-mdc mixin-джерел
68
+ * (той самий трансформ, що у синку).
69
+ * @param {string} canonicalPath абсолютний шлях `rules/<id>/main.mdc` власника
70
+ * @param {string[]} [extraDirs] теки `rules/<id>` mixin-джерел
42
71
  * @returns {Promise<string>} очікуваний текст дзеркала
43
72
  */
44
- export async function expectedMirrorContent(canonicalPath) {
73
+ export async function expectedMirrorContent(canonicalPath, extraDirs = []) {
45
74
  const dir = dirname(canonicalPath)
46
- const withTemplates = await inlineTemplateLinks(readFileSync(canonicalPath, 'utf8'), dir)
47
- return appendDiscoveredMdcFiles(withTemplates, dir)
75
+ let out = await inlineTemplateLinks(readFileSync(canonicalPath, 'utf8'), dir)
76
+ out = await appendDiscoveredMdcFiles(out, dir)
77
+ for (const extra of extraDirs) {
78
+ out = await appendDiscoveredMdcFiles(out, extra)
79
+ }
80
+ return out
48
81
  }
49
82
 
50
83
  /**
@@ -54,8 +87,8 @@ export async function expectedMirrorContent(canonicalPath) {
54
87
  */
55
88
  export async function findMirrorDrift(repoRoot) {
56
89
  const drift = []
57
- for (const m of listManagedMirrors(repoRoot)) {
58
- const expected = await expectedMirrorContent(m.canonicalPath)
90
+ for (const m of await listManagedMirrors(repoRoot)) {
91
+ const expected = await expectedMirrorContent(m.canonicalPath, m.extraDirs)
59
92
  if (readFileSync(m.mirrorPath, 'utf8') !== expected) drift.push(m.id)
60
93
  }
61
94
  return drift.toSorted()
@@ -4,7 +4,9 @@
4
4
  * Скіли читаються з `npm/skills/<id>/SKILL.md` установленого пакета (або кешу `npx`).
5
5
  * Промпт збирає інструкцію скілу + контекст поточного CWD (`package.json`, `tsconfig.json`,
6
6
  * `.n-rules.json`) — далі stdout або виконання через один з раннерів: вбудований
7
- * pi-агент, чи зовнішній ACP-агент (`cursor-agent acp`, `codex-acp`, або deprecated `claude`).
7
+ * pi-агент, чи зовнішній ACP-агент. `cursor`/`codex` через `@7n/llm-lib/acp`
8
+ * (napi-міст до `llm_cascade::acp`, без власного JSON-RPC у JS); deprecated
9
+ * `claude` — окремий JS-шим (`./lib/acp-runner.mjs`), бо Rust-крейт його не моделює.
8
10
  *
9
11
  * Підтримувані формати:
10
12
  * `npx \@7n/rules skill list`
@@ -12,14 +14,14 @@
12
14
  * `npx \@7n/rules skill pi taze` — виконати через вбудований pi-агент (рекомендовано)
13
15
  * `npx \@7n/rules skill pi taze "онови залежності"`
14
16
  * `npx \@7n/rules skill cursor taze` — Cursor CLI через ACP (`cursor-agent acp`)
15
- * `npx \@7n/rules skill codex taze` — Codex через ACP-адаптер (`@agentclientprotocol/codex-acp`)
17
+ * `npx \@7n/rules skill codex taze` — Codex через ACP (napi-міст `@7n/llm-lib/acp`, Rust спавнить `npx \@agentclientprotocol/codex-acp`)
16
18
  * `npx \@7n/rules skill claude taze` — deprecated: Claude Code через ACP-адаптер
17
19
  */
18
20
 
19
- import { spawnSync } from 'node:child_process'
21
+ import { runAcpAgent } from '@7n/llm-lib/acp'
20
22
  import { existsSync, readFileSync, readdirSync } from 'node:fs'
21
23
  import { dirname, join } from 'node:path'
22
- import { cwd } from 'node:process'
24
+ import { cwd, stdout } from 'node:process'
23
25
  import { fileURLToPath } from 'node:url'
24
26
 
25
27
  import { runAcpRunner } from './lib/acp-runner.mjs'
@@ -28,7 +30,11 @@ import { readSkillMetaRaw, skillTier } from './lib/skill-meta.mjs'
28
30
  /** Виконавці скіла. `pi` — вбудований (рекомендований); `cursor`/`codex`/`claude` — зовнішні ACP-агенти (`claude` — deprecated). */
29
31
  const RUNNERS = new Set(['pi', 'cursor', 'codex', 'claude'])
30
32
 
31
- /** Раннери, що йдуть через зовнішнього ACP-агента (`runAcpRunner`), і чи deprecated (друкує попередження перед запуском). */
33
+ /**
34
+ * Раннери, що йдуть через зовнішнього ACP-агента, і чи deprecated (друкує попередження
35
+ * перед запуском). `cursor`/`codex` — napi-міст `@7n/llm-lib/acp`; `claude` — окремий
36
+ * JS-шим `runAcpRunner` (Rust-крейт `llm_cascade::acp` `claude` не моделює).
37
+ */
32
38
  const ACP_RUNNERS = {
33
39
  claude: { deprecated: true },
34
40
  cursor: { deprecated: false },
@@ -47,17 +53,6 @@ const USAGE_LINES = [
47
53
  'Skill id: каталог у пакеті (lint, taze, …) або з префіксом n- (n-lint → lint).'
48
54
  ]
49
55
 
50
- /**
51
- * @param {string} name ім'я бінарника
52
- * @returns {boolean} чи знайдено бінарник у PATH
53
- */
54
- function isBinaryInPath(name) {
55
- // Явний `env: process.env` — Bun без нього дає дітям snapshot оточення зі старту процесу
56
- // і не бачить runtime-змін `process.env.PATH` (та сама причина, що в resolve-cmd.mjs).
57
- const probe = spawnSync('command', ['-v', name], { shell: true, encoding: 'utf8', env: process.env })
58
- return probe.status === 0
59
- }
60
-
61
56
  /**
62
57
  * @param {string} name ім'я скілу з CLI або каталогу `.cursor/skills/n-*`
63
58
  * @returns {string} id каталогу в `npm/skills/<id>/`
@@ -170,27 +165,41 @@ async function runPiRunner(prompt, rawSkillName, skillsRoot, projectDir, logErro
170
165
  }
171
166
 
172
167
  /**
173
- * Делегує виконання скіла зовнішньому ACP-агенту (`cursor-agent acp`, `codex-acp`,
174
- * або deprecated `claude`-адаптер) через `runAcpRunner` — JSON-RPC поверх stdio,
175
- * дозволи на tool-calls автоапрувляться (без інтерактивних питань, паритет із
176
- * колишнім non-interactive `-p`). `claude` лишається як deprecated fallback для
177
- * тих, у кого pi-модель ще не налаштована; буде прибрано (мігруй на `skill pi`).
168
+ * Делегує виконання скіла зовнішньому ACP-агенту. `cursor`/`codex` через
169
+ * `@7n/llm-lib/acp` (napi-міст до `llm_cascade::acp`: спавн, `session/prompt`,
170
+ * автоапрув дозволів усе в Rust, без JSON-RPC у JS). `claude` — deprecated
171
+ * JS-шим `runAcpRunner` (Rust його не моделює); буде прибрано (мігруй на `skill pi`).
172
+ * На відміну від колишнього стрімінгу по чанках, napi-шлях повертає повний текст
173
+ * лише по завершенню ходу — Rust-функція не стрімить проміжні токени в JS.
178
174
  * @param {'claude' | 'cursor' | 'codex'} kind якого ACP-агента запускати
179
175
  * @param {string} prompt промпт скіла
180
176
  * @param {string} projectDir робочий каталог агента
181
177
  * @param {(line: string) => void} logError вивід попередження/помилок
182
- * @param {{ runAcpRunner?: typeof runAcpRunner }} [deps] інжект для тестів
183
- * @returns {Promise<number>} exit code (0 — `end_turn`, 1 — інакше)
178
+ * @param {{ runAcpRunner?: typeof runAcpRunner, runAcpAgent?: typeof runAcpAgent, out?: (chunk: string) => void }} [deps] інжекти для тестів
179
+ * @returns {Promise<number>} exit code (0 — успіх, 1 — інакше)
184
180
  */
185
- function runLlmCli(kind, prompt, projectDir, logError, deps = {}) {
181
+ async function runLlmCli(kind, prompt, projectDir, logError, deps = {}) {
186
182
  const runner = ACP_RUNNERS[kind]
187
- const runAcp = deps.runAcpRunner ?? runAcpRunner
188
183
 
189
184
  if (runner.deprecated) {
190
185
  logError(`[deprecated] skill ${kind} → use 'skill pi'; зовнішній ACP-агент буде прибрано`)
191
186
  }
192
187
 
193
- return runAcp(kind, prompt, projectDir, logError, { isBinaryInPath })
188
+ if (kind === 'claude') {
189
+ const runAcp = deps.runAcpRunner ?? runAcpRunner
190
+ return runAcp(kind, prompt, projectDir, logError)
191
+ }
192
+
193
+ const runNativeAcp = deps.runAcpAgent ?? runAcpAgent
194
+ const out = deps.out ?? (chunk => stdout.write(chunk))
195
+
196
+ try {
197
+ out(await runNativeAcp(kind, prompt, projectDir))
198
+ return 0
199
+ } catch (error) {
200
+ logError(error instanceof Error ? error.message : String(error))
201
+ return 1
202
+ }
194
203
  }
195
204
 
196
205
  /**
@@ -1,19 +0,0 @@
1
- ## Rego: `ignore_branches` у `clean-merged-branch.yml`
2
-
3
- Rego-пакет: `abie.clean_merged_ignore_branches`
4
-
5
- Цільовий файл: `.github/workflows/clean-merged-branch.yml`.
6
-
7
- Перевіряє, що у workflow-файлі є крок з `uses: phpdocker-io/github-actions-delete-abandoned-branches` і що його `with.ignore_branches` містить обидва токени `dev` та `ua` (case-insensitive, пробіли навколо токенів обрізаються). Токени зчитуються з template-файлу `data.template.snippet` — зміна канону автоматично змінює вимоги.
8
-
9
- Канонічний snippet: [clean-merged-branch.yml.snippet.yml](./template/clean-merged-branch.yml.snippet.yml)
10
-
11
- ```yaml title=".github/workflows/clean-merged-branch.yml (фрагмент)"
12
- - uses: phpdocker-io/github-actions-delete-abandoned-branches@v2
13
- with:
14
- ignore_branches: "dev,ua,main" # ✓ містить dev та ua
15
- ```
16
-
17
- ```yaml
18
- ignore_branches: "main" # ✗ не вистачає dev,ua
19
- ```
@@ -1,81 +0,0 @@
1
- # Перевірка `clean-merged-branch.yml` для abie-проєктів (abie.mdc).
2
- #
3
- # Канон надходить через --data: { "template": { "snippet": ... } }
4
- # Структура --data сформована з template/clean-merged-branch.yml.snippet.yml.
5
- # Action-маркер (`uses:` substring) і required branches (parsed з template's
6
- # `ignore_branches`) читаються з template. ga.clean_merged_branch перевіряє
7
- # повний канон workflow окремо; цей пакет — лише abie-specific шар.
8
- package abie.clean_merged_ignore_branches
9
-
10
- import rego.v1
11
-
12
- # Експектації з template's step (першого з steps).
13
- expected_step := step if some step in data.template.snippet.jobs.cleanup_old_branches.steps
14
-
15
- target_action_marker := expected_step.uses
16
-
17
- required_branches := parsed_ignore_tokens(expected_step.with.ignore_branches)
18
-
19
- step_missing_msg := sprintf(
20
- "clean-merged-branch.yml: не знайдено крок з uses: %s (abie.mdc)",
21
- [target_action_marker],
22
- )
23
-
24
- ignore_branches_missing_msg := sprintf(
25
- "clean-merged-branch.yml: не знайдено with.ignore_branches у кроці %s (abie.mdc)",
26
- [target_action_marker],
27
- )
28
-
29
- # ── deny ─────────────────────────────────────────────────────────────────
30
-
31
- deny contains step_missing_msg if {
32
- count(target_steps) == 0
33
- }
34
-
35
- deny contains ignore_branches_missing_msg if {
36
- count(target_steps) > 0
37
- not has_ignore_branches_value
38
- }
39
-
40
- deny contains msg if {
41
- count(target_steps) > 0
42
- ignore_branches_value != ""
43
- missing := required_branches - parsed_ignore_tokens(ignore_branches_value)
44
- count(missing) > 0
45
- msg := sprintf(
46
- "clean-merged-branch.yml: ignore_branches має містити %v (зараз: %q; не вистачає: %v) (abie.mdc)",
47
- [concat(",", sort(required_branches)), ignore_branches_value, concat(",", sort(missing))],
48
- )
49
- }
50
-
51
- # ── helpers ──────────────────────────────────────────────────────────────
52
-
53
- target_steps contains step if {
54
- some job in object.get(input, "jobs", {})
55
- some step in object.get(job, "steps", [])
56
- uses := object.get(step, "uses", "")
57
- is_string(uses)
58
- contains(uses, target_action_marker)
59
- }
60
-
61
- has_ignore_branches_value if {
62
- some step in target_steps
63
- v := object.get(object.get(step, "with", {}), "ignore_branches", null)
64
- is_string(v)
65
- }
66
-
67
- default ignore_branches_value := ""
68
-
69
- ignore_branches_value := values[0] if {
70
- values := [v |
71
- some step in target_steps
72
- v := object.get(object.get(step, "with", {}), "ignore_branches", null)
73
- is_string(v)
74
- ]
75
- count(values) > 0
76
- }
77
-
78
- parsed_ignore_tokens(value) := {lower(trim_space(part)) |
79
- some part in split(value, ",")
80
- trim_space(part) != ""
81
- }
@@ -1,11 +0,0 @@
1
- {
2
- "$schema": "https://unpkg.com/@7n/rules/schemas/concern.json",
3
- "policy": {
4
- "files": {
5
- "single": ".github/workflows/clean-merged-branch.yml"
6
- }
7
- },
8
- "requires": {
9
- "capability": "ci:github"
10
- }
11
- }
@@ -1,31 +0,0 @@
1
- ---
2
- type: JS Module
3
- title: fix-clean_merged_ignore_branches.mjs
4
- resource: npm/rules/abie/clean_merged_ignore_branches/fix-clean_merged_ignore_branches.mjs
5
- docgen:
6
- crc: 9fe2cc60
7
- model: openai-codex/gpt-5.5
8
- tier: cloud-avg
9
- score: 100
10
- issues: judge:inaccurate:0.99
11
- judgeModel: openai-codex/gpt-5.4-mini
12
- ---
13
-
14
- ## Огляд
15
-
16
- `patterns` визначає набір шляхів репозиторію, які правило має враховувати під час перевірки. Воно свідомо не охоплює `.git` і `.github`, щоб не аналізувати службові дані Git та конфігурацію GitHub. Правило працює read-only: лише повідомляє про невідповідність очікуваній структурі, не змінюючи файли чи зовнішні сховища.
17
-
18
- ## Поведінка
19
-
20
- 1. `patterns` визначає правило виправлення, яке забезпечує наявність workflow для очищення merged branches.
21
-
22
- 2. Правило орієнтується на шаблонний стан файлу `.github/workflows/clean-merged-branch.yml`, щоб уніфікувати поведінку репозиторіїв щодо прибирання вже злитих гілок.
23
-
24
- 3. Під час перевірки свідомо не обходить службові шляхи `.github` і `.git`, окрім цільового workflow-файлу, який є предметом виправлення.
25
-
26
- 4. `patterns` лише описує очікуване виправлення й не виконує самостійного запису у файлову систему чи інші сховища.
27
-
28
- ## Гарантії поведінки
29
-
30
- - Read-only: не виконує операцій запису (ФС/БД).
31
- - Свідомо пропускає шляхи: `.github`, `.git`.
@@ -1,9 +0,0 @@
1
- ---
2
- type: Directory Index
3
- title: npm/rules/abie/clean_merged_ignore_branches
4
- resource: npm/rules/abie/clean_merged_ignore_branches/
5
- ---
6
-
7
- | Файл | Тип |
8
- | --------------------------------------------------------------------------- | --------- |
9
- | [fix-clean_merged_ignore_branches.mjs](fix-clean_merged_ignore_branches.md) | JS Module |
@@ -1,8 +0,0 @@
1
- import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
-
3
- export const patterns = [
4
- createTemplateFixPattern({
5
- id: 'abie-clean_merged_ignore_branches-template',
6
- targetPath: '.github/workflows/clean-merged-branch.yml'
7
- })
8
- ]
@@ -1,9 +0,0 @@
1
- # abie-specific шар поверх ga.clean_merged_branch:
2
- # у `clean-merged-branch.yml` крок phpdocker-io/github-actions-delete-abandoned-branches
3
- # має містити `ignore_branches: dev,ua` (обовʼязкові гілки для abie-проєктів).
4
- jobs:
5
- cleanup_old_branches:
6
- steps:
7
- - uses: phpdocker-io/github-actions-delete-abandoned-branches
8
- with:
9
- ignore_branches: dev,ua
@@ -1,12 +0,0 @@
1
- {
2
- "$schema": "https://unpkg.com/@7n/rules/schemas/concern.json",
3
- "fixability": "config",
4
- "policy": {
5
- "files": {
6
- "single": ".github/workflows/lint-docker.yml"
7
- }
8
- },
9
- "requires": {
10
- "capability": "ci:github"
11
- }
12
- }
@@ -1,31 +0,0 @@
1
- ---
2
- type: JS Module
3
- title: fix-lint_docker_yml.mjs
4
- resource: npm/rules/docker/lint_docker_yml/fix-lint_docker_yml.mjs
5
- docgen:
6
- crc: 551913c9
7
- model: openai-codex/gpt-5.5
8
- tier: cloud-avg
9
- score: 100
10
- issues: judge:inaccurate:0.98
11
- judgeModel: openai-codex/gpt-5.4-mini
12
- ---
13
-
14
- ## Огляд
15
-
16
- `patterns` визначає поведінку правила для обходу проєкту без змін у файловій системі чи зовнішніх сховищах. Службові директорії `.github` і `.git` свідомо не входять до перевірюваних шляхів, щоб правило працювало лише з релевантним кодом проєкту.
17
-
18
- ## Поведінка
19
-
20
- 1. `patterns` визначає правило автоматичного виправлення для workflow перевірки Docker.
21
-
22
- 2. `patterns` забезпечує наявність стандартного файлу `.github/workflows/lint-docker.yml`, щоб проєкт мав єдиний CI-контроль Docker-конфігурації.
23
-
24
- 3. `patterns` працює як read-only опис виправлення: сам модуль не змінює файлову систему чи зовнішні сховища.
25
-
26
- 4. Під час супровідного обходу шляхів свідомо не аналізуються `.github` і `.git`, щоб не змішувати службові директорії з основним кодом проєкту.
27
-
28
- ## Гарантії поведінки
29
-
30
- - Read-only: не виконує операцій запису (ФС/БД).
31
- - Свідомо пропускає шляхи: `.github`, `.git`.
@@ -1,9 +0,0 @@
1
- ---
2
- type: Directory Index
3
- title: npm/rules/docker/lint_docker_yml
4
- resource: npm/rules/docker/lint_docker_yml/
5
- ---
6
-
7
- | Файл | Тип |
8
- | ------------------------------------------------- | --------- |
9
- | [fix-lint_docker_yml.mjs](fix-lint_docker_yml.md) | JS Module |
@@ -1,5 +0,0 @@
1
- import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
-
3
- export const patterns = [
4
- createTemplateFixPattern({ id: 'docker-lint_docker_yml-template', targetPath: '.github/workflows/lint-docker.yml' })
5
- ]
@@ -1,14 +0,0 @@
1
- ## Перевірка `.github/workflows/lint-docker.yml`
2
-
3
- Rego-пакет: `docker.lint_docker_yml`
4
-
5
- Цільовий файл: `.github/workflows/lint-docker.yml`
6
-
7
- Перевіряє, що CI-workflow lint-docker відповідає канонічному сніпету:
8
-
9
- - `on.push.paths` містить усі три glob-маски: `**/Dockerfile`, `**/*.Dockerfile`, `**/*.dockerfile`
10
- - присутні кроки `uses: actions/checkout@v6` та `uses: ./.github/actions/setup-bun-deps`
11
- - крок install hadolint містить рядок із версією `v2.12.0` (substring-перевірка)
12
- - крок lint містить `n-rules lint docker --no-fix`
13
-
14
- Канонічний шаблон: [lint-docker.yml.snippet.yml](./template/lint-docker.yml.snippet.yml)
@@ -1,79 +0,0 @@
1
- # Перевірка `.github/workflows/lint-docker.yml` (docker.mdc).
2
- #
3
- # Канон надходить через --data: { "template": { "snippet": ... } }
4
- # Структура --data сформована з template/lint-docker.yml.snippet.yml.
5
- # Path-маркери, hadolint версія (substring "v2.12.0" у run), setup-bun-deps
6
- # composite, `n-rules lint docker --no-fix` substring — все читається з template's snippet.
7
- # Універсальні workflow-перевірки — у `ga.workflow_common`.
8
- package docker.lint_docker_yml
9
-
10
- import rego.v1
11
-
12
- # Очікувані літерали з template.
13
- expected_paths := {p | some p in data.template.snippet.on.push.paths}
14
-
15
- # Required uses set — з template's steps.
16
- expected_uses_set contains u if {
17
- some step in data.template.snippet.jobs["lint-docker"].steps
18
- u := object.get(step, "uses", "")
19
- u != ""
20
- }
21
-
22
- # Required run substrings — collected per-step з template.
23
- expected_run_substrings contains r if {
24
- some step in data.template.snippet.jobs["lint-docker"].steps
25
- r := object.get(step, "run", "")
26
- r != ""
27
- }
28
-
29
- # Аліаси на input.
30
- all_step_uses contains u if {
31
- some job in object.get(input, "jobs", {})
32
- some step in object.get(job, "steps", [])
33
- u := object.get(step, "uses", "")
34
- u != ""
35
- }
36
-
37
- all_run_text := concat("\n", [run_text |
38
- some job in object.get(input, "jobs", {})
39
- some step in object.get(job, "steps", [])
40
- run_text := step_run_to_text(step)
41
- ])
42
-
43
- # conftest парсить YAML 1.1, тож канонічний `on:` без лапок стає булевим ключем
44
- # `true` (як у `ga.lint_ga`). Тому читаємо через `input["true"]`.
45
- push_paths_set := {p |
46
- some p in object.get(object.get(object.get(input, "true", {}), "push", {}), "paths", [])
47
- }
48
-
49
- # ── deny: on.push.paths subset-of ──────────────────────────────────────
50
-
51
- deny contains msg if {
52
- some required in expected_paths
53
- not required in push_paths_set
54
- msg := sprintf("lint-docker.yml: on.push.paths має містити %q (docker.mdc)", [required])
55
- }
56
-
57
- # ── deny: required uses present ────────────────────────────────────────
58
-
59
- deny contains msg if {
60
- some required_use in expected_uses_set
61
- not required_use in all_step_uses
62
- msg := sprintf("lint-docker.yml: відсутній крок `uses: %s` (docker.mdc)", [required_use])
63
- }
64
-
65
- # ── deny: required run substrings ──────────────────────────────────────
66
-
67
- deny contains msg if {
68
- some required_run in expected_run_substrings
69
- not contains(all_run_text, required_run)
70
- msg := sprintf("lint-docker.yml: жоден крок run не містить %q (docker.mdc)", [required_run])
71
- }
72
-
73
- # ── helpers ────────────────────────────────────────────────────────────
74
-
75
- step_run_to_text(step) := step.run if is_string(step.run)
76
-
77
- else := concat("\n", [s | some s in step.run]) if is_array(step.run)
78
-
79
- else := ""