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,77 @@
1
+ // check-stage-index.test.mjs — 检查 7 模块直测(2026-10-09 engine-quality-round2 A1)
2
+ // 四面:全绿 / AGENTS 侧缺索引 / new-task 文件缺失合法 / gate-seg 段归属(局部收集器并入)
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import os from 'node:os';
6
+ import { runCheckStageIndex } from './check-stage-index.mjs';
7
+ import { makeCollector, finishSegs } from './gate-seg.mjs';
8
+
9
+ let pass = 0, fail = 0;
10
+ const check = (name, cond, detail = '') => {
11
+ if (cond) { pass++; console.log(`PASS ${name}`); }
12
+ else { fail++; console.log(`FAIL ${name}${detail ? `——${detail}` : ''}`); }
13
+ };
14
+
15
+ // ctx 构造:linesOf 提供读异常→null 语义(与 check-loop 内部实现同语义;「判定零复刻」指模块不重写
16
+ // 判据,测试 fixture 提供测试版 helper 是 check-hygiene.test 先例)
17
+ function makeCtx(root, overrides = {}) {
18
+ const fileLinesCache = new Map();
19
+ const linesOf = (file) => {
20
+ if (!fileLinesCache.has(file)) {
21
+ let lines = null;
22
+ try { lines = fs.readFileSync(file, 'utf8').split(/\r?\n/); } catch { lines = null; }
23
+ fileLinesCache.set(file, lines);
24
+ }
25
+ return fileLinesCache.get(file);
26
+ };
27
+ return { root, linesOf, ...overrides };
28
+ }
29
+ const ROUTES = '.agents/commands/plan.md → .agents/commands/design.md → .agents/commands/build.md → .agents/commands/test.md → .agents/commands/deploy.md → .agents/commands/maintain.md';
30
+ const mkRoot = (agentsBody, newTaskBody, { noAgents, noNewTask } = {}) => {
31
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'csi-'));
32
+ fs.mkdirSync(path.join(root, '.agents', 'commands'), { recursive: true });
33
+ if (!noAgents) fs.writeFileSync(path.join(root, 'AGENTS.md'), agentsBody);
34
+ if (!noNewTask) fs.writeFileSync(path.join(root, '.agents', 'commands', 'new-task.md'), newTaskBody);
35
+ return root;
36
+ };
37
+ const FULL = `# AGENTS\n\n${ROUTES}\n\n**横切**:.agents/commands/review.md\n`;
38
+ const FULL_NT = `# new-task\n\n| Plan | ${ROUTES} | Review(.agents/commands/review.md) |\n`;
39
+
40
+ {
41
+ // ① 双面全索引 → 零告警
42
+ const root = mkRoot(FULL, FULL_NT);
43
+ const r = runCheckStageIndex(makeCtx(root));
44
+ check('① 双面全索引 → 零告警', r.length === 0, JSON.stringify(r));
45
+ fs.rmSync(root, { recursive: true, force: true });
46
+ }
47
+ {
48
+ // ② AGENTS 侧缺 plan → 仅 AGENTS.md 侧告警(new-task 索引齐全不误报)
49
+ const root = mkRoot(FULL.replace('.agents/commands/plan.md → ', ''), FULL_NT);
50
+ const r = runCheckStageIndex(makeCtx(root));
51
+ check('② AGENTS 侧缺 plan → WARN 点名 AGENTS.md',
52
+ r.some((w) => w.includes('AGENTS.md 缺 plan 指令索引')), JSON.stringify(r));
53
+ check('② new-task 侧不告警(其索引齐全)', !r.some((w) => w.includes('new-task.md 缺')), JSON.stringify(r));
54
+ fs.rmSync(root, { recursive: true, force: true });
55
+ }
56
+ {
57
+ // ③ new-task 文件不存在 = 合法缺失态(existsSync 守卫)→ 零告警
58
+ const root = mkRoot(FULL, FULL_NT, { noNewTask: true });
59
+ const r = runCheckStageIndex(makeCtx(root));
60
+ check('③ new-task 文件缺失 → 合法缺失零告警', r.length === 0, JSON.stringify(r));
61
+ fs.rmSync(root, { recursive: true, force: true });
62
+ }
63
+ {
64
+ // ④ gate-seg 段归属:ctx.gateStats 在场时段 '6' 入账、warns 与实际 WARN 数一致(selfmeasure 接线
65
+ // 教训:模块须用局部收集器 + appendSegs,直接共用全局游标会错账)
66
+ const root = mkRoot(FULL.replace('.agents/commands/plan.md → ', ''), FULL_NT);
67
+ const stats = makeCollector([], []);
68
+ const r = runCheckStageIndex(makeCtx(root, { gateStats: stats }));
69
+ const seg6 = finishSegs(stats).filter((s) => s.id === '6');
70
+ check('④ gate-seg:段 6 入账一次且 warns 与实际 WARN 数一致',
71
+ seg6.length === 1 && seg6[0].warns === r.length && r.length === 1,
72
+ JSON.stringify({ seg6, ret: r }));
73
+ fs.rmSync(root, { recursive: true, force: true });
74
+ }
75
+
76
+ console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
77
+ process.exit(fail ? 1 : 0);
@@ -9,7 +9,6 @@ import { fileURLToPath } from 'node:url';
9
9
 
10
10
  const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
11
11
  const HOOK = path.join(SCRIPT_DIR, '..', 'hooks', 'commit-check.cjs');
12
- const MARK = 'TRIGGER_TEST_BUILD_RAN';
13
12
 
14
13
  let pass = 0, fail = 0;
15
14
  const check = (desc, cond, detail = '') => {
@@ -38,9 +38,8 @@ import fs from 'node:fs';
38
38
  import path from 'node:path';
39
39
  import readline from 'node:readline';
40
40
  import { createHash, randomBytes } from 'node:crypto';
41
- import { fileURLToPath } from 'node:url';
42
41
  import { confirmGateFor, doneGateFor, laneOfEntry, laneOfDoc } from './stage-gates.mjs';
43
- import { loadKitPolicy, hasFreshVerifyLine } from './policy.mjs';
42
+ import { loadKitPolicy, hasFreshVerifyLine, ledgerChainHash } from './policy.mjs';
44
43
 
45
44
  // hasFreshVerify(root, doc):读 <root>/.agents/verifications.jsonl 判 24h 窗口绿行(判定单源
46
45
  // policy.hasFreshVerifyLine)。只读,台账缺失/不可读一律 false(=无凭证,advisory 层 fail-open)。
@@ -121,10 +120,25 @@ export function applyTransition(text, targetStage, fingerprint16) {
121
120
  }
122
121
 
123
122
  // appendLedger(root, entry):追加一行 JSON(目录不存在自动建;只 append 不重写)
123
+ // 哈希链(2026-10-09 engine-quality-round2 D):每行追加 prevHash(末行 hash,末行无 hash/空文件 → '')
124
+ // 与 hash(ledgerChainHash 单源计算,check-loop 15 同函数验链)——「就地改/删中间行」必留断链痕迹;
125
+ // 历史行零回填(无 hash 行后链重启),整文件重写不可机器防(边界声明见 policy.ledgerChainHash)。
124
126
  export function appendLedger(root, entry) {
125
127
  const p = path.join(root, '.agents', 'confirmations.jsonl');
126
128
  fs.mkdirSync(path.dirname(p), { recursive: true });
127
- fs.appendFileSync(p, JSON.stringify(entry) + '\n');
129
+ let prevHash = '';
130
+ try {
131
+ if (fs.existsSync(p)) {
132
+ const lines = fs.readFileSync(p, 'utf8').split(/\r?\n/).filter((l) => l.trim());
133
+ if (lines.length) {
134
+ const last = JSON.parse(lines[lines.length - 1]);
135
+ prevHash = last && typeof last.hash === 'string' ? last.hash : '';
136
+ }
137
+ }
138
+ } catch { /* 末行不可解析 → prevHash ''(链从本行重启;断裂由 check-loop 15 出账) */ }
139
+ const { hash: _h, prevHash: _p, ...rest } = entry;
140
+ const row = { ...rest, prevHash, hash: ledgerChainHash(rest, prevHash) };
141
+ fs.appendFileSync(p, JSON.stringify(row) + '\n');
128
142
  return p;
129
143
  }
130
144
 
@@ -104,6 +104,23 @@ const readLedgerOf = (root) => {
104
104
  fs.rmSync(root, { recursive: true, force: true });
105
105
  }
106
106
 
107
+ // ---- S8b 哈希链(2026-10-09 engine-quality-round2 D):链字段写入 + 接续 + 重算自洽 ----
108
+ {
109
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-chain-'));
110
+ // 先写一行无 hash 的「存量」行(模拟 v1.5.0 前台账)——新行 prevHash 应为 ''(链重启)
111
+ fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
112
+ fs.writeFileSync(path.join(root, '.agents', 'confirmations.jsonl'), JSON.stringify({ ts: 'T0', doc: 'workflow/plans/old.md', stage: 'done', fingerprint: 'e'.repeat(64), prev: 'approved' }) + '\n');
113
+ appendLedger(root, { ts: 'T1', doc: 'workflow/plans/a.md', stage: 'approved', fingerprint: 'f'.repeat(64), prev: 'draft' });
114
+ appendLedger(root, { ts: 'T2', doc: 'workflow/plans/a.md', stage: 'done', fingerprint: 'g'.repeat(64), prev: 'approved' });
115
+ const rows = fs.readFileSync(path.join(root, '.agents', 'confirmations.jsonl'), 'utf8').trim().split('\n').map((l) => JSON.parse(l));
116
+ check('S8b 存量行后首链行 prevHash 为空(链重启,历史零回填)', rows[0].hash === undefined && rows[1].prevHash === '');
117
+ check('S8b 第二链行 prevHash 接续第一链行 hash', rows[2].prevHash === rows[1].hash);
118
+ const { ledgerChainHash } = await import('./policy.mjs');
119
+ const recompute = (row) => { const { hash, prevHash, ...rest } = row; return ledgerChainHash(rest, prevHash) === hash; };
120
+ check('S8b 两链行 hash 重算自洽(CRLF/键序规范化单源)', recompute(rows[1]) && recompute(rows[2]));
121
+ fs.rmSync(root, { recursive: true, force: true });
122
+ }
123
+
107
124
  // ---- S9【核心】非 TTY spawn(AI 调用路径)必须被拒 ----
108
125
  {
109
126
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-tty-'));
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+ // edit-face-check — 常驻面编辑时快检(2026-10-09 engine-quality-round2 B2)
3
+ // 动机:常驻面(AGENTS.md 等)的机器约束此前只在 git 钩子层生效——check-loop 仓库模式只扫 HEAD,
4
+ // 工作树编辑期零反馈(实测:常驻面重构砍掉阶段索引,到提交才被检查 7 报警)。本脚本把判定左移到编辑时。
5
+ // 判定零复刻铁律:闭环判定**单源**复用 check-loop——以 CHECK_LOOP_ROOT=<root>(全扫模式,扫工作树
6
+ // 非 HEAD)子进程消费其裁决,本脚本不实现任何门判定;唯一自算是常驻面字节预算 advisory(工作树
7
+ // LF 字节 vs rule-budgets.txt 上限——提交门口径走 git index/HEAD(rule-budget.sh),两者判据不同面,
8
+ // 此处为编辑时 advisory,冲突时以提交门为准)。
9
+ // exit 语义:check-loop hard(exit≠0)→ 2;预算 advisory 超限 → 1;其余 → 0。
10
+ // WARN **不进 exit**(打印明细供 AI/人对照)——绝对 WARN 计数会让带存量 advisory 的仓库恒非零
11
+ // (本仓即有用户拍板保留的存量 WARN),工具就不可用了;编辑破坏的表现(如阶段索引漂移 WARN)
12
+ // 在输出里一眼可见,由编辑者处置。
13
+ // 用法:node .agents/scripts/edit-face-check.mjs [--root <仓库根>](缺省 cwd;测试注入 EDIT_FACE_CHECK_SPAWN)
14
+ // 测试:node templates/_agents/scripts/edit-face-check.test.mjs
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ import { spawnSync } from 'node:child_process';
18
+ import { fileURLToPath } from 'node:url';
19
+
20
+ // LF 归一字节数(编辑时口径——工作区文件可能 CRLF,预算表口径注释声明「比较用 LF 归一」)
21
+ export function lfBytes(text) {
22
+ return Buffer.byteLength(String(text).replace(/\r\n/g, '\n'), 'utf8');
23
+ }
24
+
25
+ // globToRe:rule-budgets.txt 行首路径 → 正则(支持 `*` 段内通配与结尾 `/` 目录前缀;其余字面)
26
+ export function globToRe(pattern) {
27
+ const dir = pattern.endsWith('/');
28
+ let src = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*');
29
+ if (dir) src += '.*';
30
+ return new RegExp(`^${src}${dir ? '' : '$'}`);
31
+ }
32
+
33
+ // budgetAdvisory:常驻面预算 advisory(编辑时口径)。files: Map<rel, 工作树文本>;budgetsText: 表原文。
34
+ // 返回超限行 [{ rel, limit, actual }];表缺失/行非法 → 空数组(advisory 层 fail-open)。
35
+ export function budgetAdvisory(files, budgetsText) {
36
+ const out = [];
37
+ if (!budgetsText) return out;
38
+ for (const line of budgetsText.split(/\r?\n/)) {
39
+ const t = line.replace(/\r$/, '').trim();
40
+ if (!t || t.startsWith('#')) continue;
41
+ const m = t.match(/^(\S+)\s+(\d+)$/);
42
+ if (!m) continue;
43
+ const [, pattern, limitStr] = m;
44
+ const limit = Number(limitStr);
45
+ const re = globToRe(pattern);
46
+ for (const [rel, text] of files) {
47
+ if (!re.test(rel)) continue;
48
+ const actual = lfBytes(text);
49
+ if (actual > limit) out.push({ rel, limit, actual });
50
+ }
51
+ }
52
+ return out;
53
+ }
54
+
55
+ // runCheck:工作树面 check-loop(CHECK_LOOP_ROOT 全扫)。返回 { code, output, ms }。
56
+ function runCheck(root, scriptDir) {
57
+ const t0 = Date.now();
58
+ const r = spawnSync(process.execPath, [path.join(scriptDir, 'check-loop.mjs')], {
59
+ cwd: root, encoding: 'utf8', windowsHide: true,
60
+ env: { ...process.env, CHECK_LOOP_ROOT: root },
61
+ });
62
+ return { code: r.status ?? 1, output: `${r.stdout || ''}${r.stderr || ''}`, ms: Date.now() - t0 };
63
+ }
64
+
65
+ export function main(root = process.cwd(), scriptDir = path.dirname(fileURLToPath(import.meta.url))) {
66
+ // ① 闭环判定(单源 check-loop,工作树全扫)
67
+ const ck = runCheck(root, scriptDir);
68
+ const warnCount = (ck.output.match(/\[WARN /g) || []).length;
69
+ // ② 常驻面预算 advisory(AGENTS.md + 预算表内文件的工作树字节)
70
+ let over = [];
71
+ try {
72
+ const budgetsPath = path.join(root, '.agents', 'rule-budgets.txt');
73
+ const budgetsText = fs.existsSync(budgetsPath) ? fs.readFileSync(budgetsPath, 'utf8') : null;
74
+ const files = new Map();
75
+ const agentsPath = path.join(root, 'AGENTS.md');
76
+ if (fs.existsSync(agentsPath)) files.set('AGENTS.md', fs.readFileSync(agentsPath, 'utf8'));
77
+ over = budgetAdvisory(files, budgetsText);
78
+ } catch { /* advisory 层 fail-open:预算读不到不阻断编辑时反馈 */ }
79
+
80
+ console.log(`▶ edit-face-check → ${root}(工作树面)`);
81
+ console.log(` check-loop:${ck.code === 0 ? '通过' : `exit ${ck.code}`},WARN ${warnCount} 条(${ck.ms}ms)——与本仓存量 advisory 对照看新增`);
82
+ // WARN 明细透传(编辑破坏的判读面——如删阶段索引会在此点名「阶段索引漂移」)
83
+ for (const line of ck.output.split(/\r?\n/)) {
84
+ if (/\[WARN /.test(line)) console.log(' ' + line.trim().slice(0, 200));
85
+ }
86
+ for (const { rel, limit, actual } of over) {
87
+ console.log(` ⚠️ 预算 advisory:${rel} 工作树 ${actual}B > 上限 ${limit}B(编辑时口径;提交门以 rule-budget.sh 为准)`);
88
+ }
89
+ if (ck.code !== 0) {
90
+ console.error('❌ 闭环判定 hard 未过——先修复再提交(判定单源 check-loop,详见上方输出)');
91
+ return 2;
92
+ }
93
+ if (over.length > 0) {
94
+ console.error('⚠️ 常驻面预算 advisory 超限——建议编辑期就瘦,别留到提交门');
95
+ return 1;
96
+ }
97
+ console.log('✅ 编辑时快检完成(hard/预算两道均过;WARN 明细请对照处置)');
98
+ return 0;
99
+ }
100
+
101
+ const isMain = process.argv[1] && process.argv[1].endsWith('edit-face-check.mjs');
102
+ if (isMain) {
103
+ let root = process.cwd();
104
+ const i = process.argv.indexOf('--root');
105
+ if (i > 0 && process.argv[i + 1]) root = path.resolve(process.argv[i + 1]);
106
+ process.exit(main(root));
107
+ }
@@ -0,0 +1,98 @@
1
+ // edit-face-check.test.mjs — 编辑时快检(2026-10-09 engine-quality-round2 B2)
2
+ // 三面:纯函数(lfBytes/globToRe/budgetAdvisory)+ 端到端全绿面 + 破坏面(删阶段索引→WARN→exit 1、
3
+ // 预算超限→exit 1)。判定零复刻:端到端经 CHECK_LOOP_ROOT 消费真 check-loop,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, pathToFileURL } 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
+ const mod = await import(pathToFileURL(path.join(SRC, 'edit-face-check.mjs')).href);
18
+ const { lfBytes, globToRe, budgetAdvisory } = mod;
19
+
20
+ // ---- ① 纯函数 ----
21
+ check('lfBytes:CRLF 归一计 UTF-8 字节', lfBytes('a\r\nb中文') === Buffer.byteLength('a\nb中文', 'utf8'));
22
+ check('globToRe:精确路径', globToRe('AGENTS.md').test('AGENTS.md') && !globToRe('AGENTS.md').test('x/AGENTS.md'));
23
+ check('globToRe:段内通配', globToRe('.agents/commands/*.md').test('.agents/commands/plan.md') && !globToRe('.agents/commands/*.md').test('.agents/commands/x/plan.md'));
24
+ check('globToRe:目录前缀', globToRe('.agents/cache/').test('.agents/cache/a/b.json'));
25
+
26
+ {
27
+ const files = new Map([
28
+ ['AGENTS.md', 'x'.repeat(120)],
29
+ ['.agents/commands/plan.md', 'y'.repeat(50)],
30
+ ]);
31
+ const budgets = '# 注释行\nAGENTS.md 100\n.agents/commands/*.md 200\n非法行\n';
32
+ const r = budgetAdvisory(files, budgets);
33
+ check('budgetAdvisory:超限单文件报、未超限不报、非法行跳过',
34
+ r.length === 1 && r[0].rel === 'AGENTS.md' && r[0].limit === 100 && r[0].actual === 120, JSON.stringify(r));
35
+ check('budgetAdvisory:表缺失 fail-open', budgetAdvisory(files, null).length === 0);
36
+ }
37
+
38
+ // ---- ② 端到端 fixture ----
39
+ const SCRIPTS = ['check-loop.mjs', 'policy.mjs', 'stage-gates.mjs', 'check-metric-claims.mjs', 'check-hygiene.mjs', 'check-stage-index.mjs', 'gate-seg.mjs', 'workflow-enums.mjs', 'edit-face-check.mjs'];
40
+ // 引用完整性哑文件(检查 4/6/7 的存在性判据——避免 fixture 结构性 WARN 淹没断言面)
41
+ const REF_COMMANDS = ['new-task', 'plan', 'design', 'build', 'test', 'deploy', 'maintain', 'review'];
42
+ const REF_ROLES = ['implementer', 'independent-reviewer', 'ui-verifier'];
43
+ function mkFixture({ breakStageIndex, overBudget }) {
44
+ const fx = fs.mkdtempSync(path.join(os.tmpdir(), 'efc-'));
45
+ fs.mkdirSync(path.join(fx, '.agents', 'scripts'), { recursive: true });
46
+ for (const f of SCRIPTS) fs.copyFileSync(path.join(SRC, f), path.join(fx, '.agents', 'scripts', f));
47
+ for (const f of ['kit.json', 'workflow-modules.txt', 'workflow-enums.txt'])
48
+ if (fs.existsSync(path.join(SRC, '..', f))) fs.copyFileSync(path.join(SRC, '..', f), path.join(fx, '.agents', f));
49
+ fs.mkdirSync(path.join(fx, 'workflow', 'intents'), { recursive: true });
50
+ for (const d of ['specs', 'plans', 'incidents']) fs.mkdirSync(path.join(fx, 'workflow', d), { recursive: true });
51
+ for (const c of REF_COMMANDS) { fs.mkdirSync(path.join(fx, '.agents', 'commands'), { recursive: true }); fs.writeFileSync(path.join(fx, '.agents', 'commands', `${c}.md`), `# ${c}\n`); }
52
+ for (const rname of REF_ROLES) { fs.mkdirSync(path.join(fx, '.agents', 'roles'), { recursive: true }); fs.writeFileSync(path.join(fx, '.agents', 'roles', `${rname}.md`), `# ${rname}\n`); }
53
+ // 常驻面 AGENTS.md:阶段索引齐全(含 review)/ 破坏面删索引(破坏只在 AGENTS.md 侧——new-task
54
+ // 哑文件写全索引,钉检查 7 的双向语义差异)
55
+ const routes = '.agents/commands/plan.md → .agents/commands/design.md → .agents/commands/build.md → .agents/commands/test.md → .agents/commands/deploy.md → .agents/commands/maintain.md';
56
+ const reviewLine = breakStageIndex ? '' : '\n**横切**:.agents/commands/review.md\n';
57
+ const body = `# AGENTS\n\n## 阶段路由\n\n**入口**:.agents/commands/new-task.md\n**指令**:${breakStageIndex ? '(已删)' : routes}${reviewLine}\n`;
58
+ fs.writeFileSync(path.join(fx, 'AGENTS.md'), overBudget ? body + 'x'.repeat(200) : body);
59
+ fs.writeFileSync(path.join(fx, '.agents', 'commands', 'new-task.md'),
60
+ `# new-task\n\n| Plan | ${routes} | Review |\n`);
61
+ fs.writeFileSync(path.join(fx, '.agents', 'rule-budgets.txt'), `AGENTS.md ${overBudget ? 100 : 100000}\n`);
62
+ return fx;
63
+ }
64
+
65
+ {
66
+ // 全绿面:空 workflow + 引用齐全 + 预算内 → hard/预算两道过(exit 0)。
67
+ // 注:不要求零 WARN——check-loop 全扫对极简 fixture 有结构性 advisory(引用/角色形态),明细
68
+ // 承载给编辑者对照;exit 语义只钉 hard 与预算两道(绝对 WARN 计数会让存量仓库恒非零)。
69
+ const fx = mkFixture({});
70
+ const driver = path.join(fx, 'driver.mjs');
71
+ fs.writeFileSync(driver, `import { main } from ${JSON.stringify('file:///' + path.join(fx, '.agents', 'scripts', 'edit-face-check.mjs').replace(/\\/g, '/'))};\nprocess.exit(main(${JSON.stringify(fx)}, ${JSON.stringify(path.join(fx, '.agents', 'scripts'))}));\n`);
72
+ const r = spawnSync(process.execPath, [driver], { encoding: 'utf8' });
73
+ check('E2E 全绿面:exit 0(hard/预算两道过)且 AGENTS.md 侧无索引漂移', r.status === 0 && !/AGENTS\.md 缺 .* 指令索引/.test(r.stdout || ''), `status=${r.status}\n${(r.stdout || '') + (r.stderr || '')}`.slice(0, 600));
74
+ }
75
+
76
+ {
77
+ // 破坏面:删阶段索引 → 检查 7 WARN 点名(明细承载编辑破坏——破坏面 vs 全绿面的差异就在输出;
78
+ // hard 不误触:阶段索引是 warning 级,exit≠2)
79
+ const fx = mkFixture({ breakStageIndex: true });
80
+ const driver = path.join(fx, 'driver.mjs');
81
+ fs.writeFileSync(driver, `import { main } from ${JSON.stringify('file:///' + path.join(fx, '.agents', 'scripts', 'edit-face-check.mjs').replace(/\\/g, '/'))};\nprocess.exit(main(${JSON.stringify(fx)}, ${JSON.stringify(path.join(fx, '.agents', 'scripts'))}));\n`);
82
+ const r = spawnSync(process.execPath, [driver], { encoding: 'utf8' });
83
+ const out = (r.stdout || '') + (r.stderr || '');
84
+ check('E2E 破坏面(删阶段索引):WARN 点名 AGENTS.md 侧索引漂移且 hard 不误触', r.status !== 2 && /AGENTS\.md 缺 plan 指令索引/.test(out), `status=${r.status}\n${out.slice(0, 600)}`);
85
+ }
86
+
87
+ {
88
+ // 预算超限面:AGENTS.md 超上限 → exit 1(advisory 面单独可达——workflow 全绿)
89
+ const fx = mkFixture({ overBudget: true });
90
+ const driver = path.join(fx, 'driver.mjs');
91
+ fs.writeFileSync(driver, `import { main } from ${JSON.stringify('file:///' + path.join(fx, '.agents', 'scripts', 'edit-face-check.mjs').replace(/\\/g, '/'))};\nprocess.exit(main(${JSON.stringify(fx)}, ${JSON.stringify(path.join(fx, '.agents', 'scripts'))}));\n`);
92
+ const r = spawnSync(process.execPath, [driver], { encoding: 'utf8' });
93
+ const out = (r.stdout || '') + (r.stderr || '');
94
+ check('E2E 预算超限面:exit 1 且 advisory 点名', r.status === 1 && /预算 advisory/.test(out), `status=${r.status}\n${out.slice(0, 500)}`);
95
+ }
96
+
97
+ console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
98
+ process.exit(fail ? 1 : 0);
@@ -105,7 +105,7 @@ function openBrowser(url) {
105
105
  try {
106
106
  if (process.platform === 'win32') spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore', windowsHide: true }).unref();
107
107
  else spawn(process.platform === 'darwin' ? 'open' : 'xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();
108
- } catch { }
108
+ } catch { /* 浏览器拉起失败不阻断看板流程 */ }
109
109
  }
110
110
 
111
111
  // ---- 主流程(返回退出码,自然排水退出——不用 process.exit:强退在 Windows 上会触发 libuv 断言)----
@@ -123,7 +123,7 @@ async function main() {
123
123
  board = b;
124
124
  const stale = staleCode(b.startedAt);
125
125
  if (stale === true) {
126
- try { process.kill(b.pid); } catch { } // 进程可能已自行退出,忽略
126
+ try { process.kill(b.pid); } catch { /* 进程可能已自行退出 */ }
127
127
  await sleep(500);
128
128
  action = 'restarted (stale code detected)';
129
129
  } else if (stale === null) {
@@ -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)
@@ -31,17 +33,29 @@ const SECTIONS = [
31
33
  { title: '目标', hint: '- <可验证的目标>' },
32
34
  { title: '非目标(L1 微改动无实质内容可删本节,不硬填)', hint: '- <明确不做的,防止范围蔓延>' },
33
35
  { title: '约束(L1 微改动无实质内容可删本节,不硬填)', hint: '- <复用什么、不许动什么>' },
34
- { title: '影响面', hint: '- 模块:<词表之一,见 .agents/workflow-modules.txt>\n- 数据库:无 / <表名>(涉及 schema 变更 $\rightarrow$ L3;无 schema 的运行时 / 管线结构面亦为 L3,判据见 .agents/commands/new-task.md §级别判断)\n- 前端页面:<路由或组件,无则删本行>' },
35
- { title: '触达红线(对照 AGENTS.md,勾选仅标记触及范围;具体如何满足在同名 spec 或 Quick-Plan 中说明)', hint: '- [ ] <按本项目 AGENTS.md 红线逐行补;无则删本行>\n- [ ] 规则 / 契约变更(编码权威 / 共享契约 / 接口签名 / 既有参数语义 / 全局口径)$\rightarrow$ 级别至少 L2\n- [ ] schema / 迁移 SQL / DI 链 / 认证与中间件管线 $\rightarrow$ 级别 L3\n\n> 触及任一红线即为 L2 / L3,必须立 ../specs/YYYY-MM-DD-<主题>.md 写明如何满足,方可起草 plan。' },
36
+ { title: '影响面', hint: '- 模块:<词表之一,见 .agents/workflow-modules.txt>\n- 数据库:无 / <表名>(涉及 schema 变更 → L3;无 schema 的运行时 / 管线结构面亦为 L3,判据见 .agents/commands/new-task.md §级别判断)\n- 前端页面:<路由或组件,无则删本行>' },
37
+ { title: '触达红线(对照 AGENTS.md,勾选仅标记触及范围;具体如何满足在同名 spec 或 Quick-Plan 中说明)', hint: '- [ ] <按本项目 AGENTS.md 红线逐行补;无则删本行>\n- [ ] 规则 / 契约变更(编码权威 / 共享契约 / 接口签名 / 既有参数语义 / 全局口径)→ 级别至少 L2\n- [ ] schema / 迁移 SQL / DI 链 / 认证与中间件管线 → 级别 L3\n\n> 触及任一红线即为 L2 / L3,必须立 ../specs/YYYY-MM-DD-<主题>.md 写明如何满足,方可起草 plan。' },
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
- export function renderIntent({ topic, module, level, notes, date = TODAY }) {
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
- body += '## ' + title + '\n\n' + hint + '\n\n';
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
- const { body } = renderIntent(a);
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);
@@ -9,7 +9,6 @@ import path from 'node:path';
9
9
  import { ENUMS } from './workflow-enums.mjs';
10
10
  import { draftGateFor, resolveWorkspace } from './stage-gates.mjs';
11
11
 
12
- const TODAY = new Date().toISOString().slice(0, 10);
13
12
  const LEVELS = ENUMS['level.all']; // 级别词表单源(.agents/workflow-enums.txt)
14
13
 
15
14
  function parseArgs(argv) {
@@ -40,7 +39,7 @@ const FULL_SECTIONS = [
40
39
  L1_SECTIONS[2],
41
40
  ];
42
41
 
43
- export function renderPlan({ topic, level = 'L1', date = TODAY }) {
42
+ export function renderPlan({ topic, level = 'L1' }) {
44
43
  if (!LEVELS.includes(level)) throw new Error('level 不在枚举 ' + LEVELS.join('|') + ':' + level);
45
44
  const sections = level === 'L1' ? L1_SECTIONS : FULL_SECTIONS;
46
45
  const fm = '---\n状态: draft\n级别: ' + level + '\n模块: pipeline\n---\n';
@@ -141,7 +141,7 @@ let out = indexText;
141
141
  {
142
142
  const lines = out.split(/\r?\n/);
143
143
  const headIdx = lines.findIndex((l) => /^\| 主题 \|/.test(l));
144
- if (headIdx < 0 || !/^[\|:\-\s]+$/.test(lines[headIdx + 1] || '')) {
144
+ if (headIdx < 0 || !/^[|:\-\s]+$/.test(lines[headIdx + 1] || '')) {
145
145
  console.error('❌ INDEX 速览表表头未找到,未写盘');
146
146
  process.exit(1);
147
147
  }
@@ -12,8 +12,6 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
12
12
  const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
13
13
  const { closeDurations, pickPct, durationVerdict, qualityRows, DURATION_BAND, normalizeForCheck, gateRoiLines } =
14
14
  await import(pathToFileURL(path.join(SCRIPT_DIR, 'gen-workflow-dashboard.mjs')).href);
15
- const agg = (await import(pathToFileURL(path.join(SCRIPT_DIR, 'agg-delegations.cjs')).href)).default
16
- ?? null; // .cjs 经 import 具名空间取——见下方 createRequire 兜底
17
15
  import { createRequire } from 'node:module';
18
16
  const require2 = createRequire(import.meta.url);
19
17
  const aggCjs = require2(path.join(SCRIPT_DIR, 'agg-delegations.cjs'));
@@ -124,7 +124,7 @@ const run = (root, args = []) => spawnSync(process.execPath, [GEN, ...args], { c
124
124
  '状态: draft\n级别: L1\n日期: 2026-09-22\n模块: pipeline',
125
125
  'workflow-doc-retrieval',
126
126
  ), 'utf8');
127
- const r = run(root);
127
+ run(root);
128
128
  const idx = fs.readFileSync(path.join(root, 'workflow', 'INDEX.md'), 'utf8');
129
129
  check('场景 5:kebab-case H1 完整保留(未被截成末段)', idx.includes('| workflow-doc-retrieval |') && !idx.includes('| retrieval |'), idx.split('\n').filter((l) => l.includes('retrieval')).join('\n'));
130
130
  check('场景 5:em-dash 前缀仍正常剥离', idx.includes('| 在跑的需求甲 |'), '');
@@ -48,7 +48,7 @@ const DOC_TYPES = ['intents', 'specs', 'plans', 'incidents'];
48
48
  // **不能用固定上溯级数**(2026-09-28 修正):本脚本在两处运行、深度不同——
49
49
  // 包源 `templates/_agents/scripts/` → 上溯 3 级到仓库根
50
50
  // 装户 `.agents/scripts/` → 上溯 2 级到安装根
51
- // 首版硬编码 3 级,故**在每个装户安装里都解析不到枚举文件**(报错路径 `<install>/..​/.agents/…`)。
51
+ // 首版硬编码 3 级,故**在每个装户安装里都解析不到枚举文件**(报错路径 `<install>/../.agents/…`)。
52
52
  // 实测暴露路径:CI 新增「shipped 套件」步骤后,`.agents/scripts/gen-workflow-metrics.test.mjs`
53
53
  // 场景 1 即 exit 1(workflow-enums 单源文件不可读),而包源侧同字节跑出全绿。
54
54
  // 改为**逐级上溯探测**:找到含 `.agents/workflow-enums.txt` 的那一级即用,找不到则回退标准位置让
@@ -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
- fs.writeFileSync(METRICS_P, out, 'utf8');
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} 个月)`);
@@ -39,7 +39,6 @@ const USAGE = `用法: node .agents/scripts/kb-search.mjs [选项] <词1> [词2
39
39
 
40
40
  const args = process.argv.slice(2);
41
41
  let scope = '';
42
- let scopeExplicit = false;
43
42
  let typeFilter = null;
44
43
  let moduleFilter = '';
45
44
  let statusFilter = 'all';
@@ -53,7 +52,7 @@ let rebuild = false;
53
52
  const words = [];
54
53
  for (let i = 0; i < args.length; i++) {
55
54
  const a = args[i];
56
- if (a === '--scope') { scope = args[++i] || ''; scopeExplicit = true; }
55
+ if (a === '--scope') { scope = args[++i] || ''; }
57
56
  else if (a === '--type') typeFilter = (args[++i] || '').split(',').map((s) => s.trim()).filter(Boolean);
58
57
  else if (a === '--module') moduleFilter = args[++i] || '';
59
58
  else if (a === '--status') statusFilter = args[++i] || '';
@@ -109,7 +109,7 @@ const files = (out) => (out.match(/^📄 (workflow\/\S+)/gm) || []).map((l) => l
109
109
  // ---- 场景 5:增量失效——warm 后改 1 个源文件,仅该文件重解析且输出与纯扫一致 ----
110
110
  {
111
111
  const root = mkfix();
112
- const base = run(root, ['编码模板']);
112
+ run(root, ['编码模板']);
113
113
  const planP = path.join(root, 'workflow', 'plans', '2026-09-20-ccc.md');
114
114
  fs.appendFileSync(planP, '\n## 影响面\n- 新标记NEWWORD:编码模板修订\n', 'utf8');
115
115
  const after = run(root, ['编码模板', 'NEWWORD']);