@double-codeing/flow2spec 3.0.19 → 3.1.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.
Files changed (52) hide show
  1. package/README.en.md +15 -6
  2. package/README.md +5 -5
  3. package/cli.js +122 -11
  4. package/docs/.mermaid-cache.json +1 -1
  5. package/docs/en/architecture.md +5 -5
  6. package/docs/en/commands-reference.md +29 -48
  7. package/docs/en/design-principles.md +12 -9
  8. package/docs/en/directory-conventions.md +26 -3
  9. package/docs/en/usage-guide.md +16 -10
  10. package/docs/en/usage-scenarios.md +3 -3
  11. package/docs//344/275/223/347/263/273/344/270/216/345/216/237/347/220/206.md +4 -4
  12. package/docs//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 +2 -2
  13. package/docs//344/275/277/347/224/250/350/257/264/346/230/216.md +15 -10
  14. package/docs//345/221/275/344/273/244/350/257/264/346/230/216.md +28 -51
  15. package/docs//347/233/256/345/275/225/344/270/216/350/267/257/345/276/204/347/272/246/345/256/232.md +26 -3
  16. package/docs//350/256/276/350/256/241/350/257/264/346/230/216.md +88 -57
  17. package/lib/claudeSettingsAdapter.js +99 -30
  18. package/lib/flow2specConfig.js +32 -6
  19. package/lib/init.js +264 -4
  20. package/package.json +2 -2
  21. package/templates/AGENTS.codex-stub.md +2 -0
  22. package/templates/AGENTS.md +18 -5
  23. package/templates/flow2spec.config.json +5 -2
  24. package/templates/hooks/f2s-config-inject.js +9 -147
  25. package/templates/hooks/f2s-config-session.js +95 -0
  26. package/templates/hooks/f2s-update-check.js +141 -0
  27. package/templates/knowledge/index.md +4 -4
  28. package/templates/knowledge/manifest-routing.json +34 -5
  29. package/templates/knowledge/template//347/273/210/347/250/277/346/250/241/347/211/210.md +2 -2
  30. package/templates/knowledge/topics/f2s-config-precheck.md +2 -2
  31. package/templates/knowledge/topics/f2s-fallback-triage.md +2 -2
  32. package/templates/knowledge/topics/f2s-stock-docs-vs-req-docs.md +3 -3
  33. package/templates/rules/f2s-config-check.mdc +3 -1
  34. package/templates/rules/f2s-flow2spec-unified-entry.mdc +21 -3
  35. package/templates/rules/f2s-implement-tech-design.mdc +1 -1
  36. package/templates/rules/f2s-karpathy-guidelines.mdc +1 -1
  37. package/templates/rules/f2s-stock-docs-vs-req-docs.mdc +3 -3
  38. package/templates/rules/f2s-topic-authoring.mdc +124 -0
  39. package/templates/skills/f2s-doc-arch/SKILL.md +37 -9
  40. package/templates/skills/f2s-doc-final/SKILL.md +5 -5
  41. package/templates/skills/f2s-git-commit/SKILL.md +21 -5
  42. package/templates/skills/{f2s-doc-add → f2s-kb-add}/SKILL.md +12 -7
  43. package/templates/skills/f2s-kb-addRules/SKILL.md +165 -0
  44. package/templates/skills/{f2s-ctx-build → f2s-kb-build}/SKILL.md +14 -9
  45. package/templates/skills/f2s-kb-feat/SKILL.md +8 -6
  46. package/templates/skills/f2s-kb-fix/SKILL.md +8 -6
  47. package/templates/skills/f2s-kb-migrate/SKILL.md +12 -10
  48. package/templates/skills/{f2s-ctx-rm → f2s-kb-rm}/SKILL.md +7 -5
  49. package/templates/skills/f2s-kb-sync/SKILL.md +13 -5
  50. package/templates/skills/f2s-kb-upgrade/SKILL.md +27 -11
  51. package/templates/skills/f2s-karpathy-guidelines/SKILL.md +0 -20
  52. package/templates/skills/stock-docs-vs-req-docs/SKILL.md +0 -35
package/lib/init.js CHANGED
@@ -21,6 +21,9 @@ const {
21
21
  } = require("./flow2specConfig");
22
22
  const { writeClaudeAgentHooks } = require("./claudeSettingsAdapter");
23
23
 
24
+ const KNOWLEDGE_TOPIC_TYPES = ["feature", "module", "config", "policy"];
25
+ const KNOWLEDGE_TOPIC_CONFIDENCE = ["manual", "inferred"];
26
+
24
27
  function ensureDir(dir) {
25
28
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
26
29
  }
@@ -103,6 +106,137 @@ function writeJson(filePath, data) {
103
106
  fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
104
107
  }
105
108
 
109
+ function readPackageName(templatesDir) {
110
+ try {
111
+ return readJson(path.join(templatesDir, "..", "package.json")).name;
112
+ } catch (_) {
113
+ return "@double-codeing/flow2spec";
114
+ }
115
+ }
116
+
117
+ function writeHookScriptWithPackageName(destDir, templatesDir, scriptName) {
118
+ const src = path.join(templatesDir, "hooks", scriptName);
119
+ if (!fs.existsSync(src)) return { written: false, reason: "missing-template" };
120
+ ensureDir(destDir);
121
+ let body = fs.readFileSync(src, "utf8");
122
+ body = body.replace(/__FLOW2SPEC_PACKAGE_NAME__/g, readPackageName(templatesDir));
123
+ fs.writeFileSync(path.join(destDir, scriptName), body, "utf8");
124
+ return { written: true };
125
+ }
126
+
127
+ function hasHookCommand(groups, fragment) {
128
+ if (!Array.isArray(groups)) return false;
129
+ return groups.some((group) =>
130
+ Array.isArray(group?.hooks) &&
131
+ group.hooks.some(
132
+ (hook) =>
133
+ hook &&
134
+ hook.type === "command" &&
135
+ String(hook.command || "").includes(fragment),
136
+ ),
137
+ );
138
+ }
139
+
140
+ function mergeCodexUpdateCheckHook(existing) {
141
+ const next =
142
+ existing && typeof existing === "object" && !Array.isArray(existing)
143
+ ? JSON.parse(JSON.stringify(existing))
144
+ : {};
145
+ if (!next.hooks || typeof next.hooks !== "object" || Array.isArray(next.hooks)) {
146
+ next.hooks = {};
147
+ }
148
+ if (!Array.isArray(next.hooks.SessionStart)) {
149
+ next.hooks.SessionStart = [];
150
+ }
151
+ if (hasHookCommand(next.hooks.SessionStart, "f2s-update-check")) {
152
+ return { config: next, changed: false };
153
+ }
154
+ next.hooks.SessionStart.push({
155
+ matcher: "startup|resume",
156
+ hooks: [
157
+ {
158
+ type: "command",
159
+ command: "node .codex/hooks/f2s-update-check.js",
160
+ statusMessage: "Checking Flow2Spec knowledge version",
161
+ },
162
+ ],
163
+ });
164
+ return { config: next, changed: true };
165
+ }
166
+
167
+ function writeCodexUpdateCheckHook(cwd, templatesDir) {
168
+ const codexRoot = path.join(cwd, ".codex");
169
+ const hooksDir = path.join(codexRoot, "hooks");
170
+ const scriptResult = writeHookScriptWithPackageName(
171
+ hooksDir,
172
+ templatesDir,
173
+ "f2s-update-check.js",
174
+ );
175
+
176
+ const hooksJsonPath = path.join(codexRoot, "hooks.json");
177
+ let existing = {};
178
+ if (fs.existsSync(hooksJsonPath)) {
179
+ try {
180
+ existing = readJson(hooksJsonPath);
181
+ } catch (_) {
182
+ existing = {};
183
+ }
184
+ }
185
+ const { config, changed } = mergeCodexUpdateCheckHook(existing);
186
+ if (changed || !fs.existsSync(hooksJsonPath)) {
187
+ writeJson(hooksJsonPath, config);
188
+ }
189
+ return { scriptResult, hooksJsonChanged: changed };
190
+ }
191
+
192
+ function mergeCursorUpdateCheckHook(existing) {
193
+ const next =
194
+ existing && typeof existing === "object" && !Array.isArray(existing)
195
+ ? JSON.parse(JSON.stringify(existing))
196
+ : {};
197
+ next.version = Number.isFinite(Number(next.version)) ? Number(next.version) : 1;
198
+ if (!next.hooks || typeof next.hooks !== "object" || Array.isArray(next.hooks)) {
199
+ next.hooks = {};
200
+ }
201
+ if (!Array.isArray(next.hooks.sessionStart)) {
202
+ next.hooks.sessionStart = [];
203
+ }
204
+ const exists = next.hooks.sessionStart.some((hook) =>
205
+ hook && String(hook.command || "").includes("f2s-update-check"),
206
+ );
207
+ if (exists) return { config: next, changed: false };
208
+ next.hooks.sessionStart.push({
209
+ command: "node .cursor/hooks/f2s-update-check.js",
210
+ timeout: 10,
211
+ });
212
+ return { config: next, changed: true };
213
+ }
214
+
215
+ function writeCursorUpdateCheckHook(cwd, templatesDir) {
216
+ const cursorRoot = path.join(cwd, ".cursor");
217
+ const hooksDir = path.join(cursorRoot, "hooks");
218
+ const scriptResult = writeHookScriptWithPackageName(
219
+ hooksDir,
220
+ templatesDir,
221
+ "f2s-update-check.js",
222
+ );
223
+
224
+ const hooksJsonPath = path.join(cursorRoot, "hooks.json");
225
+ let existing = {};
226
+ if (fs.existsSync(hooksJsonPath)) {
227
+ try {
228
+ existing = readJson(hooksJsonPath);
229
+ } catch (_) {
230
+ existing = {};
231
+ }
232
+ }
233
+ const { config, changed } = mergeCursorUpdateCheckHook(existing);
234
+ if (changed || !fs.existsSync(hooksJsonPath)) {
235
+ writeJson(hooksJsonPath, config);
236
+ }
237
+ return { scriptResult, hooksJsonChanged: changed };
238
+ }
239
+
106
240
  function buildDefaultMatcherPath(matcherId) {
107
241
  return `${KNOWLEDGE_ROOT}/matchers/${matcherId}.json`;
108
242
  }
@@ -175,7 +309,46 @@ function mergeTopicDependencies(templateDeps, existingDeps) {
175
309
  return out;
176
310
  }
177
311
 
178
- function buildMergedRouting(templateRouting, existingRouting) {
312
+ function normalizeTopicMetadataEntry(entry) {
313
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null;
314
+ if (!KNOWLEDGE_TOPIC_TYPES.includes(entry.primary)) return null;
315
+ const confidence =
316
+ typeof entry.confidence === "string" &&
317
+ KNOWLEDGE_TOPIC_CONFIDENCE.includes(entry.confidence)
318
+ ? entry.confidence
319
+ : null;
320
+ if (!confidence) return null;
321
+ const result = { primary: entry.primary, confidence };
322
+ if (Array.isArray(entry.tags) && entry.tags.length > 0) {
323
+ const validTags = dedupeStringArray(entry.tags).filter(
324
+ (t) =>
325
+ KNOWLEDGE_TOPIC_TYPES.includes(t) &&
326
+ t !== entry.primary,
327
+ );
328
+ if (validTags.length > 0) result.tags = validTags;
329
+ }
330
+ return result;
331
+ }
332
+
333
+ function mergeTopicMetadata(templateMetadata, existingMetadata, topicPaths) {
334
+ const out = {};
335
+ const topicIds = new Set(Object.keys(topicPaths || {}));
336
+ // existingMetadata 先写,templateMetadata 后写覆盖——模板优先
337
+ for (const metadata of [existingMetadata, templateMetadata]) {
338
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
339
+ continue;
340
+ }
341
+ for (const [topicId, entry] of Object.entries(metadata)) {
342
+ if (!topicIds.has(topicId)) continue;
343
+ const normalized = normalizeTopicMetadataEntry(entry);
344
+ if (!normalized) continue;
345
+ out[topicId] = normalized;
346
+ }
347
+ }
348
+ return out;
349
+ }
350
+
351
+ function buildMergedRouting(templateRouting, existingRouting, pkgVersion) {
179
352
  const mergedTaskRules = unionByKey(
180
353
  templateRouting.taskToTopicRules,
181
354
  existingRouting.taskToTopicRules,
@@ -200,7 +373,7 @@ function buildMergedRouting(templateRouting, existingRouting) {
200
373
  );
201
374
 
202
375
  const knownMerged = {
203
- version: templateRouting.version || existingRouting.version,
376
+ version: pkgVersion || templateRouting.version || existingRouting.version,
204
377
  knowledgeRoot:
205
378
  existingRouting.knowledgeRoot || templateRouting.knowledgeRoot,
206
379
  generatedFrom:
@@ -223,6 +396,14 @@ function buildMergedRouting(templateRouting, existingRouting) {
223
396
  },
224
397
  taskToTopicRules: mergedTaskRules,
225
398
  };
399
+ const mergedTopicMetadata = mergeTopicMetadata(
400
+ templateRouting.topicMetadata,
401
+ existingRouting.topicMetadata,
402
+ knownMerged.topicPaths,
403
+ );
404
+ if (Object.keys(mergedTopicMetadata).length > 0) {
405
+ knownMerged.topicMetadata = mergedTopicMetadata;
406
+ }
226
407
 
227
408
  const knownKeys = new Set(Object.keys(knownMerged));
228
409
  const extras = {};
@@ -416,7 +597,13 @@ function upgradeKnowledgeRoutingAndMatchers(cwd, templatesDir, options = {}) {
416
597
  const existingRouting = hadRouting ? readJson(routingPath) : {};
417
598
  const existingMatchers = hadMatchers ? readJson(matchersPath) : {};
418
599
 
419
- const mergedRouting = buildMergedRouting(templateRouting, existingRouting);
600
+ // 读包版本号,用于写入 manifest-routing.json version 字段
601
+ let pkgVersion;
602
+ try {
603
+ pkgVersion = readJson(path.join(templatesDir, "..", "package.json")).version;
604
+ } catch (_) {}
605
+
606
+ const mergedRouting = buildMergedRouting(templateRouting, existingRouting, pkgVersion);
420
607
  const mergedMatchers = buildMergedMatchers(
421
608
  templateMatchers,
422
609
  existingMatchers,
@@ -548,6 +735,59 @@ function validateKnowledgeRouting(cwd) {
548
735
  }
549
736
  }
550
737
 
738
+ if (routing.topicMetadata !== undefined) {
739
+ if (
740
+ !routing.topicMetadata ||
741
+ typeof routing.topicMetadata !== "object" ||
742
+ Array.isArray(routing.topicMetadata)
743
+ ) {
744
+ throw new Error("topicMetadata 必须是对象。");
745
+ }
746
+ for (const [topicId, metadata] of Object.entries(routing.topicMetadata)) {
747
+ if (!topicIds.has(topicId)) {
748
+ throw new Error(`topicMetadata 引用了不存在的 topic:${topicId}`);
749
+ }
750
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
751
+ throw new Error(`topicMetadata.${topicId} 必须是对象。`);
752
+ }
753
+ for (const key of Object.keys(metadata)) {
754
+ if (!["primary", "tags", "confidence"].includes(key)) {
755
+ throw new Error(`topicMetadata.${topicId} 包含未知字段:${key}`);
756
+ }
757
+ }
758
+ if (!KNOWLEDGE_TOPIC_TYPES.includes(metadata.primary)) {
759
+ throw new Error(
760
+ `topicMetadata.${topicId}.primary 非法:${metadata.primary}`,
761
+ );
762
+ }
763
+ if (!KNOWLEDGE_TOPIC_CONFIDENCE.includes(metadata.confidence)) {
764
+ throw new Error(
765
+ `topicMetadata.${topicId}.confidence 非法:${metadata.confidence}`,
766
+ );
767
+ }
768
+ if (metadata.tags !== undefined) {
769
+ if (!Array.isArray(metadata.tags)) {
770
+ throw new Error(`topicMetadata.${topicId}.tags 必须是数组。`);
771
+ }
772
+ const seenTags = new Set();
773
+ for (const tag of metadata.tags) {
774
+ if (!KNOWLEDGE_TOPIC_TYPES.includes(tag)) {
775
+ throw new Error(`topicMetadata.${topicId}.tags 包含非法值:${tag}`);
776
+ }
777
+ if (tag === metadata.primary) {
778
+ throw new Error(
779
+ `topicMetadata.${topicId}.tags 不应与 primary 重复:${tag}`,
780
+ );
781
+ }
782
+ if (seenTags.has(tag)) {
783
+ throw new Error(`topicMetadata.${topicId}.tags 包含重复值:${tag}`);
784
+ }
785
+ seenTags.add(tag);
786
+ }
787
+ }
788
+ }
789
+ }
790
+
551
791
  const matcherMap =
552
792
  matcherData?.matchers && typeof matcherData.matchers === "object"
553
793
  ? matcherData.matchers
@@ -666,9 +906,21 @@ function copySkills(cwd, agentRoot, templatesDir) {
666
906
  if (fs.existsSync(skillsSrc)) {
667
907
  const skillsDest = path.join(destRoot, "skills");
668
908
  ensureDir(skillsDest);
669
- for (const name of fs.readdirSync(skillsSrc)) {
909
+ const templateNames = new Set(fs.readdirSync(skillsSrc));
910
+ for (const name of templateNames) {
670
911
  copyRecursive(path.join(skillsSrc, name), path.join(skillsDest, name));
671
912
  }
913
+ // 删除配置根中以 f2s- 开头、但已不存在于 templates/skills/ 的旧 skill 目录
914
+ // 只清理 Flow2Spec 管理的 skill,不触碰用户自定义 skill
915
+ // LEGACY_SKILLS:非 f2s- 开头的历史旧名,也需一并清理
916
+ const LEGACY_SKILLS = new Set(["stock-docs-vs-req-docs"]);
917
+ if (fs.existsSync(skillsDest)) {
918
+ for (const name of fs.readdirSync(skillsDest)) {
919
+ if ((name.startsWith("f2s-") || LEGACY_SKILLS.has(name)) && !templateNames.has(name)) {
920
+ fs.rmSync(path.join(skillsDest, name), { recursive: true, force: true });
921
+ }
922
+ }
923
+ }
672
924
  }
673
925
  }
674
926
 
@@ -764,6 +1016,14 @@ async function run(cwd, agentIds, options = {}) {
764
1016
  claudeHooksResult.hookScriptWritten = result.hookScriptResult?.written ?? false;
765
1017
  claudeHooksResult.settingsChanged = result.settingsChanged;
766
1018
  }
1019
+ // Cursor:写入官方 hooks.json,使 sessionStart 自动运行更新检测。
1020
+ if (id === "cursor") {
1021
+ writeCursorUpdateCheckHook(cwd, templatesDir);
1022
+ }
1023
+ // Codex:写入官方 hooks.json,使 SessionStart 自动运行更新检测。
1024
+ if (id === "codex") {
1025
+ writeCodexUpdateCheckHook(cwd, templatesDir);
1026
+ }
767
1027
  }
768
1028
  return {
769
1029
  ids,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@double-codeing/flow2spec",
3
- "version": "3.0.19",
4
- "description": "在业务仓库初始化「文档驱动、可写回知识库」的 AI 协作骨架:项目根 .Knowledge 承载 stock-docs/req-docs 与机读路由,.cursor/.claude/.codex 写入 f2s-* 规则与技能(含 Karpathy 式编码行为准则 f2s-karpathy-guidelines,init 同步 rules / Codex topics / skills);init 只落结构与模板,业务内容由各 f2s-* 技能在对话中维护。",
3
+ "version": "3.1.1",
4
+ "description": "在业务仓库初始化「文档驱动、可写回知识库」的 AI 协作骨架:项目根 .Knowledge 承载 stock-docs/req-docs 与机读路由,.cursor/.claude/.codex 写入 f2s-* 规则与技能(含 Karpathy 式编码行为准则,init 同步 rules / Codex topics / skills);init 只落结构与模板,业务内容由各 f2s-* 技能在对话中维护。",
5
5
  "homepage": "https://github.com/Lands-1203/Flow2Spec#readme",
6
6
  "repository": {
7
7
  "type": "git",
@@ -14,6 +14,8 @@
14
14
  | --- | --- |
15
15
  | `skills/` | Flow2Spec 技能(`f2s-*`) |
16
16
  | `topics/` | 规则长文镜像(与 Cursor/Claude `rules` 同源) |
17
+ | `hooks.json` | Codex SessionStart hook 配置,用于启动时检测 Flow2Spec 知识库版本 |
18
+ | `hooks/` | hook 脚本目录 |
17
19
  | `config.toml` | 项目级 Codex 配置(若已创建) |
18
20
 
19
21
  配置真值:仓库根 **`flow2spec.config.json`**(须 Read);init 快照表见根 **`AGENTS.md`**。
@@ -30,6 +30,7 @@
30
30
  1. **必须先读机器索引**:优先读取 **`./.Knowledge/manifest-routing.json`** 做主题路由;按需依据 `taskToTopicRules[].matcherPath` 读取对应 matcher 分片(单文件,路径形如 **`./.Knowledge/matchers/<id>.json`**)取匹配词;无法命中时进入补召回阶段。
31
31
  - 若存在 `taskToTopicRules`,优先按任务规则路由主题。
32
32
  - 若命中主题含 `topicDependencies`,先读依赖主题再读主主题。
33
+ - 若存在 `topicMetadata`,仅将其中 `primary` / `tags` 作为阅读预期:`config` 关注配置项 / 开关 / 默认值;`policy` 关注正文中的必须 / 禁止 / 门禁 / 流程约束;`feature` 作为已落地能力背景;`module` 作为目录 / 包 / 模块边界背景。`topicMetadata` 不参与 matcher 命中,不决定是否读取 topic,不改变执行强制性;无明确分类证据时不写 metadata,并在摘要列为待确认。
33
34
  - `manifest` 仅通过 `f2s-*` 技能流程维护,不假设存在额外 CLI 命令。
34
35
  2. **人工索引按需读取**:仅在需要校验主题语义与边界时读取 **`./.Knowledge/index.md`**。
35
36
  - `index.md` 不是机读事实源,仅承担人读导航与语义边界说明。
@@ -49,7 +50,7 @@
49
50
  - 用户明确要求“全量检查/不要遗漏”。
50
51
  9. **禁止项**:
51
52
  - 禁止跳过 **`./.Knowledge/manifest-routing.json`**、按需 `matcherPath` 分片与 **`./.Knowledge/topics/`** 直接全仓检索或直接编码;**`./.Knowledge/index.md`** 按需读取,不可替代上述机读链。
52
- - 同一任务线内避免重复全文读取 **`./.Knowledge/manifest-routing.json`**(除非用户说明已通过 `f2s-ctx-build` / `f2s-kb-sync` / `f2s-doc-add` 等更新路由或知识、或手动改了 manifest;**勿将**仅执行 `flow2spec init` 当作业务知识库已更新);禁止为枚举而遍历整个 **`./.Knowledge/matchers/`**;禁止 **`./.Knowledge/index.md`** 与 routing 交替「刷清单」。
53
+ - 同一任务线内避免重复全文读取 **`./.Knowledge/manifest-routing.json`**(除非用户说明已通过 `f2s-kb-build` / `f2s-kb-sync` / `f2s-kb-add` 等更新路由或知识、或手动改了 manifest;**勿将**仅执行 `flow2spec init` 当作业务知识库已更新);禁止为枚举而遍历整个 **`./.Knowledge/matchers/`**;禁止 **`./.Knowledge/index.md`** 与 routing 交替「刷清单」。
53
54
  - 禁止把 **`./.Knowledge/stock-docs/`** 作为“按方案实现代码”的直接输入文档。
54
55
  - Flow2Spec 执行条令以 **`./AGENTS.md`**(完整)、**`./.codex/topics/f2s-*.md`** 与 **`./.codex/skills/`** 为准;**`.codex/AGENTS.md`** 仅为目录指针,不可替代根 `AGENTS.md`;勿使用仓库内 **非上述路径** 的同名条令文件作为执行依据,以免口径分叉。
55
56
  - 禁止把 `fallbackTopic` 当作最终命中直接实施改动;`fallbackTopic` 仅作安全兜底与澄清前置上下文。
@@ -61,7 +62,7 @@
61
62
  1. `./.Knowledge/manifest-routing.json`
62
63
  2. `./.Knowledge/matchers/<matcher>.json`(按需:通过 `taskToTopicRules[].matcherPath` 定位具体文件)
63
64
  3. `./.Knowledge/index.md`(按需,用于语义校验)
64
- 4. `./.Knowledge/topics/<topic>.md`(摘要;涉及统一入口、路由细则、`implement-tech-design` / `stock-docs-vs-req-docs` 等时,按需续读下文 **「专题长文」** 所列 `./.codex/topics/f2s-*.md`)
65
+ 4. `./.Knowledge/topics/<topic>.md`(摘要;涉及统一入口、路由细则、`implement-tech-design` / `f2s-doc-routing` 等时,按需续读下文 **「专题长文」** 所列 `./.codex/topics/f2s-*.md`)
65
66
  5. `./.Knowledge/stock-docs/<doc>.md`(按需)
66
67
  6. 业务代码(按需;路径以仓库内实际目录为准)
67
68
 
@@ -74,7 +75,7 @@
74
75
  ## 可用主题
75
76
 
76
77
  - 不在此处维护静态主题列表,避免与知识库演进漂移。
77
- - 每次任务均以 **`./.Knowledge/manifest-routing.json`** 的 `topicPaths`、`taskToTopicRules`、`fallbackTopic` 为唯一路由事实,并按每条规则的 `matcherPath` 读取 matcher 分片。
78
+ - 每次任务均以 **`./.Knowledge/manifest-routing.json`** 的 `topicPaths`、`taskToTopicRules`、`fallbackTopic` 为唯一路由事实,并按每条规则的 `matcherPath` 读取 matcher 分片;`topicMetadata` 只作治理与阅读预期,不是路由事实源。
78
79
  - 若路由清单与 **`./.Knowledge/index.md`** 语义不一致,以路由清单为准并提示用户同步修正。
79
80
 
80
81
  ## 专题长文(`./.codex/topics/`)
@@ -83,16 +84,28 @@
83
84
 
84
85
  - **统一入口**:`./.codex/topics/f2s-flow2spec-unified-entry.md`
85
86
  - **implement-tech-design**:`./.codex/topics/f2s-implement-tech-design.md`
86
- - **stock-docs-vs-req-docs**:`./.codex/topics/f2s-stock-docs-vs-req-docs.md`
87
+ - **f2s-doc-routing**:`./.codex/topics/f2s-stock-docs-vs-req-docs.md`
87
88
 
88
89
  同目录下另有:
89
90
 
90
91
  - **`./.codex/topics/f2s-knowledge-preflight.md`**:**普通提问**也须先 `Read` **`./.Knowledge/manifest-routing.json`** 再下钻代码;与统一入口并行时以本条「首工具调用」为准。
91
92
  - **`./.codex/topics/f2s-config-check.md`**:内容与上文「先 Read **`./flow2spec.config.json`**」一致并含 **changeTracking** 细表;**仅**在需核对细表时按需打开,不必与上列三条并列必读。
92
- - **`./.codex/topics/f2s-karpathy-guidelines.md`**:通用编码行为准则(先澄清、极简、手术式修改、可验证目标);与 f2s 路由/任务条令**并行**,硬冲突时以 **f2s 条令**为准。
93
93
 
94
94
  执行 Flow2Spec 相关任务时,先读本文件(**`./AGENTS.md`**)与 **`./.Knowledge/manifest-routing.json`**,再按需打开上列 **`./.codex/topics/*.md`** 文件。
95
95
 
96
+ ## 知识库版本自检(SessionStart 自动触发;每日首次,仅 updateCheck.enabled=true 时)
97
+
98
+ Codex 由 `flow2spec init codex` 写入 **`.codex/hooks.json`**,在 `SessionStart` 的 `startup|resume` 事件自动执行 `node .codex/hooks/f2s-update-check.js`;首次生成或 hook 内容变化后,Codex 可能要求通过 `/hooks` 审核并信任该项目 hook。
99
+
100
+ **规则层双保险**(若 hook 未运行,则按本节补检;与脚本缓存互为备份):
101
+
102
+ 1. 读 `flow2spec.config.json` → 若 `updateCheck.enabled` 不为 `true`,跳过,不做任何提示。
103
+ 2. 读 `.Knowledge/update-check.json` → 若文件存在且 `checkedAt` 与今日为同一自然日(`new Date(checkedAt).toDateString() === new Date().toDateString()`),**跳过,不执行脚本,不做任何提示**。
104
+ 3. 上述两步均未跳过时:执行 `node .codex/hooks/f2s-update-check.js`,解析标准输出的 JSON:
105
+ - 若含 `hookSpecificOutput.additionalContext`:**告知用户**该内容(建议执行 `/f2s-kb-upgrade`)。
106
+ - 无输出或解析失败:静默,不提示。
107
+ 4. 以上步骤出现任何错误,静默跳过,不影响正常对话。
108
+
96
109
  ## 可用 Flow2Spec 技能(自动生成)
97
110
 
98
111
  {{FLOW2SPEC_CODEX_SKILLS_SUMMARY}}
@@ -2,8 +2,11 @@
2
2
  "subAgent": false,
3
3
  "switchAgentVerification": false,
4
4
  "changeTracking": {
5
- "feat": false,
5
+ "feat": true,
6
6
  "fix": false,
7
- "implement": false
7
+ "implement": true
8
+ },
9
+ "updateCheck": {
10
+ "enabled": true
8
11
  }
9
12
  }
@@ -1,26 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
  /**
4
- * flow2spec PreToolUse hook在任何 f2s-* Skill 执行前自动注入 flow2spec.config.json。
4
+ * flow2spec PreToolUse guard仅在调用 f2s-* Skill 前提示必须先 Read flow2spec.config.json。
5
+ * 不在 PreToolUse 中反复注入完整配置;配置摘要由 SessionStart hook 一次性提供。
5
6
  * 由 flow2spec init --claude 写入 .claude/hooks/f2s-config-inject.js。
6
7
  */
7
- const fs = require('fs');
8
- const path = require('path');
9
-
10
- const DEFAULT_CFG = {
11
- subAgent: false,
12
- switchAgentVerification: false,
13
- changeTracking: { feat: false, fix: false, implement: false },
14
- };
15
-
16
- /** 与 lib/flow2specConfig.js 一致,避免 hook 依赖包内路径 */
17
- function normalizeBool(value, fallback) {
18
- if (value === true || value === 'true' || value === 1 || value === '1')
19
- return true;
20
- if (value === false || value === 'false' || value === 0 || value === '0')
21
- return false;
22
- return fallback;
23
- }
24
8
 
25
9
  function emitAdditionalContext(lines) {
26
10
  process.stdout.write(
@@ -29,85 +13,8 @@ function emitAdditionalContext(lines) {
29
13
  hookEventName: 'PreToolUse',
30
14
  additionalContext: lines.join('\n'),
31
15
  },
32
- }),
33
- );
34
- }
35
-
36
- function normalizeCfg(raw) {
37
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
38
- return { ...DEFAULT_CFG, changeTracking: { ...DEFAULT_CFG.changeTracking } };
39
- }
40
- const ct = raw.changeTracking;
41
- let changeTracking = { ...DEFAULT_CFG.changeTracking };
42
- if (typeof ct === 'boolean') {
43
- changeTracking = {
44
- feat: normalizeBool(ct, DEFAULT_CFG.changeTracking.feat),
45
- fix: normalizeBool(ct, DEFAULT_CFG.changeTracking.fix),
46
- implement: normalizeBool(ct, DEFAULT_CFG.changeTracking.implement),
47
- };
48
- } else if (ct && typeof ct === 'object' && !Array.isArray(ct)) {
49
- changeTracking = {
50
- feat: normalizeBool(ct.feat, DEFAULT_CFG.changeTracking.feat),
51
- fix: normalizeBool(ct.fix, DEFAULT_CFG.changeTracking.fix),
52
- implement: normalizeBool(ct.implement, DEFAULT_CFG.changeTracking.implement),
53
- };
54
- }
55
- const switchRaw = Object.prototype.hasOwnProperty.call(raw, 'switchAgentVerification')
56
- ? raw.switchAgentVerification
57
- : raw.subAgentVerification;
58
- return {
59
- subAgent: normalizeBool(raw.subAgent, DEFAULT_CFG.subAgent),
60
- switchAgentVerification: normalizeBool(
61
- switchRaw,
62
- DEFAULT_CFG.switchAgentVerification,
63
- ),
64
- changeTracking,
65
- };
66
- }
67
-
68
- function buildChangeTrackingLines(skillName, cfg) {
69
- const lines = [];
70
- const ctKeyMap = {
71
- 'f2s-kb-feat': 'feat',
72
- 'f2s-kb-fix': 'fix',
73
- 'f2s-implement-tech-design': 'implement',
74
- };
75
- const ctKey = ctKeyMap[skillName];
76
- if (ctKey === undefined) return lines;
77
- const ctValue = normalizeBool(
78
- (cfg.changeTracking ?? {})[ctKey],
79
- DEFAULT_CFG.changeTracking[ctKey],
16
+ }) + '\n',
80
17
  );
81
- lines.push('');
82
- if (ctValue) {
83
- const stepMap = {
84
- feat: '步骤 0',
85
- fix: '步骤 0',
86
- implement: '步骤 2.5(写入任务清单)、2.6(随步打钩 task.md)、步骤 5(归档门禁后归档)',
87
- };
88
- lines.push(
89
- `changeTracking.${ctKey}=true → 本技能变更追踪【必须执行】:在 ${stepMap[ctKey]} 创建或续作 .task/active/ 任务,禁止跳过。`,
90
- );
91
- } else {
92
- lines.push(
93
- `changeTracking.${ctKey}=false → 本技能变更追踪步骤跳过,不得创建 .task/ 目录。`,
94
- );
95
- }
96
- return lines;
97
- }
98
-
99
- function buildCoreLines(cfg, sourceLabel) {
100
- const lines = [
101
- `[flow2spec] ${sourceLabel}`,
102
- ` subAgent = ${cfg.subAgent}`,
103
- ` switchAgentVerification = ${cfg.switchAgentVerification}`,
104
- ` changeTracking = ${JSON.stringify(cfg.changeTracking)}`,
105
- '',
106
- 'subAgent=true → 按技能 SKILL.md 中 B/C 模式派子 agent 并行扫描,主 agent 合并落盘。',
107
- 'subAgent=false → 全部在主 agent 内完成,不派子 agent。',
108
- 'switchAgentVerification=true → 子 agent 落盘的由主 agent 校验;主 agent 落盘的由子 agent 校验(须 subAgent=true 且实际拆出子任务)。',
109
- ];
110
- return lines;
111
18
  }
112
19
 
113
20
  const chunks = [];
@@ -116,7 +23,7 @@ process.stdin.on('end', () => {
116
23
  let skillName = '';
117
24
  try {
118
25
  const input = JSON.parse(Buffer.concat(chunks).toString('utf8'));
119
- skillName = String(input?.tool_input?.skill || '');
26
+ skillName = String(input?.tool_input?.skill || input?.tool_input?.name || '');
120
27
  } catch (_err) {
121
28
  process.exit(0);
122
29
  return;
@@ -127,55 +34,10 @@ process.stdin.on('end', () => {
127
34
  return;
128
35
  }
129
36
 
130
- const configPath = path.resolve(process.cwd(), 'flow2spec.config.json');
131
-
132
- try {
133
- if (!fs.existsSync(configPath)) {
134
- const cfg = { ...DEFAULT_CFG, changeTracking: { ...DEFAULT_CFG.changeTracking } };
135
- const lines = [
136
- '[flow2spec] 项目根不存在 flow2spec.config.json;按统一入口约定,缺失字段均视为 false。',
137
- '',
138
- ...buildCoreLines(cfg, '等价配置预览(建议随后 Read 或 flow2spec init 补齐该文件):'),
139
- '',
140
- '可执行:flow2spec init(或从包模板复制 flow2spec.config.json 到项目根)。',
141
- ...buildChangeTrackingLines(skillName, cfg),
142
- ];
143
- emitAdditionalContext(lines);
144
- process.exit(0);
145
- return;
146
- }
147
-
148
- let raw;
149
- try {
150
- raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
151
- } catch (parseErr) {
152
- const cfg = { ...DEFAULT_CFG, changeTracking: { ...DEFAULT_CFG.changeTracking } };
153
- const lines = [
154
- `[flow2spec] flow2spec.config.json 存在但 JSON 解析失败:${parseErr.message || String(parseErr)}`,
155
- '在修复文件前,请按以下默认安全语义执行本技能(与「文件不存在」一致):',
156
- '',
157
- ...buildCoreLines(cfg, '等价配置(解析失败回退)'),
158
- ...buildChangeTrackingLines(skillName, cfg),
159
- ];
160
- emitAdditionalContext(lines);
161
- process.exit(0);
162
- return;
163
- }
164
-
165
- const cfg = normalizeCfg(raw);
166
- const lines = [
167
- ...buildCoreLines(cfg, 'flow2spec.config.json 已自动注入,执行本 f2s-* 技能前必须遵守以下配置:'),
168
- '',
169
- '仍建议用 Read(flow2spec.config.json) 与磁盘核对,尤其在刚编辑过该文件时。',
170
- ...buildChangeTrackingLines(skillName, cfg),
171
- ];
172
- emitAdditionalContext(lines);
173
- } catch (err) {
174
- const msg = err && err.message ? err.message : String(err);
175
- emitAdditionalContext([
176
- `[flow2spec] f2s-config-inject hook 未预期异常:${msg}`,
177
- '请手动 Read("flow2spec.config.json") 后再执行本技能;若持续失败请检查 hook 脚本与项目根路径。',
178
- ]);
179
- }
37
+ emitAdditionalContext([
38
+ `[flow2spec] 即将调用 ${skillName}。进入该 Skill 正文前,首个动作必须 Read("flow2spec.config.json")。`,
39
+ 'SessionStart 中的配置摘要仅作提醒;若摘要与磁盘不一致,以本次 Read 结果为准。',
40
+ '读取后再按 subAgent / switchAgentVerification / changeTracking 的实际值执行后续步骤。',
41
+ ]);
180
42
  process.exit(0);
181
43
  });