workflow-ai 1.0.4 → 1.0.6
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 +1 -1
- package/src/lib/utils.mjs +32 -0
- package/src/runner.mjs +21 -33
- package/src/scripts/check-conditions.js +2 -32
- package/src/scripts/pick-next-task.js +17 -6
package/package.json
CHANGED
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
|
@@ -771,9 +771,26 @@ class StageExecutor {
|
|
|
771
771
|
reject(new Error(`Stage "${stageId}" timed out after ${timeout}s`));
|
|
772
772
|
}, timeout * 1000);
|
|
773
773
|
|
|
774
|
+
let stdoutBuffer = '';
|
|
774
775
|
child.stdout.on('data', (data) => {
|
|
775
|
-
|
|
776
|
-
|
|
776
|
+
const chunk = data.toString();
|
|
777
|
+
stdout += chunk;
|
|
778
|
+
// Парсим stream-json и выводим только текст дельт
|
|
779
|
+
stdoutBuffer += chunk;
|
|
780
|
+
const lines = stdoutBuffer.split('\n');
|
|
781
|
+
stdoutBuffer = lines.pop(); // незавершённая строка остаётся в буфере
|
|
782
|
+
for (const line of lines) {
|
|
783
|
+
if (!line.trim()) continue;
|
|
784
|
+
try {
|
|
785
|
+
const obj = JSON.parse(line);
|
|
786
|
+
if (obj.type === 'content_block_delta' && obj.delta?.text) {
|
|
787
|
+
process.stdout.write(obj.delta.text);
|
|
788
|
+
}
|
|
789
|
+
} catch {
|
|
790
|
+
// не JSON — выводим как есть
|
|
791
|
+
process.stdout.write(line + '\n');
|
|
792
|
+
}
|
|
793
|
+
}
|
|
777
794
|
});
|
|
778
795
|
|
|
779
796
|
child.stderr.on('data', (data) => {
|
|
@@ -924,8 +941,6 @@ class PipelineRunner {
|
|
|
924
941
|
// Инициализация контекста из CLI аргументов
|
|
925
942
|
if (args.plan) {
|
|
926
943
|
this.context.plan_id = args.plan;
|
|
927
|
-
} else if (!this.context.plan_id) {
|
|
928
|
-
this._detectCurrentPlanId(projectRoot);
|
|
929
944
|
}
|
|
930
945
|
|
|
931
946
|
// Инициализация FileGuard для защиты файлов от изменений агентами
|
|
@@ -937,32 +952,6 @@ class PipelineRunner {
|
|
|
937
952
|
this.setupGracefulShutdown();
|
|
938
953
|
}
|
|
939
954
|
|
|
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
955
|
/**
|
|
967
956
|
* Асинхронно инициализирует runner (logger)
|
|
968
957
|
*/
|
|
@@ -977,10 +966,9 @@ class PipelineRunner {
|
|
|
977
966
|
}
|
|
978
967
|
|
|
979
968
|
if (this.context.plan_id) {
|
|
980
|
-
|
|
981
|
-
this.logger.info(`Plan ID: ${this.context.plan_id} (${source})`, 'PipelineRunner');
|
|
969
|
+
this.logger.info(`Plan ID: ${this.context.plan_id}`, 'PipelineRunner');
|
|
982
970
|
} else {
|
|
983
|
-
this.logger.
|
|
971
|
+
this.logger.info('No plan_id set — processing all tickets', 'PipelineRunner');
|
|
984
972
|
}
|
|
985
973
|
}
|
|
986
974
|
|
|
@@ -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');
|
|
@@ -102,43 +102,13 @@ function readTickets(dir) {
|
|
|
102
102
|
return tickets;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
/**
|
|
106
|
-
* Нормализует входное значение в формат PLAN-NNN.
|
|
107
|
-
* Принимает: "PLAN-007", "7", "007", "plan-7", "plans/PLAN-007.md", "/abs/path/PLAN-007.md"
|
|
108
|
-
*/
|
|
109
|
-
function normalizePlanId(raw) {
|
|
110
|
-
if (!raw) return null;
|
|
111
|
-
|
|
112
|
-
// Извлекаем имя файла если передан путь
|
|
113
|
-
const basename = path.basename(raw, '.md');
|
|
114
|
-
|
|
115
|
-
// Уже в формате PLAN-NNN (регистронезависимо)
|
|
116
|
-
const full = basename.match(/^plan-(\d+)$/i);
|
|
117
|
-
if (full) return `PLAN-${String(parseInt(full[1], 10)).padStart(3, '0')}`;
|
|
118
|
-
|
|
119
|
-
// Просто цифра или число: "7", "007"
|
|
120
|
-
const num = raw.trim().match(/^(\d+)$/);
|
|
121
|
-
if (num) return `PLAN-${String(parseInt(num[1], 10)).padStart(3, '0')}`;
|
|
122
|
-
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Извлекает plan_id из аргументов командной строки (контекст пайплайна)
|
|
128
|
-
*/
|
|
129
|
-
function extractPlanId() {
|
|
130
|
-
const prompt = process.argv.slice(2)[0] || '';
|
|
131
|
-
const match = prompt.match(/plan_id:\s*(\S+)/i);
|
|
132
|
-
return match ? normalizePlanId(match[1]) : null;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
105
|
/**
|
|
136
106
|
* Проверяет все тикеты в backlog/ и возвращает список готовых
|
|
137
107
|
*/
|
|
138
108
|
function checkBacklog(planId) {
|
|
139
109
|
const allTickets = readTickets(BACKLOG_DIR);
|
|
140
110
|
const tickets = planId
|
|
141
|
-
? allTickets.filter(t => t.frontmatter.parent_plan === planId)
|
|
111
|
+
? allTickets.filter(t => normalizePlanId(t.frontmatter.parent_plan) === planId)
|
|
142
112
|
: allTickets;
|
|
143
113
|
|
|
144
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();
|
|
@@ -229,17 +229,22 @@ function findCompletedInProgress() {
|
|
|
229
229
|
/**
|
|
230
230
|
* Выбирает следующий тикет для выполнения
|
|
231
231
|
*/
|
|
232
|
-
function
|
|
233
|
-
|
|
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);
|
|
234
239
|
|
|
235
240
|
if (tickets.length === 0) {
|
|
236
241
|
// Если ready/ пуст, проверяем review/ — нужно завершить ревью
|
|
237
|
-
let reviewTickets = readReviewTickets();
|
|
242
|
+
let reviewTickets = filterByPlan(readReviewTickets(), planId);
|
|
238
243
|
|
|
239
244
|
if (reviewTickets.length === 0) {
|
|
240
245
|
// Нет тикетов ни в ready/, ни в review/ — проверяем in-progress/
|
|
241
246
|
// на завершённые тикеты (с заполненным Summary)
|
|
242
|
-
const completedInProgress = findCompletedInProgress();
|
|
247
|
+
const completedInProgress = filterByPlan(findCompletedInProgress(), planId);
|
|
243
248
|
if (completedInProgress.length > 0) {
|
|
244
249
|
const first = completedInProgress[0];
|
|
245
250
|
console.log(`[INFO] Found completed ticket in in-progress/: ${first.id}`);
|
|
@@ -318,9 +323,15 @@ function pickNextTicket() {
|
|
|
318
323
|
|
|
319
324
|
// Main entry point
|
|
320
325
|
async function main() {
|
|
326
|
+
const planId = extractPlanId();
|
|
327
|
+
|
|
328
|
+
if (planId) {
|
|
329
|
+
console.log(`[INFO] Filtering by plan_id: ${planId}`);
|
|
330
|
+
}
|
|
331
|
+
|
|
321
332
|
console.log(`[INFO] Scanning ready/ directory: ${READY_DIR}`);
|
|
322
333
|
|
|
323
|
-
const result = pickNextTicket();
|
|
334
|
+
const result = pickNextTicket(planId);
|
|
324
335
|
|
|
325
336
|
if (result.status === 'found') {
|
|
326
337
|
console.log(`[INFO] Selected ticket: ${result.ticket_id} (${result.title})`);
|