agentic-flow-kit 1.4.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.
Files changed (140) hide show
  1. package/modules/hosts/claude/agents/implementer.md +5 -6
  2. package/modules/hosts/claude/agents/independent-reviewer.md +6 -6
  3. package/modules/hosts/claude/agents/ui-verifier.md +5 -5
  4. package/modules/hosts/claude/commands/wf-build.md +28 -21
  5. package/modules/hosts/claude/commands/wf-deploy.md +2 -2
  6. package/modules/hosts/claude/commands/wf-design.md +14 -8
  7. package/modules/hosts/claude/commands/wf-gate-checklist.md +8 -8
  8. package/modules/hosts/claude/commands/wf-maintain.md +12 -12
  9. package/modules/hosts/claude/commands/wf-new-task.md +11 -9
  10. package/modules/hosts/claude/commands/wf-pipeline-run.md +3 -3
  11. package/modules/hosts/claude/commands/wf-plan.md +10 -11
  12. package/modules/hosts/claude/commands/wf-review.md +4 -4
  13. package/modules/hosts/claude/commands/wf-source-sync-check.md +10 -10
  14. package/modules/hosts/claude/commands/wf-sync-hosts.md +9 -9
  15. package/modules/hosts/claude/commands/wf-test.md +5 -6
  16. package/modules/hosts/codex/agents/implementer.md +5 -6
  17. package/modules/hosts/codex/agents/independent-reviewer.md +6 -6
  18. package/modules/hosts/codex/agents/ui-verifier.md +5 -5
  19. package/modules/hosts/codex/commands/wf-build.md +28 -21
  20. package/modules/hosts/codex/commands/wf-deploy.md +2 -2
  21. package/modules/hosts/codex/commands/wf-design.md +14 -8
  22. package/modules/hosts/codex/commands/wf-gate-checklist.md +8 -8
  23. package/modules/hosts/codex/commands/wf-maintain.md +12 -12
  24. package/modules/hosts/codex/commands/wf-new-task.md +11 -9
  25. package/modules/hosts/codex/commands/wf-pipeline-run.md +3 -3
  26. package/modules/hosts/codex/commands/wf-plan.md +10 -11
  27. package/modules/hosts/codex/commands/wf-review.md +4 -4
  28. package/modules/hosts/codex/commands/wf-source-sync-check.md +10 -10
  29. package/modules/hosts/codex/commands/wf-sync-hosts.md +9 -9
  30. package/modules/hosts/codex/commands/wf-test.md +5 -6
  31. package/modules/hosts/cursor/agents/implementer.md +5 -6
  32. package/modules/hosts/cursor/agents/independent-reviewer.md +6 -6
  33. package/modules/hosts/cursor/agents/ui-verifier.md +5 -5
  34. package/modules/hosts/cursor/commands/wf-build.md +28 -21
  35. package/modules/hosts/cursor/commands/wf-deploy.md +2 -2
  36. package/modules/hosts/cursor/commands/wf-design.md +14 -8
  37. package/modules/hosts/cursor/commands/wf-gate-checklist.md +8 -8
  38. package/modules/hosts/cursor/commands/wf-maintain.md +12 -12
  39. package/modules/hosts/cursor/commands/wf-new-task.md +11 -9
  40. package/modules/hosts/cursor/commands/wf-pipeline-run.md +3 -3
  41. package/modules/hosts/cursor/commands/wf-plan.md +10 -11
  42. package/modules/hosts/cursor/commands/wf-review.md +4 -4
  43. package/modules/hosts/cursor/commands/wf-source-sync-check.md +10 -10
  44. package/modules/hosts/cursor/commands/wf-sync-hosts.md +9 -9
  45. package/modules/hosts/cursor/commands/wf-test.md +5 -6
  46. package/modules/hosts/omp/agents/implementer.md +5 -6
  47. package/modules/hosts/omp/agents/independent-reviewer.md +6 -6
  48. package/modules/hosts/omp/agents/ui-verifier.md +5 -5
  49. package/modules/hosts/opencode/agents/implementer.md +5 -6
  50. package/modules/hosts/opencode/agents/independent-reviewer.md +6 -6
  51. package/modules/hosts/opencode/agents/ui-verifier.md +5 -5
  52. package/modules/hosts/opencode/commands/wf-build.md +28 -21
  53. package/modules/hosts/opencode/commands/wf-deploy.md +2 -2
  54. package/modules/hosts/opencode/commands/wf-design.md +14 -8
  55. package/modules/hosts/opencode/commands/wf-gate-checklist.md +8 -8
  56. package/modules/hosts/opencode/commands/wf-maintain.md +12 -12
  57. package/modules/hosts/opencode/commands/wf-new-task.md +11 -9
  58. package/modules/hosts/opencode/commands/wf-pipeline-run.md +3 -3
  59. package/modules/hosts/opencode/commands/wf-plan.md +10 -11
  60. package/modules/hosts/opencode/commands/wf-review.md +4 -4
  61. package/modules/hosts/opencode/commands/wf-source-sync-check.md +10 -10
  62. package/modules/hosts/opencode/commands/wf-sync-hosts.md +9 -9
  63. package/modules/hosts/opencode/commands/wf-test.md +5 -6
  64. package/modules/hosts/trae/agents/implementer.md +5 -6
  65. package/modules/hosts/trae/agents/independent-reviewer.md +6 -6
  66. package/modules/hosts/trae/agents/ui-verifier.md +5 -5
  67. package/modules/hosts/trae/commands/wf-build.md +28 -21
  68. package/modules/hosts/trae/commands/wf-deploy.md +2 -2
  69. package/modules/hosts/trae/commands/wf-design.md +14 -8
  70. package/modules/hosts/trae/commands/wf-gate-checklist.md +8 -8
  71. package/modules/hosts/trae/commands/wf-maintain.md +12 -12
  72. package/modules/hosts/trae/commands/wf-new-task.md +11 -9
  73. package/modules/hosts/trae/commands/wf-pipeline-run.md +3 -3
  74. package/modules/hosts/trae/commands/wf-plan.md +10 -11
  75. package/modules/hosts/trae/commands/wf-review.md +4 -4
  76. package/modules/hosts/trae/commands/wf-source-sync-check.md +10 -10
  77. package/modules/hosts/trae/commands/wf-sync-hosts.md +9 -9
  78. package/modules/hosts/trae/commands/wf-test.md +5 -6
  79. package/modules/hosts/zcode/agents/implementer.md +5 -6
  80. package/modules/hosts/zcode/agents/independent-reviewer.md +6 -6
  81. package/modules/hosts/zcode/agents/ui-verifier.md +5 -5
  82. package/package.json +7 -2
  83. package/src/doctor.mjs +1 -1
  84. package/src/init.test.mjs +1 -1
  85. package/src/run-tests.mjs +15 -1
  86. package/src/sync-hosts.mjs +100 -62
  87. package/src/sync-hosts.test.mjs +43 -4
  88. package/src/sync.test.mjs +1 -1
  89. package/templates/_agents/commands/build.md +28 -21
  90. package/templates/_agents/commands/deploy.md +2 -2
  91. package/templates/_agents/commands/design.md +14 -8
  92. package/templates/_agents/commands/gate-checklist.md +8 -8
  93. package/templates/_agents/commands/maintain.md +12 -12
  94. package/templates/_agents/commands/new-task.md +11 -9
  95. package/templates/_agents/commands/pipeline-run.md +3 -3
  96. package/templates/_agents/commands/plan.md +10 -11
  97. package/templates/_agents/commands/review.md +4 -4
  98. package/templates/_agents/commands/source-sync-check.md +10 -10
  99. package/templates/_agents/commands/sync-hosts.md +9 -9
  100. package/templates/_agents/commands/test.md +5 -6
  101. package/templates/_agents/roles/implementer.md +5 -6
  102. package/templates/_agents/roles/independent-reviewer.md +6 -6
  103. package/templates/_agents/roles/ui-verifier.md +5 -5
  104. package/templates/_agents/scripts/agg-delegations.cjs +0 -1
  105. package/templates/_agents/scripts/agg-delegations.test.mjs +2 -2
  106. package/templates/_agents/scripts/check-hygiene.mjs +2 -2
  107. package/templates/_agents/scripts/check-ledger-invariant.mjs +0 -1
  108. package/templates/_agents/scripts/check-loop.mjs +115 -38
  109. package/templates/_agents/scripts/check-loop.test.mjs +67 -1
  110. package/templates/_agents/scripts/check-stage-index.mjs +40 -0
  111. package/templates/_agents/scripts/check-stage-index.test.mjs +77 -0
  112. package/templates/_agents/scripts/commit-check-trigger.test.mjs +0 -1
  113. package/templates/_agents/scripts/confirm-doc.mjs +17 -3
  114. package/templates/_agents/scripts/confirm-doc.test.mjs +17 -0
  115. package/templates/_agents/scripts/edit-face-check.mjs +107 -0
  116. package/templates/_agents/scripts/edit-face-check.test.mjs +98 -0
  117. package/templates/_agents/scripts/ensure-board.mjs +2 -2
  118. package/templates/_agents/scripts/fill-intent.mjs +34 -5
  119. package/templates/_agents/scripts/fill-intent.test.mjs +13 -0
  120. package/templates/_agents/scripts/fill-plan.mjs +1 -2
  121. package/templates/_agents/scripts/gen-wiki-board.mjs +1 -1
  122. package/templates/_agents/scripts/gen-workflow-dashboard.test.mjs +0 -2
  123. package/templates/_agents/scripts/gen-workflow-index.test.mjs +1 -1
  124. package/templates/_agents/scripts/gen-workflow-metrics.mjs +53 -2
  125. package/templates/_agents/scripts/kb-search.mjs +1 -2
  126. package/templates/_agents/scripts/kb-search.test.mjs +1 -1
  127. package/templates/_agents/scripts/pipeline-run-docs.mjs +76 -0
  128. package/templates/_agents/scripts/pipeline-run-run.mjs +63 -0
  129. package/templates/_agents/scripts/pipeline-run.mjs +8 -128
  130. package/templates/_agents/scripts/policy.mjs +20 -1
  131. package/templates/_agents/scripts/source-sync-check.mjs +2 -2
  132. package/templates/_agents/scripts/source-sync-check.test.mjs +1 -1
  133. package/templates/_agents/scripts/verify-wiki-consistency.mjs +0 -1
  134. package/templates/_agents/scripts/verify-wiki-consistency.test.mjs +81 -0
  135. package/templates/_agents/scripts/verify.test.mjs +2 -2
  136. package/templates/_agents/scripts/wiki-search.test.mjs +58 -0
  137. package/templates/_agents/scripts/workflow-board-server.test.mjs +0 -1
  138. package/templates/_agents/scripts/workflow-enums.test.mjs +0 -1
  139. package/templates/_agents/scripts-test-exempt.txt +5 -3
  140. package/templates/_github/workflows/kit-ci.yml +21 -1
@@ -0,0 +1,76 @@
1
+ // pipeline-run-docs.mjs — 文档纯函数层(2026-10-08 engine-quality-batch,L2 自 pipeline-run.mjs 拆出)
2
+ // fmGet / placeholdersIn / sectionBody / validateDraftContent / closeoutComplete / summarizeDoc——
3
+ // 零 IO、零 process 依赖、单向被 pipeline-run.mjs(CLI 编排层)消费;判据注释随函数原样迁入。
4
+ // export 面 = 原主文件具名导出(placeholdersIn/validateDraftContent/closeoutComplete)+ 层内消费三函数;
5
+ // 主文件经 re-export 保持对外导出面不变(workflow-board-server 等消费方零改动)。
6
+ export function fmGet(text, key) {
7
+ const m = text.split(/\r?\n/).find((l) => l.startsWith(`${key}:`));
8
+ return m ? m.slice(key.length + 1).trim() : null;
9
+ }
10
+
11
+ // 占位符判定(复核 P1-3 收窄,2026-10-02):先剔除 code span(`…`)与引号串("…")——
12
+ // 正文合法的命令语法占位(如 `next --delegated "<原话>"`)几乎总在 code span 内,
13
+ // 模板实占位符(如 <为什么做;写明需求来源…>)从不包 code span;再匹配剩余裸尖括号。
14
+ export function placeholdersIn(text) {
15
+ const stripped = String(text)
16
+ .replace(/^```[\s\S]*?^```$/gm, '') // 复核 P2-B:跨行 code fence 整块剔除(围栏内命令示例占位不属模板占位符)
17
+ .replace(/`[^`\n]*`/g, '')
18
+ .replace(/"[^"\n]*"/g, '');
19
+ const hits = [];
20
+ for (const re of [/<[^>\n]{1,60}>/g, /YYYY-MM-DD 用户/g]) {
21
+ let m; while ((m = re.exec(stripped))) hits.push(m[0]);
22
+ }
23
+ return [...new Set(hits)].slice(0, 5);
24
+ }
25
+
26
+ export function sectionBody(text, title) {
27
+ const lines = text.split(/\r?\n/);
28
+ const i = lines.findIndex((l) => l.trim() === `## ${title}` || l.trim() === `## ${title}(L1 微改动无实质内容可删本节,不硬填)` || l.trim().startsWith(`## ${title}`));
29
+ if (i < 0) return null;
30
+ const body = [];
31
+ for (let j = i + 1; j < lines.length; j++) {
32
+ if (/^##\s/.test(lines[j])) break;
33
+ body.push(lines[j]);
34
+ }
35
+ return body.join('\n').trim();
36
+ }
37
+
38
+ export function validateDraftContent(text, kind, level) {
39
+ const problems = [];
40
+ if (!text) return ['文件不存在或为空'];
41
+ const state = fmGet(text, '状态');
42
+ if (!state) problems.push('frontmatter 缺「状态:」行');
43
+ if (kind !== 'incident' && !fmGet(text, '级别')) problems.push('frontmatter 缺「级别:」行');
44
+ const ph = placeholdersIn(text);
45
+ if (ph.length) problems.push(`模板占位符残留:${ph.join('、')}`);
46
+ const required = {
47
+ intent: level === 'L1' ? ['背景与问题', '目标', '验收标准'] : ['背景与问题', '历史教训/防复发', '目标', '非目标', '约束', '验收标准'],
48
+ spec: ['功能行为', '数据流', '系统改动', '约束遵守映射'],
49
+ plan: level === 'L1' ? ['改动方案', '约束与风险', '验证计划'] : ['改动方案', '任务拆解', '执行顺序', '验证计划'],
50
+ incident: ['时间线', '根因', '复盘三件套'],
51
+ }[kind] || [];
52
+ for (const sec of required) {
53
+ const b = sectionBody(text, sec);
54
+ if (b === null) { if (!(level === 'L1' && ['非目标', '约束', '任务拆解', '执行顺序'].includes(sec))) problems.push(`缺节:## ${sec}`); }
55
+ else if (!b) problems.push(`节为空:## ${sec}`);
56
+ }
57
+ return problems;
58
+ }
59
+
60
+ export function closeoutComplete(text) {
61
+ const body = sectionBody(text, '验收标准');
62
+ if (body === null) return { ok: false, problems: ['缺「## 验收标准」节'] };
63
+ const lines = body.split(/\r?\n/);
64
+ const open = lines.filter((l) => /^\s*-\s\[\s\]\s/.test(l));
65
+ // 复核 P2-6:证据冒号后须直接跟内容(非空白、非收括号)——「(证据:)」空值不算过
66
+ const checkedNoEv = lines.filter((l) => /^\s*-\s\[x\]\s/.test(l) && !/证据[::][^)\s]/.test(l));
67
+ const problems = [];
68
+ if (open.length) problems.push(`未勾验收 ${open.length} 条`);
69
+ if (checkedNoEv.length) problems.push(`已勾缺证据 ${checkedNoEv.length} 条`);
70
+ return { ok: problems.length === 0, problems };
71
+ }
72
+
73
+ export function summarizeDoc(text) {
74
+ const body = text.split(/\r?\n/).filter((l) => l.trim() && !l.startsWith('---') && !/^状态:|^级别:|^日期:|^模块:|^备注:|^risk_level:/.test(l));
75
+ return body.slice(0, 3).join(' / ').slice(0, 200);
76
+ }
@@ -0,0 +1,63 @@
1
+ // pipeline-run-run.mjs — run 存储层(2026-10-08 engine-quality-batch,L2 自 pipeline-run.mjs 拆出)
2
+ // ctxEnv / moduleList / runFileOf / loadRun / saveRun / emit / latestRunId / newRunId + 时间工具(nowIso/today/pad)——
3
+ // run 文件(.agents/cache/pipeline-runs/<id>.json)原子读写与事件流;run schema 零变化。
4
+ // 测试注入契约原样:PIPELINE_RUN_ROOT / PIPELINE_RUN_BIN / PIPELINE_RUN_GIT_BIN(ctxEnv)与
5
+ // PIPELINE_RUN_TODAY(today——时钟依赖消除锚,spec §测试注入契约)。
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+
9
+ export const nowIso = () => new Date().toISOString();
10
+ export const today = () => process.env.PIPELINE_RUN_TODAY || nowIso().slice(0, 10);
11
+ export const pad = (n) => String(n).padStart(2, '0');
12
+
13
+ // ── 环境解析(测试注入点) ─────────────────────────────────────────────
14
+ export function ctxEnv(overrideRoot) {
15
+ const root = path.resolve(overrideRoot || process.env.PIPELINE_RUN_ROOT || process.cwd());
16
+ const bin = path.resolve(process.env.PIPELINE_RUN_BIN || path.join(root, '.agents', 'scripts'));
17
+ const gitPrefix = (process.env.PIPELINE_RUN_GIT_BIN || 'git').split(/\s+/).filter(Boolean);
18
+ return { root, bin, gitPrefix, runsDir: path.join(root, '.agents', 'cache', 'pipeline-runs') };
19
+ }
20
+
21
+ export function moduleList(ctx) {
22
+ try {
23
+ return fs.readFileSync(path.join(ctx.root, '.agents', 'workflow-modules.txt'), 'utf8')
24
+ .split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.startsWith('#'));
25
+ } catch { return ['backend', 'frontend', 'ui', 'infra', 'integration', 'pipeline', 'wiki']; } // 夹具缺表时的回退默认(warning 由调用方打)
26
+ }
27
+
28
+ // ── run 文件(原子写;事件 append-only) ──────────────────────────────
29
+ export function runFileOf(ctx, runId) { return path.join(ctx.runsDir, `${runId}.json`); }
30
+
31
+ export function loadRun(ctx, runId) {
32
+ const f = runFileOf(ctx, runId);
33
+ if (!fs.existsSync(f)) return null;
34
+ try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return null; }
35
+ }
36
+
37
+ export function saveRun(ctx, run) {
38
+ fs.mkdirSync(ctx.runsDir, { recursive: true });
39
+ run.updatedAt = nowIso();
40
+ const f = runFileOf(ctx, run.runId);
41
+ const tmp = f + '.tmp';
42
+ fs.writeFileSync(tmp, JSON.stringify(run, null, 2) + '\n');
43
+ fs.renameSync(tmp, f);
44
+ }
45
+
46
+ export function emit(run, ev) { run.events.push({ t: nowIso(), ...ev }); }
47
+
48
+ export function latestRunId(ctx) {
49
+ if (!fs.existsSync(ctx.runsDir)) return null;
50
+ const ids = fs.readdirSync(ctx.runsDir).filter((f) => f.endsWith('.json')).map((f) => f.slice(0, -5)).sort();
51
+ for (let i = ids.length - 1; i >= 0; i--) {
52
+ const r = loadRun(ctx, ids[i]);
53
+ if (r && !['done', 'aborted'].includes(r.stopType)) return ids[i];
54
+ }
55
+ return ids[ids.length - 1] || null;
56
+ }
57
+
58
+ export function newRunId(requirement) {
59
+ const d = new Date();
60
+ const ascii = (requirement.toLowerCase().match(/[a-z0-9]{2,}/g) || []).slice(0, 3).join('-') || 'task';
61
+ const rand = Math.random().toString(36).slice(2, 6);
62
+ return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}-${ascii}-${rand}`;
63
+ }
@@ -19,9 +19,7 @@
19
19
  import fs from 'node:fs';
20
20
  import path from 'node:path';
21
21
  import { spawnSync } from 'node:child_process';
22
- import { fileURLToPath } from 'node:url';
23
22
 
24
- const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
25
23
  const KINDS = ['require', 'fix', 'verify', 'review', 'deploy'];
26
24
  const LEVELS = ['L0', 'L1', 'L2', 'L3'];
27
25
  const TERMINAL = ['done', 'superseded', 'cancelled', 'closed'];
@@ -29,61 +27,13 @@ const FIX_CAP = 3;
29
27
  const GIT_NEVER_FLAGS = ['--no-verify', '-n']; // 不变量:任何 git 调用不得携带(防御性断言,正常路径不会拼出)
30
28
 
31
29
  function fail(msg, code = 1) { console.error('❌ ' + msg); process.exit(code); }
32
- const nowIso = () => new Date().toISOString();
33
- const today = () => process.env.PIPELINE_RUN_TODAY || nowIso().slice(0, 10);
34
- const pad = (n) => String(n).padStart(2, '0');
35
-
36
- // ── 环境解析(测试注入点) ─────────────────────────────────────────────
37
- function ctxEnv(overrideRoot) {
38
- const root = path.resolve(overrideRoot || process.env.PIPELINE_RUN_ROOT || process.cwd());
39
- const bin = path.resolve(process.env.PIPELINE_RUN_BIN || path.join(root, '.agents', 'scripts'));
40
- const gitPrefix = (process.env.PIPELINE_RUN_GIT_BIN || 'git').split(/\s+/).filter(Boolean);
41
- return { root, bin, gitPrefix, runsDir: path.join(root, '.agents', 'cache', 'pipeline-runs') };
42
- }
43
-
44
- function moduleList(ctx) {
45
- try {
46
- return fs.readFileSync(path.join(ctx.root, '.agents', 'workflow-modules.txt'), 'utf8')
47
- .split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.startsWith('#'));
48
- } catch { return ['backend', 'frontend', 'ui', 'infra', 'integration', 'pipeline', 'wiki']; } // 夹具缺表时的回退默认(warning 由调用方打)
49
- }
50
30
 
51
- // ── run 文件(原子写;事件 append-only) ──────────────────────────────
52
- function runFileOf(ctx, runId) { return path.join(ctx.runsDir, `${runId}.json`); }
53
-
54
- function loadRun(ctx, runId) {
55
- const f = runFileOf(ctx, runId);
56
- if (!fs.existsSync(f)) return null;
57
- try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return null; }
58
- }
59
-
60
- function saveRun(ctx, run) {
61
- fs.mkdirSync(ctx.runsDir, { recursive: true });
62
- run.updatedAt = nowIso();
63
- const f = runFileOf(ctx, run.runId);
64
- const tmp = f + '.tmp';
65
- fs.writeFileSync(tmp, JSON.stringify(run, null, 2) + '\n');
66
- fs.renameSync(tmp, f);
67
- }
68
31
 
69
- function emit(run, ev) { run.events.push({ t: nowIso(), ...ev }); }
70
-
71
- function latestRunId(ctx) {
72
- if (!fs.existsSync(ctx.runsDir)) return null;
73
- const ids = fs.readdirSync(ctx.runsDir).filter((f) => f.endsWith('.json')).map((f) => f.slice(0, -5)).sort();
74
- for (let i = ids.length - 1; i >= 0; i--) {
75
- const r = loadRun(ctx, ids[i]);
76
- if (r && !['done', 'aborted'].includes(r.stopType)) return ids[i];
77
- }
78
- return ids[ids.length - 1] || null;
79
- }
80
-
81
- function newRunId(requirement) {
82
- const d = new Date();
83
- const ascii = (requirement.toLowerCase().match(/[a-z0-9]{2,}/g) || []).slice(0, 3).join('-') || 'task';
84
- const rand = Math.random().toString(36).slice(2, 6);
85
- return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}-${ascii}-${rand}`;
86
- }
32
+ import { nowIso, today, ctxEnv, moduleList, loadRun, saveRun, emit, latestRunId, newRunId } from './pipeline-run-run.mjs';
33
+ import { fmGet, placeholdersIn, sectionBody, validateDraftContent, closeoutComplete, summarizeDoc } from './pipeline-run-docs.mjs';
34
+ // 拆分(2026-10-08 engine-quality-batch):文档纯函数层 / run 存储层迁出本文件(判据注释随函数迁走,
35
+ // 对外导出面经下行 re-export 保持不变——stageBar/parseKV 仍在本文件);本文件 = CLI 编排层 + 状态机。
36
+ export { placeholdersIn, validateDraftContent, closeoutComplete } from './pipeline-run-docs.mjs';
87
37
 
88
38
  // ── 门执行(唯一子进程出口;记录真实退出码) ──────────────────────────
89
39
  function runNode(ctx, run, script, args) {
@@ -114,78 +64,8 @@ function runGit(ctx, run, args) {
114
64
  function readDoc(ctx, rel) {
115
65
  try { return fs.readFileSync(path.join(ctx.root, rel), 'utf8'); } catch { return null; }
116
66
  }
117
- function fmGet(text, key) {
118
- const m = text.split(/\r?\n/).find((l) => l.startsWith(`${key}:`));
119
- return m ? m.slice(key.length + 1).trim() : null;
120
- }
121
67
  function docState(ctx, rel) { const t = readDoc(ctx, rel); return t ? fmGet(t, '状态') : null; }
122
68
 
123
- // 占位符判定(复核 P1-3 收窄,2026-10-02):先剔除 code span(`…`)与引号串("…")——
124
- // 正文合法的命令语法占位(如 `next --delegated "<原话>"`)几乎总在 code span 内,
125
- // 模板实占位符(如 <为什么做;写明需求来源…>)从不包 code span;再匹配剩余裸尖括号。
126
- export function placeholdersIn(text) {
127
- const stripped = String(text)
128
- .replace(/^```[\s\S]*?^```$/gm, '') // 复核 P2-B:跨行 code fence 整块剔除(围栏内命令示例占位不属模板占位符)
129
- .replace(/`[^`\n]*`/g, '')
130
- .replace(/"[^"\n]*"/g, '');
131
- const hits = [];
132
- for (const re of [/<[^>\n]{1,60}>/g, /YYYY-MM-DD 用户/g]) {
133
- let m; while ((m = re.exec(stripped))) hits.push(m[0]);
134
- }
135
- return [...new Set(hits)].slice(0, 5);
136
- }
137
-
138
- function sectionBody(text, title) {
139
- const lines = text.split(/\r?\n/);
140
- const i = lines.findIndex((l) => l.trim() === `## ${title}` || l.trim() === `## ${title}(L1 微改动无实质内容可删本节,不硬填)` || l.trim().startsWith(`## ${title}`));
141
- if (i < 0) return null;
142
- const body = [];
143
- for (let j = i + 1; j < lines.length; j++) {
144
- if (/^##\s/.test(lines[j])) break;
145
- body.push(lines[j]);
146
- }
147
- return body.join('\n').trim();
148
- }
149
-
150
- export function validateDraftContent(text, kind, level) {
151
- const problems = [];
152
- if (!text) return ['文件不存在或为空'];
153
- const state = fmGet(text, '状态');
154
- if (!state) problems.push('frontmatter 缺「状态:」行');
155
- if (kind !== 'incident' && !fmGet(text, '级别')) problems.push('frontmatter 缺「级别:」行');
156
- const ph = placeholdersIn(text);
157
- if (ph.length) problems.push(`模板占位符残留:${ph.join('、')}`);
158
- const required = {
159
- intent: level === 'L1' ? ['背景与问题', '目标', '验收标准'] : ['背景与问题', '历史教训/防复发', '目标', '非目标', '约束', '验收标准'],
160
- spec: ['功能行为', '数据流', '系统改动', '约束遵守映射'],
161
- plan: level === 'L1' ? ['改动方案', '约束与风险', '验证计划'] : ['改动方案', '任务拆解', '执行顺序', '验证计划'],
162
- incident: ['时间线', '根因', '复盘三件套'],
163
- }[kind] || [];
164
- for (const sec of required) {
165
- const b = sectionBody(text, sec);
166
- if (b === null) { if (!(level === 'L1' && ['非目标', '约束', '任务拆解', '执行顺序'].includes(sec))) problems.push(`缺节:## ${sec}`); }
167
- else if (!b) problems.push(`节为空:## ${sec}`);
168
- }
169
- return problems;
170
- }
171
-
172
- export function closeoutComplete(text) {
173
- const body = sectionBody(text, '验收标准');
174
- if (body === null) return { ok: false, problems: ['缺「## 验收标准」节'] };
175
- const lines = body.split(/\r?\n/);
176
- const open = lines.filter((l) => /^\s*-\s\[\s\]\s/.test(l));
177
- // 复核 P2-6:证据冒号后须直接跟内容(非空白、非收括号)——「(证据:)」空值不算过
178
- const checkedNoEv = lines.filter((l) => /^\s*-\s\[x\]\s/.test(l) && !/证据[::][^)\s]/.test(l));
179
- const problems = [];
180
- if (open.length) problems.push(`未勾验收 ${open.length} 条`);
181
- if (checkedNoEv.length) problems.push(`已勾缺证据 ${checkedNoEv.length} 条`);
182
- return { ok: problems.length === 0, problems };
183
- }
184
-
185
- function summarizeDoc(text) {
186
- const body = text.split(/\r?\n/).filter((l) => l.trim() && !l.startsWith('---') && !/^状态:|^级别:|^日期:|^模块:|^备注:|^risk_level:/.test(l));
187
- return body.slice(0, 3).join(' / ').slice(0, 200);
188
- }
189
69
 
190
70
  // ── 工单构建 ───────────────────────────────────────────────────────────
191
71
  function wo(run, o) {
@@ -211,7 +91,7 @@ function stopConfirm(ctx, run, docRel, points, ledger) {
211
91
  printRun(ctx, run);
212
92
  process.exit(0);
213
93
  }
214
- function stopGateFail(ctx, run, stage, problems, exitHint) {
94
+ function stopGateFail(ctx, run, stage, problems) {
215
95
  run.workOrder = null;
216
96
  run.stopType = 'gate-fail';
217
97
  run.stage = stage;
@@ -318,11 +198,11 @@ function confirmGate(ctx, run, docRel, quote, to) {
318
198
  const tail = (r.stderr || r.stdout).split(/\r?\n/).filter(Boolean).slice(-6).join(' | ');
319
199
  return { ok: false, msg: `confirm-doc exit ${r.exit}:${tail}` };
320
200
  }
321
- emit(run, { type: 'confirm', doc: docRel, to: to || nextConfirmTarget(docState(ctx, docRel), docRel), source: quote ? 'chat-delegated' : 'tty', ...(quote ? { quote } : {}) });
201
+ emit(run, { type: 'confirm', doc: docRel, to: to || nextConfirmTarget(docState(ctx, docRel)), source: quote ? 'chat-delegated' : 'tty', ...(quote ? { quote } : {}) });
322
202
  return { ok: true };
323
203
  }
324
204
  // 语义上 confirm-doc 自行决定目标态(approved/done/fixed/closed);事件里记录结果态
325
- function nextConfirmTarget(curState, docRel) { return curState || 'approved'; }
205
+ function nextConfirmTarget(curState) { return curState || 'approved'; }
326
206
 
327
207
  // managedFaceCheck(复核 P2-4 修复 A′,2026-10-02 用户拍板):提交集命中 kit.json 台账面或包源
328
208
  // templates/_agents/ 时——包源环境(bin/flow-kit.mjs 可达)自动跑 sync 刷台账并把 kit.json 与 sync
@@ -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 = {
@@ -86,7 +87,13 @@ export const POLICIES = {
86
87
  stageGateSince: '2026-09-29',
87
88
  riskLevelSince: '2026-10-01',
88
89
  verifySince: '2026-10-07',
89
- verifyDocSince: '2026-10-08', // 凭证按 doc 绑定生效日;v1-v5 无此键 → 两个消费方维持 v5 全局行为
90
+ // 凭证按 doc 绑定的生效**时刻**(ISO 8601,含 T/Z);v1-v5 无此键 → 两个消费方维持 v5 全局行为。
91
+ // 2026-10-09 verifydoc-anchor:值从日期 '2026-10-08' 升级为时刻——本仓 v6 上线当日(本地 17:37 =
92
+ // 09:37Z)之前 done 的单(selfmeasure 00:22Z / gate-roi-metrics 03:25Z)在日期粒度下被误纳入精确匹配、
93
+ // 产 3 条不可消退 advisory(它们关单时 --doc 机制尚不存在,无从补证);升级为时刻后按 done ts 精确比较,
94
+ // 机制上线前的存量单退回 v5 全局布尔(不误报、不伪造),与检查 15/18 的生效日锚同构。
95
+ // 消费方:检查 8 的 verifyDocInWindow(done ts ≥ 本值才启用精确匹配)。
96
+ verifyDocSince: '2026-10-08T09:37:35Z',
90
97
  },
91
98
  };
92
99
 
@@ -136,3 +143,15 @@ export function hasFreshVerifyLine(lines, now = Date.now(), doc = null) {
136
143
  }
137
144
  return false;
138
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
+ }
@@ -112,12 +112,12 @@ function printDiff(result) {
112
112
 
113
113
  const isMain = process.argv[1] && process.argv[1].endsWith('source-sync-check.mjs');
114
114
  if (isMain) {
115
- let jsonMode = false, diffMode = false, gateMode = false;
115
+ let jsonMode = false, gateMode = false;
116
116
  let pkgRoot = null, target = null;
117
117
  for (let i = 0; i < process.argv.length - 2; i++) {
118
118
  const a = process.argv[2 + i];
119
119
  if (a === '--json') jsonMode = true;
120
- else if (a === '--diff') diffMode = true;
120
+ else if (a === '--diff') { /* diff 为默认输出模式,无需置位 */ }
121
121
  else if (a === '--gate') gateMode = true;
122
122
  else if (a === '--pkg-root') pkgRoot = process.argv[2 + i + 1];
123
123
  else if (a === '--target') target = process.argv[2 + i + 1];
@@ -125,7 +125,7 @@ if (!SRC_ROOT) {
125
125
  drift: r.drift.map((d) => ({ rel: d.rel, pkgSha: d.pkgSha.slice(0, 8), tgtSha: d.tgtSha.slice(0, 8) })),
126
126
  }, null, 2);
127
127
  let parsed = null;
128
- try { parsed = JSON.parse(jsonOut); } catch {}
128
+ try { parsed = JSON.parse(jsonOut); } catch { /* 非 JSON 输出时 parsed 保持 null,S6 判失败路径 */ }
129
129
  check('S6 --json 输出可解析', parsed && typeof parsed === 'object' && Array.isArray(parsed.missing));
130
130
  }
131
131
 
@@ -125,7 +125,6 @@ for (const line of fs.readFileSync(path.join(WIKI, 'INDEX.md'), 'utf8').split(/\
125
125
  }
126
126
 
127
127
  const diff = (a, b) => [...a].filter((x) => !b.has(x));
128
- const eq = (a, b) => a.size === b.size && diff(a, b).length === 0;
129
128
 
130
129
  // ---- 1. 文件级三方比对 ----
131
130
  for (const [label, extra] of [
@@ -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);
@@ -162,9 +162,9 @@ const run = (argv, opts = {}) => spawnSync(process.execPath, [VERIFY, ...argv],
162
162
  const rootC = mkfix();
163
163
  const abs = path.join(rootC, rel);
164
164
  const winStyle = rel.split('/').join(path.sep);
165
- const rC1 = run(['--test-cmd', `node ${mkCmd(0)}`, '--doc', winStyle], { cwd: rootC, env: { ...process.env, CHECK_LOOP_ROOT: rootC } });
165
+ run(['--test-cmd', `node ${mkCmd(0)}`, '--doc', winStyle], { cwd: rootC, env: { ...process.env, CHECK_LOOP_ROOT: rootC } });
166
166
  const eC1 = readLast(rootC);
167
- const rC2 = run(['--test-cmd', `node ${mkCmd(0)}`, '--doc', abs], { cwd: rootC, env: { ...process.env, CHECK_LOOP_ROOT: rootC } });
167
+ run(['--test-cmd', `node ${mkCmd(0)}`, '--doc', abs], { cwd: rootC, env: { ...process.env, CHECK_LOOP_ROOT: rootC } });
168
168
  const eC2 = readLast(rootC);
169
169
  check('场景 8③:反斜杠 / posix / 绝对路径三种写法落同一 doc 值(归一是地基)',
170
170
  eC1 && eC1.doc === rel && eC2 && eC2.doc === rel, `win=${JSON.stringify(eC1 && eC1.doc)} abs=${JSON.stringify(eC2 && eC2.doc)}`);
@@ -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);
@@ -14,7 +14,6 @@ import { spawnSync } from 'node:child_process';
14
14
  import { fileURLToPath } from 'node:url';
15
15
  import { parseLoopHardBlocks, parseAcceptance, detectAlerts, parseRunFile, runCard, safeRunPath } from './workflow-board-server.mjs';
16
16
  import { computeFingerprint } from './confirm-doc.mjs';
17
- import { stageBar } from './pipeline-run.mjs';
18
17
 
19
18
  let pass = 0;
20
19
  let fail = 0;
@@ -2,7 +2,6 @@
2
2
  // 判据:① 真实单源文件解析 + 不变量通过 ② 坏数据(坏行/重复键/重复值/缺键/子集破/重叠)fail-loud
3
3
  // ③ 缺文件 fail-loud ④ sh 消费方同口径(sed 可取到全部 8 键)
4
4
  // 用法:node templates/_agents/scripts/workflow-enums.test.mjs(npm test 随跑)
5
- import fs from 'node:fs';
6
5
  import path from 'node:path';
7
6
  import { execSync } from 'node:child_process';
8
7
  import { fileURLToPath } from 'node:url';
@@ -2,9 +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 | 检索库件 | 无(工具豁免,后续批次评估补测)
10
+ # 2026-10-08 engine-quality-batch:pipeline-run 三层拆分的库件层——纯函数/存储层由 pipeline-run.test.mjs 全量覆盖(断言零改动全过),单建同名测试属形式主义(plan 偏离留痕 ①)
11
+ pipeline-run-docs.mjs | 库件(文档纯函数层,2026-10-08 engine-quality-batch 拆分) | 由 pipeline-run.test.mjs 全量覆盖(断言零改动 29 组全过)
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
+ "