@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.
Files changed (72) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/docs/n-cursor.md +1 -9
  3. package/bin/n-cursor.js +3 -25
  4. package/package.json +1 -1
  5. package/rules/docker/lib/docs/docker-mirror.md +1 -1
  6. package/rules/docker/lib/docs/docker-native-addon.md +1 -1
  7. package/rules/npm-module/npm-module.mdc +1 -1
  8. package/rules/npm-module/policy/npm_publish_yml/template/npm-publish.yml.snippet.yml +1 -1
  9. package/rules/test/coverage/coverage.mjs +9 -19
  10. package/rules/test/test.mdc +1 -1
  11. package/scripts/dispatcher/trace.mjs +4 -16
  12. package/scripts/docs/build-agents-commands.md +1 -1
  13. package/scripts/docs/worktree-cli.md +1 -1
  14. package/scripts/lib/changed-files.mjs +19 -3
  15. package/scripts/lib/sync-gitignore-worktree.mjs +4 -5
  16. package/scripts/worktree-cli.mjs +1 -2
  17. package/skills/docgen/js/docgen-gen.mjs +7 -7
  18. package/docs/flow.MD +0 -1364
  19. package/scripts/dispatcher/docs/graph.md +0 -346
  20. package/scripts/dispatcher/docs/index.md +0 -236
  21. package/scripts/dispatcher/docs/trace.md +0 -296
  22. package/scripts/dispatcher/graph/lib/cmd-init.mjs +0 -112
  23. package/scripts/dispatcher/graph/lib/cmd-invalidate.mjs +0 -96
  24. package/scripts/dispatcher/graph/lib/cmd-kill.mjs +0 -141
  25. package/scripts/dispatcher/graph/lib/cmd-plan.mjs +0 -142
  26. package/scripts/dispatcher/graph/lib/cmd-run.mjs +0 -328
  27. package/scripts/dispatcher/graph/lib/cmd-scan.mjs +0 -115
  28. package/scripts/dispatcher/graph/lib/cmd-setup.mjs +0 -111
  29. package/scripts/dispatcher/graph/lib/cmd-signals.mjs +0 -328
  30. package/scripts/dispatcher/graph/lib/cmd-status.mjs +0 -131
  31. package/scripts/dispatcher/graph/lib/cmd-verify.mjs +0 -100
  32. package/scripts/dispatcher/graph/lib/cmd-watch.mjs +0 -128
  33. package/scripts/dispatcher/graph/lib/config.mjs +0 -103
  34. package/scripts/dispatcher/graph/lib/frontmatter.mjs +0 -224
  35. package/scripts/dispatcher/graph/lib/nnn.mjs +0 -127
  36. package/scripts/dispatcher/graph/lib/node-state.mjs +0 -157
  37. package/scripts/dispatcher/graph/lib/scanner.mjs +0 -235
  38. package/scripts/dispatcher/graph/lib/worktree-ops.mjs +0 -193
  39. package/scripts/dispatcher/graph-tasks.mjs +0 -92
  40. package/scripts/dispatcher/graph.mjs +0 -212
  41. package/scripts/dispatcher/index.mjs +0 -45
  42. package/scripts/dispatcher/lib/docs/active.md +0 -348
  43. package/scripts/dispatcher/lib/docs/artifact.md +0 -232
  44. package/scripts/dispatcher/lib/docs/budget.md +0 -167
  45. package/scripts/dispatcher/lib/docs/capability.md +0 -196
  46. package/scripts/dispatcher/lib/docs/commands.md +0 -210
  47. package/scripts/dispatcher/lib/docs/events.md +0 -183
  48. package/scripts/dispatcher/lib/docs/executor.md +0 -190
  49. package/scripts/dispatcher/lib/docs/gate.md +0 -231
  50. package/scripts/dispatcher/lib/docs/level.md +0 -335
  51. package/scripts/dispatcher/lib/docs/plan-panel.md +0 -181
  52. package/scripts/dispatcher/lib/docs/plan.md +0 -200
  53. package/scripts/dispatcher/lib/docs/planner.md +0 -269
  54. package/scripts/dispatcher/lib/docs/review.md +0 -255
  55. package/scripts/dispatcher/lib/docs/reviewer.md +0 -240
  56. package/scripts/dispatcher/lib/docs/snapshot.md +0 -247
  57. package/scripts/dispatcher/lib/docs/spec.md +0 -203
  58. package/scripts/dispatcher/lib/docs/state-store.md +0 -303
  59. package/scripts/dispatcher/lib/docs/subagent-runner.md +0 -173
  60. package/scripts/dispatcher/lib/events.mjs +0 -67
  61. package/scripts/dispatcher/lib/executor.mjs +0 -107
  62. package/scripts/dispatcher/lib/plan-panel.mjs +0 -76
  63. package/scripts/dispatcher/lib/state-store.mjs +0 -173
  64. package/scripts/dispatcher/lib/subagent-runner.mjs +0 -53
  65. package/scripts/graph/index.mjs +0 -115
  66. package/scripts/graph/lib/config.mjs +0 -62
  67. package/scripts/graph/lib/dag.mjs +0 -161
  68. package/scripts/graph/lib/frontmatter.mjs +0 -70
  69. package/scripts/graph/lib/nnn.mjs +0 -77
  70. package/scripts/graph/lib/state.mjs +0 -110
  71. package/scripts/graph/scan.mjs +0 -64
  72. package/scripts/graph/status.mjs +0 -86
@@ -1,173 +0,0 @@
1
- # subagent-runner.mjs
2
-
3
- ## Огляд
4
-
5
- `subagent-runner.mjs` — модуль абстракції спавну сфокусованого субагента для Активного Раннера (фаза Ф3/Ф4 диспетчера). Реалізує специфікацію §15.1: надає уніфікований інтерфейс `runStep(prompt, opts)` поверх трьох можливих backend-ів:
6
-
7
- 1. `claude-agent-sdk` — програмний доступ через пакет `@anthropic-ai/claude-agent-sdk`, потребує змінної середовища `ANTHROPIC_API_KEY`.
8
- 2. `claude -p` — CLI-аутентифікація користувача через виконуваний `claude` у `PATH`.
9
- 3. `cursor-agent -p` — CLI-аутентифікація через виконуваний `cursor-agent` у `PATH`.
10
-
11
- Якщо жоден backend недоступний — модуль кидає виняток із текстом `NO_BACKEND` (polyfill без runner-а не стартує, §2.2).
12
-
13
- Згідно з коментарем у заголовку файлу, для inner-спавну навмисно НЕ використовується pi.dev: у автономному режимі pi.dev — це зовнішній драйвер, тож спавнити ним внутрішні субагенти призвело б до рекурсії (§9.1).
14
-
15
- Усі probe-залежності (`spawn`, `isInPath`, `canImportSdk`, `query`) проектовані під ін'єкцію — для тестування без реальних процесів та без встановленого SDK.
16
-
17
- ## Експорти / API
18
-
19
- Модуль експортує чотири іменовані функції:
20
-
21
- - `isBinaryInPath(name, spawn?)` — перевірка наявності бінарника в `PATH`.
22
- - `selectBackend({ hasApiKey, canImportSdk, isInPath })` — вибір backend-а за пріоритетом.
23
- - `cliRunner(bin, deps?)` — фабрика runner-а для CLI-варіанту.
24
- - `sdkRunner(deps?)` — фабрика runner-а для SDK-варіанту.
25
- - `createRunner(deps?)` — головний фасад, що сам визначає та повертає потрібний runner.
26
-
27
- Внутрішня (не експортована) функція: `probeSdk()` — перевіряє можливість динамічного імпорту SDK.
28
-
29
- Константа модульного рівня `NO_BACKEND` — текст повідомлення помилки, коли жоден backend недоступний.
30
-
31
- ## Функції
32
-
33
- ### `isBinaryInPath(name, spawn = spawnSync)`
34
-
35
- Перевіряє, чи є виконуваний бінарник у `PATH` через виклик `command -v <name>`.
36
-
37
- - Параметри:
38
- - `name` (`string`) — ім'я виконуваного.
39
- - `spawn` (`typeof spawnSync`, optional) — ін'єкція для тестів; за замовчуванням `spawnSync` із `node:child_process`.
40
- - Повертає: `boolean` — `true`, якщо `spawn` повернув статус `0`; інакше `false`. Якщо `r.status` дорівнює `null`/`undefined`, трактується як `1` (тобто `false`).
41
- - Side effects: виклик дочірнього процесу `command -v` через shell (`shell: true`).
42
-
43
- ### `selectBackend({ hasApiKey, canImportSdk, isInPath })`
44
-
45
- Вибирає backend за чітко зафіксованим пріоритетом: SDK > Claude CLI > Cursor CLI.
46
-
47
- - Параметри (один об'єкт):
48
- - `hasApiKey` (`boolean`) — чи задана `ANTHROPIC_API_KEY`.
49
- - `canImportSdk` (`boolean`) — чи імпортується `@anthropic-ai/claude-agent-sdk`.
50
- - `isInPath` (`(name: string) => boolean`) — предикат наявності бінарника у `PATH`.
51
- - Повертає: рядковий літерал `'sdk'`, `'claude'`, `'cursor'` або `null`.
52
- - Логіка:
53
- 1. Якщо `hasApiKey` і `canImportSdk` одночасно істинні — `'sdk'`.
54
- 2. Інакше якщо `isInPath('claude')` — `'claude'`.
55
- 3. Інакше якщо `isInPath('cursor-agent')` — `'cursor'`.
56
- 4. Інакше — `null`.
57
- - Side effects: відсутні (за умови, що `isInPath` чистий).
58
-
59
- ### `cliRunner(bin, deps = {})`
60
-
61
- Створює CLI-runner на основі бінарника `claude` або `cursor-agent` (обидва підтримують прапор `-p` для подачі промпта зі stdin).
62
-
63
- - Параметри:
64
- - `bin` (`'claude' | 'cursor-agent'`) — який саме CLI запускати.
65
- - `deps.spawn` (`typeof spawnSync`, optional) — ін'єкція; за замовчуванням `spawnSync`.
66
- - Повертає об'єкт:
67
- - `backend` — рядок, що дорівнює переданому `bin`.
68
- - `runStep(prompt, { cwd } = {})` — синхронна функція, що викликає `spawn(bin, ['-p'], { input: prompt, cwd, encoding: 'utf8' })`. Повертає `{ ok: boolean, output: string }`, де:
69
- - `ok` — `true`, якщо `r.status === 0` (null/undefined трактується як 1 → `false`).
70
- - `output` — конкатенація `stdout` та `stderr` (порожні рядки, якщо undefined).
71
- - Side effects: спавн дочірнього CLI-процесу при кожному виклику `runStep`.
72
-
73
- ### `sdkRunner(deps = {})`
74
-
75
- Створює SDK-runner, який працює через async-iterable `query` з пакета `@anthropic-ai/claude-agent-sdk`.
76
-
77
- - Параметри:
78
- - `deps.query` (`(input: object) => AsyncIterable`, optional) — ін'єкція функції `query` для тестів. Якщо не задано — модуль динамічно імпортує `@anthropic-ai/claude-agent-sdk` і бере звідти `query`.
79
- - Повертає об'єкт:
80
- - `backend` — рядок `'sdk'`.
81
- - `runStep(prompt, { cwd } = {})` — `async` функція, що повертає `Promise<{ ok: boolean, output: string }>`.
82
- - Логіка `runStep`:
83
- 1. Лінива ініціалізація `query` (якщо не передано в `deps`).
84
- 2. Виклик `query({ prompt, options: { cwd, maxTurns: 20, allowedTools: ['Read', 'Edit', 'Bash'] } })`.
85
- 3. Ітерує асинхронно по повідомленнях:
86
- - Якщо `msg.text` — рядок, додає його до `output`.
87
- - Якщо `msg.type === 'result'`, фіналізує `ok = msg.is_error !== true`.
88
- 4. У разі винятку — повертає `{ ok: false, output: String(error?.message ?? error) }`.
89
- - Side effects: динамічний імпорт SDK (один раз на виклик, якщо `query` не ін'єктовано); мережеві/процесні дії SDK; обмеження інструментів виключно до `Read`, `Edit`, `Bash`.
90
-
91
- ### `createRunner(deps = {})`
92
-
93
- Головний фасад модуля. Підбирає та повертає runner відповідно до доступних backend-ів.
94
-
95
- - Параметри (об'єкт `deps` для тестів; усі поля опціональні):
96
- - `backend` — явне переозначення вибору (`'sdk'`/`'claude'`/`'cursor'`).
97
- - `env` — мапа змінних середовища; за замовчуванням `processEnv` (`process.env`).
98
- - `isInPath` — функція; за замовчуванням обгортка над `isBinaryInPath(name, deps.spawn)`.
99
- - `canImportSdk` — заздалегідь обчислений прапор; інакше викликається `probeSdk()`.
100
- - `spawn` — використовується як `deps.spawn` для дефолтного `isInPath`.
101
- - `query` — пробрасується в `sdkRunner` як `deps.query`.
102
- - Повертає: `Promise<runner>`, де `runner` має форму `{ backend, runStep }` (синхронний для CLI, асинхронний для SDK — обидва типи представлені в одному фасаді).
103
- - Логіка:
104
- 1. Резолвить `env`, `isInPath`, `canImportSdk`.
105
- 2. Якщо `deps.backend` не задано — викликає `selectBackend({ hasApiKey: Boolean(env.ANTHROPIC_API_KEY), canImportSdk, isInPath })`.
106
- 3. Якщо backend усе ще `null`/falsy — `throw new Error(NO_BACKEND)`.
107
- 4. Для `'sdk'` — повертає `sdkRunner(deps)`.
108
- 5. Для `'claude'` — повертає `cliRunner('claude', deps)`.
109
- 6. Для будь-якого іншого ненульового — повертає `cliRunner('cursor-agent', deps)` (тобто `'cursor'` мапиться на `cursor-agent`).
110
- - Side effects: можливий динамічний імпорт SDK через `probeSdk()`; виклики `spawn` через дефолтний `isInPath`.
111
-
112
- ### `probeSdk()` (внутрішня)
113
-
114
- Перевіряє, чи можна динамічно імпортувати `@anthropic-ai/claude-agent-sdk`.
115
-
116
- - Параметри: відсутні.
117
- - Повертає: `Promise<boolean>` — `true`, якщо `import` успішний, інакше `false` (виняток поглинається порожнім `catch`).
118
- - Side effects: динамічний `import()` модуля SDK.
119
-
120
- ## Залежності
121
-
122
- - `node:child_process` — `spawnSync` (синхронний спавн дочірніх процесів для `command -v` та CLI-runner-а).
123
- - `node:process` — `env` як `processEnv` (читання змінних середовища, передусім `ANTHROPIC_API_KEY`).
124
- - `@anthropic-ai/claude-agent-sdk` (optional, динамічний `import`) — джерело функції `query` для SDK-runner-а; відсутність пакета — допустимий сценарій (`probeSdk()` ловить виняток).
125
-
126
- Зовнішні виконувані файли, очікувані у `PATH`:
127
-
128
- - `command` — POSIX-shell builtin для `command -v`.
129
- - `claude` — CLI Claude Code.
130
- - `cursor-agent` — CLI Cursor Agent.
131
-
132
- Жодних інших імпортів із локального проєкту модуль не робить — він самодостатній.
133
-
134
- ## Потік виконання / Використання
135
-
136
- Типовий сценарій використання у диспетчері (Активний Раннер, фази Ф3/Ф4):
137
-
138
- 1. Виклик `await createRunner()` без параметрів.
139
- 2. Усередині `createRunner` виконується probe доступних backend-ів:
140
- - перевіряється `process.env.ANTHROPIC_API_KEY`;
141
- - намагається динамічно імпортувати `@anthropic-ai/claude-agent-sdk`;
142
- - перевіряються `claude` та `cursor-agent` у `PATH` через `command -v`.
143
- 3. `selectBackend` повертає перший доступний backend за пріоритетом SDK → claude → cursor.
144
- 4. Якщо нічого не знайдено — кидається `Error(NO_BACKEND)`, що зупиняє стартування polyfill-а (§2.2).
145
- 5. Інакше повертається об'єкт `{ backend, runStep }`.
146
- 6. Викликаючий код передає `runStep(prompt, { cwd })`:
147
- - для SDK — отримує `Promise<{ ok, output }>`, працюючи з обмеженим набором інструментів `Read`/`Edit`/`Bash` та лімітом `maxTurns: 20`;
148
- - для CLI — отримує синхронний `{ ok, output }` після завершення дочірнього процесу.
149
-
150
- Для тестування потік ін'єктується наскрізно: будь-яку із залежностей (`spawn`, `isInPath`, `canImportSdk`, `query`, `env`, `backend`) можна перевизначити, що дозволяє покривати модуль unit-тестами без реальних процесів і без SDK.
151
-
152
- Особливості та інваріанти:
153
-
154
- - Пріоритет backend-ів зафіксований у `selectBackend` і не змінюється від виклику до виклику.
155
- - `runStep` у CLI-варіанті завжди синхронний, у SDK-варіанті — асинхронний; форма результату `{ ok, output }` уніфікована.
156
- - `output` у CLI завжди склеює `stdout` та `stderr` без розділювача.
157
- - У SDK-варіанті помилки під час ітерації `query` ловляться й конвертуються в `{ ok: false, output: <message> }`, тобто `runStep` не пробрасує винятки нагору.
158
- - Значення `null`/`undefined` для `r.status` у `spawnSync`-результатах послідовно нормалізується через `?? 1`, що дає поведінку "невідомий статус == помилка".
159
-
160
- ## Rebuild Test
161
-
162
- Документ описує лише той API, що присутній у файлі `subagent-runner.mjs`:
163
-
164
- - Експорти: `isBinaryInPath`, `selectBackend`, `cliRunner`, `sdkRunner`, `createRunner` — усі п'ять перевірено за вихідним кодом.
165
- - Внутрішня функція `probeSdk` зафіксована як приватна (не експортована).
166
- - Константа `NO_BACKEND` згадана як модульна.
167
- - Імпорти `spawnSync` із `node:child_process` та `env` як `processEnv` із `node:process` зафіксовані.
168
- - Опційна залежність `@anthropic-ai/claude-agent-sdk` згадана з акцентом на динамічний import у двох місцях (`sdkRunner.runStep` та `probeSdk`).
169
- - Пріоритет `sdk → claude → cursor` і поведінка `createRunner` за відсутності backend (throw `NO_BACKEND`) відповідають коду.
170
- - Деталі `sdkRunner` (`maxTurns: 20`, `allowedTools: ['Read', 'Edit', 'Bash']`, обробка `msg.type === 'result'` та `msg.is_error`) узяті безпосередньо з тіла функції.
171
- - Формула `r.status ?? 1` для нормалізації статусу описана точно так, як у коді.
172
-
173
- Жодних припущень про невидимі в файлі деталі (тестові файли, інтеграцію з конкретними викликами в інших модулях диспетчера) у документі не зроблено.
@@ -1,67 +0,0 @@
1
- /**
2
- * WAL — append-only журнал подій `flow` (spec §4.1.2, §9).
3
- *
4
- * Sibling-файл `.worktrees/<sanitized-branch>.events.jsonl` (JSON Lines). Єдиний
5
- * журнал: субсумує і переходи стану (`step_*`, `blocked`…), і api-облік
6
- * (`api_call`). Append-only → краш-безпечніший за перезапис: торваний останній
7
- * рядок (краш посеред append) при читанні **толеруємо** (пропускаємо), а не
8
- * валимо весь журнал.
9
- *
10
- * **WAL-інваріант** (забезпечує `state-store.recordTransition`): подію
11
- * дописуємо ДО зміни високорівневого статусу у snapshot `.flow.json`.
12
- *
13
- * Усі шляхи — абсолютні (`no-relative-fs-path`).
14
- */
15
- import { appendFileSync, existsSync, readFileSync } from 'node:fs'
16
- import { basename, dirname, isAbsolute, join } from 'node:path'
17
-
18
- /**
19
- * Шлях sibling-журналу подій для checkout-каталогу worktree.
20
- * @param {string} worktreeDir абсолютний шлях checkout (`…/.worktrees/feat-x`)
21
- * @returns {string} `…/.worktrees/feat-x.events.jsonl`
22
- */
23
- export function flowEventsPath(worktreeDir) {
24
- if (!isAbsolute(worktreeDir)) {
25
- throw new Error(`flowEventsPath: очікується абсолютний шлях (отримано: ${worktreeDir})`)
26
- }
27
- return join(dirname(worktreeDir), `${basename(worktreeDir)}.events.jsonl`)
28
- }
29
-
30
- /**
31
- * Дописує одну подію (з міткою часу `at`) у журнал. Створює файл за потреби.
32
- * @param {string} eventsPath абсолютний шлях `.events.jsonl`
33
- * @param {object} event подія (напр. `{ type: 'step_started', step: 2 }`)
34
- * @param {() => number} [now] фабрика часу (ms) — ін'єкція для тестів
35
- * @returns {object} фактично записаний запис (зі `at`)
36
- */
37
- export function appendEvent(eventsPath, event, now = Date.now) {
38
- if (!isAbsolute(eventsPath)) {
39
- throw new Error(`appendEvent: очікується абсолютний шлях (отримано: ${eventsPath})`)
40
- }
41
- const record = { at: new Date(now()).toISOString(), ...event }
42
- appendFileSync(eventsPath, `${JSON.stringify(record)}\n`, 'utf8')
43
- return record
44
- }
45
-
46
- /**
47
- * Читає всі події. Відсутній файл → `[]`. Непарсабельні рядки (порожні або
48
- * торваний останній) **пропускаються** (append-only толерантність).
49
- * @param {string} eventsPath абсолютний шлях `.events.jsonl`
50
- * @returns {object[]} розпарсені події у порядку запису
51
- */
52
- export function readEvents(eventsPath) {
53
- if (!isAbsolute(eventsPath)) {
54
- throw new Error(`readEvents: очікується абсолютний шлях (отримано: ${eventsPath})`)
55
- }
56
- if (!existsSync(eventsPath)) return []
57
- return readFileSync(eventsPath, 'utf8')
58
- .split('\n')
59
- .filter(line => line.trim() !== '')
60
- .flatMap(line => {
61
- try {
62
- return [JSON.parse(line)]
63
- } catch {
64
- return []
65
- }
66
- })
67
- }
@@ -1,107 +0,0 @@
1
- /**
2
- * Executor (spec §3 Ф3) — виконує план покроково через SubagentRunner + verify.
3
- *
4
- * Інваріанти:
5
- * - **мікропромпт зі стану** (§3 Ф3): субагент отримує лише поточний крок +
6
- * критерії + останню помилку, не історію переписки;
7
- * - **commit лише після зеленого verify** (§4.1.7): repair-спроби не комітять,
8
- * тож HEAD завжди = останній зелений крок;
9
- * - **repair ≤ maxRepairAttempts**, далі — HITL (`blocked-on-human`, §4.2).
10
- *
11
- * Усі побічні дії (`runner`/`verify`/`commit`) ін'єктуються — тестується без
12
- * реальних LLM/git/gates.
13
- */
14
- import { readState, recordTransition } from './state-store.mjs'
15
-
16
- /**
17
- * Мікропромпт для кроку (§3 Ф3): лише поточний крок + критерії + остання помилка.
18
- * @param {{ step: number, task: string, acceptance?: string, last_error?: string }} step крок плану
19
- * @param {{ branch?: string }} state стан (для контексту гілки)
20
- * @returns {string} промпт субагента
21
- */
22
- export function microprompt(step, state) {
23
- const lines = [
24
- 'Реалізуй РІВНО цей крок плану (не більше). Iron Law of TDD: спершу падаючі тести, тоді код.',
25
- `Гілка: ${state.branch ?? '—'}`,
26
- `Крок ${step.step}: ${step.task}`
27
- ]
28
- if (step.acceptance) lines.push(`Критерії приймання: ${step.acceptance}`)
29
- if (step.hint) lines.push(`Підказка людини (HITL): ${step.hint}`)
30
- if (step.last_error) lines.push(`Попередня спроба впала на перевірці:\n${step.last_error}\nВиправ це.`)
31
- return lines.join('\n')
32
- }
33
-
34
- /**
35
- * Оновлює крок плану за індексом (pure).
36
- * @param {{ plan: object[] }} state стан
37
- * @param {number} index індекс кроку
38
- * @param {object} patch часткове оновлення кроку
39
- * @returns {object} новий стан
40
- */
41
- export function patchStep(state, index, patch) {
42
- return { ...state, plan: state.plan.map((s, i) => (i === index ? { ...s, ...patch } : s)) }
43
- }
44
-
45
- /**
46
- * Виконує план зі стану.
47
- * @param {{ statePath: string, eventsPath: string }} paths шляхи стану й журналу
48
- * @param {{ runner: { runStep: (prompt: string, opts?: object) => object }, verify: (cwd: string) => Promise<{ pass: boolean, failedOutput?: string }> | { pass: boolean, failedOutput?: string }, commit: (cwd: string, msg: string) => void, cwd?: string, maxRepairAttempts?: number, log?: (m: string) => void, now?: () => number }} deps ін'єкції
49
- * @returns {Promise<{ status: 'done' | 'blocked-on-human', step?: number }>} результат
50
- */
51
- export async function executePlan(paths, deps) {
52
- const { runner, verify, commit, cwd, maxRepairAttempts = 3, log = () => { /* noop */ }, now = Date.now } = deps
53
- let state = readState(paths.statePath)
54
- if (!state?.plan?.length) {
55
- throw new Error('executor: у стані немає плану — спершу planner')
56
- }
57
-
58
- for (let i = 0; i < state.plan.length; i++) {
59
- if (state.plan[i].status === 'done') continue
60
-
61
- let done = false
62
- while (state.plan[i].retry_count < maxRepairAttempts && !done) {
63
- const step = state.plan[i]
64
- log(`executor: крок ${step.step} (спроба ${step.retry_count + 1})`)
65
- await runner.runStep(microprompt(step, state), { cwd })
66
- const verdict = await verify(cwd)
67
- if (verdict.pass) {
68
- commit(cwd, `flow: step ${step.step} — ${step.task}`) // commit ЛИШЕ після зеленого
69
- state = recordTransition(
70
- paths,
71
- { type: 'step_done', step: step.step },
72
- s => patchStep(s, i, { status: 'done' }),
73
- now
74
- )
75
- done = true
76
- } else {
77
- state = recordTransition(
78
- paths,
79
- { type: 'step_retry', step: step.step },
80
- s => patchStep(s, i, { retry_count: s.plan[i].retry_count + 1, last_error: verdict.failedOutput ?? null }),
81
- now
82
- )
83
- }
84
- }
85
-
86
- if (!done) {
87
- const failed = state.plan[i]
88
- const question = {
89
- id: `q-${i}`,
90
- step: failed.step,
91
- question: `Крок ${failed.step} «${failed.task}» не проходить verify після ${maxRepairAttempts} спроб. Що робити?`,
92
- status: 'open',
93
- answer: ''
94
- }
95
- recordTransition(
96
- paths,
97
- { type: 'blocked', step: failed.step },
98
- s => ({ ...s, status: 'blocked-on-human', hitl: [...(s.hitl ?? []), question] }),
99
- now
100
- )
101
- return { status: 'blocked-on-human', step: failed.step }
102
- }
103
- }
104
-
105
- recordTransition(paths, { type: 'plan_done' }, s => ({ ...s, status: 'built' }), now)
106
- return { status: 'done' }
107
- }
@@ -1,76 +0,0 @@
1
- /**
2
- * Agent↔agent brainstorm (bmad party-mode + superpowers dispatching у наших
3
- * термінах): персони-субагенти пропонують погляди, суддя-субагент синтезує одну
4
- * відповідь. Спільний для фази `spec` (mode: 'spec' — підходи) і `plan`
5
- * (mode: 'plan' — JSON-кроки). Перевикористовує runner-інтерфейс Фасада B
6
- * (`runStep(prompt, opts) => { ok, output }`, як `planner.mjs`/`active.mjs`).
7
- *
8
- * HITL: panel лише ПОВЕРТАЄ синтез — апрув людини й збереження артефакту робить
9
- * агент за контрактом `flow.mdc` (фіксація — окрема команда `flow spec`/`flow plan`).
10
- */
11
-
12
- /** Персони панелі: [ім'я, системний промпт]. */
13
- const PERSONAS = [
14
- ['architect', 'Ти — architect. Запропонуй найчистішу архітектуру розв’язання. Стисло, по суті.'],
15
- ['skeptic', 'Ти — skeptic. Назви ризики, граничні випадки і що може піти не так. Стисло.'],
16
- ['tester', 'Ти — tester. Опиши, які тести доведуть коректність. Стисло.']
17
- ]
18
-
19
- /**
20
- * Промпт судді за режимом.
21
- * @param {'spec' | 'plan'} mode режим синтезу
22
- * @param {string} proposals склеєні думки персон
23
- * @param {string} task опис задачі
24
- * @returns {string} промпт судді
25
- */
26
- function judgePrompt(mode, proposals, task) {
27
- const head =
28
- mode === 'plan'
29
- ? [
30
- 'Синтезуй із думок персон ОДИН покроковий план реалізації.',
31
- 'Кожен крок — ≤ 5 хв розробки, з критерієм приймання.',
32
- 'Поверни ЛИШЕ JSON-масив без коментарів: [{ "task": "...", "acceptance": "..." }, ...].'
33
- ]
34
- : [
35
- 'Синтезуй із думок персон 2-3 підходи до розв’язання з рекомендацією й коротким дизайном.',
36
- 'Поверни людино-читабельний текст (Markdown).'
37
- ]
38
- return [...head, '', proposals, '', `Задача: ${task}`].join('\n')
39
- }
40
-
41
- /**
42
- * Проводить панель і повертає синтез.
43
- * @param {{ task: string, cwd: string, runner: { runStep: (p: string, o?: object) => { ok: boolean, output: string } | Promise<{ ok: boolean, output: string }> }, log?: (m: string) => void, mode?: 'spec' | 'plan' }} input ін'єкції
44
- * @returns {Promise<{ task: string, acceptance?: string }[] | string | null>} кроки (plan), текст (spec) або null (фейл)
45
- */
46
- export async function runPanel({ task, cwd, runner, log = console.error, mode = 'plan' }) {
47
- if (!runner) {
48
- log('panel: нема runner — режим --panel недоступний')
49
- return null
50
- }
51
- const proposals = await Promise.all(
52
- PERSONAS.map(async ([name, sys]) => {
53
- const r = await runner.runStep(`${sys}\n\nЗадача: ${task}`, { cwd })
54
- return `### ${name}\n${r.ok ? r.output : '(порожньо)'}`
55
- })
56
- )
57
- const judge = await runner.runStep(judgePrompt(mode, proposals.join('\n\n'), task), { cwd })
58
- if (!judge.ok) {
59
- log('panel: суддя-синтез завершився помилкою')
60
- return null
61
- }
62
- if (mode === 'spec') return judge.output
63
-
64
- const start = judge.output.indexOf('[')
65
- const end = judge.output.lastIndexOf(']')
66
- if (start === -1 || end === -1 || end < start) {
67
- log('panel: суддя не повернув JSON-план')
68
- return null
69
- }
70
- try {
71
- return JSON.parse(judge.output.slice(start, end + 1))
72
- } catch {
73
- log('panel: невалідний JSON синтезу')
74
- return null
75
- }
76
- }
@@ -1,173 +0,0 @@
1
- /**
2
- * Crash-safe сховище runtime-стану `flow` (spec §4, §4.1).
3
- *
4
- * Локація — **sibling-файл** `.worktrees/<sanitized-branch>.flow.json` поруч із
5
- * checkout (НЕ всередині нього: файл усередині worktree = untracked у feature-
6
- * гілці й ризикує потрапити в `git add -A`). Деривація шляху: для checkout-
7
- * директорії `.worktrees/feat-x` стан → `.worktrees/feat-x.flow.json`.
8
- *
9
- * Crash-safety (§4.1):
10
- * - **atomic write**: temp на тому ж FS → `fsync` файла → `rename` (атомарна
11
- * заміна; частковий запис неможливий);
12
- * - **fail-closed на corruption**: нечитабельний/невалідний JSON або несумісний
13
- * `schema_version` → throw (не стартуємо новий flow над зіпсованим станом).
14
- *
15
- * Усі шляхи — абсолютні (вимога `no-relative-fs-path`).
16
- */
17
- import {
18
- closeSync,
19
- existsSync,
20
- fsyncSync,
21
- mkdirSync,
22
- openSync,
23
- readFileSync,
24
- renameSync,
25
- rmSync,
26
- writeFileSync
27
- } from 'node:fs'
28
- import { basename, dirname, isAbsolute, join } from 'node:path'
29
- import { randomBytes } from 'node:crypto'
30
- import { pid } from 'node:process'
31
-
32
- import { appendEvent } from './events.mjs'
33
-
34
- export const SCHEMA_VERSION = 1
35
-
36
- /**
37
- * Шлях sibling-файла стану для заданого checkout-каталогу worktree.
38
- * @param {string} worktreeDir абсолютний шлях checkout (напр. `…/.worktrees/feat-x`)
39
- * @returns {string} абсолютний шлях `…/.worktrees/feat-x.flow.json`
40
- */
41
- export function flowStatePath(worktreeDir) {
42
- if (!isAbsolute(worktreeDir)) {
43
- throw new Error(`flowStatePath: очікується абсолютний шлях (отримано: ${worktreeDir})`)
44
- }
45
- return join(dirname(worktreeDir), `${basename(worktreeDir)}.flow.json`)
46
- }
47
-
48
- /**
49
- * fsync файла за абсолютним шляхом (дані на диск до rename).
50
- * @param {string} path абсолютний шлях
51
- * @returns {void}
52
- */
53
- function fsyncPath(path) {
54
- const fd = openSync(path, 'r')
55
- try {
56
- fsyncSync(fd)
57
- } finally {
58
- closeSync(fd)
59
- }
60
- }
61
-
62
- /**
63
- * Атомарно записує стан: temp(той самий каталог)+fsync+rename. Додає
64
- * `schema_version`. Повертає фактично записаний об'єкт.
65
- * @param {string} statePath абсолютний шлях `.flow.json`
66
- * @param {object} state стан без `schema_version`
67
- * @returns {object} записаний об'єкт (зі `schema_version`)
68
- */
69
- export function writeState(statePath, state) {
70
- if (!isAbsolute(statePath)) {
71
- throw new Error(`writeState: очікується абсолютний шлях (отримано: ${statePath})`)
72
- }
73
- const dir = dirname(statePath)
74
- mkdirSync(dir, { recursive: true })
75
- const payload = { schema_version: SCHEMA_VERSION, ...state }
76
- const tmp = join(dir, `.${basename(statePath)}.${pid}.${randomBytes(6).toString('hex')}.tmp`)
77
- writeFileSync(tmp, `${JSON.stringify(payload, null, 2)}\n`, 'utf8')
78
- fsyncPath(tmp)
79
- renameSync(tmp, statePath)
80
- // best-effort fsync каталогу (durability rename). Не на всіх платформах
81
- // (Windows кидає EISDIR/EPERM) — тому загорнуто й помилки ігноруємо.
82
- try {
83
- fsyncPath(dir)
84
- } catch {
85
- /* fsync каталогу недоступний на цій платформі — некритично */
86
- }
87
- return payload
88
- }
89
-
90
- /**
91
- * Читає стан. Відсутній файл → null. Пошкоджений JSON або несумісний
92
- * `schema_version` → throw (**fail-closed**, §4.1.6).
93
- * @param {string} statePath абсолютний шлях `.flow.json`
94
- * @returns {object | null} стан або null, якщо файлу нема
95
- */
96
- export function readState(statePath) {
97
- if (!isAbsolute(statePath)) {
98
- throw new Error(`readState: очікується абсолютний шлях (отримано: ${statePath})`)
99
- }
100
- if (!existsSync(statePath)) return null
101
- const raw = readFileSync(statePath, 'utf8')
102
- let parsed
103
- try {
104
- parsed = JSON.parse(raw)
105
- } catch {
106
- throw new Error(`readState: пошкоджений стан (невалідний JSON) у ${statePath} — fail-closed`)
107
- }
108
- if (typeof parsed !== 'object' || parsed === null || parsed.schema_version !== SCHEMA_VERSION) {
109
- throw new Error(
110
- `readState: несумісний або пошкоджений schema_version у ${statePath} ` +
111
- `(очікується ${SCHEMA_VERSION}) — fail-closed`
112
- )
113
- }
114
- return parsed
115
- }
116
-
117
- /**
118
- * Читає стан, застосовує `fn` і атомарно записує результат. Якщо файлу нема —
119
- * `fn` отримує `{}`.
120
- * @param {string} statePath абсолютний шлях `.flow.json`
121
- * @param {(state: object) => object} fn трансформер стану
122
- * @returns {object} записаний об'єкт
123
- */
124
- export function updateState(statePath, fn) {
125
- const current = readState(statePath)
126
- return writeState(statePath, fn(current ?? {}))
127
- }
128
-
129
- /**
130
- * Видаляє sibling-файл стану (cleanup при `worktree remove` / `flow cancel`).
131
- * Ідемпотентно (відсутній файл — не помилка).
132
- * @param {string} statePath абсолютний шлях `.flow.json`
133
- * @returns {void}
134
- */
135
- export function removeState(statePath) {
136
- if (!isAbsolute(statePath)) {
137
- throw new Error(`removeState: очікується абсолютний шлях (отримано: ${statePath})`)
138
- }
139
- rmSync(statePath, { force: true })
140
- }
141
-
142
- /**
143
- * WAL-перехід (§4.1.2): спершу дописує подію в журнал, ТОДІ атомарно змінює
144
- * статус у snapshot. Якщо запис стану впаде — подія вже durable (журнал —
145
- * джерело для reconcile при `resume`).
146
- * @param {{ statePath: string, eventsPath: string }} paths шляхи стану й журналу
147
- * @param {object} event подія переходу
148
- * @param {(state: object) => object} stateFn трансформер стану
149
- * @param {() => number} [now] фабрика часу (ms)
150
- * @returns {object} записаний стан
151
- */
152
- export function recordTransition({ statePath, eventsPath }, event, stateFn, now = Date.now) {
153
- appendEvent(eventsPath, event, now)
154
- return updateState(statePath, stateFn)
155
- }
156
-
157
- /**
158
- * Прибирає всі runtime-sibling-и worktree: `.flow.json`, `.events.jsonl`,
159
- * лок-каталог `.flow-lock-<branch>/`. Ідемпотентно. Викликається `flow cancel`
160
- * і `worktree remove` (інакше sibling-и осиротіють — git їх не чистить).
161
- * @param {string} worktreeDir абсолютний шлях checkout (`…/.worktrees/feat-x`)
162
- * @returns {void}
163
- */
164
- export function cleanupFlowSiblings(worktreeDir) {
165
- if (!isAbsolute(worktreeDir)) {
166
- throw new Error(`cleanupFlowSiblings: очікується абсолютний шлях (отримано: ${worktreeDir})`)
167
- }
168
- const base = basename(worktreeDir)
169
- const dir = dirname(worktreeDir)
170
- rmSync(join(dir, `${base}.flow.json`), { force: true })
171
- rmSync(join(dir, `${base}.events.jsonl`), { force: true })
172
- rmSync(join(dir, `.flow-lock-${base}`), { recursive: true, force: true })
173
- }
@@ -1,53 +0,0 @@
1
- /**
2
- * SubagentRunner — спавн субагента через pi (провайдер-нейтрально).
3
- * Модель обирається через resolveModel('avg') (каскад local→cloud) або через deps.model.
4
- *
5
- * Контракт runner-а: { backend: 'pi', runStep(prompt, { cwd }) → Promise<{ ok, output }> }.
6
- * Усі callers (planner, executor, plan-panel, review, budget) використовують саме цей контракт.
7
- *
8
- * pi НЕ спавниться рекурсивно коли pi — зовнішній драйвер (§9.1).
9
- * У цьому проєкті зовнішній драйвер — Claude Code; pi як субагент — безпечно.
10
- */
11
- import { spawnSync } from 'node:child_process'
12
-
13
- import { resolveModel } from '../../../lib/models.mjs'
14
-
15
- /**
16
- * Викликає pi і повертає { ok, output }.
17
- * @param {string} prompt текст промпта
18
- * @param {string} model provider/model-id або '' для pi-дефолту
19
- * @param {{ cwd?: string }} [opts] опційні параметри (cwd)
20
- * @returns {{ ok: boolean, output: string }} результат із статусом і output
21
- */
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 }
32
- }
33
-
34
- /**
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 }> }>} runner із backend='pi' і методом runStep
38
- */
39
- export function createRunner(deps = {}) {
40
- const model = deps.model ?? resolveModel('avg')
41
- const callPiFn = deps.callPi ?? callPi
42
-
43
- return {
44
- backend: 'pi',
45
- runStep(prompt, opts = {}) {
46
- try {
47
- return callPiFn(prompt, model, opts)
48
- } catch (error) {
49
- return { ok: false, output: String(error?.message ?? error) }
50
- }
51
- }
52
- }
53
- }