workflow-ai 1.0.23 → 1.0.25

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/, выводит список готовых
@@ -380,6 +417,7 @@ pipeline:
380
417
  timeout: 120
381
418
  goto:
382
419
  default: pick-next-task
420
+ error: pick-next-task
383
421
 
384
422
  # -------------------------------------------------------------------------
385
423
  # 6. create-report
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workflow-ai",
3
- "version": "1.0.23",
3
+ "version": "1.0.25",
4
4
  "description": "AI Agent Workflow Coordinator — kanban-based pipeline for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
+ });