agentic-flow-kit 0.9.1 → 0.9.4
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 +93 -93
- package/modules/hosts/claude/commands/wf-build.md +3 -3
- package/modules/hosts/claude/commands/wf-design.md +3 -3
- package/modules/hosts/claude/commands/wf-new-task.md +13 -2
- package/modules/hosts/claude/commands/wf-plan.md +3 -3
- package/modules/hosts/claude/commands/wf-sync-hosts.md +1 -1
- package/modules/hosts/claude/commands/wf-test.md +1 -1
- package/modules/hosts/codex/commands/wf-build.md +3 -3
- package/modules/hosts/codex/commands/wf-design.md +3 -3
- package/modules/hosts/codex/commands/wf-new-task.md +13 -2
- package/modules/hosts/codex/commands/wf-plan.md +3 -3
- package/modules/hosts/codex/commands/wf-sync-hosts.md +1 -1
- package/modules/hosts/codex/commands/wf-test.md +1 -1
- package/modules/hosts/cursor/commands/wf-build.md +3 -3
- package/modules/hosts/cursor/commands/wf-design.md +3 -3
- package/modules/hosts/cursor/commands/wf-new-task.md +13 -2
- package/modules/hosts/cursor/commands/wf-plan.md +3 -3
- package/modules/hosts/cursor/commands/wf-sync-hosts.md +1 -1
- package/modules/hosts/cursor/commands/wf-test.md +1 -1
- package/modules/hosts/opencode/commands/wf-build.md +3 -3
- package/modules/hosts/opencode/commands/wf-design.md +3 -3
- package/modules/hosts/opencode/commands/wf-new-task.md +13 -2
- package/modules/hosts/opencode/commands/wf-plan.md +3 -3
- package/modules/hosts/opencode/commands/wf-sync-hosts.md +1 -1
- package/modules/hosts/opencode/commands/wf-test.md +1 -1
- package/modules/hosts/trae/commands/wf-build.md +3 -3
- package/modules/hosts/trae/commands/wf-design.md +3 -3
- package/modules/hosts/trae/commands/wf-new-task.md +13 -2
- package/modules/hosts/trae/commands/wf-plan.md +3 -3
- package/modules/hosts/trae/commands/wf-sync-hosts.md +1 -1
- package/modules/hosts/trae/commands/wf-test.md +1 -1
- package/modules/hosts/trae/rules/project-workflow.md +1 -1
- package/package.json +45 -45
- package/templates/_agents/commands/build.md +2 -2
- package/templates/_agents/commands/design.md +1 -1
- package/templates/_agents/commands/plan.md +3 -3
- package/templates/_agents/commands/sync-hosts.md +1 -1
- package/templates/_agents/scripts/check-lane-surface.mjs +20 -7
- package/templates/_agents/scripts/check-lane-surface.test.mjs +24 -2
- package/templates/_agents/scripts/check-ledger-invariant.mjs +135 -0
- package/templates/_agents/scripts/check-ledger-invariant.test.mjs +161 -0
- package/templates/_agents/scripts/check-loop.mjs +58 -15
- package/templates/_agents/scripts/check-loop.test.mjs +100 -2
- package/templates/_agents/scripts/confirm-doc.mjs +19 -7
- package/templates/_agents/scripts/confirm-doc.test.mjs +31 -0
- package/templates/_agents/scripts/fill-intent.mjs +1 -1
- package/templates/_agents/scripts/fill-plan.test.mjs +4 -3
- package/templates/_agents/scripts/gate-checklist.mjs +1 -0
- package/templates/_agents/scripts/gen-workflow-metrics.mjs +75 -5
- package/templates/_agents/scripts/gen-workflow-metrics.test.mjs +104 -2
- package/templates/_agents/scripts/policy.mjs +1 -0
- package/templates/_agents/scripts/pre-push.test.mjs +8 -1
- package/templates/_agents/scripts/solidify-task.test.mjs +5 -2
- package/templates/_agents/scripts/stage-gates.mjs +29 -6
- package/templates/_agents/scripts/trust-mode.test.mjs +110 -0
- package/templates/_agents/scripts-test-exempt.txt +10 -0
- package/templates/_githooks/pre-commit +12 -0
- package/templates/workflow/README.md +3 -2
- package/templates/workflow/plans/_TEMPLATE.md +1 -1
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// check-ledger-invariant — confirmations.jsonl 提交不变量(2026-10-02 ledger-ci-invariant)
|
|
3
|
+
// 目的:把「台账本地可写」的伪造面从事后对质级升为机器可检出级(能力评审审计维度扣分项)。
|
|
4
|
+
// 台账不可变原则:confirmations.jsonl 历史行禁删改——修正只能走追加补偿行(README 硬规则 6)。
|
|
5
|
+
//
|
|
6
|
+
// 模式:
|
|
7
|
+
// 历史全扫(缺省,CI 机器门):遍历台账全部提交历史(时间正序),每次变更必须以旧 blob 为
|
|
8
|
+
// **逐字节前缀**(允许纯追加;禁止删行/改行/重排);再对最终树做行级校验(五项,见下)。
|
|
9
|
+
// blob 读取走 git 对象库(入库 LF 归一口径),不受工作树 autocrlf 影响。
|
|
10
|
+
// --staged(pre-commit 前置):暂存 blob 必须以 HEAD blob 为逐字节前缀——提交时刻即拦重写。
|
|
11
|
+
//
|
|
12
|
+
// 行级校验(终树,五项):
|
|
13
|
+
// ① ts 为 ISO 字符串且全表非降序(追加时序单调)
|
|
14
|
+
// ② fingerprint 为 64 位 hex
|
|
15
|
+
// ③ source=chat-delegated 行必有非空 quote(原话对质面)
|
|
16
|
+
// ④ doc 匹配 workflow/(intents|specs|plans|incidents)/[^/]+.md 且当前树存在
|
|
17
|
+
// ⑤ stage ∈ 合法跳转终态集(approved/done/fixed/closed/superseded/cancelled)
|
|
18
|
+
//
|
|
19
|
+
// 静默条件:台账不存在 / 零提交历史(装户、新仓)/ --staged 且暂存未触台账或无 HEAD 版本。
|
|
20
|
+
// 已知边界:git log --follow 未用——台账自创建无重命名(单一路径取史);克隆深度不足时仅扫可得
|
|
21
|
+
// 历史(CI 侧 fetch-depth:0 兜全史,深度不足会告警声明边界,fail-open 于深度、fail-closed 于内容)。
|
|
22
|
+
// 用法:node check-ledger-invariant.mjs [--root <仓库根>] [--staged]
|
|
23
|
+
import { execFileSync } from 'node:child_process';
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
|
|
27
|
+
const argv = process.argv.slice(2);
|
|
28
|
+
const argOf = (k) => {
|
|
29
|
+
const i = argv.indexOf(k);
|
|
30
|
+
return i >= 0 && i + 1 < argv.length ? argv[i + 1] : '';
|
|
31
|
+
};
|
|
32
|
+
const staged = argv.includes('--staged');
|
|
33
|
+
const ROOT = argOf('--root') || (staged ? process.cwd() : execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' }).trim());
|
|
34
|
+
const REL = '.agents/confirmations.jsonl';
|
|
35
|
+
const FILE = path.join(ROOT, REL);
|
|
36
|
+
const VALID_STAGES = new Set(['approved', 'done', 'fixed', 'closed', 'superseded', 'cancelled',
|
|
37
|
+
'revert-draft', 'revert-open']); // 回退注记行(check-loop :822 认可;fingerprint 恒 n/a)
|
|
38
|
+
|
|
39
|
+
const git = (args, opts = {}) => execFileSync('git', args, { cwd: ROOT, encoding: 'buffer', maxBuffer: 64 * 1024 * 1024, ...opts });
|
|
40
|
+
const gitText = (args) => execFileSync('git', args, { cwd: ROOT, encoding: 'utf8' });
|
|
41
|
+
const blobOf = (ref) => git(['show', ref]); // ref 必须为完整对象引用(<sha>:<path> / HEAD:<path> / :<path>)
|
|
42
|
+
|
|
43
|
+
const problems = [];
|
|
44
|
+
const fail = (msg) => { console.error(msg); process.exit(1); };
|
|
45
|
+
|
|
46
|
+
// ---- --staged:暂存 blob ⊇ HEAD blob ----
|
|
47
|
+
if (staged) {
|
|
48
|
+
const stagedHit = gitText(['-c', 'core.quotepath=off', 'diff', '--cached', '--name-only']).split(/\r?\n/).includes(REL);
|
|
49
|
+
if (!stagedHit) process.exit(0);
|
|
50
|
+
const headExists = (() => { try { gitText(['rev-parse', '--verify', '--quiet', `HEAD:${REL}`]); return true; } catch { return false; } })();
|
|
51
|
+
if (!headExists) process.exit(0); // 首次入库:无 HEAD 基线,历史全扫后续覆盖
|
|
52
|
+
let head, cur;
|
|
53
|
+
try {
|
|
54
|
+
head = blobOf(`HEAD:${REL}`);
|
|
55
|
+
cur = blobOf(`:${REL}`); // 暂存区
|
|
56
|
+
} catch {
|
|
57
|
+
process.exit(0); // 对象读取异常(如极端并发)交由 CI 全扫兜底
|
|
58
|
+
}
|
|
59
|
+
if (!cur.subarray(0, head.length).equals(head)) {
|
|
60
|
+
let off = 0;
|
|
61
|
+
while (off < Math.min(head.length, cur.length) && head[off] === cur[off]) off++;
|
|
62
|
+
problems.push(`- [台账重写] 暂存内容不是 HEAD 的前缀扩展(首异偏移 ${off},HEAD ${head.length}B → 暂存 ${cur.length}B)`);
|
|
63
|
+
console.error('台账不可变(--staged)— BLOCK:\n' + problems.join('\n'));
|
|
64
|
+
console.error(' 修法:台账历史行禁删改——误写走追加补偿行(node .agents/scripts/confirm-doc.mjs ...);确需撤销历史提交请 git revert 后重新追加');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---- 历史全扫(缺省)----
|
|
71
|
+
if (!fs.existsSync(FILE)) process.exit(0); // 装户/新仓无台账面
|
|
72
|
+
let shas;
|
|
73
|
+
try {
|
|
74
|
+
shas = gitText(['log', '--format=%H', '--reverse', '--', REL]).split(/\s+/).filter(Boolean);
|
|
75
|
+
} catch {
|
|
76
|
+
process.exit(0); // 非仓库或文件未入库(测试夹具)
|
|
77
|
+
}
|
|
78
|
+
if (!shas.length) process.exit(0);
|
|
79
|
+
|
|
80
|
+
let prev = null;
|
|
81
|
+
let prevSha = null;
|
|
82
|
+
for (const sha of shas) {
|
|
83
|
+
let cur;
|
|
84
|
+
// 删除提交不是静默边界(复核 P2 收口):台账在 append-only 世界里永不应删除——任一历史提交删除
|
|
85
|
+
// 台账即 hard fail(否则「删除→次提交重加」会把重加 blob 当新基线绕过前缀判据)。
|
|
86
|
+
try { cur = blobOf(`${sha}:${REL}`); } catch {
|
|
87
|
+
problems.push(`- [台账删除] ${sha.slice(0, 10)}:该提交删除了 confirmations.jsonl(append-only 世界禁删文件)`);
|
|
88
|
+
prev = null; // 后续重加无基线可比,仅受行级校验约束
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (prev !== null && !cur.subarray(0, prev.length).equals(prev)) {
|
|
92
|
+
let off = 0;
|
|
93
|
+
while (off < Math.min(prev.length, cur.length) && prev[off] === cur[off]) off++;
|
|
94
|
+
const removed = prev.length > cur.length;
|
|
95
|
+
problems.push(`- [前缀破坏] ${sha.slice(0, 10)}(前承 ${prevSha.slice(0, 10)}):历史内容被${removed ? `删减 ${prev.length - cur.length}B` : `改写(首异偏移 ${off},旧 ${prev.length}B → 新 ${cur.length}B)`}`);
|
|
96
|
+
}
|
|
97
|
+
prev = cur;
|
|
98
|
+
prevSha = sha;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---- 行级校验(终树)----
|
|
102
|
+
let rows = [];
|
|
103
|
+
try {
|
|
104
|
+
rows = fs.readFileSync(FILE, 'utf8').split(/\r?\n/).filter((l) => l.trim()).map((l) => { try { return JSON.parse(l); } catch { return null; } });
|
|
105
|
+
} catch { /* 上面 existsSync 已兜 */ }
|
|
106
|
+
let prevTs = '';
|
|
107
|
+
rows.forEach((e, idx) => {
|
|
108
|
+
const at = `第 ${idx + 1} 行`;
|
|
109
|
+
if (!e || typeof e !== 'object') { problems.push(`- [行级] ${at}:非 JSON 对象行`); return; }
|
|
110
|
+
if (typeof e.ts !== 'string' || Number.isNaN(Date.parse(e.ts))) problems.push(`- [行级] ${at}:ts 非 ISO 字符串`);
|
|
111
|
+
else {
|
|
112
|
+
if (prevTs && e.ts < prevTs) problems.push(`- [行级] ${at}:ts ${e.ts} 早于前行 ${prevTs}(追加时序非单调)`);
|
|
113
|
+
prevTs = e.ts;
|
|
114
|
+
}
|
|
115
|
+
const isRevertNote = typeof e.stage === 'string' && e.stage.startsWith('revert-');
|
|
116
|
+
if (isRevertNote) {
|
|
117
|
+
if (e.fingerprint !== 'n/a') problems.push(`- [行级] ${at}:回退注记行 fingerprint 恒为「n/a」`);
|
|
118
|
+
} else if (typeof e.fingerprint !== 'string' || !/^[0-9a-f]{64}$/.test(e.fingerprint)) {
|
|
119
|
+
problems.push(`- [行级] ${at}:fingerprint 非 64 位 hex`);
|
|
120
|
+
}
|
|
121
|
+
if (e.source === 'chat-delegated' && !(typeof e.quote === 'string' && e.quote.trim())) problems.push(`- [行级] ${at}:chat-delegated 行缺非空 quote(原话对质面)`);
|
|
122
|
+
if (typeof e.doc !== 'string' || !/^workflow\/(intents|specs|plans|incidents)\/[^/]+\.md$/.test(e.doc)) {
|
|
123
|
+
problems.push(`- [行级] ${at}:doc 路径不在 workflow 四目录协议内`);
|
|
124
|
+
} else if (!fs.existsSync(path.join(ROOT, e.doc))) {
|
|
125
|
+
problems.push(`- [行级] ${at}:doc ${e.doc} 在当前树不存在`);
|
|
126
|
+
}
|
|
127
|
+
if (!VALID_STAGES.has(e.stage)) problems.push(`- [行级] ${at}:stage「${e.stage || '缺失'}」不在合法跳转终态集`);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (problems.length) {
|
|
131
|
+
console.error(`台账不可变(历史全扫)— HARD-BLOCK:\n\n${problems.join('\n')}\n`);
|
|
132
|
+
console.error(' 修法:台账历史行禁删改(append-only 前缀不变量)——误写只能追加补偿行;确需撤销历史提交请 git revert 后重新追加。篡改属审计事件,保留现场联系仓库所有者。');
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
process.exit(0);
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// check-ledger-invariant.test.mjs — 台账提交不变量 fixture 套件(2026-10-02 ledger-ci-invariant)
|
|
3
|
+
// 真 git 仓注入:合法追加放行 / 六类篡改与行级违例检出 / --staged 双态 / 首次入库与空台账静默。
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const SCRIPT = path.join(SCRIPT_DIR, 'check-ledger-invariant.mjs');
|
|
12
|
+
|
|
13
|
+
let pass = 0;
|
|
14
|
+
let fail = 0;
|
|
15
|
+
const check = (desc, cond, detail = '') => {
|
|
16
|
+
if (cond) { pass++; console.log(`PASS ${desc}`); }
|
|
17
|
+
else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const HEX = 'a'.repeat(64);
|
|
21
|
+
const row = (o = {}) => JSON.stringify({
|
|
22
|
+
ts: '2026-10-02T00:00:00.000Z', doc: 'workflow/intents/demo.md', stage: 'approved',
|
|
23
|
+
fingerprint: HEX, prev: 'draft', source: 'chat-delegated', quote: '确认', ...o,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const G = (root, args) => spawnSync('git', args, { cwd: root, encoding: 'utf8' });
|
|
27
|
+
const commitAll = (root, msg) => { G(root, ['add', '-A']); G(root, ['-c', 'user.email=t@t', '-c', 'user.name=t', 'commit', '-qm', msg]); };
|
|
28
|
+
const run = (root, args = []) => {
|
|
29
|
+
const r = spawnSync(process.execPath, [SCRIPT, '--root', root, ...args], { encoding: 'utf8' });
|
|
30
|
+
return { code: r.status, out: `${r.stdout || ''}${r.stderr || ''}` };
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// mkrepo:临时真仓(workflow/intents/demo.md 在树 + 台账首行已提交)
|
|
34
|
+
const mkrepo = (firstRow = row()) => {
|
|
35
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'ledger-inv-test-'));
|
|
36
|
+
fs.mkdirSync(path.join(d, 'workflow', 'intents'), { recursive: true });
|
|
37
|
+
fs.mkdirSync(path.join(d, '.agents'), { recursive: true });
|
|
38
|
+
fs.writeFileSync(path.join(d, 'workflow', 'intents', 'demo.md'), '---\n状态: approved\n级别: L1\n日期: 2026-10-02\n模块: pipeline\n---\n# I\n');
|
|
39
|
+
fs.writeFileSync(path.join(d, '.agents', 'confirmations.jsonl'), firstRow + '\n');
|
|
40
|
+
G(d, ['init', '-q']);
|
|
41
|
+
G(d, ['config', 'user.email', 't@t']);
|
|
42
|
+
G(d, ['config', 'user.name', 't']);
|
|
43
|
+
commitAll(d, 'base ledger');
|
|
44
|
+
return d;
|
|
45
|
+
};
|
|
46
|
+
const ledPath = (d) => path.join(d, '.agents', 'confirmations.jsonl');
|
|
47
|
+
const append = (d, line) => fs.appendFileSync(ledPath(d), line + '\n');
|
|
48
|
+
|
|
49
|
+
// S1:合法追加 ×2 → 历史全扫过
|
|
50
|
+
{
|
|
51
|
+
const d = mkrepo();
|
|
52
|
+
append(d, row({ ts: '2026-10-02T01:00:00.000Z', stage: 'done' }));
|
|
53
|
+
commitAll(d, 'append 2nd');
|
|
54
|
+
const r = run(d);
|
|
55
|
+
check('S1 合法追加两次 → 历史全扫 exit 0', r.code === 0, r.out);
|
|
56
|
+
}
|
|
57
|
+
// S2:删历史行 → 前缀破坏拦
|
|
58
|
+
{
|
|
59
|
+
const d = mkrepo();
|
|
60
|
+
append(d, row({ ts: '2026-10-02T01:00:00.000Z', stage: 'done' }));
|
|
61
|
+
commitAll(d, 'append');
|
|
62
|
+
fs.writeFileSync(ledPath(d), row({ ts: '2026-10-02T01:00:00.000Z', stage: 'done' }) + '\n'); // 删首行
|
|
63
|
+
commitAll(d, 'drop first row');
|
|
64
|
+
const r = run(d);
|
|
65
|
+
check('S2 删历史行 → 前缀破坏 exit 1', r.code === 1 && r.out.includes('前缀破坏') && r.out.includes('删减'), r.out);
|
|
66
|
+
}
|
|
67
|
+
// S3:改历史行内容 → 拦
|
|
68
|
+
{
|
|
69
|
+
const d = mkrepo();
|
|
70
|
+
append(d, row({ ts: '2026-10-02T01:00:00.000Z', stage: 'done' }));
|
|
71
|
+
commitAll(d, 'append');
|
|
72
|
+
const lines = fs.readFileSync(ledPath(d), 'utf8').split('\n').filter(Boolean);
|
|
73
|
+
lines[0] = lines[0].replace('确认', '改成别的'); // 改首行 quote
|
|
74
|
+
fs.writeFileSync(ledPath(d), lines.join('\n') + '\n');
|
|
75
|
+
commitAll(d, 'rewrite first line');
|
|
76
|
+
const r = run(d);
|
|
77
|
+
check('S3 改历史行 → 前缀破坏 exit 1(首异偏移)', r.code === 1 && r.out.includes('改写') && r.out.includes('首异偏移'), r.out);
|
|
78
|
+
}
|
|
79
|
+
// S4:非前缀改写(截断+异内容)→ 拦
|
|
80
|
+
{
|
|
81
|
+
const d = mkrepo();
|
|
82
|
+
append(d, row({ ts: '2026-10-02T01:00:00.000Z', stage: 'done' }));
|
|
83
|
+
commitAll(d, 'append');
|
|
84
|
+
fs.writeFileSync(ledPath(d), row({ ts: '2027-01-01T00:00:00.000Z', quote: '另起炉灶' }) + '\n');
|
|
85
|
+
commitAll(d, 'replace all');
|
|
86
|
+
const r = run(d);
|
|
87
|
+
check('S4 非前缀整体改写 → exit 1', r.code === 1 && r.out.includes('前缀破坏'), r.out);
|
|
88
|
+
}
|
|
89
|
+
// S5:ts 乱序追加 → 行级拦
|
|
90
|
+
{
|
|
91
|
+
const d = mkrepo();
|
|
92
|
+
append(d, row({ ts: '2026-10-01T00:00:00.000Z', stage: 'done' })); // 早于首行
|
|
93
|
+
commitAll(d, 'append out-of-order');
|
|
94
|
+
const r = run(d);
|
|
95
|
+
check('S5 ts 非单调 → 行级 exit 1', r.code === 1 && r.out.includes('非单调'), r.out);
|
|
96
|
+
}
|
|
97
|
+
// S6:伪指纹(非 64-hex)→ 行级拦
|
|
98
|
+
{
|
|
99
|
+
const d = mkrepo();
|
|
100
|
+
append(d, row({ ts: '2026-10-02T01:00:00.000Z', stage: 'done', fingerprint: 'zzz' }));
|
|
101
|
+
commitAll(d, 'append bad fp');
|
|
102
|
+
const r = run(d);
|
|
103
|
+
check('S6 fingerprint 非 64-hex → 行级 exit 1', r.code === 1 && r.out.includes('64 位 hex'), r.out);
|
|
104
|
+
}
|
|
105
|
+
// S7:chat-delegated 空 quote → 行级拦
|
|
106
|
+
{
|
|
107
|
+
const d = mkrepo();
|
|
108
|
+
append(d, row({ ts: '2026-10-02T01:00:00.000Z', stage: 'done', quote: '' }));
|
|
109
|
+
commitAll(d, 'append empty quote');
|
|
110
|
+
const r = run(d);
|
|
111
|
+
check('S7 chat-delegated 空 quote → 行级 exit 1', r.code === 1 && r.out.includes('quote'), r.out);
|
|
112
|
+
}
|
|
113
|
+
// S8:doc 路径在树中不存在 → 行级拦
|
|
114
|
+
{
|
|
115
|
+
const d = mkrepo();
|
|
116
|
+
append(d, row({ ts: '2026-10-02T01:00:00.000Z', stage: 'done', doc: 'workflow/intents/ghost.md' }));
|
|
117
|
+
commitAll(d, 'append ghost doc');
|
|
118
|
+
const r = run(d);
|
|
119
|
+
check('S8 doc 当前树不存在 → 行级 exit 1', r.code === 1 && r.out.includes('不存在'), r.out);
|
|
120
|
+
}
|
|
121
|
+
// S9:--staged 合法追加 → 过
|
|
122
|
+
{
|
|
123
|
+
const d = mkrepo();
|
|
124
|
+
append(d, row({ ts: '2026-10-02T01:00:00.000Z', stage: 'done' }));
|
|
125
|
+
G(d, ['add', '-A']);
|
|
126
|
+
const r = run(d, ['--staged']);
|
|
127
|
+
check('S9 --staged 合法追加 → exit 0', r.code === 0, r.out);
|
|
128
|
+
}
|
|
129
|
+
// S10:--staged 删历史行 → 拦
|
|
130
|
+
{
|
|
131
|
+
const d = mkrepo();
|
|
132
|
+
append(d, row({ ts: '2026-10-02T01:00:00.000Z', stage: 'done' }));
|
|
133
|
+
commitAll(d, 'append');
|
|
134
|
+
fs.writeFileSync(ledPath(d), row({ ts: '2026-10-02T01:00:00.000Z', stage: 'done' }) + '\n');
|
|
135
|
+
G(d, ['add', '-A']);
|
|
136
|
+
const r = run(d, ['--staged']);
|
|
137
|
+
check('S10 --staged 删历史行 → exit 1', r.code === 1 && r.out.includes('台账重写'), r.out);
|
|
138
|
+
}
|
|
139
|
+
// S11:首次入库(无 HEAD 基线)与空台账 → 静默过
|
|
140
|
+
{
|
|
141
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'ledger-inv-fresh-'));
|
|
142
|
+
fs.mkdirSync(path.join(d, '.agents'), { recursive: true });
|
|
143
|
+
G(d, ['init', '-q']);
|
|
144
|
+
G(d, ['config', 'user.email', 't@t']);
|
|
145
|
+
G(d, ['config', 'user.name', 't']);
|
|
146
|
+
fs.writeFileSync(ledPath(d), row() + '\n');
|
|
147
|
+
G(d, ['add', '-A']);
|
|
148
|
+
const rs = run(d, ['--staged']);
|
|
149
|
+
const rh = run(d);
|
|
150
|
+
check('S11 首次入库(staged 无 HEAD 基线 / 历史零提交)均静默过', rs.code === 0 && rh.code === 0, `staged=${rs.out} hist=${rh.out}`);
|
|
151
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
// S12:回退注记行(revert-draft / fingerprint n/a)合法放行
|
|
154
|
+
{
|
|
155
|
+
const d = mkrepo(row({ ts: '2026-10-02T00:00:00.000Z', stage: 'revert-draft', fingerprint: 'n/a', prev: 'approved' }));
|
|
156
|
+
const r = run(d);
|
|
157
|
+
check('S12 回退注记行(n/a 指纹)→ 历史全扫过', r.code === 0, r.out);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.log(`\n${fail ? `❌ ${fail} failed` : '✅ all passed'}(${pass} passed)`);
|
|
161
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -70,6 +70,8 @@
|
|
|
70
70
|
// 17. 发版提交树上仍未收口的 intent/spec/plan [hard-block]
|
|
71
71
|
// 18. 委派台账对账 [warning]
|
|
72
72
|
// 19. 逐阶段审计(起草先于入口确认 / 台账审批顺序倒置) [warning]
|
|
73
|
+
// 20. 引擎脚本测试覆盖(包源环境:templates/_agents/scripts/ 下每个非 *.test.mjs 引擎脚本须有同名兄弟
|
|
74
|
+
// .test.mjs,或在 .agents/scripts-test-exempt.txt 登记豁免;2026-10-01 gate-script-test-coverage) [warning]
|
|
73
75
|
// 判据 A=在途扫描(同名 spec/plan 为 draft 且日期≥stageGateSince 时判入口确认;plan 另判 L2/L3 的 spec);
|
|
74
76
|
// 判据 B=台账 approved 行按主题校验 intents≤specs≤plans 顺序(组内最早 ts 日期≥stageGateSince 才判);
|
|
75
77
|
// stageGateSince 缺键(v1) → 本检查整体跳过;口径与 stage-gates.mjs / fill-* / confirm-doc 前置门互引
|
|
@@ -96,7 +98,7 @@
|
|
|
96
98
|
// 已确认状态: approved/done=已批或闭环; superseded/cancelled=放弃留档(仍算确认,不挡 push); incident: fixed/closed
|
|
97
99
|
// 用法:node .agents/scripts/check-loop.mjs [--rev <sha>] [--hardening] (或经 check-loop.sh shim)
|
|
98
100
|
// --hardening:追加「加固门」(experiment 泳道 exploring 任务入 main 的转正门槛,hard-block;
|
|
99
|
-
// .githooks/pre-push 对 remote=refs/heads/main 的推送传入;不占 1-
|
|
101
|
+
// .githooks/pre-push 对 remote=refs/heads/main 的推送传入;不占 1-20 编号,gate-checklist 登记表
|
|
100
102
|
// 按号配对面不变,门体说明见加固门段注释)
|
|
101
103
|
// 测试:node templates/_agents/scripts/check-loop.test.mjs(fixture 注入 CHECK_LOOP_ROOT)
|
|
102
104
|
import fs from 'node:fs';
|
|
@@ -109,6 +111,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
109
111
|
import { loadEnums } from './workflow-enums.mjs';
|
|
110
112
|
import { runCheck16 } from './check-metric-claims.mjs';
|
|
111
113
|
import { auditEnabled, loadKitPolicy } from './policy.mjs';
|
|
114
|
+
import { laneOfEntry } from './stage-gates.mjs';
|
|
112
115
|
// MARK_RE:incident 留痕形态的单源(第七轮复核 N3——此前 check-loop 内联复制一份同口径字面量,
|
|
113
116
|
// 两处靠注释与人工同步;改为复用 stage-gates.mjs 的导出,从结构上消除漂移可能)
|
|
114
117
|
import { MARK_RE, approvedTraceHit } from './stage-gates.mjs';
|
|
@@ -283,13 +286,14 @@ for (const intent of docFiles('intents')) {
|
|
|
283
286
|
if (lvl !== 'L0' && !fs.existsSync(path.join(ROOT, WF, 'plans', base))) {
|
|
284
287
|
blockers.push(`- [配对断裂] intent 缺 plan:${base}(应在 ${WF}/plans/ 下同名)`);
|
|
285
288
|
}
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
+
// 风险泳道一致性(2026-10-02 caliber-convergence 起单源 laneOfEntry·方案 C 分歧双严):非 high
|
|
290
|
+
// (low 或 suspect——级别/risk_level 缺失、非法、分歧)勾选触达红线 = 自认触及规则/契约或数据/运行时
|
|
291
|
+
// 结构面——就高不就低,协作道不受理 → hard 判低,须对齐两字段至 L2/L3 并补同名 spec
|
|
292
|
+
if (laneOfEntry(lvl, risk, kitPolicy.riskLevelSince, fmGet(intent, '日期')) !== 'high') {
|
|
289
293
|
const redlineTouched = (linesOf(intent) || [])
|
|
290
294
|
.some((l) => /^- \[x\]/.test(l) && /(\$\\rightarrow\$|→)\s*级别|级别至少\s*L[23]/.test(l));
|
|
291
295
|
if (redlineTouched) {
|
|
292
|
-
blockers.push(`- [红线判低] ${base} 级别/风险泳道为 ${[lvl, risk].filter(Boolean).join('/')} 但触达红线已勾选(规则/契约或 schema/结构面)——协作道不受理 STOP
|
|
296
|
+
blockers.push(`- [红线判低] ${base} 级别/风险泳道为 ${[lvl, risk].filter(Boolean).join('/')} 但触达红线已勾选(规则/契约或 schema/结构面)——协作道不受理 STOP 级改动:就高不就低,把「级别」与「risk_level」两字段对齐到 L2/L3(fill-intent 双写口径)并补同名 spec`);
|
|
293
297
|
}
|
|
294
298
|
}
|
|
295
299
|
}
|
|
@@ -336,12 +340,24 @@ for (const plan of docFiles('plans')) {
|
|
|
336
340
|
// 「复制本模板为 YYYY-MM-DD-<主题>.md」),非未填占位符;真未填的占位(标题 `# INTENT — <主题>`、
|
|
337
341
|
// `日期: YYYY-MM-DD`)不含 .md,仍照拦(2026-09-25-wf-runtime incident 记录的误报口径)
|
|
338
342
|
const isNamingConv = (line) => line.includes('<主题>') && /\.md/.test(line);
|
|
343
|
+
// 样例引用豁免(2026-10-02 caliber-convergence 假阳性消除,先例=检查 18 行尾归一):命中位于行内代码
|
|
344
|
+
// (反引号包裹)或围栏代码块内 = 引用样例而非未填占位符,不报;正文裸占位符照报——真占位符均为
|
|
345
|
+
// 裸文本(模板正文无反引号包裹),检测面不缩。
|
|
346
|
+
const stripSamples = (lines) => {
|
|
347
|
+
let inFence = false;
|
|
348
|
+
return lines.map((line) => {
|
|
349
|
+
if (/^\s*(```|~~~)/.test(line)) { inFence = !inFence; return ''; }
|
|
350
|
+
if (inFence) return '';
|
|
351
|
+
return line.replace(/`[^`]*`/g, '');
|
|
352
|
+
});
|
|
353
|
+
};
|
|
339
354
|
const groups = new Map(); // file -> [ "行号:内容" ](首现序)
|
|
340
355
|
for (const sub of DOC_DIRS) {
|
|
341
356
|
for (const f of docFiles(sub)) {
|
|
357
|
+
const lines = linesOf(f) || [];
|
|
342
358
|
const hits = [];
|
|
343
|
-
(
|
|
344
|
-
if (phRe.test(line) && !boilerRe.test(line) && !isNamingConv(line)) hits.push(`${i + 1}:${
|
|
359
|
+
stripSamples(lines).forEach((line, i) => {
|
|
360
|
+
if (phRe.test(line) && !boilerRe.test(line) && !isNamingConv(line)) hits.push(`${i + 1}:${lines[i]}`);
|
|
345
361
|
});
|
|
346
362
|
if (hits.length) groups.set(f, hits);
|
|
347
363
|
}
|
|
@@ -1034,7 +1050,7 @@ runCheck16({ ROOT, ENUMS, docFiles, fmGet, inSet, isTracked, linesOf, readdirOrN
|
|
|
1034
1050
|
const body = (linesOf(iabs) || []).join('\n');
|
|
1035
1051
|
const d19 = fmGet(iabs, '日期') || fmGet(iabs, '发现');
|
|
1036
1052
|
const storLegacy = /^流程: legacy/m.test(body) || (/^\d{4}-\d{2}-\d{2}$/.test(d19) && d19 < kitPolicy.confirmDocsEffective);
|
|
1037
|
-
return { ...r, level: lvl19, entryDraft: st19 === 'draft', storageLegacy: storLegacy };
|
|
1053
|
+
return { ...r, level: lvl19, risk: fmGet(iabs, 'risk_level'), date: d19, entryDraft: st19 === 'draft', storageLegacy: storLegacy };
|
|
1038
1054
|
}
|
|
1039
1055
|
const crel = `${WF}/incidents/${base}.md`;
|
|
1040
1056
|
const cabs = path.join(ROOT, crel);
|
|
@@ -1058,10 +1074,11 @@ runCheck16({ ROOT, ENUMS, docFiles, fmGet, inSet, isTracked, linesOf, readdirOrN
|
|
|
1058
1074
|
const rel = path.relative(ROOT, abs).split(path.sep).join('/');
|
|
1059
1075
|
const ent = entryConfirmed19(base);
|
|
1060
1076
|
if (!ent.ok) {
|
|
1061
|
-
// 协作道异步审计豁免(2026-09-30 hybrid-governance-risk-lanes
|
|
1062
|
-
//
|
|
1063
|
-
//
|
|
1064
|
-
|
|
1077
|
+
// 协作道异步审计豁免(2026-09-30 hybrid-governance-risk-lanes;2026-10-02 caliber-convergence
|
|
1078
|
+
// 起单源 laneOfEntry·方案 C):入口 intent 仍为 draft 且泳道 low(两字段一致 L0/L1)→「先动手后
|
|
1079
|
+
// 确认」是泳道语义,不告警;suspect(缺失/非法/分歧)不豁免;approved/done 却无台账行仍照报
|
|
1080
|
+
// (防手改状态冒充确认,与交叉一致性口径同);incident 入口不豁免(过目留痕本就轻量)。
|
|
1081
|
+
if (ent.entryDraft && laneOfEntry(ent.level, ent.risk, kitPolicy.riskLevelSince, ent.date) === 'low') continue;
|
|
1065
1082
|
warnings.push(`- [WARN 逐阶段] ${rel} 起草先于入口确认(${ent.why})`); continue;
|
|
1066
1083
|
}
|
|
1067
1084
|
// 库存量形态(标记 / 日期早于生效日)短路 level 校验与 spec 档(与起草门同口径,N1)
|
|
@@ -1120,7 +1137,8 @@ if (HARDENING) {
|
|
|
1120
1137
|
const st = fmGet(intent, '状态');
|
|
1121
1138
|
if (st === 'superseded' || st === 'cancelled') continue; // 放弃态:探索作废留档,不加固
|
|
1122
1139
|
const lvl = fmGet(intent, '级别');
|
|
1123
|
-
|
|
1140
|
+
// 泳道单源(2026-10-02 caliber-convergence 方案 C):suspect(缺失/非法/分歧)按 high——L2/L3 另须 spec
|
|
1141
|
+
const lane = laneOfEntry(lvl, fmGet(intent, 'risk_level'), kitPolicy.riskLevelSince, fmGet(intent, '日期'));
|
|
1124
1142
|
if (st !== 'approved' && st !== 'done') {
|
|
1125
1143
|
blockers.push(`- [加固未过] exploring 任务入 main 须先收口:${base} intent 状态仍为『${st || '缺失'}』——先确认至 approved/done 并补齐同名 spec+plan,或显式放弃(superseded/cancelled)`);
|
|
1126
1144
|
continue;
|
|
@@ -1132,11 +1150,13 @@ if (HARDENING) {
|
|
|
1132
1150
|
} else if (!stOkDoc(planSt)) {
|
|
1133
1151
|
blockers.push(`- [加固未过] exploring 任务入 main 的 plan 未收口:${base}(plan 状态『${planSt || '缺失'}』须为 approved/done)`);
|
|
1134
1152
|
}
|
|
1135
|
-
|
|
1153
|
+
// 泳道单源(2026-10-02 caliber-convergence 方案 C;复核 P1-1 修正):suspect(缺失/非法/分歧)与
|
|
1154
|
+
// high 同样另须同名 spec——仅 low(两字段一致 L0/L1)免 spec,只增不松。
|
|
1155
|
+
if (lane !== 'low') {
|
|
1136
1156
|
const specAbs = path.join(ROOT, WF, 'specs', base);
|
|
1137
1157
|
const specSt = fs.existsSync(specAbs) ? fmGet(specAbs, '状态') : '';
|
|
1138
1158
|
if (!fs.existsSync(specAbs)) {
|
|
1139
|
-
blockers.push(`- [加固未过] exploring 任务入 main 缺同名 spec:${base}(级别 ${
|
|
1159
|
+
blockers.push(`- [加固未过] exploring 任务入 main 缺同名 spec:${base}(级别 ${lvl}/risk ${fmGet(intent, 'risk_level') || '缺失'},应在 ${WF}/specs/ 下同名)`);
|
|
1140
1160
|
} else if (!stOkDoc(specSt)) {
|
|
1141
1161
|
blockers.push(`- [加固未过] exploring 任务入 main 的 spec 未收口:${base}(spec 状态『${specSt || '缺失'}』须为 approved/done)`);
|
|
1142
1162
|
}
|
|
@@ -1189,6 +1209,29 @@ function fmStatus(text) {
|
|
|
1189
1209
|
return '';
|
|
1190
1210
|
}
|
|
1191
1211
|
|
|
1212
|
+
// --- 20. 引擎脚本测试覆盖 [warning](2026-10-01 gate-script-test-coverage;仅包源环境) ---
|
|
1213
|
+
// 包源开发纪律:templates/_agents/scripts/ 下每个非 *.test.mjs 引擎脚本须有同名兄弟 .test.mjs,
|
|
1214
|
+
// 或在 .agents/scripts-test-exempt.txt 登记豁免(managed 下发)。装户(无 templates/)整体跳过。
|
|
1215
|
+
{
|
|
1216
|
+
const scriptsDir = path.join(ROOT, 'templates', '_agents', 'scripts');
|
|
1217
|
+
if (fs.existsSync(scriptsDir)) {
|
|
1218
|
+
const exemptFile = path.join(ROOT, '.agents', 'scripts-test-exempt.txt');
|
|
1219
|
+
const exempt = fs.existsSync(exemptFile)
|
|
1220
|
+
? fs.readFileSync(exemptFile, 'utf8').split(/\r?\n/)
|
|
1221
|
+
.map((l) => l.trim())
|
|
1222
|
+
.filter((l) => l && !l.startsWith('#'))
|
|
1223
|
+
.map((l) => l.split('|')[0].trim())
|
|
1224
|
+
.filter(Boolean)
|
|
1225
|
+
: [];
|
|
1226
|
+
const orphans = fs.readdirSync(scriptsDir)
|
|
1227
|
+
.filter((f) => f.endsWith('.mjs') && !f.endsWith('.test.mjs') && !exempt.includes(f))
|
|
1228
|
+
.filter((f) => !fs.existsSync(path.join(scriptsDir, f.replace(/\.mjs$/, '.test.mjs'))));
|
|
1229
|
+
for (const f of orphans.sort()) {
|
|
1230
|
+
warnings.push(`- [WARN 脚本测试缺失] ${f} 缺同名 .test.mjs——新增引擎脚本默认必须带测试;库件/工具可在 .agents/scripts-test-exempt.txt 登记豁免`);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1192
1235
|
// --- 输出(banner 沿 sh 版字样与格式 = 稳定输出契约:banner 后空行、条目 '- ' 前缀)---
|
|
1193
1236
|
if (blockers.length) {
|
|
1194
1237
|
process.stderr.write(`闭环骨架断档(check-loop.sh)— HARD-BLOCK:\n\n${blockers.join('\n')}\n\n`);
|
|
@@ -12,7 +12,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
13
|
import { computeFingerprint } from './confirm-doc.mjs';
|
|
14
14
|
import { POLICIES, loadKitPolicy } from './policy.mjs';
|
|
15
|
-
import { entryConfirmed, MARK_RE } from './stage-gates.mjs';
|
|
15
|
+
import { entryConfirmed, MARK_RE, laneOfEntry, laneOfDoc } from './stage-gates.mjs';
|
|
16
16
|
|
|
17
17
|
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
18
18
|
const CHECK_LOOP = path.join(SCRIPT_DIR, 'check-loop.mjs');
|
|
@@ -2179,7 +2179,7 @@ const mkConfirmedDoc = (root, rel, fmBody) => {
|
|
|
2179
2179
|
rmfix(T);
|
|
2180
2180
|
}
|
|
2181
2181
|
|
|
2182
|
-
// ---- 加固门(2026-09-30 hybrid-governance-explore-hardening;仅 --hardening 启用,不占 1-
|
|
2182
|
+
// ---- 加固门(2026-09-30 hybrid-governance-explore-hardening;仅 --hardening 启用,不占 1-20 编号)----
|
|
2183
2183
|
{
|
|
2184
2184
|
// H1:未标记 exploring 的 draft 件 → 带/不带 --hardening 均不拦(本门只管「已标记未收口」,
|
|
2185
2185
|
// 常规流程的 draft 中间态不受影响——intent approved 后 plan 起草属正常在途)
|
|
@@ -2253,5 +2253,103 @@ const mkConfirmedDoc = (root, rel, fmBody) => {
|
|
|
2253
2253
|
rmfix(T);
|
|
2254
2254
|
}
|
|
2255
2255
|
|
|
2256
|
+
// --- 20. 引擎脚本测试覆盖 [warning](2026-10-01 gate-script-test-coverage) ---
|
|
2257
|
+
{
|
|
2258
|
+
// C20-1:缺测试报 warning、有测试与豁免登记不报(包源环境)
|
|
2259
|
+
const T = mkfix();
|
|
2260
|
+
const sd = path.join(T, 'templates', '_agents', 'scripts');
|
|
2261
|
+
fs.mkdirSync(sd, { recursive: true });
|
|
2262
|
+
fs.writeFileSync(path.join(sd, 'foo.mjs'), 'export {};\n');
|
|
2263
|
+
fs.writeFileSync(path.join(sd, 'foo.test.mjs'), '// ok\n');
|
|
2264
|
+
fs.writeFileSync(path.join(sd, 'bar.mjs'), 'export {};\n');
|
|
2265
|
+
fs.writeFileSync(path.join(sd, 'baz.mjs'), 'export {};\n');
|
|
2266
|
+
fs.writeFileSync(path.join(T, '.agents', 'scripts-test-exempt.txt'), 'baz.mjs | 库件 | 无\n');
|
|
2267
|
+
const r1 = run(T);
|
|
2268
|
+
const o1 = outOf(r1);
|
|
2269
|
+
check('检查20 C20-1 缺测试报 warning、有测试与豁免不报',
|
|
2270
|
+
r1.status === 0 && o1.includes('[WARN 脚本测试缺失] bar.mjs') && !o1.includes('foo.mjs') && !o1.includes('baz.mjs'), o1);
|
|
2271
|
+
rmfix(T);
|
|
2272
|
+
}
|
|
2273
|
+
{
|
|
2274
|
+
// C20-2:装户环境(无 templates/)整体跳过
|
|
2275
|
+
const T = mkfix();
|
|
2276
|
+
const r2 = run(T);
|
|
2277
|
+
check('检查20 C20-2 装户(无 templates/)跳过', !outOf(r2).includes('[WARN 脚本测试缺失]'), outOf(r2));
|
|
2278
|
+
rmfix(T);
|
|
2279
|
+
}
|
|
2280
|
+
{
|
|
2281
|
+
// C20-3:豁免文件缺失时全量列出(无豁免兜底)
|
|
2282
|
+
const T = mkfix();
|
|
2283
|
+
const sd = path.join(T, 'templates', '_agents', 'scripts');
|
|
2284
|
+
fs.mkdirSync(sd, { recursive: true });
|
|
2285
|
+
fs.writeFileSync(path.join(sd, 'zeta.mjs'), 'export {};\n');
|
|
2286
|
+
const r3 = run(T);
|
|
2287
|
+
check('检查20 C20-3 无豁免文件时缺测试脚本被列出', outOf(r3).includes('[WARN 脚本测试缺失] zeta.mjs'), outOf(r3));
|
|
2288
|
+
rmfix(T);
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
// --- 方案 C:laneOfEntry 单源三态 + 检查 1 suspect 红线 + 检查 2 样例豁免(2026-10-02 caliber-convergence) ---
|
|
2292
|
+
{
|
|
2293
|
+
check('laneOfEntry 一致 low / 一致 high',
|
|
2294
|
+
laneOfEntry('L1', 'L1', '2026-10-01', '2026-10-02') === 'low' && laneOfEntry('L2', 'L2', '2026-10-01', '2026-10-02') === 'high');
|
|
2295
|
+
check('laneOfEntry 协议期:分歧 / risk 缺失 / 级别缺失 → suspect',
|
|
2296
|
+
laneOfEntry('L2', 'L1', '2026-10-01', '2026-10-02') === 'suspect'
|
|
2297
|
+
&& laneOfEntry('L2', '', '2026-10-01', '2026-10-02') === 'suspect'
|
|
2298
|
+
&& laneOfEntry('', 'L2', '2026-10-01', '2026-10-02') === 'suspect');
|
|
2299
|
+
check('laneOfEntry 协议前:单字段按级别 / 两值俱在分歧仍 suspect',
|
|
2300
|
+
laneOfEntry('L2', '', '2026-10-01', '2026-09-28') === 'high'
|
|
2301
|
+
&& laneOfEntry('L2', 'L1', '2026-10-01', '2026-09-28') === 'suspect');
|
|
2302
|
+
check('laneOfDoc 单字段三态', laneOfDoc('L1') === 'low' && laneOfDoc('L3') === 'high' && laneOfDoc('') === 'suspect');
|
|
2303
|
+
}
|
|
2304
|
+
{
|
|
2305
|
+
// 检查 1:协议期分歧件(L2/L1)勾红线 → suspect 按 low 拦红线(hard)
|
|
2306
|
+
const T = mkfix();
|
|
2307
|
+
fs.writeFileSync(path.join(T, 'workflow', 'intents', '2026-10-02-x.md'),
|
|
2308
|
+
INTENT('x', '状态: approved\n级别: L2\nrisk_level: L1\n日期: 2026-10-02\n模块: pipeline',
|
|
2309
|
+
'\n## 触达红线\n- [x] 规则 / 契约变更(→ 级别至少 L2)\n'));
|
|
2310
|
+
fs.writeFileSync(path.join(T, 'workflow', 'plans', '2026-10-02-x.md'), '# PLAN\n');
|
|
2311
|
+
const r = run(T);
|
|
2312
|
+
check('检查1 分歧件勾红线 → hard-block(suspect 拦红线)', r.status === 1 && outOf(r).includes('[红线判低]') && outOf(r).includes('两字段对齐'), outOf(r));
|
|
2313
|
+
rmfix(T);
|
|
2314
|
+
}
|
|
2315
|
+
{
|
|
2316
|
+
// 检查 1 对照:协议前存量(级别 L2 无 risk_level)勾红线 → 不拦(单字段 high)
|
|
2317
|
+
const T = mkfix();
|
|
2318
|
+
fs.writeFileSync(path.join(T, 'workflow', 'intents', '2026-09-28-y.md'),
|
|
2319
|
+
INTENT('y', '状态: approved\n级别: L2\n日期: 2026-09-28\n模块: pipeline',
|
|
2320
|
+
'\n## 触达红线\n- [x] 规则 / 契约变更(→ 级别至少 L2)\n'));
|
|
2321
|
+
fs.writeFileSync(path.join(T, 'workflow', 'plans', '2026-09-28-y.md'), '# PLAN\n');
|
|
2322
|
+
const r = run(T);
|
|
2323
|
+
check('检查1 协议前 L2 单字段勾红线 → 不拦(存量零新增损害)', !outOf(r).includes('[红线判低]'), outOf(r));
|
|
2324
|
+
rmfix(T);
|
|
2325
|
+
}
|
|
2326
|
+
{
|
|
2327
|
+
// 检查 2:样例豁免三态——反引号/围栏块内不报,正文裸占位符照报
|
|
2328
|
+
const T = mkfix();
|
|
2329
|
+
fs.writeFileSync(path.join(T, 'workflow', 'intents', '2026-10-02-z.md'),
|
|
2330
|
+
INTENT('z', '状态: approved\n级别: L2\nrisk_level: L2\n日期: 2026-10-02\n模块: pipeline',
|
|
2331
|
+
'\n行内样例 `YYYY-MM-DD` 与 `<主题>` 不报。\n\n```\n日期: YYYY-MM-DD\n```\n\n裸占位:日期: YYYY-MM-DD\n'));
|
|
2332
|
+
fs.writeFileSync(path.join(T, 'workflow', 'plans', '2026-10-02-z.md'), '# PLAN\n');
|
|
2333
|
+
const r = run(T);
|
|
2334
|
+
const o = outOf(r);
|
|
2335
|
+
const zWarn = (o.match(/2026-10-02-z\.md 含模板占位符:[\s\S]*?(?=\n- |\n\n|$)/) || [''])[0];
|
|
2336
|
+
check('检查2 反引号/围栏样例不报、裸占位符照报',
|
|
2337
|
+
zWarn.includes('裸占位') && !zWarn.includes('行内样例') && !zWarn.includes('```'), o);
|
|
2338
|
+
rmfix(T);
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
{
|
|
2342
|
+
// H7(2026-10-02 caliber-convergence 复核 P1-1):suspect(级别 L2/risk_level L1 分歧,协议期)转正
|
|
2343
|
+
// → 与 high 同样另须同名 spec——缺 spec 即拦(只增不松:suspect 不得逃 spec 前置)。
|
|
2344
|
+
const T = mkfix();
|
|
2345
|
+
w(T, 'workflow/intents/2026-10-02-h7.md',
|
|
2346
|
+
INTENT('h7', '状态: approved\n级别: L2\nrisk_level: L1\n日期: 2026-10-02\n模块: pipeline\n阶段: exploring'));
|
|
2347
|
+
w(T, 'workflow/plans/2026-10-02-h7.md', PLAN('h7', '状态: approved\n级别: L2'));
|
|
2348
|
+
const r = run(T, { args: ['--hardening'] });
|
|
2349
|
+
check('加固门 H7:suspect(L2/L1 分歧)转正 → 缺同名 spec 即拦',
|
|
2350
|
+
r.status === 1 && outOf(r).includes('缺同名 spec'), outOf(r));
|
|
2351
|
+
rmfix(T);
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2256
2354
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
2257
2355
|
process.exit(fail ? 1 : 0);
|
|
@@ -39,7 +39,8 @@ import path from 'node:path';
|
|
|
39
39
|
import readline from 'node:readline';
|
|
40
40
|
import { createHash, randomBytes } from 'node:crypto';
|
|
41
41
|
import { fileURLToPath } from 'node:url';
|
|
42
|
-
import { confirmGateFor, doneGateFor } from './stage-gates.mjs';
|
|
42
|
+
import { confirmGateFor, doneGateFor, laneOfEntry, laneOfDoc } from './stage-gates.mjs';
|
|
43
|
+
import { loadKitPolicy } from './policy.mjs';
|
|
43
44
|
|
|
44
45
|
// computeFingerprint(text):CRLF 归一 → 剔指纹行 → sha256 hex(64 位)
|
|
45
46
|
export function computeFingerprint(text) {
|
|
@@ -267,6 +268,16 @@ if (isMain) {
|
|
|
267
268
|
}
|
|
268
269
|
const target = resolveTransition(st, toTarget);
|
|
269
270
|
const lvl = fmLevel(nl); // 级别(AI 自治门 / 协作道批量门共用;缺 = '')
|
|
271
|
+
// 泳道单源(2026-10-02 caliber-convergence 方案 C 分歧双严):intent 双字段(级别+risk_level,协议
|
|
272
|
+
// 锚 policy.riskLevelSince)三态判定;其余件级别单字段。suspect(缺失/非法/分歧)按 high——--auto
|
|
273
|
+
// 与 --batch 均不受理,须先两字段对齐(fill-intent 双写口径)。
|
|
274
|
+
const fmOf = (key) => {
|
|
275
|
+
const m = nl.find((l) => l.startsWith(`${key}:`));
|
|
276
|
+
return m ? m.slice(key.length + 1).trim() : '';
|
|
277
|
+
};
|
|
278
|
+
const lane = doc.includes('/intents/')
|
|
279
|
+
? laneOfEntry(lvl, fmOf('risk_level'), loadKitPolicy(root).riskLevelSince, fmOf('日期'))
|
|
280
|
+
: laneOfDoc(lvl);
|
|
270
281
|
if (!target) {
|
|
271
282
|
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)`);
|
|
272
283
|
continue;
|
|
@@ -299,12 +310,12 @@ if (isMain) {
|
|
|
299
310
|
// ② Level 1:仅 draft→approved;Level 2:draft→approved 与 approved→done;
|
|
300
311
|
// ③ incidents 永不自治(open→fixed/fixed→closed 必须人工)。
|
|
301
312
|
if (autoMode) {
|
|
302
|
-
const isLowLevel =
|
|
313
|
+
const isLowLevel = lane === 'low';
|
|
303
314
|
const stageAllowed = trustConfig.level === 2
|
|
304
315
|
? (target === 'approved' || target === 'done')
|
|
305
316
|
: target === 'approved';
|
|
306
317
|
if (!isLowLevel || !stageAllowed || doc.startsWith('workflow/incidents/')) {
|
|
307
|
-
console.error(`❌ ${doc} 未过 AI 自治门(级别「${lvl || '缺失'}」/ 目标态「${target}」/ 类型):Level ${trustConfig.level}
|
|
318
|
+
console.error(`❌ ${doc} 未过 AI 自治门(级别「${lvl || '缺失'}」/ 泳道「${lane}」/ 目标态「${target}」/ 类型):Level ${trustConfig.level} 仅授权泳道 low(intent 两字段一致 L0/L1;suspect 须先对齐「级别/risk_level」)的 ${trustConfig.level === 2 ? 'approved/done' : 'approved'} 且不含 incidents`);
|
|
308
319
|
refused++;
|
|
309
320
|
continue;
|
|
310
321
|
}
|
|
@@ -320,11 +331,12 @@ if (isMain) {
|
|
|
320
331
|
continue;
|
|
321
332
|
}
|
|
322
333
|
if (delegated) {
|
|
323
|
-
// 协作道门(2026-09-30 hybrid-governance-risk-lanes
|
|
324
|
-
//
|
|
325
|
-
|
|
334
|
+
// 协作道门(2026-09-30 hybrid-governance-risk-lanes;2026-10-02 caliber-convergence 起单源泳道):
|
|
335
|
+
// --batch 批量仅受理泳道 low(intent 两字段一致 L0/L1;suspect 拒)——high/suspect 逐份拒绝落账;
|
|
336
|
+
// 不带 --batch 的单份代录不限级别(既有口径不变)。
|
|
337
|
+
const low = lane === 'low';
|
|
326
338
|
if (batchMode && !low) {
|
|
327
|
-
console.error(`❌ ${doc}
|
|
339
|
+
console.error(`❌ ${doc} 泳道「${lane}」(级别「${lvl || '缺失'}」)不在协作道——--batch 批量代录仅限两字段一致的 L0/L1;suspect(级别/risk_level 缺失、非法或分歧)须先对齐两字段,L2/L3 防御道须逐份确认(本份未落账、未写盘)`);
|
|
328
340
|
refused++;
|
|
329
341
|
continue;
|
|
330
342
|
}
|