@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,142 +0,0 @@
1
- /**
2
- * `n-cursor graph plan [<path>] [--mode agent]` — Stage 1: пише plan_NNN.md.
3
- *
4
- * Читає task.md вузла, знаходить наступний NNN, пише шаблон plan_NNN.md.
5
- * Якщо --mode agent — встановлює mode:agent у plan front-matter.
6
- *
7
- * FS ін'єктується для тестованості.
8
- */
9
- import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
10
- import { join } from 'node:path'
11
- import { cwd as processCwd } from 'node:process'
12
-
13
- import { buildMarkdown, parseFrontMatter } from './frontmatter.mjs'
14
- import { nextPlanNNN } from './nnn.mjs'
15
- import { loadConfig, resolveTasksDir } from './config.mjs'
16
-
17
- /**
18
- * Будує шаблон plan_NNN.md.
19
- * @param {{ mode: string, hint: string, now: string, nnn: string }} params параметри
20
- * @returns {string} вміст файлу
21
- */
22
- export function buildPlanTemplate(params) {
23
- const fm = {
24
- created_at: params.now,
25
- mode: params.mode,
26
- decision: params.hint || 'atomic'
27
- }
28
-
29
- const body = [
30
- `## Context`,
31
- `<!-- Чому саме такий підхід — що з'ясовано під час планування -->`,
32
- ``,
33
- `## Approach`,
34
- params.mode === 'composite'
35
- ? `<!-- composite: список дочірніх вузлів з описами -->`
36
- : `<!-- atomic: покроковий план виконання -->`,
37
- ``,
38
- `## Risks`,
39
- `<!-- Що може піти не так -->`,
40
- ``
41
- ].join('\n')
42
-
43
- return buildMarkdown(fm, body)
44
- }
45
-
46
- /**
47
- * `graph plan [<path>] [--mode agent]` command handler.
48
- * @param {string[]} args аргументи: [path] [--mode agent|human]
49
- * @param {{
50
- * cwd?: string,
51
- * log?: (m: string) => void,
52
- * writeFile?: (p: string, c: string, enc: string) => void,
53
- * readFile?: (p: string, enc: string) => string,
54
- * readdir?: (d: string) => string[],
55
- * exists?: (p: string) => boolean,
56
- * now?: () => string
57
- * }} [deps] ін'єкції
58
- * @returns {Promise<number>} exit code
59
- */
60
- export async function cmdPlan(args, deps = {}) {
61
- const root = deps.cwd ?? processCwd()
62
- const log = deps.log ?? console.log
63
- const writeFile = deps.writeFile ?? ((p, c, enc) => writeFileSync(p, c, enc))
64
- const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
65
- const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
66
- const exists = deps.exists ?? existsSync
67
- const nowFn = deps.now ?? (() => new Date().toISOString())
68
-
69
- // Парсимо аргументи
70
- let nodePath = null
71
- let modeOverride = null
72
-
73
- for (let i = 0; i < args.length; i++) {
74
- if (args[i] === '--mode' && args[i + 1]) {
75
- modeOverride = args[i + 1]
76
- i++
77
- } else if (!args[i].startsWith('-')) {
78
- nodePath = args[i]
79
- }
80
- }
81
-
82
- const config = loadConfig({ root, readFile, exists })
83
- const tasksDir = resolveTasksDir(config, root)
84
-
85
- // Визначаємо директорію вузла
86
- let nodeDir
87
- if (nodePath) {
88
- nodeDir = join(tasksDir, nodePath)
89
- } else {
90
- // CWD може бути в worktree — шукаємо task.md у CWD
91
- nodeDir = processCwd()
92
- }
93
-
94
- const taskPath = join(nodeDir, 'task.md')
95
- if (!exists(taskPath)) {
96
- log(`plan: task.md не знайдено в ${nodeDir}`)
97
- return 1
98
- }
99
-
100
- let taskContent
101
- try {
102
- taskContent = readFile(taskPath, 'utf8')
103
- } catch (err) {
104
- log(`plan: не вдалося прочитати task.md — ${err.message ?? String(err)}`)
105
- return 1
106
- }
107
-
108
- const fm = parseFrontMatter(taskContent)
109
- const mode = modeOverride ?? (typeof fm.mode === 'string' ? fm.mode : 'human')
110
- const hint = typeof fm.hint === 'string' ? fm.hint : ''
111
-
112
- const nnn = nextPlanNNN(nodeDir, readdir)
113
- const planPath = join(nodeDir, `plan_${nnn}.md`)
114
-
115
- const content = buildPlanTemplate({ mode, hint, now: nowFn(), nnn })
116
-
117
- try {
118
- writeFile(planPath, content, 'utf8')
119
- log(`plan: створено ${planPath} (mode: ${mode})`)
120
- } catch (err) {
121
- log(`plan: не вдалося записати ${planPath} — ${err.message ?? String(err)}`)
122
- return 1
123
- }
124
-
125
- // Виводимо контекст для агента/людини
126
- const bodyStart = taskContent.indexOf('\n---\n', 4)
127
- const taskBody = bodyStart === -1 ? taskContent : taskContent.slice(bodyStart + 5).trimStart()
128
-
129
- console.log([
130
- `## plan context`,
131
- ``,
132
- `node: ${nodePath ?? nodeDir}`,
133
- `mode: ${mode}`,
134
- hint ? `hint: ${hint}` : `hint: (не задано)`,
135
- `plan: plan_${nnn}.md`,
136
- ``,
137
- `### task.md`,
138
- taskBody.trimEnd()
139
- ].join('\n'))
140
-
141
- return 0
142
- }
@@ -1,328 +0,0 @@
1
- /**
2
- * `n-cursor graph run [<path>] [--actor a] [--auto]` — запуск вузла(ів).
3
- *
4
- * Wrapper логіка:
5
- * 1. Читає task.md → budget_sec, budget_hard_sec, deps, mode, executor
6
- * 2. Перевіряє що всі deps resolved
7
- * 3. Обчислює NNN = count(run_*.md) + 1
8
- * 4. git worktree add .worktrees/<node-epoch>/ (atomic mkdir lock — EEXIST = skip)
9
- * 5. ENV: NCURSOR_RUN_NNN, NCURSOR_BUDGET_SEC, NCURSOR_HARD_BUDGET_SEC, NCURSOR_STARTED_AT, NCURSOR_NODE_PATH
10
- * 6. Спавнить subprocess (claude або n-cursor graph run --actor auditor)
11
- * 7. Poll worktree mtime кожні 5s: progress_timeout → SIGKILL; budget_hard → SIGKILL
12
- * 8. Після exit: fact_NNN.md є → result:success; else → result:failed
13
- * 9. Пише run_NNN.md
14
- * 10. Якщо success: git merge + delete worktree
15
- *
16
- * --auto режим: сканує для готових вузлів (waiting + deps resolved), клеймить atomic mkdir.
17
- *
18
- * FS і child_process ін'єктуються для тестованості.
19
- */
20
- import { execSync, spawnSync } from 'node:child_process'
21
- import { existsSync, mkdtempSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
22
- import { join } from 'node:path'
23
- import { cwd as processCwd } from 'node:process'
24
-
25
- import { buildMarkdown, parseFrontMatter } from './frontmatter.mjs'
26
- import { latestFactNNN, nextRunNNN } from './nnn.mjs'
27
- import { loadConfig, resolveModelByTier, resolveTasksDir, resolveWorktreesDir } from './config.mjs'
28
- import { scanNodes, topoSort, areDepsResolved } from './scanner.mjs'
29
- import { createWorktree, listActiveWorktrees, mergeWorktree } from './worktree-ops.mjs'
30
- import { makeWorktreeName } from './worktree-ops.mjs'
31
-
32
- /**
33
- * Пише run_NNN.md артефакт.
34
- * @param {string} nodeDir директорія вузла
35
- * @param {string} nnn NNN рядок
36
- * @param {'success'|'failed'} result результат
37
- * @param {{ actor: string, startedAt: string, now: string }} meta метадані
38
- * @param {(p: string, c: string, enc: string) => void} writeFile функція запису
39
- */
40
- function writeRunFile(nodeDir, nnn, result, meta, writeFile) {
41
- const fm = {
42
- created_at: meta.now,
43
- started_at: meta.startedAt,
44
- actor: meta.actor,
45
- result
46
- }
47
- const content = buildMarkdown(fm, `## Run ${nnn}\n\nactor: ${meta.actor}\nresult: ${result}\n`)
48
- writeFile(join(nodeDir, `run_${nnn}.md`), content, 'utf8')
49
- }
50
-
51
- /**
52
- * Запускає один вузол: creates worktree, spawns agent, writes run_NNN.md.
53
- * @param {string} nodePath відносний шлях вузла
54
- * @param {string} nodeDir абсолютний шлях до директорії вузла
55
- * @param {object} config конфігурація
56
- * @param {string} root корінь репо
57
- * @param {{ actor?: string, dryRun?: boolean }} opts опції
58
- * @param {object} deps ін'єкції
59
- * @returns {{ ok: boolean, code: number }} результат
60
- */
61
- function runNode(nodePath, nodeDir, config, root, opts, deps) {
62
- const log = deps.log ?? console.log
63
- const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
64
- const writeFile = deps.writeFile ?? ((p, c, enc) => writeFileSync(p, c, enc))
65
- const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
66
- const exists = deps.exists ?? existsSync
67
- const execSyncFn = deps.execSync ?? ((cmd, o) => execSync(cmd, { ...o, encoding: 'utf8' }))
68
- const spawnSyncFn = deps.spawnSync ?? spawnSync
69
- const nowFn = deps.now ?? (() => new Date().toISOString())
70
- const statFn = deps.statSync ?? statSync
71
-
72
- // 1. Читаємо task.md
73
- let fm = {}
74
- try {
75
- fm = parseFrontMatter(readFile(join(nodeDir, 'task.md'), 'utf8'))
76
- } catch (err) {
77
- log(`run: не вдалося прочитати task.md для "${nodePath}" — ${err.message ?? String(err)}`)
78
- return { ok: false, code: 1 }
79
- }
80
-
81
- const budgetSec = Number(fm.budget_sec) || config.default_budget_sec
82
- const budgetHardSec = Number(fm.budget_hard_sec) || (budgetSec * config.budget_hard_sec_multiplier)
83
- const progressTimeoutSec = config.progress_timeout_sec
84
-
85
- const executor = (fm.executor && typeof fm.executor === 'object') ? fm.executor : {}
86
- const executorType = executor.type ?? 'agent'
87
- const modelTier = executor.model_tier ?? 'AVG'
88
- const model = resolveModelByTier(config, modelTier)
89
-
90
- const actor = opts.actor ?? executorType
91
-
92
- // 2. Вже перевірено deps resolved перед викликом
93
-
94
- // 3. Обчислюємо NNN
95
- const nnn = nextRunNNN(nodeDir, readdir)
96
-
97
- // 4. Створюємо worktree (atomic mkdir lock)
98
- const worktreesDir = resolveWorktreesDir(config, root)
99
- const worktreeName = makeWorktreeName(nodePath)
100
- const worktreePath = join(worktreesDir, worktreeName)
101
-
102
- log(`run: запускаємо вузол "${nodePath}" (NNN=${nnn}, actor=${actor})`)
103
-
104
- if (opts.dryRun) {
105
- log(`run: --dry-run — пропускаємо фактичний запуск`)
106
- return { ok: true, code: 0 }
107
- }
108
-
109
- let createResult
110
- try {
111
- createResult = createWorktree(worktreesDir, worktreeName, root, { execSync: execSyncFn })
112
- } catch (err) {
113
- log(`run: не вдалося створити worktree — ${err.message ?? String(err)}`)
114
- return { ok: false, code: 1 }
115
- }
116
-
117
- if (!createResult) {
118
- log(`run: вузол "${nodePath}" вже запущено (worktree існує) — пропускаємо`)
119
- return { ok: false, code: 2 }
120
- }
121
-
122
- // 5. ENV
123
- const startedAt = nowFn()
124
- const env = {
125
- ...process.env,
126
- NCURSOR_RUN_NNN: nnn,
127
- NCURSOR_BUDGET_SEC: String(budgetSec),
128
- NCURSOR_HARD_BUDGET_SEC: String(budgetHardSec),
129
- NCURSOR_STARTED_AT: startedAt,
130
- NCURSOR_NODE_PATH: nodePath
131
- }
132
-
133
- // 6. Спавнимо subprocess (spawnSync — синхронно)
134
- let spawnResult
135
- const timeoutMs = budgetHardSec > 0 ? budgetHardSec * 1000 : undefined
136
-
137
- if (actor === 'agent' || actor === 'a') {
138
- // Запускаємо claude CLI у worktree
139
- const claudeArgs = ['--model', model, '--no-session', '-p',
140
- `You are executing task node: ${nodePath}\nWorking directory: ${worktreePath}\nRun NNN: ${nnn}\nBudget: ${budgetSec}s\n\nRead task.md and plan_*.md, execute the task, write fact_${nnn}.md with results.`
141
- ]
142
- spawnResult = spawnSyncFn('claude', claudeArgs, {
143
- cwd: worktreePath,
144
- env,
145
- encoding: 'utf8',
146
- timeout: timeoutMs
147
- })
148
- } else if (actor === 'human') {
149
- // Людина виконує вручну — чекаємо на fact файл
150
- log(`run: вузол "${nodePath}" очікує ручного виконання`)
151
- log(` worktree: ${worktreePath}`)
152
- log(` NCURSOR_RUN_NNN=${nnn}`)
153
- log(` після виконання запустіть: n-cursor graph done ${nodePath}`)
154
- // Не чекаємо — повертаємо success без run_NNN.md
155
- return { ok: true, code: 0 }
156
- } else {
157
- log(`run: невідомий actor "${actor}" — підтримується: agent, human`)
158
- return { ok: false, code: 1 }
159
- }
160
-
161
- // 8. Після exit: перевіряємо fact_NNN.md
162
- const factPath = join(worktreePath, `fact_${nnn}.md`)
163
- const factInNodeDir = join(nodeDir, `fact_${nnn}.md`)
164
-
165
- // Перевіряємо у worktree та у основній директорії
166
- const hasFactInWorktree = exists(factPath)
167
- const result = hasFactInWorktree ? 'success' : 'failed'
168
-
169
- // Якщо є факт у worktree — копіюємо в node dir (якщо worktree != nodeDir)
170
- if (hasFactInWorktree && worktreePath !== nodeDir) {
171
- try {
172
- const factContent = readFile(factPath, 'utf8')
173
- writeFile(factInNodeDir, factContent, 'utf8')
174
- } catch {
175
- // пропускаємо
176
- }
177
- }
178
-
179
- // 9. Пишемо run_NNN.md у node dir
180
- try {
181
- writeRunFile(nodeDir, nnn, result, {
182
- actor,
183
- startedAt,
184
- now: nowFn()
185
- }, writeFile)
186
- log(`run: записано run_${nnn}.md (result: ${result})`)
187
- } catch (err) {
188
- log(`run: не вдалося записати run_${nnn}.md — ${err.message ?? String(err)}`)
189
- }
190
-
191
- // 10. Якщо success: merge worktree
192
- if (result === 'success') {
193
- const mergeResult = mergeWorktree(worktreePath, root, { execSync: execSyncFn })
194
- if (!mergeResult.ok) {
195
- log(`run: merge worktree не вдався — ${mergeResult.error}`)
196
- } else {
197
- log(`run: worktree merged і видалено`)
198
- }
199
- return { ok: true, code: 0 }
200
- } else {
201
- log(`run: вузол "${nodePath}" завершився з помилкою`)
202
- log(`run: worktree збережено для діагностики: ${worktreePath}`)
203
- return { ok: false, code: 1 }
204
- }
205
- }
206
-
207
- /**
208
- * `graph run [<path>] [--actor a] [--auto]` command handler.
209
- * @param {string[]} args аргументи
210
- * @param {{
211
- * cwd?: string,
212
- * log?: (m: string) => void,
213
- * readFile?: (p: string, enc: string) => string,
214
- * writeFile?: (p: string, c: string, enc: string) => void,
215
- * readdir?: (d: string) => string[],
216
- * exists?: (p: string) => boolean,
217
- * execSync?: (cmd: string, opts?: object) => string,
218
- * spawnSync?: (cmd: string, args: string[], opts?: object) => object,
219
- * statSync?: (p: string) => object,
220
- * now?: () => string
221
- * }} [deps] ін'єкції
222
- * @returns {Promise<number>} exit code
223
- */
224
- export async function cmdRun(args, deps = {}) {
225
- const root = deps.cwd ?? processCwd()
226
- const log = deps.log ?? console.log
227
- const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
228
- const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
229
- const exists = deps.exists ?? existsSync
230
- const execSyncFn = deps.execSync ?? ((cmd, o) => execSync(cmd, { ...o, encoding: 'utf8' }))
231
-
232
- // Парсимо аргументи
233
- let nodePath = null
234
- let actor = null
235
- let autoMode = false
236
-
237
- for (let i = 0; i < args.length; i++) {
238
- if (args[i] === '--actor' && args[i + 1]) {
239
- actor = args[i + 1]
240
- i++
241
- } else if (args[i] === '--auto') {
242
- autoMode = true
243
- } else if (!args[i].startsWith('-')) {
244
- nodePath = args[i]
245
- }
246
- }
247
-
248
- const config = loadConfig({ root, readFile, exists })
249
- const tasksDir = resolveTasksDir(config, root)
250
-
251
- const activeWorktrees = listActiveWorktrees(root, { execSync: execSyncFn })
252
-
253
- // Перевіряємо ліміт worktrees
254
- if (activeWorktrees.size >= config.max_worktrees) {
255
- log(`run: досягнуто max_worktrees (${config.max_worktrees}) — зачекайте завершення поточних задач`)
256
- return 1
257
- }
258
-
259
- if (activeWorktrees.size >= config.warn_worktrees_above) {
260
- log(`run: увага — ${activeWorktrees.size} активних worktrees (попередження при >${config.warn_worktrees_above})`)
261
- }
262
-
263
- if (autoMode) {
264
- // Знаходимо всі ready вузли і запускаємо їх
265
- const allNodes = scanNodes(tasksDir, activeWorktrees, {
266
- readdirSync: readdir,
267
- existsSync: exists,
268
- readFileSync: readFile
269
- })
270
- const nodeMap = new Map(allNodes.map(n => [n.id, n]))
271
- const readyNodes = topoSort(allNodes).filter(n => n.state === 'waiting' && areDepsResolved(n, nodeMap))
272
-
273
- if (readyNodes.length === 0) {
274
- log('run --auto: немає готових вузлів для запуску')
275
- return 0
276
- }
277
-
278
- log(`run --auto: знайдено ${readyNodes.length} готових вузлів`)
279
- let anyFailed = false
280
-
281
- for (const node of readyNodes) {
282
- const result = runNode(node.path, node.dir, config, root, { actor: actor ?? undefined }, {
283
- ...deps,
284
- log,
285
- execSync: execSyncFn
286
- })
287
- if (!result.ok && result.code !== 2) anyFailed = true
288
- }
289
-
290
- return anyFailed ? 1 : 0
291
- }
292
-
293
- // Запускаємо конкретний вузол або вузол у CWD
294
- if (!nodePath) {
295
- log('run: вкажіть <path> або використайте --auto')
296
- log('Usage: n-cursor graph run [<path>] [--actor agent|human] [--auto]')
297
- return 1
298
- }
299
-
300
- const nodeDir = join(tasksDir, nodePath)
301
- if (!exists(join(nodeDir, 'task.md'))) {
302
- log(`run: вузол "${nodePath}" не знайдено (немає task.md у ${nodeDir})`)
303
- return 1
304
- }
305
-
306
- // Перевіряємо deps
307
- const allNodes = scanNodes(tasksDir, activeWorktrees, {
308
- readdirSync: readdir,
309
- existsSync: exists,
310
- readFileSync: readFile
311
- })
312
- const nodeMap = new Map(allNodes.map(n => [n.id, n]))
313
- const targetNode = nodeMap.get(nodePath)
314
-
315
- if (targetNode && !areDepsResolved(targetNode, nodeMap)) {
316
- const unresolvedDeps = targetNode.deps.filter(dep => nodeMap.get(dep)?.state !== 'resolved')
317
- log(`run: вузол "${nodePath}" має невирішені залежності: ${unresolvedDeps.join(', ')}`)
318
- return 1
319
- }
320
-
321
- const result = runNode(nodePath, nodeDir, config, root, { actor: actor ?? undefined }, {
322
- ...deps,
323
- log,
324
- execSync: execSyncFn
325
- })
326
-
327
- return result.code
328
- }
@@ -1,115 +0,0 @@
1
- /**
2
- * `n-cursor graph scan [--json]` — повний скан DAG, exit 1 якщо є failed-вузли.
3
- *
4
- * Обходить весь DAG, деривує стани, виводить зведення.
5
- * exit 0 = все чисто (або лише needs-plan/waiting)
6
- * exit 1 = є failed або pending-audit без відповіді
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
- import { cwd as processCwd } from 'node:process'
14
-
15
- import { loadConfig, resolveTasksDir } from './config.mjs'
16
- import { scanNodes, topoSort, areDepsResolved } from './scanner.mjs'
17
- import { listActiveWorktrees } from './worktree-ops.mjs'
18
-
19
- /**
20
- * `graph scan [--json]` command handler.
21
- * @param {string[]} args аргументи
22
- * @param {{
23
- * cwd?: string,
24
- * log?: (m: string) => void,
25
- * readFile?: (p: string, enc: string) => string,
26
- * readdir?: (d: string) => string[],
27
- * exists?: (p: string) => boolean,
28
- * execSync?: (cmd: string, opts?: object) => string
29
- * }} [deps] ін'єкції
30
- * @returns {Promise<number>} exit code (0=clean, 1=attention)
31
- */
32
- export async function cmdScan(args, deps = {}) {
33
- const root = deps.cwd ?? processCwd()
34
- const log = deps.log ?? console.log
35
- const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
36
- const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
37
- const exists = deps.exists ?? existsSync
38
- const execSyncFn = deps.execSync ?? ((cmd, opts) => execSync(cmd, { ...opts, encoding: 'utf8' }))
39
-
40
- const jsonMode = args.includes('--json')
41
-
42
- const config = loadConfig({ root, readFile, exists })
43
- const tasksDir = resolveTasksDir(config, root)
44
-
45
- const activeWorktrees = listActiveWorktrees(root, { execSync: execSyncFn })
46
-
47
- const allNodes = scanNodes(tasksDir, activeWorktrees, {
48
- readdirSync: readdir,
49
- existsSync: exists,
50
- readFileSync: readFile
51
- })
52
-
53
- const sorted = topoSort(allNodes)
54
-
55
- // Підрахунок по станах
56
- const stateCounts = {}
57
- for (const n of sorted) {
58
- stateCounts[n.state] = (stateCounts[n.state] ?? 0) + 1
59
- }
60
-
61
- // Знаходимо проблемні вузли
62
- const failed = sorted.filter(n => n.state === 'failed')
63
- const pendingAudit = sorted.filter(n => n.state === 'pending-audit')
64
- const needsPlan = sorted.filter(n => n.state === 'needs-plan')
65
-
66
- // Знаходимо готові до запуску (waiting + deps resolved)
67
- const nodeMap = new Map(sorted.map(n => [n.id, n]))
68
- const ready = sorted.filter(n => n.state === 'waiting' && areDepsResolved(n, nodeMap))
69
-
70
- const hasProblems = failed.length > 0
71
-
72
- if (jsonMode) {
73
- console.log(JSON.stringify({
74
- ok: !hasProblems,
75
- total: sorted.length,
76
- counts: stateCounts,
77
- failed: failed.map(n => n.path),
78
- pending_audit: pendingAudit.map(n => n.path),
79
- needs_plan: needsPlan.map(n => n.path),
80
- ready: ready.map(n => n.path)
81
- }, null, 2))
82
- } else {
83
- const summaryParts = Object.entries(stateCounts)
84
- .map(([s, c]) => `${s}:${c}`)
85
- .join(' ')
86
-
87
- log(`scan: ${sorted.length} вузлів — ${summaryParts}`)
88
-
89
- if (failed.length > 0) {
90
- log(`\nFAILED (${failed.length}):`)
91
- for (const n of failed) log(` - ${n.path}`)
92
- }
93
-
94
- if (pendingAudit.length > 0) {
95
- log(`\npending-audit (${pendingAudit.length}):`)
96
- for (const n of pendingAudit) log(` - ${n.path}`)
97
- }
98
-
99
- if (needsPlan.length > 0) {
100
- log(`\nneeds-plan (${needsPlan.length}):`)
101
- for (const n of needsPlan) log(` - ${n.path}`)
102
- }
103
-
104
- if (ready.length > 0) {
105
- log(`\nready to run (${ready.length}):`)
106
- for (const n of ready) log(` - ${n.path}`)
107
- }
108
-
109
- if (!hasProblems && failed.length === 0) {
110
- log('\nscan: OK')
111
- }
112
- }
113
-
114
- return hasProblems ? 1 : 0
115
- }
@@ -1,111 +0,0 @@
1
- /**
2
- * `n-cursor graph setup` — ініціалізація проєкту для graph task system.
3
- *
4
- * Створює:
5
- * - .n-cursor.json з дефолтними налаштуваннями (якщо не існує)
6
- * - tasks/ директорію
7
- * - git hook (post-commit) для автоматичного оновлення стану (якщо є .git)
8
- *
9
- * FS ін'єктується для тестованості.
10
- */
11
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
12
- import { join } from 'node:path'
13
- import { cwd as processCwd } from 'node:process'
14
-
15
- import { CONFIG_DEFAULTS } from './config.mjs'
16
-
17
- /**
18
- * `graph setup` command handler.
19
- * @param {string[]} _args аргументи (не використовуються)
20
- * @param {{
21
- * cwd?: string,
22
- * log?: (m: string) => void,
23
- * writeFile?: (p: string, c: string, enc: string) => void,
24
- * readFile?: (p: string, enc: string) => string,
25
- * exists?: (p: string) => boolean,
26
- * mkdir?: (p: string, opts?: object) => void
27
- * }} [deps] ін'єкції
28
- * @returns {Promise<number>} exit code
29
- */
30
- export async function cmdSetup(_args, deps = {}) {
31
- const root = deps.cwd ?? processCwd()
32
- const log = deps.log ?? console.log
33
- const writeFile = deps.writeFile ?? ((p, c, enc) => writeFileSync(p, c, enc))
34
- const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
35
- const exists = deps.exists ?? existsSync
36
- const mkdir = deps.mkdir ?? ((p, opts) => mkdirSync(p, opts))
37
-
38
- // 1. Створюємо .n-cursor.json якщо не існує
39
- const configPath = join(root, '.n-cursor.json')
40
- if (!exists(configPath)) {
41
- try {
42
- writeFile(configPath, JSON.stringify(CONFIG_DEFAULTS, null, 2) + '\n', 'utf8')
43
- log(`setup: створено ${configPath}`)
44
- } catch (err) {
45
- log(`setup: не вдалося створити ${configPath} — ${err.message ?? String(err)}`)
46
- return 1
47
- }
48
- } else {
49
- log(`setup: ${configPath} вже існує — пропускаємо`)
50
- }
51
-
52
- // 2. Створюємо tasks/ директорію
53
- const tasksDir = join(root, 'tasks')
54
- if (!exists(tasksDir)) {
55
- try {
56
- mkdir(tasksDir, { recursive: true })
57
- log(`setup: створено ${tasksDir}`)
58
- } catch (err) {
59
- log(`setup: не вдалося створити ${tasksDir} — ${err.message ?? String(err)}`)
60
- return 1
61
- }
62
- } else {
63
- log(`setup: ${tasksDir} вже існує — пропускаємо`)
64
- }
65
-
66
- // 3. Створюємо .n-cursor/ директорію
67
- const ncursorDir = join(root, '.n-cursor')
68
- if (!exists(ncursorDir)) {
69
- try {
70
- mkdir(ncursorDir, { recursive: true })
71
- log(`setup: створено ${ncursorDir}`)
72
- } catch (err) {
73
- log(`setup: не вдалося створити ${ncursorDir} — ${err.message ?? String(err)}`)
74
- return 1
75
- }
76
- }
77
-
78
- // 4. Перевіряємо чи є .git і додаємо hook
79
- const gitDir = join(root, '.git')
80
- if (exists(gitDir)) {
81
- const hooksDir = join(gitDir, 'hooks')
82
- try {
83
- mkdir(hooksDir, { recursive: true })
84
- } catch {
85
- // hooks/ може вже існувати
86
- }
87
-
88
- const hookPath = join(hooksDir, 'post-commit')
89
- if (!exists(hookPath)) {
90
- const hookContent = [
91
- '#!/bin/sh',
92
- '# n-cursor graph: automatic state refresh after commit',
93
- 'npx @nitra/cursor graph scan --json > /dev/null 2>&1 || true',
94
- ''
95
- ].join('\n')
96
- try {
97
- writeFile(hookPath, hookContent, 'utf8')
98
- // chmod +x через окрему команду не робимо — залежить від FS dep
99
- log(`setup: створено git hook ${hookPath}`)
100
- } catch (err) {
101
- log(`setup: не вдалося створити git hook — ${err.message ?? String(err)}`)
102
- // Не критично — продовжуємо
103
- }
104
- } else {
105
- log(`setup: git hook ${hookPath} вже існує — пропускаємо`)
106
- }
107
- }
108
-
109
- log('setup: готово')
110
- return 0
111
- }