@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.
- package/CHANGELOG.md +22 -0
- package/bin/docs/n-cursor.md +1 -9
- package/bin/n-cursor.js +3 -25
- package/package.json +1 -1
- package/rules/docker/lib/docs/docker-mirror.md +1 -1
- package/rules/docker/lib/docs/docker-native-addon.md +1 -1
- package/rules/npm-module/npm-module.mdc +1 -1
- package/rules/npm-module/policy/npm_publish_yml/template/npm-publish.yml.snippet.yml +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/docs/flow.MD +0 -1364
- 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,142 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `n-cursor graph plan [<path>] [--mode agent]` — Stage 1: пише plan_NNN.md.
|
|
3
|
-
*
|
|
4
|
-
* Читає task.md вузла, знаходить наступний NNN, пише шаблон plan_NNN.md.
|
|
5
|
-
* Якщо --mode agent — встановлює mode:agent у plan front-matter.
|
|
6
|
-
*
|
|
7
|
-
* FS ін'єктується для тестованості.
|
|
8
|
-
*/
|
|
9
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
10
|
-
import { join } from 'node:path'
|
|
11
|
-
import { cwd as processCwd } from 'node:process'
|
|
12
|
-
|
|
13
|
-
import { buildMarkdown, parseFrontMatter } from './frontmatter.mjs'
|
|
14
|
-
import { nextPlanNNN } from './nnn.mjs'
|
|
15
|
-
import { loadConfig, resolveTasksDir } from './config.mjs'
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Будує шаблон plan_NNN.md.
|
|
19
|
-
* @param {{ mode: string, hint: string, now: string, nnn: string }} params параметри
|
|
20
|
-
* @returns {string} вміст файлу
|
|
21
|
-
*/
|
|
22
|
-
export function buildPlanTemplate(params) {
|
|
23
|
-
const fm = {
|
|
24
|
-
created_at: params.now,
|
|
25
|
-
mode: params.mode,
|
|
26
|
-
decision: params.hint || 'atomic'
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const body = [
|
|
30
|
-
`## Context`,
|
|
31
|
-
`<!-- Чому саме такий підхід — що з'ясовано під час планування -->`,
|
|
32
|
-
``,
|
|
33
|
-
`## Approach`,
|
|
34
|
-
params.mode === 'composite'
|
|
35
|
-
? `<!-- composite: список дочірніх вузлів з описами -->`
|
|
36
|
-
: `<!-- atomic: покроковий план виконання -->`,
|
|
37
|
-
``,
|
|
38
|
-
`## Risks`,
|
|
39
|
-
`<!-- Що може піти не так -->`,
|
|
40
|
-
``
|
|
41
|
-
].join('\n')
|
|
42
|
-
|
|
43
|
-
return buildMarkdown(fm, body)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* `graph plan [<path>] [--mode agent]` command handler.
|
|
48
|
-
* @param {string[]} args аргументи: [path] [--mode agent|human]
|
|
49
|
-
* @param {{
|
|
50
|
-
* cwd?: string,
|
|
51
|
-
* log?: (m: string) => void,
|
|
52
|
-
* writeFile?: (p: string, c: string, enc: string) => void,
|
|
53
|
-
* readFile?: (p: string, enc: string) => string,
|
|
54
|
-
* readdir?: (d: string) => string[],
|
|
55
|
-
* exists?: (p: string) => boolean,
|
|
56
|
-
* now?: () => string
|
|
57
|
-
* }} [deps] ін'єкції
|
|
58
|
-
* @returns {Promise<number>} exit code
|
|
59
|
-
*/
|
|
60
|
-
export async function cmdPlan(args, deps = {}) {
|
|
61
|
-
const root = deps.cwd ?? processCwd()
|
|
62
|
-
const log = deps.log ?? console.log
|
|
63
|
-
const writeFile = deps.writeFile ?? ((p, c, enc) => writeFileSync(p, c, enc))
|
|
64
|
-
const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
|
|
65
|
-
const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
|
|
66
|
-
const exists = deps.exists ?? existsSync
|
|
67
|
-
const nowFn = deps.now ?? (() => new Date().toISOString())
|
|
68
|
-
|
|
69
|
-
// Парсимо аргументи
|
|
70
|
-
let nodePath = null
|
|
71
|
-
let modeOverride = null
|
|
72
|
-
|
|
73
|
-
for (let i = 0; i < args.length; i++) {
|
|
74
|
-
if (args[i] === '--mode' && args[i + 1]) {
|
|
75
|
-
modeOverride = args[i + 1]
|
|
76
|
-
i++
|
|
77
|
-
} else if (!args[i].startsWith('-')) {
|
|
78
|
-
nodePath = args[i]
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const config = loadConfig({ root, readFile, exists })
|
|
83
|
-
const tasksDir = resolveTasksDir(config, root)
|
|
84
|
-
|
|
85
|
-
// Визначаємо директорію вузла
|
|
86
|
-
let nodeDir
|
|
87
|
-
if (nodePath) {
|
|
88
|
-
nodeDir = join(tasksDir, nodePath)
|
|
89
|
-
} else {
|
|
90
|
-
// CWD може бути в worktree — шукаємо task.md у CWD
|
|
91
|
-
nodeDir = processCwd()
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const taskPath = join(nodeDir, 'task.md')
|
|
95
|
-
if (!exists(taskPath)) {
|
|
96
|
-
log(`plan: task.md не знайдено в ${nodeDir}`)
|
|
97
|
-
return 1
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
let taskContent
|
|
101
|
-
try {
|
|
102
|
-
taskContent = readFile(taskPath, 'utf8')
|
|
103
|
-
} catch (err) {
|
|
104
|
-
log(`plan: не вдалося прочитати task.md — ${err.message ?? String(err)}`)
|
|
105
|
-
return 1
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const fm = parseFrontMatter(taskContent)
|
|
109
|
-
const mode = modeOverride ?? (typeof fm.mode === 'string' ? fm.mode : 'human')
|
|
110
|
-
const hint = typeof fm.hint === 'string' ? fm.hint : ''
|
|
111
|
-
|
|
112
|
-
const nnn = nextPlanNNN(nodeDir, readdir)
|
|
113
|
-
const planPath = join(nodeDir, `plan_${nnn}.md`)
|
|
114
|
-
|
|
115
|
-
const content = buildPlanTemplate({ mode, hint, now: nowFn(), nnn })
|
|
116
|
-
|
|
117
|
-
try {
|
|
118
|
-
writeFile(planPath, content, 'utf8')
|
|
119
|
-
log(`plan: створено ${planPath} (mode: ${mode})`)
|
|
120
|
-
} catch (err) {
|
|
121
|
-
log(`plan: не вдалося записати ${planPath} — ${err.message ?? String(err)}`)
|
|
122
|
-
return 1
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Виводимо контекст для агента/людини
|
|
126
|
-
const bodyStart = taskContent.indexOf('\n---\n', 4)
|
|
127
|
-
const taskBody = bodyStart === -1 ? taskContent : taskContent.slice(bodyStart + 5).trimStart()
|
|
128
|
-
|
|
129
|
-
console.log([
|
|
130
|
-
`## plan context`,
|
|
131
|
-
``,
|
|
132
|
-
`node: ${nodePath ?? nodeDir}`,
|
|
133
|
-
`mode: ${mode}`,
|
|
134
|
-
hint ? `hint: ${hint}` : `hint: (не задано)`,
|
|
135
|
-
`plan: plan_${nnn}.md`,
|
|
136
|
-
``,
|
|
137
|
-
`### task.md`,
|
|
138
|
-
taskBody.trimEnd()
|
|
139
|
-
].join('\n'))
|
|
140
|
-
|
|
141
|
-
return 0
|
|
142
|
-
}
|
|
@@ -1,328 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `n-cursor graph run [<path>] [--actor a] [--auto]` — запуск вузла(ів).
|
|
3
|
-
*
|
|
4
|
-
* Wrapper логіка:
|
|
5
|
-
* 1. Читає task.md → budget_sec, budget_hard_sec, deps, mode, executor
|
|
6
|
-
* 2. Перевіряє що всі deps resolved
|
|
7
|
-
* 3. Обчислює NNN = count(run_*.md) + 1
|
|
8
|
-
* 4. git worktree add .worktrees/<node-epoch>/ (atomic mkdir lock — EEXIST = skip)
|
|
9
|
-
* 5. ENV: NCURSOR_RUN_NNN, NCURSOR_BUDGET_SEC, NCURSOR_HARD_BUDGET_SEC, NCURSOR_STARTED_AT, NCURSOR_NODE_PATH
|
|
10
|
-
* 6. Спавнить subprocess (claude або n-cursor graph run --actor auditor)
|
|
11
|
-
* 7. Poll worktree mtime кожні 5s: progress_timeout → SIGKILL; budget_hard → SIGKILL
|
|
12
|
-
* 8. Після exit: fact_NNN.md є → result:success; else → result:failed
|
|
13
|
-
* 9. Пише run_NNN.md
|
|
14
|
-
* 10. Якщо success: git merge + delete worktree
|
|
15
|
-
*
|
|
16
|
-
* --auto режим: сканує для готових вузлів (waiting + deps resolved), клеймить atomic mkdir.
|
|
17
|
-
*
|
|
18
|
-
* FS і child_process ін'єктуються для тестованості.
|
|
19
|
-
*/
|
|
20
|
-
import { execSync, spawnSync } from 'node:child_process'
|
|
21
|
-
import { existsSync, mkdtempSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
22
|
-
import { join } from 'node:path'
|
|
23
|
-
import { cwd as processCwd } from 'node:process'
|
|
24
|
-
|
|
25
|
-
import { buildMarkdown, parseFrontMatter } from './frontmatter.mjs'
|
|
26
|
-
import { latestFactNNN, nextRunNNN } from './nnn.mjs'
|
|
27
|
-
import { loadConfig, resolveModelByTier, resolveTasksDir, resolveWorktreesDir } from './config.mjs'
|
|
28
|
-
import { scanNodes, topoSort, areDepsResolved } from './scanner.mjs'
|
|
29
|
-
import { createWorktree, listActiveWorktrees, mergeWorktree } from './worktree-ops.mjs'
|
|
30
|
-
import { makeWorktreeName } from './worktree-ops.mjs'
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Пише run_NNN.md артефакт.
|
|
34
|
-
* @param {string} nodeDir директорія вузла
|
|
35
|
-
* @param {string} nnn NNN рядок
|
|
36
|
-
* @param {'success'|'failed'} result результат
|
|
37
|
-
* @param {{ actor: string, startedAt: string, now: string }} meta метадані
|
|
38
|
-
* @param {(p: string, c: string, enc: string) => void} writeFile функція запису
|
|
39
|
-
*/
|
|
40
|
-
function writeRunFile(nodeDir, nnn, result, meta, writeFile) {
|
|
41
|
-
const fm = {
|
|
42
|
-
created_at: meta.now,
|
|
43
|
-
started_at: meta.startedAt,
|
|
44
|
-
actor: meta.actor,
|
|
45
|
-
result
|
|
46
|
-
}
|
|
47
|
-
const content = buildMarkdown(fm, `## Run ${nnn}\n\nactor: ${meta.actor}\nresult: ${result}\n`)
|
|
48
|
-
writeFile(join(nodeDir, `run_${nnn}.md`), content, 'utf8')
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Запускає один вузол: creates worktree, spawns agent, writes run_NNN.md.
|
|
53
|
-
* @param {string} nodePath відносний шлях вузла
|
|
54
|
-
* @param {string} nodeDir абсолютний шлях до директорії вузла
|
|
55
|
-
* @param {object} config конфігурація
|
|
56
|
-
* @param {string} root корінь репо
|
|
57
|
-
* @param {{ actor?: string, dryRun?: boolean }} opts опції
|
|
58
|
-
* @param {object} deps ін'єкції
|
|
59
|
-
* @returns {{ ok: boolean, code: number }} результат
|
|
60
|
-
*/
|
|
61
|
-
function runNode(nodePath, nodeDir, config, root, opts, deps) {
|
|
62
|
-
const log = deps.log ?? console.log
|
|
63
|
-
const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
|
|
64
|
-
const writeFile = deps.writeFile ?? ((p, c, enc) => writeFileSync(p, c, enc))
|
|
65
|
-
const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
|
|
66
|
-
const exists = deps.exists ?? existsSync
|
|
67
|
-
const execSyncFn = deps.execSync ?? ((cmd, o) => execSync(cmd, { ...o, encoding: 'utf8' }))
|
|
68
|
-
const spawnSyncFn = deps.spawnSync ?? spawnSync
|
|
69
|
-
const nowFn = deps.now ?? (() => new Date().toISOString())
|
|
70
|
-
const statFn = deps.statSync ?? statSync
|
|
71
|
-
|
|
72
|
-
// 1. Читаємо task.md
|
|
73
|
-
let fm = {}
|
|
74
|
-
try {
|
|
75
|
-
fm = parseFrontMatter(readFile(join(nodeDir, 'task.md'), 'utf8'))
|
|
76
|
-
} catch (err) {
|
|
77
|
-
log(`run: не вдалося прочитати task.md для "${nodePath}" — ${err.message ?? String(err)}`)
|
|
78
|
-
return { ok: false, code: 1 }
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const budgetSec = Number(fm.budget_sec) || config.default_budget_sec
|
|
82
|
-
const budgetHardSec = Number(fm.budget_hard_sec) || (budgetSec * config.budget_hard_sec_multiplier)
|
|
83
|
-
const progressTimeoutSec = config.progress_timeout_sec
|
|
84
|
-
|
|
85
|
-
const executor = (fm.executor && typeof fm.executor === 'object') ? fm.executor : {}
|
|
86
|
-
const executorType = executor.type ?? 'agent'
|
|
87
|
-
const modelTier = executor.model_tier ?? 'AVG'
|
|
88
|
-
const model = resolveModelByTier(config, modelTier)
|
|
89
|
-
|
|
90
|
-
const actor = opts.actor ?? executorType
|
|
91
|
-
|
|
92
|
-
// 2. Вже перевірено deps resolved перед викликом
|
|
93
|
-
|
|
94
|
-
// 3. Обчислюємо NNN
|
|
95
|
-
const nnn = nextRunNNN(nodeDir, readdir)
|
|
96
|
-
|
|
97
|
-
// 4. Створюємо worktree (atomic mkdir lock)
|
|
98
|
-
const worktreesDir = resolveWorktreesDir(config, root)
|
|
99
|
-
const worktreeName = makeWorktreeName(nodePath)
|
|
100
|
-
const worktreePath = join(worktreesDir, worktreeName)
|
|
101
|
-
|
|
102
|
-
log(`run: запускаємо вузол "${nodePath}" (NNN=${nnn}, actor=${actor})`)
|
|
103
|
-
|
|
104
|
-
if (opts.dryRun) {
|
|
105
|
-
log(`run: --dry-run — пропускаємо фактичний запуск`)
|
|
106
|
-
return { ok: true, code: 0 }
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
let createResult
|
|
110
|
-
try {
|
|
111
|
-
createResult = createWorktree(worktreesDir, worktreeName, root, { execSync: execSyncFn })
|
|
112
|
-
} catch (err) {
|
|
113
|
-
log(`run: не вдалося створити worktree — ${err.message ?? String(err)}`)
|
|
114
|
-
return { ok: false, code: 1 }
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (!createResult) {
|
|
118
|
-
log(`run: вузол "${nodePath}" вже запущено (worktree існує) — пропускаємо`)
|
|
119
|
-
return { ok: false, code: 2 }
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// 5. ENV
|
|
123
|
-
const startedAt = nowFn()
|
|
124
|
-
const env = {
|
|
125
|
-
...process.env,
|
|
126
|
-
NCURSOR_RUN_NNN: nnn,
|
|
127
|
-
NCURSOR_BUDGET_SEC: String(budgetSec),
|
|
128
|
-
NCURSOR_HARD_BUDGET_SEC: String(budgetHardSec),
|
|
129
|
-
NCURSOR_STARTED_AT: startedAt,
|
|
130
|
-
NCURSOR_NODE_PATH: nodePath
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// 6. Спавнимо subprocess (spawnSync — синхронно)
|
|
134
|
-
let spawnResult
|
|
135
|
-
const timeoutMs = budgetHardSec > 0 ? budgetHardSec * 1000 : undefined
|
|
136
|
-
|
|
137
|
-
if (actor === 'agent' || actor === 'a') {
|
|
138
|
-
// Запускаємо claude CLI у worktree
|
|
139
|
-
const claudeArgs = ['--model', model, '--no-session', '-p',
|
|
140
|
-
`You are executing task node: ${nodePath}\nWorking directory: ${worktreePath}\nRun NNN: ${nnn}\nBudget: ${budgetSec}s\n\nRead task.md and plan_*.md, execute the task, write fact_${nnn}.md with results.`
|
|
141
|
-
]
|
|
142
|
-
spawnResult = spawnSyncFn('claude', claudeArgs, {
|
|
143
|
-
cwd: worktreePath,
|
|
144
|
-
env,
|
|
145
|
-
encoding: 'utf8',
|
|
146
|
-
timeout: timeoutMs
|
|
147
|
-
})
|
|
148
|
-
} else if (actor === 'human') {
|
|
149
|
-
// Людина виконує вручну — чекаємо на fact файл
|
|
150
|
-
log(`run: вузол "${nodePath}" очікує ручного виконання`)
|
|
151
|
-
log(` worktree: ${worktreePath}`)
|
|
152
|
-
log(` NCURSOR_RUN_NNN=${nnn}`)
|
|
153
|
-
log(` після виконання запустіть: n-cursor graph done ${nodePath}`)
|
|
154
|
-
// Не чекаємо — повертаємо success без run_NNN.md
|
|
155
|
-
return { ok: true, code: 0 }
|
|
156
|
-
} else {
|
|
157
|
-
log(`run: невідомий actor "${actor}" — підтримується: agent, human`)
|
|
158
|
-
return { ok: false, code: 1 }
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// 8. Після exit: перевіряємо fact_NNN.md
|
|
162
|
-
const factPath = join(worktreePath, `fact_${nnn}.md`)
|
|
163
|
-
const factInNodeDir = join(nodeDir, `fact_${nnn}.md`)
|
|
164
|
-
|
|
165
|
-
// Перевіряємо у worktree та у основній директорії
|
|
166
|
-
const hasFactInWorktree = exists(factPath)
|
|
167
|
-
const result = hasFactInWorktree ? 'success' : 'failed'
|
|
168
|
-
|
|
169
|
-
// Якщо є факт у worktree — копіюємо в node dir (якщо worktree != nodeDir)
|
|
170
|
-
if (hasFactInWorktree && worktreePath !== nodeDir) {
|
|
171
|
-
try {
|
|
172
|
-
const factContent = readFile(factPath, 'utf8')
|
|
173
|
-
writeFile(factInNodeDir, factContent, 'utf8')
|
|
174
|
-
} catch {
|
|
175
|
-
// пропускаємо
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// 9. Пишемо run_NNN.md у node dir
|
|
180
|
-
try {
|
|
181
|
-
writeRunFile(nodeDir, nnn, result, {
|
|
182
|
-
actor,
|
|
183
|
-
startedAt,
|
|
184
|
-
now: nowFn()
|
|
185
|
-
}, writeFile)
|
|
186
|
-
log(`run: записано run_${nnn}.md (result: ${result})`)
|
|
187
|
-
} catch (err) {
|
|
188
|
-
log(`run: не вдалося записати run_${nnn}.md — ${err.message ?? String(err)}`)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// 10. Якщо success: merge worktree
|
|
192
|
-
if (result === 'success') {
|
|
193
|
-
const mergeResult = mergeWorktree(worktreePath, root, { execSync: execSyncFn })
|
|
194
|
-
if (!mergeResult.ok) {
|
|
195
|
-
log(`run: merge worktree не вдався — ${mergeResult.error}`)
|
|
196
|
-
} else {
|
|
197
|
-
log(`run: worktree merged і видалено`)
|
|
198
|
-
}
|
|
199
|
-
return { ok: true, code: 0 }
|
|
200
|
-
} else {
|
|
201
|
-
log(`run: вузол "${nodePath}" завершився з помилкою`)
|
|
202
|
-
log(`run: worktree збережено для діагностики: ${worktreePath}`)
|
|
203
|
-
return { ok: false, code: 1 }
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* `graph run [<path>] [--actor a] [--auto]` command handler.
|
|
209
|
-
* @param {string[]} args аргументи
|
|
210
|
-
* @param {{
|
|
211
|
-
* cwd?: string,
|
|
212
|
-
* log?: (m: string) => void,
|
|
213
|
-
* readFile?: (p: string, enc: string) => string,
|
|
214
|
-
* writeFile?: (p: string, c: string, enc: string) => void,
|
|
215
|
-
* readdir?: (d: string) => string[],
|
|
216
|
-
* exists?: (p: string) => boolean,
|
|
217
|
-
* execSync?: (cmd: string, opts?: object) => string,
|
|
218
|
-
* spawnSync?: (cmd: string, args: string[], opts?: object) => object,
|
|
219
|
-
* statSync?: (p: string) => object,
|
|
220
|
-
* now?: () => string
|
|
221
|
-
* }} [deps] ін'єкції
|
|
222
|
-
* @returns {Promise<number>} exit code
|
|
223
|
-
*/
|
|
224
|
-
export async function cmdRun(args, deps = {}) {
|
|
225
|
-
const root = deps.cwd ?? processCwd()
|
|
226
|
-
const log = deps.log ?? console.log
|
|
227
|
-
const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
|
|
228
|
-
const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
|
|
229
|
-
const exists = deps.exists ?? existsSync
|
|
230
|
-
const execSyncFn = deps.execSync ?? ((cmd, o) => execSync(cmd, { ...o, encoding: 'utf8' }))
|
|
231
|
-
|
|
232
|
-
// Парсимо аргументи
|
|
233
|
-
let nodePath = null
|
|
234
|
-
let actor = null
|
|
235
|
-
let autoMode = false
|
|
236
|
-
|
|
237
|
-
for (let i = 0; i < args.length; i++) {
|
|
238
|
-
if (args[i] === '--actor' && args[i + 1]) {
|
|
239
|
-
actor = args[i + 1]
|
|
240
|
-
i++
|
|
241
|
-
} else if (args[i] === '--auto') {
|
|
242
|
-
autoMode = true
|
|
243
|
-
} else if (!args[i].startsWith('-')) {
|
|
244
|
-
nodePath = args[i]
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const config = loadConfig({ root, readFile, exists })
|
|
249
|
-
const tasksDir = resolveTasksDir(config, root)
|
|
250
|
-
|
|
251
|
-
const activeWorktrees = listActiveWorktrees(root, { execSync: execSyncFn })
|
|
252
|
-
|
|
253
|
-
// Перевіряємо ліміт worktrees
|
|
254
|
-
if (activeWorktrees.size >= config.max_worktrees) {
|
|
255
|
-
log(`run: досягнуто max_worktrees (${config.max_worktrees}) — зачекайте завершення поточних задач`)
|
|
256
|
-
return 1
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (activeWorktrees.size >= config.warn_worktrees_above) {
|
|
260
|
-
log(`run: увага — ${activeWorktrees.size} активних worktrees (попередження при >${config.warn_worktrees_above})`)
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
if (autoMode) {
|
|
264
|
-
// Знаходимо всі ready вузли і запускаємо їх
|
|
265
|
-
const allNodes = scanNodes(tasksDir, activeWorktrees, {
|
|
266
|
-
readdirSync: readdir,
|
|
267
|
-
existsSync: exists,
|
|
268
|
-
readFileSync: readFile
|
|
269
|
-
})
|
|
270
|
-
const nodeMap = new Map(allNodes.map(n => [n.id, n]))
|
|
271
|
-
const readyNodes = topoSort(allNodes).filter(n => n.state === 'waiting' && areDepsResolved(n, nodeMap))
|
|
272
|
-
|
|
273
|
-
if (readyNodes.length === 0) {
|
|
274
|
-
log('run --auto: немає готових вузлів для запуску')
|
|
275
|
-
return 0
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
log(`run --auto: знайдено ${readyNodes.length} готових вузлів`)
|
|
279
|
-
let anyFailed = false
|
|
280
|
-
|
|
281
|
-
for (const node of readyNodes) {
|
|
282
|
-
const result = runNode(node.path, node.dir, config, root, { actor: actor ?? undefined }, {
|
|
283
|
-
...deps,
|
|
284
|
-
log,
|
|
285
|
-
execSync: execSyncFn
|
|
286
|
-
})
|
|
287
|
-
if (!result.ok && result.code !== 2) anyFailed = true
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
return anyFailed ? 1 : 0
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Запускаємо конкретний вузол або вузол у CWD
|
|
294
|
-
if (!nodePath) {
|
|
295
|
-
log('run: вкажіть <path> або використайте --auto')
|
|
296
|
-
log('Usage: n-cursor graph run [<path>] [--actor agent|human] [--auto]')
|
|
297
|
-
return 1
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const nodeDir = join(tasksDir, nodePath)
|
|
301
|
-
if (!exists(join(nodeDir, 'task.md'))) {
|
|
302
|
-
log(`run: вузол "${nodePath}" не знайдено (немає task.md у ${nodeDir})`)
|
|
303
|
-
return 1
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Перевіряємо deps
|
|
307
|
-
const allNodes = scanNodes(tasksDir, activeWorktrees, {
|
|
308
|
-
readdirSync: readdir,
|
|
309
|
-
existsSync: exists,
|
|
310
|
-
readFileSync: readFile
|
|
311
|
-
})
|
|
312
|
-
const nodeMap = new Map(allNodes.map(n => [n.id, n]))
|
|
313
|
-
const targetNode = nodeMap.get(nodePath)
|
|
314
|
-
|
|
315
|
-
if (targetNode && !areDepsResolved(targetNode, nodeMap)) {
|
|
316
|
-
const unresolvedDeps = targetNode.deps.filter(dep => nodeMap.get(dep)?.state !== 'resolved')
|
|
317
|
-
log(`run: вузол "${nodePath}" має невирішені залежності: ${unresolvedDeps.join(', ')}`)
|
|
318
|
-
return 1
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const result = runNode(nodePath, nodeDir, config, root, { actor: actor ?? undefined }, {
|
|
322
|
-
...deps,
|
|
323
|
-
log,
|
|
324
|
-
execSync: execSyncFn
|
|
325
|
-
})
|
|
326
|
-
|
|
327
|
-
return result.code
|
|
328
|
-
}
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `n-cursor graph scan [--json]` — повний скан DAG, exit 1 якщо є failed-вузли.
|
|
3
|
-
*
|
|
4
|
-
* Обходить весь DAG, деривує стани, виводить зведення.
|
|
5
|
-
* exit 0 = все чисто (або лише needs-plan/waiting)
|
|
6
|
-
* exit 1 = є failed або pending-audit без відповіді
|
|
7
|
-
*
|
|
8
|
-
* FS ін'єктується для тестованості.
|
|
9
|
-
*/
|
|
10
|
-
import { execSync } from 'node:child_process'
|
|
11
|
-
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
12
|
-
import { join } from 'node:path'
|
|
13
|
-
import { cwd as processCwd } from 'node:process'
|
|
14
|
-
|
|
15
|
-
import { loadConfig, resolveTasksDir } from './config.mjs'
|
|
16
|
-
import { scanNodes, topoSort, areDepsResolved } from './scanner.mjs'
|
|
17
|
-
import { listActiveWorktrees } from './worktree-ops.mjs'
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* `graph scan [--json]` command handler.
|
|
21
|
-
* @param {string[]} args аргументи
|
|
22
|
-
* @param {{
|
|
23
|
-
* cwd?: string,
|
|
24
|
-
* log?: (m: string) => void,
|
|
25
|
-
* readFile?: (p: string, enc: string) => string,
|
|
26
|
-
* readdir?: (d: string) => string[],
|
|
27
|
-
* exists?: (p: string) => boolean,
|
|
28
|
-
* execSync?: (cmd: string, opts?: object) => string
|
|
29
|
-
* }} [deps] ін'єкції
|
|
30
|
-
* @returns {Promise<number>} exit code (0=clean, 1=attention)
|
|
31
|
-
*/
|
|
32
|
-
export async function cmdScan(args, deps = {}) {
|
|
33
|
-
const root = deps.cwd ?? processCwd()
|
|
34
|
-
const log = deps.log ?? console.log
|
|
35
|
-
const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
|
|
36
|
-
const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
|
|
37
|
-
const exists = deps.exists ?? existsSync
|
|
38
|
-
const execSyncFn = deps.execSync ?? ((cmd, opts) => execSync(cmd, { ...opts, encoding: 'utf8' }))
|
|
39
|
-
|
|
40
|
-
const jsonMode = args.includes('--json')
|
|
41
|
-
|
|
42
|
-
const config = loadConfig({ root, readFile, exists })
|
|
43
|
-
const tasksDir = resolveTasksDir(config, root)
|
|
44
|
-
|
|
45
|
-
const activeWorktrees = listActiveWorktrees(root, { execSync: execSyncFn })
|
|
46
|
-
|
|
47
|
-
const allNodes = scanNodes(tasksDir, activeWorktrees, {
|
|
48
|
-
readdirSync: readdir,
|
|
49
|
-
existsSync: exists,
|
|
50
|
-
readFileSync: readFile
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
const sorted = topoSort(allNodes)
|
|
54
|
-
|
|
55
|
-
// Підрахунок по станах
|
|
56
|
-
const stateCounts = {}
|
|
57
|
-
for (const n of sorted) {
|
|
58
|
-
stateCounts[n.state] = (stateCounts[n.state] ?? 0) + 1
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Знаходимо проблемні вузли
|
|
62
|
-
const failed = sorted.filter(n => n.state === 'failed')
|
|
63
|
-
const pendingAudit = sorted.filter(n => n.state === 'pending-audit')
|
|
64
|
-
const needsPlan = sorted.filter(n => n.state === 'needs-plan')
|
|
65
|
-
|
|
66
|
-
// Знаходимо готові до запуску (waiting + deps resolved)
|
|
67
|
-
const nodeMap = new Map(sorted.map(n => [n.id, n]))
|
|
68
|
-
const ready = sorted.filter(n => n.state === 'waiting' && areDepsResolved(n, nodeMap))
|
|
69
|
-
|
|
70
|
-
const hasProblems = failed.length > 0
|
|
71
|
-
|
|
72
|
-
if (jsonMode) {
|
|
73
|
-
console.log(JSON.stringify({
|
|
74
|
-
ok: !hasProblems,
|
|
75
|
-
total: sorted.length,
|
|
76
|
-
counts: stateCounts,
|
|
77
|
-
failed: failed.map(n => n.path),
|
|
78
|
-
pending_audit: pendingAudit.map(n => n.path),
|
|
79
|
-
needs_plan: needsPlan.map(n => n.path),
|
|
80
|
-
ready: ready.map(n => n.path)
|
|
81
|
-
}, null, 2))
|
|
82
|
-
} else {
|
|
83
|
-
const summaryParts = Object.entries(stateCounts)
|
|
84
|
-
.map(([s, c]) => `${s}:${c}`)
|
|
85
|
-
.join(' ')
|
|
86
|
-
|
|
87
|
-
log(`scan: ${sorted.length} вузлів — ${summaryParts}`)
|
|
88
|
-
|
|
89
|
-
if (failed.length > 0) {
|
|
90
|
-
log(`\nFAILED (${failed.length}):`)
|
|
91
|
-
for (const n of failed) log(` - ${n.path}`)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (pendingAudit.length > 0) {
|
|
95
|
-
log(`\npending-audit (${pendingAudit.length}):`)
|
|
96
|
-
for (const n of pendingAudit) log(` - ${n.path}`)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (needsPlan.length > 0) {
|
|
100
|
-
log(`\nneeds-plan (${needsPlan.length}):`)
|
|
101
|
-
for (const n of needsPlan) log(` - ${n.path}`)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (ready.length > 0) {
|
|
105
|
-
log(`\nready to run (${ready.length}):`)
|
|
106
|
-
for (const n of ready) log(` - ${n.path}`)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (!hasProblems && failed.length === 0) {
|
|
110
|
-
log('\nscan: OK')
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return hasProblems ? 1 : 0
|
|
115
|
-
}
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `n-cursor graph setup` — ініціалізація проєкту для graph task system.
|
|
3
|
-
*
|
|
4
|
-
* Створює:
|
|
5
|
-
* - .n-cursor.json з дефолтними налаштуваннями (якщо не існує)
|
|
6
|
-
* - tasks/ директорію
|
|
7
|
-
* - git hook (post-commit) для автоматичного оновлення стану (якщо є .git)
|
|
8
|
-
*
|
|
9
|
-
* FS ін'єктується для тестованості.
|
|
10
|
-
*/
|
|
11
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
12
|
-
import { join } from 'node:path'
|
|
13
|
-
import { cwd as processCwd } from 'node:process'
|
|
14
|
-
|
|
15
|
-
import { CONFIG_DEFAULTS } from './config.mjs'
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* `graph setup` command handler.
|
|
19
|
-
* @param {string[]} _args аргументи (не використовуються)
|
|
20
|
-
* @param {{
|
|
21
|
-
* cwd?: string,
|
|
22
|
-
* log?: (m: string) => void,
|
|
23
|
-
* writeFile?: (p: string, c: string, enc: string) => void,
|
|
24
|
-
* readFile?: (p: string, enc: string) => string,
|
|
25
|
-
* exists?: (p: string) => boolean,
|
|
26
|
-
* mkdir?: (p: string, opts?: object) => void
|
|
27
|
-
* }} [deps] ін'єкції
|
|
28
|
-
* @returns {Promise<number>} exit code
|
|
29
|
-
*/
|
|
30
|
-
export async function cmdSetup(_args, deps = {}) {
|
|
31
|
-
const root = deps.cwd ?? processCwd()
|
|
32
|
-
const log = deps.log ?? console.log
|
|
33
|
-
const writeFile = deps.writeFile ?? ((p, c, enc) => writeFileSync(p, c, enc))
|
|
34
|
-
const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
|
|
35
|
-
const exists = deps.exists ?? existsSync
|
|
36
|
-
const mkdir = deps.mkdir ?? ((p, opts) => mkdirSync(p, opts))
|
|
37
|
-
|
|
38
|
-
// 1. Створюємо .n-cursor.json якщо не існує
|
|
39
|
-
const configPath = join(root, '.n-cursor.json')
|
|
40
|
-
if (!exists(configPath)) {
|
|
41
|
-
try {
|
|
42
|
-
writeFile(configPath, JSON.stringify(CONFIG_DEFAULTS, null, 2) + '\n', 'utf8')
|
|
43
|
-
log(`setup: створено ${configPath}`)
|
|
44
|
-
} catch (err) {
|
|
45
|
-
log(`setup: не вдалося створити ${configPath} — ${err.message ?? String(err)}`)
|
|
46
|
-
return 1
|
|
47
|
-
}
|
|
48
|
-
} else {
|
|
49
|
-
log(`setup: ${configPath} вже існує — пропускаємо`)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// 2. Створюємо tasks/ директорію
|
|
53
|
-
const tasksDir = join(root, 'tasks')
|
|
54
|
-
if (!exists(tasksDir)) {
|
|
55
|
-
try {
|
|
56
|
-
mkdir(tasksDir, { recursive: true })
|
|
57
|
-
log(`setup: створено ${tasksDir}`)
|
|
58
|
-
} catch (err) {
|
|
59
|
-
log(`setup: не вдалося створити ${tasksDir} — ${err.message ?? String(err)}`)
|
|
60
|
-
return 1
|
|
61
|
-
}
|
|
62
|
-
} else {
|
|
63
|
-
log(`setup: ${tasksDir} вже існує — пропускаємо`)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// 3. Створюємо .n-cursor/ директорію
|
|
67
|
-
const ncursorDir = join(root, '.n-cursor')
|
|
68
|
-
if (!exists(ncursorDir)) {
|
|
69
|
-
try {
|
|
70
|
-
mkdir(ncursorDir, { recursive: true })
|
|
71
|
-
log(`setup: створено ${ncursorDir}`)
|
|
72
|
-
} catch (err) {
|
|
73
|
-
log(`setup: не вдалося створити ${ncursorDir} — ${err.message ?? String(err)}`)
|
|
74
|
-
return 1
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// 4. Перевіряємо чи є .git і додаємо hook
|
|
79
|
-
const gitDir = join(root, '.git')
|
|
80
|
-
if (exists(gitDir)) {
|
|
81
|
-
const hooksDir = join(gitDir, 'hooks')
|
|
82
|
-
try {
|
|
83
|
-
mkdir(hooksDir, { recursive: true })
|
|
84
|
-
} catch {
|
|
85
|
-
// hooks/ може вже існувати
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const hookPath = join(hooksDir, 'post-commit')
|
|
89
|
-
if (!exists(hookPath)) {
|
|
90
|
-
const hookContent = [
|
|
91
|
-
'#!/bin/sh',
|
|
92
|
-
'# n-cursor graph: automatic state refresh after commit',
|
|
93
|
-
'npx @nitra/cursor graph scan --json > /dev/null 2>&1 || true',
|
|
94
|
-
''
|
|
95
|
-
].join('\n')
|
|
96
|
-
try {
|
|
97
|
-
writeFile(hookPath, hookContent, 'utf8')
|
|
98
|
-
// chmod +x через окрему команду не робимо — залежить від FS dep
|
|
99
|
-
log(`setup: створено git hook ${hookPath}`)
|
|
100
|
-
} catch (err) {
|
|
101
|
-
log(`setup: не вдалося створити git hook — ${err.message ?? String(err)}`)
|
|
102
|
-
// Не критично — продовжуємо
|
|
103
|
-
}
|
|
104
|
-
} else {
|
|
105
|
-
log(`setup: git hook ${hookPath} вже існує — пропускаємо`)
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
log('setup: готово')
|
|
110
|
-
return 0
|
|
111
|
-
}
|