@nitra/cursor 3.29.0 → 4.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 +7 -0
- package/package.json +1 -1
- package/scripts/dispatcher/index.mjs +20 -61
- package/scripts/dispatcher/lib/flow-plan.mjs +153 -0
- package/scripts/dispatcher/lib/flow-signals.mjs +235 -0
- package/scripts/dispatcher/lib/flow-verify.mjs +127 -0
- package/scripts/worktree-cli.mjs +2 -2
- package/skills/docgen/js/docgen-gen.mjs +42 -125
- package/scripts/dispatcher/lib/active.mjs +0 -222
- package/scripts/dispatcher/lib/artifact.mjs +0 -79
- package/scripts/dispatcher/lib/budget.mjs +0 -36
- package/scripts/dispatcher/lib/capability.mjs +0 -59
- package/scripts/dispatcher/lib/commands.mjs +0 -296
- package/scripts/dispatcher/lib/flow-lock.mjs +0 -39
- package/scripts/dispatcher/lib/gate.mjs +0 -91
- package/scripts/dispatcher/lib/level.mjs +0 -135
- package/scripts/dispatcher/lib/plan.mjs +0 -88
- package/scripts/dispatcher/lib/planner.mjs +0 -73
- package/scripts/dispatcher/lib/review.mjs +0 -176
- package/scripts/dispatcher/lib/reviewer.mjs +0 -44
- package/scripts/dispatcher/lib/snapshot.mjs +0 -58
- package/scripts/dispatcher/lib/spec.mjs +0 -97
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,86 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI-диспетчер `n-cursor flow` (
|
|
2
|
+
* CLI-диспетчер `n-cursor flow` (думка.MD — протокол всередині вузла графу).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* flow plan — Stage 1: читає task.md, створює plan_NNN.md, виводить контекст
|
|
5
|
+
* flow verify — Stage 2: структурний check + ## Done when + outputs на stdout
|
|
6
|
+
* flow done — CWD → node path → `graph done <path>`
|
|
7
|
+
* flow audit — CWD → node path → pending-audit_NNN.md → `graph audit <path>`
|
|
8
|
+
* flow failed — CWD → node path → `graph failed <path>`
|
|
9
|
+
* flow spawn — CWD → node path → `graph spawn <path>`
|
|
9
10
|
*/
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { plan } from './lib/plan.mjs'
|
|
14
|
-
import { review } from './lib/review.mjs'
|
|
15
|
-
import { spec } from './lib/spec.mjs'
|
|
11
|
+
import { plan } from './lib/flow-plan.mjs'
|
|
12
|
+
import { verify } from './lib/flow-verify.mjs'
|
|
13
|
+
import { audit, done, failed, spawn } from './lib/flow-signals.mjs'
|
|
16
14
|
|
|
17
15
|
const USAGE = [
|
|
18
16
|
'Usage:',
|
|
19
|
-
' npx @nitra/cursor flow
|
|
20
|
-
' npx @nitra/cursor flow
|
|
21
|
-
' npx @nitra/cursor flow
|
|
22
|
-
' npx @nitra/cursor flow
|
|
23
|
-
' npx @nitra/cursor flow
|
|
24
|
-
' npx @nitra/cursor flow
|
|
25
|
-
' npx @nitra/cursor flow release # Фасад A: .changes + completion snapshot',
|
|
26
|
-
' npx @nitra/cursor flow run "<опис>" # Фасад B: повний 5-фазний цикл',
|
|
27
|
-
' npx @nitra/cursor flow resume # продовжити з чекпойнта',
|
|
28
|
-
' npx @nitra/cursor flow cancel # скасувати, прибрати стан',
|
|
29
|
-
' npx @nitra/cursor flow repair [--discard-step-work] # відновлення пошкодженого стану'
|
|
17
|
+
' npx @nitra/cursor flow plan # Stage 1: читає task.md, створює plan_NNN.md',
|
|
18
|
+
' npx @nitra/cursor flow verify # Stage 2: структурна перевірка + stdout-контекст для агента',
|
|
19
|
+
' npx @nitra/cursor flow done # успіх → graph done <node-path>',
|
|
20
|
+
' npx @nitra/cursor flow audit # аудит → pending-audit_NNN.md → graph audit <node-path>',
|
|
21
|
+
' npx @nitra/cursor flow failed # провал → graph failed <node-path>',
|
|
22
|
+
' npx @nitra/cursor flow spawn # розклад → graph spawn <node-path>'
|
|
30
23
|
].join('\n')
|
|
31
24
|
|
|
32
25
|
/**
|
|
33
|
-
* Усі handler-и реальні (Ф1 Spec/Plan + Ф2 Турнікет + Ф4 Активний Раннер).
|
|
34
26
|
* @type {Record<string, (rest: string[], deps: object) => Promise<number>>}
|
|
35
27
|
*/
|
|
36
|
-
export const DEFAULT_HANDLERS = {
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Витягує опційний `--branch <гілка>` з аргументів (для cwd-незалежного резолву
|
|
40
|
-
* стану — беклог #1). Повертає очищені аргументи й значення гілки.
|
|
41
|
-
* @param {string[]} args аргументи після підкоманди
|
|
42
|
-
* @returns {{ rest: string[], branch: string | undefined }} очищені аргументи + гілка
|
|
43
|
-
*/
|
|
44
|
-
export function extractBranchFlag(args) {
|
|
45
|
-
const rest = []
|
|
46
|
-
let branch
|
|
47
|
-
for (let i = 0; i < args.length; i++) {
|
|
48
|
-
if (args[i] === '--branch') {
|
|
49
|
-
const val = args[i + 1]
|
|
50
|
-
// Поглинаємо наступний аргумент як значення лише якщо це справді значення,
|
|
51
|
-
// а не інший прапорець / кінець аргументів (інакше `--branch` був би no-op,
|
|
52
|
-
// що тихо ковтав би сусідній прапорець).
|
|
53
|
-
if (val !== undefined && !val.startsWith('-')) {
|
|
54
|
-
branch = val
|
|
55
|
-
i++
|
|
56
|
-
}
|
|
57
|
-
continue
|
|
58
|
-
}
|
|
59
|
-
const inline = args[i].startsWith('--branch=') ? args[i].slice('--branch='.length) : null
|
|
60
|
-
if (inline !== null) {
|
|
61
|
-
if (inline !== '') branch = inline
|
|
62
|
-
continue
|
|
63
|
-
}
|
|
64
|
-
rest.push(args[i])
|
|
65
|
-
}
|
|
66
|
-
return { rest, branch }
|
|
67
|
-
}
|
|
28
|
+
export const DEFAULT_HANDLERS = { plan, verify, done, audit, failed, spawn }
|
|
68
29
|
|
|
69
30
|
/**
|
|
70
31
|
* Точка входу `case 'flow'` у `bin/n-cursor.js`. Парсить підкоманду й
|
|
71
32
|
* маршрутизує до handler-а. Невідома/відсутня підкоманда → usage + код 1.
|
|
72
|
-
* Опційний `--branch <гілка>` прокидається в `deps.branch` (резолв стану поза worktree).
|
|
73
33
|
* @param {string[]} args аргументи після `flow`
|
|
74
|
-
* @param {{ handlers?: Record<string, (rest: string[], deps: object) => Promise<number
|
|
34
|
+
* @param {{ handlers?: Record<string, (rest: string[], deps: object) => Promise<number>> }} [deps] ін'єкція handler-ів (для тестів)
|
|
75
35
|
* @returns {Promise<number>} exit code
|
|
76
36
|
*/
|
|
77
37
|
export async function runFlowCli(args, deps = {}) {
|
|
78
|
-
const [sub, ...
|
|
38
|
+
const [sub, ...rest] = args
|
|
79
39
|
const handlers = deps.handlers ?? DEFAULT_HANDLERS
|
|
80
40
|
if (!sub || !Object.hasOwn(handlers, sub)) {
|
|
81
41
|
console.error(USAGE)
|
|
82
42
|
return 1
|
|
83
43
|
}
|
|
84
|
-
|
|
85
|
-
return await handlers[sub](rest, { ...deps, branch: deps.branch ?? branch })
|
|
44
|
+
return await handlers[sub](rest, deps)
|
|
86
45
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handler `flow plan` — Stage 1 (думка.MD § "flow plan").
|
|
3
|
+
*
|
|
4
|
+
* Читає `task.md` у поточному вузлі, розбирає `mode` і `hint` з front-matter,
|
|
5
|
+
* знаходить наступний номер `plan_NNN.md`, пише шаблон і виводить контекст для
|
|
6
|
+
* агента (task + mode + hint) на stdout.
|
|
7
|
+
*
|
|
8
|
+
* FS та path-резолвінг ін'єктуються — тестується без реального диска.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
11
|
+
import { join } from 'node:path'
|
|
12
|
+
import { cwd as processCwd } from 'node:process'
|
|
13
|
+
|
|
14
|
+
const FRONT_MATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Парсить YAML front-matter (мінімально: лише прості `key: value` рядки).
|
|
18
|
+
* @param {string} text вміст файлу
|
|
19
|
+
* @returns {Record<string, string>} ключ-значення з front-matter (рядки)
|
|
20
|
+
*/
|
|
21
|
+
function parseFrontMatter(text) {
|
|
22
|
+
const m = text.match(FRONT_MATTER_RE)
|
|
23
|
+
if (!m) return {}
|
|
24
|
+
const result = {}
|
|
25
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
26
|
+
const idx = line.indexOf(':')
|
|
27
|
+
if (idx === -1) continue
|
|
28
|
+
const key = line.slice(0, idx).trim()
|
|
29
|
+
const val = line.slice(idx + 1).trim()
|
|
30
|
+
if (key) result[key] = val
|
|
31
|
+
}
|
|
32
|
+
return result
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Знаходить наступний номер `plan_NNN.md` у директорії вузла.
|
|
37
|
+
* @param {string} dir абсолютний шлях до директорії вузла
|
|
38
|
+
* @param {(dir: string) => string[]} readdir інжектована readdir
|
|
39
|
+
* @returns {string} рядок типу `001`, `002`, …
|
|
40
|
+
*/
|
|
41
|
+
function nextPlanNumber(dir, readdir) {
|
|
42
|
+
const files = readdir(dir)
|
|
43
|
+
let max = 0
|
|
44
|
+
for (const f of files) {
|
|
45
|
+
const m = f.match(/^plan_(\d+)\.md$/)
|
|
46
|
+
if (m) {
|
|
47
|
+
const n = parseInt(m[1], 10)
|
|
48
|
+
if (n > max) max = n
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return String(max + 1).padStart(3, '0')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Будує вміст шаблону `plan_NNN.md`.
|
|
56
|
+
* @param {{ mode: string, hint: string, now: string }} params параметри
|
|
57
|
+
* @returns {string} вміст файлу
|
|
58
|
+
*/
|
|
59
|
+
export function buildPlanTemplate({ mode, hint, now }) {
|
|
60
|
+
return [
|
|
61
|
+
'---',
|
|
62
|
+
`created_at: ${now}`,
|
|
63
|
+
`mode: ${mode}`,
|
|
64
|
+
`decision: ${hint || 'atomic | composite'}`,
|
|
65
|
+
'---',
|
|
66
|
+
'',
|
|
67
|
+
'## Context',
|
|
68
|
+
"<!-- Чому саме такий підхід — що агент/людина з'ясували -->",
|
|
69
|
+
'',
|
|
70
|
+
'## Approach',
|
|
71
|
+
'<!-- atomic: покроковий план виконання -->',
|
|
72
|
+
'<!-- composite: список дочірніх вузлів з описами -->',
|
|
73
|
+
'',
|
|
74
|
+
'## Risks',
|
|
75
|
+
'<!-- Що може піти не так -->',
|
|
76
|
+
''
|
|
77
|
+
].join('\n')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* `flow plan` handler.
|
|
82
|
+
*
|
|
83
|
+
* @param {string[]} _rest аргументи після `plan` (не використовуються)
|
|
84
|
+
* @param {{
|
|
85
|
+
* cwd?: string,
|
|
86
|
+
* log?: (m: string) => void,
|
|
87
|
+
* readFile?: (path: string, enc: string) => string,
|
|
88
|
+
* writeFile?: (path: string, content: string, enc: string) => void,
|
|
89
|
+
* readdir?: (dir: string) => string[],
|
|
90
|
+
* exists?: (path: string) => boolean,
|
|
91
|
+
* now?: () => string
|
|
92
|
+
* }} [deps] ін'єкції
|
|
93
|
+
* @returns {Promise<number>} exit code
|
|
94
|
+
*/
|
|
95
|
+
export async function plan(_rest, deps = {}) {
|
|
96
|
+
const cwd = deps.cwd ?? processCwd()
|
|
97
|
+
const log = deps.log ?? console.error
|
|
98
|
+
const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
|
|
99
|
+
const writeFile = deps.writeFile ?? ((p, c, enc) => writeFileSync(p, c, enc))
|
|
100
|
+
const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
|
|
101
|
+
const exists = deps.exists ?? existsSync
|
|
102
|
+
const nowFn = deps.now ?? (() => new Date().toISOString())
|
|
103
|
+
|
|
104
|
+
const taskPath = join(cwd, 'task.md')
|
|
105
|
+
if (!exists(taskPath)) {
|
|
106
|
+
log('flow plan: task.md не знайдено в CWD')
|
|
107
|
+
return 1
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let taskContent
|
|
111
|
+
try {
|
|
112
|
+
taskContent = readFile(taskPath, 'utf8')
|
|
113
|
+
} catch (err) {
|
|
114
|
+
log(`flow plan: не вдалося прочитати task.md — ${err instanceof Error ? err.message : String(err)}`)
|
|
115
|
+
return 1
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const fm = parseFrontMatter(taskContent)
|
|
119
|
+
const mode = fm.mode || 'human'
|
|
120
|
+
const hint = fm.hint || ''
|
|
121
|
+
|
|
122
|
+
const num = nextPlanNumber(cwd, readdir)
|
|
123
|
+
const planPath = join(cwd, `plan_${num}.md`)
|
|
124
|
+
|
|
125
|
+
const content = buildPlanTemplate({ mode, hint, now: nowFn() })
|
|
126
|
+
try {
|
|
127
|
+
writeFile(planPath, content, 'utf8')
|
|
128
|
+
} catch (err) {
|
|
129
|
+
log(`flow plan: не вдалося записати ${planPath} — ${err instanceof Error ? err.message : String(err)}`)
|
|
130
|
+
return 1
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
log(`flow plan: створено ${planPath}`)
|
|
134
|
+
|
|
135
|
+
// Виводимо контекст для агента на stdout
|
|
136
|
+
const outLines = [
|
|
137
|
+
`## flow plan context`,
|
|
138
|
+
``,
|
|
139
|
+
`mode: ${mode}`,
|
|
140
|
+
hint ? `hint: ${hint}` : `hint: (не задано — агент вирішує сам)`,
|
|
141
|
+
`plan: plan_${num}.md`,
|
|
142
|
+
``
|
|
143
|
+
]
|
|
144
|
+
// Додаємо вміст task.md для контексту (без front-matter)
|
|
145
|
+
outLines.push(`### task.md`)
|
|
146
|
+
const bodyStart = taskContent.indexOf('\n---\n', 4)
|
|
147
|
+
const taskBody = bodyStart !== -1 ? taskContent.slice(bodyStart + 5).trimStart() : taskContent
|
|
148
|
+
outLines.push(taskBody.trimEnd())
|
|
149
|
+
|
|
150
|
+
console.log(outLines.join('\n'))
|
|
151
|
+
|
|
152
|
+
return 0
|
|
153
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handlers сигнальних команд `flow done/audit/failed/spawn` (думка.MD).
|
|
3
|
+
*
|
|
4
|
+
* Агент ніколи не знає свій абсолютний path — команди обчислюють path вузла з
|
|
5
|
+
* env var `NCURSOR_NODE_PATH` (встановлюється wrapper-скриптом) або з файлу
|
|
6
|
+
* `.n-cursor/current-node` у корені worktree. Якщо нічого — error.
|
|
7
|
+
*
|
|
8
|
+
* done → делегує `n-cursor graph done <path>`
|
|
9
|
+
* audit → створює `pending-audit_NNN.md` → делегує `n-cursor graph audit <path>`
|
|
10
|
+
* failed → делегує `n-cursor graph failed <path>`
|
|
11
|
+
* spawn → делегує `n-cursor graph spawn <path>`
|
|
12
|
+
*
|
|
13
|
+
* Всі IO ін'єктуються для тестування без реальних процесів і диска.
|
|
14
|
+
*/
|
|
15
|
+
import { spawnSync } from 'node:child_process'
|
|
16
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
17
|
+
import { join } from 'node:path'
|
|
18
|
+
import { cwd as processCwd } from 'node:process'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Резолвить шлях вузла з env або fallback-файлу.
|
|
22
|
+
* @param {{
|
|
23
|
+
* env?: Record<string, string | undefined>,
|
|
24
|
+
* cwd?: string,
|
|
25
|
+
* readFile?: (p: string, enc: string) => string,
|
|
26
|
+
* exists?: (p: string) => boolean
|
|
27
|
+
* }} deps ін'єкції
|
|
28
|
+
* @returns {{ nodePath: string | null, error: string | null }} результат
|
|
29
|
+
*/
|
|
30
|
+
export function resolveNodePath(deps = {}) {
|
|
31
|
+
const env = deps.env ?? process.env
|
|
32
|
+
const cwd = deps.cwd ?? processCwd()
|
|
33
|
+
const exists = deps.exists ?? existsSync
|
|
34
|
+
const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
|
|
35
|
+
|
|
36
|
+
// 1. Env var
|
|
37
|
+
const fromEnv = env['NCURSOR_NODE_PATH']
|
|
38
|
+
if (fromEnv && fromEnv.trim().length > 0) {
|
|
39
|
+
return { nodePath: fromEnv.trim(), error: null }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 2. Fallback-файл .n-cursor/current-node у CWD (корінь worktree)
|
|
43
|
+
const fallbackPath = join(cwd, '.n-cursor', 'current-node')
|
|
44
|
+
if (exists(fallbackPath)) {
|
|
45
|
+
try {
|
|
46
|
+
const content = readFile(fallbackPath, 'utf8').trim()
|
|
47
|
+
if (content.length > 0) {
|
|
48
|
+
return { nodePath: content, error: null }
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
// якщо не читається — fallthrough до error
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { nodePath: null, error: 'NCURSOR_NODE_PATH not set and .n-cursor/current-node not found' }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Знаходить поточний найбільший номер `outputs_NNN.md`.
|
|
60
|
+
* @param {string} dir директорія вузла
|
|
61
|
+
* @param {(dir: string) => string[]} readdir ін'єктована readdir
|
|
62
|
+
* @returns {string | null} рядок типу `001` або null якщо не знайдено
|
|
63
|
+
*/
|
|
64
|
+
function findCurrentOutputsNum(dir, readdir) {
|
|
65
|
+
const files = readdir(dir)
|
|
66
|
+
let max = -1
|
|
67
|
+
for (const f of files) {
|
|
68
|
+
const m = f.match(/^outputs_(\d+)\.md$/)
|
|
69
|
+
if (m) {
|
|
70
|
+
const n = parseInt(m[1], 10)
|
|
71
|
+
if (n > max) max = n
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return max >= 0 ? String(max).padStart(3, '0') : null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Виконує n-cursor graph <sub> <nodePath>.
|
|
79
|
+
* @param {string} sub підкоманда graph
|
|
80
|
+
* @param {string} nodePath шлях вузла
|
|
81
|
+
* @param {{
|
|
82
|
+
* run: (cmd: string, args: string[]) => { status: number, stdout: string, stderr: string }
|
|
83
|
+
* }} deps
|
|
84
|
+
* @returns {number} exit code
|
|
85
|
+
*/
|
|
86
|
+
function delegateToGraph(sub, nodePath, deps) {
|
|
87
|
+
const result = deps.run('npx', ['@nitra/cursor', 'graph', sub, nodePath])
|
|
88
|
+
return result.status ?? 1
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Реальний sync-runner процесу.
|
|
93
|
+
* @param {string} cmd
|
|
94
|
+
* @param {string[]} args
|
|
95
|
+
* @returns {{ status: number, stdout: string, stderr: string }}
|
|
96
|
+
*/
|
|
97
|
+
function realRun(cmd, args) {
|
|
98
|
+
const r = spawnSync(cmd, args, { encoding: 'utf8' })
|
|
99
|
+
return { status: r.status ?? 1, stdout: r.stdout ?? '', stderr: r.stderr ?? '' }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Базовий handler для сигнальних команд без аудиту.
|
|
104
|
+
* @param {string} sub підкоманда graph
|
|
105
|
+
* @param {{
|
|
106
|
+
* cwd?: string,
|
|
107
|
+
* env?: Record<string, string | undefined>,
|
|
108
|
+
* log?: (m: string) => void,
|
|
109
|
+
* run?: (cmd: string, args: string[]) => { status: number, stdout: string, stderr: string },
|
|
110
|
+
* readFile?: (p: string, enc: string) => string,
|
|
111
|
+
* exists?: (p: string) => boolean
|
|
112
|
+
* }} deps ін'єкції
|
|
113
|
+
* @returns {Promise<number>} exit code
|
|
114
|
+
*/
|
|
115
|
+
async function signalHandler(sub, deps = {}) {
|
|
116
|
+
const cwd = deps.cwd ?? processCwd()
|
|
117
|
+
const log = deps.log ?? console.error
|
|
118
|
+
const run = deps.run ?? realRun
|
|
119
|
+
|
|
120
|
+
const { nodePath, error } = resolveNodePath({ env: deps.env, cwd, readFile: deps.readFile, exists: deps.exists })
|
|
121
|
+
if (!nodePath) {
|
|
122
|
+
log(`flow ${sub}: ${error}`)
|
|
123
|
+
return 1
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
log(`flow ${sub}: node path = ${nodePath}`)
|
|
127
|
+
const code = delegateToGraph(sub, nodePath, { run })
|
|
128
|
+
if (code !== 0) {
|
|
129
|
+
log(`flow ${sub}: graph ${sub} завершився з кодом ${code}`)
|
|
130
|
+
}
|
|
131
|
+
return code
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* `flow done` — сигналізує успіх → `graph done <path>`.
|
|
136
|
+
* @param {string[]} _rest аргументи (не використовуються)
|
|
137
|
+
* @param {object} [deps] ін'єкції
|
|
138
|
+
* @returns {Promise<number>} exit code
|
|
139
|
+
*/
|
|
140
|
+
export async function done(_rest, deps = {}) {
|
|
141
|
+
return signalHandler('done', deps)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* `flow failed` — сигналізує провал → `graph failed <path>`.
|
|
146
|
+
* @param {string[]} _rest аргументи (не використовуються)
|
|
147
|
+
* @param {object} [deps] ін'єкції
|
|
148
|
+
* @returns {Promise<number>} exit code
|
|
149
|
+
*/
|
|
150
|
+
export async function failed(_rest, deps = {}) {
|
|
151
|
+
return signalHandler('failed', deps)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* `flow spawn` — сигналізує розклад → `graph spawn <path>`.
|
|
156
|
+
* @param {string[]} _rest аргументи (не використовуються)
|
|
157
|
+
* @param {object} [deps] ін'єкції
|
|
158
|
+
* @returns {Promise<number>} exit code
|
|
159
|
+
*/
|
|
160
|
+
export async function spawn(_rest, deps = {}) {
|
|
161
|
+
return signalHandler('spawn', deps)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* `flow audit` — створює `pending-audit_NNN.md` → `graph audit <path>`.
|
|
166
|
+
*
|
|
167
|
+
* NNN у `pending-audit_NNN.md` = NNN відповідного `outputs_NNN.md`.
|
|
168
|
+
* Якщо outputs відсутні — error.
|
|
169
|
+
*
|
|
170
|
+
* @param {string[]} _rest аргументи (не використовуються)
|
|
171
|
+
* @param {{
|
|
172
|
+
* cwd?: string,
|
|
173
|
+
* env?: Record<string, string | undefined>,
|
|
174
|
+
* log?: (m: string) => void,
|
|
175
|
+
* run?: (cmd: string, args: string[]) => { status: number, stdout: string, stderr: string },
|
|
176
|
+
* readFile?: (p: string, enc: string) => string,
|
|
177
|
+
* writeFile?: (p: string, content: string, enc: string) => void,
|
|
178
|
+
* readdir?: (dir: string) => string[],
|
|
179
|
+
* exists?: (p: string) => boolean,
|
|
180
|
+
* now?: () => string
|
|
181
|
+
* }} [deps] ін'єкції
|
|
182
|
+
* @returns {Promise<number>} exit code
|
|
183
|
+
*/
|
|
184
|
+
export async function audit(_rest, deps = {}) {
|
|
185
|
+
const cwd = deps.cwd ?? processCwd()
|
|
186
|
+
const log = deps.log ?? console.error
|
|
187
|
+
const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
|
|
188
|
+
const writeFile = deps.writeFile ?? ((p, c, enc) => writeFileSync(p, c, enc))
|
|
189
|
+
const exists = deps.exists ?? existsSync
|
|
190
|
+
const nowFn = deps.now ?? (() => new Date().toISOString())
|
|
191
|
+
const run = deps.run ?? realRun
|
|
192
|
+
|
|
193
|
+
const { nodePath, error } = resolveNodePath({ env: deps.env, cwd, readFile: deps.readFile, exists })
|
|
194
|
+
if (!nodePath) {
|
|
195
|
+
log(`flow audit: ${error}`)
|
|
196
|
+
return 1
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Знаходимо поточний outputs NNN
|
|
200
|
+
const outputsNum = findCurrentOutputsNum(cwd, readdir)
|
|
201
|
+
if (!outputsNum) {
|
|
202
|
+
log('flow audit: outputs_NNN.md не знайдено — спершу напиши outputs')
|
|
203
|
+
return 1
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const pendingPath = join(cwd, `pending-audit_${outputsNum}.md`)
|
|
207
|
+
if (exists(pendingPath)) {
|
|
208
|
+
log(`flow audit: ${pendingPath} вже існує — audit вже запитано для outputs_${outputsNum}.md`)
|
|
209
|
+
return 1
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const content = [
|
|
213
|
+
'---',
|
|
214
|
+
`created_at: ${nowFn()}`,
|
|
215
|
+
`outputs_ref: outputs_${outputsNum}.md`,
|
|
216
|
+
`actor: agent`,
|
|
217
|
+
'---',
|
|
218
|
+
''
|
|
219
|
+
].join('\n')
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
writeFile(pendingPath, content, 'utf8')
|
|
223
|
+
} catch (err) {
|
|
224
|
+
log(`flow audit: не вдалося записати ${pendingPath} — ${err instanceof Error ? err.message : String(err)}`)
|
|
225
|
+
return 1
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
log(`flow audit: ${pendingPath} створено`)
|
|
229
|
+
log(`flow audit: node path = ${nodePath}`)
|
|
230
|
+
const code = delegateToGraph('audit', nodePath, { run })
|
|
231
|
+
if (code !== 0) {
|
|
232
|
+
log(`flow audit: graph audit завершився з кодом ${code}`)
|
|
233
|
+
}
|
|
234
|
+
return code
|
|
235
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handler `flow verify` — Stage 2 structural check (думка.MD § "flow verify").
|
|
3
|
+
*
|
|
4
|
+
* Перевіряє що `outputs_NNN.md` існує і непорожній у директорії поточного вузла
|
|
5
|
+
* (CWD). Якщо так — виводить `## Done when` секцію з `task.md` та вміст
|
|
6
|
+
* `outputs_NNN.md` на stdout для агентської self-evaluation.
|
|
7
|
+
*
|
|
8
|
+
* exit 0 = структурно OK
|
|
9
|
+
* exit 1 = структурна помилка (outputs відсутній або порожній)
|
|
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
|
+
const FRONT_MATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/
|
|
18
|
+
const SECTION_RE = /^## (.+)$/m
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Читає секцію за заголовком із markdown-файлу.
|
|
22
|
+
* Повертає вміст від заголовка до наступного `## ` або кінця файлу.
|
|
23
|
+
* @param {string} text вміст файлу
|
|
24
|
+
* @param {string} heading заголовок без `## `
|
|
25
|
+
* @returns {string | null} вміст секції (включно з рядком заголовка) або null
|
|
26
|
+
*/
|
|
27
|
+
function extractSection(text, heading) {
|
|
28
|
+
const lines = text.split(/\r?\n/)
|
|
29
|
+
const start = lines.findIndex(l => l === `## ${heading}`)
|
|
30
|
+
if (start === -1) return null
|
|
31
|
+
const end = lines.findIndex((l, i) => i > start && SECTION_RE.test(l))
|
|
32
|
+
const section = end === -1 ? lines.slice(start) : lines.slice(start, end)
|
|
33
|
+
return section.join('\n').trimEnd()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Знаходить outputs-файл з найбільшим NNN у директорії вузла.
|
|
38
|
+
* @param {string[]} files список файлів директорії
|
|
39
|
+
* @returns {string | null} ім'я файлу (напр. `outputs_001.md`) або null
|
|
40
|
+
*/
|
|
41
|
+
export function findLatestOutputs(files) {
|
|
42
|
+
let max = -1
|
|
43
|
+
let best = null
|
|
44
|
+
for (const f of files) {
|
|
45
|
+
const m = f.match(/^outputs_(\d+)\.md$/)
|
|
46
|
+
if (m) {
|
|
47
|
+
const n = parseInt(m[1], 10)
|
|
48
|
+
if (n > max) {
|
|
49
|
+
max = n
|
|
50
|
+
best = f
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return best
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* `flow verify` handler.
|
|
59
|
+
*
|
|
60
|
+
* @param {string[]} _rest аргументи після `verify` (не використовуються)
|
|
61
|
+
* @param {{
|
|
62
|
+
* cwd?: string,
|
|
63
|
+
* log?: (m: string) => void,
|
|
64
|
+
* readFile?: (path: string, enc: string) => string,
|
|
65
|
+
* readdir?: (dir: string) => string[],
|
|
66
|
+
* exists?: (path: string) => boolean
|
|
67
|
+
* }} [deps] ін'єкції
|
|
68
|
+
* @returns {Promise<number>} exit code (0=OK, 1=структурна помилка)
|
|
69
|
+
*/
|
|
70
|
+
export async function verify(_rest, deps = {}) {
|
|
71
|
+
const cwd = deps.cwd ?? processCwd()
|
|
72
|
+
const log = deps.log ?? console.error
|
|
73
|
+
const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
|
|
74
|
+
const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
|
|
75
|
+
const exists = deps.exists ?? existsSync
|
|
76
|
+
|
|
77
|
+
// Перевіряємо outputs_NNN.md
|
|
78
|
+
const files = readdir(cwd)
|
|
79
|
+
const outputsName = findLatestOutputs(files)
|
|
80
|
+
if (!outputsName) {
|
|
81
|
+
log('flow verify: outputs_NNN.md не знайдено — структурна помилка')
|
|
82
|
+
return 1
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const outputsPath = join(cwd, outputsName)
|
|
86
|
+
if (!exists(outputsPath)) {
|
|
87
|
+
log(`flow verify: ${outputsName} не існує — структурна помилка`)
|
|
88
|
+
return 1
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let outputsContent
|
|
92
|
+
try {
|
|
93
|
+
outputsContent = readFile(outputsPath, 'utf8')
|
|
94
|
+
} catch (err) {
|
|
95
|
+
log(`flow verify: не вдалося прочитати ${outputsName} — ${err instanceof Error ? err.message : String(err)}`)
|
|
96
|
+
return 1
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Перевіряємо що файл не порожній (без front-matter — суто тіло)
|
|
100
|
+
const withoutFm = outputsContent.replace(FRONT_MATTER_RE, '').trim()
|
|
101
|
+
if (withoutFm.length === 0) {
|
|
102
|
+
log(`flow verify: ${outputsName} порожній — структурна помилка`)
|
|
103
|
+
return 1
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Виводимо Done when + outputs на stdout для агентської self-evaluation
|
|
107
|
+
const outLines = [`## verify context`, ``]
|
|
108
|
+
|
|
109
|
+
const taskPath = join(cwd, 'task.md')
|
|
110
|
+
if (exists(taskPath)) {
|
|
111
|
+
try {
|
|
112
|
+
const taskContent = readFile(taskPath, 'utf8')
|
|
113
|
+
const doneWhen = extractSection(taskContent, 'Done when')
|
|
114
|
+
if (doneWhen) {
|
|
115
|
+
outLines.push(doneWhen, '')
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
// якщо task.md недоступний — не блокуємо verify
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
outLines.push(`### ${outputsName}`, ``, outputsContent.trimEnd())
|
|
123
|
+
|
|
124
|
+
console.log(outLines.join('\n'))
|
|
125
|
+
|
|
126
|
+
return 0
|
|
127
|
+
}
|
package/scripts/worktree-cli.mjs
CHANGED
|
@@ -15,7 +15,6 @@ import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'no
|
|
|
15
15
|
import { join } from 'node:path'
|
|
16
16
|
import { cwd as processCwd } from 'node:process'
|
|
17
17
|
|
|
18
|
-
import { cleanupFlowSiblings } from './dispatcher/lib/state-store.mjs'
|
|
19
18
|
import {
|
|
20
19
|
buildDescription,
|
|
21
20
|
buildDirtyNotice,
|
|
@@ -161,7 +160,8 @@ function cmdRemove(rest, ctx) {
|
|
|
161
160
|
return 1
|
|
162
161
|
}
|
|
163
162
|
if (existsSync(paths.descFile)) rmSync(paths.descFile, { force: true })
|
|
164
|
-
|
|
163
|
+
// В новій архітектурі (думка.MD) state зберігається у файлах вузлів (task.md, outputs, run),
|
|
164
|
+
// а не у .flow.json/.events.jsonl/lock sibling-ах. Cleanup sibling-ів більше не потрібен.
|
|
165
165
|
ctx.log(`✅ прибрано: ${paths.checkout} (гілку ${branch} лишено)`)
|
|
166
166
|
return 0
|
|
167
167
|
}
|