@nitra/cursor 1.41.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/bin/n-cursor.js +17 -1
- package/package.json +1 -1
- package/rules/flow/fix.mjs +18 -0
- package/rules/flow/flow.mdc +51 -0
- package/rules/flow/meta.json +1 -0
- package/rules/js-bun-db/js-bun-db.mdc +20 -8
- package/rules/js-lint/js/data/tooling/oxlint-canonical.json +1 -1
- package/scripts/dispatcher/index.mjs +48 -0
- package/scripts/dispatcher/lib/active.mjs +226 -0
- package/scripts/dispatcher/lib/budget.mjs +36 -0
- package/scripts/dispatcher/lib/capability.mjs +81 -0
- package/scripts/dispatcher/lib/commands.mjs +193 -0
- package/scripts/dispatcher/lib/events.mjs +67 -0
- package/scripts/dispatcher/lib/executor.mjs +102 -0
- package/scripts/dispatcher/lib/flow-lock.mjs +39 -0
- package/scripts/dispatcher/lib/planner.mjs +66 -0
- package/scripts/dispatcher/lib/reviewer.mjs +38 -0
- package/scripts/dispatcher/lib/snapshot.mjs +58 -0
- package/scripts/dispatcher/lib/state-store.mjs +173 -0
- package/scripts/dispatcher/lib/subagent-runner.mjs +120 -0
- package/scripts/dispatcher/trace.mjs +114 -0
- package/scripts/utils/with-lock.mjs +7 -3
- package/scripts/worktree-cli.mjs +12 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.0.0] - 2026-05-31
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- n-cursor flow (v2.0-a Ф0-Ф1.1): каркас dispatcher + CLI `case 'flow'` (init/verify/release/run/resume/cancel/repair — поки stub-и), Capability Router з явною декларацією моделі (native/polyfill, default→polyfill лише за наявного runner-а), crash-safe state-store (.flow.json sibling, atomic temp+fsync+rename, fail-closed на corruption). 29 unit-тестів (withTmpDir).
|
|
8
|
+
- n-cursor flow v2.0-a Ф1.2-Ф1.4: WAL-журнал .events.jsonl (append-only, торований останній рядок толерується), per-branch lock через reuse withLock із fail-closed override (додано опцію onWaitTimeout у спільний with-lock, back-compat), cleanupFlowSiblings (.flow.json/.events.jsonl/lock). recordTransition (WAL: подія до зміни статусу). +22 тести.
|
|
9
|
+
- n-cursor flow v2.0-a Ф2 (verify): reviewer.mjs — Level-1 «Суддя» проганяє lint+coverage gates через ін'єктований runner (fail-fast, fingerprint на повному pass через reuse worktree-fingerprint); flow verify — Пасивний Турнікет: запускає gates у поточному worktree, записує результати+fingerprint у наявний стан (recordTransition), exit 0/1. +11 тестів.
|
|
10
|
+
- n-cursor flow v2.0-a Ф2·2: flow init (n-cursor worktree add + detect-existing-isolation через git rev-parse, init .flow.json з base_commit; reuse worktreePaths/sanitizeBranch) + flow release (n-cursor change + completion snapshot у стан і task record) + snapshot.mjs (buildCompletionSnapshot/upsertSummaryBlock/writeSummaryToTaskRecord). reviewer тепер захоплює вивід проваленого gate. Пасивний Турнікет (init/verify/release) завершено. +18 тестів.
|
|
11
|
+
- n-cursor flow Ф2·4: bundled-правило flow (матеріалізується як .cursor/rules/n-flow.mdc) — контракт Пасивного Турнікета для IDE-агентів (Cursor/Claude Code): init -> сам пишеш код (TDD) -> verify (3 спроби на фейл) -> release. alwaysApply; pure-doc + стандартний fix.mjs (runStandardRule). Авто-дискавериться, активується при sync. Завершує Фасад A повністю.
|
|
12
|
+
- n-cursor flow v2.0-a Ф3 (двигун-блоки): SubagentRunner (§15.1) — selectBackend (sdk>claude>cursor за ANTHROPIC_API_KEY/PATH, fail коли нічого нема), cliRunner (claude/cursor-agent -p, CLI-auth), sdkRunner (claude-agent-sdk, dynamic import); planner — parsePlan (валідація+нормалізація, fail-closed на невалідному) + generatePlan. Усе з мок-ін'єкцією (нуль реальних API/SDK у тестах). +24 тести.
|
|
13
|
+
- n-cursor flow v2.0-a Ф4·a: executor.mjs — серце Активного Раннера. Виконує план покроково: мікропромпт зі стану (не історія) -> спавн субагента -> verify -> commit ЛИШЕ після зеленого (commit-інваріант §4.1.7) -> repair (per-step retry) -> на вичерпанні HITL (blocked-on-human + structured question). microprompt/patchStep — pure. Усе з ін'єкцією runner/verify/commit (нуль реальних LLM/git у тестах). +6 тестів.
|
|
14
|
+
- n-cursor flow v2.0-a Ф4·b: Активний Раннер end-to-end. flow run (ensureWorktree -> planner -> executor; exit 0 done / 1 fail / 2 blocked-on-human), flow resume (safe-resume git reset + HITL-відповіді як hint + свіжі спроби), flow cancel (cleanup sibling-ів), flow repair (--discard-step-work / діагностика). ensureWorktree витягнуто як спільне для init/run. Усі 7 підкоманд реальні. +12 тестів (103 у dispatcher). v2.0-a (Фасади A+B) функціонально завершено.
|
|
15
|
+
- n-cursor flow v2.0-a: (1) budget guard для flow run --autonomous (withBudget обгортає runner, BudgetExceeded при перевищенні maxApiCalls з .n-cursor.json flow.autonomous; на abort -> status failed, exit 1). (2) Ф1.4 борг закрито: worktree remove тепер reuse-кличе cleanupFlowSiblings (flow-sibling-и не осиротіють). Заодно виправлено передіснуючі switch-case-braces у worktree-cli. +4 тести.
|
|
16
|
+
- n-cursor flow v2.0-b: команда n-cursor trace — наскрізна простежуваність (§5.4/§7). Читає front-matter артефактів у docs/{tasks,specs,plans,adr}, будує ланцюг за лінками adr/spec/plan/change/task, флагує розриви (лінк на неіснуючий файл) з exit 1; --json для machine-readable. parseFrontMatter/analyze/render — pure, FS ін'єктовний. Підтверджено на власних spec<->plan. +10 тестів.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- n-cursor flow v2.0.0 (major): Dual-Mode Dispatcher — Пасивний Турнікет (flow init/verify/release) + Активний Раннер (flow run/resume/cancel/repair) + n-cursor trace + docs/{specs,plans} міграція
|
|
21
|
+
|
|
3
22
|
## [1.41.0] - 2026-05-31
|
|
4
23
|
|
|
5
24
|
### Changed
|
package/bin/n-cursor.js
CHANGED
|
@@ -1534,6 +1534,22 @@ try {
|
|
|
1534
1534
|
|
|
1535
1535
|
break
|
|
1536
1536
|
}
|
|
1537
|
+
case 'flow': {
|
|
1538
|
+
// n-cursor flow — Dual-Mode Dispatcher (spec §8): Пасивний Турнікет
|
|
1539
|
+
// (init/verify/release) + Активний Раннер (run) навколо .flow.json.
|
|
1540
|
+
const { runFlowCli } = await import('../scripts/dispatcher/index.mjs')
|
|
1541
|
+
process.exitCode = await runFlowCli(args)
|
|
1542
|
+
|
|
1543
|
+
break
|
|
1544
|
+
}
|
|
1545
|
+
case 'trace': {
|
|
1546
|
+
// n-cursor trace — наскрізна простежуваність (spec §5.4/§7): граф
|
|
1547
|
+
// ADR↔spec↔plan↔change за front-matter + флаг розривів. exit 1 на розрив.
|
|
1548
|
+
const { runTraceCli } = await import('../scripts/dispatcher/trace.mjs')
|
|
1549
|
+
process.exitCode = runTraceCli(args)
|
|
1550
|
+
|
|
1551
|
+
break
|
|
1552
|
+
}
|
|
1537
1553
|
case undefined:
|
|
1538
1554
|
case '': {
|
|
1539
1555
|
await runSync()
|
|
@@ -1543,7 +1559,7 @@ try {
|
|
|
1543
1559
|
default: {
|
|
1544
1560
|
console.error(`❌ Невідома команда: ${command}`)
|
|
1545
1561
|
console.error(
|
|
1546
|
-
` Очікується: (без аргументів) синхронізація правил, check, rename-yaml-extensions, post-tool-use-fix, lint, lint-ga, lint-rego, lint-k8s, lint-docker, lint-text, coverage, change, release, skill, worktree, lint-ci`
|
|
1562
|
+
` Очікується: (без аргументів) синхронізація правил, check, rename-yaml-extensions, post-tool-use-fix, lint, lint-ga, lint-rego, lint-k8s, lint-docker, lint-text, coverage, change, release, skill, worktree, lint-ci, flow, trace`
|
|
1547
1563
|
)
|
|
1548
1564
|
process.exitCode = 1
|
|
1549
1565
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
|
|
2
|
+
import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Запускає правило: applies → JS-concerns → policy → mdc-refs (через runStandardRule).
|
|
6
|
+
* Pure-doc contract-правило: програмних concern-ів немає, тож по суті валідує `.mdc`.
|
|
7
|
+
* @param {import('../../scripts/lib/run-standard-rule.mjs').RuleContext} [ctx] контекст прогону (walkCache тощо)
|
|
8
|
+
* @returns {Promise<number>} 0 — OK, 1 — порушення
|
|
9
|
+
*/
|
|
10
|
+
export function run(ctx) {
|
|
11
|
+
return runStandardRule(import.meta.dirname, ctx)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (isRunAsCli(import.meta.url)) {
|
|
15
|
+
// Standalone: bun rules/flow/fix.mjs — повний еквівалент `npx @nitra/cursor fix flow`.
|
|
16
|
+
// eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
|
|
17
|
+
process.exit(await runRuleCli(import.meta.dirname))
|
|
18
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Контракт Пасивного Турнікета n-cursor flow — IDE-агент сам пише код, але ізолює/перевіряє/релізить через flow init/verify/release.
|
|
3
|
+
globs:
|
|
4
|
+
alwaysApply: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# n-cursor flow — Пасивний Турнікет (контракт виконавця)
|
|
8
|
+
|
|
9
|
+
`n-cursor flow` — це Dual-Mode Dispatcher. В інтерактивному середовищі (Cursor
|
|
10
|
+
Composer, Claude Code) працює **Пасивний Турнікет**: ти, агент, **сам пишеш
|
|
11
|
+
код**, а `n-cursor` лише ізолює роботу, **судить** її якість і релізить. Жодного
|
|
12
|
+
прихованого спавну субагентів — керуєш ти.
|
|
13
|
+
|
|
14
|
+
## Контракт (виконуй у цьому порядку)
|
|
15
|
+
|
|
16
|
+
1. **Старт** — на початку задачі:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
npx @nitra/cursor flow init <branch> "<опис>"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Створює ізольований worktree (`.worktrees/<branch>/`) і стан задачі. Якщо ти
|
|
23
|
+
вже в worktree — новий не вкладається.
|
|
24
|
+
|
|
25
|
+
2. **Пиши код** сам, кроками. TDD: спершу падаючі тести, тоді реалізація.
|
|
26
|
+
|
|
27
|
+
3. **Перевіряй** після кожного логічного кроку:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
npx @nitra/cursor flow verify
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Проганяє Quality Gates (lint + coverage). Повертає `0` (pass) або `1` із
|
|
34
|
+
виводом проваленого gate.
|
|
35
|
+
|
|
36
|
+
4. **На провал** — виправ код за виводом і виклич `flow verify` знову. Максимум
|
|
37
|
+
**3 спроби**; якщо не вдається — зупинись і поклич людину.
|
|
38
|
+
|
|
39
|
+
5. **Фініш** — лише після зеленого `verify`:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
npx @nitra/cursor flow release --bump <patch|minor|major> --section <Added|Changed|Fixed> --message "<що зроблено>"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Генерує `.changes/` і пише completion snapshot. Гілка готова до merge.
|
|
46
|
+
|
|
47
|
+
## Чого не роби
|
|
48
|
+
|
|
49
|
+
- Не обходь `verify` — це єдиний критерій «готово».
|
|
50
|
+
- Не редагуй `version` чи `CHANGELOG.md` вручну (це робить CI з `.changes/`).
|
|
51
|
+
- Не коміть стан `.worktrees/<branch>.flow.json` у гілку — він поза git.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
description: Використання pg / mysql2 / Bun SQL у Node.js та Bun
|
|
3
3
|
globs: "**/package.json,**/src/conn/**"
|
|
4
4
|
alwaysApply: false
|
|
5
|
-
version: '1.
|
|
5
|
+
version: '1.14'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
## Підтримувані версії баз даних
|
|
@@ -57,16 +57,12 @@ const sql = format(`MERGE INTO t USING (VALUES %s) AS s(id, date, data) ON ...`,
|
|
|
57
57
|
await pgWrite.unsafe(sql)
|
|
58
58
|
|
|
59
59
|
// ✅ UNNEST — 3 параметри незалежно від розміру batch; план стабільний і може кешуватись
|
|
60
|
-
const ids = batch.map(r => r.id)
|
|
61
|
-
const dates = batch.map(r => r.date)
|
|
62
|
-
const data = batch.map(r => JSON.stringify(r.data))
|
|
63
|
-
|
|
64
60
|
await pgWrite`
|
|
65
61
|
WITH s(id, date, data) AS (
|
|
66
62
|
SELECT * FROM unnest(
|
|
67
|
-
${
|
|
68
|
-
${
|
|
69
|
-
${data}
|
|
63
|
+
${pgWrite.array(batch.map(r => r.id), 'int4')},
|
|
64
|
+
${pgWrite.array(batch.map(r => r.date), 'date')},
|
|
65
|
+
${pgWrite.array(batch.map(r => r.data), 'jsonb')}
|
|
70
66
|
)
|
|
71
67
|
)
|
|
72
68
|
MERGE INTO my_table AS t
|
|
@@ -98,6 +94,22 @@ await pgWrite`
|
|
|
98
94
|
`
|
|
99
95
|
```
|
|
100
96
|
|
|
97
|
+
## JSONB-параметри: без `JSON.stringify`
|
|
98
|
+
|
|
99
|
+
Bun SQL серіалізує JS-об'єкти й масиви у JSON автоматично — викликати `JSON.stringify` перед передачею в `::jsonb` / `::jsonb[]` **заборонено**.
|
|
100
|
+
|
|
101
|
+
```javascript
|
|
102
|
+
// ❌ зайвий JSON.stringify — подвійна серіалізація або зайвий рядок
|
|
103
|
+
await sql`INSERT INTO events (details) VALUES (${JSON.stringify(detailsForEvent)}::jsonb)`
|
|
104
|
+
|
|
105
|
+
await sql`SELECT * FROM unnest(${sql.array(batch.map(r => JSON.stringify(r.data)), 'jsonb')})`
|
|
106
|
+
|
|
107
|
+
// ✅ об'єкт/масив передається напряму
|
|
108
|
+
await sql`INSERT INTO events (details) VALUES (${detailsForEvent}::jsonb)`
|
|
109
|
+
|
|
110
|
+
await sql`SELECT * FROM unnest(${sql.array(batch.map(r => r.data), 'jsonb')})`
|
|
111
|
+
```
|
|
112
|
+
|
|
101
113
|
`UNION ALL`-цикл замість `unnest` підходить для малих динамічних запитів (2–5 рядків), де кожна гілка семантично різна. Для bulk upsert — завжди `unnest`.
|
|
102
114
|
|
|
103
115
|
### Заборонений «drop-in» шим
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-диспетчер `n-cursor flow` (spec §8 Dual-Mode Dispatcher).
|
|
3
|
+
*
|
|
4
|
+
* Два фасади навколо єдиного джерела істини `.flow.json`:
|
|
5
|
+
* - **Пасивний Турнікет** (Фасад A): `init`, `verify`, `release` — для IDE-
|
|
6
|
+
* агентів (Cursor/Claude Code), що самі пишуть код; `n-cursor` лише судить.
|
|
7
|
+
* - **Активний Раннер** (Фасад B): `run`, `resume`, `cancel`, `repair` —
|
|
8
|
+
* повний 5-фазний polyfill-цикл для headless/CI.
|
|
9
|
+
*/
|
|
10
|
+
import { cancel, repair, resume, run } from './lib/active.mjs'
|
|
11
|
+
import { init, release, verify } from './lib/commands.mjs'
|
|
12
|
+
|
|
13
|
+
const USAGE = [
|
|
14
|
+
'Usage:',
|
|
15
|
+
' npx @nitra/cursor flow init "<опис>" # Фасад A: worktree + .flow.json',
|
|
16
|
+
' npx @nitra/cursor flow verify # Фасад A: Quality Gates (pass/fail)',
|
|
17
|
+
' npx @nitra/cursor flow release # Фасад A: .changes + completion snapshot',
|
|
18
|
+
' npx @nitra/cursor flow run "<опис>" # Фасад B: повний 5-фазний цикл',
|
|
19
|
+
' npx @nitra/cursor flow resume # продовжити з чекпойнта',
|
|
20
|
+
' npx @nitra/cursor flow cancel # скасувати, прибрати стан',
|
|
21
|
+
' npx @nitra/cursor flow repair [--discard-step-work] # відновлення пошкодженого стану'
|
|
22
|
+
].join('\n')
|
|
23
|
+
|
|
24
|
+
/** Підкоманди flow. */
|
|
25
|
+
export const SUBCOMMANDS = ['init', 'verify', 'release', 'run', 'resume', 'cancel', 'repair']
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Усі handler-и реальні (Ф2 Турнікет + Ф4 Активний Раннер).
|
|
29
|
+
* @type {Record<string, (rest: string[], deps: object) => Promise<number>>}
|
|
30
|
+
*/
|
|
31
|
+
export const DEFAULT_HANDLERS = { init, verify, release, run, resume, cancel, repair }
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Точка входу `case 'flow'` у `bin/n-cursor.js`. Парсить підкоманду й
|
|
35
|
+
* маршрутизує до handler-а. Невідома/відсутня підкоманда → usage + код 1.
|
|
36
|
+
* @param {string[]} args аргументи після `flow`
|
|
37
|
+
* @param {{ handlers?: Record<string, (rest: string[], deps: object) => Promise<number>> }} [deps] ін'єкція handler-ів (для тестів)
|
|
38
|
+
* @returns {Promise<number>} exit code
|
|
39
|
+
*/
|
|
40
|
+
export async function runFlowCli(args, deps = {}) {
|
|
41
|
+
const [sub, ...rest] = args
|
|
42
|
+
const handlers = deps.handlers ?? DEFAULT_HANDLERS
|
|
43
|
+
if (!sub || ! Object.hasOwn(handlers, sub)) {
|
|
44
|
+
console.error(USAGE)
|
|
45
|
+
return 1
|
|
46
|
+
}
|
|
47
|
+
return await handlers[sub](rest, deps)
|
|
48
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Активний Раннер (spec §8.1 Фасад B): `run`/`resume`/`cancel`/`repair`. Зшиває
|
|
3
|
+
* ensureWorktree + planner + executor + verify у повний 5-фазний цикл. Уся IO
|
|
4
|
+
* ін'єктується (`runner`/`verify`/`commit`/`run`/`now`) — тестується без
|
|
5
|
+
* реальних LLM/git/gates.
|
|
6
|
+
*/
|
|
7
|
+
import { spawnSync } from 'node:child_process'
|
|
8
|
+
import { readFileSync } from 'node:fs'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
import { cwd as processCwd } from 'node:process'
|
|
11
|
+
|
|
12
|
+
import { BudgetExceeded, withBudget } from './budget.mjs'
|
|
13
|
+
import { ensureWorktree, realRun } from './commands.mjs'
|
|
14
|
+
import { flowEventsPath } from './events.mjs'
|
|
15
|
+
import { executePlan } from './executor.mjs'
|
|
16
|
+
import { generatePlan } from './planner.mjs'
|
|
17
|
+
import { runReview } from './reviewer.mjs'
|
|
18
|
+
import {
|
|
19
|
+
cleanupFlowSiblings,
|
|
20
|
+
flowStatePath,
|
|
21
|
+
readState,
|
|
22
|
+
updateState,
|
|
23
|
+
writeState
|
|
24
|
+
} from './state-store.mjs'
|
|
25
|
+
import { createRunner } from './subagent-runner.mjs'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Дефолтний commit: `git add -A && git commit -m` у worktree.
|
|
29
|
+
* @param {string} cwd worktree
|
|
30
|
+
* @param {string} msg повідомлення
|
|
31
|
+
* @returns {void}
|
|
32
|
+
*/
|
|
33
|
+
function defaultCommit(cwd, msg) {
|
|
34
|
+
spawnSync('git', ['add', '-A'], { cwd })
|
|
35
|
+
spawnSync('git', ['commit', '-m', msg], { cwd })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Дефолтний verify для executor-а: проганяє gates і повертає verdict.
|
|
40
|
+
* @param {string} cwd worktree
|
|
41
|
+
* @returns {{ pass: boolean, failedOutput: string | null }} verdict
|
|
42
|
+
*/
|
|
43
|
+
function defaultVerify(cwd) {
|
|
44
|
+
return runReview({ run: realRun, cwd, fingerprint: () => null })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Читає `flow.autonomous` із `.n-cursor.json` (бюджет автономного режиму).
|
|
49
|
+
* @param {string} cwd корінь
|
|
50
|
+
* @returns {{ maxApiCalls?: number, maxCostUsd?: number, onBudgetExceeded?: string }} конфіг бюджету
|
|
51
|
+
*/
|
|
52
|
+
function readFlowAutonomous(cwd) {
|
|
53
|
+
try {
|
|
54
|
+
const cfg = JSON.parse(readFileSync(join(cwd, '.n-cursor.json'), 'utf8'))
|
|
55
|
+
return cfg?.flow?.autonomous ?? {}
|
|
56
|
+
} catch {
|
|
57
|
+
return {}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* `flow run [--autonomous] <branch> "<task>"` — повний цикл: ensureWorktree →
|
|
63
|
+
* план → executor. У `--autonomous` runner обгортається budget guard-ом (§9.4).
|
|
64
|
+
* @param {string[]} rest аргументи (`--autonomous` + `<branch> <task...>`)
|
|
65
|
+
* @param {{ runner?: object, verify?: (cwd: string) => object, commit?: (cwd: string, msg: string) => void, run?: (cmd: string, args: string[], opts: object) => object, autonomous?: boolean, budget?: object, cwd?: string, log?: (m: string) => void, now?: () => number }} [deps] ін'єкції
|
|
66
|
+
* @returns {Promise<number>} exit code: 0 done, 1 fail, 2 blocked-on-human
|
|
67
|
+
*/
|
|
68
|
+
export async function run(rest, deps = {}) {
|
|
69
|
+
const log = deps.log ?? console.error
|
|
70
|
+
const now = deps.now ?? Date.now
|
|
71
|
+
const autonomous = deps.autonomous ?? rest.includes('--autonomous')
|
|
72
|
+
const positional = rest.filter(a => !a.startsWith('--'))
|
|
73
|
+
|
|
74
|
+
const ew = ensureWorktree(positional, deps)
|
|
75
|
+
if (ew.code !== 0) return ew.code
|
|
76
|
+
const { worktreeDir, branch, desc, baseCommit } = ew
|
|
77
|
+
const statePath = flowStatePath(worktreeDir)
|
|
78
|
+
writeState(statePath, {
|
|
79
|
+
branch,
|
|
80
|
+
status: 'in_progress',
|
|
81
|
+
started_at: new Date(now()).toISOString(),
|
|
82
|
+
metadata: { base_commit: baseCommit },
|
|
83
|
+
plan: []
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
let runner
|
|
87
|
+
try {
|
|
88
|
+
runner = deps.runner ?? (await createRunner(deps))
|
|
89
|
+
} catch (error) {
|
|
90
|
+
log(`run: ${error.message}`)
|
|
91
|
+
return 1
|
|
92
|
+
}
|
|
93
|
+
if (autonomous) {
|
|
94
|
+
const budget = deps.budget ?? readFlowAutonomous(deps.cwd ?? processCwd())
|
|
95
|
+
runner = withBudget(runner, { maxApiCalls: budget.maxApiCalls, log })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const plan = await generatePlan({ runner, task: desc, cwd: worktreeDir })
|
|
100
|
+
updateState(statePath, s => ({ ...s, plan }))
|
|
101
|
+
const result = await executePlan(
|
|
102
|
+
{ statePath, eventsPath: flowEventsPath(worktreeDir) },
|
|
103
|
+
{ runner, verify: deps.verify ?? defaultVerify, commit: deps.commit ?? defaultCommit, cwd: worktreeDir, log, now }
|
|
104
|
+
)
|
|
105
|
+
if (result.status === 'done') {
|
|
106
|
+
log('run: build done — далі `flow release`')
|
|
107
|
+
return 0
|
|
108
|
+
}
|
|
109
|
+
if (result.status === 'blocked-on-human') {
|
|
110
|
+
log(`run: blocked-on-human на кроці ${result.step}`)
|
|
111
|
+
return 2
|
|
112
|
+
}
|
|
113
|
+
return 1
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (error instanceof BudgetExceeded) {
|
|
116
|
+
log(`run: ${error.message} — abort`)
|
|
117
|
+
updateState(statePath, s => ({ ...s, status: 'failed' }))
|
|
118
|
+
return 1
|
|
119
|
+
}
|
|
120
|
+
log(`run: ${error.message}`)
|
|
121
|
+
return 1
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* `flow resume` — продовжує з чекпойнта. Safe-resume (§4.1.7): скидає частковий
|
|
127
|
+
* доробок до останнього коміту; застосовує HITL-відповіді як підказки й дає
|
|
128
|
+
* крокам свіжі спроби.
|
|
129
|
+
* @param {string[]} _rest аргументи (не використовуються)
|
|
130
|
+
* @param {object} [deps] ін'єкції (як у `run`)
|
|
131
|
+
* @returns {Promise<number>} exit code
|
|
132
|
+
*/
|
|
133
|
+
export async function resume(_rest, deps = {}) {
|
|
134
|
+
const cwd = deps.cwd ?? processCwd()
|
|
135
|
+
const log = deps.log ?? console.error
|
|
136
|
+
const now = deps.now ?? Date.now
|
|
137
|
+
const run_ = deps.run ?? realRun
|
|
138
|
+
|
|
139
|
+
const statePath = flowStatePath(cwd)
|
|
140
|
+
const state = readState(statePath)
|
|
141
|
+
if (!state) {
|
|
142
|
+
log('resume: стану нема')
|
|
143
|
+
return 1
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const openHitl = (state.hitl ?? []).filter(q => !q.answer)
|
|
147
|
+
if (state.status === 'blocked-on-human' && openHitl.length > 0) {
|
|
148
|
+
log(`resume: ще blocked — ${openHitl.length} відкритих HITL-питань (заповни answer і повтори)`)
|
|
149
|
+
return 2
|
|
150
|
+
}
|
|
151
|
+
if (!state.plan?.length) {
|
|
152
|
+
log('resume: нема плану')
|
|
153
|
+
return 1
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// safe-resume: скинути частковий доробок невдалого кроку до останнього коміту
|
|
157
|
+
run_('git', ['reset', '--hard', 'HEAD'], { cwd })
|
|
158
|
+
|
|
159
|
+
// застосувати HITL-відповіді як hint + дати незавершеним крокам свіжі спроби
|
|
160
|
+
const answers = new Map((state.hitl ?? []).filter(q => q.answer).map(q => [q.step, q.answer]))
|
|
161
|
+
updateState(statePath, s => ({
|
|
162
|
+
...s,
|
|
163
|
+
status: 'in_progress',
|
|
164
|
+
plan: s.plan.map(st =>
|
|
165
|
+
st.status === 'done' ? st : { ...st, retry_count: 0, ...(answers.has(st.step) ? { hint: answers.get(st.step) } : {}) }
|
|
166
|
+
),
|
|
167
|
+
hitl: (s.hitl ?? []).map(q => (q.answer ? { ...q, status: 'answered' } : q))
|
|
168
|
+
}))
|
|
169
|
+
|
|
170
|
+
let runner
|
|
171
|
+
try {
|
|
172
|
+
runner = deps.runner ?? (await createRunner(deps))
|
|
173
|
+
} catch (error) {
|
|
174
|
+
log(`resume: ${error.message}`)
|
|
175
|
+
return 1
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const result = await executePlan(
|
|
179
|
+
{ statePath, eventsPath: flowEventsPath(cwd) },
|
|
180
|
+
{ runner, verify: deps.verify ?? defaultVerify, commit: deps.commit ?? defaultCommit, cwd, log, now }
|
|
181
|
+
)
|
|
182
|
+
if (result.status === 'done') return 0
|
|
183
|
+
if (result.status === 'blocked-on-human') return 2
|
|
184
|
+
return 1
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* `flow cancel` — скасування: прибирає transient sibling-и (стан/журнал/lock).
|
|
189
|
+
* @param {string[]} _rest аргументи
|
|
190
|
+
* @param {{ cwd?: string, log?: (m: string) => void }} [deps] ін'єкції
|
|
191
|
+
* @returns {Promise<number>} 0
|
|
192
|
+
*/
|
|
193
|
+
export async function cancel(_rest, deps = {}) {
|
|
194
|
+
const cwd = deps.cwd ?? processCwd()
|
|
195
|
+
const log = deps.log ?? console.error
|
|
196
|
+
cleanupFlowSiblings(cwd)
|
|
197
|
+
log('cancel: стан і sibling-и прибрано')
|
|
198
|
+
return 0
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* `flow repair [--discard-step-work]` — fail-closed escape: діагностика стану або
|
|
203
|
+
* жорстке скидання робочого дерева до HEAD (свідоме викидання доробку).
|
|
204
|
+
* @param {string[]} rest аргументи
|
|
205
|
+
* @param {{ run?: (cmd: string, args: string[], opts: object) => object, cwd?: string, log?: (m: string) => void }} [deps] ін'єкції
|
|
206
|
+
* @returns {Promise<number>} exit code
|
|
207
|
+
*/
|
|
208
|
+
export async function repair(rest, deps = {}) {
|
|
209
|
+
const cwd = deps.cwd ?? processCwd()
|
|
210
|
+
const log = deps.log ?? console.error
|
|
211
|
+
const run_ = deps.run ?? realRun
|
|
212
|
+
|
|
213
|
+
if (rest.includes('--discard-step-work')) {
|
|
214
|
+
run_('git', ['reset', '--hard', 'HEAD'], { cwd })
|
|
215
|
+
log('repair: робоче дерево скинуто до HEAD (--discard-step-work)')
|
|
216
|
+
return 0
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
const state = readState(flowStatePath(cwd))
|
|
220
|
+
log(state ? `repair: стан валідний (status: ${state.status})` : 'repair: стану нема')
|
|
221
|
+
return 0
|
|
222
|
+
} catch (error) {
|
|
223
|
+
log(`repair: стан пошкоджено — ${error.message}. Спробуй \`flow repair --discard-step-work\` або \`flow cancel\`.`)
|
|
224
|
+
return 1
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Budget guard для автономного режиму (spec §9.4): обгортає SubagentRunner
|
|
3
|
+
* лічильником викликів і кидає `BudgetExceeded` при перевищенні `maxApiCalls`.
|
|
4
|
+
* Це запобіжник проти неконтрольованих витрат на сервері (де нема людини).
|
|
5
|
+
*
|
|
6
|
+
* (`maxCostUsd` — коли runner повертатиме tokens/cost; наразі рахуємо виклики.)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Помилка перевищення бюджету (ловиться в `run`, §9.4). */
|
|
10
|
+
export class BudgetExceeded extends Error {}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Обгортає runner лічильником API-викликів.
|
|
14
|
+
* @param {{ backend?: string, runStep: (prompt: string, opts?: object) => object }} runner базовий runner
|
|
15
|
+
* @param {{ maxApiCalls?: number, log?: (m: string) => void }} [opts] ліміт і лог
|
|
16
|
+
* @returns {{ backend: string, runStep: (prompt: string, opts?: object) => Promise<object>, readonly calls: number }} обгорнутий runner
|
|
17
|
+
*/
|
|
18
|
+
export function withBudget(runner, opts = {}) {
|
|
19
|
+
const maxApiCalls = opts.maxApiCalls ?? Number.POSITIVE_INFINITY
|
|
20
|
+
const log = opts.log ?? (() => {})
|
|
21
|
+
let calls = 0
|
|
22
|
+
return {
|
|
23
|
+
backend: runner.backend,
|
|
24
|
+
get calls() {
|
|
25
|
+
return calls
|
|
26
|
+
},
|
|
27
|
+
async runStep(prompt, stepOpts) {
|
|
28
|
+
if (calls >= maxApiCalls) {
|
|
29
|
+
throw new BudgetExceeded(`budget: вичерпано maxApiCalls=${maxApiCalls}`)
|
|
30
|
+
}
|
|
31
|
+
calls += 1
|
|
32
|
+
log(`budget: API-виклик ${calls}/${maxApiCalls}`)
|
|
33
|
+
return runner.runStep(prompt, stepOpts)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability Router — резолвер режиму оркестрації (`native` vs `polyfill`)
|
|
3
|
+
* за **явною декларацією моделі** (spec §2.2).
|
|
4
|
+
*
|
|
5
|
+
* Рантайм-детекції моделі в кодобазі немає — тому модель НЕ вгадуємо, а
|
|
6
|
+
* оголошуємо за пріоритетом: CLI `--model` > env `N_CURSOR_FLOW_MODEL` >
|
|
7
|
+
* config `flow.model`. Default-режим (`polyfill`) дозволений ЛИШЕ за наявного
|
|
8
|
+
* `SubagentRunner` (§15.1); інакше — fail (caller кидає помилку), бо polyfill
|
|
9
|
+
* без runner-а не «працює з будь-якою моделлю».
|
|
10
|
+
*
|
|
11
|
+
* Усі функції чисті (без I/O) — джерела (`args`/`env`/`config`/`matrix`/
|
|
12
|
+
* `hasRunner`) передаються ззовні, що робить модуль тривіально тестованим.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_ORCHESTRATION = 'polyfill'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Витягує значення `--model <value>` з argv. Не мутує вхід.
|
|
19
|
+
* @param {string[]} args аргументи підкоманди flow
|
|
20
|
+
* @returns {string | null} оголошена модель або null
|
|
21
|
+
*/
|
|
22
|
+
export function parseModelFlag(args) {
|
|
23
|
+
const i = args.indexOf('--model')
|
|
24
|
+
return i !== -1 && i + 1 < args.length ? args[i + 1] : null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Оголошена модель за пріоритетом CLI > env > config.
|
|
29
|
+
* @param {{ cliModel?: string | null, envModel?: string | null, configModel?: string | null }} sources джерела декларації
|
|
30
|
+
* @returns {string | null} модель або null, якщо ніде не оголошено
|
|
31
|
+
*/
|
|
32
|
+
export function declaredModel({ cliModel = null, envModel = null, configModel = null } = {}) {
|
|
33
|
+
return cliModel || envModel || configModel || null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Режим оркестрації для оголошеної моделі за `capability-matrix`.
|
|
38
|
+
* Невідома/неоголошена модель → `matrix.default` → `DEFAULT_ORCHESTRATION`.
|
|
39
|
+
* @param {string | null} model оголошена модель
|
|
40
|
+
* @param {{ models?: Record<string, { orchestration?: string }>, default?: { orchestration?: string } }} matrix матриця можливостей
|
|
41
|
+
* @returns {'native' | 'polyfill'} режим
|
|
42
|
+
*/
|
|
43
|
+
export function orchestrationFor(model, matrix) {
|
|
44
|
+
const entry = model && matrix && matrix.models ? matrix.models[model] : null
|
|
45
|
+
return (
|
|
46
|
+
(entry && entry.orchestration) ||
|
|
47
|
+
(matrix && matrix.default && matrix.default.orchestration) ||
|
|
48
|
+
DEFAULT_ORCHESTRATION
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Чи стартує polyfill: потрібен доступний `SubagentRunner`.
|
|
54
|
+
* @param {{ hasRunner: boolean }} ctx контекст середовища
|
|
55
|
+
* @returns {boolean} true, якщо runner у наявності
|
|
56
|
+
*/
|
|
57
|
+
export function polyfillStartable({ hasRunner }) {
|
|
58
|
+
return hasRunner === true
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Повний резолв: оголошена модель + режим. Кидає, якщо polyfill без runner-а.
|
|
63
|
+
* @param {{ args?: string[], env?: Record<string, string | undefined>, config?: { flow?: { model?: string } }, matrix: object, hasRunner: boolean }} input джерела
|
|
64
|
+
* @returns {{ model: string | null, mode: 'native' | 'polyfill' }} оголошена модель і режим
|
|
65
|
+
*/
|
|
66
|
+
export function resolveFlow({ args = [], env = {}, config = {}, matrix, hasRunner }) {
|
|
67
|
+
const model = declaredModel({
|
|
68
|
+
cliModel: parseModelFlag(args),
|
|
69
|
+
envModel: env.N_CURSOR_FLOW_MODEL ?? null,
|
|
70
|
+
configModel: (config && config.flow && config.flow.model) ?? null
|
|
71
|
+
})
|
|
72
|
+
const mode = orchestrationFor(model, matrix)
|
|
73
|
+
if (mode === 'polyfill' && !polyfillStartable({ hasRunner })) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
'n-cursor flow: режим polyfill потребує доступного SubagentRunner ' +
|
|
76
|
+
'(`claude` або `cursor-agent` у PATH), але жодного не знайдено. ' +
|
|
77
|
+
'Оголосіть модель із native_workflows (--model) або встановіть CLI-runner.'
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
return { model, mode }
|
|
81
|
+
}
|