@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.
@@ -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 (spec §15.1) абстракція спавну сфокусованого субагента для
3
- * Активного Раннера (Ф3/Ф4). Backend обирається за доступністю:
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.dev для inner-спавну НЕ використовується: у автономному режимі pi.dev —
10
- * зовнішній драйвер, тож спавн ним внутрішніх субагентів = рекурсія (§9.1).
5
+ * Контракт runner-а: { backend: 'pi', runStep(prompt, { cwd }) Promise<{ ok, output }> }.
6
+ * Усі callers (planner, executor, plan-panel, review, budget) використовують саме цей контракт.
11
7
  *
12
- * Усі probe-залежності (`spawn`/`isInPath`/`canImportSdk`/`query`) ін'єктуються,
13
- * щоб тестувати без реальних процесів і без SDK.
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
- const NO_BACKEND =
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
- * Чи є бінарник у PATH (через `command -v`).
24
- * @param {string} name ім'я виконуваного
25
- * @param {typeof import('node:child_process').spawnSync} [spawn] ін'єкція для тестів
26
- * @returns {boolean} true, якщо знайдено
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
- export function isBinaryInPath(name, spawn = spawnSync) {
29
- const r = spawn('command', ['-v', name], { shell: true, encoding: 'utf8' })
30
- return (r.status ?? 1) === 0
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
- * Обирає backend субагентів за пріоритетом sdk > claude > cursor.
35
- * @param {{ hasApiKey: boolean, canImportSdk: boolean, isInPath: (name: string) => boolean }} probes доступність
36
- * @returns {'sdk' | 'claude' | 'cursor' | null} backend або null
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 selectBackend({ hasApiKey, canImportSdk, isInPath }) {
39
- if (hasApiKey && canImportSdk) return 'sdk'
40
- if (isInPath('claude')) return 'claude'
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: 'sdk',
71
- async runStep(prompt, { cwd } = {}) {
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
- for await (const msg of query({
81
- prompt,
82
- options: { cwd, maxTurns: 20, allowedTools: ['Read', 'Edit', 'Bash'] }
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
- }
@@ -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
- cleanupFlowSiblings(paths.checkout) // flow-sibling-и (.flow.json/.events.jsonl/lock) інакше осиротіють (§1.4)
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
  }