agentic-flow-kit 2.0.0 → 2.1.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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +92 -23
  3. package/bin/flow-kit.mjs +3 -1
  4. package/locales/en/installer.json +87 -2
  5. package/locales/en/overrides/.codebuddy/skills/tfs-pr/SKILL.md +1 -1
  6. package/locales/en/overrides/workflow/README.md +2 -0
  7. package/locales/zh-CN/installer.json +87 -2
  8. package/package.json +2 -2
  9. package/schemas/install-manifest.schema.json +3 -1
  10. package/schemas/project-profile.schema.json +1 -1
  11. package/src/cli/args.mjs +3 -1
  12. package/src/cli/main.mjs +9 -2
  13. package/src/commands/doctor.mjs +50 -4
  14. package/src/commands/install.mjs +160 -13
  15. package/src/hosts/registry.mjs +154 -10
  16. package/src/i18n.mjs +3 -0
  17. package/src/install/apply.mjs +17 -3
  18. package/src/install/frontmatter.mjs +84 -0
  19. package/src/install/plan.mjs +174 -16
  20. package/src/models/known.mjs +13 -0
  21. package/src/models/probe.mjs +163 -0
  22. package/src/models/resolve.mjs +312 -0
  23. package/src/render/tokens.mjs +56 -9
  24. package/src/tui/ansi.mjs +55 -0
  25. package/src/tui/flows.mjs +306 -0
  26. package/src/tui/prompts.mjs +139 -0
  27. package/src/tui/screen.mjs +217 -0
  28. package/src/tui/widgets.mjs +111 -0
  29. package/src/tui/wizard.mjs +68 -0
  30. package/templates/engine/.agents/commands/new-task.md +2 -2
  31. package/templates/engine/.agents/commands/pipeline-run.md +5 -1
  32. package/templates/engine/.agents/commands/stages.md +2 -1
  33. package/templates/engine/.agents/config/agent-eval.json +17 -0
  34. package/templates/engine/.agents/config/mechanism-registry.md +2 -1
  35. package/templates/engine/.agents/config/memory-tiers.json +13 -0
  36. package/templates/engine/.agents/config/pipeline.json +36 -0
  37. package/templates/engine/.agents/config/steering.json +28 -0
  38. package/templates/engine/.agents/evals/README.md +49 -0
  39. package/templates/engine/.agents/evals/tasks.tsv +6 -0
  40. package/templates/engine/.agents/hooks/pre-write-check.cjs +27 -0
  41. package/templates/engine/.agents/notes/injected-memory.md +14 -0
  42. package/templates/engine/.agents/notes/steering-tiers.md +28 -0
  43. package/templates/engine/.agents/scripts/agent-eval.mjs +268 -0
  44. package/templates/engine/.agents/scripts/check-pipeline-state.mjs +85 -0
  45. package/templates/engine/.agents/scripts/claims.mjs +166 -0
  46. package/templates/engine/.agents/scripts/dream-run.mjs +78 -3
  47. package/templates/engine/.agents/scripts/fix-task.mjs +77 -0
  48. package/templates/engine/.agents/scripts/memory-tier.mjs +196 -0
  49. package/templates/engine/.agents/scripts/memory-wiki.mjs +296 -0
  50. package/templates/engine/.agents/scripts/outcome-metrics.mjs +195 -0
  51. package/templates/engine/.agents/scripts/path-lease.mjs +177 -0
  52. package/templates/engine/.agents/scripts/session-log.mjs +157 -0
  53. package/templates/engine/.agents/scripts/steering.mjs +75 -0
  54. package/templates/engine/.agents/scripts/verify.mjs +16 -5
  55. package/templates/engine/.agents/skills/arena/SKILL.md +4 -0
  56. package/templates/engine/.agents/skills/dream/SKILL.md +21 -2
  57. package/templates/engine/.agents/skills/eval/SKILL.md +16 -0
  58. package/templates/engine/.agents/skills/model-upgrade/SKILL.md +41 -0
  59. package/templates/engine/.agents/skills/session-resume/SKILL.md +7 -0
  60. package/templates/engine/.agents/skills/show-me-your-work/SKILL.md +14 -0
  61. package/templates/engine/.agents/skills/wiki/SKILL.md +4 -1
  62. package/templates/git/.gitattributes +1 -0
  63. package/templates/git/.githooks/package.json +3 -0
  64. package/templates/git/.github/workflows/ci.yml +32 -0
  65. package/templates/git/azure-pipelines.yml +46 -0
  66. package/templates/host-meta/SKILLS.md +3 -0
  67. package/templates/host-meta/claude/skill-pointer.md +6 -0
  68. package/templates/host-meta/cline/skill-index.md +6 -0
  69. package/templates/host-meta/codebuddy/skill-pointer.md +6 -0
  70. package/templates/host-meta/commandcode/skill-index.md +5 -0
  71. package/templates/host-meta/opencode/skill-pointer.md +6 -0
  72. package/templates/host-meta/qoder/skill-index.md +5 -0
  73. package/templates/host-meta/qwen/skill-index.md +5 -0
  74. package/templates/host-meta/trae/skill-index.md +6 -0
  75. package/templates/hosts/claude/settings.hardened.json +94 -0
  76. package/templates/hosts/claude/settings.json +1 -3
  77. package/templates/hosts/commandcode/settings.json +1 -3
  78. package/templates/hosts/qoder/settings.json +1 -3
  79. package/templates/hosts/qwen/settings.json +1 -3
  80. package/templates/profiles/go/AGENTS.md +52 -0
  81. package/templates/profiles/java/AGENTS.md +52 -0
  82. package/templates/profiles/rust/AGENTS.md +52 -0
  83. package/templates/profiles/typescript/AGENTS.md +52 -0
  84. package/templates/scaffold/workflow/INDEX.md +16 -0
  85. package/templates/scaffold/workflow/README.md +2 -0
  86. package/templates/scaffold/workflow/artifact-sources.md +25 -0
  87. package/templates/scaffold/workflow/non-engineer-intent.md +51 -0
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ // fix-task.mjs — 修复任务窗口标记(配合 pre-write-check 的“测试不可弱化”层)
3
+ //
4
+ // 修复开始时 start,结束时 stop。窗口内修改既有测试会被写前门拒绝;
5
+ // 新建复现测试仍然允许(先写失败用例,再修实现)。
6
+ //
7
+ // 用法:
8
+ // node .agents/scripts/fix-task.mjs start [--id <id>] [--allow path1,path2]
9
+ // node .agents/scripts/fix-task.mjs status
10
+ // node .agents/scripts/fix-task.mjs stop
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+
14
+ export function markerFile(root) {
15
+ return path.join(root, '.agents', 'cache', 'fix-task.json');
16
+ }
17
+
18
+ export function startFixTask(root, { id = '', allowedTests = [], now = new Date() } = {}) {
19
+ const file = markerFile(root);
20
+ fs.mkdirSync(path.dirname(file), { recursive: true });
21
+ const row = {
22
+ active: true,
23
+ id: String(id || `fix-${now.toISOString().slice(0, 19).replace(/[:T]/g, '')}`),
24
+ startedAt: now.toISOString(),
25
+ allowedTests: allowedTests.map((p) => String(p).replace(/\\/g, '/')),
26
+ };
27
+ fs.writeFileSync(file, JSON.stringify(row, null, 2), 'utf8');
28
+ return row;
29
+ }
30
+
31
+ export function readFixTask(root) {
32
+ try {
33
+ return JSON.parse(fs.readFileSync(markerFile(root), 'utf8'));
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ export function stopFixTask(root) {
40
+ const file = markerFile(root);
41
+ const existed = fs.existsSync(file);
42
+ fs.rmSync(file, { force: true });
43
+ return { existed };
44
+ }
45
+
46
+ function argOf(argv, name, fallback = null) {
47
+ const i = argv.indexOf(`--${name}`);
48
+ if (i < 0) return fallback;
49
+ const v = argv[i + 1];
50
+ return v == null || v.startsWith('--') ? true : v;
51
+ }
52
+
53
+ function main() {
54
+ const argv = process.argv.slice(2);
55
+ const cmd = argv[0] || 'status';
56
+ const root = path.resolve(argOf(argv, 'root', process.cwd()));
57
+ if (cmd === 'start') {
58
+ const allow = argOf(argv, 'allow', '');
59
+ const row = startFixTask(root, {
60
+ id: argOf(argv, 'id', ''),
61
+ allowedTests: allow && allow !== true ? String(allow).split(',') : [],
62
+ });
63
+ console.log(`fix-task 窗口已开启:${row.id}${row.allowedTests.length ? `(允许测试:${row.allowedTests.join(', ')})` : ''}`);
64
+ return 0;
65
+ }
66
+ if (cmd === 'stop') {
67
+ const res = stopFixTask(root);
68
+ console.log(res.existed ? 'fix-task 窗口已关闭' : 'fix-task 无活动窗口');
69
+ return 0;
70
+ }
71
+ const row = readFixTask(root);
72
+ console.log(row ? `active ${row.id} since ${row.startedAt}` : 'inactive');
73
+ return row ? 0 : 1;
74
+ }
75
+
76
+ const isMain = process.argv[1] && process.argv[1].endsWith('fix-task.mjs');
77
+ if (isMain) process.exit(main());
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env node
2
+ // memory-tier.mjs — 注入层预算 + 记忆升降候选
3
+ //
4
+ // 背景(Anthropic《The components of agent memory》):
5
+ // - 顶层记忆会被注入每个会话,必须小;Claude Code 只加载 MEMORY.md 前 200 行 / 25KB。
6
+ // - dreaming 的职责之一是“把细节下沉、把该上浮的规则上浮”,而不是持续变大。
7
+ //
8
+ // 本脚本:
9
+ // audit 检查注入层是否超行/字节预算(超限非零)
10
+ // suggest 从错题/会话经验/主张里找重复模式,输出“可考虑上浮”的候选(只读)
11
+ // budget 打印当前用量
12
+ //
13
+ // 用法:
14
+ // node .agents/scripts/memory-tier.mjs audit
15
+ // node .agents/scripts/memory-tier.mjs suggest [--days 90] [--min 3]
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+
19
+ export const DEFAULT_CONFIG = {
20
+ version: 1,
21
+ injected: { file: '.agents/notes/injected-memory.md', maxLines: 200, maxBytes: 25600 },
22
+ promotion: { candidates: '.agents/cache/memory-promotions.jsonl', minOccurrences: 3, outputDir: 'tmp' },
23
+ };
24
+
25
+ export function loadConfig(root) {
26
+ const file = path.join(root, '.agents', 'config', 'memory-tiers.json');
27
+ try {
28
+ return { ...DEFAULT_CONFIG, ...JSON.parse(fs.readFileSync(file, 'utf8')) };
29
+ } catch {
30
+ return DEFAULT_CONFIG;
31
+ }
32
+ }
33
+
34
+ export function auditBudget(root, cfg = loadConfig(root)) {
35
+ const rel = cfg.injected.file;
36
+ const abs = path.join(root, rel);
37
+ if (!fs.existsSync(abs)) return { ok: false, reason: 'missing', rel, lines: 0, bytes: 0, cfg };
38
+ const text = fs.readFileSync(abs, 'utf8');
39
+ const lines = text.split(/\r?\n/).length;
40
+ // 字节口径与 rule-budget 一致:LF 归一,避免 CRLF 虚高。
41
+ const bytes = Buffer.byteLength(text.replace(/\r\n/g, '\n'), 'utf8');
42
+ const overLines = lines > cfg.injected.maxLines;
43
+ const overBytes = bytes > cfg.injected.maxBytes;
44
+ return {
45
+ ok: !overLines && !overBytes,
46
+ reason: overLines ? 'over-lines' : overBytes ? 'over-bytes' : 'ok',
47
+ rel,
48
+ lines,
49
+ bytes,
50
+ maxLines: cfg.injected.maxLines,
51
+ maxBytes: cfg.injected.maxBytes,
52
+ cfg,
53
+ };
54
+ }
55
+
56
+ function readJsonl(dir, re) {
57
+ const rows = [];
58
+ let names = [];
59
+ try {
60
+ names = fs.readdirSync(dir).filter((n) => re.test(n)).sort();
61
+ } catch {
62
+ return rows;
63
+ }
64
+ for (const name of names) {
65
+ let raw = '';
66
+ try {
67
+ raw = fs.readFileSync(path.join(dir, name), 'utf8');
68
+ } catch {
69
+ continue;
70
+ }
71
+ for (const line of raw.split(/\r?\n/)) {
72
+ if (!line.trim()) continue;
73
+ try {
74
+ rows.push(JSON.parse(line));
75
+ } catch {
76
+ /* ignore */
77
+ }
78
+ }
79
+ }
80
+ return rows;
81
+ }
82
+
83
+ function norm(s) {
84
+ return String(s || '')
85
+ .toLowerCase()
86
+ .replace(/[`"'“”‘’]/g, '')
87
+ .replace(/\s+/g, ' ')
88
+ .trim();
89
+ }
90
+
91
+ // 候选来源:会话失败/纠正、错题 tag、主张中的重复主题。
92
+ export function suggestPromotions(root, { days = 90, min = null, now = new Date() } = {}) {
93
+ const cfg = loadConfig(root);
94
+ const floor = Number(min) || cfg.promotion.minOccurrences;
95
+ const cutoff = now.getTime() - days * 86400000;
96
+ const buckets = new Map();
97
+ const add = (key, source, sample) => {
98
+ const k = norm(key);
99
+ if (!k || k.length < 4) return;
100
+ if (!buckets.has(k)) buckets.set(k, { key: k, count: 0, sources: new Set(), samples: [] });
101
+ const b = buckets.get(k);
102
+ b.count += 1;
103
+ b.sources.add(source);
104
+ if (sample && b.samples.length < 3) b.samples.push(sample);
105
+ };
106
+
107
+ const sessions = readJsonl(path.join(root, '.agents', 'cache', 'session-logs'), /^\d{4}-\d{2}-\d{2}\.jsonl$/)
108
+ .filter((e) => ['failure', 'correction', 'refusal'].includes(e.type))
109
+ .filter((e) => !e.ts || Date.parse(e.ts) >= cutoff);
110
+ for (const e of sessions) add(e.tag || e.text, 'session-log', e.text);
111
+
112
+ const papercuts = readJsonl(path.join(root, 'workflow', 'papercuts.d'), /\.(jsonl|json)$/)
113
+ .filter((e) => !e.ts || Date.parse(e.ts) >= cutoff);
114
+ for (const e of papercuts) add(e.tag || e.kind || e.text, 'papercut', e.text);
115
+
116
+ const claims = readJsonl(path.join(root, 'workflow', 'claims.d'), /^\d{4}-\d{2}\.jsonl$/)
117
+ .filter((e) => !e.ts || Date.parse(e.ts) >= cutoff);
118
+ for (const e of claims) {
119
+ const topic = String(e.path || '').split(/[\\/]/)[0];
120
+ add(topic || e.claim, 'claim', e.claim);
121
+ }
122
+
123
+ return [...buckets.values()]
124
+ .filter((b) => b.count >= floor)
125
+ .sort((a, b) => b.count - a.count || a.key.localeCompare(b.key))
126
+ .map((b) => ({ key: b.key, count: b.count, sources: [...b.sources].sort(), samples: b.samples }));
127
+ }
128
+
129
+ export function writeSuggestion(root, candidates, { days = 90, cfg = loadConfig(root), now = new Date() } = {}) {
130
+ const dir = path.join(root, cfg.promotion.outputDir);
131
+ fs.mkdirSync(dir, { recursive: true });
132
+ const file = path.join(dir, `memory-tier-suggest-${now.toISOString().slice(0, 10)}.md`);
133
+ const lines = [
134
+ `# 记忆上浮候选(近 ${days} 天)`,
135
+ '',
136
+ '> 只出候选,不自动改写注入层。裁决后手工改 `.agents/notes/injected-memory.md`,',
137
+ '> 并保持 200 行 / 25KB 预算;下沉详情用链接。',
138
+ '',
139
+ ];
140
+ if (!candidates.length) lines.push('_无达到阈值的重复模式。_');
141
+ else {
142
+ lines.push('| 候选 | 次数 | 来源 | 样本 |');
143
+ lines.push('|---|---|---|---|');
144
+ for (const c of candidates) {
145
+ lines.push(`| ${c.key.replace(/\|/g, '\\|')} | ${c.count} | ${c.sources.join(', ')} | ${c.samples[0]?.replace(/\|/g, '\\|') || ''} |`);
146
+ }
147
+ }
148
+ lines.push('');
149
+ fs.writeFileSync(file, `${lines.join('\n')}\n`, 'utf8');
150
+ return file;
151
+ }
152
+
153
+ function argOf(argv, name, fallback = null) {
154
+ const i = argv.indexOf(`--${name}`);
155
+ if (i < 0) return fallback;
156
+ const v = argv[i + 1];
157
+ return v == null || v.startsWith('--') ? true : v;
158
+ }
159
+
160
+ function main() {
161
+ const argv = process.argv.slice(2);
162
+ const cmd = argv[0] || 'audit';
163
+ const root = path.resolve(argOf(argv, 'root', process.cwd()));
164
+ const cfg = loadConfig(root);
165
+
166
+ if (cmd === 'audit') {
167
+ const res = auditBudget(root, cfg);
168
+ if (!res.ok) {
169
+ console.error(`✗ 注入层 ${res.reason}: ${res.rel} (${res.lines}/${cfg.injected.maxLines} 行, ${res.bytes}/${cfg.injected.maxBytes} 字节)`);
170
+ return 1;
171
+ }
172
+ console.log(`✅ 注入层预算: ${res.lines}/${cfg.injected.maxLines} 行, ${res.bytes}/${cfg.injected.maxBytes} 字节`);
173
+ return 0;
174
+ }
175
+
176
+ if (cmd === 'suggest') {
177
+ const days = Number(argOf(argv, 'days', 90)) || 90;
178
+ const min = Number(argOf(argv, 'min', 0)) || null;
179
+ const candidates = suggestPromotions(root, { days, min });
180
+ const file = writeSuggestion(root, candidates, { days, cfg });
181
+ console.log(`${file}(${candidates.length} 个候选)`);
182
+ return 0;
183
+ }
184
+
185
+ if (cmd === 'budget') {
186
+ const res = auditBudget(root, cfg);
187
+ console.log(`lines=${res.lines} bytes=${res.bytes} maxLines=${cfg.injected.maxLines} maxBytes=${cfg.injected.maxBytes} ok=${res.ok}`);
188
+ return res.ok ? 0 : 1;
189
+ }
190
+
191
+ console.error('用法: memory-tier.mjs audit | suggest [--days N] [--min N] | budget');
192
+ return 2;
193
+ }
194
+
195
+ const isMain = process.argv[1] && process.argv[1].endsWith('memory-tier.mjs');
196
+ if (isMain) process.exit(main());
@@ -0,0 +1,296 @@
1
+ #!/usr/bin/env node
2
+ // memory-wiki.mjs — 把“记忆”编译成可导航的表示(wiki)
3
+ //
4
+ // 背景(Anthropic《The components of agent memory》):wiki 不是文档归档,
5
+ // 而是 dreaming 增量构建的“记忆地图”——页面 + 链接,让智能体能遍历自己知道什么;
6
+ // 检索与 wiki 组合通常优于单独检索。
7
+ //
8
+ // 输入(存在即用):
9
+ // workflow/claims.d/*.jsonl 主张账(未来会话可能当真的话 + 原文出处)
10
+ // workflow/incidents/*.md 事故复盘
11
+ // workflow/papercuts.d/*.md 错题/纸割登记
12
+ // .agents/cache/session-logs/*.jsonl 会话经验(failure/correction/refusal)
13
+ //
14
+ // 输出:wiki/_memory/INDEX.md + 每个主题一页(主张/事故/经验),带来源链接。
15
+ // 语义:
16
+ // - 只写 wiki/_memory/**,不碰人工 wiki;
17
+ // - 幂等:内容未变不写;--check 发现漂移即非零;
18
+ // - 不自动 commit;不把 claims 的“原文还在”当成“主张为真”。
19
+ //
20
+ // 用法:
21
+ // node .agents/scripts/memory-wiki.mjs # 生成/更新
22
+ // node .agents/scripts/memory-wiki.mjs --check # 只检查是否最新
23
+ // node .agents/scripts/memory-wiki.mjs --dry-run # 只打印将写文件
24
+ import fs from 'node:fs';
25
+ import path from 'node:path';
26
+ import { recheckClaims } from './claims.mjs';
27
+
28
+ const OUT_DIR = path.join('wiki', '_memory');
29
+
30
+ function readText(file) {
31
+ try {
32
+ return fs.readFileSync(file, 'utf8');
33
+ } catch {
34
+ return '';
35
+ }
36
+ }
37
+
38
+ function listFiles(dir, re) {
39
+ try {
40
+ return fs.readdirSync(dir).filter((n) => re.test(n)).sort();
41
+ } catch {
42
+ return [];
43
+ }
44
+ }
45
+
46
+ function slugify(s) {
47
+ return String(s || 'general')
48
+ .trim()
49
+ .toLowerCase()
50
+ .replace(/[^\w一-龥-]+/g, '-')
51
+ .replace(/^-+|-+$/g, '')
52
+ .slice(0, 60) || 'general';
53
+ }
54
+
55
+ function firstHeading(text) {
56
+ const m = String(text).match(/^#\s+(.+)$/m);
57
+ return m ? m[1].trim() : '';
58
+ }
59
+
60
+ function relLink(fromRel, toRel) {
61
+ const rel = path.relative(path.dirname(fromRel), toRel).replace(/\\/g, '/');
62
+ return rel.startsWith('.') ? rel : `./${rel}`;
63
+ }
64
+
65
+ function readClaims(root) {
66
+ const dir = path.join(root, 'workflow', 'claims.d');
67
+ const rows = [];
68
+ for (const name of listFiles(dir, /^\d{4}-\d{2}\.jsonl$/)) {
69
+ const raw = readText(path.join(dir, name));
70
+ for (const line of raw.split(/\r?\n/)) {
71
+ if (!line.trim()) continue;
72
+ try {
73
+ rows.push(JSON.parse(line));
74
+ } catch {
75
+ /* ignore */
76
+ }
77
+ }
78
+ }
79
+ return rows;
80
+ }
81
+
82
+ function readSessionLessons(root) {
83
+ const dir = path.join(root, '.agents', 'cache', 'session-logs');
84
+ const rows = [];
85
+ for (const name of listFiles(dir, /^\d{4}-\d{2}-\d{2}\.jsonl$/)) {
86
+ const raw = readText(path.join(dir, name));
87
+ for (const line of raw.split(/\r?\n/)) {
88
+ if (!line.trim()) continue;
89
+ try {
90
+ const row = JSON.parse(line);
91
+ if (['failure', 'correction', 'refusal'].includes(row.type)) rows.push(row);
92
+ } catch {
93
+ /* ignore */
94
+ }
95
+ }
96
+ }
97
+ return rows.sort((a, b) => Date.parse(a.ts) - Date.parse(b.ts));
98
+ }
99
+
100
+ function readIncidents(root) {
101
+ const dir = path.join(root, 'workflow', 'incidents');
102
+ return listFiles(dir, /\.md$/).map((name) => {
103
+ const rel = `workflow/incidents/${name}`;
104
+ const text = readText(path.join(root, rel));
105
+ return { name, rel, title: firstHeading(text) || name.replace(/\.md$/, ''), text };
106
+ });
107
+ }
108
+
109
+ function readPapercuts(root) {
110
+ const dir = path.join(root, 'workflow', 'papercuts.d');
111
+ const out = [];
112
+ for (const name of listFiles(dir, /\.md$/)) {
113
+ const rel = `workflow/papercuts.d/${name}`;
114
+ const text = readText(path.join(root, rel));
115
+ out.push({ name, rel, title: firstHeading(text) || name, text });
116
+ }
117
+ const legacy = path.join(root, 'workflow', 'papercuts.md');
118
+ if (fs.existsSync(legacy)) out.push({ name: 'papercuts.md', rel: 'workflow/papercuts.md', title: 'papercuts', text: readText(legacy) });
119
+ return out;
120
+ }
121
+
122
+ export function buildMemoryWiki(root) {
123
+ const files = new Map();
124
+ const claims = readClaims(root);
125
+ const lessons = readSessionLessons(root);
126
+ const incidents = readIncidents(root);
127
+ const papercuts = readPapercuts(root);
128
+
129
+ // 主张按“出处路径的首段”聚主题;这比按时间堆叠更接近可遍历的记忆。
130
+ const claimTopics = new Map();
131
+ for (const c of claims) {
132
+ const topic = slugify(String(c.path || 'general').split(/[\\/]/)[0] || 'general');
133
+ if (!claimTopics.has(topic)) claimTopics.set(topic, []);
134
+ claimTopics.get(topic).push(c);
135
+ }
136
+
137
+ const indexRel = `${OUT_DIR.replace(/\\/g, '/')}/INDEX.md`;
138
+ const pages = [];
139
+
140
+ if (claimTopics.size) {
141
+ let recheck = { results: [] };
142
+ try {
143
+ recheck = recheckClaims(root);
144
+ } catch {
145
+ /* claims 账损坏时仍生成页面,只标 unknown */
146
+ }
147
+ const statusById = new Map((recheck.results || []).map((r) => [r.id, r.status]));
148
+ for (const topic of [...claimTopics.keys()].sort()) {
149
+ const rel = `${OUT_DIR.replace(/\\/g, '/')}/claims-${topic}.md`;
150
+ const rows = claimTopics.get(topic).slice().sort((a, b) => String(a.ts).localeCompare(String(b.ts)));
151
+ const body = [
152
+ '---',
153
+ `title: 主张 · ${topic}`,
154
+ 'generated: true',
155
+ 'source: workflow/claims.d',
156
+ '---',
157
+ '',
158
+ `# 主张 · ${topic}`,
159
+ '',
160
+ '> 主张是“未来会话可能当真的一句话”。`held` 只表示原文仍在,不表示主张为真。',
161
+ '',
162
+ ...rows.map((r) => {
163
+ const status = statusById.get(r.id) || 'unknown';
164
+ const mark = status === 'broken' ? '⚠ broken' : status;
165
+ return `- [${mark}] ${r.claim}\n - 出处:\`${r.path}\` · \`${r.quote}\``;
166
+ }),
167
+ '',
168
+ ].join('\n');
169
+ files.set(rel, body);
170
+ pages.push({ rel, title: `主张 · ${topic}`, count: rows.length, kind: 'claims' });
171
+ }
172
+ }
173
+
174
+ if (incidents.length) {
175
+ const rel = `${OUT_DIR.replace(/\\/g, '/')}/incidents.md`;
176
+ const body = [
177
+ '---',
178
+ 'title: 事故记忆',
179
+ 'generated: true',
180
+ 'source: workflow/incidents',
181
+ '---',
182
+ '',
183
+ '# 事故记忆',
184
+ '',
185
+ ...incidents.map((i) => `- [${i.title}](${relLink(rel, i.rel)}) — \`${i.rel}\``),
186
+ '',
187
+ ].join('\n');
188
+ files.set(rel, body);
189
+ pages.push({ rel, title: '事故记忆', count: incidents.length, kind: 'incidents' });
190
+ }
191
+
192
+ if (lessons.length) {
193
+ const rel = `${OUT_DIR.replace(/\\/g, '/')}/lessons.md`;
194
+ const body = [
195
+ '---',
196
+ 'title: 会话经验',
197
+ 'generated: true',
198
+ 'source: .agents/cache/session-logs',
199
+ '---',
200
+ '',
201
+ '# 会话经验(失败 / 纠正 / 拒绝)',
202
+ '',
203
+ ...lessons.slice(-40).map((e) => {
204
+ const ref = e.ref ? ` · \`${e.ref}\`` : '';
205
+ return `- ${String(e.ts).slice(0, 10)} [${e.type}] ${e.text}${ref}`;
206
+ }),
207
+ '',
208
+ ].join('\n');
209
+ files.set(rel, body);
210
+ pages.push({ rel, title: '会话经验', count: lessons.length, kind: 'lessons' });
211
+ }
212
+
213
+ if (papercuts.length) {
214
+ const rel = `${OUT_DIR.replace(/\\/g, '/')}/papercuts.md`;
215
+ const body = [
216
+ '---',
217
+ 'title: 错题与纸割',
218
+ 'generated: true',
219
+ 'source: workflow/papercuts.d',
220
+ '---',
221
+ '',
222
+ '# 错题与纸割',
223
+ '',
224
+ ...papercuts.map((p) => `- [${p.title}](${relLink(rel, p.rel)}) — \`${p.rel}\``),
225
+ '',
226
+ ].join('\n');
227
+ files.set(rel, body);
228
+ pages.push({ rel, title: '错题与纸割', count: papercuts.length, kind: 'papercuts' });
229
+ }
230
+
231
+ const index = [
232
+ '---',
233
+ 'title: 记忆地图',
234
+ 'generated: true',
235
+ 'source: memory-wiki.mjs',
236
+ '---',
237
+ '',
238
+ '# 记忆地图(INDEX)',
239
+ '',
240
+ '> 这是 dreaming 生成的**记忆表示**,不是文档归档。页面只汇总可被后续会话依赖的事实与教训;',
241
+ '> 原文仍在各自的权威文件里。人工 wiki 不受影响。',
242
+ '',
243
+ pages.length ? '| 页面 | 条目 | 类型 |' : '_暂无记忆来源。先积累 claims / incidents / papercuts / session-logs。_',
244
+ ...(pages.length ? ['|---|---|---|', ...pages.map((p) => `| [${p.title}](${relLink(indexRel, p.rel)}) | ${p.count} | ${p.kind} |`)] : []),
245
+ '',
246
+ ].join('\n');
247
+ files.set(indexRel, index);
248
+
249
+ return { files, pages, counts: { claims: claims.length, incidents: incidents.length, lessons: lessons.length, papercuts: papercuts.length } };
250
+ }
251
+
252
+ export function syncMemoryWiki(root, { check = false, dryRun = false } = {}) {
253
+ const { files, counts } = buildMemoryWiki(root);
254
+ const changed = [];
255
+ for (const [rel, content] of files) {
256
+ const abs = path.join(root, rel);
257
+ const current = fs.existsSync(abs) ? fs.readFileSync(abs, 'utf8') : null;
258
+ if (current === content) continue;
259
+ changed.push(rel);
260
+ if (!check && !dryRun) {
261
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
262
+ fs.writeFileSync(abs, content, 'utf8');
263
+ }
264
+ }
265
+ return { changed, counts, checked: check, dryRun };
266
+ }
267
+
268
+ // ---- CLI ----
269
+ function main() {
270
+ const argv = process.argv.slice(2);
271
+ const rootIdx = argv.indexOf('--root');
272
+ const root = path.resolve(rootIdx >= 0 ? argv[rootIdx + 1] : process.cwd());
273
+ const check = argv.includes('--check');
274
+ const dryRun = argv.includes('--dry-run');
275
+ const res = syncMemoryWiki(root, { check, dryRun });
276
+ const summary = `claims=${res.counts.claims} incidents=${res.counts.incidents} lessons=${res.counts.lessons} papercuts=${res.counts.papercuts}`;
277
+ if (check) {
278
+ if (res.changed.length) {
279
+ console.error(`✗ memory-wiki 漂移:${res.changed.join(', ')}(${summary})`);
280
+ return 1;
281
+ }
282
+ console.log(`✅ memory-wiki 最新(${summary})`);
283
+ return 0;
284
+ }
285
+ if (dryRun) {
286
+ console.log(`dry-run: 将写 ${res.changed.length} 个文件(${summary})`);
287
+ for (const rel of res.changed) console.log(` ${rel}`);
288
+ return 0;
289
+ }
290
+ console.log(`memory-wiki: 更新 ${res.changed.length} 个文件(${summary})`);
291
+ for (const rel of res.changed) console.log(` ${rel}`);
292
+ return 0;
293
+ }
294
+
295
+ const isMain = process.argv[1] && process.argv[1].endsWith('memory-wiki.mjs');
296
+ if (isMain) process.exit(main());