agentic-flow-kit 0.2.1 → 0.3.1
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 +3 -1
- package/package.json +1 -1
- package/src/cli.mjs +4 -2
- package/src/doctor.mjs +13 -6
- package/src/init.mjs +96 -12
- package/src/init.test.mjs +56 -0
- package/src/profiles.mjs +8 -0
- package/src/render.mjs +6 -9
- package/src/run-tests.mjs +1 -0
- package/templates/AGENTS.md +1 -0
- package/templates/_agents/board/index.html +137 -70
- package/templates/_agents/cache/kb-index.json +1 -1
- package/templates/_agents/commands/test.md +1 -0
- package/templates/_agents/scripts/verify.mjs +36 -0
- package/templates/_agents/scripts/verify.test.mjs +77 -0
- 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/README.md
CHANGED
|
@@ -60,4 +60,6 @@ modules/gates/ 可选门禁模块(dotnet-ca:Clean Architecture 参考
|
|
|
60
60
|
- [x] v0.1.0:init(flags 模式)+ doctor + 4 宿主适配 + dotnet-ca 门禁模块
|
|
61
61
|
- [x] v0.2.0:sync 升级(未动覆盖 / 已改跳过报告 + `--force` / 生成器锚点重写)+ add-host / add-gate
|
|
62
62
|
- [x] v0.2.0 已发布:npm view agentic-flow-kit(npx agentic-flow-kit init 即用;默认镜像源用户需等 npmmirror 同步或 --registry npmjs)
|
|
63
|
-
- [x] v0.2.1:Shipyard 回流第二笔(模板引用去死链 + owned 台账记账策略:add-gate 接线刷新 + sync 盘面自愈)+ 全项目审查修复(量化台账 fail-loud / 生成器锚点与 ENOENT 守卫 / sync 跳过件持续报告 / P4 清账 / metrics glob
|
|
63
|
+
- [x] v0.2.1:Shipyard 回流第二笔(模板引用去死链 + owned 台账记账策略:add-gate 接线刷新 + sync 盘面自愈)+ 全项目审查修复(量化台账 fail-loud / 生成器锚点与 ENOENT 守卫 / sync 跳过件持续报告 / P4 清账 / metrics glob 词表收口)——已发布
|
|
64
|
+
- [x] v0.3.0:init 交互升级(序号菜单 + ANSI 色彩降级 + ts/js 技术栈别名归一 node)+ AGENTS.md 骨架探测补齐(已存在无标记时文末追加,原内容保留)+ wiki 模板预置 7 主题内容目录 + 关单 verify 固定编排(npm test + check-loop 一键过门,非绿不关单)+ workflow 看板纸墨视觉改版
|
|
65
|
+
- [x] v0.3.1:doctor 修复——Windows 无 sh 环境(PowerShell)把 sh ENOENT 误报成 check-loop hard-block(探测分流:明示「未跑勿当作通过」,git 钩子门禁不受影响)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-flow-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "AI-Native 闭环工作流 + wiki 知识层脚手架——flow-kit init 一条命令为新项目装好 .agents 工作流引擎(阶段命令/角色契约/门禁钩子/kb 检索/看板/规则面预算)+ workflow 文档闭环 + wiki 知识库,装完自包含、零运行时依赖",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
package/src/cli.mjs
CHANGED
|
@@ -14,7 +14,8 @@ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'
|
|
|
14
14
|
export const HELP = `flow-kit — AI-Native 闭环工作流 + wiki 知识层脚手架(agentic-flow-kit)
|
|
15
15
|
|
|
16
16
|
用法:
|
|
17
|
-
flow-kit init 安装到当前项目(裸跑无参数 →
|
|
17
|
+
flow-kit init 安装到当前项目(裸跑无参数 → 交互确认环节;带参数直接执行;
|
|
18
|
+
已存在的 AGENTS.md 无工作流骨架时文末追加补齐,其余已存在文件保守跳过)
|
|
18
19
|
flow-kit sync 升级 managed 文件(未改动→覆盖新版;本地已改→跳过并持续报告——台账不基线化,
|
|
19
20
|
不会被下次升级静默覆盖;--force 才覆盖;INDEX/看板等生成器目标重跑锚点重写;
|
|
20
21
|
owned 文件永不触碰)
|
|
@@ -23,7 +24,8 @@ export const HELP = `flow-kit — AI-Native 闭环工作流 + wiki 知识层脚
|
|
|
23
24
|
flow-kit doctor 体检:目录布局 / git 钩子 / managed 清单 / 索引漂移 / check-loop
|
|
24
25
|
|
|
25
26
|
init 选项(全部可选,均有默认值):
|
|
26
|
-
--stack <技术栈> dotnet | node | python | go | none(默认 none
|
|
27
|
+
--stack <技术栈> dotnet | node(ts/js) | python | go | none(默认 none;ts/js/typescript/javascript
|
|
28
|
+
归一为 node)——门禁配置三处:commit-check
|
|
27
29
|
条件编译检查(builds)与质量检测(checks:lint/类型/vet 等秒级确定性检查,
|
|
28
30
|
有对应配置文件才启用)、settings.json 自检验命令权限(allow)、
|
|
29
31
|
AGENTS.md「项目适配区」命令预填
|
package/src/doctor.mjs
CHANGED
|
@@ -127,13 +127,20 @@ export function doctor(args, pkgRoot) {
|
|
|
127
127
|
else add('WARN', `delegations 台账结构漂移——${String(agg.stderr || agg.stdout || '').split('\n')[0]}`);
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
// 7. check-loop
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
130
|
+
// 7. check-loop——先探 sh 可用性(对齐 run-tests.mjs 先例):Windows PowerShell 常无 sh,
|
|
131
|
+
// ENOENT 曾被吞进 hard-block 分支报成空原因假警报(incident 2026-09-24-doctor-sh-enoent)。
|
|
132
|
+
// git 钩子门禁不受此影响——git 以自带 sh 执行钩子,与用户 PATH 无关。
|
|
133
|
+
const shProbe = spawnSync('sh', ['-c', 'true'], { encoding: 'utf8' });
|
|
134
|
+
if (shProbe.status !== 0) {
|
|
135
|
+
add('WARN', '环境无 sh——check-loop 未跑,勿当作通过;装 Git Bash / WSL 后重跑 doctor(git 钩子门禁不受影响,git 以自带 sh 执行)');
|
|
135
136
|
} else {
|
|
136
|
-
|
|
137
|
+
const cl = spawnSync('sh', ['.agents/scripts/check-loop.sh'], { cwd: target, encoding: 'utf8' });
|
|
138
|
+
if (cl.status === 0) {
|
|
139
|
+
const warnTxt = String(cl.stderr || '').trim();
|
|
140
|
+
add('PASS', `check-loop 干净${warnTxt ? `(${warnTxt.split('\n').filter((l) => l.includes('WARN')).length} 条 advisory 警告)` : ''}`);
|
|
141
|
+
} else {
|
|
142
|
+
add('FAIL', `check-loop 有 hard-block:\n${String(cl.stderr || '').split('\n').slice(0, 8).join('\n')}`);
|
|
143
|
+
}
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
// 8. 看板端口(信息级)
|
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(已存在,保守跳过)
|
|
@@ -43,15 +48,7 @@ export function renderTree(srcRoot, targetRoot, vars, { force = false } = {}) {
|
|
|
43
48
|
const raw = fs.readFileSync(abs);
|
|
44
49
|
let out = raw;
|
|
45
50
|
if (raw.length < 512 * 1024 && !raw.includes(0)) {
|
|
46
|
-
const
|
|
47
|
-
const used = new Set();
|
|
48
|
-
const replaced = text.replace(PH_RE, (m, key) => {
|
|
49
|
-
if (Object.prototype.hasOwnProperty.call(vars, key)) {
|
|
50
|
-
used.add(key);
|
|
51
|
-
return vars[key];
|
|
52
|
-
}
|
|
53
|
-
return m; // 未知占位符原样保留,doctor 会告警
|
|
54
|
-
});
|
|
51
|
+
const replaced = renderContent(raw.toString('utf8'), vars);
|
|
55
52
|
if (replaced.includes('{{')) {
|
|
56
53
|
for (const m of replaced.matchAll(/\{\{([A-Z][A-Z0-9_]*)\}\}/g)) {
|
|
57
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/templates/AGENTS.md
CHANGED
|
@@ -2,93 +2,157 @@
|
|
|
2
2
|
<html lang="zh-CN">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta name="color-scheme" content="light">
|
|
5
7
|
<title>Workflow 看板 — AI 执行流程</title>
|
|
6
8
|
<script src="/marked.min.js"></script>
|
|
7
9
|
<style>
|
|
10
|
+
/* ============ 定调:纸墨工业(极致极简 × Industrial) ============
|
|
11
|
+
暖纸底 + 暖墨字;四类文档各一枚同重量低饱和标签色,只出现在小徽标与卡片左缘;
|
|
12
|
+
朱红是唯一强调色,只留给告警 / 老化 / 挂起等必须被看见的状态。
|
|
13
|
+
标题/数字/徽标用本机等宽字体栈(JetBrains Mono→Cascadia→Consolas),正文中文用雅黑;
|
|
14
|
+
零外部字体请求——看板是 127.0.0.1 本地工具,不引网络资源。 */
|
|
8
15
|
:root {
|
|
9
|
-
--bg: #
|
|
10
|
-
--
|
|
11
|
-
--
|
|
12
|
-
--
|
|
16
|
+
--bg: #f2f0e9; /* 暖纸 */
|
|
17
|
+
--surface: #f7f5ee; /* 顶栏 / 抽屉面板 */
|
|
18
|
+
--card: #fdfcf8; /* 卡片近白 */
|
|
19
|
+
--card-hover: #ffffff;
|
|
20
|
+
--border: #ddd7c8; /* 发丝线 */
|
|
21
|
+
--border-strong: #b5ad99;
|
|
22
|
+
--text: #26241d; /* 暖墨 */
|
|
23
|
+
--muted: #6c675a;
|
|
24
|
+
--faint: #99937f; /* 仅装饰:滚动条 / 占位符 */
|
|
25
|
+
--accent: #bc4327; /* 朱 — 唯一强调色 */
|
|
26
|
+
--intent: #3f6288; --incident: #a04b35; --spec: #6f5a88; --plan: #3e7157;
|
|
27
|
+
--ok: #3e7157; --warn: #7d6224; --bad: #a04b35;
|
|
28
|
+
--shadow-1: 0 1px 2px rgba(38,36,29,.05), 0 3px 10px rgba(38,36,29,.07);
|
|
29
|
+
--shadow-2: -16px 0 40px rgba(38,36,29,.16);
|
|
30
|
+
--ease: cubic-bezier(.16, 1, .3, 1);
|
|
31
|
+
--mono: "JetBrains Mono", "Cascadia Code", Consolas, "Courier New", monospace;
|
|
13
32
|
}
|
|
14
33
|
* { box-sizing: border-box; margin: 0; }
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
34
|
+
::selection { background: rgba(188,67,39,.18); }
|
|
35
|
+
::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
36
|
+
::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 5px; border: 2px solid var(--bg); }
|
|
37
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
38
|
+
body { background: var(--bg); color: var(--text); font: 14px/1.7 "Microsoft YaHei", "PingFang SC", "Segoe UI", sans-serif; letter-spacing: .01em; }
|
|
39
|
+
/* 纸张颗粒:5% 噪点叠层让纸面有物理质感;pointer-events 关闭不挡交互 */
|
|
40
|
+
body::before { content: ""; position: fixed; inset: 0; z-index: 0; pointer-events: none; opacity: .05;
|
|
41
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); }
|
|
42
|
+
header, #board { position: relative; z-index: 1; }
|
|
43
|
+
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
44
|
+
|
|
45
|
+
/* ---- 顶栏:墨字标题 + 朱印章方块(记忆点) ---- */
|
|
46
|
+
header { display: flex; align-items: center; gap: 14px; height: 58px; padding: 0 22px; background: var(--surface); border-bottom: 1px solid var(--border); }
|
|
47
|
+
header h1 { font: 700 15px/1 var(--mono); letter-spacing: .02em; }
|
|
48
|
+
header h1::after { content: ""; display: inline-block; width: 7px; height: 7px; background: var(--accent); margin-left: 9px; }
|
|
49
|
+
.chip { font: 500 11px/1 var(--mono); color: var(--muted); background: transparent; border: 1px solid var(--border); border-radius: 3px; padding: 4px 8px; font-variant-numeric: tabular-nums; }
|
|
50
|
+
#sse { display: inline-flex; align-items: center; gap: 6px; font: 11px/1 var(--mono); color: var(--muted); margin-left: auto; }
|
|
51
|
+
#sse i { width: 7px; height: 7px; border-radius: 50%; background: var(--bad); }
|
|
21
52
|
#sse.on i { background: var(--ok); }
|
|
22
|
-
#
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
.
|
|
30
|
-
.
|
|
31
|
-
.
|
|
53
|
+
#sse.poll i { background: var(--warn); }
|
|
54
|
+
#q { background: var(--card); border: 1px solid var(--border); color: var(--text); border-radius: 3px; padding: 6px 10px; font: 12px/1.4 var(--mono); width: 200px; transition: border-color .15s ease-out; }
|
|
55
|
+
#q::placeholder { color: var(--faint); }
|
|
56
|
+
#q:focus { outline: none; border-color: var(--border-strong); }
|
|
57
|
+
|
|
58
|
+
/* ---- 看板:去列盒子,发丝竖线分栏的编辑式构图 ---- */
|
|
59
|
+
#board { display: flex; overflow-x: auto; height: calc(100vh - 58px); }
|
|
60
|
+
.col { flex: 1 1 0; min-width: 240px; display: flex; flex-direction: column; border-left: 1px solid var(--border); }
|
|
61
|
+
.col:first-child { border-left: none; }
|
|
62
|
+
.col h2 { display: flex; align-items: baseline; gap: 8px; font: 600 11px/1 var(--mono); letter-spacing: .12em; color: var(--muted); padding: 16px 16px 12px; border-bottom: 1px solid var(--border); }
|
|
63
|
+
.col h2 b { color: var(--text); font-weight: 600; }
|
|
64
|
+
.col .list { flex: 1; overflow-y: auto; padding: 14px 14px 20px; display: flex; flex-direction: column; gap: 10px; }
|
|
65
|
+
|
|
66
|
+
/* ---- 卡片:近白纸片,色彩只出现在左缘 3px 与小徽标 ---- */
|
|
67
|
+
.card { background: var(--card); border: 1px solid var(--border); border-left: 3px solid var(--border-strong); border-radius: 4px; padding: 12px 14px; cursor: pointer; transition: background .15s ease-out, border-color .15s ease-out, transform .15s ease-out, box-shadow .15s ease-out; }
|
|
68
|
+
.card:hover { background: var(--card-hover); border-color: var(--border-strong); transform: translateY(-1px); box-shadow: var(--shadow-1); }
|
|
69
|
+
.row { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin-bottom: 7px; }
|
|
70
|
+
.type { font: 700 9.5px/1 var(--mono); letter-spacing: .06em; color: var(--card); border-radius: 2px; padding: 3px 5px; }
|
|
71
|
+
.type.mini { font-size: 9px; padding: 2px 4px; }
|
|
72
|
+
.lv { font: 700 10px/1 var(--mono); border: 1px solid var(--border-strong); color: var(--muted); border-radius: 2px; padding: 2px 4px; }
|
|
32
73
|
.lv.hi { border-color: var(--warn); color: var(--warn); }
|
|
33
|
-
.date { font
|
|
34
|
-
.acpt { font
|
|
74
|
+
.date { font: 11px/1 var(--mono); color: var(--muted); margin-left: auto; font-variant-numeric: tabular-nums; }
|
|
75
|
+
.acpt { font: 600 10px/1 var(--mono); border: 1px solid var(--border-strong); color: var(--muted); border-radius: 2px; padding: 2px 4px; font-variant-numeric: tabular-nums; }
|
|
35
76
|
.acpt.full { color: var(--ok); border-color: var(--ok); }
|
|
36
|
-
.age { font
|
|
77
|
+
.age { font: 700 10px/1 var(--mono); border-radius: 2px; padding: 2px 4px; }
|
|
37
78
|
.age.a1 { color: var(--warn); border: 1px solid var(--warn); }
|
|
38
|
-
.age.a2 { color:
|
|
79
|
+
.age.a2 { color: var(--card); background: var(--accent); border: 1px solid var(--accent); }
|
|
39
80
|
.card.age1 { outline: 1px solid var(--warn); outline-offset: -1px; }
|
|
40
|
-
.card.age2 { outline: 1px solid var(--
|
|
41
|
-
.alrt {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
.
|
|
81
|
+
.card.age2 { outline: 1px solid var(--accent); outline-offset: -1px; }
|
|
82
|
+
.alrt { font: 700 11px/1 var(--mono); color: var(--accent); cursor: help; }
|
|
83
|
+
.hold { font: 700 10px/1 var(--mono); color: var(--accent); border: 1px solid var(--accent); border-radius: 2px; padding: 1px 5px; margin-right: 6px; }
|
|
84
|
+
.auth { font: 10.5px/1 var(--mono); color: var(--muted); background: var(--surface); border: 1px solid var(--border); border-radius: 2px; padding: 2px 5px; }
|
|
85
|
+
.card .title { font-size: 13px; line-height: 1.6; }
|
|
45
86
|
.ct { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; vertical-align: 1px; }
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
.
|
|
49
|
-
/* 任务卡:作者 + 展开的文档清单 */
|
|
50
|
-
.auth { font-size: 11px; color: var(--dim); background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 0 7px; }
|
|
51
|
-
.type.mini { font-size: 9px; padding: 0 4px; }
|
|
52
|
-
.docs { display: none; border-top: 1px dashed var(--border); margin-top: 6px; padding-top: 4px; flex-direction: column; gap: 2px; }
|
|
87
|
+
|
|
88
|
+
/* ---- 任务卡展开区:同族文档清单 ---- */
|
|
89
|
+
.docs { display: none; border-top: 1px dashed var(--border); margin-top: 10px; padding-top: 8px; flex-direction: column; gap: 4px; }
|
|
53
90
|
.card.open .docs { display: flex; }
|
|
54
|
-
.doc-item { display: flex; align-items: center; gap: 6px; padding:
|
|
55
|
-
.doc-item:hover { background: var(--
|
|
91
|
+
.doc-item { display: flex; align-items: center; gap: 6px; padding: 6px 7px; border-radius: 3px; cursor: pointer; font-size: 12px; transition: background .15s ease-out; }
|
|
92
|
+
.doc-item:hover { background: var(--bg); }
|
|
56
93
|
.doc-item .doc-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
57
|
-
.doc-item .chip { font
|
|
58
|
-
.empty { color: var(--
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
#drawer
|
|
62
|
-
#
|
|
63
|
-
#d-
|
|
64
|
-
#d-
|
|
65
|
-
#d-
|
|
66
|
-
#d-
|
|
67
|
-
#d-
|
|
68
|
-
#d-related
|
|
69
|
-
#d-
|
|
70
|
-
#d-
|
|
71
|
-
#d-tabs
|
|
72
|
-
#d-
|
|
73
|
-
#
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
.md
|
|
80
|
-
.md
|
|
81
|
-
.md
|
|
82
|
-
.md
|
|
83
|
-
|
|
94
|
+
.doc-item .chip { font: 500 10px/1 var(--mono); padding: 2px 5px; flex-shrink: 0; }
|
|
95
|
+
.empty { color: var(--muted); font: 11px/1 var(--mono); text-align: center; padding: 28px 0; }
|
|
96
|
+
|
|
97
|
+
/* ---- 抽屉:纸面 transform 右滑入(不触发布局) ---- */
|
|
98
|
+
#drawer { position: fixed; top: 0; right: 0; width: min(760px, 92vw); height: 100vh; background: var(--surface); border-left: 1px solid var(--border-strong); transform: translateX(calc(100% + 24px)); transition: transform .3s var(--ease); display: flex; flex-direction: column; box-shadow: var(--shadow-2); z-index: 10; }
|
|
99
|
+
#drawer.open { transform: none; }
|
|
100
|
+
#d-head { padding: 22px 26px 10px; }
|
|
101
|
+
#d-badges { display: flex; align-items: center; flex-wrap: wrap; gap: 7px; }
|
|
102
|
+
#d-title { font-size: 18px; font-weight: 700; line-height: 1.45; margin: 12px 0; }
|
|
103
|
+
#d-close { float: right; width: 32px; height: 32px; background: none; border: none; border-radius: 3px; color: var(--muted); font-family: inherit; font-size: 18px; line-height: 1; cursor: pointer; transition: background .15s ease-out, color .15s ease-out; }
|
|
104
|
+
#d-close:hover { color: var(--text); background: var(--bg); }
|
|
105
|
+
#d-related { display: flex; gap: 7px; flex-wrap: wrap; margin-bottom: 14px; }
|
|
106
|
+
#d-related a { font: 500 11px/1 var(--mono); color: var(--text); background: var(--card); border: 1px solid var(--border); border-radius: 3px; padding: 5px 9px; cursor: pointer; text-decoration: none; transition: border-color .15s ease-out, background .15s ease-out; }
|
|
107
|
+
#d-related a:hover { border-color: var(--border-strong); background: var(--card-hover); }
|
|
108
|
+
#d-tabs { display: flex; gap: 4px; padding: 0 26px; border-bottom: 1px solid var(--border); }
|
|
109
|
+
#d-tabs button { position: relative; background: none; border: none; color: var(--muted); font-family: inherit; font-size: 13px; letter-spacing: .02em; padding: 11px 14px; cursor: pointer; }
|
|
110
|
+
#d-tabs button::after { content: ""; position: absolute; left: 14px; right: 14px; bottom: -1px; height: 2px; background: var(--text); transform: scaleX(0); transition: transform .2s var(--ease); }
|
|
111
|
+
#d-tabs button.on { color: var(--text); font-weight: 600; }
|
|
112
|
+
#d-tabs button.on::after { transform: scaleX(1); }
|
|
113
|
+
#d-body { flex: 1; overflow-y: auto; padding: 26px 30px 56px; }
|
|
114
|
+
|
|
115
|
+
/* ---- md 渲染 ---- */
|
|
116
|
+
.md > :first-child { margin-top: 0; }
|
|
117
|
+
.md h1, .md h2, .md h3 { margin: 26px 0 10px; line-height: 1.4; letter-spacing: .01em; } .md h1 { font-size: 18px; } .md h2 { font-size: 15.5px; } .md h3 { font-size: 14px; }
|
|
118
|
+
.md p, .md ul, .md ol { margin: 10px 0; } .md li { margin: 5px 0; }
|
|
119
|
+
.md code { background: var(--bg); border-radius: 2px; padding: 1px 5px; font: 12px/1.4 var(--mono); }
|
|
120
|
+
.md pre { background: var(--card); border: 1px solid var(--border); border-radius: 4px; padding: 14px 16px; overflow-x: auto; margin: 12px 0; } .md pre code { background: none; padding: 0; }
|
|
121
|
+
.md table { border-collapse: collapse; margin: 12px 0; font-size: 12.5px; } .md th, .md td { border: 1px solid var(--border); padding: 6px 12px; } .md th { background: var(--bg); font-weight: 600; }
|
|
122
|
+
.md blockquote { border-left: 3px solid var(--border-strong); color: var(--muted); padding-left: 14px; margin: 12px 0; }
|
|
123
|
+
.md a { color: var(--intent); text-underline-offset: 3px; }
|
|
124
|
+
.md hr { border: none; border-top: 1px solid var(--border); margin: 20px 0; }
|
|
125
|
+
|
|
126
|
+
/* ---- 历史时间线 ---- */
|
|
84
127
|
.tl { position: relative; padding-left: 22px; }
|
|
85
128
|
.tl::before { content: ""; position: absolute; left: 6px; top: 6px; bottom: 6px; width: 1px; background: var(--border); }
|
|
86
|
-
.tl-item { position: relative; padding:
|
|
87
|
-
.tl-item::before { content: ""; position: absolute; left: -20px; top:
|
|
129
|
+
.tl-item { position: relative; padding: 6px 0 16px; }
|
|
130
|
+
.tl-item::before { content: ""; position: absolute; left: -20px; top: 10px; width: 8px; height: 8px; border-radius: 50%; background: var(--surface); border: 2px solid var(--border-strong); }
|
|
88
131
|
.tl-sub { color: var(--text); font-size: 13px; }
|
|
89
|
-
.tl-meta { color: var(--
|
|
90
|
-
.tl-meta code { cursor: pointer; } .tl-meta code:hover { color: var(--text); }
|
|
91
|
-
.none { color: var(--
|
|
132
|
+
.tl-meta { color: var(--muted); font: 12px/1.6 var(--mono); }
|
|
133
|
+
.tl-meta code { cursor: pointer; background: var(--card); border: 1px solid var(--border); border-radius: 2px; padding: 0 4px; } .tl-meta code:hover { color: var(--text); border-color: var(--border-strong); }
|
|
134
|
+
.none { color: var(--muted); font-size: 13px; }
|
|
135
|
+
|
|
136
|
+
/* ---- toast:墨底纸字,像盖下去的一方印 ---- */
|
|
137
|
+
#toast { position: fixed; top: 70px; right: 22px; background: var(--text); color: var(--bg); border-radius: 3px; padding: 9px 14px; font-size: 12px; z-index: 99; opacity: 0; transform: translateY(-6px); transition: opacity .2s ease-out, transform .2s ease-out; pointer-events: none; box-shadow: var(--shadow-1); }
|
|
138
|
+
#toast.show { opacity: 1; transform: none; }
|
|
139
|
+
|
|
140
|
+
/* ---- 动效:一次编排好的入场(列错峰 60ms),SSE 重渲染不重播 ---- */
|
|
141
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
142
|
+
header { animation: rise .4s var(--ease) both; }
|
|
143
|
+
#board.boot .col { animation: rise .45s var(--ease) both; }
|
|
144
|
+
#board.boot .col:nth-child(2) { animation-delay: 60ms; }
|
|
145
|
+
#board.boot .col:nth-child(3) { animation-delay: 120ms; }
|
|
146
|
+
#board.boot .col:nth-child(4) { animation-delay: 180ms; }
|
|
147
|
+
#board.boot .col:nth-child(5) { animation-delay: 240ms; }
|
|
148
|
+
#board.boot .col:nth-child(6) { animation-delay: 300ms; }
|
|
149
|
+
#sse.on i { animation: breath 2.4s ease-in-out infinite; }
|
|
150
|
+
}
|
|
151
|
+
@keyframes rise { from { opacity: 0; transform: translateY(10px); } }
|
|
152
|
+
@keyframes breath { 50% { opacity: .45; } }
|
|
153
|
+
@media (prefers-reduced-motion: reduce) {
|
|
154
|
+
*, *::before, *::after { transition: none !important; animation: none !important; }
|
|
155
|
+
}
|
|
92
156
|
</style>
|
|
93
157
|
</head>
|
|
94
158
|
<body>
|
|
@@ -376,7 +440,10 @@ setInterval(() => refreshBoard(), 60000);
|
|
|
376
440
|
// 搜索过滤:本地过滤标题/slug/文件,SSE 刷新时保持关键字
|
|
377
441
|
$('#q').addEventListener('input', (e) => { Q = e.target.value; refreshBoard(); });
|
|
378
442
|
|
|
443
|
+
// 首次入场:列错峰上浮一次(.boot 稍后摘除,SSE / 过滤触发的重渲染不再重播)
|
|
444
|
+
$('#board').classList.add('boot');
|
|
379
445
|
refreshBoard();
|
|
446
|
+
setTimeout(() => $('#board').classList.remove('boot'), 1500);
|
|
380
447
|
</script>
|
|
381
448
|
</body>
|
|
382
449
|
</html>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"fp":"b779761db958797b","files":{"/tmp/kb-search-test-
|
|
1
|
+
{"fp":"b779761db958797b","files":{"/tmp/kb-search-test-T8zWVC::workflow/intents/2026-09-21-bbb.md":{"st":[1790250003552.587,270],"kind":"wf","meta":{"状态":"approved","级别":"L1","日期":"2026-09-21","模块":"pipeline"},"title":"乙需求","lines":[[1,"(frontmatter)","---"],[2,"(frontmatter)","状态: approved"],[3,"(frontmatter)","级别: L1"],[4,"(frontmatter)","日期: 2026-09-21"],[5,"(frontmatter)","模块: pipeline"],[6,"(frontmatter)","---"],[7,"(标题)","# INTENT — 乙需求"],[9,"目标","## 目标"],[10,"目标","- 统一口径:编码模板与分类层级,冲突处理见 A/B乙"],[11,"目标",""]]},"/tmp/kb-search-test-T8zWVC::workflow/plans/2026-09-20-ccc.md":{"st":[1790250003552.587,200],"kind":"wf","meta":{"状态":"approved","级别":"L1","模块":"purchase"},"title":"丙计划","lines":[[1,"(frontmatter)","---"],[2,"(frontmatter)","状态: approved"],[3,"(frontmatter)","级别: L1"],[4,"(frontmatter)","模块: purchase"],[5,"(frontmatter)","---"],[6,"(标题)","# PLAN — 丙计划"],[8,"目标","## 目标"],[9,"目标","- 只讲编码模板,不提另一个词"],[10,"目标",""]]},"/tmp/kb-search-test-T8zWVC::workflow/incidents/2026-09-01-aaa.md":{"st":[1790250003552.587,270],"kind":"wf","meta":{"状态":"closed","级别":"L2","发现":"2026-09-01","模块":"material"},"title":"甲事故","lines":[[1,"(frontmatter)","---"],[2,"(frontmatter)","状态: closed"],[3,"(frontmatter)","级别: L2"],[4,"(frontmatter)","发现: 2026-09-01"],[5,"(frontmatter)","模块: material"],[6,"(frontmatter)","---"],[7,"(标题)","# INCIDENT — 甲事故"],[9,"目标","## 目标"],[10,"目标","- 统一口径:编码模板与分类层级,冲突处理见 A/B甲"],[11,"目标",""]]},"/tmp/kb-search-test-T8zWVC::wiki/测试主题/说明.md":{"st":[1790250003552.587,47],"kind":"wiki","lines":[[1,"# 说明"],[2,""],[3,"编码模板的领域知识条目。"],[4,""]]}}}
|
|
@@ -72,6 +72,7 @@ node .agents/scripts/verify-wiki-consistency.mjs # 触及 wiki/ 时跑(exit 1
|
|
|
72
72
|
- 输出测试报告(通过项 / 失败项)
|
|
73
73
|
- 失败项已修复并复测通过
|
|
74
74
|
- **关单**(日常闭环点,不依赖是否上 prod):
|
|
75
|
+
- 关单前一键过门:`node .agents/scripts/verify.mjs`(npm test + check-loop 固定编排,任一失败非零退出,非绿不关单)
|
|
75
76
|
- 新需求:逐条勾验入口 intent「验收标准」,每条补「证据:」,intent → done;同名 spec(若有)仍 approved → done
|
|
76
77
|
- 修复:防复发验证已落地,incident → closed
|
|
77
78
|
- **同族收尾**:入口置终态时,同名 plan 一并置 `done`(spec 见上条),不留 `approved` 孤儿(口径同看板「入口已 done,本 plan 未终态」告警)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// verify.mjs — 关单固定编排(2026-09-24 intent closeout-verify-script)
|
|
3
|
+
// 用途:关单前一键过门——按写死顺序执行 npm test → check-loop.sh,逐项输出 pass/fail。
|
|
4
|
+
// 语义:只编排不裁决——两条命令都是既有门禁原样跑,本脚本不新增校验、不读配置、无状态;
|
|
5
|
+
// 任一步非零即 fail-fast 退出 1,失败原因经 stdio inherit 原样透传不吞;
|
|
6
|
+
// 缺 sh 环境 fail-closed 明确报错(关单门不可跳过,不像 run-tests 那样跳过并提示)。
|
|
7
|
+
// 测试:node templates/_agents/scripts/verify.test.mjs(--test-cmd 注入假命令 + CHECK_LOOP_ROOT 注入夹具,不触真实 npm test 与 workflow)
|
|
8
|
+
// 用法:node .agents/scripts/verify.mjs [--test-cmd "<shell 命令>"](--test-cmd 仅供测试注入)
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
|
|
13
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const args = process.argv.slice(2);
|
|
15
|
+
const testCmd = args.includes('--test-cmd') ? args[args.indexOf('--test-cmd') + 1] : 'npm test';
|
|
16
|
+
const checkLoop = path.join(SCRIPT_DIR, 'check-loop.sh');
|
|
17
|
+
|
|
18
|
+
console.log(`[verify] ▶ 1/2 ${testCmd}`);
|
|
19
|
+
const t = spawnSync(testCmd, { shell: true, stdio: 'inherit' });
|
|
20
|
+
if (t.status !== 0) {
|
|
21
|
+
console.error(`[verify] ❌ 1/2 ${testCmd} 未通过(exit ${t.status ?? t.error?.code})——修复后重跑,非绿不关单`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
console.log('[verify] ✅ 1/2 测试通过');
|
|
25
|
+
|
|
26
|
+
console.log('[verify] ▶ 2/2 check-loop');
|
|
27
|
+
const c = spawnSync('sh', [checkLoop], { stdio: 'inherit' });
|
|
28
|
+
if (c.status !== 0) {
|
|
29
|
+
const hint = c.error?.code === 'ENOENT'
|
|
30
|
+
? '环境无 sh(装 Git Bash / WSL)——关单门不可跳过,不静默放行'
|
|
31
|
+
: '按上方输出修复后原路重跑';
|
|
32
|
+
console.error(`[verify] ❌ 2/2 check-loop 未通过(exit ${c.status ?? c.error?.code})——${hint}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
console.log('[verify] ✅ 2/2 闭环校验通过');
|
|
36
|
+
console.log('[verify] ✅ 全绿——可以关单(逐条勾验验收标准补证据后 intent/plan → done)');
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// verify.mjs 的 fixture 测试(--test-cmd 注入假命令 + CHECK_LOOP_ROOT 注入夹具根,不触真实 npm test 与 workflow)
|
|
3
|
+
// 断言:①绿路径(假过 + 空夹具闭环)exit 0;②步骤 1 假败 exit 1 且不出全绿;
|
|
4
|
+
// ③步骤 2 配对断裂夹具 exit 1(verify 自身标记断言——孙进程 stdio inherit 输出不进本测试管道)。
|
|
5
|
+
// 用法:node templates/_agents/scripts/verify.test.mjs
|
|
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
|
+
|
|
12
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const VERIFY = path.join(SCRIPT_DIR, 'verify.mjs');
|
|
14
|
+
|
|
15
|
+
let pass = 0;
|
|
16
|
+
let fail = 0;
|
|
17
|
+
const check = (desc, cond, detail = '') => {
|
|
18
|
+
if (cond) { pass++; console.log(`PASS ${desc}`); }
|
|
19
|
+
else { fail++; console.log(`FAIL ${desc}${detail ? `\n${detail}` : ''}`); }
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// 空闭环夹具(四目录齐全、无文档 → check-loop 无断档)
|
|
23
|
+
const mkfix = () => {
|
|
24
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'verify-fix-'));
|
|
25
|
+
for (const s of ['intents', 'specs', 'plans', 'incidents']) fs.mkdirSync(path.join(d, 'workflow', s), { recursive: true });
|
|
26
|
+
return d;
|
|
27
|
+
};
|
|
28
|
+
// 配对断裂夹具(L1 intent 无同名 plan → hard-block)
|
|
29
|
+
const mkBroken = () => {
|
|
30
|
+
const d = mkfix();
|
|
31
|
+
fs.writeFileSync(path.join(d, 'workflow', 'intents', '2026-09-12-lonely.md'), [
|
|
32
|
+
'---', '状态: done', '级别: L1', '日期: 2026-09-12', '---', '# INTENT — lonely', '',
|
|
33
|
+
'## 验收标准(可测试)', '- [x] 用例通过(证据:dotnet test 全绿)', '',
|
|
34
|
+
].join('\n'), 'utf8');
|
|
35
|
+
return d;
|
|
36
|
+
};
|
|
37
|
+
// 假命令(绝对路径直调,不依赖 PATH,跨平台无引号问题)
|
|
38
|
+
const mkCmd = (code) => {
|
|
39
|
+
const f = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'verify-cmd-')), `exit${code}.mjs`);
|
|
40
|
+
fs.writeFileSync(f, `process.exit(${code});`, 'utf8');
|
|
41
|
+
return f;
|
|
42
|
+
};
|
|
43
|
+
const run = (argv, opts = {}) => spawnSync(process.execPath, [VERIFY, ...argv], { encoding: 'utf8', ...opts });
|
|
44
|
+
|
|
45
|
+
// ---- 场景 1:绿路径——假过 + 空夹具闭环 → exit 0 ----
|
|
46
|
+
{
|
|
47
|
+
const root = mkfix();
|
|
48
|
+
const r = run(['--test-cmd', `node ${mkCmd(0)}`], { env: { ...process.env, CHECK_LOOP_ROOT: root } });
|
|
49
|
+
check('场景 1:exit 0', r.status === 0, `exit=${r.status}\n${r.stdout}\n${r.stderr}`);
|
|
50
|
+
check('场景 1:两步全过且输出全绿', r.stdout.includes('✅ 1/2') && r.stdout.includes('✅ 2/2') && r.stdout.includes('全绿'), r.stdout);
|
|
51
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---- 场景 2:红路径步骤 1——假败 → exit 1、fail-fast 不出全绿 ----
|
|
55
|
+
{
|
|
56
|
+
const root = mkfix();
|
|
57
|
+
const r = run(['--test-cmd', `node ${mkCmd(3)}`], { env: { ...process.env, CHECK_LOOP_ROOT: root } });
|
|
58
|
+
check('场景 2:exit 1', r.status === 1, `exit=${r.status}\n${r.stdout}\n${r.stderr}`);
|
|
59
|
+
check('场景 2:步骤 1 失败标记在 stderr,且不输出全绿', r.stderr.includes('❌ 1/2') && !r.stdout.includes('全绿'), `${r.stdout}\n${r.stderr}`);
|
|
60
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---- 场景 3:红路径步骤 2——配对断裂夹具 → exit 1 ----
|
|
64
|
+
{
|
|
65
|
+
const root = mkBroken();
|
|
66
|
+
const r = run(['--test-cmd', `node ${mkCmd(0)}`], { env: { ...process.env, CHECK_LOOP_ROOT: root } });
|
|
67
|
+
check('场景 3:exit 1', r.status === 1, `exit=${r.status}\n${r.stdout}\n${r.stderr}`);
|
|
68
|
+
check('场景 3:步骤 2 失败标记在 stderr,且不输出全绿', r.stderr.includes('❌ 2/2') && !r.stdout.includes('全绿'), `${r.stdout}\n${r.stderr}`);
|
|
69
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
73
|
+
if (fail) {
|
|
74
|
+
console.log('\n用法:node templates/_agents/scripts/verify.test.mjs');
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
process.exit(0);
|
package/templates/wiki/INDEX.md
CHANGED
|
@@ -9,8 +9,15 @@
|
|
|
9
9
|
|
|
10
10
|
| 主题 | 文件数 | 用途 |
|
|
11
11
|
|---|---|---|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
| 产品需求 | 1 | 需求文档与变更记录(PRD、需求说明、评审结论) |
|
|
13
|
+
| 用户功能 | 1 | 功能清单与操作说明(功能视角;与产品需求的分工:那里记"为什么做",这里记"怎么用") |
|
|
14
|
+
| 开发方案 | 1 | 技术方案与设计文档(架构、接口、关键取舍) |
|
|
15
|
+
| 项目规范 | 1 | 编码 / 流程 / 协作规范(只放现行有效版本) |
|
|
16
|
+
| 项目计划 | 1 | 计划、里程碑与会议纪要(按日期命名) |
|
|
17
|
+
| 测试报告 | 1 | 测试报告与验收记录(按 日期-主题 命名) |
|
|
18
|
+
| 数据维护 | 1 | schema 变更与数据修复台账(子目录按批次组织,批内 README 记账,不参与文件级登记) |
|
|
19
|
+
|
|
20
|
+
合计:**7 份**知识文档,**7 个主题**
|
|
14
21
|
|
|
15
22
|
## 文件 → 主题 → 归属目录 映射表
|
|
16
23
|
|