@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,212 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `n-cursor graph` — read-only позиція DAG вузлів (контракт
|
|
3
|
-
* `docs/specs/2026-06-01-node-dag-state.md`). Перший зріз: `status` —
|
|
4
|
-
* сканує `docs/graphs/<g>/nodes/*.md`, групує файли по вузлах, деривує статус
|
|
5
|
-
* (done/failed/awaiting-human/in_progress/ready/blocked) і друкує таблицю.
|
|
6
|
-
*
|
|
7
|
-
* Стан — у файлах; нічого не мутує. FS (`readdir`/`readFile`/`exists`)
|
|
8
|
-
* ін'єктується — тестується без диска. claim/tick/dispatch — наступні зрізи.
|
|
9
|
-
*/
|
|
10
|
-
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
11
|
-
import { join } from 'node:path'
|
|
12
|
-
import { cwd as processCwd } from 'node:process'
|
|
13
|
-
|
|
14
|
-
import { parseFrontMatter } from './trace.mjs'
|
|
15
|
-
|
|
16
|
-
/** Суфікси-артефакти вузла без qid. */
|
|
17
|
-
const PLAIN = [
|
|
18
|
-
['.plan', 'plan'],
|
|
19
|
-
['.claim', 'claim'],
|
|
20
|
-
['.fact', 'fact']
|
|
21
|
-
]
|
|
22
|
-
/** Префікси-артефакти з qid (`.ask-<qid>`, `.ans-<qid>`). */
|
|
23
|
-
const QID = [
|
|
24
|
-
['.ask-', 'ask'],
|
|
25
|
-
['.ans-', 'ans']
|
|
26
|
-
]
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Класифікує файл-артефакт вузла за назвою.
|
|
30
|
-
* @param {string} name назва файлу (напр. `B02-parser.ask-q1.md`)
|
|
31
|
-
* @returns {{ stem: string, kind: string, qid?: string } | null} класифікація або null
|
|
32
|
-
*/
|
|
33
|
-
export function classifyArtifact(name) {
|
|
34
|
-
if (!name.endsWith('.md')) return null
|
|
35
|
-
const base = name.slice(0, -'.md'.length)
|
|
36
|
-
for (const [suffix, kind] of PLAIN) {
|
|
37
|
-
if (base.endsWith(suffix)) return { stem: base.slice(0, -suffix.length), kind }
|
|
38
|
-
}
|
|
39
|
-
for (const [prefix, kind] of QID) {
|
|
40
|
-
const i = base.lastIndexOf(prefix)
|
|
41
|
-
if (i !== -1) return { stem: base.slice(0, i), kind, qid: base.slice(i + prefix.length) }
|
|
42
|
-
}
|
|
43
|
-
return null
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Парсить inline-список `[A, B]` із front-matter у масив id.
|
|
48
|
-
* @param {string | null | undefined} value значення поля
|
|
49
|
-
* @returns {string[]} елементи (trim, без порожніх)
|
|
50
|
-
*/
|
|
51
|
-
export function parseIdList(value) {
|
|
52
|
-
if (typeof value !== 'string') return []
|
|
53
|
-
return value
|
|
54
|
-
.replace('[', '')
|
|
55
|
-
.replace(']', '')
|
|
56
|
-
.split(',')
|
|
57
|
-
.map(s => s.trim())
|
|
58
|
-
.filter(Boolean)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Сканує вузли графа: групує файли по stem, читає plan/fact front-matter.
|
|
63
|
-
* @param {string} root корінь репо
|
|
64
|
-
* @param {string} graph id графа (каталог `docs/graphs/<graph>`)
|
|
65
|
-
* @param {{ readdir?: (dir: string) => string[], readFile?: (file: string) => string }} [deps] ін'єкції FS
|
|
66
|
-
* @returns {{ id: string, slug: string, dependsOn: string[], owner: string | null, hasClaim: boolean, hasFact: boolean, factStatus: string | null, asks: string[], answered: string[] }[]} вузли
|
|
67
|
-
*/
|
|
68
|
-
export function scanGraph(root, graph, deps = {}) {
|
|
69
|
-
const readdir = deps.readdir ?? (dir => (existsSync(dir) ? readdirSync(dir) : []))
|
|
70
|
-
const readFile = deps.readFile ?? (file => readFileSync(file, 'utf8'))
|
|
71
|
-
const dir = join(root, 'docs', 'graphs', graph, 'nodes')
|
|
72
|
-
|
|
73
|
-
const byStem = new Map()
|
|
74
|
-
const ensure = stem => {
|
|
75
|
-
if (!byStem.has(stem)) {
|
|
76
|
-
byStem.set(stem, {
|
|
77
|
-
stem,
|
|
78
|
-
id: stem.split('-')[0],
|
|
79
|
-
slug: stem.slice(stem.indexOf('-') + 1),
|
|
80
|
-
dependsOn: [],
|
|
81
|
-
owner: null,
|
|
82
|
-
hasClaim: false,
|
|
83
|
-
hasFact: false,
|
|
84
|
-
factStatus: null,
|
|
85
|
-
asks: [],
|
|
86
|
-
answered: []
|
|
87
|
-
})
|
|
88
|
-
}
|
|
89
|
-
return byStem.get(stem)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
for (const name of readdir(dir)) {
|
|
93
|
-
const art = classifyArtifact(name)
|
|
94
|
-
if (!art) continue
|
|
95
|
-
const node = ensure(art.stem)
|
|
96
|
-
switch (art.kind) {
|
|
97
|
-
case 'plan': {
|
|
98
|
-
const fm = parseFrontMatter(readFile(join(dir, name))) ?? {}
|
|
99
|
-
node.id = fm.id ?? node.id
|
|
100
|
-
node.dependsOn = parseIdList(fm.dependsOn)
|
|
101
|
-
node.owner = fm.owner ?? null
|
|
102
|
-
break
|
|
103
|
-
}
|
|
104
|
-
case 'claim': {
|
|
105
|
-
node.hasClaim = true
|
|
106
|
-
break
|
|
107
|
-
}
|
|
108
|
-
case 'fact': {
|
|
109
|
-
const fm = parseFrontMatter(readFile(join(dir, name))) ?? {}
|
|
110
|
-
node.hasFact = true
|
|
111
|
-
node.factStatus = fm.status ?? 'done'
|
|
112
|
-
break
|
|
113
|
-
}
|
|
114
|
-
case 'ask': {
|
|
115
|
-
node.asks.push(art.qid)
|
|
116
|
-
break
|
|
117
|
-
}
|
|
118
|
-
case 'ans': {
|
|
119
|
-
node.answered.push(art.qid)
|
|
120
|
-
break
|
|
121
|
-
}
|
|
122
|
-
// no default
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return [...byStem.values()]
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Деривує статус одного вузла (чиста).
|
|
130
|
-
* @param {{ hasFact: boolean, factStatus: string | null, hasClaim: boolean, asks: string[], answered: string[], dependsOn: string[] }} node вузол
|
|
131
|
-
* @param {Set<string>} doneSet id вузлів зі статусом done
|
|
132
|
-
* @returns {'done' | 'failed' | 'awaiting-human' | 'in_progress' | 'ready' | 'blocked'} статус
|
|
133
|
-
*/
|
|
134
|
-
export function deriveStatus(node, doneSet) {
|
|
135
|
-
if (node.hasFact) return node.factStatus === 'failed' ? 'failed' : 'done'
|
|
136
|
-
const openAsk = node.asks.some(q => !node.answered.includes(q))
|
|
137
|
-
if (node.hasClaim && openAsk) return 'awaiting-human'
|
|
138
|
-
if (node.hasClaim) return 'in_progress'
|
|
139
|
-
if (node.dependsOn.every(d => doneSet.has(d))) return 'ready'
|
|
140
|
-
return 'blocked'
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Деривує статуси всіх вузлів графа (спершу doneSet із fact done).
|
|
145
|
-
* @param {object[]} nodes вузли зі `scanGraph`
|
|
146
|
-
* @returns {object[]} вузли з полем `status`
|
|
147
|
-
*/
|
|
148
|
-
export function deriveGraph(nodes) {
|
|
149
|
-
const doneSet = new Set(nodes.filter(n => n.hasFact && n.factStatus !== 'failed').map(n => n.id))
|
|
150
|
-
return nodes.map(n => ({ ...n, status: deriveStatus(n, doneSet) }))
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Текстовий рендер позиції графа.
|
|
155
|
-
* @param {string} graph id графа
|
|
156
|
-
* @param {object[]} nodes вузли з полем `status`
|
|
157
|
-
* @returns {string} людино-читабельна таблиця
|
|
158
|
-
*/
|
|
159
|
-
export function renderGraph(graph, nodes) {
|
|
160
|
-
if (nodes.length === 0) return `граф ${graph}: вузлів не знайдено`
|
|
161
|
-
const order = ['in_progress', 'awaiting-human', 'ready', 'blocked', 'failed', 'done']
|
|
162
|
-
const counts = order
|
|
163
|
-
.map(s => [s, nodes.filter(n => n.status === s).length])
|
|
164
|
-
.filter(([, c]) => c > 0)
|
|
165
|
-
.map(([s, c]) => `${s}:${c}`)
|
|
166
|
-
.join(' ')
|
|
167
|
-
const lines = [`граф ${graph} — ${counts}`]
|
|
168
|
-
for (const n of nodes) {
|
|
169
|
-
const owner = n.owner ? ` ${n.owner}` : ''
|
|
170
|
-
const deps = n.dependsOn.length > 0 ? ` ←[${n.dependsOn.join(',')}]` : ''
|
|
171
|
-
lines.push(` ${n.id} · ${n.slug} [${n.status}]${owner}${deps}`)
|
|
172
|
-
}
|
|
173
|
-
return lines.join('\n')
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Перелік графів (каталоги в `docs/graphs/`).
|
|
178
|
-
* @param {string} root корінь репо
|
|
179
|
-
* @param {(dir: string) => string[]} readdir інжектована readdir
|
|
180
|
-
* @returns {string[]} id графів
|
|
181
|
-
*/
|
|
182
|
-
function listGraphs(root, readdir) {
|
|
183
|
-
return readdir(join(root, 'docs', 'graphs'))
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* CLI `n-cursor graph <status> [graph]`. Read-only.
|
|
188
|
-
* @param {string[]} args аргументи після `graph`
|
|
189
|
-
* @param {{ cwd?: string, readdir?: (dir: string) => string[], readFile?: (file: string) => string, log?: (m: string) => void }} [deps] ін'єкції
|
|
190
|
-
* @returns {number} exit code (0 ok, 1 невідома підкоманда)
|
|
191
|
-
*/
|
|
192
|
-
export function runGraphCli(args, deps = {}) {
|
|
193
|
-
const root = deps.cwd ?? processCwd()
|
|
194
|
-
const readdir = deps.readdir ?? (dir => (existsSync(dir) ? readdirSync(dir) : []))
|
|
195
|
-
const log = deps.log ?? console.log
|
|
196
|
-
const [sub, graphArg] = args
|
|
197
|
-
|
|
198
|
-
if (sub !== 'status') {
|
|
199
|
-
log('Usage: n-cursor graph status [<graph>]')
|
|
200
|
-
return 1
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const graphs = graphArg ? [graphArg] : listGraphs(root, readdir)
|
|
204
|
-
if (graphs.length === 0) {
|
|
205
|
-
log('graph: у docs/graphs/ немає графів')
|
|
206
|
-
return 0
|
|
207
|
-
}
|
|
208
|
-
for (const g of graphs) {
|
|
209
|
-
log(renderGraph(g, deriveGraph(scanGraph(root, g, { readdir, readFile: deps.readFile }))))
|
|
210
|
-
}
|
|
211
|
-
return 0
|
|
212
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CLI-диспетчер `n-cursor flow` (думка.MD — протокол всередині вузла графу).
|
|
3
|
-
*
|
|
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>`
|
|
10
|
-
*/
|
|
11
|
-
import { cmdPlan as plan } from './graph/lib/cmd-plan.mjs'
|
|
12
|
-
import { cmdVerify as verify } from './graph/lib/cmd-verify.mjs'
|
|
13
|
-
import { cmdAudit as audit, cmdDone as done, cmdFailed as failed, cmdSpawn as spawn } from './graph/lib/cmd-signals.mjs'
|
|
14
|
-
|
|
15
|
-
const USAGE = [
|
|
16
|
-
'Usage:',
|
|
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>'
|
|
23
|
-
].join('\n')
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* @type {Record<string, (rest: string[], deps: object) => Promise<number>>}
|
|
27
|
-
*/
|
|
28
|
-
export const DEFAULT_HANDLERS = { plan, verify, done, audit, failed, spawn }
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Точка входу `case 'flow'` у `bin/n-cursor.js`. Парсить підкоманду й
|
|
32
|
-
* маршрутизує до handler-а. Невідома/відсутня підкоманда → usage + код 1.
|
|
33
|
-
* @param {string[]} args аргументи після `flow`
|
|
34
|
-
* @param {{ handlers?: Record<string, (rest: string[], deps: object) => Promise<number>> }} [deps] ін'єкція handler-ів (для тестів)
|
|
35
|
-
* @returns {Promise<number>} exit code
|
|
36
|
-
*/
|
|
37
|
-
export async function runFlowCli(args, deps = {}) {
|
|
38
|
-
const [sub, ...rest] = args
|
|
39
|
-
const handlers = deps.handlers ?? DEFAULT_HANDLERS
|
|
40
|
-
if (!sub || !Object.hasOwn(handlers, sub)) {
|
|
41
|
-
console.error(USAGE)
|
|
42
|
-
return 1
|
|
43
|
-
}
|
|
44
|
-
return await handlers[sub](rest, deps)
|
|
45
|
-
}
|
|
@@ -1,348 +0,0 @@
|
|
|
1
|
-
# active.mjs — Активний Раннер flow-диспетчера
|
|
2
|
-
|
|
3
|
-
## Огляд
|
|
4
|
-
|
|
5
|
-
Модуль `active.mjs` реалізує **Активний Раннер** диспетчера потоків (Фасад B, spec §8.1) — повний 5-фазний життєвий цикл автоматизованого виконання задачі в ізольованому git-worktree. Він зшиває чотири підсистеми в одну CLI-команду:
|
|
6
|
-
|
|
7
|
-
1. `ensureWorktree` — підготовка ізольованого worktree під цільову гілку;
|
|
8
|
-
2. `planner.generatePlan` — побудова покрокового плану через LLM-runner;
|
|
9
|
-
3. `executor.executePlan` — послідовне виконання кроків з verify/commit;
|
|
10
|
-
4. `reviewer.runReview` — gate-перевірки (verify) після кожного кроку.
|
|
11
|
-
|
|
12
|
-
Усі IO-залежності (`runner`, `verify`, `commit`, `run`, `now`, `log`) **ін'єктуються** через об'єкт `deps`, тож модуль повністю тестується без реальних LLM, git-операцій чи gate-команд. У автономному режимі (`--autonomous`) runner додатково обгортається `withBudget` (§9.4) для жорсткого обмеження API-викликів і вартості.
|
|
13
|
-
|
|
14
|
-
Експортується чотири CLI-обробники:
|
|
15
|
-
|
|
16
|
-
- `run` — повний цикл build (ensureWorktree → план → executor);
|
|
17
|
-
- `resume` — продовження з чекпойнта зі скиданням часткового доробку (safe-resume, §4.1.7);
|
|
18
|
-
- `cancel` — прибирання transient sibling-файлів стану;
|
|
19
|
-
- `repair` — діагностика стану або жорстке скидання робочого дерева до HEAD.
|
|
20
|
-
|
|
21
|
-
Файл стану (`flow.json`), журнал подій (`flow.events.jsonl`) та lock-файли розташовуються поруч із worktree-кореневою текою; шляхи до них формують `flowStatePath` / `flowEventsPath` зі `state-store.mjs` та `events.mjs`.
|
|
22
|
-
|
|
23
|
-
## Експорти / API
|
|
24
|
-
|
|
25
|
-
| Експорт | Тип | Призначення |
|
|
26
|
-
| ---------------------- | ---------------- | ----------------------------------------------------------------- |
|
|
27
|
-
| `run(rest, deps?)` | `async function` | Команда `flow run` — повний цикл build (планування + виконання). |
|
|
28
|
-
| `resume(_rest, deps?)` | `async function` | Команда `flow resume` — продовжити з останнього чекпойнта. |
|
|
29
|
-
| `cancel(_rest, deps?)` | `async function` | Команда `flow cancel` — прибрати транзитні sibling-и стану. |
|
|
30
|
-
| `repair(rest, deps?)` | `async function` | Команда `flow repair [--discard-step-work]` — fail-closed escape. |
|
|
31
|
-
|
|
32
|
-
Усі експорти повертають `Promise<number>` — exit code процесу:
|
|
33
|
-
|
|
34
|
-
- `0` — успіх (`done` або no-op);
|
|
35
|
-
- `1` — fail (помилка, нема стану/плану, бюджет вичерпано, стан пошкоджено);
|
|
36
|
-
- `2` — `blocked-on-human` (потрібне HITL-втручання).
|
|
37
|
-
|
|
38
|
-
### Внутрішні (не експортовані) helper-и
|
|
39
|
-
|
|
40
|
-
| Ім'я | Призначення |
|
|
41
|
-
| ------------------------- | ------------------------------------------------------------------------------------ |
|
|
42
|
-
| `defaultCommit(cwd, msg)` | Дефолтний commit-стратег: `git add -A && git commit -m <msg>` у worktree. |
|
|
43
|
-
| `defaultVerify(cwd)` | Дефолтний verify: проганяє `runReview` з реальним `run` і `fingerprint: () => null`. |
|
|
44
|
-
| `readFlowAutonomous(cwd)` | Зчитує секцію `flow.autonomous` з `.n-cursor.json` (бюджет автономки). |
|
|
45
|
-
|
|
46
|
-
## Функції
|
|
47
|
-
|
|
48
|
-
### `defaultCommit(cwd, msg)`
|
|
49
|
-
|
|
50
|
-
**Сигнатура:** `function defaultCommit(cwd: string, msg: string): void`
|
|
51
|
-
|
|
52
|
-
**Параметри:**
|
|
53
|
-
|
|
54
|
-
- `cwd` — абсолютний шлях до worktree;
|
|
55
|
-
- `msg` — повідомлення коміту.
|
|
56
|
-
|
|
57
|
-
**Повертає:** `void`.
|
|
58
|
-
|
|
59
|
-
**Side effects:** виконує два синхронних `spawnSync('git', …)`-виклики у вказаному `cwd`:
|
|
60
|
-
|
|
61
|
-
1. `git add -A` — індексує всі зміни;
|
|
62
|
-
2. `git commit -m <msg>` — створює коміт.
|
|
63
|
-
|
|
64
|
-
Помилки git **не пробрасуються** — exit code spawnSync ігнорується (виклик «оптимістичний»).
|
|
65
|
-
|
|
66
|
-
### `defaultVerify(cwd)`
|
|
67
|
-
|
|
68
|
-
**Сигнатура:** `function defaultVerify(cwd: string): { pass: boolean, failedOutput: string | null }`
|
|
69
|
-
|
|
70
|
-
**Параметри:**
|
|
71
|
-
|
|
72
|
-
- `cwd` — корінь worktree, де крутитимуться gate-команди.
|
|
73
|
-
|
|
74
|
-
**Повертає:** verdict від `runReview` — об'єкт із полями `pass: boolean` та `failedOutput: string | null`.
|
|
75
|
-
|
|
76
|
-
**Side effects:** делегує `runReview` зі `./reviewer.mjs`, передаючи реальний `run = realRun` (синхронний spawn зі `./commands.mjs`) та `fingerprint: () => null` (порівняння за хешем артефактів вимкнено).
|
|
77
|
-
|
|
78
|
-
### `readFlowAutonomous(cwd)`
|
|
79
|
-
|
|
80
|
-
**Сигнатура:** `function readFlowAutonomous(cwd: string): { maxApiCalls?: number, maxCostUsd?: number, onBudgetExceeded?: string }`
|
|
81
|
-
|
|
82
|
-
**Параметри:**
|
|
83
|
-
|
|
84
|
-
- `cwd` — корінь проєкту, де лежить `.n-cursor.json`.
|
|
85
|
-
|
|
86
|
-
**Повертає:** об'єкт із секції `flow.autonomous` конфігу або порожній `{}`, якщо файл відсутній/невалідний.
|
|
87
|
-
|
|
88
|
-
**Side effects:** **синхронно** читає `<cwd>/.n-cursor.json` через `readFileSync`. Будь-яка помилка (відсутній файл, невалідний JSON) глушиться `try/catch` → повертається `{}`.
|
|
89
|
-
|
|
90
|
-
### `run(rest, deps?)`
|
|
91
|
-
|
|
92
|
-
**Сигнатура:**
|
|
93
|
-
|
|
94
|
-
```
|
|
95
|
-
async function run(
|
|
96
|
-
rest: string[],
|
|
97
|
-
deps?: {
|
|
98
|
-
runner?: object,
|
|
99
|
-
verify?: (cwd: string) => { pass: boolean, failedOutput: string | null },
|
|
100
|
-
commit?: (cwd: string, msg: string) => void,
|
|
101
|
-
run?: (cmd: string, args: string[], opts: object) => object,
|
|
102
|
-
autonomous?: boolean,
|
|
103
|
-
budget?: { maxApiCalls?: number, maxCostUsd?: number, onBudgetExceeded?: string },
|
|
104
|
-
cwd?: string,
|
|
105
|
-
log?: (m: string) => void,
|
|
106
|
-
now?: () => number
|
|
107
|
-
}
|
|
108
|
-
): Promise<number>
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
**Параметри:**
|
|
112
|
-
|
|
113
|
-
- `rest` — позиційні аргументи CLI, можуть містити прапор `--autonomous` плюс `<branch> <task...>`;
|
|
114
|
-
- `deps` — об'єкт ін'єкцій:
|
|
115
|
-
- `runner` — готовий subagent-runner (інакше створюється через `createRunner(deps)`);
|
|
116
|
-
- `verify` — кастомний verify-callback (дефолт: `defaultVerify`);
|
|
117
|
-
- `commit` — кастомний commit-callback (дефолт: `defaultCommit`);
|
|
118
|
-
- `run` — низькорівневий spawn-аналог (пробрасується далі в `ensureWorktree` / `createRunner`);
|
|
119
|
-
- `autonomous` — примусово вмикає budget guard незалежно від `rest`;
|
|
120
|
-
- `budget` — явний конфіг бюджету (інакше читається з `.n-cursor.json`);
|
|
121
|
-
- `cwd` — корінь проєкту для читання конфігу (дефолт: `process.cwd()`);
|
|
122
|
-
- `log` — логер (дефолт: `console.error`);
|
|
123
|
-
- `now` — постачальник часу (дефолт: `Date.now`).
|
|
124
|
-
|
|
125
|
-
**Повертає:** `Promise<number>` — exit code:
|
|
126
|
-
|
|
127
|
-
- `0` — `result.status === 'done'`;
|
|
128
|
-
- `1` — `ensureWorktree` повернув ненульовий код, runner не створився, або executor дав помилку / `BudgetExceeded` / інший fail-стан;
|
|
129
|
-
- `2` — `result.status === 'blocked-on-human'`.
|
|
130
|
-
|
|
131
|
-
**Потік:**
|
|
132
|
-
|
|
133
|
-
1. Підготовка: `log`, `now`, прапор `autonomous` (з `deps.autonomous` або `rest.includes('--autonomous')`), позиційні аргументи (фільтр без `--`-флагів).
|
|
134
|
-
2. `ensureWorktree(positional, deps)` → якщо `code !== 0`, повертає цей же код. Інакше отримуємо `{ worktreeDir, branch, desc, baseCommit }`.
|
|
135
|
-
3. `writeState(statePath, …)` — ініціальний стан із `status: 'in_progress'`, `started_at` у ISO, `metadata.base_commit`, порожнім `plan`.
|
|
136
|
-
4. Створення runner-а: `deps.runner ?? await createRunner(deps)`. Якщо `createRunner` кинув — лог `run: <msg>` і `return 1`.
|
|
137
|
-
5. Якщо `autonomous`: `runner = withBudget(runner, { maxApiCalls: budget.maxApiCalls, log })`.
|
|
138
|
-
6. **try-блок:**
|
|
139
|
-
- `plan = await generatePlan({ runner, task: desc, cwd: worktreeDir })`;
|
|
140
|
-
- `updateState(statePath, s => ({ ...s, plan }))`;
|
|
141
|
-
- `result = await executePlan({ statePath, eventsPath: flowEventsPath(worktreeDir) }, { runner, verify, commit, cwd: worktreeDir, log, now })`;
|
|
142
|
-
- якщо `result.status === 'done'` — лог `'run: build done — далі \`flow release\`'`, `return 0`;
|
|
143
|
-
- якщо `result.status === 'blocked-on-human'` — лог `run: blocked-on-human на кроці <step>`, `return 2`;
|
|
144
|
-
- інакше `return 1`.
|
|
145
|
-
7. **catch:** якщо `error instanceof BudgetExceeded` — лог `run: <msg> — abort`, оновлення стану на `status: 'failed'`, `return 1`. Будь-яка інша помилка — лог і `return 1`.
|
|
146
|
-
|
|
147
|
-
**Side effects:** створення worktree, запис/оновлення `flow.json`, запис подій у `flow.events.jsonl`, виклики LLM-runner, commit-и в worktree, виконання verify-gate-ів.
|
|
148
|
-
|
|
149
|
-
### `resume(_rest, deps?)`
|
|
150
|
-
|
|
151
|
-
**Сигнатура:**
|
|
152
|
-
|
|
153
|
-
```
|
|
154
|
-
async function resume(
|
|
155
|
-
_rest: string[],
|
|
156
|
-
deps?: {
|
|
157
|
-
runner?: object,
|
|
158
|
-
verify?: (cwd: string) => object,
|
|
159
|
-
commit?: (cwd: string, msg: string) => void,
|
|
160
|
-
run?: (cmd: string, args: string[], opts: object) => object,
|
|
161
|
-
cwd?: string,
|
|
162
|
-
log?: (m: string) => void,
|
|
163
|
-
now?: () => number
|
|
164
|
-
}
|
|
165
|
-
): Promise<number>
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
**Параметри:** `_rest` ігнорується; `deps` — як у `run`, мінус `autonomous`/`budget`.
|
|
169
|
-
|
|
170
|
-
**Повертає:** `0` / `1` / `2` — як у `run`.
|
|
171
|
-
|
|
172
|
-
**Потік (Safe-resume, §4.1.7):**
|
|
173
|
-
|
|
174
|
-
1. `cwd = deps.cwd ?? process.cwd()`, `log`, `now`, `run_ = deps.run ?? realRun`.
|
|
175
|
-
2. `state = readState(flowStatePath(cwd))`. Якщо `state` falsy — лог `'resume: стану нема'`, `return 1`.
|
|
176
|
-
3. `openHitl = (state.hitl ?? []).filter(q => !q.answer)`. Якщо `status === 'blocked-on-human'` і `openHitl.length > 0` — лог `resume: ще blocked — N відкритих HITL-питань (заповни answer і повтори)`, `return 2`.
|
|
177
|
-
4. Якщо `!state.plan?.length` — лог `'resume: нема плану'`, `return 1`.
|
|
178
|
-
5. **Скидання часткового доробку:** `run_('git', ['reset', '--hard', 'HEAD'], { cwd })`.
|
|
179
|
-
6. **HITL-злиття:** із відповідей будується `Map<step, answer>`; план переписується так, що **завершені** кроки (`status === 'done'`) не зачіпаються, а **інші** дістають `retry_count: 0` і — якщо є відповідь на цей крок — поле `hint: <answer>`. HITL-питання з відповіддю переводяться у `status: 'answered'`.
|
|
180
|
-
7. Створення runner-а: `deps.runner ?? await createRunner(deps)` (на помилку — `return 1`).
|
|
181
|
-
8. `executePlan(...)` з тими ж дефолтами verify/commit, що й у `run`.
|
|
182
|
-
9. Розбір `result.status`: `done → 0`, `blocked-on-human → 2`, інше → `1`.
|
|
183
|
-
|
|
184
|
-
**Side effects:** `git reset --hard HEAD` (потенційно деструктивно для незакомічених змін!), мутація `flow.json`, виклики LLM, commit-и, verify.
|
|
185
|
-
|
|
186
|
-
### `cancel(_rest, deps?)`
|
|
187
|
-
|
|
188
|
-
**Сигнатура:**
|
|
189
|
-
|
|
190
|
-
```
|
|
191
|
-
async function cancel(
|
|
192
|
-
_rest: string[],
|
|
193
|
-
deps?: { cwd?: string, log?: (m: string) => void }
|
|
194
|
-
): Promise<number>
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
**Параметри:** `_rest` ігнорується; `deps.cwd` (дефолт: `process.cwd()`), `deps.log` (дефолт: `console.error`).
|
|
198
|
-
|
|
199
|
-
**Повертає:** завжди `0`.
|
|
200
|
-
|
|
201
|
-
**Потік:** виклик `cleanupFlowSiblings(cwd)` (зі `./state-store.mjs`) → лог `'cancel: стан і sibling-и прибрано'` → `return 0`.
|
|
202
|
-
|
|
203
|
-
**Side effects:** видалення `flow.json`, `flow.events.jsonl`, lock-файлів навколо worktree.
|
|
204
|
-
|
|
205
|
-
### `repair(rest, deps?)`
|
|
206
|
-
|
|
207
|
-
**Сигнатура:**
|
|
208
|
-
|
|
209
|
-
```
|
|
210
|
-
async function repair(
|
|
211
|
-
rest: string[],
|
|
212
|
-
deps?: {
|
|
213
|
-
run?: (cmd: string, args: string[], opts: object) => object,
|
|
214
|
-
cwd?: string,
|
|
215
|
-
log?: (m: string) => void
|
|
216
|
-
}
|
|
217
|
-
): Promise<number>
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
**Параметри:**
|
|
221
|
-
|
|
222
|
-
- `rest` — аргументи CLI; перевіряється наявність `--discard-step-work`;
|
|
223
|
-
- `deps.run` — низькорівневий spawn (дефолт: `realRun`);
|
|
224
|
-
- `deps.cwd` — корінь worktree (дефолт: `process.cwd()`);
|
|
225
|
-
- `deps.log` — логер (дефолт: `console.error`).
|
|
226
|
-
|
|
227
|
-
**Повертає:** `0` — у двох випадках: успішне жорстке скидання або валідне читання стану (включно з «стану нема»); `1` — стан пошкоджено (виняток при `readState`).
|
|
228
|
-
|
|
229
|
-
**Потік:**
|
|
230
|
-
|
|
231
|
-
1. Якщо `rest.includes('--discard-step-work')`:
|
|
232
|
-
- `run_('git', ['reset', '--hard', 'HEAD'], { cwd })`;
|
|
233
|
-
- лог `'repair: робоче дерево скинуто до HEAD (--discard-step-work)'`;
|
|
234
|
-
- `return 0`.
|
|
235
|
-
2. Інакше try-блок: `state = readState(flowStatePath(cwd))`. Лог `repair: стан валідний (status: <s.status>)` (якщо state truthy) або `'repair: стану нема'` (якщо falsy). `return 0`.
|
|
236
|
-
3. На помилку читання — лог `repair: стан пошкоджено — <msg>. Спробуй \`flow repair --discard-step-work\` або \`flow cancel\`.`, `return 1`.
|
|
237
|
-
|
|
238
|
-
**Side effects:** опціональний `git reset --hard HEAD` (деструктивно), синхронне читання файлу стану.
|
|
239
|
-
|
|
240
|
-
## Залежності
|
|
241
|
-
|
|
242
|
-
### Стандартна бібліотека Node.js
|
|
243
|
-
|
|
244
|
-
- `node:child_process` → `spawnSync` — синхронні git-команди в `defaultCommit`.
|
|
245
|
-
- `node:fs` → `readFileSync` — читання `.n-cursor.json` у `readFlowAutonomous`.
|
|
246
|
-
- `node:path` → `join` — побудова шляху до конфігу.
|
|
247
|
-
- `node:process` → `cwd as processCwd` — дефолтний робочий каталог.
|
|
248
|
-
|
|
249
|
-
### Внутрішні модулі (relative imports)
|
|
250
|
-
|
|
251
|
-
- `./budget.mjs` → `BudgetExceeded`, `withBudget` — клас винятка для перевищення бюджету та обгортка runner-а з guard-ом (§9.4).
|
|
252
|
-
- `./commands.mjs` → `ensureWorktree`, `realRun` — підготовка worktree та реальний spawn-runner.
|
|
253
|
-
- `./events.mjs` → `flowEventsPath` — шлях до журналу подій (`flow.events.jsonl`).
|
|
254
|
-
- `./executor.mjs` → `executePlan` — послідовне виконання плану з verify/commit/HITL.
|
|
255
|
-
- `./planner.mjs` → `generatePlan` — побудова плану через LLM-runner на основі опису задачі.
|
|
256
|
-
- `./reviewer.mjs` → `runReview` — запуск gate-перевірок (verify-callback за замовчуванням).
|
|
257
|
-
- `./state-store.mjs` → `cleanupFlowSiblings`, `flowStatePath`, `readState`, `updateState`, `writeState` — IO навколо `flow.json` та sibling-файлів.
|
|
258
|
-
- `./subagent-runner.mjs` → `createRunner` — фабрика LLM-runner-а (Claude/інший subagent) з ін'єкцій.
|
|
259
|
-
|
|
260
|
-
## Потік виконання / Використання
|
|
261
|
-
|
|
262
|
-
### CLI-команди (диспатчиться зовнішнім роутером)
|
|
263
|
-
|
|
264
|
-
```
|
|
265
|
-
flow run [--autonomous] <branch> "<task...>"
|
|
266
|
-
flow resume
|
|
267
|
-
flow cancel
|
|
268
|
-
flow repair [--discard-step-work]
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
### Сценарій `flow run --autonomous feat/x "Add tests"`
|
|
272
|
-
|
|
273
|
-
1. **ensureWorktree** створює (або підхоплює) worktree під гілку `feat/x`, повертає `worktreeDir`, базовий комміт, текстовий опис задачі (`desc`).
|
|
274
|
-
2. У worktree пишеться початковий `flow.json` зі `status: 'in_progress'`, `started_at`, `metadata.base_commit`, порожнім планом.
|
|
275
|
-
3. Створюється `runner` (Claude-subagent). У `--autonomous` режимі він обгортається `withBudget` з `maxApiCalls` з `.n-cursor.json` → `flow.autonomous`.
|
|
276
|
-
4. `generatePlan` через LLM-runner будує покроковий план; план зберігається в стані.
|
|
277
|
-
5. `executePlan` ітерує план: для кожного кроку викликає runner-а → verify-gate → commit (через `defaultCommit`); події логуються у `flow.events.jsonl`; стан оновлюється.
|
|
278
|
-
6. Якщо verify падає, executor може відкрити HITL-питання → стан переходить у `blocked-on-human`, `run` повертає `2`.
|
|
279
|
-
7. Якщо `withBudget` вистрелив `BudgetExceeded` — стан стає `failed`, exit `1`.
|
|
280
|
-
|
|
281
|
-
### Сценарій `flow resume`
|
|
282
|
-
|
|
283
|
-
Користувач заповнив `answer` у HITL-питаннях `flow.json` і запускає `flow resume`:
|
|
284
|
-
|
|
285
|
-
1. Зчитується стан, відкриті (без `answer`) HITL → блокує `resume` з кодом `2`.
|
|
286
|
-
2. `git reset --hard HEAD` повертає робоче дерево до останнього коміту (відкочуємо частковий доробок невдалого кроку).
|
|
287
|
-
3. HITL-відповіді стають полями `hint` для відповідних кроків, `retry_count` обнуляється для незавершених кроків.
|
|
288
|
-
4. `executePlan` стартує з того ж списку, але вже з підказками.
|
|
289
|
-
|
|
290
|
-
### Сценарій `flow cancel`
|
|
291
|
-
|
|
292
|
-
Прибирання залишків:
|
|
293
|
-
|
|
294
|
-
- Видаляється `flow.json`, `flow.events.jsonl`, lock-файли — через `cleanupFlowSiblings(cwd)`.
|
|
295
|
-
- Worktree як такий **не видаляється** — це окрема відповідальність команд worktree-менеджмента.
|
|
296
|
-
|
|
297
|
-
### Сценарій `flow repair`
|
|
298
|
-
|
|
299
|
-
- Без аргументів — діагностика: чи стан читається валідно;
|
|
300
|
-
- `--discard-step-work` — жорсткий `git reset --hard HEAD` (свідома втрата незакомічених змін).
|
|
301
|
-
|
|
302
|
-
### Тестування
|
|
303
|
-
|
|
304
|
-
Усе IO ін'єктується через `deps`:
|
|
305
|
-
|
|
306
|
-
```js
|
|
307
|
-
import { run } from './active.mjs'
|
|
308
|
-
|
|
309
|
-
const fakeRunner = {
|
|
310
|
-
/* mock */
|
|
311
|
-
}
|
|
312
|
-
const code = await run(['feat/x', 'task'], {
|
|
313
|
-
runner: fakeRunner,
|
|
314
|
-
verify: () => ({ pass: true, failedOutput: null }),
|
|
315
|
-
commit: () => {},
|
|
316
|
-
run: () => ({ status: 0, stdout: '', stderr: '' }),
|
|
317
|
-
cwd: '/tmp/proj',
|
|
318
|
-
log: () => {},
|
|
319
|
-
now: () => 0
|
|
320
|
-
})
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
Це дозволяє тестам не торкатись реальних `git`, LLM чи gate-команд — лише перевіряти контракт переходів стану, exit-коди й послідовність викликів.
|
|
324
|
-
|
|
325
|
-
## Rebuild Test
|
|
326
|
-
|
|
327
|
-
Цей розділ верифікує, що документація відтворює поведінку файлу без звертання до самого `active.mjs`:
|
|
328
|
-
|
|
329
|
-
1. Чотири експорти модуля: `run`, `resume`, `cancel`, `repair` — усі `async`, усі повертають exit code (`0`/`1`/`2`).
|
|
330
|
-
2. У `run` спочатку викликається `ensureWorktree(positional, deps)`; якщо його `code !== 0`, цей самий код повертається без подальших дій.
|
|
331
|
-
3. Прапор `--autonomous` визначається як `deps.autonomous ?? rest.includes('--autonomous')`; у позиційні аргументи `--`-флаги **не** потрапляють (фільтруються).
|
|
332
|
-
4. Конфіг бюджету в `--autonomous` режимі береться з `deps.budget` або з `.n-cursor.json` → `flow.autonomous` (порожній `{}` при будь-якій помилці читання).
|
|
333
|
-
5. Початковий стан у `run` має поля `branch`, `status: 'in_progress'`, `started_at` (ISO від `now()`), `metadata.base_commit`, `plan: []`.
|
|
334
|
-
6. Помилка створення runner-а в `run`/`resume` → лог із префіксом `run:` / `resume:` і `return 1`.
|
|
335
|
-
7. У try-блоці `run` спочатку `generatePlan`, потім `updateState` з планом, потім `executePlan`; усі три отримують `runner` та `worktreeDir`.
|
|
336
|
-
8. Розбір `result.status` у `run` і `resume`: `done → 0`, `blocked-on-human → 2`, інше → `1`.
|
|
337
|
-
9. `BudgetExceeded` у `run`: лог `<msg> — abort`, `updateState(... status: 'failed')`, `return 1`.
|
|
338
|
-
10. `resume` блокує (exit `2`), якщо стан `blocked-on-human` і є HITL без `answer`.
|
|
339
|
-
11. `resume` робить `git reset --hard HEAD` **перед** запуском runner-а — для скидання часткового доробку.
|
|
340
|
-
12. У `resume` HITL-відповіді перетворюються на `hint` тільки для **незавершених** кроків; завершені (`status === 'done'`) не чіпаються.
|
|
341
|
-
13. HITL-питання з `answer` отримують `status: 'answered'` у новому стані.
|
|
342
|
-
14. `cancel` завжди повертає `0`; викликає `cleanupFlowSiblings(cwd)` і логує `'cancel: стан і sibling-и прибрано'`.
|
|
343
|
-
15. `repair --discard-step-work` робить `git reset --hard HEAD` і повертає `0`.
|
|
344
|
-
16. `repair` без аргументів: успішно зчитує стан (або `null`) → `0`; помилка читання → лог із префіксом `repair: стан пошкоджено —` та посиланням на `flow repair --discard-step-work` / `flow cancel`, `return 1`.
|
|
345
|
-
17. `defaultCommit` робить два `spawnSync('git', …)` у `cwd`: `add -A`, потім `commit -m <msg>`.
|
|
346
|
-
18. `defaultVerify` делегує `runReview({ run: realRun, cwd, fingerprint: () => null })`.
|
|
347
|
-
19. Усі логери дефолтяться на `console.error`; усі джерела часу — на `Date.now`; усі `cwd` — на `process.cwd()`.
|
|
348
|
-
20. Лог-повідомлення мають детерміновані префікси: `run:`, `resume:`, `cancel:`, `repair:` — це публічний контракт CLI-виводу для тестів і користувача.
|