agentic-flow-kit 0.4.0 → 0.5.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/README.md +1 -0
- package/modules/hosts/omp/agents/implementer.md +45 -6
- package/modules/hosts/omp/agents/independent-reviewer.md +42 -5
- package/modules/hosts/omp/agents/ui-verifier.md +41 -6
- package/modules/hosts/opencode/agents/implementer.md +45 -1
- package/modules/hosts/opencode/agents/independent-reviewer.md +42 -1
- package/modules/hosts/opencode/agents/ui-verifier.md +41 -1
- package/modules/hosts/opencode/commands/build.md +55 -7
- package/modules/hosts/opencode/commands/deploy.md +51 -6
- package/modules/hosts/opencode/commands/design.md +34 -6
- package/modules/hosts/opencode/commands/maintain.md +71 -6
- package/modules/hosts/opencode/commands/new-task.md +59 -6
- package/modules/hosts/opencode/commands/plan.md +32 -6
- package/modules/hosts/opencode/commands/review.md +40 -6
- package/modules/hosts/opencode/commands/test.md +56 -8
- package/modules/hosts/trae/agents/implementer.md +44 -2
- package/modules/hosts/trae/agents/independent-reviewer.md +42 -1
- package/modules/hosts/trae/agents/ui-verifier.md +39 -3
- package/modules/hosts/trae/commands/wf-build.md +55 -7
- package/modules/hosts/trae/commands/wf-deploy.md +51 -6
- package/modules/hosts/trae/commands/wf-design.md +34 -6
- package/modules/hosts/trae/commands/wf-maintain.md +71 -6
- package/modules/hosts/trae/commands/wf-new-task.md +59 -6
- package/modules/hosts/trae/commands/wf-plan.md +32 -6
- package/modules/hosts/trae/commands/wf-review.md +40 -6
- package/modules/hosts/trae/commands/wf-test.md +56 -7
- package/modules/hosts/zcode/agents/implementer.md +45 -6
- package/modules/hosts/zcode/agents/independent-reviewer.md +42 -5
- package/modules/hosts/zcode/agents/ui-verifier.md +41 -5
- package/package.json +6 -3
- package/src/cli.mjs +15 -2
- package/src/doctor.mjs +138 -11
- package/src/init.test.mjs +25 -2
- package/src/pack.test.mjs +24 -0
- package/src/run-tests.mjs +4 -11
- package/src/sync-hosts.mjs +193 -0
- package/src/sync-hosts.test.mjs +170 -0
- package/src/sync.mjs +1 -0
- package/src/sync.test.mjs +10 -0
- package/templates/AGENTS.md +1 -1
- package/templates/_agents/commands/build.md +20 -0
- package/templates/_agents/commands/design.md +1 -0
- package/templates/_agents/commands/gate-checklist.md +91 -0
- package/templates/_agents/commands/maintain.md +1 -1
- package/templates/_agents/commands/plan.md +2 -1
- package/templates/_agents/commands/review.md +1 -0
- package/templates/_agents/commands/source-sync-check.md +103 -0
- package/templates/_agents/commands/sync-hosts.md +79 -0
- package/templates/_agents/commands/test.md +2 -1
- package/templates/_agents/notes/runtime-env.md +1 -1
- package/templates/_agents/rule-budgets.txt +1 -1
- package/templates/_agents/scripts/check-loop.mjs +517 -0
- package/templates/_agents/scripts/check-loop.sh +5 -808
- package/templates/_agents/scripts/check-loop.test.mjs +481 -0
- package/templates/_agents/scripts/doctor.test.mjs +298 -0
- package/templates/_agents/scripts/ensure-board.mjs +1 -1
- package/templates/_agents/scripts/fill-intent.mjs +59 -0
- package/templates/_agents/scripts/fill-intent.test.mjs +73 -0
- package/templates/_agents/scripts/fill-plan.mjs +60 -0
- package/templates/_agents/scripts/fill-plan.test.mjs +67 -0
- package/templates/_agents/scripts/fill-spec.mjs +52 -0
- package/templates/_agents/scripts/fill-spec.test.mjs +61 -0
- package/templates/_agents/scripts/gate-checklist.mjs +192 -0
- package/templates/_agents/scripts/gate-checklist.test.mjs +204 -0
- package/templates/_agents/scripts/gen-workflow-index.mjs +4 -2
- package/templates/_agents/scripts/kb-search.mjs +3 -1
- package/templates/_agents/scripts/source-sync-check.mjs +131 -0
- package/templates/_agents/scripts/source-sync-check.test.mjs +156 -0
- package/templates/_agents/scripts/verify.mjs +8 -10
- package/templates/_agents/scripts/verify.test.mjs +13 -1
- package/templates/_agents/scripts/wf-journal.mjs +158 -0
- package/templates/_agents/scripts/wf-journal.test.mjs +150 -0
- package/templates/_agents/scripts/workflow-board-server.mjs +9 -8
- package/templates/_agents/scripts/workflow-enums.mjs +68 -0
- package/templates/_agents/scripts/workflow-enums.test.mjs +110 -0
- package/templates/_agents/scripts/workflows-check.mjs +260 -0
- package/templates/_agents/scripts/workflows-check.test.mjs +267 -0
- package/templates/_agents/workflow-enums.txt +19 -0
- package/templates/_agents/workflows/_TEMPLATE.md +74 -0
- package/templates/_agents/workflows/pipeline-closing.md +29 -0
- package/templates/_agents/workflows/source-sync-repair.md +30 -0
- package/templates/_agents/workflows/steps//347/244/272/344/276/213-/351/203/250/347/275/262/351/252/214/350/257/201.md +10 -0
- package/templates/_agents/workflows//347/244/272/344/276/213-/345/271/266/350/241/214/345/256/236/347/216/260/350/257/204/345/256/241.md +16 -0
- package/templates/workflow/README.md +1 -1
- package/templates/_agents/cache/kb-index.json +0 -1
- package/templates/_agents/scripts/check-loop.test.sh +0 -1042
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// workflow-enums.test.mjs — 枚举单源读取器测试(2026-09-25 enum-single-source)
|
|
2
|
+
// 判据:① 真实单源文件解析 + 不变量通过 ② 坏数据(坏行/重复键/重复值/缺键/子集破/重叠)fail-loud
|
|
3
|
+
// ③ 缺文件 fail-loud ④ sh 消费方同口径(sed 可取到全部 8 键)
|
|
4
|
+
// 用法:node templates/_agents/scripts/workflow-enums.test.mjs(npm test 随跑)
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { execSync } from 'node:child_process';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { parseEnums, validateEnums, loadEnums, ENUMS } from './workflow-enums.mjs';
|
|
10
|
+
|
|
11
|
+
let pass = 0;
|
|
12
|
+
let fail = 0;
|
|
13
|
+
const check = (desc, cond, detail = '') => {
|
|
14
|
+
if (cond) { pass++; console.log(`PASS ${desc}`); }
|
|
15
|
+
else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
|
|
16
|
+
};
|
|
17
|
+
const throwsWith = (fn, kw) => {
|
|
18
|
+
try { fn(); return false; } catch (e) { return String(e.message).includes(kw); }
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const ENUMS_TXT = path.join(SCRIPT_DIR, '..', 'workflow-enums.txt');
|
|
23
|
+
|
|
24
|
+
// ---- S1 模块级 ENUMS 即加载即校验(真实文件),8 键齐 ----
|
|
25
|
+
{
|
|
26
|
+
const keys = Object.keys(ENUMS).sort();
|
|
27
|
+
check('S1 真实单源文件加载成功且 8 键齐',
|
|
28
|
+
keys.length === 8 && keys.join(',') === 'doc.status.abandoned,doc.status.active,doc.status.all,doc.status.confirmed,doc.status.terminal,incident.status.active,incident.status.all,level.all',
|
|
29
|
+
JSON.stringify(keys));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ---- S2 真实文件关键值口径(check-loop/看板/检索的行为锚点,值变更须连本测试一起改)----
|
|
33
|
+
{
|
|
34
|
+
check('S2 关键集值口径:confirmed 不含 draft、active=draft+approved、terminal 含 done、abandoned ⊆ terminal',
|
|
35
|
+
!ENUMS['doc.status.confirmed'].includes('draft')
|
|
36
|
+
&& ENUMS['doc.status.active'].join('+') === 'draft+approved'
|
|
37
|
+
&& ENUMS['doc.status.terminal'].includes('done')
|
|
38
|
+
&& ENUMS['doc.status.abandoned'].every((v) => ENUMS['doc.status.terminal'].includes(v))
|
|
39
|
+
&& ENUMS['level.all'].length === 4,
|
|
40
|
+
JSON.stringify(ENUMS));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---- S3 坏行 fail-loud ----
|
|
44
|
+
{
|
|
45
|
+
check('S3 非法行抛错', throwsWith(() => parseEnums('乱行 no-equals-sign'), '行格式非法'));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---- S4 重复键 fail-loud ----
|
|
49
|
+
{
|
|
50
|
+
check('S4 重复键抛错', throwsWith(() => parseEnums('a.b=x\na.b=y'), '键重复'));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---- S5 重复值 fail-loud ----
|
|
54
|
+
{
|
|
55
|
+
check('S5 重复值抛错', throwsWith(() => parseEnums('a.b=x x'), '值重复'));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---- S6 缺键 fail-loud(validateEnums)----
|
|
59
|
+
{
|
|
60
|
+
check('S6 缺键抛错', throwsWith(() => validateEnums({ 'level.all': ['L0'] }), '缺键'));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---- S7 子集破 fail-loud ----
|
|
64
|
+
{
|
|
65
|
+
const bad = {
|
|
66
|
+
'doc.status.all': ['draft', 'approved', 'done'], 'doc.status.confirmed': ['approved', 'done', 'ghost'],
|
|
67
|
+
'doc.status.active': ['draft'], 'doc.status.terminal': ['done'], 'doc.status.abandoned': ['cancelled'],
|
|
68
|
+
'incident.status.all': ['open'], 'incident.status.active': ['open'], 'level.all': ['L0'],
|
|
69
|
+
};
|
|
70
|
+
check('S7 子集不变量破抛错', throwsWith(() => validateEnums(bad), '不变量破'));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---- S8 活跃/终态重叠 fail-loud ----
|
|
74
|
+
{
|
|
75
|
+
const bad = Object.fromEntries(Object.entries(ENUMS).map(([k]) => [k, [...ENUMS[k]]]));
|
|
76
|
+
bad['doc.status.terminal'] = [...bad['doc.status.terminal'], 'draft'];
|
|
77
|
+
check('S8 active 与 terminal 重叠抛错', throwsWith(() => validateEnums(bad), '重叠'));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---- S9 缺文件 fail-loud(loadEnums)----
|
|
81
|
+
{
|
|
82
|
+
check('S9 单源文件缺失抛错(fail-loud 无 fallback)',
|
|
83
|
+
throwsWith(() => loadEnums(path.join(SCRIPT_DIR, '..', 'no-such-enums.txt')), '不可读'));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---- S10 注释行与空行忽略 ----
|
|
87
|
+
{
|
|
88
|
+
const e = parseEnums('# 注释\n\nlevel.all=L0 L1\n');
|
|
89
|
+
check('S10 注释与空行忽略(仅 1 键)', Object.keys(e).length === 1 && e['level.all'].join('+') === 'L0+L1');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---- S11 sh 消费方同口径:check-loop.sh 的 sed 取法能取到全部 8 键 ----
|
|
93
|
+
{
|
|
94
|
+
const key = 'doc.status.confirmed';
|
|
95
|
+
const val = execSync(`sh -c "sed -n 's/^${key}=//p' '${ENUMS_TXT}' | head -n 1"`, { encoding: 'utf8' }).trim();
|
|
96
|
+
check('S11 sh sed 取键与 node 读取同值(跨语言单源口径)',
|
|
97
|
+
val === ENUMS[key].join(' '), `sh=[${val}] node=[${ENUMS[key].join(' ')}]`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---- S12 CRLF 行尾容忍(autocrlf 检出坑:rule-budgets.txt 同约定;node 侧 split(/\r?\n/) 天然免疫,显式锁定)----
|
|
101
|
+
{
|
|
102
|
+
const crlf = 'doc.status.all=draft approved done superseded cancelled\r\nlevel.all=L0 L1 L2 L3\r\n';
|
|
103
|
+
const e = parseEnums(crlf);
|
|
104
|
+
check('S12 CRLF 行尾不残留 \\r(值尾无回车)',
|
|
105
|
+
e['level.all'].join('+') === 'L0+L1+L2+L3' && !e['doc.status.all'].some((v) => v.includes('\r')),
|
|
106
|
+
JSON.stringify(e));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
110
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// flow-kit workflows-check:编排脚本机器 lint(2026-09-25 workflows-linter,审查改进方向 1)
|
|
2
|
+
// 目的:`.agents/workflows/_TEMPLATE.md`「解析校验先行」此前是纯 prose——role ∈ .agents/roles/、
|
|
3
|
+
// step ∈ steps/、after 引用存在且无环,全靠宿主 AI 自觉;而编排脚本的书写者与执行者是同一
|
|
4
|
+
// 宿主 AI,缺外部裁决者。本工具把该纪律变成机器门:解析 .agents/workflows/*.md 的 stages 表
|
|
5
|
+
// 逐行校验,error 级 exit 1(可挂 doctor / CI),告警级 advisory 不阻断。
|
|
6
|
+
//
|
|
7
|
+
// 检查项(error → exit 1):
|
|
8
|
+
// E1 缺 frontmatter(name/description/concurrency 口径见 _TEMPLATE.md)
|
|
9
|
+
// E2 concurrency 非正整数(缺省 = 2)
|
|
10
|
+
// E3 缺 stages 表(表头须含 id/after/gate 列)
|
|
11
|
+
// E4 stage 缺 id 或 id 重复
|
|
12
|
+
// E5 after 引用不存在的 stage
|
|
13
|
+
// E6 after 依赖成环
|
|
14
|
+
// E7 role 不在 .agents/roles/(词表 = 目录内 *.md 文件名)
|
|
15
|
+
// E8 step 不在 .agents/workflows/steps/(文件名即步骤名——目录即注册表)
|
|
16
|
+
// E9 四形态(role/step/gate/human)不互斥:全空或同时 ≥2 个非空(`—`/`-`/空白 均视为空;human 列可选,缺列恒空)
|
|
17
|
+
// E10 retries 非非负整数(空 = 0,口径同 _TEMPLATE「失败且 retries > 0 → 重派」)
|
|
18
|
+
// 告警(advisory,不阻断):
|
|
19
|
+
// W1 gate 含 {a,b} 花括号展开——仅 bash 展开,PowerShell/cmd 按字面路径失败(跨宿主可移植性)
|
|
20
|
+
// W2 两个 role stage 的授权文件 token 完全相同且互相无 after 先序——并行 fan-out 同文件交叉写
|
|
21
|
+
// W3 frontmatter 缺 name(编排脚本标识)
|
|
22
|
+
// W4 human 确认门行填了 retries——用户驳回不能重派,无重试语义(视作 0)
|
|
23
|
+
//
|
|
24
|
+
// 排除:`_` 前缀文件(_TEMPLATE.md 是机制文档,正文含示例表——示例 step 名「部署验证」不在 steps/,
|
|
25
|
+
// 机制文档不按可执行件校验);steps/ 子目录(是扩展点注册表,非编排脚本)。
|
|
26
|
+
// 表解析:取首个表头含 id+after+gate 的表格,列名归一(task / params → task、files(授权)→ files),
|
|
27
|
+
// 到首个非 `|` 行截止;仅此一张表——编排脚本按机制文档只允许一张 stages 表。
|
|
28
|
+
// 用法:node .agents/scripts/workflows-check.mjs [--root <目标根>] [--json]
|
|
29
|
+
// 默认根 = process.cwd()(doctor 以 cwd=target 调用;仓库根直跑即自扫)
|
|
30
|
+
// 零依赖;只报告不修复(B-b 决策口径,同 source-sync-check)
|
|
31
|
+
// 测试:node templates/_agents/scripts/workflows-check.test.mjs(fixture + 真实仓库 baseline)
|
|
32
|
+
import fs from 'node:fs';
|
|
33
|
+
import path from 'node:path';
|
|
34
|
+
|
|
35
|
+
function fail(msg) { console.error('❌ ' + msg); process.exit(1); }
|
|
36
|
+
|
|
37
|
+
// 空值规范:em/en dash、裸连字符、空白均视为「未填」(stages 表通用约定)
|
|
38
|
+
const EMPTY_CELLS = new Set(['—', '–', '-', '']);
|
|
39
|
+
const normCell = (v) => { const s = String(v ?? '').trim(); return EMPTY_CELLS.has(s) ? '' : s; };
|
|
40
|
+
|
|
41
|
+
function listMdNames(dir) {
|
|
42
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return [];
|
|
43
|
+
return fs.readdirSync(dir).filter((f) => f.endsWith('.md')).map((f) => f.slice(0, -3)).sort();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseFm(text) {
|
|
47
|
+
const m = String(text).match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
48
|
+
if (!m) return null;
|
|
49
|
+
const fm = {};
|
|
50
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
51
|
+
const kv = line.match(/^(\S+):\s*(.*)$/);
|
|
52
|
+
if (kv) fm[kv[1].trim()] = kv[2].trim();
|
|
53
|
+
}
|
|
54
|
+
return fm;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// splitRow:`| a | b |` → ['a','b'](去首尾竖线后按 | 切)
|
|
58
|
+
function splitRow(line) {
|
|
59
|
+
return line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// cellName:表头格 → 列名(小写取首个英文词:task / params → task、files(授权) → files)
|
|
63
|
+
function cellName(c) {
|
|
64
|
+
const m = c.toLowerCase().match(/^[a-z]+/);
|
|
65
|
+
return m ? m[0] : '';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// parseStages:返回 stage 行数组(id/after/role/step/files/gate/human/retries + _line 行号),无表返回 null。
|
|
69
|
+
// human 列可选(表头缺列 = 恒空,既有 9 列脚本零改动兼容);导出供 wf-journal.mjs 复用同一解析口径。
|
|
70
|
+
export function parseStages(text) {
|
|
71
|
+
const lines = String(text).split(/\r?\n/);
|
|
72
|
+
let headerIdx = -1;
|
|
73
|
+
let colIdx = null;
|
|
74
|
+
for (let i = 0; i < lines.length; i++) {
|
|
75
|
+
if (!/^\s*\|/.test(lines[i])) continue;
|
|
76
|
+
const names = splitRow(lines[i]).map(cellName);
|
|
77
|
+
if (names.includes('id') && names.includes('after') && names.includes('gate')) {
|
|
78
|
+
headerIdx = i;
|
|
79
|
+
colIdx = {};
|
|
80
|
+
names.forEach((n, k) => { if (n && !(n in colIdx)) colIdx[n] = k; });
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (headerIdx === -1) return null;
|
|
85
|
+
const COLS = ['id', 'after', 'role', 'step', 'files', 'gate', 'human', 'retries'];
|
|
86
|
+
const rows = [];
|
|
87
|
+
for (let i = headerIdx + 1; i < lines.length; i++) {
|
|
88
|
+
if (!/^\s*\|/.test(lines[i])) break;
|
|
89
|
+
const cells = splitRow(lines[i]);
|
|
90
|
+
if (cells.every((c) => /^[-: ]*$/.test(c))) continue; // 分隔行 |----|
|
|
91
|
+
if (cells.every((c) => normCell(c) === '')) continue; // 全空行
|
|
92
|
+
const row = { _line: i + 1 };
|
|
93
|
+
for (const c of COLS) row[c] = colIdx[c] !== undefined ? normCell(cells[colIdx[c]] ?? '') : '';
|
|
94
|
+
rows.push(row);
|
|
95
|
+
}
|
|
96
|
+
return rows;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// lintWorkflowText:单文件校验(pure,词表以 Set 传入)
|
|
100
|
+
export function lintWorkflowText({ file, text, roles, steps }) {
|
|
101
|
+
const errors = [];
|
|
102
|
+
const warnings = [];
|
|
103
|
+
const fm = parseFm(text);
|
|
104
|
+
if (!fm) {
|
|
105
|
+
errors.push('E1 缺 frontmatter(--- name / description / concurrency ---,口径见 _TEMPLATE.md)');
|
|
106
|
+
} else {
|
|
107
|
+
if (fm.concurrency !== undefined && fm.concurrency !== '' && !/^[1-9]\d*$/.test(fm.concurrency)) {
|
|
108
|
+
errors.push(`E2 concurrency 须为正整数(现「${fm.concurrency}」,缺省 = 2)`);
|
|
109
|
+
}
|
|
110
|
+
if (!fm.name) warnings.push('W3 frontmatter 缺 name(编排脚本标识,_TEMPLATE.md 口径)');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const rows = parseStages(text);
|
|
114
|
+
if (!rows) {
|
|
115
|
+
errors.push('E3 缺 stages 表(表头须含 id / after / gate 列——三形态各至少一行的可执行编排才有效)');
|
|
116
|
+
return { file, errors, warnings };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const byId = new Map();
|
|
120
|
+
for (const r of rows) {
|
|
121
|
+
if (!r.id) { errors.push(`E4 L${r._line} stage 缺 id`); continue; }
|
|
122
|
+
if (byId.has(r.id)) errors.push(`E4 id 重复:${r.id}(L${byId.get(r.id)._line} 与 L${r._line})`);
|
|
123
|
+
else byId.set(r.id, r);
|
|
124
|
+
const forms = [r.role && 'role', r.step && 'step', r.gate && 'gate', r.human && 'human'].filter(Boolean);
|
|
125
|
+
if (forms.length === 0) errors.push(`E9 L${r._line} ${r.id} 四形态(role/step/gate/human)全空——须恰填一项`);
|
|
126
|
+
if (forms.length > 1) errors.push(`E9 L${r._line} ${r.id} 四形态互斥违例:同时填了 ${forms.join(' + ')}`);
|
|
127
|
+
if (r.role && !roles.has(r.role)) errors.push(`E7 L${r._line} ${r.id} role「${r.role}」不在 .agents/roles/(现有:${[...roles].join('、') || '无'})`);
|
|
128
|
+
if (r.step && !steps.has(r.step)) errors.push(`E8 L${r._line} ${r.id} step「${r.step}」不在 steps/(现有:${[...steps].join('、') || '无'})`);
|
|
129
|
+
if (r.retries && !/^\d+$/.test(r.retries)) errors.push(`E10 L${r._line} ${r.id} retries 须为非负整数(现「${r.retries}」,空 = 0)`);
|
|
130
|
+
if (r.gate && /\{[^}]+,[^}]+\}/.test(r.gate)) warnings.push(`W1 L${r._line} ${r.id} gate 含 {a,b} 花括号展开——仅 bash 展开,PowerShell/cmd 按字面路径失败:${r.gate}`);
|
|
131
|
+
if (r.human && r.retries) warnings.push(`W4 L${r._line} ${r.id} human 确认门行填了 retries「${r.retries}」——用户驳回不能重派,无重试语义(视作 0)`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// after 引用存在性 + 成环(DFS 三色)
|
|
135
|
+
const ids = new Set(byId.keys());
|
|
136
|
+
const deps = new Map();
|
|
137
|
+
for (const [id, r] of byId) {
|
|
138
|
+
deps.set(id, r.after ? r.after.split(/[,,、\s]+/).filter(Boolean) : []);
|
|
139
|
+
}
|
|
140
|
+
for (const [id, ds] of deps) {
|
|
141
|
+
for (const d of ds) if (!ids.has(d)) errors.push(`E5 ${id} after 引用不存在的 stage:${d}`);
|
|
142
|
+
}
|
|
143
|
+
const color = new Map();
|
|
144
|
+
let cycle = '';
|
|
145
|
+
const dfs = (id) => {
|
|
146
|
+
color.set(id, 1);
|
|
147
|
+
for (const d of deps.get(id) || []) {
|
|
148
|
+
if (!ids.has(d)) continue;
|
|
149
|
+
const c = color.get(d) || 0;
|
|
150
|
+
if (c === 1) { cycle = `${d} → ${id}`; return true; }
|
|
151
|
+
if (c === 0 && dfs(d)) return true;
|
|
152
|
+
}
|
|
153
|
+
color.set(id, 2);
|
|
154
|
+
return false;
|
|
155
|
+
};
|
|
156
|
+
for (const id of ids) {
|
|
157
|
+
if (!color.get(id) && dfs(id)) { errors.push(`E6 after 依赖成环:${cycle}(闭环上一对)`); break; }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// W2 授权文件交叉:role stage 间完全相同 token 且互相无 after 先序(有先序 = 串行接力,不算交叉)
|
|
161
|
+
if (!errors.some((e) => e.startsWith('E5') || e.startsWith('E6'))) {
|
|
162
|
+
const depAll = new Map();
|
|
163
|
+
const depClosure = (id) => {
|
|
164
|
+
if (depAll.has(id)) return depAll.get(id);
|
|
165
|
+
const s = new Set();
|
|
166
|
+
depAll.set(id, s); // 先占位防递归环(E6 已排除,稳妥起见)
|
|
167
|
+
for (const d of deps.get(id) || []) {
|
|
168
|
+
if (!ids.has(d)) continue;
|
|
169
|
+
s.add(d);
|
|
170
|
+
for (const x of depClosure(d)) s.add(x);
|
|
171
|
+
}
|
|
172
|
+
return s;
|
|
173
|
+
};
|
|
174
|
+
const tokensOf = (r) => new Set((r.files || '').split(/[,,、\s]+/).map((t) => t.trim()).filter(Boolean));
|
|
175
|
+
const roleRows = [...byId.values()].filter((r) => r.role && r.files);
|
|
176
|
+
for (let i = 0; i < roleRows.length; i++) {
|
|
177
|
+
for (let j = i + 1; j < roleRows.length; j++) {
|
|
178
|
+
const a = roleRows[i];
|
|
179
|
+
const b = roleRows[j];
|
|
180
|
+
const shared = [...tokensOf(a)].filter((t) => tokensOf(b).has(t));
|
|
181
|
+
if (!shared.length) continue;
|
|
182
|
+
if (depClosure(a.id).has(b.id) || depClosure(b.id).has(a.id)) continue;
|
|
183
|
+
warnings.push(`W2 role stage ${a.id} 与 ${b.id} 授权文件交叉且无 after 先序(并行 fan-out 同文件交叉写):${shared.join('、')}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return { file, errors, warnings };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// lintWorkflows:扫 <root>/.agents/workflows/(pure function,测试/doctor 共用)
|
|
192
|
+
// 返回 { root, roles, steps, files: [{file, errors, warnings}], errorCount, warningCount }
|
|
193
|
+
export function lintWorkflows({ root }) {
|
|
194
|
+
const wfRoot = path.join(root, '.agents', 'workflows');
|
|
195
|
+
const roles = new Set(listMdNames(path.join(root, '.agents', 'roles')));
|
|
196
|
+
const steps = new Set(listMdNames(path.join(wfRoot, 'steps')));
|
|
197
|
+
const files = (fs.existsSync(wfRoot) && fs.statSync(wfRoot).isDirectory())
|
|
198
|
+
? fs.readdirSync(wfRoot, { withFileTypes: true })
|
|
199
|
+
.filter((e) => e.isFile() && e.name.endsWith('.md') && !e.name.startsWith('_'))
|
|
200
|
+
.map((e) => e.name)
|
|
201
|
+
.sort()
|
|
202
|
+
: [];
|
|
203
|
+
const results = files.map((f) => lintWorkflowText({
|
|
204
|
+
file: f,
|
|
205
|
+
text: fs.readFileSync(path.join(wfRoot, f), 'utf8'),
|
|
206
|
+
roles,
|
|
207
|
+
steps,
|
|
208
|
+
}));
|
|
209
|
+
return {
|
|
210
|
+
root,
|
|
211
|
+
roles: [...roles],
|
|
212
|
+
steps: [...steps],
|
|
213
|
+
files: results,
|
|
214
|
+
errorCount: results.reduce((n, r) => n + r.errors.length, 0),
|
|
215
|
+
warningCount: results.reduce((n, r) => n + r.warnings.length, 0),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function printResult(result) {
|
|
220
|
+
console.log('▶ flow-kit workflows-check(.agents/workflows/ 编排脚本 lint)');
|
|
221
|
+
console.log(` 编排脚本 ${result.files.length} 份 | roles 词表 ${result.roles.length} | steps 词表 ${result.steps.length}`);
|
|
222
|
+
for (const f of result.files) {
|
|
223
|
+
if (!f.errors.length && !f.warnings.length) {
|
|
224
|
+
console.log(` ✅ ${f.file}`);
|
|
225
|
+
} else {
|
|
226
|
+
console.log(` ${f.errors.length ? '❌' : '⚠️ '} ${f.file}(${f.errors.length} error / ${f.warnings.length} warning)`);
|
|
227
|
+
for (const e of f.errors) console.log(` - ${e}`);
|
|
228
|
+
for (const w of f.warnings) console.log(` - ⚠️ ${w}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const tail = result.errorCount
|
|
232
|
+
? `❌ ${result.errorCount} error / ${result.warningCount} warning——error 须修复后才可执行编排(解析校验先行)`
|
|
233
|
+
: `${result.warningCount ? `⚠️ 0 error / ${result.warningCount} warning(advisory 不阻断)` : '✅ 0 error / 0 warning'}`;
|
|
234
|
+
console.log(` ${tail}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const isMain = process.argv[1] && process.argv[1].endsWith('workflows-check.mjs');
|
|
238
|
+
if (isMain) {
|
|
239
|
+
let root = process.cwd();
|
|
240
|
+
let json = false;
|
|
241
|
+
for (let i = 2; i < process.argv.length; i++) {
|
|
242
|
+
const a = process.argv[i];
|
|
243
|
+
if (a === '--root') root = path.resolve(process.argv[++i] ?? fail('--root 缺少值'));
|
|
244
|
+
else if (a === '--json') json = true;
|
|
245
|
+
else fail(`未知选项:${a}(flow-kit workflows-check --root <dir> / --json)`);
|
|
246
|
+
}
|
|
247
|
+
const result = lintWorkflows({ root });
|
|
248
|
+
if (json) {
|
|
249
|
+
process.stdout.write(JSON.stringify({
|
|
250
|
+
root: result.root,
|
|
251
|
+
files: result.files,
|
|
252
|
+
errorCount: result.errorCount,
|
|
253
|
+
warningCount: result.warningCount,
|
|
254
|
+
}, null, 2) + '\n');
|
|
255
|
+
} else {
|
|
256
|
+
printResult(result);
|
|
257
|
+
}
|
|
258
|
+
process.exit(result.errorCount ? 1 : 0);
|
|
259
|
+
}
|
|
260
|
+
export default lintWorkflows;
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// workflows-check.test.mjs — 编排脚本 lint 测试(2026-09-25 workflows-linter)
|
|
3
|
+
// 方法:fixture 临时目录(.agents/workflows/*.md + roles/ + steps/ 词表)调 lintWorkflows() 拿结构化结果,
|
|
4
|
+
// 不依赖真实仓库文件;末两个场景对真实仓库自扫做 baseline 断言。
|
|
5
|
+
// 判据:error 10 类各至少一场景命中;advisory 3 类各一场景;_ 前缀排除;真实仓库 0 error。
|
|
6
|
+
// 用法:node templates/_agents/scripts/workflows-check.test.mjs(npm test 随跑)
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { lintWorkflows } from './workflows-check.mjs';
|
|
12
|
+
|
|
13
|
+
let pass = 0;
|
|
14
|
+
let fail = 0;
|
|
15
|
+
const check = (desc, cond, detail = '') => {
|
|
16
|
+
if (cond) { pass++; console.log(`PASS ${desc}`); }
|
|
17
|
+
else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const HEAD = '| id | after | role | step | task / params | files(授权) | accept(验收判据) | gate | retries |';
|
|
21
|
+
const SEP = '|----|-------|------|------|---------------|--------------|-------------------|------|---------|';
|
|
22
|
+
|
|
23
|
+
// mkfix:建临时根 + 词表(roles: implementer/reviewer;steps: deploy)+ 指定编排脚本集合
|
|
24
|
+
const mkfix = (scripts) => {
|
|
25
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'wf-lint-test-'));
|
|
26
|
+
fs.mkdirSync(path.join(root, '.agents', 'workflows', 'steps'), { recursive: true });
|
|
27
|
+
fs.mkdirSync(path.join(root, '.agents', 'roles'), { recursive: true });
|
|
28
|
+
for (const r of ['implementer', 'independent-reviewer']) {
|
|
29
|
+
fs.writeFileSync(path.join(root, '.agents', 'roles', `${r}.md`), `# ${r}\n`);
|
|
30
|
+
}
|
|
31
|
+
fs.writeFileSync(path.join(root, '.agents', 'workflows', 'steps', 'deploy.md'), '# deploy\n');
|
|
32
|
+
for (const [name, body] of Object.entries(scripts)) {
|
|
33
|
+
fs.writeFileSync(path.join(root, '.agents', 'workflows', name), body);
|
|
34
|
+
}
|
|
35
|
+
return root;
|
|
36
|
+
};
|
|
37
|
+
const fm = (extra = '') => `---\nname: t\ndescription: t\n${extra}---\n\n# w\n\n`;
|
|
38
|
+
|
|
39
|
+
// ---- S1 全绿:双 role 并行 + 汇聚 gate → 0 error 0 warning ----
|
|
40
|
+
{
|
|
41
|
+
const root = mkfix({
|
|
42
|
+
'ok.md': fm() + HEAD + '\n' + SEP + '\n' +
|
|
43
|
+
'| a | — | implementer | — | 做 A | src/a/** | 测试过 | | 1 |\n' +
|
|
44
|
+
'| b | — | implementer | — | 做 B | src/b/** | 构建过 | | 0 |\n' +
|
|
45
|
+
'| g | a,b | — | — | — | — | — | npm test | |\n',
|
|
46
|
+
});
|
|
47
|
+
const r = lintWorkflows({ root });
|
|
48
|
+
check('S1 全绿(并行 role + 汇聚 gate)→ 0 error 0 warning',
|
|
49
|
+
r.errorCount === 0 && r.warningCount === 0 && r.files.length === 1,
|
|
50
|
+
JSON.stringify(r.files[0]));
|
|
51
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---- S2 after 悬空引用 → E5 ----
|
|
55
|
+
{
|
|
56
|
+
const root = mkfix({
|
|
57
|
+
'dangling.md': fm() + HEAD + '\n' + SEP + '\n' +
|
|
58
|
+
'| a | ghost | — | — | — | — | — | npm test | |\n',
|
|
59
|
+
});
|
|
60
|
+
const r = lintWorkflows({ root });
|
|
61
|
+
check('S2 after 引用不存在 → E5',
|
|
62
|
+
r.errorCount === 1 && r.files[0].errors[0].startsWith('E5') && r.files[0].errors[0].includes('ghost'),
|
|
63
|
+
JSON.stringify(r.files[0].errors));
|
|
64
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---- S3 after 成环 → E6 ----
|
|
68
|
+
{
|
|
69
|
+
const root = mkfix({
|
|
70
|
+
'cycle.md': fm() + HEAD + '\n' + SEP + '\n' +
|
|
71
|
+
'| a | b | — | — | — | — | — | npm test | |\n' +
|
|
72
|
+
'| b | a | — | — | — | — | — | npm test | |\n',
|
|
73
|
+
});
|
|
74
|
+
const r = lintWorkflows({ root });
|
|
75
|
+
check('S3 after 依赖成环 → E6',
|
|
76
|
+
r.errorCount === 1 && r.files[0].errors[0].startsWith('E6'),
|
|
77
|
+
JSON.stringify(r.files[0].errors));
|
|
78
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---- S4 未知 role → E7(词表只有 implementer / independent-reviewer)----
|
|
82
|
+
{
|
|
83
|
+
const root = mkfix({
|
|
84
|
+
'role.md': fm() + HEAD + '\n' + SEP + '\n' +
|
|
85
|
+
'| a | — | hacker | — | 做 A | src/** | 过 | | |\n',
|
|
86
|
+
});
|
|
87
|
+
const r = lintWorkflows({ root });
|
|
88
|
+
check('S4 role 不在 .agents/roles/ → E7',
|
|
89
|
+
r.errorCount === 1 && r.files[0].errors[0].startsWith('E7') && r.files[0].errors[0].includes('hacker'),
|
|
90
|
+
JSON.stringify(r.files[0].errors));
|
|
91
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---- S5 未知 step → E8(steps/ 只有 deploy)----
|
|
95
|
+
{
|
|
96
|
+
const root = mkfix({
|
|
97
|
+
'step.md': fm() + HEAD + '\n' + SEP + '\n' +
|
|
98
|
+
'| a | — | — | rollback | env=prod | | exit 0 | | 0 |\n',
|
|
99
|
+
});
|
|
100
|
+
const r = lintWorkflows({ root });
|
|
101
|
+
check('S5 step 不在 steps/ → E8',
|
|
102
|
+
r.errorCount === 1 && r.files[0].errors[0].startsWith('E8') && r.files[0].errors[0].includes('rollback'),
|
|
103
|
+
JSON.stringify(r.files[0].errors));
|
|
104
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---- S6 三形态全空 / 双填 → E9 ×2 ----
|
|
108
|
+
{
|
|
109
|
+
const root = mkfix({
|
|
110
|
+
'forms.md': fm() + HEAD + '\n' + SEP + '\n' +
|
|
111
|
+
'| nothing | — | — | — | 只有任务没有形态 | src/** | 过 | | |\n' +
|
|
112
|
+
'| both | — | implementer | deploy | 形态双填 | src/** | 过 | npm test | |\n',
|
|
113
|
+
});
|
|
114
|
+
const r = lintWorkflows({ root });
|
|
115
|
+
const e9 = r.files[0].errors.filter((e) => e.startsWith('E9'));
|
|
116
|
+
check('S6 三形态全空与双填 → E9 ×2',
|
|
117
|
+
r.errorCount === 2 && e9.length === 2 && e9[0].includes('nothing') && e9[1].includes('both'),
|
|
118
|
+
JSON.stringify(r.files[0].errors));
|
|
119
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---- S7 retries 非法 / concurrency 非法 → E10 + E2 ----
|
|
123
|
+
{
|
|
124
|
+
const root = mkfix({
|
|
125
|
+
'nums.md': fm('concurrency: x\n') + HEAD + '\n' + SEP + '\n' +
|
|
126
|
+
'| a | — | — | — | — | — | — | npm test | 重试 |\n',
|
|
127
|
+
});
|
|
128
|
+
const r = lintWorkflows({ root });
|
|
129
|
+
const codes = r.files[0].errors.map((e) => (e.match(/^E\d+/) || [''])[0]);
|
|
130
|
+
check('S7 retries 非数字 → E10 且 concurrency 非法 → E2',
|
|
131
|
+
r.errorCount === 2 && codes.includes('E10') && codes.includes('E2'),
|
|
132
|
+
JSON.stringify(r.files[0].errors));
|
|
133
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---- S8 gate 花括号展开 → W1(0 error,advisory)----
|
|
137
|
+
{
|
|
138
|
+
const root = mkfix({
|
|
139
|
+
'brace.md': fm() + HEAD + '\n' + SEP + '\n' +
|
|
140
|
+
'| g | — | — | — | — | — | — | node scripts/fill-{intent,plan}.test.mjs | |\n',
|
|
141
|
+
});
|
|
142
|
+
const r = lintWorkflows({ root });
|
|
143
|
+
check('S8 gate 含 {a,b} 花括号展开 → W1 且不计 error',
|
|
144
|
+
r.errorCount === 0 && r.warningCount === 1 && r.files[0].warnings[0].startsWith('W1'),
|
|
145
|
+
JSON.stringify({ e: r.files[0].errors, w: r.files[0].warnings }));
|
|
146
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---- S9 授权文件交叉:无先序告警 / 有 after 先序不告警 ----
|
|
150
|
+
{
|
|
151
|
+
const root = mkfix({
|
|
152
|
+
'parallel-cross.md': fm() + HEAD + '\n' + SEP + '\n' +
|
|
153
|
+
'| a | — | implementer | — | 做 A | src/shared/** | 过 | | |\n' +
|
|
154
|
+
'| b | — | implementer | — | 做 B | src/shared/** | 过 | | |\n',
|
|
155
|
+
'ordered-ok.md': fm() + HEAD + '\n' + SEP + '\n' +
|
|
156
|
+
'| a | — | implementer | — | 做 A | src/shared/** | 过 | | |\n' +
|
|
157
|
+
'| b | a | implementer | — | 做 B | src/shared/** | 过 | | |\n',
|
|
158
|
+
});
|
|
159
|
+
const r = lintWorkflows({ root });
|
|
160
|
+
const cross = r.files.find((f) => f.file === 'parallel-cross.md');
|
|
161
|
+
const ordered = r.files.find((f) => f.file === 'ordered-ok.md');
|
|
162
|
+
check('S9 授权文件相同且无先序 → W2;有 after 先序 → 不告警',
|
|
163
|
+
r.errorCount === 0 && cross.warnings.length === 1 && cross.warnings[0].startsWith('W2') && ordered.warnings.length === 0,
|
|
164
|
+
JSON.stringify({ cross: cross.warnings, ordered: ordered.warnings }));
|
|
165
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---- S10 _ 前缀机制文档不进扫描面(内含坏表也不报)----
|
|
169
|
+
{
|
|
170
|
+
const root = mkfix({
|
|
171
|
+
'_TEMPLATE.md': fm() + HEAD + '\n' + SEP + '\n' +
|
|
172
|
+
'| bad | ghost | hacker | — | — | — | — | — | 重试 |\n',
|
|
173
|
+
});
|
|
174
|
+
const r = lintWorkflows({ root });
|
|
175
|
+
check('S10 _TEMPLATE.md 排除(机制文档含示例表,不按可执行件校验)',
|
|
176
|
+
r.files.length === 0 && r.errorCount === 0 && r.warningCount === 0,
|
|
177
|
+
JSON.stringify(r.files.map((f) => f.file)));
|
|
178
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---- S11 缺 frontmatter → E1;缺 stages 表 → E3 ----
|
|
182
|
+
{
|
|
183
|
+
const root = mkfix({
|
|
184
|
+
'nofm.md': '# 没有 frontmatter\n\n只有正文\n',
|
|
185
|
+
'notable.md': fm() + '# 有 frontmatter 没有 stages 表\n',
|
|
186
|
+
});
|
|
187
|
+
const r = lintWorkflows({ root });
|
|
188
|
+
const nofm = r.files.find((f) => f.file === 'nofm.md');
|
|
189
|
+
const notable = r.files.find((f) => f.file === 'notable.md');
|
|
190
|
+
check('S11 缺 frontmatter → E1 且缺表 → E3',
|
|
191
|
+
nofm.errors.some((e) => e.startsWith('E1')) && notable.errors.some((e) => e.startsWith('E3')),
|
|
192
|
+
JSON.stringify({ nofm: nofm.errors, notable: notable.errors }));
|
|
193
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ---- S12 id 重复 → E4;id 空 → E4 ----
|
|
197
|
+
{
|
|
198
|
+
const root = mkfix({
|
|
199
|
+
'dup.md': fm() + HEAD + '\n' + SEP + '\n' +
|
|
200
|
+
'| a | — | — | — | — | — | — | npm test | |\n' +
|
|
201
|
+
'| a | — | — | — | — | — | — | npm test | |\n' +
|
|
202
|
+
'| — | — | — | — | 缺 id 行 | — | — | — | |\n',
|
|
203
|
+
});
|
|
204
|
+
const r = lintWorkflows({ root });
|
|
205
|
+
const e4 = r.files[0].errors.filter((e) => e.startsWith('E4'));
|
|
206
|
+
check('S12 id 重复与缺 id → E4 ×2',
|
|
207
|
+
e4.length === 2 && e4.some((e) => e.includes('重复')) && e4.some((e) => e.includes('缺 id')),
|
|
208
|
+
JSON.stringify(r.files[0].errors));
|
|
209
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---- S13 step 引用 + gate + 空值规范(— / 空 retries / 空 gate)混排全绿 ----
|
|
213
|
+
{
|
|
214
|
+
const root = mkfix({
|
|
215
|
+
'mixed.md': fm('concurrency: 3\n') + HEAD + '\n' + SEP + '\n' +
|
|
216
|
+
'| build | — | implementer | — | 构建 | src/** | 测试过 | | |\n' +
|
|
217
|
+
'| verify | build | — | deploy | env=staging | | exit 0 | | 2 |\n' +
|
|
218
|
+
'| final | verify | — | — | — | — | — | npm test | |\n',
|
|
219
|
+
});
|
|
220
|
+
const r = lintWorkflows({ root });
|
|
221
|
+
check('S13 step 行 / gate 行 / em-dash 空值 / concurrency=3 / retries=2 混排全绿',
|
|
222
|
+
r.errorCount === 0 && r.warningCount === 0,
|
|
223
|
+
JSON.stringify(r.files[0]));
|
|
224
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ---- S14 真实仓库自扫 baseline:0 error(_TEMPLATE 排除后三份可执行件全过)----
|
|
228
|
+
{
|
|
229
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
|
|
230
|
+
const r = lintWorkflows({ root: ROOT });
|
|
231
|
+
const names = r.files.map((f) => f.file).join('、');
|
|
232
|
+
check('S14 真实仓库 .agents/workflows/ 自扫 0 error(扫描面:' + names + ')',
|
|
233
|
+
r.errorCount === 0 && r.files.length >= 3,
|
|
234
|
+
JSON.stringify(r.files.filter((f) => f.errors.length).map((f) => ({ f: f.file, e: f.errors }))));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ---- S14 human 确认门(第四形态):单形态全绿 / 双填 E9 / ×retries W4 / 缺列兼容 ----
|
|
238
|
+
{
|
|
239
|
+
const HEAD10 = '| id | after | role | step | task / params | files(授权) | accept(验收判据) | gate | human | retries |';
|
|
240
|
+
const SEP10 = '|----|-------|------|------|---------------|--------------|-------------------|------|-------|---------|';
|
|
241
|
+
const root = mkfix({
|
|
242
|
+
'human-ok.md': fm() + HEAD10 + '\n' + SEP10 + '\n' +
|
|
243
|
+
'| a | — | implementer | — | 做 A | src/a/** | 测试过 | | | 0 |\n' +
|
|
244
|
+
'| ask | a | — | — | 用户确认可继续 | — | 一句「可以」 | | 用户确认 | |\n' +
|
|
245
|
+
'| b | ask | — | — | — | — | — | npm test | | |\n',
|
|
246
|
+
'human-bad.md': fm() + HEAD10 + '\n' + SEP10 + '\n' +
|
|
247
|
+
'| both | — | — | — | 双填 | — | — | npm test | 用户确认 | |\n' +
|
|
248
|
+
'| retry | — | — | — | 确认 | — | 一句可以 | | 用户确认 | 2 |\n',
|
|
249
|
+
'no-human-col.md': fm() + HEAD + '\n' + SEP + '\n' +
|
|
250
|
+
'| a | — | — | — | — | — | — | npm test | |\n',
|
|
251
|
+
});
|
|
252
|
+
const r = lintWorkflows({ root });
|
|
253
|
+
const ok = r.files.find((f) => f.file === 'human-ok.md');
|
|
254
|
+
const bad = r.files.find((f) => f.file === 'human-bad.md');
|
|
255
|
+
const legacy = r.files.find((f) => f.file === 'no-human-col.md');
|
|
256
|
+
check('S14 human 单形态 10 列全绿;human+gate 双填 E9;human×retries W4;9 列无 human 表全过',
|
|
257
|
+
r.errorCount === 1
|
|
258
|
+
&& ok.errors.length === 0 && ok.warnings.length === 0
|
|
259
|
+
&& bad.errors.length === 1 && bad.errors[0].startsWith('E9') && bad.errors[0].includes('gate + human')
|
|
260
|
+
&& bad.warnings.length === 1 && bad.warnings[0].startsWith('W4')
|
|
261
|
+
&& legacy.errors.length === 0 && legacy.warnings.length === 0,
|
|
262
|
+
JSON.stringify(r.files.map((f) => ({ f: f.file, e: f.errors, w: f.warnings }))));
|
|
263
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
267
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# workflow 文档枚举单源(2026-09-25 enum-single-source)
|
|
2
|
+
# 本文件会被 core.autocrlf 检出为 CRLF——读取方显式去 \r(check-loop enum_val / node parseEnums),禁止依赖行尾格式(rule-budgets.txt 同约定)。
|
|
3
|
+
# 消费方:check-loop.sh / workflow-board-server.mjs / kb-search.mjs / gen-workflow-index.mjs /
|
|
4
|
+
# fill-{intent,plan,spec}.mjs(经 scripts/workflow-enums.mjs 读取器)——改枚举改这里,别改消费方。
|
|
5
|
+
# 格式:key=value value value(空格分隔;# 注释行忽略;键重复 / 值重复 / 缺键 = 消费方 fail-loud)
|
|
6
|
+
# 语义:doc = intent/spec/plan 三件;incident = 事故件。
|
|
7
|
+
# all 全生命周期状态
|
|
8
|
+
# confirmed 已确认(check-loop「状态未确认」放行口径 = 看板枚举告警口径)
|
|
9
|
+
# active 活跃(INDEX 活跃层 / kb-search 活跃 ×1.3 排序)
|
|
10
|
+
# terminal 终态(看板「入口已 done 而 plan 未终态」告警口径)
|
|
11
|
+
# abandoned 放弃留档(L3 确认三件豁免口径)
|
|
12
|
+
doc.status.all=draft approved done superseded cancelled
|
|
13
|
+
doc.status.confirmed=approved done superseded cancelled
|
|
14
|
+
doc.status.active=draft approved
|
|
15
|
+
doc.status.terminal=done superseded cancelled
|
|
16
|
+
doc.status.abandoned=superseded cancelled
|
|
17
|
+
incident.status.all=open fixed closed
|
|
18
|
+
incident.status.active=open
|
|
19
|
+
level.all=L0 L1 L2 L3
|