agentic-flow-kit 0.9.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +14 -2
- package/modules/hosts/claude/commands/wf-pipeline-run.md +35 -0
- 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 +14 -2
- package/modules/hosts/codex/commands/wf-pipeline-run.md +35 -0
- 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 +14 -2
- package/modules/hosts/cursor/commands/wf-pipeline-run.md +35 -0
- 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 +14 -2
- package/modules/hosts/opencode/commands/wf-pipeline-run.md +35 -0
- 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 +14 -2
- package/modules/hosts/trae/commands/wf-pipeline-run.md +36 -0
- 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/new-task.md +1 -0
- package/templates/_agents/commands/pipeline-run.md +43 -0
- 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 +70 -18
- package/templates/_agents/scripts/check-loop.test.mjs +155 -8
- 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/pipeline-run.mjs +999 -0
- package/templates/_agents/scripts/pipeline-run.test.mjs +537 -0
- 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
|
@@ -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
|
}
|
|
@@ -592,9 +608,18 @@ function verifyEvidenceTruth(evidenceStr, planBase, root) {
|
|
|
592
608
|
|
|
593
609
|
const changedFiles = show.stdout.split(/\r?\n/).filter(Boolean);
|
|
594
610
|
const hasIntersection = changedFiles.some(f => declaredFiles.some(df => f.includes(df) || df.includes(f)));
|
|
595
|
-
|
|
596
|
-
if (!hasIntersection)
|
|
597
|
-
|
|
611
|
+
|
|
612
|
+
if (!hasIntersection) {
|
|
613
|
+
// 过程证据(2026-10-03 check-evidence-process):执行器驱动的提交可作验收证据——提交信息带
|
|
614
|
+
// 「pipeline-run <runId>」标记(docsCommit 四种格式共有段,runId 严格形态 日期8-时间4-主题-尾;
|
|
615
|
+
// 与 run 事件流同源,git 历史留痕可对质)。只救 irrelevant:forged(SHA 不存在)在前已判,其余判据不动。
|
|
616
|
+
const subj = spawnSync(GIT, ['log', '-1', '--format=%s', sha], { cwd: ROOT, encoding: 'utf8' });
|
|
617
|
+
if (subj.status === 0 && /pipeline-run [0-9]{8}-[0-9]{4}-[A-Za-z0-9-]+/.test(subj.stdout)) {
|
|
618
|
+
return { ok: true, type: 'process' };
|
|
619
|
+
}
|
|
620
|
+
return { ok: false, type: 'irrelevant', msg: `提交 ${sha} 未触及 plan 声明的任何文件` };
|
|
621
|
+
}
|
|
622
|
+
|
|
598
623
|
return { ok: true, type: 'sha' };
|
|
599
624
|
}
|
|
600
625
|
|
|
@@ -1034,7 +1059,7 @@ runCheck16({ ROOT, ENUMS, docFiles, fmGet, inSet, isTracked, linesOf, readdirOrN
|
|
|
1034
1059
|
const body = (linesOf(iabs) || []).join('\n');
|
|
1035
1060
|
const d19 = fmGet(iabs, '日期') || fmGet(iabs, '发现');
|
|
1036
1061
|
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 };
|
|
1062
|
+
return { ...r, level: lvl19, risk: fmGet(iabs, 'risk_level'), date: d19, entryDraft: st19 === 'draft', storageLegacy: storLegacy };
|
|
1038
1063
|
}
|
|
1039
1064
|
const crel = `${WF}/incidents/${base}.md`;
|
|
1040
1065
|
const cabs = path.join(ROOT, crel);
|
|
@@ -1058,10 +1083,11 @@ runCheck16({ ROOT, ENUMS, docFiles, fmGet, inSet, isTracked, linesOf, readdirOrN
|
|
|
1058
1083
|
const rel = path.relative(ROOT, abs).split(path.sep).join('/');
|
|
1059
1084
|
const ent = entryConfirmed19(base);
|
|
1060
1085
|
if (!ent.ok) {
|
|
1061
|
-
// 协作道异步审计豁免(2026-09-30 hybrid-governance-risk-lanes
|
|
1062
|
-
//
|
|
1063
|
-
//
|
|
1064
|
-
|
|
1086
|
+
// 协作道异步审计豁免(2026-09-30 hybrid-governance-risk-lanes;2026-10-02 caliber-convergence
|
|
1087
|
+
// 起单源 laneOfEntry·方案 C):入口 intent 仍为 draft 且泳道 low(两字段一致 L0/L1)→「先动手后
|
|
1088
|
+
// 确认」是泳道语义,不告警;suspect(缺失/非法/分歧)不豁免;approved/done 却无台账行仍照报
|
|
1089
|
+
// (防手改状态冒充确认,与交叉一致性口径同);incident 入口不豁免(过目留痕本就轻量)。
|
|
1090
|
+
if (ent.entryDraft && laneOfEntry(ent.level, ent.risk, kitPolicy.riskLevelSince, ent.date) === 'low') continue;
|
|
1065
1091
|
warnings.push(`- [WARN 逐阶段] ${rel} 起草先于入口确认(${ent.why})`); continue;
|
|
1066
1092
|
}
|
|
1067
1093
|
// 库存量形态(标记 / 日期早于生效日)短路 level 校验与 spec 档(与起草门同口径,N1)
|
|
@@ -1120,7 +1146,8 @@ if (HARDENING) {
|
|
|
1120
1146
|
const st = fmGet(intent, '状态');
|
|
1121
1147
|
if (st === 'superseded' || st === 'cancelled') continue; // 放弃态:探索作废留档,不加固
|
|
1122
1148
|
const lvl = fmGet(intent, '级别');
|
|
1123
|
-
|
|
1149
|
+
// 泳道单源(2026-10-02 caliber-convergence 方案 C):suspect(缺失/非法/分歧)按 high——L2/L3 另须 spec
|
|
1150
|
+
const lane = laneOfEntry(lvl, fmGet(intent, 'risk_level'), kitPolicy.riskLevelSince, fmGet(intent, '日期'));
|
|
1124
1151
|
if (st !== 'approved' && st !== 'done') {
|
|
1125
1152
|
blockers.push(`- [加固未过] exploring 任务入 main 须先收口:${base} intent 状态仍为『${st || '缺失'}』——先确认至 approved/done 并补齐同名 spec+plan,或显式放弃(superseded/cancelled)`);
|
|
1126
1153
|
continue;
|
|
@@ -1132,11 +1159,13 @@ if (HARDENING) {
|
|
|
1132
1159
|
} else if (!stOkDoc(planSt)) {
|
|
1133
1160
|
blockers.push(`- [加固未过] exploring 任务入 main 的 plan 未收口:${base}(plan 状态『${planSt || '缺失'}』须为 approved/done)`);
|
|
1134
1161
|
}
|
|
1135
|
-
|
|
1162
|
+
// 泳道单源(2026-10-02 caliber-convergence 方案 C;复核 P1-1 修正):suspect(缺失/非法/分歧)与
|
|
1163
|
+
// high 同样另须同名 spec——仅 low(两字段一致 L0/L1)免 spec,只增不松。
|
|
1164
|
+
if (lane !== 'low') {
|
|
1136
1165
|
const specAbs = path.join(ROOT, WF, 'specs', base);
|
|
1137
1166
|
const specSt = fs.existsSync(specAbs) ? fmGet(specAbs, '状态') : '';
|
|
1138
1167
|
if (!fs.existsSync(specAbs)) {
|
|
1139
|
-
blockers.push(`- [加固未过] exploring 任务入 main 缺同名 spec:${base}(级别 ${
|
|
1168
|
+
blockers.push(`- [加固未过] exploring 任务入 main 缺同名 spec:${base}(级别 ${lvl}/risk ${fmGet(intent, 'risk_level') || '缺失'},应在 ${WF}/specs/ 下同名)`);
|
|
1140
1169
|
} else if (!stOkDoc(specSt)) {
|
|
1141
1170
|
blockers.push(`- [加固未过] exploring 任务入 main 的 spec 未收口:${base}(spec 状态『${specSt || '缺失'}』须为 approved/done)`);
|
|
1142
1171
|
}
|
|
@@ -1189,6 +1218,29 @@ function fmStatus(text) {
|
|
|
1189
1218
|
return '';
|
|
1190
1219
|
}
|
|
1191
1220
|
|
|
1221
|
+
// --- 20. 引擎脚本测试覆盖 [warning](2026-10-01 gate-script-test-coverage;仅包源环境) ---
|
|
1222
|
+
// 包源开发纪律:templates/_agents/scripts/ 下每个非 *.test.mjs 引擎脚本须有同名兄弟 .test.mjs,
|
|
1223
|
+
// 或在 .agents/scripts-test-exempt.txt 登记豁免(managed 下发)。装户(无 templates/)整体跳过。
|
|
1224
|
+
{
|
|
1225
|
+
const scriptsDir = path.join(ROOT, 'templates', '_agents', 'scripts');
|
|
1226
|
+
if (fs.existsSync(scriptsDir)) {
|
|
1227
|
+
const exemptFile = path.join(ROOT, '.agents', 'scripts-test-exempt.txt');
|
|
1228
|
+
const exempt = fs.existsSync(exemptFile)
|
|
1229
|
+
? fs.readFileSync(exemptFile, 'utf8').split(/\r?\n/)
|
|
1230
|
+
.map((l) => l.trim())
|
|
1231
|
+
.filter((l) => l && !l.startsWith('#'))
|
|
1232
|
+
.map((l) => l.split('|')[0].trim())
|
|
1233
|
+
.filter(Boolean)
|
|
1234
|
+
: [];
|
|
1235
|
+
const orphans = fs.readdirSync(scriptsDir)
|
|
1236
|
+
.filter((f) => f.endsWith('.mjs') && !f.endsWith('.test.mjs') && !exempt.includes(f))
|
|
1237
|
+
.filter((f) => !fs.existsSync(path.join(scriptsDir, f.replace(/\.mjs$/, '.test.mjs'))));
|
|
1238
|
+
for (const f of orphans.sort()) {
|
|
1239
|
+
warnings.push(`- [WARN 脚本测试缺失] ${f} 缺同名 .test.mjs——新增引擎脚本默认必须带测试;库件/工具可在 .agents/scripts-test-exempt.txt 登记豁免`);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1192
1244
|
// --- 输出(banner 沿 sh 版字样与格式 = 稳定输出契约:banner 后空行、条目 '- ' 前缀)---
|
|
1193
1245
|
if (blockers.length) {
|
|
1194
1246
|
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');
|
|
@@ -599,6 +599,44 @@ const mkConfirmedDoc = (root, rel, fmBody) => {
|
|
|
599
599
|
r.status === 0 && !outOf(r).includes('模板未填'), `exit=${r.status}\n${outOf(r)}`);
|
|
600
600
|
rmfix(T);
|
|
601
601
|
}
|
|
602
|
+
|
|
603
|
+
// ---- 场景 5x/5y/5z:检查 8 证据核验——过程证据形态(2026-10-03 check-evidence-process)----
|
|
604
|
+
// 造法:gitInit(激活证据核验的 rev-parse 路径)+ 锚前自报日期(豁免检查 15 存量面);
|
|
605
|
+
// 证据引用「改动面外文件的提交」:带执行器标记 → process 放行;无标记 → irrelevant 拦;SHA 不存在 → forged 拦。
|
|
606
|
+
{
|
|
607
|
+
const T = mkfix();
|
|
608
|
+
gitInit(T);
|
|
609
|
+
w(T, 'workflow/plans/2026-09-26-pe1.md', PLAN('pe1', '状态: done\n级别: L1\n模块: material', '\n## 改动方案\n\n- src/app.mjs:实现功能\n'));
|
|
610
|
+
fs.mkdirSync(path.join(T, 'notes'), { recursive: true });
|
|
611
|
+
w(T, 'notes/other.md', 'x\n');
|
|
612
|
+
gitCommitAll(T, 'docs(x): 演练穿越(pipeline-run 20260926-0900-pe1-abc1)');
|
|
613
|
+
const G = process.platform === 'win32' ? 'git.exe' : 'git';
|
|
614
|
+
const sha = spawnSync(G, ['rev-parse', '--short', 'HEAD'], { cwd: T, encoding: 'utf8' }).stdout.trim();
|
|
615
|
+
w(T, 'workflow/intents/2026-09-26-pe1.md', INTENT('pe1', '状态: done\n级别: L1\n日期: 2026-09-26', `\n## 验收标准(可测试)\n- [x] 演练穿越(证据:${sha})\n`));
|
|
616
|
+
expectOk('过程证据:提交信息带执行器标记(改动面外)→ 放行 exit 0', T);
|
|
617
|
+
rmfix(T);
|
|
618
|
+
}
|
|
619
|
+
{
|
|
620
|
+
const T = mkfix();
|
|
621
|
+
gitInit(T);
|
|
622
|
+
w(T, 'workflow/plans/2026-09-26-pe2.md', PLAN('pe2', '状态: done\n级别: L1\n模块: material', '\n## 改动方案\n\n- src/app.mjs:实现功能\n'));
|
|
623
|
+
fs.mkdirSync(path.join(T, 'notes'), { recursive: true });
|
|
624
|
+
w(T, 'notes/other.md', 'x\n');
|
|
625
|
+
gitCommitAll(T, 'docs(x): 演练穿越(L0,无标记提交)');
|
|
626
|
+
const G = process.platform === 'win32' ? 'git.exe' : 'git';
|
|
627
|
+
const sha = spawnSync(G, ['rev-parse', '--short', 'HEAD'], { cwd: T, encoding: 'utf8' }).stdout.trim();
|
|
628
|
+
w(T, 'workflow/intents/2026-09-26-pe2.md', INTENT('pe2', '状态: done\n级别: L1\n日期: 2026-09-26', `\n## 验收标准(可测试)\n- [x] 演练穿越(证据:${sha})\n`));
|
|
629
|
+
expectHard('过程证据:无标记无关提交 → 仍拦 证据无关', T, '证据无关');
|
|
630
|
+
rmfix(T);
|
|
631
|
+
}
|
|
632
|
+
{
|
|
633
|
+
const T = mkfix();
|
|
634
|
+
gitInit(T);
|
|
635
|
+
w(T, 'workflow/plans/2026-09-26-pe3.md', PLAN('pe3', '状态: done\n级别: L1\n模块: material', '\n## 改动方案\n\n- src/app.mjs:实现功能\n'));
|
|
636
|
+
w(T, 'workflow/intents/2026-09-26-pe3.md', INTENT('pe3', '状态: done\n级别: L1\n日期: 2026-09-26', '\n## 验收标准(可测试)\n- [x] 演练穿越(证据:deadbee)\n'));
|
|
637
|
+
expectHard('过程证据:SHA 不存在 → forged 仍拦(防伪线不放松)', T, '证据伪造');
|
|
638
|
+
rmfix(T);
|
|
639
|
+
}
|
|
602
640
|
// ---- 场景 45:检查 2——真未填占位(标题 <主题>,无 .md)→ 仍报模板未填 ----
|
|
603
641
|
{
|
|
604
642
|
const T = mkfix();
|
|
@@ -1676,14 +1714,25 @@ const mkConfirmedDoc = (root, rel, fmBody) => {
|
|
|
1676
1714
|
w(T, 'workflow/plans/2026-09-29-after.md', PLAN('after', '状态: draft\n级别: L1'));
|
|
1677
1715
|
gitCommitAll(T, 'draft after release');
|
|
1678
1716
|
r = run(T, { git: true });
|
|
1679
|
-
|
|
1680
|
-
|
|
1717
|
+
check('检查17 发版之后新建的 draft 不拦', r.status === 0 && !outOf(r).includes('发版草稿'), `exit=${r.status}\n${outOf(r)}`);
|
|
1718
|
+
rmfix(T);
|
|
1681
1719
|
}
|
|
1682
1720
|
{
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1721
|
+
const T = mkfix();
|
|
1722
|
+
gitInit(T);
|
|
1723
|
+
w(T, 'package.json', '{"name":"t","version":"0.1.0"}\n');
|
|
1724
|
+
w(T, 'workflow/incidents/2026-09-12-inc17.md', `---\n状态: open\n级别: L1\n发现: 2026-09-12\n模块: pipeline\n---\n# INCIDENT — inc17\n\n${三件套(false)}`);
|
|
1725
|
+
w(T, 'workflow/plans/2026-09-12-inc17.md', PLAN('inc17', '状态: draft\n级别: L1\n模块: pipeline'));
|
|
1726
|
+
gitCommitAll(T, 'v0.1.0');
|
|
1727
|
+
const r = run(T, { git: true });
|
|
1728
|
+
check('检查17 发版树上的 open incident 不参与(阻断由 plan 触发且不含 incident 路径)', r.status === 1 && outOf(r).includes('发版草稿') && !outOf(r).includes('workflow/incidents/2026-09-12-inc17.md'), `exit=${r.status}\n${outOf(r)}`);
|
|
1729
|
+
rmfix(T);
|
|
1730
|
+
}
|
|
1731
|
+
{
|
|
1732
|
+
const T = mkfix();
|
|
1733
|
+
gitInit(T);
|
|
1734
|
+
w(T, 'package.json', '{"name":"t","version":"0.1.0"}\n');
|
|
1735
|
+
w(T, 'workflow/intents/2026-09-12-deep.md', INTENT('deep', '状态: draft\n级别: L1\n日期: 2026-09-12'));
|
|
1687
1736
|
w(T, 'workflow/plans/2026-09-12-deep.md', PLAN('deep', '状态: draft\n级别: L1'));
|
|
1688
1737
|
gitCommitAll(T, 'v0.1.0');
|
|
1689
1738
|
for (let i = 1; i <= 41; i++) {
|
|
@@ -2179,7 +2228,7 @@ const mkConfirmedDoc = (root, rel, fmBody) => {
|
|
|
2179
2228
|
rmfix(T);
|
|
2180
2229
|
}
|
|
2181
2230
|
|
|
2182
|
-
// ---- 加固门(2026-09-30 hybrid-governance-explore-hardening;仅 --hardening 启用,不占 1-
|
|
2231
|
+
// ---- 加固门(2026-09-30 hybrid-governance-explore-hardening;仅 --hardening 启用,不占 1-20 编号)----
|
|
2183
2232
|
{
|
|
2184
2233
|
// H1:未标记 exploring 的 draft 件 → 带/不带 --hardening 均不拦(本门只管「已标记未收口」,
|
|
2185
2234
|
// 常规流程的 draft 中间态不受影响——intent approved 后 plan 起草属正常在途)
|
|
@@ -2253,5 +2302,103 @@ const mkConfirmedDoc = (root, rel, fmBody) => {
|
|
|
2253
2302
|
rmfix(T);
|
|
2254
2303
|
}
|
|
2255
2304
|
|
|
2305
|
+
// --- 20. 引擎脚本测试覆盖 [warning](2026-10-01 gate-script-test-coverage) ---
|
|
2306
|
+
{
|
|
2307
|
+
// C20-1:缺测试报 warning、有测试与豁免登记不报(包源环境)
|
|
2308
|
+
const T = mkfix();
|
|
2309
|
+
const sd = path.join(T, 'templates', '_agents', 'scripts');
|
|
2310
|
+
fs.mkdirSync(sd, { recursive: true });
|
|
2311
|
+
fs.writeFileSync(path.join(sd, 'foo.mjs'), 'export {};\n');
|
|
2312
|
+
fs.writeFileSync(path.join(sd, 'foo.test.mjs'), '// ok\n');
|
|
2313
|
+
fs.writeFileSync(path.join(sd, 'bar.mjs'), 'export {};\n');
|
|
2314
|
+
fs.writeFileSync(path.join(sd, 'baz.mjs'), 'export {};\n');
|
|
2315
|
+
fs.writeFileSync(path.join(T, '.agents', 'scripts-test-exempt.txt'), 'baz.mjs | 库件 | 无\n');
|
|
2316
|
+
const r1 = run(T);
|
|
2317
|
+
const o1 = outOf(r1);
|
|
2318
|
+
check('检查20 C20-1 缺测试报 warning、有测试与豁免不报',
|
|
2319
|
+
r1.status === 0 && o1.includes('[WARN 脚本测试缺失] bar.mjs') && !o1.includes('foo.mjs') && !o1.includes('baz.mjs'), o1);
|
|
2320
|
+
rmfix(T);
|
|
2321
|
+
}
|
|
2322
|
+
{
|
|
2323
|
+
// C20-2:装户环境(无 templates/)整体跳过
|
|
2324
|
+
const T = mkfix();
|
|
2325
|
+
const r2 = run(T);
|
|
2326
|
+
check('检查20 C20-2 装户(无 templates/)跳过', !outOf(r2).includes('[WARN 脚本测试缺失]'), outOf(r2));
|
|
2327
|
+
rmfix(T);
|
|
2328
|
+
}
|
|
2329
|
+
{
|
|
2330
|
+
// C20-3:豁免文件缺失时全量列出(无豁免兜底)
|
|
2331
|
+
const T = mkfix();
|
|
2332
|
+
const sd = path.join(T, 'templates', '_agents', 'scripts');
|
|
2333
|
+
fs.mkdirSync(sd, { recursive: true });
|
|
2334
|
+
fs.writeFileSync(path.join(sd, 'zeta.mjs'), 'export {};\n');
|
|
2335
|
+
const r3 = run(T);
|
|
2336
|
+
check('检查20 C20-3 无豁免文件时缺测试脚本被列出', outOf(r3).includes('[WARN 脚本测试缺失] zeta.mjs'), outOf(r3));
|
|
2337
|
+
rmfix(T);
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
// --- 方案 C:laneOfEntry 单源三态 + 检查 1 suspect 红线 + 检查 2 样例豁免(2026-10-02 caliber-convergence) ---
|
|
2341
|
+
{
|
|
2342
|
+
check('laneOfEntry 一致 low / 一致 high',
|
|
2343
|
+
laneOfEntry('L1', 'L1', '2026-10-01', '2026-10-02') === 'low' && laneOfEntry('L2', 'L2', '2026-10-01', '2026-10-02') === 'high');
|
|
2344
|
+
check('laneOfEntry 协议期:分歧 / risk 缺失 / 级别缺失 → suspect',
|
|
2345
|
+
laneOfEntry('L2', 'L1', '2026-10-01', '2026-10-02') === 'suspect'
|
|
2346
|
+
&& laneOfEntry('L2', '', '2026-10-01', '2026-10-02') === 'suspect'
|
|
2347
|
+
&& laneOfEntry('', 'L2', '2026-10-01', '2026-10-02') === 'suspect');
|
|
2348
|
+
check('laneOfEntry 协议前:单字段按级别 / 两值俱在分歧仍 suspect',
|
|
2349
|
+
laneOfEntry('L2', '', '2026-10-01', '2026-09-28') === 'high'
|
|
2350
|
+
&& laneOfEntry('L2', 'L1', '2026-10-01', '2026-09-28') === 'suspect');
|
|
2351
|
+
check('laneOfDoc 单字段三态', laneOfDoc('L1') === 'low' && laneOfDoc('L3') === 'high' && laneOfDoc('') === 'suspect');
|
|
2352
|
+
}
|
|
2353
|
+
{
|
|
2354
|
+
// 检查 1:协议期分歧件(L2/L1)勾红线 → suspect 按 low 拦红线(hard)
|
|
2355
|
+
const T = mkfix();
|
|
2356
|
+
fs.writeFileSync(path.join(T, 'workflow', 'intents', '2026-10-02-x.md'),
|
|
2357
|
+
INTENT('x', '状态: approved\n级别: L2\nrisk_level: L1\n日期: 2026-10-02\n模块: pipeline',
|
|
2358
|
+
'\n## 触达红线\n- [x] 规则 / 契约变更(→ 级别至少 L2)\n'));
|
|
2359
|
+
fs.writeFileSync(path.join(T, 'workflow', 'plans', '2026-10-02-x.md'), '# PLAN\n');
|
|
2360
|
+
const r = run(T);
|
|
2361
|
+
check('检查1 分歧件勾红线 → hard-block(suspect 拦红线)', r.status === 1 && outOf(r).includes('[红线判低]') && outOf(r).includes('两字段对齐'), outOf(r));
|
|
2362
|
+
rmfix(T);
|
|
2363
|
+
}
|
|
2364
|
+
{
|
|
2365
|
+
// 检查 1 对照:协议前存量(级别 L2 无 risk_level)勾红线 → 不拦(单字段 high)
|
|
2366
|
+
const T = mkfix();
|
|
2367
|
+
fs.writeFileSync(path.join(T, 'workflow', 'intents', '2026-09-28-y.md'),
|
|
2368
|
+
INTENT('y', '状态: approved\n级别: L2\n日期: 2026-09-28\n模块: pipeline',
|
|
2369
|
+
'\n## 触达红线\n- [x] 规则 / 契约变更(→ 级别至少 L2)\n'));
|
|
2370
|
+
fs.writeFileSync(path.join(T, 'workflow', 'plans', '2026-09-28-y.md'), '# PLAN\n');
|
|
2371
|
+
const r = run(T);
|
|
2372
|
+
check('检查1 协议前 L2 单字段勾红线 → 不拦(存量零新增损害)', !outOf(r).includes('[红线判低]'), outOf(r));
|
|
2373
|
+
rmfix(T);
|
|
2374
|
+
}
|
|
2375
|
+
{
|
|
2376
|
+
// 检查 2:样例豁免三态——反引号/围栏块内不报,正文裸占位符照报
|
|
2377
|
+
const T = mkfix();
|
|
2378
|
+
fs.writeFileSync(path.join(T, 'workflow', 'intents', '2026-10-02-z.md'),
|
|
2379
|
+
INTENT('z', '状态: approved\n级别: L2\nrisk_level: L2\n日期: 2026-10-02\n模块: pipeline',
|
|
2380
|
+
'\n行内样例 `YYYY-MM-DD` 与 `<主题>` 不报。\n\n```\n日期: YYYY-MM-DD\n```\n\n裸占位:日期: YYYY-MM-DD\n'));
|
|
2381
|
+
fs.writeFileSync(path.join(T, 'workflow', 'plans', '2026-10-02-z.md'), '# PLAN\n');
|
|
2382
|
+
const r = run(T);
|
|
2383
|
+
const o = outOf(r);
|
|
2384
|
+
const zWarn = (o.match(/2026-10-02-z\.md 含模板占位符:[\s\S]*?(?=\n- |\n\n|$)/) || [''])[0];
|
|
2385
|
+
check('检查2 反引号/围栏样例不报、裸占位符照报',
|
|
2386
|
+
zWarn.includes('裸占位') && !zWarn.includes('行内样例') && !zWarn.includes('```'), o);
|
|
2387
|
+
rmfix(T);
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
{
|
|
2391
|
+
// H7(2026-10-02 caliber-convergence 复核 P1-1):suspect(级别 L2/risk_level L1 分歧,协议期)转正
|
|
2392
|
+
// → 与 high 同样另须同名 spec——缺 spec 即拦(只增不松:suspect 不得逃 spec 前置)。
|
|
2393
|
+
const T = mkfix();
|
|
2394
|
+
w(T, 'workflow/intents/2026-10-02-h7.md',
|
|
2395
|
+
INTENT('h7', '状态: approved\n级别: L2\nrisk_level: L1\n日期: 2026-10-02\n模块: pipeline\n阶段: exploring'));
|
|
2396
|
+
w(T, 'workflow/plans/2026-10-02-h7.md', PLAN('h7', '状态: approved\n级别: L2'));
|
|
2397
|
+
const r = run(T, { args: ['--hardening'] });
|
|
2398
|
+
check('加固门 H7:suspect(L2/L1 分歧)转正 → 缺同名 spec 即拦',
|
|
2399
|
+
r.status === 1 && outOf(r).includes('缺同名 spec'), outOf(r));
|
|
2400
|
+
rmfix(T);
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2256
2403
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
2257
2404
|
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
|
}
|
|
@@ -572,6 +572,37 @@ const gitHead = (root) => {
|
|
|
572
572
|
fs.rmSync(root, { recursive: true, force: true });
|
|
573
573
|
}
|
|
574
574
|
|
|
575
|
+
// ---- S31b【方案 C 分歧双严】--batch 分歧件(级别 L1 / risk_level L2,协议期)→ 泳道 suspect 逐份拒绝(2026-10-02 caliber-convergence)----
|
|
576
|
+
{
|
|
577
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-bt3-'));
|
|
578
|
+
fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
|
|
579
|
+
fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
|
|
580
|
+
fs.writeFileSync(path.join(root, '.agents', 'kit.json'), JSON.stringify({ policyVersion: 2 }));
|
|
581
|
+
fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-10-02-dv1.md'), '---\n状态: draft\n级别: L1\nrisk_level: L2\n日期: 2026-10-02\n---\n# DV\n');
|
|
582
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-10-02-dv1.md', '--delegated', '批量放行', '--batch'], { cwd: root, encoding: 'utf8' });
|
|
583
|
+
const led = readLedgerOf(root);
|
|
584
|
+
const st = fs.readFileSync(path.join(root, 'workflow', 'intents', '2026-10-02-dv1.md'), 'utf8');
|
|
585
|
+
check('S31b --batch 分歧件 → 泳道 suspect 拒(exit 2 + 对齐两字段提示 + 零台账未落盘)',
|
|
586
|
+
r.status === 2 && /泳道「suspect」/.test(r.stderr) && /对齐/.test(r.stderr) && led.length === 0 && st.includes('状态: draft'),
|
|
587
|
+
JSON.stringify({ status: r.status, stderr: String(r.stderr).slice(0, 300), led }));
|
|
588
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// ---- S31c【方案 C 分歧双严】--auto 分歧件 → suspect 拒(消息锁定,先于 trust 判定短路)----
|
|
592
|
+
{
|
|
593
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-bt4-'));
|
|
594
|
+
fs.mkdirSync(path.join(root, 'workflow', 'intents'), { recursive: true });
|
|
595
|
+
fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
|
|
596
|
+
fs.writeFileSync(path.join(root, '.agents', 'kit.json'), JSON.stringify({ policyVersion: 2 }));
|
|
597
|
+
fs.writeFileSync(path.join(root, '.agents', 'trust-mode.json'), JSON.stringify({ enabled: true, level: 1, name: 'Standard', enabledAt: '2026-10-02T00:00:00.000Z' }));
|
|
598
|
+
fs.writeFileSync(path.join(root, 'workflow', 'intents', '2026-10-02-dv2.md'), '---\n状态: draft\n级别: L1\nrisk_level: L2\n日期: 2026-10-02\n---\n# DV2\n');
|
|
599
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-10-02-dv2.md', '--auto'], { cwd: root, encoding: 'utf8' });
|
|
600
|
+
check('S31c --auto(trust=Standard)分歧件 → 泳道 suspect 拒(消息含 suspect 与对齐指引)',
|
|
601
|
+
r.status !== 0 && /suspect/.test(r.stderr) && /对齐/.test(r.stderr),
|
|
602
|
+
JSON.stringify({ status: r.status, stderr: String(r.stderr).slice(0, 300) }));
|
|
603
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
604
|
+
}
|
|
605
|
+
|
|
575
606
|
// ---- S32 --batch 组合约束:与 --auto / --to 互斥(exit 1,零落账)----
|
|
576
607
|
{
|
|
577
608
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-bt3-'));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// flow-kit fill-intent:起草 intent
|
|
1
|
+
// flow-kit fill-intent:起草 intent 草稿(背景与问题/历史教训防复发/目标/非目标/约束/影响面/触达红线/验收标准 8 节正文 + 确认与复核 共 9 节)
|
|
2
2
|
// 读 workflow/intents/_TEMPLATE.md 的节标题 → 输出符合 frontmatter 受限子集(6 字段,含 risk_level 风险泳道)的草稿到指定文件
|
|
3
3
|
// 用法:node .agents/scripts/fill-intent.mjs --module pipeline --level L1 --topic "test" --output /tmp/x.md
|
|
4
4
|
// 零依赖;CLI 解析用最简字符串匹配;不替代 AI 起草——本工具只输出模板与提示句
|
|
@@ -124,11 +124,12 @@ const runTool = (root, base, level) => spawnSync(process.execPath,
|
|
|
124
124
|
fs.rmSync(root, { recursive: true, force: true });
|
|
125
125
|
}
|
|
126
126
|
{
|
|
127
|
-
// S10c
|
|
127
|
+
// S10c(2026-10-02 caliber-convergence 方案 C 改判):级别缺失 → suspect,不再回落 risk_level——
|
|
128
|
+
// 豁免仅认两字段一致的 low;缺失须先补「级别」再走起草(fail-closed 就严)。
|
|
128
129
|
const root = mkws({ 'workflow/intents/2026-09-30-exp0.md': '---\n状态: draft\nrisk_level: L0\n日期: 2026-09-30\n---\n# I\n' });
|
|
129
130
|
const r = runTool(root, '2026-09-30-exp0', 'L1');
|
|
130
|
-
check('S10c
|
|
131
|
-
r.status ===
|
|
131
|
+
check('S10c 方案 C:级别缺失 + risk_level L0 → suspect 拒(不回落,先补级别)',
|
|
132
|
+
r.status === 2 && !fs.existsSync(path.join(root, 'workflow', 'plans', '2026-09-30-exp0.md')),
|
|
132
133
|
JSON.stringify({ status: r.status, stderr: r.stderr }));
|
|
133
134
|
fs.rmSync(root, { recursive: true, force: true });
|
|
134
135
|
}
|
|
@@ -84,6 +84,7 @@ const PAIRS = [
|
|
|
84
84
|
{ doctor: '7', cl: '17', note: '经 §7 整体运行覆盖(发版提交树上仍未收口的 intent/spec/plan)' },
|
|
85
85
|
{ doctor: '7', cl: '18', note: '经 §7 整体运行覆盖(委派台账对账)' },
|
|
86
86
|
{ doctor: '7', cl: '19', note: '经 §7 整体运行覆盖(逐阶段审计——起草先于入口确认 / 台账审批顺序倒置)' },
|
|
87
|
+
{ doctor: '7', cl: '20', note: '经 §7 整体运行覆盖(引擎脚本测试覆盖,2026-10-01 gate-script-test-coverage)' },
|
|
87
88
|
];
|
|
88
89
|
|
|
89
90
|
// gateChecklist:按登记表对照两侧检查项(pure function;pairs 可注入供测试)
|
|
@@ -168,6 +168,59 @@ const idxText = fs.existsSync(path.join(WORKFLOW, 'INDEX.md')) ? fs.readFileSync
|
|
|
168
168
|
const idxBytes = Buffer.byteLength(idxText, 'utf8');
|
|
169
169
|
const idxActive = Number((idxText.match(/^## 活跃((\d+))/m) || [])[1] ?? NaN);
|
|
170
170
|
|
|
171
|
+
// ---- 闭环漏斗(2026-10-02 ledger-funnel-metrics;口径定义式单源 = workflow/specs/2026-10-02-ledger-funnel-metrics.md)----
|
|
172
|
+
// 只读台账一手字段(doc/stage/ts/source),坏行跳过(与检查 15/19 同容错);观测面不挂门禁、不新增文档断言(检查 16 零接触)。
|
|
173
|
+
const LEDGER_P = path.join('.agents', 'confirmations.jsonl');
|
|
174
|
+
function readLedgerRows() {
|
|
175
|
+
if (!fs.existsSync(LEDGER_P)) return [];
|
|
176
|
+
return fs.readFileSync(LEDGER_P, 'utf8').split(/\r?\n/)
|
|
177
|
+
.map((l) => { try { return JSON.parse(l); } catch { return null; } })
|
|
178
|
+
.filter((e) => e && typeof e === 'object');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function funnelFromLedger(entries, month) {
|
|
182
|
+
const byDoc = new Map();
|
|
183
|
+
for (const e of entries) {
|
|
184
|
+
if (typeof e.doc !== 'string' || typeof e.ts !== 'string') continue;
|
|
185
|
+
if (!byDoc.has(e.doc)) byDoc.set(e.doc, []);
|
|
186
|
+
byDoc.get(e.doc).push(e);
|
|
187
|
+
}
|
|
188
|
+
const res = { closed: 0, full: 0, legacy: 0, once: 0, rework: 0, ledgerRows: 0, days: [], items: [] };
|
|
189
|
+
for (const e of entries) if (typeof e.ts === 'string' && e.ts.slice(0, 7) === month) res.ledgerRows++;
|
|
190
|
+
for (const [doc, rs] of byDoc) {
|
|
191
|
+
rs.sort((a, b) => String(a.ts).localeCompare(String(b.ts)));
|
|
192
|
+
const nonRevert = rs.filter((r) => !/^revert-/.test(String(r.stage || '')));
|
|
193
|
+
const terminals = nonRevert.filter((r) => r.stage === 'done' || r.stage === 'closed');
|
|
194
|
+
if (!terminals.length) continue; // 未收口件不进漏斗桶(仍计台账行数)
|
|
195
|
+
const lastTerm = terminals[terminals.length - 1];
|
|
196
|
+
if (String(lastTerm.ts).slice(0, 7) !== month) continue; // 月度归桶 = 终态行所在月
|
|
197
|
+
// 确认阶段按件型(复核 P2-3 收口):docs=approved;incidents=fixed(两跳确认门 fixed→closed,
|
|
198
|
+
// 永无 approved 行——按 approved 判会把已走确认门的 incident 误归协议前)
|
|
199
|
+
const isInc = doc.includes('/incidents/');
|
|
200
|
+
const confirmStage = isInc ? 'fixed' : 'approved';
|
|
201
|
+
const confirms = nonRevert.filter((r) => r.stage === confirmStage);
|
|
202
|
+
const hasChain = confirms.length >= 1;
|
|
203
|
+
const hasRevert = rs.some((r) => /^revert-/.test(String(r.stage || '')));
|
|
204
|
+
const kind = !hasChain ? 'legacy' : (hasRevert || confirms.length >= 2 || terminals.length >= 2 ? 'rework' : 'once');
|
|
205
|
+
const firstTs = hasChain ? confirms[0].ts : rs[0].ts; // 协议前件首行起算
|
|
206
|
+
const days = (Date.parse(lastTerm.ts) - Date.parse(firstTs)) / 86400000;
|
|
207
|
+
res.closed++;
|
|
208
|
+
if (kind === 'legacy') res.legacy++;
|
|
209
|
+
else { res.full++; res[kind === 'once' ? 'once' : 'rework']++; }
|
|
210
|
+
if (Number.isFinite(days) && days >= 0) res.days.push(days);
|
|
211
|
+
res.items.push({ doc, kind, days: Number.isFinite(days) && days >= 0 ? days.toFixed(1) : '—' });
|
|
212
|
+
}
|
|
213
|
+
return res;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const ledgerRows = readLedgerRows();
|
|
217
|
+
const funnel = funnelFromLedger(ledgerRows, month);
|
|
218
|
+
const sortedDays = [...funnel.days].sort((a, b) => a - b);
|
|
219
|
+
const medianDays = sortedDays.length
|
|
220
|
+
? (sortedDays.length % 2 ? sortedDays[(sortedDays.length - 1) / 2] : (sortedDays[sortedDays.length / 2 - 1] + sortedDays[sortedDays.length / 2]) / 2)
|
|
221
|
+
: null;
|
|
222
|
+
const funnelRow = `| ${month} | ${funnel.closed} | ${funnel.full} | ${funnel.legacy} | ${funnel.once} | ${funnel.rework} | ${medianDays === null ? '—' : medianDays.toFixed(1)} | ${funnel.ledgerRows} |`;
|
|
223
|
+
|
|
171
224
|
const docs = scanDocs();
|
|
172
225
|
const surf = scanSurface();
|
|
173
226
|
const row = `| ${month} | ${docs.files}(活跃 ${docs.active} / 终态 ${docs.terminal}) | ${kb(docs.bytes)} | ${docs.withModule}/${docs.files} | ${kb(surf.total)}(峰值 ${surf.peak ? `${surf.peak.pct.toFixed(1)}% ${surf.peak.label}` : '—'}) | ${kb(idxBytes)}(${Number.isNaN(idxActive) ? '—' : idxActive} 行) |`;
|
|
@@ -179,9 +232,12 @@ for (const [t, v] of Object.entries(docs.perType)) console.log(` ${t.padEnd(1
|
|
|
179
232
|
console.log(` 索引:workflow/INDEX.md ${kb(idxBytes)}(活跃 ${Number.isNaN(idxActive) ? '—' : idxActive} 行)`);
|
|
180
233
|
for (const e of surf.entries) console.log(` ${e.label.padEnd(44)} ${String(e.max).padStart(6)} / ${String(e.lim).padStart(6)} B(${e.pct.toFixed(1)}%)`);
|
|
181
234
|
console.log(` 常驻面合计:${kb(surf.total)}`);
|
|
235
|
+
console.log(` 闭环漏斗:收口 ${funnel.closed}(完整链 ${funnel.full} = 一次通过 ${funnel.once} + 返工 ${funnel.rework};协议前 ${funnel.legacy})|中位周期 ${medianDays === null ? '—' : `${medianDays.toFixed(1)} 天`}|台账行 ${funnel.ledgerRows}`);
|
|
236
|
+
for (const it of funnel.items) console.log(` ${String(it.doc).padEnd(58)} ${it.kind.padEnd(7)} ${it.days} 天`);
|
|
237
|
+
if (funnel.items.length) console.log(' ℹ️ 机器口径可与 workflow/delegations.md 自做表的人工自报交叉对照(差异即信号,不对账)');
|
|
182
238
|
console.log(` ${DRY ? '(--dry-run,未写盘)' : ''}`);
|
|
183
239
|
|
|
184
|
-
if (DRY) { console.log(`\n将写入行:\n${row}`); process.exit(0); }
|
|
240
|
+
if (DRY) { console.log(`\n将写入行:\n${row}\n${funnelRow}`); process.exit(0); }
|
|
185
241
|
|
|
186
242
|
// ---- 组装:GENERATED 区内的月表按月份键 upsert(同月重跑覆盖,行序按月升序)----
|
|
187
243
|
const HEADER = `# workflow 规模与规则面月度快照
|
|
@@ -193,17 +249,31 @@ const TABLE_HEAD = [
|
|
|
193
249
|
'| 月份 | 文档(活跃/终态) | 文档字节 | 模块已填/总 | 常驻面字节(预算峰值) | INDEX(活跃行) |',
|
|
194
250
|
'|---|---|---|---|---|---|',
|
|
195
251
|
];
|
|
252
|
+
// 漏斗表头(第二表;口径定义式单源 = workflow/specs/2026-10-02-ledger-funnel-metrics.md)
|
|
253
|
+
const FUNNEL_HEAD = [
|
|
254
|
+
'## 闭环漏斗(机器口径,源:.agents/confirmations.jsonl——定义式见 specs/2026-10-02-ledger-funnel-metrics.md)',
|
|
255
|
+
'',
|
|
256
|
+
'| 月份 | 收口 | 完整链 | 协议前 | 一次通过 | 返工件 | 中位周期(天) | 台账行数 |',
|
|
257
|
+
'|---|---|---|---|---|---|---|---|',
|
|
258
|
+
];
|
|
196
259
|
const existing = fs.existsSync(METRICS_P) ? fs.readFileSync(METRICS_P, 'utf8') : '';
|
|
197
260
|
let rows = [];
|
|
261
|
+
let funnelRows = [];
|
|
198
262
|
if (existing.includes(BEGIN) && existing.includes(END)) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
263
|
+
const section = existing.slice(existing.indexOf(BEGIN), existing.indexOf(END));
|
|
264
|
+
const marker = FUNNEL_HEAD[0];
|
|
265
|
+
const bodyPart = section.includes(marker) ? section.slice(0, section.indexOf(marker)) : section;
|
|
266
|
+
const funnelPart = section.includes(marker) ? section.slice(section.indexOf(marker)) : '';
|
|
267
|
+
rows = bodyPart.split(/\r?\n/).filter((l) => /^\|\s*\d{4}-\d{2}\s*\|/.test(l));
|
|
268
|
+
funnelRows = funnelPart.split(/\r?\n/).filter((l) => /^\|\s*\d{4}-\d{2}\s*\|/.test(l));
|
|
202
269
|
}
|
|
203
270
|
rows = rows.filter((l) => !l.startsWith(`| ${month} `));
|
|
204
271
|
rows.push(row);
|
|
205
272
|
rows.sort((a, b) => a.slice(2, 9).localeCompare(b.slice(2, 9)));
|
|
206
|
-
|
|
273
|
+
funnelRows = funnelRows.filter((l) => !l.startsWith(`| ${month} `));
|
|
274
|
+
funnelRows.push(funnelRow);
|
|
275
|
+
funnelRows.sort((a, b) => a.slice(2, 9).localeCompare(b.slice(2, 9)));
|
|
276
|
+
const inner = `${BEGIN}\n\n${[...TABLE_HEAD, ...rows].join('\n')}\n\n${[...FUNNEL_HEAD, ...funnelRows].join('\n')}\n\n${END}`;
|
|
207
277
|
let out;
|
|
208
278
|
if (existing.includes(BEGIN) && existing.includes(END)) {
|
|
209
279
|
out = existing.slice(0, existing.indexOf(BEGIN)) + inner + existing.slice(existing.indexOf(END) + END.length);
|