agentic-flow-kit 1.5.0 → 1.6.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/modules/hosts/claude/commands/wf-build.md +7 -0
- package/modules/hosts/claude/commands/wf-design.md +6 -0
- package/modules/hosts/claude/commands/wf-new-task.md +2 -0
- package/modules/hosts/codex/commands/wf-build.md +7 -0
- package/modules/hosts/codex/commands/wf-design.md +6 -0
- package/modules/hosts/codex/commands/wf-new-task.md +2 -0
- package/modules/hosts/cursor/commands/wf-build.md +7 -0
- package/modules/hosts/cursor/commands/wf-design.md +6 -0
- package/modules/hosts/cursor/commands/wf-new-task.md +2 -0
- package/modules/hosts/opencode/commands/wf-build.md +7 -0
- package/modules/hosts/opencode/commands/wf-design.md +6 -0
- package/modules/hosts/opencode/commands/wf-new-task.md +2 -0
- package/modules/hosts/trae/commands/wf-build.md +7 -0
- package/modules/hosts/trae/commands/wf-design.md +6 -0
- package/modules/hosts/trae/commands/wf-new-task.md +2 -0
- package/package.json +1 -1
- package/src/sync-hosts.mjs +100 -62
- package/src/sync-hosts.test.mjs +39 -0
- package/templates/_agents/commands/build.md +7 -0
- package/templates/_agents/commands/design.md +6 -0
- package/templates/_agents/commands/new-task.md +2 -0
- package/templates/_agents/scripts/check-loop.mjs +82 -28
- package/templates/_agents/scripts/check-loop.test.mjs +66 -0
- package/templates/_agents/scripts/check-stage-index.mjs +40 -0
- package/templates/_agents/scripts/check-stage-index.test.mjs +77 -0
- package/templates/_agents/scripts/confirm-doc.mjs +17 -2
- package/templates/_agents/scripts/confirm-doc.test.mjs +17 -0
- package/templates/_agents/scripts/edit-face-check.mjs +107 -0
- package/templates/_agents/scripts/edit-face-check.test.mjs +98 -0
- package/templates/_agents/scripts/fill-intent.mjs +32 -3
- package/templates/_agents/scripts/fill-intent.test.mjs +13 -0
- package/templates/_agents/scripts/gen-workflow-metrics.mjs +52 -1
- package/templates/_agents/scripts/policy.mjs +13 -0
- package/templates/_agents/scripts/verify-wiki-consistency.test.mjs +81 -0
- package/templates/_agents/scripts/wiki-search.test.mjs +58 -0
- package/templates/_agents/scripts-test-exempt.txt +2 -3
- package/templates/_github/workflows/kit-ci.yml +21 -1
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { ENUMS } from './workflow-enums.mjs';
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
8
10
|
|
|
9
11
|
const TODAY = new Date().toISOString().slice(0, 10);
|
|
10
12
|
const LEVELS = ENUMS['level.all']; // 级别词表单源(.agents/workflow-enums.txt)
|
|
@@ -36,12 +38,24 @@ const SECTIONS = [
|
|
|
36
38
|
{ title: '验收标准(可测试)', hint: '- [ ] <逐条可测试;写不出可测试判据 = 还没想清楚>\n\n> **闭环对账**:关单在 test 阶段(不依赖 deploy)。intent 置 done 前逐条勾验,每条补证据——`- [x] <判据>(证据:<commit SHA / 测试用例名 / 冒烟脚本输出>)`。\n> done 状态仍有未勾项会被 check-loop 拦截(2026-09-12 起新建 intent 为 hard-block,存量 intent 仅 warning 提示);勾选但缺「证据:」为 hard-block。' },
|
|
37
39
|
];
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
// renderIntent:骨架渲染(纯函数)。kbHits(2026-10-09 engine-quality-round3 W2):kb-search 命中行
|
|
42
|
+
// 数组(📄 行原文)——非空时替换「历史教训/防复发」节的检索占位行为实际命中列表(历史坑强制注入,
|
|
43
|
+
// 手动路由与 pipeline-run 工单同权);空/缺省保留占位(fail-open 口径)。子进程调用在 main,本函数保持无 IO。
|
|
44
|
+
export function renderIntent({ topic, module, level, notes, date = TODAY, kbHits = [], kbFailNote = '' }) {
|
|
40
45
|
if (!LEVELS.includes(level)) throw new Error('level 不在枚举 ' + LEVELS.join('|') + ':' + level);
|
|
41
46
|
const fm = '---\n状态: draft\n级别: ' + level + '\nrisk_level: ' + level + '\n日期: ' + date + '\n模块: ' + module + '\n备注: ' + (notes || '<可选:附注自由文本,check-loop 不解析>') + '\n---\n';
|
|
42
47
|
let body = '# INTENT — ' + topic + '\n\n<!-- 复制本模板为 YYYY-MM-DD-<主题>.md 后填写;plans/ 下同名文件与本文件配对 -->\n<!-- frontmatter 受限子集(2026-09-13):每行 键: 值;状态∈draft/approved/done/superseded/cancelled(严格枚举,附注写备注键);级别∈L0/L1/L2/L3;risk_level=级别(混合治理风险泳道选道:L0/L1 协作道异步审计 / L2/L3 防御道同步确认);check-loop 只扫 frontmatter 取机器字段,正文不再写状态/级别行 -->\n\n';
|
|
43
48
|
for (const { title, hint } of SECTIONS) {
|
|
44
|
-
|
|
49
|
+
let section = hint;
|
|
50
|
+
if (title === '历史教训/防复发') {
|
|
51
|
+
const placeholder = '- 检索结果:`node .agents/scripts/kb-search.mjs "<关键词>" --type incidents` 命中结果';
|
|
52
|
+
if (kbHits.length) {
|
|
53
|
+
const injected = '- 检索结果(自动注入 ' + kbHits.length + ' 条,同域历史——起草前先读其根因与防复发):\n' + kbHits.map((h) => ' - ' + h).join('\n');
|
|
54
|
+
if (section.includes(placeholder)) section = section.replace(placeholder, injected);
|
|
55
|
+
}
|
|
56
|
+
else if (kbFailNote) section = section.replace('- 检索结果:', '- 检索结果' + kbFailNote + '\n- 检索结果:'); // 复核 P2-2:不可用时留手工命令注释行
|
|
57
|
+
}
|
|
58
|
+
body += '## ' + title + '\n\n' + section + '\n\n';
|
|
45
59
|
}
|
|
46
60
|
body += '## 确认与复核\n\n- 确认日期:\n- 确认人:用户(对话内明确放行即确认)\n- 确认范围:\n- 复核:L1 不要求独立复核\n';
|
|
47
61
|
return { frontmatter: fm, body: fm + body, sections: SECTIONS.map((s) => s.title) };
|
|
@@ -52,7 +66,22 @@ if (isMain) {
|
|
|
52
66
|
const a = parseArgs(process.argv.slice(2));
|
|
53
67
|
if (!a.topic || !a.module || !a.output) fail('必填:--topic / --module / --output;选填:--level(默认 L1)/ --notes');
|
|
54
68
|
if (!LEVELS.includes(a.level)) fail('level 必须在 ' + LEVELS.join('|') + ':' + a.level);
|
|
55
|
-
|
|
69
|
+
// 历史坑强制注入(2026-10-09 engine-quality-round3 W2):同目录 kb-search 子进程检索同域历史
|
|
70
|
+
// (关键词 = topic ascii 段 + 模块名;--type incidents,plans——修复先例与计划同查)。fail-open:
|
|
71
|
+
// 非零/零命中/3s 超时 → kbHits 空 → 保留占位(与手动路由「AI 自觉检索」不同,此处机器保证尝试)。
|
|
72
|
+
let kbFailNote = '';
|
|
73
|
+
let kbHits = [];
|
|
74
|
+
try {
|
|
75
|
+
const words = (String(a.topic).toLowerCase().match(/[a-z0-9]{2,}/g) || []).slice(0, 2);
|
|
76
|
+
const kw = [...new Set([...words, String(a.module || '')].filter(Boolean))]; // 复核 P1-1:模块名入关键词(spec S2 口径)
|
|
77
|
+
const r = spawnSync(process.execPath, [path.join(path.dirname(fileURLToPath(import.meta.url)), 'kb-search.mjs'),
|
|
78
|
+
...kw, '--scope', 'workflow', '--type', 'incidents,plans', '-n', '4'], { encoding: 'utf8', timeout: 3000, windowsHide: true });
|
|
79
|
+
if (r.status === 0 && r.stdout) {
|
|
80
|
+
kbHits = r.stdout.split(/\r?\n/).filter((l) => l.startsWith('📄')).slice(0, 4);
|
|
81
|
+
if (!kbHits.length) kbFailNote = '(自动检索不可用或零命中,手工跑:node .agents/scripts/kb-search.mjs "<关键词>" --type incidents)';
|
|
82
|
+
}
|
|
83
|
+
} catch { /* fail-open:检索不可用保留占位 */ }
|
|
84
|
+
const { body } = renderIntent({ ...a, kbHits, kbFailNote });
|
|
56
85
|
fs.mkdirSync(path.dirname(a.output), { recursive: true });
|
|
57
86
|
fs.writeFileSync(a.output, body);
|
|
58
87
|
console.log('✅ 已生成 intent 草稿:' + a.output + '(状态 draft / 级别 ' + a.level + ' / 模块 ' + a.module + ')');
|
|
@@ -78,5 +78,18 @@ function check(name, cond, detail = '') {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
|
|
82
|
+
// ---- 历史坑注入 kbHits(2026-10-09 engine-quality-round3 W2)----
|
|
83
|
+
{
|
|
84
|
+
const hits = ["📄 workflow/incidents/2026-10-01-aaa.md — closed · L2 · pipeline · 3 处命中 / 1KB"];
|
|
85
|
+
const { body } = renderIntent({ topic: 't', module: 'pipeline', level: 'L2', kbHits: hits });
|
|
86
|
+
check('kb 注入:命中行替换占位(含自动注入计数与 📄 原文)',
|
|
87
|
+
body.includes('检索结果(自动注入 1 条') && body.includes("📄 workflow/incidents/2026-10-01-aaa.md — closed · L2 · pipeline · 3 处命中 / 1KB") && !body.includes("- 检索结果:`node .agents/scripts/kb-search.mjs \"<关键词>\" --type incidents` 命中结果"), body.slice(0, 200));
|
|
88
|
+
}
|
|
89
|
+
{
|
|
90
|
+
const { body } = renderIntent({ topic: 't', module: 'pipeline', level: 'L2', kbHits: [] });
|
|
91
|
+
check('kb 零命中:保留占位(fail-open 口径)', body.includes("- 检索结果:`node .agents/scripts/kb-search.mjs \"<关键词>\" --type incidents` 命中结果"), body.slice(0, 200));
|
|
92
|
+
}
|
|
93
|
+
|
|
81
94
|
console.log('\n合计: PASS ' + pass + ' / FAIL ' + failCount);
|
|
82
95
|
process.exit(failCount ? 1 : 0);
|
|
@@ -280,5 +280,56 @@ if (existing.includes(BEGIN) && existing.includes(END)) {
|
|
|
280
280
|
} else {
|
|
281
281
|
out = (existing ? existing.replace(/\s*$/, '') + '\n\n' : HEADER) + inner + '\n';
|
|
282
282
|
}
|
|
283
|
-
|
|
283
|
+
|
|
284
|
+
// ---- 趋势序列 + 确认负担(2026-10-09 engine-quality-round3 W4)----
|
|
285
|
+
// ① 确认负担:本月 confirmations.jsonl 行数(stage=void 除外)与上月比(口径:每次 confirm-doc 调用
|
|
286
|
+
// 落一行——「确认门调用次数」即确认负担的机器事实;void 为作废标记不计)。
|
|
287
|
+
// ② 趋势序列:.agents/cache/metrics-history.jsonl 每日一行幂等(同日重跑覆盖)——metrics.md「趋势」
|
|
288
|
+
// 行对比昨日 docsTotal/confirmCalls;history 损坏重建(fail-open)。
|
|
289
|
+
const HISTORY_P = path.join('.agents', 'cache', 'metrics-history.jsonl');
|
|
290
|
+
const voidExcluded = ledgerRows.filter((e) => e.stage !== 'void');
|
|
291
|
+
const monthOf = (e) => (typeof e.ts === 'string' ? e.ts.slice(0, 7) : '');
|
|
292
|
+
const thisMonth = month.slice(0, 7); // month 变量 = 生成锚月(YYYY-MM)
|
|
293
|
+
const prevMonthDate = new Date(thisMonth + '-01T00:00:00Z');
|
|
294
|
+
prevMonthDate.setUTCMonth(prevMonthDate.getUTCMonth() - 1);
|
|
295
|
+
const prevMonth = prevMonthDate.toISOString().slice(0, 7);
|
|
296
|
+
const confirmThis = voidExcluded.filter((e) => monthOf(e) === thisMonth).length;
|
|
297
|
+
const confirmPrev = voidExcluded.filter((e) => monthOf(e) === prevMonth).length;
|
|
298
|
+
const confirmDelta = confirmPrev > 0 ? Math.round(((confirmThis - confirmPrev) / confirmPrev) * 100) : null;
|
|
299
|
+
|
|
300
|
+
// docsTotal:metrics 主表本月行的文档数列(第一数值列)——从 row 提取,避免重复统计逻辑
|
|
301
|
+
const docsTotal = docs.files; // 直取作用域变量(正则提取曾因「287(活跃…)」复合列失配——round3 实测修正)
|
|
302
|
+
|
|
303
|
+
// history 读写(fail-open:损坏重建)
|
|
304
|
+
let history = [];
|
|
305
|
+
try {
|
|
306
|
+
if (fs.existsSync(HISTORY_P)) {
|
|
307
|
+
history = fs.readFileSync(HISTORY_P, 'utf8').split(/\r?\n/).filter(Boolean)
|
|
308
|
+
.map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
309
|
+
}
|
|
310
|
+
} catch { history = []; }
|
|
311
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
312
|
+
history = history.filter((h) => h && h.date !== today); // 同日幂等:覆盖当日行
|
|
313
|
+
history.push({ date: today, docsTotal, passRate: funnel.closed ? Math.round((funnel.once / funnel.closed) * 100) + '%' : '', confirmCalls: confirmThis, savedAt: new Date().toISOString() });
|
|
314
|
+
try {
|
|
315
|
+
fs.mkdirSync(path.dirname(HISTORY_P), { recursive: true });
|
|
316
|
+
const tmpH = HISTORY_P + '.tmp';
|
|
317
|
+
fs.writeFileSync(tmpH, history.map((h) => JSON.stringify(h)).join('\n') + '\n');
|
|
318
|
+
fs.renameSync(tmpH, HISTORY_P);
|
|
319
|
+
} catch { /* history 写失败不影响 metrics.md 主产物 */ }
|
|
320
|
+
|
|
321
|
+
// 趋势行(对比昨日快照)
|
|
322
|
+
const yesterday = history.filter((h) => h.date !== today).slice(-1)[0];
|
|
323
|
+
const trendParts = [];
|
|
324
|
+
if (yesterday) {
|
|
325
|
+
trendParts.push('docs ' + docsTotal + '(昨日 ' + (yesterday.docsTotal ?? '?') + ')');
|
|
326
|
+
trendParts.push('确认调用 ' + confirmThis + '(昨日 ' + (yesterday.confirmCalls ?? '?') + ')');
|
|
327
|
+
} else {
|
|
328
|
+
trendParts.push('无前值(history 首日)');
|
|
329
|
+
}
|
|
330
|
+
const trendLine = '> 趋势(环比昨日):' + trendParts.join(';') + ';确认调用本月 ' + confirmThis + ' 次(上月 ' + confirmPrev + ' 次' + (confirmDelta === null ? '' : ',' + (confirmDelta >= 0 ? '+' : '') + confirmDelta + '%') + ';void 不计)';
|
|
331
|
+
|
|
332
|
+
// 注入「闭环漏斗」节尾(inner 的 FUNNEL 段之后、END 之前)
|
|
333
|
+
|
|
334
|
+
fs.writeFileSync(METRICS_P, out.replace(END, trendLine + '\n\n' + END), 'utf8');
|
|
284
335
|
console.log(`✅ 已更新 workflow/metrics.md(${month} 行;共 ${rows.length} 个月)`);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// kit.json 缺 policyVersion、或版本不在表内:回退版本 1(不启用版本 2 的 approved 锚)。
|
|
4
4
|
// audit 不在本表:缺省视为全量检查(已装仓库与无 kit 的测试夹具);init 新装显式写 false。
|
|
5
5
|
import fs from 'node:fs';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
6
7
|
import path from 'node:path';
|
|
7
8
|
|
|
8
9
|
export const POLICIES = {
|
|
@@ -142,3 +143,15 @@ export function hasFreshVerifyLine(lines, now = Date.now(), doc = null) {
|
|
|
142
143
|
}
|
|
143
144
|
return false;
|
|
144
145
|
}
|
|
146
|
+
|
|
147
|
+
// ---- 台账哈希链(2026-10-09 engine-quality-round2 D)----
|
|
148
|
+
// 单源(confirm-doc appendLedger 写入与 check-loop 检查 15 验链共用——判定零复刻):
|
|
149
|
+
// ledgerChainHash(row, prevHash) = sha256(prevHash + JSON.stringify(row))。
|
|
150
|
+
// row = 台账行**去掉 prevHash/hash 两字段**后的对象(键序 = 写入序,JSON.parse 保序 → 重算一致);
|
|
151
|
+
// prevHash = 上一行的 hash(上一行无 hash/文件首行 → ''——历史行零回填,链从首个带 hash 行起算)。
|
|
152
|
+
// 边界(诚实声明):整文件重写可重建完整链,不可机器防——兜底 = 台账文件自身的 git 历史
|
|
153
|
+
// (check-ledger-invariant 已扫 append-only 历史);本链堵的是「就地改/删中间行」这一高频伪造面,
|
|
154
|
+
// 把主动伪造从静默可行抬到必留断链痕迹。
|
|
155
|
+
export function ledgerChainHash(row, prevHash) {
|
|
156
|
+
return createHash('sha256').update((prevHash || '') + JSON.stringify(row)).digest('hex');
|
|
157
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// verify-wiki-consistency.test.mjs — wiki 三方一致性验证(2026-10-09 engine-quality-round2 A2 补测)
|
|
2
|
+
// 此前 exempt 表登记「无覆盖」——本套件补齐后移出豁免。三场景:三方一致(绿)/ 磁盘多文件未登记(红)/
|
|
3
|
+
// drafts-archive 协议目录缺失(红)。脚本自足(仅 node 标准库 + cwd 相对读 wiki/),fixture 子进程跑真脚本。
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const SRC = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
let pass = 0, fail = 0;
|
|
12
|
+
const check = (name, cond, detail = '') => {
|
|
13
|
+
if (cond) { pass++; console.log(`PASS ${name}`); }
|
|
14
|
+
else { fail++; console.log(`FAIL ${name}${detail ? `——${detail}` : ''}`); }
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function mkWiki({ extraDiskFile = false, noArchive = false } = {}) {
|
|
18
|
+
const fx = fs.mkdtempSync(path.join(os.tmpdir(), 'vwc-'));
|
|
19
|
+
fs.mkdirSync(path.join(fx, 'wiki', '主题A'), { recursive: true });
|
|
20
|
+
fs.writeFileSync(path.join(fx, 'wiki', '主题A', '文档1.md'), '# 文档1\n\n无外链。\n');
|
|
21
|
+
if (extraDiskFile) fs.writeFileSync(path.join(fx, 'wiki', '主题A', '文档2.md'), '# 文档2\n');
|
|
22
|
+
if (!noArchive) fs.mkdirSync(path.join(fx, 'wiki', 'drafts-archive'), { recursive: true });
|
|
23
|
+
fs.writeFileSync(path.join(fx, 'wiki', 'INDEX.md'), [
|
|
24
|
+
'# Wiki 索引',
|
|
25
|
+
'',
|
|
26
|
+
'## 主题目录速览',
|
|
27
|
+
'',
|
|
28
|
+
'| 主题 | 文件数 | 用途 |',
|
|
29
|
+
'|---|---|---|',
|
|
30
|
+
'| 主题A | 1 | 测试主题 |',
|
|
31
|
+
'',
|
|
32
|
+
'## 映射表',
|
|
33
|
+
'',
|
|
34
|
+
'| 文件 | 用途 | 路径 |',
|
|
35
|
+
'|---|---|---|',
|
|
36
|
+
'| 文档1.md | 测试 | wiki/主题A/文档1.md |',
|
|
37
|
+
'',
|
|
38
|
+
`合计:**1 份**知识文档,**1 个主题**`,
|
|
39
|
+
'',
|
|
40
|
+
].join('\n'));
|
|
41
|
+
const DATA = {
|
|
42
|
+
topics: [{ name: '主题A', files: [{ file: '文档1.md', dir: '主题A' }] }],
|
|
43
|
+
summary: { total: 1, files: 1, topics: 1, archive: 0 },
|
|
44
|
+
};
|
|
45
|
+
fs.writeFileSync(path.join(fx, 'wiki', '知识沉淀总览.html'),
|
|
46
|
+
'<!doctype html><script>\nconst DATA = ' + JSON.stringify(DATA, null, 2) + ';\n</script>\n');
|
|
47
|
+
return fx;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const run = (fx) => spawnSync(process.execPath, [path.join(SRC, 'verify-wiki-consistency.mjs')], {
|
|
51
|
+
cwd: fx, encoding: 'utf8',
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
{
|
|
55
|
+
// ① 三方一致 → exit 0
|
|
56
|
+
const fx = mkWiki();
|
|
57
|
+
const r = run(fx);
|
|
58
|
+
check('① 三方一致 → exit 0', r.status === 0, `status=${r.status}\n${(r.stdout || '') + (r.stderr || '')}`.slice(0, 500));
|
|
59
|
+
fs.rmSync(fx, { recursive: true, force: true });
|
|
60
|
+
}
|
|
61
|
+
{
|
|
62
|
+
// ② 磁盘多文件未登记 → exit 1(合计行 fail-loud 最先拦——INDEX 合计 1 份 vs 磁盘 2 份;
|
|
63
|
+
// 该检查在脚本内先于三方文件比对出账,是同问题的合法拦截路径)
|
|
64
|
+
const fx = mkWiki({ extraDiskFile: true });
|
|
65
|
+
const r = run(fx);
|
|
66
|
+
const out = (r.stdout || '') + (r.stderr || '');
|
|
67
|
+
check('② 磁盘多文件未登记 → exit 1 且合计行拦(INDEX 1 份 vs 磁盘 2 份)',
|
|
68
|
+
r.status === 1 && out.includes('INDEX 合计行与磁盘不一致'), `status=${r.status}\n${out.slice(0, 500)}`);
|
|
69
|
+
fs.rmSync(fx, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
{
|
|
72
|
+
// ③ drafts-archive 协议目录缺失 → exit 1(门禁明示失败而非崩栈,2026-09-24 口径)
|
|
73
|
+
const fx = mkWiki({ noArchive: true });
|
|
74
|
+
const r = run(fx);
|
|
75
|
+
const out = (r.stdout || '') + (r.stderr || '');
|
|
76
|
+
check('③ drafts-archive 缺失 → exit 1 且明示', r.status === 1 && out.includes('drafts-archive'), `status=${r.status}\n${out.slice(0, 400)}`);
|
|
77
|
+
fs.rmSync(fx, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
81
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// wiki-search.test.mjs — wiki 检索转发壳(2026-10-09 engine-quality-round2 A2 补测)
|
|
2
|
+
// 壳语义三断言:①默认 -n 20 + --scope wiki 追加(固定语料=wiki 活跃层);②用户显式 -n 不被覆盖;
|
|
3
|
+
// ③exit 码透传(kb-search 非零 → 壳非零)。fixture 复制壳 + kb-search 及其依赖(workflow-enums/
|
|
4
|
+
// 配置文件),wiki 语料一文件命中(kb-search.test 场景 1 已覆盖 kb-search 本体判定,此处只测壳)。
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
const SRC = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
let pass = 0, fail = 0;
|
|
13
|
+
const check = (name, cond, detail = '') => {
|
|
14
|
+
if (cond) { pass++; console.log(`PASS ${name}`); }
|
|
15
|
+
else { fail++; console.log(`FAIL ${name}${detail ? `——${detail}` : ''}`); }
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const COPIED = ['wiki-search.mjs', 'kb-search.mjs', 'workflow-enums.mjs'];
|
|
19
|
+
function mkFixture() {
|
|
20
|
+
const fx = fs.mkdtempSync(path.join(os.tmpdir(), 'ws-'));
|
|
21
|
+
fs.mkdirSync(path.join(fx, '.agents', 'scripts'), { recursive: true });
|
|
22
|
+
for (const f of COPIED) fs.copyFileSync(path.join(SRC, f), path.join(fx, '.agents', 'scripts', f));
|
|
23
|
+
for (const f of ['workflow-enums.txt', 'workflow-modules.txt'])
|
|
24
|
+
fs.copyFileSync(path.join(SRC, '..', f), path.join(fx, '.agents', f));
|
|
25
|
+
fs.mkdirSync(path.join(fx, 'wiki', '测试主题'), { recursive: true });
|
|
26
|
+
fs.writeFileSync(path.join(fx, 'wiki', '测试主题', '说明.md'), '# 说明\n\n编码模板的领域知识条目。\n');
|
|
27
|
+
fs.mkdirSync(path.join(fx, 'workflow', 'intents'), { recursive: true });
|
|
28
|
+
fs.writeFileSync(path.join(fx, 'workflow', 'intents', '2026-10-09-a.md'),
|
|
29
|
+
'---\n状态: approved\n级别: L1\n日期: 2026-10-09\n模块: pipeline\n---\n# INTENT\n\n编码模板的 workflow 侧条目。\n');
|
|
30
|
+
return fx;
|
|
31
|
+
}
|
|
32
|
+
const run = (fx, args) => spawnSync(process.execPath, [path.join(fx, '.agents', 'scripts', 'wiki-search.mjs'), ...args], {
|
|
33
|
+
cwd: fx, encoding: 'utf8',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
{
|
|
37
|
+
const fx = mkFixture();
|
|
38
|
+
const r = run(fx, ['编码模板']);
|
|
39
|
+
const out = (r.stdout || '') + (r.stderr || '');
|
|
40
|
+
check('① 默认形态命中 wiki 语料且不含 workflow 语料(--scope wiki 固定)',
|
|
41
|
+
r.status === 0 && out.includes('wiki/测试主题/说明.md') && !out.includes('workflow/intents'), out.slice(0, 400));
|
|
42
|
+
fs.rmSync(fx, { recursive: true, force: true });
|
|
43
|
+
}
|
|
44
|
+
{
|
|
45
|
+
const fx = mkFixture();
|
|
46
|
+
const r = run(fx, ['-n', '0', '编码模板']);
|
|
47
|
+
check('② 显式 -n 透传(0=不限,不因壳默认覆盖报错)且命中', r.status === 0 && (r.stdout || '').includes('说明.md'), (r.stdout || '').slice(0, 300));
|
|
48
|
+
fs.rmSync(fx, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
{
|
|
51
|
+
const fx = mkFixture();
|
|
52
|
+
const r = run(fx, []); // 无词 → kb-search 用法错误 exit 1
|
|
53
|
+
check('③ exit 码透传(无词 → 壳非零)', (r.status ?? 1) !== 0, `status=${r.status}`);
|
|
54
|
+
fs.rmSync(fx, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
58
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
# 格式:<脚本名> | <豁免理由> | <覆盖证据(套件名;无自动化覆盖如实写「无」)>
|
|
3
3
|
# 语义:登记 ≠ 免责——须写明理由与覆盖证据;无覆盖的登记是「工具/启动器豁免」,后续批次评估补测。
|
|
4
4
|
# 新增引擎脚本默认必须带同名 .test.mjs(README 规范条目);确属库件/工具才登记本文件。
|
|
5
|
+
# 2026-10-09 A2 评估:verify-wiki-consistency 已移出(补测 3 场景);wiki-search 已移出(转发壳,判定单源 kb-search.test 覆盖)。
|
|
5
6
|
check-metric-claims.mjs | 经 check-loop.mjs 消费 | 由 check-loop.test.mjs 间接覆盖其调用路径
|
|
6
|
-
ensure-board.mjs |
|
|
7
|
+
ensure-board.mjs | 看板启动器 | 永久工具豁免(需真实端口/进程环境,自动化覆盖无性价比——2026-10-09 engine-quality-round2 A2 评估定性)
|
|
7
8
|
policy.mjs | 库件 | 被 check-loop.test.mjs 直接 import 覆盖
|
|
8
9
|
stage-gates.mjs | 库件 | 被 check-loop.test.mjs 直接 import 覆盖(fill-*/confirm-doc.test 消费其行为)
|
|
9
|
-
verify-wiki-consistency.mjs | wiki 一致性校验工具 | 无(工具豁免,后续批次评估补测)
|
|
10
|
-
wiki-search.mjs | 检索库件 | 无(工具豁免,后续批次评估补测)
|
|
11
10
|
# 2026-10-08 engine-quality-batch:pipeline-run 三层拆分的库件层——纯函数/存储层由 pipeline-run.test.mjs 全量覆盖(断言零改动全过),单建同名测试属形式主义(plan 偏离留痕 ①)
|
|
12
11
|
pipeline-run-docs.mjs | 库件(文档纯函数层,2026-10-08 engine-quality-batch 拆分) | 由 pipeline-run.test.mjs 全量覆盖(断言零改动 29 组全过)
|
|
13
12
|
pipeline-run-run.mjs | 库件(run 存储层,同上拆分) | 由 pipeline-run.test.mjs 全量覆盖(含 PIPELINE_RUN_* 注入契约场景)
|
|
@@ -23,6 +23,26 @@ jobs:
|
|
|
23
23
|
node-version: 22 # 按项目 engines 调整(kit 引擎要求 >=18)
|
|
24
24
|
- name: 安装依赖(有根 package.json 才跑;私有源在此加 registry 配置)
|
|
25
25
|
if: hashFiles('package.json') != ''
|
|
26
|
-
run: npm ci || npm install
|
|
26
|
+
run: npm ci || npm install || npm install --registry=https://registry.npmjs.org # 末级官方源兜底(2026-10-09 engine-quality-round3 W3:镜像故障不阻塞 CI)
|
|
27
27
|
- name: 远端门(测试 + 闭环校验)
|
|
28
28
|
run: node .agents/scripts/verify.mjs
|
|
29
|
+
- name: CI 红回溯点名(2026-10-09 engine-quality-round2 B3:红时列出最近关单,复核优先)
|
|
30
|
+
if: failure()
|
|
31
|
+
run: |
|
|
32
|
+
node -e "
|
|
33
|
+
try {
|
|
34
|
+
const fs = require('fs');
|
|
35
|
+
const p = '.agents/confirmations.jsonl';
|
|
36
|
+
if (!fs.existsSync(p)) process.exit(0);
|
|
37
|
+
const cut = Date.now() - 7 * 864e5;
|
|
38
|
+
const rows = fs.readFileSync(p, 'utf8').split(/\r?\n/).filter(Boolean)
|
|
39
|
+
.map((l) => { try { return JSON.parse(l); } catch { return null; } })
|
|
40
|
+
.filter((e) => e && e.ts && ['done', 'closed'].includes(e.stage) && Date.parse(e.ts) >= cut)
|
|
41
|
+
.map((e) => e.doc);
|
|
42
|
+
const uniq = [...new Set(rows)];
|
|
43
|
+
if (uniq.length) {
|
|
44
|
+
console.log('⚠️ CI 红——最近 7 天关单文档(优先复核是否本批引入):');
|
|
45
|
+
for (const d of uniq) console.log(' - ' + d);
|
|
46
|
+
} else console.log('最近 7 天无关单记录');
|
|
47
|
+
} catch { /* 回溯是辅助面,读不到不添乱 */ }
|
|
48
|
+
"
|