agentic-flow-kit 0.8.0 → 0.9.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 +36 -12
- package/modules/gates/README.md +24 -0
- package/modules/gates/generated-readonly/README.md +13 -0
- package/modules/gates/generated-readonly/check-generated.mjs +61 -0
- package/modules/gates/generated-readonly/local-pre-commit.line +1 -0
- package/modules/gates/node-layer/README.md +9 -0
- package/modules/gates/node-layer/check-node-layer.mjs +29 -0
- package/modules/gates/node-layer/local-pre-commit.line +1 -0
- package/modules/gates/py-import/README.md +9 -0
- package/modules/gates/py-import/check-py-import.mjs +30 -0
- package/modules/gates/py-import/local-pre-commit.line +1 -0
- package/modules/hosts/claude/agents/implementer.md +52 -0
- package/modules/hosts/claude/agents/independent-reviewer.md +49 -0
- package/modules/hosts/claude/agents/ui-verifier.md +48 -0
- package/modules/hosts/claude/commands/wf-build.md +79 -0
- package/modules/hosts/claude/commands/wf-deploy.md +57 -0
- package/modules/hosts/claude/commands/wf-design.md +42 -0
- package/modules/hosts/claude/commands/wf-gate-checklist.md +77 -0
- package/modules/hosts/claude/commands/wf-maintain.md +76 -0
- package/modules/hosts/claude/commands/wf-new-task.md +65 -0
- package/modules/hosts/claude/commands/wf-plan.md +41 -0
- package/modules/hosts/claude/commands/wf-review.md +47 -0
- package/modules/hosts/claude/commands/wf-source-sync-check.md +89 -0
- package/modules/hosts/claude/commands/wf-sync-hosts.md +66 -0
- package/modules/hosts/claude/commands/wf-test.md +66 -0
- package/modules/hosts/codex/agents/implementer.md +52 -0
- package/modules/hosts/codex/agents/independent-reviewer.md +49 -0
- package/modules/hosts/codex/agents/ui-verifier.md +48 -0
- package/modules/hosts/codex/commands/wf-build.md +79 -0
- package/modules/hosts/codex/commands/wf-deploy.md +57 -0
- package/modules/hosts/codex/commands/wf-design.md +42 -0
- package/modules/hosts/codex/commands/wf-gate-checklist.md +77 -0
- package/modules/hosts/codex/commands/wf-maintain.md +76 -0
- package/modules/hosts/codex/commands/wf-new-task.md +65 -0
- package/modules/hosts/codex/commands/wf-plan.md +41 -0
- package/modules/hosts/codex/commands/wf-review.md +47 -0
- package/modules/hosts/codex/commands/wf-source-sync-check.md +89 -0
- package/modules/hosts/codex/commands/wf-sync-hosts.md +66 -0
- package/modules/hosts/codex/commands/wf-test.md +66 -0
- package/modules/hosts/codex/skills/flow-kit/SKILL.md +14 -0
- package/modules/hosts/cursor/agents/implementer.md +52 -0
- package/modules/hosts/cursor/agents/independent-reviewer.md +49 -0
- package/modules/hosts/cursor/agents/ui-verifier.md +48 -0
- package/modules/hosts/cursor/commands/wf-build.md +79 -0
- package/modules/hosts/cursor/commands/wf-deploy.md +57 -0
- package/modules/hosts/cursor/commands/wf-design.md +42 -0
- package/modules/hosts/cursor/commands/wf-gate-checklist.md +77 -0
- package/modules/hosts/cursor/commands/wf-maintain.md +76 -0
- package/modules/hosts/cursor/commands/wf-new-task.md +65 -0
- package/modules/hosts/cursor/commands/wf-plan.md +41 -0
- package/modules/hosts/cursor/commands/wf-review.md +47 -0
- package/modules/hosts/cursor/commands/wf-source-sync-check.md +89 -0
- package/modules/hosts/cursor/commands/wf-sync-hosts.md +66 -0
- package/modules/hosts/cursor/commands/wf-test.md +66 -0
- package/modules/hosts/omp/agents/implementer.md +1 -1
- package/modules/hosts/omp/agents/independent-reviewer.md +1 -1
- package/modules/hosts/omp/agents/ui-verifier.md +1 -1
- package/modules/hosts/opencode/agents/implementer.md +1 -1
- package/modules/hosts/opencode/agents/independent-reviewer.md +1 -1
- package/modules/hosts/opencode/agents/ui-verifier.md +1 -1
- package/modules/hosts/opencode/commands/wf-build.md +3 -4
- package/modules/hosts/opencode/commands/wf-design.md +2 -2
- package/modules/hosts/opencode/commands/wf-maintain.md +1 -1
- package/modules/hosts/opencode/commands/wf-plan.md +3 -3
- package/modules/hosts/opencode/commands/wf-review.md +1 -1
- package/modules/hosts/opencode/commands/wf-source-sync-check.md +1 -1
- package/modules/hosts/opencode/commands/wf-test.md +3 -1
- package/modules/hosts/trae/agents/implementer.md +1 -1
- package/modules/hosts/trae/agents/independent-reviewer.md +1 -1
- package/modules/hosts/trae/agents/ui-verifier.md +1 -1
- package/modules/hosts/trae/commands/wf-build.md +3 -4
- package/modules/hosts/trae/commands/wf-design.md +2 -2
- package/modules/hosts/trae/commands/wf-maintain.md +1 -1
- package/modules/hosts/trae/commands/wf-plan.md +3 -3
- package/modules/hosts/trae/commands/wf-review.md +1 -1
- package/modules/hosts/trae/commands/wf-source-sync-check.md +1 -1
- package/modules/hosts/trae/commands/wf-test.md +3 -1
- package/modules/hosts/zcode/agents/implementer.md +1 -1
- package/modules/hosts/zcode/agents/independent-reviewer.md +1 -1
- package/modules/hosts/zcode/agents/ui-verifier.md +1 -1
- package/package.json +1 -1
- package/src/cli.mjs +16 -4
- package/src/doctor.mjs +49 -1
- package/src/fresh-init.test.mjs +36 -0
- package/src/gates.test.mjs +126 -0
- package/src/init.mjs +8 -1
- package/src/init.test.mjs +7 -1
- package/src/profiles.mjs +10 -4
- package/src/run-tests.mjs +3 -0
- package/src/stack-profile.test.mjs +89 -0
- package/src/sync-hosts.test.mjs +26 -6
- package/templates/AGENTS.md +4 -4
- package/templates/_agents/board/index.html +3 -3
- package/templates/_agents/commands/build.md +4 -5
- package/templates/_agents/commands/design.md +4 -4
- package/templates/_agents/commands/maintain.md +1 -1
- package/templates/_agents/commands/new-task.md +13 -2
- package/templates/_agents/commands/plan.md +4 -4
- package/templates/_agents/commands/review.md +1 -1
- package/templates/_agents/commands/source-sync-check.md +1 -1
- package/templates/_agents/commands/test.md +4 -2
- package/templates/_agents/hooks/commit-check.cjs +45 -24
- package/templates/_agents/lane-surfaces.txt +14 -0
- package/templates/_agents/roles/implementer.md +1 -1
- package/templates/_agents/roles/independent-reviewer.md +1 -1
- package/templates/_agents/roles/ui-verifier.md +1 -1
- package/templates/_agents/scripts/check-lane-surface.mjs +164 -0
- package/templates/_agents/scripts/check-lane-surface.test.mjs +200 -0
- package/templates/_agents/scripts/check-loop-rev.test.mjs +337 -0
- package/templates/_agents/scripts/check-loop.mjs +460 -268
- package/templates/_agents/scripts/check-loop.test.mjs +677 -17
- package/templates/_agents/scripts/check-metric-claims.mjs +275 -0
- package/templates/_agents/scripts/commit-check-trigger.test.mjs +64 -1
- package/templates/_agents/scripts/confirm-doc.mjs +159 -15
- package/templates/_agents/scripts/confirm-doc.test.mjs +362 -10
- package/templates/_agents/scripts/doctor.test.mjs +102 -1
- package/templates/_agents/scripts/fill-intent.mjs +8 -7
- package/templates/_agents/scripts/fill-intent.test.mjs +12 -3
- package/templates/_agents/scripts/fill-plan.mjs +27 -11
- package/templates/_agents/scripts/fill-plan.test.mjs +148 -8
- package/templates/_agents/scripts/fill-spec.mjs +18 -3
- package/templates/_agents/scripts/fill-spec.test.mjs +198 -1
- package/templates/_agents/scripts/gate-checklist.mjs +4 -1
- package/templates/_agents/scripts/gen-workflow-index.mjs +6 -2
- package/templates/_agents/scripts/gen-workflow-index.test.mjs +12 -0
- package/templates/_agents/scripts/policy.mjs +46 -0
- package/templates/_agents/scripts/solidify-task.mjs +101 -0
- package/templates/_agents/scripts/stage-gates.mjs +256 -0
- package/templates/_agents/scripts/trust-mode.mjs +67 -0
- package/templates/_agents/scripts/wf-journal.mjs +71 -2
- package/templates/_agents/scripts/wf-journal.test.mjs +66 -0
- package/templates/_agents/scripts/workflow-board-server.mjs +18 -15
- package/templates/_agents/scripts/workflow-board-server.test.mjs +30 -1
- package/templates/_agents/workflows/pipeline-closing.md +1 -1
- package/templates/_githooks/pre-commit +33 -2
- package/templates/_githooks/pre-push +56 -12
- package/templates/workflow/README.md +22 -3
- package/templates/workflow/incidents/_TEMPLATE.md +4 -0
- package/templates/workflow/intents/_TEMPLATE.md +1 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// check-metric-claims — 检查 16「量化断言指标签名对账」实现(2026-09-28 claim-exceeds-fix 引入;
|
|
3
|
+
// 2026-09-28 check16-inline-debt 自 check-loop.mjs 原内联块迁出为独立模块——检查 16 自带三层子结构
|
|
4
|
+
// (登记表解析 / 装户取数器载入 / 签名扫描),内联时与检查 1-15 的主扫描流搅在同一文件;
|
|
5
|
+
// 迁出后 check-loop 只留判据摘要 + ctx 组装 + 调用,判据语义 / 消息文案 / exit 语义零变化)。
|
|
6
|
+
// 判据(与 check-loop.mjs 头部检查项清单 16 同源):
|
|
7
|
+
// 活跃态文档(draft/approved/open)中的 `{{指标名}}`(小写点分)签名须替换为实时值,留签名=未回填=warning;
|
|
8
|
+
// 只查显式签名、不全文扫数字(历史叙述假阳性恒 0);未登记签名 / 取数器缺失 → fail-loud 出账;
|
|
9
|
+
// 登记表缺失 → 静默跳过(未启用该检查的装户零噪声);装户自有指标走 .agents/metric-derivers.cjs(CJS 契约,内置优先)。
|
|
10
|
+
// 契约:runCheck16(ctx) 由 check-loop.mjs 调用(调用位置在检查 15 之后——warnings 按插入序输出,
|
|
11
|
+
// 位置变化会改变输出行序);ctx 显式携带本检查引用的外层符号(以代码实际引用集为准):
|
|
12
|
+
// ROOT / ENUMS / docFiles / fmGet / inSet / isTracked / linesOf / readdirOrNull / warnings。
|
|
13
|
+
// 不隐式读 check-loop 闭包——迁出前后行为逐字节一致(check-loop 全量输出 diff 基线验证)。
|
|
14
|
+
// docFiles / isTracked 使扫描面与取数面和检查 1-15 同口径(tracked 过滤链单源定义于 check-loop.mjs docFiles)。
|
|
15
|
+
// 本文件只 import node:* 内置模块(无同目录依赖;先例:check-loop.mjs import 同目录 workflow-enums.mjs)。
|
|
16
|
+
// 测试:node templates/_agents/scripts/check-loop.test.mjs(黑盒跑整脚本,场景组「检查 16」)
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
|
|
21
|
+
export function runCheck16(ctx) {
|
|
22
|
+
const { ROOT, ENUMS, docFiles, fmGet, inSet, isTracked, linesOf, readdirOrNull, warnings } = ctx;
|
|
23
|
+
const mcFile = path.join(ROOT, '.agents', 'metric-claims.txt');
|
|
24
|
+
if (fs.existsSync(mcFile)) {
|
|
25
|
+
const ledgerLines = () => {
|
|
26
|
+
const p = path.join(ROOT, '.agents', 'confirmations.jsonl');
|
|
27
|
+
const ls = (linesOf(p) || []).filter((l) => l.trim());
|
|
28
|
+
return { total: ls.length, withBatch: ls.filter((l) => /"batch"/.test(l)).length };
|
|
29
|
+
};
|
|
30
|
+
// 内置取数器:全部本地确定性、零网络。**装户零配置即用**(不依赖任何装户文件)。
|
|
31
|
+
// **取数与扫描面同口径(复核 P2;2026-09-28 check16-inline-debt 起单源化)**:文档扫描与取数统计
|
|
32
|
+
// 统一走 ctx.docFiles()——过滤链(.md ∧ 数字前缀 ∧ tracked)全仓唯一定义于 check-loop.mjs 的
|
|
33
|
+
// docFiles,本文件不再自持 countDocs 复刻。首版独立 readdir 数盘面曾把并行会话的**未跟踪**新档
|
|
34
|
+
// 算进取数(盘面 60 / HEAD 59),令「签名填对了」却是别的读者复现不出的数;单源后
|
|
35
|
+
// 「扫描看得到的文档集 == 取数统计的文档集」由构造保证,口径漂移在编译期即不可能。
|
|
36
|
+
const derivers = {
|
|
37
|
+
'ledger.lines': () => ledgerLines().total,
|
|
38
|
+
'ledger.linesWithBatch': () => ledgerLines().withBatch,
|
|
39
|
+
'ledger.linesWithoutBatch': () => ledgerLines().total - ledgerLines().withBatch,
|
|
40
|
+
'docs.count.intents': () => docFiles('intents').length,
|
|
41
|
+
'docs.count.specs': () => docFiles('specs').length,
|
|
42
|
+
'docs.count.plans': () => docFiles('plans').length,
|
|
43
|
+
'docs.count.incidents': () => docFiles('incidents').length,
|
|
44
|
+
'docs.count.all': () => docFiles('intents').length + docFiles('specs').length + docFiles('plans').length + docFiles('incidents').length,
|
|
45
|
+
};
|
|
46
|
+
// ---- 装户侧取数器(2026-09-28 adopter-derivers;修「owned 登记表配 managed 取数器」P1)----
|
|
47
|
+
// 背景:登记表是 owned(装户自增指标),取数器却曾硬编码在本文件(managed)——装户一新增指标
|
|
48
|
+
// 就报「无取数器」,唯一出路是改本文件,而那是 managed:sync 永久报「本地已改」+ doctor WARN
|
|
49
|
+
// + 本脚本因供应链防线**跳过执行**(门禁静默停摆)。故改为分层:
|
|
50
|
+
// ① 内置优先(行为与今完全一致,装户零配置可用);② 未命中则查装户模块 `.agents/metric-derivers.cjs`
|
|
51
|
+
// **必须是 CJS(`.cjs` + module.exports)**(2026-09-28 跨版本实测修正):
|
|
52
|
+
// 首版约定 `.mjs` 并用 `createRequire` 载入,在 Node v24.12 上实测可用(**故本机没暴露问题**);
|
|
53
|
+
// 但跨版本实测(真下 18.20.5 / 20.18.0 / 22.11.0 二进制跑)**三者全部 `ERR_REQUIRE_ESM`**——
|
|
54
|
+
// `require(esm)` 只在 Node ≥20.19 / ≥22.12 默认可用,Node 18 从未支持。而本仓 engines 写
|
|
55
|
+
// `>=18.0.0`、CI 矩阵跑 18/22 → **装户取数功能在 CI 目标版本上完全不可用**(fail-loud 正常、
|
|
56
|
+
// 不崩,但功能不工作)。故改用 CJS:`createRequire` 载入 `.cjs` 在 18/20/22/24 全线可用,
|
|
57
|
+
// 且**仍是同步**(无须把本脚本改成 async 主线)。契约要求同步函数也与本检查「零网络、
|
|
58
|
+
// 本地确定性」的前提一致(异步的唯一真实收益是网络/并发 IO,此处都不允许)。
|
|
59
|
+
// **失败一律响亮、绝不静默降级**(沿 claim-exceeds-fix「假绿比红危险」与本检查自身语法错教训):
|
|
60
|
+
// 若载入失败就「只用内置指标」,装户会看到「引用了 my.metric → 未登记」,看起来像**自己忘了登记**,
|
|
61
|
+
// 而真因是模块坏了——他会去 registry 反复核对,问题永远查不到。故载入/调用失败均出明确 WARN。
|
|
62
|
+
const adopterModRel = '.agents/metric-derivers.cjs';
|
|
63
|
+
// 旧路径(首版 .mjs):若存在则**明确提示已改用 .cjs**,避免装户对着「无取数器」查不出所以然
|
|
64
|
+
const adopterLegacyRel = '.agents/metric-derivers.mjs';
|
|
65
|
+
let adopterState = null; // { ok:true, derivers } | { ok:false, errors:[...] } —— 懒载入 + 单次运行内缓存
|
|
66
|
+
const loadAdopterDerivers = () => {
|
|
67
|
+
if (adopterState) return adopterState;
|
|
68
|
+
const abs = path.join(ROOT, adopterModRel);
|
|
69
|
+
if (!fs.existsSync(abs)) {
|
|
70
|
+
// 常见误写:仍用首版的 .mjs 路径 → 给出可操作的迁移指引(而非任其对着「无取数器」猜)
|
|
71
|
+
if (fs.existsSync(path.join(ROOT, adopterLegacyRel))) {
|
|
72
|
+
adopterState = {
|
|
73
|
+
ok: false,
|
|
74
|
+
errors: [`${adopterModRel} 不存在,但发现旧版路径 ${adopterLegacyRel}——该路径已于 2026-09-28 弃用(\`require(esm)\` 在 Node 18/22 不可用,实测 ERR_REQUIRE_ESM)。请改名为 ${adopterModRel} 并把写法改为 CommonJS:\`module.exports = { derivers: { ... } }\``],
|
|
75
|
+
};
|
|
76
|
+
return adopterState;
|
|
77
|
+
}
|
|
78
|
+
adopterState = { ok: true, derivers: {} }; // 缺失=正常态,零告警
|
|
79
|
+
return adopterState;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const req = createRequire(import.meta.url);
|
|
83
|
+
const mod = req(abs);
|
|
84
|
+
// CJS / ESM 双兼容取导出:CJS 走 module.exports.derivers;若装户误用了 ESM 语法而环境恰好
|
|
85
|
+
// 支持(新 Node 的 require(esm)),default 里也可能挂 derivers——两处都认,避免假阴性。
|
|
86
|
+
const d = (mod && mod.derivers) || (mod && mod.default && mod.default.derivers);
|
|
87
|
+
if (!d || typeof d !== 'object' || Array.isArray(d)) {
|
|
88
|
+
adopterState = { ok: false, errors: [`${adopterModRel} 未导出 \`derivers\` 对象(实际导出形态:${d === undefined ? '无 derivers 键' : Array.isArray(d) ? 'array' : typeof d})——载入失败不降级,请用 \`module.exports = { derivers: { ... } }\``] };
|
|
89
|
+
return adopterState;
|
|
90
|
+
}
|
|
91
|
+
adopterState = { ok: true, derivers: d };
|
|
92
|
+
} catch (e) {
|
|
93
|
+
const hint = /ERR_REQUIRE_ESM/.test(String(e && e.code)) ? `(该错误表示模块被当成了 ESM——本契约要求 CommonJS:文件后缀 .cjs 且用 \`module.exports = { derivers: {...} }\`)` : '';
|
|
94
|
+
adopterState = { ok: false, errors: [`${adopterModRel} 载入失败:${String(e && e.message ? e.message : e).split('\n')[0]}${hint}——载入失败不降级(静默降级会让你误以为「忘了登记」而非「模块坏了」)`] };
|
|
95
|
+
}
|
|
96
|
+
return adopterState;
|
|
97
|
+
};
|
|
98
|
+
// deriverCtx:注入给装户取数器的同步辅助(免其重复造轮子;2026-09-28 check16-inline-debt 迁出时
|
|
99
|
+
// 自 `ctx` 改名——runCheck16 的参数 ctx 已被外层符号注入契约占用,避免遮蔽混淆)
|
|
100
|
+
// **glob 逐字符转义**(自查修正):首版用「先整体转义特殊字符、再 replace 星号」的写法,
|
|
101
|
+
// 实测 `src/**/*.ts` 匹配不到 `src/a/b/c.ts`、「**/*.md」匹配不到 `a/b/c.md`——因为 `**/`
|
|
102
|
+
// 在被替换前已被字符类转义破坏。改为逐字符状态机,语义明确:
|
|
103
|
+
// `**/` → 任意层级(含零层);`**` → 任意(可跨 /);`*` → 段内任意(不跨 /);`?` → 段内单字符
|
|
104
|
+
// 其余一律字面转义(故 `a.b.sql` 的点是字面点、`[`/`{` 也按字面处理——本 helper 不承诺
|
|
105
|
+
// 字符类 / 花括号展开,避免装户误以为支持完整 glob 语法;不支持即按字面匹配,行为可预测)
|
|
106
|
+
const globToRegExp = (p) => {
|
|
107
|
+
const s = String(p);
|
|
108
|
+
let re = '';
|
|
109
|
+
for (let i = 0; i < s.length; i++) {
|
|
110
|
+
const c = s[i];
|
|
111
|
+
if (c === '*' && s[i + 1] === '*') {
|
|
112
|
+
if (s[i + 2] === '/') { re += '(?:.*/)?'; i += 2; } // `**/` 含零层
|
|
113
|
+
else { re += '.*'; i += 1; } // `**` 跨段
|
|
114
|
+
} else if (c === '*') re += '[^/]*';
|
|
115
|
+
else if (c === '?') re += '[^/]';
|
|
116
|
+
else re += c.replace(/[.+^${}()|[\]\\]/, '\\$&');
|
|
117
|
+
}
|
|
118
|
+
return new RegExp('^' + re + '$');
|
|
119
|
+
};
|
|
120
|
+
// 遍历时**跳过重目录 + 防环**(自查 + 复核 P2 修正):
|
|
121
|
+
// · 剪枝 node_modules / .git / cache——无剪枝时 `deriverCtx.glob('**/*')` 会走进依赖树,既慢又会把
|
|
122
|
+
// 依赖文件算进取数(对「我项目有多少个 X」是错的数)。
|
|
123
|
+
// · **visited 集合防环**:复核实测软链接/junction 成环时递归到 depth 128、同一文件重复 64 份
|
|
124
|
+
// 才被 statSync 的 ELOOP 拦下;POSIX 无 MAX_PATH 时该递归无界。故按**真实路径**去重。
|
|
125
|
+
const WALK_SKIP = new Set(['node_modules', '.git', 'cache']);
|
|
126
|
+
const walkFiles = (dir, out = [], rel = '', visited = new Set()) => {
|
|
127
|
+
let real = dir;
|
|
128
|
+
try { real = fs.realpathSync(dir); } catch { /* 不可解析则按原路径处理 */ }
|
|
129
|
+
if (visited.has(real)) return out; // 环 → 剪枝
|
|
130
|
+
visited.add(real);
|
|
131
|
+
for (const e of readdirOrNull(dir) || []) {
|
|
132
|
+
if (WALK_SKIP.has(e)) continue;
|
|
133
|
+
const abs = path.join(dir, e);
|
|
134
|
+
const r = rel ? `${rel}/${e}` : e;
|
|
135
|
+
let st = null;
|
|
136
|
+
try { st = fs.statSync(abs); } catch { continue; }
|
|
137
|
+
if (st.isDirectory()) walkFiles(abs, out, r, visited);
|
|
138
|
+
else out.push(r);
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
};
|
|
142
|
+
const deriverCtx = {
|
|
143
|
+
root: ROOT,
|
|
144
|
+
read: (rel) => { try { return fs.readFileSync(path.join(ROOT, rel), 'utf8').replace(/\r\n/g, '\n'); } catch { return ''; } },
|
|
145
|
+
glob: (pattern) => { const re = globToRegExp(pattern); return walkFiles(ROOT).filter((r) => re.test(r)); },
|
|
146
|
+
countFiles: (dir, pred) => walkFiles(path.join(ROOT, dir)).filter((r) => (typeof pred === 'function' ? pred(r) : true)).length,
|
|
147
|
+
};
|
|
148
|
+
// resolveDeriver(name) → { fn } | { error }(**绝不抛出**:引擎不因装户代码崩溃)
|
|
149
|
+
const resolveDeriver = (name) => {
|
|
150
|
+
if (derivers[name]) return { fn: derivers[name], builtin: true };
|
|
151
|
+
const st = loadAdopterDerivers();
|
|
152
|
+
if (!st.ok) return { error: st.errors.join(';') };
|
|
153
|
+
const fn = st.derivers[name];
|
|
154
|
+
if (typeof fn !== 'function') {
|
|
155
|
+
if (fn !== undefined) return { error: `${adopterModRel} 的「${name}」不是函数(实际类型:${typeof fn})` };
|
|
156
|
+
return { error: `无对应取数器——内置 8 个指标均不含该名,${adopterModRel} 亦未定义(登记了却没实现=其实没查)` };
|
|
157
|
+
}
|
|
158
|
+
return { fn, builtin: false };
|
|
159
|
+
};
|
|
160
|
+
// 装户模块载入失败:**独立成条**(2026-09-28 adopter-derivers)。
|
|
161
|
+
// 关键设计(真绿 vs 假绿的分界):载入失败若只表现为「该指标无取数器」,装户会看到
|
|
162
|
+
// 「引用了 my.metric → 未登记」,看起来像**自己忘了登记**,而真因是模块坏了——他会去 registry
|
|
163
|
+
// 反复核对,永远查不到。故此处单独出一条,明示文件与错误,与「真的没登记」可区分。
|
|
164
|
+
//
|
|
165
|
+
// **仅在真的需要装户指标时才报**(复核 P2 更正):首版在解析登记表前无条件出账——若登记表只有
|
|
166
|
+
// 内置指标、而装户留了个半成品模块(或模块坏掉但没人用),会平白多一条不可消除的 advisory。
|
|
167
|
+
// 沿本仓红线「无判定依据的行不产出噪声」:先看登记表是否真的有**非内置**指标,再决定报不报。
|
|
168
|
+
const mcLines = (linesOf(mcFile) || []).map((l) => l.trim()).filter((l) => l && !l.startsWith('#'));
|
|
169
|
+
const hasAdopterMetric = mcLines.some((l) => {
|
|
170
|
+
const mm = /^([A-Za-z][\w.]*)\s*=\s*([A-Za-z][\w.]*)$/.exec(l);
|
|
171
|
+
return mm && !derivers[mm[1]];
|
|
172
|
+
});
|
|
173
|
+
const adopterLoad = loadAdopterDerivers();
|
|
174
|
+
if (!adopterLoad.ok && hasAdopterMetric) {
|
|
175
|
+
for (const e of adopterLoad.errors) warnings.push(`- [WARN 装户取数器载入失败] ${e}`);
|
|
176
|
+
}
|
|
177
|
+
// 解析登记表:`<指标名> = <取数表达式>`(表达式须与指标名同形,二者不一致即登记笔误)
|
|
178
|
+
const declared = new Map();
|
|
179
|
+
const precedenceWarned = new Set(); // 同名 advisory 按指标去重(复核 P2 更正:首版按行重复出账)
|
|
180
|
+
for (const line of mcLines) {
|
|
181
|
+
const m = /^([A-Za-z][\w.]*)\s*=\s*([A-Za-z][\w.]*)$/.exec(line);
|
|
182
|
+
if (!m) { warnings.push(`- [WARN 指标登记] .agents/metric-claims.txt 行格式非法(应为 \`指标名 = 取数表达式\`):${line}`); continue; }
|
|
183
|
+
if (m[1] !== m[2]) { warnings.push(`- [WARN 指标登记] 登记名与取数表达式不一致:${m[1]} ≠ ${m[2]}`); continue; }
|
|
184
|
+
const r = resolveDeriver(m[1]);
|
|
185
|
+
if (r.error) {
|
|
186
|
+
// 无取数器(内置无 + 装户模块无同名)或装户模块载入失败 → 明示可操作方向
|
|
187
|
+
const hint = adopterLoad.ok
|
|
188
|
+
? `若为项目自有指标,请在 ${adopterModRel} 的 derivers 中实现(契约见 .agents/metric-claims.txt 头部)`
|
|
189
|
+
: `且 ${adopterModRel} 载入失败(见上方「装户取数器载入失败」条目——**这不是「忘了登记」**)`;
|
|
190
|
+
warnings.push(`- [WARN 指标登记] 指标「${m[1]}」无对应取数器(fail-loud:登记了却没实现=其实没查)——内置 8 指标不含该名,${hint}`);
|
|
191
|
+
// **不 continue**:该指标在 registry 里确实登记过,故仍计入 declared——
|
|
192
|
+
// 否则引用它的文档会被误报为「未登记」(把「模块坏了」伪装成「忘了登记」,正是本单要防的假绿)。
|
|
193
|
+
// 取数失败在扫描阶段以「指标取数失败」单独出账,两条信息不互相掩盖。
|
|
194
|
+
} else if (r.builtin && adopterLoad.ok && typeof adopterLoad.derivers[m[1]] === 'function' && !precedenceWarned.has(m[1])) {
|
|
195
|
+
// 内置与装户同名:内置优先(保持本仓/存量行为稳定),并明示装户定义被忽略(**按指标去重**)
|
|
196
|
+
precedenceWarned.add(m[1]);
|
|
197
|
+
warnings.push(`- [WARN 指标登记] 指标「${m[1]}」由引擎内置提供,${adopterModRel} 中的同名定义**被忽略**(内置优先)——如需改用自有实现,请换一个指标名`);
|
|
198
|
+
}
|
|
199
|
+
declared.set(m[1], true);
|
|
200
|
+
}
|
|
201
|
+
// 扫活跃态文档(沿检查 4 现有尺度:终态件的历史数字是历史叙述,不扫)。
|
|
202
|
+
// tracked-only 口径(2026-09-28 check16-inline-debt T2,与检查 1-15 统一):
|
|
203
|
+
// 子目录走 ctx.docFiles(sub)——过滤链(.md ∧ 数字前缀 ∧ tracked)全仓唯一处定义,首版独立
|
|
204
|
+
// readdir 扫描不含 tracked 过滤,会把并行会话的**未跟踪**半成品也拖进对账面(别人的草稿不该
|
|
205
|
+
// 拦我的 push——沿仓库模式「只扫已提交(HEAD)内容」总口径);
|
|
206
|
+
// 根级 workflow/*.md 滤 .md ∧ tracked(无数字前缀约束——README/INDEX/delegations 等本无前缀,
|
|
207
|
+
// 维持现状;根级本无 _TEMPLATE,无需显式排除)。
|
|
208
|
+
const activeSet16 = (sub) => (sub === 'incidents' ? ENUMS['incident.status.active'] : ENUMS['doc.status.active']);
|
|
209
|
+
const scanFiles16 = [];
|
|
210
|
+
for (const sub of ['intents', 'specs', 'plans', 'incidents']) {
|
|
211
|
+
for (const abs of docFiles(sub)) {
|
|
212
|
+
if (!inSet(fmGet(abs, '状态'), activeSet16(sub))) continue;
|
|
213
|
+
scanFiles16.push({ rel: path.relative(ROOT, abs).split(path.sep).join('/'), abs });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
for (const f of readdirOrNull(path.join(ROOT, 'workflow')) || []) {
|
|
217
|
+
const abs = path.join(ROOT, 'workflow', f);
|
|
218
|
+
if (f.endsWith('.md') && isTracked(abs)) scanFiles16.push({ rel: `workflow/${f}`, abs });
|
|
219
|
+
}
|
|
220
|
+
for (const { rel, abs } of scanFiles16) {
|
|
221
|
+
const ls = linesOf(abs) || [];
|
|
222
|
+
ls.forEach((line, i) => {
|
|
223
|
+
// 签名形态收窄为**小写点分**(如 `{{ledger.lines}}`):装户模板占位符是全大写 SCREAMING_CASE
|
|
224
|
+
// (构建命令 / 看板端口 / 项目名三类,由 init 渲染替换,非本检查对象)
|
|
225
|
+
// ——不收窄会对这些合法的模板占位符产生大量假阳性(实测:本仓 plans 内 11 处)。
|
|
226
|
+
// **转义(2026-09-28,本检查在自己的文档上抓到该需求)**:讲语法 / 举例 / 引用告警原文的
|
|
227
|
+
// 文档在签名前加反斜杠(`\{{ledger.lines}}`)表示「此处是示意、非断言」——否则本检查
|
|
228
|
+
// 会把「文档在教怎么用签名」误判为「作者忘了回填」。选显式转义而非「同句含『如/例』等标记词
|
|
229
|
+
// 即豁免」的启发式:后者会按措辞松紧漂移,且作者能无意中触发豁免而漏过真断言。
|
|
230
|
+
// **转义按「逐个出现」生效、不按整行**(复核自查 P1 更正):首版实现是「行内出现任一 `\{{`
|
|
231
|
+
// 即整行跳过」,实测可被这样藏住真断言——`… \{{a.b}} 示意… {{ledger.lines}} 未回填` 整行静默
|
|
232
|
+
// 放过。故改为用带后顾的否匹配排除转义位,同行的真签名照常出账。
|
|
233
|
+
for (const m of line.matchAll(/(?<!\\)\{\{([a-z][a-z0-9]*(?:\.[a-z0-9]+)+)\}\}/g)) {
|
|
234
|
+
const name = m[1];
|
|
235
|
+
if (!declared.has(name)) {
|
|
236
|
+
warnings.push(`- [WARN 指标未登记] ${rel}:${i + 1} 引用 {{${name}}} 但 .agents/metric-claims.txt 未登记该指标(未登记=无从对账)`);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const r = resolveDeriver(name);
|
|
240
|
+
if (r.error) {
|
|
241
|
+
// 取数器不可用(含装户模块载入失败 / 无该名 / 定义非法)——**响亮出账,绝不静默跳过**
|
|
242
|
+
warnings.push(`- [WARN 指标取数失败] ${rel}:${i + 1} {{${name}}} 无法取数:${r.error}`);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
let real;
|
|
246
|
+
try {
|
|
247
|
+
real = r.fn(deriverCtx);
|
|
248
|
+
} catch (e) {
|
|
249
|
+
warnings.push(`- [WARN 指标取数失败] ${rel}:${i + 1} {{${name}}} 取数器抛异常:${String(e && e.message ? e.message : e).split('\n')[0]}`);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
// 返回值必须是有限数字:Promise(误写 async)/ NaN / 字符串 / undefined 一律拦下并明示类型
|
|
253
|
+
if (typeof real !== 'number' || !Number.isFinite(real)) {
|
|
254
|
+
const kind = real && typeof real.then === 'function' ? 'Promise(本检查要求**同步**函数,请去掉 async/await)' : `${typeof real}${typeof real === 'number' ? `(${real})` : ''}`;
|
|
255
|
+
warnings.push(`- [WARN 指标取数失败] ${rel}:${i + 1} {{${name}}} 取数器返回值不是有限数字:${kind}`);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
// 判据:签名**必须**被替换为实时值——留有 `{{...}}` 即视为未回填(写作期占位,关单前须落实)
|
|
259
|
+
warnings.push(`- [WARN 指标待回填] ${rel}:${i + 1} {{${name}}} 实时值 = ${real}——请把签名替换为该数字(留签名=未回填)`);
|
|
260
|
+
}
|
|
261
|
+
// **形态不符的签名也不静默放过(复核 P2 更正)**:首版只认严格小写点分形态,于是
|
|
262
|
+
// `{{Ledger.Lines}}` / `{{ ledger.lines }}`(含空格)这类**明显本意是签名**的写法
|
|
263
|
+
// 既不匹配、也不出账——静默漏过,且绕开了「未登记」的 fail-loud。此处补一道:
|
|
264
|
+
// 非转义的 `{{...}}` 中,凡**不是**严格形态、且**不是**装户模板占位符(全大写 SCREAMING_CASE)
|
|
265
|
+
// 的,一律提示形态不符(作者才能立刻发现笔误)。
|
|
266
|
+
for (const m of line.matchAll(/(?<!\\)\{\{([^{}]+)\}\}/g)) {
|
|
267
|
+
const raw = m[1];
|
|
268
|
+
if (/^[a-z][a-z0-9]*(?:\.[a-z0-9]+)+$/.test(raw)) continue; // 严格形态,上面已处理
|
|
269
|
+
if (/^[A-Z][A-Z0-9_]*$/.test(raw)) continue; // 装户模板占位符(init 渲染对象,非本检查面)
|
|
270
|
+
warnings.push(`- [WARN 指标形态] ${rel}:${i + 1} \`{{${raw}}}\` 不是合法签名形态(应为小写点分,如 {{ledger.lines}})——若本意是量化断言,请改正形态;若只是举例,请加反斜杠转义`);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
@@ -37,7 +37,7 @@ const mkfix = () => {
|
|
|
37
37
|
],
|
|
38
38
|
checks: [],
|
|
39
39
|
}));
|
|
40
|
-
return { root, marks };
|
|
40
|
+
return { root, marks, cmd };
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
// stage:写文件并暂存,跑 hook(extraArgs 透传如 --full),返回 { status, out, ran }
|
|
@@ -87,6 +87,69 @@ stage(f.root, 'unrelated.txt', 'x');
|
|
|
87
87
|
r = runHook(f.root, ['--full']);
|
|
88
88
|
check('S5 --full 无条件全跑全部 builds', ran(f.marks, 'extbuild') && ran(f.marks, 'prefixbuild') && r.status === 0, r.out);
|
|
89
89
|
|
|
90
|
+
// 场景 6:builds.when = pkg:scripts.build。无脚本则 SKIP 且不执行;文件型 when 缺失仍跳过。
|
|
91
|
+
f = mkfix();
|
|
92
|
+
fs.writeFileSync(path.join(f.root, '.agents', 'hooks', 'commit-check.config.json'), JSON.stringify({
|
|
93
|
+
knownPatterns: [],
|
|
94
|
+
builds: [{ name: 'build', command: f.cmd('build'), ext: ['.js'], when: ['pkg:scripts.build'] }],
|
|
95
|
+
checks: [{ name: 'lint', command: f.cmd('lint'), ext: ['.js'], when: ['eslint.config.js'] }],
|
|
96
|
+
}));
|
|
97
|
+
fs.writeFileSync(path.join(f.root, 'package.json'), JSON.stringify({ name: 't', scripts: {} }));
|
|
98
|
+
stage(f.root, 'src/a.js', 'console.log(1)\n');
|
|
99
|
+
r = runHook(f.root);
|
|
100
|
+
check('S6 无 scripts.build → 构建 SKIP、命令不跑、退出 0',
|
|
101
|
+
r.status === 0 && !ran(f.marks, 'build') && r.out.includes('pkg:scripts.build') && r.out.includes('SKIP')
|
|
102
|
+
&& !r.out.includes('无匹配的源码变更'), r.out);
|
|
103
|
+
check('S6 文件型 when 缺失仍跳过(eslint.config.js)', !ran(f.marks, 'lint') && r.out.includes('eslint.config.js'), r.out);
|
|
104
|
+
|
|
105
|
+
// 场景 7:scripts.build 为非空字符串 → 构建命令执行;补上 eslint 配置后质量检查也执行。
|
|
106
|
+
f = mkfix();
|
|
107
|
+
fs.writeFileSync(path.join(f.root, '.agents', 'hooks', 'commit-check.config.json'), JSON.stringify({
|
|
108
|
+
knownPatterns: [],
|
|
109
|
+
builds: [{ name: 'build', command: f.cmd('build'), ext: ['.js'], when: ['pkg:scripts.build'] }],
|
|
110
|
+
checks: [{ name: 'lint', command: f.cmd('lint'), ext: ['.js'], when: ['eslint.config.js'] }],
|
|
111
|
+
}));
|
|
112
|
+
fs.writeFileSync(path.join(f.root, 'package.json'), JSON.stringify({ name: 't', scripts: { build: 'node -e 0' } }));
|
|
113
|
+
fs.writeFileSync(path.join(f.root, 'eslint.config.js'), 'export default [];\n');
|
|
114
|
+
stage(f.root, 'src/a.js', 'console.log(1)\n');
|
|
115
|
+
r = runHook(f.root);
|
|
116
|
+
check('S7 有 scripts.build 且 eslint 配置存在 → 构建与质量检查都执行',
|
|
117
|
+
r.status === 0 && ran(f.marks, 'build') && ran(f.marks, 'lint'), r.out);
|
|
118
|
+
|
|
119
|
+
// 场景 8:--full 也不越过 pkg: when(没有脚本就不跑)
|
|
120
|
+
f = mkfix();
|
|
121
|
+
fs.writeFileSync(path.join(f.root, '.agents', 'hooks', 'commit-check.config.json'), JSON.stringify({
|
|
122
|
+
knownPatterns: [],
|
|
123
|
+
builds: [{ name: 'build', command: f.cmd('build'), ext: ['.js'], when: ['pkg:scripts.build'] }],
|
|
124
|
+
checks: [],
|
|
125
|
+
}));
|
|
126
|
+
fs.writeFileSync(path.join(f.root, 'package.json'), JSON.stringify({ name: 't' }));
|
|
127
|
+
stage(f.root, 'src/a.js', 'console.log(1)\n');
|
|
128
|
+
r = runHook(f.root, ['--full']);
|
|
129
|
+
check('S8 --full 仍尊重 pkg:scripts.build',
|
|
130
|
+
r.status === 0 && !ran(f.marks, 'build') && r.out.includes('SKIP') && !r.out.includes('无匹配的源码变更'), r.out);
|
|
131
|
+
|
|
132
|
+
// 场景 9:pkg:scripts.build 四种不满足都不执行(空白、非字符串、非法 JSON、没有 package.json)
|
|
133
|
+
const unsatisfied = [
|
|
134
|
+
['空白字符串', (root) => fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 't', scripts: { build: ' ' } }))],
|
|
135
|
+
['非字符串', (root) => fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 't', scripts: { build: 1 } }))],
|
|
136
|
+
['非法 JSON', (root) => fs.writeFileSync(path.join(root, 'package.json'), '{')],
|
|
137
|
+
['缺少 package.json', () => {}],
|
|
138
|
+
];
|
|
139
|
+
for (const [label, writePkg] of unsatisfied) {
|
|
140
|
+
f = mkfix();
|
|
141
|
+
fs.writeFileSync(path.join(f.root, '.agents', 'hooks', 'commit-check.config.json'), JSON.stringify({
|
|
142
|
+
knownPatterns: [],
|
|
143
|
+
builds: [{ name: 'build', command: f.cmd('build'), ext: ['.js'], when: ['pkg:scripts.build'] }],
|
|
144
|
+
checks: [],
|
|
145
|
+
}));
|
|
146
|
+
writePkg(f.root);
|
|
147
|
+
stage(f.root, 'src/a.js', 'console.log(1)\n');
|
|
148
|
+
r = runHook(f.root);
|
|
149
|
+
check(`S9 ${label} → SKIP、命令不跑、不报无匹配`,
|
|
150
|
+
r.status === 0 && !ran(f.marks, 'build') && r.out.includes('pkg:scripts.build') && !r.out.includes('无匹配的源码变更'), r.out);
|
|
151
|
+
}
|
|
152
|
+
|
|
90
153
|
for (const d of fs.readdirSync(os.tmpdir()).filter((x) => x.startsWith('cc-trigger-test-'))) {
|
|
91
154
|
fs.rmSync(path.join(os.tmpdir(), d), { recursive: true, force: true });
|
|
92
155
|
}
|
|
@@ -23,15 +23,23 @@
|
|
|
23
23
|
// 指纹:内容 CRLF 归一 → 剔除「确认指纹:」行(防自引用)→ sha256;frontmatter 存前 16 位,台账存全量。
|
|
24
24
|
// 用法:node .agents/scripts/confirm-doc.mjs <workflow/intents|x.md> [<doc2>...] [--root <仓库根>]
|
|
25
25
|
// node .agents/scripts/confirm-doc.mjs <doc...> --delegated "<用户对话原话>"(委托代录)
|
|
26
|
+
// node .agents/scripts/confirm-doc.mjs <doc...> --delegated "<原话>" --batch(协作道批量:L0/L1 多份一次代录)
|
|
26
27
|
// node .agents/scripts/confirm-doc.mjs <doc...> --to superseded|cancelled(放弃态:取代/取消,留指纹与台账)
|
|
27
|
-
//
|
|
28
|
-
//
|
|
28
|
+
// 多文档一次传入:TTY 模式天然逐份过目键入;--delegated 默认一次仅一份(2026-09-27
|
|
29
|
+
// confirm-gate-one-per-call:多份并录曾系统性塌掉 build.md「逐件确认」三道门),L0/L1 协作道可
|
|
30
|
+
// `--batch` 多份一次代录(2026-09-30 hybrid-governance-risk-lanes):逐份读「级别」,L2/L3 逐份拒绝
|
|
31
|
+
// (防御道不批量);台账行带 `brief:true` 简洁审计标记(quote 原话仍入账供对质),check-loop 检查 15
|
|
32
|
+
// 并录审计对全行带标记的批次豁免「确认并录」告警。--auto 一次仍仅一份(自治放行不批量)。
|
|
33
|
+
// 逐阶段前置门(2026-09-30 stage-gate-machine):specs/plans 的 draft→approved 须同主题入口已确认
|
|
34
|
+
// (plan 另须入口级别 L2/L3 时同名 spec 已确认;判定口径见 stage-gates.mjs)——未过拒绝落账:
|
|
35
|
+
// 不写盘、不 append 台账、该份计为「拒绝」,任一被拒进程 exit 2。
|
|
29
36
|
// 测试:node templates/_agents/scripts/confirm-doc.test.mjs(纯函数逐项 + 非 TTY spawn 拒绝断言 + 委托场景)
|
|
30
37
|
import fs from 'node:fs';
|
|
31
38
|
import path from 'node:path';
|
|
32
39
|
import readline from 'node:readline';
|
|
33
40
|
import { createHash, randomBytes } from 'node:crypto';
|
|
34
41
|
import { fileURLToPath } from 'node:url';
|
|
42
|
+
import { confirmGateFor, doneGateFor } from './stage-gates.mjs';
|
|
35
43
|
|
|
36
44
|
// computeFingerprint(text):CRLF 归一 → 剔指纹行 → sha256 hex(64 位)
|
|
37
45
|
export function computeFingerprint(text) {
|
|
@@ -97,17 +105,31 @@ export function appendLedger(root, entry) {
|
|
|
97
105
|
|
|
98
106
|
const DOC_RE = /^workflow\/(intents|specs|plans|incidents)\/[^/]+\.md$/;
|
|
99
107
|
|
|
108
|
+
// fmLevel(nl):frontmatter「级别:」值(无 frontmatter / 无该键 → '')——AI 自治门与协作道批量门共用
|
|
109
|
+
export function fmLevel(nl) {
|
|
110
|
+
if (!/^---\s*$/.test(nl[0] || '')) return '';
|
|
111
|
+
for (let i = 1; i < nl.length && !/^---\s*$/.test(nl[i]); i++) {
|
|
112
|
+
const m = nl[i].match(/^级别:\s*(.*)$/);
|
|
113
|
+
if (m) return m[1].trim();
|
|
114
|
+
}
|
|
115
|
+
return '';
|
|
116
|
+
}
|
|
117
|
+
|
|
100
118
|
const isMain = process.argv[1] && process.argv[1].endsWith('confirm-doc.mjs');
|
|
101
119
|
if (isMain) {
|
|
102
120
|
const argv = process.argv.slice(2);
|
|
103
121
|
let root = process.cwd();
|
|
104
122
|
let delegatedQuote = null; // null = TTY 模式;字符串 = 委托代录的用户对话原话
|
|
105
123
|
let toTarget = null; // null = 默认前向跳转;superseded|cancelled = 放弃态显式目标
|
|
124
|
+
let autoMode = false; // AI 自治模式(信任等级授权,2026-09-30 ai-autonomy-trust)
|
|
125
|
+
let batchMode = false; // 协作道批量代录(--batch,2026-09-30 hybrid-governance-risk-lanes:仅 --delegated + L0/L1)
|
|
106
126
|
const docs = [];
|
|
107
127
|
for (let i = 0; i < argv.length; i++) {
|
|
108
128
|
if (argv[i] === '--root') root = path.resolve(argv[++i]);
|
|
109
129
|
else if (argv[i] === '--delegated') delegatedQuote = argv[++i] ?? '';
|
|
110
130
|
else if (argv[i] === '--to') toTarget = argv[++i] ?? '';
|
|
131
|
+
else if (argv[i] === '--auto') autoMode = true;
|
|
132
|
+
else if (argv[i] === '--batch') batchMode = true;
|
|
111
133
|
else docs.push(argv[i].replace(/\\/g, '/'));
|
|
112
134
|
}
|
|
113
135
|
if (toTarget !== null && !ABANDON_TARGETS.includes(toTarget)) {
|
|
@@ -115,6 +137,40 @@ if (isMain) {
|
|
|
115
137
|
process.exit(1);
|
|
116
138
|
}
|
|
117
139
|
const delegated = delegatedQuote !== null;
|
|
140
|
+
if (delegated && autoMode) {
|
|
141
|
+
console.error('❌ --auto 与 --delegated 互斥:自治模式是用户预先授权的机器放行,委托代录是单次对话确认');
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
if (batchMode && autoMode) {
|
|
145
|
+
console.error('❌ --batch 与 --auto 互斥:批量代录是协作道的对话委托形态,自治放行不批量(一次一份)');
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
if (batchMode && toTarget) {
|
|
149
|
+
console.error('❌ --batch 不支持 --to 放弃态——放弃须逐份显式确认,不批量');
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
// AI 自治门(2026-09-30 ai-autonomy-trust;2026-09-30 hybrid-governance-explore-hardening 加隐式形态):
|
|
153
|
+
// 读 .agents/trust-mode.json。除显式 --auto 外,Trusted(level=2)下的非 TTY 单份调用在 TTY 门处
|
|
154
|
+
// 免旗标自动转入本分支(见「Trusted 自动泳道」)——L0/L1 门 / incidents 排除 / 逐阶段与 done 门照常执行。
|
|
155
|
+
let trustConfig = null;
|
|
156
|
+
if (autoMode) {
|
|
157
|
+
try {
|
|
158
|
+
trustConfig = JSON.parse(fs.readFileSync(path.join(root, '.agents', 'trust-mode.json'), 'utf8'));
|
|
159
|
+
} catch { trustConfig = { enabled: false, level: 0 }; }
|
|
160
|
+
if (!trustConfig.enabled || ![1, 2].includes(trustConfig.level)) {
|
|
161
|
+
console.error('❌ AI 自治未开启或等级非法(.agents/trust-mode.json enabled=false 或 level∉[1,2])——跑 node .agents/scripts/trust-mode.mjs --enable --level <1|2> 开启');
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
if (toTarget) {
|
|
165
|
+
console.error('❌ AI 自治模式不支持 --to 放弃态(放弃态必须由用户显式确认)');
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
if (docs.length > 1) {
|
|
169
|
+
console.error('❌ AI 自治模式一次仅接受一份文档(与 --delegated 同口径,逐件放行)');
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
console.error(`⚠️ AI 自治模式(Trust L${trustConfig.level}):仅限 L0/L1 文档;Level 1 只放行 draft→approved,Level 2 放行至 done`);
|
|
173
|
+
}
|
|
118
174
|
if (delegated && !String(delegatedQuote).trim()) {
|
|
119
175
|
console.error('用法:--delegated 须带用户对话原话(node .agents/scripts/confirm-doc.mjs <doc...> --delegated "<用户原话>")——原话入台账供事后对质,不可缺省');
|
|
120
176
|
process.exit(1);
|
|
@@ -122,25 +178,51 @@ if (isMain) {
|
|
|
122
178
|
// TTY 门(仅 TTY 模式;先于文档参数校验):AI 会话内非委托调用一律拒绝
|
|
123
179
|
// CONFIRM_DOC_TEST_TTY:**仅测试用**逃生门——本测试环境无 TTY,而 TTY 多文档分支
|
|
124
180
|
// (batch 相同 / seq 递增 / of=N)是并录审计的判别基础,必须可测(复核 P2-2)。
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
181
|
+
// 双条件(2026-09-28 check16-inline-debt):须同时 NODE_ENV === 'test' 才放行——单看
|
|
182
|
+
// 「TEST」字样的变量名不构成机器约束,AI 会话可能「顺手」在生产路径设置它绕过 TTY 门;
|
|
183
|
+
// 加 NODE_ENV 双条件后,生产路径(不设 NODE_ENV=test)即便误设 CONFIRM_DOC_TEST_TTY 也不生效。
|
|
184
|
+
// **信任边界声明(沿 check8-git-anchor 口径)**:本地信任边界内不可机器防——环境变量终究可被
|
|
185
|
+
// 本机进程任意设置,刻意伪造 `NODE_ENV=test CONFIRM_DOC_TEST_TTY=1` 仍可绕过,与伪造 git 时间戳 /
|
|
186
|
+
// 手改台账同属事后对质(git 历史与台账)兜底的范畴;此处只把「顺手设置单变量」的成本抬到
|
|
187
|
+
// 「主动伪造双变量组合」,不宣称「通道已关闭」。
|
|
188
|
+
// 两变量只影响 isTTY 判定,不改动任何落态/记账语义;生产路径不设置这两个变量。
|
|
189
|
+
const ttyForced = process.env.CONFIRM_DOC_TEST_TTY === '1' && process.env.NODE_ENV === 'test';
|
|
190
|
+
if (!delegated && !autoMode && !ttyForced && (!process.stdin.isTTY || !process.stdout.isTTY)) {
|
|
191
|
+
// Trusted 自动泳道(2026-09-30 hybrid-governance-explore-hardening):trust-mode.json 为 Trusted
|
|
192
|
+
// (enabled 且 level=2)时,非 TTY 调用(AI 会话)对文档**免手工触发**(无需 --delegated 原话 /
|
|
193
|
+
// --auto 旗标)自动转入 AI 自治放行——单份、非放弃态才可隐式(与 --auto 同口径,L0/L1 门与
|
|
194
|
+
// incidents 排除由主循环自治门照常执行);不满足则维持原拒绝(fail-closed,Standard/Strict 不隐式)。
|
|
195
|
+
let implicitTrust = null;
|
|
196
|
+
try {
|
|
197
|
+
implicitTrust = JSON.parse(fs.readFileSync(path.join(root, '.agents', 'trust-mode.json'), 'utf8'));
|
|
198
|
+
} catch { implicitTrust = { enabled: false, level: 0 }; }
|
|
199
|
+
if (implicitTrust.enabled && implicitTrust.level === 2 && docs.length === 1 && !toTarget) {
|
|
200
|
+
autoMode = true;
|
|
201
|
+
trustConfig = implicitTrust; // 主循环自治门(L0/L1 与 incidents 排除)直读本配置——顶部 if (autoMode) 块先于 TTY 门执行,须在此补装
|
|
202
|
+
console.error('⚠️ Trusted 自动泳道(隐式):单份 L0/L1 文档按 AI 自治放行(Level 2 全闭环,台账 source=ai-auto-trust-L2);L2/L3 与 incidents 仍须人工确认');
|
|
203
|
+
} else {
|
|
204
|
+
console.error('确认门须由用户在终端亲手运行(node .agents/scripts/confirm-doc.mjs <workflow/文档>...)——AI 会话内不可代确认;用户对话内明确确认后可用 --delegated "<用户原话>" 委托代录(台账如实记来源)');
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
130
207
|
}
|
|
131
208
|
if (!docs.length) {
|
|
132
|
-
console.error('用法:node .agents/scripts/confirm-doc.mjs <workflow/intents|specs|plans/x.md> [...] [--root <仓库根>] [--delegated "<用户原话>"]');
|
|
209
|
+
console.error('用法:node .agents/scripts/confirm-doc.mjs <workflow/intents|specs|plans/x.md> [...] [--root <仓库根>] [--delegated "<用户原话>" [--batch]]');
|
|
133
210
|
process.exit(1);
|
|
134
211
|
}
|
|
135
212
|
if (delegated) {
|
|
136
213
|
console.error('⚠️ 委托代录模式:确认语义 = 用户已在对话中明确放行;台账行将如实记 source=chat-delegated 与原话,不伪装 TTY 确认');
|
|
137
214
|
console.error(` 用户原话:「${delegatedQuote}」`);
|
|
138
|
-
//
|
|
215
|
+
// 单文档默认(2026-09-27 confirm-gate-one-per-call):delegated 没有TTY「逐份过目」的天然机制——
|
|
139
216
|
// 多文档并录曾系统性塌掉三道阶段门(build.md「逐件确认不得并作一次」)。逐件调用:一次一份、
|
|
140
217
|
// 每次带当次用户原话。TTY 形态不受限(用户亲手逐份过目键入,天然逐件)。
|
|
218
|
+
// 协作道批量(2026-09-30 hybrid-governance-risk-lanes):L0/L1 可 `--batch` 多份一次代录——
|
|
219
|
+
// 逐份读「级别」,L2/L3 逐份拒绝(见主循环协作道门);台账行带 brief:true,检查 15 据此豁免并录告警。
|
|
141
220
|
if (docs.length > 1) {
|
|
142
|
-
|
|
143
|
-
|
|
221
|
+
if (!batchMode) {
|
|
222
|
+
console.error(`❌ --delegated 一次仅接受一份文档(现 ${docs.length} 份:${docs.join(' ')})——逐件确认口径(build.md):每份一次调用、每次带当次用户原话;L0/L1 协作道批量可加 --batch`);
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
console.error(` 批量代录(--batch):${docs.length} 份,仅受理 L0/L1(协作道,异步审计兜底);L2/L3 逐份拒绝`);
|
|
144
226
|
}
|
|
145
227
|
}
|
|
146
228
|
// 调用事实(2026-09-28 batch-ledger-audit):本次进程调用的批次标识与件序——
|
|
@@ -164,6 +246,7 @@ if (isMain) {
|
|
|
164
246
|
return rl.question(q, res);
|
|
165
247
|
});
|
|
166
248
|
let confirmed = 0;
|
|
249
|
+
let refused = 0; // 逐阶段前置门拒绝数(exit 2 信号;2026-09-30 stage-gate-machine)
|
|
167
250
|
for (const doc of docs) {
|
|
168
251
|
if (!DOC_RE.test(doc)) {
|
|
169
252
|
console.error(`跳过 ${doc}:路径须匹配 workflow/{intents,specs,plans,incidents}/<文件>.md`);
|
|
@@ -183,20 +266,81 @@ if (isMain) {
|
|
|
183
266
|
}
|
|
184
267
|
}
|
|
185
268
|
const target = resolveTransition(st, toTarget);
|
|
269
|
+
const lvl = fmLevel(nl); // 级别(AI 自治门 / 协作道批量门共用;缺 = '')
|
|
186
270
|
if (!target) {
|
|
187
271
|
console.error(`跳过 ${doc}:当前状态「${st || '缺失'}」无合法跳转(前向:docs draft→approved / approved→done;incidents open→fixed / fixed→closed;放弃 --to:cancelled 自 draft/approved/open/fixed,superseded 自 approved/done/fixed/closed)`);
|
|
188
272
|
continue;
|
|
189
273
|
}
|
|
274
|
+
// 逐阶段前置门(2026-09-30 stage-gate-machine):specs/plans 的 draft→approved 须前置已确认
|
|
275
|
+
// (plan 另须入口级别 L2/L3 时同名 spec 已确认)——未过拒绝落账(不写盘、不 append 台账)。
|
|
276
|
+
if (target === 'approved' && (doc.startsWith('workflow/specs/') || doc.startsWith('workflow/plans/'))) {
|
|
277
|
+
const gate = confirmGateFor(root, doc);
|
|
278
|
+
if (!gate.ok) {
|
|
279
|
+
console.error(`❌ ${doc} 未过逐阶段前置门(stage-gates):${gate.reason}`);
|
|
280
|
+
console.error(` ${gate.hint}(本份未落账、未写盘)`);
|
|
281
|
+
refused++;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// done 前置门(2026-09-30 confirm-gate-approved-history):→done 须 git 历史已留 approved 态
|
|
286
|
+
// (先 done 后提交 / 两跳同批提交 → 拒绝;防「确认态缺失」缺陷复发;判据单源见 stage-gates.doneGateFor)。
|
|
287
|
+
if (target === 'done') {
|
|
288
|
+
const gate = doneGateFor(root, abs, doc);
|
|
289
|
+
if (!gate.ok) {
|
|
290
|
+
console.error(`❌ ${doc} 未过 done 前置门(approved 历史):${gate.reason}`);
|
|
291
|
+
console.error(` ${gate.hint}(本份未落账、未写盘)`);
|
|
292
|
+
refused++;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
190
296
|
const fp = computeFingerprint(text);
|
|
297
|
+
// AI 自治放行分支(2026-09-30 ai-autonomy-trust):
|
|
298
|
+
// ① 仅 L0/L1(读 frontmatter「级别」,缺级别按不可自治处理——fail-closed);
|
|
299
|
+
// ② Level 1:仅 draft→approved;Level 2:draft→approved 与 approved→done;
|
|
300
|
+
// ③ incidents 永不自治(open→fixed/fixed→closed 必须人工)。
|
|
301
|
+
if (autoMode) {
|
|
302
|
+
const isLowLevel = lvl === 'L0' || lvl === 'L1';
|
|
303
|
+
const stageAllowed = trustConfig.level === 2
|
|
304
|
+
? (target === 'approved' || target === 'done')
|
|
305
|
+
: target === 'approved';
|
|
306
|
+
if (!isLowLevel || !stageAllowed || doc.startsWith('workflow/incidents/')) {
|
|
307
|
+
console.error(`❌ ${doc} 未过 AI 自治门(级别「${lvl || '缺失'}」/ 目标态「${target}」/ 类型):Level ${trustConfig.level} 仅授权 L0/L1 的 ${trustConfig.level === 2 ? 'approved/done' : 'approved'} 且不含 incidents`);
|
|
308
|
+
refused++;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
fs.writeFileSync(abs, applyTransition(text, target, fp.slice(0, 16)));
|
|
312
|
+
appendLedger(root, {
|
|
313
|
+
ts: new Date().toISOString(), doc, stage: target, fingerprint: fp, prev: st,
|
|
314
|
+
source: `ai-auto-trust-L${trustConfig.level}`, quote: `AI 自治放行 (Trust Level ${trustConfig.level})`,
|
|
315
|
+
batch, seq, of: docs.length,
|
|
316
|
+
});
|
|
317
|
+
console.log(`✓ ${doc} ${st} → ${target}(指纹 ${fp.slice(0, 16)},AI 自治已记账:source=ai-auto-trust-L${trustConfig.level})`);
|
|
318
|
+
confirmed++;
|
|
319
|
+
seq++;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
191
322
|
if (delegated) {
|
|
323
|
+
// 协作道门(2026-09-30 hybrid-governance-risk-lanes):--batch 批量仅受理 L0/L1——L2/L3(或缺级别,
|
|
324
|
+
// fail-closed)逐份拒绝落账;不带 --batch 的单份代录不限级别(既有口径不变)。
|
|
325
|
+
const low = lvl === 'L0' || lvl === 'L1';
|
|
326
|
+
if (batchMode && !low) {
|
|
327
|
+
console.error(`❌ ${doc} 级别「${lvl || '缺失'}」不在协作道(L0/L1)——--batch 批量代录仅限 L0/L1,L2/L3 防御道须逐份确认(本份未落账、未写盘)`);
|
|
328
|
+
refused++;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
192
331
|
fs.writeFileSync(abs, applyTransition(text, target, fp.slice(0, 16)));
|
|
193
332
|
appendLedger(root, {
|
|
194
333
|
ts: new Date().toISOString(), doc, stage: target, fingerprint: fp, prev: st,
|
|
195
334
|
source: 'chat-delegated', quote: String(delegatedQuote),
|
|
196
335
|
// 调用事实(batch-ledger-audit):seq=本次调用内件序(从 1),of=本次调用总份数
|
|
197
336
|
batch, seq, of: docs.length,
|
|
337
|
+
// 简洁审计标记(hybrid-governance-risk-lanes):L0/L1 协作道行带 brief(quote 仍入账供对质),
|
|
338
|
+
// 检查 15 并录审计对全 brief 批次豁免「确认并录」告警
|
|
339
|
+
...(low ? { brief: true } : {}),
|
|
198
340
|
});
|
|
199
|
-
console.log(
|
|
341
|
+
console.log(low
|
|
342
|
+
? `✓ ${doc} ${st} → ${target} [${lvl} 协作道/异步审计](指纹 ${fp.slice(0, 16)})`
|
|
343
|
+
: `✓ ${doc} ${st} → ${target}(指纹 ${fp.slice(0, 16)},代录已记账:source=chat-delegated)`);
|
|
200
344
|
confirmed++;
|
|
201
345
|
seq++;
|
|
202
346
|
continue;
|
|
@@ -222,7 +366,7 @@ if (isMain) {
|
|
|
222
366
|
seq++;
|
|
223
367
|
}
|
|
224
368
|
if (rl) rl.close();
|
|
225
|
-
console.log(`\n完成:确认 ${confirmed} 份 / 跳过 ${docs.length - confirmed} 份${delegated ? '(委托代录)' : ''}`);
|
|
226
|
-
process.exit(0);
|
|
369
|
+
console.log(`\n完成:确认 ${confirmed} 份 / 跳过 ${docs.length - confirmed - refused} 份 / 拒绝 ${refused} 份${delegated ? '(委托代录)' : ''}`);
|
|
370
|
+
process.exit(refused ? 2 : 0);
|
|
227
371
|
}
|
|
228
|
-
export default { computeFingerprint, nextStage, applyTransition, appendLedger };
|
|
372
|
+
export default { computeFingerprint, nextStage, applyTransition, appendLedger, fmLevel };
|