workflow-ai 1.3.0 → 1.3.1
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 +61 -61
- package/README.md +27 -0
- package/package.json +1 -1
- package/src/lib/marker.mjs +108 -0
- package/src/lib/process-alive.mjs +11 -0
- package/src/lib/stop-command.mjs +82 -0
- package/src/runner.mjs +38 -5
- package/src/scripts/get-next-id.js +1 -1
- package/src/skills/coach/SKILL.md +1 -0
- package/src/skills/execute-task/SKILL.md +28 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,61 +1,61 @@
|
|
|
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
|
-
> фактически опубликованной версии.
|
|
14
|
-
|
|
15
|
-
### New Features
|
|
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`).
|
|
17
|
-
|
|
18
|
-
**Key capabilities:**
|
|
19
|
-
- Deterministic `step_id` generation: `{ticket_id}_{stageId}_{attempt}` (e.g., `QA-12_manual-approve_0`)
|
|
20
|
-
- Idempotent file creation — pending file is not overwritten on retry/restart
|
|
21
|
-
- Configurable polling interval (`poll_interval_ms`, default 2000ms) and optional timeout (`timeout_seconds`)
|
|
22
|
-
- Graceful handling of SIGTERM/runner stop (returns `aborted`)
|
|
23
|
-
- Immediate return if file already has `approved`/`rejected` status (crash recovery)
|
|
24
|
-
- JSON approval file format with full audit trail (`created_at`, `updated_at`, `decided_by`, `comment`, `context_snapshot`)
|
|
25
|
-
|
|
26
|
-
**Pipeline configuration example:**
|
|
27
|
-
```yaml
|
|
28
|
-
stages:
|
|
29
|
-
manual-approve-deploy:
|
|
30
|
-
type: manual-gate
|
|
31
|
-
poll_interval_ms: 2000
|
|
32
|
-
timeout_seconds: 86400
|
|
33
|
-
goto:
|
|
34
|
-
approved: continue-deploy
|
|
35
|
-
rejected: rollback
|
|
36
|
-
timeout: notify-stuck
|
|
37
|
-
aborted: end
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
**Two approval methods (both opt-in):**
|
|
41
|
-
1. **External MCP/client**: tools like `workflow-mcp` can write to approval files programmatically
|
|
42
|
-
2. **Direct file edit**: users can simply edit `.workflow/approvals/{step_id}.json` and change `status` to `approved` or `rejected`
|
|
43
|
-
|
|
44
|
-
**Important:** `manual-gate` is **opt-in** — pipelines without such stages work identically to previous versions. No breaking changes.
|
|
45
|
-
|
|
46
|
-
### Changed
|
|
47
|
-
- No breaking changes. All existing pipelines without `manual-gate` stages are fully backward compatible.
|
|
48
|
-
|
|
49
|
-
### Fixes
|
|
50
|
-
- Исправлено сохранение временной метки `created_at` в файлах одобрений (approval-pending.mjs)
|
|
51
|
-
|
|
52
|
-
### Technical Notes
|
|
53
|
-
- Approval files are stored in `<project_root>/.workflow/approvals/`
|
|
54
|
-
- Runner validates `manual-gate` stages on startup — requires `goto.approved` and `goto.rejected`, validates numeric parameters
|
|
55
|
-
- New methods added to `PipelineRunner`: `computeStepId()`, `writeApprovalPending()`, `readApprovalFile()`, `executeManualGate()`
|
|
56
|
-
|
|
57
|
-
### References
|
|
58
|
-
- PLAN-009: workflow-ai 1.2 — manual-gate stage and approval files for workflow-mcp Sprint 2 integration
|
|
59
|
-
- IMPL-55, IMPL-56, IMPL-57, IMPL-58: Implementation tickets
|
|
60
|
-
- QA-35, QA-36, QA-37: Test coverage
|
|
61
|
-
- IMPL-51, QA-55 in workflow-mcp: Dependent consumer work
|
|
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
|
+
> фактически опубликованной версии.
|
|
14
|
+
|
|
15
|
+
### New Features
|
|
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`).
|
|
17
|
+
|
|
18
|
+
**Key capabilities:**
|
|
19
|
+
- Deterministic `step_id` generation: `{ticket_id}_{stageId}_{attempt}` (e.g., `QA-12_manual-approve_0`)
|
|
20
|
+
- Idempotent file creation — pending file is not overwritten on retry/restart
|
|
21
|
+
- Configurable polling interval (`poll_interval_ms`, default 2000ms) and optional timeout (`timeout_seconds`)
|
|
22
|
+
- Graceful handling of SIGTERM/runner stop (returns `aborted`)
|
|
23
|
+
- Immediate return if file already has `approved`/`rejected` status (crash recovery)
|
|
24
|
+
- JSON approval file format with full audit trail (`created_at`, `updated_at`, `decided_by`, `comment`, `context_snapshot`)
|
|
25
|
+
|
|
26
|
+
**Pipeline configuration example:**
|
|
27
|
+
```yaml
|
|
28
|
+
stages:
|
|
29
|
+
manual-approve-deploy:
|
|
30
|
+
type: manual-gate
|
|
31
|
+
poll_interval_ms: 2000
|
|
32
|
+
timeout_seconds: 86400
|
|
33
|
+
goto:
|
|
34
|
+
approved: continue-deploy
|
|
35
|
+
rejected: rollback
|
|
36
|
+
timeout: notify-stuck
|
|
37
|
+
aborted: end
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Two approval methods (both opt-in):**
|
|
41
|
+
1. **External MCP/client**: tools like `workflow-mcp` can write to approval files programmatically
|
|
42
|
+
2. **Direct file edit**: users can simply edit `.workflow/approvals/{step_id}.json` and change `status` to `approved` or `rejected`
|
|
43
|
+
|
|
44
|
+
**Important:** `manual-gate` is **opt-in** — pipelines without such stages work identically to previous versions. No breaking changes.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
- No breaking changes. All existing pipelines without `manual-gate` stages are fully backward compatible.
|
|
48
|
+
|
|
49
|
+
### Fixes
|
|
50
|
+
- Исправлено сохранение временной метки `created_at` в файлах одобрений (approval-pending.mjs)
|
|
51
|
+
|
|
52
|
+
### Technical Notes
|
|
53
|
+
- Approval files are stored in `<project_root>/.workflow/approvals/`
|
|
54
|
+
- Runner validates `manual-gate` stages on startup — requires `goto.approved` and `goto.rejected`, validates numeric parameters
|
|
55
|
+
- New methods added to `PipelineRunner`: `computeStepId()`, `writeApprovalPending()`, `readApprovalFile()`, `executeManualGate()`
|
|
56
|
+
|
|
57
|
+
### References
|
|
58
|
+
- PLAN-009: workflow-ai 1.2 — manual-gate stage and approval files for workflow-mcp Sprint 2 integration
|
|
59
|
+
- IMPL-55, IMPL-56, IMPL-57, IMPL-58: Implementation tickets
|
|
60
|
+
- QA-35, QA-36, QA-37: Test coverage
|
|
61
|
+
- IMPL-51, QA-55 in workflow-mcp: Dependent consumer work
|
package/README.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
- Добавлено упоминание `human-gate` в разделе про runner-стадии (пример конфига в pipeline.yaml: human-review-step типа manual-gate).
|
|
3
3
|
- Добавлено упоминание `mark-blocked` как механизм нотификации autoblocked-тикетов (поля auto_blocked_reason/attempts/at).
|
|
4
4
|
|
|
5
|
+
## Синглтон семантика Pipeline
|
|
6
|
+
|
|
7
|
+
Система workflow реализует паттерн синглтона для выполнения пайплайна: одновременно разрешена только одна активная инстанция пайплайна для каждого проекта.
|
|
8
|
+
|
|
9
|
+
### Семантика Синглтона
|
|
10
|
+
При попытке запустить второй пайплайн, когда первый уже работает, система вернёт ошибку `PIPELINE_ALREADY_RUNNING`.
|
|
11
|
+
|
|
12
|
+
### Структура `.workflow/logs/.pipeline.lock`
|
|
13
|
+
Файл блокировки содержит следующие поля:
|
|
14
|
+
- `pid`: ID процесса работающего пайплайна
|
|
15
|
+
- `started_at`: Временная метка запуска пайплайна
|
|
16
|
+
- `started_by`: Инициатор запуска пайплайна (cli | mcp | extension)
|
|
17
|
+
- `run_id`: Уникальный идентификатор этого запуска
|
|
18
|
+
- `pipeline_log`: Путь к файлу логов пайплайна
|
|
19
|
+
- `project_root`: Корневая директория проекта
|
|
20
|
+
- `pipeline_version`: Версия выполняемого пайплайна
|
|
21
|
+
|
|
22
|
+
### Команды
|
|
23
|
+
- `workflow run --project <path> [--started-by cli|mcp|extension] [--force]` - Запустить пайплайн для указанного проекта
|
|
24
|
+
- `workflow stop --project <path> [--grace-sec N]` - Мягко остановить пайплайн (SIGTERM → SIGKILL после N секунд)
|
|
25
|
+
- `--force` флаг - Обход блокировки (только для отладки, когда процесс зависает)
|
|
26
|
+
|
|
27
|
+
### Восстановление
|
|
28
|
+
Если файл блокировки устарел (процесс упал, но блокировка не удалена):
|
|
29
|
+
1. Проверь, что процесс мёртв: `workflow stop --project <path>` (автоматически детектит устаревшие блокировки)
|
|
30
|
+
2. Если стандартная остановка не решает проблему: `workflow run --force --project <path>`
|
|
31
|
+
|
|
5
32
|
## Runner-стадии
|
|
6
33
|
|
|
7
34
|
В системе поддерживаются различные типы стадий для управления процессами разработки:
|
package/package.json
CHANGED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
const MARKER_FILE = '.pipeline.lock';
|
|
5
|
+
const LOGS_DIR = '.workflow/logs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Ensures the logs directory exists
|
|
9
|
+
*/
|
|
10
|
+
function ensureLogsDir(projectRoot) {
|
|
11
|
+
const logsPath = path.join(projectRoot, LOGS_DIR);
|
|
12
|
+
if (!fs.existsSync(logsPath)) {
|
|
13
|
+
fs.mkdirSync(logsPath, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Atomic write via temp file + rename.
|
|
19
|
+
* Fallback to O_EXCL (wx flag) on EXDEV/ENOTSUP.
|
|
20
|
+
* Throws Error if marker file already exists (to prevent race conditions).
|
|
21
|
+
*/
|
|
22
|
+
export function writeMarker(projectRoot, payload) {
|
|
23
|
+
ensureLogsDir(projectRoot);
|
|
24
|
+
const markerPath = path.join(projectRoot, LOGS_DIR, MARKER_FILE);
|
|
25
|
+
const content = JSON.stringify(payload, null, 2);
|
|
26
|
+
|
|
27
|
+
// First, try exclusive create (O_EXCL) - this guarantees atomicity
|
|
28
|
+
// and prevents race conditions where two processes both think they created the marker
|
|
29
|
+
try {
|
|
30
|
+
const fd = fs.openSync(markerPath, 'wx');
|
|
31
|
+
fs.writeFileSync(fd, content, 'utf-8');
|
|
32
|
+
fs.closeSync(fd);
|
|
33
|
+
return;
|
|
34
|
+
} catch (openErr) {
|
|
35
|
+
if (openErr.code === 'EEXIST') {
|
|
36
|
+
// Marker already exists - another process created it
|
|
37
|
+
throw new Error(`Marker file already exists at ${markerPath}`);
|
|
38
|
+
}
|
|
39
|
+
// If EXDEV/ENOTSUP (cross-device link), fall back to temp+rename
|
|
40
|
+
if (openErr.code !== 'EXDEV' && openErr.code !== 'ENOTSUP') {
|
|
41
|
+
throw openErr;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Fallback: temp file + rename (less atomic but works across devices)
|
|
46
|
+
const tempPath = markerPath + '.tmp.' + process.pid + '.' + Date.now();
|
|
47
|
+
try {
|
|
48
|
+
fs.writeFileSync(tempPath, content, 'utf-8');
|
|
49
|
+
fs.renameSync(tempPath, markerPath);
|
|
50
|
+
return;
|
|
51
|
+
} catch (renameErr) {
|
|
52
|
+
// Cleanup temp file if it still exists
|
|
53
|
+
try {
|
|
54
|
+
fs.unlinkSync(tempPath);
|
|
55
|
+
} catch {
|
|
56
|
+
// ignore
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// If rename failed because marker was created by another process in the meantime
|
|
60
|
+
if (renameErr.code === 'EEXIST' || !fs.existsSync(tempPath)) {
|
|
61
|
+
throw new Error(`Marker file already exists at ${markerPath}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
throw renameErr;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Reads and parses marker file. Returns null if file doesn't exist or is invalid.
|
|
70
|
+
*/
|
|
71
|
+
export function readMarker(projectRoot) {
|
|
72
|
+
const markerPath = path.join(projectRoot, LOGS_DIR, MARKER_FILE);
|
|
73
|
+
try {
|
|
74
|
+
const data = fs.readFileSync(markerPath, 'utf-8');
|
|
75
|
+
return JSON.parse(data);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
// ENOENT (file missing) or SyntaxError (invalid JSON) → return null
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Silently removes marker file. Ignores ENOENT.
|
|
84
|
+
*/
|
|
85
|
+
export function removeMarker(projectRoot) {
|
|
86
|
+
const markerPath = path.join(projectRoot, LOGS_DIR, MARKER_FILE);
|
|
87
|
+
try {
|
|
88
|
+
fs.unlinkSync(markerPath);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
// Ignore ENOENT — file already gone
|
|
91
|
+
if (err.code !== 'ENOENT') {
|
|
92
|
+
// Log warning but don't throw — silent unlink
|
|
93
|
+
console.warn(`[marker] failed to remove ${markerPath}: ${err.message}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Validates that marker exists and its pid matches expectedPid.
|
|
100
|
+
* Returns true only if both conditions hold, false otherwise.
|
|
101
|
+
*/
|
|
102
|
+
export function validateMarker(projectRoot, expectedPid) {
|
|
103
|
+
const marker = readMarker(projectRoot);
|
|
104
|
+
if (!marker) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
return marker.pid === expectedPid;
|
|
108
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function processAlive(pid) {
|
|
2
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
3
|
+
try {
|
|
4
|
+
process.kill(pid, 0); // signal 0: existence check, не убивает процесс
|
|
5
|
+
return true;
|
|
6
|
+
} catch (err) {
|
|
7
|
+
// ESRCH = no process; EPERM = process exists but no permission
|
|
8
|
+
if (err.code === 'EPERM') return true;
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { readMarker, removeMarker } from './marker.mjs';
|
|
2
|
+
import { processAlive } from './process-alive.mjs';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Sleep helper
|
|
8
|
+
*/
|
|
9
|
+
function sleep(ms) {
|
|
10
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Gracefully stops a running pipeline.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
17
|
+
* @param {object} options - Options
|
|
18
|
+
* @param {number} [options.graceSec=10] - Grace period in seconds before SIGKILL
|
|
19
|
+
* @returns {object} Result object
|
|
20
|
+
* - { ok: true, pid: number, escalated: boolean, duration_ms: number } on success
|
|
21
|
+
* - { ok: true, was_stale: true } if marker existed but process was dead
|
|
22
|
+
* - { ok: false, code: 'NOT_RUNNING' } if no marker found
|
|
23
|
+
*/
|
|
24
|
+
export async function stopPipeline(projectRoot, options = {}) {
|
|
25
|
+
const graceSec = options.graceSec ?? 10;
|
|
26
|
+
const marker = readMarker(projectRoot);
|
|
27
|
+
|
|
28
|
+
if (!marker) {
|
|
29
|
+
return { ok: false, code: 'NOT_RUNNING' };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!processAlive(marker.pid)) {
|
|
33
|
+
removeMarker(projectRoot);
|
|
34
|
+
return { ok: true, was_stale: true };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const startMs = Date.now();
|
|
38
|
+
const isWindows = process.platform === 'win32';
|
|
39
|
+
|
|
40
|
+
if (isWindows) {
|
|
41
|
+
// taskkill /T /F /PID <pid> — kills process tree immediately (forceful)
|
|
42
|
+
// No graceful period on Windows — taskkill /T does tree kill in one shot
|
|
43
|
+
try {
|
|
44
|
+
execSync(`taskkill /T /F /PID ${marker.pid}`, { stdio: 'ignore' });
|
|
45
|
+
} catch (err) {
|
|
46
|
+
// Ignore errors — process may have exited already
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
// POSIX: send SIGTERM, wait, then escalate to SIGKILL if needed
|
|
50
|
+
try {
|
|
51
|
+
process.kill(marker.pid, 'SIGTERM');
|
|
52
|
+
} catch (err) {
|
|
53
|
+
// Process may have exited between check and kill
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let escalated = false;
|
|
57
|
+
const deadline = Date.now() + graceSec * 1000;
|
|
58
|
+
|
|
59
|
+
while (processAlive(marker.pid)) {
|
|
60
|
+
if (Date.now() >= deadline) {
|
|
61
|
+
try {
|
|
62
|
+
process.kill(marker.pid, 'SIGKILL');
|
|
63
|
+
escalated = true;
|
|
64
|
+
} catch (err) {
|
|
65
|
+
// Process may have already exited
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
await sleep(200);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Ensure marker is removed
|
|
74
|
+
removeMarker(projectRoot);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
ok: true,
|
|
78
|
+
pid: marker.pid,
|
|
79
|
+
escalated: isWindows ? false : (escalated || false),
|
|
80
|
+
duration_ms: Date.now() - startMs
|
|
81
|
+
};
|
|
82
|
+
}
|
package/src/runner.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { findProjectRoot } from './lib/find-root.mjs';
|
|
|
9
9
|
import { loadRules, scanStderrForFatalRule, classify } from './lib/error-classifier.mjs';
|
|
10
10
|
import { snapshot, diff, isEmpty } from './lib/artifact-snapshot.mjs';
|
|
11
11
|
import { markUnhealthy, isHealthy } from './lib/agent-health-registry.mjs';
|
|
12
|
+
import { writeMarker, removeMarker } from './lib/marker.mjs';
|
|
12
13
|
|
|
13
14
|
// ============================================================================
|
|
14
15
|
// Logger — система логирования с уровнями DEBUG/INFO/WARN/ERROR
|
|
@@ -1564,7 +1565,7 @@ class PipelineRunner {
|
|
|
1564
1565
|
}
|
|
1565
1566
|
throw err;
|
|
1566
1567
|
}
|
|
1567
|
-
}
|
|
1568
|
+
}
|
|
1568
1569
|
|
|
1569
1570
|
/**
|
|
1570
1571
|
* Читает approval-файл и парсит его как JSON.
|
|
@@ -1587,7 +1588,7 @@ class PipelineRunner {
|
|
|
1587
1588
|
// Перебрасываем другие ошибки (например, fs-ошибки) без изменений
|
|
1588
1589
|
throw err;
|
|
1589
1590
|
}
|
|
1590
|
-
}
|
|
1591
|
+
}
|
|
1591
1592
|
|
|
1592
1593
|
/**
|
|
1593
1594
|
* Выполняет встроенный стейдж типа update-counter:
|
|
@@ -1756,7 +1757,7 @@ class PipelineRunner {
|
|
|
1756
1757
|
this.logger.warn(`[${stageId}] manual-gate: aborted (runner stopped)`, stageId);
|
|
1757
1758
|
}
|
|
1758
1759
|
return { status: 'aborted', result: { step_id: stepId } };
|
|
1759
|
-
}
|
|
1760
|
+
}
|
|
1760
1761
|
|
|
1761
1762
|
/**
|
|
1762
1763
|
* Запускает основной цикл выполнения
|
|
@@ -1802,7 +1803,7 @@ class PipelineRunner {
|
|
|
1802
1803
|
this.currentExecutor = new StageExecutor(this.config, this.context, this.counters, {}, this.fileGuard, this.logger, this.projectRoot);
|
|
1803
1804
|
result = await this.currentExecutor.execute(this.currentStage);
|
|
1804
1805
|
this.currentExecutor = null;
|
|
1805
|
-
}
|
|
1806
|
+
}
|
|
1806
1807
|
|
|
1807
1808
|
this.logger.info(`Stage ${this.currentStage} completed with status: ${result.status}`, 'PipelineRunner');
|
|
1808
1809
|
|
|
@@ -2146,18 +2147,40 @@ async function runPipeline(argv = process.argv.slice(2)) {
|
|
|
2146
2147
|
return { exitCode: 0, help: true };
|
|
2147
2148
|
}
|
|
2148
2149
|
|
|
2149
|
-
// Resolve config path
|
|
2150
|
+
// Resolve config path and projectRoot
|
|
2150
2151
|
if (!args.config) {
|
|
2151
2152
|
const projectRoot = args.project ? path.resolve(args.project) : findProjectRoot();
|
|
2152
2153
|
args.config = path.resolve(projectRoot, '.workflow/config/pipeline.yaml');
|
|
2154
|
+
args.projectRoot = projectRoot;
|
|
2153
2155
|
}
|
|
2154
2156
|
|
|
2157
|
+
const projectRoot = args.projectRoot || (args.project ? path.resolve(args.project) : findProjectRoot());
|
|
2158
|
+
|
|
2155
2159
|
console.log('=== Workflow Runner ===');
|
|
2156
2160
|
console.log(`Config: ${args.config}`);
|
|
2157
2161
|
if (args.plan) console.log(`Plan: ${args.plan}`);
|
|
2158
2162
|
if (args.project) console.log(`Project: ${args.project}`);
|
|
2159
2163
|
console.log('');
|
|
2160
2164
|
|
|
2165
|
+
// Write marker to protect against stale processes
|
|
2166
|
+
try {
|
|
2167
|
+
writeMarker(projectRoot, {
|
|
2168
|
+
pid: process.pid,
|
|
2169
|
+
timestamp: new Date().toISOString()
|
|
2170
|
+
});
|
|
2171
|
+
} catch (err) {
|
|
2172
|
+
console.error(`[runner] failed to write marker: ${err.message}`);
|
|
2173
|
+
return { exitCode: 1, error: 'Failed to acquire pipeline lock', details: err.message };
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
// Register signal handlers for cleanup
|
|
2177
|
+
const cleanup = () => {
|
|
2178
|
+
removeMarker(projectRoot);
|
|
2179
|
+
process.exit(130); // 128 + SIGINT(2) — standard exit code for signal-terminated
|
|
2180
|
+
};
|
|
2181
|
+
process.once('SIGINT', cleanup);
|
|
2182
|
+
process.once('SIGTERM', cleanup);
|
|
2183
|
+
|
|
2161
2184
|
try {
|
|
2162
2185
|
const config = loadConfig(args.config);
|
|
2163
2186
|
const errors = validateConfig(config);
|
|
@@ -2192,6 +2215,16 @@ async function runPipeline(argv = process.argv.slice(2)) {
|
|
|
2192
2215
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
2193
2216
|
|
|
2194
2217
|
return { exitCode: 1, error: err.message, stack: err.stack };
|
|
2218
|
+
} finally {
|
|
2219
|
+
// Ensure marker is cleaned up on normal exit
|
|
2220
|
+
try {
|
|
2221
|
+
removeMarker(projectRoot);
|
|
2222
|
+
} catch (err) {
|
|
2223
|
+
// ENOENT is expected, log warnings for other errors
|
|
2224
|
+
if (err.code && err.code !== 'ENOENT') {
|
|
2225
|
+
console.warn(`[runner] cleanup warning: ${err.message}`);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2195
2228
|
}
|
|
2196
2229
|
}
|
|
2197
2230
|
|
|
@@ -85,7 +85,7 @@ function readPrefixesFromConfig() {
|
|
|
85
85
|
throw new Error(`config.yaml not found: ${configPath}`);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
const text = fs.readFileSync(configPath, "utf8");
|
|
88
|
+
const text = fs.readFileSync(configPath, "utf8");
|
|
89
89
|
const lines = text.split(/\r?\n/);
|
|
90
90
|
|
|
91
91
|
const prefixes = [];
|
|
@@ -128,6 +128,7 @@ ticket_prefix: COACH
|
|
|
128
128
|
**⚠️ Антипаттерн «уход в формулировки вместо root cause»:** стейкхолдер задаёт вопрос о наблюдаемом дефекте («почему не поймали?»), а коуч анализирует текст формулировок, семантику переносов, чеклисты — вместо того чтобы ответить на прямой вопрос: какой конкретный шаг в какой конкретной стадии не выполнил конкретное физическое действие (открыть файл, посмотреть на картинку, запустить команду). Формулировки — это причина второго порядка; причина первого порядка — «агент X не сделал действие Y». Всегда начинай с причины первого порядка, потом объясняй, почему инструкции это допустили.
|
|
129
129
|
**⚠️ Антипаттерн «оценка по результату вместо сверки с инструкцией»:** при анализе действия агента — **не оценивай** его «разумность» или «допустимость» по своему суждению. Вместо этого открой скил агента и **дословно сверь** действие с инструкцией. Если инструкция говорит «разбей тикет», а агент объединил шаги — это нарушение, даже если результат выглядит «приемлемо». Коуч не имеет права смягчать finding на основании того, что дефект «небольшой» или «единичный» — скил либо нарушен, либо нет.
|
|
130
130
|
**⚠️ Антипаттерн «пересказ вместо цитаты» при утверждениях о коде:** перед утверждением вида «скрипт/функция X использует/читает/пишет Y» обязан открыть файл и **дословно процитировать** строку, на которой это поведение происходит. Пересказ по памяти (даже свежей) теряет операторы-fallback (`a || b`, `a ?? b`), условные ветви, ранние return'ы — те детали, которые как раз и задают реальное поведение. Источник ошибки: агент видит ключевое слово в строке, строит «достаточное» утверждение о поведении и идёт дальше. Правило: если утверждение про код войдёт в финдинг, CHG, черновик правки или ответ стейкхолдеру — строка должна быть в отчёте целиком (либо скопированной в цитату, либо явной ссылкой `file:line`, открытой и перечитанной непосредственно перед утверждением).
|
|
131
|
+
**⚠️ Антипаттерн «отрицательное утверждение о capability инструмента без проверки» (расширение предыдущего):** правило «дословная цитата» применяется не только к **позитивным** утверждениям («X делает Y»), но и к **отрицательным** («инструмент/фреймворк/тест X **не умеет / не покрывает / не подходит для** Y»). Отрицательное утверждение о capability — такое же утверждение о коде, как и позитивное, и требует тех же доказательств: либо (1) Grep по тестам/коду проекта, использующим инструмент, на ключевой паттерн использования (например, для гипотезы «Playwright не покрывает extension popup» — `Grep "loadExtension|launchPersistentContext|chrome-extension://"` по `tests/`), либо (2) дословная цитата из официальной документации. Источник ошибки: агент экстраполирует «общеизвестное» назначение инструмента (Playwright = веб-сайты, jest = unit-тесты и т.п.) без проверки фактической практики проекта или актуальных возможностей. Триггер срабатывания: если в черновике финдинга/ответа есть конструкция «{инструмент} не {глагол} {объект}» и от неё зависит root cause или CHG — **обязательна** проверка перед показом стейкхолдеру. Без проверки финдинг невалиден, даже если интуиция кажется правильной.
|
|
131
132
|
3. **Итеративность** — улучшай скилы инкрементально. Маленькие точечные улучшения > масштабные переписывания.
|
|
132
133
|
4. **Обратная совместимость** — улучшения не должны ломать существующие воркфлоу и интеграции.
|
|
133
134
|
5. **Актуальность знаний** — активно ищи в интернете лучшие практики, фреймворки и подходы для обогащения скилов.
|
|
@@ -70,6 +70,14 @@ description: >
|
|
|
70
70
|
|----------|----------------|
|
|
71
71
|
| `algorithms/execution-strategy.md` | **ВСЕГДА** — стратегия анализа, выполнения и верификации задачи |
|
|
72
72
|
|
|
73
|
+
## Загрузка шаблонов
|
|
74
|
+
|
|
75
|
+
Подгружай из `templates/` при необходимости:
|
|
76
|
+
|
|
77
|
+
| Шаблон | Когда загружать |
|
|
78
|
+
|--------|----------------|
|
|
79
|
+
| `templates/result-template.md` | При создании секции `## Result` в тикете — структура и правила заполнения |
|
|
80
|
+
|
|
73
81
|
## Шаги выполнения
|
|
74
82
|
|
|
75
83
|
### 1. Прочитать тикет
|
|
@@ -122,6 +130,8 @@ description: >
|
|
|
122
130
|
3. Прочитать `context.notes` — дополнительный контекст от создателя тикета
|
|
123
131
|
4. Если тикет ссылается на план (`parent_plan`) — прочитать план для понимания общей картины
|
|
124
132
|
|
|
133
|
+
**⛔ Валидация путей перед Edit:** если DoD требует изменить конкретный файл, указанный в `context.files` или описании, используй **ровно тот путь**, который указан. Перед каждым Edit сверь целевой путь с путём из контекста тикета. Одноимённый файл в другой директории — не целевой файл. Пример нарушения: тикет указывает `workflow-ai/README.md`, агент редактирует `./README.md`.
|
|
134
|
+
|
|
125
135
|
### 5. Выполнить работу и фиксировать результат инкрементально
|
|
126
136
|
|
|
127
137
|
Действовать по описанию и DoD тикета. Подход определяется **содержимым тикета**, а не типом:
|
|
@@ -131,6 +141,8 @@ description: >
|
|
|
131
141
|
- Если тикет требует тестирования — выполнить чеклист проверок из DoD, зафиксировать pass/fail по каждому пункту
|
|
132
142
|
- Если тикет требует исследования — использовать доступные инструменты для сбора данных, подкреплять источниками
|
|
133
143
|
|
|
144
|
+
**⛔ Перед вставкой кода в существующий файл — прочитай целевой участок и пойми его структуру.** Определи границы функции/класса/блока, в который вставляешь код. Вставляй код в семантически правильное место, а не в произвольную точку файла. Если не уверен в месте вставки — прочитай окружающий контекст (строки до и после) и убедись, что вставка не разрывает существующую структуру.
|
|
145
|
+
|
|
134
146
|
**⚠️ ИНКРЕМЕНТАЛЬНАЯ ЗАПИСЬ (ОБЯЗАТЕЛЬНО):**
|
|
135
147
|
|
|
136
148
|
После выполнения **каждого пункта** — **сразу** запиши результат в тикет:
|
|
@@ -156,7 +168,7 @@ description: >
|
|
|
156
168
|
|
|
157
169
|
К этому моменту секция Result уже содержит результаты по каждому пункту (записаны инкрементально на шаге 5). Осталось:
|
|
158
170
|
|
|
159
|
-
- Обновить/добавить
|
|
171
|
+
- Обновить/добавить **Что сделано** — краткое резюме всей работы
|
|
160
172
|
- Дополнить **Изменённые файлы** и **Заметки** если нужно
|
|
161
173
|
- **НЕ удалять и не переписывать** уже записанные результаты
|
|
162
174
|
|
|
@@ -191,6 +203,14 @@ description: >
|
|
|
191
203
|
|
|
192
204
|
### 9. Вывести структурированный результат
|
|
193
205
|
|
|
206
|
+
**⛔ Перед выводом RESULT пройди все три GATE из `workflows/execute.md` (шаг 6).** GATE-1 (Edit-проверка), GATE-2 (механическая Read-проверка), GATE-3 (self-check). При любом нарушении — вернись к шагам 5–7.
|
|
207
|
+
|
|
208
|
+
⛔ **GATE-1 — EDIT-ПРОВЕРКА (выполни ПЕРЕД Read-проверкой):**
|
|
209
|
+
|
|
210
|
+
За текущую сессию ты должен был вызвать инструмент **`Edit`** на файл тикета как минимум дважды: один раз для обновления DoD-чекбоксов (`[ ]` → `[x]`), один раз для записи секции Result. Если ни одного вызова `Edit` на файл тикета `.workflow/tickets/in-progress/{TICKET-ID}.md` не было — секция Result физически пустая и DoD не отмечен, независимо от написанного в stdout. Это призрачное выполнение (ограничение #9). Немедленно вернись к шагам 5–7.
|
|
211
|
+
|
|
212
|
+
**Ключевой принцип:** stdout (текст ответа) ≠ файл тикета. Результат существует только в том, что записано через инструмент `Edit`.
|
|
213
|
+
|
|
194
214
|
**⛔ ОБЯЗАТЕЛЬНАЯ МЕХАНИЧЕСКАЯ ПРОВЕРКА — перечитай файл тикета перед RESULT:**
|
|
195
215
|
|
|
196
216
|
Перед выводом `---RESULT---` выполни `Read` на файл тикета (`.workflow/tickets/in-progress/{TICKET-ID}.md`) и глазами убедись:
|
|
@@ -219,6 +239,7 @@ description: >
|
|
|
219
239
|
- Нет побочных эффектов — не созданы тикеты/планы, не перемещены файлы
|
|
220
240
|
- Поля `status` и `completed_at` не записаны в файл тикета ни в каком виде
|
|
221
241
|
- Секция `## Ревью` не создавалась и не редактировалась тобой
|
|
242
|
+
- Все временные/промежуточные файлы и пустые директории, созданные в ходе работы и не являющиеся deliverable, удалены; файлы, путь которых зависит от конфига инструмента (mock/test/fixture/snapshot/output), лежат в директории, указанной в конфиге, а не в произвольном месте
|
|
222
243
|
|
|
223
244
|
**⛔ ФОРМАТ STDOUT — СТРОГО:**
|
|
224
245
|
|
|
@@ -273,6 +294,12 @@ status: default
|
|
|
273
294
|
|
|
274
295
|
При визуальных/семантических/поведенческих критериях в Result **явно обоснуй**, почему структурной проверки недостаточно (одна строка). Полная таблица соответствий — `algorithms/execution-strategy.md` раздел «Соразмерность проверки критерию».
|
|
275
296
|
|
|
297
|
+
9. **Configured paths и cleanup** — файлы, создаваемые во время работы тикета, размещай в местах, явно указанных в конфиге соответствующего инструмента. Перед созданием файла, путь которого может зависеть от конфигурации (mock, тест, фикстура, стаб, snapshot, сгенерированный artifact, временный файл сборки и подобное) — **прочитай соответствующий конфиг проекта**, найди в нём поле target-директории (`roots`, `testDir`, `paths`, `output`, `outDir`, `srcDir` или аналог в конфиге твоего инструмента) и помести файл туда. Создавать файл в корне репозитория без проверки конфига запрещено.
|
|
298
|
+
|
|
299
|
+
**После удаления временных/ошибочных файлов — проверь и удали пустые родительские директории**, созданные в ходе тикета. Каждая созданная директория — твоя ответственность вплоть до закрытия тикета. Пустая папка или артефакт вне scope, оставленные в репозитории, — побочный эффект (см. принцип 4 «No Side Effects»).
|
|
300
|
+
|
|
301
|
+
⛔ **Антипаттерн:** создал файл в корне (mock/snapshot/test) → инструмент не подхватил из-за неправильного пути → удалил файл, но оставил пустую папку. Лечение: до создания — проверка конфига; после rm — удаление пустых директорий.
|
|
302
|
+
|
|
276
303
|
## Формат вывода
|
|
277
304
|
|
|
278
305
|
- Русский язык
|