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.
Files changed (44) hide show
  1. package/package.json +10 -7
  2. package/src/lib/operations/plans.mjs +85 -0
  3. package/src/lib/operations/skills.mjs +124 -0
  4. package/src/lib/operations/tickets.mjs +332 -0
  5. package/src/scripts/get-next-id.js +39 -165
  6. package/src/scripts/move-ticket.js +68 -225
  7. package/src/scripts/pick-next-task.js +93 -759
  8. package/src/skills/analyze-report/tests/cases/TC-ANALYZE-REPORT-001/current/claude-sonnet/trial-1.md +4 -68
  9. package/src/skills/analyze-report/tests/cases/TC-ANALYZE-REPORT-001/current/claude-sonnet/trial-2.md +53 -58
  10. package/src/skills/analyze-report/tests/cases/TC-ANALYZE-REPORT-001/current/claude-sonnet/trial-3.md +48 -48
  11. package/src/skills/analyze-report/tests/cases/TC-ANALYZE-REPORT-001/current/judge.json +15 -15
  12. package/src/skills/analyze-report/tests/cases/TC-ANALYZE-REPORT-001/current/meta.json +16 -16
  13. package/src/skills/analyze-report/tests/cases/TC-ANALYZE-REPORT-002/current/claude-sonnet/trial-3.md +4 -76
  14. package/src/skills/coach/tests/cases/TC-COACH-001/current/meta.json +93 -93
  15. package/src/skills/coach/tests/cases/TC-COACH-002/current/meta.json +93 -93
  16. package/src/skills/decompose-plan/tests/cases/TC-DECOMPOSE-PLAN-005/current/meta.json +113 -113
  17. package/src/skills/execute-task/tests/cases/TC-EXECUTE-TASK-001/current/meta.json +87 -87
  18. package/src/skills/execute-task/tests/cases/TC-EXECUTE-TASK-005/current/meta.json +87 -87
  19. package/src/skills/review-result/SKILL.md +1 -0
  20. package/src/skills/review-result/knowledge/baseline-snapshot-validation.md +67 -0
  21. package/src/skills/review-result/knowledge/dod-patterns.md +1 -0
  22. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-001/current/claude-sonnet/trial-1.md +2 -2
  23. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-001/current/claude-sonnet/trial-2.md +2 -2
  24. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-001/current/claude-sonnet/trial-3.md +2 -14
  25. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-001/current/judge.json +18 -18
  26. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-001/current/meta.json +20 -20
  27. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-002/current/claude-sonnet/trial-2.md +2 -34
  28. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-002/current/judge.json +19 -19
  29. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-002/current/meta.json +21 -21
  30. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/claude-sonnet/trial-1.md +36 -3
  31. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/claude-sonnet/trial-2.md +11 -3
  32. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/claude-sonnet/trial-3.md +3 -3
  33. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/judge.json +18 -18
  34. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-003/current/meta.json +20 -20
  35. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-004/current/claude-sonnet/trial-1.md +5 -0
  36. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-004/current/claude-sonnet/trial-2.md +5 -0
  37. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-004/current/claude-sonnet/trial-3.md +6 -0
  38. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-004/current/judge.json +46 -0
  39. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-004/current/meta.json +37 -0
  40. package/src/skills/review-result/tests/cases/TC-REVIEW-RESULT-004-baseline-snapshot.yaml +50 -0
  41. package/src/skills/review-result/tests/fixtures/QA-905-baseline-regex-instead-of-snapshot/QA-905.md +62 -0
  42. package/src/skills/review-result/tests/fixtures/QA-905-baseline-regex-instead-of-snapshot/baseline.test.mjs +124 -0
  43. package/src/skills/review-result/tests/index.yaml +5 -0
  44. 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 "workflow-ai/lib/find-root.mjs";
23
- import { printResult } from "workflow-ai/lib/utils.mjs";
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
- let prefix = null;
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
- prefix = args[i + 1];
36
- i++;
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
- let inTaskTypes = false;
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() === "") continue;
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
- // Вышли из секции task_types (вернулись на тот же или меньший отступ с новым ключом)
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
- return path.join(PROJECT_DIR, ".workflow", "tickets");
142
- } else if (dir === "plans") {
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
- if (allFromConfig) {
190
- try {
191
- await runAllFromConfig();
192
- } catch (err) {
193
- console.error(err.message);
194
- printResult({ status: "error", error: err.message });
195
- process.exit(1);
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
- return;
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 <ticket_id> <target>
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
- * node move-ticket.js IMPL-001 in-progress
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
- parseFrontmatter,
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
- // Вызов через pipeline runner: один аргумент — промпт с контекстом
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
- error: "Missing ticket_id or target in pipeline context",
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
- console.error("Usage: node move-ticket.js <ticket_id> <target>");
249
- console.error("Example: node move-ticket.js IMPL-001 in-progress");
250
- console.error("Available targets:", VALID_STATUSES.join(", "));
251
- printResult({ status: "error", error: "Missing arguments" });
54
+ printResult({
55
+ status: "error",
56
+ error: "Missing arguments",
57
+ });
252
58
  process.exit(1);
253
59
  }
254
60
 
255
- moveTicket(ticketId, target).then((result) => {
256
- printResult(result);
257
- if (result.status === "error") {
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();