agentic-flow-kit 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/modules/hosts/omp/agents/implementer.md +45 -6
- package/modules/hosts/omp/agents/independent-reviewer.md +42 -5
- package/modules/hosts/omp/agents/ui-verifier.md +41 -6
- package/modules/hosts/opencode/agents/implementer.md +45 -1
- package/modules/hosts/opencode/agents/independent-reviewer.md +42 -1
- package/modules/hosts/opencode/agents/ui-verifier.md +41 -1
- package/modules/hosts/opencode/commands/build.md +55 -7
- package/modules/hosts/opencode/commands/deploy.md +51 -6
- package/modules/hosts/opencode/commands/design.md +34 -6
- package/modules/hosts/opencode/commands/maintain.md +71 -6
- package/modules/hosts/opencode/commands/new-task.md +59 -6
- package/modules/hosts/opencode/commands/plan.md +32 -6
- package/modules/hosts/opencode/commands/review.md +40 -6
- package/modules/hosts/opencode/commands/test.md +56 -8
- package/modules/hosts/trae/agents/implementer.md +44 -2
- package/modules/hosts/trae/agents/independent-reviewer.md +42 -1
- package/modules/hosts/trae/agents/ui-verifier.md +39 -3
- package/modules/hosts/trae/commands/wf-build.md +55 -7
- package/modules/hosts/trae/commands/wf-deploy.md +51 -6
- package/modules/hosts/trae/commands/wf-design.md +34 -6
- package/modules/hosts/trae/commands/wf-maintain.md +71 -6
- package/modules/hosts/trae/commands/wf-new-task.md +59 -6
- package/modules/hosts/trae/commands/wf-plan.md +32 -6
- package/modules/hosts/trae/commands/wf-review.md +40 -6
- package/modules/hosts/trae/commands/wf-test.md +56 -7
- package/modules/hosts/zcode/agents/implementer.md +45 -6
- package/modules/hosts/zcode/agents/independent-reviewer.md +42 -5
- package/modules/hosts/zcode/agents/ui-verifier.md +41 -5
- package/package.json +6 -3
- package/src/cli.mjs +15 -2
- package/src/doctor.mjs +138 -11
- package/src/init.test.mjs +25 -2
- package/src/pack.test.mjs +24 -0
- package/src/run-tests.mjs +4 -11
- package/src/sync-hosts.mjs +193 -0
- package/src/sync-hosts.test.mjs +170 -0
- package/src/sync.mjs +1 -0
- package/src/sync.test.mjs +10 -0
- package/templates/AGENTS.md +1 -1
- package/templates/_agents/commands/build.md +20 -0
- package/templates/_agents/commands/design.md +1 -0
- package/templates/_agents/commands/gate-checklist.md +91 -0
- package/templates/_agents/commands/maintain.md +1 -1
- package/templates/_agents/commands/plan.md +2 -1
- package/templates/_agents/commands/review.md +1 -0
- package/templates/_agents/commands/source-sync-check.md +103 -0
- package/templates/_agents/commands/sync-hosts.md +79 -0
- package/templates/_agents/commands/test.md +2 -1
- package/templates/_agents/notes/runtime-env.md +1 -1
- package/templates/_agents/rule-budgets.txt +1 -1
- package/templates/_agents/scripts/check-loop.mjs +517 -0
- package/templates/_agents/scripts/check-loop.sh +5 -808
- package/templates/_agents/scripts/check-loop.test.mjs +481 -0
- package/templates/_agents/scripts/doctor.test.mjs +298 -0
- package/templates/_agents/scripts/ensure-board.mjs +1 -1
- package/templates/_agents/scripts/fill-intent.mjs +59 -0
- package/templates/_agents/scripts/fill-intent.test.mjs +73 -0
- package/templates/_agents/scripts/fill-plan.mjs +60 -0
- package/templates/_agents/scripts/fill-plan.test.mjs +67 -0
- package/templates/_agents/scripts/fill-spec.mjs +52 -0
- package/templates/_agents/scripts/fill-spec.test.mjs +61 -0
- package/templates/_agents/scripts/gate-checklist.mjs +192 -0
- package/templates/_agents/scripts/gate-checklist.test.mjs +204 -0
- package/templates/_agents/scripts/gen-workflow-index.mjs +4 -2
- package/templates/_agents/scripts/kb-search.mjs +3 -1
- package/templates/_agents/scripts/source-sync-check.mjs +131 -0
- package/templates/_agents/scripts/source-sync-check.test.mjs +156 -0
- package/templates/_agents/scripts/verify.mjs +8 -10
- package/templates/_agents/scripts/verify.test.mjs +13 -1
- package/templates/_agents/scripts/wf-journal.mjs +158 -0
- package/templates/_agents/scripts/wf-journal.test.mjs +150 -0
- package/templates/_agents/scripts/workflow-board-server.mjs +9 -8
- package/templates/_agents/scripts/workflow-enums.mjs +68 -0
- package/templates/_agents/scripts/workflow-enums.test.mjs +110 -0
- package/templates/_agents/scripts/workflows-check.mjs +260 -0
- package/templates/_agents/scripts/workflows-check.test.mjs +267 -0
- package/templates/_agents/workflow-enums.txt +19 -0
- package/templates/_agents/workflows/_TEMPLATE.md +74 -0
- package/templates/_agents/workflows/pipeline-closing.md +29 -0
- package/templates/_agents/workflows/source-sync-repair.md +30 -0
- package/templates/_agents/workflows/steps//347/244/272/344/276/213-/351/203/250/347/275/262/351/252/214/350/257/201.md +10 -0
- package/templates/_agents/workflows//347/244/272/344/276/213-/345/271/266/350/241/214/345/256/236/347/216/260/350/257/204/345/256/241.md +16 -0
- package/templates/workflow/README.md +1 -1
- package/templates/_agents/cache/kb-index.json +0 -1
- package/templates/_agents/scripts/check-loop.test.sh +0 -1042
package/src/doctor.mjs
CHANGED
|
@@ -50,8 +50,11 @@ export function doctor(args, pkgRoot) {
|
|
|
50
50
|
'.agents/board/index.html',
|
|
51
51
|
'.agents/hooks/commit-check.cjs',
|
|
52
52
|
'.agents/workflow-modules.txt',
|
|
53
|
+
'.agents/workflow-enums.txt',
|
|
53
54
|
'.agents/rule-budgets.txt',
|
|
54
55
|
'.agents/settings.json',
|
|
56
|
+
'.agents/workflows/_TEMPLATE.md',
|
|
57
|
+
'.agents/scripts/check-loop.mjs',
|
|
55
58
|
'.githooks/pre-commit',
|
|
56
59
|
'.githooks/pre-push',
|
|
57
60
|
'.githooks/commit-msg',
|
|
@@ -127,22 +130,63 @@ export function doctor(args, pkgRoot) {
|
|
|
127
130
|
else add('WARN', `delegations 台账结构漂移——${String(agg.stderr || agg.stdout || '').split('\n')[0]}`);
|
|
128
131
|
}
|
|
129
132
|
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
// 6.6 owned 漂移校验(kit.owned 列表盘面 sha 不一致;engine 双源纪律对 owned 走「项目自持 + 哈希记账不约束」,
|
|
134
|
+
// 漂移信号靠本校验给装户可见性。2026-09-25 wf-runtime 复盘:包源改了装副本未同步 = 漂移但 sync 不报。)
|
|
135
|
+
const ownedRes = checkOwnedDrift(target);
|
|
136
|
+
if (ownedRes.error) {
|
|
137
|
+
add('WARN', `owned 校验跳过(kit.json 解析失败:${ownedRes.error})`);
|
|
138
|
+
} else if (ownedRes.skipped) {
|
|
139
|
+
add('PASS', 'owned 校验跳过(kit.json 无 owned 条目)');
|
|
140
|
+
} else if (ownedRes.total === 0) {
|
|
141
|
+
add('PASS', 'owned 校验跳过(kit.json 无 owned 条目)');
|
|
142
|
+
} else if (ownedRes.drift === 0 && ownedRes.gone.length === 0) {
|
|
143
|
+
add('PASS', `owned ${ownedRes.total} 份无漂移(装副本与台账 sha 对齐)`);
|
|
136
144
|
} else {
|
|
137
|
-
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
|
|
145
|
+
// owned 漂移已严化为 FAIL(2026-09-25 doctor-owned-drift-strict 复盘;首次引入用 WARN,installed 装户吃过警告后升级)
|
|
146
|
+
if (ownedRes.drift) add('FAIL', `owned 漂移 ${ownedRes.drift} 份——装副本手改未跑 sync 刷台账 / 包源改了装副本未同步;须手动同步装副本后跑 node bin/flow-kit.mjs sync 刷台账(见 incidents/2026-09-25-wf-runtime 复盘)`);
|
|
147
|
+
if (ownedRes.gone.length) add('FAIL', `owned 文件缺失:${ownedRes.gone.join('、')}——sync 恢复或手动恢复`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 6.7 跨宿主薄适配正文段漂移校验(2026-09-25 cross-host-sync,参见 workflows/intents/2026-09-25-cross-host-sync.md)
|
|
151
|
+
// 装户侧视角:.agents/{commands,roles}/*.md 是权威源;4 宿主目录(.zcode/.omp/.opencode/.trae)下的
|
|
152
|
+
// {agents,commands}/*.md 是薄适配。按正文段 sha 比对(B-b 语义,frontmatter 不计入漂移),
|
|
153
|
+
// trae commands 加 wf- 前缀映射。首次引入为 WARN(沿用 wf-runtime 复盘「先 WARN 升级 FAIL」路径),
|
|
154
|
+
// 待装户升级视反馈升级 FAIL。包源侧对应工具是 flow-kit sync-hosts(src/sync-hosts.mjs)。
|
|
155
|
+
const adapterRes = checkAdapterDrift(target);
|
|
156
|
+
if (adapterRes.skipped) {
|
|
157
|
+
add('PASS', `跨宿主薄适配校验跳过(${adapterRes.note || '权威源目录不存在'})`);
|
|
158
|
+
} else if (adapterRes.drift === 0) {
|
|
159
|
+
add('PASS', `跨宿主薄适配 ${adapterRes.total} 对无正文漂移(装副本引擎 vs 4 宿主薄适配,B-b 语义)`);
|
|
160
|
+
} else {
|
|
161
|
+
add('WARN', `跨宿主薄适配正文漂移 ${adapterRes.drift} 对——权威源与薄适配正文段 sha 不一致;按 .agents/commands/sync-hosts.md 跑 flow-kit sync-hosts --apply 单向同步薄适配正文(frontmatter 不动);装户可在 bin/flow-kit.mjs sync 时一并修复`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 6.8 workflows 编排脚本 lint(stages 表解析校验:role/step/after 引用、无环、三形态互斥、数值枚举;
|
|
165
|
+
// 2026-09-25 workflows-linter 把 _TEMPLATE.md「解析校验先行」从 prose 变机器门。首次引入 WARN
|
|
166
|
+
// (沿用 wf-runtime 复盘「先 WARN 升级 FAIL」渐进路径),装户吃过警告后可升 FAIL。
|
|
167
|
+
// 旧版装户 sync 前 .agents/scripts/workflows-check.mjs 不存在 → 自然跳过不误报)
|
|
168
|
+
if (fs.existsSync(path.join(target, '.agents/scripts/workflows-check.mjs')) && fs.existsSync(path.join(target, '.agents/workflows'))) {
|
|
169
|
+
const wf = spawnSync(process.execPath, ['.agents/scripts/workflows-check.mjs'], { cwd: target, encoding: 'utf8' });
|
|
170
|
+
if (wf.status === 0) {
|
|
171
|
+
const warnN = (String(wf.stdout || '').match(/- ⚠️ W\d/g) || []).length;
|
|
172
|
+
add('PASS', `workflows 编排脚本 lint 干净${warnN ? `(${warnN} 条 advisory 告警)` : ''}`);
|
|
141
173
|
} else {
|
|
142
|
-
|
|
174
|
+
const firstHit = String(wf.stdout || wf.stderr || '').split('\n').find((l) => / E\d/.test(l)) || `exit ${wf.status}`;
|
|
175
|
+
add('WARN', `workflows 编排脚本 lint 未过——${firstHit.trim()};修 .agents/workflows/ 编排脚本(role/step/after 引用、三形态、retries)后重跑`);
|
|
143
176
|
}
|
|
144
177
|
}
|
|
145
178
|
|
|
179
|
+
// 7. check-loop——2026-09-26 check-loop-node 起 node 实现直跑(sh 版曾需先探 sh 可用性:Windows
|
|
180
|
+
// PowerShell 常无 sh,ENOENT 曾被吞进 hard-block 分支报成空原因假警报,incident 2026-09-24-doctor-sh-enoent;
|
|
181
|
+
// 迁移后无 sh 依赖,探针退役。check-loop.sh 为兼容 shim,pre-push 钩子路径照常)
|
|
182
|
+
const cl = spawnSync(process.execPath, ['.agents/scripts/check-loop.mjs'], { cwd: target, encoding: 'utf8' });
|
|
183
|
+
if (cl.status === 0) {
|
|
184
|
+
const warnTxt = String(cl.stderr || '').trim();
|
|
185
|
+
add('PASS', `check-loop 干净${warnTxt ? `(${warnTxt.split('\n').filter((l) => l.includes('WARN')).length} 条 advisory 警告)` : ''}`);
|
|
186
|
+
} else {
|
|
187
|
+
add('FAIL', `check-loop 有 hard-block:\n${String(cl.stderr || '').split('\n').slice(0, 8).join('\n')}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
146
190
|
// 8. 看板端口(信息级)
|
|
147
191
|
const kit = fs.existsSync(kitPath) ? JSON.parse(fs.readFileSync(kitPath, 'utf8')) : null;
|
|
148
192
|
const port = kit?.options?.boardPort || '8933';
|
|
@@ -162,3 +206,86 @@ function print(results) {
|
|
|
162
206
|
const warns = results.filter((r) => r.level === 'WARN').length;
|
|
163
207
|
console.log(`\ndoctor:${results.length - fails - warns} PASS | ${warns} WARN | ${fails} FAIL`);
|
|
164
208
|
}
|
|
209
|
+
|
|
210
|
+
// owned 漂移校验(独立 export 供 doctor 主流程 + 单元测试共用;2026-09-25 wf-runtime 复盘)
|
|
211
|
+
// 返回:{ drift, gone, total, skipped, error? }
|
|
212
|
+
// - skipped=true:kit.json 不存在或解析失败/无 owned 字段
|
|
213
|
+
// - drift:盘面 sha != kit.owned sha 的文件数
|
|
214
|
+
// - gone:盘面缺失的文件 rel 列表
|
|
215
|
+
// - total:kit.owned 列表总文件数
|
|
216
|
+
export function checkOwnedDrift(target) {
|
|
217
|
+
const kitPath = path.join(target, '.agents', 'kit.json');
|
|
218
|
+
if (!fs.existsSync(kitPath)) return { drift: 0, gone: [], total: 0, skipped: true };
|
|
219
|
+
let kit;
|
|
220
|
+
try {
|
|
221
|
+
kit = JSON.parse(fs.readFileSync(kitPath, 'utf8'));
|
|
222
|
+
} catch (e) {
|
|
223
|
+
return { drift: 0, gone: [], total: 0, skipped: true, error: e.message };
|
|
224
|
+
}
|
|
225
|
+
const owned = Array.isArray(kit.owned) ? kit.owned : [];
|
|
226
|
+
if (owned.length === 0) return { drift: 0, gone: [], total: 0, skipped: true };
|
|
227
|
+
let drift = 0;
|
|
228
|
+
const gone = [];
|
|
229
|
+
for (const f of owned) {
|
|
230
|
+
const p = path.join(target, f.rel);
|
|
231
|
+
if (!fs.existsSync(p)) { gone.push(f.rel); continue; }
|
|
232
|
+
const h = createHash('sha256').update(fs.readFileSync(p)).digest('hex');
|
|
233
|
+
if (h !== f.sha256) drift++;
|
|
234
|
+
}
|
|
235
|
+
return { drift, gone, total: owned.length, skipped: false };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 跨宿主薄适配正文段漂移校验(独立 export 供 doctor 主流程 + 单元测试共用;2026-09-25 cross-host-sync)
|
|
239
|
+
// 装户侧视角:.agents/{commands,roles}/*.md 是权威源;4 宿主目录(zcode→.zcode、omp→.omp、
|
|
240
|
+
// opencode→.opencode、trae→.trae)下 {agents,commands}/*.md 是薄适配。按正文段 sha 比对
|
|
241
|
+
// (B-b 语义;frontmatter 不计入漂移);trae commands 加 wf- 前缀映射。
|
|
242
|
+
// 返回:{ drift, total, skipped, note? }
|
|
243
|
+
// - skipped=true:权威源目录不存在 / 包源环境 / 无 .md 文件
|
|
244
|
+
// - drift:权威源正文段与薄适配正文段 sha 不一致的对数
|
|
245
|
+
// - total:参与比对的对数(权威源文件 × 适配数)
|
|
246
|
+
export function checkAdapterDrift(target) {
|
|
247
|
+
const authorityRoot = path.join(target, '.agents');
|
|
248
|
+
if (!fs.existsSync(authorityRoot)) return { drift: 0, total: 0, skipped: true, note: '.agents 目录不存在' };
|
|
249
|
+
// 包源环境检测:包源仓库同时含 templates/ 与 modules/,装户不应有;包源下跑 doctor §7.x 永远是
|
|
250
|
+
// drift(装副本是历史 init 渲染产物,不会随包源改动而重渲),无信息价值——直接 skip
|
|
251
|
+
if (fs.existsSync(path.join(target, 'templates', '_agents')) && fs.existsSync(path.join(target, 'modules', 'hosts'))) {
|
|
252
|
+
return { drift: 0, total: 0, skipped: true, note: '包源环境(templates/ + modules/ 同时存在)——§7.x 仅在装户环境有意义' };
|
|
253
|
+
}
|
|
254
|
+
// 4 宿主目录映射(与 src/profiles.mjs#HOSTS 同源:装副本目录名前缀带点)
|
|
255
|
+
const HOST_DIR = { zcode: '.zcode', omp: '.omp', opencode: '.opencode', trae: '.trae' };
|
|
256
|
+
const splitFm = (text) => {
|
|
257
|
+
const m = String(text || '').match(/^---\r?\n([\s\S]*?\r?\n)---\r?\n?([\s\S]*)$/);
|
|
258
|
+
if (!m) return { fm: '', body: String(text || '') };
|
|
259
|
+
return { fm: `---\n${m[1]}---\n`, body: m[2] };
|
|
260
|
+
};
|
|
261
|
+
const bodySha = (p) => {
|
|
262
|
+
try { return createHash('sha256').update(splitFm(fs.readFileSync(p, 'utf8')).body).digest('hex'); } catch { return null; }
|
|
263
|
+
};
|
|
264
|
+
let total = 0;
|
|
265
|
+
let drift = 0;
|
|
266
|
+
const listMd = (dir) => fs.existsSync(dir) && fs.statSync(dir).isDirectory() ? fs.readdirSync(dir).filter((f) => f.endsWith('.md')) : [];
|
|
267
|
+
for (const sub of ['commands', 'roles']) {
|
|
268
|
+
const subAbs = path.join(authorityRoot, sub);
|
|
269
|
+
for (const f of listMd(subAbs)) {
|
|
270
|
+
const authRel = `${sub}/${f}`;
|
|
271
|
+
const authSha = bodySha(path.join(authorityRoot, authRel));
|
|
272
|
+
const name = f.slice(0, -'.md'.length);
|
|
273
|
+
// 构建映射:commands/* → opencode/commands/<name>.md + trae/commands/wf-<name>.md;roles/* → 4 宿主 agents/<name>.md
|
|
274
|
+
const targets = [];
|
|
275
|
+
if (sub === 'commands') {
|
|
276
|
+
targets.push([HOST_DIR.opencode, 'commands', `${name}.md`], [HOST_DIR.trae, 'commands', `wf-${name}.md`]);
|
|
277
|
+
} else {
|
|
278
|
+
for (const h of Object.keys(HOST_DIR)) targets.push([HOST_DIR[h], 'agents', `${name}.md`]);
|
|
279
|
+
}
|
|
280
|
+
for (const [hostDir, subDir, fileName] of targets) {
|
|
281
|
+
const adapterAbs = path.join(target, hostDir, subDir, fileName);
|
|
282
|
+
const adapterSha = bodySha(adapterAbs);
|
|
283
|
+
if (adapterSha === null) continue; // 缺失不在 drift 统计(apply 不自动创建)
|
|
284
|
+
total++;
|
|
285
|
+
if (authSha !== adapterSha) drift++;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (total === 0) return { drift: 0, total: 0, skipped: true, note: '权威源无 commands/roles .md 文件' };
|
|
290
|
+
return { drift, total, skipped: false };
|
|
291
|
+
}
|
package/src/init.test.mjs
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// init.mjs 交互解析纯函数测试(normalizeStack / parseChoices
|
|
2
|
+
// init.mjs 交互解析纯函数测试(normalizeStack / parseChoices)+ 装户面断言——解析层不触真实安装与 stdin;
|
|
3
|
+
// 装户面节对真实 templates/ 树跑 renderTree(init/sync 同一装户路径),断言编排 runner 与 workflows 目录在装户清单内。
|
|
3
4
|
// 断言:①ts/js 别名归一 node;②序号/名称/混输/全角逗号/空白分隔解析正确(去重保序);
|
|
4
|
-
// ③非法输入返回 null(就地重问信号);④空输入回默认(EOF
|
|
5
|
+
// ③非法输入返回 null(就地重问信号);④空输入回默认(EOF/直接回车安全回退);⑥装户面含 runner/workflows。
|
|
5
6
|
// 用法:node src/init.test.mjs
|
|
6
7
|
import { normalizeStack, parseChoices, hasAgentsSkeleton, mergeAgents } from './init.mjs';
|
|
8
|
+
import { renderTree } from './render.mjs';
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
7
13
|
|
|
8
14
|
let pass = 0;
|
|
9
15
|
let fail = 0;
|
|
@@ -48,6 +54,23 @@ check('合并:原内容尾部空白折叠不产生连续空行',
|
|
|
48
54
|
mergeAgents('# A\n内容\n\n\n', '<!-- m -->\n# B') === '# A\n内容\n\n<!-- m -->\n# B\n');
|
|
49
55
|
check('合并:原内容为空时骨架即全文', mergeAgents('', '<!-- m -->\n# B') === '<!-- m -->\n# B\n');
|
|
50
56
|
|
|
57
|
+
// ---- ⑥ 装户面:真实模板树含编排机制件(init/sync 同走 renderTree,结构保证装户必有) ----
|
|
58
|
+
{
|
|
59
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-surface-'));
|
|
60
|
+
const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
61
|
+
const t = renderTree(path.join(pkgRoot, 'templates'), tmp, { BOARD_PORT: '8933' }, { force: true });
|
|
62
|
+
const rels = new Set(t.written.map((w) => w.rel));
|
|
63
|
+
check('装户面含机制文档与编排脚本示例(_TEMPLATE + 示例 .md)',
|
|
64
|
+
rels.has('.agents/workflows/_TEMPLATE.md') && rels.has('.agents/workflows/示例-并行实现评审.md'),
|
|
65
|
+
[...rels].filter((r) => r.includes('workflows')).join('、'));
|
|
66
|
+
check('装户面含 steps 扩展点示例', rels.has('.agents/workflows/steps/示例-部署验证.md'),
|
|
67
|
+
[...rels].filter((r) => r.includes('steps')).join('、'));
|
|
68
|
+
check('装户面无 orchestrate 命令残留(编排机制为目录约定,2026-09-25-wf-runtime)',
|
|
69
|
+
![...rels].some((r) => r.includes('orchestrate')),
|
|
70
|
+
[...rels].filter((r) => r.includes('orchestrate')).join('、'));
|
|
71
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
72
|
+
}
|
|
73
|
+
|
|
51
74
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
52
75
|
if (fail) {
|
|
53
76
|
console.log('\n用法:node src/init.test.mjs');
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// pack.test.mjs — npm 打包防线断言:templates/_agents/cache(运行时生成、gitignore)不得进发布包。
|
|
2
|
+
// 0.4.0 实发包含测试残留 kb-index.json(incident 2026-09-24-npm-pack-cache-leak)——files 白名单列出
|
|
3
|
+
// templates 后 npm-packlist 不再按根 .gitignore 排除目录内文件,须显式负向排除;prepack 兜底物理清除。
|
|
4
|
+
// 此处断言配置层两道防线在位(npm 10/11 双版本 pack 行为已在 incident 实测验证)。
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
|
|
11
|
+
|
|
12
|
+
let pass = 0, fail = 0;
|
|
13
|
+
function check(name, cond, detail = '') {
|
|
14
|
+
if (cond) { pass++; console.log(`PASS ${name}`); }
|
|
15
|
+
else { fail++; console.log(`FAIL ${name}${detail ? `——${detail}` : ''}`); }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const files = pkg.files || [];
|
|
19
|
+
const prepack = pkg.scripts?.prepack || '';
|
|
20
|
+
check('P1 files 负向排除 cache 目录与内容', files.includes('!templates/_agents/cache') && files.includes('!templates/_agents/cache/**'), JSON.stringify(files));
|
|
21
|
+
check('P2 prepack 兜底清除 cache 目录', /templates[\\/]_agents[\\/]cache/.test(prepack) && /rmSync/.test(prepack), prepack || 'scripts.prepack 缺失');
|
|
22
|
+
|
|
23
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
24
|
+
process.exit(fail ? 1 : 0);
|
package/src/run-tests.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// run-tests.mjs — 全套测试入口(npm test):CLI 单测(sync/add-host/add-gate)+ 引擎 *.test.mjs
|
|
2
|
-
// 顺序执行、任一非零整体失败(exit 1
|
|
1
|
+
// run-tests.mjs — 全套测试入口(npm test):CLI 单测(sync/add-host/add-gate)+ 引擎 *.test.mjs 套件。
|
|
2
|
+
// 顺序执行、任一非零整体失败(exit 1)。check-loop 套件自 2026-09-26 check-loop-node 起为 node 实现
|
|
3
|
+
// (check-loop.test.mjs 由下方 glob 自动发现——旧 bash 特判与「无 sh 跳过」提示随迁移退役)。
|
|
3
4
|
import { spawnSync } from 'node:child_process';
|
|
4
5
|
import fs from 'node:fs';
|
|
5
6
|
import path from 'node:path';
|
|
@@ -10,6 +11,7 @@ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
|
10
11
|
const nodeSuites = [
|
|
11
12
|
'src/sync.test.mjs',
|
|
12
13
|
'src/init.test.mjs',
|
|
14
|
+
'src/pack.test.mjs',
|
|
13
15
|
...fs.readdirSync(path.join(ROOT, 'templates/_agents/scripts'))
|
|
14
16
|
.filter((f) => f.endsWith('.test.mjs'))
|
|
15
17
|
.sort()
|
|
@@ -23,14 +25,5 @@ for (const rel of nodeSuites) {
|
|
|
23
25
|
if (r.status !== 0) failed++;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
const shSuite = 'templates/_agents/scripts/check-loop.test.sh';
|
|
27
|
-
if (spawnSync('sh', ['-c', 'true']).status === 0) {
|
|
28
|
-
console.log(`\n===== ${shSuite}(bash 套件) =====`);
|
|
29
|
-
const r = spawnSync('sh', [path.join(ROOT, shSuite)], { cwd: ROOT, stdio: 'inherit' });
|
|
30
|
-
if (r.status !== 0) failed++;
|
|
31
|
-
} else {
|
|
32
|
-
console.warn(`\n⚠️ 环境无 sh——跳过 ${shSuite}(装 Git Bash / 在 WSL 里补跑,勿静默当作通过)`);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
28
|
console.log(`\n${failed ? `❌ ${failed} 个套件失败` : '✅ 全部套件通过'}`);
|
|
36
29
|
process.exit(failed ? 1 : 0);
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// flow-kit sync-hosts:跨宿主适配层正文段漂移检查 + 单向同步(B-b 方案,2026-09-25)
|
|
2
|
+
// 目的:改权威源(templates/_agents/{commands,roles}/*.md)正文要同步到 N 份薄适配
|
|
3
|
+
// (modules/hosts/<h>/{agents,commands}/*.md)的对应正文段;薄适配 frontmatter 保留宿主特化字段
|
|
4
|
+
// (trae commands 加 name: wf-X、opencode/zcode/omp 各自原描述),不在同步范围。
|
|
5
|
+
// B-b 语义:sha 比对与 apply 只动正文段,frontmatter 双方各自维护。
|
|
6
|
+
// 算法:diffHosts 抽为 pure function(authorityRoot + adaptersRoot 两个根);
|
|
7
|
+
// sync-hosts CLI 在 pkgRoot 跑(包源视角);doctor §7.x 在 target 跑(装户视角)——同源逻辑。
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { HOSTS } from './profiles.mjs';
|
|
11
|
+
import { sha256 } from './render.mjs';
|
|
12
|
+
|
|
13
|
+
function fail(msg) {
|
|
14
|
+
console.error(`❌ ${msg}`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 剥离 frontmatter 返回 { fm, body };无 frontmatter 时 fm = '',body = 整文件
|
|
19
|
+
// frontmatter 形如 "---\n...\n---\n",位于文件首;其后(含分隔空行)为正文段。
|
|
20
|
+
function splitFm(text) {
|
|
21
|
+
const m = String(text || '').match(/^---\r?\n([\s\S]*?\r?\n)---\r?\n?([\s\S]*)$/);
|
|
22
|
+
if (!m) return { fm: '', body: String(text || '') };
|
|
23
|
+
// fm 还原为带分隔横线的形式;body 保留首部空行
|
|
24
|
+
return { fm: `---\n${m[1]}---\n`, body: m[2] };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// bodySha:剥离 frontmatter 后的正文段 sha256(缺失返 null)
|
|
28
|
+
function bodySha(p) {
|
|
29
|
+
try {
|
|
30
|
+
const { body } = splitFm(fs.readFileSync(p, 'utf8'));
|
|
31
|
+
return sha256(Buffer.from(body, 'utf8'));
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// listMd:列目录下 .md 文件(同步当前层;不递归)
|
|
38
|
+
function listMd(dir) {
|
|
39
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return [];
|
|
40
|
+
return fs.readdirSync(dir).filter((f) => f.endsWith('.md'));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// pairsFor:权威源文件 → 薄适配 rel 列表(与 sync-hosts 早版同映射;trae commands 加 wf- 前缀)
|
|
44
|
+
function pairsFor(authRel) {
|
|
45
|
+
const out = [];
|
|
46
|
+
const norm = authRel.split(path.sep).join('/');
|
|
47
|
+
if (norm.startsWith('commands/') && norm.endsWith('.md')) {
|
|
48
|
+
const name = norm.slice('commands/'.length, -'.md'.length);
|
|
49
|
+
out.push({ adapterRel: `opencode/commands/${name}.md` });
|
|
50
|
+
out.push({ adapterRel: `trae/commands/wf-${name}.md` });
|
|
51
|
+
} else if (norm.startsWith('roles/') && norm.endsWith('.md')) {
|
|
52
|
+
const name = norm.slice('roles/'.length, -'.md'.length);
|
|
53
|
+
for (const h of Object.keys(HOSTS)) out.push({ adapterRel: `${h}/agents/${name}.md` });
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// diffHosts:权威源正文段 vs 薄适配正文段的漂移分析(B-b 语义;pure function)。
|
|
59
|
+
// authorityRoot:权威源根(含 commands/ + roles/)
|
|
60
|
+
// adaptersRoot:薄适配根(含 <h>/{agents,commands}/)
|
|
61
|
+
// 返回:{ drift, inSync, authorityMissing, adapterOrphans, note }
|
|
62
|
+
// drift = 正文段 sha 不一致(权威源改 / 薄适配手改 都计入;不区分方向)
|
|
63
|
+
// inSync = 正文段 sha 对齐的文件对数
|
|
64
|
+
// authorityMissing = 权威源声明但薄适配文件不存在(apply 不自动创建——薄适配须经 add-host 接管)
|
|
65
|
+
// adapterOrphans = 薄适配存在但权威源无对应文件(apply 不删——可能是宿主特化或历史残留)
|
|
66
|
+
export function diffHosts({ authorityRoot, adaptersRoot }) {
|
|
67
|
+
if (!fs.existsSync(authorityRoot)) return { drift: [], inSync: 0, authorityMissing: [], adapterOrphans: [], note: `权威源根不存在:${authorityRoot}` };
|
|
68
|
+
|
|
69
|
+
const drift = [];
|
|
70
|
+
let inSync = 0;
|
|
71
|
+
const adapterSeen = new Set();
|
|
72
|
+
const authorityMissing = [];
|
|
73
|
+
|
|
74
|
+
// 扫权威源 commands/ + roles/
|
|
75
|
+
for (const sub of ['commands', 'roles']) {
|
|
76
|
+
const subAbs = path.join(authorityRoot, sub);
|
|
77
|
+
for (const f of listMd(subAbs)) {
|
|
78
|
+
const authRel = `${sub}/${f}`;
|
|
79
|
+
const authAbs = path.join(authorityRoot, authRel);
|
|
80
|
+
const authSha = bodySha(authAbs);
|
|
81
|
+
const pairs = pairsFor(authRel);
|
|
82
|
+
if (pairs.length === 0) continue;
|
|
83
|
+
for (const { adapterRel } of pairs) {
|
|
84
|
+
adapterSeen.add(adapterRel);
|
|
85
|
+
const adapterAbs = path.join(adaptersRoot, adapterRel);
|
|
86
|
+
const adapterSha = bodySha(adapterAbs);
|
|
87
|
+
if (adapterSha === null) {
|
|
88
|
+
authorityMissing.push({ authRel, adapterRel });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (authSha === adapterSha) { inSync++; continue; }
|
|
92
|
+
drift.push({ authRel, adapterRel, authSha, adapterSha });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 扫薄适配找孤儿(权威源无对应文件)
|
|
98
|
+
const adapterOrphans = [];
|
|
99
|
+
for (const h of Object.keys(HOSTS)) {
|
|
100
|
+
for (const sub of ['agents', 'commands']) {
|
|
101
|
+
const subAbs = path.join(adaptersRoot, h, sub);
|
|
102
|
+
for (const f of listMd(subAbs)) {
|
|
103
|
+
const adapterRel = `${h}/${sub}/${f}`;
|
|
104
|
+
if (adapterSeen.has(adapterRel)) continue;
|
|
105
|
+
let authGuess;
|
|
106
|
+
if (h === 'trae' && sub === 'commands' && f.startsWith('wf-')) {
|
|
107
|
+
authGuess = `commands/${f.slice('wf-'.length)}`;
|
|
108
|
+
} else if (sub === 'agents') {
|
|
109
|
+
authGuess = `roles/${f}`;
|
|
110
|
+
} else {
|
|
111
|
+
authGuess = `commands/${f}`;
|
|
112
|
+
}
|
|
113
|
+
adapterOrphans.push({ adapterRel, authGuess });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { drift, inSync, authorityMissing, adapterOrphans };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// printDiff:人类可读 diff 报告(B-b 语义:正文段比对 + frontmatter 双方不动)
|
|
122
|
+
function printDiff(result) {
|
|
123
|
+
const { drift, inSync, authorityMissing, adapterOrphans, note } = result;
|
|
124
|
+
if (note) console.log(`ℹ️ ${note}\n`);
|
|
125
|
+
console.log(` 正文对齐:${inSync} 对`);
|
|
126
|
+
if (authorityMissing.length) {
|
|
127
|
+
console.log(` 权威源声明但薄适配缺失(${authorityMissing.length},apply 不自动创建——薄适配须经 add-host 接管):`);
|
|
128
|
+
for (const { authRel, adapterRel } of authorityMissing) console.log(` - ${authRel} → ${adapterRel}`);
|
|
129
|
+
}
|
|
130
|
+
if (drift.length) {
|
|
131
|
+
console.log(` 正文段漂移(${drift.length},权威源与薄适配正文 sha 不一致——可能是权威源改了或薄适配正文手改了;run with --apply to sync 正文段,frontmatter 不动):`);
|
|
132
|
+
for (const { authRel, adapterRel } of drift) console.log(` - ${authRel} → ${adapterRel}`);
|
|
133
|
+
} else if (!authorityMissing.length) {
|
|
134
|
+
console.log(' 无漂移 ✅');
|
|
135
|
+
}
|
|
136
|
+
if (adapterOrphans.length) {
|
|
137
|
+
console.log(` ⚠️ 孤儿薄适配(${adapterOrphans.length},权威源无对应文件,可能是历史残留或宿主特化;apply 不删):`);
|
|
138
|
+
for (const { adapterRel, authGuess } of adapterOrphans) console.log(` - ${adapterRel}(推测权威源:${authGuess})`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// applyForward:把权威源正文段同步到薄适配(保留薄适配原 frontmatter)
|
|
143
|
+
// 返回:{ synced: [{adapterRel}] }
|
|
144
|
+
function applyForward({ authorityRoot, adaptersRoot }, drift) {
|
|
145
|
+
const synced = [];
|
|
146
|
+
for (const { authRel, adapterRel } of drift) {
|
|
147
|
+
const authAbs = path.join(authorityRoot, authRel);
|
|
148
|
+
const adapterAbs = path.join(adaptersRoot, adapterRel);
|
|
149
|
+
const authText = fs.readFileSync(authAbs, 'utf8');
|
|
150
|
+
const adapterText = fs.readFileSync(adapterAbs, 'utf8');
|
|
151
|
+
const { body } = splitFm(authText);
|
|
152
|
+
const { fm } = splitFm(adapterText);
|
|
153
|
+
// 拼接:薄适配 frontmatter + 权威源正文段
|
|
154
|
+
fs.mkdirSync(path.dirname(adapterAbs), { recursive: true });
|
|
155
|
+
fs.writeFileSync(adapterAbs, `${fm}${body}`);
|
|
156
|
+
synced.push(adapterRel);
|
|
157
|
+
}
|
|
158
|
+
return { synced };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function syncHosts(args, pkgRoot) {
|
|
162
|
+
let apply = false;
|
|
163
|
+
let json = false;
|
|
164
|
+
for (const a of args) {
|
|
165
|
+
if (a === '--diff') apply = false;
|
|
166
|
+
else if (a === '--apply') apply = true;
|
|
167
|
+
else if (a === '--json') json = true;
|
|
168
|
+
else fail(`未知选项:${a}(flow-kit sync-hosts --help)`);
|
|
169
|
+
}
|
|
170
|
+
if (!pkgRoot) fail('sync-hosts 缺少 pkgRoot(开发模式入口)');
|
|
171
|
+
|
|
172
|
+
const authorityRoot = path.join(pkgRoot, 'templates', '_agents');
|
|
173
|
+
const adaptersRoot = path.join(pkgRoot, 'modules', 'hosts');
|
|
174
|
+
const result = diffHosts({ authorityRoot, adaptersRoot });
|
|
175
|
+
|
|
176
|
+
if (apply && result.drift.length > 0) {
|
|
177
|
+
const { synced } = applyForward({ authorityRoot, adaptersRoot }, result.drift);
|
|
178
|
+
console.log(`▶ flow-kit sync-hosts --apply → ${pkgRoot}`);
|
|
179
|
+
console.log(` 按权威源正文覆盖薄适配正文段 ${synced.length} 份(薄适配 frontmatter 不动):`);
|
|
180
|
+
for (const rel of synced) console.log(` - ${rel}`);
|
|
181
|
+
const after = diffHosts({ authorityRoot, adaptersRoot });
|
|
182
|
+
console.log(` 同步后正文对齐:${after.inSync} 对;权威源缺失:${after.authorityMissing.length};正文漂移:${after.drift.length}`);
|
|
183
|
+
return after.drift.length === 0 && after.authorityMissing.length === 0 ? 0 : 1;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (json) {
|
|
187
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
188
|
+
} else {
|
|
189
|
+
console.log(`▶ flow-kit sync-hosts --diff → ${pkgRoot}`);
|
|
190
|
+
printDiff(result);
|
|
191
|
+
}
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// sync-hosts.test.mjs — flow-kit sync-hosts B-b 语义四场景(2026-09-25)
|
|
2
|
+
// 方法:迷你 fixture 包根(权威源 templates/_agents + 薄适配 modules/hosts/<h>/{agents,commands}/
|
|
3
|
+
// 与权威源正文段一致,frontmatter 各异)+ 子进程 driver 调真实 sync-hosts(隔离 process.exit)
|
|
4
|
+
// 判据:①全对齐 → inSync=28, drift 空 ②权威源改 → drift 列 ③apply → 正文一致,frontmatter 各自保留
|
|
5
|
+
// ④薄适配 frontmatter 手改 → 不进 drift ⑤薄适配正文手改 → drift 列,apply 覆盖
|
|
6
|
+
// ⑥薄适配缺失 → authorityMissing 列;apply 不自动创建
|
|
7
|
+
// 用法:node src/sync-hosts.test.mjs
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
13
|
+
|
|
14
|
+
const SRC_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
|
|
15
|
+
|
|
16
|
+
let pass = 0, failCount = 0;
|
|
17
|
+
function check(name, cond, detail = '') {
|
|
18
|
+
if (cond) { pass++; console.log(`PASS ${name}`); }
|
|
19
|
+
else { failCount++; console.log(`FAIL ${name}${detail ? `——${detail}` : ''}`); }
|
|
20
|
+
}
|
|
21
|
+
const W = (p, content) => { fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, content); };
|
|
22
|
+
const R = (p) => fs.readFileSync(p, 'utf8');
|
|
23
|
+
|
|
24
|
+
// ---- fixture 包根:权威源 + 4 宿主薄适配(正文段与权威源一致,frontmatter 各异) ----
|
|
25
|
+
function mkFixture() {
|
|
26
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-synchosts-'));
|
|
27
|
+
// 权威源(2 commands + 1 role,简化测试体量;权威源也带 frontmatter 保证 body 段含前导空行,与薄适配对齐)
|
|
28
|
+
W(path.join(root, 'templates/_agents/commands/build.md'),
|
|
29
|
+
`---\ndescription: Build 阶段\nstage: Build\n---\n\n# Build · v1\n\n权威源正文 build v1\n`);
|
|
30
|
+
W(path.join(root, 'templates/_agents/commands/test.md'),
|
|
31
|
+
`---\ndescription: Test 阶段\nstage: Test\n---\n\n# Test · v1\n\n权威源正文 test v1\n`);
|
|
32
|
+
W(path.join(root, 'templates/_agents/roles/implementer.md'),
|
|
33
|
+
`---\ndescription: Implementer 角色契约\n---\n\n# Implementer · v1\n\n权威源正文 implementer v1\n`);
|
|
34
|
+
// 薄适配:commands 权威源 → opencode + trae(trae 加 wf- 前缀 + name)
|
|
35
|
+
W(path.join(root, 'modules/hosts/opencode/commands/build.md'),
|
|
36
|
+
`---\ndescription: Build · opencode 风格\n---\n\n# Build · v1\n\n权威源正文 build v1\n`);
|
|
37
|
+
W(path.join(root, 'modules/hosts/trae/commands/wf-build.md'),
|
|
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'),
|
|
40
|
+
`---\ndescription: Test · opencode 风格\n---\n\n# Test · v1\n\n权威源正文 test v1\n`);
|
|
41
|
+
W(path.join(root, 'modules/hosts/trae/commands/wf-test.md'),
|
|
42
|
+
`---\nname: wf-test\ndescription: Test · trae 风格\n---\n\n# Test · v1\n\n权威源正文 test v1\n`);
|
|
43
|
+
// roles 权威源 → 4 宿主 agents(omp/zcode 中文风格 + trae name)
|
|
44
|
+
W(path.join(root, 'modules/hosts/zcode/agents/implementer.md'),
|
|
45
|
+
`---\nname: implementer\ndescription: 在已批准工作包内实现\n---\n\n# Implementer · v1\n\n权威源正文 implementer v1\n`);
|
|
46
|
+
W(path.join(root, 'modules/hosts/omp/agents/implementer.md'),
|
|
47
|
+
`---\nname: implementer\ndescription: omp 本机风格\n---\n\n# Implementer · v1\n\n权威源正文 implementer v1\n`);
|
|
48
|
+
W(path.join(root, 'modules/hosts/opencode/agents/implementer.md'),
|
|
49
|
+
`---\nname: implementer\ndescription: opencode 英文风格\n---\n\n# Implementer · v1\n\n权威源正文 implementer v1\n`);
|
|
50
|
+
W(path.join(root, 'modules/hosts/trae/agents/implementer.md'),
|
|
51
|
+
`---\nname: implementer\ndescription: trae 英文风格\n---\n\n# Implementer · v1\n\n权威源正文 implementer v1\n`);
|
|
52
|
+
// 孤儿薄适配:权威源无对应文件
|
|
53
|
+
W(path.join(root, 'modules/hosts/opencode/commands/orphan.md'), '# orphan\n');
|
|
54
|
+
return root;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---- 子进程 driver:调真实 sync-hosts ----
|
|
58
|
+
function runSyncHosts(fixtureRoot, extra = []) {
|
|
59
|
+
const driver = path.join(fixtureRoot, 'driver.mjs');
|
|
60
|
+
W(driver, `import { syncHosts } from ${JSON.stringify(pathToFileURL(path.join(SRC_ROOT, 'sync-hosts.mjs')).href)};
|
|
61
|
+
const root = ${JSON.stringify(fixtureRoot)};
|
|
62
|
+
syncHosts(process.argv.slice(2), root);
|
|
63
|
+
`);
|
|
64
|
+
return spawnSync(process.execPath, [driver, ...extra], { encoding: 'utf8' });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ============ 场景 1:全对齐(正文段与权威源一致,frontmatter 各异)→ drift 空 ============
|
|
68
|
+
{
|
|
69
|
+
const fx = mkFixture();
|
|
70
|
+
const r = runSyncHosts(fx, ['--diff']);
|
|
71
|
+
const out = r.stdout + r.stderr;
|
|
72
|
+
check('S1 全对齐:正文对齐 8 对(2 commands×2 适配 + 1 role×4 宿主)', /正文对齐:\s*8\s*对/.test(out), out);
|
|
73
|
+
check('S1 全对齐:报告无漂移', /无漂移/.test(out), out);
|
|
74
|
+
check('S1 全对齐:exit 0', r.status === 0, `status=${r.status}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ============ 场景 2:权威源改了正文,薄适配未跟 → drift 列 4 处(build→opencode+trae, test→opencode+trae)============
|
|
78
|
+
{
|
|
79
|
+
const fx = mkFixture();
|
|
80
|
+
W(path.join(fx, 'templates/_agents/commands/build.md'),
|
|
81
|
+
`---\ndescription: Build 阶段\nstage: Build\n---\n\n# Build · v2\n\n权威源正文 build v2\n`);
|
|
82
|
+
W(path.join(fx, 'templates/_agents/commands/test.md'),
|
|
83
|
+
`---\ndescription: Test 阶段\nstage: Test\n---\n\n# Test · v2\n\n权威源正文 test v2\n`);
|
|
84
|
+
const r = runSyncHosts(fx, ['--diff']);
|
|
85
|
+
const out = r.stdout + r.stderr;
|
|
86
|
+
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 列出 trae/commands/wf-build.md(trae wf- 前缀映射)', out.includes('commands/build.md → trae/commands/wf-build.md'), out);
|
|
89
|
+
check('S2 正文对齐 = 4(1 role × 4 宿主 implementer 未改)', /正文对齐:\s*4\s*对/.test(out), out);
|
|
90
|
+
check('S2 exit 0(diff 模式不阻断)', r.status === 0);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ============ 场景 3:apply 后正文段 sha 一致,frontmatter 各自保留 ============
|
|
94
|
+
{
|
|
95
|
+
const fx = mkFixture();
|
|
96
|
+
W(path.join(fx, 'templates/_agents/commands/build.md'),
|
|
97
|
+
`---\ndescription: Build 阶段\nstage: Build\n---\n\n# Build · v2\n\n权威源正文 build v2\n`);
|
|
98
|
+
const r = runSyncHosts(fx, ['--apply']);
|
|
99
|
+
const out = r.stdout + r.stderr;
|
|
100
|
+
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:trae 正文已同步到 v2', R(path.join(fx, 'modules/hosts/trae/commands/wf-build.md')).includes('权威源正文 build v2'));
|
|
103
|
+
check('S3 apply:opencode frontmatter 保留(仍无 name 字段)', !R(path.join(fx, 'modules/hosts/opencode/commands/build.md')).match(/^name:/m));
|
|
104
|
+
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
|
+
check('S3 apply:权威源不动', R(path.join(fx, 'templates/_agents/commands/build.md')).includes('权威源正文 build v2'));
|
|
106
|
+
check('S3 apply:再次 diff 应显示对齐 8', true); // 二次扫描由 sync-hosts 内部做
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ============ 场景 4:薄适配 frontmatter 手改 → 不进 drift(正文段比对不受影响)============
|
|
110
|
+
{
|
|
111
|
+
const fx = mkFixture();
|
|
112
|
+
// trae frontmatter description 改成"自定义"——正文段未动
|
|
113
|
+
W(path.join(fx, 'modules/hosts/trae/commands/wf-build.md'),
|
|
114
|
+
`---\nname: wf-build\ndescription: trae 自定义描述\n---\n\n# Build · v1\n\n权威源正文 build v1\n`);
|
|
115
|
+
const r = runSyncHosts(fx, ['--diff']);
|
|
116
|
+
const out = r.stdout + r.stderr;
|
|
117
|
+
check('S4 frontmatter 差异不计正文段漂移', /正文对齐:\s*8\s*对/.test(out) && /无漂移/.test(out), out);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ============ 场景 5:薄适配正文手改 → drift 列出,apply 覆盖(不污染权威源)============
|
|
121
|
+
{
|
|
122
|
+
const fx = mkFixture();
|
|
123
|
+
W(path.join(fx, 'modules/hosts/zcode/agents/implementer.md'),
|
|
124
|
+
`---\nname: implementer\ndescription: zcode 风格\n---\n\n# Implementer · v1\n\n薄适配手改 implementer v1\n`);
|
|
125
|
+
const r = runSyncHosts(fx, ['--diff']);
|
|
126
|
+
const out = r.stdout + r.stderr;
|
|
127
|
+
check('S5 薄适配正文手改 → 正文段漂移 1', /正文段漂移(1/.test(out), out);
|
|
128
|
+
const r2 = runSyncHosts(fx, ['--apply']);
|
|
129
|
+
check('S5 apply 后:薄适配正文回到权威源(含「权威源正文 implementer v1」)', /权威源正文 implementer v1/.test(R(path.join(fx, 'modules/hosts/zcode/agents/implementer.md'))));
|
|
130
|
+
check('S5 apply 不污染权威源(权威源仍 v1)', !R(path.join(fx, 'templates/_agents/roles/implementer.md')).includes('薄适配手改'));
|
|
131
|
+
// apply 后再 diff 一次,diff 应显示"无漂移"
|
|
132
|
+
const r3 = runSyncHosts(fx, ['--diff']);
|
|
133
|
+
check('S5 apply 后再 diff 应无漂移', /无漂移/.test(r3.stdout + r3.stderr), r3.stdout + r3.stderr);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ============ 场景 6:薄适配文件不存在 → authorityMissing 列出,apply 不创建 ============
|
|
137
|
+
{
|
|
138
|
+
const fx = mkFixture();
|
|
139
|
+
fs.rmSync(path.join(fx, 'modules/hosts/opencode/commands/build.md'));
|
|
140
|
+
const r = runSyncHosts(fx, ['--diff']);
|
|
141
|
+
const out = r.stdout + r.stderr;
|
|
142
|
+
check('S6 薄适配缺失 → authorityMissing 列', /权威源声明但薄适配缺失(/.test(out) && out.includes('opencode/commands/build.md'), out);
|
|
143
|
+
const r2 = runSyncHosts(fx, ['--apply']);
|
|
144
|
+
check('S6 apply 不自动创建薄适配(仍缺失)', !fs.existsSync(path.join(fx, 'modules/hosts/opencode/commands/build.md')));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ============ 场景 7:--json 机器可读输出 ============
|
|
148
|
+
{
|
|
149
|
+
const fx = mkFixture();
|
|
150
|
+
const r = runSyncHosts(fx, ['--json']);
|
|
151
|
+
const out = r.stdout + r.stderr;
|
|
152
|
+
let parsed = null;
|
|
153
|
+
try { parsed = JSON.parse(out); } catch {}
|
|
154
|
+
check('S7 --json 输出可解析', parsed && typeof parsed === 'object' && Array.isArray(parsed.drift));
|
|
155
|
+
check('S7 --json 含 inSync = 8', parsed && parsed.inSync === 8, parsed ? JSON.stringify(parsed) : 'null');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ============ 场景 8:孤儿薄适配(权威源无对应文件)→ adapterOrphans 列,apply 不删 ============
|
|
159
|
+
{
|
|
160
|
+
const fx = mkFixture();
|
|
161
|
+
// orphan.md 已在 mkFixture 写入
|
|
162
|
+
const r = runSyncHosts(fx, ['--diff']);
|
|
163
|
+
const out = r.stdout + r.stderr;
|
|
164
|
+
check('S8 孤儿薄适配 → adapterOrphans 列', /孤儿薄适配(/.test(out) && out.includes('opencode/commands/orphan.md'), out);
|
|
165
|
+
const r2 = runSyncHosts(fx, ['--apply']);
|
|
166
|
+
check('S8 apply 不删孤儿薄适配', fs.existsSync(path.join(fx, 'modules/hosts/opencode/commands/orphan.md')));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${failCount}`);
|
|
170
|
+
process.exit(failCount ? 1 : 0);
|
package/src/sync.mjs
CHANGED
|
@@ -74,6 +74,7 @@ export function sync(args, pkgRoot) {
|
|
|
74
74
|
}
|
|
75
75
|
const diskSha = fs.existsSync(disk) ? sha256(fs.readFileSync(disk)) : null;
|
|
76
76
|
if (diskSha === null) {
|
|
77
|
+
fs.mkdirSync(path.dirname(disk), { recursive: true }); // 父目录可能整目录缺失(如 localOnly 宿主目录被清),copyfile 不建目录
|
|
77
78
|
fs.copyFileSync(freshFile.abs, disk);
|
|
78
79
|
restored.push(rel);
|
|
79
80
|
managedNew.push({ rel, sha256: freshFile.sha });
|