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