@7n/rules 1.24.0 → 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 +7 -0
- package/bin/n-rules.js +46 -14
- 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/skills/taze/js/docs/orchestrate.md +1 -1
- package/skills/taze/js/orchestrate.mjs +13 -122
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
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
|
+
|
|
3
10
|
## [1.24.0] - 2026-07-19
|
|
4
11
|
|
|
5
12
|
### Added
|
package/bin/n-rules.js
CHANGED
|
@@ -84,6 +84,11 @@ 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'
|
|
89
94
|
import { collectSkillFragments, injectSkillFragments } from '../scripts/lib/skill-fragments.mjs'
|
|
@@ -1812,28 +1817,55 @@ try {
|
|
|
1812
1817
|
baseRef
|
|
1813
1818
|
}
|
|
1814
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
|
|
1815
1832
|
// Глобальна черга full-прогонів (spec 2026-07-03): одночасно виконується один
|
|
1816
1833
|
// `lint --full` на машину, паралельні --full чекають лока і бачать чергу та
|
|
1817
1834
|
// живий прогрес активного прогону; не-full запуски йдуть без лока
|
|
1818
1835
|
// (див. lint-lock.mjs). Publisher пише знімки прогресу для черги.
|
|
1819
1836
|
const { withGlobalLintLock, createProgressPublisher } =
|
|
1820
1837
|
await import('../scripts/lib/lint-surface/lint-lock.mjs')
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
if (
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
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
|
+
)
|
|
1830
1865
|
}
|
|
1831
|
-
|
|
1832
|
-
return await runFixPipeline(runOpts)
|
|
1833
|
-
} finally {
|
|
1834
|
-
publisher?.stop()
|
|
1866
|
+
removeAutoCreatedWorktree(worktree.branchArg, cwdArg, spawnSync, line => console.log(line))
|
|
1835
1867
|
}
|
|
1836
|
-
}
|
|
1868
|
+
}
|
|
1837
1869
|
|
|
1838
1870
|
break
|
|
1839
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 |
|
|
@@ -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 {
|