@nitra/cursor 3.29.0 → 4.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.
@@ -1,73 +0,0 @@
1
- /**
2
- * Декларативний планувальник (spec §3 Ф1). Просить субагента видати суворий
3
- * покроковий JSON-план, парсить і **валідує** його (fail-closed на невалідному).
4
- * Нормалізує кроки до `{ step, task, status: 'pending', retry_count: 0 }`.
5
- */
6
-
7
- /** Заборонені плейсхолдер-значення `task` (план із ними — не план, fail-closed). */
8
- const PLACEHOLDER = /^(tbd|todo|fixme|\.\.\.|placeholder)$/i
9
-
10
- /**
11
- * Системно-користувацький промпт планувальника.
12
- * @param {string} task опис фічі
13
- * @returns {string} промпт
14
- */
15
- export function plannerPrompt(task) {
16
- return [
17
- 'Ти — архітектор. Розбий задачу на суворий покроковий план реалізації.',
18
- 'Кожен крок — ≤ 5 хв розробки, з чіткими критеріями приймання коду.',
19
- 'Поверни ЛИШЕ JSON-масив без коментарів: [{ "task": "...", "acceptance": "..." }, ...].',
20
- '',
21
- `Задача: ${task}`
22
- ].join('\n')
23
- }
24
-
25
- /**
26
- * Парсить і валідує план із тексту відповіді (толерує markdown-огорожу).
27
- * @param {string} text відповідь субагента
28
- * @returns {{ step: number, task: string, status: string, retry_count: number, acceptance?: string }[]} нормалізований план
29
- */
30
- export function parsePlan(text) {
31
- const str = String(text)
32
- const start = str.indexOf('[')
33
- const end = str.lastIndexOf(']')
34
- if (start === -1 || end === -1 || end < start) {
35
- throw new Error('planner: не знайдено JSON-масив плану — fail-closed')
36
- }
37
- let arr
38
- try {
39
- arr = JSON.parse(str.slice(start, end + 1))
40
- } catch {
41
- throw new Error('planner: невалідний JSON плану — fail-closed')
42
- }
43
- if (!Array.isArray(arr) || arr.length === 0) {
44
- throw new Error('planner: план має бути непорожнім масивом — fail-closed')
45
- }
46
- return arr.map((s, i) => {
47
- const task = typeof s === 'string' ? s : s?.task
48
- if (!task || typeof task !== 'string') {
49
- throw new Error(`planner: крок ${i} без текстового поля task — fail-closed`)
50
- }
51
- const trimmed = task.trim()
52
- if (!trimmed || PLACEHOLDER.test(trimmed)) {
53
- throw new Error(`planner: крок ${i} — placeholder/порожній task (${task}) — fail-closed`)
54
- }
55
- const step = { step: i, task, status: 'pending', retry_count: 0 }
56
- if (s?.acceptance) step.acceptance = String(s.acceptance)
57
- return step
58
- })
59
- }
60
-
61
- /**
62
- * Генерує план через субагента-планувальника.
63
- * @param {{ runner: { runStep: (prompt: string, opts?: object) => { ok: boolean, output: string } | Promise<{ ok: boolean, output: string }> }, task: string, cwd?: string }} input ін'єкції
64
- * @returns {Promise<object[]>} нормалізований план
65
- */
66
- export async function generatePlan({ runner, task, cwd }) {
67
- const res = await runner.runStep(plannerPrompt(task), { cwd })
68
- if (!res.ok) {
69
- const detail = res.output ? `:\n${res.output}` : ''
70
- throw new Error(`planner: субагент-планувальник завершився помилкою${detail}`)
71
- }
72
- return parsePlan(res.output)
73
- }
@@ -1,176 +0,0 @@
1
- /**
2
- * `flow review` — adversarial-перевірка коду ПІСЛЯ написання (ідея з BMAD
3
- * quick-dev: self-check → adversarial-review). Незалежний субагент читає ЛИШЕ
4
- * `git diff base_commit` і шукає логічні баги/ризики, яких не ловлять механічні
5
- * гейти `verify` (lint+coverage). Findings пишуться у `.flow.json`; команда
6
- * інформативна (м'які ворота — завжди код 0). Кількість рецензентів — за `level`.
7
- *
8
- * Уся IO (`run`/`runner`/`now`) ін'єктується — тестується без git/LLM.
9
- */
10
- import { cwd as processCwd } from 'node:process'
11
-
12
- import { realRun } from './commands.mjs'
13
- import { flowEventsPath } from './events.mjs'
14
- import { reviewersFor } from './level.mjs'
15
- import { readState, recordTransition } from './state-store.mjs'
16
- import { resolveActiveFlowState } from './flow-resolve.mjs'
17
- import { createRunner } from './subagent-runner.mjs'
18
-
19
- /** Ліміт diff у промпті (символів) — щоб не роздувати контекст рецензента. */
20
- const DIFF_LIMIT = 12_000
21
-
22
- /**
23
- * Текст diff від base: `base...HEAD` (закомічене) + `git diff` (робоче дерево).
24
- * @param {string} base базовий комміт
25
- * @param {(cmd: string, args: string[], opts: object) => { stdout: string }} run git-runner
26
- * @param {string} cwd worktree
27
- * @returns {string} склеєний diff (trim)
28
- */
29
- export function diffFromBase(base, run, cwd) {
30
- const committed = run('git', ['diff', `${base}...HEAD`], { cwd })
31
- const working = run('git', ['diff'], { cwd })
32
- return `${committed.stdout ?? ''}\n${working.stdout ?? ''}`.trim()
33
- }
34
-
35
- /**
36
- * Промпт adversarial-рецензента. Фокус — diff, але рецензент працює у робочій теці
37
- * репо й має інструмент `Read`, тож cross-file твердження мусить верифікувати читанням.
38
- * Для high-risk додає безпекову лінзу.
39
- * @param {string} diff текст diff
40
- * @param {string} [risk] low|med|high — фокус перевірки
41
- * @returns {string} промпт
42
- */
43
- export function reviewerPrompt(diff, risk) {
44
- const lens =
45
- risk === 'high'
46
- ? "ОСОБЛИВА УВАГА БЕЗПЕЦІ: auth/доступи, секрети/токени, ін'єкції, валідація входу, незворотні операції."
47
- : ''
48
- return [
49
- 'Ти — прискіпливий adversarial-рецензент. Знайди баги, ризики й smells, які ВНОСИТЬ або зачіпає цей diff.',
50
- 'Якщо тобі доступний інструмент Read — ти в робочій теці репо: читай ТОЧКОВО потрібні referenced-файли' +
51
- ' (викликану функцію, інший модуль, spec/plan, конфіг), щоб ПЕРЕВІРИТИ cross-file твердження перед репортом.' +
52
- ' Якщо Read недоступний — рецензуй лише diff.',
53
- 'Сусідні файли читай ДЛЯ КОНТЕКСТУ й верифікації, а не щоб шукати в них окремі преіснуючі баги:' +
54
- ' репортуй лише те, що вносить/ламає цей diff.',
55
- 'НЕ видавай нефальсифіковних findings виду «з diff не видно / не показано / можливо» —' +
56
- ' або підтверди читанням файлу, або відкинь. Кожен finding має бути перевірним фактом.',
57
- lens,
58
- 'Поверни ЛИШЕ JSON-масив: [{ "severity": "high|med|low", "file": "...", "issue": "...", "suggestion": "..." }].',
59
- 'Якщо проблем нема — поверни [].',
60
- '',
61
- 'DIFF (фокус рецензування):',
62
- diff.slice(0, DIFF_LIMIT)
63
- ]
64
- .filter(Boolean)
65
- .join('\n')
66
- }
67
-
68
- /**
69
- * Парсить findings із відповіді рецензента. Fail-soft: сміття/невалідний JSON → [].
70
- * @param {string} text відповідь субагента
71
- * @returns {{ severity?: string, file?: string, issue?: string, suggestion?: string }[]} findings
72
- */
73
- export function parseFindings(text) {
74
- const s = String(text)
75
- const start = s.indexOf('[')
76
- const end = s.lastIndexOf(']')
77
- if (start === -1 || end === -1 || end < start) return []
78
- try {
79
- const arr = JSON.parse(s.slice(start, end + 1))
80
- return Array.isArray(arr) ? arr : []
81
- } catch {
82
- return []
83
- }
84
- }
85
-
86
- /**
87
- * Дедуплікує findings за (file, issue).
88
- * @param {object[]} findings вхідні
89
- * @returns {object[]} без дублікатів
90
- */
91
- export function dedupeFindings(findings) {
92
- const seen = new Set()
93
- const out = []
94
- for (const f of findings) {
95
- const key = `${f?.file ?? ''}::${f?.issue ?? ''}`
96
- if (seen.has(key)) continue
97
- seen.add(key)
98
- out.push(f)
99
- }
100
- return out
101
- }
102
-
103
- /**
104
- * Іконка за severity finding-а.
105
- * @param {string} severity рівень
106
- * @returns {string} емодзі
107
- */
108
- function severityIcon(severity) {
109
- if (severity === 'high') return '🔴'
110
- if (severity === 'med') return '🟡'
111
- return '⚪'
112
- }
113
-
114
- /**
115
- * `flow review` — спавнить adversarial-рецензента(ів) на diff від base.
116
- * @param {string[]} _rest аргументи (не використовуються)
117
- * @param {{ cwd?: string, log?: (m: string) => void, run?: (cmd: string, args: string[], opts: object) => { stdout: string }, runner?: object, now?: () => number }} [deps] ін'єкції
118
- * @returns {Promise<number>} exit code (0 завжди — інформативна; 1 лише якщо нема стану/runner)
119
- */
120
- export async function review(_rest, deps = {}) {
121
- const cwd0 = deps.cwd ?? processCwd()
122
- const log = deps.log ?? console.error
123
- const run = deps.run ?? realRun
124
- const now = deps.now ?? Date.now
125
-
126
- const resolved = resolveActiveFlowState({ cwd: cwd0, branch: deps.branch }, deps)
127
- if (!resolved.statePath) {
128
- log(`review: ${resolved.error}`)
129
- return 1
130
- }
131
- if (resolved.autoResolved) log(`flow: авторезолвлено активний flow «${resolved.label}» (cwd поза worktree)`)
132
- const cwd = resolved.worktreeDir ?? cwd0
133
- const statePath = resolved.statePath
134
- const state = readState(statePath)
135
- if (!state) {
136
- log('review: стану нема — спершу `flow init`')
137
- return 1
138
- }
139
-
140
- const base = state.metadata?.base_commit ?? 'HEAD~1'
141
- const diff = diffFromBase(base, run, cwd)
142
- if (!diff) {
143
- log('review: нема змін від base — нічого ревʼювити')
144
- return 0
145
- }
146
-
147
- let runner = deps.runner
148
- if (!runner) {
149
- try {
150
- runner = await createRunner(deps)
151
- } catch (error) {
152
- log(`review: ${error.message}`)
153
- return 1
154
- }
155
- }
156
-
157
- const reviewers = reviewersFor(state.level ?? 1, state.risk)
158
- const prompt = reviewerPrompt(diff, state.risk)
159
- const results = await Promise.all(Array.from({ length: reviewers }, () => runner.runStep(prompt, { cwd })))
160
- const findings = dedupeFindings(results.flatMap(r => (r.ok ? parseFindings(r.output) : [])))
161
-
162
- recordTransition(
163
- { statePath, eventsPath: flowEventsPath(cwd) },
164
- { type: 'review', findings: findings.length },
165
- s => ({ ...s, review: { at: new Date(now()).toISOString(), reviewers, findings } }),
166
- now
167
- )
168
-
169
- for (const f of findings) {
170
- log(`${severityIcon(f.severity)} ${f.file ?? '?'}: ${f.issue ?? ''}`)
171
- }
172
- const high = findings.filter(f => f.severity === 'high').length
173
- if (high > 0) log(`⚠️ review: ${high} high-severity — рекомендовано виправити перед release`)
174
- log(`review: ${findings.length} findings (рецензентів: ${reviewers})`)
175
- return 0
176
- }
@@ -1,44 +0,0 @@
1
- /**
2
- * Level-1 «Суддя» (spec §8.4): проганяє Quality Gates (§5) через **ін'єктований**
3
- * `run`-runner і повертає структурований verdict. Не знає про LLM/API-ключі —
4
- * чистий FS/Git/процеси. Один і той самий `runReview` обслуговує і Пасивний
5
- * Турнікет (`flow verify`), і Активний Раннер (per-step Ф4).
6
- *
7
- * Fail-fast: на першому проваленому gate зупиняємось. Fingerprint дерева
8
- * (`worktree-fingerprint`) фіксуємо лише на повному pass — щоб зберегти у стан і
9
- * пізніше ловити stale-результат (§5).
10
- */
11
- import { worktreeFingerprint } from '../../utils/worktree-fingerprint.mjs'
12
-
13
- /**
14
- * Канонічні gate-и verify (lint + coverage; coverage включає тести+мутації).
15
- * Обидва — scoped до змінених файлів: `lint` через quick-режим (`changed-files.mjs`),
16
- * `coverage --changed` через vitest `--changed`/Stryker `--mutate` по diff від base.
17
- * Турнікет (`flow verify`/per-step) перевіряє лише змінене; повний coverage —
18
- * окремо (`bun run coverage`, `/n-coverage-fix`).
19
- */
20
- export const DEFAULT_GATES = [
21
- { name: 'lint', cmd: ['npx', '@nitra/cursor', 'lint'] },
22
- { name: 'coverage', cmd: ['npx', '@nitra/cursor', 'coverage', '--changed'] }
23
- ]
24
-
25
- /**
26
- * Проганяє gate-и й повертає verdict.
27
- * @param {{ run: (cmd: string, args: string[], opts: object) => { status: number, stdout?: string, stderr?: string }, cwd: string, gates?: { name: string, cmd: string[] }[], fingerprint?: () => string | null }} input ін'єкції
28
- * @returns {{ pass: boolean, gates: { name: string, ok: boolean }[], failedOutput: string | null, fingerprint: string | null }} verdict
29
- */
30
- export function runReview({ run, cwd, gates = DEFAULT_GATES, fingerprint = () => worktreeFingerprint() }) {
31
- const results = []
32
- let failedOutput = null
33
- for (const g of gates) {
34
- const r = run(g.cmd[0], g.cmd.slice(1), { cwd })
35
- const ok = (r?.status ?? 1) === 0
36
- results.push({ name: g.name, ok })
37
- if (!ok) {
38
- failedOutput = `${r?.stdout ?? ''}\n${r?.stderr ?? ''}`.trim() || null
39
- break
40
- }
41
- }
42
- const pass = results.length === gates.length && results.every(x => x.ok)
43
- return { pass, gates: results, failedOutput, fingerprint: pass ? fingerprint() : null }
44
- }
@@ -1,58 +0,0 @@
1
- /**
2
- * Completion snapshot (spec §3 Ф5, §7): перед cleanup transient `.flow.json`
3
- * durable-слід задачі має пережити. Будуємо стислий summary і вписуємо його в
4
- * task record (`docs/tasks/<id>.md`) між HTML-маркерами (idempotent upsert).
5
- */
6
- import { existsSync, readFileSync, writeFileSync } from 'node:fs'
7
- import { isAbsolute } from 'node:path'
8
-
9
- const SUMMARY_START = '<!-- flow:summary:start -->'
10
- const SUMMARY_END = '<!-- flow:summary:end -->'
11
-
12
- /**
13
- * Будує completion snapshot зі стану.
14
- * @param {object} state стан `.flow.json`
15
- * @param {() => number} [now] фабрика часу (ms)
16
- * @returns {object} snapshot (status, branch, base_commit, gates, change, notified, finished_at)
17
- */
18
- export function buildCompletionSnapshot(state, now = Date.now) {
19
- return {
20
- status: state.status ?? 'done',
21
- branch: state.branch ?? null,
22
- base_commit: state.metadata?.base_commit ?? state.base_commit ?? null,
23
- gates: Object.fromEntries((state.gates ?? []).map(g => [g.name, g.ok ? 'ok' : 'fail'])),
24
- change: state.change ?? null,
25
- notified: state.notified ?? null,
26
- finished_at: new Date(now()).toISOString()
27
- }
28
- }
29
-
30
- /**
31
- * Вставляє/оновлює блок Summary в markdown-контенті (між маркерами).
32
- * @param {string} content вихідний markdown
33
- * @param {object} snapshot completion snapshot
34
- * @returns {string} оновлений markdown
35
- */
36
- export function upsertSummaryBlock(content, snapshot) {
37
- const block = `${SUMMARY_START}\n## Summary\n\`\`\`json\n${JSON.stringify(snapshot, null, 2)}\n\`\`\`\n${SUMMARY_END}`
38
- const i = content.indexOf(SUMMARY_START)
39
- const j = content.indexOf(SUMMARY_END)
40
- if (i !== -1 && j !== -1 && j > i) {
41
- return content.slice(0, i) + block + content.slice(j + SUMMARY_END.length)
42
- }
43
- return `${content.trimEnd()}\n\n${block}\n`
44
- }
45
-
46
- /**
47
- * Вписує snapshot у task record (створює файл, якщо його нема).
48
- * @param {string} taskPath абсолютний шлях `docs/tasks/<id>.md`
49
- * @param {object} snapshot completion snapshot
50
- * @returns {void}
51
- */
52
- export function writeSummaryToTaskRecord(taskPath, snapshot) {
53
- if (!isAbsolute(taskPath)) {
54
- throw new Error(`writeSummaryToTaskRecord: очікується абсолютний шлях (отримано: ${taskPath})`)
55
- }
56
- const content = existsSync(taskPath) ? readFileSync(taskPath, 'utf8') : ''
57
- writeFileSync(taskPath, upsertSummaryBlock(content, snapshot), 'utf8')
58
- }
@@ -1,97 +0,0 @@
1
- /**
2
- * `flow spec [--panel] [<spec.md>]` — фаза дизайну (Пасивний Турнікет, lifecycle
3
- * §3). Фіксує `docs/specs/<date>-<slug>.md` (дизайн із brainstorm) у стані й
4
- * верифікує ланцюг через read-only `trace`. Код не пише; лінки front-matter
5
- * пише агент за контрактом `flow.mdc`.
6
- *
7
- * Brainstorm: human↔agent — у діалозі IDE-агента (контракт); agent↔agent —
8
- * `--panel` (панель персон → суддя, синтез презентується людині).
9
- */
10
- import { existsSync, readFileSync } from 'node:fs'
11
- import { cwd as processCwd } from 'node:process'
12
-
13
- import { resolveArtifact, verifyTrace } from './artifact.mjs'
14
- import { flowEventsPath } from './events.mjs'
15
- import { runPanel } from './plan-panel.mjs'
16
- import { createRunner } from './subagent-runner.mjs'
17
- import { readState, recordTransition } from './state-store.mjs'
18
- import { resolveActiveFlowState } from './flow-resolve.mjs'
19
- import { parseFrontMatter } from '../trace.mjs'
20
-
21
- /** Допустимі значення ризику у spec-frontmatter. */
22
- const RISKS = new Set(['low', 'med', 'high'])
23
-
24
- /**
25
- * Зчитує `risk` зі spec-frontmatter, якщо валідний (інакше — поточний у стані).
26
- * Так risk-нотатка у spec керує глибиною подальшого `flow review`.
27
- * @param {string} doc шлях spec-doc
28
- * @param {string | undefined} current поточний risk у стані
29
- * @returns {string | undefined} ризик
30
- */
31
- function riskFromSpec(doc, current) {
32
- try {
33
- const fm = parseFrontMatter(readFileSync(doc, 'utf8'))
34
- return fm && RISKS.has(fm.risk) ? fm.risk : current
35
- } catch {
36
- return current
37
- }
38
- }
39
-
40
- /**
41
- * @param {string[]} rest аргументи (`--panel`, опц. `<spec.md>`)
42
- * @param {{ cwd?: string, log?: (m: string) => void, runner?: object, trace?: (cwd: string) => number, now?: () => number }} [deps] ін'єкції
43
- * @returns {Promise<number>} exit code (0 ok, 1 нема стану/доку)
44
- */
45
- export async function spec(rest, deps = {}) {
46
- const cwd0 = deps.cwd ?? processCwd()
47
- const log = deps.log ?? console.error
48
- const resolved = resolveActiveFlowState({ cwd: cwd0, branch: deps.branch }, deps)
49
- if (!resolved.statePath) {
50
- log(`spec: ${resolved.error}`)
51
- return 1
52
- }
53
- if (resolved.autoResolved) log(`flow: авторезолвлено активний flow «${resolved.label}» (cwd поза worktree)`)
54
- const cwd = resolved.worktreeDir ?? cwd0
55
- const statePath = resolved.statePath
56
- const state = readState(statePath)
57
- if (!state) {
58
- log('spec: стану нема — спершу `flow init`')
59
- return 1
60
- }
61
-
62
- if (rest.includes('--panel')) {
63
- let runner = deps.runner
64
- if (!runner) {
65
- try {
66
- runner = await createRunner(deps)
67
- } catch (error) {
68
- log(`spec: ${error.message}`)
69
- return 1
70
- }
71
- }
72
- const synth = await runPanel({ task: state.branch, cwd, runner, log, mode: 'spec' })
73
- if (synth) {
74
- log('spec: панель синтезувала підходи (нижче) — збережи дизайн у docs/specs/ і повтори `flow spec`:')
75
- log(typeof synth === 'string' ? synth : JSON.stringify(synth))
76
- }
77
- }
78
-
79
- const doc = rest.find(a => a.endsWith('.md')) ?? resolveArtifact(cwd, 'specs', state.branch)
80
- if (!doc || !existsSync(doc)) {
81
- log('spec: нема docs/specs/<date>-<slug>.md — спершу пройди brainstorm (див. flow.mdc)')
82
- return 1
83
- }
84
- if (!verifyTrace(cwd, deps.trace)) {
85
- log('⚠️ spec: trace виявив розрив ланцюга — перевір лінки front-matter (adr/spec/plan)')
86
- }
87
-
88
- const risk = riskFromSpec(doc, state.risk)
89
- recordTransition(
90
- { statePath, eventsPath: flowEventsPath(cwd) },
91
- { type: 'spec' },
92
- s => ({ ...s, spec_doc: doc, risk, status: 'spec' }),
93
- deps.now ?? Date.now
94
- )
95
- log(`spec: зафіксовано ${doc} → status: spec (risk ${risk ?? '—'})`)
96
- return 0
97
- }