agentic-flow-kit 0.9.0 → 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/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 +1 -1
- package/templates/_agents/commands/build.md +2 -2
- package/templates/_agents/commands/design.md +1 -1
- package/templates/_agents/commands/new-task.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 +89 -0
- package/templates/_agents/scripts/solidify-task.mjs +76 -70
- package/templates/_agents/scripts/solidify-task.test.mjs +129 -0
- 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/_githooks/pre-push +34 -30
- package/templates/workflow/README.md +4 -3
- package/templates/workflow/plans/_TEMPLATE.md +1 -1
|
@@ -70,8 +70,12 @@ const readMetrics = (root) => fs.readFileSync(path.join(root, 'workflow', 'metri
|
|
|
70
70
|
run(root, ['--month', '2026-09']);
|
|
71
71
|
run(root, ['--month', '2026-09']);
|
|
72
72
|
let md = readMetrics(root);
|
|
73
|
-
const
|
|
74
|
-
|
|
73
|
+
const funnelMark = '## 闭环漏斗';
|
|
74
|
+
const bodyPart = md.slice(0, md.includes(funnelMark) ? md.indexOf(funnelMark) : undefined);
|
|
75
|
+
const n09 = (bodyPart.match(/^\| 2026-09 \|/gm) || []).length;
|
|
76
|
+
const n09f = ((md.includes(funnelMark) ? md.slice(md.indexOf(funnelMark)) : '').match(/^\| 2026-09 \|/gm) || []).length;
|
|
77
|
+
check('场景 2:同月重跑只保留一行(体量表分段计数)', n09 === 1, `${n09} 行\n${md}`);
|
|
78
|
+
check('场景 2:漏斗表同月 upsert 不重复', n09f === 1, `${n09f} 行\n${md}`);
|
|
75
79
|
run(root, ['--month', '2026-08']);
|
|
76
80
|
md = readMetrics(root);
|
|
77
81
|
const i08 = md.indexOf('| 2026-08 |');
|
|
@@ -114,6 +118,104 @@ const readMetrics = (root) => fs.readFileSync(path.join(root, 'workflow', 'metri
|
|
|
114
118
|
fs.rmSync(root, { recursive: true, force: true });
|
|
115
119
|
}
|
|
116
120
|
|
|
121
|
+
// ---- 闭环漏斗(2026-10-02 ledger-funnel-metrics;口径定义式 = specs/2026-10-02-ledger-funnel-metrics.md)----
|
|
122
|
+
const LROW = (o = {}) => JSON.stringify({
|
|
123
|
+
ts: '2026-10-02T00:00:00.000Z', doc: 'workflow/intents/demo.md', stage: 'approved',
|
|
124
|
+
fingerprint: 'b'.repeat(64), prev: 'draft', source: 'chat-delegated', quote: '确认', ...o,
|
|
125
|
+
});
|
|
126
|
+
// mkfixLedger:在 mkfix 基础上写台账(LROW 已是 JSON 字符串,直接 join——勿再 stringify 双重编码)
|
|
127
|
+
const mkfixLedger = (lines) => {
|
|
128
|
+
const root = mkfix();
|
|
129
|
+
fs.writeFileSync(path.join(root, '.agents', 'confirmations.jsonl'), lines.join('\n') + '\n', 'utf8');
|
|
130
|
+
return root;
|
|
131
|
+
};
|
|
132
|
+
{
|
|
133
|
+
// L1:一次通过(approved→done 同月)→ 收口 1 / 完整链 1 / 一次通过 1 / 周期 1.0 天
|
|
134
|
+
const root = mkfixLedger([
|
|
135
|
+
LROW({ ts: '2026-10-01T00:00:00.000Z' }),
|
|
136
|
+
LROW({ ts: '2026-10-02T00:00:00.000Z', stage: 'done', prev: 'approved' }),
|
|
137
|
+
]);
|
|
138
|
+
const r = run(root, ['--month', '2026-10']);
|
|
139
|
+
const md = readMetrics(root);
|
|
140
|
+
check('漏斗 L1 一次通过:| 2026-10 | 1 | 1 | 0 | 1 | 0 | 1.0 | 2 |',
|
|
141
|
+
r.status === 0 && /\| 2026-10 \| 1 \| 1 \| 0 \| 1 \| 0 \| 1\.0 \| 2 \|/.test(md), `${r.stdout}${md}`);
|
|
142
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
143
|
+
}
|
|
144
|
+
{
|
|
145
|
+
// L2:revert 行(回退后重走)→ 返工件
|
|
146
|
+
const root = mkfixLedger([
|
|
147
|
+
LROW({ ts: '2026-10-01T00:00:00.000Z', stage: 'revert-draft', fingerprint: 'n/a', prev: 'approved' }),
|
|
148
|
+
LROW({ ts: '2026-10-02T00:00:00.000Z' }),
|
|
149
|
+
LROW({ ts: '2026-10-03T00:00:00.000Z', stage: 'done', prev: 'approved' }),
|
|
150
|
+
]);
|
|
151
|
+
const r = run(root, ['--month', '2026-10']);
|
|
152
|
+
const md = readMetrics(root);
|
|
153
|
+
check('漏斗 L2 revert 行 → 返工件 1(周期按首 approved 起算 1.0 天)',
|
|
154
|
+
r.status === 0 && /\| 2026-10 \| 1 \| 1 \| 0 \| 0 \| 1 \| 1\.0 \| 3 \|/.test(md), `${r.stdout}${md}`);
|
|
155
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
156
|
+
}
|
|
157
|
+
{
|
|
158
|
+
// L3:重确认(两次 approved)→ 返工件
|
|
159
|
+
const root = mkfixLedger([
|
|
160
|
+
LROW({ ts: '2026-10-01T00:00:00.000Z' }),
|
|
161
|
+
LROW({ ts: '2026-10-02T00:00:00.000Z', quote: '重确认' }),
|
|
162
|
+
LROW({ ts: '2026-10-03T00:00:00.000Z', stage: 'done', prev: 'approved' }),
|
|
163
|
+
]);
|
|
164
|
+
const r = run(root, ['--month', '2026-10']);
|
|
165
|
+
const md = readMetrics(root);
|
|
166
|
+
check('漏斗 L3 两次 approved → 返工件 1',
|
|
167
|
+
r.status === 0 && /\| 2026-10 \| 1 \| 1 \| 0 \| 0 \| 1 \| 2\.0 \| 3 \|/.test(md), `${r.stdout}${md}`);
|
|
168
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
{
|
|
171
|
+
// L4:协议前(首行即终态,无 approved)→ 协议前 1(不计一次通过分母)
|
|
172
|
+
const root = mkfixLedger([
|
|
173
|
+
LROW({ ts: '2026-10-02T00:00:00.000Z', stage: 'done', prev: 'approved' }),
|
|
174
|
+
]);
|
|
175
|
+
const r = run(root, ['--month', '2026-10']);
|
|
176
|
+
const md = readMetrics(root);
|
|
177
|
+
check('漏斗 L4 协议前 → | 1 | 0 | 1 | 0 | 0 |',
|
|
178
|
+
r.status === 0 && /\| 2026-10 \| 1 \| 0 \| 1 \| 0 \| 0 \| 0\.0 \| 1 \|/.test(md), `${r.stdout}${md}`);
|
|
179
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
180
|
+
}
|
|
181
|
+
{
|
|
182
|
+
// L5:incident 两跳(fixed→closed = incident 确认链,复核 P2-3 收口)→ 完整链一次通过
|
|
183
|
+
const root = mkfixLedger([
|
|
184
|
+
LROW({ ts: '2026-10-01T00:00:00.000Z', doc: 'workflow/incidents/inc.md', stage: 'fixed' }),
|
|
185
|
+
LROW({ ts: '2026-10-02T00:00:00.000Z', doc: 'workflow/incidents/inc.md', stage: 'closed', prev: 'fixed' }),
|
|
186
|
+
]);
|
|
187
|
+
const r = run(root, ['--month', '2026-10']);
|
|
188
|
+
const md = readMetrics(root);
|
|
189
|
+
check('漏斗 L5 incident 两跳确认链 → 完整链 1 / 一次通过 1 / 周期 1.0',
|
|
190
|
+
r.status === 0 && /\| 2026-10 \| 1 \| 1 \| 0 \| 1 \| 0 \| 1\.0 \|/.test(md), `${r.stdout}${md}`);
|
|
191
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
192
|
+
}
|
|
193
|
+
{
|
|
194
|
+
// L7:incident 单行 closed(存量补关,无确认链)→ 协议前
|
|
195
|
+
const root = mkfixLedger([
|
|
196
|
+
LROW({ ts: '2026-10-02T00:00:00.000Z', doc: 'workflow/incidents/old.md', stage: 'closed', prev: 'fixed' }),
|
|
197
|
+
]);
|
|
198
|
+
const r = run(root, ['--month', '2026-10']);
|
|
199
|
+
const md = readMetrics(root);
|
|
200
|
+
check('漏斗 L7 incident 单行 closed → 协议前 1',
|
|
201
|
+
r.status === 0 && /\| 2026-10 \| 1 \| 0 \| 1 \| 0 \| 0 \|/.test(md), `${r.stdout}${md}`);
|
|
202
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
203
|
+
}
|
|
204
|
+
{
|
|
205
|
+
// L6:跨月收口(9 月 approved、10 月 done)→ 归桶 10 月;9 月只计台账行
|
|
206
|
+
const root = mkfixLedger([
|
|
207
|
+
LROW({ ts: '2026-09-30T00:00:00.000Z' }),
|
|
208
|
+
LROW({ ts: '2026-10-02T00:00:00.000Z', stage: 'done', prev: 'approved' }),
|
|
209
|
+
]);
|
|
210
|
+
const rSep = run(root, ['--month', '2026-09']);
|
|
211
|
+
const rOct = run(root, ['--month', '2026-10']);
|
|
212
|
+
const md = readMetrics(root);
|
|
213
|
+
check('漏斗 L6 跨月收口归桶 10 月(9 月零收口 + 台账行 1)',
|
|
214
|
+
rSep.status === 0 && rOct.status === 0 && /\| 2026-09 \| 0 \| 0 \| 0 \| 0 \| 0 \| — \| 1 \|/.test(md)
|
|
215
|
+
&& /\| 2026-10 \| 1 \| 1 \| 0 \| 1 \| 0 \| 2\.0 \| 1 \|/.test(md), `${rSep.stdout}${rOct.stdout}${md}`);
|
|
216
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
217
|
+
}
|
|
218
|
+
|
|
117
219
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
118
220
|
if (fail) {
|
|
119
221
|
console.log(`\n${USAGE}`);
|
|
@@ -23,6 +23,7 @@ export const POLICIES = {
|
|
|
23
23
|
bindingTs: '2026-09-28',
|
|
24
24
|
check17UnclosedAfter: '0.8.0',
|
|
25
25
|
stageGateSince: '2026-09-29', // 检查 19 逐阶段审计的生效日(2026-09-30 stage-gate-machine);锚 = 台账 ts 的 UTC 日期(复核 P2-3:本批 ts 2026-09-29T17:33Z 落在受审范围,顺序合规故静默);v1 无此键 → 该检查整体跳过
|
|
26
|
+
riskLevelSince: '2026-10-01', // intent risk_level 双字段协议生效日(2026-10-01 hybrid-governance 落地 fill-intent 双写;2026-10-02 caliber-convergence 方案 C 引入消费)——此日前建档的 intent 无该字段不判 suspect;v1 无此键 → 双字段协议视为从未生效(单字段判)
|
|
26
27
|
},
|
|
27
28
|
};
|
|
28
29
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// pre-push.test.mjs — 加固门目标分支判定 fixture 套件(2026-10-01 v09-review-defects)
|
|
3
|
+
// 真仓端到端:临时仓库挂载真实 pre-push,.agents/scripts/check-loop.sh 以桩脚本记录 argv。
|
|
4
|
+
// 断言三态 refspec:
|
|
5
|
+
// main→main → argv 含 --hardening(加固门)
|
|
6
|
+
// experiment/x→main → argv 含 --hardening(修复回归:直推漏口)
|
|
7
|
+
// experiment/y→experiment/y → argv 不含 --hardening(advisory)
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
// 钩子双布局探测:templates 布局(../../_githooks)与装户布局(../../.githooks)——本测试随 sync
|
|
16
|
+
// 落到 .agents/scripts 后 ../../_githooks 不存在(装户钩子在仓库根 .githooks/),2026-10-02 ci-red-batch
|
|
17
|
+
const PRE_PUSH = [
|
|
18
|
+
path.join(SCRIPT_DIR, '..', '..', '_githooks', 'pre-push'),
|
|
19
|
+
path.join(SCRIPT_DIR, '..', '..', '.githooks', 'pre-push'),
|
|
20
|
+
].find((p) => fs.existsSync(p));
|
|
21
|
+
|
|
22
|
+
let pass = 0;
|
|
23
|
+
let fail = 0;
|
|
24
|
+
const check = (desc, cond, detail = '') => {
|
|
25
|
+
if (cond) { pass++; console.log(`PASS ${desc}`); }
|
|
26
|
+
else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const git = (cwd, args) => spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
30
|
+
const run = (cwd, args) => spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
31
|
+
|
|
32
|
+
const setup = () => {
|
|
33
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'prepush-test-'));
|
|
34
|
+
const remote = path.join(root, 'remote.git');
|
|
35
|
+
fs.mkdirSync(path.join(root, '.githooks'), { recursive: true });
|
|
36
|
+
fs.mkdirSync(path.join(root, '.agents', 'scripts'), { recursive: true });
|
|
37
|
+
// 挂载真实 pre-push(装户同款),stub 替换 check-loop 记录 argv
|
|
38
|
+
fs.copyFileSync(PRE_PUSH, path.join(root, '.githooks', 'pre-push'));
|
|
39
|
+
// Linux:git 静默跳过不可执行钩子(推送照常成功、桩无记录)——挂载后必须设执行位;桩经 sh 调用无须 +x
|
|
40
|
+
fs.chmodSync(path.join(root, '.githooks', 'pre-push'), 0o755);
|
|
41
|
+
fs.writeFileSync(path.join(root, '.agents', 'scripts', 'check-loop.sh'), '#!/bin/sh\necho "$@" >> .stub-args\nexit 0\n');
|
|
42
|
+
fs.writeFileSync(path.join(root, 'a.txt'), 'a\n');
|
|
43
|
+
git(root, ['init', '-q']);
|
|
44
|
+
git(root, ['config', 'user.email', 't@t']);
|
|
45
|
+
git(root, ['config', 'user.name', 't']);
|
|
46
|
+
git(root, ['config', 'core.hooksPath', '.githooks']);
|
|
47
|
+
git(root, ['add', 'a.txt']);
|
|
48
|
+
git(root, ['commit', '-q', '-m', 'init']);
|
|
49
|
+
git(root, ['init', '-q', '--bare', remote]);
|
|
50
|
+
git(root, ['remote', 'add', 'origin', remote]);
|
|
51
|
+
return root;
|
|
52
|
+
};
|
|
53
|
+
const stubArgs = (root) => {
|
|
54
|
+
const p = path.join(root, '.stub-args');
|
|
55
|
+
return fs.existsSync(p) ? fs.readFileSync(p, 'utf8').split(/\n/).filter(Boolean) : [];
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const root = setup();
|
|
59
|
+
const w = (f, c) => fs.writeFileSync(path.join(root, f), c);
|
|
60
|
+
|
|
61
|
+
// 1) main → main:应含 --hardening
|
|
62
|
+
git(root, ['checkout', '-q', '-b', 'main']);
|
|
63
|
+
let r = run(root, ['push', 'origin', 'HEAD:main']);
|
|
64
|
+
check('main→main 推送成功', r.status === 0, `${r.stdout}${r.stderr}`);
|
|
65
|
+
check('main→main argv 含 --hardening', (stubArgs(root)[0] || '').includes('--hardening'), stubArgs(root).join('|'));
|
|
66
|
+
|
|
67
|
+
// 2) experiment/x → main(回归漏口):本地分支在 experiment,目标 main → 必须含 --hardening
|
|
68
|
+
git(root, ['checkout', '-q', '-b', 'experiment/x']);
|
|
69
|
+
w('b.txt', 'b\n');
|
|
70
|
+
git(root, ['add', 'b.txt']);
|
|
71
|
+
git(root, ['commit', '-q', '-m', 'x']);
|
|
72
|
+
r = run(root, ['push', 'origin', 'HEAD:main']);
|
|
73
|
+
check('experiment/x→main 推送成功', r.status === 0, `${r.stdout}${r.stderr}`);
|
|
74
|
+
check('experiment/x→main argv 含 --hardening(直推漏口修复)', (stubArgs(root)[1] || '').includes('--hardening'), stubArgs(root).join('|'));
|
|
75
|
+
|
|
76
|
+
// 3) experiment/y → experiment/y:目标在探索泳道 → 不含 --hardening
|
|
77
|
+
git(root, ['fetch', '-q', 'origin']);
|
|
78
|
+
git(root, ['branch', 'experiment/y', 'origin/main']);
|
|
79
|
+
git(root, ['checkout', '-q', 'experiment/y']);
|
|
80
|
+
w('c.txt', 'c\n');
|
|
81
|
+
git(root, ['add', 'c.txt']);
|
|
82
|
+
git(root, ['commit', '-q', '-m', 'y']);
|
|
83
|
+
r = run(root, ['push', 'origin', 'HEAD:experiment/y']);
|
|
84
|
+
check('experiment→experiment 推送成功', r.status === 0, `${r.stdout}${r.stderr}`);
|
|
85
|
+
const line3 = stubArgs(root)[2] || '';
|
|
86
|
+
check('experiment→experiment argv 不含 --hardening', !line3.includes('--hardening'), stubArgs(root).join('|'));
|
|
87
|
+
|
|
88
|
+
console.log(`\n${fail ? `❌ ${fail} failed` : '✅ all passed'}(${pass} passed)`);
|
|
89
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -1,101 +1,107 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// solidify-task: 将 L1
|
|
3
|
-
// 用法:
|
|
2
|
+
// solidify-task: 将 L1 液态草稿固化为正式 workflow 文档(迁移 + 可选确认 + 索引更新)
|
|
3
|
+
// 用法:
|
|
4
|
+
// node .agents/scripts/solidify-task.mjs --topic <主题> [--root <仓库根>]
|
|
5
|
+
// # 只迁移 + 更新索引,绝不写确认台账;末尾打印确认指引
|
|
6
|
+
// node .agents/scripts/solidify-task.mjs --topic <主题> --delegated "<用户原话>"
|
|
7
|
+
// # 迁移后,以调用方显式传入的用户原话逐份 confirm-doc --delegated(quote 原样转发)
|
|
8
|
+
// node .agents/scripts/solidify-task.mjs --topic <主题> --auto
|
|
9
|
+
// # 迁移后,逐份 confirm-doc --auto(trust-mode=Trusted 才放行;Strict 下被拒并计数失败)
|
|
10
|
+
// 确认门契约(incidents/2026-10-01-v09-review-defects):--delegated 的 quote 必须来自用户在对话内的
|
|
11
|
+
// 明确原话,由调用方显式传入——本脚本永不编造用户话术。无确认来源时只迁移不落账,末尾打印指引。
|
|
4
12
|
import fs from 'node:fs';
|
|
5
13
|
import path from 'node:path';
|
|
6
14
|
import { spawnSync } from 'node:child_process';
|
|
7
15
|
import { fileURLToPath } from 'node:url';
|
|
8
16
|
|
|
9
17
|
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
const ROOT = process.argv.includes('--root') ?
|
|
11
|
-
process.argv[process.argv.indexOf('--root') + 1] :
|
|
12
|
-
process.cwd();
|
|
13
|
-
const DRAFTS_DIR = path.join(ROOT, '.zcode', 'drafts');
|
|
14
|
-
const WF_ROOT = path.join(ROOT, 'workflow');
|
|
15
18
|
|
|
16
|
-
function fail(msg) {
|
|
19
|
+
function fail(msg) {
|
|
20
|
+
console.error(`❌ ${msg}`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
17
23
|
|
|
18
24
|
function parseArgs() {
|
|
19
25
|
const topicIdx = process.argv.indexOf('--topic');
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
const topic = topicIdx !== -1 ? process.argv[topicIdx + 1] : '';
|
|
27
|
+
if (!topic) fail('用法: node .agents/scripts/solidify-task.mjs --topic "主题" [--root <仓库根>] [--delegated "<用户原话>" | --auto]');
|
|
28
|
+
let root = process.cwd();
|
|
29
|
+
const rootIdx = process.argv.indexOf('--root');
|
|
30
|
+
if (rootIdx !== -1 && process.argv[rootIdx + 1]) root = process.argv[rootIdx + 1];
|
|
31
|
+
const delegatedIdx = process.argv.indexOf('--delegated');
|
|
32
|
+
const delegated = delegatedIdx !== -1 ? String(process.argv[delegatedIdx + 1] ?? '') : '';
|
|
33
|
+
const auto = process.argv.includes('--auto');
|
|
34
|
+
if (delegated && auto) fail('--delegated 与 --auto 互斥,只能选其一');
|
|
35
|
+
return { topic, root, delegated, auto };
|
|
25
36
|
}
|
|
26
37
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
38
|
+
const classify = (content) => {
|
|
39
|
+
if (content.includes('# INTENT')) return 'intents';
|
|
40
|
+
if (content.includes('# PLAN')) return 'plans';
|
|
41
|
+
if (content.includes('# SPEC')) return 'specs';
|
|
42
|
+
if (content.includes('# INCIDENT')) return 'incidents';
|
|
43
|
+
return null;
|
|
44
|
+
};
|
|
30
45
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
function run() {
|
|
47
|
+
const { topic, root, delegated, auto } = parseArgs();
|
|
48
|
+
const draftsDir = path.join(root, '.zcode', 'drafts');
|
|
49
|
+
if (!fs.existsSync(draftsDir)) fail(`草稿目录 ${draftsDir} 不存在`);
|
|
50
|
+
const files = fs.readdirSync(draftsDir).filter((f) => f.includes(topic) && f.endsWith('.md'));
|
|
51
|
+
if (!files.length) fail(`在 ${draftsDir} 中未找到主题为 ${topic} 的草稿文件`);
|
|
36
52
|
|
|
37
53
|
const today = new Date().toISOString().slice(0, 10);
|
|
38
54
|
const solidified = [];
|
|
39
|
-
|
|
40
|
-
// 2. 迁移并规范化命名
|
|
41
55
|
for (const file of files) {
|
|
42
|
-
const content = fs.readFileSync(path.join(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
if (content.includes('# INTENT')) targetDir = 'intents';
|
|
47
|
-
else if (content.includes('# PLAN')) targetDir = 'plans';
|
|
48
|
-
else if (content.includes('# SPEC')) targetDir = 'specs';
|
|
49
|
-
else if (content.includes('# INCIDENT')) targetDir = 'incidents';
|
|
50
|
-
else continue;
|
|
51
|
-
|
|
52
|
-
const targetName = `${today}-${file.replace(/.*-/, '').replace('.md', '.md')}`;
|
|
53
|
-
if (!targetName.startsWith('20')) {
|
|
54
|
-
// 如果文件名没日期前缀,强制加上
|
|
55
|
-
// 假设文件名为 <topic>.md
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// 统一处理:强制 YYYY-MM-DD- 前缀
|
|
56
|
+
const content = fs.readFileSync(path.join(draftsDir, file), 'utf8');
|
|
57
|
+
const targetDir = classify(content);
|
|
58
|
+
if (!targetDir) continue;
|
|
59
|
+
// 统一强制 YYYY-MM-DD- 前缀;已有前缀的文件名原样保留
|
|
59
60
|
const finalName = /^\d{4}-\d{2}-\d{2}-/.test(file) ? file : `${today}-${file}`;
|
|
60
|
-
const targetPath = path.join(
|
|
61
|
-
|
|
61
|
+
const targetPath = path.join(root, 'workflow', targetDir, finalName);
|
|
62
62
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
63
|
-
fs.copyFileSync(path.join(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
rel: `workflow/${targetDir}/${finalName}`,
|
|
67
|
-
abs: targetPath,
|
|
68
|
-
type: targetDir
|
|
69
|
-
});
|
|
70
|
-
console.log(` 📦 迁移: ${file} -> ${targetKebab(targetDir)}/${finalName}`);
|
|
63
|
+
fs.copyFileSync(path.join(draftsDir, file), targetPath);
|
|
64
|
+
solidified.push({ rel: `workflow/${targetDir}/${finalName}`, abs: targetPath, type: targetDir });
|
|
65
|
+
console.log(` 📦 迁移: ${file} -> ${targetDir}/${finalName}`);
|
|
71
66
|
}
|
|
67
|
+
if (!solidified.length) fail(`草稿中未找到可识别的 INTENT/PLAN/SPEC/INCIDENT 文档(主题 ${topic})`);
|
|
72
68
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
'
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
69
|
+
// 确认:仅当调用方显式提供确认来源时执行;否则只迁移,绝不落账。
|
|
70
|
+
let failed = 0;
|
|
71
|
+
if (delegated || auto) {
|
|
72
|
+
console.log('🛡️ 执行逐份确认...');
|
|
73
|
+
for (const item of solidified) {
|
|
74
|
+
const args = [path.join(SCRIPT_DIR, 'confirm-doc.mjs'), item.rel, '--root', root];
|
|
75
|
+
if (delegated) args.push('--delegated', delegated);
|
|
76
|
+
else args.push('--auto');
|
|
77
|
+
const res = spawnSync('node', args, { cwd: root, encoding: 'utf8' });
|
|
78
|
+
if (res.status !== 0) {
|
|
79
|
+
failed++;
|
|
80
|
+
console.error(` ❌ 确认失败 [${item.rel}]: ${(res.stderr || '').trim()}`);
|
|
81
|
+
} else {
|
|
82
|
+
console.log(` ✅ 已确认: ${item.rel}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
console.log(`确认汇总:${solidified.length - failed}/${solidified.length} 成功${failed ? `,${failed} 失败` : ''}`);
|
|
86
|
+
} else {
|
|
87
|
+
console.log('⏭️ 未提供确认来源,仅迁移不落账。');
|
|
88
|
+
for (const item of solidified) {
|
|
89
|
+
console.log(` → 确认指引: node .agents/scripts/confirm-doc.mjs ${item.rel}(TTY 键入「可以」)`);
|
|
90
|
+
console.log(` 或(用户对话内明确确认后): node .agents/scripts/confirm-doc.mjs ${item.rel} --delegated "<用户原话>"`);
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
//
|
|
94
|
+
// 更新索引;失败同样传播退出码。
|
|
94
95
|
console.log('📖 更新工作流索引...');
|
|
95
|
-
spawnSync('node', [path.join(SCRIPT_DIR, 'gen-workflow-index.mjs')], { stdio: 'inherit' });
|
|
96
|
+
const idx = spawnSync('node', [path.join(SCRIPT_DIR, 'gen-workflow-index.mjs')], { cwd: root, stdio: 'inherit' });
|
|
97
|
+
if (idx.status !== 0) {
|
|
98
|
+
console.error(' ❌ 索引更新失败');
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
96
101
|
|
|
102
|
+
if (failed) process.exit(1);
|
|
97
103
|
console.log(`\n✨ 任务 ${topic} 已成功固化!`);
|
|
98
104
|
console.log(`👉 建议执行: git add workflow/ .agents/confirmations.jsonl && git commit -m "docs(workflow): solidify L1 task ${topic}"`);
|
|
99
105
|
}
|
|
100
106
|
|
|
101
|
-
run();
|
|
107
|
+
run();
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// solidify-task.test.mjs — 快车道固化脚本 fixture 套件(2026-10-01 v09-review-defects)
|
|
3
|
+
// 验证确认门契约:无确认来源只迁移不落账;--delegated quote 原样转发;--auto Strict 下拒绝且失败退出;
|
|
4
|
+
// 互斥/缺草稿/非文档等边界。gen-workflow-index 无 --root 参数,spawn 时以 fixture 根为 cwd。
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const SCRIPT = path.join(SCRIPT_DIR, 'solidify-task.mjs');
|
|
13
|
+
|
|
14
|
+
let pass = 0;
|
|
15
|
+
let fail = 0;
|
|
16
|
+
const check = (desc, cond, detail = '') => {
|
|
17
|
+
if (cond) { pass++; console.log(`PASS ${desc}`); }
|
|
18
|
+
else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const ENUMS_FIXTURE = [
|
|
22
|
+
'doc.status.all=draft approved done superseded cancelled',
|
|
23
|
+
'doc.status.confirmed=approved done superseded cancelled',
|
|
24
|
+
'doc.status.active=draft approved',
|
|
25
|
+
'doc.status.terminal=done superseded cancelled',
|
|
26
|
+
'doc.status.abandoned=superseded cancelled',
|
|
27
|
+
'incident.status.all=open fixed closed',
|
|
28
|
+
'incident.status.active=open',
|
|
29
|
+
'level.all=L0 L1 L2 L3',
|
|
30
|
+
].join('\n') + '\n';
|
|
31
|
+
|
|
32
|
+
const mkfix = () => {
|
|
33
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'solidify-test-'));
|
|
34
|
+
for (const s of ['intents', 'plans', 'specs', 'incidents']) {
|
|
35
|
+
fs.mkdirSync(path.join(d, 'workflow', s), { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
fs.mkdirSync(path.join(d, '.agents'), { recursive: true });
|
|
38
|
+
fs.mkdirSync(path.join(d, '.zcode', 'drafts'), { recursive: true });
|
|
39
|
+
fs.writeFileSync(path.join(d, '.agents', 'workflow-enums.txt'), ENUMS_FIXTURE);
|
|
40
|
+
fs.writeFileSync(path.join(d, '.agents', 'workflow-modules.txt'), '# modules\npipeline\nui\n');
|
|
41
|
+
return d;
|
|
42
|
+
};
|
|
43
|
+
const DRAFT = (name, eol = '\n') =>
|
|
44
|
+
`---${eol}状态: draft${eol}级别: L1${eol}日期: 2026-10-01${eol}模块: pipeline${eol}---${eol}# INTENT — ${name}${eol}`;
|
|
45
|
+
const run = (root, args) => {
|
|
46
|
+
const r = spawnSync(process.execPath, [SCRIPT, ...args], { cwd: root, encoding: 'utf8' });
|
|
47
|
+
return { code: r.status, out: `${r.stdout || ''}${r.stderr || ''}` };
|
|
48
|
+
};
|
|
49
|
+
const ledgerLines = (root) => {
|
|
50
|
+
const p = path.join(root, '.agents', 'confirmations.jsonl');
|
|
51
|
+
return fs.existsSync(p) ? fs.readFileSync(p, 'utf8').split(/\n/).filter(Boolean) : [];
|
|
52
|
+
};
|
|
53
|
+
// 迁移目标名含当天日期前缀(solidify 以 toISOString 生成)——动态取值防跨天脆断
|
|
54
|
+
const TODAY = new Date().toISOString().slice(0, 10);
|
|
55
|
+
const migratedPath = (root) => path.join(root, 'workflow', 'intents', `${TODAY}-demo-task.md`);
|
|
56
|
+
|
|
57
|
+
// T1:默认只迁移不落账
|
|
58
|
+
{
|
|
59
|
+
const d = mkfix();
|
|
60
|
+
fs.writeFileSync(path.join(d, '.zcode', 'drafts', 'demo-task.md'), DRAFT('demo'));
|
|
61
|
+
const r = run(d, ['--topic', 'demo', '--root', d]);
|
|
62
|
+
check('T1 默认迁移成功 exit 0', r.code === 0, r.out);
|
|
63
|
+
check('T1 草稿已迁移到 intents 且日期前缀正确',
|
|
64
|
+
fs.existsSync(migratedPath(d)), '');
|
|
65
|
+
check('T1 零台账新增(无确认来源绝不落账)', ledgerLines(d).length === 0, JSON.stringify(ledgerLines(d)));
|
|
66
|
+
check('T1 输出含确认指引', r.out.includes('确认指引') && r.out.includes('--delegated'), r.out);
|
|
67
|
+
}
|
|
68
|
+
// T2:--delegated 原话原样转发
|
|
69
|
+
{
|
|
70
|
+
const d = mkfix();
|
|
71
|
+
fs.writeFileSync(path.join(d, '.zcode', 'drafts', 'demo-task.md'), DRAFT('demo'));
|
|
72
|
+
const r = run(d, ['--topic', 'demo', '--root', d, '--delegated', '固化了可以']);
|
|
73
|
+
check('T2 --delegated 流程 exit 0', r.code === 0, r.out);
|
|
74
|
+
const lines = ledgerLines(d);
|
|
75
|
+
check('T2 台账落 1 行', lines.length === 1, JSON.stringify(lines));
|
|
76
|
+
check('T2 quote 原样(脚本不编造话术)',
|
|
77
|
+
lines[0].includes('"quote":"固化了可以"') && lines[0].includes('chat-delegated'), lines[0]);
|
|
78
|
+
check('T2 目标文档状态已 approved',
|
|
79
|
+
fs.readFileSync(migratedPath(d), 'utf8').includes('状态: approved'), '');
|
|
80
|
+
}
|
|
81
|
+
// T3:--auto 在 Strict(缺 trust-mode.json)下被拒 → 失败计数并 exit 1
|
|
82
|
+
{
|
|
83
|
+
const d = mkfix();
|
|
84
|
+
fs.writeFileSync(path.join(d, '.zcode', 'drafts', 'demo-task.md'), DRAFT('demo'));
|
|
85
|
+
const r = run(d, ['--topic', 'demo', '--root', d, '--auto']);
|
|
86
|
+
check('T3 --auto 在缺省 Strict 下 exit 1(不吞失败)', r.code === 1, r.out);
|
|
87
|
+
check('T3 输出含失败汇总', r.out.includes('确认汇总') && r.out.includes('失败'), r.out);
|
|
88
|
+
check('T3 台账零行(拒绝不落账)', ledgerLines(d).length === 0, JSON.stringify(ledgerLines(d)));
|
|
89
|
+
}
|
|
90
|
+
// T4:缺草稿 → exit 1
|
|
91
|
+
{
|
|
92
|
+
const d = mkfix();
|
|
93
|
+
fs.writeFileSync(path.join(d, '.zcode', 'drafts', 'other.md'), DRAFT('other'));
|
|
94
|
+
const r = run(d, ['--topic', 'nope', '--root', d]);
|
|
95
|
+
check('T4 缺主题草稿 exit 1 且带指引', r.code === 1 && r.out.includes('未找到主题'), r.out);
|
|
96
|
+
}
|
|
97
|
+
// T5:草稿非可识别文档 → exit 1
|
|
98
|
+
{
|
|
99
|
+
const d = mkfix();
|
|
100
|
+
fs.writeFileSync(path.join(d, '.zcode', 'drafts', 'demo-note.md'), '# 随手笔记\nnot a workflow doc\n');
|
|
101
|
+
const r = run(d, ['--topic', 'demo', '--root', d]);
|
|
102
|
+
check('T5 无可识别文档 exit 1', r.code === 1 && r.out.includes('未找到可识别'), r.out);
|
|
103
|
+
}
|
|
104
|
+
// T6:--delegated 与 --auto 互斥 → exit 1
|
|
105
|
+
{
|
|
106
|
+
const d = mkfix();
|
|
107
|
+
fs.writeFileSync(path.join(d, '.zcode', 'drafts', 'demo-task.md'), DRAFT('demo'));
|
|
108
|
+
const r = run(d, ['--topic', 'demo', '--root', d, '--delegated', 'x', '--auto']);
|
|
109
|
+
check('T6 互斥 exit 1', r.code === 1 && r.out.includes('互斥'), r.out);
|
|
110
|
+
}
|
|
111
|
+
// T7:失败路径退出码传播(确认失败后 exit 1,不吞)
|
|
112
|
+
{
|
|
113
|
+
const d = mkfix();
|
|
114
|
+
fs.writeFileSync(path.join(d, '.zcode', 'drafts', 'demo-task.md'), DRAFT('demo'));
|
|
115
|
+
// 让确认失败:用 --auto 触发 Strict 拒绝(缺 trust-mode.json 即 Strict)。
|
|
116
|
+
const r = run(d, ['--topic', 'demo', '--root', d, '--auto']);
|
|
117
|
+
check('T7 失败后 exit 1 且索引仍生成', r.code === 1 && fs.existsSync(path.join(d, 'workflow', 'INDEX.md')), r.out);
|
|
118
|
+
}
|
|
119
|
+
// T8:索引更新失败传播退出码(独立于确认失败路径——删除 workflow/plans 目录使 gen-workflow-index 失败)
|
|
120
|
+
{
|
|
121
|
+
const d = mkfix();
|
|
122
|
+
fs.writeFileSync(path.join(d, '.zcode', 'drafts', 'demo-task.md'), DRAFT('demo'));
|
|
123
|
+
fs.rmSync(path.join(d, 'workflow', 'plans'), { recursive: true });
|
|
124
|
+
const r = run(d, ['--topic', 'demo', '--root', d]);
|
|
125
|
+
check('T8 索引更新失败 exit 1 且输出指明', r.code === 1 && r.out.includes('索引更新失败'), r.out);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
console.log(`\n${fail ? `❌ ${fail} failed` : '✅ all passed'}(${pass} passed)`);
|
|
129
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -119,7 +119,7 @@ export function entryConfirmed(root, base) {
|
|
|
119
119
|
const intentAbs = path.join(root, intentRel);
|
|
120
120
|
if (fs.existsSync(intentAbs)) {
|
|
121
121
|
const r = judgeDoc(root, intentAbs, intentRel, ENTRY_STATUSES);
|
|
122
|
-
return { ...r, kind: 'intent', rel: intentRel, level: readFm(intentAbs, '级别'), risk: readFm(intentAbs, 'risk_level'), storageLegacy: entryStorageLegacy(root, intentAbs) };
|
|
122
|
+
return { ...r, kind: 'intent', rel: intentRel, level: readFm(intentAbs, '级别'), risk: readFm(intentAbs, 'risk_level'), date: readFm(intentAbs, '日期'), storageLegacy: entryStorageLegacy(root, intentAbs) };
|
|
123
123
|
}
|
|
124
124
|
const incRel = `workflow/incidents/${base}.md`;
|
|
125
125
|
const incAbs = path.join(root, incRel);
|
|
@@ -143,15 +143,38 @@ export function specConfirmed(root, base) {
|
|
|
143
143
|
return judgeDoc(root, abs, rel, ENTRY_STATUSES);
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
// laneOfEntry / laneOfDoc:泳道单源取法(2026-10-02 caliber-convergence 方案 C「分歧双严」)
|
|
147
|
+
// risk_level 为 intent 专属协议字段(fill-intent 双写),生效锚 policy.riskLevelSince:
|
|
148
|
+
// 级别非法/缺失 → suspect(fail-closed 就严);
|
|
149
|
+
// 日期 ≥ 锚(双字段协议期):risk 合法且与级别一致 → 按值判 low/high,否则(缺失/非法/分歧)→ suspect;
|
|
150
|
+
// 日期 < 锚(协议前存量):两值俱在且分歧 → suspect,否则按级别单字段判(risk 缺席不惩罚——字段协议尚不存在)。
|
|
151
|
+
// suspect 消费规则(双严):红线类检查按 low 执行(拦红线);豁免类门(起草豁免/--batch/--auto/加固门
|
|
152
|
+
// lane/泳道豁免/逐阶段深度)按 high 执行(不豁免)。取代原三处并存语义(任一低即低 / 级别优先回落 / 仅级别)。
|
|
153
|
+
export function laneOfEntry(level, risk, riskLevelSince, date) {
|
|
154
|
+
const L = /^L[0-3]$/.test(level || '') ? level : '';
|
|
155
|
+
if (!L) return 'suspect';
|
|
156
|
+
const R = /^L[0-3]$/.test(risk || '') ? risk : '';
|
|
157
|
+
const dualProtocol = !!riskLevelSince && /^\d{4}-\d{2}-\d{2}$/.test(date || '') && date >= riskLevelSince;
|
|
158
|
+
if (dualProtocol) return (R && R === L) ? ((L === 'L0' || L === 'L1') ? 'low' : 'high') : 'suspect';
|
|
159
|
+
if (R && R !== L) return 'suspect';
|
|
160
|
+
return (L === 'L0' || L === 'L1') ? 'low' : 'high';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// laneOfDoc:非 intent 件(spec/plan/incident——级别单字段协议):合法按值判,非法 → suspect。
|
|
164
|
+
export function laneOfDoc(level) {
|
|
165
|
+
if (!/^L[0-3]$/.test(level || '')) return 'suspect';
|
|
166
|
+
return (level === 'L0' || level === 'L1') ? 'low' : 'high';
|
|
167
|
+
}
|
|
168
|
+
|
|
146
169
|
// draftGateFor:起草门总判(kind = 'spec' | 'plan',fill 工具与 confirm-doc 逐阶段门共用)
|
|
147
170
|
export function draftGateFor(kind, root, base) {
|
|
148
171
|
const entry = entryConfirmed(root, base);
|
|
149
172
|
if (!entry.ok) {
|
|
150
|
-
// 探索泳道豁免(2026-09-30 hybrid-governance-explore-hardening
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
const lane =
|
|
154
|
-
if (entry.kind === 'intent' && entry.status === 'draft' &&
|
|
173
|
+
// 探索泳道豁免(2026-09-30 hybrid-governance-explore-hardening):L0/L1 draft intent 放行起草
|
|
174
|
+
// ——「先动手后确认」;泳道取法自 2026-10-02 caliber-convergence 起单源 laneOfEntry(方案 C 分歧
|
|
175
|
+
// 双严:suspect 不豁免——级别/risk_level 缺失、非法或分歧均按防御道走入口确认)。
|
|
176
|
+
const lane = laneOfEntry(entry.level, entry.risk, loadKitPolicy(root).riskLevelSince, entry.date);
|
|
177
|
+
if (entry.kind === 'intent' && entry.status === 'draft' && lane === 'low') {
|
|
155
178
|
return { ok: true, entry, laneBypass: true };
|
|
156
179
|
}
|
|
157
180
|
return {
|