@7n/rules 1.0.0 → 1.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.2.0] - 2026-07-14
4
+
5
+ ### Added
6
+
7
+ - `skill`-CLI: додано зовнішній Codex-раннер (`skill codex <id>` → `codex exec -`); Cursor-раннер (`skill cursor <id>`) більше не deprecated — обидва повноцінні альтернативи вбудованому `pi`. `n-taze` доповнено Rust/`cargo-edit`-гілкою (детекція `Cargo.toml`, `cargo upgrade --incompatible allow`, `cargo fmt`/`clippy`/`test`) поряд з існуючою npm/bun-гілкою.
8
+ - `n-rules release`: додано `package.json#release.maxBump` — стеля на bump цього workspace (`major|minor|patch`); change-файл із `bump: major` понад стелею обрізається й друкує попередження, замість підняти major-версію. `@7n/rules` тепер зафіксований на `maxBump: minor` — власна major-версія пакета більше не змінюється автоматично через реліз-CI.
9
+
10
+ ### Removed
11
+
12
+ - Команду `mt-run-node` (B2 executor-міст) видалено: fix-вузли MT-графа виконує вбудований шлях MT — підписочні CLI `claude|codex|cursor|pi` з user-level ENV-конфігом (`MT_AGENT_CLI`, `MT_CLOUD_AGENT_CLIS`, `MT_AGENT_CLI_MODEL_MAP`; локальні omlx — через pi.dev CLI), за mt ADR `260713-2110` «ACP — єдиний транспорт AI-викликів». `mt-tail` більше не конфігурує `node_executor` у `.mt.json`; тир-канон вузлів — `MIN|AVG|MAX`
13
+
14
+ ## [1.1.0] - 2026-07-13
15
+
16
+ ### Changed
17
+
18
+ - Пакет перейменовано: `@nitra/cursor` → `@7n/rules` (bin `n-rules` + перехідний alias `n-cursor`, конфіг `.n-rules.json` з авто-міграцією з `.n-cursor.json`, маркери блоків `n-rules:*`, `$schema` на `unpkg.com/@7n/rules/...`); legacy-назви читаються як fallback для безшовної міграції споживачів
19
+
3
20
  ## [14.33.0] - 2026-07-13
4
21
 
5
22
  ### Changed
package/bin/n-rules.js CHANGED
@@ -1577,9 +1577,7 @@ try {
1577
1577
  if (ROOT_GUARDED_COMMANDS.has(command)) {
1578
1578
  assertCwdIsProjectRoot(effectiveRoot, describeRootGuardedAction(command))
1579
1579
  }
1580
- // mt-run-node — MT-екзекутор у worktree вузла: жодних side-effect-ів поза фіксом
1581
- // (devDeps-ensure мутив би package.json чужого worktree).
1582
- if (command !== 'mt-run-node') await ensureNRulesInRootDevDependencies(effectiveRoot)
1580
+ await ensureNRulesInRootDevDependencies(effectiveRoot)
1583
1581
  // Підкоманди-оркестратори (hook/lint/skill/adr-normalize-local/taze/release тощо)
1584
1582
  // можуть спавнити внутрішню agent/LLM-сесію — ADR Stop-hooks (capture/normalize)
1585
1583
  // мають пропустити її як технічний шум, не людську думку (spec 2026-06-30).
@@ -1665,14 +1663,6 @@ try {
1665
1663
 
1666
1664
  break
1667
1665
  }
1668
- case 'mt-run-node': {
1669
- // B2 executor-міст (Фаза B): MT node_executor спавнить це для fix-вузла.
1670
- // Виконує вузол нашим pi-harness (тир-канон); stdout={applied,touchedFiles}.
1671
- const { runNodeCli } = await import('../scripts/lib/lint-surface/mt-run-node.mjs')
1672
- process.exitCode = await runNodeCli(args)
1673
-
1674
- break
1675
- }
1676
1666
  case undefined:
1677
1667
  case '': {
1678
1668
  await runSync()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "CLI еталонних правил і skills (префікс n-): синк у репозиторій, дельта-lint, конформність",
5
5
  "keywords": [
6
6
  "cli",
@@ -20,6 +20,9 @@
20
20
  "type": "git",
21
21
  "url": "git+https://github.com/nitra/7n-rules.git"
22
22
  },
23
+ "release": {
24
+ "maxBump": "minor"
25
+ },
23
26
  "bin": {
24
27
  "n-rules": "bin/n-rules.js",
25
28
  "n-cursor": "bin/n-rules.js"
@@ -23,9 +23,24 @@ import { getMonorepoPackageRootDirs, isIgnoredWorkspaceRoot } from '../../../scr
23
23
  * @property {string | null} version semver-рядок
24
24
  * @property {boolean} registryPublishable чи застосовується режим порівняння з реєстром
25
25
  * @property {string[] | null} [npmFiles] лише npm: `files` з package.json
26
+ * @property {'major' | 'minor' | 'patch' | null} maxBump стеля для `n-rules release` (з `package.json#release.maxBump`); `null` — без обмеження
26
27
  */
27
28
 
28
29
  const PYPROJECT_GLOB_IGNORE = ['**/node_modules/**', '**/.git/**', '**/.venv/**', '**/venv/**']
30
+ const VALID_MAX_BUMPS = ['major', 'minor', 'patch']
31
+
32
+ /**
33
+ * @param {Record<string, unknown>} pkg розпарсений package.json
34
+ * @returns {'major' | 'minor' | 'patch' | null} стеля бампа з `release.maxBump`, якщо валідна
35
+ */
36
+ function maxBumpFromPackageJson(pkg) {
37
+ const release = pkg.release
38
+ if (!release || typeof release !== 'object' || Array.isArray(release)) return null
39
+ const value = /** @type {Record<string, unknown>} */ (release).maxBump
40
+ return typeof value === 'string' && VALID_MAX_BUMPS.includes(value)
41
+ ? /** @type {'major' | 'minor' | 'patch'} */ (value)
42
+ : null
43
+ }
29
44
 
30
45
  /**
31
46
  * @param {unknown} doc розпарсений pyproject.toml
@@ -93,7 +108,8 @@ export async function readPackageManifest(ws, cwd = process.cwd()) {
93
108
  name: typeof pkg.name === 'string' ? pkg.name : null,
94
109
  version: typeof pkg.version === 'string' ? pkg.version : null,
95
110
  registryPublishable,
96
- npmFiles: Array.isArray(pkg.files) ? pkg.files : null
111
+ npmFiles: Array.isArray(pkg.files) ? pkg.files : null,
112
+ maxBump: maxBumpFromPackageJson(pkg)
97
113
  }
98
114
  } catch {
99
115
  return null
@@ -113,7 +129,8 @@ export async function readPackageManifest(ws, cwd = process.cwd()) {
113
129
  name: fields.name,
114
130
  version: fields.version,
115
131
  registryPublishable,
116
- npmFiles: null
132
+ npmFiles: null,
133
+ maxBump: null
117
134
  }
118
135
  }
119
136
 
@@ -30,6 +30,20 @@ export function maxBump(bumps) {
30
30
  return VALID_BUMPS.find(level => bumps.includes(level)) ?? 'patch'
31
31
  }
32
32
 
33
+ /**
34
+ * Обмежує bump зверху стелею (`package.json#release.maxBump`) — наприклад, не дає
35
+ * `major`-change-файлу підняти major-версію пакета, навіть якщо його явно поставили.
36
+ * @param {string} bump обчислений bump (`major|minor|patch`)
37
+ * @param {string | null} [cap] стеля (`major|minor|patch`) або `null` — без обмеження
38
+ * @returns {string} bump не суворіший за `cap`
39
+ */
40
+ export function capBump(bump, cap) {
41
+ if (!cap) return bump
42
+ const bumpRank = VALID_BUMPS.indexOf(bump)
43
+ const capRank = VALID_BUMPS.indexOf(cap)
44
+ return bumpRank < capRank ? cap : bump
45
+ }
46
+
33
47
  /**
34
48
  * @param {string} version нова версія
35
49
  * @param {string} date `YYYY-MM-DD`
@@ -68,11 +82,13 @@ export function prependChangelogSection(existingText, sectionBlock) {
68
82
  * @param {string} params.currentVersion поточна version маніфесту
69
83
  * @param {Array<{ file: string, entry: { bump: string, section: string, description: string } }>} params.changeFiles change-файли workspace
70
84
  * @param {string} params.date `YYYY-MM-DD`
85
+ * @param {string | null} [params.maxBumpCap] стеля bump з `package.json#release.maxBump`; `null` — без обмеження
71
86
  * @returns {{ newVersion: string, sectionBlock: string, consumedFiles: string[] } | null} результат або null, якщо змін нема
72
87
  */
73
- export function aggregateWorkspace({ currentVersion, changeFiles, date }) {
88
+ export function aggregateWorkspace({ currentVersion, changeFiles, date, maxBumpCap = null }) {
74
89
  if (changeFiles.length === 0) return null
75
- const newVersion = bumpVersion(currentVersion, maxBump(changeFiles.map(c => c.entry.bump)))
90
+ const bump = capBump(maxBump(changeFiles.map(c => c.entry.bump)), maxBumpCap)
91
+ const newVersion = bumpVersion(currentVersion, bump)
76
92
  const sectionBlock = renderChangelogSection(
77
93
  newVersion,
78
94
  date,
@@ -9,7 +9,7 @@ import { join } from 'node:path'
9
9
  import { env } from 'node:process'
10
10
 
11
11
  import { getMonorepoProjectRootDirs, readPackageManifest } from '../changelog/lib/package-manifest.mjs'
12
- import { aggregateWorkspace, prependChangelogSection } from './lib/aggregate.mjs'
12
+ import { aggregateWorkspace, capBump, maxBump, prependChangelogSection } from './lib/aggregate.mjs'
13
13
  import { CHANGES_DIR, readChangeFiles } from './lib/change-file.mjs'
14
14
  import { defaultRunGit, synthesizeChangeFromCommits } from './lib/fallback.mjs'
15
15
 
@@ -120,8 +120,14 @@ async function processReleaseWorkspace(ws, cwd, date, runGit) {
120
120
  if (!manifest || !manifest.version) return null
121
121
 
122
122
  const changeFiles = await collectChangeFiles(cwd, manifest, runGit)
123
- const agg = aggregateWorkspace({ currentVersion: manifest.version, changeFiles, date })
123
+ const requestedBump = changeFiles.length > 0 ? maxBump(changeFiles.map(c => c.entry.bump)) : null
124
+ const agg = aggregateWorkspace({ currentVersion: manifest.version, changeFiles, date, maxBumpCap: manifest.maxBump })
124
125
  if (!agg) return null
126
+ if (requestedBump && capBump(requestedBump, manifest.maxBump) !== requestedBump) {
127
+ console.warn(
128
+ `⚠️ ${manifest.ws}: bump «${requestedBump}» обмежено стелею «${manifest.maxBump}» (package.json#release.maxBump) → ${agg.newVersion}`
129
+ )
130
+ }
125
131
 
126
132
  await writeManifestVersion(cwd, manifest, agg.newVersion)
127
133
  await prependWorkspaceChangelog(cwd, ws, agg.sectionBlock)
@@ -3,13 +3,13 @@ type: JS Module
3
3
  title: skills-cli.mjs
4
4
  resource: npm/scripts/skills-cli.mjs
5
5
  docgen:
6
- crc: 5d954e7d
6
+ crc: 3a6aab39
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  ---
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`, `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-агентом; поряд з ним підтримуються повноцінні зовнішні CLI-раннери (`cursor`, `codex`) і deprecated `claude`.
13
13
 
14
14
  ## Поведінка
15
15
 
@@ -18,7 +18,7 @@ normalizeSkillId знімає префікс `n-` з імені скілу дл
18
18
  listSkillIds отримує відсортований список ID скілів, які мають файл `SKILL.md` у вказаній директорії скілів.
19
19
  buildSkillPrompt збирає комплексний промпт для виконання скілу, об'єднуючи інструкцію скілу з конфігураційними файлами проєкту, такими як `package.json`, `tsconfig.json` та `.n-rules.json`.
20
20
  resolveBundledPackageRoot визначає абсолютний шлях до кореня пакету `@7n/rules`, використовуючи інформацію про поточний модуль.
21
- runSkillsCli виконує логіку командного інтерфейсу для керування скілами: може вивести список доступних скілів, зібрати промпт для скілу або ініціювати його виконання через вбудований pi-агент, використовуючи зовнішні CLI як резервний варіант.
21
+ runSkillsCli виконує логіку командного інтерфейсу для керування скілами: може вивести список доступних скілів, зібрати промпт для скілу або ініціювати його виконання через вбудований pi-агент чи один із зовнішніх CLI-раннерів (`cursor`, `codex`, `claude`).
22
22
 
23
23
  ## Публічний API
24
24
 
@@ -26,10 +26,10 @@ runSkillsCli виконує логіку командного інтерфейс
26
26
  - listSkillIds — повертає відсортований список id скілів, що мають `SKILL.md`.
27
27
  - buildSkillPrompt — збирає промпт виконання: інструкція скілу + контекст проєкту (`package.json`, `tsconfig.json`, `.n-rules.json`); кидає, якщо скіл невідомий.
28
28
  - resolveBundledPackageRoot — абсолютний шлях до кореня встановленого пакета `@7n/rules`.
29
- - runSkillsCli — асинхронний entrypoint підкоманди `skill`: `list`, друк промпта на stdout, або виконання через `pi` (рекомендовано), `cursor`/`claude` (deprecated). Повертає exit-код.
29
+ - runSkillsCli — асинхронний entrypoint підкоманди `skill`: `list`, друк промпта на stdout, або виконання через `pi` (рекомендовано), `cursor`/`codex` (зовнішні CLI), `claude` (deprecated). Повертає exit-код.
30
30
 
31
31
  ## Гарантії поведінки
32
32
 
33
- - Сам модуль лише читає файли й збирає промпт; **виконання** делегується агенту: `pi` (вбудований, мутує дерево, запускає bash) або зовнішнім `cursor`/`claude` CLI.
33
+ - Сам модуль лише читає файли й збирає промпт; **виконання** делегується агенту: `pi` (вбудований, мутує дерево, запускає bash) або зовнішнім `cursor`/`codex`/`claude` CLI (промпт передається через stdin: `cursor-agent -p`, `codex exec -`, `claude -p`).
34
34
  - Тира моделі для `pi`-runner береться з `main.json.tier` скіла (дефолт `max`).
35
- - `cursor`/`claude` — deprecated: друкують попередження й лишаються як fallback, доки не налаштовано pi-модель.
35
+ - Лише `claude` — deprecated: друкує попередження й лишається як fallback, доки не налаштовано pi-модель. `cursor`/`codex` — повноцінні раннери без попередження.
@@ -12,7 +12,6 @@ resource: npm/scripts/lib/lint-surface/
12
12
  | [detect.mjs](detect.md) | JS Module |
13
13
  | [ladder.mjs](ladder.md) | JS Module |
14
14
  | [lint-lock.mjs](lint-lock.md) | JS Module |
15
- | [mt-run-node.mjs](mt-run-node.md) | JS Module |
16
15
  | [mt-tail.mjs](mt-tail.md) | JS Module |
17
16
  | [policy-lint-adapter.mjs](policy-lint-adapter.md) | JS Module |
18
17
  | [policy-test-step.mjs](policy-test-step.md) | JS Module |
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: mt-tail.mjs
4
4
  resource: npm/scripts/lib/lint-surface/mt-tail.mjs
5
5
  docgen:
6
- crc: a84afa7f
6
+ crc: df14bc2e
7
7
  model: openai-codex/gpt-5.4-mini
8
8
  score: 100
9
9
  judgeModel: openai-codex/gpt-5.4-mini
@@ -11,7 +11,7 @@ docgen:
11
11
 
12
12
  ## Огляд
13
13
 
14
- Файл матеріалізує lint-хвіст у пласкі root-level MT-вузли `mt/<node>/task.md` і `a.md` без composite-плану й `spawn-approve`, щоб окремі fix-правки далі виконував orchestrator MT за контрактом `graph.md` з `Task`, `Done when` і `Check`. Ядро модулю охоплює кластеризацію, сигнатуру вузла та формування `buildTaskMd`, `buildCheckCommand` і `buildAgentFlag`, а конфіг, на який він спирається, — `.mt.json`. Єдиний гейт — onboarded-репо (наявність `.mt.json`, перевіряє `mtPreflight`); якщо MT недоступний, він працює fail-open і повертає `{ materialized: false }`, не валячи lint. Широкі фікси (кількість target-файлів від порога `N_LINT_MT_AUDIT_FILES`, дефолт 4, або whole-repo) отримують `audit: required` — collateral-контроль штатним MT-аудитом (Фаза C); вузькі — `optional`.
14
+ Файл матеріалізує lint-хвіст у пласкі root-level MT-вузли `mt/<node>/task.md` і `a.md` без composite-плану й `spawn-approve`, щоб окремі fix-правки далі виконував orchestrator MT за контрактом `graph.md` з `Task`, `Done when` і `Check`. Ядро модулю охоплює кластеризацію, сигнатуру вузла та формування `buildTaskMd`, `buildCheckCommand` і `buildAgentFlag`, а конфіг, на який він спирається, — `.mt.json`. Єдиний гейт — onboarded-репо (наявність `.mt.json`, перевіряє `mtPreflight`); якщо MT недоступний, він працює fail-open і повертає `{ materialized: false }`, не валячи lint. Широкі фікси (кількість target-файлів від порога `N_LINT_MT_AUDIT_FILES`, дефолт 4, або whole-repo) отримують `audit: required` — collateral-контроль штатним MT-аудитом (Фаза C); вузькі — `optional`. Вузли виконує вбудований шлях MT — підписочні CLI (`claude`|`codex`|`cursor`|`pi`) з user-level ENV-конфігом (`MT_AGENT_CLI`, `MT_CLOUD_AGENT_CLIS`, `MT_AGENT_CLI_MODEL_MAP`); власний `node_executor` (mt-run-node) видалено за mt ADR `260713-2110`.
15
15
 
16
16
  ## Поведінка
17
17
 
@@ -33,7 +33,6 @@ docgen:
33
33
  - buildTaskMd — генерує канонічний `task.md` для fix-вузла за контрактом `graph.md`; час не вставляє, `budgetSec` може бути заданий окремо.
34
34
  - buildAgentFlag — пише прапор `a.md`, який позначає агента-виконавця; tier перетворює на `model_tier`.
35
35
  - mtPreflight — перевіряє, чи є MT CLI+addon і чи репо вже onboarded через `.mt.json`.
36
- - ensureNodeExecutor — гарантує, що `.mt.json` задає `node_executor` = наша команда (B2), щоб вузли виконувались нашим harness, а не Claude-шляхом MT; ідемпотентно, не перезаписує наявне налаштування.
37
36
  - materializeTail — перетворює хвіст порушень на MT fix-вузли; якщо MT недоступний, не валить lint і повертає `{ materialized: false }`.
38
37
 
39
38
  ## Гарантії поведінки
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: run-fix.mjs
4
4
  resource: npm/scripts/lib/lint-surface/run-fix.mjs
5
5
  docgen:
6
- crc: d26e2efc
6
+ crc: 0a7a2c75
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  ---
9
9
 
@@ -584,9 +584,9 @@ async function renderRemaining(failing, cwd, log, verbose = false) {
584
584
  /**
585
585
  * Фаза B: матеріалізує невиправлений хвіст у вузли MT-графа. Єдиний гейт —
586
586
  * **onboarded-репо** (наявність `.mt.json`, перевіряє preflight у materializeTail):
587
- * не-MT-репо → fail-open skip; MT-репо → хвіст стає графом задач, executor яких —
588
- * наш pi-harness (`ensureNodeExecutor` конфігурує `node_executor`). Env-прапорець
589
- * скасовано (rollout-guard більше не потрібен B2/executor-міст у main). Fail-open
587
+ * не-MT-репо → fail-open skip; MT-репо → хвіст стає графом задач, які виконує
588
+ * вбудований шлях MT (підписочні CLI claude|codex|cursor|pi, user-level ENV
589
+ * `MT_AGENT_CLI*` mt ADR 260713-2110; власний node_executor видалено). Fail-open
590
590
  * на всіх рівнях: lint ніколи не падає через MT.
591
591
  * @param {LintViolation[]} remaining Невиправлені порушення (з renderRemaining).
592
592
  * @param {string} cwd Робоча директорія.
@@ -3,15 +3,16 @@
3
3
  *
4
4
  * Скіли читаються з `npm/skills/<id>/SKILL.md` установленого пакета (або кешу `npx`).
5
5
  * Промпт збирає інструкцію скілу + контекст поточного CWD (`package.json`, `tsconfig.json`,
6
- * `.n-rules.json`) — далі stdout або виконання через вбудований pi-агент
7
- * (рекомендовано), чи deprecated-делегування в `cursor-agent` / `claude`.
6
+ * `.n-rules.json`) — далі stdout або виконання через один з раннерів: вбудований
7
+ * pi-агент, чи зовнішній CLI (`cursor-agent`, `codex exec`, або deprecated `claude`).
8
8
  *
9
9
  * Підтримувані формати:
10
10
  * `npx \@7n/rules skill list`
11
11
  * `npx \@7n/rules skill taze`
12
12
  * `npx \@7n/rules skill pi taze` — виконати через вбудований pi-агент (рекомендовано)
13
13
  * `npx \@7n/rules skill pi taze "онови залежності"`
14
- * `npx \@7n/rules skill cursor taze` — deprecated: зовнішній Cursor CLI
14
+ * `npx \@7n/rules skill cursor taze` — зовнішній Cursor CLI (`cursor-agent -p`)
15
+ * `npx \@7n/rules skill codex taze` — зовнішній Codex CLI (`codex exec -`)
15
16
  * `npx \@7n/rules skill claude taze` — deprecated: зовнішній Claude Code CLI
16
17
  */
17
18
 
@@ -23,15 +24,27 @@ import { fileURLToPath } from 'node:url'
23
24
 
24
25
  import { readSkillMetaRaw, skillTier } from './lib/skill-meta.mjs'
25
26
 
26
- /** Виконавці скіла. `pi` — вбудований (рекомендований); `cursor`/`claude` — deprecated зовнішні CLI. */
27
- const RUNNERS = new Set(['pi', 'cursor', 'claude'])
27
+ /** Виконавці скіла. `pi` — вбудований (рекомендований); `cursor`/`codex`/`claude` — зовнішні CLI (`claude` — deprecated). */
28
+ const RUNNERS = new Set(['pi', 'cursor', 'codex', 'claude'])
29
+
30
+ /**
31
+ * Табличний опис зовнішніх CLI-раннерів: бінарник, аргументи для non-interactive
32
+ * режиму зі stdin-промптом, і чи раннер deprecated (друкує попередження перед запуском).
33
+ * @type {Record<'claude' | 'cursor' | 'codex', { bin: string, args: string[], deprecated: boolean }>}
34
+ */
35
+ const EXTERNAL_RUNNERS = {
36
+ claude: { bin: 'claude', args: ['-p'], deprecated: true },
37
+ cursor: { bin: 'cursor-agent', args: ['-p'], deprecated: false },
38
+ codex: { bin: 'codex', args: ['exec', '-'], deprecated: false }
39
+ }
28
40
 
29
41
  const USAGE_LINES = [
30
42
  'Usage:',
31
43
  ' npx @7n/rules skill list',
32
44
  ' npx @7n/rules skill <skill-id> ["task"]',
33
45
  ' npx @7n/rules skill pi <skill-id> ["task"] # вбудований pi-агент (рекомендовано)',
34
- ' npx @7n/rules skill cursor <skill-id> ["task"] # deprecated',
46
+ ' npx @7n/rules skill cursor <skill-id> ["task"] # зовнішній Cursor CLI',
47
+ ' npx @7n/rules skill codex <skill-id> ["task"] # зовнішній Codex CLI',
35
48
  ' npx @7n/rules skill claude <skill-id> ["task"] # deprecated',
36
49
  '',
37
50
  'Skill id: каталог у пакеті (lint, taze, …) або з префіксом n- (n-lint → lint).'
@@ -158,36 +171,28 @@ async function runPiRunner(prompt, rawSkillName, skillsRoot, projectDir, logErro
158
171
  }
159
172
 
160
173
  /**
161
- * Deprecated: делегує у зовнішній `claude -p` / `cursor-agent -p`. Лишається як fallback
162
- * для тих, у кого pi-модель ще не налаштована; буде прибрано (мігруй на `skill pi`).
163
- * @param {'claude' | 'cursor'} kind який LLM CLI запускати
174
+ * Делегує виконання скіла у зовнішній non-interactive CLI (`cursor-agent -p`, `codex exec -`,
175
+ * або deprecated `claude -p`) промпт передається через stdin. `claude` лишається як
176
+ * deprecated fallback для тих, у кого pi-модель ще не налаштована; буде прибрано
177
+ * (мігруй на `skill pi`). `cursor`/`codex` — повноцінні альтернативні раннери.
178
+ * @param {'claude' | 'cursor' | 'codex'} kind який LLM CLI запускати
164
179
  * @param {string} prompt промпт для передачі у stdin
165
180
  * @param {string} projectDir робочий каталог дочірнього процесу
166
181
  * @param {(line: string) => void} logError вивід попередження/помилок
167
182
  * @returns {number} exit code дочірнього процесу
168
183
  */
169
184
  function runLlmCli(kind, prompt, projectDir, logError) {
170
- logError(`[deprecated] skill ${kind} → use 'skill pi'; зовнішній CLI буде прибрано`)
171
-
172
- if (kind === 'claude') {
173
- if (!isBinaryInPath('claude')) {
174
- throw new Error('`claude` not found in PATH. Install Claude Code CLI or use `skill pi`.')
175
- }
185
+ const runner = EXTERNAL_RUNNERS[kind]
176
186
 
177
- const result = spawnSync('claude', ['-p'], {
178
- input: prompt,
179
- cwd: projectDir,
180
- stdio: ['pipe', 'inherit', 'inherit'],
181
- encoding: 'utf8'
182
- })
183
- return result.status ?? 1
187
+ if (runner.deprecated) {
188
+ logError(`[deprecated] skill ${kind} → use 'skill pi'; зовнішній CLI буде прибрано`)
184
189
  }
185
190
 
186
- if (!isBinaryInPath('cursor-agent')) {
187
- throw new Error('`cursor-agent` not found in PATH. Install Cursor CLI or use `skill pi`.')
191
+ if (!isBinaryInPath(runner.bin)) {
192
+ throw new Error(`\`${runner.bin}\` not found in PATH. Install ${kind} CLI or use \`skill pi\`.`)
188
193
  }
189
194
 
190
- const result = spawnSync('cursor-agent', ['-p'], {
195
+ const result = spawnSync(runner.bin, runner.args, {
191
196
  input: prompt,
192
197
  cwd: projectDir,
193
198
  stdio: ['pipe', 'inherit', 'inherit'],
@@ -244,7 +249,7 @@ export async function runSkillsCli(argv, options = {}) {
244
249
  if (first === 'pi') {
245
250
  return await runPiRunner(prompt, second, skillsRoot, projectDir, logError, deps)
246
251
  }
247
- return runLlmCli(/** @type {'claude' | 'cursor'} */ (first), prompt, projectDir, logError)
252
+ return runLlmCli(/** @type {'claude' | 'cursor' | 'codex'} */ (first), prompt, projectDir, logError)
248
253
  }
249
254
 
250
255
  if (skillIds.includes(normalizeSkillId(first))) {
@@ -1,22 +1,32 @@
1
1
  ---
2
2
  name: n-taze
3
3
  description: >-
4
- Оновлення версій модулів проекту з аналізом major-змін і автоматичним
5
- рефакторингом несумісного коду
6
- version: '1.0'
4
+ Оновлення версій модулів проекту (bun/npm і, якщо є Cargo.toml, Rust-крейти
5
+ через cargo-edit) з аналізом major-змін і автоматичним рефакторингом
6
+ несумісного коду
7
+ version: '1.1'
7
8
  ---
8
9
 
9
10
  # n-taze — Оновлення версій проекту
10
11
 
11
12
  ## Мета
12
13
 
13
- Оновити всі модулі проекту до останніх версій, виявити major-оновлення, перевірити сумісність змін з кодом проекту і за потреби зрефакторити несумісні місця.
14
+ Оновити всі модулі проекту (npm/bun-залежності, а за наявності `Cargo.toml` — і Rust-крейти) до останніх версій, виявити major-оновлення, перевірити сумісність змін з кодом проекту і за потреби зрефакторити несумісні місця.
14
15
 
15
16
  ## Передумови
16
17
 
17
18
  - Чисте робоче дерево (`git status` без незакомічених змін у `package.json` / `bun.lock` / `node_modules`) — інакше різницю не відрізнити від оновлення.
18
19
  - Встановлений `bun` і доступний `bunx`.
19
20
  - Запуск з кореня проекту (де лежить `package.json` / `bun.lock`).
21
+ - Якщо в проекті є хоч один `Cargo.toml` (не в `node_modules`/`.worktrees`) — додатково встановлений `cargo-edit` (`cargo install cargo-edit`, дає команду `cargo upgrade`). Без нього major-бампи Rust-залежностей неможливо застосувати детерміновано (голий `cargo update` піднімає лише semver-сумісні версії) — **STOP** і попроси користувача встановити перед продовженням кроку 2 для Rust-гілки.
22
+
23
+ ### 0.2. Детекція Rust-крейтів
24
+
25
+ ```bash
26
+ find . -name Cargo.toml -not -path "*/node_modules/*" -not -path "*/.worktrees/*" -not -path "*/target/*"
27
+ ```
28
+
29
+ Якщо список непорожній — паралельно з npm-гілкою виконуються кроки 1–8 у Rust-варіанті (позначені нижче як «Rust-гілка»). Якщо порожній — Rust-кроки повністю пропускаються.
20
30
 
21
31
  ## Workflow
22
32
 
@@ -31,6 +41,13 @@ cp bun.lock bun.lock.taze-bak
31
41
 
32
42
  (У monorepo — також усі `*/package.json` воркспейсів. Файли тимчасові, видалити в кінці.)
33
43
 
44
+ **Rust-гілка** — для кожного знайденого на кроці 0.2 `Cargo.toml` (включно з кореневим, якщо є):
45
+
46
+ ```bash
47
+ cp Cargo.toml Cargo.toml.taze-bak
48
+ cp Cargo.lock Cargo.lock.taze-bak # якщо lock-файл спільний на workspace — достатньо одного бекапу в корені
49
+ ```
50
+
34
51
  ### 2. Запустити оновлення
35
52
 
36
53
  ```bash
@@ -42,6 +59,16 @@ bun install
42
59
  - `-r` — рекурсивно по всіх воркспейсах.
43
60
  - `latest` — піднімати навіть major.
44
61
 
62
+ **Rust-гілка:**
63
+
64
+ ```bash
65
+ cargo upgrade --incompatible allow
66
+ cargo update
67
+ ```
68
+
69
+ - `cargo upgrade` (з `cargo-edit`) переписує вимоги версій у кожному `Cargo.toml` workspace-у на останні; `--incompatible allow` явно дозволяє перетинати major-межу (аналог `-r latest` у taze) — без цього флага incompatible-оновлення за замовчуванням ігноруються.
70
+ - `cargo update` після цього синхронізує `Cargo.lock` з новими вимогами.
71
+
45
72
  ### 3. Виявити major-оновлення
46
73
 
47
74
  > **Не порівнюй `package.json` вручну.** Класифікацію semver несе CLI — детерміновано, по всіх воркспейсах.
@@ -54,6 +81,8 @@ n-rules taze diff
54
81
 
55
82
  Покриває **прямі** залежності з `package.json` (root + воркспейси). Транзитивні major-стрибки (`bun.lock`) — за потреби переглянь окремо; основний ризик breaking-змін — у прямих.
56
83
 
84
+ **Rust-гілка** — для `n-rules taze diff` немає cargo-еквівалента, класифікація ручна: для кожного `Cargo.toml.taze-bak` порівняти версію кожної залежності зі свіжим `Cargo.toml` за тим самим правилом (зміна найлівішої ненульової semver-компоненти = major). Швидкий спосіб — `diff Cargo.toml.taze-bak Cargo.toml` по рядках `<name> = "<version>"` і вручну класифікувати кожну зміну.
85
+
57
86
  ### 4. Зібрати breaking changes по кожному major-оновленню
58
87
 
59
88
  Для кожного модуля зі списку зібрати фактичні відмінності одним з джерел (у порядку пріоритету):
@@ -64,6 +93,8 @@ n-rules taze diff
64
93
 
65
94
  Цікавлять: видалені/перейменовані експорти, змінені сигнатури функцій, змінені типи, змінена поведінка за замовчуванням, видалені CLI-прапорці.
66
95
 
96
+ **Rust-гілка** — адресу репозиторію взяти з поля `repository`/`documentation` крейта на `crates.io` (`https://crates.io/crates/<name>`) або з `[package.metadata]`; CHANGELOG зазвичай у `CHANGELOG.md` репозиторію або в GitHub Releases. Якщо немає — `cargo doc` різниця по публічному API (`pub fn`/`pub struct`/`pub trait`) між закешованою старою версією (`~/.cargo/registry/src/*/<name>-<old-version>/`) і новою (`~/.cargo/registry/src/*/<name>-<new-version>/`) через `diff -r src/`.
97
+
67
98
  ### 5. Перевірити сумісність з кодом проекту
68
99
 
69
100
  Для кожного breaking change знайти його використання в коді проекту:
@@ -77,6 +108,14 @@ rg -n "<імпорт|функція|опція>" --type ts --type js --type vue
77
108
  - **сумісно** — проект не використовує зачеплене API → нічого не робити.
78
109
  - **несумісно** — використання знайдено → перейти до п. 6.
79
110
 
111
+ **Rust-гілка:**
112
+
113
+ ```bash
114
+ rg -n "<use-шлях|функція|макрос>" --type rust
115
+ ```
116
+
117
+ Та сама класифікація сумісно/несумісно.
118
+
80
119
  ### 6. Рефакторинг несумісних місць
81
120
 
82
121
  Для кожного несумісного місця — застосувати міграцію згідно з changelog модуля (перейменувати імпорт, оновити сигнатуру виклику, замінити видалену опцію еквівалентом тощо). Після правок:
@@ -87,6 +126,14 @@ bun run typecheck # якщо є
87
126
  bun test # якщо є
88
127
  ```
89
128
 
129
+ **Rust-гілка** — після правок:
130
+
131
+ ```bash
132
+ cargo fmt --all -- --check
133
+ cargo clippy --all-targets --all-features -- -D warnings
134
+ cargo test
135
+ ```
136
+
90
137
  Якщо міграція нетривіальна або неоднозначна — **не вгадувати**, залишити TODO у коді з посиланням на CHANGELOG і винести в підсумковий звіт як ручну дію.
91
138
 
92
139
  ### 7. Прибрати тимчасові файли
@@ -97,6 +144,14 @@ rm package.json.taze-bak bun.lock.taze-bak
97
144
 
98
145
  (І решту бекапів воркспейсів, якщо створювались.)
99
146
 
147
+ **Rust-гілка:**
148
+
149
+ ```bash
150
+ rm Cargo.toml.taze-bak Cargo.lock.taze-bak
151
+ ```
152
+
153
+ (І бекапи по кожному workspace-члену, якщо створювались окремо.)
154
+
100
155
  ### 8. Звіт користувачу
101
156
 
102
157
  Коротко в одному повідомленні:
@@ -107,8 +162,11 @@ rm package.json.taze-bak bun.lock.taze-bak
107
162
  - **Потребує ручного втручання:** список TODO з причиною (нетривіальна міграція / неоднозначність / падіння тестів).
108
163
  - **Стан перевірок:** `lint` / `typecheck` / `test` — pass/fail з номером рядка, де впало.
109
164
 
165
+ Якщо на кроці 0.2 знайдені Rust-крейти — додати окрему секцію **Rust-крейти** з тим самим переліком (оновлено / major / зрефакторено / потребує ручного втручання), і в **Стан перевірок** — окремо `cargo fmt` / `cargo clippy` / `cargo test`.
166
+
110
167
  ## Примітка
111
168
 
112
169
  - Не запускати `npx @7n/rules lint` паралельно з іншими ESLint-задачами — діє правило з кореневого `CLAUDE.md`.
113
170
  - Якщо проект — `npm/` пакет цього репо, після змін у `package.json` / коді треба підняти `version` і додати запис у `CHANGELOG.md` згідно з `npm/CLAUDE.md`.
114
- - При великій кількості major-оновлень розбити PR по одному модулю на коміт — щоб `git bisect` залишався корисним.
171
+ - При великій кількості major-оновлень розбити PR по одному модулю на коміт — щоб `git bisect` залишався корисним. Це стосується і Rust-крейтів окремо від npm-пакетів.
172
+ - `cargo upgrade --incompatible allow` редагує `Cargo.toml` навіть для залежностей без доступних breaking changes в змінах API — завжди звіряй крок 4 (CHANGELOG) перед тим, як вважати оновлення безпечним, а не лише факт успішної компіляції.
@@ -1,28 +0,0 @@
1
- ## Покриття + мутаційне тестування Rust
2
-
3
- Покриття + мутаційне тестування Rust постачаються через `@7n/test coverage` (правило `test.mdc`). Реалізація провайдера — у `npm/rules/rust/coverage/coverage.mjs`: `cargo llvm-cov --json --summary-only` + `cargo mutants --jobs N`.
4
-
5
- Бінарники: `cargo install cargo-llvm-cov && cargo install cargo-mutants`.
6
-
7
- ### Паралелізм cargo-mutants
8
-
9
- Паралельні воркери: дефолт `min(4, cpus/2)`; override через env **`CARGO_MUTANTS_JOBS`**. Прапорець `--in-place` відсутній — cargo-mutants створює власну sandbox-копію в `target/mutants.<i>/`, що сумісне з `--jobs > 1`.
10
-
11
- ### Incremental mutation через `--in-diff`
12
-
13
- cargo-mutants **не** має persistent-кешу вердиктів між прогонами (на відміну від Stryker `incremental.json`). Штатний аналог «не передивляйся незмінений код» — scoping за git-diff. Вмикається env-змінною **`CARGO_MUTANTS_BASE_REF`**:
14
-
15
- - **не задано** (дефолт, типово для `main`) — повний прогін усіх мутантів;
16
- - задано (напр. `origin/main`, типово для feature-гілки в CI) — мутуються лише рядки, змінені у `<baseRef>...HEAD`. Провайдер бере `git diff --relative <baseRef>...HEAD` з каталогу crate, пише diff у sandbox і передає cargo-mutants `--in-diff`.
17
-
18
- Краєві випадки: порожній diff (немає змін під crate) → mutation `0/0` без запуску cargo-mutants; невідомий ref / не git-репо → попередження у stderr і fallback до повного прогону.
19
-
20
- ### Пропуск baseline через `CARGO_MUTANTS_BASELINE=skip`
21
-
22
- cargo-mutants спершу ганяє немутований baseline (повний build+test), щоб переконатися, що suite зелений. **`CARGO_MUTANTS_BASELINE=skip`** прибирає цей крок (`--baseline skip`), економлячи один повний `cargo test`.
23
-
24
- Безпечно **лише** коли тести вже зелені у попередньому CI-степі (типовий порядок: `cargo test` → потім `@7n/test coverage` зі `skip`). Без цієї гарантії всі вердикти стають сміттєвими — дефолт: baseline-прогін. Найкорисніше в парі з `--in-diff`.
25
-
26
- ### CI-кеш `target/` — множник
27
-
28
- `--in-diff` ріже **кількість** мутантів, кеш `target/` — **вартість кожної** компіляції; вони множаться. Без кешу холодний CI щоразу перебудовує всі залежності, і baseline-build (для Tauri — хвилини) затьмарює економію від меншої кількості мутантів. У workflow, що викликає `@7n/test coverage` для Rust, став `Swatinem/rust-cache@v2` після `dtolnay/rust-toolchain@stable` — вимога й автофікс тепер перевіряються загально (будь-який job, будь-який workflow) concern-ом `rust/toolchain_cache`.
@@ -1,37 +0,0 @@
1
- ---
2
- type: JS Module
3
- title: mt-run-node.mjs
4
- resource: npm/scripts/lib/lint-surface/mt-run-node.mjs
5
- docgen:
6
- crc: 01c3876b
7
- model: openai-codex/gpt-5.4-mini
8
- score: 100
9
- issues: judge:inaccurate:0.98
10
- judgeModel: openai-codex/gpt-5.4-mini
11
- ---
12
-
13
- ## Огляд
14
-
15
- Цей файл є executor-містом для fix-вузла MT у pi-harness: MT-runner (`@7n/mt#29`) спавнить цю команду замість вбудованого Claude-шляху для actor=agent вузлів. `argv[0]` і `MT_NODE_DIR` вказують на директорію вузла з `task.md` та `a.md`, `MT_WORKTREE` задає дерево, у якому застосовуються зміни, а `MT_MODEL_TIER` дублює `a.md` у значеннях `MIM|AVG|MAX`. Команда повертає stdout у форматі JSON `{"applied","touchedFiles"}`; `exit 0` означає, що MT сам запускає `## Check` і синтезує fact, а ненульовий exit означає `failed-run`. Її роль — виконати лише застосування змін у межах канонів тирів `omlx/pi-тири llm-lib`, тоді як claim/lease, worktree-ізоляція, budget/timeout, `## Check` і publish залишаються на стороні MT.
16
-
17
- ## Поведінка
18
-
19
- - `parseNodeContract` — дістає з `task.md` правило з `## Check`, список target-файлів з `## Inputs` і текст задачі з `## Task`.
20
- - `resolveTierLabel` — перетворює `MT_MODEL_TIER` у tier-label для `llm-lib`, а невідоме або порожнє значення зводить до `avg`.
21
- - `buildViolationText` — формує текст порушення для agent-fix із задачі та, за наявності, додає перелік target-файлів; canonical-check лишається за `## Check`.
22
- - `runNode` — читає `task.md`, парсить контракт вузла, підбирає tier і запускає виправлення в заданому worktree через інʼєкований або стандартний fix.
23
- - `runNodeCli` — бере `node-dir` з argv або `MT_NODE_DIR`, запускає обробку вузла, друкує JSON зі станом виконання та повертає код завершення без пробросу винятків назовні.
24
- - `recordNodeFixTelemetry` — успішний фікс із реальними правками пише запис `oldText→newText` у глобальний distillation-стор (Фаза C): MT-вузли живлять той самий крос-репо корпус маховика T0, що й інлайн-драбина; best-effort, лише applied без error.
25
-
26
- ## Публічний API
27
-
28
- - parseNodeContract — дістає з `task.md` контракт вузла й перетворює його на придатний для виконання опис.
29
- - resolveTierLabel — зв’язує `MT model_tier` із міткою `llm-lib`; для невідомого або порожнього значення підставляє `avg`.
30
- - buildViolationText — формує текст порушення для агента на основі контракту вузла, без повторного визначення канонічної перевірки `## Check`.
31
- - runNode — запускає один MT-вузол: читає контракт, готує виправлення нашим harness у worktree та повертає результат.
32
- - runNodeCli — запускає `node_executor` як CLI: бере `node-dir` з `argv[0]`, використовує `MT_WORKTREE` і `MT_MODEL_TIER`, виводить у stdout JSON з `applied` і `touchedFiles`, а кодом завершення показує успіх або помилку екзекутора.
33
-
34
- ## Гарантії поведінки
35
-
36
- - Read-only: не виконує операцій запису (ФС/БД).
37
- - Перехоплює помилки і не пропускає винятків назовні (fail-safe).
@@ -1,206 +0,0 @@
1
- /**
2
- * B2 — executor-міст: виконання fix-вузла MT нашим pi-harness (Фаза B, spec
3
- * docs/specs/2026-07-11-phase-b-lint-mt-adapter-dev-design.md).
4
- *
5
- * MT-runner (node_executor, `@7n/mt`#29) спавнить цю команду замість вбудованого
6
- * Claude-шляху для actor=agent вузлів. Контракт (з #29):
7
- * - argv[0] / `MT_NODE_DIR` = директорія вузла (task.md + a.md);
8
- * - `MT_WORKTREE` = дерево, у якому застосовувати зміни (cwd фіксу);
9
- * - `MT_MODEL_TIER` = MIM|AVG|MAX (дублює a.md);
10
- * - stdout = JSON `{applied, touchedFiles}`; exit 0 → MT ганяє `## Check` і
11
- * синтезує fact; ненульовий → failed-run.
12
- * MT володіє claim/lease, worktree-ізоляцією, budget/timeout, `## Check`, publish;
13
- * екзекутор ЛИШЕ «застосуй зміни» нашим тир-каноном (omlx/pi-тири llm-lib).
14
- *
15
- * Екзекутор НЕ re-detect-ить: canonical-гейт — `## Check` за MT. violation-текст
16
- * будується з контракту вузла (task.md ## Task + target-файли), агент читає файли
17
- * й правило сам. Чисте ядро (парсинг контракту, tier-мапінг) — тестовне; fix
18
- * інжектується через `deps`.
19
- */
20
-
21
- import { readFileSync } from 'node:fs'
22
- import { join } from 'node:path'
23
- import { env } from 'node:process'
24
-
25
- /** MT model_tier → llm-lib tier-label (для resolveModel/thinkingLevel). */
26
- const TIER_MAP = { MIM: 'min', AVG: 'avg', MAX: 'max' }
27
-
28
- /** Маркер `## Check`-команди (індексний пошук — без regex-backtracking). */
29
- const CHECK_MARKER = 'lint --no-fix'
30
- /** Розділювач токенів команди. */
31
- const RE_TOKEN_SPLIT = /\s+/
32
- /** Рядок target-файлу в `## Inputs` (`- \`path\``). */
33
- const RE_INPUT_FILE = /^- `([^`]+)`/
34
-
35
- /**
36
- * Витягує правило з `## Check`-команди вузла. Правило — ОСТАННІЙ токен рядка
37
- * (перед ним можуть стояти флаги на кшталт `--cwd ../..`, які додає
38
- * buildCheckCommand, бо MT ганяє `## Check` із node-dir). Токен-флаг (`-…`)
39
- * останнім бути не може — тоді правило відсутнє.
40
- * @param {string} taskMd вміст task.md
41
- * @returns {string|null} id правила або null
42
- */
43
- function parseRule(taskMd) {
44
- const idx = taskMd.indexOf(CHECK_MARKER)
45
- if (idx === -1) return null
46
- const eol = taskMd.indexOf('\n', idx)
47
- const rest = taskMd.slice(idx + CHECK_MARKER.length, eol === -1 ? undefined : eol).trim()
48
- const tokens = rest.length > 0 ? rest.split(RE_TOKEN_SPLIT) : []
49
- const last = tokens.at(-1)
50
- return last && !last.startsWith('-') ? last : null
51
- }
52
-
53
- /**
54
- * Витягує target-файли з секції Inputs (bullet-рядки з path у backtick-ах).
55
- * @param {string} taskMd вміст task.md
56
- * @returns {string[]} перелік файлів (whole-repo маркер відкидається)
57
- */
58
- function parseTargetFiles(taskMd) {
59
- const idx = taskMd.indexOf('## Inputs')
60
- if (idx === -1) return []
61
- const section = taskMd.slice(idx)
62
- const files = []
63
- for (const line of section.split('\n')) {
64
- const m = RE_INPUT_FILE.exec(line.trim())
65
- if (m && !m[1].startsWith('(')) files.push(m[1])
66
- }
67
- return files
68
- }
69
-
70
- /**
71
- * Парсить контракт вузла з task.md.
72
- * @param {string} taskMd вміст task.md
73
- * @returns {{ rule: string|null, targetFiles: string[], taskText: string }} контракт
74
- */
75
- export function parseNodeContract(taskMd) {
76
- const taskIdx = taskMd.indexOf('## Task')
77
- const doneIdx = taskMd.indexOf('## Done when')
78
- const end = doneIdx === -1 ? taskMd.length : doneIdx
79
- const taskText = taskIdx === -1 ? '' : taskMd.slice(taskIdx + '## Task'.length, end).trim()
80
- return { rule: parseRule(taskMd), targetFiles: parseTargetFiles(taskMd), taskText }
81
- }
82
-
83
- /**
84
- * MT model_tier → llm-lib tier-label. Невідоме/відсутнє → 'avg'.
85
- * @param {string|undefined} mtTier MIM|AVG|MAX
86
- * @returns {'min'|'avg'|'max'} tier-label
87
- */
88
- export function resolveTierLabel(mtTier) {
89
- return TIER_MAP[String(mtTier ?? '').toUpperCase()] ?? 'avg'
90
- }
91
-
92
- /**
93
- * Будує violation-текст для агента з контракту вузла (без re-detect —
94
- * canonical-гейт лишається за MT `## Check`).
95
- * @param {{ rule: string|null, targetFiles: string[], taskText: string }} contract контракт
96
- * @returns {string} опис проблеми для runAgentFix
97
- */
98
- export function buildViolationText(contract) {
99
- const parts = [contract.taskText || `Порушення правила ${contract.rule ?? '(невідоме)'}.`]
100
- if (contract.targetFiles.length > 0) {
101
- parts.push(`Target-файли: ${contract.targetFiles.join(', ')}.`)
102
- }
103
- return parts.join('\n')
104
- }
105
-
106
- /**
107
- * Дефолтний fix через pi-harness (lazy import — тримає top-level pi-free).
108
- * Успішний фікс пише запис у глобальний distillation-стор (Фаза C, §13
109
- * pi-migration): MT-вузли живлять той самий корпус oldText→newText, що й
110
- * інлайн-драбина — крос-репо маховик T0 не розрізняє шлях фіксу.
111
- * @param {{ rule: string, violation: string, cwd: string, tier: string, targetFiles: string[] }} args параметри
112
- * @returns {Promise<{ applied: boolean, touchedFiles: string[], error: string|null }>} результат
113
- */
114
- async function defaultFix({ rule, violation, cwd, tier, targetFiles }) {
115
- const [{ runAgentFix }, { resolveModel }] = await Promise.all([
116
- import('@7n/llm-lib/agent-fix'),
117
- import('@7n/llm-lib/model-tiers')
118
- ])
119
- const model = resolveModel(tier)
120
- const res = await runAgentFix(rule, violation, cwd, {
121
- model,
122
- tier,
123
- targetFiles,
124
- caller: `mt-run-node:${rule}`
125
- })
126
- await recordNodeFixTelemetry({ rule, tier, model, cwd, res })
127
- return { applied: res.applied, touchedFiles: res.touchedFiles ?? [], error: res.error }
128
- }
129
-
130
- /**
131
- * Пише успішний MT-фікс у глобальний distillation-стор (Фаза C). Лише applied без
132
- * error і з реальними правками; best-effort (стор сам ковтає IO-помилки).
133
- * @param {{ rule: string, tier: string, model: string|undefined, cwd: string,
134
- * res: { applied: boolean, error: string|null, telemetry?: object|null },
135
- * record?: (r: object) => object|null }} args контекст (record — інʼєкція для тестів)
136
- * @returns {Promise<void>} нічого не повертає
137
- */
138
- export async function recordNodeFixTelemetry({ rule, tier, model, cwd, res, record }) {
139
- const edits = res.telemetry?.edits
140
- if (!res.applied || res.error || !Array.isArray(edits) || edits.length === 0) return
141
- let rec = record
142
- if (!rec) {
143
- const mod = await import('@7n/llm-lib/telemetry-store')
144
- rec = mod.recordFixTelemetry
145
- }
146
- rec({
147
- rule,
148
- rung: `mt-${tier}`,
149
- model,
150
- cwd,
151
- edits,
152
- verifyAttempts: res.telemetry?.verifyAttempts?.length ?? 0,
153
- anchoredEdits: res.telemetry?.anchoredEdits ?? false
154
- })
155
- }
156
-
157
- /**
158
- * Виконує один MT-вузол: парсить контракт, застосовує фікс нашим harness у worktree.
159
- * @param {{ nodeDir: string, worktree: string, mtTier?: string,
160
- * deps?: { readFile?: (p: string) => string, fix?: typeof defaultFix } }} args параметри
161
- * @returns {Promise<{ applied: boolean, touchedFiles: string[], error: string|null }>} результат для stdout
162
- */
163
- export async function runNode({ nodeDir, worktree, mtTier, deps = {} }) {
164
- const read = deps.readFile ?? (p => readFileSync(p, 'utf8'))
165
- const fix = deps.fix ?? defaultFix
166
-
167
- let taskMd
168
- try {
169
- taskMd = read(join(nodeDir, 'task.md'))
170
- } catch (error) {
171
- return { applied: false, touchedFiles: [], error: `не прочитано task.md: ${error.message}` }
172
- }
173
- const contract = parseNodeContract(taskMd)
174
- if (!contract.rule) return { applied: false, touchedFiles: [], error: 'не знайдено правило у ## Check' }
175
-
176
- const tier = resolveTierLabel(mtTier)
177
- return await fix({
178
- rule: contract.rule,
179
- violation: buildViolationText(contract),
180
- cwd: worktree,
181
- tier,
182
- targetFiles: contract.targetFiles
183
- })
184
- }
185
-
186
- /**
187
- * CLI-ентрі node_executor: argv[0]=node-dir; env MT_WORKTREE/MT_MODEL_TIER.
188
- * Друкує JSON `{applied, touchedFiles}` у stdout; повертає exit-код (0 — успіх
189
- * виконання, MT далі ганяє `## Check`; 1 — помилка екзекутора).
190
- * @param {string[]} argv аргументи (без node/script)
191
- * @param {{ env?: Record<string, string|undefined>, out?: (s: string) => void, run?: typeof runNode }} [deps] інʼєкції
192
- * @returns {Promise<number>} exit-код
193
- */
194
- export async function runNodeCli(argv, deps = {}) {
195
- const e = deps.env ?? env
196
- const out = deps.out ?? (s => process.stdout.write(s))
197
- const run = deps.run ?? runNode
198
- const nodeDir = argv[0] ?? e.MT_NODE_DIR
199
- if (!nodeDir) {
200
- out(`${JSON.stringify({ applied: false, touchedFiles: [], error: 'не задано node-dir (argv/MT_NODE_DIR)' })}\n`)
201
- return 1
202
- }
203
- const res = await run({ nodeDir, worktree: e.MT_WORKTREE ?? nodeDir, mtTier: e.MT_MODEL_TIER })
204
- out(`${JSON.stringify({ applied: res.applied, touchedFiles: res.touchedFiles })}\n`)
205
- return res.error ? 1 : 0
206
- }