@double-codeing/flow2spec 2.2.3 → 3.0.7

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 (63) hide show
  1. package/README.md +73 -54
  2. package/cli.js +254 -14
  3. package/docs/Flow2Spec-/344/275/277/347/224/250/346/241/210/344/276/213-/346/250/241/346/213/237/345/257/271/350/257/235.md +123 -134
  4. package/docs/Flow2Spec-/346/274/224/350/256/262/347/250/277.md +411 -0
  5. package/docs/Flow2Spec-/350/256/276/350/256/241/350/257/264/346/230/216.md +574 -0
  6. package/docs/Flow2Spec/344/275/277/347/224/250/350/257/264/346/230/216.md +116 -76
  7. package/docs/README-/344/275/223/347/263/273/344/270/216/345/216/237/347/220/206.md +85 -44
  8. package/docs/README-/345/221/275/344/273/244/350/257/264/346/230/216.md +548 -79
  9. package/docs/README-/347/233/256/345/275/225/344/270/216/350/267/257/345/276/204/347/272/246/345/256/232.md +33 -62
  10. package/docs/images//346/212/200/350/203/275/351/227/255/347/216/257/345/233/276.png +0 -0
  11. package/lib/agents.js +15 -3
  12. package/lib/claudeSettingsAdapter.js +114 -0
  13. package/lib/codexAgentsAdapter.js +70 -0
  14. package/lib/flow2specConfig.js +229 -0
  15. package/lib/init.js +698 -25
  16. package/package.json +2 -2
  17. package/templates/AGENTS.md +98 -0
  18. package/templates/flow2spec.config.json +9 -0
  19. package/templates/hooks/f2s-config-inject.js +181 -0
  20. package/templates/knowledge/index.md +68 -0
  21. package/templates/knowledge/manifest-matchers.json +35 -0
  22. package/templates/knowledge/manifest-routing.json +45 -0
  23. package/templates/knowledge/matchers/m-doc-routing.json +11 -0
  24. package/templates/knowledge/matchers/m-f2s-config-precheck.json +15 -0
  25. package/templates/knowledge/matchers/m-implement-from-spec.json +10 -0
  26. package/templates/{template → knowledge/template}//345/220/216/347/253/257/346/212/200/346/234/257/346/250/241/347/211/210.md +3 -2
  27. package/templates/{template → knowledge/template}//347/273/210/347/250/277/346/250/241/347/211/210.md +5 -4
  28. package/templates/knowledge/topics/f2s-config-precheck.md +24 -0
  29. package/templates/knowledge/topics/f2s-fallback-triage.md +60 -0
  30. package/templates/knowledge/topics/f2s-implement-tech-design.md +21 -0
  31. package/templates/knowledge/topics/f2s-stock-docs-vs-req-docs.md +25 -0
  32. package/templates/rules/f2s-config-check.mdc +35 -0
  33. package/templates/rules/f2s-flow2spec-unified-entry.mdc +88 -0
  34. package/templates/rules/f2s-implement-tech-design.mdc +144 -0
  35. package/templates/rules/f2s-karpathy-guidelines.mdc +77 -0
  36. package/templates/rules/f2s-knowledge-preflight.mdc +70 -0
  37. package/templates/rules/f2s-stock-docs-vs-req-docs.mdc +16 -0
  38. package/templates/rules/f2s-task.mdc +202 -0
  39. package/templates/skills/f2s-ctx-build/SKILL.md +74 -173
  40. package/templates/skills/f2s-ctx-rm/SKILL.md +39 -43
  41. package/templates/skills/f2s-doc-add/SKILL.md +69 -106
  42. package/templates/skills/f2s-doc-arch/SKILL.md +20 -9
  43. package/templates/skills/f2s-doc-final/SKILL.md +29 -21
  44. package/templates/skills/f2s-doc-pdf/SKILL.md +17 -10
  45. package/templates/skills/f2s-git-commit/SKILL.md +189 -0
  46. package/templates/skills/f2s-karpathy-guidelines/SKILL.md +20 -0
  47. package/templates/skills/f2s-kb-feat/SKILL.md +72 -50
  48. package/templates/skills/f2s-kb-fix/SKILL.md +77 -46
  49. package/templates/skills/f2s-kb-merge/SKILL.md +9 -0
  50. package/templates/skills/f2s-kb-migrate/SKILL.md +356 -0
  51. package/templates/skills/f2s-kb-sync/SKILL.md +80 -59
  52. package/templates/skills/f2s-kb-upgrade/SKILL.md +225 -0
  53. package/templates/skills/f2s-req-backend/SKILL.md +35 -12
  54. package/templates/skills/f2s-req-clarify/SKILL.md +10 -2
  55. package/templates/skills/f2s-req-plan/SKILL.md +110 -0
  56. package/templates/skills/stock-docs-vs-req-docs/SKILL.md +10 -4
  57. package/docs/images//345/216/237/347/220/206/345/233/2761.png +0 -0
  58. package/docs/images//345/216/237/347/220/206/345/233/2762.png +0 -0
  59. package/docs/images//345/221/275/344/273/244/346/230/216/347/273/206/345/233/276.png +0 -0
  60. package/docs/images//346/227/245/345/270/270/346/223/215/344/275/234/346/265/201/347/250/213/345/233/276.png +0 -0
  61. package/docs/images//347/256/200/350/277/260/345/233/276.png +0 -0
  62. package/templates/rules/implement-tech-design.mdc +0 -177
  63. package/templates/rules/stock-docs-vs-req-docs.mdc +0 -14
package/lib/init.js CHANGED
@@ -1,12 +1,39 @@
1
1
  const path = require("path");
2
2
  const fs = require("fs");
3
- const { AGENTS, SUBDIRS, normalizeAgentIds } = require("./agents");
4
- const { adaptRuleMdcToClaudeMd, shouldWriteClaudeStyleRules } = require("./claudeRulesAdapter");
3
+ const {
4
+ AGENTS,
5
+ KNOWLEDGE_ROOT,
6
+ KNOWLEDGE_SUBDIRS,
7
+ AGENT_SUBDIRS,
8
+ normalizeAgentIds,
9
+ } = require("./agents");
10
+ const {
11
+ adaptRuleMdcToClaudeMd,
12
+ shouldWriteClaudeStyleRules,
13
+ } = require("./claudeRulesAdapter");
14
+ const { buildCodexAgentsMd } = require("./codexAgentsAdapter");
15
+ const {
16
+ loadFlow2specConfig,
17
+ ensureFlow2specProjectConfig,
18
+ } = require("./flow2specConfig");
19
+ const { writeClaudeAgentHooks } = require("./claudeSettingsAdapter");
5
20
 
6
- function ensureDirs(cwd, agentRoot) {
7
- for (const sub of SUBDIRS) {
8
- const full = path.join(cwd, agentRoot, sub);
9
- if (!fs.existsSync(full)) fs.mkdirSync(full, { recursive: true });
21
+ function ensureDir(dir) {
22
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
23
+ }
24
+
25
+ function ensureKnowledgeDirs(cwd) {
26
+ ensureDir(path.join(cwd, KNOWLEDGE_ROOT));
27
+ for (const sub of KNOWLEDGE_SUBDIRS) {
28
+ ensureDir(path.join(cwd, KNOWLEDGE_ROOT, sub));
29
+ }
30
+ }
31
+
32
+ function ensureAgentDirs(cwd, agentId) {
33
+ const root = AGENTS[agentId].root;
34
+ ensureDir(path.join(cwd, root));
35
+ for (const sub of AGENT_SUBDIRS[agentId] || []) {
36
+ ensureDir(path.join(cwd, root, sub));
10
37
  }
11
38
  }
12
39
 
@@ -23,12 +50,584 @@ function copyRecursive(src, dest) {
23
50
  }
24
51
  }
25
52
 
26
- /** 复制 templates/rules:Cursor 保留 .mdc;Claude Code 写入 .md 并转换 frontmatter(globs→paths,去掉 alwaysApply) */
53
+ function copyKnowledgeTemplates(cwd, templatesDir, options = {}) {
54
+ const { overwrite = false } = options;
55
+ const srcRoot = path.join(templatesDir, "knowledge");
56
+ const destRoot = path.join(cwd, KNOWLEDGE_ROOT);
57
+ if (!fs.existsSync(srcRoot)) return;
58
+ const result = { written: 0, skipped: 0 };
59
+ for (const name of fs.readdirSync(srcRoot)) {
60
+ if (name === "manifest-matchers.json") {
61
+ continue;
62
+ }
63
+ copyRecursivePreserve(
64
+ path.join(srcRoot, name),
65
+ path.join(destRoot, name),
66
+ overwrite,
67
+ result,
68
+ );
69
+ }
70
+ return result;
71
+ }
72
+
73
+ function copyRecursivePreserve(src, dest, overwrite, result) {
74
+ const stat = fs.statSync(src);
75
+ if (stat.isDirectory()) {
76
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
77
+ for (const name of fs.readdirSync(src)) {
78
+ copyRecursivePreserve(
79
+ path.join(src, name),
80
+ path.join(dest, name),
81
+ overwrite,
82
+ result,
83
+ );
84
+ }
85
+ return;
86
+ }
87
+ if (!overwrite && fs.existsSync(dest)) {
88
+ result.skipped += 1;
89
+ return;
90
+ }
91
+ fs.copyFileSync(src, dest);
92
+ result.written += 1;
93
+ }
94
+
95
+ function readJson(filePath) {
96
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
97
+ }
98
+
99
+ function writeJson(filePath, data) {
100
+ fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
101
+ }
102
+
103
+ function buildDefaultMatcherPath(matcherId) {
104
+ return `${KNOWLEDGE_ROOT}/matchers/${matcherId}.json`;
105
+ }
106
+
107
+ function normalizeMatcherShardData(raw, matcherId) {
108
+ const safeRaw =
109
+ raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
110
+ return {
111
+ ...safeRaw,
112
+ id: matcherId,
113
+ includeAny: dedupeStringArray(safeRaw.includeAny || []),
114
+ };
115
+ }
116
+
117
+ function dedupeStringArray(values) {
118
+ const out = [];
119
+ const seen = new Set();
120
+ for (const item of values || []) {
121
+ if (typeof item !== "string") continue;
122
+ if (seen.has(item)) continue;
123
+ seen.add(item);
124
+ out.push(item);
125
+ }
126
+ return out;
127
+ }
128
+
129
+ function unionByKey(templateList, existingList, key, mergeItem) {
130
+ const existingMap = new Map();
131
+ for (const item of existingList || []) {
132
+ if (!item || typeof item !== "object") continue;
133
+ if (!item[key] || typeof item[key] !== "string") continue;
134
+ existingMap.set(item[key], item);
135
+ }
136
+
137
+ const out = [];
138
+ const orderedKeys = [];
139
+
140
+ for (const item of templateList || []) {
141
+ if (!item || typeof item !== "object") continue;
142
+ const id = item[key];
143
+ if (!id || typeof id !== "string") continue;
144
+ orderedKeys.push(id);
145
+ const existing = existingMap.get(id);
146
+ out.push(mergeItem(item, existing));
147
+ }
148
+
149
+ for (const item of existingList || []) {
150
+ if (!item || typeof item !== "object") continue;
151
+ const id = item[key];
152
+ if (!id || typeof id !== "string") continue;
153
+ if (orderedKeys.includes(id)) continue;
154
+ out.push(item);
155
+ }
156
+
157
+ return out;
158
+ }
159
+
160
+ function mergeTopicDependencies(templateDeps, existingDeps) {
161
+ const out = {};
162
+ const keys = new Set([
163
+ ...Object.keys(templateDeps || {}),
164
+ ...Object.keys(existingDeps || {}),
165
+ ]);
166
+ for (const key of keys) {
167
+ out[key] = dedupeStringArray([
168
+ ...(templateDeps?.[key] || []),
169
+ ...(existingDeps?.[key] || []),
170
+ ]);
171
+ }
172
+ return out;
173
+ }
174
+
175
+ function buildMergedRouting(templateRouting, existingRouting) {
176
+ const mergedTaskRules = unionByKey(
177
+ templateRouting.taskToTopicRules,
178
+ existingRouting.taskToTopicRules,
179
+ "task",
180
+ (templateRule, existingRule) => {
181
+ if (!existingRule) return templateRule;
182
+ const mergedMatcherId = existingRule.matcherId || templateRule.matcherId;
183
+ return {
184
+ ...templateRule,
185
+ ...existingRule,
186
+ matcherId: mergedMatcherId,
187
+ matcherPath:
188
+ existingRule.matcherPath ||
189
+ templateRule.matcherPath ||
190
+ (mergedMatcherId ? buildDefaultMatcherPath(mergedMatcherId) : null),
191
+ topics: dedupeStringArray([
192
+ ...(templateRule.topics || []),
193
+ ...(existingRule.topics || []),
194
+ ]),
195
+ };
196
+ },
197
+ );
198
+
199
+ const knownMerged = {
200
+ version: templateRouting.version || existingRouting.version,
201
+ knowledgeRoot:
202
+ existingRouting.knowledgeRoot || templateRouting.knowledgeRoot,
203
+ generatedFrom:
204
+ existingRouting.generatedFrom || templateRouting.generatedFrom,
205
+ matcherKey:
206
+ existingRouting.matcherKey || templateRouting.matcherKey || "matcherId",
207
+ sourceOfTruth:
208
+ existingRouting.sourceOfTruth ||
209
+ templateRouting.sourceOfTruth ||
210
+ `${KNOWLEDGE_ROOT}/manifest-routing.json`,
211
+ fallbackTopic:
212
+ existingRouting.fallbackTopic || templateRouting.fallbackTopic,
213
+ topicDependencies: mergeTopicDependencies(
214
+ templateRouting.topicDependencies,
215
+ existingRouting.topicDependencies,
216
+ ),
217
+ topicPaths: {
218
+ ...(templateRouting.topicPaths || {}),
219
+ ...(existingRouting.topicPaths || {}),
220
+ },
221
+ taskToTopicRules: mergedTaskRules,
222
+ };
223
+
224
+ const knownKeys = new Set(Object.keys(knownMerged));
225
+ const extras = {};
226
+ for (const [key, value] of Object.entries(existingRouting || {})) {
227
+ if (knownKeys.has(key)) continue;
228
+ extras[key] = value;
229
+ }
230
+
231
+ const merged = {
232
+ ...knownMerged,
233
+ ...extras,
234
+ };
235
+ delete merged.matchersFile;
236
+ return merged;
237
+ }
238
+
239
+ function buildMergedMatchers(templateMatchers, existingMatchers) {
240
+ const templateMap = templateMatchers.matchers || {};
241
+ const existingMap = existingMatchers.matchers || {};
242
+ const allMatcherIds = new Set([
243
+ ...Object.keys(templateMap),
244
+ ...Object.keys(existingMap),
245
+ ]);
246
+ const mergedMatchers = {};
247
+ for (const matcherId of allMatcherIds) {
248
+ const templateItem = templateMap[matcherId] || {};
249
+ const existingItem = existingMap[matcherId] || {};
250
+ mergedMatchers[matcherId] = {
251
+ ...templateItem,
252
+ ...existingItem,
253
+ includeAny: dedupeStringArray([
254
+ ...(templateItem.includeAny || []),
255
+ ...(existingItem.includeAny || []),
256
+ ]),
257
+ };
258
+ }
259
+
260
+ const knownMerged = {
261
+ version: templateMatchers.version || existingMatchers.version,
262
+ generatedFrom:
263
+ existingMatchers.generatedFrom || templateMatchers.generatedFrom,
264
+ matcherKey:
265
+ existingMatchers.matcherKey || templateMatchers.matcherKey || "matcherId",
266
+ sourceOfTruth:
267
+ existingMatchers.sourceOfTruth ||
268
+ templateMatchers.sourceOfTruth ||
269
+ `${KNOWLEDGE_ROOT}/manifest-routing.json`,
270
+ matchers: mergedMatchers,
271
+ };
272
+
273
+ const knownKeys = new Set(Object.keys(knownMerged));
274
+ const extras = {};
275
+ for (const [key, value] of Object.entries(existingMatchers || {})) {
276
+ if (knownKeys.has(key)) continue;
277
+ extras[key] = value;
278
+ }
279
+
280
+ return {
281
+ ...knownMerged,
282
+ ...extras,
283
+ };
284
+ }
285
+
286
+ function ensureRoutingMatcherPaths(routing) {
287
+ const rules = Array.isArray(routing.taskToTopicRules)
288
+ ? routing.taskToTopicRules
289
+ : [];
290
+ let changed = false;
291
+ const nextRules = rules.map((rule) => {
292
+ if (!rule || typeof rule !== "object") return rule;
293
+ if (!rule.matcherId || typeof rule.matcherId !== "string") return rule;
294
+ if (rule.matcherPath && typeof rule.matcherPath === "string") return rule;
295
+ changed = true;
296
+ return {
297
+ ...rule,
298
+ matcherPath: buildDefaultMatcherPath(rule.matcherId),
299
+ };
300
+ });
301
+ if (!changed) return { routing, changed };
302
+ return {
303
+ routing: {
304
+ ...routing,
305
+ taskToTopicRules: nextRules,
306
+ },
307
+ changed,
308
+ };
309
+ }
310
+
311
+ function buildMatcherIdToPathMap(routing) {
312
+ const out = new Map();
313
+ const rules = Array.isArray(routing.taskToTopicRules)
314
+ ? routing.taskToTopicRules
315
+ : [];
316
+ for (const rule of rules) {
317
+ if (!rule || typeof rule !== "object") continue;
318
+ if (!rule.matcherId || typeof rule.matcherId !== "string") continue;
319
+ const matcherPath =
320
+ rule.matcherPath && typeof rule.matcherPath === "string"
321
+ ? rule.matcherPath
322
+ : buildDefaultMatcherPath(rule.matcherId);
323
+ if (!out.has(rule.matcherId)) {
324
+ out.set(rule.matcherId, matcherPath);
325
+ }
326
+ }
327
+ return out;
328
+ }
329
+
330
+ function ensureMatcherShards(cwd, routing, mergedMatchers) {
331
+ const matcherIdToPath = buildMatcherIdToPathMap(routing);
332
+ const matcherMap =
333
+ mergedMatchers?.matchers && typeof mergedMatchers.matchers === "object"
334
+ ? mergedMatchers.matchers
335
+ : {};
336
+
337
+ for (const matcherId of Object.keys(matcherMap)) {
338
+ if (!matcherIdToPath.has(matcherId)) {
339
+ matcherIdToPath.set(matcherId, buildDefaultMatcherPath(matcherId));
340
+ }
341
+ }
342
+
343
+ let changed = false;
344
+ let writtenCount = 0;
345
+ for (const [matcherId, matcherPath] of matcherIdToPath.entries()) {
346
+ const matcherAbs = resolveFromCwd(cwd, matcherPath);
347
+ ensureDir(path.dirname(matcherAbs));
348
+
349
+ const compatMatcher = matcherMap[matcherId];
350
+ const existingShard = fs.existsSync(matcherAbs) ? readJson(matcherAbs) : {};
351
+ const nextShard = normalizeMatcherShardData(
352
+ {
353
+ ...(compatMatcher && typeof compatMatcher === "object"
354
+ ? compatMatcher
355
+ : {}),
356
+ ...(existingShard && typeof existingShard === "object"
357
+ ? existingShard
358
+ : {}),
359
+ },
360
+ matcherId,
361
+ );
362
+
363
+ const prevRaw = fs.existsSync(matcherAbs)
364
+ ? JSON.stringify(existingShard)
365
+ : null;
366
+ const nextRaw = JSON.stringify(nextShard);
367
+ if (prevRaw === nextRaw) continue;
368
+
369
+ writeJson(matcherAbs, nextShard);
370
+ writtenCount += 1;
371
+ changed = true;
372
+ }
373
+
374
+ return { changed, writtenCount };
375
+ }
376
+
377
+ function upgradeKnowledgeRoutingAndMatchers(cwd, templatesDir, options = {}) {
378
+ const { overwrite = false } = options;
379
+ if (overwrite) {
380
+ return {
381
+ upgraded: false,
382
+ reason: "overwrite",
383
+ };
384
+ }
385
+
386
+ const templateRoutingPath = path.join(
387
+ templatesDir,
388
+ "knowledge",
389
+ "manifest-routing.json",
390
+ );
391
+ const templateMatchersPath = path.join(
392
+ templatesDir,
393
+ "knowledge",
394
+ "manifest-matchers.json",
395
+ );
396
+ if (
397
+ !fs.existsSync(templateRoutingPath) ||
398
+ !fs.existsSync(templateMatchersPath)
399
+ ) {
400
+ return {
401
+ upgraded: false,
402
+ reason: "missing-routing-templates",
403
+ };
404
+ }
405
+
406
+ const routingPath = path.join(cwd, KNOWLEDGE_ROOT, "manifest-routing.json");
407
+ const matchersPath = path.join(cwd, KNOWLEDGE_ROOT, "manifest-matchers.json");
408
+
409
+ const templateRouting = readJson(templateRoutingPath);
410
+ const templateMatchers = readJson(templateMatchersPath);
411
+ const hadRouting = fs.existsSync(routingPath);
412
+ const hadMatchers = fs.existsSync(matchersPath);
413
+ const existingRouting = hadRouting ? readJson(routingPath) : {};
414
+ const existingMatchers = hadMatchers ? readJson(matchersPath) : {};
415
+
416
+ const mergedRouting = buildMergedRouting(templateRouting, existingRouting);
417
+ const mergedMatchers = buildMergedMatchers(
418
+ templateMatchers,
419
+ existingMatchers,
420
+ );
421
+ const {
422
+ routing: mergedRoutingWithMatcherPath,
423
+ changed: matcherPathBackfilled,
424
+ } = ensureRoutingMatcherPaths(mergedRouting);
425
+ const matcherShardUpgrade = ensureMatcherShards(
426
+ cwd,
427
+ mergedRoutingWithMatcherPath,
428
+ mergedMatchers,
429
+ );
430
+
431
+ const oldRoutingRaw = JSON.stringify(existingRouting);
432
+ const newRoutingRaw = JSON.stringify(mergedRoutingWithMatcherPath);
433
+ const oldMatchersRaw = JSON.stringify(existingMatchers);
434
+ const newMatchersRaw = JSON.stringify(mergedMatchers);
435
+
436
+ if (!hadRouting || oldRoutingRaw !== newRoutingRaw) {
437
+ writeJson(routingPath, mergedRoutingWithMatcherPath);
438
+ }
439
+
440
+ const routingChanged = !hadRouting || oldRoutingRaw !== newRoutingRaw;
441
+ const legacyAggregateDiffers =
442
+ hadMatchers && oldMatchersRaw !== newMatchersRaw;
443
+ let legacyMatchersFileRemoved = false;
444
+ if (fs.existsSync(matchersPath)) {
445
+ try {
446
+ fs.unlinkSync(matchersPath);
447
+ legacyMatchersFileRemoved = true;
448
+ } catch (e) {
449
+ /* 保留文件时由下次 init 重试 */
450
+ }
451
+ }
452
+ const upgraded =
453
+ routingChanged ||
454
+ legacyAggregateDiffers ||
455
+ matcherShardUpgrade.changed ||
456
+ legacyMatchersFileRemoved;
457
+
458
+ return {
459
+ upgraded,
460
+ reason: upgraded ? "merged" : "up-to-date",
461
+ routingChanged,
462
+ legacyAggregateDiffers,
463
+ legacyMatchersFileRemoved,
464
+ matcherPathBackfilled,
465
+ matcherShardChanged: matcherShardUpgrade.changed,
466
+ matcherShardWritten: matcherShardUpgrade.writtenCount,
467
+ };
468
+ }
469
+
470
+ function resolveFromCwd(cwd, maybeRelativePath) {
471
+ return path.isAbsolute(maybeRelativePath)
472
+ ? maybeRelativePath
473
+ : path.join(cwd, maybeRelativePath);
474
+ }
475
+
476
+ function validateKnowledgeRouting(cwd) {
477
+ const routingPath = path.join(cwd, KNOWLEDGE_ROOT, "manifest-routing.json");
478
+ const matchersPath = path.join(cwd, KNOWLEDGE_ROOT, "manifest-matchers.json");
479
+ if (!fs.existsSync(routingPath)) {
480
+ throw new Error(
481
+ `缺少知识库路由清单:${path.join(KNOWLEDGE_ROOT, "manifest-routing.json")}`,
482
+ );
483
+ }
484
+ let routing;
485
+ let matcherData = null;
486
+ try {
487
+ routing = JSON.parse(fs.readFileSync(routingPath, "utf8"));
488
+ } catch (e) {
489
+ throw new Error(`路由清单 JSON 解析失败:${routingPath}`);
490
+ }
491
+ if (fs.existsSync(matchersPath)) {
492
+ try {
493
+ matcherData = JSON.parse(fs.readFileSync(matchersPath, "utf8"));
494
+ } catch (e) {
495
+ throw new Error(`匹配清单 JSON 解析失败:${matchersPath}`);
496
+ }
497
+ }
498
+
499
+ if (!routing.topicPaths || typeof routing.topicPaths !== "object") {
500
+ throw new Error("路由清单缺少 topicPaths,无法执行主题路由。");
501
+ }
502
+
503
+ const topicIds = new Set(Object.keys(routing.topicPaths));
504
+ if (topicIds.size === 0) {
505
+ throw new Error("路由清单 topicPaths 为空,无法执行主题路由。");
506
+ }
507
+
508
+ for (const [topicId, topicPath] of Object.entries(routing.topicPaths)) {
509
+ if (!topicId || typeof topicId !== "string") {
510
+ throw new Error("topicPaths 中存在非法 topicId。");
511
+ }
512
+ if (!topicPath || typeof topicPath !== "string") {
513
+ throw new Error(`topicPaths.${topicId} 必须是字符串路径。`);
514
+ }
515
+ const topicAbs = resolveFromCwd(cwd, topicPath);
516
+ if (!fs.existsSync(topicAbs)) {
517
+ throw new Error(`路由清单引用的 topic 不存在:${topicPath}`);
518
+ }
519
+ }
520
+
521
+ if (routing.fallbackTopic && !topicIds.has(routing.fallbackTopic)) {
522
+ throw new Error(
523
+ `fallbackTopic 不存在于 topicPaths:${routing.fallbackTopic}`,
524
+ );
525
+ }
526
+
527
+ if (
528
+ routing.topicDependencies &&
529
+ typeof routing.topicDependencies === "object"
530
+ ) {
531
+ for (const [topicId, deps] of Object.entries(routing.topicDependencies)) {
532
+ if (!topicIds.has(topicId)) {
533
+ throw new Error(`topicDependencies 引用了不存在的 topic:${topicId}`);
534
+ }
535
+ if (!Array.isArray(deps)) {
536
+ throw new Error(`topicDependencies.${topicId} 必须是数组。`);
537
+ }
538
+ for (const depId of deps) {
539
+ if (!topicIds.has(depId)) {
540
+ throw new Error(
541
+ `topicDependencies.${topicId} 引用了不存在的依赖:${depId}`,
542
+ );
543
+ }
544
+ }
545
+ }
546
+ }
547
+
548
+ const matcherMap =
549
+ matcherData?.matchers && typeof matcherData.matchers === "object"
550
+ ? matcherData.matchers
551
+ : null;
552
+ if (matcherData && !matcherMap) {
553
+ throw new Error("匹配清单缺少 matchers 对象。");
554
+ }
555
+
556
+ if (Array.isArray(routing.taskToTopicRules)) {
557
+ for (const rule of routing.taskToTopicRules) {
558
+ if (!rule || typeof rule !== "object") {
559
+ throw new Error("taskToTopicRules 存在非法项(非对象)。");
560
+ }
561
+ if (!rule.task || typeof rule.task !== "string") {
562
+ throw new Error("taskToTopicRules 每项必须包含字符串类型的 task。");
563
+ }
564
+ if (!Array.isArray(rule.topics) || rule.topics.length === 0) {
565
+ throw new Error(`taskToTopicRules(${rule.task}) 必须包含非空 topics。`);
566
+ }
567
+ if (!rule.matcherId || typeof rule.matcherId !== "string") {
568
+ throw new Error(`taskToTopicRules(${rule.task}) 必须包含 matcherId。`);
569
+ }
570
+ if (!rule.matcherPath || typeof rule.matcherPath !== "string") {
571
+ throw new Error(`taskToTopicRules(${rule.task}) 必须包含 matcherPath。`);
572
+ }
573
+ const matcherAbs = resolveFromCwd(cwd, rule.matcherPath);
574
+ if (!fs.existsSync(matcherAbs)) {
575
+ throw new Error(
576
+ `taskToTopicRules(${rule.task}) 引用了不存在的 matcherPath:${rule.matcherPath}`,
577
+ );
578
+ }
579
+ let matcherShard;
580
+ try {
581
+ matcherShard = JSON.parse(fs.readFileSync(matcherAbs, "utf8"));
582
+ } catch (e) {
583
+ throw new Error(`matcherPath JSON 解析失败:${rule.matcherPath}`);
584
+ }
585
+ if (!matcherShard || typeof matcherShard !== "object") {
586
+ throw new Error(`matcherPath 内容非法(非对象):${rule.matcherPath}`);
587
+ }
588
+ if (matcherShard.id !== rule.matcherId) {
589
+ throw new Error(
590
+ `matcherPath(${rule.matcherPath}) 的 id 与 matcherId 不一致:${matcherShard.id} vs ${rule.matcherId}`,
591
+ );
592
+ }
593
+ if (!Array.isArray(matcherShard.includeAny)) {
594
+ throw new Error(
595
+ `matcherPath(${rule.matcherPath}) 的 includeAny 必须为数组。`,
596
+ );
597
+ }
598
+ for (const topicId of rule.topics) {
599
+ if (!topicIds.has(topicId)) {
600
+ throw new Error(
601
+ `taskToTopicRules(${rule.task}) 引用了不存在的 topic:${topicId}`,
602
+ );
603
+ }
604
+ }
605
+ }
606
+ }
607
+ }
608
+
609
+ /**
610
+ * 将包内 templates/knowledge/index.md 原样复制到目标 cwd 下 .Knowledge/template/index.template.md,
611
+ * 供 f2s-kb-upgrade 技能步骤 3b 与宿主仓 .Knowledge/index.md 对照;init 不修改 index.md 正文。
612
+ * 注意:模板正文声明「.Knowledge」指宿主仓;与 flow2spec 开发仓根 .Knowledge(产品自用知识库)职责不同。
613
+ */
614
+ function copyKnowledgeIndexTemplateSnapshot(cwd, templatesDir) {
615
+ const src = path.join(templatesDir, "knowledge", "index.md");
616
+ const destDir = path.join(cwd, KNOWLEDGE_ROOT, "template");
617
+ const dest = path.join(destDir, "index.template.md");
618
+ if (!fs.existsSync(src)) {
619
+ return { written: false, reason: "missing-template-index" };
620
+ }
621
+ ensureDir(destDir);
622
+ fs.copyFileSync(src, dest);
623
+ return { written: true };
624
+ }
625
+
27
626
  function copyRulesTemplates(cwd, agentRoot, templatesDir) {
28
627
  const rulesSrc = path.join(templatesDir, "rules");
29
628
  const rulesDest = path.join(cwd, agentRoot, "rules");
30
629
  if (!fs.existsSync(rulesSrc)) return;
31
- if (!fs.existsSync(rulesDest)) fs.mkdirSync(rulesDest, { recursive: true });
630
+ ensureDir(rulesDest);
32
631
 
33
632
  const claudeStyle = shouldWriteClaudeStyleRules(agentRoot);
34
633
  if (claudeStyle) {
@@ -57,41 +656,115 @@ function copyRulesTemplates(cwd, agentRoot, templatesDir) {
57
656
  }
58
657
  }
59
658
 
60
- /** templates 下全部内容复制到指定配置根目录(如 .cursor、.claude) */
61
- function copyTemplatesToAgentRoot(cwd, agentRoot) {
62
- const templatesDir = path.join(__dirname, "..", "templates");
659
+ function copySkills(cwd, agentRoot, templatesDir) {
63
660
  const destRoot = path.join(cwd, agentRoot);
64
-
65
- copyRulesTemplates(cwd, agentRoot, templatesDir);
66
-
67
661
  const skillsSrc = path.join(templatesDir, "skills");
68
- const skillsDest = path.join(destRoot, "skills");
662
+
69
663
  if (fs.existsSync(skillsSrc)) {
664
+ const skillsDest = path.join(destRoot, "skills");
665
+ ensureDir(skillsDest);
70
666
  for (const name of fs.readdirSync(skillsSrc)) {
71
667
  copyRecursive(path.join(skillsSrc, name), path.join(skillsDest, name));
72
668
  }
73
669
  }
670
+ }
671
+
672
+ function removeLegacyAgentTemplateDir(cwd, agentRoot) {
673
+ const templateDir = path.join(cwd, agentRoot, "template");
674
+ if (fs.existsSync(templateDir)) {
675
+ fs.rmSync(templateDir, { recursive: true, force: true });
676
+ }
677
+ }
678
+
679
+ function stripMdcFrontmatter(src) {
680
+ return src.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
681
+ }
74
682
 
75
- const templateSrc = path.join(templatesDir, "template");
76
- const templateDest = path.join(destRoot, "template");
77
- if (fs.existsSync(templateSrc)) {
78
- ensureDirs(cwd, agentRoot);
79
- copyRecursive(templateSrc, templateDest);
683
+ function writeCodexTopicMirrors(cwd, templatesDir) {
684
+ const rulesDir = path.join(templatesDir, "rules");
685
+ const outDir = path.join(cwd, ".codex", "topics");
686
+ ensureDir(outDir);
687
+ if (!fs.existsSync(rulesDir)) return;
688
+ // 与 `.cursor/rules/*.mdc` 同源:Codex 不读 rules/,故镜像全部包模板规则到 `.codex/topics/*.md`
689
+ const names = fs
690
+ .readdirSync(rulesDir)
691
+ .filter((n) => n.toLowerCase().endsWith(".mdc"))
692
+ .sort();
693
+ for (const name of names) {
694
+ const srcPath = path.join(rulesDir, name);
695
+ if (!fs.statSync(srcPath).isFile()) continue;
696
+ const raw = fs.readFileSync(srcPath, "utf8");
697
+ const body = stripMdcFrontmatter(raw).trimStart();
698
+ const outName = name.replace(/\.mdc$/i, ".md");
699
+ fs.writeFileSync(path.join(outDir, outName), body, "utf8");
700
+ }
701
+ }
702
+
703
+ function writeCodexEntry(cwd, templatesDir, projectConfig) {
704
+ const out = buildCodexAgentsMd(templatesDir, projectConfig);
705
+ fs.writeFileSync(path.join(cwd, ".codex", "AGENTS.md"), out, "utf8");
706
+ writeCodexTopicMirrors(cwd, templatesDir);
707
+ }
708
+
709
+ function writeAgentArtifacts(cwd, agentId, templatesDir, projectConfig) {
710
+ const root = AGENTS[agentId].root;
711
+ copySkills(cwd, root, templatesDir);
712
+ removeLegacyAgentTemplateDir(cwd, root);
713
+ if (agentId !== "codex") {
714
+ copyRulesTemplates(cwd, root, templatesDir);
715
+ } else {
716
+ writeCodexEntry(cwd, templatesDir, projectConfig);
80
717
  }
81
718
  }
82
719
 
83
720
  /**
84
721
  * @param {string} cwd
85
722
  * @param {string[]} [agentIds] 不传则仅 cursor
723
+ * @param {object} [options]
724
+ * @param {boolean} [options.overwriteKnowledge]
725
+ * @param {object} [options.configValues] init 交互收集的配置字段值
86
726
  */
87
- async function run(cwd, agentIds) {
727
+ async function run(cwd, agentIds, options = {}) {
728
+ const { overwriteKnowledge = false, configValues } = options;
88
729
  const ids = normalizeAgentIds(agentIds || []);
730
+ const templatesDir = path.join(__dirname, "..", "templates");
731
+
732
+ ensureKnowledgeDirs(cwd);
733
+ ensureFlow2specProjectConfig(cwd, templatesDir, {
734
+ overwrite: false,
735
+ values: configValues,
736
+ });
737
+ const knowledgeResult = copyKnowledgeTemplates(cwd, templatesDir, {
738
+ overwrite: overwriteKnowledge,
739
+ });
740
+ const routingUpgrade = upgradeKnowledgeRoutingAndMatchers(cwd, templatesDir, {
741
+ overwrite: overwriteKnowledge,
742
+ });
743
+ validateKnowledgeRouting(cwd);
744
+
745
+ const indexSnapshot = copyKnowledgeIndexTemplateSnapshot(cwd, templatesDir);
746
+
747
+ const projectConfig = loadFlow2specConfig(cwd);
748
+
749
+ const claudeHooksResult = {};
89
750
  for (const id of ids) {
90
- const root = AGENTS[id].root;
91
- ensureDirs(cwd, root);
92
- copyTemplatesToAgentRoot(cwd, root);
751
+ ensureAgentDirs(cwd, id);
752
+ writeAgentArtifacts(cwd, id, templatesDir, projectConfig);
753
+ if (id === "claude") {
754
+ const result = writeClaudeAgentHooks(cwd, templatesDir);
755
+ claudeHooksResult.hookScriptWritten = result.hookScriptResult?.written ?? false;
756
+ claudeHooksResult.settingsChanged = result.settingsChanged;
757
+ }
93
758
  }
94
- return ids;
759
+ return {
760
+ ids,
761
+ knowledgeResult,
762
+ overwriteKnowledge,
763
+ routingUpgrade,
764
+ indexSnapshot,
765
+ projectConfig,
766
+ claudeHooksResult,
767
+ };
95
768
  }
96
769
 
97
770
  module.exports = run;