@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,235 +0,0 @@
1
- /**
2
- * DAG-сканер вузлів задач.
3
- *
4
- * Рекурсивно обходить tasks_dir, знаходить всі вузли (директорії з task.md),
5
- * читає їх залежності з task.md front-matter, деривує стани та виконує
6
- * топологічне сортування (Kahn's algorithm).
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
-
14
- import { parseFrontMatter } from './frontmatter.mjs'
15
- import { deriveNodeState, isComposite } from './node-state.mjs'
16
-
17
- /**
18
- * @typedef {{
19
- * id: string,
20
- * path: string,
21
- * dir: string,
22
- * deps: string[],
23
- * state: string,
24
- * composite: boolean,
25
- * children: string[]
26
- * }} NodeInfo
27
- */
28
-
29
- /**
30
- * Рекурсивно знаходить всі вузли DAG у tasks_dir.
31
- * Вузол = директорія що містить task.md.
32
- * @param {string} tasksDir абсолютний шлях до tasks/
33
- * @param {{
34
- * readdirSync?: (d: string) => string[],
35
- * existsSync?: (p: string) => boolean,
36
- * readFileSync?: (p: string, enc: string) => string
37
- * }} [deps] ін'єкції
38
- * @returns {{ dir: string, relPath: string }[]} список знайдених вузлів
39
- */
40
- export function findNodes(tasksDir, deps = {}) {
41
- const readdir = deps.readdirSync ?? readdirSync
42
- const exists = deps.existsSync ?? existsSync
43
-
44
- const nodes = []
45
-
46
- function scan(dir, prefix = '') {
47
- let entries
48
- try {
49
- entries = readdir(dir)
50
- } catch {
51
- return
52
- }
53
-
54
- const hasTaskMd = entries.includes('task.md')
55
- if (hasTaskMd) {
56
- nodes.push({
57
- dir,
58
- relPath: prefix ? prefix : dir.split('/').pop() ?? dir
59
- })
60
- }
61
-
62
- // Рекурсивно шукаємо дочірні директорії
63
- for (const name of entries) {
64
- // Пропускаємо зарезервовані та приховані директорії/файли
65
- if (name.startsWith('.') || name.includes('.')) continue
66
- const childDir = join(dir, name)
67
- // Перевіряємо що це директорія (якщо має subdirs або task.md)
68
- const childRelPath = prefix ? `${prefix}/${name}` : name
69
- try {
70
- // Перевірка що childDir — дійсно директорія
71
- readdir(childDir)
72
- scan(childDir, childRelPath)
73
- } catch {
74
- // не директорія або не читається
75
- }
76
- }
77
- }
78
-
79
- scan(tasksDir)
80
- return nodes
81
- }
82
-
83
- /**
84
- * Сканує DAG і повертає всі вузли з деривованими станами.
85
- * @param {string} tasksDir абсолютний шлях до tasks/
86
- * @param {Set<string>} activeWorktrees активні worktree imена
87
- * @param {{
88
- * readdirSync?: (d: string) => string[],
89
- * existsSync?: (p: string) => boolean,
90
- * readFileSync?: (p: string, enc: string) => string
91
- * }} [deps] ін'єкції
92
- * @returns {NodeInfo[]} список вузлів
93
- */
94
- export function scanNodes(tasksDir, activeWorktrees, deps = {}) {
95
- const readdir = deps.readdirSync ?? readdirSync
96
- const exists = deps.existsSync ?? existsSync
97
- const readFile = deps.readFileSync ?? ((p, enc) => readFileSync(p, enc))
98
-
99
- const found = findNodes(tasksDir, { readdirSync: readdir, existsSync: exists, readFileSync: readFile })
100
-
101
- return found.map(({ dir, relPath }) => {
102
- let fm = {}
103
- try {
104
- const taskContent = readFile(join(dir, 'task.md'), 'utf8')
105
- fm = parseFrontMatter(taskContent)
106
- } catch {
107
- // порожній front-matter
108
- }
109
-
110
- const deps_ = Array.isArray(fm.deps) ? fm.deps.map(String) : []
111
- const state = deriveNodeState(dir, activeWorktrees, { readdirSync: readdir, readFileSync: readFile, existsSync: exists })
112
- const composite = isComposite(dir, { readdirSync: readdir, existsSync: exists })
113
-
114
- // Дочірні вузли
115
- let children = []
116
- if (composite) {
117
- let entries
118
- try {
119
- entries = readdir(dir)
120
- } catch {
121
- entries = []
122
- }
123
- children = entries
124
- .filter(name => !name.startsWith('.') && !name.endsWith('.md') && !name.endsWith('.json'))
125
- .filter(name => {
126
- try {
127
- return exists(join(dir, name, 'task.md'))
128
- } catch {
129
- return false
130
- }
131
- })
132
- .map(name => `${relPath}/${name}`)
133
- }
134
-
135
- return {
136
- id: relPath,
137
- path: relPath,
138
- dir,
139
- deps: deps_,
140
- state,
141
- composite,
142
- children
143
- }
144
- })
145
- }
146
-
147
- /**
148
- * Топологічне сортування вузлів (алгоритм Кана).
149
- * Вузли без залежностей — першими. Циклічні залежності — не гарантовано.
150
- * @param {NodeInfo[]} nodes вузли зі списком deps
151
- * @returns {NodeInfo[]} відсортований список (або той самий порядок якщо циклічні)
152
- */
153
- export function topoSort(nodes) {
154
- const idToNode = new Map(nodes.map(n => [n.id, n]))
155
- const inDegree = new Map(nodes.map(n => [n.id, 0]))
156
- const adj = new Map(nodes.map(n => [n.id, []]))
157
-
158
- for (const node of nodes) {
159
- for (const dep of node.deps) {
160
- if (idToNode.has(dep)) {
161
- adj.get(dep).push(node.id)
162
- inDegree.set(node.id, (inDegree.get(node.id) ?? 0) + 1)
163
- }
164
- }
165
- }
166
-
167
- const queue = nodes.filter(n => (inDegree.get(n.id) ?? 0) === 0).map(n => n.id)
168
- const sorted = []
169
-
170
- while (queue.length > 0) {
171
- const id = queue.shift()
172
- const node = idToNode.get(id)
173
- if (node) sorted.push(node)
174
- for (const next of (adj.get(id) ?? [])) {
175
- const deg = (inDegree.get(next) ?? 0) - 1
176
- inDegree.set(next, deg)
177
- if (deg === 0) queue.push(next)
178
- }
179
- }
180
-
181
- // Якщо є цикли — додаємо решту у кінець
182
- if (sorted.length < nodes.length) {
183
- for (const n of nodes) {
184
- if (!sorted.includes(n)) sorted.push(n)
185
- }
186
- }
187
-
188
- return sorted
189
- }
190
-
191
- /**
192
- * Перевіряє чи всі залежності вузла resolved.
193
- * @param {NodeInfo} node вузол
194
- * @param {Map<string, NodeInfo>} nodeMap map id -> NodeInfo
195
- * @returns {boolean} true якщо всі deps resolved
196
- */
197
- export function areDepsResolved(node, nodeMap) {
198
- return node.deps.every(dep => {
199
- const depNode = nodeMap.get(dep)
200
- return depNode?.state === 'resolved'
201
- })
202
- }
203
-
204
- /**
205
- * Знаходить активні worktrees з git worktree list.
206
- * @param {string} root корінь репо
207
- * @param {{ execSync?: (cmd: string, opts?: object) => string }} [deps] ін'єкції
208
- * @returns {Set<string>} set імен worktree
209
- */
210
- export function getActiveWorktrees(root, deps = {}) {
211
- const execSyncFn = deps.execSync ?? ((cmd, opts) => execSync(cmd, opts))
212
- try {
213
- const out = execSyncFn('git worktree list --porcelain', { cwd: root, encoding: 'utf8' })
214
- return parseWorktreeList(String(out))
215
- } catch {
216
- return new Set()
217
- }
218
- }
219
-
220
- /**
221
- * Парсить вивід `git worktree list --porcelain` і повертає набір імен worktree.
222
- * @param {string} output вивід команди
223
- * @returns {Set<string>} set імен (останній компонент шляху)
224
- */
225
- export function parseWorktreeList(output) {
226
- const names = new Set()
227
- for (const line of output.split('\n')) {
228
- if (line.startsWith('worktree ')) {
229
- const path = line.slice('worktree '.length).trim()
230
- const name = path.split('/').pop() ?? ''
231
- if (name) names.add(name)
232
- }
233
- }
234
- return names
235
- }
@@ -1,193 +0,0 @@
1
- /**
2
- * Git worktree management для graph task system.
3
- *
4
- * Atomic mkdir lock: EEXIST → skip (вже запущено).
5
- * Worktree name: sanitize(node-path) + '-' + epoch (секунди).
6
- *
7
- * Всі git операції через execSync (node:child_process). FS через ін'єкцію.
8
- */
9
- import { execSync } from 'node:child_process'
10
- import { mkdirSync, readdirSync, rmSync } from 'node:fs'
11
- import { join } from 'node:path'
12
-
13
- import { sanitizeNodeName } from './node-state.mjs'
14
-
15
- /**
16
- * Генерує ім'я worktree для вузла.
17
- * @param {string} nodePath відносний шлях вузла (напр. "research/collect-data")
18
- * @param {number} [epochSec] epoch в секундах (default: Date.now()/1000)
19
- * @returns {string} ім'я worktree
20
- */
21
- export function makeWorktreeName(nodePath, epochSec) {
22
- const epoch = epochSec ?? Math.floor(Date.now() / 1000)
23
- const sanitized = sanitizeNodeName(nodePath.replace(/\//g, '-'))
24
- return `${sanitized}-${epoch}`
25
- }
26
-
27
- /**
28
- * Створює git worktree для вузла з atomic mkdir lock.
29
- * Повертає null якщо worktree вже існує (EEXIST → вже запущено).
30
- *
31
- * @param {string} worktreesDir абсолютний шлях до .worktrees/
32
- * @param {string} worktreeName ім'я нового worktree
33
- * @param {string} root корінь репо
34
- * @param {{
35
- * execSync?: (cmd: string, opts?: object) => string,
36
- * mkdirSync?: (p: string, opts?: object) => void
37
- * }} [deps] ін'єкції
38
- * @returns {{ worktreePath: string } | null} шлях worktree або null якщо вже існує
39
- */
40
- export function createWorktree(worktreesDir, worktreeName, root, deps = {}) {
41
- const execSyncFn = deps.execSync ?? ((cmd, opts) => execSync(cmd, opts))
42
- const mkdirSyncFn = deps.mkdirSync ?? mkdirSync
43
-
44
- const worktreePath = join(worktreesDir, worktreeName)
45
-
46
- // Atomic mkdir lock: якщо директорія вже є — хтось вже запустив цей вузол
47
- try {
48
- mkdirSyncFn(worktreePath, { recursive: false })
49
- } catch (err) {
50
- if (err.code === 'EEXIST') return null
51
- throw err
52
- }
53
-
54
- try {
55
- // Видаляємо порожню директорію — git worktree add створить її сам
56
- rmSync(worktreePath, { recursive: true, force: true })
57
- execSyncFn(`git worktree add "${worktreePath}" HEAD`, { cwd: root, encoding: 'utf8' })
58
- } catch (err) {
59
- // Якщо git worktree add не вдався — прибираємо директорію
60
- try {
61
- rmSync(worktreePath, { recursive: true, force: true })
62
- } catch {
63
- // пропускаємо
64
- }
65
- throw err
66
- }
67
-
68
- return { worktreePath }
69
- }
70
-
71
- /**
72
- * Видаляє git worktree.
73
- * @param {string} worktreePath абсолютний шлях до worktree
74
- * @param {string} root корінь репо
75
- * @param {{
76
- * execSync?: (cmd: string, opts?: object) => string
77
- * }} [deps] ін'єкції
78
- */
79
- export function removeWorktree(worktreePath, root, deps = {}) {
80
- const execSyncFn = deps.execSync ?? ((cmd, opts) => execSync(cmd, opts))
81
- try {
82
- execSyncFn(`git worktree remove --force "${worktreePath}"`, { cwd: root, encoding: 'utf8' })
83
- } catch {
84
- // Якщо не вдалось через git — видаляємо вручну
85
- try {
86
- rmSync(worktreePath, { recursive: true, force: true })
87
- execSyncFn('git worktree prune', { cwd: root, encoding: 'utf8' })
88
- } catch {
89
- // пропускаємо — можливо вже видалено
90
- }
91
- }
92
- }
93
-
94
- /**
95
- * Мерджить зміни з worktree у main-гілку і видаляє worktree.
96
- * @param {string} worktreePath абсолютний шлях до worktree
97
- * @param {string} root корінь репо
98
- * @param {{
99
- * execSync?: (cmd: string, opts?: object) => string
100
- * }} [deps] ін'єкції
101
- * @returns {{ ok: boolean, error?: string }} результат
102
- */
103
- export function mergeWorktree(worktreePath, root, deps = {}) {
104
- const execSyncFn = deps.execSync ?? ((cmd, opts) => execSync(cmd, opts))
105
-
106
- try {
107
- // Отримуємо ім'я гілки worktree
108
- const branch = execSyncFn('git rev-parse --abbrev-ref HEAD', {
109
- cwd: worktreePath,
110
- encoding: 'utf8'
111
- }).trim()
112
-
113
- // Додаємо всі зміни і комітимо
114
- execSyncFn('git add -A', { cwd: worktreePath, encoding: 'utf8' })
115
-
116
- let hasChanges = false
117
- try {
118
- execSyncFn('git diff --cached --quiet', { cwd: worktreePath, encoding: 'utf8' })
119
- } catch {
120
- hasChanges = true
121
- }
122
-
123
- if (hasChanges) {
124
- execSyncFn('git commit -m "graph: node task completion"', { cwd: worktreePath, encoding: 'utf8' })
125
- }
126
-
127
- // Якщо worktree на окремій гілці — мерджимо в main
128
- if (branch && branch !== 'HEAD' && branch !== 'main' && branch !== 'master') {
129
- execSyncFn(`git merge --no-ff "${branch}" -m "graph: merge node ${branch}"`, {
130
- cwd: root,
131
- encoding: 'utf8'
132
- })
133
- }
134
- } catch (err) {
135
- return { ok: false, error: err.message ?? String(err) }
136
- }
137
-
138
- // Видаляємо worktree
139
- removeWorktree(worktreePath, root, { execSync: execSyncFn })
140
- return { ok: true }
141
- }
142
-
143
- /**
144
- * Повертає список активних worktrees з репо.
145
- * @param {string} root корінь репо
146
- * @param {{
147
- * execSync?: (cmd: string, opts?: object) => string
148
- * }} [deps] ін'єкції
149
- * @returns {Set<string>} set імен worktrees
150
- */
151
- export function listActiveWorktrees(root, deps = {}) {
152
- const execSyncFn = deps.execSync ?? ((cmd, opts) => execSync(cmd, opts))
153
-
154
- try {
155
- const out = execSyncFn('git worktree list --porcelain', { cwd: root, encoding: 'utf8' })
156
- const names = new Set()
157
- for (const line of String(out).split('\n')) {
158
- if (line.startsWith('worktree ')) {
159
- const path = line.slice('worktree '.length).trim()
160
- const name = path.split('/').pop() ?? ''
161
- if (name) names.add(name)
162
- }
163
- }
164
- return names
165
- } catch {
166
- return new Set()
167
- }
168
- }
169
-
170
- /**
171
- * Знаходить worktree що належить вузлу (за prefix).
172
- * @param {string} nodePath відносний шлях вузла
173
- * @param {string} worktreesDir абсолютний шлях до .worktrees/
174
- * @param {{
175
- * readdirSync?: (d: string) => string[],
176
- * execSync?: (cmd: string, opts?: object) => string
177
- * }} [deps] ін'єкції
178
- * @returns {string | null} абсолютний шлях до worktree або null
179
- */
180
- export function findNodeWorktree(nodePath, worktreesDir, deps = {}) {
181
- const readdirSyncFn = deps.readdirSync ?? readdirSync
182
- const prefix = sanitizeNodeName(nodePath.replace(/\//g, '-'))
183
-
184
- let entries
185
- try {
186
- entries = readdirSyncFn(worktreesDir)
187
- } catch {
188
- return null
189
- }
190
-
191
- const match = entries.find(name => name.startsWith(prefix + '-') || name === prefix)
192
- return match ? join(worktreesDir, match) : null
193
- }
@@ -1,92 +0,0 @@
1
- /**
2
- * `n-cursor graph` — task DAG orchestration system.
3
- *
4
- * Управляє задачами у `tasks/<node>/` директоріях. Стан вузлів деривується
5
- * з файлів (immutable protocol): task.md + plan/run/fact/pending-audit/audit-result/invalidated.
6
- *
7
- * Підкоманди:
8
- * setup — ініціалізація проєкту (.n-cursor.json, tasks/, git hook)
9
- * init <name> — створити task.md шаблон
10
- * plan [<path>] [--mode agent] — Stage 1: написати plan_NNN.md
11
- * status [<path>] [--json] — показати стан DAG
12
- * scan [--json] — повний скан, exit 1 якщо failed
13
- * run [<path>] [--actor a] [--auto] — запустити вузол(и)
14
- * kill <path> — вбити worktree + каскадна інвалідація + видалити plan_*.md
15
- * invalidate <path> [--no-cascade] — позначити як invalidated
16
- * done <path> — успіх → merge worktree
17
- * audit <path> — pending-audit_NNN.md + merge agent worktree
18
- * failed <path> — провал → run_NNN.md (failed)
19
- * spawn <path> — composite → перевірити дочірні вузли
20
- * watch — одноразовий скан: audit queue + stale + needs-plan
21
- */
22
-
23
- import { cmdSetup } from './graph/lib/cmd-setup.mjs'
24
- import { cmdInit } from './graph/lib/cmd-init.mjs'
25
- import { cmdPlan } from './graph/lib/cmd-plan.mjs'
26
- import { cmdStatus } from './graph/lib/cmd-status.mjs'
27
- import { cmdScan } from './graph/lib/cmd-scan.mjs'
28
- import { cmdRun } from './graph/lib/cmd-run.mjs'
29
- import { cmdKill } from './graph/lib/cmd-kill.mjs'
30
- import { cmdInvalidate } from './graph/lib/cmd-invalidate.mjs'
31
- import { cmdDone, cmdAudit, cmdFailed, cmdSpawn } from './graph/lib/cmd-signals.mjs'
32
- import { cmdWatch } from './graph/lib/cmd-watch.mjs'
33
-
34
- const USAGE = [
35
- 'Usage: n-cursor graph <command> [args]',
36
- '',
37
- 'Commands:',
38
- ' setup init project: .n-cursor.json, tasks/, git hook',
39
- ' init <name> create task.md template',
40
- ' plan [<path>] [--mode agent] Stage 1: write plan_NNN.md',
41
- ' status [<path>] [--json] show DAG state',
42
- ' scan [--json] full scan, exit 1 if failed',
43
- ' run [<path>] [--actor a] [--auto] run node(s)',
44
- ' kill <path> kill worktree + cascade invalidate + delete plan_*.md',
45
- ' invalidate <path> [--no-cascade] mark invalidated',
46
- ' done <path> success → merge worktree',
47
- ' audit <path> pending-audit_NNN.md + merge agent worktree',
48
- ' failed <path> failure → write run_NNN.md',
49
- ' spawn <path> composite → validate children registered',
50
- ' watch one-shot scan: audit queue + stale + needs-plan'
51
- ].join('\n')
52
-
53
- /** @type {Record<string, (args: string[], deps: object) => Promise<number>>} */
54
- const COMMANDS = {
55
- setup: cmdSetup,
56
- init: cmdInit,
57
- plan: cmdPlan,
58
- status: cmdStatus,
59
- scan: cmdScan,
60
- run: cmdRun,
61
- kill: cmdKill,
62
- invalidate: cmdInvalidate,
63
- done: cmdDone,
64
- audit: cmdAudit,
65
- failed: cmdFailed,
66
- spawn: cmdSpawn,
67
- watch: cmdWatch
68
- }
69
-
70
- /**
71
- * Точка входу `n-cursor graph` та `n-cursor watch`.
72
- * Парсить підкоманду і маршрутизує до відповідного handler-а.
73
- *
74
- * @param {string[]} args аргументи після `graph` (або `watch`)
75
- * @param {{
76
- * cwd?: string,
77
- * log?: (m: string) => void,
78
- * [key: string]: unknown
79
- * }} [deps] ін'єкції (пробрасуються до підкоманд)
80
- * @returns {Promise<number>} exit code
81
- */
82
- export async function runGraphTasksCli(args, deps = {}) {
83
- const [sub, ...rest] = args
84
-
85
- if (!sub || !Object.hasOwn(COMMANDS, sub)) {
86
- console.error(USAGE)
87
- if (sub) console.error(`\nНевідома підкоманда: "${sub}"`)
88
- return 1
89
- }
90
-
91
- return await COMMANDS[sub](rest, deps)
92
- }