agentic-flow-kit 0.7.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/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/scripts/check-ledger.mjs +65 -0
- package/templates/_agents/scripts/check-ledger.test.mjs +127 -0
- package/templates/_agents/scripts/doctor.test.mjs +3 -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/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,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);
|
|
@@ -214,7 +214,7 @@ const CMD_BODY = '# Build\n\n正文 build v1\n';
|
|
|
214
214
|
'roles/implementer.md': `---\ndesc: r\n---\n\n${ROLE_BODY}`,
|
|
215
215
|
},
|
|
216
216
|
adapters: {
|
|
217
|
-
'.opencode/commands/build.md': `---\ndesc: oc\n---\n\n${CMD_BODY}`,
|
|
217
|
+
'.opencode/commands/wf-build.md': `---\ndesc: oc\n---\n\n${CMD_BODY}`,
|
|
218
218
|
'.trae/commands/wf-build.md': `---\nname: wf-build\ndesc: trae\n---\n\n${CMD_BODY}`,
|
|
219
219
|
'.zcode/agents/implementer.md': `---\nname: impl\ndesc: zcode\n---\n\n${ROLE_BODY}`,
|
|
220
220
|
'.opencode/agents/implementer.md': `---\nname: impl\ndesc: oc\n---\n\n${ROLE_BODY}`,
|
|
@@ -249,7 +249,7 @@ const CMD_BODY = '# Build\n\n正文 build v1\n';
|
|
|
249
249
|
const root = mkfixAdapter({
|
|
250
250
|
pkgMarker: true,
|
|
251
251
|
authority: { 'commands/build.md': `---\ndesc: x\n---\n\n${CMD_BODY}` },
|
|
252
|
-
adapters: { '.opencode/commands/build.md': `---\ndesc: oc\n---\n\n漂移` },
|
|
252
|
+
adapters: { '.opencode/commands/wf-build.md': `---\ndesc: oc\n---\n\n漂移` },
|
|
253
253
|
});
|
|
254
254
|
const r = checkAdapterDrift(root);
|
|
255
255
|
check('场景 11:包源环境(templates/ + modules/ 同时存在)→ skipped=true',
|
|
@@ -291,7 +291,7 @@ const CMD_BODY = '# Build\n\n正文 build v1\n';
|
|
|
291
291
|
'roles/implementer.md': `---\ndesc: r\n---\n\n${ROLE_BODY}`,
|
|
292
292
|
},
|
|
293
293
|
adapters: {
|
|
294
|
-
'.opencode/commands/build.md': `---\ndesc: oc\n---\n\n${CMD_BODY}`,
|
|
294
|
+
'.opencode/commands/wf-build.md': `---\ndesc: oc\n---\n\n${CMD_BODY}`,
|
|
295
295
|
},
|
|
296
296
|
});
|
|
297
297
|
const r = checkAdapterDrift(root);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
2
|
# Git 原生 pre-commit hook(经 core.hooksPath 挂载,git 执行、不依赖 agent 客户端)— agentic-flow-kit 通用编排
|
|
3
|
-
# 通用门禁序:闭环配对(增量) → wiki 台账(增量) → 常驻面体积预算(增量) → 双源一致性(增量) → 项目门禁(local-pre-commit) → 敏感信息扫描+条件构建(commit-check.cjs)
|
|
3
|
+
# 通用门禁序:闭环配对(增量) → wiki 台账(增量) → 常驻面体积预算(增量) → 双源一致性(增量) → managed 台账快检(包源恒跑) → 项目门禁(local-pre-commit) → 敏感信息扫描+条件构建(commit-check.cjs)
|
|
4
4
|
# 项目专属门禁(架构红线/业务守卫等)放 .agents/hooks/local-pre-commit——存在即在本钩子内执行,返回非零即阻断提交
|
|
5
5
|
# 合并提交场景由 .githooks/pre-merge-commit 原样转发到本脚本(合并时 git 调的是 pre-merge-commit,
|
|
6
6
|
# 不是 pre-commit)——门禁清单只此一份,避免两处分叉
|
|
@@ -46,6 +46,22 @@ if [ -d templates/_agents ] && [ -f .agents/scripts/source-sync-check.mjs ] \
|
|
|
46
46
|
fi
|
|
47
47
|
fi
|
|
48
48
|
|
|
49
|
+
# managed 台账快检(包源环境恒跑,2026-09-28 ledger-precommit-gate;同日独立复核 P2-2 采纳):
|
|
50
|
+
# kit.json 台账 sha ↔ 盘面工作树(LF 归一,同 doctor §4 口径)——拦「台账 sha 预 landing」:台账记了
|
|
51
|
+
# 盘面尚不存在的内容 sha。双源门禁对此静默(templates↔.agents 成对回退时两侧一致),doctor §4 只在
|
|
52
|
+
# 手动全量跑——该故障模式三次真实发生(0272a1c 预入 / ff3c721 还原窗口 / 537df18 闭合),本块补齐
|
|
53
|
+
# 提交时刻的第三边。恒跑不作暂存触发:预 landing 是仓态而非暂存态,暂存 grep 有前缀缝(如 .githooks/*
|
|
54
|
+
# managed 件单边漂移不命中),毫秒级 80 份 hash 不构成开销理由。比较工作树而非暂存区(沿双源门禁
|
|
55
|
+
# 口径)。装户环境(无 templates/_agents/ 包源)自然跳过——装户 managed 手改走 doctor §4 WARN 口径,
|
|
56
|
+
# 不硬拦。已知边界:managed 删除即拦(sync 对 removed 只报不删,删件须手动同步装副本并重跑 sync 刷
|
|
57
|
+
# 台账)。
|
|
58
|
+
if [ -d templates/_agents ] && [ -f .agents/scripts/check-ledger.mjs ]; then
|
|
59
|
+
if ! node .agents/scripts/check-ledger.mjs; then
|
|
60
|
+
echo "pre-commit: managed 台账快检未通过,提交已阻止——kit.json 与盘面不一致,跑 node bin/flow-kit.mjs sync 成对后原路重试" >&2
|
|
61
|
+
exit 1
|
|
62
|
+
fi
|
|
63
|
+
fi
|
|
64
|
+
|
|
49
65
|
# 项目门禁挂载点:.agents/hooks/local-pre-commit(项目自建门禁/门禁模块接线处,存在即执行)
|
|
50
66
|
if [ -f .agents/hooks/local-pre-commit ]; then
|
|
51
67
|
run_gate "项目门禁(local-pre-commit)" .agents/hooks/local-pre-commit
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|