agentic-flow-kit 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/modules/gates/dotnet-ca/check-architecture.sh +14 -10
- package/modules/hosts/trae/commands/wf-build.md +14 -14
- package/modules/hosts/trae/commands/wf-deploy.md +13 -13
- package/modules/hosts/trae/commands/wf-design.md +13 -13
- package/modules/hosts/trae/commands/wf-maintain.md +13 -13
- package/modules/hosts/trae/commands/wf-new-task.md +13 -13
- package/modules/hosts/trae/commands/wf-plan.md +13 -13
- package/modules/hosts/trae/commands/wf-review.md +13 -13
- package/modules/hosts/trae/commands/wf-test.md +14 -14
- package/modules/hosts/trae/hooks/post-edit-check.cjs +10 -0
- package/modules/hosts/trae/hooks/pre-shell-check.cjs +14 -4
- package/modules/hosts/trae/hooks/resolve-shell.cjs +30 -21
- package/modules/hosts/trae/rules/beautify-input-chinese.md +6 -6
- package/modules/hosts/trae/rules/git-commit-message.md +10 -10
- package/package.json +10 -2
- package/src/add-gate.mjs +8 -1
- package/src/add-host.mjs +4 -0
- package/src/cli.mjs +8 -5
- package/src/doctor.mjs +9 -2
- package/src/init.mjs +96 -12
- package/src/init.test.mjs +56 -0
- package/src/profiles.mjs +8 -0
- package/src/render.mjs +8 -9
- package/src/run-tests.mjs +1 -0
- package/src/sync.mjs +20 -4
- package/src/sync.test.mjs +40 -1
- package/templates/AGENTS.md +3 -2
- package/templates/_agents/board/index.html +449 -382
- package/templates/_agents/board/marked.min.js +6 -6
- package/templates/_agents/cache/kb-index.json +1 -1
- package/templates/_agents/commands/build.md +1 -1
- package/templates/_agents/commands/design.md +1 -1
- package/templates/_agents/commands/maintain.md +1 -1
- package/templates/_agents/commands/new-task.md +73 -73
- package/templates/_agents/commands/plan.md +2 -2
- package/templates/_agents/commands/test.md +2 -1
- package/templates/_agents/hooks/check-pairing-incremental.sh +30 -29
- package/templates/_agents/hooks/commit-check.cjs +6 -1
- package/templates/_agents/scripts/agg-delegations.cjs +35 -18
- package/templates/_agents/scripts/agg-delegations.test.mjs +95 -0
- package/templates/_agents/scripts/check-loop.sh +6 -0
- package/templates/_agents/scripts/check-loop.test.sh +1042 -1042
- package/templates/_agents/scripts/commit-check-trigger.test.mjs +94 -0
- package/templates/_agents/scripts/ensure-board.ps1 +1 -1
- package/templates/_agents/scripts/gen-wiki-board.mjs +13 -3
- package/templates/_agents/scripts/gen-wiki-board.test.mjs +22 -0
- package/templates/_agents/scripts/gen-workflow-index.mjs +7 -2
- package/templates/_agents/scripts/gen-workflow-index.test.mjs +27 -0
- package/templates/_agents/scripts/gen-workflow-metrics.mjs +17 -3
- package/templates/_agents/scripts/gen-workflow-metrics.test.mjs +21 -0
- package/templates/_agents/scripts/kb-search.mjs +2 -1
- package/templates/_agents/scripts/rule-budget.sh +8 -2
- package/templates/_agents/scripts/verify-wiki-consistency.mjs +6 -1
- package/templates/_agents/scripts/verify.mjs +36 -0
- package/templates/_agents/scripts/verify.test.mjs +77 -0
- package/templates/_agents/scripts/workflow-board-server.mjs +245 -244
- package/templates/_githooks/commit-msg +4 -2
- package/templates/_githooks/pre-push +32 -32
- package/templates/wiki/INDEX.md +9 -2
- package/templates/wiki//344/272/247/345/223/201/351/234/200/346/261/202/README.md +3 -0
- package/templates/wiki//345/274/200/345/217/221/346/226/271/346/241/210/README.md +3 -0
- package/templates/wiki//346/225/260/346/215/256/347/273/264/346/212/244/README.md +3 -0
- package/templates/wiki//346/265/213/350/257/225/346/212/245/345/221/212/README.md +3 -0
- package/templates/wiki//347/224/250/346/210/267/345/212/237/350/203/275/README.md +3 -0
- package/templates/wiki//351/241/271/347/233/256/350/247/204/350/214/203/README.md +3 -0
- package/templates/wiki//351/241/271/347/233/256/350/256/241/345/210/222/README.md +3 -0
- package/templates/workflow/README.md +2 -2
- package/templates/workflow/intents/_TEMPLATE.md +40 -40
- package/templates/workflow/specs/_TEMPLATE.md +41 -41
package/src/init.mjs
CHANGED
|
@@ -4,8 +4,8 @@ import fs from 'node:fs';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import readline from 'node:readline';
|
|
6
6
|
import { execSync, spawnSync } from 'node:child_process';
|
|
7
|
-
import { renderTree, sha256 } from './render.mjs';
|
|
8
|
-
import { HOSTS, STACKS, settingsJson, commitCheckConfig, pickStackVars, isOwned } from './profiles.mjs';
|
|
7
|
+
import { renderTree, renderContent, sha256 } from './render.mjs';
|
|
8
|
+
import { HOSTS, STACKS, STACK_ALIASES, settingsJson, commitCheckConfig, pickStackVars, isOwned } from './profiles.mjs';
|
|
9
9
|
import { doctor } from './doctor.mjs';
|
|
10
10
|
|
|
11
11
|
function fail(msg) {
|
|
@@ -42,21 +42,85 @@ function makePrompt() {
|
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
//
|
|
45
|
+
// ---- TUI 风格:hand-rolled ANSI(零依赖纪律);NO_COLOR / 非 TTY 自动降级纯文本 ----
|
|
46
|
+
const RAW = !process.env.NO_COLOR && process.stdout.isTTY;
|
|
47
|
+
const paint = (code, s) => (RAW ? `\x1b[${code}m${s}\x1b[0m` : String(s));
|
|
48
|
+
const bold = (s) => paint('1', s);
|
|
49
|
+
const dim = (s) => paint('2', s);
|
|
50
|
+
const cyan = (s) => paint('36', s);
|
|
51
|
+
|
|
52
|
+
// normalizeStack:ts/js 用户口径归一为 node 栈;其余小写透传(STACKS 校验兜底未知值)
|
|
53
|
+
export function normalizeStack(s) {
|
|
54
|
+
const t = String(s || '').trim().toLowerCase();
|
|
55
|
+
return STACK_ALIASES[t] || t;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// parseChoices:序号/名称/混输多选解析(逗号/全角逗号/空白分隔,去重保序);
|
|
59
|
+
// 空输入 → [def](EOF/直接回车安全回退默认);含非法 token 返回 null(调用方就地重问);
|
|
60
|
+
// map 用于输入侧归一(如 ts→node),序号先于 map 解析。
|
|
61
|
+
export function parseChoices(input, names, def, map = (x) => x) {
|
|
62
|
+
const text = String(input || '').trim();
|
|
63
|
+
if (!text) return [def];
|
|
64
|
+
const out = [];
|
|
65
|
+
for (const tok of text.split(/[,,\s]+/).filter(Boolean)) {
|
|
66
|
+
const i = /^\d+$/.test(tok) ? Number(tok) - 1 : names.indexOf(map(tok.toLowerCase()));
|
|
67
|
+
if (!Number.isInteger(i) || i < 0 || i >= names.length) return null;
|
|
68
|
+
if (!out.includes(names[i])) out.push(names[i]);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---- AGENTS.md 骨架探测与追加补齐 ----
|
|
74
|
+
// 标记随模板渲染落盘:新装/补齐后的 AGENTS.md 都含它,探测以此为准(不猜标题——避免误伤项目自写的同名「AI工作流」节)
|
|
75
|
+
const SKELETON_MARKER = '<!-- flow-kit:agents-skeleton -->';
|
|
76
|
+
export function hasAgentsSkeleton(text) {
|
|
77
|
+
return String(text || '').includes(SKELETON_MARKER);
|
|
78
|
+
}
|
|
79
|
+
// mergeAgents:原内容在上(尾部空白折叠),空行分隔,含标记的完整骨架在下
|
|
80
|
+
export function mergeAgents(existing, incoming) {
|
|
81
|
+
const head = String(existing || '').replace(/\s+$/, '');
|
|
82
|
+
const tail = String(incoming || '').trim();
|
|
83
|
+
return head ? `${head}\n\n${tail}\n` : `${tail}\n`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 交互确认环节:序号菜单问答(宿主多选 → 技术栈 → 看板端口)→ 已选 recap → 显式确认
|
|
87
|
+
// (EOF / 非 y 一律取消,不装;makePrompt 行式语义不变——管道/EOF 安全回退默认)
|
|
46
88
|
async function interactive() {
|
|
47
89
|
const p = makePrompt();
|
|
48
|
-
|
|
49
|
-
|
|
90
|
+
const hostNames = Object.keys(HOSTS);
|
|
91
|
+
const stackNames = Object.keys(STACKS);
|
|
92
|
+
const stackLabel = (n) => (n === 'node' ? 'node(ts/js)' : n);
|
|
93
|
+
const menu = (names) => ' ' + names.map((n, i) => dim(`${'①②③④⑤⑥'[i]} ${stackLabel(n)}`)).join(dim(' '));
|
|
94
|
+
|
|
95
|
+
console.log(bold(cyan('▶ flow-kit init')) + dim(' · AI 闭环工作流 + wiki 知识层脚手架'));
|
|
96
|
+
console.log(dim(' 安装到当前目录:.agents/ · .githooks/ · workflow/ · wiki/ · AGENTS.md(已存在的文件保守跳过;AGENTS.md 无工作流骨架时文末追加补齐)'));
|
|
50
97
|
try {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
98
|
+
let hosts;
|
|
99
|
+
for (;;) {
|
|
100
|
+
console.log(`\n${cyan('── 1/3 · agent 宿主')} ${dim('· 可多选')}`);
|
|
101
|
+
console.log(menu(hostNames));
|
|
102
|
+
const picked = parseChoices(await p.ask(dim(' 序号或名称,逗号分隔(直接回车 = zcode)')), hostNames, hostNames[0]);
|
|
103
|
+
if (picked) { hosts = picked; break; }
|
|
104
|
+
console.log(dim(' ⚠️ 没认出这个组合——序号或名称再来一次(如 1,3 或 zcode,trae)'));
|
|
105
|
+
}
|
|
106
|
+
let stack;
|
|
107
|
+
for (;;) {
|
|
108
|
+
console.log(`\n${cyan('── 2/3 · 技术栈')} ${dim('· 决定编译检查与自检验命令初值')}`);
|
|
109
|
+
console.log(menu(stackNames));
|
|
110
|
+
const picked = parseChoices(await p.ask(dim(' 序号或名称(直接回车 = none;ts/js 自动归一 node)')), stackNames, 'none', normalizeStack);
|
|
111
|
+
if (picked) { stack = picked[0]; break; }
|
|
112
|
+
console.log(dim(' ⚠️ 没认出——序号或名称再来一次'));
|
|
113
|
+
}
|
|
114
|
+
console.log(`\n${cyan('── 3/3 · workflow 看板端口')}`);
|
|
115
|
+
const boardPort = (await p.ask(dim(' 直接回车 = 8933'))) || '8933';
|
|
116
|
+
|
|
117
|
+
console.log(`\n ${bold(`宿主 ${hosts.join('、')}`)}${dim(' | ')}${bold(`技术栈 ${stack}`)}${dim(' | ')}${bold(`端口 ${boardPort}`)}`);
|
|
118
|
+
const yes = await p.ask(dim(' 确认安装到当前目录?(y/n,回车取消)'));
|
|
55
119
|
if (!/^(y|yes)$/i.test(yes)) {
|
|
56
120
|
console.log('已取消(未做任何改动)');
|
|
57
121
|
process.exit(0);
|
|
58
122
|
}
|
|
59
|
-
return { hosts, stack, boardPort, dir: process.cwd(), force: false };
|
|
123
|
+
return { hosts: hosts.join(','), stack, boardPort, dir: process.cwd(), force: false };
|
|
60
124
|
} finally {
|
|
61
125
|
p.close();
|
|
62
126
|
}
|
|
@@ -84,7 +148,8 @@ export async function init(args, pkgRoot) {
|
|
|
84
148
|
}
|
|
85
149
|
}
|
|
86
150
|
|
|
87
|
-
|
|
151
|
+
opt.stack = normalizeStack(opt.stack);
|
|
152
|
+
if (!STACKS[opt.stack]) fail(`未知技术栈:${opt.stack}(可选 dotnet | node(ts/js) | python | go | none——对应编译检查与自检验命令初值)`);
|
|
88
153
|
const hosts = opt.hosts.split(',').map((s) => s.trim()).filter(Boolean);
|
|
89
154
|
for (const h of hosts) {
|
|
90
155
|
if (!HOSTS[h]) fail(`未知宿主:${h}(可选 ${Object.keys(HOSTS).join(' | ')})`);
|
|
@@ -99,11 +164,29 @@ export async function init(args, pkgRoot) {
|
|
|
99
164
|
|
|
100
165
|
const vars = { BOARD_PORT: String(opt.boardPort), ...pickStackVars(opt.stack) };
|
|
101
166
|
|
|
102
|
-
console.log(
|
|
167
|
+
console.log(`${bold(cyan('▶ flow-kit init'))}${dim(' → ')}${target}`);
|
|
103
168
|
console.log(` 宿主:${hosts.join(', ')} | 技术栈:${opt.stack} | 看板端口:${opt.boardPort}`);
|
|
104
169
|
|
|
105
170
|
// 1) 模板树(保守:已存在文件跳过,--force 覆盖)
|
|
106
171
|
const t = renderTree(path.join(pkgRoot, 'templates'), target, vars, { force: opt.force });
|
|
172
|
+
|
|
173
|
+
// 1.5) AGENTS.md 特例:已存在且无骨架标记 → 文末追加补齐(原内容保留);带标记 → 保守跳过
|
|
174
|
+
let agentsMergedSha = null;
|
|
175
|
+
if (t.skipped.includes('AGENTS.md')) {
|
|
176
|
+
const agentsAbs = path.join(target, 'AGENTS.md');
|
|
177
|
+
const existing = fs.readFileSync(agentsAbs, 'utf8');
|
|
178
|
+
if (hasAgentsSkeleton(existing)) {
|
|
179
|
+
t.skipped = t.skipped.filter((r) => r !== 'AGENTS.md'); // 专属提示已报,不进末尾「未覆盖」汇总重复列
|
|
180
|
+
console.log(' 跳过(已存在且含工作流骨架):AGENTS.md');
|
|
181
|
+
} else {
|
|
182
|
+
const incoming = renderContent(fs.readFileSync(path.join(pkgRoot, 'templates', 'AGENTS.md'), 'utf8'), vars);
|
|
183
|
+
fs.writeFileSync(agentsAbs, mergeAgents(existing, incoming));
|
|
184
|
+
agentsMergedSha = sha256(fs.readFileSync(agentsAbs));
|
|
185
|
+
t.skipped = t.skipped.filter((r) => r !== 'AGENTS.md');
|
|
186
|
+
console.log(' AGENTS.md 已存在但无工作流骨架——文末追加补齐(原内容保留,「项目适配区」照常自填)');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
107
190
|
// 2) 宿主适配层
|
|
108
191
|
const hostResults = {};
|
|
109
192
|
for (const h of hosts) {
|
|
@@ -155,6 +238,7 @@ export async function init(args, pkgRoot) {
|
|
|
155
238
|
...hosts.flatMap((h) => hostResults[h].written.map((f) => ({ ...f, rel: `${HOSTS[h].dir}/${f.rel}` }))),
|
|
156
239
|
];
|
|
157
240
|
const owned = [...t.written.filter((f) => isOwned(f.rel)), ...ownedGenerated];
|
|
241
|
+
if (agentsMergedSha) owned.push({ rel: 'AGENTS.md', sha256: agentsMergedSha });
|
|
158
242
|
const pkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf8'));
|
|
159
243
|
fs.mkdirSync(path.dirname(kitPath), { recursive: true });
|
|
160
244
|
fs.writeFileSync(kitPath, `${JSON.stringify({
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// init.mjs 交互解析纯函数测试(normalizeStack / parseChoices)——不触真实安装与 stdin
|
|
3
|
+
// 断言:①ts/js 别名归一 node;②序号/名称/混输/全角逗号/空白分隔解析正确(去重保序);
|
|
4
|
+
// ③非法输入返回 null(就地重问信号);④空输入回默认(EOF/直接回车安全回退)。
|
|
5
|
+
// 用法:node src/init.test.mjs
|
|
6
|
+
import { normalizeStack, parseChoices, hasAgentsSkeleton, mergeAgents } from './init.mjs';
|
|
7
|
+
|
|
8
|
+
let pass = 0;
|
|
9
|
+
let fail = 0;
|
|
10
|
+
const check = (desc, cond, detail = '') => {
|
|
11
|
+
if (cond) { pass++; console.log(`PASS ${desc}`); }
|
|
12
|
+
else { fail++; console.log(`FAIL ${desc}${detail ? `\n${detail}` : ''}`); }
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const HOSTS = ['zcode', 'opencode', 'trae', 'omp'];
|
|
16
|
+
const STACKS = ['dotnet', 'node', 'python', 'go', 'none'];
|
|
17
|
+
const eq = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
18
|
+
|
|
19
|
+
// ---- ① 别名归一 ----
|
|
20
|
+
check('ts/TS/TypeScript/javascript 均归一 node', normalizeStack('ts') === 'node' && normalizeStack('TS') === 'node' && normalizeStack('TypeScript') === 'node' && normalizeStack('javascript') === 'node');
|
|
21
|
+
check('规范名与空值透传', normalizeStack('node') === 'node' && normalizeStack('dotnet') === 'dotnet' && normalizeStack(' go ') === 'go' && normalizeStack('') === '' && normalizeStack(undefined) === '');
|
|
22
|
+
|
|
23
|
+
// ---- ② 序号/名称/混输解析 ----
|
|
24
|
+
check('序号 1,3 → zcode,trae', eq(parseChoices('1,3', HOSTS, 'zcode'), ['zcode', 'trae']));
|
|
25
|
+
check('名称 trae → trae', eq(parseChoices('trae', HOSTS, 'zcode'), ['trae']));
|
|
26
|
+
check('混输 zcode,3(全角逗号)→ zcode,trae', eq(parseChoices('zcode,3', HOSTS, 'zcode'), ['zcode', 'trae']));
|
|
27
|
+
check('空白分隔 1 3 → zcode,trae', eq(parseChoices('1 3', HOSTS, 'zcode'), ['zcode', 'trae']));
|
|
28
|
+
check('去重保序 3,1,1 → trae,zcode', eq(parseChoices('3,1,1', HOSTS, 'zcode'), ['trae', 'zcode']));
|
|
29
|
+
check('栈序号 2 + map 归一 → node', eq(parseChoices('2', STACKS, 'none', normalizeStack), ['node']));
|
|
30
|
+
check('栈别名 ts + map 归一 → node', eq(parseChoices('ts', STACKS, 'none', normalizeStack), ['node']));
|
|
31
|
+
|
|
32
|
+
// ---- ③ 非法输入 → null(就地重问) ----
|
|
33
|
+
check('越界序号 9 → null', parseChoices('9', HOSTS, 'zcode') === null);
|
|
34
|
+
check('未知名称 bad → null', parseChoices('zcode,bad', HOSTS, 'zcode') === null);
|
|
35
|
+
check('栈别名未传 map 不归一(ts → null)', parseChoices('ts', STACKS, 'none') === null);
|
|
36
|
+
|
|
37
|
+
// ---- ④ 空输入回默认 ----
|
|
38
|
+
check('空串 → [def]', eq(parseChoices('', HOSTS, 'zcode'), ['zcode']));
|
|
39
|
+
check('undefined → [def]', eq(parseChoices(undefined, STACKS, 'none'), ['none']));
|
|
40
|
+
|
|
41
|
+
// ---- ⑤ AGENTS.md 骨架探测与合并 ----
|
|
42
|
+
check('标记探测:带标记 true', hasAgentsSkeleton('x\n<!-- flow-kit:agents-skeleton -->\n# y') === true);
|
|
43
|
+
check('标记探测:外部自写 AGENTS.md false', hasAgentsSkeleton('# 我的项目\n\n构建: dotnet build') === false);
|
|
44
|
+
check('标记探测:空值 false', hasAgentsSkeleton('') === false && hasAgentsSkeleton(undefined) === false);
|
|
45
|
+
check('合并:原内容在上、空行分隔、骨架在下',
|
|
46
|
+
mergeAgents('# A\n内容', '<!-- m -->\n# B') === '# A\n内容\n\n<!-- m -->\n# B\n');
|
|
47
|
+
check('合并:原内容尾部空白折叠不产生连续空行',
|
|
48
|
+
mergeAgents('# A\n内容\n\n\n', '<!-- m -->\n# B') === '# A\n内容\n\n<!-- m -->\n# B\n');
|
|
49
|
+
check('合并:原内容为空时骨架即全文', mergeAgents('', '<!-- m -->\n# B') === '<!-- m -->\n# B\n');
|
|
50
|
+
|
|
51
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
52
|
+
if (fail) {
|
|
53
|
+
console.log('\n用法:node src/init.test.mjs');
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
process.exit(0);
|
package/src/profiles.mjs
CHANGED
|
@@ -62,6 +62,14 @@ export const STACKS = {
|
|
|
62
62
|
},
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
+
// 技术栈别名归一:ts/js 用户口径 → node 栈(init 交互与 --stack 共用;别名不进 kit.json,落盘始终是规范名)
|
|
66
|
+
export const STACK_ALIASES = {
|
|
67
|
+
ts: 'node',
|
|
68
|
+
js: 'node',
|
|
69
|
+
typescript: 'node',
|
|
70
|
+
javascript: 'node',
|
|
71
|
+
};
|
|
72
|
+
|
|
65
73
|
// AGENTS.md「项目适配区」的命令预填变量(技术栈未知时留 <填写> 占位)
|
|
66
74
|
export function pickStackVars(stackKey) {
|
|
67
75
|
const s = STACKS[stackKey] || STACKS.none;
|
package/src/render.mjs
CHANGED
|
@@ -17,6 +17,11 @@ function relOf(absFile, srcRoot) {
|
|
|
17
17
|
return parts.join('/');
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// renderContent:单文件文本渲染({{KEY}} 替换,未知占位符原样保留)——renderTree 与 init 的 AGENTS.md 追加补齐共用
|
|
21
|
+
export function renderContent(text, vars) {
|
|
22
|
+
return String(text || '').replace(PH_RE, (m, key) => (Object.prototype.hasOwnProperty.call(vars, key) ? vars[key] : m));
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
export function renderTree(srcRoot, targetRoot, vars, { force = false } = {}) {
|
|
21
26
|
const written = []; // { rel, sha256 }
|
|
22
27
|
const skipped = []; // rel(已存在,保守跳过)
|
|
@@ -27,6 +32,8 @@ export function renderTree(srcRoot, targetRoot, vars, { force = false } = {}) {
|
|
|
27
32
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
28
33
|
const abs = path.join(dir, entry.name);
|
|
29
34
|
if (entry.isDirectory()) {
|
|
35
|
+
// 运行时缓存不是模板(包源跑测试会在 templates/_agents/cache/ 落盘残留)——init/sync 一律跳过
|
|
36
|
+
if (relOf(abs, srcRoot) === '.agents/cache') continue;
|
|
30
37
|
dirs.push(relOf(abs, srcRoot));
|
|
31
38
|
walk(abs);
|
|
32
39
|
continue;
|
|
@@ -41,15 +48,7 @@ export function renderTree(srcRoot, targetRoot, vars, { force = false } = {}) {
|
|
|
41
48
|
const raw = fs.readFileSync(abs);
|
|
42
49
|
let out = raw;
|
|
43
50
|
if (raw.length < 512 * 1024 && !raw.includes(0)) {
|
|
44
|
-
const
|
|
45
|
-
const used = new Set();
|
|
46
|
-
const replaced = text.replace(PH_RE, (m, key) => {
|
|
47
|
-
if (Object.prototype.hasOwnProperty.call(vars, key)) {
|
|
48
|
-
used.add(key);
|
|
49
|
-
return vars[key];
|
|
50
|
-
}
|
|
51
|
-
return m; // 未知占位符原样保留,doctor 会告警
|
|
52
|
-
});
|
|
51
|
+
const replaced = renderContent(raw.toString('utf8'), vars);
|
|
53
52
|
if (replaced.includes('{{')) {
|
|
54
53
|
for (const m of replaced.matchAll(/\{\{([A-Z][A-Z0-9_]*)\}\}/g)) {
|
|
55
54
|
if (!Object.prototype.hasOwnProperty.call(vars, m[1])) warnings.push(`${rel}: 未知占位符 {{${m[1]}}}(原样保留)`);
|
package/src/run-tests.mjs
CHANGED
|
@@ -9,6 +9,7 @@ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
|
9
9
|
|
|
10
10
|
const nodeSuites = [
|
|
11
11
|
'src/sync.test.mjs',
|
|
12
|
+
'src/init.test.mjs',
|
|
12
13
|
...fs.readdirSync(path.join(ROOT, 'templates/_agents/scripts'))
|
|
13
14
|
.filter((f) => f.endsWith('.test.mjs'))
|
|
14
15
|
.sort()
|
package/src/sync.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// flow-kit sync:按 .agents/kit.json 台账升级 managed 文件。
|
|
2
|
-
// 三态:未改动(磁盘 sha==台账)→ 覆盖新版;本地已改 → 跳过并报告(--force
|
|
2
|
+
// 三态:未改动(磁盘 sha==台账)→ 覆盖新版;本地已改 → 跳过并报告(--force 覆盖;台账保持包侧基线,
|
|
3
|
+
// 每次持续报告直至 --force 或本地对齐新版——防跳过一次后下次升级被静默覆盖,2026-09-24 语义修正);
|
|
3
4
|
// 生成器目标(INDEX.md / wiki 看板)→ 不比对,收尾重跑生成器走锚点重写。
|
|
4
5
|
// 附带:包内新增 managed 文件 → 安装;包内已删 → 仅报告不删盘、出台账;managed 缺失 → 恢复。
|
|
5
6
|
// owned 文件永不触碰(两态模型,见 profiles.isOwned)。
|
|
@@ -86,7 +87,7 @@ export function sync(args, pkgRoot) {
|
|
|
86
87
|
// 本地已改
|
|
87
88
|
if (freshFile.sha === diskSha) { unchanged++; managedNew.push({ rel, sha256: diskSha }); continue; } // 改动恰好等于新版
|
|
88
89
|
if (force) { fs.copyFileSync(freshFile.abs, disk); updated.push(`${rel}(--force 覆盖本地改动)`); managedNew.push({ rel, sha256: freshFile.sha }); }
|
|
89
|
-
else { skipped.push(rel); managedNew.push({ rel, sha256:
|
|
90
|
+
else { skipped.push(rel); managedNew.push({ rel, sha256: ledgerSha }); } // 台账保持包侧基线:持续报告「本地已改」,直到 --force 或本地对齐新版
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
// 台账外的新文件:managed 类安装;owned 类只对「盘上缺失」的起步文档报告(已装的不动不报)
|
|
@@ -109,7 +110,7 @@ export function sync(args, pkgRoot) {
|
|
|
109
110
|
list('恢复缺失', restored);
|
|
110
111
|
list('新增安装', added);
|
|
111
112
|
list('本地已改,跳过', skipped);
|
|
112
|
-
if (skipped.some((s) => !s.includes('未入台账'))) console.log(' ↑ --force 覆盖本地改动;git diff
|
|
113
|
+
if (skipped.some((s) => !s.includes('未入台账'))) console.log(' ↑ --force 覆盖本地改动;git diff 自查差异;台账保持包侧基线,后续 sync 持续报告直至处理');
|
|
113
114
|
list('包内已移除(文件保留在盘上,可手动删除)', removed);
|
|
114
115
|
list('新增 owned 起步文档(项目自持,未自动安装)', newOwned);
|
|
115
116
|
if (!updated.length && !restored.length && !added.length && !removed.length && !skipped.length && !newOwned.length) {
|
|
@@ -118,7 +119,22 @@ export function sync(args, pkgRoot) {
|
|
|
118
119
|
console.log(` 无变化 ${unchanged} 份`);
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
//
|
|
122
|
+
// owned 台账按盘面自愈:owned 归项目所有,哈希只是记账不是约束——手改后无须手工刷 kit.json
|
|
123
|
+
// (Shipyard 回流策略,2026-09-24:曾在消费仓被迫手工刷新 owned 哈希,根因收敛到此处)
|
|
124
|
+
let ownedRefreshed = 0;
|
|
125
|
+
if (Array.isArray(kit.owned)) {
|
|
126
|
+
kit.owned = kit.owned.map((f) => {
|
|
127
|
+
const p = path.join(target, f.rel);
|
|
128
|
+
if (!fs.existsSync(p)) return f;
|
|
129
|
+
const disk = sha256(fs.readFileSync(p));
|
|
130
|
+
if (disk === f.sha256) return f;
|
|
131
|
+
ownedRefreshed++;
|
|
132
|
+
return { ...f, sha256: disk };
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (ownedRefreshed) console.log(` owned 台账哈希按盘面刷新 ${ownedRefreshed} 份(owned 归项目所有,仅记账不约束)`);
|
|
136
|
+
|
|
137
|
+
// 台账重写:版本对齐当前包;removed 出册;skipped 保持包侧基线持续报告
|
|
122
138
|
kit.managed = managedNew.sort((a, b) => a.rel.localeCompare(b.rel));
|
|
123
139
|
kit.version = pkg.version;
|
|
124
140
|
fs.writeFileSync(kitPath, `${JSON.stringify(kit, null, 2)}\n`);
|
package/src/sync.test.mjs
CHANGED
|
@@ -94,7 +94,7 @@ else if (cmd === 'add-gate') addGate(rest, root);
|
|
|
94
94
|
const rels = kit.managed.map((f) => f.rel);
|
|
95
95
|
check('S1 台账:版本对齐包版本', kit.version === '9.9.9');
|
|
96
96
|
check('S1 台账:gone 出册、new 入册', !rels.includes('.agents/scripts/gone.txt') && rels.includes('.agents/commands/new.txt'));
|
|
97
|
-
check('S1
|
|
97
|
+
check('S1 台账:跳过文件保持包侧基线 sha(持续报告,不随盘面基线化)', kit.managed.find((f) => f.rel === '.agents/scripts/loc.txt').sha256 === sha256(Buffer.from('loc v1\n')));
|
|
98
98
|
check('S1 台账:覆盖文件 sha 为新版', kit.managed.find((f) => f.rel === '.agents/scripts/chg.txt').sha256 === sha256(Buffer.from('chg v2 port 777\n')));
|
|
99
99
|
check('S1 owned 不触碰(AGENTS.md 原样)', R(path.join(t, 'AGENTS.md')) === 'owned skeleton v2\n');
|
|
100
100
|
check('S1 已装 owned 不误报新增', !out.includes('新增 owned 起步文档'), out);
|
|
@@ -182,6 +182,10 @@ else if (cmd === 'add-gate') addGate(rest, root);
|
|
|
182
182
|
check('S8 local-pre-commit 接线(缺失则建 set -e 头)', R(path.join(t, '.agents/hooks/local-pre-commit')) === '#!/bin/sh\nset -e\nsh .agents/hooks/gate.sh\n', out);
|
|
183
183
|
const kit = JSON.parse(R(path.join(t, '.agents/kit.json')));
|
|
184
184
|
check('S8 owned 台账补记(归项目所有)', kit.owned.some((f) => f.rel === '.agents/hooks/gate.sh'));
|
|
185
|
+
check('S8 接线后 local-pre-commit 进 owned 且哈希=盘面(Shipyard 回流根因,2026-09-24)', (() => {
|
|
186
|
+
const e = kit.owned.find((f) => f.rel === '.agents/hooks/local-pre-commit');
|
|
187
|
+
return e && e.sha256 === sha256(Buffer.from(R(path.join(t, '.agents/hooks/local-pre-commit')), 'utf8'));
|
|
188
|
+
})(), JSON.stringify(kit.owned));
|
|
185
189
|
runCmd('add-gate', fx, t, ['fakeg']); // 再跑一遍
|
|
186
190
|
check('S8 幂等:接线行不重复', R(path.join(t, '.agents/hooks/local-pre-commit')) === '#!/bin/sh\nset -e\nsh .agents/hooks/gate.sh\n');
|
|
187
191
|
const r3 = runCmd('add-gate', fx, t, ['nope']);
|
|
@@ -203,5 +207,40 @@ else if (cmd === 'add-gate') addGate(rest, root);
|
|
|
203
207
|
check('S9 挂载行在 exit 0 原位之前追加', R(path.join(t, '.agents/hooks/local-pre-commit')) === '#!/bin/sh\n# 旧装态注释\nsh .agents/hooks/gate.sh\n', R(path.join(t, '.agents/hooks/local-pre-commit')));
|
|
204
208
|
}
|
|
205
209
|
|
|
210
|
+
// ============ 场景 10:模板树内的运行时缓存不进安装与台账 ============
|
|
211
|
+
{
|
|
212
|
+
const fx = mkFixture();
|
|
213
|
+
W(path.join(fx, 'templates/_agents/cache/kb-index.json'), '{"stale":"test-residue"}');
|
|
214
|
+
const t = mkTarget(fx); // v1 安装态无 cache 文件
|
|
215
|
+
const r = runCmd('sync', fx, t);
|
|
216
|
+
check('S10 cache 残留不被安装', !fs.existsSync(path.join(t, '.agents/cache/kb-index.json')), r.stdout + r.stderr);
|
|
217
|
+
const kit = JSON.parse(R(path.join(t, '.agents/kit.json')));
|
|
218
|
+
check('S10 cache 不入 managed 台账', !kit.managed.some((f) => f.rel.startsWith('.agents/cache/')), JSON.stringify(kit.managed.filter((f) => f.rel.includes('cache'))));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ============ 场景 11:跳过件不被下次升级静默覆盖(台账保持包侧基线,2026-09-24 语义修正回归) ============
|
|
222
|
+
{
|
|
223
|
+
const fx = mkFixture(), t = mkTarget(fx);
|
|
224
|
+
W(path.join(t, '.agents/scripts/loc.txt'), 'loc USER\n');
|
|
225
|
+
runCmd('sync', fx, t); // 第一次:本地已改 → 跳过
|
|
226
|
+
W(path.join(fx, 'templates/_agents/scripts/loc.txt'), 'loc v3\n'); // 包再出新版
|
|
227
|
+
const r = runCmd('sync', fx, t);
|
|
228
|
+
const out = r.stdout + r.stderr;
|
|
229
|
+
check('S11 二次 sync 不覆盖本地改动(loc.txt 仍为用户版)', R(path.join(t, '.agents/scripts/loc.txt')) === 'loc USER\n', out);
|
|
230
|
+
check('S11 仍按「本地已改,跳过」报告', out.includes('本地已改,跳过') && out.includes('loc.txt'), out);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ============ 场景 12:owned 台账哈希按盘面自愈(Shipyard 回流策略,2026-09-24) ============
|
|
234
|
+
{
|
|
235
|
+
const fx = mkFixture(), t = mkTarget(fx);
|
|
236
|
+
W(path.join(t, 'AGENTS.md'), 'owned skeleton v2 EDITED BY USER\n'); // owned 件手改 → 台账停旧值
|
|
237
|
+
const r = runCmd('sync', fx, t);
|
|
238
|
+
const out = r.stdout + r.stderr;
|
|
239
|
+
check('S12 owned 文件不被触碰(AGENTS.md 保持用户版)', R(path.join(t, 'AGENTS.md')) === 'owned skeleton v2 EDITED BY USER\n', out);
|
|
240
|
+
const kit = JSON.parse(R(path.join(t, '.agents/kit.json')));
|
|
241
|
+
check('S12 owned 台账哈希自愈为盘面值', kit.owned.find((f) => f.rel === 'AGENTS.md').sha256 === sha256(Buffer.from('owned skeleton v2 EDITED BY USER\n')), JSON.stringify(kit.owned));
|
|
242
|
+
check('S12 有刷新报告', out.includes('owned 台账哈希按盘面刷新 1 份'), out);
|
|
243
|
+
}
|
|
244
|
+
|
|
206
245
|
console.log(`\n合计: PASS ${pass} / FAIL ${failCount}`);
|
|
207
246
|
process.exit(failCount ? 1 : 0);
|
package/templates/AGENTS.md
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<!-- flow-kit:agents-skeleton -->
|
|
1
2
|
# AGENTS.md — AI 工作流 + Wiki
|
|
2
3
|
|
|
3
4
|
> 本文件由 agentic-flow-kit 生成:说明本仓库的 AI 工作流与 Wiki 用法。「项目适配区」由项目自行填写;工作流约定可增不可删。
|
|
@@ -25,12 +26,12 @@
|
|
|
25
26
|
- `.githooks/`(经 `core.hooksPath` 挂载,本地 clone 后执行 `git config core.hooksPath .githooks`):pre-commit(闭环配对 / wiki 台账 / 规则面预算 / 敏感信息扫描与条件构建)、pre-push(闭环断档扫描)、commit-msg、post-commit、pre-merge-commit。
|
|
26
27
|
- **`git commit` / `git merge` / `git push` 三处一律禁 `--no-verify`**——被拦说明产出不合规,按提示修完原路重试。
|
|
27
28
|
- 项目专属门禁挂 `.agents/hooks/local-pre-commit`。
|
|
28
|
-
- 提交遵循 Conventional Commits 中文(feat / fix / docs / style / refactor / perf);L1+ 入口文档 / spec / plan
|
|
29
|
+
- 提交遵循 Conventional Commits 中文(feat / fix / docs / style / refactor / perf);L1+ 入口文档 / spec / plan 随代码同一提交;确认(approved)后立即 `docs(*)` 单独提交留痕。
|
|
29
30
|
|
|
30
31
|
### 检索、看板与量化
|
|
31
32
|
|
|
32
33
|
- 跨语料检索:`node .agents/scripts/kb-search.mjs "<词>"`(workflow 节级定位 + wiki 全文);活跃流程读 `workflow/INDEX.md`(状态变更后 `node .agents/scripts/gen-workflow-index.mjs` 重生成)。
|
|
33
|
-
- workflow
|
|
34
|
+
- workflow 看板(可选,默认不拉起):需要时手动跑 `.agents/scripts/ensure-board.ps1`,然后访问 http://127.0.0.1:{{BOARD_PORT}}(只读预警层,非门禁)。
|
|
34
35
|
- 委派 / 自做结果留痕 `workflow/delegations.md`(聚合 `node .agents/scripts/agg-delegations.cjs`);命令 / 技能卡壳记 `workflow/papercuts.md`,不当场顺手改。
|
|
35
36
|
|
|
36
37
|
### 验证与关单
|