agentic-flow-kit 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -4
- package/modules/hosts/claude/commands/wf-test.md +1 -1
- package/modules/hosts/codex/commands/wf-test.md +1 -1
- package/modules/hosts/cursor/commands/wf-test.md +1 -1
- package/modules/hosts/opencode/commands/wf-test.md +1 -1
- package/modules/hosts/trae/commands/wf-test.md +1 -1
- package/package.json +1 -1
- package/templates/_agents/commands/test.md +1 -1
- package/templates/_agents/scripts/agg-delegations.cjs +41 -12
- package/templates/_agents/scripts/agg-delegations.test.mjs +76 -1
- package/templates/_agents/scripts/agg-gate-stats.mjs +113 -0
- package/templates/_agents/scripts/agg-gate-stats.test.mjs +95 -0
- package/templates/_agents/scripts/check-hygiene.mjs +167 -0
- package/templates/_agents/scripts/check-hygiene.test.mjs +182 -0
- package/templates/_agents/scripts/check-loop.mjs +85 -113
- package/templates/_agents/scripts/check-loop.test.mjs +114 -0
- package/templates/_agents/scripts/confirm-doc.mjs +27 -6
- package/templates/_agents/scripts/confirm-doc.test.mjs +35 -0
- package/templates/_agents/scripts/gate-seg.mjs +54 -0
- package/templates/_agents/scripts/gate-seg.test.mjs +112 -0
- package/templates/_agents/scripts/gen-workflow-dashboard.mjs +76 -9
- package/templates/_agents/scripts/gen-workflow-dashboard.test.mjs +64 -1
- package/templates/_agents/scripts/policy.mjs +31 -3
- package/templates/_agents/scripts/verify.mjs +27 -2
- package/templates/_agents/scripts/verify.test.mjs +53 -0
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
// L0/L1 勾触达红线 → 「红线判低」hard——协作道不受理 STOP 级改动,就高不就低,
|
|
18
18
|
// 2026-09-30 hybrid-governance-risk-lanes) [hard-block]
|
|
19
19
|
// 2. 模板字段占位符残留(YYYY-MM-DD / <主题> 等未替换;<主题> 与 .md 同行 = 命名约定描述,豁免) [warning]
|
|
20
|
+
// (2026-10-08 selfmeasure-and-modularize:实现已迁 check-hygiene.mjs,判据与文案未变)
|
|
20
21
|
// 3. incidents 复盘三件套完整性 + 状态严格枚举 + 新 intent 回路(回路断档=hard,其他=warning)
|
|
21
22
|
// 4. 引用有效性(文档/指令中引用的 .agents/ 路径必须存在;支持 fill-{a,b,c}.mjs 花括号展开与 fill-*.mjs 通配;
|
|
22
23
|
// workflow 文档仅扫活跃态——终态件的引用是历史叙述,不扫,2026-09-27 audit-gate-hardening) [warning]
|
|
@@ -29,11 +30,15 @@
|
|
|
29
30
|
// 非 git / 查不到加入记录 → 不可判定 → 走存量口径(不误报 hard)
|
|
30
31
|
// 证据可写在 [x] 行的续行(仓库通写法「(证据:…)」另起一行;全/半角冒号皆认)
|
|
31
32
|
// 9. 文件名英文 kebab-case(非 ASCII 文件名=warning,2026-09-11 规则)
|
|
33
|
+
// (2026-10-08 selfmeasure-and-modularize:实现已迁 check-hygiene.mjs,判据与文案未变)
|
|
32
34
|
// 10. 级别 vs 迁移文件一致性(L1/L2 入口文档加入提交触及迁移 SQL/Migrations=疑似判低,warning)
|
|
33
|
-
// 11. workflow/INDEX.md
|
|
35
|
+
// 11. 生成物漂移:workflow/INDEX.md(2026-09-21 检索层) + workflow/DASHBOARD.md(2026-10-08 扩覆盖面)=warning;
|
|
36
|
+
// 各自调对应生成器 --check,口径单一不复刻渲染;实现已迁 check-hygiene.mjs
|
|
34
37
|
// 12. frontmatter「模块:」合法性(枚举非法 / 2026-09-22 起新建缺字段=warning;词表单源 .agents/workflow-modules.txt)
|
|
38
|
+
// (2026-10-08 selfmeasure-and-modularize:实现已迁 check-hygiene.mjs,判据与文案未变)
|
|
35
39
|
// 13. 常驻面体积预算(超限=warning;判定单源 rule-budget.sh——经 sh 调用,
|
|
36
40
|
// 无 sh 环境静默跳过:advisory 级且 pre-commit 侧在 git 钩子 sh 环境照常硬拦)
|
|
41
|
+
// (2026-10-08 selfmeasure-and-modularize:实现已迁 check-hygiene.mjs,判据与文案未变)
|
|
37
42
|
// 14. 新 done 的 spec/plan 须在 git 历史里出现过 `状态: approved`(确认环节留痕,2026-09-22;恒 advisory 永不升级 hard)
|
|
38
43
|
// 15. 确认指纹对账(2026-09-27 起:approved/done 须 confirm-doc.mjs 确认指纹+台账配对,缺=hard-block;
|
|
39
44
|
// 两形态——TTY 亲手 / --delegated 对话委托代录,台账 source 如实区分,配对判据与 source 无关)
|
|
@@ -118,6 +123,11 @@ import { laneOfEntry } from './stage-gates.mjs';
|
|
|
118
123
|
// MARK_RE:incident 留痕形态的单源(第七轮复核 N3——此前 check-loop 内联复制一份同口径字面量,
|
|
119
124
|
// 两处靠注释与人工同步;改为复用 stage-gates.mjs 的导出,从结构上消除漂移可能)
|
|
120
125
|
import { MARK_RE, approvedTraceHit } from './stage-gates.mjs';
|
|
126
|
+
// 卫生类检查已拆出(2026-10-08 selfmeasure-and-modularize):检查 2/9/11/12/13 迁入 check-hygiene.mjs,
|
|
127
|
+
// 本文件只组装 ctx 并消费返回的文案数组(先例 = check-metric-claims.mjs 承载检查 16)。
|
|
128
|
+
// 调用点位置即输出顺序位:须留在检查 10 与 12 之间以保持 warnings 行序(稳定输出契约),见该模块内注释。
|
|
129
|
+
import { runCheckHygiene } from './check-hygiene.mjs';
|
|
130
|
+
import { gateSeg, finishSegs, makeCollector } from './gate-seg.mjs';
|
|
121
131
|
|
|
122
132
|
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
123
133
|
// CHECK_LOOP_GIT:测试注入钩子(指向不存在/不可执行路径可稳定触发 spawn 异常);未设置时与原行为逐字节一致(2026-10-05-gitout-fail-open)
|
|
@@ -125,18 +135,20 @@ const GIT = process.env.CHECK_LOOP_GIT || (process.platform === 'win32' ? 'git.e
|
|
|
125
135
|
|
|
126
136
|
function takeOpts() {
|
|
127
137
|
const args = process.argv.slice(2);
|
|
128
|
-
const opts = { rev: null, hardening: false };
|
|
138
|
+
const opts = { rev: null, hardening: false, gateStats: false };
|
|
129
139
|
for (let i = 0; i < args.length; i++) {
|
|
130
140
|
if (args[i] === '--rev') {
|
|
131
141
|
opts.rev = args[++i] ?? '';
|
|
132
142
|
if (!opts.rev) {
|
|
133
|
-
console.error('check-loop: 用法 node check-loop.mjs [--rev <sha>] [--hardening]');
|
|
143
|
+
console.error('check-loop: 用法 node check-loop.mjs [--rev <sha>] [--hardening] [--gate-stats]');
|
|
134
144
|
process.exit(1);
|
|
135
145
|
}
|
|
136
146
|
} else if (args[i] === '--hardening') {
|
|
137
147
|
opts.hardening = true; // 加固门(2026-09-30 hybrid-governance-explore-hardening):pre-push 对 main 目标传入
|
|
148
|
+
} else if (args[i] === '--gate-stats') {
|
|
149
|
+
opts.gateStats = true; // 门禁 ROI 落盘(2026-10-08 gate-roi-metrics):默认路径输出逐字节不变
|
|
138
150
|
} else {
|
|
139
|
-
console.error('check-loop: 用法 node check-loop.mjs [--rev <sha>] [--hardening]');
|
|
151
|
+
console.error('check-loop: 用法 node check-loop.mjs [--rev <sha>] [--hardening] [--gate-stats]');
|
|
140
152
|
process.exit(1);
|
|
141
153
|
}
|
|
142
154
|
}
|
|
@@ -148,6 +160,29 @@ function takeOpts() {
|
|
|
148
160
|
// 优先于 CHECK_LOOP_ROOT env 优先于 git toplevel(CLI 缺省行为逐字节不变);返回 { exitCode, blockers, warnings },
|
|
149
161
|
// 输出打印照旧在本函数内(稳定输出契约:banner 后空行、条目 `- ` 前缀、HARD-BLOCK/WARN 两段式——消费者 .githooks/pre-push)。
|
|
150
162
|
// opts.rev 的 worktree 清理挂 process.on('exit')——进程内多次调用累计监听器(测试场景 ≤3 次,可接受)。
|
|
163
|
+
// ---- 门禁 ROI 插桩件已抽至 gate-seg.mjs(2026-10-08 gate-roi-metrics)----
|
|
164
|
+
// 抽因:check-loop.mjs 已 import check-hygiene.mjs,后者若再 import check-loop.mjs 即循环依赖——
|
|
165
|
+
// 而两者都要插桩,判据单源必须只有一处。gateSeg / finishSegs / makeCollector 见该件;
|
|
166
|
+
// 本文件在每段开头调 markGate(id,label),统计由数组长度差推出,不动任何 push 调用点。
|
|
167
|
+
|
|
168
|
+
// writeGateStats:门禁 ROI 落盘(2026-10-08 gate-roi-metrics)——append 一行到
|
|
169
|
+
// <root>/.agents/cache/gate-stats.jsonl(该目录已 gitignore,属运行时数据、不入 git、不进 kit.json
|
|
170
|
+
// managed 台账——刻意避开 papercuts 2026-10-05 探针临时件被 sync 收编的事故面)。
|
|
171
|
+
// IO 失败只出账不阻断(沿 verify.mjs 凭证落账同款容错——度量失败不该让门禁变成红灯)。
|
|
172
|
+
export function writeGateStats(root, segs) {
|
|
173
|
+
try {
|
|
174
|
+
const dir = path.join(root, '.agents', 'cache');
|
|
175
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
176
|
+
const total = segs.reduce((a, s) => a + s.ms, 0);
|
|
177
|
+
fs.appendFileSync(path.join(dir, 'gate-stats.jsonl'),
|
|
178
|
+
`${JSON.stringify({ ts: new Date().toISOString(), root, segs, totalMs: total })}\n`);
|
|
179
|
+
const hits = segs.filter((s) => s.warns || s.blocks).length;
|
|
180
|
+
console.error(`[gate-stats] ${segs.length} 段已记账(命中 ${hits} 段)→ .agents/cache/gate-stats.jsonl`);
|
|
181
|
+
} catch (e) {
|
|
182
|
+
console.error(`[gate-stats] ⚠️ 落盘失败(${e.code || e.message})——不阻断门禁`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
151
186
|
export function runCheckLoop(opts = {}) {
|
|
152
187
|
const REV_ARG = opts.rev ?? null;
|
|
153
188
|
const HARDENING = !!opts.hardening;
|
|
@@ -293,6 +328,10 @@ export function runCheckLoop(opts = {}) {
|
|
|
293
328
|
const DOC_DIRS = ['intents', 'specs', 'plans', 'incidents'];
|
|
294
329
|
const blockers = [];
|
|
295
330
|
const warnings = [];
|
|
331
|
+
// 门禁 ROI 收集器(2026-10-08 gate-roi-metrics):引用同一对数组 + 段游标,不复制任何数据。
|
|
332
|
+
// 默认路径不打印不写盘(见 gate-seg.mjs 注释的输出契约硬约束)。
|
|
333
|
+
const gateStats = makeCollector(warnings, blockers);
|
|
334
|
+
const markGate = (id, label) => gateSeg(gateStats, id, label); // 局部别名,避免遮蔽同名导出
|
|
296
335
|
// audit:false(init 新装)只保留 blockers。缺省与 audit:true 保持全量警告。
|
|
297
336
|
// 硬规则:配对 / 验收勾验的阻断 / 确认留痕 / 发版草稿。卫生项走 warnings。
|
|
298
337
|
const kitPolicy = loadKitPolicy(ROOT);
|
|
@@ -318,6 +357,7 @@ export function runCheckLoop(opts = {}) {
|
|
|
318
357
|
};
|
|
319
358
|
|
|
320
359
|
// --- 1. intent/spec/plan 同名配对 + 状态确认 + L3 确认三件 + 风险泳道一致性 [hard-block] ---
|
|
360
|
+
markGate('1', '意图/spec/plan 配对');
|
|
321
361
|
for (const intent of docFiles('intents')) {
|
|
322
362
|
const base = path.basename(intent);
|
|
323
363
|
const st = fmGet(intent, '状态');
|
|
@@ -383,51 +423,17 @@ export function runCheckLoop(opts = {}) {
|
|
|
383
423
|
}
|
|
384
424
|
}
|
|
385
425
|
|
|
386
|
-
// --- 2
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
// 「复制本模板为 YYYY-MM-DD-<主题>.md」),非未填占位符;真未填的占位(标题 `# INTENT — <主题>`、
|
|
396
|
-
// `日期: YYYY-MM-DD`)不含 .md,仍照拦(2026-09-25-wf-runtime incident 记录的误报口径)
|
|
397
|
-
const isNamingConv = (line) => line.includes('<主题>') && /\.md/.test(line);
|
|
398
|
-
// 样例引用豁免(2026-10-02 caliber-convergence 假阳性消除,先例=检查 18 行尾归一):命中位于行内代码
|
|
399
|
-
// (反引号包裹)或围栏代码块内 = 引用样例而非未填占位符,不报;正文裸占位符照报——真占位符均为
|
|
400
|
-
// 裸文本(模板正文无反引号包裹),检测面不缩。
|
|
401
|
-
// 日期显示格式豁免(2026-10-06 check2-datetime-literal-exempt):`YYYY-MM-DD HH:mm(:ss)` 是功能
|
|
402
|
-
// 自实现的显示格式描述(文档叙述「匹配时间列显示为 YYYY-MM-DD HH:mm」属常态——cancel-export
|
|
403
|
-
// intent/plan 实证 2 条 advisory、2026-10-06 loop-audit intent 现场复现),非未填占位——行内剔除
|
|
404
|
-
// 后再判;真占位(`日期: YYYY-MM-DD`,无时间后缀)不含该形态仍拦。只豁免实证形态,变体(斜杠日期
|
|
405
|
-
// /十二小时制等)出现假阳性再扩。
|
|
406
|
-
const stripSamples = (lines) => {
|
|
407
|
-
let inFence = false;
|
|
408
|
-
return lines.map((line) => {
|
|
409
|
-
if (/^\s*(```|~~~)/.test(line)) { inFence = !inFence; return ''; }
|
|
410
|
-
if (inFence) return '';
|
|
411
|
-
return line.replace(/`[^`]*`/g, '').replace(/YYYY-MM-DD\s+HH:mm(:ss)?/g, '');
|
|
412
|
-
});
|
|
413
|
-
};
|
|
414
|
-
const groups = new Map(); // file -> [ "行号:内容" ](首现序)
|
|
415
|
-
for (const sub of DOC_DIRS) {
|
|
416
|
-
for (const f of docFiles(sub)) {
|
|
417
|
-
const lines = linesOf(f) || [];
|
|
418
|
-
const hits = [];
|
|
419
|
-
stripSamples(lines).forEach((line, i) => {
|
|
420
|
-
if (phRe.test(line) && !boilerRe.test(line) && !isNamingConv(line)) hits.push(`${i + 1}:${lines[i]}`);
|
|
421
|
-
});
|
|
422
|
-
if (hits.length) groups.set(f, hits);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
for (const [f, hits] of groups) {
|
|
426
|
-
warnings.push(`- [WARN 模板未填] ${path.relative(ROOT, f).split(path.sep).join('/')} 含模板占位符:\n${hits.join('\n')}`);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
426
|
+
// --- 卫生类检查 2 / 9 / 11 / 12 / 13(2026-10-08 selfmeasure-and-modularize 拆出)---
|
|
427
|
+
// ⚠️ 调用点位置即输出顺序位:原实现分处五处,逐条顺序 2 → 9 → 11 → 12 → 13,而 warnings 按插入序输出、
|
|
428
|
+
// 行序属稳定输出契约(pre-push / pre-commit 消费者 + doctor 按 WARN 行计数)。拆成一次调用后本块统一落在
|
|
429
|
+
// 原检查 2 的位置,**块内顺序由 check-hygiene.mjs 保证**(与其内注释互引)。新增检查请按原编号插入
|
|
430
|
+
// 到正确位置,勿一律追加末尾。
|
|
431
|
+
// ctx 只传既有 helper、模块内零复刻:docFiles 的 tracked 过滤、fmGet 的 frontmatter 受限子集、
|
|
432
|
+
// linesOf 的读异常响亮出账各自承载不可漂移语义,复刻即造口径分叉。audit:false 下 warnings.push 是
|
|
433
|
+
// no-op,卫生项因此不出账(硬规则语义保持)。
|
|
434
|
+
warnings.push(...runCheckHygiene({ root: ROOT, WF, DOC_DIRS, docFiles, linesOf, fmGet, isTracked, readdirOrNull, kitPolicy, gateStats }));
|
|
430
435
|
// --- 3. incidents 复盘三件套 + 状态严格枚举 + 新 intent 回路(回路断档 = hard)---
|
|
436
|
+
markGate('3', 'incidents 复盘三件套');
|
|
431
437
|
for (const inc of docFiles('incidents')) {
|
|
432
438
|
const name = path.basename(inc);
|
|
433
439
|
const lines = linesOf(inc) || [];
|
|
@@ -472,6 +478,7 @@ export function runCheckLoop(opts = {}) {
|
|
|
472
478
|
}
|
|
473
479
|
|
|
474
480
|
// --- 4. 引用有效性 [warning](.agents/ 路径须存在;skills 特例允许用户级 ~/.agents/skills)---
|
|
481
|
+
markGate('4', '引用有效性');
|
|
475
482
|
{
|
|
476
483
|
// 引用可带 {a,b,c} 花括号展开(仓库通写法 `fill-{intent,spec,plan}.mjs`)或 `*` 通配(`fill-*.mjs`)——
|
|
477
484
|
// 花括号展开逐路查存在,任一路存在即有效;`*` 按目录枚举 glob 匹配,命中即有效;全不中才算断档
|
|
@@ -535,6 +542,7 @@ export function runCheckLoop(opts = {}) {
|
|
|
535
542
|
}
|
|
536
543
|
|
|
537
544
|
// --- 5. 子智能体角色契约 + 宿主 Adapter 一致性 + 旧委派残留 + 钉死模型 [warning] ---
|
|
545
|
+
markGate('5', '角色契约与宿主 Adapter');
|
|
538
546
|
{
|
|
539
547
|
const cmdFiles = () => (readdirOrNull(path.join(ROOT, '.agents', 'commands')) || [])
|
|
540
548
|
.filter((f) => f.endsWith('.md')).map((f) => `.agents/commands/${f}`);
|
|
@@ -593,6 +601,7 @@ export function runCheckLoop(opts = {}) {
|
|
|
593
601
|
}
|
|
594
602
|
|
|
595
603
|
// --- 6. 阶段索引同步 [warning] ---
|
|
604
|
+
markGate('6', '阶段索引同步');
|
|
596
605
|
for (const cmd of ['plan', 'design', 'build', 'test', 'deploy', 'maintain', 'review']) {
|
|
597
606
|
for (const doc of ['AGENTS.md', '.agents/commands/new-task.md']) {
|
|
598
607
|
// existsSync 守卫(2026-10-06-check19-entry-enoent 同类):两文件可合法缺失(下方 text 判空本就
|
|
@@ -744,6 +753,7 @@ export function runCheckLoop(opts = {}) {
|
|
|
744
753
|
}
|
|
745
754
|
|
|
746
755
|
// --- 8. intent 验收标准对账(done 须逐条勾验并补证据;新建 hard,存量聚合 warning)---
|
|
756
|
+
markGate('8', '验收标准对账');
|
|
747
757
|
// 生效日锚 2026-09-28 改「git 首次加入日期」(此前取文件名前 10 字符——命名规范强制的字段、
|
|
748
758
|
// 写早零成本,见 incidents/2026-09-28-check8-git-anchor)。非 git → 不可判定 → 走存量口径(不误报 hard)。
|
|
749
759
|
{
|
|
@@ -752,15 +762,24 @@ export function runCheckLoop(opts = {}) {
|
|
|
752
762
|
// 测试绿凭证状态(2026-10-07 verify-evidence):台账读一次循环外缓存;policy 缺 verifySince
|
|
753
763
|
//(v1-v4)→ ctxBase null → 对账整体跳过(向后兼容)
|
|
754
764
|
const verifySinceOf = kitPolicy.verifySince;
|
|
755
|
-
|
|
765
|
+
// 台账内容循环外读一次并缓存(doc 过滤是纯内存行匹配,不新增子进程——门禁成本零增加)。
|
|
766
|
+
const verifyLines = (() => {
|
|
756
767
|
try {
|
|
757
768
|
const vp = path.join(ROOT, '.agents', 'verifications.jsonl');
|
|
758
|
-
return fs.existsSync(vp) ?
|
|
759
|
-
} catch { return
|
|
769
|
+
return fs.existsSync(vp) ? fs.readFileSync(vp, 'utf8').split(/\r?\n/) : null;
|
|
770
|
+
} catch { return null; }
|
|
760
771
|
})();
|
|
772
|
+
// 全局绿行(v5 语义):仅供 v1-v5 装户走原路径,本仓 v6+ 不再消费它。
|
|
773
|
+
const verifyFreshGlobal = verifyLines ? hasFreshVerifyLine(verifyLines) : false;
|
|
774
|
+
// 2026-10-08 verify-doc-binding:v6+(verifyDocSince)按本 doc 精确匹配;缺键维持全局布尔。
|
|
775
|
+
const verifyDocBound = typeof kitPolicy.verifyDocSince === 'string';
|
|
776
|
+
const verifyFreshFor = (rel) => (verifyLines
|
|
777
|
+
? hasFreshVerifyLine(verifyLines, Date.now(), verifyDocBound ? rel : null)
|
|
778
|
+
: false);
|
|
761
779
|
for (const intent of docFiles('intents')) {
|
|
762
780
|
if (fmGet(intent, '状态') !== 'done') continue;
|
|
763
781
|
const base = path.basename(intent);
|
|
782
|
+
const rel = path.relative(ROOT, intent).split(path.sep).join('/'); // 与 verify.mjs --doc 落账口径同源
|
|
764
783
|
const lines = linesOf(intent) || [];
|
|
765
784
|
// 节扫描:hs=有验收节;insec 至下一个二级标题;uc=未勾项;ne=勾选缺证据;ex=存量豁免声明
|
|
766
785
|
// 证据可写在 [x] 行的续行(仓库通写法「(证据:…)」另起一行)——pendX 记「待证据项」,
|
|
@@ -811,12 +830,14 @@ export function runCheckLoop(opts = {}) {
|
|
|
811
830
|
// verify 对账 ctx:intent 首次加入 git ≥ verifySince(v5 锚)才参与;不可判定 → null 走 text 豁免
|
|
812
831
|
const iadd = addedDateOf(intent);
|
|
813
832
|
const vctx = typeof verifySinceOf === 'string' && iadd !== '' && iadd >= verifySinceOf
|
|
814
|
-
? { verifyRelevant: true, hasFresh:
|
|
833
|
+
? { verifyRelevant: true, hasFresh: verifyDocBound ? verifyFreshFor(rel) : verifyFreshGlobal } : null;
|
|
815
834
|
const results = evidenceItems.map((ev) => ({ t: verifyEvidenceTruth(ev, planBase, ROOT, vctx) }));
|
|
816
835
|
const hasImplEvidence = results.some((r) => r.t.ok && (r.t.type === 'sha' || r.t.type === 'process'));
|
|
817
836
|
for (const r of results) {
|
|
818
837
|
if (r.t.ok && r.t.type === 'verify-missing') {
|
|
819
|
-
|
|
838
|
+
// ⚠️ 文案锚点硬约束(既有 check-loop.test.mjs:2743 锚定「测试绿缺凭证」四字连续,零改动前提):
|
|
839
|
+
// doc 与修复命令只能追加在整句之后,不得插进这四个字中间。
|
|
840
|
+
warnings.push(`- [WARN 测试绿缺凭证] ${base} 验收证据声明「测试绿」但 24h 内无 verify 凭证(verifications.jsonl${verifyDocBound ? `;本单须带 --doc 落账` : ''})——跑 node .agents/scripts/verify.mjs${verifyDocBound ? ` --doc ${rel}` : ''} 后重跑(advisory,灰度第一档)`);
|
|
820
841
|
continue;
|
|
821
842
|
}
|
|
822
843
|
if (r.t.ok) continue;
|
|
@@ -851,20 +872,8 @@ export function runCheckLoop(opts = {}) {
|
|
|
851
872
|
}
|
|
852
873
|
}
|
|
853
874
|
|
|
854
|
-
// --- 9. 文件名英文 kebab-case [warning] ---
|
|
855
|
-
for (const sub of DOC_DIRS) {
|
|
856
|
-
const dir = path.join(ROOT, WF, sub);
|
|
857
|
-
for (const f of readdirOrNull(dir) || []) {
|
|
858
|
-
if (!f.endsWith('.md') || f === '_TEMPLATE.md') continue;
|
|
859
|
-
const abs = path.join(dir, f);
|
|
860
|
-
if (!isTracked(abs)) continue;
|
|
861
|
-
if (!/^[\x20-\x7e]*$/.test(f)) {
|
|
862
|
-
warnings.push(`- [WARN 文件名非英文] 文件名含非 ASCII 字符,应为英文 kebab-case:${WF}/${sub}/${f}`);
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
|
|
867
875
|
// --- 10. 级别 vs 迁移文件一致性 [warning](启发式:入口文档加入提交触及迁移 SQL/Migrations → 疑似判低;非 git 跳过)---
|
|
876
|
+
markGate('10', '级别 vs 迁移文件');
|
|
868
877
|
if (gitOut(['rev-parse', '--git-dir']) !== null) {
|
|
869
878
|
const log = gitOut(['log', '--diff-filter=A', '--format=@%H', '--name-only']);
|
|
870
879
|
if (log !== null) {
|
|
@@ -893,54 +902,8 @@ export function runCheckLoop(opts = {}) {
|
|
|
893
902
|
}
|
|
894
903
|
}
|
|
895
904
|
|
|
896
|
-
// --- 11. workflow/INDEX.md 漂移 [warning](调生成器 --check,口径单一不复刻渲染)---
|
|
897
|
-
{
|
|
898
|
-
const gen = path.join(ROOT, '.agents', 'scripts', 'gen-workflow-index.mjs');
|
|
899
|
-
if (fs.existsSync(gen)) {
|
|
900
|
-
const r = spawnSync(process.execPath, [gen, '--check'], { cwd: ROOT, encoding: 'utf8' });
|
|
901
|
-
if (r.status !== 0) {
|
|
902
|
-
const first = `${r.stdout || ''}${r.stderr || ''}`.split('\n')[0];
|
|
903
|
-
warnings.push(`- [WARN 索引漂移] workflow/INDEX.md 与磁盘不一致——跑 node .agents/scripts/gen-workflow-index.mjs 重新生成(${first})`);
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
// --- 12. frontmatter「模块:」合法性 [warning](词表单源;缺字段只对 2026-09-22 起新建提示)---
|
|
909
|
-
{
|
|
910
|
-
const modsFile = path.join(ROOT, '.agents', 'workflow-modules.txt');
|
|
911
|
-
if (fs.existsSync(modsFile)) {
|
|
912
|
-
const mods = linesOf(modsFile).filter((l) => l.trim() && !l.startsWith('#')).map((l) => l.trim());
|
|
913
|
-
const vocab = new Set(mods);
|
|
914
|
-
for (const sub of ['intents', 'specs', 'plans', 'incidents']) {
|
|
915
|
-
for (const doc of docFiles(sub)) {
|
|
916
|
-
const base = path.basename(doc);
|
|
917
|
-
const mod = fmGet(doc, '模块');
|
|
918
|
-
let d = fmGet(doc, '日期') || fmGet(doc, '发现');
|
|
919
|
-
if (!d) d = /^\d{4}-\d{2}-\d{2}$/.test(base.slice(0, 10)) ? base.slice(0, 10) : '';
|
|
920
|
-
if (mod) {
|
|
921
|
-
if (!vocab.has(mod)) warnings.push(`- [WARN 模块元数据] ${base}「模块: ${mod}」不在词表(见 .agents/workflow-modules.txt)`);
|
|
922
|
-
} else if (/^\d{4}-\d{2}-\d{2}$/.test(d) && d >= kitPolicy.moduleSince) {
|
|
923
|
-
warnings.push(`- [WARN 模块元数据] ${base} 缺「模块:」字段(${kitPolicy.moduleSince} 起新建文档必填)`);
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
// --- 13. 常驻面体积预算 [warning](判定单源 rule-budget.sh;无 sh 环境静默跳过——advisory,pre-commit 硬拦兜底)---
|
|
931
|
-
{
|
|
932
|
-
const rb = path.join(ROOT, '.agents', 'scripts', 'rule-budget.sh');
|
|
933
|
-
const budgets = path.join(ROOT, '.agents', 'rule-budgets.txt');
|
|
934
|
-
if (fs.existsSync(rb) && fs.existsSync(budgets)) {
|
|
935
|
-
const r = spawnSync('sh', [rb, '--all'], { cwd: ROOT, encoding: 'utf8' });
|
|
936
|
-
if (!r.error && r.status !== 0) {
|
|
937
|
-
const head = `${r.stdout || ''}${r.stderr || ''}`.split('\n').slice(0, 5).map((l) => ' ' + l).join('\n');
|
|
938
|
-
warnings.push(`- [WARN 常驻面超限] 常驻面体积超预算(先删除或下沉被取代条目再增——一进一出):\n${head}`);
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
|
|
943
905
|
// --- 14. 新 done 的 spec/plan 须有确认留痕:git 历史「状态: approved」 或 confirmations.jsonl 台账 approved 行 [warning](恒 advisory;非 git 跳过)---
|
|
906
|
+
markGate('14', '确认留痕(approved 快照)');
|
|
944
907
|
// 2026-10-08 papercuts-cleanup-batch 修 3:两跳确认同批提交时 git 历史不出现行首「状态: approved」曾误报,
|
|
945
908
|
// 而台账已证明两跳均走(2026-09-28-adopter-derivers 实证)——判据改 OR 并集(git 判据保留不弱化,
|
|
946
909
|
// 台账为第二通道;确认事件以台账为准)。台账伪造面由检查 15 指纹 hard 门把关,本修仅消 advisory 误报。
|
|
@@ -982,6 +945,7 @@ export function runCheckLoop(opts = {}) {
|
|
|
982
945
|
// --- 15. 确认指纹对账 [hard-block](2026-09-26 confirm-gate-machine;2026-09-27 confirm-gate-delegated 两形态;
|
|
983
946
|
// 生效日锚 2026-09-28 改台账 ts——此前锚取自文档自报「日期/发现」,新档只要把日期写早即整段跳过判定,
|
|
984
947
|
// incident 2026-09-28-confirm-gate-effective-date-anchor 实证)---
|
|
948
|
+
markGate('15', '确认指纹对账');
|
|
985
949
|
// 确认两形态(confirm-doc.mjs):TTY 亲手键入「可以」/ --delegated 对话委托代录(用户对话内明确放行后
|
|
986
950
|
// AI 代录,台账行如实记 source=chat-delegated + quote 原话,永不伪装 TTY)。intents/specs/plans 凡
|
|
987
951
|
// approved/done 须有 confirm-doc 产生的 frontmatter 确认指纹 + 台账(.agents/confirmations.jsonl)
|
|
@@ -1099,6 +1063,7 @@ export function runCheckLoop(opts = {}) {
|
|
|
1099
1063
|
}
|
|
1100
1064
|
|
|
1101
1065
|
// --- 16. 量化断言指标签名对账 [warning](2026-09-28 claim-exceeds-fix;登记表单源 .agents/metric-claims.txt)---
|
|
1066
|
+
markGate('16', '量化断言签名对账');
|
|
1102
1067
|
// 判据要点:活跃态文档(draft/approved/open)中的 `{{指标名}}`(小写点分)签名须替换为实时值,留签名=未回填=
|
|
1103
1068
|
// warning;只查显式签名、不全文扫数字(历史叙述假阳性恒 0);未登记签名 / 取数器缺失 → fail-loud 出账;
|
|
1104
1069
|
// 登记表缺失 → 静默跳过(未启用该检查的装户零噪声);装户自有指标走 .agents/metric-derivers.cjs(CJS 契约,
|
|
@@ -1109,6 +1074,7 @@ export function runCheckLoop(opts = {}) {
|
|
|
1109
1074
|
runCheck16({ ROOT, ENUMS, docFiles, fmGet, inSet, isTracked, linesOf, readdirOrNull, warnings });
|
|
1110
1075
|
|
|
1111
1076
|
// --- 17. 发版提交树上仍未收口的 intent/spec/plan [hard-block] ---
|
|
1077
|
+
markGate('17', '发版树未收口文档');
|
|
1112
1078
|
// 最近一次 package.json version 发生变化的提交里,当时状态已是 draft 或 approved 的
|
|
1113
1079
|
// intent/spec/plan,被扫描的树上仍是 draft 或 approved 则阻断。
|
|
1114
1080
|
// 当时已是 draft:不看版本锚。当时已是 approved:只在发版版本大于
|
|
@@ -1148,6 +1114,7 @@ export function runCheckLoop(opts = {}) {
|
|
|
1148
1114
|
}
|
|
1149
1115
|
|
|
1150
1116
|
// --- 18. 委派台账对账 [warning] ---
|
|
1117
|
+
markGate('18', '委派台账对账');
|
|
1151
1118
|
// L2/L3 的 intent、spec、plan 状态为 done,或同级别 incident 状态为 fixed 或 closed 时,
|
|
1152
1119
|
// 「委派结果」表与「自做任务结果」表均没有日期不早于文档日期、且含该文件名(带/不带 .md 均匹配)的一行,则警告。
|
|
1153
1120
|
// 缺级别、缺日期、以及其他状态不警告。audit:false 时 warnings.push 已被换成空函数。
|
|
@@ -1177,6 +1144,7 @@ export function runCheckLoop(opts = {}) {
|
|
|
1177
1144
|
}
|
|
1178
1145
|
|
|
1179
1146
|
// --- 19. 逐阶段审计 [warning](2026-09-30 stage-gate-machine;口径与 stage-gates.mjs / fill-* / confirm-doc 前置门互引) ---
|
|
1147
|
+
markGate('19', '逐阶段审计');
|
|
1180
1148
|
// 判据 A(在途扫描):同名 spec/plan 为 draft 且其日期 ≥ stageGateSince 时——入口未确认(intent:状态非
|
|
1181
1149
|
// approved/done、或缺台账行且非存量;incident:「时间线」小节缺行首「用户确认」条目)→ warning;
|
|
1182
1150
|
// 入口缺合法「级别」→ warning(与起草门 fail-closed 同口径,2026-09-30 复核 P2-7);
|
|
@@ -1352,6 +1320,7 @@ export function runCheckLoop(opts = {}) {
|
|
|
1352
1320
|
}
|
|
1353
1321
|
|
|
1354
1322
|
// --- 20. 引擎脚本测试覆盖 [warning](2026-10-01 gate-script-test-coverage;仅包源环境) ---
|
|
1323
|
+
markGate('20', '引擎脚本测试覆盖');
|
|
1355
1324
|
// 包源开发纪律:templates/_agents/scripts/ 下每个非 *.test.mjs 引擎脚本须有同名兄弟 .test.mjs,
|
|
1356
1325
|
// 或在 .agents/scripts-test-exempt.txt 登记豁免(managed 下发)。装户(无 templates/)整体跳过。
|
|
1357
1326
|
{
|
|
@@ -1375,6 +1344,9 @@ export function runCheckLoop(opts = {}) {
|
|
|
1375
1344
|
}
|
|
1376
1345
|
|
|
1377
1346
|
// --- 输出(banner 沿 sh 版字样与格式 = 稳定输出契约:banner 后空行、条目 '- ' 前缀)---
|
|
1347
|
+
// 门禁 ROI 收口(2026-10-08 gate-roi-metrics):末段结算(末段之后无下次 gateSeg 调用)+ 可选落盘。
|
|
1348
|
+
// ⚠️ 结算在输出**之前**:否则检查 20 之后的段不计入。落盘只在 --gate-stats 时发生。
|
|
1349
|
+
if (opts.gateStats) writeGateStats(ROOT, finishSegs(gateStats));
|
|
1378
1350
|
if (blockers.length) {
|
|
1379
1351
|
process.stderr.write(`闭环骨架断档(check-loop.sh)— HARD-BLOCK:\n\n${blockers.join('\n')}\n\n`);
|
|
1380
1352
|
if (warnings.length) process.stderr.write(`WARN(advisory,不阻断):\n\n${warnings.join('\n')}\n`);
|
|
@@ -423,6 +423,37 @@ const mkConfirmedDoc = (root, rel, fmBody) => {
|
|
|
423
423
|
rmfix(T);
|
|
424
424
|
}
|
|
425
425
|
|
|
426
|
+
// ---- 场景 27b:DASHBOARD 漂移门(2026-10-08 selfmeasure-and-modularize 检查 11 扩覆盖面)----
|
|
427
|
+
// 三态:陈旧盘面 → WARN;盘面缺失(装户从未生成过看板)→ 不报(防常驻噪声);一致 → 不报。
|
|
428
|
+
{
|
|
429
|
+
const mkDashFix = () => {
|
|
430
|
+
const T = mkfix();
|
|
431
|
+
fs.mkdirSync(path.join(T, '.agents', 'scripts'), { recursive: true });
|
|
432
|
+
for (const f of ['gen-workflow-index.mjs', 'gen-workflow-dashboard.mjs', 'agg-delegations.cjs', 'workflow-enums.mjs']) {
|
|
433
|
+
fs.copyFileSync(path.join(SCRIPT_DIR, f), path.join(T, '.agents', 'scripts', f));
|
|
434
|
+
}
|
|
435
|
+
fs.copyFileSync(path.join(SCRIPT_DIR, '..', 'workflow-modules.txt'), path.join(T, '.agents', 'workflow-modules.txt'));
|
|
436
|
+
w(T, 'workflow/intents/2026-09-22-dash.md', INTENT('dash', '状态: approved\n级别: L1\n日期: 2026-09-22\n模块: pipeline'));
|
|
437
|
+
w(T, 'workflow/plans/2026-09-22-dash.md', PLAN('dash', '状态: approved\n级别: L1\n模块: pipeline'));
|
|
438
|
+
spawnSync(process.execPath, ['.agents/scripts/gen-workflow-index.mjs'], { cwd: T });
|
|
439
|
+
return T;
|
|
440
|
+
};
|
|
441
|
+
// ① DASHBOARD.md 从未生成(装户形态)→ 不得出 WARN
|
|
442
|
+
let T = mkDashFix();
|
|
443
|
+
let r = run(T);
|
|
444
|
+
check('观测面:装户未生成过 DASHBOARD.md → 不出仪表盘漂移 WARN(防常驻噪声)',
|
|
445
|
+
r.status === 0 && !outOf(r).includes('WARN 仪表盘漂移'), outOf(r));
|
|
446
|
+
rmfix(T);
|
|
447
|
+
// ② 生成后改事实 → 漂移 WARN,且 exit 0(advisory 不阻断)
|
|
448
|
+
T = mkDashFix();
|
|
449
|
+
spawnSync(process.execPath, ['.agents/scripts/gen-workflow-dashboard.mjs'], { cwd: T });
|
|
450
|
+
w(T, 'workflow/intents/2026-09-22-dash.md', INTENT('dash', '状态: done\n级别: L1\n日期: 2026-09-22\n模块: pipeline'));
|
|
451
|
+
r = run(T);
|
|
452
|
+
check('观测面:DASHBOARD 漂移 → WARN 仪表盘漂移 且 exit 0',
|
|
453
|
+
r.status === 0 && outOf(r).includes('WARN 仪表盘漂移') && outOf(r).includes('gen-workflow-dashboard.mjs'), outOf(r));
|
|
454
|
+
rmfix(T);
|
|
455
|
+
}
|
|
456
|
+
|
|
426
457
|
// ---- 场景 28/29:常驻面预算——合规无告警;AGENTS.md 超限 → WARN 含一进一出 ----
|
|
427
458
|
{
|
|
428
459
|
const T = mkfix();
|
|
@@ -2721,6 +2752,54 @@ w(T, 'workflow/intents/2026-09-12-deep.md', INTENT('deep', '状态: draft\n级
|
|
|
2721
2752
|
rmfix(T2);
|
|
2722
2753
|
}
|
|
2723
2754
|
|
|
2755
|
+
// ---- 检查 8 凭证按 doc 绑定(2026-10-08 verify-doc-binding):v6 收紧 / v5 不变 ----
|
|
2756
|
+
// 漏洞本体:凭证行不带 doc 字段 + 判定只看「全仓任意一份 24h 内绿行」→ 跑一次 verify 就能给全仓
|
|
2757
|
+
// 任意一单的「测试绿」声明背书。本组三条:① v6 + 只有他单绿行 → 仍出 WARN(漏洞已堵);
|
|
2758
|
+
// ② v6 + 本单绿行 → 豁免零 WARN;③ **v5 + 只有他单绿行 → 无 WARN**(兼容反例,收紧只对 v6+ 生效)。
|
|
2759
|
+
{
|
|
2760
|
+
const mkVeDoc = (pv, docField) => {
|
|
2761
|
+
const T = mkfix();
|
|
2762
|
+
gitInit(T);
|
|
2763
|
+
w(T, '.agents/kit.json', JSON.stringify({ policyVersion: pv }));
|
|
2764
|
+
const green = { ts: new Date().toISOString(), exitCode: 0, suite: 'npm test' };
|
|
2765
|
+
if (docField) green.doc = docField;
|
|
2766
|
+
w(T, '.agents/verifications.jsonl', `${JSON.stringify(green)}\n`);
|
|
2767
|
+
const iBody = '# INTENT — ve\n\n## 验收标准(可测试)\n- [x] 用例通过(证据:测试全绿)\n';
|
|
2768
|
+
const pBody = '# PLAN — ve\n';
|
|
2769
|
+
const fpI = computeFingerprint(`---\n状态: approved\n级别: L1\n日期: 2026-10-07\n---\n${iBody}`);
|
|
2770
|
+
const fpP = computeFingerprint(`---\n状态: approved\n级别: L1\n---\n${pBody}`);
|
|
2771
|
+
w(T, 'workflow/intents/2026-10-07-ve.md', `---\n状态: done\n级别: L1\n日期: 2026-10-07\n确认指纹: ${fpI.slice(0, 16)}\n---\n${iBody}`);
|
|
2772
|
+
w(T, 'workflow/plans/2026-10-07-ve.md', `---\n状态: done\n级别: L1\n确认指纹: ${fpP.slice(0, 16)}\n---\n${pBody}`);
|
|
2773
|
+
const now = new Date().toISOString();
|
|
2774
|
+
writeLedger(T, [
|
|
2775
|
+
{ ts: now, doc: 'workflow/intents/2026-10-07-ve.md', stage: 'done', fingerprint: fpI, prev: 'approved' },
|
|
2776
|
+
{ ts: now, doc: 'workflow/plans/2026-10-07-ve.md', stage: 'done', fingerprint: fpP, prev: 'approved' },
|
|
2777
|
+
]);
|
|
2778
|
+
gitCommitAll(T, 'feat: ve doc-binding fixture');
|
|
2779
|
+
return T;
|
|
2780
|
+
};
|
|
2781
|
+
const MINE = 'workflow/intents/2026-10-07-ve.md';
|
|
2782
|
+
const OTHER = 'workflow/intents/2026-10-07-someone-else.md';
|
|
2783
|
+
|
|
2784
|
+
const D1 = mkVeDoc(6, OTHER);
|
|
2785
|
+
let r = run(D1);
|
|
2786
|
+
check('检查8 凭证绑定:v6 + 只有**他单**绿行 → 仍出 WARN(跨单背书漏洞已堵)',
|
|
2787
|
+
r.status === 0 && outOf(r).includes('测试绿缺凭证') && !outOf(r).includes('证据豁免 verify'), `exit=${r.status}\n${outOf(r)}`);
|
|
2788
|
+
rmfix(D1);
|
|
2789
|
+
|
|
2790
|
+
const D2 = mkVeDoc(6, MINE);
|
|
2791
|
+
r = run(D2);
|
|
2792
|
+
check('检查8 凭证绑定:v6 + 本单绿行 → verify 豁免零 WARN(绑定成立)',
|
|
2793
|
+
r.status === 0 && outOf(r).includes('证据豁免 verify') && !outOf(r).includes('测试绿缺凭证'), `exit=${r.status}\n${outOf(r)}`);
|
|
2794
|
+
rmfix(D2);
|
|
2795
|
+
|
|
2796
|
+
const D3 = mkVeDoc(5, OTHER);
|
|
2797
|
+
r = run(D3);
|
|
2798
|
+
check('检查8 凭证绑定:v5 + 只有他单绿行 → **无 WARN**(兼容反例:收紧只对 v6+ 生效)',
|
|
2799
|
+
r.status === 0 && !outOf(r).includes('测试绿缺凭证') && outOf(r).includes('证据豁免 verify'), `exit=${r.status}\n${outOf(r)}`);
|
|
2800
|
+
rmfix(D3);
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2724
2803
|
// ---- 检查 18 表头签名识别(2026-10-08 papercuts-cleanup-batch 修 2):节标题漂移不再静默停解析 ----
|
|
2725
2804
|
// 修前按 `## 委派结果|自做任务结果` 精确节标题切段,装户把标题写成「委派结果表」即静默停解析该表
|
|
2726
2805
|
// → L2/L3 done 回流无告警;修后按表头签名(与 agg-delegations.cjs splitTables 同口径)识别。
|
|
@@ -2788,5 +2867,40 @@ w(T, 'workflow/intents/2026-09-12-deep.md', INTENT('deep', '状态: draft\n级
|
|
|
2788
2867
|
rmfix(T);
|
|
2789
2868
|
}
|
|
2790
2869
|
|
|
2870
|
+
// ---- 门禁 ROI 插桩(2026-10-08 gate-roi-metrics)----
|
|
2871
|
+
// 断言两件事:① 默认路径不落盘、stderr 无 gate-stats 行(输出契约零污染);
|
|
2872
|
+
// ② --gate-stats 落一行 jsonl 且段覆盖全部检查(判据不漏插)。
|
|
2873
|
+
// 既有 223 条断言零改动本身即最强钉子——任何 push 到 warnings 的插桩副作用都会把它们打红。
|
|
2874
|
+
{
|
|
2875
|
+
const T = mkfix();
|
|
2876
|
+
const statsRel = path.join('.agents', 'cache', 'gate-stats.jsonl');
|
|
2877
|
+
const plain = run(T);
|
|
2878
|
+
check('门禁ROI:默认路径不落 gate-stats.jsonl(统计默认静默)',
|
|
2879
|
+
!fs.existsSync(path.join(T, statsRel)), outOf(plain));
|
|
2880
|
+
check('门禁ROI:默认路径 stderr 无 [gate-stats] 摘要行(稳定输出契约零污染)',
|
|
2881
|
+
!outOf(plain).includes('[gate-stats]'), outOf(plain));
|
|
2882
|
+
const before = fs.existsSync(path.join(T, statsRel)) ? fs.readFileSync(path.join(T, statsRel), 'utf8') : '';
|
|
2883
|
+
|
|
2884
|
+
const st = run(T, { args: ['--gate-stats'] });
|
|
2885
|
+
check('门禁ROI:--gate-stats → 落盘 + stderr 摘要行', fs.existsSync(path.join(T, statsRel)) && outOf(st).includes('[gate-stats]'), outOf(st));
|
|
2886
|
+
const lines = fs.readFileSync(path.join(T, statsRel), 'utf8').split('\n').filter(Boolean);
|
|
2887
|
+
check('门禁ROI:--gate-stats 只 append 一行(每跑一次门禁 = 一条记录)',
|
|
2888
|
+
lines.length === 1 && !before, `lines=${lines.length} before=${before.length}`);
|
|
2889
|
+
let row = null;
|
|
2890
|
+
try { row = JSON.parse(lines[0]); } catch { /* 留给下一条断言报错 */ }
|
|
2891
|
+
const ids = row && Array.isArray(row.segs) ? row.segs.map((s) => String(s.id)) : [];
|
|
2892
|
+
const uniq = new Set(ids);
|
|
2893
|
+
check('门禁ROI:段 id 唯一且覆盖 19 个插桩点(14 主流程 + 5 hygiene 模块),无重复插桩',
|
|
2894
|
+
ids.length === 19 && uniq.size === 19, `ids=${ids.join(',')}`);
|
|
2895
|
+
const expected = ['1','2','3','4','5','6','8','9','10','11','12','13','14','15','16','17','18','19','20'];
|
|
2896
|
+
check('门禁ROI:段 id 集合 == 现有检查编号(不新增/不遗漏检查项编号)',
|
|
2897
|
+
expected.every((e) => uniq.has(e)) && uniq.size === expected.length, `got=${[...uniq].sort().join(',')}`);
|
|
2898
|
+
check('门禁ROI:每段都有 label + 非负 ms(成本度量的地基)',
|
|
2899
|
+
row && row.segs.every((s) => s.label && Number(s.ms) >= 0 && Number.isInteger(Number(s.ms))), JSON.stringify(row && row.segs.slice(0, 3)));
|
|
2900
|
+
check('门禁ROI:行顶层含 ts / root / totalMs(聚合器的窗口过滤依赖 ts)',
|
|
2901
|
+
row && typeof row.ts === 'string' && typeof row.root === 'string' && Number(row.totalMs) >= 0, JSON.stringify({ ts: row && row.ts, totalMs: row && row.totalMs }));
|
|
2902
|
+
rmfix(T);
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2791
2905
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
2792
2906
|
process.exit(fail ? 1 : 0);
|
|
@@ -42,18 +42,30 @@ import { fileURLToPath } from 'node:url';
|
|
|
42
42
|
import { confirmGateFor, doneGateFor, laneOfEntry, laneOfDoc } from './stage-gates.mjs';
|
|
43
43
|
import { loadKitPolicy, hasFreshVerifyLine } from './policy.mjs';
|
|
44
44
|
|
|
45
|
-
// hasFreshVerify(root):读 <root>/.agents/verifications.jsonl 判 24h 窗口绿行(判定单源
|
|
45
|
+
// hasFreshVerify(root, doc):读 <root>/.agents/verifications.jsonl 判 24h 窗口绿行(判定单源
|
|
46
46
|
// policy.hasFreshVerifyLine)。只读,台账缺失/不可读一律 false(=无凭证,advisory 层 fail-open)。
|
|
47
|
-
|
|
47
|
+
// doc 绑定(2026-10-08 verify-doc-binding):非空时只认 e.doc === doc 的绿行——不带 doc 的绿行
|
|
48
|
+
// (CI 场景)与**别的单**的绿行都不给本单背证。调用方负责「仅 v6+ 才传 doc」,本函数不做版本判断。
|
|
49
|
+
function hasFreshVerify(root, doc = null) {
|
|
48
50
|
try {
|
|
49
51
|
const p = path.join(root, '.agents', 'verifications.jsonl');
|
|
50
52
|
if (!fs.existsSync(p)) return false;
|
|
51
|
-
return hasFreshVerifyLine(fs.readFileSync(p, 'utf8').split(/\r?\n/));
|
|
53
|
+
return hasFreshVerifyLine(fs.readFileSync(p, 'utf8').split(/\r?\n/), Date.now(), doc);
|
|
52
54
|
} catch {
|
|
53
55
|
return false;
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
// docRelOf:把用户传入的 doc 路径归一成「相对仓库根的 posix 相对路径」——与 verify.mjs --doc 的落账口径
|
|
60
|
+
// 必须逐字一致(一个转得动、另一个转不动 = 凭证永远匹配不上)。win32 反斜杠 / posix / 绝对路径三者同值。
|
|
61
|
+
function docRelOf(root, doc) {
|
|
62
|
+
try {
|
|
63
|
+
return path.relative(root, path.resolve(root, doc)).split(path.sep).join('/');
|
|
64
|
+
} catch {
|
|
65
|
+
return doc;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
57
69
|
// computeFingerprint(text):CRLF 归一 → 剔指纹行 → sha256 hex(64 位)
|
|
58
70
|
export function computeFingerprint(text) {
|
|
59
71
|
const norm = String(text).replace(/\r\n/g, '\n').split('\n')
|
|
@@ -319,10 +331,19 @@ if (isMain) {
|
|
|
319
331
|
// 测试绿凭证前置(2026-10-07 verify-evidence,advisory 不拦):→done 且文档含「验收标准」节且
|
|
320
332
|
// policy v5(verifySince)→ 24h 窗口内须有 verifications.jsonl 绿行。只读判定,任何读取异常降级
|
|
321
333
|
// 静默(fail-open——advisory 层不因台账缺失/坏行阻断关单);升 hard 走后续 policyVersion 演进。
|
|
334
|
+
// 2026-10-08 verify-doc-binding:v6+(verifyDocSince)时按本 doc 精确匹配绿行——他单的绿行不再
|
|
335
|
+
// 给本单背书;v1-v5 维持 v5 全局行为逐字不变。
|
|
336
|
+
// ⚠️ 文案锚点硬约束(既有 confirm-doc.test.mjs 断言锚定子串,零改动前提):
|
|
337
|
+
// 「无测试绿凭证」六字须**连续**,doc 与命令只能追加在其后——不得写成「无本单(x)的测试绿凭证」。
|
|
322
338
|
if (target === 'done' && typeof loadKitPolicy(root).verifySince === 'string'
|
|
323
|
-
&& /^##\s*[^#]*验收标准/m.test(text)
|
|
324
|
-
|
|
325
|
-
|
|
339
|
+
&& /^##\s*[^#]*验收标准/m.test(text)) {
|
|
340
|
+
const pol = loadKitPolicy(root);
|
|
341
|
+
const rel = docRelOf(root, doc);
|
|
342
|
+
const bound = typeof pol.verifyDocSince === 'string' ? rel : null;
|
|
343
|
+
if (!hasFreshVerify(root, bound)) {
|
|
344
|
+
console.error(`⚠️ ${doc} 置 done:24h 内无测试绿凭证(${bound ? `本单 ${rel} 须带 --doc 落账` : '凭证未按单绑定'})——关单勾验的「测试绿」声明缺机器事实`);
|
|
345
|
+
console.error(` 跑 node .agents/scripts/verify.mjs${bound ? ` --doc ${rel}` : ''} 后重新关单;本次不拦截(advisory,灰度第一档)`);
|
|
346
|
+
}
|
|
326
347
|
}
|
|
327
348
|
const fp = computeFingerprint(text);
|
|
328
349
|
// AI 自治放行分支(2026-09-30 ai-autonomy-trust):
|
|
@@ -736,5 +736,40 @@ const writeTrust = (root, cfg) => {
|
|
|
736
736
|
fs.rmSync(root, { recursive: true, force: true });
|
|
737
737
|
}
|
|
738
738
|
|
|
739
|
+
// ---- S40 测试绿凭证按 doc 绑定(2026-10-08 verify-doc-binding)----
|
|
740
|
+
// 核心防线三条:① v6 + 只有**他单**绿行 → advisory 出账(漏洞已堵);② v6 + 本单绿行 → 静默;
|
|
741
|
+
// ③ **v5 + 只有他单绿行 → 静默**(向后兼容反例——收紧只许对 v6+ 生效,存量装户零变化)。
|
|
742
|
+
// 文案锚点:既有 S38/S39 锚定「无测试绿凭证」六字连续 + 「verify.mjs」出现,本组不得破坏。
|
|
743
|
+
{
|
|
744
|
+
const mkVerifyRoot = (pv, lines) => {
|
|
745
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-docbind-'));
|
|
746
|
+
const docP = path.join(root, 'workflow', 'intents');
|
|
747
|
+
fs.mkdirSync(docP, { recursive: true });
|
|
748
|
+
fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
|
|
749
|
+
fs.writeFileSync(path.join(root, '.agents', 'kit.json'), JSON.stringify({ policyVersion: pv }), 'utf8');
|
|
750
|
+
fs.writeFileSync(path.join(root, '.agents', 'verifications.jsonl'), lines, 'utf8');
|
|
751
|
+
fs.writeFileSync(path.join(docP, '2026-10-08-m.md'),
|
|
752
|
+
'---\n状态: approved\n级别: L1\n确认指纹: cccc3333cccc3333\n---\n# I\n\n## 验收标准(可测试)\n- [x] 用例通过(证据:测试全绿)\n', 'utf8');
|
|
753
|
+
return root;
|
|
754
|
+
};
|
|
755
|
+
const green = (doc) => `${JSON.stringify({ ts: new Date().toISOString(), exitCode: 0, suite: 'npm test', passed: 1, failed: 0, ...(doc ? { doc } : {}) })}\n`;
|
|
756
|
+
const MINE = 'workflow/intents/2026-10-08-m.md';
|
|
757
|
+
const OTHER = 'workflow/intents/2026-10-08-other.md';
|
|
758
|
+
const runM = (root) => spawnSync(process.execPath, [CLI, MINE, '--delegated', '可以'], { cwd: root, encoding: 'utf8' });
|
|
759
|
+
|
|
760
|
+
const r40 = runM(mkVerifyRoot(6, green(OTHER)));
|
|
761
|
+
check('S40① v6 + 只有他单绿行 → advisory 出账(跨单背书漏洞已堵)+ 文案带 --doc',
|
|
762
|
+
r40.status === 0 && /无测试绿凭证/.test(r40.stderr) && /--doc/.test(r40.stderr), String(r40.stderr).slice(0, 200));
|
|
763
|
+
const r41 = runM(mkVerifyRoot(6, green(MINE)));
|
|
764
|
+
check('S40② v6 + 本单绿行 → 静默(绑定成立)',
|
|
765
|
+
r41.status === 0 && !/无测试绿凭证/.test(r41.stderr), String(r41.stderr).slice(0, 200));
|
|
766
|
+
const r42 = runM(mkVerifyRoot(5, green(OTHER)));
|
|
767
|
+
check('S40③ v5 + 只有他单绿行 → **静默**(兼容反例:收紧只对 v6+ 生效,存量装户零变化)',
|
|
768
|
+
r42.status === 0 && !/无测试绿凭证/.test(r42.stderr), String(r42.stderr).slice(0, 200));
|
|
769
|
+
const r43 = runM(mkVerifyRoot(6, green(null)));
|
|
770
|
+
check('S40④ v6 + 不带 doc 的绿行(CI 场景)→ 不给本单背书,advisory 出账',
|
|
771
|
+
r43.status === 0 && /无测试绿凭证/.test(r43.stderr), String(r43.stderr).slice(0, 200));
|
|
772
|
+
}
|
|
773
|
+
|
|
739
774
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
740
775
|
process.exit(fail ? 1 : 0);
|