agentic-flow-kit 1.7.0 → 1.7.16
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 +81 -9
- package/modules/gates/README.md +1 -0
- package/modules/gates/test-protect/README.md +23 -0
- package/modules/gates/test-protect/check-test-protect.mjs +64 -0
- package/modules/gates/test-protect/local-pre-commit.line +1 -0
- package/modules/hosts/README.md +57 -0
- package/modules/hosts/claude/commands/wf-build.md +5 -24
- package/modules/hosts/claude/commands/wf-deploy.md +1 -0
- package/modules/hosts/claude/commands/wf-design.md +7 -6
- package/modules/hosts/claude/commands/wf-gate-checklist.md +1 -3
- package/modules/hosts/claude/commands/wf-new-task.md +42 -21
- package/modules/hosts/claude/commands/wf-plan.md +1 -1
- package/modules/hosts/claude/commands/wf-review.md +1 -1
- package/modules/hosts/claude/commands/wf-sync-hosts.md +8 -16
- package/modules/hosts/claude/commands/wf-test.md +5 -4
- package/modules/hosts/codex/commands/wf-build.md +5 -24
- package/modules/hosts/codex/commands/wf-deploy.md +1 -0
- package/modules/hosts/codex/commands/wf-design.md +7 -6
- package/modules/hosts/codex/commands/wf-gate-checklist.md +1 -3
- package/modules/hosts/codex/commands/wf-new-task.md +42 -21
- package/modules/hosts/codex/commands/wf-plan.md +1 -1
- package/modules/hosts/codex/commands/wf-review.md +1 -1
- package/modules/hosts/codex/commands/wf-sync-hosts.md +8 -16
- package/modules/hosts/codex/commands/wf-test.md +5 -4
- package/modules/hosts/cursor/commands/wf-build.md +5 -24
- package/modules/hosts/cursor/commands/wf-deploy.md +1 -0
- package/modules/hosts/cursor/commands/wf-design.md +7 -6
- package/modules/hosts/cursor/commands/wf-gate-checklist.md +1 -3
- package/modules/hosts/cursor/commands/wf-new-task.md +42 -21
- package/modules/hosts/cursor/commands/wf-plan.md +1 -1
- package/modules/hosts/cursor/commands/wf-review.md +1 -1
- package/modules/hosts/cursor/commands/wf-sync-hosts.md +8 -16
- package/modules/hosts/cursor/commands/wf-test.md +5 -4
- package/modules/hosts/opencode/commands/wf-build.md +5 -24
- package/modules/hosts/opencode/commands/wf-deploy.md +1 -0
- package/modules/hosts/opencode/commands/wf-design.md +7 -6
- package/modules/hosts/opencode/commands/wf-gate-checklist.md +1 -3
- package/modules/hosts/opencode/commands/wf-new-task.md +42 -21
- package/modules/hosts/opencode/commands/wf-plan.md +1 -1
- package/modules/hosts/opencode/commands/wf-review.md +1 -1
- package/modules/hosts/opencode/commands/wf-sync-hosts.md +8 -16
- package/modules/hosts/opencode/commands/wf-test.md +5 -4
- package/modules/hosts/trae/commands/wf-build.md +5 -24
- package/modules/hosts/trae/commands/wf-deploy.md +1 -0
- package/modules/hosts/trae/commands/wf-design.md +7 -6
- package/modules/hosts/trae/commands/wf-gate-checklist.md +1 -3
- package/modules/hosts/trae/commands/wf-new-task.md +42 -21
- package/modules/hosts/trae/commands/wf-plan.md +1 -1
- package/modules/hosts/trae/commands/wf-review.md +1 -1
- package/modules/hosts/trae/commands/wf-sync-hosts.md +8 -16
- package/modules/hosts/trae/commands/wf-test.md +5 -4
- package/package.json +2 -2
- package/src/add-gate.mjs +25 -1
- package/src/add-gate.test.mjs +103 -0
- package/src/cli.mjs +59 -5
- package/src/doctor.mjs +136 -12
- package/src/executor.mjs +245 -0
- package/src/executor.test.mjs +292 -0
- package/src/gates.test.mjs +17 -0
- package/src/init.mjs +14 -1
- package/src/init.test.mjs +19 -0
- package/src/instance-ref.test.mjs +62 -0
- package/src/jscpd-drift.test.mjs +144 -0
- package/src/profiles.mjs +3 -0
- package/src/render.mjs +6 -1
- package/src/rule-budget.test.mjs +103 -0
- package/src/sync-hosts.mjs +41 -45
- package/src/sync-hosts.test.mjs +67 -56
- package/src/trae-hooks.test.mjs +15 -3
- package/src/validate.mjs +72 -0
- package/src/validate.test.mjs +95 -0
- package/templates/.gitattributes +8 -0
- package/templates/AGENTS.md +2 -1
- package/templates/_agents/commands/build.md +5 -24
- package/templates/_agents/commands/deploy.md +1 -0
- package/templates/_agents/commands/design.md +7 -6
- package/templates/_agents/commands/gate-checklist.md +1 -3
- package/templates/_agents/commands/new-task.md +42 -21
- package/templates/_agents/commands/plan.md +1 -1
- package/templates/_agents/commands/review.md +1 -1
- package/templates/_agents/commands/sync-hosts.md +8 -16
- package/templates/_agents/commands/test.md +5 -4
- package/templates/_agents/control-surfaces.txt +53 -0
- package/templates/_agents/engine-lock.json +246 -0
- package/templates/_agents/lane-surfaces.txt +1 -1
- package/templates/_agents/level-hints.txt +30 -0
- package/templates/_agents/scripts/agg-delegations.cjs +51 -6
- package/templates/_agents/scripts/agg-delegations.test.mjs +84 -4
- package/templates/_agents/scripts/check-engine-integrity.mjs +217 -0
- package/templates/_agents/scripts/check-engine-integrity.test.mjs +215 -0
- package/templates/_agents/scripts/check-lane-surface.mjs +78 -24
- package/templates/_agents/scripts/check-lane-surface.test.mjs +66 -0
- package/templates/_agents/scripts/check-ledger-invariant.mjs +112 -14
- package/templates/_agents/scripts/check-ledger-invariant.test.mjs +5 -1
- package/templates/_agents/scripts/check-loop-rev.test.mjs +6 -4
- package/templates/_agents/scripts/check-loop.mjs +255 -45
- package/templates/_agents/scripts/check-loop.test.mjs +330 -15
- package/templates/_agents/scripts/check-metric-claims.mjs +4 -2
- package/templates/_agents/scripts/compact-ledgers.mjs +97 -0
- package/templates/_agents/scripts/compact-ledgers.test.mjs +162 -0
- package/templates/_agents/scripts/confirm-doc.mjs +54 -12
- package/templates/_agents/scripts/confirm-doc.test.mjs +59 -0
- package/templates/_agents/scripts/doctor.test.mjs +294 -1
- package/templates/_agents/scripts/draft-sediment.mjs +178 -0
- package/templates/_agents/scripts/draft-sediment.test.mjs +272 -0
- package/templates/_agents/scripts/edit-face-check.test.mjs +5 -4
- package/templates/_agents/scripts/fill-intent.mjs +2 -2
- package/templates/_agents/scripts/fixture-scripts.mjs +153 -0
- package/templates/_agents/scripts/fixture-scripts.test.mjs +107 -0
- package/templates/_agents/scripts/gate-checklist.mjs +6 -3
- package/templates/_agents/scripts/gate-checklist.test.mjs +2 -5
- package/templates/_agents/scripts/gen-wiki-board.mjs +61 -6
- package/templates/_agents/scripts/gen-wiki-board.test.mjs +74 -1
- package/templates/_agents/scripts/gen-workflow-dashboard.mjs +10 -13
- package/templates/_agents/scripts/gen-workflow-metrics.mjs +12 -30
- package/templates/_agents/scripts/git-merge-jsonl.mjs +63 -0
- package/templates/_agents/scripts/git-merge-jsonl.test.mjs +125 -0
- package/templates/_agents/scripts/kb-search.mjs +87 -12
- package/templates/_agents/scripts/kb-search.test.mjs +83 -0
- package/templates/_agents/scripts/ledger-doc-chain.test.mjs +166 -0
- package/templates/_agents/scripts/ledger-loader.mjs +108 -0
- package/templates/_agents/scripts/ledger-loader.test.mjs +131 -0
- package/templates/_agents/scripts/ledger-prune.mjs +96 -0
- package/templates/_agents/scripts/ledger-prune.test.mjs +120 -0
- package/templates/_agents/scripts/ledger-ts-monotonic.test.mjs +135 -0
- package/templates/_agents/scripts/level-criteria.mjs +232 -0
- package/templates/_agents/scripts/level-criteria.test.mjs +279 -0
- package/templates/_agents/scripts/pipeline-run.mjs +5 -3
- package/templates/_agents/scripts/policy.mjs +47 -0
- package/templates/_agents/scripts/solidify-task.mjs +1 -1
- package/templates/_agents/scripts/stage-gates.mjs +52 -10
- package/templates/_agents/scripts/stage-gates.test.mjs +105 -0
- package/templates/_agents/scripts/verify-wiki-consistency.mjs +34 -0
- package/templates/_agents/scripts/verify-wiki-consistency.test.mjs +44 -2
- package/templates/_agents/scripts/verify.mjs +1 -1
- package/templates/_agents/scripts/wf-journal.test.mjs +1 -1
- package/templates/_agents/scripts/wiki-search.test.mjs +5 -6
- package/templates/_agents/scripts/workflows-check.mjs +1 -1
- package/templates/_agents/scripts-test-exempt.txt +3 -1
- package/templates/_agents/skills/wiki/SKILL.md +5 -3
- package/templates/_agents/workflows/pipeline-closing.md +4 -5
- package/templates/_githooks/pre-commit +3 -34
- package/templates/_githooks/pre-push +3 -3
- package/templates/wiki/INDEX.md +44 -1
- package/templates/wiki//347/237/245/350/257/206/346/262/211/346/267/200/346/200/273/350/247/210.html +22 -1
- package/templates/workflow/README.md +11 -0
- package/templates/workflow/delegations.md +5 -4
- package/templates/workflow/incidents/_TEMPLATE.md +2 -0
- package/templates/workflow/intents/_TEMPLATE.md +3 -0
- package/templates/workflow/plans/_TEMPLATE.md +4 -0
- package/templates/workflow/specs/_TEMPLATE.md +23 -0
- package/modules/hosts/claude/commands/wf-source-sync-check.md +0 -91
- package/modules/hosts/codex/commands/wf-source-sync-check.md +0 -91
- package/modules/hosts/cursor/commands/wf-source-sync-check.md +0 -91
- package/modules/hosts/opencode/commands/wf-source-sync-check.md +0 -91
- package/modules/hosts/trae/commands/wf-source-sync-check.md +0 -92
- package/templates/_agents/commands/source-sync-check.md +0 -105
- package/templates/_agents/scripts/check-ledger.mjs +0 -109
- package/templates/_agents/scripts/check-ledger.test.mjs +0 -197
- package/templates/_agents/scripts/source-sync-check.mjs +0 -156
- package/templates/_agents/scripts/source-sync-check.test.mjs +0 -281
- package/templates/_agents/workflows/source-sync-repair.md +0 -30
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// rule-budget.test.mjs — 纯包源预算自检门(2026-10-11 budget-self-check)——关闭「纯包源仓不跑装户预算门」盲区。
|
|
2
|
+
// 背景:常驻面预算单源 templates/_agents/rule-budgets.txt(装户命名空间),此前只在装户侧强制
|
|
3
|
+
// (pre-commit --staged 硬拦 / check-loop 13 --all warning);v1.7.1 起本仓纯包源(5ca9e8c 删装副本)、
|
|
4
|
+
// 两处都不跑——bb69e19/418960a 重构净增 78B 把 .agents/commands/ 推到 65581B > 65536B,包源侧零暴露,
|
|
5
|
+
// 装户首提被硬拦才发现(2026-10-11 S1 冒烟)。本套件把同一张预算表映射到包源路径核对,npm test / CI
|
|
6
|
+
// 直接拦超限——模板增长不再静默入包。run-tests 盘面 glob 自动挂载本套件。
|
|
7
|
+
// 口径(对照 templates/_agents/scripts/rule-budget.sh 单源,勿两处漂移):
|
|
8
|
+
// 字节 = LF 归一(工作树读入去 \r\n——等价 fsize 的 git cat-file 口径,core.autocrlf 检出形态无关);
|
|
9
|
+
// 目录合计 = 扁平 glob(直接子文件,不入子目录);pat=首空格前 / lim=末空格后;lim 非纯数字行跳过;
|
|
10
|
+
// # 注释与空行跳过。
|
|
11
|
+
// 与装户门的口径差异(诚实声明):
|
|
12
|
+
// - AGENTS.md 是渲染模板:装户拿到的是占位符展开后的文件,渲染形态权威仍在装户门;本套件按全部
|
|
13
|
+
// STACKS 渲染取 max(BOARD_PORT 按最坏 5 位端口)做包源侧近似——单源复用 profiles/render,不复制命令表。
|
|
14
|
+
// - fail-loud 项(装户侧是 skip,包源侧是打包回归——表不随包或常驻面失踪 = 装户门整体变暗):
|
|
15
|
+
// 预算表缺失 / 解析 0 行 / 映射目标不存在 / 文件 glob 零命中。装户侧专属面(无包源对应)SKIP 并注记。
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { renderContent } from './render.mjs';
|
|
20
|
+
import { STACKS, pickStackVars } from './profiles.mjs';
|
|
21
|
+
|
|
22
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
23
|
+
const BUDGETS = path.join(ROOT, 'templates', '_agents', 'rule-budgets.txt');
|
|
24
|
+
|
|
25
|
+
let pass = 0, fail = 0;
|
|
26
|
+
function check(name, cond, detail = '') {
|
|
27
|
+
if (cond) { pass++; console.log(`PASS ${name}`); }
|
|
28
|
+
else { fail++; console.log(`FAIL ${name}${detail ? `——${detail}` : ''}`); }
|
|
29
|
+
}
|
|
30
|
+
const lfBytes = (abs) => Buffer.byteLength(fs.readFileSync(abs, 'utf8').replace(/\r\n/g, '\n'), 'utf8');
|
|
31
|
+
|
|
32
|
+
// ---- 预算表(缺失即 fail-loud:装户门会整体变暗)----
|
|
33
|
+
if (!fs.existsSync(BUDGETS)) {
|
|
34
|
+
console.log('FAIL R0 预算表缺失 templates/_agents/rule-budgets.txt——装户侧 pre-commit/检查13 将整体 skip');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
const rows = [];
|
|
38
|
+
for (const line of fs.readFileSync(BUDGETS, 'utf8').split('\n')) {
|
|
39
|
+
const t = line.replace(/\r/g, ''); // 表文件检出即 CRLF——显式去 \r(rule-budget.sh 同款)
|
|
40
|
+
if (!t || t.startsWith('#')) continue;
|
|
41
|
+
const i = t.indexOf(' ');
|
|
42
|
+
if (i < 0) continue;
|
|
43
|
+
const pat = t.slice(0, i);
|
|
44
|
+
const lim = t.slice(t.lastIndexOf(' ') + 1);
|
|
45
|
+
if (!/^\d+$/.test(lim)) continue; // 非纯数字 lim 跳过(sh case 同款)
|
|
46
|
+
rows.push({ pat, lim: Number(lim) });
|
|
47
|
+
}
|
|
48
|
+
check('R1 预算表解析非空', rows.length > 0, `仅 ${rows.length} 行——表被清空即盲区复活`);
|
|
49
|
+
|
|
50
|
+
// ---- 映射:装户命名空间 → 包源路径(新预算行若只属装户侧,在此登记映射或留 SKIP)----
|
|
51
|
+
function mapToPkg(pat) {
|
|
52
|
+
if (pat === 'AGENTS.md') return 'templates/AGENTS.md';
|
|
53
|
+
if (pat.startsWith('.agents/')) return 'templates/_agents/' + pat.slice('.agents/'.length);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const REMEDY = '处置沿先例:压缩版本括号/措辞(71e0b0e、v1.7.10 三处),或在 rule-budgets.txt 注明理由调预算(512B 步进)';
|
|
58
|
+
|
|
59
|
+
for (const { pat, lim } of rows) {
|
|
60
|
+
const rel = mapToPkg(pat);
|
|
61
|
+
if (!rel) { console.log(`SKIP ${pat}——装户侧专属面,无包源对应`); continue; }
|
|
62
|
+
const abs = path.join(ROOT, rel);
|
|
63
|
+
if (pat.endsWith('/')) {
|
|
64
|
+
// 目录合计:扁平直接子文件,不入子目录(sh `for f in "$pat"*` + [ -f ] 同款)
|
|
65
|
+
if (!fs.existsSync(abs)) { check(`R-dir ${pat} 目标存在`, false, `${rel} 缺失`); continue; }
|
|
66
|
+
const files = fs.readdirSync(abs, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name);
|
|
67
|
+
const total = files.reduce((s, n) => s + lfBytes(path.join(abs, n)), 0);
|
|
68
|
+
check(`R-dir ${rel} 合计 ${total}B ≤ ${lim}B(${files.length} 文件)`, total <= lim, `超 ${total - lim}B。${REMEDY}`);
|
|
69
|
+
} else if (pat.includes('*')) {
|
|
70
|
+
// 文件 glob:仅支持 dir 内后缀形(*.md)——与 sh glob 展开同效
|
|
71
|
+
const dir = path.dirname(abs);
|
|
72
|
+
const base = path.basename(pat);
|
|
73
|
+
const suffix = base.slice(base.indexOf('*') + 1);
|
|
74
|
+
const files = fs.existsSync(dir) ? fs.readdirSync(dir).filter((n) => n.endsWith(suffix)) : [];
|
|
75
|
+
if (files.length === 0) { check(`R-glob ${pat} 有命中`, false, `${dir} 下无 *${suffix}——常驻面失踪或映射失效`); continue; }
|
|
76
|
+
const sizes = files.map((n) => ({ n, b: lfBytes(path.join(dir, n)) }));
|
|
77
|
+
const over = sizes.filter((s) => s.b > lim);
|
|
78
|
+
const maxS = sizes.slice().sort((a, b) => b.b - a.b)[0];
|
|
79
|
+
check(`R-glob ${path.dirname(rel)}/${base} ${sizes.length} 份,最大 ${maxS.n} ${maxS.b}B ≤ ${lim}B`, over.length === 0,
|
|
80
|
+
`${over.map((s) => `${s.n}=${s.b}B 超 ${s.b - lim}B`).join(';')}。${REMEDY}`);
|
|
81
|
+
} else {
|
|
82
|
+
if (!fs.existsSync(abs)) { check(`R-file ${pat} 目标存在`, false, `${rel} 缺失`); continue; }
|
|
83
|
+
const b = lfBytes(abs);
|
|
84
|
+
check(`R-file ${rel} = ${b}B ≤ ${lim}B`, b <= lim, `超 ${b - lim}B。${REMEDY}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---- AGENTS.md 渲染近似(装户渲染形态 = 占位符展开;包源侧按全部栈取 max)----
|
|
89
|
+
const agentsRow = rows.find((r) => r.pat === 'AGENTS.md');
|
|
90
|
+
if (agentsRow) {
|
|
91
|
+
const tpl = fs.readFileSync(path.join(ROOT, 'templates', 'AGENTS.md'), 'utf8');
|
|
92
|
+
let worst = { stack: '-', b: 0 };
|
|
93
|
+
for (const stack of Object.keys(STACKS)) {
|
|
94
|
+
const text = renderContent(tpl, { BOARD_PORT: '99999', ...pickStackVars(stack) });
|
|
95
|
+
const b = Buffer.byteLength(text.replace(/\r\n/g, '\n'), 'utf8');
|
|
96
|
+
if (b > worst.b) worst = { stack, b };
|
|
97
|
+
}
|
|
98
|
+
check(`R-render templates/AGENTS.md 渲染 max(${worst.stack}) ${worst.b}B ≤ ${agentsRow.lim}B`, worst.b <= agentsRow.lim,
|
|
99
|
+
`超 ${worst.b - agentsRow.lim}B——渲染形态超限装户门同样会拦,先压模板正文。${REMEDY}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
103
|
+
process.exit(fail ? 1 : 0);
|
package/src/sync-hosts.mjs
CHANGED
|
@@ -26,8 +26,10 @@ function splitFm(text) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
// bodySha:剥离 frontmatter 后的正文段 sha256(缺失返 null);正文 LF 归一(2026-09-27 closing-coverage:
|
|
29
|
-
// CRLF/LF
|
|
30
|
-
|
|
29
|
+
// CRLF/LF 盘面同值——权威源与薄适配行尾不一致不再误报漂移)。
|
|
30
|
+
// 单源 export(2026-10-11 bodysha-single-source):doctor §6.7 checkAdapterDrift 复用——此前 doctor
|
|
31
|
+
// 内联第二份实现靠注释约束同口径,收敛为单源 import 消灭漂移面。
|
|
32
|
+
export function bodySha(p) {
|
|
31
33
|
try {
|
|
32
34
|
const { body } = splitFm(fs.readFileSync(p, 'utf8'));
|
|
33
35
|
return sha256(Buffer.from(body.replace(/\r\n/g, '\n'), 'utf8'));
|
|
@@ -45,10 +47,8 @@ function listMd(dir) {
|
|
|
45
47
|
// pairsFor:权威源文件 → 薄适配 rel 列表(映射单源见 profiles.mjs#HOSTS.commandPrefix)
|
|
46
48
|
// commands 权威源 → 带 commandPrefix 的宿主各一份(<dir>/commands/<prefix><name>.md)
|
|
47
49
|
// roles 权威源 → 全部宿主 agents 各一份
|
|
48
|
-
// hostDirOf:宿主 →
|
|
49
|
-
//
|
|
50
|
-
// hostFilter(2026-10-09 sync-hosts-root-coverage):宿主级过滤(多根模式下「项目根宿主点」根只扫
|
|
51
|
-
// 实际存在 dir 的宿主——不存在=没装该宿主点,不算缺失不算漂移)。
|
|
50
|
+
// hostDirOf:宿主 → 适配目录名。包源调用传入 (h) => h,配对落在 modules/hosts/<宿主键>/。
|
|
51
|
+
// hostFilter:宿主级过滤。调用方只扫实际存在的宿主目录时传入。
|
|
52
52
|
function pairsFor(authRel, hostDirOf = (h) => h, hostFilter = null) {
|
|
53
53
|
const out = [];
|
|
54
54
|
const norm = authRel.split(path.sep).join('/');
|
|
@@ -72,12 +72,9 @@ function pairsFor(authRel, hostDirOf = (h) => h, hostFilter = null) {
|
|
|
72
72
|
|
|
73
73
|
// diffHosts:权威源正文段 vs 薄适配正文段的漂移分析(B-b 语义;pure function)。
|
|
74
74
|
// authorityRoot:权威源根(含 commands/ + roles/)
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
// 根因教训(engine-quality-round2 B1):包源布局此前只扫 modules/hosts,项目根宿主点(本仓 .zcode/)
|
|
79
|
-
// 完全不在扫描面——bb69e19「81 对全对齐」声明漏掉 .zcode 三份(独立复核 P2-3 实证)。
|
|
80
|
-
// hostDirOf:宿主 → 适配目录名(默认宿主键 = 包源布局;装户布局传 (h) => HOSTS[h].dir)
|
|
75
|
+
// adapterRoots = [{root, hostDirOf, hostFilter?, label?}]。只扫 modules/hosts。
|
|
76
|
+
// 向后兼容:旧单根入参 adaptersRoot/hostDirOf 归一为单元素数组。
|
|
77
|
+
// hostDirOf:宿主 → 适配目录名(默认宿主键)
|
|
81
78
|
// 返回:{ drift, inSync, authorityMissing, adapterOrphans, faces }
|
|
82
79
|
// drift 条目自带 root(applyForward 按它定位适配文件);faces = 对账面自描述行(防不可解释的「N 对」)
|
|
83
80
|
export function diffHosts(opts) {
|
|
@@ -194,55 +191,35 @@ function applyForward({ authorityRoot }, drift) {
|
|
|
194
191
|
export function syncHosts(args, pkgRoot) {
|
|
195
192
|
let apply = false;
|
|
196
193
|
let json = false;
|
|
194
|
+
let strict = false;
|
|
197
195
|
let dir = null;
|
|
198
196
|
for (let i = 0; i < args.length; i++) {
|
|
199
197
|
const a = args[i];
|
|
200
198
|
if (a === '--diff') apply = false;
|
|
201
199
|
else if (a === '--apply') apply = true;
|
|
202
200
|
else if (a === '--json') json = true;
|
|
201
|
+
else if (a === '--strict') strict = true;
|
|
203
202
|
else if (a === '--dir') { dir = args[++i]; if (!dir) fail('--dir 缺少参数(目标项目根)'); }
|
|
204
203
|
else fail(`未知选项:${a}(flow-kit sync-hosts --help)`);
|
|
205
204
|
}
|
|
206
205
|
if (!pkgRoot) fail('sync-hosts 缺少 pkgRoot(开发模式入口)');
|
|
207
206
|
|
|
208
|
-
// 目标根:--dir > cwd > pkgRoot
|
|
209
|
-
//
|
|
210
|
-
// 解析为**包安装目录**,于是 doctor §7.x 报漂移、照提示跑 `--apply` 会去改包源仓(装户自己的适配层纹丝不动)。
|
|
207
|
+
// 目标根:--dir > cwd > pkgRoot。只认包源仓(templates/_agents + modules/hosts)。
|
|
208
|
+
// 装户仓的宿主文件由 init / add-host 写入,不在这里把 .agents 再当成第二份权威源。
|
|
211
209
|
const target = path.resolve(dir || process.cwd() || pkgRoot);
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
// 漏掉其三份,独立复核 P2-3 实证);不存在 = 没装该宿主点,不算缺失。
|
|
217
|
-
// 装户(其余)→ 权威源 = <target>/.agents,薄适配根 = <target>(宿主目录 .opencode/.trae/… 直接位于项目根)
|
|
218
|
-
const isSourceRepo = fs.existsSync(path.join(target, 'templates', '_agents'))
|
|
219
|
-
&& fs.existsSync(path.join(target, 'modules', 'hosts'));
|
|
220
|
-
const authorityRoot = isSourceRepo ? path.join(target, 'templates', '_agents') : path.join(target, '.agents');
|
|
221
|
-
const adapterRoots = isSourceRepo
|
|
222
|
-
? [
|
|
223
|
-
{ root: path.join(target, 'modules', 'hosts'), hostDirOf: (h) => h, label: 'modules/hosts' },
|
|
224
|
-
{
|
|
225
|
-
root: target,
|
|
226
|
-
hostDirOf: (h) => (HOSTS[h] && HOSTS[h].dir) || h,
|
|
227
|
-
hostFilter: (h) => HOSTS[h] && fs.existsSync(path.join(target, HOSTS[h].dir)),
|
|
228
|
-
label: '项目根宿主点',
|
|
229
|
-
},
|
|
230
|
-
]
|
|
231
|
-
: [{
|
|
232
|
-
root: target,
|
|
233
|
-
hostDirOf: (h) => (HOSTS[h] && HOSTS[h].dir) || h,
|
|
234
|
-
hostFilter: (h) => HOSTS[h] && fs.existsSync(path.join(target, HOSTS[h].dir)),
|
|
235
|
-
label: '项目根宿主点',
|
|
236
|
-
}];
|
|
237
|
-
if (!fs.existsSync(authorityRoot)) {
|
|
238
|
-
fail(`未找到权威源目录:${authorityRoot}——目标根 ${target} 既非包源(无 templates/_agents)也非装户(无 .agents);用 --dir <项目根> 指定`);
|
|
210
|
+
const authorityRoot = path.join(target, 'templates', '_agents');
|
|
211
|
+
const hostsRoot = path.join(target, 'modules', 'hosts');
|
|
212
|
+
if (!fs.existsSync(authorityRoot) || !fs.existsSync(hostsRoot)) {
|
|
213
|
+
fail(`sync-hosts 只在包源仓运行(需要 templates/_agents 与 modules/hosts)。目标根 ${target} 不是包源仓。装户的宿主文件由 init / add-host 写入。`);
|
|
239
214
|
}
|
|
240
|
-
const
|
|
215
|
+
const adapterRoots = [
|
|
216
|
+
{ root: hostsRoot, hostDirOf: (h) => h, label: 'modules/hosts' },
|
|
217
|
+
];
|
|
241
218
|
const result = diffHosts({ authorityRoot, adapterRoots });
|
|
242
219
|
|
|
243
220
|
if (apply && result.drift.length > 0) {
|
|
244
221
|
const { synced } = applyForward({ authorityRoot }, result.drift);
|
|
245
|
-
console.log(`▶ flow-kit sync-hosts --apply → ${target}
|
|
222
|
+
console.log(`▶ flow-kit sync-hosts --apply → ${target}(包源)`);
|
|
246
223
|
console.log(` 对账面:${result.faces.join(' + ')}`);
|
|
247
224
|
console.log(` 按权威源正文覆盖薄适配正文段 ${synced.length} 份(薄适配 frontmatter 不动):`);
|
|
248
225
|
for (const rel of synced) console.log(` - ${rel}`);
|
|
@@ -254,8 +231,27 @@ export function syncHosts(args, pkgRoot) {
|
|
|
254
231
|
if (json) {
|
|
255
232
|
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
256
233
|
} else {
|
|
257
|
-
console.log(`▶ flow-kit sync-hosts --diff → ${target}
|
|
234
|
+
console.log(`▶ flow-kit sync-hosts --diff → ${target}(包源)`);
|
|
258
235
|
printDiff(result);
|
|
259
236
|
}
|
|
237
|
+
// --strict(B4,2026-10-13):漂移**报告器**变机器门。
|
|
238
|
+
// 背景:source-sync-check 删除后,跨宿主正文漂移只剩人工跑一次 --diff,CI 里无任何调用点——
|
|
239
|
+
// 而改权威源要同步 N 份薄适配,人工 grep 易漏(本次评审前 10 份薄适配就带着漂移)。
|
|
240
|
+
// 默认仍返回 0(保持「报告器」语义,人工跑不因漂移而失败);CI 用 --strict 把漂移变成红。
|
|
241
|
+
// 口径:**只判「跑一次 --apply 就能归零」的两项**(正文漂移 + 权威源缺失)。
|
|
242
|
+
// 孤儿薄适配不判红——--apply 按设计不删它(可能是宿主特化/历史残留),
|
|
243
|
+
// 把它算红就是一条机器无法自愈的假红,按本仓「假阳性即噪声」纪律不能设成门(仍照常报告)。
|
|
244
|
+
if (strict) {
|
|
245
|
+
const fixable = result.drift.length + result.authorityMissing.length;
|
|
246
|
+
if (fixable > 0) {
|
|
247
|
+
console.error(`[跨宿主漂移 strict] 正文漂移 ${result.drift.length} · 权威源缺失 ${result.authorityMissing.length}`
|
|
248
|
+
+ `${result.adapterOrphans.length ? `(另有孤儿薄适配 ${result.adapterOrphans.length} 份,按设计不判红)` : ''}`
|
|
249
|
+
+ '——须先 flow-kit sync-hosts --apply 再提交');
|
|
250
|
+
return 1;
|
|
251
|
+
}
|
|
252
|
+
if (result.adapterOrphans.length) {
|
|
253
|
+
console.log(` ℹ️ 孤儿薄适配 ${result.adapterOrphans.length} 份(--apply 按设计不删,故 strict 不判红;确为残留请手工删)`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
260
256
|
return 0;
|
|
261
257
|
}
|
package/src/sync-hosts.test.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// 判据:①全对齐 → inSync=28, drift 空 ②权威源改 → drift 列 ③apply → 正文一致,frontmatter 各自保留
|
|
5
5
|
// ④薄适配 frontmatter 手改 → 不进 drift ⑤薄适配正文手改 → drift 列,apply 覆盖
|
|
6
6
|
// ⑥薄适配缺失 → authorityMissing 列;apply 不自动创建
|
|
7
|
-
// ⑦--json ⑧孤儿薄适配
|
|
7
|
+
// ⑦--json ⑧孤儿薄适配 ⑨非包源根拒绝(不把 .agents 当权威源)
|
|
8
8
|
// 用法:node src/sync-hosts.test.mjs
|
|
9
9
|
import fs from 'node:fs';
|
|
10
10
|
import path from 'node:path';
|
|
@@ -78,7 +78,18 @@ syncHosts(process.argv.slice(2), root);
|
|
|
78
78
|
return spawnSync(process.execPath, [driver, '--dir', fixtureRoot, ...extra], { encoding: 'utf8', ...opts });
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
//
|
|
81
|
+
// runSyncHostsExit:与 CLI 同口径**传播退出码**(cli.mjs 会 process.exit(syncHosts(...)))。
|
|
82
|
+
// 上面的 runSyncHosts driver 丢弃返回值,故退出码恒 0——不能用来断言 --strict 判红(B4)。
|
|
83
|
+
function runSyncHostsExit(fixtureRoot, extra = []) {
|
|
84
|
+
const driver = path.join(fixtureRoot, 'driver-exit.mjs');
|
|
85
|
+
W(driver, `import { syncHosts } from ${JSON.stringify(pathToFileURL(path.join(SRC_ROOT, 'sync-hosts.mjs')).href)};
|
|
86
|
+
const code = syncHosts(process.argv.slice(2), ${JSON.stringify(fixtureRoot)});
|
|
87
|
+
process.exit(typeof code === 'number' ? code : 0);
|
|
88
|
+
`);
|
|
89
|
+
return spawnSync(process.execPath, [driver, '--dir', fixtureRoot, ...extra], { encoding: 'utf8' });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 不传 --dir 形态:把 cwd 设为 fixture(验「默认当前目录」解析)
|
|
82
93
|
function runSyncHostsByCwd(fixtureRoot, extra = []) {
|
|
83
94
|
const driver = path.join(fixtureRoot, 'driver.mjs');
|
|
84
95
|
W(driver, `import { syncHosts } from ${JSON.stringify(pathToFileURL(path.join(SRC_ROOT, 'sync-hosts.mjs')).href)};
|
|
@@ -192,39 +203,23 @@ syncHosts(process.argv.slice(2), root);
|
|
|
192
203
|
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')));
|
|
193
204
|
}
|
|
194
205
|
|
|
195
|
-
// ============ 场景 9
|
|
196
|
-
// 2026-10-07 sync-hosts-target-fix:此前目标根恒为 pkgRoot(= 包安装目录),装户跑 sync-hosts
|
|
197
|
-
// 会去改包源仓。本场景钉住「--dir / cwd 指向装户根 → 按装户布局比对」。
|
|
206
|
+
// ============ 场景 9:非包源根拒绝(.agents 不再当权威源,宿主文件保持原样)============
|
|
198
207
|
{
|
|
199
208
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-synchosts-inst-'));
|
|
200
209
|
W(path.join(root, '.agents/commands/test.md'),
|
|
201
210
|
`---\ndescription: Test 阶段\n---\n\n# Test · v2\n\n权威源正文 test v2\n`);
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
W(path.join(root, '.trae/commands/wf-test.md'),
|
|
205
|
-
`---\nname: wf-test\ndescription: Test · trae\n---\n\n# Test · v1\n\n旧正文 test v1\n`);
|
|
211
|
+
const hostBody = `---\ndescription: Test · opencode\n---\n\n# Test · v1\n\n旧正文 test v1\n`;
|
|
212
|
+
W(path.join(root, '.opencode/commands/wf-test.md'), hostBody);
|
|
206
213
|
|
|
207
|
-
// 9a:--dir 指向装户根 → 布局判定「装户」,列出 2 处漂移
|
|
208
214
|
const r1 = runSyncHosts(root, ['--diff']);
|
|
209
215
|
const out1 = r1.stdout + r1.stderr;
|
|
210
|
-
check('S9
|
|
211
|
-
check('S9
|
|
212
|
-
// 未装宿主(claude/cursor/codex)目录不存在 → 不算「缺失」(否则装户刷屏 + apply 恒 exit 1)
|
|
213
|
-
check('S9 装户布局:未装宿主不进「权威源声明但薄适配缺失」', !/权威源声明但薄适配缺失/.test(out1), out1);
|
|
216
|
+
check('S9 非包源根 --diff:拒绝且点明只在包源仓运行', r1.status !== 0 && /只在包源仓运行/.test(out1), out1);
|
|
217
|
+
check('S9 非包源根 --diff:不改宿主文件', R(path.join(root, '.opencode/commands/wf-test.md')) === hostBody, out1);
|
|
214
218
|
|
|
215
|
-
|
|
216
|
-
const r2 = runSyncHostsByCwd(root, ['--diff']);
|
|
219
|
+
const r2 = runSyncHostsByCwd(root, ['--apply']);
|
|
217
220
|
const out2 = r2.stdout + r2.stderr;
|
|
218
|
-
check('S9 cwd
|
|
219
|
-
|
|
220
|
-
// 9c:apply → 正文段对齐、薄适配 frontmatter 保留
|
|
221
|
-
const r3 = runSyncHosts(root, ['--apply']);
|
|
222
|
-
const out3 = r3.stdout + r3.stderr;
|
|
223
|
-
const bodyOf = (p) => { const t = R(p); const m = t.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n/); return t.slice(m ? m[0].length : 0); };
|
|
224
|
-
check('S9 装户布局 apply:opencode 正文段对齐', bodyOf(path.join(root, '.opencode/commands/wf-test.md')) === bodyOf(path.join(root, '.agents/commands/test.md')), out3);
|
|
225
|
-
check('S9 装户布局 apply:trae frontmatter 保留(name: wf-test)', /^---\nname: wf-test/m.test(R(path.join(root, '.trae/commands/wf-test.md'))), out3);
|
|
226
|
-
// 未装宿主不计缺失 → apply 摘要「权威源缺失:0」且 exit 0
|
|
227
|
-
check('S9 装户布局 apply:未装宿主不计缺失 → 权威源缺失 0 且 exit 0', /权威源缺失:0/.test(out3) && r3.status === 0, `status=${r3.status}\n${out3}`);
|
|
221
|
+
check('S9 cwd 为装户根 --apply:同样拒绝', r2.status !== 0 && /只在包源仓运行/.test(out2), out2);
|
|
222
|
+
check('S9 cwd 为装户根 --apply:宿主文件未被改写', R(path.join(root, '.opencode/commands/wf-test.md')) === hostBody, out2);
|
|
228
223
|
}
|
|
229
224
|
|
|
230
225
|
{
|
|
@@ -237,43 +232,59 @@ syncHosts(process.argv.slice(2), root);
|
|
|
237
232
|
check('实仓薄适配无缺失无正文漂移', r.authorityMissing.length === 0 && r.drift.length === 0, `missing=${r.authorityMissing.length} drift=${r.drift.length} ${sample}`);
|
|
238
233
|
}
|
|
239
234
|
|
|
240
|
-
// ============ 场景 10
|
|
241
|
-
// 根因教训(bb69e19「81 对全对齐」漏 .zcode 三份,独立复核 P2-3):包源布局此前只扫 modules/hosts,
|
|
242
|
-
// 项目根 HOSTS.dir 形态宿主点(本仓 .zcode/)不在扫描面。三场景钉住:漂移可检出 / apply 可修复且
|
|
243
|
-
// frontmatter 保留 / 对账面自描述行在案。
|
|
235
|
+
// ============ 场景 10:包源仓不扫描项目根宿主点(.zcode 等不是第二对账面)============
|
|
244
236
|
{
|
|
245
|
-
// S10:包源 fixture + 项目根 .zcode/agents 漂移(正文旧 + frontmatter zcode 风格)→ --diff 报漂移
|
|
246
237
|
const fx = mkFixture();
|
|
247
|
-
|
|
248
|
-
|
|
238
|
+
const zcodeBody = `---\nname: implementer\ndescription: zcode 本仓风格\n---\n\n# Implementer · v0\n\n旧版正文 implementer v0\n`;
|
|
239
|
+
W(path.join(fx, '.zcode/agents/implementer.md'), zcodeBody);
|
|
249
240
|
const r = runSyncHosts(fx, ['--diff']);
|
|
250
241
|
const out = r.stdout + r.stderr;
|
|
251
|
-
check('S10
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
// S11:--apply 修复项目根漂移——正文覆盖为权威源、frontmatter 保留、漂移归零
|
|
256
|
-
const r2 = runSyncHosts(fx, ['--apply']);
|
|
257
|
-
const out2 = r2.stdout + r2.stderr;
|
|
258
|
-
const synced = /- \.zcode\/agents\/implementer\.md/.test(out2);
|
|
259
|
-
const fixed = R(path.join(fx, '.zcode/agents/implementer.md'));
|
|
260
|
-
check('S11 --apply 同步清单含 .zcode 条目', synced, out2.slice(0, 500));
|
|
261
|
-
check('S11 --apply 正文覆盖 + frontmatter 保留',
|
|
262
|
-
fixed.includes('权威源正文 implementer v1') && fixed.includes('description: zcode 本仓风格'),
|
|
263
|
-
fixed.slice(0, 200));
|
|
264
|
-
check('S11 --apply 后漂移归零', /正文漂移:\s*0/.test(out2), out2.slice(-300));
|
|
242
|
+
check('S10 对账面只有 modules/hosts', /对账面:modules\/hosts/.test(out) && !/项目根宿主点/.test(out), out.split('\n').slice(0, 6).join('\n'));
|
|
243
|
+
check('S10 项目根 .zcode 漂移不入报告', !out.includes('.zcode/agents/implementer.md'), out.slice(0, 500));
|
|
244
|
+
runSyncHosts(fx, ['--apply']);
|
|
245
|
+
check('S10 --apply 不改写项目根 .zcode', R(path.join(fx, '.zcode/agents/implementer.md')) === zcodeBody);
|
|
265
246
|
}
|
|
266
247
|
|
|
248
|
+
// ============ 场景 11:--strict 把漂移报告器变成机器门(B4)============
|
|
249
|
+
// 背景:source-sync-check 删除后,跨宿主正文漂移只剩人工跑一次 --diff,CI 里无任何调用点——
|
|
250
|
+
// 而改权威源要同步 N 份薄适配,人工 grep 易漏(本仓评审前就带着 10 份漂移)。
|
|
251
|
+
// 默认 --diff 仍恒 exit 0(人工跑不因漂移失败),--strict 才判红,供 CI 当门。
|
|
267
252
|
{
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
253
|
+
const fx = mkFixture();
|
|
254
|
+
// 先 --apply 归零:夹具出厂状态未必干净(默认就可能带孤儿/缺失),
|
|
255
|
+
// 「--strict 在无漂移时 exit 0」这句话得先有个干净盘面才成立。
|
|
256
|
+
runSyncHosts(fx, ['--apply']);
|
|
257
|
+
const clean = runSyncHostsExit(fx, ['--diff', '--strict']);
|
|
258
|
+
check('S11 无漂移 + --strict → exit 0', clean.status === 0, `status=${clean.status} ${(clean.stdout + clean.stderr).slice(0, 300)}`);
|
|
259
|
+
|
|
260
|
+
// 制造一处薄适配正文漂移
|
|
261
|
+
const anyAdapter = fs.readdirSync(path.join(fx, 'modules/hosts'), { withFileTypes: true })
|
|
262
|
+
.filter((d) => d.isDirectory()).map((d) => d.name);
|
|
263
|
+
let broke = null;
|
|
264
|
+
for (const host of anyAdapter) {
|
|
265
|
+
const cmdDir = path.join(fx, 'modules/hosts', host, 'commands');
|
|
266
|
+
if (!fs.existsSync(cmdDir)) continue;
|
|
267
|
+
const f = fs.readdirSync(cmdDir).find((x) => x.endsWith('.md'));
|
|
268
|
+
if (!f) continue;
|
|
269
|
+
const abs = path.join(cmdDir, f);
|
|
270
|
+
fs.appendFileSync(abs, '\n手改一行,权威源没有这段。\n');
|
|
271
|
+
broke = `${host}/commands/${f}`;
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
check('S11 夹具确实制造了漂移', !!broke, '未找到可改的薄适配');
|
|
275
|
+
|
|
276
|
+
const loose = runSyncHostsExit(fx, ['--diff']);
|
|
277
|
+
check('S11 有漂移但默认 --diff 仍 exit 0(报告器语义不变)', loose.status === 0, `status=${loose.status}`);
|
|
278
|
+
check('S11 默认 --diff 仍报告漂移', (loose.stdout + loose.stderr).includes('正文段漂移'), loose.stdout.slice(0, 300));
|
|
279
|
+
|
|
280
|
+
const strict = runSyncHostsExit(fx, ['--diff', '--strict']);
|
|
281
|
+
const sout = strict.stdout + strict.stderr;
|
|
282
|
+
check('S11 有漂移 + --strict → exit 1(CI 机器门)', strict.status === 1, `status=${strict.status}`);
|
|
283
|
+
check('S11 strict 报错点名修法(sync-hosts --apply)', sout.includes('--apply') && sout.includes('strict'), sout.slice(-300));
|
|
284
|
+
|
|
285
|
+
runSyncHosts(fx, ['--apply']);
|
|
286
|
+
const after = runSyncHostsExit(fx, ['--diff', '--strict']);
|
|
287
|
+
check('S11 --apply 归零后 --strict 回到 exit 0', after.status === 0, `status=${after.status}`);
|
|
277
288
|
}
|
|
278
289
|
|
|
279
290
|
console.log(`\n合计: PASS ${pass} / FAIL ${failCount}`);
|
package/src/trae-hooks.test.mjs
CHANGED
|
@@ -39,6 +39,17 @@ const runHook = (command, cwd) => {
|
|
|
39
39
|
return { decision, reason, raw: r.stdout || '', status: r.status };
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
+
// 门禁在位桩(2026-10-09 装户端内容清理后):纯包源仓无 .githooks/——hook 第二道关卡
|
|
43
|
+
// (commit/push 前置 .githooks/*)在仓内必判「门禁未执行」。①/②/②附 的 token 边界断言
|
|
44
|
+
// 需要「门禁通过」语境:造临时目录放通过型桩门禁当 cwd;③ 另用空目录保留「缺失 → 门禁未执行」deny。
|
|
45
|
+
const GATE_FIXTURE = fs.mkdtempSync(path.join(os.tmpdir(), 'trae-hook-gates-'));
|
|
46
|
+
fs.mkdirSync(path.join(GATE_FIXTURE, '.githooks'));
|
|
47
|
+
for (const name of ['pre-commit', 'pre-push']) {
|
|
48
|
+
const stub = path.join(GATE_FIXTURE, '.githooks', name);
|
|
49
|
+
fs.writeFileSync(stub, '#!/usr/bin/env bash\nexit 0\n');
|
|
50
|
+
fs.chmodSync(stub, 0o755);
|
|
51
|
+
}
|
|
52
|
+
|
|
42
53
|
// ---- ① 强推 token 化(P1-B2)----
|
|
43
54
|
{
|
|
44
55
|
const cases = [
|
|
@@ -54,7 +65,7 @@ const runHook = (command, cwd) => {
|
|
|
54
65
|
['dotnet build --force', 'none', '非 push 命令带 --force 不误拦'],
|
|
55
66
|
];
|
|
56
67
|
for (const [cmd, expect, label] of cases) {
|
|
57
|
-
const r = runHook(cmd);
|
|
68
|
+
const r = runHook(cmd, GATE_FIXTURE);
|
|
58
69
|
check(`①${label}(${cmd})→ ${expect === 'none' ? '放行' : expect}`,
|
|
59
70
|
r.decision === expect,
|
|
60
71
|
`decision=${r.decision} reason=${r.reason}`);
|
|
@@ -79,7 +90,7 @@ const runHook = (command, cwd) => {
|
|
|
79
90
|
['git commit -m "fix: 修复"', 'none'], // commit 走 .githooks 链(本仓有钩子且全绿);deny 门本身不应拦 commit
|
|
80
91
|
];
|
|
81
92
|
for (const [cmd, expect] of cases) {
|
|
82
|
-
const r = runHook(cmd,
|
|
93
|
+
const r = runHook(cmd, GATE_FIXTURE); // cwd=桩门禁目录:git commit 能通过第二道关卡,只测 deny/ask 门
|
|
83
94
|
check(`②${cmd} → ${expect === 'none' ? '不因 deny/ask 门拦' : expect}`,
|
|
84
95
|
r.decision === expect,
|
|
85
96
|
`decision=${r.decision} reason=${r.reason}`);
|
|
@@ -94,7 +105,7 @@ const runHook = (command, cwd) => {
|
|
|
94
105
|
['git commit -m "真正要拦的消息"', 'none', '普通消息照常'],
|
|
95
106
|
];
|
|
96
107
|
for (const [cmd, expect, label] of cases) {
|
|
97
|
-
const r = runHook(cmd,
|
|
108
|
+
const r = runHook(cmd, GATE_FIXTURE);
|
|
98
109
|
check(`②附 ${label} → ${expect === 'none' ? '不误拦' : expect}`,
|
|
99
110
|
r.decision === expect,
|
|
100
111
|
`decision=${r.decision} reason=${r.reason}`);
|
|
@@ -112,4 +123,5 @@ const runHook = (command, cwd) => {
|
|
|
112
123
|
}
|
|
113
124
|
|
|
114
125
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
126
|
+
fs.rmSync(GATE_FIXTURE, { recursive: true, force: true });
|
|
115
127
|
process.exit(fail ? 1 : 0);
|
package/src/validate.mjs
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// flow-kit validate:只读校验装户盘面(2026-10-10 validate-entry;对照 OpenSpec validate 形态:
|
|
2
|
+
// 只读 + 0/1 退出码 + --strict/--json)。
|
|
3
|
+
// 语义:用**包内置** check-loop 引擎(templates/_agents/scripts/check-loop.mjs 的 runCheckLoop,
|
|
4
|
+
// isMain 主守卫保证 import 零副作用)对目标仓 workflow/ 闭环跑全部 20 项检查——不执行装户侧
|
|
5
|
+
// .agents/scripts/*(供应链面:判定引擎随包版本走、目标侧零代码执行;doctor 走另一极:scriptGuard
|
|
6
|
+
// 先验后跑装户侧脚本,两命令互补不重复)。root 经 runCheckLoop({root}) 传入,spawnGit 以 cwd:ROOT
|
|
7
|
+
// 落在目标仓——tracked 过滤按目标仓 HEAD 算,与 git 钩子看到的提交面一致。
|
|
8
|
+
// 只读范围:check-loop 自身仅有的两处落盘都在 .agents/cache/ 运行时缓存(gate-stats 需 --gate-stats
|
|
9
|
+
// 才触发、added-dates 缓存 best-effort),且该目录已 gitignore——本命令不改任何文档与台账。
|
|
10
|
+
// 不暴露 --rev:runCheckLoop 中 rev 与 root 互斥(提交时点判定属钩子链职责,validate 只看盘面现状)。
|
|
11
|
+
// 输出契约:check-loop 的 HARD-BLOCK/WARN 两段式原样走 stderr(稳定输出契约,消费者 .githooks/pre-push);
|
|
12
|
+
// stdout 只承载本命令的汇总行 / --json——机器消费者按 stdout 解析,人按 stderr 读明细。
|
|
13
|
+
// 退出码:blocker → 1;--strict 下 advisory warning 同样 → 1(判据单源 validateExit,纯函数可单测)。
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { pathToFileURL } from 'node:url';
|
|
17
|
+
|
|
18
|
+
// 退出码判定单源(独立 export 供单元测试):引擎 exitCode 非 0 → 1;--strict 时 warning 也红;
|
|
19
|
+
// 结果缺失(引擎调用异常路径)fail-closed → 1。
|
|
20
|
+
export function validateExit(result, strict) {
|
|
21
|
+
if (!result || result.exitCode !== 0) return 1;
|
|
22
|
+
if (strict && (result.warnings || []).length > 0) return 1;
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function validate(args, pkgRoot) {
|
|
27
|
+
let target = process.cwd();
|
|
28
|
+
let strict = false;
|
|
29
|
+
let json = false;
|
|
30
|
+
for (let i = 0; i < args.length; i++) {
|
|
31
|
+
if (args[i] === '--dir') target = path.resolve(args[++i]);
|
|
32
|
+
else if (args[i] === '--strict') strict = true;
|
|
33
|
+
else if (args[i] === '--json') json = true;
|
|
34
|
+
else {
|
|
35
|
+
console.error(`validate: 未知参数 ${args[i]}——用法 flow-kit validate [--strict] [--json] [--dir <目录>]`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 装户盘面前置:枚举单源(workflow-enums.txt 等)都在 .agents/ 下,无装副本则引擎无从判定——
|
|
41
|
+
// 先给指引再红,不产出「20 项全断档」的噪音墙
|
|
42
|
+
if (!fs.existsSync(path.join(target, '.agents'))) {
|
|
43
|
+
const msg = `缺 .agents/——validate 校验 flow-kit 装户盘面,先跑 npx agentic-flow-kit init`;
|
|
44
|
+
if (json) console.log(JSON.stringify({ target, strict, checkLoopExitCode: null, exitCode: 1, blockers: [msg], warnings: [] }, null, 2));
|
|
45
|
+
else console.error(`❌ validate: ${target} ${msg}`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 动态 import 包内置引擎:pkgRoot = npx 缓存 / 全局安装 / 仓内直跑三种形态下都指向包源模板
|
|
50
|
+
const engineUrl = pathToFileURL(path.join(pkgRoot, 'templates', '_agents', 'scripts', 'check-loop.mjs')).href;
|
|
51
|
+
const { runCheckLoop } = await import(engineUrl);
|
|
52
|
+
const result = runCheckLoop({ root: target });
|
|
53
|
+
const code = validateExit(result, strict);
|
|
54
|
+
|
|
55
|
+
if (json) {
|
|
56
|
+
console.log(JSON.stringify({
|
|
57
|
+
target,
|
|
58
|
+
strict,
|
|
59
|
+
checkLoopExitCode: result.exitCode,
|
|
60
|
+
exitCode: code,
|
|
61
|
+
blockers: result.blockers,
|
|
62
|
+
warnings: result.warnings,
|
|
63
|
+
}, null, 2));
|
|
64
|
+
} else {
|
|
65
|
+
const n = (result.blockers || []).length;
|
|
66
|
+
const w = (result.warnings || []).length;
|
|
67
|
+
if (code === 0) console.log(`✅ flow-kit validate 通过——blocker 0 / warning ${w}${w ? '(advisory 不阻断;--strict 可把 warning 判红)' : ''}`);
|
|
68
|
+
else if (result.exitCode !== 0) console.log(`❌ flow-kit validate 未通过——blocker ${n} / warning ${w}(明细见上方 HARD-BLOCK / WARN 输出)`);
|
|
69
|
+
else console.log(`❌ flow-kit validate(--strict)未通过——warning ${w} 条判红(默认退出码 0,advisory 不阻断)`);
|
|
70
|
+
}
|
|
71
|
+
process.exit(code);
|
|
72
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// validate.test.mjs — flow-kit validate 断言(2026-10-10 validate-entry)。
|
|
2
|
+
// 两层:validateExit 纯函数直测(退出码判据单源)+ 端到端 spawn 真 CLI(bin/flow-kit.mjs validate,
|
|
3
|
+
// --json 断言 stdout 可解析、退出码契约、缺 .agents/ 指引、未知参数用法错)。
|
|
4
|
+
// fixture 沿 check-loop.test.mjs 同款:临时根(workflow 四目录 + .agents/ + 枚举单源内联 8 键),
|
|
5
|
+
// 自包含、不依赖真实 git 与 workflow 文档。
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { validateExit } from './validate.mjs';
|
|
12
|
+
|
|
13
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
14
|
+
const CLI = path.join(ROOT, 'bin', 'flow-kit.mjs');
|
|
15
|
+
|
|
16
|
+
let pass = 0, fail = 0;
|
|
17
|
+
function check(name, cond, detail = '') {
|
|
18
|
+
if (cond) { pass++; console.log(`PASS ${name}`); }
|
|
19
|
+
else { fail++; console.log(`FAIL ${name}${detail ? `——${detail}` : ''}`); }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ---- validateExit 纯函数(判据单源)----
|
|
23
|
+
check('V1 blocker → 1', validateExit({ exitCode: 1, blockers: ['x'], warnings: [] }, false) === 1);
|
|
24
|
+
check('V2 干净 → 0', validateExit({ exitCode: 0, blockers: [], warnings: [] }, false) === 0);
|
|
25
|
+
check('V3 warning 非 strict → 0', validateExit({ exitCode: 0, blockers: [], warnings: ['w'] }, false) === 0);
|
|
26
|
+
check('V4 warning + strict → 1', validateExit({ exitCode: 0, blockers: [], warnings: ['w'] }, true) === 1);
|
|
27
|
+
check('V5 引擎结果缺失 fail-closed → 1', validateExit(null, false) === 1 && validateExit(undefined, true) === 1);
|
|
28
|
+
|
|
29
|
+
// ---- fixture(枚举单源内联,同 check-loop.test.mjs#ENUMS_FIXTURE)----
|
|
30
|
+
const ENUMS_FIXTURE = [
|
|
31
|
+
'doc.status.all=draft approved done superseded cancelled',
|
|
32
|
+
'doc.status.confirmed=approved done superseded cancelled',
|
|
33
|
+
'doc.status.active=draft approved',
|
|
34
|
+
'doc.status.terminal=done superseded cancelled',
|
|
35
|
+
'doc.status.abandoned=superseded cancelled',
|
|
36
|
+
'incident.status.all=open fixed closed',
|
|
37
|
+
'incident.status.active=open',
|
|
38
|
+
'level.all=L0 L1 L2 L3',
|
|
39
|
+
].join('\n') + '\n';
|
|
40
|
+
|
|
41
|
+
const mkfix = () => {
|
|
42
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'flow-kit-validate-'));
|
|
43
|
+
for (const s of ['intents', 'specs', 'plans', 'incidents']) {
|
|
44
|
+
fs.mkdirSync(path.join(d, 'workflow', s), { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
fs.mkdirSync(path.join(d, '.agents'), { recursive: true });
|
|
47
|
+
fs.writeFileSync(path.join(d, '.agents', 'workflow-enums.txt'), ENUMS_FIXTURE);
|
|
48
|
+
return d;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const runValidate = (dir, args = []) => spawnSync(process.execPath, [CLI, 'validate', '--dir', dir, ...args], {
|
|
52
|
+
cwd: ROOT, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024,
|
|
53
|
+
});
|
|
54
|
+
const stdoutOf = (r) => r.stdout || '';
|
|
55
|
+
const stderrOf = (r) => r.stderr || '';
|
|
56
|
+
|
|
57
|
+
// ---- 端到端:绿盘面(空四目录 + 枚举单源)----
|
|
58
|
+
const green = mkfix();
|
|
59
|
+
const rg = runValidate(green, ['--json']);
|
|
60
|
+
let greenJson = null;
|
|
61
|
+
try { greenJson = JSON.parse(stdoutOf(rg)); } catch { /* 解析失败在断言里报 */ }
|
|
62
|
+
check('V6 绿盘面 exit 0', rg.status === 0, `exit=${rg.status}\n${stdoutOf(rg)}\n${stderrOf(rg).slice(0, 300)}`);
|
|
63
|
+
check('V7 --json stdout 可解析且 blockers 空', !!greenJson && Array.isArray(greenJson.blockers) && greenJson.blockers.length === 0, stdoutOf(rg).slice(0, 300));
|
|
64
|
+
check('V8 --json 契约字段在位', !!greenJson && greenJson.exitCode === 0 && greenJson.checkLoopExitCode === 0 && greenJson.strict === false && typeof greenJson.target === 'string', JSON.stringify(greenJson));
|
|
65
|
+
// --strict 契约端到端:同一盘面 fixture 恒产出 advisory warning(装副本骨架件缺失类,绿判不受其扰),
|
|
66
|
+
// 非 strict 绿(V6 已证)、strict 判红且 checkLoopExitCode 保持 0——判红来源是 strict 规则而非引擎
|
|
67
|
+
const rs = runValidate(green, ['--json', '--strict']);
|
|
68
|
+
let strictJson = null;
|
|
69
|
+
try { strictJson = JSON.parse(stdoutOf(rs)); } catch { /* 同上 */ }
|
|
70
|
+
check('V9 --strict 把 advisory warning 判红(checkLoopExitCode 仍 0)',
|
|
71
|
+
rs.status === 1 && !!strictJson && strictJson.exitCode === 1 && strictJson.checkLoopExitCode === 0 && strictJson.strict === true && strictJson.warnings.length > 0,
|
|
72
|
+
`exit=${rs.status}\n${stdoutOf(rs).slice(0, 300)}`);
|
|
73
|
+
|
|
74
|
+
// ---- 端到端:红盘面(同名配对断档:intent 无同名 spec/plan → 检查 1 hard-block)----
|
|
75
|
+
const red = mkfix();
|
|
76
|
+
fs.writeFileSync(path.join(red, 'workflow', 'intents', '2026-01-01-solo.md'),
|
|
77
|
+
'---\nstatus: draft\nlevel: L1\n---\n# INTENT — solo\n');
|
|
78
|
+
const rr = runValidate(red, ['--json']);
|
|
79
|
+
let redJson = null;
|
|
80
|
+
try { redJson = JSON.parse(stdoutOf(rr)); } catch { /* 同上 */ }
|
|
81
|
+
check('V10 配对断档 exit 1', rr.status === 1, `exit=${rr.status}\n${stdoutOf(rr).slice(0, 300)}\n${stderrOf(rr).slice(0, 500)}`);
|
|
82
|
+
check('V11 --json blockers 非空且明细走 stdout JSON', !!redJson && redJson.blockers.length > 0, stdoutOf(rr).slice(0, 300));
|
|
83
|
+
|
|
84
|
+
// ---- 指引与用法错 ----
|
|
85
|
+
const bare = fs.mkdtempSync(path.join(os.tmpdir(), 'flow-kit-validate-bare-'));
|
|
86
|
+
const rb = runValidate(bare);
|
|
87
|
+
check('V12 缺 .agents/ → exit 1 且给 init 指引', rb.status === 1 && /init/.test(stderrOf(rb)), `exit=${rb.status}\n${stderrOf(rb).slice(0, 300)}`);
|
|
88
|
+
const ru = runValidate(green, ['--bogus']);
|
|
89
|
+
check('V13 未知参数 → exit 1 用法行', ru.status === 1 && /用法/.test(stderrOf(ru)), `exit=${ru.status}\n${stderrOf(ru).slice(0, 300)}`);
|
|
90
|
+
|
|
91
|
+
// ---- 清理 ----
|
|
92
|
+
for (const d of [green, red, bare]) fs.rmSync(d, { recursive: true, force: true });
|
|
93
|
+
|
|
94
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
95
|
+
process.exit(fail ? 1 : 0);
|