@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.
- package/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/scripts/dispatcher/index.mjs +20 -61
- package/scripts/dispatcher/lib/flow-plan.mjs +153 -0
- package/scripts/dispatcher/lib/flow-signals.mjs +235 -0
- package/scripts/dispatcher/lib/flow-verify.mjs +127 -0
- package/scripts/worktree-cli.mjs +2 -2
- package/skills/docgen/js/docgen-gen.mjs +42 -125
- package/scripts/dispatcher/lib/active.mjs +0 -222
- package/scripts/dispatcher/lib/artifact.mjs +0 -79
- package/scripts/dispatcher/lib/budget.mjs +0 -36
- package/scripts/dispatcher/lib/capability.mjs +0 -59
- package/scripts/dispatcher/lib/commands.mjs +0 -296
- package/scripts/dispatcher/lib/flow-lock.mjs +0 -39
- package/scripts/dispatcher/lib/gate.mjs +0 -91
- package/scripts/dispatcher/lib/level.mjs +0 -135
- package/scripts/dispatcher/lib/plan.mjs +0 -88
- package/scripts/dispatcher/lib/planner.mjs +0 -73
- package/scripts/dispatcher/lib/review.mjs +0 -176
- package/scripts/dispatcher/lib/reviewer.mjs +0 -44
- package/scripts/dispatcher/lib/snapshot.mjs +0 -58
- package/scripts/dispatcher/lib/spec.mjs +0 -97
|
@@ -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
|
-
}
|