agentic-flow-kit 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/modules/gates/dotnet-ca/check-architecture.sh +23 -5
- package/modules/hosts/omp/agents/implementer.md +47 -6
- package/modules/hosts/omp/agents/independent-reviewer.md +44 -5
- package/modules/hosts/omp/agents/ui-verifier.md +43 -6
- package/modules/hosts/opencode/agents/implementer.md +47 -1
- package/modules/hosts/opencode/agents/independent-reviewer.md +44 -1
- package/modules/hosts/opencode/agents/ui-verifier.md +43 -1
- package/modules/hosts/opencode/commands/build.md +74 -7
- package/modules/hosts/opencode/commands/deploy.md +51 -6
- package/modules/hosts/opencode/commands/design.md +36 -6
- package/modules/hosts/opencode/commands/gate-checklist.md +77 -0
- package/modules/hosts/opencode/commands/maintain.md +70 -6
- package/modules/hosts/opencode/commands/new-task.md +59 -6
- package/modules/hosts/opencode/commands/plan.md +35 -6
- package/modules/hosts/opencode/commands/review.md +41 -6
- package/modules/hosts/opencode/commands/source-sync-check.md +89 -0
- package/modules/hosts/opencode/commands/sync-hosts.md +66 -0
- package/modules/hosts/opencode/commands/test.md +58 -8
- package/modules/hosts/trae/agents/implementer.md +46 -2
- package/modules/hosts/trae/agents/independent-reviewer.md +44 -1
- package/modules/hosts/trae/agents/ui-verifier.md +41 -3
- package/modules/hosts/trae/commands/wf-build.md +74 -7
- package/modules/hosts/trae/commands/wf-deploy.md +51 -6
- package/modules/hosts/trae/commands/wf-design.md +36 -6
- package/modules/hosts/trae/commands/wf-gate-checklist.md +78 -0
- package/modules/hosts/trae/commands/wf-maintain.md +70 -6
- package/modules/hosts/trae/commands/wf-new-task.md +59 -6
- package/modules/hosts/trae/commands/wf-plan.md +35 -6
- package/modules/hosts/trae/commands/wf-review.md +41 -6
- package/modules/hosts/trae/commands/wf-source-sync-check.md +90 -0
- package/modules/hosts/trae/commands/wf-sync-hosts.md +67 -0
- package/modules/hosts/trae/commands/wf-test.md +58 -7
- package/modules/hosts/trae/hooks/post-edit-check.cjs +3 -2
- package/modules/hosts/trae/hooks/pre-shell-check.cjs +40 -9
- package/modules/hosts/trae/hooks.json +1 -1
- package/modules/hosts/zcode/agents/implementer.md +47 -6
- package/modules/hosts/zcode/agents/independent-reviewer.md +44 -5
- package/modules/hosts/zcode/agents/ui-verifier.md +43 -5
- package/package.json +6 -3
- package/src/add-gate.mjs +5 -3
- package/src/cli.mjs +15 -2
- package/src/doctor.mjs +245 -15
- package/src/gate-dotnet-ca.test.mjs +124 -0
- package/src/init.mjs +79 -17
- package/src/init.test.mjs +84 -2
- package/src/pack.test.mjs +24 -0
- package/src/render.mjs +64 -4
- package/src/run-tests.mjs +7 -11
- package/src/sync-hosts.mjs +194 -0
- package/src/sync-hosts.test.mjs +170 -0
- package/src/sync.mjs +65 -20
- package/src/sync.test.mjs +52 -0
- package/src/trae-hooks.test.mjs +115 -0
- package/templates/AGENTS.md +2 -1
- package/templates/_agents/board/index.html +32 -3
- package/templates/_agents/commands/build.md +22 -1
- package/templates/_agents/commands/design.md +2 -0
- package/templates/_agents/commands/gate-checklist.md +91 -0
- package/templates/_agents/commands/maintain.md +2 -3
- package/templates/_agents/commands/plan.md +4 -1
- package/templates/_agents/commands/review.md +1 -0
- package/templates/_agents/commands/source-sync-check.md +103 -0
- package/templates/_agents/commands/sync-hosts.md +79 -0
- package/templates/_agents/commands/test.md +4 -2
- package/templates/_agents/hooks/check-pairing-incremental.sh +20 -7
- package/templates/_agents/notes/runtime-env.md +1 -1
- package/templates/_agents/roles/implementer.md +2 -0
- package/templates/_agents/roles/independent-reviewer.md +2 -0
- package/templates/_agents/roles/ui-verifier.md +2 -0
- package/templates/_agents/rule-budgets.txt +1 -1
- package/templates/_agents/scripts/agg-delegations.cjs +39 -30
- package/templates/_agents/scripts/agg-delegations.test.mjs +32 -0
- package/templates/_agents/scripts/check-loop.mjs +763 -0
- package/templates/_agents/scripts/check-loop.sh +5 -808
- package/templates/_agents/scripts/check-loop.test.mjs +1102 -0
- package/templates/_agents/scripts/confirm-doc.mjs +228 -0
- package/templates/_agents/scripts/confirm-doc.test.mjs +321 -0
- package/templates/_agents/scripts/doctor.test.mjs +371 -0
- package/templates/_agents/scripts/ensure-board.mjs +30 -3
- package/templates/_agents/scripts/fill-intent.mjs +59 -0
- package/templates/_agents/scripts/fill-intent.test.mjs +73 -0
- package/templates/_agents/scripts/fill-plan.mjs +60 -0
- package/templates/_agents/scripts/fill-plan.test.mjs +67 -0
- package/templates/_agents/scripts/fill-spec.mjs +52 -0
- package/templates/_agents/scripts/fill-spec.test.mjs +61 -0
- package/templates/_agents/scripts/gate-checklist.mjs +194 -0
- package/templates/_agents/scripts/gate-checklist.test.mjs +204 -0
- package/templates/_agents/scripts/gen-wiki-board.mjs +38 -24
- package/templates/_agents/scripts/gen-wiki-board.test.mjs +18 -0
- package/templates/_agents/scripts/gen-workflow-index.mjs +4 -2
- package/templates/_agents/scripts/gen-workflow-metrics.mjs +12 -3
- package/templates/_agents/scripts/kb-search.mjs +25 -9
- package/templates/_agents/scripts/kb-search.test.mjs +38 -8
- package/templates/_agents/scripts/source-sync-check.mjs +145 -0
- package/templates/_agents/scripts/source-sync-check.test.mjs +244 -0
- package/templates/_agents/scripts/verify-wiki-consistency.mjs +46 -23
- package/templates/_agents/scripts/verify.mjs +8 -10
- package/templates/_agents/scripts/verify.test.mjs +13 -1
- package/templates/_agents/scripts/wf-journal.mjs +158 -0
- package/templates/_agents/scripts/wf-journal.test.mjs +150 -0
- package/templates/_agents/scripts/workflow-board-server.mjs +73 -17
- package/templates/_agents/scripts/workflow-board-server.test.mjs +136 -0
- package/templates/_agents/scripts/workflow-enums.mjs +70 -0
- package/templates/_agents/scripts/workflow-enums.test.mjs +110 -0
- package/templates/_agents/scripts/workflows-check.mjs +260 -0
- package/templates/_agents/scripts/workflows-check.test.mjs +267 -0
- package/templates/_agents/workflow-enums.txt +19 -0
- package/templates/_agents/workflows/_TEMPLATE.md +75 -0
- package/templates/_agents/workflows/pipeline-closing.md +29 -0
- package/templates/_agents/workflows/source-sync-repair.md +30 -0
- package/templates/_agents/workflows/steps//347/244/272/344/276/213-/351/203/250/347/275/262/351/252/214/350/257/201.md +10 -0
- package/templates/_agents/workflows//347/244/272/344/276/213-/345/271/266/350/241/214/345/256/236/347/216/260/350/257/204/345/256/241.md +16 -0
- package/templates/_githooks/pre-commit +16 -1
- package/templates/workflow/README.md +1 -1
- package/templates/_agents/cache/kb-index.json +0 -1
- package/templates/_agents/scripts/check-loop.test.sh +0 -1042
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// check-loop: AI-Native 闭环骨架断档扫描(agentic-flow-kit 通用版;2026-09-26 check-loop-node 自 POSIX sh 全量迁移 node)
|
|
3
|
+
// 扫描 workflow/ 目录,输出 intent/spec/plan 配对、模板字段占位符、incidents 复盘三件套、新 intent 回路等断档。
|
|
4
|
+
// 迁移背景:sh 版为规避 MSYS fork 开销引入 awk 预扫临时文件 + fd 3/4 与 glob 序逐行对齐的隐式契约
|
|
5
|
+
// (重排循环/加 continue 即静默错位)、四处临时文件无 trap 清理——node 为 kit 硬依赖,双语言实现面纯负担。
|
|
6
|
+
// 判定语义/消息文案/exit 语义与 sh 版逐条对齐(check-loop.test.mjs 37+ 场景为对齐安全网);
|
|
7
|
+
// 输出 banner 保留「check-loop.sh」字样为稳定输出契约(doctor 按 WARN 行计数、pre-push 按 exit 码判定)。
|
|
8
|
+
// 旧入口 check-loop.sh 为 4 行兼容 shim(exec node 本文件)——.githooks/pre-push 与文档引用零改动继续有效。
|
|
9
|
+
//
|
|
10
|
+
// 严重性分级(沿用):
|
|
11
|
+
// - hard-block(阻断 push / CI):配对断裂 + 回路断档(影响三件套可追溯性与闭环电路)
|
|
12
|
+
// - warning(打印提示,exit 0):其余卫生项(状态未确认 / 模板占位残留 / 引用漂移 / Adapter 一致性等)
|
|
13
|
+
// 违例输出到 stderr 并 exit 1;只剩警告时 exit 0。
|
|
14
|
+
//
|
|
15
|
+
// 检查项清单(编号/标题/severity 逐条沿 sh 版头部——gate-checklist 配对登记表按 id 消费,不得增删改号):
|
|
16
|
+
// 1. 入口文档/spec/plan 同名配对(L1 必须有 plan;L2/L3 必须有 spec+plan) [hard-block]
|
|
17
|
+
// 2. 模板字段占位符残留(YYYY-MM-DD / <主题> 等未替换;<主题> 与 .md 同行 = 命名约定描述,豁免) [warning]
|
|
18
|
+
// 3. incidents 复盘三件套完整性 + 状态严格枚举 + 新 intent 回路(回路断档=hard,其他=warning)
|
|
19
|
+
// 4. 引用有效性(文档/指令中引用的 .agents/ 路径必须存在;支持 fill-{a,b,c}.mjs 花括号展开与 fill-*.mjs 通配;
|
|
20
|
+
// workflow 文档仅扫活跃态——终态件的引用是历史叙述,不扫,2026-09-27 audit-gate-hardening) [warning]
|
|
21
|
+
// 5. intent/spec/plan 状态字段 + L3 独立复核 [warning]
|
|
22
|
+
// 6. 子智能体角色契约 + OpenCode/Trae/ZCode Adapter 一致性(含旧委派残留/钉死模型) [warning]
|
|
23
|
+
// 7. 阶段索引同步(AGENTS.md 与 new-task.md 须双向索引全部阶段指令) [warning]
|
|
24
|
+
// 8. intent 验收标准对账(新建:done 未勾验/缺节=hard,勾选缺证据=warning;存量聚合 warning,含「存量对账豁免」声明者出账)
|
|
25
|
+
// **生效日锚=git 首次加入日期(2026-09-28 改;此前取文件名前 10 字符——命名规范强制的字段、
|
|
26
|
+
// 写早零成本,一条 hard 门可被平凡绕过;incident 2026-09-28-check8-git-anchor 实证)**:
|
|
27
|
+
// 非 git / 查不到加入记录 → 不可判定 → 走存量口径(不误报 hard)
|
|
28
|
+
// 证据可写在 [x] 行的续行(仓库通写法「(证据:…)」另起一行;全/半角冒号皆认)
|
|
29
|
+
// 9. 文件名英文 kebab-case(非 ASCII 文件名=warning,2026-09-11 规则)
|
|
30
|
+
// 10. 级别 vs 迁移文件一致性(L1/L2 入口文档加入提交触及迁移 SQL/Migrations=疑似判低,warning)
|
|
31
|
+
// 11. workflow/INDEX.md 漂移(活跃层索引与磁盘不一致=warning,2026-09-21 检索层;调生成器 --check,口径单一)
|
|
32
|
+
// 12. frontmatter「模块:」合法性(枚举非法 / 2026-09-22 起新建缺字段=warning;词表单源 .agents/workflow-modules.txt)
|
|
33
|
+
// 13. 常驻面体积预算(超限=warning;判定单源 rule-budget.sh——经 sh 调用,
|
|
34
|
+
// 无 sh 环境静默跳过:advisory 级且 pre-commit 侧在 git 钩子 sh 环境照常硬拦)
|
|
35
|
+
// 14. 新 done 的 spec/plan 须在 git 历史里出现过 `状态: approved`(确认环节留痕,2026-09-22;恒 advisory 永不升级 hard)
|
|
36
|
+
// 15. 确认指纹对账(2026-09-27 起:approved/done 须 confirm-doc.mjs 确认指纹+台账配对,缺=hard-block;
|
|
37
|
+
// 两形态——TTY 亲手 / --delegated 对话委托代录,台账 source 如实区分,配对判据与 source 无关)
|
|
38
|
+
// **受管准入 = 两条件取或(2026-09-28 改锚;此前仅「自报日期 ≥ 生效日」单条件——
|
|
39
|
+
// 新档只要把日期写早即整段跳过判定,incident 2026-09-28-confirm-gate-effective-date-anchor 实证)**:
|
|
40
|
+
// ① 该 doc 在台账中有合法跳转行(stage∈approved/done/fixed/closed/superseded/cancelled,append-only
|
|
41
|
+
// 取末次)——「确认确实发生过」的机器事实,作者不可手填;
|
|
42
|
+
// ② 自报日期(日期/发现/文件名兜底)≥ 生效日——覆盖「新档完全没跑 confirm-doc」的漏网面
|
|
43
|
+
// (此类文档无台账行,仅靠 ① 会误判为存量豁免)。
|
|
44
|
+
// 两条件皆不满足 = 存量豁免(生效日前既有、从未走过确认门,不追溯)。
|
|
45
|
+
// 改锚收益:把「确认过却把日期写早以逃掉对账」这条攻破路径堵死(条件 ① 独立于自报日期成立)。
|
|
46
|
+
// **注意条件②的日期阈值仍分档、且不等价**:docs 2026-09-27 / incidents 2026-09-28——本仓现役
|
|
47
|
+
// incident 的「发现」均 ≤2026-09-27,故该档差异当前不显;新 incident 若把「发现」写成 09-27 且
|
|
48
|
+
// 无台账行,会因 incidents 阈值(09-28)被豁免,而同日期文档在 docs 档则受管。
|
|
49
|
+
// (复核 P2-1 更正:此前本注释误称「incidents 常量退役 / 两档等价」,与代码 :eff 实际保留分档不符;
|
|
50
|
+
// 台账锚两档共用为真,条件②阈值分档亦为真,二者不是一回事——README 遣词为准确口径)
|
|
51
|
+
// + incidents 覆盖(2026-09-27 gate-coverage:fixed/closed 须配对;open=起草态不加门;
|
|
52
|
+
// 不支持 open→closed 单跳——confirm-doc 状态机强制经 fixed)
|
|
53
|
+
// + 放弃态覆盖(2026-09-27 closing-coverage:superseded/cancelled 入配对集,confirm-doc --to 唯一通道)
|
|
54
|
+
// + 四终态内容绑定(2026-09-27 audit-gate-hardening;生效锚=台账行 ts≥2026-09-28——同日 p2-batch
|
|
55
|
+
// 自文档自报日期改锚:旧日期文档晚关单也绑定,3 份失配存量 ts 均 09-27 天然豁免):
|
|
56
|
+
// 按 prev 复原跳转前文本重算 sha256 与台账全量比对,不符=hard「确认内容漂移」;状态行保分隔符换值
|
|
57
|
+
// (非规范格式不误伤);台账行缺 prev 降级 warning;关单编辑顺序新约定:勾验/回填先于关单确认,
|
|
58
|
+
// confirm-doc 是最后一次写入)
|
|
59
|
+
// + 并录批次审计子检查(2026-09-28 batch-ledger-audit 改判据;**读调用事实,不猜时间戳模式**):
|
|
60
|
+
// 判据 = 台账行按 batch 分组、组内 of>1(delegated 行) → warning「确认并录」;batch/seq/of 由
|
|
61
|
+
// confirm-doc 写入时记录(那才是"本次调用落几份"确定已知的时刻)。旧判据「同 quote + 相邻 ts<2s」
|
|
62
|
+
// 整段退役——已实证四类失效:假阳性(合规逐件复用同句必然误报)/假阴性(换 quote 即零告警,
|
|
63
|
+
// 等于引导伪装)/噪声不可消退(不按日期门豁免,退役前本仓 22 条 advisory 中 14 条为该告警)/
|
|
64
|
+
// quote 字段职责冲突(对质凭据 vs 并录指纹,现归还单一职责)。无 batch 的历史行静默跳过
|
|
65
|
+
// (无判定依据的行不产出不可消除噪声——沿 audit-gate-hardening P3 教训)
|
|
66
|
+
//
|
|
67
|
+
// 注:清单条目 5(状态字段+L3 复核)与 1(配对)在同一遍 intents/specs/plans 循环里实现(沿 sh 版代码结构);
|
|
68
|
+
// 条目 6 的旧委派残留/钉死模型子项在「角色契约与 Adapter」代码段实现。
|
|
69
|
+
//
|
|
70
|
+
// 运行模式:CHECK_LOOP_ROOT 注入 fixture 根(全扫不过滤,非 git 时检查 10/14 跳过);
|
|
71
|
+
// 缺省 = git 仓库根(不在 git 仓库内 stderr 提示后 exit 0 跳过);仓库模式只扫已提交(HEAD)内容
|
|
72
|
+
// (并行会话未跟踪半成品不拦别人的 push;ls-tree 失败退化为全扫,门禁不失效)。
|
|
73
|
+
// 文档协议:机器字段一律由文件头 YAML frontmatter(受限子集:每行 `键: 值`)承载,本脚本只扫 frontmatter 取字段;
|
|
74
|
+
// 叙述性字段(独立复核/复盘三件套/验收勾验)按正文行锚定。枚举单源 .agents/workflow-enums.txt
|
|
75
|
+
// (缺文件/缺键 fail-loud exit 1;经 workflow-enums.mjs 读取,CRLF 天然容忍)。
|
|
76
|
+
// 已确认状态: approved/done=已批或闭环; superseded/cancelled=放弃留档(仍算确认,不挡 push); incident: fixed/closed
|
|
77
|
+
// 用法:node .agents/scripts/check-loop.mjs (或经 check-loop.sh shim)
|
|
78
|
+
// 测试:node templates/_agents/scripts/check-loop.test.mjs(fixture 注入 CHECK_LOOP_ROOT)
|
|
79
|
+
import fs from 'node:fs';
|
|
80
|
+
import path from 'node:path';
|
|
81
|
+
import { spawnSync } from 'node:child_process';
|
|
82
|
+
import { createHash } from 'node:crypto';
|
|
83
|
+
import { fileURLToPath } from 'node:url';
|
|
84
|
+
import { loadEnums } from './workflow-enums.mjs';
|
|
85
|
+
|
|
86
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
87
|
+
|
|
88
|
+
// ---- 根目录与枚举单源 ----
|
|
89
|
+
let ROOT;
|
|
90
|
+
if (process.env.CHECK_LOOP_ROOT) {
|
|
91
|
+
ROOT = path.resolve(process.env.CHECK_LOOP_ROOT);
|
|
92
|
+
if (!fs.existsSync(ROOT)) {
|
|
93
|
+
console.error('check-loop: CHECK_LOOP_ROOT 不可访问:' + process.env.CHECK_LOOP_ROOT);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
const top = gitOut(['rev-parse', '--show-toplevel']);
|
|
98
|
+
if (top === null) {
|
|
99
|
+
console.error('check-loop: 不在 git 仓库内,跳过扫描');
|
|
100
|
+
process.exit(0);
|
|
101
|
+
}
|
|
102
|
+
ROOT = top.trim();
|
|
103
|
+
}
|
|
104
|
+
// 枚举单源(<root>/.agents/workflow-enums.txt——fixture 契约:缺文件/缺键 fail-loud)
|
|
105
|
+
let ENUMS;
|
|
106
|
+
try {
|
|
107
|
+
ENUMS = loadEnums(path.join(ROOT, '.agents', 'workflow-enums.txt'));
|
|
108
|
+
} catch (e) {
|
|
109
|
+
const msg = String(e.message || '');
|
|
110
|
+
if (msg.includes('不可读')) {
|
|
111
|
+
console.error(`check-loop: 缺 ${path.join('.agents', 'workflow-enums.txt')}(枚举单源)——跑 flow-kit sync 恢复后重试`);
|
|
112
|
+
} else {
|
|
113
|
+
console.error('check-loop: 枚举单源 ' + msg);
|
|
114
|
+
}
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
const inSet = (v, arr) => arr.includes(v);
|
|
118
|
+
const stOkDoc = (st) => inSet(st, ENUMS['doc.status.confirmed']);
|
|
119
|
+
|
|
120
|
+
// git 调用(参数数组形式不走 shell——Windows cmd 不认单引号;git.exe 显式解析沿 doctor 先例)
|
|
121
|
+
function gitOut(args, { ok = () => true } = {}) {
|
|
122
|
+
const r = spawnSync(process.platform === 'win32' ? 'git.exe' : 'git', args,
|
|
123
|
+
{ cwd: ROOT, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 });
|
|
124
|
+
if (r.error || r.status !== 0 || !ok(r)) return null;
|
|
125
|
+
return r.stdout || '';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---- frontmatter 受限子集读取(fm_get 语义:首行 --- 进入、下一 --- 闭合、键在行首、首个命中、值两侧去空白)----
|
|
129
|
+
const fileLinesCache = new Map();
|
|
130
|
+
function linesOf(file) {
|
|
131
|
+
if (!fileLinesCache.has(file)) {
|
|
132
|
+
try { fileLinesCache.set(file, fs.readFileSync(file, 'utf8').split(/\r?\n/)); }
|
|
133
|
+
catch { fileLinesCache.set(file, null); }
|
|
134
|
+
}
|
|
135
|
+
return fileLinesCache.get(file);
|
|
136
|
+
}
|
|
137
|
+
function fmGet(file, key) {
|
|
138
|
+
const lines = linesOf(file);
|
|
139
|
+
if (!lines) return '';
|
|
140
|
+
const isDelim = (l) => /^---\s*$/.test(l);
|
|
141
|
+
if (!isDelim(lines[0] || '')) return '';
|
|
142
|
+
for (let i = 1; i < lines.length; i++) {
|
|
143
|
+
if (isDelim(lines[i])) return '';
|
|
144
|
+
if (lines[i].startsWith(key + ':')) return lines[i].slice(key.length + 1).trim();
|
|
145
|
+
}
|
|
146
|
+
return '';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const WF = 'workflow';
|
|
150
|
+
const DOC_DIRS = ['intents', 'specs', 'plans', 'incidents'];
|
|
151
|
+
const blockers = [];
|
|
152
|
+
const warnings = [];
|
|
153
|
+
|
|
154
|
+
// ---- 已提交(HEAD)过滤:仓库模式只扫 tracked(fixture 模式恒真;ls-tree 失败退化全扫)----
|
|
155
|
+
let trackedSet = null; // null = 不过滤
|
|
156
|
+
if (!process.env.CHECK_LOOP_ROOT) {
|
|
157
|
+
const out = gitOut(['ls-tree', '-r', '--name-only', 'HEAD', '--', 'workflow']);
|
|
158
|
+
if (out !== null) trackedSet = new Set(out.split('\n').map((l) => l.trim()).filter(Boolean));
|
|
159
|
+
}
|
|
160
|
+
const isTracked = (abs) => trackedSet === null || trackedSet.has(path.relative(ROOT, abs).split(path.sep).join('/'));
|
|
161
|
+
const docFiles = (sub) => {
|
|
162
|
+
const dir = path.join(ROOT, WF, sub);
|
|
163
|
+
if (!fs.existsSync(dir)) return [];
|
|
164
|
+
return fs.readdirSync(dir)
|
|
165
|
+
.filter((f) => f.endsWith('.md') && /^\d/.test(f))
|
|
166
|
+
.map((f) => path.join(dir, f))
|
|
167
|
+
.filter((f) => isTracked(f))
|
|
168
|
+
.sort();
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// --- 1. intent/spec/plan 同名配对 + 状态确认 + L3 确认三件 [hard-block] ---
|
|
172
|
+
for (const intent of docFiles('intents')) {
|
|
173
|
+
const base = path.basename(intent);
|
|
174
|
+
const st = fmGet(intent, '状态');
|
|
175
|
+
const lvl = fmGet(intent, '级别');
|
|
176
|
+
if (!stOkDoc(st)) {
|
|
177
|
+
const msg = `${base}(frontmatter 状态键当前值:『${st || '缺失'}』)`;
|
|
178
|
+
if (lvl === 'L3') blockers.push(`- [状态未确认] L3 intent 必须为 approved/done/superseded/cancelled:${msg}`);
|
|
179
|
+
else warnings.push(`- [WARN 状态未确认] intent 必须为 approved/done/superseded/cancelled:${msg}`);
|
|
180
|
+
}
|
|
181
|
+
if (lvl === 'L2' || lvl === 'L3') {
|
|
182
|
+
if (!fs.existsSync(path.join(ROOT, WF, 'specs', base))) {
|
|
183
|
+
blockers.push(`- [配对断裂] intent 缺 spec:${base}(应在 ${WF}/specs/ 下同名)`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (!fs.existsSync(path.join(ROOT, WF, 'plans', base))) {
|
|
187
|
+
blockers.push(`- [配对断裂] intent 缺 plan:${base}(应在 ${WF}/plans/ 下同名)`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (const spec of docFiles('specs')) {
|
|
192
|
+
const base = path.basename(spec);
|
|
193
|
+
const st = fmGet(spec, '状态');
|
|
194
|
+
const lvl = fmGet(spec, '级别');
|
|
195
|
+
const hasEntry = fs.existsSync(path.join(ROOT, WF, 'intents', base)) || fs.existsSync(path.join(ROOT, WF, 'incidents', base));
|
|
196
|
+
if (!hasEntry) blockers.push(`- [配对断裂] spec 缺 intent/incident:${base}(应在 ${WF}/intents/ 或 ${WF}/incidents/ 下同名)`);
|
|
197
|
+
if (!stOkDoc(st)) {
|
|
198
|
+
const msg = `${base}(frontmatter 状态键当前值:『${st || '缺失'}』)`;
|
|
199
|
+
if (lvl === 'L3') blockers.push(`- [状态未确认] L3 spec 必须为 approved/done/superseded/cancelled:${msg}`);
|
|
200
|
+
else warnings.push(`- [WARN 状态未确认] spec 必须为 approved/done/superseded/cancelled:${msg}`);
|
|
201
|
+
}
|
|
202
|
+
// L3 确认三件:确认结果/确认时间读 frontmatter;独立复核按正文行锚定(放弃件豁免 = 枚举单源 abandoned 集)
|
|
203
|
+
if (lvl === 'L3' && !inSet(st, ENUMS['doc.status.abandoned'])) {
|
|
204
|
+
if (fmGet(spec, '确认结果') !== 'approved') blockers.push(`- [L3 确认缺失] ${base} 确认结果必须为 approved`);
|
|
205
|
+
if (!fmGet(spec, '确认时间')) blockers.push(`- [L3 确认缺失] ${base} 必须记录确认时间`);
|
|
206
|
+
if (!(linesOf(spec) || []).some((l) => /^- 独立复核:[ \t]*[^<\s]/.test(l))) {
|
|
207
|
+
blockers.push(`- [L3 复核缺失] ${base} 必须记录新会话独立复核结论`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (const plan of docFiles('plans')) {
|
|
213
|
+
const base = path.basename(plan);
|
|
214
|
+
const st = fmGet(plan, '状态');
|
|
215
|
+
const lvl = fmGet(plan, '级别');
|
|
216
|
+
const hasEntry = fs.existsSync(path.join(ROOT, WF, 'intents', base)) || fs.existsSync(path.join(ROOT, WF, 'incidents', base));
|
|
217
|
+
if (!hasEntry) blockers.push(`- [配对断裂] plan 缺 intent/incident:${base}(应在 ${WF}/intents/ 或 ${WF}/incidents/ 下同名)`);
|
|
218
|
+
if (!stOkDoc(st)) {
|
|
219
|
+
const msg = `${base}(frontmatter 状态键当前值:『${st || '缺失'}』)`;
|
|
220
|
+
if (lvl === 'L3') blockers.push(`- [状态未确认] L3 plan 必须为 approved/done/superseded/cancelled:${msg}`);
|
|
221
|
+
else warnings.push(`- [WARN 状态未确认] plan 必须为 approved/done/superseded/cancelled:${msg}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// --- 2. 模板字段占位符残留 [warning](排除运行时文件名格式与协议样板,口径沿 sh 版)---
|
|
226
|
+
{
|
|
227
|
+
const phRe = /YYYY-MM-DD|<主题>|<日期 主题>|L0 \/ L1 \/ L2 \/ L3|draft \/ approved \/ done|open \/ fixed \/ closed/;
|
|
228
|
+
const boilerRe = /\.\.\/specs\/[A-Za-z0-9-]*\.md|写明如何满足|防复发验证|_YYYY-MM-DD\.|format\('YYYY-MM-DD'\)|value-format="YYYY-MM-DD"/;
|
|
229
|
+
// 命名约定豁免:`<主题>` 与 `.md` 同行 = 在描述文件命名规则(如 `.agents/workflows/<主题>.md`、
|
|
230
|
+
// 「复制本模板为 YYYY-MM-DD-<主题>.md」),非未填占位符;真未填的占位(标题 `# INTENT — <主题>`、
|
|
231
|
+
// `日期: YYYY-MM-DD`)不含 .md,仍照拦(2026-09-25-wf-runtime incident 记录的误报口径)
|
|
232
|
+
const isNamingConv = (line) => line.includes('<主题>') && /\.md/.test(line);
|
|
233
|
+
const groups = new Map(); // file -> [ "行号:内容" ](首现序)
|
|
234
|
+
for (const sub of DOC_DIRS) {
|
|
235
|
+
for (const f of docFiles(sub)) {
|
|
236
|
+
const hits = [];
|
|
237
|
+
(linesOf(f) || []).forEach((line, i) => {
|
|
238
|
+
if (phRe.test(line) && !boilerRe.test(line) && !isNamingConv(line)) hits.push(`${i + 1}:${line}`);
|
|
239
|
+
});
|
|
240
|
+
if (hits.length) groups.set(f, hits);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
for (const [f, hits] of groups) {
|
|
244
|
+
warnings.push(`- [WARN 模板未填] ${path.relative(ROOT, f).split(path.sep).join('/')} 含模板占位符:\n${hits.join('\n')}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// --- 3. incidents 复盘三件套 + 状态严格枚举 + 新 intent 回路(回路断档 = hard)---
|
|
249
|
+
for (const inc of docFiles('incidents')) {
|
|
250
|
+
const name = path.basename(inc);
|
|
251
|
+
const lines = linesOf(inc) || [];
|
|
252
|
+
const incLvl = fmGet(inc, '级别');
|
|
253
|
+
const incSt = fmGet(inc, '状态');
|
|
254
|
+
|
|
255
|
+
if (!inSet(incSt, ENUMS['incident.status.all'])) {
|
|
256
|
+
warnings.push(`- [WARN 状态非法] incident 状态必须为 open/fixed/closed:${name}(frontmatter 状态键当前值:『${incSt || '缺失'}』)`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (fmGet(inc, '流程') !== 'legacy') {
|
|
260
|
+
if (!inSet(incLvl, ENUMS['level.all'])) {
|
|
261
|
+
warnings.push(`- [WARN 级别缺失] ${name} 必须填写 L0/L1/L2/L3(配对检查已跳过——补级别后重跑本脚本校验配对)`);
|
|
262
|
+
} else {
|
|
263
|
+
if (['L1', 'L2', 'L3'].includes(incLvl) && !fs.existsSync(path.join(ROOT, WF, 'plans', name))) {
|
|
264
|
+
blockers.push(`- [配对断裂] incident 缺 plan:${name}(应在 ${WF}/plans/ 下同名)`);
|
|
265
|
+
}
|
|
266
|
+
if (['L2', 'L3'].includes(incLvl) && !fs.existsSync(path.join(ROOT, WF, 'specs', name))) {
|
|
267
|
+
blockers.push(`- [配对断裂] incident 缺 spec:${name}(应在 ${WF}/specs/ 下同名)`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 三件套:正文行锚定 /^1\. /^2\. /^3\. (sh awk 同款)
|
|
273
|
+
for (const n of [1, 2, 3]) {
|
|
274
|
+
if (!lines.some((l) => l.startsWith(`${n}. `))) {
|
|
275
|
+
warnings.push(`- [WARN 三件套不全] ${name} 缺复盘三件套之 ${n}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const hasParent = lines.some((l) => l.includes('是否需要新 intent'));
|
|
279
|
+
if (!hasParent) {
|
|
280
|
+
warnings.push(`- [WARN 三件套不全] ${name} 缺「是否需要新 intent」子项`);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
const yesLine = lines.find((l) => l.includes('是 → '));
|
|
284
|
+
if (yesLine) {
|
|
285
|
+
const m = yesLine.match(/intents\/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[^ )]*\.md/);
|
|
286
|
+
const ref = m ? m[0] : null;
|
|
287
|
+
if (!ref) blockers.push(`- [回路断档] ${name} 选「是」但未指明 intent 文件路径`);
|
|
288
|
+
else if (!fs.existsSync(path.join(ROOT, WF, ref))) blockers.push(`- [回路断档] ${name} 引用的 intent 不存在:${ref}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// --- 4. 引用有效性 [warning](.agents/ 路径须存在;skills 特例允许用户级 ~/.agents/skills)---
|
|
293
|
+
{
|
|
294
|
+
// 引用可带 {a,b,c} 花括号展开(仓库通写法 `fill-{intent,spec,plan}.mjs`)或 `*` 通配(`fill-*.mjs`)——
|
|
295
|
+
// 花括号展开逐路查存在,任一路存在即有效;`*` 按目录枚举 glob 匹配,命中即有效;全不中才算断档
|
|
296
|
+
// (2026-09-26 审查修复:正则字符类不含 {} *,曾把 `fill-{intent,spec,plan}.mjs` / `fill-*.mjs`
|
|
297
|
+
// 截断成 `fill-` 误报引用断档)
|
|
298
|
+
const refRe = /\.agents\/(commands|hooks|scripts|skills|roles)\/[A-Za-z0-9_][A-Za-z0-9_./{},,*-]*/g;
|
|
299
|
+
const expandRef = (ref) => {
|
|
300
|
+
const m = ref.match(/^([^{]*)\{([^}]*)\}(.*)$/);
|
|
301
|
+
if (!m) return [ref];
|
|
302
|
+
return m[2].split(/[,,]/).map((v) => m[1] + v.trim() + m[3]);
|
|
303
|
+
};
|
|
304
|
+
// globExists:`*` 通配引用按目录枚举匹配(仅文件名层,`*` 不跨 /;目录不存在 = false)
|
|
305
|
+
const globExists = (ref) => {
|
|
306
|
+
const re = new RegExp('^' + path.basename(ref).replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/\\\\]*') + '$');
|
|
307
|
+
try { return fs.readdirSync(path.dirname(path.join(ROOT, ref))).some((f) => re.test(f)); } catch { return false; }
|
|
308
|
+
};
|
|
309
|
+
const refExists = (r) => (r.includes('*') ? globExists(r) : fs.existsSync(path.join(ROOT, r)));
|
|
310
|
+
const HOME_DIR = process.env.HOME || process.env.USERPROFILE || '';
|
|
311
|
+
const files = [];
|
|
312
|
+
if (fs.existsSync(path.join(ROOT, 'AGENTS.md'))) files.push('AGENTS.md');
|
|
313
|
+
for (const e of readdirOrNull(ROOT) || []) {
|
|
314
|
+
const p = path.join(ROOT, e, 'AGENTS.md');
|
|
315
|
+
if (isDirectoryOrNull(path.join(ROOT, e)) && fs.existsSync(p)) files.push(path.join(e, 'AGENTS.md'));
|
|
316
|
+
}
|
|
317
|
+
for (const f of readdirOrNull(path.join(ROOT, WF)) || []) {
|
|
318
|
+
if (f.endsWith('.md')) files.push(`${WF}/${f}`); // workflow 根级 *.md(README 等)
|
|
319
|
+
}
|
|
320
|
+
for (const sub of DOC_DIRS) {
|
|
321
|
+
const dir = path.join(ROOT, WF, sub);
|
|
322
|
+
// 仅扫活跃态(draft/approved/open)——终态件的引用是历史叙述(提及已改名/已废文件属常态),
|
|
323
|
+
// 扫了只会产生不可消除的 advisory 噪声,真漏点被淹没(2026-09-27 audit-gate-hardening)
|
|
324
|
+
const activeSet = sub === 'incidents' ? ENUMS['incident.status.active'] : ENUMS['doc.status.active'];
|
|
325
|
+
for (const f of readdirOrNull(dir) || []) {
|
|
326
|
+
if (!f.endsWith('.md') || !(f === '_TEMPLATE.md' || /^\d/.test(f))) continue;
|
|
327
|
+
if (f !== '_TEMPLATE.md' && !inSet(fmGet(path.join(dir, f), '状态'), activeSet)) continue;
|
|
328
|
+
files.push(`${WF}/${sub}/${f}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const cmdDir = path.join(ROOT, '.agents', 'commands');
|
|
332
|
+
for (const f of readdirOrNull(cmdDir) || []) {
|
|
333
|
+
if (f.endsWith('.md')) files.push(`.agents/commands/${f}`);
|
|
334
|
+
}
|
|
335
|
+
for (const rel of files) {
|
|
336
|
+
const text = linesOf(path.join(ROOT, rel));
|
|
337
|
+
if (!text) continue;
|
|
338
|
+
const seen = new Set();
|
|
339
|
+
for (const line of text) {
|
|
340
|
+
for (const m of line.matchAll(refRe)) {
|
|
341
|
+
const ref = m[0];
|
|
342
|
+
if (seen.has(ref)) continue;
|
|
343
|
+
seen.add(ref);
|
|
344
|
+
const refs = expandRef(ref);
|
|
345
|
+
if (refs.some(refExists)) continue;
|
|
346
|
+
if (refs.some((r) => r.startsWith('.agents/skills/') && !r.includes('*') && fs.existsSync(path.join(HOME_DIR, r)))) continue;
|
|
347
|
+
warnings.push(`- [WARN 引用断档] ${rel.split(path.sep).join('/')} 引用不存在的文件:${ref}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// --- 5. 子智能体角色契约 + 宿主 Adapter 一致性 + 旧委派残留 + 钉死模型 [warning] ---
|
|
354
|
+
{
|
|
355
|
+
const cmdFiles = () => (readdirOrNull(path.join(ROOT, '.agents', 'commands')) || [])
|
|
356
|
+
.filter((f) => f.endsWith('.md')).map((f) => `.agents/commands/${f}`);
|
|
357
|
+
const textOf = (rel) => linesOf(path.join(ROOT, rel)) || [];
|
|
358
|
+
for (const role of ['implementer', 'independent-reviewer', 'ui-verifier']) {
|
|
359
|
+
const rolePath = `.agents/roles/${role}.md`;
|
|
360
|
+
if (!fs.existsSync(path.join(ROOT, rolePath))) {
|
|
361
|
+
warnings.push(`- [WARN 角色缺失] 公共角色契约不存在:${rolePath}`);
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (fs.existsSync(path.join(ROOT, '.opencode', 'agents'))) {
|
|
365
|
+
const p = `.opencode/agents/${role}.md`;
|
|
366
|
+
if (!fs.existsSync(path.join(ROOT, p))) warnings.push(`- [WARN Adapter 缺失] OpenCode 缺 ${role}:${p}`);
|
|
367
|
+
else {
|
|
368
|
+
if (!/^mode:\s*subagent\s*$/m.test(textOf(p).join('\n'))) warnings.push(`- [WARN Adapter 无效] OpenCode ${role} 未声明 mode:subagent:${p}`);
|
|
369
|
+
if (!textOf(p).some((l) => l.includes(rolePath))) warnings.push(`- [WARN Adapter 断线] OpenCode ${role} 未引用公共角色:${rolePath}`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (fs.existsSync(path.join(ROOT, '.trae', 'agents'))) {
|
|
373
|
+
const p = `.trae/agents/${role}.md`;
|
|
374
|
+
if (!fs.existsSync(path.join(ROOT, p))) warnings.push(`- [WARN Adapter 缺失] Trae 缺 ${role}:${p}`);
|
|
375
|
+
else {
|
|
376
|
+
if (!new RegExp(`^name:\\s*${role}\\s*$`, 'm').test(textOf(p).join('\n'))) warnings.push(`- [WARN Adapter 无效] Trae ${role} 的 name 与文件名不一致:${p}`);
|
|
377
|
+
if (!textOf(p).some((l) => l.includes(rolePath))) warnings.push(`- [WARN Adapter 断线] Trae ${role} 未引用公共角色:${rolePath}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const zc = `.zcode/agents/${role}.md`;
|
|
381
|
+
if (fs.existsSync(path.join(ROOT, zc))) { // 本地配置不入 git:缺失不告警,存在时校验一致性
|
|
382
|
+
if (!new RegExp(`^name:\\s*["']?${role}["']?\\s*$`, 'm').test(textOf(zc).join('\n'))) warnings.push(`- [WARN Adapter 无效] ZCode ${role} 的 name 与文件名不一致:${zc}`);
|
|
383
|
+
if (!textOf(zc).some((l) => l.includes(rolePath))) warnings.push(`- [WARN Adapter 断线] ZCode ${role} 未引用公共角色:${rolePath}`);
|
|
384
|
+
}
|
|
385
|
+
const cited = ['AGENTS.md', ...cmdFiles()].some((f) => textOf(f).some((l) => l.includes(rolePath)));
|
|
386
|
+
if (!cited) warnings.push(`- [WARN 命令断线] 工作流未引用公共角色:${rolePath}`);
|
|
387
|
+
}
|
|
388
|
+
// 旧委派残留 / 钉死模型(grep -nH 行格式 file:line:content)
|
|
389
|
+
const legacyRe = /(^subagent:|subagent_type=|general_purpose_task|browser_use)/;
|
|
390
|
+
const legacyHits = [];
|
|
391
|
+
for (const f of ['AGENTS.md', ...cmdFiles()]) {
|
|
392
|
+
textOf(f).forEach((l, i) => { if (legacyRe.test(l)) legacyHits.push(`${f}:${i + 1}:${l}`); });
|
|
393
|
+
}
|
|
394
|
+
if (legacyHits.length) warnings.push(`- [WARN 旧委派残留] 主规则或阶段命令仍含宿主特定/旧委派声明:\n${legacyHits.join('\n')}`);
|
|
395
|
+
const modelHits = [];
|
|
396
|
+
for (const h of ['.opencode', '.trae', '.zcode']) {
|
|
397
|
+
for (const f of readdirOrNull(path.join(ROOT, h, 'agents')) || []) {
|
|
398
|
+
if (!f.endsWith('.md')) continue;
|
|
399
|
+
const rel = `${h}/agents/${f}`;
|
|
400
|
+
textOf(rel).forEach((l, i) => { if (/^model:/.test(l) && !/^model:\s*inherit/.test(l)) modelHits.push(`${rel}:${i + 1}:${l}`); });
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (modelHits.length) warnings.push(`- [WARN 宿主耦合] 薄 Adapter 不应固定模型:\n${modelHits.join('\n')}`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// --- 6. 阶段索引同步 [warning] ---
|
|
407
|
+
for (const cmd of ['plan', 'design', 'build', 'test', 'deploy', 'maintain', 'review']) {
|
|
408
|
+
for (const doc of ['AGENTS.md', '.agents/commands/new-task.md']) {
|
|
409
|
+
const text = linesOf(path.join(ROOT, doc));
|
|
410
|
+
if (text && !text.some((l) => l.includes(`.agents/commands/${cmd}.md`))) {
|
|
411
|
+
warnings.push(`- [WARN 阶段索引漂移] ${doc} 缺 ${cmd} 指令索引(两处阶段表须同步维护)`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// --- 共享:文件「首次加入 git」的日期索引(2026-09-28 check8-git-anchor)---
|
|
417
|
+
// 用途:检查 8 的生效日锚从**可手填的文件名前缀**(项目命名规范本身即 `YYYY-MM-DD-<主题>`,作者
|
|
418
|
+
// 日常手写该前缀,「写早」零成本、顺手即发生——一条 hard 门可被平凡绕过)改为**首次加入 git 的日期**。
|
|
419
|
+
// **诚实边界(复核 P1 更正)**:该锚取 `git log --diff-filter=A --format=@%aI`,即 **author date
|
|
420
|
+
// (作者自报时间戳)**,**不是** commit 时刻——`git commit --date=<过去>` 一条参数即可伪造,故
|
|
421
|
+
// 「锚不可手填」的说法**不成立**。本改动的确切收益是:关掉「改文件名(日常必写、零成本)」这条通道,
|
|
422
|
+
// 把伪造成本抬到「主动加参数伪造时间戳」——后者与「伪造台账」同属**本地信任边界内不可机器防**的范畴
|
|
423
|
+
// (事后对质靠 git 历史与台账)。此处如实声明,不宣称「通道已关闭」。
|
|
424
|
+
// 口径:git 按时间新→旧遍历,同一文件首遇即最早那次加入(与检查 10 的 addpath 同款判据)。
|
|
425
|
+
// 非 git / 命令失败 → null,调用方按既有「非 git 跳过」语义退化(不 fail-loud)。
|
|
426
|
+
// 注:检查 10 需要的是「文件→加入 commit」+「commit→文件清单」两份映射(判断同提交是否触及迁移 SQL),
|
|
427
|
+
// 与本索引「文件→日期」不同构,故**不合并**——避免把两处判据耦死。
|
|
428
|
+
const addedDates = (() => {
|
|
429
|
+
if (gitOut(['rev-parse', '--git-dir']) === null) return null;
|
|
430
|
+
const log = gitOut(['log', '--diff-filter=A', '--format=@%aI', '--name-only']);
|
|
431
|
+
if (log === null) return null;
|
|
432
|
+
const m = new Map();
|
|
433
|
+
let cur = null;
|
|
434
|
+
for (const line of log.split('\n')) {
|
|
435
|
+
if (line.startsWith('@')) { cur = line.slice(1); continue; }
|
|
436
|
+
if (line.trim() && cur && !m.has(line.trim())) m.set(line.trim(), cur); // 新→旧序首遇 = 最早
|
|
437
|
+
}
|
|
438
|
+
return m;
|
|
439
|
+
})();
|
|
440
|
+
// 某文档「首次加入日期」(YYYY-MM-DD);不可判定(非 git / 查不到)返回 ''
|
|
441
|
+
const addedDateOf = (absDoc) => {
|
|
442
|
+
if (!addedDates) return '';
|
|
443
|
+
const rel = path.relative(ROOT, absDoc).split(path.sep).join('/');
|
|
444
|
+
const iso = addedDates.get(rel);
|
|
445
|
+
return iso ? iso.slice(0, 10) : '';
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
// --- 8. intent 验收标准对账(done 须逐条勾验并补证据;新建 hard,存量聚合 warning)---
|
|
449
|
+
// 生效日锚 2026-09-28 改「git 首次加入日期」(此前取文件名前 10 字符——命名规范强制的字段、
|
|
450
|
+
// 写早零成本,见 incidents/2026-09-28-check8-git-anchor)。非 git → 不可判定 → 走存量口径(不误报 hard)。
|
|
451
|
+
{
|
|
452
|
+
const accCutoff = '2026-09-12';
|
|
453
|
+
let legacyUnaccounted = 0;
|
|
454
|
+
for (const intent of docFiles('intents')) {
|
|
455
|
+
if (fmGet(intent, '状态') !== 'done') continue;
|
|
456
|
+
const base = path.basename(intent);
|
|
457
|
+
const lines = linesOf(intent) || [];
|
|
458
|
+
// 节扫描:hs=有验收节;insec 至下一个二级标题;uc=未勾项;ne=勾选缺证据;ex=存量豁免声明
|
|
459
|
+
// 证据可写在 [x] 行的续行(仓库通写法「(证据:…)」另起一行)——pendX 记「待证据项」,
|
|
460
|
+
// 遇证据行(全角「:」/半角「:」皆认)清零;遇下一条目 / 下一小节 / 节末仍无证据 → ne
|
|
461
|
+
let hs = false, uc = false, ne = false;
|
|
462
|
+
let insec = false, ex = false;
|
|
463
|
+
let pendX = false; // 上一 [x] 项尚无证据,证据可能在紧随的续行
|
|
464
|
+
const evRe = /证据[::]/;
|
|
465
|
+
const closeItem = () => { if (pendX) { ne = true; pendX = false; } };
|
|
466
|
+
for (const line of lines) {
|
|
467
|
+
if (/存量对账豁免(/.test(line)) ex = true;
|
|
468
|
+
if (/^\s*##\s+[^#]*验收标准/.test(line)) { hs = true; insec = true; closeItem(); continue; }
|
|
469
|
+
if (insec && /^\s*##\s/.test(line)) { insec = false; closeItem(); continue; }
|
|
470
|
+
if (!insec) continue;
|
|
471
|
+
if (/^\s*- \[ \]/.test(line)) { closeItem(); uc = true; continue; }
|
|
472
|
+
if (/^\s*- \[x\]/.test(line)) { closeItem(); pendX = !evRe.test(line); continue; }
|
|
473
|
+
if (pendX && evRe.test(line)) pendX = false; // 续行补上证据
|
|
474
|
+
}
|
|
475
|
+
closeItem(); // 节末(或全文末)仍无证据 → 计缺证据
|
|
476
|
+
// 生效日锚 = git 首次加入日期(2026-09-28 改;此前取文件名前 10 字符)。**准确收益**:关掉
|
|
477
|
+
// 「改文件名日期(日常必写、零成本)即整段跳过 hard 门」这条通道;锚取 author date(%aI),
|
|
478
|
+
// 刻意伪造(`git commit --date=`)仍可绕过——与伪造台账同属本地信任边界内,不宣称"通道已关闭"。
|
|
479
|
+
// 非 git / 查不到 → '' → isNew=false → 走存量口径(与既有语义一致:不可判定时不误报 hard)。
|
|
480
|
+
const adddate = addedDateOf(intent);
|
|
481
|
+
const isNew = adddate !== '' && adddate >= accCutoff;
|
|
482
|
+
if (!hs) {
|
|
483
|
+
if (isNew) blockers.push(`- [验收未对账] done intent 缺「## 验收标准」节(勾验无从核对):${base}`);
|
|
484
|
+
else if (!ex) legacyUnaccounted++;
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (!uc && !ne) continue;
|
|
488
|
+
if (isNew) {
|
|
489
|
+
if (uc) blockers.push(`- [验收未对账] done intent 验收标准有未勾验项:${base}(逐条勾验并补证据:commit/用例/冒烟输出)`);
|
|
490
|
+
if (ne) warnings.push(`- [WARN 验收缺证据] ${base} 验收标准勾选项缺「证据:」标注`);
|
|
491
|
+
} else if (!ex) {
|
|
492
|
+
legacyUnaccounted++;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (legacyUnaccounted > 0) {
|
|
496
|
+
warnings.push(`- [WARN 验收对账存量] ${legacyUnaccounted} 个存量 done intent 验收标准未对账(生效日锚之前加入仓库,豁免 hard,不回填)`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// --- 9. 文件名英文 kebab-case [warning] ---
|
|
501
|
+
for (const sub of DOC_DIRS) {
|
|
502
|
+
const dir = path.join(ROOT, WF, sub);
|
|
503
|
+
for (const f of readdirOrNull(dir) || []) {
|
|
504
|
+
if (!f.endsWith('.md') || f === '_TEMPLATE.md') continue;
|
|
505
|
+
const abs = path.join(dir, f);
|
|
506
|
+
if (!isTracked(abs)) continue;
|
|
507
|
+
if (!/^[\x20-\x7e]*$/.test(f)) {
|
|
508
|
+
warnings.push(`- [WARN 文件名非英文] 文件名含非 ASCII 字符,应为英文 kebab-case:${WF}/${sub}/${f}`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// --- 10. 级别 vs 迁移文件一致性 [warning](启发式:入口文档加入提交触及迁移 SQL/Migrations → 疑似判低;非 git 跳过)---
|
|
514
|
+
if (gitOut(['rev-parse', '--git-dir']) !== null) {
|
|
515
|
+
const log = gitOut(['log', '--diff-filter=A', '--format=@%H', '--name-only']);
|
|
516
|
+
if (log !== null) {
|
|
517
|
+
const addpath = new Map(); // 文件 → 首次加入 commit(新→旧序首遇)
|
|
518
|
+
const cfiles = new Map(); // commit → 文件清单
|
|
519
|
+
let cur = null;
|
|
520
|
+
for (const line of log.split('\n')) {
|
|
521
|
+
if (line.startsWith('@')) { cur = line.slice(1); cfiles.set(cur, []); continue; }
|
|
522
|
+
if (line.trim() && cur && !addpath.has(line.trim())) {
|
|
523
|
+
addpath.set(line.trim(), cur);
|
|
524
|
+
cfiles.get(cur).push(line.trim());
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const docs = [...docFiles('intents'), ...docFiles('incidents')];
|
|
528
|
+
for (const doc of docs) {
|
|
529
|
+
const rel = path.relative(ROOT, doc).split(path.sep).join('/');
|
|
530
|
+
const lvl = fmGet(doc, '级别');
|
|
531
|
+
if (lvl !== 'L1' && lvl !== 'L2') continue;
|
|
532
|
+
const c = addpath.get(rel);
|
|
533
|
+
if (!c) continue;
|
|
534
|
+
const hits = (cfiles.get(c) || []).filter((f) => /(^|\/)docs\/scripts\/.*\.sql$/.test(f) || /(^|\/)Migrations\//.test(f));
|
|
535
|
+
if (hits.length) {
|
|
536
|
+
warnings.push(`- [WARN 级别疑似判低] ${path.basename(doc)} 级别 ${lvl} 但其加入提交触及迁移文件(混合改动应就高不就低,请复核级别):\n${hits.join('\n')}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// --- 11. workflow/INDEX.md 漂移 [warning](调生成器 --check,口径单一不复刻渲染)---
|
|
543
|
+
{
|
|
544
|
+
const gen = path.join(ROOT, '.agents', 'scripts', 'gen-workflow-index.mjs');
|
|
545
|
+
if (fs.existsSync(gen)) {
|
|
546
|
+
const r = spawnSync(process.execPath, [gen, '--check'], { cwd: ROOT, encoding: 'utf8' });
|
|
547
|
+
if (r.status !== 0) {
|
|
548
|
+
const first = `${r.stdout || ''}${r.stderr || ''}`.split('\n')[0];
|
|
549
|
+
warnings.push(`- [WARN 索引漂移] workflow/INDEX.md 与磁盘不一致——跑 node .agents/scripts/gen-workflow-index.mjs 重新生成(${first})`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// --- 12. frontmatter「模块:」合法性 [warning](词表单源;缺字段只对 2026-09-22 起新建提示)---
|
|
555
|
+
{
|
|
556
|
+
const modsFile = path.join(ROOT, '.agents', 'workflow-modules.txt');
|
|
557
|
+
if (fs.existsSync(modsFile)) {
|
|
558
|
+
const mods = linesOf(modsFile).filter((l) => l.trim() && !l.startsWith('#')).map((l) => l.trim());
|
|
559
|
+
const vocab = new Set(mods);
|
|
560
|
+
for (const sub of ['intents', 'specs', 'plans', 'incidents']) {
|
|
561
|
+
for (const doc of docFiles(sub)) {
|
|
562
|
+
const base = path.basename(doc);
|
|
563
|
+
const mod = fmGet(doc, '模块');
|
|
564
|
+
let d = fmGet(doc, '日期') || fmGet(doc, '发现');
|
|
565
|
+
if (!d) d = /^\d{4}-\d{2}-\d{2}$/.test(base.slice(0, 10)) ? base.slice(0, 10) : '';
|
|
566
|
+
if (mod) {
|
|
567
|
+
if (!vocab.has(mod)) warnings.push(`- [WARN 模块元数据] ${base}「模块: ${mod}」不在词表(见 .agents/workflow-modules.txt)`);
|
|
568
|
+
} else if (/^\d{4}-\d{2}-\d{2}$/.test(d) && d >= '2026-09-22') {
|
|
569
|
+
warnings.push(`- [WARN 模块元数据] ${base} 缺「模块:」字段(2026-09-22 起新建文档必填)`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// --- 13. 常驻面体积预算 [warning](判定单源 rule-budget.sh;无 sh 环境静默跳过——advisory,pre-commit 硬拦兜底)---
|
|
577
|
+
{
|
|
578
|
+
const rb = path.join(ROOT, '.agents', 'scripts', 'rule-budget.sh');
|
|
579
|
+
const budgets = path.join(ROOT, '.agents', 'rule-budgets.txt');
|
|
580
|
+
if (fs.existsSync(rb) && fs.existsSync(budgets)) {
|
|
581
|
+
const r = spawnSync('sh', [rb, '--all'], { cwd: ROOT, encoding: 'utf8' });
|
|
582
|
+
if (!r.error && r.status !== 0) {
|
|
583
|
+
const head = `${r.stdout || ''}${r.stderr || ''}`.split('\n').slice(0, 5).map((l) => ' ' + l).join('\n');
|
|
584
|
+
warnings.push(`- [WARN 常驻面超限] 常驻面体积超预算(先删除或下沉被取代条目再增——一进一出):\n${head}`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// --- 14. 新 done 的 spec/plan 须在 git 历史里出现过 `状态: approved` [warning](恒 advisory;非 git 跳过)---
|
|
590
|
+
if (gitOut(['rev-parse', '--git-dir']) !== null && gitOut(['rev-parse', '-q', '--verify', 'HEAD']) !== null) {
|
|
591
|
+
for (const sub of ['specs', 'plans']) {
|
|
592
|
+
for (const doc of docFiles(sub)) {
|
|
593
|
+
if (fmGet(doc, '状态') !== 'done') continue;
|
|
594
|
+
const base = path.basename(doc);
|
|
595
|
+
let d = fmGet(doc, '日期') || fmGet(doc, '发现');
|
|
596
|
+
if (!d) d = /^\d{4}-\d{2}-\d{2}$/.test(base.slice(0, 10)) ? base.slice(0, 10) : '';
|
|
597
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(d) || d < '2026-09-23') continue; // 生效 2026-09-23 起(规则发布次日)
|
|
598
|
+
const lines = linesOf(doc) || [];
|
|
599
|
+
if (lines.some((l) => l.includes('存量确认态豁免('))) continue;
|
|
600
|
+
const rel = path.relative(ROOT, doc).split(path.sep).join('/');
|
|
601
|
+
const hit = gitOut(['log', '-1', '--format=%H', '-G', '^状态:[[:space:]]*approved', '--', rel]);
|
|
602
|
+
if (hit !== null && hit.trim() === '') {
|
|
603
|
+
warnings.push(`- [WARN 确认态缺失] ${base} 状态已 done 但 git 历史中从未出现行首「状态: approved」——确认环节未留痕(draft 直跳 done)`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// --- 15. 确认指纹对账 [hard-block](2026-09-26 confirm-gate-machine;2026-09-27 confirm-gate-delegated 两形态;
|
|
610
|
+
// 生效日锚 2026-09-28 改台账 ts——此前锚取自文档自报「日期/发现」,新档只要把日期写早即整段跳过判定,
|
|
611
|
+
// incident 2026-09-28-confirm-gate-effective-date-anchor 实证)---
|
|
612
|
+
// 确认两形态(confirm-doc.mjs):TTY 亲手键入「可以」/ --delegated 对话委托代录(用户对话内明确放行后
|
|
613
|
+
// AI 代录,台账行如实记 source=chat-delegated + quote 原话,永不伪装 TTY)。intents/specs/plans 凡
|
|
614
|
+
// approved/done 须有 confirm-doc 产生的 frontmatter 确认指纹 + 台账(.agents/confirmations.jsonl)
|
|
615
|
+
// 配对行——配对判据 doc/stage/fingerprint 三键,与 source 无关。
|
|
616
|
+
// **生效日锚 = 台账 ts(2026-09-28 起)**:受管准入 = 该 doc 在台账中有合法跳转行(stage ∈ approved/done/
|
|
617
|
+
// fixed/closed/superseded/cancelled,append-only 取末次);无台账行 = 存量豁免(不追溯)。自报日期
|
|
618
|
+
// (日期/发现/文件名)不再参与门禁判定——它可被作者手填,把「门是否生效」的决定权交给被约束对象本身
|
|
619
|
+
// 是自举漏洞;台账行是「确认确实发生过」的唯一机器事实。docs 与 incidents 共用同一锚(原 incidents
|
|
620
|
+
// 独立生效日 2026-09-28 常量退役:台账起算日 = docs 生效日,两档等价)。
|
|
621
|
+
// 台账坏行容忍跳过(审计件,jsonl 追加式);frontmatter 存 16 位、台账存 64 位,按前 16 位配对。
|
|
622
|
+
{
|
|
623
|
+
const EFFECTIVE = '2026-09-27';
|
|
624
|
+
const ledgerPath = path.join(ROOT, '.agents', 'confirmations.jsonl');
|
|
625
|
+
const ledger = [];
|
|
626
|
+
if (fs.existsSync(ledgerPath)) {
|
|
627
|
+
for (const line of linesOf(ledgerPath) || []) {
|
|
628
|
+
if (!line.trim()) continue;
|
|
629
|
+
try { ledger.push(JSON.parse(line)); } catch { /* 坏行跳过 */ }
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
// incidents 侧覆盖(2026-09-27 gate-coverage):fixed/closed 为已确认态(open = 起草态不加门——
|
|
633
|
+
// maintain.md 创建即对话确认的既有口径)
|
|
634
|
+
// 放弃态覆盖(2026-09-27 closing-coverage):superseded/cancelled 入配对集——confirm-doc --to 是唯一
|
|
635
|
+
// 合法产生通道(cancelled 自 draft/approved/open/fixed;superseded 自 approved/done/fixed/closed),
|
|
636
|
+
// 此前手改 frontmatter 即出账的口子收死
|
|
637
|
+
const CONFIRMED_BY_SUB = {
|
|
638
|
+
intents: ['approved', 'done', 'superseded', 'cancelled'],
|
|
639
|
+
specs: ['approved', 'done', 'superseded', 'cancelled'],
|
|
640
|
+
plans: ['approved', 'done', 'superseded', 'cancelled'],
|
|
641
|
+
incidents: ['fixed', 'closed', 'superseded', 'cancelled'],
|
|
642
|
+
};
|
|
643
|
+
// 合法跳转 stage 集(台账行「确认确实发生过」的判据;revert-draft / revert-open 类回退注记行不算)
|
|
644
|
+
const VALID_STAGES = new Set(['approved', 'done', 'fixed', 'closed', 'superseded', 'cancelled']);
|
|
645
|
+
for (const [sub, confirmedSet] of Object.entries(CONFIRMED_BY_SUB)) {
|
|
646
|
+
for (const doc of docFiles(sub)) {
|
|
647
|
+
const st = fmGet(doc, '状态');
|
|
648
|
+
if (!confirmedSet.includes(st)) continue;
|
|
649
|
+
const base = path.basename(doc);
|
|
650
|
+
const rel = path.relative(ROOT, doc).split(path.sep).join('/');
|
|
651
|
+
// 受管准入 = 两条件取或(2026-09-28 改锚):
|
|
652
|
+
// ① 台账中有该 doc 的合法跳转行——「确认确实发生过」的机器事实;它独立于自报日期成立,
|
|
653
|
+
// 故堵死「确认过却把日期写早以逃掉对账」这条攻破路径(本 incident 的核心缺陷);
|
|
654
|
+
// ② 自报日期 ≥ 生效日——覆盖「新档完全没跑 confirm-doc」的漏网面(此类无台账行,
|
|
655
|
+
// 仅靠 ① 会被误判为存量豁免而静默放行)。
|
|
656
|
+
// 两条件皆不满足 = 存量豁免(生效日前既有、从未走确认门,不追溯)。
|
|
657
|
+
const docEntries = ledger.filter((e) => e && e.doc === rel && VALID_STAGES.has(e.stage));
|
|
658
|
+
const eff = sub === 'incidents' ? '2026-09-28' : EFFECTIVE;
|
|
659
|
+
let d = fmGet(doc, '日期') || fmGet(doc, '发现');
|
|
660
|
+
if (!d) d = /^\d{4}-\d{2}-\d{2}$/.test(base.slice(0, 10)) ? base.slice(0, 10) : '';
|
|
661
|
+
const dateManaged = /^\d{4}-\d{2}-\d{2}$/.test(d) && d >= eff;
|
|
662
|
+
if (!docEntries.length && !dateManaged) continue; // 存量豁免
|
|
663
|
+
const fp = fmGet(doc, '确认指纹');
|
|
664
|
+
const ok = !!fp && docEntries.some((e) => e.stage === st
|
|
665
|
+
&& typeof e.fingerprint === 'string' && e.fingerprint.startsWith(fp));
|
|
666
|
+
if (!ok) {
|
|
667
|
+
blockers.push(`- [确认未对账] ${base} 状态 ${st} 无用户确认记录——AI 不得代确认,用户在终端跑 node .agents/scripts/confirm-doc.mjs ${rel} 后重试`);
|
|
668
|
+
}
|
|
669
|
+
// 内容绑定(2026-09-27 audit-gate-hardening;生效锚 2026-09-27 gate-hardening-p2-batch 自文档自报日期
|
|
670
|
+
// 改为台账 done 行 ts):done 文档当前内容须与台账 done 行确认时的内容一致——按 prev 复原跳转前文本
|
|
671
|
+
// 重算比对,防「确认后篡改」(改验收标准/正文均触发)。复原口径 = confirm-doc computeFingerprint 的
|
|
672
|
+
// 逆推:CRLF 归一 → frontmatter 首个「状态:」行保分隔符换值为台账 prev(双空格等非规范分隔符不误伤,
|
|
673
|
+
// 与前向按原行字面计算对称)→ 剔「确认指纹:」行。两条降级路径:台账行缺 prev(schema 演进前存量,
|
|
674
|
+
// 无以复原)→ warning 不拦;entry.ts 早于 2026-09-28(UTC 字符串比较——该次确认在旧关单顺序时代完成)→
|
|
675
|
+
// 豁免绑定(配对判定已过,3 份失配存量即此列)。关单编辑顺序新约定不变:confirm-doc 是最后一次写入,
|
|
676
|
+
// 此后修订走 superseded 或新 intent。
|
|
677
|
+
if (ok && ['done', 'closed', 'superseded', 'cancelled'].includes(st)) {
|
|
678
|
+
const doneEntries = ledger.filter((e) => e && e.doc === rel && e.stage === st && typeof e.fingerprint === 'string');
|
|
679
|
+
const entry = doneEntries[doneEntries.length - 1]; // append-only 台账,末次生效(重确认场景)
|
|
680
|
+
if (!entry || !entry.prev) {
|
|
681
|
+
warnings.push(`- [WARN 绑定降级] ${base} 台账 stage=${st} 行缺 prev 字段(schema 演进前行),内容绑定跳过——仅配对判定`);
|
|
682
|
+
} else if (!(typeof entry.ts === 'string' && entry.ts >= '2026-09-28')) {
|
|
683
|
+
// 该 done 确认发生在生效锚前(旧关单顺序时代)——豁免内容绑定,配对判定照常
|
|
684
|
+
} else if (bindingSha256(linesOf(doc) || [], entry.prev) !== entry.fingerprint) {
|
|
685
|
+
blockers.push(`- [确认内容漂移] ${base} ${st} 后内容与确认台账不符——已关单文档不得直接改(关单编辑先于关单确认);确需修订走 superseded 或新 intent 引用`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
// 并录批次审计(2026-09-28 batch-ledger-audit 改判据;原 2026-09-27 confirm-gate-one-per-call 引入):
|
|
691
|
+
// **读调用事实,不猜时间戳模式**。判据 = 台账行按 `batch` 分组,组内 `of > 1` → warning「确认并录」。
|
|
692
|
+
// batch/seq/of 由 confirm-doc 在**写入时**记录(那才是「本次调用落了几份」确定已知的时刻);
|
|
693
|
+
// 旧判据「同 quote + 相邻 ts 差 < 2s」是拿两个间接信号反推该事实,已实证四类失效并**整段退役**:
|
|
694
|
+
// ① 假阳性:合规逐件调用复用同句(用户两次都说「可以」)→ 必然误报;
|
|
695
|
+
// ② 假阴性:并录时给每份换不同 quote → 零告警(判据的实际效果是引导伪装者改 quote);
|
|
696
|
+
// ③ 噪声不可消退:不按日期门豁免,退役前本仓 22 条 advisory 中 14 条为该告警,且文案自认「不可区分」;
|
|
697
|
+
// ④ 字段职责冲突:`quote` 同时当「对质凭据」与「并录指纹」——现归还单一职责,只记用户原话。
|
|
698
|
+
// 无 `batch` 字段的历史行(schema 演进前,本仓全部 67 行均无该字段)→ **静默跳过**,不降级出账、不回溯:
|
|
699
|
+
// 按 audit-gate-hardening P3 教训(「扫了只产生不可消除的 advisory 噪声,真漏点被淹没」),无判定依据
|
|
700
|
+
// 的行不该产出告警。VALID_STAGES 复用外层声明(复核 P2-3 更正:此前本块内另有一份同名声明遮蔽外层)。
|
|
701
|
+
{
|
|
702
|
+
const byBatch = new Map(); // batch → 该批的台账行(仅计合法跳转 stage)
|
|
703
|
+
for (const e of ledger) {
|
|
704
|
+
if (!e || !VALID_STAGES.has(e.stage)) continue;
|
|
705
|
+
if (typeof e.batch !== 'string' || !e.batch) continue; // 历史行无 batch → 无判定依据,跳过
|
|
706
|
+
if (!byBatch.has(e.batch)) byBatch.set(e.batch, []);
|
|
707
|
+
byBatch.get(e.batch).push(e);
|
|
708
|
+
}
|
|
709
|
+
for (const [b, rows] of byBatch) {
|
|
710
|
+
// 判据取组内**任一行** of>1(复核 P2-3 更正:原取 rows[0].of 使结论依赖行序——同批首行 of=1、
|
|
711
|
+
// 次行 of=2 会漏报。confirm-doc 对同批所有行写同一个 of=docs.length,故正常批次组内一致;
|
|
712
|
+
// 改用 some 消除该顺序脆弱性,并对 craft 出来的不一致批次按「最大值」报,方向偏严不偏松)
|
|
713
|
+
if (!rows.some((r) => typeof r.of === 'number' && r.of > 1)) continue;
|
|
714
|
+
// of > 1 = 一次调用落多份态。delegated 形态下这正是「并录」(入口已拒多份,此处抓历史/绕行);
|
|
715
|
+
// TTY 形态天然逐份过目(用户亲手键入),of>1 不构成违规——故只对 delegated 行报。
|
|
716
|
+
if (!rows.some((r) => r.source === 'chat-delegated')) continue;
|
|
717
|
+
const of = Math.max(...rows.map((r) => (typeof r.of === 'number' ? r.of : 0)));
|
|
718
|
+
const docs = rows.map((r) => String(r.doc).replace(/^workflow\//, '')).join('、');
|
|
719
|
+
warnings.push(`- [WARN 确认并录] 单次调用落账 ${rows.length} 份(batch ${b},of=${of}):${docs}——同一次 --delegated 调用放行多份,塌掉「逐件确认」门(build.md);口径见 workflow/papercuts.md 2026-09-28 与 incidents/2026-09-28-batch-ledger-audit.md`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// --- 输出(banner 沿 sh 版字样与格式 = 稳定输出契约:banner 后空行、条目 '- ' 前缀)---
|
|
725
|
+
if (blockers.length) {
|
|
726
|
+
process.stderr.write(`闭环骨架断档(check-loop.sh)— HARD-BLOCK:\n\n${blockers.join('\n')}\n\n`);
|
|
727
|
+
if (warnings.length) process.stderr.write(`WARN(advisory,不阻断):\n\n${warnings.join('\n')}\n`);
|
|
728
|
+
process.exit(1);
|
|
729
|
+
}
|
|
730
|
+
if (warnings.length) {
|
|
731
|
+
process.stderr.write(`check-loop.sh WARN(advisory,不阻断):\n\n${warnings.join('\n')}\n`);
|
|
732
|
+
}
|
|
733
|
+
process.exit(0);
|
|
734
|
+
|
|
735
|
+
// bindingSha256:done 内容绑定的复原重算(检查 15 专用)——confirm-doc.mjs computeFingerprint 的逆推:
|
|
736
|
+
// lines 已 CRLF 归一(split(/\r?\n/)),frontmatter 区内首个「状态:」行保分隔符换值为 prev(`状态: done`
|
|
737
|
+
// 双空格 → `状态: approved`——与前向按原行字面计算对称,非规范分隔符不误伤;行尾空白等更奇异格式仍会
|
|
738
|
+
// 失配,触发前提本身已违反「确认落态唯一入口」约定,接受),全文剔「确认指纹:」行后 join('\n') 再 sha256。
|
|
739
|
+
function bindingSha256(lines, prevStatus) {
|
|
740
|
+
const isDelim = (l) => /^---\s*$/.test(l);
|
|
741
|
+
const out = [];
|
|
742
|
+
let i = 0;
|
|
743
|
+
if (isDelim(lines[0] || '')) {
|
|
744
|
+
out.push(lines[0]);
|
|
745
|
+
let replaced = false;
|
|
746
|
+
for (i = 1; i < lines.length; i++) {
|
|
747
|
+
if (isDelim(lines[i])) { out.push(lines[i]); i++; break; }
|
|
748
|
+
if (!replaced && /^状态:/.test(lines[i])) {
|
|
749
|
+
out.push(lines[i].replace(/^(\s*状态:\s*)\S.*$/, `$1${prevStatus}`));
|
|
750
|
+
replaced = true;
|
|
751
|
+
} else out.push(lines[i]);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
for (; i < lines.length; i++) out.push(lines[i]);
|
|
755
|
+
return createHash('sha256').update(out.filter((l) => !/^确认指纹:/.test(l)).join('\n'), 'utf8').digest('hex');
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function readdirOrNull(dir) {
|
|
759
|
+
try { return fs.readdirSync(dir); } catch { return null; }
|
|
760
|
+
}
|
|
761
|
+
function isDirectoryOrNull(p) {
|
|
762
|
+
try { return fs.statSync(p).isDirectory(); } catch { return false; }
|
|
763
|
+
}
|