@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,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
- }