@nitra/cursor 4.1.1 → 5.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 +20 -0
- package/bin/docs/n-cursor.md +1 -9
- package/bin/n-cursor.js +3 -25
- package/docs/stryker.config.md +37 -0
- package/docs/vitest.config.md +23 -0
- package/package.json +2 -1
- package/rules/docker/lib/docs/docker-mirror.md +1 -1
- package/rules/docker/lib/docs/docker-native-addon.md +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/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,210 +0,0 @@
|
|
|
1
|
-
# commands.mjs
|
|
2
|
-
|
|
3
|
-
## Огляд
|
|
4
|
-
|
|
5
|
-
Модуль `commands.mjs` реалізує handler-и підкоманд CLI `n-cursor flow` згідно зі специфікацією §8 (Пасивний Турнікет / Flow). Він є диспетчерською точкою для Фази Ф2 робочого потоку — підкоманд `init`, `verify` та `release`, — а також надає допоміжні утиліти: реальний sync-runner `realRun`, гарантію наявності worktree `ensureWorktree` та інференс воркспейсу за зміненими файлами `matchChangedWorkspaces`.
|
|
6
|
-
|
|
7
|
-
Усі побічні ефекти (виконання процесів, логування, обчислення fingerprint, час) реалізовані як ін'єктовані залежності в `deps`, тож логіку модуля можна тестувати без реальних `git`, `npx` чи годинника. Це частина архітектури «командна логіка + чистий ядро». Підкоманди `run` / `resume` / `cancel` / `repair` зі специфікації належать Фазі Ф4 і в цьому файлі ще не реалізовані.
|
|
8
|
-
|
|
9
|
-
Модуль є ESM (`.mjs`), використовує імпорти Node.js (`node:child_process`, `node:path`, `node:process`) і не має станового глобалу — стан тримається у JSON-файлах `.flow.json` (через `state-store.mjs`).
|
|
10
|
-
|
|
11
|
-
## Експорти / API
|
|
12
|
-
|
|
13
|
-
| Експорт | Тип | Призначення |
|
|
14
|
-
| ----------------------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
15
|
-
| `realRun(cmd, args, opts?)` | `function` | Реальний sync-обгортка над `spawnSync` із захопленням stdout/stderr. |
|
|
16
|
-
| `ensureWorktree(rest, deps?)` | `function` | Парсить аргументи `<branch> "<опис>"`, гарантує worktree (детектить існуючу ізоляцію або створює новий), повертає метадані. |
|
|
17
|
-
| `init(rest, deps?)` | `async function` | Handler `flow init`: ізоляція + первинна ініціалізація `.flow.json`. |
|
|
18
|
-
| `verify(_rest, deps?)` | `async function` | Handler `flow verify`: Quality Gates («Суддя») у поточному worktree з толерантністю до відсутності стану. |
|
|
19
|
-
| `matchChangedWorkspaces(subWorkspaces, changedFiles)` | `function` | Чистий хелпер: підмножина воркспейсів, у яких є зміни (з прив'язкою до найглибшого збігу). |
|
|
20
|
-
| `release(rest, deps?)` | `async function` | Handler `flow release`: генерує `.changes` через `n-cursor change` і пише completion snapshot. |
|
|
21
|
-
|
|
22
|
-
Усі async handler-и повертають Promise<number> — exit code (0 — ок, 1 — помилка). Решта — синхронні.
|
|
23
|
-
|
|
24
|
-
## Функції
|
|
25
|
-
|
|
26
|
-
### `realRun(cmd, args, opts = {})`
|
|
27
|
-
|
|
28
|
-
- Сигнатура: `(cmd: string, args: string[], opts?: object) => { status: number, stdout: string, stderr: string }`.
|
|
29
|
-
- Параметри:
|
|
30
|
-
- `cmd` — назва/шлях виконуваного файлу.
|
|
31
|
-
- `args` — масив аргументів.
|
|
32
|
-
- `opts` — додаткові опції для `spawnSync` (наприклад, `cwd`). Завжди примусово `encoding: 'utf8'`.
|
|
33
|
-
- Повертає об'єкт із полями `status` (1, якщо `spawnSync` повернув `null` через сигнал/помилку запуску), `stdout`, `stderr`.
|
|
34
|
-
- Side effects: синхронно стартує процес ОС.
|
|
35
|
-
|
|
36
|
-
### `inLinkedWorktree(run, cwd)` (внутрішня)
|
|
37
|
-
|
|
38
|
-
- Сигнатура: `(run, cwd: string) => boolean`.
|
|
39
|
-
- Логіка: викликає `git rev-parse --git-dir`, `--git-common-dir`, `--show-superproject-working-tree`. Worktree вважається «лінкованим», якщо обидва git-dir команди повернули код 0, це **не** submodule, і `git-dir !== git-common-dir`.
|
|
40
|
-
- Повертає `true`, якщо процес виконується всередині linked worktree (не основного checkout і не submodule). Це дозволяє `ensureWorktree` не створювати вкладений worktree.
|
|
41
|
-
- Side effects: три `git rev-parse` через переданий `run`.
|
|
42
|
-
|
|
43
|
-
### `ensureWorktree(rest, deps = {})`
|
|
44
|
-
|
|
45
|
-
- Сигнатура: `(rest: string[], deps?) => { code: number, worktreeDir?: string, branch?: string, desc?: string, baseCommit?: string | null }`.
|
|
46
|
-
- Параметри:
|
|
47
|
-
- `rest` — `[branch, ...descWords]`. Опис склеюється пробілом, `trim`-иться.
|
|
48
|
-
- `deps.run` — runner (default `realRun`).
|
|
49
|
-
- `deps.cwd` — стартовий каталог (default `process.cwd()`).
|
|
50
|
-
- `deps.log` — функція логування (default `console.error`).
|
|
51
|
-
- Поведінка:
|
|
52
|
-
1. Якщо `branch` або `desc` порожні — логує usage і повертає `{ code: 1 }`.
|
|
53
|
-
2. Якщо вже в linked worktree — використовує поточний `cwd` як `worktreeDir`, логує підказку.
|
|
54
|
-
3. Інакше — викликає `npx @nitra/cursor worktree add <branch> <desc>`; на помилку повертає `{ code: 1 }` із поясненням зі `stderr`.
|
|
55
|
-
4. Дізнається `HEAD` у `worktreeDir`. Якщо `git rev-parse HEAD` успішний — це `baseCommit`, інакше `null`.
|
|
56
|
-
- Side effects: можливе створення worktree через зовнішній CLI, git-виклики.
|
|
57
|
-
|
|
58
|
-
### `init(rest, deps = {})`
|
|
59
|
-
|
|
60
|
-
- Сигнатура: `(rest: string[], deps?) => Promise<number>`.
|
|
61
|
-
- Параметри:
|
|
62
|
-
- `rest` — `[branch, ...descWords]`.
|
|
63
|
-
- `deps.now` — джерело часу (default `Date.now`); решта успадковуються `ensureWorktree`.
|
|
64
|
-
- Кроки:
|
|
65
|
-
1. Делегує `ensureWorktree`; ранній exit при `code !== 0`.
|
|
66
|
-
2. Шлях стану: `flowStatePath(worktreeDir)` (з `state-store.mjs`).
|
|
67
|
-
3. Визначає `level` та `risk` через `detectLevel(desc)` / `detectRisk(desc)` (з `level.mjs`).
|
|
68
|
-
4. Через `writeState` записує початковий запис: `branch`, `status: 'in_progress'`, `started_at` (ISO від `now()`), `metadata.base_commit`, `level`, `risk`, порожній `plan: []`.
|
|
69
|
-
5. Логує підсумок і повертає `0`.
|
|
70
|
-
- Side effects: створення/перезапис `.flow.json`.
|
|
71
|
-
|
|
72
|
-
### `verify(_rest, deps = {})`
|
|
73
|
-
|
|
74
|
-
- Сигнатура: `(_rest: string[], deps?) => Promise<number>`. `_rest` не використовується.
|
|
75
|
-
- Параметри `deps`:
|
|
76
|
-
- `run`, `cwd`, `log` (стандартні).
|
|
77
|
-
- `branch` — опціональний явний фільтр для резолва активного flow.
|
|
78
|
-
- `fingerprint` — фабрика fingerprint-функції; default — `worktreeFingerprint` із `cwd`-залежним sync-runner-ом.
|
|
79
|
-
- Кроки:
|
|
80
|
-
1. `resolveActiveFlowState({ cwd, branch }, deps)` — cwd-незалежний пошук активного `.flow.json`. Якщо знайшли autoResolved — логує лейбл.
|
|
81
|
-
2. Якщо `branch` явно задано і не резолвиться — це помилка наміру: повертає `1` із поясненням (інакше `flow verify --branch typo` міг би «зеленіти» в CI).
|
|
82
|
-
3. Робочий `cwd` для gate-ів: `resolved.worktreeDir ?? cwd0`.
|
|
83
|
-
4. Читає стан; якщо нема (відсутній/пошкоджений `.flow.json`) — verify лишається толерантним: гейти прогоняються standalone, без запису стану. Логує warn із описом причини.
|
|
84
|
-
5. Якщо стан є, але `plan` порожній — лише warning (м'які ворота).
|
|
85
|
-
6. Викликає `runReview({ run, cwd, fingerprint })` (з `reviewer.mjs`). Отримує `{ pass, gates, fingerprint, failedOutput }`.
|
|
86
|
-
7. Для кожного gate логує `✅`/`❌`. На фейл — `failedOutput`.
|
|
87
|
-
8. Якщо стан був — `recordTransition` записує подію `{ type: 'verify', pass }` і оновлює `gates`, `fingerprint`, `status` (`failed` при провалі, інакше зберігає попередній).
|
|
88
|
-
9. Повертає `0` / `1` залежно від `verdict.pass`.
|
|
89
|
-
- Side effects: gate-команди (lint/test/тощо) у `cwd`, лог-вивід, можливі `.flow.json` + події.
|
|
90
|
-
|
|
91
|
-
### `matchChangedWorkspaces(subWorkspaces, changedFiles)`
|
|
92
|
-
|
|
93
|
-
- Сигнатура: `(subWorkspaces: string[], changedFiles: string[]) => string[]`.
|
|
94
|
-
- Параметри:
|
|
95
|
-
- `subWorkspaces` — теки воркспейсів **без** кореня (`.`).
|
|
96
|
-
- `changedFiles` — змінені шляхи відносно кореня репозиторію у posix-форматі.
|
|
97
|
-
- Логіка: сортує воркспейси за довжиною (спадно), для кожного зміненого файла знаходить **найглибший** збіг (`f === w || f.startsWith(w + '/')`). Таке правило усуває хибне `«кілька воркспейсів»` для випадку, коли `apps` і `apps/web` обидва зареєстровані, а файл `apps/web/x` має належати лише найглибшому.
|
|
98
|
-
- Повертає підмножину `subWorkspaces` (у вхідному порядку), які отримали хоч один хіт.
|
|
99
|
-
- Side effects: немає (чиста функція).
|
|
100
|
-
|
|
101
|
-
### `resolveChangeWsArgs({ rest, baseCommit, cwd, listWorkspaces, changedFilesSince, log })` (внутрішня)
|
|
102
|
-
|
|
103
|
-
- Сигнатура: `(input) => Promise<{ args: string[], error?: boolean }>`.
|
|
104
|
-
- Призначення: добудовує `--ws <шлях>` до аргументів `change`, якщо користувач не задав явно.
|
|
105
|
-
- Кроки:
|
|
106
|
-
1. Якщо `rest` уже містить `--ws` або `--ws=...` — повертає `rest` без змін (поважає явний намір).
|
|
107
|
-
2. `listWorkspaces(cwd)` → масив. Відсіює корінь (`.`); якщо subworkspace-ів нема — `change` дефолтиться на `.`, лишаємо як є.
|
|
108
|
-
3. `hits = matchChangedWorkspaces(subWs, changedFilesSince(baseCommit, cwd))`.
|
|
109
|
-
4. `hits.length > 1` → fail-hard: `{ args: rest, error: true }`, лог із переліком.
|
|
110
|
-
5. `hits.length === 1` → додає `--ws <hits[0]>` і логує інференс.
|
|
111
|
-
6. `hits.length === 0` → лишає `rest`.
|
|
112
|
-
7. У будь-якому виключенні від `listWorkspaces` / `changedFilesSince` — fail-soft: лог warning, повертає `rest`.
|
|
113
|
-
- Side effects: `git diff`-подібні виклики через `changedFilesSince`, виклик `listWorkspaces`, логування.
|
|
114
|
-
|
|
115
|
-
### `release(rest, deps = {})`
|
|
116
|
-
|
|
117
|
-
- Сигнатура: `(rest: string[], deps?) => Promise<number>`.
|
|
118
|
-
- Параметри `deps`:
|
|
119
|
-
- `run`, `cwd`, `log`, `now` — стандартні.
|
|
120
|
-
- `branch` — опціональний фільтр активного flow.
|
|
121
|
-
- `listWorkspaces` — default `getMonorepoProjectRootDirs` (з `rules/changelog/lib/package-manifest.mjs`).
|
|
122
|
-
- `changedFilesSince` — default `collectChangedFilesSince`.
|
|
123
|
-
- Кроки:
|
|
124
|
-
1. `resolveActiveFlowState({ cwd, branch }, deps)`. Якщо `statePath` не знайдено — `release` падає (`code 1`) із поясненням (це обов'язкова прив'язка).
|
|
125
|
-
2. `effectiveCwd = resolved.worktreeDir ?? cwd`.
|
|
126
|
-
3. `readState(statePath)`. Якщо стану нема — `release: стану нема — спершу 'flow init'`, `1`.
|
|
127
|
-
4. Якщо `state.gate?.verdict === 'FAIL'` — лише warning (м'які ворота, рішення за людиною).
|
|
128
|
-
5. `resolveChangeWsArgs(...)`. Якщо повернув `error: true` (кілька воркспейсів) — exit `1`.
|
|
129
|
-
6. Викликає `npx @nitra/cursor change <args>` у `effectiveCwd`. Помилка → exit `1` із поясненням зі stderr.
|
|
130
|
-
7. `buildCompletionSnapshot({ ...state, status: 'done' }, now)` — снапшот завершення.
|
|
131
|
-
8. `recordTransition` записує подію `{ type: 'release' }`, оновлює стан: `status: 'done'`, `completion: snapshot`.
|
|
132
|
-
9. Якщо в стані вказано `state.task` (шлях task-record) — пише summary у task через `writeSummaryToTaskRecord` (з абсолютизацією через `join(effectiveCwd, …)`, якщо шлях відносний).
|
|
133
|
-
10. Логує `release: done`, повертає `0`.
|
|
134
|
-
- Side effects: `npx … change`, запис у `.flow.json`, можливий запис у task-record, події.
|
|
135
|
-
|
|
136
|
-
## Залежності
|
|
137
|
-
|
|
138
|
-
### Імпорти Node-стандарту
|
|
139
|
-
|
|
140
|
-
- `node:child_process` → `spawnSync` (для `realRun` і дефолтного `fingerprint`).
|
|
141
|
-
- `node:path` → `isAbsolute`, `join` (для шляху task-record у `release`).
|
|
142
|
-
- `node:process` → `cwd as processCwd` (default `cwd`).
|
|
143
|
-
|
|
144
|
-
### Внутрішні модулі проєкту
|
|
145
|
-
|
|
146
|
-
- `../../lib/worktree.mjs` → `worktreePaths` — резолв шляху checkout після `worktree add`.
|
|
147
|
-
- `../../lib/changed-files.mjs` → `collectChangedFilesSince` — default для `changedFilesSince` у `release`.
|
|
148
|
-
- `../../utils/worktree-fingerprint.mjs` → `worktreeFingerprint` — default для `verify`.
|
|
149
|
-
- `../../../rules/changelog/lib/package-manifest.mjs` → `getMonorepoProjectRootDirs` — default для `listWorkspaces`.
|
|
150
|
-
- `./events.mjs` → `flowEventsPath` — шлях файла подій flow.
|
|
151
|
-
- `./level.mjs` → `detectLevel`, `detectRisk` — класифікація задачі на основі опису.
|
|
152
|
-
- `./reviewer.mjs` → `runReview` — Quality Gates («Суддя»).
|
|
153
|
-
- `./snapshot.mjs` → `buildCompletionSnapshot`, `writeSummaryToTaskRecord` — completion-снапшот для `release`.
|
|
154
|
-
- `./state-store.mjs` → `flowStatePath`, `readState`, `recordTransition`, `writeState` — персистенція `.flow.json` + події.
|
|
155
|
-
- `./flow-resolve.mjs` → `resolveActiveFlowState` — cwd-незалежний резолв активного flow.
|
|
156
|
-
|
|
157
|
-
### Зовнішні CLI (через `run`)
|
|
158
|
-
|
|
159
|
-
- `npx @nitra/cursor worktree add <branch> <desc>` — створення worktree (`ensureWorktree`).
|
|
160
|
-
- `npx @nitra/cursor change <args>` — генерація `.changes` (`release`).
|
|
161
|
-
- `git rev-parse --git-dir|--git-common-dir|--show-superproject-working-tree|HEAD` — детекція worktree та base-коміт.
|
|
162
|
-
|
|
163
|
-
## Потік виконання / Використання
|
|
164
|
-
|
|
165
|
-
### Типовий happy-path Ф2
|
|
166
|
-
|
|
167
|
-
1. `n-cursor flow init feature/x "опис задачі"` → `init`:
|
|
168
|
-
- `ensureWorktree` створює (або підхоплює) worktree.
|
|
169
|
-
- `detectLevel` / `detectRisk` класифікують задачу.
|
|
170
|
-
- Створюється `.flow.json` зі `status: 'in_progress'`, фіксується `base_commit`.
|
|
171
|
-
2. Робота над кодом всередині worktree.
|
|
172
|
-
3. `n-cursor flow verify` → `verify`:
|
|
173
|
-
- Резолвиться активний flow (cwd-незалежно).
|
|
174
|
-
- `runReview` прогоняє gate-и (lint, тести, тощо).
|
|
175
|
-
- Стан оновлюється: `gates`, `fingerprint`. На фейл — `status: 'failed'`.
|
|
176
|
-
4. `n-cursor flow release --bump minor --section feat --message "…"` → `release`:
|
|
177
|
-
- Резолв активного flow (обов'язковий).
|
|
178
|
-
- Інференс `--ws` із diff від `base_commit` (якщо не задано явно).
|
|
179
|
-
- `npx @nitra/cursor change …` пише `.changes`.
|
|
180
|
-
- `buildCompletionSnapshot` + `recordTransition` фіксують `status: 'done'`.
|
|
181
|
-
- Якщо є `state.task` — summary йде у task-record.
|
|
182
|
-
|
|
183
|
-
### Точки розширення (через `deps`)
|
|
184
|
-
|
|
185
|
-
- Юніт-тести підставляють фейкові `run`, `log`, `now`, `fingerprint`, `listWorkspaces`, `changedFilesSince`, `branch`, `cwd`.
|
|
186
|
-
- Це дозволяє покрити edge-кейси: відсутній стан, кілька воркспейсів у diff, помилки `npx`, артефакти `git rev-parse`.
|
|
187
|
-
|
|
188
|
-
### Контракти CLI
|
|
189
|
-
|
|
190
|
-
- `init` / `release` потребують `<branch> "<опис>"` (init) та активного flow + опційних `--bump|--section|--message|--ws` (release).
|
|
191
|
-
- `verify` — без обов'язкових аргументів; толерантний до відсутності стану (запуск standalone у поточному `cwd`).
|
|
192
|
-
- `--branch <name>` як `deps.branch` дозволяє адресувати конкретний flow (CI/довільний `cwd`).
|
|
193
|
-
|
|
194
|
-
### Інваріанти
|
|
195
|
-
|
|
196
|
-
- Усі handler-и **не змінюють код** проєкту (`verify` read-only, `release` лише пише `.changes` / `.flow.json` / task-summary).
|
|
197
|
-
- Worktree-вкладеність заборонена: `ensureWorktree` детектить, що `cwd` уже linked worktree, і повторно `worktree add` не викликає.
|
|
198
|
-
- М'які ворота: і відсутність `plan`, і `gate.verdict === 'FAIL'` дають лише warning, рішення про реліз — за людиною.
|
|
199
|
-
- Fail-hard у `release`: відсутність активного flow та неоднозначний воркспейс (multi-hit `matchChangedWorkspaces`).
|
|
200
|
-
|
|
201
|
-
## Rebuild Test
|
|
202
|
-
|
|
203
|
-
Маючи цю документацію, інженер може відтворити публічний контракт модуля без читання вихідного коду:
|
|
204
|
-
|
|
205
|
-
- Знає список і сигнатури експортованих функцій (`realRun`, `ensureWorktree`, `init`, `verify`, `matchChangedWorkspaces`, `release`) та їхні exit code.
|
|
206
|
-
- Розуміє роль `deps` як набору ін'єкцій (`run`, `cwd`, `log`, `now`, `fingerprint`, `branch`, `listWorkspaces`, `changedFilesSince`).
|
|
207
|
-
- Може повторити доменну поведінку: детекцію linked worktree, інференс `--ws` (single/multi/empty/explicit), толерантність `verify` до відсутнього стану, fail-hard `release`, м'які ворота на `plan`/`gate.verdict`.
|
|
208
|
-
- Знає форму `.flow.json` (поля `branch`, `status`, `started_at`, `metadata.base_commit`, `level`, `risk`, `plan`, `gates`, `fingerprint`, `completion`, `task`) і які підкоманди їх записують.
|
|
209
|
-
- Бачить зовнішні CLI/git-залежності та внутрішні модулі, від яких залежить логіка.
|
|
210
|
-
- Розуміє, які підкоманди (`run`/`resume`/`cancel`/`repair`) ще не реалізовані тут (Ф4).
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
# events.mjs — WAL-журнал подій `flow`
|
|
2
|
-
|
|
3
|
-
## Огляд
|
|
4
|
-
|
|
5
|
-
Модуль `events.mjs` реалізує **append-only WAL** (Write-Ahead Log) журнал подій для підсистеми `flow` (специфікація §4.1.2, §9). Журнал зберігається у sibling-файлі поряд із checkout-каталогом worktree: для `…/.worktrees/feat-x` файл подій має шлях `…/.worktrees/feat-x.events.jsonl` (формат JSON Lines — по одному JSON-об'єкту на рядок).
|
|
6
|
-
|
|
7
|
-
Журнал є **єдиним** для двох категорій подій:
|
|
8
|
-
|
|
9
|
-
- **Переходи стану** flow (наприклад, `step_started`, `step_completed`, `blocked` тощо).
|
|
10
|
-
- **Облік API-викликів** (події типу `api_call`).
|
|
11
|
-
|
|
12
|
-
Ключові інваріанти та властивості:
|
|
13
|
-
|
|
14
|
-
- **Append-only**. Нові записи лише дописуються в кінець файлу. Це робить журнал **краш-безпечнішим** за повний перезапис: якщо аварія сталася посередині запису рядка, ушкоджений (торваний) рядок при читанні **толерується** — він просто пропускається, а решта журналу залишається валідною.
|
|
15
|
-
- **WAL-інваріант**: подію слід дописати в журнал **ДО** оновлення високорівневого статусу у snapshot-файлі `.flow.json`. За дотримання цього інваріанта відповідає сторонній викликач — `state-store.recordTransition`.
|
|
16
|
-
- **Усі шляхи — абсолютні**. Модуль явно валідує кожен вхідний `path` і кидає помилку для відносних шляхів (правило проекту `no-relative-fs-path`).
|
|
17
|
-
|
|
18
|
-
Модуль використовує **синхронні** Node.js fs-API (`appendFileSync`, `readFileSync`, `existsSync`), що адекватно для коротких записів короткоживучих CLI/dispatcher-сценаріїв і гарантує лінійний порядок без race-conditions у межах одного процесу.
|
|
19
|
-
|
|
20
|
-
## Експорти / API
|
|
21
|
-
|
|
22
|
-
Усі експорти — іменовані ESM-функції.
|
|
23
|
-
|
|
24
|
-
| Експорт | Тип | Призначення |
|
|
25
|
-
| -------------------------------------- | ------------------------------------------- | --------------------------------------------------------------------------------------------------- |
|
|
26
|
-
| `flowEventsPath(worktreeDir)` | `(string) => string` | Обчислити абсолютний шлях sibling-журналу для checkout-каталогу worktree. |
|
|
27
|
-
| `appendEvent(eventsPath, event, now?)` | `(string, object, () => number?) => object` | Дописати одну подію (зі стампом `at`) у журнал; повертає фактично записаний об'єкт. |
|
|
28
|
-
| `readEvents(eventsPath)` | `(string) => object[]` | Прочитати всі події з журналу в порядку запису; відсутній файл → `[]`; невалідні рядки ігноруються. |
|
|
29
|
-
|
|
30
|
-
Default-експорт відсутній.
|
|
31
|
-
|
|
32
|
-
## Функції
|
|
33
|
-
|
|
34
|
-
### `flowEventsPath(worktreeDir)`
|
|
35
|
-
|
|
36
|
-
**Сигнатура.** `function flowEventsPath(worktreeDir: string): string`
|
|
37
|
-
|
|
38
|
-
**Параметри.**
|
|
39
|
-
|
|
40
|
-
- `worktreeDir` — **абсолютний** шлях до checkout-каталогу worktree (наприклад, `/repo/.worktrees/feat-x`).
|
|
41
|
-
|
|
42
|
-
**Повертає.** Абсолютний шлях до sibling-файлу журналу подій: каталог-батько (`dirname(worktreeDir)`) + базове ім'я worktree + суфікс `.events.jsonl`. Наприклад, для входу `/repo/.worktrees/feat-x` повертається `/repo/.worktrees/feat-x.events.jsonl`.
|
|
43
|
-
|
|
44
|
-
**Помилки.** Якщо `worktreeDir` не є абсолютним шляхом — кидає `Error` з префіксом `flowEventsPath: очікується абсолютний шлях …`.
|
|
45
|
-
|
|
46
|
-
**Side effects.** Жодних. Чиста функція (працює лише з рядками через `path`-утиліти).
|
|
47
|
-
|
|
48
|
-
**Деталі реалізації.** Використовує `dirname` і `basename` з `node:path`, що робить функцію крос-платформною щодо роздільників шляху (хоча в результаті завжди формується шлях у стилі поточної ОС).
|
|
49
|
-
|
|
50
|
-
### `appendEvent(eventsPath, event, now?)`
|
|
51
|
-
|
|
52
|
-
**Сигнатура.** `function appendEvent(eventsPath: string, event: object, now?: () => number): object`
|
|
53
|
-
|
|
54
|
-
**Параметри.**
|
|
55
|
-
|
|
56
|
-
- `eventsPath` — **абсолютний** шлях до файлу `.events.jsonl`.
|
|
57
|
-
- `event` — об'єкт події, наприклад `{ type: 'step_started', step: 2 }`. Може містити довільні поля.
|
|
58
|
-
- `now` — _необов'язкова_ фабрика часу, що повертає мілісекунди (за замовчуванням `Date.now`). Дозволяє **ін'єкцію** детермінованого часу у тестах.
|
|
59
|
-
|
|
60
|
-
**Повертає.** Фактично записаний запис у формі `{ at, ...event }`, де `at` — ISO-рядок часу (`new Date(now()).toISOString()`). Поля переданого `event` накладаються **поверх** `at`, тому якщо в `event` уже є власне поле `at`, воно перепише згенероване (це може бути корисно для бек-філу/міграцій).
|
|
61
|
-
|
|
62
|
-
**Side effects.**
|
|
63
|
-
|
|
64
|
-
- **Файлова система**: дописує один рядок (`JSON.stringify(record) + '\n'`) у `eventsPath` через `appendFileSync` із кодуванням `utf8`. Якщо файлу не існує — він **створюється** автоматично (`appendFileSync` діє як append-or-create).
|
|
65
|
-
- **Жодних мережевих, БД- чи stdout-ефектів.**
|
|
66
|
-
|
|
67
|
-
**Помилки.**
|
|
68
|
-
|
|
69
|
-
- Якщо `eventsPath` не абсолютний — кидає `Error` з префіксом `appendEvent: очікується абсолютний шлях …`.
|
|
70
|
-
- Будь-яка помилка `appendFileSync` (відсутня тека-батько, права, EROFS тощо) пробрасується назовні як є.
|
|
71
|
-
|
|
72
|
-
**Краш-безпека.** Якщо процес аварійно завершиться під час `appendFileSync`, у файлі може залишитися **частково записаний останній рядок**. Це навмисно толерується читачем (`readEvents`), тому WAL не вимагає fsync чи двофазного запису для базової стійкості.
|
|
73
|
-
|
|
74
|
-
### `readEvents(eventsPath)`
|
|
75
|
-
|
|
76
|
-
**Сигнатура.** `function readEvents(eventsPath: string): object[]`
|
|
77
|
-
|
|
78
|
-
**Параметри.**
|
|
79
|
-
|
|
80
|
-
- `eventsPath` — **абсолютний** шлях до файлу `.events.jsonl`.
|
|
81
|
-
|
|
82
|
-
**Повертає.** Масив об'єктів-подій у тому самому порядку, в якому вони були дописані (хронологічний порядок append-only). Якщо файлу немає — повертається `[]`.
|
|
83
|
-
|
|
84
|
-
**Side effects.** Лише читання файлу (`readFileSync`, `existsSync`). Файл **не модифікується** і **не створюється**.
|
|
85
|
-
|
|
86
|
-
**Поведінка при пошкодженнях.**
|
|
87
|
-
|
|
88
|
-
- Порожні рядки (включно з трейлінговим `\n` після останнього запису) **відфільтровуються** через `line.trim() !== ''`.
|
|
89
|
-
- Кожен непорожній рядок намагається парситися як JSON. Якщо `JSON.parse` кидає виняток (наприклад, торваний останній рядок після краху під час `appendFileSync`), цей рядок **пропускається** (через `flatMap` + порожній масив у `catch`), і читання продовжується далі. Інші рядки повертаються нормально.
|
|
90
|
-
- Це і є **append-only толерантність**: один пошкоджений запис не валить весь журнал.
|
|
91
|
-
|
|
92
|
-
**Помилки.** Якщо `eventsPath` не абсолютний — кидає `Error` з префіксом `readEvents: очікується абсолютний шлях …`. Інші помилки fs (наприклад, EACCES при `readFileSync`) пробрасуються.
|
|
93
|
-
|
|
94
|
-
**Складність.** Лінійна за розміром файлу (`O(N)`), читання здійснюється повністю в пам'ять. Для дуже довгих журналів варто розглянути потокове читання — модуль навмисно тримає просту синхронну реалізацію.
|
|
95
|
-
|
|
96
|
-
## Залежності
|
|
97
|
-
|
|
98
|
-
### Зовнішні (Node.js stdlib)
|
|
99
|
-
|
|
100
|
-
- `node:fs`
|
|
101
|
-
- `appendFileSync` — атомарний (на рівні syscall) append одного запису.
|
|
102
|
-
- `existsSync` — перевірка наявності файлу перед читанням.
|
|
103
|
-
- `readFileSync` — повне читання файлу журналу.
|
|
104
|
-
- `node:path`
|
|
105
|
-
- `basename`, `dirname`, `isAbsolute`, `join` — формування sibling-шляху і валідація абсолютних шляхів.
|
|
106
|
-
|
|
107
|
-
### Внутрішні
|
|
108
|
-
|
|
109
|
-
Модуль **не має внутрішніх імпортів**. Він — найнижчий рівень WAL-абстракції, на який спираються інші частини dispatcher/flow:
|
|
110
|
-
|
|
111
|
-
- `state-store.recordTransition` (споживач) — викликає `appendEvent` перед оновленням `.flow.json` (забезпечення WAL-інваріанта).
|
|
112
|
-
- Будь-який код обліку `api_call`-подій також використовує `appendEvent`.
|
|
113
|
-
- Читачі (наприклад, інструменти діагностики, відновлення стану, аналітики) використовують `readEvents`.
|
|
114
|
-
|
|
115
|
-
## Потік виконання / Використання
|
|
116
|
-
|
|
117
|
-
### Типовий життєвий цикл подій worktree
|
|
118
|
-
|
|
119
|
-
1. **Обчислення шляху журналу** — один раз на сесію/процес:
|
|
120
|
-
```js
|
|
121
|
-
import { flowEventsPath, appendEvent, readEvents } from './events.mjs'
|
|
122
|
-
|
|
123
|
-
const eventsPath = flowEventsPath('/repo/.worktrees/feat-x')
|
|
124
|
-
// → '/repo/.worktrees/feat-x.events.jsonl'
|
|
125
|
-
```
|
|
126
|
-
2. **Дозапис події під час переходу стану** (через `state-store.recordTransition`, що дотримується WAL-інваріанта):
|
|
127
|
-
```js
|
|
128
|
-
appendEvent(eventsPath, { type: 'step_started', step: 2 })
|
|
129
|
-
// потім: оновити .flow.json
|
|
130
|
-
```
|
|
131
|
-
3. **Дозапис обліку API-виклику** (тим самим API, у тому ж файлі):
|
|
132
|
-
```js
|
|
133
|
-
appendEvent(eventsPath, {
|
|
134
|
-
type: 'api_call',
|
|
135
|
-
provider: 'anthropic',
|
|
136
|
-
model: 'claude-opus',
|
|
137
|
-
tokens: 1234
|
|
138
|
-
})
|
|
139
|
-
```
|
|
140
|
-
4. **Читання історії** (наприклад, для відновлення стану або аудиту):
|
|
141
|
-
```js
|
|
142
|
-
const events = readEvents(eventsPath)
|
|
143
|
-
for (const ev of events) {
|
|
144
|
-
// обробка ev.at, ev.type, …
|
|
145
|
-
}
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
### Сценарій краху
|
|
149
|
-
|
|
150
|
-
1. `appendFileSync` починає писати рядок, процес гине посеред запису.
|
|
151
|
-
2. Файл містить N−1 валідних рядків + один торваний (наприклад, без `}` і `\n` у кінці).
|
|
152
|
-
3. На наступному старті `readEvents`:
|
|
153
|
-
- відфільтрує порожні рядки;
|
|
154
|
-
- спробує `JSON.parse` для кожного непорожнього;
|
|
155
|
-
- на торваному останньому отримає виняток → `flatMap` поверне `[]` для нього → запис **пропускається**;
|
|
156
|
-
- решта N−1 подій нормально потрапить у результат.
|
|
157
|
-
4. Дані до моменту краху збережено; пошкоджений запис вважається втраченим — це прийнятна семантика append-only WAL.
|
|
158
|
-
|
|
159
|
-
### Інваріанти, що накладає модуль
|
|
160
|
-
|
|
161
|
-
- **Усі шляхи — абсолютні** (`isAbsolute` перевірка у всіх трьох експортах).
|
|
162
|
-
- **Кодування файлу — UTF-8** (явно вказано як в `appendFileSync`, так і в `readFileSync`).
|
|
163
|
-
- **Формат рядка — однорядковий JSON + `\n`** (jsonl).
|
|
164
|
-
- **`at` — ISO-8601-рядок UTC** (через `Date.prototype.toISOString`).
|
|
165
|
-
- **Порядок збереження — порядок дозапису** (нативна семантика append-only).
|
|
166
|
-
|
|
167
|
-
### Чого модуль свідомо не робить
|
|
168
|
-
|
|
169
|
-
- Не реалізує **rotation / compaction** журналу — це відповідальність вищих рівнів або інфраструктури worktree.
|
|
170
|
-
- Не виконує **fsync** після кожного append — покладається на ОС-кеш та append-only толерантність читача.
|
|
171
|
-
- Не валідує **схему події** — приймає будь-який `object` (контрактом структури подій відає `flow`-специфікація і споживачі).
|
|
172
|
-
- Не обробляє **багатопроцесний конкурентний доступ** — для одного worktree передбачається один пишучий процес; для читача конкурентність безпечна, оскільки `appendFileSync` атомарно дозаписує цілий буфер.
|
|
173
|
-
|
|
174
|
-
## Rebuild Test
|
|
175
|
-
|
|
176
|
-
Документація достатня, щоб переписати модуль з нуля:
|
|
177
|
-
|
|
178
|
-
- три іменовані експорти з точними сигнатурами;
|
|
179
|
-
- формула sibling-шляху (`dirname` + `basename` + `.events.jsonl`);
|
|
180
|
-
- WAL-інваріант і його носій (`state-store.recordTransition`);
|
|
181
|
-
- формат запису (`{ at: ISO, ...event }`, `JSON.stringify` + `\n`, кодування UTF-8);
|
|
182
|
-
- стратегія читання (`existsSync`-гард → split `\n` → фільтр порожніх → `JSON.parse` у `try/catch` + `flatMap`);
|
|
183
|
-
- правила валідації абсолютних шляхів і повідомлення про помилки.
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
# executor.mjs
|
|
2
|
-
|
|
3
|
-
## Огляд
|
|
4
|
-
|
|
5
|
-
Модуль `executor.mjs` — це **Фаза 3 (Ф3) диспетчера** з flow-специфікації: він покроково виконує план, що раніше зібрав `planner`, і просуває стан через журнал подій. Відповідно до спеки (§3 Ф3), кожен крок плану надсилається в субагент через **мікропромпт зі стану** — субагент бачить лише поточний крок, критерії приймання й, опційно, останню помилку (без історії переписки чи історії інших кроків).
|
|
6
|
-
|
|
7
|
-
Базові інваріанти, які реалізовано в коді:
|
|
8
|
-
|
|
9
|
-
- **Мікропромпт зі стану** (§3 Ф3): субагент отримує тільки поточний крок + критерії + останню помилку, а не повний контекст ланцюга кроків.
|
|
10
|
-
- **Commit лише після зеленого `verify`** (§4.1.7): жоден repair-прохід не комітиться, тому `HEAD` git-репозиторію завжди вказує на останній «зелений» крок плану.
|
|
11
|
-
- **Repair обмежений `maxRepairAttempts`** (за замовчуванням 3): коли спроби вичерпано, виконання переходить у режим `blocked-on-human` (HITL, §4.2) і записує питання в `state.hitl`.
|
|
12
|
-
|
|
13
|
-
Усі побічні дії (запуск субагента, верифікація, commit, годинник) **інжектуються** через об’єкт `deps`. Це робить модуль повністю детермінованим і тестованим: реальний LLM, git і ворота (gates) не викликаються з нього напряму.
|
|
14
|
-
|
|
15
|
-
## Експорти / API
|
|
16
|
-
|
|
17
|
-
Модуль експортує три функції (ESM, named exports):
|
|
18
|
-
|
|
19
|
-
| Експорт | Тип | Призначення |
|
|
20
|
-
| ------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
21
|
-
| `microprompt` | `function` | Чиста функція побудови тексту мікропромпта для конкретного кроку плану. |
|
|
22
|
-
| `patchStep` | `function` | Чиста функція, що повертає новий обʼєкт стану з оновленим кроком за індексом (immutable update). |
|
|
23
|
-
| `executePlan` | `async` | Головна функція: читає стан, ітерує план, дергає `runner`/`verify`/`commit`, записує транзиції стану та повертає підсумковий статус. |
|
|
24
|
-
|
|
25
|
-
Default-експорту немає.
|
|
26
|
-
|
|
27
|
-
## Функції
|
|
28
|
-
|
|
29
|
-
### `microprompt(step, state)`
|
|
30
|
-
|
|
31
|
-
**Сигнатура:**
|
|
32
|
-
|
|
33
|
-
```js
|
|
34
|
-
function microprompt(step, state) → string
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
**Параметри:**
|
|
38
|
-
|
|
39
|
-
- `step` — обʼєкт поточного кроку плану. Очікувані поля:
|
|
40
|
-
- `step` (`number`) — номер кроку (для людини).
|
|
41
|
-
- `task` (`string`) — формулювання задачі кроку.
|
|
42
|
-
- `acceptance` (`string`, опційно) — критерії приймання.
|
|
43
|
-
- `hint` (`string`, опційно) — підказка від людини (HITL).
|
|
44
|
-
- `last_error` (`string`, опційно) — останній текст помилки `verify` для repair-спроби.
|
|
45
|
-
- `state` — поточний стан flow. Використовується лише поле `state.branch` для рядка «Гілка: …» (якщо немає — підставляється `'—'`).
|
|
46
|
-
|
|
47
|
-
**Повертає:** рядок-промпт, який скріплює кілька рядків через `\n`:
|
|
48
|
-
|
|
49
|
-
1. Заклик зробити **рівно** один крок плану з нагадуванням про Iron Law of TDD (спершу падаючі тести, тоді код).
|
|
50
|
-
2. `Гілка: <state.branch ?? '—'>`.
|
|
51
|
-
3. `Крок <step.step>: <step.task>`.
|
|
52
|
-
4. (Якщо є `acceptance`) `Критерії приймання: …`.
|
|
53
|
-
5. (Якщо є `hint`) `Підказка людини (HITL): …`.
|
|
54
|
-
6. (Якщо є `last_error`) `Попередня спроба впала на перевірці:\n<last_error>\nВиправ це.`.
|
|
55
|
-
|
|
56
|
-
**Side effects:** немає — це чиста функція форматування.
|
|
57
|
-
|
|
58
|
-
### `patchStep(state, index, patch)`
|
|
59
|
-
|
|
60
|
-
**Сигнатура:**
|
|
61
|
-
|
|
62
|
-
```js
|
|
63
|
-
function patchStep(state, index, patch) → newState
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
**Параметри:**
|
|
67
|
-
|
|
68
|
-
- `state` — обʼєкт стану з масивом `state.plan` (`object[]`).
|
|
69
|
-
- `index` (`number`) — індекс кроку в `state.plan`, який треба оновити.
|
|
70
|
-
- `patch` (`object`) — поля, які треба змерджити в крок (`{ ...step, ...patch }`).
|
|
71
|
-
|
|
72
|
-
**Повертає:** новий обʼєкт стану `{ ...state, plan: [...] }`, де крок під `index` замінено на `{ ...step, ...patch }`, інші кроки залишаються тими самими посиланнями.
|
|
73
|
-
|
|
74
|
-
**Side effects:** немає — immutable update через `Array.prototype.map`.
|
|
75
|
-
|
|
76
|
-
### `executePlan(paths, deps)`
|
|
77
|
-
|
|
78
|
-
**Сигнатура:**
|
|
79
|
-
|
|
80
|
-
```js
|
|
81
|
-
async function executePlan(paths, deps) → Promise<{ status: 'done' | 'blocked-on-human', step?: number }>
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
**Параметри:**
|
|
85
|
-
|
|
86
|
-
- `paths` — обʼєкт зі шляхами до файлів стану й журналу подій:
|
|
87
|
-
- `paths.statePath` — шлях до файлу стану, що читається `readState`.
|
|
88
|
-
- `paths.eventsPath` — шлях до журналу подій (передається далі в `recordTransition`).
|
|
89
|
-
- `deps` — обʼєкт ін’єкцій:
|
|
90
|
-
- `runner` (обовʼязково) — обʼєкт із методом `runStep(prompt, opts?)`. Викликається з мікропромптом і `{ cwd }`.
|
|
91
|
-
- `verify` (обовʼязково) — `(cwd) → { pass: boolean, failedOutput?: string }` або проміс такого ж обʼєкта. Поле `pass` визначає, чи крок зелений; `failedOutput` йде у `last_error`.
|
|
92
|
-
- `commit` (обовʼязково) — `(cwd, msg) → void`. Викликається **лише** після зеленого `verify`, з повідомленням `flow: step <N> — <task>`.
|
|
93
|
-
- `cwd` (опційно) — робочий каталог, який передається `runner` і `verify`/`commit`.
|
|
94
|
-
- `maxRepairAttempts` (`number`, за замовчуванням `3`) — максимальна кількість repair-спроб на крок (перша спроба теж рахується в `retry_count`).
|
|
95
|
-
- `log` (`(msg) => void`, за замовчуванням `() => {}`) — лоґер прогресу.
|
|
96
|
-
- `now` (`() => number`, за замовчуванням `Date.now`) — джерело часу для `recordTransition`.
|
|
97
|
-
|
|
98
|
-
**Повертає:**
|
|
99
|
-
|
|
100
|
-
- `{ status: 'done' }` — якщо всі кроки плану позначено `done`, фінальна транзиція `plan_done` піднімає `state.status = 'built'`.
|
|
101
|
-
- `{ status: 'blocked-on-human', step: <N> }` — якщо на якомусь кроці вичерпано `maxRepairAttempts`. Тоді у стан додано HITL-питання й виставлено `state.status = 'blocked-on-human'`.
|
|
102
|
-
|
|
103
|
-
**Кидає:** `Error('executor: у стані немає плану — спершу planner')` — якщо `readState` повертає порожній обʼєкт або `plan` відсутній/порожній.
|
|
104
|
-
|
|
105
|
-
**Side effects:**
|
|
106
|
-
|
|
107
|
-
- Читає файл стану через `readState(paths.statePath)`.
|
|
108
|
-
- Записує транзиції в журнал/стан через `recordTransition(paths, event, reducer, now)` для подій: `step_done`, `step_retry`, `blocked`, `plan_done`.
|
|
109
|
-
- Викликає `runner.runStep(...)` — потенційний LLM/субагент-виклик.
|
|
110
|
-
- Викликає `verify(cwd)` — потенційний запуск тестів/гейтів.
|
|
111
|
-
- Викликає `commit(cwd, msg)` — git-commit; **тільки** при зеленому `verify`.
|
|
112
|
-
- Записує повідомлення через `log(...)`.
|
|
113
|
-
|
|
114
|
-
## Залежності
|
|
115
|
-
|
|
116
|
-
### Внутрішні модулі (імпорти)
|
|
117
|
-
|
|
118
|
-
- `./state-store.mjs`:
|
|
119
|
-
- `readState(statePath)` — синхронне читання обʼєкта стану з диска.
|
|
120
|
-
- `recordTransition(paths, event, reducer, now)` — атомарне оновлення стану й запис події в `events`-журнал; повертає новий стан.
|
|
121
|
-
|
|
122
|
-
### Інжектовані залежності (через `deps`)
|
|
123
|
-
|
|
124
|
-
- `runner.runStep` — реалізація запуску субагента (наприклад, `subagent-runner.mjs`).
|
|
125
|
-
- `verify` — функція верифікації (наприклад, обгортка над тест-командою/ворітьми).
|
|
126
|
-
- `commit` — функція git-комміту.
|
|
127
|
-
- `log`, `now` — необовʼязкові утиліти.
|
|
128
|
-
|
|
129
|
-
### Зовнішні залежності
|
|
130
|
-
|
|
131
|
-
Стандартних модулів Node.js немає у файлі напряму — увесь I/O делеговано в `state-store.mjs` та інжектовані залежності.
|
|
132
|
-
|
|
133
|
-
## Потік виконання / Використання
|
|
134
|
-
|
|
135
|
-
### Алгоритм `executePlan`
|
|
136
|
-
|
|
137
|
-
1. **Деструктуризація `deps`** з дефолтами: `maxRepairAttempts = 3`, `log = noop`, `now = Date.now`.
|
|
138
|
-
2. **Зчитування стану**: `state = readState(paths.statePath)`. Якщо `state?.plan` відсутній або порожній — кидається помилка `executor: у стані немає плану — спершу planner`.
|
|
139
|
-
3. **Ітерація плану** циклом `for (let i = 0; i < state.plan.length; i++)`:
|
|
140
|
-
- Якщо крок уже `status === 'done'` — пропустити (resume-friendly).
|
|
141
|
-
- Інакше зайти у внутрішній `while`-цикл, поки `retry_count < maxRepairAttempts` і `done === false`:
|
|
142
|
-
1. Зчитати свіжий `step = state.plan[i]`.
|
|
143
|
-
2. Залогувати: `executor: крок N (спроба M)`, де `M = retry_count + 1`.
|
|
144
|
-
3. `await runner.runStep(microprompt(step, state), { cwd })` — виконати крок субагентом з мікропромптом.
|
|
145
|
-
4. `verdict = await verify(cwd)` — перевірити результат.
|
|
146
|
-
5. **Якщо `verdict.pass === true`:**
|
|
147
|
-
- `commit(cwd, "flow: step <step.step> — <step.task>")`.
|
|
148
|
-
- `recordTransition(paths, { type: 'step_done', step }, s => patchStep(s, i, { status: 'done' }), now)`.
|
|
149
|
-
- `done = true`, вихід з `while`.
|
|
150
|
-
6. **Інакше (red):**
|
|
151
|
-
- `recordTransition` із подією `step_retry`, що інкрементує `retry_count` і кладе `last_error: verdict.failedOutput ?? null`.
|
|
152
|
-
- `while` повторює спробу (наступна ітерація прочитає вже оновлений `state.plan[i]`).
|
|
153
|
-
4. **Якщо `while` вийшов з `done === false`** (тобто всі спроби спалено):
|
|
154
|
-
- Будується HITL-питання `{ id: 'q-<i>', step, question: '…не проходить verify після N спроб…', status: 'open', answer: '' }`.
|
|
155
|
-
- `recordTransition` із подією `blocked`, що ставить `state.status = 'blocked-on-human'` і додає питання в `state.hitl`.
|
|
156
|
-
- Повертається `{ status: 'blocked-on-human', step: failed.step }` — подальші кроки не запускаються.
|
|
157
|
-
5. **Після успішного проходу всіх кроків** записується фінальна транзиція `plan_done`, що піднімає `state.status = 'built'`, і функція повертає `{ status: 'done' }`.
|
|
158
|
-
|
|
159
|
-
### Інваріанти, які тримає алгоритм
|
|
160
|
-
|
|
161
|
-
- **HEAD git завжди зелений**: `commit` викликається тільки в гілці `verdict.pass === true`. Жодна repair-спроба не записується в git.
|
|
162
|
-
- **Стан eventsourcing-friendly**: усі зміни плану й верхнього статусу йдуть через `recordTransition`, тож у журналі подій лежить повна історія `step_done` / `step_retry` / `blocked` / `plan_done`.
|
|
163
|
-
- **Resume-семантика**: при повторному виклику `executePlan` уже виконані кроки (`status === 'done'`) пропускаються; кроки з частковим `retry_count` продовжаться з того ж лічильника, але вже без обнулення (поки лічильник менший за `maxRepairAttempts`).
|
|
164
|
-
- **Мінімальний контекст субагента**: усе, що знає субагент про задачу — це повернений `microprompt(step, state)`; жодного «склейного» історичного контексту в нього не передається.
|
|
165
|
-
|
|
166
|
-
### Приклад використання
|
|
167
|
-
|
|
168
|
-
```js
|
|
169
|
-
import { executePlan } from './executor.mjs'
|
|
170
|
-
|
|
171
|
-
const result = await executePlan(
|
|
172
|
-
{ statePath: '.flow/state.json', eventsPath: '.flow/events.jsonl' },
|
|
173
|
-
{
|
|
174
|
-
runner: subagentRunner, // { runStep(prompt, { cwd }) }
|
|
175
|
-
verify: async cwd => runGates(cwd), // { pass, failedOutput }
|
|
176
|
-
commit: (cwd, msg) => gitCommit(cwd, msg),
|
|
177
|
-
cwd: process.cwd(),
|
|
178
|
-
maxRepairAttempts: 3,
|
|
179
|
-
log: m => console.error(m)
|
|
180
|
-
}
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
if (result.status === 'blocked-on-human') {
|
|
184
|
-
// потрібна відповідь людини у state.hitl[] для кроку result.step
|
|
185
|
-
}
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
### Інтеграція в диспетчер
|
|
189
|
-
|
|
190
|
-
`executor.mjs` — третя фаза (Ф3) flow-диспетчера, що йде після `planner.mjs` (який наповнює `state.plan`) і працює зі сховищем стану `state-store.mjs`. Цей файл не знає про конкретні CLI-команди диспетчера — він викликається з `commands.mjs`/`active.mjs` або тестів, де користувач підставляє реальні реалізації `runner`, `verify`, `commit`.
|