@nitra/cursor 4.1.0 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/bin/n-cursor.js +25 -13
- package/lib/models.mjs +1 -2
- package/package.json +1 -1
- package/rules/abie/fix.mjs +1 -1
- package/rules/bun/docs/fix.md +3 -0
- package/rules/bun/fix.mjs +1 -1
- package/rules/capacitor/fix.mjs +1 -1
- package/rules/changelog/docs/fix.md +3 -0
- package/rules/changelog/fix.mjs +1 -1
- package/rules/ci4/fix.mjs +1 -1
- package/rules/ci4/js/docs/marksman_config.md +1 -0
- package/rules/docker/docs/fix.md +1 -1
- package/rules/docker/fix.mjs +1 -1
- package/rules/docker/lint/docs/lint.md +1 -0
- package/rules/efes/docs/fix.md +2 -1
- package/rules/efes/fix.mjs +1 -1
- package/rules/feedback/fix.mjs +1 -1
- package/rules/ga/fix.mjs +1 -1
- package/rules/ga/js/lint.mjs +1 -1
- package/rules/graphql/docs/fix.md +4 -1
- package/rules/graphql/fix.mjs +1 -1
- package/rules/graphql/lib/docs/graphql-gql-scan.md +3 -0
- package/rules/hasura/fix.mjs +1 -1
- package/rules/image-avif/docs/fix.md +4 -1
- package/rules/image-avif/fix.mjs +1 -1
- package/rules/image-avif/js/docs/avif_generation.md +1 -0
- package/rules/image-compress/fix.mjs +1 -1
- package/rules/js-bun-db/fix.mjs +1 -1
- package/rules/js-bun-db/lib/docs/bun-sql-scan.md +6 -0
- package/rules/js-bun-redis/fix.mjs +1 -1
- package/rules/js-lint/fix.mjs +1 -1
- package/rules/js-lint/js/docs/utils_imports.md +1 -0
- package/rules/js-lint-ci/docs/fix.md +4 -1
- package/rules/js-lint-ci/fix.mjs +1 -1
- package/rules/js-mssql/docs/fix.md +3 -0
- package/rules/js-mssql/fix.mjs +1 -1
- package/rules/js-mssql/lib/docs/mssql-pool-scan.md +9 -0
- package/rules/js-run/docs/fix.md +3 -0
- package/rules/js-run/fix.mjs +1 -1
- package/rules/js-run/lib/docs/check-env-scan.md +2 -1
- package/rules/js-run/lib/docs/promise-settimeout-scan.md +4 -0
- package/rules/k8s/docs/fix.md +3 -0
- package/rules/k8s/fix.mjs +1 -1
- package/rules/nginx-default-tpl/docs/fix.md +3 -0
- package/rules/nginx-default-tpl/fix.mjs +1 -1
- package/rules/npm-module/fix.mjs +1 -1
- package/rules/npm-module/js/header_doc_pointer.mjs +14 -3
- package/rules/php/docs/fix.md +2 -1
- package/rules/php/fix.mjs +1 -1
- package/rules/python/docs/fix.md +4 -1
- package/rules/python/fix.mjs +1 -1
- package/rules/rego/fix.mjs +1 -1
- package/rules/rego/js/lint.mjs +1 -1
- package/rules/release/docs/fix.md +4 -1
- package/rules/release/fix.mjs +1 -1
- package/rules/rust/fix.mjs +1 -1
- package/rules/security/docs/fix.md +1 -1
- package/rules/security/fix.mjs +1 -1
- package/rules/style-lint/docs/fix.md +3 -0
- package/rules/style-lint/fix.mjs +1 -1
- package/rules/tauri/docs/fix.md +4 -1
- package/rules/tauri/fix.mjs +1 -1
- package/rules/test/docs/fix.md +3 -0
- package/rules/test/fix.mjs +1 -1
- package/rules/test/js/no-relative-fs-path.mjs +2 -1
- package/rules/text/docs/fix.md +3 -0
- package/rules/text/fix.mjs +1 -1
- package/rules/text/js/lint.mjs +1 -1
- package/rules/vue/fix.mjs +1 -1
- package/rules/worktree/fix.mjs +1 -1
- package/scripts/auto-rules.mjs +1 -1
- package/scripts/coverage-classify/index.mjs +10 -10
- package/scripts/coverage-fix.mjs +2 -2
- package/scripts/dispatcher/graph/lib/cmd-init.mjs +112 -0
- package/scripts/dispatcher/graph/lib/cmd-invalidate.mjs +96 -0
- package/scripts/dispatcher/graph/lib/cmd-kill.mjs +141 -0
- package/scripts/dispatcher/graph/lib/cmd-plan.mjs +142 -0
- package/scripts/dispatcher/graph/lib/cmd-run.mjs +328 -0
- package/scripts/dispatcher/graph/lib/cmd-scan.mjs +115 -0
- package/scripts/dispatcher/graph/lib/cmd-setup.mjs +111 -0
- package/scripts/dispatcher/graph/lib/cmd-signals.mjs +328 -0
- package/scripts/dispatcher/graph/lib/cmd-status.mjs +131 -0
- package/scripts/dispatcher/graph/lib/cmd-verify.mjs +100 -0
- package/scripts/dispatcher/graph/lib/cmd-watch.mjs +128 -0
- package/scripts/dispatcher/graph/lib/config.mjs +103 -0
- package/scripts/dispatcher/graph/lib/frontmatter.mjs +224 -0
- package/scripts/dispatcher/graph/lib/nnn.mjs +127 -0
- package/scripts/dispatcher/graph/lib/node-state.mjs +157 -0
- package/scripts/dispatcher/graph/lib/scanner.mjs +235 -0
- package/scripts/dispatcher/graph/lib/worktree-ops.mjs +193 -0
- package/scripts/dispatcher/graph-tasks.mjs +92 -0
- package/scripts/dispatcher/index.mjs +3 -3
- package/scripts/dispatcher/lib/docs/events.md +1 -0
- package/scripts/dispatcher/lib/executor.mjs +1 -1
- package/scripts/dispatcher/lib/subagent-runner.mjs +9 -9
- package/scripts/dispatcher/trace.mjs +6 -2
- package/scripts/docs/build-agents-commands.md +1 -0
- package/scripts/docs/cli-entry.md +6 -0
- package/scripts/graph/index.mjs +115 -0
- package/scripts/graph/lib/config.mjs +62 -0
- package/scripts/graph/lib/dag.mjs +161 -0
- package/scripts/graph/lib/frontmatter.mjs +70 -0
- package/scripts/graph/lib/nnn.mjs +77 -0
- package/scripts/graph/lib/state.mjs +110 -0
- package/scripts/graph/scan.mjs +64 -0
- package/scripts/graph/status.mjs +86 -0
- package/scripts/lib/docs/load-cursor-config.md +3 -0
- package/scripts/lib/root-notice.mjs +4 -2
- package/scripts/lib/rule-predicates.mjs +1 -1
- package/scripts/lib/worktree-notice.mjs +14 -7
- package/scripts/lib/worktree.mjs +3 -2
- package/scripts/utils/resolve-js-root.mjs +2 -1
- package/scripts/utils/with-lock.mjs +1 -1
- package/skills/docgen/js/docgen-batch.mjs +7 -7
- package/skills/docgen/js/docgen-extract.mjs +80 -37
- package/skills/docgen/js/docgen-ignore.mjs +1 -1
- package/skills/docgen/js/docgen-prompts.mjs +21 -5
- package/skills/fix/js/llm-worker.mjs +19 -22
- package/skills/fix/js/orchestrator.mjs +6 -7
- package/skills/fix/js/t0.mjs +14 -13
- package/types/bin/n-cursor.d.ts +1 -1
- package/rules/flow/docs/fix.md +0 -152
- package/rules/flow/fix.mjs +0 -18
- package/rules/flow/flow.mdc +0 -127
- package/rules/flow/meta.json +0 -1
- package/scripts/dispatcher/lib/docs/flow-lock.md +0 -161
- package/scripts/dispatcher/lib/docs/flow-resolve.md +0 -267
- package/scripts/dispatcher/lib/flow-plan.mjs +0 -153
- package/scripts/dispatcher/lib/flow-resolve.mjs +0 -156
- package/scripts/dispatcher/lib/flow-signals.mjs +0 -235
- package/scripts/dispatcher/lib/flow-verify.mjs +0 -127
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
# flow-lock.mjs
|
|
2
|
-
|
|
3
|
-
## Огляд
|
|
4
|
-
|
|
5
|
-
Модуль `flow-lock.mjs` надає тонку обгортку над спільною утилітою `withLock` для серіалізації мутацій стану `flow` у межах конкретного git worktree (per-branch). Він не реалізує власної логіки взяття/звільнення лока — натомість **повторно використовує** перевірений механізм `withLock` (spec §4.1.3), який уже коректно:
|
|
6
|
-
|
|
7
|
-
- чистить stale-локи (TTL + перевірка процесу через `process.kill(pid, 0)`);
|
|
8
|
-
- релізить лок на `SIGINT` / `SIGTERM`;
|
|
9
|
-
- підтримує очікування з poll-інтервалом і таймаутом.
|
|
10
|
-
|
|
11
|
-
Ключові поведінкові відмінності цього модуля від базового `withLock` (override-и для контексту `flow`):
|
|
12
|
-
|
|
13
|
-
- **`onWaitTimeout: 'fail'`** — fail-closed. На відміну від lint, де після таймауту прийнятно стартонути «без лока», для `flow` мутацію стану двома writer-ами одночасно не допускається. Якщо лок не вдалось узяти за `waitTimeout` — кидається помилка.
|
|
14
|
-
- **`getFingerprint: () => null`** — dedup за «однаковим деревом» вимкнено. Flow повинен виконатись завжди, а не пропускатись через те, що інший writer щойно мутував той самий стан.
|
|
15
|
-
- **Лок-каталог — sibling до worktree-checkout**: `.flow-lock-<branch>/` створюється поряд із самим worktree-каталогом (`.worktrees/.flow-lock-<branch>/`), а не в глобальному кеш-каталозі ОС. Це усуває залежність від `XDG_CACHE_HOME` / `os.tmpdir()` і прив’язує лок до того ж тому, де живе стан.
|
|
16
|
-
|
|
17
|
-
Модуль входить у бібліотеку диспетчера (`npm/scripts/dispatcher/lib/`) і є будівельним блоком для усіх кроків, що змінюють persistent-стан `flow` конкретної гілки.
|
|
18
|
-
|
|
19
|
-
## Експорти / API
|
|
20
|
-
|
|
21
|
-
Модуль експортує одну іменовану функцію:
|
|
22
|
-
|
|
23
|
-
| Експорт | Тип | Призначення |
|
|
24
|
-
| ----------------------------------------- | ---------- | ---------------------------------------------------------------------------------------- |
|
|
25
|
-
| `withFlowLock(worktreeDir, runFn, opts?)` | `function` | Виконує `runFn` під per-branch локом flow, прив’язаним до конкретного worktree-каталогу. |
|
|
26
|
-
|
|
27
|
-
Default export відсутній.
|
|
28
|
-
|
|
29
|
-
## Функції
|
|
30
|
-
|
|
31
|
-
### `withFlowLock(worktreeDir, runFn, opts = {})`
|
|
32
|
-
|
|
33
|
-
Виконує користувацьку асинхронну (або синхронну) функцію `runFn` ексклюзивно для конкретного worktree. Якщо інший процес уже тримає лок, очікує до `opts.waitTimeout`, інакше кидає помилку (fail-closed).
|
|
34
|
-
|
|
35
|
-
#### Сигнатура
|
|
36
|
-
|
|
37
|
-
```js
|
|
38
|
-
withFlowLock(worktreeDir: string,
|
|
39
|
-
runFn: () => unknown | Promise<unknown>,
|
|
40
|
-
opts?: object): Promise<unknown>
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
#### Параметри
|
|
44
|
-
|
|
45
|
-
- `worktreeDir` — `string`, **абсолютний** шлях до checkout-каталогу worktree (наприклад, `/repo/.worktrees/feat-x`). Якщо шлях не абсолютний, функція кидає `Error` (див. нижче).
|
|
46
|
-
- `runFn` — `() => unknown | Promise<unknown>`, критична секція. Викликається без аргументів усередині лока. Може повертати як значення, так і `Promise`.
|
|
47
|
-
- `opts` — `object`, опціональний. Прокидається у `withLock` як є (через spread). Дозволяє переозначити, зокрема:
|
|
48
|
-
- `waitTimeout` — максимальний час очікування лока (мс);
|
|
49
|
-
- `pollInterval` — період опитування стану лока (мс);
|
|
50
|
-
- будь-які інші поля, що підтримує `withLock`.
|
|
51
|
-
|
|
52
|
-
**Увага:** опції з override-ів (`onWaitTimeout`, `cacheDir`, `getFingerprint`) **можуть бути перевизначені** користувачем, бо `...opts` стоїть після них у літералі. Якщо потрібно зберегти fail-closed поведінку — не передавайте ці три поля.
|
|
53
|
-
|
|
54
|
-
#### Повертає
|
|
55
|
-
|
|
56
|
-
`Promise<unknown>` — те, що повернув `runFn`. Якщо `runFn` кинув помилку — `Promise` відхиляється тією ж помилкою (лок звільнюється у `finally` всередині `withLock`).
|
|
57
|
-
|
|
58
|
-
#### Винятки
|
|
59
|
-
|
|
60
|
-
- `Error('withFlowLock: очікується абсолютний шлях (отримано: <worktreeDir>)')` — якщо `worktreeDir` не є абсолютним шляхом (`!isAbsolute(worktreeDir)`).
|
|
61
|
-
- Будь-яка помилка, кинута з `withLock`: зокрема, при `onWaitTimeout: 'fail'` — помилка таймауту очікування лока.
|
|
62
|
-
- Будь-яка помилка, кинута з `runFn` — пробрасується без обгортки.
|
|
63
|
-
|
|
64
|
-
#### Side effects
|
|
65
|
-
|
|
66
|
-
- Створює (через `withLock`) каталог `${dirname(worktreeDir)}/.flow-lock-${basename(worktreeDir)}/` для зберігання lock-файлу та допоміжних артефактів (PID, мітки часу — деталі визначає `withLock`).
|
|
67
|
-
- На час виконання `runFn` тримає файловий лок у вищезгаданому каталозі.
|
|
68
|
-
- Реагує на `SIGINT`/`SIGTERM` (через хендлери у `withLock`) — звільняє лок при перериванні процесу.
|
|
69
|
-
- Не виконує жодних мережевих чи git-операцій сам по собі — лише обчислює шляхи та делегує у `withLock`.
|
|
70
|
-
|
|
71
|
-
#### Логіка обчислення параметрів лока
|
|
72
|
-
|
|
73
|
-
Усередині функції:
|
|
74
|
-
|
|
75
|
-
1. `base = basename(worktreeDir)` — ім’я останнього сегменту шляху (наприклад, `feat-x` для `/repo/.worktrees/feat-x`). Використовується як суфікс імені лока та назви кеш-каталогу.
|
|
76
|
-
2. `cacheDir = join(dirname(worktreeDir), `.flow-lock-${base}`)` — каталог, у якому `withLock` зберігає сам lock-файл. Розташовується **поряд** із worktree-каталогом, а не всередині нього (sibling).
|
|
77
|
-
3. Виклик `withLock(name, runFn, options)`:
|
|
78
|
-
- `name = `flow-${base}`` — стабільний ідентифікатор лока, унікальний у межах гілки.
|
|
79
|
-
- `options`:
|
|
80
|
-
- `onWaitTimeout: 'fail'` — fail-closed по таймауту очікування;
|
|
81
|
-
- `cacheDir` — обчислений вище;
|
|
82
|
-
- `getFingerprint: () => null` — dedup вимкнено: рівноцінні запуски не «склеюються»;
|
|
83
|
-
- `...opts` — користувацькі опції зверху (можуть переозначити будь-яке з трьох попередніх полів).
|
|
84
|
-
|
|
85
|
-
## Залежності
|
|
86
|
-
|
|
87
|
-
### Зовнішні (Node.js core)
|
|
88
|
-
|
|
89
|
-
- `node:path` — імпортуються `basename`, `dirname`, `isAbsolute`, `join`. Використовуються для валідації абсолютності шляху та обчислення sibling-каталогу лока і `name` лока.
|
|
90
|
-
|
|
91
|
-
### Внутрішні (репозиторій)
|
|
92
|
-
|
|
93
|
-
- `../../utils/with-lock.mjs` (відносно `npm/scripts/dispatcher/lib/flow-lock.mjs` — це `npm/scripts/utils/with-lock.mjs`) — спільна утиліта `withLock`. Відповідає за:
|
|
94
|
-
- створення lock-файлу;
|
|
95
|
-
- очищення stale-локів за TTL і `process.kill(pid, 0)`;
|
|
96
|
-
- обробку `SIGINT`/`SIGTERM`;
|
|
97
|
-
- polling/wait/timeout логіку;
|
|
98
|
-
- dedup за fingerprint (тут — вимкнено).
|
|
99
|
-
|
|
100
|
-
Інших зовнішніх npm-залежностей модуль не має.
|
|
101
|
-
|
|
102
|
-
## Потік виконання / Використання
|
|
103
|
-
|
|
104
|
-
### Високорівневий потік виклику
|
|
105
|
-
|
|
106
|
-
1. Викликач формує абсолютний шлях `worktreeDir` (наприклад, `/repo/.worktrees/feat-x`).
|
|
107
|
-
2. Передає його в `withFlowLock(worktreeDir, runFn, opts?)`.
|
|
108
|
-
3. `withFlowLock`:
|
|
109
|
-
- валідує, що `worktreeDir` абсолютний (інакше — кидає);
|
|
110
|
-
- обчислює `base` та `cacheDir`;
|
|
111
|
-
- делегує у `withLock('flow-<base>', runFn, { onWaitTimeout: 'fail', cacheDir, getFingerprint: () => null, ...opts })`.
|
|
112
|
-
4. `withLock`:
|
|
113
|
-
- намагається взяти лок у `cacheDir`;
|
|
114
|
-
- якщо лок зайнятий — чекає до `waitTimeout` з кроком `pollInterval` (значення за замовчуванням — з `withLock`);
|
|
115
|
-
- якщо за таймаут лок не взято — через `onWaitTimeout: 'fail'` кидає помилку;
|
|
116
|
-
- якщо взято — виконує `runFn` і у `finally` звільняє лок;
|
|
117
|
-
- реєструє хендлери `SIGINT`/`SIGTERM`, що звільняють лок при перериванні.
|
|
118
|
-
5. Результат `runFn` повертається через `Promise`.
|
|
119
|
-
|
|
120
|
-
### Приклад використання
|
|
121
|
-
|
|
122
|
-
```js
|
|
123
|
-
import { withFlowLock } from './flow-lock.mjs'
|
|
124
|
-
|
|
125
|
-
const worktreeDir = '/repo/.worktrees/feat-x'
|
|
126
|
-
|
|
127
|
-
await withFlowLock(
|
|
128
|
-
worktreeDir,
|
|
129
|
-
async () => {
|
|
130
|
-
// Критична секція: мутації стану flow для гілки feat-x
|
|
131
|
-
await mutateFlowState(worktreeDir)
|
|
132
|
-
},
|
|
133
|
-
{ waitTimeout: 30_000, pollInterval: 250 }
|
|
134
|
-
)
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
Поведінка:
|
|
138
|
-
|
|
139
|
-
- Якщо інший процес уже виконує `withFlowLock` для **того ж самого** `worktreeDir`, поточний виклик чекатиме до 30 с.
|
|
140
|
-
- Якщо за 30 с лок не звільниться — буде кинуто помилку (fail-closed).
|
|
141
|
-
- Для **іншого** `worktreeDir` (інша гілка) лок не блокує — `name` лока містить `basename(worktreeDir)`, тож гілки серіалізуються незалежно.
|
|
142
|
-
|
|
143
|
-
### Розташування артефактів на диску
|
|
144
|
-
|
|
145
|
-
Для `worktreeDir = /repo/.worktrees/feat-x`:
|
|
146
|
-
|
|
147
|
-
- worktree-checkout: `/repo/.worktrees/feat-x/`
|
|
148
|
-
- lock-каталог (sibling): `/repo/.worktrees/.flow-lock-feat-x/`
|
|
149
|
-
|
|
150
|
-
Sibling-розміщення обрано свідомо, аби:
|
|
151
|
-
|
|
152
|
-
- не «забруднювати» сам worktree файлом лока (він не є частиною робочого дерева);
|
|
153
|
-
- не залежати від глобального кеш-каталогу ОС (інакше лок-семантика «per-branch у цьому репо» загубилась би на машинах з нестандартним `XDG_CACHE_HOME`).
|
|
154
|
-
|
|
155
|
-
### Гарантії та обмеження
|
|
156
|
-
|
|
157
|
-
- **Серіалізація per-branch:** два паралельні виклики `withFlowLock` для одного `worktreeDir` виконуються послідовно.
|
|
158
|
-
- **Незалежність гілок:** виклики для різних `worktreeDir` (різний `basename`) не блокують одне одного.
|
|
159
|
-
- **Fail-closed:** при недоступному локі writer **не** запуститься «оптимістично» — на відміну від lint-сценаріїв.
|
|
160
|
-
- **Без dedup:** навіть якщо викликач передасть свій `getFingerprint`, дефолт `() => null` вимикає «склеювання» однакових запусків; для увімкнення dedup потрібно явно передати `getFingerprint` в `opts` (override `...opts` після дефолтів дозволяє це).
|
|
161
|
-
- **Обов’язковий абсолютний шлях:** відносні шляхи відхиляються одразу — це усуває клас помилок «лок узяли не там, де думали».
|
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
# flow-resolve.mjs
|
|
2
|
-
|
|
3
|
-
## Огляд
|
|
4
|
-
|
|
5
|
-
Модуль `flow-resolve.mjs` реалізує **cwd-незалежний резолвер активного flow** для команд `spec`, `plan`, `verify`, `review`, `gate`, `release` (беклог адаптації #1).
|
|
6
|
-
|
|
7
|
-
Призначення: знайти файл стану `*.flow.json` поточної задачі навіть тоді, коли команду запущено **не** з кореня worktree (наприклад, з кореня головного дерева репозиторію або з вкладеної підтеки worktree). Інакше функція `flowStatePath(cwd)` обчислила б хибний шлях і команда повідомила б «стану нема», хоча flow реально активний.
|
|
8
|
-
|
|
9
|
-
Порядок резолвингу (відповідно до spec `2026-06-01-flow-cwd-state-resolution`):
|
|
10
|
-
|
|
11
|
-
1. Якщо передано явний `branch` — формуємо шлях `<repoRoot>/.worktrees/<sanitizeBranch(branch)>.flow.json` і перевіряємо існування теки worktree.
|
|
12
|
-
2. Швидкий шлях: якщо `cwd` сам уже є текою worktree і поряд лежить файл стану — беремо його (без виклику git).
|
|
13
|
-
3. Toplevel-резолвинг: `git rev-parse --show-toplevel` від `cwd`; якщо toplevel розташований **безпосередньо** під `<repoRoot>/.worktrees/` і для нього існує стан — повертаємо його. Якщо стану нема — це проблема саме цього worktree, чужий активний flow **не** підтягуємо.
|
|
14
|
-
4. Скан: якщо `cwd` поза будь-яким worktree (наприклад, головне дерево) — перебираємо `<repoRoot>/.worktrees/*.flow.json`, шукаючи статуси `in_progress`. Якщо рівно один — авторезолв; якщо кілька — помилка зі списком; якщо нуль — «стану нема».
|
|
15
|
-
|
|
16
|
-
Резолвер **не пише** на диск і **не модифікує** стан. Усі залежності (`git`, FS-операції, `readState`) ін'єктуються через параметр `deps`, тож логіку можна тестувати без реального git-репозиторію.
|
|
17
|
-
|
|
18
|
-
## Експорти / API
|
|
19
|
-
|
|
20
|
-
| Експорт | Тип | Призначення |
|
|
21
|
-
| ---------------------------------------- | --------------- | --------------------------------------------------------- |
|
|
22
|
-
| `resolveActiveFlowState(params?, deps?)` | named function | Основна функція-резолвер; повертає об'єкт `ResolvedFlow`. |
|
|
23
|
-
| `ResolvedFlow` | JSDoc-`typedef` | Тип-опис форми результату резолвингу. |
|
|
24
|
-
|
|
25
|
-
Внутрішні (не експортуються) допоміжні функції: `realGit`, `mainRepoRoot`, `currentToplevel`, `notFound`.
|
|
26
|
-
|
|
27
|
-
### Тип `ResolvedFlow`
|
|
28
|
-
|
|
29
|
-
JSDoc-`typedef`, що описує форму результату:
|
|
30
|
-
|
|
31
|
-
| Поле | Тип | Семантика |
|
|
32
|
-
| -------------- | ---------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
33
|
-
| `statePath` | `string \| null` | Абсолютний шлях до `.flow.json` або `null`, якщо стан не знайдено. |
|
|
34
|
-
| `worktreeDir` | `string \| null` | Тека worktree (ефективний `cwd` для гейтів) або `null`. |
|
|
35
|
-
| `label` | `string \| null` | Мітка flow (sanitized branch) або `null`. |
|
|
36
|
-
| `autoResolved` | `boolean` | `true`, якщо стан знайдено скануванням (тобто `cwd` був поза worktree, а активний flow знайшовся однозначно). |
|
|
37
|
-
| `error` | `string \| null` | Повідомлення для логу, якщо `statePath === null`; інакше `null`. |
|
|
38
|
-
|
|
39
|
-
### Константа `FLOW_STATE_SUFFIX`
|
|
40
|
-
|
|
41
|
-
Внутрішня (не експортована) константа: `'.flow.json'`. Використовується для фільтрації імен файлів у теці `.worktrees/` під час скану та для обрізання суфікса при формуванні `label`.
|
|
42
|
-
|
|
43
|
-
## Функції
|
|
44
|
-
|
|
45
|
-
### `realGit(args, cwd)`
|
|
46
|
-
|
|
47
|
-
Сигнатура: `function realGit(args: string[], cwd: string): { status: number, stdout: string }`
|
|
48
|
-
|
|
49
|
-
Параметри:
|
|
50
|
-
|
|
51
|
-
- `args` — масив аргументів для бінарника `git` (наприклад, `['worktree', 'list', '--porcelain']`);
|
|
52
|
-
- `cwd` — робочий каталог, у якому запустити `git`.
|
|
53
|
-
|
|
54
|
-
Повертає об'єкт `{ status, stdout }`:
|
|
55
|
-
|
|
56
|
-
- `status` — exit-код процесу `git`; якщо `spawnSync` повернув `null` — підставляється `1`;
|
|
57
|
-
- `stdout` — захоплений stdout у кодуванні `utf8`; якщо `null` — порожній рядок.
|
|
58
|
-
|
|
59
|
-
Side effects: синхронний запуск дочірнього процесу `git` через `spawnSync`. Це **єдина** функція в модулі, що звертається до зовнішнього процесу; усе інше — чистий FS/обчислення. Використовується як **дефолтний** git-runner: при кожному виклику резолвера для конкретного `cwd` створюється стрілкова замикальна функція `args => realGit(args, cwd)`, якщо `deps.git` не передано.
|
|
60
|
-
|
|
61
|
-
### `mainRepoRoot(git)`
|
|
62
|
-
|
|
63
|
-
Сигнатура: `function mainRepoRoot(git: (args: string[]) => { status: number, stdout: string }): string | null`
|
|
64
|
-
|
|
65
|
-
Параметри:
|
|
66
|
-
|
|
67
|
-
- `git` — git-runner (для тестів — мок; у проді — обгортка над `realGit`).
|
|
68
|
-
|
|
69
|
-
Повертає абсолютний шлях кореня **головного** worktree репозиторію або `null`, якщо неможливо встановити (git недоступний, не репо тощо).
|
|
70
|
-
|
|
71
|
-
Алгоритм:
|
|
72
|
-
|
|
73
|
-
1. Виконати `git worktree list --porcelain`.
|
|
74
|
-
2. Якщо `status !== 0` — повернути `null`.
|
|
75
|
-
3. У stdout знайти **перший** рядок, що починається з `worktree ` (це і є головне дерево за конвенцією porcelain-формату).
|
|
76
|
-
4. Зрізати префікс `worktree ` і пробіли; якщо рядок непорожній — повернути; інакше `null`.
|
|
77
|
-
|
|
78
|
-
Side effects: один git-виклик через переданий runner.
|
|
79
|
-
|
|
80
|
-
### `currentToplevel(git)`
|
|
81
|
-
|
|
82
|
-
Сигнатура: `function currentToplevel(git: (args: string[]) => { status: number, stdout: string }): string | null`
|
|
83
|
-
|
|
84
|
-
Параметри:
|
|
85
|
-
|
|
86
|
-
- `git` — git-runner.
|
|
87
|
-
|
|
88
|
-
Повертає абсолютний шлях `toplevel`-теки для **поточного** worktree (`git rev-parse --show-toplevel`) або `null`, якщо команда впала або stdout порожній.
|
|
89
|
-
|
|
90
|
-
Side effects: один git-виклик через переданий runner.
|
|
91
|
-
|
|
92
|
-
### `resolveActiveFlowState(params?, deps?)`
|
|
93
|
-
|
|
94
|
-
Сигнатура:
|
|
95
|
-
|
|
96
|
-
```
|
|
97
|
-
export function resolveActiveFlowState(
|
|
98
|
-
params?: { cwd?: string, branch?: string },
|
|
99
|
-
deps?: {
|
|
100
|
-
git?: (args: string[]) => { status: number, stdout: string },
|
|
101
|
-
exists?: (p: string) => boolean,
|
|
102
|
-
readState?: (p: string) => object | null,
|
|
103
|
-
readdir?: (d: string) => string[],
|
|
104
|
-
repoRoot?: string,
|
|
105
|
-
},
|
|
106
|
-
): ResolvedFlow
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
Параметри:
|
|
110
|
-
|
|
111
|
-
- `params.cwd` — робочий каталог, від якого вести резолвинг. За замовчуванням — результат `process.cwd()` (через `cwd as processCwd` з `node:process`).
|
|
112
|
-
- `params.branch` — явна гілка, для якої треба знайти стан. Якщо задана, активний flow в інших worktree **ігнорується** — використовується «фіксований» режим.
|
|
113
|
-
- `deps.git` — кастомний git-runner; за замовчуванням — `args => realGit(args, cwd)` (замикає `cwd` із `params`).
|
|
114
|
-
- `deps.exists` — кастомна реалізація `existsSync`; за замовчуванням — `existsSync` з `node:fs`.
|
|
115
|
-
- `deps.readState` — кастомний читач стану; за замовчуванням — `readState`, реекспортований із `./state-store.mjs` як `defaultReadState`.
|
|
116
|
-
- `deps.readdir` — кастомне читання каталогу; за замовчуванням — функція, що повертає `readdirSync(d)` якщо тека існує, інакше `[]` (захист від ENOENT, коли `.worktrees/` ще не створено).
|
|
117
|
-
- `deps.repoRoot` — наперед обчислений корінь репозиторію; якщо передано — обхід git для пошуку кореня пропускається.
|
|
118
|
-
|
|
119
|
-
Повертає об'єкт `ResolvedFlow` (див. вище).
|
|
120
|
-
|
|
121
|
-
Алгоритм (по гілках):
|
|
122
|
-
|
|
123
|
-
1. **Локальний `resolveRoot()`** — стрілкова функція, що повертає `deps.repoRoot` або викликає `mainRepoRoot(git)`. Викликається лазиво — лише там, де потрібен корінь.
|
|
124
|
-
|
|
125
|
-
2. **Гілка 1: явний `branch`.**
|
|
126
|
-
- Отримати `repoRoot` через `resolveRoot()`. Якщо `null` — повернути `notFound('стану нема — спершу `flow init`')`.
|
|
127
|
-
- `label = sanitizeBranch(branch)` (нормалізація імені гілки для імені файлу стану).
|
|
128
|
-
- `worktreeDir = worktreePaths(repoRoot, branch).checkout` — фізична тека worktree.
|
|
129
|
-
- Якщо `worktreeDir` **не** існує (`!exists(worktreeDir)`) — повернути `notFound` із повідомленням, що worktree для цієї гілки не знайдено (з підказкою перевірити назву або виконати `flow init`). Це захищає від ENOENT при подальших гейтах.
|
|
130
|
-
- Інакше повернути `{ statePath: flowStatePath(worktreeDir), worktreeDir, label, autoResolved: false, error: null }`.
|
|
131
|
-
|
|
132
|
-
3. **Гілка 2: швидкий шлях без git.**
|
|
133
|
-
- Обчислити `direct = flowStatePath(cwd)`.
|
|
134
|
-
- Якщо файл існує — повернути `{ statePath: direct, worktreeDir: cwd, label: basename(cwd), autoResolved: false, error: null }`. Це типовий випадок: користувач у корені свого worktree.
|
|
135
|
-
|
|
136
|
-
4. **Потрібен `repoRoot` через git** (`resolveRoot()`). Якщо `null` — повернути `notFound('стану нема — спершу `flow init`')`. Це безпечна деградація: якщо git недоступний, не лазимо далі.
|
|
137
|
-
|
|
138
|
-
5. Обчислити `worktreesDir = join(repoRoot, '.worktrees')`.
|
|
139
|
-
|
|
140
|
-
6. **Гілка 3: toplevel-резолвинг.**
|
|
141
|
-
- `top = currentToplevel(git)`.
|
|
142
|
-
- Якщо `top` визначено **і** `dirname(top) === worktreesDir` (тобто toplevel лежить безпосередньо під `<repoRoot>/.worktrees/`) — ми всередині worktree (можливо, з вкладеної підтеки):
|
|
143
|
-
- `statePath = flowStatePath(top)`.
|
|
144
|
-
- Якщо `exists(statePath)` — повернути `{ statePath, worktreeDir: top, label: basename(top), autoResolved: false, error: null }`.
|
|
145
|
-
- Інакше — `notFound('стану нема — спершу `flow init`')`. **Чужий** активний flow тут **не** підтягуємо: це проблема саме цього worktree.
|
|
146
|
-
|
|
147
|
-
7. **Гілка 4: скан активних flow** (виконується, якщо `top` неможливо отримати або `dirname(top) !== worktreesDir`, тобто `cwd` поза будь-яким worktree — наприклад, у головному дереві).
|
|
148
|
-
- `active = []`.
|
|
149
|
-
- Для кожного `name` із `readdir(worktreesDir)`:
|
|
150
|
-
- Якщо ім'я не закінчується на `FLOW_STATE_SUFFIX` (`.flow.json`) — пропустити.
|
|
151
|
-
- Сформувати `statePath = join(worktreesDir, name)`.
|
|
152
|
-
- Спробувати `state = readState(statePath)`; будь-яка помилка (`catch`) — пропустити елемент (пошкоджений стан не валить скан).
|
|
153
|
-
- Якщо `state?.status === 'in_progress'`:
|
|
154
|
-
- `label = name.slice(0, -FLOW_STATE_SUFFIX.length)` — обрізати суфікс `.flow.json`.
|
|
155
|
-
- `worktreeDir = join(worktreesDir, label)`.
|
|
156
|
-
- Додати `{ statePath, worktreeDir, label }` у `active`.
|
|
157
|
-
- Якщо `active.length === 1` — повернути `{ ...active[0], autoResolved: true, error: null }`.
|
|
158
|
-
- Якщо `active.length > 1` — повернути `notFound` із форматованим списком: «кілька активних flow — уточни `--branch <гілка>` або `cd` у потрібний worktree:\n - <label1>\n - <label2>...».
|
|
159
|
-
- Інакше — `notFound('стану нема — спершу `flow init`')`.
|
|
160
|
-
|
|
161
|
-
Side effects: тільки **читання** — `git`, `existsSync`, `readdirSync`, `readState`. Жодних записів на диск.
|
|
162
|
-
|
|
163
|
-
### `notFound(error)`
|
|
164
|
-
|
|
165
|
-
Сигнатура: `function notFound(error: string): ResolvedFlow`
|
|
166
|
-
|
|
167
|
-
Параметри:
|
|
168
|
-
|
|
169
|
-
- `error` — текст повідомлення для логу.
|
|
170
|
-
|
|
171
|
-
Повертає об'єкт-«заглушку» з усіма полями стану, виставленими в `null`/`false`, і заданим `error`:
|
|
172
|
-
|
|
173
|
-
```
|
|
174
|
-
{ statePath: null, worktreeDir: null, label: null, autoResolved: false, error }
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
Side effects: немає (чиста функція).
|
|
178
|
-
|
|
179
|
-
## Залежності
|
|
180
|
-
|
|
181
|
-
### Стандартна бібліотека Node.js
|
|
182
|
-
|
|
183
|
-
- `node:fs` — `existsSync`, `readdirSync` (дефолтні реалізації `exists`/`readdir`).
|
|
184
|
-
- `node:child_process` — `spawnSync` (запуск `git`).
|
|
185
|
-
- `node:path` — `basename`, `dirname`, `join` (формування та аналіз шляхів).
|
|
186
|
-
- `node:process` — `cwd as processCwd` (дефолт для `params.cwd`).
|
|
187
|
-
|
|
188
|
-
### Внутрішні модулі
|
|
189
|
-
|
|
190
|
-
- `../../lib/worktree.mjs` — імпортуються:
|
|
191
|
-
- `sanitizeBranch(branch)` — нормалізація імені гілки → ім'я-мітка файлу стану;
|
|
192
|
-
- `worktreePaths(repoRoot, branch)` — повертає об'єкт із полем `checkout` (фізична тека worktree).
|
|
193
|
-
- `./state-store.mjs` — імпортуються:
|
|
194
|
-
- `flowStatePath(worktreeDir)` — формує шлях файлу стану для заданої теки worktree;
|
|
195
|
-
- `readState as defaultReadState` — функція читання + парсингу JSON-стану; під час скану викликається у `try/catch`, тож може кидати помилку.
|
|
196
|
-
|
|
197
|
-
### Зовнішні процеси
|
|
198
|
-
|
|
199
|
-
- `git` (через `spawnSync`):
|
|
200
|
-
- `git worktree list --porcelain` — для знаходження кореня головного worktree;
|
|
201
|
-
- `git rev-parse --show-toplevel` — для визначення поточного worktree.
|
|
202
|
-
|
|
203
|
-
## Потік виконання / Використання
|
|
204
|
-
|
|
205
|
-
### Сценарій 1 — запуск із кореня worktree
|
|
206
|
-
|
|
207
|
-
```
|
|
208
|
-
cwd = /repo/.worktrees/feat-foo
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
1. `branch` не передано → Гілка 1 пропускається.
|
|
212
|
-
2. `direct = flowStatePath('/repo/.worktrees/feat-foo')` існує → **повертається одразу** без виклику git. Це найшвидший і найчастіший випадок.
|
|
213
|
-
|
|
214
|
-
### Сценарій 2 — запуск із підтеки worktree
|
|
215
|
-
|
|
216
|
-
```
|
|
217
|
-
cwd = /repo/.worktrees/feat-foo/src/lib
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
1. Гілка 1 — ні.
|
|
221
|
-
2. Гілка 2 — `direct` не існує (бо `cwd` не корінь worktree).
|
|
222
|
-
3. Через `mainRepoRoot(git)` отримуємо `/repo`; `worktreesDir = /repo/.worktrees`.
|
|
223
|
-
4. `currentToplevel(git)` → `/repo/.worktrees/feat-foo`; `dirname === /repo/.worktrees` ✓ → повертаємо стан саме цього worktree.
|
|
224
|
-
|
|
225
|
-
### Сценарій 3 — запуск із головного дерева (поза worktree)
|
|
226
|
-
|
|
227
|
-
```
|
|
228
|
-
cwd = /repo
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
1. Гілка 1 — ні.
|
|
232
|
-
2. Гілка 2 — `flowStatePath('/repo')` не існує.
|
|
233
|
-
3. `mainRepoRoot` → `/repo`; `worktreesDir = /repo/.worktrees`.
|
|
234
|
-
4. `currentToplevel` → `/repo`; `dirname('/repo') !== '/repo/.worktrees'` → переходимо до скану.
|
|
235
|
-
5. Скан `.worktrees/*.flow.json`:
|
|
236
|
-
- 1 із `status: in_progress` → `autoResolved: true`, повертається.
|
|
237
|
-
- 2+ → помилка зі списком кандидатів та пропозицією `--branch` або `cd`.
|
|
238
|
-
- 0 → `notFound('стану нема — спершу `flow init`')`.
|
|
239
|
-
|
|
240
|
-
### Сценарій 4 — явний `--branch`
|
|
241
|
-
|
|
242
|
-
```
|
|
243
|
-
params = { branch: 'feat-foo' }
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
1. `repoRoot` отриманий → перевіряється існування фізичної теки worktree `feat-foo`.
|
|
247
|
-
2. Якщо тека існує — повертається стан (навіть якщо файлу стану ще нема — це **очікуваний** шлях, користувач обере цю гілку).
|
|
248
|
-
3. Якщо теки нема — `notFound` з підказкою перевірити назву / зробити `flow init`.
|
|
249
|
-
|
|
250
|
-
### Сценарій 5 — git недоступний
|
|
251
|
-
|
|
252
|
-
- Якщо `mainRepoRoot` повертає `null` (наприклад, бінарника git нема або це не репо) і прямий шлях через `cwd` теж не спрацював — повертаємо `notFound('стану нема — спершу `flow init`')`. Жодних падінь.
|
|
253
|
-
|
|
254
|
-
### Контракт виклику з боку команд
|
|
255
|
-
|
|
256
|
-
Команди `spec/plan/verify/review/gate/release` мають викликати `resolveActiveFlowState(...)` **на самому старті**, потім:
|
|
257
|
-
|
|
258
|
-
- Якщо `result.statePath === null` — вивести `result.error` і завершитися ненульовим кодом.
|
|
259
|
-
- Інакше використовувати:
|
|
260
|
-
- `result.statePath` — для читання/запису файлу стану через `state-store.mjs`;
|
|
261
|
-
- `result.worktreeDir` — як **ефективний** `cwd` для виконання гейтів (linter, tests, scripts) у потрібному worktree;
|
|
262
|
-
- `result.label` — для логів/повідомлень;
|
|
263
|
-
- `result.autoResolved` — щоб показати користувачу, що flow знайдено скануванням (корисно для UX: «авторезолв на гілку X»).
|
|
264
|
-
|
|
265
|
-
### Тестування
|
|
266
|
-
|
|
267
|
-
Усі зовнішні залежності (`git`, `exists`, `readState`, `readdir`, `repoRoot`) ін'єктуються через параметр `deps`. Це дає змогу повністю покрити функцію unit-тестами без реального git-репозиторію та без створення файлів на диску — достатньо передати моки, що повертають фіктивні `status`/`stdout`/булеві значення.
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Handler `flow plan` — Stage 1 (думка.MD § "flow plan").
|
|
3
|
-
*
|
|
4
|
-
* Читає `task.md` у поточному вузлі, розбирає `mode` і `hint` з front-matter,
|
|
5
|
-
* знаходить наступний номер `plan_NNN.md`, пише шаблон і виводить контекст для
|
|
6
|
-
* агента (task + mode + hint) на stdout.
|
|
7
|
-
*
|
|
8
|
-
* FS та path-резолвінг ін'єктуються — тестується без реального диска.
|
|
9
|
-
*/
|
|
10
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
11
|
-
import { join } from 'node:path'
|
|
12
|
-
import { cwd as processCwd } from 'node:process'
|
|
13
|
-
|
|
14
|
-
const FRONT_MATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Парсить YAML front-matter (мінімально: лише прості `key: value` рядки).
|
|
18
|
-
* @param {string} text вміст файлу
|
|
19
|
-
* @returns {Record<string, string>} ключ-значення з front-matter (рядки)
|
|
20
|
-
*/
|
|
21
|
-
function parseFrontMatter(text) {
|
|
22
|
-
const m = text.match(FRONT_MATTER_RE)
|
|
23
|
-
if (!m) return {}
|
|
24
|
-
const result = {}
|
|
25
|
-
for (const line of m[1].split(/\r?\n/)) {
|
|
26
|
-
const idx = line.indexOf(':')
|
|
27
|
-
if (idx === -1) continue
|
|
28
|
-
const key = line.slice(0, idx).trim()
|
|
29
|
-
const val = line.slice(idx + 1).trim()
|
|
30
|
-
if (key) result[key] = val
|
|
31
|
-
}
|
|
32
|
-
return result
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Знаходить наступний номер `plan_NNN.md` у директорії вузла.
|
|
37
|
-
* @param {string} dir абсолютний шлях до директорії вузла
|
|
38
|
-
* @param {(dir: string) => string[]} readdir інжектована readdir
|
|
39
|
-
* @returns {string} рядок типу `001`, `002`, …
|
|
40
|
-
*/
|
|
41
|
-
function nextPlanNumber(dir, readdir) {
|
|
42
|
-
const files = readdir(dir)
|
|
43
|
-
let max = 0
|
|
44
|
-
for (const f of files) {
|
|
45
|
-
const m = f.match(/^plan_(\d+)\.md$/)
|
|
46
|
-
if (m) {
|
|
47
|
-
const n = parseInt(m[1], 10)
|
|
48
|
-
if (n > max) max = n
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return String(max + 1).padStart(3, '0')
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Будує вміст шаблону `plan_NNN.md`.
|
|
56
|
-
* @param {{ mode: string, hint: string, now: string }} params параметри
|
|
57
|
-
* @returns {string} вміст файлу
|
|
58
|
-
*/
|
|
59
|
-
export function buildPlanTemplate({ mode, hint, now }) {
|
|
60
|
-
return [
|
|
61
|
-
'---',
|
|
62
|
-
`created_at: ${now}`,
|
|
63
|
-
`mode: ${mode}`,
|
|
64
|
-
`decision: ${hint || 'atomic | composite'}`,
|
|
65
|
-
'---',
|
|
66
|
-
'',
|
|
67
|
-
'## Context',
|
|
68
|
-
"<!-- Чому саме такий підхід — що агент/людина з'ясували -->",
|
|
69
|
-
'',
|
|
70
|
-
'## Approach',
|
|
71
|
-
'<!-- atomic: покроковий план виконання -->',
|
|
72
|
-
'<!-- composite: список дочірніх вузлів з описами -->',
|
|
73
|
-
'',
|
|
74
|
-
'## Risks',
|
|
75
|
-
'<!-- Що може піти не так -->',
|
|
76
|
-
''
|
|
77
|
-
].join('\n')
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* `flow plan` handler.
|
|
82
|
-
*
|
|
83
|
-
* @param {string[]} _rest аргументи після `plan` (не використовуються)
|
|
84
|
-
* @param {{
|
|
85
|
-
* cwd?: string,
|
|
86
|
-
* log?: (m: string) => void,
|
|
87
|
-
* readFile?: (path: string, enc: string) => string,
|
|
88
|
-
* writeFile?: (path: string, content: string, enc: string) => void,
|
|
89
|
-
* readdir?: (dir: string) => string[],
|
|
90
|
-
* exists?: (path: string) => boolean,
|
|
91
|
-
* now?: () => string
|
|
92
|
-
* }} [deps] ін'єкції
|
|
93
|
-
* @returns {Promise<number>} exit code
|
|
94
|
-
*/
|
|
95
|
-
export async function plan(_rest, deps = {}) {
|
|
96
|
-
const cwd = deps.cwd ?? processCwd()
|
|
97
|
-
const log = deps.log ?? console.error
|
|
98
|
-
const readFile = deps.readFile ?? ((p, enc) => readFileSync(p, enc))
|
|
99
|
-
const writeFile = deps.writeFile ?? ((p, c, enc) => writeFileSync(p, c, enc))
|
|
100
|
-
const readdir = deps.readdir ?? (d => (existsSync(d) ? readdirSync(d) : []))
|
|
101
|
-
const exists = deps.exists ?? existsSync
|
|
102
|
-
const nowFn = deps.now ?? (() => new Date().toISOString())
|
|
103
|
-
|
|
104
|
-
const taskPath = join(cwd, 'task.md')
|
|
105
|
-
if (!exists(taskPath)) {
|
|
106
|
-
log('flow plan: task.md не знайдено в CWD')
|
|
107
|
-
return 1
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
let taskContent
|
|
111
|
-
try {
|
|
112
|
-
taskContent = readFile(taskPath, 'utf8')
|
|
113
|
-
} catch (err) {
|
|
114
|
-
log(`flow plan: не вдалося прочитати task.md — ${err instanceof Error ? err.message : String(err)}`)
|
|
115
|
-
return 1
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const fm = parseFrontMatter(taskContent)
|
|
119
|
-
const mode = fm.mode || 'human'
|
|
120
|
-
const hint = fm.hint || ''
|
|
121
|
-
|
|
122
|
-
const num = nextPlanNumber(cwd, readdir)
|
|
123
|
-
const planPath = join(cwd, `plan_${num}.md`)
|
|
124
|
-
|
|
125
|
-
const content = buildPlanTemplate({ mode, hint, now: nowFn() })
|
|
126
|
-
try {
|
|
127
|
-
writeFile(planPath, content, 'utf8')
|
|
128
|
-
} catch (err) {
|
|
129
|
-
log(`flow plan: не вдалося записати ${planPath} — ${err instanceof Error ? err.message : String(err)}`)
|
|
130
|
-
return 1
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
log(`flow plan: створено ${planPath}`)
|
|
134
|
-
|
|
135
|
-
// Виводимо контекст для агента на stdout
|
|
136
|
-
const outLines = [
|
|
137
|
-
`## flow plan context`,
|
|
138
|
-
``,
|
|
139
|
-
`mode: ${mode}`,
|
|
140
|
-
hint ? `hint: ${hint}` : `hint: (не задано — агент вирішує сам)`,
|
|
141
|
-
`plan: plan_${num}.md`,
|
|
142
|
-
``
|
|
143
|
-
]
|
|
144
|
-
// Додаємо вміст task.md для контексту (без front-matter)
|
|
145
|
-
outLines.push(`### task.md`)
|
|
146
|
-
const bodyStart = taskContent.indexOf('\n---\n', 4)
|
|
147
|
-
const taskBody = bodyStart !== -1 ? taskContent.slice(bodyStart + 5).trimStart() : taskContent
|
|
148
|
-
outLines.push(taskBody.trimEnd())
|
|
149
|
-
|
|
150
|
-
console.log(outLines.join('\n'))
|
|
151
|
-
|
|
152
|
-
return 0
|
|
153
|
-
}
|