agentic-flow-kit 0.6.0 → 0.8.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/modules/hosts/opencode/commands/{sync-hosts.md → wf-sync-hosts.md} +1 -1
- package/modules/hosts/trae/commands/wf-sync-hosts.md +1 -1
- package/package.json +1 -1
- package/src/doctor.mjs +9 -4
- package/src/profiles.mjs +19 -2
- package/src/sync-hosts.mjs +12 -6
- package/src/sync-hosts.test.mjs +14 -11
- package/templates/AGENTS.md +1 -1
- package/templates/_agents/commands/sync-hosts.md +1 -1
- package/templates/_agents/metric-claims.txt +81 -0
- package/templates/_agents/scripts/check-ledger.mjs +65 -0
- package/templates/_agents/scripts/check-ledger.test.mjs +127 -0
- package/templates/_agents/scripts/check-loop.mjs +277 -1
- package/templates/_agents/scripts/check-loop.test.mjs +496 -1
- package/templates/_agents/scripts/doctor.test.mjs +10 -5
- package/templates/_agents/scripts/gate-checklist.mjs +1 -0
- package/templates/_agents/scripts/gen-workflow-metrics.mjs +21 -2
- package/templates/_agents/scripts/source-sync-check.mjs +13 -2
- package/templates/_agents/scripts/source-sync-check.test.mjs +37 -0
- package/templates/_agents/scripts/workflows-check.test.mjs +14 -3
- package/templates/_githooks/pre-commit +17 -1
- /package/modules/hosts/opencode/commands/{build.md → wf-build.md} +0 -0
- /package/modules/hosts/opencode/commands/{deploy.md → wf-deploy.md} +0 -0
- /package/modules/hosts/opencode/commands/{design.md → wf-design.md} +0 -0
- /package/modules/hosts/opencode/commands/{gate-checklist.md → wf-gate-checklist.md} +0 -0
- /package/modules/hosts/opencode/commands/{maintain.md → wf-maintain.md} +0 -0
- /package/modules/hosts/opencode/commands/{new-task.md → wf-new-task.md} +0 -0
- /package/modules/hosts/opencode/commands/{plan.md → wf-plan.md} +0 -0
- /package/modules/hosts/opencode/commands/{review.md → wf-review.md} +0 -0
- /package/modules/hosts/opencode/commands/{source-sync-check.md → wf-source-sync-check.md} +0 -0
- /package/modules/hosts/opencode/commands/{test.md → wf-test.md} +0 -0
|
@@ -4,7 +4,7 @@ description: 跨宿主适配层同步(正文段 sha 比对 + 用户拍板 +
|
|
|
4
4
|
|
|
5
5
|
# Sync-Hosts · 跨宿主适配层正文段同步
|
|
6
6
|
|
|
7
|
-
> 改权威源(`templates/_agents/{commands,roles}/*.md`)后薄适配(`modules/hosts/<h>/{agents,commands}/*.md`)要同步——薄适配正文 = 权威源正文,frontmatter 各自保留宿主特化(trae
|
|
7
|
+
> 改权威源(`templates/_agents/{commands,roles}/*.md`)后薄适配(`modules/hosts/<h>/{agents,commands}/*.md`)要同步——薄适配正文 = 权威源正文,frontmatter 各自保留宿主特化(commands 层文件名 = 宿主命令名,opencode / trae 同用 `wf-` 前缀,单源见 `src/profiles.mjs#HOSTS.commandPrefix`;trae 另有 `name: wf-X`,zcode/omp 各自原描述)。
|
|
8
8
|
> 装户视角:`flow-kit doctor` §7.x 跑同名检查(仅装户,包源环境跳过——包源下永远是 drift)。
|
|
9
9
|
|
|
10
10
|
## 何时跑
|
|
@@ -5,7 +5,7 @@ description: 跨宿主适配层同步(正文段 sha 比对 + 用户拍板 +
|
|
|
5
5
|
|
|
6
6
|
# Sync-Hosts · 跨宿主适配层正文段同步
|
|
7
7
|
|
|
8
|
-
> 改权威源(`templates/_agents/{commands,roles}/*.md`)后薄适配(`modules/hosts/<h>/{agents,commands}/*.md`)要同步——薄适配正文 = 权威源正文,frontmatter 各自保留宿主特化(trae
|
|
8
|
+
> 改权威源(`templates/_agents/{commands,roles}/*.md`)后薄适配(`modules/hosts/<h>/{agents,commands}/*.md`)要同步——薄适配正文 = 权威源正文,frontmatter 各自保留宿主特化(commands 层文件名 = 宿主命令名,opencode / trae 同用 `wf-` 前缀,单源见 `src/profiles.mjs#HOSTS.commandPrefix`;trae 另有 `name: wf-X`,zcode/omp 各自原描述)。
|
|
9
9
|
> 装户视角:`flow-kit doctor` §7.x 跑同名检查(仅装户,包源环境跳过——包源下永远是 drift)。
|
|
10
10
|
|
|
11
11
|
## 何时跑
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-flow-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "AI-Native 闭环工作流 + wiki 知识层脚手架——flow-kit init 一条命令为新项目装好 .agents 工作流引擎(阶段命令/角色契约/门禁钩子/kb 检索/看板/规则面预算)+ workflow 文档闭环 + wiki 知识库,装完自包含、零运行时依赖",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
package/src/doctor.mjs
CHANGED
|
@@ -9,6 +9,8 @@ import { isOwned, HOSTS, pickStackVars } from './profiles.mjs';
|
|
|
9
9
|
|
|
10
10
|
// 宿主目录映射(与 src/profiles.mjs#HOSTS 同源:装副本目录名前缀带点)
|
|
11
11
|
const HOST_DIR = Object.fromEntries(Object.entries(HOSTS).map(([k, v]) => [k, v.dir]));
|
|
12
|
+
// 命令层薄适配宿主:[装副本目录名, 文件名前缀](单源 profiles.mjs#HOSTS.commandPrefix;与 sync-hosts pairsFor 同口径)
|
|
13
|
+
const HOST_CMD_DIR = Object.entries(HOSTS).filter(([, v]) => v.commandPrefix).map(([k, v]) => [v.dir, v.commandPrefix]);
|
|
12
14
|
|
|
13
15
|
function sh(cmd, cwd) {
|
|
14
16
|
try {
|
|
@@ -209,7 +211,8 @@ export function doctor(args, pkgRoot) {
|
|
|
209
211
|
// 6.7 跨宿主薄适配正文段漂移校验(2026-09-25 cross-host-sync,参见 workflows/intents/2026-09-25-cross-host-sync.md)
|
|
210
212
|
// 装户侧视角:.agents/{commands,roles}/*.md 是权威源;4 宿主目录(.zcode/.omp/.opencode/.trae)下的
|
|
211
213
|
// {agents,commands}/*.md 是薄适配。按正文段 sha 比对(B-b 语义,frontmatter 不计入漂移),
|
|
212
|
-
//
|
|
214
|
+
// commands 层按 profiles.mjs#HOSTS.commandPrefix 前缀映射(2026-09-28 起 opencode/trae 同 wf-)。
|
|
215
|
+
// 首次引入为 WARN(沿用 wf-runtime 复盘「先 WARN 升级 FAIL」路径),
|
|
213
216
|
// 待装户升级视反馈升级 FAIL。包源侧对应工具是 flow-kit sync-hosts(src/sync-hosts.mjs)。
|
|
214
217
|
const adapterRes = checkAdapterDrift(target);
|
|
215
218
|
if (adapterRes.skipped) {
|
|
@@ -341,7 +344,8 @@ export function checkOwnedDrift(target) {
|
|
|
341
344
|
// 跨宿主薄适配正文段漂移校验(独立 export 供 doctor 主流程 + 单元测试共用;2026-09-25 cross-host-sync)
|
|
342
345
|
// 装户侧视角:.agents/{commands,roles}/*.md 是权威源;4 宿主目录(zcode→.zcode、omp→.omp、
|
|
343
346
|
// opencode→.opencode、trae→.trae)下 {agents,commands}/*.md 是薄适配。按正文段 sha 比对
|
|
344
|
-
// (B-b 语义;frontmatter 不计入漂移);
|
|
347
|
+
// (B-b 语义;frontmatter 不计入漂移);commands 层映射走 HOSTS.commandPrefix 前缀(2026-09-28 起
|
|
348
|
+
// opencode 与 trae 同为 wf- 前缀,单源见 profiles.mjs;roles 层映射全宿主无前缀)。
|
|
345
349
|
// 返回:{ drift, total, skipped, note? }
|
|
346
350
|
// - skipped=true:权威源目录不存在 / 包源环境 / 无 .md 文件
|
|
347
351
|
// - drift:权威源正文段与薄适配正文段 sha 不一致的对数
|
|
@@ -373,10 +377,11 @@ export function checkAdapterDrift(target) {
|
|
|
373
377
|
const authRel = `${sub}/${f}`;
|
|
374
378
|
const authSha = bodySha(path.join(authorityRoot, authRel));
|
|
375
379
|
const name = f.slice(0, -'.md'.length);
|
|
376
|
-
// 构建映射:commands/* →
|
|
380
|
+
// 构建映射:commands/* → 每个带 commandPrefix 的宿主 commands/<prefix><name>.md(单源 profiles.mjs#HOSTS);
|
|
381
|
+
// roles/* → 4 宿主 agents/<name>.md(无前缀)
|
|
377
382
|
const targets = [];
|
|
378
383
|
if (sub === 'commands') {
|
|
379
|
-
targets.push([
|
|
384
|
+
for (const [dir, prefix] of HOST_CMD_DIR) targets.push([dir, 'commands', `${prefix}${name}.md`]);
|
|
380
385
|
} else {
|
|
381
386
|
for (const h of Object.keys(HOST_DIR)) targets.push([HOST_DIR[h], 'agents', `${name}.md`]);
|
|
382
387
|
}
|
package/src/profiles.mjs
CHANGED
|
@@ -6,10 +6,14 @@
|
|
|
6
6
|
// 配置的项目自动跳过,不 fail-closed 误拦。none = 不配置(项目日后自填)。
|
|
7
7
|
export const HOSTS = {
|
|
8
8
|
zcode: { dir: '.zcode', localOnly: true },
|
|
9
|
-
opencode: { dir: '.opencode', localOnly: false },
|
|
10
|
-
trae: { dir: '.trae', localOnly: false },
|
|
9
|
+
opencode: { dir: '.opencode', localOnly: false, commandPrefix: 'wf-' },
|
|
10
|
+
trae: { dir: '.trae', localOnly: false, commandPrefix: 'wf-' },
|
|
11
11
|
omp: { dir: '.omp', localOnly: true },
|
|
12
12
|
};
|
|
13
|
+
// commandPrefix = 该宿主的命令层薄适配文件名(= 宿主命令名)统一前缀,2026-09-28 opencode-cmd-wf-prefix
|
|
14
|
+
// 与 trae 对齐后单源:命令名不再裸占宿主顶层命名空间(撞宿主内置/第三方命令无从规避);缺该字段的宿主
|
|
15
|
+
// 表示只有角色层适配、不参与 commands 权威源映射(zcode/omp)。sync-hosts(包源侧)与 doctor §7.x
|
|
16
|
+
// (装户侧)两份实现均从本表派生,新增命令层宿主只改这一处。
|
|
13
17
|
|
|
14
18
|
const ESLINT_CONFIGS = [
|
|
15
19
|
'eslint.config.js', 'eslint.config.mjs', 'eslint.config.cjs',
|
|
@@ -78,10 +82,23 @@ export function pickStackVars(stackKey) {
|
|
|
78
82
|
|
|
79
83
|
// 两态文件模型的归属判定:managed(随包升级)之外皆 owned(项目内容,sync 永不覆盖)
|
|
80
84
|
// local-pre-commit 是项目门禁挂载点——一旦接线即含项目内容,按 owned 起步(旧台账里若在 managed,改动后 sync 也会保守跳过)
|
|
85
|
+
// metric-claims.txt 是**项目自持的指标登记表**(2026-09-28 metric-claim-gate 复核 P2 更正):装户按
|
|
86
|
+
// 设计要在其中新增自有指标,若按 managed 记账则 sync 每次都报「本地已改、升级时需合并」,且将来包侧
|
|
87
|
+
// 新增内置指标**永远下不来**(台账停在包侧基线)。故归 owned——随包附带一份起步内容,此后归项目。
|
|
88
|
+
// metric-derivers.cjs 是**项目自持的取数器模块**(2026-09-28 adopter-derivers):它正是上面那份登记表的
|
|
89
|
+
// 扩展点——装户在此实现自有指标的取数逻辑。**必须与 metric-claims.txt 同归 owned**:若落 managed,
|
|
90
|
+
// 装户一用该功能就会让 sync 永久报「本地已改」、doctor WARN,并因供应链防线使 check-loop 跳过执行
|
|
91
|
+
// (门禁静默停摆)——那正是本单要修的缺陷本身(同名 incident 有实测链路)。
|
|
92
|
+
// **后缀为 .cjs 而非 .mjs**(2026-09-28 跨版本实测修正):首版用 .mjs + require(esm),实测在
|
|
93
|
+
// Node 18.20 / 20.18 / 22.11 全部 ERR_REQUIRE_ESM(只有 ≥20.19/≥22.12 可用),而本仓 engines
|
|
94
|
+
// 写 >=18.0.0、CI 跑 18/22 → 装户在 CI 目标版本上根本用不了。改 .cjs 后 createRequire 同步载入
|
|
95
|
+
// 全线可用。旧 .mjs 路径同样登记为 owned(免装户残留旧文件时落 managed 重演缺陷)。
|
|
81
96
|
export function isOwned(rel) {
|
|
82
97
|
return rel === 'AGENTS.md' || rel === '.gitattributes' || rel.startsWith('workflow/') || rel.startsWith('wiki/')
|
|
83
98
|
|| rel.startsWith('.agents/notes/')
|
|
84
99
|
|| rel === '.agents/workflow-modules.txt' || rel === '.agents/rule-budgets.txt'
|
|
100
|
+
|| rel === '.agents/metric-claims.txt' || rel === '.agents/metric-derivers.cjs'
|
|
101
|
+
|| rel === '.agents/metric-derivers.mjs'
|
|
85
102
|
|| rel === '.agents/hooks/local-pre-commit';
|
|
86
103
|
}
|
|
87
104
|
|
package/src/sync-hosts.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// flow-kit sync-hosts:跨宿主适配层正文段漂移检查 + 单向同步(B-b 方案,2026-09-25)
|
|
2
2
|
// 目的:改权威源(templates/_agents/{commands,roles}/*.md)正文要同步到 N 份薄适配
|
|
3
3
|
// (modules/hosts/<h>/{agents,commands}/*.md)的对应正文段;薄适配 frontmatter 保留宿主特化字段
|
|
4
|
-
// (trae
|
|
4
|
+
// (commands 层 trae 侧另有 name: wf-X,opencode/zcode/omp 各自原描述),不在同步范围。
|
|
5
5
|
// B-b 语义:sha 比对与 apply 只动正文段,frontmatter 双方各自维护。
|
|
6
|
+
// 映射单源在 profiles.mjs#HOSTS.commandPrefix(命令层薄适配文件名前缀;2026-09-28 opencode 对齐 trae 的 wf- 前缀)。
|
|
6
7
|
// 算法:diffHosts 抽为 pure function(authorityRoot + adaptersRoot 两个根);
|
|
7
8
|
// sync-hosts CLI 在 pkgRoot 跑(包源视角);doctor §7.x 在 target 跑(装户视角)——同源逻辑。
|
|
8
9
|
import fs from 'node:fs';
|
|
@@ -41,14 +42,17 @@ function listMd(dir) {
|
|
|
41
42
|
return fs.readdirSync(dir).filter((f) => f.endsWith('.md'));
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
// pairsFor:权威源文件 → 薄适配 rel
|
|
45
|
+
// pairsFor:权威源文件 → 薄适配 rel 列表(映射单源见 profiles.mjs#HOSTS.commandPrefix)
|
|
46
|
+
// commands 权威源 → 带 commandPrefix 的宿主各一份(<dir>/commands/<prefix><name>.md)
|
|
47
|
+
// roles 权威源 → 全部宿主 agents 各一份
|
|
45
48
|
function pairsFor(authRel) {
|
|
46
49
|
const out = [];
|
|
47
50
|
const norm = authRel.split(path.sep).join('/');
|
|
48
51
|
if (norm.startsWith('commands/') && norm.endsWith('.md')) {
|
|
49
52
|
const name = norm.slice('commands/'.length, -'.md'.length);
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
for (const [h, v] of Object.entries(HOSTS)) {
|
|
54
|
+
if (v.commandPrefix) out.push({ adapterRel: `${h}/commands/${v.commandPrefix}${name}.md` });
|
|
55
|
+
}
|
|
52
56
|
} else if (norm.startsWith('roles/') && norm.endsWith('.md')) {
|
|
53
57
|
const name = norm.slice('roles/'.length, -'.md'.length);
|
|
54
58
|
for (const h of Object.keys(HOSTS)) out.push({ adapterRel: `${h}/agents/${name}.md` });
|
|
@@ -104,8 +108,10 @@ export function diffHosts({ authorityRoot, adaptersRoot }) {
|
|
|
104
108
|
const adapterRel = `${h}/${sub}/${f}`;
|
|
105
109
|
if (adapterSeen.has(adapterRel)) continue;
|
|
106
110
|
let authGuess;
|
|
107
|
-
|
|
108
|
-
|
|
111
|
+
const prefix = HOSTS[h] && HOSTS[h].commandPrefix;
|
|
112
|
+
if (sub === 'commands' && prefix && f.startsWith(prefix)) {
|
|
113
|
+
// 命令层薄适配带宿主 commandPrefix(2026-09-28:opencode 对齐 trae 后两宿主同口径)
|
|
114
|
+
authGuess = `commands/${f.slice(prefix.length)}`;
|
|
109
115
|
} else if (sub === 'agents') {
|
|
110
116
|
authGuess = `roles/${f}`;
|
|
111
117
|
} else {
|
package/src/sync-hosts.test.mjs
CHANGED
|
@@ -31,12 +31,12 @@ function mkFixture() {
|
|
|
31
31
|
`---\ndescription: Test 阶段\nstage: Test\n---\n\n# Test · v1\n\n权威源正文 test v1\n`);
|
|
32
32
|
W(path.join(root, 'templates/_agents/roles/implementer.md'),
|
|
33
33
|
`---\ndescription: Implementer 角色契约\n---\n\n# Implementer · v1\n\n权威源正文 implementer v1\n`);
|
|
34
|
-
// 薄适配:commands 权威源 → opencode + trae
|
|
35
|
-
W(path.join(root, 'modules/hosts/opencode/commands/build.md'),
|
|
34
|
+
// 薄适配:commands 权威源 → opencode + trae(两宿主同为 wf- 前缀;trae 另有 name 字段)
|
|
35
|
+
W(path.join(root, 'modules/hosts/opencode/commands/wf-build.md'),
|
|
36
36
|
`---\ndescription: Build · opencode 风格\n---\n\n# Build · v1\n\n权威源正文 build v1\n`);
|
|
37
37
|
W(path.join(root, 'modules/hosts/trae/commands/wf-build.md'),
|
|
38
38
|
`---\nname: wf-build\ndescription: Build · trae 风格\n---\n\n# Build · v1\n\n权威源正文 build v1\n`);
|
|
39
|
-
W(path.join(root, 'modules/hosts/opencode/commands/test.md'),
|
|
39
|
+
W(path.join(root, 'modules/hosts/opencode/commands/wf-test.md'),
|
|
40
40
|
`---\ndescription: Test · opencode 风格\n---\n\n# Test · v1\n\n权威源正文 test v1\n`);
|
|
41
41
|
W(path.join(root, 'modules/hosts/trae/commands/wf-test.md'),
|
|
42
42
|
`---\nname: wf-test\ndescription: Test · trae 风格\n---\n\n# Test · v1\n\n权威源正文 test v1\n`);
|
|
@@ -49,8 +49,9 @@ function mkFixture() {
|
|
|
49
49
|
`---\nname: implementer\ndescription: opencode 英文风格\n---\n\n# Implementer · v1\n\n权威源正文 implementer v1\n`);
|
|
50
50
|
W(path.join(root, 'modules/hosts/trae/agents/implementer.md'),
|
|
51
51
|
`---\nname: implementer\ndescription: trae 英文风格\n---\n\n# Implementer · v1\n\n权威源正文 implementer v1\n`);
|
|
52
|
-
//
|
|
52
|
+
// 孤儿薄适配:权威源无对应文件(两种形态——带 wf- 前缀与不带前缀)
|
|
53
53
|
W(path.join(root, 'modules/hosts/opencode/commands/orphan.md'), '# orphan\n');
|
|
54
|
+
W(path.join(root, 'modules/hosts/opencode/commands/wf-legacy.md'), '# legacy\n');
|
|
54
55
|
return root;
|
|
55
56
|
}
|
|
56
57
|
|
|
@@ -84,7 +85,7 @@ syncHosts(process.argv.slice(2), root);
|
|
|
84
85
|
const r = runSyncHosts(fx, ['--diff']);
|
|
85
86
|
const out = r.stdout + r.stderr;
|
|
86
87
|
check('S2 权威源改了 → 正文漂移 4(build×2 + test×2)', /正文段漂移(4/.test(out), out);
|
|
87
|
-
check('S2 列出 opencode/commands/build.md', out.includes('commands/build.md → opencode/commands/build.md'), out);
|
|
88
|
+
check('S2 列出 opencode/commands/wf-build.md(opencode wf- 前缀映射)', out.includes('commands/build.md → opencode/commands/wf-build.md'), out);
|
|
88
89
|
check('S2 列出 trae/commands/wf-build.md(trae wf- 前缀映射)', out.includes('commands/build.md → trae/commands/wf-build.md'), out);
|
|
89
90
|
check('S2 正文对齐 = 4(1 role × 4 宿主 implementer 未改)', /正文对齐:\s*4\s*对/.test(out), out);
|
|
90
91
|
check('S2 exit 0(diff 模式不阻断)', r.status === 0);
|
|
@@ -98,9 +99,9 @@ syncHosts(process.argv.slice(2), root);
|
|
|
98
99
|
const r = runSyncHosts(fx, ['--apply']);
|
|
99
100
|
const out = r.stdout + r.stderr;
|
|
100
101
|
check('S3 apply:报告同步 2 份(opencode + trae)', /按权威源正文覆盖薄适配正文段\s*2\s*份/.test(out), out);
|
|
101
|
-
check('S3 apply:opencode 正文已同步到 v2', R(path.join(fx, 'modules/hosts/opencode/commands/build.md')).includes('权威源正文 build v2'));
|
|
102
|
+
check('S3 apply:opencode 正文已同步到 v2', R(path.join(fx, 'modules/hosts/opencode/commands/wf-build.md')).includes('权威源正文 build v2'));
|
|
102
103
|
check('S3 apply:trae 正文已同步到 v2', R(path.join(fx, 'modules/hosts/trae/commands/wf-build.md')).includes('权威源正文 build v2'));
|
|
103
|
-
check('S3 apply:opencode frontmatter 保留(仍无 name
|
|
104
|
+
check('S3 apply:opencode frontmatter 保留(仍无 name 字段——前缀由文件名承载)', !R(path.join(fx, 'modules/hosts/opencode/commands/wf-build.md')).match(/^name:/m));
|
|
104
105
|
check('S3 apply:trae frontmatter 保留(仍含 name: wf-build)', R(path.join(fx, 'modules/hosts/trae/commands/wf-build.md')).match(/^name:\s*wf-build/m) !== null);
|
|
105
106
|
check('S3 apply:权威源不动', R(path.join(fx, 'templates/_agents/commands/build.md')).includes('权威源正文 build v2'));
|
|
106
107
|
check('S3 apply:再次 diff 应显示对齐 8', true); // 二次扫描由 sync-hosts 内部做
|
|
@@ -136,12 +137,12 @@ syncHosts(process.argv.slice(2), root);
|
|
|
136
137
|
// ============ 场景 6:薄适配文件不存在 → authorityMissing 列出,apply 不创建 ============
|
|
137
138
|
{
|
|
138
139
|
const fx = mkFixture();
|
|
139
|
-
fs.rmSync(path.join(fx, 'modules/hosts/opencode/commands/build.md'));
|
|
140
|
+
fs.rmSync(path.join(fx, 'modules/hosts/opencode/commands/wf-build.md'));
|
|
140
141
|
const r = runSyncHosts(fx, ['--diff']);
|
|
141
142
|
const out = r.stdout + r.stderr;
|
|
142
|
-
check('S6 薄适配缺失 → authorityMissing 列', /权威源声明但薄适配缺失(/.test(out) && out.includes('opencode/commands/build.md'), out);
|
|
143
|
+
check('S6 薄适配缺失 → authorityMissing 列', /权威源声明但薄适配缺失(/.test(out) && out.includes('opencode/commands/wf-build.md'), out);
|
|
143
144
|
const r2 = runSyncHosts(fx, ['--apply']);
|
|
144
|
-
check('S6 apply 不自动创建薄适配(仍缺失)', !fs.existsSync(path.join(fx, 'modules/hosts/opencode/commands/build.md')));
|
|
145
|
+
check('S6 apply 不自动创建薄适配(仍缺失)', !fs.existsSync(path.join(fx, 'modules/hosts/opencode/commands/wf-build.md')));
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
// ============ 场景 7:--json 机器可读输出 ============
|
|
@@ -162,8 +163,10 @@ syncHosts(process.argv.slice(2), root);
|
|
|
162
163
|
const r = runSyncHosts(fx, ['--diff']);
|
|
163
164
|
const out = r.stdout + r.stderr;
|
|
164
165
|
check('S8 孤儿薄适配 → adapterOrphans 列', /孤儿薄适配(/.test(out) && out.includes('opencode/commands/orphan.md'), out);
|
|
166
|
+
check('S8 opencode 孤儿带 wf- 前缀 → 反推权威源去掉前缀', out.includes('opencode/commands/wf-legacy.md(推测权威源:commands/legacy.md)'), out);
|
|
167
|
+
check('S8 opencode 孤儿不带前缀 → 原样反推 commands/<file>', out.includes('opencode/commands/orphan.md(推测权威源:commands/orphan.md)'), out);
|
|
165
168
|
const r2 = runSyncHosts(fx, ['--apply']);
|
|
166
|
-
check('S8 apply 不删孤儿薄适配', fs.existsSync(path.join(fx, 'modules/hosts/opencode/commands/orphan.md')));
|
|
169
|
+
check('S8 apply 不删孤儿薄适配', fs.existsSync(path.join(fx, 'modules/hosts/opencode/commands/orphan.md')) && fs.existsSync(path.join(fx, 'modules/hosts/opencode/commands/wf-legacy.md')));
|
|
167
170
|
}
|
|
168
171
|
|
|
169
172
|
console.log(`\n合计: PASS ${pass} / FAIL ${failCount}`);
|
package/templates/AGENTS.md
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
### 门禁与提交
|
|
25
25
|
|
|
26
|
-
- `.githooks/`(经 `core.hooksPath` 挂载,本地 clone 后执行 `git config core.hooksPath .githooks`):pre-commit(闭环配对 / wiki 台账 / 规则面预算 / 敏感信息扫描与条件构建)、pre-push(闭环断档扫描)、commit-msg、post-commit、pre-merge-commit。
|
|
26
|
+
- `.githooks/`(经 `core.hooksPath` 挂载,本地 clone 后执行 `git config core.hooksPath .githooks`):pre-commit(闭环配对 / wiki 台账 / 规则面预算 / 双源一致性 / managed 台账快检 / 敏感信息扫描与条件构建)、pre-push(闭环断档扫描)、commit-msg、post-commit、pre-merge-commit。
|
|
27
27
|
- **`git commit` / `git merge` / `git push` 三处一律禁 `--no-verify`**——被拦说明产出不合规,按提示修完原路重试。
|
|
28
28
|
- 项目专属门禁挂 `.agents/hooks/local-pre-commit`。
|
|
29
29
|
- 提交遵循 Conventional Commits 中文(feat / fix / docs / style / refactor / perf);L1+ 入口文档 / spec / plan 随代码同一提交;确认(approved)后立即 `docs(*)` 单独提交留痕。
|
|
@@ -17,7 +17,7 @@ next: .agents/commands/build.md(漂移属 build 改权威源未同步导致的
|
|
|
17
17
|
|
|
18
18
|
# Sync-Hosts · 跨宿主适配层正文段同步
|
|
19
19
|
|
|
20
|
-
> 改权威源(`templates/_agents/{commands,roles}/*.md`)后薄适配(`modules/hosts/<h>/{agents,commands}/*.md`)要同步——薄适配正文 = 权威源正文,frontmatter 各自保留宿主特化(trae
|
|
20
|
+
> 改权威源(`templates/_agents/{commands,roles}/*.md`)后薄适配(`modules/hosts/<h>/{agents,commands}/*.md`)要同步——薄适配正文 = 权威源正文,frontmatter 各自保留宿主特化(commands 层文件名 = 宿主命令名,opencode / trae 同用 `wf-` 前缀,单源见 `src/profiles.mjs#HOSTS.commandPrefix`;trae 另有 `name: wf-X`,zcode/omp 各自原描述)。
|
|
21
21
|
> 装户视角:`flow-kit doctor` §7.x 跑同名检查(仅装户,包源环境跳过——包源下永远是 drift)。
|
|
22
22
|
|
|
23
23
|
## 何时跑
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# 量化断言「指标签名」单源登记表(check-loop 检查 16 共读)
|
|
2
|
+
#
|
|
3
|
+
# 背景(2026-09-28 claim-exceeds-fix):结论文档里跨文档引用的量化值若各自手写,
|
|
4
|
+
# 极易在事实变更后不同步——本仓已实证:同一指标在一处改了、另一处没改,
|
|
5
|
+
# 而「五处文件同步」这句话本身无人核对(该单真正被门禁抓住的方式,是事后人工复核)。
|
|
6
|
+
# 本表把「哪个指标、当前取值多少」收敛为单源,供 check-loop 检查 16 对账。
|
|
7
|
+
#
|
|
8
|
+
# 用法:活跃态文档(draft/approved/open)引用某个已登记指标时,按**签名形态**书写,
|
|
9
|
+
# 检查 16 会实时取数比对。签名形态:\{\{指标名\}\}(双花括号包裹指标名,小写点分)。
|
|
10
|
+
# 例(下面是「讲语法」,故加反斜杠转义;**写真断言时不加反斜杠**):
|
|
11
|
+
# 本仓台账共 \{\{ledger.lines\}\} 行,其中无 batch 字段 \{\{ledger.linesWithoutBatch\}\} 行。
|
|
12
|
+
# 写作时**不要手写数字**——写完跑 check-loop,工具会把实时值填进提示里供你替换。
|
|
13
|
+
# 转义按「逐个出现」生效:同一行里 \{\{x\}\} 是示意、\{\{y\}\} 是真断言时,只豁免前者。
|
|
14
|
+
# (本例的指标名曾写成 ledger.total / ledger.noBatch 两个**并不存在**的名字——本表不被
|
|
15
|
+
# 检查 16 扫描(它是 .txt 非 .md),故这类笔误不会被自动抓到,只能靠复核。已据实更正。)
|
|
16
|
+
#
|
|
17
|
+
# 为什么用显式签名而非全文数字扫描(据实说明,非偷懒):
|
|
18
|
+
# 本仓活跃文档中「N 行 / N 条 / N 份」类表述有 49 处(实测:活跃态 21 份文档逐行计),绝大多数是**历史叙述**
|
|
19
|
+
# (描述某次提交当时的规模,天然不会随当前事实变化)。全文扫描会对这些正确的
|
|
20
|
+
# 历史叙述产生大量假阳性——而 check-loop.mjs 已把「无判定依据的行不产出不可
|
|
21
|
+
# 消除噪声」立为红线(沿 2026-09-27 audit-gate-hardening P3 教训:噪声淹没真漏点)。
|
|
22
|
+
# 故本检查**只校验显式声明的签名**:假阳性恒为 0,代价是覆盖面依赖作者登记。
|
|
23
|
+
#
|
|
24
|
+
# 格式:每行 `<指标名> = <取数表达式>`;`#` 开头为注释;空行忽略。
|
|
25
|
+
# 取数表达式(闭集,全部本地确定性、零网络):
|
|
26
|
+
# ledger.lines 台账 .agents/confirmations.jsonl 的非空行数
|
|
27
|
+
# ledger.linesWithoutBatch 台账中不含 "batch" 字段的行数(schema 演进前历史行)
|
|
28
|
+
# ledger.linesWithBatch 台账中含 "batch" 字段的行数
|
|
29
|
+
# docs.count.<sub> workflow/<sub> 下文档份数——**与文档扫描同口径**:以数字开头的
|
|
30
|
+
# .md、排除 _TEMPLATE.md、且仓库模式下只计 HEAD 已跟踪件
|
|
31
|
+
# (故并行会话的未跟踪新档不计入);sub ∈ intents/specs/plans/incidents
|
|
32
|
+
# docs.count.all 上述四类合计
|
|
33
|
+
# ── 新增指标(2026-09-28 adopter-derivers 起)────────────────────────────────
|
|
34
|
+
# 分两层取数,**内置优先**:
|
|
35
|
+
# ① 引擎内置 8 个指标(下表)——装户零配置即用,不需要做任何事。
|
|
36
|
+
# ② 项目自有指标——写 `.agents/metric-derivers.cjs`(**owned**,sync 永不覆盖),
|
|
37
|
+
# 用 CommonJS 导出 `derivers` 对象(指标名 → 取数函数),并在此表加同名一行。
|
|
38
|
+
# **后缀必须是 `.cjs`、写法必须是 CommonJS**(`module.exports`)。
|
|
39
|
+
# 为什么不用 `.mjs`:引擎用 `require()` 同步载入,而 `require(esm)` 仅在
|
|
40
|
+
# Node ≥20.19 / ≥22.12 可用——**实测 Node 18.20 / 20.18 / 22.11 全部
|
|
41
|
+
# ERR_REQUIRE_ESM**,而本仓 engines 声明 `>=18.0.0`。故 `.cjs` 是唯一
|
|
42
|
+
# 全版本可用的形态(同步、零版本依赖)。
|
|
43
|
+
# 旧的 `.mjs` 路径已弃用:若残留在盘上,检查 16 会**明确提示迁移**。
|
|
44
|
+
#
|
|
45
|
+
# 示例 `.agents/metric-derivers.cjs`:
|
|
46
|
+
# module.exports = {
|
|
47
|
+
# derivers: {
|
|
48
|
+
# 'my.migrations': (ctx) => ctx.glob('backend/migrations/*.sql').length,
|
|
49
|
+
# 'my.apiCount': (ctx) => ctx.countFiles('src/api', (f) => f.endsWith('.controller.ts')),
|
|
50
|
+
# },
|
|
51
|
+
# };
|
|
52
|
+
# ctx 提供(同步,免你重复造轮子;其余请自行 `require('node:fs')`):
|
|
53
|
+
# ctx.root 仓库根绝对路径
|
|
54
|
+
# ctx.read(rel) 读文本文件(LF 归一),不存在返回 ''
|
|
55
|
+
# ctx.glob(pattern) 极简 glob(* / **),返回相对路径数组
|
|
56
|
+
# ctx.countFiles(dir, pred) 递归计数(可传谓词)
|
|
57
|
+
#
|
|
58
|
+
# 三条硬约定(违反即 fail-loud):
|
|
59
|
+
# 1. **必须是同步函数**(返回 number,不得 async/Promise)——本检查零网络、要求本地确定性
|
|
60
|
+
# 2. 必须**确定性**:不得依赖网络、不得依赖当前时间
|
|
61
|
+
# 3. 返回值须为**有限数字**(Number.isFinite)
|
|
62
|
+
#
|
|
63
|
+
# 失败语义(**绝不静默降级**):模块语法错 / 抛异常 / 未导出 derivers / 返回值非法
|
|
64
|
+
# → 一律出 `[WARN 装户取数器载入失败]` 或 `[WARN 指标取数失败]`,**不会**退化成
|
|
65
|
+
# 「只用内置指标」——否则你会误以为「忘了登记」而查不到真因。
|
|
66
|
+
# 指标名与内置同名时:**内置优先**,你的定义被忽略(会出 advisory 明示)。
|
|
67
|
+
#
|
|
68
|
+
# 注:本检查**不设取数超时**——请让取数器保持轻量(秒级)。死循环会拖慢门禁。
|
|
69
|
+
# 另:装户模块的「取数器」在引擎侧(check-loop.mjs,managed);本表与 derivers.cjs 同归 owned。
|
|
70
|
+
#
|
|
71
|
+
# 注意:不登记 advisory 总数类指标——它由 check-loop 自身产出(检查 16 是其一部分),
|
|
72
|
+
# 自引用会让「报告本身改变被报告的数」,产生不可稳定对账的循环。
|
|
73
|
+
|
|
74
|
+
ledger.lines = ledger.lines
|
|
75
|
+
ledger.linesWithoutBatch = ledger.linesWithoutBatch
|
|
76
|
+
ledger.linesWithBatch = ledger.linesWithBatch
|
|
77
|
+
docs.count.intents = docs.count.intents
|
|
78
|
+
docs.count.specs = docs.count.specs
|
|
79
|
+
docs.count.plans = docs.count.plans
|
|
80
|
+
docs.count.incidents = docs.count.incidents
|
|
81
|
+
docs.count.all = docs.count.all
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// check-ledger.mjs — managed 台账快检(2026-09-28 ledger-precommit-gate):kit.json 台账 sha ↔ 盘面工作树
|
|
2
|
+
// 补 pre-commit 盲区:双源门禁(source-sync-check)查 templates↔.agents 正文,两侧成对回退时静默;
|
|
3
|
+
// doctor §4 查台账↔盘面但只在手动全量跑——「台账 sha 预 landing」(台账记了盘面尚不存在的内容 sha)
|
|
4
|
+
// 随常规提交静默落 main,三次真实发生(0272a1c 预入 / ff3c721 还原窗口 / 537df18 闭合),本脚本在
|
|
5
|
+
// 提交时刻补齐第三边。sha 口径:LF 归一 utf8(同 doctor §4 / render.mjs shaText / source-sync-check
|
|
6
|
+
// 读取侧,跨 checkout 稳定;装户自包含约束决定独立实现,三处口径改动须同批)。
|
|
7
|
+
// 边界:仅包源环境由 .githooks/pre-commit 触发(装户 managed 手改走 doctor §4 WARN 口径,不硬拦);
|
|
8
|
+
// managed 删除即拦——sync 对 removed 只报不删,删件须手动同步装副本并重跑 sync 刷台账。
|
|
9
|
+
// 用法:node .agents/scripts/check-ledger.mjs(cwd = 仓库根;非零出口即阻断提交)
|
|
10
|
+
// 单元测试:import { ledgerDrift } from './check-ledger.mjs'
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
|
|
15
|
+
export function ledgerDrift(target = process.cwd()) {
|
|
16
|
+
const kitPath = path.join(target, '.agents', 'kit.json');
|
|
17
|
+
if (!fs.existsSync(kitPath)) return { skipped: true, note: '无 .agents/kit.json(非 flow-kit 安装)' };
|
|
18
|
+
let kit;
|
|
19
|
+
try {
|
|
20
|
+
kit = JSON.parse(fs.readFileSync(kitPath, 'utf8'));
|
|
21
|
+
} catch (e) {
|
|
22
|
+
return { skipped: false, fatal: `kit.json 解析失败:${e.message}`, modified: [], gone: [], total: 0, version: '?' };
|
|
23
|
+
}
|
|
24
|
+
if (!Array.isArray(kit.managed)) {
|
|
25
|
+
// fail-loud(独立复核 P2-3,2026-09-28):JSON 合法但 managed 缺失/非数组时静默「全对齐」= 假放,
|
|
26
|
+
// 同 doctor §4 的 Array.isArray 显式判口径,台账结构坏了必须响
|
|
27
|
+
return { skipped: false, fatal: 'kit.json managed 缺失或非数组', modified: [], gone: [], total: 0, version: kit.version };
|
|
28
|
+
}
|
|
29
|
+
const modified = [];
|
|
30
|
+
const gone = [];
|
|
31
|
+
const managed = kit.managed;
|
|
32
|
+
for (const f of managed) {
|
|
33
|
+
const p = path.join(target, f.rel);
|
|
34
|
+
if (!fs.existsSync(p)) { gone.push(f.rel); continue; }
|
|
35
|
+
const h = createHash('sha256').update(fs.readFileSync(p, 'utf8').replace(/\r\n/g, '\n'), 'utf8').digest('hex'); // LF 归一,同 doctor §4
|
|
36
|
+
if (h !== f.sha256) modified.push(f.rel);
|
|
37
|
+
}
|
|
38
|
+
return { skipped: false, fatal: '', modified, gone, total: managed.length, version: kit.version };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const isMain = process.argv[1] && process.argv[1].endsWith('check-ledger.mjs');
|
|
42
|
+
if (isMain) {
|
|
43
|
+
const r = ledgerDrift();
|
|
44
|
+
console.log('▶ flow-kit check-ledger');
|
|
45
|
+
if (r.skipped) {
|
|
46
|
+
console.log(' ' + r.note + ',跳过 ✅');
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
console.log(' kit.json v' + r.version + ':managed ' + r.total + ' 份,台账 ↔ 盘面(工作树,LF 归一)');
|
|
50
|
+
if (r.fatal) console.error('❌ ' + r.fatal);
|
|
51
|
+
if (r.modified.length) {
|
|
52
|
+
console.error(' 台账漂移(盘面 ≠ 台账 sha)' + r.modified.length + ':');
|
|
53
|
+
for (const rel of r.modified) console.error(' - ' + rel);
|
|
54
|
+
}
|
|
55
|
+
if (r.gone.length) {
|
|
56
|
+
console.error(' managed 缺失 ' + r.gone.length + ':');
|
|
57
|
+
for (const rel of r.gone) console.error(' - ' + rel);
|
|
58
|
+
}
|
|
59
|
+
if (r.fatal || r.modified.length || r.gone.length) {
|
|
60
|
+
console.error(' 提交被拦:kit.json 与盘面不一致(预 landing / 手改未随动)——跑 node bin/flow-kit.mjs sync 成对后原路重试');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
console.log(' 全对齐 ✅');
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// check-ledger.test.mjs — managed 台账快检测试(2026-09-28 ledger-precommit-gate)
|
|
2
|
+
// 方法:fixture 临时目录 .agents/kit.json + managed 件,调 ledgerDrift() + spawnSync CLI 出口码;
|
|
3
|
+
// 真实仓库 baseline(包源环境全对齐;装户环境 SKIP 不计失败,沿 source-sync-check 先例)
|
|
4
|
+
// 判据:S1 全对齐 S2 内容漂移逐份列出 S3 缺失 S4 CRLF×LF 归一不误报 S5 无 kit.json skip
|
|
5
|
+
// S6 kit.json 解析失败 fatal S7/S8 CLI 出口码 0/1 S9 真实仓 baseline
|
|
6
|
+
// 用法:node templates/_agents/scripts/check-ledger.test.mjs(装副本直跑亦然)
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import { spawnSync } from 'node:child_process';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { createHash } from 'node:crypto';
|
|
13
|
+
import { ledgerDrift } from './check-ledger.mjs';
|
|
14
|
+
|
|
15
|
+
let pass = 0, failCount = 0;
|
|
16
|
+
function check(name, cond, detail = '') {
|
|
17
|
+
if (cond) { pass++; console.log('PASS ' + name); }
|
|
18
|
+
else { failCount++; console.log('FAIL ' + name + (detail ? '——' + detail : '')); }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// LF 归一 sha(同 check-ledger.mjs / doctor §4 口径)
|
|
22
|
+
const lfSha = (s) => createHash('sha256').update(s.replace(/\r\n/g, '\n'), 'utf8').digest('hex');
|
|
23
|
+
|
|
24
|
+
function mkFixture(managed, entries, kitRaw) {
|
|
25
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-cl-'));
|
|
26
|
+
for (const [rel, content] of Object.entries(managed)) {
|
|
27
|
+
fs.mkdirSync(path.join(root, path.dirname(rel)), { recursive: true });
|
|
28
|
+
fs.writeFileSync(path.join(root, rel), content);
|
|
29
|
+
}
|
|
30
|
+
fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
|
|
31
|
+
fs.writeFileSync(path.join(root, '.agents', 'kit.json'),
|
|
32
|
+
kitRaw !== undefined ? kitRaw : JSON.stringify({ kit: 't', version: '0.0.0-test', managed: entries }));
|
|
33
|
+
return root;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const SCRIPT = path.join(path.dirname(fileURLToPath(import.meta.url)), 'check-ledger.mjs');
|
|
37
|
+
const runCli = (cwd) => spawnSync(process.execPath, [SCRIPT], { encoding: 'utf8', cwd });
|
|
38
|
+
|
|
39
|
+
// ---- S1 全对齐 → 无漂移无缺失 ----
|
|
40
|
+
{
|
|
41
|
+
const root = mkFixture({ '.agents/scripts/a.mjs': 'A v1\n' }, [{ rel: '.agents/scripts/a.mjs', sha256: lfSha('A v1\n') }]);
|
|
42
|
+
const r = ledgerDrift(root);
|
|
43
|
+
check('S1 全对齐 → modified=0 gone=0 total=1', !r.skipped && r.modified.length === 0 && r.gone.length === 0 && r.total === 1, JSON.stringify(r));
|
|
44
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---- S2 内容漂移 → 逐份列出 rel ----
|
|
48
|
+
{
|
|
49
|
+
const root = mkFixture({ '.agents/scripts/a.mjs': 'A 改了\n' }, [{ rel: '.agents/scripts/a.mjs', sha256: lfSha('A v1\n') }]);
|
|
50
|
+
const r = ledgerDrift(root);
|
|
51
|
+
check('S2 内容漂移 → modified=[a.mjs]', !r.skipped && r.modified.length === 1 && r.modified[0] === '.agents/scripts/a.mjs' && r.gone.length === 0, JSON.stringify(r));
|
|
52
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---- S3 managed 缺失 → gone ----
|
|
56
|
+
{
|
|
57
|
+
const root = mkFixture({}, [{ rel: '.agents/scripts/gone.mjs', sha256: lfSha('x') }]);
|
|
58
|
+
const r = ledgerDrift(root);
|
|
59
|
+
check('S3 缺失 → gone=[gone.mjs]', !r.skipped && r.gone.length === 1 && r.gone[0] === '.agents/scripts/gone.mjs' && r.modified.length === 0, JSON.stringify(r));
|
|
60
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---- S4 CRLF 盘面 × LF 台账 → 归一不误报 ----
|
|
64
|
+
{
|
|
65
|
+
const root = mkFixture({ '.agents/scripts/crlf.mjs': 'B v1\r\nline2\r\n' }, [{ rel: '.agents/scripts/crlf.mjs', sha256: lfSha('B v1\nline2\n') }]);
|
|
66
|
+
const r = ledgerDrift(root);
|
|
67
|
+
check('S4 CRLF 盘面 × LF 台账 → modified=0(归一口径)', !r.skipped && r.modified.length === 0 && r.gone.length === 0, JSON.stringify(r));
|
|
68
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---- S5 无 kit.json → skipped(非 flow-kit 安装兼容) ----
|
|
72
|
+
{
|
|
73
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-cl-'));
|
|
74
|
+
const r = ledgerDrift(root);
|
|
75
|
+
check('S5 无 kit.json → skipped=true', r.skipped === true && typeof r.note === 'string', JSON.stringify(r));
|
|
76
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---- S6 kit.json 解析失败 → fatal ----
|
|
80
|
+
{
|
|
81
|
+
const root = mkFixture({}, [], '{ kit: 坏 json');
|
|
82
|
+
const r = ledgerDrift(root);
|
|
83
|
+
check('S6 解析失败 → fatal 非空', !r.skipped && typeof r.fatal === 'string' && r.fatal.includes('解析失败'), JSON.stringify(r));
|
|
84
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---- S6b JSON 合法但 managed 缺失/非数组 → fatal(不静默全对齐;独立复核 P2-3) ----
|
|
88
|
+
{
|
|
89
|
+
const root = mkFixture({}, [], JSON.stringify({ kit: 't', version: '0.0.0-test' }));
|
|
90
|
+
const r = ledgerDrift(root);
|
|
91
|
+
check('S6b managed 非数组 → fatal 非空(fail-loud)', !r.skipped && typeof r.fatal === 'string' && r.fatal.includes('managed'), JSON.stringify(r));
|
|
92
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---- S7 CLI 干净 fixture → exit 0 + 全对齐 ----
|
|
96
|
+
{
|
|
97
|
+
const root = mkFixture({ '.agents/scripts/a.mjs': 'A v1\n' }, [{ rel: '.agents/scripts/a.mjs', sha256: lfSha('A v1\n') }]);
|
|
98
|
+
const p = runCli(root);
|
|
99
|
+
check('S7 CLI 全对齐 → exit 0 + 「全对齐」', p.status === 0 && p.stdout.includes('全对齐'), `status=${p.status} out=${p.stdout}`);
|
|
100
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---- S8 CLI 漂移 fixture → exit 1 + 列出 rel + sync 提示 ----
|
|
104
|
+
{
|
|
105
|
+
const root = mkFixture({ '.agents/scripts/a.mjs': 'A 改了\n' }, [{ rel: '.agents/scripts/a.mjs', sha256: lfSha('A v1\n') }]);
|
|
106
|
+
const p = runCli(root);
|
|
107
|
+
check('S8 CLI 漂移 → exit 1 + rel + sync 提示', p.status === 1 && p.stderr.includes('.agents/scripts/a.mjs') && p.stderr.includes('sync'), `status=${p.status} err=${p.stderr}`);
|
|
108
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---- S9 真实仓 baseline:包源环境台账↔盘面全对齐;装户环境 SKIP 不计失败 ----
|
|
112
|
+
{
|
|
113
|
+
let srcRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
114
|
+
for (let i = 0; i < 5 && srcRoot; i++) {
|
|
115
|
+
if (fs.existsSync(path.join(srcRoot, 'templates', '_agents'))) break;
|
|
116
|
+
srcRoot = path.dirname(srcRoot);
|
|
117
|
+
}
|
|
118
|
+
if (!srcRoot || !fs.existsSync(path.join(srcRoot, 'templates', '_agents'))) {
|
|
119
|
+
console.log('SKIP S9(装户环境无包源,不计失败)');
|
|
120
|
+
} else {
|
|
121
|
+
const r = ledgerDrift(srcRoot);
|
|
122
|
+
check('S9 真实仓 baseline → 台账↔盘面全对齐', !r.skipped && r.modified.length === 0 && r.gone.length === 0, JSON.stringify({ modified: r.modified, gone: r.gone }));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${failCount}`);
|
|
127
|
+
process.exit(failCount ? 1 : 0);
|