agentic-flow-kit 1.2.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/fresh-init.test.mjs +10 -0
- package/src/init.mjs +1 -1
- package/src/profiles.mjs +12 -6
- package/src/sync.mjs +30 -26
- package/src/sync.test.mjs +82 -0
- package/templates/AGENTS.md +1 -0
- package/templates/_agents/rule-budgets.txt +3 -1
- package/templates/_agents/scripts/agg-delegations.cjs +15 -5
- package/templates/_agents/scripts/check-ledger.mjs +60 -16
- package/templates/_agents/scripts/check-ledger.test.mjs +70 -0
- package/templates/_agents/scripts/check-loop-unit.test.mjs +95 -0
- package/templates/_agents/scripts/check-loop.mjs +1134 -1084
- package/templates/_agents/scripts/check-loop.test.mjs +86 -1
- package/templates/_agents/scripts/gen-workflow-dashboard.mjs +182 -0
- package/templates/_agents/scripts/gen-workflow-dashboard.test.mjs +126 -0
- package/templates/_github/workflows/kit-ci.yml +28 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-flow-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "AI-Native 闭环工作流 + wiki 知识层脚手架——flow-kit init 一条命令为新项目装好 .agents 工作流引擎(阶段命令/角色契约/门禁钩子/kb 检索/看板/规则面预算)+ workflow 文档闭环 + wiki 知识库,装完自包含、零运行时依赖",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
package/src/fresh-init.test.mjs
CHANGED
|
@@ -42,6 +42,16 @@ check('新装 audit 为 false 且 policyVersion 为 2', kit.audit === false && k
|
|
|
42
42
|
check('新装无包源模板对应条目无锚(生成类配置不参与感知)', cfg !== undefined && cfg.srcSha256 === undefined);
|
|
43
43
|
const rb = tplAnchor('.agents/rule-budgets.txt');
|
|
44
44
|
check('新装 _agents 前缀翻译参与感知(rule-budgets 带锚)', rb !== undefined && typeof rb.srcSha256 === 'string');
|
|
45
|
+
// CI 远端门模板(2026-10-07 adopter-ci-github):.github → _github 通用点前缀翻译 + owned 落盘带锚
|
|
46
|
+
const ci = tplAnchor('.github/workflows/kit-ci.yml');
|
|
47
|
+
const ciSrc = fs.readFileSync(path.join(ROOT, 'templates', '_github', 'workflows', 'kit-ci.yml'), 'utf8').replace(/\r\n/g, '\n');
|
|
48
|
+
check('新装 CI 远端门模板落盘且 kit.json owned 带锚(srcSha256 == 包源 LF 归一 sha)',
|
|
49
|
+
fs.existsSync(path.join(dir, '.github', 'workflows', 'kit-ci.yml'))
|
|
50
|
+
&& ci !== undefined && ci.srcSha256 === shaOf(Buffer.from(ciSrc, 'utf8')),
|
|
51
|
+
JSON.stringify(ci || null));
|
|
52
|
+
const ciText = fs.readFileSync(path.join(dir, '.github', 'workflows', 'kit-ci.yml'), 'utf8');
|
|
53
|
+
check('新装 CI 模板引用统一入口 verify.mjs、含分支策略人工边界提示且无 Tab 缩进',
|
|
54
|
+
ciText.includes('node .agents/scripts/verify.mjs') && ciText.includes('branch protection') && !ciText.includes('\t'));
|
|
45
55
|
}
|
|
46
56
|
check('claude 命令与角色薄适配落盘',
|
|
47
57
|
fs.existsSync(path.join(dir, '.claude', 'commands', 'wf-plan.md'))
|
package/src/init.mjs
CHANGED
|
@@ -97,7 +97,7 @@ async function interactive() {
|
|
|
97
97
|
const menu = (names) => ' ' + names.map((n, i) => dim(`${menuMark(i)} ${stackLabel(n)}`)).join(dim(' '));
|
|
98
98
|
|
|
99
99
|
console.log(bold(cyan('▶ flow-kit init')) + dim(' · AI 闭环工作流 + wiki 知识层脚手架'));
|
|
100
|
-
console.log(dim(' 安装到当前目录:.agents/ · .githooks/ · workflow/ · wiki/ · AGENTS.md
|
|
100
|
+
console.log(dim(' 安装到当前目录:.agents/ · .githooks/ · workflow/ · wiki/ · AGENTS.md · .github/workflows/kit-ci.yml(CI 远端门,非 GitHub 装户可删)(已存在的文件保守跳过;AGENTS.md 无工作流骨架时文末追加补齐)'));
|
|
101
101
|
try {
|
|
102
102
|
let hosts;
|
|
103
103
|
for (;;) {
|
package/src/profiles.mjs
CHANGED
|
@@ -106,13 +106,17 @@ export function pickStackVars(stackKey) {
|
|
|
106
106
|
// Node 18.20 / 20.18 / 22.11 全部 ERR_REQUIRE_ESM(只有 ≥20.19/≥22.12 可用),而本仓 engines
|
|
107
107
|
// 写 >=18.0.0、CI 跑 18/22 → 装户在 CI 目标版本上根本用不了。改 .cjs 后 createRequire 同步载入
|
|
108
108
|
// 全线可用。旧 .mjs 路径同样登记为 owned(免装户残留旧文件时落 managed 重演缺陷)。
|
|
109
|
+
// .github/ 是装户 CI 远端门(2026-10-07 adopter-ci-github):kit 只下发 workflows/kit-ci.yml 薄模板,
|
|
110
|
+
// 触发分支 / node 版本 / 测试命令均项目特定——归 owned,sync 永不覆盖;感知锚经 srcTemplatePath
|
|
111
|
+
// 通用点前缀翻译命中 templates/_github/。
|
|
109
112
|
export function isOwned(rel) {
|
|
110
113
|
return rel === 'AGENTS.md' || rel === '.gitattributes' || rel.startsWith('workflow/') || rel.startsWith('wiki/')
|
|
111
114
|
|| rel.startsWith('.agents/notes/')
|
|
112
115
|
|| rel === '.agents/workflow-modules.txt' || rel === '.agents/rule-budgets.txt'
|
|
113
116
|
|| rel === '.agents/metric-claims.txt' || rel === '.agents/metric-derivers.cjs'
|
|
114
117
|
|| rel === '.agents/metric-derivers.mjs'
|
|
115
|
-
|| rel === '.agents/hooks/local-pre-commit'
|
|
118
|
+
|| rel === '.agents/hooks/local-pre-commit'
|
|
119
|
+
|| rel.startsWith('.github/');
|
|
116
120
|
}
|
|
117
121
|
|
|
118
122
|
// ---- 模板下发感知(2026-10-06 template-downstream)----
|
|
@@ -124,12 +128,14 @@ export function isOwned(rel) {
|
|
|
124
128
|
// 装户不期望跟随包源模板——模板感知排除出比对面(防「拉取了也被生成器重写」的无谓出账)。新增生成器目标须同步本清单。
|
|
125
129
|
export const TEMPLATE_DRIFT_EXCLUDE = new Set(['workflow/INDEX.md', 'wiki/INDEX.md', 'wiki/知识沉淀总览.html']);
|
|
126
130
|
|
|
127
|
-
// 映射规则单源:renderTree 的「srcRoot/templates ↔ targetRoot/项目根」关系 +
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
+
// 映射规则单源:renderTree 的「srcRoot/templates ↔ targetRoot/项目根」关系 + 点目录前缀翻译。
|
|
132
|
+
// 通用点前缀翻译(2026-10-07 adopter-ci-github,由 .agents/ 特例泛化):装户 rel 首段以 `.` 开头时探
|
|
133
|
+
// templates/_<去点首段>/<余径>(.agents/notes/x → _agents/notes/x、.github/workflows/kit-ci.yml →
|
|
134
|
+
// _github/…)——与 render.mjs relOf 的顶层 `_`→`.` 同一规则的逆向;无点前缀 rel(AGENTS.md / workflow/…)
|
|
135
|
+
// 原样探 templates/<rel>。无对应模板返回 null(不参与感知)。
|
|
131
136
|
export function srcTemplatePath(pkgRoot, rel) {
|
|
132
|
-
const
|
|
137
|
+
const first = rel.split('/')[0];
|
|
138
|
+
const tplRel = first.startsWith('.') ? `_${first.slice(1)}${rel.slice(first.length)}` : rel;
|
|
133
139
|
const p = path.join(pkgRoot, 'templates', tplRel);
|
|
134
140
|
return fs.existsSync(p) ? p : null;
|
|
135
141
|
}
|
package/src/sync.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// flow-kit sync:按 .agents/kit.json 台账升级 managed 文件。
|
|
2
2
|
// 三态:未改动(磁盘 sha==台账)→ 覆盖新版;本地已改 → 跳过并报告(--force 覆盖;台账保持包侧基线,
|
|
3
3
|
// 每次持续报告直至 --force 或本地对齐新版——防跳过一次后下次升级被静默覆盖,2026-09-24 语义修正);
|
|
4
|
-
// 生成器目标(INDEX.md / wiki 看板)→
|
|
4
|
+
// 生成器目标(INDEX.md / wiki 看板)→ 不比对,生成器先于台账记账执行(2026-10-08 修 1:一次 sync
|
|
5
|
+
// 自洽——生成器重写盘面后按终态记账),锚点内重写。
|
|
5
6
|
// 附带:包内新增 managed 文件 → 安装;包内已删 → 仅报告不删盘、出台账;managed 缺失 → 恢复。
|
|
6
7
|
// 台账外文件收养(2026-09-26 managed-ledger-adopt):盘上存在但台账无该 rel 的 managed 类文件,
|
|
7
8
|
// 若内容恰好等于新版渲染 sha(diskSha === fresh.sha)则收养登记——判据与台账内「改动恰好等于新版」
|
|
@@ -149,9 +150,35 @@ export function sync(args, pkgRoot) {
|
|
|
149
150
|
console.log(` 无变化 ${unchanged} 份`);
|
|
150
151
|
}
|
|
151
152
|
|
|
153
|
+
// 生成器目标走锚点重写(不参与 sha 比对)——执行前过供应链防线(init-p1-batch P1-3):
|
|
154
|
+
// 目标侧脚本与包源渲染值 sha 一致才执行(预置/被改动的脚本不可信,跳过并显式提示)。
|
|
155
|
+
// 先于台账记账执行(2026-10-08 papercuts-cleanup-batch 修 1):生成器会重写 owned 生成目标
|
|
156
|
+
// (INDEX/看板),记账段按生成后盘面收口——一次 sync 自洽,不再出现「wiki 变更后需二次 sync 自愈」。
|
|
157
|
+
const genGuard = (rel) => {
|
|
158
|
+
const g = scriptTrusted({ pkgRoot, target, rel, vars });
|
|
159
|
+
if (!g.ok) {
|
|
160
|
+
console.log(` ⚠️ 跳过执行 ${rel}——${g.note}(供应链防线:只执行与包源渲染值一致的目标侧脚本)`);
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
};
|
|
165
|
+
if (genGuard('.agents/scripts/gen-workflow-index.mjs')) {
|
|
166
|
+
const genIndex = runNode(target, '.agents/scripts/gen-workflow-index.mjs');
|
|
167
|
+
if (genIndex.ok || fs.existsSync(path.join(target, '.agents/scripts/gen-workflow-index.mjs'))) {
|
|
168
|
+
console.log(genIndex.ok ? ' 已重生成 workflow/INDEX.md(锚点内重写)' : ` ⚠️ gen-workflow-index 失败:${genIndex.out.split('\n')[0]}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (genGuard('.agents/scripts/gen-wiki-board.mjs')) {
|
|
172
|
+
const genBoard = runNode(target, '.agents/scripts/gen-wiki-board.mjs');
|
|
173
|
+
if (genBoard.ok || fs.existsSync(path.join(target, '.agents/scripts/gen-wiki-board.mjs'))) {
|
|
174
|
+
console.log(genBoard.ok ? ' 已重生成 wiki 速览与看板 DATA(锚点内重写)' : ` ⚠️ gen-wiki-board 失败:${genBoard.out.split('\n')[0]}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
152
178
|
// owned 台账按盘面自愈:owned 归项目所有,哈希只是记账不是约束——手改后无须手工刷 kit.json
|
|
153
179
|
// (Shipyard 回流策略,2026-09-24:曾在消费仓被迫手工刷新 owned 哈希,根因收敛到此处)。
|
|
154
|
-
// sha 按 LF 归一记(与 doctor checkOwnedDrift 同口径,2026-09-27 gate-coverage:跨 checkout
|
|
180
|
+
// sha 按 LF 归一记(与 doctor checkOwnedDrift 同口径,2026-09-27 gate-coverage:跨 checkout 字节稳定)。
|
|
181
|
+
// 位于生成器之后(2026-10-08 papercuts-cleanup-batch 修 1):生成器重写盘面后按终态记账。
|
|
155
182
|
const ownedSha = (p) => sha256(Buffer.from(fs.readFileSync(p, 'utf8').replace(/\r\n/g, '\n'), 'utf8'));
|
|
156
183
|
let ownedRefreshed = 0;
|
|
157
184
|
if (Array.isArray(kit.owned)) {
|
|
@@ -188,33 +215,10 @@ export function sync(args, pkgRoot) {
|
|
|
188
215
|
console.log(' owned 归项目所有,不自动覆盖;如需跟随:从包源 templates/ 拷贝对应文件;有意定制可忽略(下次 sync 起静默)');
|
|
189
216
|
}
|
|
190
217
|
|
|
191
|
-
//
|
|
218
|
+
// 台账重写(生成器之后):版本对齐当前包;removed 出册;skipped 保持包侧基线持续报告
|
|
192
219
|
kit.managed = managedNew.sort((a, b) => a.rel.localeCompare(b.rel));
|
|
193
220
|
kit.version = pkg.version;
|
|
194
221
|
fs.writeFileSync(kitPath, `${JSON.stringify(kit, null, 2)}\n`);
|
|
195
|
-
|
|
196
|
-
// 生成器目标走锚点重写(不参与 sha 比对)——执行前过供应链防线(init-p1-batch P1-3):
|
|
197
|
-
// 目标侧脚本与包源渲染值 sha 一致才执行(预置/被改动的脚本不可信,跳过并显式提示)
|
|
198
|
-
const genGuard = (rel) => {
|
|
199
|
-
const g = scriptTrusted({ pkgRoot, target, rel, vars });
|
|
200
|
-
if (!g.ok) {
|
|
201
|
-
console.log(` ⚠️ 跳过执行 ${rel}——${g.note}(供应链防线:只执行与包源渲染值一致的目标侧脚本)`);
|
|
202
|
-
return false;
|
|
203
|
-
}
|
|
204
|
-
return true;
|
|
205
|
-
};
|
|
206
|
-
if (genGuard('.agents/scripts/gen-workflow-index.mjs')) {
|
|
207
|
-
const genIndex = runNode(target, '.agents/scripts/gen-workflow-index.mjs');
|
|
208
|
-
if (genIndex.ok || fs.existsSync(path.join(target, '.agents/scripts/gen-workflow-index.mjs'))) {
|
|
209
|
-
console.log(genIndex.ok ? ' 已重生成 workflow/INDEX.md(锚点内重写)' : ` ⚠️ gen-workflow-index 失败:${genIndex.out.split('\n')[0]}`);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
if (genGuard('.agents/scripts/gen-wiki-board.mjs')) {
|
|
213
|
-
const genBoard = runNode(target, '.agents/scripts/gen-wiki-board.mjs');
|
|
214
|
-
if (genBoard.ok || fs.existsSync(path.join(target, '.agents/scripts/gen-wiki-board.mjs'))) {
|
|
215
|
-
console.log(genBoard.ok ? ' 已重生成 wiki 速览与看板 DATA(锚点内重写)' : ` ⚠️ gen-wiki-board 失败:${genBoard.out.split('\n')[0]}`);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
222
|
} finally {
|
|
219
223
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
220
224
|
}
|
package/src/sync.test.mjs
CHANGED
|
@@ -366,5 +366,87 @@ else if (cmd === 'add-gate') addGate(rest, root);
|
|
|
366
366
|
check('S15⑥ .agents/ 前缀翻译 → _agents 模板参与感知出账', (r7.stdout + r7.stderr).includes('有演进且盘面未跟随') && (r7.stdout + r7.stderr).includes('.agents/rule-budgets.txt'));
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
+
// ============ 场景 16:装户 GitHub CI 远端门模板(2026-10-07 adopter-ci-github)============
|
|
370
|
+
// 包源 templates/_github/workflows/kit-ci.yml → 装户 .github/workflows/kit-ci.yml(owned):
|
|
371
|
+
// ① isOwned / srcTemplatePath 契约(含 .agents 存量不回归、无模板反例);
|
|
372
|
+
// ② 存量装户盘上无该件 → newOwned 提示且不自动落盘;③ 已装 + 源演进 → 感知出账 + 锚刷新 + 再跑静默
|
|
373
|
+
{
|
|
374
|
+
const sha = (s) => sha256(Buffer.from(s, 'utf8'));
|
|
375
|
+
const CI_REL = '.github/workflows/kit-ci.yml';
|
|
376
|
+
const CI_TPL = 'ci v2\n';
|
|
377
|
+
const { isOwned, srcTemplatePath } = await import(pathToFileURL(path.join(SRC_ROOT, 'profiles.mjs')).href);
|
|
378
|
+
|
|
379
|
+
// ① 契约断言:.github/ 归 owned;点前缀翻译 .github ↔ _github 命中、.agents 严格不回归、无模板 null
|
|
380
|
+
check('S16① isOwned:.github/ 前缀归 owned + 存量正例不回归(spec 验收 1)',
|
|
381
|
+
isOwned(CI_REL) === true && isOwned('.github/other.yml') === true
|
|
382
|
+
&& isOwned('AGENTS.md') === true && isOwned('.agents/notes/x.md') === true && isOwned('workflow/plans/a.md') === true
|
|
383
|
+
&& isOwned('.agents/scripts/check-loop.mjs') === false);
|
|
384
|
+
const fxC = mkFixture();
|
|
385
|
+
W(path.join(fxC, 'templates/_github/workflows/kit-ci.yml'), CI_TPL);
|
|
386
|
+
const norm = (p) => p.split(path.sep).join('/');
|
|
387
|
+
check('S16① srcTemplatePath 通用点前缀翻译:.github → _github 命中 / .agents 严格不回归',
|
|
388
|
+
norm(srcTemplatePath(fxC, CI_REL)).endsWith('templates/_github/workflows/kit-ci.yml')
|
|
389
|
+
&& norm(srcTemplatePath(fxC, '.agents/scripts/keep.txt')).endsWith('templates/_agents/scripts/keep.txt'),
|
|
390
|
+
JSON.stringify({ ci: srcTemplatePath(fxC, CI_REL), agents: srcTemplatePath(fxC, '.agents/scripts/keep.txt') }));
|
|
391
|
+
check('S16① srcTemplatePath 无对应模板 → null(.gitattributes 反例,与旧实现等价)',
|
|
392
|
+
srcTemplatePath(fxC, '.gitattributes') === null);
|
|
393
|
+
|
|
394
|
+
// ② 存量装户(盘上无该件)→ newOwned 提示且不自动落盘
|
|
395
|
+
const fx1 = mkFixture();
|
|
396
|
+
W(path.join(fx1, 'templates/_github/workflows/kit-ci.yml'), CI_TPL);
|
|
397
|
+
const t1 = mkTarget(fx1);
|
|
398
|
+
const r1 = runCmd('sync', fx1, t1);
|
|
399
|
+
const out1 = r1.stdout + r1.stderr;
|
|
400
|
+
check('S16② 存量装户无该件 → newOwned 提示含文件名且不自动落盘',
|
|
401
|
+
out1.includes('新增 owned 起步文档') && out1.includes(CI_REL) && !fs.existsSync(path.join(t1, CI_REL)), out1);
|
|
402
|
+
|
|
403
|
+
// ③ 装户已装(带旧锚)+ 源演进 → 感知出账;锚刷新后(custom-synced)再跑静默
|
|
404
|
+
const fx2 = mkFixture();
|
|
405
|
+
W(path.join(fx2, 'templates/_github/workflows/kit-ci.yml'), CI_TPL);
|
|
406
|
+
const t2 = mkTarget(fx2);
|
|
407
|
+
W(path.join(t2, CI_REL), 'ci CUSTOM\n');
|
|
408
|
+
W(path.join(t2, '.agents/kit.json'), `${JSON.stringify({
|
|
409
|
+
kit: 'agentic-flow-kit', version: '1.0.0', createdAt: '2026-09-23T00:00:00Z',
|
|
410
|
+
options: { hosts: ['zcode'], stack: 'none', boardPort: '777' },
|
|
411
|
+
managed: [{ rel: '.agents/scripts/keep.txt', sha256: sha('keep v2\n') }],
|
|
412
|
+
owned: [{ rel: CI_REL, sha256: sha('ci CUSTOM\n'), srcSha256: sha('ci v1\n') }],
|
|
413
|
+
}, null, 2)}\n`);
|
|
414
|
+
const r2 = runCmd('sync', fx2, t2);
|
|
415
|
+
const out2 = r2.stdout + r2.stderr;
|
|
416
|
+
check('S16③ 源演进未跟随 → 感知出账含 .github rel', out2.includes('有演进且盘面未跟随') && out2.includes(CI_REL), out2.slice(0, 300));
|
|
417
|
+
check('S16③ 装户定制不被覆盖(盘面仍 CUSTOM)', R(path.join(t2, CI_REL)) === 'ci CUSTOM\n', out2);
|
|
418
|
+
const kit2 = JSON.parse(R(path.join(t2, '.agents/kit.json')));
|
|
419
|
+
check('S16③ 锚刷新为当前包源 sha', kit2.owned.find((f) => f.rel === CI_REL).srcSha256 === sha(CI_TPL), JSON.stringify(kit2.owned));
|
|
420
|
+
const r2b = runCmd('sync', fx2, t2);
|
|
421
|
+
check('S16③ 锚已刷(custom-synced)→ 再跑静默', !(r2b.stdout + r2b.stderr).includes('有演进且盘面未跟随'));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ============ 场景 17:sync 一次自洽——生成器重写 owned 目标后按终态记账(2026-10-08 papercuts-cleanup-batch 修 1)============
|
|
425
|
+
// 修前:记账段(owned 自愈/感知/台账重写)先于生成器执行 → 生成器重写 wiki 类 owned 目标后台账停旧值
|
|
426
|
+
// → doctor §6.6 owned 漂移 FAIL 一次,需二次 sync 自愈。修后:生成器先跑,记账按生成后盘面收口。
|
|
427
|
+
{
|
|
428
|
+
const sha = (s) => sha256(Buffer.from(s, 'utf8'));
|
|
429
|
+
const GEN = "import fs from 'node:fs';\nfs.writeFileSync('wiki/INDEX.md', 'generated v2\\n');\n";
|
|
430
|
+
const fx = mkFixture();
|
|
431
|
+
W(path.join(fx, 'templates/_agents/scripts/gen-wiki-board.mjs'), GEN);
|
|
432
|
+
const t = mkTarget(fx);
|
|
433
|
+
W(path.join(t, '.agents/scripts/gen-wiki-board.mjs'), GEN); // 目标侧与包源渲染一致(genGuard/scriptTrusted 放行)
|
|
434
|
+
W(path.join(t, 'wiki/INDEX.md'), 'generated v1\n'); // 盘面停旧版——生成器将重写它
|
|
435
|
+
const k0 = JSON.parse(R(path.join(t, '.agents/kit.json')));
|
|
436
|
+
k0.managed.push({ rel: '.agents/scripts/gen-wiki-board.mjs', sha256: sha(GEN) });
|
|
437
|
+
k0.owned.push({ rel: 'wiki/INDEX.md', sha256: sha('generated v1\n') });
|
|
438
|
+
W(path.join(t, '.agents/kit.json'), `${JSON.stringify(k0, null, 2)}\n`);
|
|
439
|
+
const r = runCmd('sync', fx, t);
|
|
440
|
+
const out = r.stdout + r.stderr;
|
|
441
|
+
check('S17 一次 sync:生成器重写后 owned 台账按终态记账(wiki/INDEX.md → v2 sha)',
|
|
442
|
+
JSON.parse(R(path.join(t, '.agents/kit.json'))).owned.find((f) => f.rel === 'wiki/INDEX.md').sha256 === sha('generated v2\n'),
|
|
443
|
+
JSON.stringify(JSON.parse(R(path.join(t, '.agents/kit.json'))).owned) + out.slice(0, 300));
|
|
444
|
+
const { checkOwnedDrift } = await import(pathToFileURL(path.join(SRC_ROOT, 'doctor.mjs')).href);
|
|
445
|
+
const d = checkOwnedDrift(t);
|
|
446
|
+
check('S17 一次 sync 后 doctor owned 零漂移(修前必漂移需二次 sync)', d.drift === 0 && d.gone.length === 0, JSON.stringify(d));
|
|
447
|
+
const r2 = runCmd('sync', fx, t);
|
|
448
|
+
check('S17 二次 sync 幂等(无 owned 刷新报告)', !(r2.stdout + r2.stderr).includes('owned 台账哈希按盘面刷新'), r2.stdout + r2.stderr);
|
|
449
|
+
}
|
|
450
|
+
|
|
369
451
|
console.log(`\n合计: PASS ${pass} / FAIL ${failCount}`);
|
|
370
452
|
process.exit(failCount ? 1 : 0);
|
package/templates/AGENTS.md
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
### 门禁与提交
|
|
25
25
|
|
|
26
26
|
- `.githooks/`(经 `core.hooksPath` 挂载,本地 clone 后执行 `git config core.hooksPath .githooks`):pre-commit(闭环配对 / 泳道完整性 / wiki 台账 / 规则面预算 / 双源一致性 / managed 台账快检 / 敏感信息扫描与条件构建)、pre-push(闭环断档扫描)、commit-msg、post-commit、pre-merge-commit。
|
|
27
|
+
- **CI 远端门(2026-10-07 起)**:`.github/workflows/kit-ci.yml`(owned)在 GitHub runner 复跑 `node .agents/scripts/verify.mjs`(云端兜底本地钩子);非 GitHub 装户可删,等价门 = 同入口命令 + 服务端分支策略(人工一次性配置,机器不校验)。
|
|
27
28
|
- **`git commit` / `git merge` / `git push` 三处一律禁 `--no-verify`**——被拦说明产出不合规,按提示修完原路重试。
|
|
28
29
|
- 项目专属门禁挂 `.agents/hooks/local-pre-commit`。装户五条硬规则(配对、验收、确认留痕、敏感信息、双源台账)与 `audit` 档见 `workflow/README.md`「硬规则」。
|
|
29
30
|
- 提交遵循 Conventional Commits 中文(feat / fix / docs / style / refactor / perf)。L1+ 三段式闭环链:`docs(*)` approved 留痕 → 代码 → 关单 `docs(*)`,相邻提交即同一闭环(2026-10-04 修订)。
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
# 口径:预算比较用「索引 / HEAD 字节」(git cat-file,LF 归一);禁用工作区 wc -c 核对(CRLF 膨胀)。
|
|
6
6
|
# 2026-10-06 backflow-loop-audit-remediation:6 份命令文档顶部加「装户边界」标注(build.md 实测 8386B
|
|
7
7
|
# 超 8192 被拦)——单篇上限 8192→8704(与装户 Shipyard.Material 同值,512B 步进);目录合计未超不动。
|
|
8
|
-
AGENTS.md 7680
|
|
8
|
+
# 2026-10-07 adopter-ci-github:AGENTS.md 门禁节补 CI 远端门一行(299B,实测 7969B 超 7680 被拦)——
|
|
9
|
+
# 新门入册属合法增长,同 512B 步进上调 7680→8192。
|
|
10
|
+
AGENTS.md 8192
|
|
9
11
|
.agents/commands/*.md 8704
|
|
10
12
|
.agents/commands/ 65536
|
|
@@ -41,6 +41,9 @@ function parseResult(raw) {
|
|
|
41
41
|
|
|
42
42
|
// splitTables:按表头签名识别两张结果表——节标题只服务人类阅读,表头才是数据边界
|
|
43
43
|
// (2026-09-24 修复:曾按 `## 委派结果` 节标题定位,节结构漂移时已有记录被静默读成「台账为空」)
|
|
44
|
+
// ⚠️ 互引(2026-10-08 papercuts-cleanup-batch 修 2):check-loop.mjs delegationResultRows 已对齐本签名
|
|
45
|
+
// 口径(委派表头=含「被委派方」列 / 自做表头=首列「日期」且含「任务一句话」列)——一边改另一边须跟;
|
|
46
|
+
// 差异保留:本函数按「连续 | 行」断表(聚合需分行归属),check-loop 按「## 节」终止收集(对账只需并集)
|
|
44
47
|
function splitTables(text) {
|
|
45
48
|
const tables = { delegated: [], self: [] };
|
|
46
49
|
let cur = null;
|
|
@@ -54,9 +57,15 @@ function splitTables(text) {
|
|
|
54
57
|
return tables;
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
// readLedger:root 显式参数 + soft 模式(2026-10-08 workflow-dashboard 使能导出,判据单源复用——
|
|
61
|
+
// 仪表盘 require 本函数取行,不重写表解析;缺省 root/无 soft 行为与原签名完全一致,main() 零变化)
|
|
62
|
+
function readLedger(root = ROOT, opts = {}) {
|
|
63
|
+
const ledger = path.join(root, 'workflow', 'delegations.md');
|
|
64
|
+
if (!fs.existsSync(ledger)) {
|
|
65
|
+
if (opts.soft) return [];
|
|
66
|
+
fail(`找不到 ${ledger}`);
|
|
67
|
+
}
|
|
68
|
+
const text = fs.readFileSync(ledger, 'utf8');
|
|
60
69
|
const tables = splitTables(text);
|
|
61
70
|
const delegated = parseTableRows(tables.delegated.join('\n')).map((c) => ({
|
|
62
71
|
scope: '委派',
|
|
@@ -73,7 +82,8 @@ function readLedger() {
|
|
|
73
82
|
const rows = delegated.concat(selfDone);
|
|
74
83
|
// 零数据行时区分「台账真空」与「结构漂移」:有日期开头的数据行却识别不到表 → fail-loud,不再静默「台账为空」
|
|
75
84
|
if (!rows.length && /^\s*\|\s*\d{4}-\d{2}-\d{2}\s*\|/m.test(text)) {
|
|
76
|
-
|
|
85
|
+
if (opts.soft) return [];
|
|
86
|
+
fail(`台账结构漂移:${ledger} 存在日期开头的数据行但未识别到结果表(委派表表头须含「被委派方」列,自做表表头须为「日期 | 任务一句话 | 结果 | 备注」)`);
|
|
77
87
|
}
|
|
78
88
|
return rows;
|
|
79
89
|
}
|
|
@@ -197,5 +207,5 @@ function main() {
|
|
|
197
207
|
if (require.main === module) {
|
|
198
208
|
main();
|
|
199
209
|
} else {
|
|
200
|
-
module.exports = { parseResult, metrics, gateMonth, expansionVerdict, GATE };
|
|
210
|
+
module.exports = { parseResult, metrics, gateMonth, expansionVerdict, GATE, readLedger };
|
|
201
211
|
}
|
|
@@ -4,49 +4,93 @@
|
|
|
4
4
|
// 随常规提交静默落 main,三次真实发生(0272a1c 预入 / ff3c721 还原窗口 / 537df18 闭合),本脚本在
|
|
5
5
|
// 提交时刻补齐第三边。sha 口径:LF 归一 utf8(同 doctor §4 / render.mjs shaText / source-sync-check
|
|
6
6
|
// 读取侧,跨 checkout 稳定;装户自包含约束决定独立实现,三处口径改动须同批)。
|
|
7
|
+
// 入库态口径(2026-10-08 papercuts-cleanup-batch 修 5,独立复核 P1 修正取值链):快检原比「工作树」,
|
|
8
|
+
// 而分批提交时提交树(= HEAD + 暂存合成,删除同样被应用)里的 kit.json sha 可能指向「工作区已改、
|
|
9
|
+
// 未提交」内容 → clone/CI 跑 doctor 必红(e8445c5 实证 6 项不符)。无参运行自适应:**暂存区非空即
|
|
10
|
+
// 入库态**(不判「提交集含 kit.json」——diff --cached 只列与 HEAD 有差异的文件,kit.json 无变化暂存
|
|
11
|
+
// 时不出现,但其合成提交树同样可能预 landing:只 add 件不 add 台账是姊妹漏洞)。入库态取值:
|
|
12
|
+
// 台账清单 = 暂存版 kit.json(`git show :.agents/kit.json`);暂存无而 HEAD 有 = 退出性提交(kit.json
|
|
13
|
+
// 被暂存删除)放行;managed 内容 = `git show :rel` 暂存版,**暂存无即 gone**(提交树=index——
|
|
14
|
+
// 未暂存改动的文件 index 仍在,`:rel` 必命中;`:rel` 缺失唯一可达路径是暂存删除,HEAD 兜底会把它
|
|
15
|
+
// 误读为「未触碰」放行 = 提交门绕过,复核 P1 实证)。
|
|
16
|
+
// 暂存区空(amend 等罕见态)→ 维持工作树口径零变化。
|
|
7
17
|
// 边界:仅包源环境由 .githooks/pre-commit 触发(装户 managed 手改走 doctor §4 WARN 口径,不硬拦);
|
|
8
18
|
// managed 删除即拦——sync 对 removed 只报不删,删件须手动同步装副本并重跑 sync 刷台账。
|
|
9
|
-
// 用法:node .agents/scripts/check-ledger.mjs(cwd =
|
|
10
|
-
// 单元测试:import { ledgerDrift } from './check-ledger.mjs'
|
|
19
|
+
// 用法:node .agents/scripts/check-ledger.mjs(cwd = 仓库根;非零出口即阻断提交;无参自适应两口径)
|
|
20
|
+
// 单元测试:import { ledgerDrift } from './check-ledger.mjs'({staged:true} 显式入库态)
|
|
11
21
|
import fs from 'node:fs';
|
|
12
22
|
import path from 'node:path';
|
|
13
23
|
import { createHash } from 'node:crypto';
|
|
24
|
+
import { spawnSync } from 'node:child_process';
|
|
14
25
|
|
|
15
|
-
|
|
26
|
+
const gitText = (target, args) => {
|
|
27
|
+
const r = spawnSync('git', args, { cwd: target, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 });
|
|
28
|
+
return r.status === 0 ? r.stdout : null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function ledgerDrift(target = process.cwd(), { staged = false } = {}) {
|
|
16
32
|
const kitPath = path.join(target, '.agents', 'kit.json');
|
|
17
33
|
if (!fs.existsSync(kitPath)) return { skipped: true, note: '无 .agents/kit.json(非 flow-kit 安装)' };
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
34
|
+
// staged 模式:台账清单取暂存版 kit.json(提交树=index 合成语义);暂存无而 HEAD 有 = 退出性提交
|
|
35
|
+
// (kit.json 被暂存删除)放行;两者皆无(新仓无 kit.json)同样放行
|
|
36
|
+
let kit = null;
|
|
37
|
+
if (staged) {
|
|
38
|
+
const stagedKit = gitText(target, ['show', ':.agents/kit.json']);
|
|
39
|
+
const headKit = stagedKit === null ? gitText(target, ['show', 'HEAD:.agents/kit.json']) : null;
|
|
40
|
+
if (stagedKit === null) {
|
|
41
|
+
return { skipped: true, note: headKit !== null ? '提交树无 .agents/kit.json(退出 flow-kit 的提交),跳过' : '提交树无 .agents/kit.json(新仓),跳过', mode: 'staged' };
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
kit = JSON.parse(stagedKit);
|
|
45
|
+
} catch (e) {
|
|
46
|
+
return { skipped: false, fatal: `kit.json(暂存版)解析失败:${e.message}`, modified: [], gone: [], total: 0, version: '?', mode: 'staged' };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (!kit) {
|
|
50
|
+
try {
|
|
51
|
+
kit = JSON.parse(fs.readFileSync(kitPath, 'utf8'));
|
|
52
|
+
} catch (e) {
|
|
53
|
+
return { skipped: false, fatal: `kit.json 解析失败:${e.message}`, modified: [], gone: [], total: 0, version: '?', mode: staged ? 'staged' : 'worktree' };
|
|
54
|
+
}
|
|
23
55
|
}
|
|
24
56
|
if (!Array.isArray(kit.managed)) {
|
|
25
57
|
// fail-loud(独立复核 P2-3,2026-09-28):JSON 合法但 managed 缺失/非数组时静默「全对齐」= 假放,
|
|
26
58
|
// 同 doctor §4 的 Array.isArray 显式判口径,台账结构坏了必须响
|
|
27
|
-
return { skipped: false, fatal: 'kit.json managed 缺失或非数组', modified: [], gone: [], total: 0, version: kit.version };
|
|
59
|
+
return { skipped: false, fatal: 'kit.json managed 缺失或非数组', modified: [], gone: [], total: 0, version: kit.version, mode: staged ? 'staged' : 'worktree' };
|
|
28
60
|
}
|
|
29
61
|
const modified = [];
|
|
30
62
|
const gone = [];
|
|
31
63
|
const managed = kit.managed;
|
|
32
64
|
for (const f of managed) {
|
|
33
|
-
|
|
34
|
-
if (
|
|
35
|
-
|
|
65
|
+
let content = null;
|
|
66
|
+
if (staged) {
|
|
67
|
+
// 入库态:提交树 = index(暂存版);`:rel` 缺失唯一可达路径是暂存删除 → gone 拦(复核 P1:
|
|
68
|
+
// HEAD 兜底会把暂存删除误读为「未触碰」放行——提交树已无该件而台账仍跟踪,clone/CI 必红)
|
|
69
|
+
content = gitText(target, ['show', `:${f.rel}`]);
|
|
70
|
+
if (content === null) { gone.push(f.rel); continue; }
|
|
71
|
+
} else {
|
|
72
|
+
const p = path.join(target, f.rel);
|
|
73
|
+
if (!fs.existsSync(p)) { gone.push(f.rel); continue; }
|
|
74
|
+
content = fs.readFileSync(p, 'utf8');
|
|
75
|
+
}
|
|
76
|
+
const h = createHash('sha256').update(content.replace(/\r\n/g, '\n'), 'utf8').digest('hex'); // LF 归一,同 doctor §4
|
|
36
77
|
if (h !== f.sha256) modified.push(f.rel);
|
|
37
78
|
}
|
|
38
|
-
return { skipped: false, fatal: '', modified, gone, total: managed.length, version: kit.version };
|
|
79
|
+
return { skipped: false, fatal: '', modified, gone, total: managed.length, version: kit.version, mode: staged ? 'staged' : 'worktree' };
|
|
39
80
|
}
|
|
40
81
|
|
|
41
82
|
const isMain = process.argv[1] && process.argv[1].endsWith('check-ledger.mjs');
|
|
42
83
|
if (isMain) {
|
|
43
|
-
|
|
84
|
+
// 无参自适应:暂存区非空(有提交发生)→ 入库态口径;暂存区空(amend 等罕见态/非 git 仓)→ 工作树口径
|
|
85
|
+
const cached = gitText(process.cwd(), ['diff', '--cached', '--name-only']) || '';
|
|
86
|
+
const staged = cached.split(/\r?\n/).some((l) => l.trim() !== '');
|
|
87
|
+
const r = ledgerDrift(process.cwd(), { staged });
|
|
44
88
|
console.log('▶ flow-kit check-ledger');
|
|
45
89
|
if (r.skipped) {
|
|
46
90
|
console.log(' ' + r.note + ',跳过 ✅');
|
|
47
91
|
process.exit(0);
|
|
48
92
|
}
|
|
49
|
-
console.log(' kit.json v' + r.version + ':managed ' + r.total + ' 份,台账 ↔ 盘面(工作树,LF 归一)');
|
|
93
|
+
console.log(' kit.json v' + r.version + ':managed ' + r.total + ' 份,台账 ↔ ' + (r.mode === 'staged' ? '提交树入库态(暂存版=index,暂存删除即拦,LF 归一)' : '盘面(工作树,LF 归一)'));
|
|
50
94
|
if (r.fatal) console.error('❌ ' + r.fatal);
|
|
51
95
|
if (r.modified.length) {
|
|
52
96
|
console.error(' 台账漂移(盘面 ≠ 台账 sha)' + r.modified.length + ':');
|
|
@@ -57,7 +101,7 @@ if (isMain) {
|
|
|
57
101
|
for (const rel of r.gone) console.error(' - ' + rel);
|
|
58
102
|
}
|
|
59
103
|
if (r.fatal || r.modified.length || r.gone.length) {
|
|
60
|
-
console.error(' 提交被拦:kit.json
|
|
104
|
+
console.error(' 提交被拦:kit.json 与' + (r.mode === 'staged' ? '提交树(暂存面)' : '盘面') + '不一致(预 landing / 手改未随动)——跑 node bin/flow-kit.mjs sync 成对后原路重试');
|
|
61
105
|
process.exit(1);
|
|
62
106
|
}
|
|
63
107
|
console.log(' 全对齐 ✅');
|
|
@@ -123,5 +123,75 @@ const runCli = (cwd) => spawnSync(process.execPath, [SCRIPT], { encoding: 'utf8'
|
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
// ---- S10 入库态口径(2026-10-08 papercuts-cleanup-batch 修 5):git 仓 fixture,分批提交场景 ----
|
|
127
|
+
// 核心场景(papercuts 2026-09-30 行):sync 刷新 kit.json 后只 add 台账不 add 件 → 提交树内 kit.json
|
|
128
|
+
// 预 landing → 修前工作树口径「全对齐」放行(假绿),clone/CI doctor 必红;修后入库态模式拦。
|
|
129
|
+
{
|
|
130
|
+
const GIT = process.platform === 'win32' ? 'git.exe' : 'git';
|
|
131
|
+
const g = (cwd, ...args) => spawnSync(GIT, args, { cwd, encoding: 'utf8' });
|
|
132
|
+
const W2 = (root, rel, content) => {
|
|
133
|
+
fs.mkdirSync(path.join(root, path.dirname(rel)), { recursive: true });
|
|
134
|
+
fs.writeFileSync(path.join(root, rel), content);
|
|
135
|
+
};
|
|
136
|
+
const repo = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-cl-git-'));
|
|
137
|
+
g(repo, 'init', '-q');
|
|
138
|
+
g(repo, 'config', 'user.email', 't@t');
|
|
139
|
+
g(repo, 'config', 'user.name', 't');
|
|
140
|
+
g(repo, 'config', 'core.autocrlf', 'false');
|
|
141
|
+
const kitOf = (aSha) => JSON.stringify({
|
|
142
|
+
kit: 't', version: '0.0.0-test',
|
|
143
|
+
managed: [{ rel: '.agents/scripts/a.mjs', sha256: aSha }],
|
|
144
|
+
}) + '\n';
|
|
145
|
+
W2(repo, '.agents/scripts/a.mjs', 'A v1\n');
|
|
146
|
+
W2(repo, '.agents/kit.json', kitOf(lfSha('A v1\n')));
|
|
147
|
+
g(repo, 'add', '-A');
|
|
148
|
+
g(repo, 'commit', '-q', '-m', 'v1');
|
|
149
|
+
// sync 模拟:件与台账同刷 v2(工作区自洽,均未提交)
|
|
150
|
+
W2(repo, '.agents/scripts/a.mjs', 'A v2\n');
|
|
151
|
+
W2(repo, '.agents/kit.json', kitOf(lfSha('A v2\n')));
|
|
152
|
+
|
|
153
|
+
// S10① 只 add kit.json(分批提交)→ 暂存台账(v2) vs 入库件(HEAD v1) → 拦
|
|
154
|
+
g(repo, 'add', '.agents/kit.json');
|
|
155
|
+
const r1 = ledgerDrift(repo, { staged: true });
|
|
156
|
+
check('S10① 入库态:台账(v2 暂存) vs 件(HEAD v1) → 拦(修前工作树口径假绿放行)',
|
|
157
|
+
r1.mode === 'staged' && r1.modified.includes('.agents/scripts/a.mjs'), JSON.stringify(r1));
|
|
158
|
+
// S10② 件与台账同 add → 入库态一致 → 过
|
|
159
|
+
g(repo, 'add', '.agents/scripts/a.mjs');
|
|
160
|
+
const r2 = ledgerDrift(repo, { staged: true });
|
|
161
|
+
check('S10② 入库态:件与台账同 add → 一致过', r2.mode === 'staged' && !r2.modified.length && !r2.gone.length, JSON.stringify(r2));
|
|
162
|
+
g(repo, 'commit', '-q', '-m', 'v2 both');
|
|
163
|
+
// S10③ 工作树删件未暂存 → index 仍有该件(`:rel` 命中 HEAD 内容)→ 不误报 gone(复核 P1 修后语义:
|
|
164
|
+
// HEAD 兜底已移除,判定路径 = `:rel` 命中而非兜底)
|
|
165
|
+
fs.rmSync(path.join(repo, '.agents/scripts/a.mjs'));
|
|
166
|
+
const r3 = ledgerDrift(repo, { staged: true });
|
|
167
|
+
check('S10③ 入库态:工作树删件未暂存 → index 仍在,不误报', !r3.gone.length && !r3.modified.length, JSON.stringify(r3));
|
|
168
|
+
// S10④ 暂存删除(git rm)→ 提交树已无该件而台账仍跟踪 → gone 拦(复核 P1:HEAD 兜底曾把它误读
|
|
169
|
+
// 为「未触碰」放行 = 提交门绕过;修后与模块头「managed 删除即拦」政策一致)
|
|
170
|
+
g(repo, 'rm', '-q', '--cached', '.agents/scripts/a.mjs');
|
|
171
|
+
const r4 = ledgerDrift(repo, { staged: true });
|
|
172
|
+
check('S10④ 入库态:暂存删除 → gone 拦(提交树=index,删除被应用)', r4.gone.includes('.agents/scripts/a.mjs'), JSON.stringify(r4));
|
|
173
|
+
// S10④b 退出性提交:kit.json 被暂存删除 → 放行(skip,语义与 managed 删除拦截区分)
|
|
174
|
+
g(repo, 'rm', '-q', '--cached', '.agents/kit.json');
|
|
175
|
+
const r4b = ledgerDrift(repo, { staged: true });
|
|
176
|
+
check('S10④b 入库态:kit.json 暂存删除(退出性提交)→ skip 放行', r4b.skipped === true, JSON.stringify(r4b));
|
|
177
|
+
g(repo, 'reset', '-q', '--hard', 'HEAD'); // 复原(防脏)
|
|
178
|
+
|
|
179
|
+
// S10⑤ isMain 无参自适应(子进程):暂存区空 → 「工作树」;暂存区非空 → 自动「入库态」
|
|
180
|
+
g(repo, 'reset', '-q', '--hard', 'HEAD'); // 清掉 ④ 的暂存删除,回到干净 v2 both
|
|
181
|
+
const s1 = runCli(repo); // 暂存区空 → 工作树口径
|
|
182
|
+
check('S10⑤ 暂存区空 → 工作树口径', s1.status === 0 && (s1.stdout || '').includes('工作树'), `out=${s1.stdout}`);
|
|
183
|
+
W2(repo, 'unrelated.txt', 'x\n');
|
|
184
|
+
g(repo, 'add', 'unrelated.txt'); // 暂存非空(无关文件即可)→ 自动入库态;kit.json 未暂存走 HEAD 兜底
|
|
185
|
+
const s2 = runCli(repo);
|
|
186
|
+
check('S10⑤ 暂存区非空 → 自动切入库态且一致 exit 0',
|
|
187
|
+
s2.status === 0 && (s2.stdout || '').includes('入库态'), `status=${s2.status} out=${s2.stdout}`);
|
|
188
|
+
// S10⑥ 姊妹漏洞(完备口径价值场景):只 add 件不 add 台账 → 提交树 = HEAD 台账(v1? v2) + 暂存件 → 错位拦
|
|
189
|
+
W2(repo, '.agents/scripts/a.mjs', 'A v3\n');
|
|
190
|
+
g(repo, 'add', '.agents/scripts/a.mjs');
|
|
191
|
+
const s3 = runCli(repo);
|
|
192
|
+
check('S10⑥ 只 add 件不 add 台账 → 入库态错位拦 exit 1', s3.status === 1 && (s3.stderr || '').includes('.agents/scripts/a.mjs'), `status=${s3.status} err=${s3.stderr}`);
|
|
193
|
+
fs.rmSync(repo, { recursive: true, force: true });
|
|
194
|
+
}
|
|
195
|
+
|
|
126
196
|
console.log(`\n合计: PASS ${pass} / FAIL ${failCount}`);
|
|
127
197
|
process.exit(failCount ? 1 : 0);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// check-loop-unit.test.mjs — check-loop 零件直测(2026-10-08 checkloop-importable)
|
|
2
|
+
// papercuts 2026-10-04 isMain 行的兑现:check-loop.mjs 自 isMain 主守卫 + runCheckLoop 接缝后可 import——
|
|
3
|
+
// 本套件全部进程内直测(不 spawn),与 check-loop.test.mjs(端到端 spawn,行为回归网)互补。
|
|
4
|
+
// 覆盖:①runCheckLoop({root}) 三态(干净 exit 0 / 构造 blocker exit 1 且数组带内容 / rev+root 互斥);
|
|
5
|
+
// ②四纯零件:delegationResultRows 表头签名(节标题漂移仍解析)、versionGreater 边界、
|
|
6
|
+
// fmStatus 形态、bindingSha256 prev 复原与「确认指纹」剔除对称。
|
|
7
|
+
// 用法:node templates/_agents/scripts/check-loop-unit.test.mjs
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
13
|
+
|
|
14
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const { runCheckLoop, delegationResultRows, versionGreater, fmStatus, bindingSha256 } =
|
|
16
|
+
await import(pathToFileURL(path.join(SCRIPT_DIR, 'check-loop.mjs')).href);
|
|
17
|
+
|
|
18
|
+
let pass = 0, fail = 0;
|
|
19
|
+
const check = (name, cond, detail = '') => {
|
|
20
|
+
if (cond) { pass++; console.log(`PASS ${name}`); }
|
|
21
|
+
else { fail++; console.log(`FAIL ${name}${detail ? `——${detail}` : ''}`); }
|
|
22
|
+
};
|
|
23
|
+
const W = (root, rel, content) => { fs.mkdirSync(path.join(root, path.dirname(rel)), { recursive: true }); fs.writeFileSync(path.join(root, rel), content); };
|
|
24
|
+
|
|
25
|
+
// ---- ① runCheckLoop 进程内直测 ----
|
|
26
|
+
{
|
|
27
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cl-unit-'));
|
|
28
|
+
W(root, '.agents/workflow-enums.txt', fs.readFileSync(path.join(SCRIPT_DIR, '..', 'workflow-enums.txt'), 'utf8'));
|
|
29
|
+
let r = runCheckLoop({ root });
|
|
30
|
+
check('run:干净 fixture → exit 0(blockers/warnings 均数组)',
|
|
31
|
+
r.exitCode === 0 && Array.isArray(r.blockers) && Array.isArray(r.warnings) && r.blockers.length === 0,
|
|
32
|
+
`exit=${r.exitCode} b=${r.blockers.length}`);
|
|
33
|
+
// 构造 blocker:L2 done intent 缺 spec 配对(配对断裂 hard)
|
|
34
|
+
W(root, 'workflow/intents/2026-10-08-x.md', '---\n状态: done\n级别: L2\n日期: 2026-10-08\n模块: pipeline\n---\n# x\n');
|
|
35
|
+
r = runCheckLoop({ root });
|
|
36
|
+
check('run:构造 L2 缺配对 → exit 1 且 blockers 数组带内容',
|
|
37
|
+
r.exitCode === 1 && r.blockers.length > 0 && r.blockers[0].includes('配对断裂'),
|
|
38
|
+
`exit=${r.exitCode} b=${JSON.stringify(r.blockers).slice(0, 80)}`);
|
|
39
|
+
// rev+root 互斥(root 参数通道)
|
|
40
|
+
const r2 = runCheckLoop({ root, rev: 'abc1234' });
|
|
41
|
+
check('run:rev+root 互斥 → exit 1(报错走 stderr,返回空数组判决)', r2.exitCode === 1, `exit=${r2.exitCode}`);
|
|
42
|
+
// root 不可访问 → exit 1(不抛异常)
|
|
43
|
+
const r3 = runCheckLoop({ root: path.join(root, '不存在目录') });
|
|
44
|
+
check('run:root 不可访问 → exit 1(同款报错语义)', r3.exitCode === 1, `exit=${r3.exitCode}`);
|
|
45
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---- ② delegationResultRows:表头签名识别(零件级直测)----
|
|
49
|
+
{
|
|
50
|
+
const f = path.join(os.tmpdir(), `cl-unit-${Date.now()}-delegations.md`);
|
|
51
|
+
// 节标题漂移(「委派结果表」≠精确节名)而表头不动 → 仍解析(papercuts-cleanup-batch 修 2 语义)
|
|
52
|
+
fs.writeFileSync(f, '## 委派结果表\n\n| 日期 | 被委派方 | 任务一句话 | 结果 | 备注 |\n|---|---|---|---|---|\n| 2026-10-01 | x | y | 一次通过 | z |\n\n## 自做台账\n\n| 日期 | 任务一句话 | 结果 | 备注 |\n|---|---|---|---|\n| 2026-10-02 | p | 返工×2 | q |\n');
|
|
53
|
+
const rows = delegationResultRows(f);
|
|
54
|
+
check('零件 delegationResultRows:节标题漂移仍按表头签名解析(委派 1 + 自做 1)',
|
|
55
|
+
rows.length === 2 && rows[0].date === '2026-10-01' && rows[1].date === '2026-10-02' && rows[1].line.includes('返工×2'),
|
|
56
|
+
JSON.stringify(rows));
|
|
57
|
+
check('零件 delegationResultRows:文件不存在 → 空数组不抛', delegationResultRows(path.join(os.tmpdir(), 'cl-unit-nope.md')).length === 0);
|
|
58
|
+
fs.rmSync(f, { force: true });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---- ③ versionGreater 边界 ----
|
|
62
|
+
{
|
|
63
|
+
check('零件 versionGreater:1.2.3>1.2.2 / 1.10.0>1.9.9 / 相等 false / 非法 false',
|
|
64
|
+
versionGreater('1.2.3', '1.2.2') === true
|
|
65
|
+
&& versionGreater('1.10.0', '1.9.9') === true
|
|
66
|
+
&& versionGreater('1.2.3', '1.2.3') === false
|
|
67
|
+
&& versionGreater('abc', '1.0.0') === false);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---- ④ fmStatus 形态 ----
|
|
71
|
+
{
|
|
72
|
+
check('零件 fmStatus:标准/双空格/CRLF/无 frontmatter',
|
|
73
|
+
fmStatus('---\n状态: done\n级别: L1\n---\n# x') === 'done'
|
|
74
|
+
&& fmStatus('---\r\n状态: approved\r\n---\r\n# x') === 'approved'
|
|
75
|
+
&& fmStatus('# 无 frontmatter') === '');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---- ⑤ bindingSha256:prev 复原与「确认指纹」剔除对称 ----
|
|
79
|
+
{
|
|
80
|
+
// 前向:approved 落态全文算指纹(confirm-doc computeFingerprint 口径:CRLF 归一→剔指纹行→sha256)
|
|
81
|
+
const fpOf = (text) => createHash('sha256')
|
|
82
|
+
.update(text.replace(/\r\n/g, '\n').split('\n').filter((l) => !/^确认指纹:/.test(l)).join('\n'), 'utf8').digest('hex');
|
|
83
|
+
const approved = '---\n状态: approved\n级别: L2\n日期: 2026-10-08\n模块: pipeline\n确认指纹: ab12cd34ef56ab12\n---\n# T\n\n## 验收标准(可测试)\n\n- [x] 用例(证据:a1b2c3d)\n';
|
|
84
|
+
const done = approved.replace('状态: approved', '状态: done');
|
|
85
|
+
// 逆推:done 行集(lines 已 CRLF 归一)+ prev='approved' → 应还原出与 approved 落态一致的指纹
|
|
86
|
+
const reversed = bindingSha256(done.replace(/\r\n/g, '\n').split('\n'), 'approved');
|
|
87
|
+
check('零件 bindingSha256:done 行集经 prev 复原重算 == approved 落态指纹(内容绑定对称性)',
|
|
88
|
+
reversed === fpOf(approved), `${reversed.slice(0, 12)} vs ${fpOf(approved).slice(0, 12)}`);
|
|
89
|
+
// prev 不同 → 指纹不同(判定不恒真)
|
|
90
|
+
check('零件 bindingSha256:prev 不同 → 复原指纹不同(判定不恒真)',
|
|
91
|
+
bindingSha256(done.replace(/\r\n/g, '\n').split('\n'), 'draft') !== reversed);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
95
|
+
process.exit(fail ? 1 : 0);
|