@nitra/cursor 4.1.2 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/bin/docs/n-cursor.md +1 -9
- package/bin/n-cursor.js +3 -25
- package/package.json +1 -1
- package/rules/docker/lib/docs/docker-mirror.md +1 -1
- package/rules/docker/lib/docs/docker-native-addon.md +1 -1
- package/rules/npm-module/npm-module.mdc +1 -1
- package/rules/npm-module/policy/npm_publish_yml/template/npm-publish.yml.snippet.yml +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/docs/flow.MD +0 -1364
- 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,328 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Сигнальні команди: `done`, `audit`, `failed`, `spawn`.
|
|
3
|
-
*
|
|
4
|
-
* Ці команди викликаються зсередини worktree (агентом або скриптом),
|
|
5
|
-
* або зовні через `n-cursor graph done|audit|failed|spawn <path>`.
|
|
6
|
-
*
|
|
7
|
-
* done → записує run_NNN.md (result:success), мерджить worktree
|
|
8
|
-
* audit → знаходить latest fact_NNN.md, створює pending-audit_NNN.md,
|
|
9
|
-
* записує run_NNN.md, мерджить worktree
|
|
10
|
-
* failed → записує run_NNN.md (result:failed), залишає worktree
|
|
11
|
-
* spawn → перевіряє що дочірні вузли зареєстровані (мають task.md)
|
|
12
|
-
*
|
|
13
|
-
* FS і child_process ін'єктуються для тестованості.
|
|
14
|
-
*/
|
|
15
|
-
import { execSync } from 'node:child_process'
|
|
16
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
17
|
-
import { join } from 'node:path'
|
|
18
|
-
import { cwd as processCwd } from 'node:process'
|
|
19
|
-
|
|
20
|
-
import { buildMarkdown } from './frontmatter.mjs'
|
|
21
|
-
import { latestFactNNN, nextRunNNN } from './nnn.mjs'
|
|
22
|
-
import { loadConfig, resolveTasksDir, resolveWorktreesDir } from './config.mjs'
|
|
23
|
-
import { findNodeWorktree, listActiveWorktrees, mergeWorktree } from './worktree-ops.mjs'
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Пише run_NNN.md артефакт.
|
|
27
|
-
* @param {string} nodeDir директорія вузла
|
|
28
|
-
* @param {string} nnn NNN рядок
|
|
29
|
-
* @param {'success'|'failed'} result результат
|
|
30
|
-
* @param {{ actor: string, now: string }} meta метадані
|
|
31
|
-
* @param {(p: string, c: string, enc: string) => void} writeFile функція запису
|
|
32
|
-
*/
|
|
33
|
-
function writeRunFile(nodeDir, nnn, result, meta, writeFile) {
|
|
34
|
-
const fm = {
|
|
35
|
-
created_at: meta.now,
|
|
36
|
-
actor: meta.actor,
|
|
37
|
-
result
|
|
38
|
-
}
|
|
39
|
-
const content = buildMarkdown(fm, `## Run ${nnn}\n\nactor: ${meta.actor}\nresult: ${result}\n`)
|
|
40
|
-
writeFile(join(nodeDir, `run_${nnn}.md`), content, 'utf8')
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Резолвить шлях вузла з аргументів або env/fallback-файлу.
|
|
45
|
-
* @param {string[]} args аргументи командного рядка
|
|
46
|
-
* @param {{ env?: Record<string, string>, cwd?: string, exists?: (p: string) => boolean, readFile?: (p: string, enc: string) => string }} deps ін'єкції
|
|
47
|
-
* @returns {{ nodePath: string | null, error: string | null }} результат
|
|
48
|
-
*/
|
|
49
|
-
function resolveNodePath(args, deps) {
|
|
50
|
-
// 1. Прямий аргумент
|
|
51
|
-
if (args[0] && !args[0].startsWith('-')) {
|
|
52
|
-
return { nodePath: args[0], error: null }
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// 2. ENV var
|
|
56
|
-
const env = deps.env ?? process.env
|
|
57
|
-
const fromEnv = env['NCURSOR_NODE_PATH']
|
|
58
|
-
if (fromEnv?.trim()) {
|
|
59
|
-
return { nodePath: fromEnv.trim(), error: null }
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// 3. Fallback файл .n-cursor/current-node
|
|
63
|
-
const cwd = deps.cwd ?? processCwd()
|
|
64
|
-
const exists = deps.exists ?? existsSync
|
|
65
|
-
const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
|
|
66
|
-
const fallbackPath = join(cwd, '.n-cursor', 'current-node')
|
|
67
|
-
if (exists(fallbackPath)) {
|
|
68
|
-
try {
|
|
69
|
-
const content = readFile(fallbackPath, 'utf8').trim()
|
|
70
|
-
if (content.length > 0) return { nodePath: content, error: null }
|
|
71
|
-
} catch {
|
|
72
|
-
// пропускаємо
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return { nodePath: null, error: 'NCURSOR_NODE_PATH not set and .n-cursor/current-node not found' }
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* `graph done <path>` — успіх → пише run_NNN.md (success), мерджить worktree.
|
|
81
|
-
* @param {string[]} args аргументи
|
|
82
|
-
* @param {object} [deps] ін'єкції
|
|
83
|
-
* @returns {Promise<number>} exit code
|
|
84
|
-
*/
|
|
85
|
-
export async function cmdDone(args, deps = {}) {
|
|
86
|
-
const root = deps.cwd ?? processCwd()
|
|
87
|
-
const log = deps.log ?? console.log
|
|
88
|
-
const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
|
|
89
|
-
const writeFile = deps.writeFile ?? ((p, c, enc) => writeFileSync(p, c, enc))
|
|
90
|
-
const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
|
|
91
|
-
const exists = deps.exists ?? existsSync
|
|
92
|
-
const execSyncFn = deps.execSync ?? ((cmd, o) => execSync(cmd, { ...o, encoding: 'utf8' }))
|
|
93
|
-
const nowFn = deps.now ?? (() => new Date().toISOString())
|
|
94
|
-
|
|
95
|
-
const { nodePath, error } = resolveNodePath(args, { env: deps.env, cwd: root, exists, readFile })
|
|
96
|
-
if (!nodePath) {
|
|
97
|
-
log(`done: ${error}`)
|
|
98
|
-
return 1
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const config = loadConfig({ root, readFile, exists })
|
|
102
|
-
const tasksDir = resolveTasksDir(config, root)
|
|
103
|
-
const worktreesDir = resolveWorktreesDir(config, root)
|
|
104
|
-
const nodeDir = join(tasksDir, nodePath)
|
|
105
|
-
|
|
106
|
-
if (!exists(join(nodeDir, 'task.md'))) {
|
|
107
|
-
log(`done: вузол "${nodePath}" не знайдено`)
|
|
108
|
-
return 1
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Записуємо run_NNN.md
|
|
112
|
-
const nnn = nextRunNNN(nodeDir, readdir)
|
|
113
|
-
try {
|
|
114
|
-
writeRunFile(nodeDir, nnn, 'success', { actor: 'agent', now: nowFn() }, writeFile)
|
|
115
|
-
log(`done: записано run_${nnn}.md (result: success)`)
|
|
116
|
-
} catch (err) {
|
|
117
|
-
log(`done: не вдалося записати run_${nnn}.md — ${err.message ?? String(err)}`)
|
|
118
|
-
return 1
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Знаходимо і мерджимо worktree
|
|
122
|
-
const worktreePath = findNodeWorktree(nodePath, worktreesDir, {
|
|
123
|
-
readdirSync: readdir,
|
|
124
|
-
execSync: execSyncFn
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
if (worktreePath) {
|
|
128
|
-
const mergeResult = mergeWorktree(worktreePath, root, { execSync: execSyncFn })
|
|
129
|
-
if (!mergeResult.ok) {
|
|
130
|
-
log(`done: merge не вдався — ${mergeResult.error}`)
|
|
131
|
-
return 1
|
|
132
|
-
}
|
|
133
|
-
log(`done: worktree merged і видалено`)
|
|
134
|
-
} else {
|
|
135
|
-
log(`done: worktree не знайдено для "${nodePath}" — пропускаємо merge`)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
log(`done: вузол "${nodePath}" успішно завершено`)
|
|
139
|
-
return 0
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* `graph audit <path>` — аудит → creates pending-audit_NNN.md, merge worktree.
|
|
144
|
-
* @param {string[]} args аргументи
|
|
145
|
-
* @param {object} [deps] ін'єкції
|
|
146
|
-
* @returns {Promise<number>} exit code
|
|
147
|
-
*/
|
|
148
|
-
export async function cmdAudit(args, deps = {}) {
|
|
149
|
-
const root = deps.cwd ?? processCwd()
|
|
150
|
-
const log = deps.log ?? console.log
|
|
151
|
-
const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
|
|
152
|
-
const writeFile = deps.writeFile ?? ((p, c, enc) => writeFileSync(p, c, enc))
|
|
153
|
-
const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
|
|
154
|
-
const exists = deps.exists ?? existsSync
|
|
155
|
-
const execSyncFn = deps.execSync ?? ((cmd, o) => execSync(cmd, { ...o, encoding: 'utf8' }))
|
|
156
|
-
const nowFn = deps.now ?? (() => new Date().toISOString())
|
|
157
|
-
|
|
158
|
-
const { nodePath, error } = resolveNodePath(args, { env: deps.env, cwd: root, exists, readFile })
|
|
159
|
-
if (!nodePath) {
|
|
160
|
-
log(`audit: ${error}`)
|
|
161
|
-
return 1
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const config = loadConfig({ root, readFile, exists })
|
|
165
|
-
const tasksDir = resolveTasksDir(config, root)
|
|
166
|
-
const worktreesDir = resolveWorktreesDir(config, root)
|
|
167
|
-
const nodeDir = join(tasksDir, nodePath)
|
|
168
|
-
|
|
169
|
-
if (!exists(join(nodeDir, 'task.md'))) {
|
|
170
|
-
log(`audit: вузол "${nodePath}" не знайдено`)
|
|
171
|
-
return 1
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Знаходимо latest fact_NNN.md NNN
|
|
175
|
-
const factNNN = latestFactNNN(nodeDir, readdir)
|
|
176
|
-
if (!factNNN) {
|
|
177
|
-
log(`audit: fact_NNN.md не знайдено для "${nodePath}" — спершу виконайте задачу`)
|
|
178
|
-
return 1
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Створюємо pending-audit_NNN.md
|
|
182
|
-
const pendingPath = join(nodeDir, `pending-audit_${factNNN}.md`)
|
|
183
|
-
if (exists(pendingPath)) {
|
|
184
|
-
log(`audit: ${pendingPath} вже існує — audit вже запитано`)
|
|
185
|
-
return 1
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const pendingContent = buildMarkdown({
|
|
189
|
-
created_at: nowFn(),
|
|
190
|
-
fact_ref: `fact_${factNNN}.md`,
|
|
191
|
-
actor: 'agent'
|
|
192
|
-
}, '')
|
|
193
|
-
|
|
194
|
-
try {
|
|
195
|
-
writeFile(pendingPath, pendingContent, 'utf8')
|
|
196
|
-
log(`audit: створено ${pendingPath}`)
|
|
197
|
-
} catch (err) {
|
|
198
|
-
log(`audit: не вдалося записати ${pendingPath} — ${err.message ?? String(err)}`)
|
|
199
|
-
return 1
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Записуємо run_NNN.md
|
|
203
|
-
const nnn = nextRunNNN(nodeDir, readdir)
|
|
204
|
-
try {
|
|
205
|
-
writeRunFile(nodeDir, nnn, 'success', { actor: 'agent', now: nowFn() }, writeFile)
|
|
206
|
-
log(`audit: записано run_${nnn}.md`)
|
|
207
|
-
} catch (err) {
|
|
208
|
-
log(`audit: не вдалося записати run_${nnn}.md — ${err.message ?? String(err)}`)
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Мерджимо worktree агента
|
|
212
|
-
const worktreePath = findNodeWorktree(nodePath, worktreesDir, {
|
|
213
|
-
readdirSync: readdir,
|
|
214
|
-
execSync: execSyncFn
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
if (worktreePath) {
|
|
218
|
-
const mergeResult = mergeWorktree(worktreePath, root, { execSync: execSyncFn })
|
|
219
|
-
if (!mergeResult.ok) {
|
|
220
|
-
log(`audit: merge не вдався — ${mergeResult.error}`)
|
|
221
|
-
} else {
|
|
222
|
-
log(`audit: agent worktree merged і видалено`)
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
log(`audit: запит аудиту для "${nodePath}" (fact_${factNNN}.md) успішно створено`)
|
|
227
|
-
return 0
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* `graph failed <path>` — провал → пише run_NNN.md (failed), залишає worktree.
|
|
232
|
-
* @param {string[]} args аргументи
|
|
233
|
-
* @param {object} [deps] ін'єкції
|
|
234
|
-
* @returns {Promise<number>} exit code
|
|
235
|
-
*/
|
|
236
|
-
export async function cmdFailed(args, deps = {}) {
|
|
237
|
-
const root = deps.cwd ?? processCwd()
|
|
238
|
-
const log = deps.log ?? console.log
|
|
239
|
-
const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
|
|
240
|
-
const writeFile = deps.writeFile ?? ((p, c, enc) => writeFileSync(p, c, enc))
|
|
241
|
-
const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
|
|
242
|
-
const exists = deps.exists ?? existsSync
|
|
243
|
-
const nowFn = deps.now ?? (() => new Date().toISOString())
|
|
244
|
-
|
|
245
|
-
const { nodePath, error } = resolveNodePath(args, { env: deps.env, cwd: root, exists, readFile })
|
|
246
|
-
if (!nodePath) {
|
|
247
|
-
log(`failed: ${error}`)
|
|
248
|
-
return 1
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const config = loadConfig({ root, readFile, exists })
|
|
252
|
-
const tasksDir = resolveTasksDir(config, root)
|
|
253
|
-
const nodeDir = join(tasksDir, nodePath)
|
|
254
|
-
|
|
255
|
-
if (!exists(join(nodeDir, 'task.md'))) {
|
|
256
|
-
log(`failed: вузол "${nodePath}" не знайдено`)
|
|
257
|
-
return 1
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Записуємо run_NNN.md з result:failed
|
|
261
|
-
const nnn = nextRunNNN(nodeDir, readdir)
|
|
262
|
-
try {
|
|
263
|
-
writeRunFile(nodeDir, nnn, 'failed', { actor: 'agent', now: nowFn() }, writeFile)
|
|
264
|
-
log(`failed: записано run_${nnn}.md (result: failed)`)
|
|
265
|
-
} catch (err) {
|
|
266
|
-
log(`failed: не вдалося записати run_${nnn}.md — ${err.message ?? String(err)}`)
|
|
267
|
-
return 1
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
log(`failed: вузол "${nodePath}" позначено як failed — worktree збережено для діагностики`)
|
|
271
|
-
return 0
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* `graph spawn <path>` — composite → перевіряє що дочірні вузли зареєстровані.
|
|
276
|
-
* @param {string[]} args аргументи
|
|
277
|
-
* @param {object} [deps] ін'єкції
|
|
278
|
-
* @returns {Promise<number>} exit code
|
|
279
|
-
*/
|
|
280
|
-
export async function cmdSpawn(args, deps = {}) {
|
|
281
|
-
const root = deps.cwd ?? processCwd()
|
|
282
|
-
const log = deps.log ?? console.log
|
|
283
|
-
const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
|
|
284
|
-
const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
|
|
285
|
-
const exists = deps.exists ?? existsSync
|
|
286
|
-
|
|
287
|
-
const { nodePath, error } = resolveNodePath(args, { env: deps.env, cwd: root, exists, readFile })
|
|
288
|
-
if (!nodePath) {
|
|
289
|
-
log(`spawn: ${error}`)
|
|
290
|
-
return 1
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const config = loadConfig({ root, readFile, exists })
|
|
294
|
-
const tasksDir = resolveTasksDir(config, root)
|
|
295
|
-
const nodeDir = join(tasksDir, nodePath)
|
|
296
|
-
|
|
297
|
-
if (!exists(join(nodeDir, 'task.md'))) {
|
|
298
|
-
log(`spawn: вузол "${nodePath}" не знайдено`)
|
|
299
|
-
return 1
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Перевіряємо дочірні директорії
|
|
303
|
-
let entries
|
|
304
|
-
try {
|
|
305
|
-
entries = readdir(nodeDir)
|
|
306
|
-
} catch {
|
|
307
|
-
log(`spawn: не вдалося прочитати директорію вузла`)
|
|
308
|
-
return 1
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const childDirs = entries.filter(name => {
|
|
312
|
-
if (name.startsWith('.') || name.endsWith('.md') || name.endsWith('.json')) return false
|
|
313
|
-
return exists(join(nodeDir, name, 'task.md'))
|
|
314
|
-
})
|
|
315
|
-
|
|
316
|
-
if (childDirs.length === 0) {
|
|
317
|
-
log(`spawn: вузол "${nodePath}" не має дочірніх вузлів із task.md`)
|
|
318
|
-
log(`spawn: для composite вузла треба створити дочірні директорії з task.md`)
|
|
319
|
-
return 1
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
log(`spawn: вузол "${nodePath}" є composite з ${childDirs.length} дочірніми вузлами:`)
|
|
323
|
-
for (const child of childDirs) {
|
|
324
|
-
log(` - ${nodePath}/${child}`)
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
return 0
|
|
328
|
-
}
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `n-cursor graph status [<path>] [--json]` — показує стан DAG вузлів.
|
|
3
|
-
*
|
|
4
|
-
* Без path — показує всі вузли. З path — лише вузол і його нащадків.
|
|
5
|
-
* --json — machine-readable JSON вивід.
|
|
6
|
-
*
|
|
7
|
-
* FS ін'єктується для тестованості.
|
|
8
|
-
*/
|
|
9
|
-
import { execSync } from 'node:child_process'
|
|
10
|
-
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
11
|
-
import { join } from 'node:path'
|
|
12
|
-
import { cwd as processCwd } from 'node:process'
|
|
13
|
-
|
|
14
|
-
import { loadConfig, resolveTasksDir } from './config.mjs'
|
|
15
|
-
import { scanNodes, topoSort } from './scanner.mjs'
|
|
16
|
-
import { listActiveWorktrees } from './worktree-ops.mjs'
|
|
17
|
-
|
|
18
|
-
/** Кольори для стану (ANSI). */
|
|
19
|
-
const STATE_COLORS = {
|
|
20
|
-
'needs-plan': '\x1b[33m', // жовтий
|
|
21
|
-
waiting: '\x1b[36m', // блакитний
|
|
22
|
-
running: '\x1b[34m', // синій
|
|
23
|
-
'pending-audit': '\x1b[35m', // фіолетовий
|
|
24
|
-
resolved: '\x1b[32m', // зелений
|
|
25
|
-
failed: '\x1b[31m', // червоний
|
|
26
|
-
invalidated: '\x1b[90m' // сірий
|
|
27
|
-
}
|
|
28
|
-
const RESET = '\x1b[0m'
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Повертає colored рядок стану (якщо TTY).
|
|
32
|
-
* @param {string} state стан вузла
|
|
33
|
-
* @param {boolean} color чи потрібен колір
|
|
34
|
-
* @returns {string} рядок
|
|
35
|
-
*/
|
|
36
|
-
function colorState(state, color) {
|
|
37
|
-
if (!color) return state
|
|
38
|
-
const c = STATE_COLORS[state] ?? ''
|
|
39
|
-
return `${c}${state}${RESET}`
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* `graph status [<path>] [--json]` command handler.
|
|
44
|
-
* @param {string[]} args аргументи
|
|
45
|
-
* @param {{
|
|
46
|
-
* cwd?: string,
|
|
47
|
-
* log?: (m: string) => void,
|
|
48
|
-
* readFile?: (p: string, enc: string) => string,
|
|
49
|
-
* readdir?: (d: string) => string[],
|
|
50
|
-
* exists?: (p: string) => boolean,
|
|
51
|
-
* execSync?: (cmd: string, opts?: object) => string
|
|
52
|
-
* }} [deps] ін'єкції
|
|
53
|
-
* @returns {Promise<number>} exit code
|
|
54
|
-
*/
|
|
55
|
-
export async function cmdStatus(args, deps = {}) {
|
|
56
|
-
const root = deps.cwd ?? processCwd()
|
|
57
|
-
const log = deps.log ?? console.log
|
|
58
|
-
const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
|
|
59
|
-
const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
|
|
60
|
-
const exists = deps.exists ?? existsSync
|
|
61
|
-
const execSyncFn = deps.execSync ?? ((cmd, opts) => execSync(cmd, { ...opts, encoding: 'utf8' }))
|
|
62
|
-
|
|
63
|
-
// Парсимо аргументи
|
|
64
|
-
let nodePath = null
|
|
65
|
-
let jsonMode = false
|
|
66
|
-
|
|
67
|
-
for (const arg of args) {
|
|
68
|
-
if (arg === '--json') jsonMode = true
|
|
69
|
-
else if (!arg.startsWith('-')) nodePath = arg
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const config = loadConfig({ root, readFile, exists })
|
|
73
|
-
const tasksDir = resolveTasksDir(config, root)
|
|
74
|
-
const worktreesDir = join(root, config.worktrees_dir.startsWith('/') ? config.worktrees_dir : config.worktrees_dir.slice(2))
|
|
75
|
-
|
|
76
|
-
const activeWorktrees = listActiveWorktrees(root, { execSync: execSyncFn })
|
|
77
|
-
|
|
78
|
-
const allNodes = scanNodes(tasksDir, activeWorktrees, {
|
|
79
|
-
readdirSync: readdir,
|
|
80
|
-
existsSync: exists,
|
|
81
|
-
readFileSync: readFile
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
// Фільтруємо якщо є path
|
|
85
|
-
let nodes = allNodes
|
|
86
|
-
if (nodePath) {
|
|
87
|
-
nodes = allNodes.filter(n => n.path === nodePath || n.path.startsWith(nodePath + '/'))
|
|
88
|
-
if (nodes.length === 0) {
|
|
89
|
-
log(`status: вузол "${nodePath}" не знайдено`)
|
|
90
|
-
return 1
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const sorted = topoSort(nodes)
|
|
95
|
-
|
|
96
|
-
if (jsonMode) {
|
|
97
|
-
console.log(JSON.stringify(sorted.map(n => ({
|
|
98
|
-
id: n.id,
|
|
99
|
-
path: n.path,
|
|
100
|
-
state: n.state,
|
|
101
|
-
deps: n.deps,
|
|
102
|
-
composite: n.composite,
|
|
103
|
-
children: n.children
|
|
104
|
-
})), null, 2))
|
|
105
|
-
return 0
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Текстовий вивід
|
|
109
|
-
const useColor = process.stdout.isTTY ?? false
|
|
110
|
-
|
|
111
|
-
// Підрахунок по станах
|
|
112
|
-
const stateCounts = {}
|
|
113
|
-
for (const n of sorted) {
|
|
114
|
-
stateCounts[n.state] = (stateCounts[n.state] ?? 0) + 1
|
|
115
|
-
}
|
|
116
|
-
const summary = Object.entries(stateCounts)
|
|
117
|
-
.map(([s, c]) => `${colorState(s, useColor)}:${c}`)
|
|
118
|
-
.join(' ')
|
|
119
|
-
|
|
120
|
-
log(`DAG tasks — ${summary}`)
|
|
121
|
-
log('')
|
|
122
|
-
|
|
123
|
-
for (const node of sorted) {
|
|
124
|
-
const indent = node.path.includes('/') ? ' '.repeat(node.path.split('/').length - 1) : ''
|
|
125
|
-
const composite = node.composite ? ' [composite]' : ''
|
|
126
|
-
const deps = node.deps.length > 0 ? ` ← [${node.deps.join(', ')}]` : ''
|
|
127
|
-
log(`${indent}${node.path} [${colorState(node.state, useColor)}]${composite}${deps}`)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return 0
|
|
131
|
-
}
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Handler `flow verify` — Stage 2 structural check (думка.MD § "flow verify").
|
|
3
|
-
*
|
|
4
|
-
* Перевіряє що `fact_NNN.md` існує і непорожній у директорії поточного вузла
|
|
5
|
-
* (CWD). Якщо так — виводить `## Done when` секцію з `task.md` та вміст
|
|
6
|
-
* `fact_NNN.md` на stdout для агентської self-evaluation.
|
|
7
|
-
*
|
|
8
|
-
* exit 0 = структурно OK
|
|
9
|
-
* exit 1 = структурна помилка (fact відсутній або порожній)
|
|
10
|
-
*
|
|
11
|
-
* FS ін'єктується для тестування без диска.
|
|
12
|
-
*/
|
|
13
|
-
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
14
|
-
import { join } from 'node:path'
|
|
15
|
-
import { cwd as processCwd } from 'node:process'
|
|
16
|
-
|
|
17
|
-
import { latestFactNNN } from './nnn.mjs'
|
|
18
|
-
|
|
19
|
-
const FRONT_MATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/
|
|
20
|
-
const SECTION_RE = /^## (.+)$/m
|
|
21
|
-
const LINE_SPLIT_RE = /\r?\n/
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Читає секцію за заголовком із markdown-файлу.
|
|
25
|
-
* @param {string} text вміст файлу
|
|
26
|
-
* @param {string} heading заголовок без `## `
|
|
27
|
-
* @returns {string | null} вміст секції або null
|
|
28
|
-
*/
|
|
29
|
-
function extractSection(text, heading) {
|
|
30
|
-
const lines = text.split(LINE_SPLIT_RE)
|
|
31
|
-
const start = lines.indexOf(`## ${heading}`)
|
|
32
|
-
if (start === -1) return null
|
|
33
|
-
const end = lines.findIndex((l, i) => i > start && SECTION_RE.test(l))
|
|
34
|
-
const section = end === -1 ? lines.slice(start) : lines.slice(start, end)
|
|
35
|
-
return section.join('\n').trimEnd()
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* `flow verify` handler.
|
|
40
|
-
* @param {string[]} _rest аргументи після `verify` (не використовуються)
|
|
41
|
-
* @param {{
|
|
42
|
-
* cwd?: string,
|
|
43
|
-
* log?: (m: string) => void,
|
|
44
|
-
* readFile?: (path: string, enc: string) => string,
|
|
45
|
-
* readdir?: (dir: string) => string[],
|
|
46
|
-
* exists?: (path: string) => boolean
|
|
47
|
-
* }} [deps] ін'єкції
|
|
48
|
-
* @returns {number} exit code (0=OK, 1=структурна помилка)
|
|
49
|
-
*/
|
|
50
|
-
export function cmdVerify(_rest, deps = {}) {
|
|
51
|
-
const cwd = deps.cwd ?? processCwd()
|
|
52
|
-
const log = deps.log ?? console.error
|
|
53
|
-
const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
|
|
54
|
-
const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
|
|
55
|
-
const exists = deps.exists ?? existsSync
|
|
56
|
-
|
|
57
|
-
const factNNN = latestFactNNN(cwd, readdir)
|
|
58
|
-
if (!factNNN) {
|
|
59
|
-
log('verify: fact_NNN.md не знайдено — структурна помилка')
|
|
60
|
-
return 1
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const factPath = join(cwd, `fact_${factNNN}.md`)
|
|
64
|
-
if (!exists(factPath)) {
|
|
65
|
-
log(`verify: fact_${factNNN}.md не існує — структурна помилка`)
|
|
66
|
-
return 1
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
let factContent
|
|
70
|
-
try {
|
|
71
|
-
factContent = readFile(factPath, 'utf8')
|
|
72
|
-
} catch (error) {
|
|
73
|
-
log(`verify: не вдалося прочитати fact_${factNNN}.md — ${error instanceof Error ? error.message : String(error)}`)
|
|
74
|
-
return 1
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const withoutFm = factContent.replace(FRONT_MATTER_RE, '').trim()
|
|
78
|
-
if (withoutFm.length === 0) {
|
|
79
|
-
log(`verify: fact_${factNNN}.md порожній — структурна помилка`)
|
|
80
|
-
return 1
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const outLines = [`## verify context`, ``]
|
|
84
|
-
|
|
85
|
-
const taskPath = join(cwd, 'task.md')
|
|
86
|
-
if (exists(taskPath)) {
|
|
87
|
-
try {
|
|
88
|
-
const taskContent = readFile(taskPath, 'utf8')
|
|
89
|
-
const doneWhen = extractSection(taskContent, 'Done when')
|
|
90
|
-
if (doneWhen) outLines.push(doneWhen, '')
|
|
91
|
-
} catch {
|
|
92
|
-
// task.md недоступний — не блокуємо verify
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
outLines.push(`### fact_${factNNN}.md`, ``, factContent.trimEnd())
|
|
97
|
-
console.log(outLines.join('\n'))
|
|
98
|
-
|
|
99
|
-
return 0
|
|
100
|
-
}
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `n-cursor watch` — одноразовий скан стану DAG.
|
|
3
|
-
*
|
|
4
|
-
* Спрощена (no-daemon) реалізація:
|
|
5
|
-
* - Знаходить pending-audit без audit-result → логує (треба ручний аудит)
|
|
6
|
-
* - Знаходить stale worktrees > stale_worktree_min хвилин → попереджає
|
|
7
|
-
* - Знаходить needs-plan вузли → перелічує
|
|
8
|
-
* - exit 0 якщо чисто, exit 1 якщо потрібна увага
|
|
9
|
-
*
|
|
10
|
-
* FS і child_process ін'єктуються для тестованості.
|
|
11
|
-
*/
|
|
12
|
-
import { execSync } from 'node:child_process'
|
|
13
|
-
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
14
|
-
import { join } from 'node:path'
|
|
15
|
-
import { cwd as processCwd } from 'node:process'
|
|
16
|
-
|
|
17
|
-
import { loadConfig, resolveTasksDir, resolveWorktreesDir } from './config.mjs'
|
|
18
|
-
import { scanNodes } from './scanner.mjs'
|
|
19
|
-
import { listActiveWorktrees } from './worktree-ops.mjs'
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* `watch` command handler (one-shot scan).
|
|
23
|
-
* @param {string[]} args аргументи (зазвичай порожні)
|
|
24
|
-
* @param {{
|
|
25
|
-
* cwd?: string,
|
|
26
|
-
* log?: (m: string) => void,
|
|
27
|
-
* readFile?: (p: string, enc: string) => string,
|
|
28
|
-
* readdir?: (d: string) => string[],
|
|
29
|
-
* exists?: (p: string) => boolean,
|
|
30
|
-
* execSync?: (cmd: string, opts?: object) => string,
|
|
31
|
-
* statSync?: (p: string) => { mtimeMs: number },
|
|
32
|
-
* now?: () => number
|
|
33
|
-
* }} [deps] ін'єкції
|
|
34
|
-
* @returns {Promise<number>} exit code (0=clean, 1=attention)
|
|
35
|
-
*/
|
|
36
|
-
export async function cmdWatch(args, deps = {}) {
|
|
37
|
-
const root = deps.cwd ?? processCwd()
|
|
38
|
-
const log = deps.log ?? console.log
|
|
39
|
-
const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
|
|
40
|
-
const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
|
|
41
|
-
const exists = deps.exists ?? existsSync
|
|
42
|
-
const execSyncFn = deps.execSync ?? ((cmd, o) => execSync(cmd, { ...o, encoding: 'utf8' }))
|
|
43
|
-
const statFn = deps.statSync ?? statSync
|
|
44
|
-
const nowMs = deps.now ?? (() => Date.now())
|
|
45
|
-
|
|
46
|
-
const config = loadConfig({ root, readFile, exists })
|
|
47
|
-
const tasksDir = resolveTasksDir(config, root)
|
|
48
|
-
const worktreesDir = resolveWorktreesDir(config, root)
|
|
49
|
-
const staleMs = config.stale_worktree_min * 60 * 1000
|
|
50
|
-
|
|
51
|
-
const activeWorktrees = listActiveWorktrees(root, { execSync: execSyncFn })
|
|
52
|
-
|
|
53
|
-
const allNodes = scanNodes(tasksDir, activeWorktrees, {
|
|
54
|
-
readdirSync: readdir,
|
|
55
|
-
existsSync: exists,
|
|
56
|
-
readFileSync: readFile
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
let needsAttention = false
|
|
60
|
-
|
|
61
|
-
// 1. Pending-audit без audit-result
|
|
62
|
-
const pendingAudit = allNodes.filter(n => n.state === 'pending-audit')
|
|
63
|
-
if (pendingAudit.length > 0) {
|
|
64
|
-
needsAttention = true
|
|
65
|
-
log(`[watch] pending-audit (${pendingAudit.length}) — потрібна ручна перевірка:`)
|
|
66
|
-
for (const n of pendingAudit) {
|
|
67
|
-
log(` - ${n.path}`)
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// 2. Stale worktrees
|
|
72
|
-
let worktreeEntries = []
|
|
73
|
-
try {
|
|
74
|
-
worktreeEntries = readdir(worktreesDir)
|
|
75
|
-
} catch {
|
|
76
|
-
// worktrees dir може не існувати
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const now = nowMs()
|
|
80
|
-
const staleWorktrees = []
|
|
81
|
-
for (const name of worktreeEntries) {
|
|
82
|
-
const wtPath = join(worktreesDir, name)
|
|
83
|
-
try {
|
|
84
|
-
const stat = statFn(wtPath)
|
|
85
|
-
const ageMs = now - stat.mtimeMs
|
|
86
|
-
if (ageMs > staleMs) {
|
|
87
|
-
staleWorktrees.push({ name, ageMin: Math.floor(ageMs / 60000) })
|
|
88
|
-
}
|
|
89
|
-
} catch {
|
|
90
|
-
// пропускаємо
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (staleWorktrees.length > 0) {
|
|
95
|
-
needsAttention = true
|
|
96
|
-
log(`[watch] stale worktrees (${staleWorktrees.length}) — неактивні > ${config.stale_worktree_min} хв:`)
|
|
97
|
-
for (const wt of staleWorktrees) {
|
|
98
|
-
log(` - ${wt.name} (${wt.ageMin} хв)`)
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// 3. Needs-plan вузли
|
|
103
|
-
const needsPlan = allNodes.filter(n => n.state === 'needs-plan')
|
|
104
|
-
if (needsPlan.length > 0) {
|
|
105
|
-
log(`[watch] needs-plan (${needsPlan.length}) — потрібне планування:`)
|
|
106
|
-
for (const n of needsPlan) {
|
|
107
|
-
log(` - ${n.path}`)
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// 4. Failed вузли
|
|
112
|
-
const failed = allNodes.filter(n => n.state === 'failed')
|
|
113
|
-
if (failed.length > 0) {
|
|
114
|
-
needsAttention = true
|
|
115
|
-
log(`[watch] failed (${failed.length}) — завершились з помилкою:`)
|
|
116
|
-
for (const n of failed) {
|
|
117
|
-
log(` - ${n.path}`)
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (!needsAttention && pendingAudit.length === 0 && failed.length === 0) {
|
|
122
|
-
const running = allNodes.filter(n => n.state === 'running').length
|
|
123
|
-
const resolved = allNodes.filter(n => n.state === 'resolved').length
|
|
124
|
-
log(`[watch] OK — total:${allNodes.length} running:${running} resolved:${resolved}`)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return needsAttention ? 1 : 0
|
|
128
|
-
}
|