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,61 @@
|
|
|
1
|
+
// fill-spec.test.mjs — fill-spec 工具测试(2026-09-25 doc-fill-tools)
|
|
2
|
+
import { renderSpec } from './fill-spec.mjs';
|
|
3
|
+
|
|
4
|
+
let pass = 0, failCount = 0;
|
|
5
|
+
function check(name, cond, detail = '') {
|
|
6
|
+
if (cond) { pass++; console.log('PASS ' + name); }
|
|
7
|
+
else { failCount++; console.log('FAIL ' + name + (detail ? '——' + detail : '')); }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// ---- 场景 1:5 节正文齐 ----
|
|
11
|
+
{
|
|
12
|
+
const { body } = renderSpec({ topic: 'test', level: 'L2' });
|
|
13
|
+
const expectedSections = ['功能行为', '数据流', '系统改动', '约束遵守映射', '风险评估', '确认与复核'];
|
|
14
|
+
for (const s of expectedSections) check('S1 含节 ## ' + s, body.includes('## ' + s));
|
|
15
|
+
check('S1 共 6 节标题', (body.match(/^##\s/gm) || []).length === 6);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ---- 场景 2:frontmatter 含 5 字段 ----
|
|
19
|
+
{
|
|
20
|
+
const { body } = renderSpec({ topic: 't', level: 'L2' });
|
|
21
|
+
const fm = body.match(/^---([\s\S]*?)---/);
|
|
22
|
+
check('S2 frontmatter 含状态 draft', /状态:\s*draft/.test(fm[1]));
|
|
23
|
+
check('S2 frontmatter 含级别 L2', /级别:\s*L2/.test(fm[1]));
|
|
24
|
+
check('S2 frontmatter 含日期', /日期:\s*\d{4}-\d{2}-\d{2}/.test(fm[1]));
|
|
25
|
+
check('S2 frontmatter 含模块', /模块:/.test(fm[1]));
|
|
26
|
+
check('S2 frontmatter 含备注', /备注:/.test(fm[1]));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---- 场景 3:L3 也合法(默认 L2,但显式 L3 也接受)----
|
|
30
|
+
{
|
|
31
|
+
const { body } = renderSpec({ topic: 't', level: 'L3' });
|
|
32
|
+
check('S3 L3 合法', /级别:\s*L3/.test(body.match(/^---([\s\S]*?)---/)[1]));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---- 场景 4:非法 level 抛错 ----
|
|
36
|
+
{
|
|
37
|
+
let threw = false;
|
|
38
|
+
try { renderSpec({ topic: 't', level: 'XX' }); } catch { threw = true; }
|
|
39
|
+
check('S4 非法 level 抛错', threw);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---- 场景 5:默认 L2 ----
|
|
43
|
+
{
|
|
44
|
+
const { body } = renderSpec({ topic: 't' });
|
|
45
|
+
check('S5 默认级别 L2', /级别:\s*L2/.test(body.match(/^---([\s\S]*?)---/)[1]));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---- 场景 6:日期透传 ----
|
|
49
|
+
{
|
|
50
|
+
const { body } = renderSpec({ topic: 't', date: '2026-12-31' });
|
|
51
|
+
check('S6 自定义日期透传', body.includes('日期: 2026-12-31'));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---- 场景 7:L3 必填提示独立复核 ----
|
|
55
|
+
{
|
|
56
|
+
const { body } = renderSpec({ topic: 't', level: 'L3' });
|
|
57
|
+
check('S7 L3 节确认与复核含独立复核提示', body.includes('L3 必须独立复核'));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.log('\n合计: PASS ' + pass + ' / FAIL ' + failCount);
|
|
61
|
+
process.exit(failCount ? 1 : 0);
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// flow-kit gate-checklist:三处口径一致性检查(doctor §检查项 ↔ check-loop §检查项,显式配对登记表)
|
|
2
|
+
// 目的:任何一处加新检查项,另一处不会自动同步——本工具按登记表给出对照表 + 断档/未登记告警。
|
|
3
|
+
// 机制(2026-09-25 gate-checklist-registry 重写):关键词猜测匹配退役(实测 18×14 项只匹配 2、报 28 条
|
|
4
|
+
// 噪声缺点,真漏点不可见),改为显式配对登记表 PAIRS(单源本文件)——沿 workflow-modules.txt
|
|
5
|
+
// 的单源思路与 check-loop「frontmatter 受限子集替代全文正则猜测」同一演进路径。
|
|
6
|
+
// 解析面(只读不改两处源码,均为既有稳定结构):
|
|
7
|
+
// doctor:src/doctor.mjs 节注释 `// N. 标题`(N 支持 6.5 小节号)
|
|
8
|
+
// check-loop:.agents/scripts/check-loop.sh 头部检查项注释 `# N. 标题 [severity]`(多行标题截断仅影响展示,配对按 id)
|
|
9
|
+
// 登记三形态:
|
|
10
|
+
// { doctor, cl } 直接配对——同一检查两处实现
|
|
11
|
+
// { doctor, cl: null } 声明独有——doctor 装户体检面,无 check-loop 对应(原 intent「只报告不修复」拍板的已知漏点入账)
|
|
12
|
+
// { doctor: '7', cl } 经 §7 覆盖——doctor §7 整体运行 check-loop,各 #N 自动流入 doctor
|
|
13
|
+
// 报告(只报告不修复,B-b;exit 恒 0):
|
|
14
|
+
// 断档:登记表声明的 id 在对应侧已不存在(检查项被删/改号而登记未跟)
|
|
15
|
+
// 未登记:任一侧出现登记表之外的 § 检查项(新检查项没对账——本工具的核心信号)
|
|
16
|
+
// 用法:node .agents/scripts/gate-checklist.mjs --diff
|
|
17
|
+
// node .agents/scripts/gate-checklist.mjs --json
|
|
18
|
+
// 零依赖;不修改 doctor / check-loop 既有检查项
|
|
19
|
+
// 测试:node templates/_agents/scripts/gate-checklist.test.mjs(fixture + 真实仓库 baseline)
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
|
|
23
|
+
function fail(msg) { console.error('❌ ' + msg); process.exit(1); }
|
|
24
|
+
|
|
25
|
+
// parseDoctorSections:扫 doctor.mjs 节注释 → [{id, title}]。两种真实格式:
|
|
26
|
+
// `// N. 标题`(整数节号带点,如 // 1. Node 版本)与 `// N.M 标题`(小节号不带点,如 // 6.5 delegations)。
|
|
27
|
+
// 整数无点(如正文注释 // 4 宿主目录映射)不匹配——避免函数体普通注释误判为节。
|
|
28
|
+
function parseDoctorSections(doctorSrc) {
|
|
29
|
+
const out = [];
|
|
30
|
+
const re = /^\s*\/\/\s+(\d+\.\d+|\d+\.)\s+([^\n]+)/gm;
|
|
31
|
+
let m;
|
|
32
|
+
while ((m = re.exec(doctorSrc)) !== null) {
|
|
33
|
+
const title = m[2].split(/[((]/)[0].trim() || m[2].trim();
|
|
34
|
+
out.push({ id: m[1].replace(/\.$/, ''), title });
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// parseCheckLoopChecks:扫 check-loop 检查项注释(`// N. 标题 [severity]` 于 .mjs 头部清单——2026-09-26
|
|
40
|
+
// check-loop-node 迁移后的主位;旧装户 sync 前只有 .sh 时按 `# N. 标题` fallback,登记表口径不变)
|
|
41
|
+
function parseCheckLoopChecks(clSrc) {
|
|
42
|
+
const checks = [];
|
|
43
|
+
const re = /^\s*(?:\/\/|#)\s*(\d+)\.\s+(.+?)(?:\s*\[(hard-block|warning|advisory)\])?\s*$/gm;
|
|
44
|
+
let m;
|
|
45
|
+
while ((m = re.exec(clSrc)) !== null) {
|
|
46
|
+
checks.push({ id: m[1], title: m[2].trim(), severity: m[3] || null });
|
|
47
|
+
}
|
|
48
|
+
return checks;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---- 配对登记表(单源契约;2026-09-25 gate-checklist-registry 全量登记)----
|
|
52
|
+
// 纪律:doctor / check-loop 任一处加新 § 检查项,必须同步在本表登记(直接配对 / 声明独有 / 经 §7 覆盖),
|
|
53
|
+
// 否则工具报「未登记」。删/改检查项须同步改本表,否则报「断档」。
|
|
54
|
+
const PAIRS = [
|
|
55
|
+
// 直接配对:同一检查两处实现
|
|
56
|
+
{ doctor: '5', cl: '2', note: '占位符残留' },
|
|
57
|
+
{ doctor: '6', cl: '11', note: 'workflow/INDEX.md 漂移' },
|
|
58
|
+
// 声明独有:doctor 装户体检面(原 intent 拍板「只报告不修复」的已知漏点入账)
|
|
59
|
+
{ doctor: '1', cl: null, note: 'Node 版本——装户体检独有' },
|
|
60
|
+
{ doctor: '2', cl: null, note: '目录布局——装户体检独有' },
|
|
61
|
+
{ doctor: '3', cl: null, note: 'git 仓库与钩子——装户体检独有' },
|
|
62
|
+
{ doctor: '4', cl: null, note: 'kit.json managed 台账——装户体检独有' },
|
|
63
|
+
{ doctor: '6.5', cl: null, note: 'delegations 台账结构——装户体检独有' },
|
|
64
|
+
{ doctor: '6.6', cl: null, note: 'owned 漂移——装户体检独有' },
|
|
65
|
+
{ doctor: '6.7', cl: null, note: '跨宿主薄适配——装户体检独有' },
|
|
66
|
+
{ doctor: '6.8', cl: null, note: 'workflows 编排脚本 lint——装户体检独有' },
|
|
67
|
+
{ doctor: '8', cl: null, note: '看板端口——信息级独有' },
|
|
68
|
+
// 经 §7 覆盖:doctor §7 整体运行 check-loop(#2/#11 已直接配对,不重复登记)
|
|
69
|
+
{ doctor: '7', cl: '1', note: '经 §7 整体运行覆盖' },
|
|
70
|
+
{ doctor: '7', cl: '3', note: '经 §7 整体运行覆盖' },
|
|
71
|
+
{ doctor: '7', cl: '4', note: '经 §7 整体运行覆盖' },
|
|
72
|
+
{ doctor: '7', cl: '5', note: '经 §7 整体运行覆盖' },
|
|
73
|
+
{ doctor: '7', cl: '6', note: '经 §7 整体运行覆盖' },
|
|
74
|
+
{ doctor: '7', cl: '7', note: '经 §7 整体运行覆盖' },
|
|
75
|
+
{ doctor: '7', cl: '8', note: '经 §7 整体运行覆盖' },
|
|
76
|
+
{ doctor: '7', cl: '9', note: '经 §7 整体运行覆盖' },
|
|
77
|
+
{ doctor: '7', cl: '10', note: '经 §7 整体运行覆盖' },
|
|
78
|
+
{ doctor: '7', cl: '12', note: '经 §7 整体运行覆盖' },
|
|
79
|
+
{ doctor: '7', cl: '13', note: '经 §7 整体运行覆盖' },
|
|
80
|
+
{ doctor: '7', cl: '14', note: '经 §7 整体运行覆盖' },
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
// gateChecklist:按登记表对照两侧检查项(pure function;pairs 可注入供测试)
|
|
84
|
+
// 返回 { doctorCount, checkLoopCount, pairs, matched, broken, unregistered, doctor, checkLoop }
|
|
85
|
+
// matched = 登记两侧都存在的条目(直接配对 + 经 §7 覆盖)
|
|
86
|
+
// broken = 登记声明的 id 在对应侧已不存在
|
|
87
|
+
// unregistered = 两侧出现登记表之外的 § 检查项(新检查项未对账——核心信号)
|
|
88
|
+
export function gateChecklist({ doctorSrc, checkLoopSrc, pairs = PAIRS }) {
|
|
89
|
+
const doctor = parseDoctorSections(doctorSrc);
|
|
90
|
+
const checkLoop = parseCheckLoopChecks(checkLoopSrc);
|
|
91
|
+
const doctorIds = new Set(doctor.map((d) => d.id));
|
|
92
|
+
const clIds = new Set(checkLoop.map((c) => c.id));
|
|
93
|
+
const declaredDoctor = new Set(pairs.map((p) => p.doctor).filter(Boolean));
|
|
94
|
+
const declaredCl = new Set(pairs.map((p) => p.cl).filter(Boolean));
|
|
95
|
+
|
|
96
|
+
const broken = [];
|
|
97
|
+
for (const p of pairs) {
|
|
98
|
+
if (p.doctor && !doctorIds.has(p.doctor)) broken.push({ side: 'doctor', id: p.doctor, note: p.note || '' });
|
|
99
|
+
if (p.cl && !clIds.has(p.cl)) broken.push({ side: 'check-loop', id: p.cl, note: p.note || '' });
|
|
100
|
+
}
|
|
101
|
+
const unregistered = [];
|
|
102
|
+
for (const d of doctor) {
|
|
103
|
+
if (!declaredDoctor.has(d.id)) unregistered.push({ side: 'doctor', id: d.id, title: d.title });
|
|
104
|
+
}
|
|
105
|
+
for (const c of checkLoop) {
|
|
106
|
+
if (!declaredCl.has(c.id)) unregistered.push({ side: 'check-loop', id: c.id, title: c.title });
|
|
107
|
+
}
|
|
108
|
+
const matched = pairs.filter((p) => p.doctor && doctorIds.has(p.doctor) && p.cl && clIds.has(p.cl));
|
|
109
|
+
return {
|
|
110
|
+
doctorCount: doctor.length,
|
|
111
|
+
checkLoopCount: checkLoop.length,
|
|
112
|
+
pairs,
|
|
113
|
+
matched,
|
|
114
|
+
broken,
|
|
115
|
+
unregistered,
|
|
116
|
+
doctor,
|
|
117
|
+
checkLoop,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const clip = (s, n) => (String(s).length > n ? String(s).slice(0, n) + '…' : String(s));
|
|
122
|
+
|
|
123
|
+
function printDiff(result) {
|
|
124
|
+
console.log('▶ flow-kit gate-checklist --diff(显式配对登记表)');
|
|
125
|
+
const direct = result.pairs.filter((p) => p.doctor !== '7' && p.cl).length;
|
|
126
|
+
const via = result.pairs.filter((p) => p.doctor === '7').length;
|
|
127
|
+
const solo = result.pairs.filter((p) => !p.cl).length;
|
|
128
|
+
console.log(` doctor §检查项: ${result.doctorCount} | check-loop §检查项: ${result.checkLoopCount} | 直接配对 ${direct} + 经 §7 覆盖 ${via} + 声明独有 ${solo}`);
|
|
129
|
+
const doctorById = new Map(result.doctor.map((d) => [d.id, d]));
|
|
130
|
+
const clById = new Map(result.checkLoop.map((c) => [c.id, c]));
|
|
131
|
+
console.log(' 对照表(登记表展开):');
|
|
132
|
+
for (const p of result.pairs) {
|
|
133
|
+
const d = doctorById.get(p.doctor);
|
|
134
|
+
if (!p.cl) {
|
|
135
|
+
console.log(` ● §${p.doctor} ${clip(d?.title || '?', 22)}(声明独有:${p.note})`);
|
|
136
|
+
} else {
|
|
137
|
+
const c = clById.get(p.cl);
|
|
138
|
+
const mark = d && c ? '✓' : '✗';
|
|
139
|
+
console.log(` ${mark} §${p.doctor} ${clip(d?.title || '?', 22)} ↔ #${p.cl} ${clip(c?.title || '?', 26)}(${p.note})`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (result.broken.length) {
|
|
143
|
+
console.log(' 断档(登记的检查项已不存在——删/改检查项后登记表未跟):');
|
|
144
|
+
for (const b of result.broken) {
|
|
145
|
+
console.log(` ⚠️ ${b.side === 'doctor' ? 'doctor §' + b.id : 'check-loop #' + b.id} 已不存在(登记备注:${b.note || '无'})`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (result.unregistered.length) {
|
|
149
|
+
console.log(' 未登记(新检查项未对账——配对 / 声明独有 / 经 §7 三选一登记进 PAIRS):');
|
|
150
|
+
for (const u of result.unregistered) {
|
|
151
|
+
console.log(` ⚠️ ${u.side === 'doctor' ? 'doctor §' + u.id : 'check-loop #' + u.id} ${clip(u.title, 30)}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (!result.broken.length && !result.unregistered.length) {
|
|
155
|
+
console.log(' ✅ 登记完整(0 断档 / 0 未登记)');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const isMain = process.argv[1] && process.argv[1].endsWith('gate-checklist.mjs');
|
|
160
|
+
if (isMain) {
|
|
161
|
+
let jsonMode = false;
|
|
162
|
+
for (const a of process.argv.slice(2)) {
|
|
163
|
+
if (a === '--json') jsonMode = true;
|
|
164
|
+
else if (a === '--diff') { /* 默认模式,兼容显式传参 */ }
|
|
165
|
+
else fail('未知选项:' + a + '(flow-kit gate-checklist --diff / --json)');
|
|
166
|
+
}
|
|
167
|
+
const pkgRoot = process.argv[2] && !process.argv[2].startsWith('--') ? process.argv[2] : null;
|
|
168
|
+
const base = pkgRoot || process.cwd();
|
|
169
|
+
const doctorPath = path.join(base, 'src', 'doctor.mjs');
|
|
170
|
+
const clPathMjs = path.join(base, '.agents', 'scripts', 'check-loop.mjs');
|
|
171
|
+
const clPath = fs.existsSync(clPathMjs) ? clPathMjs : path.join(base, '.agents', 'scripts', 'check-loop.sh');
|
|
172
|
+
if (!fs.existsSync(doctorPath)) fail('找不到 doctor.mjs:' + doctorPath);
|
|
173
|
+
if (!fs.existsSync(clPath)) fail('找不到 check-loop(.mjs / .sh 均无):' + clPath);
|
|
174
|
+
const doctorSrc = fs.readFileSync(doctorPath, 'utf8');
|
|
175
|
+
const checkLoopSrc = fs.readFileSync(clPath, 'utf8');
|
|
176
|
+
const result = gateChecklist({ doctorSrc, checkLoopSrc });
|
|
177
|
+
if (jsonMode) {
|
|
178
|
+
process.stdout.write(JSON.stringify({
|
|
179
|
+
doctorCount: result.doctorCount,
|
|
180
|
+
checkLoopCount: result.checkLoopCount,
|
|
181
|
+
matched: result.matched.map((p) => ({ doctor: p.doctor, cl: p.cl, note: p.note || '' })),
|
|
182
|
+
solo: result.pairs.filter((p) => !p.cl).map((p) => ({ doctor: p.doctor, note: p.note || '' })),
|
|
183
|
+
broken: result.broken,
|
|
184
|
+
unregistered: result.unregistered,
|
|
185
|
+
}, null, 2) + '\n');
|
|
186
|
+
} else {
|
|
187
|
+
printDiff(result);
|
|
188
|
+
}
|
|
189
|
+
// diff/json 模式总 exit 0;断档/未登记不视作错误(人类阅读用;B-b 决策「只报告不修复」)
|
|
190
|
+
process.exit(0);
|
|
191
|
+
}
|
|
192
|
+
export default gateChecklist;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// gate-checklist.test.mjs — 显式配对登记表测试(2026-09-25 gate-checklist-registry)
|
|
2
|
+
// 方法:fixture mini doctor.mjs / check-loop.sh 源码 + 注入 pairs,调 gateChecklist() 拿结构化结果;
|
|
3
|
+
// 真实仓库两场景做 baseline;--json CLI spawn 冒烟回归原「字段丢失」bug。
|
|
4
|
+
// 判据:① 两侧解析正确 ② 三种登记形态不误报 ③ 断档/未登记检测命中 ④ 真实仓库 0 断档 0 未登记
|
|
5
|
+
// 用法:node templates/_agents/scripts/gate-checklist.test.mjs(npm test 随跑)
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { gateChecklist } from './gate-checklist.mjs';
|
|
11
|
+
|
|
12
|
+
let pass = 0;
|
|
13
|
+
let failCount = 0;
|
|
14
|
+
function check(name, cond, detail = '') {
|
|
15
|
+
if (cond) { pass++; console.log('PASS ' + name); }
|
|
16
|
+
else { failCount++; console.log('FAIL ' + name + (detail ? '——' + detail : '')); }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const SRC_ROOT = path.resolve(SCRIPT_DIR, '..', '..', '..');
|
|
21
|
+
|
|
22
|
+
// fixture:5 节 doctor(1 / 5 / 6.5 / 7 / 9)+ 2 项 check-loop(#2 / #3)
|
|
23
|
+
const FIXTURE_DOCTOR = `// doctor fixture
|
|
24
|
+
function f() {
|
|
25
|
+
// 1. Node 版本(装户体检独有)
|
|
26
|
+
const a = 1;
|
|
27
|
+
// 5. 占位符残留(与 check-loop #2 直接配对)
|
|
28
|
+
const b = 2;
|
|
29
|
+
// 6.5 delegations 台账结构(小节号解析)
|
|
30
|
+
const c = 3;
|
|
31
|
+
// 7. check-loop(fixture 里也有 §7,供经 §7 覆盖登记)
|
|
32
|
+
const e = 5;
|
|
33
|
+
// 9. 未登记的新检查项(应报未登记)
|
|
34
|
+
const d = 4;
|
|
35
|
+
return [a, b, c, d, e];
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
const FIXTURE_CL = `#!/bin/sh
|
|
39
|
+
# 2. 模板字段占位符残留 [warning]
|
|
40
|
+
# 3. incidents 复盘三件套完整性
|
|
41
|
+
# 7. 未登记的新检查项
|
|
42
|
+
exit 0
|
|
43
|
+
`;
|
|
44
|
+
// fixture 登记表:5↔2 直接配对、1 声明独有、6.5 声明独有、7→#3 经 §7 覆盖;
|
|
45
|
+
// §99 / #99 是登记了但两侧都不存在的 id(应报断档×2)
|
|
46
|
+
const FIXTURE_PAIRS = [
|
|
47
|
+
{ doctor: '5', cl: '2', note: '占位符' },
|
|
48
|
+
{ doctor: '1', cl: null, note: '独有' },
|
|
49
|
+
{ doctor: '6.5', cl: null, note: '独有' },
|
|
50
|
+
{ doctor: '7', cl: '3', note: '经 §7' },
|
|
51
|
+
{ doctor: '99', cl: null, note: '断档——doctor 侧不存在' },
|
|
52
|
+
{ doctor: '1', cl: '99', note: '断档——check-loop 侧不存在' },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const run = (pairs = FIXTURE_PAIRS, doctorSrc = FIXTURE_DOCTOR, clSrc = FIXTURE_CL) =>
|
|
56
|
+
gateChecklist({ doctorSrc, checkLoopSrc: clSrc, pairs });
|
|
57
|
+
|
|
58
|
+
// ---- S1 解析:doctor 5 节(含 6.5 小节号、§7 与未登记的 9)----
|
|
59
|
+
{
|
|
60
|
+
const r = run();
|
|
61
|
+
check('S1 fixture doctor 解析 5 节且含 6.5 小节号',
|
|
62
|
+
r.doctorCount === 5 && r.doctor.some((d) => d.id === '6.5' && d.title.includes('delegations')),
|
|
63
|
+
JSON.stringify(r.doctor));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---- S2 解析:check-loop 3 项(#2 带 severity,#3 无)----
|
|
67
|
+
{
|
|
68
|
+
const r = run();
|
|
69
|
+
const c2 = r.checkLoop.find((c) => c.id === '2');
|
|
70
|
+
const c3 = r.checkLoop.find((c) => c.id === '3');
|
|
71
|
+
check('S2 fixture check-loop 解析 3 项且 severity 提取正确',
|
|
72
|
+
r.checkLoopCount === 3 && c2.severity === 'warning' && c3.severity === null,
|
|
73
|
+
JSON.stringify(r.checkLoop));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---- S3 直接配对:5↔2 双侧存在 → matched 命中 ----
|
|
77
|
+
{
|
|
78
|
+
const r = run();
|
|
79
|
+
check('S3 直接配对 §5↔#2 进入 matched',
|
|
80
|
+
r.matched.some((p) => p.doctor === '5' && p.cl === '2'),
|
|
81
|
+
JSON.stringify(r.matched.map((p) => [p.doctor, p.cl])));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---- S4 声明独有:§1 / §6.5 不产生任何 findings ----
|
|
85
|
+
{
|
|
86
|
+
const r = run();
|
|
87
|
+
const hit = [...r.broken, ...r.unregistered].filter((f) => f.id === '1' || f.id === '6.5');
|
|
88
|
+
check('S4 声明独有(§1 / §6.5)不误报',
|
|
89
|
+
hit.length === 0,
|
|
90
|
+
JSON.stringify(hit));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---- S5 经 §7 覆盖:7→#3 不产生 findings 且进 matched ----
|
|
94
|
+
{
|
|
95
|
+
const r = run();
|
|
96
|
+
check('S5 经 §7 覆盖登记不误报且进 matched',
|
|
97
|
+
r.matched.some((p) => p.doctor === '7' && p.cl === '3') && !r.unregistered.some((u) => u.id === '3'),
|
|
98
|
+
JSON.stringify(r.unregistered));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---- S6 断档:登记 §99 / #99 两侧不存在 → broken ×2 ----
|
|
102
|
+
{
|
|
103
|
+
const r = run();
|
|
104
|
+
const bDoc = r.broken.find((b) => b.side === 'doctor' && b.id === '99');
|
|
105
|
+
const bCl = r.broken.find((b) => b.side === 'check-loop' && b.id === '99');
|
|
106
|
+
check('S6 登记断档两侧各报一条(doctor §99 / check-loop #99)',
|
|
107
|
+
r.broken.length === 2 && !!bDoc && !!bCl,
|
|
108
|
+
JSON.stringify(r.broken));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---- S7 未登记:doctor §9 与 check-loop #7 → unregistered ×2 ----
|
|
112
|
+
{
|
|
113
|
+
const r = run();
|
|
114
|
+
const uDoc = r.unregistered.find((u) => u.side === 'doctor' && u.id === '9');
|
|
115
|
+
const uCl = r.unregistered.find((u) => u.side === 'check-loop' && u.id === '7');
|
|
116
|
+
check('S7 未登记两侧各报一条(doctor §9 / check-loop #7)',
|
|
117
|
+
r.unregistered.length === 2 && !!uDoc && !!uCl,
|
|
118
|
+
JSON.stringify(r.unregistered));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---- S8 空源码 + 空登记表不崩(0 findings)----
|
|
122
|
+
{
|
|
123
|
+
const r = run([], '', '');
|
|
124
|
+
check('S8 空源码 + 空登记表不崩(0/0 且 0 findings)',
|
|
125
|
+
r.doctorCount === 0 && r.checkLoopCount === 0 && r.broken.length === 0 && r.unregistered.length === 0,
|
|
126
|
+
JSON.stringify(r));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---- S8b 整数无点注释不误判为节(doctor.mjs:257 // 4 宿主目录映射 形态)----
|
|
130
|
+
{
|
|
131
|
+
const doctorSrc = `// 4. kit.json managed 台账(真节)
|
|
132
|
+
const x = 1;
|
|
133
|
+
// 4 宿主目录映射(与 profiles 同源——正文注释,非节)
|
|
134
|
+
const y = 2;
|
|
135
|
+
`;
|
|
136
|
+
const r = gateChecklist({ doctorSrc, checkLoopSrc: '', pairs: [{ doctor: '4', cl: null, note: '独有' }] });
|
|
137
|
+
check('S8b 整数无点的正文注释不误判(仅解析 1 节 §4 且标题为真节)',
|
|
138
|
+
r.doctorCount === 1 && r.doctor[0].title.includes('kit.json') && r.broken.length === 0 && r.unregistered.length === 0,
|
|
139
|
+
JSON.stringify(r.doctor));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---- S9 重复 id 登记(同 cl 两个直接配对)不崩且均可命中 ----
|
|
143
|
+
{
|
|
144
|
+
const pairs = [
|
|
145
|
+
{ doctor: '5', cl: '2', note: 'a' },
|
|
146
|
+
{ doctor: '1', cl: '2', note: 'b' },
|
|
147
|
+
];
|
|
148
|
+
const r = run(pairs);
|
|
149
|
+
check('S9 多条目登记同一 cl 不崩(matched 2)',
|
|
150
|
+
r.matched.length === 2 && r.unregistered.some((u) => u.side === 'check-loop' && u.id === '3'),
|
|
151
|
+
JSON.stringify({ m: r.matched.length, u: r.unregistered }));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const realDoctor = () => fs.readFileSync(path.join(SRC_ROOT, 'src', 'doctor.mjs'), 'utf8');
|
|
155
|
+
// check-loop 解析面与 CLI 同口径:.mjs 优先(2026-09-26 迁移后主位),fallback .sh(旧装户)
|
|
156
|
+
const realCl = () => {
|
|
157
|
+
const mjs = path.join(SRC_ROOT, '.agents', 'scripts', 'check-loop.mjs');
|
|
158
|
+
return fs.readFileSync(fs.existsSync(mjs) ? mjs : path.join(SRC_ROOT, '.agents', 'scripts', 'check-loop.sh'), 'utf8');
|
|
159
|
+
};
|
|
160
|
+
const hasReal = fs.existsSync(path.join(SRC_ROOT, 'src', 'doctor.mjs'));
|
|
161
|
+
|
|
162
|
+
// ---- S10 真实仓库 baseline:PAIRS 全量登记 → 0 断档 / 0 未登记 ----
|
|
163
|
+
if (hasReal) {
|
|
164
|
+
const r = gateChecklist({ doctorSrc: realDoctor(), checkLoopSrc: realCl() });
|
|
165
|
+
check('S10 真实仓库登记完整(0 断档 / 0 未登记)',
|
|
166
|
+
r.broken.length === 0 && r.unregistered.length === 0,
|
|
167
|
+
JSON.stringify({ broken: r.broken, unregistered: r.unregistered }));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---- S11 真实仓库计数:doctor ≥ 12 节 / check-loop ≥ 14 项 ----
|
|
171
|
+
if (hasReal) {
|
|
172
|
+
const r = gateChecklist({ doctorSrc: realDoctor(), checkLoopSrc: realCl() });
|
|
173
|
+
check('S11 真实仓库 doctor ≥ 12 节且 check-loop ≥ 14 项(baseline)',
|
|
174
|
+
r.doctorCount >= 12 && r.checkLoopCount >= 14,
|
|
175
|
+
`doctor ${r.doctorCount} / check-loop ${r.checkLoopCount}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---- S12 --json CLI 冒烟:键齐无 undefined(回归原 .title 字段丢失 bug)----
|
|
179
|
+
if (hasReal) {
|
|
180
|
+
const p = spawnSync(process.execPath, [path.join(SCRIPT_DIR, 'gate-checklist.mjs'), '--json'], { cwd: SRC_ROOT, encoding: 'utf8' });
|
|
181
|
+
let j = null;
|
|
182
|
+
try { j = JSON.parse(p.stdout); } catch { /* fail below */ }
|
|
183
|
+
const keysOk = j && ['doctorCount', 'checkLoopCount', 'matched', 'solo', 'broken', 'unregistered'].every((k) => k in j);
|
|
184
|
+
const noHoles = j && JSON.stringify(j).includes('undefined') === false
|
|
185
|
+
&& j.matched.every((m) => typeof m.doctor === 'string' && typeof m.cl === 'string' && 'note' in m);
|
|
186
|
+
check('S12 --json 可解析、键齐、条目无 undefined 丢字段、exit 0',
|
|
187
|
+
p.status === 0 && !!keysOk && !!noHoles,
|
|
188
|
+
String(p.stdout).slice(0, 200));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---- S13 doctor 多行节注释:首行即标题(6.6 形态)解析不漏 ----
|
|
192
|
+
{
|
|
193
|
+
const doctorSrc = `// 6.6 owned 漂移校验(kit.owned 列表盘面 sha 不一致;
|
|
194
|
+
// 漂移信号靠本校验给装户可见性)
|
|
195
|
+
const x = 1;
|
|
196
|
+
`;
|
|
197
|
+
const r = gateChecklist({ doctorSrc, checkLoopSrc: '', pairs: [{ doctor: '6.6', cl: null, note: '独有' }] });
|
|
198
|
+
check('S13 多行节注释(§6.6 形态)首行命中且登记不报 findings',
|
|
199
|
+
r.doctorCount === 1 && r.doctor[0].title.includes('owned') && r.broken.length === 0 && r.unregistered.length === 0,
|
|
200
|
+
JSON.stringify(r.doctor));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
console.log('\n合计: PASS ' + pass + ' / FAIL ' + failCount);
|
|
204
|
+
process.exit(failCount ? 1 : 0);
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// 生成区:INDEX.md 的 GENERATED:BEGIN/END 锚点注释之间整段重写,锚点外内容原样保留。
|
|
11
11
|
import fs from 'node:fs';
|
|
12
12
|
import path from 'node:path';
|
|
13
|
+
import { ENUMS } from './workflow-enums.mjs';
|
|
13
14
|
|
|
14
15
|
const USAGE = `用法:node .agents/scripts/gen-workflow-index.mjs [--dry-run] [--check] [--help]
|
|
15
16
|
|
|
@@ -32,14 +33,15 @@ const WORKFLOW = 'workflow';
|
|
|
32
33
|
const INDEX_P = path.join(WORKFLOW, 'INDEX.md');
|
|
33
34
|
const DOC_TYPES = ['intents', 'specs', 'plans', 'incidents'];
|
|
34
35
|
const TYPE_LABEL = { intents: 'INTENT', specs: 'SPEC', plans: 'PLAN', incidents: 'INCIDENT' };
|
|
35
|
-
|
|
36
|
+
// 活跃口径 = 单源 doc.status.active ∪ incident.status.active;其余状态折叠进档案计数
|
|
37
|
+
const ACTIVE_STATUS = [...ENUMS['doc.status.active'], ...ENUMS['incident.status.active']];
|
|
36
38
|
|
|
37
39
|
const BEGIN = '<!-- GENERATED:BEGIN — gen-workflow-index.mjs 整段重写,手工说明写在本行之前 -->';
|
|
38
40
|
const END = '<!-- GENERATED:END -->';
|
|
39
41
|
const HEADER = `# workflow 索引 — 活跃层与档案(生成物,勿手改)
|
|
40
42
|
|
|
41
43
|
> 生成器 \`node .agents/scripts/gen-workflow-index.mjs\`(状态变更 / 新建文档后重跑;\`--check\` 校验漂移,check-loop 会告警)。
|
|
42
|
-
> 活跃 =
|
|
44
|
+
> 活跃 = ${ACTIVE_STATUS.join(' / ')}(在跑);其余状态折叠进档案计数,检索用 \`node .agents/scripts/kb-search.mjs "<词>"\`(默认含全部状态、活跃优先排序)。
|
|
43
45
|
> 模块词表见 \`.agents/workflow-modules.txt\`。
|
|
44
46
|
`;
|
|
45
47
|
|
|
@@ -16,6 +16,7 @@ import fs from 'node:fs';
|
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
import { createHash } from 'node:crypto';
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { ENUMS } from './workflow-enums.mjs';
|
|
19
20
|
|
|
20
21
|
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
21
22
|
const MODULES_FILE = path.join(SCRIPT_DIR, '..', 'workflow-modules.txt');
|
|
@@ -78,7 +79,8 @@ if (moduleFilter && !MODULES.includes(moduleFilter)) {
|
|
|
78
79
|
process.exit(1);
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
|
|
82
|
+
// 活跃口径 = 单源 doc.status.active ∪ incident.status.active(与 workflow/INDEX.md 活跃层一致,fixed 归档案默认也可命中)
|
|
83
|
+
const ACTIVE_STATUS = [...ENUMS['doc.status.active'], ...ENUMS['incident.status.active']];
|
|
82
84
|
const WF_SECTIONS = ['背景', '目标', '根因', '复盘三件套', '验收标准', '影响面'];
|
|
83
85
|
const WIKI_EXCLUDE = new Set(['INDEX.md', '知识沉淀总览.html']);
|
|
84
86
|
const WIKI_MAX_KB = 180;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// flow-kit source-sync-check:装户面同步一致性检查(包源 templates/_agents/ vs 装副本 .agents/)
|
|
2
|
+
// 目的:包源改了 templates/_agents/ 但 .agents/ 装副本没刷新——这种"真空地带"sync.mjs 不扫、doctor.mjs §6.6 不扫
|
|
3
|
+
// 本工具扫 templates/_agents/ 下所有 .md / .mjs / .json / .txt(排除 cache/)vs .agents/ 对应路径
|
|
4
|
+
// 输出三类差异:缺失(包源有装副本无)/ 孤儿(包源无装副本有)/ 漂移(两者都有 sha 不一致)
|
|
5
|
+
// 排除装副本独有文件:kit.json / settings.json / hooks/commit-check.config.json(init 渲染产物,不属双源)
|
|
6
|
+
// 用法:node .agents/scripts/source-sync-check.mjs --diff
|
|
7
|
+
// node .agents/scripts/source-sync-check.mjs --json
|
|
8
|
+
// node .agents/scripts/source-sync-check.mjs --pkg-root <path> --target <path>
|
|
9
|
+
// 零依赖;B-b 决策「只报告不修复」
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { createHash } from 'node:crypto';
|
|
13
|
+
|
|
14
|
+
function fail(msg) { console.error('❌ ' + msg); process.exit(1); }
|
|
15
|
+
|
|
16
|
+
const VALID_EXTS = new Set(['.md', '.mjs', '.json', '.txt']);
|
|
17
|
+
// 装副本独有文件(init 渲染产物,不属双源结构)
|
|
18
|
+
const RENDER_OUTPUT_FILES = new Set(['kit.json', 'settings.json', 'hooks/commit-check.config.json']);
|
|
19
|
+
const TARGET_EXCLUDE = RENDER_OUTPUT_FILES; // 向后兼容别名
|
|
20
|
+
|
|
21
|
+
function sha256(p) {
|
|
22
|
+
try {
|
|
23
|
+
return createHash('sha256').update(fs.readFileSync(p)).digest('hex');
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// walkMd:递归扫目录,列出所有 validExt 文件(排除 cache/)
|
|
30
|
+
function walkMd(root) {
|
|
31
|
+
const out = [];
|
|
32
|
+
function walk(dir, relBase) {
|
|
33
|
+
if (!fs.existsSync(dir)) return;
|
|
34
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
35
|
+
const rel = relBase ? relBase + '/' + e.name : e.name;
|
|
36
|
+
const abs = path.join(dir, e.name);
|
|
37
|
+
// 排除 cache/ 目录(与 sync.mjs 既有约定一致)
|
|
38
|
+
if (rel === 'cache' || rel.startsWith('cache/')) continue;
|
|
39
|
+
if (e.isDirectory()) walk(abs, rel);
|
|
40
|
+
else if (VALID_EXTS.has(path.extname(e.name))) out.push(rel);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
walk(root, '');
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function sourceSyncCheck({ pkgRoot, target }) {
|
|
48
|
+
const authorityRoot = path.join(pkgRoot, 'templates', '_agents');
|
|
49
|
+
const adapterRoot = path.join(target, '.agents');
|
|
50
|
+
const pkgFiles = walkMd(authorityRoot);
|
|
51
|
+
const tgtFiles = walkMd(adapterRoot).filter((f) => !TARGET_EXCLUDE.has(f));
|
|
52
|
+
const pkgSet = new Set(pkgFiles);
|
|
53
|
+
const tgtSet = new Set(tgtFiles);
|
|
54
|
+
const missing = []; // 包源有 / 装副本无
|
|
55
|
+
const orphan = []; // 包源无 / 装副本有
|
|
56
|
+
const drift = []; // 两者都有但 sha 不一致
|
|
57
|
+
for (const f of pkgFiles) {
|
|
58
|
+
if (!tgtSet.has(f)) { missing.push({ rel: f }); continue; }
|
|
59
|
+
const pkgSha = sha256(path.join(authorityRoot, f));
|
|
60
|
+
const tgtSha = sha256(path.join(adapterRoot, f));
|
|
61
|
+
if (pkgSha !== null && tgtSha !== null && pkgSha !== tgtSha) {
|
|
62
|
+
drift.push({ rel: f, pkgSha, tgtSha });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
for (const f of tgtFiles) {
|
|
66
|
+
if (!pkgSet.has(f)) orphan.push({ rel: f });
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
pkgRoot, target,
|
|
70
|
+
pkgCount: pkgFiles.length,
|
|
71
|
+
tgtCount: tgtFiles.length,
|
|
72
|
+
missing: missing.sort((a, b) => a.rel.localeCompare(b.rel)),
|
|
73
|
+
orphan: orphan.sort((a, b) => a.rel.localeCompare(b.rel)),
|
|
74
|
+
drift: drift.sort((a, b) => a.rel.localeCompare(b.rel)),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function printDiff(result) {
|
|
79
|
+
console.log('▶ flow-kit source-sync-check --diff');
|
|
80
|
+
console.log(' 包源 (templates/_agents/) ' + result.pkgCount + ' 份 | 装副本 (.agents/) ' + result.tgtCount + ' 份');
|
|
81
|
+
if (!result.missing.length && !result.orphan.length && !result.drift.length) {
|
|
82
|
+
console.log(' 无差异 ✅');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (result.missing.length) {
|
|
86
|
+
console.log(' 缺失(包源有 / 装副本无)' + result.missing.length + ':');
|
|
87
|
+
for (const m of result.missing) console.log(' - ' + m.rel);
|
|
88
|
+
}
|
|
89
|
+
if (result.orphan.length) {
|
|
90
|
+
console.log(' 孤儿(包源无 / 装副本有)' + result.orphan.length + ':');
|
|
91
|
+
for (const o of result.orphan) console.log(' - ' + o.rel);
|
|
92
|
+
}
|
|
93
|
+
if (result.drift.length) {
|
|
94
|
+
console.log(' 漂移(包源装副本都有但 sha 不一致)' + result.drift.length + ':');
|
|
95
|
+
for (const d of result.drift) console.log(' - ' + d.rel + ' pkg=' + d.pkgSha.slice(0, 8) + '… tgt=' + d.tgtSha.slice(0, 8) + '…');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const isMain = process.argv[1] && process.argv[1].endsWith('source-sync-check.mjs');
|
|
100
|
+
if (isMain) {
|
|
101
|
+
let jsonMode = false, diffMode = false;
|
|
102
|
+
let pkgRoot = null, target = null;
|
|
103
|
+
for (let i = 0; i < process.argv.length - 2; i++) {
|
|
104
|
+
const a = process.argv[2 + i];
|
|
105
|
+
if (a === '--json') jsonMode = true;
|
|
106
|
+
else if (a === '--diff') diffMode = true;
|
|
107
|
+
else if (a === '--pkg-root') pkgRoot = process.argv[2 + i + 1];
|
|
108
|
+
else if (a === '--target') target = process.argv[2 + i + 1];
|
|
109
|
+
}
|
|
110
|
+
if (!pkgRoot) pkgRoot = process.cwd();
|
|
111
|
+
if (!target) target = process.cwd();
|
|
112
|
+
const authorityRoot = path.join(pkgRoot, 'templates', '_agents');
|
|
113
|
+
const adapterRoot = path.join(target, '.agents');
|
|
114
|
+
if (!fs.existsSync(authorityRoot)) fail('找不到包源:' + authorityRoot);
|
|
115
|
+
if (!fs.existsSync(adapterRoot)) fail('找不到装副本:' + adapterRoot);
|
|
116
|
+
const result = sourceSyncCheck({ pkgRoot, target });
|
|
117
|
+
if (jsonMode) {
|
|
118
|
+
process.stdout.write(JSON.stringify({
|
|
119
|
+
pkgCount: result.pkgCount,
|
|
120
|
+
tgtCount: result.tgtCount,
|
|
121
|
+
missing: result.missing.map((m) => m.rel),
|
|
122
|
+
orphan: result.orphan.map((o) => o.rel),
|
|
123
|
+
drift: result.drift.map((d) => ({ rel: d.rel, pkgSha: d.pkgSha.slice(0, 8), tgtSha: d.tgtSha.slice(0, 8) })),
|
|
124
|
+
}, null, 2) + '\n');
|
|
125
|
+
} else {
|
|
126
|
+
printDiff(result);
|
|
127
|
+
}
|
|
128
|
+
// diff/json 模式总 exit 0;差异存在不视作错误(人类阅读用;B-b 决策「只报告不修复」)
|
|
129
|
+
process.exit(0);
|
|
130
|
+
}
|
|
131
|
+
export default sourceSyncCheck;
|