@nitra/cursor 3.22.0 → 3.23.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 (229) hide show
  1. package/.pi-template/extensions/n-cursor-adr/docs/index.md +181 -0
  2. package/AGENTS.template.md +4 -0
  3. package/CHANGELOG.md +37 -3
  4. package/bin/docs/n-cursor.md +636 -0
  5. package/bin/docs/rename-yaml-extensions.md +207 -0
  6. package/bin/n-cursor.js +30 -3
  7. package/package.json +1 -1
  8. package/rules/abie/docs/fix.md +18 -0
  9. package/rules/abie/js/docs/applies.md +26 -0
  10. package/rules/abie/js/docs/env_dns.md +32 -0
  11. package/rules/abie/js/docs/firebase_hosting.md +23 -0
  12. package/rules/abie/js/docs/hc_pairing.md +35 -0
  13. package/rules/abie/js/docs/ua_http_route.md +28 -0
  14. package/rules/abie/js/docs/ua_node_selector.md +28 -0
  15. package/rules/abie/lib/docs/enabled.md +29 -0
  16. package/rules/abie/lib/docs/env-dns.md +35 -0
  17. package/rules/abie/lib/docs/hc-yaml.md +33 -0
  18. package/rules/abie/lib/docs/http-route.md +44 -0
  19. package/rules/abie/lib/docs/k8s-tree.md +40 -0
  20. package/rules/abie/lib/docs/kustomization-patches.md +47 -0
  21. package/rules/abie/lib/docs/overlay-paths.md +38 -0
  22. package/rules/abie/lib/docs/yaml.md +29 -0
  23. package/rules/adr/docs/fix.md +148 -0
  24. package/rules/adr/js/docs/hooks.md +259 -0
  25. package/rules/bun/docs/fix.md +156 -0
  26. package/rules/bun/js/docs/layout.md +393 -0
  27. package/rules/capacitor/docs/fix.md +121 -0
  28. package/rules/capacitor/js/docs/platforms.md +295 -0
  29. package/rules/changelog/changelog.mdc +4 -2
  30. package/rules/changelog/docs/fix.md +174 -0
  31. package/rules/changelog/js/consistency.mjs +114 -13
  32. package/rules/changelog/js/docs/consistency.md +387 -0
  33. package/rules/changelog/lib/docs/package-manifest.md +210 -0
  34. package/rules/ci4/docs/fix.md +179 -0
  35. package/rules/ci4/js/docs/marksman_config.md +128 -0
  36. package/rules/docker/docker.mdc +8 -3
  37. package/rules/docker/docs/fix.md +171 -0
  38. package/rules/docker/js/docs/lint.md +258 -0
  39. package/rules/docker/lib/docs/docker-hadolint.md +184 -0
  40. package/rules/docker/lib/docs/docker-mirror.md +247 -0
  41. package/rules/docker/lib/docs/docker-native-addon.md +170 -0
  42. package/rules/docker/lib/docs/docker-nginx-user.md +219 -0
  43. package/rules/docker/lint/docs/lint.md +193 -0
  44. package/rules/efes/docs/fix.md +203 -0
  45. package/rules/feedback/docs/fix.md +140 -0
  46. package/rules/flow/docs/fix.md +152 -0
  47. package/rules/ga/docs/fix.md +158 -0
  48. package/rules/ga/js/docs/lint.md +100 -0
  49. package/rules/ga/js/docs/workflows.md +217 -0
  50. package/rules/ga/lint/docs/lint.md +209 -0
  51. package/rules/ga/policy/clean_merged_branch/clean_merged_branch.rego +11 -2
  52. package/rules/ga/policy/clean_merged_branch/template/clean-merged-branch.yml.snippet.yml +3 -4
  53. package/rules/graphql/docs/fix.md +126 -0
  54. package/rules/graphql/js/docs/tooling.md +264 -0
  55. package/rules/graphql/lib/docs/graphql-gql-scan.md +219 -0
  56. package/rules/hasura/docs/fix.md +120 -0
  57. package/rules/hasura/hasura.mdc +14 -0
  58. package/rules/hasura/js/docs/internal_urls.md +326 -0
  59. package/rules/image-avif/docs/fix.md +132 -0
  60. package/rules/image-avif/js/docs/avif_generation.md +241 -0
  61. package/rules/image-compress/docs/fix.md +150 -0
  62. package/rules/image-compress/js/docs/package_setup.md +191 -0
  63. package/rules/js-bun-db/docs/fix.md +148 -0
  64. package/rules/js-bun-db/js/docs/safety.md +231 -0
  65. package/rules/js-bun-db/js-bun-db.mdc +42 -13
  66. package/rules/js-bun-db/lib/docs/bun-sql-scan.md +347 -0
  67. package/rules/js-bun-redis/docs/fix.md +123 -0
  68. package/rules/js-bun-redis/js/docs/imports.md +176 -0
  69. package/rules/js-bun-redis/lib/docs/redis-imports.md +223 -0
  70. package/rules/js-lint/docs/fix.md +117 -0
  71. package/rules/js-lint/js/docs/lint.md +250 -0
  72. package/rules/js-lint/js/docs/tooling.md +348 -0
  73. package/rules/js-lint/js/docs/utils_imports.md +207 -0
  74. package/rules/js-lint-ci/docs/fix.md +154 -0
  75. package/rules/js-lint-ci/js/docs/lint.md +144 -0
  76. package/rules/js-mssql/docs/fix.md +128 -0
  77. package/rules/js-mssql/js/docs/deps.md +263 -0
  78. package/rules/js-mssql/lib/docs/mssql-pool-scan.md +367 -0
  79. package/rules/js-run/docs/fix.md +144 -0
  80. package/rules/js-run/js/docs/runtime.md +388 -0
  81. package/rules/js-run/lib/docs/bunyan-imports.md +117 -0
  82. package/rules/js-run/lib/docs/check-env-scan.md +433 -0
  83. package/rules/js-run/lib/docs/conn-file-rules.md +300 -0
  84. package/rules/js-run/lib/docs/conn-imports-scan.md +204 -0
  85. package/rules/js-run/lib/docs/promise-settimeout-scan.md +326 -0
  86. package/rules/k8s/docs/fix.md +129 -0
  87. package/rules/k8s/js/docs/manifests.md +344 -0
  88. package/rules/k8s/js/manifests.mjs +6 -2
  89. package/rules/k8s/k8s.mdc +4 -2
  90. package/rules/k8s/lint/docs/lint.md +411 -0
  91. package/rules/k8s/policy/network_policy/template/deployment.snippet.yaml +2 -0
  92. package/rules/k8s/policy/network_policy/template/stateful-set.snippet.yaml +2 -0
  93. package/rules/nginx-default-tpl/docs/fix.md +124 -0
  94. package/rules/nginx-default-tpl/js/docs/template.md +378 -0
  95. package/rules/npm-module/docs/fix.md +98 -0
  96. package/rules/npm-module/js/docs/package_structure.md +274 -0
  97. package/rules/npm-module/js/docs/rule_meta.md +137 -0
  98. package/rules/npm-module/js/docs/skill_meta.md +190 -0
  99. package/rules/php/docs/fix.md +107 -0
  100. package/rules/php/js/docs/tooling.md +152 -0
  101. package/rules/php/lint/docs/lint.md +215 -0
  102. package/rules/python/docs/fix.md +163 -0
  103. package/rules/python/js/docs/applies.md +108 -0
  104. package/rules/python/js/docs/tooling.md +153 -0
  105. package/rules/python/lint/docs/lint.md +322 -0
  106. package/rules/rego/docs/fix.md +121 -0
  107. package/rules/rego/js/docs/applies.md +174 -0
  108. package/rules/rego/js/docs/lint.md +118 -0
  109. package/rules/rego/lint/docs/lint.md +204 -0
  110. package/rules/release/docs/change.md +185 -0
  111. package/rules/release/docs/fix.md +119 -0
  112. package/rules/release/docs/release.md +222 -0
  113. package/rules/release/lib/docs/aggregate.md +246 -0
  114. package/rules/release/lib/docs/change-file.md +200 -0
  115. package/rules/release/lib/docs/fallback.md +203 -0
  116. package/rules/rust/docs/fix.md +129 -0
  117. package/rules/rust/js/docs/applies.md +140 -0
  118. package/rules/rust/lib/docs/has-cargo-toml.md +130 -0
  119. package/rules/security/docs/fix.md +86 -0
  120. package/rules/security/js/docs/lint.md +171 -0
  121. package/rules/security/js/docs/sample_secret.md +190 -0
  122. package/rules/security/js/docs/trufflehog.md +137 -0
  123. package/rules/security/js/lint.mjs +9 -1
  124. package/rules/style-lint/docs/fix.md +155 -0
  125. package/rules/style-lint/js/docs/lint.md +184 -0
  126. package/rules/style-lint/js/docs/tooling.md +194 -0
  127. package/rules/tauri/docs/fix.md +158 -0
  128. package/rules/tauri/js/docs/cargo_mutants_config.md +168 -0
  129. package/rules/tauri/js/docs/tooling.md +228 -0
  130. package/rules/test/coverage/coverage.mjs +15 -3
  131. package/rules/test/docs/fix.md +132 -0
  132. package/rules/test/js/data/stryker_config/docs/stryker-vue-macros-ignorer.md +138 -0
  133. package/rules/test/js/data/stryker_config/docs/stryker.config.baseline.md +134 -0
  134. package/rules/test/js/data/stryker_config/docs/stryker.config.vue.baseline.md +160 -0
  135. package/rules/test/js/data/vitest_config/docs/vitest.config.baseline.md +195 -0
  136. package/rules/test/js/docs/cargo_mutants_config.md +173 -0
  137. package/rules/test/js/docs/location.md +136 -0
  138. package/rules/test/js/docs/no-process-chdir.md +160 -0
  139. package/rules/test/js/docs/no-relative-fs-path.md +271 -0
  140. package/rules/test/js/docs/stryker_config.md +152 -0
  141. package/rules/test/js/docs/vitest-config-pool-forks.md +174 -0
  142. package/rules/text/docs/fix.md +118 -0
  143. package/rules/text/js/docs/forbidden-prettier.md +143 -0
  144. package/rules/text/js/docs/formatting.md +256 -0
  145. package/rules/text/js/docs/lint.md +122 -0
  146. package/rules/text/lint/docs/lint.md +220 -0
  147. package/rules/text/lint/docs/run-dotenv-linter.md +157 -0
  148. package/rules/text/lint/docs/run-shellcheck.md +212 -0
  149. package/rules/text/lint/docs/run-v8r.md +197 -0
  150. package/rules/vue/docs/fix.md +127 -0
  151. package/rules/vue/js/docs/packages.md +335 -0
  152. package/rules/vue/lib/docs/vue-forbidden-imports.md +261 -0
  153. package/rules/worktree/docs/fix.md +161 -0
  154. package/schemas/rule-meta.json +5 -1
  155. package/scripts/auto-rules.mjs +7 -4
  156. package/scripts/coverage-classify/docs/apply.md +202 -0
  157. package/scripts/coverage-classify/docs/cache.md +203 -0
  158. package/scripts/coverage-classify/docs/index.md +218 -0
  159. package/scripts/coverage-classify/docs/prompt.md +132 -0
  160. package/scripts/coverage-classify/docs/verdict-schema.md +169 -0
  161. package/scripts/coverage-fix-extract.mjs +122 -0
  162. package/scripts/coverage-fix.mjs +1 -1
  163. package/scripts/dispatcher/docs/graph.md +346 -0
  164. package/scripts/dispatcher/docs/index.md +236 -0
  165. package/scripts/dispatcher/docs/trace.md +296 -0
  166. package/scripts/dispatcher/index.mjs +1 -1
  167. package/scripts/dispatcher/lib/active.mjs +4 -8
  168. package/scripts/dispatcher/lib/commands.mjs +7 -11
  169. package/scripts/dispatcher/lib/docs/active.md +348 -0
  170. package/scripts/dispatcher/lib/docs/artifact.md +232 -0
  171. package/scripts/dispatcher/lib/docs/budget.md +167 -0
  172. package/scripts/dispatcher/lib/docs/capability.md +196 -0
  173. package/scripts/dispatcher/lib/docs/commands.md +210 -0
  174. package/scripts/dispatcher/lib/docs/events.md +182 -0
  175. package/scripts/dispatcher/lib/docs/executor.md +190 -0
  176. package/scripts/dispatcher/lib/docs/flow-lock.md +161 -0
  177. package/scripts/dispatcher/lib/docs/flow-resolve.md +267 -0
  178. package/scripts/dispatcher/lib/docs/gate.md +231 -0
  179. package/scripts/dispatcher/lib/docs/level.md +335 -0
  180. package/scripts/dispatcher/lib/docs/plan-panel.md +181 -0
  181. package/scripts/dispatcher/lib/docs/plan.md +200 -0
  182. package/scripts/dispatcher/lib/docs/planner.md +269 -0
  183. package/scripts/dispatcher/lib/docs/review.md +255 -0
  184. package/scripts/dispatcher/lib/docs/reviewer.md +240 -0
  185. package/scripts/dispatcher/lib/docs/snapshot.md +247 -0
  186. package/scripts/dispatcher/lib/docs/spec.md +203 -0
  187. package/scripts/dispatcher/lib/docs/state-store.md +303 -0
  188. package/scripts/dispatcher/lib/docs/subagent-runner.md +173 -0
  189. package/scripts/dispatcher/lib/executor.mjs +6 -1
  190. package/scripts/dispatcher/lib/flow-resolve.mjs +3 -1
  191. package/scripts/dispatcher/lib/level.mjs +29 -3
  192. package/scripts/dispatcher/lib/review.mjs +1 -1
  193. package/scripts/dispatcher/lib/subagent-runner.mjs +5 -3
  194. package/scripts/docs/auto-rules.md +376 -0
  195. package/scripts/docs/auto-skills.md +173 -0
  196. package/scripts/docs/build-agents-commands.md +183 -0
  197. package/scripts/docs/cli-entry.md +153 -0
  198. package/scripts/docs/coverage-fix.md +177 -0
  199. package/scripts/docs/ensure-nitra-cursor-dev-dependencies.md +189 -0
  200. package/scripts/lib/changed-files.mjs +4 -1
  201. package/scripts/lib/docs/changed-files.md +149 -0
  202. package/scripts/lib/docs/check-mdc-template-refs.md +222 -0
  203. package/scripts/lib/docs/check-reporter.md +175 -0
  204. package/scripts/lib/docs/discover-check-rules-from-cursor.md +157 -0
  205. package/scripts/lib/docs/discover-checkable-rules.md +165 -0
  206. package/scripts/lib/docs/ensure-tool.md +254 -0
  207. package/scripts/lib/docs/generated-markdown.md +275 -0
  208. package/scripts/lib/docs/gha-workflow.md +326 -0
  209. package/scripts/lib/docs/inline-template-links.md +303 -0
  210. package/scripts/lib/docs/list-rule-ids.md +156 -0
  211. package/scripts/lib/docs/load-cursor-config.md +147 -0
  212. package/scripts/lib/docs/mirror-parity.md +167 -0
  213. package/scripts/lib/worktree.mjs +26 -0
  214. package/scripts/worktree-cli.mjs +12 -2
  215. package/skills/coverage-fix/SKILL.md +34 -45
  216. package/skills/docgen/SKILL.md +44 -23
  217. package/skills/docgen/bench/etalon/firebase_hosting.md +19 -0
  218. package/skills/docgen/bench/etalon/k8s-tree.md +24 -0
  219. package/skills/docgen/bench/etalon/overlay-paths.md +24 -0
  220. package/skills/docgen/js/docgen-ignore.mjs +54 -0
  221. package/skills/docgen/js/docgen-scan.mjs +37 -21
  222. package/skills/llm-patch/SKILL.md +23 -2
  223. package/skills/start-check/SKILL.md +26 -53
  224. package/skills/start-check/js/check.mjs +211 -0
  225. package/skills/taze/SKILL.md +9 -3
  226. package/skills/taze/js/diff.mjs +154 -0
  227. package/types/bin/n-cursor.d.ts +1 -1
  228. package/skills/fix-tests/SKILL.md +0 -119
  229. package/skills/fix-tests/meta.json +0 -1
@@ -0,0 +1,182 @@
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
+ const eventsPath = flowEventsPath('/repo/.worktrees/feat-x')
123
+ // → '/repo/.worktrees/feat-x.events.jsonl'
124
+ ```
125
+ 2. **Дозапис події під час переходу стану** (через `state-store.recordTransition`, що дотримується WAL-інваріанта):
126
+ ```js
127
+ appendEvent(eventsPath, { type: 'step_started', step: 2 })
128
+ // потім: оновити .flow.json
129
+ ```
130
+ 3. **Дозапис обліку API-виклику** (тим самим API, у тому ж файлі):
131
+ ```js
132
+ appendEvent(eventsPath, {
133
+ type: 'api_call',
134
+ provider: 'anthropic',
135
+ model: 'claude-opus',
136
+ tokens: 1234
137
+ })
138
+ ```
139
+ 4. **Читання історії** (наприклад, для відновлення стану або аудиту):
140
+ ```js
141
+ const events = readEvents(eventsPath)
142
+ for (const ev of events) {
143
+ // обробка ev.at, ev.type, …
144
+ }
145
+ ```
146
+
147
+ ### Сценарій краху
148
+
149
+ 1. `appendFileSync` починає писати рядок, процес гине посеред запису.
150
+ 2. Файл містить N−1 валідних рядків + один торваний (наприклад, без `}` і `\n` у кінці).
151
+ 3. На наступному старті `readEvents`:
152
+ - відфільтрує порожні рядки;
153
+ - спробує `JSON.parse` для кожного непорожнього;
154
+ - на торваному останньому отримає виняток → `flatMap` поверне `[]` для нього → запис **пропускається**;
155
+ - решта N−1 подій нормально потрапить у результат.
156
+ 4. Дані до моменту краху збережено; пошкоджений запис вважається втраченим — це прийнятна семантика append-only WAL.
157
+
158
+ ### Інваріанти, що накладає модуль
159
+
160
+ - **Усі шляхи — абсолютні** (`isAbsolute` перевірка у всіх трьох експортах).
161
+ - **Кодування файлу — UTF-8** (явно вказано як в `appendFileSync`, так і в `readFileSync`).
162
+ - **Формат рядка — однорядковий JSON + `\n`** (jsonl).
163
+ - **`at` — ISO-8601-рядок UTC** (через `Date.prototype.toISOString`).
164
+ - **Порядок збереження — порядок дозапису** (нативна семантика append-only).
165
+
166
+ ### Чого модуль свідомо не робить
167
+
168
+ - Не реалізує **rotation / compaction** журналу — це відповідальність вищих рівнів або інфраструктури worktree.
169
+ - Не виконує **fsync** після кожного append — покладається на ОС-кеш та append-only толерантність читача.
170
+ - Не валідує **схему події** — приймає будь-який `object` (контрактом структури подій відає `flow`-специфікація і споживачі).
171
+ - Не обробляє **багатопроцесний конкурентний доступ** — для одного worktree передбачається один пишучий процес; для читача конкурентність безпечна, оскільки `appendFileSync` атомарно дозаписує цілий буфер.
172
+
173
+ ## Rebuild Test
174
+
175
+ Документація достатня, щоб переписати модуль з нуля:
176
+
177
+ - три іменовані експорти з точними сигнатурами;
178
+ - формула sibling-шляху (`dirname` + `basename` + `.events.jsonl`);
179
+ - WAL-інваріант і його носій (`state-store.recordTransition`);
180
+ - формат запису (`{ at: ISO, ...event }`, `JSON.stringify` + `\n`, кодування UTF-8);
181
+ - стратегія читання (`existsSync`-гард → split `\n` → фільтр порожніх → `JSON.parse` у `try/catch` + `flatMap`);
182
+ - правила валідації абсолютних шляхів і повідомлення про помилки.
@@ -0,0 +1,190 @@
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`.
@@ -0,0 +1,161 @@
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
+ - **Обов’язковий абсолютний шлях:** відносні шляхи відхиляються одразу — це усуває клас помилок «лок узяли не там, де думали».