@nitra/cursor 3.6.1 → 3.8.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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.8.0] - 2026-06-01
4
+
5
+ ### Added
6
+
7
+ - flow: контракт — advanced elicitation (меню технік поглиблення вимог у фазі spec: Expand/Contract, Critique&Refine, Identify Risks, Tree-of-Thoughts, Stakeholder Roundtable, Self-Consistency; Elicitation History у спеці)
8
+
9
+ ## [3.7.0] - 2026-06-01
10
+
11
+ ### Added
12
+
13
+ - flow: risk-aware глибина review — detectRisk у init + risk зі spec-frontmatter керують кількістю рецензентів (max за рівнем і ризиком) і безпековим фокусом промпта
14
+
3
15
  ## [3.6.1] - 2026-06-01
4
16
 
5
17
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "3.6.1",
3
+ "version": "3.8.0",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -21,9 +21,11 @@ Composer, Claude Code) працює **Пасивний Турнікет**: ти,
21
21
 
22
22
  Створює ізольований worktree (`.worktrees/<branch>/`) і стан задачі. Якщо ти
23
23
  вже в worktree — новий не вкладається. `init` визначає **рівень** задачі
24
- (L0 тривіальне … L3 архітектурне) за описом: для L0 (fix/typo/bump) фази
25
- Spec/План можна пропустити; для L≥1 вони рекомендовані. Рівень також керує
26
- глибиною `review`.
24
+ (L0 тривіальне … L3 архітектурне) і **ризик** (low/med/high — за описом:
25
+ security/auth/secret high) за описом: для L0 (fix/typo/bump) фази Spec/План
26
+ можна пропустити; для L≥1 вони рекомендовані. Рівень **і ризик** разом керують
27
+ глибиною та фокусом `review`. Ризик можна уточнити полем `risk:` у frontmatter
28
+ spec — `flow spec` його підхопить.
27
29
 
28
30
  2. **Spec (дизайн)** — рекомендовано, не блокує. Brainstorm нашими термінами
29
31
  (НЕ викликаючи superpowers):
@@ -33,6 +35,20 @@ Composer, Claude Code) працює **Пасивний Турнікет**: ти,
33
35
  - **agent↔agent:** `npx @nitra/cursor flow spec --panel` — панель персон
34
36
  (architect/skeptic/tester) → суддя-синтез; презентуй синтез людині.
35
37
 
38
+ **Поглиблення (advanced elicitation, за потреби).** Якщо чернетка дизайну
39
+ «сира» чи є непевність — запропонуй людині одну з технік (по одній за раз),
40
+ застосуй обрану, повтори до «досить»:
41
+
42
+ - **Expand / Contract** — розгорнути деталь, що бракує, або згорнути зайве до суті;
43
+ - **Critique & Refine** — самокритика чернетки (слабкі місця, припущення), тоді доопрацювання;
44
+ - **Identify Risks** — явно перелічити ризики/граничні випадки (наповнює поле `risk:` у frontmatter);
45
+ - **Tree-of-Thoughts** — кілька гілок рішення паралельно, обрати найкращу з обґрунтуванням;
46
+ - **Stakeholder Roundtable** — пройтись поглядами ролей (user / ops / security / maintainer);
47
+ - **Self-Consistency** — 2-3 незалежні версії відповіді, звірити на суперечності.
48
+
49
+ Що застосовано — фіксуй коротко в секції `## Elicitation History` спеки
50
+ (traceability рішень).
51
+
36
52
  Збережи дизайн → `docs/specs/<date>-<slug>.md` (`kind: nitra-spec`,
37
53
  `plan: null`), тоді зафіксуй:
38
54
 
@@ -72,9 +88,10 @@ Composer, Claude Code) працює **Пасивний Турнікет**: ти,
72
88
  ```
73
89
 
74
90
  Незалежний субагент читає ЛИШЕ `git diff` від `base_commit` і шукає логічні
75
- баги/ризики, яких не ловлять механічні гейти. Кількість рецензентів — за
76
- рівнем (L0→1 … L3→3). Findings пишуться у `.flow.json` (не блокує); high-
77
- severity варто виправити перед фінішем.
91
+ баги/ризики, яких не ловлять механічні гейти. Кількість рецензентів —
92
+ `max(рівень, ризик)` (L0→1 … L3→3; high-risk 3, з безпековим фокусом
93
+ промпта). Findings пишуться у `.flow.json` (не блокує); high-severity варто
94
+ виправити перед фінішем.
78
95
 
79
96
  7. **На провал** — виправ код за виводом і виклич `flow verify` знову. Максимум
80
97
  **3 спроби**; якщо не вдається — зупинись і поклич людину.
@@ -12,7 +12,7 @@ import { cwd as processCwd } from 'node:process'
12
12
  import { worktreePaths } from '../../lib/worktree.mjs'
13
13
  import { worktreeFingerprint } from '../../utils/worktree-fingerprint.mjs'
14
14
  import { flowEventsPath } from './events.mjs'
15
- import { detectLevel } from './level.mjs'
15
+ import { detectLevel, detectRisk } from './level.mjs'
16
16
  import { runReview } from './reviewer.mjs'
17
17
  import { buildCompletionSnapshot, writeSummaryToTaskRecord } from './snapshot.mjs'
18
18
  import { flowStatePath, readState, recordTransition, writeState } from './state-store.mjs'
@@ -103,15 +103,17 @@ export async function init(rest, deps = {}) {
103
103
  const log = deps.log ?? console.error
104
104
  const statePath = flowStatePath(ew.worktreeDir)
105
105
  const level = detectLevel(ew.desc)
106
+ const risk = detectRisk(ew.desc)
106
107
  writeState(statePath, {
107
108
  branch: ew.branch,
108
109
  status: 'in_progress',
109
110
  started_at: new Date(now()).toISOString(),
110
111
  metadata: { base_commit: ew.baseCommit },
111
112
  level,
113
+ risk,
112
114
  plan: []
113
115
  })
114
- log(`init: ${ew.branch} (level ${level}) → ${statePath}`)
116
+ log(`init: ${ew.branch} (level ${level}, risk ${risk}) → ${statePath}`)
115
117
  return 0
116
118
  }
117
119
 
@@ -1,7 +1,8 @@
1
1
  /**
2
- * Scale-adaptive рівень задачі (ідея з BMAD project-levels, у наших термінах).
3
- * `init` визначає рівень за описом; рівень right-size'ить, скільки adversarial-
4
- * рецензентів спавнить `flow review`, і які фази рекомендовані (контракт).
2
+ * Scale-adaptive рівень + ризик задачі (ідея з BMAD project-levels/risk-profile,
3
+ * у наших термінах). `init` визначає рівень і ризик за описом; разом вони
4
+ * right-size'ять, скільки adversarial-рецензентів спавнить `flow review` (і його
5
+ * фокус), і які фази рекомендовані (контракт).
5
6
  *
6
7
  * Детекція — підрядками (case-insensitive), без regex (уникаємо slow-regex і
7
8
  * проблем зі словомежами для кирилиці).
@@ -30,7 +31,7 @@ export function detectLevel(desc) {
30
31
  }
31
32
 
32
33
  /**
33
- * Скільки adversarial-рецензентів спавнити для рівня (глибина review за ризиком).
34
+ * Скільки adversarial-рецензентів спавнити для рівня (глибина review за розміром).
34
35
  * @param {number} level рівень 0..3
35
36
  * @returns {number} кількість рецензентів (1..3)
36
37
  */
@@ -39,3 +40,42 @@ export function reviewersForLevel(level) {
39
40
  if (level === 2) return 2
40
41
  return 1
41
42
  }
43
+
44
+ /** Ключові слова високого ризику (безпека/гроші/доступи). */
45
+ const HIGH_RISK_KEYS = ['security', 'auth', 'crypto', 'payment', 'secret', 'token', 'permission', 'password', 'безпек']
46
+ /** Ключові слова середнього ризику (дані/незворотність). */
47
+ const MED_RISK_KEYS = ['data', ' db', 'database', 'migration', 'delete', 'gateway', 'міграц', 'видален']
48
+
49
+ /**
50
+ * Рівень ризику задачі за описом: low | med | high.
51
+ * @param {string} desc опис задачі
52
+ * @returns {'low' | 'med' | 'high'} ризик
53
+ */
54
+ export function detectRisk(desc) {
55
+ const d = String(desc ?? '').toLowerCase()
56
+ const has = keys => keys.some(k => d.includes(k))
57
+ if (has(HIGH_RISK_KEYS)) return 'high'
58
+ if (has(MED_RISK_KEYS)) return 'med'
59
+ return 'low'
60
+ }
61
+
62
+ /**
63
+ * Скільки рецензентів диктує сам ризик.
64
+ * @param {string} risk low|med|high
65
+ * @returns {number} 1..3
66
+ */
67
+ export function reviewersForRisk(risk) {
68
+ if (risk === 'high') return 3
69
+ if (risk === 'med') return 2
70
+ return 1
71
+ }
72
+
73
+ /**
74
+ * Підсумкова глибина review: максимум вимог за рівнем і за ризиком (кап 3).
75
+ * @param {number} level рівень 0..3
76
+ * @param {string} [risk] low|med|high
77
+ * @returns {number} кількість рецензентів (1..3)
78
+ */
79
+ export function reviewersFor(level, risk) {
80
+ return Math.min(3, Math.max(reviewersForLevel(level), reviewersForRisk(risk)))
81
+ }
@@ -11,7 +11,7 @@ import { cwd as processCwd } from 'node:process'
11
11
 
12
12
  import { realRun } from './commands.mjs'
13
13
  import { flowEventsPath } from './events.mjs'
14
- import { reviewersForLevel } from './level.mjs'
14
+ import { reviewersFor } from './level.mjs'
15
15
  import { flowStatePath, readState, recordTransition } from './state-store.mjs'
16
16
  import { createRunner } from './subagent-runner.mjs'
17
17
 
@@ -32,18 +32,27 @@ export function diffFromBase(base, run, cwd) {
32
32
  }
33
33
 
34
34
  /**
35
- * Промпт adversarial-рецензента (читає ЛИШЕ diff).
35
+ * Промпт adversarial-рецензента (читає ЛИШЕ diff). Для high-risk додає
36
+ * безпекову лінзу.
36
37
  * @param {string} diff текст diff
38
+ * @param {string} [risk] low|med|high — фокус перевірки
37
39
  * @returns {string} промпт
38
40
  */
39
- export function reviewerPrompt(diff) {
41
+ export function reviewerPrompt(diff, risk) {
42
+ const lens =
43
+ risk === 'high'
44
+ ? 'ОСОБЛИВА УВАГА БЕЗПЕЦІ: auth/доступи, секрети/токени, ін\'єкції, валідація входу, незворотні операції.'
45
+ : ''
40
46
  return [
41
47
  'Ти — прискіпливий adversarial-рецензент. Знайди баги, ризики й smells ЛИШЕ в цьому diff.',
48
+ lens,
42
49
  'Поверни ЛИШЕ JSON-масив: [{ "severity": "high|med|low", "file": "...", "issue": "...", "suggestion": "..." }].',
43
50
  'Якщо проблем нема — поверни [].',
44
51
  '',
45
52
  diff.slice(0, DIFF_LIMIT)
46
- ].join('\n')
53
+ ]
54
+ .filter(Boolean)
55
+ .join('\n')
47
56
  }
48
57
 
49
58
  /**
@@ -128,8 +137,8 @@ export async function review(_rest, deps = {}) {
128
137
  }
129
138
  }
130
139
 
131
- const reviewers = reviewersForLevel(state.level ?? 1)
132
- const prompt = reviewerPrompt(diff)
140
+ const reviewers = reviewersFor(state.level ?? 1, state.risk)
141
+ const prompt = reviewerPrompt(diff, state.risk)
133
142
  const results = await Promise.all(Array.from({ length: reviewers }, () => runner.runStep(prompt, { cwd })))
134
143
  const findings = dedupeFindings(results.flatMap(r => (r.ok ? parseFindings(r.output) : [])))
135
144
 
@@ -7,7 +7,7 @@
7
7
  * Brainstorm: human↔agent — у діалозі IDE-агента (контракт); agent↔agent —
8
8
  * `--panel` (панель персон → суддя, синтез презентується людині).
9
9
  */
10
- import { existsSync } from 'node:fs'
10
+ import { existsSync, readFileSync } from 'node:fs'
11
11
  import { cwd as processCwd } from 'node:process'
12
12
 
13
13
  import { resolveArtifact, verifyTrace } from './artifact.mjs'
@@ -15,6 +15,26 @@ import { flowEventsPath } from './events.mjs'
15
15
  import { runPanel } from './plan-panel.mjs'
16
16
  import { createRunner } from './subagent-runner.mjs'
17
17
  import { flowStatePath, readState, recordTransition } from './state-store.mjs'
18
+ import { parseFrontMatter } from '../trace.mjs'
19
+
20
+ /** Допустимі значення ризику у spec-frontmatter. */
21
+ const RISKS = new Set(['low', 'med', 'high'])
22
+
23
+ /**
24
+ * Зчитує `risk` зі spec-frontmatter, якщо валідний (інакше — поточний у стані).
25
+ * Так risk-нотатка у spec керує глибиною подальшого `flow review`.
26
+ * @param {string} doc шлях spec-doc
27
+ * @param {string | undefined} current поточний risk у стані
28
+ * @returns {string | undefined} ризик
29
+ */
30
+ function riskFromSpec(doc, current) {
31
+ try {
32
+ const fm = parseFrontMatter(readFileSync(doc, 'utf8'))
33
+ return fm && RISKS.has(fm.risk) ? fm.risk : current
34
+ } catch {
35
+ return current
36
+ }
37
+ }
18
38
 
19
39
  /**
20
40
  * @param {string[]} rest аргументи (`--panel`, опц. `<spec.md>`)
@@ -57,12 +77,13 @@ export async function spec(rest, deps = {}) {
57
77
  log('⚠️ spec: trace виявив розрив ланцюга — перевір лінки front-matter (adr/spec/plan)')
58
78
  }
59
79
 
80
+ const risk = riskFromSpec(doc, state.risk)
60
81
  recordTransition(
61
82
  { statePath, eventsPath: flowEventsPath(cwd) },
62
83
  { type: 'spec' },
63
- s => ({ ...s, spec_doc: doc, status: 'spec' }),
84
+ s => ({ ...s, spec_doc: doc, risk, status: 'spec' }),
64
85
  deps.now ?? Date.now
65
86
  )
66
- log(`spec: зафіксовано ${doc} → status: spec`)
87
+ log(`spec: зафіксовано ${doc} → status: spec (risk ${risk ?? '—'})`)
67
88
  return 0
68
89
  }