@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.
- package/CHANGELOG.md +20 -0
- package/bin/docs/n-cursor.md +1 -9
- package/bin/n-cursor.js +3 -25
- package/docs/stryker.config.md +37 -0
- package/docs/vitest.config.md +23 -0
- package/package.json +2 -1
- package/rules/docker/lib/docs/docker-mirror.md +1 -1
- package/rules/docker/lib/docs/docker-native-addon.md +1 -1
- package/rules/test/coverage/coverage.mjs +9 -19
- package/rules/test/test.mdc +1 -1
- package/scripts/dispatcher/trace.mjs +4 -16
- package/scripts/docs/build-agents-commands.md +1 -1
- package/scripts/docs/worktree-cli.md +1 -1
- package/scripts/lib/changed-files.mjs +19 -3
- package/scripts/lib/sync-gitignore-worktree.mjs +4 -5
- package/scripts/worktree-cli.mjs +1 -2
- package/skills/docgen/js/docgen-gen.mjs +7 -7
- package/scripts/dispatcher/docs/graph.md +0 -346
- package/scripts/dispatcher/docs/index.md +0 -236
- package/scripts/dispatcher/docs/trace.md +0 -296
- package/scripts/dispatcher/graph/lib/cmd-init.mjs +0 -112
- package/scripts/dispatcher/graph/lib/cmd-invalidate.mjs +0 -96
- package/scripts/dispatcher/graph/lib/cmd-kill.mjs +0 -141
- package/scripts/dispatcher/graph/lib/cmd-plan.mjs +0 -142
- package/scripts/dispatcher/graph/lib/cmd-run.mjs +0 -328
- package/scripts/dispatcher/graph/lib/cmd-scan.mjs +0 -115
- package/scripts/dispatcher/graph/lib/cmd-setup.mjs +0 -111
- package/scripts/dispatcher/graph/lib/cmd-signals.mjs +0 -328
- package/scripts/dispatcher/graph/lib/cmd-status.mjs +0 -131
- package/scripts/dispatcher/graph/lib/cmd-verify.mjs +0 -100
- package/scripts/dispatcher/graph/lib/cmd-watch.mjs +0 -128
- package/scripts/dispatcher/graph/lib/config.mjs +0 -103
- package/scripts/dispatcher/graph/lib/frontmatter.mjs +0 -224
- package/scripts/dispatcher/graph/lib/nnn.mjs +0 -127
- package/scripts/dispatcher/graph/lib/node-state.mjs +0 -157
- package/scripts/dispatcher/graph/lib/scanner.mjs +0 -235
- package/scripts/dispatcher/graph/lib/worktree-ops.mjs +0 -193
- package/scripts/dispatcher/graph-tasks.mjs +0 -92
- package/scripts/dispatcher/graph.mjs +0 -212
- package/scripts/dispatcher/index.mjs +0 -45
- package/scripts/dispatcher/lib/docs/active.md +0 -348
- package/scripts/dispatcher/lib/docs/artifact.md +0 -232
- package/scripts/dispatcher/lib/docs/budget.md +0 -167
- package/scripts/dispatcher/lib/docs/capability.md +0 -196
- package/scripts/dispatcher/lib/docs/commands.md +0 -210
- package/scripts/dispatcher/lib/docs/events.md +0 -183
- package/scripts/dispatcher/lib/docs/executor.md +0 -190
- package/scripts/dispatcher/lib/docs/gate.md +0 -231
- package/scripts/dispatcher/lib/docs/level.md +0 -335
- package/scripts/dispatcher/lib/docs/plan-panel.md +0 -181
- package/scripts/dispatcher/lib/docs/plan.md +0 -200
- package/scripts/dispatcher/lib/docs/planner.md +0 -269
- package/scripts/dispatcher/lib/docs/review.md +0 -255
- package/scripts/dispatcher/lib/docs/reviewer.md +0 -240
- package/scripts/dispatcher/lib/docs/snapshot.md +0 -247
- package/scripts/dispatcher/lib/docs/spec.md +0 -203
- package/scripts/dispatcher/lib/docs/state-store.md +0 -303
- package/scripts/dispatcher/lib/docs/subagent-runner.md +0 -173
- package/scripts/dispatcher/lib/events.mjs +0 -67
- package/scripts/dispatcher/lib/executor.mjs +0 -107
- package/scripts/dispatcher/lib/plan-panel.mjs +0 -76
- package/scripts/dispatcher/lib/state-store.mjs +0 -173
- package/scripts/dispatcher/lib/subagent-runner.mjs +0 -53
- package/scripts/graph/index.mjs +0 -115
- package/scripts/graph/lib/config.mjs +0 -62
- package/scripts/graph/lib/dag.mjs +0 -161
- package/scripts/graph/lib/frontmatter.mjs +0 -70
- package/scripts/graph/lib/nnn.mjs +0 -77
- package/scripts/graph/lib/state.mjs +0 -110
- package/scripts/graph/scan.mjs +0 -64
- package/scripts/graph/status.mjs +0 -86
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Завантаження конфігурації `.n-cursor.json` для graph-команд.
|
|
3
|
-
*
|
|
4
|
-
* Читає JSON з кореня репо (або з вказаного шляху), мержить із дефолтами.
|
|
5
|
-
* Підтримує per-node override через task.md (якщо вузол передає свої налаштування).
|
|
6
|
-
*
|
|
7
|
-
* FS ін'єктується для тестованості без диска.
|
|
8
|
-
*/
|
|
9
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
10
|
-
import { join } from 'node:path'
|
|
11
|
-
import { cwd as processCwd } from 'node:process'
|
|
12
|
-
|
|
13
|
-
/** Дефолтні значення конфігурації. */
|
|
14
|
-
export const CONFIG_DEFAULTS = {
|
|
15
|
-
tasks_dir: './tasks',
|
|
16
|
-
worktrees_dir: './.worktrees',
|
|
17
|
-
warn_worktrees_above: 4,
|
|
18
|
-
max_worktrees: 8,
|
|
19
|
-
default_budget_sec: 1800,
|
|
20
|
-
budget_hard_sec_multiplier: 3,
|
|
21
|
-
progress_timeout_sec: 300,
|
|
22
|
-
claude_model: 'claude-sonnet-4-6',
|
|
23
|
-
audit_model: 'claude-haiku-4-5-20251001',
|
|
24
|
-
model_map: {
|
|
25
|
-
MIM: 'claude-haiku-4-5-20251001',
|
|
26
|
-
AVG: 'claude-sonnet-4-6',
|
|
27
|
-
MAX: 'claude-opus-4-8'
|
|
28
|
-
},
|
|
29
|
-
stale_worktree_min: 30,
|
|
30
|
-
system_prompt: '.n-cursor/system-prompt.md'
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Завантажує конфігурацію з `.n-cursor.json` і мержить із дефолтами.
|
|
35
|
-
* @param {{
|
|
36
|
-
* root?: string,
|
|
37
|
-
* readFile?: (p: string, enc: string) => string,
|
|
38
|
-
* exists?: (p: string) => boolean
|
|
39
|
-
* }} [deps] ін'єкції
|
|
40
|
-
* @returns {typeof CONFIG_DEFAULTS} злита конфігурація
|
|
41
|
-
*/
|
|
42
|
-
export function loadConfig(deps = {}) {
|
|
43
|
-
const root = deps.root ?? processCwd()
|
|
44
|
-
const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
|
|
45
|
-
const exists = deps.exists ?? existsSync
|
|
46
|
-
|
|
47
|
-
const configPath = join(root, '.n-cursor.json')
|
|
48
|
-
|
|
49
|
-
if (!exists(configPath)) {
|
|
50
|
-
return { ...CONFIG_DEFAULTS }
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
let raw
|
|
54
|
-
try {
|
|
55
|
-
raw = JSON.parse(readFile(configPath, 'utf8'))
|
|
56
|
-
} catch {
|
|
57
|
-
return { ...CONFIG_DEFAULTS }
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
...CONFIG_DEFAULTS,
|
|
62
|
-
...raw,
|
|
63
|
-
model_map: {
|
|
64
|
-
...CONFIG_DEFAULTS.model_map,
|
|
65
|
-
...(raw.model_map ?? {})
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Повертає абсолютний шлях до tasks_dir.
|
|
72
|
-
* @param {typeof CONFIG_DEFAULTS} config конфігурація
|
|
73
|
-
* @param {string} root корінь репо
|
|
74
|
-
* @returns {string} абсолютний шлях
|
|
75
|
-
*/
|
|
76
|
-
export function resolveTasksDir(config, root) {
|
|
77
|
-
const d = config.tasks_dir
|
|
78
|
-
return d.startsWith('/') ? d : join(root, d)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Повертає абсолютний шлях до worktrees_dir.
|
|
83
|
-
* @param {typeof CONFIG_DEFAULTS} config конфігурація
|
|
84
|
-
* @param {string} root корінь репо
|
|
85
|
-
* @returns {string} абсолютний шлях
|
|
86
|
-
*/
|
|
87
|
-
export function resolveWorktreesDir(config, root) {
|
|
88
|
-
const d = config.worktrees_dir
|
|
89
|
-
return d.startsWith('/') ? d : join(root, d)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Резолвить модель за model_tier із model_map або повертає claude_model (дефолт).
|
|
94
|
-
* @param {typeof CONFIG_DEFAULTS} config конфігурація
|
|
95
|
-
* @param {string | undefined} modelTier 'MIM' | 'AVG' | 'MAX'
|
|
96
|
-
* @returns {string} model id
|
|
97
|
-
*/
|
|
98
|
-
export function resolveModelByTier(config, modelTier) {
|
|
99
|
-
if (modelTier && config.model_map[modelTier]) {
|
|
100
|
-
return config.model_map[modelTier]
|
|
101
|
-
}
|
|
102
|
-
return config.claude_model
|
|
103
|
-
}
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* YAML front-matter parser/serializer для graph task-файлів.
|
|
3
|
-
*
|
|
4
|
-
* Підтримує:
|
|
5
|
-
* - Прості `key: value` рядки
|
|
6
|
-
* - Вкладені об'єкти (блок з відступами), напр. `executor:` + indented children
|
|
7
|
-
* - Списки: `deps:` / `skills:` із рядками ` - item`
|
|
8
|
-
* - Серіалізацію назад у YAML (для запису front-matter)
|
|
9
|
-
*
|
|
10
|
-
* Чисто — без залежностей, тільки вбудований JS. FS не торкається.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
const FM_BOUNDARY_RE = /^---\r?\n([\s\S]*?)\r?\n---/
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Парсить YAML front-matter з markdown-тексту.
|
|
17
|
-
* Повертає словник (може містити вкладені об'єкти та масиви).
|
|
18
|
-
* @param {string} text вміст файлу
|
|
19
|
-
* @returns {Record<string, unknown>} ключ-значення, або {} якщо front-matter відсутній
|
|
20
|
-
*/
|
|
21
|
-
export function parseFrontMatter(text) {
|
|
22
|
-
const m = text.match(FM_BOUNDARY_RE)
|
|
23
|
-
if (!m) return {}
|
|
24
|
-
return parseYamlBlock(m[1])
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Отримує тіло документа (без front-matter).
|
|
29
|
-
* @param {string} text вміст файлу
|
|
30
|
-
* @returns {string} тіло без front-matter
|
|
31
|
-
*/
|
|
32
|
-
export function getBody(text) {
|
|
33
|
-
const m = text.match(FM_BOUNDARY_RE)
|
|
34
|
-
if (!m) return text
|
|
35
|
-
return text.slice(m[0].length).trimStart()
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Парсить YAML-блок (без --- рядків).
|
|
40
|
-
* @param {string} block YAML-текст
|
|
41
|
-
* @returns {Record<string, unknown>} розпарсений об'єкт
|
|
42
|
-
*/
|
|
43
|
-
function parseYamlBlock(block) {
|
|
44
|
-
const lines = block.split(/\r?\n/)
|
|
45
|
-
const result = {}
|
|
46
|
-
let i = 0
|
|
47
|
-
|
|
48
|
-
while (i < lines.length) {
|
|
49
|
-
const line = lines[i]
|
|
50
|
-
if (!line.trim() || line.trimStart().startsWith('#')) {
|
|
51
|
-
i++
|
|
52
|
-
continue
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const indent = getIndent(line)
|
|
56
|
-
if (indent > 0) {
|
|
57
|
-
// Верхній рівень — пропускаємо "бродячі" дочірні рядки
|
|
58
|
-
i++
|
|
59
|
-
continue
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const colonIdx = line.indexOf(':')
|
|
63
|
-
if (colonIdx === -1) {
|
|
64
|
-
i++
|
|
65
|
-
continue
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const key = line.slice(0, colonIdx).trim()
|
|
69
|
-
const rawVal = line.slice(colonIdx + 1).trim()
|
|
70
|
-
|
|
71
|
-
if (rawVal.length > 0) {
|
|
72
|
-
// Inline значення — scalar
|
|
73
|
-
result[key] = parseScalar(rawVal)
|
|
74
|
-
i++
|
|
75
|
-
} else {
|
|
76
|
-
// Значення відсутнє після ':' — дивимось наступні рядки
|
|
77
|
-
i++
|
|
78
|
-
if (i >= lines.length) {
|
|
79
|
-
result[key] = null
|
|
80
|
-
continue
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const nextLine = lines[i]
|
|
84
|
-
if (!nextLine.trim()) {
|
|
85
|
-
result[key] = null
|
|
86
|
-
continue
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const nextIndent = getIndent(nextLine)
|
|
90
|
-
if (nextIndent === 0) {
|
|
91
|
-
// Без відступу — null
|
|
92
|
-
result[key] = null
|
|
93
|
-
continue
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const nextTrimmed = nextLine.trimStart()
|
|
97
|
-
if (nextTrimmed.startsWith('- ')) {
|
|
98
|
-
// Список
|
|
99
|
-
const arr = []
|
|
100
|
-
while (i < lines.length) {
|
|
101
|
-
const l = lines[i]
|
|
102
|
-
if (!l.trim()) {
|
|
103
|
-
i++
|
|
104
|
-
continue
|
|
105
|
-
}
|
|
106
|
-
const ind = getIndent(l)
|
|
107
|
-
if (ind === 0) break
|
|
108
|
-
const t = l.trimStart()
|
|
109
|
-
if (t.startsWith('- ')) {
|
|
110
|
-
arr.push(parseScalar(t.slice(2).trim()))
|
|
111
|
-
}
|
|
112
|
-
i++
|
|
113
|
-
}
|
|
114
|
-
result[key] = arr
|
|
115
|
-
} else {
|
|
116
|
-
// Вкладений об'єкт
|
|
117
|
-
const childLines = []
|
|
118
|
-
while (i < lines.length) {
|
|
119
|
-
const l = lines[i]
|
|
120
|
-
if (!l.trim()) {
|
|
121
|
-
i++
|
|
122
|
-
continue
|
|
123
|
-
}
|
|
124
|
-
if (getIndent(l) === 0) break
|
|
125
|
-
// Нормалізуємо відступ (видаляємо перший рівень)
|
|
126
|
-
childLines.push(l.slice(nextIndent))
|
|
127
|
-
i++
|
|
128
|
-
}
|
|
129
|
-
result[key] = parseYamlBlock(childLines.join('\n'))
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return result
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Повертає кількість пробілів на початку рядка.
|
|
139
|
-
* @param {string} line рядок
|
|
140
|
-
* @returns {number} кількість пробілів
|
|
141
|
-
*/
|
|
142
|
-
function getIndent(line) {
|
|
143
|
-
let count = 0
|
|
144
|
-
for (const ch of line) {
|
|
145
|
-
if (ch === ' ') count++
|
|
146
|
-
else break
|
|
147
|
-
}
|
|
148
|
-
return count
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Парсить скалярне значення: число, булеве, null, або рядок.
|
|
153
|
-
* @param {string} s рядок-значення
|
|
154
|
-
* @returns {unknown} розпарсене значення
|
|
155
|
-
*/
|
|
156
|
-
function parseScalar(s) {
|
|
157
|
-
if (s === 'true') return true
|
|
158
|
-
if (s === 'false') return false
|
|
159
|
-
if (s === 'null' || s === '~') return null
|
|
160
|
-
const n = Number(s)
|
|
161
|
-
if (!Number.isNaN(n) && s.trim().length > 0) return n
|
|
162
|
-
// Знімаємо лапки
|
|
163
|
-
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
|
164
|
-
return s.slice(1, -1)
|
|
165
|
-
}
|
|
166
|
-
return s
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Серіалізує об'єкт у YAML-рядок (для front-matter).
|
|
171
|
-
* Підтримує прості scalar, масиви та вкладені об'єкти.
|
|
172
|
-
* @param {Record<string, unknown>} obj об'єкт для серіалізації
|
|
173
|
-
* @param {number} [indentLevel=0] рівень відступу
|
|
174
|
-
* @returns {string} YAML-рядок (без --- маркерів)
|
|
175
|
-
*/
|
|
176
|
-
export function serializeYaml(obj, indentLevel = 0) {
|
|
177
|
-
const indent = ' '.repeat(indentLevel)
|
|
178
|
-
const lines = []
|
|
179
|
-
|
|
180
|
-
for (const [key, val] of Object.entries(obj)) {
|
|
181
|
-
if (val === null || val === undefined) {
|
|
182
|
-
lines.push(`${indent}${key}:`)
|
|
183
|
-
} else if (Array.isArray(val)) {
|
|
184
|
-
lines.push(`${indent}${key}:`)
|
|
185
|
-
for (const item of val) {
|
|
186
|
-
lines.push(`${indent} - ${serializeScalar(item)}`)
|
|
187
|
-
}
|
|
188
|
-
} else if (typeof val === 'object') {
|
|
189
|
-
lines.push(`${indent}${key}:`)
|
|
190
|
-
lines.push(serializeYaml(val, indentLevel + 1))
|
|
191
|
-
} else {
|
|
192
|
-
lines.push(`${indent}${key}: ${serializeScalar(val)}`)
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return lines.join('\n')
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Серіалізує скалярне значення у рядок.
|
|
201
|
-
* @param {unknown} val значення
|
|
202
|
-
* @returns {string} рядкове представлення
|
|
203
|
-
*/
|
|
204
|
-
function serializeScalar(val) {
|
|
205
|
-
if (typeof val === 'string') {
|
|
206
|
-
// Додаємо лапки якщо містить спецсимволи
|
|
207
|
-
if (/[:#\[\]{},\n]/.test(val) || val.trim() !== val) {
|
|
208
|
-
return `"${val.replace(/"/g, '\\"')}"`
|
|
209
|
-
}
|
|
210
|
-
return val
|
|
211
|
-
}
|
|
212
|
-
return String(val)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Будує markdown-файл із front-matter і тілом.
|
|
217
|
-
* @param {Record<string, unknown>} fm об'єкт front-matter
|
|
218
|
-
* @param {string} [body=''] тіло документа
|
|
219
|
-
* @returns {string} повний вміст файлу
|
|
220
|
-
*/
|
|
221
|
-
export function buildMarkdown(fm, body = '') {
|
|
222
|
-
const yaml = serializeYaml(fm)
|
|
223
|
-
return ['---', yaml, '---', '', body].join('\n')
|
|
224
|
-
}
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* NNN-нумерація для артефактів вузлів графу (run_NNN.md, fact_NNN.md, тощо).
|
|
3
|
-
*
|
|
4
|
-
* Всі функції — чисті утиліти, файлову систему отримують через ін'єкцію.
|
|
5
|
-
* NNN = рядок з ведучими нулями до 3 цифр: '001', '002', …
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const RUN_FILE_RE = /^run_(\d+)\.md$/
|
|
9
|
-
const PLAN_FILE_RE = /^plan_(\d+)\.md$/
|
|
10
|
-
const FACT_FILE_RE = /^fact_(\d+)\.md$/
|
|
11
|
-
const PENDING_AUDIT_FILE_RE = /^pending-audit_(\d+)\.md$/
|
|
12
|
-
const AUDIT_RESULT_FILE_RE = /^audit-result_(\d+)\.md$/
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Форматує число як NNN рядок (три цифри з ведучими нулями).
|
|
16
|
-
* @param {number} n невід'ємне ціле число
|
|
17
|
-
* @returns {string} '001', '002', …
|
|
18
|
-
*/
|
|
19
|
-
export function padNNN(n) {
|
|
20
|
-
return String(n).padStart(3, '0')
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Знаходить максимальний NNN серед файлів що відповідають regex, або 0 якщо не знайдено.
|
|
25
|
-
* @param {string[]} files список файлів директорії
|
|
26
|
-
* @param {RegExp} re regex з групою захоплення числа
|
|
27
|
-
* @returns {number} максимальний NNN або 0
|
|
28
|
-
*/
|
|
29
|
-
function maxNNN(files, re) {
|
|
30
|
-
let max = 0
|
|
31
|
-
for (const f of files) {
|
|
32
|
-
const m = f.match(re)
|
|
33
|
-
if (m) {
|
|
34
|
-
const n = parseInt(m[1], 10)
|
|
35
|
-
if (n > max) max = n
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return max
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Наступний NNN для run_NNN.md: count(run_*.md) + 1.
|
|
43
|
-
* @param {string} nodeDir абсолютний шлях до директорії вузла
|
|
44
|
-
* @param {(dir: string) => string[]} readdirSync ін'єктована функція readdir
|
|
45
|
-
* @returns {string} наступний NNN рядок
|
|
46
|
-
*/
|
|
47
|
-
export function nextRunNNN(nodeDir, readdirSync) {
|
|
48
|
-
const files = readdirSync(nodeDir)
|
|
49
|
-
let count = 0
|
|
50
|
-
for (const f of files) {
|
|
51
|
-
if (RUN_FILE_RE.test(f)) count++
|
|
52
|
-
}
|
|
53
|
-
return padNNN(count + 1)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Наступний NNN для plan_NNN.md: max(plan_*.md numbers) + 1.
|
|
58
|
-
* @param {string} nodeDir абсолютний шлях до директорії вузла
|
|
59
|
-
* @param {(dir: string) => string[]} readdirSync ін'єктована функція readdir
|
|
60
|
-
* @returns {string} наступний NNN рядок
|
|
61
|
-
*/
|
|
62
|
-
export function nextPlanNNN(nodeDir, readdirSync) {
|
|
63
|
-
const files = readdirSync(nodeDir)
|
|
64
|
-
return padNNN(maxNNN(files, PLAN_FILE_RE) + 1)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Найвищий NNN серед fact_NNN.md, або null якщо немає.
|
|
69
|
-
* @param {string} nodeDir абсолютний шлях до директорії вузла
|
|
70
|
-
* @param {(dir: string) => string[]} readdirSync ін'єктована функція readdir
|
|
71
|
-
* @returns {string | null} NNN рядок або null
|
|
72
|
-
*/
|
|
73
|
-
export function latestFactNNN(nodeDir, readdirSync) {
|
|
74
|
-
const files = readdirSync(nodeDir)
|
|
75
|
-
const m = maxNNN(files, FACT_FILE_RE)
|
|
76
|
-
return m > 0 ? padNNN(m) : null
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Перевіряє чи є pending-audit без відповідного audit-result.
|
|
81
|
-
* @param {string} nodeDir абсолютний шлях до директорії вузла
|
|
82
|
-
* @param {(dir: string) => string[]} readdirSync ін'єктована функція readdir
|
|
83
|
-
* @returns {{ has: boolean, nnn: string | null }} результат
|
|
84
|
-
*/
|
|
85
|
-
export function hasPendingAudit(nodeDir, readdirSync) {
|
|
86
|
-
const files = readdirSync(nodeDir)
|
|
87
|
-
const fileSet = new Set(files)
|
|
88
|
-
|
|
89
|
-
const pendingNNNs = []
|
|
90
|
-
for (const f of files) {
|
|
91
|
-
const m = f.match(PENDING_AUDIT_FILE_RE)
|
|
92
|
-
if (m) pendingNNNs.push(m[1])
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
for (const nnn of pendingNNNs) {
|
|
96
|
-
const resultFile = `audit-result_${nnn}.md`
|
|
97
|
-
if (!fileSet.has(resultFile)) {
|
|
98
|
-
return { has: true, nnn: padNNN(parseInt(nnn, 10)) }
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return { has: false, nnn: null }
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Знаходить NNN для останнього pending-audit_NNN.md (для audit-result).
|
|
107
|
-
* @param {string} nodeDir абсолютний шлях до директорії вузла
|
|
108
|
-
* @param {(dir: string) => string[]} readdirSync ін'єктована функція readdir
|
|
109
|
-
* @returns {string | null} NNN рядок або null
|
|
110
|
-
*/
|
|
111
|
-
export function latestPendingAuditNNN(nodeDir, readdirSync) {
|
|
112
|
-
const files = readdirSync(nodeDir)
|
|
113
|
-
const m = maxNNN(files, PENDING_AUDIT_FILE_RE)
|
|
114
|
-
return m > 0 ? padNNN(m) : null
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Знаходить NNN для останнього audit-result_NNN.md.
|
|
119
|
-
* @param {string} nodeDir абсолютний шлях до директорії вузла
|
|
120
|
-
* @param {(dir: string) => string[]} readdirSync ін'єктована функція readdir
|
|
121
|
-
* @returns {string | null} NNN рядок або null
|
|
122
|
-
*/
|
|
123
|
-
export function latestAuditResultNNN(nodeDir, readdirSync) {
|
|
124
|
-
const files = readdirSync(nodeDir)
|
|
125
|
-
const m = maxNNN(files, AUDIT_RESULT_FILE_RE)
|
|
126
|
-
return m > 0 ? padNNN(m) : null
|
|
127
|
-
}
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Деривація стану вузла з файлової системи (immutable file-presence protocol).
|
|
3
|
-
*
|
|
4
|
-
* Стан визначається виключно наявністю файлів у tasks/<node>/:
|
|
5
|
-
* invalidated > resolved > pending-audit > running > failed > waiting > needs-plan
|
|
6
|
-
*
|
|
7
|
-
* Чиста функція — FS ін'єктується. Не пише нічого на диск.
|
|
8
|
-
*/
|
|
9
|
-
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
10
|
-
import { join } from 'node:path'
|
|
11
|
-
|
|
12
|
-
import { hasPendingAudit, latestFactNNN } from './nnn.mjs'
|
|
13
|
-
|
|
14
|
-
/** Всі можливі стани вузла. */
|
|
15
|
-
export const NODE_STATES = /** @type {const} */ ([
|
|
16
|
-
'needs-plan',
|
|
17
|
-
'waiting',
|
|
18
|
-
'running',
|
|
19
|
-
'pending-audit',
|
|
20
|
-
'resolved',
|
|
21
|
-
'failed',
|
|
22
|
-
'invalidated'
|
|
23
|
-
])
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Перевіряє чи директорія є composite-вузлом (містить дочірні директорії з task.md).
|
|
27
|
-
* @param {string} nodeDir абсолютний шлях до директорії вузла
|
|
28
|
-
* @param {{ readdirSync?: (d: string) => string[], existsSync?: (p: string) => boolean }} [deps] ін'єкції
|
|
29
|
-
* @returns {boolean} true якщо є хоча б один дочірній вузол
|
|
30
|
-
*/
|
|
31
|
-
export function isComposite(nodeDir, deps = {}) {
|
|
32
|
-
const readdir = deps.readdirSync ?? readdirSync
|
|
33
|
-
const exists = deps.existsSync ?? existsSync
|
|
34
|
-
|
|
35
|
-
let entries
|
|
36
|
-
try {
|
|
37
|
-
entries = readdir(nodeDir)
|
|
38
|
-
} catch {
|
|
39
|
-
return false
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return entries.some(name => {
|
|
43
|
-
const childTask = join(nodeDir, name, 'task.md')
|
|
44
|
-
return exists(childTask)
|
|
45
|
-
})
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Деривує composite-стан з масиву станів дочірніх вузлів.
|
|
50
|
-
* @param {string[]} childStates масив станів дочірніх вузлів
|
|
51
|
-
* @returns {string} агрегований стан
|
|
52
|
-
*/
|
|
53
|
-
export function deriveCompositeState(childStates) {
|
|
54
|
-
if (childStates.length === 0) return 'waiting'
|
|
55
|
-
if (childStates.some(s => s === 'invalidated')) return 'invalidated'
|
|
56
|
-
if (childStates.some(s => s === 'failed')) return 'failed'
|
|
57
|
-
if (childStates.some(s => s === 'running')) return 'running'
|
|
58
|
-
if (childStates.some(s => s === 'pending-audit')) return 'pending-audit'
|
|
59
|
-
if (childStates.every(s => s === 'resolved')) return 'resolved'
|
|
60
|
-
return 'waiting'
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Деривує стан одного вузла з присутності файлів.
|
|
65
|
-
*
|
|
66
|
-
* Пріоритет: invalidated > resolved > pending-audit > running > failed > waiting > needs-plan
|
|
67
|
-
*
|
|
68
|
-
* @param {string} nodeDir абсолютний шлях до директорії вузла
|
|
69
|
-
* @param {Set<string>} activeWorktrees set імен активних worktree (наприклад, 'my-node-1234567890')
|
|
70
|
-
* @param {{
|
|
71
|
-
* readdirSync?: (d: string) => string[],
|
|
72
|
-
* readFileSync?: (p: string, enc: string) => string,
|
|
73
|
-
* existsSync?: (p: string) => boolean
|
|
74
|
-
* }} [deps] ін'єкції
|
|
75
|
-
* @returns {string} стан вузла
|
|
76
|
-
*/
|
|
77
|
-
export function deriveNodeState(nodeDir, activeWorktrees, deps = {}) {
|
|
78
|
-
const readdir = deps.readdirSync ?? readdirSync
|
|
79
|
-
const readFile = deps.readFileSync ?? ((p, enc) => readFileSync(p, enc))
|
|
80
|
-
const exists = deps.existsSync ?? existsSync
|
|
81
|
-
|
|
82
|
-
// Файл task.md обов'язковий
|
|
83
|
-
if (!exists(join(nodeDir, 'task.md'))) {
|
|
84
|
-
return 'needs-plan'
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
let files
|
|
88
|
-
try {
|
|
89
|
-
files = readdir(nodeDir)
|
|
90
|
-
} catch {
|
|
91
|
-
return 'needs-plan'
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const fileSet = new Set(files)
|
|
95
|
-
|
|
96
|
-
// 1. invalidated — sentinel файл
|
|
97
|
-
if (fileSet.has('invalidated')) return 'invalidated'
|
|
98
|
-
|
|
99
|
-
// 2. resolved — є fact_NNN.md і немає invalidated
|
|
100
|
-
const factNNN = latestFactNNN(nodeDir, readdir)
|
|
101
|
-
if (factNNN !== null) return 'resolved'
|
|
102
|
-
|
|
103
|
-
// 3. pending-audit — є pending-audit_NNN.md без відповідного audit-result_NNN.md
|
|
104
|
-
const { has: hasPending } = hasPendingAudit(nodeDir, readdir)
|
|
105
|
-
if (hasPending) return 'pending-audit'
|
|
106
|
-
|
|
107
|
-
// 4. running — активний worktree існує (перевіряємо за prefix node dir name)
|
|
108
|
-
const nodeName = nodeDir.split('/').filter(Boolean).pop() ?? ''
|
|
109
|
-
if (activeWorktrees.size > 0) {
|
|
110
|
-
for (const wt of activeWorktrees) {
|
|
111
|
-
// worktree name: sanitized-node-path-epoch
|
|
112
|
-
if (wt.includes(sanitizeNodeName(nodeName))) return 'running'
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// 5. failed — є run_NNN.md з result:failed, без fact_NNN.md і без активного worktree
|
|
117
|
-
const runFiles = files.filter(f => /^run_\d+\.md$/.test(f))
|
|
118
|
-
if (runFiles.length > 0) {
|
|
119
|
-
// Перевіряємо останній run файл
|
|
120
|
-
let hasFailedRun = false
|
|
121
|
-
for (const runFile of runFiles) {
|
|
122
|
-
try {
|
|
123
|
-
const content = readFile(join(nodeDir, runFile), 'utf8')
|
|
124
|
-
if (content.includes('result: failed') || content.includes('result:failed')) {
|
|
125
|
-
hasFailedRun = true
|
|
126
|
-
}
|
|
127
|
-
} catch {
|
|
128
|
-
// пропускаємо нечитабельні файли
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
if (hasFailedRun) return 'failed'
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// 6. waiting — є plan_NNN.md АБО mode:agent
|
|
135
|
-
const hasPlan = files.some(f => /^plan_\d+\.md$/.test(f))
|
|
136
|
-
if (hasPlan) return 'waiting'
|
|
137
|
-
|
|
138
|
-
// Читаємо mode з task.md
|
|
139
|
-
try {
|
|
140
|
-
const taskContent = readFile(join(nodeDir, 'task.md'), 'utf8')
|
|
141
|
-
if (taskContent.includes('mode: agent')) return 'waiting'
|
|
142
|
-
} catch {
|
|
143
|
-
// пропускаємо
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// 7. needs-plan — task.md є, mode:human (default), немає plan_NNN.md
|
|
147
|
-
return 'needs-plan'
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Санітизує ім'я вузла для використання в назві worktree.
|
|
152
|
-
* @param {string} name ім'я вузла (може містити /)
|
|
153
|
-
* @returns {string} санітизоване ім'я
|
|
154
|
-
*/
|
|
155
|
-
export function sanitizeNodeName(name) {
|
|
156
|
-
return name.replace(/[^a-zA-Z0-9_-]/g, '-')
|
|
157
|
-
}
|