@7n/rules 1.1.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 +11 -0
- package/bin/n-rules.js +1 -11
- package/package.json +4 -1
- package/rules/changelog/lib/package-manifest.mjs +19 -2
- package/rules/release/lib/aggregate.mjs +18 -2
- package/rules/release/release.mjs +8 -2
- package/scripts/docs/skills-cli.md +6 -6
- package/scripts/lib/lint-surface/docs/index.md +0 -1
- package/scripts/lib/lint-surface/docs/mt-tail.md +2 -3
- package/scripts/lib/lint-surface/docs/run-fix.md +1 -1
- package/scripts/lib/lint-surface/mt-tail.mjs +0 -0
- package/scripts/lib/lint-surface/run-fix.mjs +3 -3
- package/scripts/skills-cli.mjs +31 -26
- package/skills/taze/SKILL.md +63 -5
- package/scripts/lib/lint-surface/docs/mt-run-node.md +0 -37
- package/scripts/lib/lint-surface/mt-run-node.mjs +0 -206
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
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
|
+
|
|
3
14
|
## [1.1.0] - 2026-07-13
|
|
4
15
|
|
|
5
16
|
### 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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
- `
|
|
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:
|
|
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
|
## Гарантії поведінки
|
|
Binary file
|
|
@@ -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-репо → хвіст стає графом задач,
|
|
588
|
-
*
|
|
589
|
-
*
|
|
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 Робоча директорія.
|
package/scripts/skills-cli.mjs
CHANGED
|
@@ -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 або виконання через вбудований
|
|
7
|
-
*
|
|
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` —
|
|
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` —
|
|
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"] #
|
|
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
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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(
|
|
187
|
-
throw new Error(
|
|
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(
|
|
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))) {
|
package/skills/taze/SKILL.md
CHANGED
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: n-taze
|
|
3
3
|
description: >-
|
|
4
|
-
Оновлення версій модулів проекту
|
|
5
|
-
|
|
6
|
-
|
|
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,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
|
-
}
|