workflow-ai 1.0.68 → 1.1.0
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 +10 -7
- package/src/lib/operations/plans.mjs +85 -0
- package/src/lib/operations/skills.mjs +124 -0
- package/src/lib/operations/tickets.mjs +332 -0
- package/src/scripts/get-next-id.js +39 -165
- package/src/scripts/move-ticket.js +68 -225
- package/src/scripts/pick-next-task.js +93 -759
- package/src/skills/analyze-report/tests/cases/TC-ANALYZE-REPORT-001/current/claude-sonnet/trial-1.md +4 -68
- package/src/skills/analyze-report/tests/cases/TC-ANALYZE-REPORT-001/current/claude-sonnet/trial-2.md +53 -58
- package/src/skills/analyze-report/tests/cases/TC-ANALYZE-REPORT-001/current/claude-sonnet/trial-3.md +48 -48
- package/src/skills/analyze-report/tests/cases/TC-ANALYZE-REPORT-001/current/judge.json +15 -15
- package/src/skills/analyze-report/tests/cases/TC-ANALYZE-REPORT-001/current/meta.json +16 -16
- package/src/skills/analyze-report/tests/cases/TC-ANALYZE-REPORT-002/current/claude-sonnet/trial-3.md +4 -76
- package/src/skills/coach/tests/cases/TC-COACH-001/current/meta.json +93 -93
- package/src/skills/coach/tests/cases/TC-COACH-002/current/meta.json +93 -93
- package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/meta.json +113 -113
- package/src/skills/execute-task/tests/cases/TC-EXECUTE-TASK-001/current/meta.json +87 -87
- package/src/skills/execute-task/tests/cases/TC-EXECUTE-TASK-005/current/meta.json +87 -87
- package/src/skills/review-result/SKILL.md +1 -0
- package/src/skills/review-result/knowledge/baseline-snapshot-validation.md +67 -0
- package/src/skills/review-result/knowledge/dod-patterns.md +1 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-001/current/claude-sonnet/trial-1.md +2 -2
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-001/current/claude-sonnet/trial-2.md +2 -2
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-001/current/claude-sonnet/trial-3.md +2 -14
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-001/current/judge.json +18 -18
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-001/current/meta.json +20 -20
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-002/current/claude-sonnet/trial-2.md +2 -34
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-002/current/judge.json +19 -19
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-002/current/meta.json +21 -21
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/claude-sonnet/trial-1.md +36 -3
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/claude-sonnet/trial-2.md +11 -3
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/claude-sonnet/trial-3.md +3 -3
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/judge.json +18 -18
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/meta.json +20 -20
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-004/current/claude-sonnet/trial-1.md +5 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-004/current/claude-sonnet/trial-2.md +5 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-004/current/claude-sonnet/trial-3.md +6 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-004/current/judge.json +46 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-004/current/meta.json +37 -0
- package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-004-baseline-snapshot.yaml +50 -0
- package/src/skills/review-result/tests/fixtures/QA-905-baseline-regex-instead-of-snapshot/QA-905.md +62 -0
- package/src/skills/review-result/tests/fixtures/QA-905-baseline-regex-instead-of-snapshot/baseline.test.mjs +124 -0
- package/src/skills/review-result/tests/index.yaml +5 -0
- package/src/skills/review-result/tests/rubrics/baseline-snapshot.md +20 -0
|
@@ -1,110 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* get-next-id.js - Универсальный генератор ID для тикетов, планов и других артефактов
|
|
5
|
-
*
|
|
6
|
-
* Режимы:
|
|
7
|
-
*
|
|
8
|
-
* 1) Одиночный префикс (legacy, для create-plan / create-report):
|
|
9
|
-
* node get-next-id.js --prefix TASK --dir tickets
|
|
10
|
-
* node get-next-id.js --prefix PLAN --dir plans
|
|
11
|
-
* Вывод: { status: "success", id: "TASK-007" }
|
|
12
|
-
*
|
|
13
|
-
* 2) Карта по всем префиксам из config.yaml (для decompose-plan):
|
|
14
|
-
* node get-next-id.js --all-from-config
|
|
15
|
-
* Читает .workflow/config/config.yaml → task_types.*.prefix,
|
|
16
|
-
* для каждого префикса возвращает следующий свободный номер по .workflow/tickets/.
|
|
17
|
-
* Вывод: { status: "success", id_ranges: { TASK: 8, QA: 26, HUMAN: 2, ... } }
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
3
|
import fs from "fs";
|
|
21
4
|
import path from "path";
|
|
22
|
-
import { findProjectRoot } from "
|
|
23
|
-
import { printResult } from "
|
|
5
|
+
import { findProjectRoot } from "../lib/find-root.mjs";
|
|
6
|
+
import { printResult } from "../lib/utils.mjs";
|
|
7
|
+
import { getNextId } from "../lib/operations/tickets.mjs";
|
|
24
8
|
|
|
25
9
|
const PROJECT_DIR = findProjectRoot();
|
|
26
10
|
|
|
27
11
|
function parseArgs() {
|
|
28
12
|
const args = process.argv.slice(2);
|
|
29
|
-
|
|
30
|
-
let dir = null;
|
|
31
|
-
let allFromConfig = false;
|
|
32
|
-
|
|
13
|
+
const result = { prefix: null, dir: null, allFromConfig: false };
|
|
33
14
|
for (let i = 0; i < args.length; i++) {
|
|
34
|
-
if (args[i] === "--prefix" && i + 1 < args.length)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
} else if (args[i] === "--dir" && i + 1 < args.length) {
|
|
38
|
-
dir = args[i + 1];
|
|
39
|
-
i++;
|
|
40
|
-
} else if (args[i] === "--all-from-config") {
|
|
41
|
-
allFromConfig = true;
|
|
42
|
-
}
|
|
15
|
+
if (args[i] === "--prefix" && i + 1 < args.length) result.prefix = args[++i];
|
|
16
|
+
else if (args[i] === "--dir" && i + 1 < args.length) result.dir = args[++i];
|
|
17
|
+
else if (args[i] === "--all-from-config") result.allFromConfig = true;
|
|
43
18
|
}
|
|
44
|
-
|
|
45
|
-
return { prefix, dir, allFromConfig };
|
|
19
|
+
return result;
|
|
46
20
|
}
|
|
47
21
|
|
|
48
|
-
function findMaxNumber(targetDir, prefix) {
|
|
49
|
-
let maxNum = 0;
|
|
50
|
-
const regex = new RegExp(`^${prefix}-(\\d+)\\.md$`, "i");
|
|
51
|
-
|
|
52
|
-
function scanDirectory(dir) {
|
|
53
|
-
if (!fs.existsSync(dir)) {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
58
|
-
|
|
59
|
-
for (const entry of entries) {
|
|
60
|
-
const fullPath = path.join(dir, entry.name);
|
|
61
|
-
|
|
62
|
-
if (entry.isDirectory()) {
|
|
63
|
-
scanDirectory(fullPath);
|
|
64
|
-
} else if (entry.isFile()) {
|
|
65
|
-
const match = entry.name.match(regex);
|
|
66
|
-
if (match) {
|
|
67
|
-
const num = parseInt(match[1], 10);
|
|
68
|
-
if (num > maxNum) {
|
|
69
|
-
maxNum = num;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
scanDirectory(targetDir);
|
|
77
|
-
return maxNum;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function formatNumber(num) {
|
|
81
|
-
return num.toString().padStart(3, "0");
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Извлекает значения prefix из .workflow/config/config.yaml → task_types.*.prefix.
|
|
86
|
-
* Минимальный YAML-парсер: не тянем зависимость, читаем только нужную секцию.
|
|
87
|
-
* Возвращает массив уникальных префиксов в порядке появления.
|
|
88
|
-
*/
|
|
89
22
|
function readPrefixesFromConfig() {
|
|
90
23
|
const configPath = path.join(PROJECT_DIR, ".workflow", "config", "config.yaml");
|
|
91
|
-
|
|
92
|
-
if (!fs.existsSync(configPath)) {
|
|
93
|
-
throw new Error(`config.yaml not found: ${configPath}`);
|
|
94
|
-
}
|
|
24
|
+
if (!fs.existsSync(configPath)) throw new Error("config.yaml not found");
|
|
95
25
|
|
|
96
26
|
const text = fs.readFileSync(configPath, "utf8");
|
|
97
|
-
const lines = text.split(/\r?\n/);
|
|
98
|
-
|
|
99
27
|
const prefixes = [];
|
|
100
|
-
|
|
101
|
-
let taskTypesIndent = -1;
|
|
28
|
+
const lines = text.split(/\r?\n/);
|
|
29
|
+
let inTaskTypes = false, taskTypesIndent = -1;
|
|
102
30
|
|
|
103
31
|
for (const rawLine of lines) {
|
|
104
|
-
// Убрать комментарии
|
|
105
32
|
const line = rawLine.replace(/\s+#.*$/, "").replace(/^#.*$/, "");
|
|
106
|
-
if (line.trim()
|
|
107
|
-
|
|
33
|
+
if (!line.trim()) continue;
|
|
108
34
|
const indent = line.length - line.trimStart().length;
|
|
109
35
|
|
|
110
36
|
if (!inTaskTypes) {
|
|
@@ -115,100 +41,48 @@ function readPrefixesFromConfig() {
|
|
|
115
41
|
continue;
|
|
116
42
|
}
|
|
117
43
|
|
|
118
|
-
|
|
119
|
-
if (indent <= taskTypesIndent && line.trim() !== "") {
|
|
120
|
-
break;
|
|
121
|
-
}
|
|
122
|
-
|
|
44
|
+
if (indent <= taskTypesIndent && line.trim()) break;
|
|
123
45
|
const m = line.match(/^\s*prefix\s*:\s*["']?([A-Z][A-Z0-9_]*)["']?\s*$/);
|
|
124
|
-
if (m)
|
|
125
|
-
const p = m[1];
|
|
126
|
-
if (!prefixes.includes(p)) {
|
|
127
|
-
prefixes.push(p);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (prefixes.length === 0) {
|
|
133
|
-
throw new Error("No prefixes found in config.yaml → task_types.*.prefix");
|
|
46
|
+
if (m && !prefixes.includes(m[1])) prefixes.push(m[1]);
|
|
134
47
|
}
|
|
135
48
|
|
|
136
49
|
return prefixes;
|
|
137
50
|
}
|
|
138
51
|
|
|
139
52
|
function resolveTargetDir(dir) {
|
|
140
|
-
if (dir === "tickets")
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
return path.join(PROJECT_DIR, ".workflow", "plans");
|
|
144
|
-
} else {
|
|
145
|
-
return path.join(PROJECT_DIR, dir);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
async function runSinglePrefix(prefix, dir) {
|
|
150
|
-
const targetDir = resolveTargetDir(dir);
|
|
151
|
-
|
|
152
|
-
if (!fs.existsSync(targetDir)) {
|
|
153
|
-
const nextId = `${prefix}-001`;
|
|
154
|
-
printResult({ status: "success", id: nextId });
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const maxNum = findMaxNumber(targetDir, prefix);
|
|
159
|
-
const nextNum = maxNum + 1;
|
|
160
|
-
const nextId = `${prefix}-${formatNumber(nextNum)}`;
|
|
161
|
-
|
|
162
|
-
printResult({ status: "success", id: nextId });
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
async function runAllFromConfig() {
|
|
166
|
-
const prefixes = readPrefixesFromConfig();
|
|
167
|
-
const ticketsDir = path.join(PROJECT_DIR, ".workflow", "tickets");
|
|
168
|
-
|
|
169
|
-
const idRanges = {};
|
|
170
|
-
for (const prefix of prefixes) {
|
|
171
|
-
const maxNum = fs.existsSync(ticketsDir) ? findMaxNumber(ticketsDir, prefix) : 0;
|
|
172
|
-
idRanges[prefix] = maxNum + 1;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Параллельно возвращаем JSON-строку, потому что runner workflow-ai
|
|
176
|
-
// при подстановке $context.<key> в строку instructions применяет неявный
|
|
177
|
-
// toString() — объекты превращаются в "[object Object]". Скалярная
|
|
178
|
-
// JSON-строка подставляется корректно, декомпозитор распарсит её сам.
|
|
179
|
-
printResult({
|
|
180
|
-
status: "success",
|
|
181
|
-
id_ranges: idRanges,
|
|
182
|
-
id_ranges_json: JSON.stringify(idRanges),
|
|
183
|
-
});
|
|
53
|
+
if (dir === "tickets") return path.join(PROJECT_DIR, ".workflow", "tickets");
|
|
54
|
+
if (dir === "plans") return path.join(PROJECT_DIR, ".workflow", "plans");
|
|
55
|
+
return path.join(PROJECT_DIR, dir);
|
|
184
56
|
}
|
|
185
57
|
|
|
186
58
|
async function main() {
|
|
187
59
|
const { prefix, dir, allFromConfig } = parseArgs();
|
|
188
60
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
61
|
+
try {
|
|
62
|
+
if (allFromConfig) {
|
|
63
|
+
const prefixes = readPrefixesFromConfig();
|
|
64
|
+
const idRanges = {};
|
|
65
|
+
for (const p of prefixes) {
|
|
66
|
+
const id = await getNextId(PROJECT_DIR, p);
|
|
67
|
+
const num = parseInt(id.split("-")[1], 10);
|
|
68
|
+
idRanges[p] = num;
|
|
69
|
+
}
|
|
70
|
+
const bulkResult = { status: "success", id_ranges: idRanges, id_ranges_json: JSON.stringify(idRanges) };
|
|
71
|
+
console.log("---RESULT---");
|
|
72
|
+
console.log(JSON.stringify(bulkResult, null, 2));
|
|
73
|
+
console.log("---RESULT---");
|
|
74
|
+
} else {
|
|
75
|
+
if (!prefix || !dir) {
|
|
76
|
+
throw new Error("Either --all-from-config, or both --prefix and --dir required");
|
|
77
|
+
}
|
|
78
|
+
const id = await getNextId(PROJECT_DIR, prefix);
|
|
79
|
+
printResult({ status: "success", id });
|
|
196
80
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (!prefix || !dir) {
|
|
201
|
-
console.error("Usage:");
|
|
202
|
-
console.error(" node get-next-id.js --prefix <PREFIX> --dir <DIRECTORY>");
|
|
203
|
-
console.error(" node get-next-id.js --all-from-config");
|
|
204
|
-
printResult({
|
|
205
|
-
status: "error",
|
|
206
|
-
error: "Missing required arguments: either --all-from-config, or both --prefix and --dir",
|
|
207
|
-
});
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.error(err.message);
|
|
83
|
+
printResult({ status: "error", error: err.message });
|
|
208
84
|
process.exit(1);
|
|
209
85
|
}
|
|
210
|
-
|
|
211
|
-
await runSinglePrefix(prefix, dir);
|
|
212
86
|
}
|
|
213
87
|
|
|
214
88
|
main();
|
|
@@ -1,222 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* move-ticket.js -
|
|
4
|
+
* move-ticket.js - Тонкая прослойка: парсинг argv → вызов operation → печать RESULT
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* node move-ticket.js <
|
|
6
|
+
* Контракт:
|
|
7
|
+
* 1. Прямой режим: node move-ticket.js <id> <target>
|
|
8
|
+
* 2. Runner-режим: node move-ticket.js "<prompt-block>" (с regex ticket_id: и target:)
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
10
|
+
* RESULT format (success):
|
|
11
|
+
* status: moved
|
|
12
|
+
* ticket_id: <id>
|
|
13
|
+
* from: <source-status>
|
|
14
|
+
* to: <target-status>
|
|
15
|
+
*
|
|
16
|
+
* RESULT format (error):
|
|
17
|
+
* status: error
|
|
18
|
+
* ticket_id: <id> (если удалось распарсить)
|
|
19
|
+
* from: <source-or-empty> (если есть)
|
|
20
|
+
* to: <target> (если есть)
|
|
21
|
+
* error: <message>
|
|
11
22
|
*/
|
|
12
23
|
|
|
13
|
-
import fs from "fs";
|
|
14
|
-
import path from "path";
|
|
15
|
-
import YAML from "workflow-ai/lib/js-yaml.mjs";
|
|
16
24
|
import { findProjectRoot } from "workflow-ai/lib/find-root.mjs";
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
printResult,
|
|
20
|
-
serializeFrontmatter,
|
|
21
|
-
getLastReviewStatus,
|
|
22
|
-
} from "workflow-ai/lib/utils.mjs";
|
|
23
|
-
|
|
24
|
-
// Корень проекта
|
|
25
|
-
const PROJECT_DIR = findProjectRoot();
|
|
26
|
-
// Базовая директория workflow
|
|
27
|
-
const WORKFLOW_DIR = path.join(PROJECT_DIR, ".workflow");
|
|
28
|
-
const TICKETS_DIR = path.join(WORKFLOW_DIR, "tickets");
|
|
29
|
-
|
|
30
|
-
// Доступные статусы
|
|
31
|
-
const VALID_STATUSES = [
|
|
32
|
-
"backlog",
|
|
33
|
-
"ready",
|
|
34
|
-
"in-progress",
|
|
35
|
-
"blocked",
|
|
36
|
-
"review",
|
|
37
|
-
"done",
|
|
38
|
-
"archive",
|
|
39
|
-
];
|
|
40
|
-
|
|
41
|
-
// Таблица допустимых переходов
|
|
42
|
-
const VALID_TRANSITIONS = {
|
|
43
|
-
backlog: ["ready", "blocked", "done"],
|
|
44
|
-
ready: ["in-progress", "review", "backlog"],
|
|
45
|
-
"in-progress": ["done", "blocked", "review"],
|
|
46
|
-
blocked: ["ready"],
|
|
47
|
-
review: ["done", "ready", "in-progress", "blocked"],
|
|
48
|
-
done: ["ready", "blocked", "archive"],
|
|
49
|
-
archive: ["backlog"],
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Определяет текущий статус тикета по расположению файла
|
|
54
|
-
*/
|
|
55
|
-
function getStatusFromPath(filePath) {
|
|
56
|
-
const fileName = path.basename(filePath);
|
|
57
|
-
for (const status of VALID_STATUSES) {
|
|
58
|
-
const statusDir = path.join(TICKETS_DIR, status);
|
|
59
|
-
const expectedPath = path.join(statusDir, fileName);
|
|
60
|
-
if (filePath === expectedPath) {
|
|
61
|
-
return status;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Проверяет допустимость перехода
|
|
69
|
-
*/
|
|
70
|
-
function isValidTransition(from, to) {
|
|
71
|
-
if (!VALID_STATUSES.includes(from)) {
|
|
72
|
-
return { valid: false, error: `Неверный исходный статус: ${from}` };
|
|
73
|
-
}
|
|
74
|
-
if (!VALID_STATUSES.includes(to)) {
|
|
75
|
-
return {
|
|
76
|
-
valid: false,
|
|
77
|
-
error: `Неверный целевой статус: ${to}. Доступные: ${VALID_STATUSES.join(", ")}`,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const allowedTransitions = VALID_TRANSITIONS[from] || [];
|
|
82
|
-
if (!allowedTransitions.includes(to)) {
|
|
83
|
-
return {
|
|
84
|
-
valid: false,
|
|
85
|
-
error: `Переход из ${from} в ${to} недопустим. Доступные переходы: ${allowedTransitions.join(", ") || "нет"}`,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return { valid: true };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Основная функция перемещения тикета
|
|
94
|
-
*/
|
|
95
|
-
async function moveTicket(ticketId, target) {
|
|
96
|
-
// Поиск файла тикета во всех директориях
|
|
97
|
-
let sourceDir = null;
|
|
98
|
-
let currentStatus = null;
|
|
99
|
-
|
|
100
|
-
for (const status of VALID_STATUSES) {
|
|
101
|
-
const statusDir = path.join(TICKETS_DIR, status);
|
|
102
|
-
const ticketPath = path.join(statusDir, `${ticketId}.md`);
|
|
103
|
-
if (fs.existsSync(ticketPath)) {
|
|
104
|
-
sourceDir = statusDir;
|
|
105
|
-
currentStatus = status;
|
|
106
|
-
break;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (!sourceDir) {
|
|
111
|
-
return {
|
|
112
|
-
status: "error",
|
|
113
|
-
ticket_id: ticketId,
|
|
114
|
-
error: `Тикет ${ticketId} не найден ни в одной из директорий`,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Проверка допустимости перехода
|
|
119
|
-
const transitionCheck = isValidTransition(currentStatus, target);
|
|
120
|
-
if (!transitionCheck.valid) {
|
|
121
|
-
return {
|
|
122
|
-
status: "error",
|
|
123
|
-
ticket_id: ticketId,
|
|
124
|
-
from: currentStatus,
|
|
125
|
-
to: target,
|
|
126
|
-
error: transitionCheck.error,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const sourcePath = path.join(sourceDir, `${ticketId}.md`);
|
|
131
|
-
const targetDir = path.join(TICKETS_DIR, target);
|
|
132
|
-
const targetPath = path.join(targetDir, `${ticketId}.md`);
|
|
133
|
-
|
|
134
|
-
// Чтение файла тикета
|
|
135
|
-
let content;
|
|
136
|
-
try {
|
|
137
|
-
content = fs.readFileSync(sourcePath, "utf8");
|
|
138
|
-
} catch (e) {
|
|
139
|
-
return {
|
|
140
|
-
status: "error",
|
|
141
|
-
ticket_id: ticketId,
|
|
142
|
-
error: `Не удалось прочитать файл: ${e.message}`,
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Парсинг frontmatter
|
|
147
|
-
let frontmatter, body;
|
|
148
|
-
try {
|
|
149
|
-
({ frontmatter, body } = parseFrontmatter(content));
|
|
150
|
-
} catch (e) {
|
|
151
|
-
return {
|
|
152
|
-
status: "error",
|
|
153
|
-
ticket_id: ticketId,
|
|
154
|
-
error: e.message,
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Обновление frontmatter
|
|
159
|
-
const now = new Date().toISOString();
|
|
160
|
-
frontmatter.updated_at = now;
|
|
161
|
-
|
|
162
|
-
// Если переход в done, добавляем completed_at
|
|
163
|
-
if (target === "done" && currentStatus !== "done") {
|
|
164
|
-
frontmatter.completed_at = now;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Fallback: если тикет идёт в done из review, но агент не записал секцию "## Ревью" — дописываем
|
|
168
|
-
if (
|
|
169
|
-
target === "done" &&
|
|
170
|
-
currentStatus === "review" &&
|
|
171
|
-
getLastReviewStatus(content) === null
|
|
172
|
-
) {
|
|
173
|
-
const date = now.slice(0, 16).replace("T", " ");
|
|
174
|
-
const reviewSection = `\n## Ревью\n\n| Дата | Статус | Самари |\n|------|--------|--------|\n| ${date} | ✅ passed | Pipeline fallback: агент не записал секцию ревью |\n`;
|
|
175
|
-
body = body.trimEnd() + "\n" + reviewSection;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Если переход из blocked, удаляем blocked_reason
|
|
179
|
-
if (currentStatus === "blocked" && frontmatter.blocked_reason) {
|
|
180
|
-
delete frontmatter.blocked_reason;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Сериализация нового контента
|
|
184
|
-
const newContent = serializeFrontmatter(frontmatter) + body;
|
|
185
|
-
|
|
186
|
-
// Создание целевой директории если не существует
|
|
187
|
-
if (!fs.existsSync(targetDir)) {
|
|
188
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Перемещение файла
|
|
192
|
-
try {
|
|
193
|
-
fs.renameSync(sourcePath, targetPath);
|
|
194
|
-
} catch (e) {
|
|
195
|
-
return {
|
|
196
|
-
status: "error",
|
|
197
|
-
ticket_id: ticketId,
|
|
198
|
-
error: `Не удалось переместить файл: ${e.message}`,
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Запись обновлённого контента
|
|
203
|
-
try {
|
|
204
|
-
fs.writeFileSync(targetPath, newContent, "utf8");
|
|
205
|
-
} catch (e) {
|
|
206
|
-
return {
|
|
207
|
-
status: "error",
|
|
208
|
-
ticket_id: ticketId,
|
|
209
|
-
error: `Не удалось записать файл: ${e.message}`,
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return {
|
|
214
|
-
status: "moved",
|
|
215
|
-
ticket_id: ticketId,
|
|
216
|
-
from: currentStatus,
|
|
217
|
-
to: target,
|
|
218
|
-
};
|
|
219
|
-
}
|
|
25
|
+
import { moveTicket } from "../lib/operations/tickets.mjs";
|
|
26
|
+
import { printResult } from "workflow-ai/lib/utils.mjs";
|
|
220
27
|
|
|
221
28
|
// Main entry point
|
|
222
29
|
const rawArgs = process.argv.slice(2);
|
|
@@ -227,34 +34,70 @@ if (rawArgs.length >= 2) {
|
|
|
227
34
|
ticketId = rawArgs[0];
|
|
228
35
|
target = rawArgs[1];
|
|
229
36
|
} else if (rawArgs.length === 1) {
|
|
230
|
-
//
|
|
231
|
-
// Формат: "skill-name\n\nContext:\n ticket_id: X\n target: Y\n..."
|
|
37
|
+
// Runner-режим: один аргумент — многострочный текст с ticket_id: и target:
|
|
232
38
|
const prompt = rawArgs[0];
|
|
233
39
|
const ticketMatch = prompt.match(/ticket_id:\s*(\S+)/);
|
|
234
40
|
const targetMatch = prompt.match(/target:\s*(\S+)/);
|
|
235
41
|
ticketId = ticketMatch?.[1];
|
|
236
42
|
target = targetMatch?.[1];
|
|
237
43
|
if (!ticketId || !target) {
|
|
238
|
-
console.error(
|
|
239
|
-
"[ERROR] Cannot parse ticket_id or target from pipeline context",
|
|
240
|
-
);
|
|
241
44
|
printResult({
|
|
242
45
|
status: "error",
|
|
243
|
-
|
|
46
|
+
ticket_id: ticketId || "",
|
|
47
|
+
from: "",
|
|
48
|
+
to: target || "",
|
|
49
|
+
error: "Missing ticket_id or target",
|
|
244
50
|
});
|
|
245
51
|
process.exit(1);
|
|
246
52
|
}
|
|
247
53
|
} else {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
54
|
+
printResult({
|
|
55
|
+
status: "error",
|
|
56
|
+
error: "Missing arguments",
|
|
57
|
+
});
|
|
252
58
|
process.exit(1);
|
|
253
59
|
}
|
|
254
60
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
61
|
+
async function main() {
|
|
62
|
+
try {
|
|
63
|
+
const projectRoot = findProjectRoot();
|
|
64
|
+
const result = await moveTicket(projectRoot, ticketId, target);
|
|
65
|
+
printResult({
|
|
66
|
+
status: "moved",
|
|
67
|
+
ticket_id: ticketId,
|
|
68
|
+
from: result.from,
|
|
69
|
+
to: result.to,
|
|
70
|
+
});
|
|
71
|
+
} catch (err) {
|
|
72
|
+
// moveTicket throws { code: 'INVALID_TRANSITION', from, to, id } для ошибок валидации
|
|
73
|
+
if (err.code === "INVALID_TRANSITION") {
|
|
74
|
+
const fromDisplay = err.from || "";
|
|
75
|
+
const errorMsg =
|
|
76
|
+
!err.from || err.from === null
|
|
77
|
+
? `Тикет ${ticketId} не найден ни в одной из директорий`
|
|
78
|
+
: `Недопустимый переход: ${err.from} → ${err.to}`;
|
|
79
|
+
printResult({
|
|
80
|
+
status: "error",
|
|
81
|
+
ticket_id: ticketId,
|
|
82
|
+
from: fromDisplay,
|
|
83
|
+
to: err.to,
|
|
84
|
+
error: errorMsg,
|
|
85
|
+
});
|
|
86
|
+
} else if (err.message) {
|
|
87
|
+
printResult({
|
|
88
|
+
status: "error",
|
|
89
|
+
ticket_id: ticketId,
|
|
90
|
+
error: err.message,
|
|
91
|
+
});
|
|
92
|
+
} else {
|
|
93
|
+
printResult({
|
|
94
|
+
status: "error",
|
|
95
|
+
ticket_id: ticketId,
|
|
96
|
+
error: err.toString(),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
258
99
|
process.exit(1);
|
|
259
100
|
}
|
|
260
|
-
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
main();
|