@nitra/cursor 3.5.0 → 3.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/rules/flow/flow.mdc +11 -1
- package/scripts/dispatcher/index.mjs +4 -2
- package/scripts/dispatcher/lib/artifact.mjs +19 -7
- package/scripts/dispatcher/lib/commands.mjs +4 -0
- package/scripts/dispatcher/lib/gate.mjs +83 -0
- package/scripts/dispatcher/lib/plan.mjs +1 -1
- package/scripts/dispatcher/lib/spec.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.6.1] - 2026-06-01
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- flow: resolveArtifact обирає артефакт за mtime + пріоритетом slug гілки замість лексикографічного (spec/plan фіксували не той doc при кількох на одну дату)
|
|
8
|
+
|
|
9
|
+
## [3.6.0] - 2026-06-01
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- flow: команда gate — структурований вердикт релізної готовності (PASS/CONCERNS/FAIL + score + причини, синтез verify-гейтів і review-findings); release м'яко попереджає на FAIL
|
|
14
|
+
|
|
3
15
|
## [3.5.0] - 2026-06-01
|
|
4
16
|
|
|
5
17
|
### Added
|
package/package.json
CHANGED
package/rules/flow/flow.mdc
CHANGED
|
@@ -79,7 +79,17 @@ Composer, Claude Code) працює **Пасивний Турнікет**: ти,
|
|
|
79
79
|
7. **На провал** — виправ код за виводом і виклич `flow verify` знову. Максимум
|
|
80
80
|
**3 спроби**; якщо не вдається — зупинись і поклич людину.
|
|
81
81
|
|
|
82
|
-
8.
|
|
82
|
+
8. **Gate (вердикт готовності)** — перед фінішем:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
npx @nitra/cursor flow gate
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Синтезує verify-гейти і review-findings у `PASS / CONCERNS / FAIL` + score +
|
|
89
|
+
причини (пише у `.flow.json`). `FAIL` (провалений gate або high-severity
|
|
90
|
+
finding) → код 1; `release` на FAIL лише попередить (рішення за тобою).
|
|
91
|
+
|
|
92
|
+
9. **Фініш** — після зеленого `verify` і бажано `gate` ≠ FAIL:
|
|
83
93
|
|
|
84
94
|
```
|
|
85
95
|
npx @nitra/cursor flow release --bump <patch|minor|major> --section <Added|Changed|Fixed> --message "<що зроблено>"
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { cancel, repair, resume, run } from './lib/active.mjs'
|
|
11
11
|
import { init, release, verify } from './lib/commands.mjs'
|
|
12
|
+
import { gate } from './lib/gate.mjs'
|
|
12
13
|
import { plan } from './lib/plan.mjs'
|
|
13
14
|
import { review } from './lib/review.mjs'
|
|
14
15
|
import { spec } from './lib/spec.mjs'
|
|
@@ -20,6 +21,7 @@ const USAGE = [
|
|
|
20
21
|
' npx @nitra/cursor flow plan [--panel] # Фасад A: фаза плану → docs/plans/<…> + state',
|
|
21
22
|
' npx @nitra/cursor flow verify # Фасад A: Quality Gates (pass/fail)',
|
|
22
23
|
' npx @nitra/cursor flow review # Фасад A: adversarial diff-review (за level)',
|
|
24
|
+
' npx @nitra/cursor flow gate # Фасад A: вердикт PASS/CONCERNS/FAIL (verify+review)',
|
|
23
25
|
' npx @nitra/cursor flow release # Фасад A: .changes + completion snapshot',
|
|
24
26
|
' npx @nitra/cursor flow run "<опис>" # Фасад B: повний 5-фазний цикл',
|
|
25
27
|
' npx @nitra/cursor flow resume # продовжити з чекпойнта',
|
|
@@ -28,13 +30,13 @@ const USAGE = [
|
|
|
28
30
|
].join('\n')
|
|
29
31
|
|
|
30
32
|
/** Підкоманди flow. */
|
|
31
|
-
export const SUBCOMMANDS = ['init', 'spec', 'plan', 'verify', 'review', 'release', 'run', 'resume', 'cancel', 'repair']
|
|
33
|
+
export const SUBCOMMANDS = ['init', 'spec', 'plan', 'verify', 'review', 'gate', 'release', 'run', 'resume', 'cancel', 'repair']
|
|
32
34
|
|
|
33
35
|
/**
|
|
34
36
|
* Усі handler-и реальні (Ф1 Spec/Plan + Ф2 Турнікет + Ф4 Активний Раннер).
|
|
35
37
|
* @type {Record<string, (rest: string[], deps: object) => Promise<number>>}
|
|
36
38
|
*/
|
|
37
|
-
export const DEFAULT_HANDLERS = { init, spec, plan, verify, review, release, run, resume, cancel, repair }
|
|
39
|
+
export const DEFAULT_HANDLERS = { init, spec, plan, verify, review, gate, release, run, resume, cancel, repair }
|
|
38
40
|
|
|
39
41
|
/**
|
|
40
42
|
* Точка входу `case 'flow'` у `bin/n-cursor.js`. Парсить підкоманду й
|
|
@@ -6,24 +6,36 @@
|
|
|
6
6
|
* Лінки front-matter (`spec.plan`/`plan.spec`/`plan.flow`) пише сам агент за
|
|
7
7
|
* контрактом `flow.mdc` — тут лише ВЕРИФІКАЦІЯ (мутатора `trace link` нема).
|
|
8
8
|
*/
|
|
9
|
-
import { existsSync, readdirSync } from 'node:fs'
|
|
9
|
+
import { existsSync, readdirSync, statSync } from 'node:fs'
|
|
10
10
|
import { join } from 'node:path'
|
|
11
11
|
|
|
12
12
|
import { runTraceCli } from '../trace.mjs'
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
15
|
+
* Резолвить артефакт у `docs/<kind>/`. Пріоритет: файли, чия назва містить
|
|
16
|
+
* хвіст гілки (slug, напр. `flow-gate` з `claude/flow-gate`); серед них (або
|
|
17
|
+
* серед усіх, якщо збігу нема) — **найсвіжіший за mtime**. Лексикографічний
|
|
18
|
+
* вибір був хибним при кількох артефактах на одну дату (виявлено dogfood'ом).
|
|
16
19
|
* @param {string} cwd корінь worktree
|
|
17
20
|
* @param {'specs' | 'plans'} kind підкаталог `docs`
|
|
21
|
+
* @param {string} [branch] гілка задачі — для пріоритету за slug
|
|
18
22
|
* @returns {string | null} абсолютний шлях або null, якщо каталог/файли відсутні
|
|
19
23
|
*/
|
|
20
|
-
export function resolveArtifact(cwd, kind) {
|
|
24
|
+
export function resolveArtifact(cwd, kind, branch) {
|
|
21
25
|
const dir = join(cwd, 'docs', kind)
|
|
22
26
|
if (!existsSync(dir)) return null
|
|
23
|
-
const md = readdirSync(dir)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
const md = readdirSync(dir).filter(f => f.endsWith('.md'))
|
|
28
|
+
if (md.length === 0) return null
|
|
29
|
+
|
|
30
|
+
const slug = branch ? branch.split('/').pop() : null
|
|
31
|
+
const matched = slug ? md.filter(f => f.includes(slug)) : []
|
|
32
|
+
const pool = matched.length > 0 ? matched : md
|
|
33
|
+
|
|
34
|
+
const best = pool
|
|
35
|
+
.map(f => ({ f, mtime: statSync(join(dir, f)).mtimeMs }))
|
|
36
|
+
.toSorted((a, b) => a.mtime - b.mtime || (a.f < b.f ? -1 : 1))
|
|
37
|
+
.at(-1)
|
|
38
|
+
return join(dir, best.f)
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
/** Маркер критерію приймання в рядку кроку (порівняння — case-insensitive). */
|
|
@@ -179,6 +179,10 @@ export async function release(rest, deps = {}) {
|
|
|
179
179
|
log('release: стану нема — спершу `flow init`')
|
|
180
180
|
return 1
|
|
181
181
|
}
|
|
182
|
+
// М'які ворота: FAIL-гейт — лише попередження, рішення за людиною.
|
|
183
|
+
if (state.gate?.verdict === 'FAIL') {
|
|
184
|
+
log(`⚠️ release: gate = FAIL (score ${state.gate.score}) — релізиш свідомо? (див. flow gate)`)
|
|
185
|
+
}
|
|
182
186
|
|
|
183
187
|
const ch = run('npx', ['@nitra/cursor', 'change', ...rest], { cwd })
|
|
184
188
|
if ((ch.status ?? 1) !== 0) {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `flow gate` — структурований вердикт релізної готовності (ідея BMAD qa-gate, у
|
|
3
|
+
* нашому стані). Синтезує механічні гейти `verify` (`state.gates`) і adversarial
|
|
4
|
+
* findings `review` (`state.review.findings`) у єдине PASS/CONCERNS/FAIL + score
|
|
5
|
+
* + причини. Дає traceability «чому готово/не готово». `gate` лише агрегує —
|
|
6
|
+
* рішення verify/review не дублює.
|
|
7
|
+
*
|
|
8
|
+
* Уся IO (`now`) ін'єктується; `computeGate` — чиста (тестується без стану на диску).
|
|
9
|
+
*/
|
|
10
|
+
import { cwd as processCwd } from 'node:process'
|
|
11
|
+
|
|
12
|
+
import { flowEventsPath } from './events.mjs'
|
|
13
|
+
import { flowStatePath, readState, recordTransition } from './state-store.mjs'
|
|
14
|
+
|
|
15
|
+
/** Штрафи score за кожен тип проблеми. */
|
|
16
|
+
const PENALTY = { failedGate: 40, high: 25, med: 8, noVerify: 15 }
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Чистий синтез вердикту з наявного стану.
|
|
20
|
+
* @param {{ gates?: { name: string, ok: boolean }[], review?: { findings?: { severity?: string }[] } }} state стан flow
|
|
21
|
+
* @returns {{ verdict: 'PASS' | 'CONCERNS' | 'FAIL', score: number, reasons: string[] }} вердикт
|
|
22
|
+
*/
|
|
23
|
+
export function computeGate(state) {
|
|
24
|
+
const gates = state.gates ?? []
|
|
25
|
+
const findings = state.review?.findings ?? []
|
|
26
|
+
const failedGates = gates.filter(g => !g.ok)
|
|
27
|
+
const high = findings.filter(f => f.severity === 'high')
|
|
28
|
+
const med = findings.filter(f => f.severity === 'med')
|
|
29
|
+
const noVerify = gates.length === 0
|
|
30
|
+
|
|
31
|
+
const reasons = []
|
|
32
|
+
for (const g of failedGates) reasons.push(`gate «${g.name}» провалено`)
|
|
33
|
+
if (high.length > 0) reasons.push(`${high.length} high-severity review finding(s)`)
|
|
34
|
+
if (med.length > 0) reasons.push(`${med.length} med-severity review finding(s)`)
|
|
35
|
+
if (noVerify) reasons.push('verify ще не запускався')
|
|
36
|
+
|
|
37
|
+
let verdict = 'PASS'
|
|
38
|
+
if (failedGates.length > 0 || high.length > 0) {
|
|
39
|
+
verdict = 'FAIL'
|
|
40
|
+
} else if (med.length > 0 || noVerify) {
|
|
41
|
+
verdict = 'CONCERNS'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const penalty =
|
|
45
|
+
PENALTY.failedGate * failedGates.length +
|
|
46
|
+
PENALTY.high * high.length +
|
|
47
|
+
PENALTY.med * med.length +
|
|
48
|
+
(noVerify ? PENALTY.noVerify : 0)
|
|
49
|
+
const score = Math.max(0, Math.min(100, 100 - penalty))
|
|
50
|
+
|
|
51
|
+
return { verdict, score, reasons }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* `flow gate` — обчислює й фіксує вердикт у `.flow.json`.
|
|
56
|
+
* @param {string[]} _rest аргументи (не використовуються)
|
|
57
|
+
* @param {{ cwd?: string, log?: (m: string) => void, now?: () => number }} [deps] ін'єкції
|
|
58
|
+
* @returns {Promise<number>} exit code (FAIL → 1; PASS/CONCERNS → 0)
|
|
59
|
+
*/
|
|
60
|
+
export async function gate(_rest, deps = {}) {
|
|
61
|
+
const cwd = deps.cwd ?? processCwd()
|
|
62
|
+
const log = deps.log ?? console.error
|
|
63
|
+
const now = deps.now ?? Date.now
|
|
64
|
+
|
|
65
|
+
const statePath = flowStatePath(cwd)
|
|
66
|
+
const state = readState(statePath)
|
|
67
|
+
if (!state) {
|
|
68
|
+
log('gate: стану нема — спершу `flow init`')
|
|
69
|
+
return 1
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const result = computeGate(state)
|
|
73
|
+
recordTransition(
|
|
74
|
+
{ statePath, eventsPath: flowEventsPath(cwd) },
|
|
75
|
+
{ type: 'gate', verdict: result.verdict },
|
|
76
|
+
s => ({ ...s, gate: { ...result, at: new Date(now()).toISOString() } }),
|
|
77
|
+
now
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
log(`gate: ${result.verdict} (score ${result.score})`)
|
|
81
|
+
for (const r of result.reasons) log(` · ${r}`)
|
|
82
|
+
return result.verdict === 'FAIL' ? 1 : 0
|
|
83
|
+
}
|
|
@@ -35,7 +35,7 @@ export async function plan(rest, deps = {}) {
|
|
|
35
35
|
log('plan: дизайн ще не зафіксовано — рекомендовано спершу `flow spec` (не блокує)')
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
const doc = rest.find(a => a.endsWith('.md')) ?? resolveArtifact(cwd, 'plans')
|
|
38
|
+
const doc = rest.find(a => a.endsWith('.md')) ?? resolveArtifact(cwd, 'plans', state.branch)
|
|
39
39
|
let steps
|
|
40
40
|
if (rest.includes('--panel')) {
|
|
41
41
|
let runner = deps.runner
|
|
@@ -48,7 +48,7 @@ export async function spec(rest, deps = {}) {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
const doc = rest.find(a => a.endsWith('.md')) ?? resolveArtifact(cwd, 'specs')
|
|
51
|
+
const doc = rest.find(a => a.endsWith('.md')) ?? resolveArtifact(cwd, 'specs', state.branch)
|
|
52
52
|
if (!doc || !existsSync(doc)) {
|
|
53
53
|
log('spec: нема docs/specs/<date>-<slug>.md — спершу пройди brainstorm (див. flow.mdc)')
|
|
54
54
|
return 1
|