workflow-ai 1.2.1 → 1.3.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/CHANGELOG.md +14 -2
- package/README.md +37 -475
- package/configs/pipeline.yaml +90 -2
- package/package.json +11 -1
- package/src/runner.mjs +2 -1
- package/src/scripts/check-relevance.js +3 -1
- package/src/scripts/mark-blocked.js +160 -0
- package/src/scripts/move-ticket.js +100 -35
- package/src/scripts/pick-next-task.js +64 -35
- package/src/skills/__test-cal-001-1777553217513/SKILL.md +2 -0
- package/src/skills/__test-runner-1777553217483/SKILL.md +5 -0
- package/src/skills/coach/SKILL.md +1 -1
- package/src/skills/execute-task/SKILL.md +1 -1
- package/src/skills/review-result/SKILL.md +23 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
## [1.
|
|
1
|
+
## [1.3.0] — 2026-04-30
|
|
2
|
+
|
|
3
|
+
### Добавлено
|
|
4
|
+
- **Новый тип стейджа: `mark-blocked` и расширение frontmatter** — Добавлены поля `auto_blocked_reason`, `auto_blocked_attempts`, `auto_blocked_at` для автоматического отслеживания причин и попыток блокировки тикетов. Стейдж `mark-blocked` позволяет устанавливать эти поля в зависимости от условий бизнес-логики.
|
|
5
|
+
- **Новый тип стейджа: `manual-gate-human`** — Добавлен статус `human_ready` в `pick-next-task.js`. При достижении этого статуса задача ожидает ручного решения оператора перед продолжением выполнения.
|
|
6
|
+
- **Хук одобрения в `move-ticket.js`** — Добавлен approval-hook, проверяющий наличие ожидающих решения одобрений перед перемещением тикета в терминальные состояния.
|
|
7
|
+
- **Расширение START-лога полем `ticket="X"`** — Добавлено поле с идентификатором тикета в стартовый лог для улучшенной трассировки и связывания логов с конкретными задачами.
|
|
8
|
+
- **Исправление `approval-pending.mjs` (поле `created_at`)** — Исправлено некорректное заполнение поля `created_at` при создании файлов ожидающих решения.
|
|
9
|
+
|
|
10
|
+
> **Примечание:** версия 1.2.0 не была опубликована в npm. При выполнении `npm publish`
|
|
11
|
+
> сработал prepublishOnly/postversion скрипт, автоматически поднявший версию до 1.2.1.
|
|
12
|
+
> Git-тег `v1.2.0` (коммит 179d52b) соответствует состоянию до publish; `v1.2.1` (коммит 83d1b70) —
|
|
13
|
+
> фактически опубликованной версии.
|
|
2
14
|
|
|
3
15
|
### New Features
|
|
4
16
|
- **New built-in stage type: `manual-gate`** — Adds support for manual approval steps in pipelines. When a stage with `type: manual-gate` is encountered, the runner creates a pending approval file in `.workflow/approvals/{step_id}.json` and enters a polling loop, waiting for an external decision (`approved`/`rejected`).
|
|
@@ -35,7 +47,7 @@
|
|
|
35
47
|
- No breaking changes. All existing pipelines without `manual-gate` stages are fully backward compatible.
|
|
36
48
|
|
|
37
49
|
### Fixes
|
|
38
|
-
-
|
|
50
|
+
- Исправлено сохранение временной метки `created_at` в файлах одобрений (approval-pending.mjs)
|
|
39
51
|
|
|
40
52
|
### Technical Notes
|
|
41
53
|
- Approval files are stored in `<project_root>/.workflow/approvals/`
|
package/README.md
CHANGED
|
@@ -1,475 +1,37 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
| `workflow eject-scripts [path]` | Извлечь скрипты (скопировать из глобальной директории в проект) |
|
|
39
|
-
| `workflow eject-configs [path]` | Извлечь конфиги (скопировать из глобальной директории в проект) |
|
|
40
|
-
| `workflow list [path]` | Вывести список скилов со статусом (shared/ejected/project-only) |
|
|
41
|
-
| `workflow help` | Показать справку |
|
|
42
|
-
| `workflow version` | Показать версию |
|
|
43
|
-
|
|
44
|
-
### Опции команды `run`
|
|
45
|
-
|
|
46
|
-
| Опция | Описание |
|
|
47
|
-
|-------|----------|
|
|
48
|
-
| `--plan <plan>` | ID плана для выполнения |
|
|
49
|
-
| `--config <path>` | Путь к конфиг-файлу |
|
|
50
|
-
| `--project <path>` | Корень проекта (по умолчанию: автоопределение) |
|
|
51
|
-
|
|
52
|
-
## Инициализация
|
|
53
|
-
|
|
54
|
-
Команда `workflow init` создаёт структуру директории `.workflow/`:
|
|
55
|
-
|
|
56
|
-
```
|
|
57
|
-
.workflow/
|
|
58
|
-
├── config/ # → junction на ~/.workflow/configs/ (eject для кастомизации)
|
|
59
|
-
├── plans/
|
|
60
|
-
│ ├── current/ # Текущие планы разработки
|
|
61
|
-
│ ├── templates/ # Шаблоны планов с триггерами (повторяющиеся планы)
|
|
62
|
-
│ └── archive/ # Архивные планы
|
|
63
|
-
├── tickets/
|
|
64
|
-
│ ├── backlog/ # Ожидают условий
|
|
65
|
-
│ ├── ready/ # Готовы к выполнению
|
|
66
|
-
│ ├── in-progress/ # В работе
|
|
67
|
-
│ ├── blocked/ # Заблокированы зависимостями
|
|
68
|
-
│ ├── review/ # Ожидают ревью
|
|
69
|
-
│ └── done/ # Завершены
|
|
70
|
-
├── reports/ # Сгенерированные отчёты
|
|
71
|
-
├── logs/ # Логи выполнения конвейера
|
|
72
|
-
├── metrics/ # Метрики производительности
|
|
73
|
-
├── templates/ # Шаблоны тикетов/планов/отчётов
|
|
74
|
-
└── src/
|
|
75
|
-
├── skills/ # Инструкции скилов (junctions на глобальные, по каждому скилу)
|
|
76
|
-
└── scripts/ # Скрипты автоматизации (junction на глобальные)
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
## Конвейер
|
|
80
|
-
|
|
81
|
-
Команда `workflow run` исполняет многоэтапный конвейер:
|
|
82
|
-
|
|
83
|
-
1. **pick-first-task** — выбрать тикет из очереди ready
|
|
84
|
-
2. **check-plan-templates** — проверить триггеры шаблонов планов, создать планы при срабатывании
|
|
85
|
-
3. **check-plan-decomposition** — проверить состояние декомпозиции/активации планов
|
|
86
|
-
4. **allocate-ticket-ids** — выделить стартовые ID для префиксов до декомпозиции
|
|
87
|
-
5. **decompose-plan** — разбить план на тикеты (при необходимости)
|
|
88
|
-
6. **check-atomicity-limit / verify-atomicity / increment-atomicity-counter** — проверить атомарность тикетов плана
|
|
89
|
-
7. **check-conditions** — проверить условия готовности тикета
|
|
90
|
-
8. **move-to-ready** — переместить тикеты из backlog в ready
|
|
91
|
-
9. **pick-next-task** — выбрать следующий тикет для выполнения
|
|
92
|
-
10. **move-to-in-progress** — начать выполнение
|
|
93
|
-
11. **check-relevance** — проверить, что тикет всё ещё актуален (на скриптах, без LLM)
|
|
94
|
-
12. **check-mcp** — проверить доступность MCP-зависимостей тикета
|
|
95
|
-
13. **execute-task** — выполнить работу через AI-агента
|
|
96
|
-
14. **move-to-review** — отправить на ревью
|
|
97
|
-
15. **verify-artifacts** — детерминированная проверка артефактов тикета
|
|
98
|
-
16. **review-result** — проверить результаты по Definition of Done
|
|
99
|
-
17. **increment-task-attempts** — учесть попытки повторов
|
|
100
|
-
18. **move-ticket** — переместить в done/blocked по результатам ревью
|
|
101
|
-
19. **create-report** — сгенерировать отчёт о выполнении
|
|
102
|
-
20. **analyze-report / decompose-gaps** — проанализировать результаты и итерировать
|
|
103
|
-
21. **complete-plan / increment-plan-iterations** — закрыть план или запустить следующую итерацию
|
|
104
|
-
|
|
105
|
-
### Типы стадий
|
|
106
|
-
|
|
107
|
-
#### `update-counter`
|
|
108
|
-
Встроенная стадия, инкрементирует счётчик и возвращает статус для перехода. Требует `counter` и опционально `max`. Возвращает `default` или `max_reached`.
|
|
109
|
-
|
|
110
|
-
#### `manual-gate`
|
|
111
|
-
Встроенная стадия для ручного одобрения. Создаёт файл `.workflow/approvals/{step_id}.json` со статусом `pending` и ждёт решения через polling (интервал по умолчанию 2000мс).
|
|
112
|
-
|
|
113
|
-
**Параметры:**
|
|
114
|
-
- `poll_interval_ms` — интервал опроса (опц., default 2000)
|
|
115
|
-
- `timeout_seconds` — таймаут в секундах (опц., default без таймаута)
|
|
116
|
-
|
|
117
|
-
**Требуемые goto:**
|
|
118
|
-
- `approved` — обязательный, переход при одобрении
|
|
119
|
-
- `rejected` — обязательный, переход при отклонении
|
|
120
|
-
|
|
121
|
-
**Опциональные goto:**
|
|
122
|
-
- `timeout` — при истечении таймаута
|
|
123
|
-
- `aborted` — при остановке runner'а (SIGTERM)
|
|
124
|
-
|
|
125
|
-
**Формат step_id:** `{ticket_id}_{stageId}_{attempt}` (например, `QA-12_manual-approve_0`)
|
|
126
|
-
|
|
127
|
-
**Approval-файл содержит:**
|
|
128
|
-
```json
|
|
129
|
-
{
|
|
130
|
-
"step_id": "QA-12_manual-approve_0",
|
|
131
|
-
"ticket_id": "QA-12",
|
|
132
|
-
"stage_id": "manual-approve",
|
|
133
|
-
"attempt": 0,
|
|
134
|
-
"status": "pending",
|
|
135
|
-
"created_at": "2026-04-28T12:34:56.789Z",
|
|
136
|
-
"updated_at": "2026-04-28T12:34:56.789Z",
|
|
137
|
-
"decided_by": null,
|
|
138
|
-
"comment": null,
|
|
139
|
-
"context_snapshot": { ... }
|
|
140
|
-
}
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
**Два способа одобрения:**
|
|
144
|
-
1. **Через MCP-клиент** (например `workflow-mcp`): tool `approve_step({step_id, decision, comment})` пишет в файл approval
|
|
145
|
-
2. **Прямая правка файла:** открыть `.workflow/approvals/{step_id}.json`, изменить `"status": "pending"` на `"approved"` или `"rejected"`, сохранить
|
|
146
|
-
|
|
147
|
-
**Важное:** `manual-gate` — **opt-in**. Если ваш pipeline не использует стадии `manual-gate`, поведение полностью идентично предыдущим версиям! Никаких breaking changes.
|
|
148
|
-
|
|
149
|
-
**Пример:**
|
|
150
|
-
```yaml
|
|
151
|
-
stages:
|
|
152
|
-
manual-approve-deploy:
|
|
153
|
-
type: manual-gate
|
|
154
|
-
poll_interval_ms: 2000
|
|
155
|
-
timeout_seconds: 86400
|
|
156
|
-
goto:
|
|
157
|
-
approved: continue-deploy
|
|
158
|
-
rejected: rollback
|
|
159
|
-
timeout: notify-stuck
|
|
160
|
-
aborted: end
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
Агенты настраиваются в `configs/pipeline.yaml`.
|
|
164
|
-
|
|
165
|
-
## Скилы
|
|
166
|
-
|
|
167
|
-
Встроенные скилы для разных типов задач:
|
|
168
|
-
|
|
169
|
-
| Скил | Описание |
|
|
170
|
-
|------|----------|
|
|
171
|
-
| `analyze-report` | Анализ отчёта |
|
|
172
|
-
| `coach` | Управление и улучшение скилов |
|
|
173
|
-
| `create-plan` | Создание плана |
|
|
174
|
-
| `create-report` | Генерация отчёта |
|
|
175
|
-
| `decompose-gaps` | Декомпозиция пробелов |
|
|
176
|
-
| `decompose-plan` | Декомпозиция плана на тикеты |
|
|
177
|
-
| `deep-research` | Глубокий ресерч |
|
|
178
|
-
| `execute-task` | Выполнение задачи |
|
|
179
|
-
| `manual-testing` | UI-observability: ручное тестирование сценариев |
|
|
180
|
-
| `review-result` | Ревью результата по DoD |
|
|
181
|
-
|
|
182
|
-
Скилы хранятся глобально в `~/.workflow/skills/` и подключаются в проекты через junctions.
|
|
183
|
-
|
|
184
|
-
Используйте `workflow eject <skill>` для копирования скила в проект для кастомизации.
|
|
185
|
-
|
|
186
|
-
### Как работать с коучем
|
|
187
|
-
|
|
188
|
-
Коуч — мета-скил для создания и улучшения остальных скилов. Правки в `.workflow/src/skills/` делаются **только** через него.
|
|
189
|
-
|
|
190
|
-
```text
|
|
191
|
-
# Запрос к AI-агенту:
|
|
192
|
-
Загрузи коуча из .workflow/src/skills/coach/SKILL.md и <действие>
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
Варианты `<действия>`:
|
|
196
|
-
|
|
197
|
-
| Тип задачи | Пример запроса |
|
|
198
|
-
|------------|----------------|
|
|
199
|
-
| Создать новый скил | `создай скил <имя> для <назначение>` |
|
|
200
|
-
| Аудит существующего | `сделай аудит скила <имя>` |
|
|
201
|
-
| Анализ эффективности | `проанализируй результаты скила <имя> по завершённым тикетам` |
|
|
202
|
-
| Точечное улучшение | `улучши скил <имя>: <что именно>` |
|
|
203
|
-
| Ресерч практик | `найди лучшие практики для <тема> и обогати скил <имя>` |
|
|
204
|
-
| Ревью скила | `сделай ревью скила <имя>` |
|
|
205
|
-
|
|
206
|
-
Коуч сам определит тип задачи, загрузит нужный воркфлоу, внесёт правку, прогонит тест скила и запишет результат в `.workflow/coach-backlog.yaml`. Коммит делает пользователь.
|
|
207
|
-
|
|
208
|
-
## Регрессионные тесты скилов
|
|
209
|
-
|
|
210
|
-
Трёхуровневая система тестирования скилов для проверки качества AI-агентов.
|
|
211
|
-
|
|
212
|
-
### Три слоя тестирования
|
|
213
|
-
|
|
214
|
-
| Уровень | Название | Описание |
|
|
215
|
-
|---------|----------|----------|
|
|
216
|
-
| L0 | Static | Базовая проверка синтаксиса и структуры: YAML-валидация, проверка обязательных полей, линтер |
|
|
217
|
-
| L1 | Deterministic | Детерминированные тесты: эталонные входные данные → ожидаемый результат (strict match) |
|
|
218
|
-
| L2 | Rubric | Гибкая оценка по критериям: scorer выставляет баллы на основе качества результата |
|
|
219
|
-
|
|
220
|
-
### Структура директорий
|
|
221
|
-
|
|
222
|
-
```
|
|
223
|
-
src/skills/<name>/tests/
|
|
224
|
-
├── index.yaml # Метаданные тестов, список test cases
|
|
225
|
-
├── cases/ # Входные данные для тестов
|
|
226
|
-
│ └── <case-id>/
|
|
227
|
-
│ └── input.yaml
|
|
228
|
-
├── fixtures/ # Ожидаемые выходные данные (для L1)
|
|
229
|
-
│ └── <case-id>/
|
|
230
|
-
│ └── expected.yaml
|
|
231
|
-
└── rubrics/ # Критерии оценки (для L2)
|
|
232
|
-
└── <case-id>/
|
|
233
|
-
└── rubric.yaml
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
### Запуск тестов
|
|
237
|
-
|
|
238
|
-
```bash
|
|
239
|
-
npm run test:skills
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
### CLI-флаги
|
|
243
|
-
|
|
244
|
-
| Флаг | Описание |
|
|
245
|
-
|------|----------|
|
|
246
|
-
| `--skill <name>` | Запустить тесты только для указанного скила |
|
|
247
|
-
| `--relevant` | Запустить только тесты, соответствующие изменённым файлам |
|
|
248
|
-
| `--establish-baseline` | Запустить тесты и сохранить результаты как baseline |
|
|
249
|
-
| `--baseline-ref <ref>` | Использовать конкретный baseline (коммит, тег) |
|
|
250
|
-
| `--yes` | Автоматически подтверждать все действия |
|
|
251
|
-
|
|
252
|
-
### Режимы вердикта
|
|
253
|
-
|
|
254
|
-
| Режим | Описание |
|
|
255
|
-
|-------|----------|
|
|
256
|
-
| `no-baseline` | Первый запуск — результаты сохраняются как baseline без сравнения |
|
|
257
|
-
| `no-regression` | Сравнение с baseline — тест считается пройденным, если результат не хуже baseline |
|
|
258
|
-
|
|
259
|
-
### Принцип git write
|
|
260
|
-
|
|
261
|
-
Runner и коуч **не выполняют git write-операций**. Все изменения в кодовой базе делает исключительно пользователь. Runner только анализирует и рекомендует, но не коммитит.
|
|
262
|
-
|
|
263
|
-
### Первый запуск на новом проекте
|
|
264
|
-
|
|
265
|
-
1. Запустить тесты с флагом `--establish-baseline`
|
|
266
|
-
2. Проверить результаты: красные тесты — ожидаемы для нового проекта
|
|
267
|
-
3. Зафиксировать baseline: `git commit current/` как baseline-коммит
|
|
268
|
-
|
|
269
|
-
## Скрипты
|
|
270
|
-
|
|
271
|
-
Скрипты хранятся глобально в `~/.workflow/scripts/` и подключаются одним junction в `.workflow/src/scripts/`.
|
|
272
|
-
|
|
273
|
-
Используйте `workflow eject-scripts` для копирования скриптов в проект для кастомизации.
|
|
274
|
-
|
|
275
|
-
## Конфиги
|
|
276
|
-
|
|
277
|
-
Конфиги хранятся глобально в `~/.workflow/configs/` и подключаются одним junction в `.workflow/config/`.
|
|
278
|
-
|
|
279
|
-
Используйте `workflow eject-configs` для копирования конфигов в проект для кастомизации.
|
|
280
|
-
|
|
281
|
-
## Шаблоны планов
|
|
282
|
-
|
|
283
|
-
Шаблоны планов позволяют автоматически создавать повторяющиеся планы. Шаблоны лежат в `.workflow/plans/templates/` и содержат условия триггеров во frontmatter.
|
|
284
|
-
|
|
285
|
-
### Формат шаблона
|
|
286
|
-
|
|
287
|
-
```yaml
|
|
288
|
-
id: "TMPL-001"
|
|
289
|
-
title: "Daily manual testing"
|
|
290
|
-
type: template
|
|
291
|
-
trigger:
|
|
292
|
-
type: daily # daily | weekly | date_after | interval_days
|
|
293
|
-
params: {} # параметры, зависящие от типа
|
|
294
|
-
last_triggered: "" # обновляется автоматически при срабатывании
|
|
295
|
-
enabled: true
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
### Типы триггеров
|
|
299
|
-
|
|
300
|
-
| Тип | Параметры | Описание |
|
|
301
|
-
|-----|-----------|----------|
|
|
302
|
-
| `daily` | — | Раз в день |
|
|
303
|
-
| `weekly` | `days_of_week: [1,3,5]` (0=вс) | В указанные дни недели |
|
|
304
|
-
| `date_after` | `date: "2026-04-01"` | Один раз после указанной даты |
|
|
305
|
-
| `interval_days` | `days: 3` | Каждые N дней |
|
|
306
|
-
|
|
307
|
-
При срабатывании триггера конвейер создаёт план в `plans/current/` со статусом `approved`, далее идёт обычный поток декомпозиции.
|
|
308
|
-
|
|
309
|
-
## Типы задач
|
|
310
|
-
|
|
311
|
-
| Тип | Префикс | Описание |
|
|
312
|
-
|-----|---------|----------|
|
|
313
|
-
| `arch` | ARCH | Архитектура и планирование |
|
|
314
|
-
| `impl` | IMPL | Реализация кода |
|
|
315
|
-
| `fix` | FIX | Исправления ошибок |
|
|
316
|
-
| `review` | REVIEW | Ревью кода/документации |
|
|
317
|
-
| `docs` | DOCS | Документация |
|
|
318
|
-
| `admin` | ADMIN | Административные задачи |
|
|
319
|
-
|
|
320
|
-
## Fallback агентов и правила здоровья
|
|
321
|
-
|
|
322
|
-
Система включает механизм in-stage fallback и health-мониторинг агентов.
|
|
323
|
-
|
|
324
|
-
### Механика fallback
|
|
325
|
-
|
|
326
|
-
Когда агент падает во время выполнения задачи, система использует **artifact-snapshot** для принятия решения:
|
|
327
|
-
- Если snapshot пустой (нет записанных файлов) → выполняется fallback на следующего агента
|
|
328
|
-
- Если snapshot непустой (есть изменения) → задача переходит в состояние `goto.error`
|
|
329
|
-
|
|
330
|
-
**Пример сценария:** Qwen превысил quota и упал без записи файлов → Kilo вызван в той же попытке, task_attempts не инкрементирован.
|
|
331
|
-
|
|
332
|
-
Конфигурация snapshot:
|
|
333
|
-
```yaml
|
|
334
|
-
execution:
|
|
335
|
-
artifact_snapshot_enabled: false # по умолчанию выключено
|
|
336
|
-
snapshot_paths: ["src/", "configs/"] # что мониторить
|
|
337
|
-
snapshot_max_file_size: 524288 # файлы >512KB — только mtime+size
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
**Baseline производительности:** `p50=169ms p95=299ms files=598` (из QA-20 benchmark).
|
|
341
|
-
|
|
342
|
-
### Классификатор ошибок и health-реестр
|
|
343
|
-
|
|
344
|
-
Ошибки классифицируются по классам:
|
|
345
|
-
- `unavailable` — агент временно недоступен (quota, rate limit)
|
|
346
|
-
- `transient` — временная ошибка сети (timeout, 5xx)
|
|
347
|
-
- `misconfigured` — ошибка конфигурации (401, 403, отсутствует API key)
|
|
348
|
-
- `unmatched` — ошибка не распознана
|
|
349
|
-
|
|
350
|
-
**Семантика TTL:**
|
|
351
|
-
- `5m` — 5 минут
|
|
352
|
-
- `1h` — 1 час
|
|
353
|
-
- `until_utc_midnight` — до полуночи UTC (минимум 30 минут)
|
|
354
|
-
- `infinite` — навсегда
|
|
355
|
-
|
|
356
|
-
Файл конфигурации: `configs/agent-health-rules.yaml`. Файл состояния: `.workflow/state/agent-health.json`.
|
|
357
|
-
|
|
358
|
-
### Команда сброса
|
|
359
|
-
|
|
360
|
-
```bash
|
|
361
|
-
# показать текущее состояние
|
|
362
|
-
node .workflow/src/scripts/reset-agent-health.js
|
|
363
|
-
|
|
364
|
-
# сбросить конкретного агента
|
|
365
|
-
node .workflow/src/scripts/reset-agent-health.js --agent qwen-code
|
|
366
|
-
|
|
367
|
-
# сбросить всех агентов
|
|
368
|
-
node .workflow/src/scripts/reset-agent-health.js --all
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
### Пример добавления правила
|
|
372
|
-
|
|
373
|
-
```yaml
|
|
374
|
-
# В configs/agent-health-rules.yaml:
|
|
375
|
-
agents:
|
|
376
|
-
my-new-agent:
|
|
377
|
-
rules:
|
|
378
|
-
- id: "my-agent-quota"
|
|
379
|
-
class: "unavailable"
|
|
380
|
-
ttl: "until_utc_midnight"
|
|
381
|
-
pattern: "quota exceeded|daily limit reached"
|
|
382
|
-
exit_codes: "any"
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
## Конфигурация
|
|
386
|
-
|
|
387
|
-
### `configs/config.yaml`
|
|
388
|
-
|
|
389
|
-
Основная конфигурация воркфлоу: информация о проекте, типы задач, приоритеты, статусы, типы условий, пути, настройки отчётности.
|
|
390
|
-
|
|
391
|
-
### `configs/pipeline.yaml`
|
|
392
|
-
|
|
393
|
-
Определение конвейера: агенты, стадии, управление потоком, goto-логика, стратегии повторов.
|
|
394
|
-
|
|
395
|
-
#### `manual-gate` stage
|
|
396
|
-
|
|
397
|
-
`type: manual-gate` — встроенный тип стадии для ручного одобрения. Создаёт файл `.workflow/approvals/{step_id}.json` со статусом `pending` и ждёт решения через polling.
|
|
398
|
-
|
|
399
|
-
**Поля:**
|
|
400
|
-
- `type: manual-gate` — обязательное, идентификатор типа стадии
|
|
401
|
-
- `poll_interval_ms` — опциональное, интервал опроса в мс (default: 2000, минимум: 100)
|
|
402
|
-
- `timeout_seconds` — опциональное, таймаут в секундах (default: null — без таймаута)
|
|
403
|
-
- `goto.approved` — обязательное, следующая стадия при одобрении
|
|
404
|
-
- `goto.rejected` — обязательное, следующая стадия при отклонении
|
|
405
|
-
- `goto.timeout` — опциональное, следующая стадия при истечении таймаута
|
|
406
|
-
- `goto.aborted` — опциональное, следующая стадия при остановке runner'а (SIGTERM)
|
|
407
|
-
|
|
408
|
-
**Пример:**
|
|
409
|
-
```yaml
|
|
410
|
-
stages:
|
|
411
|
-
manual-approve-deploy:
|
|
412
|
-
type: manual-gate
|
|
413
|
-
poll_interval_ms: 2000
|
|
414
|
-
timeout_seconds: 86400
|
|
415
|
-
goto:
|
|
416
|
-
approved: continue-deploy
|
|
417
|
-
rejected: rollback
|
|
418
|
-
timeout: notify-stuck
|
|
419
|
-
aborted: end
|
|
420
|
-
```
|
|
421
|
-
|
|
422
|
-
**Два способа одобрения:**
|
|
423
|
-
1. **Через MCP-клиент** (например `workflow-mcp`): tool `approve_step({step_id, decision, comment})` пишет в файл approval. Опционально — пользователю не обязал подключать MCP.
|
|
424
|
-
2. **Прямая правка файла:** открыть `.workflow/approvals/{step_id}.json`, изменить `"status": "pending"` на `"approved"` или `"rejected"`, сохранить. **Базовый способ, работающий без какой-либо внешней инфраструктуры** — достаточно текстового редактора.
|
|
425
|
-
|
|
426
|
-
**Важное:** `manual-gate` — **opt-in**. Если ваш pipeline не использует стадии `manual-gate`, поведение полностью идентично предыдущим версиям! Никаких breaking changes.
|
|
427
|
-
|
|
428
|
-
**Формат approval-файла:**
|
|
429
|
-
```json
|
|
430
|
-
{
|
|
431
|
-
"step_id": "QA-12_manual-approve_0",
|
|
432
|
-
"ticket_id": "QA-12",
|
|
433
|
-
"stage_id": "manual-approve",
|
|
434
|
-
"attempt": 0,
|
|
435
|
-
"status": "pending",
|
|
436
|
-
"created_at": "2026-04-28T12:34:56.789Z",
|
|
437
|
-
"updated_at": "2026-04-28T12:34:56.789Z",
|
|
438
|
-
"decided_by": null,
|
|
439
|
-
"comment": null,
|
|
440
|
-
"context_snapshot": { ... }
|
|
441
|
-
}
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
### `configs/ticket-movement-rules.yaml`
|
|
445
|
-
|
|
446
|
-
Правила автоматического перемещения тикетов на основе статуса ревью.
|
|
447
|
-
|
|
448
|
-
## Структура проекта
|
|
449
|
-
|
|
450
|
-
```
|
|
451
|
-
workflow-ai/
|
|
452
|
-
├── bin/ # Точка входа CLI
|
|
453
|
-
├── src/
|
|
454
|
-
│ ├── cli.mjs # Парсинг команд
|
|
455
|
-
│ ├── runner.mjs # Оркестратор конвейера
|
|
456
|
-
│ ├── init.mjs # Инициализация проекта
|
|
457
|
-
│ ├── global-dir.mjs # Управление глобальной ~/.workflow/
|
|
458
|
-
│ ├── junction-manager.mjs # Управление junction/symlink
|
|
459
|
-
│ ├── wf-loader.mjs # Загрузчик конфигов
|
|
460
|
-
│ ├── lib/ # Библиотеки утилит
|
|
461
|
-
│ └── tests/ # Набор тестов
|
|
462
|
-
├── configs/ # Файлы конфигурации (источник)
|
|
463
|
-
├── templates/ # Шаблоны воркфлоу (источник)
|
|
464
|
-
├── agent-templates/ # Шаблоны инструкций для AI-агентов
|
|
465
|
-
└── package.json
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
## Требования
|
|
469
|
-
|
|
470
|
-
- Node.js >= 18.0.0
|
|
471
|
-
- npm
|
|
472
|
-
|
|
473
|
-
## Лицензия
|
|
474
|
-
|
|
475
|
-
MIT
|
|
1
|
+
|
|
2
|
+
- Добавлено упоминание `human-gate` в разделе про runner-стадии (пример конфига в pipeline.yaml: human-review-step типа manual-gate).
|
|
3
|
+
- Добавлено упоминание `mark-blocked` как механизм нотификации autoblocked-тикетов (поля auto_blocked_reason/attempts/at).
|
|
4
|
+
|
|
5
|
+
## Runner-стадии
|
|
6
|
+
|
|
7
|
+
В системе поддерживаются различные типы стадий для управления процессами разработки:
|
|
8
|
+
|
|
9
|
+
### Human Gate
|
|
10
|
+
|
|
11
|
+
`human-gate` — стадия ручного контроля, которая требует вмешательства человека для продолжения выполнения пайплайна. Используется для критических точек, где необходима ручная проверка или утверждение.
|
|
12
|
+
|
|
13
|
+
**Условия срабатывания:**
|
|
14
|
+
- Стадия `manual-gate-human` блокирует пайплайн до ручного снятия gate
|
|
15
|
+
- Gate снимается через команду `move-ticket.js <id> unblock`
|
|
16
|
+
- Поддерживает настройку таймаутов и условий повторных попыток
|
|
17
|
+
|
|
18
|
+
**Пример конфигурации pipeline.yaml:**
|
|
19
|
+
```yaml
|
|
20
|
+
stages:
|
|
21
|
+
- name: manual-gate-human
|
|
22
|
+
type: manual-gate-human
|
|
23
|
+
config:
|
|
24
|
+
timeout: 3600 # 1 hour
|
|
25
|
+
max_attempts: 3
|
|
26
|
+
message: "Requires human approval before deployment"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Пример полного файла `pipeline.yaml` доступен в `docs/samples/pipeline-with-human-gate.yaml`.
|
|
30
|
+
|
|
31
|
+
### Autoblock notifications
|
|
32
|
+
|
|
33
|
+
Для автоматического блокировки тикетов при возникновении проблем используется механизм `mark-blocked`. Блокированные тикеты получают дополнительные поля в frontmatter для отслеживания причин и попыток:
|
|
34
|
+
|
|
35
|
+
- `auto_blocked_reason` — причина автоматической блокировки
|
|
36
|
+
- `auto_blocked_attempts` — количество попыток разблокировки
|
|
37
|
+
- `auto_blocked_at` — время автоматической блокировки
|
package/configs/pipeline.yaml
CHANGED
|
@@ -185,6 +185,18 @@ pipeline:
|
|
|
185
185
|
workdir: "."
|
|
186
186
|
description: "Проверка доступности MCP-серверов"
|
|
187
187
|
|
|
188
|
+
script-mark-blocked:
|
|
189
|
+
command: "node"
|
|
190
|
+
args: [".workflow/src/scripts/mark-blocked.js"]
|
|
191
|
+
workdir: "."
|
|
192
|
+
description: "Обновление frontmatter и запись в alerts.jsonl при автоблокировке"
|
|
193
|
+
|
|
194
|
+
script-mark-human-rejected:
|
|
195
|
+
command: "node"
|
|
196
|
+
args: [".workflow/src/scripts/mark-blocked.js"]
|
|
197
|
+
workdir: "."
|
|
198
|
+
description: "Обновление frontmatter при отклонении human-тикета"
|
|
199
|
+
|
|
188
200
|
script-verify-artifacts:
|
|
189
201
|
command: "node"
|
|
190
202
|
args: [".workflow/src/skills/review-result/scripts/verify-artifacts.js"]
|
|
@@ -274,6 +286,10 @@ pipeline:
|
|
|
274
286
|
task_type: "$result.type"
|
|
275
287
|
required_capabilities: "$result.required_capabilities"
|
|
276
288
|
target: in-progress
|
|
289
|
+
human_ready:
|
|
290
|
+
stage: manual-gate-human
|
|
291
|
+
params:
|
|
292
|
+
ticket_id: "$result.ticket_id"
|
|
277
293
|
in_review:
|
|
278
294
|
stage: verify-artifacts
|
|
279
295
|
params:
|
|
@@ -487,6 +503,10 @@ pipeline:
|
|
|
487
503
|
task_type: "$result.type"
|
|
488
504
|
required_capabilities: "$result.required_capabilities"
|
|
489
505
|
target: in-progress
|
|
506
|
+
human_ready:
|
|
507
|
+
stage: manual-gate-human
|
|
508
|
+
params:
|
|
509
|
+
ticket_id: "$result.ticket_id"
|
|
490
510
|
in_review:
|
|
491
511
|
stage: verify-artifacts
|
|
492
512
|
params:
|
|
@@ -649,6 +669,73 @@ pipeline:
|
|
|
649
669
|
ticket_id: "$context.ticket_id"
|
|
650
670
|
attempt: "$counter.task_attempts"
|
|
651
671
|
|
|
672
|
+
# -------------------------------------------------------------------------
|
|
673
|
+
# 2d. manual-gate-human
|
|
674
|
+
# Используется для созревших human-тикетов (pick-first-task возвращает human_ready).
|
|
675
|
+
# Ждёт решения по approval-файлу (approved/rejected/timeout).
|
|
676
|
+
# -------------------------------------------------------------------------
|
|
677
|
+
manual-gate-human:
|
|
678
|
+
description: "Ожидание ручного выполнения созревшего human-тикета"
|
|
679
|
+
type: manual-gate
|
|
680
|
+
poll_interval_ms: 2000
|
|
681
|
+
timeout_seconds: 86400
|
|
682
|
+
goto:
|
|
683
|
+
approved: pick-first-task
|
|
684
|
+
rejected:
|
|
685
|
+
stage: mark-human-rejected
|
|
686
|
+
params:
|
|
687
|
+
ticket_id: "$context.ticket_id"
|
|
688
|
+
timeout:
|
|
689
|
+
stage: mark-human-rejected
|
|
690
|
+
params:
|
|
691
|
+
ticket_id: "$context.ticket_id"
|
|
692
|
+
reason: "human_gate_timeout"
|
|
693
|
+
aborted: end
|
|
694
|
+
|
|
695
|
+
# -------------------------------------------------------------------------
|
|
696
|
+
# 3b. mark-blocked
|
|
697
|
+
# Записывает причину автоблокировки в frontmatter тикета и эмитит alert
|
|
698
|
+
# в alerts.jsonl. Вставлен между increment-task-attempts.max_reached и
|
|
699
|
+
# move-ticket(blocked).
|
|
700
|
+
# -------------------------------------------------------------------------
|
|
701
|
+
mark-blocked:
|
|
702
|
+
description: "Записать причину автоблокировки в frontmatter и эмитить alert"
|
|
703
|
+
agent: script-mark-blocked
|
|
704
|
+
timeout: 30
|
|
705
|
+
goto:
|
|
706
|
+
default:
|
|
707
|
+
stage: move-ticket
|
|
708
|
+
params:
|
|
709
|
+
ticket_id: "$context.ticket_id"
|
|
710
|
+
target: blocked
|
|
711
|
+
error:
|
|
712
|
+
stage: move-ticket
|
|
713
|
+
params:
|
|
714
|
+
ticket_id: "$context.ticket_id"
|
|
715
|
+
target: blocked
|
|
716
|
+
|
|
717
|
+
# -------------------------------------------------------------------------
|
|
718
|
+
# 3b'. mark-human-rejected
|
|
719
|
+
# Записывает причину отклонения human-тикета (reject/timeout) и переходит
|
|
720
|
+
# в move-ticket(blocked). Используется ветками rejected и timeout стейджа
|
|
721
|
+
# manual-gate-human.
|
|
722
|
+
# -------------------------------------------------------------------------
|
|
723
|
+
mark-human-rejected:
|
|
724
|
+
description: "Записать причину отклонения human-тикета и переместить в blocked"
|
|
725
|
+
agent: script-mark-human-rejected
|
|
726
|
+
timeout: 30
|
|
727
|
+
goto:
|
|
728
|
+
default:
|
|
729
|
+
stage: move-ticket
|
|
730
|
+
params:
|
|
731
|
+
ticket_id: "$context.ticket_id"
|
|
732
|
+
target: blocked
|
|
733
|
+
error:
|
|
734
|
+
stage: move-ticket
|
|
735
|
+
params:
|
|
736
|
+
ticket_id: "$context.ticket_id"
|
|
737
|
+
target: blocked
|
|
738
|
+
|
|
652
739
|
# -------------------------------------------------------------------------
|
|
653
740
|
# 3c. verify-artifacts
|
|
654
741
|
# Механическая предпроверка артефактов тикета перед AI-ревью.
|
|
@@ -729,10 +816,11 @@ pipeline:
|
|
|
729
816
|
ticket_id: "$context.ticket_id"
|
|
730
817
|
target: ready
|
|
731
818
|
max_reached:
|
|
732
|
-
stage:
|
|
819
|
+
stage: mark-blocked
|
|
733
820
|
params:
|
|
734
821
|
ticket_id: "$context.ticket_id"
|
|
735
|
-
|
|
822
|
+
attempts: "$counter.task_attempts"
|
|
823
|
+
reason: "max_review_attempts"
|
|
736
824
|
|
|
737
825
|
# -------------------------------------------------------------------------
|
|
738
826
|
# 5. move-ticket
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "workflow-ai",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "AI Agent Workflow Coordinator — kanban-based pipeline for AI coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -36,9 +36,19 @@
|
|
|
36
36
|
"scripts": {
|
|
37
37
|
"test": "node --test src/tests/*.test.mjs",
|
|
38
38
|
"test:skills": "node src/scripts/run-skill-tests.js --all",
|
|
39
|
+
"coverage": "c8 node --test src/tests/*.test.mjs",
|
|
40
|
+
"coverage:mark-blocked": "c8 node --test src/tests/scripts-mark-blocked.test.mjs",
|
|
41
|
+
"coverage:pick-next-task": "node scripts/coverage-pick-next-task.js",
|
|
42
|
+
"coverage:move-ticket": "c8 node --test src/tests/scripts-move-ticket-approval-hook.test.mjs",
|
|
43
|
+
"mutation-test": "stryker run",
|
|
39
44
|
"release": "npm version patch && npm publish"
|
|
40
45
|
},
|
|
41
46
|
"dependencies": {
|
|
42
47
|
"js-yaml": "^4.1.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@stryker-mutator/core": "^9.6.1",
|
|
51
|
+
"@stryker-mutator/javascript-mutator": "^4.0.0",
|
|
52
|
+
"c8": "^11.0.0"
|
|
43
53
|
}
|
|
44
54
|
}
|
package/src/runner.mjs
CHANGED
|
@@ -176,7 +176,8 @@ class Logger {
|
|
|
176
176
|
*/
|
|
177
177
|
stageStart(stageId, agentId, skillId) {
|
|
178
178
|
this.stats.stagesStarted++;
|
|
179
|
-
this.
|
|
179
|
+
const ticketInfo = this.context && this.context.ticket_id ? ` ticket="${this.context.ticket_id}"` : "";
|
|
180
|
+
this.info(`START stage="${stageId}" agent="${agentId}" skill="${skillId}"${ticketInfo}`, stageId);
|
|
180
181
|
}
|
|
181
182
|
|
|
182
183
|
/**
|
|
@@ -100,10 +100,12 @@ function findTicketInColumns(ticketId) {
|
|
|
100
100
|
|
|
101
101
|
async function checkRelevance(ticketPath) {
|
|
102
102
|
if (!fs.existsSync(ticketPath)) {
|
|
103
|
+
// file_not_found — это не ошибка скрипта (verdict=relevant — fail-safe).
|
|
104
|
+
// Не выходим с exit=1, чтобы pipeline продолжил выполнение следующего стейджа.
|
|
103
105
|
return {
|
|
104
106
|
verdict: "relevant",
|
|
105
107
|
reason: "file_not_found",
|
|
106
|
-
|
|
108
|
+
warning: `Ticket file not found: ${ticketPath}`,
|
|
107
109
|
};
|
|
108
110
|
}
|
|
109
111
|
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* mark-blocked.js - Скрипт для обновления frontmatter тикета и записи в alerts.jsonl
|
|
5
|
+
*
|
|
6
|
+
* Использование:
|
|
7
|
+
* node mark-blocked.js <ticket_id> --attempts=N --reason=<str>
|
|
8
|
+
*
|
|
9
|
+
* Примеры:
|
|
10
|
+
* node mark-blocked.js IMPL-59 --attempts=6 --reason=max_review_attempts
|
|
11
|
+
* node mark-blocked.js QA-40 --attempts=3 --reason=human_gate_rejected
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import fs from "fs";
|
|
15
|
+
import path from "path";
|
|
16
|
+
import YAML from "workflow-ai/lib/js-yaml.mjs";
|
|
17
|
+
import { findProjectRoot } from "workflow-ai/lib/find-root.mjs";
|
|
18
|
+
import {
|
|
19
|
+
parseFrontmatter,
|
|
20
|
+
printResult,
|
|
21
|
+
serializeFrontmatter,
|
|
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
|
+
// Директория state
|
|
30
|
+
const STATE_DIR = path.join(PROJECT_DIR, ".workflow", "state");
|
|
31
|
+
const ALERTS_FILE = path.join(STATE_DIR, "alerts.jsonl");
|
|
32
|
+
|
|
33
|
+
// Парсинг аргументов
|
|
34
|
+
const args = process.argv.slice(2);
|
|
35
|
+
if (args.length < 3) {
|
|
36
|
+
console.error("Ошибка: недостаточно аргументов");
|
|
37
|
+
console.error("Использование: node mark-blocked.js <ticket_id> --attempts=N --reason=<str>");
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const ticketId = args[0];
|
|
42
|
+
let attempts = null;
|
|
43
|
+
let reason = null;
|
|
44
|
+
|
|
45
|
+
// Парсинг флагов
|
|
46
|
+
for (let i = 1; i < args.length; i++) {
|
|
47
|
+
const arg = args[i];
|
|
48
|
+
if (arg.startsWith("--attempts=")) {
|
|
49
|
+
attempts = parseInt(arg.substring("--attempts=".length), 10);
|
|
50
|
+
if (isNaN(attempts)) {
|
|
51
|
+
console.error("Ошибка: некорректный формат --attempts");
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
} else if (arg.startsWith("--reason=")) {
|
|
55
|
+
reason = arg.substring("--reason=".length);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Проверка обязательного параметра reason
|
|
60
|
+
if (!reason) {
|
|
61
|
+
console.error("Ошибка: параметр --reason обязателен");
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Поиск файла тикета рекурсивно
|
|
66
|
+
function findTicketFile(ticketId, searchDir) {
|
|
67
|
+
try {
|
|
68
|
+
const files = fs.readdirSync(searchDir, { withFileTypes: true });
|
|
69
|
+
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
const fullPath = path.join(searchDir, file.name);
|
|
72
|
+
|
|
73
|
+
if (file.isDirectory()) {
|
|
74
|
+
// Рекурсивный поиск в поддиректориях
|
|
75
|
+
const found = findTicketFile(ticketId, fullPath);
|
|
76
|
+
if (found) return found;
|
|
77
|
+
} else if (file.isFile() && file.name.endsWith('.md') && file.name.startsWith(ticketId)) {
|
|
78
|
+
return fullPath;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error(`Ошибка при чтении директории ${searchDir}:`, error.message);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Основная функция
|
|
89
|
+
function main() {
|
|
90
|
+
try {
|
|
91
|
+
// Поиск файла тикета
|
|
92
|
+
const ticketFile = findTicketFile(ticketId, TICKETS_DIR);
|
|
93
|
+
if (!ticketFile) {
|
|
94
|
+
console.error(`Ошибка: тикет ${ticketId} не найден в ${TICKETS_DIR}`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Чтение файла тикета
|
|
99
|
+
const content = fs.readFileSync(ticketFile, 'utf8');
|
|
100
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
101
|
+
|
|
102
|
+
// Обновление frontmatter
|
|
103
|
+
const now = new Date().toISOString();
|
|
104
|
+
frontmatter.auto_blocked_reason = reason;
|
|
105
|
+
frontmatter.auto_blocked_attempts = attempts;
|
|
106
|
+
frontmatter.auto_blocked_at = now;
|
|
107
|
+
|
|
108
|
+
// Сериализация и запись обратно в файл
|
|
109
|
+
const newContent = serializeFrontmatter(frontmatter) + body;
|
|
110
|
+
fs.writeFileSync(ticketFile, newContent, 'utf8');
|
|
111
|
+
|
|
112
|
+
console.log(`✅ Frontmatter тикета ${ticketId} обновлен`);
|
|
113
|
+
|
|
114
|
+
// Попытка записи в alerts.jsonl
|
|
115
|
+
try {
|
|
116
|
+
// Создание директории state если не существует
|
|
117
|
+
if (!fs.existsSync(STATE_DIR)) {
|
|
118
|
+
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
119
|
+
console.log(`✅ Директория ${STATE_DIR} создана`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Формирование JSONL записи
|
|
123
|
+
const alertEntry = {
|
|
124
|
+
timestamp: now,
|
|
125
|
+
severity: "warning",
|
|
126
|
+
kind: "ticket_auto_blocked",
|
|
127
|
+
project: path.basename(PROJECT_DIR),
|
|
128
|
+
ticket_id: ticketId,
|
|
129
|
+
attempts: attempts,
|
|
130
|
+
reason: reason,
|
|
131
|
+
stage: "review-result"
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Append-only запись в alerts.jsonl
|
|
135
|
+
fs.appendFileSync(ALERTS_FILE, JSON.stringify(alertEntry) + '\n', 'utf8');
|
|
136
|
+
console.log(`✅ Запись добавлена в ${ALERTS_FILE}`);
|
|
137
|
+
|
|
138
|
+
} catch (alertError) {
|
|
139
|
+
console.warn(`⚠️ Предупреждение: не удалось записать в alerts.jsonl: ${alertError.message}`);
|
|
140
|
+
console.log(`ℹ️ Frontmatter обновлен, запись в alerts пропущена`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Вывод результата
|
|
144
|
+
printResult({
|
|
145
|
+
ticket_id: ticketId,
|
|
146
|
+
reason: reason,
|
|
147
|
+
attempts: attempts,
|
|
148
|
+
blocked_at: now,
|
|
149
|
+
alerts_file: ALERTS_FILE,
|
|
150
|
+
status: "completed"
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
} catch (error) {
|
|
154
|
+
console.error(`Ошибка: ${error.message}`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Запуск скрипта
|
|
160
|
+
main();
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import fs from "fs";
|
|
14
14
|
import path from "path";
|
|
15
|
+
import { fileURLToPath } from "url";
|
|
15
16
|
import YAML from "workflow-ai/lib/js-yaml.mjs";
|
|
16
17
|
import { findProjectRoot } from "workflow-ai/lib/find-root.mjs";
|
|
17
18
|
import {
|
|
@@ -21,6 +22,11 @@ import {
|
|
|
21
22
|
getLastReviewStatus,
|
|
22
23
|
} from "workflow-ai/lib/utils.mjs";
|
|
23
24
|
|
|
25
|
+
const logger = {
|
|
26
|
+
info: (msg) => console.error(`[INFO] ${msg}`),
|
|
27
|
+
warn: (msg) => console.error(`[WARN] ${msg}`),
|
|
28
|
+
};
|
|
29
|
+
|
|
24
30
|
// Корень проекта
|
|
25
31
|
const PROJECT_DIR = findProjectRoot();
|
|
26
32
|
// Базовая директория workflow
|
|
@@ -89,6 +95,45 @@ function isValidTransition(from, to) {
|
|
|
89
95
|
return { valid: true };
|
|
90
96
|
}
|
|
91
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Hook для обновления approval-файлов при перемещении тикета
|
|
100
|
+
* @param {string} ticketId - ID тикета
|
|
101
|
+
* @param {string} target - целевой статус
|
|
102
|
+
* @param {object} fsModule - модуль fs (для mock в тестах)
|
|
103
|
+
* @param {string} workflowDir - директория .workflow
|
|
104
|
+
*/
|
|
105
|
+
function updateApprovalFilesHook(ticketId, target, fsModule = fs, workflowDir = WORKFLOW_DIR) {
|
|
106
|
+
try {
|
|
107
|
+
const approvalsDir = path.join(workflowDir, "approvals");
|
|
108
|
+
if (fsModule.existsSync(approvalsDir)) {
|
|
109
|
+
const files = fsModule.readdirSync(approvalsDir);
|
|
110
|
+
const escapedTicketId = ticketId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
111
|
+
const pattern = new RegExp(`^${escapedTicketId}_manual-gate-.*_\\d+\\.json$`);
|
|
112
|
+
for (const file of files) {
|
|
113
|
+
if (!pattern.test(file)) continue;
|
|
114
|
+
const filePath = path.join(approvalsDir, file);
|
|
115
|
+
try {
|
|
116
|
+
const data = JSON.parse(fsModule.readFileSync(filePath, "utf8"));
|
|
117
|
+
if (data.status === "pending") {
|
|
118
|
+
data.status = "approved";
|
|
119
|
+
data.decided_by = "move-ticket";
|
|
120
|
+
data.comment = `auto-approved on move to ${target}`;
|
|
121
|
+
data.updated_at = new Date().toISOString();
|
|
122
|
+
fsModule.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
|
|
123
|
+
logger.info(`Approval file ${file} auto-approved on move to ${target}`);
|
|
124
|
+
}
|
|
125
|
+
} catch (err) {
|
|
126
|
+
logger.warn(`Corrupt approval file ${file}: ${err.message}`);
|
|
127
|
+
// продолжаем, не падаем
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} catch (err) {
|
|
132
|
+
// Ошибка hook'а не должна фейлить само перемещение
|
|
133
|
+
logger.warn(`Approval hook error: ${err.message}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
92
137
|
/**
|
|
93
138
|
* Основная функция перемещения тикета
|
|
94
139
|
*/
|
|
@@ -210,6 +255,9 @@ async function moveTicket(ticketId, target) {
|
|
|
210
255
|
};
|
|
211
256
|
}
|
|
212
257
|
|
|
258
|
+
// Hook: обновление approval-файлов (если есть) — срабатывает на любой move
|
|
259
|
+
updateApprovalFilesHook(ticketId, target);
|
|
260
|
+
|
|
213
261
|
return {
|
|
214
262
|
status: "moved",
|
|
215
263
|
ticket_id: ticketId,
|
|
@@ -218,43 +266,60 @@ async function moveTicket(ticketId, target) {
|
|
|
218
266
|
};
|
|
219
267
|
}
|
|
220
268
|
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const targetMatch = prompt.match(/target:\s*(\S+)/);
|
|
235
|
-
ticketId = ticketMatch?.[1];
|
|
236
|
-
target = targetMatch?.[1];
|
|
237
|
-
if (!ticketId || !target) {
|
|
238
|
-
console.error(
|
|
239
|
-
"[ERROR] Cannot parse ticket_id or target from pipeline context",
|
|
240
|
-
);
|
|
241
|
-
printResult({
|
|
242
|
-
status: "error",
|
|
243
|
-
error: "Missing ticket_id or target in pipeline context",
|
|
244
|
-
});
|
|
245
|
-
process.exit(1);
|
|
269
|
+
// Export for testing
|
|
270
|
+
export { moveTicket, updateApprovalFilesHook };
|
|
271
|
+
|
|
272
|
+
// Main entry point — guard prevents execution when imported as module in tests.
|
|
273
|
+
// Используем fs.realpathSync чтобы корректно сравнивать пути на Windows когда .workflow/src/scripts/ — junction.
|
|
274
|
+
// Без realpathSync argv[1] = путь через junction, а import.meta.url = разрешённый target — строки не совпадают.
|
|
275
|
+
function __isMainModule() {
|
|
276
|
+
try {
|
|
277
|
+
const argvPath = fs.realpathSync(path.resolve(process.argv[1] || ""));
|
|
278
|
+
const metaPath = fileURLToPath(import.meta.url);
|
|
279
|
+
return argvPath === metaPath;
|
|
280
|
+
} catch {
|
|
281
|
+
return false;
|
|
246
282
|
}
|
|
247
|
-
} 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" });
|
|
252
|
-
process.exit(1);
|
|
253
283
|
}
|
|
254
284
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
285
|
+
if (__isMainModule()) {
|
|
286
|
+
const rawArgs = process.argv.slice(2);
|
|
287
|
+
let ticketId, target;
|
|
288
|
+
|
|
289
|
+
if (rawArgs.length >= 2) {
|
|
290
|
+
// Прямой вызов: node move-ticket.js IMPL-001 in-progress
|
|
291
|
+
ticketId = rawArgs[0];
|
|
292
|
+
target = rawArgs[1];
|
|
293
|
+
} else if (rawArgs.length === 1) {
|
|
294
|
+
// Вызов через pipeline runner: один аргумент — промпт с контекстом
|
|
295
|
+
// Формат: "skill-name\n\nContext:\n ticket_id: X\n target: Y\n..."
|
|
296
|
+
const prompt = rawArgs[0];
|
|
297
|
+
const ticketMatch = prompt.match(/ticket_id:\s*(\S+)/);
|
|
298
|
+
const targetMatch = prompt.match(/target:\s*(\S+)/);
|
|
299
|
+
ticketId = ticketMatch?.[1];
|
|
300
|
+
target = targetMatch?.[1];
|
|
301
|
+
if (!ticketId || !target) {
|
|
302
|
+
console.error(
|
|
303
|
+
"[ERROR] Cannot parse ticket_id or target from pipeline context",
|
|
304
|
+
);
|
|
305
|
+
printResult({
|
|
306
|
+
status: "error",
|
|
307
|
+
error: "Missing ticket_id or target in pipeline context",
|
|
308
|
+
});
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
console.error("Usage: node move-ticket.js <ticket_id> <target>");
|
|
313
|
+
console.error("Example: node move-ticket.js IMPL-001 in-progress");
|
|
314
|
+
console.error("Available targets:", VALID_STATUSES.join(", "));
|
|
315
|
+
printResult({ status: "error", error: "Missing arguments" });
|
|
258
316
|
process.exit(1);
|
|
259
317
|
}
|
|
260
|
-
|
|
318
|
+
|
|
319
|
+
moveTicket(ticketId, target).then((result) => {
|
|
320
|
+
printResult(result);
|
|
321
|
+
if (result.status === "error") {
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
@@ -567,28 +567,25 @@ function pickNextTicket(planId) {
|
|
|
567
567
|
return { status: 'empty', reason: 'No tickets in ready/' };
|
|
568
568
|
}
|
|
569
569
|
|
|
570
|
-
//
|
|
571
|
-
const
|
|
570
|
+
// Фильтрация: разделяем на обычные и human с проверкой условий/зависимостей
|
|
571
|
+
const eligibleNonHuman = [];
|
|
572
|
+
const humanCandidates = [];
|
|
573
|
+
|
|
574
|
+
for (const ticket of tickets) {
|
|
572
575
|
const { frontmatter } = ticket;
|
|
573
576
|
|
|
574
|
-
// Пропускаем тикеты, требующие ручного выполнения
|
|
575
|
-
if (frontmatter.type === 'human') {
|
|
576
|
-
logger.info(`Skipping ticket ${ticket.id}: type is 'human' (requires manual execution)`);
|
|
577
|
-
return false;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
577
|
// Проверка условий
|
|
581
578
|
const conditions = frontmatter.conditions || [];
|
|
582
579
|
const conditionsMet = conditions.every(checkCondition);
|
|
583
580
|
if (!conditionsMet) {
|
|
584
|
-
|
|
581
|
+
continue;
|
|
585
582
|
}
|
|
586
583
|
|
|
587
584
|
// Проверка зависимостей
|
|
588
585
|
const dependencies = frontmatter.dependencies || [];
|
|
589
586
|
const depsMet = checkDependencies(dependencies);
|
|
590
587
|
if (!depsMet) {
|
|
591
|
-
|
|
588
|
+
continue;
|
|
592
589
|
}
|
|
593
590
|
|
|
594
591
|
// Обнаружение и удаление дубликатов: тикет не должен существовать в других колонках
|
|
@@ -607,46 +604,75 @@ function pickNextTicket(planId) {
|
|
|
607
604
|
} catch (err) {
|
|
608
605
|
logger.error(`Failed to archive duplicate ${ticket.id}: ${err.message}`);
|
|
609
606
|
}
|
|
610
|
-
|
|
607
|
+
continue;
|
|
611
608
|
}
|
|
612
609
|
|
|
613
|
-
|
|
614
|
-
|
|
610
|
+
// Разделение по типу
|
|
611
|
+
if (frontmatter.type === 'human') {
|
|
612
|
+
humanCandidates.push(ticket);
|
|
613
|
+
} else {
|
|
614
|
+
eligibleNonHuman.push(ticket);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Имеются ли обычные (non-human) готовые тикеты — старший приоритет
|
|
619
|
+
if (eligibleNonHuman.length > 0) {
|
|
620
|
+
eligibleNonHuman.sort((a, b) => {
|
|
621
|
+
const priorityA = a.frontmatter.priority || 999;
|
|
622
|
+
const priorityB = b.frontmatter.priority || 999;
|
|
615
623
|
|
|
616
|
-
|
|
624
|
+
if (priorityA !== priorityB) {
|
|
625
|
+
return priorityA - priorityB;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const dateA = new Date(a.frontmatter.created_at || '9999-12-31');
|
|
629
|
+
const dateB = new Date(b.frontmatter.created_at || '9999-12-31');
|
|
630
|
+
return dateA - dateB;
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
const selected = eligibleNonHuman[0];
|
|
617
634
|
return {
|
|
618
|
-
status: '
|
|
619
|
-
|
|
635
|
+
status: 'found',
|
|
636
|
+
ticket_id: selected.id,
|
|
637
|
+
priority: selected.frontmatter.priority,
|
|
638
|
+
title: selected.frontmatter.title,
|
|
639
|
+
type: selected.frontmatter.type,
|
|
640
|
+
required_capabilities: JSON.stringify(selected.frontmatter.required_capabilities || [])
|
|
620
641
|
};
|
|
621
642
|
}
|
|
622
643
|
|
|
623
|
-
//
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
644
|
+
// Если есть созревшие human-тикеты — новый статус human_ready (для manual-gate)
|
|
645
|
+
if (humanCandidates.length > 0) {
|
|
646
|
+
humanCandidates.sort((a, b) => {
|
|
647
|
+
const priorityA = a.frontmatter.priority || 999;
|
|
648
|
+
const priorityB = b.frontmatter.priority || 999;
|
|
627
649
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
650
|
+
if (priorityA !== priorityB) {
|
|
651
|
+
return priorityA - priorityB;
|
|
652
|
+
}
|
|
631
653
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
});
|
|
654
|
+
const dateA = new Date(a.frontmatter.created_at || '9999-12-31');
|
|
655
|
+
const dateB = new Date(b.frontmatter.created_at || '9999-12-31');
|
|
656
|
+
return dateA - dateB;
|
|
657
|
+
});
|
|
637
658
|
|
|
638
|
-
|
|
659
|
+
const selected = humanCandidates[0];
|
|
660
|
+
return {
|
|
661
|
+
status: 'human_ready',
|
|
662
|
+
ticket_id: selected.id,
|
|
663
|
+
priority: selected.frontmatter.priority,
|
|
664
|
+
title: selected.frontmatter.title,
|
|
665
|
+
pending_count: humanCandidates.length
|
|
666
|
+
};
|
|
667
|
+
}
|
|
639
668
|
|
|
640
669
|
return {
|
|
641
|
-
status: '
|
|
642
|
-
|
|
643
|
-
priority: selected.frontmatter.priority,
|
|
644
|
-
title: selected.frontmatter.title,
|
|
645
|
-
type: selected.frontmatter.type,
|
|
646
|
-
required_capabilities: JSON.stringify(selected.frontmatter.required_capabilities || [])
|
|
670
|
+
status: 'empty',
|
|
671
|
+
reason: 'No eligible non-human tickets (and no ready human tickets)'
|
|
647
672
|
};
|
|
648
673
|
}
|
|
649
674
|
|
|
675
|
+
|
|
650
676
|
/**
|
|
651
677
|
* Архивирует все done-тикеты, принадлежащие архивным планам (plans/archive/).
|
|
652
678
|
* Сканирует все планы в plans/archive/, находит их тикеты в done/ и перемещает в archive/.
|
|
@@ -778,6 +804,9 @@ async function main() {
|
|
|
778
804
|
}
|
|
779
805
|
}
|
|
780
806
|
|
|
807
|
+
// Экспортируем функции для тестирования
|
|
808
|
+
export { pickNextTicket, readReadyTickets, readReviewTickets, readInProgressTickets, findCompletedInProgress, filterByPlan };
|
|
809
|
+
|
|
781
810
|
main().catch(e => {
|
|
782
811
|
logger.error(e.message);
|
|
783
812
|
printResult({ status: 'error', error: e.message });
|
|
@@ -123,7 +123,7 @@ ticket_prefix: COACH
|
|
|
123
123
|
|
|
124
124
|
## Принципы
|
|
125
125
|
|
|
126
|
-
1. **Root Cause First** — при обнаружении проблемы в артефакте (тикете, плане, отчёте) всегда определи скил-источник, который создал этот артефакт, и предложи исправить **скил** первым. Не предлагай ручную правку артефактов (последствий), пока корневая причина (скил) не исправлена. Порядок действий: (1) найти скил-источник → (2) проследить цепочку вверх: если артефакт-источник (план, шаблон) уже содержал дефект — root cause в скиле, создавшем **его**, а не в скиле-обработчике → (3) исправить скил → (4) если нужно, предложить пересоздать артефакт исправленным скилом. **Антипаттерн «остановка на ближайшем скиле»:** тикет неатомарен → правишь декомпозитор. Но если задача **плана** уже неатомарна — root cause в скиле планирования, декомпозитор — вторая линия обороны. **Антипаттерн:** если данные невалидны — root cause в том, кто/что генерирует данные (шаблон, скил, воркфлоу), а НЕ в обработчике данных (скрипт, парсер). Не правь обработчик под невалидный формат — исправь источник формата. **⚠️ Обязательно перед правкой:** прочитай лог или артефакт до конца — определи точный паттерн нарушения (кто, когда, что именно записал). Гипотеза о root cause без evidence из лога — не основание для правки. **Семантика первична:** перед диагностикой сформулируй назначение скила одним предложением (что он должен решать, что НЕ должен). Если поведение противоречит назначению — это ошибка в скиле, не в смежных компонентах. **⚠️ Физический автор ≠ семантический владелец:** при определении скила-источника ищи не «кто владеет предметной областью артефакта», а **кто физически записывает** (Edit/Write) проблемный фрагмент. Если скил A выполняет предметную работу, но скил B записывает результат в тикет — root cause в инструкциях скила B, а не A. Антипаттерн: «тикет предметной области X → правлю скил предметной области», хотя физическую запись в тикет выполняет скил-исполнитель. **⛔ Повторный инцидент по той же корневой проблеме:** перед формулированием правки **обязательно** прогрепай `coach-backlog.yaml` на ключевые термины текущей проблемы (имя скила-жертвы, имя нарушенного правила, имя задействованной нормы). Если обнаружен CHG за последние 30 дней на тот же скил и ту же корневую проблему — это сигнал, что **текстовое усиление инструкции не работает** (предыдущий текст уже содержал норму, но нарушитель её проигнорировал). В этом случае: (а) ещё одна текстовая правка того же скила — недостаточная мера; (б) обязательно создай тикет эскалации стейкхолдеру с рекомендацией ввести **машинную защиту**, не зависящую от дисциплины агента (валидация пайплайном, пост-гейт-стадия, автоматический откат, инфраструктурная проверка); (в) в тикете явно опиши, что попытки дисциплинарного усиления исчерпаны, и почему только машинная защита закрывает класс ошибки. Текстовую правку всё равно применяй — она страхует дисциплинированного агента, — но **не считай её решением проблемы**, пока машинная защита не введена.
|
|
126
|
+
1. **Root Cause First** — при обнаружении проблемы в артефакте (тикете, плане, отчёте) всегда определи скил-источник, который создал этот артефакт, и предложи исправить **скил** первым. Не предлагай ручную правку артефактов (последствий), пока корневая причина (скил) не исправлена. Порядок действий: (1) найти скил-источник → (2) проследить цепочку вверх: если артефакт-источник (план, шаблон) уже содержал дефект — root cause в скиле, создавшем **его**, а не в скиле-обработчике → (3) исправить скил → (4) если нужно, предложить пересоздать артефакт исправленным скилом. **Антипаттерн «остановка на ближайшем скиле»:** тикет неатомарен → правишь декомпозитор. Но если задача **плана** уже неатомарна — root cause в скиле планирования, декомпозитор — вторая линия обороны. **Антипаттерн:** если данные невалидны — root cause в том, кто/что генерирует данные (шаблон, скил, воркфлоу), а НЕ в обработчике данных (скрипт, парсер). Не правь обработчик под невалидный формат — исправь источник формата. **⚠️ Обязательно перед правкой:** прочитай лог или артефакт до конца — определи точный паттерн нарушения (кто, когда, что именно записал). Гипотеза о root cause без evidence из лога — не основание для правки. **Семантика первична:** перед диагностикой сформулируй назначение скила одним предложением (что он должен решать, что НЕ должен). Если поведение противоречит назначению — это ошибка в скиле, не в смежных компонентах. **⚠️ Физический автор ≠ семантический владелец:** при определении скила-источника ищи не «кто владеет предметной областью артефакта», а **кто физически записывает** (Edit/Write) проблемный фрагмент. Если скил A выполняет предметную работу, но скил B записывает результат в тикет — root cause в инструкциях скила B, а не A. Антипаттерн: «тикет предметной области X → правлю скил предметной области», хотя физическую запись в тикет выполняет скил-исполнитель. **⛔ Повторный инцидент по той же корневой проблеме:** перед формулированием правки **обязательно** прогрепай `coach-backlog.yaml` на ключевые термины текущей проблемы (имя скила-жертвы, имя нарушенного правила, имя задействованной нормы). Если обнаружен CHG за последние 30 дней на тот же скил и ту же корневую проблему — **сначала сравни даты выполнения тикета-нарушителя и применения предыдущего CHG, до классификации «повторный инцидент»**. Источники дат: для CHG — `git log` на файл скила (commit-дата правки) или `analyzed_tickets.analyzed_date` в `coach-backlog.yaml`; для тикета — `updated_at`/`completed_at` в frontmatter или ближайшие временные метки в логе пайплайна, где произошёл инцидент (стадии execute-task / review-result, дата записи `## Ревью`). **Если выполнение тикета предшествует дате CHG** — это **не** повторный инцидент: тикет проходил пайплайн до того, как текстовое усиление было применено, нарушение исторически объяснимо (фикса ещё не существовало). Класс — обычный новый, эскалация на машинную защиту **не нужна**, обработай как стандартный CHG. **Только если выполнение тикета строго ПОСЛЕ даты CHG** — это сигнал, что **текстовое усиление инструкции не работает** (предыдущий текст уже содержал норму, но нарушитель её проигнорировал). В этом случае: (а) ещё одна текстовая правка того же скила — недостаточная мера; (б) обязательно создай тикет эскалации стейкхолдеру с рекомендацией ввести **машинную защиту**, не зависящую от дисциплины агента (валидация пайплайном, пост-гейт-стадия, автоматический откат, инфраструктурная проверка); (в) в тикете явно опиши, что попытки дисциплинарного усиления исчерпаны, и почему только машинная защита закрывает класс ошибки. Текстовую правку всё равно применяй — она страхует дисциплинированного агента, — но **не считай её решением проблемы**, пока машинная защита не введена. Антипаттерн 1: «усилю формулировку ещё жёстче, напишу ⛔ крупнее» — агент, который не прочитал прошлую норму, не прочитает и новую. Антипаттерн 2: классифицировать «повторный инцидент» по факту повторяемости пары (скил, класс ошибки) **без сравнения дат** CHG и выполнения тикета. Эскалация без date-check — преждевременная: возможно, тикет проходил пайплайн ещё до текстового усиления, и фикс не имел шанса сработать. **Date-check — необходимое, но не достаточное условие.** Дополнительно сверь **семантический класс ошибки** предыдущего CHG и текущего инцидента: даже если выполнение тикета строго после CHG, инцидент может быть другого класса (CHG покрывал ортогональную проблему — например, semantic-mismatch vs structural-integrity, test-isolation vs review-rubric). Тогда это новый класс, а не повторный инцидент, и эскалация не требуется. Условие эскалации: **(дата выполнения > дата CHG) И (семантический класс совпадает)**.
|
|
127
127
|
2. **Evidence-Based** — все выводы основаны на данных из завершённых тикетов, планов и логов пайплайна, а не на предположениях. **При анализе лога обязательно строй временную диаграмму ключевых событий по ID артефакта** (тикет, план, отчёт): проследи всю цепочку перемещений/изменений артефакта от первого упоминания до последнего, обращая внимание на события, отстоящие далеко друг от друга по времени, но связанные одним ID. **Антипаттерн:** прочитал начало лога (события archive/cleanup), прочитал середину (события create/decompose), но **не сопоставил** их — упустил коллизию ID или другой паттерн взаимного влияния. Перед формулированием findings задай себе вопрос: «Я проверил всю историю каждого упомянутого ID, или только последнее событие с ним?» **⚠️ Проверка фактической практики перед нормативной правкой:** если правка вводит новое правило про путь, имя, формат, расположение — **обязательно `Grep` по всему проекту** (код, конфиги, скилы, тикеты) на ключевой термин этого правила, чтобы измерить **масштаб уже существующей практики**. Один-два аномальных артефакта — не основание объявлять их новой нормой. Если фактическая практика противоположна гипотезе — гипотеза неверна, или (если стейкхолдер действительно хочет миграцию) нужен явный миграционный план и согласие на масштаб правок. Антипаттерн: получил короткий ответ стейкхолдера на развилку → принял за сильное правило → пошёл править скилы → не проверил, что в проекте 20+ артефактов уже живут по противоположному правилу. Перед каждой нормативной правкой задай себе вопрос: «Сколько уже существующих файлов/строк проекта противоречат тому, что я собираюсь записать?» Если ответ > 5 — остановись и переспроси у стейкхолдера, точно ли это миграция. **⚠️ Обязательный diff формулировок при анализе цепочки артефактов:** когда анализируешь инцидент, прошедший через несколько стадий (план → тикет → исполнение → ревью), **перед назначением виновного** обязан построчно сопоставить формулировки критериев на каждом стыке: (1) дословная строка критерия в плане, (2) дословная строка в тикете, (3) что реально проверяет assertion/тест, (4) что ревьюер проверял. Виновник — стадия, на которой произошла первая потеря семантики. Антипаттерн: прочитал план и увидел расхождение с результатом → обвинил последнюю стадию (ревьюера), не проверив, на какой промежуточной стадии формулировка была ослаблена. Гипотеза «ревьюер должен был поймать» невалидна, если ревьюер работал по формулировке тикета, а тикет уже не содержал потерянного уточнения.
|
|
128
128
|
**⚠️ Антипаттерн «уход в формулировки вместо root cause»:** стейкхолдер задаёт вопрос о наблюдаемом дефекте («почему не поймали?»), а коуч анализирует текст формулировок, семантику переносов, чеклисты — вместо того чтобы ответить на прямой вопрос: какой конкретный шаг в какой конкретной стадии не выполнил конкретное физическое действие (открыть файл, посмотреть на картинку, запустить команду). Формулировки — это причина второго порядка; причина первого порядка — «агент X не сделал действие Y». Всегда начинай с причины первого порядка, потом объясняй, почему инструкции это допустили.
|
|
129
129
|
**⚠️ Антипаттерн «оценка по результату вместо сверки с инструкцией»:** при анализе действия агента — **не оценивай** его «разумность» или «допустимость» по своему суждению. Вместо этого открой скил агента и **дословно сверь** действие с инструкцией. Если инструкция говорит «разбей тикет», а агент объединил шаги — это нарушение, даже если результат выглядит «приемлемо». Коуч не имеет права смягчать finding на основании того, что дефект «небольшой» или «единичный» — скил либо нарушен, либо нет.
|
|
@@ -197,7 +197,7 @@ description: >
|
|
|
197
197
|
|
|
198
198
|
1. **Ни одного чекбокса `[ ]`** в секции критериев готовности / DoD. Все переведены в `[x]` или помечены причиной невыполнения (`[x] Пункт — не применимо: <причина>`).
|
|
199
199
|
2. **Секция `## Result` / `## Результат выполнения` физически заполнена** — содержит реальный текст (summary, изменённые файлы, заметки), а не оставлена в виде скелета-шаблона с `### Что сделано\n- ...`.
|
|
200
|
-
3. **Frontmatter не
|
|
200
|
+
3. **Frontmatter не модифицирован вообще** — никаких новых ключей, никаких изменённых значений (включая `notes`, `tags`, `context.*`). Frontmatter — собственность создателя тикета и пайплайна. Эта проверка шире, чем запрет на `status:` и `completed_at:` (см. ограничение #5): любая правка frontmatter — потенциально источник YAML-несовместимостей (двоеточие+пробел в неэкранированном скаляре, дубль ключа, нарушенный отступ массива). Если необходимо зафиксировать прогресс/итерации/наблюдения — пиши в **тело тикета** (секции Result/Заметки), не в frontmatter.
|
|
201
201
|
|
|
202
202
|
Если хоть один пункт нарушен — **вернись к шагу 5 или 7** и выполни правки инструментом `Edit` на файл тикета. Не обходи эту проверку: вывод `---RESULT---` при пустом Result или `[ ]`-чекбоксах считается **призрачным выполнением** (см. ограничение #9) и ведёт к retry → blocked.
|
|
203
203
|
|
|
@@ -57,7 +57,29 @@ description: >
|
|
|
57
57
|
|
|
58
58
|
## Шаги проверки
|
|
59
59
|
|
|
60
|
-
### 0.
|
|
60
|
+
### 0. Целостность frontmatter (pre-flight)
|
|
61
|
+
|
|
62
|
+
Перед любой содержательной проверкой убедись, что frontmatter тикета **валидно парсится** YAML-парсером. Запусти:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
node -e "const y=require('js-yaml'),f=require('fs');const c=f.readFileSync(process.argv[1],'utf8');const m=c.match(/^---\\n([\\s\\S]*?)\\n---/);if(!m){console.log('NO_FRONTMATTER');process.exit(1)}try{y.load(m[1]);console.log('OK')}catch(e){console.log('YAML_ERROR:'+e.message);process.exit(2)}" .workflow/tickets/in-progress/{TICKET-ID}.md
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
(или эквивалент в проекте — `js-yaml.load` секции между `---`).
|
|
69
|
+
|
|
70
|
+
Если parse возвращает ошибку (`duplicated mapping key`, `bad indentation`, `:` followed by space в неэкранированном скаляре и т.п.) — **немедленный fail**:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
---RESULT---
|
|
74
|
+
status: failed
|
|
75
|
+
issues:
|
|
76
|
+
- "Frontmatter тикета невалиден: <текст YAML-ошибки>. Файл нельзя смержить в done — downstream MCP-ресурсы (workflow://human-queue, alerts) падают на парсинге. Исправить frontmatter и перезапустить."
|
|
77
|
+
---RESULT---
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Не пытайся «прочесть содержимое глазами» и одобрить — структурно повреждённый frontmatter ломает скрипты пайплайна и MCP-ресурсы.
|
|
81
|
+
|
|
82
|
+
### 0.1. Быстрый выход
|
|
61
83
|
|
|
62
84
|
Прочитай тикет. Если секция `## Ревью` существует и последняя запись — `passed` или `⏭ skipped` → немедленно верни `status: passed`.
|
|
63
85
|
|