workflow-ai 1.0.3 → 1.0.5

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.
@@ -53,7 +53,7 @@ pipeline:
53
53
  command: "kilo"
54
54
  args: ["-m", "kilo/minimax/minimax-m2.5:free", "--agent", "orchestrator", "run"]
55
55
  workdir: "."
56
- description: "Kilo Code — агент с мульти-режимами (architect, code, debug)"
56
+ description: "Агент с мульти-режимами (architect, code, debug)"
57
57
 
58
58
  kilo-glm:
59
59
  command: "kilo"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workflow-ai",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
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
@@ -53,6 +53,38 @@ export function printResult(result) {
53
53
  console.log('---RESULT---');
54
54
  }
55
55
 
56
+ /**
57
+ * Нормализует входное значение в формат PLAN-NNN.
58
+ * Принимает: "PLAN-007", "7", "007", "plan-7", "plans/PLAN-007.md", "/abs/path/PLAN-007.md"
59
+ *
60
+ * @param {string} raw - Входное значение
61
+ * @returns {string|null} Нормализованный ID плана или null
62
+ */
63
+ export function normalizePlanId(raw) {
64
+ if (!raw) return null;
65
+
66
+ const basename = path.basename(raw, '.md');
67
+
68
+ const full = basename.match(/^plan-(\d+)$/i);
69
+ if (full) return `PLAN-${String(parseInt(full[1], 10)).padStart(3, '0')}`;
70
+
71
+ const num = raw.trim().match(/^(\d+)$/);
72
+ if (num) return `PLAN-${String(parseInt(num[1], 10)).padStart(3, '0')}`;
73
+
74
+ return null;
75
+ }
76
+
77
+ /**
78
+ * Извлекает plan_id из аргументов командной строки (контекст пайплайна).
79
+ *
80
+ * @returns {string|null} Нормализованный plan_id или null
81
+ */
82
+ export function extractPlanId() {
83
+ const prompt = process.argv.slice(2)[0] || '';
84
+ const match = prompt.match(/plan_id:\s*(\S+)/i);
85
+ return match ? normalizePlanId(match[1]) : null;
86
+ }
87
+
56
88
  /**
57
89
  * Возвращает абсолютный путь к корню npm-пакета через import.meta.url.
58
90
  *
package/src/runner.mjs CHANGED
@@ -924,8 +924,6 @@ class PipelineRunner {
924
924
  // Инициализация контекста из CLI аргументов
925
925
  if (args.plan) {
926
926
  this.context.plan_id = args.plan;
927
- } else if (!this.context.plan_id) {
928
- this._detectCurrentPlanId(projectRoot);
929
927
  }
930
928
 
931
929
  // Инициализация FileGuard для защиты файлов от изменений агентами
@@ -937,32 +935,6 @@ class PipelineRunner {
937
935
  this.setupGracefulShutdown();
938
936
  }
939
937
 
940
- /**
941
- * Автоматически определяет plan_id из .workflow/plans/current/
942
- * Ищет первый .md файл (не .gitkeep) и читает поле id: из frontmatter
943
- * @param {string} projectRoot - корневая директория проекта
944
- * @returns {string} plan_id или пустая строка если не найден
945
- */
946
- _detectCurrentPlanId(projectRoot) {
947
- const plansDir = path.resolve(projectRoot, '.workflow/plans/current');
948
- if (!fs.existsSync(plansDir)) return '';
949
-
950
- const files = fs.readdirSync(plansDir)
951
- .filter(f => f.endsWith('.md') && f !== '.gitkeep.md')
952
- .sort();
953
-
954
- for (const file of files) {
955
- const content = fs.readFileSync(path.join(plansDir, file), 'utf8');
956
- const match = content.match(/^id:\s*["']?([^"'\n]+)["']?/m);
957
- if (match) {
958
- this.context.plan_id = match[1].trim();
959
- return this.context.plan_id;
960
- }
961
- }
962
-
963
- return '';
964
- }
965
-
966
938
  /**
967
939
  * Асинхронно инициализирует runner (logger)
968
940
  */
@@ -977,10 +949,9 @@ class PipelineRunner {
977
949
  }
978
950
 
979
951
  if (this.context.plan_id) {
980
- const source = this.args.plan ? 'CLI' : 'auto-detected';
981
- this.logger.info(`Plan ID: ${this.context.plan_id} (${source})`, 'PipelineRunner');
952
+ this.logger.info(`Plan ID: ${this.context.plan_id}`, 'PipelineRunner');
982
953
  } else {
983
- this.logger.warn('No plan_id set and no active plan found in .workflow/plans/current/', 'PipelineRunner');
954
+ this.logger.info('No plan_id set processing all tickets', 'PipelineRunner');
984
955
  }
985
956
  }
986
957
 
@@ -28,7 +28,7 @@
28
28
  import fs from 'fs';
29
29
  import path from 'path';
30
30
  import { findProjectRoot } from '../lib/find-root.mjs';
31
- import { parseFrontmatter, printResult } from '../lib/utils.mjs';
31
+ import { parseFrontmatter, printResult, normalizePlanId, extractPlanId } from '../lib/utils.mjs';
32
32
 
33
33
  const PROJECT_DIR = findProjectRoot();
34
34
  const WORKFLOW_DIR = path.join(PROJECT_DIR, '.workflow');
@@ -51,6 +51,7 @@ function checkCondition(condition) {
51
51
  return !fs.existsSync(path.join(PROJECT_DIR, value));
52
52
 
53
53
  case 'tasks_completed': {
54
+ if (!value || (Array.isArray(value) && value.length === 0)) return true;
54
55
  const ids = Array.isArray(value) ? value : [value];
55
56
  return ids.every(taskId => fs.existsSync(path.join(DONE_DIR, `${taskId}.md`)));
56
57
  }
@@ -101,43 +102,13 @@ function readTickets(dir) {
101
102
  return tickets;
102
103
  }
103
104
 
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
105
  /**
135
106
  * Проверяет все тикеты в backlog/ и возвращает список готовых
136
107
  */
137
108
  function checkBacklog(planId) {
138
109
  const allTickets = readTickets(BACKLOG_DIR);
139
110
  const tickets = planId
140
- ? allTickets.filter(t => t.frontmatter.parent_plan === planId)
111
+ ? allTickets.filter(t => normalizePlanId(t.frontmatter.parent_plan) === planId)
141
112
  : allTickets;
142
113
 
143
114
  const ready = [];
@@ -21,7 +21,7 @@
21
21
  import fs from 'fs';
22
22
  import path from 'path';
23
23
  import { findProjectRoot } from '../lib/find-root.mjs';
24
- import { parseFrontmatter, printResult } from '../lib/utils.mjs';
24
+ import { parseFrontmatter, printResult, normalizePlanId, extractPlanId } from '../lib/utils.mjs';
25
25
 
26
26
  // Корень проекта
27
27
  const PROJECT_DIR = findProjectRoot();
@@ -50,6 +50,7 @@ function checkCondition(condition) {
50
50
 
51
51
  case 'tasks_completed':
52
52
  // Проверяет, что указанные задачи выполнены (находятся в done/)
53
+ if (!value || (Array.isArray(value) && value.length === 0)) return true;
53
54
  const ids = Array.isArray(value) ? value : [value];
54
55
  return ids.every(taskId => {
55
56
  const donePath = path.join(DONE_DIR, `${taskId}.md`);
@@ -228,17 +229,22 @@ function findCompletedInProgress() {
228
229
  /**
229
230
  * Выбирает следующий тикет для выполнения
230
231
  */
231
- function pickNextTicket() {
232
- const tickets = readReadyTickets();
232
+ function filterByPlan(tickets, planId) {
233
+ if (!planId) return tickets;
234
+ return tickets.filter(t => normalizePlanId(t.frontmatter.parent_plan) === planId);
235
+ }
236
+
237
+ function pickNextTicket(planId) {
238
+ const tickets = filterByPlan(readReadyTickets(), planId);
233
239
 
234
240
  if (tickets.length === 0) {
235
241
  // Если ready/ пуст, проверяем review/ — нужно завершить ревью
236
- let reviewTickets = readReviewTickets();
242
+ let reviewTickets = filterByPlan(readReviewTickets(), planId);
237
243
 
238
244
  if (reviewTickets.length === 0) {
239
245
  // Нет тикетов ни в ready/, ни в review/ — проверяем in-progress/
240
246
  // на завершённые тикеты (с заполненным Summary)
241
- const completedInProgress = findCompletedInProgress();
247
+ const completedInProgress = filterByPlan(findCompletedInProgress(), planId);
242
248
  if (completedInProgress.length > 0) {
243
249
  const first = completedInProgress[0];
244
250
  console.log(`[INFO] Found completed ticket in in-progress/: ${first.id}`);
@@ -317,9 +323,15 @@ function pickNextTicket() {
317
323
 
318
324
  // Main entry point
319
325
  async function main() {
326
+ const planId = extractPlanId();
327
+
328
+ if (planId) {
329
+ console.log(`[INFO] Filtering by plan_id: ${planId}`);
330
+ }
331
+
320
332
  console.log(`[INFO] Scanning ready/ directory: ${READY_DIR}`);
321
333
 
322
- const result = pickNextTicket();
334
+ const result = pickNextTicket(planId);
323
335
 
324
336
  if (result.status === 'found') {
325
337
  console.log(`[INFO] Selected ticket: ${result.ticket_id} (${result.title})`);
@@ -1,5 +1,5 @@
1
1
  ---
2
- # Шаблон плана для системы Kilo Code Agent Workflow
2
+ # Шаблон плана
3
3
 
4
4
  id: "PLAN-{NNN}"
5
5
  title: "Название плана"
@@ -1,7 +1,8 @@
1
1
  ---
2
- # Шаблон отчёта для системы Kilo Code Agent Workflow
2
+ # Шаблон отчёта
3
3
 
4
4
  id: "REPORT-{NNN}"
5
+ title: "" # Краткое название отчёта
5
6
  type: daily # daily | sprint | milestone
6
7
  period_start: "" # ISO 8601
7
8
  period_end: ""