workflow-ai 1.0.0 → 1.0.2

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/package.json CHANGED
@@ -1,14 +1,17 @@
1
1
  {
2
2
  "name": "workflow-ai",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "AI Agent Workflow Coordinator — kanban-based pipeline for AI coding agents",
5
5
  "type": "module",
6
- "bin": { "workflow": "./bin/workflow.mjs" },
6
+ "bin": {
7
+ "workflow": "./bin/workflow.mjs"
8
+ },
7
9
  "files": [
8
10
  "bin/",
9
11
  "src/cli.mjs",
10
12
  "src/init.mjs",
11
13
  "src/runner.mjs",
14
+ "src/wf-loader.mjs",
12
15
  "src/lib/",
13
16
  "src/scripts/",
14
17
  "src/skills/",
@@ -16,9 +19,13 @@
16
19
  "configs/",
17
20
  "agent-templates/"
18
21
  ],
19
- "engines": { "node": ">=18.0.0" },
22
+ "engines": {
23
+ "node": ">=18.0.0"
24
+ },
20
25
  "scripts": {
21
26
  "test": "node --test src/tests/*.test.mjs"
22
27
  },
23
- "dependencies": { "js-yaml": "^4.1.0" }
28
+ "dependencies": {
29
+ "js-yaml": "^4.1.0"
30
+ }
24
31
  }
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * check-conditions.js — Проверяет условия тикетов в backlog/ и выводит список готовых
5
+ *
6
+ * Использование:
7
+ * node check-conditions.js
8
+ *
9
+ * Выводит результат в формате:
10
+ * ---RESULT---
11
+ * status: has_ready
12
+ * ready_tickets: IMPL-002, DOCS-001
13
+ * ---RESULT---
14
+ *
15
+ * или если готовых нет, но есть тикеты в ready/:
16
+ * ---RESULT---
17
+ * status: default
18
+ * ready_tickets:
19
+ * ---RESULT---
20
+ *
21
+ * или если backlog пуст и нет тикетов в ready/:
22
+ * ---RESULT---
23
+ * status: empty
24
+ * ready_tickets:
25
+ * ---RESULT---
26
+ */
27
+
28
+ import fs from 'fs';
29
+ import path from 'path';
30
+ import { findProjectRoot } from '../lib/find-root.mjs';
31
+ import { parseFrontmatter, printResult } from '../lib/utils.mjs';
32
+
33
+ const PROJECT_DIR = findProjectRoot();
34
+ const WORKFLOW_DIR = path.join(PROJECT_DIR, '.workflow');
35
+ const TICKETS_DIR = path.join(WORKFLOW_DIR, 'tickets');
36
+ const BACKLOG_DIR = path.join(TICKETS_DIR, 'backlog');
37
+ const READY_DIR = path.join(TICKETS_DIR, 'ready');
38
+ const DONE_DIR = path.join(TICKETS_DIR, 'done');
39
+
40
+ /**
41
+ * Проверяет одно условие тикета
42
+ */
43
+ function checkCondition(condition) {
44
+ const { type, value } = condition;
45
+
46
+ switch (type) {
47
+ case 'file_exists':
48
+ return fs.existsSync(path.join(PROJECT_DIR, value));
49
+
50
+ case 'file_not_exists':
51
+ return !fs.existsSync(path.join(PROJECT_DIR, value));
52
+
53
+ case 'tasks_completed': {
54
+ const ids = Array.isArray(value) ? value : [value];
55
+ return ids.every(taskId => fs.existsSync(path.join(DONE_DIR, `${taskId}.md`)));
56
+ }
57
+
58
+ case 'date_after':
59
+ return new Date() > new Date(value);
60
+
61
+ case 'date_before':
62
+ return new Date() < new Date(value);
63
+
64
+ case 'manual_approval':
65
+ return false;
66
+
67
+ default:
68
+ console.error(`[WARN] Unknown condition type: ${type}`);
69
+ return true;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Проверяет зависимости тикета
75
+ */
76
+ function checkDependencies(dependencies) {
77
+ if (!dependencies || dependencies.length === 0) return true;
78
+ return dependencies.every(depId => fs.existsSync(path.join(DONE_DIR, `${depId}.md`)));
79
+ }
80
+
81
+ /**
82
+ * Считывает все тикеты из директории
83
+ */
84
+ function readTickets(dir) {
85
+ if (!fs.existsSync(dir)) return [];
86
+
87
+ const tickets = [];
88
+ const files = fs.readdirSync(dir).filter(f => f.endsWith('.md') && f !== '.gitkeep.md');
89
+
90
+ for (const file of files) {
91
+ const filePath = path.join(dir, file);
92
+ try {
93
+ const content = fs.readFileSync(filePath, 'utf8');
94
+ const { frontmatter } = parseFrontmatter(content);
95
+ tickets.push({ id: frontmatter.id || file.replace('.md', ''), frontmatter });
96
+ } catch (e) {
97
+ console.error(`[WARN] Failed to read ticket ${file}: ${e.message}`);
98
+ }
99
+ }
100
+
101
+ return tickets;
102
+ }
103
+
104
+ /**
105
+ * Нормализует входное значение в формат PLAN-NNN.
106
+ * Принимает: "PLAN-007", "7", "007", "plan-7", "plans/PLAN-007.md", "/abs/path/PLAN-007.md"
107
+ */
108
+ function normalizePlanId(raw) {
109
+ if (!raw) return null;
110
+
111
+ // Извлекаем имя файла если передан путь
112
+ const basename = path.basename(raw, '.md');
113
+
114
+ // Уже в формате PLAN-NNN (регистронезависимо)
115
+ const full = basename.match(/^plan-(\d+)$/i);
116
+ if (full) return `PLAN-${String(parseInt(full[1], 10)).padStart(3, '0')}`;
117
+
118
+ // Просто цифра или число: "7", "007"
119
+ const num = raw.trim().match(/^(\d+)$/);
120
+ if (num) return `PLAN-${String(parseInt(num[1], 10)).padStart(3, '0')}`;
121
+
122
+ return null;
123
+ }
124
+
125
+ /**
126
+ * Извлекает plan_id из аргументов командной строки (контекст пайплайна)
127
+ */
128
+ function extractPlanId() {
129
+ const prompt = process.argv.slice(2)[0] || '';
130
+ const match = prompt.match(/plan_id:\s*(\S+)/i);
131
+ return match ? normalizePlanId(match[1]) : null;
132
+ }
133
+
134
+ /**
135
+ * Проверяет все тикеты в backlog/ и возвращает список готовых
136
+ */
137
+ function checkBacklog(planId) {
138
+ const allTickets = readTickets(BACKLOG_DIR);
139
+ const tickets = planId
140
+ ? allTickets.filter(t => t.frontmatter.parent_plan === planId)
141
+ : allTickets;
142
+
143
+ const ready = [];
144
+ const waiting = [];
145
+
146
+ for (const ticket of tickets) {
147
+ const { frontmatter, id } = ticket;
148
+ const conditions = frontmatter.conditions || [];
149
+ const dependencies = frontmatter.dependencies || [];
150
+
151
+ const depsMet = checkDependencies(dependencies);
152
+ const conditionsMet = conditions.every(checkCondition);
153
+
154
+ if (depsMet && conditionsMet) {
155
+ ready.push(id);
156
+ } else {
157
+ const reasons = [];
158
+ if (!depsMet) reasons.push(`ждёт зависимости: ${dependencies.join(', ')}`);
159
+ conditions.forEach(c => {
160
+ if (!checkCondition(c)) reasons.push(`условие не выполнено: ${c.type}`);
161
+ });
162
+ waiting.push({ id, reasons });
163
+ }
164
+ }
165
+
166
+ return { ready, waiting, total: tickets.length };
167
+ }
168
+
169
+ async function main() {
170
+ const planId = extractPlanId();
171
+
172
+ if (planId) {
173
+ console.log(`[INFO] Filtering by plan_id: ${planId}`);
174
+ }
175
+
176
+ console.log(`[INFO] Scanning backlog/: ${BACKLOG_DIR}`);
177
+
178
+ const { ready, waiting, total } = checkBacklog(planId);
179
+
180
+ console.log(`[INFO] Total in backlog${planId ? ` (plan ${planId})` : ''}: ${total}`);
181
+ console.log(`[INFO] Ready: ${ready.length}, Waiting: ${waiting.length}`);
182
+
183
+ if (ready.length > 0) {
184
+ console.log(`[INFO] Ready tickets: ${ready.join(', ')}`);
185
+ }
186
+
187
+ for (const { id, reasons } of waiting) {
188
+ console.log(`[INFO] ${id}: ${reasons.join('; ')}`);
189
+ }
190
+
191
+ if (ready.length > 0) {
192
+ printResult({ status: 'has_ready', ready_tickets: ready.join(', ') });
193
+ return;
194
+ }
195
+
196
+ // Нет готовых — проверяем есть ли что-то в ready/
197
+ const readyDirTickets = readTickets(READY_DIR);
198
+ if (readyDirTickets.length > 0) {
199
+ console.log(`[INFO] No new ready tickets, but ready/ has ${readyDirTickets.length} ticket(s)`);
200
+ printResult({ status: 'default', ready_tickets: '' });
201
+ } else {
202
+ console.log('[INFO] No ready tickets and ready/ is empty');
203
+ printResult({ status: 'empty', ready_tickets: '' });
204
+ }
205
+ }
206
+
207
+ main().catch(e => {
208
+ console.error(`[ERROR] ${e.message}`);
209
+ printResult({ status: 'error', error: e.message });
210
+ process.exit(1);
211
+ });
@@ -140,7 +140,6 @@ async function moveTicket(ticketId, target) {
140
140
 
141
141
  // Обновление frontmatter
142
142
  const now = new Date().toISOString();
143
- frontmatter.status = target;
144
143
  frontmatter.updated_at = now;
145
144
 
146
145
  // Если переход в done, добавляем completed_at
@@ -51,7 +51,6 @@ function moveToReady(ticketId) {
51
51
  const content = fs.readFileSync(sourcePath, 'utf8');
52
52
  const { frontmatter, body } = parseFrontmatter(content);
53
53
 
54
- frontmatter.status = 'ready';
55
54
  frontmatter.updated_at = new Date().toISOString();
56
55
 
57
56
  const newContent = serializeFrontmatter(frontmatter) + body;
@@ -45,7 +45,6 @@ function moveToReview(ticketId) {
45
45
  const content = fs.readFileSync(sourcePath, 'utf8');
46
46
  const { frontmatter, body } = parseFrontmatter(content);
47
47
 
48
- frontmatter.status = 'review';
49
48
  frontmatter.updated_at = new Date().toISOString();
50
49
 
51
50
  const newContent = serializeFrontmatter(frontmatter) + body;
@@ -20,7 +20,6 @@
20
20
 
21
21
  import fs from 'fs';
22
22
  import path from 'path';
23
- import YAML from 'js-yaml';
24
23
  import { findProjectRoot } from '../lib/find-root.mjs';
25
24
  import { parseFrontmatter, printResult } from '../lib/utils.mjs';
26
25
 
@@ -42,22 +41,20 @@ function checkCondition(condition) {
42
41
 
43
42
  switch (type) {
44
43
  case 'file_exists':
45
- const filePath = path.join(WORKFLOW_DIR, value);
44
+ const filePath = path.join(PROJECT_DIR, value);
46
45
  return fs.existsSync(filePath);
47
46
 
48
47
  case 'file_not_exists':
49
- const filePath2 = path.join(WORKFLOW_DIR, value);
48
+ const filePath2 = path.join(PROJECT_DIR, value);
50
49
  return !fs.existsSync(filePath2);
51
50
 
52
51
  case 'tasks_completed':
53
52
  // Проверяет, что указанные задачи выполнены (находятся в done/)
54
- if (Array.isArray(value)) {
55
- return value.every(taskId => {
56
- const donePath = path.join(DONE_DIR, `${taskId}.md`);
57
- return fs.existsSync(donePath);
58
- });
59
- }
60
- return false;
53
+ const ids = Array.isArray(value) ? value : [value];
54
+ return ids.every(taskId => {
55
+ const donePath = path.join(DONE_DIR, `${taskId}.md`);
56
+ return fs.existsSync(donePath);
57
+ });
61
58
 
62
59
  case 'date_after':
63
60
  return new Date() > new Date(value);
@@ -0,0 +1,30 @@
1
+ /**
2
+ * ESM loader that adds wf's node_modules as a fallback for module resolution.
3
+ * Used via NODE_OPTIONS=--import to allow target project scripts to use
4
+ * wf's dependencies without installing them locally.
5
+ */
6
+ import { register } from 'node:module';
7
+ import { pathToFileURL, fileURLToPath } from 'node:url';
8
+ import { dirname, join } from 'node:path';
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const wfNodeModules = join(__dirname, '..', 'node_modules');
12
+
13
+ register(
14
+ 'data:text/javascript,' + encodeURIComponent(`
15
+ import { pathToFileURL } from 'node:url';
16
+ const base = ${JSON.stringify('file:///' + wfNodeModules.replace(/\\/g, '/') + '/')};
17
+
18
+ export async function resolve(specifier, context, nextResolve) {
19
+ try {
20
+ return await nextResolve(specifier, context);
21
+ } catch (err) {
22
+ if (err.code === 'ERR_MODULE_NOT_FOUND' && !specifier.startsWith('.') && !specifier.startsWith('/')) {
23
+ return nextResolve(specifier, { ...context, parentURL: base });
24
+ }
25
+ throw err;
26
+ }
27
+ }
28
+ `),
29
+ pathToFileURL('./')
30
+ );
@@ -4,7 +4,6 @@
4
4
 
5
5
  id: "{TYPE}-{NNN}" # IMPL-001, FIX-015, PLAN-003
6
6
  title: "Название задачи"
7
- status: backlog # backlog | ready | in-progress | blocked | review | done
8
7
  priority: 3 # 1-критический, 2-высокий, 3-средний, 4-низкий, 5-когда-нибудь
9
8
 
10
9
  # Тип задачи
@@ -1,140 +0,0 @@
1
- ---
2
- name: check-conditions
3
- description: Проверить готовность задач к выполнению. Проверяет условия тикетов в backlog и выводит список готовых к перемещению в ready. НЕ перемещает файлы — перемещение выполняется отдельным stage.
4
- ---
5
-
6
- # Проверка условий готовности задач
7
-
8
- Этот skill проверяет условия выполнения для задач в backlog и **выводит список готовых**.
9
-
10
- **Важно:** Этот skill НЕ перемещает тикеты. Перемещение backlog → ready выполняется отдельным stage пайплайна (скриптом).
11
-
12
- ## Когда использовать
13
-
14
- - Перед выбором следующей задачи
15
- - После завершения задачи (для разблокировки зависимых)
16
- - Периодически для актуализации доски
17
-
18
- ## Шаги выполнения
19
-
20
- ### 1. Получить список задач
21
-
22
- Прочитать все тикеты из `.workflow/tickets/backlog/` — **обязательно выполни реальное чтение директории**, не полагайся на память или предыдущий контекст.
23
-
24
- ### 2. Для каждой задачи проверить условия
25
-
26
- Извлечь из frontmatter:
27
- - `dependencies` — зависимости
28
- - `conditions` — дополнительные условия
29
-
30
- ### 3. Проверить каждое условие
31
-
32
- #### tasks_completed
33
-
34
- ```yaml
35
- conditions:
36
- - type: tasks_completed
37
- value: ["IMPL-001", "IMPL-002"]
38
- ```
39
-
40
- Проверка:
41
- - Найти тикет IMPL-001.md в `.workflow/tickets/done/`
42
- - Если найден — условие выполнено
43
- - Если не найден — условие НЕ выполнено
44
-
45
- #### date_after
46
-
47
- ```yaml
48
- conditions:
49
- - type: date_after
50
- value: "2024-01-15"
51
- ```
52
-
53
- Проверка:
54
- - Сравнить текущую дату с указанной
55
- - Если текущая >= указанной — условие выполнено
56
-
57
- #### file_exists
58
-
59
- ```yaml
60
- conditions:
61
- - type: file_exists
62
- value: "src/config.py"
63
- ```
64
-
65
- Проверка:
66
- - Проверить существование файла
67
- - Если существует — условие выполнено
68
-
69
- #### manual_approval
70
-
71
- ```yaml
72
- conditions:
73
- - type: manual_approval
74
- value: true
75
- ```
76
-
77
- Проверка:
78
- - Требуется подтверждение человека
79
- - Задача НЕ может быть автоматически перемещена в ready
80
-
81
- ### 4. Определить статус
82
-
83
- | Все условия | Результат |
84
- |-------------|-----------|
85
- | Выполнены | Включить в `ready_tickets` |
86
- | Частично | Оставить в `backlog/` |
87
- | manual_approval | Оставить в `backlog/`, уведомить |
88
-
89
- ### 5. Сформировать отчёт
90
-
91
- ```
92
- === Проверка условий ===
93
-
94
- Готовы к выполнению:
95
- - IMPL-002 (все зависимости выполнены)
96
- - DOCS-001 (дата наступила)
97
-
98
- Ожидают:
99
- - IMPL-003: ждёт IMPL-002
100
- - REVIEW-001: требует manual_approval
101
-
102
- Заблокированы:
103
- - FIX-001: файл src/broken.py не существует
104
- ```
105
-
106
- ### 6. Вывести структурированный результат
107
-
108
- **Важно:** НЕ перемещай тикеты. Только выведи список готовых.
109
-
110
- Если есть готовые тикеты:
111
- ```
112
- ---RESULT---
113
- status: has_ready
114
- ready_tickets: IMPL-002, DOCS-001
115
- ---RESULT---
116
- ```
117
-
118
- Если готовых нет:
119
- ```
120
- ---RESULT---
121
- status: default
122
- ready_tickets:
123
- ---RESULT---
124
- ```
125
-
126
- ## Запрещено
127
-
128
- - Перемещать файлы тикетов
129
- - Редактировать frontmatter (status, updated_at)
130
- - Вызывать move-ticket.js или другие скрипты перемещения
131
-
132
- ## Результат
133
-
134
- - Отчёт о статусе всех задач в backlog
135
- - Список готовых тикетов в поле `ready_tickets` блока RESULT
136
-
137
- ## Связанные skills
138
-
139
- - `pick-next-task` — выбор задачи из ready
140
- - `move-ticket` — перемещение тикетов (выполняется отдельным stage)