workflow-ai 1.0.22 → 1.0.24

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.
@@ -97,6 +97,12 @@ pipeline:
97
97
  workdir: "."
98
98
  description: "Скрипт для проверки условий тикетов в backlog/"
99
99
 
100
+ script-check-plan-decomposed:
101
+ command: "node"
102
+ args: [".workflow/src/scripts/check-plan-decomposed.js"]
103
+ workdir: "."
104
+ description: "Скрипт для проверки, декомпозирован ли текущий план"
105
+
100
106
  script-move-to-review:
101
107
  command: "node"
102
108
  args: [".workflow/src/scripts/move-to-review.js"]
@@ -167,9 +173,40 @@ pipeline:
167
173
  stage: move-to-review
168
174
  params:
169
175
  ticket_id: "$result.ticket_id"
170
- empty: check-conditions
176
+ empty: check-plan-decomposition
171
177
  error: create-report
172
178
 
179
+ # -------------------------------------------------------------------------
180
+ # 0b. check-plan-decomposition
181
+ # Если нет тикетов — проверяет, есть ли недекомпозированный план.
182
+ # Если план не декомпозирован — декомпозирует перед check-conditions.
183
+ # -------------------------------------------------------------------------
184
+ check-plan-decomposition:
185
+ description: "Проверить, декомпозирован ли текущий план"
186
+ agent: script-check-plan-decomposed
187
+ goto:
188
+ needs_decomposition:
189
+ stage: decompose-plan
190
+ params:
191
+ plan_file: "$result.plan_file"
192
+ decomposed: check-conditions
193
+ no_plan: check-conditions
194
+ default: check-conditions
195
+ error: check-conditions
196
+
197
+ # -------------------------------------------------------------------------
198
+ # 0c. decompose-plan
199
+ # Декомпозирует план на тикеты в backlog/
200
+ # -------------------------------------------------------------------------
201
+ decompose-plan:
202
+ description: "Декомпозировать план на тикеты"
203
+ agent: claude-sonnet
204
+ fallback_agent: qwen-code
205
+ skill: decompose-plan
206
+ instructions: "Декомпозируй план .workflow/$context.plan_file на тикеты."
207
+ goto:
208
+ default: check-conditions
209
+
173
210
  # -------------------------------------------------------------------------
174
211
  # 1. check-conditions
175
212
  # Проверяет условия тикетов в backlog/, выводит список готовых
@@ -482,6 +519,6 @@ pipeline:
482
519
  # ===========================================================================
483
520
  execution:
484
521
  max_steps: 1500
485
- delay_between_stages: 1
522
+ delay_between_stages: 5
486
523
  timeout_per_stage: 1800
487
524
  log_file: ".workflow/logs/pipeline.log"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workflow-ai",
3
- "version": "1.0.22",
3
+ "version": "1.0.24",
4
4
  "description": "AI Agent Workflow Coordinator — kanban-based pipeline for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
package/src/lib/utils.mjs CHANGED
@@ -117,10 +117,16 @@ export function getLastReviewStatus(content) {
117
117
  if (!content) return null;
118
118
 
119
119
  // Находим секцию "## Ревью" — захватываем всё до следующего заголовка ## или конца файла
120
- const reviewSectionMatch = content.match(/^##\s*Ревью\s*\n([\s\S]*)(?=\n^##\s|$)/m);
121
- if (!reviewSectionMatch) return null;
120
+ const headerIdx = content.search(/^##\s*Ревью\s*$/m);
121
+ if (headerIdx === -1) return null;
122
122
 
123
- const reviewSection = reviewSectionMatch[1].trim();
123
+ const bodyStart = content.indexOf('\n', headerIdx);
124
+ if (bodyStart === -1) return null;
125
+
126
+ const nextH2 = content.indexOf('\n## ', bodyStart);
127
+ const reviewSection = (nextH2 === -1
128
+ ? content.slice(bodyStart + 1)
129
+ : content.slice(bodyStart + 1, nextH2)).trim();
124
130
  if (!reviewSection) return null;
125
131
 
126
132
  // Пробуем распарсить табличный формат
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * check-plan-decomposed.js — Проверяет, есть ли недекомпозированные планы.
5
+ *
6
+ * Логика:
7
+ * A) Если plan_id задан — проверяет только этот план
8
+ * B) Если plan_id НЕ задан — сканирует все планы в plans/current/
9
+ *
10
+ * Для каждого плана:
11
+ * 1. Если есть тикеты (backlog/, ready/, in-progress/, review/) с parent_plan == planId — decomposed
12
+ * 2. Иначе — needs_decomposition
13
+ *
14
+ * Результат:
15
+ * - needs_decomposition + plan_file — найден первый недекомпозированный план
16
+ * - decomposed — все планы декомпозированы
17
+ * - no_plan — нет планов в plans/current/
18
+ *
19
+ * Использование:
20
+ * node check-plan-decomposed.js "plan_id: PLAN-007"
21
+ * node check-plan-decomposed.js
22
+ */
23
+
24
+ import fs from 'fs';
25
+ import path from 'path';
26
+ import { findProjectRoot } from '../lib/find-root.mjs';
27
+ import { parseFrontmatter, printResult, normalizePlanId, extractPlanId } from '../lib/utils.mjs';
28
+
29
+ const PROJECT_DIR = findProjectRoot();
30
+ const WORKFLOW_DIR = path.join(PROJECT_DIR, '.workflow');
31
+ const TICKETS_DIR = path.join(WORKFLOW_DIR, 'tickets');
32
+ const PLANS_DIR = path.join(WORKFLOW_DIR, 'plans', 'current');
33
+
34
+ const TICKET_DIRS = ['backlog', 'ready', 'in-progress', 'review'];
35
+
36
+ /**
37
+ * Проверяет, есть ли тикеты, привязанные к данному плану
38
+ */
39
+ function hasTicketsForPlan(planId) {
40
+ for (const dir of TICKET_DIRS) {
41
+ const dirPath = path.join(TICKETS_DIR, dir);
42
+ if (!fs.existsSync(dirPath)) continue;
43
+
44
+ const files = fs.readdirSync(dirPath)
45
+ .filter(f => f.endsWith('.md') && f !== '.gitkeep.md');
46
+
47
+ for (const file of files) {
48
+ try {
49
+ const content = fs.readFileSync(path.join(dirPath, file), 'utf8');
50
+ const { frontmatter } = parseFrontmatter(content);
51
+ if (normalizePlanId(frontmatter.parent_plan) === planId) {
52
+ return true;
53
+ }
54
+ } catch (e) {
55
+ console.error(`[WARN] Failed to read ${dir}/${file}: ${e.message}`);
56
+ }
57
+ }
58
+ }
59
+ return false;
60
+ }
61
+
62
+ /**
63
+ * Находит файл плана в plans/current/
64
+ */
65
+ function findPlanFile(planId) {
66
+ if (!fs.existsSync(PLANS_DIR)) return null;
67
+
68
+ const expectedName = `${planId}.md`;
69
+ const filePath = path.join(PLANS_DIR, expectedName);
70
+ if (fs.existsSync(filePath)) {
71
+ return `plans/current/${expectedName}`;
72
+ }
73
+
74
+ // Поиск по всем файлам на случай другого именования
75
+ const files = fs.readdirSync(PLANS_DIR).filter(f => f.endsWith('.md'));
76
+ for (const file of files) {
77
+ if (normalizePlanId(file) === planId) {
78
+ return `plans/current/${file}`;
79
+ }
80
+ }
81
+
82
+ return null;
83
+ }
84
+
85
+ /**
86
+ * Возвращает все файлы планов из plans/current/
87
+ */
88
+ function getAllPlanFiles() {
89
+ if (!fs.existsSync(PLANS_DIR)) return [];
90
+
91
+ return fs.readdirSync(PLANS_DIR)
92
+ .filter(f => f.endsWith('.md'))
93
+ .map(f => ({
94
+ planId: normalizePlanId(f),
95
+ planFile: `plans/current/${f}`
96
+ }))
97
+ .filter(p => p.planId !== null);
98
+ }
99
+
100
+ async function main() {
101
+ const planId = extractPlanId();
102
+
103
+ if (planId) {
104
+ // Режим A: конкретный план
105
+ console.log(`[INFO] Checking decomposition for plan: ${planId}`);
106
+
107
+ const planFile = findPlanFile(planId);
108
+ if (!planFile) {
109
+ console.log(`[INFO] Plan ${planId} not found in plans/current/`);
110
+ printResult({ status: 'no_plan' });
111
+ return;
112
+ }
113
+
114
+ console.log(`[INFO] Found plan file: ${planFile}`);
115
+
116
+ if (hasTicketsForPlan(planId)) {
117
+ console.log(`[INFO] Plan ${planId} already has tickets — decomposed`);
118
+ printResult({ status: 'decomposed' });
119
+ return;
120
+ }
121
+
122
+ console.log(`[INFO] Plan ${planId} has no tickets — needs decomposition`);
123
+ printResult({ status: 'needs_decomposition', plan_file: planFile });
124
+ return;
125
+ }
126
+
127
+ // Режим B: сканируем все планы в plans/current/
128
+ console.log('[INFO] No plan_id specified, scanning all plans in plans/current/');
129
+
130
+ const allPlans = getAllPlanFiles();
131
+ if (allPlans.length === 0) {
132
+ console.log('[INFO] No plans found in plans/current/');
133
+ printResult({ status: 'no_plan' });
134
+ return;
135
+ }
136
+
137
+ console.log(`[INFO] Found ${allPlans.length} plan(s) in plans/current/`);
138
+
139
+ for (const { planId: pid, planFile } of allPlans) {
140
+ if (!hasTicketsForPlan(pid)) {
141
+ console.log(`[INFO] Plan ${pid} has no tickets — needs decomposition`);
142
+ printResult({ status: 'needs_decomposition', plan_file: planFile });
143
+ return;
144
+ }
145
+ console.log(`[INFO] Plan ${pid} already decomposed`);
146
+ }
147
+
148
+ console.log('[INFO] All plans are decomposed');
149
+ printResult({ status: 'decomposed' });
150
+ }
151
+
152
+ main().catch(e => {
153
+ console.error(`[ERROR] ${e.message}`);
154
+ printResult({ status: 'error', error: e.message });
155
+ process.exit(1);
156
+ });
@@ -83,6 +83,8 @@ ID тикета передаётся в промпте как `ticket_id` в с
83
83
 
84
84
  ### 4. Сформировать вердикт
85
85
 
86
+ > **ВАЖНО:** Review-result НИКОГДА не пишет статус `skipped`. Допустимы только `passed` или `failed`. Статус `skipped` — прерогатива check-relevance.
87
+
86
88
  Возвращать структурированный результат строго в одном из двух форматов:
87
89
 
88
90
  > **КРИТИЧНО**: `status` принимает ТОЛЬКО два значения: `passed` или `failed`.