agentic-flow-kit 1.8.1 → 1.8.7
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 +1 -1
- package/modules/hosts/claude/commands/wf-build.md +1 -1
- package/modules/hosts/claude/commands/wf-design.md +1 -1
- package/modules/hosts/claude/commands/wf-new-task.md +1 -0
- package/modules/hosts/claude/commands/wf-review.md +1 -1
- package/modules/hosts/codex/commands/wf-build.md +1 -1
- package/modules/hosts/codex/commands/wf-design.md +1 -1
- package/modules/hosts/codex/commands/wf-new-task.md +1 -0
- package/modules/hosts/codex/commands/wf-review.md +1 -1
- package/modules/hosts/cursor/commands/wf-build.md +1 -1
- package/modules/hosts/cursor/commands/wf-design.md +1 -1
- package/modules/hosts/cursor/commands/wf-new-task.md +1 -0
- package/modules/hosts/cursor/commands/wf-review.md +1 -1
- package/modules/hosts/opencode/commands/wf-build.md +1 -1
- package/modules/hosts/opencode/commands/wf-design.md +1 -1
- package/modules/hosts/opencode/commands/wf-new-task.md +1 -0
- package/modules/hosts/opencode/commands/wf-review.md +1 -1
- package/modules/hosts/trae/commands/wf-build.md +1 -1
- package/modules/hosts/trae/commands/wf-design.md +1 -1
- package/modules/hosts/trae/commands/wf-new-task.md +1 -0
- package/modules/hosts/trae/commands/wf-review.md +1 -1
- package/package.json +1 -1
- package/src/jscpd-drift.test.mjs +3 -2
- package/templates/AGENTS.md +1 -0
- package/templates/_agents/commands/build.md +1 -1
- package/templates/_agents/commands/design.md +1 -1
- package/templates/_agents/commands/new-task.md +1 -0
- package/templates/_agents/commands/review.md +1 -1
- package/templates/_agents/engine-lock.json +32 -24
- package/templates/_agents/hooks/commit-check.cjs +57 -30
- package/templates/_agents/mechanism-registry.md +8 -0
- package/templates/_agents/notes/engine-internals.md +17 -0
- package/templates/_agents/rule-budgets.txt +10 -2
- package/templates/_agents/scripts/agg-judgments.mjs +77 -1
- package/templates/_agents/scripts/agg-judgments.test.mjs +49 -1
- package/templates/_agents/scripts/archive-sweep.mjs +3 -1
- package/templates/_agents/scripts/check-convergence.mjs +8 -3
- package/templates/_agents/scripts/check-lane-surface.mjs +9 -2
- package/templates/_agents/scripts/check-lane-surface.test.mjs +17 -3
- package/templates/_agents/scripts/check-ledger-invariant.mjs +109 -24
- package/templates/_agents/scripts/check-ledger-invariant.test.mjs +29 -1
- package/templates/_agents/scripts/check-loop-rev.test.mjs +8 -14
- package/templates/_agents/scripts/check-loop.mjs +41 -3
- package/templates/_agents/scripts/check-loop.test.mjs +37 -0
- package/templates/_agents/scripts/check-scope.test.mjs +4 -1
- package/templates/_agents/scripts/check-scripts-syntax.mjs +33 -10
- package/templates/_agents/scripts/closeout-l1.mjs +217 -0
- package/templates/_agents/scripts/closeout-l1.test.mjs +105 -0
- package/templates/_agents/scripts/commit-check-trigger.test.mjs +49 -0
- package/templates/_agents/scripts/confirm-doc.mjs +27 -8
- package/templates/_agents/scripts/confirm-doc.test.mjs +68 -9
- package/templates/_agents/scripts/gen-workflow-metrics.mjs +158 -9
- package/templates/_agents/scripts/gen-workflow-metrics.test.mjs +119 -0
- package/templates/_agents/scripts/lane-surface-audit.test.mjs +1 -1
- package/templates/_agents/scripts/ledger-shards.mjs +42 -5
- package/templates/_agents/scripts/ledger-shards.test.mjs +17 -0
- package/templates/_agents/scripts/mechanism-retire-watch.mjs +183 -0
- package/templates/_agents/scripts/mechanism-retire-watch.test.mjs +114 -0
- package/templates/_agents/scripts/pre-push.test.mjs +27 -11
- package/templates/_agents/scripts/rule-budget.mjs +14 -1
- package/templates/_agents/scripts/rule-budget.test.mjs +118 -0
- package/templates/_agents/scripts/solidify-task.mjs +3 -1
- package/templates/_agents/scripts/solidify-task.test.mjs +10 -10
- package/templates/_agents/scripts/trust-mode.test.mjs +1 -1
- package/templates/_agents/scripts/verify.mjs +237 -78
- package/templates/_agents/scripts/verify.test.mjs +44 -0
- package/templates/_agents/scripts-test-exempt.txt +2 -1
- package/templates/_agents/skills/review-method/SKILL.md +3 -0
- package/templates/_githooks/pre-commit +9 -4
- package/templates/_githooks/pre-merge-commit +4 -3
- package/templates/_githooks/pre-push +129 -75
- package/templates/workflow/README.md +2 -1
- package/templates/workflow/intents/_TEMPLATE.md +5 -0
- package/templates/workflow/specs/_TEMPLATE.md +1 -1
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// closeout-l1 — L1 关单一把编排(2026-10-15 减重手段④ 自源仓回灌:记录性动作一把生成,判断留给人)
|
|
3
|
+
// 把 test.md §6 的 L1 关单链(预检 → confirm-doc 代录 → INDEX 重生成 → 提交清单)收进一条命令。
|
|
4
|
+
// 用法:node .agents/scripts/closeout-l1.mjs <workflow/plans/xxx.md> # 预检模式:只查差距,不落任何状态
|
|
5
|
+
// node .agents/scripts/closeout-l1.mjs <plan> --delegated "<用户对话原话>" # 执行模式:预检过 → confirm-doc 代录 → 重生成 INDEX
|
|
6
|
+
// (可加 --root <仓库根>;缺省 git toplevel,无 git 用 cwd)
|
|
7
|
+
// 边界(判据单源,本工具不复制强判断):
|
|
8
|
+
// - 证据格式 / approved 历史 / 测试绿硬门 = confirm-doc(stage-gates/policy 单源),本工具预检只是
|
|
9
|
+
// 提前暴露差距的 advisory 快筛;执行模式原样转发 confirm-doc,其拒绝即本命令拒绝。
|
|
10
|
+
// - --delegated 调用纪律:AI 须先取得用户对话内明确放行(AGENTS.md 确认门条款),原话入账供对质。
|
|
11
|
+
// - L2/L3 不受理(spec 收敛对账 + 独立复核是防御道本体,不做捷径);incidents 不受理(两跳制)。
|
|
12
|
+
// kit 适配(源仓→kit 逐条):① 源仓引用 test.md §5/§5.5,kit 关单链在 test.md §6;② verify 预检
|
|
13
|
+
// 镜像 kit done 前置口径(policy v6+ 按 verifyDocSince 绑定本单绿行,不带 --doc 的绿行不给背书);
|
|
14
|
+
// ③ 分片路径修复:源仓 main() 拼 `<基名.md>.jsonl`,而 confirm-doc/ledger-loader 写入口径是
|
|
15
|
+
// `<基名>.jsonl`(basename 去 .md)——源仓提交清单会把分片误报成主台账(其测试只盖纯函数未暴露),
|
|
16
|
+
// kit 抽 shardOf 纯函数修正并补断言。
|
|
17
|
+
// 测试:node .agents/scripts/closeout-l1.test.mjs(parseArgs / fmOf / acceptanceOf / preflight / shardOf 纯函数进程内断言)
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { spawnSync } from 'node:child_process';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
import { hasFreshVerifyLine, loadKitPolicy } from './policy.mjs';
|
|
23
|
+
|
|
24
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
|
|
26
|
+
// parseArgs(argv):{ plan, delegated, root } | { error }
|
|
27
|
+
export function parseArgs(argv = []) {
|
|
28
|
+
const out = { plan: null, delegated: null, root: null, error: null };
|
|
29
|
+
for (let i = 0; i < argv.length; i++) {
|
|
30
|
+
const a = argv[i];
|
|
31
|
+
if (a === '--delegated') {
|
|
32
|
+
out.delegated = argv[++i];
|
|
33
|
+
if (!out.delegated) out.error = '--delegated 须带「<用户对话原话>」参数';
|
|
34
|
+
} else if (a.startsWith('--delegated=')) {
|
|
35
|
+
out.delegated = a.slice('--delegated='.length);
|
|
36
|
+
} else if (a === '--root') {
|
|
37
|
+
out.root = argv[++i];
|
|
38
|
+
if (!out.root) out.error = '--root 须带仓库根路径';
|
|
39
|
+
} else if (!a.startsWith('--')) {
|
|
40
|
+
if (out.plan) out.error = `只受理一份 plan(现已有 ${out.plan})`;
|
|
41
|
+
else out.plan = a;
|
|
42
|
+
} else {
|
|
43
|
+
out.error = `未知参数 ${a}`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (!out.error && !out.plan) out.error = '用法:node .agents/scripts/closeout-l1.mjs <workflow/plans/xxx.md> [--delegated "<用户对话原话>"] [--root <仓库根>]';
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// frontmatter 平铺解析(key: value;本工具只消费 状态/级别 两个键)
|
|
51
|
+
export function fmOf(text) {
|
|
52
|
+
const lines = String(text).replace(/\r\n/g, '\n').split('\n');
|
|
53
|
+
if (!/^---\s*$/.test(lines[0] || '')) return {};
|
|
54
|
+
const fm = {};
|
|
55
|
+
for (let i = 1; i < lines.length; i++) {
|
|
56
|
+
if (/^---\s*$/.test(lines[i])) break;
|
|
57
|
+
const m = /^([^\s:]+):\s*(.*)$/.exec(lines[i]);
|
|
58
|
+
if (m) fm[m[1]] = m[2].trim();
|
|
59
|
+
}
|
|
60
|
+
return fm;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// acceptanceOf(text):定位「## …验收标准…」节,统计勾选框条目。
|
|
64
|
+
// 返回 { found, total, unchecked: [条目文本] };节内无任何勾选框条目 → found=true, total=0(判为异常)。
|
|
65
|
+
export function acceptanceOf(text) {
|
|
66
|
+
const lines = String(text).replace(/\r\n/g, '\n').split('\n');
|
|
67
|
+
let start = -1;
|
|
68
|
+
for (let i = 0; i < lines.length; i++) {
|
|
69
|
+
if (/^##\s/.test(lines[i]) && lines[i].includes('验收标准')) { start = i; break; }
|
|
70
|
+
if (i > 0 && /^##\s/.test(lines[i]) && start >= 0) break; // 上一节结束仍无(不可达,防御)
|
|
71
|
+
}
|
|
72
|
+
if (start < 0) return { found: false, total: 0, unchecked: [] };
|
|
73
|
+
const unchecked = [];
|
|
74
|
+
let total = 0;
|
|
75
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
76
|
+
if (/^##\s/.test(lines[i])) break; // 节结束
|
|
77
|
+
const m = /^\s*-\s*\[([ xX])\]\s*(.*)$/.exec(lines[i]);
|
|
78
|
+
if (!m) continue;
|
|
79
|
+
total++;
|
|
80
|
+
if (m[1] === ' ') unchecked.push(m[2].trim());
|
|
81
|
+
}
|
|
82
|
+
return { found: true, total, unchecked };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// preflight({ rel, text, freshVerify }):L1 关单前置快筛(advisory;强门在 confirm-doc)。
|
|
86
|
+
// 返回 { ok, problems: [], level, status, total, checked }。
|
|
87
|
+
export function preflight({ rel, text, freshVerify }) {
|
|
88
|
+
const problems = [];
|
|
89
|
+
if (!/^workflow\/plans\/[^/]+\.md$/.test(rel) || rel.includes('_TEMPLATE')) {
|
|
90
|
+
problems.push(`路径须为 workflow/plans/ 直下文档(现「${rel}」)——L2/L3 intent/spec 与 incidents 不在本工具受理面`);
|
|
91
|
+
}
|
|
92
|
+
const fm = fmOf(text);
|
|
93
|
+
const level = fm['级别'] || '';
|
|
94
|
+
const status = fm['状态'] || '';
|
|
95
|
+
if (level !== 'L1') {
|
|
96
|
+
problems.push(`级别「${level || '缺失'}」≠ L1——L2/L3 关单走 .agents/commands/test.md 全流程(验收判据对账 + 独立复核),本工具只服务协作道 L1`);
|
|
97
|
+
}
|
|
98
|
+
if (status === 'done' || status === 'superseded' || status === 'cancelled') {
|
|
99
|
+
problems.push(`状态已为终态「${status}」——本 plan 已收口,无需重复关单`);
|
|
100
|
+
} else if (status !== 'approved') {
|
|
101
|
+
problems.push(`状态「${status || '缺失'}」≠ approved——draft 须先过起草确认(confirm-doc approved),关单只受理 approved → done`);
|
|
102
|
+
}
|
|
103
|
+
const acc = acceptanceOf(text);
|
|
104
|
+
if (!acc.found) {
|
|
105
|
+
problems.push('未找到「## 验收标准」节——L1 plan 须有可勾验的验收标准(模板口径)');
|
|
106
|
+
} else if (acc.total === 0) {
|
|
107
|
+
problems.push('「验收标准」节内无勾选框条目(- [ ] / - [x])——验收标准须逐条可勾验');
|
|
108
|
+
} else if (acc.unchecked.length) {
|
|
109
|
+
problems.push(`验收标准 ${acc.unchecked.length}/${acc.total} 条未勾验(勾验与补证据是判断动作,留在关单前完成):`);
|
|
110
|
+
for (const u of acc.unchecked) problems.push(` - [ ] ${u}`);
|
|
111
|
+
}
|
|
112
|
+
if (!freshVerify) {
|
|
113
|
+
problems.push('24h 内无本单绑定的 verify 绿行(.agents/verifications.jsonl)——先跑 node .agents/scripts/verify.mjs --doc <本单路径>(全绿自动落绿凭证与 verify-evidence 摘要;v6+ 只认带本单 --doc 的绿行)');
|
|
114
|
+
}
|
|
115
|
+
return { ok: problems.length === 0, problems, level, status, total: acc.total, checked: acc.total - acc.unchecked.length };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// shardOf(rel):该 plan 的确认台账分片路径(posix,仓库根相对)。写入口径单源 = ledger-loader
|
|
119
|
+
// writeConfirmation(basename 去 .md)——源仓首版此处拼了 `<基名.md>.jsonl`,提交清单会漏列分片。
|
|
120
|
+
export function shardOf(rel) {
|
|
121
|
+
return path.join('.agents', 'confirmations.d', `${path.basename(rel, '.md')}.jsonl`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function gitRoot() {
|
|
125
|
+
const r = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' });
|
|
126
|
+
return r.status === 0 && r.stdout.trim() ? r.stdout.trim() : process.cwd();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// freshVerifyOf(root, rel):镜像 kit confirm-doc done 前置的绿行口径(判定单源 policy.hasFreshVerifyLine):
|
|
130
|
+
// policy v5+(verifySince)才有 24h 绿行门;v6+(verifyDocSince)按本 doc 精确绑定。预检与强门同口径,
|
|
131
|
+
// 差距快筛才不失真。只读,台账缺失/不可读一律 false(advisory 层 fail-open,同 confirm-doc)。
|
|
132
|
+
function freshVerifyOf(root, rel) {
|
|
133
|
+
try {
|
|
134
|
+
const p = path.join(root, '.agents', 'verifications.jsonl');
|
|
135
|
+
if (!fs.existsSync(p)) return false;
|
|
136
|
+
const pol = loadKitPolicy(root);
|
|
137
|
+
if (typeof pol.verifySince !== 'string') return true; // v1-v4 无绿行门 → 预检不产差距
|
|
138
|
+
const bound = typeof pol.verifyDocSince === 'string' ? rel : null;
|
|
139
|
+
return hasFreshVerifyLine(fs.readFileSync(p, 'utf8').split(/\r?\n/), Date.now(), bound);
|
|
140
|
+
} catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function findArchived(root, rel) {
|
|
146
|
+
const base = path.basename(rel);
|
|
147
|
+
for (const monthDir of fs.existsSync(path.join(root, 'workflow', 'archive'))
|
|
148
|
+
? fs.readdirSync(path.join(root, 'workflow', 'archive')) : []) {
|
|
149
|
+
const cand = path.join('workflow', 'archive', monthDir, 'plans', base);
|
|
150
|
+
if (fs.existsSync(path.join(root, cand))) return cand;
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function main() {
|
|
156
|
+
const args = parseArgs(process.argv.slice(2));
|
|
157
|
+
if (args.error) {
|
|
158
|
+
console.error(`closeout-l1: ${args.error}`);
|
|
159
|
+
process.exit(2);
|
|
160
|
+
}
|
|
161
|
+
const root = path.resolve(args.root || gitRoot());
|
|
162
|
+
const rel = path.relative(root, path.resolve(args.plan)).replace(/\\/g, '/');
|
|
163
|
+
const abs = path.join(root, rel);
|
|
164
|
+
if (!fs.existsSync(abs)) {
|
|
165
|
+
console.error(`closeout-l1: 文件不存在 ${rel}(相对仓库根 ${root})`);
|
|
166
|
+
process.exit(2);
|
|
167
|
+
}
|
|
168
|
+
const text = fs.readFileSync(abs, 'utf8');
|
|
169
|
+
const pf = preflight({ rel, text, freshVerify: freshVerifyOf(root, rel) });
|
|
170
|
+
|
|
171
|
+
console.log(`L1 关单预检|${rel}`);
|
|
172
|
+
console.log(` 级别 ${pf.level || '—'}|状态 ${pf.status || '—'}|验收 ${pf.checked}/${pf.total || 0} 勾验|verify 绿行 ${pf.problems.some((p) => p.includes('verify')) ? '✗' : '✓(24h)'}`);
|
|
173
|
+
if (!pf.ok) {
|
|
174
|
+
console.error(`\n预检未过(${pf.problems.length} 项差距,逐条处理后再来):`);
|
|
175
|
+
for (const p of pf.problems) console.error(` ✗ ${p}`);
|
|
176
|
+
process.exit(2);
|
|
177
|
+
}
|
|
178
|
+
console.log(' 预检通过。');
|
|
179
|
+
|
|
180
|
+
if (!args.delegated) {
|
|
181
|
+
console.log('\n下一步(执行关单,须先取得用户对话内明确放行):');
|
|
182
|
+
console.log(` node .agents/scripts/closeout-l1.mjs ${rel} --delegated "<用户确认原话>"`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 执行模式:confirm-doc 原样转发(全部强门在彼处),随后重生成 INDEX。
|
|
187
|
+
const cd = spawnSync(process.execPath, [
|
|
188
|
+
path.join(SCRIPT_DIR, 'confirm-doc.mjs'), rel, '--delegated', args.delegated, '--root', root,
|
|
189
|
+
], { stdio: 'inherit' });
|
|
190
|
+
if (cd.status !== 0) {
|
|
191
|
+
console.error(`closeout-l1: confirm-doc 拒绝(exit ${cd.status})——按上方提示修复后重试;本命令不做任何绕过。`);
|
|
192
|
+
process.exit(cd.status === null ? 2 : cd.status);
|
|
193
|
+
}
|
|
194
|
+
const gi = spawnSync(process.execPath, [path.join(SCRIPT_DIR, 'gen-workflow-index.mjs')], { cwd: root, encoding: 'utf8' });
|
|
195
|
+
if (gi.status !== 0) {
|
|
196
|
+
console.error('closeout-l1: ⚠️ INDEX 重生成失败(确认已落账不可回滚;提交前须手动跑 node .agents/scripts/gen-workflow-index.mjs 修齐,否则索引漂移门会拦提交)');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 汇总:关单后的落盘面与提交清单(归档 mv 时给出新路径)。
|
|
200
|
+
const base = path.basename(rel);
|
|
201
|
+
const archivedTo = fs.existsSync(abs) ? null : findArchived(root, rel);
|
|
202
|
+
const shard = shardOf(rel);
|
|
203
|
+
const adds = [
|
|
204
|
+
archivedTo || rel,
|
|
205
|
+
fs.existsSync(path.join(root, shard)) ? shard : '.agents/confirmations.jsonl',
|
|
206
|
+
'workflow/INDEX.md',
|
|
207
|
+
];
|
|
208
|
+
console.log(`\n关单完成:approved → done(台账已记 chat-delegated,原话入账供对质)`);
|
|
209
|
+
if (archivedTo) console.log(`📦 已随确认滚动归档:${rel} → ${archivedTo}(家族原子,git mv 已暂存)`);
|
|
210
|
+
console.log(`提交清单(git add):
|
|
211
|
+
${adds.join('\n ')}`);
|
|
212
|
+
console.log(`建议提交说明:docs(workflow): 关单 ${base.replace(/\.md$/, '')}(L1 done)——用户对话内确认代录${archivedTo ? `,归档至 ${path.dirname(archivedTo)}/` : ''}`);
|
|
213
|
+
console.log('提醒:本会话自做任务另记 workflow/delegations.md 并跑 agg-delegations.mjs(test.md §6);verify-evidence.md 有改动时随同提交。');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const isMain = process.argv[1] && process.argv[1].endsWith('closeout-l1.mjs');
|
|
217
|
+
if (isMain) main();
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// closeout-l1.test.mjs — L1 关单编排纯函数套件(2026-10-15 减重手段④ 自源仓回灌随附)
|
|
3
|
+
// parseArgs / fmOf / acceptanceOf / preflight / shardOf 进程内断言:快筛差距逐项暴露、
|
|
4
|
+
// L2/L3 与终态件不受理、勾验缺一即拦、verify 凭证缺失即拦、分片路径与写入口径同形。
|
|
5
|
+
// 运行:node templates/_agents/scripts/closeout-l1.test.mjs
|
|
6
|
+
import { parseArgs, fmOf, acceptanceOf, preflight, shardOf } from './closeout-l1.mjs';
|
|
7
|
+
|
|
8
|
+
let pass = 0;
|
|
9
|
+
let fail = 0;
|
|
10
|
+
const check = (desc, cond, detail = '') => {
|
|
11
|
+
if (cond) { pass++; console.log(`PASS ${desc}`); }
|
|
12
|
+
else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const PLAN_OK = `---
|
|
16
|
+
状态: approved
|
|
17
|
+
级别: L1
|
|
18
|
+
模块: purchase
|
|
19
|
+
---
|
|
20
|
+
# PLAN — demo
|
|
21
|
+
|
|
22
|
+
## 改动方案
|
|
23
|
+
|
|
24
|
+
- 改 A
|
|
25
|
+
|
|
26
|
+
## 验收标准(可测试)
|
|
27
|
+
|
|
28
|
+
- [x] 判据一(证据:UI 实测 A1 PASS)
|
|
29
|
+
- [x] 判据二(证据:UT 用例名 demo_case)
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
// ---- A1 parseArgs ----
|
|
33
|
+
{
|
|
34
|
+
const a = parseArgs(['workflow/plans/x.md', '--delegated', '可以关单', '--root', 'E:/r']);
|
|
35
|
+
check('A1 三参齐解析', a.plan === 'workflow/plans/x.md' && a.delegated === '可以关单' && a.root === 'E:/r' && !a.error, JSON.stringify(a));
|
|
36
|
+
check('A1 缺 plan → error', !!parseArgs(['--delegated', 'q']).error);
|
|
37
|
+
check('A1 --delegated 缺值 → error', !!parseArgs(['workflow/plans/x.md', '--delegated']).error);
|
|
38
|
+
check('A1 未知参数 → error', !!parseArgs(['workflow/plans/x.md', '--wow']).error);
|
|
39
|
+
check('A1 两份 plan → error', !!parseArgs(['a.md', 'b.md']).error);
|
|
40
|
+
check('A1 --delegated= 等号形态', parseArgs(['x.md', '--delegated=原话']).delegated === '原话');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---- A2 fmOf / acceptanceOf ----
|
|
44
|
+
{
|
|
45
|
+
const fm = fmOf(PLAN_OK);
|
|
46
|
+
check('A2 frontmatter 状态/级别', fm['状态'] === 'approved' && fm['级别'] === 'L1' && fm['模块'] === 'purchase', JSON.stringify(fm));
|
|
47
|
+
const acc = acceptanceOf(PLAN_OK);
|
|
48
|
+
check('A2 验收节 2 条全勾', acc.found === true && acc.total === 2 && acc.unchecked.length === 0, JSON.stringify(acc));
|
|
49
|
+
check('A2 无验收节 → found=false', acceptanceOf('# 无节文档\n\n- [ ] 孤儿勾选不算').found === false);
|
|
50
|
+
const mixed = acceptanceOf('## 验收标准\n\n- [x] 甲\n- [ ] 乙待办\n- [X] 丙大写勾\n\n## 下一节\n\n- [ ] 节外不算\n');
|
|
51
|
+
check('A2 未勾只数节内(乙),大写 X 认勾、节外不计', mixed.total === 3 && mixed.unchecked.length === 1 && mixed.unchecked[0] === '乙待办', JSON.stringify(mixed));
|
|
52
|
+
check('A2 节在无勾选框条目 → total=0', acceptanceOf('## 验收标准\n\n纯文字段\n').total === 0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---- A3 preflight 快乐路径 ----
|
|
56
|
+
{
|
|
57
|
+
const pf = preflight({ rel: 'workflow/plans/2026-10-15-demo.md', text: PLAN_OK, freshVerify: true });
|
|
58
|
+
check('A3 L1+approved+全勾+有凭证 → ok', pf.ok === true && pf.problems.length === 0 && pf.checked === 2, JSON.stringify(pf));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---- A4 级别/状态不受理 ----
|
|
62
|
+
{
|
|
63
|
+
const l2 = preflight({ rel: 'workflow/plans/x.md', text: PLAN_OK.replace('级别: L1', '级别: L2'), freshVerify: true });
|
|
64
|
+
check('A4 L2 → 拒并指回 test.md', !l2.ok && l2.problems.some((p) => p.includes('L2/L3') && p.includes('test.md')), JSON.stringify(l2.problems));
|
|
65
|
+
const draft = preflight({ rel: 'workflow/plans/x.md', text: PLAN_OK.replace('状态: approved', '状态: draft'), freshVerify: true });
|
|
66
|
+
check('A4 draft → 拒(须先过起草确认)', !draft.ok && draft.problems.some((p) => p.includes('draft')), JSON.stringify(draft.problems));
|
|
67
|
+
const done = preflight({ rel: 'workflow/plans/x.md', text: PLAN_OK.replace('状态: approved', '状态: done'), freshVerify: true });
|
|
68
|
+
check('A4 done → 拒(已收口)', !done.ok && done.problems.some((p) => p.includes('终态')), JSON.stringify(done.problems));
|
|
69
|
+
check('A4 级别缺失也拒(fail-closed)', !preflight({ rel: 'workflow/plans/x.md', text: PLAN_OK.replace('级别: L1\n', ''), freshVerify: true }).ok);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---- A5 路径受理面 ----
|
|
73
|
+
{
|
|
74
|
+
check('A5 intents 路径不受理', !preflight({ rel: 'workflow/intents/x.md', text: PLAN_OK, freshVerify: true }).ok);
|
|
75
|
+
check('A5 incidents 路径不受理', !preflight({ rel: 'workflow/incidents/x.md', text: PLAN_OK, freshVerify: true }).ok);
|
|
76
|
+
check('A5 模板不受理', !preflight({ rel: 'workflow/plans/_TEMPLATE.md', text: PLAN_OK, freshVerify: true }).ok);
|
|
77
|
+
check('A5 子目录路径不受理', !preflight({ rel: 'workflow/plans/sub/x.md', text: PLAN_OK, freshVerify: true }).ok);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---- A6 勾验与凭证差距 ----
|
|
81
|
+
{
|
|
82
|
+
const unchecked = preflight({
|
|
83
|
+
rel: 'workflow/plans/x.md',
|
|
84
|
+
text: PLAN_OK.replace('- [x] 判据二(证据:UT 用例名 demo_case)', '- [ ] 判据二'),
|
|
85
|
+
freshVerify: true,
|
|
86
|
+
});
|
|
87
|
+
check('A6 未勾验逐条列出(计数行 + 条目行分项)', !unchecked.ok
|
|
88
|
+
&& unchecked.problems.some((p) => p.includes('1/2 条未勾验'))
|
|
89
|
+
&& unchecked.problems.some((p) => p.includes('判据二')), JSON.stringify(unchecked.problems));
|
|
90
|
+
const noVerify = preflight({ rel: 'workflow/plans/x.md', text: PLAN_OK, freshVerify: false });
|
|
91
|
+
check('A6 verify 凭证缺失 → 拦并指 verify.mjs --doc', !noVerify.ok && noVerify.problems.some((p) => p.includes('verify.mjs') && p.includes('--doc')), JSON.stringify(noVerify.problems));
|
|
92
|
+
const noAcc = preflight({ rel: 'workflow/plans/x.md', text: PLAN_OK.split('## 验收标准')[0], freshVerify: true });
|
|
93
|
+
check('A6 无验收节 → 拦', !noAcc.ok && noAcc.problems.some((p) => p.includes('验收标准')));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---- A7 shardOf:与 ledger-loader writeConfirmation 写入口径同形(<基名去.md>.jsonl)----
|
|
97
|
+
// 源仓首版 main() 拼 `<基名.md>.jsonl` 与写入口径错位——提交清单会把分片误报成主台账。本断言钉死修正。
|
|
98
|
+
{
|
|
99
|
+
const s = shardOf('workflow/plans/2026-10-15-demo.md');
|
|
100
|
+
check('A7 分片路径 = confirmations.d/<基名>.jsonl(去 .md)', s.replace(/\\/g, '/') === '.agents/confirmations.d/2026-10-15-demo.jsonl', s);
|
|
101
|
+
check('A7 分片路径不含 .md.jsonl 双后缀', !s.replace(/\\/g, '/').includes('.md.jsonl'), s);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
105
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -149,6 +149,55 @@ for (const [label, writePkg] of unsatisfied) {
|
|
|
149
149
|
r.status === 0 && !ran(f.marks, 'build') && r.out.includes('pkg:scripts.build') && !r.out.includes('无匹配的源码变更'), r.out);
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
// 场景 10:--no-build 跳过全部构建(2026-10-16 构建门后移 pre-push)——ext 命中也不跑
|
|
153
|
+
f = mkfix();
|
|
154
|
+
stage(f.root, 'src/a.xyz', 'x');
|
|
155
|
+
r = runHook(f.root, ['--no-build']);
|
|
156
|
+
check('S10 --no-build 命中 ext 也不构建(后移 pre-push)',
|
|
157
|
+
r.status === 0 && !ran(f.marks, 'extbuild') && r.out.includes('--no-build'), r.out);
|
|
158
|
+
|
|
159
|
+
// 场景 11:--range 推送范围模式——范围内 .xyz 触发 extbuild;敏感扫描跳过有提示
|
|
160
|
+
f = mkfix();
|
|
161
|
+
fs.writeFileSync(path.join(f.root, 'old.txt'), 'old\n');
|
|
162
|
+
git(f.root, ['add', 'old.txt']);
|
|
163
|
+
git(f.root, ['commit', '-q', '-m', 'old']);
|
|
164
|
+
stage(f.root, 'src/ranged.xyz', 'x');
|
|
165
|
+
const shaOld = git(f.root, ['rev-parse', 'HEAD']).stdout.trim();
|
|
166
|
+
git(f.root, ['commit', '-q', '-m', 'ranged']);
|
|
167
|
+
const shaNew = git(f.root, ['rev-parse', 'HEAD']).stdout.trim();
|
|
168
|
+
r = runHook(f.root, ['--range', shaOld, shaNew]);
|
|
169
|
+
check('S11 --range 范围内 .xyz 触发 extbuild 且提示跳过敏感扫描',
|
|
170
|
+
r.status === 0 && ran(f.marks, 'extbuild') && r.out.includes('SKIP: --range 模式跳过敏感扫描'), r.out);
|
|
171
|
+
|
|
172
|
+
// 场景 11b:--range 范围只含非触发文件 → 零构建
|
|
173
|
+
f = mkfix();
|
|
174
|
+
fs.writeFileSync(path.join(f.root, 'a.txt'), 'a\n');
|
|
175
|
+
git(f.root, ['add', 'a.txt']);
|
|
176
|
+
git(f.root, ['commit', '-q', '-m', 'a']);
|
|
177
|
+
const s1 = git(f.root, ['rev-parse', 'HEAD']).stdout.trim();
|
|
178
|
+
fs.writeFileSync(path.join(f.root, 'b.txt'), 'b\n');
|
|
179
|
+
git(f.root, ['add', 'b.txt']);
|
|
180
|
+
git(f.root, ['commit', '-q', '-m', 'b']);
|
|
181
|
+
const s2 = git(f.root, ['rev-parse', 'HEAD']).stdout.trim();
|
|
182
|
+
r = runHook(f.root, ['--range', s1, s2]);
|
|
183
|
+
check('S11b --range 范围无触发文件 → 零构建',
|
|
184
|
+
r.status === 0 && !ran(f.marks, 'extbuild') && !ran(f.marks, 'prefixbuild'), r.out);
|
|
185
|
+
|
|
186
|
+
// 场景 12:--range 全零 old sha(新分支首推)→ 全量构建语义(等价 --full 构建面)
|
|
187
|
+
f = mkfix();
|
|
188
|
+
fs.writeFileSync(path.join(f.root, 'seed.txt'), 'seed\n');
|
|
189
|
+
git(f.root, ['add', 'seed.txt']);
|
|
190
|
+
git(f.root, ['commit', '-q', '-m', 'seed']);
|
|
191
|
+
const shaHead = git(f.root, ['rev-parse', 'HEAD']).stdout.trim();
|
|
192
|
+
r = runHook(f.root, ['--range', '0'.repeat(40), shaHead]);
|
|
193
|
+
check('S12 --range 全零 old = 全量构建语义',
|
|
194
|
+
r.status === 0 && ran(f.marks, 'extbuild') && ran(f.marks, 'prefixbuild'), r.out);
|
|
195
|
+
|
|
196
|
+
// 场景 13:--range sha 非法 → BLOCK(信任边界:sha 来自 git 管道仍校验)
|
|
197
|
+
f = mkfix();
|
|
198
|
+
r = runHook(f.root, ['--range', 'not-a-sha', 'HEAD']);
|
|
199
|
+
check('S13 --range 非 sha 参数 → BLOCK exit 1', r.status === 1 && r.out.includes('BLOCK'), r.out);
|
|
200
|
+
|
|
152
201
|
for (const d of fs.readdirSync(os.tmpdir()).filter((x) => x.startsWith('cc-trigger-test-'))) {
|
|
153
202
|
fs.rmSync(path.join(os.tmpdir(), d), { recursive: true, force: true });
|
|
154
203
|
}
|
|
@@ -406,12 +406,19 @@ if (isMain) {
|
|
|
406
406
|
refused++;
|
|
407
407
|
continue;
|
|
408
408
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
409
|
+
}
|
|
410
|
+
// 待澄清项门(2026-10-12 needs-clarification;2026-10-18 立项跳对称化扩到 intents):起草期挂
|
|
411
|
+
// 「待澄清」标记**不阻断**(零阻塞),但 draft→approved 确认时若仍挂着未澄清项则拒绝——确认即
|
|
412
|
+
// 「认可这份文档」,文档里还写着「这里我不知道」,确认就没有意义。intents 原不挂本门(旧判例
|
|
413
|
+
// 「澄清天然发生在需求单处」,S19c 旧断言),2026-10-18 推翻:intent 是后链全部文档的种子,
|
|
414
|
+
// 立项时被含混带过的不确定项到 spec/plan 跳已无人能补标——与 specs/plans 同口径挂门。判据与
|
|
415
|
+
// 文档类型无关(clarifyGateFor 只扫正文标记行),存量 intent 无标记零回溯;incidents 生命周期
|
|
416
|
+
// 无 approved 态,不在本门面。与上面的前置门分开出账:拒绝原因必须指向真正的原因(合并会让
|
|
417
|
+
// 作者去查「前置」,而前置其实是好的;前置门只适用于 specs/plans——入口文档无「前置」概念)。
|
|
418
|
+
// ⚠️ 文案锚点硬约束:本段禁止并入上方 gate 分支的输出契约(既有 confirm-doc.test.mjs 断言
|
|
419
|
+
// 锚定「未过逐阶段前置门(stage-gates)」子串),故单列独立分支。
|
|
420
|
+
if (target === 'approved' && (doc.startsWith('workflow/specs/') || doc.startsWith('workflow/plans/')
|
|
421
|
+
|| doc.startsWith('workflow/intents/'))) {
|
|
415
422
|
const cl = clarifyGateFor(abs);
|
|
416
423
|
if (!cl.ok) {
|
|
417
424
|
console.error(`❌ ${doc} 含未澄清项(needs-clarification):${cl.reason}`);
|
|
@@ -541,9 +548,21 @@ if (isMain) {
|
|
|
541
548
|
// 内容绑定约束,与既有归档批次同口径)。失败一律 fail-open:归档是卫生动作,不得回滚已完成的
|
|
542
549
|
// 确认(**归档失败不阻断关单**——与源仓语义逐字一致)。启用锚(照搬源仓,无 policy 锚):
|
|
543
550
|
// ① 仓库已建立归档约定(workflow/archive/ 存在)——真实装户仓有,临时 fixture 无,避免测试夹具
|
|
544
|
-
// 被静默搬走(也避免在未采纳归档的仓库里凭空造目录);② env 逃生门 CONFIRM_DOC_NO_ARCHIVE
|
|
551
|
+
// 被静默搬走(也避免在未采纳归档的仓库里凭空造目录);② env 逃生门 CONFIRM_DOC_NO_ARCHIVE;
|
|
552
|
+
// ③ 特性分支默认不归档(2026-10-16 自源仓 13f67ef7 回灌):分支上先关单再合并时,diff 只剩
|
|
553
|
+
// archive/ 路径,泳道/配对增量门只认 workflow 活跃直路径,合法合并被拦。main/master/develop 与
|
|
554
|
+
// 游离 HEAD 维持关单即归档。CONFIRM_DOC_NO_ARCHIVE=1 仍强制关;CONFIRM_DOC_ARCHIVE=1 可在特性分支强制归档。
|
|
545
555
|
const archiveRoot = path.join(root, 'workflow', 'archive');
|
|
546
|
-
|
|
556
|
+
let featureHold = false;
|
|
557
|
+
if (confirmed > 0 && !process.env.CONFIRM_DOC_NO_ARCHIVE && process.env.CONFIRM_DOC_ARCHIVE !== '1') {
|
|
558
|
+
let branch = '';
|
|
559
|
+
try { branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: root, encoding: 'utf8' }).trim(); } catch { branch = ''; }
|
|
560
|
+
featureHold = !!branch && branch !== 'HEAD' && !['main', 'master', 'develop'].includes(branch);
|
|
561
|
+
if (featureHold && fs.existsSync(archiveRoot)) {
|
|
562
|
+
console.log(`📦 特性分支 ${branch} 不归档(合并前保持 workflow 活跃路径;main/develop 关单才归档)。CONFIRM_DOC_ARCHIVE=1 可强制`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
if (confirmed > 0 && !process.env.CONFIRM_DOC_NO_ARCHIVE && !featureHold && fs.existsSync(archiveRoot)) {
|
|
547
566
|
try {
|
|
548
567
|
const { analyze, buildDocList, loadLastTouch, parseStatus, TERMINAL, ACTIVE } = await import('./archive-sweep.mjs');
|
|
549
568
|
// 台账终态锚:kit 读侧单源 = readLedgerRows(主台账+分片透明合并;源仓走
|
|
@@ -436,15 +436,31 @@ const readLedgerOf = (root) => {
|
|
|
436
436
|
JSON.stringify({ status: r.status, stderr: r.stderr }));
|
|
437
437
|
fs.rmSync(root, { recursive: true, force: true });
|
|
438
438
|
}
|
|
439
|
-
// intents
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
439
|
+
// intents 挂门(2026-10-18 立项跳对称化,推翻旧判例「澄清天然发生在需求单处」——intent 是后链
|
|
440
|
+
// 全部文档的种子,立项时被含混带过的不确定项到 spec/plan 跳已无人能补标;存量无标记零回溯)
|
|
441
|
+
{
|
|
442
|
+
const root3 = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-cl3-'));
|
|
443
|
+
fs.mkdirSync(path.join(root3, 'workflow', 'intents'), { recursive: true });
|
|
444
|
+
const ip = path.join(root3, 'workflow', 'intents', '2026-10-12-p.md');
|
|
445
|
+
fs.writeFileSync(ip, '---\n状态: draft\n级别: L1\n---\n# I\n\n- 待澄清:范围含不含移动端?\n');
|
|
446
|
+
const r3 = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-10-12-p.md', '--delegated', '可以'], { cwd: root3, encoding: 'utf8' });
|
|
447
|
+
check('S19c 待澄清门:intent 挂未澄清项 → exit 2 + 点名内容 + 未落盘(立项跳对称化)',
|
|
448
|
+
r3.status === 2 && /needs-clarification/.test(r3.stderr) && /移动端/.test(r3.stderr)
|
|
449
|
+
&& !/逐阶段前置门/.test(r3.stderr)
|
|
450
|
+
&& fs.readFileSync(ip, 'utf8').includes('状态: draft'),
|
|
451
|
+
JSON.stringify({ status: r3.status, stderr: r3.stderr }));
|
|
452
|
+
fs.writeFileSync(ip, '---\n状态: draft\n级别: L1\n---\n# I\n\n- 已澄清:范围仅桌面端(用户 10-12 拍板)\n');
|
|
453
|
+
const r4 = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-10-12-p.md', '--delegated', '可以'], { cwd: root3, encoding: 'utf8' });
|
|
454
|
+
check('S19c 待澄清门:intent 已澄清 → 放行落态',
|
|
455
|
+
r4.status === 0 && fs.readFileSync(ip, 'utf8').includes('状态: approved'),
|
|
456
|
+
JSON.stringify({ status: r4.status, stderr: r4.stderr }));
|
|
457
|
+
fs.writeFileSync(ip, '---\n状态: draft\n级别: L1\n---\n# I\n\n## 目标\n- 无标记存量件\n');
|
|
458
|
+
const r5 = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-10-12-p.md', '--delegated', '可以'], { cwd: root3, encoding: 'utf8' });
|
|
459
|
+
check('S19c 待澄清门:intent 无标记(存量口径)→ 放行零回溯',
|
|
460
|
+
r5.status === 0 && fs.readFileSync(ip, 'utf8').includes('状态: approved'),
|
|
461
|
+
JSON.stringify({ status: r5.status, stderr: r5.stderr }));
|
|
462
|
+
fs.rmSync(root3, { recursive: true, force: true });
|
|
463
|
+
}
|
|
448
464
|
}
|
|
449
465
|
|
|
450
466
|
// ---- S20【逐阶段门】入口存在但未确认(draft)→ 拒绝 ----
|
|
@@ -1056,5 +1072,48 @@ const writeTrust = (root, cfg) => {
|
|
|
1056
1072
|
fs.rmSync(root2, { recursive: true, force: true });
|
|
1057
1073
|
}
|
|
1058
1074
|
|
|
1075
|
+
// ---- S45c/S45d 特性分支默认不归档(2026-10-16 自源仓 13f67ef7 回灌)----
|
|
1076
|
+
// 分支上先关单再合并时 diff 只剩 archive/ 路径,泳道/配对增量门只认 workflow 活跃直路径;
|
|
1077
|
+
// main/master/develop 与游离 HEAD 维持关单即归档。CONFIRM_DOC_ARCHIVE=1 可在特性分支强制归档。
|
|
1078
|
+
{
|
|
1079
|
+
const mkRepoBranch = (branchName) => {
|
|
1080
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-branch-'));
|
|
1081
|
+
const git = (...a) => spawnSync(process.platform === 'win32' ? 'git.exe' : 'git', a, { cwd: root, encoding: 'utf8' });
|
|
1082
|
+
git('init', '-q');
|
|
1083
|
+
git('config', 'user.email', 't@t');
|
|
1084
|
+
git('config', 'user.name', 't');
|
|
1085
|
+
git('checkout', '-q', '-b', branchName);
|
|
1086
|
+
const ip = path.join(root, 'workflow', 'intents');
|
|
1087
|
+
fs.mkdirSync(ip, { recursive: true });
|
|
1088
|
+
fs.mkdirSync(path.join(root, 'workflow', 'archive'), { recursive: true });
|
|
1089
|
+
const fp = branchName === 'main' ? 'eeee5555eeee5555' : 'ffff6666ffff6666';
|
|
1090
|
+
fs.writeFileSync(path.join(ip, '2026-09-01-branch.md'), `---\n状态: approved\n级别: L1\n确认指纹: ${fp}\n---\n# I\n\n## 验收标准(可测试)\n- [x] 通过(证据:测试全绿)\n`, 'utf8');
|
|
1091
|
+
git('add', '-A');
|
|
1092
|
+
spawnSync(process.platform === 'win32' ? 'git.exe' : 'git', ['commit', '-q', '-m', 'c1'], { cwd: root, env: { ...process.env, GIT_AUTHOR_DATE: '2026-09-01T10:00:00+08:00', GIT_COMMITTER_DATE: '2026-09-01T10:00:00+08:00' } });
|
|
1093
|
+
return { root, ip };
|
|
1094
|
+
};
|
|
1095
|
+
// S45c:特性分支关单 → 不归档(保持 workflow 活跃直路径),但状态照常 done
|
|
1096
|
+
{
|
|
1097
|
+
const { root, ip } = mkRepoBranch('feature/x');
|
|
1098
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-01-branch.md', '--delegated', '可以'],
|
|
1099
|
+
{ cwd: root, encoding: 'utf8', env: { ...process.env, CONFIRM_DOC_TEST_NOW: '2026-10-13' } });
|
|
1100
|
+
check('S45c 特性分支关单默认不归档(状态置 done、文档留活跃路径)',
|
|
1101
|
+
r.status === 0 && /状态: done/.test(fs.readFileSync(path.join(ip, '2026-09-01-branch.md'), 'utf8'))
|
|
1102
|
+
&& !fs.existsSync(path.join(root, 'workflow', 'archive', 'intents')) && /不归档/.test(r.stdout),
|
|
1103
|
+
JSON.stringify({ status: r.status, stdout: String(r.stdout).slice(-260) }));
|
|
1104
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
1105
|
+
}
|
|
1106
|
+
// S45d:特性分支 + CONFIRM_DOC_ARCHIVE=1 → 强制归档
|
|
1107
|
+
{
|
|
1108
|
+
const { root } = mkRepoBranch('feature/y');
|
|
1109
|
+
const r = spawnSync(process.execPath, [CLI, 'workflow/intents/2026-09-01-branch.md', '--delegated', '可以'],
|
|
1110
|
+
{ cwd: root, encoding: 'utf8', env: { ...process.env, CONFIRM_DOC_TEST_NOW: '2026-10-13', CONFIRM_DOC_ARCHIVE: '1' } });
|
|
1111
|
+
check('S45d 特性分支 CONFIRM_DOC_ARCHIVE=1 强制归档',
|
|
1112
|
+
r.status === 0 && /📦 归档/.test(r.stdout),
|
|
1113
|
+
JSON.stringify({ status: r.status, stdout: String(r.stdout).slice(-260) }));
|
|
1114
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1059
1118
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
1060
1119
|
process.exit(fail ? 1 : 0);
|