@nitra/cursor 4.1.0 → 4.1.2

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 (135) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/bin/n-cursor.js +25 -13
  3. package/docs/flow.MD +1364 -0
  4. package/docs/stryker.config.md +37 -0
  5. package/docs/vitest.config.md +23 -0
  6. package/lib/models.mjs +1 -2
  7. package/package.json +2 -1
  8. package/rules/abie/fix.mjs +1 -1
  9. package/rules/bun/docs/fix.md +3 -0
  10. package/rules/bun/fix.mjs +1 -1
  11. package/rules/capacitor/fix.mjs +1 -1
  12. package/rules/changelog/docs/fix.md +3 -0
  13. package/rules/changelog/fix.mjs +1 -1
  14. package/rules/ci4/fix.mjs +1 -1
  15. package/rules/ci4/js/docs/marksman_config.md +1 -0
  16. package/rules/docker/docs/fix.md +1 -1
  17. package/rules/docker/fix.mjs +1 -1
  18. package/rules/docker/lint/docs/lint.md +1 -0
  19. package/rules/efes/docs/fix.md +2 -1
  20. package/rules/efes/fix.mjs +1 -1
  21. package/rules/feedback/fix.mjs +1 -1
  22. package/rules/ga/fix.mjs +1 -1
  23. package/rules/ga/js/lint.mjs +1 -1
  24. package/rules/graphql/docs/fix.md +4 -1
  25. package/rules/graphql/fix.mjs +1 -1
  26. package/rules/graphql/lib/docs/graphql-gql-scan.md +3 -0
  27. package/rules/hasura/fix.mjs +1 -1
  28. package/rules/image-avif/docs/fix.md +4 -1
  29. package/rules/image-avif/fix.mjs +1 -1
  30. package/rules/image-avif/js/docs/avif_generation.md +1 -0
  31. package/rules/image-compress/fix.mjs +1 -1
  32. package/rules/js-bun-db/fix.mjs +1 -1
  33. package/rules/js-bun-db/lib/docs/bun-sql-scan.md +6 -0
  34. package/rules/js-bun-redis/fix.mjs +1 -1
  35. package/rules/js-lint/fix.mjs +1 -1
  36. package/rules/js-lint/js/docs/utils_imports.md +1 -0
  37. package/rules/js-lint-ci/docs/fix.md +4 -1
  38. package/rules/js-lint-ci/fix.mjs +1 -1
  39. package/rules/js-mssql/docs/fix.md +3 -0
  40. package/rules/js-mssql/fix.mjs +1 -1
  41. package/rules/js-mssql/lib/docs/mssql-pool-scan.md +9 -0
  42. package/rules/js-run/docs/fix.md +3 -0
  43. package/rules/js-run/fix.mjs +1 -1
  44. package/rules/js-run/lib/docs/check-env-scan.md +2 -1
  45. package/rules/js-run/lib/docs/promise-settimeout-scan.md +4 -0
  46. package/rules/k8s/docs/fix.md +3 -0
  47. package/rules/k8s/fix.mjs +1 -1
  48. package/rules/nginx-default-tpl/docs/fix.md +3 -0
  49. package/rules/nginx-default-tpl/fix.mjs +1 -1
  50. package/rules/npm-module/fix.mjs +1 -1
  51. package/rules/npm-module/js/header_doc_pointer.mjs +14 -3
  52. package/rules/php/docs/fix.md +2 -1
  53. package/rules/php/fix.mjs +1 -1
  54. package/rules/python/docs/fix.md +4 -1
  55. package/rules/python/fix.mjs +1 -1
  56. package/rules/rego/fix.mjs +1 -1
  57. package/rules/rego/js/lint.mjs +1 -1
  58. package/rules/release/docs/fix.md +4 -1
  59. package/rules/release/fix.mjs +1 -1
  60. package/rules/rust/fix.mjs +1 -1
  61. package/rules/security/docs/fix.md +1 -1
  62. package/rules/security/fix.mjs +1 -1
  63. package/rules/style-lint/docs/fix.md +3 -0
  64. package/rules/style-lint/fix.mjs +1 -1
  65. package/rules/tauri/docs/fix.md +4 -1
  66. package/rules/tauri/fix.mjs +1 -1
  67. package/rules/test/docs/fix.md +3 -0
  68. package/rules/test/fix.mjs +1 -1
  69. package/rules/test/js/no-relative-fs-path.mjs +2 -1
  70. package/rules/text/docs/fix.md +3 -0
  71. package/rules/text/fix.mjs +1 -1
  72. package/rules/text/js/lint.mjs +1 -1
  73. package/rules/vue/fix.mjs +1 -1
  74. package/rules/worktree/fix.mjs +1 -1
  75. package/scripts/auto-rules.mjs +1 -1
  76. package/scripts/coverage-classify/index.mjs +10 -10
  77. package/scripts/coverage-fix.mjs +2 -2
  78. package/scripts/dispatcher/graph/lib/cmd-init.mjs +112 -0
  79. package/scripts/dispatcher/graph/lib/cmd-invalidate.mjs +96 -0
  80. package/scripts/dispatcher/graph/lib/cmd-kill.mjs +141 -0
  81. package/scripts/dispatcher/graph/lib/cmd-plan.mjs +142 -0
  82. package/scripts/dispatcher/graph/lib/cmd-run.mjs +328 -0
  83. package/scripts/dispatcher/graph/lib/cmd-scan.mjs +115 -0
  84. package/scripts/dispatcher/graph/lib/cmd-setup.mjs +111 -0
  85. package/scripts/dispatcher/graph/lib/cmd-signals.mjs +328 -0
  86. package/scripts/dispatcher/graph/lib/cmd-status.mjs +131 -0
  87. package/scripts/dispatcher/graph/lib/cmd-verify.mjs +100 -0
  88. package/scripts/dispatcher/graph/lib/cmd-watch.mjs +128 -0
  89. package/scripts/dispatcher/graph/lib/config.mjs +103 -0
  90. package/scripts/dispatcher/graph/lib/frontmatter.mjs +224 -0
  91. package/scripts/dispatcher/graph/lib/nnn.mjs +127 -0
  92. package/scripts/dispatcher/graph/lib/node-state.mjs +157 -0
  93. package/scripts/dispatcher/graph/lib/scanner.mjs +235 -0
  94. package/scripts/dispatcher/graph/lib/worktree-ops.mjs +193 -0
  95. package/scripts/dispatcher/graph-tasks.mjs +92 -0
  96. package/scripts/dispatcher/index.mjs +3 -3
  97. package/scripts/dispatcher/lib/docs/events.md +1 -0
  98. package/scripts/dispatcher/lib/executor.mjs +1 -1
  99. package/scripts/dispatcher/lib/subagent-runner.mjs +9 -9
  100. package/scripts/dispatcher/trace.mjs +6 -2
  101. package/scripts/docs/build-agents-commands.md +1 -0
  102. package/scripts/docs/cli-entry.md +6 -0
  103. package/scripts/graph/index.mjs +115 -0
  104. package/scripts/graph/lib/config.mjs +62 -0
  105. package/scripts/graph/lib/dag.mjs +161 -0
  106. package/scripts/graph/lib/frontmatter.mjs +70 -0
  107. package/scripts/graph/lib/nnn.mjs +77 -0
  108. package/scripts/graph/lib/state.mjs +110 -0
  109. package/scripts/graph/scan.mjs +64 -0
  110. package/scripts/graph/status.mjs +86 -0
  111. package/scripts/lib/docs/load-cursor-config.md +3 -0
  112. package/scripts/lib/root-notice.mjs +4 -2
  113. package/scripts/lib/rule-predicates.mjs +1 -1
  114. package/scripts/lib/worktree-notice.mjs +14 -7
  115. package/scripts/lib/worktree.mjs +3 -2
  116. package/scripts/utils/resolve-js-root.mjs +2 -1
  117. package/scripts/utils/with-lock.mjs +1 -1
  118. package/skills/docgen/js/docgen-batch.mjs +7 -7
  119. package/skills/docgen/js/docgen-extract.mjs +80 -37
  120. package/skills/docgen/js/docgen-ignore.mjs +1 -1
  121. package/skills/docgen/js/docgen-prompts.mjs +21 -5
  122. package/skills/fix/js/llm-worker.mjs +19 -22
  123. package/skills/fix/js/orchestrator.mjs +6 -7
  124. package/skills/fix/js/t0.mjs +14 -13
  125. package/types/bin/n-cursor.d.ts +1 -1
  126. package/rules/flow/docs/fix.md +0 -152
  127. package/rules/flow/fix.mjs +0 -18
  128. package/rules/flow/flow.mdc +0 -127
  129. package/rules/flow/meta.json +0 -1
  130. package/scripts/dispatcher/lib/docs/flow-lock.md +0 -161
  131. package/scripts/dispatcher/lib/docs/flow-resolve.md +0 -267
  132. package/scripts/dispatcher/lib/flow-plan.mjs +0 -153
  133. package/scripts/dispatcher/lib/flow-resolve.mjs +0 -156
  134. package/scripts/dispatcher/lib/flow-signals.mjs +0 -235
  135. package/scripts/dispatcher/lib/flow-verify.mjs +0 -127
@@ -0,0 +1,115 @@
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
+ }
@@ -0,0 +1,111 @@
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
+ }
@@ -0,0 +1,328 @@
1
+ /**
2
+ * Сигнальні команди: `done`, `audit`, `failed`, `spawn`.
3
+ *
4
+ * Ці команди викликаються зсередини worktree (агентом або скриптом),
5
+ * або зовні через `n-cursor graph done|audit|failed|spawn <path>`.
6
+ *
7
+ * done → записує run_NNN.md (result:success), мерджить worktree
8
+ * audit → знаходить latest fact_NNN.md, створює pending-audit_NNN.md,
9
+ * записує run_NNN.md, мерджить worktree
10
+ * failed → записує run_NNN.md (result:failed), залишає worktree
11
+ * spawn → перевіряє що дочірні вузли зареєстровані (мають task.md)
12
+ *
13
+ * FS і child_process ін'єктуються для тестованості.
14
+ */
15
+ import { execSync } from 'node:child_process'
16
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
17
+ import { join } from 'node:path'
18
+ import { cwd as processCwd } from 'node:process'
19
+
20
+ import { buildMarkdown } from './frontmatter.mjs'
21
+ import { latestFactNNN, nextRunNNN } from './nnn.mjs'
22
+ import { loadConfig, resolveTasksDir, resolveWorktreesDir } from './config.mjs'
23
+ import { findNodeWorktree, listActiveWorktrees, mergeWorktree } from './worktree-ops.mjs'
24
+
25
+ /**
26
+ * Пише run_NNN.md артефакт.
27
+ * @param {string} nodeDir директорія вузла
28
+ * @param {string} nnn NNN рядок
29
+ * @param {'success'|'failed'} result результат
30
+ * @param {{ actor: string, now: string }} meta метадані
31
+ * @param {(p: string, c: string, enc: string) => void} writeFile функція запису
32
+ */
33
+ function writeRunFile(nodeDir, nnn, result, meta, writeFile) {
34
+ const fm = {
35
+ created_at: meta.now,
36
+ actor: meta.actor,
37
+ result
38
+ }
39
+ const content = buildMarkdown(fm, `## Run ${nnn}\n\nactor: ${meta.actor}\nresult: ${result}\n`)
40
+ writeFile(join(nodeDir, `run_${nnn}.md`), content, 'utf8')
41
+ }
42
+
43
+ /**
44
+ * Резолвить шлях вузла з аргументів або env/fallback-файлу.
45
+ * @param {string[]} args аргументи командного рядка
46
+ * @param {{ env?: Record<string, string>, cwd?: string, exists?: (p: string) => boolean, readFile?: (p: string, enc: string) => string }} deps ін'єкції
47
+ * @returns {{ nodePath: string | null, error: string | null }} результат
48
+ */
49
+ function resolveNodePath(args, deps) {
50
+ // 1. Прямий аргумент
51
+ if (args[0] && !args[0].startsWith('-')) {
52
+ return { nodePath: args[0], error: null }
53
+ }
54
+
55
+ // 2. ENV var
56
+ const env = deps.env ?? process.env
57
+ const fromEnv = env['NCURSOR_NODE_PATH']
58
+ if (fromEnv?.trim()) {
59
+ return { nodePath: fromEnv.trim(), error: null }
60
+ }
61
+
62
+ // 3. Fallback файл .n-cursor/current-node
63
+ const cwd = deps.cwd ?? processCwd()
64
+ const exists = deps.exists ?? existsSync
65
+ const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
66
+ const fallbackPath = join(cwd, '.n-cursor', 'current-node')
67
+ if (exists(fallbackPath)) {
68
+ try {
69
+ const content = readFile(fallbackPath, 'utf8').trim()
70
+ if (content.length > 0) return { nodePath: content, error: null }
71
+ } catch {
72
+ // пропускаємо
73
+ }
74
+ }
75
+
76
+ return { nodePath: null, error: 'NCURSOR_NODE_PATH not set and .n-cursor/current-node not found' }
77
+ }
78
+
79
+ /**
80
+ * `graph done <path>` — успіх → пише run_NNN.md (success), мерджить worktree.
81
+ * @param {string[]} args аргументи
82
+ * @param {object} [deps] ін'єкції
83
+ * @returns {Promise<number>} exit code
84
+ */
85
+ export async function cmdDone(args, deps = {}) {
86
+ const root = deps.cwd ?? processCwd()
87
+ const log = deps.log ?? console.log
88
+ const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
89
+ const writeFile = deps.writeFile ?? ((p, c, enc) => writeFileSync(p, c, enc))
90
+ const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
91
+ const exists = deps.exists ?? existsSync
92
+ const execSyncFn = deps.execSync ?? ((cmd, o) => execSync(cmd, { ...o, encoding: 'utf8' }))
93
+ const nowFn = deps.now ?? (() => new Date().toISOString())
94
+
95
+ const { nodePath, error } = resolveNodePath(args, { env: deps.env, cwd: root, exists, readFile })
96
+ if (!nodePath) {
97
+ log(`done: ${error}`)
98
+ return 1
99
+ }
100
+
101
+ const config = loadConfig({ root, readFile, exists })
102
+ const tasksDir = resolveTasksDir(config, root)
103
+ const worktreesDir = resolveWorktreesDir(config, root)
104
+ const nodeDir = join(tasksDir, nodePath)
105
+
106
+ if (!exists(join(nodeDir, 'task.md'))) {
107
+ log(`done: вузол "${nodePath}" не знайдено`)
108
+ return 1
109
+ }
110
+
111
+ // Записуємо run_NNN.md
112
+ const nnn = nextRunNNN(nodeDir, readdir)
113
+ try {
114
+ writeRunFile(nodeDir, nnn, 'success', { actor: 'agent', now: nowFn() }, writeFile)
115
+ log(`done: записано run_${nnn}.md (result: success)`)
116
+ } catch (err) {
117
+ log(`done: не вдалося записати run_${nnn}.md — ${err.message ?? String(err)}`)
118
+ return 1
119
+ }
120
+
121
+ // Знаходимо і мерджимо worktree
122
+ const worktreePath = findNodeWorktree(nodePath, worktreesDir, {
123
+ readdirSync: readdir,
124
+ execSync: execSyncFn
125
+ })
126
+
127
+ if (worktreePath) {
128
+ const mergeResult = mergeWorktree(worktreePath, root, { execSync: execSyncFn })
129
+ if (!mergeResult.ok) {
130
+ log(`done: merge не вдався — ${mergeResult.error}`)
131
+ return 1
132
+ }
133
+ log(`done: worktree merged і видалено`)
134
+ } else {
135
+ log(`done: worktree не знайдено для "${nodePath}" — пропускаємо merge`)
136
+ }
137
+
138
+ log(`done: вузол "${nodePath}" успішно завершено`)
139
+ return 0
140
+ }
141
+
142
+ /**
143
+ * `graph audit <path>` — аудит → creates pending-audit_NNN.md, merge worktree.
144
+ * @param {string[]} args аргументи
145
+ * @param {object} [deps] ін'єкції
146
+ * @returns {Promise<number>} exit code
147
+ */
148
+ export async function cmdAudit(args, deps = {}) {
149
+ const root = deps.cwd ?? processCwd()
150
+ const log = deps.log ?? console.log
151
+ const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
152
+ const writeFile = deps.writeFile ?? ((p, c, enc) => writeFileSync(p, c, enc))
153
+ const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
154
+ const exists = deps.exists ?? existsSync
155
+ const execSyncFn = deps.execSync ?? ((cmd, o) => execSync(cmd, { ...o, encoding: 'utf8' }))
156
+ const nowFn = deps.now ?? (() => new Date().toISOString())
157
+
158
+ const { nodePath, error } = resolveNodePath(args, { env: deps.env, cwd: root, exists, readFile })
159
+ if (!nodePath) {
160
+ log(`audit: ${error}`)
161
+ return 1
162
+ }
163
+
164
+ const config = loadConfig({ root, readFile, exists })
165
+ const tasksDir = resolveTasksDir(config, root)
166
+ const worktreesDir = resolveWorktreesDir(config, root)
167
+ const nodeDir = join(tasksDir, nodePath)
168
+
169
+ if (!exists(join(nodeDir, 'task.md'))) {
170
+ log(`audit: вузол "${nodePath}" не знайдено`)
171
+ return 1
172
+ }
173
+
174
+ // Знаходимо latest fact_NNN.md NNN
175
+ const factNNN = latestFactNNN(nodeDir, readdir)
176
+ if (!factNNN) {
177
+ log(`audit: fact_NNN.md не знайдено для "${nodePath}" — спершу виконайте задачу`)
178
+ return 1
179
+ }
180
+
181
+ // Створюємо pending-audit_NNN.md
182
+ const pendingPath = join(nodeDir, `pending-audit_${factNNN}.md`)
183
+ if (exists(pendingPath)) {
184
+ log(`audit: ${pendingPath} вже існує — audit вже запитано`)
185
+ return 1
186
+ }
187
+
188
+ const pendingContent = buildMarkdown({
189
+ created_at: nowFn(),
190
+ fact_ref: `fact_${factNNN}.md`,
191
+ actor: 'agent'
192
+ }, '')
193
+
194
+ try {
195
+ writeFile(pendingPath, pendingContent, 'utf8')
196
+ log(`audit: створено ${pendingPath}`)
197
+ } catch (err) {
198
+ log(`audit: не вдалося записати ${pendingPath} — ${err.message ?? String(err)}`)
199
+ return 1
200
+ }
201
+
202
+ // Записуємо run_NNN.md
203
+ const nnn = nextRunNNN(nodeDir, readdir)
204
+ try {
205
+ writeRunFile(nodeDir, nnn, 'success', { actor: 'agent', now: nowFn() }, writeFile)
206
+ log(`audit: записано run_${nnn}.md`)
207
+ } catch (err) {
208
+ log(`audit: не вдалося записати run_${nnn}.md — ${err.message ?? String(err)}`)
209
+ }
210
+
211
+ // Мерджимо worktree агента
212
+ const worktreePath = findNodeWorktree(nodePath, worktreesDir, {
213
+ readdirSync: readdir,
214
+ execSync: execSyncFn
215
+ })
216
+
217
+ if (worktreePath) {
218
+ const mergeResult = mergeWorktree(worktreePath, root, { execSync: execSyncFn })
219
+ if (!mergeResult.ok) {
220
+ log(`audit: merge не вдався — ${mergeResult.error}`)
221
+ } else {
222
+ log(`audit: agent worktree merged і видалено`)
223
+ }
224
+ }
225
+
226
+ log(`audit: запит аудиту для "${nodePath}" (fact_${factNNN}.md) успішно створено`)
227
+ return 0
228
+ }
229
+
230
+ /**
231
+ * `graph failed <path>` — провал → пише run_NNN.md (failed), залишає worktree.
232
+ * @param {string[]} args аргументи
233
+ * @param {object} [deps] ін'єкції
234
+ * @returns {Promise<number>} exit code
235
+ */
236
+ export async function cmdFailed(args, deps = {}) {
237
+ const root = deps.cwd ?? processCwd()
238
+ const log = deps.log ?? console.log
239
+ const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
240
+ const writeFile = deps.writeFile ?? ((p, c, enc) => writeFileSync(p, c, enc))
241
+ const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
242
+ const exists = deps.exists ?? existsSync
243
+ const nowFn = deps.now ?? (() => new Date().toISOString())
244
+
245
+ const { nodePath, error } = resolveNodePath(args, { env: deps.env, cwd: root, exists, readFile })
246
+ if (!nodePath) {
247
+ log(`failed: ${error}`)
248
+ return 1
249
+ }
250
+
251
+ const config = loadConfig({ root, readFile, exists })
252
+ const tasksDir = resolveTasksDir(config, root)
253
+ const nodeDir = join(tasksDir, nodePath)
254
+
255
+ if (!exists(join(nodeDir, 'task.md'))) {
256
+ log(`failed: вузол "${nodePath}" не знайдено`)
257
+ return 1
258
+ }
259
+
260
+ // Записуємо run_NNN.md з result:failed
261
+ const nnn = nextRunNNN(nodeDir, readdir)
262
+ try {
263
+ writeRunFile(nodeDir, nnn, 'failed', { actor: 'agent', now: nowFn() }, writeFile)
264
+ log(`failed: записано run_${nnn}.md (result: failed)`)
265
+ } catch (err) {
266
+ log(`failed: не вдалося записати run_${nnn}.md — ${err.message ?? String(err)}`)
267
+ return 1
268
+ }
269
+
270
+ log(`failed: вузол "${nodePath}" позначено як failed — worktree збережено для діагностики`)
271
+ return 0
272
+ }
273
+
274
+ /**
275
+ * `graph spawn <path>` — composite → перевіряє що дочірні вузли зареєстровані.
276
+ * @param {string[]} args аргументи
277
+ * @param {object} [deps] ін'єкції
278
+ * @returns {Promise<number>} exit code
279
+ */
280
+ export async function cmdSpawn(args, deps = {}) {
281
+ const root = deps.cwd ?? processCwd()
282
+ const log = deps.log ?? console.log
283
+ const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
284
+ const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
285
+ const exists = deps.exists ?? existsSync
286
+
287
+ const { nodePath, error } = resolveNodePath(args, { env: deps.env, cwd: root, exists, readFile })
288
+ if (!nodePath) {
289
+ log(`spawn: ${error}`)
290
+ return 1
291
+ }
292
+
293
+ const config = loadConfig({ root, readFile, exists })
294
+ const tasksDir = resolveTasksDir(config, root)
295
+ const nodeDir = join(tasksDir, nodePath)
296
+
297
+ if (!exists(join(nodeDir, 'task.md'))) {
298
+ log(`spawn: вузол "${nodePath}" не знайдено`)
299
+ return 1
300
+ }
301
+
302
+ // Перевіряємо дочірні директорії
303
+ let entries
304
+ try {
305
+ entries = readdir(nodeDir)
306
+ } catch {
307
+ log(`spawn: не вдалося прочитати директорію вузла`)
308
+ return 1
309
+ }
310
+
311
+ const childDirs = entries.filter(name => {
312
+ if (name.startsWith('.') || name.endsWith('.md') || name.endsWith('.json')) return false
313
+ return exists(join(nodeDir, name, 'task.md'))
314
+ })
315
+
316
+ if (childDirs.length === 0) {
317
+ log(`spawn: вузол "${nodePath}" не має дочірніх вузлів із task.md`)
318
+ log(`spawn: для composite вузла треба створити дочірні директорії з task.md`)
319
+ return 1
320
+ }
321
+
322
+ log(`spawn: вузол "${nodePath}" є composite з ${childDirs.length} дочірніми вузлами:`)
323
+ for (const child of childDirs) {
324
+ log(` - ${nodePath}/${child}`)
325
+ }
326
+
327
+ return 0
328
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * `n-cursor graph status [<path>] [--json]` — показує стан DAG вузлів.
3
+ *
4
+ * Без path — показує всі вузли. З path — лише вузол і його нащадків.
5
+ * --json — machine-readable JSON вивід.
6
+ *
7
+ * FS ін'єктується для тестованості.
8
+ */
9
+ import { execSync } from 'node:child_process'
10
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
11
+ import { join } from 'node:path'
12
+ import { cwd as processCwd } from 'node:process'
13
+
14
+ import { loadConfig, resolveTasksDir } from './config.mjs'
15
+ import { scanNodes, topoSort } from './scanner.mjs'
16
+ import { listActiveWorktrees } from './worktree-ops.mjs'
17
+
18
+ /** Кольори для стану (ANSI). */
19
+ const STATE_COLORS = {
20
+ 'needs-plan': '\x1b[33m', // жовтий
21
+ waiting: '\x1b[36m', // блакитний
22
+ running: '\x1b[34m', // синій
23
+ 'pending-audit': '\x1b[35m', // фіолетовий
24
+ resolved: '\x1b[32m', // зелений
25
+ failed: '\x1b[31m', // червоний
26
+ invalidated: '\x1b[90m' // сірий
27
+ }
28
+ const RESET = '\x1b[0m'
29
+
30
+ /**
31
+ * Повертає colored рядок стану (якщо TTY).
32
+ * @param {string} state стан вузла
33
+ * @param {boolean} color чи потрібен колір
34
+ * @returns {string} рядок
35
+ */
36
+ function colorState(state, color) {
37
+ if (!color) return state
38
+ const c = STATE_COLORS[state] ?? ''
39
+ return `${c}${state}${RESET}`
40
+ }
41
+
42
+ /**
43
+ * `graph status [<path>] [--json]` command handler.
44
+ * @param {string[]} args аргументи
45
+ * @param {{
46
+ * cwd?: string,
47
+ * log?: (m: string) => void,
48
+ * readFile?: (p: string, enc: string) => string,
49
+ * readdir?: (d: string) => string[],
50
+ * exists?: (p: string) => boolean,
51
+ * execSync?: (cmd: string, opts?: object) => string
52
+ * }} [deps] ін'єкції
53
+ * @returns {Promise<number>} exit code
54
+ */
55
+ export async function cmdStatus(args, deps = {}) {
56
+ const root = deps.cwd ?? processCwd()
57
+ const log = deps.log ?? console.log
58
+ const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
59
+ const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
60
+ const exists = deps.exists ?? existsSync
61
+ const execSyncFn = deps.execSync ?? ((cmd, opts) => execSync(cmd, { ...opts, encoding: 'utf8' }))
62
+
63
+ // Парсимо аргументи
64
+ let nodePath = null
65
+ let jsonMode = false
66
+
67
+ for (const arg of args) {
68
+ if (arg === '--json') jsonMode = true
69
+ else if (!arg.startsWith('-')) nodePath = arg
70
+ }
71
+
72
+ const config = loadConfig({ root, readFile, exists })
73
+ const tasksDir = resolveTasksDir(config, root)
74
+ const worktreesDir = join(root, config.worktrees_dir.startsWith('/') ? config.worktrees_dir : config.worktrees_dir.slice(2))
75
+
76
+ const activeWorktrees = listActiveWorktrees(root, { execSync: execSyncFn })
77
+
78
+ const allNodes = scanNodes(tasksDir, activeWorktrees, {
79
+ readdirSync: readdir,
80
+ existsSync: exists,
81
+ readFileSync: readFile
82
+ })
83
+
84
+ // Фільтруємо якщо є path
85
+ let nodes = allNodes
86
+ if (nodePath) {
87
+ nodes = allNodes.filter(n => n.path === nodePath || n.path.startsWith(nodePath + '/'))
88
+ if (nodes.length === 0) {
89
+ log(`status: вузол "${nodePath}" не знайдено`)
90
+ return 1
91
+ }
92
+ }
93
+
94
+ const sorted = topoSort(nodes)
95
+
96
+ if (jsonMode) {
97
+ console.log(JSON.stringify(sorted.map(n => ({
98
+ id: n.id,
99
+ path: n.path,
100
+ state: n.state,
101
+ deps: n.deps,
102
+ composite: n.composite,
103
+ children: n.children
104
+ })), null, 2))
105
+ return 0
106
+ }
107
+
108
+ // Текстовий вивід
109
+ const useColor = process.stdout.isTTY ?? false
110
+
111
+ // Підрахунок по станах
112
+ const stateCounts = {}
113
+ for (const n of sorted) {
114
+ stateCounts[n.state] = (stateCounts[n.state] ?? 0) + 1
115
+ }
116
+ const summary = Object.entries(stateCounts)
117
+ .map(([s, c]) => `${colorState(s, useColor)}:${c}`)
118
+ .join(' ')
119
+
120
+ log(`DAG tasks — ${summary}`)
121
+ log('')
122
+
123
+ for (const node of sorted) {
124
+ const indent = node.path.includes('/') ? ' '.repeat(node.path.split('/').length - 1) : ''
125
+ const composite = node.composite ? ' [composite]' : ''
126
+ const deps = node.deps.length > 0 ? ` ← [${node.deps.join(', ')}]` : ''
127
+ log(`${indent}${node.path} [${colorState(node.state, useColor)}]${composite}${deps}`)
128
+ }
129
+
130
+ return 0
131
+ }