@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,115 +0,0 @@
1
- /**
2
- * `n-cursor graph` — CLI entry point для нової graph-архітектури.
3
- * Реалізує команди з docs/думка.MD (фінальний дизайн 2026-06-07).
4
- */
5
-
6
- /**
7
- * @param {string[]} argv процесні аргументи після 'graph'
8
- * @returns {Promise<number>} exit code
9
- */
10
- export async function runGraphCli(argv) {
11
- const [cmd, ...rest] = argv
12
-
13
- const hasJson = rest.includes('--json')
14
- const cleanRest = rest.filter(a => a !== '--json')
15
-
16
- switch (cmd) {
17
- case 'scan':
18
- return (await import('./scan.mjs')).runScan(cleanRest, { json: hasJson })
19
-
20
- case 'status': {
21
- const path = cleanRest[0]
22
- return (await import('./status.mjs')).runStatus(path, { json: hasJson })
23
- }
24
-
25
- case 'setup':
26
- return (await import('./setup.mjs')).runSetup(cleanRest)
27
-
28
- case 'init': {
29
- const [name, ...initRest] = cleanRest
30
- return (await import('./init.mjs')).runInit(name, parseFlags(initRest))
31
- }
32
-
33
- case 'plan': {
34
- const flags = parseFlags(cleanRest)
35
- return (await import('./plan.mjs')).runPlan(flags.path, flags)
36
- }
37
-
38
- case 'run': {
39
- const flags = parseFlags(cleanRest)
40
- return (await import('./run.mjs')).runRun(flags.path, flags)
41
- }
42
-
43
- case 'kill': {
44
- const path = cleanRest[0]
45
- return (await import('./kill.mjs')).runKill(path)
46
- }
47
-
48
- case 'invalidate': {
49
- const flags = parseFlags(cleanRest)
50
- return (await import('./invalidate.mjs')).runInvalidate(flags.path, flags)
51
- }
52
-
53
- case 'done':
54
- case 'audit':
55
- case 'failed':
56
- case 'spawn': {
57
- const path = cleanRest[0]
58
- return (await import('./signals.mjs')).runSignal(cmd, path, parseFlags(cleanRest.slice(1)))
59
- }
60
-
61
- case undefined:
62
- case '--help':
63
- case 'help':
64
- printHelp()
65
- return 0
66
-
67
- default:
68
- console.error(`Unknown graph command: ${cmd}`)
69
- printHelp()
70
- return 1
71
- }
72
- }
73
-
74
- /** @param {string[]} args @returns {Record<string, string | boolean>} */
75
- function parseFlags(args) {
76
- /** @type {Record<string, string | boolean>} */
77
- const flags = {}
78
- for (let i = 0; i < args.length; i++) {
79
- const a = args[i]
80
- if (a.startsWith('--')) {
81
- const key = a.slice(2)
82
- const next = args[i + 1]
83
- if (next && !next.startsWith('--')) {
84
- flags[key] = next
85
- i++
86
- } else {
87
- flags[key] = true
88
- }
89
- } else if (!flags.path) {
90
- flags.path = a
91
- }
92
- }
93
- return flags
94
- }
95
-
96
- function printHelp() {
97
- console.log(`n-cursor graph <command> [options]
98
-
99
- Commands:
100
- setup Initialize project (.n-cursor.json, hooks)
101
- init <name> [--task "..."] Create task.md for a new node
102
- plan [<path>] [--mode agent] Stage 1: spec + decompose → plan_NNN.md
103
- status [<path>] [--json] Show graph or node state
104
- scan [--json] Full scan; exit 1 if any failed nodes
105
- run [<path>] [--actor a] [--auto] Execute node or run orchestrator
106
- kill <path> Kill worktrees + cascade invalidate
107
- invalidate <path> [--no-cascade] Mark node as invalidated
108
-
109
- Agent signals (called from within worktree):
110
- done <path> Signal success → merge
111
- audit <path> Request audit → pending-audit_NNN.md
112
- failed <path> Signal failure
113
- spawn <path> Register composite subgraph
114
- `)
115
- }
@@ -1,62 +0,0 @@
1
- /**
2
- * Завантаження `.n-cursor.json` + per-node `.n-cursor-override.json`.
3
- * Всі поля опціональні — повертає merge із дефолтами.
4
- */
5
- import { existsSync, readFileSync } from 'node:fs'
6
- import { join } from 'node:path'
7
-
8
- /** @typedef {{ tasks_dir: string, worktrees_dir: string, warn_worktrees_above: number, max_worktrees: number, default_budget_sec: number, budget_hard_sec_multiplier: number, progress_timeout_sec: number, stderr_lines: number, claude_model: string, audit_model: string, model_map: Record<string,string>, stale_worktree_min: number, system_prompt: string }} GraphConfig */
9
-
10
- /** @type {GraphConfig} */
11
- const DEFAULTS = {
12
- tasks_dir: './tasks',
13
- worktrees_dir: './.worktrees',
14
- warn_worktrees_above: 4,
15
- max_worktrees: 8,
16
- default_budget_sec: 1800,
17
- budget_hard_sec_multiplier: 3,
18
- progress_timeout_sec: 300,
19
- stderr_lines: 50,
20
- claude_model: 'claude-sonnet-4-6',
21
- audit_model: 'claude-haiku-4-5-20251001',
22
- model_map: {
23
- MIM: 'claude-haiku-4-5-20251001',
24
- AVG: 'claude-sonnet-4-6',
25
- MAX: 'claude-opus-4-8',
26
- },
27
- stale_worktree_min: 30,
28
- system_prompt: '.n-cursor/system-prompt.md',
29
- }
30
-
31
- /**
32
- * Читає `.n-cursor.json` з root та мержить із дефолтами.
33
- * @param {string} root
34
- * @returns {GraphConfig}
35
- */
36
- export function loadConfig(root) {
37
- const path = join(root, '.n-cursor.json')
38
- if (!existsSync(path)) return { ...DEFAULTS }
39
- try {
40
- const raw = JSON.parse(readFileSync(path, 'utf8'))
41
- return { ...DEFAULTS, ...raw, model_map: { ...DEFAULTS.model_map, ...raw.model_map } }
42
- } catch {
43
- return { ...DEFAULTS }
44
- }
45
- }
46
-
47
- /**
48
- * Читає per-node `.n-cursor-override.json` та мержить із базовим конфігом.
49
- * @param {GraphConfig} base
50
- * @param {string} nodePath абсолютний шлях до директорії вузла
51
- * @returns {GraphConfig}
52
- */
53
- export function loadNodeOverride(base, nodePath) {
54
- const path = join(nodePath, '.n-cursor-override.json')
55
- if (!existsSync(path)) return base
56
- try {
57
- const raw = JSON.parse(readFileSync(path, 'utf8'))
58
- return { ...base, ...raw }
59
- } catch {
60
- return base
61
- }
62
- }
@@ -1,161 +0,0 @@
1
- /**
2
- * Побудова DAG з файлової структури `tasks/`.
3
- * Читає task.md кожного вузла (один раз), будує граф в пам'яті.
4
- * Deps satisfaction вираховується пакетно — не per-node.
5
- */
6
- import { existsSync, readFileSync, readdirSync } from 'node:fs'
7
- import { join, relative } from 'node:path'
8
-
9
- import { parseFrontmatter } from './frontmatter.mjs'
10
- import { deriveAtomicState, deriveCompositeState } from './state.mjs'
11
-
12
- /**
13
- * @typedef {{
14
- * id: string,
15
- * path: string,
16
- * parentId: string | null,
17
- * deps: string[],
18
- * mode: string,
19
- * executor: { type: string, model_tier: string, skills: string[] },
20
- * budget_sec: number,
21
- * isComposite: boolean,
22
- * children: string[],
23
- * state: import('./state.mjs').NodeState,
24
- * meta: Record<string, unknown>
25
- * }} GraphNode
26
- */
27
-
28
- /**
29
- * Сканує tasks_dir і будує повний DAG.
30
- * @param {string} tasksDir абсолютний шлях
31
- * @param {string} [worktreesDir]
32
- * @returns {Map<string, GraphNode>}
33
- */
34
- export function buildDag(tasksDir, worktreesDir = '') {
35
- /** @type {Map<string, GraphNode>} */
36
- const nodes = new Map()
37
-
38
- // 1. Collect all nodes
39
- collectNodes(tasksDir, tasksDir, null, nodes)
40
-
41
- // 2. Resolve parent-child relationships
42
- for (const node of nodes.values()) {
43
- if (node.parentId) {
44
- const parent = nodes.get(node.parentId)
45
- if (parent && !parent.children.includes(node.id)) {
46
- parent.children.push(node.id)
47
- parent.isComposite = true
48
- }
49
- }
50
- }
51
-
52
- // 3. Derive states (bottom-up)
53
- const resolvedIds = new Set()
54
- deriveStatesBottomUp(tasksDir, nodes, resolvedIds, worktreesDir)
55
-
56
- return nodes
57
- }
58
-
59
- /**
60
- * @param {string} dir поточна директорія
61
- * @param {string} tasksDir корінь tasks
62
- * @param {string | null} parentId
63
- * @param {Map<string, GraphNode>} nodes
64
- */
65
- function collectNodes(dir, tasksDir, parentId, nodes) {
66
- const taskFile = join(dir, 'task.md')
67
- if (!existsSync(taskFile)) return
68
-
69
- const id = relative(tasksDir, dir).replace(/\\/gu, '/')
70
- if (!id) return
71
-
72
- const { data } = parseFrontmatter(readSafe(taskFile))
73
-
74
- /** @type {string[]} */
75
- const deps = Array.isArray(data.deps) ? data.deps.map(String) : []
76
-
77
- const executor = typeof data.executor === 'object' && data.executor !== null
78
- ? data.executor
79
- : { type: 'agent', model_tier: 'AVG', skills: [] }
80
-
81
- /** @type {GraphNode} */
82
- const node = {
83
- id,
84
- path: dir,
85
- parentId,
86
- deps,
87
- mode: String(data.mode ?? 'human'),
88
- executor: {
89
- type: String(executor.type ?? 'agent'),
90
- model_tier: String(executor.model_tier ?? 'AVG'),
91
- skills: Array.isArray(executor.skills) ? executor.skills : [],
92
- },
93
- budget_sec: Number(data.budget_sec ?? 1800),
94
- isComposite: false,
95
- children: [],
96
- state: 'needs-plan',
97
- meta: data,
98
- }
99
-
100
- nodes.set(id, node)
101
-
102
- // Scan children
103
- try {
104
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
105
- if (entry.isDirectory()) {
106
- collectNodes(join(dir, entry.name), tasksDir, id, nodes)
107
- }
108
- }
109
- } catch { /* skip */ }
110
- }
111
-
112
- /**
113
- * Обходить граф знизу вверх (листи → корінь) і деривує стани.
114
- * @param {string} tasksDir
115
- * @param {Map<string, GraphNode>} nodes
116
- * @param {Set<string>} resolvedIds
117
- * @param {string} worktreesDir
118
- */
119
- function deriveStatesBottomUp(tasksDir, nodes, resolvedIds, worktreesDir) {
120
- // Topological sort (Kahn's algorithm by deps within siblings)
121
- const visited = new Set()
122
- const order = []
123
-
124
- /** @param {string} id */
125
- function visit(id) {
126
- if (visited.has(id)) return
127
- visited.add(id)
128
- const node = nodes.get(id)
129
- if (!node) return
130
- for (const child of node.children) visit(child)
131
- order.push(id)
132
- }
133
-
134
- for (const id of nodes.keys()) visit(id)
135
-
136
- // Process leaves first (order is reversed)
137
- for (const id of order) {
138
- const node = nodes.get(id)
139
- if (!node) continue
140
-
141
- if (node.isComposite) {
142
- const childStates = node.children.map(cid => nodes.get(cid)?.state ?? 'needs-plan')
143
- node.state = deriveCompositeState(node.path, childStates)
144
- } else {
145
- const depsResolved = node.deps.every(dep => {
146
- // Deps are sibling IDs — resolve relative to parent
147
- const siblingId = node.parentId ? `${node.parentId}/${dep}` : dep
148
- const sibling = nodes.get(siblingId) ?? nodes.get(dep)
149
- return sibling?.state === 'resolved'
150
- })
151
- node.state = deriveAtomicState(node.path, { depsResolved })
152
- }
153
-
154
- if (node.state === 'resolved') resolvedIds.add(id)
155
- }
156
- }
157
-
158
- /** @param {string} path @returns {string} */
159
- function readSafe(path) {
160
- try { return readFileSync(path, 'utf8') } catch { return '' }
161
- }
@@ -1,70 +0,0 @@
1
- /**
2
- * Мінімальний YAML-фронтматер парсер для Markdown-файлів.
3
- * Підтримує: рядки, числа, boolean, масиви (flow і block), вкладені об'єкти (один рівень).
4
- */
5
-
6
- const FM_RE = /^---\r?\n([\s\S]*?)\r?\n---/u
7
-
8
- /**
9
- * @param {string} content вміст md-файлу
10
- * @returns {{ data: Record<string, unknown>, body: string }}
11
- */
12
- export function parseFrontmatter(content) {
13
- const m = FM_RE.exec(content)
14
- if (!m) return { data: {}, body: content }
15
- const raw = m[1]
16
- const body = content.slice(m[0].length).trimStart()
17
- return { data: parseYamlBlock(raw), body }
18
- }
19
-
20
- /**
21
- * @param {string} block YAML-блок між ---
22
- * @returns {Record<string, unknown>}
23
- */
24
- function parseYamlBlock(block) {
25
- const result = {}
26
- const lines = block.split(/\r?\n/u)
27
- let i = 0
28
-
29
- while (i < lines.length) {
30
- const line = lines[i]
31
- const keyMatch = /^([a-z_][a-z0-9_]*):\s*(.*)/iu.exec(line)
32
- if (!keyMatch) { i++; continue }
33
-
34
- const key = keyMatch[1]
35
- const rest = keyMatch[2].trim()
36
-
37
- if (rest === '' || rest === '|' || rest === '>') {
38
- // block scalar або об'єкт — збираємо indented рядки
39
- const children = []
40
- i++
41
- while (i < lines.length && /^\s+/u.test(lines[i])) {
42
- children.push(lines[i])
43
- i++
44
- }
45
- if (children.length > 0 && /^\s+-\s+/u.test(children[0])) {
46
- result[key] = children.map(l => l.replace(/^\s+-\s+/u, '').trim())
47
- } else {
48
- result[key] = parseYamlBlock(children.map(l => l.replace(/^\s{2}/u, '')).join('\n'))
49
- }
50
- } else if (rest.startsWith('[')) {
51
- result[key] = rest.slice(1, rest.lastIndexOf(']')).split(',').map(s => s.trim()).filter(Boolean)
52
- i++
53
- } else {
54
- result[key] = parseScalar(rest)
55
- i++
56
- }
57
- }
58
-
59
- return result
60
- }
61
-
62
- /** @param {string} s */
63
- function parseScalar(s) {
64
- if (s === 'true') return true
65
- if (s === 'false') return false
66
- if (s === 'null' || s === '~') return null
67
- const n = Number(s)
68
- if (!Number.isNaN(n) && s !== '') return n
69
- return s.replace(/^["']|["']$/gu, '')
70
- }
@@ -1,77 +0,0 @@
1
- /**
2
- * NNN-лічильники — рахують існуючі файли певного патерну і повертають наступний NNN.
3
- * Zero-padded до 3 цифр: 001, 002, …
4
- */
5
- import { readdirSync } from 'node:fs'
6
-
7
- /**
8
- * @param {string} dir абсолютний шлях до директорії вузла
9
- * @param {RegExp} pattern регексп для імен файлів (напр. /^run_(\d{3})\.md$/)
10
- * @returns {number} кількість файлів що підходять
11
- */
12
- export function countFiles(dir, pattern) {
13
- try {
14
- return readdirSync(dir).filter(f => pattern.test(f)).length
15
- } catch {
16
- return 0
17
- }
18
- }
19
-
20
- /**
21
- * @param {string} dir
22
- * @returns {string} наступний NNN для run_NNN.md (zero-padded)
23
- */
24
- export function nextRunNNN(dir) {
25
- const count = countFiles(dir, /^run_\d{3}\.md$/u)
26
- return String(count + 1).padStart(3, '0')
27
- }
28
-
29
- /**
30
- * @param {string} dir
31
- * @returns {string} наступний NNN для plan_NNN.md (zero-padded)
32
- */
33
- export function nextPlanNNN(dir) {
34
- const count = countFiles(dir, /^plan_\d{3}\.md$/u)
35
- return String(count + 1).padStart(3, '0')
36
- }
37
-
38
- /**
39
- * Знаходить NNN останнього fact_NNN.md.
40
- * @param {string} dir
41
- * @returns {string | null}
42
- */
43
- export function latestFactNNN(dir) {
44
- try {
45
- const files = readdirSync(dir)
46
- .filter(f => /^fact_\d{3}\.md$/u.test(f))
47
- .sort()
48
- if (files.length === 0) return null
49
- return files.at(-1).replace('fact_', '').replace('.md', '')
50
- } catch {
51
- return null
52
- }
53
- }
54
-
55
- /**
56
- * Знаходить NNN pending-audit без відповідного audit-result.
57
- * @param {string} dir
58
- * @returns {string | null}
59
- */
60
- export function pendingAuditNNN(dir) {
61
- try {
62
- const files = readdirSync(dir)
63
- const pending = files.filter(f => /^pending-audit_\d{3}\.md$/u.test(f)).sort()
64
- for (const p of pending) {
65
- const nnn = p.replace('pending-audit_', '').replace('.md', '')
66
- if (!files.includes(`audit-result_${nnn}.md`)) return nnn
67
- }
68
- return null
69
- } catch {
70
- return null
71
- }
72
- }
73
-
74
- /** @param {number} n @returns {string} */
75
- export function pad3(n) {
76
- return String(n).padStart(3, '0')
77
- }
@@ -1,110 +0,0 @@
1
- /**
2
- * Визначення стану вузла за наявністю файлів (O(1), без читання вмісту).
3
- * Пріоритет: invalidated > resolved > pending-audit > stalled > running > waiting/blocked > failed > needs-plan
4
- */
5
- import { existsSync, readdirSync } from 'node:fs'
6
- import { join } from 'node:path'
7
-
8
- /**
9
- * @typedef {'needs-plan'|'waiting'|'blocked'|'running'|'stalled'|'pending-audit'|'resolved'|'failed'|'invalidated'} NodeState
10
- */
11
-
12
- /**
13
- * Визначає стан атомарного вузла за файлами у директорії.
14
- * @param {string} dir абсолютний шлях до директорії вузла
15
- * @param {{ depsResolved: boolean }} opts
16
- * @returns {NodeState}
17
- */
18
- export function deriveAtomicState(dir, { depsResolved = true } = {}) {
19
- if (existsSync(join(dir, 'invalidated'))) return 'invalidated'
20
-
21
- const files = listFiles(dir)
22
-
23
- if (hasFact(files) && !files.includes('invalidated')) return 'resolved'
24
-
25
- const pendingNNN = findPendingAudit(files)
26
- if (pendingNNN !== null) return 'pending-audit'
27
-
28
- const runningUntil = findRunningUntil(files)
29
- if (runningUntil !== null) {
30
- const ts = Number(runningUntil)
31
- const now = Math.floor(Date.now() / 1000)
32
- return ts > now ? 'running' : 'stalled'
33
- }
34
-
35
- const hasPlan = files.some(f => /^plan_\d{3}\.md$/u.test(f))
36
- const hasRun = files.some(f => /^run_\d{3}\.md$/u.test(f))
37
-
38
- if (!hasPlan) return 'needs-plan'
39
- if (hasRun && !hasFact(files)) return 'failed'
40
- if (hasPlan && !depsResolved) return 'blocked'
41
- return 'waiting'
42
- }
43
-
44
- /**
45
- * Визначає стан composite вузла за станами дітей.
46
- * @param {string} dir
47
- * @param {NodeState[]} childStates
48
- * @returns {NodeState}
49
- */
50
- export function deriveCompositeState(dir, childStates) {
51
- if (existsSync(join(dir, 'invalidated'))) return 'invalidated'
52
-
53
- if (childStates.length === 0) return 'needs-plan'
54
- if (childStates.every(s => s === 'resolved')) return 'resolved'
55
- if (childStates.some(s => s === 'running' || s === 'pending-audit')) return 'running'
56
- if (childStates.some(s => s === 'stalled')) return 'stalled'
57
- if (childStates.some(s => s === 'failed') && !childStates.some(s => s === 'running')) return 'failed'
58
- return 'waiting'
59
- }
60
-
61
- /**
62
- * Перевіряє наявність orphan worktree для вузла (resolved + worktree exists).
63
- * @param {string} dir
64
- * @param {string} worktreesDir
65
- * @returns {boolean}
66
- */
67
- export function hasOrphanWorktree(dir, worktreesDir) {
68
- const nodeName = dir.split('/').at(-1)
69
- try {
70
- return readdirSync(worktreesDir).some(d => d.startsWith(nodeName))
71
- } catch {
72
- return false
73
- }
74
- }
75
-
76
- // --- helpers ---
77
-
78
- /** @param {string} dir @returns {string[]} */
79
- function listFiles(dir) {
80
- try { return readdirSync(dir) } catch { return [] }
81
- }
82
-
83
- /** @param {string[]} files @returns {boolean} */
84
- function hasFact(files) {
85
- return files.some(f => /^fact_\d{3}\.md$/u.test(f))
86
- }
87
-
88
- /**
89
- * Знаходить перший pending-audit_NNN без audit-result_NNN.
90
- * @param {string[]} files
91
- * @returns {string | null} NNN або null
92
- */
93
- function findPendingAudit(files) {
94
- const pending = files.filter(f => /^pending-audit_\d{3}\.md$/u.test(f)).sort()
95
- for (const p of pending) {
96
- const nnn = p.replace('pending-audit_', '').replace('.md', '')
97
- if (!files.includes(`audit-result_${nnn}.md`)) return nnn
98
- }
99
- return null
100
- }
101
-
102
- /**
103
- * Знаходить `running_until_<ts>` файл і повертає ts як рядок.
104
- * @param {string[]} files
105
- * @returns {string | null}
106
- */
107
- function findRunningUntil(files) {
108
- const f = files.find(f => /^running_until_\d+$/u.test(f))
109
- return f ? f.replace('running_until_', '') : null
110
- }
@@ -1,64 +0,0 @@
1
- /**
2
- * `graph scan` — повне сканування граф: відновлює стан з файлів, виводить результат.
3
- * exit 0 — граф чистий; exit 1 — є вузли у стані failed.
4
- */
5
- import { resolve } from 'node:path'
6
- import { cwd } from 'node:process'
7
-
8
- import { loadConfig } from './lib/config.mjs'
9
- import { buildDag } from './lib/dag.mjs'
10
-
11
- /**
12
- * @param {string[]} args
13
- * @param {{ json?: boolean }} opts
14
- */
15
- export async function runScan(args, opts = {}) {
16
- const root = cwd()
17
- const config = loadConfig(root)
18
- const tasksDir = resolve(root, config.tasks_dir)
19
- const worktreesDir = resolve(root, config.worktrees_dir)
20
-
21
- const nodes = buildDag(tasksDir, worktreesDir)
22
-
23
- if (opts.json) {
24
- const out = {}
25
- for (const [id, node] of nodes) out[id] = { state: node.state, deps: node.deps }
26
- process.stdout.write(JSON.stringify(out, null, 2) + '\n')
27
- } else {
28
- printScanTable(nodes)
29
- }
30
-
31
- const hasFailed = [...nodes.values()].some(n => n.state === 'failed')
32
- return hasFailed ? 1 : 0
33
- }
34
-
35
- /** @param {Map<string, import('./lib/dag.mjs').GraphNode>} nodes */
36
- function printScanTable(nodes) {
37
- const STATE_ICON = {
38
- 'needs-plan': '⏳',
39
- waiting: '○',
40
- blocked: '○',
41
- running: '◉',
42
- stalled: '⚠',
43
- 'pending-audit': '🔍',
44
- resolved: '✓',
45
- failed: '✗',
46
- invalidated: '⊘',
47
- }
48
-
49
- const counts = {}
50
- for (const { state } of nodes.values()) counts[state] = (counts[state] ?? 0) + 1
51
-
52
- const summary = Object.entries(counts).map(([s, n]) => `${s}:${n}`).join(' ')
53
- console.log(`graph — ${summary}\n`)
54
-
55
- for (const [id, node] of nodes) {
56
- const icon = STATE_ICON[node.state] ?? '?'
57
- let detail = ''
58
- if (node.state === 'needs-plan') detail = `run: graph plan tasks/${id}/`
59
- else if (node.state === 'blocked') detail = `blocked: ${node.deps.join(', ')}`
60
- else if (node.state === 'stalled') detail = 'stalled — deadline passed'
61
- const suffix = detail ? ` [${detail}]` : ''
62
- console.log(` ${icon} ${id.padEnd(30)} [${node.state}]${suffix}`)
63
- }
64
- }