@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.
Files changed (71) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/bin/docs/n-cursor.md +1 -9
  3. package/bin/n-cursor.js +3 -25
  4. package/docs/stryker.config.md +37 -0
  5. package/docs/vitest.config.md +23 -0
  6. package/package.json +2 -1
  7. package/rules/docker/lib/docs/docker-mirror.md +1 -1
  8. package/rules/docker/lib/docs/docker-native-addon.md +1 -1
  9. package/rules/test/coverage/coverage.mjs +9 -19
  10. package/rules/test/test.mdc +1 -1
  11. package/scripts/dispatcher/trace.mjs +4 -16
  12. package/scripts/docs/build-agents-commands.md +1 -1
  13. package/scripts/docs/worktree-cli.md +1 -1
  14. package/scripts/lib/changed-files.mjs +19 -3
  15. package/scripts/lib/sync-gitignore-worktree.mjs +4 -5
  16. package/scripts/worktree-cli.mjs +1 -2
  17. package/skills/docgen/js/docgen-gen.mjs +7 -7
  18. package/scripts/dispatcher/docs/graph.md +0 -346
  19. package/scripts/dispatcher/docs/index.md +0 -236
  20. package/scripts/dispatcher/docs/trace.md +0 -296
  21. package/scripts/dispatcher/graph/lib/cmd-init.mjs +0 -112
  22. package/scripts/dispatcher/graph/lib/cmd-invalidate.mjs +0 -96
  23. package/scripts/dispatcher/graph/lib/cmd-kill.mjs +0 -141
  24. package/scripts/dispatcher/graph/lib/cmd-plan.mjs +0 -142
  25. package/scripts/dispatcher/graph/lib/cmd-run.mjs +0 -328
  26. package/scripts/dispatcher/graph/lib/cmd-scan.mjs +0 -115
  27. package/scripts/dispatcher/graph/lib/cmd-setup.mjs +0 -111
  28. package/scripts/dispatcher/graph/lib/cmd-signals.mjs +0 -328
  29. package/scripts/dispatcher/graph/lib/cmd-status.mjs +0 -131
  30. package/scripts/dispatcher/graph/lib/cmd-verify.mjs +0 -100
  31. package/scripts/dispatcher/graph/lib/cmd-watch.mjs +0 -128
  32. package/scripts/dispatcher/graph/lib/config.mjs +0 -103
  33. package/scripts/dispatcher/graph/lib/frontmatter.mjs +0 -224
  34. package/scripts/dispatcher/graph/lib/nnn.mjs +0 -127
  35. package/scripts/dispatcher/graph/lib/node-state.mjs +0 -157
  36. package/scripts/dispatcher/graph/lib/scanner.mjs +0 -235
  37. package/scripts/dispatcher/graph/lib/worktree-ops.mjs +0 -193
  38. package/scripts/dispatcher/graph-tasks.mjs +0 -92
  39. package/scripts/dispatcher/graph.mjs +0 -212
  40. package/scripts/dispatcher/index.mjs +0 -45
  41. package/scripts/dispatcher/lib/docs/active.md +0 -348
  42. package/scripts/dispatcher/lib/docs/artifact.md +0 -232
  43. package/scripts/dispatcher/lib/docs/budget.md +0 -167
  44. package/scripts/dispatcher/lib/docs/capability.md +0 -196
  45. package/scripts/dispatcher/lib/docs/commands.md +0 -210
  46. package/scripts/dispatcher/lib/docs/events.md +0 -183
  47. package/scripts/dispatcher/lib/docs/executor.md +0 -190
  48. package/scripts/dispatcher/lib/docs/gate.md +0 -231
  49. package/scripts/dispatcher/lib/docs/level.md +0 -335
  50. package/scripts/dispatcher/lib/docs/plan-panel.md +0 -181
  51. package/scripts/dispatcher/lib/docs/plan.md +0 -200
  52. package/scripts/dispatcher/lib/docs/planner.md +0 -269
  53. package/scripts/dispatcher/lib/docs/review.md +0 -255
  54. package/scripts/dispatcher/lib/docs/reviewer.md +0 -240
  55. package/scripts/dispatcher/lib/docs/snapshot.md +0 -247
  56. package/scripts/dispatcher/lib/docs/spec.md +0 -203
  57. package/scripts/dispatcher/lib/docs/state-store.md +0 -303
  58. package/scripts/dispatcher/lib/docs/subagent-runner.md +0 -173
  59. package/scripts/dispatcher/lib/events.mjs +0 -67
  60. package/scripts/dispatcher/lib/executor.mjs +0 -107
  61. package/scripts/dispatcher/lib/plan-panel.mjs +0 -76
  62. package/scripts/dispatcher/lib/state-store.mjs +0 -173
  63. package/scripts/dispatcher/lib/subagent-runner.mjs +0 -53
  64. package/scripts/graph/index.mjs +0 -115
  65. package/scripts/graph/lib/config.mjs +0 -62
  66. package/scripts/graph/lib/dag.mjs +0 -161
  67. package/scripts/graph/lib/frontmatter.mjs +0 -70
  68. package/scripts/graph/lib/nnn.mjs +0 -77
  69. package/scripts/graph/lib/state.mjs +0 -110
  70. package/scripts/graph/scan.mjs +0 -64
  71. 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
- }