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