@nitra/cursor 3.21.1 → 3.23.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.
Files changed (231) hide show
  1. package/.pi-template/extensions/n-cursor-adr/docs/index.md +181 -0
  2. package/CHANGELOG.md +37 -3
  3. package/bin/docs/n-cursor.md +636 -0
  4. package/bin/docs/rename-yaml-extensions.md +207 -0
  5. package/bin/n-cursor.js +30 -3
  6. package/package.json +1 -1
  7. package/rules/abie/docs/fix.md +18 -0
  8. package/rules/abie/js/docs/applies.md +26 -0
  9. package/rules/abie/js/docs/env_dns.md +32 -0
  10. package/rules/abie/js/docs/firebase_hosting.md +23 -0
  11. package/rules/abie/js/docs/hc_pairing.md +35 -0
  12. package/rules/abie/js/docs/ua_http_route.md +28 -0
  13. package/rules/abie/js/docs/ua_node_selector.md +28 -0
  14. package/rules/abie/lib/docs/enabled.md +29 -0
  15. package/rules/abie/lib/docs/env-dns.md +35 -0
  16. package/rules/abie/lib/docs/hc-yaml.md +33 -0
  17. package/rules/abie/lib/docs/http-route.md +44 -0
  18. package/rules/abie/lib/docs/k8s-tree.md +40 -0
  19. package/rules/abie/lib/docs/kustomization-patches.md +47 -0
  20. package/rules/abie/lib/docs/overlay-paths.md +38 -0
  21. package/rules/abie/lib/docs/yaml.md +29 -0
  22. package/rules/adr/docs/fix.md +148 -0
  23. package/rules/adr/js/docs/hooks.md +259 -0
  24. package/rules/bun/docs/fix.md +156 -0
  25. package/rules/bun/js/docs/layout.md +393 -0
  26. package/rules/capacitor/docs/fix.md +121 -0
  27. package/rules/capacitor/js/docs/platforms.md +295 -0
  28. package/rules/changelog/changelog.mdc +2 -2
  29. package/rules/changelog/docs/fix.md +174 -0
  30. package/rules/changelog/js/consistency.mjs +114 -13
  31. package/rules/changelog/js/docs/consistency.md +387 -0
  32. package/rules/changelog/lib/docs/package-manifest.md +210 -0
  33. package/rules/ci4/docs/fix.md +179 -0
  34. package/rules/ci4/js/docs/marksman_config.md +128 -0
  35. package/rules/docker/docker.mdc +8 -3
  36. package/rules/docker/docs/fix.md +171 -0
  37. package/rules/docker/js/docs/lint.md +258 -0
  38. package/rules/docker/lib/docs/docker-hadolint.md +184 -0
  39. package/rules/docker/lib/docs/docker-mirror.md +247 -0
  40. package/rules/docker/lib/docs/docker-native-addon.md +170 -0
  41. package/rules/docker/lib/docs/docker-nginx-user.md +219 -0
  42. package/rules/docker/lint/docs/lint.md +193 -0
  43. package/rules/efes/docs/fix.md +203 -0
  44. package/rules/feedback/docs/fix.md +140 -0
  45. package/rules/flow/docs/fix.md +152 -0
  46. package/rules/ga/docs/fix.md +158 -0
  47. package/rules/ga/js/docs/lint.md +100 -0
  48. package/rules/ga/js/docs/workflows.md +217 -0
  49. package/rules/ga/lint/docs/lint.md +209 -0
  50. package/rules/ga/policy/clean_merged_branch/clean_merged_branch.rego +11 -2
  51. package/rules/ga/policy/clean_merged_branch/template/clean-merged-branch.yml.snippet.yml +3 -4
  52. package/rules/graphql/docs/fix.md +126 -0
  53. package/rules/graphql/js/docs/tooling.md +264 -0
  54. package/rules/graphql/lib/docs/graphql-gql-scan.md +219 -0
  55. package/rules/hasura/docs/fix.md +120 -0
  56. package/rules/hasura/hasura.mdc +14 -0
  57. package/rules/hasura/js/docs/internal_urls.md +326 -0
  58. package/rules/image-avif/docs/fix.md +132 -0
  59. package/rules/image-avif/js/docs/avif_generation.md +241 -0
  60. package/rules/image-compress/docs/fix.md +150 -0
  61. package/rules/image-compress/js/docs/package_setup.md +191 -0
  62. package/rules/js-bun-db/docs/fix.md +148 -0
  63. package/rules/js-bun-db/js/docs/safety.md +231 -0
  64. package/rules/js-bun-db/js-bun-db.mdc +42 -13
  65. package/rules/js-bun-db/lib/docs/bun-sql-scan.md +347 -0
  66. package/rules/js-bun-redis/docs/fix.md +123 -0
  67. package/rules/js-bun-redis/js/docs/imports.md +176 -0
  68. package/rules/js-bun-redis/lib/docs/redis-imports.md +223 -0
  69. package/rules/js-lint/docs/fix.md +117 -0
  70. package/rules/js-lint/js/docs/lint.md +250 -0
  71. package/rules/js-lint/js/docs/tooling.md +348 -0
  72. package/rules/js-lint/js/docs/utils_imports.md +207 -0
  73. package/rules/js-lint/js/lint-findings.mjs +110 -0
  74. package/rules/js-lint/js/lint.mjs +86 -15
  75. package/rules/js-lint-ci/docs/fix.md +154 -0
  76. package/rules/js-lint-ci/js/docs/lint.md +144 -0
  77. package/rules/js-mssql/docs/fix.md +128 -0
  78. package/rules/js-mssql/js/docs/deps.md +263 -0
  79. package/rules/js-mssql/lib/docs/mssql-pool-scan.md +367 -0
  80. package/rules/js-run/docs/fix.md +144 -0
  81. package/rules/js-run/js/docs/runtime.md +388 -0
  82. package/rules/js-run/lib/docs/bunyan-imports.md +117 -0
  83. package/rules/js-run/lib/docs/check-env-scan.md +433 -0
  84. package/rules/js-run/lib/docs/conn-file-rules.md +300 -0
  85. package/rules/js-run/lib/docs/conn-imports-scan.md +204 -0
  86. package/rules/js-run/lib/docs/promise-settimeout-scan.md +326 -0
  87. package/rules/k8s/docs/fix.md +129 -0
  88. package/rules/k8s/js/docs/manifests.md +344 -0
  89. package/rules/k8s/js/manifests.mjs +6 -2
  90. package/rules/k8s/k8s.mdc +4 -2
  91. package/rules/k8s/lint/docs/lint.md +411 -0
  92. package/rules/k8s/policy/network_policy/template/deployment.snippet.yaml +2 -0
  93. package/rules/k8s/policy/network_policy/template/stateful-set.snippet.yaml +2 -0
  94. package/rules/nginx-default-tpl/docs/fix.md +124 -0
  95. package/rules/nginx-default-tpl/js/docs/template.md +378 -0
  96. package/rules/npm-module/docs/fix.md +98 -0
  97. package/rules/npm-module/js/docs/package_structure.md +274 -0
  98. package/rules/npm-module/js/docs/rule_meta.md +137 -0
  99. package/rules/npm-module/js/docs/skill_meta.md +190 -0
  100. package/rules/php/docs/fix.md +107 -0
  101. package/rules/php/js/docs/tooling.md +152 -0
  102. package/rules/php/lint/docs/lint.md +215 -0
  103. package/rules/python/docs/fix.md +163 -0
  104. package/rules/python/js/docs/applies.md +108 -0
  105. package/rules/python/js/docs/tooling.md +153 -0
  106. package/rules/python/lint/docs/lint.md +322 -0
  107. package/rules/rego/docs/fix.md +121 -0
  108. package/rules/rego/js/docs/applies.md +174 -0
  109. package/rules/rego/js/docs/lint.md +118 -0
  110. package/rules/rego/lint/docs/lint.md +204 -0
  111. package/rules/release/docs/change.md +185 -0
  112. package/rules/release/docs/fix.md +119 -0
  113. package/rules/release/docs/release.md +222 -0
  114. package/rules/release/lib/docs/aggregate.md +246 -0
  115. package/rules/release/lib/docs/change-file.md +200 -0
  116. package/rules/release/lib/docs/fallback.md +203 -0
  117. package/rules/rust/docs/fix.md +129 -0
  118. package/rules/rust/js/docs/applies.md +140 -0
  119. package/rules/rust/lib/docs/has-cargo-toml.md +130 -0
  120. package/rules/security/docs/fix.md +86 -0
  121. package/rules/security/js/docs/lint.md +171 -0
  122. package/rules/security/js/docs/sample_secret.md +190 -0
  123. package/rules/security/js/docs/trufflehog.md +137 -0
  124. package/rules/security/js/lint.mjs +9 -1
  125. package/rules/style-lint/docs/fix.md +155 -0
  126. package/rules/style-lint/js/docs/lint.md +184 -0
  127. package/rules/style-lint/js/docs/tooling.md +194 -0
  128. package/rules/tauri/docs/fix.md +158 -0
  129. package/rules/tauri/js/docs/cargo_mutants_config.md +168 -0
  130. package/rules/tauri/js/docs/tooling.md +228 -0
  131. package/rules/test/coverage/coverage.mjs +15 -3
  132. package/rules/test/docs/fix.md +132 -0
  133. package/rules/test/js/data/stryker_config/docs/stryker-vue-macros-ignorer.md +138 -0
  134. package/rules/test/js/data/stryker_config/docs/stryker.config.baseline.md +134 -0
  135. package/rules/test/js/data/stryker_config/docs/stryker.config.vue.baseline.md +160 -0
  136. package/rules/test/js/data/vitest_config/docs/vitest.config.baseline.md +195 -0
  137. package/rules/test/js/docs/cargo_mutants_config.md +173 -0
  138. package/rules/test/js/docs/location.md +136 -0
  139. package/rules/test/js/docs/no-process-chdir.md +160 -0
  140. package/rules/test/js/docs/no-relative-fs-path.md +271 -0
  141. package/rules/test/js/docs/stryker_config.md +152 -0
  142. package/rules/test/js/docs/vitest-config-pool-forks.md +174 -0
  143. package/rules/text/docs/fix.md +118 -0
  144. package/rules/text/js/docs/forbidden-prettier.md +143 -0
  145. package/rules/text/js/docs/formatting.md +256 -0
  146. package/rules/text/js/docs/lint.md +122 -0
  147. package/rules/text/lint/docs/lint.md +220 -0
  148. package/rules/text/lint/docs/run-dotenv-linter.md +157 -0
  149. package/rules/text/lint/docs/run-shellcheck.md +212 -0
  150. package/rules/text/lint/docs/run-v8r.md +197 -0
  151. package/rules/vue/docs/fix.md +127 -0
  152. package/rules/vue/js/docs/packages.md +335 -0
  153. package/rules/vue/lib/docs/vue-forbidden-imports.md +261 -0
  154. package/rules/worktree/docs/fix.md +161 -0
  155. package/schemas/rule-meta.json +5 -1
  156. package/scripts/auto-rules.mjs +7 -4
  157. package/scripts/coverage-classify/docs/apply.md +202 -0
  158. package/scripts/coverage-classify/docs/cache.md +203 -0
  159. package/scripts/coverage-classify/docs/index.md +218 -0
  160. package/scripts/coverage-classify/docs/prompt.md +132 -0
  161. package/scripts/coverage-classify/docs/verdict-schema.md +169 -0
  162. package/scripts/coverage-fix-extract.mjs +122 -0
  163. package/scripts/coverage-fix.mjs +1 -1
  164. package/scripts/dispatcher/docs/graph.md +346 -0
  165. package/scripts/dispatcher/docs/index.md +236 -0
  166. package/scripts/dispatcher/docs/trace.md +296 -0
  167. package/scripts/dispatcher/index.mjs +1 -1
  168. package/scripts/dispatcher/lib/active.mjs +4 -8
  169. package/scripts/dispatcher/lib/commands.mjs +7 -11
  170. package/scripts/dispatcher/lib/docs/active.md +348 -0
  171. package/scripts/dispatcher/lib/docs/artifact.md +232 -0
  172. package/scripts/dispatcher/lib/docs/budget.md +167 -0
  173. package/scripts/dispatcher/lib/docs/capability.md +196 -0
  174. package/scripts/dispatcher/lib/docs/commands.md +210 -0
  175. package/scripts/dispatcher/lib/docs/events.md +182 -0
  176. package/scripts/dispatcher/lib/docs/executor.md +190 -0
  177. package/scripts/dispatcher/lib/docs/flow-lock.md +161 -0
  178. package/scripts/dispatcher/lib/docs/flow-resolve.md +267 -0
  179. package/scripts/dispatcher/lib/docs/gate.md +231 -0
  180. package/scripts/dispatcher/lib/docs/level.md +335 -0
  181. package/scripts/dispatcher/lib/docs/plan-panel.md +181 -0
  182. package/scripts/dispatcher/lib/docs/plan.md +200 -0
  183. package/scripts/dispatcher/lib/docs/planner.md +269 -0
  184. package/scripts/dispatcher/lib/docs/review.md +255 -0
  185. package/scripts/dispatcher/lib/docs/reviewer.md +240 -0
  186. package/scripts/dispatcher/lib/docs/snapshot.md +247 -0
  187. package/scripts/dispatcher/lib/docs/spec.md +203 -0
  188. package/scripts/dispatcher/lib/docs/state-store.md +303 -0
  189. package/scripts/dispatcher/lib/docs/subagent-runner.md +173 -0
  190. package/scripts/dispatcher/lib/executor.mjs +6 -1
  191. package/scripts/dispatcher/lib/flow-resolve.mjs +3 -1
  192. package/scripts/dispatcher/lib/level.mjs +29 -3
  193. package/scripts/dispatcher/lib/review.mjs +1 -1
  194. package/scripts/dispatcher/lib/subagent-runner.mjs +5 -3
  195. package/scripts/docs/auto-rules.md +376 -0
  196. package/scripts/docs/auto-skills.md +173 -0
  197. package/scripts/docs/build-agents-commands.md +183 -0
  198. package/scripts/docs/cli-entry.md +153 -0
  199. package/scripts/docs/coverage-fix.md +177 -0
  200. package/scripts/docs/ensure-nitra-cursor-dev-dependencies.md +189 -0
  201. package/scripts/lib/changed-files.mjs +4 -1
  202. package/scripts/lib/diff-added-lines.mjs +85 -0
  203. package/scripts/lib/docs/changed-files.md +149 -0
  204. package/scripts/lib/docs/check-mdc-template-refs.md +222 -0
  205. package/scripts/lib/docs/check-reporter.md +175 -0
  206. package/scripts/lib/docs/discover-check-rules-from-cursor.md +157 -0
  207. package/scripts/lib/docs/discover-checkable-rules.md +165 -0
  208. package/scripts/lib/docs/ensure-tool.md +254 -0
  209. package/scripts/lib/docs/generated-markdown.md +275 -0
  210. package/scripts/lib/docs/gha-workflow.md +326 -0
  211. package/scripts/lib/docs/inline-template-links.md +303 -0
  212. package/scripts/lib/docs/list-rule-ids.md +156 -0
  213. package/scripts/lib/docs/load-cursor-config.md +147 -0
  214. package/scripts/lib/docs/mirror-parity.md +167 -0
  215. package/scripts/lib/worktree.mjs +26 -0
  216. package/scripts/worktree-cli.mjs +12 -2
  217. package/skills/coverage-fix/SKILL.md +34 -45
  218. package/skills/docgen/SKILL.md +44 -23
  219. package/skills/docgen/bench/etalon/firebase_hosting.md +19 -0
  220. package/skills/docgen/bench/etalon/k8s-tree.md +24 -0
  221. package/skills/docgen/bench/etalon/overlay-paths.md +24 -0
  222. package/skills/docgen/js/docgen-ignore.mjs +54 -0
  223. package/skills/docgen/js/docgen-scan.mjs +37 -21
  224. package/skills/llm-patch/SKILL.md +23 -2
  225. package/skills/start-check/SKILL.md +26 -53
  226. package/skills/start-check/js/check.mjs +211 -0
  227. package/skills/taze/SKILL.md +9 -3
  228. package/skills/taze/js/diff.mjs +154 -0
  229. package/types/bin/n-cursor.d.ts +1 -1
  230. package/skills/fix-tests/SKILL.md +0 -119
  231. package/skills/fix-tests/meta.json +0 -1
@@ -0,0 +1,303 @@
1
+ # state-store.mjs
2
+
3
+ ## Огляд
4
+
5
+ Модуль `state-store.mjs` реалізує **crash-safe сховище runtime-стану `flow`** (відповідно до spec §4 та §4.1 правила `n-flow`). Він зберігає поточний стан виконання worktree-флоу у вигляді JSON-файла, гарантує атомарність запису та fail-closed поведінку при пошкодженні даних.
6
+
7
+ Ключові архітектурні рішення:
8
+
9
+ - **Sibling-файл, а не файл усередині worktree.** Файл стану розміщується **поруч** із checkout-директорією worktree, а не всередині неї. Для checkout-директорії `…/.worktrees/feat-x` файл стану — `…/.worktrees/feat-x.flow.json`. Причина: файл усередині worktree був би `untracked` у feature-гілці й міг би випадково потрапити у `git add -A`.
10
+ - **Атомарний запис** — через temp-файл на тому самому файловому системному рівні, `fsync` даних і атомарний `rename` (POSIX-гарантія: операція або повністю успішна, або не відбулась).
11
+ - **Fail-closed на corruption** — будь-яка некоректність (невалідний JSON, несумісний `schema_version`) призводить до `throw`, а не до тихого скидання стану. Принцип: краще зупинити flow, ніж стартувати новий поверх зіпсованого стану.
12
+ - **WAL-перехід** — пара `appendEvent` (журнал) → `updateState` (snapshot). Журнал — джерело істини для reconcile при `resume`, snapshot — швидкий зріз поточного стану.
13
+ - **Усі шляхи абсолютні** — вимога правила `no-relative-fs-path`. Кожна публічна функція робить `isAbsolute()`-перевірку й кидає, якщо їй передали відносний шлях.
14
+
15
+ ## Експорти / API
16
+
17
+ | Експорт | Тип | Призначення |
18
+ | ------------------------------------------------------------------- | ---------------------- | ---------------------------------------------------------------------------------------- |
19
+ | `SCHEMA_VERSION` | `const number` (= `1`) | Поточна версія схеми JSON-стану. Несумісність → fail-closed read. |
20
+ | `flowStatePath(worktreeDir)` | function | Дериватор шляху sibling-файла стану з абсолютного шляху worktree-checkout. |
21
+ | `writeState(statePath, state)` | function | Атомарний запис стану з автоматичним проставленням `schema_version`. |
22
+ | `readState(statePath)` | function | Читання стану з валідацією `schema_version`; `null`, якщо файлу нема. |
23
+ | `updateState(statePath, fn)` | function | Read-modify-write: читає, прогонить через трансформер `fn`, атомарно пише. |
24
+ | `removeState(statePath)` | function | Ідемпотентне видалення sibling-файла стану. |
25
+ | `recordTransition({ statePath, eventsPath }, event, stateFn, now?)` | function | WAL-перехід: спершу `appendEvent`, потім `updateState`. |
26
+ | `cleanupFlowSiblings(worktreeDir)` | function | Видалення всіх runtime-sibling-ів worktree (`.flow.json`, `.events.jsonl`, лок-каталог). |
27
+
28
+ ## Функції
29
+
30
+ ### `flowStatePath(worktreeDir)`
31
+
32
+ **Сигнатура:** `(worktreeDir: string) => string`
33
+
34
+ **Параметри:**
35
+
36
+ - `worktreeDir` — абсолютний шлях checkout-директорії worktree (наприклад, `…/.worktrees/feat-x`).
37
+
38
+ **Повертає:** Абсолютний шлях sibling-файла стану виду `…/.worktrees/feat-x.flow.json`.
39
+
40
+ **Логіка:** Бере `dirname(worktreeDir)` (батьківську теку, як правило `.worktrees/`) і конкатенує з `basename(worktreeDir) + '.flow.json'`.
41
+
42
+ **Помилки:** `Error('flowStatePath: очікується абсолютний шлях …')` — якщо `worktreeDir` не абсолютний.
43
+
44
+ **Side effects:** Немає (чиста функція над шляхами).
45
+
46
+ ---
47
+
48
+ ### `fsyncPath(path)` (внутрішня)
49
+
50
+ **Сигнатура:** `(path: string) => void`
51
+
52
+ **Параметри:**
53
+
54
+ - `path` — абсолютний шлях до файла або каталогу, який треба `fsync`-нути.
55
+
56
+ **Повертає:** `void`.
57
+
58
+ **Логіка:** Відкриває файл/каталог у режимі читання (`openSync(path, 'r')`), викликає `fsyncSync(fd)`, у `finally` закриває дескриптор через `closeSync`.
59
+
60
+ **Side effects:** Системний виклик `fsync` — гарантує, що дані файла записані на фізичний носій (необхідно перед `rename`, щоб уникнути ситуації, коли rename видно, а вміст ще в буфері).
61
+
62
+ **Примітка:** Функція приватна (не експортується), використовується лише `writeState`.
63
+
64
+ ---
65
+
66
+ ### `writeState(statePath, state)`
67
+
68
+ **Сигнатура:** `(statePath: string, state: object) => object`
69
+
70
+ **Параметри:**
71
+
72
+ - `statePath` — абсолютний шлях кінцевого файла стану (`.flow.json`).
73
+ - `state` — об'єкт стану **без** поля `schema_version` (воно проставляється автоматично).
74
+
75
+ **Повертає:** Фактично записаний об'єкт виду `{ schema_version: SCHEMA_VERSION, ...state }`.
76
+
77
+ **Алгоритм (атомарний запис):**
78
+
79
+ 1. Перевірка абсолютності шляху → `throw`, якщо відносний.
80
+ 2. `mkdirSync(dir, { recursive: true })` — гарантуємо існування батьківської теки.
81
+ 3. Збираємо `payload = { schema_version: SCHEMA_VERSION, ...state }`.
82
+ 4. Генеруємо унікальне ім'я temp-файла: `.${basename(statePath)}.${pid}.${randomHex6}.tmp` у тій самій теці (важливо — той самий FS, щоб `rename` був атомарним).
83
+ 5. `writeFileSync(tmp, JSON.stringify(payload, null, 2) + '\n', 'utf8')`.
84
+ 6. `fsyncPath(tmp)` — flush даних temp-файла на диск.
85
+ 7. `renameSync(tmp, statePath)` — атомарна заміна.
86
+ 8. Best-effort `fsyncPath(dir)` — fsync батьківського каталогу для durability rename. На Windows може кинути `EISDIR`/`EPERM` — помилка проковтується (некритично).
87
+
88
+ **Помилки:** `Error('writeState: очікується абсолютний шлях …')` — якщо `statePath` не абсолютний. Інші помилки I/O (нема прав, диск повний тощо) пробрасуються нагору.
89
+
90
+ **Side effects:**
91
+
92
+ - Створення батьківської теки (якщо її нема).
93
+ - Створення й видалення temp-файла з PID та випадковим суфіксом.
94
+ - Перейменування `tmp → statePath`.
95
+ - `fsync` файла та (best-effort) каталогу.
96
+
97
+ ---
98
+
99
+ ### `readState(statePath)`
100
+
101
+ **Сигнатура:** `(statePath: string) => object | null`
102
+
103
+ **Параметри:**
104
+
105
+ - `statePath` — абсолютний шлях `.flow.json`.
106
+
107
+ **Повертає:**
108
+
109
+ - `null`, якщо файлу не існує (нормальна ситуація: flow ще не починався).
110
+ - Розпарсений об'єкт стану — за успішного читання й валідації.
111
+
112
+ **Алгоритм:**
113
+
114
+ 1. Перевірка абсолютності шляху.
115
+ 2. `existsSync(statePath)` → `false` → повертаємо `null`.
116
+ 3. `readFileSync(statePath, 'utf8')`.
117
+ 4. Спроба `JSON.parse(raw)`; якщо `SyntaxError` → `throw` з повідомленням `пошкоджений стан (невалідний JSON) … fail-closed`.
118
+ 5. Валідація типу й `schema_version`: якщо не об'єкт, `null`, або `schema_version !== SCHEMA_VERSION` → `throw` `несумісний або пошкоджений schema_version … fail-closed`.
119
+ 6. Повертаємо розпарсений об'єкт.
120
+
121
+ **Помилки:**
122
+
123
+ - `Error('readState: очікується абсолютний шлях …')`.
124
+ - `Error('readState: пошкоджений стан (невалідний JSON) … fail-closed')` (§4.1.6).
125
+ - `Error('readState: несумісний або пошкоджений schema_version … fail-closed')` (§4.1.6).
126
+
127
+ **Side effects:** Тільки читання файла (без модифікацій).
128
+
129
+ ---
130
+
131
+ ### `updateState(statePath, fn)`
132
+
133
+ **Сигнатура:** `(statePath: string, fn: (state: object) => object) => object`
134
+
135
+ **Параметри:**
136
+
137
+ - `statePath` — абсолютний шлях `.flow.json`.
138
+ - `fn` — функція-трансформер: приймає поточний стан (або `{}`, якщо файла нема) і повертає новий стан.
139
+
140
+ **Повертає:** Записаний об'єкт (результат `writeState`).
141
+
142
+ **Алгоритм:**
143
+
144
+ 1. `current = readState(statePath)`.
145
+ 2. `next = fn(current ?? {})` — якщо файла не існує, `fn` отримує порожній об'єкт.
146
+ 3. `return writeState(statePath, next)`.
147
+
148
+ **Помилки:** Будь-яке пробивання з `readState` чи `writeState`. Окрім того, помилки всередині `fn` пробросяться як є.
149
+
150
+ **Side effects:** Read-modify-write — повний цикл (читання → трансформ → атомарний запис).
151
+
152
+ ---
153
+
154
+ ### `removeState(statePath)`
155
+
156
+ **Сигнатура:** `(statePath: string) => void`
157
+
158
+ **Параметри:**
159
+
160
+ - `statePath` — абсолютний шлях `.flow.json`.
161
+
162
+ **Повертає:** `void`.
163
+
164
+ **Алгоритм:** `rmSync(statePath, { force: true })` — ідемпотентно (відсутній файл не помилка).
165
+
166
+ **Помилки:** `Error('removeState: очікується абсолютний шлях …')`.
167
+
168
+ **Side effects:** Видалення sibling-файла стану.
169
+
170
+ **Контекст використання:** Cleanup при `worktree remove` або `flow cancel`.
171
+
172
+ ---
173
+
174
+ ### `recordTransition({ statePath, eventsPath }, event, stateFn, now?)`
175
+
176
+ **Сигнатура:** `({ statePath: string, eventsPath: string }, event: object, stateFn: (state: object) => object, now?: () => number) => object`
177
+
178
+ **Параметри:**
179
+
180
+ - `paths.statePath` — абсолютний шлях `.flow.json`.
181
+ - `paths.eventsPath` — абсолютний шлях журналу подій `.events.jsonl`.
182
+ - `event` — об'єкт події переходу (формат визначається `appendEvent`).
183
+ - `stateFn` — трансформер стану (як у `updateState`).
184
+ - `now` — фабрика поточного часу в ms (за замовчуванням `Date.now`); використовується для тестів.
185
+
186
+ **Повертає:** Записаний стан (результат `updateState`).
187
+
188
+ **Алгоритм (WAL-перехід, §4.1.2):**
189
+
190
+ 1. `appendEvent(eventsPath, event, now)` — спершу **журнал** (durable WAL-запис події).
191
+ 2. `updateState(statePath, stateFn)` — потім snapshot.
192
+
193
+ **Гарантія:** Якщо крок 2 впаде, подія в журналі вже durable; на `resume` reconcile-логіка зможе відновити стан зі snapshot + хвоста журналу.
194
+
195
+ **Side effects:** Дописування в `.events.jsonl` та атомарний read-modify-write `.flow.json`.
196
+
197
+ ---
198
+
199
+ ### `cleanupFlowSiblings(worktreeDir)`
200
+
201
+ **Сигнатура:** `(worktreeDir: string) => void`
202
+
203
+ **Параметри:**
204
+
205
+ - `worktreeDir` — абсолютний шлях checkout-директорії worktree (`…/.worktrees/feat-x`).
206
+
207
+ **Повертає:** `void`.
208
+
209
+ **Алгоритм:** Для `base = basename(worktreeDir)` та `dir = dirname(worktreeDir)` видаляє:
210
+
211
+ 1. `<dir>/<base>.flow.json` — sibling-snapshot стану.
212
+ 2. `<dir>/<base>.events.jsonl` — sibling-журнал подій.
213
+ 3. `<dir>/.flow-lock-<base>/` — лок-каталог (з `recursive: true`).
214
+
215
+ Усі `rmSync` з `force: true` — ідемпотентні.
216
+
217
+ **Помилки:** `Error('cleanupFlowSiblings: очікується абсолютний шлях …')`.
218
+
219
+ **Side effects:** Видалення трьох sibling-артефактів. Викликається з `flow cancel` та `worktree remove`. Інакше sibling-и осиротіють — git їх не чистить (бо вони поза worktree).
220
+
221
+ ## Залежності
222
+
223
+ ### Node.js stdlib
224
+
225
+ - `node:fs` — `closeSync`, `existsSync`, `fsyncSync`, `mkdirSync`, `openSync`, `readFileSync`, `renameSync`, `rmSync`, `writeFileSync`.
226
+ - `node:path` — `basename`, `dirname`, `isAbsolute`, `join`.
227
+ - `node:crypto` — `randomBytes` (унікальне ім'я temp-файла).
228
+ - `node:process` — `pid` (також для унікальності temp-імені, особливо при паралельних процесах).
229
+
230
+ ### Внутрішні залежності модуля
231
+
232
+ - `./events.mjs` — функція `appendEvent(eventsPath, event, now)`. Використовується лише в `recordTransition`.
233
+
234
+ ### Логічні залежності (через дизайн, не через `import`)
235
+
236
+ - Spec `n-flow` §4, §4.1, §4.1.2, §4.1.6 — формальні вимоги до crash-safety та fail-closed.
237
+ - Конвенція розміщення worktree: усі worktree під `.worktrees/<branch>/`; sibling-файли — на одному рівні з checkout.
238
+
239
+ ## Потік виконання / Використання
240
+
241
+ ### Типовий життєвий цикл стану flow
242
+
243
+ ```
244
+ 1. Старт flow:
245
+ const statePath = flowStatePath('/abs/path/.worktrees/feat-x')
246
+ // → '/abs/path/.worktrees/feat-x.flow.json'
247
+
248
+ 2. Перший запис (initial state):
249
+ writeState(statePath, { stage: 'init', step: 0 })
250
+ // → файл містить { schema_version: 1, stage: 'init', step: 0 }
251
+
252
+ 3. Перехід стану з журналюванням (WAL):
253
+ recordTransition(
254
+ { statePath, eventsPath: statePath.replace('.flow.json', '.events.jsonl') },
255
+ { type: 'stage_advanced', from: 'init', to: 'apply' },
256
+ (s) => ({ ...s, stage: 'apply', step: s.step + 1 })
257
+ )
258
+
259
+ 4. Read-modify-write без події (рідко):
260
+ updateState(statePath, (s) => ({ ...s, last_seen_at: Date.now() }))
261
+
262
+ 5. Читання при resume:
263
+ const state = readState(statePath)
264
+ if (state === null) { /* новий flow */ }
265
+ else { /* відновлення з state */ }
266
+
267
+ 6. Cleanup на завершення / cancel:
268
+ cleanupFlowSiblings('/abs/path/.worktrees/feat-x')
269
+ // → видалить feat-x.flow.json, feat-x.events.jsonl, .flow-lock-feat-x/
270
+ ```
271
+
272
+ ### Гарантії crash-safety
273
+
274
+ - **Crash під час `writeFileSync(tmp, …)`** — кінцевий файл `statePath` залишається попередньою версією; temp-файл — orphan (буде перезаписаний при наступному запуску завдяки `pid + randomBytes`).
275
+ - **Crash між `writeFileSync` і `fsyncPath(tmp)`** — temp-файл може бути порожнім/частковим, але `rename` ще не виконано — `statePath` цілий.
276
+ - **Crash між `fsyncPath(tmp)` і `renameSync`** — temp-файл цілий і durable, але не на місці; `statePath` цілий.
277
+ - **Crash під час `renameSync`** — POSIX гарантує атомарність: або `statePath` — старий вміст, або новий, **ніколи частковий**.
278
+ - **Crash після `renameSync` до fsync каталогу** — на більшості FS rename вже durable; fsync каталогу — best-effort страховка.
279
+
280
+ ### Fail-closed reading
281
+
282
+ При `readState`:
283
+
284
+ - **Файл порожній / не JSON** → `throw 'пошкоджений стан (невалідний JSON) … fail-closed'`. Адмін має вручну інспектувати журнал подій та прийняти рішення (replay або видалення).
285
+ - **`schema_version` відсутня / інша** → `throw 'несумісний або пошкоджений schema_version … fail-closed'`. Захищає від запуску нової версії dispatcher над станом старого формату й навпаки.
286
+
287
+ ### Контекст у dispatcher
288
+
289
+ Модуль є інфраструктурною частиною `npm/scripts/dispatcher/`. Виклики йдуть із вищих шарів (стейт-машини flow, CLI-команди `flow start/resume/cancel`, обробники сигналів). Модуль сам не керує життєвим циклом — він лише надає примітиви read/write/update/remove/transition та шляхові деривації.
290
+
291
+ ## Rebuild Test
292
+
293
+ Документація містить достатньо інформації, щоб відновити функціональність модуля з нуля:
294
+
295
+ - Точні імпорти з Node stdlib та локального `./events.mjs`.
296
+ - Константу `SCHEMA_VERSION = 1`.
297
+ - Сім публічних функцій з повними сигнатурами, валідацією аргументів, алгоритмами та повідомленнями про помилки.
298
+ - Внутрішню helper-функцію `fsyncPath` зі сценарієм використання.
299
+ - Точну схему атомарного запису (temp у тій самій теці → write → fsync → rename → best-effort fsync каталогу).
300
+ - Точну схему fail-closed reading (`null` для відсутнього файла, throw для пошкодженого/несумісного).
301
+ - WAL-послідовність `appendEvent` → `updateState` у `recordTransition`.
302
+ - Перелік усіх трьох sibling-артефактів для `cleanupFlowSiblings` (`.flow.json`, `.events.jsonl`, `.flow-lock-<base>/`).
303
+ - Дериватор `flowStatePath`: `join(dirname(worktreeDir), basename(worktreeDir) + '.flow.json')`.
@@ -0,0 +1,173 @@
1
+ # subagent-runner.mjs
2
+
3
+ ## Огляд
4
+
5
+ `subagent-runner.mjs` — модуль абстракції спавну сфокусованого субагента для Активного Раннера (фаза Ф3/Ф4 диспетчера). Реалізує специфікацію §15.1: надає уніфікований інтерфейс `runStep(prompt, opts)` поверх трьох можливих backend-ів:
6
+
7
+ 1. `claude-agent-sdk` — програмний доступ через пакет `@anthropic-ai/claude-agent-sdk`, потребує змінної середовища `ANTHROPIC_API_KEY`.
8
+ 2. `claude -p` — CLI-аутентифікація користувача через виконуваний `claude` у `PATH`.
9
+ 3. `cursor-agent -p` — CLI-аутентифікація через виконуваний `cursor-agent` у `PATH`.
10
+
11
+ Якщо жоден backend недоступний — модуль кидає виняток із текстом `NO_BACKEND` (polyfill без runner-а не стартує, §2.2).
12
+
13
+ Згідно з коментарем у заголовку файлу, для inner-спавну навмисно НЕ використовується pi.dev: у автономному режимі pi.dev — це зовнішній драйвер, тож спавнити ним внутрішні субагенти призвело б до рекурсії (§9.1).
14
+
15
+ Усі probe-залежності (`spawn`, `isInPath`, `canImportSdk`, `query`) проектовані під ін'єкцію — для тестування без реальних процесів та без встановленого SDK.
16
+
17
+ ## Експорти / API
18
+
19
+ Модуль експортує чотири іменовані функції:
20
+
21
+ - `isBinaryInPath(name, spawn?)` — перевірка наявності бінарника в `PATH`.
22
+ - `selectBackend({ hasApiKey, canImportSdk, isInPath })` — вибір backend-а за пріоритетом.
23
+ - `cliRunner(bin, deps?)` — фабрика runner-а для CLI-варіанту.
24
+ - `sdkRunner(deps?)` — фабрика runner-а для SDK-варіанту.
25
+ - `createRunner(deps?)` — головний фасад, що сам визначає та повертає потрібний runner.
26
+
27
+ Внутрішня (не експортована) функція: `probeSdk()` — перевіряє можливість динамічного імпорту SDK.
28
+
29
+ Константа модульного рівня `NO_BACKEND` — текст повідомлення помилки, коли жоден backend недоступний.
30
+
31
+ ## Функції
32
+
33
+ ### `isBinaryInPath(name, spawn = spawnSync)`
34
+
35
+ Перевіряє, чи є виконуваний бінарник у `PATH` через виклик `command -v <name>`.
36
+
37
+ - Параметри:
38
+ - `name` (`string`) — ім'я виконуваного.
39
+ - `spawn` (`typeof spawnSync`, optional) — ін'єкція для тестів; за замовчуванням `spawnSync` із `node:child_process`.
40
+ - Повертає: `boolean` — `true`, якщо `spawn` повернув статус `0`; інакше `false`. Якщо `r.status` дорівнює `null`/`undefined`, трактується як `1` (тобто `false`).
41
+ - Side effects: виклик дочірнього процесу `command -v` через shell (`shell: true`).
42
+
43
+ ### `selectBackend({ hasApiKey, canImportSdk, isInPath })`
44
+
45
+ Вибирає backend за чітко зафіксованим пріоритетом: SDK > Claude CLI > Cursor CLI.
46
+
47
+ - Параметри (один об'єкт):
48
+ - `hasApiKey` (`boolean`) — чи задана `ANTHROPIC_API_KEY`.
49
+ - `canImportSdk` (`boolean`) — чи імпортується `@anthropic-ai/claude-agent-sdk`.
50
+ - `isInPath` (`(name: string) => boolean`) — предикат наявності бінарника у `PATH`.
51
+ - Повертає: рядковий літерал `'sdk'`, `'claude'`, `'cursor'` або `null`.
52
+ - Логіка:
53
+ 1. Якщо `hasApiKey` і `canImportSdk` одночасно істинні — `'sdk'`.
54
+ 2. Інакше якщо `isInPath('claude')` — `'claude'`.
55
+ 3. Інакше якщо `isInPath('cursor-agent')` — `'cursor'`.
56
+ 4. Інакше — `null`.
57
+ - Side effects: відсутні (за умови, що `isInPath` чистий).
58
+
59
+ ### `cliRunner(bin, deps = {})`
60
+
61
+ Створює CLI-runner на основі бінарника `claude` або `cursor-agent` (обидва підтримують прапор `-p` для подачі промпта зі stdin).
62
+
63
+ - Параметри:
64
+ - `bin` (`'claude' | 'cursor-agent'`) — який саме CLI запускати.
65
+ - `deps.spawn` (`typeof spawnSync`, optional) — ін'єкція; за замовчуванням `spawnSync`.
66
+ - Повертає об'єкт:
67
+ - `backend` — рядок, що дорівнює переданому `bin`.
68
+ - `runStep(prompt, { cwd } = {})` — синхронна функція, що викликає `spawn(bin, ['-p'], { input: prompt, cwd, encoding: 'utf8' })`. Повертає `{ ok: boolean, output: string }`, де:
69
+ - `ok` — `true`, якщо `r.status === 0` (null/undefined трактується як 1 → `false`).
70
+ - `output` — конкатенація `stdout` та `stderr` (порожні рядки, якщо undefined).
71
+ - Side effects: спавн дочірнього CLI-процесу при кожному виклику `runStep`.
72
+
73
+ ### `sdkRunner(deps = {})`
74
+
75
+ Створює SDK-runner, який працює через async-iterable `query` з пакета `@anthropic-ai/claude-agent-sdk`.
76
+
77
+ - Параметри:
78
+ - `deps.query` (`(input: object) => AsyncIterable`, optional) — ін'єкція функції `query` для тестів. Якщо не задано — модуль динамічно імпортує `@anthropic-ai/claude-agent-sdk` і бере звідти `query`.
79
+ - Повертає об'єкт:
80
+ - `backend` — рядок `'sdk'`.
81
+ - `runStep(prompt, { cwd } = {})` — `async` функція, що повертає `Promise<{ ok: boolean, output: string }>`.
82
+ - Логіка `runStep`:
83
+ 1. Лінива ініціалізація `query` (якщо не передано в `deps`).
84
+ 2. Виклик `query({ prompt, options: { cwd, maxTurns: 20, allowedTools: ['Read', 'Edit', 'Bash'] } })`.
85
+ 3. Ітерує асинхронно по повідомленнях:
86
+ - Якщо `msg.text` — рядок, додає його до `output`.
87
+ - Якщо `msg.type === 'result'`, фіналізує `ok = msg.is_error !== true`.
88
+ 4. У разі винятку — повертає `{ ok: false, output: String(error?.message ?? error) }`.
89
+ - Side effects: динамічний імпорт SDK (один раз на виклик, якщо `query` не ін'єктовано); мережеві/процесні дії SDK; обмеження інструментів виключно до `Read`, `Edit`, `Bash`.
90
+
91
+ ### `createRunner(deps = {})`
92
+
93
+ Головний фасад модуля. Підбирає та повертає runner відповідно до доступних backend-ів.
94
+
95
+ - Параметри (об'єкт `deps` для тестів; усі поля опціональні):
96
+ - `backend` — явне переозначення вибору (`'sdk'`/`'claude'`/`'cursor'`).
97
+ - `env` — мапа змінних середовища; за замовчуванням `processEnv` (`process.env`).
98
+ - `isInPath` — функція; за замовчуванням обгортка над `isBinaryInPath(name, deps.spawn)`.
99
+ - `canImportSdk` — заздалегідь обчислений прапор; інакше викликається `probeSdk()`.
100
+ - `spawn` — використовується як `deps.spawn` для дефолтного `isInPath`.
101
+ - `query` — пробрасується в `sdkRunner` як `deps.query`.
102
+ - Повертає: `Promise<runner>`, де `runner` має форму `{ backend, runStep }` (синхронний для CLI, асинхронний для SDK — обидва типи представлені в одному фасаді).
103
+ - Логіка:
104
+ 1. Резолвить `env`, `isInPath`, `canImportSdk`.
105
+ 2. Якщо `deps.backend` не задано — викликає `selectBackend({ hasApiKey: Boolean(env.ANTHROPIC_API_KEY), canImportSdk, isInPath })`.
106
+ 3. Якщо backend усе ще `null`/falsy — `throw new Error(NO_BACKEND)`.
107
+ 4. Для `'sdk'` — повертає `sdkRunner(deps)`.
108
+ 5. Для `'claude'` — повертає `cliRunner('claude', deps)`.
109
+ 6. Для будь-якого іншого ненульового — повертає `cliRunner('cursor-agent', deps)` (тобто `'cursor'` мапиться на `cursor-agent`).
110
+ - Side effects: можливий динамічний імпорт SDK через `probeSdk()`; виклики `spawn` через дефолтний `isInPath`.
111
+
112
+ ### `probeSdk()` (внутрішня)
113
+
114
+ Перевіряє, чи можна динамічно імпортувати `@anthropic-ai/claude-agent-sdk`.
115
+
116
+ - Параметри: відсутні.
117
+ - Повертає: `Promise<boolean>` — `true`, якщо `import` успішний, інакше `false` (виняток поглинається порожнім `catch`).
118
+ - Side effects: динамічний `import()` модуля SDK.
119
+
120
+ ## Залежності
121
+
122
+ - `node:child_process` — `spawnSync` (синхронний спавн дочірніх процесів для `command -v` та CLI-runner-а).
123
+ - `node:process` — `env` як `processEnv` (читання змінних середовища, передусім `ANTHROPIC_API_KEY`).
124
+ - `@anthropic-ai/claude-agent-sdk` (optional, динамічний `import`) — джерело функції `query` для SDK-runner-а; відсутність пакета — допустимий сценарій (`probeSdk()` ловить виняток).
125
+
126
+ Зовнішні виконувані файли, очікувані у `PATH`:
127
+
128
+ - `command` — POSIX-shell builtin для `command -v`.
129
+ - `claude` — CLI Claude Code.
130
+ - `cursor-agent` — CLI Cursor Agent.
131
+
132
+ Жодних інших імпортів із локального проєкту модуль не робить — він самодостатній.
133
+
134
+ ## Потік виконання / Використання
135
+
136
+ Типовий сценарій використання у диспетчері (Активний Раннер, фази Ф3/Ф4):
137
+
138
+ 1. Виклик `await createRunner()` без параметрів.
139
+ 2. Усередині `createRunner` виконується probe доступних backend-ів:
140
+ - перевіряється `process.env.ANTHROPIC_API_KEY`;
141
+ - намагається динамічно імпортувати `@anthropic-ai/claude-agent-sdk`;
142
+ - перевіряються `claude` та `cursor-agent` у `PATH` через `command -v`.
143
+ 3. `selectBackend` повертає перший доступний backend за пріоритетом SDK → claude → cursor.
144
+ 4. Якщо нічого не знайдено — кидається `Error(NO_BACKEND)`, що зупиняє стартування polyfill-а (§2.2).
145
+ 5. Інакше повертається об'єкт `{ backend, runStep }`.
146
+ 6. Викликаючий код передає `runStep(prompt, { cwd })`:
147
+ - для SDK — отримує `Promise<{ ok, output }>`, працюючи з обмеженим набором інструментів `Read`/`Edit`/`Bash` та лімітом `maxTurns: 20`;
148
+ - для CLI — отримує синхронний `{ ok, output }` після завершення дочірнього процесу.
149
+
150
+ Для тестування потік ін'єктується наскрізно: будь-яку із залежностей (`spawn`, `isInPath`, `canImportSdk`, `query`, `env`, `backend`) можна перевизначити, що дозволяє покривати модуль unit-тестами без реальних процесів і без SDK.
151
+
152
+ Особливості та інваріанти:
153
+
154
+ - Пріоритет backend-ів зафіксований у `selectBackend` і не змінюється від виклику до виклику.
155
+ - `runStep` у CLI-варіанті завжди синхронний, у SDK-варіанті — асинхронний; форма результату `{ ok, output }` уніфікована.
156
+ - `output` у CLI завжди склеює `stdout` та `stderr` без розділювача.
157
+ - У SDK-варіанті помилки під час ітерації `query` ловляться й конвертуються в `{ ok: false, output: <message> }`, тобто `runStep` не пробрасує винятки нагору.
158
+ - Значення `null`/`undefined` для `r.status` у `spawnSync`-результатах послідовно нормалізується через `?? 1`, що дає поведінку "невідомий статус == помилка".
159
+
160
+ ## Rebuild Test
161
+
162
+ Документ описує лише той API, що присутній у файлі `subagent-runner.mjs`:
163
+
164
+ - Експорти: `isBinaryInPath`, `selectBackend`, `cliRunner`, `sdkRunner`, `createRunner` — усі п'ять перевірено за вихідним кодом.
165
+ - Внутрішня функція `probeSdk` зафіксована як приватна (не експортована).
166
+ - Константа `NO_BACKEND` згадана як модульна.
167
+ - Імпорти `spawnSync` із `node:child_process` та `env` як `processEnv` із `node:process` зафіксовані.
168
+ - Опційна залежність `@anthropic-ai/claude-agent-sdk` згадана з акцентом на динамічний import у двох місцях (`sdkRunner.runStep` та `probeSdk`).
169
+ - Пріоритет `sdk → claude → cursor` і поведінка `createRunner` за відсутності backend (throw `NO_BACKEND`) відповідають коду.
170
+ - Деталі `sdkRunner` (`maxTurns: 20`, `allowedTools: ['Read', 'Edit', 'Bash']`, обробка `msg.type === 'result'` та `msg.is_error`) узяті безпосередньо з тіла функції.
171
+ - Формула `r.status ?? 1` для нормалізації статусу описана точно так, як у коді.
172
+
173
+ Жодних припущень про невидимі в файлі деталі (тестові файли, інтеграцію з конкретними викликами в інших модулях диспетчера) у документі не зроблено.
@@ -66,7 +66,12 @@ export async function executePlan(paths, deps) {
66
66
  const verdict = await verify(cwd)
67
67
  if (verdict.pass) {
68
68
  commit(cwd, `flow: step ${step.step} — ${step.task}`) // commit ЛИШЕ після зеленого
69
- state = recordTransition(paths, { type: 'step_done', step: step.step }, s => patchStep(s, i, { status: 'done' }), now)
69
+ state = recordTransition(
70
+ paths,
71
+ { type: 'step_done', step: step.step },
72
+ s => patchStep(s, i, { status: 'done' }),
73
+ now
74
+ )
70
75
  done = true
71
76
  } else {
72
77
  state = recordTransition(
@@ -90,7 +90,9 @@ export function resolveActiveFlowState({ cwd = processCwd(), branch } = {}, deps
90
90
  const label = sanitizeBranch(branch)
91
91
  const worktreeDir = worktreePaths(repoRoot, branch).checkout
92
92
  if (!exists(worktreeDir)) {
93
- return notFound(`worktree для гілки «${branch}» не знайдено (${worktreeDir}) — перевір назву або зроби \`flow init\``)
93
+ return notFound(
94
+ `worktree для гілки «${branch}» не знайдено (${worktreeDir}) — перевір назву або зроби \`flow init\``
95
+ )
94
96
  }
95
97
  return { statePath: flowStatePath(worktreeDir), worktreeDir, label, autoResolved: false, error: null }
96
98
  }
@@ -16,6 +16,30 @@ const L0_WORD_KEYS = ['fix', 'typo', 'bump', 'rename', 'hotfix']
16
16
  const L0_SUBSTR_KEYS = ['опечат', 'перейменув']
17
17
  /** L2 — багатофайлова фіча/рефактор. */
18
18
  const L2_KEYS = ['feature', 'epic', 'refactor', 'рефактор', 'фіча']
19
+ /**
20
+ * Сигнали складності (cross-cutting rules/checks/correctness): разом із L0-дієсловом
21
+ * задача НЕ тривіальна. Перекривають L0 (мінімум L2) — щоб rules/checks-роботу не
22
+ * класифікувати як trivial і не пропускати spec (беклог #2). Підрядком (case-insensitive).
23
+ */
24
+ const COMPLEXITY_KEYS = [
25
+ 'mdc',
26
+ 'policy',
27
+ 'політик',
28
+ 'rego',
29
+ 'checker',
30
+ 'чекер',
31
+ 'правило',
32
+ 'правила',
33
+ 'rules',
34
+ 'суперечн',
35
+ 'інваріант',
36
+ 'invariant',
37
+ 'порушен',
38
+ 'violation',
39
+ 'кілька файл',
40
+ 'декілька',
41
+ 'meta-'
42
+ ]
19
43
 
20
44
  /**
21
45
  * Чи символ — ASCII-літера/цифра (межа слова). `undefined` (край рядка) — не alnum.
@@ -44,7 +68,9 @@ function hasWord(text, word) {
44
68
 
45
69
  /**
46
70
  * Рівень складності задачі за описом: 0 (тривіальне) … 3 (архітектурне).
47
- * Пріоритет: L3 > L0 > L2 > дефолт L1.
71
+ * Пріоритет: L3 > L0 (якщо без сигналів складності) > L2/складність > дефолт L1.
72
+ * Лише сигнал складності перекриває L0-дієслово (`fix mdc checker` → L2, не L0);
73
+ * L2-ключі порядок L0 не змінюють (`rename feature` лишається L0, як і раніше).
48
74
  * @param {string} desc опис задачі
49
75
  * @returns {0 | 1 | 2 | 3} рівень
50
76
  */
@@ -53,8 +79,8 @@ export function detectLevel(desc) {
53
79
  const has = keys => keys.some(k => d.includes(k))
54
80
  const isL0 = L0_WORD_KEYS.some(k => hasWord(d, k)) || L0_SUBSTR_KEYS.some(k => d.includes(k))
55
81
  if (has(L3_KEYS)) return 3
56
- if (isL0) return 0
57
- if (has(L2_KEYS)) return 2
82
+ if (isL0 && !has(COMPLEXITY_KEYS)) return 0
83
+ if (has(L2_KEYS) || has(COMPLEXITY_KEYS)) return 2
58
84
  return 1
59
85
  }
60
86
 
@@ -43,7 +43,7 @@ export function diffFromBase(base, run, cwd) {
43
43
  export function reviewerPrompt(diff, risk) {
44
44
  const lens =
45
45
  risk === 'high'
46
- ? 'ОСОБЛИВА УВАГА БЕЗПЕЦІ: auth/доступи, секрети/токени, ін\'єкції, валідація входу, незворотні операції.'
46
+ ? "ОСОБЛИВА УВАГА БЕЗПЕЦІ: auth/доступи, секрети/токени, ін'єкції, валідація входу, незворотні операції."
47
47
  : ''
48
48
  return [
49
49
  'Ти — прискіпливий adversarial-рецензент. Знайди баги, ризики й smells, які ВНОСИТЬ або зачіпає цей diff.',
@@ -77,7 +77,10 @@ export function sdkRunner(deps = {}) {
77
77
  let output = ''
78
78
  let ok = true
79
79
  try {
80
- for await (const msg of query({ prompt, options: { cwd, maxTurns: 20, allowedTools: ['Read', 'Edit', 'Bash'] } })) {
80
+ for await (const msg of query({
81
+ prompt,
82
+ options: { cwd, maxTurns: 20, allowedTools: ['Read', 'Edit', 'Bash'] }
83
+ })) {
81
84
  if (typeof msg?.text === 'string') output += msg.text
82
85
  if (msg?.type === 'result') ok = msg.is_error !== true
83
86
  }
@@ -99,8 +102,7 @@ export async function createRunner(deps = {}) {
99
102
  const env = deps.env ?? processEnv
100
103
  const isInPath = deps.isInPath ?? (name => isBinaryInPath(name, deps.spawn))
101
104
  const canImportSdk = deps.canImportSdk ?? (await probeSdk())
102
- const backend =
103
- deps.backend ?? selectBackend({ hasApiKey: Boolean(env.ANTHROPIC_API_KEY), canImportSdk, isInPath })
105
+ const backend = deps.backend ?? selectBackend({ hasApiKey: Boolean(env.ANTHROPIC_API_KEY), canImportSdk, isInPath })
104
106
  if (!backend) throw new Error(NO_BACKEND)
105
107
  if (backend === 'sdk') return sdkRunner(deps)
106
108
  return cliRunner(backend === 'claude' ? 'claude' : 'cursor-agent', deps)