@nitra/cursor 3.28.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 +13 -0
- package/package.json +1 -3
- package/scripts/coverage-classify/index.mjs +60 -72
- package/scripts/coverage-fix.mjs +26 -23
- 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/dispatcher/lib/subagent-runner.mjs +33 -102
- package/scripts/worktree-cli.mjs +2 -2
- package/skills/docgen/js/docgen-gen.mjs +54 -178
- package/skills/fix/js/llm-worker.mjs +4 -4
- 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
|
@@ -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
|
+
}
|
|
@@ -1,122 +1,53 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SubagentRunner
|
|
3
|
-
*
|
|
4
|
-
* 1. `claude-agent-sdk` (програмний, потребує `ANTHROPIC_API_KEY`);
|
|
5
|
-
* 2. `claude -p` (CLI-auth користувача);
|
|
6
|
-
* 3. `cursor-agent -p` (CLI-auth).
|
|
7
|
-
* Нема жодного → throw (polyfill без runner-а не стартує, §2.2).
|
|
2
|
+
* SubagentRunner — спавн субагента через pi (провайдер-нейтрально).
|
|
3
|
+
* Модель обирається через resolveModel('avg') (каскад local→cloud) або через deps.model.
|
|
8
4
|
*
|
|
9
|
-
* pi
|
|
10
|
-
*
|
|
5
|
+
* Контракт runner-а: { backend: 'pi', runStep(prompt, { cwd }) → Promise<{ ok, output }> }.
|
|
6
|
+
* Усі callers (planner, executor, plan-panel, review, budget) використовують саме цей контракт.
|
|
11
7
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
8
|
+
* pi НЕ спавниться рекурсивно коли pi — зовнішній драйвер (§9.1).
|
|
9
|
+
* У цьому проєкті зовнішній драйвер — Claude Code; pi як субагент — безпечно.
|
|
14
10
|
*/
|
|
15
11
|
import { spawnSync } from 'node:child_process'
|
|
16
|
-
import { env as processEnv } from 'node:process'
|
|
17
12
|
|
|
18
|
-
|
|
19
|
-
'SubagentRunner: ні claude-agent-sdk (з ANTHROPIC_API_KEY), ні `claude`/`cursor-agent` у PATH — ' +
|
|
20
|
-
'субагентів спавнити нічим. Встанови CLI-runner або задай ANTHROPIC_API_KEY.'
|
|
13
|
+
import { resolveModel } from '../../../lib/models.mjs'
|
|
21
14
|
|
|
22
15
|
/**
|
|
23
|
-
*
|
|
24
|
-
* @param {string}
|
|
25
|
-
* @param {
|
|
26
|
-
* @
|
|
16
|
+
* Викликає pi і повертає { ok, output }.
|
|
17
|
+
* @param {string} prompt
|
|
18
|
+
* @param {string} model provider/model-id або '' для pi-дефолту
|
|
19
|
+
* @param {{ cwd?: string }} [opts]
|
|
20
|
+
* @returns {{ ok: boolean, output: string }}
|
|
27
21
|
*/
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
22
|
+
function callPi(prompt, model, { cwd } = {}) {
|
|
23
|
+
const modelArgs = model ? ['--model', model] : []
|
|
24
|
+
const r = spawnSync('pi', ['-p', prompt, ...modelArgs, '--no-session'], {
|
|
25
|
+
cwd,
|
|
26
|
+
encoding: 'utf8',
|
|
27
|
+
timeout: 600_000
|
|
28
|
+
})
|
|
29
|
+
const ok = !r.error && r.status === 0
|
|
30
|
+
const output = (r.stdout ?? '') + (r.error ? r.error.message : !ok ? (r.stderr ?? '') : '')
|
|
31
|
+
return { ok, output }
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
|
-
*
|
|
35
|
-
* @param {{
|
|
36
|
-
* @returns {
|
|
35
|
+
* Створює pi-runner. Повертає { backend: 'pi', runStep }.
|
|
36
|
+
* @param {{ model?: string, callPi?: Function }} [deps] ін'єкції для тестів
|
|
37
|
+
* @returns {Promise<{ backend: string, runStep: (prompt: string, opts?: object) => Promise<{ ok: boolean, output: string }> }>}
|
|
37
38
|
*/
|
|
38
|
-
export function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (isInPath('cursor-agent')) return 'cursor'
|
|
42
|
-
return null
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* CLI-runner (`claude -p` / `cursor-agent -p`) — CLI-auth, без API key.
|
|
47
|
-
* @param {'claude' | 'cursor-agent'} bin виконуваний
|
|
48
|
-
* @param {{ spawn?: typeof import('node:child_process').spawnSync }} [deps] ін'єкція
|
|
49
|
-
* @returns {{ backend: string, runStep: (prompt: string, opts?: { cwd?: string }) => { ok: boolean, output: string } }} runner
|
|
50
|
-
*/
|
|
51
|
-
export function cliRunner(bin, deps = {}) {
|
|
52
|
-
const spawn = deps.spawn ?? spawnSync
|
|
53
|
-
return {
|
|
54
|
-
backend: bin,
|
|
55
|
-
runStep(prompt, { cwd } = {}) {
|
|
56
|
-
const r = spawn(bin, ['-p'], { input: prompt, cwd, encoding: 'utf8' })
|
|
57
|
-
return { ok: (r.status ?? 1) === 0, output: `${r.stdout ?? ''}${r.stderr ?? ''}` }
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
39
|
+
export async function createRunner(deps = {}) {
|
|
40
|
+
const model = deps.model ?? resolveModel('avg')
|
|
41
|
+
const callPiFn = deps.callPi ?? callPi
|
|
61
42
|
|
|
62
|
-
/**
|
|
63
|
-
* SDK-runner (`claude-agent-sdk`). `query` ін'єктується; за замовчуванням —
|
|
64
|
-
* динамічний import (optional dependency).
|
|
65
|
-
* @param {{ query?: (input: object) => object }} [deps] ін'єкція (query повертає async-iterable повідомлень)
|
|
66
|
-
* @returns {{ backend: string, runStep: (prompt: string, opts?: { cwd?: string }) => Promise<{ ok: boolean, output: string }> }} runner
|
|
67
|
-
*/
|
|
68
|
-
export function sdkRunner(deps = {}) {
|
|
69
43
|
return {
|
|
70
|
-
backend: '
|
|
71
|
-
async runStep(prompt,
|
|
72
|
-
let query = deps.query
|
|
73
|
-
if (!query) {
|
|
74
|
-
const mod = await import('@anthropic-ai/claude-agent-sdk')
|
|
75
|
-
query = mod.query
|
|
76
|
-
}
|
|
77
|
-
let output = ''
|
|
78
|
-
let ok = true
|
|
44
|
+
backend: 'pi',
|
|
45
|
+
async runStep(prompt, opts = {}) {
|
|
79
46
|
try {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
})) {
|
|
84
|
-
if (typeof msg?.text === 'string') output += msg.text
|
|
85
|
-
if (msg?.type === 'result') ok = msg.is_error !== true
|
|
86
|
-
}
|
|
87
|
-
} catch (error) {
|
|
88
|
-
return { ok: false, output: String(error?.message ?? error) }
|
|
47
|
+
return callPiFn(prompt, model, opts)
|
|
48
|
+
} catch (e) {
|
|
49
|
+
return { ok: false, output: String(e?.message ?? e) }
|
|
89
50
|
}
|
|
90
|
-
return { ok, output }
|
|
91
51
|
}
|
|
92
52
|
}
|
|
93
53
|
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Створює runner за доступним backend-ом. `backend`/probe-и можна задати явно
|
|
97
|
-
* (тести); інакше визначаються з env/PATH/SDK.
|
|
98
|
-
* @param {{ backend?: string, env?: Record<string, string | undefined>, isInPath?: (name: string) => boolean, canImportSdk?: boolean, spawn?: (cmd: string, args: string[], opts: object) => object, query?: (input: object) => object }} [deps] ін'єкції
|
|
99
|
-
* @returns {Promise<{ backend: string, runStep: (prompt: string, opts?: object) => object }>} runner
|
|
100
|
-
*/
|
|
101
|
-
export async function createRunner(deps = {}) {
|
|
102
|
-
const env = deps.env ?? processEnv
|
|
103
|
-
const isInPath = deps.isInPath ?? (name => isBinaryInPath(name, deps.spawn))
|
|
104
|
-
const canImportSdk = deps.canImportSdk ?? (await probeSdk())
|
|
105
|
-
const backend = deps.backend ?? selectBackend({ hasApiKey: Boolean(env.ANTHROPIC_API_KEY), canImportSdk, isInPath })
|
|
106
|
-
if (!backend) throw new Error(NO_BACKEND)
|
|
107
|
-
if (backend === 'sdk') return sdkRunner(deps)
|
|
108
|
-
return cliRunner(backend === 'claude' ? 'claude' : 'cursor-agent', deps)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Чи імпортується `claude-agent-sdk` (optional dependency).
|
|
113
|
-
* @returns {Promise<boolean>} true, якщо доступний
|
|
114
|
-
*/
|
|
115
|
-
async function probeSdk() {
|
|
116
|
-
try {
|
|
117
|
-
await import('@anthropic-ai/claude-agent-sdk')
|
|
118
|
-
return true
|
|
119
|
-
} catch {
|
|
120
|
-
return false
|
|
121
|
-
}
|
|
122
|
-
}
|
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
|
}
|