@double-codeing/flow2spec 2.2.3 → 3.0.8

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 (61) hide show
  1. package/README.md +77 -53
  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-/350/256/276/350/256/241/350/257/264/346/230/216.md +574 -0
  5. package/docs/Flow2Spec/344/275/277/347/224/250/350/257/264/346/230/216.md +116 -76
  6. package/docs/README-/344/275/223/347/263/273/344/270/216/345/216/237/347/220/206.md +85 -44
  7. package/docs/README-/345/221/275/344/273/244/350/257/264/346/230/216.md +548 -79
  8. 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
  9. package/lib/agents.js +15 -3
  10. package/lib/claudeSettingsAdapter.js +114 -0
  11. package/lib/codexAgentsAdapter.js +70 -0
  12. package/lib/flow2specConfig.js +229 -0
  13. package/lib/init.js +698 -25
  14. package/package.json +2 -2
  15. package/templates/AGENTS.md +98 -0
  16. package/templates/flow2spec.config.json +9 -0
  17. package/templates/hooks/f2s-config-inject.js +181 -0
  18. package/templates/knowledge/index.md +68 -0
  19. package/templates/knowledge/manifest-matchers.json +35 -0
  20. package/templates/knowledge/manifest-routing.json +45 -0
  21. package/templates/knowledge/matchers/m-doc-routing.json +11 -0
  22. package/templates/knowledge/matchers/m-f2s-config-precheck.json +15 -0
  23. package/templates/knowledge/matchers/m-implement-from-spec.json +10 -0
  24. 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
  25. package/templates/{template → knowledge/template}//347/273/210/347/250/277/346/250/241/347/211/210.md +5 -4
  26. package/templates/knowledge/topics/f2s-config-precheck.md +24 -0
  27. package/templates/knowledge/topics/f2s-fallback-triage.md +60 -0
  28. package/templates/knowledge/topics/f2s-implement-tech-design.md +21 -0
  29. package/templates/knowledge/topics/f2s-stock-docs-vs-req-docs.md +25 -0
  30. package/templates/rules/f2s-config-check.mdc +35 -0
  31. package/templates/rules/f2s-flow2spec-unified-entry.mdc +88 -0
  32. package/templates/rules/f2s-implement-tech-design.mdc +144 -0
  33. package/templates/rules/f2s-karpathy-guidelines.mdc +77 -0
  34. package/templates/rules/f2s-knowledge-preflight.mdc +70 -0
  35. package/templates/rules/f2s-stock-docs-vs-req-docs.mdc +16 -0
  36. package/templates/rules/f2s-task.mdc +202 -0
  37. package/templates/skills/f2s-ctx-build/SKILL.md +74 -173
  38. package/templates/skills/f2s-ctx-rm/SKILL.md +39 -43
  39. package/templates/skills/f2s-doc-add/SKILL.md +69 -106
  40. package/templates/skills/f2s-doc-arch/SKILL.md +20 -9
  41. package/templates/skills/f2s-doc-final/SKILL.md +29 -21
  42. package/templates/skills/f2s-doc-pdf/SKILL.md +17 -10
  43. package/templates/skills/f2s-git-commit/SKILL.md +189 -0
  44. package/templates/skills/f2s-karpathy-guidelines/SKILL.md +20 -0
  45. package/templates/skills/f2s-kb-feat/SKILL.md +72 -50
  46. package/templates/skills/f2s-kb-fix/SKILL.md +77 -46
  47. package/templates/skills/f2s-kb-merge/SKILL.md +9 -0
  48. package/templates/skills/f2s-kb-migrate/SKILL.md +356 -0
  49. package/templates/skills/f2s-kb-sync/SKILL.md +80 -59
  50. package/templates/skills/f2s-kb-upgrade/SKILL.md +225 -0
  51. package/templates/skills/f2s-req-backend/SKILL.md +35 -12
  52. package/templates/skills/f2s-req-clarify/SKILL.md +10 -2
  53. package/templates/skills/f2s-req-plan/SKILL.md +110 -0
  54. package/templates/skills/stock-docs-vs-req-docs/SKILL.md +10 -4
  55. package/docs/images//345/216/237/347/220/206/345/233/2761.png +0 -0
  56. package/docs/images//345/216/237/347/220/206/345/233/2762.png +0 -0
  57. package/docs/images//345/221/275/344/273/244/346/230/216/347/273/206/345/233/276.png +0 -0
  58. 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
  59. package/docs/images//347/256/200/350/277/260/345/233/276.png +0 -0
  60. package/templates/rules/implement-tech-design.mdc +0 -177
  61. package/templates/rules/stock-docs-vs-req-docs.mdc +0 -14
@@ -1,76 +1,47 @@
1
1
  # 目录与路径约定
2
2
 
3
- **配置根**:`flow2spec init` 写入的目录(默认 **`.cursor/`**,亦可 **`.claude/`**、**`.codex/`**)。下文以 **`.cursor/`** 为例,其它 agent 将 `.cursor` 换成对应目录名即可。
3
+ ## 核心边界
4
4
 
5
- **文档**:[Flow2Spec使用说明](./Flow2Spec使用说明.md) · [README-命令说明](./README-命令说明.md) · [README-体系与原理](./README-体系与原理.md) · [Flow2Spec-使用案例-模拟对话](./Flow2Spec-使用案例-模拟对话.md)
6
-
7
- | 路径(逻辑) | 示例(Cursor) | 说明 |
8
- |--------------|----------------|------|
9
- | **stock-docs/** | `.cursor/stock-docs/` | 存量源文档(终稿/初稿/架构等)→ **f2s-ctx-build**;**f2s-doc-arch**(架构初稿)、**f2s-doc-add**(**工作中**已落地能力、多文件聚合进上下文)的初稿/终稿亦在此,**勿**与 **req-docs** 混用 |
10
- | **req-docs/** | `.cursor/req-docs/` | 技术方案、澄清文档等 → **implement-tech-design** 按方案写代码 |
11
- | **rules/** | `.cursor/rules/` | **main.mdc**(唯一 alwaysApply)+ 专题 `*-context.mdc` |
12
- | **skills/** | `.cursor/skills/` | 各 `SKILL.md` |
13
- | **template/** | `.cursor/template/` | 终稿模版、后端技术模版 |
14
- | **docs-index.md** | `.cursor/docs-index.md` | 文档 ↔ Rules / Skills 索引表 |
15
-
16
- **init**:创建上表目录并复制模板。**升级**:旧名 `docs/` 可改名为 `stock-docs/`;`req-docs` 须在**配置根内**,勿与 `.cursor` 同级。
17
-
18
- ### 1.1 Cursor 与 Claude Code:`rules/` 扩展名
19
-
20
- | 配置根 | 规则文件扩展名 | 路径范围(frontmatter) | 说明 |
21
- |--------|----------------|-------------------------|------|
22
- | **`.cursor/`**(Cursor) | **`.mdc`** | **`globs:`** + **`alwaysApply:`** | Cursor 约定 |
23
- | **`.claude/`**(Claude Code) | **`.md`** | **`paths:`**(无 `paths` 则与会话同载) | Claude Code 不识别 `.mdc`;`flow2spec init claude` 由模板自动转换 |
24
-
25
- 在 **`.claude/`** 下手工维护规则时,请使用 **`.md`** 与 **`paths:`**;勿复制 Cursor 的 **`globs:`** / **`.mdc`** 以免不生效。Claude Code **不会**把 **`.mdx`** 当作项目规则加载;请勿用 **`.mdx`** 作为 `rules/` 内规则扩展名。
5
+ - `.Knowledge/`:只放业务知识文档与索引
6
+ - `配置根`(`.cursor/.claude/.codex`):放规则与技能入口
26
7
 
27
8
  ---
28
9
 
29
- ## 2. 链接写法(生成 Rule / Skill / docs-index 时必守)
30
-
31
- **rules/**、**skills/**、**docs-index.md** 指向 **stock-docs/** 的 **href** 必须如下(否则链接断):
32
-
33
- | 写入位置 | 链接 href |
34
- |----------|-----------|
35
- | `rules/*.mdc` | `../stock-docs/<文件名>.md` |
36
- | `skills/<主题>/SKILL.md` | `../../stock-docs/<文件名>.md` |
37
- | `docs-index.md` | `stock-docs/<文件名>.md`(无 `../`) |
38
- | **sourceDoc**(frontmatter) | **`<配置根>/stock-docs/<文件名>.md`**(如 `.cursor/stock-docs/xxx.md`) |
39
-
40
- **禁止**:Rule/Skill/docs-index **req-docs** 当 stock-docs 链出;docs-index 的 href 写成 `../stock-docs/` 或裸绝对路径。
41
-
42
- **记忆**:链出只认 **stock-docs**;Rule `../`,Skill `../../`,索引 `stock-docs/`;sourceDoc 用配置根全路径。
10
+ ## 目录职责
11
+
12
+ | 路径 | 职责 |
13
+ | --- | --- |
14
+ | `.Knowledge/stock-docs/` | 架构、终稿、沉淀文档 |
15
+ | `.Knowledge/req-docs/` | 需求澄清、技术方案 |
16
+ | `.Knowledge/topics/` | 主题路由文档(用于规则与流程执行) |
17
+ | `.Knowledge/template/` | 终稿/技术方案模板 |
18
+ | `.Knowledge/index.md` | 人类可读索引 |
19
+ | `.Knowledge/manifest-routing.json` | 机器可读路由骨架(task/topic/dependencies) |
20
+ | `.Knowledge/matchers/*.json` | 关键词分片(`id/includeAny`),由 `manifest-routing.taskToTopicRules[].matcherPath` 直链指向 |
21
+ | `.Knowledge/migration-report.md` | `f2s-kb-migrate` 落盘的迁移对照表与拟删除路径列表 |
22
+ | `.task/` | 变更追踪任务清单目录(`active/` 进行中,`completed/` 已归档且目录名为 **`<YYYYMMDD>-<task-name>`**(日期在前),`todo.json` 活跃任务索引);仅当 `changeTracking.*` 为 `true` 或显式调用 `f2s-req-plan` 时创建 |
23
+ | `配置根/rules/` | 规则文件(Cursor `.mdc`,Claude `.md`) |
24
+ | `配置根/skills/` | 技能定义(`SKILL.md`) |
25
+ | `配置根/template/` | (废弃)不再写入;历史目录可清理 |
26
+ | `.codex/AGENTS.md` | Codex 统一入口与加载说明 |
27
+ | `flow2spec.config.json` | 项目根配置,控制 `subAgent`、`switchAgentVerification`、`changeTracking`(嵌套对象,含 `feat` / `fix` / `implement` 三个子项) |
28
+
29
+ > 多端提示与路径表见 [Flow2Spec使用说明 § 一](./Flow2Spec使用说明.md)(详表单点维护);**权威仍为 Read(`flow2spec.config.json`)**。
43
30
 
44
31
  ---
45
32
 
46
- ## 3. 文档产物阶段(均在 stock-docs/)
47
-
48
- | 阶段 | 含义 | 典型名 |
49
- |------|------|--------|
50
- | 原稿 | 未纳入体系前的材料 | 任意 PDF、杂乱 MD |
51
- | 初稿 | **f2s-doc-final**(PDF 首次)、**f2s-doc-arch**(架构说明),或 **f2s-doc-add**(**工作中**把**已落地能力**从多文件聚合成的初稿) | `*_初稿.md`、`*架构说明_初稿.md` 等;**f2s-doc-add** 与 **f2s-doc-arch** 分工见各自 `SKILL.md` |
52
- | 终稿 | **f2s-doc-final** 规范输出 | `*_终稿.md` → **f2s-ctx-build** 入参 |
33
+ ## 路径约束
53
34
 
54
- Rules/Skills **文件名不带 `_终稿`**。
35
+ 1. `.Knowledge/topics` 是知识路由主题层,允许并鼓励通过 `f2s-*` 技能维护。
36
+ 2. `f2s-ctx-build` 从 `.Knowledge/stock-docs` 读,更新 `.Knowledge/topics`、`.Knowledge/index.md`、`.Knowledge/manifest-routing.json`、`.Knowledge/matchers/*.json`。
37
+ 3. 实现类任务统一读取 `.Knowledge/req-docs/*.md`。
38
+ 4. `manifest-routing.json` 与 `matchers/*.json` 由 `f2s-*` 技能流程维护;不再使用 `.Knowledge/manifest-matchers.json`(`flow2spec init` 会删除遗留文件)。
55
39
 
56
40
  ---
57
41
 
58
- ## 4. 版本管理(sourceDoc 与 generatedAt)
59
-
60
- 每条 Rule、Skill 的 frontmatter:**sourceDoc**(同上表)、**generatedAt**(东八区 ISO 8601,如 `2026-01-28T20:00:00+08:00`)。
61
- **索源**:产物看 `sourceDoc`;文档看 **docs-index** 对应行;更新对同路径再跑 **f2s-ctx-build**。用法见 [README-体系与原理 §5](./README-体系与原理.md#5-版本管理与索源)。
62
-
63
- ---
64
-
65
- ## 5. template/
66
-
67
- 包内 **templates/template/** → init 复制到 **配置根/template/**。**f2s-doc-final** 读 `template/终稿模版.md`;**f2s-req-backend** 参考 `template/后端技术模版.md`。再次 init **覆盖** `template/`。
68
-
69
- ---
70
-
71
- ## 6. 小结
72
-
73
- - **stock-docs** = 上下文源;**req-docs** = 实现用方案。
74
- - 链接层级见 §2;产物阶段见 §3;版本字段见 §4。
42
+ ## 相关文档
75
43
 
76
- **相关文档**:[Flow2Spec使用说明](./Flow2Spec使用说明.md) | [Flow2Spec-使用案例-模拟对话](./Flow2Spec-使用案例-模拟对话.md) | [README-命令说明](./README-命令说明.md) | [README-体系与原理](./README-体系与原理.md)
44
+ - [Flow2Spec使用说明](./Flow2Spec使用说明.md)
45
+ - [README-命令说明](./README-命令说明.md)
46
+ - [README-体系与原理](./README-体系与原理.md)
47
+ - [Flow2Spec-使用案例-模拟对话](./Flow2Spec-使用案例-模拟对话.md)
package/lib/agents.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * flow2spec init 支持的 AI 工具配置目录。
3
- * 模板以相同子目录结构写入各配置根:rules/、skills/、stock-docs/、req-docs/、template/
3
+ * 知识库统一写入项目根 `.Knowledge/`(含 template),rules/skills 保留在各配置根。
4
4
  */
5
5
  const AGENTS = {
6
6
  cursor: { root: ".cursor", label: "Cursor" },
@@ -8,7 +8,13 @@ const AGENTS = {
8
8
  codex: { root: ".codex", label: "Codex" },
9
9
  };
10
10
 
11
- const SUBDIRS = ["stock-docs", "req-docs", "template", "rules", "skills"];
11
+ const KNOWLEDGE_ROOT = ".Knowledge";
12
+ const KNOWLEDGE_SUBDIRS = ["stock-docs", "req-docs", "matchers"];
13
+ const AGENT_SUBDIRS = {
14
+ cursor: ["rules", "skills"],
15
+ claude: ["rules", "skills"],
16
+ codex: ["skills"],
17
+ };
12
18
 
13
19
  /**
14
20
  * @param {string[]} argv init 后的参数,如 []、['cursor']、['cursor','claude']
@@ -32,4 +38,10 @@ function normalizeAgentIds(argv) {
32
38
  return out;
33
39
  }
34
40
 
35
- module.exports = { AGENTS, SUBDIRS, normalizeAgentIds };
41
+ module.exports = {
42
+ AGENTS,
43
+ KNOWLEDGE_ROOT,
44
+ KNOWLEDGE_SUBDIRS,
45
+ AGENT_SUBDIRS,
46
+ normalizeAgentIds,
47
+ };
@@ -0,0 +1,114 @@
1
+ 'use strict';
2
+ /**
3
+ * 负责合并 flow2spec hook 配置到 .claude/settings.json。
4
+ * 仅在 init --claude 时调用。
5
+ */
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ const HOOK_COMMAND = 'node .claude/hooks/f2s-config-inject.js';
10
+
11
+ /**
12
+ * 判断 PreToolUse 数组里是否已存在 f2s-config-inject hook。
13
+ * @param {Array} preToolUseArr
14
+ * @returns {boolean}
15
+ */
16
+ function hasF2sHook(preToolUseArr) {
17
+ if (!Array.isArray(preToolUseArr)) return false;
18
+ for (const group of preToolUseArr) {
19
+ if (!group || !Array.isArray(group.hooks)) continue;
20
+ for (const h of group.hooks) {
21
+ if (h && h.type === 'command' && String(h.command || '').includes('f2s-config-inject')) {
22
+ return true;
23
+ }
24
+ }
25
+ }
26
+ return false;
27
+ }
28
+
29
+ /**
30
+ * 将 f2s PreToolUse hook 合并进现有 settings,返回新对象(不修改原对象)。
31
+ * @param {object} existing 现有 settings(可为 {})
32
+ * @returns {object}
33
+ */
34
+ function mergeF2sHook(existing) {
35
+ const next = JSON.parse(JSON.stringify(existing || {}));
36
+ if (!next.hooks) next.hooks = {};
37
+ if (!next.hooks.PreToolUse) next.hooks.PreToolUse = [];
38
+
39
+ if (hasF2sHook(next.hooks.PreToolUse)) {
40
+ return { settings: next, changed: false };
41
+ }
42
+
43
+ next.hooks.PreToolUse.push({
44
+ matcher: 'Skill',
45
+ hooks: [{ type: 'command', command: HOOK_COMMAND }],
46
+ });
47
+
48
+ return { settings: next, changed: true };
49
+ }
50
+
51
+ /**
52
+ * 读取 .claude/settings.json(不存在则返回 {})。
53
+ * @param {string} claudeRoot .claude 目录绝对路径
54
+ * @returns {object}
55
+ */
56
+ function readSettings(claudeRoot) {
57
+ const settingsPath = path.join(claudeRoot, 'settings.json');
58
+ if (!fs.existsSync(settingsPath)) return {};
59
+ try {
60
+ return JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
61
+ } catch (_err) {
62
+ return {};
63
+ }
64
+ }
65
+
66
+ /**
67
+ * 写入 .claude/settings.json。
68
+ * @param {string} claudeRoot
69
+ * @param {object} settings
70
+ */
71
+ function writeSettings(claudeRoot, settings) {
72
+ const settingsPath = path.join(claudeRoot, 'settings.json');
73
+ fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, 'utf8');
74
+ }
75
+
76
+ /**
77
+ * 将 templates/hooks/f2s-config-inject.js 复制到 .claude/hooks/。
78
+ * @param {string} claudeRoot
79
+ * @param {string} templatesDir
80
+ */
81
+ function copyHookScript(claudeRoot, templatesDir) {
82
+ const src = path.join(templatesDir, 'hooks', 'f2s-config-inject.js');
83
+ if (!fs.existsSync(src)) return { written: false, reason: 'missing-template' };
84
+
85
+ const hooksDir = path.join(claudeRoot, 'hooks');
86
+ if (!fs.existsSync(hooksDir)) fs.mkdirSync(hooksDir, { recursive: true });
87
+
88
+ const dest = path.join(hooksDir, 'f2s-config-inject.js');
89
+ fs.copyFileSync(src, dest);
90
+ return { written: true };
91
+ }
92
+
93
+ /**
94
+ * 主入口:为 claude agent 配置 f2s PreToolUse hook。
95
+ * @param {string} cwd 项目根目录
96
+ * @param {string} templatesDir flow2spec 包 templates 目录
97
+ * @returns {{ hookScriptResult, settingsChanged }}
98
+ */
99
+ function writeClaudeAgentHooks(cwd, templatesDir) {
100
+ const claudeRoot = path.join(cwd, '.claude');
101
+ if (!fs.existsSync(claudeRoot)) fs.mkdirSync(claudeRoot, { recursive: true });
102
+
103
+ const hookScriptResult = copyHookScript(claudeRoot, templatesDir);
104
+
105
+ const existing = readSettings(claudeRoot);
106
+ const { settings, changed } = mergeF2sHook(existing);
107
+ if (changed) {
108
+ writeSettings(claudeRoot, settings);
109
+ }
110
+
111
+ return { hookScriptResult, settingsChanged: changed };
112
+ }
113
+
114
+ module.exports = { writeClaudeAgentHooks, mergeF2sHook };
@@ -0,0 +1,70 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ function readSkillSummary(skillsDir) {
5
+ if (!fs.existsSync(skillsDir)) return [];
6
+ const out = [];
7
+ for (const name of fs.readdirSync(skillsDir)) {
8
+ const skillFile = path.join(skillsDir, name, "SKILL.md");
9
+ if (!fs.existsSync(skillFile)) continue;
10
+ const raw = fs.readFileSync(skillFile, "utf8");
11
+ const frontmatter = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
12
+ const body = frontmatter ? frontmatter[1] : "";
13
+ const skillName = (body.match(/^\s*name:\s*(.+)\s*$/m) || [])[1] || name;
14
+ const desc = (body.match(/^\s*description:\s*(.+)\s*$/m) || [])[1] || "暂无描述";
15
+ out.push(`- \`${skillName.trim()}\`:${desc.trim()}`);
16
+ }
17
+ return out.sort((a, b) => a.localeCompare(b, "zh-Hans-CN"));
18
+ }
19
+
20
+ function renderProjectConfigBlock(projectConfig) {
21
+ const c = projectConfig || {};
22
+ const subAgent = Boolean(c.subAgent);
23
+ const switchAgentVerification = Boolean(c.switchAgentVerification);
24
+ const ct = c.changeTracking || {};
25
+ const feat = Boolean(ct.feat);
26
+ const fix = Boolean(ct.fix);
27
+ const implement = Boolean(ct.implement);
28
+ return [
29
+ "| 配置项 | 当前值 | 说明 |",
30
+ "| --- | --- | --- |",
31
+ "| `subAgent` | " +
32
+ String(subAgent) +
33
+ " | 技能规定用子 agent 的步骤:`true` 执行,`false` 全在主会话。用户「动态判断谁用子 agent」**仅当本项为 true** 时有效,否则该说明失效。各 f2s 阶段细则见技能正文(模板未统一写死)。 |",
34
+ "| `switchAgentVerification` | " +
35
+ String(switchAgentVerification) +
36
+ " | **切换 agent 校验**:`false` 时落盘侧同会话内验(子写子验、主写主验)。`true` 且技能写明依赖本项时交叉验:子落盘→主验,主落盘→子验;无子 agent(如 `subAgent` false)则主落盘→子验不发生、全主验。旧键 `subAgentVerification` 仍可被解析。 |",
37
+ "| `changeTracking.feat` | " +
38
+ String(feat) +
39
+ " | `true` → `f2s-kb-feat` **步骤 0** 必须创建/续作 `.task/active/` 变更追踪任务;`false` → 跳过,不创建 `.task/` 目录。 |",
40
+ "| `changeTracking.fix` | " +
41
+ String(fix) +
42
+ " | `true` → `f2s-kb-fix` **步骤 0** 必须创建/续作 `.task/active/` 变更追踪任务;`false` → 跳过。 |",
43
+ "| `changeTracking.implement` | " +
44
+ String(implement) +
45
+ " | `true` → `f2s-implement-tech-design` **步骤 2.5** 写入任务清单、**步骤 5** 归档完成;`false` → 跳过。 |",
46
+ ].join("\n");
47
+ }
48
+
49
+ function renderCodexAgents(templateBody, skillsSummaryLines, projectConfig) {
50
+ const summary =
51
+ skillsSummaryLines.length > 0
52
+ ? skillsSummaryLines.join("\n")
53
+ : "- 当前未发现可用技能。";
54
+ let body = templateBody.replace("{{FLOW2SPEC_CODEX_SKILLS_SUMMARY}}", summary);
55
+ body = body.replace(
56
+ "{{FLOW2SPEC_PROJECT_CONFIG}}",
57
+ renderProjectConfigBlock(projectConfig),
58
+ );
59
+ return body;
60
+ }
61
+
62
+ function buildCodexAgentsMd(templatesDir, projectConfig) {
63
+ const templatePath = path.join(templatesDir, "AGENTS.md");
64
+ const skillsDir = path.join(templatesDir, "skills");
65
+ const templateBody = fs.readFileSync(templatePath, "utf8");
66
+ const skillLines = readSkillSummary(skillsDir);
67
+ return renderCodexAgents(templateBody, skillLines, projectConfig);
68
+ }
69
+
70
+ module.exports = { buildCodexAgentsMd, renderProjectConfigBlock };
@@ -0,0 +1,229 @@
1
+ const path = require("path");
2
+ const fs = require("fs");
3
+
4
+ const CONFIG_FILENAME = "flow2spec.config.json";
5
+
6
+ const DEFAULTS = {
7
+ subAgent: false,
8
+ // switchAgentVerification:false=落盘侧同会话内验;true+技能绑定=交叉验(子落盘主验/主落盘子验)
9
+ switchAgentVerification: false,
10
+ changeTracking: {
11
+ feat: false,
12
+ fix: false,
13
+ implement: false,
14
+ },
15
+ };
16
+
17
+ /**
18
+ * 所有已知配置字段描述,供 init 交互提示使用。
19
+ * 新增字段在此追加,cli.js 会自动对缺失字段发起提问。
20
+ * 支持点号分隔的嵌套键,如 "changeTracking.feat"(对应 { changeTracking: { feat: ... } })。
21
+ */
22
+ const CONFIG_FIELDS = [
23
+ {
24
+ key: "subAgent",
25
+ type: "boolean",
26
+ default: false,
27
+ question: "启用子 Agent 并行执行?(适合大型项目;小项目建议默认 N)",
28
+ },
29
+ {
30
+ key: "switchAgentVerification",
31
+ type: "boolean",
32
+ default: false,
33
+ question: "启用交叉验证(子 agent 落盘 → 主 agent 验;需配合技能使用)",
34
+ },
35
+ {
36
+ key: "changeTracking.feat",
37
+ type: "boolean",
38
+ default: false,
39
+ question: "启用变更追踪 - f2s-kb-feat(新增能力时创建可续作的任务清单)?",
40
+ },
41
+ {
42
+ key: "changeTracking.fix",
43
+ type: "boolean",
44
+ default: false,
45
+ question: "启用变更追踪 - f2s-kb-fix(修正能力时创建可续作的任务清单)?",
46
+ },
47
+ {
48
+ key: "changeTracking.implement",
49
+ type: "boolean",
50
+ default: false,
51
+ question: "启用变更追踪 - f2s-implement-tech-design(实现技术方案时创建可续作的任务清单)?",
52
+ },
53
+ ];
54
+
55
+ function normalizeBool(value, fallback) {
56
+ if (value === true || value === "true" || value === 1 || value === "1")
57
+ return true;
58
+ if (value === false || value === "false" || value === 0 || value === "0")
59
+ return false;
60
+ return fallback;
61
+ }
62
+
63
+ /**
64
+ * 读取点号分隔键对应的嵌套值,如 "changeTracking.feat" → raw.changeTracking?.feat
65
+ */
66
+ function getNestedValue(obj, dottedKey) {
67
+ const parts = dottedKey.split(".");
68
+ let cur = obj;
69
+ for (const p of parts) {
70
+ if (!cur || typeof cur !== "object") return undefined;
71
+ cur = cur[p];
72
+ }
73
+ return cur;
74
+ }
75
+
76
+ /**
77
+ * 读取项目根 flow2spec.config.json,与 DEFAULTS 合并。
78
+ * 文件不存在时返回默认副本(不自动创建文件)。
79
+ * changeTracking 兼容旧版布尔值(true/false → 全部子项同值)。
80
+ */
81
+ function loadFlow2specConfig(cwd) {
82
+ const abs = path.join(cwd, CONFIG_FILENAME);
83
+ const out = {
84
+ ...DEFAULTS,
85
+ changeTracking: { ...DEFAULTS.changeTracking },
86
+ };
87
+ if (!fs.existsSync(abs)) {
88
+ return out;
89
+ }
90
+ let raw;
91
+ try {
92
+ raw = JSON.parse(fs.readFileSync(abs, "utf8"));
93
+ } catch (e) {
94
+ throw new Error(
95
+ `${CONFIG_FILENAME} JSON 解析失败:${e.message || String(e)}`,
96
+ );
97
+ }
98
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
99
+ return out;
100
+ }
101
+ if (Object.prototype.hasOwnProperty.call(raw, "subAgent")) {
102
+ out.subAgent = normalizeBool(raw.subAgent, DEFAULTS.subAgent);
103
+ }
104
+ if (Object.prototype.hasOwnProperty.call(raw, "switchAgentVerification")) {
105
+ out.switchAgentVerification = normalizeBool(
106
+ raw.switchAgentVerification,
107
+ DEFAULTS.switchAgentVerification,
108
+ );
109
+ } else if (Object.prototype.hasOwnProperty.call(raw, "subAgentVerification")) {
110
+ // 旧键名,仍读取;新落盘请用 switchAgentVerification
111
+ out.switchAgentVerification = normalizeBool(
112
+ raw.subAgentVerification,
113
+ DEFAULTS.switchAgentVerification,
114
+ );
115
+ }
116
+ if (Object.prototype.hasOwnProperty.call(raw, "changeTracking")) {
117
+ const ct = raw.changeTracking;
118
+ if (typeof ct === "boolean") {
119
+ // 旧版布尔值:统一应用到全部子项
120
+ out.changeTracking = { feat: ct, fix: ct, implement: ct };
121
+ } else if (ct && typeof ct === "object" && !Array.isArray(ct)) {
122
+ out.changeTracking = {
123
+ feat: normalizeBool(ct.feat, DEFAULTS.changeTracking.feat),
124
+ fix: normalizeBool(ct.fix, DEFAULTS.changeTracking.fix),
125
+ implement: normalizeBool(ct.implement, DEFAULTS.changeTracking.implement),
126
+ };
127
+ }
128
+ }
129
+ return out;
130
+ }
131
+
132
+ /**
133
+ * 返回配置文件中尚未存在的字段列表(用于 init 时只提示新增字段)。
134
+ * 文件不存在时返回全部字段。支持点号嵌套键。
135
+ */
136
+ function getMissingConfigFields(cwd) {
137
+ const abs = path.join(cwd, CONFIG_FILENAME);
138
+ if (!fs.existsSync(abs)) return CONFIG_FIELDS;
139
+ let raw;
140
+ try {
141
+ raw = JSON.parse(fs.readFileSync(abs, "utf8"));
142
+ } catch {
143
+ return [];
144
+ }
145
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return CONFIG_FIELDS;
146
+ return CONFIG_FIELDS.filter((f) => {
147
+ const parts = f.key.split(".");
148
+ if (parts.length === 2) {
149
+ const parent = raw[parts[0]];
150
+ // 旧版布尔值视为已配置,不再重复询问
151
+ if (typeof parent === "boolean") return false;
152
+ return !parent || !Object.prototype.hasOwnProperty.call(parent, parts[1]);
153
+ }
154
+ return !Object.prototype.hasOwnProperty.call(raw, f.key);
155
+ });
156
+ }
157
+
158
+ /**
159
+ * 将点号嵌套键的 values 对象合并入 target,支持一层嵌套。
160
+ * 例如 { "changeTracking.feat": true } → target.changeTracking.feat = true
161
+ */
162
+ function mergeValues(target, values) {
163
+ const result = { ...target };
164
+ for (const [key, val] of Object.entries(values)) {
165
+ const parts = key.split(".");
166
+ if (parts.length === 2) {
167
+ result[parts[0]] = {
168
+ ...(result[parts[0]] && typeof result[parts[0]] === "object"
169
+ ? result[parts[0]]
170
+ : {}),
171
+ [parts[1]]: val,
172
+ };
173
+ } else {
174
+ result[key] = val;
175
+ }
176
+ }
177
+ return result;
178
+ }
179
+
180
+ /**
181
+ * 若项目根不存在配置文件,则写入配置(优先用 values,其次包模板,再次 DEFAULTS)。
182
+ * 已存在时:若 values 中有缺失字段,则补写这些字段;否则不覆盖。
183
+ * @param {object} [options.values] 用户交互收集到的字段值,优先级高于模板文件
184
+ */
185
+ function ensureFlow2specProjectConfig(cwd, templatesDir, options = {}) {
186
+ const { overwrite = false, values } = options;
187
+ const dest = path.join(cwd, CONFIG_FILENAME);
188
+ const src = path.join(templatesDir, CONFIG_FILENAME);
189
+
190
+ if (fs.existsSync(dest) && !overwrite) {
191
+ if (values && typeof values === "object" && Object.keys(values).length > 0) {
192
+ let existing;
193
+ try {
194
+ existing = JSON.parse(fs.readFileSync(dest, "utf8"));
195
+ } catch {
196
+ existing = {};
197
+ }
198
+ const merged = mergeValues(existing, values);
199
+ if (JSON.stringify(merged) !== JSON.stringify(existing)) {
200
+ fs.writeFileSync(dest, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
201
+ return { created: false, updated: true, path: dest };
202
+ }
203
+ }
204
+ return { created: false, path: dest };
205
+ }
206
+
207
+ let base;
208
+ if (fs.existsSync(src)) {
209
+ try {
210
+ base = JSON.parse(fs.readFileSync(src, "utf8"));
211
+ } catch {
212
+ base = { ...DEFAULTS, changeTracking: { ...DEFAULTS.changeTracking } };
213
+ }
214
+ } else {
215
+ base = { ...DEFAULTS, changeTracking: { ...DEFAULTS.changeTracking } };
216
+ }
217
+ const merged = values && typeof values === "object" ? mergeValues(base, values) : base;
218
+ fs.writeFileSync(dest, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
219
+ return { created: true, path: dest };
220
+ }
221
+
222
+ module.exports = {
223
+ CONFIG_FILENAME,
224
+ DEFAULTS,
225
+ CONFIG_FIELDS,
226
+ loadFlow2specConfig,
227
+ getMissingConfigFields,
228
+ ensureFlow2specProjectConfig,
229
+ };