agentic-flow-kit 1.6.0 → 1.7.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/package.json +1 -1
- package/src/run-tests.mjs +16 -9
- package/templates/AGENTS.md +2 -1
- package/templates/_agents/scripts/check-ledger-invariant.mjs +18 -0
- package/templates/_agents/scripts/check-ledger-invariant.test.mjs +16 -0
- package/templates/_agents/scripts/check-loop.mjs +41 -1
- package/templates/_agents/scripts/check-loop.test.mjs +55 -0
- package/templates/_agents/scripts/confirm-doc.mjs +36 -2
- package/templates/_agents/scripts/confirm-doc.test.mjs +34 -1
- package/templates/_agents/scripts/policy.mjs +49 -0
- package/templates/_agents/scripts/verify.mjs +11 -3
- package/templates/_agents/scripts/verify.test.mjs +30 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-flow-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "AI-Native 闭环工作流 + wiki 知识层脚手架——flow-kit init 一条命令为新项目装好 .agents 工作流引擎(阶段命令/角色契约/门禁钩子/kb 检索/看板/规则面预算)+ workflow 文档闭环 + wiki 知识库,装完自包含、零运行时依赖",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
package/src/run-tests.mjs
CHANGED
|
@@ -11,16 +11,17 @@ import { fileURLToPath } from 'node:url';
|
|
|
11
11
|
|
|
12
12
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
13
13
|
|
|
14
|
+
// src/ 侧按盘面枚举,不硬编码清单(2026-10-09 审查 P0-1)——此前硬编码 8 项,`src/sync-hosts.test.mjs`
|
|
15
|
+
// 漏挂:npm test 不跑它,CI 也盖不住(ci.yml shipped 步骤只 glob `.agents/scripts/*.test.mjs`),
|
|
16
|
+
// 而 `templates/_agents/commands/sync-hosts.md:62`「npm test 含 sync-hosts.test.mjs 8 场景」随模板
|
|
17
|
+
// 下发 5 个装户宿主。检查 20 只扫 `templates/_agents/scripts/` 面,结构上抓不到 src/ 漏挂。
|
|
18
|
+
// 改 glob 后:新增 src 测试文件不可能再被静默漏跑(枚举漂移由盘面兜住,不靠人记得补)。
|
|
19
|
+
// 含宿主门禁模块回归(2026-09-27 host-gates-p1:dotnet-ca BRE 豁免/fail-closed + trae 强推 token 化)。
|
|
14
20
|
const nodeSuites = [
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
'src/fresh-init.test.mjs',
|
|
20
|
-
'src/stack-profile.test.mjs',
|
|
21
|
-
// 宿主门禁模块回归(2026-09-27 host-gates-p1:dotnet-ca BRE 豁免/fail-closed + trae 强推 token 化)
|
|
22
|
-
'src/gate-dotnet-ca.test.mjs',
|
|
23
|
-
'src/trae-hooks.test.mjs',
|
|
21
|
+
...fs.readdirSync(path.join(ROOT, 'src'))
|
|
22
|
+
.filter((f) => f.endsWith('.test.mjs'))
|
|
23
|
+
.sort()
|
|
24
|
+
.map((f) => `src/${f}`),
|
|
24
25
|
...fs.readdirSync(path.join(ROOT, 'templates/_agents/scripts'))
|
|
25
26
|
.filter((f) => f.endsWith('.test.mjs'))
|
|
26
27
|
.sort()
|
|
@@ -46,4 +47,10 @@ for (const rel of nodeSuites) {
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
console.log(`\n${failed ? `❌ ${failed} 个套件失败` : '✅ 全部套件通过'}`);
|
|
50
|
+
// 机器可读汇总行(2026-10-09 审查 P0-2):verify.mjs 的凭证计数**只认这一行**。
|
|
51
|
+
// 此前它 pop 通配「合计: PASS n / FAIL n」,而 45+ 套件每个都打印自己那行同形文本——
|
|
52
|
+
// 取到的是字母序最后一个套件的场景数(本仓实况:workflows-check.test.mjs 的 15),
|
|
53
|
+
// 于是「一行机器事实」的 passed 字段落成了错的事实。独立前缀 + 套件口径,嵌套同形行不再被误取。
|
|
54
|
+
// 口径:SUITES = 本编排器实际执行的套件数(lint 不计入;lint 红照样 exit 1,凭证不落)。
|
|
55
|
+
console.log(`[run-tests] 合计: SUITES ${nodeSuites.length} / FAILED ${failed}`);
|
|
49
56
|
process.exit(failed ? 1 : 0);
|
package/templates/AGENTS.md
CHANGED
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
- **`git commit` / `git merge` / `git push` 三处一律禁 `--no-verify`**——被拦说明产出不合规,按提示修完原路重试。
|
|
29
29
|
- 项目专属门禁挂 `.agents/hooks/local-pre-commit`。装户五条硬规则(配对、验收、确认留痕、敏感信息、双源台账)与 `audit` 档见 `workflow/README.md`「硬规则」。
|
|
30
30
|
- 提交遵循 Conventional Commits 中文(feat / fix / docs / style / refactor / perf)。L1+ 三段式闭环链:`docs(*)` approved 留痕 → 代码 → 关单 `docs(*)`,相邻提交即同一闭环(2026-10-04 修订)。
|
|
31
|
-
- **确认门(2026-09-27 起)**:intent/spec/plan 的 approved/done 与 incident 的 fixed/closed(两跳、无单跳)唯一入口 = `node .agents/scripts/confirm-doc.mjs <path>`,放弃态走显式 `--to superseded|cancelled`(合法前态见脚本提示;四终态同样内容绑定),两形态:① 用户终端亲手运行键入「可以」(TTY,AI 会话内被拒);② **对话委托代录**——用户在对话内明确确认后,AI 跑 `confirm-doc.mjs <path> --delegated "
|
|
31
|
+
- **确认门(2026-09-27 起)**:intent/spec/plan 的 approved/done 与 incident 的 fixed/closed(两跳、无单跳)唯一入口 = `node .agents/scripts/confirm-doc.mjs <path>`,放弃态走显式 `--to superseded|cancelled`(合法前态见脚本提示;四终态同样内容绑定),两形态:① 用户终端亲手运行键入「可以」(TTY,AI 会话内被拒);② **对话委托代录**——用户在对话内明确确认后,AI 跑 `confirm-doc.mjs <path> --delegated "<授权摘要>"` 逐件代录(默认一次一份,多份并录被拒),台账如实记 `source: chat-delegated`,永不伪装 TTY 行(check-loop 15 指纹+台账对账拦截,与 source 无关);L0/L1 协作道可 `--batch` 多份一次代录(台账 `brief:true`,15 并录告警豁免;L2/L3 仍逐份)。
|
|
32
|
+
- **改动面 scope = 授权的机器事实(2026-10-09 起)**:confirm-doc 落账自动记 `scope{files,sha256}`(被确认文档 + `git status` 实测未提交改动集,机器派生、**AI 无输入面**)。分辨力由 scope + 指纹承担,`--delegated` 的摘要降级为人类可读留痕——**「每次换一句原话」纪律退役**,复用同一句摘要不再削弱任何判据。check-loop 15 校验受管行带 scope 且摘要自洽(`policyVersion ≥ 7` 起,旧版本与存量行零新增告警)。
|
|
32
33
|
|
|
33
34
|
### 检索、看板与量化
|
|
34
35
|
|
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
// ④ doc 匹配 workflow/(intents|specs|plans|incidents)/[^/]+.md 且当前树存在
|
|
17
17
|
// ⑤ stage ∈ 合法跳转终态集(approved/done/fixed/closed/superseded/cancelled)
|
|
18
18
|
//
|
|
19
|
+
// 行级豁免(2026-10-09 ledger-line-exempt,CI 红事故处置):.agents/ledger-line-exempt.json 登记
|
|
20
|
+
// 已定性的污染行({line, doc, reason}——line = 校验序号即非空行序)——行级五项对豁免行整体跳过、
|
|
21
|
+
// 行**保留在台账**(审计面完整),豁免计数照常出账保持可见。定位:事故行无法追加修复(行级扫最终树
|
|
22
|
+
// 全行)且不可就地改(历史前缀不变量)——superseded 重立单之外的唯一出口。**滥用边界**:豁免文件
|
|
23
|
+
// 自身入库、git 历史可对质;逐条须 papercuts/incident 定性引用,无定性登记 = 审计事件。
|
|
24
|
+
//
|
|
19
25
|
// 静默条件:台账不存在 / 零提交历史(装户、新仓)/ --staged 且暂存未触台账或无 HEAD 版本。
|
|
20
26
|
// 已知边界:git log --follow 未用——台账自创建无重命名(单一路径取史);克隆深度不足时仅扫可得
|
|
21
27
|
// 历史(CI 侧 fetch-depth:0 兜全史,深度不足会告警声明边界,fail-open 于深度、fail-closed 于内容)。
|
|
@@ -102,9 +108,20 @@ let rows = [];
|
|
|
102
108
|
try {
|
|
103
109
|
rows = fs.readFileSync(FILE, 'utf8').split(/\r?\n/).filter((l) => l.trim()).map((l) => { try { return JSON.parse(l); } catch { return null; } });
|
|
104
110
|
} catch { /* 上面 existsSync 已兜 */ }
|
|
111
|
+
const EXEMPT_P = path.join(ROOT, '.agents', 'ledger-line-exempt.json');
|
|
112
|
+
const exemptLines = new Set();
|
|
113
|
+
try {
|
|
114
|
+
if (fs.existsSync(EXEMPT_P)) {
|
|
115
|
+
for (const e of JSON.parse(fs.readFileSync(EXEMPT_P, 'utf8'))) {
|
|
116
|
+
if (e && Number.isInteger(e.line)) exemptLines.add(e.line);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch { /* 豁免表损坏 → 视同无豁免(fail-closed 于校验面) */ }
|
|
120
|
+
let exemptCount = 0;
|
|
105
121
|
let prevTs = '';
|
|
106
122
|
rows.forEach((e, idx) => {
|
|
107
123
|
const at = `第 ${idx + 1} 行`;
|
|
124
|
+
if (exemptLines.has(idx + 1)) { exemptCount++; return; } // 事故行豁免(登记文件入库可对质)
|
|
108
125
|
if (!e || typeof e !== 'object') { problems.push(`- [行级] ${at}:非 JSON 对象行`); return; }
|
|
109
126
|
if (typeof e.ts !== 'string' || Number.isNaN(Date.parse(e.ts))) problems.push(`- [行级] ${at}:ts 非 ISO 字符串`);
|
|
110
127
|
else {
|
|
@@ -126,6 +143,7 @@ rows.forEach((e, idx) => {
|
|
|
126
143
|
if (!VALID_STAGES.has(e.stage)) problems.push(`- [行级] ${at}:stage「${e.stage || '缺失'}」不在合法跳转终态集`);
|
|
127
144
|
});
|
|
128
145
|
|
|
146
|
+
if (exemptCount) console.log(`ℹ️ 台账豁免行 ${exemptCount} 条(定性见 .agents/ledger-line-exempt.json)`);
|
|
129
147
|
if (problems.length) {
|
|
130
148
|
console.error(`台账不可变(历史全扫)— HARD-BLOCK:\n\n${problems.join('\n')}\n`);
|
|
131
149
|
console.error(' 修法:台账历史行禁删改(append-only 前缀不变量)——误写只能追加补偿行;确需撤销历史提交请 git revert 后重新追加。篡改属审计事件,保留现场联系仓库所有者。');
|
|
@@ -157,5 +157,21 @@ const append = (d, line) => fs.appendFileSync(ledPath(d), line + '\n');
|
|
|
157
157
|
check('S12 回退注记行(n/a 指纹)→ 历史全扫过', r.code === 0, r.out);
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
// ---- 豁免登记(2026-10-09 ledger-line-exempt,CI 红事故处置)----
|
|
161
|
+
{
|
|
162
|
+
// 坏行(fingerprint 非 hex + doc 不存在)→ 红;登记豁免 → 绿 + 计数可见
|
|
163
|
+
const d = mkrepo(row({ fingerprint: 'probe-fp', doc: 'workflow/intents/ghost.md' }));
|
|
164
|
+
let r = run(d);
|
|
165
|
+
check('豁免前:行级坏行 hard(fingerprint/doc 双违规)', r.code === 1 && r.out.includes('行级'), r.out.slice(0, 200));
|
|
166
|
+
fs.writeFileSync(path.join(d, '.agents', 'ledger-line-exempt.json'), JSON.stringify([{ line: 1, doc: 'workflow/intents/ghost.md', reason: '事故定性' }], null, 2));
|
|
167
|
+
r = run(d);
|
|
168
|
+
check('豁免后:exit 0 且计数可见', r.code === 0 && r.out.includes('台账豁免行 1 条'), r.out.slice(0, 200));
|
|
169
|
+
// 行号错位豁免不生效(line=99 无对应坏行)
|
|
170
|
+
fs.writeFileSync(path.join(d, '.agents', 'ledger-line-exempt.json'), JSON.stringify([{ line: 99, reason: '错位' }]));
|
|
171
|
+
r = run(d);
|
|
172
|
+
check('豁免行号不匹配:仍 hard(精确豁免)', r.code === 1, r.out.slice(0, 150));
|
|
173
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
174
|
+
}
|
|
175
|
+
|
|
160
176
|
console.log(`\n${fail ? `❌ ${fail} failed` : '✅ all passed'}(${pass} passed)`);
|
|
161
177
|
process.exit(fail ? 1 : 0);
|
|
@@ -60,6 +60,13 @@
|
|
|
60
60
|
// 按 prev 复原跳转前文本重算 sha256 与台账全量比对,不符=hard「确认内容漂移」;状态行保分隔符换值
|
|
61
61
|
// (非规范格式不误伤);台账行缺 prev 降级 warning;关单编辑顺序新约定:勾验/回填先于关单确认,
|
|
62
62
|
// confirm-doc 是最后一次写入)
|
|
63
|
+
// + scope 子项(2026-10-09 审查 A1 干净版):受管行(ts≥policy.scopeSince,**时刻**锚,缺键 v1-v6 整体跳过
|
|
64
|
+
// →存量 406 行零新增告警)须带机器派生的改动面 scope{files,sha256}——写入侧 confirm-doc.machineScope
|
|
65
|
+
// 自动派生(被确认文档 + `git status --porcelain` 未提交改动集),**AI 无输入面**,分辨力从
|
|
66
|
+
// 「一句可复用的用户原话」换成「机器实测的这 N 个文件」。判据:缺 scope→warning(灰度第一档);
|
|
67
|
+
// scope.files 重算 sha256 不符→hard(「改了 files 没改摘要」= 同一信任边界内手改痕迹,兜住整文件
|
|
68
|
+
// 重写重建链后的二次篡改)。**边界**:scope 证「放行时盘面上哪些文件在动」,不证「这些改动语义正确」
|
|
69
|
+
// ——后者由 fingerprint(文档内容绑定)+ commit diff(事后对质)承担,不越界宣称
|
|
63
70
|
// + 并录批次审计子检查(2026-09-28 batch-ledger-audit 改判据;**读调用事实,不猜时间戳模式**):
|
|
64
71
|
// 判据 = 台账行按 batch 分组、组内 of>1(delegated 行) → warning「确认并录」;batch/seq/of 由
|
|
65
72
|
// confirm-doc 写入时记录(那才是"本次调用落几份"确定已知的时刻)。旧判据「同 quote + 相邻 ts<2s」
|
|
@@ -111,7 +118,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
111
118
|
import { createHash } from 'node:crypto';
|
|
112
119
|
import { loadEnums } from './workflow-enums.mjs';
|
|
113
120
|
import { runCheck16 } from './check-metric-claims.mjs';
|
|
114
|
-
import { auditEnabled, loadKitPolicy, hasFreshVerifyLine, ledgerChainHash } from './policy.mjs';
|
|
121
|
+
import { auditEnabled, loadKitPolicy, hasFreshVerifyLine, ledgerChainHash, scopeMatches } from './policy.mjs';
|
|
115
122
|
import { laneOfEntry } from './stage-gates.mjs';
|
|
116
123
|
// MARK_RE:incident 留痕形态的单源(第七轮复核 N3——此前 check-loop 内联复制一份同口径字面量,
|
|
117
124
|
// 两处靠注释与人工同步;改为复用 stage-gates.mjs 的导出,从结构上消除漂移可能)
|
|
@@ -1006,6 +1013,39 @@ export function runCheckLoop(opts = {}) {
|
|
|
1006
1013
|
blockers.push(`- [台账链断裂] confirmations.jsonl 自第 ${physical} 行(物理)起哈希链校验失败(共 ${brokenCount} 行不一致——append-only 台账出现就地改/删痕迹;prevHash 接续或内容重算不符。整文件重写不可机器防,对质走台账自身 git 历史)`);
|
|
1007
1014
|
}
|
|
1008
1015
|
}
|
|
1016
|
+
// scope 子项(2026-10-09 审查 A1 干净版):受管台账行须带**机器派生**的改动面 scope{files,sha256}。
|
|
1017
|
+
// 背景:委托代录的授权凭据原是 `--delegated` 的用户原话——自由文本机器不可校验、AI 可零成本复用
|
|
1018
|
+
// (本仓实测「可以」133 次、「确认」60 次,406 行里仅 6 行 tty),整条授权链源头不可核对。
|
|
1019
|
+
// 干净版把分辨力从「一句说过的话」换成「机器实测的这 N 个文件」:写入侧 confirm-doc.machineScope
|
|
1020
|
+
// 自动派生(被确认文档 + `git status --porcelain` 未提交改动集),AI 无输入面。
|
|
1021
|
+
// 判定(scopeSince 生效**时刻**锚,缺键 v1-v6 整体跳过——存量行零新增告警、不追溯):
|
|
1022
|
+
// ① 缺 scope → warning(灰度第一档:新机制先观察,升 hard 走后续 policyVersion 演进)
|
|
1023
|
+
// ② scope 存在但按其 files 重算 sha256 不符 → hard-block(「改了 files 没改摘要」= 同一信任
|
|
1024
|
+
// 边界内的手改痕迹;哈希链本已覆盖就地改,本项兜住「整文件重写重建链」后二次篡改的场景)
|
|
1025
|
+
// 注:scope 证明的是「放行时盘面上哪些文件在动」,**不是**「这些改动语义正确」——后者由 fingerprint
|
|
1026
|
+
// (文档内容绑定)+ commit diff(事后对质)承担。不越界宣称(沿本检查既有口径纪律)。
|
|
1027
|
+
{
|
|
1028
|
+
const since = loadKitPolicy(ROOT).scopeSince;
|
|
1029
|
+
if (typeof since === 'string' && Number.isFinite(Date.parse(since))) {
|
|
1030
|
+
const sinceT = Date.parse(since);
|
|
1031
|
+
const missing = [];
|
|
1032
|
+
const mismatched = [];
|
|
1033
|
+
for (const row of ledger) {
|
|
1034
|
+
if (!row || typeof row.ts !== 'string') continue;
|
|
1035
|
+
const t = Date.parse(row.ts);
|
|
1036
|
+
if (!Number.isFinite(t) || t < sinceT) continue; // 存量豁免(不追溯)
|
|
1037
|
+
const name = row.doc || '(未知 doc)';
|
|
1038
|
+
if (!row.scope) { missing.push(name); continue; }
|
|
1039
|
+
if (!scopeMatches(row)) mismatched.push(name);
|
|
1040
|
+
}
|
|
1041
|
+
if (missing.length) {
|
|
1042
|
+
warnings.push(`- [WARN 缺机器改动面] ${missing.length} 条受管台账行无 scope 字段(${missing.slice(0, 3).join('、')}${missing.length > 3 ? ' …' : ''})——confirm-doc 落账自动派生 scope;此形态多见于手工/旧脚本写的行,advisory 不阻断`);
|
|
1043
|
+
}
|
|
1044
|
+
if (mismatched.length) {
|
|
1045
|
+
blockers.push(`- [台账 scope 失配] ${mismatched.length} 条受管台账行的 scope.sha256 与其 files 重算不符(${mismatched.slice(0, 3).join('、')}${mismatched.length > 3 ? ' …' : ''})——「改了 files 没同步摘要」,同一信任边界内的手改痕迹;对质走台账 git 历史`);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1009
1049
|
// incidents 侧覆盖(2026-09-27 gate-coverage):fixed/closed 为已确认态(open = 起草态不加门——
|
|
1010
1050
|
// maintain.md 创建即对话确认的既有口径)
|
|
1011
1051
|
// 放弃态覆盖(2026-09-27 closing-coverage):superseded/cancelled 入配对集——confirm-doc --to 是唯一
|
|
@@ -2940,6 +2940,61 @@ w(T, 'workflow/intents/2026-09-12-deep.md', INTENT('deep', '状态: draft\n级
|
|
|
2940
2940
|
}
|
|
2941
2941
|
|
|
2942
2942
|
|
|
2943
|
+
// ---- 场景 23:台账 scope 子项(2026-10-09 审查 A1 干净版)——缺 scope 出账 / 摘要失配 hard / 存量与旧 policy 零告警 ----
|
|
2944
|
+
// 背景:委托代录的授权凭据原是「用户原话」(自由文本、AI 可零成本复用)。干净版改记机器派生的
|
|
2945
|
+
// scope{files,sha256}(AI 无输入面),本子项校验受管行带该字段且摘要自洽。
|
|
2946
|
+
{
|
|
2947
|
+
const AFTER = '2026-10-09T12:00:00Z'; // ≥ policy v7 scopeSince(生效时刻锚)
|
|
2948
|
+
const BEFORE = '2026-10-01T00:00:00Z'; // 存量(生效前):豁免、不追溯
|
|
2949
|
+
const { scopeDigest } = await import('./policy.mjs');
|
|
2950
|
+
|
|
2951
|
+
// 23a:受管行缺 scope → advisory 出账(灰度第一档,不阻断)
|
|
2952
|
+
const Ta = mkfix();
|
|
2953
|
+
w(Ta, '.agents/kit.json', '{"policyVersion":7}\n');
|
|
2954
|
+
writeLedger(Ta, [{ ts: AFTER, doc: 'workflow/intents/2026-10-09-a.md', stage: 'approved', fingerprint: 'a'.repeat(64), prev: 'draft', source: 'chat-delegated', quote: '可以' }]);
|
|
2955
|
+
const ra = run(Ta);
|
|
2956
|
+
check('场景 23a 缺机器改动面:受管行无 scope → WARN 出账且不阻断',
|
|
2957
|
+
ra.status === 0 && outOf(ra).includes('缺机器改动面'), `exit=${ra.status}\n${outOf(ra).slice(-400)}`);
|
|
2958
|
+
rmfix(Ta);
|
|
2959
|
+
|
|
2960
|
+
// 23b:scope 存在但摘要与 files 重算不符(「改了 files 没改摘要」)→ hard 阻断
|
|
2961
|
+
const Tb = mkfix();
|
|
2962
|
+
w(Tb, '.agents/kit.json', '{"policyVersion":7}\n');
|
|
2963
|
+
const good = scopeDigest(['workflow/intents/2026-10-09-b.md', 'src/x.mjs']);
|
|
2964
|
+
writeLedger(Tb, [{ ts: AFTER, doc: 'workflow/intents/2026-10-09-b.md', stage: 'approved', fingerprint: 'b'.repeat(64), prev: 'draft', source: 'chat-delegated', quote: '可以', scope: { ...good, files: [...good.files, 'src/悄悄加的.mjs'] } }]);
|
|
2965
|
+
const rb = run(Tb);
|
|
2966
|
+
check('场景 23b scope 摘要失配:files 被改未同步 sha256 → hard 阻断',
|
|
2967
|
+
rb.status !== 0 && outOf(rb).includes('台账 scope 失配'), `exit=${rb.status}\n${outOf(rb).slice(-400)}`);
|
|
2968
|
+
rmfix(Tb);
|
|
2969
|
+
|
|
2970
|
+
// 23c:scope 完整自洽 → 零 scope 告警(断言非恒红)
|
|
2971
|
+
const Tc = mkfix();
|
|
2972
|
+
w(Tc, '.agents/kit.json', '{"policyVersion":7}\n');
|
|
2973
|
+
writeLedger(Tc, [{ ts: AFTER, doc: 'workflow/intents/2026-10-09-c.md', stage: 'approved', fingerprint: 'c'.repeat(64), prev: 'draft', source: 'chat-delegated', quote: '可以', scope: scopeDigest(['workflow/intents/2026-10-09-c.md']) }]);
|
|
2974
|
+
const rc = run(Tc);
|
|
2975
|
+
check('场景 23c scope 自洽:零 scope 告警(断言非恒红)',
|
|
2976
|
+
!outOf(rc).includes('缺机器改动面') && !outOf(rc).includes('台账 scope 失配'), outOf(rc).slice(-300));
|
|
2977
|
+
rmfix(Tc);
|
|
2978
|
+
|
|
2979
|
+
// 23d:存量行(ts < scopeSince)缺 scope → 零告警(生效日锚不追溯,不产不可消除噪声)
|
|
2980
|
+
const Td = mkfix();
|
|
2981
|
+
w(Td, '.agents/kit.json', '{"policyVersion":7}\n');
|
|
2982
|
+
writeLedger(Td, [{ ts: BEFORE, doc: 'workflow/intents/2026-10-01-d.md', stage: 'approved', fingerprint: 'd'.repeat(64), prev: 'draft', source: 'chat-delegated', quote: '可以' }]);
|
|
2983
|
+
const rd = run(Td);
|
|
2984
|
+
check('场景 23d 存量豁免:ts < scopeSince 的行缺 scope → 零告警(不追溯)',
|
|
2985
|
+
!outOf(rd).includes('缺机器改动面'), outOf(rd).slice(-300));
|
|
2986
|
+
rmfix(Td);
|
|
2987
|
+
|
|
2988
|
+
// 23e:旧 policy(v6 无 scopeSince 键)→ 子项整体跳过,存量 406 行零新增告警
|
|
2989
|
+
const Te = mkfix();
|
|
2990
|
+
w(Te, '.agents/kit.json', '{"policyVersion":6}\n');
|
|
2991
|
+
writeLedger(Te, [{ ts: AFTER, doc: 'workflow/intents/2026-10-09-e.md', stage: 'approved', fingerprint: 'e'.repeat(64), prev: 'draft', source: 'chat-delegated', quote: '可以' }]);
|
|
2992
|
+
const re = run(Te);
|
|
2993
|
+
check('场景 23e 旧 policy 跳过:v6 无 scopeSince 键 → 子项整体跳过(装户零新增告警)',
|
|
2994
|
+
!outOf(re).includes('缺机器改动面') && !outOf(re).includes('台账 scope 失配'), outOf(re).slice(-300));
|
|
2995
|
+
rmfix(Te);
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2943
2998
|
// ---- 场景 22:addedDates 持久缓存(2026-10-09 engine-quality-round3 W1)----
|
|
2944
2999
|
{
|
|
2945
3000
|
const log = '@2026-10-02T10:00:00+08:00\nworkflow/intents/a.md\nworkflow/plans/a.md\n@2026-10-01T09:00:00+08:00\nworkflow/intents/a.md\n';
|
|
@@ -38,8 +38,9 @@ import fs from 'node:fs';
|
|
|
38
38
|
import path from 'node:path';
|
|
39
39
|
import readline from 'node:readline';
|
|
40
40
|
import { createHash, randomBytes } from 'node:crypto';
|
|
41
|
+
import { spawnSync } from 'node:child_process';
|
|
41
42
|
import { confirmGateFor, doneGateFor, laneOfEntry, laneOfDoc } from './stage-gates.mjs';
|
|
42
|
-
import { loadKitPolicy, hasFreshVerifyLine, ledgerChainHash } from './policy.mjs';
|
|
43
|
+
import { loadKitPolicy, hasFreshVerifyLine, ledgerChainHash, scopeDigest } from './policy.mjs';
|
|
43
44
|
|
|
44
45
|
// hasFreshVerify(root, doc):读 <root>/.agents/verifications.jsonl 判 24h 窗口绿行(判定单源
|
|
45
46
|
// policy.hasFreshVerifyLine)。只读,台账缺失/不可读一律 false(=无凭证,advisory 层 fail-open)。
|
|
@@ -65,6 +66,33 @@ function docRelOf(root, doc) {
|
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
// gitDirtyFiles(root):机器实测的「工作树处于改动态的文件集合」(已跟踪改动 + 未跟踪新件,gitignore 项天然排除)。
|
|
70
|
+
// 解析 `git status --porcelain`(-z 不用,逐行解析足够;重命名行取 `->` 后段)。
|
|
71
|
+
// 非 git 仓库 / git 不可用 / 命令失败 → [](**静默降级为「无额外改动面」而非假装有**——scope 退化成
|
|
72
|
+
// 只含被确认文档本身,仍然自洽可验;不因探测失败而拒绝关单)。
|
|
73
|
+
function gitDirtyFiles(root) {
|
|
74
|
+
try {
|
|
75
|
+
const r = spawnSync('git', ['-C', root, 'status', '--porcelain'], { encoding: 'utf8', timeout: 10_000 });
|
|
76
|
+
if (r.status !== 0 || !r.stdout) return [];
|
|
77
|
+
return r.stdout.split(/\r?\n/).filter(Boolean).map((line) => {
|
|
78
|
+
const p = line.slice(3).trim();
|
|
79
|
+
const arrow = p.lastIndexOf(' -> ');
|
|
80
|
+
const rel = arrow >= 0 ? p.slice(arrow + 4) : p;
|
|
81
|
+
return rel.replace(/^"(.*)"$/, '$1');
|
|
82
|
+
}).filter(Boolean);
|
|
83
|
+
} catch {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// machineScope(root, doc):落账用的改动面事实(2026-10-09 审查 A1 干净版)。
|
|
89
|
+
// **AI 无输入面**——files 全部由机器派生(被确认文档 + git 实测工作树改动集),不是调用方能填的参数。
|
|
90
|
+
// 这正是相对旧设计的核心改进:旧的 quote 是 AI 自填的自由文本(可复用、可伪造、机器不可校验);
|
|
91
|
+
// scope 是机器事实,AI 填不了假,对质时问的是「这 N 个文件」而非「一句说过的话」。
|
|
92
|
+
// 归一/摘要计算单源在 policy.scopeDigest(check-loop 检查 15 侧校验复用同一实现)。
|
|
93
|
+
export function machineScope(root, doc) {
|
|
94
|
+
return scopeDigest([docRelOf(root, doc), ...gitDirtyFiles(root)]);
|
|
95
|
+
}
|
|
68
96
|
// computeFingerprint(text):CRLF 归一 → 剔指纹行 → sha256 hex(64 位)
|
|
69
97
|
export function computeFingerprint(text) {
|
|
70
98
|
const norm = String(text).replace(/\r\n/g, '\n').split('\n')
|
|
@@ -360,6 +388,9 @@ if (isMain) {
|
|
|
360
388
|
}
|
|
361
389
|
}
|
|
362
390
|
const fp = computeFingerprint(text);
|
|
391
|
+
// 改动面事实(2026-10-09 审查 A1 干净版):机器派生,三形态(TTY / 委托代录 / AI 自治)一律记入——
|
|
392
|
+
// TTY 形态同样需要(事后对质问的是「放行时盘面上哪些文件在动」,与谁点的确认无关)。
|
|
393
|
+
const scope = machineScope(root, doc);
|
|
363
394
|
// AI 自治放行分支(2026-09-30 ai-autonomy-trust):
|
|
364
395
|
// ① 仅 L0/L1(读 frontmatter「级别」,缺级别按不可自治处理——fail-closed);
|
|
365
396
|
// ② Level 1:仅 draft→approved;Level 2:draft→approved 与 approved→done;
|
|
@@ -378,7 +409,7 @@ if (isMain) {
|
|
|
378
409
|
appendLedger(root, {
|
|
379
410
|
ts: new Date().toISOString(), doc, stage: target, fingerprint: fp, prev: st,
|
|
380
411
|
source: `ai-auto-trust-L${trustConfig.level}`, quote: `AI 自治放行 (Trust Level ${trustConfig.level})`,
|
|
381
|
-
batch, seq, of: docs.length,
|
|
412
|
+
batch, seq, of: docs.length, scope,
|
|
382
413
|
});
|
|
383
414
|
console.log(`✓ ${doc} ${st} → ${target}(指纹 ${fp.slice(0, 16)},AI 自治已记账:source=ai-auto-trust-L${trustConfig.level})`);
|
|
384
415
|
confirmed++;
|
|
@@ -401,6 +432,9 @@ if (isMain) {
|
|
|
401
432
|
source: 'chat-delegated', quote: String(delegatedQuote),
|
|
402
433
|
// 调用事实(batch-ledger-audit):seq=本次调用内件序(从 1),of=本次调用总份数
|
|
403
434
|
batch, seq, of: docs.length,
|
|
435
|
+
// 改动面事实(A1 干净版):quote 降级为「人类可读摘要」,分辨力改由 scope(机器派生)+ fingerprint
|
|
436
|
+
// (文档内容)承担——「不复用同句」纪律据此退役(复用一句摘要不再削弱任何判据)。
|
|
437
|
+
scope,
|
|
404
438
|
// 简洁审计标记(hybrid-governance-risk-lanes):L0/L1 协作道行带 brief(quote 仍入账供对质),
|
|
405
439
|
// 检查 15 并录审计对全 brief 批次豁免「确认并录」告警
|
|
406
440
|
...(low ? { brief: true } : {}),
|
|
@@ -13,7 +13,8 @@ import os from 'node:os';
|
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { spawnSync } from 'node:child_process';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
|
-
import { computeFingerprint, nextStage, applyTransition, appendLedger, resolveTransition } from './confirm-doc.mjs';
|
|
16
|
+
import { computeFingerprint, nextStage, applyTransition, appendLedger, resolveTransition, machineScope } from './confirm-doc.mjs';
|
|
17
|
+
import { scopeDigest, scopeMatches } from './policy.mjs';
|
|
17
18
|
|
|
18
19
|
let pass = 0;
|
|
19
20
|
let fail = 0;
|
|
@@ -788,5 +789,37 @@ const writeTrust = (root, cfg) => {
|
|
|
788
789
|
r43.status === 0 && /无测试绿凭证/.test(r43.stderr), String(r43.stderr).slice(0, 200));
|
|
789
790
|
}
|
|
790
791
|
|
|
792
|
+
// ---- S41 台账 scope:机器派生改动面(2026-10-09 审查 A1 干净版)----
|
|
793
|
+
// 背景:委托代录的授权凭据原是 `--delegated` 的用户原话——自由文本机器不可校验、AI 可零成本复用。
|
|
794
|
+
// 干净版把分辨力换成机器派生的 scope{files,sha256}:写入侧机器派生(AI 无输入面),校验侧重算比对。
|
|
795
|
+
// ① 归一与摘要单源(反斜杠 / 前导 ./ / 重复项 / 顺序无关 → 同一摘要);② scopeMatches 抓「改了 files
|
|
796
|
+
// 没改摘要」;③ 委托落账行带 scope 且含被确认文档;④ 非 git 夹具降级不崩(scope 退化为仅含 doc)。
|
|
797
|
+
{
|
|
798
|
+
const d = scopeDigest(['b.md', 'a.md', './a.md', 'x\\y.md']);
|
|
799
|
+
check('S41① scopeDigest 归一:反斜杠→posix、去重、排序无关,摘要自洽',
|
|
800
|
+
d.files.join(',') === 'a.md,b.md,x/y.md' && d.sha256 === scopeDigest(['x/y.md', 'a.md', 'b.md']).sha256
|
|
801
|
+
&& scopeMatches({ scope: d }), JSON.stringify(d));
|
|
802
|
+
check('S41② scopeMatches:files 被改而摘要未同步 → 判否',
|
|
803
|
+
!scopeMatches({ scope: { files: [...d.files, 'zz.md'], sha256: d.sha256 } })
|
|
804
|
+
&& !scopeMatches({}) && !scopeMatches({ scope: { files: d.files } }));
|
|
805
|
+
|
|
806
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-scope-'));
|
|
807
|
+
const docP = path.join(root, 'workflow', 'intents'); // 用 intents:无逐阶段前置门,本用例只验 scope 落账
|
|
808
|
+
fs.mkdirSync(docP, { recursive: true });
|
|
809
|
+
fs.writeFileSync(path.join(docP, '2026-10-09-s.md'), '---\n状态: draft\n---\n# I\n');
|
|
810
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-10-09-s.md', '--delegated', '放行'], { cwd: root, encoding: 'utf8' });
|
|
811
|
+
const led = readLedgerOf(root);
|
|
812
|
+
const j = led[led.length - 1];
|
|
813
|
+
check('S41③ 委托落账行带机器派生 scope:含被确认文档 + 摘要自洽',
|
|
814
|
+
r.status === 0 && j && j.scope && Array.isArray(j.scope.files)
|
|
815
|
+
&& j.scope.files.includes('workflow/intents/2026-10-09-s.md') && scopeMatches(j),
|
|
816
|
+
JSON.stringify({ status: r.status, stderr: r.stderr, scope: j && j.scope }));
|
|
817
|
+
check('S41④ 非 git 夹具降级:machineScope 不崩且仍含 doc(静默退化为无额外改动面,不拒绝关单)',
|
|
818
|
+
(() => { const s = machineScope(root, 'workflow/intents/2026-10-09-s.md');
|
|
819
|
+
return Array.isArray(s.files) && s.files.includes('workflow/intents/2026-10-09-s.md') && scopeMatches({ scope: s }); })(),
|
|
820
|
+
JSON.stringify(machineScope(root, 'workflow/intents/2026-10-09-s.md')));
|
|
821
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
822
|
+
}
|
|
823
|
+
|
|
791
824
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
792
825
|
process.exit(fail ? 1 : 0);
|
|
@@ -95,6 +95,30 @@ export const POLICIES = {
|
|
|
95
95
|
// 消费方:检查 8 的 verifyDocInWindow(done ts ≥ 本值才启用精确匹配)。
|
|
96
96
|
verifyDocSince: '2026-10-08T09:37:35Z',
|
|
97
97
|
},
|
|
98
|
+
// 版本 7 = 版本 6 的全部键 + scopeSince(台账「机器派生改动面」的生效时刻,2026-10-09 审查 A1 干净版)。
|
|
99
|
+
// 背景:委托代录的授权凭据是 `--delegated` 的**用户原话**——机器无法校验自由文本,AI 可无成本复用同一句
|
|
100
|
+
// (本仓实测「可以」133 次、「确认」60 次),且台账 406 行里仅 6 行为 tty。整条授权链的源头不可核对,
|
|
101
|
+
// 下游的哈希链/指纹对账/内容绑定全建得很好却是建在空地基上。
|
|
102
|
+
// 干净版解法:**改动面改由机器派生,AI 无输入面**——confirm-doc 落账时自动记
|
|
103
|
+
// `scope:{files:[…], sha256}`(被确认文档 + `git status --porcelain` 实测的未提交改动集,归一去重排序后
|
|
104
|
+
// 取 sha256)。AI 无法伪造这个字段,因为它根本不参与输入;事后对质问的是「这 N 个文件」,而不是一句可复用的原话。
|
|
105
|
+
// 本版本只加一个开关键:**缺键(v1-v6)时该子检查整体跳过**,存量 406 行零新增告警;抄全键仍是铁律。
|
|
106
|
+
7: {
|
|
107
|
+
moduleSince: '2026-09-22',
|
|
108
|
+
check14Since: '2026-09-26',
|
|
109
|
+
confirmDocsEffective: '2026-09-27',
|
|
110
|
+
confirmIncidentsEffective: '2026-09-28',
|
|
111
|
+
bindingTs: '2026-09-28',
|
|
112
|
+
check17UnclosedAfter: '0.8.0',
|
|
113
|
+
stageGateSince: '2026-09-29',
|
|
114
|
+
riskLevelSince: '2026-10-01',
|
|
115
|
+
verifySince: '2026-10-07',
|
|
116
|
+
verifyDocSince: '2026-10-08T09:37:35Z',
|
|
117
|
+
// 生效「时刻」(ISO 8601,UTC)而非日期:本日 04:29Z 已有一批旧口径台账行写入,日期粒度会把它们
|
|
118
|
+
// 纳入受审范围产出不可消退告警——沿 v6 verifyDocSince 升级为时刻的同款教训。
|
|
119
|
+
// 消费方:check-loop 检查 15 的 scope 子项(台账行 ts ≥ 本值才判)。
|
|
120
|
+
scopeSince: '2026-10-09T11:24:43Z',
|
|
121
|
+
},
|
|
98
122
|
};
|
|
99
123
|
|
|
100
124
|
export function loadKitPolicy(root) {
|
|
@@ -155,3 +179,28 @@ export function hasFreshVerifyLine(lines, now = Date.now(), doc = null) {
|
|
|
155
179
|
export function ledgerChainHash(row, prevHash) {
|
|
156
180
|
return createHash('sha256').update((prevHash || '') + JSON.stringify(row)).digest('hex');
|
|
157
181
|
}
|
|
182
|
+
|
|
183
|
+
// ---- 台账 scope(机器派生改动面,2026-10-09 审查 A1 干净版)----
|
|
184
|
+
// scopeDigest(files) = {files, sha256}:files 归一(反斜杠→posix)、去重、排序;sha256 = files.join('\n') 的摘要。
|
|
185
|
+
// **单源**(confirm-doc 落账侧构造与 check-loop 检查 15 校验侧重算共用——防两处归一/排序口径漂移,
|
|
186
|
+
// N3 教训同款):任一侧改了口径而另一侧没改,台账会整体判失效。
|
|
187
|
+
// 边界(诚实声明):scope 是**机器实测**的「确认时刻工作树处于改动态的文件集合」,不是「本次任务应改什么」
|
|
188
|
+
// 的语义判定——它能证明「你放行时盘面上就是这些文件在动」,不能证明「这些改动就是你要的」。
|
|
189
|
+
// 语义正确性仍由指纹(文档内容)+ commit diff(事后对质)承担。
|
|
190
|
+
export function scopeDigest(files) {
|
|
191
|
+
const norm = [...new Set((Array.isArray(files) ? files : [])
|
|
192
|
+
.map((f) => String(f).replace(/\\/g, '/').replace(/^\.\//, ''))
|
|
193
|
+
.filter(Boolean))]
|
|
194
|
+
.sort();
|
|
195
|
+
return { files: norm, sha256: createHash('sha256').update(norm.join('\n'), 'utf8').digest('hex') };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// scopeMatches(row):台账行自带的 scope 是否与按其 files 重算的结果逐字一致。
|
|
199
|
+
// 用于检出「行里改了 files 却没同步 sha256」的手改形态(哈希链能兜住就地改,但整文件重写可重建链——
|
|
200
|
+
// 同一信任边界内的第二道,见 ledgerChainHash 边界声明)。
|
|
201
|
+
export function scopeMatches(row) {
|
|
202
|
+
if (!row || typeof row !== 'object' || !row.scope) return false;
|
|
203
|
+
const s = row.scope;
|
|
204
|
+
if (!Array.isArray(s.files) || typeof s.sha256 !== 'string') return false;
|
|
205
|
+
return scopeDigest(s.files).sha256 === s.sha256;
|
|
206
|
+
}
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
// 一行机器事实 {ts, exitCode:0, suite, passed?, failed?, runId?}——append-only 事实记录,不属裁决
|
|
10
10
|
// (窗口判定/门禁在消费方 confirm-doc 与 check-loop 检查 8);消费语义 = 「按门跑了 verify 且全绿」,
|
|
11
11
|
// 裸跑 npm test 不落账(对齐引导)。npm test 步骤输出从 inherit 改 pipe 捕获后回放(延迟显示,
|
|
12
|
-
//
|
|
12
|
+
// 关单场景可接受)——仅 best-effort 抓编排器权威汇总行 `[run-tests] 合计: SUITES n / FAILED m`,
|
|
13
|
+
// 抓不到则省略计数键(宁缺勿错:exitCode 才是硬事实,计数缺失不阻断)。
|
|
13
14
|
// 装户仓(根目录无 package.json,工程在子目录)下默认 npm test 恒 ENOENT(exit 4058)→ **显式跳过步骤 1**
|
|
14
15
|
// 且**不落测试绿凭证**(凭证语义 = 测试真跑绿,跳过即无凭证,防假绿),并提示用
|
|
15
16
|
// --test-cmd "<项目测试命令>" 指定;显式 --test-cmd 不受此判定影响(2026-10-07 papercut)。
|
|
@@ -84,8 +85,15 @@ try {
|
|
|
84
85
|
if (skipTest) {
|
|
85
86
|
console.log('[verify] ⚠️ 步骤 1 已跳过——本次不落测试绿凭证(凭证语义 = 测试真跑绿)');
|
|
86
87
|
} else {
|
|
87
|
-
|
|
88
|
+
// 计数行**只认编排器权威前缀**(2026-10-09 审查 P0-2):此前是 pop 通配「合计: PASS n / FAIL n」,
|
|
89
|
+
// 而全仓 45+ 套件每个都打印自己那行同形文本 → 取到的是字母序最后一个套件的场景数
|
|
90
|
+
// (本仓实况:workflows-check.test.mjs 的 15),落进「一行机器事实」的 passed 字段。
|
|
91
|
+
// 改为精确匹配 run-tests.mjs 的专属行 + 套件口径:嵌套套件同形行不再被误取,
|
|
92
|
+
// 非本编排器口径的 --test-cmd(装户项目自有测试命令)抓不到即省略计数键,不回退到错的事实。
|
|
93
|
+
const m = [...testStdout.matchAll(/\[run-tests\] 合计: SUITES (\d+) \/ FAILED (\d+)/g)].pop();
|
|
88
94
|
const entry = { ts: new Date().toISOString(), exitCode: 0, suite: 'npm test' };
|
|
95
|
+
// passed/failed 口径 = **套件**(非断言、非场景):凭证明此记录的是「跑过多少套、挂了几套」。
|
|
96
|
+
// 步骤 1 红已在上面 fail-fast 退出,故落账行 failed 恒为 0。
|
|
89
97
|
if (m) { entry.passed = Number(m[1]); entry.failed = Number(m[2]); }
|
|
90
98
|
if (process.env.PIPELINE_RUN_ID) entry.runId = process.env.PIPELINE_RUN_ID;
|
|
91
99
|
// doc 键:未传 --doc 时**不写该键**(不是写 null)——消费方按 doc 精确匹配,undefined 与 null 都恒不命中,
|
|
@@ -93,7 +101,7 @@ try {
|
|
|
93
101
|
if (docRel) entry.doc = docRel;
|
|
94
102
|
fs.mkdirSync(path.join(ROOT, '.agents'), { recursive: true });
|
|
95
103
|
fs.appendFileSync(path.join(ROOT, '.agents', 'verifications.jsonl'), `${JSON.stringify(entry)}\n`);
|
|
96
|
-
console.log(`[verify] 🧾 测试绿凭证已落账 verifications.jsonl${entry.passed !== undefined ?
|
|
104
|
+
console.log(`[verify] 🧾 测试绿凭证已落账 verifications.jsonl${entry.passed !== undefined ? `(套件 ${entry.passed} / FAIL ${entry.failed})` : ''}${docRel ? ` 绑定本单 ${docRel}` : '(未带 --doc:不给任何单背书)'}`);
|
|
97
105
|
}
|
|
98
106
|
} catch (e) {
|
|
99
107
|
console.error(`[verify] ⚠️ 凭证落账失败(${e.code || e.message})——不阻断,关单门前置将按无凭证告警`);
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// 断言:①绿路径(假过 + 空夹具闭环)exit 0;②步骤 1 假败 exit 1 且不出全绿;
|
|
4
4
|
// ③步骤 2 配对断裂夹具 exit 1(verify 自身标记断言——孙进程 stdio inherit 输出不进本测试管道);
|
|
5
5
|
// ④⑤⑥凭证落账三态(绿落 / check-loop 红不落 / 无汇总行省略计数键);
|
|
6
|
-
// ⑦装户仓(cwd 无 package.json)未给 --test-cmd → 跳过步骤 1、不落凭证、仍跑步骤 2、exit 0
|
|
6
|
+
// ⑦装户仓(cwd 无 package.json)未给 --test-cmd → 跳过步骤 1、不落凭证、仍跑步骤 2、exit 0;
|
|
7
|
+
// ⑧--doc 绑定五态;⑨⑩嵌套套件同形汇总行不得被误取(计数口径=编排器权威套件数,2026-10-09 审查 P0-2 回归锁)。
|
|
7
8
|
// 用法:node templates/_agents/scripts/verify.test.mjs
|
|
8
9
|
import fs from 'node:fs';
|
|
9
10
|
import os from 'node:os';
|
|
@@ -86,7 +87,7 @@ const run = (argv, opts = {}) => spawnSync(process.execPath, [VERIFY, ...argv],
|
|
|
86
87
|
// ---- 场景 4:绿路径凭证落账(2026-10-07 verify-evidence)——全绿后 fixture 台账出合法绿行 ----
|
|
87
88
|
{
|
|
88
89
|
const root = mkfix();
|
|
89
|
-
const r = run(['--test-cmd', `node ${mkCmd(0, '合计:
|
|
90
|
+
const r = run(['--test-cmd', `node ${mkCmd(0, '[run-tests] 合计: SUITES 3 / FAILED 0\n')}`], { env: { ...process.env, CHECK_LOOP_ROOT: root } });
|
|
90
91
|
const vp = path.join(root, '.agents', 'verifications.jsonl');
|
|
91
92
|
const line = fs.existsSync(vp) ? JSON.parse(fs.readFileSync(vp, 'utf8').trim()) : null;
|
|
92
93
|
check('场景 4:全绿 → verifications.jsonl 落绿行(exitCode 0 + 计数捕获)',
|
|
@@ -146,7 +147,7 @@ const run = (argv, opts = {}) => spawnSync(process.execPath, [VERIFY, ...argv],
|
|
|
146
147
|
|
|
147
148
|
const rootA = mkfix();
|
|
148
149
|
const rel = 'workflow/intents/2026-10-08-demo.md';
|
|
149
|
-
const rA = run(['--test-cmd', `node ${mkCmd(0, '合计:
|
|
150
|
+
const rA = run(['--test-cmd', `node ${mkCmd(0, '[run-tests] 合计: SUITES 3 / FAILED 0\n')}`, '--doc', rel],
|
|
150
151
|
{ env: { ...process.env, CHECK_LOOP_ROOT: rootA } });
|
|
151
152
|
const eA = readLast(rootA);
|
|
152
153
|
check('场景 8①:带 --doc → 末行含 doc 且为 posix 相对路径',
|
|
@@ -185,6 +186,32 @@ const run = (argv, opts = {}) => spawnSync(process.execPath, [VERIFY, ...argv],
|
|
|
185
186
|
fs.rmSync(bare, { recursive: true, force: true });
|
|
186
187
|
}
|
|
187
188
|
|
|
189
|
+
// ---- 场景 9 / 10:嵌套套件同形汇总行不得被误取(2026-10-09 审查 P0-2 回归锁)----
|
|
190
|
+
// 缺陷复现:旧判据 pop 通配「合计: PASS n / FAIL n」,而全仓 45+ 套件每个都打印自己那行同形文本,
|
|
191
|
+
// 取到的是字母序最后一个套件的场景数(本仓实况 workflows-check.test.mjs = 15),落进凭证 passed。
|
|
192
|
+
// ⑨ 权威行与同形行并存 → 取权威套件数;⑩ 仅有同形行 → 省略计数键(宁缺勿错,不回退到错的事实)。
|
|
193
|
+
{
|
|
194
|
+
const root9 = mkfix();
|
|
195
|
+
const r9 = run(['--test-cmd', `node ${mkCmd(0, '合计: PASS 15 / FAIL 0\n[run-tests] 合计: SUITES 45 / FAILED 0\n')}`],
|
|
196
|
+
{ env: { ...process.env, CHECK_LOOP_ROOT: root9 } });
|
|
197
|
+
const vp9 = path.join(root9, '.agents', 'verifications.jsonl');
|
|
198
|
+
const l9 = fs.existsSync(vp9) ? JSON.parse(fs.readFileSync(vp9, 'utf8').trim()) : null;
|
|
199
|
+
check('场景 9:嵌套套件同形行不被误取,计数取编排器权威套件数',
|
|
200
|
+
r9.status === 0 && l9 !== null && l9.passed === 45 && l9.failed === 0,
|
|
201
|
+
`line=${JSON.stringify(l9)}`);
|
|
202
|
+
fs.rmSync(root9, { recursive: true, force: true });
|
|
203
|
+
|
|
204
|
+
const root10 = mkfix();
|
|
205
|
+
const r10 = run(['--test-cmd', `node ${mkCmd(0, '合计: PASS 15 / FAIL 0\n')}`],
|
|
206
|
+
{ env: { ...process.env, CHECK_LOOP_ROOT: root10 } });
|
|
207
|
+
const vp10 = path.join(root10, '.agents', 'verifications.jsonl');
|
|
208
|
+
const l10 = fs.existsSync(vp10) ? JSON.parse(fs.readFileSync(vp10, 'utf8').trim()) : null;
|
|
209
|
+
check('场景 10:仅有嵌套同形行 → 绿行落盘但省略计数键(不回退到错的事实)',
|
|
210
|
+
r10.status === 0 && l10 !== null && l10.exitCode === 0 && !('passed' in l10) && !('failed' in l10),
|
|
211
|
+
`line=${JSON.stringify(l10)}`);
|
|
212
|
+
fs.rmSync(root10, { recursive: true, force: true });
|
|
213
|
+
}
|
|
214
|
+
|
|
188
215
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
189
216
|
if (fail) {
|
|
190
217
|
console.log('\n用法:node templates/_agents/scripts/verify.test.mjs');
|