@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,167 @@
1
+ # budget.mjs
2
+
3
+ ## Огляд
4
+
5
+ Модуль `budget.mjs` реалізує **запобіжник бюджету** (budget guard) для автономного режиму диспатчера (відповідно до spec §9.4). Його основна задача — обмежити кількість викликів зовнішнього API, які робить `SubagentRunner`, щоб запобігти неконтрольованим витратам у середовищах, де нема людини-оператора для ручної зупинки (наприклад, на сервері або в CI).
6
+
7
+ Модуль реалізує патерн **декоратор/обгортка**: приймає базовий runner, що має метод `runStep`, і повертає функціонально еквівалентний об'єкт, який додатково:
8
+
9
+ - веде лічильник викликів `runStep`;
10
+ - порівнює лічильник із заданим лімітом `maxApiCalls` перед кожним викликом;
11
+ - кидає кастомну помилку `BudgetExceeded` при перевищенні ліміту;
12
+ - логує кожен виклик через переданий колбек `log`.
13
+
14
+ Окремо експортується клас помилки `BudgetExceeded`, який споживачі модуля (зокрема `run`-функція диспатчера, §9.4) можуть ловити для коректного завершення з відповідним статусом.
15
+
16
+ У коментарях файлу зазначено, що параметр `maxCostUsd` (бюджет за вартістю в доларах) поки не реалізований і запланований на майбутнє — на момент написання модуль рахує лише факти виклику, а не споживані токени чи вартість.
17
+
18
+ ## Експорти / API
19
+
20
+ Модуль експортує два ідентифікатори (обидва — іменовані експорти, default-експорту немає):
21
+
22
+ | Експорт | Тип | Призначення |
23
+ | ---------------- | --------------------- | ------------------------------------------------------------------------------------------------ |
24
+ | `BudgetExceeded` | `class extends Error` | Кастомний клас помилки, що сигналізує про вичерпання бюджету `maxApiCalls`. |
25
+ | `withBudget` | функція | Фабрика, що обгортає переданий runner лічильником і поверненням нового runner-сумісного об'єкта. |
26
+
27
+ ## Функції
28
+
29
+ ### `class BudgetExceeded extends Error`
30
+
31
+ **Сигнатура:** `class BudgetExceeded extends Error {}`
32
+
33
+ **Опис:** Маркерний підклас стандартної `Error`. Тіло класу порожнє — використовується виключно для відрізнення цього типу помилки від інших через `instanceof BudgetExceeded` або зіставлення в `try/catch`. Конструктор успадковується від `Error` і приймає рядок-повідомлення.
34
+
35
+ **Параметри:**
36
+
37
+ - `message` (string, через `super`) — текст помилки. У місці кидання передається рядок виду `budget: вичерпано maxApiCalls=<N>`.
38
+
39
+ **Side effects:** жодних.
40
+
41
+ **Кидає:** не застосовно (це сам клас помилки, його екземпляр кидається в `withBudget`).
42
+
43
+ ### `withBudget(runner, opts)`
44
+
45
+ **Сигнатура:**
46
+
47
+ ```
48
+ withBudget(
49
+ runner: { backend?: string, runStep: (prompt: string, opts?: object) => object },
50
+ opts?: { maxApiCalls?: number, log?: (m: string) => void }
51
+ ): {
52
+ backend: string,
53
+ runStep: (prompt: string, opts?: object) => Promise<object>,
54
+ readonly calls: number
55
+ }
56
+ ```
57
+
58
+ **Параметри:**
59
+
60
+ - `runner` (object, обовʼязковий) — базовий runner, який потрібно обмежити бюджетом. Інтерфейс:
61
+ - `backend?: string` — опційний ідентифікатор бекенду (пробросовується у поверненому обʼєкті).
62
+ - `runStep(prompt, opts?) => object` — метод виконання одного кроку (виклику API). Повертати може як значення, так і `Promise`; в обгортці він викликається без `await`, але метод обгортки оголошено `async`, тож результат буде успішно "розгорнуто" викликаючою стороною.
63
+ - `opts` (object, опційний, за замовчуванням `{}`) — налаштування бюджету:
64
+ - `maxApiCalls?: number` — максимально дозволена кількість викликів `runStep`. Якщо не передано, використовується `Number.POSITIVE_INFINITY` (фактично — без ліміту).
65
+ - `log?: (m: string) => void` — колбек логування. Якщо не передано, використовується no-op `() => {}`.
66
+
67
+ **Повертає:** новий обʼєкт-обгортку з такими членами:
68
+
69
+ - `backend` — копія значення `runner.backend` (один до одного, через звичайне присвоєння у літералі). Допомагає вищим шарам визначати, з яким бекендом працює runner, без розпаковки обгортки.
70
+ - `get calls()` — getter (read-only ззовні), що повертає поточне значення внутрішньої змінної `calls` (кількість зроблених викликів). Дозволяє споживачам читати лічильник, але не дає його модифікувати.
71
+ - `async runStep(prompt, stepOpts)` — обгортка над `runner.runStep`. Алгоритм:
72
+ 1. Якщо `calls >= maxApiCalls` — кидає `new BudgetExceeded('budget: вичерпано maxApiCalls=<maxApiCalls>')`. Перевірка виконується **до** інкременту, тобто `maxApiCalls=N` дозволяє рівно `N` викликів `runStep`, які успішно дійдуть до делегування у внутрішній runner.
73
+ 2. Інкрементує `calls` на 1.
74
+ 3. Викликає `log` із повідомленням виду `budget: API-виклик <calls>/<maxApiCalls>`.
75
+ 4. Повертає результат `runner.runStep(prompt, stepOpts)` (як значення промісу, бо метод `async`).
76
+
77
+ **Side effects:**
78
+
79
+ - Мутує закриту (замкнену в замиканні) змінну `calls` — інкремент на кожен валідний виклик.
80
+ - Викликає переданий `log` як побічний ефект логування (виклик відбувається **після** інкременту, тому в логах перший виклик буде `1/<maxApiCalls>`).
81
+ - Делегує виклик `runner.runStep`, що має власні side effects (мережа, файлова система тощо — залежно від реалізації runner).
82
+
83
+ **Кидає:**
84
+
85
+ - `BudgetExceeded` — коли бюджет уже вичерпано на момент чергового виклику `runStep`. У цьому випадку внутрішній `runner.runStep` **не** викликається.
86
+ - Все, що кидає `runner.runStep`, проходить наскрізь без обгортання (бо результат повертається, а не `await`-иться у tryблоку).
87
+
88
+ **Особливості реалізації:**
89
+
90
+ - Лічильник зберігається в локальній змінній `let calls`, доступ ззовні — лише через getter, що робить значення фактично read-only. Прямого сетера немає.
91
+ - `maxApiCalls ?? Number.POSITIVE_INFINITY` — використання nullish coalescing: значення `0` залишається `0` (тобто 0 викликів дозволено = одразу `BudgetExceeded`). Тільки `undefined`/`null` замінюються на `Infinity`.
92
+ - `log` за замовчуванням — функція-заглушка `() => {}`, тож виклик безпечний у будь-якому випадку.
93
+ - Перевірка `calls >= maxApiCalls` робиться перед інкрементом: для `maxApiCalls=3` третій виклик пройде (бо до інкременту `calls=2 < 3`), четвертий — впаде з `BudgetExceeded`.
94
+
95
+ ## Залежності
96
+
97
+ **Зовнішні (npm/standard):** жодних — модуль не імпортує нічого.
98
+
99
+ **Внутрішні (проєктні):** жодних — модуль самодостатній.
100
+
101
+ **Глобальні залежності runtime:**
102
+
103
+ - `Error` — стандартний клас JavaScript (базовий для `BudgetExceeded`).
104
+ - `Number.POSITIVE_INFINITY` — стандартна константа.
105
+
106
+ **Стандарт мови:** використовуються ES-modules (`export`), `class` (ES2015), `async`/`await` (ES2017), nullish coalescing `??` (ES2020), getter у літералі обʼєкта (ES5+). Файл має розширення `.mjs`, що явно вказує Node.js на ESM-режим.
107
+
108
+ ## Потік виконання / Використання
109
+
110
+ ### Загальний потік
111
+
112
+ 1. Споживач (наприклад, диспатчер у `run`) створює базовий `SubagentRunner` із власним методом `runStep`.
113
+ 2. Перед запуском автономного циклу runner обгортається: `const guarded = withBudget(runner, { maxApiCalls: 50, log: logger.info })`.
114
+ 3. У циклі дисперчера викликається `await guarded.runStep(prompt, opts)` стільки разів, скільки потрібно.
115
+ 4. Поточне споживання можна читати через `guarded.calls`.
116
+ 5. При перевищенні `maxApiCalls` наступний `runStep` кине `BudgetExceeded`, який споживач ловить і завершує сесію з відповідним статусом (§9.4).
117
+
118
+ ### Приклад використання
119
+
120
+ ```js
121
+ import { withBudget, BudgetExceeded } from './budget.mjs'
122
+
123
+ const baseRunner = {
124
+ backend: 'claude',
125
+ async runStep(prompt, opts) {
126
+ // ... виклик API
127
+ return { text: '...', tokens: 123 }
128
+ }
129
+ }
130
+
131
+ const runner = withBudget(baseRunner, {
132
+ maxApiCalls: 10,
133
+ log: msg => console.log(msg)
134
+ })
135
+
136
+ try {
137
+ for (const step of plan) {
138
+ const result = await runner.runStep(step.prompt)
139
+ // ... обробка
140
+ }
141
+ } catch (err) {
142
+ if (err instanceof BudgetExceeded) {
143
+ console.error('Зупинка: бюджет вичерпано після', runner.calls, 'викликів')
144
+ process.exit(2)
145
+ }
146
+ throw err
147
+ }
148
+ ```
149
+
150
+ ### Контракт з вищими шарами
151
+
152
+ - **`SubagentRunner`** має надавати `{ backend, runStep(prompt, opts) }`. Обгортка не вимагає інших полів; усе, що поза `backend` і `runStep`, **губиться** при обгортанні (їх немає у поверненому літералі).
153
+ - **`run`-функція (§9.4)** має ловити `BudgetExceeded` окремо від інших помилок, інакше бюджет проявиться як звичайний краш.
154
+ - **Тестування:** клас `BudgetExceeded` пустий, тож порівнювати краще через `instanceof`, а не через `err.name` чи `err.constructor.name` (хоча обидва теж працюватимуть стандартно для підкласу `Error`).
155
+
156
+ ### Граничні випадки
157
+
158
+ - `maxApiCalls: 0` — будь-який перший виклик `runStep` одразу кидає `BudgetExceeded`.
159
+ - `maxApiCalls: undefined` (опція не передана) — фактично без обмеження (Infinity).
160
+ - `maxApiCalls: Infinity` явно — те саме, без обмеження.
161
+ - `opts` не переданий — використовуються всі дефолти (`Infinity` і no-op log).
162
+ - `runner.backend === undefined` — у поверненому обʼєкті `backend` буде `undefined` (типізація показує `string`, але рантайм цьому не перешкоджає).
163
+ - `runner.runStep` синхронний — все одно повертає Promise, бо обгортка `async`.
164
+
165
+ ### Звʼязок зі spec
166
+
167
+ Файл явно посилається на **spec §9.4** ("Budget guard для автономного режиму"). Логіка цього модуля є реалізацією контракту, описаного в специфікації: лічильник API-викликів + кастомна помилка `BudgetExceeded` як механізм аварійного виходу.
@@ -0,0 +1,196 @@
1
+ # capability.mjs
2
+
3
+ ## Огляд
4
+
5
+ Модуль `capability.mjs` реалізує **Capability Router** — шар резолюції режиму оркестрації для підкоманди `flow` диспетчера `n-cursor`. Завдання модуля — відповісти на запитання: «у якому режимі (`native` чи `polyfill`) виконувати flow для оголошеної моделі LLM?».
6
+
7
+ Ключові архітектурні рішення модуля:
8
+
9
+ - **Жодної рантайм-детекції моделі.** Модель не вгадується з оточення/процесу — її потрібно **явно оголосити** (CLI-прапорець, env, config). Це відповідає вимозі spec §2.2 проєкту.
10
+ - **Чисті функції без I/O.** Усі вхідні джерела (`args`, `env`, `config`, `matrix`, `hasRunner`) приходять параметрами ззовні. Модуль не читає файлову систему, не звертається до мережі та не використовує `process.*` напряму. Завдяки цьому функції тривіально тестуються без моків.
11
+ - **Розділення відповідальності.** Сам модуль лише **резолвить** режим і повідомляє про можливість запуску `polyfill`. Власне кидання помилок (`fail`) та інтеграція з runner-ом виконуються caller-ом — `polyfill` без доступного `SubagentRunner` (§15.1) не може стартувати, але рішення про помилку приймається вище за стеком.
12
+
13
+ Модуль використовується як read-only утиліта диспетчером: спочатку парситься CLI-прапорець `--model`, потім збирається оголошена модель за пріоритетом, далі береться режим оркестрації з `capability-matrix`, і нарешті перевіряється, чи доступний `SubagentRunner` для polyfill-шляху.
14
+
15
+ ## Експорти / API
16
+
17
+ | Експорт | Тип | Опис |
18
+ | --------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
19
+ | `DEFAULT_ORCHESTRATION` | константа: `string` (`'polyfill'`) | Дефолтний режим оркестрації, що повертається, коли в `matrix` немає інформації для моделі і не задано `matrix.default.orchestration`. |
20
+ | `parseModelFlag(args)` | функція | Витягує значення прапорця `--model <value>` з масиву argv. |
21
+ | `declaredModel(sources)` | функція | Повертає оголошену модель за пріоритетом CLI > env > config. |
22
+ | `orchestrationFor(model, matrix)` | функція | Резолвить режим оркестрації (`'native' | 'polyfill'`) для оголошеної моделі за матрицею. |
23
+ | `polyfillStartable(ctx)` | функція | Перевіряє, чи доступний `SubagentRunner`, необхідний для старту polyfill-режиму. |
24
+
25
+ Усі експорти — `named exports`; `default export` відсутній.
26
+
27
+ ## Функції
28
+
29
+ ### `parseModelFlag(args)`
30
+
31
+ **Сигнатура:** `parseModelFlag(args: string[]): string | null`
32
+
33
+ **Параметри:**
34
+
35
+ - `args` — масив рядків, що представляє argv підкоманди `flow` (без імені виконуваного файлу). Зазвичай це частина `process.argv`, передана у диспетчер.
36
+
37
+ **Повертає:**
38
+
39
+ - `string` — значення, що йде безпосередньо за токеном `--model` в argv.
40
+ - `null` — якщо токен `--model` відсутній або є останнім елементом масиву (тобто значення за ним немає).
41
+
42
+ **Алгоритм:**
43
+
44
+ 1. Знайти індекс першого входження рядка `'--model'` через `Array.prototype.indexOf`.
45
+ 2. Якщо індекс не `-1` **і** наступний елемент існує (`i + 1 < args.length`) — повернути `args[i + 1]`.
46
+ 3. Інакше — повернути `null`.
47
+
48
+ **Side effects:** жодних. Вхідний масив не мутується, лише читається.
49
+
50
+ **Зауваги:**
51
+
52
+ - Розпізнається лише форма `--model <value>` через пробіл. Форма `--model=value` цією функцією **не** підтримується.
53
+ - Враховується лише перше входження `--model` (наслідок `indexOf`).
54
+ - Якщо аргумент після `--model` сам є прапорцем (наприклад, `--model --foo`), він однаково буде повернутий — валідація формату значення не виконується.
55
+
56
+ ### `declaredModel(sources)`
57
+
58
+ **Сигнатура:** `declaredModel(sources?: { cliModel?: string | null, envModel?: string | null, configModel?: string | null }): string | null`
59
+
60
+ **Параметри (об’єкт-деструктуризація з дефолтами `null`):**
61
+
62
+ - `cliModel` — модель, отримана з CLI (зазвичай результат `parseModelFlag`). Найвищий пріоритет.
63
+ - `envModel` — модель з env-змінної (за конвенцією проєкту — `N_CURSOR_FLOW_MODEL`). Середній пріоритет.
64
+ - `configModel` — модель з конфігураційного файла (ключ `flow.model`). Найнижчий пріоритет.
65
+
66
+ Усі три поля **опціональні**; виклик без аргументів (`declaredModel()`) валідний завдяки дефолту `= {}`.
67
+
68
+ **Повертає:**
69
+
70
+ - `string` — перше істинне (truthy) значення серед `cliModel`, `envModel`, `configModel` у вказаному порядку.
71
+ - `null` — якщо всі три джерела falsy (`null`, `undefined`, `''`).
72
+
73
+ **Алгоритм:** короткозамкнений ланцюг `cliModel || envModel || configModel || null`.
74
+
75
+ **Side effects:** жодних.
76
+
77
+ **Зауваги:** оскільки використовується `||`, **порожній рядок** `''` трактується як «не оголошено» і пропускається — це узгоджується з намірами модуля (модель повинна бути не лише визначена, а й непорожня).
78
+
79
+ ### `orchestrationFor(model, matrix)`
80
+
81
+ **Сигнатура:** `orchestrationFor(model: string | null, matrix: { models?: Record<string, { orchestration?: string }>, default?: { orchestration?: string } }): 'native' | 'polyfill'`
82
+
83
+ **Параметри:**
84
+
85
+ - `model` — оголошена модель (зазвичай результат `declaredModel`). Може бути `null`.
86
+ - `matrix` — capability-matrix:
87
+ - `matrix.models` — мапа `модель → { orchestration }` з режимом для конкретних моделей.
88
+ - `matrix.default.orchestration` — фолбек-режим для невідомих/неоголошених моделей.
89
+
90
+ **Повертає:** літеральний рядок `'native'` або `'polyfill'` (типи з JSDoc; у реальності повертається будь-яке рядкове значення, прочитане з матриці, але інваріант протоколу — саме ці два).
91
+
92
+ **Алгоритм каскадного фолбеку:**
93
+
94
+ 1. Якщо `model` truthy **і** є `matrix.models` — узяти `entry = matrix.models[model]`; інакше `entry = null`.
95
+ 2. Повернути перше істинне з трьох:
96
+ - `entry?.orchestration` — режим, прописаний для конкретної моделі;
97
+ - `matrix?.default?.orchestration` — дефолт із самої матриці;
98
+ - `DEFAULT_ORCHESTRATION` — глобальний дефолт модуля (`'polyfill'`).
99
+
100
+ **Side effects:** жодних. Матриця читається ad hoc без копіювання.
101
+
102
+ **Зауваги:**
103
+
104
+ - Функція стійка до `null`/`undefined` як `matrix`, так і `matrix.models`/`matrix.default` завдяки явним перевіркам та операторам `&&`.
105
+ - Невідома модель (відсутня в `matrix.models`) автоматично потрапляє в гілку `matrix.default` → `DEFAULT_ORCHESTRATION`. Це означає, що нові/незареєстровані моделі за замовчуванням підуть через `polyfill` (за наявності runner-а).
106
+
107
+ ### `polyfillStartable(ctx)`
108
+
109
+ **Сигнатура:** `polyfillStartable(ctx: { hasRunner: boolean }): boolean`
110
+
111
+ **Параметри:**
112
+
113
+ - `ctx.hasRunner` — прапорець наявності `SubagentRunner` у середовищі (відповідно до §15.1 spec). Тип повинен бути саме `boolean`.
114
+
115
+ **Повертає:**
116
+
117
+ - `true` — якщо `ctx.hasRunner === true` (strict equality).
118
+ - `false` — у будь-якому іншому випадку (`false`, `undefined`, truthy-не-`true`, тощо).
119
+
120
+ **Алгоритм:** одна перевірка `hasRunner === true`.
121
+
122
+ **Side effects:** жодних.
123
+
124
+ **Зауваги:** strict-перевірка свідома — модуль не приймає «приблизно правда» значення (`1`, `'yes'`, об’єкти runner-а тощо). Це змушує caller-а передавати дискретний boolean-прапорець, що дисциплінує контракт.
125
+
126
+ ## Залежності
127
+
128
+ **Імпорти:** жодних. Модуль **самодостатній** — не залежить ні від npm-пакетів, ні від інших файлів проєкту.
129
+
130
+ **Глобали / середовище:** не використовуються. Зокрема, не читається `process.argv`, `process.env`, файлова система, мережа.
131
+
132
+ **Споживачі (caller-и):** модуль очікувано викликається з реалізації підкоманди `flow` диспетчера (`npm/scripts/dispatcher/...`), яка:
133
+
134
+ 1. збирає `args` з `process.argv`;
135
+ 2. підставляє `process.env.N_CURSOR_FLOW_MODEL` як `envModel`;
136
+ 3. читає `flow.model` з config-файла як `configModel`;
137
+ 4. вантажить `capability-matrix` (ймовірно, із статичного JSON/JS);
138
+ 5. визначає `hasRunner` за наявністю `SubagentRunner` у рантаймі;
139
+ 6. за результатом `orchestrationFor` + `polyfillStartable` або стартує flow, або кидає помилку.
140
+
141
+ ## Потік виконання / Використання
142
+
143
+ Типовий ланцюг викликів caller-а:
144
+
145
+ ```js
146
+ import {
147
+ parseModelFlag,
148
+ declaredModel,
149
+ orchestrationFor,
150
+ polyfillStartable,
151
+ DEFAULT_ORCHESTRATION
152
+ } from './capability.mjs'
153
+
154
+ // 1. CLI-парсинг
155
+ const cliModel = parseModelFlag(args)
156
+
157
+ // 2. Резолюція оголошеної моделі за пріоритетом
158
+ const model = declaredModel({
159
+ cliModel,
160
+ envModel: process.env.N_CURSOR_FLOW_MODEL ?? null,
161
+ configModel: config?.flow?.model ?? null
162
+ })
163
+
164
+ // 3. Режим оркестрації за матрицею
165
+ const mode = orchestrationFor(model, capabilityMatrix)
166
+
167
+ // 4. Перевірка можливості старту polyfill
168
+ if (mode === 'polyfill' && !polyfillStartable({ hasRunner })) {
169
+ throw new Error('polyfill requires SubagentRunner (spec §15.1)')
170
+ }
171
+
172
+ // 5. Старт flow у режимі mode
173
+ startFlow({ mode, model })
174
+ ```
175
+
176
+ **Інваріанти потоку:**
177
+
178
+ - Якщо модель **не оголошена** в жодному з трьох джерел, `declaredModel` повертає `null`. `orchestrationFor(null, matrix)` пропустить `matrix.models` і впаде на `matrix.default` або `DEFAULT_ORCHESTRATION = 'polyfill'`. Тобто за замовчуванням flow без оголошеної моделі піде через `polyfill` — і вимагатиме `SubagentRunner`.
179
+ - Caller відповідає за **fail-fast** у разі `mode === 'polyfill' && !hasRunner`. Сам модуль помилок не кидає.
180
+ - `DEFAULT_ORCHESTRATION = 'polyfill'` означає: «дефолт — мати polyfill доступним»; це сумісно з ідеєю, що `polyfill` має «працювати з будь-якою моделлю» **лише** за наявності runner-а.
181
+
182
+ **Таблиця рішень `orchestrationFor`:**
183
+
184
+ | `model` | `matrix.models[model]` | `matrix.default.orchestration` | Результат |
185
+ | ---------------- | ------------------------------- | ------------------------------ | -------------------------------------- |
186
+ | `'modelA'` | `{ orchestration: 'native' }` | будь-що | `'native'` |
187
+ | `'modelB'` | `{ orchestration: 'polyfill' }` | будь-що | `'polyfill'` |
188
+ | `'unknownModel'` | відсутній | `'native'` | `'native'` |
189
+ | `'unknownModel'` | відсутній | `'polyfill'` | `'polyfill'` |
190
+ | `'unknownModel'` | відсутній | відсутній | `DEFAULT_ORCHESTRATION` (`'polyfill'`) |
191
+ | `null` | (не дивимось) | `'native'` | `'native'` |
192
+ | `null` | (не дивимось) | відсутній | `DEFAULT_ORCHESTRATION` (`'polyfill'`) |
193
+
194
+ ## Rebuild Test
195
+
196
+ На основі цієї документації модуль `capability.mjs` можна повністю відтворити: він складається з однієї константи `DEFAULT_ORCHESTRATION = 'polyfill'` та чотирьох чистих експортних функцій (`parseModelFlag`, `declaredModel`, `orchestrationFor`, `polyfillStartable`) з описаними вище сигнатурами, алгоритмами та інваріантами; жодних імпортів, жодного I/O, жодного default-export — тільки named exports.
@@ -0,0 +1,210 @@
1
+ # commands.mjs
2
+
3
+ ## Огляд
4
+
5
+ Модуль `commands.mjs` реалізує handler-и підкоманд CLI `n-cursor flow` згідно зі специфікацією §8 (Пасивний Турнікет / Flow). Він є диспетчерською точкою для Фази Ф2 робочого потоку — підкоманд `init`, `verify` та `release`, — а також надає допоміжні утиліти: реальний sync-runner `realRun`, гарантію наявності worktree `ensureWorktree` та інференс воркспейсу за зміненими файлами `matchChangedWorkspaces`.
6
+
7
+ Усі побічні ефекти (виконання процесів, логування, обчислення fingerprint, час) реалізовані як ін'єктовані залежності в `deps`, тож логіку модуля можна тестувати без реальних `git`, `npx` чи годинника. Це частина архітектури «командна логіка + чистий ядро». Підкоманди `run` / `resume` / `cancel` / `repair` зі специфікації належать Фазі Ф4 і в цьому файлі ще не реалізовані.
8
+
9
+ Модуль є ESM (`.mjs`), використовує імпорти Node.js (`node:child_process`, `node:path`, `node:process`) і не має станового глобалу — стан тримається у JSON-файлах `.flow.json` (через `state-store.mjs`).
10
+
11
+ ## Експорти / API
12
+
13
+ | Експорт | Тип | Призначення |
14
+ | ----------------------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------- |
15
+ | `realRun(cmd, args, opts?)` | `function` | Реальний sync-обгортка над `spawnSync` із захопленням stdout/stderr. |
16
+ | `ensureWorktree(rest, deps?)` | `function` | Парсить аргументи `<branch> "<опис>"`, гарантує worktree (детектить існуючу ізоляцію або створює новий), повертає метадані. |
17
+ | `init(rest, deps?)` | `async function` | Handler `flow init`: ізоляція + первинна ініціалізація `.flow.json`. |
18
+ | `verify(_rest, deps?)` | `async function` | Handler `flow verify`: Quality Gates («Суддя») у поточному worktree з толерантністю до відсутності стану. |
19
+ | `matchChangedWorkspaces(subWorkspaces, changedFiles)` | `function` | Чистий хелпер: підмножина воркспейсів, у яких є зміни (з прив'язкою до найглибшого збігу). |
20
+ | `release(rest, deps?)` | `async function` | Handler `flow release`: генерує `.changes` через `n-cursor change` і пише completion snapshot. |
21
+
22
+ Усі async handler-и повертають Promise<number> — exit code (0 — ок, 1 — помилка). Решта — синхронні.
23
+
24
+ ## Функції
25
+
26
+ ### `realRun(cmd, args, opts = {})`
27
+
28
+ - Сигнатура: `(cmd: string, args: string[], opts?: object) => { status: number, stdout: string, stderr: string }`.
29
+ - Параметри:
30
+ - `cmd` — назва/шлях виконуваного файлу.
31
+ - `args` — масив аргументів.
32
+ - `opts` — додаткові опції для `spawnSync` (наприклад, `cwd`). Завжди примусово `encoding: 'utf8'`.
33
+ - Повертає об'єкт із полями `status` (1, якщо `spawnSync` повернув `null` через сигнал/помилку запуску), `stdout`, `stderr`.
34
+ - Side effects: синхронно стартує процес ОС.
35
+
36
+ ### `inLinkedWorktree(run, cwd)` (внутрішня)
37
+
38
+ - Сигнатура: `(run, cwd: string) => boolean`.
39
+ - Логіка: викликає `git rev-parse --git-dir`, `--git-common-dir`, `--show-superproject-working-tree`. Worktree вважається «лінкованим», якщо обидва git-dir команди повернули код 0, це **не** submodule, і `git-dir !== git-common-dir`.
40
+ - Повертає `true`, якщо процес виконується всередині linked worktree (не основного checkout і не submodule). Це дозволяє `ensureWorktree` не створювати вкладений worktree.
41
+ - Side effects: три `git rev-parse` через переданий `run`.
42
+
43
+ ### `ensureWorktree(rest, deps = {})`
44
+
45
+ - Сигнатура: `(rest: string[], deps?) => { code: number, worktreeDir?: string, branch?: string, desc?: string, baseCommit?: string | null }`.
46
+ - Параметри:
47
+ - `rest` — `[branch, ...descWords]`. Опис склеюється пробілом, `trim`-иться.
48
+ - `deps.run` — runner (default `realRun`).
49
+ - `deps.cwd` — стартовий каталог (default `process.cwd()`).
50
+ - `deps.log` — функція логування (default `console.error`).
51
+ - Поведінка:
52
+ 1. Якщо `branch` або `desc` порожні — логує usage і повертає `{ code: 1 }`.
53
+ 2. Якщо вже в linked worktree — використовує поточний `cwd` як `worktreeDir`, логує підказку.
54
+ 3. Інакше — викликає `npx @nitra/cursor worktree add <branch> <desc>`; на помилку повертає `{ code: 1 }` із поясненням зі `stderr`.
55
+ 4. Дізнається `HEAD` у `worktreeDir`. Якщо `git rev-parse HEAD` успішний — це `baseCommit`, інакше `null`.
56
+ - Side effects: можливе створення worktree через зовнішній CLI, git-виклики.
57
+
58
+ ### `init(rest, deps = {})`
59
+
60
+ - Сигнатура: `(rest: string[], deps?) => Promise<number>`.
61
+ - Параметри:
62
+ - `rest` — `[branch, ...descWords]`.
63
+ - `deps.now` — джерело часу (default `Date.now`); решта успадковуються `ensureWorktree`.
64
+ - Кроки:
65
+ 1. Делегує `ensureWorktree`; ранній exit при `code !== 0`.
66
+ 2. Шлях стану: `flowStatePath(worktreeDir)` (з `state-store.mjs`).
67
+ 3. Визначає `level` та `risk` через `detectLevel(desc)` / `detectRisk(desc)` (з `level.mjs`).
68
+ 4. Через `writeState` записує початковий запис: `branch`, `status: 'in_progress'`, `started_at` (ISO від `now()`), `metadata.base_commit`, `level`, `risk`, порожній `plan: []`.
69
+ 5. Логує підсумок і повертає `0`.
70
+ - Side effects: створення/перезапис `.flow.json`.
71
+
72
+ ### `verify(_rest, deps = {})`
73
+
74
+ - Сигнатура: `(_rest: string[], deps?) => Promise<number>`. `_rest` не використовується.
75
+ - Параметри `deps`:
76
+ - `run`, `cwd`, `log` (стандартні).
77
+ - `branch` — опціональний явний фільтр для резолва активного flow.
78
+ - `fingerprint` — фабрика fingerprint-функції; default — `worktreeFingerprint` із `cwd`-залежним sync-runner-ом.
79
+ - Кроки:
80
+ 1. `resolveActiveFlowState({ cwd, branch }, deps)` — cwd-незалежний пошук активного `.flow.json`. Якщо знайшли autoResolved — логує лейбл.
81
+ 2. Якщо `branch` явно задано і не резолвиться — це помилка наміру: повертає `1` із поясненням (інакше `flow verify --branch typo` міг би «зеленіти» в CI).
82
+ 3. Робочий `cwd` для gate-ів: `resolved.worktreeDir ?? cwd0`.
83
+ 4. Читає стан; якщо нема (відсутній/пошкоджений `.flow.json`) — verify лишається толерантним: гейти прогоняються standalone, без запису стану. Логує warn із описом причини.
84
+ 5. Якщо стан є, але `plan` порожній — лише warning (м'які ворота).
85
+ 6. Викликає `runReview({ run, cwd, fingerprint })` (з `reviewer.mjs`). Отримує `{ pass, gates, fingerprint, failedOutput }`.
86
+ 7. Для кожного gate логує `✅`/`❌`. На фейл — `failedOutput`.
87
+ 8. Якщо стан був — `recordTransition` записує подію `{ type: 'verify', pass }` і оновлює `gates`, `fingerprint`, `status` (`failed` при провалі, інакше зберігає попередній).
88
+ 9. Повертає `0` / `1` залежно від `verdict.pass`.
89
+ - Side effects: gate-команди (lint/test/тощо) у `cwd`, лог-вивід, можливі `.flow.json` + події.
90
+
91
+ ### `matchChangedWorkspaces(subWorkspaces, changedFiles)`
92
+
93
+ - Сигнатура: `(subWorkspaces: string[], changedFiles: string[]) => string[]`.
94
+ - Параметри:
95
+ - `subWorkspaces` — теки воркспейсів **без** кореня (`.`).
96
+ - `changedFiles` — змінені шляхи відносно кореня репозиторію у posix-форматі.
97
+ - Логіка: сортує воркспейси за довжиною (спадно), для кожного зміненого файла знаходить **найглибший** збіг (`f === w || f.startsWith(w + '/')`). Таке правило усуває хибне `«кілька воркспейсів»` для випадку, коли `apps` і `apps/web` обидва зареєстровані, а файл `apps/web/x` має належати лише найглибшому.
98
+ - Повертає підмножину `subWorkspaces` (у вхідному порядку), які отримали хоч один хіт.
99
+ - Side effects: немає (чиста функція).
100
+
101
+ ### `resolveChangeWsArgs({ rest, baseCommit, cwd, listWorkspaces, changedFilesSince, log })` (внутрішня)
102
+
103
+ - Сигнатура: `(input) => Promise<{ args: string[], error?: boolean }>`.
104
+ - Призначення: добудовує `--ws <шлях>` до аргументів `change`, якщо користувач не задав явно.
105
+ - Кроки:
106
+ 1. Якщо `rest` уже містить `--ws` або `--ws=...` — повертає `rest` без змін (поважає явний намір).
107
+ 2. `listWorkspaces(cwd)` → масив. Відсіює корінь (`.`); якщо subworkspace-ів нема — `change` дефолтиться на `.`, лишаємо як є.
108
+ 3. `hits = matchChangedWorkspaces(subWs, changedFilesSince(baseCommit, cwd))`.
109
+ 4. `hits.length > 1` → fail-hard: `{ args: rest, error: true }`, лог із переліком.
110
+ 5. `hits.length === 1` → додає `--ws <hits[0]>` і логує інференс.
111
+ 6. `hits.length === 0` → лишає `rest`.
112
+ 7. У будь-якому виключенні від `listWorkspaces` / `changedFilesSince` — fail-soft: лог warning, повертає `rest`.
113
+ - Side effects: `git diff`-подібні виклики через `changedFilesSince`, виклик `listWorkspaces`, логування.
114
+
115
+ ### `release(rest, deps = {})`
116
+
117
+ - Сигнатура: `(rest: string[], deps?) => Promise<number>`.
118
+ - Параметри `deps`:
119
+ - `run`, `cwd`, `log`, `now` — стандартні.
120
+ - `branch` — опціональний фільтр активного flow.
121
+ - `listWorkspaces` — default `getMonorepoProjectRootDirs` (з `rules/changelog/lib/package-manifest.mjs`).
122
+ - `changedFilesSince` — default `collectChangedFilesSince`.
123
+ - Кроки:
124
+ 1. `resolveActiveFlowState({ cwd, branch }, deps)`. Якщо `statePath` не знайдено — `release` падає (`code 1`) із поясненням (це обов'язкова прив'язка).
125
+ 2. `effectiveCwd = resolved.worktreeDir ?? cwd`.
126
+ 3. `readState(statePath)`. Якщо стану нема — `release: стану нема — спершу 'flow init'`, `1`.
127
+ 4. Якщо `state.gate?.verdict === 'FAIL'` — лише warning (м'які ворота, рішення за людиною).
128
+ 5. `resolveChangeWsArgs(...)`. Якщо повернув `error: true` (кілька воркспейсів) — exit `1`.
129
+ 6. Викликає `npx @nitra/cursor change <args>` у `effectiveCwd`. Помилка → exit `1` із поясненням зі stderr.
130
+ 7. `buildCompletionSnapshot({ ...state, status: 'done' }, now)` — снапшот завершення.
131
+ 8. `recordTransition` записує подію `{ type: 'release' }`, оновлює стан: `status: 'done'`, `completion: snapshot`.
132
+ 9. Якщо в стані вказано `state.task` (шлях task-record) — пише summary у task через `writeSummaryToTaskRecord` (з абсолютизацією через `join(effectiveCwd, …)`, якщо шлях відносний).
133
+ 10. Логує `release: done`, повертає `0`.
134
+ - Side effects: `npx … change`, запис у `.flow.json`, можливий запис у task-record, події.
135
+
136
+ ## Залежності
137
+
138
+ ### Імпорти Node-стандарту
139
+
140
+ - `node:child_process` → `spawnSync` (для `realRun` і дефолтного `fingerprint`).
141
+ - `node:path` → `isAbsolute`, `join` (для шляху task-record у `release`).
142
+ - `node:process` → `cwd as processCwd` (default `cwd`).
143
+
144
+ ### Внутрішні модулі проєкту
145
+
146
+ - `../../lib/worktree.mjs` → `worktreePaths` — резолв шляху checkout після `worktree add`.
147
+ - `../../lib/changed-files.mjs` → `collectChangedFilesSince` — default для `changedFilesSince` у `release`.
148
+ - `../../utils/worktree-fingerprint.mjs` → `worktreeFingerprint` — default для `verify`.
149
+ - `../../../rules/changelog/lib/package-manifest.mjs` → `getMonorepoProjectRootDirs` — default для `listWorkspaces`.
150
+ - `./events.mjs` → `flowEventsPath` — шлях файла подій flow.
151
+ - `./level.mjs` → `detectLevel`, `detectRisk` — класифікація задачі на основі опису.
152
+ - `./reviewer.mjs` → `runReview` — Quality Gates («Суддя»).
153
+ - `./snapshot.mjs` → `buildCompletionSnapshot`, `writeSummaryToTaskRecord` — completion-снапшот для `release`.
154
+ - `./state-store.mjs` → `flowStatePath`, `readState`, `recordTransition`, `writeState` — персистенція `.flow.json` + події.
155
+ - `./flow-resolve.mjs` → `resolveActiveFlowState` — cwd-незалежний резолв активного flow.
156
+
157
+ ### Зовнішні CLI (через `run`)
158
+
159
+ - `npx @nitra/cursor worktree add <branch> <desc>` — створення worktree (`ensureWorktree`).
160
+ - `npx @nitra/cursor change <args>` — генерація `.changes` (`release`).
161
+ - `git rev-parse --git-dir|--git-common-dir|--show-superproject-working-tree|HEAD` — детекція worktree та base-коміт.
162
+
163
+ ## Потік виконання / Використання
164
+
165
+ ### Типовий happy-path Ф2
166
+
167
+ 1. `n-cursor flow init feature/x "опис задачі"` → `init`:
168
+ - `ensureWorktree` створює (або підхоплює) worktree.
169
+ - `detectLevel` / `detectRisk` класифікують задачу.
170
+ - Створюється `.flow.json` зі `status: 'in_progress'`, фіксується `base_commit`.
171
+ 2. Робота над кодом всередині worktree.
172
+ 3. `n-cursor flow verify` → `verify`:
173
+ - Резолвиться активний flow (cwd-незалежно).
174
+ - `runReview` прогоняє gate-и (lint, тести, тощо).
175
+ - Стан оновлюється: `gates`, `fingerprint`. На фейл — `status: 'failed'`.
176
+ 4. `n-cursor flow release --bump minor --section feat --message "…"` → `release`:
177
+ - Резолв активного flow (обов'язковий).
178
+ - Інференс `--ws` із diff від `base_commit` (якщо не задано явно).
179
+ - `npx @nitra/cursor change …` пише `.changes`.
180
+ - `buildCompletionSnapshot` + `recordTransition` фіксують `status: 'done'`.
181
+ - Якщо є `state.task` — summary йде у task-record.
182
+
183
+ ### Точки розширення (через `deps`)
184
+
185
+ - Юніт-тести підставляють фейкові `run`, `log`, `now`, `fingerprint`, `listWorkspaces`, `changedFilesSince`, `branch`, `cwd`.
186
+ - Це дозволяє покрити edge-кейси: відсутній стан, кілька воркспейсів у diff, помилки `npx`, артефакти `git rev-parse`.
187
+
188
+ ### Контракти CLI
189
+
190
+ - `init` / `release` потребують `<branch> "<опис>"` (init) та активного flow + опційних `--bump|--section|--message|--ws` (release).
191
+ - `verify` — без обов'язкових аргументів; толерантний до відсутності стану (запуск standalone у поточному `cwd`).
192
+ - `--branch <name>` як `deps.branch` дозволяє адресувати конкретний flow (CI/довільний `cwd`).
193
+
194
+ ### Інваріанти
195
+
196
+ - Усі handler-и **не змінюють код** проєкту (`verify` read-only, `release` лише пише `.changes` / `.flow.json` / task-summary).
197
+ - Worktree-вкладеність заборонена: `ensureWorktree` детектить, що `cwd` уже linked worktree, і повторно `worktree add` не викликає.
198
+ - М'які ворота: і відсутність `plan`, і `gate.verdict === 'FAIL'` дають лише warning, рішення про реліз — за людиною.
199
+ - Fail-hard у `release`: відсутність активного flow та неоднозначний воркспейс (multi-hit `matchChangedWorkspaces`).
200
+
201
+ ## Rebuild Test
202
+
203
+ Маючи цю документацію, інженер може відтворити публічний контракт модуля без читання вихідного коду:
204
+
205
+ - Знає список і сигнатури експортованих функцій (`realRun`, `ensureWorktree`, `init`, `verify`, `matchChangedWorkspaces`, `release`) та їхні exit code.
206
+ - Розуміє роль `deps` як набору ін'єкцій (`run`, `cwd`, `log`, `now`, `fingerprint`, `branch`, `listWorkspaces`, `changedFilesSince`).
207
+ - Може повторити доменну поведінку: детекцію linked worktree, інференс `--ws` (single/multi/empty/explicit), толерантність `verify` до відсутнього стану, fail-hard `release`, м'які ворота на `plan`/`gate.verdict`.
208
+ - Знає форму `.flow.json` (поля `branch`, `status`, `started_at`, `metadata.base_commit`, `level`, `risk`, `plan`, `gates`, `fingerprint`, `completion`, `task`) і які підкоманди їх записують.
209
+ - Бачить зовнішні CLI/git-залежності та внутрішні модулі, від яких залежить логіка.
210
+ - Розуміє, які підкоманди (`run`/`resume`/`cancel`/`repair`) ще не реалізовані тут (Ф4).