agentic-flow-kit 0.2.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/LICENSE +21 -0
- package/README.md +62 -0
- package/bin/flow-kit.mjs +4 -0
- package/modules/gates/dotnet-ca/README.md +24 -0
- package/modules/gates/dotnet-ca/check-architecture.sh +128 -0
- package/modules/gates/dotnet-ca/local-pre-commit.line +1 -0
- package/modules/hosts/omp/agents/implementer.md +12 -0
- package/modules/hosts/omp/agents/independent-reviewer.md +11 -0
- package/modules/hosts/omp/agents/ui-verifier.md +12 -0
- package/modules/hosts/opencode/agents/implementer.md +14 -0
- package/modules/hosts/opencode/agents/independent-reviewer.md +10 -0
- package/modules/hosts/opencode/agents/ui-verifier.md +10 -0
- package/modules/hosts/opencode/commands/build.md +13 -0
- package/modules/hosts/opencode/commands/deploy.md +12 -0
- package/modules/hosts/opencode/commands/design.md +12 -0
- package/modules/hosts/opencode/commands/maintain.md +12 -0
- package/modules/hosts/opencode/commands/new-task.md +12 -0
- package/modules/hosts/opencode/commands/plan.md +12 -0
- package/modules/hosts/opencode/commands/review.md +12 -0
- package/modules/hosts/opencode/commands/test.md +14 -0
- package/modules/hosts/trae/agents/implementer.md +9 -0
- package/modules/hosts/trae/agents/independent-reviewer.md +8 -0
- package/modules/hosts/trae/agents/ui-verifier.md +12 -0
- package/modules/hosts/trae/commands/wf-build.md +14 -0
- package/modules/hosts/trae/commands/wf-deploy.md +13 -0
- package/modules/hosts/trae/commands/wf-design.md +13 -0
- package/modules/hosts/trae/commands/wf-maintain.md +13 -0
- package/modules/hosts/trae/commands/wf-new-task.md +13 -0
- package/modules/hosts/trae/commands/wf-plan.md +13 -0
- package/modules/hosts/trae/commands/wf-review.md +13 -0
- package/modules/hosts/trae/commands/wf-test.md +14 -0
- package/modules/hosts/trae/hooks/post-edit-check.cjs +87 -0
- package/modules/hosts/trae/hooks/pre-shell-check.cjs +151 -0
- package/modules/hosts/trae/hooks/resolve-shell.cjs +21 -0
- package/modules/hosts/trae/hooks.json +29 -0
- package/modules/hosts/trae/rules/beautify-input-chinese.md +6 -0
- package/modules/hosts/trae/rules/git-commit-message.md +10 -0
- package/modules/hosts/trae/rules/project-workflow.md +14 -0
- package/modules/hosts/zcode/agents/implementer.md +11 -0
- package/modules/hosts/zcode/agents/independent-reviewer.md +10 -0
- package/modules/hosts/zcode/agents/ui-verifier.md +10 -0
- package/package.json +34 -0
- package/src/add-gate.mjs +83 -0
- package/src/add-host.mjs +63 -0
- package/src/cli.mjs +81 -0
- package/src/doctor.mjs +150 -0
- package/src/init.mjs +190 -0
- package/src/profiles.mjs +101 -0
- package/src/render.mjs +68 -0
- package/src/run-tests.mjs +35 -0
- package/src/sync.mjs +141 -0
- package/src/sync.test.mjs +207 -0
- package/templates/.gitattributes +6 -0
- package/templates/AGENTS.md +52 -0
- package/templates/_agents/board/index.html +382 -0
- package/templates/_agents/board/marked.min.js +6 -0
- package/templates/_agents/cache/kb-index.json +1 -0
- package/templates/_agents/commands/build.md +74 -0
- package/templates/_agents/commands/deploy.md +65 -0
- package/templates/_agents/commands/design.md +54 -0
- package/templates/_agents/commands/maintain.md +91 -0
- package/templates/_agents/commands/new-task.md +73 -0
- package/templates/_agents/commands/plan.md +46 -0
- package/templates/_agents/commands/review.md +59 -0
- package/templates/_agents/commands/test.md +80 -0
- package/templates/_agents/hooks/check-pairing-incremental.sh +29 -0
- package/templates/_agents/hooks/check-wiki-ledger.sh +18 -0
- package/templates/_agents/hooks/commit-check.cjs +140 -0
- package/templates/_agents/hooks/local-pre-commit +6 -0
- package/templates/_agents/notes/runtime-env.md +18 -0
- package/templates/_agents/roles/implementer.md +46 -0
- package/templates/_agents/roles/independent-reviewer.md +43 -0
- package/templates/_agents/roles/ui-verifier.md +42 -0
- package/templates/_agents/rule-budgets.txt +8 -0
- package/templates/_agents/scripts/agg-delegations.cjs +175 -0
- package/templates/_agents/scripts/check-loop.sh +803 -0
- package/templates/_agents/scripts/check-loop.test.sh +1042 -0
- package/templates/_agents/scripts/ensure-board.ps1 +48 -0
- package/templates/_agents/scripts/gen-wiki-board.mjs +197 -0
- package/templates/_agents/scripts/gen-wiki-board.test.mjs +136 -0
- package/templates/_agents/scripts/gen-workflow-index.mjs +190 -0
- package/templates/_agents/scripts/gen-workflow-index.test.mjs +113 -0
- package/templates/_agents/scripts/gen-workflow-metrics.mjs +172 -0
- package/templates/_agents/scripts/gen-workflow-metrics.test.mjs +101 -0
- package/templates/_agents/scripts/kb-cache-evict.mjs +50 -0
- package/templates/_agents/scripts/kb-cache-evict.test.mjs +104 -0
- package/templates/_agents/scripts/kb-search.mjs +307 -0
- package/templates/_agents/scripts/kb-search.test.mjs +153 -0
- package/templates/_agents/scripts/rule-budget.sh +68 -0
- package/templates/_agents/scripts/verify-wiki-consistency.mjs +192 -0
- package/templates/_agents/scripts/wiki-search.mjs +19 -0
- package/templates/_agents/scripts/workflow-board-server.mjs +244 -0
- package/templates/_agents/skills/verify-ui/SKILL.md +24 -0
- package/templates/_agents/skills/wiki/SKILL.md +104 -0
- package/templates/_agents/workflow-modules.txt +11 -0
- package/templates/_githooks/commit-msg +15 -0
- package/templates/_githooks/post-commit +10 -0
- package/templates/_githooks/pre-commit +58 -0
- package/templates/_githooks/pre-merge-commit +24 -0
- package/templates/_githooks/pre-push +32 -0
- package/templates/wiki/INDEX.md +23 -0
- package/templates/wiki/drafts-archive/README.md +3 -0
- package/templates/wiki//347/237/245/350/257/206/346/262/211/346/267/200/346/200/273/350/247/210.html +56 -0
- package/templates/workflow/README.md +52 -0
- package/templates/workflow/delegations.md +28 -0
- package/templates/workflow/incidents/_TEMPLATE.md +42 -0
- package/templates/workflow/intents/_TEMPLATE.md +40 -0
- package/templates/workflow/papercuts.md +7 -0
- package/templates/workflow/plans/_TEMPLATE.md +38 -0
- package/templates/workflow/regression-checklist.md +22 -0
- package/templates/workflow/specs/_TEMPLATE.md +41 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// wiki 三方一致性验证:磁盘 ↔ INDEX.md ↔ 知识沉淀总览.html(DATA) + 数据维护台账登记 + 看板链接可达性
|
|
2
|
+
// 用法: node .agents/scripts/verify-wiki-consistency.mjs (不一致 exit 1)
|
|
3
|
+
// 约定见 .agents/skills/wiki/SKILL.md;归档 drafts-archive 仅计数,不参与文件比对。
|
|
4
|
+
// 口径(2026-09-17 起,与 gen-wiki-board.mjs 保持一致):主题目录顶层全量文件登记 +
|
|
5
|
+
// 直属子目录中的 md/html 知识文档(排除 *.visual-check.* 工具产物;数据维护主题例外——
|
|
6
|
+
// 其子目录 schema变更/<批次>/ 等走 README 台账,不参与三方文件比对与 summary 统计);
|
|
7
|
+
// 递归扫描仅用于「断链守卫 + 数据维护批次台账登记」两项断言,避免改变既有统计口径误报。
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { execSync } from 'node:child_process';
|
|
11
|
+
|
|
12
|
+
const WIKI = 'wiki';
|
|
13
|
+
const problems = [];
|
|
14
|
+
const notes = [];
|
|
15
|
+
|
|
16
|
+
// 递归列目录(相对 dir 的路径,仅正斜杠);drafts-archive 由调用方排除
|
|
17
|
+
const walkTree = (dir, rel = '') => {
|
|
18
|
+
const acc = { files: [], dirs: [] };
|
|
19
|
+
for (const e of fs.readdirSync(dir)) {
|
|
20
|
+
const p = path.join(dir, e);
|
|
21
|
+
const r = rel ? `${rel}/${e}` : e;
|
|
22
|
+
if (fs.statSync(p).isDirectory()) {
|
|
23
|
+
acc.dirs.push(r);
|
|
24
|
+
const sub = walkTree(p, r);
|
|
25
|
+
acc.files.push(...sub.files);
|
|
26
|
+
acc.dirs.push(...sub.dirs);
|
|
27
|
+
} else acc.files.push(r);
|
|
28
|
+
}
|
|
29
|
+
return acc;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// ---- 磁盘枚举(登记口径与 gen-wiki-board.mjs 一致)----
|
|
33
|
+
const isKnowledgeDoc = (f) => /\.(md|html)$/i.test(f) && !/\.visual-check\./i.test(f);
|
|
34
|
+
const diskFiles = new Set(); // "主题/文件名"(子目录文件名带 子目录/ 前缀)
|
|
35
|
+
const diskTopics = new Set();
|
|
36
|
+
const activeTree = { files: [], dirs: [] }; // 递归视图(断链守卫 / 台账登记断言用)
|
|
37
|
+
for (const d of fs.readdirSync(WIKI)) {
|
|
38
|
+
const p = path.join(WIKI, d);
|
|
39
|
+
if (!fs.statSync(p).isDirectory()) continue;
|
|
40
|
+
if (d === 'drafts-archive') continue;
|
|
41
|
+
diskTopics.add(d);
|
|
42
|
+
for (const f of fs.readdirSync(p)) {
|
|
43
|
+
if (fs.statSync(path.join(p, f)).isFile()) diskFiles.add(`${d}/${f}`);
|
|
44
|
+
}
|
|
45
|
+
if (d !== '数据维护') {
|
|
46
|
+
for (const sub of fs.readdirSync(p)) {
|
|
47
|
+
const sp = path.join(p, sub);
|
|
48
|
+
if (!fs.statSync(sp).isDirectory()) continue;
|
|
49
|
+
for (const f of fs.readdirSync(sp)) {
|
|
50
|
+
if (fs.statSync(path.join(sp, f)).isFile() && isKnowledgeDoc(f)) diskFiles.add(`${d}/${sub}/${f}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const sub = walkTree(p, d);
|
|
55
|
+
activeTree.files.push(...sub.files);
|
|
56
|
+
activeTree.dirs.push(...sub.dirs);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// gitignore 豁免(F4,incident 2026-09-23-code-review-f3-f4-closeout):drafts-archive 含
|
|
60
|
+
// .gitignore 忽略的本机文件(如 *.xlsx),计入会使生成物计数在 fresh clone 必然漂移;
|
|
61
|
+
// 口径 = 排除忽略文件(git 不可用/非仓库时退化为全量计数,fixture 环境不受影响)
|
|
62
|
+
const gitIgnoredArchive = (() => {
|
|
63
|
+
try {
|
|
64
|
+
const out = execSync('git -c core.quotepath=false status --ignored=matching --porcelain -uall -- wiki/drafts-archive',
|
|
65
|
+
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
66
|
+
const prefix = 'wiki/drafts-archive/';
|
|
67
|
+
return new Set(out.split(/\r?\n/)
|
|
68
|
+
.filter((l) => l.startsWith('!! '))
|
|
69
|
+
.map((l) => l.slice(3).trim().replace(/\/$/, ''))
|
|
70
|
+
.filter((p) => p.startsWith(prefix))
|
|
71
|
+
.map((p) => p.slice(prefix.length)));
|
|
72
|
+
} catch {
|
|
73
|
+
return new Set();
|
|
74
|
+
}
|
|
75
|
+
})();
|
|
76
|
+
const archiveCount = walkTree(path.join(WIKI, 'drafts-archive')).files.filter((r) => !gitIgnoredArchive.has(r)).length;
|
|
77
|
+
|
|
78
|
+
// ---- 看板 DATA 解析 ----
|
|
79
|
+
const html = fs.readFileSync(path.join(WIKI, '知识沉淀总览.html'), 'utf8');
|
|
80
|
+
const m = html.match(/const DATA = (\{[\s\S]*?\n\});/);
|
|
81
|
+
if (!m) {
|
|
82
|
+
console.error('❌ 看板 DATA 解析失败:知识沉淀总览.html 中找不到 `const DATA = {...}`');
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
const DATA = eval('(' + m[1] + ')');
|
|
86
|
+
const boardFiles = new Set();
|
|
87
|
+
for (const t of DATA.topics) for (const f of t.files) boardFiles.add(`${t.name}/${f.file}`);
|
|
88
|
+
const boardTopics = new Set(DATA.topics.map((t) => t.name));
|
|
89
|
+
|
|
90
|
+
// ---- INDEX 映射表解析 ----
|
|
91
|
+
const indexFiles = new Set();
|
|
92
|
+
const indexTopicCounts = new Map(); // 速览表 topic → 文件数
|
|
93
|
+
for (const line of fs.readFileSync(path.join(WIKI, 'INDEX.md'), 'utf8').split(/\r?\n/)) {
|
|
94
|
+
const mm = line.match(/^\| ([^|]+) \| ([^|]+) \| wiki\/([^/]+)\/[^|]*\|$/);
|
|
95
|
+
if (mm) indexFiles.add(`${mm[3].trim()}/${mm[1].trim()}`);
|
|
96
|
+
const ov = line.match(/^\| ([^|]+) \| (\d+) \|[^|]*\|$/);
|
|
97
|
+
if (ov && ov[1].trim() !== '主题') indexTopicCounts.set(ov[1].trim(), Number(ov[2]));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const diff = (a, b) => [...a].filter((x) => !b.has(x));
|
|
101
|
+
const eq = (a, b) => a.size === b.size && diff(a, b).length === 0;
|
|
102
|
+
|
|
103
|
+
// ---- 1. 文件级三方比对 ----
|
|
104
|
+
for (const [label, extra] of [
|
|
105
|
+
['看板→磁盘', diff(boardFiles, diskFiles)],
|
|
106
|
+
['磁盘→看板', diff(diskFiles, boardFiles)],
|
|
107
|
+
['INDEX→磁盘', diff(indexFiles, diskFiles)],
|
|
108
|
+
['磁盘→INDEX', diff(diskFiles, indexFiles)],
|
|
109
|
+
]) {
|
|
110
|
+
if (extra.length) problems.push(`文件集合不一致(${label}): ${extra.join(';')}`);
|
|
111
|
+
}
|
|
112
|
+
if (problems.length === 0) notes.push(`文件级三方一致:${diskFiles.size} 份`);
|
|
113
|
+
|
|
114
|
+
// ---- 2. 幽灵目录:磁盘有主题目录但看板没有 ----
|
|
115
|
+
const ghosts = diff(diskTopics, boardTopics);
|
|
116
|
+
if (ghosts.length) problems.push(`磁盘存在看板未登记的主题目录: ${ghosts.join('、')}`);
|
|
117
|
+
|
|
118
|
+
// ---- 3. 占位主题(看板有、磁盘 0 文件)——允许,仅提示 ----
|
|
119
|
+
for (const t of DATA.topics) {
|
|
120
|
+
if (t.files.length === 0 && fs.existsSync(path.join(WIKI, t.name))) {
|
|
121
|
+
const real = fs.readdirSync(path.join(WIKI, t.name)).filter((f) => fs.statSync(path.join(WIKI, t.name, f)).isFile()).length;
|
|
122
|
+
if (real > 0) problems.push(`看板主题「${t.name}」files 为空但磁盘有 ${real} 个文件`);
|
|
123
|
+
else notes.push(`占位主题「${t.name}」:0 文件(允许)`);
|
|
124
|
+
} else if (t.files.length === 0) {
|
|
125
|
+
problems.push(`看板主题「${t.name}」files 为空且磁盘无此目录`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---- 4. INDEX 速览表文件数 vs 磁盘 ----
|
|
130
|
+
for (const [topic, count] of indexTopicCounts) {
|
|
131
|
+
const actual = [...diskFiles].filter((f) => f.startsWith(topic + '/')).length;
|
|
132
|
+
if (!diskTopics.has(topic) && count > 0) problems.push(`INDEX 速览表主题「${topic}」(${count}) 磁盘无此目录`);
|
|
133
|
+
else if (actual !== count) problems.push(`INDEX 速览表「${topic}」标 ${count},磁盘实际 ${actual}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---- 5. summary 统计字段 ----
|
|
137
|
+
const s = DATA.summary;
|
|
138
|
+
const sumChecks = [
|
|
139
|
+
['summary.total', s.total, diskFiles.size],
|
|
140
|
+
['summary.files', s.files, diskFiles.size],
|
|
141
|
+
['summary.topics', s.topics, DATA.topics.length],
|
|
142
|
+
['summary.archive', s.archive, archiveCount],
|
|
143
|
+
];
|
|
144
|
+
for (const [label, declared, actual] of sumChecks) {
|
|
145
|
+
if (declared !== actual) problems.push(`${label}=${declared},实际 ${actual}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---- 6. 活跃文件禁 file:/// 绝对链(搬家断链防复发;递归覆盖子目录)----
|
|
149
|
+
const badLinks = [];
|
|
150
|
+
for (const f of activeTree.files) {
|
|
151
|
+
if (!/\.(md|html)$/i.test(f)) continue;
|
|
152
|
+
if (fs.readFileSync(path.join(WIKI, f), 'utf8').includes('file:///')) badLinks.push(f);
|
|
153
|
+
}
|
|
154
|
+
if (badLinks.length) problems.push(`以下活跃文件含 file:/// 绝对路径链接: ${badLinks.join('、')}`);
|
|
155
|
+
|
|
156
|
+
// ---- 7. 数据维护台账登记:子目录(含批次目录)须在 wiki/数据维护/README.md 有登记行 ----
|
|
157
|
+
// 「数据维护走 README 台账」是可选主题约定——目录不存在(新项目未建该主题)时整体跳过
|
|
158
|
+
const maintLedger = path.join(WIKI, '数据维护', 'README.md');
|
|
159
|
+
if (fs.existsSync(maintLedger)) {
|
|
160
|
+
const maintReadme = fs.readFileSync(maintLedger, 'utf8');
|
|
161
|
+
const maintReadmeFlat = maintReadme.replace(/\s+/g, ''); // README 标题写作「schema 变更」(带空格),故去空白再比对
|
|
162
|
+
const maintDirs = activeTree.dirs.filter((d) => d.startsWith('数据维护/'));
|
|
163
|
+
const unregistered = maintDirs
|
|
164
|
+
.map((d) => d.split('/').pop())
|
|
165
|
+
.filter((name) => !maintReadme.includes(name) && !maintReadmeFlat.includes(name.replace(/\s+/g, '')));
|
|
166
|
+
if (unregistered.length) problems.push(`数据维护台账未登记(wiki/数据维护/README.md 无此行):${unregistered.join('、')}`);
|
|
167
|
+
else notes.push(`数据维护台账登记完整:${maintDirs.length} 个子目录(含批次目录)均有登记`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---- 8. 看板链接可达性:DATA.dir(「打开目录」href)+ 静态相对 href 解析后须存在 ----
|
|
171
|
+
const deadLinks = [];
|
|
172
|
+
for (const t of DATA.topics) {
|
|
173
|
+
for (const f of t.files) {
|
|
174
|
+
const p = path.join(WIKI, f.dir); // 看板自身位于 wiki/,dir 为相对它解析
|
|
175
|
+
if (!fs.existsSync(p) || !fs.statSync(p).isDirectory()) deadLinks.push(`${t.name}/${f.file} → ${f.dir}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
for (const m2 of html.matchAll(/href="([^"]+)"/g)) {
|
|
179
|
+
const href = m2[1];
|
|
180
|
+
if (/^(https?:|mailto:|#)/i.test(href) || href.includes('${')) continue; // 外链 / 锚点 / 模板占位
|
|
181
|
+
if (!fs.existsSync(path.join(WIKI, href))) deadLinks.push(`静态 href → ${href}`);
|
|
182
|
+
}
|
|
183
|
+
if (deadLinks.length) problems.push(`看板链接指向不存在的路径(相对 wiki/ 解析): ${deadLinks.join(';')}`);
|
|
184
|
+
|
|
185
|
+
// ---- 输出 ----
|
|
186
|
+
for (const n of notes) console.log('ℹ️ ' + n);
|
|
187
|
+
if (problems.length) {
|
|
188
|
+
console.error('\n❌ wiki 一致性校验失败:');
|
|
189
|
+
for (const p of problems) console.error(' - ' + p);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
console.log(`✅ wiki 三方一致:文件 ${diskFiles.size} 份 / 主题 ${DATA.topics.length} 个(含占位)/ 归档 ${archiveCount} 份`);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// wiki 活跃层全文检索(2026-09-13 引入;2026-09-21 起降为 kb-search.mjs 的 wiki 域转发壳)
|
|
3
|
+
// —— 检索实现与降噪口径单一维护在 .agents/scripts/kb-search.mjs,本脚本保留以兼容既有文档 / SKILL §8 的命令。
|
|
4
|
+
// 用法:node .agents/scripts/wiki-search.mjs <词1> [词2 ...] 多词 AND(全部命中才算)
|
|
5
|
+
// node .agents/scripts/wiki-search.mjs -t 数据维护 <词> 限定主题
|
|
6
|
+
// node .agents/scripts/wiki-search.mjs -n 0 <词> 输出上限(本脚本默认 20,0=不限)
|
|
7
|
+
// 语料固定为 wiki 活跃层(含数据维护子目录;排除 drafts-archive、INDEX.md、知识沉淀总览.html);
|
|
8
|
+
// 默认跳过 .json/.csv、*.visual-check.* 与 >180KB bulk(--include-generated 纳入)。
|
|
9
|
+
// 跨语料(workflow + wiki)请直接用:node .agents/scripts/kb-search.mjs "<词>"
|
|
10
|
+
import { spawnSync } from 'node:child_process';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const args = process.argv.slice(2);
|
|
16
|
+
// 兼容保留 P0 版默认输出上限 20;用户显式传 -n 时不覆盖;--scope wiki 追加在末尾以固定语料(后出现者生效)
|
|
17
|
+
const pass = [...args, ...(args.includes('-n') ? [] : ['-n', '20']), '--scope', 'wiki'];
|
|
18
|
+
const r = spawnSync(process.execPath, [path.join(here, 'kb-search.mjs'), ...pass], { stdio: 'inherit' });
|
|
19
|
+
process.exit(r.status ?? 1);
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// workflow 实时看板服务:本地只读 http + SSE 文件监听,可视化 AI 执行流程(intents/incidents/plans/specs)。
|
|
3
|
+
// 用法:node .agents/scripts/workflow-board-server.mjs [--port 8933]
|
|
4
|
+
// 零依赖(node:http/node:fs/node:path/node:url/node:child_process);只读 workflow/ 与 git,仅绑 127.0.0.1。
|
|
5
|
+
// API:GET /(看板页) /marked.min.js(vendor) /api/board /api/doc?file=<相对路径> /api/history?file=<相对路径> /api/events(SSE)
|
|
6
|
+
// 实时边界:文档内容与状态随落盘实时(fs.watch→SSE 推送);git 历史仅含已提交记录(git 语义)。
|
|
7
|
+
import http from 'node:http';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { execFile } from 'node:child_process';
|
|
12
|
+
|
|
13
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
14
|
+
const WORKFLOW = path.join(ROOT, 'workflow');
|
|
15
|
+
const BOARD_DIR = path.join(ROOT, '.agents', 'board');
|
|
16
|
+
const DOC_TYPES = ['intents', 'incidents', 'plans', 'specs'];
|
|
17
|
+
|
|
18
|
+
const portArg = process.argv.indexOf('--port');
|
|
19
|
+
const PORT = portArg > 0 ? Number(process.argv[portArg + 1]) || 8933 : 8933;
|
|
20
|
+
|
|
21
|
+
// ---- front matter + 标题解析(对照 _TEMPLATE.md 受限子集:每行 `键: 值`)----
|
|
22
|
+
function parseDoc(text) {
|
|
23
|
+
const meta = {};
|
|
24
|
+
const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
25
|
+
if (m) {
|
|
26
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
27
|
+
const kv = line.match(/^(\S+):\s*(.*)$/);
|
|
28
|
+
if (kv) meta[kv[1].trim()] = kv[2].trim();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const h1 = text.match(/^# (.+)$/m);
|
|
32
|
+
let title = h1 ? h1[1] : '';
|
|
33
|
+
const dash = title.match(/\S+\s*[—–-]+\s*(.+)$/); // 去掉 "# INTENT — " 前缀,留主题
|
|
34
|
+
if (dash) title = dash[1];
|
|
35
|
+
return { meta, title };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---- 验收标准 checkbox 统计:仅「## 验收标准」节内(至下一个 ## 标题),无该节返回 null ----
|
|
39
|
+
function parseAcceptance(text) {
|
|
40
|
+
const h = text.match(/^## 验收标准.*$/m);
|
|
41
|
+
if (!h) return null;
|
|
42
|
+
const rest = text.slice(h.index + h[0].length);
|
|
43
|
+
const next = rest.match(/^## /m);
|
|
44
|
+
const section = next ? rest.slice(0, next.index) : rest;
|
|
45
|
+
const total = (section.match(/^\s*[-*] \[[ xX]\]/gm) || []).length;
|
|
46
|
+
if (!total) return null;
|
|
47
|
+
const done = (section.match(/^\s*[-*] \[[xX]\]/gm) || []).length;
|
|
48
|
+
return { done, total };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---- 配对断裂检测:按 slug 聚合同族(同名 intent/incident/plan/spec),异常卡附 alerts ----
|
|
52
|
+
// 规则(口径对齐 check-loop.sh 的硬断档 + 状态枚举,看板为预警层、不阻断):
|
|
53
|
+
// 孤儿 spec / 孤儿 plan(无同名 intent·incident 入口);入口 done 但 plan 未终态(看板启发式);
|
|
54
|
+
// 入口缺 plan(intent 一律要求;incident 需非 legacy 且级别 L1/L2/L3);L2/L3 入口缺同名 spec;
|
|
55
|
+
// spec L3 确认三件缺失(确认结果 / 确认时间 / 正文独立复核行);
|
|
56
|
+
// 状态不在枚举内或缺失(intent·spec·plan: approved/done/superseded/cancelled;incident: open/fixed/closed)
|
|
57
|
+
const PLAN_TERMINAL = ['done', 'superseded', 'cancelled'];
|
|
58
|
+
const STATUS_ENUM = {
|
|
59
|
+
intents: ['approved', 'done', 'superseded', 'cancelled'],
|
|
60
|
+
specs: ['approved', 'done', 'superseded', 'cancelled'],
|
|
61
|
+
plans: ['approved', 'done', 'superseded', 'cancelled'],
|
|
62
|
+
incidents: ['open', 'fixed', 'closed'],
|
|
63
|
+
};
|
|
64
|
+
const NO_STATUS = '(未填)';
|
|
65
|
+
const L3_REVIEW = /^- 独立复核:[ \t]*[^<\s]/m; // 正文独立复核行须有实质内容(对照 check-loop 锚定)
|
|
66
|
+
|
|
67
|
+
function detectAlerts(cards) {
|
|
68
|
+
const bySlug = new Map();
|
|
69
|
+
for (const c of cards) {
|
|
70
|
+
if (!bySlug.has(c.slug)) bySlug.set(c.slug, []);
|
|
71
|
+
bySlug.get(c.slug).push(c);
|
|
72
|
+
}
|
|
73
|
+
for (const c of cards) {
|
|
74
|
+
const fam = bySlug.get(c.slug) || [];
|
|
75
|
+
const types = new Set(fam.map((x) => x.type));
|
|
76
|
+
const alerts = [];
|
|
77
|
+
const isEntry = c.type === 'intents' || c.type === 'incidents';
|
|
78
|
+
// legacy 复盘件免配对类告警(口径同 check-loop);孤儿 spec / 孤儿 plan 两条仍按 spec·plan 自身判定
|
|
79
|
+
const legacyIncident = c.type === 'incidents' && c.flow === 'legacy';
|
|
80
|
+
if (!STATUS_ENUM[c.type].includes(c.status))
|
|
81
|
+
alerts.push(`状态不在枚举内:须为 ${STATUS_ENUM[c.type].join('/')}(现 ${c.status === NO_STATUS ? '缺失' : c.status})`);
|
|
82
|
+
if (c.type === 'specs' && !types.has('intents') && !types.has('incidents'))
|
|
83
|
+
alerts.push('孤儿 spec:无同名 intent/incident 入口');
|
|
84
|
+
if (c.type === 'plans' && !types.has('intents') && !types.has('incidents'))
|
|
85
|
+
alerts.push('孤儿 plan:无同名 intent/incident 入口');
|
|
86
|
+
if (c.type === 'plans') {
|
|
87
|
+
const entry = fam.find((x) => x.type === 'intents' || x.type === 'incidents');
|
|
88
|
+
if (entry && entry.status === 'done' && !PLAN_TERMINAL.includes(c.status))
|
|
89
|
+
alerts.push(`入口已 done,本 plan 未终态(现 ${c.status})`);
|
|
90
|
+
}
|
|
91
|
+
if (isEntry && !legacyIncident && !types.has('plans') && (c.type === 'intents' || ['L1', 'L2', 'L3'].includes(c.level)))
|
|
92
|
+
alerts.push('入口缺 plan:无同名 plan');
|
|
93
|
+
if (isEntry && !legacyIncident && ['L2', 'L3'].includes(c.level) && !types.has('specs'))
|
|
94
|
+
alerts.push(`${c.level} 入口缺同名 spec`);
|
|
95
|
+
if (c.type === 'specs' && c.level === 'L3' && !['superseded', 'cancelled'].includes(c.status)) {
|
|
96
|
+
if (c.confirm !== 'approved') alerts.push(`L3 确认缺失:确认结果须为 approved(现 ${c.confirm || '缺失'})`);
|
|
97
|
+
if (!c.confirmTime) alerts.push('L3 确认缺失:须记录确认时间');
|
|
98
|
+
if (!c.hasReview) alerts.push('L3 复核缺失:须记录新会话独立复核结论');
|
|
99
|
+
}
|
|
100
|
+
if (alerts.length) c.alerts = alerts;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---- 文档作者:单次 git log 批量取「文件 → 最早提交者」(--reverse 首次出现即创建者)----
|
|
105
|
+
function gitAuthors() {
|
|
106
|
+
return new Promise((resolve) => {
|
|
107
|
+
const GIT = process.platform === 'win32' ? 'git.exe' : 'git';
|
|
108
|
+
execFile(GIT, ['-c', 'core.quotepath=off', 'log', '--reverse', '--pretty=format:%x00%an', '--name-only', '--', 'workflow'], { cwd: ROOT, maxBuffer: 8 * 1024 * 1024 }, (err, stdout) => {
|
|
109
|
+
if (err) { console.error('git log 取作者失败(卡片 author 置空):', err.message); return resolve(new Map()); }
|
|
110
|
+
const map = new Map();
|
|
111
|
+
let author = '';
|
|
112
|
+
for (const line of stdout.toString('utf8').split(/\r?\n/)) {
|
|
113
|
+
if (line.startsWith('\0')) author = line.slice(1);
|
|
114
|
+
else if (line.trim() && !map.has(line.trim())) map.set(line.trim(), author);
|
|
115
|
+
}
|
|
116
|
+
resolve(map);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---- 看板全量数据:每次请求现扫磁盘(数据量小,无缓存即无失效 bug)----
|
|
122
|
+
async function scanBoard() {
|
|
123
|
+
const authors = await gitAuthors();
|
|
124
|
+
const cards = [];
|
|
125
|
+
const counts = {};
|
|
126
|
+
for (const type of DOC_TYPES) {
|
|
127
|
+
const dir = path.join(WORKFLOW, type);
|
|
128
|
+
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md') && !f.startsWith('_TEMPLATE'));
|
|
129
|
+
counts[type] = files.length;
|
|
130
|
+
for (const f of files) {
|
|
131
|
+
let text = '';
|
|
132
|
+
try { text = fs.readFileSync(path.join(dir, f), 'utf8'); } catch { continue; } // watch 触发瞬间可能正被写,跳过等下次推送
|
|
133
|
+
const { meta, title } = parseDoc(text);
|
|
134
|
+
const accept = parseAcceptance(text);
|
|
135
|
+
const name = f.slice(0, -3);
|
|
136
|
+
cards.push({
|
|
137
|
+
type,
|
|
138
|
+
file: `${type}/${f}`,
|
|
139
|
+
name,
|
|
140
|
+
slug: name.replace(/^\d{4}-\d{2}-\d{2}-/, ''),
|
|
141
|
+
date: meta['日期'] || name.slice(0, 10),
|
|
142
|
+
status: meta['状态'] || NO_STATUS,
|
|
143
|
+
level: meta['级别'] || '',
|
|
144
|
+
title: title || name,
|
|
145
|
+
author: authors.get(`workflow/${type}/${f}`) || '',
|
|
146
|
+
...(accept ? { accept } : {}),
|
|
147
|
+
// 告警规则用到的 frontmatter / 正文锚点(对照 check-loop:legacy 豁免、L3 确认三件)
|
|
148
|
+
...(meta['流程'] ? { flow: meta['流程'] } : {}),
|
|
149
|
+
...(meta['确认结果'] ? { confirm: meta['确认结果'] } : {}),
|
|
150
|
+
...(meta['确认时间'] ? { confirmTime: meta['确认时间'] } : {}),
|
|
151
|
+
...(L3_REVIEW.test(text) ? { hasReview: true } : {}),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
detectAlerts(cards);
|
|
156
|
+
cards.sort((a, b) => b.date.localeCompare(a.date) || a.type.localeCompare(b.type));
|
|
157
|
+
return { root: ROOT, counts, cards };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---- 路径白名单:resolve 后必须仍在 workflow/ 内 ----
|
|
161
|
+
function safeDocPath(rel) {
|
|
162
|
+
const abs = path.resolve(WORKFLOW, rel);
|
|
163
|
+
if (!abs.startsWith(WORKFLOW + path.sep) || !abs.endsWith('.md')) return null;
|
|
164
|
+
return abs;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function gitLog(rel) {
|
|
168
|
+
return new Promise((resolve) => {
|
|
169
|
+
const GIT = process.platform === 'win32' ? 'git.exe' : 'git'; // Windows spawn 不补 .exe 扩展名
|
|
170
|
+
execFile(GIT, ['log', '--follow', '--date=short', '--pretty=%H|%ad|%s', '--', rel.split('/').join(path.sep)], { cwd: WORKFLOW, maxBuffer: 4 * 1024 * 1024 }, (err, stdout) => {
|
|
171
|
+
if (err) { console.error('git log 失败:', rel, err.message); return resolve([]); }
|
|
172
|
+
resolve(stdout.toString('utf8').split(/\r?\n/).filter(Boolean).map((l) => {
|
|
173
|
+
const [hash, date, ...rest] = l.split('|');
|
|
174
|
+
return { hash, date, subject: rest.join('|') };
|
|
175
|
+
}));
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---- SSE 广播:fs.watch 防抖 500ms(AI 写盘常连发多事件,合并为一次推送)----
|
|
181
|
+
const sseClients = new Set();
|
|
182
|
+
let debounceTimer = null;
|
|
183
|
+
function broadcast(event) {
|
|
184
|
+
for (const res of sseClients) { if (!res.destroyed) res.write(`event: ${event}\ndata: {}\n\n`); }
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const watcher = fs.watch(WORKFLOW, { recursive: true }, () => {
|
|
188
|
+
clearTimeout(debounceTimer);
|
|
189
|
+
debounceTimer = setTimeout(() => broadcast('changed'), 500);
|
|
190
|
+
});
|
|
191
|
+
// watch 中途失效(目录被动/杀软锁):显式告知前端降级轮询,避免假实时
|
|
192
|
+
watcher.on('error', (e) => { console.error('fs.watch 失效,已通知前端降级轮询:', e.message); broadcast('watchdead'); });
|
|
193
|
+
} catch (e) {
|
|
194
|
+
console.error('fs.watch 初始化失败(前端将走 60s 兜底轮询):', e.message);
|
|
195
|
+
}
|
|
196
|
+
// 25s 心跳:防半开连接;顺带清理已断开的 client
|
|
197
|
+
setInterval(() => { for (const res of sseClients) { if (res.destroyed) sseClients.delete(res); else res.write(': ping\n\n'); } }, 25000).unref();
|
|
198
|
+
|
|
199
|
+
const MIME = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8' };
|
|
200
|
+
|
|
201
|
+
function sendJson(res, code, obj) {
|
|
202
|
+
res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
203
|
+
res.end(JSON.stringify(obj));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const server = http.createServer(async (req, res) => {
|
|
207
|
+
const url = new URL(req.url, `http://127.0.0.1:${PORT}`);
|
|
208
|
+
try {
|
|
209
|
+
if (url.pathname === '/api/events') {
|
|
210
|
+
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' });
|
|
211
|
+
res.write('retry: 3000\n\n');
|
|
212
|
+
sseClients.add(res);
|
|
213
|
+
req.on('close', () => sseClients.delete(res));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (url.pathname === '/api/board') return sendJson(res, 200, await scanBoard());
|
|
217
|
+
|
|
218
|
+
if (url.pathname === '/api/doc' || url.pathname === '/api/history') {
|
|
219
|
+
const rel = url.searchParams.get('file') || '';
|
|
220
|
+
const abs = safeDocPath(rel);
|
|
221
|
+
if (!abs) return sendJson(res, 403, { error: '路径不在 workflow/ 白名单内' });
|
|
222
|
+
if (url.pathname === '/api/doc') {
|
|
223
|
+
try { return sendJson(res, 200, { text: fs.readFileSync(abs, 'utf8') }); }
|
|
224
|
+
catch { return sendJson(res, 404, { error: '文件不存在' }); }
|
|
225
|
+
}
|
|
226
|
+
return sendJson(res, 200, { commits: await gitLog(rel) });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 静态文件:/ 与 /marked.min.js,同样防穿越
|
|
230
|
+
const rel = url.pathname === '/' ? 'index.html' : url.pathname.slice(1);
|
|
231
|
+
const abs = path.resolve(BOARD_DIR, rel);
|
|
232
|
+
if (!abs.startsWith(BOARD_DIR + path.sep) || !fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
|
|
233
|
+
res.writeHead(404); return res.end('not found');
|
|
234
|
+
}
|
|
235
|
+
res.writeHead(200, { 'Content-Type': MIME[path.extname(abs)] || 'application/octet-stream' });
|
|
236
|
+
fs.createReadStream(abs).pipe(res);
|
|
237
|
+
} catch (e) {
|
|
238
|
+
sendJson(res, 500, { error: e.message });
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
server.listen(PORT, '127.0.0.1', () => {
|
|
243
|
+
console.log(`workflow 看板: http://127.0.0.1:${PORT} (Ctrl+C 停止;只读 workflow/,不写任何文件)`);
|
|
244
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: verify-ui
|
|
3
|
+
description: 启动本地服务并用浏览器实测 UI 改动(只读验证、不留脏数据);项目无测试基建时靠实测兜底。当用户要求"实测 / UI 验证 / 浏览器验证"时使用。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# verify-ui — UI 实测引导(骨架,项目按实际填「启动命令 / 实测入口」)
|
|
7
|
+
|
|
8
|
+
## 1. 起服务
|
|
9
|
+
|
|
10
|
+
- API:<待填:启动命令(见 .agents/notes/runtime-env.md §2)>
|
|
11
|
+
- 前端:<待填:启动命令>
|
|
12
|
+
|
|
13
|
+
进程清单记进 `.agents/notes/runtime-env.md`,实测结束必须关闭。
|
|
14
|
+
|
|
15
|
+
## 2. 实测步骤
|
|
16
|
+
|
|
17
|
+
1. 打开目标页面,按改动点逐项操作:正常路径一条 + 失败路径一条(校验/空态/权限)。
|
|
18
|
+
2. 证据落盘:截图或关键控制台/网络报文,路径写在任务文档「验收标准」勾验项后。
|
|
19
|
+
3. 不留脏数据:验证产生的临时数据当场删除或还原;不确定可回滚的操作先停下问用户。
|
|
20
|
+
|
|
21
|
+
## 3. 收尾
|
|
22
|
+
|
|
23
|
+
- 关闭本次起动的全部进程(按 §1 清单逐个停)。
|
|
24
|
+
- 结论按「页面 / 步骤 / 期望 vs 实际 / 证据」回报;发现缺陷只记录定性,不修代码(修复走 build/maintain 流程)。
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wiki
|
|
3
|
+
description: 项目 wiki 整理与验证技能。负责把项目内散落文档按主题归位到 wiki/,并维护 INDEX.md、知识沉淀总览.html、drafts-archive 归档一致性。当用户要求"整理 wiki/归类文档/建知识库/查文档归属"时使用。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Wiki 整理与验证技能
|
|
7
|
+
|
|
8
|
+
## 1. 何时调用本技能
|
|
9
|
+
|
|
10
|
+
触发短语:
|
|
11
|
+
- "整理 wiki / 归类文档 / 建知识库 / 同步知识沉淀看板"
|
|
12
|
+
- "新增一篇文档到 wiki"
|
|
13
|
+
- "校对 wiki 与 INDEX 是否一致"
|
|
14
|
+
- "把 X.md 归到 wiki 的哪个主题"
|
|
15
|
+
- "wiki 草稿归档 / 把原文归档"
|
|
16
|
+
|
|
17
|
+
不要调用本技能的场景:
|
|
18
|
+
- 询问文档内容本身(直接 Read 文件即可)
|
|
19
|
+
- workflow/ 下文档(已明确排除);`docs/`、`.trae/documents/` 均已于 2026-09-12 删除
|
|
20
|
+
|
|
21
|
+
## 2. 核心约定(与 AGENTS.md 一致)
|
|
22
|
+
|
|
23
|
+
- **范围**:主数据知识文档 + 全项目数据维护 SQL。MTO/采购/库存/UI 等非主数据知识文档原位不动,不入 wiki。
|
|
24
|
+
- **目录结构**:`wiki/<主题>/<关键词文件>`,一级目录 = 主题,二级由文件组成(无强制二级子目录)。子目录(成套产物,见 INDEX 命名规则)中的 md/html 知识文档 2026-09-17 起计入登记与看板(`*.visual-check.*`/`.json`/`.png` 工具产物不计入;`数据维护` 主题子目录例外,仍走 README 台账)。看板中 html 格式文档的文件名为可点击链接(相对看板解析)。
|
|
25
|
+
- **命名**:`<主题>-<关键词>[-范围/版本].ext`,例:`需求口径说明-正式.md`、`某系统-PRD-v3.0.md`。
|
|
26
|
+
- **权威**:红线与闭环以 AGENTS.md / workflow 为准;wiki 承载当前态知识(领域 / 表 / 编码 / SQL 台账)。wiki 当前文档优于 `drafts-archive/`(归档仅历史快照)。
|
|
27
|
+
- **双格式对(md + html 同内容)**:md 为权威内容源,html 为展示快照——内容冲突以 md 为准,改 md 后按需重出 html;速览表「用途」列标注权威关系。
|
|
28
|
+
- **索引生成(2026-09-13 起)**:速览表计数/映射表/看板 DATA 由 `node .agents/scripts/gen-wiki-board.mjs` 自动生成——人工只维护磁盘文件 + 速览表「用途」列;收录/删除/移动文件后必跑生成器(新主题用途列为 `<待补>` 时补描述后重跑)。写盘前可先 `--dry-run` 看差异(`--help` 打印用法,未知参数 exit 1)。
|
|
29
|
+
- **归档目录**:`wiki/drafts-archive/YYYY-MM-DD_<主题>/`,按整理批次日期+主题命名,保留原文件名。
|
|
30
|
+
- **排除**:`workflow/`、依赖锁文件、IDE 配置。
|
|
31
|
+
|
|
32
|
+
## 3. 标准操作流程
|
|
33
|
+
|
|
34
|
+
### 3.1 整理(新建/批量归位)
|
|
35
|
+
|
|
36
|
+
1. **识别范围**:列出候选文件后逐篇读首行判断主题——pwsh:`Get-ChildItem -Recurse -File -Include *.md` / `Get-Content <文件> -TotalCount 8`;POSIX:`find` / `head -8`。
|
|
37
|
+
2. **主题判定**:主题由内容自决,不预设分类。当前已沉淀主题:`产品需求 / 项目计划 / 开发方案 / 用户功能 / 项目规范 / 测试报告 / 数据维护`。新增主题前先与用户对齐。
|
|
38
|
+
3. **建目录**:pwsh `New-Item -ItemType Directory -Force wiki/<主题>`(POSIX:`mkdir -p`);归档目录同理建 `wiki/drafts-archive/YYYY-MM-DD_<主题>`。
|
|
39
|
+
4. **复制并改名**:pwsh `Copy-Item 原路径 wiki/<主题>/<新名>`(POSIX:`cp`);来源为仓库内原位文件时,同时按原名复制一份到 `drafts-archive/YYYY-MM-DD_<主题>/` 留溯源。
|
|
40
|
+
5. **生成索引**:跑 `node .agents/scripts/gen-wiki-board.mjs`(速览计数/映射表/看板 DATA 自动重生成);新主题在速览表「用途」列补一行描述后重跑。
|
|
41
|
+
6. **自检**:跑第 4 节"验证脚本",确认三方一致、目录无孤儿。
|
|
42
|
+
|
|
43
|
+
### 3.2 新增单篇文档(新产出直接落活跃层)
|
|
44
|
+
|
|
45
|
+
新产出的文档**直接归入 `wiki/<主题>/`**,不强制进 `drafts-archive/`——归档只承载整理前的历史原位文档(口径见 §3.1 第 4 步)。
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
1. 读文件首行判定主题
|
|
49
|
+
2. Copy-Item 原文件 wiki/<主题>/<主题>-<关键词>.<ext> # POSIX:cp
|
|
50
|
+
3. node .agents/scripts/gen-wiki-board.mjs (索引自动生成,无需手工改映射表/看板)
|
|
51
|
+
4. 跑 §4 验证
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 3.3 删除/归档 wiki 中错误归位
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
1. 从 wiki/<错误主题>/ 移到 wiki/<正确主题>/,若改名也同步
|
|
58
|
+
2. drafts-archive 不动(归档记录原貌)
|
|
59
|
+
3. node .agents/scripts/gen-wiki-board.mjs
|
|
60
|
+
4. 跑 §4 验证
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 3.4 重建看板
|
|
64
|
+
|
|
65
|
+
`wiki/知识沉淀总览.html` 的 DATA 块由生成器从磁盘 + 速览表用途列自动生成(2026-09-13 起)。看板与磁盘不一致时:先跑 `node .agents/scripts/gen-wiki-board.mjs` 再跑 §4 验证;仍不一致说明有人手改了生成区——以生成器输出为准。
|
|
66
|
+
|
|
67
|
+
## 4. 验证脚本(必跑)
|
|
68
|
+
一条命令完成全部校验(磁盘 ↔ INDEX ↔ 看板 DATA ↔ summary 统计 ↔ 幽灵目录 ↔ 活跃层 file:/// 断链守卫 ↔ 数据维护台账登记 ↔ 看板链接可达性):
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
node .agents/scripts/verify-wiki-consistency.mjs
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- 通过:输出 `✅ wiki 三方一致:文件 N 份 / 主题 N 个(含占位)/ 归档 N 份`,exit 0
|
|
75
|
+
- 失败:逐条列出不一致项,exit 1。除文件集合 / 计数外还覆盖:`wiki/数据维护/` 下子目录(含批次目录)未在 `wiki/数据维护/README.md` 登记、看板「打开目录」链接(`DATA.dir`,相对看板自身 `wiki/` 解析)指向不存在的目录
|
|
76
|
+
- 占位主题(磁盘有目录但 0 文件)只输出 `ℹ️` 提示、不算失败;当前 7 个主题均非空(`开发方案` 2026-09-12 起已有 2 份表设计 md+html)
|
|
77
|
+
- 改了 wiki 内容/INDEX/看板任一处后必须重跑;三者任何一处漏同步都会被拦下
|
|
78
|
+
|
|
79
|
+
## 5. 命名冲突与歧义处置
|
|
80
|
+
|
|
81
|
+
- **同名不同扩展**:例如 `数据字典-修复.sql` 与 `数据字典-修复.md` —— 文件名可重复,但 `INDEX.md` 与看板 `DATA` 用 `file` 字段存完整文件名(含扩展)以区分。
|
|
82
|
+
- **跨主题歧义**:用户说"X 放哪"拿不准时,读首行 + 第一段判断;仍无法判定就先与用户确认主题,不预设兜底主题、不自造归类。
|
|
83
|
+
- **二进制(xlsx/csv/sql/json)**:允许直接归位到对应主题;不要为它们新建 markdown 索引——需要说明用途时写进 `wiki/INDEX.md` 速览表的「用途」列(唯一人工维护位,用例描述一句话;该表计数与映射表、看板 DATA 均为生成区,会被 `gen-wiki-board.mjs` 重写)。`wiki/数据维护/` 下的脚本与批次目录则在 `wiki/数据维护/README.md` 台账加登记行(verify 会拦未登记目录)。
|
|
84
|
+
|
|
85
|
+
## 6. 已知边界
|
|
86
|
+
|
|
87
|
+
- **workflow/ 永不入 wiki**:闭环工作流目录是项目规范明文排除项。
|
|
88
|
+
- **非主数据知识文档不入 wiki**(MTO/采购/库存/UI 等)——有意决策(2026-09-13 显式化;债与扩展触发条件见 `wiki/INDEX.md` §范围边界决策);数据维护 SQL 为全项目,不受此限。
|
|
89
|
+
- **dependency 锁文件 / IDE 配置**:不入 wiki(`package-lock.json`、`pnpm-lock.yaml`、`*.sln`、`*.esproj` 等)。
|
|
90
|
+
- **批量整理只动一份副本**:本技能只复制(`Copy-Item` / `cp`),不删除原文件;删除原文件需要用户在对话内单独确认(这是 STOP 线场景)。
|
|
91
|
+
|
|
92
|
+
## 7. 与 AGENTS.md 衔接
|
|
93
|
+
|
|
94
|
+
- 改动 `wiki/INDEX.md`、`wiki/知识沉淀总览.html`、`.agents/skills/wiki/SKILL.md` 属 docs 级改动,**豁免 intent**,走 Conventional Commits 直接 commit:`docs(wiki): <动作>`。
|
|
95
|
+
- 若同时改 wiki 内某个具体知识文档(含路径变更/改名),按该文档原主题级别立 intent / incident。
|
|
96
|
+
- wiki 不替代 AGENTS.md / workflow 的红线与闭环;与二者冲突时以 AGENTS.md / workflow 为准。wiki 与 `drafts-archive/` 冲突时以 wiki 当前文档为准。
|
|
97
|
+
|
|
98
|
+
## 8. 失败模式与回滚
|
|
99
|
+
|
|
100
|
+
- **忘跑生成器**:磁盘文件动了没跑 `gen-wiki-board.mjs` → §4 脚本报「仅在看板上 / 仅在磁盘上」差异 → 跑生成器即对齐。
|
|
101
|
+
- **手改生成区**:速览计数/映射表/看板 DATA 是生成产物,手改会被 verify 拦且下次生成被覆盖——用途列才是人工编辑位。
|
|
102
|
+
- **原文件误删**:本技能只复制(`Copy-Item` / `cp`),不删原文件;若用户单独要求删除原文件,必须先列目录确认(pwsh `Get-ChildItem` / POSIX `ls`)再操作。
|
|
103
|
+
- **主题命名反复**:以 §4 脚本 A 段为最终事实;速览表用途列的 `<待补>` 提示新主题未补描述。
|
|
104
|
+
- **全文检索**:找「哪个文件讲过 X」用 `node .agents/scripts/wiki-search.mjs <关键词>`(wiki 活跃层,多词 AND;不扫归档;默认跳过 json/csv、视觉校验产物与 >180KB bulk 文件,`--include-generated` 纳入);跨语料(workflow 流程留痕 + wiki)用 `node .agents/scripts/kb-search.mjs "<词>"`。
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# workflow 文档模块词表(单源:check-loop.sh 与 .agents/scripts/gen-workflow-index.mjs、kb-search.mjs 共读)
|
|
2
|
+
# 用法:workflow/{intents,specs,plans,incidents} 文档 frontmatter `模块:` 取下列之一(单值,取主导模块;
|
|
3
|
+
# 跨模块任务取承载任务意图的模块,检索面由 kb-search 全文兜住)。
|
|
4
|
+
# 新建文档必填;check-loop 只警告不阻断。按项目模块自行改这张表(改后同步 workflow/README.md「文档协议」段说明)。
|
|
5
|
+
backend
|
|
6
|
+
frontend
|
|
7
|
+
ui
|
|
8
|
+
infra
|
|
9
|
+
integration
|
|
10
|
+
pipeline
|
|
11
|
+
wiki
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Git commit-msg hook:提交类型枚举校验(自 pre-commit 迁入,2026-09-18 papercut 升级修复)
|
|
3
|
+
# AGENTS.md「git提交」:Conventional Commits 中文,feat/fix/docs/style/refactor/perf
|
|
4
|
+
# merge/revert 提交豁免(含自定义小写 merge:/revert: 前缀);$1 为 git 传入的本次消息文件(真实待提交内容)
|
|
5
|
+
|
|
6
|
+
msg=$(head -1 "$1" 2>/dev/null)
|
|
7
|
+
case "$msg" in
|
|
8
|
+
[Mm]erge*|[Rr]evert*|"") ;;
|
|
9
|
+
*)
|
|
10
|
+
if ! echo "$msg" | grep -qE "^(feat|fix|docs|style|refactor|perf)(\(|:|()"; then
|
|
11
|
+
echo "commit-msg: 提交类型须为 feat/fix/docs/style/refactor/perf(AGENTS.md「git提交」),当前首行:$msg" >&2
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
;;
|
|
15
|
+
esac
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Git 原生 post-commit hook(经 core.hooksPath 挂载,git commit 后触发)
|
|
3
|
+
# 非阻断缓存维护:把本次提交触及的 workflow/、wiki/ 文件从 kb-search 结构缓存
|
|
4
|
+
# (.agents/cache/kb-index.json)摘除,收敛 [mtimeMs,size] 两元组的
|
|
5
|
+
# 「同刻同尺寸」失效盲区至提交时点。
|
|
6
|
+
# 判据与设计:workflow/intents/2026-09-23-kb-cache-post-commit-evict.md
|
|
7
|
+
# 失败绝不影响提交结果:post-commit 本身非阻断 + evict 脚本坏输入静默 exit 0 + 此处 || true 三重兜底。
|
|
8
|
+
git diff-tree --no-commit-id --name-only -r HEAD \
|
|
9
|
+
| node .agents/scripts/kb-cache-evict.mjs 2>/dev/null || true
|
|
10
|
+
exit 0
|