@nitra/cursor 4.1.1 → 5.0.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 +20 -0
- package/bin/docs/n-cursor.md +1 -9
- package/bin/n-cursor.js +3 -25
- package/docs/stryker.config.md +37 -0
- package/docs/vitest.config.md +23 -0
- package/package.json +2 -1
- package/rules/docker/lib/docs/docker-mirror.md +1 -1
- package/rules/docker/lib/docs/docker-native-addon.md +1 -1
- package/rules/test/coverage/coverage.mjs +9 -19
- package/rules/test/test.mdc +1 -1
- package/scripts/dispatcher/trace.mjs +4 -16
- package/scripts/docs/build-agents-commands.md +1 -1
- package/scripts/docs/worktree-cli.md +1 -1
- package/scripts/lib/changed-files.mjs +19 -3
- package/scripts/lib/sync-gitignore-worktree.mjs +4 -5
- package/scripts/worktree-cli.mjs +1 -2
- package/skills/docgen/js/docgen-gen.mjs +7 -7
- package/scripts/dispatcher/docs/graph.md +0 -346
- package/scripts/dispatcher/docs/index.md +0 -236
- package/scripts/dispatcher/docs/trace.md +0 -296
- package/scripts/dispatcher/graph/lib/cmd-init.mjs +0 -112
- package/scripts/dispatcher/graph/lib/cmd-invalidate.mjs +0 -96
- package/scripts/dispatcher/graph/lib/cmd-kill.mjs +0 -141
- package/scripts/dispatcher/graph/lib/cmd-plan.mjs +0 -142
- package/scripts/dispatcher/graph/lib/cmd-run.mjs +0 -328
- package/scripts/dispatcher/graph/lib/cmd-scan.mjs +0 -115
- package/scripts/dispatcher/graph/lib/cmd-setup.mjs +0 -111
- package/scripts/dispatcher/graph/lib/cmd-signals.mjs +0 -328
- package/scripts/dispatcher/graph/lib/cmd-status.mjs +0 -131
- package/scripts/dispatcher/graph/lib/cmd-verify.mjs +0 -100
- package/scripts/dispatcher/graph/lib/cmd-watch.mjs +0 -128
- package/scripts/dispatcher/graph/lib/config.mjs +0 -103
- package/scripts/dispatcher/graph/lib/frontmatter.mjs +0 -224
- package/scripts/dispatcher/graph/lib/nnn.mjs +0 -127
- package/scripts/dispatcher/graph/lib/node-state.mjs +0 -157
- package/scripts/dispatcher/graph/lib/scanner.mjs +0 -235
- package/scripts/dispatcher/graph/lib/worktree-ops.mjs +0 -193
- package/scripts/dispatcher/graph-tasks.mjs +0 -92
- package/scripts/dispatcher/graph.mjs +0 -212
- package/scripts/dispatcher/index.mjs +0 -45
- package/scripts/dispatcher/lib/docs/active.md +0 -348
- package/scripts/dispatcher/lib/docs/artifact.md +0 -232
- package/scripts/dispatcher/lib/docs/budget.md +0 -167
- package/scripts/dispatcher/lib/docs/capability.md +0 -196
- package/scripts/dispatcher/lib/docs/commands.md +0 -210
- package/scripts/dispatcher/lib/docs/events.md +0 -183
- package/scripts/dispatcher/lib/docs/executor.md +0 -190
- package/scripts/dispatcher/lib/docs/gate.md +0 -231
- package/scripts/dispatcher/lib/docs/level.md +0 -335
- package/scripts/dispatcher/lib/docs/plan-panel.md +0 -181
- package/scripts/dispatcher/lib/docs/plan.md +0 -200
- package/scripts/dispatcher/lib/docs/planner.md +0 -269
- package/scripts/dispatcher/lib/docs/review.md +0 -255
- package/scripts/dispatcher/lib/docs/reviewer.md +0 -240
- package/scripts/dispatcher/lib/docs/snapshot.md +0 -247
- package/scripts/dispatcher/lib/docs/spec.md +0 -203
- package/scripts/dispatcher/lib/docs/state-store.md +0 -303
- package/scripts/dispatcher/lib/docs/subagent-runner.md +0 -173
- package/scripts/dispatcher/lib/events.mjs +0 -67
- package/scripts/dispatcher/lib/executor.mjs +0 -107
- package/scripts/dispatcher/lib/plan-panel.mjs +0 -76
- package/scripts/dispatcher/lib/state-store.mjs +0 -173
- package/scripts/dispatcher/lib/subagent-runner.mjs +0 -53
- package/scripts/graph/index.mjs +0 -115
- package/scripts/graph/lib/config.mjs +0 -62
- package/scripts/graph/lib/dag.mjs +0 -161
- package/scripts/graph/lib/frontmatter.mjs +0 -70
- package/scripts/graph/lib/nnn.mjs +0 -77
- package/scripts/graph/lib/state.mjs +0 -110
- package/scripts/graph/scan.mjs +0 -64
- package/scripts/graph/status.mjs +0 -86
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DAG-сканер вузлів задач.
|
|
3
|
-
*
|
|
4
|
-
* Рекурсивно обходить tasks_dir, знаходить всі вузли (директорії з task.md),
|
|
5
|
-
* читає їх залежності з task.md front-matter, деривує стани та виконує
|
|
6
|
-
* топологічне сортування (Kahn's algorithm).
|
|
7
|
-
*
|
|
8
|
-
* FS ін'єктується. Нічого не пише на диск.
|
|
9
|
-
*/
|
|
10
|
-
import { execSync } from 'node:child_process'
|
|
11
|
-
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
12
|
-
import { join } from 'node:path'
|
|
13
|
-
|
|
14
|
-
import { parseFrontMatter } from './frontmatter.mjs'
|
|
15
|
-
import { deriveNodeState, isComposite } from './node-state.mjs'
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* @typedef {{
|
|
19
|
-
* id: string,
|
|
20
|
-
* path: string,
|
|
21
|
-
* dir: string,
|
|
22
|
-
* deps: string[],
|
|
23
|
-
* state: string,
|
|
24
|
-
* composite: boolean,
|
|
25
|
-
* children: string[]
|
|
26
|
-
* }} NodeInfo
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Рекурсивно знаходить всі вузли DAG у tasks_dir.
|
|
31
|
-
* Вузол = директорія що містить task.md.
|
|
32
|
-
* @param {string} tasksDir абсолютний шлях до tasks/
|
|
33
|
-
* @param {{
|
|
34
|
-
* readdirSync?: (d: string) => string[],
|
|
35
|
-
* existsSync?: (p: string) => boolean,
|
|
36
|
-
* readFileSync?: (p: string, enc: string) => string
|
|
37
|
-
* }} [deps] ін'єкції
|
|
38
|
-
* @returns {{ dir: string, relPath: string }[]} список знайдених вузлів
|
|
39
|
-
*/
|
|
40
|
-
export function findNodes(tasksDir, deps = {}) {
|
|
41
|
-
const readdir = deps.readdirSync ?? readdirSync
|
|
42
|
-
const exists = deps.existsSync ?? existsSync
|
|
43
|
-
|
|
44
|
-
const nodes = []
|
|
45
|
-
|
|
46
|
-
function scan(dir, prefix = '') {
|
|
47
|
-
let entries
|
|
48
|
-
try {
|
|
49
|
-
entries = readdir(dir)
|
|
50
|
-
} catch {
|
|
51
|
-
return
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const hasTaskMd = entries.includes('task.md')
|
|
55
|
-
if (hasTaskMd) {
|
|
56
|
-
nodes.push({
|
|
57
|
-
dir,
|
|
58
|
-
relPath: prefix ? prefix : dir.split('/').pop() ?? dir
|
|
59
|
-
})
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Рекурсивно шукаємо дочірні директорії
|
|
63
|
-
for (const name of entries) {
|
|
64
|
-
// Пропускаємо зарезервовані та приховані директорії/файли
|
|
65
|
-
if (name.startsWith('.') || name.includes('.')) continue
|
|
66
|
-
const childDir = join(dir, name)
|
|
67
|
-
// Перевіряємо що це директорія (якщо має subdirs або task.md)
|
|
68
|
-
const childRelPath = prefix ? `${prefix}/${name}` : name
|
|
69
|
-
try {
|
|
70
|
-
// Перевірка що childDir — дійсно директорія
|
|
71
|
-
readdir(childDir)
|
|
72
|
-
scan(childDir, childRelPath)
|
|
73
|
-
} catch {
|
|
74
|
-
// не директорія або не читається
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
scan(tasksDir)
|
|
80
|
-
return nodes
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Сканує DAG і повертає всі вузли з деривованими станами.
|
|
85
|
-
* @param {string} tasksDir абсолютний шлях до tasks/
|
|
86
|
-
* @param {Set<string>} activeWorktrees активні worktree imена
|
|
87
|
-
* @param {{
|
|
88
|
-
* readdirSync?: (d: string) => string[],
|
|
89
|
-
* existsSync?: (p: string) => boolean,
|
|
90
|
-
* readFileSync?: (p: string, enc: string) => string
|
|
91
|
-
* }} [deps] ін'єкції
|
|
92
|
-
* @returns {NodeInfo[]} список вузлів
|
|
93
|
-
*/
|
|
94
|
-
export function scanNodes(tasksDir, activeWorktrees, deps = {}) {
|
|
95
|
-
const readdir = deps.readdirSync ?? readdirSync
|
|
96
|
-
const exists = deps.existsSync ?? existsSync
|
|
97
|
-
const readFile = deps.readFileSync ?? ((p, enc) => readFileSync(p, enc))
|
|
98
|
-
|
|
99
|
-
const found = findNodes(tasksDir, { readdirSync: readdir, existsSync: exists, readFileSync: readFile })
|
|
100
|
-
|
|
101
|
-
return found.map(({ dir, relPath }) => {
|
|
102
|
-
let fm = {}
|
|
103
|
-
try {
|
|
104
|
-
const taskContent = readFile(join(dir, 'task.md'), 'utf8')
|
|
105
|
-
fm = parseFrontMatter(taskContent)
|
|
106
|
-
} catch {
|
|
107
|
-
// порожній front-matter
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const deps_ = Array.isArray(fm.deps) ? fm.deps.map(String) : []
|
|
111
|
-
const state = deriveNodeState(dir, activeWorktrees, { readdirSync: readdir, readFileSync: readFile, existsSync: exists })
|
|
112
|
-
const composite = isComposite(dir, { readdirSync: readdir, existsSync: exists })
|
|
113
|
-
|
|
114
|
-
// Дочірні вузли
|
|
115
|
-
let children = []
|
|
116
|
-
if (composite) {
|
|
117
|
-
let entries
|
|
118
|
-
try {
|
|
119
|
-
entries = readdir(dir)
|
|
120
|
-
} catch {
|
|
121
|
-
entries = []
|
|
122
|
-
}
|
|
123
|
-
children = entries
|
|
124
|
-
.filter(name => !name.startsWith('.') && !name.endsWith('.md') && !name.endsWith('.json'))
|
|
125
|
-
.filter(name => {
|
|
126
|
-
try {
|
|
127
|
-
return exists(join(dir, name, 'task.md'))
|
|
128
|
-
} catch {
|
|
129
|
-
return false
|
|
130
|
-
}
|
|
131
|
-
})
|
|
132
|
-
.map(name => `${relPath}/${name}`)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return {
|
|
136
|
-
id: relPath,
|
|
137
|
-
path: relPath,
|
|
138
|
-
dir,
|
|
139
|
-
deps: deps_,
|
|
140
|
-
state,
|
|
141
|
-
composite,
|
|
142
|
-
children
|
|
143
|
-
}
|
|
144
|
-
})
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Топологічне сортування вузлів (алгоритм Кана).
|
|
149
|
-
* Вузли без залежностей — першими. Циклічні залежності — не гарантовано.
|
|
150
|
-
* @param {NodeInfo[]} nodes вузли зі списком deps
|
|
151
|
-
* @returns {NodeInfo[]} відсортований список (або той самий порядок якщо циклічні)
|
|
152
|
-
*/
|
|
153
|
-
export function topoSort(nodes) {
|
|
154
|
-
const idToNode = new Map(nodes.map(n => [n.id, n]))
|
|
155
|
-
const inDegree = new Map(nodes.map(n => [n.id, 0]))
|
|
156
|
-
const adj = new Map(nodes.map(n => [n.id, []]))
|
|
157
|
-
|
|
158
|
-
for (const node of nodes) {
|
|
159
|
-
for (const dep of node.deps) {
|
|
160
|
-
if (idToNode.has(dep)) {
|
|
161
|
-
adj.get(dep).push(node.id)
|
|
162
|
-
inDegree.set(node.id, (inDegree.get(node.id) ?? 0) + 1)
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const queue = nodes.filter(n => (inDegree.get(n.id) ?? 0) === 0).map(n => n.id)
|
|
168
|
-
const sorted = []
|
|
169
|
-
|
|
170
|
-
while (queue.length > 0) {
|
|
171
|
-
const id = queue.shift()
|
|
172
|
-
const node = idToNode.get(id)
|
|
173
|
-
if (node) sorted.push(node)
|
|
174
|
-
for (const next of (adj.get(id) ?? [])) {
|
|
175
|
-
const deg = (inDegree.get(next) ?? 0) - 1
|
|
176
|
-
inDegree.set(next, deg)
|
|
177
|
-
if (deg === 0) queue.push(next)
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Якщо є цикли — додаємо решту у кінець
|
|
182
|
-
if (sorted.length < nodes.length) {
|
|
183
|
-
for (const n of nodes) {
|
|
184
|
-
if (!sorted.includes(n)) sorted.push(n)
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return sorted
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Перевіряє чи всі залежності вузла resolved.
|
|
193
|
-
* @param {NodeInfo} node вузол
|
|
194
|
-
* @param {Map<string, NodeInfo>} nodeMap map id -> NodeInfo
|
|
195
|
-
* @returns {boolean} true якщо всі deps resolved
|
|
196
|
-
*/
|
|
197
|
-
export function areDepsResolved(node, nodeMap) {
|
|
198
|
-
return node.deps.every(dep => {
|
|
199
|
-
const depNode = nodeMap.get(dep)
|
|
200
|
-
return depNode?.state === 'resolved'
|
|
201
|
-
})
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Знаходить активні worktrees з git worktree list.
|
|
206
|
-
* @param {string} root корінь репо
|
|
207
|
-
* @param {{ execSync?: (cmd: string, opts?: object) => string }} [deps] ін'єкції
|
|
208
|
-
* @returns {Set<string>} set імен worktree
|
|
209
|
-
*/
|
|
210
|
-
export function getActiveWorktrees(root, deps = {}) {
|
|
211
|
-
const execSyncFn = deps.execSync ?? ((cmd, opts) => execSync(cmd, opts))
|
|
212
|
-
try {
|
|
213
|
-
const out = execSyncFn('git worktree list --porcelain', { cwd: root, encoding: 'utf8' })
|
|
214
|
-
return parseWorktreeList(String(out))
|
|
215
|
-
} catch {
|
|
216
|
-
return new Set()
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Парсить вивід `git worktree list --porcelain` і повертає набір імен worktree.
|
|
222
|
-
* @param {string} output вивід команди
|
|
223
|
-
* @returns {Set<string>} set імен (останній компонент шляху)
|
|
224
|
-
*/
|
|
225
|
-
export function parseWorktreeList(output) {
|
|
226
|
-
const names = new Set()
|
|
227
|
-
for (const line of output.split('\n')) {
|
|
228
|
-
if (line.startsWith('worktree ')) {
|
|
229
|
-
const path = line.slice('worktree '.length).trim()
|
|
230
|
-
const name = path.split('/').pop() ?? ''
|
|
231
|
-
if (name) names.add(name)
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
return names
|
|
235
|
-
}
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Git worktree management для graph task system.
|
|
3
|
-
*
|
|
4
|
-
* Atomic mkdir lock: EEXIST → skip (вже запущено).
|
|
5
|
-
* Worktree name: sanitize(node-path) + '-' + epoch (секунди).
|
|
6
|
-
*
|
|
7
|
-
* Всі git операції через execSync (node:child_process). FS через ін'єкцію.
|
|
8
|
-
*/
|
|
9
|
-
import { execSync } from 'node:child_process'
|
|
10
|
-
import { mkdirSync, readdirSync, rmSync } from 'node:fs'
|
|
11
|
-
import { join } from 'node:path'
|
|
12
|
-
|
|
13
|
-
import { sanitizeNodeName } from './node-state.mjs'
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Генерує ім'я worktree для вузла.
|
|
17
|
-
* @param {string} nodePath відносний шлях вузла (напр. "research/collect-data")
|
|
18
|
-
* @param {number} [epochSec] epoch в секундах (default: Date.now()/1000)
|
|
19
|
-
* @returns {string} ім'я worktree
|
|
20
|
-
*/
|
|
21
|
-
export function makeWorktreeName(nodePath, epochSec) {
|
|
22
|
-
const epoch = epochSec ?? Math.floor(Date.now() / 1000)
|
|
23
|
-
const sanitized = sanitizeNodeName(nodePath.replace(/\//g, '-'))
|
|
24
|
-
return `${sanitized}-${epoch}`
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Створює git worktree для вузла з atomic mkdir lock.
|
|
29
|
-
* Повертає null якщо worktree вже існує (EEXIST → вже запущено).
|
|
30
|
-
*
|
|
31
|
-
* @param {string} worktreesDir абсолютний шлях до .worktrees/
|
|
32
|
-
* @param {string} worktreeName ім'я нового worktree
|
|
33
|
-
* @param {string} root корінь репо
|
|
34
|
-
* @param {{
|
|
35
|
-
* execSync?: (cmd: string, opts?: object) => string,
|
|
36
|
-
* mkdirSync?: (p: string, opts?: object) => void
|
|
37
|
-
* }} [deps] ін'єкції
|
|
38
|
-
* @returns {{ worktreePath: string } | null} шлях worktree або null якщо вже існує
|
|
39
|
-
*/
|
|
40
|
-
export function createWorktree(worktreesDir, worktreeName, root, deps = {}) {
|
|
41
|
-
const execSyncFn = deps.execSync ?? ((cmd, opts) => execSync(cmd, opts))
|
|
42
|
-
const mkdirSyncFn = deps.mkdirSync ?? mkdirSync
|
|
43
|
-
|
|
44
|
-
const worktreePath = join(worktreesDir, worktreeName)
|
|
45
|
-
|
|
46
|
-
// Atomic mkdir lock: якщо директорія вже є — хтось вже запустив цей вузол
|
|
47
|
-
try {
|
|
48
|
-
mkdirSyncFn(worktreePath, { recursive: false })
|
|
49
|
-
} catch (err) {
|
|
50
|
-
if (err.code === 'EEXIST') return null
|
|
51
|
-
throw err
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
// Видаляємо порожню директорію — git worktree add створить її сам
|
|
56
|
-
rmSync(worktreePath, { recursive: true, force: true })
|
|
57
|
-
execSyncFn(`git worktree add "${worktreePath}" HEAD`, { cwd: root, encoding: 'utf8' })
|
|
58
|
-
} catch (err) {
|
|
59
|
-
// Якщо git worktree add не вдався — прибираємо директорію
|
|
60
|
-
try {
|
|
61
|
-
rmSync(worktreePath, { recursive: true, force: true })
|
|
62
|
-
} catch {
|
|
63
|
-
// пропускаємо
|
|
64
|
-
}
|
|
65
|
-
throw err
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return { worktreePath }
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Видаляє git worktree.
|
|
73
|
-
* @param {string} worktreePath абсолютний шлях до worktree
|
|
74
|
-
* @param {string} root корінь репо
|
|
75
|
-
* @param {{
|
|
76
|
-
* execSync?: (cmd: string, opts?: object) => string
|
|
77
|
-
* }} [deps] ін'єкції
|
|
78
|
-
*/
|
|
79
|
-
export function removeWorktree(worktreePath, root, deps = {}) {
|
|
80
|
-
const execSyncFn = deps.execSync ?? ((cmd, opts) => execSync(cmd, opts))
|
|
81
|
-
try {
|
|
82
|
-
execSyncFn(`git worktree remove --force "${worktreePath}"`, { cwd: root, encoding: 'utf8' })
|
|
83
|
-
} catch {
|
|
84
|
-
// Якщо не вдалось через git — видаляємо вручну
|
|
85
|
-
try {
|
|
86
|
-
rmSync(worktreePath, { recursive: true, force: true })
|
|
87
|
-
execSyncFn('git worktree prune', { cwd: root, encoding: 'utf8' })
|
|
88
|
-
} catch {
|
|
89
|
-
// пропускаємо — можливо вже видалено
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Мерджить зміни з worktree у main-гілку і видаляє worktree.
|
|
96
|
-
* @param {string} worktreePath абсолютний шлях до worktree
|
|
97
|
-
* @param {string} root корінь репо
|
|
98
|
-
* @param {{
|
|
99
|
-
* execSync?: (cmd: string, opts?: object) => string
|
|
100
|
-
* }} [deps] ін'єкції
|
|
101
|
-
* @returns {{ ok: boolean, error?: string }} результат
|
|
102
|
-
*/
|
|
103
|
-
export function mergeWorktree(worktreePath, root, deps = {}) {
|
|
104
|
-
const execSyncFn = deps.execSync ?? ((cmd, opts) => execSync(cmd, opts))
|
|
105
|
-
|
|
106
|
-
try {
|
|
107
|
-
// Отримуємо ім'я гілки worktree
|
|
108
|
-
const branch = execSyncFn('git rev-parse --abbrev-ref HEAD', {
|
|
109
|
-
cwd: worktreePath,
|
|
110
|
-
encoding: 'utf8'
|
|
111
|
-
}).trim()
|
|
112
|
-
|
|
113
|
-
// Додаємо всі зміни і комітимо
|
|
114
|
-
execSyncFn('git add -A', { cwd: worktreePath, encoding: 'utf8' })
|
|
115
|
-
|
|
116
|
-
let hasChanges = false
|
|
117
|
-
try {
|
|
118
|
-
execSyncFn('git diff --cached --quiet', { cwd: worktreePath, encoding: 'utf8' })
|
|
119
|
-
} catch {
|
|
120
|
-
hasChanges = true
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (hasChanges) {
|
|
124
|
-
execSyncFn('git commit -m "graph: node task completion"', { cwd: worktreePath, encoding: 'utf8' })
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Якщо worktree на окремій гілці — мерджимо в main
|
|
128
|
-
if (branch && branch !== 'HEAD' && branch !== 'main' && branch !== 'master') {
|
|
129
|
-
execSyncFn(`git merge --no-ff "${branch}" -m "graph: merge node ${branch}"`, {
|
|
130
|
-
cwd: root,
|
|
131
|
-
encoding: 'utf8'
|
|
132
|
-
})
|
|
133
|
-
}
|
|
134
|
-
} catch (err) {
|
|
135
|
-
return { ok: false, error: err.message ?? String(err) }
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Видаляємо worktree
|
|
139
|
-
removeWorktree(worktreePath, root, { execSync: execSyncFn })
|
|
140
|
-
return { ok: true }
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Повертає список активних worktrees з репо.
|
|
145
|
-
* @param {string} root корінь репо
|
|
146
|
-
* @param {{
|
|
147
|
-
* execSync?: (cmd: string, opts?: object) => string
|
|
148
|
-
* }} [deps] ін'єкції
|
|
149
|
-
* @returns {Set<string>} set імен worktrees
|
|
150
|
-
*/
|
|
151
|
-
export function listActiveWorktrees(root, deps = {}) {
|
|
152
|
-
const execSyncFn = deps.execSync ?? ((cmd, opts) => execSync(cmd, opts))
|
|
153
|
-
|
|
154
|
-
try {
|
|
155
|
-
const out = execSyncFn('git worktree list --porcelain', { cwd: root, encoding: 'utf8' })
|
|
156
|
-
const names = new Set()
|
|
157
|
-
for (const line of String(out).split('\n')) {
|
|
158
|
-
if (line.startsWith('worktree ')) {
|
|
159
|
-
const path = line.slice('worktree '.length).trim()
|
|
160
|
-
const name = path.split('/').pop() ?? ''
|
|
161
|
-
if (name) names.add(name)
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return names
|
|
165
|
-
} catch {
|
|
166
|
-
return new Set()
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Знаходить worktree що належить вузлу (за prefix).
|
|
172
|
-
* @param {string} nodePath відносний шлях вузла
|
|
173
|
-
* @param {string} worktreesDir абсолютний шлях до .worktrees/
|
|
174
|
-
* @param {{
|
|
175
|
-
* readdirSync?: (d: string) => string[],
|
|
176
|
-
* execSync?: (cmd: string, opts?: object) => string
|
|
177
|
-
* }} [deps] ін'єкції
|
|
178
|
-
* @returns {string | null} абсолютний шлях до worktree або null
|
|
179
|
-
*/
|
|
180
|
-
export function findNodeWorktree(nodePath, worktreesDir, deps = {}) {
|
|
181
|
-
const readdirSyncFn = deps.readdirSync ?? readdirSync
|
|
182
|
-
const prefix = sanitizeNodeName(nodePath.replace(/\//g, '-'))
|
|
183
|
-
|
|
184
|
-
let entries
|
|
185
|
-
try {
|
|
186
|
-
entries = readdirSyncFn(worktreesDir)
|
|
187
|
-
} catch {
|
|
188
|
-
return null
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const match = entries.find(name => name.startsWith(prefix + '-') || name === prefix)
|
|
192
|
-
return match ? join(worktreesDir, match) : null
|
|
193
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `n-cursor graph` — task DAG orchestration system.
|
|
3
|
-
*
|
|
4
|
-
* Управляє задачами у `tasks/<node>/` директоріях. Стан вузлів деривується
|
|
5
|
-
* з файлів (immutable protocol): task.md + plan/run/fact/pending-audit/audit-result/invalidated.
|
|
6
|
-
*
|
|
7
|
-
* Підкоманди:
|
|
8
|
-
* setup — ініціалізація проєкту (.n-cursor.json, tasks/, git hook)
|
|
9
|
-
* init <name> — створити task.md шаблон
|
|
10
|
-
* plan [<path>] [--mode agent] — Stage 1: написати plan_NNN.md
|
|
11
|
-
* status [<path>] [--json] — показати стан DAG
|
|
12
|
-
* scan [--json] — повний скан, exit 1 якщо failed
|
|
13
|
-
* run [<path>] [--actor a] [--auto] — запустити вузол(и)
|
|
14
|
-
* kill <path> — вбити worktree + каскадна інвалідація + видалити plan_*.md
|
|
15
|
-
* invalidate <path> [--no-cascade] — позначити як invalidated
|
|
16
|
-
* done <path> — успіх → merge worktree
|
|
17
|
-
* audit <path> — pending-audit_NNN.md + merge agent worktree
|
|
18
|
-
* failed <path> — провал → run_NNN.md (failed)
|
|
19
|
-
* spawn <path> — composite → перевірити дочірні вузли
|
|
20
|
-
* watch — одноразовий скан: audit queue + stale + needs-plan
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import { cmdSetup } from './graph/lib/cmd-setup.mjs'
|
|
24
|
-
import { cmdInit } from './graph/lib/cmd-init.mjs'
|
|
25
|
-
import { cmdPlan } from './graph/lib/cmd-plan.mjs'
|
|
26
|
-
import { cmdStatus } from './graph/lib/cmd-status.mjs'
|
|
27
|
-
import { cmdScan } from './graph/lib/cmd-scan.mjs'
|
|
28
|
-
import { cmdRun } from './graph/lib/cmd-run.mjs'
|
|
29
|
-
import { cmdKill } from './graph/lib/cmd-kill.mjs'
|
|
30
|
-
import { cmdInvalidate } from './graph/lib/cmd-invalidate.mjs'
|
|
31
|
-
import { cmdDone, cmdAudit, cmdFailed, cmdSpawn } from './graph/lib/cmd-signals.mjs'
|
|
32
|
-
import { cmdWatch } from './graph/lib/cmd-watch.mjs'
|
|
33
|
-
|
|
34
|
-
const USAGE = [
|
|
35
|
-
'Usage: n-cursor graph <command> [args]',
|
|
36
|
-
'',
|
|
37
|
-
'Commands:',
|
|
38
|
-
' setup init project: .n-cursor.json, tasks/, git hook',
|
|
39
|
-
' init <name> create task.md template',
|
|
40
|
-
' plan [<path>] [--mode agent] Stage 1: write plan_NNN.md',
|
|
41
|
-
' status [<path>] [--json] show DAG state',
|
|
42
|
-
' scan [--json] full scan, exit 1 if failed',
|
|
43
|
-
' run [<path>] [--actor a] [--auto] run node(s)',
|
|
44
|
-
' kill <path> kill worktree + cascade invalidate + delete plan_*.md',
|
|
45
|
-
' invalidate <path> [--no-cascade] mark invalidated',
|
|
46
|
-
' done <path> success → merge worktree',
|
|
47
|
-
' audit <path> pending-audit_NNN.md + merge agent worktree',
|
|
48
|
-
' failed <path> failure → write run_NNN.md',
|
|
49
|
-
' spawn <path> composite → validate children registered',
|
|
50
|
-
' watch one-shot scan: audit queue + stale + needs-plan'
|
|
51
|
-
].join('\n')
|
|
52
|
-
|
|
53
|
-
/** @type {Record<string, (args: string[], deps: object) => Promise<number>>} */
|
|
54
|
-
const COMMANDS = {
|
|
55
|
-
setup: cmdSetup,
|
|
56
|
-
init: cmdInit,
|
|
57
|
-
plan: cmdPlan,
|
|
58
|
-
status: cmdStatus,
|
|
59
|
-
scan: cmdScan,
|
|
60
|
-
run: cmdRun,
|
|
61
|
-
kill: cmdKill,
|
|
62
|
-
invalidate: cmdInvalidate,
|
|
63
|
-
done: cmdDone,
|
|
64
|
-
audit: cmdAudit,
|
|
65
|
-
failed: cmdFailed,
|
|
66
|
-
spawn: cmdSpawn,
|
|
67
|
-
watch: cmdWatch
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Точка входу `n-cursor graph` та `n-cursor watch`.
|
|
72
|
-
* Парсить підкоманду і маршрутизує до відповідного handler-а.
|
|
73
|
-
*
|
|
74
|
-
* @param {string[]} args аргументи після `graph` (або `watch`)
|
|
75
|
-
* @param {{
|
|
76
|
-
* cwd?: string,
|
|
77
|
-
* log?: (m: string) => void,
|
|
78
|
-
* [key: string]: unknown
|
|
79
|
-
* }} [deps] ін'єкції (пробрасуються до підкоманд)
|
|
80
|
-
* @returns {Promise<number>} exit code
|
|
81
|
-
*/
|
|
82
|
-
export async function runGraphTasksCli(args, deps = {}) {
|
|
83
|
-
const [sub, ...rest] = args
|
|
84
|
-
|
|
85
|
-
if (!sub || !Object.hasOwn(COMMANDS, sub)) {
|
|
86
|
-
console.error(USAGE)
|
|
87
|
-
if (sub) console.error(`\nНевідома підкоманда: "${sub}"`)
|
|
88
|
-
return 1
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return await COMMANDS[sub](rest, deps)
|
|
92
|
-
}
|