@7n/rules 1.23.1 → 1.25.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 +13 -0
- package/bin/n-rules.js +55 -17
- package/package.json +1 -1
- package/scripts/lib/auto-worktree.mjs +157 -0
- package/scripts/lib/docs/auto-worktree.md +31 -0
- package/scripts/lib/docs/index.md +1 -0
- package/scripts/lib/docs/skill-fragments.md +27 -0
- package/scripts/lib/skill-fragments.mjs +59 -0
- package/skills/taze/SKILL.md +19 -133
- package/skills/taze/js/docs/orchestrate.md +1 -1
- package/skills/taze/js/orchestrate.mjs +13 -122
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.25.0] - 2026-07-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- taze: по завершенню переносить зміни з автоствореного worktree назад у вихідне дерево (untracked) і прибирає worktree
|
|
8
|
+
- lint --full: worktree-only гейт (auto-create + bring-back + cleanup), спільна логіка з taze винесена у scripts/lib/auto-worktree.mjs
|
|
9
|
+
|
|
10
|
+
## [1.24.0] - 2026-07-19
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Фаза 4b spec lang-plugins-extraction: SKILL-фрагменти плагінів — sync доклеює конвенційні `skills/<id>/SKILL.fragment.md` активних плагінів до синхронізованого SKILL.md між стабільними маркерами (ідемпотентний ре-синк, блок зникає без активних фрагментів). Мовні гілки taze SKILL.md (Rust: детекція/cargo-команди/перевірки; Python: uv-цикл/PEP-класифікація) переїхали у фрагменти lang-плагінів — канон ядра описує npm/bun-гілку і generic-скелет
|
|
15
|
+
|
|
3
16
|
## [1.23.1] - 2026-07-18
|
|
4
17
|
|
|
5
18
|
### Changed
|
package/bin/n-rules.js
CHANGED
|
@@ -84,12 +84,18 @@ import {
|
|
|
84
84
|
RULE_MIGRATIONS
|
|
85
85
|
} from '../scripts/auto-rules.mjs'
|
|
86
86
|
import { detectAutoSkills } from '../scripts/auto-skills.mjs'
|
|
87
|
+
import {
|
|
88
|
+
bringChangesBackToOriginal,
|
|
89
|
+
ensureRunningInWorktree,
|
|
90
|
+
removeAutoCreatedWorktree
|
|
91
|
+
} from '../scripts/lib/auto-worktree.mjs'
|
|
87
92
|
import { readSkillMetaRaw } from '../scripts/lib/skill-meta.mjs'
|
|
88
93
|
import { injectWorktreeNotice } from '../scripts/lib/worktree-notice.mjs'
|
|
94
|
+
import { collectSkillFragments, injectSkillFragments } from '../scripts/lib/skill-fragments.mjs'
|
|
89
95
|
import { injectRootNotice } from '../scripts/lib/root-notice.mjs'
|
|
90
96
|
import { listProjectRulesMdcFiles } from '../scripts/lib/list-project-rules-mdc.mjs'
|
|
91
97
|
import { ensureNRulesInRootDevDependencies } from '../scripts/ensure-n-rules-dev-dependencies.mjs'
|
|
92
|
-
import { resolvePluginList, resolveRulesDirs } from '../scripts/lib/resolve-plugins.mjs'
|
|
98
|
+
import { resolvePluginList, resolvePlugins, resolveRulesDirs } from '../scripts/lib/resolve-plugins.mjs'
|
|
93
99
|
import { assertCwdIsProjectRoot } from '../scripts/lib/assert-project-root.mjs'
|
|
94
100
|
import { syncClaudeConfig } from '../scripts/sync-claude-config.mjs'
|
|
95
101
|
import { syncGitignoreWorktree } from '../scripts/lib/sync-gitignore-worktree.mjs'
|
|
@@ -885,15 +891,19 @@ async function syncAgentsMd(agentsTemplatePath = BUNDLED_AGENTS_TEMPLATE_PATH) {
|
|
|
885
891
|
* Копіює лише skills зі списку configSkills (джерело: skills/<id>/ у пакеті)
|
|
886
892
|
* @param {string[]} configSkills id без префікса n-
|
|
887
893
|
* @param {string} [bundledSkillsDir] каталог `skills/` у корені пакету-джерела
|
|
894
|
+
* @param {{ plugins?: unknown } | null} [config] конфіг `.n-rules.json` — активні плагіни для SKILL-фрагментів
|
|
888
895
|
* @returns {Promise<{ success: number, fail: number }>} лічильники успішних і невдалих копіювань
|
|
889
896
|
*/
|
|
890
|
-
async function syncSkills(configSkills, bundledSkillsDir = BUNDLED_SKILLS_DIR) {
|
|
897
|
+
async function syncSkills(configSkills, bundledSkillsDir = BUNDLED_SKILLS_DIR, config = null) {
|
|
891
898
|
if (configSkills.length === 0 || !existsSync(bundledSkillsDir)) {
|
|
892
899
|
return { success: 0, fail: 0 }
|
|
893
900
|
}
|
|
894
901
|
|
|
895
902
|
const skillsRoot = join(cwd(), SKILLS_DIR)
|
|
896
903
|
await mkdir(skillsRoot, { recursive: true })
|
|
904
|
+
// Активні плагіни — джерело конвенційних фрагментів skills/<id>/SKILL.fragment.md
|
|
905
|
+
// (фаза 4b spec lang-plugins-extraction): мовні гілки скіла їдуть з плагіном.
|
|
906
|
+
const activePlugins = resolvePlugins(cwd(), config, { allowInstall: false, quiet: true })
|
|
897
907
|
|
|
898
908
|
let success = 0
|
|
899
909
|
let fail = 0
|
|
@@ -923,6 +933,7 @@ async function syncSkills(configSkills, bundledSkillsDir = BUNDLED_SKILLS_DIR) {
|
|
|
923
933
|
if (entry.name === 'SKILL.md') {
|
|
924
934
|
content = injectWorktreeNotice(content, worktree)
|
|
925
935
|
content = injectRootNotice(content, rootOnly)
|
|
936
|
+
content = injectSkillFragments(content, collectSkillFragments(id, activePlugins))
|
|
926
937
|
}
|
|
927
938
|
await writeFile(join(destDir, entry.name), content, 'utf8')
|
|
928
939
|
}
|
|
@@ -1487,7 +1498,7 @@ async function runSync() {
|
|
|
1487
1498
|
|
|
1488
1499
|
await runSyncStep('❌ Skills: ', async () => {
|
|
1489
1500
|
const { fail: skillFail } = await captureOutput(async () => {
|
|
1490
|
-
const { success: skillOk, fail } = await syncSkills(skills, bundledSkillsDir)
|
|
1501
|
+
const { success: skillOk, fail } = await syncSkills(skills, bundledSkillsDir, config)
|
|
1491
1502
|
if (skills.length > 0) {
|
|
1492
1503
|
console.log(`\n🧩 Skills: ${skillOk} скопійовано, ${fail} з помилками`)
|
|
1493
1504
|
}
|
|
@@ -1806,28 +1817,55 @@ try {
|
|
|
1806
1817
|
baseRef
|
|
1807
1818
|
}
|
|
1808
1819
|
const noFix = args.includes('--no-fix')
|
|
1820
|
+
// `--full` без `--no-fix` мутує ВЕСЬ репо (не лише дельту) — той самий клас
|
|
1821
|
+
// ризику, що й taze: якщо запущено поза .worktrees/, треба ізолювати. `--no-fix`
|
|
1822
|
+
// (detect-only, нуль мутацій) і не-full (дельта, за дизайном працює на живому
|
|
1823
|
+
// дереві задачі, worktree-ізоляція зламала б саму суть дельти) — пропускаємо.
|
|
1824
|
+
const needsWorktreeIsolation = full && !noFix
|
|
1825
|
+
const worktree = needsWorktreeIsolation
|
|
1826
|
+
? ensureRunningInWorktree(cwdArg, spawnSync, line => console.log(line), {
|
|
1827
|
+
suffix: 'lint',
|
|
1828
|
+
description: 'n-rules lint --full: worktree-only full-repo run'
|
|
1829
|
+
})
|
|
1830
|
+
: { cwd: cwdArg, autoCreated: false, branchArg: null }
|
|
1831
|
+
const runCwd = worktree.cwd
|
|
1809
1832
|
// Глобальна черга full-прогонів (spec 2026-07-03): одночасно виконується один
|
|
1810
1833
|
// `lint --full` на машину, паралельні --full чекають лока і бачать чергу та
|
|
1811
1834
|
// живий прогрес активного прогону; не-full запуски йдуть без лока
|
|
1812
1835
|
// (див. lint-lock.mjs). Publisher пише знімки прогресу для черги.
|
|
1813
1836
|
const { withGlobalLintLock, createProgressPublisher } =
|
|
1814
1837
|
await import('../scripts/lib/lint-surface/lint-lock.mjs')
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
if (
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1838
|
+
try {
|
|
1839
|
+
process.exitCode = await withGlobalLintLock({ ...lintOpts, cwd: runCwd, noFix }, async () => {
|
|
1840
|
+
const runOpts = { ...lintOpts, cwd: runCwd }
|
|
1841
|
+
const publisher = runOpts.full ? createProgressPublisher() : null
|
|
1842
|
+
if (publisher) runOpts.onProgress = publisher.onUpdate
|
|
1843
|
+
try {
|
|
1844
|
+
if (noFix) {
|
|
1845
|
+
const { detectAll } = await import('../scripts/lib/lint-surface/run-detectors.mjs')
|
|
1846
|
+
// окрема змінна замість (await detectAll(...)).exitCode — no-await-expression-member (oxlint)
|
|
1847
|
+
const detectResult = await detectAll(runOpts)
|
|
1848
|
+
return detectResult.exitCode
|
|
1849
|
+
}
|
|
1850
|
+
const { runFixPipeline } = await import('../scripts/lib/lint-surface/run-fix.mjs')
|
|
1851
|
+
return await runFixPipeline(runOpts)
|
|
1852
|
+
} finally {
|
|
1853
|
+
publisher?.stop()
|
|
1854
|
+
}
|
|
1855
|
+
})
|
|
1856
|
+
} finally {
|
|
1857
|
+
// Лише для АВТОстворених worktree (лінт уже сидів у своєму — не наш, не чіпаємо).
|
|
1858
|
+
if (worktree.autoCreated) {
|
|
1859
|
+
try {
|
|
1860
|
+
await bringChangesBackToOriginal(runCwd, cwdArg, spawnSync, line => console.log(line))
|
|
1861
|
+
} catch (error) {
|
|
1862
|
+
console.log(
|
|
1863
|
+
`⚠️ Перенесення змін назад провалилось: ${error instanceof Error ? error.message : String(error)}`
|
|
1864
|
+
)
|
|
1824
1865
|
}
|
|
1825
|
-
|
|
1826
|
-
return await runFixPipeline(runOpts)
|
|
1827
|
-
} finally {
|
|
1828
|
-
publisher?.stop()
|
|
1866
|
+
removeAutoCreatedWorktree(worktree.branchArg, cwdArg, spawnSync, line => console.log(line))
|
|
1829
1867
|
}
|
|
1830
|
-
}
|
|
1868
|
+
}
|
|
1831
1869
|
|
|
1832
1870
|
break
|
|
1833
1871
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/** @see ./docs/auto-worktree.md */
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { copyFile, mkdir, rm } from 'node:fs/promises'
|
|
4
|
+
import { dirname, join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Гарантує, що подальші кроки виконуються в ізольованому worktree
|
|
8
|
+
* (`main.json.worktree: true`-контракт), навіть коли викликач — детермінований
|
|
9
|
+
* JS-код, що не годує SKILL.md жодному LLM-агенту (тож агентський preflight-блок
|
|
10
|
+
* з `worktree-notice.mjs` нікому виконувати). Якщо `cwd` вже під `.worktrees/` —
|
|
11
|
+
* повертає його без змін. Інакше сам створює `.worktrees/<branch>-<suffix>`
|
|
12
|
+
* (`npx \@7n/mt worktree create`) і ставить залежності (`bun install`).
|
|
13
|
+
*
|
|
14
|
+
* **Гейт на чисте дерево.** Auto-create читає стан і потім переносить зміни
|
|
15
|
+
* назад копіюванням файлів (`bringChangesBackToOriginal`) — а не git merge.
|
|
16
|
+
* Якщо у вихідному `cwd` вже є незакомічені зміни, вони НЕ потраплять у щойно
|
|
17
|
+
* створений worktree (той — checkout HEAD), і перенесення назад мовчки
|
|
18
|
+
* затерло б їх версією з worktree. Тому за замовчуванням (`requireCleanTree:
|
|
19
|
+
* true`) auto-create кидає на брудному дереві замість ризикувати чужими
|
|
20
|
+
* незакоміченими правками. Викликач, що гарантує чистоту дерева сам (наприклад
|
|
21
|
+
* taze — SKILL.md вимагає цього як передумову ще ДО виклику), може передати
|
|
22
|
+
* `requireCleanTree: false`, щоб не платити за зайву git-команду.
|
|
23
|
+
* @param {string} cwd каталог для перевірки
|
|
24
|
+
* @param {typeof import('node:child_process').spawnSync} spawnFn інжект для тестів
|
|
25
|
+
* @param {(line: string) => void} log колбек прогресу
|
|
26
|
+
* @param {{ suffix: string, description: string, requireCleanTree?: boolean }} opts `suffix` — коротка (до 10 символів) назва задачі для `<branch>-<suffix>`; `description` — текст для `npx \@7n/mt worktree create`
|
|
27
|
+
* @returns {{ cwd: string, autoCreated: boolean, branchArg: string|null }} `autoCreated: false` — `cwd` без змін
|
|
28
|
+
* (вже worktree); `autoCreated: true` — `cwd` щойно створеного worktree і `branchArg`, з яким його створено
|
|
29
|
+
*/
|
|
30
|
+
export function ensureRunningInWorktree(cwd, spawnFn, log, { suffix, description, requireCleanTree = true }) {
|
|
31
|
+
const toplevelResult = spawnFn('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8' })
|
|
32
|
+
const toplevel = toplevelResult.status === 0 ? toplevelResult.stdout.trim() : ''
|
|
33
|
+
const segments = new Set(toplevel.replaceAll('\\', '/').split('/'))
|
|
34
|
+
if (segments.has('.worktrees')) return { cwd, autoCreated: false, branchArg: null }
|
|
35
|
+
|
|
36
|
+
const branchResult = spawnFn('git', ['branch', '--show-current'], { cwd, encoding: 'utf8' })
|
|
37
|
+
const currentBranch = branchResult.status === 0 ? branchResult.stdout.trim() : ''
|
|
38
|
+
if (!currentBranch) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`"${cwd}" не в ізольованому worktree (git toplevel: "${toplevel || '?'}"), і поточну гілку визначити не вдалось ` +
|
|
41
|
+
'(detached HEAD?) — автоматичне створення worktree за конвенцією `<current-branch>-<suffix>` неможливе. Перейди на гілку вручну.'
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (requireCleanTree) {
|
|
46
|
+
const statusResult = spawnFn('git', ['status', '--porcelain'], { cwd, encoding: 'utf8' })
|
|
47
|
+
if (statusResult.status === 0 && statusResult.stdout.trim().length > 0) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`"${cwd}" не в ізольованому worktree і має незакомічені зміни — auto-create worktree тут НЕБЕЗПЕЧНИЙ: ` +
|
|
50
|
+
'перенесення результату назад копіюванням файлів затерло б ці незакомічені правки версією зі свіжого ' +
|
|
51
|
+
'checkout (worktree = HEAD, без твоїх правок). Закомить/застеш зміни або створи worktree вручну.'
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const branchArg = `${currentBranch}-${suffix}`
|
|
57
|
+
const pathSegment = branchArg.replaceAll('/', '-')
|
|
58
|
+
log(`⚠️ "${cwd}" не в ізольованому worktree — створюю ".worktrees/${pathSegment}"...`)
|
|
59
|
+
runCommand('npx', ['@7n/mt', 'worktree', 'create', branchArg, description], cwd, spawnFn)
|
|
60
|
+
|
|
61
|
+
const newCwd = join(cwd, '.worktrees', pathSegment)
|
|
62
|
+
log('📥 bun install (bootstrap нового дерева)...')
|
|
63
|
+
runCommand('bun', ['install'], newCwd, spawnFn)
|
|
64
|
+
return { cwd: newCwd, autoCreated: true, branchArg }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Переносить зміни з автоствореного worktree назад у вихідне дерево як
|
|
69
|
+
* **untracked/незакомічені** правки (просте копіювання файлів, без git
|
|
70
|
+
* merge/cherry-pick). Джерело істини — `git status --porcelain` у worktree:
|
|
71
|
+
* для кожного шляху копіює файл, якщо він існує (модифікація/додавання), або
|
|
72
|
+
* видаляє його у вихідному дереві, якщо existsSync каже, що в worktree його
|
|
73
|
+
* вже нема (видалення). Перейменування (`old -> new` у porcelain) переносять
|
|
74
|
+
* лише нову назву — стара лишається як була, прийнятний компроміс для
|
|
75
|
+
* інструментів, що самі файли не перейменовують (ефект можливий лише як
|
|
76
|
+
* побічний результат LLM-рефакторингу чи форматера).
|
|
77
|
+
* @param {string} worktreeCwd автостворений worktree, з якого переносимо
|
|
78
|
+
* @param {string} originalCwd вихідне дерево, куди переносимо
|
|
79
|
+
* @param {typeof import('node:child_process').spawnSync} spawnFn інжект для тестів
|
|
80
|
+
* @param {(line: string) => void} log колбек прогресу
|
|
81
|
+
* @param {{ copyFile?: (src: string, dest: string) => Promise<void>, rm?: (path: string, opts?: object) => Promise<void>, mkdir?: (path: string, opts?: object) => Promise<void> }} [deps] інжекти для тестів
|
|
82
|
+
* @returns {Promise<string[]>} відносні шляхи перенесених файлів
|
|
83
|
+
*/
|
|
84
|
+
export async function bringChangesBackToOriginal(worktreeCwd, originalCwd, spawnFn, log, deps = {}) {
|
|
85
|
+
const copy = deps.copyFile ?? copyFile
|
|
86
|
+
const removeFile = deps.rm ?? rm
|
|
87
|
+
const makeDir = deps.mkdir ?? mkdir
|
|
88
|
+
|
|
89
|
+
const statusResult = spawnFn('git', ['status', '--porcelain'], { cwd: worktreeCwd, encoding: 'utf8' })
|
|
90
|
+
if (statusResult.status !== 0) {
|
|
91
|
+
log(
|
|
92
|
+
`⚠️ Не вдалось прочитати git status у "${worktreeCwd}" — зміни НЕ перенесені назад, worktree лишиться для ручного розбору.`
|
|
93
|
+
)
|
|
94
|
+
return []
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const lines = statusResult.stdout.split('\n').filter(Boolean)
|
|
98
|
+
if (lines.length === 0) {
|
|
99
|
+
log('ℹ️ Worktree без змін — нічого переносити назад.')
|
|
100
|
+
return []
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const brought = []
|
|
104
|
+
for (const line of lines) {
|
|
105
|
+
const rest = line.slice(3)
|
|
106
|
+
const relPath = rest.includes(' -> ') ? rest.split(' -> ', 2)[1] : rest
|
|
107
|
+
const srcPath = join(worktreeCwd, relPath)
|
|
108
|
+
const destPath = join(originalCwd, relPath)
|
|
109
|
+
if (existsSync(srcPath)) {
|
|
110
|
+
await makeDir(dirname(destPath), { recursive: true })
|
|
111
|
+
await copy(srcPath, destPath)
|
|
112
|
+
} else {
|
|
113
|
+
await removeFile(destPath, { force: true })
|
|
114
|
+
}
|
|
115
|
+
brought.push(relPath)
|
|
116
|
+
}
|
|
117
|
+
log(`📤 Перенесено назад у "${originalCwd}" як untracked: ${brought.join(', ')}`)
|
|
118
|
+
return brought
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Прибирає автостворений worktree разом з його ефемерною git-гілкою
|
|
123
|
+
* (`npx \@7n/mt worktree remove <branch>`) — викликати лише ПІСЛЯ
|
|
124
|
+
* `bringChangesBackToOriginal`, інакше зміни згорять разом з деревом.
|
|
125
|
+
* Не кидає при провалі — це прибирання, а не крок, від якого залежить
|
|
126
|
+
* результат прогону; провал лише логується, worktree лишається для
|
|
127
|
+
* ручного розбору.
|
|
128
|
+
* @param {string} branchArg гілка, з якою worktree був створений (з `ensureRunningInWorktree`)
|
|
129
|
+
* @param {string} originalCwd вихідне дерево, звідки виконати `npx \@7n/mt worktree remove`
|
|
130
|
+
* @param {typeof import('node:child_process').spawnSync} spawnFn інжект для тестів
|
|
131
|
+
* @param {(line: string) => void} log колбек прогресу
|
|
132
|
+
* @returns {void}
|
|
133
|
+
*/
|
|
134
|
+
export function removeAutoCreatedWorktree(branchArg, originalCwd, spawnFn, log) {
|
|
135
|
+
log(`🧹 Прибираю автостворений worktree "${branchArg}"...`)
|
|
136
|
+
const result = spawnFn('npx', ['@7n/mt', 'worktree', 'remove', branchArg], { cwd: originalCwd, encoding: 'utf8' })
|
|
137
|
+
if (result.status !== 0) {
|
|
138
|
+
log(`⚠️ Не вдалось прибрати worktree "${branchArg}" — приберіть вручну (${result.stderr || result.stdout})`)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Синхронно виконує детерміновану команду (bunx/bun/npx), кидає з
|
|
144
|
+
* exit-кодом+stderr при провалі.
|
|
145
|
+
* @param {string} cmd бінарник
|
|
146
|
+
* @param {string[]} args аргументи
|
|
147
|
+
* @param {string} cwd робочий каталог
|
|
148
|
+
* @param {typeof import('node:child_process').spawnSync} spawnFn інжект для тестів
|
|
149
|
+
* @returns {string} stdout
|
|
150
|
+
*/
|
|
151
|
+
function runCommand(cmd, args, cwd, spawnFn) {
|
|
152
|
+
const result = spawnFn(cmd, args, { cwd, encoding: 'utf8' })
|
|
153
|
+
if (result.status !== 0) {
|
|
154
|
+
throw new Error(`${cmd} ${args.join(' ')} → exit ${result.status}: ${result.stderr || result.stdout}`)
|
|
155
|
+
}
|
|
156
|
+
return result.stdout
|
|
157
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: auto-worktree.mjs
|
|
4
|
+
resource: npm/scripts/lib/auto-worktree.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 8d55dfc5
|
|
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
|
+
Файл керує запуском у ізольованому worktree, щоб підготувати зміни в окремому середовищі, потім викликати bringChangesBackToOriginal для повернення результату до вихідного дерева і removeAutoCreatedWorktree для очищення тимчасового worktree. Поведінка спирається на конфіг main.json.
|
|
16
|
+
|
|
17
|
+
## Поведінка
|
|
18
|
+
|
|
19
|
+
- `ensureRunningInWorktree` — перевіряє, чи виконання вже йде в ізольованому worktree; якщо ні, за потреби створює його для поточної гілки, ставить залежності й повертає новий `cwd`.
|
|
20
|
+
- `bringChangesBackToOriginal` — переносить зміни з автоствореного worktree назад у вихідне дерево як untracked/незакомічені правки, включно з додаванням, оновленням і видаленням файлів; перейменування переносить лише в нову назву.
|
|
21
|
+
- `removeAutoCreatedWorktree` — прибирає автостворений worktree разом із його ефемерною гілкою після перенесення змін назад, а у разі збою лише повідомляє про ручне прибирання.
|
|
22
|
+
|
|
23
|
+
## Публічний API
|
|
24
|
+
|
|
25
|
+
- ensureRunningInWorktree — Гарантує ізольований worktree для наступних кроків; якщо вже працюєш у `.worktrees/`, лишає поточний каталог без змін, інакше створює окремий worktree та ставить залежності. За замовчуванням блокує запуск на брудному дереві, щоб не втратити незакомічені правки; це можна послабити лише там, де чистоту вже забезпечили іншим способом.
|
|
26
|
+
- bringChangesBackToOriginal — Повертає зміни з автоствореного worktree у початкове дерево простим копіюванням файлів: нові й змінені файли переносять, видалені — прибирають у вихідному дереві. Працює як перенесення фактичного стану, а не як merge.
|
|
27
|
+
- removeAutoCreatedWorktree — Прибирає тимчасовий worktree та пов’язану з ним ephemeral branch після того, як зміни вже перенесено назад; якщо прибирання не вдалось, залишає worktree для ручного розбору.
|
|
28
|
+
|
|
29
|
+
## Гарантії поведінки
|
|
30
|
+
|
|
31
|
+
- (специфічних машинно-виведених гарантій немає)
|
|
@@ -8,6 +8,7 @@ resource: npm/scripts/lib/
|
|
|
8
8
|
| --------------------------------------------------------------------------- | --------- |
|
|
9
9
|
| [acp-runner.mjs](acp-runner.md) | JS Module |
|
|
10
10
|
| [assert-project-root.mjs](assert-project-root.md) | JS Module |
|
|
11
|
+
| [auto-worktree.mjs](auto-worktree.md) | JS Module |
|
|
11
12
|
| [blue-oak.mjs](blue-oak.md) | JS Module |
|
|
12
13
|
| [changed-files.mjs](changed-files.md) | JS Module |
|
|
13
14
|
| [check-reporter.mjs](check-reporter.md) | JS Module |
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: skill-fragments.mjs
|
|
4
|
+
resource: npm/scripts/lib/skill-fragments.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 8b9c6f98
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Огляд
|
|
10
|
+
|
|
11
|
+
Фрагменти SKILL.md від плагінів (фаза 4b spec lang-plugins-extraction): плагін шипить конвенційний файл `skills/<skillId>/SKILL.fragment.md` із власною секцією скіла (напр. Rust-гілку taze), а синк доклеює фрагменти активних плагінів до скопійованого `SKILL.md` між стабільними маркерами. Так мовні знання скіла їдуть разом із кодом плагіна, а не сиротіють у ядрі.
|
|
12
|
+
|
|
13
|
+
## Поведінка
|
|
14
|
+
|
|
15
|
+
- **collectSkillFragments** — збирає фрагменти скіла з активних плагінів у порядку списку плагінів; відсутній чи порожній файл фрагмента мовчки пропускається.
|
|
16
|
+
- **injectSkillFragments** — вшиває блок фрагментів у текст SKILL.md між маркерами `n-rules:plugin-fragments:start/end` (кожен фрагмент — у власних маркерах з іменем плагіна). Ре-синк ідемпотентний: наявний блок замінюється повністю; порожній список фрагментів — блок прибирається зовсім, текст без блоку не змінюється.
|
|
17
|
+
|
|
18
|
+
## Публічний API
|
|
19
|
+
|
|
20
|
+
- FRAGMENTS_START / FRAGMENTS_END — стабільні маркери блоку фрагментів.
|
|
21
|
+
- collectSkillFragments(skillId, plugins) — знайдені фрагменти `{ pluginName, content }`.
|
|
22
|
+
- injectSkillFragments(content, fragments) — текст SKILL.md з актуальним блоком.
|
|
23
|
+
|
|
24
|
+
## Гарантії поведінки
|
|
25
|
+
|
|
26
|
+
- Read-only щодо ФС: лише читає файли фрагментів; запис робить викликач (синк).
|
|
27
|
+
- Ідемпотентність вшивання: повторний виклик із тим самим списком не змінює текст.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Фрагменти SKILL.md від плагінів (фаза 4b spec lang-plugins-extraction).
|
|
3
|
+
*
|
|
4
|
+
* Плагін шипить конвенційний файл `skills/<skillId>/SKILL.fragment.md` —
|
|
5
|
+
* власну секцію скіла (напр. Rust-гілку taze). Під час синку скіла ядро
|
|
6
|
+
* доклеює фрагменти АКТИВНИХ плагінів до скопійованого `SKILL.md` між
|
|
7
|
+
* стабільними маркерами — ре-синк ідемпотентний: наявний блок замінюється
|
|
8
|
+
* повністю, без активних фрагментів — видаляється. Так мовні знання їдуть
|
|
9
|
+
* разом із кодом плагіна, а не сиротіють у ядрі.
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
12
|
+
import { join } from 'node:path'
|
|
13
|
+
|
|
14
|
+
/** Маркер початку блоку плагінних фрагментів (стабільний). */
|
|
15
|
+
export const FRAGMENTS_START = '<!-- n-rules:plugin-fragments:start -->'
|
|
16
|
+
/** Маркер кінця блоку плагінних фрагментів. */
|
|
17
|
+
export const FRAGMENTS_END = '<!-- n-rules:plugin-fragments:end -->'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Збирає фрагменти скіла з активних плагінів (у порядку списку плагінів).
|
|
21
|
+
* @param {string} skillId id скіла без префікса (напр. `taze`)
|
|
22
|
+
* @param {Array<{ name: string, packageRoot: string }>} plugins активні плагіни (з `resolvePlugins`)
|
|
23
|
+
* @returns {Array<{ pluginName: string, content: string }>} знайдені фрагменти
|
|
24
|
+
*/
|
|
25
|
+
export function collectSkillFragments(skillId, plugins) {
|
|
26
|
+
const out = []
|
|
27
|
+
for (const plugin of plugins) {
|
|
28
|
+
const fragmentPath = join(plugin.packageRoot, 'skills', skillId, 'SKILL.fragment.md')
|
|
29
|
+
if (!existsSync(fragmentPath)) continue
|
|
30
|
+
const content = readFileSync(fragmentPath, 'utf8').trim()
|
|
31
|
+
if (content !== '') out.push({ pluginName: plugin.name, content })
|
|
32
|
+
}
|
|
33
|
+
return out
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Вшиває блок фрагментів у текст SKILL.md (перед фінальним переносом рядка).
|
|
38
|
+
* Наявний блок між маркерами замінюється; порожній список фрагментів — блок
|
|
39
|
+
* прибирається зовсім.
|
|
40
|
+
* @param {string} content текст SKILL.md
|
|
41
|
+
* @param {Array<{ pluginName: string, content: string }>} fragments зібрані фрагменти
|
|
42
|
+
* @returns {string} текст із актуальним блоком фрагментів
|
|
43
|
+
*/
|
|
44
|
+
export function injectSkillFragments(content, fragments) {
|
|
45
|
+
const startIdx = content.indexOf(FRAGMENTS_START)
|
|
46
|
+
const endIdx = content.indexOf(FRAGMENTS_END)
|
|
47
|
+
let base = content
|
|
48
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
49
|
+
base = content.slice(0, startIdx) + content.slice(endIdx + FRAGMENTS_END.length)
|
|
50
|
+
base = `${base.trimEnd()}\n`
|
|
51
|
+
}
|
|
52
|
+
if (fragments.length === 0) return base
|
|
53
|
+
|
|
54
|
+
const parts = fragments.map(
|
|
55
|
+
f => `<!-- n-rules:plugin:${f.pluginName}:start -->\n\n${f.content}\n\n<!-- n-rules:plugin:${f.pluginName}:end -->`
|
|
56
|
+
)
|
|
57
|
+
const block = `${FRAGMENTS_START}\n\n${parts.join('\n\n')}\n\n${FRAGMENTS_END}`
|
|
58
|
+
return `${base.trimEnd()}\n\n${block}\n`
|
|
59
|
+
}
|
package/skills/taze/SKILL.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: n-taze
|
|
3
3
|
description: >-
|
|
4
|
-
Оновлення версій модулів проекту (bun/npm
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
Оновлення версій модулів проекту (bun/npm вбудовано; інші екосистеми — Rust,
|
|
5
|
+
Python тощо — через активні lang-плагіни) з аналізом major-змін і
|
|
6
|
+
автоматичним рефакторингом несумісного коду
|
|
7
7
|
version: '1.2'
|
|
8
8
|
---
|
|
9
9
|
|
|
@@ -11,63 +11,42 @@ version: '1.2'
|
|
|
11
11
|
|
|
12
12
|
## Мета
|
|
13
13
|
|
|
14
|
-
Оновити всі модулі проекту (npm/bun
|
|
14
|
+
Оновити всі модулі проекту (npm/bun-залежності вбудовано; інші екосистеми — за активними lang-плагінами, їхні гілки описані у фрагментах наприкінці цього файлу) до останніх версій, виявити major-оновлення, перевірити сумісність змін з кодом проекту і за потреби зрефакторити несумісні місця.
|
|
15
15
|
|
|
16
|
-
## Оркестрація (npm/bun
|
|
16
|
+
## Оркестрація (npm/bun + екосистемні гілки плагінів) — не одним промптом
|
|
17
17
|
|
|
18
18
|
`npx @7n/rules skill pi|cursor|codex taze` **не** передає цей файл одним суцільним
|
|
19
19
|
промптом в один агентський хід — так робив старий дизайн, і саме тому реальний
|
|
20
20
|
прогін міг зависати без жодної діагностики (один величезний непрозорий хід на
|
|
21
21
|
весь монорепо, без проміжних зупинок для перевірки прогресу). Замість цього `npm/skills/taze/js/orchestrate.mjs`:
|
|
22
22
|
|
|
23
|
-
1. Детерміновано, без LLM, виконує кроки 1-3
|
|
24
|
-
2. Для кожного **окремого** major
|
|
23
|
+
1. Детерміновано, без LLM, виконує кроки 1-3 нижче: npm/bun вбудовано (бекап → `bunx taze -w -r latest` → `bun install` → `n-rules taze diff`), а кожну екосистемну гілку — через її EcosystemProvider (detect → backup → bump → diff; деталі команд — у фрагменті відповідного плагіна наприкінці файлу).
|
|
24
|
+
2. Для кожного **окремого** major-запису з diff-у — один ізольований, обмежений виклик обраного раннера (кроки 4-6, лише для цього запису; промпт генерує `buildDependencyPrompt` ядра або `promptFor` провайдера, не цей SKILL.md).
|
|
25
25
|
3. Детерміновано прибирає бекапи (крок 7) і компонує звіт (крок 8) з результатів усіх ітерацій.
|
|
26
26
|
|
|
27
27
|
Не-npm екосистеми оркестратор веде через **EcosystemProvider-порт**
|
|
28
28
|
(`@7n/rules/plugin-api`, spec `docs/specs/2026-07-18-lang-plugins-extraction-spec.md`)
|
|
29
|
-
—
|
|
30
|
-
(extension-point `taze`)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
Якщо `Cargo.toml` знайдені, але `cargo-edit` не встановлено — Rust-гілка **не**
|
|
35
|
-
блокує інші гілки: оркестратор пропускає Rust-обробку і в звіті перелічує
|
|
36
|
-
знайдені `Cargo.toml` як такі, що потребують ручного прогону (нижче). Так само,
|
|
37
|
-
якщо `pyproject.toml` знайдений, але `uv` не встановлено — Python-гілка **не**
|
|
38
|
-
блокує інші гілки.
|
|
29
|
+
— lang-плагіни доставляються автоматично при першому запуску за файловим
|
|
30
|
+
сигналом (extension-point `taze`) і додають свої гілки як фрагменти цього
|
|
31
|
+
SKILL.md (блок наприкінці файлу). На чисто-JS проєкті без таких сигналів у
|
|
32
|
+
звіті немає навіть згадки про інші екосистеми. Якщо тулчейн гілки не
|
|
33
|
+
встановлено — вона пропускається з причиною у звіті, не блокуючи інші.
|
|
39
34
|
|
|
40
35
|
Переваги: падіння/timeout на одному пакеті/крейті не втрачає прогрес по інших;
|
|
41
36
|
кожен виклик успадковує власний timeout раннера (короткий, бо скоуп малий —
|
|
42
37
|
один запис, не весь монорепо); видно прогрес по записах, а не чорну скриньку.
|
|
43
38
|
|
|
44
|
-
Кроки 1-8 нижче лишаються джерелом правди щодо ЗМІСТУ роботи
|
|
45
|
-
|
|
46
|
-
один запис (4-6)
|
|
39
|
+
Кроки 1-8 нижче лишаються джерелом правди щодо ЗМІСТУ роботи npm/bun-гілки
|
|
40
|
+
(і скелетом для екосистемних) — оркестратор їх виконує програмно (1-3/7/8)
|
|
41
|
+
або як промпт на один запис (4-6). Мовні відповідники кроків — у фрагментах
|
|
42
|
+
плагінів наприкінці файлу.
|
|
47
43
|
|
|
48
44
|
## Передумови
|
|
49
45
|
|
|
50
46
|
- Чисте робоче дерево (`git status` без незакомічених змін у `package.json` / `bun.lock` / `node_modules`) — інакше різницю не відрізнити від оновлення.
|
|
51
47
|
- Встановлений `bun` і доступний `bunx`.
|
|
52
48
|
- Запуск з кореня проекту (де лежить `package.json` / `bun.lock`).
|
|
53
|
-
-
|
|
54
|
-
- Якщо в проекті є кореневий `pyproject.toml` — бажано встановлений `uv`. Без нього major-бампи Python-залежностей неможливо застосувати детерміновано — оркестратор пропускає Python-гілку (без блокування інших гілок) і перелічує `pyproject.toml` у звіті як такий, що потребує ручного прогону кроків 2-8 нижче.
|
|
55
|
-
|
|
56
|
-
### 0.2. Детекція Rust-крейтів
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
find . -name Cargo.toml -not -path "*/node_modules/*" -not -path "*/.worktrees/*" -not -path "*/target/*"
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
Якщо список непорожній — паралельно з npm-гілкою виконуються кроки 1–8 у Rust-варіанті (позначені нижче як «Rust-гілка»). Якщо порожній — Rust-кроки повністю пропускаються.
|
|
63
|
-
|
|
64
|
-
### 0.3. Детекція Python/uv-проекту
|
|
65
|
-
|
|
66
|
-
```bash
|
|
67
|
-
test -f pyproject.toml && echo pyproject.toml
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
v1: лише кореневий `pyproject.toml` (uv-конвенція single-project, без обходу workspace-членів, на відміну від Cargo). Якщо файл існує — паралельно виконуються кроки 1–8 у Python-варіанті (позначені нижче як «Python-гілка»). Якщо відсутній — Python-кроки повністю пропускаються.
|
|
49
|
+
- Передумови екосистемних гілок (інструменти на кшталт cargo-edit чи uv) — у фрагментах плагінів наприкінці файлу; відсутній тулчейн не блокує інші гілки.
|
|
71
50
|
|
|
72
51
|
## Workflow
|
|
73
52
|
|
|
@@ -82,20 +61,6 @@ cp bun.lock bun.lock.taze-bak
|
|
|
82
61
|
|
|
83
62
|
(У monorepo — також усі `*/package.json` воркспейсів. Файли тимчасові, видалити в кінці.)
|
|
84
63
|
|
|
85
|
-
**Rust-гілка** — для кожного знайденого на кроці 0.2 `Cargo.toml` (включно з кореневим, якщо є):
|
|
86
|
-
|
|
87
|
-
```bash
|
|
88
|
-
cp Cargo.toml Cargo.toml.taze-bak
|
|
89
|
-
cp Cargo.lock Cargo.lock.taze-bak # якщо lock-файл спільний на workspace — достатньо одного бекапу в корені
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
**Python-гілка** — якщо на кроці 0.3 знайдений `pyproject.toml`:
|
|
93
|
-
|
|
94
|
-
```bash
|
|
95
|
-
cp pyproject.toml pyproject.toml.taze-bak
|
|
96
|
-
cp uv.lock uv.lock.taze-bak # якщо є
|
|
97
|
-
```
|
|
98
|
-
|
|
99
64
|
### 2. Запустити оновлення
|
|
100
65
|
|
|
101
66
|
```bash
|
|
@@ -107,27 +72,6 @@ bun install
|
|
|
107
72
|
- `-r` — рекурсивно по всіх воркспейсах.
|
|
108
73
|
- `latest` — піднімати навіть major.
|
|
109
74
|
|
|
110
|
-
**Rust-гілка:**
|
|
111
|
-
|
|
112
|
-
```bash
|
|
113
|
-
cargo upgrade --incompatible allow
|
|
114
|
-
cargo update
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
- `cargo upgrade` (з `cargo-edit`) переписує вимоги версій у кожному `Cargo.toml` workspace-у на останні; `--incompatible allow` явно дозволяє перетинати major-межу (аналог `-r latest` у taze) — без цього флага incompatible-оновлення за замовчуванням ігноруються.
|
|
118
|
-
- `cargo update` після цього синхронізує `Cargo.lock` з новими вимогами.
|
|
119
|
-
|
|
120
|
-
**Python-гілка:**
|
|
121
|
-
|
|
122
|
-
```bash
|
|
123
|
-
for pkg in $(<список прямих залежностей із [project].dependencies>); do
|
|
124
|
-
uv remove "$pkg"
|
|
125
|
-
uv add "$pkg" --bounds lower
|
|
126
|
-
done
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
`uv` **не має** єдиної команди "підняти все до latest, навіть через major" (на відміну від `bunx taze -w -r latest`/`cargo upgrade --incompatible allow`) — підтверджено емпірично: `uv add <pkg>` на вже присутній залежності НЕ переписує specifier без попереднього `uv remove`. `--bounds lower` записує нижню межу без верхньої (`>=<latest>`), коректно зберігає `[extras]`. Провал одного пакета (мережа/резолюція) не втрачає прогрес по інших — оркестратор best-effort відновлює оригінальний рядок, якщо `uv add` не вдався після `uv remove`.
|
|
130
|
-
|
|
131
75
|
### 3. Виявити major-оновлення
|
|
132
76
|
|
|
133
77
|
> **Не порівнюй `package.json` вручну.** Класифікацію semver несе CLI — детерміновано, по всіх воркспейсах.
|
|
@@ -140,10 +84,6 @@ n-rules taze diff
|
|
|
140
84
|
|
|
141
85
|
Покриває **прямі** залежності з `package.json` (root + воркспейси). Транзитивні major-стрибки (`bun.lock`) — за потреби переглянь окремо; основний ризик breaking-змін — у прямих.
|
|
142
86
|
|
|
143
|
-
**Rust-гілка (оркестровано)** — `collectCargoDiff` (`npm/skills/taze/js/cargo-diff.mjs`) робить те саме детерміновано: парсить кожен `Cargo.toml.taze-bak`/`Cargo.toml` через `smol-toml`, порівнює `dependencies`/`dev-dependencies`/`build-dependencies` (рядок чи `{ version = "...", features = [...] }`), класифікує за тим самим правилом caret-семантики — Cargo-скорочені версії (`"1"`, `"0.4"`) трактуються як відсутні компоненти = 0. Ручний прогін поза оркестратором — той самий принцип: `diff Cargo.toml.taze-bak Cargo.toml` по рядках `<name> = "<version>"`.
|
|
144
|
-
|
|
145
|
-
**Python-гілка (оркестровано)** — `collectUvDiff` (`npm/skills/taze/js/uv-diff.mjs`) робить те саме детерміновано: парсить `pyproject.toml.taze-bak`/`pyproject.toml` через `smol-toml`, порівнює `[project].dependencies` (масив PEP 508-рядків, матчинг за іменем пакета — не за позицією), дістає нижню межу PEP 440-специфікатора (`extractLowerBoundVersion`) і класифікує за тим самим правилом caret-семантики. Ручний прогін поза оркестратором — той самий принцип: `diff pyproject.toml.taze-bak pyproject.toml` по записах `[project].dependencies`.
|
|
146
|
-
|
|
147
87
|
### 4. Зібрати breaking changes по кожному major-оновленню
|
|
148
88
|
|
|
149
89
|
Для кожного модуля зі списку зібрати фактичні відмінності одним з джерел (у порядку пріоритету):
|
|
@@ -154,10 +94,6 @@ n-rules taze diff
|
|
|
154
94
|
|
|
155
95
|
Цікавлять: видалені/перейменовані експорти, змінені сигнатури функцій, змінені типи, змінена поведінка за замовчуванням, видалені CLI-прапорці.
|
|
156
96
|
|
|
157
|
-
**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/`.
|
|
158
|
-
|
|
159
|
-
**Python-гілка** — адресу репозиторію взяти зі сторінки `https://pypi.org/project/<name>/` (поле "Homepage"/"Source"); CHANGELOG зазвичай у `CHANGELOG.md`/`HISTORY.md` репозиторію або в GitHub Releases. Якщо немає — різниця по публічному API між закешованою старою версією (`~/.cache/uv/`) і новою (`.venv/lib/python*/site-packages/<name>/`).
|
|
160
|
-
|
|
161
97
|
### 5. Перевірити сумісність з кодом проекту
|
|
162
98
|
|
|
163
99
|
Для кожного breaking change знайти його використання в коді проекту:
|
|
@@ -171,22 +107,6 @@ rg -n "<імпорт|функція|опція>" --type ts --type js --type vue
|
|
|
171
107
|
- **сумісно** — проект не використовує зачеплене API → нічого не робити.
|
|
172
108
|
- **несумісно** — використання знайдено → перейти до п. 6.
|
|
173
109
|
|
|
174
|
-
**Rust-гілка:**
|
|
175
|
-
|
|
176
|
-
```bash
|
|
177
|
-
rg -n "<use-шлях|функція|макрос>" --type rust
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
Та сама класифікація сумісно/несумісно.
|
|
181
|
-
|
|
182
|
-
**Python-гілка:**
|
|
183
|
-
|
|
184
|
-
```bash
|
|
185
|
-
rg -n "<імпорт|функція|клас>" --type py
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
Та сама класифікація сумісно/несумісно.
|
|
189
|
-
|
|
190
110
|
### 6. Рефакторинг несумісних місць
|
|
191
111
|
|
|
192
112
|
Для кожного несумісного місця — застосувати міграцію згідно з changelog модуля (перейменувати імпорт, оновити сигнатуру виклику, замінити видалену опцію еквівалентом тощо). Після правок:
|
|
@@ -197,24 +117,6 @@ bun run typecheck # якщо є
|
|
|
197
117
|
bun test # якщо є
|
|
198
118
|
```
|
|
199
119
|
|
|
200
|
-
**Rust-гілка** — після правок:
|
|
201
|
-
|
|
202
|
-
```bash
|
|
203
|
-
cargo fmt --all -- --check
|
|
204
|
-
cargo clippy --all-targets --all-features -- -D warnings
|
|
205
|
-
cargo test
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
**Python-гілка** — після правок (залежно від того, що реально налаштовано в проєкті):
|
|
209
|
-
|
|
210
|
-
```bash
|
|
211
|
-
ruff check .
|
|
212
|
-
mypy .
|
|
213
|
-
pytest
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
Якщо міграція нетривіальна або неоднозначна — **не вгадувати**, залишити TODO у коді з посиланням на CHANGELOG і винести в підсумковий звіт як ручну дію.
|
|
217
|
-
|
|
218
120
|
### 7. Прибрати тимчасові файли
|
|
219
121
|
|
|
220
122
|
```bash
|
|
@@ -223,20 +125,6 @@ rm package.json.taze-bak bun.lock.taze-bak
|
|
|
223
125
|
|
|
224
126
|
(І решту бекапів воркспейсів, якщо створювались.)
|
|
225
127
|
|
|
226
|
-
**Rust-гілка:**
|
|
227
|
-
|
|
228
|
-
```bash
|
|
229
|
-
rm Cargo.toml.taze-bak Cargo.lock.taze-bak
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
(І бекапи по кожному workspace-члену, якщо створювались окремо.)
|
|
233
|
-
|
|
234
|
-
**Python-гілка:**
|
|
235
|
-
|
|
236
|
-
```bash
|
|
237
|
-
rm pyproject.toml.taze-bak uv.lock.taze-bak
|
|
238
|
-
```
|
|
239
|
-
|
|
240
128
|
### 8. Звіт користувачу
|
|
241
129
|
|
|
242
130
|
Коротко в одному повідомленні:
|
|
@@ -247,12 +135,10 @@ rm pyproject.toml.taze-bak uv.lock.taze-bak
|
|
|
247
135
|
- **Потребує ручного втручання:** список TODO з причиною (нетривіальна міграція / неоднозначність / падіння тестів).
|
|
248
136
|
- **Стан перевірок:** `lint` / `typecheck` / `test` — pass/fail з номером рядка, де впало.
|
|
249
137
|
|
|
250
|
-
|
|
138
|
+
Для кожної обробленої екосистемної гілки — окрема секція з тим самим переліком (оновлено / major / зрефакторено / потребує ручного втручання) і власними перевірками (див. фрагменти плагінів наприкінці файлу).
|
|
251
139
|
|
|
252
140
|
## Примітка
|
|
253
141
|
|
|
254
142
|
- Не запускати `npx @7n/rules lint` паралельно з іншими ESLint-задачами — діє правило з кореневого `CLAUDE.md`.
|
|
255
143
|
- Якщо проект — `npm/` пакет цього репо, після змін у `package.json` / коді треба підняти `version` і додати запис у `CHANGELOG.md` згідно з `npm/CLAUDE.md`.
|
|
256
|
-
- При великій кількості major-оновлень розбити PR по одному модулю на коміт — щоб `git bisect` залишався корисним. Це стосується і
|
|
257
|
-
- `cargo upgrade --incompatible allow` редагує `Cargo.toml` навіть для залежностей без доступних breaking changes в змінах API — завжди звіряй крок 4 (CHANGELOG) перед тим, як вважати оновлення безпечним, а не лише факт успішної компіляції.
|
|
258
|
-
- `uv add <pkg>` на вже присутній залежності — no-op (specifier НЕ переписується); детерміновану гілку це не блокує (вона робить `uv remove` спершу), але при ручному прогоні кроків 2-8 легко про це забути.
|
|
144
|
+
- При великій кількості major-оновлень розбити PR по одному модулю на коміт — щоб `git bisect` залишався корисним. Це стосується і пакетів екосистемних гілок окремо від npm-пакетів.
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
/** @see ./docs/orchestrate.md */
|
|
2
2
|
import { spawnSync } from 'node:child_process'
|
|
3
3
|
import { existsSync } from 'node:fs'
|
|
4
|
-
import { copyFile,
|
|
5
|
-
import {
|
|
4
|
+
import { copyFile, rm } from 'node:fs/promises'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
6
|
import { pathToFileURL } from 'node:url'
|
|
7
7
|
|
|
8
|
+
import {
|
|
9
|
+
bringChangesBackToOriginal,
|
|
10
|
+
ensureRunningInWorktree,
|
|
11
|
+
removeAutoCreatedWorktree
|
|
12
|
+
} from '../../../scripts/lib/auto-worktree.mjs'
|
|
8
13
|
import { assertEcosystemProvider } from '../../../scripts/lib/plugin-api.mjs'
|
|
9
14
|
import { readNRulesConfigLite } from '../../../scripts/lib/read-n-rules-config-lite.mjs'
|
|
10
15
|
import { getHandlers, resolvePlugins } from '../../../scripts/lib/resolve-plugins.mjs'
|
|
11
16
|
import { getMonorepoPackageRootDirs } from '../../../scripts/lib/workspaces.mjs'
|
|
12
17
|
import { collectTazeDiff } from './diff.mjs'
|
|
13
18
|
|
|
19
|
+
export { bringChangesBackToOriginal, removeAutoCreatedWorktree } from '../../../scripts/lib/auto-worktree.mjs'
|
|
20
|
+
|
|
14
21
|
/** Суфікс бекапу package.json — той самий, що й у `diff.mjs`/кроці 1 SKILL.md. */
|
|
15
22
|
const BACKUP_SUFFIX = '.taze-bak'
|
|
16
23
|
|
|
@@ -80,125 +87,6 @@ export async function callRunner(runner, prompt, cwd, deps = {}) {
|
|
|
80
87
|
}
|
|
81
88
|
}
|
|
82
89
|
|
|
83
|
-
/**
|
|
84
|
-
* Гарантує, що подальші кроки оркестратора виконуються в ізольованому
|
|
85
|
-
* worktree (`main.json.worktree: true`, той самий контракт, що й для інших
|
|
86
|
-
* worktree-only скілів). Оркестратор не годує SKILL.md жодному агенту, тож
|
|
87
|
-
* замість покладатись на агента, що читає SKILL.md-preflight, сам детерміновано
|
|
88
|
-
* створює worktree конвенцією `<current-branch>-taze` (`npx \@7n/mt worktree
|
|
89
|
-
* create`) і ставить залежності — щоб `bunx taze -w -r latest`/`bun install`
|
|
90
|
-
* ніколи не виконались прямо в основному дереві виклику. `autoCreated: true`
|
|
91
|
-
* сигналить викликачеві, що після завершення прогону варто перенести зміни
|
|
92
|
-
* назад (`bringChangesBackToOriginal`) і прибрати worktree
|
|
93
|
-
* (`removeAutoCreatedWorktree`) — а не лишати сирітське дерево.
|
|
94
|
-
* @param {string} cwd каталог для перевірки
|
|
95
|
-
* @param {typeof spawnSync} spawnFn інжект для тестів
|
|
96
|
-
* @param {(line: string) => void} log колбек прогресу
|
|
97
|
-
* @returns {{ cwd: string, autoCreated: boolean, branchArg: string|null }} `autoCreated: false` — `cwd` без змін
|
|
98
|
-
* (вже worktree); `autoCreated: true` — `cwd` щойно створеного worktree і `branchArg`, з яким його створено
|
|
99
|
-
*/
|
|
100
|
-
function ensureRunningInWorktree(cwd, spawnFn, log) {
|
|
101
|
-
const toplevelResult = spawnFn('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8' })
|
|
102
|
-
const toplevel = toplevelResult.status === 0 ? toplevelResult.stdout.trim() : ''
|
|
103
|
-
const segments = new Set(toplevel.replaceAll('\\', '/').split('/'))
|
|
104
|
-
if (segments.has('.worktrees')) return { cwd, autoCreated: false, branchArg: null }
|
|
105
|
-
|
|
106
|
-
const branchResult = spawnFn('git', ['branch', '--show-current'], { cwd, encoding: 'utf8' })
|
|
107
|
-
const currentBranch = branchResult.status === 0 ? branchResult.stdout.trim() : ''
|
|
108
|
-
if (!currentBranch) {
|
|
109
|
-
throw new Error(
|
|
110
|
-
`taze: "${cwd}" не в ізольованому worktree (git toplevel: "${toplevel || '?'}"), і поточну гілку визначити не вдалось ` +
|
|
111
|
-
'(detached HEAD?) — автоматичне створення worktree за конвенцією `<current-branch>-taze` неможливе. Перейди на гілку вручну.'
|
|
112
|
-
)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const branchArg = `${currentBranch}-taze`
|
|
116
|
-
const pathSegment = branchArg.replaceAll('/', '-')
|
|
117
|
-
log(`⚠️ "${cwd}" не в ізольованому worktree — створюю ".worktrees/${pathSegment}"...`)
|
|
118
|
-
runCommand('npx', ['@7n/mt', 'worktree', 'create', branchArg, 'n-taze: worktree-only skill'], cwd, spawnFn)
|
|
119
|
-
|
|
120
|
-
const newCwd = join(cwd, '.worktrees', pathSegment)
|
|
121
|
-
log('📥 bun install (bootstrap нового дерева)...')
|
|
122
|
-
runCommand('bun', ['install'], newCwd, spawnFn)
|
|
123
|
-
return { cwd: newCwd, autoCreated: true, branchArg }
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Переносить зміни з автоствореного worktree назад у вихідне дерево як
|
|
128
|
-
* **untracked/незакомічені** правки (просте копіювання файлів, без git
|
|
129
|
-
* merge/cherry-pick) — так само, як їх бачив би користувач, якби сам
|
|
130
|
-
* працював у вихідному дереві. Джерело істини — `git status --porcelain`
|
|
131
|
-
* у worktree: для кожного шляху копіює файл, якщо він існує (модифікація/
|
|
132
|
-
* додавання), або видаляє його у вихідному дереві, якщо existsSync каже,
|
|
133
|
-
* що в worktree його вже нема (видалення). Перейменування (`old -> new` у
|
|
134
|
-
* porcelain) переносять лише нову назву — стара лишається як була, це
|
|
135
|
-
* прийнятний компроміс: taze не перейменовує файли сам, ефект можливий
|
|
136
|
-
* лише як побічний результат LLM-рефакторингу.
|
|
137
|
-
* @param {string} worktreeCwd автостворений worktree, з якого переносимо
|
|
138
|
-
* @param {string} originalCwd вихідне дерево, куди переносимо
|
|
139
|
-
* @param {typeof spawnSync} spawnFn інжект для тестів
|
|
140
|
-
* @param {(line: string) => void} log колбек прогресу
|
|
141
|
-
* @param {{ copyFile?: (src: string, dest: string) => Promise<void>, rm?: (path: string, opts?: object) => Promise<void>, mkdir?: (path: string, opts?: object) => Promise<void> }} [deps] інжекти для тестів
|
|
142
|
-
* @returns {Promise<string[]>} відносні шляхи перенесених файлів
|
|
143
|
-
*/
|
|
144
|
-
export async function bringChangesBackToOriginal(worktreeCwd, originalCwd, spawnFn, log, deps = {}) {
|
|
145
|
-
const copy = deps.copyFile ?? copyFile
|
|
146
|
-
const removeFile = deps.rm ?? rm
|
|
147
|
-
const makeDir = deps.mkdir ?? mkdir
|
|
148
|
-
|
|
149
|
-
const statusResult = spawnFn('git', ['status', '--porcelain'], { cwd: worktreeCwd, encoding: 'utf8' })
|
|
150
|
-
if (statusResult.status !== 0) {
|
|
151
|
-
log(
|
|
152
|
-
`⚠️ Не вдалось прочитати git status у "${worktreeCwd}" — зміни НЕ перенесені назад, worktree лишиться для ручного розбору.`
|
|
153
|
-
)
|
|
154
|
-
return []
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const lines = statusResult.stdout.split('\n').filter(Boolean)
|
|
158
|
-
if (lines.length === 0) {
|
|
159
|
-
log('ℹ️ Worktree без змін — нічого переносити назад.')
|
|
160
|
-
return []
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const brought = []
|
|
164
|
-
for (const line of lines) {
|
|
165
|
-
const rest = line.slice(3)
|
|
166
|
-
const relPath = rest.includes(' -> ') ? rest.split(' -> ', 2)[1] : rest
|
|
167
|
-
const srcPath = join(worktreeCwd, relPath)
|
|
168
|
-
const destPath = join(originalCwd, relPath)
|
|
169
|
-
if (existsSync(srcPath)) {
|
|
170
|
-
await makeDir(dirname(destPath), { recursive: true })
|
|
171
|
-
await copy(srcPath, destPath)
|
|
172
|
-
} else {
|
|
173
|
-
await removeFile(destPath, { force: true })
|
|
174
|
-
}
|
|
175
|
-
brought.push(relPath)
|
|
176
|
-
}
|
|
177
|
-
log(`📤 Перенесено назад у "${originalCwd}" як untracked: ${brought.join(', ')}`)
|
|
178
|
-
return brought
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Прибирає автостворений worktree разом з його ефемерною git-гілкою
|
|
183
|
-
* (`npx \@7n/mt worktree remove <branch>`) — викликати лише ПІСЛЯ
|
|
184
|
-
* `bringChangesBackToOriginal`, інакше зміни згорять разом з деревом.
|
|
185
|
-
* Не кидає при провалі — це прибирання, а не крок, від якого залежить
|
|
186
|
-
* результат прогону; провал лише логується, worktree лишається для
|
|
187
|
-
* ручного розбору.
|
|
188
|
-
* @param {string} branchArg гілка, з якою worktree був створений (з `ensureRunningInWorktree`)
|
|
189
|
-
* @param {string} originalCwd вихідне дерево, звідки виконати `npx \@7n/mt worktree remove`
|
|
190
|
-
* @param {typeof spawnSync} spawnFn інжект для тестів
|
|
191
|
-
* @param {(line: string) => void} log колбек прогресу
|
|
192
|
-
* @returns {void}
|
|
193
|
-
*/
|
|
194
|
-
export function removeAutoCreatedWorktree(branchArg, originalCwd, spawnFn, log) {
|
|
195
|
-
log(`🧹 Прибираю автостворений worktree "${branchArg}"...`)
|
|
196
|
-
const result = spawnFn('npx', ['@7n/mt', 'worktree', 'remove', branchArg], { cwd: originalCwd, encoding: 'utf8' })
|
|
197
|
-
if (result.status !== 0) {
|
|
198
|
-
log(`⚠️ Не вдалось прибрати worktree "${branchArg}" — приберіть вручну (${result.stderr || result.stdout})`)
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
90
|
/**
|
|
203
91
|
* Синхронно виконує детерміновану команду (bunx/bun), кидає з
|
|
204
92
|
* exit-кодом+stderr при провалі.
|
|
@@ -430,7 +318,10 @@ export async function runTazeOrchestrator(options = {}) {
|
|
|
430
318
|
const call = deps.callRunner ?? callRunner
|
|
431
319
|
|
|
432
320
|
const originalCwd = options.cwd ?? process.cwd()
|
|
433
|
-
const worktree = ensureRunningInWorktree(originalCwd, spawnFn, log
|
|
321
|
+
const worktree = ensureRunningInWorktree(originalCwd, spawnFn, log, {
|
|
322
|
+
suffix: 'taze',
|
|
323
|
+
description: 'n-taze: worktree-only skill'
|
|
324
|
+
})
|
|
434
325
|
const cwd = worktree.cwd
|
|
435
326
|
|
|
436
327
|
try {
|