@nitra/cursor 4.1.0 → 4.1.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 +6 -0
- package/bin/n-cursor.js +25 -13
- package/lib/models.mjs +1 -2
- package/package.json +1 -1
- package/rules/abie/fix.mjs +1 -1
- package/rules/bun/docs/fix.md +3 -0
- package/rules/bun/fix.mjs +1 -1
- package/rules/capacitor/fix.mjs +1 -1
- package/rules/changelog/docs/fix.md +3 -0
- package/rules/changelog/fix.mjs +1 -1
- package/rules/ci4/fix.mjs +1 -1
- package/rules/ci4/js/docs/marksman_config.md +1 -0
- package/rules/docker/docs/fix.md +1 -1
- package/rules/docker/fix.mjs +1 -1
- package/rules/docker/lint/docs/lint.md +1 -0
- package/rules/efes/docs/fix.md +2 -1
- package/rules/efes/fix.mjs +1 -1
- package/rules/feedback/fix.mjs +1 -1
- package/rules/ga/fix.mjs +1 -1
- package/rules/ga/js/lint.mjs +1 -1
- package/rules/graphql/docs/fix.md +4 -1
- package/rules/graphql/fix.mjs +1 -1
- package/rules/graphql/lib/docs/graphql-gql-scan.md +3 -0
- package/rules/hasura/fix.mjs +1 -1
- package/rules/image-avif/docs/fix.md +4 -1
- package/rules/image-avif/fix.mjs +1 -1
- package/rules/image-avif/js/docs/avif_generation.md +1 -0
- package/rules/image-compress/fix.mjs +1 -1
- package/rules/js-bun-db/fix.mjs +1 -1
- package/rules/js-bun-db/lib/docs/bun-sql-scan.md +6 -0
- package/rules/js-bun-redis/fix.mjs +1 -1
- package/rules/js-lint/fix.mjs +1 -1
- package/rules/js-lint/js/docs/utils_imports.md +1 -0
- package/rules/js-lint-ci/docs/fix.md +4 -1
- package/rules/js-lint-ci/fix.mjs +1 -1
- package/rules/js-mssql/docs/fix.md +3 -0
- package/rules/js-mssql/fix.mjs +1 -1
- package/rules/js-mssql/lib/docs/mssql-pool-scan.md +9 -0
- package/rules/js-run/docs/fix.md +3 -0
- package/rules/js-run/fix.mjs +1 -1
- package/rules/js-run/lib/docs/check-env-scan.md +2 -1
- package/rules/js-run/lib/docs/promise-settimeout-scan.md +4 -0
- package/rules/k8s/docs/fix.md +3 -0
- package/rules/k8s/fix.mjs +1 -1
- package/rules/nginx-default-tpl/docs/fix.md +3 -0
- package/rules/nginx-default-tpl/fix.mjs +1 -1
- package/rules/npm-module/fix.mjs +1 -1
- package/rules/npm-module/js/header_doc_pointer.mjs +14 -3
- package/rules/php/docs/fix.md +2 -1
- package/rules/php/fix.mjs +1 -1
- package/rules/python/docs/fix.md +4 -1
- package/rules/python/fix.mjs +1 -1
- package/rules/rego/fix.mjs +1 -1
- package/rules/rego/js/lint.mjs +1 -1
- package/rules/release/docs/fix.md +4 -1
- package/rules/release/fix.mjs +1 -1
- package/rules/rust/fix.mjs +1 -1
- package/rules/security/docs/fix.md +1 -1
- package/rules/security/fix.mjs +1 -1
- package/rules/style-lint/docs/fix.md +3 -0
- package/rules/style-lint/fix.mjs +1 -1
- package/rules/tauri/docs/fix.md +4 -1
- package/rules/tauri/fix.mjs +1 -1
- package/rules/test/docs/fix.md +3 -0
- package/rules/test/fix.mjs +1 -1
- package/rules/test/js/no-relative-fs-path.mjs +2 -1
- package/rules/text/docs/fix.md +3 -0
- package/rules/text/fix.mjs +1 -1
- package/rules/text/js/lint.mjs +1 -1
- package/rules/vue/fix.mjs +1 -1
- package/rules/worktree/fix.mjs +1 -1
- package/scripts/auto-rules.mjs +1 -1
- package/scripts/coverage-classify/index.mjs +10 -10
- package/scripts/coverage-fix.mjs +2 -2
- package/scripts/dispatcher/graph/lib/cmd-init.mjs +112 -0
- package/scripts/dispatcher/graph/lib/cmd-invalidate.mjs +96 -0
- package/scripts/dispatcher/graph/lib/cmd-kill.mjs +141 -0
- package/scripts/dispatcher/graph/lib/cmd-plan.mjs +142 -0
- package/scripts/dispatcher/graph/lib/cmd-run.mjs +328 -0
- package/scripts/dispatcher/graph/lib/cmd-scan.mjs +115 -0
- package/scripts/dispatcher/graph/lib/cmd-setup.mjs +111 -0
- package/scripts/dispatcher/graph/lib/cmd-signals.mjs +328 -0
- package/scripts/dispatcher/graph/lib/cmd-status.mjs +131 -0
- package/scripts/dispatcher/graph/lib/cmd-verify.mjs +100 -0
- package/scripts/dispatcher/graph/lib/cmd-watch.mjs +128 -0
- package/scripts/dispatcher/graph/lib/config.mjs +103 -0
- package/scripts/dispatcher/graph/lib/frontmatter.mjs +224 -0
- package/scripts/dispatcher/graph/lib/nnn.mjs +127 -0
- package/scripts/dispatcher/graph/lib/node-state.mjs +157 -0
- package/scripts/dispatcher/graph/lib/scanner.mjs +235 -0
- package/scripts/dispatcher/graph/lib/worktree-ops.mjs +193 -0
- package/scripts/dispatcher/graph-tasks.mjs +92 -0
- package/scripts/dispatcher/index.mjs +3 -3
- package/scripts/dispatcher/lib/docs/events.md +1 -0
- package/scripts/dispatcher/lib/executor.mjs +1 -1
- package/scripts/dispatcher/lib/subagent-runner.mjs +9 -9
- package/scripts/dispatcher/trace.mjs +6 -2
- package/scripts/docs/build-agents-commands.md +1 -0
- package/scripts/docs/cli-entry.md +6 -0
- package/scripts/graph/index.mjs +115 -0
- package/scripts/graph/lib/config.mjs +62 -0
- package/scripts/graph/lib/dag.mjs +161 -0
- package/scripts/graph/lib/frontmatter.mjs +70 -0
- package/scripts/graph/lib/nnn.mjs +77 -0
- package/scripts/graph/lib/state.mjs +110 -0
- package/scripts/graph/scan.mjs +64 -0
- package/scripts/graph/status.mjs +86 -0
- package/scripts/lib/docs/load-cursor-config.md +3 -0
- package/scripts/lib/root-notice.mjs +4 -2
- package/scripts/lib/rule-predicates.mjs +1 -1
- package/scripts/lib/worktree-notice.mjs +14 -7
- package/scripts/lib/worktree.mjs +3 -2
- package/scripts/utils/resolve-js-root.mjs +2 -1
- package/scripts/utils/with-lock.mjs +1 -1
- package/skills/docgen/js/docgen-batch.mjs +7 -7
- package/skills/docgen/js/docgen-extract.mjs +80 -37
- package/skills/docgen/js/docgen-ignore.mjs +1 -1
- package/skills/docgen/js/docgen-prompts.mjs +21 -5
- package/skills/fix/js/llm-worker.mjs +19 -22
- package/skills/fix/js/orchestrator.mjs +6 -7
- package/skills/fix/js/t0.mjs +14 -13
- package/types/bin/n-cursor.d.ts +1 -1
- package/rules/flow/docs/fix.md +0 -152
- package/rules/flow/fix.mjs +0 -18
- package/rules/flow/flow.mdc +0 -127
- package/rules/flow/meta.json +0 -1
- package/scripts/dispatcher/lib/docs/flow-lock.md +0 -161
- package/scripts/dispatcher/lib/docs/flow-resolve.md +0 -267
- package/scripts/dispatcher/lib/flow-plan.mjs +0 -153
- package/scripts/dispatcher/lib/flow-resolve.mjs +0 -156
- package/scripts/dispatcher/lib/flow-signals.mjs +0 -235
- package/scripts/dispatcher/lib/flow-verify.mjs +0 -127
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handler `flow verify` — Stage 2 structural check (думка.MD § "flow verify").
|
|
3
|
+
*
|
|
4
|
+
* Перевіряє що `fact_NNN.md` існує і непорожній у директорії поточного вузла
|
|
5
|
+
* (CWD). Якщо так — виводить `## Done when` секцію з `task.md` та вміст
|
|
6
|
+
* `fact_NNN.md` на stdout для агентської self-evaluation.
|
|
7
|
+
*
|
|
8
|
+
* exit 0 = структурно OK
|
|
9
|
+
* exit 1 = структурна помилка (fact відсутній або порожній)
|
|
10
|
+
*
|
|
11
|
+
* FS ін'єктується для тестування без диска.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
14
|
+
import { join } from 'node:path'
|
|
15
|
+
import { cwd as processCwd } from 'node:process'
|
|
16
|
+
|
|
17
|
+
import { latestFactNNN } from './nnn.mjs'
|
|
18
|
+
|
|
19
|
+
const FRONT_MATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/
|
|
20
|
+
const SECTION_RE = /^## (.+)$/m
|
|
21
|
+
const LINE_SPLIT_RE = /\r?\n/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Читає секцію за заголовком із markdown-файлу.
|
|
25
|
+
* @param {string} text вміст файлу
|
|
26
|
+
* @param {string} heading заголовок без `## `
|
|
27
|
+
* @returns {string | null} вміст секції або null
|
|
28
|
+
*/
|
|
29
|
+
function extractSection(text, heading) {
|
|
30
|
+
const lines = text.split(LINE_SPLIT_RE)
|
|
31
|
+
const start = lines.indexOf(`## ${heading}`)
|
|
32
|
+
if (start === -1) return null
|
|
33
|
+
const end = lines.findIndex((l, i) => i > start && SECTION_RE.test(l))
|
|
34
|
+
const section = end === -1 ? lines.slice(start) : lines.slice(start, end)
|
|
35
|
+
return section.join('\n').trimEnd()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* `flow verify` handler.
|
|
40
|
+
* @param {string[]} _rest аргументи після `verify` (не використовуються)
|
|
41
|
+
* @param {{
|
|
42
|
+
* cwd?: string,
|
|
43
|
+
* log?: (m: string) => void,
|
|
44
|
+
* readFile?: (path: string, enc: string) => string,
|
|
45
|
+
* readdir?: (dir: string) => string[],
|
|
46
|
+
* exists?: (path: string) => boolean
|
|
47
|
+
* }} [deps] ін'єкції
|
|
48
|
+
* @returns {number} exit code (0=OK, 1=структурна помилка)
|
|
49
|
+
*/
|
|
50
|
+
export function cmdVerify(_rest, deps = {}) {
|
|
51
|
+
const cwd = deps.cwd ?? processCwd()
|
|
52
|
+
const log = deps.log ?? console.error
|
|
53
|
+
const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
|
|
54
|
+
const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
|
|
55
|
+
const exists = deps.exists ?? existsSync
|
|
56
|
+
|
|
57
|
+
const factNNN = latestFactNNN(cwd, readdir)
|
|
58
|
+
if (!factNNN) {
|
|
59
|
+
log('verify: fact_NNN.md не знайдено — структурна помилка')
|
|
60
|
+
return 1
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const factPath = join(cwd, `fact_${factNNN}.md`)
|
|
64
|
+
if (!exists(factPath)) {
|
|
65
|
+
log(`verify: fact_${factNNN}.md не існує — структурна помилка`)
|
|
66
|
+
return 1
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let factContent
|
|
70
|
+
try {
|
|
71
|
+
factContent = readFile(factPath, 'utf8')
|
|
72
|
+
} catch (error) {
|
|
73
|
+
log(`verify: не вдалося прочитати fact_${factNNN}.md — ${error instanceof Error ? error.message : String(error)}`)
|
|
74
|
+
return 1
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const withoutFm = factContent.replace(FRONT_MATTER_RE, '').trim()
|
|
78
|
+
if (withoutFm.length === 0) {
|
|
79
|
+
log(`verify: fact_${factNNN}.md порожній — структурна помилка`)
|
|
80
|
+
return 1
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const outLines = [`## verify context`, ``]
|
|
84
|
+
|
|
85
|
+
const taskPath = join(cwd, 'task.md')
|
|
86
|
+
if (exists(taskPath)) {
|
|
87
|
+
try {
|
|
88
|
+
const taskContent = readFile(taskPath, 'utf8')
|
|
89
|
+
const doneWhen = extractSection(taskContent, 'Done when')
|
|
90
|
+
if (doneWhen) outLines.push(doneWhen, '')
|
|
91
|
+
} catch {
|
|
92
|
+
// task.md недоступний — не блокуємо verify
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
outLines.push(`### fact_${factNNN}.md`, ``, factContent.trimEnd())
|
|
97
|
+
console.log(outLines.join('\n'))
|
|
98
|
+
|
|
99
|
+
return 0
|
|
100
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `n-cursor watch` — одноразовий скан стану DAG.
|
|
3
|
+
*
|
|
4
|
+
* Спрощена (no-daemon) реалізація:
|
|
5
|
+
* - Знаходить pending-audit без audit-result → логує (треба ручний аудит)
|
|
6
|
+
* - Знаходить stale worktrees > stale_worktree_min хвилин → попереджає
|
|
7
|
+
* - Знаходить needs-plan вузли → перелічує
|
|
8
|
+
* - exit 0 якщо чисто, exit 1 якщо потрібна увага
|
|
9
|
+
*
|
|
10
|
+
* FS і child_process ін'єктуються для тестованості.
|
|
11
|
+
*/
|
|
12
|
+
import { execSync } from 'node:child_process'
|
|
13
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
14
|
+
import { join } from 'node:path'
|
|
15
|
+
import { cwd as processCwd } from 'node:process'
|
|
16
|
+
|
|
17
|
+
import { loadConfig, resolveTasksDir, resolveWorktreesDir } from './config.mjs'
|
|
18
|
+
import { scanNodes } from './scanner.mjs'
|
|
19
|
+
import { listActiveWorktrees } from './worktree-ops.mjs'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* `watch` command handler (one-shot scan).
|
|
23
|
+
* @param {string[]} args аргументи (зазвичай порожні)
|
|
24
|
+
* @param {{
|
|
25
|
+
* cwd?: string,
|
|
26
|
+
* log?: (m: string) => void,
|
|
27
|
+
* readFile?: (p: string, enc: string) => string,
|
|
28
|
+
* readdir?: (d: string) => string[],
|
|
29
|
+
* exists?: (p: string) => boolean,
|
|
30
|
+
* execSync?: (cmd: string, opts?: object) => string,
|
|
31
|
+
* statSync?: (p: string) => { mtimeMs: number },
|
|
32
|
+
* now?: () => number
|
|
33
|
+
* }} [deps] ін'єкції
|
|
34
|
+
* @returns {Promise<number>} exit code (0=clean, 1=attention)
|
|
35
|
+
*/
|
|
36
|
+
export async function cmdWatch(args, deps = {}) {
|
|
37
|
+
const root = deps.cwd ?? processCwd()
|
|
38
|
+
const log = deps.log ?? console.log
|
|
39
|
+
const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
|
|
40
|
+
const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
|
|
41
|
+
const exists = deps.exists ?? existsSync
|
|
42
|
+
const execSyncFn = deps.execSync ?? ((cmd, o) => execSync(cmd, { ...o, encoding: 'utf8' }))
|
|
43
|
+
const statFn = deps.statSync ?? statSync
|
|
44
|
+
const nowMs = deps.now ?? (() => Date.now())
|
|
45
|
+
|
|
46
|
+
const config = loadConfig({ root, readFile, exists })
|
|
47
|
+
const tasksDir = resolveTasksDir(config, root)
|
|
48
|
+
const worktreesDir = resolveWorktreesDir(config, root)
|
|
49
|
+
const staleMs = config.stale_worktree_min * 60 * 1000
|
|
50
|
+
|
|
51
|
+
const activeWorktrees = listActiveWorktrees(root, { execSync: execSyncFn })
|
|
52
|
+
|
|
53
|
+
const allNodes = scanNodes(tasksDir, activeWorktrees, {
|
|
54
|
+
readdirSync: readdir,
|
|
55
|
+
existsSync: exists,
|
|
56
|
+
readFileSync: readFile
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
let needsAttention = false
|
|
60
|
+
|
|
61
|
+
// 1. Pending-audit без audit-result
|
|
62
|
+
const pendingAudit = allNodes.filter(n => n.state === 'pending-audit')
|
|
63
|
+
if (pendingAudit.length > 0) {
|
|
64
|
+
needsAttention = true
|
|
65
|
+
log(`[watch] pending-audit (${pendingAudit.length}) — потрібна ручна перевірка:`)
|
|
66
|
+
for (const n of pendingAudit) {
|
|
67
|
+
log(` - ${n.path}`)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 2. Stale worktrees
|
|
72
|
+
let worktreeEntries = []
|
|
73
|
+
try {
|
|
74
|
+
worktreeEntries = readdir(worktreesDir)
|
|
75
|
+
} catch {
|
|
76
|
+
// worktrees dir може не існувати
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const now = nowMs()
|
|
80
|
+
const staleWorktrees = []
|
|
81
|
+
for (const name of worktreeEntries) {
|
|
82
|
+
const wtPath = join(worktreesDir, name)
|
|
83
|
+
try {
|
|
84
|
+
const stat = statFn(wtPath)
|
|
85
|
+
const ageMs = now - stat.mtimeMs
|
|
86
|
+
if (ageMs > staleMs) {
|
|
87
|
+
staleWorktrees.push({ name, ageMin: Math.floor(ageMs / 60000) })
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
// пропускаємо
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (staleWorktrees.length > 0) {
|
|
95
|
+
needsAttention = true
|
|
96
|
+
log(`[watch] stale worktrees (${staleWorktrees.length}) — неактивні > ${config.stale_worktree_min} хв:`)
|
|
97
|
+
for (const wt of staleWorktrees) {
|
|
98
|
+
log(` - ${wt.name} (${wt.ageMin} хв)`)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 3. Needs-plan вузли
|
|
103
|
+
const needsPlan = allNodes.filter(n => n.state === 'needs-plan')
|
|
104
|
+
if (needsPlan.length > 0) {
|
|
105
|
+
log(`[watch] needs-plan (${needsPlan.length}) — потрібне планування:`)
|
|
106
|
+
for (const n of needsPlan) {
|
|
107
|
+
log(` - ${n.path}`)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 4. Failed вузли
|
|
112
|
+
const failed = allNodes.filter(n => n.state === 'failed')
|
|
113
|
+
if (failed.length > 0) {
|
|
114
|
+
needsAttention = true
|
|
115
|
+
log(`[watch] failed (${failed.length}) — завершились з помилкою:`)
|
|
116
|
+
for (const n of failed) {
|
|
117
|
+
log(` - ${n.path}`)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!needsAttention && pendingAudit.length === 0 && failed.length === 0) {
|
|
122
|
+
const running = allNodes.filter(n => n.state === 'running').length
|
|
123
|
+
const resolved = allNodes.filter(n => n.state === 'resolved').length
|
|
124
|
+
log(`[watch] OK — total:${allNodes.length} running:${running} resolved:${resolved}`)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return needsAttention ? 1 : 0
|
|
128
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
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
|
+
}
|