@7n/rules 1.1.0 → 1.2.1
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 +18 -0
- package/bin/n-rules.js +1 -11
- package/package.json +4 -1
- package/rules/changelog/lib/docs/package-manifest.md +6 -4
- package/rules/changelog/lib/package-manifest.mjs +19 -2
- package/rules/release/docs/release.md +4 -3
- package/rules/release/lib/aggregate.mjs +18 -2
- package/rules/release/lib/docs/aggregate.md +18 -7
- package/rules/release/release.mjs +8 -2
- package/rules/text/run-v8r/docs/main.md +3 -1
- package/rules/text/run-v8r/main.mjs +18 -2
- package/scripts/docs/skills-cli.md +6 -6
- package/scripts/lib/adr/docs/normalize-pipeline.md +1 -1
- package/scripts/lib/adr/normalize-pipeline.mjs +3 -1
- 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 +36 -28
- 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,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.2.1] - 2026-07-14
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Bun-сумісність тестового прогону: namespace-імпорт zod (фантомний __esModule на ESM-неймспейсах ламає vitest-interop), явний env: process.env у spawnSync skills-cli (Bun дає дітям snapshot оточення), чистка bun-node-* shim-тек з PATH для дочірнього v8r (node-shebang під --bun резолвився в bun і падав на node:sea); тест run-v8r приведено до контракту verbose-виводу (#44); root scripts.test → bun run --bun vitest run
|
|
8
|
+
- package-manifest: VALID_MAX_BUMPS → Set (oxlint prefer-set-has), дока maxBump освіжена
|
|
9
|
+
|
|
10
|
+
## [1.2.0] - 2026-07-14
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `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-гілкою.
|
|
15
|
+
- `n-rules release`: додано `package.json#release.maxBump` — стеля на bump цього workspace (`major|minor|patch`); change-файл із `bump: major` понад стелею обрізається й друкує попередження, замість підняти major-версію. `@7n/rules` тепер зафіксований на `maxBump: minor` — власна major-версія пакета більше не змінюється автоматично через реліз-CI.
|
|
16
|
+
|
|
17
|
+
### Removed
|
|
18
|
+
|
|
19
|
+
- Команду `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`
|
|
20
|
+
|
|
3
21
|
## [1.1.0] - 2026-07-13
|
|
4
22
|
|
|
5
23
|
### 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.1
|
|
3
|
+
"version": "1.2.1",
|
|
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"
|
|
@@ -3,7 +3,7 @@ type: JS Module
|
|
|
3
3
|
title: package-manifest.mjs
|
|
4
4
|
resource: npm/rules/changelog/lib/package-manifest.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 10bad2ce
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
Модуль `package-manifest.mjs` реалізує **уніфіковану абстракцію маніфесту пакета** для перевірок changelog у багатомовному монорепо. Він приховує відмінності між двома типами маніфестів:
|
|
@@ -17,7 +17,7 @@ docgen:
|
|
|
17
17
|
2. **Читання маніфесту воркспейсу** в єдиній структурі `PackageManifest` (з пріоритетом `package.json` над `pyproject.toml`).
|
|
18
18
|
3. **Виявлення коренів усіх пакетів монорепо**: npm-воркспейси через скрипт `workspaces.mjs` + автоматичний пошук Python-проєктів за `pyproject.toml`.
|
|
19
19
|
|
|
20
|
-
Базова мета — дати правилам категорії `changelog` єдину модель «пакет = (kind, ws, name, version, registryPublishable)», незалежно від мови.
|
|
20
|
+
Базова мета — дати правилам категорії `changelog` єдину модель «пакет = (kind, ws, name, version, registryPublishable, maxBump)», незалежно від мови.
|
|
21
21
|
|
|
22
22
|
## Експорти / API
|
|
23
23
|
|
|
@@ -44,12 +44,14 @@ docgen:
|
|
|
44
44
|
* @property {string | null} version semver-рядок
|
|
45
45
|
* @property {boolean} registryPublishable чи застосовується режим порівняння з реєстром
|
|
46
46
|
* @property {string[] | null} [npmFiles] лише npm: 'files' з package.json
|
|
47
|
+
* @property {'major' | 'minor' | 'patch' | null} maxBump стеля для 'n-rules release' (з 'package.json#release.maxBump'); null — без обмеження
|
|
47
48
|
*/
|
|
48
49
|
```
|
|
49
50
|
|
|
50
51
|
### Константи модуля
|
|
51
52
|
|
|
52
53
|
- `PYPROJECT_GLOB_IGNORE = ['**/node_modules/**', '**/.git/**', '**/.venv/**', '**/venv/**']` — патерни, що **виключаються** під час сканування `pyproject.toml` глобом, щоб не зачіпати чужі залежності та віртуальні середовища.
|
|
54
|
+
- `VALID_MAX_BUMPS` — `Set` дозволених значень `release.maxBump` (`'major' | 'minor' | 'patch'`); будь-яке інше значення тихо ігнорується (`maxBump: null`).
|
|
53
55
|
|
|
54
56
|
## Функції
|
|
55
57
|
|
|
@@ -93,14 +95,14 @@ docgen:
|
|
|
93
95
|
- Читає текст через `readFile(pkgPath, 'utf8')` і парсить як JSON.
|
|
94
96
|
- Якщо JSON — не об'єкт (null, масив, примітив), повертає `null`.
|
|
95
97
|
- Обчислює `registryPublishable = name є непорожнім рядком && private !== true && files є масивом`. Тобто пакет вважається «публікованим у реєстр», тільки якщо в `package.json` явно вказано непорожній `name`, не зведено `private: true`, і визначено поле `files` (whitelist того, що публікується).
|
|
96
|
-
- Повертає об'єкт із `kind: 'npm'`, `manifestRel: 'package.json'`, з `name`/`version` (тільки якщо вони — рядки, інакше `null`), `registryPublishable`, `npmFiles = pkg.files` або `null
|
|
98
|
+
- Повертає об'єкт із `kind: 'npm'`, `manifestRel: 'package.json'`, з `name`/`version` (тільки якщо вони — рядки, інакше `null`), `registryPublishable`, `npmFiles = pkg.files` або `null`, а також `maxBump` — валідне значення `package.json#release.maxBump` (`major`/`minor`/`patch`) або `null`, якщо поле відсутнє, не рядок чи поза дозволеним списком.
|
|
97
99
|
- Будь-яка помилка читання/парсингу JSON у блоці `try` → повертає `null` (catch без логування).
|
|
98
100
|
2. **Гілка Python:**
|
|
99
101
|
- Складає шлях `cwd/ws/pyproject.toml`.
|
|
100
102
|
- `existsSync` → якщо файлу немає, повертає `null`.
|
|
101
103
|
- Читає файл і викликає `parsePyprojectFields`.
|
|
102
104
|
- `registryPublishable = Boolean(name && version)` — для Python публікація в PyPI потребує лише валідних `name` і `version` (PyPI не має аналога `files` whitelist).
|
|
103
|
-
- Повертає об'єкт із `kind: 'python'`, `manifestRel: 'pyproject.toml'`, `npmFiles: null
|
|
105
|
+
- Повертає об'єкт із `kind: 'python'`, `manifestRel: 'pyproject.toml'`, `npmFiles: null`, `maxBump: null` (стеля бампа підтримується лише для npm-маніфестів).
|
|
104
106
|
- **Side effects:**
|
|
105
107
|
- Синхронний `existsSync` (двічі: на `package.json` і `pyproject.toml`).
|
|
106
108
|
- Асинхронне читання файлу через `fs/promises.readFile`.
|
|
@@ -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 = new Set(['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.has(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
|
|
|
@@ -3,9 +3,7 @@ type: JS Module
|
|
|
3
3
|
title: release.mjs
|
|
4
4
|
resource: npm/rules/release/release.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
7
|
-
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
|
-
score: 100
|
|
6
|
+
crc: 3ac9ee4c
|
|
9
7
|
---
|
|
10
8
|
|
|
11
9
|
Файл автоматизує процес випуску версій. Він агрегує зміна-файли для всіх робочих просторів у `version-bump` та `CHANGELOG`, комітить зміни, ставить тег `<name>@<version>` та видаляє використані зміна-файли. Цей процес виконується у CI на гілці `main` (n-rules-release-design, варіант A). Публічні функції, що надає модуль, — `release` та `runReleaseCli`.
|
|
@@ -13,6 +11,9 @@ docgen:
|
|
|
13
11
|
## Поведінка
|
|
14
12
|
|
|
15
13
|
release агрегує change-файли для всіх робочих просторів, оновлює версії в маніфестах, додає секції до `CHANGELOG.md`, видаляє використані change-файли, створює коміт та анотовані теги для всіх зібраних релізів, а потім намагається пушити їх у апстрім з повторними спробами.
|
|
14
|
+
|
|
15
|
+
Обчислений bump обрізається зверху стелею `package.json#release.maxBump` робочого простору: наприклад, `major`-change-файл при стелі `minor` дає лише minor-реліз. Коли стеля реально спрацювала, у консоль друкується попередження з початковим bump, стелею та фінальною версією.
|
|
16
|
+
|
|
16
17
|
runReleaseCli виконує процес релізу, викликаючи функцію release, та виводить відповідний статус у консоль.
|
|
17
18
|
|
|
18
19
|
## Публічний API
|
|
@@ -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,
|
|
@@ -3,7 +3,7 @@ type: JS Module
|
|
|
3
3
|
title: aggregate.mjs
|
|
4
4
|
resource: npm/rules/release/lib/aggregate.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: ff9ff216
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
Модуль `aggregate.mjs` забезпечує **агрегацію change-файлів одного workspace** у дві основні сутності:
|
|
@@ -23,7 +23,8 @@ docgen:
|
|
|
23
23
|
| `maxBump(bumps)` | функція | Найвищий пріоритет бампа у списку (`major` > `minor` > `patch`) |
|
|
24
24
|
| `renderChangelogSection(version, date, entries)` | функція | Рендер однієї версійної секції у markdown |
|
|
25
25
|
| `prependChangelogSection(existingText, sectionBlock)` | функція | Вставка нової секції зверху наявного `CHANGELOG.md` |
|
|
26
|
-
| `
|
|
26
|
+
| `capBump(bump, cap)` | функція | Обрізає bump зверху стелею `release.maxBump` (`major` → `minor` тощо) |
|
|
27
|
+
| `aggregateWorkspace({ currentVersion, changeFiles, date, maxBumpCap })` | функція | Високорівневе об’єднання change-файлів workspace у `newVersion` + `sectionBlock` |
|
|
27
28
|
|
|
28
29
|
Усі експорти — **іменовані** (`export function …`). Default-export відсутній.
|
|
29
30
|
|
|
@@ -76,6 +77,14 @@ docgen:
|
|
|
76
77
|
- `maxBump(['patch'])` → `'patch'`
|
|
77
78
|
- `maxBump([])` → `'patch'` (через fallback `?? 'patch'`)
|
|
78
79
|
|
|
80
|
+
### `capBump(bump, cap)`
|
|
81
|
+
|
|
82
|
+
**Сигнатура:** `capBump(bump: string, cap?: string | null): string`
|
|
83
|
+
|
|
84
|
+
Обмежує обчислений bump **зверху** стелею з `package.json#release.maxBump`: якщо `bump` суворіший за `cap` (наприклад, `major` при `cap: 'minor'`), повертається `cap`; інакше — `bump` без змін. `cap` у `null`/`undefined` вимикає обмеження. Так пакет може заборонити автоматичну зміну власної major-версії, навіть якщо change-файл явно поставив `major`.
|
|
85
|
+
|
|
86
|
+
**Side effects:** немає.
|
|
87
|
+
|
|
79
88
|
### `renderChangelogSection(version, date, entries)`
|
|
80
89
|
|
|
81
90
|
**Сигнатура:** `renderChangelogSection(version: string, date: string, entries: Array<{ section: string, description: string }>): string`
|
|
@@ -152,7 +161,7 @@ docgen:
|
|
|
152
161
|
- `existingText = 'тут нічого корисного'`:
|
|
153
162
|
- вихід: `# Changelog\n\n<sectionBlock>` (попередній вміст відкидається).
|
|
154
163
|
|
|
155
|
-
### `aggregateWorkspace({ currentVersion, changeFiles, date })`
|
|
164
|
+
### `aggregateWorkspace({ currentVersion, changeFiles, date, maxBumpCap })`
|
|
156
165
|
|
|
157
166
|
**Сигнатура:**
|
|
158
167
|
|
|
@@ -161,6 +170,7 @@ aggregateWorkspace({
|
|
|
161
170
|
currentVersion: string,
|
|
162
171
|
changeFiles: Array<{ file: string, entry: { bump: string, section: string, description: string } }>,
|
|
163
172
|
date: string,
|
|
173
|
+
maxBumpCap?: string | null,
|
|
164
174
|
}): { newVersion: string, sectionBlock: string, consumedFiles: string[] } | null
|
|
165
175
|
```
|
|
166
176
|
|
|
@@ -169,12 +179,13 @@ aggregateWorkspace({
|
|
|
169
179
|
- `currentVersion` — поточна версія маніфесту workspace (`x.y.z`).
|
|
170
180
|
- `changeFiles` — масив об’єктів, що відповідає виходу `readChangeFiles` з `change-file.mjs`. Кожен елемент має `file` (ім’я файлу `.md`) та `entry` (розпарсений frontmatter + опис).
|
|
171
181
|
- `date` — рядок дати `YYYY-MM-DD` для секції CHANGELOG.
|
|
182
|
+
- `maxBumpCap` — стеля bump з `package.json#release.maxBump` (`'major' | 'minor' | 'patch'`); `null`/відсутній — без обмеження.
|
|
172
183
|
|
|
173
184
|
**Повертає:**
|
|
174
185
|
|
|
175
186
|
- `null`, якщо `changeFiles.length === 0` (явна ознака «нема чого релізити»).
|
|
176
187
|
- Інакше — об’єкт:
|
|
177
|
-
- `newVersion` — результат `bumpVersion(currentVersion, maxBump(<усі bumps>))`.
|
|
188
|
+
- `newVersion` — результат `bumpVersion(currentVersion, capBump(maxBump(<усі bumps>), maxBumpCap))`.
|
|
178
189
|
- `sectionBlock` — результат `renderChangelogSection(newVersion, date, <усі entries>)`.
|
|
179
190
|
- `consumedFiles` — імена change-файлів (`c.file`), які мають бути видалені викликачем після успішного запису маніфесту й `CHANGELOG.md`.
|
|
180
191
|
|
|
@@ -211,7 +222,7 @@ aggregateWorkspace({
|
|
|
211
222
|
1. **Збір change-файлів.** Викликач звертається до `readChangeFiles(ws, cwd)` з `change-file.mjs`, отримуючи `Array<{ file, entry }>` — усі `.md` із `<ws>/.changes/`, відсортовані за іменем (тобто де-факто за `timestamp`).
|
|
212
223
|
2. **Отримання поточної версії.** Викликач читає `package.json` (або інший маніфест) і дістає `currentVersion`.
|
|
213
224
|
3. **Дата.** Викликач формує `date = new Date().toISOString().slice(0, 10)` або еквівалент.
|
|
214
|
-
4. **Агрегація.** Виклик `aggregateWorkspace({ currentVersion, changeFiles, date })`:
|
|
225
|
+
4. **Агрегація.** Виклик `aggregateWorkspace({ currentVersion, changeFiles, date, maxBumpCap })`:
|
|
215
226
|
- Якщо повертає `null` — викликач пропускає workspace (нема `change-файлів`).
|
|
216
227
|
- Якщо повертає об’єкт — отримуємо `newVersion`, `sectionBlock`, `consumedFiles`.
|
|
217
228
|
5. **Оновлення `CHANGELOG.md`.** Викликач читає поточний `CHANGELOG.md` (або порожній рядок, якщо файлу нема), застосовує `prependChangelogSection(existingText, sectionBlock)` і записує результат на диск.
|
|
@@ -244,7 +255,7 @@ aggregateWorkspace({
|
|
|
244
255
|
5. Експортує функцію `prependChangelogSection(existingText, sectionBlock)`, яка:
|
|
245
256
|
- Якщо `existingText.trimStart()` не починається з `# Changelog`, повертає `# Changelog\n\n<sectionBlock>`.
|
|
246
257
|
- Інакше відокремлює перший рядок (`head`) і решту (`rest`, із `trimStart`) і повертає `head + '\n\n' + sectionBlock + '\n' + rest`.
|
|
247
|
-
6. Експортує функцію `aggregateWorkspace({ currentVersion, changeFiles, date })`, яка:
|
|
258
|
+
6. Експортує функцію `aggregateWorkspace({ currentVersion, changeFiles, date, maxBumpCap })`, яка:
|
|
248
259
|
- Повертає `null`, якщо `changeFiles` порожній.
|
|
249
|
-
- Інакше повертає `{ newVersion, sectionBlock, consumedFiles }`, де `newVersion = bumpVersion(currentVersion, maxBump(<усі c.entry.bump>))`, `sectionBlock = renderChangelogSection(newVersion, date, <усі c.entry>)`, `consumedFiles = <усі c.file>`.
|
|
260
|
+
- Інакше повертає `{ newVersion, sectionBlock, consumedFiles }`, де `newVersion = bumpVersion(currentVersion, capBump(maxBump(<усі c.entry.bump>), maxBumpCap))`, `sectionBlock = renderChangelogSection(newVersion, date, <усі c.entry>)`, `consumedFiles = <усі c.file>`.
|
|
250
261
|
7. Не виконує жодного I/O й не залежить від `node:*`.
|
|
@@ -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,7 +3,7 @@ type: JS Module
|
|
|
3
3
|
title: main.mjs
|
|
4
4
|
resource: npm/rules/text/run-v8r/main.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 804dce1c
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
## Огляд
|
|
@@ -23,6 +23,7 @@ docgen:
|
|
|
23
23
|
- `runV8rWithGlobs(globs?)` — послідовно запускає `v8r` для кожного glob-у з `V8R_CONFIG_FILE`, вказаним на резольвнутий конфіг; при коді 0/98 вивід приховано, інакше друкується й повертається код першого невдалого прогону.
|
|
24
24
|
- `runV8rWithFiles(files)` — delta-режим: один запуск `v8r` по конкретних існуючих шляхах (код 98 неможливий), порожній список — одразу 0.
|
|
25
25
|
- `warnAboutRemoteSchemaFallback(stderrText)` — парсить stderr `v8r` і на кожен файл, чию схему знайдено мережевим fallback-ом (schemastore, не customCatalog), друкує в stdout пораду додати схему в каталог.
|
|
26
|
+
- `stripBunNodeShimDirs(pathValue)` — прибирає з PATH shim-теки `bun-node-*`, які `bun run --bun` додає з підміненим `node`: дочірній `v8r` (node-shebang) інакше виконувався б під bun і падав на непідтримуваному `node:sea`. Дочірній процес `v8r` завжди отримує очищений PATH.
|
|
26
27
|
- `lint(ctx)` — detector `text/run-v8r`: `ctx.files` → delta по відфільтрованих розширеннях, без `ctx.files` → full за `DEFAULT_V8R_GLOBS`.
|
|
27
28
|
|
|
28
29
|
## Публічний API
|
|
@@ -32,6 +33,7 @@ docgen:
|
|
|
32
33
|
- `writeResolvedV8rConfig()` — матеріалізація тимчасового конфігу, повертає його шлях.
|
|
33
34
|
- `runV8rWithGlobs(globs?)`, `runV8rWithFiles(files)` — точки входу перевірки (full/delta).
|
|
34
35
|
- `warnAboutRemoteSchemaFallback(stderrText)` — попередження про мережевий fallback схем.
|
|
36
|
+
- `stripBunNodeShimDirs(pathValue)` — PATH без shim-тек `bun-node-*` (для дочірнього v8r).
|
|
35
37
|
- `lint(ctx)` — інтеграція в lint-пайплайн.
|
|
36
38
|
|
|
37
39
|
## Гарантії поведінки
|
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
import { spawnSync } from 'node:child_process'
|
|
48
48
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
49
49
|
import { tmpdir } from 'node:os'
|
|
50
|
-
import { dirname, isAbsolute, join } from 'node:path'
|
|
50
|
+
import { basename, delimiter, dirname, isAbsolute, join } from 'node:path'
|
|
51
|
+
import { env } from 'node:process'
|
|
51
52
|
import { fileURLToPath } from 'node:url'
|
|
52
53
|
|
|
53
54
|
import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
|
|
@@ -150,6 +151,21 @@ export function warnAboutRemoteSchemaFallback(stderrText) {
|
|
|
150
151
|
}
|
|
151
152
|
}
|
|
152
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Прибирає з PATH shim-теки `bun-node-*`: їх додає `bun run --bun`, підміняючи `node` через
|
|
156
|
+
* symlink на bun. `bun x v8r` поважає node-shebang і бере `node` з PATH — під shim v8r виконується bun-ом
|
|
157
|
+
* і падає на непідтримуваному `node:sea`, тому дочірній v8r має бачити справжній node.
|
|
158
|
+
* @param {string | undefined} pathValue значення PATH батьківського процесу
|
|
159
|
+
* @returns {string | undefined} PATH без shim-тек (undefined — якщо PATH не задано)
|
|
160
|
+
*/
|
|
161
|
+
export function stripBunNodeShimDirs(pathValue) {
|
|
162
|
+
if (!pathValue) return pathValue
|
|
163
|
+
return pathValue
|
|
164
|
+
.split(delimiter)
|
|
165
|
+
.filter(entry => !basename(entry).startsWith('bun-node-'))
|
|
166
|
+
.join(delimiter)
|
|
167
|
+
}
|
|
168
|
+
|
|
153
169
|
/**
|
|
154
170
|
* Один виклик `bun x v8r <targets...>` з підготовленим `customCatalog`-конфігом.
|
|
155
171
|
* @param {string[]} targets glob-и або конкретні шляхи файлів
|
|
@@ -165,7 +181,7 @@ function runOneV8rInvocation(targets, configPath, verbose = false) {
|
|
|
165
181
|
maxBuffer: 50 * 1024 * 1024,
|
|
166
182
|
shell: false,
|
|
167
183
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
168
|
-
env: { ...
|
|
184
|
+
env: { ...env, PATH: stripBunNodeShimDirs(env.PATH), V8R_CONFIG_FILE: configPath }
|
|
169
185
|
})
|
|
170
186
|
|
|
171
187
|
if (result.error) {
|
|
@@ -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: 30b6209c
|
|
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` — повноцінні раннери без попередження.
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
*
|
|
24
24
|
* Повертає той самий operations[]-контракт, що й single-shot — apply-логіка спільна.
|
|
25
25
|
*/
|
|
26
|
-
import { z }
|
|
26
|
+
// Namespace-імпорт замість `import { z }`: Bun показує фантомний `__esModule` на ESM-неймспейсах,
|
|
27
|
+
// через що interop у vitest приймає default-експорт zod за CJS-обгортку і губить named-експорт `z`.
|
|
28
|
+
import * as z from 'zod'
|
|
27
29
|
import { runOneShot } from '@7n/llm-lib/one-shot'
|
|
28
30
|
import { startChain } from '@7n/llm-lib/chain'
|
|
29
31
|
import { CLOUD_MIN, resolveModel } from '@7n/llm-lib/model-tiers'
|
|
@@ -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).'
|
|
@@ -42,7 +55,9 @@ const USAGE_LINES = [
|
|
|
42
55
|
* @returns {boolean} чи знайдено бінарник у PATH
|
|
43
56
|
*/
|
|
44
57
|
function isBinaryInPath(name) {
|
|
45
|
-
|
|
58
|
+
// Явний `env: process.env` — Bun без нього дає дітям snapshot оточення зі старту процесу
|
|
59
|
+
// і не бачить runtime-змін `process.env.PATH` (та сама причина, що в resolve-cmd.mjs).
|
|
60
|
+
const probe = spawnSync('command', ['-v', name], { shell: true, encoding: 'utf8', env: process.env })
|
|
46
61
|
return probe.status === 0
|
|
47
62
|
}
|
|
48
63
|
|
|
@@ -158,40 +173,33 @@ async function runPiRunner(prompt, rawSkillName, skillsRoot, projectDir, logErro
|
|
|
158
173
|
}
|
|
159
174
|
|
|
160
175
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
176
|
+
* Делегує виконання скіла у зовнішній non-interactive CLI (`cursor-agent -p`, `codex exec -`,
|
|
177
|
+
* або deprecated `claude -p`) — промпт передається через stdin. `claude` лишається як
|
|
178
|
+
* deprecated fallback для тих, у кого pi-модель ще не налаштована; буде прибрано
|
|
179
|
+
* (мігруй на `skill pi`). `cursor`/`codex` — повноцінні альтернативні раннери.
|
|
180
|
+
* @param {'claude' | 'cursor' | 'codex'} kind який LLM CLI запускати
|
|
164
181
|
* @param {string} prompt промпт для передачі у stdin
|
|
165
182
|
* @param {string} projectDir робочий каталог дочірнього процесу
|
|
166
183
|
* @param {(line: string) => void} logError вивід попередження/помилок
|
|
167
184
|
* @returns {number} exit code дочірнього процесу
|
|
168
185
|
*/
|
|
169
186
|
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
|
-
}
|
|
187
|
+
const runner = EXTERNAL_RUNNERS[kind]
|
|
176
188
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
cwd: projectDir,
|
|
180
|
-
stdio: ['pipe', 'inherit', 'inherit'],
|
|
181
|
-
encoding: 'utf8'
|
|
182
|
-
})
|
|
183
|
-
return result.status ?? 1
|
|
189
|
+
if (runner.deprecated) {
|
|
190
|
+
logError(`[deprecated] skill ${kind} → use 'skill pi'; зовнішній CLI буде прибрано`)
|
|
184
191
|
}
|
|
185
192
|
|
|
186
|
-
if (!isBinaryInPath(
|
|
187
|
-
throw new Error(
|
|
193
|
+
if (!isBinaryInPath(runner.bin)) {
|
|
194
|
+
throw new Error(`\`${runner.bin}\` not found in PATH. Install ${kind} CLI or use \`skill pi\`.`)
|
|
188
195
|
}
|
|
189
196
|
|
|
190
|
-
const result = spawnSync(
|
|
197
|
+
const result = spawnSync(runner.bin, runner.args, {
|
|
191
198
|
input: prompt,
|
|
192
199
|
cwd: projectDir,
|
|
193
200
|
stdio: ['pipe', 'inherit', 'inherit'],
|
|
194
|
-
encoding: 'utf8'
|
|
201
|
+
encoding: 'utf8',
|
|
202
|
+
env: process.env
|
|
195
203
|
})
|
|
196
204
|
return result.status ?? 1
|
|
197
205
|
}
|
|
@@ -244,7 +252,7 @@ export async function runSkillsCli(argv, options = {}) {
|
|
|
244
252
|
if (first === 'pi') {
|
|
245
253
|
return await runPiRunner(prompt, second, skillsRoot, projectDir, logError, deps)
|
|
246
254
|
}
|
|
247
|
-
return runLlmCli(/** @type {'claude' | 'cursor'} */ (first), prompt, projectDir, logError)
|
|
255
|
+
return runLlmCli(/** @type {'claude' | 'cursor' | 'codex'} */ (first), prompt, projectDir, logError)
|
|
248
256
|
}
|
|
249
257
|
|
|
250
258
|
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
|
-
}
|