diff --git "a/.Knowledge/req-docs/Flow2Spec-Core\346\213\206\345\210\206\346\212\200\346\234\257\346\226\271\346\241\210.md" "b/.Knowledge/req-docs/Flow2Spec-Core\346\213\206\345\210\206\346\212\200\346\234\257\346\226\271\346\241\210.md" new file mode 100644 index 0000000..30ede39 --- /dev/null +++ "b/.Knowledge/req-docs/Flow2Spec-Core\346\213\206\345\210\206\346\212\200\346\234\257\346\226\271\346\241\210.md" @@ -0,0 +1,557 @@ +# Flow2Spec Core 拆分技术方案 + +## 需求概述 + +Flow2Spec 当前以 `@double-coding/flow2spec` 单一 npm 包发布,CLI、初始化、知识库引擎、协作能力、诊断能力、客户端适配器与模板资源均位于同一包内。DeepSeek Harness 原生插件需要在 Harness 进程中直接调用 Flow2Spec 能力,不能依赖启动 CLI 子进程,也不能在插件仓库复制知识库算法、Skills 或模板。 + +本次改造目标: + +- 在当前 Flow2Spec GitHub 仓库内新增 `@double-coding/flow2spec-core` npm 包。 +- 保留 `@double-coding/flow2spec` 作为现有 CLI 与传统客户端项目级适配入口。 +- 让 CLI 与后续 `@double-coding/flow2spec-plugin-dsh` 共同依赖 Core,复用同一套实现、协议与资源。 +- 为项目初始化、知识路由、KB Engine、团队协作、Doctor、Skills/规则资产和能力发现提供稳定的程序化接口。 +- 保持现有 CLI 命令、项目目录、配置文件与知识库协议向后兼容。 + +本次改造范围: + +- 当前 Flow2Spec 仓库的 npm workspace、Core 包、CLI 包、测试、构建和发布流程。 +- 现有 `lib/` 能力的归属调整与公共 API 封装。 +- 供原生插件消费的能力清单、版本契约与资源访问接口。 +- 现有 DSH 项目级适配继续保留,作为未安装原生插件时的兼容接入方式。 + +本次不包含: + +- 不在当前仓库实现 Cordis Plugin、Harness Tools、Hooks、Service 或 Web UI。 +- 不在 Core 中依赖 `@deepseek-ai/cordis` 或其他 Harness 专用包。 +- 不改变 `.Knowledge/`、`.task/`、`flow2spec.config.json` 的项目级数据标准。 +- 不在本次拆分中批量重写现有 Skills 的业务流程。 +- 不自动发布 DSH 插件;插件在独立仓库规划和发布。 + +## 重点问题概述 + +### 单一实现来源 + +Core 是初始化、知识库操作、协作状态、诊断、路由资源与 Flow2Spec 资产的唯一实现来源。CLI 和原生插件只负责各自运行环境中的参数、交互、展示和生命周期集成。 + +### 兼容现有 CLI + +现有用户继续使用: + +```bash +npx @double-coding/flow2spec@latest init +flow2spec config +flow2spec doctor +flow2spec kb status +flow2spec kb check --strict +flow2spec kb plan +flow2spec kb apply +flow2spec kb build +``` + +命令名称、主要参数、退出码、默认目录和非破坏式初始化语义保持兼容。CLI 改为 Core 的薄适配层,不再承载业务逻辑。 + +### 插件进程安全 + +Core 被 Harness 进程加载时必须满足: + +- 导入模块不会读取 `process.argv`。 +- 导入模块不会调用 `process.exit()`。 +- 核心操作不会直接执行交互式问答。 +- 核心操作默认不向 stdout/stderr 输出内容。 +- 写操作显式接收 `cwd`、参数和选项,并返回结构化结果。 +- 可通过 `AbortSignal` 和进度回调接入宿主生命周期;当前同步操作至少在阶段边界检查取消状态。 + +### 模板与包路径 + +当前 `lib/init.js`、`lib/doctor.js` 和部分适配器通过相对路径读取仓库根 `package.json` 与 `templates/`。拆包后必须由 Core 内部的资源解析器统一定位自身包内资源,禁止依赖调用方的当前工作目录或 CLI 包目录。 + +### 完整能力同步 + +“插件包含 Flow2Spec 全部功能”通过 Core 能力清单和契约测试保证,不在插件仓库维护人工复制的功能列表。新增或变更 Core 能力时,能力清单与兼容性测试必须同步更新。 + +## 架构决策 + +### 仓库与 npm 包结构 + +当前仓库调整为 npm workspaces: + +```text +Flow2Spec/ +├── package.json # 私有 workspace 根、统一测试与发布脚本 +├── packages/ +│ ├── core/ +│ │ ├── package.json # @double-coding/flow2spec-core +│ │ ├── index.js # 稳定公共入口 +│ │ ├── lib/ +│ │ │ ├── project/ +│ │ │ ├── knowledge/ +│ │ │ ├── collaboration/ +│ │ │ ├── diagnostics/ +│ │ │ ├── integrations/ +│ │ │ └── resources/ +│ │ ├── templates/ +│ │ └── capabilities.json +│ └── cli/ +│ ├── package.json # @double-coding/flow2spec +│ ├── cli.js # flow2spec bin +│ └── lib/ +│ ├── commands/ +│ ├── prompts/ +│ └── formatters/ +├── scripts/ +├── tests/ +├── docs/ +├── website/ +└── .Knowledge/ +``` + +约束: + +- Core 与 CLI 同仓维护、同一 PR 修改、同一 CI 验证。 +- Core 和 CLI 首阶段采用相同版本号并在一次 Release 中依次发布。 +- CLI 对 Core 使用精确版本依赖,避免 CLI 安装到不匹配的 Core。 +- 插件使用经过验证的 Core 兼容范围,并在锁文件中锁定实际版本。 +- workspace 根设为 `private: true`,防止误发布根包。 + +### 包职责 + +| 能力 | Core | CLI | DSH 插件仓库 | +| --- | --- | --- | --- | +| `.Knowledge` 协议与读写 | 负责 | 调用 | 调用 | +| 初始化与升级底层操作 | 负责 | 参数/问答适配 | Harness 操作/UI 适配 | +| 知识路由与依赖展开 | 负责 | 展示结果 | 运行时调用与门禁 | +| KB plan/apply/build/check/status | 负责 | 命令适配 | Tool/Service 适配 | +| developerId 与 TASK_ROOT | 负责 | 展示 | 会话状态集成 | +| Doctor 检查 | 负责 | 文本/JSON 格式化 | Harness 诊断视图 | +| Skills、规则与模板资产 | 负责 | 安装到配置根 | 原生注册或加载 | +| Cursor/Claude/Codex 项目适配 | 可复用适配器 | 触发 | 不负责 | +| DSH `.dsh/skills` 兼容适配 | 可复用适配器 | 触发 | 原生插件安装后不重复执行 | +| Cordis 生命周期与 Hooks | 不依赖 | 不负责 | 负责 | +| Harness Web UI | 不依赖 | 不负责 | 负责 | +| npm 自更新交互 | 不负责 | 负责 | 使用 DSH 插件升级机制 | + +### 现有功能覆盖矩阵 + +拆包验收以当前 Flow2Spec 的实际能力为基线。每项能力必须明确归入 Core API、Core 资源或 CLI/宿主适配层,禁止在迁移时静默删除。 + +| 能力组 | 当前能力 | 拆分后归属 | +| --- | --- | --- | +| 项目生命周期 | `init`、配置补齐、locale、非破坏式模板对齐、`.gitignore`、版本字段 | Core `project` / `config` | +| CLI 管理 | `version`、`update`、全局包升级提示、TTY 问答 | CLI;版本信息由 Core 提供只读接口 | +| 诊断 | `doctor`、Node/配置/集成/协作/知识图检查 | Core `doctor`;CLI/插件负责展示 | +| KB Engine | `status`、`check`、`plan`、`apply`、`build`、`--strict`、`--fix-topics`、`--dry-run` | Core `knowledge` | +| 知识路由 | manifest、matcher、topic dependencies、fallback、`match -> expand -> verify -> act` | Core `routing` + Core 规则资源 | +| 团队协作 | developerId、TASK_ROOT、个人任务隔离、kb-delta、revision 冲突 | Core `collaboration` / `knowledge` + Skills | +| 客户端适配 | Cursor、Claude、Codex、DSH 项目级目录、入口、规则、Skills、Hooks | Core integrations/resources;CLI 触发 | +| 文档工作流 | `f2s-doc-arch`、`f2s-doc-final`、`f2s-doc-milestone`、`f2s-doc-pdf` | Core Skills/规则/模板资源;宿主 Agent 执行 | +| 需求工作流 | `f2s-req-clarify`、`f2s-req-tech`、`f2s-req-plan`、`implement-tech-design` | Core Skills/规则/模板资源;宿主 Agent 执行 | +| 知识维护 | `f2s-kb-add`、`f2s-kb-addRules`、`f2s-kb-build`、`f2s-kb-distill`、`f2s-kb-feat`、`f2s-kb-fix`、`f2s-kb-merge`、`f2s-kb-migrate`、`f2s-kb-rm`、`f2s-kb-sync`、`f2s-kb-upgrade` | Core Skills/规则资源调用 Core API | +| Git 收口 | `f2s-git-commit`、知识覆盖检查和提交口径 | Core Skill/规则资源;Git 操作由宿主执行 | +| 意图与编排 | intent recognition、subAgent、switchAgentVerification、changeTracking | Core 配置/规则/Skills;宿主提供 Agent 能力 | +| 版本检查 Hooks | SessionStart、PreToolUse、更新检测脚本 | Core Hook 资源与客户端适配;DSH 插件使用原生生命周期 | + +Skills 属于 Flow2Spec 的核心产品能力,但它们是宿主 Agent 执行的工作流资产,不应被错误改写为纯 JavaScript 函数。Core 同时提供资源发现、版本、校验和与所需底层 API,CLI 和插件负责在各自宿主中加载并执行这些工作流。 + +## Core 公共契约 + +### 公共入口 + +Core 使用 CommonJS 并兼容 Node.js >= 16,公共入口只暴露经过承诺的 API: + +```js +const { + createFlow2Spec, + getCapabilities, + Flow2SpecError, +} = require("@double-coding/flow2spec-core"); + +const flow2spec = createFlow2Spec({ + cwd, + signal, + onProgress(event) {}, +}); +``` + +`createFlow2Spec()` 返回按领域组织的门面: + +```js +flow2spec.project +flow2spec.config +flow2spec.routing +flow2spec.knowledge +flow2spec.collaboration +flow2spec.doctor +flow2spec.resources +``` + +Core 内部文件不作为公共契约。CLI、插件和外部调用方不得继续使用 `require(".../lib/")` 深路径导入。 + +### 通用调用上下文 + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `cwd` | `string` | 必填,目标项目绝对路径 | +| `signal` | `AbortSignal?` | 宿主取消信号 | +| `onProgress` | `(event) => void` | 可选进度通知,不承载业务返回值 | +| `locale` | `zh-CN \| en-US` | 可按单次操作覆盖项目默认语言 | + +进度事件使用稳定结构: + +```js +{ + operation: "project.init", + phase: "write-templates", + status: "start" | "complete" | "skip", + detail: {} +} +``` + +### 错误契约 + +Core 统一抛出 `Flow2SpecError`: + +```js +{ + name: "Flow2SpecError", + code: "F2S_KNOWLEDGE_CONFLICT", + message: "...", + details: {}, + recoverable: true +} +``` + +首批稳定错误类型: + +| 错误码 | 说明 | 调用方处理 | +| --- | --- | --- | +| `F2S_INVALID_ARGUMENT` | 参数或调用契约错误 | CLI 返回 1;插件展示配置错误 | +| `F2S_CONFIG_INVALID` | 项目配置无法解析 | 提示修复配置,不自动覆盖 | +| `F2S_NOT_INITIALIZED` | 项目未初始化 | 引导调用 `project.init` | +| `F2S_KNOWLEDGE_INVALID` | 知识图严格校验失败 | 展示 issues/warnings | +| `F2S_KNOWLEDGE_CONFLICT` | revision 或 delta 冲突 | 停止 apply,要求重读 | +| `F2S_OPERATION_ABORTED` | 宿主取消操作 | CLI/插件安静结束当前操作 | +| `F2S_RESOURCE_MISSING` | 包内模板或资产缺失 | 阻止写入并报告包完整性错误 | + +Core 不把 CLI 退出码写入错误对象;退出码由 CLI 命令适配器决定。 + +## 交付单元 + +### Core 项目初始化 + +公共契约: + +```js +await flow2spec.project.init({ + integrations: ["cursor", "claude", "codex", "dsh"], + mode: "project-adapter" | "native-host", + resetKnowledge: false, + configValues: {}, + locale: "zh-CN", +}); +``` + +处理规则: + +- `project-adapter` 保持当前 `flow2spec init ` 行为,写入对应配置根、Skills、规则、入口与 Hooks。 +- `native-host` 只初始化共享项目层:`.Knowledge/`、`flow2spec.config.json`、`.gitignore`、路由结构、模板快照和版本字段。 +- `native-host` 不写 `.dsh/skills`、`.dsh/topics` 或 DSH 入口;这些由原生插件注册。 +- 已有配置与业务知识继续采用非破坏式补齐,只有明确 `resetKnowledge` 时覆盖模板承载部分。 +- 返回 `changedFiles`、`skippedFiles`、`warnings`、`projectConfig`、`routingUpgrade` 和资源版本。 + +迁移来源:现有 `lib/init.js`、`lib/agents.js`、各客户端适配器和 `templates/`。 + +### 配置与项目状态 + +公共契约: + +```js +flow2spec.config.load(); +flow2spec.config.getMissingFields(); +flow2spec.collaboration.resolveDeveloper(); +flow2spec.project.inspect(); +``` + +处理规则: + +- 保留旧版 `changeTracking` 布尔值和 `subAgentVerification` 兼容解析。 +- 默认值继续以代码、双语模板和生成说明四处一致为门禁。 +- `resolveDeveloper()` 保持 config -> git -> legacy 的解析顺序。 +- `project.inspect()` 只读返回初始化状态、现有集成、知识库版本和资源版本。 + +迁移来源:`lib/flow2specConfig.js`、`lib/developerId.js`。 + +### 知识路由 + +公共契约: + +```js +const result = flow2spec.routing.match({ + request: "<用户请求>", + task: "<可选稳定任务名>", +}); + +flow2spec.routing.expand(result); +flow2spec.routing.verify(result, { requiredContext: [] }); +flow2spec.routing.loadContext(result, { maxFiles, maxLines }); +``` + +返回内容至少包含: + +- 主候选与次候选。 +- 命中的 task rule、matcher、关键词和置信度依据。 +- 展开后的 `topicDependencies`。 +- 缺失 topic、matcher、文档或必要上下文。 +- `fallbackTopic` 是否仅作为低置信兜底。 +- 建议读取的文件列表,不直接把整个仓库内容注入宿主。 + +匹配规则必须确定化并提供测试夹具:稳定 task 映射优先,其次 matcher phrase 命中;无法确定时返回低置信结果,由宿主模型执行澄清,不在 Core 中伪造语义理解。 + +### Knowledge Engine + +公共契约: + +```js +flow2spec.knowledge.status(); +flow2spec.knowledge.check({ strict: true }); +flow2spec.knowledge.plan({ deltaFile }); +flow2spec.knowledge.apply({ deltaFile, dryRun: false }); +flow2spec.knowledge.build({ fixTopics: false, dryRun: false }); +``` + +处理规则: + +- 保留现有 topic frontmatter、revision、delta schema 和 routing drift 语义。 +- `apply` 写入前必须复用 `plan` 的 revision 校验。 +- 所有写操作返回精确变更文件,不打印日志。 +- Core 继续提供低层文档解析函数,但只在明确的高级导出下暴露,避免插件直接拼接 Markdown。 + +迁移来源:`lib/knowledgeEngine.js`。 + +### Skills、规则与模板资源 + +公共契约: + +```js +flow2spec.resources.listSkills(); +flow2spec.resources.getSkill("f2s-kb-sync", { locale: "zh-CN" }); +flow2spec.resources.listRules(); +flow2spec.resources.getTemplate("knowledge/template/技术方案模版.md"); +flow2spec.resources.getManifestSeed(); +``` + +处理规则: + +- Skills、规则、Hooks、知识模板和双语资源随 Core 包发布。 +- CLI 的客户端适配器从 Core 资源接口读取,不自行维护副本。 +- DSH 插件通过资源接口注册或调用完整 Skills,不复制模板到插件仓库作为第二真值源。 +- 资源接口返回内容、校验和、locale、资源修订号和来源路径标识。 +- 包内资源路径只由 Core 资源解析器处理。 + +### Doctor + +公共契约: + +```js +const report = await flow2spec.doctor.run({ + strictKnowledge: true, + integrations: "detected", +}); +``` + +处理规则: + +- Doctor 保持只读、离线和确定性。 +- Core 返回结构化报告,不负责 `[PASS]`、`[WARN]`、`[FAIL]` 文本格式。 +- CLI 保持现有人读格式和 `--json` 格式。 +- 插件可以增加 Harness 专属检查,但不能修改 Core 通用报告含义。 + +迁移来源:`lib/doctor.js`。 + +### 能力清单 + +`packages/core/capabilities.json` 为客户端能力对齐的机读契约: + +```json +{ + "schema": "flow2spec.capabilities.v1", + "protocolVersion": 1, + "capabilities": [ + { "id": "project.init", "api": "project.init", "since": "3.3.0" }, + { "id": "routing.match", "api": "routing.match", "since": "3.3.0" }, + { "id": "knowledge.check", "api": "knowledge.check", "since": "3.3.0" }, + { "id": "knowledge.plan", "api": "knowledge.plan", "since": "3.3.0" }, + { "id": "knowledge.apply", "api": "knowledge.apply", "since": "3.3.0" }, + { "id": "collaboration.resolve", "api": "collaboration.resolveDeveloper", "since": "3.3.0" }, + { "id": "doctor.run", "api": "doctor.run", "since": "3.3.0" }, + { "id": "resources.skills", "api": "resources.listSkills", "since": "3.3.0" } + ] +} +``` + +规则: + +- 新增、删除或改变稳定能力时必须更新能力清单。 +- Core CI 校验清单中的 API 实际存在。 +- CLI CI 校验需要命令映射的能力均已映射。 +- DSH 插件 CI 读取清单并维护 `implemented`、`native`、`notApplicable` 三态覆盖结果。 +- `notApplicable` 必须写明原因,禁止用它掩盖未实现功能。 + +### CLI 薄适配层 + +CLI 只保留: + +- `process.argv` 解析。 +- TTY 交互问答。 +- 人读与 JSON 输出格式化。 +- Core 错误到退出码的映射。 +- npm registry 查询、全局更新和版本提示。 + +CLI 不再直接读取或写入 `.Knowledge`,不再直接拼装 delta,不再直接解析包内模板。 + +`@double-coding/flow2spec` 的 `bin.flow2spec`、README 安装命令和现有 npm 包名保持不变。 + +## 版本与发布 + +### 版本策略 + +- Core 与 CLI 首阶段锁步版本,例如 `3.3.0` 与 `3.3.0`。 +- CLI 使用精确依赖:`"@double-coding/flow2spec-core": "3.3.0"`。 +- 插件使用经过测试的兼容范围,例如 `^3.3.0`,并提交 lockfile。 +- `protocolVersion` 独立于 npm 版本;项目知识协议发生不兼容变化时递增。 +- 仅新增 Core API 且 CLI 行为兼容时可发布 minor;修改现有 CLI 契约、公共 API 或协议时按 breaking change 管理。 + +### 发布顺序 + +1. 运行 workspace 全量测试和 pack 安装测试。 +2. 发布 `@double-coding/flow2spec-core@`。 +3. 验证 registry 可安装并执行公共入口 smoke test。 +4. 发布依赖该精确版本的 `@double-coding/flow2spec@`。 +5. 创建统一 Git tag 和 GitHub Release。 + +发布脚本不得再假设仓库根 `package.json` 就是唯一待发布包。Tag 仍以统一产品版本生成,发布日志同时列出 Core 与 CLI 包。 + +## 迁移顺序 + +### 阶段一:建立契约与 workspace + +- 建立 `packages/core`、`packages/cli` 和 workspace 根。 +- 添加公共 API、错误类型、能力清单 schema 与空门面。 +- 建立 packed-package smoke test,验证 Core 可独立导入且无控制台输出、无进程退出。 + +### 阶段二:迁移纯 Core 模块 + +- 迁移 `knowledgeEngine`、`flow2specConfig`、`developerId`、`doctor`。 +- 保留现有返回结构,并通过兼容测试锁定行为。 +- 将 Doctor 文本格式化移动到 CLI。 + +### 阶段三:迁移初始化与资源 + +- 将模板移入 Core 包。 +- 增加资源解析器,移除对仓库根相对路径的依赖。 +- 拆分共享项目初始化与客户端配置根安装。 +- 保留 DSH 项目级适配兼容模式。 + +### 阶段四:增加知识路由 API + +- 实现 task rule、matcher、依赖展开和缺口检查的确定性 API。 +- 以当前 `manifest-routing.json` 和 matcher schema 建立测试夹具。 +- 保持 `fallbackTopic` 只作为低置信兜底。 + +### 阶段五:CLI 切换到 Core + +- 每个 CLI 子命令逐一改为调用 Core。 +- 删除 CLI 中重复的知识库和初始化逻辑。 +- 对照旧版本执行输出、退出码和文件树回归测试。 + +### 阶段六:发布链与插件对接门禁 + +- 调整版本、Tag、npm pack 和发布脚本。 +- 发布 prerelease 验证两个 npm 包的安装关系。 +- 输出插件仓库可消费的 API 文档、能力清单和兼容矩阵模板。 + +每个阶段必须保持 `main` 可测试;不使用一次性“大搬家”提交跨越全部阶段。 + +## 异常处理与兼容策略 + +| 场景 | 处理策略 | +| --- | --- | +| Core 资源缺失 | 初始化前失败,不写入部分项目文件 | +| CLI 与 Core 版本不一致 | CLI 启动时报告包不匹配并停止写操作 | +| 插件请求未知能力 | 返回能力不存在,插件不得静默降级为自实现 | +| 老项目缺新增配置 | 沿用当前缺字段补齐机制,不覆盖已有值 | +| 旧 topic 缺 revision | 保持 `kb build --fix-topics` 迁移路径 | +| delta revision 冲突 | plan/apply 停止,返回结构化冲突信息 | +| 原生插件未安装 | `flow2spec init dsh` 项目级适配继续可用 | +| 原生插件已安装 | 插件使用 `native-host` 初始化,不重复写 `.dsh` 兼容产物 | +| Core 操作被取消 | 抛出 `F2S_OPERATION_ABORTED`,已完成的原子文件写入保留并在结果中报告 | + +## 测试与验收 + +### Core 单元测试 + +- 所有现有 knowledge engine、developerId、config 和 Doctor 用例迁移后继续通过。 +- Core 导入不读取 argv、不退出进程、不产生 stdout/stderr。 +- 每个写操作在临时目录验证变更文件与非目标文件。 +- 中英文资源目录和技能清单保持一致。 +- 能力清单中的每个 API 都可解析并调用。 + +### CLI 兼容测试 + +- `--help`、`version`、`config`、`doctor`、`kb`、`init` 命令回归。 +- 人读与 JSON 输出字段保持兼容。 +- PASS/WARN/FAIL 对应退出码保持兼容。 +- `npx @double-coding/flow2spec init` 仍创建预期目录和配置根。 +- `init dsh` 兼容用例继续通过。 + +### 包测试 + +- 对 Core 与 CLI 分别执行 `npm pack --dry-run`。 +- 在空临时目录安装两个 tgz,禁止依赖仓库内未打包文件。 +- 只安装 CLI 时能够自动安装 Core 并执行全部命令。 +- 只安装 Core 时能够通过公共 API 初始化临时项目。 +- 包内不包含 `.task/`、配置根产物或开发期临时文件。 + +### 行为等价测试 + +同一测试夹具分别通过旧版行为快照和新 Core/CLI 执行,比较: + +- 初始化后的文件树和关键文件内容。 +- 配置缺省值与旧键兼容结果。 +- knowledge status/check/plan/apply/build 结果。 +- Doctor 结构化报告。 +- developerId 和 TASK_ROOT 解析。 + +允许差异必须在迁移记录中列明,不能以“重构”为由接受未解释差异。 + +### 完成标准 + +- `@double-coding/flow2spec-core` 可独立打包、安装和调用。 +- `@double-coding/flow2spec` 所有命令通过 Core 执行。 +- 当前 Flow2Spec 自动化测试全部迁移并通过。 +- DSH 项目级适配未回归。 +- 能力清单覆盖初始化、路由、KB Engine、协作、Doctor 和资源访问。 +- 文档说明普通用户无需单独安装 Core。 +- Core 不含 Harness 运行时依赖。 +- 插件仓库无需复制 Core 算法、Skills、规则或模板即可开始开发。 + +## 风险与取舍 + +### workspace 改造影响发布脚本 + +当前版本与 Tag 脚本只读取仓库根 `package.json`。改造时必须先补多包 pack/release 测试,再调整正式发布流程,避免只发布其中一个包。 + +### 模板移动造成路径回归 + +初始化代码存在基于 `__dirname` 和仓库根的模板路径假设。迁移采用资源解析器并通过 tgz 安装测试验证,不能只在源码仓库内运行测试。 + +### 公共 API 过早固化 + +首个 Core 版本只承诺门面 API、错误契约和能力清单。底层 Markdown 解析、文件帮助函数保持内部实现,减少未来兼容负担。 + +### 路由能力与模型语义边界 + +Core 负责确定性的 task/matcher 匹配、依赖展开和缺口报告;自然语言歧义和低置信澄清由宿主 Agent 完成。Core 不内置模型调用,不绑定模型供应商。 + +### 双入口并存 + +原生插件发布后,`init dsh` 仍作为项目级兼容入口保留一段迁移周期。文档必须明确两种方式的适用场景,Doctor 应能检测重复接入并给出非破坏性建议。 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69d169a..c35029f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,12 @@ jobs: - name: Run CLI tests run: npm test + - name: Run Core API tests + run: npm run test:core + + - name: Verify workspace package installation + run: node scripts/test-package-install.js + - name: Verify npm package contents run: npm run pack:check diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml index 96f1a3b..85215ed 100644 --- a/.github/workflows/publish-npm.yml +++ b/.github/workflows/publish-npm.yml @@ -15,7 +15,7 @@ concurrency: jobs: publish: - name: Publish @double-coding/flow2spec + name: Publish Flow2Spec workspace packages runs-on: ubuntu-latest steps: - name: Check out release commit @@ -42,10 +42,20 @@ jobs: - name: Verify tag matches package version shell: bash run: | - package_version="$(node -p "require('./package.json').version")" + package_version="$(node -p "require('./packages/core/package.json').version")" + cli_version="$(node -p "require('./packages/cli/package.json').version")" + root_version="$(node -p "require('./package.json').version")" tag_version="${GITHUB_REF_NAME#[vV]}" + if [[ "$package_version" != "$cli_version" || "$package_version" != "$root_version" ]]; then + echo "Root, Core, and CLI workspace versions must match." >&2 + exit 1 + fi + if [[ "$(node -p "require('./packages/cli/package.json').dependencies['@double-coding/flow2spec-core']")" != "$package_version" ]]; then + echo "CLI Core dependency must match the release version $package_version." >&2 + exit 1 + fi if [[ "$tag_version" != "$package_version" ]]; then - echo "Release tag $GITHUB_REF_NAME does not match package version $package_version." >&2 + echo "Release tag $GITHUB_REF_NAME does not match workspace package version $package_version." >&2 exit 1 fi @@ -55,5 +65,8 @@ jobs: - name: Verify npm package contents run: npm run pack:check - - name: Publish package with provenance - run: npm publish --access public --provenance + - name: Publish Core package with provenance + run: npm publish --workspace @double-coding/flow2spec-core --access public --provenance + + - name: Publish CLI package with provenance + run: npm publish --workspace @double-coding/flow2spec --access public --provenance diff --git a/cli.js b/cli.js index 8ae7bc9..41502ab 100644 --- a/cli.js +++ b/cli.js @@ -1,841 +1,3 @@ #!/usr/bin/env node -const path = require("path"); -const fs = require("fs"); -const os = require("os"); -const readline = require("readline"); -const runInit = require("./lib/init"); -const { AGENTS } = require("./lib/agents"); -const { - loadFlow2specConfig, - CONFIG_FILENAME, - CONFIG_FIELDS, - getMissingConfigFields, - SUPPORTED_LOCALES, - normalizeLocale, -} = require("./lib/flow2specConfig"); -const knowledgeEngine = require("./lib/knowledgeEngine"); -const { runDoctor, formatDoctorReport } = require("./lib/doctor"); - -const { execFileSync } = require("child_process"); - -const args = process.argv.slice(2); -const sub = args[0]; - -const agentList = Object.entries(AGENTS) - .map(([id, { label }]) => `${id}(${label})`) - .join(", "); - -const pkg = require("./package.json"); - -const UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1000; - -function parseVersion(version) { - return String(version || "") - .replace(/^v/, "") - .split(/[.-]/) - .slice(0, 3) - .map((part) => { - const n = Number.parseInt(part, 10); - return Number.isFinite(n) ? n : 0; - }); -} - -function compareVersions(a, b) { - const av = parseVersion(a); - const bv = parseVersion(b); - for (let i = 0; i < 3; i += 1) { - const diff = (av[i] || 0) - (bv[i] || 0); - if (diff !== 0) return diff; - } - return 0; -} - -function updateCheckCacheFile() { - const safeName = String(pkg.name || "flow2spec").replace(/[^a-z0-9_.-]+/gi, "_"); - return path.join(os.homedir(), ".flow2spec", `${safeName}-update-check.json`); -} - -function readUpdateCheckCache() { - const file = updateCheckCacheFile(); - if (!fs.existsSync(file)) return null; - try { - const data = JSON.parse(fs.readFileSync(file, "utf8")); - if (!data || typeof data !== "object") return null; - if (Date.now() - Number(data.checkedAt || 0) > UPDATE_CHECK_TTL_MS) { - return null; - } - return data; - } catch { - return null; - } -} - -function writeUpdateCheckCache(latest) { - try { - const file = updateCheckCacheFile(); - fs.mkdirSync(path.dirname(file), { recursive: true }); - fs.writeFileSync( - file, - `${JSON.stringify({ latest, checkedAt: Date.now() }, null, 2)}\n`, - "utf8", - ); - } catch { - // 更新检查不能影响主命令。 - } -} - -function queryLatestPackageVersion() { - const cached = readUpdateCheckCache(); - if (cached?.latest) return cached.latest; - const latest = execFileSync("npm", ["view", pkg.name, "version"], { - encoding: "utf8", - timeout: 2000, - stdio: ["ignore", "pipe", "ignore"], - }).trim(); - if (latest) writeUpdateCheckCache(latest); - return latest; -} - -function shouldCheckForUpdates() { - if (process.env.FLOW2SPEC_SKIP_UPDATE_CHECK === "1") return false; - if (process.env.CI) return false; - if (!process.stdout.isTTY) return false; - return Boolean(pkg.name && pkg.version); -} - -/** - * 读取全局安装的同名包版本(如果有)。 - * - * 用 `npm root -g` 拿全局 node_modules 根目录,再读 `//package.json` 的 version。 - * 这是跨 Node / npm 版本最稳定的判断"用户是否全局装过"的方式(避免 `npm ls -g` 输出格式差异)。 - * - * @returns {string|null} 全局已装版本号;未装、读取失败一律返回 null - */ -function getGlobalInstalledVersion() { - if (!pkg.name) return null; - let globalRoot; - try { - globalRoot = execFileSync("npm", ["root", "-g"], { - encoding: "utf8", - timeout: 2000, - stdio: ["ignore", "pipe", "ignore"], - }).trim(); - } catch { - return null; - } - if (!globalRoot) return null; - const pkgJsonPath = path.join(globalRoot, pkg.name, "package.json"); - try { - if (!fs.existsSync(pkgJsonPath)) return null; - const data = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8")); - return typeof data.version === "string" ? data.version : null; - } catch { - return null; - } -} - -/** - * init 收尾时自动把全局 flow2spec 升到 latest。 - * - * 触发条件(同时满足): - * 1. 用户已经全局 `npm i -g` 装过本包(用 getGlobalInstalledVersion 检测); - * 2. npm registry 上 latest 严格高于全局已装版本。 - * - * 用户没全局装过 → 静默跳过;当前 cli 是 npx 临时缓存跑的,不要侵入用户全局环境。 - * `npm i -g` 失败(权限 / 私服 404 / 网络) → 打印错误 + 手动升级提示,但 init 整体仍 exit 0。 - * - * 该函数本身永不抛错,永远不影响 init 主流程。 - */ -function maybeAutoUpdateGlobalInstall() { - let installed; - try { - installed = getGlobalInstalledVersion(); - } catch { - return; - } - if (!installed) return; // 没全局装过 → 不打扰 - let latest; - try { - latest = queryLatestPackageVersion(); - } catch { - return; // 查 latest 失败静默跳过 - } - if (!latest) return; - if (compareVersions(latest, installed) <= 0) return; // 全局已是最新 - console.log(` -↻ 检测到全局 ${pkg.name}@${installed},正在升级到 v${latest}...`); - try { - execFileSync("npm", ["install", "-g", `${pkg.name}@latest`], { - stdio: "inherit", - }); - console.log(`✓ 全局 ${pkg.name} 已升级到 v${latest}`); - } catch (e) { - console.error(` -⚠ 全局升级失败:${e.message || e} - 可手动执行: flow2spec update`); - } -} - -function printKnowledgeUpgradeHint(latest) { - console.log(` -⚠ Flow2Spec 有新版本 v${latest}(当前 v${pkg.version}) -建议先更新包: - flow2spec update - -更新后请在 Agent 对话中执行: - f2s-kb-upgrade - -用于对齐项目知识库模板、manifest/matchers 与配置根产物;不要把单独 flow2spec init 当作知识库升级。 -`); -} - -function maybePrintUpdateNotice() { - if (!shouldCheckForUpdates()) return; - try { - const latest = queryLatestPackageVersion(); - if (latest && compareVersions(latest, pkg.version) > 0) { - printKnowledgeUpgradeHint(latest); - } - } catch { - // 静默跳过,不能因为网络或 npm registry 影响主命令。 - } -} - -function printJson(data) { - console.log(`${JSON.stringify(data, null, 2)}\n`); -} - -function printKnowledgeHelp() { - console.log(` -Flow2Spec KB - knowledge collaboration engine - -用法: - flow2spec kb status [--json] - flow2spec kb check [--strict] [--json] - flow2spec kb plan [--json] - flow2spec kb apply [--dry-run] [--json] - flow2spec kb build [--fix-topics] [--json] - -说明: - status - 汇总当前知识图、active task delta 与潜在漂移 - check - 校验 manifest/topic/frontmatter/revision - plan - 预演一个 kb-delta 是否可自动合并 - apply - 应用 kb-delta 并同步 topic frontmatter / routing - build - 基于 topic frontmatter 归一化 routing 元数据;--fix-topics 可为旧 topic 补 frontmatter/revision - -delta changes: - appendBody / replaceBody / updateFrontmatter / createTopic -`); -} - -const help = ` -Flow2Spec - 统一知识库工作流(AI 配置入口) v${pkg.version} - -用法: - flow2spec init [agent ...] [--reset-knowledge] [--yes] [--locale zh-CN|en-US] 在当前项目初始化:写入 .Knowledge 与所选 agent 入口 - flow2spec config 打印项目根 ${CONFIG_FILENAME} 的解析结果(缺省值合并后) - flow2spec doctor [--json] 只读检查运行环境、项目初始化、协作上下文与知识库健康 - flow2spec kb 知识库协作引擎:status / check / plan / apply / build - flow2spec version 显示当前 flow2spec 版本 - flow2spec update 更新 flow2spec 到最新版本;更新后提示执行 f2s-kb-upgrade - flow2spec --help 显示本说明 - -agent(可多个,空格分隔;省略时交互选择): - ${agentList} - -示例: - flow2spec init # 交互选择工具和配置 - flow2spec init # 直接写入指定客户端配置根,跳过工具选择 - flow2spec init # 同时初始化多个客户端 - flow2spec init --locale en-US # 使用英文模板初始化指定客户端 - flow2spec init dsh # 初始化 DeepSeek Harness 项目技能 - flow2spec init --yes # 跳过所有问答,使用默认值(适合 CI) - flow2spec init --reset-knowledge # 强制用模板覆盖 .Knowledge(谨慎) - -init 会: - 1. 交互询问要初始化的 AI 工具(见上方 agent 列表,可多选);已通过参数指定则跳过。 - 传 --yes 或非 TTY 环境时跳过问答,使用默认值。 - 2. 对 ${CONFIG_FILENAME} 中缺失的配置字段逐项提问(已有字段不覆盖)。模板语言由 --locale、已有 locale 或默认 zh-CN 决定。 - 传 --yes 时所有缺失字段使用各自默认值。 - 3. 默认仅补齐 .Knowledge 缺失模板,并对路由清单做包级/结构增量对齐(manifest-routing + matcherPath 分片;关键词仅写在 matchers/*.json);不替代 f2s-* 对业务文档与路由内容的写入。 - 传 --reset-knowledge 时才会强制用模板覆盖 .Knowledge 中模板承载部分。 - 4. 在各 agent 配置根写入对应的 rules、skills、入口和 hooks(若该客户端支持);具体落盘方式以客户端适配器为准。 - 5. 每次 init 将当前 locale 包模板 knowledge/index.md 复制到 .Knowledge/template/index.template.md,供 f2s-kb-upgrade 技能与 .Knowledge/index.md 对照;不自动改写 index.md。(「知识库升级」指 f2s-kb-upgrade 技能,init 本身不是升级命令。) - 6. 非破坏式补充 .gitignore:忽略 .task/ 与 .Knowledge/update-check.json 这类本地运行态。 - 7. 规则与技能在各 agent 配置根加载;其他模版类文件在 .Knowledge/template/ 等目录。 - -更多说明见 README.md 或 docs/使用说明.md -`; - -if (sub === "--help" || sub === "-h" || !sub) { - console.log(help.trim()); - process.exit(0); -} - -if (sub === "version" || sub === "--version" || sub === "-v") { - console.log(`flow2spec v${pkg.version}`); - maybePrintUpdateNotice(); - process.exit(0); -} - -if (sub === "update") { - console.log(`当前版本: v${pkg.version}`); - console.log("正在检查最新版本..."); - try { - const latest = execFileSync("npm", ["view", pkg.name, "version"], { - encoding: "utf8", - }).trim(); - if (compareVersions(latest, pkg.version) <= 0) { - console.log(`当前版本不低于 npm 最新版本 v${latest}`); - process.exit(0); - } - console.log(`发现新版本: v${latest}`); - console.log("正在更新..."); - execFileSync("npm", ["install", "-g", `${pkg.name}@latest`], { - stdio: "inherit", - }); - console.log(`\n✓ 已更新到 v${latest}`); - console.log(` -下一步:请在需要升级的项目 Agent 对话中执行: - f2s-kb-upgrade - -用于对齐项目知识库模板、manifest/matchers 与配置根产物;不要把单独 flow2spec init 当作知识库升级。 -`); - } catch (e) { - console.error("更新失败:", e.message || e); - process.exit(1); - } - process.exit(0); -} - -if (sub === "config") { - const cwd = process.cwd(); - const abs = path.join(cwd, CONFIG_FILENAME); - try { - const cfg = loadFlow2specConfig(cwd); - console.log(JSON.stringify({ configPath: abs, ...cfg }, null, 2)); - } catch (e) { - console.error(e.message || e); - process.exit(1); - } - process.exit(0); -} - -if (sub === "doctor") { - const doctorArgs = args.slice(1); - if (doctorArgs.includes("--help") || doctorArgs.includes("-h")) { - console.log(` -用法: - flow2spec doctor [--json] - -只读检查 Node.js、项目配置、Agent 初始化、协作上下文、.task 忽略规则与知识库健康。 -警告不阻塞(exit 0),错误会返回 exit 1;本命令不会修改文件或访问网络。 -`.trim()); - process.exit(0); - } - const unknown = doctorArgs.filter((arg) => arg !== "--json"); - if (unknown.length > 0) { - console.error(`doctor 不支持参数:${unknown.join(" ")}`); - process.exit(1); - } - const report = runDoctor(process.cwd()); - if (doctorArgs.includes("--json")) { - printJson(report); - } else { - console.log(formatDoctorReport(report)); - } - process.exit(report.ok ? 0 : 1); -} - -if (sub === "kb") { - const kbSub = args[1]; - const kbFlags = new Set(args.slice(2).filter((arg) => String(arg || "").startsWith("--"))); - const kbPositionals = args.slice(2).filter((arg) => !String(arg || "").startsWith("--")); - const cwd = process.cwd(); - const jsonOut = kbFlags.has("--json"); - - try { - if (!kbSub || kbSub === "--help" || kbSub === "-h") { - printKnowledgeHelp(); - process.exit(0); - } - - if (kbSub === "status") { - const report = knowledgeEngine.summarizeKnowledgeState(cwd); - if (jsonOut) { - printJson(report); - } else { - console.log(`knowledge topics: ${report.topicCount}`); - console.log(`routing drift: ${report.routingDrift ? "yes" : "no"}`); - console.log(`validation: ${report.validation.ok ? "ok" : "has issues"}`); - if (report.validation.warnings.length) { - console.log(`warnings: ${report.validation.warnings.length}`); - } - if (report.tasks.length) { - console.log("active kb deltas:"); - for (const task of report.tasks) { - if (task.error) { - console.log(`- ${task.taskName}: ${task.error}`); - continue; - } - console.log( - `- ${task.taskName}: ${task.mergeable ? "mergeable" : "conflict"} (${task.plan.length} changes, ${task.conflicts.length} conflicts)`, - ); - } - } - } - process.exit(report.validation.ok ? 0 : 1); - } - - if (kbSub === "check") { - const strict = kbFlags.has("--strict"); - const graph = knowledgeEngine.loadKnowledgeGraph(cwd); - const validation = knowledgeEngine.validateKnowledgeGraph(graph, { - strictRevision: strict, - }); - const normalized = knowledgeEngine.normalizeRoutingWithGraph( - graph, - ); - const routingDrift = - knowledgeEngine.stableStringify(normalized.routing) !== - knowledgeEngine.stableStringify(graph.routing); - const report = knowledgeEngine.summarizeKnowledgeState(cwd); - const ok = - validation.issues.length === 0 && - !routingDrift && - (!strict || validation.warnings.length === 0); - const result = { - ok, - strict, - topicCount: report.topicCount, - issues: validation.issues, - warnings: validation.warnings, - routingDrift, - activeDeltas: report.tasks, - }; - if (jsonOut) { - printJson(result); - } else { - console.log(`knowledge check: ${result.ok ? "ok" : "failed"}`); - console.log(`topics: ${result.topicCount}`); - console.log(`routing drift: ${routingDrift ? "yes" : "no"}`); - if (result.issues.length) { - console.log(`issues: ${result.issues.length}`); - for (const issue of result.issues.slice(0, 10)) { - console.log(`- ${issue}`); - } - } - if (result.warnings.length) { - console.log(`warnings: ${result.warnings.length}`); - } - } - process.exit(result.ok ? 0 : 1); - } - - if (kbSub === "plan" || kbSub === "apply") { - const deltaArg = kbPositionals[0]; - if (!deltaArg) { - console.error(`kb ${kbSub} 需要 delta 文件路径`); - process.exit(1); - } - const deltaPath = path.resolve(cwd, deltaArg); - const dryRun = kbFlags.has("--dry-run") || kbSub === "plan"; - const graph = knowledgeEngine.loadKnowledgeGraph(cwd); - const delta = knowledgeEngine.parseKnowledgeDelta(deltaPath); - const plan = knowledgeEngine.planKnowledgeDelta(graph, delta); - if (kbSub === "plan") { - const result = { - ok: plan.mergeable, - deltaPath, - plan: plan.plan, - conflicts: plan.conflicts, - }; - if (jsonOut) { - printJson(result); - } else { - console.log(`kb plan: ${result.ok ? "mergeable" : "conflict"}`); - for (const item of result.plan) { - console.log( - `- ${item.topicId}: ${item.type} ${item.beforeRevision} -> ${item.afterRevision}`, - ); - } - for (const conflict of result.conflicts) { - console.log(`! ${conflict.topicId}: ${conflict.reason}`); - } - } - process.exit(result.ok ? 0 : 1); - } - const result = knowledgeEngine.applyKnowledgeDelta(cwd, deltaPath, { - dryRun, - }); - if (jsonOut) { - printJson(result); - } else { - console.log(`kb apply: ${dryRun ? "dry-run" : "applied"}`); - for (const file of result.changedFiles) { - console.log(`- ${file}`); - } - } - process.exit(0); - } - - if (kbSub === "build") { - const result = knowledgeEngine.buildKnowledgeGraph(cwd, { - writeTopicFrontmatter: kbFlags.has("--fix-topics"), - }); - if (jsonOut) { - printJson(result); - } else { - console.log(`kb build: ${result.changed ? "updated" : "up-to-date"}`); - console.log(`routing: ${path.relative(cwd, result.routingPath)}`); - if (result.topicFrontmatterChanged?.length) { - console.log(`topic frontmatter: ${result.topicFrontmatterChanged.length} updated`); - for (const file of result.topicFrontmatterChanged.slice(0, 10)) { - console.log(`- ${file}`); - } - } - console.log( - `validation: ${result.validation.ok ? "ok" : "has issues"}`, - ); - } - process.exit(result.validation.ok ? 0 : 1); - } - - console.error(`unknown kb subcommand: ${kbSub}`); - printKnowledgeHelp(); - process.exit(1); - } catch (e) { - console.error(e.message || e); - process.exit(1); - } -} - -if (sub === "init") { - const rawArgs = args.slice(1); - const overwriteKnowledge = rawArgs.includes("--reset-knowledge"); - const skipPrompts = rawArgs.includes("--yes") || rawArgs.includes("-y"); - let cliLocale; - const agentArgs = []; - for (let i = 0; i < rawArgs.length; i += 1) { - const arg = rawArgs[i]; - if (arg === "--reset-knowledge" || arg === "--yes" || arg === "-y") continue; - if (arg === "--locale") { - if (!rawArgs[i + 1] || rawArgs[i + 1].startsWith("--")) { - console.error(`--locale 需要取值。可选:${SUPPORTED_LOCALES.join(", ")}`); - process.exit(1); - } - cliLocale = String(rawArgs[i + 1] || "").trim(); - i += 1; - continue; - } - if (arg.startsWith("--locale=")) { - cliLocale = String(arg.slice("--locale=".length) || "").trim(); - continue; - } - agentArgs.push(arg); - } - if (cliLocale && !SUPPORTED_LOCALES.includes(cliLocale)) { - console.error(`不支持的 locale:${cliLocale}。可选:${SUPPORTED_LOCALES.join(", ")}`); - process.exit(1); - } - if (cliLocale) cliLocale = normalizeLocale(cliLocale); - - const cwd = process.cwd(); - - // ── 清除已输出的 n 行(用于多选 UI 重绘) - function clearLines(n) { - if (n <= 0) return; - process.stdout.write(`\x1b[${n}A\x1b[0J`); - } - - /** - * 多选 UI(raw mode):箭头键移动,空格选/取消,回车确认。 - * 非 TTY 环境直接返回默认选中项。 - */ - async function promptMultiSelect(title, items, defaultSelected = []) { - if (!process.stdin.isTTY || skipPrompts) { - return defaultSelected.length ? defaultSelected : [items[0].value]; - } - - const selected = new Set(defaultSelected.length ? defaultSelected : [items[0].value]); - let cursor = 0; - let rendered = 0; - - function render() { - if (rendered > 0) clearLines(rendered); - const lines = []; - lines.push(` ${title}`); - for (let i = 0; i < items.length; i++) { - const sel = selected.has(items[i].value); - const check = sel ? "\x1b[32m◉\x1b[0m" : "○"; - const arr = i === cursor ? "\x1b[36m›\x1b[0m" : " "; - const label = items[i].label.padEnd(10); - const desc = items[i].desc ? ` \x1b[2m${items[i].desc}\x1b[0m` : ""; - lines.push(` ${arr} ${check} ${label}${desc}`); - } - lines.push(""); - lines.push(" \x1b[2m↑↓ 移动 空格 选/取消 回车 确认\x1b[0m"); - rendered = lines.length; - process.stdout.write(lines.join("\n") + "\n"); - } - - render(); - - return new Promise((resolve) => { - function onKey(str, key) { - if (!key) return; - if (key.ctrl && key.name === "c") process.exit(0); - - if (key.name === "up") { - cursor = (cursor - 1 + items.length) % items.length; - render(); - } else if (key.name === "down") { - cursor = (cursor + 1) % items.length; - render(); - } else if (key.name === "space") { - const val = items[cursor].value; - if (selected.has(val)) selected.delete(val); - else selected.add(val); - render(); - } else if (key.name === "return") { - process.stdin.removeListener("keypress", onKey); - const result = selected.size ? [...selected] : [items[0].value]; - if (rendered > 0) clearLines(rendered); - const labels = result - .map((v) => items.find((i) => i.value === v)?.value) - .join(", "); - process.stdout.write(` ${title} \x1b[32m${labels}\x1b[0m\n`); - resolve(result); - } - } - process.stdin.on("keypress", onKey); - }); - } - - /** - * 单选 UI(raw mode):箭头键移动,回车确认。 - * 非 TTY 或 skipPrompts 时直接返回默认值。 - */ - async function promptSingleSelect(title, items, defaultValue) { - const fallback = defaultValue || items[0].value; - if (!process.stdin.isTTY || skipPrompts) return fallback; - - let cursor = Math.max(0, items.findIndex((item) => item.value === fallback)); - let rendered = 0; - - function render() { - if (rendered > 0) clearLines(rendered); - const lines = []; - lines.push(` ${title}`); - for (let i = 0; i < items.length; i++) { - const selected = i === cursor; - const check = selected ? "\x1b[32m◉\x1b[0m" : "○"; - const arr = selected ? "\x1b[36m›\x1b[0m" : " "; - const label = items[i].label.padEnd(10); - const desc = items[i].desc ? ` \x1b[2m${items[i].desc}\x1b[0m` : ""; - lines.push(` ${arr} ${check} ${label}${desc}`); - } - lines.push(""); - lines.push(" \x1b[2m↑↓ 移动 回车 确认\x1b[0m"); - rendered = lines.length; - process.stdout.write(lines.join("\n") + "\n"); - } - - render(); - - return new Promise((resolve) => { - function onKey(str, key) { - if (!key) return; - if (key.ctrl && key.name === "c") process.exit(0); - - if (key.name === "up") { - cursor = (cursor - 1 + items.length) % items.length; - render(); - } else if (key.name === "down") { - cursor = (cursor + 1) % items.length; - render(); - } else if (key.name === "return") { - process.stdin.removeListener("keypress", onKey); - const result = items[cursor]?.value || fallback; - if (rendered > 0) clearLines(rendered); - process.stdout.write(` ${title} \x1b[32m${result}\x1b[0m\n`); - resolve(result); - } - } - process.stdin.on("keypress", onKey); - }); - } - - /** - * 单键 y/n 问答(raw mode)。 - * 非 TTY 或 skipPrompts 时直接返回默认值。 - */ - async function promptBooleanKey(question, defaultValue = false) { - if (!process.stdin.isTTY || skipPrompts) return defaultValue; - - const hint = defaultValue - ? "\x1b[2m[Y/n]\x1b[0m" - : "\x1b[2m[y/N]\x1b[0m"; - process.stdout.write(` ${question} ${hint} `); - - return new Promise((resolve) => { - process.stdin.once("keypress", function (str, key) { - if (key && key.ctrl && key.name === "c") process.exit(0); - let result; - if (!str || str.trim() === "" || key?.name === "return") { - result = defaultValue; - } else { - result = str.trim().toLowerCase() === "y"; - } - process.stdout.write((result ? "\x1b[32my\x1b[0m" : "n") + "\n"); - resolve(result); - }); - }); - } - - async function promptLocale(question, defaultValue = "zh-CN") { - if (!process.stdin.isTTY || skipPrompts) return defaultValue; - const items = SUPPORTED_LOCALES.map((locale) => ({ - value: locale, - label: locale, - desc: locale === "zh-CN" ? "中文模板" : "English templates", - })); - return promptSingleSelect(question, items, defaultValue); - } - - async function collectInitOptions() { - const needAgentPrompt = agentArgs.length === 0 && !skipPrompts; - const missingFields = getMissingConfigFields(cwd); - const needConfigPrompt = missingFields.length > 0; - - // 没有任何需要处理的事情 - if (!needAgentPrompt && !needConfigPrompt) { - return { configValues: cliLocale ? { locale: cliLocale } : undefined, chosenAgents: agentArgs }; - } - - // --yes 模式:缺失字段直接用默认值,不弹交互 - if (skipPrompts) { - const configValues = needConfigPrompt - ? Object.fromEntries(missingFields.map((f) => [f.key, f.default])) - : undefined; - return { configValues, chosenAgents: agentArgs }; - } - - const isInteractive = process.stdin.isTTY; - if (isInteractive) { - readline.emitKeypressEvents(process.stdin); - process.stdin.setRawMode(true); - process.stdin.resume(); - } - - let chosenAgents = agentArgs; - let configValues; - - try { - process.stdout.write("\n"); - - if (needAgentPrompt) { - const agentItems = Object.entries(AGENTS).map(([id, { label }]) => ({ - value: id, - label: id, - desc: label, - })); - chosenAgents = await promptMultiSelect( - "选择要初始化的 AI 工具(可多选)", - agentItems, - ["cursor"], - ); - } - - if (needConfigPrompt) { - if (needAgentPrompt) process.stdout.write("\n"); - const isFirstTime = missingFields.length === CONFIG_FIELDS.length; - process.stdout.write( - ` 配置 ${CONFIG_FILENAME}${isFirstTime ? "(首次创建)" : "(补充新增字段)"}:\n\n`, - ); - const values = {}; - for (const field of missingFields) { - if (field.type === "locale") { - values[field.key] = cliLocale || await promptLocale(field.question, field.default); - } else { - values[field.key] = await promptBooleanKey( - field.question, - field.default, - ); - } - } - if (cliLocale) values.locale = cliLocale; - configValues = values; - } else if (cliLocale) { - configValues = { locale: cliLocale }; - } - } finally { - if (isInteractive) { - process.stdin.setRawMode(false); - process.stdin.pause(); - } - } - - process.stdout.write("\n"); - return { configValues, chosenAgents }; - } - - collectInitOptions() - .then(({ configValues, chosenAgents }) => - runInit(cwd, chosenAgents, { overwriteKnowledge, configValues, locale: cliLocale }), - ) - .then(({ ids, knowledgeResult, routingUpgrade, indexSnapshot, gitignoreResult, projectConfig, locale, claudeHooksResult }) => { - const lines = ids.map((id) => { - const { root, label } = AGENTS[id]; - if (id === "codex") - return ` - ${root}/:(${label})skills/、topics/、hooks/、hooks.json、AGENTS.md(指针);仓库根 AGENTS.md(完整)`; - if (id === "dsh") - return ` - ${root}/:(${label})skills/、topics/、AGENTS.md(指针);根 AGENTS.md(缺少时生成)`; - if (id === "claude") { - const hookLine = claudeHooksResult?.settingsChanged - ? "rules/、skills/、hooks/f2s-config-session.js、hooks/f2s-config-inject.js、settings.json(已写入 f2s SessionStart/PreToolUse hooks)" - : "rules/、skills/(settings.json 中 f2s hook 已存在,跳过)"; - return ` - ${root}/:(${label})${hookLine}`; - } - return ` - ${root}/:(${label})rules/、skills/`; - }); - const knowledgeLine = overwriteKnowledge - ? " - .Knowledge/:已按 --reset-knowledge 强制覆盖模板" - : ` - .Knowledge/:保留已有内容,补齐缺失模板(新增 ${knowledgeResult?.written || 0},跳过 ${knowledgeResult?.skipped || 0})`; - const routingLine = overwriteKnowledge - ? " - .Knowledge/manifest-routing.json + .Knowledge/matchers/*:已随 reset 覆盖到模板版本(不再写入 manifest-matchers.json)" - : routingUpgrade?.upgraded - ? " - 路由清单已与模板增量对齐" - : " - 路由清单已是最新能力路由,无需变更"; - const indexLine = - indexSnapshot?.written === false - ? ` - .Knowledge/template/index.template.md:未复制(${indexSnapshot?.reason || "skip"})` - : ` - .Knowledge/template/index.template.md:已从包内 templates/${locale}/knowledge/index.md 复制(与 .Knowledge/index.md 对照见 f2s-kb-upgrade 技能)`; - const pc = projectConfig || {}; - const configLine = ` - ${CONFIG_FILENAME}:locale=${pc.locale || locale}, subAgent=${Boolean(pc.subAgent)}, switchAgentVerification=${Boolean(pc.switchAgentVerification)}`; - const gitignoreLine = gitignoreResult?.changed - ? ` - .gitignore:已补充 ${gitignoreResult.added.join(", ")}` - : " - .gitignore:Flow2Spec 本地态忽略项已存在"; - console.log(` -✓ Flow2Spec init 完成 -${knowledgeLine} -${routingLine} -${indexLine} -${gitignoreLine} -${configLine} -${lines.join("\n")} - -建议阅读 README 或 docs/使用说明.md,按「规则在配置根、文档在 .Knowledge」的方式使用。 -`); - maybeAutoUpdateGlobalInstall(); - maybePrintUpdateNotice(); - }) - .catch((e) => { - console.error(e.message || e); - process.exit(1); - }); -} else { - console.log(help.trim()); - process.exit(1); -} +require("./packages/cli/cli.js"); diff --git "a/docs/\345\217\221\345\270\203\344\270\216\351\203\250\347\275\262.md" "b/docs/\345\217\221\345\270\203\344\270\216\351\203\250\347\275\262.md" index 3aef7e9..5d300e1 100644 --- "a/docs/\345\217\221\345\270\203\344\270\216\351\203\250\347\275\262.md" +++ "b/docs/\345\217\221\345\270\203\344\270\216\351\203\250\347\275\262.md" @@ -6,7 +6,7 @@ Flow2Spec 使用 GitHub Actions 分别处理持续集成、网站部署和 npm `.github/workflows/ci.yml` 在 Pull Request 和 `main` 推送时执行: -- Node.js 18、20、22 下运行 CLI 测试与 npm 打包检查; +- Node.js 18、20、22 下运行 CLI、Core API、tgz 安装回归与 npm 打包检查; - Node.js 22 下安装网站依赖并构建 Astro 静态站点。 ## 网站部署 @@ -20,13 +20,14 @@ Flow2Spec 使用 GitHub Actions 分别处理持续集成、网站部署和 npm `.github/workflows/publish-npm.yml` 只在 GitHub Release 发布时触发。发布前依次校验: 1. Release 对应的提交属于 `main`; -2. Tag(`v3.3.0` 或 `V3.3.0`)与 `package.json` 版本一致; -3. CLI 测试通过; -4. `npm pack --dry-run` 通过。 +2. 根 workspace、Core、CLI 三处版本一致,且 CLI 对 Core 的依赖版本一致; +3. Tag(`v3.3.0` 或 `V3.3.0`)与 workspace 版本一致; +4. CLI、Core API 和安装回归测试通过; +5. 两个包的 `npm pack --dry-run` 通过。 -全部通过后,工作流使用 npm Trusted Publishing(OIDC)执行带 provenance 的公开发布,不需要在 GitHub 保存长期 `NPM_TOKEN`。 +全部通过后,工作流使用 npm Trusted Publishing(OIDC)执行带 provenance 的公开发布,不需要在 GitHub 保存长期 `NPM_TOKEN`。发布顺序固定为先 `@double-coding/flow2spec-core`,再 `@double-coding/flow2spec`,确保 CLI 安装时可以解析同版本 Core。 -首次启用时,在 npmjs.com 的 `@double-coding/flow2spec` 包设置中添加 GitHub Actions Trusted Publisher: +首次启用时,在 npmjs.com 的 `@double-coding/flow2spec-core` 和 `@double-coding/flow2spec` 两个包设置中分别添加 GitHub Actions Trusted Publisher: | 字段 | 值 | | --- | --- | @@ -37,10 +38,12 @@ Flow2Spec 使用 GitHub Actions 分别处理持续集成、网站部署和 npm 正式发布顺序: -1. 在分支中修改 `package.json` 版本并完成 PR; +1. 在分支中同步修改根 `package.json`、`packages/core/package.json` 和 `packages/cli/package.json` 版本,并完成 PR; 2. PR 合并到 `main`; -3. 在合并提交上创建匹配版本的 Git Tag; +3. 在合并提交上创建匹配版本的 Git Tag(可运行 `npm run tag:version`); 4. 基于该 Tag 创建并发布 GitHub Release; -5. 在 Actions 中确认 `Publish package to npm` 成功。 +5. 在 Actions 中确认 Core 与 CLI 两个发布步骤均成功。 + +根目录 workspace 仅用于统一开发、测试和版本校验,不会发布到 npm。普通用户继续安装 `@double-coding/flow2spec`;原生开发工具插件直接依赖 `@double-coding/flow2spec-core`。 失败的发布不得通过修改同一版本重试;修复后递增版本,再创建新的 Tag 与 Release。 diff --git a/lib/agents.js b/lib/agents.js index d6937e7..8fe9444 100644 --- a/lib/agents.js +++ b/lib/agents.js @@ -1,49 +1 @@ -/** - * flow2spec init 支持的 AI 工具配置目录。 - * 知识库统一写入项目根 `.Knowledge/`(含 template),rules/skills 保留在各配置根。 - */ -const AGENTS = { - cursor: { root: ".cursor", label: "Cursor" }, - claude: { root: ".claude", label: "Claude" }, - codex: { root: ".codex", label: "Codex" }, - dsh: { root: ".dsh", label: "DeepSeek Harness" }, -}; - -const KNOWLEDGE_ROOT = ".Knowledge"; -const KNOWLEDGE_SUBDIRS = ["stock-docs", "req-docs", "matchers"]; -const AGENT_SUBDIRS = { - cursor: ["rules", "skills"], - claude: ["rules", "skills"], - codex: ["skills"], - dsh: ["skills", "topics"], -}; - -/** - * @param {string[]} argv init 后的参数,如 []、['cursor']、['cursor','claude'] - * @returns {string[]} 去重后的 agent id 列表 - */ -function normalizeAgentIds(argv) { - const list = argv.length ? argv : ["cursor"]; - const seen = new Set(); - const out = []; - for (const raw of list) { - const id = String(raw).toLowerCase().replace(/^--/, ""); - if (!AGENTS[id]) { - const keys = Object.keys(AGENTS).join(", "); - throw new Error(`未知 agent:${raw}。可选:${keys}`); - } - if (!seen.has(id)) { - seen.add(id); - out.push(id); - } - } - return out; -} - -module.exports = { - AGENTS, - KNOWLEDGE_ROOT, - KNOWLEDGE_SUBDIRS, - AGENT_SUBDIRS, - normalizeAgentIds, -}; +module.exports = require("@double-coding/flow2spec-core").legacy.agents; diff --git a/lib/claudeRulesAdapter.js b/lib/claudeRulesAdapter.js index 032d8f7..73ea2d2 100644 --- a/lib/claudeRulesAdapter.js +++ b/lib/claudeRulesAdapter.js @@ -1,28 +1 @@ -/** - * Cursor 规则为 .mdc + frontmatter globs、alwaysApply。 - * Claude Code 仅识别 .claude/rules 下扩展名为 .md 的规则文件,路径范围用 paths(见 Claude Code 文档:Organize rules with .claude/rules)。 - */ - -/** - * @param {string} mdcSource 完整 .mdc 文件正文 - * @returns {string} 写入 `.claude/rules/*.md` 的正文 - */ -function adaptRuleMdcToClaudeMd(mdcSource) { - let out = mdcSource; - // YAML:globs → paths(与 Cursor 语义等价) - out = out.replace(/^globs:/m, "paths:"); - // Claude Code 不按 Cursor 的 alwaysApply 解析;无 paths 的规则与会话同载 - out = out.replace(/^\s*alwaysApply:\s*(true|false)\s*\r?\n/m, ""); - // 正文与示例中的 .mdc 引用改为 .md,与落盘扩展名一致 - out = out.replace(/\.mdc\b/g, ".md"); - return out; -} - -/** - * @param {string} agentRoot AGENTS[id].root,如 `.claude` - */ -function shouldWriteClaudeStyleRules(agentRoot) { - return agentRoot === ".claude"; -} - -module.exports = { adaptRuleMdcToClaudeMd, shouldWriteClaudeStyleRules }; +module.exports = require("@double-coding/flow2spec-core").legacy.claudeRulesAdapter; diff --git a/lib/claudeSettingsAdapter.js b/lib/claudeSettingsAdapter.js index 44d9d9e..bf471c3 100644 --- a/lib/claudeSettingsAdapter.js +++ b/lib/claudeSettingsAdapter.js @@ -1,228 +1 @@ -'use strict'; -/** - * 负责合并 flow2spec hook 配置到 .claude/settings.json。 - * 仅在 init --claude 时调用。 - */ -const fs = require('fs'); -const path = require('path'); - -const HOOK_COMMAND_CONFIG_INJECT = 'node .claude/hooks/f2s-config-inject.js'; -const HOOK_COMMAND_CONFIG_SESSION = 'node .claude/hooks/f2s-config-session.js'; -const HOOK_COMMAND_UPDATE_CHECK = 'node .claude/hooks/f2s-update-check.js'; - -// 向下兼容 -const HOOK_COMMAND = HOOK_COMMAND_CONFIG_INJECT; - -function findPackageJsonDir(startDir) { - let cur = startDir; - while (cur && cur !== path.dirname(cur)) { - if (fs.existsSync(path.join(cur, 'package.json'))) return cur; - cur = path.dirname(cur); - } - return null; -} - -function hasHookCommand(arr, fragment) { - if (!Array.isArray(arr)) return false; - for (const group of arr) { - if (!group || !Array.isArray(group.hooks)) continue; - for (const h of group.hooks) { - if (h && h.type === 'command' && String(h.command || '').includes(fragment)) { - return true; - } - } - } - return false; -} - -function removeHookCommand(arr, fragment) { - if (!Array.isArray(arr)) return []; - const next = []; - for (const group of arr) { - if (!group || !Array.isArray(group.hooks)) { - next.push(group); - continue; - } - const hooks = group.hooks.filter((h) => { - return !(h && h.type === 'command' && String(h.command || '').includes(fragment)); - }); - if (hooks.length) next.push(Object.assign({}, group, { hooks })); - } - return next; -} - -// 旧函数名保持兼容 -function hasF2sHook(preToolUseArr) { - return hasHookCommand(preToolUseArr, 'f2s-config-inject'); -} - -/** - * 将 f2s PreToolUse 守门 hook 合并进现有 settings,返回新对象(不修改原对象)。 - * @param {object} existing 现有 settings(可为 {}) - * @returns {object} - */ -function mergeF2sHook(existing) { - const next = JSON.parse(JSON.stringify(existing || {})); - if (!next.hooks) next.hooks = {}; - if (!next.hooks.PreToolUse) next.hooks.PreToolUse = []; - - if (hasF2sHook(next.hooks.PreToolUse)) { - return { settings: next, changed: false }; - } - - next.hooks.PreToolUse.push({ - matcher: 'Skill', - hooks: [{ type: 'command', command: HOOK_COMMAND }], - }); - - return { settings: next, changed: true }; -} - -/** - * 将 f2s SessionStart 配置摘要 hook 合并进 settings。 - * @param {object} existing - * @returns {{ settings, changed }} - */ -function mergeConfigSessionHook(existing) { - const next = JSON.parse(JSON.stringify(existing || {})); - if (!next.hooks) next.hooks = {}; - if (!next.hooks.SessionStart) next.hooks.SessionStart = []; - - if (hasHookCommand(next.hooks.SessionStart, 'f2s-config-session')) { - return { settings: next, changed: false }; - } - - next.hooks.SessionStart.push({ - hooks: [{ type: 'command', command: HOOK_COMMAND_CONFIG_SESSION }], - }); - - return { settings: next, changed: true }; -} - -/** - * 将 f2s 更新检查 hook 合并进 settings: - * - SessionStart:执行完整检测,写入 .Knowledge/update-check.json,并直接 emit 提示 - * - 同时清理旧版 UserPromptSubmit 中的 f2s-update-check / f2s-update-notice - * @param {object} existing - * @returns {{ settings, changed }} - */ -function mergeUpdateCheckHook(existing) { - const next = JSON.parse(JSON.stringify(existing || {})); - if (!next.hooks) next.hooks = {}; - if (!next.hooks.SessionStart) next.hooks.SessionStart = []; - - let changed = false; - - if (Array.isArray(next.hooks.UserPromptSubmit)) { - const before = JSON.stringify(next.hooks.UserPromptSubmit); - next.hooks.UserPromptSubmit = removeHookCommand(next.hooks.UserPromptSubmit, 'f2s-update-check'); - next.hooks.UserPromptSubmit = removeHookCommand(next.hooks.UserPromptSubmit, 'f2s-update-notice'); - if (JSON.stringify(next.hooks.UserPromptSubmit) !== before) changed = true; - if (next.hooks.UserPromptSubmit.length === 0) { - delete next.hooks.UserPromptSubmit; - } - } - - if (!hasHookCommand(next.hooks.SessionStart, 'f2s-update-check')) { - next.hooks.SessionStart.push({ - hooks: [{ type: 'command', command: HOOK_COMMAND_UPDATE_CHECK }], - }); - changed = true; - } - - return { settings: next, changed }; -} - -/** - * 读取 .claude/settings.json(不存在则返回 {})。 - * @param {string} claudeRoot .claude 目录绝对路径 - * @returns {object} - */ -function readSettings(claudeRoot) { - const settingsPath = path.join(claudeRoot, 'settings.json'); - if (!fs.existsSync(settingsPath)) return {}; - try { - return JSON.parse(fs.readFileSync(settingsPath, 'utf8')); - } catch (_err) { - return {}; - } -} - -/** - * 写入 .claude/settings.json。 - * @param {string} claudeRoot - * @param {object} settings - */ -function writeSettings(claudeRoot, settings) { - const settingsPath = path.join(claudeRoot, 'settings.json'); - fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, 'utf8'); -} - -/** - * 复制 hook 脚本到 .claude/hooks/。 - */ -function copyHookScript(claudeRoot, templatesDir, scriptName) { - const src = path.join(templatesDir, 'hooks', scriptName); - if (!fs.existsSync(src)) return { written: false, reason: 'missing-template' }; - - const hooksDir = path.join(claudeRoot, 'hooks'); - if (!fs.existsSync(hooksDir)) fs.mkdirSync(hooksDir, { recursive: true }); - - let body = fs.readFileSync(src, 'utf8'); - if (body.includes('__FLOW2SPEC_PACKAGE_NAME__')) { - let packageName = '@double-coding/flow2spec'; - try { - const packageDir = findPackageJsonDir(templatesDir); - packageName = JSON.parse( - fs.readFileSync(path.join(packageDir || path.join(templatesDir, '..'), 'package.json'), 'utf8'), - ).name || packageName; - } catch (_) {} - body = body.replace(/__FLOW2SPEC_PACKAGE_NAME__/g, packageName); - } - fs.writeFileSync(path.join(hooksDir, scriptName), body, 'utf8'); - return { written: true }; -} - -/** - * 主入口:为 claude agent 配置 f2s hooks(SessionStart 配置摘要 + PreToolUse 守门 + 更新检测/提示)。 - * @param {string} cwd - * @param {string} templatesDir - * @returns {{ hookScriptResult, updateCheckResult, settingsChanged }} - */ -function writeClaudeAgentHooks(cwd, templatesDir) { - const claudeRoot = path.join(cwd, '.claude'); - if (!fs.existsSync(claudeRoot)) fs.mkdirSync(claudeRoot, { recursive: true }); - - const hookScriptResult = copyHookScript(claudeRoot, templatesDir, 'f2s-config-inject.js'); - const configSessionResult = copyHookScript(claudeRoot, templatesDir, 'f2s-config-session.js'); - const updateCheckResult = copyHookScript(claudeRoot, templatesDir, 'f2s-update-check.js'); - - // 清理旧版残留的 f2s-update-notice.js - const noticeStale = path.join(claudeRoot, 'hooks', 'f2s-update-notice.js'); - if (fs.existsSync(noticeStale)) { - try { fs.unlinkSync(noticeStale); } catch (_) {} - } - - let settings = readSettings(claudeRoot); - let changed = false; - - const r1 = mergeF2sHook(settings); - if (r1.changed) { settings = r1.settings; changed = true; } - - const r2 = mergeConfigSessionHook(settings); - if (r2.changed) { settings = r2.settings; changed = true; } - - const r3 = mergeUpdateCheckHook(settings); - if (r3.changed) { settings = r3.settings; changed = true; } - - if (changed) writeSettings(claudeRoot, settings); - - return { hookScriptResult, configSessionResult, updateCheckResult, settingsChanged: changed }; -} - -module.exports = { - writeClaudeAgentHooks, - mergeF2sHook, - mergeConfigSessionHook, - mergeUpdateCheckHook, -}; +module.exports = require("@double-coding/flow2spec-core").legacy.claudeSettingsAdapter; diff --git a/lib/codexAgentsAdapter.js b/lib/codexAgentsAdapter.js index b217732..d570649 100644 --- a/lib/codexAgentsAdapter.js +++ b/lib/codexAgentsAdapter.js @@ -1,72 +1 @@ -const fs = require("fs"); -const path = require("path"); - -function readSkillSummary(skillsDir) { - if (!fs.existsSync(skillsDir)) return []; - const out = []; - for (const name of fs.readdirSync(skillsDir)) { - // Try SKILL.md first, then SKILL.mdc - let skillFile = path.join(skillsDir, name, "SKILL.md"); - if (!fs.existsSync(skillFile)) { - skillFile = path.join(skillsDir, name, "SKILL.mdc"); - } - if (!fs.existsSync(skillFile)) continue; - const raw = fs.readFileSync(skillFile, "utf8"); - const frontmatter = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/); - const body = frontmatter ? frontmatter[1] : ""; - const skillName = (body.match(/^\s*name:\s*(.+)\s*$/m) || [])[1] || name; - const desc = (body.match(/^\s*description:\s*(.+)\s*$/m) || [])[1] || "暂无描述"; - out.push(`- \`${skillName.trim()}\`:${desc.trim()}`); - } - return out.sort((a, b) => a.localeCompare(b, "zh-Hans-CN")); -} - -function renderProjectConfigBlock() { - return [ - "| 配置项 | init 默认 | 说明 |", - "| --- | --- | --- |", - "| `subAgent` | `true` | 技能正文写明某步可用子 agent 时,`true` 才允许拆子;`false` 一律主会话完成。用户「动态判断谁用子 agent」仅当本项为 `true` 时有效。 |", - "| `switchAgentVerification` | `true` | 切换 agent 校验。仅当本项为 `true` 且当前技能正文明确绑定该字段时启用交叉校验;否则仍是谁落盘谁自验。旧键 `subAgentVerification` 仍可被解析。 |", - "| `intentRecognition` | `true` | `true` 时可按 `f2s-intent-routing` 对高置信操作意图自动进入对应 `f2s-*` 技能;`false` 或缺失时不自动分流。 |", - "| `changeTracking.feat` | `true` | `true` 时 `f2s-kb-feat` 步骤 0 必须创建/续作 `.task/active/` 变更追踪任务;`false` 时跳过。 |", - "| `changeTracking.fix` | `false` | `true` 时 `f2s-kb-fix` 步骤 0 必须创建/续作 `.task/active/` 变更追踪任务;`false` 时跳过。 |", - "| `changeTracking.implement` | `true` | `true` 时 `f2s-implement-tech-design` 写入任务清单并在满足归档门禁后归档;`false` 时跳过变更追踪部分。 |", - "| `collaboration.enabled` | `true` | `true` 时按 developerId 隔离任务根 `.task//`;`false` 时始终 legacy 单根 `.task/`。 |", - "| `collaboration.developerId` | `\"\"` | 非空则作为任务进度目录名;空则尝试 git user.email/name 规范化;仍无则 legacy `.task/`。解析顺序:config → git → legacy。 |", - ].join("\n"); -} - -function renderCodexAgents(templateBody, skillsSummaryLines) { - const summary = - skillsSummaryLines.length > 0 - ? skillsSummaryLines.join("\n") - : "- 当前未发现可用技能。"; - let body = templateBody.replace( - "{{FLOW2SPEC_PROJECT_CONFIG}}", - renderProjectConfigBlock(), - ); - body = body.replace("{{FLOW2SPEC_CODEX_SKILLS_SUMMARY}}", summary); - return body; -} - -function buildCodexAgentsMd(templatesDir, projectConfig) { - const templatePath = path.join(templatesDir, "AGENTS.md"); - const skillsDir = path.join(templatesDir, "skills"); - const templateBody = fs.readFileSync(templatePath, "utf8"); - const skillLines = readSkillSummary(skillsDir); - return renderCodexAgents(templateBody, skillLines); -} - -function buildCodexAgentsStubMd(templatesDir) { - const stubPath = path.join(templatesDir, "AGENTS.codex-stub.md"); - if (!fs.existsSync(stubPath)) { - throw new Error(`缺少 Codex 指针模板:${stubPath}`); - } - return fs.readFileSync(stubPath, "utf8"); -} - -module.exports = { - buildCodexAgentsMd, - buildCodexAgentsStubMd, - renderProjectConfigBlock, -}; +module.exports = require("@double-coding/flow2spec-core").legacy.codexAgentsAdapter; diff --git a/lib/developerId.js b/lib/developerId.js index 03fb274..154ff1d 100644 --- a/lib/developerId.js +++ b/lib/developerId.js @@ -1,260 +1 @@ -/** - * 多人协作:developerId 解析与任务根路径。 - * - * 优先级(已定口径,勿再加 env/local 层): - * 1. flow2spec.config.json → collaboration.developerId - * - 非空但 sanitize 后为空(如纯中文、纯符号):抛错,让用户显式修正配置。 - * - 显式配置视为「用户明确表达了隔离意图」,不做静默降级。 - * 2. git user.email(@ 前)或 user.name,规范化 - * - 若规范化失败(如纯中文邮箱前缀 / 用户名),走 hash 兜底: - * 基于原始字符串 sha256 前 8 位生成 `dev-xxxxxxxx`,同时在 warnings 中提示。 - * 这样避免中文用户被静默塞回 legacy 单根。 - * 3. 都没有 → null(调用方使用 legacy `.task/` 根) - * - * collaboration.enabled === false 时强制 legacy(返回 null)。 - * - * 需要「是否隔离 / 是否 legacy」语义的调用方**请用 resolveDeveloperContext**; - * taskRootFor 只做拼路径,不看 enabled 开关,仅供内部/外部工具在已确定 id 的 - * 情形下拼路径。 - */ - -const { execFileSync } = require("child_process"); -const crypto = require("crypto"); -const path = require("path"); - -const TASK_DIR = ".task"; -const HASH_FALLBACK_PREFIX = "dev-"; - -/** - * @param {string} raw - * @returns {string|null} sanitize 后的 id;非法则 null - */ -function sanitizeDeveloperId(raw) { - if (raw == null) return null; - let s = String(raw).trim().toLowerCase(); - if (!s) return null; - // email → local part - if (s.includes("@")) { - s = s.split("@")[0] || ""; - } - s = s - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-+|-+$/g, "") - .replace(/-{2,}/g, "-"); - if (s.length < 1 || s.length > 64) return null; - return s; -} - -/** - * 基于原始字符串生成稳定的 hash 兜底 id,例如 `dev-a1b2c3d4`。 - * 用于 git identity 存在但 sanitize 失败(如纯中文)的情况, - * 保证隔离仍然按人生效、跨机器一致。 - * @param {string} raw - * @returns {string|null} - */ -function hashDeveloperId(raw) { - if (raw == null) return null; - const trimmed = String(raw).trim(); - if (!trimmed) return null; - const digest = crypto.createHash("sha256").update(trimmed).digest("hex"); - return `${HASH_FALLBACK_PREFIX}${digest.slice(0, 8)}`; -} - -/** - * @param {string} [cwd] - * @returns {{ email: string|null, name: string|null }} - */ -function readGitIdentity(cwd) { - const opts = { - encoding: "utf8", - timeout: 3000, - stdio: ["ignore", "pipe", "ignore"], - cwd: cwd || process.cwd(), - }; - let email = null; - let name = null; - try { - email = execFileSync("git", ["config", "user.email"], opts).trim() || null; - } catch { - email = null; - } - try { - name = execFileSync("git", ["config", "user.name"], opts).trim() || null; - } catch { - name = null; - } - return { email, name }; -} - -/** - * @param {object} config loadFlow2specConfig 的返回值 - * @param {object} [options] - * @param {string} [options.cwd] - * @param {{ email?: string|null, name?: string|null }} [options.gitIdentity] 测试注入 - * @param {boolean} [options.skipGit] - * @returns {{ - * developerId: string|null, - * source: 'config'|'git-email'|'git-name'|'git-email-hash'|'git-name-hash'|'legacy', - * legacy: boolean, - * taskRoot: string, - * enabled: boolean, - * warnings: string[], - * }} - * @throws {Error} 当 collaboration.developerId 非空但 sanitize 后为空时。 - */ -function resolveDeveloperContext(config, options = {}) { - const cwd = options.cwd || process.cwd(); - const collab = - config && config.collaboration && typeof config.collaboration === "object" - ? config.collaboration - : {}; - const enabled = collab.enabled !== false; // 缺省 true:有 id 就隔离;无 id 仍 legacy - - if (!enabled) { - return { - developerId: null, - source: "legacy", - legacy: true, - taskRoot: TASK_DIR, - enabled: false, - warnings: [], - }; - } - - const warnings = []; - - // 1) 显式 config:非空但非法 → 抛错,防止静默降级 - const rawConfigId = - typeof collab.developerId === "string" ? collab.developerId.trim() : ""; - if (rawConfigId) { - const fromConfig = sanitizeDeveloperId(rawConfigId); - if (!fromConfig) { - throw new Error( - `flow2spec.config.json → collaboration.developerId "${rawConfigId}" 无法规范化为 [a-z0-9-]。` + - `请改用英文/数字标识(如 "alice"),或留空让 Flow2Spec 从 git 身份推断。`, - ); - } - return { - developerId: fromConfig, - source: "config", - legacy: false, - taskRoot: path.posix.join(TASK_DIR, fromConfig), - enabled: true, - warnings, - }; - } - - // 2) git identity:先直接 sanitize,失败则 hash 兜底并 warn - const git = - options.gitIdentity || - (options.skipGit ? { email: null, name: null } : readGitIdentity(cwd)); - - if (git.email) { - const fromEmail = sanitizeDeveloperId(git.email); - if (fromEmail) { - return { - developerId: fromEmail, - source: "git-email", - legacy: false, - taskRoot: path.posix.join(TASK_DIR, fromEmail), - enabled: true, - warnings, - }; - } - const hashed = hashDeveloperId(git.email); - if (hashed) { - warnings.push( - `git user.email "${git.email}" 无法直接规范化,已回退到 hash id "${hashed}"。` + - `建议在 flow2spec.config.json 显式配置 collaboration.developerId 以获得可读的目录名。`, - ); - return { - developerId: hashed, - source: "git-email-hash", - legacy: false, - taskRoot: path.posix.join(TASK_DIR, hashed), - enabled: true, - warnings, - }; - } - } - - if (git.name) { - const fromName = sanitizeDeveloperId(git.name); - if (fromName) { - return { - developerId: fromName, - source: "git-name", - legacy: false, - taskRoot: path.posix.join(TASK_DIR, fromName), - enabled: true, - warnings, - }; - } - const hashed = hashDeveloperId(git.name); - if (hashed) { - warnings.push( - `git user.name "${git.name}" 无法直接规范化,已回退到 hash id "${hashed}"。` + - `建议在 flow2spec.config.json 显式配置 collaboration.developerId 以获得可读的目录名。`, - ); - return { - developerId: hashed, - source: "git-name-hash", - legacy: false, - taskRoot: path.posix.join(TASK_DIR, hashed), - enabled: true, - warnings, - }; - } - } - - return { - developerId: null, - source: "legacy", - legacy: true, - taskRoot: TASK_DIR, - enabled: true, - warnings, - }; -} - -/** - * 仅用于「已确定 id」时的路径拼接。**不检查 collaboration.enabled**; - * 需要开关语义的调用方请用 resolveDeveloperContext。 - * @param {string|null|undefined} developerId - * @returns {string} posix 风格相对路径,如 `.task` 或 `.task/alice` - */ -function taskRootFor(developerId) { - const id = sanitizeDeveloperId(developerId); - if (!id) return TASK_DIR; - return path.posix.join(TASK_DIR, id); -} - -function todoJsonPath(taskRoot) { - return path.posix.join(taskRoot || TASK_DIR, "todo.json"); -} - -function activeTaskDir(taskRoot, taskName) { - return path.posix.join(taskRoot || TASK_DIR, "active", taskName); -} - -function completedTaskDir(taskRoot, taskName, yyyymmdd) { - const date = yyyymmdd || "YYYYMMDD"; - return path.posix.join( - taskRoot || TASK_DIR, - "completed", - `${date}-${taskName}`, - ); -} - -module.exports = { - TASK_DIR, - HASH_FALLBACK_PREFIX, - sanitizeDeveloperId, - hashDeveloperId, - readGitIdentity, - resolveDeveloperContext, - taskRootFor, - todoJsonPath, - activeTaskDir, - completedTaskDir, -}; +module.exports = require("@double-coding/flow2spec-core").legacy.developerId; diff --git a/lib/doctor.js b/lib/doctor.js index b833ecd..17ea889 100644 --- a/lib/doctor.js +++ b/lib/doctor.js @@ -1,348 +1 @@ -const fs = require("fs"); -const path = require("path"); - -const { AGENTS } = require("./agents"); -const { - loadFlow2specConfig, - CONFIG_FILENAME, -} = require("./flow2specConfig"); -const { resolveDeveloperContext } = require("./developerId"); -const knowledgeEngine = require("./knowledgeEngine"); - -const STATUS = { - pass: "pass", - warning: "warning", - error: "error", -}; - -function numericVersion(version) { - return String(version || "") - .replace(/^v/, "") - .split(".") - .slice(0, 3) - .map((part) => Number.parseInt(part, 10) || 0); -} - -function compareVersions(left, right) { - const a = numericVersion(left); - const b = numericVersion(right); - for (let index = 0; index < 3; index += 1) { - const difference = (a[index] || 0) - (b[index] || 0); - if (difference !== 0) return difference; - } - return 0; -} - -function satisfiesNodeEngine(version, engine) { - const minimum = String(engine || "").match(/>=\s*v?(\d+(?:\.\d+){0,2})/); - if (!minimum) return true; - return compareVersions(version, minimum[1]) >= 0; -} - -function makeCheck(id, label, status, message, repair = null, details) { - const check = { id, label, status, message, repair }; - if (details !== undefined) check.details = details; - return check; -} - -function checkKnowledge(cwd) { - try { - const graph = knowledgeEngine.loadKnowledgeGraph(cwd); - const validation = knowledgeEngine.validateKnowledgeGraph(graph, { - strictRevision: true, - }); - const normalized = knowledgeEngine.normalizeRoutingWithGraph(graph); - const routingDrift = - normalized.changed || - knowledgeEngine.stableStringify(normalized.routing) !== - knowledgeEngine.stableStringify(graph.routing); - const details = { - topicCount: validation.topicCount, - issues: validation.issues, - warnings: validation.warnings, - routingDrift, - }; - - if (!validation.ok || routingDrift) { - const reasons = [...validation.issues]; - if (routingDrift) reasons.push("routing metadata differs from topic frontmatter"); - return makeCheck( - "knowledge", - "知识库", - STATUS.error, - `知识图存在 ${reasons.length} 个问题。`, - "运行 flow2spec kb build --fix-topics,再运行 flow2spec kb check --strict。", - details, - ); - } - if (validation.warnings.length > 0) { - return makeCheck( - "knowledge", - "知识库", - STATUS.warning, - `知识图可用,但有 ${validation.warnings.length} 条警告。`, - "运行 flow2spec kb check --strict 查看详情。", - details, - ); - } - return makeCheck( - "knowledge", - "知识库", - STATUS.pass, - `${validation.topicCount} 个 topic 校验通过,routing 无漂移。`, - null, - details, - ); - } catch (error) { - return makeCheck( - "knowledge", - "知识库", - STATUS.error, - error.message || String(error), - "确认 .Knowledge/manifest-routing.json 与其引用的 topic、matcher 均存在且为有效格式。", - ); - } -} - -function isIgnoredByRootGitignore(cwd, entry) { - const gitignore = path.join(cwd, ".gitignore"); - if (!fs.existsSync(gitignore)) return false; - const lines = fs - .readFileSync(gitignore, "utf8") - .split(/\r?\n/) - .map((line) => line.trim()) - .filter((line) => line && !line.startsWith("#")); - return lines.includes(entry) || lines.includes(entry.replace(/\/$/, "")); -} - -function runDoctor(cwd = process.cwd(), options = {}) { - const pkg = options.package || require("../package.json"); - const nodeVersion = options.nodeVersion || process.version; - const knowledgeCheck = options.knowledgeCheck || checkKnowledge; - const checks = []; - - const engine = pkg.engines?.node || ""; - const runtimeOk = satisfiesNodeEngine(nodeVersion, engine); - checks.push( - makeCheck( - "runtime", - "Node.js", - runtimeOk ? STATUS.pass : STATUS.error, - runtimeOk - ? `${nodeVersion} 满足 ${engine || "包要求"}。` - : `${nodeVersion} 不满足 ${engine}。`, - runtimeOk ? null : `升级 Node.js 到满足 ${engine} 的版本。`, - { version: nodeVersion, required: engine }, - ), - ); - - const configPath = path.join(cwd, CONFIG_FILENAME); - let config = null; - if (!fs.existsSync(configPath)) { - checks.push( - makeCheck( - "config", - "项目配置", - STATUS.error, - `缺少 ${CONFIG_FILENAME}。`, - "在项目根运行 flow2spec init。", - ), - ); - } else { - try { - config = loadFlow2specConfig(cwd); - checks.push( - makeCheck( - "config", - "项目配置", - STATUS.pass, - `${CONFIG_FILENAME} 存在且可解析。`, - null, - { locale: config.locale }, - ), - ); - } catch (error) { - checks.push( - makeCheck( - "config", - "项目配置", - STATUS.error, - error.message || String(error), - `修正 ${CONFIG_FILENAME} 的 JSON 格式。`, - ), - ); - } - } - - const agentsPath = path.join(cwd, "AGENTS.md"); - checks.push( - fs.existsSync(agentsPath) - ? makeCheck("agents-entry", "项目入口", STATUS.pass, "根 AGENTS.md 已就绪。") - : makeCheck( - "agents-entry", - "项目入口", - STATUS.error, - "缺少根 AGENTS.md。", - "运行 flow2spec init codex 或 flow2spec init dsh,或重新初始化所需 Agent。", - ), - ); - - const manifestPath = path.join(cwd, ".Knowledge", "manifest-routing.json"); - checks.push( - fs.existsSync(manifestPath) - ? makeCheck( - "knowledge-entry", - "知识库入口", - STATUS.pass, - ".Knowledge/manifest-routing.json 已就绪。", - ) - : makeCheck( - "knowledge-entry", - "知识库入口", - STATUS.error, - "缺少 .Knowledge/manifest-routing.json。", - "在项目根运行 flow2spec init。", - ), - ); - - const requiredAgentFiles = { - codex: ["AGENTS.md", "hooks.json"], - dsh: ["AGENTS.md", "skills", "topics"], - claude: ["settings.json"], - cursor: ["hooks.json"], - }; - const initializedAgents = Object.entries(AGENTS).filter(([, agent]) => - fs.existsSync(path.join(cwd, agent.root)), - ); - if (initializedAgents.length === 0) { - checks.push( - makeCheck( - "agent-roots", - "Agent 配置", - STATUS.warning, - "未检测到 .codex、.claude、.cursor 或 .dsh 配置根。", - "运行 flow2spec init 初始化实际使用的 Agent。", - ), - ); - } else { - for (const [id, agent] of initializedAgents) { - const missing = (requiredAgentFiles[id] || []).filter( - (file) => !fs.existsSync(path.join(cwd, agent.root, file)), - ); - checks.push( - missing.length === 0 - ? makeCheck( - `agent-${id}`, - `${agent.label} 配置`, - STATUS.pass, - `${agent.root} 初始化文件完整。`, - ) - : makeCheck( - `agent-${id}`, - `${agent.label} 配置`, - STATUS.error, - `${agent.root} 缺少 ${missing.join("、")}。`, - `运行 flow2spec init ${id} 补齐配置。`, - { missing }, - ), - ); - } - } - - if (config) { - try { - const context = resolveDeveloperContext(config, { - cwd, - gitIdentity: options.gitIdentity, - skipGit: Boolean(options.gitIdentity), - }); - const warnings = [...context.warnings]; - if (context.legacy && context.enabled) { - warnings.push("未找到 developerId,将使用 legacy .task/ 根。"); - } - checks.push( - makeCheck( - "collaboration", - "协作上下文", - warnings.length > 0 ? STATUS.warning : STATUS.pass, - context.legacy - ? `使用 ${context.taskRoot}(${context.enabled ? "legacy" : "协作隔离已关闭"})。` - : `developerId=${context.developerId},TASK_ROOT=${context.taskRoot}。`, - warnings.length > 0 - ? "在 flow2spec.config.json 配置 collaboration.developerId。" - : null, - { ...context, warnings }, - ), - ); - } catch (error) { - checks.push( - makeCheck( - "collaboration", - "协作上下文", - STATUS.error, - error.message || String(error), - "修正 flow2spec.config.json 的 collaboration 配置。", - ), - ); - } - } - - const taskIgnored = isIgnoredByRootGitignore(cwd, ".task/"); - checks.push( - taskIgnored - ? makeCheck("task-ignore", "任务目录", STATUS.pass, ".task/ 已在根 .gitignore 中忽略。") - : makeCheck( - "task-ignore", - "任务目录", - STATUS.warning, - ".task/ 未在根 .gitignore 中忽略。", - "在根 .gitignore 中加入 .task/,或重新运行 flow2spec init。", - ), - ); - - checks.push(knowledgeCheck(cwd)); - - const summary = checks.reduce( - (result, check) => { - if (check.status === STATUS.pass) result.passed += 1; - if (check.status === STATUS.warning) result.warnings += 1; - if (check.status === STATUS.error) result.errors += 1; - return result; - }, - { passed: 0, warnings: 0, errors: 0 }, - ); - - return { - ok: summary.errors === 0, - package: { name: pkg.name, version: pkg.version }, - cwd: path.resolve(cwd), - summary, - checks, - }; -} - -function formatDoctorReport(report) { - const marker = { pass: "[PASS]", warning: "[WARN]", error: "[FAIL]" }; - const lines = [ - `Flow2Spec Doctor v${report.package.version}`, - `项目: ${report.cwd}`, - "", - ]; - for (const check of report.checks) { - lines.push(`${marker[check.status]} ${check.label}: ${check.message}`); - if (check.repair) lines.push(` 建议: ${check.repair}`); - } - lines.push( - "", - `结果: ${report.summary.passed} 通过,${report.summary.warnings} 警告,${report.summary.errors} 错误。`, - ); - return lines.join("\n"); -} - -module.exports = { - STATUS, - runDoctor, - formatDoctorReport, - satisfiesNodeEngine, - checkKnowledge, -}; +module.exports = require("@double-coding/flow2spec-core").legacy.doctor; diff --git a/lib/dshAgentsAdapter.js b/lib/dshAgentsAdapter.js index 09388fd..800996b 100644 --- a/lib/dshAgentsAdapter.js +++ b/lib/dshAgentsAdapter.js @@ -1,81 +1 @@ -const fs = require("fs"); -const path = require("path"); -const { buildCodexAgentsMd } = require("./codexAgentsAdapter"); - -function replaceSection(body, startHeading, endHeading, replacement) { - const start = body.indexOf(startHeading); - const end = body.indexOf(endHeading, start + startHeading.length); - if (start < 0 || end < 0) return body; - return `${body.slice(0, start)}${replacement.trim()}\n\n${body.slice(end)}`; -} - -function buildDshAgentsMd(templatesDir, projectConfig) { - let body = buildCodexAgentsMd(templatesDir, projectConfig) - .replace(/Codex/g, "DeepSeek Harness") - .replace(/codex/g, "dsh"); - - const isEnglish = path.basename(path.dirname(templatesDir)) === "templates" && - path.basename(templatesDir) === "en-US"; - if (isEnglish) { - body = body - .replace(" **`./.dsh/AGENTS.md`** is only a pointer.", "") - .replace("\n**`.dsh/AGENTS.md`** is only a pointer and cannot replace root `AGENTS.md`.\n", "\n"); - body = replaceSection( - body, - "## DeepSeek Harness Hooks", - "## Flow2Spec Skills", - `## DeepSeek Harness Integration - -DeepSeek Harness loads the repository-root \`AGENTS.md\` and discovers project skills from \`./.dsh/skills/\`. Flow2Spec mirrors its long-form rules to \`./.dsh/topics/\` for on-demand reading. Native Cordis plugin integration is outside this initialization adapter.`, - ); - return body; - } - - body = body - .replace("**`./.dsh/AGENTS.md`** 仅为指针。", "") - .replace("- **`.dsh/AGENTS.md`** 仅为目录指针,不能替代根 `AGENTS.md`。\n", ""); - return replaceSection( - body, - "## DeepSeek Harness Hooks", - "## Flow2Spec 技能", - `## DeepSeek Harness 适配 - -DeepSeek Harness 会加载仓库根 \`AGENTS.md\`,并从 \`./.dsh/skills/\` 发现项目技能。Flow2Spec 将规则长文镜像到 \`./.dsh/topics/\` 供按需读取。原生 Cordis 插件集成不属于本初始化适配范围。`, - ); -} - -function buildDshAgentsStubMd(templatesDir) { - const isEnglish = path.basename(templatesDir) === "en-US"; - if (isEnglish) { - return `# Flow2Spec (\`.dsh/\` Directory Notes) - -DeepSeek Harness loads the complete project instructions from repository-root [\`AGENTS.md\`](../AGENTS.md). - -- \`skills/\`: Flow2Spec \`f2s-*\` skills discovered by DeepSeek Harness -- \`topics/\`: long-form rule mirrors loaded on demand -`; - } - return `# Flow2Spec(\`.dsh/\` 目录说明) - -DeepSeek Harness 从仓库根 [\`AGENTS.md\`](../AGENTS.md) 加载完整项目说明。 - -- \`skills/\`:DeepSeek Harness 可发现的 Flow2Spec \`f2s-*\` 技能 -- \`topics/\`:按需读取的规则长文镜像 -`; -} - -function writeDshAgentsStub(cwd, templatesDir) { - const dshRoot = path.join(cwd, ".dsh"); - fs.mkdirSync(dshRoot, { recursive: true }); - fs.writeFileSync( - path.join(dshRoot, "AGENTS.md"), - buildDshAgentsStubMd(templatesDir), - "utf8", - ); -} - -module.exports = { - buildDshAgentsMd, - buildDshAgentsStubMd, - writeDshAgentsStub, -}; +module.exports = require("@double-coding/flow2spec-core").legacy.dshAgentsAdapter; diff --git a/lib/flow2specConfig.js b/lib/flow2specConfig.js index bbe8bf0..6de2f56 100644 --- a/lib/flow2specConfig.js +++ b/lib/flow2specConfig.js @@ -1,317 +1 @@ -const path = require("path"); -const fs = require("fs"); - -const CONFIG_FILENAME = "flow2spec.config.json"; -const DEFAULT_LOCALE = "zh-CN"; -const SUPPORTED_LOCALES = ["zh-CN", "en-US"]; - -const DEFAULTS = { - locale: DEFAULT_LOCALE, - subAgent: true, - // switchAgentVerification:false=落盘侧同会话内验;true+技能绑定=交叉验(子落盘主验/主落盘子验) - switchAgentVerification: true, - intentRecognition: true, - changeTracking: { - feat: true, - fix: false, - implement: true, - }, - updateCheck: { - enabled: true, - }, - // 多人协作:进度按 developerId 隔离到 .task//;缺省 enabled - // developerId 解析:config → git → legacy 单根 .task/(见 lib/developerId.js) - collaboration: { - enabled: true, - developerId: "", - }, -}; - -/** - * 所有已知配置字段描述,供 init 交互提示使用。 - * 新增字段在此追加,cli.js 会自动对缺失字段发起提问。 - * 支持点号分隔的嵌套键,如 "changeTracking.feat"(对应 { changeTracking: { feat: ... } })。 - */ -const CONFIG_FIELDS = [ - { - key: "locale", - type: "locale", - default: DEFAULT_LOCALE, - question: "选择 Flow2Spec 模板语言", - }, - { - key: "subAgent", - type: "boolean", - default: true, - question: "启用子 Agent 并行执行?(默认 Y,开启后小型任务仍可由主 agent 一气完成)", - }, - { - key: "switchAgentVerification", - type: "boolean", - default: true, - question: "启用交叉验证(子 agent 落盘 → 主 agent 验;需配合技能使用,默认 Y)", - }, - { - key: "intentRecognition", - type: "boolean", - default: true, - question: "启用意图识别自动分流(高置信操作意图自动进入对应 f2s-* 技能,默认 Y)?", - }, - { - key: "changeTracking.feat", - type: "boolean", - default: true, - question: "启用变更追踪 - f2s-kb-feat(新增能力时创建可续作的任务清单)?", - }, - { - key: "changeTracking.fix", - type: "boolean", - default: false, - question: "启用变更追踪 - f2s-kb-fix(修正能力时创建可续作的任务清单)?", - }, - { - key: "changeTracking.implement", - type: "boolean", - default: true, - question: "启用变更追踪 - f2s-implement-tech-design(实现技术方案时创建可续作的任务清单)?", - }, - { - key: "updateCheck.enabled", - type: "boolean", - default: true, - question: "启用每日版本更新提示(每天第一次 Agent 对话时检查是否有新版 flow2spec)?", - }, - { - key: "collaboration.enabled", - type: "boolean", - default: true, - question: - "启用多人协作进度隔离(.task//;关闭则始终用单人 .task/ 根路径)?", - }, -]; - -function normalizeBool(value, fallback) { - if (value === true || value === "true" || value === 1 || value === "1") - return true; - if (value === false || value === "false" || value === 0 || value === "0") - return false; - return fallback; -} - -function normalizeLocale(value, fallback = DEFAULT_LOCALE) { - const raw = String(value || "").trim(); - return SUPPORTED_LOCALES.includes(raw) ? raw : fallback; -} - -/** - * 读取点号分隔键对应的嵌套值,如 "changeTracking.feat" → raw.changeTracking?.feat - */ -function getNestedValue(obj, dottedKey) { - const parts = dottedKey.split("."); - let cur = obj; - for (const p of parts) { - if (!cur || typeof cur !== "object") return undefined; - cur = cur[p]; - } - return cur; -} - -/** - * 读取项目根 flow2spec.config.json,与 DEFAULTS 合并。 - * 文件不存在时返回默认副本(不自动创建文件)。 - * changeTracking 兼容旧版布尔值(true/false → 全部子项同值)。 - */ -function loadFlow2specConfig(cwd) { - const abs = path.join(cwd, CONFIG_FILENAME); - const out = { - ...DEFAULTS, - changeTracking: { ...DEFAULTS.changeTracking }, - updateCheck: { ...DEFAULTS.updateCheck }, - collaboration: { ...DEFAULTS.collaboration }, - }; - if (!fs.existsSync(abs)) { - return out; - } - let raw; - try { - raw = JSON.parse(fs.readFileSync(abs, "utf8")); - } catch (e) { - throw new Error( - `${CONFIG_FILENAME} JSON 解析失败:${e.message || String(e)}`, - ); - } - if (!raw || typeof raw !== "object" || Array.isArray(raw)) { - return out; - } - if (Object.prototype.hasOwnProperty.call(raw, "locale")) { - out.locale = normalizeLocale(raw.locale, DEFAULTS.locale); - } - if (Object.prototype.hasOwnProperty.call(raw, "subAgent")) { - out.subAgent = normalizeBool(raw.subAgent, DEFAULTS.subAgent); - } - if (Object.prototype.hasOwnProperty.call(raw, "switchAgentVerification")) { - out.switchAgentVerification = normalizeBool( - raw.switchAgentVerification, - DEFAULTS.switchAgentVerification, - ); - } else if (Object.prototype.hasOwnProperty.call(raw, "subAgentVerification")) { - // 旧键名,仍读取;新落盘请用 switchAgentVerification - out.switchAgentVerification = normalizeBool( - raw.subAgentVerification, - DEFAULTS.switchAgentVerification, - ); - } - if (Object.prototype.hasOwnProperty.call(raw, "intentRecognition")) { - out.intentRecognition = normalizeBool( - raw.intentRecognition, - DEFAULTS.intentRecognition, - ); - } - if (Object.prototype.hasOwnProperty.call(raw, "changeTracking")) { - const ct = raw.changeTracking; - if (typeof ct === "boolean") { - // 旧版布尔值:统一应用到全部子项 - out.changeTracking = { feat: ct, fix: ct, implement: ct }; - } else if (ct && typeof ct === "object" && !Array.isArray(ct)) { - out.changeTracking = { - feat: normalizeBool(ct.feat, DEFAULTS.changeTracking.feat), - fix: normalizeBool(ct.fix, DEFAULTS.changeTracking.fix), - implement: normalizeBool(ct.implement, DEFAULTS.changeTracking.implement), - }; - } - } - if (Object.prototype.hasOwnProperty.call(raw, "updateCheck")) { - const uc = raw.updateCheck; - if (uc && typeof uc === "object" && !Array.isArray(uc)) { - out.updateCheck = { - enabled: normalizeBool(uc.enabled, DEFAULTS.updateCheck.enabled), - }; - } - } - if (Object.prototype.hasOwnProperty.call(raw, "collaboration")) { - const collab = raw.collaboration; - if (collab && typeof collab === "object" && !Array.isArray(collab)) { - const idRaw = - collab.developerId == null ? "" : String(collab.developerId).trim(); - out.collaboration = { - enabled: normalizeBool(collab.enabled, DEFAULTS.collaboration.enabled), - developerId: idRaw, - }; - } - } - return out; -} - -/** - * 返回配置文件中尚未存在的字段列表(用于 init 时只提示新增字段)。 - * 文件不存在时返回全部字段。支持点号嵌套键。 - */ -function getMissingConfigFields(cwd) { - const abs = path.join(cwd, CONFIG_FILENAME); - if (!fs.existsSync(abs)) return CONFIG_FIELDS; - let raw; - try { - raw = JSON.parse(fs.readFileSync(abs, "utf8")); - } catch { - return []; - } - if (!raw || typeof raw !== "object" || Array.isArray(raw)) return CONFIG_FIELDS; - return CONFIG_FIELDS.filter((f) => { - const parts = f.key.split("."); - if (parts.length === 2) { - const parent = raw[parts[0]]; - // 旧版布尔值视为已配置,不再重复询问 - if (typeof parent === "boolean") return false; - return !parent || !Object.prototype.hasOwnProperty.call(parent, parts[1]); - } - return !Object.prototype.hasOwnProperty.call(raw, f.key); - }); -} - -/** - * 将点号嵌套键的 values 对象合并入 target,支持一层嵌套。 - * 例如 { "changeTracking.feat": true } → target.changeTracking.feat = true - */ -function mergeValues(target, values) { - const result = { ...target }; - for (const [key, val] of Object.entries(values)) { - const parts = key.split("."); - if (parts.length === 2) { - result[parts[0]] = { - ...(result[parts[0]] && typeof result[parts[0]] === "object" - ? result[parts[0]] - : {}), - [parts[1]]: val, - }; - } else { - result[key] = val; - } - } - return result; -} - -/** - * 若项目根不存在配置文件,则写入配置(优先用 values,其次包模板,再次 DEFAULTS)。 - * 已存在时:若 values 中有缺失字段,则补写这些字段;否则不覆盖。 - * @param {object} [options.values] 用户交互收集到的字段值,优先级高于模板文件 - */ -function ensureFlow2specProjectConfig(cwd, templatesDir, options = {}) { - const { overwrite = false, values } = options; - const dest = path.join(cwd, CONFIG_FILENAME); - const src = path.join(templatesDir, CONFIG_FILENAME); - - if (fs.existsSync(dest) && !overwrite) { - if (values && typeof values === "object" && Object.keys(values).length > 0) { - let existing; - try { - existing = JSON.parse(fs.readFileSync(dest, "utf8")); - } catch { - existing = {}; - } - const merged = mergeValues(existing, values); - if (JSON.stringify(merged) !== JSON.stringify(existing)) { - fs.writeFileSync(dest, `${JSON.stringify(merged, null, 2)}\n`, "utf8"); - return { created: false, updated: true, path: dest }; - } - } - return { created: false, path: dest }; - } - - let base; - if (fs.existsSync(src)) { - try { - base = JSON.parse(fs.readFileSync(src, "utf8")); - } catch { - base = { - ...DEFAULTS, - locale: DEFAULTS.locale, - changeTracking: { ...DEFAULTS.changeTracking }, - updateCheck: { ...DEFAULTS.updateCheck }, - collaboration: { ...DEFAULTS.collaboration }, - }; - } - } else { - base = { - ...DEFAULTS, - locale: DEFAULTS.locale, - changeTracking: { ...DEFAULTS.changeTracking }, - updateCheck: { ...DEFAULTS.updateCheck }, - collaboration: { ...DEFAULTS.collaboration }, - }; - } - const merged = values && typeof values === "object" ? mergeValues(base, values) : base; - fs.writeFileSync(dest, `${JSON.stringify(merged, null, 2)}\n`, "utf8"); - return { created: true, path: dest }; -} - -module.exports = { - CONFIG_FILENAME, - DEFAULT_LOCALE, - SUPPORTED_LOCALES, - DEFAULTS, - CONFIG_FIELDS, - normalizeLocale, - loadFlow2specConfig, - getMissingConfigFields, - ensureFlow2specProjectConfig, -}; +module.exports = require("@double-coding/flow2spec-core").legacy.config; diff --git a/lib/init.js b/lib/init.js index eda0a7a..d3ee21a 100644 --- a/lib/init.js +++ b/lib/init.js @@ -1,1303 +1 @@ -const path = require("path"); -const fs = require("fs"); -const { - AGENTS, - KNOWLEDGE_ROOT, - KNOWLEDGE_SUBDIRS, - AGENT_SUBDIRS, - normalizeAgentIds, -} = require("./agents"); -const { - adaptRuleMdcToClaudeMd, - shouldWriteClaudeStyleRules, -} = require("./claudeRulesAdapter"); -const { - buildCodexAgentsMd, - buildCodexAgentsStubMd, -} = require("./codexAgentsAdapter"); -const { - buildDshAgentsMd, - writeDshAgentsStub, -} = require("./dshAgentsAdapter"); -const { - loadFlow2specConfig, - ensureFlow2specProjectConfig, - DEFAULT_LOCALE, - normalizeLocale, -} = require("./flow2specConfig"); -const { writeClaudeAgentHooks } = require("./claudeSettingsAdapter"); - -const KNOWLEDGE_TOPIC_TYPES = ["feature", "module", "config", "policy"]; -const KNOWLEDGE_TOPIC_CONFIDENCE = ["manual", "inferred"]; - -function ensureDir(dir) { - if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); -} - -function ensureKnowledgeDirs(cwd) { - ensureDir(path.join(cwd, KNOWLEDGE_ROOT)); - for (const sub of KNOWLEDGE_SUBDIRS) { - ensureDir(path.join(cwd, KNOWLEDGE_ROOT, sub)); - } -} - -function removeKnowledgeUpdateCheckCache(cwd) { - const cachePath = path.join(cwd, KNOWLEDGE_ROOT, "update-check.json"); - if (fs.existsSync(cachePath)) { - fs.rmSync(cachePath, { force: true }); - } -} - -function ensureFlow2specGitignore(cwd) { - const gitignorePath = path.join(cwd, ".gitignore"); - const required = [".task/", ".Knowledge/update-check.json"]; - const existing = fs.existsSync(gitignorePath) - ? fs.readFileSync(gitignorePath, "utf8") - : ""; - const lines = existing.split(/\r?\n/).map((line) => line.trim()); - const missing = required.filter((item) => !lines.includes(item)); - if (missing.length === 0) { - return { path: gitignorePath, changed: false, added: [] }; - } - const additions = []; - if (!lines.includes("# Flow2Spec local state")) { - additions.push("# Flow2Spec local state"); - } - additions.push(...missing); - const prefix = existing && !existing.endsWith("\n") ? `${existing}\n` : existing; - const separator = prefix && !prefix.endsWith("\n\n") ? "\n" : ""; - fs.writeFileSync(gitignorePath, `${prefix}${separator}${additions.join("\n")}\n`, "utf8"); - return { path: gitignorePath, changed: true, added: missing }; -} - -function ensureAgentDirs(cwd, agentId) { - const root = AGENTS[agentId].root; - ensureDir(path.join(cwd, root)); - for (const sub of AGENT_SUBDIRS[agentId] || []) { - ensureDir(path.join(cwd, root, sub)); - } -} - -/** 递归复制目录或文件到目标,已存在则覆盖 */ -function copyRecursive(src, dest) { - const stat = fs.statSync(src); - if (stat.isDirectory()) { - if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true }); - for (const name of fs.readdirSync(src)) { - copyRecursive(path.join(src, name), path.join(dest, name)); - } - } else { - fs.copyFileSync(src, dest); - } -} - -/** 递归复制目录或文件,支持扩展名转换(.md <-> .mdc) */ -function copyRecursiveWithExtConversion(src, dest, shouldConvertToMdc) { - const stat = fs.statSync(src); - if (stat.isDirectory()) { - if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true }); - for (const name of fs.readdirSync(src)) { - let destName = name; - // 处理文件扩展名转换 - if (shouldConvertToMdc && name.endsWith(".md")) { - destName = name.replace(/\.md$/i, ".mdc"); - } - copyRecursiveWithExtConversion( - path.join(src, name), - path.join(dest, destName), - shouldConvertToMdc - ); - } - } else { - fs.copyFileSync(src, dest); - } -} - -function copyKnowledgeTemplates(cwd, templatesDir, options = {}) { - const { overwrite = false } = options; - const srcRoot = path.join(templatesDir, "knowledge"); - const destRoot = path.join(cwd, KNOWLEDGE_ROOT); - if (!fs.existsSync(srcRoot)) return; - const result = { written: 0, skipped: 0 }; - for (const name of fs.readdirSync(srcRoot)) { - if (name === "manifest-matchers.json") { - continue; - } - copyRecursivePreserve( - path.join(srcRoot, name), - path.join(destRoot, name), - overwrite, - result, - ); - } - return result; -} - -function copyRecursivePreserve(src, dest, overwrite, result) { - const stat = fs.statSync(src); - if (stat.isDirectory()) { - if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true }); - for (const name of fs.readdirSync(src)) { - copyRecursivePreserve( - path.join(src, name), - path.join(dest, name), - overwrite, - result, - ); - } - return; - } - if (!overwrite && fs.existsSync(dest)) { - result.skipped += 1; - return; - } - fs.copyFileSync(src, dest); - result.written += 1; -} - -function readJson(filePath) { - return JSON.parse(fs.readFileSync(filePath, "utf8")); -} - -function writeJson(filePath, data) { - fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8"); -} - -function findPackageJsonDir(startDir) { - let cur = startDir; - while (cur && cur !== path.dirname(cur)) { - if (fs.existsSync(path.join(cur, "package.json"))) return cur; - cur = path.dirname(cur); - } - return null; -} - -function readPackageName(templatesDir) { - try { - const packageDir = findPackageJsonDir(templatesDir); - return readJson(path.join(packageDir || path.join(templatesDir, ".."), "package.json")).name; - } catch (_) { - return "@double-coding/flow2spec"; - } -} - -function resolveTemplatesDir(templatesRoot, locale) { - const normalized = normalizeLocale(locale, DEFAULT_LOCALE); - const preferred = path.join(templatesRoot, normalized); - if (fs.existsSync(preferred)) { - return { templatesDir: preferred, locale: normalized }; - } - const fallback = path.join(templatesRoot, DEFAULT_LOCALE); - if (fs.existsSync(fallback)) { - return { templatesDir: fallback, locale: DEFAULT_LOCALE }; - } - return { templatesDir: templatesRoot, locale: DEFAULT_LOCALE }; -} - -function writeHookScriptWithPackageName(destDir, templatesDir, scriptName) { - const src = path.join(templatesDir, "hooks", scriptName); - if (!fs.existsSync(src)) return { written: false, reason: "missing-template" }; - ensureDir(destDir); - let body = fs.readFileSync(src, "utf8"); - body = body.replace(/__FLOW2SPEC_PACKAGE_NAME__/g, readPackageName(templatesDir)); - fs.writeFileSync(path.join(destDir, scriptName), body, "utf8"); - return { written: true }; -} - -function hasHookCommand(groups, fragment) { - if (!Array.isArray(groups)) return false; - return groups.some((group) => - Array.isArray(group?.hooks) && - group.hooks.some( - (hook) => - hook && - hook.type === "command" && - String(hook.command || "").includes(fragment), - ), - ); -} - -function mergeCodexUpdateCheckHook(existing) { - const next = - existing && typeof existing === "object" && !Array.isArray(existing) - ? JSON.parse(JSON.stringify(existing)) - : {}; - if (!next.hooks || typeof next.hooks !== "object" || Array.isArray(next.hooks)) { - next.hooks = {}; - } - if (!Array.isArray(next.hooks.SessionStart)) { - next.hooks.SessionStart = []; - } - if (hasHookCommand(next.hooks.SessionStart, "f2s-update-check")) { - return { config: next, changed: false }; - } - next.hooks.SessionStart.push({ - matcher: "startup|resume", - hooks: [ - { - type: "command", - command: "node .codex/hooks/f2s-update-check.js", - statusMessage: "Checking Flow2Spec knowledge version", - }, - ], - }); - return { config: next, changed: true }; -} - -function mergeCodexConfigSessionHook(existing) { - const next = - existing && typeof existing === "object" && !Array.isArray(existing) - ? JSON.parse(JSON.stringify(existing)) - : {}; - if (!next.hooks || typeof next.hooks !== "object" || Array.isArray(next.hooks)) { - next.hooks = {}; - } - if (!Array.isArray(next.hooks.SessionStart)) { - next.hooks.SessionStart = []; - } - if (hasHookCommand(next.hooks.SessionStart, "f2s-config-session")) { - return { config: next, changed: false }; - } - next.hooks.SessionStart.unshift({ - matcher: "startup|resume", - hooks: [ - { - type: "command", - command: "node .codex/hooks/f2s-config-session.js", - }, - ], - }); - return { config: next, changed: true }; -} - -function writeCodexUpdateCheckHook(cwd, templatesDir) { - const codexRoot = path.join(cwd, ".codex"); - const hooksDir = path.join(codexRoot, "hooks"); - const configSessionResult = writeHookScriptWithPackageName( - hooksDir, - templatesDir, - "f2s-config-session.js", - ); - const scriptResult = writeHookScriptWithPackageName( - hooksDir, - templatesDir, - "f2s-update-check.js", - ); - - const hooksJsonPath = path.join(codexRoot, "hooks.json"); - let existing = {}; - if (fs.existsSync(hooksJsonPath)) { - try { - existing = readJson(hooksJsonPath); - } catch (_) { - existing = {}; - } - } - const mergedConfigSession = mergeCodexConfigSessionHook(existing); - const mergedUpdateCheck = mergeCodexUpdateCheckHook(mergedConfigSession.config); - const config = mergedUpdateCheck.config; - const changed = mergedConfigSession.changed || mergedUpdateCheck.changed; - if (changed || !fs.existsSync(hooksJsonPath)) { - writeJson(hooksJsonPath, config); - } - return { configSessionResult, scriptResult, hooksJsonChanged: changed }; -} - -function mergeCursorUpdateCheckHook(existing) { - const next = - existing && typeof existing === "object" && !Array.isArray(existing) - ? JSON.parse(JSON.stringify(existing)) - : {}; - next.version = Number.isFinite(Number(next.version)) ? Number(next.version) : 1; - if (!next.hooks || typeof next.hooks !== "object" || Array.isArray(next.hooks)) { - next.hooks = {}; - } - if (!Array.isArray(next.hooks.sessionStart)) { - next.hooks.sessionStart = []; - } - const exists = next.hooks.sessionStart.some((hook) => - hook && String(hook.command || "").includes("f2s-update-check"), - ); - if (exists) return { config: next, changed: false }; - next.hooks.sessionStart.push({ - command: "node .cursor/hooks/f2s-update-check.js", - timeout: 10, - }); - return { config: next, changed: true }; -} - -function writeCursorUpdateCheckHook(cwd, templatesDir) { - const cursorRoot = path.join(cwd, ".cursor"); - const hooksDir = path.join(cursorRoot, "hooks"); - const scriptResult = writeHookScriptWithPackageName( - hooksDir, - templatesDir, - "f2s-update-check.js", - ); - - const hooksJsonPath = path.join(cursorRoot, "hooks.json"); - let existing = {}; - if (fs.existsSync(hooksJsonPath)) { - try { - existing = readJson(hooksJsonPath); - } catch (_) { - existing = {}; - } - } - const { config, changed } = mergeCursorUpdateCheckHook(existing); - if (changed || !fs.existsSync(hooksJsonPath)) { - writeJson(hooksJsonPath, config); - } - return { scriptResult, hooksJsonChanged: changed }; -} - -function buildDefaultMatcherPath(matcherId) { - return `${KNOWLEDGE_ROOT}/matchers/${matcherId}.json`; -} - -function normalizeMatcherShardData(raw, matcherId) { - const safeRaw = - raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {}; - return { - ...safeRaw, - id: matcherId, - includeAny: dedupeStringArray(safeRaw.includeAny || []), - }; -} - -function dedupeStringArray(values) { - const out = []; - const seen = new Set(); - for (const item of values || []) { - if (typeof item !== "string") continue; - if (seen.has(item)) continue; - seen.add(item); - out.push(item); - } - return out; -} - -function unionByKey(templateList, existingList, key, mergeItem) { - const existingMap = new Map(); - for (const item of existingList || []) { - if (!item || typeof item !== "object") continue; - if (!item[key] || typeof item[key] !== "string") continue; - existingMap.set(item[key], item); - } - - const out = []; - const orderedKeys = []; - - for (const item of templateList || []) { - if (!item || typeof item !== "object") continue; - const id = item[key]; - if (!id || typeof id !== "string") continue; - orderedKeys.push(id); - const existing = existingMap.get(id); - out.push(mergeItem(item, existing)); - } - - for (const item of existingList || []) { - if (!item || typeof item !== "object") continue; - const id = item[key]; - if (!id || typeof id !== "string") continue; - if (orderedKeys.includes(id)) continue; - out.push(item); - } - - return out; -} - -function mergeTopicDependencies(templateDeps, existingDeps) { - const out = {}; - const keys = new Set([ - ...Object.keys(templateDeps || {}), - ...Object.keys(existingDeps || {}), - ]); - for (const key of keys) { - out[key] = dedupeStringArray([ - ...(templateDeps?.[key] || []), - ...(existingDeps?.[key] || []), - ]); - } - return out; -} - -function normalizeTopicMetadataEntry(entry) { - if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null; - if (!KNOWLEDGE_TOPIC_TYPES.includes(entry.primary)) return null; - const confidence = - typeof entry.confidence === "string" && - KNOWLEDGE_TOPIC_CONFIDENCE.includes(entry.confidence) - ? entry.confidence - : null; - if (!confidence) return null; - const result = { primary: entry.primary, confidence }; - if (Array.isArray(entry.tags) && entry.tags.length > 0) { - const validTags = dedupeStringArray(entry.tags).filter( - (t) => - KNOWLEDGE_TOPIC_TYPES.includes(t) && - t !== entry.primary, - ); - if (validTags.length > 0) result.tags = validTags; - } - return result; -} - -function mergeTopicMetadata(templateMetadata, existingMetadata, topicPaths) { - const out = {}; - const topicIds = new Set(Object.keys(topicPaths || {})); - // existingMetadata 先写,templateMetadata 后写覆盖——模板优先 - for (const metadata of [existingMetadata, templateMetadata]) { - if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) { - continue; - } - for (const [topicId, entry] of Object.entries(metadata)) { - if (!topicIds.has(topicId)) continue; - const normalized = normalizeTopicMetadataEntry(entry); - if (!normalized) continue; - out[topicId] = normalized; - } - } - return out; -} - -function buildMergedRouting(templateRouting, existingRouting, pkgVersion, isFirstInit = false) { - const mergedTaskRules = unionByKey( - templateRouting.taskToTopicRules, - existingRouting.taskToTopicRules, - "task", - (templateRule, existingRule) => { - if (!existingRule) return templateRule; - const mergedMatcherId = existingRule.matcherId || templateRule.matcherId; - return { - ...templateRule, - ...existingRule, - matcherId: mergedMatcherId, - matcherPath: - existingRule.matcherPath || - templateRule.matcherPath || - (mergedMatcherId ? buildDefaultMatcherPath(mergedMatcherId) : null), - topics: dedupeStringArray([ - ...(templateRule.topics || []), - ...(existingRule.topics || []), - ]), - }; - }, - ); - - const knownMerged = { - version: pkgVersion || templateRouting.version || existingRouting.version, - knowledgeRoot: - existingRouting.knowledgeRoot || templateRouting.knowledgeRoot, - generatedFrom: - existingRouting.generatedFrom || templateRouting.generatedFrom, - matcherKey: - existingRouting.matcherKey || templateRouting.matcherKey || "matcherId", - sourceOfTruth: - existingRouting.sourceOfTruth || - templateRouting.sourceOfTruth || - `${KNOWLEDGE_ROOT}/manifest-routing.json`, - fallbackTopic: - existingRouting.fallbackTopic || templateRouting.fallbackTopic, - topicDependencies: mergeTopicDependencies( - templateRouting.topicDependencies, - existingRouting.topicDependencies, - ), - topicPaths: { - ...(templateRouting.topicPaths || {}), - ...(existingRouting.topicPaths || {}), - }, - taskToTopicRules: mergedTaskRules, - }; - // projectRev:本项目已基线对齐到的包模板修订号(由 f2s-kb-upgrade 完整流程跑完 3a/3b 后写入)。 - // init 行为: - // - 首次初始化(项目侧 manifest-routing.json 此前不存在):按模板写入,等同首次落地即视为已对齐。 - // - 已存在 manifest-routing.json:不写、不覆盖项目侧值;由 f2s-kb-upgrade 在完整流程末尾改写。 - if (isFirstInit) { - if (Object.prototype.hasOwnProperty.call(templateRouting, "projectRev")) { - knownMerged.projectRev = templateRouting.projectRev; - } - } else if ( - Object.prototype.hasOwnProperty.call(existingRouting, "projectRev") - ) { - knownMerged.projectRev = existingRouting.projectRev; - } - // pkgRev:本次 init 用的包模板修订号(= 包侧 projectRev 快照),供 f2s-kb-upgrade 步骤 2c 取用。 - // 每次 init 都按当前包模板覆盖(不沿用项目原值)。包模板未声明该字段 → 删除项目侧旧值,让 SKILL 走 null 兜底。 - if ( - Object.prototype.hasOwnProperty.call(templateRouting, "projectRev") && - typeof templateRouting.projectRev === "number" && - Number.isFinite(templateRouting.projectRev) - ) { - knownMerged.pkgRev = templateRouting.projectRev; - } - const mergedTopicMetadata = mergeTopicMetadata( - templateRouting.topicMetadata, - existingRouting.topicMetadata, - knownMerged.topicPaths, - ); - if (Object.keys(mergedTopicMetadata).length > 0) { - knownMerged.topicMetadata = mergedTopicMetadata; - } - - const knownKeys = new Set(Object.keys(knownMerged)); - const extras = {}; - for (const [key, value] of Object.entries(existingRouting || {})) { - if (knownKeys.has(key)) continue; - extras[key] = value; - } - - const merged = { - ...knownMerged, - ...extras, - }; - delete merged.matchersFile; - return merged; -} - -function buildMergedMatchers(templateMatchers, existingMatchers) { - const templateMap = templateMatchers.matchers || {}; - const existingMap = existingMatchers.matchers || {}; - const allMatcherIds = new Set([ - ...Object.keys(templateMap), - ...Object.keys(existingMap), - ]); - const mergedMatchers = {}; - for (const matcherId of allMatcherIds) { - const templateItem = templateMap[matcherId] || {}; - const existingItem = existingMap[matcherId] || {}; - mergedMatchers[matcherId] = { - ...templateItem, - ...existingItem, - includeAny: dedupeStringArray([ - ...(templateItem.includeAny || []), - ...(existingItem.includeAny || []), - ]), - }; - } - - const knownMerged = { - version: templateMatchers.version || existingMatchers.version, - generatedFrom: - existingMatchers.generatedFrom || templateMatchers.generatedFrom, - matcherKey: - existingMatchers.matcherKey || templateMatchers.matcherKey || "matcherId", - sourceOfTruth: - existingMatchers.sourceOfTruth || - templateMatchers.sourceOfTruth || - `${KNOWLEDGE_ROOT}/manifest-routing.json`, - matchers: mergedMatchers, - }; - - const knownKeys = new Set(Object.keys(knownMerged)); - const extras = {}; - for (const [key, value] of Object.entries(existingMatchers || {})) { - if (knownKeys.has(key)) continue; - extras[key] = value; - } - - return { - ...knownMerged, - ...extras, - }; -} - -function ensureRoutingMatcherPaths(routing) { - const rules = Array.isArray(routing.taskToTopicRules) - ? routing.taskToTopicRules - : []; - let changed = false; - const nextRules = rules.map((rule) => { - if (!rule || typeof rule !== "object") return rule; - if (!rule.matcherId || typeof rule.matcherId !== "string") return rule; - if (rule.matcherPath && typeof rule.matcherPath === "string") return rule; - changed = true; - return { - ...rule, - matcherPath: buildDefaultMatcherPath(rule.matcherId), - }; - }); - if (!changed) return { routing, changed }; - return { - routing: { - ...routing, - taskToTopicRules: nextRules, - }, - changed, - }; -} - -function buildMatcherIdToPathMap(routing) { - const out = new Map(); - const rules = Array.isArray(routing.taskToTopicRules) - ? routing.taskToTopicRules - : []; - for (const rule of rules) { - if (!rule || typeof rule !== "object") continue; - if (!rule.matcherId || typeof rule.matcherId !== "string") continue; - const matcherPath = - rule.matcherPath && typeof rule.matcherPath === "string" - ? rule.matcherPath - : buildDefaultMatcherPath(rule.matcherId); - if (!out.has(rule.matcherId)) { - out.set(rule.matcherId, matcherPath); - } - } - return out; -} - -function ensureMatcherShards(cwd, routing, mergedMatchers) { - const matcherIdToPath = buildMatcherIdToPathMap(routing); - const matcherMap = - mergedMatchers?.matchers && typeof mergedMatchers.matchers === "object" - ? mergedMatchers.matchers - : {}; - - for (const matcherId of Object.keys(matcherMap)) { - if (!matcherIdToPath.has(matcherId)) { - matcherIdToPath.set(matcherId, buildDefaultMatcherPath(matcherId)); - } - } - - let changed = false; - let writtenCount = 0; - for (const [matcherId, matcherPath] of matcherIdToPath.entries()) { - const matcherAbs = resolveFromCwd(cwd, matcherPath); - ensureDir(path.dirname(matcherAbs)); - - const compatMatcher = matcherMap[matcherId]; - const existingShard = fs.existsSync(matcherAbs) ? readJson(matcherAbs) : {}; - const nextShard = normalizeMatcherShardData( - { - ...(compatMatcher && typeof compatMatcher === "object" - ? compatMatcher - : {}), - ...(existingShard && typeof existingShard === "object" - ? existingShard - : {}), - }, - matcherId, - ); - - const prevRaw = fs.existsSync(matcherAbs) - ? JSON.stringify(existingShard) - : null; - const nextRaw = JSON.stringify(nextShard); - if (prevRaw === nextRaw) continue; - - writeJson(matcherAbs, nextShard); - writtenCount += 1; - changed = true; - } - - return { changed, writtenCount }; -} - -/** - * 把「本次 init 用的包模板 projectRev」写入项目侧 `.Knowledge/manifest-routing.json` 的 `pkgRev` 顶层字段, - * 供 `f2s-kb-upgrade` 步骤 2c 取用。在 reset 与 incremental 两条路径之后无条件调用一次。 - * - * @param {string} cwd 项目根 - * @param {string} templatesDir 当前 locale 模板根 - */ -function finalizePkgRev(cwd, templatesDir) { - const routingPath = path.join(cwd, KNOWLEDGE_ROOT, "manifest-routing.json"); - const templateRoutingPath = path.join( - templatesDir, - "knowledge", - "manifest-routing.json", - ); - if (!fs.existsSync(routingPath) || !fs.existsSync(templateRoutingPath)) { - return { written: false, pkgRev: null }; - } - let templateRev = null; - try { - const tpl = readJson(templateRoutingPath); - if ( - Object.prototype.hasOwnProperty.call(tpl, "projectRev") && - typeof tpl.projectRev === "number" && - Number.isFinite(tpl.projectRev) - ) { - templateRev = tpl.projectRev; - } - } catch { - // 模板读不到则不写;保留项目侧旧值(如已有) - return { written: false, pkgRev: null }; - } - let routing; - try { - routing = readJson(routingPath); - } catch { - return { written: false, pkgRev: null }; - } - // 顺便用本包版本号覆盖 manifest.version——reset 路径直接 cp 模板会留下模板里的占位版本号 - let changed = false; - try { - const pkgJsonPath = path.join(__dirname, "..", "package.json"); - if (fs.existsSync(pkgJsonPath)) { - const pkgVersion = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8")).version; - if (typeof pkgVersion === "string" && routing.version !== pkgVersion) { - routing.version = pkgVersion; - changed = true; - } - } - } catch { - // 读不到 package.json 不影响主流程 - } - if (templateRev !== null) { - if (routing.pkgRev !== templateRev) { - routing.pkgRev = templateRev; - changed = true; - } - } else if ( - Object.prototype.hasOwnProperty.call(routing, "pkgRev") - ) { - // 包模板未声明 projectRev 时,清掉项目侧的陈旧 pkgRev,让 SKILL 走 null 兜底 - delete routing.pkgRev; - changed = true; - } - if (!changed) { - return { written: false, pkgRev: templateRev }; - } - writeJson(routingPath, routing); - return { written: true, pkgRev: templateRev }; -} - -function upgradeKnowledgeRoutingAndMatchers(cwd, templatesDir, options = {}) { - const { overwrite = false } = options; - if (overwrite) { - return { - upgraded: false, - reason: "overwrite", - }; - } - - const templateRoutingPath = path.join( - templatesDir, - "knowledge", - "manifest-routing.json", - ); - const templateMatchersPath = path.join( - templatesDir, - "knowledge", - "manifest-matchers.json", - ); - if ( - !fs.existsSync(templateRoutingPath) || - !fs.existsSync(templateMatchersPath) - ) { - return { - upgraded: false, - reason: "missing-routing-templates", - }; - } - - const routingPath = path.join(cwd, KNOWLEDGE_ROOT, "manifest-routing.json"); - const matchersPath = path.join(cwd, KNOWLEDGE_ROOT, "manifest-matchers.json"); - - const templateRouting = readJson(templateRoutingPath); - const templateMatchers = readJson(templateMatchersPath); - const hadRouting = fs.existsSync(routingPath); - const hadMatchers = fs.existsSync(matchersPath); - const existingRouting = hadRouting ? readJson(routingPath) : {}; - const existingMatchers = hadMatchers ? readJson(matchersPath) : {}; - - // 读包版本号,用于写入 manifest-routing.json 的 version 字段 - let pkgVersion; - try { - const packageDir = findPackageJsonDir(templatesDir); - pkgVersion = readJson(path.join(packageDir || path.join(templatesDir, ".."), "package.json")).version; - } catch (_) {} - - const mergedRouting = buildMergedRouting(templateRouting, existingRouting, pkgVersion, !hadRouting); - const mergedMatchers = buildMergedMatchers( - templateMatchers, - existingMatchers, - ); - const { - routing: mergedRoutingWithMatcherPath, - changed: matcherPathBackfilled, - } = ensureRoutingMatcherPaths(mergedRouting); - const matcherShardUpgrade = ensureMatcherShards( - cwd, - mergedRoutingWithMatcherPath, - mergedMatchers, - ); - - const oldRoutingRaw = JSON.stringify(existingRouting); - const newRoutingRaw = JSON.stringify(mergedRoutingWithMatcherPath); - const oldMatchersRaw = JSON.stringify(existingMatchers); - const newMatchersRaw = JSON.stringify(mergedMatchers); - - if (!hadRouting || oldRoutingRaw !== newRoutingRaw) { - writeJson(routingPath, mergedRoutingWithMatcherPath); - } - - const routingChanged = !hadRouting || oldRoutingRaw !== newRoutingRaw; - const legacyAggregateDiffers = - hadMatchers && oldMatchersRaw !== newMatchersRaw; - let legacyMatchersFileRemoved = false; - if (fs.existsSync(matchersPath)) { - try { - fs.unlinkSync(matchersPath); - legacyMatchersFileRemoved = true; - } catch (e) { - /* 保留文件时由下次 init 重试 */ - } - } - const upgraded = - routingChanged || - legacyAggregateDiffers || - matcherShardUpgrade.changed || - legacyMatchersFileRemoved; - - return { - upgraded, - reason: upgraded ? "merged" : "up-to-date", - routingChanged, - legacyAggregateDiffers, - legacyMatchersFileRemoved, - matcherPathBackfilled, - matcherShardChanged: matcherShardUpgrade.changed, - matcherShardWritten: matcherShardUpgrade.writtenCount, - }; -} - -function resolveFromCwd(cwd, maybeRelativePath) { - return path.isAbsolute(maybeRelativePath) - ? maybeRelativePath - : path.join(cwd, maybeRelativePath); -} - -function validateKnowledgeRouting(cwd) { - const routingPath = path.join(cwd, KNOWLEDGE_ROOT, "manifest-routing.json"); - const matchersPath = path.join(cwd, KNOWLEDGE_ROOT, "manifest-matchers.json"); - if (!fs.existsSync(routingPath)) { - throw new Error( - `缺少知识库路由清单:${path.join(KNOWLEDGE_ROOT, "manifest-routing.json")}`, - ); - } - let routing; - let matcherData = null; - try { - routing = JSON.parse(fs.readFileSync(routingPath, "utf8")); - } catch (e) { - throw new Error(`路由清单 JSON 解析失败:${routingPath}`); - } - if (fs.existsSync(matchersPath)) { - try { - matcherData = JSON.parse(fs.readFileSync(matchersPath, "utf8")); - } catch (e) { - throw new Error(`匹配清单 JSON 解析失败:${matchersPath}`); - } - } - - if (!routing.topicPaths || typeof routing.topicPaths !== "object") { - throw new Error("路由清单缺少 topicPaths,无法执行主题路由。"); - } - - const topicIds = new Set(Object.keys(routing.topicPaths)); - if (topicIds.size === 0) { - throw new Error("路由清单 topicPaths 为空,无法执行主题路由。"); - } - - for (const [topicId, topicPath] of Object.entries(routing.topicPaths)) { - if (!topicId || typeof topicId !== "string") { - throw new Error("topicPaths 中存在非法 topicId。"); - } - if (!topicPath || typeof topicPath !== "string") { - throw new Error(`topicPaths.${topicId} 必须是字符串路径。`); - } - const topicAbs = resolveFromCwd(cwd, topicPath); - if (!fs.existsSync(topicAbs)) { - throw new Error(`路由清单引用的 topic 不存在:${topicPath}`); - } - } - - if (routing.fallbackTopic && !topicIds.has(routing.fallbackTopic)) { - throw new Error( - `fallbackTopic 不存在于 topicPaths:${routing.fallbackTopic}`, - ); - } - - if ( - routing.topicDependencies && - typeof routing.topicDependencies === "object" - ) { - for (const [topicId, deps] of Object.entries(routing.topicDependencies)) { - if (!topicIds.has(topicId)) { - throw new Error(`topicDependencies 引用了不存在的 topic:${topicId}`); - } - if (!Array.isArray(deps)) { - throw new Error(`topicDependencies.${topicId} 必须是数组。`); - } - for (const depId of deps) { - if (!topicIds.has(depId)) { - throw new Error( - `topicDependencies.${topicId} 引用了不存在的依赖:${depId}`, - ); - } - } - } - } - - if (routing.topicMetadata !== undefined) { - if ( - !routing.topicMetadata || - typeof routing.topicMetadata !== "object" || - Array.isArray(routing.topicMetadata) - ) { - throw new Error("topicMetadata 必须是对象。"); - } - for (const [topicId, metadata] of Object.entries(routing.topicMetadata)) { - if (!topicIds.has(topicId)) { - throw new Error(`topicMetadata 引用了不存在的 topic:${topicId}`); - } - if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) { - throw new Error(`topicMetadata.${topicId} 必须是对象。`); - } - for (const key of Object.keys(metadata)) { - if (!["primary", "tags", "confidence"].includes(key)) { - throw new Error(`topicMetadata.${topicId} 包含未知字段:${key}`); - } - } - if (!KNOWLEDGE_TOPIC_TYPES.includes(metadata.primary)) { - throw new Error( - `topicMetadata.${topicId}.primary 非法:${metadata.primary}`, - ); - } - if (!KNOWLEDGE_TOPIC_CONFIDENCE.includes(metadata.confidence)) { - throw new Error( - `topicMetadata.${topicId}.confidence 非法:${metadata.confidence}`, - ); - } - if (metadata.tags !== undefined) { - if (!Array.isArray(metadata.tags)) { - throw new Error(`topicMetadata.${topicId}.tags 必须是数组。`); - } - const seenTags = new Set(); - for (const tag of metadata.tags) { - if (!KNOWLEDGE_TOPIC_TYPES.includes(tag)) { - throw new Error(`topicMetadata.${topicId}.tags 包含非法值:${tag}`); - } - if (tag === metadata.primary) { - throw new Error( - `topicMetadata.${topicId}.tags 不应与 primary 重复:${tag}`, - ); - } - if (seenTags.has(tag)) { - throw new Error(`topicMetadata.${topicId}.tags 包含重复值:${tag}`); - } - seenTags.add(tag); - } - } - } - } - - const matcherMap = - matcherData?.matchers && typeof matcherData.matchers === "object" - ? matcherData.matchers - : null; - if (matcherData && !matcherMap) { - throw new Error("匹配清单缺少 matchers 对象。"); - } - - if (Array.isArray(routing.taskToTopicRules)) { - for (const rule of routing.taskToTopicRules) { - if (!rule || typeof rule !== "object") { - throw new Error("taskToTopicRules 存在非法项(非对象)。"); - } - if (!rule.task || typeof rule.task !== "string") { - throw new Error("taskToTopicRules 每项必须包含字符串类型的 task。"); - } - if (!Array.isArray(rule.topics) || rule.topics.length === 0) { - throw new Error(`taskToTopicRules(${rule.task}) 必须包含非空 topics。`); - } - if (!rule.matcherId || typeof rule.matcherId !== "string") { - throw new Error(`taskToTopicRules(${rule.task}) 必须包含 matcherId。`); - } - if (!rule.matcherPath || typeof rule.matcherPath !== "string") { - throw new Error(`taskToTopicRules(${rule.task}) 必须包含 matcherPath。`); - } - const matcherAbs = resolveFromCwd(cwd, rule.matcherPath); - if (!fs.existsSync(matcherAbs)) { - throw new Error( - `taskToTopicRules(${rule.task}) 引用了不存在的 matcherPath:${rule.matcherPath}`, - ); - } - let matcherShard; - try { - matcherShard = JSON.parse(fs.readFileSync(matcherAbs, "utf8")); - } catch (e) { - throw new Error(`matcherPath JSON 解析失败:${rule.matcherPath}`); - } - if (!matcherShard || typeof matcherShard !== "object") { - throw new Error(`matcherPath 内容非法(非对象):${rule.matcherPath}`); - } - if (matcherShard.id !== rule.matcherId) { - throw new Error( - `matcherPath(${rule.matcherPath}) 的 id 与 matcherId 不一致:${matcherShard.id} vs ${rule.matcherId}`, - ); - } - if (!Array.isArray(matcherShard.includeAny)) { - throw new Error( - `matcherPath(${rule.matcherPath}) 的 includeAny 必须为数组。`, - ); - } - for (const topicId of rule.topics) { - if (!topicIds.has(topicId)) { - throw new Error( - `taskToTopicRules(${rule.task}) 引用了不存在的 topic:${topicId}`, - ); - } - } - } - } -} - -/** - * 将当前 locale 包模板 knowledge/index.md 原样复制到目标 cwd 下 .Knowledge/template/index.template.md, - * 供 f2s-kb-upgrade 技能步骤 3b 与宿主仓 .Knowledge/index.md 对照;init 不修改 index.md 正文。 - * 注意:模板正文声明「.Knowledge」指宿主仓;与 flow2spec 开发仓根 .Knowledge(产品自用知识库)职责不同。 - */ -function copyKnowledgeIndexTemplateSnapshot(cwd, templatesDir) { - const src = path.join(templatesDir, "knowledge", "index.md"); - const destDir = path.join(cwd, KNOWLEDGE_ROOT, "template"); - const dest = path.join(destDir, "index.template.md"); - if (!fs.existsSync(src)) { - return { written: false, reason: "missing-template-index" }; - } - ensureDir(destDir); - fs.copyFileSync(src, dest); - return { written: true }; -} - -function copyRulesTemplates(cwd, agentRoot, templatesDir) { - const rulesSrc = path.join(templatesDir, "rules"); - const rulesDest = path.join(cwd, agentRoot, "rules"); - if (!fs.existsSync(rulesSrc)) return; - ensureDir(rulesDest); - - // 判断是否应该转换为 .md:Claude 和 Codex 使用 .md,Cursor 使用 .mdc - const isCursorAgent = agentRoot === ".cursor"; - const shouldConvertToMd = !isCursorAgent; - - const claudeStyle = shouldWriteClaudeStyleRules(agentRoot); - if (claudeStyle) { - for (const name of fs.readdirSync(rulesDest)) { - if (name.endsWith(".mdc")) { - fs.unlinkSync(path.join(rulesDest, name)); - } - } - } - - for (const name of fs.readdirSync(rulesSrc)) { - const srcPath = path.join(rulesSrc, name); - const st = fs.statSync(srcPath); - if (st.isDirectory()) { - copyRecursive(srcPath, path.join(rulesDest, name)); - continue; - } - // 对于 .md 文件(模板源) - if (name.endsWith(".md")) { - const raw = fs.readFileSync(srcPath, "utf8"); - if (isCursorAgent) { - // Cursor 需要转换为 .mdc - const destName = name.replace(/\.md$/i, ".mdc"); - fs.writeFileSync(path.join(rulesDest, destName), raw, "utf8"); - } else { - // Claude 和 Codex 保持 .md - const body = claudeStyle ? adaptRuleMdcToClaudeMd(raw) : raw; - fs.writeFileSync(path.join(rulesDest, name), body, "utf8"); - } - continue; - } - if (!name.endsWith(".mdc")) { - fs.copyFileSync(srcPath, path.join(rulesDest, name)); - continue; - } - const raw = fs.readFileSync(srcPath, "utf8"); - const body = claudeStyle ? adaptRuleMdcToClaudeMd(raw) : raw; - const destName = claudeStyle ? name.replace(/\.mdc$/i, ".md") : name; - fs.writeFileSync(path.join(rulesDest, destName), body, "utf8"); - } -} - -function copySkills(cwd, agentRoot, templatesDir) { - const destRoot = path.join(cwd, agentRoot); - const skillsSrc = path.join(templatesDir, "skills"); - - if (fs.existsSync(skillsSrc)) { - const skillsDest = path.join(destRoot, "skills"); - ensureDir(skillsDest); - const templateNames = new Set(fs.readdirSync(skillsSrc)); - // skills 目录:所有平台都保持 .md 格式,不转换 - for (const name of templateNames) { - copyRecursive(path.join(skillsSrc, name), path.join(skillsDest, name)); - } - // 删除配置根中以 f2s- 开头、但已不存在于当前 locale templates/skills/ 的旧 skill 目录 - // 只清理 Flow2Spec 管理的 skill,不触碰用户自定义 skill - // LEGACY_SKILLS:非 f2s- 开头的历史旧名,也需一并清理 - const LEGACY_SKILLS = new Set(["stock-docs-vs-req-docs"]); - if (fs.existsSync(skillsDest)) { - for (const name of fs.readdirSync(skillsDest)) { - if ((name.startsWith("f2s-") || LEGACY_SKILLS.has(name)) && !templateNames.has(name)) { - fs.rmSync(path.join(skillsDest, name), { recursive: true, force: true }); - } - } - } - } -} - -function removeLegacyAgentTemplateDir(cwd, agentRoot) { - const templateDir = path.join(cwd, agentRoot, "template"); - if (fs.existsSync(templateDir)) { - fs.rmSync(templateDir, { recursive: true, force: true }); - } -} - -function stripMdcFrontmatter(src) { - return src.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, ""); -} - -function writeTopicMirrors(cwd, templatesDir, agentRoot) { - const rulesDir = path.join(templatesDir, "rules"); - const outDir = path.join(cwd, agentRoot, "topics"); - ensureDir(outDir); - if (!fs.existsSync(rulesDir)) return; - // Mirror rule templates for clients that load long-form guidance on demand. - const names = fs - .readdirSync(rulesDir) - .filter((n) => { - const lower = n.toLowerCase(); - return lower.endsWith(".md") || lower.endsWith(".mdc"); - }) - .sort(); - for (const name of names) { - const srcPath = path.join(rulesDir, name); - if (!fs.statSync(srcPath).isFile()) continue; - const raw = fs.readFileSync(srcPath, "utf8"); - const body = stripMdcFrontmatter(raw).trimStart(); - const outName = name.replace(/\.(md|mdc)$/i, ".md"); - fs.writeFileSync(path.join(outDir, outName), body, "utf8"); - } -} - -function writeCodexTopicMirrors(cwd, templatesDir) { - writeTopicMirrors(cwd, templatesDir, ".codex"); -} - -/** - * 完整条令写仓库根;.codex/AGENTS.md 仅为指针,避免双份全文重复与 cwd 在 .codex 时双倍拼接。 - */ -function writeCodexEntry(cwd, templatesDir, projectConfig) { - const full = buildCodexAgentsMd(templatesDir, projectConfig); - const stub = buildCodexAgentsStubMd(templatesDir); - fs.writeFileSync(path.join(cwd, "AGENTS.md"), full, "utf8"); - fs.writeFileSync(path.join(cwd, ".codex", "AGENTS.md"), stub, "utf8"); - writeCodexTopicMirrors(cwd, templatesDir); -} - -/** Write the root instructions needed by DeepSeek Harness without overwriting an existing entry. */ -function writeDshEntry(cwd, templatesDir, projectConfig) { - const agentsPath = path.join(cwd, "AGENTS.md"); - if (!fs.existsSync(agentsPath)) { - fs.writeFileSync( - agentsPath, - buildDshAgentsMd(templatesDir, projectConfig), - "utf8", - ); - } - writeDshAgentsStub(cwd, templatesDir); - writeTopicMirrors(cwd, templatesDir, ".dsh"); -} - -function writeAgentArtifacts(cwd, agentId, templatesDir, projectConfig) { - const root = AGENTS[agentId].root; - copySkills(cwd, root, templatesDir); - removeLegacyAgentTemplateDir(cwd, root); - if (agentId === "dsh") { - writeDshEntry(cwd, templatesDir, projectConfig); - } else if (agentId !== "codex") { - copyRulesTemplates(cwd, root, templatesDir); - } else { - writeCodexEntry(cwd, templatesDir, projectConfig); - } -} - -/** - * @param {string} cwd - * @param {string[]} [agentIds] 不传则仅 cursor - * @param {object} [options] - * @param {boolean} [options.overwriteKnowledge] - * @param {object} [options.configValues] init 交互收集的配置字段值 - * @param {string} [options.locale] 显式模板语言 - */ -async function run(cwd, agentIds, options = {}) { - const { overwriteKnowledge = false, configValues } = options; - const ids = normalizeAgentIds(agentIds || []); - const templatesRoot = path.join(__dirname, "..", "templates"); - const existingConfig = loadFlow2specConfig(cwd); - const requestedLocale = normalizeLocale( - options.locale || configValues?.locale || existingConfig.locale, - DEFAULT_LOCALE, - ); - const { templatesDir, locale } = resolveTemplatesDir(templatesRoot, requestedLocale); - const effectiveConfigValues = { ...(configValues || {}) }; - if (options.locale) { - effectiveConfigValues.locale = locale; - } - - ensureKnowledgeDirs(cwd); - removeKnowledgeUpdateCheckCache(cwd); - const gitignoreResult = ensureFlow2specGitignore(cwd); - ensureFlow2specProjectConfig(cwd, templatesDir, { - overwrite: false, - values: Object.keys(effectiveConfigValues).length ? effectiveConfigValues : undefined, - }); - const knowledgeResult = copyKnowledgeTemplates(cwd, templatesDir, { - overwrite: overwriteKnowledge, - }); - const routingUpgrade = upgradeKnowledgeRoutingAndMatchers(cwd, templatesDir, { - overwrite: overwriteKnowledge, - }); - finalizePkgRev(cwd, templatesDir); - validateKnowledgeRouting(cwd); - - const indexSnapshot = copyKnowledgeIndexTemplateSnapshot(cwd, templatesDir); - - const projectConfig = loadFlow2specConfig(cwd); - - const claudeHooksResult = {}; - for (const id of ids) { - ensureAgentDirs(cwd, id); - writeAgentArtifacts(cwd, id, templatesDir, projectConfig); - if (id === "claude") { - const result = writeClaudeAgentHooks(cwd, templatesDir); - claudeHooksResult.hookScriptWritten = result.hookScriptResult?.written ?? false; - claudeHooksResult.settingsChanged = result.settingsChanged; - } - // Cursor:写入官方 hooks.json,使 sessionStart 自动运行更新检测。 - if (id === "cursor") { - writeCursorUpdateCheckHook(cwd, templatesDir); - } - // Codex:写入官方 hooks.json,使 SessionStart 自动运行更新检测。 - if (id === "codex") { - writeCodexUpdateCheckHook(cwd, templatesDir); - } - } - return { - ids, - knowledgeResult, - overwriteKnowledge, - routingUpgrade, - indexSnapshot, - gitignoreResult, - projectConfig, - locale, - claudeHooksResult, - }; -} - -module.exports = run; +module.exports = require("@double-coding/flow2spec-core").legacy.init; diff --git a/lib/knowledgeEngine.js b/lib/knowledgeEngine.js index 0bf2cc4..cccd203 100644 --- a/lib/knowledgeEngine.js +++ b/lib/knowledgeEngine.js @@ -1,1415 +1 @@ -const fs = require("fs"); -const path = require("path"); -const { KNOWLEDGE_ROOT } = require("./agents"); -const { loadFlow2specConfig } = require("./flow2specConfig"); -const { resolveDeveloperContext, activeTaskDir } = require("./developerId"); - -const KNOWLEDGE_FILENAME = "manifest-routing.json"; -const MATCHERS_FILENAME = "manifest-matchers.json"; -const INDEX_FILENAME = "index.md"; -const TOPIC_DIR = "topics"; -const DELTA_FILENAME = "kb-delta.json"; -const KB_COMMANDS = new Set([ - "appendBody", - "replaceBody", - "updateFrontmatter", - "createTopic", -]); -const ALLOWED_TOPIC_PRIMARY = new Set(["policy", "config", "feature", "module"]); -const ALLOWED_TOPIC_CONFIDENCE = new Set(["manual", "inferred"]); -const TOPIC_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/; -const MATCHER_ID_RE = /^m-[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/; - -function ensureDir(dir) { - if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); -} - -function readJson(filePath) { - return JSON.parse(fs.readFileSync(filePath, "utf8")); -} - -function writeJson(filePath, data) { - fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8"); -} - -function stableStringify(value) { - if (Array.isArray(value)) { - return `[${value.map((item) => stableStringify(item)).join(",")}]`; - } - if (isPlainObject(value)) { - return `{${Object.keys(value) - .sort() - .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`) - .join(",")}}`; - } - return JSON.stringify(value); -} - -function resolveFromCwd(cwd, maybeRelativePath) { - return path.isAbsolute(maybeRelativePath) - ? maybeRelativePath - : path.join(cwd, maybeRelativePath); -} - -function isPlainObject(value) { - return ( - value && - typeof value === "object" && - !Array.isArray(value) && - Object.getPrototypeOf(value) === Object.prototype - ); -} - -function normalizeStringArray(values) { - const out = []; - const seen = new Set(); - for (const value of Array.isArray(values) ? values : []) { - if (typeof value !== "string") continue; - const item = value.trim(); - if (!item || seen.has(item)) continue; - seen.add(item); - out.push(item); - } - return out; -} - -function parseInlineArray(raw) { - const source = String(raw || "").trim(); - if (!source) return []; - const out = []; - let token = ""; - let quote = null; - let escaped = false; - const pushToken = () => { - const value = token.trim(); - if (value) out.push(parseFrontmatterScalar(value)); - token = ""; - }; - for (const ch of source) { - if (escaped) { - token += ch; - escaped = false; - continue; - } - if (ch === "\\") { - token += ch; - escaped = true; - continue; - } - if (quote) { - token += ch; - if (ch === quote) quote = null; - continue; - } - if (ch === '"' || ch === "'") { - token += ch; - quote = ch; - continue; - } - if (ch === ",") { - pushToken(); - continue; - } - token += ch; - } - pushToken(); - return out; -} - -// NOTE(frontmatter-subset): parse* / stringify* 只支持有限 YAML 子集 -// (null/bool/int/float/裸字符串/单层数组)。当前所有 change 类型 -// 都会经 normalizeTopicFrontmatter / normalizeStringArray 归一化, -// 类型是可控的。若未来允许 delta 直接写入任意 frontmatter,需要: -// 1) 显式声明每个字段的期望类型(否则会踩到 "3"↔3 类型漂移); -// 2) 或者引入一个真正的 YAML 库(如 yaml/js-yaml)替换本节。 -// 详情见 review 结论 L2。 -function parseFrontmatterScalar(raw) { - const value = String(raw || "").trim(); - if (value === "null" || value === "~") return null; - if (value === "true") return true; - if (value === "false") return false; - if (/^-?\d+$/.test(value)) return Number.parseInt(value, 10); - if (/^-?\d+\.\d+$/.test(value)) return Number.parseFloat(value); - if ( - (value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'")) - ) { - return value.slice(1, -1).replace(/\\"/g, '"').replace(/\\'/g, "'"); - } - if (value.startsWith("[") && value.endsWith("]")) { - return parseInlineArray(value.slice(1, -1)); - } - return value; -} - -function stringifyFrontmatterScalar(value) { - if (value === null) return "null"; - if (typeof value === "number" || typeof value === "boolean") { - return String(value); - } - const str = String(value); - if (!str) return '""'; - if (/^[A-Za-z0-9_./:@-]+$/.test(str)) return str; - return JSON.stringify(str); -} - -function stringifyFrontmatterValue(value) { - if (Array.isArray(value)) { - return `[${value.map((item) => stringifyFrontmatterScalar(item)).join(", ")}]`; - } - return stringifyFrontmatterScalar(value); -} - -function parseFrontmatterBlock(block) { - const out = {}; - const lines = String(block || "").split(/\r?\n/); - for (const line of lines) { - if (!line.trim()) continue; - const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/); - if (!match) continue; - const key = match[1]; - const value = match[2].trim(); - if (!value) { - out[key] = ""; - continue; - } - out[key] = parseFrontmatterScalar(value); - } - return out; -} - -function parseTopicDocument(raw) { - const source = String(raw || ""); - const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/); - if (!match) { - return { - hasFrontmatter: false, - frontmatter: {}, - body: source, - }; - } - return { - hasFrontmatter: true, - frontmatter: parseFrontmatterBlock(match[1]), - body: source.slice(match[0].length), - }; -} - -function stringifyTopicDocument(frontmatter, body) { - const fm = stringifyFrontmatter(frontmatter); - const normalizedBody = String(body || "").replace(/^\n+/, ""); - if (!fm) { - return normalizedBody.endsWith("\n") ? normalizedBody : `${normalizedBody}\n`; - } - const bodyText = normalizedBody.endsWith("\n") - ? normalizedBody - : `${normalizedBody}\n`; - return `---\n${fm}---\n${bodyText}`; -} - -function stringifyFrontmatter(frontmatter) { - const source = isPlainObject(frontmatter) ? frontmatter : {}; - const preferredOrder = [ - "id", - "revision", - "summary", - "dependsOn", - "primary", - "confidence", - ]; - const keys = []; - for (const key of preferredOrder) { - if (Object.prototype.hasOwnProperty.call(source, key)) keys.push(key); - } - for (const key of Object.keys(source).sort()) { - if (!keys.includes(key)) keys.push(key); - } - const lines = []; - for (const key of keys) { - const value = source[key]; - if (value === undefined) continue; - lines.push(`${key}: ${stringifyFrontmatterValue(value)}`); - } - return lines.length ? `${lines.join("\n")}\n` : ""; -} - -function topicPathFor(topicId) { - return path.posix.join(KNOWLEDGE_ROOT, TOPIC_DIR, `${topicId}.md`); -} - -function topicAbsPath(cwd, topicPath) { - return resolveFromCwd(cwd, topicPath); -} - -function matcherPathFor(matcherId) { - return path.posix.join(KNOWLEDGE_ROOT, "matchers", `${matcherId}.json`); -} - -function graphCwd(graph) { - if (graph.cwd) return graph.cwd; - if (graph.routingPath) { - return path.dirname(path.dirname(graph.routingPath)); - } - return process.cwd(); -} - -function loadKnowledgeGraph(cwd) { - const routingPath = path.join(cwd, KNOWLEDGE_ROOT, KNOWLEDGE_FILENAME); - const matchersPath = path.join(cwd, KNOWLEDGE_ROOT, MATCHERS_FILENAME); - if (!fs.existsSync(routingPath)) { - throw new Error( - `缺少知识库路由清单:${path.join(KNOWLEDGE_ROOT, KNOWLEDGE_FILENAME)}`, - ); - } - const routing = readJson(routingPath); - const matchers = fs.existsSync(matchersPath) ? readJson(matchersPath) : null; - const topicEntries = []; - const topicPaths = routing.topicPaths || {}; - for (const [topicId, topicPath] of Object.entries(topicPaths)) { - const absPath = topicAbsPath(cwd, topicPath); - if (!fs.existsSync(absPath)) { - topicEntries.push({ - topicId, - path: topicPath, - absPath, - exists: false, - }); - continue; - } - const raw = fs.readFileSync(absPath, "utf8"); - const parsed = parseTopicDocument(raw); - const meta = routing.topicMetadata?.[topicId] || {}; - const frontmatter = isPlainObject(parsed.frontmatter) - ? { ...parsed.frontmatter } - : {}; - if (!Object.prototype.hasOwnProperty.call(frontmatter, "id")) { - frontmatter.id = topicId; - } - if (Object.prototype.hasOwnProperty.call(frontmatter, "dependsOn")) { - frontmatter.dependsOn = normalizeStringArray(frontmatter.dependsOn); - } - if (Object.prototype.hasOwnProperty.call(frontmatter, "primary")) { - frontmatter.primary = String(frontmatter.primary).trim(); - } else if (meta.primary) { - frontmatter.primary = meta.primary; - } - if (Object.prototype.hasOwnProperty.call(frontmatter, "confidence")) { - frontmatter.confidence = String(frontmatter.confidence).trim(); - } else if (meta.confidence) { - frontmatter.confidence = meta.confidence; - } - if (Object.prototype.hasOwnProperty.call(frontmatter, "tags")) { - frontmatter.tags = normalizeStringArray(frontmatter.tags); - } else if (Array.isArray(meta.tags)) { - frontmatter.tags = normalizeStringArray(meta.tags); - } - if ( - Object.prototype.hasOwnProperty.call(frontmatter, "summary") && - typeof frontmatter.summary !== "string" - ) { - frontmatter.summary = String(frontmatter.summary); - } - topicEntries.push({ - topicId, - path: topicPath, - absPath, - exists: true, - raw, - body: parsed.body, - hasFrontmatter: parsed.hasFrontmatter, - frontmatter, - routingMeta: meta, - }); - } - return { - routingPath, - matchersPath, - routing, - matchers, - topics: topicEntries, - }; -} - -function deriveRoutingOverlayFromGraph(graph) { - const topicMetadata = {}; - const topicDependencies = {}; - for (const topic of graph.topics) { - if (!topic.exists) continue; - const fm = topic.frontmatter || {}; - const entry = {}; - if (Object.prototype.hasOwnProperty.call(fm, "primary")) { - const primary = String(fm.primary || "").trim(); - if (ALLOWED_TOPIC_PRIMARY.has(primary)) { - entry.primary = primary; - } - } - if (Object.prototype.hasOwnProperty.call(fm, "confidence")) { - const confidence = String(fm.confidence || "").trim(); - if (ALLOWED_TOPIC_CONFIDENCE.has(confidence)) { - entry.confidence = confidence; - } - } - if (Array.isArray(fm.tags)) { - const tags = normalizeStringArray(fm.tags).filter( - (tag) => ALLOWED_TOPIC_PRIMARY.has(tag) && tag !== entry.primary, - ); - if (tags.length > 0) { - entry.tags = tags; - } - } - if (Object.keys(entry).length > 0) { - topicMetadata[topic.topicId] = entry; - } - if (Array.isArray(fm.dependsOn) && fm.dependsOn.length > 0) { - topicDependencies[topic.topicId] = normalizeStringArray(fm.dependsOn); - } - } - return { topicMetadata, topicDependencies }; -} - -function normalizeRoutingWithGraph(graph) { - const overlay = deriveRoutingOverlayFromGraph(graph); - const next = JSON.parse(JSON.stringify(graph.routing || {})); - let changed = false; - if (!isPlainObject(next.topicMetadata)) { - next.topicMetadata = {}; - changed = true; - } - if (!isPlainObject(next.topicDependencies)) { - next.topicDependencies = {}; - changed = true; - } - for (const [topicId, entry] of Object.entries(overlay.topicMetadata)) { - const raw = JSON.stringify(next.topicMetadata[topicId] || {}); - const nextRaw = JSON.stringify(entry); - if (raw !== nextRaw) { - next.topicMetadata[topicId] = entry; - changed = true; - } - } - for (const topicId of Object.keys(next.topicMetadata)) { - if (!Object.prototype.hasOwnProperty.call(overlay.topicMetadata, topicId)) { - if (graph.routing.topicMetadata?.[topicId]) continue; - delete next.topicMetadata[topicId]; - changed = true; - } - } - for (const [topicId, deps] of Object.entries(overlay.topicDependencies)) { - const raw = JSON.stringify(next.topicDependencies[topicId] || []); - const nextRaw = JSON.stringify(deps); - if (raw !== nextRaw) { - next.topicDependencies[topicId] = deps; - changed = true; - } - } - for (const topicId of Object.keys(next.topicDependencies)) { - if (!Object.prototype.hasOwnProperty.call(overlay.topicDependencies, topicId)) { - if (graph.routing.topicDependencies?.[topicId]) continue; - delete next.topicDependencies[topicId]; - changed = true; - } - } - return { routing: next, changed }; -} - -function validateKnowledgeGraph(graph, options = {}) { - const issues = []; - const warnings = []; - const strictRevision = Boolean(options.strictRevision); - const topicIds = new Set(); - - if (!graph || typeof graph !== "object") { - return { - ok: false, - issues: ["knowledge graph is empty"], - warnings, - topicCount: 0, - }; - } - - const routing = graph.routing || {}; - const topics = Array.isArray(graph.topics) ? graph.topics : []; - for (const topic of topics) { - topicIds.add(topic.topicId); - if (!topic.exists) { - issues.push(`topic missing: ${topic.topicId} -> ${topic.path}`); - continue; - } - const fm = topic.frontmatter || {}; - if (Object.prototype.hasOwnProperty.call(fm, "id") && fm.id !== topic.topicId) { - issues.push( - `topic frontmatter id mismatch: ${topic.topicId} vs ${String(fm.id)}`, - ); - } - if (Object.prototype.hasOwnProperty.call(fm, "revision")) { - const revision = Number(fm.revision); - if (!Number.isInteger(revision) || revision < 0) { - issues.push(`topic revision must be a non-negative integer: ${topic.topicId}`); - } - } else if (strictRevision) { - issues.push(`topic revision missing: ${topic.topicId}`); - } else { - warnings.push(`topic revision missing: ${topic.topicId}`); - } - if (Object.prototype.hasOwnProperty.call(fm, "primary")) { - const primary = String(fm.primary || "").trim(); - if (!ALLOWED_TOPIC_PRIMARY.has(primary)) { - issues.push(`topic primary invalid: ${topic.topicId} -> ${primary}`); - } - } - if (Object.prototype.hasOwnProperty.call(fm, "confidence")) { - const confidence = String(fm.confidence || "").trim(); - if (!ALLOWED_TOPIC_CONFIDENCE.has(confidence)) { - issues.push(`topic confidence invalid: ${topic.topicId} -> ${confidence}`); - } - } - if (Array.isArray(fm.dependsOn)) { - for (const depId of fm.dependsOn) { - if (!topic.topicId || typeof depId !== "string" || !depId.trim()) { - issues.push(`topic dependsOn contains empty value: ${topic.topicId}`); - continue; - } - if (!routing.topicPaths?.[depId]) { - issues.push(`topic dependsOn references missing topic: ${topic.topicId} -> ${depId}`); - } - } - } - } - - if (!routing.topicPaths || typeof routing.topicPaths !== "object") { - issues.push("routing.topicPaths missing or invalid"); - } - - if (routing.fallbackTopic && !routing.topicPaths?.[routing.fallbackTopic]) { - issues.push(`fallbackTopic missing from topicPaths: ${routing.fallbackTopic}`); - } - - if (routing.topicDependencies && typeof routing.topicDependencies === "object") { - for (const [topicId, deps] of Object.entries(routing.topicDependencies)) { - if (!routing.topicPaths?.[topicId]) { - issues.push(`topicDependencies references unknown topic: ${topicId}`); - } - if (!Array.isArray(deps)) { - issues.push(`topicDependencies.${topicId} must be an array`); - continue; - } - for (const depId of deps) { - if (!routing.topicPaths?.[depId]) { - issues.push( - `topicDependencies.${topicId} references unknown dependency: ${depId}`, - ); - } - } - } - } - - if (routing.topicMetadata && typeof routing.topicMetadata === "object") { - for (const [topicId, meta] of Object.entries(routing.topicMetadata)) { - if (!routing.topicPaths?.[topicId]) { - issues.push(`topicMetadata references unknown topic: ${topicId}`); - } - if (!meta || typeof meta !== "object" || Array.isArray(meta)) { - issues.push(`topicMetadata.${topicId} must be an object`); - continue; - } - if ( - Object.prototype.hasOwnProperty.call(meta, "primary") && - !ALLOWED_TOPIC_PRIMARY.has(String(meta.primary || "").trim()) - ) { - issues.push(`topicMetadata.${topicId}.primary invalid`); - } - if ( - Object.prototype.hasOwnProperty.call(meta, "confidence") && - !ALLOWED_TOPIC_CONFIDENCE.has(String(meta.confidence || "").trim()) - ) { - issues.push(`topicMetadata.${topicId}.confidence invalid`); - } - if (Array.isArray(meta.tags)) { - const seen = new Set(); - for (const tag of meta.tags) { - const normalized = String(tag || "").trim(); - if (!ALLOWED_TOPIC_PRIMARY.has(normalized)) { - issues.push(`topicMetadata.${topicId}.tags invalid value: ${normalized}`); - continue; - } - if (seen.has(normalized)) { - issues.push(`topicMetadata.${topicId}.tags contains duplicate: ${normalized}`); - } - seen.add(normalized); - } - } - } - } - - const matcherMap = - graph.matchers && graph.matchers.matchers && typeof graph.matchers.matchers === "object" - ? graph.matchers.matchers - : null; - if (graph.matchers && !matcherMap) { - issues.push("manifest-matchers structure invalid"); - } - - if (Array.isArray(routing.taskToTopicRules)) { - for (const rule of routing.taskToTopicRules) { - if (!rule || typeof rule !== "object") { - issues.push("taskToTopicRules contains a non-object rule"); - continue; - } - if (!rule.task || typeof rule.task !== "string") { - issues.push("taskToTopicRules entry missing task"); - } - if (!Array.isArray(rule.topics) || rule.topics.length === 0) { - issues.push(`taskToTopicRules(${rule.task || "unknown"}) must contain topics`); - } else { - for (const topicId of rule.topics) { - if (!routing.topicPaths?.[topicId]) { - issues.push( - `taskToTopicRules(${rule.task || "unknown"}) references unknown topic: ${topicId}`, - ); - } - } - } - if (!rule.matcherId || typeof rule.matcherId !== "string") { - issues.push(`taskToTopicRules(${rule.task || "unknown"}) missing matcherId`); - } - if (!rule.matcherPath || typeof rule.matcherPath !== "string") { - issues.push(`taskToTopicRules(${rule.task || "unknown"}) missing matcherPath`); - } else { - const matcherAbs = resolveFromCwd(graph.cwd || process.cwd(), rule.matcherPath); - if (!fs.existsSync(matcherAbs)) { - issues.push( - `taskToTopicRules(${rule.task || "unknown"}) matcherPath missing: ${rule.matcherPath}`, - ); - } else { - try { - const matcherShard = readJson(matcherAbs); - if (matcherShard.id !== rule.matcherId) { - issues.push( - `matcher id mismatch: ${rule.matcherPath} -> ${matcherShard.id} vs ${rule.matcherId}`, - ); - } - if (!Array.isArray(matcherShard.includeAny)) { - issues.push(`matcher includeAny invalid: ${rule.matcherPath}`); - } - } catch (error) { - issues.push(`matcher JSON invalid: ${rule.matcherPath}`); - } - } - } - } - } - - return { - ok: issues.length === 0, - issues, - warnings, - topicCount: topicIds.size, - }; -} - -function loadKnowledgeState(cwd) { - const graph = loadKnowledgeGraph(cwd); - graph.cwd = cwd; - const validation = validateKnowledgeGraph(graph); - return { graph, validation }; -} - -function parseKnowledgeDelta(input) { - const delta = typeof input === "string" ? readJson(input) : input; - if (!isPlainObject(delta)) { - throw new Error("kb delta 必须是对象"); - } - if (!delta.taskId || typeof delta.taskId !== "string") { - throw new Error("kb delta 缺少 taskId"); - } - if (!delta.developerId || typeof delta.developerId !== "string") { - throw new Error("kb delta 缺少 developerId"); - } - const baseRevisions = isPlainObject(delta.baseRevisions) - ? delta.baseRevisions - : {}; - const normalizedBaseRevisions = {}; - for (const [topicId, revision] of Object.entries(baseRevisions)) { - const nextRevision = Number(revision); - if (!topicId || !Number.isInteger(nextRevision) || nextRevision < 0) { - throw new Error(`kb delta baseRevisions 非法: ${topicId}`); - } - normalizedBaseRevisions[topicId] = nextRevision; - } - if (!Array.isArray(delta.changes) || delta.changes.length === 0) { - throw new Error("kb delta 需要至少一个 change"); - } - const changes = delta.changes.map((change, index) => - normalizeKnowledgeDeltaChange(change, index), - ); - return { - taskId: delta.taskId.trim(), - developerId: delta.developerId.trim(), - baseRevisions: normalizedBaseRevisions, - changes, - notes: typeof delta.notes === "string" ? delta.notes : "", - }; -} - -function normalizeKnowledgeDeltaChange(change, index) { - if (!isPlainObject(change)) { - throw new Error(`kb delta change[${index}] 必须是对象`); - } - const type = String(change.type || "").trim(); - if (!KB_COMMANDS.has(type)) { - throw new Error(`kb delta change[${index}] type 非法: ${type}`); - } - const targetTopic = String(change.targetTopic || "").trim(); - if (!targetTopic) { - throw new Error(`kb delta change[${index}] 缺少 targetTopic`); - } - const normalized = { - type, - targetTopic, - }; - if (Object.prototype.hasOwnProperty.call(change, "summary")) { - normalized.summary = String(change.summary || "").trim(); - } - if (Object.prototype.hasOwnProperty.call(change, "content")) { - normalized.content = String(change.content || ""); - } - if (Object.prototype.hasOwnProperty.call(change, "frontmatter")) { - if (!isPlainObject(change.frontmatter)) { - throw new Error(`kb delta change[${index}].frontmatter 必须是对象`); - } - normalized.frontmatter = JSON.parse(JSON.stringify(change.frontmatter)); - } - if (Object.prototype.hasOwnProperty.call(change, "taskRule")) { - if (!isPlainObject(change.taskRule)) { - throw new Error(`kb delta change[${index}].taskRule 必须是对象`); - } - normalized.taskRule = normalizeDeltaTaskRule( - change.taskRule, - normalized.targetTopic, - index, - ); - } - if (Object.prototype.hasOwnProperty.call(change, "matcher")) { - if (!isPlainObject(change.matcher)) { - throw new Error(`kb delta change[${index}].matcher 必须是对象`); - } - const matcherId = - normalized.taskRule?.matcherId || - String(change.matcher.id || `m-${normalized.targetTopic}`).trim(); - normalized.matcher = normalizeDeltaMatcher( - change.matcher, - matcherId, - index, - ); - if (normalized.taskRule && !normalized.taskRule.matcherId) { - normalized.taskRule.matcherId = normalized.matcher.id; - normalized.taskRule.matcherPath = matcherPathFor(normalized.matcher.id); - } - } - if (normalized.taskRule && !normalized.matcher) { - throw new Error( - `kb delta change[${index}] 带 taskRule 时必须同时提供 matcher`, - ); - } - if ( - normalized.taskRule && - normalized.matcher && - normalized.taskRule.matcherId !== normalized.matcher.id - ) { - throw new Error( - `kb delta change[${index}] matcher id 不一致: ${normalized.matcher.id} vs ${normalized.taskRule.matcherId}`, - ); - } - if ( - (normalized.type === "appendBody" || normalized.type === "replaceBody") && - !String(normalized.content || "").trim() - ) { - // 提前到 parse 阶段:kb status/plan 命中此错误时会被 - // scanTaskKnowledgeDeltas 的 try/catch 转成结构化 error 字段, - // 而不是让 applyTopicChangeDraft 在 plan 时抛出裸异常炸掉 CLI。 - throw new Error( - `kb delta change[${index}] ${normalized.type} 缺少 content(不能为空字符串)`, - ); - } - if (normalized.type === "createTopic") { - if (!TOPIC_ID_RE.test(normalized.targetTopic)) { - throw new Error( - `kb delta change[${index}] targetTopic 非法: ${normalized.targetTopic}`, - ); - } - if (!String(normalized.content || "").trim()) { - throw new Error(`createTopic change for ${normalized.targetTopic} 缺少 content`); - } - normalized.frontmatter = normalizeTopicFrontmatter( - normalized.targetTopic, - normalized.frontmatter || {}, - { defaultPrimary: "feature", defaultConfidence: "inferred" }, - ); - } - return normalized; -} - -function normalizeDeltaTaskRule(rule, targetTopic, index) { - const task = String(rule.task || "").trim(); - if (!task) { - throw new Error(`kb delta change[${index}].taskRule 缺少 task`); - } - const matcherIdRaw = String(rule.matcherId || `m-${task}`).trim(); - if (!MATCHER_ID_RE.test(matcherIdRaw)) { - throw new Error( - `kb delta change[${index}].taskRule.matcherId 非法: ${matcherIdRaw}`, - ); - } - const topics = normalizeStringArray(rule.topics || [targetTopic]); - if (!topics.includes(targetTopic)) topics.push(targetTopic); - return { - task, - matcherId: matcherIdRaw, - matcherPath: - typeof rule.matcherPath === "string" && rule.matcherPath.trim() - ? rule.matcherPath.trim().replace(/\\/g, "/") - : matcherPathFor(matcherIdRaw), - topics, - }; -} - -function normalizeDeltaMatcher(matcher, matcherId, index) { - const id = String(matcher.id || matcherId || "").trim(); - if (!MATCHER_ID_RE.test(id)) { - throw new Error(`kb delta change[${index}].matcher.id 非法: ${id}`); - } - const out = { - id, - version: - typeof matcher.version === "string" && matcher.version.trim() - ? matcher.version.trim() - : "1.0.0", - schema: - typeof matcher.schema === "string" && matcher.schema.trim() - ? matcher.schema.trim() - : "flow2spec.matcher.v1", - includeAny: normalizeStringArray(matcher.includeAny), - }; - for (const key of ["includeAll", "excludeAny", "excludeAll"]) { - const values = normalizeStringArray(matcher[key]); - if (values.length > 0) out[key] = values; - } - if (out.includeAny.length === 0 && !out.includeAll?.length) { - throw new Error(`kb delta change[${index}].matcher 缺少 includeAny/includeAll`); - } - return out; -} - -function normalizeTopicFrontmatter(topicId, frontmatter, options = {}) { - const source = isPlainObject(frontmatter) ? frontmatter : {}; - const out = JSON.parse(JSON.stringify(source)); - out.id = topicId; - const revision = Number(out.revision || 0); - out.revision = Number.isInteger(revision) && revision >= 0 ? revision : 0; - if (!out.primary) out.primary = options.defaultPrimary || "feature"; - if (!out.confidence) out.confidence = options.defaultConfidence || "inferred"; - if (Object.prototype.hasOwnProperty.call(out, "dependsOn")) { - out.dependsOn = normalizeStringArray(out.dependsOn); - } - if (Object.prototype.hasOwnProperty.call(out, "tags")) { - out.tags = normalizeStringArray(out.tags); - } - return out; -} - -function inferSummaryFromBody(body) { - const heading = String(body || "") - .split(/\r?\n/) - .find((line) => line.trim().startsWith("#")); - return heading ? heading.replace(/^#+\s*/, "").trim() : ""; -} - -function frontmatterForRouting(topic, routing) { - const meta = routing.topicMetadata?.[topic.topicId] || {}; - const deps = routing.topicDependencies?.[topic.topicId] || []; - const current = isPlainObject(topic.frontmatter) ? topic.frontmatter : {}; - const out = JSON.parse(JSON.stringify(current)); - let changed = false; - const setIfMissingOrInvalid = (key, value, isValid = (item) => item !== undefined) => { - if (!isValid(value)) return; - if (!Object.prototype.hasOwnProperty.call(out, key) || out[key] === "") { - out[key] = value; - changed = true; - } - }; - - if (out.id !== topic.topicId) { - out.id = topic.topicId; - changed = true; - } - const revision = Number(out.revision); - if (!Number.isInteger(revision) || revision < 0) { - out.revision = 0; - changed = true; - } - setIfMissingOrInvalid("summary", inferSummaryFromBody(topic.body), (item) => Boolean(item)); - if (Array.isArray(deps) && deps.length > 0) { - const normalizedDeps = normalizeStringArray(deps); - if (JSON.stringify(out.dependsOn || []) !== JSON.stringify(normalizedDeps)) { - out.dependsOn = normalizedDeps; - changed = true; - } - } - if (meta.primary && ALLOWED_TOPIC_PRIMARY.has(meta.primary)) { - setIfMissingOrInvalid("primary", meta.primary); - } - if (meta.confidence && ALLOWED_TOPIC_CONFIDENCE.has(meta.confidence)) { - setIfMissingOrInvalid("confidence", meta.confidence); - } - if (Array.isArray(meta.tags) && meta.tags.length > 0) { - const tags = normalizeStringArray(meta.tags).filter((tag) => - ALLOWED_TOPIC_PRIMARY.has(tag), - ); - if (tags.length > 0 && JSON.stringify(out.tags || []) !== JSON.stringify(tags)) { - out.tags = tags; - changed = true; - } - } - return { frontmatter: out, changed }; -} - -function ensureTopicFrontmatterFromRouting(graph, options = {}) { - const dryRun = Boolean(options.dryRun); - const changedFiles = []; - for (const topic of graph.topics) { - if (!topic.exists) continue; - const next = frontmatterForRouting(topic, graph.routing); - if (!next.changed && topic.hasFrontmatter) continue; - const content = stringifyTopicDocument(next.frontmatter, topic.body); - topic.frontmatter = next.frontmatter; - topic.hasFrontmatter = true; - topic.raw = content; - if (!dryRun) { - fs.writeFileSync(topic.absPath, content, "utf8"); - } - changedFiles.push(topic.path); - } - return { changedFiles }; -} - -function planKnowledgeDelta(graph, delta) { - const parsedDelta = typeof delta === "string" ? parseKnowledgeDelta(delta) : parseKnowledgeDelta(delta); - const working = new Map(); - const originalRevisions = new Map(); - const pendingTaskRules = new Set(); - const pendingMatcherIds = new Set(); - const pendingMatcherPaths = new Set(); - for (const topic of graph.topics) { - if (!topic.exists) continue; - working.set(topic.topicId, JSON.parse(JSON.stringify(topic))); - originalRevisions.set(topic.topicId, Number(topic.frontmatter?.revision || 0)); - } - const plan = []; - const conflicts = []; - - for (const change of parsedDelta.changes) { - if (change.type === "createTopic") { - const createPlan = planCreateTopicChange(graph, working, change, { - pendingTaskRules, - pendingMatcherIds, - pendingMatcherPaths, - }); - if (createPlan.conflict) { - conflicts.push(createPlan.conflict); - continue; - } - working.set(change.targetTopic, createPlan.topic); - originalRevisions.set(change.targetTopic, 0); - if (change.taskRule) { - pendingTaskRules.add(change.taskRule.task); - } - if (change.matcher) { - pendingMatcherIds.add(change.matcher.id); - pendingMatcherPaths.add(change.taskRule?.matcherPath || matcherPathFor(change.matcher.id)); - } - plan.push(createPlan.plan); - continue; - } - const current = working.get(change.targetTopic); - if (!current) { - conflicts.push({ - topicId: change.targetTopic, - reason: "topic missing", - change, - }); - continue; - } - const currentRevision = Number(current.frontmatter?.revision || 0); - const originalRevision = originalRevisions.get(change.targetTopic) || 0; - const expected = parsedDelta.baseRevisions[change.targetTopic]; - if ( - Number.isInteger(expected) && - expected >= 0 && - expected !== originalRevision - ) { - conflicts.push({ - topicId: change.targetTopic, - reason: `revision mismatch ${expected} -> ${originalRevision}`, - change, - }); - continue; - } - const next = applyTopicChangeDraft(current, change); - working.set(change.targetTopic, next); - plan.push({ - topicId: change.targetTopic, - type: change.type, - beforeRevision: currentRevision, - afterRevision: next.frontmatter.revision, - summary: change.summary || "", - }); - } - - return { - delta: parsedDelta, - plan, - conflicts, - mergeable: conflicts.length === 0, - }; -} - -function planCreateTopicChange(graph, working, change, pending = {}) { - const topicId = change.targetTopic; - const cwd = graphCwd(graph); - const topic = createTopicDraft(cwd, change); - const topicPath = topic.path; - const absPath = topic.absPath; - if (working.has(topicId) || graph.routing.topicPaths?.[topicId] || fs.existsSync(absPath)) { - return { - conflict: { - topicId, - reason: "topic already exists", - change, - }, - }; - } - const deps = normalizeStringArray(change.frontmatter?.dependsOn); - for (const depId of deps) { - if (!working.has(depId) && !graph.routing.topicPaths?.[depId]) { - return { - conflict: { - topicId, - reason: `dependency missing: ${depId}`, - change, - }, - }; - } - } - if (change.taskRule) { - const rules = Array.isArray(graph.routing.taskToTopicRules) - ? graph.routing.taskToTopicRules - : []; - const duplicateRule = rules.find( - (rule) => - rule.task === change.taskRule.task || - rule.matcherId === change.taskRule.matcherId || - rule.matcherPath === change.taskRule.matcherPath, - ); - if (duplicateRule) { - return { - conflict: { - topicId, - reason: `task rule already exists: ${duplicateRule.task}`, - change, - }, - }; - } - if (pending.pendingTaskRules?.has(change.taskRule.task)) { - return { - conflict: { - topicId, - reason: `task rule duplicated in delta: ${change.taskRule.task}`, - change, - }, - }; - } - } - if (change.matcher) { - const matcherId = change.matcher.id; - const matcherPath = change.taskRule?.matcherPath || matcherPathFor(matcherId); - const matcherAbs = resolveFromCwd(cwd, matcherPath); - const matcherMap = graph.matchers?.matchers || {}; - if (matcherMap[matcherId] || fs.existsSync(matcherAbs)) { - return { - conflict: { - topicId, - reason: `matcher already exists: ${matcherId}`, - change, - }, - }; - } - if ( - pending.pendingMatcherIds?.has(matcherId) || - pending.pendingMatcherPaths?.has(matcherPath) - ) { - return { - conflict: { - topicId, - reason: `matcher duplicated in delta: ${matcherId}`, - change, - }, - }; - } - if (change.taskRule && matcherId !== change.taskRule.matcherId) { - return { - conflict: { - topicId, - reason: `matcher id mismatch: ${matcherId} vs ${change.taskRule.matcherId}`, - change, - }, - }; - } - } - return { - topic, - plan: { - topicId, - type: change.type, - beforeRevision: null, - afterRevision: topic.frontmatter.revision, - summary: change.summary || "", - creates: { - topicPath, - matcherPath: change.matcher - ? change.taskRule?.matcherPath || matcherPathFor(change.matcher.id) - : null, - taskRule: change.taskRule?.task || null, - }, - }, - }; -} - -function createTopicDraft(cwd, change) { - const topicId = change.targetTopic; - const topicPath = topicPathFor(topicId); - const absPath = topicAbsPath(cwd, topicPath); - const body = String(change.content || ""); - const bodyText = body.endsWith("\n") ? body : `${body}\n`; - const frontmatter = normalizeTopicFrontmatter(topicId, change.frontmatter || {}); - return { - topicId, - path: topicPath, - absPath, - exists: true, - raw: stringifyTopicDocument(frontmatter, bodyText), - body: bodyText, - hasFrontmatter: true, - frontmatter, - routingMeta: {}, - }; -} - -function applyTopicChangeDraft(topic, change) { - const next = JSON.parse(JSON.stringify(topic)); - const frontmatter = isPlainObject(next.frontmatter) ? next.frontmatter : {}; - const currentRevision = Number(frontmatter.revision || 0); - let body = String(next.body || ""); - - frontmatter.id = topic.topicId; - if (change.type === "appendBody") { - const fragment = String(change.content || "").trim(); - if (!fragment) { - throw new Error(`appendBody change for ${topic.topicId} 缺少 content`); - } - body = body.trimEnd(); - body = body ? `${body}\n\n${fragment}\n` : `${fragment}\n`; - } else if (change.type === "replaceBody") { - body = String(change.content || ""); - if (!body.trim()) { - throw new Error(`replaceBody change for ${topic.topicId} 缺少 content`); - } - if (!body.endsWith("\n")) body += "\n"; - } else if (change.type === "updateFrontmatter") { - const incoming = isPlainObject(change.frontmatter) ? change.frontmatter : {}; - for (const [key, value] of Object.entries(incoming)) { - if (value === undefined) continue; - if (key === "dependsOn") { - frontmatter.dependsOn = normalizeStringArray(value); - } else if (key === "revision") { - const revision = Number(value); - if (!Number.isInteger(revision) || revision < 0) { - throw new Error(`topic ${topic.topicId} revision 非法`); - } - frontmatter.revision = revision; - } else if (key === "tags") { - frontmatter.tags = normalizeStringArray(value); - } else { - frontmatter[key] = value; - } - } - } - - frontmatter.revision = currentRevision + 1; - next.frontmatter = frontmatter; - next.body = body; - next.hasFrontmatter = true; - return next; -} - -function applyKnowledgeDelta(cwd, deltaInput, options = {}) { - const graph = loadKnowledgeGraph(cwd); - graph.cwd = cwd; - const parsedDelta = parseKnowledgeDelta(deltaInput); - const dryRun = Boolean(options.dryRun); - const planResult = planKnowledgeDelta(graph, parsedDelta); - if (!planResult.mergeable) { - const error = new Error("kb delta 存在冲突,无法自动合并"); - error.planResult = planResult; - throw error; - } - - const changedFiles = []; - const changedTopicIds = []; - const createdTopicIds = []; - const matcherWrites = []; - const taskRuleWrites = []; - const drafts = new Map(); - for (const topic of graph.topics) { - if (!topic.exists) continue; - drafts.set(topic.topicId, JSON.parse(JSON.stringify(topic))); - } - for (const change of parsedDelta.changes) { - if (change.type === "createTopic") { - const nextTopic = createTopicDraft(cwd, change); - drafts.set(change.targetTopic, nextTopic); - if (!changedTopicIds.includes(change.targetTopic)) { - changedTopicIds.push(change.targetTopic); - } - if (!createdTopicIds.includes(change.targetTopic)) { - createdTopicIds.push(change.targetTopic); - } - if (change.matcher) { - matcherWrites.push({ - matcher: change.matcher, - matcherPath: change.taskRule?.matcherPath || matcherPathFor(change.matcher.id), - }); - } - if (change.taskRule) { - taskRuleWrites.push(change.taskRule); - } - continue; - } - if (!drafts.has(change.targetTopic)) { - throw new Error(`未知 topic: ${change.targetTopic}`); - } - const topic = drafts.get(change.targetTopic); - const nextTopic = applyTopicChangeDraft(topic, change); - drafts.set(change.targetTopic, nextTopic); - if (!changedTopicIds.includes(change.targetTopic)) { - changedTopicIds.push(change.targetTopic); - } - } - - for (const topicId of changedTopicIds) { - const topicIndex = graph.topics.findIndex((item) => item.topicId === topicId); - const nextTopic = drafts.get(topicId); - const nextContent = stringifyTopicDocument(nextTopic.frontmatter, nextTopic.body); - if (!dryRun) { - ensureDir(path.dirname(nextTopic.absPath)); - fs.writeFileSync(nextTopic.absPath, nextContent, "utf8"); - } - if (!changedFiles.includes(nextTopic.path)) { - changedFiles.push(nextTopic.path); - } - if (topicIndex >= 0) { - const topic = graph.topics[topicIndex]; - graph.topics[topicIndex] = { - ...topic, - ...nextTopic, - raw: nextContent, - }; - } else { - graph.topics.push({ - ...nextTopic, - raw: nextContent, - }); - } - } - - if (!isPlainObject(graph.routing.topicPaths)) { - graph.routing.topicPaths = {}; - } - for (const topicId of createdTopicIds) { - const topic = drafts.get(topicId); - graph.routing.topicPaths[topicId] = topic.path; - } - - if (!Array.isArray(graph.routing.taskToTopicRules)) { - graph.routing.taskToTopicRules = []; - } - for (const rule of taskRuleWrites) { - graph.routing.taskToTopicRules.push(rule); - } - - for (const item of matcherWrites) { - const matcherAbs = resolveFromCwd(cwd, item.matcherPath); - if (!dryRun) { - ensureDir(path.dirname(matcherAbs)); - writeJson(matcherAbs, item.matcher); - } - if (!changedFiles.includes(item.matcherPath)) { - changedFiles.push(item.matcherPath); - } - } - - if (graph.matchersPath && fs.existsSync(graph.matchersPath) && matcherWrites.length > 0) { - const manifestMatchers = graph.matchers || { - version: "1.0.0", - generatedFrom: ".Knowledge/manifest-routing.json", - matcherKey: "matcherId", - sourceOfTruth: ".Knowledge/manifest-routing.json", - matchers: {}, - }; - if (!isPlainObject(manifestMatchers.matchers)) { - manifestMatchers.matchers = {}; - } - for (const item of matcherWrites) { - const { id, ...matcherBody } = item.matcher; - manifestMatchers.matchers[id] = matcherBody; - } - graph.matchers = manifestMatchers; - if (!dryRun) { - writeJson(graph.matchersPath, manifestMatchers); - } - const manifestMatchersPath = path.posix.join(KNOWLEDGE_ROOT, MATCHERS_FILENAME); - if (!changedFiles.includes(manifestMatchersPath)) { - changedFiles.push(manifestMatchersPath); - } - } - - const normalizedRouting = normalizeRoutingWithGraph(graph); - if (normalizedRouting.changed && !dryRun) { - writeJson(graph.routingPath, normalizedRouting.routing); - changedFiles.push(path.posix.join(KNOWLEDGE_ROOT, KNOWLEDGE_FILENAME)); - } - - return { - dryRun, - changedFiles, - plan: planResult.plan, - conflicts: planResult.conflicts, - delta: parsedDelta, - }; -} - -function scanTaskKnowledgeDeltas(cwd, taskRoot) { - const resolvedRoot = taskRoot || resolveDeveloperContext(loadFlow2specConfig(cwd), { cwd }).taskRoot; - const activeRoot = path.join(cwd, resolvedRoot, "active"); - if (!fs.existsSync(activeRoot)) { - return []; - } - const tasks = []; - for (const name of fs.readdirSync(activeRoot)) { - const taskDir = path.join(activeRoot, name); - if (!fs.statSync(taskDir).isDirectory()) continue; - const deltaPath = path.join(taskDir, DELTA_FILENAME); - if (!fs.existsSync(deltaPath)) continue; - try { - const delta = parseKnowledgeDelta(deltaPath); - tasks.push({ - taskName: name, - taskDir, - deltaPath, - delta, - }); - } catch (error) { - tasks.push({ - taskName: name, - taskDir, - deltaPath, - error: error.message || String(error), - }); - } - } - return tasks; -} - -function summarizeKnowledgeState(cwd, options = {}) { - const { graph, validation } = loadKnowledgeState(cwd); - const taskRoot = options.taskRoot || - resolveDeveloperContext(loadFlow2specConfig(cwd), { cwd }).taskRoot; - const deltaFiles = scanTaskKnowledgeDeltas(cwd, taskRoot); - const normalizedRouting = normalizeRoutingWithGraph(graph); - const drift = - stableStringify(normalizedRouting.routing) !== stableStringify(graph.routing); - const tasks = deltaFiles.map((item) => { - if (item.error) { - return { - taskName: item.taskName, - deltaPath: item.deltaPath, - error: item.error, - }; - } - const plan = planKnowledgeDelta(graph, item.delta); - return { - taskName: item.taskName, - deltaPath: item.deltaPath, - mergeable: plan.mergeable, - plan: plan.plan, - conflicts: plan.conflicts, - }; - }); - return { - cwd, - taskRoot, - topicCount: graph.topics.length, - validation, - routingDrift: drift, - tasks, - }; -} - -function buildKnowledgeGraph(cwd, options = {}) { - const graph = loadKnowledgeGraph(cwd); - graph.cwd = cwd; - const topicFrontmatter = options.writeTopicFrontmatter - ? ensureTopicFrontmatterFromRouting(graph, { - dryRun: options.dryRun, - }) - : { changedFiles: [] }; - const normalizedRouting = normalizeRoutingWithGraph(graph); - const changed = - stableStringify(normalizedRouting.routing) !== stableStringify(graph.routing); - if (changed && !options.dryRun) { - writeJson(graph.routingPath, normalizedRouting.routing); - } - return { - changed: changed || topicFrontmatter.changedFiles.length > 0, - routingPath: graph.routingPath, - topicFrontmatterChanged: topicFrontmatter.changedFiles, - normalizedRouting: normalizedRouting.routing, - validation: validateKnowledgeGraph({ - ...graph, - routing: normalizedRouting.routing, - }), - }; -} - -module.exports = { - KNOWLEDGE_ROOT, - KNOWLEDGE_FILENAME, - MATCHERS_FILENAME, - INDEX_FILENAME, - TOPIC_DIR, - DELTA_FILENAME, - loadKnowledgeGraph, - loadKnowledgeState, - validateKnowledgeGraph, - parseTopicDocument, - stringifyTopicDocument, - parseKnowledgeDelta, - planKnowledgeDelta, - applyKnowledgeDelta, - scanTaskKnowledgeDeltas, - summarizeKnowledgeState, - buildKnowledgeGraph, - ensureTopicFrontmatterFromRouting, - normalizeRoutingWithGraph, - stableStringify, - topicPathFor, -}; +module.exports = require("@double-coding/flow2spec-core").legacy.knowledgeEngine; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..be3c7dc --- /dev/null +++ b/package-lock.json @@ -0,0 +1,49 @@ +{ + "name": "flow2spec-workspace", + "version": "3.3.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "flow2spec-workspace", + "version": "3.3.0", + "workspaces": [ + "packages/core", + "packages/cli" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@double-coding/flow2spec": { + "resolved": "packages/cli", + "link": true + }, + "node_modules/@double-coding/flow2spec-core": { + "resolved": "packages/core", + "link": true + }, + "packages/cli": { + "name": "@double-coding/flow2spec", + "version": "3.3.0", + "license": "ISC", + "dependencies": { + "@double-coding/flow2spec-core": "3.3.0" + }, + "bin": { + "flow2spec": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "packages/core": { + "name": "@double-coding/flow2spec-core", + "version": "3.3.0", + "license": "ISC", + "engines": { + "node": ">=16" + } + } + } +} diff --git a/package.json b/package.json index 5728986..459aada 100644 --- a/package.json +++ b/package.json @@ -1,61 +1,21 @@ { - "name": "@double-coding/flow2spec", - "version": "3.2.13", - "description": "在业务仓库初始化「文档驱动、可写回知识库」的 AI 协作骨架:项目根 .Knowledge 承载 stock-docs/req-docs 与机读路由,.cursor/.claude/.codex 写入 f2s-* 规则与技能(含 Karpathy 式编码行为准则,init 同步 rules / Codex topics / skills);init 只落结构与模板,业务内容由各 f2s-* 技能在对话中维护。", - "homepage": "https://github.com/double-coding-lab/Flow2Spec#readme", - "repository": { - "type": "git", - "url": "git+https://github.com/double-coding-lab/Flow2Spec.git" - }, - "bugs": { - "url": "https://github.com/double-coding-lab/Flow2Spec/issues" - }, - "main": "./cli.js", - "bin": { - "flow2spec": "cli.js" - }, - "files": [ - "assets/readme", - "cli.js", - "lib", - "templates", - "README.md" + "name": "flow2spec-workspace", + "private": true, + "version": "3.3.0", + "description": "Flow2Spec workspace for the Core library and CLI packages", + "workspaces": [ + "packages/core", + "packages/cli" ], - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org/" + "engines": { + "node": ">=16" }, "scripts": { - "test": "node cli.js --help && node cli.js kb check && node scripts/test-knowledge-engine.js && node scripts/test-developer-id.js && node scripts/test-template-knowledge.js && node scripts/test-init-gitignore.js && node scripts/test-dsh-init.js && node scripts/test-doctor.js", + "test": "node cli.js --help && node cli.js kb check && node scripts/test-knowledge-engine.js && node scripts/test-developer-id.js && node scripts/test-template-knowledge.js && node scripts/test-init-gitignore.js && node scripts/test-dsh-init.js && node scripts/test-doctor.js && node scripts/test-core-api.js && node scripts/test-package-install.js", + "test:core": "node scripts/test-core-api.js", + "test:cli": "node cli.js --help && node cli.js doctor --json", "sync:agents": "node cli.js init cursor claude codex", - "prepublishOnly": "node cli.js --help", - "pack:check": "npm pack --dry-run", + "pack:check": "npm pack --workspace @double-coding/flow2spec-core --dry-run && npm pack --workspace @double-coding/flow2spec --dry-run", "tag:version": "node scripts/git-tag-version.js" - }, - "keywords": [ - "flow2spec", - "cursor", - "cursor-rules", - "agent-skills", - "claude", - "codex", - "ai-workflow", - "project-context", - "knowledge-base", - "documentation", - "rules", - "skills", - "stock-docs", - "req-docs", - "context-engineering", - "f2s" - ], - "author": "兰大神 <550947002@qq.com>", - "contributors": [ - "七七是只猫 <292761894@qq.com>" - ], - "license": "ISC", - "engines": { - "node": ">=16" } } diff --git a/packages/cli/README.md b/packages/cli/README.md new file mode 100644 index 0000000..312111c --- /dev/null +++ b/packages/cli/README.md @@ -0,0 +1,3 @@ +# @double-coding/flow2spec + +Flow2Spec CLI。业务项目继续使用 `npx @double-coding/flow2spec@latest init`,CLI 内部调用 `@double-coding/flow2spec-core`。 diff --git a/packages/cli/cli.js b/packages/cli/cli.js new file mode 100644 index 0000000..8ae7bc9 --- /dev/null +++ b/packages/cli/cli.js @@ -0,0 +1,841 @@ +#!/usr/bin/env node + +const path = require("path"); +const fs = require("fs"); +const os = require("os"); +const readline = require("readline"); +const runInit = require("./lib/init"); +const { AGENTS } = require("./lib/agents"); +const { + loadFlow2specConfig, + CONFIG_FILENAME, + CONFIG_FIELDS, + getMissingConfigFields, + SUPPORTED_LOCALES, + normalizeLocale, +} = require("./lib/flow2specConfig"); +const knowledgeEngine = require("./lib/knowledgeEngine"); +const { runDoctor, formatDoctorReport } = require("./lib/doctor"); + +const { execFileSync } = require("child_process"); + +const args = process.argv.slice(2); +const sub = args[0]; + +const agentList = Object.entries(AGENTS) + .map(([id, { label }]) => `${id}(${label})`) + .join(", "); + +const pkg = require("./package.json"); + +const UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1000; + +function parseVersion(version) { + return String(version || "") + .replace(/^v/, "") + .split(/[.-]/) + .slice(0, 3) + .map((part) => { + const n = Number.parseInt(part, 10); + return Number.isFinite(n) ? n : 0; + }); +} + +function compareVersions(a, b) { + const av = parseVersion(a); + const bv = parseVersion(b); + for (let i = 0; i < 3; i += 1) { + const diff = (av[i] || 0) - (bv[i] || 0); + if (diff !== 0) return diff; + } + return 0; +} + +function updateCheckCacheFile() { + const safeName = String(pkg.name || "flow2spec").replace(/[^a-z0-9_.-]+/gi, "_"); + return path.join(os.homedir(), ".flow2spec", `${safeName}-update-check.json`); +} + +function readUpdateCheckCache() { + const file = updateCheckCacheFile(); + if (!fs.existsSync(file)) return null; + try { + const data = JSON.parse(fs.readFileSync(file, "utf8")); + if (!data || typeof data !== "object") return null; + if (Date.now() - Number(data.checkedAt || 0) > UPDATE_CHECK_TTL_MS) { + return null; + } + return data; + } catch { + return null; + } +} + +function writeUpdateCheckCache(latest) { + try { + const file = updateCheckCacheFile(); + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync( + file, + `${JSON.stringify({ latest, checkedAt: Date.now() }, null, 2)}\n`, + "utf8", + ); + } catch { + // 更新检查不能影响主命令。 + } +} + +function queryLatestPackageVersion() { + const cached = readUpdateCheckCache(); + if (cached?.latest) return cached.latest; + const latest = execFileSync("npm", ["view", pkg.name, "version"], { + encoding: "utf8", + timeout: 2000, + stdio: ["ignore", "pipe", "ignore"], + }).trim(); + if (latest) writeUpdateCheckCache(latest); + return latest; +} + +function shouldCheckForUpdates() { + if (process.env.FLOW2SPEC_SKIP_UPDATE_CHECK === "1") return false; + if (process.env.CI) return false; + if (!process.stdout.isTTY) return false; + return Boolean(pkg.name && pkg.version); +} + +/** + * 读取全局安装的同名包版本(如果有)。 + * + * 用 `npm root -g` 拿全局 node_modules 根目录,再读 `//package.json` 的 version。 + * 这是跨 Node / npm 版本最稳定的判断"用户是否全局装过"的方式(避免 `npm ls -g` 输出格式差异)。 + * + * @returns {string|null} 全局已装版本号;未装、读取失败一律返回 null + */ +function getGlobalInstalledVersion() { + if (!pkg.name) return null; + let globalRoot; + try { + globalRoot = execFileSync("npm", ["root", "-g"], { + encoding: "utf8", + timeout: 2000, + stdio: ["ignore", "pipe", "ignore"], + }).trim(); + } catch { + return null; + } + if (!globalRoot) return null; + const pkgJsonPath = path.join(globalRoot, pkg.name, "package.json"); + try { + if (!fs.existsSync(pkgJsonPath)) return null; + const data = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8")); + return typeof data.version === "string" ? data.version : null; + } catch { + return null; + } +} + +/** + * init 收尾时自动把全局 flow2spec 升到 latest。 + * + * 触发条件(同时满足): + * 1. 用户已经全局 `npm i -g` 装过本包(用 getGlobalInstalledVersion 检测); + * 2. npm registry 上 latest 严格高于全局已装版本。 + * + * 用户没全局装过 → 静默跳过;当前 cli 是 npx 临时缓存跑的,不要侵入用户全局环境。 + * `npm i -g` 失败(权限 / 私服 404 / 网络) → 打印错误 + 手动升级提示,但 init 整体仍 exit 0。 + * + * 该函数本身永不抛错,永远不影响 init 主流程。 + */ +function maybeAutoUpdateGlobalInstall() { + let installed; + try { + installed = getGlobalInstalledVersion(); + } catch { + return; + } + if (!installed) return; // 没全局装过 → 不打扰 + let latest; + try { + latest = queryLatestPackageVersion(); + } catch { + return; // 查 latest 失败静默跳过 + } + if (!latest) return; + if (compareVersions(latest, installed) <= 0) return; // 全局已是最新 + console.log(` +↻ 检测到全局 ${pkg.name}@${installed},正在升级到 v${latest}...`); + try { + execFileSync("npm", ["install", "-g", `${pkg.name}@latest`], { + stdio: "inherit", + }); + console.log(`✓ 全局 ${pkg.name} 已升级到 v${latest}`); + } catch (e) { + console.error(` +⚠ 全局升级失败:${e.message || e} + 可手动执行: flow2spec update`); + } +} + +function printKnowledgeUpgradeHint(latest) { + console.log(` +⚠ Flow2Spec 有新版本 v${latest}(当前 v${pkg.version}) +建议先更新包: + flow2spec update + +更新后请在 Agent 对话中执行: + f2s-kb-upgrade + +用于对齐项目知识库模板、manifest/matchers 与配置根产物;不要把单独 flow2spec init 当作知识库升级。 +`); +} + +function maybePrintUpdateNotice() { + if (!shouldCheckForUpdates()) return; + try { + const latest = queryLatestPackageVersion(); + if (latest && compareVersions(latest, pkg.version) > 0) { + printKnowledgeUpgradeHint(latest); + } + } catch { + // 静默跳过,不能因为网络或 npm registry 影响主命令。 + } +} + +function printJson(data) { + console.log(`${JSON.stringify(data, null, 2)}\n`); +} + +function printKnowledgeHelp() { + console.log(` +Flow2Spec KB - knowledge collaboration engine + +用法: + flow2spec kb status [--json] + flow2spec kb check [--strict] [--json] + flow2spec kb plan [--json] + flow2spec kb apply [--dry-run] [--json] + flow2spec kb build [--fix-topics] [--json] + +说明: + status - 汇总当前知识图、active task delta 与潜在漂移 + check - 校验 manifest/topic/frontmatter/revision + plan - 预演一个 kb-delta 是否可自动合并 + apply - 应用 kb-delta 并同步 topic frontmatter / routing + build - 基于 topic frontmatter 归一化 routing 元数据;--fix-topics 可为旧 topic 补 frontmatter/revision + +delta changes: + appendBody / replaceBody / updateFrontmatter / createTopic +`); +} + +const help = ` +Flow2Spec - 统一知识库工作流(AI 配置入口) v${pkg.version} + +用法: + flow2spec init [agent ...] [--reset-knowledge] [--yes] [--locale zh-CN|en-US] 在当前项目初始化:写入 .Knowledge 与所选 agent 入口 + flow2spec config 打印项目根 ${CONFIG_FILENAME} 的解析结果(缺省值合并后) + flow2spec doctor [--json] 只读检查运行环境、项目初始化、协作上下文与知识库健康 + flow2spec kb 知识库协作引擎:status / check / plan / apply / build + flow2spec version 显示当前 flow2spec 版本 + flow2spec update 更新 flow2spec 到最新版本;更新后提示执行 f2s-kb-upgrade + flow2spec --help 显示本说明 + +agent(可多个,空格分隔;省略时交互选择): + ${agentList} + +示例: + flow2spec init # 交互选择工具和配置 + flow2spec init # 直接写入指定客户端配置根,跳过工具选择 + flow2spec init # 同时初始化多个客户端 + flow2spec init --locale en-US # 使用英文模板初始化指定客户端 + flow2spec init dsh # 初始化 DeepSeek Harness 项目技能 + flow2spec init --yes # 跳过所有问答,使用默认值(适合 CI) + flow2spec init --reset-knowledge # 强制用模板覆盖 .Knowledge(谨慎) + +init 会: + 1. 交互询问要初始化的 AI 工具(见上方 agent 列表,可多选);已通过参数指定则跳过。 + 传 --yes 或非 TTY 环境时跳过问答,使用默认值。 + 2. 对 ${CONFIG_FILENAME} 中缺失的配置字段逐项提问(已有字段不覆盖)。模板语言由 --locale、已有 locale 或默认 zh-CN 决定。 + 传 --yes 时所有缺失字段使用各自默认值。 + 3. 默认仅补齐 .Knowledge 缺失模板,并对路由清单做包级/结构增量对齐(manifest-routing + matcherPath 分片;关键词仅写在 matchers/*.json);不替代 f2s-* 对业务文档与路由内容的写入。 + 传 --reset-knowledge 时才会强制用模板覆盖 .Knowledge 中模板承载部分。 + 4. 在各 agent 配置根写入对应的 rules、skills、入口和 hooks(若该客户端支持);具体落盘方式以客户端适配器为准。 + 5. 每次 init 将当前 locale 包模板 knowledge/index.md 复制到 .Knowledge/template/index.template.md,供 f2s-kb-upgrade 技能与 .Knowledge/index.md 对照;不自动改写 index.md。(「知识库升级」指 f2s-kb-upgrade 技能,init 本身不是升级命令。) + 6. 非破坏式补充 .gitignore:忽略 .task/ 与 .Knowledge/update-check.json 这类本地运行态。 + 7. 规则与技能在各 agent 配置根加载;其他模版类文件在 .Knowledge/template/ 等目录。 + +更多说明见 README.md 或 docs/使用说明.md +`; + +if (sub === "--help" || sub === "-h" || !sub) { + console.log(help.trim()); + process.exit(0); +} + +if (sub === "version" || sub === "--version" || sub === "-v") { + console.log(`flow2spec v${pkg.version}`); + maybePrintUpdateNotice(); + process.exit(0); +} + +if (sub === "update") { + console.log(`当前版本: v${pkg.version}`); + console.log("正在检查最新版本..."); + try { + const latest = execFileSync("npm", ["view", pkg.name, "version"], { + encoding: "utf8", + }).trim(); + if (compareVersions(latest, pkg.version) <= 0) { + console.log(`当前版本不低于 npm 最新版本 v${latest}`); + process.exit(0); + } + console.log(`发现新版本: v${latest}`); + console.log("正在更新..."); + execFileSync("npm", ["install", "-g", `${pkg.name}@latest`], { + stdio: "inherit", + }); + console.log(`\n✓ 已更新到 v${latest}`); + console.log(` +下一步:请在需要升级的项目 Agent 对话中执行: + f2s-kb-upgrade + +用于对齐项目知识库模板、manifest/matchers 与配置根产物;不要把单独 flow2spec init 当作知识库升级。 +`); + } catch (e) { + console.error("更新失败:", e.message || e); + process.exit(1); + } + process.exit(0); +} + +if (sub === "config") { + const cwd = process.cwd(); + const abs = path.join(cwd, CONFIG_FILENAME); + try { + const cfg = loadFlow2specConfig(cwd); + console.log(JSON.stringify({ configPath: abs, ...cfg }, null, 2)); + } catch (e) { + console.error(e.message || e); + process.exit(1); + } + process.exit(0); +} + +if (sub === "doctor") { + const doctorArgs = args.slice(1); + if (doctorArgs.includes("--help") || doctorArgs.includes("-h")) { + console.log(` +用法: + flow2spec doctor [--json] + +只读检查 Node.js、项目配置、Agent 初始化、协作上下文、.task 忽略规则与知识库健康。 +警告不阻塞(exit 0),错误会返回 exit 1;本命令不会修改文件或访问网络。 +`.trim()); + process.exit(0); + } + const unknown = doctorArgs.filter((arg) => arg !== "--json"); + if (unknown.length > 0) { + console.error(`doctor 不支持参数:${unknown.join(" ")}`); + process.exit(1); + } + const report = runDoctor(process.cwd()); + if (doctorArgs.includes("--json")) { + printJson(report); + } else { + console.log(formatDoctorReport(report)); + } + process.exit(report.ok ? 0 : 1); +} + +if (sub === "kb") { + const kbSub = args[1]; + const kbFlags = new Set(args.slice(2).filter((arg) => String(arg || "").startsWith("--"))); + const kbPositionals = args.slice(2).filter((arg) => !String(arg || "").startsWith("--")); + const cwd = process.cwd(); + const jsonOut = kbFlags.has("--json"); + + try { + if (!kbSub || kbSub === "--help" || kbSub === "-h") { + printKnowledgeHelp(); + process.exit(0); + } + + if (kbSub === "status") { + const report = knowledgeEngine.summarizeKnowledgeState(cwd); + if (jsonOut) { + printJson(report); + } else { + console.log(`knowledge topics: ${report.topicCount}`); + console.log(`routing drift: ${report.routingDrift ? "yes" : "no"}`); + console.log(`validation: ${report.validation.ok ? "ok" : "has issues"}`); + if (report.validation.warnings.length) { + console.log(`warnings: ${report.validation.warnings.length}`); + } + if (report.tasks.length) { + console.log("active kb deltas:"); + for (const task of report.tasks) { + if (task.error) { + console.log(`- ${task.taskName}: ${task.error}`); + continue; + } + console.log( + `- ${task.taskName}: ${task.mergeable ? "mergeable" : "conflict"} (${task.plan.length} changes, ${task.conflicts.length} conflicts)`, + ); + } + } + } + process.exit(report.validation.ok ? 0 : 1); + } + + if (kbSub === "check") { + const strict = kbFlags.has("--strict"); + const graph = knowledgeEngine.loadKnowledgeGraph(cwd); + const validation = knowledgeEngine.validateKnowledgeGraph(graph, { + strictRevision: strict, + }); + const normalized = knowledgeEngine.normalizeRoutingWithGraph( + graph, + ); + const routingDrift = + knowledgeEngine.stableStringify(normalized.routing) !== + knowledgeEngine.stableStringify(graph.routing); + const report = knowledgeEngine.summarizeKnowledgeState(cwd); + const ok = + validation.issues.length === 0 && + !routingDrift && + (!strict || validation.warnings.length === 0); + const result = { + ok, + strict, + topicCount: report.topicCount, + issues: validation.issues, + warnings: validation.warnings, + routingDrift, + activeDeltas: report.tasks, + }; + if (jsonOut) { + printJson(result); + } else { + console.log(`knowledge check: ${result.ok ? "ok" : "failed"}`); + console.log(`topics: ${result.topicCount}`); + console.log(`routing drift: ${routingDrift ? "yes" : "no"}`); + if (result.issues.length) { + console.log(`issues: ${result.issues.length}`); + for (const issue of result.issues.slice(0, 10)) { + console.log(`- ${issue}`); + } + } + if (result.warnings.length) { + console.log(`warnings: ${result.warnings.length}`); + } + } + process.exit(result.ok ? 0 : 1); + } + + if (kbSub === "plan" || kbSub === "apply") { + const deltaArg = kbPositionals[0]; + if (!deltaArg) { + console.error(`kb ${kbSub} 需要 delta 文件路径`); + process.exit(1); + } + const deltaPath = path.resolve(cwd, deltaArg); + const dryRun = kbFlags.has("--dry-run") || kbSub === "plan"; + const graph = knowledgeEngine.loadKnowledgeGraph(cwd); + const delta = knowledgeEngine.parseKnowledgeDelta(deltaPath); + const plan = knowledgeEngine.planKnowledgeDelta(graph, delta); + if (kbSub === "plan") { + const result = { + ok: plan.mergeable, + deltaPath, + plan: plan.plan, + conflicts: plan.conflicts, + }; + if (jsonOut) { + printJson(result); + } else { + console.log(`kb plan: ${result.ok ? "mergeable" : "conflict"}`); + for (const item of result.plan) { + console.log( + `- ${item.topicId}: ${item.type} ${item.beforeRevision} -> ${item.afterRevision}`, + ); + } + for (const conflict of result.conflicts) { + console.log(`! ${conflict.topicId}: ${conflict.reason}`); + } + } + process.exit(result.ok ? 0 : 1); + } + const result = knowledgeEngine.applyKnowledgeDelta(cwd, deltaPath, { + dryRun, + }); + if (jsonOut) { + printJson(result); + } else { + console.log(`kb apply: ${dryRun ? "dry-run" : "applied"}`); + for (const file of result.changedFiles) { + console.log(`- ${file}`); + } + } + process.exit(0); + } + + if (kbSub === "build") { + const result = knowledgeEngine.buildKnowledgeGraph(cwd, { + writeTopicFrontmatter: kbFlags.has("--fix-topics"), + }); + if (jsonOut) { + printJson(result); + } else { + console.log(`kb build: ${result.changed ? "updated" : "up-to-date"}`); + console.log(`routing: ${path.relative(cwd, result.routingPath)}`); + if (result.topicFrontmatterChanged?.length) { + console.log(`topic frontmatter: ${result.topicFrontmatterChanged.length} updated`); + for (const file of result.topicFrontmatterChanged.slice(0, 10)) { + console.log(`- ${file}`); + } + } + console.log( + `validation: ${result.validation.ok ? "ok" : "has issues"}`, + ); + } + process.exit(result.validation.ok ? 0 : 1); + } + + console.error(`unknown kb subcommand: ${kbSub}`); + printKnowledgeHelp(); + process.exit(1); + } catch (e) { + console.error(e.message || e); + process.exit(1); + } +} + +if (sub === "init") { + const rawArgs = args.slice(1); + const overwriteKnowledge = rawArgs.includes("--reset-knowledge"); + const skipPrompts = rawArgs.includes("--yes") || rawArgs.includes("-y"); + let cliLocale; + const agentArgs = []; + for (let i = 0; i < rawArgs.length; i += 1) { + const arg = rawArgs[i]; + if (arg === "--reset-knowledge" || arg === "--yes" || arg === "-y") continue; + if (arg === "--locale") { + if (!rawArgs[i + 1] || rawArgs[i + 1].startsWith("--")) { + console.error(`--locale 需要取值。可选:${SUPPORTED_LOCALES.join(", ")}`); + process.exit(1); + } + cliLocale = String(rawArgs[i + 1] || "").trim(); + i += 1; + continue; + } + if (arg.startsWith("--locale=")) { + cliLocale = String(arg.slice("--locale=".length) || "").trim(); + continue; + } + agentArgs.push(arg); + } + if (cliLocale && !SUPPORTED_LOCALES.includes(cliLocale)) { + console.error(`不支持的 locale:${cliLocale}。可选:${SUPPORTED_LOCALES.join(", ")}`); + process.exit(1); + } + if (cliLocale) cliLocale = normalizeLocale(cliLocale); + + const cwd = process.cwd(); + + // ── 清除已输出的 n 行(用于多选 UI 重绘) + function clearLines(n) { + if (n <= 0) return; + process.stdout.write(`\x1b[${n}A\x1b[0J`); + } + + /** + * 多选 UI(raw mode):箭头键移动,空格选/取消,回车确认。 + * 非 TTY 环境直接返回默认选中项。 + */ + async function promptMultiSelect(title, items, defaultSelected = []) { + if (!process.stdin.isTTY || skipPrompts) { + return defaultSelected.length ? defaultSelected : [items[0].value]; + } + + const selected = new Set(defaultSelected.length ? defaultSelected : [items[0].value]); + let cursor = 0; + let rendered = 0; + + function render() { + if (rendered > 0) clearLines(rendered); + const lines = []; + lines.push(` ${title}`); + for (let i = 0; i < items.length; i++) { + const sel = selected.has(items[i].value); + const check = sel ? "\x1b[32m◉\x1b[0m" : "○"; + const arr = i === cursor ? "\x1b[36m›\x1b[0m" : " "; + const label = items[i].label.padEnd(10); + const desc = items[i].desc ? ` \x1b[2m${items[i].desc}\x1b[0m` : ""; + lines.push(` ${arr} ${check} ${label}${desc}`); + } + lines.push(""); + lines.push(" \x1b[2m↑↓ 移动 空格 选/取消 回车 确认\x1b[0m"); + rendered = lines.length; + process.stdout.write(lines.join("\n") + "\n"); + } + + render(); + + return new Promise((resolve) => { + function onKey(str, key) { + if (!key) return; + if (key.ctrl && key.name === "c") process.exit(0); + + if (key.name === "up") { + cursor = (cursor - 1 + items.length) % items.length; + render(); + } else if (key.name === "down") { + cursor = (cursor + 1) % items.length; + render(); + } else if (key.name === "space") { + const val = items[cursor].value; + if (selected.has(val)) selected.delete(val); + else selected.add(val); + render(); + } else if (key.name === "return") { + process.stdin.removeListener("keypress", onKey); + const result = selected.size ? [...selected] : [items[0].value]; + if (rendered > 0) clearLines(rendered); + const labels = result + .map((v) => items.find((i) => i.value === v)?.value) + .join(", "); + process.stdout.write(` ${title} \x1b[32m${labels}\x1b[0m\n`); + resolve(result); + } + } + process.stdin.on("keypress", onKey); + }); + } + + /** + * 单选 UI(raw mode):箭头键移动,回车确认。 + * 非 TTY 或 skipPrompts 时直接返回默认值。 + */ + async function promptSingleSelect(title, items, defaultValue) { + const fallback = defaultValue || items[0].value; + if (!process.stdin.isTTY || skipPrompts) return fallback; + + let cursor = Math.max(0, items.findIndex((item) => item.value === fallback)); + let rendered = 0; + + function render() { + if (rendered > 0) clearLines(rendered); + const lines = []; + lines.push(` ${title}`); + for (let i = 0; i < items.length; i++) { + const selected = i === cursor; + const check = selected ? "\x1b[32m◉\x1b[0m" : "○"; + const arr = selected ? "\x1b[36m›\x1b[0m" : " "; + const label = items[i].label.padEnd(10); + const desc = items[i].desc ? ` \x1b[2m${items[i].desc}\x1b[0m` : ""; + lines.push(` ${arr} ${check} ${label}${desc}`); + } + lines.push(""); + lines.push(" \x1b[2m↑↓ 移动 回车 确认\x1b[0m"); + rendered = lines.length; + process.stdout.write(lines.join("\n") + "\n"); + } + + render(); + + return new Promise((resolve) => { + function onKey(str, key) { + if (!key) return; + if (key.ctrl && key.name === "c") process.exit(0); + + if (key.name === "up") { + cursor = (cursor - 1 + items.length) % items.length; + render(); + } else if (key.name === "down") { + cursor = (cursor + 1) % items.length; + render(); + } else if (key.name === "return") { + process.stdin.removeListener("keypress", onKey); + const result = items[cursor]?.value || fallback; + if (rendered > 0) clearLines(rendered); + process.stdout.write(` ${title} \x1b[32m${result}\x1b[0m\n`); + resolve(result); + } + } + process.stdin.on("keypress", onKey); + }); + } + + /** + * 单键 y/n 问答(raw mode)。 + * 非 TTY 或 skipPrompts 时直接返回默认值。 + */ + async function promptBooleanKey(question, defaultValue = false) { + if (!process.stdin.isTTY || skipPrompts) return defaultValue; + + const hint = defaultValue + ? "\x1b[2m[Y/n]\x1b[0m" + : "\x1b[2m[y/N]\x1b[0m"; + process.stdout.write(` ${question} ${hint} `); + + return new Promise((resolve) => { + process.stdin.once("keypress", function (str, key) { + if (key && key.ctrl && key.name === "c") process.exit(0); + let result; + if (!str || str.trim() === "" || key?.name === "return") { + result = defaultValue; + } else { + result = str.trim().toLowerCase() === "y"; + } + process.stdout.write((result ? "\x1b[32my\x1b[0m" : "n") + "\n"); + resolve(result); + }); + }); + } + + async function promptLocale(question, defaultValue = "zh-CN") { + if (!process.stdin.isTTY || skipPrompts) return defaultValue; + const items = SUPPORTED_LOCALES.map((locale) => ({ + value: locale, + label: locale, + desc: locale === "zh-CN" ? "中文模板" : "English templates", + })); + return promptSingleSelect(question, items, defaultValue); + } + + async function collectInitOptions() { + const needAgentPrompt = agentArgs.length === 0 && !skipPrompts; + const missingFields = getMissingConfigFields(cwd); + const needConfigPrompt = missingFields.length > 0; + + // 没有任何需要处理的事情 + if (!needAgentPrompt && !needConfigPrompt) { + return { configValues: cliLocale ? { locale: cliLocale } : undefined, chosenAgents: agentArgs }; + } + + // --yes 模式:缺失字段直接用默认值,不弹交互 + if (skipPrompts) { + const configValues = needConfigPrompt + ? Object.fromEntries(missingFields.map((f) => [f.key, f.default])) + : undefined; + return { configValues, chosenAgents: agentArgs }; + } + + const isInteractive = process.stdin.isTTY; + if (isInteractive) { + readline.emitKeypressEvents(process.stdin); + process.stdin.setRawMode(true); + process.stdin.resume(); + } + + let chosenAgents = agentArgs; + let configValues; + + try { + process.stdout.write("\n"); + + if (needAgentPrompt) { + const agentItems = Object.entries(AGENTS).map(([id, { label }]) => ({ + value: id, + label: id, + desc: label, + })); + chosenAgents = await promptMultiSelect( + "选择要初始化的 AI 工具(可多选)", + agentItems, + ["cursor"], + ); + } + + if (needConfigPrompt) { + if (needAgentPrompt) process.stdout.write("\n"); + const isFirstTime = missingFields.length === CONFIG_FIELDS.length; + process.stdout.write( + ` 配置 ${CONFIG_FILENAME}${isFirstTime ? "(首次创建)" : "(补充新增字段)"}:\n\n`, + ); + const values = {}; + for (const field of missingFields) { + if (field.type === "locale") { + values[field.key] = cliLocale || await promptLocale(field.question, field.default); + } else { + values[field.key] = await promptBooleanKey( + field.question, + field.default, + ); + } + } + if (cliLocale) values.locale = cliLocale; + configValues = values; + } else if (cliLocale) { + configValues = { locale: cliLocale }; + } + } finally { + if (isInteractive) { + process.stdin.setRawMode(false); + process.stdin.pause(); + } + } + + process.stdout.write("\n"); + return { configValues, chosenAgents }; + } + + collectInitOptions() + .then(({ configValues, chosenAgents }) => + runInit(cwd, chosenAgents, { overwriteKnowledge, configValues, locale: cliLocale }), + ) + .then(({ ids, knowledgeResult, routingUpgrade, indexSnapshot, gitignoreResult, projectConfig, locale, claudeHooksResult }) => { + const lines = ids.map((id) => { + const { root, label } = AGENTS[id]; + if (id === "codex") + return ` - ${root}/:(${label})skills/、topics/、hooks/、hooks.json、AGENTS.md(指针);仓库根 AGENTS.md(完整)`; + if (id === "dsh") + return ` - ${root}/:(${label})skills/、topics/、AGENTS.md(指针);根 AGENTS.md(缺少时生成)`; + if (id === "claude") { + const hookLine = claudeHooksResult?.settingsChanged + ? "rules/、skills/、hooks/f2s-config-session.js、hooks/f2s-config-inject.js、settings.json(已写入 f2s SessionStart/PreToolUse hooks)" + : "rules/、skills/(settings.json 中 f2s hook 已存在,跳过)"; + return ` - ${root}/:(${label})${hookLine}`; + } + return ` - ${root}/:(${label})rules/、skills/`; + }); + const knowledgeLine = overwriteKnowledge + ? " - .Knowledge/:已按 --reset-knowledge 强制覆盖模板" + : ` - .Knowledge/:保留已有内容,补齐缺失模板(新增 ${knowledgeResult?.written || 0},跳过 ${knowledgeResult?.skipped || 0})`; + const routingLine = overwriteKnowledge + ? " - .Knowledge/manifest-routing.json + .Knowledge/matchers/*:已随 reset 覆盖到模板版本(不再写入 manifest-matchers.json)" + : routingUpgrade?.upgraded + ? " - 路由清单已与模板增量对齐" + : " - 路由清单已是最新能力路由,无需变更"; + const indexLine = + indexSnapshot?.written === false + ? ` - .Knowledge/template/index.template.md:未复制(${indexSnapshot?.reason || "skip"})` + : ` - .Knowledge/template/index.template.md:已从包内 templates/${locale}/knowledge/index.md 复制(与 .Knowledge/index.md 对照见 f2s-kb-upgrade 技能)`; + const pc = projectConfig || {}; + const configLine = ` - ${CONFIG_FILENAME}:locale=${pc.locale || locale}, subAgent=${Boolean(pc.subAgent)}, switchAgentVerification=${Boolean(pc.switchAgentVerification)}`; + const gitignoreLine = gitignoreResult?.changed + ? ` - .gitignore:已补充 ${gitignoreResult.added.join(", ")}` + : " - .gitignore:Flow2Spec 本地态忽略项已存在"; + console.log(` +✓ Flow2Spec init 完成 +${knowledgeLine} +${routingLine} +${indexLine} +${gitignoreLine} +${configLine} +${lines.join("\n")} + +建议阅读 README 或 docs/使用说明.md,按「规则在配置根、文档在 .Knowledge」的方式使用。 +`); + maybeAutoUpdateGlobalInstall(); + maybePrintUpdateNotice(); + }) + .catch((e) => { + console.error(e.message || e); + process.exit(1); + }); +} else { + console.log(help.trim()); + process.exit(1); +} diff --git a/packages/cli/lib/agents.js b/packages/cli/lib/agents.js new file mode 100644 index 0000000..8fe9444 --- /dev/null +++ b/packages/cli/lib/agents.js @@ -0,0 +1 @@ +module.exports = require("@double-coding/flow2spec-core").legacy.agents; diff --git a/packages/cli/lib/doctor.js b/packages/cli/lib/doctor.js new file mode 100644 index 0000000..17ea889 --- /dev/null +++ b/packages/cli/lib/doctor.js @@ -0,0 +1 @@ +module.exports = require("@double-coding/flow2spec-core").legacy.doctor; diff --git a/packages/cli/lib/flow2specConfig.js b/packages/cli/lib/flow2specConfig.js new file mode 100644 index 0000000..6de2f56 --- /dev/null +++ b/packages/cli/lib/flow2specConfig.js @@ -0,0 +1 @@ +module.exports = require("@double-coding/flow2spec-core").legacy.config; diff --git a/packages/cli/lib/init.js b/packages/cli/lib/init.js new file mode 100644 index 0000000..d3ee21a --- /dev/null +++ b/packages/cli/lib/init.js @@ -0,0 +1 @@ +module.exports = require("@double-coding/flow2spec-core").legacy.init; diff --git a/packages/cli/lib/knowledgeEngine.js b/packages/cli/lib/knowledgeEngine.js new file mode 100644 index 0000000..cccd203 --- /dev/null +++ b/packages/cli/lib/knowledgeEngine.js @@ -0,0 +1 @@ +module.exports = require("@double-coding/flow2spec-core").legacy.knowledgeEngine; diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 0000000..e6a5af6 --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,34 @@ +{ + "name": "@double-coding/flow2spec", + "version": "3.3.0", + "description": "在业务仓库初始化文档驱动、可写回知识库的 AI 协作骨架", + "homepage": "https://github.com/double-coding-lab/Flow2Spec#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/double-coding-lab/Flow2Spec.git", + "directory": "packages/cli" + }, + "bugs": { + "url": "https://github.com/double-coding-lab/Flow2Spec/issues" + }, + "main": "./cli.js", + "bin": { + "flow2spec": "cli.js" + }, + "files": [ + "cli.js", + "lib", + "README.md" + ], + "dependencies": { + "@double-coding/flow2spec-core": "3.3.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "engines": { + "node": ">=16" + }, + "license": "ISC" +} diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 0000000..3901612 --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,5 @@ +# @double-coding/flow2spec-core + +Flow2Spec 的程序化核心能力包,供 CLI 和原生开发工具插件共同使用。 + +普通用户通常不需要单独安装此包;安装 `@double-coding/flow2spec` 或 Flow2Spec 原生插件时会自动带上对应版本的 Core。 diff --git a/packages/core/capabilities.json b/packages/core/capabilities.json new file mode 100644 index 0000000..44cdd51 --- /dev/null +++ b/packages/core/capabilities.json @@ -0,0 +1,27 @@ +{ + "schema": "flow2spec.capabilities.v1", + "protocolVersion": 1, + "package": "@double-coding/flow2spec-core", + "capabilities": [ + { "id": "project.init", "api": "project.init", "since": "3.3.0" }, + { "id": "project.inspect", "api": "project.inspect", "since": "3.3.0" }, + { "id": "config.load", "api": "config.load", "since": "3.3.0" }, + { "id": "routing.graph", "api": "routing.graph", "since": "3.3.0" }, + { "id": "routing.state", "api": "routing.state", "since": "3.3.0" }, + { "id": "routing.match", "api": "routing.match", "since": "3.3.0" }, + { "id": "routing.expand", "api": "routing.expand", "since": "3.3.0" }, + { "id": "routing.verify", "api": "routing.verify", "since": "3.3.0" }, + { "id": "routing.load-context", "api": "routing.loadContext", "since": "3.3.0" }, + { "id": "knowledge.status", "api": "knowledge.status", "since": "3.3.0" }, + { "id": "knowledge.check", "api": "knowledge.check", "since": "3.3.0" }, + { "id": "knowledge.plan", "api": "knowledge.plan", "since": "3.3.0" }, + { "id": "knowledge.apply", "api": "knowledge.apply", "since": "3.3.0" }, + { "id": "knowledge.build", "api": "knowledge.build", "since": "3.3.0" }, + { "id": "collaboration.resolve", "api": "collaboration.resolveDeveloper", "since": "3.3.0" }, + { "id": "doctor.run", "api": "doctor.run", "since": "3.3.0" }, + { "id": "resources.capabilities", "api": "resources.capabilities", "since": "3.3.0" }, + { "id": "resources.skills", "api": "resources.listSkills", "since": "3.3.0" }, + { "id": "resources.rules", "api": "resources.listRules", "since": "3.3.0" }, + { "id": "resources.read", "api": "resources.read", "since": "3.3.0" } + ] +} diff --git a/packages/core/index.js b/packages/core/index.js new file mode 100644 index 0000000..3023d68 --- /dev/null +++ b/packages/core/index.js @@ -0,0 +1,158 @@ +"use strict"; + +const fs = require("fs"); +const path = require("path"); +const agents = require("./lib/agents"); +const claudeRulesAdapter = require("./lib/claudeRulesAdapter"); +const claudeSettingsAdapter = require("./lib/claudeSettingsAdapter"); +const codexAgentsAdapter = require("./lib/codexAgentsAdapter"); +const config = require("./lib/flow2specConfig"); +const developerId = require("./lib/developerId"); +const doctor = require("./lib/doctor"); +const dshAgentsAdapter = require("./lib/dshAgentsAdapter"); +const knowledgeEngine = require("./lib/knowledgeEngine"); +const init = require("./lib/init"); +const routing = require("./lib/routing"); +const capabilities = require("./capabilities.json"); + +class Flow2SpecError extends Error { + constructor(code, message, details = {}, options = {}) { + super(message); + this.name = "Flow2SpecError"; + this.code = code; + this.details = details; + this.recoverable = options.recoverable !== false; + } +} + +function assertCwd(cwd) { + if (typeof cwd !== "string" || !cwd.trim()) { + throw new Flow2SpecError( + "F2S_INVALID_ARGUMENT", + "cwd must be a non-empty project path", + { field: "cwd" }, + { recoverable: false }, + ); + } + return cwd; +} + +function resourceRoot(locale = "zh-CN") { + const selected = locale === "en-US" ? "en-US" : "zh-CN"; + return path.join(__dirname, "templates", selected); +} + +function listResourceFiles(locale, relativeRoot) { + const root = path.join(__dirname, "templates", locale === "en-US" ? "en-US" : "zh-CN", relativeRoot); + if (!fs.existsSync(root)) return []; + const files = []; + const visit = (current, prefix) => { + for (const entry of fs.readdirSync(current, { withFileTypes: true })) { + const relative = path.join(prefix, entry.name).replace(/\\/g, "/"); + if (entry.isDirectory()) visit(path.join(current, entry.name), relative); + else files.push(relative); + } + }; + visit(root, ""); + return files.sort(); +} + +function readResource(relativePath, locale = "zh-CN") { + const root = resourceRoot(locale); + const resolved = path.resolve(root, relativePath); + if (!resolved.startsWith(`${root}${path.sep}`)) { + throw new Flow2SpecError("F2S_INVALID_ARGUMENT", "resource path escapes package root", { + relativePath, + }); + } + if (!fs.existsSync(resolved)) { + throw new Flow2SpecError("F2S_RESOURCE_MISSING", `resource not found: ${relativePath}`, { + relativePath, + locale, + }); + } + return fs.readFileSync(resolved, "utf8"); +} + +function createFlow2Spec(options = {}) { + const cwd = assertCwd(options.cwd || process.cwd()); + const context = { + cwd, + signal: options.signal, + onProgress: typeof options.onProgress === "function" ? options.onProgress : () => {}, + }; + + return { + context, + project: { + init: (initOptions = {}) => init(cwd, initOptions.integrations || [], initOptions), + inspect: () => ({ cwd, config: config.loadFlow2specConfig(cwd) }), + }, + config: { + load: () => config.loadFlow2specConfig(cwd), + missingFields: () => config.getMissingConfigFields(cwd), + }, + routing: { + graph: () => knowledgeEngine.loadKnowledgeGraph(cwd), + state: () => knowledgeEngine.loadKnowledgeState(cwd), + match: (input = {}) => routing.match(cwd, input), + expand: (result) => routing.expand(cwd, result), + verify: (result, verifyOptions = {}) => routing.verify(cwd, result, verifyOptions), + loadContext: (result, contextOptions = {}) => + routing.loadContext(cwd, result, contextOptions), + }, + knowledge: { + status: (statusOptions = {}) => knowledgeEngine.summarizeKnowledgeState(cwd, statusOptions), + check: (checkOptions = {}) => { + const graph = knowledgeEngine.loadKnowledgeGraph(cwd); + return knowledgeEngine.validateKnowledgeGraph(graph, checkOptions); + }, + plan: ({ delta, deltaFile } = {}) => { + const graph = knowledgeEngine.loadKnowledgeGraph(cwd); + const parsed = delta || knowledgeEngine.parseKnowledgeDelta(deltaFile); + return knowledgeEngine.planKnowledgeDelta(graph, parsed); + }, + apply: ({ deltaFile, ...applyOptions } = {}) => + knowledgeEngine.applyKnowledgeDelta(cwd, deltaFile, applyOptions), + build: (buildOptions = {}) => knowledgeEngine.buildKnowledgeGraph(cwd, buildOptions), + }, + collaboration: { + resolveDeveloper: (resolveOptions = {}) => + developerId.resolveDeveloperContext(config.loadFlow2specConfig(cwd), { + cwd, + ...resolveOptions, + }), + }, + doctor: { + run: (doctorOptions = {}) => doctor.runDoctor(cwd, doctorOptions), + }, + resources: { + root: __dirname, + capabilities: () => capabilities, + listSkills: (locale = "zh-CN") => listResourceFiles(locale, "skills"), + listRules: (locale = "zh-CN") => listResourceFiles(locale, "rules"), + listHooks: (locale = "zh-CN") => listResourceFiles(locale, "hooks"), + read: (relativePath, locale = "zh-CN") => readResource(relativePath, locale), + }, + }; +} + +module.exports = { + Flow2SpecError, + createFlow2Spec, + getCapabilities: () => capabilities, + resourcesRoot: __dirname, + legacy: { + AGENTS: agents.AGENTS, + agents, + claudeRulesAdapter, + claudeSettingsAdapter, + codexAgentsAdapter, + config, + developerId, + doctor, + dshAgentsAdapter, + init, + knowledgeEngine, + }, +}; diff --git a/packages/core/lib/agents.js b/packages/core/lib/agents.js new file mode 100644 index 0000000..d6937e7 --- /dev/null +++ b/packages/core/lib/agents.js @@ -0,0 +1,49 @@ +/** + * flow2spec init 支持的 AI 工具配置目录。 + * 知识库统一写入项目根 `.Knowledge/`(含 template),rules/skills 保留在各配置根。 + */ +const AGENTS = { + cursor: { root: ".cursor", label: "Cursor" }, + claude: { root: ".claude", label: "Claude" }, + codex: { root: ".codex", label: "Codex" }, + dsh: { root: ".dsh", label: "DeepSeek Harness" }, +}; + +const KNOWLEDGE_ROOT = ".Knowledge"; +const KNOWLEDGE_SUBDIRS = ["stock-docs", "req-docs", "matchers"]; +const AGENT_SUBDIRS = { + cursor: ["rules", "skills"], + claude: ["rules", "skills"], + codex: ["skills"], + dsh: ["skills", "topics"], +}; + +/** + * @param {string[]} argv init 后的参数,如 []、['cursor']、['cursor','claude'] + * @returns {string[]} 去重后的 agent id 列表 + */ +function normalizeAgentIds(argv) { + const list = argv.length ? argv : ["cursor"]; + const seen = new Set(); + const out = []; + for (const raw of list) { + const id = String(raw).toLowerCase().replace(/^--/, ""); + if (!AGENTS[id]) { + const keys = Object.keys(AGENTS).join(", "); + throw new Error(`未知 agent:${raw}。可选:${keys}`); + } + if (!seen.has(id)) { + seen.add(id); + out.push(id); + } + } + return out; +} + +module.exports = { + AGENTS, + KNOWLEDGE_ROOT, + KNOWLEDGE_SUBDIRS, + AGENT_SUBDIRS, + normalizeAgentIds, +}; diff --git a/packages/core/lib/claudeRulesAdapter.js b/packages/core/lib/claudeRulesAdapter.js new file mode 100644 index 0000000..032d8f7 --- /dev/null +++ b/packages/core/lib/claudeRulesAdapter.js @@ -0,0 +1,28 @@ +/** + * Cursor 规则为 .mdc + frontmatter globs、alwaysApply。 + * Claude Code 仅识别 .claude/rules 下扩展名为 .md 的规则文件,路径范围用 paths(见 Claude Code 文档:Organize rules with .claude/rules)。 + */ + +/** + * @param {string} mdcSource 完整 .mdc 文件正文 + * @returns {string} 写入 `.claude/rules/*.md` 的正文 + */ +function adaptRuleMdcToClaudeMd(mdcSource) { + let out = mdcSource; + // YAML:globs → paths(与 Cursor 语义等价) + out = out.replace(/^globs:/m, "paths:"); + // Claude Code 不按 Cursor 的 alwaysApply 解析;无 paths 的规则与会话同载 + out = out.replace(/^\s*alwaysApply:\s*(true|false)\s*\r?\n/m, ""); + // 正文与示例中的 .mdc 引用改为 .md,与落盘扩展名一致 + out = out.replace(/\.mdc\b/g, ".md"); + return out; +} + +/** + * @param {string} agentRoot AGENTS[id].root,如 `.claude` + */ +function shouldWriteClaudeStyleRules(agentRoot) { + return agentRoot === ".claude"; +} + +module.exports = { adaptRuleMdcToClaudeMd, shouldWriteClaudeStyleRules }; diff --git a/packages/core/lib/claudeSettingsAdapter.js b/packages/core/lib/claudeSettingsAdapter.js new file mode 100644 index 0000000..44d9d9e --- /dev/null +++ b/packages/core/lib/claudeSettingsAdapter.js @@ -0,0 +1,228 @@ +'use strict'; +/** + * 负责合并 flow2spec hook 配置到 .claude/settings.json。 + * 仅在 init --claude 时调用。 + */ +const fs = require('fs'); +const path = require('path'); + +const HOOK_COMMAND_CONFIG_INJECT = 'node .claude/hooks/f2s-config-inject.js'; +const HOOK_COMMAND_CONFIG_SESSION = 'node .claude/hooks/f2s-config-session.js'; +const HOOK_COMMAND_UPDATE_CHECK = 'node .claude/hooks/f2s-update-check.js'; + +// 向下兼容 +const HOOK_COMMAND = HOOK_COMMAND_CONFIG_INJECT; + +function findPackageJsonDir(startDir) { + let cur = startDir; + while (cur && cur !== path.dirname(cur)) { + if (fs.existsSync(path.join(cur, 'package.json'))) return cur; + cur = path.dirname(cur); + } + return null; +} + +function hasHookCommand(arr, fragment) { + if (!Array.isArray(arr)) return false; + for (const group of arr) { + if (!group || !Array.isArray(group.hooks)) continue; + for (const h of group.hooks) { + if (h && h.type === 'command' && String(h.command || '').includes(fragment)) { + return true; + } + } + } + return false; +} + +function removeHookCommand(arr, fragment) { + if (!Array.isArray(arr)) return []; + const next = []; + for (const group of arr) { + if (!group || !Array.isArray(group.hooks)) { + next.push(group); + continue; + } + const hooks = group.hooks.filter((h) => { + return !(h && h.type === 'command' && String(h.command || '').includes(fragment)); + }); + if (hooks.length) next.push(Object.assign({}, group, { hooks })); + } + return next; +} + +// 旧函数名保持兼容 +function hasF2sHook(preToolUseArr) { + return hasHookCommand(preToolUseArr, 'f2s-config-inject'); +} + +/** + * 将 f2s PreToolUse 守门 hook 合并进现有 settings,返回新对象(不修改原对象)。 + * @param {object} existing 现有 settings(可为 {}) + * @returns {object} + */ +function mergeF2sHook(existing) { + const next = JSON.parse(JSON.stringify(existing || {})); + if (!next.hooks) next.hooks = {}; + if (!next.hooks.PreToolUse) next.hooks.PreToolUse = []; + + if (hasF2sHook(next.hooks.PreToolUse)) { + return { settings: next, changed: false }; + } + + next.hooks.PreToolUse.push({ + matcher: 'Skill', + hooks: [{ type: 'command', command: HOOK_COMMAND }], + }); + + return { settings: next, changed: true }; +} + +/** + * 将 f2s SessionStart 配置摘要 hook 合并进 settings。 + * @param {object} existing + * @returns {{ settings, changed }} + */ +function mergeConfigSessionHook(existing) { + const next = JSON.parse(JSON.stringify(existing || {})); + if (!next.hooks) next.hooks = {}; + if (!next.hooks.SessionStart) next.hooks.SessionStart = []; + + if (hasHookCommand(next.hooks.SessionStart, 'f2s-config-session')) { + return { settings: next, changed: false }; + } + + next.hooks.SessionStart.push({ + hooks: [{ type: 'command', command: HOOK_COMMAND_CONFIG_SESSION }], + }); + + return { settings: next, changed: true }; +} + +/** + * 将 f2s 更新检查 hook 合并进 settings: + * - SessionStart:执行完整检测,写入 .Knowledge/update-check.json,并直接 emit 提示 + * - 同时清理旧版 UserPromptSubmit 中的 f2s-update-check / f2s-update-notice + * @param {object} existing + * @returns {{ settings, changed }} + */ +function mergeUpdateCheckHook(existing) { + const next = JSON.parse(JSON.stringify(existing || {})); + if (!next.hooks) next.hooks = {}; + if (!next.hooks.SessionStart) next.hooks.SessionStart = []; + + let changed = false; + + if (Array.isArray(next.hooks.UserPromptSubmit)) { + const before = JSON.stringify(next.hooks.UserPromptSubmit); + next.hooks.UserPromptSubmit = removeHookCommand(next.hooks.UserPromptSubmit, 'f2s-update-check'); + next.hooks.UserPromptSubmit = removeHookCommand(next.hooks.UserPromptSubmit, 'f2s-update-notice'); + if (JSON.stringify(next.hooks.UserPromptSubmit) !== before) changed = true; + if (next.hooks.UserPromptSubmit.length === 0) { + delete next.hooks.UserPromptSubmit; + } + } + + if (!hasHookCommand(next.hooks.SessionStart, 'f2s-update-check')) { + next.hooks.SessionStart.push({ + hooks: [{ type: 'command', command: HOOK_COMMAND_UPDATE_CHECK }], + }); + changed = true; + } + + return { settings: next, changed }; +} + +/** + * 读取 .claude/settings.json(不存在则返回 {})。 + * @param {string} claudeRoot .claude 目录绝对路径 + * @returns {object} + */ +function readSettings(claudeRoot) { + const settingsPath = path.join(claudeRoot, 'settings.json'); + if (!fs.existsSync(settingsPath)) return {}; + try { + return JSON.parse(fs.readFileSync(settingsPath, 'utf8')); + } catch (_err) { + return {}; + } +} + +/** + * 写入 .claude/settings.json。 + * @param {string} claudeRoot + * @param {object} settings + */ +function writeSettings(claudeRoot, settings) { + const settingsPath = path.join(claudeRoot, 'settings.json'); + fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, 'utf8'); +} + +/** + * 复制 hook 脚本到 .claude/hooks/。 + */ +function copyHookScript(claudeRoot, templatesDir, scriptName) { + const src = path.join(templatesDir, 'hooks', scriptName); + if (!fs.existsSync(src)) return { written: false, reason: 'missing-template' }; + + const hooksDir = path.join(claudeRoot, 'hooks'); + if (!fs.existsSync(hooksDir)) fs.mkdirSync(hooksDir, { recursive: true }); + + let body = fs.readFileSync(src, 'utf8'); + if (body.includes('__FLOW2SPEC_PACKAGE_NAME__')) { + let packageName = '@double-coding/flow2spec'; + try { + const packageDir = findPackageJsonDir(templatesDir); + packageName = JSON.parse( + fs.readFileSync(path.join(packageDir || path.join(templatesDir, '..'), 'package.json'), 'utf8'), + ).name || packageName; + } catch (_) {} + body = body.replace(/__FLOW2SPEC_PACKAGE_NAME__/g, packageName); + } + fs.writeFileSync(path.join(hooksDir, scriptName), body, 'utf8'); + return { written: true }; +} + +/** + * 主入口:为 claude agent 配置 f2s hooks(SessionStart 配置摘要 + PreToolUse 守门 + 更新检测/提示)。 + * @param {string} cwd + * @param {string} templatesDir + * @returns {{ hookScriptResult, updateCheckResult, settingsChanged }} + */ +function writeClaudeAgentHooks(cwd, templatesDir) { + const claudeRoot = path.join(cwd, '.claude'); + if (!fs.existsSync(claudeRoot)) fs.mkdirSync(claudeRoot, { recursive: true }); + + const hookScriptResult = copyHookScript(claudeRoot, templatesDir, 'f2s-config-inject.js'); + const configSessionResult = copyHookScript(claudeRoot, templatesDir, 'f2s-config-session.js'); + const updateCheckResult = copyHookScript(claudeRoot, templatesDir, 'f2s-update-check.js'); + + // 清理旧版残留的 f2s-update-notice.js + const noticeStale = path.join(claudeRoot, 'hooks', 'f2s-update-notice.js'); + if (fs.existsSync(noticeStale)) { + try { fs.unlinkSync(noticeStale); } catch (_) {} + } + + let settings = readSettings(claudeRoot); + let changed = false; + + const r1 = mergeF2sHook(settings); + if (r1.changed) { settings = r1.settings; changed = true; } + + const r2 = mergeConfigSessionHook(settings); + if (r2.changed) { settings = r2.settings; changed = true; } + + const r3 = mergeUpdateCheckHook(settings); + if (r3.changed) { settings = r3.settings; changed = true; } + + if (changed) writeSettings(claudeRoot, settings); + + return { hookScriptResult, configSessionResult, updateCheckResult, settingsChanged: changed }; +} + +module.exports = { + writeClaudeAgentHooks, + mergeF2sHook, + mergeConfigSessionHook, + mergeUpdateCheckHook, +}; diff --git a/packages/core/lib/codexAgentsAdapter.js b/packages/core/lib/codexAgentsAdapter.js new file mode 100644 index 0000000..b217732 --- /dev/null +++ b/packages/core/lib/codexAgentsAdapter.js @@ -0,0 +1,72 @@ +const fs = require("fs"); +const path = require("path"); + +function readSkillSummary(skillsDir) { + if (!fs.existsSync(skillsDir)) return []; + const out = []; + for (const name of fs.readdirSync(skillsDir)) { + // Try SKILL.md first, then SKILL.mdc + let skillFile = path.join(skillsDir, name, "SKILL.md"); + if (!fs.existsSync(skillFile)) { + skillFile = path.join(skillsDir, name, "SKILL.mdc"); + } + if (!fs.existsSync(skillFile)) continue; + const raw = fs.readFileSync(skillFile, "utf8"); + const frontmatter = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/); + const body = frontmatter ? frontmatter[1] : ""; + const skillName = (body.match(/^\s*name:\s*(.+)\s*$/m) || [])[1] || name; + const desc = (body.match(/^\s*description:\s*(.+)\s*$/m) || [])[1] || "暂无描述"; + out.push(`- \`${skillName.trim()}\`:${desc.trim()}`); + } + return out.sort((a, b) => a.localeCompare(b, "zh-Hans-CN")); +} + +function renderProjectConfigBlock() { + return [ + "| 配置项 | init 默认 | 说明 |", + "| --- | --- | --- |", + "| `subAgent` | `true` | 技能正文写明某步可用子 agent 时,`true` 才允许拆子;`false` 一律主会话完成。用户「动态判断谁用子 agent」仅当本项为 `true` 时有效。 |", + "| `switchAgentVerification` | `true` | 切换 agent 校验。仅当本项为 `true` 且当前技能正文明确绑定该字段时启用交叉校验;否则仍是谁落盘谁自验。旧键 `subAgentVerification` 仍可被解析。 |", + "| `intentRecognition` | `true` | `true` 时可按 `f2s-intent-routing` 对高置信操作意图自动进入对应 `f2s-*` 技能;`false` 或缺失时不自动分流。 |", + "| `changeTracking.feat` | `true` | `true` 时 `f2s-kb-feat` 步骤 0 必须创建/续作 `.task/active/` 变更追踪任务;`false` 时跳过。 |", + "| `changeTracking.fix` | `false` | `true` 时 `f2s-kb-fix` 步骤 0 必须创建/续作 `.task/active/` 变更追踪任务;`false` 时跳过。 |", + "| `changeTracking.implement` | `true` | `true` 时 `f2s-implement-tech-design` 写入任务清单并在满足归档门禁后归档;`false` 时跳过变更追踪部分。 |", + "| `collaboration.enabled` | `true` | `true` 时按 developerId 隔离任务根 `.task//`;`false` 时始终 legacy 单根 `.task/`。 |", + "| `collaboration.developerId` | `\"\"` | 非空则作为任务进度目录名;空则尝试 git user.email/name 规范化;仍无则 legacy `.task/`。解析顺序:config → git → legacy。 |", + ].join("\n"); +} + +function renderCodexAgents(templateBody, skillsSummaryLines) { + const summary = + skillsSummaryLines.length > 0 + ? skillsSummaryLines.join("\n") + : "- 当前未发现可用技能。"; + let body = templateBody.replace( + "{{FLOW2SPEC_PROJECT_CONFIG}}", + renderProjectConfigBlock(), + ); + body = body.replace("{{FLOW2SPEC_CODEX_SKILLS_SUMMARY}}", summary); + return body; +} + +function buildCodexAgentsMd(templatesDir, projectConfig) { + const templatePath = path.join(templatesDir, "AGENTS.md"); + const skillsDir = path.join(templatesDir, "skills"); + const templateBody = fs.readFileSync(templatePath, "utf8"); + const skillLines = readSkillSummary(skillsDir); + return renderCodexAgents(templateBody, skillLines); +} + +function buildCodexAgentsStubMd(templatesDir) { + const stubPath = path.join(templatesDir, "AGENTS.codex-stub.md"); + if (!fs.existsSync(stubPath)) { + throw new Error(`缺少 Codex 指针模板:${stubPath}`); + } + return fs.readFileSync(stubPath, "utf8"); +} + +module.exports = { + buildCodexAgentsMd, + buildCodexAgentsStubMd, + renderProjectConfigBlock, +}; diff --git a/packages/core/lib/developerId.js b/packages/core/lib/developerId.js new file mode 100644 index 0000000..03fb274 --- /dev/null +++ b/packages/core/lib/developerId.js @@ -0,0 +1,260 @@ +/** + * 多人协作:developerId 解析与任务根路径。 + * + * 优先级(已定口径,勿再加 env/local 层): + * 1. flow2spec.config.json → collaboration.developerId + * - 非空但 sanitize 后为空(如纯中文、纯符号):抛错,让用户显式修正配置。 + * - 显式配置视为「用户明确表达了隔离意图」,不做静默降级。 + * 2. git user.email(@ 前)或 user.name,规范化 + * - 若规范化失败(如纯中文邮箱前缀 / 用户名),走 hash 兜底: + * 基于原始字符串 sha256 前 8 位生成 `dev-xxxxxxxx`,同时在 warnings 中提示。 + * 这样避免中文用户被静默塞回 legacy 单根。 + * 3. 都没有 → null(调用方使用 legacy `.task/` 根) + * + * collaboration.enabled === false 时强制 legacy(返回 null)。 + * + * 需要「是否隔离 / 是否 legacy」语义的调用方**请用 resolveDeveloperContext**; + * taskRootFor 只做拼路径,不看 enabled 开关,仅供内部/外部工具在已确定 id 的 + * 情形下拼路径。 + */ + +const { execFileSync } = require("child_process"); +const crypto = require("crypto"); +const path = require("path"); + +const TASK_DIR = ".task"; +const HASH_FALLBACK_PREFIX = "dev-"; + +/** + * @param {string} raw + * @returns {string|null} sanitize 后的 id;非法则 null + */ +function sanitizeDeveloperId(raw) { + if (raw == null) return null; + let s = String(raw).trim().toLowerCase(); + if (!s) return null; + // email → local part + if (s.includes("@")) { + s = s.split("@")[0] || ""; + } + s = s + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") + .replace(/-{2,}/g, "-"); + if (s.length < 1 || s.length > 64) return null; + return s; +} + +/** + * 基于原始字符串生成稳定的 hash 兜底 id,例如 `dev-a1b2c3d4`。 + * 用于 git identity 存在但 sanitize 失败(如纯中文)的情况, + * 保证隔离仍然按人生效、跨机器一致。 + * @param {string} raw + * @returns {string|null} + */ +function hashDeveloperId(raw) { + if (raw == null) return null; + const trimmed = String(raw).trim(); + if (!trimmed) return null; + const digest = crypto.createHash("sha256").update(trimmed).digest("hex"); + return `${HASH_FALLBACK_PREFIX}${digest.slice(0, 8)}`; +} + +/** + * @param {string} [cwd] + * @returns {{ email: string|null, name: string|null }} + */ +function readGitIdentity(cwd) { + const opts = { + encoding: "utf8", + timeout: 3000, + stdio: ["ignore", "pipe", "ignore"], + cwd: cwd || process.cwd(), + }; + let email = null; + let name = null; + try { + email = execFileSync("git", ["config", "user.email"], opts).trim() || null; + } catch { + email = null; + } + try { + name = execFileSync("git", ["config", "user.name"], opts).trim() || null; + } catch { + name = null; + } + return { email, name }; +} + +/** + * @param {object} config loadFlow2specConfig 的返回值 + * @param {object} [options] + * @param {string} [options.cwd] + * @param {{ email?: string|null, name?: string|null }} [options.gitIdentity] 测试注入 + * @param {boolean} [options.skipGit] + * @returns {{ + * developerId: string|null, + * source: 'config'|'git-email'|'git-name'|'git-email-hash'|'git-name-hash'|'legacy', + * legacy: boolean, + * taskRoot: string, + * enabled: boolean, + * warnings: string[], + * }} + * @throws {Error} 当 collaboration.developerId 非空但 sanitize 后为空时。 + */ +function resolveDeveloperContext(config, options = {}) { + const cwd = options.cwd || process.cwd(); + const collab = + config && config.collaboration && typeof config.collaboration === "object" + ? config.collaboration + : {}; + const enabled = collab.enabled !== false; // 缺省 true:有 id 就隔离;无 id 仍 legacy + + if (!enabled) { + return { + developerId: null, + source: "legacy", + legacy: true, + taskRoot: TASK_DIR, + enabled: false, + warnings: [], + }; + } + + const warnings = []; + + // 1) 显式 config:非空但非法 → 抛错,防止静默降级 + const rawConfigId = + typeof collab.developerId === "string" ? collab.developerId.trim() : ""; + if (rawConfigId) { + const fromConfig = sanitizeDeveloperId(rawConfigId); + if (!fromConfig) { + throw new Error( + `flow2spec.config.json → collaboration.developerId "${rawConfigId}" 无法规范化为 [a-z0-9-]。` + + `请改用英文/数字标识(如 "alice"),或留空让 Flow2Spec 从 git 身份推断。`, + ); + } + return { + developerId: fromConfig, + source: "config", + legacy: false, + taskRoot: path.posix.join(TASK_DIR, fromConfig), + enabled: true, + warnings, + }; + } + + // 2) git identity:先直接 sanitize,失败则 hash 兜底并 warn + const git = + options.gitIdentity || + (options.skipGit ? { email: null, name: null } : readGitIdentity(cwd)); + + if (git.email) { + const fromEmail = sanitizeDeveloperId(git.email); + if (fromEmail) { + return { + developerId: fromEmail, + source: "git-email", + legacy: false, + taskRoot: path.posix.join(TASK_DIR, fromEmail), + enabled: true, + warnings, + }; + } + const hashed = hashDeveloperId(git.email); + if (hashed) { + warnings.push( + `git user.email "${git.email}" 无法直接规范化,已回退到 hash id "${hashed}"。` + + `建议在 flow2spec.config.json 显式配置 collaboration.developerId 以获得可读的目录名。`, + ); + return { + developerId: hashed, + source: "git-email-hash", + legacy: false, + taskRoot: path.posix.join(TASK_DIR, hashed), + enabled: true, + warnings, + }; + } + } + + if (git.name) { + const fromName = sanitizeDeveloperId(git.name); + if (fromName) { + return { + developerId: fromName, + source: "git-name", + legacy: false, + taskRoot: path.posix.join(TASK_DIR, fromName), + enabled: true, + warnings, + }; + } + const hashed = hashDeveloperId(git.name); + if (hashed) { + warnings.push( + `git user.name "${git.name}" 无法直接规范化,已回退到 hash id "${hashed}"。` + + `建议在 flow2spec.config.json 显式配置 collaboration.developerId 以获得可读的目录名。`, + ); + return { + developerId: hashed, + source: "git-name-hash", + legacy: false, + taskRoot: path.posix.join(TASK_DIR, hashed), + enabled: true, + warnings, + }; + } + } + + return { + developerId: null, + source: "legacy", + legacy: true, + taskRoot: TASK_DIR, + enabled: true, + warnings, + }; +} + +/** + * 仅用于「已确定 id」时的路径拼接。**不检查 collaboration.enabled**; + * 需要开关语义的调用方请用 resolveDeveloperContext。 + * @param {string|null|undefined} developerId + * @returns {string} posix 风格相对路径,如 `.task` 或 `.task/alice` + */ +function taskRootFor(developerId) { + const id = sanitizeDeveloperId(developerId); + if (!id) return TASK_DIR; + return path.posix.join(TASK_DIR, id); +} + +function todoJsonPath(taskRoot) { + return path.posix.join(taskRoot || TASK_DIR, "todo.json"); +} + +function activeTaskDir(taskRoot, taskName) { + return path.posix.join(taskRoot || TASK_DIR, "active", taskName); +} + +function completedTaskDir(taskRoot, taskName, yyyymmdd) { + const date = yyyymmdd || "YYYYMMDD"; + return path.posix.join( + taskRoot || TASK_DIR, + "completed", + `${date}-${taskName}`, + ); +} + +module.exports = { + TASK_DIR, + HASH_FALLBACK_PREFIX, + sanitizeDeveloperId, + hashDeveloperId, + readGitIdentity, + resolveDeveloperContext, + taskRootFor, + todoJsonPath, + activeTaskDir, + completedTaskDir, +}; diff --git a/packages/core/lib/doctor.js b/packages/core/lib/doctor.js new file mode 100644 index 0000000..b833ecd --- /dev/null +++ b/packages/core/lib/doctor.js @@ -0,0 +1,348 @@ +const fs = require("fs"); +const path = require("path"); + +const { AGENTS } = require("./agents"); +const { + loadFlow2specConfig, + CONFIG_FILENAME, +} = require("./flow2specConfig"); +const { resolveDeveloperContext } = require("./developerId"); +const knowledgeEngine = require("./knowledgeEngine"); + +const STATUS = { + pass: "pass", + warning: "warning", + error: "error", +}; + +function numericVersion(version) { + return String(version || "") + .replace(/^v/, "") + .split(".") + .slice(0, 3) + .map((part) => Number.parseInt(part, 10) || 0); +} + +function compareVersions(left, right) { + const a = numericVersion(left); + const b = numericVersion(right); + for (let index = 0; index < 3; index += 1) { + const difference = (a[index] || 0) - (b[index] || 0); + if (difference !== 0) return difference; + } + return 0; +} + +function satisfiesNodeEngine(version, engine) { + const minimum = String(engine || "").match(/>=\s*v?(\d+(?:\.\d+){0,2})/); + if (!minimum) return true; + return compareVersions(version, minimum[1]) >= 0; +} + +function makeCheck(id, label, status, message, repair = null, details) { + const check = { id, label, status, message, repair }; + if (details !== undefined) check.details = details; + return check; +} + +function checkKnowledge(cwd) { + try { + const graph = knowledgeEngine.loadKnowledgeGraph(cwd); + const validation = knowledgeEngine.validateKnowledgeGraph(graph, { + strictRevision: true, + }); + const normalized = knowledgeEngine.normalizeRoutingWithGraph(graph); + const routingDrift = + normalized.changed || + knowledgeEngine.stableStringify(normalized.routing) !== + knowledgeEngine.stableStringify(graph.routing); + const details = { + topicCount: validation.topicCount, + issues: validation.issues, + warnings: validation.warnings, + routingDrift, + }; + + if (!validation.ok || routingDrift) { + const reasons = [...validation.issues]; + if (routingDrift) reasons.push("routing metadata differs from topic frontmatter"); + return makeCheck( + "knowledge", + "知识库", + STATUS.error, + `知识图存在 ${reasons.length} 个问题。`, + "运行 flow2spec kb build --fix-topics,再运行 flow2spec kb check --strict。", + details, + ); + } + if (validation.warnings.length > 0) { + return makeCheck( + "knowledge", + "知识库", + STATUS.warning, + `知识图可用,但有 ${validation.warnings.length} 条警告。`, + "运行 flow2spec kb check --strict 查看详情。", + details, + ); + } + return makeCheck( + "knowledge", + "知识库", + STATUS.pass, + `${validation.topicCount} 个 topic 校验通过,routing 无漂移。`, + null, + details, + ); + } catch (error) { + return makeCheck( + "knowledge", + "知识库", + STATUS.error, + error.message || String(error), + "确认 .Knowledge/manifest-routing.json 与其引用的 topic、matcher 均存在且为有效格式。", + ); + } +} + +function isIgnoredByRootGitignore(cwd, entry) { + const gitignore = path.join(cwd, ".gitignore"); + if (!fs.existsSync(gitignore)) return false; + const lines = fs + .readFileSync(gitignore, "utf8") + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith("#")); + return lines.includes(entry) || lines.includes(entry.replace(/\/$/, "")); +} + +function runDoctor(cwd = process.cwd(), options = {}) { + const pkg = options.package || require("../package.json"); + const nodeVersion = options.nodeVersion || process.version; + const knowledgeCheck = options.knowledgeCheck || checkKnowledge; + const checks = []; + + const engine = pkg.engines?.node || ""; + const runtimeOk = satisfiesNodeEngine(nodeVersion, engine); + checks.push( + makeCheck( + "runtime", + "Node.js", + runtimeOk ? STATUS.pass : STATUS.error, + runtimeOk + ? `${nodeVersion} 满足 ${engine || "包要求"}。` + : `${nodeVersion} 不满足 ${engine}。`, + runtimeOk ? null : `升级 Node.js 到满足 ${engine} 的版本。`, + { version: nodeVersion, required: engine }, + ), + ); + + const configPath = path.join(cwd, CONFIG_FILENAME); + let config = null; + if (!fs.existsSync(configPath)) { + checks.push( + makeCheck( + "config", + "项目配置", + STATUS.error, + `缺少 ${CONFIG_FILENAME}。`, + "在项目根运行 flow2spec init。", + ), + ); + } else { + try { + config = loadFlow2specConfig(cwd); + checks.push( + makeCheck( + "config", + "项目配置", + STATUS.pass, + `${CONFIG_FILENAME} 存在且可解析。`, + null, + { locale: config.locale }, + ), + ); + } catch (error) { + checks.push( + makeCheck( + "config", + "项目配置", + STATUS.error, + error.message || String(error), + `修正 ${CONFIG_FILENAME} 的 JSON 格式。`, + ), + ); + } + } + + const agentsPath = path.join(cwd, "AGENTS.md"); + checks.push( + fs.existsSync(agentsPath) + ? makeCheck("agents-entry", "项目入口", STATUS.pass, "根 AGENTS.md 已就绪。") + : makeCheck( + "agents-entry", + "项目入口", + STATUS.error, + "缺少根 AGENTS.md。", + "运行 flow2spec init codex 或 flow2spec init dsh,或重新初始化所需 Agent。", + ), + ); + + const manifestPath = path.join(cwd, ".Knowledge", "manifest-routing.json"); + checks.push( + fs.existsSync(manifestPath) + ? makeCheck( + "knowledge-entry", + "知识库入口", + STATUS.pass, + ".Knowledge/manifest-routing.json 已就绪。", + ) + : makeCheck( + "knowledge-entry", + "知识库入口", + STATUS.error, + "缺少 .Knowledge/manifest-routing.json。", + "在项目根运行 flow2spec init。", + ), + ); + + const requiredAgentFiles = { + codex: ["AGENTS.md", "hooks.json"], + dsh: ["AGENTS.md", "skills", "topics"], + claude: ["settings.json"], + cursor: ["hooks.json"], + }; + const initializedAgents = Object.entries(AGENTS).filter(([, agent]) => + fs.existsSync(path.join(cwd, agent.root)), + ); + if (initializedAgents.length === 0) { + checks.push( + makeCheck( + "agent-roots", + "Agent 配置", + STATUS.warning, + "未检测到 .codex、.claude、.cursor 或 .dsh 配置根。", + "运行 flow2spec init 初始化实际使用的 Agent。", + ), + ); + } else { + for (const [id, agent] of initializedAgents) { + const missing = (requiredAgentFiles[id] || []).filter( + (file) => !fs.existsSync(path.join(cwd, agent.root, file)), + ); + checks.push( + missing.length === 0 + ? makeCheck( + `agent-${id}`, + `${agent.label} 配置`, + STATUS.pass, + `${agent.root} 初始化文件完整。`, + ) + : makeCheck( + `agent-${id}`, + `${agent.label} 配置`, + STATUS.error, + `${agent.root} 缺少 ${missing.join("、")}。`, + `运行 flow2spec init ${id} 补齐配置。`, + { missing }, + ), + ); + } + } + + if (config) { + try { + const context = resolveDeveloperContext(config, { + cwd, + gitIdentity: options.gitIdentity, + skipGit: Boolean(options.gitIdentity), + }); + const warnings = [...context.warnings]; + if (context.legacy && context.enabled) { + warnings.push("未找到 developerId,将使用 legacy .task/ 根。"); + } + checks.push( + makeCheck( + "collaboration", + "协作上下文", + warnings.length > 0 ? STATUS.warning : STATUS.pass, + context.legacy + ? `使用 ${context.taskRoot}(${context.enabled ? "legacy" : "协作隔离已关闭"})。` + : `developerId=${context.developerId},TASK_ROOT=${context.taskRoot}。`, + warnings.length > 0 + ? "在 flow2spec.config.json 配置 collaboration.developerId。" + : null, + { ...context, warnings }, + ), + ); + } catch (error) { + checks.push( + makeCheck( + "collaboration", + "协作上下文", + STATUS.error, + error.message || String(error), + "修正 flow2spec.config.json 的 collaboration 配置。", + ), + ); + } + } + + const taskIgnored = isIgnoredByRootGitignore(cwd, ".task/"); + checks.push( + taskIgnored + ? makeCheck("task-ignore", "任务目录", STATUS.pass, ".task/ 已在根 .gitignore 中忽略。") + : makeCheck( + "task-ignore", + "任务目录", + STATUS.warning, + ".task/ 未在根 .gitignore 中忽略。", + "在根 .gitignore 中加入 .task/,或重新运行 flow2spec init。", + ), + ); + + checks.push(knowledgeCheck(cwd)); + + const summary = checks.reduce( + (result, check) => { + if (check.status === STATUS.pass) result.passed += 1; + if (check.status === STATUS.warning) result.warnings += 1; + if (check.status === STATUS.error) result.errors += 1; + return result; + }, + { passed: 0, warnings: 0, errors: 0 }, + ); + + return { + ok: summary.errors === 0, + package: { name: pkg.name, version: pkg.version }, + cwd: path.resolve(cwd), + summary, + checks, + }; +} + +function formatDoctorReport(report) { + const marker = { pass: "[PASS]", warning: "[WARN]", error: "[FAIL]" }; + const lines = [ + `Flow2Spec Doctor v${report.package.version}`, + `项目: ${report.cwd}`, + "", + ]; + for (const check of report.checks) { + lines.push(`${marker[check.status]} ${check.label}: ${check.message}`); + if (check.repair) lines.push(` 建议: ${check.repair}`); + } + lines.push( + "", + `结果: ${report.summary.passed} 通过,${report.summary.warnings} 警告,${report.summary.errors} 错误。`, + ); + return lines.join("\n"); +} + +module.exports = { + STATUS, + runDoctor, + formatDoctorReport, + satisfiesNodeEngine, + checkKnowledge, +}; diff --git a/packages/core/lib/dshAgentsAdapter.js b/packages/core/lib/dshAgentsAdapter.js new file mode 100644 index 0000000..09388fd --- /dev/null +++ b/packages/core/lib/dshAgentsAdapter.js @@ -0,0 +1,81 @@ +const fs = require("fs"); +const path = require("path"); +const { buildCodexAgentsMd } = require("./codexAgentsAdapter"); + +function replaceSection(body, startHeading, endHeading, replacement) { + const start = body.indexOf(startHeading); + const end = body.indexOf(endHeading, start + startHeading.length); + if (start < 0 || end < 0) return body; + return `${body.slice(0, start)}${replacement.trim()}\n\n${body.slice(end)}`; +} + +function buildDshAgentsMd(templatesDir, projectConfig) { + let body = buildCodexAgentsMd(templatesDir, projectConfig) + .replace(/Codex/g, "DeepSeek Harness") + .replace(/codex/g, "dsh"); + + const isEnglish = path.basename(path.dirname(templatesDir)) === "templates" && + path.basename(templatesDir) === "en-US"; + if (isEnglish) { + body = body + .replace(" **`./.dsh/AGENTS.md`** is only a pointer.", "") + .replace("\n**`.dsh/AGENTS.md`** is only a pointer and cannot replace root `AGENTS.md`.\n", "\n"); + body = replaceSection( + body, + "## DeepSeek Harness Hooks", + "## Flow2Spec Skills", + `## DeepSeek Harness Integration + +DeepSeek Harness loads the repository-root \`AGENTS.md\` and discovers project skills from \`./.dsh/skills/\`. Flow2Spec mirrors its long-form rules to \`./.dsh/topics/\` for on-demand reading. Native Cordis plugin integration is outside this initialization adapter.`, + ); + return body; + } + + body = body + .replace("**`./.dsh/AGENTS.md`** 仅为指针。", "") + .replace("- **`.dsh/AGENTS.md`** 仅为目录指针,不能替代根 `AGENTS.md`。\n", ""); + return replaceSection( + body, + "## DeepSeek Harness Hooks", + "## Flow2Spec 技能", + `## DeepSeek Harness 适配 + +DeepSeek Harness 会加载仓库根 \`AGENTS.md\`,并从 \`./.dsh/skills/\` 发现项目技能。Flow2Spec 将规则长文镜像到 \`./.dsh/topics/\` 供按需读取。原生 Cordis 插件集成不属于本初始化适配范围。`, + ); +} + +function buildDshAgentsStubMd(templatesDir) { + const isEnglish = path.basename(templatesDir) === "en-US"; + if (isEnglish) { + return `# Flow2Spec (\`.dsh/\` Directory Notes) + +DeepSeek Harness loads the complete project instructions from repository-root [\`AGENTS.md\`](../AGENTS.md). + +- \`skills/\`: Flow2Spec \`f2s-*\` skills discovered by DeepSeek Harness +- \`topics/\`: long-form rule mirrors loaded on demand +`; + } + return `# Flow2Spec(\`.dsh/\` 目录说明) + +DeepSeek Harness 从仓库根 [\`AGENTS.md\`](../AGENTS.md) 加载完整项目说明。 + +- \`skills/\`:DeepSeek Harness 可发现的 Flow2Spec \`f2s-*\` 技能 +- \`topics/\`:按需读取的规则长文镜像 +`; +} + +function writeDshAgentsStub(cwd, templatesDir) { + const dshRoot = path.join(cwd, ".dsh"); + fs.mkdirSync(dshRoot, { recursive: true }); + fs.writeFileSync( + path.join(dshRoot, "AGENTS.md"), + buildDshAgentsStubMd(templatesDir), + "utf8", + ); +} + +module.exports = { + buildDshAgentsMd, + buildDshAgentsStubMd, + writeDshAgentsStub, +}; diff --git a/packages/core/lib/flow2specConfig.js b/packages/core/lib/flow2specConfig.js new file mode 100644 index 0000000..bbe8bf0 --- /dev/null +++ b/packages/core/lib/flow2specConfig.js @@ -0,0 +1,317 @@ +const path = require("path"); +const fs = require("fs"); + +const CONFIG_FILENAME = "flow2spec.config.json"; +const DEFAULT_LOCALE = "zh-CN"; +const SUPPORTED_LOCALES = ["zh-CN", "en-US"]; + +const DEFAULTS = { + locale: DEFAULT_LOCALE, + subAgent: true, + // switchAgentVerification:false=落盘侧同会话内验;true+技能绑定=交叉验(子落盘主验/主落盘子验) + switchAgentVerification: true, + intentRecognition: true, + changeTracking: { + feat: true, + fix: false, + implement: true, + }, + updateCheck: { + enabled: true, + }, + // 多人协作:进度按 developerId 隔离到 .task//;缺省 enabled + // developerId 解析:config → git → legacy 单根 .task/(见 lib/developerId.js) + collaboration: { + enabled: true, + developerId: "", + }, +}; + +/** + * 所有已知配置字段描述,供 init 交互提示使用。 + * 新增字段在此追加,cli.js 会自动对缺失字段发起提问。 + * 支持点号分隔的嵌套键,如 "changeTracking.feat"(对应 { changeTracking: { feat: ... } })。 + */ +const CONFIG_FIELDS = [ + { + key: "locale", + type: "locale", + default: DEFAULT_LOCALE, + question: "选择 Flow2Spec 模板语言", + }, + { + key: "subAgent", + type: "boolean", + default: true, + question: "启用子 Agent 并行执行?(默认 Y,开启后小型任务仍可由主 agent 一气完成)", + }, + { + key: "switchAgentVerification", + type: "boolean", + default: true, + question: "启用交叉验证(子 agent 落盘 → 主 agent 验;需配合技能使用,默认 Y)", + }, + { + key: "intentRecognition", + type: "boolean", + default: true, + question: "启用意图识别自动分流(高置信操作意图自动进入对应 f2s-* 技能,默认 Y)?", + }, + { + key: "changeTracking.feat", + type: "boolean", + default: true, + question: "启用变更追踪 - f2s-kb-feat(新增能力时创建可续作的任务清单)?", + }, + { + key: "changeTracking.fix", + type: "boolean", + default: false, + question: "启用变更追踪 - f2s-kb-fix(修正能力时创建可续作的任务清单)?", + }, + { + key: "changeTracking.implement", + type: "boolean", + default: true, + question: "启用变更追踪 - f2s-implement-tech-design(实现技术方案时创建可续作的任务清单)?", + }, + { + key: "updateCheck.enabled", + type: "boolean", + default: true, + question: "启用每日版本更新提示(每天第一次 Agent 对话时检查是否有新版 flow2spec)?", + }, + { + key: "collaboration.enabled", + type: "boolean", + default: true, + question: + "启用多人协作进度隔离(.task//;关闭则始终用单人 .task/ 根路径)?", + }, +]; + +function normalizeBool(value, fallback) { + if (value === true || value === "true" || value === 1 || value === "1") + return true; + if (value === false || value === "false" || value === 0 || value === "0") + return false; + return fallback; +} + +function normalizeLocale(value, fallback = DEFAULT_LOCALE) { + const raw = String(value || "").trim(); + return SUPPORTED_LOCALES.includes(raw) ? raw : fallback; +} + +/** + * 读取点号分隔键对应的嵌套值,如 "changeTracking.feat" → raw.changeTracking?.feat + */ +function getNestedValue(obj, dottedKey) { + const parts = dottedKey.split("."); + let cur = obj; + for (const p of parts) { + if (!cur || typeof cur !== "object") return undefined; + cur = cur[p]; + } + return cur; +} + +/** + * 读取项目根 flow2spec.config.json,与 DEFAULTS 合并。 + * 文件不存在时返回默认副本(不自动创建文件)。 + * changeTracking 兼容旧版布尔值(true/false → 全部子项同值)。 + */ +function loadFlow2specConfig(cwd) { + const abs = path.join(cwd, CONFIG_FILENAME); + const out = { + ...DEFAULTS, + changeTracking: { ...DEFAULTS.changeTracking }, + updateCheck: { ...DEFAULTS.updateCheck }, + collaboration: { ...DEFAULTS.collaboration }, + }; + if (!fs.existsSync(abs)) { + return out; + } + let raw; + try { + raw = JSON.parse(fs.readFileSync(abs, "utf8")); + } catch (e) { + throw new Error( + `${CONFIG_FILENAME} JSON 解析失败:${e.message || String(e)}`, + ); + } + if (!raw || typeof raw !== "object" || Array.isArray(raw)) { + return out; + } + if (Object.prototype.hasOwnProperty.call(raw, "locale")) { + out.locale = normalizeLocale(raw.locale, DEFAULTS.locale); + } + if (Object.prototype.hasOwnProperty.call(raw, "subAgent")) { + out.subAgent = normalizeBool(raw.subAgent, DEFAULTS.subAgent); + } + if (Object.prototype.hasOwnProperty.call(raw, "switchAgentVerification")) { + out.switchAgentVerification = normalizeBool( + raw.switchAgentVerification, + DEFAULTS.switchAgentVerification, + ); + } else if (Object.prototype.hasOwnProperty.call(raw, "subAgentVerification")) { + // 旧键名,仍读取;新落盘请用 switchAgentVerification + out.switchAgentVerification = normalizeBool( + raw.subAgentVerification, + DEFAULTS.switchAgentVerification, + ); + } + if (Object.prototype.hasOwnProperty.call(raw, "intentRecognition")) { + out.intentRecognition = normalizeBool( + raw.intentRecognition, + DEFAULTS.intentRecognition, + ); + } + if (Object.prototype.hasOwnProperty.call(raw, "changeTracking")) { + const ct = raw.changeTracking; + if (typeof ct === "boolean") { + // 旧版布尔值:统一应用到全部子项 + out.changeTracking = { feat: ct, fix: ct, implement: ct }; + } else if (ct && typeof ct === "object" && !Array.isArray(ct)) { + out.changeTracking = { + feat: normalizeBool(ct.feat, DEFAULTS.changeTracking.feat), + fix: normalizeBool(ct.fix, DEFAULTS.changeTracking.fix), + implement: normalizeBool(ct.implement, DEFAULTS.changeTracking.implement), + }; + } + } + if (Object.prototype.hasOwnProperty.call(raw, "updateCheck")) { + const uc = raw.updateCheck; + if (uc && typeof uc === "object" && !Array.isArray(uc)) { + out.updateCheck = { + enabled: normalizeBool(uc.enabled, DEFAULTS.updateCheck.enabled), + }; + } + } + if (Object.prototype.hasOwnProperty.call(raw, "collaboration")) { + const collab = raw.collaboration; + if (collab && typeof collab === "object" && !Array.isArray(collab)) { + const idRaw = + collab.developerId == null ? "" : String(collab.developerId).trim(); + out.collaboration = { + enabled: normalizeBool(collab.enabled, DEFAULTS.collaboration.enabled), + developerId: idRaw, + }; + } + } + return out; +} + +/** + * 返回配置文件中尚未存在的字段列表(用于 init 时只提示新增字段)。 + * 文件不存在时返回全部字段。支持点号嵌套键。 + */ +function getMissingConfigFields(cwd) { + const abs = path.join(cwd, CONFIG_FILENAME); + if (!fs.existsSync(abs)) return CONFIG_FIELDS; + let raw; + try { + raw = JSON.parse(fs.readFileSync(abs, "utf8")); + } catch { + return []; + } + if (!raw || typeof raw !== "object" || Array.isArray(raw)) return CONFIG_FIELDS; + return CONFIG_FIELDS.filter((f) => { + const parts = f.key.split("."); + if (parts.length === 2) { + const parent = raw[parts[0]]; + // 旧版布尔值视为已配置,不再重复询问 + if (typeof parent === "boolean") return false; + return !parent || !Object.prototype.hasOwnProperty.call(parent, parts[1]); + } + return !Object.prototype.hasOwnProperty.call(raw, f.key); + }); +} + +/** + * 将点号嵌套键的 values 对象合并入 target,支持一层嵌套。 + * 例如 { "changeTracking.feat": true } → target.changeTracking.feat = true + */ +function mergeValues(target, values) { + const result = { ...target }; + for (const [key, val] of Object.entries(values)) { + const parts = key.split("."); + if (parts.length === 2) { + result[parts[0]] = { + ...(result[parts[0]] && typeof result[parts[0]] === "object" + ? result[parts[0]] + : {}), + [parts[1]]: val, + }; + } else { + result[key] = val; + } + } + return result; +} + +/** + * 若项目根不存在配置文件,则写入配置(优先用 values,其次包模板,再次 DEFAULTS)。 + * 已存在时:若 values 中有缺失字段,则补写这些字段;否则不覆盖。 + * @param {object} [options.values] 用户交互收集到的字段值,优先级高于模板文件 + */ +function ensureFlow2specProjectConfig(cwd, templatesDir, options = {}) { + const { overwrite = false, values } = options; + const dest = path.join(cwd, CONFIG_FILENAME); + const src = path.join(templatesDir, CONFIG_FILENAME); + + if (fs.existsSync(dest) && !overwrite) { + if (values && typeof values === "object" && Object.keys(values).length > 0) { + let existing; + try { + existing = JSON.parse(fs.readFileSync(dest, "utf8")); + } catch { + existing = {}; + } + const merged = mergeValues(existing, values); + if (JSON.stringify(merged) !== JSON.stringify(existing)) { + fs.writeFileSync(dest, `${JSON.stringify(merged, null, 2)}\n`, "utf8"); + return { created: false, updated: true, path: dest }; + } + } + return { created: false, path: dest }; + } + + let base; + if (fs.existsSync(src)) { + try { + base = JSON.parse(fs.readFileSync(src, "utf8")); + } catch { + base = { + ...DEFAULTS, + locale: DEFAULTS.locale, + changeTracking: { ...DEFAULTS.changeTracking }, + updateCheck: { ...DEFAULTS.updateCheck }, + collaboration: { ...DEFAULTS.collaboration }, + }; + } + } else { + base = { + ...DEFAULTS, + locale: DEFAULTS.locale, + changeTracking: { ...DEFAULTS.changeTracking }, + updateCheck: { ...DEFAULTS.updateCheck }, + collaboration: { ...DEFAULTS.collaboration }, + }; + } + const merged = values && typeof values === "object" ? mergeValues(base, values) : base; + fs.writeFileSync(dest, `${JSON.stringify(merged, null, 2)}\n`, "utf8"); + return { created: true, path: dest }; +} + +module.exports = { + CONFIG_FILENAME, + DEFAULT_LOCALE, + SUPPORTED_LOCALES, + DEFAULTS, + CONFIG_FIELDS, + normalizeLocale, + loadFlow2specConfig, + getMissingConfigFields, + ensureFlow2specProjectConfig, +}; diff --git a/packages/core/lib/init.js b/packages/core/lib/init.js new file mode 100644 index 0000000..23de9fe --- /dev/null +++ b/packages/core/lib/init.js @@ -0,0 +1,1306 @@ +const path = require("path"); +const fs = require("fs"); +const { + AGENTS, + KNOWLEDGE_ROOT, + KNOWLEDGE_SUBDIRS, + AGENT_SUBDIRS, + normalizeAgentIds, +} = require("./agents"); +const { + adaptRuleMdcToClaudeMd, + shouldWriteClaudeStyleRules, +} = require("./claudeRulesAdapter"); +const { + buildCodexAgentsMd, + buildCodexAgentsStubMd, +} = require("./codexAgentsAdapter"); +const { + buildDshAgentsMd, + writeDshAgentsStub, +} = require("./dshAgentsAdapter"); +const { + loadFlow2specConfig, + ensureFlow2specProjectConfig, + DEFAULT_LOCALE, + normalizeLocale, +} = require("./flow2specConfig"); +const { writeClaudeAgentHooks } = require("./claudeSettingsAdapter"); + +const KNOWLEDGE_TOPIC_TYPES = ["feature", "module", "config", "policy"]; +const KNOWLEDGE_TOPIC_CONFIDENCE = ["manual", "inferred"]; + +function ensureDir(dir) { + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); +} + +function ensureKnowledgeDirs(cwd) { + ensureDir(path.join(cwd, KNOWLEDGE_ROOT)); + for (const sub of KNOWLEDGE_SUBDIRS) { + ensureDir(path.join(cwd, KNOWLEDGE_ROOT, sub)); + } +} + +function removeKnowledgeUpdateCheckCache(cwd) { + const cachePath = path.join(cwd, KNOWLEDGE_ROOT, "update-check.json"); + if (fs.existsSync(cachePath)) { + fs.rmSync(cachePath, { force: true }); + } +} + +function ensureFlow2specGitignore(cwd) { + const gitignorePath = path.join(cwd, ".gitignore"); + const required = [".task/", ".Knowledge/update-check.json"]; + const existing = fs.existsSync(gitignorePath) + ? fs.readFileSync(gitignorePath, "utf8") + : ""; + const lines = existing.split(/\r?\n/).map((line) => line.trim()); + const missing = required.filter((item) => !lines.includes(item)); + if (missing.length === 0) { + return { path: gitignorePath, changed: false, added: [] }; + } + const additions = []; + if (!lines.includes("# Flow2Spec local state")) { + additions.push("# Flow2Spec local state"); + } + additions.push(...missing); + const prefix = existing && !existing.endsWith("\n") ? `${existing}\n` : existing; + const separator = prefix && !prefix.endsWith("\n\n") ? "\n" : ""; + fs.writeFileSync(gitignorePath, `${prefix}${separator}${additions.join("\n")}\n`, "utf8"); + return { path: gitignorePath, changed: true, added: missing }; +} + +function ensureAgentDirs(cwd, agentId) { + const root = AGENTS[agentId].root; + ensureDir(path.join(cwd, root)); + for (const sub of AGENT_SUBDIRS[agentId] || []) { + ensureDir(path.join(cwd, root, sub)); + } +} + +/** 递归复制目录或文件到目标,已存在则覆盖 */ +function copyRecursive(src, dest) { + const stat = fs.statSync(src); + if (stat.isDirectory()) { + if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true }); + for (const name of fs.readdirSync(src)) { + copyRecursive(path.join(src, name), path.join(dest, name)); + } + } else { + fs.copyFileSync(src, dest); + } +} + +/** 递归复制目录或文件,支持扩展名转换(.md <-> .mdc) */ +function copyRecursiveWithExtConversion(src, dest, shouldConvertToMdc) { + const stat = fs.statSync(src); + if (stat.isDirectory()) { + if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true }); + for (const name of fs.readdirSync(src)) { + let destName = name; + // 处理文件扩展名转换 + if (shouldConvertToMdc && name.endsWith(".md")) { + destName = name.replace(/\.md$/i, ".mdc"); + } + copyRecursiveWithExtConversion( + path.join(src, name), + path.join(dest, destName), + shouldConvertToMdc + ); + } + } else { + fs.copyFileSync(src, dest); + } +} + +function copyKnowledgeTemplates(cwd, templatesDir, options = {}) { + const { overwrite = false } = options; + const srcRoot = path.join(templatesDir, "knowledge"); + const destRoot = path.join(cwd, KNOWLEDGE_ROOT); + if (!fs.existsSync(srcRoot)) return; + const result = { written: 0, skipped: 0 }; + for (const name of fs.readdirSync(srcRoot)) { + if (name === "manifest-matchers.json") { + continue; + } + copyRecursivePreserve( + path.join(srcRoot, name), + path.join(destRoot, name), + overwrite, + result, + ); + } + return result; +} + +function copyRecursivePreserve(src, dest, overwrite, result) { + const stat = fs.statSync(src); + if (stat.isDirectory()) { + if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true }); + for (const name of fs.readdirSync(src)) { + copyRecursivePreserve( + path.join(src, name), + path.join(dest, name), + overwrite, + result, + ); + } + return; + } + if (!overwrite && fs.existsSync(dest)) { + result.skipped += 1; + return; + } + fs.copyFileSync(src, dest); + result.written += 1; +} + +function readJson(filePath) { + return JSON.parse(fs.readFileSync(filePath, "utf8")); +} + +function writeJson(filePath, data) { + fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8"); +} + +function findPackageJsonDir(startDir) { + let cur = startDir; + while (cur && cur !== path.dirname(cur)) { + if (fs.existsSync(path.join(cur, "package.json"))) return cur; + cur = path.dirname(cur); + } + return null; +} + +function readPackageName(templatesDir) { + try { + const packageDir = findPackageJsonDir(templatesDir); + return readJson(path.join(packageDir || path.join(templatesDir, ".."), "package.json")).name; + } catch (_) { + return "@double-coding/flow2spec"; + } +} + +function resolveTemplatesDir(templatesRoot, locale) { + const normalized = normalizeLocale(locale, DEFAULT_LOCALE); + const preferred = path.join(templatesRoot, normalized); + if (fs.existsSync(preferred)) { + return { templatesDir: preferred, locale: normalized }; + } + const fallback = path.join(templatesRoot, DEFAULT_LOCALE); + if (fs.existsSync(fallback)) { + return { templatesDir: fallback, locale: DEFAULT_LOCALE }; + } + return { templatesDir: templatesRoot, locale: DEFAULT_LOCALE }; +} + +function writeHookScriptWithPackageName(destDir, templatesDir, scriptName) { + const src = path.join(templatesDir, "hooks", scriptName); + if (!fs.existsSync(src)) return { written: false, reason: "missing-template" }; + ensureDir(destDir); + let body = fs.readFileSync(src, "utf8"); + body = body.replace(/__FLOW2SPEC_PACKAGE_NAME__/g, readPackageName(templatesDir)); + fs.writeFileSync(path.join(destDir, scriptName), body, "utf8"); + return { written: true }; +} + +function hasHookCommand(groups, fragment) { + if (!Array.isArray(groups)) return false; + return groups.some((group) => + Array.isArray(group?.hooks) && + group.hooks.some( + (hook) => + hook && + hook.type === "command" && + String(hook.command || "").includes(fragment), + ), + ); +} + +function mergeCodexUpdateCheckHook(existing) { + const next = + existing && typeof existing === "object" && !Array.isArray(existing) + ? JSON.parse(JSON.stringify(existing)) + : {}; + if (!next.hooks || typeof next.hooks !== "object" || Array.isArray(next.hooks)) { + next.hooks = {}; + } + if (!Array.isArray(next.hooks.SessionStart)) { + next.hooks.SessionStart = []; + } + if (hasHookCommand(next.hooks.SessionStart, "f2s-update-check")) { + return { config: next, changed: false }; + } + next.hooks.SessionStart.push({ + matcher: "startup|resume", + hooks: [ + { + type: "command", + command: "node .codex/hooks/f2s-update-check.js", + statusMessage: "Checking Flow2Spec knowledge version", + }, + ], + }); + return { config: next, changed: true }; +} + +function mergeCodexConfigSessionHook(existing) { + const next = + existing && typeof existing === "object" && !Array.isArray(existing) + ? JSON.parse(JSON.stringify(existing)) + : {}; + if (!next.hooks || typeof next.hooks !== "object" || Array.isArray(next.hooks)) { + next.hooks = {}; + } + if (!Array.isArray(next.hooks.SessionStart)) { + next.hooks.SessionStart = []; + } + if (hasHookCommand(next.hooks.SessionStart, "f2s-config-session")) { + return { config: next, changed: false }; + } + next.hooks.SessionStart.unshift({ + matcher: "startup|resume", + hooks: [ + { + type: "command", + command: "node .codex/hooks/f2s-config-session.js", + }, + ], + }); + return { config: next, changed: true }; +} + +function writeCodexUpdateCheckHook(cwd, templatesDir) { + const codexRoot = path.join(cwd, ".codex"); + const hooksDir = path.join(codexRoot, "hooks"); + const configSessionResult = writeHookScriptWithPackageName( + hooksDir, + templatesDir, + "f2s-config-session.js", + ); + const scriptResult = writeHookScriptWithPackageName( + hooksDir, + templatesDir, + "f2s-update-check.js", + ); + + const hooksJsonPath = path.join(codexRoot, "hooks.json"); + let existing = {}; + if (fs.existsSync(hooksJsonPath)) { + try { + existing = readJson(hooksJsonPath); + } catch (_) { + existing = {}; + } + } + const mergedConfigSession = mergeCodexConfigSessionHook(existing); + const mergedUpdateCheck = mergeCodexUpdateCheckHook(mergedConfigSession.config); + const config = mergedUpdateCheck.config; + const changed = mergedConfigSession.changed || mergedUpdateCheck.changed; + if (changed || !fs.existsSync(hooksJsonPath)) { + writeJson(hooksJsonPath, config); + } + return { configSessionResult, scriptResult, hooksJsonChanged: changed }; +} + +function mergeCursorUpdateCheckHook(existing) { + const next = + existing && typeof existing === "object" && !Array.isArray(existing) + ? JSON.parse(JSON.stringify(existing)) + : {}; + next.version = Number.isFinite(Number(next.version)) ? Number(next.version) : 1; + if (!next.hooks || typeof next.hooks !== "object" || Array.isArray(next.hooks)) { + next.hooks = {}; + } + if (!Array.isArray(next.hooks.sessionStart)) { + next.hooks.sessionStart = []; + } + const exists = next.hooks.sessionStart.some((hook) => + hook && String(hook.command || "").includes("f2s-update-check"), + ); + if (exists) return { config: next, changed: false }; + next.hooks.sessionStart.push({ + command: "node .cursor/hooks/f2s-update-check.js", + timeout: 10, + }); + return { config: next, changed: true }; +} + +function writeCursorUpdateCheckHook(cwd, templatesDir) { + const cursorRoot = path.join(cwd, ".cursor"); + const hooksDir = path.join(cursorRoot, "hooks"); + const scriptResult = writeHookScriptWithPackageName( + hooksDir, + templatesDir, + "f2s-update-check.js", + ); + + const hooksJsonPath = path.join(cursorRoot, "hooks.json"); + let existing = {}; + if (fs.existsSync(hooksJsonPath)) { + try { + existing = readJson(hooksJsonPath); + } catch (_) { + existing = {}; + } + } + const { config, changed } = mergeCursorUpdateCheckHook(existing); + if (changed || !fs.existsSync(hooksJsonPath)) { + writeJson(hooksJsonPath, config); + } + return { scriptResult, hooksJsonChanged: changed }; +} + +function buildDefaultMatcherPath(matcherId) { + return `${KNOWLEDGE_ROOT}/matchers/${matcherId}.json`; +} + +function normalizeMatcherShardData(raw, matcherId) { + const safeRaw = + raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {}; + return { + ...safeRaw, + id: matcherId, + includeAny: dedupeStringArray(safeRaw.includeAny || []), + }; +} + +function dedupeStringArray(values) { + const out = []; + const seen = new Set(); + for (const item of values || []) { + if (typeof item !== "string") continue; + if (seen.has(item)) continue; + seen.add(item); + out.push(item); + } + return out; +} + +function unionByKey(templateList, existingList, key, mergeItem) { + const existingMap = new Map(); + for (const item of existingList || []) { + if (!item || typeof item !== "object") continue; + if (!item[key] || typeof item[key] !== "string") continue; + existingMap.set(item[key], item); + } + + const out = []; + const orderedKeys = []; + + for (const item of templateList || []) { + if (!item || typeof item !== "object") continue; + const id = item[key]; + if (!id || typeof id !== "string") continue; + orderedKeys.push(id); + const existing = existingMap.get(id); + out.push(mergeItem(item, existing)); + } + + for (const item of existingList || []) { + if (!item || typeof item !== "object") continue; + const id = item[key]; + if (!id || typeof id !== "string") continue; + if (orderedKeys.includes(id)) continue; + out.push(item); + } + + return out; +} + +function mergeTopicDependencies(templateDeps, existingDeps) { + const out = {}; + const keys = new Set([ + ...Object.keys(templateDeps || {}), + ...Object.keys(existingDeps || {}), + ]); + for (const key of keys) { + out[key] = dedupeStringArray([ + ...(templateDeps?.[key] || []), + ...(existingDeps?.[key] || []), + ]); + } + return out; +} + +function normalizeTopicMetadataEntry(entry) { + if (!entry || typeof entry !== "object" || Array.isArray(entry)) return null; + if (!KNOWLEDGE_TOPIC_TYPES.includes(entry.primary)) return null; + const confidence = + typeof entry.confidence === "string" && + KNOWLEDGE_TOPIC_CONFIDENCE.includes(entry.confidence) + ? entry.confidence + : null; + if (!confidence) return null; + const result = { primary: entry.primary, confidence }; + if (Array.isArray(entry.tags) && entry.tags.length > 0) { + const validTags = dedupeStringArray(entry.tags).filter( + (t) => + KNOWLEDGE_TOPIC_TYPES.includes(t) && + t !== entry.primary, + ); + if (validTags.length > 0) result.tags = validTags; + } + return result; +} + +function mergeTopicMetadata(templateMetadata, existingMetadata, topicPaths) { + const out = {}; + const topicIds = new Set(Object.keys(topicPaths || {})); + // existingMetadata 先写,templateMetadata 后写覆盖——模板优先 + for (const metadata of [existingMetadata, templateMetadata]) { + if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) { + continue; + } + for (const [topicId, entry] of Object.entries(metadata)) { + if (!topicIds.has(topicId)) continue; + const normalized = normalizeTopicMetadataEntry(entry); + if (!normalized) continue; + out[topicId] = normalized; + } + } + return out; +} + +function buildMergedRouting(templateRouting, existingRouting, pkgVersion, isFirstInit = false) { + const mergedTaskRules = unionByKey( + templateRouting.taskToTopicRules, + existingRouting.taskToTopicRules, + "task", + (templateRule, existingRule) => { + if (!existingRule) return templateRule; + const mergedMatcherId = existingRule.matcherId || templateRule.matcherId; + return { + ...templateRule, + ...existingRule, + matcherId: mergedMatcherId, + matcherPath: + existingRule.matcherPath || + templateRule.matcherPath || + (mergedMatcherId ? buildDefaultMatcherPath(mergedMatcherId) : null), + topics: dedupeStringArray([ + ...(templateRule.topics || []), + ...(existingRule.topics || []), + ]), + }; + }, + ); + + const knownMerged = { + version: pkgVersion || templateRouting.version || existingRouting.version, + knowledgeRoot: + existingRouting.knowledgeRoot || templateRouting.knowledgeRoot, + generatedFrom: + existingRouting.generatedFrom || templateRouting.generatedFrom, + matcherKey: + existingRouting.matcherKey || templateRouting.matcherKey || "matcherId", + sourceOfTruth: + existingRouting.sourceOfTruth || + templateRouting.sourceOfTruth || + `${KNOWLEDGE_ROOT}/manifest-routing.json`, + fallbackTopic: + existingRouting.fallbackTopic || templateRouting.fallbackTopic, + topicDependencies: mergeTopicDependencies( + templateRouting.topicDependencies, + existingRouting.topicDependencies, + ), + topicPaths: { + ...(templateRouting.topicPaths || {}), + ...(existingRouting.topicPaths || {}), + }, + taskToTopicRules: mergedTaskRules, + }; + // projectRev:本项目已基线对齐到的包模板修订号(由 f2s-kb-upgrade 完整流程跑完 3a/3b 后写入)。 + // init 行为: + // - 首次初始化(项目侧 manifest-routing.json 此前不存在):按模板写入,等同首次落地即视为已对齐。 + // - 已存在 manifest-routing.json:不写、不覆盖项目侧值;由 f2s-kb-upgrade 在完整流程末尾改写。 + if (isFirstInit) { + if (Object.prototype.hasOwnProperty.call(templateRouting, "projectRev")) { + knownMerged.projectRev = templateRouting.projectRev; + } + } else if ( + Object.prototype.hasOwnProperty.call(existingRouting, "projectRev") + ) { + knownMerged.projectRev = existingRouting.projectRev; + } + // pkgRev:本次 init 用的包模板修订号(= 包侧 projectRev 快照),供 f2s-kb-upgrade 步骤 2c 取用。 + // 每次 init 都按当前包模板覆盖(不沿用项目原值)。包模板未声明该字段 → 删除项目侧旧值,让 SKILL 走 null 兜底。 + if ( + Object.prototype.hasOwnProperty.call(templateRouting, "projectRev") && + typeof templateRouting.projectRev === "number" && + Number.isFinite(templateRouting.projectRev) + ) { + knownMerged.pkgRev = templateRouting.projectRev; + } + const mergedTopicMetadata = mergeTopicMetadata( + templateRouting.topicMetadata, + existingRouting.topicMetadata, + knownMerged.topicPaths, + ); + if (Object.keys(mergedTopicMetadata).length > 0) { + knownMerged.topicMetadata = mergedTopicMetadata; + } + + const knownKeys = new Set(Object.keys(knownMerged)); + const extras = {}; + for (const [key, value] of Object.entries(existingRouting || {})) { + if (knownKeys.has(key)) continue; + extras[key] = value; + } + + const merged = { + ...knownMerged, + ...extras, + }; + delete merged.matchersFile; + return merged; +} + +function buildMergedMatchers(templateMatchers, existingMatchers) { + const templateMap = templateMatchers.matchers || {}; + const existingMap = existingMatchers.matchers || {}; + const allMatcherIds = new Set([ + ...Object.keys(templateMap), + ...Object.keys(existingMap), + ]); + const mergedMatchers = {}; + for (const matcherId of allMatcherIds) { + const templateItem = templateMap[matcherId] || {}; + const existingItem = existingMap[matcherId] || {}; + mergedMatchers[matcherId] = { + ...templateItem, + ...existingItem, + includeAny: dedupeStringArray([ + ...(templateItem.includeAny || []), + ...(existingItem.includeAny || []), + ]), + }; + } + + const knownMerged = { + version: templateMatchers.version || existingMatchers.version, + generatedFrom: + existingMatchers.generatedFrom || templateMatchers.generatedFrom, + matcherKey: + existingMatchers.matcherKey || templateMatchers.matcherKey || "matcherId", + sourceOfTruth: + existingMatchers.sourceOfTruth || + templateMatchers.sourceOfTruth || + `${KNOWLEDGE_ROOT}/manifest-routing.json`, + matchers: mergedMatchers, + }; + + const knownKeys = new Set(Object.keys(knownMerged)); + const extras = {}; + for (const [key, value] of Object.entries(existingMatchers || {})) { + if (knownKeys.has(key)) continue; + extras[key] = value; + } + + return { + ...knownMerged, + ...extras, + }; +} + +function ensureRoutingMatcherPaths(routing) { + const rules = Array.isArray(routing.taskToTopicRules) + ? routing.taskToTopicRules + : []; + let changed = false; + const nextRules = rules.map((rule) => { + if (!rule || typeof rule !== "object") return rule; + if (!rule.matcherId || typeof rule.matcherId !== "string") return rule; + if (rule.matcherPath && typeof rule.matcherPath === "string") return rule; + changed = true; + return { + ...rule, + matcherPath: buildDefaultMatcherPath(rule.matcherId), + }; + }); + if (!changed) return { routing, changed }; + return { + routing: { + ...routing, + taskToTopicRules: nextRules, + }, + changed, + }; +} + +function buildMatcherIdToPathMap(routing) { + const out = new Map(); + const rules = Array.isArray(routing.taskToTopicRules) + ? routing.taskToTopicRules + : []; + for (const rule of rules) { + if (!rule || typeof rule !== "object") continue; + if (!rule.matcherId || typeof rule.matcherId !== "string") continue; + const matcherPath = + rule.matcherPath && typeof rule.matcherPath === "string" + ? rule.matcherPath + : buildDefaultMatcherPath(rule.matcherId); + if (!out.has(rule.matcherId)) { + out.set(rule.matcherId, matcherPath); + } + } + return out; +} + +function ensureMatcherShards(cwd, routing, mergedMatchers) { + const matcherIdToPath = buildMatcherIdToPathMap(routing); + const matcherMap = + mergedMatchers?.matchers && typeof mergedMatchers.matchers === "object" + ? mergedMatchers.matchers + : {}; + + for (const matcherId of Object.keys(matcherMap)) { + if (!matcherIdToPath.has(matcherId)) { + matcherIdToPath.set(matcherId, buildDefaultMatcherPath(matcherId)); + } + } + + let changed = false; + let writtenCount = 0; + for (const [matcherId, matcherPath] of matcherIdToPath.entries()) { + const matcherAbs = resolveFromCwd(cwd, matcherPath); + ensureDir(path.dirname(matcherAbs)); + + const compatMatcher = matcherMap[matcherId]; + const existingShard = fs.existsSync(matcherAbs) ? readJson(matcherAbs) : {}; + const nextShard = normalizeMatcherShardData( + { + ...(compatMatcher && typeof compatMatcher === "object" + ? compatMatcher + : {}), + ...(existingShard && typeof existingShard === "object" + ? existingShard + : {}), + }, + matcherId, + ); + + const prevRaw = fs.existsSync(matcherAbs) + ? JSON.stringify(existingShard) + : null; + const nextRaw = JSON.stringify(nextShard); + if (prevRaw === nextRaw) continue; + + writeJson(matcherAbs, nextShard); + writtenCount += 1; + changed = true; + } + + return { changed, writtenCount }; +} + +/** + * 把「本次 init 用的包模板 projectRev」写入项目侧 `.Knowledge/manifest-routing.json` 的 `pkgRev` 顶层字段, + * 供 `f2s-kb-upgrade` 步骤 2c 取用。在 reset 与 incremental 两条路径之后无条件调用一次。 + * + * @param {string} cwd 项目根 + * @param {string} templatesDir 当前 locale 模板根 + */ +function finalizePkgRev(cwd, templatesDir) { + const routingPath = path.join(cwd, KNOWLEDGE_ROOT, "manifest-routing.json"); + const templateRoutingPath = path.join( + templatesDir, + "knowledge", + "manifest-routing.json", + ); + if (!fs.existsSync(routingPath) || !fs.existsSync(templateRoutingPath)) { + return { written: false, pkgRev: null }; + } + let templateRev = null; + try { + const tpl = readJson(templateRoutingPath); + if ( + Object.prototype.hasOwnProperty.call(tpl, "projectRev") && + typeof tpl.projectRev === "number" && + Number.isFinite(tpl.projectRev) + ) { + templateRev = tpl.projectRev; + } + } catch { + // 模板读不到则不写;保留项目侧旧值(如已有) + return { written: false, pkgRev: null }; + } + let routing; + try { + routing = readJson(routingPath); + } catch { + return { written: false, pkgRev: null }; + } + // 顺便用本包版本号覆盖 manifest.version——reset 路径直接 cp 模板会留下模板里的占位版本号 + let changed = false; + try { + const pkgJsonPath = path.join(__dirname, "..", "package.json"); + if (fs.existsSync(pkgJsonPath)) { + const pkgVersion = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8")).version; + if (typeof pkgVersion === "string" && routing.version !== pkgVersion) { + routing.version = pkgVersion; + changed = true; + } + } + } catch { + // 读不到 package.json 不影响主流程 + } + if (templateRev !== null) { + if (routing.pkgRev !== templateRev) { + routing.pkgRev = templateRev; + changed = true; + } + } else if ( + Object.prototype.hasOwnProperty.call(routing, "pkgRev") + ) { + // 包模板未声明 projectRev 时,清掉项目侧的陈旧 pkgRev,让 SKILL 走 null 兜底 + delete routing.pkgRev; + changed = true; + } + if (!changed) { + return { written: false, pkgRev: templateRev }; + } + writeJson(routingPath, routing); + return { written: true, pkgRev: templateRev }; +} + +function upgradeKnowledgeRoutingAndMatchers(cwd, templatesDir, options = {}) { + const { overwrite = false } = options; + if (overwrite) { + return { + upgraded: false, + reason: "overwrite", + }; + } + + const templateRoutingPath = path.join( + templatesDir, + "knowledge", + "manifest-routing.json", + ); + const templateMatchersPath = path.join( + templatesDir, + "knowledge", + "manifest-matchers.json", + ); + if ( + !fs.existsSync(templateRoutingPath) || + !fs.existsSync(templateMatchersPath) + ) { + return { + upgraded: false, + reason: "missing-routing-templates", + }; + } + + const routingPath = path.join(cwd, KNOWLEDGE_ROOT, "manifest-routing.json"); + const matchersPath = path.join(cwd, KNOWLEDGE_ROOT, "manifest-matchers.json"); + + const templateRouting = readJson(templateRoutingPath); + const templateMatchers = readJson(templateMatchersPath); + const hadRouting = fs.existsSync(routingPath); + const hadMatchers = fs.existsSync(matchersPath); + const existingRouting = hadRouting ? readJson(routingPath) : {}; + const existingMatchers = hadMatchers ? readJson(matchersPath) : {}; + + // 读包版本号,用于写入 manifest-routing.json 的 version 字段 + let pkgVersion; + try { + const packageDir = findPackageJsonDir(templatesDir); + pkgVersion = readJson(path.join(packageDir || path.join(templatesDir, ".."), "package.json")).version; + } catch (_) {} + + const mergedRouting = buildMergedRouting(templateRouting, existingRouting, pkgVersion, !hadRouting); + const mergedMatchers = buildMergedMatchers( + templateMatchers, + existingMatchers, + ); + const { + routing: mergedRoutingWithMatcherPath, + changed: matcherPathBackfilled, + } = ensureRoutingMatcherPaths(mergedRouting); + const matcherShardUpgrade = ensureMatcherShards( + cwd, + mergedRoutingWithMatcherPath, + mergedMatchers, + ); + + const oldRoutingRaw = JSON.stringify(existingRouting); + const newRoutingRaw = JSON.stringify(mergedRoutingWithMatcherPath); + const oldMatchersRaw = JSON.stringify(existingMatchers); + const newMatchersRaw = JSON.stringify(mergedMatchers); + + if (!hadRouting || oldRoutingRaw !== newRoutingRaw) { + writeJson(routingPath, mergedRoutingWithMatcherPath); + } + + const routingChanged = !hadRouting || oldRoutingRaw !== newRoutingRaw; + const legacyAggregateDiffers = + hadMatchers && oldMatchersRaw !== newMatchersRaw; + let legacyMatchersFileRemoved = false; + if (fs.existsSync(matchersPath)) { + try { + fs.unlinkSync(matchersPath); + legacyMatchersFileRemoved = true; + } catch (e) { + /* 保留文件时由下次 init 重试 */ + } + } + const upgraded = + routingChanged || + legacyAggregateDiffers || + matcherShardUpgrade.changed || + legacyMatchersFileRemoved; + + return { + upgraded, + reason: upgraded ? "merged" : "up-to-date", + routingChanged, + legacyAggregateDiffers, + legacyMatchersFileRemoved, + matcherPathBackfilled, + matcherShardChanged: matcherShardUpgrade.changed, + matcherShardWritten: matcherShardUpgrade.writtenCount, + }; +} + +function resolveFromCwd(cwd, maybeRelativePath) { + return path.isAbsolute(maybeRelativePath) + ? maybeRelativePath + : path.join(cwd, maybeRelativePath); +} + +function validateKnowledgeRouting(cwd) { + const routingPath = path.join(cwd, KNOWLEDGE_ROOT, "manifest-routing.json"); + const matchersPath = path.join(cwd, KNOWLEDGE_ROOT, "manifest-matchers.json"); + if (!fs.existsSync(routingPath)) { + throw new Error( + `缺少知识库路由清单:${path.join(KNOWLEDGE_ROOT, "manifest-routing.json")}`, + ); + } + let routing; + let matcherData = null; + try { + routing = JSON.parse(fs.readFileSync(routingPath, "utf8")); + } catch (e) { + throw new Error(`路由清单 JSON 解析失败:${routingPath}`); + } + if (fs.existsSync(matchersPath)) { + try { + matcherData = JSON.parse(fs.readFileSync(matchersPath, "utf8")); + } catch (e) { + throw new Error(`匹配清单 JSON 解析失败:${matchersPath}`); + } + } + + if (!routing.topicPaths || typeof routing.topicPaths !== "object") { + throw new Error("路由清单缺少 topicPaths,无法执行主题路由。"); + } + + const topicIds = new Set(Object.keys(routing.topicPaths)); + if (topicIds.size === 0) { + throw new Error("路由清单 topicPaths 为空,无法执行主题路由。"); + } + + for (const [topicId, topicPath] of Object.entries(routing.topicPaths)) { + if (!topicId || typeof topicId !== "string") { + throw new Error("topicPaths 中存在非法 topicId。"); + } + if (!topicPath || typeof topicPath !== "string") { + throw new Error(`topicPaths.${topicId} 必须是字符串路径。`); + } + const topicAbs = resolveFromCwd(cwd, topicPath); + if (!fs.existsSync(topicAbs)) { + throw new Error(`路由清单引用的 topic 不存在:${topicPath}`); + } + } + + if (routing.fallbackTopic && !topicIds.has(routing.fallbackTopic)) { + throw new Error( + `fallbackTopic 不存在于 topicPaths:${routing.fallbackTopic}`, + ); + } + + if ( + routing.topicDependencies && + typeof routing.topicDependencies === "object" + ) { + for (const [topicId, deps] of Object.entries(routing.topicDependencies)) { + if (!topicIds.has(topicId)) { + throw new Error(`topicDependencies 引用了不存在的 topic:${topicId}`); + } + if (!Array.isArray(deps)) { + throw new Error(`topicDependencies.${topicId} 必须是数组。`); + } + for (const depId of deps) { + if (!topicIds.has(depId)) { + throw new Error( + `topicDependencies.${topicId} 引用了不存在的依赖:${depId}`, + ); + } + } + } + } + + if (routing.topicMetadata !== undefined) { + if ( + !routing.topicMetadata || + typeof routing.topicMetadata !== "object" || + Array.isArray(routing.topicMetadata) + ) { + throw new Error("topicMetadata 必须是对象。"); + } + for (const [topicId, metadata] of Object.entries(routing.topicMetadata)) { + if (!topicIds.has(topicId)) { + throw new Error(`topicMetadata 引用了不存在的 topic:${topicId}`); + } + if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) { + throw new Error(`topicMetadata.${topicId} 必须是对象。`); + } + for (const key of Object.keys(metadata)) { + if (!["primary", "tags", "confidence"].includes(key)) { + throw new Error(`topicMetadata.${topicId} 包含未知字段:${key}`); + } + } + if (!KNOWLEDGE_TOPIC_TYPES.includes(metadata.primary)) { + throw new Error( + `topicMetadata.${topicId}.primary 非法:${metadata.primary}`, + ); + } + if (!KNOWLEDGE_TOPIC_CONFIDENCE.includes(metadata.confidence)) { + throw new Error( + `topicMetadata.${topicId}.confidence 非法:${metadata.confidence}`, + ); + } + if (metadata.tags !== undefined) { + if (!Array.isArray(metadata.tags)) { + throw new Error(`topicMetadata.${topicId}.tags 必须是数组。`); + } + const seenTags = new Set(); + for (const tag of metadata.tags) { + if (!KNOWLEDGE_TOPIC_TYPES.includes(tag)) { + throw new Error(`topicMetadata.${topicId}.tags 包含非法值:${tag}`); + } + if (tag === metadata.primary) { + throw new Error( + `topicMetadata.${topicId}.tags 不应与 primary 重复:${tag}`, + ); + } + if (seenTags.has(tag)) { + throw new Error(`topicMetadata.${topicId}.tags 包含重复值:${tag}`); + } + seenTags.add(tag); + } + } + } + } + + const matcherMap = + matcherData?.matchers && typeof matcherData.matchers === "object" + ? matcherData.matchers + : null; + if (matcherData && !matcherMap) { + throw new Error("匹配清单缺少 matchers 对象。"); + } + + if (Array.isArray(routing.taskToTopicRules)) { + for (const rule of routing.taskToTopicRules) { + if (!rule || typeof rule !== "object") { + throw new Error("taskToTopicRules 存在非法项(非对象)。"); + } + if (!rule.task || typeof rule.task !== "string") { + throw new Error("taskToTopicRules 每项必须包含字符串类型的 task。"); + } + if (!Array.isArray(rule.topics) || rule.topics.length === 0) { + throw new Error(`taskToTopicRules(${rule.task}) 必须包含非空 topics。`); + } + if (!rule.matcherId || typeof rule.matcherId !== "string") { + throw new Error(`taskToTopicRules(${rule.task}) 必须包含 matcherId。`); + } + if (!rule.matcherPath || typeof rule.matcherPath !== "string") { + throw new Error(`taskToTopicRules(${rule.task}) 必须包含 matcherPath。`); + } + const matcherAbs = resolveFromCwd(cwd, rule.matcherPath); + if (!fs.existsSync(matcherAbs)) { + throw new Error( + `taskToTopicRules(${rule.task}) 引用了不存在的 matcherPath:${rule.matcherPath}`, + ); + } + let matcherShard; + try { + matcherShard = JSON.parse(fs.readFileSync(matcherAbs, "utf8")); + } catch (e) { + throw new Error(`matcherPath JSON 解析失败:${rule.matcherPath}`); + } + if (!matcherShard || typeof matcherShard !== "object") { + throw new Error(`matcherPath 内容非法(非对象):${rule.matcherPath}`); + } + if (matcherShard.id !== rule.matcherId) { + throw new Error( + `matcherPath(${rule.matcherPath}) 的 id 与 matcherId 不一致:${matcherShard.id} vs ${rule.matcherId}`, + ); + } + if (!Array.isArray(matcherShard.includeAny)) { + throw new Error( + `matcherPath(${rule.matcherPath}) 的 includeAny 必须为数组。`, + ); + } + for (const topicId of rule.topics) { + if (!topicIds.has(topicId)) { + throw new Error( + `taskToTopicRules(${rule.task}) 引用了不存在的 topic:${topicId}`, + ); + } + } + } + } +} + +/** + * 将当前 locale 包模板 knowledge/index.md 原样复制到目标 cwd 下 .Knowledge/template/index.template.md, + * 供 f2s-kb-upgrade 技能步骤 3b 与宿主仓 .Knowledge/index.md 对照;init 不修改 index.md 正文。 + * 注意:模板正文声明「.Knowledge」指宿主仓;与 flow2spec 开发仓根 .Knowledge(产品自用知识库)职责不同。 + */ +function copyKnowledgeIndexTemplateSnapshot(cwd, templatesDir) { + const src = path.join(templatesDir, "knowledge", "index.md"); + const destDir = path.join(cwd, KNOWLEDGE_ROOT, "template"); + const dest = path.join(destDir, "index.template.md"); + if (!fs.existsSync(src)) { + return { written: false, reason: "missing-template-index" }; + } + ensureDir(destDir); + fs.copyFileSync(src, dest); + return { written: true }; +} + +function copyRulesTemplates(cwd, agentRoot, templatesDir) { + const rulesSrc = path.join(templatesDir, "rules"); + const rulesDest = path.join(cwd, agentRoot, "rules"); + if (!fs.existsSync(rulesSrc)) return; + ensureDir(rulesDest); + + // 判断是否应该转换为 .md:Claude 和 Codex 使用 .md,Cursor 使用 .mdc + const isCursorAgent = agentRoot === ".cursor"; + const shouldConvertToMd = !isCursorAgent; + + const claudeStyle = shouldWriteClaudeStyleRules(agentRoot); + if (claudeStyle) { + for (const name of fs.readdirSync(rulesDest)) { + if (name.endsWith(".mdc")) { + fs.unlinkSync(path.join(rulesDest, name)); + } + } + } + + for (const name of fs.readdirSync(rulesSrc)) { + const srcPath = path.join(rulesSrc, name); + const st = fs.statSync(srcPath); + if (st.isDirectory()) { + copyRecursive(srcPath, path.join(rulesDest, name)); + continue; + } + // 对于 .md 文件(模板源) + if (name.endsWith(".md")) { + const raw = fs.readFileSync(srcPath, "utf8"); + if (isCursorAgent) { + // Cursor 需要转换为 .mdc + const destName = name.replace(/\.md$/i, ".mdc"); + fs.writeFileSync(path.join(rulesDest, destName), raw, "utf8"); + } else { + // Claude 和 Codex 保持 .md + const body = claudeStyle ? adaptRuleMdcToClaudeMd(raw) : raw; + fs.writeFileSync(path.join(rulesDest, name), body, "utf8"); + } + continue; + } + if (!name.endsWith(".mdc")) { + fs.copyFileSync(srcPath, path.join(rulesDest, name)); + continue; + } + const raw = fs.readFileSync(srcPath, "utf8"); + const body = claudeStyle ? adaptRuleMdcToClaudeMd(raw) : raw; + const destName = claudeStyle ? name.replace(/\.mdc$/i, ".md") : name; + fs.writeFileSync(path.join(rulesDest, destName), body, "utf8"); + } +} + +function copySkills(cwd, agentRoot, templatesDir) { + const destRoot = path.join(cwd, agentRoot); + const skillsSrc = path.join(templatesDir, "skills"); + + if (fs.existsSync(skillsSrc)) { + const skillsDest = path.join(destRoot, "skills"); + ensureDir(skillsDest); + const templateNames = new Set(fs.readdirSync(skillsSrc)); + // skills 目录:所有平台都保持 .md 格式,不转换 + for (const name of templateNames) { + copyRecursive(path.join(skillsSrc, name), path.join(skillsDest, name)); + } + // 删除配置根中以 f2s- 开头、但已不存在于当前 locale templates/skills/ 的旧 skill 目录 + // 只清理 Flow2Spec 管理的 skill,不触碰用户自定义 skill + // LEGACY_SKILLS:非 f2s- 开头的历史旧名,也需一并清理 + const LEGACY_SKILLS = new Set(["stock-docs-vs-req-docs"]); + if (fs.existsSync(skillsDest)) { + for (const name of fs.readdirSync(skillsDest)) { + if ((name.startsWith("f2s-") || LEGACY_SKILLS.has(name)) && !templateNames.has(name)) { + fs.rmSync(path.join(skillsDest, name), { recursive: true, force: true }); + } + } + } + } +} + +function removeLegacyAgentTemplateDir(cwd, agentRoot) { + const templateDir = path.join(cwd, agentRoot, "template"); + if (fs.existsSync(templateDir)) { + fs.rmSync(templateDir, { recursive: true, force: true }); + } +} + +function stripMdcFrontmatter(src) { + return src.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, ""); +} + +function writeTopicMirrors(cwd, templatesDir, agentRoot) { + const rulesDir = path.join(templatesDir, "rules"); + const outDir = path.join(cwd, agentRoot, "topics"); + ensureDir(outDir); + if (!fs.existsSync(rulesDir)) return; + // Mirror rule templates for clients that load long-form guidance on demand. + const names = fs + .readdirSync(rulesDir) + .filter((n) => { + const lower = n.toLowerCase(); + return lower.endsWith(".md") || lower.endsWith(".mdc"); + }) + .sort(); + for (const name of names) { + const srcPath = path.join(rulesDir, name); + if (!fs.statSync(srcPath).isFile()) continue; + const raw = fs.readFileSync(srcPath, "utf8"); + const body = stripMdcFrontmatter(raw).trimStart(); + const outName = name.replace(/\.(md|mdc)$/i, ".md"); + fs.writeFileSync(path.join(outDir, outName), body, "utf8"); + } +} + +function writeCodexTopicMirrors(cwd, templatesDir) { + writeTopicMirrors(cwd, templatesDir, ".codex"); +} + +/** + * 完整条令写仓库根;.codex/AGENTS.md 仅为指针,避免双份全文重复与 cwd 在 .codex 时双倍拼接。 + */ +function writeCodexEntry(cwd, templatesDir, projectConfig) { + const full = buildCodexAgentsMd(templatesDir, projectConfig); + const stub = buildCodexAgentsStubMd(templatesDir); + fs.writeFileSync(path.join(cwd, "AGENTS.md"), full, "utf8"); + fs.writeFileSync(path.join(cwd, ".codex", "AGENTS.md"), stub, "utf8"); + writeCodexTopicMirrors(cwd, templatesDir); +} + +/** Write the root instructions needed by DeepSeek Harness without overwriting an existing entry. */ +function writeDshEntry(cwd, templatesDir, projectConfig) { + const agentsPath = path.join(cwd, "AGENTS.md"); + if (!fs.existsSync(agentsPath)) { + fs.writeFileSync( + agentsPath, + buildDshAgentsMd(templatesDir, projectConfig), + "utf8", + ); + } + writeDshAgentsStub(cwd, templatesDir); + writeTopicMirrors(cwd, templatesDir, ".dsh"); +} + +function writeAgentArtifacts(cwd, agentId, templatesDir, projectConfig) { + const root = AGENTS[agentId].root; + copySkills(cwd, root, templatesDir); + removeLegacyAgentTemplateDir(cwd, root); + if (agentId === "dsh") { + writeDshEntry(cwd, templatesDir, projectConfig); + } else if (agentId !== "codex") { + copyRulesTemplates(cwd, root, templatesDir); + } else { + writeCodexEntry(cwd, templatesDir, projectConfig); + } +} + +/** + * @param {string} cwd + * @param {string[]} [agentIds] 不传则仅 cursor + * @param {object} [options] + * @param {boolean} [options.overwriteKnowledge] + * @param {object} [options.configValues] init 交互收集的配置字段值 + * @param {string} [options.locale] 显式模板语言 + */ +async function run(cwd, agentIds, options = {}) { + const { overwriteKnowledge = false, configValues } = options; + const ids = options.mode === "native-host" + ? [] + : normalizeAgentIds(agentIds || []); + const templatesRoot = path.join(__dirname, "..", "templates"); + const existingConfig = loadFlow2specConfig(cwd); + const requestedLocale = normalizeLocale( + options.locale || configValues?.locale || existingConfig.locale, + DEFAULT_LOCALE, + ); + const { templatesDir, locale } = resolveTemplatesDir(templatesRoot, requestedLocale); + const effectiveConfigValues = { ...(configValues || {}) }; + if (options.locale) { + effectiveConfigValues.locale = locale; + } + + ensureKnowledgeDirs(cwd); + removeKnowledgeUpdateCheckCache(cwd); + const gitignoreResult = ensureFlow2specGitignore(cwd); + ensureFlow2specProjectConfig(cwd, templatesDir, { + overwrite: false, + values: Object.keys(effectiveConfigValues).length ? effectiveConfigValues : undefined, + }); + const knowledgeResult = copyKnowledgeTemplates(cwd, templatesDir, { + overwrite: overwriteKnowledge, + }); + const routingUpgrade = upgradeKnowledgeRoutingAndMatchers(cwd, templatesDir, { + overwrite: overwriteKnowledge, + }); + finalizePkgRev(cwd, templatesDir); + validateKnowledgeRouting(cwd); + + const indexSnapshot = copyKnowledgeIndexTemplateSnapshot(cwd, templatesDir); + + const projectConfig = loadFlow2specConfig(cwd); + + const claudeHooksResult = {}; + for (const id of ids) { + ensureAgentDirs(cwd, id); + writeAgentArtifacts(cwd, id, templatesDir, projectConfig); + if (id === "claude") { + const result = writeClaudeAgentHooks(cwd, templatesDir); + claudeHooksResult.hookScriptWritten = result.hookScriptResult?.written ?? false; + claudeHooksResult.settingsChanged = result.settingsChanged; + } + // Cursor:写入官方 hooks.json,使 sessionStart 自动运行更新检测。 + if (id === "cursor") { + writeCursorUpdateCheckHook(cwd, templatesDir); + } + // Codex:写入官方 hooks.json,使 SessionStart 自动运行更新检测。 + if (id === "codex") { + writeCodexUpdateCheckHook(cwd, templatesDir); + } + } + return { + ids, + mode: options.mode || "project-adapter", + knowledgeResult, + overwriteKnowledge, + routingUpgrade, + indexSnapshot, + gitignoreResult, + projectConfig, + locale, + claudeHooksResult, + }; +} + +module.exports = run; diff --git a/packages/core/lib/knowledgeEngine.js b/packages/core/lib/knowledgeEngine.js new file mode 100644 index 0000000..0bf2cc4 --- /dev/null +++ b/packages/core/lib/knowledgeEngine.js @@ -0,0 +1,1415 @@ +const fs = require("fs"); +const path = require("path"); +const { KNOWLEDGE_ROOT } = require("./agents"); +const { loadFlow2specConfig } = require("./flow2specConfig"); +const { resolveDeveloperContext, activeTaskDir } = require("./developerId"); + +const KNOWLEDGE_FILENAME = "manifest-routing.json"; +const MATCHERS_FILENAME = "manifest-matchers.json"; +const INDEX_FILENAME = "index.md"; +const TOPIC_DIR = "topics"; +const DELTA_FILENAME = "kb-delta.json"; +const KB_COMMANDS = new Set([ + "appendBody", + "replaceBody", + "updateFrontmatter", + "createTopic", +]); +const ALLOWED_TOPIC_PRIMARY = new Set(["policy", "config", "feature", "module"]); +const ALLOWED_TOPIC_CONFIDENCE = new Set(["manual", "inferred"]); +const TOPIC_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/; +const MATCHER_ID_RE = /^m-[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/; + +function ensureDir(dir) { + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); +} + +function readJson(filePath) { + return JSON.parse(fs.readFileSync(filePath, "utf8")); +} + +function writeJson(filePath, data) { + fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8"); +} + +function stableStringify(value) { + if (Array.isArray(value)) { + return `[${value.map((item) => stableStringify(item)).join(",")}]`; + } + if (isPlainObject(value)) { + return `{${Object.keys(value) + .sort() + .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`) + .join(",")}}`; + } + return JSON.stringify(value); +} + +function resolveFromCwd(cwd, maybeRelativePath) { + return path.isAbsolute(maybeRelativePath) + ? maybeRelativePath + : path.join(cwd, maybeRelativePath); +} + +function isPlainObject(value) { + return ( + value && + typeof value === "object" && + !Array.isArray(value) && + Object.getPrototypeOf(value) === Object.prototype + ); +} + +function normalizeStringArray(values) { + const out = []; + const seen = new Set(); + for (const value of Array.isArray(values) ? values : []) { + if (typeof value !== "string") continue; + const item = value.trim(); + if (!item || seen.has(item)) continue; + seen.add(item); + out.push(item); + } + return out; +} + +function parseInlineArray(raw) { + const source = String(raw || "").trim(); + if (!source) return []; + const out = []; + let token = ""; + let quote = null; + let escaped = false; + const pushToken = () => { + const value = token.trim(); + if (value) out.push(parseFrontmatterScalar(value)); + token = ""; + }; + for (const ch of source) { + if (escaped) { + token += ch; + escaped = false; + continue; + } + if (ch === "\\") { + token += ch; + escaped = true; + continue; + } + if (quote) { + token += ch; + if (ch === quote) quote = null; + continue; + } + if (ch === '"' || ch === "'") { + token += ch; + quote = ch; + continue; + } + if (ch === ",") { + pushToken(); + continue; + } + token += ch; + } + pushToken(); + return out; +} + +// NOTE(frontmatter-subset): parse* / stringify* 只支持有限 YAML 子集 +// (null/bool/int/float/裸字符串/单层数组)。当前所有 change 类型 +// 都会经 normalizeTopicFrontmatter / normalizeStringArray 归一化, +// 类型是可控的。若未来允许 delta 直接写入任意 frontmatter,需要: +// 1) 显式声明每个字段的期望类型(否则会踩到 "3"↔3 类型漂移); +// 2) 或者引入一个真正的 YAML 库(如 yaml/js-yaml)替换本节。 +// 详情见 review 结论 L2。 +function parseFrontmatterScalar(raw) { + const value = String(raw || "").trim(); + if (value === "null" || value === "~") return null; + if (value === "true") return true; + if (value === "false") return false; + if (/^-?\d+$/.test(value)) return Number.parseInt(value, 10); + if (/^-?\d+\.\d+$/.test(value)) return Number.parseFloat(value); + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + return value.slice(1, -1).replace(/\\"/g, '"').replace(/\\'/g, "'"); + } + if (value.startsWith("[") && value.endsWith("]")) { + return parseInlineArray(value.slice(1, -1)); + } + return value; +} + +function stringifyFrontmatterScalar(value) { + if (value === null) return "null"; + if (typeof value === "number" || typeof value === "boolean") { + return String(value); + } + const str = String(value); + if (!str) return '""'; + if (/^[A-Za-z0-9_./:@-]+$/.test(str)) return str; + return JSON.stringify(str); +} + +function stringifyFrontmatterValue(value) { + if (Array.isArray(value)) { + return `[${value.map((item) => stringifyFrontmatterScalar(item)).join(", ")}]`; + } + return stringifyFrontmatterScalar(value); +} + +function parseFrontmatterBlock(block) { + const out = {}; + const lines = String(block || "").split(/\r?\n/); + for (const line of lines) { + if (!line.trim()) continue; + const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/); + if (!match) continue; + const key = match[1]; + const value = match[2].trim(); + if (!value) { + out[key] = ""; + continue; + } + out[key] = parseFrontmatterScalar(value); + } + return out; +} + +function parseTopicDocument(raw) { + const source = String(raw || ""); + const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/); + if (!match) { + return { + hasFrontmatter: false, + frontmatter: {}, + body: source, + }; + } + return { + hasFrontmatter: true, + frontmatter: parseFrontmatterBlock(match[1]), + body: source.slice(match[0].length), + }; +} + +function stringifyTopicDocument(frontmatter, body) { + const fm = stringifyFrontmatter(frontmatter); + const normalizedBody = String(body || "").replace(/^\n+/, ""); + if (!fm) { + return normalizedBody.endsWith("\n") ? normalizedBody : `${normalizedBody}\n`; + } + const bodyText = normalizedBody.endsWith("\n") + ? normalizedBody + : `${normalizedBody}\n`; + return `---\n${fm}---\n${bodyText}`; +} + +function stringifyFrontmatter(frontmatter) { + const source = isPlainObject(frontmatter) ? frontmatter : {}; + const preferredOrder = [ + "id", + "revision", + "summary", + "dependsOn", + "primary", + "confidence", + ]; + const keys = []; + for (const key of preferredOrder) { + if (Object.prototype.hasOwnProperty.call(source, key)) keys.push(key); + } + for (const key of Object.keys(source).sort()) { + if (!keys.includes(key)) keys.push(key); + } + const lines = []; + for (const key of keys) { + const value = source[key]; + if (value === undefined) continue; + lines.push(`${key}: ${stringifyFrontmatterValue(value)}`); + } + return lines.length ? `${lines.join("\n")}\n` : ""; +} + +function topicPathFor(topicId) { + return path.posix.join(KNOWLEDGE_ROOT, TOPIC_DIR, `${topicId}.md`); +} + +function topicAbsPath(cwd, topicPath) { + return resolveFromCwd(cwd, topicPath); +} + +function matcherPathFor(matcherId) { + return path.posix.join(KNOWLEDGE_ROOT, "matchers", `${matcherId}.json`); +} + +function graphCwd(graph) { + if (graph.cwd) return graph.cwd; + if (graph.routingPath) { + return path.dirname(path.dirname(graph.routingPath)); + } + return process.cwd(); +} + +function loadKnowledgeGraph(cwd) { + const routingPath = path.join(cwd, KNOWLEDGE_ROOT, KNOWLEDGE_FILENAME); + const matchersPath = path.join(cwd, KNOWLEDGE_ROOT, MATCHERS_FILENAME); + if (!fs.existsSync(routingPath)) { + throw new Error( + `缺少知识库路由清单:${path.join(KNOWLEDGE_ROOT, KNOWLEDGE_FILENAME)}`, + ); + } + const routing = readJson(routingPath); + const matchers = fs.existsSync(matchersPath) ? readJson(matchersPath) : null; + const topicEntries = []; + const topicPaths = routing.topicPaths || {}; + for (const [topicId, topicPath] of Object.entries(topicPaths)) { + const absPath = topicAbsPath(cwd, topicPath); + if (!fs.existsSync(absPath)) { + topicEntries.push({ + topicId, + path: topicPath, + absPath, + exists: false, + }); + continue; + } + const raw = fs.readFileSync(absPath, "utf8"); + const parsed = parseTopicDocument(raw); + const meta = routing.topicMetadata?.[topicId] || {}; + const frontmatter = isPlainObject(parsed.frontmatter) + ? { ...parsed.frontmatter } + : {}; + if (!Object.prototype.hasOwnProperty.call(frontmatter, "id")) { + frontmatter.id = topicId; + } + if (Object.prototype.hasOwnProperty.call(frontmatter, "dependsOn")) { + frontmatter.dependsOn = normalizeStringArray(frontmatter.dependsOn); + } + if (Object.prototype.hasOwnProperty.call(frontmatter, "primary")) { + frontmatter.primary = String(frontmatter.primary).trim(); + } else if (meta.primary) { + frontmatter.primary = meta.primary; + } + if (Object.prototype.hasOwnProperty.call(frontmatter, "confidence")) { + frontmatter.confidence = String(frontmatter.confidence).trim(); + } else if (meta.confidence) { + frontmatter.confidence = meta.confidence; + } + if (Object.prototype.hasOwnProperty.call(frontmatter, "tags")) { + frontmatter.tags = normalizeStringArray(frontmatter.tags); + } else if (Array.isArray(meta.tags)) { + frontmatter.tags = normalizeStringArray(meta.tags); + } + if ( + Object.prototype.hasOwnProperty.call(frontmatter, "summary") && + typeof frontmatter.summary !== "string" + ) { + frontmatter.summary = String(frontmatter.summary); + } + topicEntries.push({ + topicId, + path: topicPath, + absPath, + exists: true, + raw, + body: parsed.body, + hasFrontmatter: parsed.hasFrontmatter, + frontmatter, + routingMeta: meta, + }); + } + return { + routingPath, + matchersPath, + routing, + matchers, + topics: topicEntries, + }; +} + +function deriveRoutingOverlayFromGraph(graph) { + const topicMetadata = {}; + const topicDependencies = {}; + for (const topic of graph.topics) { + if (!topic.exists) continue; + const fm = topic.frontmatter || {}; + const entry = {}; + if (Object.prototype.hasOwnProperty.call(fm, "primary")) { + const primary = String(fm.primary || "").trim(); + if (ALLOWED_TOPIC_PRIMARY.has(primary)) { + entry.primary = primary; + } + } + if (Object.prototype.hasOwnProperty.call(fm, "confidence")) { + const confidence = String(fm.confidence || "").trim(); + if (ALLOWED_TOPIC_CONFIDENCE.has(confidence)) { + entry.confidence = confidence; + } + } + if (Array.isArray(fm.tags)) { + const tags = normalizeStringArray(fm.tags).filter( + (tag) => ALLOWED_TOPIC_PRIMARY.has(tag) && tag !== entry.primary, + ); + if (tags.length > 0) { + entry.tags = tags; + } + } + if (Object.keys(entry).length > 0) { + topicMetadata[topic.topicId] = entry; + } + if (Array.isArray(fm.dependsOn) && fm.dependsOn.length > 0) { + topicDependencies[topic.topicId] = normalizeStringArray(fm.dependsOn); + } + } + return { topicMetadata, topicDependencies }; +} + +function normalizeRoutingWithGraph(graph) { + const overlay = deriveRoutingOverlayFromGraph(graph); + const next = JSON.parse(JSON.stringify(graph.routing || {})); + let changed = false; + if (!isPlainObject(next.topicMetadata)) { + next.topicMetadata = {}; + changed = true; + } + if (!isPlainObject(next.topicDependencies)) { + next.topicDependencies = {}; + changed = true; + } + for (const [topicId, entry] of Object.entries(overlay.topicMetadata)) { + const raw = JSON.stringify(next.topicMetadata[topicId] || {}); + const nextRaw = JSON.stringify(entry); + if (raw !== nextRaw) { + next.topicMetadata[topicId] = entry; + changed = true; + } + } + for (const topicId of Object.keys(next.topicMetadata)) { + if (!Object.prototype.hasOwnProperty.call(overlay.topicMetadata, topicId)) { + if (graph.routing.topicMetadata?.[topicId]) continue; + delete next.topicMetadata[topicId]; + changed = true; + } + } + for (const [topicId, deps] of Object.entries(overlay.topicDependencies)) { + const raw = JSON.stringify(next.topicDependencies[topicId] || []); + const nextRaw = JSON.stringify(deps); + if (raw !== nextRaw) { + next.topicDependencies[topicId] = deps; + changed = true; + } + } + for (const topicId of Object.keys(next.topicDependencies)) { + if (!Object.prototype.hasOwnProperty.call(overlay.topicDependencies, topicId)) { + if (graph.routing.topicDependencies?.[topicId]) continue; + delete next.topicDependencies[topicId]; + changed = true; + } + } + return { routing: next, changed }; +} + +function validateKnowledgeGraph(graph, options = {}) { + const issues = []; + const warnings = []; + const strictRevision = Boolean(options.strictRevision); + const topicIds = new Set(); + + if (!graph || typeof graph !== "object") { + return { + ok: false, + issues: ["knowledge graph is empty"], + warnings, + topicCount: 0, + }; + } + + const routing = graph.routing || {}; + const topics = Array.isArray(graph.topics) ? graph.topics : []; + for (const topic of topics) { + topicIds.add(topic.topicId); + if (!topic.exists) { + issues.push(`topic missing: ${topic.topicId} -> ${topic.path}`); + continue; + } + const fm = topic.frontmatter || {}; + if (Object.prototype.hasOwnProperty.call(fm, "id") && fm.id !== topic.topicId) { + issues.push( + `topic frontmatter id mismatch: ${topic.topicId} vs ${String(fm.id)}`, + ); + } + if (Object.prototype.hasOwnProperty.call(fm, "revision")) { + const revision = Number(fm.revision); + if (!Number.isInteger(revision) || revision < 0) { + issues.push(`topic revision must be a non-negative integer: ${topic.topicId}`); + } + } else if (strictRevision) { + issues.push(`topic revision missing: ${topic.topicId}`); + } else { + warnings.push(`topic revision missing: ${topic.topicId}`); + } + if (Object.prototype.hasOwnProperty.call(fm, "primary")) { + const primary = String(fm.primary || "").trim(); + if (!ALLOWED_TOPIC_PRIMARY.has(primary)) { + issues.push(`topic primary invalid: ${topic.topicId} -> ${primary}`); + } + } + if (Object.prototype.hasOwnProperty.call(fm, "confidence")) { + const confidence = String(fm.confidence || "").trim(); + if (!ALLOWED_TOPIC_CONFIDENCE.has(confidence)) { + issues.push(`topic confidence invalid: ${topic.topicId} -> ${confidence}`); + } + } + if (Array.isArray(fm.dependsOn)) { + for (const depId of fm.dependsOn) { + if (!topic.topicId || typeof depId !== "string" || !depId.trim()) { + issues.push(`topic dependsOn contains empty value: ${topic.topicId}`); + continue; + } + if (!routing.topicPaths?.[depId]) { + issues.push(`topic dependsOn references missing topic: ${topic.topicId} -> ${depId}`); + } + } + } + } + + if (!routing.topicPaths || typeof routing.topicPaths !== "object") { + issues.push("routing.topicPaths missing or invalid"); + } + + if (routing.fallbackTopic && !routing.topicPaths?.[routing.fallbackTopic]) { + issues.push(`fallbackTopic missing from topicPaths: ${routing.fallbackTopic}`); + } + + if (routing.topicDependencies && typeof routing.topicDependencies === "object") { + for (const [topicId, deps] of Object.entries(routing.topicDependencies)) { + if (!routing.topicPaths?.[topicId]) { + issues.push(`topicDependencies references unknown topic: ${topicId}`); + } + if (!Array.isArray(deps)) { + issues.push(`topicDependencies.${topicId} must be an array`); + continue; + } + for (const depId of deps) { + if (!routing.topicPaths?.[depId]) { + issues.push( + `topicDependencies.${topicId} references unknown dependency: ${depId}`, + ); + } + } + } + } + + if (routing.topicMetadata && typeof routing.topicMetadata === "object") { + for (const [topicId, meta] of Object.entries(routing.topicMetadata)) { + if (!routing.topicPaths?.[topicId]) { + issues.push(`topicMetadata references unknown topic: ${topicId}`); + } + if (!meta || typeof meta !== "object" || Array.isArray(meta)) { + issues.push(`topicMetadata.${topicId} must be an object`); + continue; + } + if ( + Object.prototype.hasOwnProperty.call(meta, "primary") && + !ALLOWED_TOPIC_PRIMARY.has(String(meta.primary || "").trim()) + ) { + issues.push(`topicMetadata.${topicId}.primary invalid`); + } + if ( + Object.prototype.hasOwnProperty.call(meta, "confidence") && + !ALLOWED_TOPIC_CONFIDENCE.has(String(meta.confidence || "").trim()) + ) { + issues.push(`topicMetadata.${topicId}.confidence invalid`); + } + if (Array.isArray(meta.tags)) { + const seen = new Set(); + for (const tag of meta.tags) { + const normalized = String(tag || "").trim(); + if (!ALLOWED_TOPIC_PRIMARY.has(normalized)) { + issues.push(`topicMetadata.${topicId}.tags invalid value: ${normalized}`); + continue; + } + if (seen.has(normalized)) { + issues.push(`topicMetadata.${topicId}.tags contains duplicate: ${normalized}`); + } + seen.add(normalized); + } + } + } + } + + const matcherMap = + graph.matchers && graph.matchers.matchers && typeof graph.matchers.matchers === "object" + ? graph.matchers.matchers + : null; + if (graph.matchers && !matcherMap) { + issues.push("manifest-matchers structure invalid"); + } + + if (Array.isArray(routing.taskToTopicRules)) { + for (const rule of routing.taskToTopicRules) { + if (!rule || typeof rule !== "object") { + issues.push("taskToTopicRules contains a non-object rule"); + continue; + } + if (!rule.task || typeof rule.task !== "string") { + issues.push("taskToTopicRules entry missing task"); + } + if (!Array.isArray(rule.topics) || rule.topics.length === 0) { + issues.push(`taskToTopicRules(${rule.task || "unknown"}) must contain topics`); + } else { + for (const topicId of rule.topics) { + if (!routing.topicPaths?.[topicId]) { + issues.push( + `taskToTopicRules(${rule.task || "unknown"}) references unknown topic: ${topicId}`, + ); + } + } + } + if (!rule.matcherId || typeof rule.matcherId !== "string") { + issues.push(`taskToTopicRules(${rule.task || "unknown"}) missing matcherId`); + } + if (!rule.matcherPath || typeof rule.matcherPath !== "string") { + issues.push(`taskToTopicRules(${rule.task || "unknown"}) missing matcherPath`); + } else { + const matcherAbs = resolveFromCwd(graph.cwd || process.cwd(), rule.matcherPath); + if (!fs.existsSync(matcherAbs)) { + issues.push( + `taskToTopicRules(${rule.task || "unknown"}) matcherPath missing: ${rule.matcherPath}`, + ); + } else { + try { + const matcherShard = readJson(matcherAbs); + if (matcherShard.id !== rule.matcherId) { + issues.push( + `matcher id mismatch: ${rule.matcherPath} -> ${matcherShard.id} vs ${rule.matcherId}`, + ); + } + if (!Array.isArray(matcherShard.includeAny)) { + issues.push(`matcher includeAny invalid: ${rule.matcherPath}`); + } + } catch (error) { + issues.push(`matcher JSON invalid: ${rule.matcherPath}`); + } + } + } + } + } + + return { + ok: issues.length === 0, + issues, + warnings, + topicCount: topicIds.size, + }; +} + +function loadKnowledgeState(cwd) { + const graph = loadKnowledgeGraph(cwd); + graph.cwd = cwd; + const validation = validateKnowledgeGraph(graph); + return { graph, validation }; +} + +function parseKnowledgeDelta(input) { + const delta = typeof input === "string" ? readJson(input) : input; + if (!isPlainObject(delta)) { + throw new Error("kb delta 必须是对象"); + } + if (!delta.taskId || typeof delta.taskId !== "string") { + throw new Error("kb delta 缺少 taskId"); + } + if (!delta.developerId || typeof delta.developerId !== "string") { + throw new Error("kb delta 缺少 developerId"); + } + const baseRevisions = isPlainObject(delta.baseRevisions) + ? delta.baseRevisions + : {}; + const normalizedBaseRevisions = {}; + for (const [topicId, revision] of Object.entries(baseRevisions)) { + const nextRevision = Number(revision); + if (!topicId || !Number.isInteger(nextRevision) || nextRevision < 0) { + throw new Error(`kb delta baseRevisions 非法: ${topicId}`); + } + normalizedBaseRevisions[topicId] = nextRevision; + } + if (!Array.isArray(delta.changes) || delta.changes.length === 0) { + throw new Error("kb delta 需要至少一个 change"); + } + const changes = delta.changes.map((change, index) => + normalizeKnowledgeDeltaChange(change, index), + ); + return { + taskId: delta.taskId.trim(), + developerId: delta.developerId.trim(), + baseRevisions: normalizedBaseRevisions, + changes, + notes: typeof delta.notes === "string" ? delta.notes : "", + }; +} + +function normalizeKnowledgeDeltaChange(change, index) { + if (!isPlainObject(change)) { + throw new Error(`kb delta change[${index}] 必须是对象`); + } + const type = String(change.type || "").trim(); + if (!KB_COMMANDS.has(type)) { + throw new Error(`kb delta change[${index}] type 非法: ${type}`); + } + const targetTopic = String(change.targetTopic || "").trim(); + if (!targetTopic) { + throw new Error(`kb delta change[${index}] 缺少 targetTopic`); + } + const normalized = { + type, + targetTopic, + }; + if (Object.prototype.hasOwnProperty.call(change, "summary")) { + normalized.summary = String(change.summary || "").trim(); + } + if (Object.prototype.hasOwnProperty.call(change, "content")) { + normalized.content = String(change.content || ""); + } + if (Object.prototype.hasOwnProperty.call(change, "frontmatter")) { + if (!isPlainObject(change.frontmatter)) { + throw new Error(`kb delta change[${index}].frontmatter 必须是对象`); + } + normalized.frontmatter = JSON.parse(JSON.stringify(change.frontmatter)); + } + if (Object.prototype.hasOwnProperty.call(change, "taskRule")) { + if (!isPlainObject(change.taskRule)) { + throw new Error(`kb delta change[${index}].taskRule 必须是对象`); + } + normalized.taskRule = normalizeDeltaTaskRule( + change.taskRule, + normalized.targetTopic, + index, + ); + } + if (Object.prototype.hasOwnProperty.call(change, "matcher")) { + if (!isPlainObject(change.matcher)) { + throw new Error(`kb delta change[${index}].matcher 必须是对象`); + } + const matcherId = + normalized.taskRule?.matcherId || + String(change.matcher.id || `m-${normalized.targetTopic}`).trim(); + normalized.matcher = normalizeDeltaMatcher( + change.matcher, + matcherId, + index, + ); + if (normalized.taskRule && !normalized.taskRule.matcherId) { + normalized.taskRule.matcherId = normalized.matcher.id; + normalized.taskRule.matcherPath = matcherPathFor(normalized.matcher.id); + } + } + if (normalized.taskRule && !normalized.matcher) { + throw new Error( + `kb delta change[${index}] 带 taskRule 时必须同时提供 matcher`, + ); + } + if ( + normalized.taskRule && + normalized.matcher && + normalized.taskRule.matcherId !== normalized.matcher.id + ) { + throw new Error( + `kb delta change[${index}] matcher id 不一致: ${normalized.matcher.id} vs ${normalized.taskRule.matcherId}`, + ); + } + if ( + (normalized.type === "appendBody" || normalized.type === "replaceBody") && + !String(normalized.content || "").trim() + ) { + // 提前到 parse 阶段:kb status/plan 命中此错误时会被 + // scanTaskKnowledgeDeltas 的 try/catch 转成结构化 error 字段, + // 而不是让 applyTopicChangeDraft 在 plan 时抛出裸异常炸掉 CLI。 + throw new Error( + `kb delta change[${index}] ${normalized.type} 缺少 content(不能为空字符串)`, + ); + } + if (normalized.type === "createTopic") { + if (!TOPIC_ID_RE.test(normalized.targetTopic)) { + throw new Error( + `kb delta change[${index}] targetTopic 非法: ${normalized.targetTopic}`, + ); + } + if (!String(normalized.content || "").trim()) { + throw new Error(`createTopic change for ${normalized.targetTopic} 缺少 content`); + } + normalized.frontmatter = normalizeTopicFrontmatter( + normalized.targetTopic, + normalized.frontmatter || {}, + { defaultPrimary: "feature", defaultConfidence: "inferred" }, + ); + } + return normalized; +} + +function normalizeDeltaTaskRule(rule, targetTopic, index) { + const task = String(rule.task || "").trim(); + if (!task) { + throw new Error(`kb delta change[${index}].taskRule 缺少 task`); + } + const matcherIdRaw = String(rule.matcherId || `m-${task}`).trim(); + if (!MATCHER_ID_RE.test(matcherIdRaw)) { + throw new Error( + `kb delta change[${index}].taskRule.matcherId 非法: ${matcherIdRaw}`, + ); + } + const topics = normalizeStringArray(rule.topics || [targetTopic]); + if (!topics.includes(targetTopic)) topics.push(targetTopic); + return { + task, + matcherId: matcherIdRaw, + matcherPath: + typeof rule.matcherPath === "string" && rule.matcherPath.trim() + ? rule.matcherPath.trim().replace(/\\/g, "/") + : matcherPathFor(matcherIdRaw), + topics, + }; +} + +function normalizeDeltaMatcher(matcher, matcherId, index) { + const id = String(matcher.id || matcherId || "").trim(); + if (!MATCHER_ID_RE.test(id)) { + throw new Error(`kb delta change[${index}].matcher.id 非法: ${id}`); + } + const out = { + id, + version: + typeof matcher.version === "string" && matcher.version.trim() + ? matcher.version.trim() + : "1.0.0", + schema: + typeof matcher.schema === "string" && matcher.schema.trim() + ? matcher.schema.trim() + : "flow2spec.matcher.v1", + includeAny: normalizeStringArray(matcher.includeAny), + }; + for (const key of ["includeAll", "excludeAny", "excludeAll"]) { + const values = normalizeStringArray(matcher[key]); + if (values.length > 0) out[key] = values; + } + if (out.includeAny.length === 0 && !out.includeAll?.length) { + throw new Error(`kb delta change[${index}].matcher 缺少 includeAny/includeAll`); + } + return out; +} + +function normalizeTopicFrontmatter(topicId, frontmatter, options = {}) { + const source = isPlainObject(frontmatter) ? frontmatter : {}; + const out = JSON.parse(JSON.stringify(source)); + out.id = topicId; + const revision = Number(out.revision || 0); + out.revision = Number.isInteger(revision) && revision >= 0 ? revision : 0; + if (!out.primary) out.primary = options.defaultPrimary || "feature"; + if (!out.confidence) out.confidence = options.defaultConfidence || "inferred"; + if (Object.prototype.hasOwnProperty.call(out, "dependsOn")) { + out.dependsOn = normalizeStringArray(out.dependsOn); + } + if (Object.prototype.hasOwnProperty.call(out, "tags")) { + out.tags = normalizeStringArray(out.tags); + } + return out; +} + +function inferSummaryFromBody(body) { + const heading = String(body || "") + .split(/\r?\n/) + .find((line) => line.trim().startsWith("#")); + return heading ? heading.replace(/^#+\s*/, "").trim() : ""; +} + +function frontmatterForRouting(topic, routing) { + const meta = routing.topicMetadata?.[topic.topicId] || {}; + const deps = routing.topicDependencies?.[topic.topicId] || []; + const current = isPlainObject(topic.frontmatter) ? topic.frontmatter : {}; + const out = JSON.parse(JSON.stringify(current)); + let changed = false; + const setIfMissingOrInvalid = (key, value, isValid = (item) => item !== undefined) => { + if (!isValid(value)) return; + if (!Object.prototype.hasOwnProperty.call(out, key) || out[key] === "") { + out[key] = value; + changed = true; + } + }; + + if (out.id !== topic.topicId) { + out.id = topic.topicId; + changed = true; + } + const revision = Number(out.revision); + if (!Number.isInteger(revision) || revision < 0) { + out.revision = 0; + changed = true; + } + setIfMissingOrInvalid("summary", inferSummaryFromBody(topic.body), (item) => Boolean(item)); + if (Array.isArray(deps) && deps.length > 0) { + const normalizedDeps = normalizeStringArray(deps); + if (JSON.stringify(out.dependsOn || []) !== JSON.stringify(normalizedDeps)) { + out.dependsOn = normalizedDeps; + changed = true; + } + } + if (meta.primary && ALLOWED_TOPIC_PRIMARY.has(meta.primary)) { + setIfMissingOrInvalid("primary", meta.primary); + } + if (meta.confidence && ALLOWED_TOPIC_CONFIDENCE.has(meta.confidence)) { + setIfMissingOrInvalid("confidence", meta.confidence); + } + if (Array.isArray(meta.tags) && meta.tags.length > 0) { + const tags = normalizeStringArray(meta.tags).filter((tag) => + ALLOWED_TOPIC_PRIMARY.has(tag), + ); + if (tags.length > 0 && JSON.stringify(out.tags || []) !== JSON.stringify(tags)) { + out.tags = tags; + changed = true; + } + } + return { frontmatter: out, changed }; +} + +function ensureTopicFrontmatterFromRouting(graph, options = {}) { + const dryRun = Boolean(options.dryRun); + const changedFiles = []; + for (const topic of graph.topics) { + if (!topic.exists) continue; + const next = frontmatterForRouting(topic, graph.routing); + if (!next.changed && topic.hasFrontmatter) continue; + const content = stringifyTopicDocument(next.frontmatter, topic.body); + topic.frontmatter = next.frontmatter; + topic.hasFrontmatter = true; + topic.raw = content; + if (!dryRun) { + fs.writeFileSync(topic.absPath, content, "utf8"); + } + changedFiles.push(topic.path); + } + return { changedFiles }; +} + +function planKnowledgeDelta(graph, delta) { + const parsedDelta = typeof delta === "string" ? parseKnowledgeDelta(delta) : parseKnowledgeDelta(delta); + const working = new Map(); + const originalRevisions = new Map(); + const pendingTaskRules = new Set(); + const pendingMatcherIds = new Set(); + const pendingMatcherPaths = new Set(); + for (const topic of graph.topics) { + if (!topic.exists) continue; + working.set(topic.topicId, JSON.parse(JSON.stringify(topic))); + originalRevisions.set(topic.topicId, Number(topic.frontmatter?.revision || 0)); + } + const plan = []; + const conflicts = []; + + for (const change of parsedDelta.changes) { + if (change.type === "createTopic") { + const createPlan = planCreateTopicChange(graph, working, change, { + pendingTaskRules, + pendingMatcherIds, + pendingMatcherPaths, + }); + if (createPlan.conflict) { + conflicts.push(createPlan.conflict); + continue; + } + working.set(change.targetTopic, createPlan.topic); + originalRevisions.set(change.targetTopic, 0); + if (change.taskRule) { + pendingTaskRules.add(change.taskRule.task); + } + if (change.matcher) { + pendingMatcherIds.add(change.matcher.id); + pendingMatcherPaths.add(change.taskRule?.matcherPath || matcherPathFor(change.matcher.id)); + } + plan.push(createPlan.plan); + continue; + } + const current = working.get(change.targetTopic); + if (!current) { + conflicts.push({ + topicId: change.targetTopic, + reason: "topic missing", + change, + }); + continue; + } + const currentRevision = Number(current.frontmatter?.revision || 0); + const originalRevision = originalRevisions.get(change.targetTopic) || 0; + const expected = parsedDelta.baseRevisions[change.targetTopic]; + if ( + Number.isInteger(expected) && + expected >= 0 && + expected !== originalRevision + ) { + conflicts.push({ + topicId: change.targetTopic, + reason: `revision mismatch ${expected} -> ${originalRevision}`, + change, + }); + continue; + } + const next = applyTopicChangeDraft(current, change); + working.set(change.targetTopic, next); + plan.push({ + topicId: change.targetTopic, + type: change.type, + beforeRevision: currentRevision, + afterRevision: next.frontmatter.revision, + summary: change.summary || "", + }); + } + + return { + delta: parsedDelta, + plan, + conflicts, + mergeable: conflicts.length === 0, + }; +} + +function planCreateTopicChange(graph, working, change, pending = {}) { + const topicId = change.targetTopic; + const cwd = graphCwd(graph); + const topic = createTopicDraft(cwd, change); + const topicPath = topic.path; + const absPath = topic.absPath; + if (working.has(topicId) || graph.routing.topicPaths?.[topicId] || fs.existsSync(absPath)) { + return { + conflict: { + topicId, + reason: "topic already exists", + change, + }, + }; + } + const deps = normalizeStringArray(change.frontmatter?.dependsOn); + for (const depId of deps) { + if (!working.has(depId) && !graph.routing.topicPaths?.[depId]) { + return { + conflict: { + topicId, + reason: `dependency missing: ${depId}`, + change, + }, + }; + } + } + if (change.taskRule) { + const rules = Array.isArray(graph.routing.taskToTopicRules) + ? graph.routing.taskToTopicRules + : []; + const duplicateRule = rules.find( + (rule) => + rule.task === change.taskRule.task || + rule.matcherId === change.taskRule.matcherId || + rule.matcherPath === change.taskRule.matcherPath, + ); + if (duplicateRule) { + return { + conflict: { + topicId, + reason: `task rule already exists: ${duplicateRule.task}`, + change, + }, + }; + } + if (pending.pendingTaskRules?.has(change.taskRule.task)) { + return { + conflict: { + topicId, + reason: `task rule duplicated in delta: ${change.taskRule.task}`, + change, + }, + }; + } + } + if (change.matcher) { + const matcherId = change.matcher.id; + const matcherPath = change.taskRule?.matcherPath || matcherPathFor(matcherId); + const matcherAbs = resolveFromCwd(cwd, matcherPath); + const matcherMap = graph.matchers?.matchers || {}; + if (matcherMap[matcherId] || fs.existsSync(matcherAbs)) { + return { + conflict: { + topicId, + reason: `matcher already exists: ${matcherId}`, + change, + }, + }; + } + if ( + pending.pendingMatcherIds?.has(matcherId) || + pending.pendingMatcherPaths?.has(matcherPath) + ) { + return { + conflict: { + topicId, + reason: `matcher duplicated in delta: ${matcherId}`, + change, + }, + }; + } + if (change.taskRule && matcherId !== change.taskRule.matcherId) { + return { + conflict: { + topicId, + reason: `matcher id mismatch: ${matcherId} vs ${change.taskRule.matcherId}`, + change, + }, + }; + } + } + return { + topic, + plan: { + topicId, + type: change.type, + beforeRevision: null, + afterRevision: topic.frontmatter.revision, + summary: change.summary || "", + creates: { + topicPath, + matcherPath: change.matcher + ? change.taskRule?.matcherPath || matcherPathFor(change.matcher.id) + : null, + taskRule: change.taskRule?.task || null, + }, + }, + }; +} + +function createTopicDraft(cwd, change) { + const topicId = change.targetTopic; + const topicPath = topicPathFor(topicId); + const absPath = topicAbsPath(cwd, topicPath); + const body = String(change.content || ""); + const bodyText = body.endsWith("\n") ? body : `${body}\n`; + const frontmatter = normalizeTopicFrontmatter(topicId, change.frontmatter || {}); + return { + topicId, + path: topicPath, + absPath, + exists: true, + raw: stringifyTopicDocument(frontmatter, bodyText), + body: bodyText, + hasFrontmatter: true, + frontmatter, + routingMeta: {}, + }; +} + +function applyTopicChangeDraft(topic, change) { + const next = JSON.parse(JSON.stringify(topic)); + const frontmatter = isPlainObject(next.frontmatter) ? next.frontmatter : {}; + const currentRevision = Number(frontmatter.revision || 0); + let body = String(next.body || ""); + + frontmatter.id = topic.topicId; + if (change.type === "appendBody") { + const fragment = String(change.content || "").trim(); + if (!fragment) { + throw new Error(`appendBody change for ${topic.topicId} 缺少 content`); + } + body = body.trimEnd(); + body = body ? `${body}\n\n${fragment}\n` : `${fragment}\n`; + } else if (change.type === "replaceBody") { + body = String(change.content || ""); + if (!body.trim()) { + throw new Error(`replaceBody change for ${topic.topicId} 缺少 content`); + } + if (!body.endsWith("\n")) body += "\n"; + } else if (change.type === "updateFrontmatter") { + const incoming = isPlainObject(change.frontmatter) ? change.frontmatter : {}; + for (const [key, value] of Object.entries(incoming)) { + if (value === undefined) continue; + if (key === "dependsOn") { + frontmatter.dependsOn = normalizeStringArray(value); + } else if (key === "revision") { + const revision = Number(value); + if (!Number.isInteger(revision) || revision < 0) { + throw new Error(`topic ${topic.topicId} revision 非法`); + } + frontmatter.revision = revision; + } else if (key === "tags") { + frontmatter.tags = normalizeStringArray(value); + } else { + frontmatter[key] = value; + } + } + } + + frontmatter.revision = currentRevision + 1; + next.frontmatter = frontmatter; + next.body = body; + next.hasFrontmatter = true; + return next; +} + +function applyKnowledgeDelta(cwd, deltaInput, options = {}) { + const graph = loadKnowledgeGraph(cwd); + graph.cwd = cwd; + const parsedDelta = parseKnowledgeDelta(deltaInput); + const dryRun = Boolean(options.dryRun); + const planResult = planKnowledgeDelta(graph, parsedDelta); + if (!planResult.mergeable) { + const error = new Error("kb delta 存在冲突,无法自动合并"); + error.planResult = planResult; + throw error; + } + + const changedFiles = []; + const changedTopicIds = []; + const createdTopicIds = []; + const matcherWrites = []; + const taskRuleWrites = []; + const drafts = new Map(); + for (const topic of graph.topics) { + if (!topic.exists) continue; + drafts.set(topic.topicId, JSON.parse(JSON.stringify(topic))); + } + for (const change of parsedDelta.changes) { + if (change.type === "createTopic") { + const nextTopic = createTopicDraft(cwd, change); + drafts.set(change.targetTopic, nextTopic); + if (!changedTopicIds.includes(change.targetTopic)) { + changedTopicIds.push(change.targetTopic); + } + if (!createdTopicIds.includes(change.targetTopic)) { + createdTopicIds.push(change.targetTopic); + } + if (change.matcher) { + matcherWrites.push({ + matcher: change.matcher, + matcherPath: change.taskRule?.matcherPath || matcherPathFor(change.matcher.id), + }); + } + if (change.taskRule) { + taskRuleWrites.push(change.taskRule); + } + continue; + } + if (!drafts.has(change.targetTopic)) { + throw new Error(`未知 topic: ${change.targetTopic}`); + } + const topic = drafts.get(change.targetTopic); + const nextTopic = applyTopicChangeDraft(topic, change); + drafts.set(change.targetTopic, nextTopic); + if (!changedTopicIds.includes(change.targetTopic)) { + changedTopicIds.push(change.targetTopic); + } + } + + for (const topicId of changedTopicIds) { + const topicIndex = graph.topics.findIndex((item) => item.topicId === topicId); + const nextTopic = drafts.get(topicId); + const nextContent = stringifyTopicDocument(nextTopic.frontmatter, nextTopic.body); + if (!dryRun) { + ensureDir(path.dirname(nextTopic.absPath)); + fs.writeFileSync(nextTopic.absPath, nextContent, "utf8"); + } + if (!changedFiles.includes(nextTopic.path)) { + changedFiles.push(nextTopic.path); + } + if (topicIndex >= 0) { + const topic = graph.topics[topicIndex]; + graph.topics[topicIndex] = { + ...topic, + ...nextTopic, + raw: nextContent, + }; + } else { + graph.topics.push({ + ...nextTopic, + raw: nextContent, + }); + } + } + + if (!isPlainObject(graph.routing.topicPaths)) { + graph.routing.topicPaths = {}; + } + for (const topicId of createdTopicIds) { + const topic = drafts.get(topicId); + graph.routing.topicPaths[topicId] = topic.path; + } + + if (!Array.isArray(graph.routing.taskToTopicRules)) { + graph.routing.taskToTopicRules = []; + } + for (const rule of taskRuleWrites) { + graph.routing.taskToTopicRules.push(rule); + } + + for (const item of matcherWrites) { + const matcherAbs = resolveFromCwd(cwd, item.matcherPath); + if (!dryRun) { + ensureDir(path.dirname(matcherAbs)); + writeJson(matcherAbs, item.matcher); + } + if (!changedFiles.includes(item.matcherPath)) { + changedFiles.push(item.matcherPath); + } + } + + if (graph.matchersPath && fs.existsSync(graph.matchersPath) && matcherWrites.length > 0) { + const manifestMatchers = graph.matchers || { + version: "1.0.0", + generatedFrom: ".Knowledge/manifest-routing.json", + matcherKey: "matcherId", + sourceOfTruth: ".Knowledge/manifest-routing.json", + matchers: {}, + }; + if (!isPlainObject(manifestMatchers.matchers)) { + manifestMatchers.matchers = {}; + } + for (const item of matcherWrites) { + const { id, ...matcherBody } = item.matcher; + manifestMatchers.matchers[id] = matcherBody; + } + graph.matchers = manifestMatchers; + if (!dryRun) { + writeJson(graph.matchersPath, manifestMatchers); + } + const manifestMatchersPath = path.posix.join(KNOWLEDGE_ROOT, MATCHERS_FILENAME); + if (!changedFiles.includes(manifestMatchersPath)) { + changedFiles.push(manifestMatchersPath); + } + } + + const normalizedRouting = normalizeRoutingWithGraph(graph); + if (normalizedRouting.changed && !dryRun) { + writeJson(graph.routingPath, normalizedRouting.routing); + changedFiles.push(path.posix.join(KNOWLEDGE_ROOT, KNOWLEDGE_FILENAME)); + } + + return { + dryRun, + changedFiles, + plan: planResult.plan, + conflicts: planResult.conflicts, + delta: parsedDelta, + }; +} + +function scanTaskKnowledgeDeltas(cwd, taskRoot) { + const resolvedRoot = taskRoot || resolveDeveloperContext(loadFlow2specConfig(cwd), { cwd }).taskRoot; + const activeRoot = path.join(cwd, resolvedRoot, "active"); + if (!fs.existsSync(activeRoot)) { + return []; + } + const tasks = []; + for (const name of fs.readdirSync(activeRoot)) { + const taskDir = path.join(activeRoot, name); + if (!fs.statSync(taskDir).isDirectory()) continue; + const deltaPath = path.join(taskDir, DELTA_FILENAME); + if (!fs.existsSync(deltaPath)) continue; + try { + const delta = parseKnowledgeDelta(deltaPath); + tasks.push({ + taskName: name, + taskDir, + deltaPath, + delta, + }); + } catch (error) { + tasks.push({ + taskName: name, + taskDir, + deltaPath, + error: error.message || String(error), + }); + } + } + return tasks; +} + +function summarizeKnowledgeState(cwd, options = {}) { + const { graph, validation } = loadKnowledgeState(cwd); + const taskRoot = options.taskRoot || + resolveDeveloperContext(loadFlow2specConfig(cwd), { cwd }).taskRoot; + const deltaFiles = scanTaskKnowledgeDeltas(cwd, taskRoot); + const normalizedRouting = normalizeRoutingWithGraph(graph); + const drift = + stableStringify(normalizedRouting.routing) !== stableStringify(graph.routing); + const tasks = deltaFiles.map((item) => { + if (item.error) { + return { + taskName: item.taskName, + deltaPath: item.deltaPath, + error: item.error, + }; + } + const plan = planKnowledgeDelta(graph, item.delta); + return { + taskName: item.taskName, + deltaPath: item.deltaPath, + mergeable: plan.mergeable, + plan: plan.plan, + conflicts: plan.conflicts, + }; + }); + return { + cwd, + taskRoot, + topicCount: graph.topics.length, + validation, + routingDrift: drift, + tasks, + }; +} + +function buildKnowledgeGraph(cwd, options = {}) { + const graph = loadKnowledgeGraph(cwd); + graph.cwd = cwd; + const topicFrontmatter = options.writeTopicFrontmatter + ? ensureTopicFrontmatterFromRouting(graph, { + dryRun: options.dryRun, + }) + : { changedFiles: [] }; + const normalizedRouting = normalizeRoutingWithGraph(graph); + const changed = + stableStringify(normalizedRouting.routing) !== stableStringify(graph.routing); + if (changed && !options.dryRun) { + writeJson(graph.routingPath, normalizedRouting.routing); + } + return { + changed: changed || topicFrontmatter.changedFiles.length > 0, + routingPath: graph.routingPath, + topicFrontmatterChanged: topicFrontmatter.changedFiles, + normalizedRouting: normalizedRouting.routing, + validation: validateKnowledgeGraph({ + ...graph, + routing: normalizedRouting.routing, + }), + }; +} + +module.exports = { + KNOWLEDGE_ROOT, + KNOWLEDGE_FILENAME, + MATCHERS_FILENAME, + INDEX_FILENAME, + TOPIC_DIR, + DELTA_FILENAME, + loadKnowledgeGraph, + loadKnowledgeState, + validateKnowledgeGraph, + parseTopicDocument, + stringifyTopicDocument, + parseKnowledgeDelta, + planKnowledgeDelta, + applyKnowledgeDelta, + scanTaskKnowledgeDeltas, + summarizeKnowledgeState, + buildKnowledgeGraph, + ensureTopicFrontmatterFromRouting, + normalizeRoutingWithGraph, + stableStringify, + topicPathFor, +}; diff --git a/packages/core/lib/routing.js b/packages/core/lib/routing.js new file mode 100644 index 0000000..b410ed4 --- /dev/null +++ b/packages/core/lib/routing.js @@ -0,0 +1,165 @@ +"use strict"; + +const fs = require("fs"); +const path = require("path"); + +const KNOWLEDGE_ROOT = ".Knowledge"; + +function normalizeText(value) { + return String(value || "") + .toLowerCase() + .replace(/[\s_\-./]+/g, " ") + .trim(); +} + +function readJson(file) { + return JSON.parse(fs.readFileSync(file, "utf8")); +} + +function manifestPath(cwd) { + return path.join(cwd, KNOWLEDGE_ROOT, "manifest-routing.json"); +} + +function loadManifest(cwd) { + const file = manifestPath(cwd); + if (!fs.existsSync(file)) { + const error = new Error(`Knowledge manifest not found: ${file}`); + error.code = "F2S_NOT_INITIALIZED"; + throw error; + } + return { file, value: readJson(file) }; +} + +function matcherPathFor(cwd, rule) { + const relative = rule.matcherPath || ""; + return path.join(cwd, KNOWLEDGE_ROOT, relative.replace(/^\.Knowledge[\\/]/, "")); +} + +function phraseScore(text, phrase) { + const normalizedPhrase = normalizeText(phrase); + if (!normalizedPhrase || !text.includes(normalizedPhrase)) return 0; + return normalizedPhrase.split(" ").length * 10 + normalizedPhrase.length; +} + +function match(cwd, input = {}) { + const { value: manifest } = loadManifest(cwd); + const request = normalizeText(input.request || input.query); + const task = normalizeText(input.task); + const rules = Array.isArray(manifest.taskToTopicRules) ? manifest.taskToTopicRules : []; + const candidates = []; + + for (let index = 0; index < rules.length; index += 1) { + const rule = rules[index]; + const exactTask = task && normalizeText(rule.task) === task; + let phrases = []; + const matcherFile = matcherPathFor(cwd, rule); + if (fs.existsSync(matcherFile)) { + try { + const matcher = readJson(matcherFile); + phrases = Array.isArray(matcher.includeAny) ? matcher.includeAny : []; + } catch { + phrases = []; + } + } + const phraseHits = phrases + .map((phrase) => ({ phrase, score: phraseScore(request, phrase) })) + .filter((hit) => hit.score > 0) + .sort((a, b) => b.score - a.score); + if (!exactTask && phraseHits.length === 0) continue; + const score = exactTask ? 10000 : phraseHits[0].score; + candidates.push({ + rule, + score, + order: index, + confidence: exactTask ? "high" : score >= 30 ? "medium" : "low", + matchedPhrases: phraseHits.map((hit) => hit.phrase), + topics: Array.isArray(rule.topics) ? rule.topics : [], + }); + } + + candidates.sort((a, b) => b.score - a.score || a.order - b.order); + const fallback = manifest.fallbackTopic; + const primary = candidates[0] || { + rule: null, + score: 0, + confidence: "low", + matchedPhrases: [], + topics: fallback ? [fallback] : [], + fallback: true, + }; + return { + request: input.request || input.query || "", + task: input.task || null, + primary, + alternatives: candidates.slice(1), + candidates, + manifestVersion: manifest.version, + }; +} + +function expand(cwd, result) { + const { value: manifest } = loadManifest(cwd); + const dependencies = manifest.topicDependencies || {}; + const topics = []; + const seen = new Set(); + const visit = (topic) => { + if (!topic || seen.has(topic)) return; + seen.add(topic); + for (const dependency of dependencies[topic] || []) visit(dependency); + topics.push(topic); + }; + for (const topic of result?.primary?.topics || []) visit(topic); + for (const candidate of result?.alternatives || []) { + for (const topic of candidate.topics || []) visit(topic); + } + return { ...result, topics }; +} + +function verify(cwd, result, options = {}) { + const { value: manifest } = loadManifest(cwd); + const missing = []; + for (const topic of result?.topics || result?.primary?.topics || []) { + const topicPath = manifest.topicPaths?.[topic]; + if (!topicPath || !fs.existsSync(path.join(cwd, topicPath))) { + missing.push({ kind: "topic", id: topic, path: topicPath || null }); + } + } + for (const required of options.requiredContext || []) { + const file = path.isAbsolute(required) ? required : path.join(cwd, required); + if (!fs.existsSync(file)) missing.push({ kind: "context", path: required }); + } + return { + ok: missing.length === 0, + missing, + confidence: result?.primary?.confidence || "low", + fallback: Boolean(result?.primary?.fallback), + }; +} + +function loadContext(cwd, result, options = {}) { + const { value: manifest } = loadManifest(cwd); + const maxFiles = Number.isFinite(options.maxFiles) ? options.maxFiles : 20; + const maxLines = Number.isFinite(options.maxLines) ? options.maxLines : 400; + const files = []; + let lineCount = 0; + for (const topic of result?.topics || result?.primary?.topics || []) { + const relative = manifest.topicPaths?.[topic]; + if (!relative || files.length >= maxFiles || lineCount >= maxLines) continue; + const file = path.join(cwd, relative); + if (!fs.existsSync(file)) continue; + const lines = fs.readFileSync(file, "utf8").split(/\r?\n/); + const remaining = Math.max(0, maxLines - lineCount); + const content = lines.slice(0, remaining).join("\n"); + lineCount += content ? content.split(/\r?\n/).length : 0; + files.push({ topic, path: relative, content, truncated: lines.length > remaining }); + } + return { files, lineCount, truncated: files.length < (result?.topics || []).length }; +} + +module.exports = { + loadManifest, + match, + expand, + verify, + loadContext, +}; diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..92280d2 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,27 @@ +{ + "name": "@double-coding/flow2spec-core", + "version": "3.3.0", + "description": "Flow2Spec Core APIs, knowledge engine, project initialization and shared resources", + "homepage": "https://github.com/double-coding-lab/Flow2Spec#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/double-coding-lab/Flow2Spec.git", + "directory": "packages/core" + }, + "main": "./index.js", + "files": [ + "index.js", + "lib", + "templates", + "capabilities.json", + "README.md" + ], + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "engines": { + "node": ">=16" + }, + "license": "ISC" +} diff --git a/packages/core/templates/en-US/AGENTS.codex-stub.md b/packages/core/templates/en-US/AGENTS.codex-stub.md new file mode 100644 index 0000000..59590dc --- /dev/null +++ b/packages/core/templates/en-US/AGENTS.codex-stub.md @@ -0,0 +1,21 @@ +# Flow2Spec (`.codex/` Directory Notes) + +> This file is a **pointer**, not the complete instruction set. It is written by `flow2spec init`; **do not read only this file**. + +## Complete Instructions + +The repository-root **[`AGENTS.md`](../AGENTS.md)** is the complete Flow2Spec project guide. Codex reads it when started from the repository root. + +If the current session does not include the full root `AGENTS.md`, **you must first Read the repository-root `AGENTS.md`** before running `f2s-*` or modifying `.Knowledge/`. + +## Directory Purpose + +| Path | Description | +| --- | --- | +| `skills/` | Flow2Spec skills (`f2s-*`) | +| `topics/` | Long-form rule mirrors, sourced from the same content as Cursor/Claude `rules` | +| `hooks.json` | Codex SessionStart hook configuration for injecting a configuration summary and checking the Flow2Spec knowledge-base version on startup | +| `hooks/` | Hook script directory | +| `config.toml` | Project-level Codex configuration, if created | + +Configuration source of truth: repository-root **`flow2spec.config.json`** (must be Read); the field-semantics table is in root **`AGENTS.md`**. diff --git a/packages/core/templates/en-US/AGENTS.md b/packages/core/templates/en-US/AGENTS.md new file mode 100644 index 0000000..e7aaf36 --- /dev/null +++ b/packages/core/templates/en-US/AGENTS.md @@ -0,0 +1,90 @@ +# Flow2Spec Project Entry + +This file is written by `flow2spec init` to repository-root **`./AGENTS.md`** as the Codex project entry. **`./.codex/AGENTS.md`** is only a pointer. The knowledge-base root is **`./.Knowledge/`**. + +## Do These Two Things First + +1. **On the first repository-related turn in this conversation, read `./.Knowledge/manifest-routing.json`.** +2. **Before executing any `f2s-*` skill, `Read("flow2spec.config.json")`.** + +```text +Must execute: Read(".Knowledge/manifest-routing.json") +Must execute: Read("flow2spec.config.json") <- only before entering an f2s-* skill +``` + +Do not enter any `f2s-*` skill-body step before reading `flow2spec.config.json`. + +## Configuration Switches (disk is authoritative) + +The table below explains field semantics and the defaults written by `flow2spec init`; the source of truth is the result of `Read("flow2spec.config.json")` in this turn (the user may have edited values). + +{{FLOW2SPEC_PROJECT_CONFIG}} + +- When `subAgent=true`, the main agent must make **one explicit split/no-split decision** near the start of the skill body and state why; even when deciding not to split, it must output the no-split reason. When `subAgent=false`, do not split to sub-agents. +- When `intentRecognition=false` or the field is missing, do not auto-enter any skill; enter only on explicit user trigger or high-confidence routing allowed by current rules. + +For the detailed config table and supplemental rules, see **`./.codex/topics/f2s-config-check.md`**. + +## KB Routing Rules + +- The machine-readable source of truth is only **`./.Knowledge/manifest-routing.json`** plus the **`./.Knowledge/matchers/*.json`** file pointed to by each `matcherPath`. +- Execute `match -> expand -> verify -> act`: after the primary match, expand `topicDependencies`, then check for missing critical context. +- Cross-matcher full supplemental search is allowed only when there is no hit, the top candidates are too close, the gap check fails, or the user explicitly asks for a full check. +- `fallbackTopic` is only a low-confidence fallback and is not final execution authority. + +## Ordinary-Q&A Closing Gate + +- If ordinary Q&A / troubleshooting / explanation needs to drill into business source code, first follow **`./.codex/topics/f2s-knowledge-preflight.md`** for the initial read and gap note. +- If this turn read business source code and the final answer cites source-code facts, run the four-case closing in **`./.codex/topics/f2s-kb-feedback-closing.md`** before sending the answer; the answer must explicitly append either **`Knowledge-base follow-up suggestion`** or **`Knowledge base already covers this`**. Do not silently omit the closing marker. +- If this turn already entered an `f2s-*` skill, `implement-tech-design`, `f2s-git-commit`, or another existing follow-up flow, do not append the ordinary-Q&A closing prompt again. + +## Progressive Reading Order + +1. `./.Knowledge/manifest-routing.json` +2. The matched `./.Knowledge/matchers/.json` +3. The relevant `./.Knowledge/topics/.md` +4. Only if the topic points there or context is still missing, read `./.Knowledge/index.md` / `stock-docs` / `req-docs` +5. Drill into business code last + +Do not skip `manifest-routing.json` and jump straight to full-repository search. +Do not use `./.Knowledge/stock-docs/` as the direct input for implementing code from a spec. +Within the same task line, do not repeatedly reread the full manifest unless the user explicitly says routing/knowledge changed. + +## Execution Authority + +Flow2Spec execution authority is limited to: + +- repository-root **`./AGENTS.md`** +- **`./.codex/topics/f2s-*.md`** +- **`./.codex/skills/`** + +**`.codex/AGENTS.md`** is only a pointer and cannot replace root `AGENTS.md`. + +## Codex Rule Mirrors (open on demand) + +These files are mirrored by `flow2spec init codex` from rule templates into `.codex/topics/`. They are not automatically loaded in full; open them only when the current task needs the details. + +| Rule | Path | When to read | +| --- | --- | --- | +| Unified entry | `./.codex/topics/f2s-flow2spec-unified-entry.md` | When executing an `f2s-*` skill or deciding KB routing, sub-agent, or verification semantics | +| Config preflight | `./.codex/topics/f2s-config-check.md` | When checking `flow2spec.config.json`, `subAgent`, or `changeTracking` details | +| Ordinary-Q&A initial gate | `./.codex/topics/f2s-knowledge-preflight.md` | Before ordinary Q&A drills into source code | +| Ordinary-Q&A closing | `./.codex/topics/f2s-kb-feedback-closing.md` | After ordinary Q&A reads source code and may need a KB follow-up suggestion | +| Intent routing | `./.codex/topics/f2s-intent-routing.md` | Only when `intentRecognition=true` and deciding whether to auto-enter a skill | + +Open long-form topics such as `implement-tech-design` or `f2s-doc-routing` only when the matched topic requires them. + +## Codex Hooks + +`flow2spec init codex` writes **`.codex/hooks.json`**. In Codex, Flow2Spec currently uses hooks only for: + +- `SessionStart` configuration-summary reminder: `.codex/hooks/f2s-config-session.js` +- `SessionStart` knowledge-base version check: `.codex/hooks/f2s-update-check.js` + +These hooks are only reminders / checks. They do not replace `Read("flow2spec.config.json")` or the KB routing gate. + +## Flow2Spec Skills + +Available skills live under **`./.codex/skills/`**. Enter a skill only when the user explicitly triggers it or the current routing rules allow automatic entry. + +{{FLOW2SPEC_CODEX_SKILLS_SUMMARY}} diff --git a/packages/core/templates/en-US/flow2spec.config.json b/packages/core/templates/en-US/flow2spec.config.json new file mode 100644 index 0000000..92e2810 --- /dev/null +++ b/packages/core/templates/en-US/flow2spec.config.json @@ -0,0 +1,18 @@ +{ + "locale": "en-US", + "subAgent": true, + "switchAgentVerification": true, + "intentRecognition": true, + "changeTracking": { + "feat": true, + "fix": false, + "implement": true + }, + "updateCheck": { + "enabled": true + }, + "collaboration": { + "enabled": true, + "developerId": "" + } +} diff --git a/packages/core/templates/en-US/hooks/f2s-config-inject.js b/packages/core/templates/en-US/hooks/f2s-config-inject.js new file mode 100644 index 0000000..304722d --- /dev/null +++ b/packages/core/templates/en-US/hooks/f2s-config-inject.js @@ -0,0 +1,43 @@ +#!/usr/bin/env node +'use strict'; +/** + * flow2spec PreToolUse guard — only reminds before invoking an f2s-* Skill that flow2spec.config.json must be Read first. + * Does not repeatedly inject the full configuration during PreToolUse; the configuration summary is provided once by the SessionStart hook. + * Written by flow2spec init --claude to .claude/hooks/f2s-config-inject.js. + */ + +function emitAdditionalContext(lines) { + process.stdout.write( + JSON.stringify({ + hookSpecificOutput: { + hookEventName: 'PreToolUse', + additionalContext: lines.join('\n'), + }, + }) + '\n', + ); +} + +const chunks = []; +process.stdin.on('data', (d) => chunks.push(d)); +process.stdin.on('end', () => { + let skillName = ''; + try { + const input = JSON.parse(Buffer.concat(chunks).toString('utf8')); + skillName = String(input?.tool_input?.skill || input?.tool_input?.name || ''); + } catch (_err) { + process.exit(0); + return; + } + + if (!/^f2s-/.test(skillName)) { + process.exit(0); + return; + } + + emitAdditionalContext([ + `[flow2spec] About to invoke ${skillName}. Before entering that Skill body, the first action must be Read("flow2spec.config.json").`, + 'The configuration summary from SessionStart is only a reminder; if it differs from disk, use the result of this Read.', + 'After reading, continue according to the actual subAgent / switchAgentVerification / changeTracking values.', + ]); + process.exit(0); +}); diff --git a/packages/core/templates/en-US/hooks/f2s-config-session.js b/packages/core/templates/en-US/hooks/f2s-config-session.js new file mode 100644 index 0000000..acfc211 --- /dev/null +++ b/packages/core/templates/en-US/hooks/f2s-config-session.js @@ -0,0 +1,95 @@ +#!/usr/bin/env node +'use strict'; +/** + * flow2spec SessionStart hook — injects a flow2spec.config.json summary once at session start. + * This summary does not replace Read("flow2spec.config.json") before an f2s-* Skill body. + * Written by flow2spec init to the corresponding agent's hooks/f2s-config-session.js. + */ +const fs = require('fs'); +const path = require('path'); + +const DEFAULT_CFG = { + subAgent: false, + switchAgentVerification: false, + changeTracking: { feat: true, fix: false, implement: true }, +}; + +function normalizeBool(value, fallback) { + if (value === true || value === 'true' || value === 1 || value === '1') + return true; + if (value === false || value === 'false' || value === 0 || value === '0') + return false; + return fallback; +} + +function normalizeCfg(raw) { + if (!raw || typeof raw !== 'object' || Array.isArray(raw)) { + return { ...DEFAULT_CFG, changeTracking: { ...DEFAULT_CFG.changeTracking } }; + } + const ct = raw.changeTracking; + let changeTracking = { ...DEFAULT_CFG.changeTracking }; + if (typeof ct === 'boolean') { + changeTracking = { + feat: normalizeBool(ct, DEFAULT_CFG.changeTracking.feat), + fix: normalizeBool(ct, DEFAULT_CFG.changeTracking.fix), + implement: normalizeBool(ct, DEFAULT_CFG.changeTracking.implement), + }; + } else if (ct && typeof ct === 'object' && !Array.isArray(ct)) { + changeTracking = { + feat: normalizeBool(ct.feat, DEFAULT_CFG.changeTracking.feat), + fix: normalizeBool(ct.fix, DEFAULT_CFG.changeTracking.fix), + implement: normalizeBool(ct.implement, DEFAULT_CFG.changeTracking.implement), + }; + } + const switchRaw = Object.prototype.hasOwnProperty.call(raw, 'switchAgentVerification') + ? raw.switchAgentVerification + : raw.subAgentVerification; + return { + subAgent: normalizeBool(raw.subAgent, DEFAULT_CFG.subAgent), + switchAgentVerification: normalizeBool( + switchRaw, + DEFAULT_CFG.switchAgentVerification, + ), + changeTracking, + }; +} + +function emit(lines) { + process.stdout.write( + JSON.stringify({ + hookSpecificOutput: { + hookEventName: 'SessionStart', + additionalContext: lines.join('\n'), + }, + }) + '\n', + ); +} + +function main() { + const configPath = path.resolve(process.cwd(), 'flow2spec.config.json'); + if (!fs.existsSync(configPath)) { + const cfg = { ...DEFAULT_CFG, changeTracking: { ...DEFAULT_CFG.changeTracking } }; + emit([ + '[flow2spec] flow2spec.config.json was not found for this session; before any f2s-* Skill, still attempt Read("flow2spec.config.json"). Missing fields use defaults.', + `Configuration summary: subAgent=${cfg.subAgent}, switchAgentVerification=${cfg.switchAgentVerification}, changeTracking=${JSON.stringify(cfg.changeTracking)}`, + ]); + return; + } + + try { + const cfg = normalizeCfg(JSON.parse(fs.readFileSync(configPath, 'utf8'))); + emit([ + '[flow2spec] SessionStart configuration summary (reminder only; before executing any f2s-* Skill, you must still Read the disk file):', + `subAgent=${cfg.subAgent}`, + `switchAgentVerification=${cfg.switchAgentVerification}`, + `changeTracking=${JSON.stringify(cfg.changeTracking)}`, + ]); + } catch (err) { + emit([ + `[flow2spec] Failed to parse flow2spec.config.json: ${err.message || String(err)}`, + 'Before executing any f2s-* Skill, you must first fix or Read this file; if it cannot be read, missing fields use defaults.', + ]); + } +} + +main(); diff --git a/packages/core/templates/en-US/hooks/f2s-update-check.js b/packages/core/templates/en-US/hooks/f2s-update-check.js new file mode 100644 index 0000000..3f390a3 --- /dev/null +++ b/packages/core/templates/en-US/hooks/f2s-update-check.js @@ -0,0 +1,187 @@ +#!/usr/bin/env node +'use strict'; +/** + * flow2spec SessionStart hook — checks for version updates on the first conversation each day. + * Compares the local knowledge-base manifest-routing.json version with the latest npm version: + * - Same or local is newer -> exit silently + * - Behind -> inject one notice into Agent context (suggest running f2s-kb-upgrade) + * If already checked and no upgrade is needed, stay silent; if already checked and an upgrade is still needed, keep injecting the reminder in each new session. + * Written by flow2spec init to the corresponding agent's hooks/f2s-update-check.js. + */ +const fs = require('fs'); +const path = require('path'); +const { execFileSync } = require('child_process'); + +const MANIFEST_PATH = path.join(process.cwd(), '.Knowledge', 'manifest-routing.json'); +const CACHE_DIR = path.join(process.cwd(), '.Knowledge'); +const CACHE_FILE = path.join(CACHE_DIR, 'update-check.json'); +const PACKAGE_NAME_PLACEHOLDER = '__FLOW2SPEC_' + 'PACKAGE_NAME__'; +const PACKAGE_NAME = '__FLOW2SPEC_PACKAGE_NAME__'; + +// ── Cache ─────────────────────────────────────────────────────────────────── + +function readCache() { + if (!fs.existsSync(CACHE_FILE)) return null; + try { + const d = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8')); + if (!d || typeof d !== 'object') return null; + const checkedAt = Number(d.checkedAt || 0); + if (!checkedAt) return null; + if (new Date(checkedAt).toDateString() !== new Date().toDateString()) return null; + return d; + } catch (_) { return null; } +} + +function getProjectName() { + try { + const pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8')); + if (pkg && pkg.name) return String(pkg.name); + } catch (_) {} + return path.basename(process.cwd()); +} + +function buildNotice(latestNpm, manifestVersion) { + const userMsg = `📦 [flow2spec] The current project "${getProjectName()}" knowledge-base version is v${manifestVersion}, which is lower than the latest package version v${latestNpm}. You can run the f2s-kb-upgrade skill to align templates and routing.`; + return [ + `[flow2spec/agent-instruction] At the start of this response, you must relay the following upgrade notice to the user verbatim. Do not omit it or skip it after only internal judgment:`, + ``, + userMsg, + ].join('\n'); +} + +function emitNotice(notice) { + process.stdout.write( + JSON.stringify({ + additional_context: notice, + hookSpecificOutput: { + hookEventName: 'SessionStart', + additionalContext: notice, + }, + }) + '\n' + ); +} + +function writeCache(latestNpm, manifestVersion) { + try { + const needsUpgrade = cmpVer(manifestVersion, latestNpm) < 0; + fs.writeFileSync( + CACHE_FILE, + `${JSON.stringify({ + latestNpm, + manifestVersion, + needsUpgrade, + notice: needsUpgrade ? buildNotice(latestNpm, manifestVersion) : '', + checkedAt: Date.now(), + }, null, 2)}\n`, + 'utf8' + ); + } catch (_) {} +} + +function deleteCache() { + try { + if (fs.existsSync(CACHE_FILE)) fs.unlinkSync(CACHE_FILE); + } catch (_) {} +} + +// ── Version comparison ─────────────────────────────────────────────────────── + +function parseVer(v) { + return String(v || '').replace(/^v/, '').split(/[.-]/).slice(0, 3).map((p) => { + const n = Number.parseInt(p, 10); + return Number.isFinite(n) ? n : 0; + }); +} + +/** a < b -> negative; a === b -> 0; a > b -> positive */ +function cmpVer(a, b) { + const av = parseVer(a), bv = parseVer(b); + for (let i = 0; i < 3; i++) { + const d = (av[i] || 0) - (bv[i] || 0); + if (d !== 0) return d; + } + return 0; +} + +// ── Reads ──────────────────────────────────────────────────────────────────── + +function getManifestVersion() { + if (!fs.existsSync(MANIFEST_PATH)) return null; + try { + return JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8')).version || null; + } catch (_) { return null; } +} + +function getPackageName() { + if (PACKAGE_NAME && PACKAGE_NAME !== PACKAGE_NAME_PLACEHOLDER) { + return PACKAGE_NAME; + } + return '@double-coding/flow2spec'; +} + +function queryNpmLatest(pkgName) { + return execFileSync('npm', ['view', pkgName, 'version'], { + encoding: 'utf8', + timeout: 5000, + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); +} + +// ── Configuration switch ───────────────────────────────────────────────────── + +function isEnabled() { + try { + const cfg = JSON.parse(fs.readFileSync( + path.join(process.cwd(), 'flow2spec.config.json'), 'utf8' + )); + const uc = cfg && cfg.updateCheck; + if (uc && typeof uc.enabled === 'boolean') return uc.enabled; + return true; + } catch (_) { return true; } +} + +// ── Main flow ──────────────────────────────────────────────────────────────── + +function main() { + if (process.env.CI || process.env.CONTINUOUS_INTEGRATION) return; + if (!isEnabled()) return; + const cache = readCache(); + if (cache) { + // If already checked today, do not query npm again; if the cache still says an upgrade is needed, keep reminding in each new session. + const needsUpgrade = cache.needsUpgrade === true || + cmpVer(cache.manifestVersion, cache.latestNpm) < 0; + if (needsUpgrade) { + const currentManifestVersion = getManifestVersion(); + if (currentManifestVersion && cache.latestNpm && + cmpVer(currentManifestVersion, cache.latestNpm) >= 0) { + deleteCache(); + return; + } + // SessionStart enters a new session: cache hit and upgrade still needed, so emit directly. + const notice = buildNotice(cache.latestNpm, cache.manifestVersion); + emitNotice(notice); + } + return; + } + + const manifestVersion = getManifestVersion(); + if (!manifestVersion) return; // No knowledge base, skip + + let latestNpm; + try { + const pkgName = getPackageName(); + latestNpm = queryNpmLatest(pkgName); + } catch (_) { + return; // Network unavailable, exit silently without writing cache (retry next time) + } + + // Write cache (whether or not upgrade is needed, do not repeat the check today) + writeCache(latestNpm, manifestVersion); + + if (cmpVer(manifestVersion, latestNpm) >= 0) return; // Already up to date + + const notice = buildNotice(latestNpm, manifestVersion); + emitNotice(notice); +} + +main(); diff --git a/packages/core/templates/en-US/knowledge/index.md b/packages/core/templates/en-US/knowledge/index.md new file mode 100644 index 0000000..f31add5 --- /dev/null +++ b/packages/core/templates/en-US/knowledge/index.md @@ -0,0 +1,71 @@ +# Flow2Spec Knowledge Index + +> **Path convention**: paths such as **`.Knowledge/`** and **`manifest-routing.json`** below are relative to **this repository root** (the current project where `flow2spec init` has been run). + +This file is **human-readable navigation**: topic descriptions, related-document summaries, and semantic boundaries. +The **machine-readable source of truth** is `.Knowledge/manifest-routing.json` plus the `.Knowledge/matchers/*.json` shards pointed to by `taskToTopicRules[].matcherPath` (`.Knowledge/manifest-matchers.json` is no longer used). + +--- + +## Recommended Reading Order + +1. `.Knowledge/manifest-routing.json` (task routing, `topicPaths`, `topicDependencies`, `fallbackTopic`) +2. As needed: read `.Knowledge/matchers/.json` from `matcherPath` (`includeAny` keywords) +3. As needed: this `index.md` (topic semantics and boundaries) +4. `.Knowledge/topics/.md` (execution constraints and flows) +5. As needed: `.Knowledge/stock-docs/`, `.Knowledge/req-docs/` +6. Drill into business code only if still insufficient + +--- + +## Topic Overview + +| Topic | Path | Applies when | Related documents (summary) | +| --- | --- | --- | --- | +| implement-tech-design | `.Knowledge/topics/f2s-implement-tech-design.md` | Implement code from a technical spec | req: [technical spec](.Knowledge/req-docs/.md) (required) | +| f2s-doc-routing | `.Knowledge/topics/f2s-stock-docs-vs-req-docs.md` | stock-docs / req-docs directory responsibilities | stock: [directory boundary notes](.Knowledge/stock-docs/.md) (optional) | +| fallback-triage | `.Knowledge/topics/f2s-fallback-triage.md` | No hit or low confidence: triage and clarification | stock: [routing triage notes](.Knowledge/stock-docs/.md) (optional) | +| config-precheck | `.Knowledge/topics/f2s-config-precheck.md` | Read `flow2spec.config.json` / orchestration switches before executing `f2s-*` | Codex long-form: repository-root `.codex/topics/f2s-config-check.md`; [routing summary](topics/f2s-config-precheck.md) | +| f2s-task | `.Knowledge/topics/f2s-task.md` | Change tracking, `.task/` task lists, and cross-session resume | Long-form: configuration-root `rules/f2s-task.*`; Codex: `.codex/topics/f2s-task.md` | +| f2s-req-plan | `.Knowledge/topics/f2s-req-plan.md` | Requirement/spec planning and implementation; always maintain `.task/` | Skill: `skills/f2s-req-plan/SKILL.md`; depends on `f2s-task` | +| flow2spec-dsh-adapter | `.Knowledge/topics/flow2spec-dsh-adapter.md` | `flow2spec init dsh` and DeepSeek Harness project skill discovery | User guide: `docs/en/usage-guide.md`; implementation: `lib/dshAgentsAdapter.js` | + +Keep **1-3** clickable summary links per topic. Full path mappings are written to `.Knowledge/migration-report.md` in migration scenarios. +Among these, **`implement-tech-design`**, **`f2s-doc-routing`**, **`config-precheck`**, and **`f2s-task`** are **routing summaries** under `topics/`; long-form execution instructions live in configuration-root **`rules/f2s-*.md(c)`**. When using Codex, see **`.codex/AGENTS.md`** and **`.codex/topics/f2s-*.md`** (`f2s-config-check` shares the same pre-step source as `AGENTS`; open on demand). **`f2s-knowledge-preflight`** and **`f2s-kb-feedback-closing`** are gates for initial reads in ordinary Q&A and closure after source-code supplementation. They are effective as configuration-root rules / Codex long-form topics and are not written into `topicPaths` or `taskToTopicRules`. + +--- + +## Match and Execute (consistent with the unified entry) + +- **Routing**: `taskToTopicRules` maps tasks to topic sets; **keywords** live in matcher-shard `includeAny`. +- **Dependencies**: before using the main topic, read dependency topics according to `topicDependencies`. +- **Fallback**: `fallbackTopic` points to a triage topic (such as `fallback-triage`) and is only low-confidence context. It **must not** be treated as a final hit for direct code changes. +- **Execution chain**: `match → expand → verify → act`; `expand` must include dependency expansion and keep the next-highest candidate for validation. +- **Full supplemental search**: cross-matcher supplemental search is allowed only when there is no hit, candidate margins are too small, the gap check fails, or the user explicitly requests a "full check". + +--- + +## Directory Responsibilities + +| Directory | Responsibility | +| --- | --- | +| `topics/` | Topic rules and execution flows | +| `matchers/` | Matcher shards (pointed to by `matcherPath`) | +| `stock-docs/` | Existing knowledge deposits (architecture, final drafts, etc.) | +| `req-docs/` | Requirements and technical specs (implementation drivers) | +| `template/` | Final-draft and spec templates | + +The routing manifest is maintained by `f2s-*` skill flows and does not depend on an additional CLI subcommand. + +--- + +## How to Handle Common Gaps (consistent with the unified entry) + +| Situation | What to do | +| --- | --- | +| Docs exist but are not routed (1a) | Maintenance side: use `f2s-kb-build` / `f2s-kb-sync` / `f2s-kb-add` to supplement routing and `includeAny`. Execution side: use the triage topic to clarify task type; **do not** replace the manifest with full-repository scanning. | +| Routed but insufficient (1b) | Follow dependencies and next-highest candidates -> in `verify`, name the missing document; if still missing, ask the user for the path or add `req-docs`. | +| Not in the KB (2) | Acknowledge the gap -> drill into code or ask the user to add requirement/spec documents. | +| Repeated manifest reads waste tokens (2a) | Within the same task line, treat routing as a snapshot; read only the single matcher for the hit; do not enumerate the entire `matchers/` directory; do not repeatedly refresh `index.md` and routing against each other. | + +**Note**: "routing/knowledge has been updated" means output from `f2s-*` flows (such as `f2s-kb-build`, `f2s-kb-sync`, `f2s-kb-add`, `f2s-kb-fix`) or manual edits to `manifest-routing` / `matchers` shards. **`flow2spec init` does not author business documents**; it mainly fills templates and writes configuration roots. Do not confuse it with knowledge-base content updates. diff --git a/packages/core/templates/en-US/knowledge/manifest-matchers.json b/packages/core/templates/en-US/knowledge/manifest-matchers.json new file mode 100644 index 0000000..89607ce --- /dev/null +++ b/packages/core/templates/en-US/knowledge/manifest-matchers.json @@ -0,0 +1,92 @@ +{ + "version": "1.0.0", + "generatedFrom": ".Knowledge/manifest-routing.json", + "matcherKey": "matcherId", + "sourceOfTruth": ".Knowledge/manifest-routing.json", + "matchers": { + "m-implement-from-spec": { + "includeAny": [ + "按技术方案实现", + "实现接口", + "需求文档开发", + "implement from technical spec", + "implement API", + "develop from requirement document", + "build from spec" + ] + }, + "m-doc-routing": { + "includeAny": [ + "文档放哪", + "stock-docs", + "req-docs", + "目录约定", + "where should docs go", + "document routing", + "directory convention", + "directory responsibilities" + ] + }, + "m-f2s-config-precheck": { + "includeAny": [ + "flow2spec.config.json", + "subAgent", + "switchAgentVerification", + "切换 agent 校验", + "技能前置", + "f2s-config-check", + "f2s-config-inject", + "changeTracking", + "agent switch verification", + "skill precheck", + "skill pre-step", + "configuration precheck" + ] + }, + "m-change-tracking": { + "includeAny": [ + "changeTracking", + "变更追踪", + "任务追踪", + "任务清单", + ".task", + "续作", + "继续上次任务", + "todo.json", + "task.md", + "f2s-task", + "任务清单归档", + "跨会话", + "change tracking", + "task tracking", + "task list", + "resume task", + "continue previous task", + "archive task list", + "cross-session" + ] + }, + "m-req-plan": { + "includeAny": [ + "f2s-req-plan", + "任务规划", + "任务清单设计", + "需求规划", + "创建任务清单", + "规划需求", + "req-plan", + "需求实现", + "按需求实现", + "按方案规划", + "task planning", + "task list design", + "requirement planning", + "create task list", + "plan requirement", + "requirement implementation", + "implement requirement", + "plan from spec" + ] + } + } +} diff --git a/packages/core/templates/en-US/knowledge/manifest-routing.json b/packages/core/templates/en-US/knowledge/manifest-routing.json new file mode 100644 index 0000000..41feb7f --- /dev/null +++ b/packages/core/templates/en-US/knowledge/manifest-routing.json @@ -0,0 +1,110 @@ +{ + "version": "3.1.5", + "projectRev": 2, + "knowledgeRoot": ".Knowledge", + "matcherKey": "matcherId", + "sourceOfTruth": ".Knowledge/manifest-routing.json", + "fallbackTopic": "fallback-triage", + "topicDependencies": { + "implement-tech-design": [ + "f2s-doc-routing" + ], + "f2s-req-plan": [ + "f2s-task" + ] + }, + "topicMetadata": { + "implement-tech-design": { + "primary": "policy", + "confidence": "manual" + }, + "f2s-doc-routing": { + "primary": "policy", + "confidence": "manual" + }, + "fallback-triage": { + "primary": "policy", + "confidence": "manual" + }, + "config-precheck": { + "primary": "config", + "tags": [ + "policy" + ], + "confidence": "manual" + }, + "f2s-task": { + "primary": "policy", + "confidence": "manual" + }, + "f2s-req-plan": { + "primary": "policy", + "confidence": "manual" + }, + "flow2spec-dsh-adapter": { + "primary": "feature", + "confidence": "inferred", + "tags": ["module"] + } + }, + "topicPaths": { + "implement-tech-design": ".Knowledge/topics/f2s-implement-tech-design.md", + "f2s-doc-routing": ".Knowledge/topics/f2s-stock-docs-vs-req-docs.md", + "fallback-triage": ".Knowledge/topics/f2s-fallback-triage.md", + "config-precheck": ".Knowledge/topics/f2s-config-precheck.md", + "f2s-task": ".Knowledge/topics/f2s-task.md", + "f2s-req-plan": ".Knowledge/topics/f2s-req-plan.md", + "flow2spec-dsh-adapter": ".Knowledge/topics/flow2spec-dsh-adapter.md" + }, + "taskToTopicRules": [ + { + "task": "f2s-config-precheck", + "matcherId": "m-f2s-config-precheck", + "matcherPath": ".Knowledge/matchers/m-f2s-config-precheck.json", + "topics": [ + "config-precheck" + ] + }, + { + "task": "implement-from-spec", + "matcherId": "m-implement-from-spec", + "matcherPath": ".Knowledge/matchers/m-implement-from-spec.json", + "topics": [ + "f2s-doc-routing", + "implement-tech-design" + ] + }, + { + "task": "doc-routing", + "matcherId": "m-doc-routing", + "matcherPath": ".Knowledge/matchers/m-doc-routing.json", + "topics": [ + "f2s-doc-routing" + ] + }, + { + "task": "change-tracking", + "matcherId": "m-change-tracking", + "matcherPath": ".Knowledge/matchers/m-change-tracking.json", + "topics": [ + "f2s-task" + ] + }, + { + "task": "req-plan", + "matcherId": "m-req-plan", + "matcherPath": ".Knowledge/matchers/m-req-plan.json", + "topics": [ + "f2s-req-plan" + ] + }, + { + "task": "flow2spec-dsh-adapter", + "matcherId": "m-flow2spec-dsh-adapter", + "matcherPath": ".Knowledge/matchers/m-flow2spec-dsh-adapter.json", + "topics": [ + "flow2spec-dsh-adapter" + ] + } + ] +} diff --git a/packages/core/templates/en-US/knowledge/matchers/m-change-tracking.json b/packages/core/templates/en-US/knowledge/matchers/m-change-tracking.json new file mode 100644 index 0000000..bc34b5c --- /dev/null +++ b/packages/core/templates/en-US/knowledge/matchers/m-change-tracking.json @@ -0,0 +1,29 @@ +{ + "id": "m-change-tracking", + "includeAny": [ + "changeTracking", + "变更追踪", + "任务追踪", + "任务清单", + ".task", + "续作", + "继续上次任务", + "todo.json", + "task.md", + "f2s-task", + "任务清单归档", + "跨会话", + "验收清单", + "acceptance.md", + "归档前验收", + "change tracking", + "task tracking", + "task list", + "resume task", + "continue previous task", + "archive task list", + "cross-session", + "acceptance checklist", + "acceptance before archive" + ] +} diff --git a/packages/core/templates/en-US/knowledge/matchers/m-doc-routing.json b/packages/core/templates/en-US/knowledge/matchers/m-doc-routing.json new file mode 100644 index 0000000..698e9d3 --- /dev/null +++ b/packages/core/templates/en-US/knowledge/matchers/m-doc-routing.json @@ -0,0 +1,15 @@ +{ + "id": "m-doc-routing", + "version": "1.0.0", + "schema": "flow2spec.matcher.v1", + "includeAny": [ + "文档放哪", + "stock-docs", + "req-docs", + "目录约定", + "where should docs go", + "document routing", + "directory convention", + "directory responsibilities" + ] +} diff --git a/packages/core/templates/en-US/knowledge/matchers/m-f2s-config-precheck.json b/packages/core/templates/en-US/knowledge/matchers/m-f2s-config-precheck.json new file mode 100644 index 0000000..5023435 --- /dev/null +++ b/packages/core/templates/en-US/knowledge/matchers/m-f2s-config-precheck.json @@ -0,0 +1,19 @@ +{ + "includeAny": [ + "flow2spec.config.json", + "subAgent", + "switchAgentVerification", + "切换 agent 校验", + "技能前置", + "f2s-config-check", + "f2s-config-inject", + "changeTracking", + "agent switch verification", + "skill precheck", + "skill pre-step", + "configuration precheck" + ], + "id": "m-f2s-config-precheck", + "version": "1.0.0", + "schema": "flow2spec.matcher.v1" +} diff --git a/packages/core/templates/en-US/knowledge/matchers/m-flow2spec-dsh-adapter.json b/packages/core/templates/en-US/knowledge/matchers/m-flow2spec-dsh-adapter.json new file mode 100644 index 0000000..e7a277e --- /dev/null +++ b/packages/core/templates/en-US/knowledge/matchers/m-flow2spec-dsh-adapter.json @@ -0,0 +1,6 @@ +{ + "includeAny": ["DeepSeek Harness", "deepseek-harness", "dsh", "flow2spec init dsh", ".dsh/skills", ".dsh/topics", "Cordis plugin", "Harness adapter"], + "id": "m-flow2spec-dsh-adapter", + "version": "1.0.0", + "schema": "flow2spec.matcher.v1" +} diff --git a/packages/core/templates/en-US/knowledge/matchers/m-implement-from-spec.json b/packages/core/templates/en-US/knowledge/matchers/m-implement-from-spec.json new file mode 100644 index 0000000..a7cc298 --- /dev/null +++ b/packages/core/templates/en-US/knowledge/matchers/m-implement-from-spec.json @@ -0,0 +1,14 @@ +{ + "id": "m-implement-from-spec", + "version": "1.0.0", + "schema": "flow2spec.matcher.v1", + "includeAny": [ + "按技术方案实现", + "实现接口", + "需求文档开发", + "implement from technical spec", + "implement API", + "develop from requirement document", + "build from spec" + ] +} diff --git a/packages/core/templates/en-US/knowledge/matchers/m-req-plan.json b/packages/core/templates/en-US/knowledge/matchers/m-req-plan.json new file mode 100644 index 0000000..2f3197c --- /dev/null +++ b/packages/core/templates/en-US/knowledge/matchers/m-req-plan.json @@ -0,0 +1,23 @@ +{ + "id": "m-req-plan", + "includeAny": [ + "f2s-req-plan", + "任务规划", + "任务清单设计", + "需求规划", + "创建任务清单", + "规划需求", + "req-plan", + "需求实现", + "按需求实现", + "按方案规划", + "task planning", + "task list design", + "requirement planning", + "create task list", + "plan requirement", + "requirement implementation", + "implement requirement", + "plan from spec" + ] +} diff --git a/packages/core/templates/en-US/knowledge/template/final-overview-template.md b/packages/core/templates/en-US/knowledge/template/final-overview-template.md new file mode 100644 index 0000000..2313784 --- /dev/null +++ b/packages/core/templates/en-US/knowledge/template/final-overview-template.md @@ -0,0 +1,102 @@ +> **Primary convention (unified knowledge base)**: final-draft templates and final documents are maintained in `.Knowledge/template/` and `.Knowledge/stock-docs/`. + +# Final Overview Template + +> This template organizes documents such as "architecture notes" and "feature/technical specs" into a **final-draft** form so the **f2s-kb-build** skill can update `.Knowledge/topics`, `.Knowledge/index.md`, and, as needed, the routing manifest (`manifest-routing` + `matchers/*.json`). +> Applies to backend services, frontend/client, full-stack, product, design notes, and similar documents. Keep or omit sections as needed. +> **In Flow2Spec**: the primary template path is `.Knowledge/template/final-overview-template.md`; configuration roots no longer receive a duplicate `template/` copy. +> **When executing the f2s-doc-final skill**: this template is only a **structural reference and writing prompt**, not a mandatory form. Conversion should primarily follow the original content and logic, adopting section suggestions as needed. + +--- + +## Core Concepts + +| Concept | Description | +|------|------| +| (Term 1) | Definition and purpose. | +| (Term 2) | Definition and purpose. | + +Use a table to list **terms, entities, and key IDs** so AI can extract them into Rules/Skills. For architecture notes, include directory conventions, module boundaries, shared capability entry points, and similar items. For feature specs, include domain entities, configuration keys, API/page names, and similar items. + +--- + +## States and Transitions + +(Fill this if there are states, phases, or lifecycle steps; otherwise summarize briefly or omit.) + +- **State A**: meaning; when it transitions to state B/C. +- **State B**: meaning; later transitions. + +If there is no complex state model, write "phases in the main flow" or "this document has no state machine; see key flows". + +--- + +## Business Rules + +- Rule 1: constraints, purchase limits, validity windows, permissions, and so on. +- Rule 2: validation dimensions, failure conditions, edge cases. +- Rule 3: relationships to configuration, flags, and environments. + +Clarify **constraints, validations, and configuration items** so Rules can generate "rule highlights". Frontend/backend behavior, configuration, data consistency, error handling, and similar items may all be listed here. + +--- + +## Key Flows + +1. **Flow 1**: brief steps; entry point (interface/API/page/event); result. +2. **Flow 2**: brief steps; entry point; result. +3. **Flow 3**: brief steps; entry point; result. + +Write the main flow from the "user side or system side" so AI can extract it into the "key flows" section of Skills. May include API call order, page navigation, event triggers, background tasks, and similar items. + +--- + +## Interfaces / APIs / Pages (optional) + +(Choose one or more based on document type: backend writes interfaces, frontend writes pages/components and data flow, full-stack can include both.) + +### Interface / API Name or Path + +- **Request**: input parameters (body/query/header). +- **Response**: output parameters; error codes or exception cases. +- **Internal calls**: in-project methods, services, or modules (if any). + +### Page / Component / Route (if any) + +- **Entry**: route, entry component, or event. +- **Dependent data**: API, Store, local state. +- **Output**: page/component responsibility and key interactions. + +--- + +## Configuration / Data / Errors (optional) + +(Include as needed; not all items must appear.) + +- **Configuration**: configuration items, keys, required/optional status, meaning; configuration-center or environment-variable conventions. +- **Data**: core data model, table structure, field notes; or frontend Store/state structure; relationship to the business. +- **Error codes or exceptions**: code, scenario, description; or frontend error states and fallback logic. + +--- + +## Implementation Location and Integration Method + +- **Implementation location**: code/service/repository path (such as `src/xxx/yyy`, service name, repository name), explaining where the wrapper or implementation lives. +- **Integration method**: how new features/businesses integrate: required configuration, table creation, route registration, component references, and similar steps; the minimal integration steps when no wrapper changes are needed. + +--- + +## Source Files + +> Original paths actually read when generating this final draft, for traceability and later updates. + +- `` +- `` + +--- + +### Usage Notes + +- Replace the placeholders in parentheses above with actual content; sections unrelated to the document type may be deleted entirely or marked "(not applicable)". +- Keep at least the three H2 headings **Core Concepts, Business Rules, Key Flows**; add or remove the rest as needed. +- After saving as `.Knowledge/stock-docs/_final.md`, run the **f2s-kb-build** skill with that path as input to update `.Knowledge/topics`, `.Knowledge/index.md`, and the routing manifest when needed. diff --git a/packages/core/templates/en-US/knowledge/template/project-milestone-template.md b/packages/core/templates/en-US/knowledge/template/project-milestone-template.md new file mode 100644 index 0000000..9dcb257 --- /dev/null +++ b/packages/core/templates/en-US/knowledge/template/project-milestone-template.md @@ -0,0 +1,32 @@ +> **Primary convention**: the template is at `.Knowledge/template/project-milestone-template.md` (written by `flow2spec init`); generated outputs go to `.Knowledge/stock-docs/-milestones.md`. +> **When executing `f2s-doc-milestone`**: structure follows this template; content comes only from **req-docs, git log, .task**, and knowledge-base topics. Do not invent content. +> **Milestone phases**: Mx is **only** for feature/capability changes. Integration testing, testing, acceptance, or pure environment/operations work must **not** become standalone phases; engineering changes are merged into the corresponding feature phase. + +# (Scope Name) Milestones + +> **Scope**: (user-provided semantic scope; if unspecified, write "entire project") +> **Updated**: `YYYY-MM-DD` + +## Overview + +| Phase | Time | Summary | +| --- | --- | --- | +| MN · (latest phase title) | YYYY-MM | (verifiable feature delivery summary; not integration testing/testing/acceptance) | +| … | … | … | +| M1 · (initial phase title) | YYYY-MM | … | + +## MN · (Latest Phase Title) + +- (Delivered feature items, one per line, verifiable) + +## … + +(Same structure as MN; each Mx row in the overview table must have a corresponding H2 heading; all in newest-first order.) + +## M1 · (Initial Phase Title) + +- (Delivered feature items, one per line, verifiable) + +## Pending Confirmation + +- (List functional/delivery gaps or inconsistencies from the four sources; write "None" if absent) diff --git a/packages/core/templates/en-US/knowledge/template/technical-spec-template.md b/packages/core/templates/en-US/knowledge/template/technical-spec-template.md new file mode 100644 index 0000000..320f35c --- /dev/null +++ b/packages/core/templates/en-US/knowledge/template/technical-spec-template.md @@ -0,0 +1,89 @@ +> **Primary convention (unified knowledge base)**: technical-spec templates and outputs are maintained in `/.Knowledge/template/` and `/.Knowledge/req-docs/`. + +# Technical Spec Template + +> For use by the **f2s-req-tech** skill. The primary template path is `.Knowledge/template/technical-spec-template.md`; configuration roots no longer receive a duplicate `template/` copy. **Sections are optional building blocks**: omit entire sections that do not apply to the requirement, and add project-specific sections as needed. The section order below is recommended and may be adjusted. Do not force API, database, error-code, or message-queue sections merely to fit the template. + +--- + +## 1. Document Title + +- H1 title: `# Technical Spec` + +--- + +## 2. Requirement Overview + +- H2 title: `## Requirement Overview` +- Use unordered lists or short paragraphs to clarify: background, goals, scope, and **explicit non-goals**. + +--- + +## 3. Key Issues Overview + +- H2 title: `## Key Issues Overview` +- Technical difficulties, concurrency/consistency/performance, boundaries with existing modules, risks, and tradeoffs (a list is enough). +- **When to fill**: when technical tradeoffs require decisions; omit if there are no obvious difficulties. + +--- + +## 4. External Dependencies and Internal Calls + +- H2 title: `## External Dependencies and Internal Calls` +- Briefly list external services depended on (HTTP/RPC/third-party APIs/SDKs, etc.) and internal modules or methods that will be called (no need to expand every parameter). +- **When to fill**: when cross-module or cross-service calls exist; omit for changes contained within a single module. + +--- + +## 5. Configuration + +- H2 title: `## Configuration` +- Split subsections by configuration source (such as `### Environment Variables`, `### Configuration Files`, `### Feature Flags`), include examples, and comment field meanings. +- **When to fill**: when configuration items need to be added or changed; omit if there is no new configuration. + +--- + +## 6. Message Queue / Event Bus (if any) + +- H2 title: `## Message Queue / Event Bus` +- Split subsections by scenario (such as `### xxx Flow`) and describe producers/consumers, topic/queue names, trigger timing, consumption logic, idempotency handling, and so on. +- **When to fill**: when asynchronous messages or event-driven architecture are involved; omit if there is no message queue. + +--- + +## 7. Delivery Units + +- Name the H2 title according to the actual delivery shape (for example `## API Contract`, `## Component Design`, `## Page / Interaction`, `## Script / Tool`, `## Service Logic`, `## Data Processing`). +- **Use one H3 title per delivery unit**: `### ` (optionally add path or type in parentheses). +- **Within each section, include as needed** (recommended order): + 1. **Business notes** (optional): prerequisites, usage scenarios, cautions. + 2. **Input / trigger**: parameter signature, request body, user action, event, scheduled task, or script parameter examples; lists or tables are acceptable. + 3. **Output / result**: response body, page state, component Props, database write result, message event, or file output description. + 4. **Field notes** (optional): table `| Field | Type | Description |`. + 5. **Processing flow** (as needed): required when business logic, state changes, cross-module calls, or exception branches are involved; omit for pure structure descriptions or static configuration. +- For units that **reuse shared capabilities**: input/output examples + "see " are enough; **processing flow** may be summarized as "same as xxx logic". +- **Prohibited**: do not open a separate chapter that lists all delivery-unit flows; do not repeat each unit's step-by-step flow again at the end of the document (unless it is a full-chain **one-page-level** sequence; see the next item). + +--- + +## 8. Call / Interaction Flow (optional, overview only) + +- H2 title: `## Call Flow` or `## Interaction Flow` +- Write **only** the **call order** from the user/system perspective (for example: load configuration on page entry, then trigger submit, then show results). Do **not** repeat internal steps from each unit. If there is no need to connect multiple units, omit the whole section. + +--- + +## 9. Error Codes / Exception Handling + +- H2 title: `## Error Codes` or `## Exception Handling` +- Table: `| Error code / exception type | Description | Handling suggestion |` (keep it consistent with project conventions). +- **When to fill**: when a clear error-code system exists or unified exception-handling strategy is needed; otherwise omit. + +--- + +## 10. Data Model / Table Design + +- H2 title: `## Data Model` or `## Table Design` +- Database: each table uses `### table-display-name table_name`, with inline field notes and index notes. +- Frontend/state: use type definitions or TypeScript interface examples. +- **When to fill**: when data structures are added or changed; omit for pure logic changes. diff --git a/packages/core/templates/en-US/knowledge/topics/f2s-config-precheck.md b/packages/core/templates/en-US/knowledge/topics/f2s-config-precheck.md new file mode 100644 index 0000000..7e54f3a --- /dev/null +++ b/packages/core/templates/en-US/knowledge/topics/f2s-config-precheck.md @@ -0,0 +1,32 @@ +--- +id: config-precheck +revision: 0 +summary: "config-precheck (routing summary)" +primary: config +confidence: manual +tags: [policy] +--- +# config-precheck (routing summary) + +## Purpose + +- Anchors topic id **`config-precheck`** for `manifest-routing.topicPaths`. +- Relates to reading the project-root **`flow2spec.config.json`** (`subAgent`, `switchAgentVerification`, `changeTracking`) before executing any **`f2s-*` skill**. Its semantics match the top of repository-root **`AGENTS.md`** and the "unified entry". + +## Complete Instructions (on demand; do not maintain a second full body in `.Knowledge`) + +| Side | Path | +| --- | --- | +| Codex | Repository-root `.codex/topics/f2s-config-check.md` (init mirror, same source as template); SessionStart: `.codex/hooks/f2s-config-session.js` | +| Cursor | Repository-root `.cursor/rules/f2s-config-check.mdc` (`flow2spec init cursor`) | +| Claude | `.claude/rules/f2s-config-check.md`; SessionStart: `.claude/hooks/f2s-config-session.js`; PreToolUse guard: `.claude/hooks/f2s-config-inject.js` | + +## Required Steps + +1. Use **Read** to open project-root **`flow2spec.config.json`** (must happen before any step in an `f2s-*` skill body). +2. The `{{FLOW2SPEC_PROJECT_CONFIG}}` table in repository-root **`AGENTS.md`** only explains field semantics; current values come from the **Read** result. +3. When `subAgent=true`, the main agent must explicitly decide early in the skill body whether this run meets the split preconditions / thresholds; even when deciding not to split, it must output the no-split reason. The SessionStart summary is only a reminder and does not replace that decision. + +## Prohibitions + +- Do not enter **`f2s-*`** skill-body steps before reading **`flow2spec.config.json`** (same rule as `AGENTS` and `.codex/topics/f2s-config-check.md`). Claude/Codex SessionStart summaries and Claude's PreToolUse guard reminder do not replace this Read. diff --git a/packages/core/templates/en-US/knowledge/topics/f2s-fallback-triage.md b/packages/core/templates/en-US/knowledge/topics/f2s-fallback-triage.md new file mode 100644 index 0000000..a3d0cad --- /dev/null +++ b/packages/core/templates/en-US/knowledge/topics/f2s-fallback-triage.md @@ -0,0 +1,67 @@ +--- +id: fallback-triage +revision: 0 +summary: fallback-triage +primary: policy +confidence: manual +--- +# fallback-triage + +## Triggers + +Enter this topic when any of the following is true: + +- `taskToTopicRules` has no hit +- It is unclear which topic to route to (multiple candidates are close, keywords are generic, and no domain terms hit) +- The gap check fails (dependency topics or context documents are missing) + +> **When entering this topic**: `manifest-routing.json` has already been read in this task line and is treated as a stable snapshot. Do not reread it; triage directly from the existing routing result. +> +> **This topic is for triage only. It is not final hit authority and must not directly implement business changes.** + +--- + +## Triage Flow + +### Step 1: Determine Whether Routing Hit + +**A topic was hit, but context is insufficient**: + +1. Read dependency topics in `topicDependencies`, and keep the next-highest candidate for supplemental validation +2. Name the missing document or topic section +3. Ask the user for the specific document path, then continue after it is supplied +4. **Do not perform threshold-free cross-matcher full search** + +**No topic was hit**: proceed to step 2. + +--- + +### Step 2: Ask the User to Confirm Domain Coverage + +Do not let the Agent infer this on its own. Ask the user directly: + +> The current task did not hit routing. Please confirm: **Has documentation for this domain already been added to the knowledge base?** +> - Yes -> routing terms may be missing; consider running `f2s-kb-build` / `f2s-kb-sync` to supplement routing, then retry +> - No -> the knowledge base currently has no coverage; choose either drilling into business source code or adding `req-docs` and then implementing from the spec +> - Unsure -> please check whether `.Knowledge/stock-docs/` contains related documents, then tell me + +Follow the corresponding exit based on the user's answer. **Do not guess when there is no answer.** + +--- + +## Exit Paths + +| Triage conclusion | Next step | +|----------|--------| +| Topic was hit and context is filled | Jump to that topic and execute `match → expand → verify → act` | +| User confirms docs exist but routing terms are missing | Suggest running `f2s-kb-build` / `f2s-kb-sync` to supplement routing; pause this run or drill into source code | +| User confirms no KB coverage exists | Offer two paths: drill into source code / add `req-docs` and then implement | +| User is unsure and it still cannot be located | Stop execution, explain the reason, and wait for a clear instruction | + +--- + +## Prohibitions + +- Do not use this topic as final hit authority to directly implement changes +- Do not skip user confirmation and infer domain coverage yourself +- Do not perform cross-matcher full supplemental search when context is insufficient diff --git a/packages/core/templates/en-US/knowledge/topics/f2s-implement-tech-design.md b/packages/core/templates/en-US/knowledge/topics/f2s-implement-tech-design.md new file mode 100644 index 0000000..ef1d5d5 --- /dev/null +++ b/packages/core/templates/en-US/knowledge/topics/f2s-implement-tech-design.md @@ -0,0 +1,29 @@ +--- +id: implement-tech-design +revision: 0 +summary: "implement-tech-design (routing summary)" +dependsOn: [f2s-doc-routing] +primary: policy +confidence: manual +--- +# implement-tech-design (routing summary) + +> **Only long-form source**: Cursor / Claude use configuration-root **`rules/f2s-implement-tech-design.md(c)`** as authoritative. +> **Codex**: do not read `rules/`; execute the equivalent constraints in **`.codex/topics/f2s-implement-tech-design.md`** (automatically mirrored from template `rules` by `flow2spec init`). + +## Purpose + +- Anchors topic id **`implement-tech-design`** for `manifest-routing.topicPaths` and `index.md`. +- Keeps only **path and role** reminders to avoid maintaining a duplicate long-form body alongside `rules/`. + +## Paths and Roles (must match the rules) + +- Technical-spec input: `.Knowledge/req-docs/*.md` (and Markdown generated from PDFs by `f2s-doc-pdf` into the same directory). +- Existing knowledge deposits: `.Knowledge/stock-docs/` — **not** direct input for "implement code from a spec". + +## What to Read Next + +| Environment | Next step | +| --- | --- | +| Cursor / Claude | Open or @ **`rules/f2s-implement-tech-design`**, then follow its steps. | +| Codex | Read **`.codex/topics/f2s-implement-tech-design.md`**. | diff --git a/packages/core/templates/en-US/knowledge/topics/f2s-req-plan.md b/packages/core/templates/en-US/knowledge/topics/f2s-req-plan.md new file mode 100644 index 0000000..a0f52b0 --- /dev/null +++ b/packages/core/templates/en-US/knowledge/topics/f2s-req-plan.md @@ -0,0 +1,35 @@ +--- +id: f2s-req-plan +revision: 0 +summary: "f2s-req-plan (routing summary)" +dependsOn: [f2s-task] +primary: policy +confidence: manual +--- +# f2s-req-plan (routing summary) + +> For the long-form body, see configuration-root **`skills/f2s-req-plan/SKILL.md`**. +> **`.task/` source of truth**: configuration-root **`rules/f2s-task.*`** (Codex: `.codex/topics/f2s-task.md`). +> Design background (optional): [task list and change tracking](../stock-docs/.md). + +## Dependency + +Before executing this topic, first read dependency topic **`f2s-task`** (`manifest-routing.topicDependencies`). + +## Purpose + +Starting from a technical spec or requirement description: **resume triage -> draft confirmation -> write to disk according to f2s-task -> implement -> archive**. + +1. Step 0: `flow2spec.config.json` + the full **`f2s-task`** text +2. `f2s-task` "task start": check `todo.json` / keywords for resume work +3. Draft confirmation (main agent) +4. Write `task.md` / `context.md` / `user-todos.md` / `todo.json` to disk (`linkedSkill: f2s-req-plan`) +5. Implement and check off steps as they complete; write user-side todos to `user-todos.md` +6. After archive gates are satisfied, move into `completed/-/` + +Does not depend on `changeTracking`, but **always** follows `f2s-task`. + +## Next Step + +- Full skill text: `skills/f2s-req-plan/SKILL.md` +- Task rules: `rules/f2s-task.*` or `.codex/topics/f2s-task.md` diff --git a/packages/core/templates/en-US/knowledge/topics/f2s-stock-docs-vs-req-docs.md b/packages/core/templates/en-US/knowledge/topics/f2s-stock-docs-vs-req-docs.md new file mode 100644 index 0000000..a8ea3e5 --- /dev/null +++ b/packages/core/templates/en-US/knowledge/topics/f2s-stock-docs-vs-req-docs.md @@ -0,0 +1,32 @@ +--- +id: f2s-doc-routing +revision: 0 +summary: "f2s-doc-routing (routing summary)" +primary: policy +confidence: manual +--- +# f2s-doc-routing (routing summary) + +> **Only long-form source**: Cursor / Claude use configuration-root **`rules/f2s-stock-docs-vs-req-docs.md(c)`** as authoritative. +> **Codex**: do not read `rules/`; execute the equivalent constraints in **`.codex/topics/f2s-stock-docs-vs-req-docs.md`** (automatically mirrored from template `rules` by `flow2spec init`). + +## Purpose + +- Anchors topic id **`f2s-doc-routing`** for `manifest-routing.topicPaths`, **`topicDependencies`**, and `index.md`. +- Keeps only reminders about **directory responsibilities**. + +## Directory Responsibilities (must match the rules) + +| Directory | Purpose | +| --- | --- | +| `.Knowledge/stock-docs/` | Architecture, final drafts, and deposited knowledge; preferred destination for `f2s-kb-build` / `f2s-doc-final`, and similar flows. | +| `.Knowledge/req-docs/` | Requirement clarification, **technical specs**, and Markdown input when implementing from a spec. | + +**Principle**: when coding from a spec, read only **`req-docs`**; do not treat **`stock-docs`** as direct coding input. + +## What to Read Next + +| Environment | Next step | +| --- | --- | +| Cursor / Claude | Open or @ **`rules/f2s-stock-docs-vs-req-docs`**. | +| Codex | Read **`.codex/topics/f2s-stock-docs-vs-req-docs.md`**. | diff --git a/packages/core/templates/en-US/knowledge/topics/f2s-task.md b/packages/core/templates/en-US/knowledge/topics/f2s-task.md new file mode 100644 index 0000000..a69359a --- /dev/null +++ b/packages/core/templates/en-US/knowledge/topics/f2s-task.md @@ -0,0 +1,53 @@ +--- +id: f2s-task +revision: 0 +summary: "f2s-task (routing summary)" +primary: policy +confidence: manual +--- +# f2s-task (routing summary) + +> For the long-form body, see configuration-root **`rules/f2s-task.*`**. +> Codex: **`.codex/f2s-rules/f2s-task.md`**. + +## Purpose + +Change-tracking rules (`alwaysApply: true`). When the corresponding skill's `changeTracking.*` is `true`, automatically create, update, and archive task lists under **`TASK_ROOT`** (see multi-developer section). Supports cross-session resume. + +## Effective Scope + +| Configuration item | Corresponding skill | +| --- | --- | +| `changeTracking.feat` | `f2s-kb-feat` | +| `changeTracking.fix` | `f2s-kb-fix` | +| `changeTracking.implement` | `f2s-implement-tech-design` | + +`f2s-req-plan` always maintains a task list (not gated by `changeTracking`). + +## Task root `TASK_ROOT` (multi-developer) + +- Resolve: `collaboration.developerId` (config) → git email/name → legacy `.task` +- Non-legacy: `.task//…`; **only** current `TASK_ROOT` (no cross-developer todo scan) +- `.Knowledge/` remains shared for the whole team + +## Directory Structure + +``` +TASK_ROOT/ ← `.task` or `.task/` +├── todo.json +├── active// +│ ├── task.md +│ ├── context.md +│ ├── user-todos.md +│ └── acceptance.md +└── completed/-/ + └── … +``` + +## Cross-session continuation + +Resolve `TASK_ROOT` first; match keywords **only** in that root's `todo.json`. On hit, show remaining checklist and optional user-todos/acceptance; load `linkedSkill` if set. + +## Next step + +Read configuration-root `rules/f2s-task.*` for full rules. diff --git a/packages/core/templates/en-US/knowledge/topics/flow2spec-dsh-adapter.md b/packages/core/templates/en-US/knowledge/topics/flow2spec-dsh-adapter.md new file mode 100644 index 0000000..7223d7e --- /dev/null +++ b/packages/core/templates/en-US/knowledge/topics/flow2spec-dsh-adapter.md @@ -0,0 +1,16 @@ +--- +id: flow2spec-dsh-adapter +revision: 0 +summary: "DeepSeek Harness project skill initialization and directory adapter" +primary: feature +confidence: inferred +tags: [module] +--- +# DeepSeek Harness Adapter + +Use this topic for `flow2spec init dsh`, DeepSeek Harness skill discovery, `.dsh/skills`, `.dsh/topics`, and the repository-root `AGENTS.md` entry. + +- Skills are written to `.dsh/skills//SKILL.md`. +- Long-form rules are mirrored to `.dsh/topics/*.md`, with `.dsh/AGENTS.md` as a directory pointer. +- A missing root `AGENTS.md` receives a full entry; an existing entry is preserved. +- Native Cordis plugin work remains a later roadmap item. diff --git a/packages/core/templates/en-US/rules/f2s-config-check.md b/packages/core/templates/en-US/rules/f2s-config-check.md new file mode 100644 index 0000000..d4bdb6f --- /dev/null +++ b/packages/core/templates/en-US/rules/f2s-config-check.md @@ -0,0 +1,50 @@ +--- +description: Before running any f2s-* skill, force-read flow2spec.config.json to determine the actual subAgent and switchAgentVerification values +alwaysApply: true +--- + +> **Task paths**: all `.task/` reads/writes must use **`TASK_ROOT` from `rules/f2s-task`** (` .task` or `.task/`; config → git → legacy). Bare `.task/todo.json` / `.task/active/` below mean **`TASK_ROOT/...`**. + + +# Mandatory Preflight for f2s Skills + +**The first action before running any `f2s-*` skill must be to read the project-root `flow2spec.config.json` with the Read tool**, obtain the actual `subAgent` and `switchAgentVerification` values, and then decide the orchestration approach. + +``` +Required: Read("flow2spec.config.json") <- before any step in the skill body +``` + +| Read result | Behavior | +|---------|------| +| `subAgent: true` | First make an explicit decision about whether the current skill meets the split preconditions / scale threshold; if it does, follow the skill's B/C mode to dispatch sub agents and record "whether this run split work, to whom, and why"; otherwise continue in the main agent, but still output the no-split reason | +| `subAgent: false` | Complete everything in the main agent; do not split work to sub agents | +| `switchAgentVerification: true` | Writes from a sub agent are verified by the main agent; writes from the main agent are verified by a sub agent (requires `subAgent=true` and an actual split subtask) | +| `switchAgentVerification: false` | The writing side verifies its own work; no cross-verification | +| File does not exist | Treat every field as `false` | + +**Claude Code**: `f2s-config-session` injects one configuration summary at `SessionStart`; `f2s-config-inject` only acts as a guard reminder in `PreToolUse`, reminding that the first step before invoking an `f2s-*` Skill must be `Read("flow2spec.config.json")`. Neither replaces the Read requirement in this rule. + +**Cursor**: configuration reading still relies on text constraints (this `alwaysApply` rule) and does not depend on hooks reading configuration automatically. + +**Codex**: `SessionStart` injects one configuration summary, but before entering any `f2s-*` skill body you still must `Read("flow2spec.config.json")`. When `subAgent=true`, the main agent **must first make an explicit split/no-split decision** for the current skill based on its preconditions / thresholds before deciding whether to dispatch sub agents; even when deciding not to split, it must output the no-split reason. Codex does **not** have Claude's `PreToolUse Skill` guard, so this decision cannot remain implicit. + +### changeTracking + +| Field | Effective skill | Behavior | +|------|---------|------| +| `changeTracking.feat: true` | `f2s-kb-feat` | **Step 0 is mandatory**: create or continue a change-tracking task under `.task/active/` | +| `changeTracking.feat: false` | `f2s-kb-feat` | Skip step 0 and do not create a `.task/` directory | +| `changeTracking.fix: true` | `f2s-kb-fix` | **Step 0 is mandatory**: create or continue a change-tracking task under `.task/active/` | +| `changeTracking.fix: false` | `f2s-kb-fix` | Skip step 0 and do not create a `.task/` directory | +| `changeTracking.implement: true` | `f2s-implement-tech-design` | **Step 2.5 writes the task list, step 2.6 checks off `task.md` as implementation progresses, and step 5 archives after the archive gate passes** | +| `changeTracking.implement: false` | `f2s-implement-tech-design` | Skip step 2.5, step 2.6, and the change-tracking portion of step 5 | + +### intentRecognition + +| Field | Behavior | +|------|------| +| `intentRecognition: true` | Enable intent recognition: high-confidence operational intents automatically enter the corresponding Skill according to `rules/f2s-intent-routing.*`; discussion, evaluation, and low-confidence input must not auto-invoke a Skill | +| `intentRecognition: false` | Do not enable automatic routing; enter a Skill only for an explicit `$f2s-*` command or a clear request to run a specific skill | +| Field does not exist | Treat as `false` | + +**Do not enter any execution step in a skill body before this file has been read.** diff --git a/packages/core/templates/en-US/rules/f2s-flow2spec-unified-entry.md b/packages/core/templates/en-US/rules/f2s-flow2spec-unified-entry.md new file mode 100644 index 0000000..0d01fa3 --- /dev/null +++ b/packages/core/templates/en-US/rules/f2s-flow2spec-unified-entry.md @@ -0,0 +1,117 @@ +--- +description: Flow2Spec unified knowledge-base entry, read progressively through .Knowledge +alwaysApply: true +--- + +# Flow2Spec Unified Entry Rule + +This project's knowledge base has been unified under `.Knowledge/`. Read in the following order and avoid unbounded searches. + +## Project-Root CLI Switches (Read When Needed) + +The business repository's **project-root** `flow2spec.config.json` (`flow2spec init` fills it when missing) contains boolean fields **`subAgent`** and **`switchAgentVerification`** (**switch-agent verification**), defaulting to `false`. Before running any **`f2s-*` skill** or explaining Flow2Spec initialization, read this file. Whenever a skill or rule says a step applies "only when `subAgent` / `switchAgentVerification` is true", **the actual file value must decide whether the step runs**. Missing fields or a missing file are both treated as `false`. + +> **`init` and routing source**: **`flow2spec init`** writes the unified entry into the current repository. **Cursor / Claude** read the configuration-root **`rules/f2s-flow2spec-unified-entry.*`**; **Codex** reads **`.codex/topics/f2s-flow2spec-unified-entry.md`**. The two bodies share the same source; read the entry for the current tool. When a skill references the "unified entry", **Codex** uses **`.codex/topics/f2s-flow2spec-unified-entry.md`** as authoritative. + +### Semantics of the Two Fields (Template Convention) + +- **`subAgent`**: if an `f2s-*` skill specifies that a step should "run in a sub agent", then **`true`** means use sub agents according to the skill, and **`false`** means complete it in the main agent. The user may request that "**only when** this field is **`true`**, the main agent should **dynamically decide** which subtasks are suitable for sub agents"; that request is valid **only when the configuration is `true`**. When configured as `false`, any instruction depending on sub-agent splitting **does not apply**, and all work is completed in the main agent. When `subAgent=true`, the main agent must explicitly decide near the start of the skill body whether to split this run; even when deciding not to split, it must output the no-split reason. **Which stage of each `f2s-*` must or should use a sub agent** is specified progressively in the skill body; if the skill does not specify a split, do not split by default. +- **`switchAgentVerification` (switch-agent verification)**: **verification/review** after writes or changes (checklist comparison, diff, self-check) is **not** "always in the main agent". By default, the **agent that performed the write is the "current agent"** and verifies within that session (**sub-agent writes are verified in the sub agent; main-agent writes are verified in the main agent**). **Only when** (1) **`switchAgentVerification` is `true`** in config, and (2) the **current `f2s-*` skill body** explicitly says that a step behaves differently "when **`switchAgentVerification`** is **`true`**", enable **cross-verification**: **sub-agent writes -> verified by the main agent**; **main-agent writes -> verified by a sub agent** (a sub-agent session **must** exist, meaning **`subAgent` is `true`** and a subtask was actually split out). If **`subAgent` is `false`**, there is no sub side to take over, so **"main write -> sub verify" does not happen**, and all verification stays in the main agent. If config is `false`, the skill does not explicitly depend on this field, or the user only vaguely asks "let the other side verify", do **not** enable cross-verification; verification remains inside the writing-side agent. + +### Git Worktree and Subtask Working-Directory Hygiene (Required When `subAgent: true` or Parallel Subtasks Are Used) + +Some environments create an **independent `git worktree`** or equivalent isolated directory for sub agents / parallel attempts. Rules: + +1. **Creator cleans up**: if the sub side created it, the sub side should clean it before returning whenever possible. If the sub session has ended and cannot clean up, the **main agent must clean it after merging results**. Do **not** rely on "automatic cleanup later." +2. **Closing action (mandatory)**: for a worktree added **only for this subtask**, after merging or discarding the subtask result, run `git worktree remove ` (if the worktree is clean but removal still fails, use `git worktree remove --force ` after **confirming** the path has no uncommitted changes by others). Then self-check with `git worktree list`; do **not** leave known orphaned paths. +3. **Before interruption / user topic change**: if this session added a worktree, complete the removal above before ending, or write the leftover path and deletion command under `task.md` "## Notes"; when appropriate, also write it to **`user-todos.md`** for the user to run locally (see `f2s-task`). +4. **Prohibited**: after a subtask has ended and the main branch has continued, do not keep a worktree directory that was only for an attempt (it easily causes confusing commits and disk buildup). + +## Read Order (Mandatory) + +1. First read `.Knowledge/manifest-routing.json`, prefer routing by `taskToTopicRules`; as needed, read the matcher shard from `matcherPath` to obtain `includeAny` keywords. If nothing matches, enter fallback recall. + - If the matched topic has dependencies in `topicDependencies`, read dependency topics first, then the main topic. + - Routing manifests are maintained only by `f2s-*` skill flows and do not depend on extra CLI subcommands. +2. Read `.Knowledge/index.md` only as needed to confirm topic semantics and boundaries. +3. Then read `.Knowledge/topics/.md` (**routing summary**: topic id, path conventions, next-step pointers). If the topic is **`implement-tech-design`** or **`f2s-doc-routing`**, **continue reading the full configuration-root rules** **`rules/f2s-implement-tech-design.*` / `rules/f2s-stock-docs-vs-req-docs.*`** as execution basis (`.Knowledge/topics` does not duplicate those long-form texts). +4. If background is needed, read `.Knowledge/stock-docs/.md`. +5. Drill into business source code only when the first four steps are insufficient. +6. After a match, always run `match -> expand -> verify -> act`: + - `match`: take the primary candidate first; + - `expand`: expand `topicDependencies` and keep the next-highest candidate for supplementary verification; + - `verify`: check gaps before acting (missing key topics, boundaries, or context); + - `act`: act only when confidence is sufficient; clarify first when confidence is low. +7. A full cross-matcher supplemental search (top-k) is allowed only when one of these conditions is true: + - `taskToTopicRules` has no match; + - the score gap between primary and secondary candidates is too small (low confidence); + - the gap check fails (missing key topic/dependency/context); + - the user explicitly asks for "full check / don't miss anything". + +## Task Routing + +- Technical-design implementation: first read `.Knowledge/topics/f2s-implement-tech-design.md` (summary), then read the **full `rules/f2s-implement-tech-design.*`**; requirement documents live in `.Knowledge/req-docs/` by default. +- Directory-boundary decisions: first read `.Knowledge/topics/f2s-stock-docs-vs-req-docs.md` (summary), then read the **full `rules/f2s-stock-docs-vs-req-docs.*`**. + +## Machine-Readable Source-of-Truth Semantics (Rule Layer) + +- `taskToTopicRules`: first-priority task routing. +- `taskToTopicRules[].matcherPath`: direct path to the matcher-word shard; read a single matcher file as needed. +- `taskToTopicRules[].matcherId`: stable matcher identifier; must match `id` inside the matcher shard. +- `topicDependencies`: load dependency topics first after the main topic matches. +- `topicMetadata`: topic-governance metadata. It only affects reading expectations, does not participate in matcher hits, does not decide whether a topic is read, and does not change execution mandatoryness. Execution mandatoryness always comes from explicit requirements in `AGENTS.md`, rules, skills, and topic bodies. When reading `topicMetadata[topicId].primary` / `tags`: `config` means focus on configuration items, switches, defaults, and initialization parameters; `policy` means prioritize mandatory/prohibited/gate/process constraints in the body; `feature` is background for implemented business/product capability; `module` is background for directories, packages, module boundaries, and engineering structure. `confidence` only allows `manual` / `inferred`; do not write metadata without clear classification evidence. +- `matcherPath(includeAny)`: task keyword matcher word list. +- `fallbackTopic`: must be read when neither task nor keyword matches, but is only a low-confidence fallback, not final execution basis. +- `.Knowledge/manifest-routing.json + matcherPath shard files` are the machine-readable source of truth (keywords live only in `matchers/*.json`). +- `.Knowledge/index.md` is not a machine-readable source of truth; it is only human-readable navigation and semantic-boundary validation. +- After entering `fallbackTopic`, first perform supplemental recall or clarification, then decide whether to make changes. + +## Knowledge Gaps and Responses (By Scenario) + +| Scenario | Response | +| --- | --- | +| **1a Documents exist in the KB but routing is missing** | Use `f2s-kb-build` / `f2s-kb-sync` / `f2s-kb-add` to add `taskToTopicRules`, `matcherPath` shards, and `topicPaths`; expand `includeAny` to cover common user phrasing. Agent side: use `fallbackTopic` triage and state that "routing needs to be added"; do **not** replace configuration with full-repo file scanning. | +| **1b Matched, but context is insufficient** | First `expand` (`topicDependencies` + secondary candidate), then `verify` and name which `stock-docs`/`req-docs` file or topic section is missing. If still insufficient, **ask the user for a document or path** instead of running an unconditional full cross-matcher search. **If the Agent needs to drill into source code**: first give the user a **visible gap note** (KB read, what is missing, which 1-2 files you plan to read); see the "gap gate" in **`f2s-knowledge-preflight`**. **Do not** run consecutive `Grep` calls or disorderly source exploration without that note. | +| **2 The KB has no corresponding document** | After reading routing + matched matcher + related topics once, **explicitly acknowledge in the reply that the KB has no coverage**, then choose: drill into business code / ask the user to provide `req-docs` or a PRD. **Do not** repeatedly read lists to pretend "one more search will find it." Before source drilling, also satisfy the visible gap-note requirement in **`f2s-knowledge-preflight`**. | +| **2a Repeated list reading wastes tokens** | Within the **same task line**, treat `manifest-routing.json` as a stable snapshot: rereading it in full requires a reason (for example, the user says routing/knowledge was updated by `f2s-kb-build` / `f2s-kb-sync` / `f2s-kb-add`, or the manifest/matcher was **manually edited**). **Do not equate** running **`flow2spec init`** alone with "business KB was updated": `init` mainly writes the configuration root, fills missing directories, and aligns package-level routing structure. **stock-docs / req-docs, topic routing summaries, and matcher entries** are maintained by **`f2s-*` skill flows**. `init` writes rules into the configuration root **`rules/*`** (or equivalent extension) and writes Codex mirrors into **`.codex/topics/*.md`**. Read only the **single** `matcherPath` corresponding to the current rule; do not traverse the whole `matchers/` directory for enumeration. Open `index.md` only when topic semantics need checking; do not alternate between manifest and index to "refresh lists." | + +### Execution Points for Knowledge Gaps (Avoid "Table Says It, Behavior Skips It") + +- **"Explain to the user" and "explicitly acknowledge no coverage" must be visible natural language to the user**; they must not be hidden only in internal analysis or tool traces. Details and stop conditions are in **`f2s-knowledge-preflight`** (gap gate, exploration limit). +- **Prohibited**: after hitting **1b / 2**, entering chained "multiple files + dependency directories" source exploration without the visible note above. Running another `Grep` round for each new "entry symbol" is a typical anti-pattern. +- Facts such as **HTTP status, error body, and whether redirects happen** must **not** be answered from training data or experience with other repositories; use only the implementation actually read in the current repository during this turn. +- When ordinary Q&A drills into source code and answers from it, first complete the initial read and gap gate according to **`f2s-knowledge-preflight`**, then complete final KB follow-up closing according to **`f2s-kb-feedback-closing`**. Only suggest; do not write automatically. + +## Knowledge-Base Writing Style (Global; Applies When Writing stock-docs / topics / index) + +**Prefer affirmative phrasing**: when stating correct information, directly say "what it is / where it is / how to do it". Do not communicate it through "not X / non-X / no longer X"; even if the old description is wrong, negating the old version anchors the wrong premise in the reader's mind. + +- Wrong: `imported with the package, not injected through window` +- Right: `import with import { } from ''` + +**Exception (explicit negation is appropriate)**: when both A and B are logically valid approaches but the project has made an **exclusive choice**, write "do not use B"; otherwise readers cannot tell whether B remains optional. + +## Knowledge-Base Version Self-Check (Hook Auto-Triggered; First Time Each Day Only When updateCheck.enabled=true) + +Each initialized client uses its own startup/update mechanism when supported; the generated client entrypoint is authoritative. Clients without hooks continue to use the generated rules, skills, `AGENTS.md`, or topic mirrors. Version-check scripts compare versions and, when an upgrade is needed, inject an imperative upgrade notice through `additional_context` where the client supports it. Project-level skill discovery clients use `.dsh/skills/` and `.dsh/topics/` through `flow2spec init dsh`. + +**Rule-layer fallback check** (backup for script cache): + +1. Read `flow2spec.config.json` -> if `updateCheck.enabled` is not `true`, skip and show no notice. +2. Read `.Knowledge/update-check.json` -> if the file exists and `checkedAt` is the same local calendar day as today (`new Date(checkedAt).toDateString() === new Date().toDateString()`), do not check npm again. However, if `needsUpgrade=true` or `latestNpm > manifestVersion`, the first user reply in this session must still remind the user to run `f2s-kb-upgrade`; if the current `.Knowledge/manifest-routing.json.version` is already no lower than `latestNpm`, delete that cache and stop reminding. +3. If neither of the two steps above skipped the check: run the update-check script under the current agent configuration root (Claude: `node .claude/hooks/f2s-update-check.js`; Cursor: `node .cursor/hooks/f2s-update-check.js`; Codex: `node .codex/hooks/f2s-update-check.js`) and parse JSON from stdout: + - If it contains `hookSpecificOutput.additionalContext`: **tell the user** that content (suggest running the `f2s-kb-upgrade` skill). + - If there is no output or parsing fails: stay silent. +4. If any step errors, silently skip it and do not affect normal conversation. + +## Topic Authoring Pointer + +When adding or modifying `.Knowledge/topics/.md`, adjusting `manifest-routing.topicDependencies`, or deleting / migrating topics, the **authoring-side** guideline uses **`rules/f2s-topic-authoring.*`** as the single source of truth (**Cursor/Claude**: `rules/f2s-topic-authoring.mdc`; **Codex**: `.codex/topics/f2s-topic-authoring.md`). This entry is the **consumption side** (how to route / read / fall back using existing topics), and both coexist; in hard conflicts, this unified entry wins. `f2s-kb-build` / `f2s-kb-add` / `f2s-kb-feat` / `f2s-kb-fix` / `f2s-kb-sync` / `f2s-kb-migrate` / `f2s-kb-rm` must Read that full rule before any topic write. + +## Prohibited + +- **Neutrality for distributed content**: examples in skill/rule/knowledge bodies must be **neutral**. Do not write a specific business-domain name, a single organization's npm package name, or a `docs/` path that exists only in the Flow2Spec product repository. Use placeholders such as `` and `src//`. +- After using `git worktree` or an isolated directory for subtasks, **do not** end the session without `git worktree remove` or without handing off the deletion command (see "Git worktree and subtask working-directory hygiene" above). +- Before viewing `.Knowledge/manifest-routing.json`, do not run unbounded full-repository scans. Read `.Knowledge/index.md` only when topic semantics need confirmation; do not repeatedly alternate between index and manifest as a substitute for decision-making. +- Do not use `stock-docs` as direct coding input documents; implementation from a design should use `req-docs`. +- Do not treat `fallbackTopic` as a final match and directly implement changes from it. +- Do not run a full cross-matcher supplemental search unless the trigger conditions are met. diff --git a/packages/core/templates/en-US/rules/f2s-implement-tech-design.md b/packages/core/templates/en-US/rules/f2s-implement-tech-design.md new file mode 100644 index 0000000..4c068ef --- /dev/null +++ b/packages/core/templates/en-US/rules/f2s-implement-tech-design.md @@ -0,0 +1,147 @@ +--- +description: When the user asks to implement a runnable deliverable from a technical design document, follow this rule (read the document, list tasks, confirm, implement, and provide pending items and reminders). The user provides the technical design path in the conversation (MD or PDF); if it is a PDF, convert it to MD with f2s-doc-pdf first. +globs: + - "**/.Knowledge/req-docs/**/*.md" +alwaysApply: false +--- + +> **Task paths**: all `.task/` reads/writes must use **`TASK_ROOT` from `rules/f2s-task`** (` .task` or `.task/`; config → git → legacy). Bare `.task/todo.json` / `.task/active/` below mean **`TASK_ROOT/...`**. + + +> **Single long-form rule**: this file is the complete execution rule for **implement-tech-design**. `.Knowledge/topics/f2s-implement-tech-design.md` is only a routing summary; **Codex** reads `.codex/topics/f2s-implement-tech-design.md` (automatically mirrored from this file by `flow2spec init`) as the equivalent rule text. + +> Execution scope: the unified knowledge-base path is `/.Knowledge/`. All paths below are interpreted according to the `.Knowledge` convention. + +# Implement Deliverables From a Technical Design (General) + +When the user asks to implement a runnable deliverable based on a **technical design document** (the user provides a document path such as `.Knowledge/req-docs/xxx.md`, or a PDF), follow these conventions. + +**Directory convention**: `.Knowledge/req-docs/` stores technical designs "used for implementation"; `.Knowledge/stock-docs/` stores consolidated documents and must not be used as direct coding input. + +**Trigger note**: this rule auto-loads when opening `.md` files under `req-docs` (`**/req-docs/**/*.md`). If the technical design was not opened before the conversation, the user may @ this rule in the conversation and then provide the path. + +- If the user provides a PDF: first run `f2s-doc-pdf`, convert the PDF to MD under `.Knowledge/req-docs/`, then continue. +- If the user provides MD/text: read it directly and enter the implementation flow. + +--- + +## 1. Goals and Principles + +- **Goal**: implement a runnable deliverable from the technical design while staying consistent with existing project conventions. Deliverables may include frontend pages/components, backend APIs/services, data-processing logic, task orchestration, scripts, configuration, and similar items, trimmed to the design's actual scope. +- **Principles**: + 1. **List tasks before acting**: output the "implementation task list" before asking questions or implementing. + 2. **Read before doing**: fully understand the design, boundaries, dependencies, and acceptance criteria before coding. + 3. **Align with project conventions**: directory, naming, dependencies, encapsulation, error handling, and existing project style must match. + 4. **Ask when something is missing**: confirm key decisions not specified in the document; unanswered items go into the pending list. + 5. **Make the result executable**: provide verification steps and external todos so the user can complete acceptance. + +--- + +## 2. Design Elements and Implementation Mapping (General) + +| Technical design content | Implementation action (land according to project conventions) | +| --- | --- | +| Requirement goal / scope / non-goals | Clarify the implementation boundary for this turn and avoid out-of-scope development. | +| Key flow / state transitions / sequence | Implement the main flow and branches, adding brief comments at key decision points. | +| Data structure / protocol / field constraints | Land type definitions, models, validators, or contract layers. | +| APIs / events / messages | Implement call entry points, event handlers, subscriptions, or callbacks, choosing based on design scope. | +| Pages / components / interactions | Implement UI structure, state management, interaction flow, and fault-tolerant prompts when the design covers UI. | +| Configuration / switches / environment differences | Register and read them in project-conventional locations; add defaults and fallback strategy. | +| Error codes / exception strategy / retries | Unify error returns and logging strategy, matching existing wrappers. | +| Release / routing / permissions / task scheduling | Implement the corresponding code and remind the user to finish platform-side configuration when relevant. | + +### Flowchart Handling (Important) + +- If the flowchart is a PDF/image without textual steps, first ask the user for a textual flow or supplementary document. +- If textual steps already exist, implement strictly in their order and branches. +- When branches cannot be confirmed, ask first, or implement with a default strategy and record it in the pending list. + +--- + +## 3. Execution Steps + +### Step 1: Normalize Input + +- PDF input: first run `f2s-doc-pdf` and obtain `.Knowledge/req-docs/*.md`. +- MD/text input: read directly. + +### Step 2: Understand the Design and Context + +1. Read the full technical design and extract: goals, scope, flow, APIs/interactions, data, configuration, dependencies, and acceptance conditions. +2. Read project conventions such as README, `.Knowledge/stock-docs/`, architecture notes, and existing modules to align implementation style. +3. If a flowchart lacks textual explanation, record the gap first and confirm it with the user in step 3. + +### Step 2.5: Output the Implementation Task List First (Mandatory) + +Before asking questions or coding, output a task list first (trim as appropriate for the design): + +```markdown +## Implementation Task List (Based on the Technical Design "xxx") + +| No. | Task | Notes | +| --- | --- | --- | +| 1 | Core structure and data contracts | Land types/models/validation rules and clarify inputs/outputs. | +| 2 | Business flow implementation | Implement the main path and branches according to the flowchart/text steps. | +| 3 | External capability integration | Implement external entry points such as APIs/events/page interactions. | +| 4 | Configuration and exception handling | Register configuration, handle errors, and add retry/fallback strategies. | +| 5 | Verification and closing | Provide self-test notes, pending list, and platform-side reminders. | +``` + +If `changeTracking.implement: true`, after outputting the task list, write this checklist to `.task/active//task.md` according to the `f2s-task` rule. + +### Step 2.6: Sync Change Tracking With `task.md` / `user-todos.md` (Only When `changeTracking.implement: true`) + +- Whenever work corresponding to an implementation task-list item is completed, use `Edit` **in the same session** to update the corresponding `[ ]` -> `[x]` in `.task/active//task.md`. Do not defer this to closing, and do not replace disk updates with verbal completion claims (see `f2s-task` "During execution" and "Interruption and session end"). +- Whenever an item appears during execution that **must be done by the user** (database changes, environment configuration, etc.), append it **in the same session** to `.task/active//user-todos.md` (see `f2s-task` "user-todos.md"). + +### Step 3: Ask Pre-Implementation Questions (Mandatory; Do Not Skip) + +Before coding, list all unclear items at once and ask the user to confirm. Common questions: + +- **Scope and acceptance**: what must be delivered in this turn, and what is explicitly out of scope; +- **Technical boundary**: which module/side to implement in (frontend, backend, script, data task, etc.); +- **Dependencies and contracts**: external APIs, message protocols, data sources, authentication method; +- **Configuration and environment**: configuration key, environment differences, defaults, and rollout strategy; +- **Flowchart gaps**: branch conditions, failure fallback, timeout and retry strategy; +- **Release constraints**: whether routing, permissions, scheduling, and deployment steps are ready. + +If the user does not answer an item, implement using a reasonable default or placeholder and mark it as "requires user confirmation" in the pending list. + +### Step 4: Implement According to the Task List + +Trim the order according to the design and the actual project. Recommended sequence: + +1. Land data/contracts and shared abstractions first; +2. Implement the main flow and core capability next; +3. Integrate entry layers such as APIs/pages/events/tasks next; +4. Finally add configuration, exception handling, logging, and test helpers. + +Requirements: reuse existing dependencies and wrappers; match project naming, directory, and style; keep key branches readable and maintainable. + +### Step 5: Closing Output (Mandatory) + +1. **Pending list (mandatory)**: list every item still requiring user or platform completion. +2. **Post-implementation reminder list (mandatory)**: remind about configuration, dependencies, data, release, permissions, scheduling, and similar items according to the actual scope. +3. **Verification suggestions (recommended)**: provide the minimal executable verification steps (local, test environment, or regression path). +4. **Persist user todos (only when `changeTracking.implement: true`)**: append the items from step 5 points 1-2 that **must be executed by the user** (database scripts, configuration, approvals, etc.) to `.task/active//user-todos.md` (create the file first if missing; see `f2s-task`). Do not leave them only in the conversation or at the end of the design without writing this file. +5. If `changeTracking.implement: true`: **first confirm** every item under `task.md` "Steps" is `[x]` (or canceled items are recorded in notes), then after satisfying the `f2s-task` archive gate, move `.task/active//` to `.task/completed/-/` and remove the corresponding entry from `todo.json`. Do not archive while any `[ ]` remains. + +--- + +## 4. Optional Additions + +- If the design naming is unclear, suggest a name first and ask the user to confirm. +- If the design scope is large, split delivery into "minimum viable version -> incremental iterations". +- If the user wants to consolidate knowledge into the KB, remind them that `f2s-kb-build` can later sync topics and routing. + +--- + +## 5. Constraints and Summary + +- PDFs must be converted to MD before entering the implementation flow. +- Do not skip step 2.5 (task list) or step 3 (pre-implementation questions) and code directly. +- If `changeTracking.implement: true`: do not skip step 2.6 (write back `task.md` checkboxes as implementation progresses and append `user-todos.md`); archiving must satisfy the `f2s-task` archive gate. +- The output must include a pending list and post-implementation reminder list. If `changeTracking.implement: true`, user-side items in those lists must be synced into `user-todos.md`. +- Keep the content general. Do not assume a "backend only" scenario; trim implementation objects according to the design's actual scope. + +When complete, a one-sentence summary may be used: this round of implementation based on the "xxx" technical design is complete, with pending items and verification suggestions provided; please complete the platform and environment-side configuration according to the checklist before acceptance. diff --git a/packages/core/templates/en-US/rules/f2s-intent-routing.md b/packages/core/templates/en-US/rules/f2s-intent-routing.md new file mode 100644 index 0000000..9704f97 --- /dev/null +++ b/packages/core/templates/en-US/rules/f2s-intent-routing.md @@ -0,0 +1,90 @@ +--- +description: Intent recognition: high-confidence operational intent automatically enters the corresponding f2s-* Skill, controlled by the intentRecognition switch +alwaysApply: true +--- + +# f2s Intent Routing + +## Preflight + +**Before applying this rule, read `flow2spec.config.json`:** + +- `intentRecognition: true` -> continue with this rule +- `intentRecognition: false` or missing field -> **skip all logic in this rule** and do not make any automatic invocation + +## Priority + +1. An explicit user `$f2s-*` command has the highest priority; execute the explicit command. +2. If the user clearly says "only discuss / don't change yet / don't execute / evaluate first / discuss the plan first", do not auto-invoke any Skill. +3. If an `f2s-*` flow is already in progress, stay in the current flow; do not automatically switch to another flow unless the user explicitly says "stop the current flow and switch to X". +4. **Incomplete requirements block auto-entering write-phase skills**: If the user asks for code changes but the requirement is incomplete, prefer `f2s-req-clarify`; do not directly enter `f2s-kb-feat` / `f2s-kb-fix`. Likewise, if the user asks to "draft a design / generate technical design" while the requirement still has clear open questions, prefer `f2s-req-clarify`; **do not** directly enter `f2s-req-tech`. If the user asks to "break down tasks / implement" but the design has not been written to disk yet, prefer `f2s-req-tech`; **do not** directly enter `f2s-req-plan` / `implement-tech-design`. +5. **Process-orchestration skills do not auto-chain to the next skill within the same turn (one allowed single-hop exception)**: After `f2s-req-clarify` / `f2s-req-tech` / `f2s-req-plan` / `f2s-doc-*` writes its deliverable to disk, **this turn** by default outputs only a one-line "document ready + next-step pointer" hint and stops; **the next skill must be explicitly triggered by the user in a new turn** and routed by this rule. **The only same-turn single-hop allowed**: after `f2s-req-clarify` writes the clarification document to disk, it auto-chains directly to `f2s-req-tech` (see the completion section of `skills/f2s-req-clarify/SKILL.md`); no further hop is allowed. After `f2s-req-tech` writes to disk it must not auto-chain to `f2s-req-plan` / `implement-tech-design`. +6. If the user is only asking, comparing, evaluating, or requesting an explanation, do not invoke a Skill. +7. For low-confidence intent or conflicting multiple intents, briefly state the candidate routes and ask a clarifying question; do not invoke a Skill. + +## Intent -> Skill Mapping + +When the user input **clearly triggers** one of the following operational intents and does not violate the priority rules above, the Agent may directly enter the corresponding Skill without waiting for a second confirmation: + +| Intent signal (examples; Chinese compatibility terms retained) | Skill to invoke | +|----------------|-----------| +| 需求澄清、PRD 澄清、帮我理清需求、澄清一下; requirement clarification, PRD clarification, help clarify requirements | `f2s-req-clarify` | +| 生成技术方案、出方案、技术设计; generate technical design, draft a plan, technical design | `f2s-req-tech` | +| 提交代码、git commit、帮我提交、快捷提交; commit code, git commit, help me commit, quick commit | `f2s-git-commit` | +| 新增能力、加功能、f2s-kb-feat; add capability, add feature, f2s-kb-feat | `f2s-kb-feat` | +| 修正实现规则、规则错了、f2s-kb-fix; fix implementation rule, the rule is wrong, f2s-kb-fix | `f2s-kb-fix` | +| 任务规划、创建任务; task planning, create task | `f2s-req-plan` | +| 知识库同步、全局同步、已实现能力同步; knowledge-base sync, global sync, sync implemented capability | `f2s-kb-sync` | +| 已有能力进知识库、多文件生成上下文; add existing capability to KB, generate context from multiple files | `f2s-kb-add` | +| 新增规则、口述规则、把这条记到知识库; add rule, spoken rule, record this in the KB | `f2s-kb-addRules` | +| 生成项目上下文、终稿生成上下文; generate project context, generate context from final draft | `f2s-kb-build` | +| 合并上下文冲突、解决知识库冲突; merge context conflict, resolve KB conflict | `f2s-kb-merge` | +| 知识库迁移、旧版迁移; knowledge-base migration, legacy migration | `f2s-kb-migrate` | +| 删除项目上下文; delete project context | `f2s-kb-rm` | +| 知识库模板升级、知识库升级、一键升级迁移; KB template upgrade, KB upgrade, one-click upgrade migration | `f2s-kb-upgrade` | +| 项目架构说明、架构初稿; project architecture description, architecture draft | `f2s-doc-arch` | +| 转成终稿模版、f2s-doc-final; convert to final-overview-template, f2s-doc-final | `f2s-doc-final` | +| 生成项目里程碑、里程碑; generate project milestones, milestones | `f2s-doc-milestone` | +| PDF 转 MD; PDF to MD | `f2s-doc-pdf` | + +## Decision Boundary + +**Invoke**: the user clearly initiates an operational intent with high confidence. + +- "帮我做需求澄清" / "help me clarify requirements" -> invoke `f2s-req-clarify` +- "生成一份技术方案" / "generate a technical design" -> invoke `f2s-req-tech` +- "修复这个 bug,表现是 X,期望是 Y" / "fix this bug; behavior is X, expected Y" -> invoke `f2s-kb-fix` +- "新增这个配置开关,默认 false,影响范围是 X" / "add this config switch, default false, scope X" -> invoke `f2s-kb-feat` + +**Do not invoke**: the user is asking or discussing rather than initiating an operation. + +- "这个需求需要澄清吗?" / "does this requirement need clarification?" -> answer the question first +- "技术方案一般怎么写?" / "how is a technical design usually written?" -> answer the question first +- "f2s-req-tech 是干什么的?" / "what does f2s-req-tech do?" -> answer the question first +- "我们讨论一下这个能力怎么做" / "let's discuss how to build this capability" -> discuss first; do not enter implementation +- "我想加一个能力,但还没想清楚" / "I want to add a capability but haven't thought it through" -> clarify or ask back; do not enter feat + +**Decision basis**: whether there is clear action semantics such as "help me do X", "execute X", or "start X". Pure questions, discussion, and evaluation do not trigger routing. + +## Routing Notice + +Before automatically entering a Skill, state the routing reason in one sentence: + +```text +I will handle this with : . +``` + +For low confidence, only output the candidates and a clarifying question: + +```text +This may be or ; the current request is missing , so confirm first before entering a flow. +``` + +## Prohibited + +- Automatically invoking any Skill when `intentRecognition` has not been read or is `false` +- Misclassifying question-style input as operational intent +- Automatically jumping to feat/fix/plan/tech before requirement clarification is complete +- Automatically jumping to `f2s-req-plan` / `implement-tech-design` before the technical design has been written to disk +- Auto-chaining to the next `f2s-*` skill within the **same turn** that a process-orchestration skill (`f2s-req-clarify` / `f2s-req-tech` / `f2s-req-plan` / `f2s-doc-*`) writes its deliverable (**only exception**: `f2s-req-clarify` → `f2s-req-tech` single hop; `f2s-req-tech` must not auto-chain further) +- Automatically switching to another Skill before the current flow is complete diff --git a/packages/core/templates/en-US/rules/f2s-karpathy-guidelines.md b/packages/core/templates/en-US/rules/f2s-karpathy-guidelines.md new file mode 100644 index 0000000..aaff89e --- /dev/null +++ b/packages/core/templates/en-US/rules/f2s-karpathy-guidelines.md @@ -0,0 +1,77 @@ +--- +description: Karpathy-style coding behavior guidelines: clarify assumptions first, implement minimally, change only what is necessary, and execute against verifiable goals. Coexists with f2s-* rules; process hard constraints defer to f2s. +alwaysApply: true +--- + +# Karpathy-Style Coding Behavior Guidelines + +> Runs **in parallel** with the project's Flow2Spec / `f2s-*` rules. If any item conflicts with a mandatory f2s step, **f2s and project conventions take precedence**. + +Behavior conventions for reducing common "model writes code" mistakes. + +**Tradeoff:** These guidelines favor **carefulness over pure speed**. For obviously tiny changes, such as a one-line typo, use judgment instead of applying every item rigidly. + +## 1. Think Clearly Before Writing Code + +**Do not assume, do not hide confusion, and put tradeoffs on the table.** + +Before implementing: + +- **State assumptions clearly**; ask when uncertain instead of guessing. +- **List multiple possible interpretations** when they exist; do not silently choose one and proceed. +- **Propose the simpler approach** when one exists; push back when pushback is warranted. +- **Stop when unclear**: name the confusion and ask the user for information. + +## 2. Prefer Simplicity + +**Solve the problem with the least code; do not add speculative extensions.** + +- Do not add features beyond the request. +- Do not create abstractions for code used only once. +- Do not add unrequested "flexibility" or "configurability". +- Do not pile on error handling for scenarios that are nearly impossible. +- If you wrote 200 lines where 50 are enough, **rewrite it**. + +Ask yourself: "Would a senior engineer consider this over-designed?" If yes, simplify. + +## 3. Make Surgical Changes + +**Change only what needs changing; clean up only what your change disturbed.** + +When editing existing code: + +- Do not casually "optimize" adjacent code, comments, or formatting. +- Do not refactor things that are not broken. +- **Match the existing code style**, even if your personal preference differs. +- If you notice dead code unrelated to the task, **you may mention it; do not delete it on your own**. + +If your change creates orphaned references or variables: + +- **Remove imports, variables, and functions that became unused because of this change**. +- **Do not** delete dead code that already existed unless the user asked for it. + +Validation standard: **every changed line can be traced back to the user's explicit request**. + +## 4. Execute Against Goals + +**Define success criteria first, then iterate until they are verifiably met.** + +Turn the task into verifiable goals, for example: + +- "Add validation" -> "write an invalid-input test first, then change code until it passes" +- "Fix bug" -> "write a reproducing test first, then change code until it passes" +- "Refactor X" -> "the test suite passes before and after" + +For multi-step tasks, a short plan can be written: + +``` +1. [Step] -> Verify: [check method] +2. [Step] -> Verify: [check method] +3. [Step] -> Verify: [check method] +``` + +The more concrete the success criteria, the easier it is to iterate independently; vague "just make it work" goals tend to cause repeated clarification. + +--- + +**Signals that the guidelines are working:** fewer unrelated changes in diffs, less rework from over-design, and **clarifying questions appear before implementation** rather than after a wrong implementation. diff --git a/packages/core/templates/en-US/rules/f2s-kb-feedback-closing.md b/packages/core/templates/en-US/rules/f2s-kb-feedback-closing.md new file mode 100644 index 0000000..49630de --- /dev/null +++ b/packages/core/templates/en-US/rules/f2s-kb-feedback-closing.md @@ -0,0 +1,117 @@ +--- +description: Closing rule for knowledge-base follow-up suggestions after source code was read during ordinary Q&A; only suggest f2s-kb-distill, do not write automatically +alwaysApply: true +--- + +# Flow2Spec Knowledge-Base Feedback Closing + +This rule governs knowledge-base follow-up suggestions after ordinary Q&A reads business source code. It only decides whether the final answer should append one minimal suggestion. + +## Scope + +Run this rule only when all of the following are true: + +- This turn is **ordinary Q&A / troubleshooting / explanation**; +- This turn has **not entered** an `f2s-*` skill, `implement-tech-design`, `f2s-git-commit`, or another existing follow-up flow; +- This turn read business source code and the final answer cites source-code facts. + +**Prohibited**: Do NOT output **any** of this rule's case 1–4 closing blocks in either of the following situations — + +1. **This turn has already entered `f2s-kb-distill`**: `f2s-kb-distill` is the skill that ingests this turn's knowledge into the KB; appending its own ingestion hint is both redundant and self-referential. +2. **This turn entered a process-orchestration skill**: `f2s-req-clarify` / `f2s-req-tech` / `f2s-req-plan` / `f2s-doc-arch` / `f2s-doc-final` / `f2s-doc-milestone` / `f2s-doc-pdf`. The deliverable of these skills is a **`.Knowledge/req-docs/*`, `docs/*`, or task-planning artifact for this specific delivery**; reading source code serves that deliverable, it is not "picking up a piece of general knowledge on the side". Even if source code was read and its facts were written into the clarification / design / planning document, **do not** append a distill hint (clarification / design docs live under `req-docs`, not under `topics` / `stock-docs`; planning artifacts are archived with the task; doc skills each have their own write target). + +Other `f2s-kb-*` skills (e.g., `f2s-kb-feat` / `f2s-kb-fix` / `f2s-kb-sync`) **still judge per the four cases** after they finish: if this turn's answer contains reusable knowledge facts **outside the main path** that the current SKILL **did not ingest** (typical scenarios: while fixing a bug you incidentally read another module's source, or you answered a follow-up unrelated to the current SKILL's main subject), output the closing block as usual; the agent judges by what was actually written this turn, not by a blanket prohibition. + +## Judgment Timing and Basis + +**Judgment timing**: After generating the final answer, judge based on the knowledge content actually included in the answer, not the reading process. + +**Judgment basis**: +- What knowledge did the final answer supplement that the KB did not write or wrote insufficiently +- Does this knowledge belong to "reusable knowledge facts" +- Not: all files/information encountered during the reading process + +**Reusable knowledge facts** include: +- Core mechanisms (e.g., cache semantics, retry strategy, fallback logic) +- State transitions (e.g., order state machine, session lifecycle) +- Return value / error code contracts (e.g., HTTP status code semantics, business error code meanings) +- Configuration switch impacts (e.g., switch X affects behavior Y) +- Failure fallback strategies (e.g., fallback plan when primary path fails) +- Module boundaries or calling conventions (e.g., module A calling module B contract) +- Data models and field semantics (e.g., business meaning of key fields) + +**Evidence only, do not trigger sync** includes: +- Line numbers (e.g., `client.py:51`) +- Function names (e.g., `send_message_to_session()`) +- Code snippets (concrete implementation code for demonstration) +- Call paths (e.g., `A → B → C` call chain) +- Local implementation expanded to answer user follow-up questions +- Source-code verification of facts already written in the topic (KB wrote it clearly, source code only confirms) + +## Mechanical Gate + +- After reading the first business source file, treat this turn as having triggered `sourceFallbackUsed=true`. +- When `sourceFallbackUsed=true` and the final answer cites source-code facts, this four-case self-check must run before sending the answer. +- **One of the four cases must be stated explicitly**: every closing pass must choose cases 1-4 and **output the corresponding block explicitly**; silently skipping the whole closing flow is not allowed. +- Decision logic: + - topic matched + final answer supplemented "reusable knowledge facts" → use **case 2** + - topic not matched + final answer supplemented "reusable knowledge facts" → use **case 1** + - topic matched + final answer only contains "evidence content" (line numbers/function names/call paths) + KB already wrote core facts clearly → use **case 4** + - If the gap noted before drilling down was **mechanism/contract/process-type knowledge gap**, use **case 2** afterwards + - If the gap noted before drilling down was only **evidence/source-code location/line number/implementation provenance gap**, and the topic already covers core facts, may use **case 4** + +## Four Closing Cases + +1. **KB does not cover it + source code provided the answer**: append this at the end of the answer: + ```md + > 💡 Run `f2s-kb-distill` to ingest knowledge from this turn + > + > **This turn will ingest**: + ``` + **Decision criteria**: no topic covers this capability / module / problem domain, and the final answer supplemented reusable knowledge facts. + +2. **KB covers it but lacks detail + source code completed the answer**: append this at the end of the answer: + ```md + > 💡 Run `f2s-kb-distill` to ingest knowledge from this turn + > + > **This turn will ingest**: `> + ``` + **Decision criteria**: an existing topic covers the direction but lacks details, and the final answer supplemented reusable knowledge facts (core mechanisms, state transitions, contracts, etc.). + +3. **KB and source code disagree**: answer according to source-code facts and append this at the end of the answer: + ```md + > 💡 Run `f2s-kb-distill` to ingest knowledge from this turn + > + > **This turn will ingest**: ` to fix that disagrees with source code"> + ``` + +4. **KB fully covers it; source code was only verification**: append this at the end of the answer: + ```md + > **Knowledge base already covers this**: the core facts in this answer were fully provided by ``; source-code reading was only verification. + ``` + **Decision criteria**: + - The relevant KB topic already states the core answer to this question (mechanisms, transitions, contracts and other reusable knowledge facts) + - This turn's final answer did not introduce new reusable knowledge facts outside the KB + - Source code cited in the answer was only for evidence (line numbers, function names, call paths) or to verify KB-written content + - If the gap noted before drilling down mentioned mechanism/contract/process-type knowledge gap, case 4 is prohibited + +> **Summary requirement (required for cases 1-3)**: one line stating "what this distill run will ingest" — capability / module name + knowledge type (mechanism / transition / contract / config etc.) + whether this is first ingestion or supplementing some topic. **Do not** post only the command without the summary; the summary is what lets the user decide whether to actually run distill. + +## Boundary Between Case 1 and Case 2 + +- **case 1** (`f2s-kb-distill` uncovered scenario): no topic covers this capability / module / problem domain + - Example: user asks "module X's retry mechanism", but manifest has no topic related to module X + - Example: user asks "implementation of new feature Y", but KB has no document or topic about feature Y + +- **case 2** (`f2s-kb-distill` supplement scenario): an existing topic covers the direction but lacks details + - Example: topic wrote "cache-first strategy" but did not write concrete failure-fallback logic + - Example: topic wrote "action-chain judgment" but did not write concrete state-checking method + +This avoids misjudging "already has a topic but still suggests add". + +## Output Format + +- Cases 1-3: output one Markdown blockquote containing, in order, the `f2s-kb-distill` command + one blank line + the **This turn will ingest** summary (one line, see "Summary requirement" above). +- Case 4: output one Markdown blockquote stating "Knowledge base already covers this" plus the related topicId. +- Do not omit this block; do not output a list of KB paths read, a coverage comparison table, explanations, or multi-line background. +- Only suggest; do not automatically run `f2s-kb-distill`. diff --git a/packages/core/templates/en-US/rules/f2s-knowledge-preflight.md b/packages/core/templates/en-US/rules/f2s-knowledge-preflight.md new file mode 100644 index 0000000..cd6b6af --- /dev/null +++ b/packages/core/templates/en-US/rules/f2s-knowledge-preflight.md @@ -0,0 +1,72 @@ +--- +description: Even ordinary questions must first read the .Knowledge machine-readable routing before searching code; hard constraint on the first tool call +alwaysApply: true +--- + +# Flow2Spec KB Preflight + +This rule coexists with `f2s-flow2spec-unified-entry`; for answers involving implementation, configuration, troubleshooting, or Flow2Spec knowledge routing **inside the current repository**, this rule determines **when the on-disk knowledge base must be read first**. The read order in the unified entry continues to apply after this rule has been satisfied. + +## Scope (Preflight Required) + +If the user question may depend on any of the following information, it counts as requiring knowledge-base preflight: + +- **Implementation code** in the current repository, directory and module conventions, build/deploy/runtime behavior, `.Knowledge/`, `f2s-*` skills, topic routing described by `manifest-routing`, and similar repository facts; +- Context that clearly depends on current-repository facts, unless the user explicitly states it is unrelated to the current repository. + +## Hard Constraint: First Tool Call + +Before giving a substantive conclusion or modification suggestion: + +1. **For this user message**, if no tool has yet read **`.Knowledge/manifest-routing.json`**, then the **first** code/knowledge-base tool used must be: + + `Read` -> path **`.Knowledge/manifest-routing.json`** (relative to the project root, consistent with the unified entry). + +2. After reading the manifest, `Read` a **single** matcher shard and **`.Knowledge/topics/.md`** (plus `topicDependencies`) **as needed** according to `taskToTopicRules` / `matcherPath`. Only then may `SemanticSearch`, `Grep`, or `Read` be used on business source paths **outside `.Knowledge/`**. + +3. **Prohibited**: without step 1, directly asserting repository-specific paths, configuration, or behavior from memory/training data. If the manifest or topic already covers the point, **the KB is authoritative**; source code may verify or fill in details missing from the KB. + +4. **At the end of the answer (one short line is enough)**: state the KB paths used this turn, for example "Read manifest + `topics/.md`". If the manifest did not match and the `fallbackTopic` topic was read, state that fallback triage was used. + +## Rare Cases Where Preflight Can Be Skipped + +- The user only asks about **IDE/editor usage itself**, unrelated to the current repository directory; +- The user provides an **absolute path + explicit instruction** (for example "only change this line to x") and the edit is purely mechanical and unrelated to business knowledge; +- In the **same session**, `.Knowledge/manifest-routing.json` has already been read for the current workspace and the user has not requested "reroute / full check"; for a direct follow-up, the answer may begin with "manifest was read earlier in this session; continuing with the previous route" and avoid reading the manifest again. + +## Answer Closing Check (After Source-Code Follow-Up) + +Knowledge-base follow-up suggestions after ordinary Q&A reads business source code are governed solely by **`f2s-kb-feedback-closing`**. This rule only keeps the trigger relationship: after reading the first business source file, treat this turn as having triggered `sourceFallbackUsed=true`; if the final answer cites source-code facts, the four-case self-check in `f2s-kb-feedback-closing` must run before sending the answer. If this turn has already entered an `f2s-*` skill, `implement-tech-design`, `f2s-git-commit`, or another existing follow-up flow, do not repeat the suggestion. + +Consistent with **"Knowledge gaps and responses"** in **`f2s-flow2spec-unified-entry`**, when case **1b (matched but context is insufficient)** or **2 (the KB has no corresponding document)** is reached, also obey: + +1. **Explain to the user before expanding tools**: after reading `manifest-routing.json` and the required `topics/*.md` (plus dependency topics), if the KB alone still cannot answer the user question precisely, **first** state in natural language: **which KB paths were read**, **what information is still missing**, and **which 1-2 source files you plan to read** or **which `req-docs`/stock-docs document the user should provide**. Do not silently stack "find another entry point" style exploration. +2. **Exploration limit**: before giving the gap note above, do **not** launch **4 or more consecutive** `Grep` calls or targetless `SemanticSearch` calls whose only purpose is broadening the search surface. After the note is given and the user tacitly allows it (or the question explicitly asks to chase it down), drill down in an orderly way. +3. **Prefer single-point drilling**: if only behavior details need confirmation, prefer to **Read one** implementation file most relevant to the question and answer from it. Do not chain into multiple files under third-party dependency directories for the same subquestion without a new hypothesis, unless the user explicitly asks to read dependencies thoroughly. + +### Gap Gate (For "Rule Exists, Execution Skipped") + +Only when **manifest + required topics** have already been read, the situation is still judged as **1b / 2**, and the **next step** is to use `Read` / `Grep` / `SemanticSearch` against the **business source tree** (not `.Knowledge/`): + +- **First output** a piece of **visible natural language for the end user** (it may be shown with a brief conclusion) that includes at least: **(a)** KB paths read; **(b)** one sentence describing the gap (what kind of information the topic lacks or that the KB has no document); **(c)** the **1-2 concrete file paths** to open next, or a question asking whether the user would rather add `req-docs`/stock-docs first. +- **Prohibited**: if no visible note satisfying (a)(b)(c) has ever been output, do not launch multiple source-side tool calls used only to "find another entry point"; this is exactly the execution-level omission where "the rule says it, but the gap note was skipped." +- When source drilling later reports facts such as **behavior, status codes, or error text**, those facts **must come from source code and contracts actually read in this turn**; do not fill them from speculation or external project experience unrelated to the current repository. + +The (a)(b)(c) requirements above have the same meaning as "ask the user for a document or path" and "explicitly acknowledge no KB coverage" in the **`f2s-flow2spec-unified-entry`** table. Do not substitute an internal judgment of "this is 1b" for a **written visible gap note** to the user. + +### Interaction With Execution Environments (Permissions and Confirmation Noise) + +In IDEs with sandbox or permission gates, launching many `Grep` / `SemanticSearch` / broad file reads in a short time often appears as repeated prompts for the same kind of permission or confirmation. This is not directly caused by whether Flow2Spec rules mention the constraint; it is usually amplified by an exploration chain that is too long and lacks stop conditions. Following this section's "gap gate", "exploration limit", and the "search volume and answer rhythm" below, while preferring single-file `Read`, can substantially reduce such interruptions. + +## Search Volume and Answer Rhythm (Reduce Multi-Round Scans and Perceived Slowness) + +This section targets common causes in **Codex / terminal IDE** environments where a single Q&A turn performs multiple rounds of `grep`, produces huge output, and takes a long time. It does **not conflict** with "read manifest first": still `Read` the manifest first, then narrow the search surface. + +1. **`Grep` / text-search scope**: when the matched topic has already been read and gives a **specific file or directory path**, the search scope must not exceed that path. If no path is given, narrow to the **single** most likely directory, such as `src/utils/` or `src/functions//`. **Prohibited**: without an explicit user request for "full-repo check" and without meeting the unified entry's "full matcher fallback search trigger", do not run one huge broad scan across multiple parallel roots such as **`src/` root, all of `src/functions`, and `.Knowledge`**. +2. **When matches are excessive**: if one search returns obviously too many hits, stop expanding keywords or paths and instead prefer to **Read the 1-2 main files named by the topic or stock-docs**. If still insufficient, perform a **second** narrow `Grep` with a smaller pattern or directory. +3. **Two-stage answers**: if the user did not explicitly ask to "list all implementation details / read dependencies thoroughly / audit the full chain", and manifest + required topics (plus any stock/req materials named by the topic) are already enough to form a conclusion, **first output a short useful answer**. Implementation details and additional file lists should be drilled only when the user asks for evidence or expansion. Do not lengthen the exploration chain solely for self-verification completeness. +4. **Avoid repeated disk reads**: in the same session, if a file has already been fully `Read` and there is no new user instruction or hypothesis, do **not** launch an equivalent full-file `Read` again. + +## Agent Self-Check + +If you notice that you are answering a current-repository question without having `Read` the manifest, **stop writing immediately**, `Read` the manifest and the matched topic, then correct or continue the answer. If this turn read business source code and cites source-code facts, continue with `f2s-kb-feedback-closing` before sending the answer. diff --git a/packages/core/templates/en-US/rules/f2s-stock-docs-vs-req-docs.md b/packages/core/templates/en-US/rules/f2s-stock-docs-vs-req-docs.md new file mode 100644 index 0000000..1c41861 --- /dev/null +++ b/packages/core/templates/en-US/rules/f2s-stock-docs-vs-req-docs.md @@ -0,0 +1,16 @@ +--- +description: Distinguish .Knowledge/stock-docs (existing context) from .Knowledge/req-docs (requirements and technical designs); do not mix paths or downstream targets +globs: + - "**/.Knowledge/stock-docs/**/*.md" + - "**/.Knowledge/req-docs/**/*.md" +alwaysApply: false +--- + +> **Single long-form rule**: this file is the complete convention for **f2s-doc-routing**. `.Knowledge/topics/f2s-stock-docs-vs-req-docs.md` is only a routing summary; **Codex** reads `.codex/topics/f2s-stock-docs-vs-req-docs.md` (automatically mirrored from this file by `flow2spec init`) as the equivalent rule text. + +# stock-docs and req-docs + +- **`.Knowledge/stock-docs/`**: **existing source documents** such as PDFs, drafts, final drafts, and architecture notes. Document writes from `f2s-kb-build`, `f2s-doc-final`, `f2s-doc-arch`, and `f2s-kb-add` should prefer this directory. Always write `sourceDoc` as `.Knowledge/stock-docs/.md`. +- **`.Knowledge/req-docs/`**: requirement clarifications, technical designs (frontend/backend/data/tasks, etc.), and "implement from design" MD files output by `f2s-doc-pdf`. The trigger scope for `implement-tech-design` is `.Knowledge/req-docs/**/*.md`. + +For the complete convention, see this rule and **`skills/f2s-doc-routing/SKILL.md`**; `.Knowledge/topics/f2s-stock-docs-vs-req-docs.md` is the routing summary. diff --git a/packages/core/templates/en-US/rules/f2s-task.md b/packages/core/templates/en-US/rules/f2s-task.md new file mode 100644 index 0000000..548ada2 --- /dev/null +++ b/packages/core/templates/en-US/rules/f2s-task.md @@ -0,0 +1,305 @@ +--- +name: f2s-task +description: > + Change tracking: automatically create and maintain task checklists under .task/ during code changes, supporting continuation across sessions. + The corresponding skill takes effect only when at least one of changeTracking.feat / fix / implement in flow2spec.config.json is true. + Trigger words: changeTracking, task tracking, change tracking, continue work, resume previous task; 任务追踪、变更追踪、续作、继续上次任务 +alwaysApply: true +--- + +# f2s-task (Change-Tracking Rule) + +## Effective Conditions + +Each skill checks its own subfield: + +- `f2s-kb-feat`: read `changeTracking.feat` +- `f2s-kb-fix`: read `changeTracking.fix` +- `f2s-implement-tech-design`: read `changeTracking.implement` + +If the corresponding subfield is `false` or missing, **the change-tracking steps inside that skill do not run** and are skipped directly. + +> The `f2s-req-plan` command is not constrained by this condition and always runs (see `skills/f2s-req-plan/SKILL.md`). + +## Multi-developer collaboration and `TASK_ROOT` (resolve first) + +Before any read/write under `.task`, **must** `Read("flow2spec.config.json")` and resolve **`TASK_ROOT`** (fixed for the session; do not change id mid-session): + +| Condition | `TASK_ROOT` | `developerId` source | +| --- | --- | --- | +| `collaboration.enabled === false` | `.task` | legacy (force single-root) | +| non-empty `collaboration.developerId` (after trim) | `.task/` | **config** | +| else `git config user.email` available | `.task/` | **git-email** | +| else `git config user.name` available | `.task/` | **git-name** | +| still none | `.task` | **legacy** (warn: set `collaboration.developerId`) | + +**sanitize**: lower-case; if `@` present take local part only; non `[a-z0-9]` → `-`; trim `-`; length 1–64 or treat as missing. + +**All paths use `TASK_ROOT`**: + +- index: `TASK_ROOT/todo.json` +- active: `TASK_ROOT/active//` +- completed: `TASK_ROOT/completed/-/` + +**Anti cross-talk (hard)**: + +1. Read/write **only** this session's `TASK_ROOT`; **do not** scan `.task/*/todo.json` or other developer dirs for resume. +2. Keyword match **only** entries in current `TASK_ROOT/todo.json`. +3. New task `folder` must be under current `TASK_ROOT/active//`. +4. Optionally echo: `[task] developerId= TASK_ROOT=`. +5. **`.Knowledge/` stays shared**; this rule does not per-developer the knowledge base. + +> Implementation reference: package `lib/developerId.js` (`resolveDeveloperContext` / `taskRootFor`). + +## Binding When f2s-req-plan Is Invoked + +When executing **`f2s-req-plan`** (or continuing a task matched by `linkedSkill: "f2s-req-plan"`): + +- It is **not constrained** by `changeTracking.feat` / `fix` / `implement`, but **must** maintain the task tree under **`TASK_ROOT`** per this rule. +- Skill **step 0** must `Read` this full rule (**Cursor/Claude**: `rules/f2s-task.*`; **Codex**: `.codex/topics/f2s-task.md`). +- Disk writes, checkbox updates, archiving, and `user-todos.md` / `acceptance.md` format **are governed by this rule**. + +## Directory Structure + +``` +TASK_ROOT/ <- `.task` or `.task/` +├── todo.json <- active task index, written only by the main agent +├── active/ +│ └── / +│ ├── task.md +│ ├── context.md +│ ├── user-todos.md +│ └── acceptance.md +└── completed/ + └── -/ + ├── task.md + ├── context.md + ├── user-todos.md + └── acceptance.md +``` + +**Archive directory naming**: **`-`** under `completed/`. + +**Migration from single-root**: if root `.task/active/` still exists while `TASK_ROOT=.task/`, move only after user confirmation. + +## todo.json Structure + +```json +[ + { + "name": "task name", + "folder": "TASK_ROOT/active//", + "keywords": ["keyword1", "keyword2"], + "linkedSkill": "f2s-kb-fix", + "createdAt": "YYYY-MM-DD", + "assignee": "" + } +] +``` + +**Write ownership constraint**: `todo.json` is written only by the main agent; sub agents must not modify it. + +## Task Start (Before Code Changes) + +0. Resolve and fix **`TASK_ROOT`** (and developerId / legacy) as above. +1. Check whether `TASK_ROOT/todo.json` contains active tasks. +2. Match user input against **that file's** `keywords` only (**do not** read other roots): + - One match -> load `task.md` / `context.md` / optional `user-todos.md` + - Multiple matches -> ask user to choose + - No match -> create a new task +3. Create a new task (when there is no match): + a. Confirm snake_case task name + b. Create `TASK_ROOT/active//` + c. Write steps into `task.md` + d. Write paths into `context.md` + e. **Create `user-todos.md`** + f. Append entry to `TASK_ROOT/todo.json` (main agent only; `folder` points at this task dir) + +## During Execution + +- Each time a step is completed, **immediately** use `Edit` / `Write` to change the corresponding checkbox in `task.md` from `[ ]` to `[x]` (treat this like a code change; **do not** rely only on verbal "completed" claims in the conversation). +- Do not batch-check boxes or skip steps. +- **User todos must be persisted**: whenever an item must be completed by the task owner (the user) on the local machine, in a database, on a configuration platform, or in a process (for example running DDL/DML, entering secrets, clicking approvals, releasing, or backfilling data), append it to `user-todos.md` **in the same session** (`Edit` a new section or list item). **Do not** only mention it in the conversation without writing it to this file. It may also appear in the conversation summary; the disk file is the handoff source of truth. + +## Interruption and Session End (Hard Constraints) + +- **Long memory uses checkboxes in `task.md` as the source of truth**: the next session locates progress by the first step still marked `[ ]`; if not written to disk, continuation becomes inaccurate. +- Each time a real step listed in `task.md` is completed in this session: check it off **at that step**. Do not postpone all checkbox updates until archiving. +- If the user ends the conversation, the tool flow is interrupted, or you expect you cannot continue: before ending, check off at least the steps that were truly completed, and write the blocking reason or "continue from step N next session" under "## Notes". **Do not** end directly without updating `task.md` (that is equivalent to losing the progress signal). +- If this session has identified **user todos** before interruption: **write or append them to `user-todos.md`** so the next session does not lose what was handed to the user. +- If this session created a **`git worktree`** or equivalent isolated directory for a subtask: before ending, follow **`f2s-flow2spec-unified-entry`** "Git worktree and subtask working-directory hygiene" to remove it or record the leftover path and deletion command (write it to `user-todos.md` when needed). + +## Task Completion + +**Archive gate (self-check before moving directories)**: + +- Move the directory into `completed/` **if and only if** every item under "## Steps" in `task.md` that is related to this delivery is **`[x]`** (or items explicitly canceled by the user are explained under "## Notes", and the corresponding list item has been changed to `[x]` / deleted with a cancellation note). +- After every `task.md` item is `[x]` and before moving the directory, `acceptance.md` **must** have already been created or updated (see "`acceptance.md` format and disk-write obligation" below). A missing `acceptance.md`, or one still containing only the placeholder note from task creation, fails the gate; archiving is forbidden. +- If any `[ ]` remains: **do not** move `active` -> `completed/`, and **do not** remove the entry from `todo.json`; first return to "During execution" to finish the work or adjust the checklist, then archive. + +After the gate passes: + +1. Move `TASK_ROOT/active//` as a whole to `TASK_ROOT/completed/-/`. +2. Remove the entry from `todo.json`. +3. If `todo.json` becomes an empty array, delete that file. + +## New-Session Continuation + +At the start of a new session, resolve **`TASK_ROOT` first**; if `TASK_ROOT/todo.json` exists: + +1. Read all active tasks. +2. Match the user's first message against each entry's `keywords`. +3. If matched, show the remaining checklist. **If `user-todos.md` exists, summarize any user todo items still marked `- [ ]`**; **if `acceptance.md` exists, report its current state** (placeholder / final; final form is required before archiving). Ask "An unfinished task was detected. Continue?" +4. After the user confirms: **if `linkedSkill` is non-empty, first load the corresponding skill rule file (configuration-root `skills//SKILL.md`) as execution context**, then continue according to the remaining steps in `task.md`. The skill's disk-write constraints, writing style rules, and self-check checklist all apply as they did on the first invocation. +5. If there is no match, do not interrupt; respond normally. + +**Orphaned `active/` directories (`todo.json` missing or damaged)**: if `TASK_ROOT/active//` still exists on disk and its `task.md` contains unchecked steps, `Read` that `task.md` and ask the user whether to continue. Before continuing, it is recommended to restore or rewrite `todo.json` according to "Task start" (main agent only), so progress is not trapped in directories without an active index. + +## task.md Format + +```markdown +# + +## Steps +- [ ] Step 1 +- [ ] Step 2 +- [x] Step 3 (completed) + +## Notes + +``` + +## context.md Format + +```markdown +# Context + +## Involved Files +- `src//callback.js` +- `src//retry.js` + +## Related Materials +- `.Knowledge/req-docs/-spec.md` +- `.Knowledge/stock-docs/-arch.md` + +## User Todo List +- See `user-todos.md` in the same directory (items that the user must execute are centralized in that file; do not list them only in the conversation) + +## Acceptance +- See `acceptance.md` in the same directory (generated after every `task.md` item is `[x]` and before archiving) +``` + +## `user-todos.md` Format and Disk-Write Obligation + +**Path**: `TASK_ROOT/active//user-todos.md` (after archiving: `TASK_ROOT/completed/-/user-todos.md`). The filename **must be exactly** `user-todos.md` so hooks and scripts can reference it. + +**Purpose**: collect items that **the Agent cannot do on behalf of the user** and that must be completed by the user (or a privileged operator on a platform), for example: + +- Run SQL / migration scripts in a specified environment (may reference `req-docs` or repository `.sql` paths) +- Configuration center / environment variables / secrets / allowlists +- Release, approvals, tickets, external-system switches + +**Disk-write obligation**: + +1. **When creating a task** (`f2s-task` "Task start" step 3.e): create this file; it may contain a short note plus an empty list. +2. **During execution**: each time a new category of user todo appears, append it **in that turn** (recommended: second-level heading by date `## YYYY-MM-DD`, followed by `- [ ]` checklist items or step numbers). +3. **Division from `task.md`**: `task.md` tracks Agent-side step checkboxes; `user-todos.md` tracks user-side pending items. **Do not** write long "user-only" operation instructions only in `task.md` as a substitute for this file. +4. **Continuation**: when loading a task, `Read` this file and show the user any `- [ ]` items that remain unchecked. + +**Example structure**: + +```markdown +# User Todo List + +> Appended by the Agent; after completion, the user may change the corresponding `- [ ]` to `- [x]` or delete the line. + +## 2026-05-09 + +- [ ] Execute in the target environment: `.Knowledge/req-docs/xxx.sql` (back up first) +- [ ] Enable feature switch `feature.foo.enabled` in the configuration center + +## 2026-05-10 + +- [ ] After production release, write the actual version number back into this document's notes +``` + +## `acceptance.md` Format and Disk-Write Obligation + +**Path**: `TASK_ROOT/active//acceptance.md` (after archiving: `TASK_ROOT/completed/-/acceptance.md`). The filename **must be exactly** `acceptance.md`, kept in the same directory as `task.md` / `user-todos.md`. + +**Purpose**: after every `task.md` item is `[x]` and before archiving, the Agent distills the **acceptance checklist** based on what was actually delivered this round. The user can verify item by item that "this task is truly done." Responsibilities are **separated** from `user-todos.md`: + +| File | Who acts | Focus | +| --- | --- | --- | +| `task.md` | Agent | Progress checkboxes for implementation steps | +| `user-todos.md` | User | **Todos**: things the Agent cannot do; the user must run them externally (database / platform / approval) | +| `acceptance.md` | User | **Acceptance**: deliverables produced by the Agent this round; the user verifies they actually work | + +**Scope of effect**: generated for any task that uses `.task/` — both automatic mode (`changeTracking.feat` / `fix` / `implement`) and explicit mode (`f2s-req-plan`); not skill-specific. + +**Disk-write obligation**: + +1. **When creating a task** (after `f2s-task` "Task start" step 3.e): `acceptance.md` **may** be created at the same time with a placeholder note (e.g. "After every `task.md` item is `[x]`, the Agent fills in the acceptance checklist here"). **Do not** prewrite acceptance items before implementation; that would risk drifting from the final delivery. +2. **During execution**: in principle **do not write**; if the delivery boundary materially shifts, add a one-line record under "## Notes" and consolidate when finalizing. +3. **After every `task.md` item is `[x]` and before archiving** (**required**): the Agent compiles the formal acceptance checklist based on the actual changes; placeholder notes must be replaced by the final content. **This is the archive gate** (see "Task Completion"). +4. **Continuation**: when loading the task, `Read` this file and show the user the current state (placeholder / final). + +**Content shape**: a checklist of `- [ ]` items plus a verification method. Each item looks like: + +```markdown +- [ ] (verification: ) +``` + +Group by delivery domain via second-level headings (e.g. `## Code`, `## Rules and knowledge base`, `## Task list itself`). **Do not** repeat the execution steps from `task.md`; **do not** move "user todos" from `user-todos.md` into this file. + +**Example structure**: + +```markdown +# Acceptance Checklist + +> Compiled by the Agent; after verification the user may change the corresponding `- [ ]` to `- [x]`. + +## Code + +- [ ] `src//.ts`: (verification: read the file / run `npm test -- `) + +## Rules and knowledge base + +- [ ] `.Knowledge/topics/.md`: (verification: open the file to confirm sections are complete) +- [ ] `.Knowledge/manifest-routing.json`: (verification: read the corresponding field) + +## Task list itself + +- [ ] `TASK_ROOT/completed/-/` directory complete: `task.md` / `context.md` / `user-todos.md` / `acceptance.md` +- [ ] The entry in `todo.json` has been removed (or the file has been deleted if the array became empty) +``` + +## Recommended Hook Configuration (Claude Code) + +Add this to the project's `.claude/settings.json` to inject active tasks into context before each file change: + +```json +{ + "hooks": { + "PreToolUse": [{ + "matcher": "Edit|Write", + "hooks": [{ + "type": "command", + "command": "node -e \"try{const f='TASK_ROOT/todo.json',fs=require('fs');if(fs.existsSync(f)){const t=JSON.parse(fs.readFileSync(f,'utf8'));if(t.length)console.log('[task] active tasks: '+t.map(x=>x.name).join(', '))}}catch(e){}\" 2>/dev/null || true" + }] + }] + } +} +``` + +## Prohibited + +- Sub agents must not write `todo.json`. +- Do not move a task to `completed/` before all steps are complete. +- Do not batch-check checkboxes; they must be checked step by step. +- Do not create a `.task/` directory when all of `changeTracking.feat` / `changeTracking.fix` / `changeTracking.implement` are `false` or missing (`f2s-req-plan` is not constrained by this). +- In a task that already uses `.task/`, do not write "todos that the user must execute" **only** in the conversation or only in `task.md` without appending them to `user-todos.md` (when there are no todos, the file may keep a placeholder note). +- Do not archive while `acceptance.md` is still a placeholder note or is missing; do not merge `user-todos.md` (user todos) and `acceptance.md` (user acceptance) into the same file. +- Do not prewrite concrete acceptance items before implementation is finished (only a placeholder is allowed), to avoid drifting from the actual delivery. +- **Do not** scan other developers' `.task//` or merge multiple todo.json files for resume. +- **Do not** write to repo-root `.task/active/` without resolving `TASK_ROOT` first (unless resolved root is legacy `.task`). diff --git a/packages/core/templates/en-US/rules/f2s-topic-authoring.md b/packages/core/templates/en-US/rules/f2s-topic-authoring.md new file mode 100644 index 0000000..b55d726 --- /dev/null +++ b/packages/core/templates/en-US/rules/f2s-topic-authoring.md @@ -0,0 +1,128 @@ +--- +description: Flow2Spec topic-authoring guidelines: topic naming / skeleton / topicMetadata / topicDependencies decisions / whether a rule needs a corresponding topic / disk-write ownership pointers +alwaysApply: false +--- + +# Flow2Spec Topic Authoring Guidelines + +This rule is the single source of truth for the **authoring side**. Whenever an `f2s-*` skill adds or modifies `.Knowledge/topics/.md`, adjusts `manifest-routing.topicMetadata` / `manifest-routing.topicDependencies`, or deletes / migrates a topic, it **must Read this full rule first**, then continue with the corresponding SKILL steps. It coexists with `f2s-flow2spec-unified-entry` (the consumption side); in hard conflicts, the unified entry wins. + +## Scope + +This rule is touched when any of the following is true: + +- Add or rewrite `.Knowledge/topics/.md`; +- Modify an existing topic's title / applicable scenarios / key flow boundaries; +- Add, delete, or adjust `manifest-routing.topicMetadata`; +- Add, delete, or adjust dependency edges in `manifest-routing.topicDependencies`; +- Add a reference to a topic id in `taskToTopicRules[].topics`; +- Delete or migrate a topic (`f2s-kb-rm` / `f2s-kb-migrate` / `f2s-kb-upgrade`). + +## 1. Topic Naming + +- **id**: `kebab-case`, matching the key in `manifest-routing.topicPaths`. +- **Filename**: `.Knowledge/topics/.md`. If the topic is strongly bound to an `f2s-*` skill / rule of the same name (for example `f2s-task` / `f2s-req-plan`), the filename may include the `f2s-` prefix to show shared origin. +- **Avoid**: version suffixes (`-v2` / `-new`), personal nicknames, and synonyms that conflict with heading-level titles in `index.md`. + +## 2. Topic Positioning and Body Skeleton + +**Topic positioning**: executable routing summary + key boundaries. A topic may contain necessary boundary notes, key flow steps, prohibited items, and configuration summaries. After reading it, the Agent should be able to execute or decide whether more drilling is needed. It **should not carry** complete implementation details, long-form background, or raw content that can be found in a stock-doc. Stock-docs carry full background and long-form details; topics point to them. + +**Directory boundary for long-form background references (hard rule)**: in a topic, reference slots that point at long-form sources — sections titled "Detailed background / Related materials / Long-form source / Reference documents" — **may only** point at `.Knowledge/stock-docs/*_终稿.md` or already-finalized `stock-docs/*`. **Do not** put `.Knowledge/req-docs/*` (clarifications / technical designs / SQL / PRDs) in these slots: `req-docs` are **temporary inputs** for a given delivery — they get archived or migrated once the delivery lands, so using them as a topic's long-form source of truth leaves a dangling reference. If the corresponding `.Knowledge/stock-docs/*_终稿.md` does not yet exist when syncing / creating a topic, **first trigger `f2s-doc-final`** to consolidate the stock-doc (or confirm with the user and hand-write it), and only then point the topic at that stock-doc; you may not skip stock-doc consolidation and mount the topic on `req-docs`. **Allowed**: the topic body may **briefly cite** a single sentence or a field name from a design document as evidence (e.g., "see `.Knowledge/req-docs/xxx_技术方案.md`" as an occasional inline pointer), but the **long-form background reference slot** (the whole "Detailed background / Related materials" section) still must point at a stock-doc. + +Every topic must include at least: + +1. **Title and one-sentence intent** (one line stating "what this topic solves"); +2. **Applicable scenarios / trigger words** (semantically consistent with the corresponding `matchers/.json` `includeAny`); +3. **Core rules / flow** (executable knowledge; steps must be reproducible by an Agent); +4. **Dependency declaration** (if dependencies exist in `topicDependencies`, the body must explicitly state "before executing, read dependency topic `` first"; use the first paragraph of `topics/f2s-req-plan.md` as a reference); +5. **Boundaries and prohibited items** (avoid expanding into neighboring topics); +6. **Long-form background / detailed materials reference** (when the topic needs to carry business background): list only clickable Markdown links to `.Knowledge/stock-docs/*_终稿.md` (1–3 links); **do not** list `.Knowledge/req-docs/*` here. If no stock-doc has been consolidated yet, **generate the stock-doc first** and then fill this section. + +## 3. topicMetadata Decision Criteria + +`topicMetadata` is governance metadata. It only affects inventory, audit, and reading expectations; it does not participate in matcher hits, does not decide whether a topic is read, and does not change execution mandatoryness. Execution mandatoryness comes from explicit requirements in `AGENTS.md`, rules, skills, and topic bodies. + +Fields: + +- `primary`: main category, single value from `feature` / `module` / `config` / `policy`. +- `tags`: optional array, values from the same set as `primary`, and must not repeat `primary`. Used to describe secondary properties when a topic also contains them; only for audit/reading expectations, not routing or execution. +- `confidence`: `manual` / `inferred`. + +Decision rules: + +1. A `topicMetadata` key must exist in `topicPaths`; write metadata only for a topicId that already exists or is confirmed to be created in this turn. +2. `primary` should capture the topic's most central nature: read the topic body, decide which type its main content belongs to, and write that into `primary`. +3. `config`: configuration items, switches, defaults, initialization parameters. Use as `primary` only when these form the topic's main semantics. +4. `policy`: processes, rules, constraints, gates, prohibited items, agent orchestration, skill steps. Use as `primary` only when these form the topic's main semantics. +5. `feature`: implemented business / product capability. +6. `module`: shared capability, shared package, module boundaries, and engineering structure. +7. When a topic covers multiple properties, put the most important one in `primary`; put other clearly present properties in `tags` (optional array, values from the same set as `primary`, not repeating `primary`). +8. Use `manual` only when the user or maintainer explicitly confirms the category value. If there is clear evidence but no human confirmation of the category value, write `inferred`. When evidence is insufficient, **do not write metadata**, but list the inferred direction and evidence in the summary (for example, "suggest policy; the body contains multiple mandatory constraints") so the user can confirm and manually write `manual`. **Do not infer classification only from the topicId name; Read the topic body before deciding.** **`inferred` does NOT require prior user consent before being written**: when evidence is sufficient, write it directly per this clause; escalate to `manual` only when the user/maintainer actively specifies a category or when evidence conflicts and requires a decision. Treating `inferred` as "awaiting user approval" is a common misreading — it turns "evidence-backed auto-classification" into "mandatory human confirmation", which conflicts with clause 7's allowance for direct `inferred` writes. + +Prohibited: creating, renaming, or splitting topics solely for classification; duplicating classification blocks in topic markdown bodies or `index.md`. + +## 4. topicDependencies Decision Criteria + +Let the current topic be A and the candidate dependency be B. **Declare `A -> B` if any of these four questions hit**: + +1. **Strong reference to a prerequisite rule**: A's execution steps **explicitly mention** B's terminology / artifacts / disk-write constraints (example: `f2s-req-plan` requires maintaining `.task/` according to `f2s-task`). +2. **Without B, the result would be wrong**: can reading only A and not B produce the right result? If no, this is typical when A says "how to do it" and B says "where to do it / which input to use." +3. **Shared disk-write target**: A and B write the same set of files and B defines the disk-write format (such as `.task/` or `.Knowledge/topics/`). +4. **Fallback jumps to B**: A's own coverage is incomplete and the existing convention falls back to B. + +**Reverse exclusions** (avoid dependency bloat): + +- Only neighboring terminology (both discuss "knowledge base") -> do not write a dependency; rely on `index.md` semantic boundaries. +- Cross-topic information lookup (A wants to "learn about" B) -> do not write a dependency; rely on `taskToTopicRules` secondary candidates + `expand` recall. +- **Overview -> detail navigation**: a major feature's main topic and its submodule topics are an "association/navigation" relationship, not a strong prerequisite dependency. Submodule topics should be independently matched by their own matchers; do not write `A -> B`. In the main topic body, write clickable stock-doc links for submodules as navigation entries. +- **Do not duplicate transitive dependencies**: if `A->B` and `B->C` already hold, do not also write `A->C` (reading B naturally brings C). + +**DAG and minimization**: `topicDependencies` must be a DAG; cycles are prohibited. Keep the edge set minimal. + +**Decision timing**: after final drafts and new/modified topics are written, scan the body for other topic ids and rule filenames referenced in **backticks**, apply the four questions one by one, and write `manifest-routing.topicDependencies` on hit. Also write an explicit dependency declaration in the new topic body (see skeleton item 4). + +## 5. Large-Feature Splitting Strategy + +When a business feature is large, prefer a "main topic + subtopics" structure instead of one oversized topic. + +**When to split (soft constraints; evaluate splitting when any condition is met)**: + +- The corresponding stock-doc exceeds **300-500 lines**: evaluate splitting, but do not hard-block; +- matcher `includeAny` exceeds **12 entries**: signal that the topic is too broad; +- the topic body contains second-level headings for more than **3 unrelated responsibility domains**; +- during a `f2s-kb-upgrade` audit, the same topic is repeatedly matched by several unrelated task types. + +**How to split**: + +- **Main topic** (`primary: feature`): describe the business loop, entry boundaries, and submodule index; use clickable stock-doc links in the body to point to detail documents; do not write submodule implementation details. +- **Submodule topics**: write each one as `feature` / `module` / `config` / `policy` according to its real semantics; do not preset the type. Each has its own matcher and is independently matched through more specific trigger words. +- **stock-doc**: long-form content is allowed. When above threshold, prefer splitting into multiple focused stock-docs, such as `-business-rules_final.md` and `-data-model_final.md`, each corresponding to one subtopic. + +**Do not**: + +- Do not use `topicDependencies` to express "overview -> detail" navigation relationships (see reverse exclusions in section 4). +- Do not force-create subtopics solely for splitting. If a submodule will not be independently routed, a topic is unnecessary. + +## 6. Whether a Rule Needs a Corresponding Topic + +Criterion: **will this rule be matched as user-task routing?** + +- **Yes** (user questions / inputs can trigger this rule's execution) -> create a corresponding routing summary in `.Knowledge/topics/` and configure an entry in `taskToTopicRules`. Examples: `f2s-task` (matched by change-tracking user scenarios), `f2s-implement-tech-design` ("implement from design" user scenario). +- **No** (only referenced internally by other rules / SKILLs, and users will not initiate it directly) -> **do not create** a topic. Examples: `f2s-knowledge-preflight`, `f2s-karpathy-guidelines`, `f2s-config-check`, this rule `f2s-topic-authoring`. + +Misconception: "Important rules should have topics." Importance is not the same as "matched by user routing"; let the consuming SKILL directly `Read rules/.*` in its body instead of going through manifest routing. + +## 7. Disk-Write Ownership (Pointer) + +Write-ownership constraints for `manifest-routing.json` / `.Knowledge/index.md` / `.Knowledge/topics/*.md` **are governed by `f2s-flow2spec-unified-entry` and the "hard write-ownership constraints" inside each SKILL**. This rule does not repeat them; in conflicts, follow the unified entry and the corresponding SKILL. + +## Prohibited + +- Adding / modifying a topic or `topicDependencies` before reading this rule. +- Creating, renaming, or splitting topics only to fill classification. +- Writing duplicated metadata sections such as `## Concept Classification` in a topic body or `index.md`. +- Forcing "important rules" into `taskToTopicRules` (see section 6). +- Using `topicDependencies` to express "information is related" (use `index.md` semantic boundaries + matcher keyword recall instead of dependency edges). +- Writing transitive redundant edges or cycles in `topicDependencies`. +- **Listing `.Knowledge/req-docs/*` files (clarifications / technical designs / SQL / PRDs) in a topic's "Long-form background / Detailed materials / Related materials / Long-form source / Reference documents" reference slot.** These slots may only point at `.Knowledge/stock-docs/*_终稿.md`; when no stock-doc exists yet, consolidate it first and then fill the slot. Short-sentence / inline evidence pointers are not covered by this prohibition. diff --git a/packages/core/templates/en-US/skills/f2s-doc-arch/SKILL.md b/packages/core/templates/en-US/skills/f2s-doc-arch/SKILL.md new file mode 100644 index 0000000..a3ba65f --- /dev/null +++ b/packages/core/templates/en-US/skills/f2s-doc-arch/SKILL.md @@ -0,0 +1,128 @@ +--- +name: f2s-doc-arch +description: Generate a first draft of project architecture documentation from user notes, documents, or code scanning; no fixed format is required as long as the explanation is clear. Triggers: 项目架构说明、f2s-doc-arch、架构初稿、architecture draft、project architecture +--- +> Execution scope: this skill writes its artifact to `.Knowledge/stock-docs/` by default; later knowledge-base skill chains such as `f2s-doc-final` and `f2s-kb-build` sync it into `.Knowledge/topics/index/manifest`. + +## Orchestration (main / sub agent) + +- The semantics of `subAgent` / `switchAgentVerification` use the unified entry as the only source of truth: **Cursor/Claude** read the config-root `rules/f2s-flow2spec-unified-entry.*`; **Codex** reads `.codex/topics/f2s-flow2spec-unified-entry.md` (same source, mirrored by `flow2spec init`). This section does not restate those semantics. +- When `subAgent=true`, choose one of the following sub-agent strategies: + - **Mode B (default, single-round parallel)**: the main agent first produces an "inventory" (entry points + core module names, handwritten by the main agent) and a "scanning contract" (readable paths / directories forbidden to scan / unified output fields). Sub agents then perform parallel read-only scans and return tables. The main agent merges and deduplicates once, writes the `stock-docs` draft, and keeps user confirmation and acceptance in the main agent. + - **Mode C (multi-round correction)**: switch to this mode when any of the following is true: multiple workspaces / monorepo; extremely deep directories or > 20 source paths; the first-round sub-agent tables are contradictory or obviously thin; multiple source narratives overlap or conflict heavily. +- **Sub-agent delivery hard constraint**: sub agents must not trim directory scope on their own. They must follow the main agent's handwritten inventory. Their delivery must follow the "sub-agent delivery YAML schema" (fields: `source` / `scope` / `cross_refs` / `pending`); prose-style returns are forbidden. +- **Write-authority hard constraint**: `.Knowledge/index.md` / `manifest-routing.json` are always written by the main agent. Sub agents must not touch them. +- The writing side self-verifies. This SKILL does not bind to cross-agent verification. + +# Generate Project Architecture Documentation (Draft) + +This skill helps users generate **project architecture documentation** in a **draft** form. There is no fixed format; the goal is to **explain things clearly**. The user may provide plain-text notes, an existing document, or, when no input is provided, allow the AI to scan code as a fallback (not recommended; fallback only). + +**Division of responsibility with f2s-kb-add**: this skill is responsible **only** for the "architecture documentation **draft**" step. By default, it does **not** write the final version in the same skill and does not directly run **f2s-kb-build**. If the user wants to parse an **already completed capability** into the knowledge base **in one pass** from multiple related file paths (draft -> final -> topics/index/manifest), use **`f2s-kb-add`**. **Do not use this skill to impersonate that workflow**. + +--- + +## Inputs (All Optional) + +| Parameter | Description | +| -------------- | -------------------------- | +| **First argument** | Optional. One of: **a plain-text description** written after the command, or **a local document path** such as `.Knowledge/stock-docs/xxx.md`, `.Knowledge/req-docs/README.md`, or `README.md`. If omitted, enter the "no input" flow. | +| **Second argument** | Optional. Output file path. If omitted, default to `.Knowledge/stock-docs/architecture-overview_draft.md` (the project name may be inferred from `package.json` `name` or the directory name, then sanitized for a valid filename). | + +**Note**: when no description or document is provided, the skill uses **AI scanning of project code and directories** to generate the architecture draft, and **quality is not guaranteed**. Before executing, you **must first ask the user**: "Do you confirm that no arguments will be provided and that AI should still scan the code to generate the draft? (quality not guaranteed)" Continue only after the user explicitly confirms. + +--- + +## Execution Flow + +### 1. If the User Provides Notes or a Document + +1. **Read and understand** + - If the first argument is a **document path**: read that file under the parent directory of the config root (supports text formats such as .md and .txt). + - If the first argument is a **plain-text description**: use the user input directly as "user notes". +2. **Supplement with project context** + - Based on clues in the user notes such as **code paths, module names, and entry points**, combine the actual directory structure and key files under the parent directory of the config root (for example package.json, entry files, config files) to **summarize and complete** the architecture. + - If the user notes are broad (for example "an admin system"), **actively guide** the user to add: main code paths, module/package split, entry points and startup approach, boundaries with external systems, and similar details, so the architecture documentation is more accurate. +3. **Generate the draft** + - If splitting is enabled (Mode B), sub agents must scan according to the main agent's handwritten inventory, and their delivery must follow the sub-agent delivery YAML schema. + - Produce a **project architecture document**. It may include, but is not limited to: project positioning, technology stack, directory/module split, key paths and entry points, configuration and deployment notes, and how this document maps to documentation artifact stages if applicable. + - **No fixed format**: clear headings and paragraphs are enough. Do not force the `final-overview-template`. +4. **Output** + - Default output: `.Knowledge/stock-docs/architecture-overview_draft.md`. If the user provides a second argument, write to that path. + - If the directory does not exist, create it first. + +### 2. If the User Provides No Notes or Document + +1. **Warn and confirm** + - Clearly state: "**No arguments were received.** Without notes or a document, AI will scan project code and directories to generate an architecture draft. **Quality is not guaranteed**, and it may miss key points or fail to distinguish priority. I recommend providing a brief description or existing document (such as README or design doc) before running this skill." + - **Must ask the user**: "Do you confirm that no arguments will be provided and that AI should still scan the code to generate the draft? (quality not guaranteed)" + - Continue to Step 2 only after the user **explicitly confirms** (for example "确认", "yes", "directly scan"). If the user does not confirm or cancels, do not scan or generate. +2. **Scan and generate** + - Based on the parent directory of the config root: list main directories and representative files (with package.json, common entry names, and config filenames when useful), and summarize "directory structure, likely modules, entry points, and configuration". + - Generate an **architecture draft**, and state inside the document: "This draft was generated by scanning the project structure; it should be further completed with business notes and code details." +3. **Output** + - Same as above: default `.Knowledge/stock-docs/architecture-overview_draft.md`, or the second argument specified by the user. + +--- + +## Guidance and Iteration + +- If the user's description has a **large scope** (for example "the entire middle platform"), suggest adding **main code paths, submodule/package names, external entry points, dependencies**, and similar details. The user may add them in this or later conversations and rerun this skill to update the draft. + +## Large-Feature Split Recommendation + +After scanning or understanding the source/notes, if any of the following signals appear, output a "拆分建议" section at the **end** of the draft for the user's reference (does not block generation): + +- Total source volume exceeds **~5000 lines**, or more than **20 files** are involved; +- More than **3 unrelated responsibility domains** are clearly identifiable (for example API layer / core rules / data model / external dependencies are independent); +- The user notes already mention "multiple submodules" or "multiple features". + +**Split recommendation format** (write at the end of the draft as a standalone section): + +``` +## Split Recommendation + +The current feature is large. Split it into multiple focused stock-docs, each mapped to an independent topic: + +| Suggested document | Main content | Suggested topic primary | +|---|---|---| +| -overview_draft.md | Entry boundaries, submodule relationships, quick index | feature | +| -business-rules_draft.md | Core flows, gates, state machine | policy | +| -data-model_draft.md | Table structure, enums, model conventions | module | +| -external-dependencies_draft.md | SOA/QMQ/Redis/risk-control wrappers | config | + +After splitting, each sub-topic is independently matched through its own matcher, while the main topic body contains navigation links. +Do not chain "overview -> details" through topicDependencies (see f2s-topic-authoring section 5). +``` + +The user may choose: **A) run `f2s-doc-arch` separately for each split recommendation** (recommended), or **B) continue with the current single draft** for later steps. + +## Next Step After Completion (Hard Constraint) + +This skill **only produces a draft**. At the end, guide the user in the following order. **Do not** let the user skip the final version and directly run `f2s-kb-build`: + +1. Tell the user the draft path and recommend reviewing and completing it first. +2. **The next step must be `f2s-doc-final`**: use the draft path as input and produce `.Knowledge/stock-docs/_final.md` in the `final-overview-template` standard format. +3. **Only after the final document is written** guide the user to **`f2s-kb-build`**, and its input must be the final path (containing `_final` or just generated by `f2s-doc-final`). +4. **Do not** write only "please run `f2s-kb-build`" in the completion reply with input pointing to `*_draft.md`; **do not** present `f2s-kb-build` and `f2s-doc-final` as alternatives. +5. **Only exception**: the user **explicitly requests** skipping the final step, and the draft has already been manually made compliant with the `final-overview-template`. First explain the risk of skipping finalization, then allow `f2s-kb-build`. + +**Completion reply template** (must include both `f2s-doc-final` and `f2s-kb-build`, with ctx-build after the final document): + +> Architecture draft generated: ``. Please review and edit it first; next run **`f2s-doc-final `** to convert it to a final document, then run **`f2s-kb-build `** to sync knowledge routing topics and indexes. + +--- + +## Path and Output Conventions + +- All paths are relative to the **parent directory of the config root**. +- **Default output**: `.Knowledge/stock-docs/architecture-overview_draft.md`; the project name is taken from `package.json` `name` (with scope and illegal characters removed) or the current directory name. +- If the user provides the second argument as an output path, use it first. If the directory does not exist, create it first. + +--- + +## Constraints and Notes + +- **No mandatory format**: this skill produces an "architecture documentation draft"; clarity is the priority. It does not need to conform to the `final-overview-template` or a fixed section structure. +- **Must confirm when no arguments are provided**: if the user provides no argument, first ask "Do you confirm that no arguments will be provided and that AI should still scan the code to generate the draft? (quality not guaranteed)". Execute scanning and generation only after explicit confirmation. +- Summarize according to the "completion reply template" above: draft path + **must run `f2s-doc-final` before `f2s-kb-build`**; do not recommend build only. diff --git a/packages/core/templates/en-US/skills/f2s-doc-final/SKILL.md b/packages/core/templates/en-US/skills/f2s-doc-final/SKILL.md new file mode 100644 index 0000000..ab52a9b --- /dev/null +++ b/packages/core/templates/en-US/skills/f2s-doc-final/SKILL.md @@ -0,0 +1,92 @@ +--- +name: f2s-doc-final +description: Convert a PDF or MD document into the `final-overview-template` standard format so f2s-kb-build can later sync topics/index/manifest; triggers: f2s-doc-final、转成概述模板、终稿模版、final-overview-template, final template、convert to final draft +--- + +> Execution scope: drafts and final documents are both written to `.Knowledge/stock-docs/`; prefer reading `.Knowledge/template/final-overview-template.md` as the template. + +## Orchestration (main / sub agent) + +- The semantics of `subAgent` / `switchAgentVerification` use the unified entry as the only source of truth: **Cursor/Claude** read the config-root `rules/f2s-flow2spec-unified-entry.*`; **Codex** reads `.codex/topics/f2s-flow2spec-unified-entry.md` (same source, mirrored by `flow2spec init`). This section does not restate those semantics. +- **Do not split by default**: MD / PDF -> `final-overview-template` conversion is most coherent when the main session completes understanding, template mapping, and finalization in one pass. +- **Optional split** (only when `subAgent=true` and the input is large / multi-file; threshold: PDF **> 50 pages** or **> ~5MB of text**): a sub agent may produce a **draft** for template mapping, formatting, and structural transfer; the main agent compares it against the `final-overview-template`, identifies gaps, asks the user follow-up questions, aligns with the user, and **finalizes / accepts** the document. **A sub agent must not claim the final document is compliant on its own**. +- Do not split by default just because "format conversion can be isolated": final compliance depends on template semantics plus business wording, and the main-side acceptance cost usually remains. +- Verification is performed by the writing agent. This skill does not bind to cross-agent verification. + +# Convert a PDF or MD into the `final-overview-template` Standard Format (spec -> context) + +The user provides **at least one argument** after this skill: the **first argument** is a local **PDF file path** or **Markdown file path** (required); the **second argument** (optional) is an output file path and overrides the default output location. Execute the following workflow based on the file type, and output a final-style Markdown document that can later be consumed by **f2s-kb-build**. + +**The `final-overview-template` is only guidance**: if `.Knowledge/template/final-overview-template.md` exists, read it as a structural reference. Do not force an exact template fit. + +## Embedded Template Structure (Use When `.Knowledge/template/final-overview-template.md` Does Not Exist) + +Standard requirements: + +- **Level-1 heading**: design name (for example `# xxx Technical Design`). +- **Level-2 headings should include at least**: `## Core Concepts`, `## Business Rules`, `## Key Flows`; add or remove others as needed (for example status and transitions, APIs, configuration/table design/error codes, implementation locations and integration approach). +- **Core concepts**: use a table listing terms, entities, and key IDs (columns: concept, description). +- **Status and transitions**: if there is a state machine, list states and transitions; otherwise summarize briefly or omit. +- **Business rules**: list constraints, validations, and configuration items. +- **Key flows**: describe the main user-side or system-side flows; list flow name, brief steps, entry API/method, and result. +- **Optional sections**: APIs, configuration/table design/error codes, implementation locations and integration approach. Keep and fill them as needed. + +--- + +## Flow 1: The User Provides Markdown (`.md`) + +1. **Read** the `.md` file provided by the user. +2. **Reference format** (not mandatory): if `.Knowledge/template/final-overview-template.md` exists, read it as structural guidance; otherwise use the embedded template structure below. +3. **Analyze and convert**: + - Understand the source topic and structure, and extract the "design name", "core concepts", "business rules", "key flows", and other source-relevant sections (such as status and transitions, APIs, configuration/table design/error codes, implementation locations, and so on). + - Reorganize the content into clear final-style Markdown: the level-1 heading is the design name; the document should include at least the three level-2 headings `Core Concepts`, `Business Rules`, and `Key Flows`; add or remove other sections according to the source and need. Table/list formatting may reference the template but does not need to match exactly. + - If the source lacks a section, mark it as `(待补充)` or infer and complete it from the source. If the source structure is already clear, keep the original section names. +4. **Output**: + - Default output is `.Knowledge/stock-docs/_final.md` (the final artifact includes the `_final` marker). + - If the user specifies an output path as the second argument, use that path; otherwise use the default. +5. **Reply**: tell the user that `.Knowledge/stock-docs/_final.md` has been generated, and say they may continue with `f2s-kb-build` to sync `.Knowledge/topics`, `.Knowledge/index.md`, and `manifest` if needed. + +--- + +## Flow 2: The User Provides a PDF (`.pdf`) + +Complete this in two stages: **first PDF -> draft MD, then after user confirmation draft MD -> template-format MD**. + +### Step A: First Execution (PDF Path Provided) + +1. **Try to read the PDF**: read the PDF from the user-provided path (absolute or relative to the project root, for example `.Knowledge/stock-docs/xxx.pdf`). + - If the current environment can parse PDF text: extract the body and convert it to a Markdown draft (preserve heading hierarchy, lists, paragraphs, and tables if recognizable). + - If the PDF cannot be read directly (for example only binary data is available): ask the user to export the PDF content to `.Knowledge/stock-docs/xxx.md` and then run the skill again. +2. **Generate the draft**: + - Save the extracted content as `.Knowledge/stock-docs/_draft.md` (infer the design name from the PDF filename or first heading). + - In the reply, **show the full draft or its main structure**, and clearly state: + - "The draft has been saved as `.Knowledge/stock-docs/_draft.md`; please review and edit it." + - "After confirming it is correct, run: `f2s-doc-final .Knowledge/stock-docs/_draft.md`." +3. **Do not perform template-format conversion in this round**. This round only completes PDF -> draft MD. + +### Step B: Second Execution After User Confirmation (Draft `.md` Path Provided) + +When the user **runs this skill again with the draft `.md` path** (for example `.Knowledge/stock-docs/技术方案设计_draft.md`): + +- Execute Steps 2-5 from **"Flow 1: The User Provides Markdown"**: read the format guidance -> analyze and convert -> output template-format content. +- **Output suggestion**: generate `.Knowledge/stock-docs/_final.md`. +- **Reply**: tell the user the standardized version has been generated, and say they may continue with `f2s-kb-build` to sync `.Knowledge/topics` and the index. + +--- + +## Path and Output Conventions + +- All paths are relative to the project root. Drafts and final documents are both placed under `.Knowledge/stock-docs/`. +- **Input**: the first argument is a required file path, such as `.Knowledge/stock-docs/方案.pdf` or `.Knowledge/stock-docs/方案_draft.md`; the second argument is optional. +- **Output**: + - First PDF run: `.Knowledge/stock-docs/_draft.md` + - MD or draft MD: `.Knowledge/stock-docs/_final.md` +- If `.Knowledge/stock-docs/` does not exist, create it before writing. + +--- + +## Constraints and Notes + +- During conversion, **do not copy the source verbatim**. Extract, summarize, and complete it according to the template so core concepts, business rules, and key flows are easy to find. +- It is recommended, but not mandatory, to keep the three level-2 headings **Core Concepts**, **Business Rules**, and **Key Flows**. Add or remove other sections based on the source and need. The `final-overview-template` is guidance only and is not mandatory. +- End with a one-sentence summary: the generated draft/final path and the next step, `f2s-kb-build`, for syncing knowledge routing topics and indexes. diff --git a/packages/core/templates/en-US/skills/f2s-doc-milestone/SKILL.md b/packages/core/templates/en-US/skills/f2s-doc-milestone/SKILL.md new file mode 100644 index 0000000..7617619 --- /dev/null +++ b/packages/core/templates/en-US/skills/f2s-doc-milestone/SKILL.md @@ -0,0 +1,148 @@ +--- +name: f2s-doc-milestone +description: Generate a milestone document (`project-milestone-template`) from req-docs, git log, `.task`, and knowledge-topic semantics; triggers: f2s-doc-milestone、生成项目里程碑、里程碑、project milestone、generate milestone. A semantic scope may be appended after the command. This skill always uses a sub agent for generation and the main agent for verification, regardless of flow2spec.config orchestration switches +--- + +> **Task paths**: all `.task/` reads/writes must use **`TASK_ROOT` from `rules/f2s-task`** (` .task` or `.task/`; config → git → legacy). Bare `.task/todo.json` / `.task/active/` below mean **`TASK_ROOT/...`**. + + +> Execution scope: read `.Knowledge/template/project-milestone-template.md`; write **only** `.Knowledge/stock-docs/-milestones.md` (no second path argument). + +## Orchestration (Fixed, Not Affected by Project Config) + +**This skill is not affected by** `subAgent`, `switchAgentVerification` (or old key `subAgentVerification`) in `flow2spec.config.json`: regardless of whether they are `true` or `false`, **always** use the division below. It is **forbidden** to switch to "all main session" or "sub agent self-verifies and ends" because of config values. + +| Role | Steps | Responsibilities | +| --- | --- | --- | +| **Main agent** | 0, 3, 4 | Read template and knowledge topic index, parse scope, dispatch sub agent, **verify**, revise if needed, reply to user | +| **Sub agent** | 1, 2 | Collect four sources, apply template, **Write draft** | + +1. **Main agent**: Step 0 -> issue the "collection contract" -> sub agent executes Steps 1-2 and writes the draft. +2. **Main agent**: Step 3 verifies against the four sources and the "important node checklist" (do not rewrite wholesale; fill gaps, correct errors, add "Pending Confirmation") -> Step 4 replies. +3. The sub agent is **forbidden** from claiming "the milestone has been accepted as complete"; the final document is the version verified by the main agent. + +> Step 0 still **`Read("flow2spec.config.json")`** (satisfies the `f2s-config-check` preflight), but its `subAgent` / `switchAgentVerification` values **must not** change this skill's orchestration. + +**Sub-Agent Collection Contract (Main Agent Writes into the Prompt Before Dispatch)** + +| Field | Content | +| --- | --- | +| `scope` | One sentence describing the user's semantic scope | +| `outputPath` | `stock-docs/-milestones.md` | +| `sources` | See "Four Sources" below; **must include knowledge-topic semantics** | +| `template` | `.Knowledge/template/project-milestone-template.md` (do not write the template's top explanatory blockquote) | +| `delivery` | Complete Markdown that can be directly `Write`n to `outputPath` | +| `stagePolicy` | See "Stage Granularity" below; the contract must restate it in one sentence | + +## Stage Granularity (Required, Write into the Contract) + +Milestones **Mx record only feature/capability changes**: deliverables that are already implemented or verifiable in the current repository (or user-specified scope), such as modules/APIs/data models/domain behavior/knowledge routing, and must be supported by the four sources. + +**Do not** write the following stage types as separate overview rows or standalone `## Mx ·` sections (do not invent them without four-source delivery support; even when delivery exists, do not split them into "pure testing / pure integration" stages): + +- Joint debugging, integration testing, UAT, regression, acceptance, test submission, launch checks (process-only, no functional diff) +- Environment/ops-only actions (executing DDL, filling configs, release windows, cross-repo scheduling) with **no** feature delivery in this scope +- Stages named "stabilization / engineering / wrap-up" whose substance is only the process work above + +**Merge rule**: engineering changes within the same capability iteration (such as id type alignment, pagination format, locking and concurrency) are **merged into** the corresponding feature stage body, not written as a separate "joint debugging / testing / acceptance" stage. + +**Gap handling**: if the four sources mention only pending joint debugging, pending acceptance, or missing environment setup without feature delivery in this scope, **do not write** a corresponding Mx. Add one sentence under **Pending Confirmation** if needed, and **do not** fill the overview table with "planned items". + +## Four Sources (Both Collection and Verification Must Cover Them) + +| Source | What to Read | How to Use in Milestones | +| --- | --- | --- | +| **req-docs** | In-scope `.Knowledge/req-docs/*.md` | Requirement/design nodes, delivery summaries | +| **git** | `git log --no-merges`, `git tag -l`, `package.json` version | Timeline, major versions/tags, commit anchors | +| **`.task`** | `todo.json`, `active/`, `completed/` `task.md`, etc. | Task closure, delivered steps | +| **Knowledge topics (semantics)** | See "Topic Source" below | Align with capabilities already registered in index/manifest and avoid missing stages that are already semantic in the knowledge base | + +### Topic Source (Knowledge Semantics; Main Agent Step 0 Must Read, Sub Agent Step 1 Must Read) + +1. **`Read(".Knowledge/manifest-routing.json")`**: extract `topicPaths`, `taskToTopicRules`, and scope-related `topicDependencies`. +2. **`Read(".Knowledge/index.md")`**: at least the "**Topic Overview**" table (topic id, applicable scenario, linked document summary). +3. **Read `.Knowledge/topics/.md` as needed**: summaries related to the scope or manifest hits (**do not** enumerate the entire `topics/` directory; read only topics named by manifest/index, usually no more than the table rows). +4. Summarize topic semantics into a list of "capability/scenario nodes" for the sub-agent contract. Milestone stages must either cover them or explain related gaps under "Pending Confirmation". + +> **Source information is for collection and verification only — do not write it into the generated document.** The output must not contain a "Sources" line, topic file paths, or internal manifest names. + +## Input (Only One, Optional) + +After the command name, the user may append **one semantic scope** (natural language): + +| User Intent | Example | Output Filename | +| --- | --- | --- | +| Entire project (default) | omitted / `entire project` / `full project` / `整个项目` / `全项目` | `project-milestones.md` | +| One requirement or capability | `callback refactor` / `login module` / `回调改造` / `登录模块` | `-milestones.md` | + +**Filename rule**: suffix `-milestones.md`; entire project -> prefix `project`; single requirement -> semantic phrase or requirement title summary (keep concise). + +**Scope narrowing**: filter the four sources by keyword, path, and date. If no scope is provided, all four sources are traceable in full (topic source reads full index table + manifest, and expands topics as needed). + +## Step 0: Preflight (Main Agent) + +1. **`Read("flow2spec.config.json")`** (do not use its `subAgent` / `switchAgentVerification` values to orchestrate this skill) +2. **`Read(".Knowledge/template/project-milestone-template.md")`** +3. **Topic source** (see above: manifest -> index topic table -> topic summaries as needed) +4. Parse the scope -> determine default path **`stock-docs/-milestones.md`**. +5. **Similar-file check (required before writing)**: list existing `*milestone*.md` files under `.Knowledge/stock-docs/` (including `*milestone.md`). If there is a file with the **same target path** or a **semantically similar** file (for example, another whole-project milestone such as `project-milestones.md`, or high overlap in prefix/scope keywords), **ask the user first**. Do **not** silently overwrite or invent a new filename: + - **Overwrite**: keep the original path; the sub agent overwrites this file, and after verification it remains the final path. + - **Generate another copy**: use a new path (recommended: scope summary + `_YYYYMMDD` + `-milestones.md`, or the user's specified `-milestones.md`), and update `outputPath` in the contract. + - If no similar file exists, or only one file exists with exactly the target path and the user has already explicitly asked to "regenerate/overwrite" in this round, no further question is required; continue with the default path. +6. Restate to the user: scope, **final** `outputPath`, and number of topics read. If a similar-file question was asked, wait for the user's choice before continuing. +7. Assemble the "collection contract" (including final `outputPath` and topic node list) and **dispatch the sub agent** for Steps 1-2. + +## Step 1: Collect Sources (Sub Agent) + +- Complete **four-source** collection according to the contract. Git **must** compare tags and main version/semver transitions (based on this repository's `git tag` / `package.json`). +- Topic semantics: check whether capabilities from manifest/index align with git/req/task in the same window. Put temporarily unaligned items into internal notes for "Pending Confirmation". + +If sources are empty: still generate the document and explain gaps under "Pending Confirmation"; **do not** fill deliverables from training data. + +## Step 2: Apply Template and Write (Sub Agent) + +**Generation principle: write for readers, not internal tooling.** + +1. Document header: title `# (Scope Name) Milestones`, scope, and updated date only. **Do not write** a Sources line, topic paths, manifest names, commit hashes, npm publish status, environment status, or any other internal information. +2. **Newest first**: both the overview table and each `## Mx ·` section are ordered **latest phase first** (MN → … → M1). Each stage title must reflect a feature change; do not use "joint debugging / testing / acceptance" as a name (see "Stage Granularity"). +3. Each stage body: list **delivered features only**, one item per line, verifiable. Do not include timing details, process narration, or background context. +4. **Pending Confirmation**: only list functional/delivery gaps or inconsistencies. **Do not** include internal operations, release, or environment status. Write "None" if there are no gaps. +5. Do not write the template's top explanatory blockquote. +6. **`Write`** to `outputPath`. + +## Step 3: Verify (Main Agent, Required) + +After the sub agent writes the draft, the main agent **must** verify whether **important nodes** are wrong, missing, or over-merged. + +1. **Reread four-source essentials**: git tags/commits, req/task, and **index topic table + read topics**, then compare against the draft. +2. **Check against the important node checklist**: + +| Category | What to Check | +| --- | --- | +| Version / tag | Major tags and `package.json` version transitions from the four sources are reflected in overview or Mx | +| Route/architecture turning point | Major directory restructuring or technology-route replacement in the four sources is reflected separately or merged appropriately | +| Feature delivery | Verifiable capabilities in req/git/task have corresponding stages in Mx | +| **Knowledge topics** | If manifest/index exists: topics related to the scope are covered or listed under "Pending Confirmation" | +| Task closure | If `.task/` exists: archived tasks are reflected in related Mx | +| Traceable basis | Each Mx deliverable can be traced to the four sources | +| Timeline | Ordering is reasonable; same-window multi-version changes are split if needed | +| **Ordering** | Overview table and all Mx sections are newest-first; reorder if not | +| **Stage granularity** | No Mx consists only of joint debugging/testing/acceptance/environment work without feature delivery; if found, **delete it or merge it** into an adjacent feature stage | +| **Internal information** | Document contains no Sources line, commit hashes, topic paths, or npm/environment status; **remove** if found | + +3. Missing items -> add Mx (**must be feature changes**); errors -> correct according to the four sources; unknowns -> "Pending Confirmation" (**do not** replace unknowns with fake Mx). +4. Only after verification or revision is complete may Step 4 run. + +## Step 4: Reply (Main Agent) + +Report the disk path, stage count, one-sentence verification conclusion, and "Pending Confirmation" summary. + +## Forbidden Items + +- Do not use `subAgent` / `switchAgentVerification` to skip sub-agent generation or main-agent verification. +- Do not dispatch a sub agent or `Write` before the user chooses "overwrite / generate another copy" when a **similar milestone** already exists in `stock-docs/`. +- Do not use a second argument to change the output path (the path is determined by scope + similar-file choice). Do not write to `req-docs`. +- Do not write deliverables without reading the four sources. Do not let the sub agent claim completion before verification. +- Do not enumerate the entire `matchers/` directory or all topics as a substitute for "manifest + index + topics as needed". +- Do not use training data or milestone structures from other projects instead of the **current repository's** four sources. Do not write unrelated `stock-docs` documents outside this `outputPath`. +- Do not create standalone joint debugging / integration testing / UAT / acceptance / pure environment-ops Mx stages. Do not write "planned item" stages when there is no four-source feature delivery. diff --git a/packages/core/templates/en-US/skills/f2s-doc-pdf/SKILL.md b/packages/core/templates/en-US/skills/f2s-doc-pdf/SKILL.md new file mode 100644 index 0000000..bd7bb2f --- /dev/null +++ b/packages/core/templates/en-US/skills/f2s-doc-pdf/SKILL.md @@ -0,0 +1,69 @@ +--- +name: f2s-doc-pdf +description: Convert a PDF technical design into Markdown and save it under req-docs, with optional flow-description completion; triggers: PDF转MD、按方案实现前的 PDF、PDF to Markdown、technical design PDF +--- + +> Execution scope: technical design documents are written to `.Knowledge/req-docs/`; rule capabilities are still loaded from the config-root `rules/skills`. + +## Orchestration (main / sub agent) + +- The semantics of `subAgent` / `switchAgentVerification` use the unified entry as the only source of truth: **Cursor/Claude** read the config-root `rules/f2s-flow2spec-unified-entry.*`; **Codex** reads `.codex/topics/f2s-flow2spec-unified-entry.md` (same source, mirrored by `flow2spec init`). This document does not restate those semantics. +- **Do not split by default**: follow-up questions and disk writes must be completed in the main agent session. A sub agent cannot ask the user follow-up questions. +- **Optional split**: enable only when `subAgent=true` and the PDF exceeds the threshold (**> 50 pages or > ~5MB of text**). The sub agent is responsible only for the first PDF-to-MD draft and writes `.Knowledge/req-docs/.md`; it must **not ask follow-up questions and must not write the "Flow Description" section**. The main agent then handles follow-up questions and flow-description completion. +- Verification is performed by the writing agent by default. This skill does not bind to cross-agent verification. + +# Convert a PDF Technical Design to Markdown (and Complete Flow Descriptions) + +The user provides **one argument** after this skill: the local path to the **PDF technical design document** (for example `~/Downloads/技术方案.pdf` or `.Knowledge/req-docs/某草稿.pdf`). Follow the steps below to convert the PDF into Markdown, save it under `.Knowledge/req-docs/`, and guide the user to complete flow descriptions when needed. + +## Step 1: Read the PDF and Convert It to Markdown + +1. If sub-agent splitting is enabled (PDF > 50 pages or > ~5MB), the sub agent is responsible only for the PDF-to-MD draft and writes `req-docs/.md`; it does not ask follow-up questions and does not write flow descriptions. The main agent takes over the following steps. **Read** the PDF file provided by the user, extract its **text content** (preserving tables, sections, lists, code blocks, and other structure as much as possible), and organize it as Markdown. +2. **Save it to** `.Knowledge/req-docs/`. Recommended path: `.Knowledge/req-docs/.md`. The filename should be the original PDF filename with `.pdf` replaced by `.md`. +3. If the directory does not exist, create it before writing. +4. After saving, tell the user: "The PDF has been converted to Markdown and saved as `xxx.md`." + +--- + +## Step 2: Ask the User for Flow Diagrams (Optional but Recommended) + +Embedded **flow diagrams** in the PDF cannot be parsed directly into steps and branches. If code will be implemented based on the diagram, the user needs to provide additional material. + +1. Tell the user: "The document may contain flow diagrams, and I cannot reliably parse the steps and branches inside those diagrams from the PDF. If you will later implement code from this technical design (see the `implement-tech-design` rule), I recommend completing the flow description: + - **Option 1**: send the relevant flow diagram images in this conversation; I will parse them and write a textual version into the MD file above. + - **Option 2**: describe each API/flow in text directly (for example: 1. Is the user logged in? 2. Query a table. 3. Check a field -> return the result); I will write it into the MD file as provided. + If the document has no flow diagram or you do not want to provide one now, reply `skip`, and I will finish this skill." +2. **If the user replies `skip` or clearly says no flow description is needed**: tell the user, "Provide the MD path above in the conversation and say that you want to implement code from the technical design; I will follow the `implement-tech-design` rule." Then stop. +3. **If the user provides a flow diagram image or text**: proceed to Step 3. + +--- + +## Step 3: Write the Flow Description into the MD File + +1. If the user provides an **image**: parse the steps, decision branches, and returns in the image, then organize them as textual steps. +2. If the user provides **text**: use it directly. +3. **Append** the flow content to the end of that MD file, or add a new "Flow Description" section. Example format: + +```markdown +## Flow Description (provided by the user / parsed from a flow diagram) + +### Example API A +1. Frontend sends the request +2. Backend queries the latest record from a table +3. Check: does a certain ID exist? yes -> return true, no -> return false +4. Return result + +### Example API B +1. Is the user logged in? -> no: return 401 +2. Is it expired? -> yes: return 403 +… +``` + +1. After saving, tell the user: "The flow description has been written to `xxx.md`. Next, provide this MD path in the conversation and say that you want to implement code from the technical design; I will follow the `implement-tech-design` rule." + +--- + +## Constraints and Summary + +- **Path**: the PDF path provided by the user may be absolute or relative to the project root. The output MD should be saved to `.Knowledge/req-docs/.md` (`req-docs` stores implementation documents, while `stock-docs` stores knowledge-source documents). +- **This skill only handles**: PDF -> Markdown conversion plus optional flow-description completion. It does not implement code. After completion, you may tell the user: provide the generated MD path in the conversation and say that implementation should follow the technical design; the AI will follow **f2s-implement-tech-design.mdc**. diff --git a/packages/core/templates/en-US/skills/f2s-git-commit/SKILL.md b/packages/core/templates/en-US/skills/f2s-git-commit/SKILL.md new file mode 100644 index 0000000..de04c48 --- /dev/null +++ b/packages/core/templates/en-US/skills/f2s-git-commit/SKILL.md @@ -0,0 +1,251 @@ +--- +name: f2s-git-commit +description: Commit completed code to Git: by default check both changes and knowledge-base coverage; when the user explicitly asks for "快捷提交" / quick commit, skip only the knowledge coverage check; **when the pending changes are pure docs / knowledge-base itself**, or **f2s-kb-sync / kb-feat / kb-fix / kb-add / kb-addRules / kb-distill ran within the last 30 min**, auto-skip the coverage check; after generating a commit message with an emoji first line, commit directly (the first line must be shown in the same reply; no separate confirmation is required); git pull-like fetch/merge operations require user confirmation first. Triggers: f2s-git-commit、提交代码、快捷提交、git commit、帮我提交、quick commit、commit code +--- + +> Execution scope: this skill performs Git operations for the user. Do not use `git add -A` / `git add .`, do not skip hooks (`--no-verify`), and do not push automatically. Before any `git pull` / `git fetch` operation that merges into local work, obtain explicit user confirmation for the "pull". `git commit` does not require a separate confirmation round (see Steps 3-4). When the user explicitly asks for "快捷提交", skip only Step 2, the knowledge-base coverage check; all other safety steps still apply. + +## Orchestration (main / sub agent) + +- The semantics of `subAgent` / `switchAgentVerification` use the unified entry as the only source of truth: **Cursor/Claude** read the config-root `rules/f2s-flow2spec-unified-entry.*`; **Codex** reads `.codex/topics/f2s-flow2spec-unified-entry.md`. +- This skill is performed entirely by the main agent (**pull confirmation** cannot be delegated to a sub agent; `git commit` does not require a separate user-confirmation round; see Steps 3-4). + +# f2s-git-commit (Commit Code) + +## Mandatory Flow + +### Quick Commit Mode + +When the user explicitly says **"快捷提交"**, **"快速提交"**, or **"quick commit"** in this turn, enter quick commit mode: + +- Skip **Step 2: Knowledge-base coverage check**. Do not read `.Knowledge/topics/` / `.Knowledge/stock-docs/` for coverage judgment. +- Do not prompt the user to run `f2s-kb-sync` / `f2s-kb-feat` first. +- **Do not skip** Step 1 change reading and conflict-marker checks. +- **Do not skip** Step 3 commit-message generation and display. +- **Do not skip** Step 4 precise `git add `, normal `git commit`, and Git hooks. +- **Do not** use `git add -A` / `git add .` / `--no-verify` / automatic push because this is a quick commit. + +### Step 1: Read Changes (Read-Only) + +```bash +git status --short +git diff HEAD +``` + +- Distinguish three file categories from `git status --short`: + - **Staged**: already `git add`ed, prefixes such as `M `, `A `, `D ` (first column non-empty) + - **Unstaged**: tracked but not added, prefixes such as ` M`, ` D` (second column non-empty) + - **Untracked**: `??` prefix, new files not tracked yet +- If all three categories are empty (nothing to commit), tell the user and stop. + +**Conflict check (required, before everything else)**: + +Scan all changed file contents. If any file contains conflict markers `<<<<<<<`, `=======`, or `>>>>>>>`, stop immediately and report: + +``` +❌ Unresolved merge conflict detected: + - + +Please resolve the conflict before committing. +``` + +### Step 2: Knowledge-Base Coverage Check (Required by Default; Skipped for Quick Commit) + +If in **quick commit mode**, skip this step and mention in the Step 5 closing note that "the knowledge-base coverage check was skipped according to quick commit mode." + +**First check whether `.Knowledge/` exists:** + +- If `.Knowledge/manifest-routing.json` does not exist: skip this step, mention in Step 5 that "the project has not initialized the Flow2Spec knowledge base; consider running flow2spec init", and continue to Step 3. + +**Skip rule A: Changes are pure documentation / knowledge base itself** (evaluated before running the coverage check) + +If **every** pending file collected in Step 1 matches one of these patterns, skip this step directly (in Step 5, note "changes are pure docs; coverage check skipped"): + +- `.Knowledge/**` (you're editing the knowledge base itself; checking coverage against itself is meaningless) +- `docs/**` / `docs/en/**` +- `README*.md` / `LICENSE` / `CHANGELOG*` +- `.claude/**` / `.cursor/**` / `.codex/**` (agent config roots; distributed by `flow2spec init` and unrelated to business capability coverage) +- `presentations/**` / `assets/**` / other pure static resources + +**Any** file falling under `src/` / `lib/` / `cli.js` / `templates/` / business code directories disables this shortcut; continue to the coverage check. + +**Skip rule B: A recent knowledge-base sync exists** + +Read `.Knowledge/.last-sync.json` (if it does not exist, skip this rule): + +```json +{ + "syncedAt": "2026-08-04T10:30:00.000Z", + "skill": "f2s-kb-sync", + "developerId": "" +} +``` + +- If `Date.now() - Date.parse(syncedAt) < 30 * 60 * 1000` (within 30 minutes) → skip this step directly, and note in Step 5 "skipped coverage check because ran within the last 30 min". +- If the timestamp is stale or the file is corrupted → ignore and run the normal coverage check. +- This file is written by **knowledge-base-writing skills** (`f2s-kb-sync` / `f2s-kb-feat` / `f2s-kb-fix` / `f2s-kb-add` / `f2s-kb-addRules` / `f2s-kb-distill`) on successful completion. `f2s-git-commit` **reads only**, never writes. +- If the user explicitly says "re-check coverage" / "don't skip coverage check", this rule is disabled and coverage runs regardless. + +**When it exists, perform the coverage check:** + +**First perform the KB auto-merge preflight (required; do not ask the user to run commands manually):** + +1. The agent runs `flow2spec kb check --json` and `flow2spec kb status --json` inside this step, or uses an equivalent built-in KB engine capability. Do not turn these commands into manual pre-commit chores for the user. +2. If `check` reports knowledge-structure errors, missing matchers, routing drift, or other health issues: stop this commit, report the concrete issues and suggested fix actions, and do not commit a broken knowledge base. +3. If `status.tasks` contains `kb-delta.json` under the current developer task root: + - If the current task line can be uniquely identified and `mergeable=true`: automatically run `plan -> apply -> build -> check` (via CLI or equivalent built-in capability), and include the written `.Knowledge/**` files in the commit file list. + - If `mergeable=false`, delta parsing failed, or multiple active deltas exist and the agent cannot determine which one belongs to this commit: stop the automatic write, list `topic / reason / deltaPath`, and tell the user that semantic merge or task-line selection is required. Do not guess the merge. +4. Only when there is no active `kb-delta.json` for the current task line, continue to the coarse coverage check below. + +**When there is no auto-applicable delta, perform the coarse coverage check:** + +1. Infer the **functional modules** touched by this change from `git diff HEAD` and untracked file paths (use actual repository directories/package names; do not invent business names that do not appear). +2. Read the directory lists of `.Knowledge/topics/` and `.Knowledge/stock-docs/`. +3. Compare the functional modules inferred in Step 1 and determine whether corresponding docs are registered in the knowledge base. +4. Conclude: **covered / partially covered / not covered**. + +> Coarse-grained judgment is enough: if a corresponding topic or stock-docs document exists, treat it as covered; if the knowledge base is empty or no related doc is found, treat it as not covered. + +**When not covered or partially covered (must prompt):** + +``` +⚠️ The following capabilities touched by this change are not yet in the knowledge base: + - + +Recommended before committing: + A) Run f2s-kb-sync now to record them, then automatically continue the commit flow + B) Commit first and record them manually later (enter B to confirm) + C) Cancel this commit (enter C) +``` + +- Choose **A**: prompt the user to run `f2s-kb-sync` or `f2s-kb-feat`. After the user finishes recording and says so in the **same session**, or triggers this skill again, continue from Step 1 or Step 3 (**do not require** a separate "continue commit" confirmation; this matches Steps 3-4). +- Choose **B**: record the uncovered capability descriptions and output them in the Step 5 closing note. +- Choose **C**: stop this skill. + +### Step 3: Generate a Commit Message Draft (Required) + +Read `git diff HEAD` (if too long, use the first 300 lines), and generate a commit message from the actual changes. + +#### First-Line Format (Required): Type Emoji + Conventional Commits + +The **first line** must satisfy all of the following: + +1. **Start with one emoji** corresponding to the `type` table below. **Do not** stack multiple decorative emojis. +2. Follow it with **one ASCII space**, then lowercase **`type`**, an English colon `:`, **one space**, and a short Chinese or English summary. +3. **Optional scope**: use Conventional `type(scope):`, directly after `type` and before the colon, for example `🐛 fix(auth): fix lost login state`. +4. Recommended total first-line length: **<= 72 characters** (including emoji). If too wide, shorten the description first. + +**Recommended template (single line)**: + +```text + [(scope)]: +``` + +Omit the parentheses segment when there is no scope, for example: `🚀 feat: add cache warmup`. + +**`type` -> first-character emoji (use exactly from this table for searchability and release notes)**: + +| `type` | emoji | Typical scenario | +|--------|--------|----------| +| `feat` | 🚀 | New feature or user-visible capability increment | +| `fix` | 🐛 | Bug fix or production/test issue | +| `docs` | 📚 | Docs only, comments, README, knowledge-base body content | +| `style` | 💄 | Pure formatting, indentation, semicolons, and layout with no behavior change | +| `refactor` | ♻️ | Refactor, rename, structural change with no behavior change | +| `perf` | ⚡ | Performance optimization | +| `test` | 🧪 | Tests, stubs, snapshots | +| `build` | 🏗️ | Packaging, dependencies, compile scripts, artifacts | +| `ci` | 👷 | CI config, pipelines, automation scripts | +| `chore` | 🔧 | Miscellaneous maintenance or tooling not build/ci | +| `revert` | ↩️ | Revert a commit | + +**Examples**: + +```text +🚀 feat: support activity cache warmup +🐛 fix(coupon): correct coupon window boundary condition +📚 docs: add QConfig notes for shared modules +♻️ refactor: extract group-buying validation +🔧 chore: upgrade ESLint config +``` + +**Body (optional)**: from the second line onward, use paragraphs or list items. **Do not require** an emoji on each body line. Use `- ` for list items if needed. + +**If the user already provided the first line**: if it already contains one table emoji and the emoji matches the `type`, respect the user's wording. If it has only `type:` without an emoji, **add the emoji** before Step 4. + +**Confirmation strategy for `git commit` (required)**: + +- In the **same assistant reply**: **first** show the finalized commit message **first line** (and optional body), then immediately execute Step 4 (`git add` item by item + `git commit`). **Do not require** the user to reply "confirm" before committing. +- If the user already provided a compliant commit message in this turn, use it directly and enter Step 4, but still **repeat the first line** before committing. +- If the user explicitly says "change the commit message / use another type": revise it, then under the same strategy **show and commit** without adding a "please confirm" gate. + +### Step 4: Execute the Commit (Immediately After Showing the Message) + +Handle the three file categories from Step 1: + +```bash +# 1. Unstaged files: add first +git add + +# 2. Untracked files: add first +git add + +# 3. Staged files: already added; no need to add again + +# Execute commit +git commit -m "" +``` + +- Do not use `git add -A` / `git add .`; only add the explicit file list from Step 1. +- If a pre-commit hook fails: output the full error, ask the user to fix it and trigger this skill again, and **do not** bypass it with `--no-verify`. +- If commit succeeds: read the commit hash (`git rev-parse --short HEAD`) and proceed to Step 5. + +### Step 5: Closing Note + +``` +✅ commit complete + + +[If Step 2 chose B] +📌 Reminder: the following capabilities are still not in the knowledge base; record them before merging: + - + You can run: f2s-kb-sync or f2s-kb-feat + +[If Step 2 was skipped because .Knowledge does not exist] +💡 This project has not initialized the Flow2Spec knowledge base. To enable it, run: flow2spec init + +[If Step 2 was skipped by quick commit] +⚡ The knowledge-base coverage check was skipped according to quick commit mode. + +[If skip rule A matched: pure-doc changes] +📄 Changes are pure docs / knowledge base itself; coverage check skipped. + +[If skip rule B matched: recent sync within 30 min] +🔄 Skipped coverage check because ran within the last 30 min (.Knowledge/.last-sync.json). +``` + +## Constraints + +- Do not use `git add -A` / `git add .`; add only confirmed changed files. +- Do not use `--no-verify`; if a hook fails, fix and retry. +- Do not `--amend` a pushed commit unless the user explicitly asks. +- Do not push automatically. Stop after the commit completes. +- In default mode, when the knowledge base does not cover the changes, you must prompt the user; the user decides whether to record now (choosing B does not block the commit). In quick commit mode, skip the knowledge-base coverage check and do not prompt recording options. +- **`git pull` / `git pull --rebase` / `git fetch` followed by merge operations that modify the current branch working tree**: you **must** explain the purpose and risk first and obtain explicit user confirmation for the **pull** (for example the user replies "confirm pull") before executing it. **Do not** silently pull as part of committing. +- **`git commit`**: a separate user reply of "confirm" is **not required**; however, it is **forbidden** to commit without showing the proposed first line in the same reply first. +- The commit-message **first line** must follow the **emoji + type** format in Step 3 (keep a user-provided compliant line as-is). +- If merge-conflict markers exist, stop and do not continue. + +## Completion Self-Check + +1. Did Step 1 check merge conflicts? Must be yes. +2. Were staged / unstaged / untracked files distinguished? Must be yes. +3. Was `git add -A` / `git add .` used? Must be no. +4. Was the knowledge-base check performed or skipped with an explicit reason (quick commit / `.Knowledge` missing)? Must be yes. If an active `kb-delta.json` exists, was it automatically planned/applied/built/checked or was a conflict explicitly reported? Must be yes. +5. Was the Step 3 commit message generated from actual `git diff` content? Must be yes, not only `--stat`. +6. Was the proposed first line **shown in the same reply** before executing commit? Must be yes; do **not** require the user to separately "confirm commit". +7. Does the commit-message **first line** match ` [(scope)]: `, with emoji and type consistent with the table? Exceptions such as merge revert must be explained when shown. +8. If pre-commit failed, was the hook bypassed? Must be no. +9. If Step 2 chose B, does the closing note include an uncovered-knowledge reminder? +10. If Step 2 chose A, does the flow continue after the user records knowledge or triggers again (**without** requiring a separate confirmation just to continue commit)? +11. If this flow ever needed `git pull`: was explicit confirmation for **pull** obtained before running it? Must be yes; if not involved, mark N/A. diff --git a/packages/core/templates/en-US/skills/f2s-kb-add/SKILL.md b/packages/core/templates/en-US/skills/f2s-kb-add/SKILL.md new file mode 100644 index 0000000..69cd9c3 --- /dev/null +++ b/packages/core/templates/en-US/skills/f2s-kb-add/SKILL.md @@ -0,0 +1,132 @@ +--- +name: f2s-kb-add +description: Parse already implemented capabilities into the knowledge base during work (multi-file aggregation): draft -> final draft -> topics/index/manifest; triggers: f2s-kb-add、已有能力进知识库、多文件生成上下文、add existing capability to knowledge base、multi-file context generation +--- + +> Execution scope: this skill only maintains `.Knowledge`; it does not modify the configuration-root `rules/skills`. + +## Orchestration (main / sub-agent) + +- The meaning of `subAgent` / `switchAgentVerification` uses the unified entry as the only source of truth: **Cursor/Claude** read the configuration-root `rules/f2s-flow2spec-unified-entry.*`; **Codex** reads `.codex/topics/f2s-flow2spec-unified-entry.md` (same source, mirrored by `flow2spec init`). +- Do not split by default: the main session completes the full workflow; below the threshold, sub-agent benefit is lower than context-switching cost. +- Split threshold (only when `subAgent=true` and any condition is met): (1) input paths >= 5; (2) a single source file > ~3000 lines; (3) total across paths > ~10000 lines. +- **Split strategy (enabled only when the split threshold is reached and `subAgent=true`)**: + - **Mode B (default, single-round parallel)**: main first produces an "inventory" (source document path list to parse + core capability names, handwritten by main; sub-agents may not add/remove paths) + "scan contract" (which sections/line ranges to read for each source, forbidden scan directories, unified output fields and table headers) -> sub-agents read only and fill the table in parallel -> main merges + dedupes in one round -> writes `.Knowledge/stock-docs/_draft.md` -> main performs user confirmation and acceptance. Suitable when source boundaries are clear, scale is medium, and a first version is needed quickly. + - **Mode C (large repository / high risk, multi-round correction)**: before B or replacing B's first round, main creates the inventory -> sub-agents submit tables -> main performs one dedicated **table comparison** round (mark overlaps / conflicts / missing dependencies / cross-source boundaries) -> if needed, assign small follow-up tasks for conflicts or main reads key points directly -> main writes / finalizes. Suitable for multi-workspace / monorepo, very deep directories, source paths > 20, first-round sub-agent tables with obvious conflicts or holes, or severe overlap/conflict among sources. + - **Switch criteria** (switch to C if any is true): multi-workspace / monorepo; very deep directories or source paths > 20; first-round sub-agent tables have obvious conflicts / holes; source narratives overlap / conflict severely. +- **Sub-agent delivery hard rule**: sub-agents may not trim the source path scope on their own; they must follow the handwritten main inventory. Delivery must use the "sub-agent YAML schema" (fields: `source` / `scope` / `capabilities` / `cross_refs` / `pending`); prose replies are forbidden. Sub-agents must not write `manifest-routing.json` / `.Knowledge/index.md`, and must not independently announce "added to knowledge base". +- Main agent must control overlap judgment, final-draft finalization, `f2s-kb-build` dispatch, and overall acceptance. +- Write-authority hard rule: `manifest-routing.json` and `.Knowledge/index.md` are always written by the main agent. +- The writing side verifies its own work. + +# f2s-kb-add: Multi-File Aggregation -> Draft -> Final Draft -> Knowledge Routing Sync + +## When to Use + +- A capability has already been implemented in code, but information is scattered across multiple files and needs to be captured as searchable knowledge. +- Different from `f2s-doc-arch`: `doc-arch` produces an architecture draft; `doc-add` produces the knowledge-capture chain for an "already implemented capability". + +## Input + +| Parameter | Required | Description | +| --- | --- | --- | +| File path list | Yes | One or more paths (space/newline/`@`); supports source code, config, and docs | +| Plan name | No | Used to generate `_draft.md` and `_final.md` | +| Draft/final path | No | Defaults to `.Knowledge/stock-docs/` | + +Abort and ask the user for valid paths if no valid path is provided. + +## Step 0: Overlap Check (Important) + +Before execution, compare against: + +- `.Knowledge/index.md` +- `.Knowledge/topics/*.md` +- `.Knowledge/stock-docs/*.md` + +If the same topic has already been captured, update it in place first to avoid duplicate topics and duplicate index rows. + +## Step 0.5: Multi-Module Detection (Required when input paths >= 2) + +1. **Directory aggregation**: group files by functional-layer directories in paths (for example `src//`, top-level directory names). +2. **Judgment rules** (any one hit means "multi-module"): + - Files belong to >= 2 different top-level functional directories (for example `auth/`, `payment/`). + - The user explicitly mentions "multiple features / different modules / handle separately" or similar. + - Filename prefixes are clearly different and have no common parent directory. +3. **Single module (no trigger)**: do not interrupt; continue to step 1 and generate `_draft.md` using the existing single-output logic. +4. **Multi-module (triggered)**: **pause**, show the grouping result to the user, and ask: + - **Option A (recommended)**: generate knowledge files by module -> each group independently runs steps 1 -> 2 -> 3 -> 4 and outputs `_draft.md` / `_final.md`. + - **Option B (merge)**: ignore module boundaries and generate one `_draft.md` (original behavior). + - It is **forbidden** to default to option B and continue before the user explicitly chooses. +5. **Single module but large stock-doc**: if one input document or aggregated source exceeds **300-500 lines**, or covers **more than 3 unrelated responsibility domains**, suggest to the user that it can be split into multiple focused stock-docs, each corresponding to an independent topic. If the user confirms continuing, do not block, but record "recommended later split" in the output summary. + +## Step 1: Moderate-Depth Analysis + +- Read small files fully. +- For large files, prioritize structure and key fragments (exports, interfaces, config, flows). +- Mark uncertain content explicitly as "pending confirmation"; do not invent. +- If any split threshold is met (input paths >= 5 / single source > ~3000 lines / total across paths > ~10000 lines) and `subAgent=true`, split into parallel read-only scans using Mode B (default) or Mode C (when switch criteria are met); otherwise the main agent performs the full workflow. **When sub-agents are enabled, they must follow the main agent's handwritten inventory and scan contract; they must not add/remove source paths on their own.** + +## Step 2: Generate Draft + +- Default output: `.Knowledge/stock-docs/_draft.md` +- Recommended draft structure: + - Overview + - Source list (including unreadable files) + - Module-by-module summary + - Cross relationships + - Pending confirmations + +## Step 3: Generate Final Draft + +- Reference `.Knowledge/template/final-overview-template.md` +- Output: `.Knowledge/stock-docs/_final.md` +- **Must fill the `## 来源文件` section**, listing the original source file paths actually read in step 1. +- If the user asks to "review the draft first", stop at the draft and wait for confirmation. + +## Step 4: Sync Knowledge Routing + +Based on the final draft, use the `f2s-kb-build` approach to update: + +- `.Knowledge/topics/` +- `.Knowledge/index.md` +- Routing manifest (when needed) +- `manifest-routing.json.topicMetadata` (as needed): write `primary` / `tags` / `confidence` only for topicIds that already exist or are confirmed as created in this run; `tags` may be omitted and must not duplicate `primary`. Classification is only for governance, audit, and reading expectations; it does not participate in routing or execution requirements. If evidence is insufficient, do not write metadata and list it as pending confirmation in the summary. Do not create, rename, or split topics solely for classification. + +> **Authoring-side guideline**: this step triggers adding/modifying topics and `topicDependencies`, so first Read the full `rules/f2s-topic-authoring.*` (**Cursor/Claude**: `rules/f2s-topic-authoring.mdc`; **Codex**: `.codex/topics/f2s-topic-authoring.md`) before invoking the `f2s-kb-build` approach to sync. + +## Output Summary (Required) + +1. Draft/final-draft paths. +2. Updated topic/index/routing-manifest paths. +3. Incomplete items and reasons (for example invalid paths or insufficient information). + +## Complex Scenario Examples + +The user provides 6 files (mixed code, config, old docs), and 2 paths are unreadable. + +- Continue processing readable files first, and explicitly list unreadable paths and gaps in the draft. Do not abort the whole flow because of partial failure. +- If an existing `.Knowledge/stock-docs/_final.md` is found, revise that final draft first instead of creating a duplicate final draft. +- If the user asks to "review the draft first", stop at the draft, wait for confirmation, then generate the final draft and enter `f2s-kb-build` sync. + +The user provides 3 files: `src/auth/login.ts`, `src/payment/checkout.ts`, `src/notification/email.ts`. + +- Step 0.5 detects that the files belong to `auth/`, `payment/`, and `notification/`, three different top-level functional directories, and classifies this as "multi-module". +- Show the grouping to the user: `auth` group 1 file, `payment` group 1 file, `notification` group 1 file; ask for option A (separate generation) or option B (merge). +- User chooses option A: run steps 1 -> 2 -> 3 -> 4 for the `auth`, `payment`, and `notification` groups separately, outputting `auth_draft.md`, `payment_draft.md`, and `notification_draft.md`. +- It is **forbidden** to directly merge the three modules into `综合_draft.md` before the user chooses. + +## Constraints + +- Final-draft `sourceDoc` only points to `.Knowledge/stock-docs/*`. +- Do not modify the configuration-root `rules/skills`. +- Prefer updating the same topic; do not create duplicate parallel knowledge. +- `manifest-routing.json` and `.Knowledge/index.md` are always written by the main agent (write-authority hard rule); sub-agents must not touch them. + +## Completion Self-Check + +1. Draft/final-draft paths are under `.Knowledge/stock-docs/`. +2. Duplicate creation for the same topic was avoided. +3. topic/index/manifest semantics are consistent with the final draft. +4. If `topicMetadata` was written: it only covers topicIds that already existed or were created in this run; `primary` / `tags` / `confidence` are valid; type-prefix naming and renaming were avoided. +5. When input paths >= 2, step 0.5 multi-module detection was performed; if classified as multi-module, grouping was shown to the user and an explicit choice was awaited, with no default merged output. diff --git a/packages/core/templates/en-US/skills/f2s-kb-addRules/SKILL.md b/packages/core/templates/en-US/skills/f2s-kb-addRules/SKILL.md new file mode 100644 index 0000000..a00a729 --- /dev/null +++ b/packages/core/templates/en-US/skills/f2s-kb-addRules/SKILL.md @@ -0,0 +1,168 @@ +--- +name: f2s-kb-addRules +description: Capture user-spoken rules into the knowledge base, automatically decide "create new topic / merge into existing topic", and sync routing; does not write code or create `.task/`; triggers: f2s-kb-addRules、新增规则、口述规则、把这条记到知识库、add rule、capture spoken rule +--- + +> **Task paths**: all `.task/` reads/writes must use **`TASK_ROOT` from `rules/f2s-task`** (` .task` or `.task/`; config → git → legacy). Bare `.task/todo.json` / `.task/active/` below mean **`TASK_ROOT/...`**. + + +> Execution scope: this skill only maintains `.Knowledge` (`topics/index/manifest-routing/matchers` shards), does not modify the configuration-root `rules/skills`, does not touch business code, and does not create `.task/` (spoken rules are meta-configuration changes, not business change tracking). + +# f2s-kb-addRules: Put User-Spoken Rules into the Knowledge Base + +## Boundary with Existing Skills + +- Different from `f2s-kb-feat`: `f2s-kb-feat` is strongly tied to "code implementation + KB sync" and creates `.task/` when `changeTracking.feat` is hit; this skill **only captures rules**, does not change code, and does not track tasks. +- Different from `f2s-kb-build`: `f2s-kb-build` takes `.Knowledge/stock-docs/_final.md` as input; this skill takes **rule text spoken by the user in the current conversation**. +- Different from `f2s-kb-add`: `f2s-kb-add` aggregates "multiple source/config files" into stock-docs; this skill skips stock-docs and writes directly to topics. + +## Orchestration (main / sub-agent) + +- The meaning of `subAgent` / `switchAgentVerification` uses the unified entry as the only source of truth (**Cursor/Claude** read `rules/f2s-flow2spec-unified-entry.*`; **Codex** reads `.codex/topics/f2s-flow2spec-unified-entry.md`). This SKILL does not repeat it. +- By default, the main agent performs the full workflow: a spoken rule is usually a short text, so sub-agent splitting has lower benefit than context-switching cost. +- **Write-authority hard rule**: `.Knowledge/manifest-routing.json` / `.Knowledge/index.md` are always written by the main agent. +- The writing side verifies its own work. + +## Input + +- One rule sentence or paragraph spoken by the user (free text; no fixed format). +- The user **does not need** to specify a target topic, filename, `alwaysApply`, or similar parameters; this skill judges and proposes them. + +## Mandatory Prerequisite: Read Authoring-Side Guideline + +Before executing any step, **Read** the full `rules/f2s-topic-authoring.*` (**Cursor/Claude**: `rules/f2s-topic-authoring.mdc`; **Codex**: `.codex/topics/f2s-topic-authoring.md`). All later naming, skeleton, dependency judgment, DAG minimization, and write ownership follow that guideline. + +## Step 1: Normalize Intent + +Normalize the user's spoken text into a writeable "rule unit": + +- Extract **constraint phrasing** ("when doing X, must / must not / prefer Y") or **workflow description** ("the processing order of X is A -> B -> C"). +- Identify the rule's **applicable scenario** (trigger condition, file-path scope, lifecycle phase, etc.). +- Do not infer boundaries the user did not state; write only what was spoken, and keep unclear parts for questions in step 3. + +## Step 2: Scan Existing Topics (Required) + +- Read `.Knowledge/manifest-routing.json` to get the full `topicPaths` set. +- Read the topic table in `.Knowledge/index.md`, scanning once by topic id + one-sentence intent. +- When necessary, Read the first 10-30 lines of candidate `topics/.md` files by **keywords** in the rule body (do not load all topics in full). +- Output a **candidate list** (highest to lowest overlap, at most 3) as input to step 3. + +## Step 3: Decide New vs Merge (Required, Confirm with User) + +Show the **candidates** to the user and propose one branch: + +- **High overlap** (the spoken rule is clearly a refinement / supplement / exception of an existing topic) -> propose "**merge into** `topics/.md`", and identify the intended insertion point (section name / paragraph anchor). +- **No overlap / low overlap** (no suitable host found) -> propose "**create** `topics/.md`"; generate the new id from the rule body in **kebab-case**, following `f2s-topic-authoring` naming constraints (no version suffix, no personal nickname, no conflict with existing `index.md` titles). +- **Crosses multiple topics** (one spoken rule constrains >= 2 topics) -> **pause** and present split options: + - Option A: split into >= 2 rule units and merge each into the corresponding topic. + - Option B: choose one primary topic to merge into, and add one-line cross-references in the other topics. + - Option C: create a **general** topic to govern them, and add references from old topics; use only when the rule truly cuts across multiple domains. + +> Before the user confirms, writing `topics/` / `manifest-routing.json` / `index.md` is **forbidden**. + +## Step 4: Write to Disk (After User Confirmation) + +### 4a. Write `topics/.md` + +- **Create**: write the five skeleton items from `f2s-topic-authoring` section 2 "topic body skeleton" one by one (title and one-sentence intent / applicable scenario / core rules / dependency declaration / boundaries and forbidden items). +- **Merge**: perform **surgical insertion** at the confirmed section / paragraph: only add sentences or paragraphs directly related to this rule; do not rewrite the whole file or restate background opportunistically. +- Writing style follows `f2s-flow2spec-unified-entry` "knowledge-base writing style": **affirmative wording first**; mutually exclusive choices are exceptions. + +### 4b. Judge `topicDependencies` (Required) + +Use the four questions + reverse exclusion + DAG minimization in `f2s-topic-authoring` section 4. Scan newly written text for **backtick references to other topic ids / rule filenames**, and judge each one: + +- Hit -> add an edge to `manifest-routing.topicDependencies`, **and** write an explicit sentence in the new/modified topic body: "Before execution, read dependency topic `` first". +- No hit -> do not write a dependency; rely on `taskToTopicRules` second-highest candidates + `expand` supplemental recall. + +When merging into an existing topic, if the change only refines an existing rule and does not introduce a strong reference to a new topic, usually no new dependency edge is needed. + +### 4c. Sync Routing (Main Agent Only) + +- **New topic**: + - Add `manifest-routing.topicPaths`: ` -> .Knowledge/topics/.md`. + - Add `manifest-routing.topicMetadata` as needed: spoken-rule topics are usually `{ "primary": "policy", "confidence": "inferred" }`; write `manual` when the user explicitly confirms the classification. If it also contains config/module/capability characteristics, write `tags` that do not duplicate `primary`; if evidence is insufficient, do not write metadata and list it as pending confirmation in the summary. Classification is only for governance, audit, and reading expectations; it does not participate in route matching or execution requirements. + - Add `taskToTopicRules[]` only when this rule should be matched as **user task routing** (see `f2s-topic-authoring` section 5 criteria); internal rules that are only referenced by other rules / SKILLs **do not enter** `taskToTopicRules`. + - If `taskToTopicRules[]` is added, create `.Knowledge/matchers/.json`, extracting `includeAny` keywords from the user's wording (the original words + 1-2 obvious synonyms; prefer missing terms over over-broad terms). +- **Merge into existing topic**: + - `topicPaths` does not change. + - Fill this topic's `topicMetadata` as needed, but do not create, rename, or split topics for classification. + - Only when the spoken rule **adds a trigger scenario**, minimally update the corresponding `matchers/.json` `includeAny`; otherwise leave the matcher unchanged. + +### 4d. Update `index.md` + +- New topic: add one row to the topic table (one-row-per-topic principle). In the "Associated documents (summary)" column, write "none" or "to be added" (spoken rules usually have no stock-docs / req-docs anchor); do not leave it blank. +- Merge into existing topic: update that row's "topic intent" summary only if the topic intent changes; otherwise leave it unchanged. + +## Step 5: Output Summary (Required) + +```markdown +## Rule Capture Result + +### Spoken Rule +> + +### Write Decision +- Mode: create / merge / cross-topic split +- Target: .Knowledge/topics/.md (section: ) + +### Knowledge-Base Changes +- .Knowledge/topics/.md: +- .Knowledge/manifest-routing.json: +- .Knowledge/matchers/.json: +- .Knowledge/index.md: + +### Follow-up for User +- +``` + +## Constraints + +- Do not write code, do not touch the configuration-root `rules/skills`, and do not create `.task/`. +- Before the user confirms "create / merge / cross-topic split", writing is forbidden. +- Prefer merging into the same topic to avoid creating near-duplicate topics (see the "do not" naming items in `f2s-topic-authoring`). +- `manifest-routing.json` and `.Knowledge/index.md` are always written by the main agent (write-authority hard rule). +- The routing manifest receives only minimal changes; unrelated fields are not rewritten. +- Writing style follows the unified entry "knowledge-base writing style" and single-file length soft constraints (spoken rules usually need <= 30 added lines of body text). + +## Complex Scenario Examples + +**Scenario A: High-overlap merge** + +User says: "When writing commit messages, the first line must start with a Chinese emoji." +The scan finds existing `topics/f2s-git-commit.md` (describes the git commit flow). +- Step 3 proposes: **merge into** the "commit style" section of `topics/f2s-git-commit.md`. +- Step 4a appends a rule paragraph to that section and does not change other sections. +- Step 4b adds no dependency. +- Step 4c leaves manifest unchanged, only adding 1-2 keywords to the topic's matcher if one exists. +- Step 4d leaves index unchanged. + +**Scenario B: New topic** + +User says: "All user-facing error messages must start with a verb, such as 'Retry' or 'Check X', instead of 'Error: X failed'." +The scan finds no suitable host. +- Step 3 proposes: **create** `topics/error-message-style.md`. +- Step 4a writes it using the skeleton. +- Step 4b evaluates whether it depends on existing i18n / copywriting convention topics, and declares the dependency if hit. +- Step 4c judges whether daily user conversations will trigger "error-message copy" task routing. If yes, add `taskToTopicRules` + create matcher; if it is only an internal convention referenced by other SKILLs, **do not add** `taskToTopicRules`. +- Step 4d adds one index row. + +**Scenario C: Cross-topic split** + +User says: "When implementing from a design, do not edit docs while coding; before submitting a PR, tests must be run first." +This clearly involves two topics: `f2s-implement-tech-design` (implementation discipline) and `f2s-git-commit` (submission flow). +- Step 3 pauses and presents options A / B / C. +- User chooses A -> split into two rule units and merge them into the two topics separately. +- Step 4 writes to both topics, and the output summary lists both changes. + +## Completion Self-Check + +1. The full `rules/f2s-topic-authoring.*` was Read before writing. +2. No files were written before the user confirmed "create / merge / cross-topic split" (must be false). +3. New topic: `topicPaths` is complete; the body contains the five skeleton items; `taskToTopicRules` and matcher `includeAny` satisfy the criteria for whether a rule needs topic routing. +4. If `topicMetadata` was written: the key exists in `topicPaths`; `primary` / `tags` / `confidence` are valid; no topicId / filename was changed for classification. +5. Merged topic: only surgical insertion was performed; unrelated sections were not rewritten opportunistically. +6. `topicDependencies` was judged by the four questions; no redundant transitive edge or cycle was introduced. +7. `index.md` and `topics/` file set correspond one-to-one; new topics fill the "Associated documents (summary)" column. +8. The configuration-root `rules/skills` was not touched; `.task/` was not created. +9. Output summary is complete (original text / decision / changes / follow-ups). diff --git a/packages/core/templates/en-US/skills/f2s-kb-build/SKILL.md b/packages/core/templates/en-US/skills/f2s-kb-build/SKILL.md new file mode 100644 index 0000000..b031e98 --- /dev/null +++ b/packages/core/templates/en-US/skills/f2s-kb-build/SKILL.md @@ -0,0 +1,113 @@ +--- +name: f2s-kb-build +description: Generate knowledge-routing topics and indexes from `.Knowledge/stock-docs` documents; triggers: 生成项目上下文、f2s-kb-build、终稿生成上下文、generate project context、build knowledge context +--- + +> Execution scope: this skill only maintains `.Knowledge` (`topics/index/manifest-routing/matchers` shards) and does not modify the configuration-root `rules/skills`. It no longer maintains `.Knowledge/manifest-matchers.json` (deprecated aggregate file; `flow2spec init` deletes legacy copies). + +# Generate Project Context from Documents (topics/index/routing manifest) + +## Orchestration (main / sub-agent) + +- The meaning of `subAgent` / `switchAgentVerification` uses the unified entry as the only source of truth: **Cursor/Claude** read the configuration-root `rules/f2s-flow2spec-unified-entry.*`; **Codex** reads `.codex/topics/f2s-flow2spec-unified-entry.md` (same source, mirrored by `flow2spec init`). This SKILL does not repeat those definitions. +- **Preferred branch (small change -> main-only workflow)**: when this change has **<= 2 new/modified topics**, **<= 1 new matcher**, and **no batch cross-topic reference adjustment**, the main agent completes the full workflow without splitting. +- **Medium/large change branch** (`subAgent=true` and above threshold): + - The main agent lists a **file-level contract** in the main session: sub-agent A only writes `.Knowledge/topics/.md`, sub-agent B only writes `.Knowledge/matchers/.json`, and paths do not overlap. + - Sub-agents only write files inside their contract and do not cross boundaries. + - The **main agent alone** edits `.Knowledge/manifest-routing.json` / `.Knowledge/index.md` (adding `taskToTopicRules`, `topicPaths`, `matcherPath`, `topicDependencies`, `topicMetadata`). + - The main agent performs overall verification. +- **Not recommended**: one sub-agent modifying manifest / index / multiple topics / matchers at the same time; or "sub-agent A writes, sub-agent B verifies". +- **"One sub-agent writes, main verifies"**: acceptable only when the delivery boundary is extremely narrow, for example only producing one new matcher-shard draft while the manifest reference is still written by the main agent. +- **Write-authority hard rule**: `.Knowledge/manifest-routing.json` (including `topicMetadata`) / `.Knowledge/index.md` are **always written by the main agent**; sub-agents must not touch them. +- By default, the writing side verifies its own work; this SKILL does not bind cross-agent verification. + +## Input + +- Accepts one argument: a URL or local path. +- Local paths must be under `.Knowledge/stock-docs/`. +- **Must be a final draft**: recommended filename contains `_final.md`, or has been normalized by **`f2s-doc-final`**. It is **forbidden** to execute this skill directly with a `*_draft.md` produced by `f2s-doc-arch`. +- If the input path contains **`_draft`**, or the user has just completed an architecture draft but has not run `f2s-doc-final`: **stop** and reply that they must first run **`f2s-doc-final `**, then call this skill with the final-draft path after it is written. +- If `.Knowledge/req-docs/` is passed, tell the user to organize it into a `stock-docs` final draft before executing. + +## Generation Principles + +1. **Split**: when the document is long or contains multiple independent capabilities, split it into multiple topics; avoid putting unrelated capabilities into one topic. +2. **Responsibilities**: + - `topics/`: rule and workflow body (executable knowledge) + - `index.md`: topic index and semantic explanation (human entry) + - `manifest-routing.json` + `matchers/*.json` pointed to by `taskToTopicRules[].matcherPath`: task routing and keyword dictionaries (machine-readable entry) + +## Step 1: Get Document Content + +- URL: fetch the body; if inaccessible, ask the user to first save it as `.Knowledge/stock-docs/*.md`. +- Local path: read the Markdown document and extract topics and capability boundaries. + +## Step 2: Semantic Analysis (Required) + +Extract from the document: + +- Topic name and topic intent (can form a topic id) +- Core concepts and key flows +- Business rules and boundary conditions +- Task trigger terms (write to the corresponding `matchers/.json` `includeAny`) +- Dependencies on existing topics (for `topicDependencies`) + +> **Authoring-side guideline**: this step involves adding/modifying topics and `topicDependencies`, so first Read the full `rules/f2s-topic-authoring.*` (**Cursor/Claude**: `rules/f2s-topic-authoring.mdc`; **Codex**: `.codex/topics/f2s-topic-authoring.md`) before continuing to step 3 / step 5. Naming, skeleton, dependency judgment, DAG minimization, and judgment timing all follow that guideline; this SKILL does not repeat them. + +> **Split evaluation**: if the input stock-doc exceeds **300-500 lines**, or semantic analysis finds it covers **more than 3 unrelated responsibility domains**, state in the output summary that it is recommended to split it into multiple focused stock-docs (each corresponding to an independent topic) and execute in batches after user confirmation. If the user chooses to continue with one large topic, do not block, but record "topic is large; recommend later split" in the summary. A large feature's main topic should describe the business closure/entry/submodule stock-doc navigation links; submodule topics should match independently, and **overview/detail must not be chained via `topicDependencies`**. + +## Step 3: Write topics + +- Target path: `.Knowledge/topics/.md` +- If the same topic already exists: prefer incremental updates to avoid duplicate topics. +- If it is a new topic: add the file with a clear title, applicable scenarios, rules, and workflow. + +## Step 4: Update index + +- Update the topic routing table in `.Knowledge/index.md`. +- Guarantee "one row per topic". +- The topic routing table must maintain an "Associated documents (summary)" column: add 1-3 key document **clickable Markdown links** for each topic (format: `[title](relative path)`, preferably `stock-docs/req-docs`). +- If a topic has no public document yet, write "none" or "to be added"; do not leave it blank. +- When topics are added/deleted, update the index to avoid orphan paths. + +## Step 5: Update Routing Manifest (As Needed) + +- This step is written by the main agent (write-authority hard rule); sub-agents must not perform it. +- Update `manifest-routing.topicPaths` (topicId -> topic file path). +- Update `manifest-routing.taskToTopicRules[]` (task-to-topic set + matcherId). +- Update `manifest-routing.topicDependencies` (read dependency topics before main topics). +- Update `manifest-routing.topicMetadata` (as needed): write `{ "primary": "feature|module|config|policy", "tags": ["..."], "confidence": "manual|inferred" }` only for topicIds that already exist or are confirmed as created in this run; `tags` may be omitted and must not duplicate `primary`. Classification is only for governance, audit, and reading expectations; it does not participate in route matching or execution requirements. New topics may use `inferred` when evidence is clear; write `manual` only after user confirmation; if evidence is insufficient, do not write metadata and list it as pending confirmation in the summary. Do not create, rename, or split topics for classification. +- Update `matchers/.json` `includeAny` (keyword dictionary; path must match `taskToTopicRules[].matcherPath`). +- Validate that `fallbackTopic`, `topicPaths`, and `matcherId` references are valid. +- Make only minimal changes; do not rewrite unrelated fields. + +## Path and Reference Constraints + +- `sourceDoc` or document references uniformly point to `.Knowledge/stock-docs/.md`. +- Do not use `.Knowledge/req-docs/` as a topic `sourceDoc`. +- Do not rewrite the configuration-root `rules/skills`. + +## Output Summary (Required) + +- New/updated topic files. +- `index` updates. +- Routing-manifest updates, if any. +- Failed or skipped items and reasons. + +## Complex Scenario Example + +User input: `f2s-kb-build .Knowledge/stock-docs/_final.md`, and an existing `topics/.md` already exists. + +- If the new document highly overlaps with the existing `` topic: update `topics/.md` in place; do not create `-v2.md`. +- If the new document adds a sub-capability: create `topics/-.md` if appropriate, and declare dependencies in `manifest-routing.topicDependencies`. +- After updating, sync `index` and the routing manifest, ensuring `topicPaths`, `fallbackTopic`, and `matcherId` remain valid. + +## Completion Self-Check + +1. `.Knowledge/topics/*.md` and `manifest-routing.topicPaths` correspond one-to-one. +2. The `index.md` topic table is consistent with the topic-file set, and each topic contains "Associated documents (summary)". +3. Every `taskToTopicRules[].matcherPath` file exists, and its `id` matches `matcherId`. +4. If `topicMetadata` was written: every key exists in `topicPaths`; `primary` / `tags` / `confidence` are valid; `tags` do not duplicate `primary`; no topic was changed just for classification. +5. The configuration-root `rules/skills` was not touched. +6. For medium/large changes, sub-agents were split by file-level contract (sub-agent A / B paths do not overlap). +7. `manifest-routing.json` / `.Knowledge/index.md` were written at a single point by the main agent, with no unauthorized sub-agent writes. diff --git a/packages/core/templates/en-US/skills/f2s-kb-distill/SKILL.md b/packages/core/templates/en-US/skills/f2s-kb-distill/SKILL.md new file mode 100644 index 0000000..db1d5db --- /dev/null +++ b/packages/core/templates/en-US/skills/f2s-kb-distill/SKILL.md @@ -0,0 +1,380 @@ +--- +name: f2s-kb-distill +description: Extract reusable knowledge facts from Q&A and auto-commit to KB; decide whether to create new topic or append to existing topic based on drill-down depth; trigger: f2s-kb-distill, extract knowledge from Q&A, distill knowledge from conversation +--- + +> **Task paths**: all `.task/` reads/writes must use **`TASK_ROOT` from `rules/f2s-task`** (` .task` or `.task/`; config → git → legacy). Bare `.task/todo.json` / `.task/active/` below mean **`TASK_ROOT/...`**. + + +> Execution scope: This skill only maintains `.Knowledge`, does not modify config root `rules/skills` by default. + +## KB Auto-Merge Protocol (Required) + +This skill must not make manual command execution part of the user flow. After the user triggers this skill, the agent performs knowledge candidate generation, merge planning, build, and validation by itself: + +1. If reusable knowledge should be recorded, first form a `kb-delta` draft in the current task context with `taskId`, `developerId`, `baseRevisions`, `changes`, and evidence summary. If there is no explicit task directory, an equivalent in-memory object is acceptable; do not create `.task` only for this skill. `changes` may use `appendBody` / `replaceBody` / `updateFrontmatter`; when a new topic is needed, use `createTopic` and optionally include `taskRule` plus `matcher` so routing is connected in the same merge. +2. Before writing `.Knowledge`, run `flow2spec kb plan ` or the equivalent internal capability. If a topic revision differs, stop automatic writing and switch to semantic-merge reporting. +3. When the change is auto-mergeable, run `flow2spec kb apply ` or the equivalent internal capability, then run `flow2spec kb build` and `flow2spec kb check`. +4. The user should only see "knowledge base synced / semantic conflict needs confirmation / skipped with reason"; do not ask the user to manually run `kb plan/apply/build/check`. + +## Orchestration (main / sub agent) + +- `subAgent` / `switchAgentVerification` semantics follow unified entry as single source of truth: **Cursor/Claude** read config root `rules/f2s-flow2spec-unified-entry.*`; **Codex** read `.codex/topics/f2s-flow2spec-unified-entry.md` (same source, mirrored by `flow2spec init`). +- This skill does not split sub by default: Q&A knowledge extraction is a single-round focused task, completed by main agent is more efficient. +- Write permission constraint: `manifest-routing.json` and `.Knowledge/index.md` are always written by main agent only. +- Verification: self-verify on the side that writes to disk. + +# f2s-kb-distill: Q&A-Driven Knowledge Extraction and Ingestion + +## When to Use + +- User asks → agent drills down source code to answer → need to solidify discovered knowledge into KB +- Usually auto-suggested by `f2s-kb-feedback-closing` rule, can also be manually invoked by user +- Distinction from `f2s-kb-sync`: `sync` is for batch syncing multiple capabilities; `distill` focuses on knowledge extraction from single Q&A + +## Input + +| Parameter | Required | Description | +| --- | --- | --- | +| User question | Auto-extract | Previous user question (auto-extract from conversation history) | +| Agent answer | Auto-extract | Previous agent answer content (auto-extract from conversation history) | +| Matched topic | Optional | If triggered by `f2s-kb-feedback-closing`, carries matched topicId | +| Drilled files | Auto-analyze | Extract referenced files/functions from answer (auto-analyze) | + +Abort and prompt user when no valid Q&A context exists. + +## Execution Tiers (Light / Strict, agent auto-judged) + +`f2s-kb-distill` has **only one** entry (no `--fast` parameter). The first thing on entering this flow is to **decide the tier**: + +### Tier judgment (4 dimensions; light tier only when all 4 are satisfied) + +| Dimension | How to measure | Condition for **light tier** | +| --- | --- | --- | +| Upstream `f2s-kb-feedback-closing` case | Look at the closing block at the end of this/last agent reply | **case 2 or case 3** (case 1 / no closing block → strict tier) | +| Business source files Read this turn | Agent reviews its own tool calls this turn | **≤ 3 files** | +| Function / class names cited in this turn's reply | Count backtick-wrapped `xxx()` / class names in the reply | **≤ 5** | +| Did the user reject the upstream conclusion in a follow-up? | Look at the latest user input for "no / re-analyze / that's wrong" etc. | **No** | + +**All 4 satisfied** → **light tier**: skip step 2.1 (quantitative scoring) / 2.4 (existing topic description depth) / step 3 (decision matrix) / step 4.1's "read neighboring topics for style alignment"; adopt the upstream "this round will ingest: " directly to decide strategy and target topicId, then proceed to step 4 to generate content. + +**Any one fails** → **strict tier**: run the full 6 steps. + +> **Business source defined**: a Read whose path is **not** under `.claude/` / `.cursor/` / `.codex/` / `.Knowledge/` / `.task/` counts. Rule files / topics / config / task lists do not count. + +### Why these dimensions are enough (design intent) + +- **Case type** filters out the "new topic" scenario: creating a new topic must read neighboring topics for style, configure matcher `includeAny`, and update `taskToTopicRules` — none of these can be skipped; +- **Read count + function-citation count** reflects whether this turn's knowledge is "light enough": only light supplements may skip judgments. Deep ingestion (many files + many functions) skipping them risks "description depth mismatch ≥ 2 levels" incidents; +- **User rejection signal** backstops the "upstream case judged wrong" edge. + +### Steps 5 / 6 never skipped + +Regardless of tier, **step 5 routing / matcher / index sync** and **step 6 write + self-check** always run in full — these are hard correctness constraints. + +## Mandatory Flow (cannot be reordered) + +### Step 0: Read Config and Rules + +1. Read `flow2spec.config.json` (get `subAgent` / `switchAgentVerification`) +2. Read `rules/f2s-kb-feedback-closing.mdc` (get "reusable knowledge facts" definition) +3. Read `rules/f2s-topic-authoring.mdc` (get topic authoring guidelines) + +### Step 1: Extract Q&A Context + +Extract from previous conversation turn: + +1. **User question**: Original question text +2. **Agent answer**: Complete answer content +3. **Matched topic**: Extract matched topicId if `f2s-kb-feedback-closing` already analyzed; otherwise re-route based on question +4. **Drilled files**: Extract all referenced file paths, function names, line numbers from answer +5. **Referenced code**: Extract code snippets quoted in answer + +### Step 2: Analyze Drill-Down Depth and Knowledge Nature + +> ****Light tier** skips**: sections 2.1 / 2.4 are skipped entirely; 2.2 (extract knowledge facts) must run; 2.3 (knowledge description depth) is reduced to **a brief annotation** (one line stating "summary-level / detailed-level / implementation-level", no multi-dimensional evaluation). + +#### 2.1 Calculate Drill-Down Depth Score + +Accumulate following indicators (each 0-10 points, total 0-50): + +- **Files read count**: + - 0 files: 0 points + - 1-2 files: 3 points + - 3-5 files: 7 points + - 6+ files: 10 points + +- **Segmented read count** (same file read multiple times at different line ranges): + - 0-1 times: 0 points + - 2-4 times: 3 points + - 5-8 times: 7 points + - 9+ times: 10 points + +- **Function/class reference count**: + - 0-2: 0 points + - 3-5: 3 points + - 6-10: 7 points + - 11+: 10 points + +- **Code snippet length**: + - 0-50 lines: 0 points + - 51-150 lines: 3 points + - 151-300 lines: 7 points + - 301+ lines: 10 points + +- **Answer length**: + - 0-200 chars: 0 points + - 201-500 chars: 3 points + - 501-1000 chars: 7 points + - 1001+ chars: 10 points + +**Drill-down depth classification**: +- **Shallow** (0-15 points): Simple Q&A, minimal source code reference +- **Medium** (16-30 points): Medium complexity, multi-file consultation +- **Deep** (31-50 points): Deep exploration, extensive source code analysis + +#### 2.2 Extract Reusable Knowledge Facts + +Extract following types of knowledge from answer (refer to `f2s-kb-feedback-closing`): + +- Core mechanisms (cache semantics, retry strategy, fallback logic) +- State transitions (state machine, lifecycle) +- Return value / error code contracts +- Configuration switch impacts +- Failure fallback strategies +- Module boundaries or calling conventions +- Data models and field semantics + +**Extraction result**: +- Each knowledge fact includes: type, description, source (file:line) +- Sorted by importance + +#### 2.3 Judge Extracted Knowledge Description Depth + +Evaluate detail level of extracted knowledge facts (judged by content characteristics, not length): + +- **Summary level**: Only conclusive description ("what it is", "what it does"), no conditions/flows/function details + - Example: `Cache-first, fallback to OCR` +- **Detailed level**: Includes mechanism explanation, process steps, key judgment conditions ("when X", "if Y then", "first...then...") + - Example: `Cache-first: use cached coordinates when hit; fallback condition: popup not dismissed, coordinates out of bounds` +- **Implementation level**: Includes function call relationships, state transition details, boundary condition handling, code examples + - Example: `Cache read: call _get_cached_point_in_bounds("chat.input"), fallback when returns None; failure detection: VisualSearchPopup.find(timeout=0.08) is None` + +#### 2.4 Evaluate Existing Topic Description Depth (only when "matched") + +If matched an existing topic, need to evaluate its description depth (judged by content, not length): + +1. **Read target topic content** +2. **Randomly sample 3-5 entries** (from different paragraphs) +3. **Judge each entry's description depth**: + - **Summary level characteristics**: Only says "what it is", "what it does", enumeration style, no conditions/flows/function details + - **Detailed level characteristics**: Contains "when X", "if Y then", "first...then...", judgment conditions, mechanism explanation + - **Implementation level characteristics**: Contains function names `xxx()`, class names, file paths, parameters, code examples, state transition logic +4. **Majority level of entries = overall topic description depth** + +**Judgment examples**: + +| Topic Content | Judgment | Reason | +|--------------|----------|--------| +| `- Cache-first, fallback to OCR`
`- Send message action chain detection` | Summary | Only says "what", no details | +| `- Cache-first: use cached coordinates when hit`
`- Fallback: clear cache and re-OCR when popup not dismissed` | Detailed | Has condition explanation ("when...") | +| `- Cache read: _get_cached_point_in_bounds("chat.input")`
`- Failure detection: VisualSearchPopup.find(timeout=0.08) is None` | Implementation | Has function names, parameters | + +**Important**: A 300+ line topic where every entry is "Module X: responsible for YYY" is still summary level; a 50 line topic where every entry has "conditional judgment + function call" is implementation level. + +### Step 3: Decide Ingestion Strategy + +> ****Light tier** skips the entire decision matrix**: adopt the upstream `f2s-kb-feedback-closing` "this round will ingest: " conclusion directly: +> - Summary contains "append to / supplement / fill in `` section X" → strategy = **append to existing topic**, target topicId = the topic named in the summary; +> - Summary contains "first-time ingestion", "new ``", "new ``" → strategy = **new topic** (default small topic; step 4.3 internally upgrades to "new independent module topic" when drill-down is deep and the module is standalone); +> - Summary contains "fix `` entry X" → strategy = **append to existing topic** (overwrite-style append; the original wording is rewritten during content generation). + +Decide based on following decision matrix: + +| Drill-Down Depth | Matched Topic | Existing Topic Depth | Extracted Knowledge Depth | Strategy | +|-----------------|---------------|---------------------|--------------------------|----------| +| Shallow | Matched | Summary | Summary | **Append to existing topic** (add brief note) | +| Shallow | Matched | Summary/Detailed | Detailed | **Append to existing topic** (add detailed paragraph) | +| Shallow | Matched | Summary | Implementation | **Create sub-topic** (existing too brief, new too detailed) | +| Shallow | Not matched | - | Any | **Create new topic** (small topic) | +| Medium | Matched | Summary | Summary/Detailed | **Append to existing topic** (add detailed paragraph) | +| Medium | Matched | Summary | Implementation | **Create sub-topic** (gap ≥ 2 levels) | +| Medium | Matched | Detailed/Implementation | Detailed/Implementation | **Append to existing topic** (levels match) | +| Medium | Not matched | - | Any | **Create new topic** (medium topic) | +| Deep | Matched | Summary | Any | **Create sub-topic** (independent topic + stock-doc) | +| Deep | Matched | Detailed/Implementation | Detailed/Implementation | **Append to existing topic** or **Create sub-topic** (judge by semantic focus) | +| Deep | Not matched | - | Any | **Create independent module topic** (complete topic + stock-doc) | + +**Decision keys**: +- **Description depth gap ≥ 2 levels** (summary vs implementation) → force create sub-topic, avoid style inconsistency +- **Description depth gap = 1 level** (summary vs detailed, or detailed vs implementation) → can append, but write detailed paragraphs +- **Description depth matches** (same level) → normal append +- **Drill-down depth ≥ deep** → prefer create sub-topic, unless existing topic already very detailed and semantics fully overlap + +**Decision output**: +- Strategy type: `Append to existing topic` / `Create sub-topic` / `Create independent module topic` +- Target topicId: Existing topic id or suggested id for new topic +- Update content: Content to append or structure of new topic +- Description depth match: same level / 1 level gap / ≥ 2 level gap + +### Step 4: Generate Knowledge Content + +> **Authoring guidelines**: This step triggers creation/modification of topics and possible `topicDependencies`, must follow already-read `f2s-topic-authoring` guidelines. + +#### 4.1 Append to Existing Topic + +If strategy is "append to existing topic": + +1. Read current content of target topic +2. Read style samples from 2-3 neighboring topics (for style alignment) + ****Light tier** skips this**: do not read neighboring topics; just match the list / paragraph form already used in the target topic itself. +3. Generate content to append: + - **Position**: Find most relevant paragraph, append after it + - **Format**: Keep list/paragraph style consistent with existing topic + - **Length**: Decide based on knowledge description depth: + - Summary level: 1-3 lines + - Detailed level: 5-10 lines, include mechanism explanation + - Implementation level: 10-20 lines, include process steps and key functions + +#### 4.2 Create Sub-Topic + +If strategy is "create sub-topic": + +1. Generate new topicId (based on parent topic + focus point) +2. Create new topic content: + - Title and one-sentence intent + - Applicable scenarios / trigger words + - Core mechanism details (generated from extracted knowledge facts) + - Dependency declaration (depends on parent topic) + - Boundaries and prohibitions +3. Update parent topic: + - Append link to sub-topic in relevant paragraph + - Explain focus point of sub-topic +4. Update `topicDependencies`: + - Add `sub-topic → parent topic` dependency edge + +#### 4.3 Create Independent Module Topic + +If strategy is "create independent module topic": + +1. Generate new topicId (based on module name or problem domain) +2. Decide whether to create stock-doc: + - Drill-down depth ≥ deep: create stock-doc (`_final.md`) + - Drill-down depth < deep: only create topic, no stock-doc +3. If creating stock-doc: + - Structure: overview, core mechanisms, source files, key functions and flows + - Content: generated from extracted knowledge facts and referenced code snippets + - Length: 100-500 lines depending on drill-down depth +4. Create topic: + - If has stock-doc, topic serves as summary + pointer + - If no stock-doc, topic contains complete mechanism explanation + +### Step 5: Sync Routing and Index + +#### 5.1 Update manifest and matcher + +- If creating new topic: + - Add entry in `manifest-routing.json.topicPaths` + - Create corresponding `matchers/.json`, including: + - Keywords extracted from user question + - Terms extracted from answer + - Suggested `includeAny`: 5-10 trigger words + - Add routing rule in `taskToTopicRules` + +- If updating existing topic: + - Check if matcher needs new trigger words + - Extract uncovered keywords from user question, append to `includeAny` + +#### 5.2 Update index.md + +- If creating new topic: + - Add new entry in `.Knowledge/index.md` + - Format: `- **[topic title](topics/.md)** - one-line description | Related docs: [Final](stock-docs/.md)` (if any) +- If updating existing topic: + - Check if description in index needs update + - If added stock-doc, update "Related docs" column + +#### 5.3 Handle topicMetadata (optional) + +If has clear evidence, write to `topicMetadata`: + +- Judge `primary` type from extracted knowledge facts: + - Core mechanism/state transition/failure fallback → `policy` + - Config switch impact → `config` + - Module boundary/calling convention → `module` + - Implemented capability/business logic → `feature` +- Set `confidence` to `inferred` +- Don't write when no clear evidence, list as "unclassified" in output summary + +### Step 6: Write to Disk and Self-Check + +Write in following order: + +1. If has stock-doc: write to `.Knowledge/stock-docs/.md` +2. Write or update `.Knowledge/topics/.md` +3. Update `.Knowledge/manifest-routing.json` +4. Update `.Knowledge/matchers/.json` +5. Update `.Knowledge/index.md` + +Self-check list: + +1. Does topic content include extracted core knowledge facts +2. Does new topic have corresponding entry in index.md +3. Do topicPaths / taskToTopicRules in manifest reference valid paths +4. Does includeAny in matcher cover keywords from user question +5. If created sub-topic, is topicDependencies correctly set +6. Does appended content maintain style of existing topic (if read neighboring topics) + +## Output Summary Format + +```markdown +## Knowledge Extraction and Ingestion Result + +- Execution tier: `Strict tier (full flow)` / `Light tier (2.1 / 2.4 / 3 / 4.1 skipped)` / `Light tier → strict tier (downgrade reason: )` + +### Q&A Analysis +- User question: +- Matched topic: +- Drill-down depth: () ← **Light tier**: `not evaluated` +- Knowledge description depth: + +### Extracted Knowledge Facts +- [Core Mechanism] (source: ) +- [State Transition] (source: ) +- ... + +### Ingestion Strategy +- Strategy: +- Target topic: +- Operation: + +### Modified Files +- .Knowledge/topics/.md: +- .Knowledge/index.md: +- .Knowledge/manifest-routing.json: +- .Knowledge/matchers/.json: +- .Knowledge/stock-docs/.md: + +### Verification Suggestion +- When encountering similar question "" next time, should match topic: +- Suggested verification trigger words: +``` + +## Constraints + +- Only maintain `.Knowledge`, do not modify config root `rules/skills` +- No user confirmation needed (Q&A already verified knowledge correctness) +- Keep lightweight, single Q&A knowledge extraction complete within 30 seconds +- Avoid over-splitting: unless drill-down depth ≥ deep and knowledge description depth ≥ detailed level, prioritize appending to existing topic +- Generated matcher includeAny should cover expressions users actually use, not just technical terms + +## Self-Check After Completion + +1. Correctly analyzed drill-down depth and knowledge description depth (**Light tier**: correctly parsed the strategy and target topicId from the upstream summary) +2. Extracted all "reusable knowledge facts" (refer to `f2s-kb-feedback-closing` definition) +3. Ingestion strategy conforms to decision matrix (**Light tier**: matches the case named in the upstream summary) +4. New or updated topic has entry in index.md +5. manifest / matcher correctly configured routing rules +6. Generated content maintains style of existing topic (**Light tier**: at least matches the list/paragraph form of the target topic itself) +7. **Light tier specific**: the summary header includes the "Execution tier" line; if downgraded mid-run, the downgrade reason is recorded +8. The reply does NOT append any of `f2s-kb-feedback-closing`'s case 1–4 closing blocks at the end (must be yes; this skill itself is a knowledge-base write, and by the reverse prohibition in `f2s-kb-feedback-closing`'s "Applicable Scope", pasting a distill hint here is forbidden). diff --git a/packages/core/templates/en-US/skills/f2s-kb-feat/SKILL.md b/packages/core/templates/en-US/skills/f2s-kb-feat/SKILL.md new file mode 100644 index 0000000..7f942be --- /dev/null +++ b/packages/core/templates/en-US/skills/f2s-kb-feat/SKILL.md @@ -0,0 +1,115 @@ +--- +name: f2s-kb-feat +description: Complete implementation and knowledge-base sync when adding a capability; if already implemented, only sync the knowledge base; triggers: f2s-kb-feat、新增能力、add capability、new feature +--- + +> **Task paths**: all `.task/` reads/writes must use **`TASK_ROOT` from `rules/f2s-task`** (` .task` or `.task/`; config → git → legacy). Bare `.task/todo.json` / `.task/active/` below mean **`TASK_ROOT/...`**. + + +> Execution scope: `f2s-kb-feat` syncs `.Knowledge` by default; the user does not need to separately ask "please sync the knowledge base". + +## KB Auto-Merge Protocol (Required) + +This skill must not make manual command execution part of the user flow. After the implementation is completed or confirmed to already exist, the agent performs knowledge candidate generation, merge planning, build, and validation by itself: + +1. Convert this capability change into a `kb-delta` draft with `taskId`, `developerId`, `baseRevisions`, `changes`, and implementation evidence. If `changeTracking.feat=true` and a task directory already exists, the delta may be written to `TASK_ROOT/active//kb-delta.json`; otherwise an equivalent in-memory object is acceptable. `changes` may use `appendBody` / `replaceBody` / `updateFrontmatter`; when a new topic is needed, use `createTopic` and optionally include `taskRule` plus `matcher` so routing is connected in the same merge. +2. Before writing `.Knowledge`, run `flow2spec kb plan ` or the equivalent internal capability. If a topic revision differs, stop automatic writing and switch to semantic-merge reporting. +3. When the change is auto-mergeable, run `flow2spec kb apply ` or the equivalent internal capability, then run `flow2spec kb build` and `flow2spec kb check`. +4. The user should only see "capability and knowledge base synced / semantic conflict needs confirmation / skipped with reason"; do not ask the user to manually run `kb plan/apply/build/check`. + +## Orchestration (main / sub-agent) + +- The meaning of `subAgent` and `switchAgentVerification` uses the unified entry as the only source of truth: **Cursor/Claude** read the configuration-root `rules/f2s-flow2spec-unified-entry.*`; **Codex** reads `.codex/topics/f2s-flow2spec-unified-entry.md` (same source, mirrored by `flow2spec init`). Do not repeat those definitions here. +- **Code subpackage** (new / modified implementation code): when `subAgent=true`, it may be delegated to a sub-agent. +- **Documentation subpackage** (style-sensitive changes to rules / skills / topics / stock-docs): by default, do not split; the main agent writes them to preserve writing constraints such as "current truth wins", length limits, and no stacked historical negation. +- If documentation changes must be delegated: the sub-agent only outputs an "in-place replacement diff" (small before / after snippets) and must not rewrite whole files; the main agent merges and writes the result. +- **Write-authority hard rule**: `manifest-routing.json` / `.Knowledge/index.md` are always written by the main agent; sub-agents must not touch them. +- The writing side verifies its own work. + +# Add Capability (f2s-kb-feat) + +## Input + +- The user describes the new capability, scenario, boundaries, and optional paths. + +## Steps + +**Step 0: Change Tracking (only when `changeTracking.feat: true`)** + +Before execution, read `flow2spec.config.json`. If `changeTracking.feat: true`: + +- Check whether `.task/todo.json` has an active task, and match the user description against `keywords`. +- Match -> load the corresponding `task.md`, show the remaining checklist, and continue in the existing task. +- No match -> create a new task (see the `f2s-task` rule), and write steps 1-4 into `task.md` as a task checklist. +- **Mandatory in-progress writes**: every time a step in `task.md` is completed, immediately `Edit` that step from `[ ]` to `[x]` in the same session; do not accumulate checkmarks until the "closing/archive" step, and do not use verbal completion instead of writing to disk (see `f2s-task` "interruption and session end" and "archive gate"). +- **User todos**: whenever the user must change a repository, configure an environment, click a platform, or handle similar items, append them in the same session to `.task/active//user-todos.md` (see `f2s-task`); when creating a new task and there are no todos yet, still create this file (a placeholder is allowed). + +1. Determine capability status: not implemented / partially implemented / already implemented. +2. Complete code implementation (skip this step if already implemented). +3. Sync the knowledge base (default behavior): + - `.Knowledge/stock-docs/`: capability description and usage. + - `.Knowledge/topics/`: add or revise topic rules and workflows. + - `.Knowledge/index.md`: topic index. + - Routing manifest: minimally update it when routing, dependencies, or `topicMetadata` change. + - **Authoring-side guideline**: if this step adds or modifies topics, `topicMetadata`, or `topicDependencies`, first Read the full `rules/f2s-topic-authoring.*` (**Cursor/Claude**: `rules/f2s-topic-authoring.mdc`; **Codex**: `.codex/topics/f2s-topic-authoring.md`) before writing. +4. Output a summary (capability points, implementation, knowledge-base changes). + +## Output Summary Format (Recommended) + +```markdown +## New Capability: + +### Scope +- +- + +### Implementation +- : (if no code changed, write "existing implementation") + +### Knowledge Base +- .Knowledge/stock-docs/.md: +- .Knowledge/topics/.md: +- .Knowledge/index.md: +- .Knowledge/manifest-routing.json: +- .Knowledge/matchers/.json: +``` + +## Complex Scenario Example + +The user asks to "add a failure retry queue capability", and the code already contains a partial implementation. + +- First classify it as "partially implemented" and fill the code gaps instead of rebuilding the whole module. +- Add or revise `topics/retry-queue.md`, and update the `index` entry description. +- If the capability needs to be matched by task routing (for example, "retry queue refactor"), supplement `manifest.taskToTopicRules`. + +## Constraints + +- When there is a conflict with an old convention: **rewrite to the current truth**; do not create additional historical-negation sentences such as "(no longer related to X)". +- Prefer in-place updates for overlapping existing topics. +- At least one knowledge-base update must be written, avoiding "code exists but cannot be retrieved". +- Do not modify the configuration-root `rules/skills`. +- Documentation subpackages are not split by default; when delegation is necessary, the sub-agent only outputs before/after diff snippets, and the main agent merges and writes them. `manifest-routing.json` / `.Knowledge/index.md` are always written by the main agent (write-authority hard rule). + +## Knowledge-Base Writing Style (Required, Anti-Redundancy) + +When writing `stock-docs` / `topics` / `index`, follow these rules: + +1. **Minimal increment**: only append or rewrite text directly related to **this capability**; do not use "sync the knowledge base" as a reason to restate background, requirements, or tutorial-style setup unrelated to the implementation. +2. **Affirmative wording first (see the unified entry "knowledge-base writing style")**: state the correct description directly; do not communicate the new convention by negating the old one, except for mutually exclusive choices. +3. **Avoid duplicate narration**: do not write a long version of the same fact in both `stock-docs` and `topics`; make the executable convention clear in one place, and use a short paragraph + link in the other, or only list key points and reference paths. +4. **Prefer structured writing**: `topics` should focus on rules, boundaries, steps, errors, and configuration points; use lists/tables instead of long paragraphs when possible. +5. **Length limit (soft constraint)**: in one sync, new body text added to the **same file** should generally not exceed about **80 lines** (excluding code-block lines); if it exceeds that, split into a new topic or write "summary + see code path / another doc" first. Do not stack repeated explanation in one file. +6. **`index.md`**: only modify rows/table items related to this topic; do not refresh the whole table or section by copy-paste. +7. **Forbidden**: repeatedly explain Flow2Spec directory responsibilities, paste the full user conversation, or add long "historical review" sections unrelated to this diff. + +## Completion Self-Check + +1. Capability description matches the code implementation. +2. The new capability can be retrieved through a topic. +3. `index` and `manifest` were synced. +4. If `topicMetadata` was written: every key exists in `topicPaths`; `primary` / `tags` / `confidence` are valid; no topic was created, renamed, or split just for classification. +5. Knowledge-base changes cannot be compressed further without losing the rules and links after removing unrelated boilerplate. +6. No "negate old version / no longer related to something" redundant phrasing remains; if the current rule is clear, such sentences should be deleted or moved into a user-requested migration section. +7. No sub-agent rewrote documentation whole-file; manifest / index were written by the main agent only. +8. If `changeTracking.feat: true`: only archive `.task/active//` to `completed/` and remove the corresponding `todo.json` entry after all `task.md` "steps" are `[x]` (or canceled items are noted); do not move the directory while `[ ]` remains (same as the `f2s-task` archive gate). +9. If `changeTracking.feat: true`: `user-todos.md` exists; when user todos exist, its content matches the session conclusion. diff --git a/packages/core/templates/en-US/skills/f2s-kb-fix/SKILL.md b/packages/core/templates/en-US/skills/f2s-kb-fix/SKILL.md new file mode 100644 index 0000000..9d1b942 --- /dev/null +++ b/packages/core/templates/en-US/skills/f2s-kb-fix/SKILL.md @@ -0,0 +1,112 @@ +--- +name: f2s-kb-fix +description: Fix implementation or rule errors identified by the user, and sync the knowledge base by default; triggers: f2s-kb-fix、修正实现规则、fix implementation rules、fix kb rule +--- + +> **Task paths**: all `.task/` reads/writes must use **`TASK_ROOT` from `rules/f2s-task`** (` .task` or `.task/`; config → git → legacy). Bare `.task/todo.json` / `.task/active/` below mean **`TASK_ROOT/...`**. + + +> Execution scope: `f2s-kb-fix` defaults to "fix code + sync `.Knowledge`"; the user does not need to separately ask "please sync the knowledge base". + +## KB Auto-Merge Protocol (Required) + +This skill must not make manual command execution part of the user flow. After the fix is completed, the agent performs knowledge candidate generation, merge planning, build, and validation by itself: + +1. Convert the corrected rule or implementation boundary into a `kb-delta` draft with `taskId`, `developerId`, `baseRevisions`, `changes`, and fix evidence. If `changeTracking.fix=true` and a task directory already exists, the delta may be written to `TASK_ROOT/active//kb-delta.json`; otherwise an equivalent in-memory object is acceptable. `changes` may use `appendBody` / `replaceBody` / `updateFrontmatter`; when a new topic is needed, use `createTopic` and optionally include `taskRule` plus `matcher` so routing is connected in the same merge. +2. Before writing `.Knowledge`, run `flow2spec kb plan ` or the equivalent internal capability. If a topic revision differs, stop automatic writing and switch to semantic-merge reporting. +3. When the change is auto-mergeable, run `flow2spec kb apply ` or the equivalent internal capability, then run `flow2spec kb build` and `flow2spec kb check`. +4. The user should only see "fix and knowledge base synced / semantic conflict needs confirmation / skipped with reason"; do not ask the user to manually run `kb plan/apply/build/check`. + +## Orchestration (main / sub-agent) + +- The meaning of `subAgent` / `switchAgentVerification` uses the unified entry as the only source of truth: **Cursor/Claude** read the configuration-root `rules/f2s-flow2spec-unified-entry.*`; **Codex** reads `.codex/topics/f2s-flow2spec-unified-entry.md` (same source, mirrored by `flow2spec init`). Do not repeat those definitions here. +- Code subpackage (bug-fix implementation code): when `subAgent=true`, it may be delegated to a sub-agent. +- Documentation subpackage (style-sensitive changes to rules / skills / topics / stock-docs): by default, do not split; the main agent writes them directly to preserve writing constraints such as "current truth wins", length limits, and no stacked historical negation. +- If documentation changes must be delegated, the sub-agent **only outputs an in-place replacement diff** (small before / after snippets) and **must not rewrite whole files**; the main agent merges and writes the result. +- Write-authority hard rule: `manifest-routing.json` / `.Knowledge/index.md` are always written by the main agent; sub-agents must not touch them. +- The writing side verifies its own work. + +# Fix Capability (f2s-kb-fix) + +## Input + +- The user describes the violation, the correct behavior, and an optional scope. + +## Steps + +**Step 0: Change Tracking (only when `changeTracking.fix: true`)** + +Before execution, read `flow2spec.config.json`. If `changeTracking.fix: true`: + +- Check whether `.task/todo.json` has an active task, and match the user description against `keywords`. +- Match -> load the corresponding `task.md`, show the remaining checklist, and continue in the existing task. +- No match -> create a new task (see the `f2s-task` rule), and write steps 1-4 into `task.md` as a task checklist. +- **Mandatory in-progress writes**: every time a step in `task.md` is completed, immediately `Edit` that step from `[ ]` to `[x]` in the same session; do not accumulate checkmarks or use verbal completion instead of writing to disk (see `f2s-task` "interruption and session end" and "archive gate"). +- **User todos**: whenever the user must change a repository, configure an environment, perform regression verification, or handle similar items, append them in the same session to `.task/active//user-todos.md` (see `f2s-task`); when creating a new task and there are no todos, write a placeholder note. + +1. Clarify the violation and impact scope (ask follow-up questions first if unclear). +2. Fix the code implementation. +3. Sync the knowledge base (default behavior): + - `.Knowledge/stock-docs/`: revise convention descriptions. + - `.Knowledge/topics/`: revise the corresponding topic rules / workflows. + - `.Knowledge/index.md`: update the topic index. + - Routing manifest: minimally update it if routing, dependencies, or `topicMetadata` are affected. + - **Authoring-side guideline**: if this step adds or modifies topics, `topicMetadata`, or `topicDependencies`, first Read the full `rules/f2s-topic-authoring.*` (**Cursor/Claude**: `rules/f2s-topic-authoring.mdc`; **Codex**: `.codex/topics/f2s-topic-authoring.md`) before writing. +4. Output a summary (code changes + knowledge-base changes). + +## Output Summary Format (Recommended) + +```markdown +## Fix Result: + +### Code +- : + +### Knowledge Base +- .Knowledge/stock-docs/.md: +- .Knowledge/topics/.md: +- .Knowledge/index.md: +- .Knowledge/manifest-routing.json: +- .Knowledge/matchers/.json: +``` + +## Complex Scenario Example + +The user points out that an idempotency implementation in a callback interface is wrong, but does not provide a clear file scope. + +- First fix the already located callback-processing path with the smallest viable scope, and state in the summary that "similar fixes can be extended across the repository". +- Sync the idempotency-rule paragraph in `topics` to avoid regenerating the same wrong implementation later. +- If this fix affects task routing (for example, by adding an "idempotency fix" topic), then minimally update `manifest`. + +## Constraints + +- When there is a conflict with an old convention: **rewrite to the current truth**; do not stack contrastive legacy phrases such as "(no longer related to X)". +- Prefer in-place updates for the same topic. +- If the scope is unclear, fix the smallest viable scope and explain it. +- Do not modify the configuration-root `rules/skills`. +- Documentation subpackages are not split by default; when delegation is necessary, the sub-agent only outputs before/after diff snippets, and the main agent merges and writes them. `manifest-routing.json` / `.Knowledge/index.md` are always written by the main agent (write-authority hard rule). + +## Knowledge-Base Writing Style (Required, Anti-Redundancy) + +When writing `stock-docs` / `topics` / `index`, follow these rules: + +1. **Minimal increment**: only change paragraphs or list items directly related to **this fix**; do not use the sync as an excuse to restate the whole design, historical background, or unrelated explanations. +2. **Affirmative wording first (see the unified entry "knowledge-base writing style")**: state the correct behavior directly; do not communicate the new convention by negating the old one, except for mutually exclusive choices. +3. **Avoid duplicate narration**: do not write long explanations of the same fix in both `stock-docs` and `topics`; write the "cause / correct convention / notes" clearly in one place, and use a short reference or link in the other. +4. **Prefer structured bullets**: use short lists ordered as "symptom -> root cause -> correct behavior / boundary" instead of prose-heavy expansion. +5. **Length limit (soft constraint)**: in one sync, new or replaced text in the **same file** should generally not exceed about **60 lines** (excluding code-block lines); if it does, keep only the minimal explanation relevant to the fix and replace the rest with "see commit / see path". +6. **`index.md`**: update only affected index rows or summary columns; do not rewrite unrelated tables. +7. **Forbidden**: repeatedly paste the user's full error text (one identifying line + link is enough), or repeatedly explain how Flow2Spec works. + +## Completion Self-Check + +1. The code fix covers the scope named by the user. +2. Topic documents match the fixed implementation. +3. `index` points to the correct topic. +4. If `manifest` was updated, routing fields are still parseable. +5. If `topicMetadata` was written: every key exists in `topicPaths`; `primary` / `tags` / `confidence` are valid; no topic was created, renamed, or split just for classification. +6. Knowledge-base changes can no longer be compressed without losing the convention after removing boilerplate. +7. No redundant "negate old version / no longer related to something" phrasing remains; if the current rule is clear, such phrasing should be removed. +8. No sub-agent rewrote documentation whole-file; manifest / index were written by the main agent only. +9. If `changeTracking.fix: true`: only archive `.task/active//` to `completed/` and remove the corresponding `todo.json` entry after all `task.md` "steps" are `[x]` (or canceled items are noted); do not move the directory while `[ ]` remains (same as the `f2s-task` archive gate). +10. If `changeTracking.fix: true`: `user-todos.md` exists; when user todos exist, its content matches the session conclusion. diff --git a/packages/core/templates/en-US/skills/f2s-kb-merge/SKILL.md b/packages/core/templates/en-US/skills/f2s-kb-merge/SKILL.md new file mode 100644 index 0000000..c86d4c9 --- /dev/null +++ b/packages/core/templates/en-US/skills/f2s-kb-merge/SKILL.md @@ -0,0 +1,80 @@ +--- +name: f2s-kb-merge +description: Resolve editor-context conflicts after a Git merge; optionally accept conflict files; implementation-side conflicts are only summarized for user confirmation; triggers: 合并上下文冲突、f2s-kb-merge、merge context conflicts、resolve kb merge +--- + +## Orchestration (main / sub-agent) + +- The meaning of `subAgent` / `switchAgentVerification` uses the unified entry as the only source of truth: **Cursor/Claude** read the configuration-root `rules/f2s-flow2spec-unified-entry.*`; **Codex** reads `.codex/topics/f2s-flow2spec-unified-entry.md` (same source, mirrored by `flow2spec init`). This skill does not repeat those definitions. +- **Sub-agent responsibility** (only when `subAgent=true`): perform only **conflict scanning + categorized comparison table**. Each entry contains five fields: `file` / `category` (document index / overview rule / module rule / skill / explanatory document / implementation / dependency metadata) / `ours_summary` / `theirs_summary` / `recommendation` (union / keep one side / merge mandatory items / needs user choice). +- **Sub-agents do not produce finished merged drafts**, to avoid forcing the main agent to rewrite them again. +- **Main-agent responsibility**: write files according to the strategy, make implementation-side decisions, and verify. +- By default, the writing side verifies its own work; this skill does not bind cross-agent verification. + +# Resolve Context Merge Conflicts (f2s-kb-merge) + +When `<<<<<<<` / `=======` / `>>>>>>>` appears after **rebase / merge**, first automatically merge files related to **AI and developer context** so indexes, rules, skills, and explanatory docs stay aligned. Conflicts involving executable implementation, deployment, or dependency declarations must **not** be merged without authorization; show the differences between both sides and wait for user confirmation before editing. + +## Arguments (Optional) + +- **No arguments**: search the workspace for files that still contain conflict markers, then classify and process them according to this skill (including a summary after the full scan). +- **With arguments**: the user may specify **one or more files that still contain conflicts** (via attached files or listed paths). The assistant **prioritizes only those files**. If any specified file falls into a "do not auto-merge" category, only list the differences and recommendation; **do not write changes without permission**. After the specified files are handled, you may ask whether the user wants an additional workspace scan. + +## Applicable Scope (Auto-Merge Allowed) + +Conflicts in the following **categories** are handled by this skill's **merge strategy** and do **not** require line-by-line confirmation unless the two sides are **mutually exclusive** and the correct result cannot be determined. + +| Category | Description | +| --- | --- | +| Document index | Index-table files carrying "document <-> rule / skill" mappings | +| Project overview rule | Main entry files in the rules directory | +| Module rule | Other rule fragments in the same rule directory | +| Skill | SKILL documentation files under the skills directory | +| Context explanatory document | Markdown docs paired with rules and skills | +| Index-linked pure explanatory document | Conventionally stored docs that are referenced only by indexes or rules and **do not contain executable implementation semantics** | + +## Auto-Merge Forbidden (User Confirmation Required) + +The following conflicts **must not** be merged before the user makes an explicit choice: + +- **Application or service implementation source code** (business logic, interface implementation, data access, etc.). +- Configuration that **changes externally exposed behavior** (routes, function registration, middleware chains, runtime entry points, etc.). +- **Dependency and build metadata** (dependency declarations, lockfiles, build and deployment scripts, etc.). +- **Implementation modules that centrally maintain external-resource inventories**: if the two sides have different **item sets or registration content**, this is a runtime-behavior difference. The user must confirm the kept scope; the assistant may recommend "union + dedupe", but writes only **after user approval**. + +**Handling method**: list conflict files, briefly summarize each side's intent, provide a recommendation, and **ask the user to choose** before modifying files in the above scope. + +## Merge Strategy (Context Files) + +1. **Remove all** Git conflict markers (`<<<<<<<` / `=======` / `>>>>>>>`); none may remain. +2. **Index tables** + - For **Rules / Skills / link columns** in the same index row: take the **union**, dedupe paths, and separate with spaces. + - For **independent index rows** that appear only on one side: **keep** them after the merge to avoid losing entries. +3. **Overview rules** + - Multiple bullets under the same topic should be merged into **one complete bullet or several parallel bullets**; do **not discard** constraints or references unique to either side. +4. **Tables in long documents** + - Rows describing **different capability dimensions**: keep the **union**. + - Duplicate rows describing **the same topic**: merge into **one** coherent row covering both sides' points. +5. **rules / skills** + - Prefer wording that is **more specific and clearer in constraints**; merge any unique **must / must not** clauses from the other side to avoid rule regression. +6. **Links and paths** + - Normalize to repository-resolvable relative paths, consistent with index entries in the overview rule. + +## Execution Steps + +1. **Determine scope**: if the user specified conflict files, use only those files; otherwise scan the full workspace for conflict markers (or combine with the IDE conflict list). Then classify according to **applicable scope**. + - If sub-agent splitting is enabled, the sub-agent outputs the categorized comparison table using the schema `file` / `category` / `ours_summary` / `theirs_summary` / `recommendation`; the main agent takes over writing / decisions / verification. +2. **Context files**: edit and save directly using the merge strategy. +3. **Implementation files**: only output comparison summaries and recommendations; **do not modify files** until the user confirms. +4. **Output summary** (Markdown): resolved files + key points; pending files + differences + recommendations. +5. Confirm again that **processed files** contain **no** conflict markers. If a full scan was not performed, optionally ask whether to run an additional scan. + +## Relationship to Related Commands + +- **`/修正实现规则` (`f2s-kb-fix`)**: targeted correction and documentation/rule sync after the user has identified a problem. +- **This skill**: batch conflicts caused by a merge, focused on separating **editor context and explanatory docs** from **implementation-side** files. + +## When to Use + +- After merge / rebase, **rules / skills / indexes / paired explanatory docs** have conflicts (can handle all conflicts or only user-specified conflict files). +- You need to align "index <-> rule <-> skill <-> explanatory doc" in one pass while **avoiding accidental merges of implementation or deployment changes**. diff --git a/packages/core/templates/en-US/skills/f2s-kb-migrate/SKILL.md b/packages/core/templates/en-US/skills/f2s-kb-migrate/SKILL.md new file mode 100644 index 0000000..209d827 --- /dev/null +++ b/packages/core/templates/en-US/skills/f2s-kb-migrate/SKILL.md @@ -0,0 +1,358 @@ +--- +name: f2s-kb-migrate +description: Migrate a legacy knowledge base to `.Knowledge` in one pass: use the configuration-root `docs-index.md` plus the unified rule entry (legacy `rules/main.md(c)` or current package `rules/f2s-flow2spec-unified-entry.md(c)`) as primary index clues; fully process business `rules/` and business `skills/` (excluding `f2s-*` package skills), and fully migrate `stock-docs`/`req-docs`; **after migration acceptance, must write** `.Knowledge/migration-report.md` (migration mapping table + proposed deletion path list); **closing must delete** migrated legacy `rules/`, migrated business `skills/`, and legacy `docs-index.md`/`index-doc.md`; the user only **reviews/revises the deletion list (exclusions)**; triggers: f2s-kb-migrate、知识库迁移、旧版迁移、knowledge-base migration、legacy migration +--- + +> Execution scope: this is an `f2s-*` skill workflow, not a CLI subcommand. Migration targets include: +> 1) Structure layer: `.Knowledge/topics`, `.Knowledge/index.md`, `.Knowledge/manifest-routing.json`, `.Knowledge/matchers/*.json` +> 2) Document layer: `.Knowledge/stock-docs`, `.Knowledge/req-docs` +> +> **Hard boundary**: `skills/f2s-*` (under each agent configuration root) are Flow2Spec package skills / execution-layer capabilities. They **must not** be written into `.Knowledge` (including `topics/stock-docs/req-docs`) and must not be used as sources for "business skill migration". They also **must not** be deleted in this workflow (version alignment is handled by `flow2spec init` / package upgrade). +> +> **Baseline rule keep-list (must not delete)**: `rules/f2s-flow2spec-unified-entry.md(c)`, `rules/f2s-implement-tech-design.md(c)`, `rules/f2s-stock-docs-vs-req-docs.md(c)`. + +## Orchestration (main / sub-agent) + +- The meaning of `subAgent` / `switchAgentVerification` uses the unified entry as the only source of truth: **Cursor/Claude** read the configuration-root `rules/f2s-flow2spec-unified-entry.*`; **Codex** reads `.codex/topics/f2s-flow2spec-unified-entry.md` (same source, mirrored by `flow2spec init`). This section does not repeat those definitions. +- **Sub-agent responsibility** (only when `subAgent=true`): under the main agent's given inventory, perform moving work and generate **draft fragments** for `migration-report.md`; all outputs are submitted as patches and merged/written by the main agent. +- **Main agent must control**: + - `.Knowledge/.migrate-state.json` **write authority belongs to main** (state-machine source of truth; concurrent main/sub writes can misalign queues). + - The **"Deletion execution record"** section of `migration-report.md` is always appended by the main agent. + - **Deletion-list confirmation** and closed-loop cleanup must be completed by the main agent. +- **Write-authority hard rule**: `manifest-routing.json` / `.Knowledge/index.md` / `.Knowledge/.migrate-state.json` / the migration report "Deletion execution record" are always written by the main agent. +- By default, the writing side verifies its own work; this SKILL does not bind cross-agent verification. + +# f2s-kb-migrate (Legacy Knowledge Base -> New Knowledge Base) + +## Why this coexists with `f2s-kb-upgrade` + +| Skill | Problem solved | +| --- | --- | +| **This skill `f2s-kb-migrate`** | **One-time structural move**: legacy indexes (`docs-index.md` / `index-doc.md`), `rules/main.md(c)`, business `skills/`, scattered `stock-docs`/`req-docs` -> **`.Knowledge`**, plus deletion list and `migration-report.md`. | +| **`f2s-kb-upgrade`** | **Knowledge-base template upgrade skill (the only "upgrade" meaning)**: execute the full **`skills/f2s-kb-upgrade/SKILL.md`** workflow. It runs **`flow2spec init`** inside the process to align **`manifest-routing` + `matchers/`** and each agent's **`rules`/`skills`**. It includes **V1 / current repository (V2+)** branching (legacy projects must **migrate first, then run this skill**; **V2+ includes npm v3.x and other projects already using `.Knowledge`**, see `f2s-kb-upgrade` step 0). | + +- **After migration acceptance and deletion-list confirmation are complete**: remind the user to execute, or execute for them, the **full `f2s-kb-upgrade` skill** (whose **step 2** runs **`flow2spec init`**) to align the Flow2Spec package version, routing shards, and configuration-root artifacts to the current package. **Do not** let the user think that running `init` alone completes knowledge-base template upgrade. +- **Projects already stably using `.Knowledge` with no legacy-index burden**: do not run this skill again; daily package/template alignment uses **`f2s-kb-upgrade`** only (not just `init`). + +**Why does each agent directory have a same-named `SKILL.md`?** Each tool reads only its own configuration-root `skills/`; `flow2spec init` **syncs** the current-language skill content into the selected agent directories. + +## What This Command Does (External Wording) + +Move the legacy "scattered configuration-root document index + rules + business skills + stock/req document trees" **as a whole into the new `.Knowledge`**, then perform **legacy entry and legacy business-artifact cleanup**, cutting over from the old knowledge-base organization. + +Objects that must be covered: + +1. **Index entry**: business docs and rule clues declared/mapped in configuration-root `docs-index.md` (compatible with `index-doc.md`). +2. **Rule entry**: the rule set declared/referenced in `rules/main.md` / `rules/main.mdc` (common legacy form) or `rules/f2s-flow2spec-unified-entry.md` / `rules/f2s-flow2spec-unified-entry.mdc` (compatible with historical `rules/flow2spec-unified-entry.md(c)`), plus other business rule files under `rules/`. +3. **Business skills**: business skill directories under each agent configuration-root `skills/`, excluding `f2s-*` (full inventory). +4. **Document trees**: legacy `stock-docs/`, `req-docs/` (or synonymous directories) fully migrated into the corresponding `.Knowledge` directories. + +For objects **not covered by the index**: + +- First output a candidate list (path + inferred reason: naming/directory/reference relationship). +- **By default, user confirmation is required** before including them in migration. Only when evidence is very strong (for example explicitly referenced by `rules/main` / `f2s-flow2spec-unified-entry`, or clearly referenced by an indexed document) may the Agent decide to include them, and the basis must be written in the migration summary. + +Cleanup after migration (mandatory closing; only when migration has no failures and no pending confirmations; **`skills/f2s-*` are never deleted**): + +- **Must execute**: delete migrated business rule files in legacy **`rules/`** (including `main.md(c)` if it is only a legacy entry), but **must not delete** the three `f2s-*` root rule files in the baseline keep-list. +- **Must execute**: delete migrated **business** subdirectories under legacy **`skills/`** (**excluding** `f2s-*`; if a directory still has unmigrated items, do not delete it until completed or removed from the list). +- **Must execute**: delete legacy entry **`docs-index.md`** (compatible with **`index-doc.md`**) to avoid dual entry points with `.Knowledge/index.md`. +- **Default optional deletion sub-list** (the user may exclude): legacy **`stock-docs/`** and **`req-docs/`** source directories, only when the corresponding document-layer migration has passed acceptance with no failures and no pending confirmations. + +**Meaning of user confirmation (important)**: + +- This is **not** asking "whether to clean up"; cleanup is part of the workflow. +- Instead, output the default-selected **"deletion path list"** (rule files one by one, business skill directories one by one, index filenames, and optional legacy document root directories) and ask the user to **review**. The user can only: + - Reply "**确认清单**" to delete according to the current list; or + - Reply "**排除:<路径…>**" to remove specified items from the list before deletion (removed items must be written to `.migrate-state.json` `notes[]` with reasons). +- If the user asks to **defer deleting a path**, keep that item in the list, end the cleanup round with `status=paused`, and **do not** pretend migration closure is complete. + +## Applicable Scenarios + +- The project still uses legacy knowledge organization (`docs-index.md` / `index-doc.md` + `rules/main.md(c)` or `rules/f2s-flow2spec-unified-entry.md(c)` (compatible with old `flow2spec-unified-entry.md(c)`) + business `skills/` + scattered `stock-docs`/`req-docs`). +- The user wants to migrate to the new `.Knowledge` format and confirm topic by topic to avoid one-shot large changes. +- The user needs **all req-docs / stock-docs** migrated into `.Knowledge` and wants to cut over from legacy knowledge-base directories/wording (paths, index, topic text unified to the new architecture). + +## Input + +- Optional inputs: + - Legacy unified rule entry path: `rules/main.md` / `rules/main.mdc` and/or `rules/f2s-flow2spec-unified-entry.md` / `rules/f2s-flow2spec-unified-entry.mdc` (compatible with old `rules/flow2spec-unified-entry.md(c)`) + - Legacy `index-doc.md` (or `docs-index.md`) path + - Legacy stock document directory (for example `stock-docs/`, `docs/stock/`) + - Legacy requirement document directory (for example `req-docs/`, `docs/req/`) + - Migration scope (all topics / specified topics) +- If not provided, locate the above files in the repository first and ask the user to confirm. + +## Resumable Migration State File (Required) + +- State file path: `.Knowledge/.migrate-state.json` +- Purpose: record migration progress and support recovery after session interruption without migrating completed items again. +- Initialization timing: create immediately after the user confirms "start migration". +- Ending timing: + - All migration complete and user confirms completion: delete the state file. + - User actively says "stop": keep the state file for later recovery. +- `.migrate-state.json` is written only by the main agent; sub-agents submit patch fragments for the main agent to merge (write-authority hard rule). + +Recommended fields (minimal set): + +```json +{ + "version": "1", + "status": "running", + "currentStage": "inventory|orphans|topics|stock-docs|req-docs|cleanup", + "topicQueue": [], + "topicDone": [], + "bizRuleQueue": [], + "bizRuleDone": [], + "bizSkillQueue": [], + "bizSkillDone": [], + "stockQueue": [], + "stockDone": [], + "reqQueue": [], + "reqDone": [], + "pendingManual": [], + "failed": [], + "notes": [], + "updatedAt": "ISO-8601" +} +``` + +Update rules (required): + +1. After completing each topic, business skill directory, business rule file, or document file, immediately write the state-file update. +2. When receiving "重试 ", roll back that item's state before retrying. +3. When receiving "继续", read the state file first and continue from unfinished queues. +4. When receiving "停止", write `status=paused` and end this round. +5. When receiving a resume request, first show a state summary (current stage, remaining counts, failed/pending items) and wait for user confirmation to continue. + +## Mandatory Flow (Phased Execution) + +### Step 1: Read Legacy Mappings + +1. Read `docs-index.md` (compatible with `index-doc.md`) and extract "business document -> rule/topic" mappings (**primary index**). +2. Read **`rules/main.md` (compatible with `main.mdc`)** or **`rules/f2s-flow2spec-unified-entry.md` (compatible with `f2s-flow2spec-unified-entry.mdc`; compatible with old `flow2spec-unified-entry.md(c)`)** (usually only one exists), and extract module/topic directory clues (**cross-check with the index**). +3. **Full inventory of business rule files**: scan files under `rules/` except the following, and build `bizRuleQueue` (deduped): + - Unified entry: `main.md(c)`, `f2s-flow2spec-unified-entry.md(c)`, `flow2spec-unified-entry.md(c)` (compatible with old name) + - Baseline keep: `f2s-implement-tech-design.md(c)`, `f2s-stock-docs-vs-req-docs.md(c)` +4. **Full inventory of business skills**: scan each agent configuration-root `skills/` directory; **exclude** `f2s-*`; all other directories enter `bizSkillQueue` (deduped). +5. Scan legacy `stock-docs` and `req-docs` candidate source directories if they exist. +6. Generate the migration inventory and show it to the user for confirmation: + - Topic list (deduped, sorted) + - Business rule file list (`bizRuleQueue`) + - Business skill directory list (`bizSkillQueue`) + - `stock-docs` file list + - `req-docs` file list +7. Document classification rules (must be explicit): + - Source path matches `stock-docs` (including synonyms such as `docs/stock`) -> migrate to `.Knowledge/stock-docs` + - Source path matches `req-docs` (including synonyms such as `docs/req`) -> migrate to `.Knowledge/req-docs` + - Unclassifiable files -> put into "manual confirmation list"; do not migrate before confirmation +8. Compute "out-of-index candidates" (`orphans`): + - Files in `bizRuleQueue` not covered by `docs-index` / unified entry (`rules/main` or `f2s-flow2spec-unified-entry`) + - Directories in `bizSkillQueue` not covered by index mappings + - By default, require user confirmation for every item; only in high-confidence reference scenarios may the Agent include it autonomously, and the basis must be appended to state-file `notes[]` (without breaking JSON parseability). +9. After the user confirms the inventory, initialize the state file and write queues (inventory/orphans/topics/stock/req). + +### Step 2: Migrate Topic by Topic (Structure-Layer Core) + +For each topic, execute in this order: + +1. Collect legacy materials for the topic: + - Related `rules/*.md(c)` (business rules) + - Related **business** `skills/` (merge their content into the topic narrative/workflow; do not copy them as skill files under `.Knowledge`) + - **Business document** paths in index mappings + - **Must not** include any file under `skills/f2s-*` +2. Generate or update `.Knowledge/topics/.md`: + - Body text uses the new architecture vocabulary (`.Knowledge` layering, `manifest` routing, `stock-docs`/`req-docs` responsibilities). + - Remove legacy-only paths/terms (such as old `docs-index` root paths or scattered legacy directory names) and replace them with `.Knowledge/...` or stable paths relative to `.Knowledge`. + - **Authoring-side guideline**: if this step generates/rewrites a topic or adjusts `topicMetadata` / `topicDependencies`, first Read the full `rules/f2s-topic-authoring.*` (**Cursor/Claude**: `rules/f2s-topic-authoring.mdc`; **Codex**: `.codex/topics/f2s-topic-authoring.md`) before writing. +3. Update the topic index row in `.Knowledge/index.md`, and maintain the "Associated documents (summary)" column (1-3 key `stock-docs/req-docs` **clickable Markdown links** per topic, format: `[title](relative path)`). +4. Update the routing manifest as needed: + - `.Knowledge/manifest-routing.json`: `topicPaths`, `taskToTopicRules[]`, `topicDependencies`, `topicMetadata`, `fallbackTopic` + - `.Knowledge/matchers/.json`: `includeAny` (consistent with `manifest-routing.taskToTopicRules[].matcherPath`) +5. Output this topic's migration summary and **pause**, prompting the user: + - Reply "继续" to migrate the next topic + - Or reply "停止" to stop this round + - Or reply "重试 " to redo the current topic + +> Before receiving "继续", do not migrate the next topic. +> After completing each topic, update the state file before waiting. + +### Step 3: Migrate `stock-docs` (Document Layer) + +After step 2 completes, execute: + +1. Migrate into `.Knowledge/stock-docs/` according to "source-directory relative path"; do not flatten. +2. Default scenario is first migration from a legacy repo into the new knowledge base, so target paths are treated as "not existing". +3. After each file migration, output a result and pause, waiting for "继续 / 停止 / 重试 <文件>". +4. After all files complete, output a `stock-docs` sub-summary (success/failure/pending confirmation). + +> Before receiving "继续", do not migrate the next file. +> After completing each file, update the state file before waiting. + +### Step 4: Migrate `req-docs` (Document Layer) + +After the `stock-docs` phase completes, execute: + +1. Migrate into `.Knowledge/req-docs/` according to "source-directory relative path"; do not flatten. +2. Default scenario is first migration from a legacy repo into the new knowledge base, so target paths are treated as "not existing". +3. After each file migration, output a result and pause, waiting for "继续 / 停止 / 重试 <文件>". +4. After all files complete, output a `req-docs` sub-summary (success/failure/pending confirmation). + +> Before receiving "继续", do not migrate the next file. +> After completing each file, update the state file before waiting. + +### Step 5: Closing After All Migration Completes (Required: Migration Report + Deletion-List Confirmation) + +When topic migration (step 2) and document-layer `stock-docs` / `req-docs` migration (steps 3-4) have **all passed acceptance** (no failures and no blocking pending confirmations, or pending items are separately listed in the report), execute the following substeps in order. + +#### 5.0 Migration Report (Required: Write Project Markdown) + +1. **Must** create or overwrite this file in the project repository: **`.Knowledge/migration-report.md`** (relative to project root; same repo as `.Knowledge`, convenient for review and traceability). +2. The report body must contain at least two major blocks (tables or nested lists are allowed; all paths use POSIX style relative to project root): + - **"Migration mapping table"**: + - **Topics**: each migrated `topic` -> legacy sources (corresponding `rules/*.md(c)`, business `skills/`, `docs-index` mapping-line summary) -> new path `.Knowledge/topics/.md`; also indicate whether `.Knowledge/index.md` / routing-manifest fields were modified. + - **`stock-docs`**: every **source path -> `.Knowledge/stock-docs/...` target path** (include skipped files and reasons; write "none" if none). + - **`req-docs`**: same as above. + - **"Proposed deletion path list"**: exactly consistent with the **default-selected deletion list** shown to the user in step 5.2 (each file under `rules/`, each business `skills/` directory to delete, `docs-index`/`index-doc`, and optionally legacy `stock-docs/`/`req-docs/` roots). Prefer `- [ ] ` for each item so humans can review/check. +3. If the user later sends **"排除:<路径…>"** in step 5.2, update the same file **before physical deletion**: append or write in a "User exclusions" section the excluded paths and reasons, and sync the "Proposed deletion path list" checkbox state or list so the report on disk matches the final deletion set. +4. After physical deletion is executed according to the final list in step 5.2 step 3, append a **`## Deletion Execution Record`** section at the **end of the same file** (include execution time and actual deleted paths; for undeleted items, state reason and `status=paused`, etc.). Do not leave this only in the conversation. +5. The migration report "Deletion execution record" section is always appended by the main agent; sub-agents must not write it directly (write-authority hard rule). + +> **Forbidden**: entering physical deletion or ending the migration closure before `.Knowledge/migration-report.md` is written. + +#### 5.1 Overall Summary (In Conversation, May Match Report Summary) + +- Migrated topic list +- New/updated `.Knowledge` files +- Migrated `stock-docs` files +- Migrated `req-docs` files +- Unmigrated or failed items + +#### 5.2 Required Cleanup Phase (Deletion-List Confirmation; Must Not Skip) + +1. Output the default-selected **"deletion path list"** (same source as the "Proposed deletion path list" in `migration-report.md`), including at least: + - Every **business rule** file path under legacy **`rules/`** to delete (may include `main.md(c)`; **must not include** the baseline `f2s-*` root rules) + - Every subdirectory path under legacy **business** `skills/` to delete (**excluding** `f2s-*`) + - Legacy **`docs-index.md` / `index-doc.md`** + - (Optional sub-list) legacy **`stock-docs/`** and **`req-docs/`** root directories, only when document migration has passed acceptance and no pending items remain; the user may exclude them. +2. Wait for the user to reply **"确认清单"** or **"排除:<路径…>"** to update the list. **Do not** ask a binary "whether to clean up" question. +3. Delete according to the **final list**; **do not** delete paths outside the list; **do not** delete **`skills/f2s-*`**. +4. After closing is complete, handle the state file: + - Fully completed round: delete `.Knowledge/.migrate-state.json` + - Paused/aborted round: keep `.Knowledge/.migrate-state.json` (`status=paused`) and record undeleted paths and reasons + +## Output Summary Format (Recommended) + +```markdown +## Topic Migration Complete: + +### Sources +- rules: +- business docs: +- mapping: + +### Written +- .Knowledge/topics/.md +- .Knowledge/index.md (updated rows) +- .Knowledge/manifest-routing.json (updated fields: ...) +- .Knowledge/matchers/.json (updated `includeAny`, etc.: ...) + +### Next Step +- Reply "继续" to migrate the next topic +- Reply "停止" to stop migration +``` + +```markdown +## Document Migration Complete: / + +### Source +- source: + +### Written +- .Knowledge// + +### Next Step +- Reply "继续" to migrate the next file +- Reply "停止" to stop migration +``` + +## Constraints + +- Must confirm topic by topic; do not skip confirmation and migrate everything in batch. +- `stock-docs` / `req-docs` must confirm file by file; do not batch-migrate without confirmation. +- Document migration must preserve source-directory relative paths; do not flatten to single-layer filenames. +- **`f2s-*` skills must not enter `.Knowledge` and must not be merged into topics during topic migration.** +- **Business** `skills/` (non-`f2s-*`) must be fully inventoried; out-of-index items require user confirmation by default before migration. +- Before all topics complete, do not delete legacy business `rules/` or old business `skills/` that are **non-`f2s-*`**; the baseline `f2s-*` root rule files are never deleted. +- Before document migration completes, do not delete legacy document directories. +- Before deleting legacy directories, complete **"deletion path list"** review (exclusions allowed); **do not** replace list confirmation with "whether to clean up". +- During migration, modify only `.Knowledge` and (after **final deletion-list** confirmation) deletion of old paths in the list; do not modify business code. +- Must maintain `.Knowledge/.migrate-state.json`; do not keep migration progress only in memory. +- After topic and document-layer migration acceptance, **first** write `.Knowledge/migration-report.md` (including migration mapping table and proposed deletion path list), then enter physical deletion; report and in-conversation deletion list must share a traceable source. +- `.migrate-state.json` / `migration-report.md` deletion execution record / `manifest-routing.json` / `.Knowledge/index.md` are always written by the main agent. + +## Migration Report Template (Recommended Structure for `migration-report.md`) + +The following skeleton may be copied and filled; all paths are relative to project root. + +```markdown +# Knowledge-Base Migration Report + +- **Generated at (ISO-8601)**: <...> +- **Configuration root (for example `.cursor/`)**: <...> + +## Migration Mapping Table + +### Topics (legacy sources -> new path) + +| topic ID | legacy rules / legacy business skills / index clues | new path | +| --- | --- | --- | +| | <...> | `.Knowledge/topics/.md` | + +### stock-docs (source -> target) + +| source path | target path | note | +| --- | --- | --- | +| <...> | `.Knowledge/stock-docs/...` | success / skip reason | + +### req-docs (source -> target) + +| source path | target path | note | +| --- | --- | --- | +| <...> | `.Knowledge/req-docs/...` | success / skip reason | + +## Proposed Deletion Path List (default selected; same as in-conversation list) + +- [ ] `` (each file under `rules/`) +- [ ] `` (business `skills/`, excluding `f2s-*`) +- [ ] `.cursor/docs-index.md` (or actual path) +- [ ] (optional) legacy `stock-docs/` / `req-docs/` root directories + +## User Exclusions (if any) + +- (write "none" if none) + +## Failed or Unmigrated Items (if any) + +- (write "none" if none) + +## Deletion Execution Record + +(Append only after physical deletion: time, deleted list, undeleted items and reasons) +``` + +## Completion Self-Check + +1. Topic count aligns with the legacy mapping count (unless the user explicitly skipped items). +2. Every `manifest.topics[].path` exists. +3. `index` can locate every migrated topic. +4. `topicMetadata` only references topicIds that exist in `topicPaths`; `primary` / `tags` / `confidence` are valid. +5. `.Knowledge/stock-docs` and `.Knowledge/req-docs` match the confirmed migration lists. +6. Manual confirmation list is empty; if not, deleting legacy document directories is forbidden. +7. Legacy business `rules/`, old business `skills/` that are **non-`f2s-*`**, legacy indexes, and legacy document directories (if listed) were deleted according to the **final deletion list**; the three `f2s-*` root rules in the baseline keep-list are still kept. +8. Legacy entries `docs-index.md` / `index-doc.md` and `rules/main.md(c)` were deleted according to the list (and `.Knowledge` can replace their responsibilities), or explicitly kept due to user exclusion and written to `notes[]`. +9. State file matches migration result (delete if complete; keep with `status=paused` if paused). +10. `.Knowledge/index.md` has synced the "Associated documents (summary)" column for every topic (may write "none", but not blank). +11. `skills/f2s-*` were not accidentally deleted and were not written into `.Knowledge`. +12. `.Knowledge/migration-report.md` is written and contains the **migration mapping table** and **proposed deletion path list**; if deletion was executed, **`## Deletion Execution Record`** was appended and matches actual disk state. +13. State-machine file and deletion execution record were not written by sub-agents without authority; manifest / index were written by the main agent only. diff --git a/packages/core/templates/en-US/skills/f2s-kb-rm/SKILL.md b/packages/core/templates/en-US/skills/f2s-kb-rm/SKILL.md new file mode 100644 index 0000000..1e6d8ca --- /dev/null +++ b/packages/core/templates/en-US/skills/f2s-kb-rm/SKILL.md @@ -0,0 +1,61 @@ +--- +name: f2s-kb-rm +description: Remove the knowledge topics and index mappings associated with a stock-docs document; triggers: 删除项目上下文、f2s-kb-rm、remove project context、delete knowledge context +--- + +> Execution scope: only maintain `.Knowledge`; do not modify the configuration-root `rules/skills`. + +## Orchestration (main / sub-agent) + +- The meaning of `subAgent` / `switchAgentVerification` uses the unified entry as the only source of truth: **Cursor/Claude** read the configuration-root `rules/f2s-flow2spec-unified-entry.*`; **Codex** reads `.codex/topics/f2s-flow2spec-unified-entry.md` (same source, mirrored by `flow2spec init`). Do not repeat those definitions here. +- By default, the main agent completes the full workflow (single-point deletion has low benefit from sub-agent splitting). +- Split threshold: only when `subAgent=true` and **one batch deletes >= 5 topics** may deletion and reference cleanup be delegated to sub-agents. +- Main agent must control scope confirmation and `fallbackTopic` reassignment. +- Write-authority hard rule: `manifest-routing.json` and `.Knowledge/index.md` are always written by the main agent. +- Verification: by default, the writing side verifies its own work; this SKILL does not bind cross-agent verification. + +# Delete the Project Context Associated with a Document + +## Input + +- One argument: a `.Knowledge/stock-docs/.md` path, or a filename fragment that can match one. + +## Procedure + +1. Read `.Knowledge/index.md` and match topics associated with the target document. +2. Delete the corresponding `.Knowledge/topics/.md` files. +3. Remove the matching entries from `.Knowledge/index.md` and write it back. +4. Update the routing manifest: + - `.Knowledge/manifest-routing.json`: remove invalid `topicPaths`, `taskToTopicRules`, `topicDependencies`, and `topicMetadata` references. + - The corresponding `matchers/.json`: remove invalid rules or `includeAny` terms aligned with the deleted `task` / `matcherId`. + - If the deleted topic was `fallbackTopic`, a new fallback topic must be specified. + - **Authoring-side guideline**: this step adjusts `topicDependencies` (deleted depended-on topics or orphan edges), so first Read the full `rules/f2s-topic-authoring.*` (**Cursor/Claude**: `rules/f2s-topic-authoring.mdc`; **Codex**: `.codex/topics/f2s-topic-authoring.md`) and verify DAG and minimization constraints before writing. + +## Output Summary (Required) + +- List of deleted topic files. +- Entries removed from `.Knowledge/index.md`. +- Fields adjusted in the routing manifest. +- Items not executed, if any. + +## Complex Scenario Example + +The user provides the filename fragment "回调", and it matches 2 topic documents. + +- First list the two candidates and ask the user to confirm the deletion scope to avoid accidental deletion. +- After deletion, clean up invalid routing-manifest references; if the deleted topic was `fallbackTopic`, specify a new fallback topic before writing. +- In the final summary, state which topics were deleted, which topics were kept, and why. + +## Constraints + +- Ask the user to confirm when the match is ambiguous. +- Delete only matched topics; do not affect other topics. +- `manifest-routing.json` and `.Knowledge/index.md` are always written by the main agent (write-authority hard rule); scope confirmation and `fallbackTopic` reassignment must not be delegated to sub-agents. + +## Completion Self-Check + +1. The deleted topic is no longer referenced by `manifest` (must be false). +2. `index` no longer contains invalid topic paths (must be false). +3. `topicMetadata` no longer references deleted topics (must be false). +4. `fallbackTopic` is still valid. +5. No sub-agent split was forced below the threshold (< 5 topics); manifest / index were written by the main agent only. diff --git a/packages/core/templates/en-US/skills/f2s-kb-sync/SKILL.md b/packages/core/templates/en-US/skills/f2s-kb-sync/SKILL.md new file mode 100644 index 0000000..f00caa7 --- /dev/null +++ b/packages/core/templates/en-US/skills/f2s-kb-sync/SKILL.md @@ -0,0 +1,160 @@ +--- +name: f2s-kb-sync +description: Accept an explicit capability list or infer from zero input; first output a knowledge-base update outline, then write topics/index/manifest after confirmation; triggers: f2s-kb-sync、全局同步、知识库同步、已实现能力、global sync、sync knowledge base、implemented capability +--- + +> Execution scope: this skill only maintains `.Knowledge`; by default it does not modify the configuration-root `rules/skills`. + +## KB Auto-Merge Protocol (Required) + +This skill must not make manual command execution part of the user flow. After the user confirms the sync outline, the agent performs knowledge candidate generation, merge planning, build, and validation by itself: + +1. Convert the confirmed outline into one or more `kb-delta` drafts with `taskId`, `developerId`, `baseRevisions`, `changes`, and evidence summary. If there is no explicit task directory, an equivalent in-memory object is acceptable; do not create `.task` only for this skill. `changes` may use `appendBody` / `replaceBody` / `updateFrontmatter`; when a new topic is needed, use `createTopic` and optionally include `taskRule` plus `matcher` so routing is connected in the same merge. +2. Before writing `.Knowledge`, run `flow2spec kb plan ` or the equivalent internal capability. If a topic revision differs, stop automatic writing and switch to semantic-merge reporting. +3. When the change is auto-mergeable, run `flow2spec kb apply ` or the equivalent internal capability, then run `flow2spec kb build` and `flow2spec kb check`. +4. The user should only see "knowledge base synced / semantic conflict needs confirmation / skipped with reason"; do not ask the user to manually run `kb plan/apply/build/check`. + +## Orchestration (main / sub-agent) + +- The meaning of `subAgent` / `switchAgentVerification` uses the unified entry as the only source of truth: **Cursor/Claude** read the configuration-root `rules/f2s-flow2spec-unified-entry.*`; **Codex** reads `.codex/topics/f2s-flow2spec-unified-entry.md` (same source, mirrored by `flow2spec init`). +- Step 1 (material collection): when `subAgent=true`, read-only collection may be split across sub-agents; they must not write files. +- Step 2 (outline + user confirmation): must be completed by the main agent; confirmation authority must not be delegated. +- Step 3 (write): when `subAgent=true`, writing may be split by confirmed outline item. Hard rule: before a sub-agent writes, it must load the opening summaries of 2-3 neighboring topics to align narrative style. +- Write-authority hard rule: `manifest-routing.json` and `.Knowledge/index.md` are always written at a single point by the main agent; delegation is forbidden. +- Verification: by default, the writing side verifies its own work; this SKILL does not bind cross-agent verification. + +# f2s-kb-sync (Outline First, Then Write) + +## Input (Optional) + +1. The user explicitly provides an "implemented capability list". +2. Zero input: the Agent infers from current context. +3. Supporting materials: `@` files, requirement documents, architecture notes, etc. + +## Mandatory Flow (Order Must Not Be Reversed) + +### Step 1: Collect Materials (Read-Only) + +- Summarize the user's target, scope, and priority. +- Summarize implemented capabilities (user-specified + Agent-inferred). +- Compare against the existing knowledge base: + - `.Knowledge/topics/` + - `.Knowledge/index.md` + - `.Knowledge/manifest-routing.json` + - `.Knowledge/matchers/*.json` (the shards corresponding to routing `matcherPath`) + - `.Knowledge/stock-docs/` +- **Topic-granularity scan**: roughly scan existing topics for the following signals. If hit, list them as "recommended split" in the step 2 outline (does not block the sync flow): + - The corresponding stock-doc exceeds **300-500 lines**. + - `includeAny` has more than **12 terms**. + - The topic body contains second-level headings covering more than **3 unrelated responsibility domains**. + +### Step 2: Output the "Update Outline" (Required) + +The outline must include at least: + +1. Sync goal. +2. Capability list (user-specified / Agent-inferred / merged result). +3. Information sources. +4. Proposed file-change list (exact paths). +5. Topic sync plan: for each capability, state whether it updates an existing topic or creates a new topic, and list topicId, topic file, index row, and manifest/matcher changes. If `topicMetadata` is involved, list candidate `primary` / `tags` / `confidence` and evidence; if evidence is unclear, write "do not classify / do not write for now". +6. **Stock-doc consolidation plan (hard rule)**: For every topic being "created / updated", check whether its "Detailed background / Related materials / Long-form source / Reference documents" reference slot already has a corresponding `.Knowledge/stock-docs/*_终稿.md`: + - **Exists** → reference it directly; + - **Missing but the capability being synced is already implemented in code** → the outline **must list** "generate `stock-docs/_终稿.md`", noting the consolidation sources (the matching `req-docs/*_技术方案.md` + implemented code + clarification doc). Before step 3, this SKILL first triggers `f2s-doc-final` to consolidate (or waits for user confirmation to hand-write), **then** points the topic to the stock-doc; + - **Capability is still at the req-docs stage without code** → the topic's "Long-form background" section writes a placeholder "to be generated by `f2s-doc-final` after the code lands". **Do not** list `req-docs/*` in this slot. + - Basis: see `rules/f2s-topic-authoring.*` "Directory boundary for long-form background references (hard rule)". +7. Out-of-scope items. +8. Prompt to wait for user confirmation. + +> Before confirmation, writing any changes is forbidden. + +### Step 2.5: Consolidate Stock-Docs (If Step 2 Listed Any) + +After the user confirms the outline and before any `.Knowledge/topics/` write, **consolidate each `stock-docs/*_终稿.md`** listed in outline item 6: + +- Prefer entering **`f2s-doc-final`** directly (within the same session; no re-trigger needed); +- Or hand-write per `.Knowledge/template/` (if a final-draft template exists) and save it; +- Only after the stock-doc is written and its path is known, enter step 3 and point the topic's "Long-form background" section at that stock-doc. + +**Prohibited**: skipping this step and writing a topic whose "Long-form background / Related materials" slot lists `req-docs/*` files. + +### Step 3: Write After Confirmation + +> Hard rule: if sub-agent splitting is enabled, a sub-agent must read the opening summaries of 2-3 neighboring topics before writing to align narrative style; `manifest-routing.json` and `.Knowledge/index.md` are written at a single point by the main agent, and sub-agents have no write authority for them. +> +> **Authoring-side guideline**: if this step adds or modifies topics, `topicMetadata`, or `topicDependencies`, first Read the full `rules/f2s-topic-authoring.*` (**Cursor/Claude**: `rules/f2s-topic-authoring.mdc`; **Codex**: `.codex/topics/f2s-topic-authoring.md`) before writing. + +Update according to the outline, item by item: + +- `.Knowledge/topics/*.md` +- `.Knowledge/index.md` (sync the "Associated documents (summary)" column in the topic routing table) +- Routing manifest (as needed). When creating a new topic, also sync `topicPaths` and necessary `taskToTopicRules` / matcher shards. Write `topicMetadata` only when evidence is clear; classification is only for governance, audit, and reading expectations, does not participate in route matching or execution requirements, and must not be used to create, rename, or split topics. +- `.Knowledge/stock-docs/*.md` (add source documents as needed) + +### Step 4: Closing Summary + +- List modified paths and purpose. +- List skipped items and reasons. + +### Step 5: Write Sync Timestamp (Required; f2s-git-commit depends on it) + +After this skill successfully writes to disk (Step 3 actually modified files), the main agent writes `.Knowledge/.last-sync.json`: + +```json +{ + "syncedAt": "", + "skill": "f2s-kb-sync", + "developerId": "" +} +``` + +- `f2s-git-commit` reads this file before its **default coverage check**; if `syncedAt` is within 30 min, it skips coverage to avoid double-syncing what was just synced. +- **When to write**: only when this turn actually wrote to disk (Step 3 modified topic / index / manifest / stock-docs). A pure "read the KB, nothing changed" pass **does not** write. +- Overwrite, do not append history. +- If writing fails (read-only disk, insufficient permission), do not block the main flow; add a warning line to the closing summary. +- Peer knowledge-base-writing skills (`f2s-kb-feat` / `f2s-kb-fix` / `f2s-kb-add` / `f2s-kb-addRules` / `f2s-kb-distill`) follow the same convention on successful write, with `skill` set to their own id. + +## Output Summary Format (Recommended) + +```markdown +## Knowledge-Base Sync Result + +### Confirmed Capability Scope +- +- + +### Modified Files +- .Knowledge/topics/.md: +- .Knowledge/index.md: +- .Knowledge/manifest-routing.json: +- .Knowledge/matchers/.json: +- .Knowledge/stock-docs/.md: + +### Skipped Items +- : +``` + +## Complex Scenario Example + +The user only says "/f2s-kb-sync sync it" and provides no capability list. + +- Step 1 first makes a minimal inference (for example, identify 1-2 capability domains from `git diff` / directory names), and provides the evidence. +- Step 2 must output an outline and wait for "confirm"; before confirmation, writing any `.Knowledge` file is forbidden. +- After confirmation, execute only items in the outline. If the user narrows the scope mid-flow, record skipped items in the closing summary. + +## Constraints + +- Outline first, write second. +- Add in small increments; avoid whole-file rewrites. +- Prefer in-place updates for the same topic. +- Each topic in `index.md` needs summary-level **clickable Markdown links** to `stock-docs/req-docs` (format: `[title](relative path)`, 1-3 links; "none" is allowed). +- Do not modify the configuration-root `rules/skills`. + +## Completion Self-Check + +1. No writes happened before confirmation (must be false). +2. Topic files and index rows correspond one-to-one, and the "Associated documents (summary)" column has been updated. +3. `topics` / `taskToTopicRules` / `topicDependencies` in manifest still reference valid paths. +4. If `topicMetadata` was written: every key exists in `topicPaths`; `primary` / `tags` / `confidence` are valid; type-prefix naming was avoided. +5. The configuration-root `rules/skills` was not modified (must be false). +6. The step 2 outline + user confirmation were not delegated to a sub-agent; before step 3 sub-agent writes, 2-3 neighboring topic summaries were loaded; manifest / index were written by the main agent only. +7. **For every topic created or updated**, its "Long-form background / Detailed materials / Related materials / Long-form source / Reference documents" reference slot **points only at `.Knowledge/stock-docs/*_终稿.md`** (or a finalized stock-doc); it **must not** list `.Knowledge/req-docs/*` files as the long-form source of truth. If the code has landed but the corresponding stock-doc is missing, step 2.5 consolidation must have been completed. diff --git a/packages/core/templates/en-US/skills/f2s-kb-upgrade/SKILL.md b/packages/core/templates/en-US/skills/f2s-kb-upgrade/SKILL.md new file mode 100644 index 0000000..9133c8d --- /dev/null +++ b/packages/core/templates/en-US/skills/f2s-kb-upgrade/SKILL.md @@ -0,0 +1,363 @@ +--- +name: f2s-kb-upgrade +description: Knowledge-base template upgrade skill (this SKILL only): **V1 flow branch** must run f2s-kb-migrate first, then run flow2spec init inside the workflow; **current repositories (flow branch V2+, including Flow2Spec npm v3.x projects already using .Knowledge)** run init to align manifest-routing + matcher shards (package `manifest-matchers.json` is only an init merge seed and is not written into .Knowledge). Triggers: f2s-kb-upgrade、一键升级迁移、旧项目升级、知识库模板升级、upgrade knowledge base、template upgrade. Note: do not call standalone flow2spec init an "upgrade command"; **V1/V2+ are flow-branch labels inside this skill, not npm package major versions**. +--- + +> Execution scope: this skill is used to "run shell for the user" to complete Flow2Spec **template and configuration-root alignment as defined by this SKILL**. One step runs **`flow2spec init`**, but **`init` is not an "upgrade command"**. **Upgrade command / knowledge-base upgrade** refers only to the **full `f2s-kb-upgrade` workflow**. + +# f2s-kb-upgrade (Knowledge-Base Template Upgrade Skill) + +**Terminology (required)**: **"upgrade", "upgrade command", and "knowledge-base upgrade"** refer only to the complete skill workflow executed according to this file **`f2s-kb-upgrade`**. **`flow2spec init`** is a CLI **initialization/write-to-disk** command; this skill runs it in **step 2**. It is **forbidden** to describe standalone user execution of `init` or `init` in CLI help as an "upgrade command". + +## Boundaries (Avoid Misunderstandings) + +- **`flow2spec init` does not write business knowledge**: it does not replace `f2s-kb-add`, `f2s-kb-fix`, `f2s-kb-feat`, `f2s-kb-sync`, `f2s-kb-build`, or similar maintenance of `stock-docs` / `req-docs` / `topics` bodies and business routing terms. +- This skill completes **directory, template placeholder, and routing-structure alignment under the package version**. If the user says "write the new capability into the knowledge base", guide them to **`f2s-kb-sync` / `f2s-kb-add`** etc., not just `f2s-kb-upgrade`. +- This skill is responsible for auditing existing `topicMetadata`: `primary` / `tags` are only for governance, audit, inventory, and reading expectations; they do not participate in route matching or execution requirements. Execution requirements still come from `AGENTS.md`, rules, skills, and topic bodies. + +## Package-side Release Discipline (`projectRev` MUST be bumped correctly) + +**Field location**: root-level integer field `projectRev` in `templates/{zh-CN,en-US}/knowledge/manifest-routing.json` (starts at `1`). + +**Field write semantics (read first)**: +- **Package side**: maintainers manually bump per the rules below (the package template's own `projectRev` is always the latest). +- **Project side** (written into `.Knowledge/manifest-routing.json`): + - **First init**: project `.Knowledge/manifest-routing.json` does not exist -> `init` writes the template value, equivalent to baselined-on-arrival. + - **Subsequent init**: project `.Knowledge/manifest-routing.json` already exists -> `init` **no longer overwrites this field** (project value preserved); the field is only written by this skill's full flow tail (see step 3b "Write back `projectRev`"). + - This gives "project-side `projectRev`" a clean meaning: **"the package-template revision this project has been baselined to"** — not "what the last init carried over". + +**Must bump (at least `+1` per release)** when any of the following changes: +- Any body change / addition / deletion / rename of `templates//knowledge/topics/.md`; +- `includeAny` entries, `id` changes, or addition / deletion of any `templates//knowledge/matchers/.json`; +- Any change in `topicPaths` / `taskToTopicRules` / `topicDependencies` / `fallbackTopic` / `topicMetadata` of `templates//knowledge/manifest-routing.json`; +- Any change in the "topic overview" section or package-level sections of `templates//knowledge/index.md`. + +**Does NOT require a bump**: +- Package source (`lib/`, `cli.js`, `scripts/`), `AGENTS.md`, `README*`; +- `templates//flow2spec.config.json` default values; +- `templates//rules/*` / `templates//skills/*` rule/skill body changes (unrelated to the topic layer; no need to trigger the full flow). + +**One-line rule**: any topic-layer artifact (topic / matcher / manifest / index) under `templates//knowledge/` changed -> must bump; otherwise do not. Missing a bump causes user `f2s-kb-upgrade` to take the fast path and miss the topic changes from the package. + +## Orchestration (main / sub-agent) + +- The meaning of `subAgent` / `switchAgentVerification` uses the unified entry as the only source of truth: **Cursor/Claude** read the configuration-root `rules/f2s-flow2spec-unified-entry.*`; **Codex** reads `.codex/topics/f2s-flow2spec-unified-entry.md` (same source, mirrored by `flow2spec init`). This section does not repeat those definitions. +- **Sub-agent responsibility** (only when `subAgent=true`): run shell commands such as `flow2spec init`; only command execution is delegated, not knowledge-base body writing. +- **Main agent must control** (must not delegate): + 1. **Version branching**: **V1** runs `f2s-kb-migrate` first, then enters this skill; **current repositories (V2+)** directly enter the `init` flow (including Flow2Spec **npm v3.x** etc.; as long as step 0 "current repository" conditions are met, use this branch. **Do not** create a separate flow just because the package major version is 3). + 2. **Re-read after `init`**: re-read `f2s-kb-upgrade/SKILL.md` from disk and compare whether the identifier changed. + 3. **Rerun the whole skill**: when SKILL changed, rerun from the beginning according to the new literal text until two consecutive rounds show no changes. + 4. **Step 3b merge**: preserve the maintained section of `.Knowledge/index.md` and merge with the package version; main agent performs this. + 5. **Verification summary**: the main agent summarizes verification conclusions and output. +- **Write-authority hard rule**: `.Knowledge/index.md` is **written only by the main agent**; sub-agents **must not touch it**. `manifest-routing.json` is also written by main. +- This SKILL does not bind cross-agent verification; the writing side verifies its own work. + +## Why this coexists with `f2s-kb-migrate` + +| Skill | Problem solved | +| --- | --- | +| **`f2s-kb-migrate`** | **Structural move**: `docs-index.md` / `index-doc.md`, `rules/main.md(c)`, business `skills/`, scattered `stock-docs`/`req-docs` -> **migrate into `.Knowledge`**, write `migration-report.md`, and confirm deletion list with the user. It does not run npm package upgrade. | +| **This skill `f2s-kb-upgrade`** | **Package and template alignment**: run **`flow2spec init`**, merge **`manifest-routing.json`** with **`matchers/*.json`**, refresh each agent's **`rules`/`skills`** (or Codex **`AGENTS.md`**); `init` also copies the current-language **`index.md` -> `.Knowledge/template/index.template.md`** as a comparison snapshot. **`.Knowledge/index.md`** is diff-aligned in step 3b; init **does not** automatically change its body. | + +- **One-click closure for old projects**: **first `f2s-kb-migrate`** -> **then this skill** (`init`). Do not use only `init` as a substitute for full migration. +- **Projects already using new `.Knowledge`**: **run only this skill**; do not repeat migrate. + +**Why does each configured client directory have a same-named `SKILL.md`?** +Each client only loads `skills/` under **its own configuration root**. `flow2spec init` writes the current-language skill content into the selected agent directories. + +## Goal + +When the user says "help me upgrade the knowledge-base template / run f2s-kb-upgrade / sync latest Flow2Spec", the Agent **executes the full `f2s-kb-upgrade` workflow in this skill** (including running `flow2spec init`, cleanup, verification, and summary). **Do not** treat only executing `init` as completing this skill. + +## Default Behavior + +1. When this skill's step 2 runs **`flow2spec init`**, it defaults to **incremental write** (without `--reset-knowledge`). +2. Append `--reset-knowledge` only when the user explicitly requests "overwrite reset". +3. Prefer agents specified by the user; if unspecified, use the package's default client selection. + +## init and Skill Self-Update (Required) + +This skill executes **`flow2spec init`** in **step 2**. `init` syncs the current-language skill content into each agent **configuration root**, so after **`init` succeeds**, the repository's **`skills/f2s-kb-upgrade/SKILL.md`** may be overwritten by a new version and differ from the old instructions cached in the current conversation. + +**Closed loop (avoid stale instructions)**: + +1. **Before `init`** (recommended): record the current configuration-root **`skills/f2s-kb-upgrade/SKILL.md`** identifier (for example `mtime`, file size, or body hash). +2. **After `init` succeeds**: **re-read from disk** the full **`SKILL.md`** (Cursor: `.cursor/skills/f2s-kb-upgrade/SKILL.md`; Claude: `.claude/skills/...`; Codex: `.codex/skills/...`, matching the agent(s) written by this `init` run). +3. **If it changed relative to step 1** (or Flow2Spec package was just upgraded and unchanged status cannot be confirmed): **use the latest SKILL as authoritative** and **rerun evaluation and write-back per the new literal text** — that is, **start from "step 2c"**: re-read `projectRev` / `pkgRev`, decide fast path or full flow per the new judgment table, and run steps 3 / 3a / 3b / 4 / 5 accordingly. **Do not re-execute `flow2spec init` during this rerun** — `init` already ran in step 2 this round; running it again brings no new information and would trap the SKILL self-update loop. Loop until two consecutive rounds read an unchanged SKILL, or the user explicitly asks to stop. +4. **If unchanged**: continue with step 2c and later. + +> **Fast-path exception**: when step 2c judges as "fast path" (`projectRev == pkgRev`, no topic-layer change), even if SKILL.md content has changed, rerunning per the new SKILL is **not required** — a rerun will only judge fast path again, wasting cycles. The loop above applies only when running the full flow. + +> Wording: **after this skill's step 2 executes `init`** -> re-read latest `f2s-kb-upgrade/SKILL.md` -> if changed **and the run goes through the full flow**, **rerun from step 2c per the new literal text** (**do not run `init` a second time**). Do not rely on session memory to execute **this skill**. + +## Mandatory Flow + +### Step -1: Global flow2spec Version Preflight (Required, Before Everything, Foreground Probe by Main Agent) + +**Purpose**: prefer using the **already-installed global `flow2spec`** whenever possible. Only trigger a global upgrade when it is **missing** or **out of date**; when it is already at latest, **completely skip** any upgrade action. This also decides the **default form** of the step 2 command (whether to use `flow2spec init` or `npx @latest init`). + +**Action**: before entering step 0, the main agent **sequentially runs 3 probes in the foreground** (all read-only, no side effects, seconds to return — no sub-agent needed): + +```bash +# 1. Probe whether flow2spec is installed globally on this machine +flow2spec --version 2>/dev/null || echo __F2S_NOT_INSTALLED__ +# 2. Query the latest version on npm (may fail on restricted networks — allowed) +npm view @double-coding/flow2spec version 2>/dev/null || echo __F2S_NPM_UNREACHABLE__ +# 3. (Backup) if step 1 returned __F2S_NOT_INSTALLED__, confirm npx is available +command -v npx >/dev/null 2>&1 && echo __NPX_OK__ || echo __NPX_MISSING__ +``` + +**Three-way decision** (pick one branch based on the results; record it in this-turn context and use it to drive step 2 & step 5 summary): + +| Case | Condition | Action | Default step 2 command | +| --- | --- | --- | --- | +| **A. Installed & on latest** | Step 1 returned version `V`, step 2 returned version `L`, and `V === L` | **Skip upgrade entirely** — no sub-agent, no `npm i -g` this turn | **`flow2spec init `** (use global CLI) | +| **B. Installed but behind** | Step 1 returned `V`, step 2 returned `L`, and `V !== L` (`V < L` or semver-unequal) | **Dispatch an independent sub-agent (fire-and-forget)** to run `npm i -g @double-coding/flow2spec@latest` in the background — no wait, no block. Current turn's step 2 still uses `npx @latest` to guarantee this session gets the latest template | **`npx @double-coding/flow2spec@latest init `** | +| **C. Not installed or latest unknown** | Step 1 hit `__F2S_NOT_INSTALLED__`, OR step 2 hit `__F2S_NPM_UNREACHABLE__` and step 1 also didn't return a version | If A doesn't hold and **not installed**: same as B — dispatch a sub-agent to `npm i -g ...@latest`. If step 2 failed but step 1 shows some installed version: treat as B without a way to compare to latest — **do not** dispatch an upgrade, just note "latest unknown, use npx conservatively" | **`npx @double-coding/flow2spec@latest init `** | + +**Orchestration (required)**: + +- **Branch A**: main agent skips all upgrade actions and **does not** dispatch a sub-agent; step 2's default is `flow2spec init`. +- **Branch B / C**: only when an upgrade is actually needed (missing or behind), dispatch an **independent sub-agent** fire-and-forget to run `npm i -g @double-coding/flow2spec@latest`; **do not wait**, **do not block** the main flow. Success/failure does not enter the SKILL summary conclusion. This sub-agent dispatch is **mandatory** and **not subject to** `flow2spec.config.json.subAgent` (a one-off global npm install is not a business split). +- **Write permission**: the sub-agent only runs that shell command and **does not** touch any project file (`.Knowledge`, `manifest-routing.json`, `index.md`, etc.). Write-permission constraints remain unchanged. +- **Probe failure fallback**: if all 3 probes fail (no shell permission, extremely restricted env), treat as branch C and use `npx @latest`; alternatively, skip step -1 entirely and rely on `cli.js`'s `maybeAutoUpdateGlobalInstall()` tail fallback. + +**Relation to cli.js**: + +- `maybeAutoUpdateGlobalInstall()` inside cli.js is the `init` tail fallback. **No conflict with this step**: this step probes/dispatches before the foreground init; cli's fallback runs once more at init's tail. If both succeed it's a no-op; if the first fails the second still has a chance to fix it. + +### Step 0: Version Judgment and Branching (Required, Before init) + +> **Naming note**: **"V1"** and **"current repository (V2+)"** below are **flow-branch labels inside this skill**. If the **npm package is v3.x, v4.x, ...** and the repository is already in `.Knowledge` + `manifest-routing` shape, still use the **"current repository (V2+)"** branch (only `init` alignment). **Do not** interpret the npm major version number as the literal "V2" here. + +**V1 - Legacy knowledge organization (must migrate before init)** +Hit **any** strong signal: + +- The configuration root still has **`docs-index.md` or `index-doc.md`**, and mostly still closes through **`rules/main.md` / `rules/main.mdc`**; or +- Business **`stock-docs` / `req-docs`, rules, and business skills** are still mainly in the old configuration-root tree and are **not** stably under `.Knowledge`. + +**Action**: first execute the full **`f2s-kb-migrate`** workflow (including `migration-report` and deletion-list confirmation), **then** enter steps 1-5 to execute `flow2spec init`. + +**Current repository (V2+) - Already on `.Knowledge` + new routing (package/shape alignment only)** +Both conditions are met: + +- **`.Knowledge/manifest-routing.json`** exists, and **`topicPaths` / `taskToTopicRules`** are usable. +- Business docs are mainly under **`.Knowledge/stock-docs`, `req-docs`, `topics`** (can also be the state right after V1 migration completes). + +**Historical wording**: if the repository still has legacy single-file **`manifest.json`**, **do not** use it as the machine-readable source of truth; machine reading uses **`manifest-routing.json` + `matchers/*.json` pointed to by `matcherPath`**. `init` handles merge/backfill of shards with the template. + +**Action**: directly enter steps 1-5; **no** migrate is needed unless the user explicitly asks to redo migration. + +### Step 1: Confirm `init` Mode Inside This Skill (Required) + +- If the user did not explicitly request "overwrite reset", this skill's step 2 defaults to **incremental `init`**. +- If the user mentions "overwrite everything according to template / reset", confirm a second time before using `--reset-knowledge`. +- **locale rule**: regular upgrade follows project `flow2spec.config.json.locale`; if missing, fill as `zh-CN`. Do not opportunistically switch languages in this skill; pass `--locale en-US` / `--locale zh-CN` only when the user explicitly requests it. + +### Step 2: Execute Command (Run Shell for User) + +**Before step 2 starts**: read the project-side **`.Knowledge/manifest-routing.json`** `projectRev` field (**record as `null` if missing**), store it as **`projectRev`**. `projectRev` means **"the package-template revision this project has been baselined to"** (written by this skill's full-flow tail after steps 3 / 3a / 3b; on first init the `init` command writes the template value as the baseline). **`init` no longer overwrites this field when manifest already exists**, so `projectRev` reflects the version this project most recently completed full-flow alignment to — not "what the last init carried over". `projectRev` will be compared with `pkgRev` in step 2c. + +Run one of the following in the target project root (**choose the default form based on the step -1 branch conclusion**): + +1. **Step -1 returned A (installed & on latest)**: use the global CLI directly (**preferred**): + - `flow2spec init ` +2. **Step -1 returned B/C (missing / behind / latest unknown)**: fetch npm latest (**guarantees this session gets the latest template**): + - `npx @double-coding/flow2spec@latest init ` +3. For overwrite reset: + - Append `--reset-knowledge` to the above command. +4. If the user explicitly requests a template-language switch: + - Append `--locale ` to the above command. +5. **Manual override**: if the user explicitly says "use global" or "use npx", follow the user's choice and skip the step -1 branch-driven auto-selection. + +> `` example: `cursor claude codex`. + +> **Helper commands (user self-inspection)**: `flow2spec --version` shows the current global version; `flow2spec update` triggers the CLI's built-in self-update. These do **not** replace this SKILL's full flow — they only keep the global CLI fresh; topic-layer alignment still requires step 2 and beyond. + +**After step 2 completes**: immediately execute the above **"init and skill self-update"** loop: re-read **`skills/f2s-kb-upgrade/SKILL.md`**. If updated, **rerun from step 2c per the new literal text** (**do not run `init` a second time**; avoid using the old SKILL for subsequent verification). + +### Step 2c: Topic-layer Change Judgment (Required, decides fast path vs full flow) + +**Goal**: when the package upgrade **does not bring topic-layer changes** (topic / matcher / index template body unchanged), skip steps 3 / 3a / 3b and the "rerun per new SKILL" loop, go directly to step 4 lightweight verification. Only when the package side explicitly bumped `projectRev` should the full flow run. + +**Procedure**: + +1. **After `init` finished**, take `pkgRev` from the **project-side manifest**. **Convention**: directly `Read` the **`pkgRev`** top-level field in `.Knowledge/manifest-routing.json` at the project root. This field is written by the current `init` and records "the package-template `projectRev` used by this init run" — i.e. the latest package-side value, paired with the `projectRev` field in the same file (= `projectRev`, "the package-template revision this project has been baselined to") to form a "package side / project side" comparison without adding a new file. + + - Field exists and is an integer -> `pkgRev = `; + - Field missing or not an integer -> `pkgRev = null` (the package template itself does not declare `projectRev`); + - Project-side manifest file itself missing -> not handled here; it should be caught during step 2 / step 1 self-check. + +2. Compare `projectRev` (recorded before step 2) with `pkgRev`: + +| `projectRev` | `pkgRev` | Judgment | Next | +| --- | --- | --- | --- | +| any | `null` | **Full flow** (package did not declare the field; fall back to legacy behavior) | run full steps 3 / 3a / 3b | +| `null` | any int | **Full flow** (project first-time onboarding or legacy upgrade; needs baseline alignment) | run full steps 3 / 3a / 3b | +| int X | int X (equal) | **Fast path** (topic layer unchanged) | **skip** steps 3 / 3a / 3b and the "rerun whole skill" loop, **go directly to step 4** | +| int X | int Y (different) | **Full flow** (package brings topic-layer changes) | run full steps 3 / 3a / 3b | + +3. **`--reset-knowledge` exception**: when the user explicitly resets, **force full flow**, ignore this judgment (reset must rebuild via full 3b). + +4. **The conclusion of this step must be written into step 5 summary**, e.g. "`projectRev`: project `X` vs package `Y` -> fast path / full flow / field-missing fallback". + +> **Blind-spot disclosure**: this judgment looks only at `projectRev` and **trusts the package maintainer to bump correctly when topic / matcher template bodies change**. If the package side does not follow discipline, missed bumps occur; when the user feels it is wrong, they can explicitly request "full flow" (verbally is enough) and the skill should ignore the fast path and go to the full flow directly. + +### Step 3: Old Topic-Template Cleanup and Reference Fixes (Required If Present) + +> **Fast-path skip**: when step 2c judges fast path, **the whole step is skipped**, go directly to step 4. Run the content below only on the full flow. + +After this skill's step 2 `flow2spec init` succeeds, first perform "old file cleanup + reference fixes": + +> **skill directory auto-alignment**: `flow2spec init` now automatically deletes old directories in configuration-root `skills/` that the current version no longer provides (renamed/deleted skills such as `f2s-ctx-build`, `f2s-doc-add`, `f2s-rule-capture`, `stock-docs-vs-req-docs`, etc.). **No manual Agent cleanup is needed**. + +1. Clean old-name topic files (delete only if they exist; all are old legacy names without the `f2s-` prefix): + - `.Knowledge/topics/flow2spec-architecture.md` + - `.Knowledge/topics/implement-tech-design.md` +2. Fix references (update only if files exist; **`.Knowledge/index.md` body is not rewritten by init**, see step 3b): + - `.Knowledge/index.md` (manually or skill-side as needed for paths/paragraphs) + - `.Knowledge/manifest-routing.json` +3. Reference targets (confirm current names): + - `.Knowledge/topics/f2s-flow2spec-architecture.md` + - `.Knowledge/topics/f2s-implement-tech-design.md` + - `.Knowledge/topics/f2s-stock-docs-vs-req-docs.md` + +> Wording: only clean "old-name topic files"; do not delete current topic files with the `f2s-` prefix. + +### Step 3a: Existing `topicMetadata` Audit (Required) + +> **Fast-path skip**: when step 2c judges fast path, **the whole step is skipped**. Run the content below only on the full flow. + +1. Read `.Knowledge/manifest-routing.json`, using `topicPaths` as the complete topic set. +2. Validate `topicMetadata`: keys must exist in `topicPaths`; `primary` may only be `feature` / `module` / `config` / `policy`; `tags`, if present, must be an array, elements must use the same allowed values as `primary`, and must not duplicate `primary`; `confidence` may only be `manual` / `inferred`. +3. For topics in `topicPaths` that lack metadata, perform classification analysis: **must Read the corresponding `.Knowledge/topics/.md` body**; do not infer only from the topicId name. If evidence is clear, write `inferred`; if evidence is insufficient, **do not write metadata**, but list the inferred direction and basis in the summary (for example, "recommend policy because the body contains multiple mandatory constraints") for user confirmation before manual `manual` fill-in. +4. Classification follows `f2s-topic-authoring` guideline section 3. Agent judges the primary nature from the topic body and writes `primary`; when it covers multiple natures, write the rest to optional `tags`. +5. Do not create, rename, or split topics because of classification. +6. **Topic-granularity audit** (does not block upgrade; list in step 5 summary): check each item and list as "recommended split" if any signal is hit: + - The corresponding stock-doc exceeds **300-500 lines**. + - `includeAny` has more than **12 terms**. + - The topic body contains second-level headings covering more than **3 unrelated responsibility domains**. + - The topic is frequently matched by multiple unrelated task types (can be judged from `taskToTopicRules` and matcher term breadth). +7. **Automatic old-topic frontmatter repair**: in the full flow, the agent must run `flow2spec kb build --fix-topics` (or the equivalent internal capability) to add `id`, `revision`, and `summary` to existing topics that lack frontmatter / `revision`, and to fill `dependsOn` / `primary` / `confidence` / `tags` from `manifest-routing.json`. Then run `flow2spec kb check --strict`; if strict validation fails, stop and list the concrete topic / reason in the summary. Do not ask the user to manually add topic headers one by one. + +### Step 3b: `index.md` Merge and `template/index.template.md` (Required) + +> **Fast-path skip**: when step 2c judges fast path, **the whole step is skipped** (the "topic overview" section of the package template is unchanged -> existing `index.md` is still correct). Run the content below only on the full flow. + +> **Scope**: this "merge" is written to `.Knowledge/index.md` **only by the Agent in this skill**. It does **not** require or assume changes to Flow2Spec package **`cli.js` / `lib/init.js`** or other JS. `init` behavior follows the repository's current implementation (snapshot copy, etc.). + +**Role of `flow2spec init` in this workflow**: copy the current-language `index.md` snapshot to **`.Knowledge/template/index.template.md`** as a **package-shell comparison snapshot**. It does **not** replace this step's merge writing for **`index.md`**. + +#### Merge Rules (Required) + +0. **Write ownership**: this step's `.Knowledge/index.md` merge is always performed and written by the main agent; sub-agents must not write directly (write-authority hard rule). +1. **Comparison sources** + - **Package full text**: **`.Knowledge/template/index.template.md`**. + - **Project current state**: **`.Knowledge/index.md`**. + +2. **Project-maintained section (anchor: `## Topic Overview` in `.Knowledge/template/index.template.md`)** + - Using `.Knowledge/template/index.template.md` as reference: from the second-level heading **`## Topic Overview`** **until the end of that section**: specifically, up to the `---` immediately before **`## Match and Execute`** (including the "Topic Overview" table and intra-section explanatory paragraphs). + - This whole block **must preserve the body from current project `.Knowledge/index.md`** (maintained by business and **f2s-***). It is **forbidden** to wholesale replace it with the same block from the package template (to avoid losing business topic rows and summary columns). + - **Allowed** minimal repairs inside this block: for example, add rows for package-added `topicPaths` topics, correct the "path" column according to **`manifest-routing.json` `topicPaths`**, and add new table-column explanations introduced by the package snapshot while preserving existing project behavior. + +3. **Parts that must match the package template** + - Everything outside the maintained block above (from file start to before **`## Topic Overview`**, and from **`## Match and Execute`** through file end) must match the corresponding parts of **`.Knowledge/template/index.template.md`** (package version is authoritative; after diff, overwrite old project text with the template). + +4. **Output** + - Write the complete merged **`index.md`** back to **`.Knowledge/index.md`**. + - Include the **diff** conclusion and whether it changed in step 5 summary. + +5. **Relationship to `--reset-knowledge`** + - If the user used `reset`, `.Knowledge/index.md` may have been overwritten by the template whole-file. Still, this step must restore the "Topic Overview" block from backup or version control before performing merge rule **3**. If the repository has no backup, rebuild the topic table from `topicPaths` + snapshot and ask the user to confirm. + +#### End of full flow: write back `projectRev` (Required) + +After steps 3 / 3a / 3b above are completed in the **full flow** (**not executed on the fast path**), the main agent **rewrites** the project-side **`.Knowledge/manifest-routing.json`** `projectRev` field to **`pkgRev`** (the integer obtained in step 2c; if `pkgRev` is `null`, **leave the field unchanged**): + +- This is the **only** write path for `projectRev` (besides the first-init template default-write); +- The next `f2s-kb-upgrade` will then judge `projectRev == pkgRev` and take the fast path, avoiding repeated 3 / 3a / 3b runs; +- This write shares the same main-agent write authority as the rest of `manifest-routing.json` (write-authority hard constraint). + +### Step 4: Verify This Skill's Execution Result (Required) + +Verify at least: + +1. Step 2 `flow2spec init` exited successfully (exit code = 0). +2. init output includes the conclusion for **routing manifest and `.Knowledge`** (aligned/latest/reset overwrite, etc.) and a line indicating **`index.template.md` was copied** (if the package lacks `index.md`, this line may be absent). +3. `manifest-routing` and every `matcherPath` shard are parseable, and all `topicPaths` / `matcherId` references are valid. +4. **`.Knowledge/template/index.template.md`** exists; step **3b** completed the **`index.md` merge** (maintained block preserved + rest matches package version), or the reason pending user handling is written. +5. Configuration-root artifacts exist: + - Cursor/Claude: `rules/`, `skills/` + - Codex: `.codex/AGENTS.md`, `skills/` +6. After this skill succeeds, delete `.Knowledge/update-check.json` if it exists so the next new session rechecks and clears stale upgrade hints; if deletion fails, state it in the step 5 summary. + +### Step 5: Output Result Summary (Required) + +Output: + +- **Step -1 global version preflight**: branch (`A Installed & on latest (upgrade skipped) / B Installed but behind (sub-agent dispatched to upgrade) / C Missing or latest unknown (dispatched / advised)`) + current global version + npm latest (if obtained) +- Executed command (including agents and whether reset was used) +- Whether it succeeded +- **`projectRev` judgment**: project `X` vs package `Y` -> fast path / full flow / field-missing fallback (step 2c) +- Old topic-template cleanup conclusion (what was deleted / what did not exist; **not executed on fast path**) +- `index/manifest` reference-fix conclusion (**not executed on fast path**) +- **index**: whether `index.template.md` was generated; whether **`index.md` merge** completed (anchor **lines 18-19 "Topic Overview" section** preserved, rest matching package version) and `topicPaths` / diff conclusion (step 3b; **not executed on fast path**) +- **`projectRev` write-back**: whether the project-side `projectRev` was rewritten to `pkgRev` after the full flow finished (step 3b tail "Write back `projectRev`"; **not executed on fast path**) +- **SKILL self-update**: whether `f2s-kb-upgrade/SKILL.md` was re-read after `init`; whether file changes caused **a rerun from step 2c per the new literal text** and how many rounds (**no second `init`**; see "init and skill self-update"; **this loop is skipped on fast path**) +- manifest / matchers alignment conclusion (from init output) +- Key file verification conclusion +- `.Knowledge/update-check.json` cleanup conclusion (deleted / absent / deletion failed) +- If failed, provide the next executable repair suggestion + +## Output Summary Template (Recommended) + +```markdown +## f2s-kb-upgrade Execution Result + +- **Step -1 global version preflight**: `A Installed & on latest (upgrade skipped) / B Installed but behind (sub-agent dispatched to run npm i -g in background) / C Missing or latest unknown (dispatched / conservative npx)`; current version=``, latest=`` +- Command run inside this skill: `` +- init mode: `incremental` / `overwrite reset (--reset-knowledge)` +- Result: `success` / `failure` +- **Topic-layer judgment**: `projectRev=` vs `pkgRev=` -> `fast path (skipped 3/3a/3b)` / `full flow` / `field-missing fallback` + +### Core Verification +- Old topic files: `cleaned` / `no cleanup needed` / `not executed on fast path` +- Reference fixes: `updated` / `already consistent` / `not executed on fast path` +- **index (snapshot + merge)**: `snapshot copied` / `index.md merged` / `not executed on fast path` / `pending (see notes)` +- **topicMetadata (existing audit)**: `filled` / `pending user confirmation` / `not executed on fast path`; list added / fixed / deleted topicIds +- **topic frontmatter**: `auto-filled N topics` / `already complete` / `strict validation failed` / `not executed on fast path` +- **f2s-kb-upgrade SKILL**: `unchanged after init` / `reran N rounds from step 2c per new SKILL (no second init)` / `loop skipped on fast path` / `pending confirmation` +- **`projectRev` write-back**: `written to project manifest (value=pkgRev)` / `not executed on fast path` / `pkgRev=null, field untouched` +- manifest-routing / matcher shards: `aligned with template` / `already latest` / `reset overwrite` +- topics.path: `all exist` / `missing paths (see below)` +- agent artifacts: `pass` / `issue (see below)` +- update-check cache: `deleted` / `absent` / `delete failed` + +### Notes +- +``` + +## Constraints + +- Do not default to "ask the user to run the command themselves"; the Agent should run it directly. +- Do not execute `--reset-knowledge` without explicit consent. +- Do not modify business code; only verify according to **this `f2s-kb-upgrade`** workflow and result. +- Step 3b `.Knowledge/index.md` merge and `manifest-routing.json` are always written by the main agent (write-authority hard rule); sub-agents may only run shell commands. + +## Completion Self-Check + +1. **Step -1** was performed: before entering step 0, **3 foreground probes** were run sequentially (`flow2spec --version` / `npm view ... version` / `npx` availability) and one of the A/B/C branches was determined. Only under B/C did an **independent sub-agent** get dispatched to run `npm i -g @double-coding/flow2spec@latest` in the background (fire-and-forget); under A **no upgrade action** was dispatched. Step 2's default command form was chosen accordingly (A → `flow2spec init`, B/C → `npx @latest init`); the summary clearly states the branch and version comparison. +2. **Step 0** was performed: V1 did not skip migrate, and **current repositories (V2+)** did not incorrectly run migrate. +3. **Before step 2** recorded the project-side `projectRev` (`projectRev`), and **after step 2 `init`** re-read `pkgRev` and executed **step 2c** judgment. +4. After **step 2 `init`**, **`f2s-kb-upgrade/SKILL.md`** was re-read: on full flow, a change must trigger **a rerun from step 2c per the new literal text** (**no second `init`**); on fast path, the loop can be skipped (see "init and skill self-update" / "fast-path exception"). +5. A shell command was actually executed, not only suggested. +6. Incremental or reset mode was clearly labeled. +7. **On full flow**: old topic-file cleanup and `index/manifest` reference fixes were handled (step 3). +8. **On full flow**: **Step 3a** was executed: `topicMetadata` audited, with no orphan keys / illegal primary / illegal confidence; missing old topics were filled with `inferred` based on evidence or listed as pending confirmation. +9. **On full flow**: `flow2spec kb build --fix-topics` or an equivalent internal capability was executed, followed by `flow2spec kb check --strict`, ensuring existing topics have `revision`. +10. **On full flow**: **Step 3b** was executed: `index.md` was **merged** (from **`Topic Overview`** section through before "Match and Execute" is project-maintained; the rest matches the package version), and `topicPaths` were checked; **at the end of full flow**, the project-side `projectRev` was **written back** to `pkgRev` (if `pkgRev=null`, the field was left unchanged). +11. **On fast path**: steps 3 / 3a / 3b were actually skipped (no unrelated scans), and the summary explicitly labels "not executed on fast path". +12. Manifest and key-path verification results were output. +13. If failed, a concrete next command suggestion was provided. +14. Step 3b `index.md` merge was completed and written by the main agent, with no unauthorized sub-agent write (applies only on full flow). +15. After successful upgrade, `.Knowledge/update-check.json` was deleted to avoid stale upgrade hints in new sessions that day. diff --git a/packages/core/templates/en-US/skills/f2s-req-clarify/SKILL.md b/packages/core/templates/en-US/skills/f2s-req-clarify/SKILL.md new file mode 100644 index 0000000..62d886f --- /dev/null +++ b/packages/core/templates/en-US/skills/f2s-req-clarify/SKILL.md @@ -0,0 +1,32 @@ +--- +name: f2s-req-clarify +description: Clarify a PRD or requirement through follow-up questions until it is actionable, then use f2s-req-tech to produce a technical design; triggers: 需求澄清、PRD 澄清、requirement clarification、PRD clarification +--- + +## Orchestration (main / sub agent) + +- The semantics of `subAgent` / `switchAgentVerification` use the unified entry as the only source of truth: **Cursor/Claude** read the config-root `rules/f2s-flow2spec-unified-entry.*`; **Codex** reads `.codex/topics/f2s-flow2spec-unified-entry.md` (same source, mirrored by `flow2spec init`). This skill does not restate those semantics. +- This skill does **not** split work by default: regardless of the `subAgent` value, the clarification process stays entirely in the main conversation. Follow-up questions and alignment with the user strongly depend on continuous context; sub-agent splitting would break that context. +- Verification is performed by the agent that writes the artifact. This skill does not bind to cross-agent verification. + +# Requirement Clarification + +> Execution scope: clarification documents are written to `.Knowledge/req-docs/`. + +**Input**: Optional. The full PRD, a requirement description, or a document path (for example `.Knowledge/req-docs/xxx.md`). If omitted, clarify based on the current conversation. Later replies may add requirement conditions. + +**Behavior**: Identify vague wording, undefined concepts, missing information, contradictions, and implementation-relevant details that are not specified. Group them, ask concrete answerable follow-up questions, then iterate based on the answers until the flow, boundaries, exceptions, and key concepts are unambiguous. Do not make business assumptions for the user; ask when something is unclear. + +**Completion (write clarification doc → auto-chain to technical design)**: When the information is clear enough, output a Markdown "requirement clarification document" that can be written directly to disk. The document must include at least: background and goals, scope (included / excluded), key flows, boundaries and exceptions, key concept definitions, acceptance criteria, and open questions if any. Save it under `.Knowledge/req-docs/` (recommended name: `_需求澄清.md`). + +**After the clarification document is written to disk, this skill auto-chains to `f2s-req-tech` within the same turn**: feed the just-written clarification path directly into technical-design generation without waiting for another user trigger. Before chaining, emit a one-line notice "Clarification document ready: ``; proceeding to generate the technical design via `f2s-req-tech`", then continue. + +**Exceptions — stay at clarification, do NOT auto-chain to technical design** (any one triggers a stop): +- The clarification document's "open questions" section still has items that materially shape the design structure (e.g., core contracts for tables / APIs / state machines are undefined) — in that case, list the remaining questions, wait for answers, then write and chain; +- The user explicitly says "only produce the clarification / don't rush the design / discuss first" or a similar stop phrase during clarification; +- The user explicitly specifies a different next action (e.g., "stop after clarification", "break down tasks first"). + +**Prohibited**: +- Appending an `f2s-kb-distill` closing hint at the end of the clarification document or immediately after it (see the prohibited section of `rules/f2s-kb-feedback-closing.*` — process-orchestration skills do not trigger distill on write); +- Auto-chaining to `f2s-req-tech` before the clarification document is written to disk (the auto-chain requires an on-disk clarification path); +- Skip-chaining to `f2s-req-plan` / `implement-tech-design` / any other `f2s-*` skill in the same turn (only one hop — to `f2s-req-tech` — is allowed; subsequent skills still require a new user turn). diff --git a/packages/core/templates/en-US/skills/f2s-req-plan/SKILL.md b/packages/core/templates/en-US/skills/f2s-req-plan/SKILL.md new file mode 100644 index 0000000..f9eb769 --- /dev/null +++ b/packages/core/templates/en-US/skills/f2s-req-plan/SKILL.md @@ -0,0 +1,150 @@ +--- +name: f2s-req-plan +description: Plan and implement tasks from a technical design, requirement description, or change description; always maintain `.task/` according to f2s-task; supports parallel sub-agent implementation. Triggers: f2s-req-plan、创建任务、任务规划、我需要任务清单、task planning、create task list +--- + +> **Task paths**: all `.task/` reads/writes must use **`TASK_ROOT` from `rules/f2s-task`** (` .task` or `.task/`; config → git → legacy). Bare `.task/todo.json` / `.task/active/` below mean **`TASK_ROOT/...`**. + + +# Requirement Task Planning and Implementation (f2s-req-plan) + +Start from a requirement or technical design and cover the full "plan -> implement" chain. This skill **does not depend on** `changeTracking.*`, but the full `.task/` lifecycle **must use `f2s-task` as the only source of truth** (directory structure, format, continuation, checkbox updates, archiving, and user-todos). Knowledge-base sync is invoked later by the user as needed through `f2s-kb-feat` / `f2s-kb-sync`. + +## Relationship with f2s-task (Hard Constraint) + +| Item | Description | +| --- | --- | +| **Source of truth** | Config-root **`rules/f2s-task.*`** (`alwaysApply: true`); Codex reads **`.codex/topics/f2s-task.md`** (init mirror, same source as rules) | +| **This skill's responsibility** | Planning draft, code implementation, and sub-agent orchestration; **must not** define a custom `.task/` structure or weaken checkbox/archive requirements | +| **Relationship with changeTracking** | `f2s-req-plan` is **not constrained by** `changeTracking.feat/fix/implement`; it **always** uses task lists. See `f2s-task` "Activation Conditions" | + +**Every client initialized for the project must read the full `f2s-task` text (Step 0 is mandatory, before any step below).** Use the active client's generated rules, `AGENTS.md`, or topic entrypoint; do not substitute this skill's summary for the full text. + +## Orchestration (main / sub agent) + +- `subAgent` / `switchAgentVerification` use the unified entry as the only source of truth: **Cursor/Claude** -> `rules/f2s-flow2spec-unified-entry.*`; **Codex** -> `.codex/topics/f2s-flow2spec-unified-entry.md`. +- **Step 1 (continuation triage + parsing)**: the main agent must perform `f2s-task` "Task Start" 1-2. Document parsing may be split to a sub agent (read-only). +- **Step 2 (draft confirmation)**: must be handled by the main agent. Before confirmation, do not create `.task/` and do not write business code. +- **Step 3 (write task files)**: follow `f2s-task` "Task Start" 3.a-3.f. `todo.json` is **main-agent only**. Drafts of `task.md` / `context.md` / `user-todos.md` may be created by a sub agent; additions to `user-todos.md` during execution are merged by the main agent. +- **Step 4 (implementation)**: sub agents may write only business code. **Sub agents must not** write `todo.json` or modify `task.md` checkboxes. The main agent checks off items after merging. +- **Step 5 (archive)**: main agent only. Execute only after the archive gates in `f2s-task` "Task Completion" are satisfied. +- Worktree hygiene follows `f2s-flow2spec-unified-entry`; interruption/end-of-session handling follows `f2s-task` "Interruption and Session End". + +## Input (Choose One) + +- Technical design path (`.Knowledge/req-docs/*.md` or PDF) +- Requirement / change description (free text) + +## Steps + +### Step 0: Preflight (Mandatory, Before Any Step) + +1. **`Read("flow2spec.config.json")`** (project root; missing fields are treated as `false`). +2. **`Read` the full `f2s-task` text from the active client's generated entrypoint** (do not skip; do not use only this SKILL summary as a substitute). +3. Decide whether to split to sub agents and whether to cross-verify based on the read `subAgent` / `switchAgentVerification` values. + +### Step 1: Continuation Triage + Parse Input + +#### 1a. Continuation Triage (`f2s-task` "Task Start" 1-2, Main Agent) + +1. If **`.task/todo.json`** exists, `Read` it and match the **current user input** against each entry's **`keywords`**. +2. **Exactly 1 match** -> `Read` the corresponding `task.md` and `context.md`; if present, `Read` **`user-todos.md`**. Show the remaining checklist and unchecked user todos, then ask whether to **continue** that task. + - User confirms continuation -> **load the full text of this SKILL** (`linkedSkill` should be `f2s-req-plan`), continue from the first `[ ]` in `task.md`, and **do not** create a duplicate `active/` directory. **Jump to Step 4** (if planning still needs additions, record them first under `## Notes`, then implement). + - User explicitly wants a **new task** -> proceed to 1b. +3. **Multiple matches** -> list candidates and let the user choose which one to continue or choose a new task. +4. **No match** -> check for **orphan `active/`** tasks (`f2s-task`): if any unarchived task has a `task.md` containing `[ ]`, ask whether to continue it or restore `todo.json`; otherwise proceed to 1b. +5. **No `todo.json`** -> proceed to 1b. + +#### 1b. Parse Input (New Task or Draft Needed) + +When `subAgent=true`, read-only parsing may be split to sub agents: + +- Read the full design/requirement and extract goals, scope, work items, and touched files. +- Read `.Knowledge/stock-docs/` and other context for alignment. +- Convert PDFs to MD first with `f2s-doc-pdf`. + +Sub agents return only a "parsing summary"; when `subAgent=false`, the main agent does this work. -> **Step 2**. + +### Step 2: Output Draft and Confirm (Main Agent Required) + +The main agent outputs: + +1. **Task name** (`snake_case`) +2. **Implementation checklist draft** (each step may be a checkbox and will be written into `task.md` under `## Steps`) +3. **Touched file list** (will be written into `context.md`) +4. **Suggested `keywords`** (2-5 terms for continuation matching in `todo.json`) +5. **Wait for user confirmation** + +> Before confirmation, it is forbidden to create `.task/`, write `todo.json`, or write business code. + +### Step 3: Write Task Files (`f2s-task` "Task Start" 3.a-3.f) + +After the user confirms, **strictly follow `f2s-task`** (the format is defined by that rule body; do not omit files): + +| Sub-step | Action | Write authority | +| --- | --- | --- | +| 3.a | Confirm `` (`snake_case`) | Main | +| 3.b | Create `.task/active//` | Main or sub (draft) | +| 3.c | Write **`task.md`**: `# Task Name` + `## Steps` + `- [ ]` list + empty `## Notes` | Main or sub | +| 3.d | Write **`context.md`**: touched files, `.Knowledge` links; user todos point to `user-todos.md` | Main or sub | +| 3.e | Create **`user-todos.md`** (fixed filename; when there are no todos, write a placeholder note) | Main or sub | +| 3.f | Add a **`todo.json` entry**: `name`, `folder`, `keywords` (including Step 2 suggestions), `linkedSkill: "f2s-req-plan"`, `createdAt` | **Main agent only** | + +**Forbidden**: creating only `task.md` without writing `todo.json`; omitting `user-todos.md`; using the old archive name format `completed/-`. + +### Step 4: Implement Code + +Follow `f2s-task` "In Progress" and "Interruption and Session End": + +- Implement in `task.md` order. **Every time a step is truly completed**, the main agent must immediately `Edit` that step from `[ ]` to `[x]` (no batch checking, no oral-only completion). +- For any required user action such as database changes, environment configuration, or approvals, append it to **`user-todos.md`** in the **same session** (group by date). Do not leave it only in the conversation or in `task.md` body. +- When `subAgent=true`: sub agents modify only business source code; after they report back, the main agent checks off tasks and writes `user-todos.md`. +- After merging sub-agent work, clean the **git worktree** (see unified entry). + +### Step 5: Archive the Task (`f2s-task` "Task Completion") + +**Archive gates** (move the directory only after self-check passes): + +- All items related to the current delivery in `task.md` under `## Steps` are **`[x]`** (canceled items are explained under `## Notes`). +- If any `[ ]` remains -> **do not** move to `completed/` and **do not** delete the `todo.json` entry. + +After passing: + +1. `.task/active//` -> `.task/completed/-/` (**8-digit date first**) +2. Remove the entry from `todo.json`; if the array becomes empty, delete the file +3. Archive `user-todos.md` together with the directory + +### Step 6: Output Summary + +```markdown +## f2s-req-plan complete: + +### Implementation +- : + +### Task List +- Archived: `.task/completed/-/` (or, if still active, show the path and remaining `[ ]`) + +### Todo (Knowledge Base) +- You may later call f2s-kb-sync / f2s-kb-feat + +### User Todos +- See `user-todos.md` (after archiving, under the same completed path) +``` + +## Constraints + +- **Step 0**: must first `Read` `flow2spec.config.json` + the **full `f2s-task` text** (three-client paths above). +- **`.task/`**: always obey `f2s-task`; this SKILL must not conflict with it. +- Does not depend on `changeTracking`, but **always** creates and maintains a task list (unless continuing an existing active task). +- Step 2 must be handled by the main agent; before confirmation, no disk writes. +- `todo.json` is main-agent only; sub agents must not write it. +- No batch checkbox updates; do not skip `user-todos.md`. + +## Completion Self-Check + +1. Has the **full `f2s-task` text** been read, and do written files match its format? +2. Are all `task.md` steps checked as `[x]` on disk (not only orally)? +3. When archive gates are satisfied, is the directory under `completed/-/`, and has `todo.json` been updated? +4. Does `user-todos.md` match user todos from the session (placeholder if none)? +5. Is the worktree clean, or have cleanup commands been handed off (mark N/A if not applicable)? diff --git a/packages/core/templates/en-US/skills/f2s-req-tech/SKILL.md b/packages/core/templates/en-US/skills/f2s-req-tech/SKILL.md new file mode 100644 index 0000000..7634534 --- /dev/null +++ b/packages/core/templates/en-US/skills/f2s-req-tech/SKILL.md @@ -0,0 +1,82 @@ +--- +name: f2s-req-tech +description: Generate a technical design document from clarified requirements using the project knowledge base, Skills, and Rules; triggers: 生成技术方案、技术方案、f2s-req-tech、generate technical design、technical design +--- +> Execution scope: business documents live under `/.Knowledge/`; this skill only produces `.Knowledge/req-docs` technical design documents and references knowledge under `.Knowledge`. It does not modify the config-root `rules/skills`. + +## Orchestration (main / sub agent) + +- The semantics of `subAgent` / `switchAgentVerification` use the unified entry as the only source of truth: **Cursor/Claude** read the config-root `rules/f2s-flow2spec-unified-entry.*`; **Codex** reads `.codex/topics/f2s-flow2spec-unified-entry.md` (same source, mirrored by `flow2spec init`). This skill does not restate those semantics. +- **Precondition for splitting (hard constraint)**: when `subAgent=true`, the main agent **must first** extract a "project convention summary" as mandatory context for the sub agent. It must cover: external contract conventions, error and return conventions, async/integration conventions, data and storage conventions, engineering structure, and module boundaries, with a total length **< 80 lines**. If this precondition is not met, **do not split**: the acceptance rework cost is greater than the benefit of splitting. +- **Sub-agent responsibility**: perform multi-source read-only analysis (`.Knowledge/topics`, `stock-docs`, clarified `req-docs`, and template), then write a `.Knowledge/req-docs` technical design draft according to `.Knowledge/template/technical-spec-template.md`. +- **Main-agent responsibility**: finalize the contract, verify against the template and clarification document, and handle consistency of delivery units and flows. +- **Verification**: performed by the writing agent by default. This skill does not bind to cross-agent verification. + +# Generate a Technical Design Document from Requirements + +The user provides a **clarified requirement** in the conversation (or a requirement summary / PRD path), and may optionally attach **requirement conditions** such as scope constraints, required or forbidden technologies, client-side limits, priority, and so on. You need to use the business knowledge documents (`.Knowledge/`) and currently loaded agent rules/skills to output a technical design document that can be used directly for implementation. + +**Purpose**: the technical design produced by this skill is **for later code implementation**. Developers implement the feature according to this document. It is not limited to backend work; it applies to backend, frontend, full-stack, mobile, scripts/tools, and any other scenario. It is not used to generate Rules/Skills. + +**Structural model**: assemble the technical design from the **optional blocks** in `.Knowledge/template/technical-spec-template.md` as needed. **Do not force a fixed section set**: write only the delivery units, data structures, configuration, dependencies, flows, or exception handling that this implementation truly needs. Within each delivery-unit section, describe both the contract/input-output and the necessary processing flow, instead of splitting repeated large chapters such as "API and flow description", "related call flow", or "flow description". + +--- + +## Input + +- **First argument (required)**: the clarified requirement description or a **requirement/PRD document path** (for example `.Knowledge/req-docs/xxx.md` or `.Knowledge/stock-docs/需求_final.md`). +- **Subsequent arguments or user additions (optional)**: requirement conditions and constraints, such as: + - Scope (only a certain module or client) + - Required/forbidden technology stack or API style + - Boundaries with an existing module + - Performance, security, or compliance requirements + +--- + +## Output Structure + +When generating the document, **first read `.Knowledge/template/technical-spec-template.md`** as structural guidance and select its section blocks as needed. Entire sections unrelated to the requirement may be omitted, and new sections not listed in the template may be added according to the project. + +--- + +## Precondition for Sub-Agent Splitting (Optional, Only When `subAgent=true`) + +Before splitting, the main agent must produce a "project convention summary" as **mandatory input** for the sub agent; otherwise, **do not split**. The summary must be **< 80 lines** and include the following six categories (technology-agnostic; fill in concrete values based on the project): + +1. **External contract conventions**: naming, versioning, authentication, pagination, common return fields, and related conventions for APIs / events / messages / components / script entries. +2. **Error and return conventions**: source of the error-code system, prefix/segment rules, required fields (such as code / message / data), and status layering. +3. **Async / integration conventions**: naming, consumer grouping, retry, and idempotency conventions for message queues / event buses / scheduled tasks / external service calls. +4. **Data and storage conventions**: naming for databases / tables / fields / cache / files / search, primary key / index / time-field conventions, and sharding strategy if any. +5. **Engineering structure**: module layering (for example controller / service / dao / domain, or frontend pages / components / hooks / store, or equivalent names) and package-path / directory conventions. +6. **Module boundaries**: call and data boundaries between existing modules involved in this design and other modules. + +Splitting before this precondition is complete violates the hard constraint. Only after the summary is complete may the main agent hand sub-tasks to a sub agent. + +--- + +## Steps + +1. **Clarification-completeness gate (hard constraint)**: Before entering the write phase, decide whether the current requirement is **already clarified**: + - **Clarified** criteria (any one suffices): ① **This turn was auto-chained from `f2s-req-clarify`** with the just-written clarification document path passed in as input (this is the preferred path — the user can flow from `f2s-req-clarify` straight through to the design within one turn); ② the user explicitly provides a path such as `.Knowledge/req-docs/*_需求澄清.md` or an equivalent clarification document; ③ the user explicitly says "already clarified / requirement is settled / just draft the design"; ④ the input itself is a complete PRD (scope, key flows, boundaries, acceptance criteria) and **within this turn** contains no obvious undefined concepts or contradictions. + - **Not-clarified** signals (any one triggers): the requirement description contains hedges such as "I understand it as / I plan to / roughly / probably / to be determined / not decided yet"; interfaces / tables / state machines / interactions with existing modules only state "what to do" without "what counts as done"; the user's input already lists three or more key questions that are still unanswered; and **this turn is not chained from `f2s-req-clarify`**. + - **If not clarified, switch to clarify**: **do not** enter the write phase within the same turn. Instead switch into `f2s-req-clarify` to complete the clarification write, then let its auto-chain rule **come back to this skill within the same turn to continue** (this is the intended direct path; it does not interrupt the user). If switching to clarify is not possible (e.g., the user explicitly says "just do the design; skip clarification"), list 3–6 clarification questions that most affect how the design is written and wait for answers; **do not draft the design**. +2. **Read the requirement**: get requirement content from the path or text provided by the user (or the clarification path handed in by `f2s-req-clarify`); include requirement conditions if any. +3. **Load project context**: actively read and apply: + - Relevant topic rules/flows under `.Knowledge/topics/`; + - Background documents and historical technical designs under `.Knowledge/stock-docs/`; + - **Structural reference** `.Knowledge/template/technical-spec-template.md`. +4. **Align with project conventions**: keep naming conventions, directory structure, configuration conventions, message queues, error codes, data models, and similar items consistent with the existing project. +5. **Write the document**: select and write section blocks from `.Knowledge/template/technical-spec-template.md` as needed. When a delivery unit involves behavior logic, write the processing flow in the same section so the deliverable and flow are not disconnected. If splitting is enabled, the sub agent must use the "project convention summary" plus the clarification document as mandatory input and must not expand the reading scope on its own. +6. **Output location**: default `.Knowledge/req-docs/_技术方案.md`; if the user specifies a path, use that path. +7. **Closing stop (hard constraint)**: After the design is written to disk, output only a single-line hint "Technical design ready: ``; run `f2s-req-plan` to break down tasks, or `implement-tech-design` to implement when you're ready", then **stop**. **Prohibited**: + - Automatically chaining into `f2s-req-plan` / `implement-tech-design` / any other `f2s-*` skill within the same turn (`f2s-req-clarify` → `f2s-req-tech` is the allowed **single hop**; anything after the design requires a new user turn); + - Appending an `f2s-kb-distill` closing hint at the end of the design document or immediately after it (see the prohibited section of `rules/f2s-kb-feedback-closing.*` — process-orchestration skills do not trigger distill on write); + - Proactively listing an "A/B/C next-step menu" that funnels the user straight into the next skill. + +--- + +## Constraints + +- All paths are relative to the project root (same level as `.Knowledge`). +- Do not invent conventions that do not match the project. If uncertain, mark `confirm with project conventions`. +- **Principle**: each delivery-unit section should include the contract (input/output) and processing flow as needed. Do not split them into repeated chapters. Use `.Knowledge/template/technical-spec-template.md` as reference and select blocks as needed; do not force the whole template. diff --git a/packages/core/templates/zh-CN/AGENTS.codex-stub.md b/packages/core/templates/zh-CN/AGENTS.codex-stub.md new file mode 100644 index 0000000..a615c3a --- /dev/null +++ b/packages/core/templates/zh-CN/AGENTS.codex-stub.md @@ -0,0 +1,21 @@ +# Flow2Spec(`.codex/` 目录说明) + +> 本文件为 **指针**,非完整条令。`flow2spec init` 写入;**请勿只读本文件**。 + +## 完整条令 + +仓库根 **[`AGENTS.md`](../AGENTS.md)** 为 Flow2Spec 完整项目说明。在仓库根启动 Codex 时读取该文件。 + +若当前会话未包含根 `AGENTS.md` 全文,**必须先 Read 仓库根 `AGENTS.md`**,再执行 `f2s-*` 或改动 `.Knowledge/`。 + +## 本目录用途 + +| 路径 | 说明 | +| --- | --- | +| `skills/` | Flow2Spec 技能(`f2s-*`) | +| `topics/` | 规则长文镜像(与 Cursor/Claude `rules` 同源) | +| `hooks.json` | Codex SessionStart hook 配置,用于启动时注入配置摘要并检测 Flow2Spec 知识库版本 | +| `hooks/` | hook 脚本目录 | +| `config.toml` | 项目级 Codex 配置(若已创建) | + +配置真值:仓库根 **`flow2spec.config.json`**(须 Read);字段语义表见根 **`AGENTS.md`**。 diff --git a/packages/core/templates/zh-CN/AGENTS.md b/packages/core/templates/zh-CN/AGENTS.md new file mode 100644 index 0000000..a390b16 --- /dev/null +++ b/packages/core/templates/zh-CN/AGENTS.md @@ -0,0 +1,88 @@ +# Flow2Spec 项目入口 + +本文件由 `flow2spec init` 写入仓库根 **`./AGENTS.md`**,作为 Codex 读取的项目入口。**`./.codex/AGENTS.md`** 仅为指针。知识库根目录为 **`./.Knowledge/`**。 + +## 先做这两步 + +1. **本轮首次处理当前仓库相关问题时,先读 `./.Knowledge/manifest-routing.json`。** +2. **执行任何 `f2s-*` 技能前,先 `Read("flow2spec.config.json")`。** + +```text +必须执行:Read(".Knowledge/manifest-routing.json") +必须执行:Read("flow2spec.config.json") ← 仅在进入 f2s-* 技能前 +``` + +禁止在未读 `flow2spec.config.json` 的情况下进入 `f2s-*` 技能正文。 + +## 配置开关(以磁盘为准) + +下表只说明字段语义与 `flow2spec init` 写入的默认值;配置真值仍以本轮 `Read("flow2spec.config.json")` 结果为准(用户可能手工改过)。 + +{{FLOW2SPEC_PROJECT_CONFIG}} + +- `subAgent=true` 时,主 agent 必须在技能前段**显式判断一次**本次是否拆子,并说明原因;即使判断不拆,也必须输出不拆原因。`subAgent=false` 时不得拆子 agent。 +- `intentRecognition=false` 或字段缺失时,禁止自动进入任何 skill;只能按用户显式触发或当前规则允许的高置信分流进入。 + +配置细表与补充规则见 **`./.codex/topics/f2s-config-check.md`**。 + +## KB 路由规则 + +- 机读事实源只认 **`./.Knowledge/manifest-routing.json`** 与其 `matcherPath` 指向的 **`./.Knowledge/matchers/*.json`**。 +- 按 `match -> expand -> verify -> act` 执行:主命中后先展开 `topicDependencies`,再检查是否缺关键上下文。 +- 仅在以下情况允许跨 matcher 全量补检索:无命中、主次候选过近、缺口检查失败、用户明确要求“全量检查/不要遗漏”。 +- `fallbackTopic` 仅作低置信兜底,不能直接作为最终执行依据。 + +## 普通问答收口门禁 + +- 普通问答 / 排查 / 解释若需要下钻业务源码,先按 **`./.codex/topics/f2s-knowledge-preflight.md`** 执行首读与缺口说明。 +- 只要本轮读取过业务源码,且最终答案引用了源码事实,发出答案前必须按 **`./.codex/topics/f2s-kb-feedback-closing.md`** 四 case 收口;答案末尾必须显式输出 **`知识库补充建议`** 或 **`知识库已覆盖`**,不得静默省略。 +- 已进入 `f2s-*` 技能、`implement-tech-design`、`f2s-git-commit` 或其他已有后续流程时,不重复追加普通问答收口提示。 + +## 渐进式读取顺序 + +1. `./.Knowledge/manifest-routing.json` +2. 命中规则的 `./.Knowledge/matchers/.json` +3. 相关 `./.Knowledge/topics/.md` +4. 仅在 topic 指向或上下文不足时再读 `./.Knowledge/index.md` / `stock-docs` / `req-docs` +5. 最后才下钻业务代码 + +禁止跳过 `manifest-routing.json` 直接全仓搜索。 +禁止把 `./.Knowledge/stock-docs/` 作为“按方案实现代码”的直接输入。 +同一任务线内不要反复全文读取 `manifest-routing.json`,除非用户明确说路由/知识已更新。 + +## 执行依据 + +- Flow2Spec 执行依据只认: + - 仓库根 **`./AGENTS.md`** + - **`./.codex/topics/f2s-*.md`** + - **`./.codex/skills/`** +- **`.codex/AGENTS.md`** 仅为目录指针,不能替代根 `AGENTS.md`。 + +## Codex 规则镜像(按需打开) + +这些文件由 `flow2spec init codex` 从规则模板镜像到 `.codex/topics/`。它们不会自动全文加载;当前任务需要细则时再打开。 + +| 规则 | 路径 | 什么时候读 | +| --- | --- | --- | +| 统一入口 | `./.codex/topics/f2s-flow2spec-unified-entry.md` | 执行 `f2s-*` 技能、判断 KB 路由 / 子 agent / 校验语义时 | +| 配置前置 | `./.codex/topics/f2s-config-check.md` | 核对 `flow2spec.config.json`、`subAgent`、`changeTracking` 细则时 | +| 普通问答首读门禁 | `./.codex/topics/f2s-knowledge-preflight.md` | 普通问答要下钻源码前 | +| 普通问答收口 | `./.codex/topics/f2s-kb-feedback-closing.md` | 普通问答读取源码后判断是否建议补知识库 | +| 意图识别 | `./.codex/topics/f2s-intent-routing.md` | 仅当 `intentRecognition=true`,需要判断是否自动进入 skill 时 | + +`implement-tech-design`、`f2s-doc-routing` 等长文按命中 topic 再打开,不必默认通读。 + +## Codex Hooks + +`flow2spec init codex` 会写入 **`.codex/hooks.json`**。当前 Flow2Spec 在 Codex 侧只把 hooks 用于: + +- `SessionStart` 配置摘要提醒:`.codex/hooks/f2s-config-session.js` +- `SessionStart` 知识库版本检查:`.codex/hooks/f2s-update-check.js` + +这些 hook 只做提醒 / 检测,不替代 `Read("flow2spec.config.json")` 与 KB 路由门禁。 + +## Flow2Spec 技能 + +可用技能位于 **`./.codex/skills/`**。仅在用户显式触发或当前规则允许自动分流时进入对应 skill。 + +{{FLOW2SPEC_CODEX_SKILLS_SUMMARY}} diff --git a/packages/core/templates/zh-CN/flow2spec.config.json b/packages/core/templates/zh-CN/flow2spec.config.json new file mode 100644 index 0000000..e8c0197 --- /dev/null +++ b/packages/core/templates/zh-CN/flow2spec.config.json @@ -0,0 +1,18 @@ +{ + "locale": "zh-CN", + "subAgent": true, + "switchAgentVerification": true, + "intentRecognition": true, + "changeTracking": { + "feat": true, + "fix": false, + "implement": true + }, + "updateCheck": { + "enabled": true + }, + "collaboration": { + "enabled": true, + "developerId": "" + } +} diff --git a/packages/core/templates/zh-CN/hooks/f2s-config-inject.js b/packages/core/templates/zh-CN/hooks/f2s-config-inject.js new file mode 100644 index 0000000..5df2768 --- /dev/null +++ b/packages/core/templates/zh-CN/hooks/f2s-config-inject.js @@ -0,0 +1,43 @@ +#!/usr/bin/env node +'use strict'; +/** + * flow2spec PreToolUse guard — 仅在调用 f2s-* Skill 前提示必须先 Read flow2spec.config.json。 + * 不在 PreToolUse 中反复注入完整配置;配置摘要由 SessionStart hook 一次性提供。 + * 由 flow2spec init --claude 写入 .claude/hooks/f2s-config-inject.js。 + */ + +function emitAdditionalContext(lines) { + process.stdout.write( + JSON.stringify({ + hookSpecificOutput: { + hookEventName: 'PreToolUse', + additionalContext: lines.join('\n'), + }, + }) + '\n', + ); +} + +const chunks = []; +process.stdin.on('data', (d) => chunks.push(d)); +process.stdin.on('end', () => { + let skillName = ''; + try { + const input = JSON.parse(Buffer.concat(chunks).toString('utf8')); + skillName = String(input?.tool_input?.skill || input?.tool_input?.name || ''); + } catch (_err) { + process.exit(0); + return; + } + + if (!/^f2s-/.test(skillName)) { + process.exit(0); + return; + } + + emitAdditionalContext([ + `[flow2spec] 即将调用 ${skillName}。进入该 Skill 正文前,首个动作必须 Read("flow2spec.config.json")。`, + 'SessionStart 中的配置摘要仅作提醒;若摘要与磁盘不一致,以本次 Read 结果为准。', + '读取后再按 subAgent / switchAgentVerification / changeTracking 的实际值执行后续步骤。', + ]); + process.exit(0); +}); diff --git a/packages/core/templates/zh-CN/hooks/f2s-config-session.js b/packages/core/templates/zh-CN/hooks/f2s-config-session.js new file mode 100644 index 0000000..e586a4d --- /dev/null +++ b/packages/core/templates/zh-CN/hooks/f2s-config-session.js @@ -0,0 +1,95 @@ +#!/usr/bin/env node +'use strict'; +/** + * flow2spec SessionStart hook — 会话开始时一次性注入 flow2spec.config.json 摘要。 + * 该摘要不替代 f2s-* Skill 正文前的 Read("flow2spec.config.json")。 + * 由 flow2spec init 写入对应 agent 的 hooks/f2s-config-session.js。 + */ +const fs = require('fs'); +const path = require('path'); + +const DEFAULT_CFG = { + subAgent: false, + switchAgentVerification: false, + changeTracking: { feat: true, fix: false, implement: true }, +}; + +function normalizeBool(value, fallback) { + if (value === true || value === 'true' || value === 1 || value === '1') + return true; + if (value === false || value === 'false' || value === 0 || value === '0') + return false; + return fallback; +} + +function normalizeCfg(raw) { + if (!raw || typeof raw !== 'object' || Array.isArray(raw)) { + return { ...DEFAULT_CFG, changeTracking: { ...DEFAULT_CFG.changeTracking } }; + } + const ct = raw.changeTracking; + let changeTracking = { ...DEFAULT_CFG.changeTracking }; + if (typeof ct === 'boolean') { + changeTracking = { + feat: normalizeBool(ct, DEFAULT_CFG.changeTracking.feat), + fix: normalizeBool(ct, DEFAULT_CFG.changeTracking.fix), + implement: normalizeBool(ct, DEFAULT_CFG.changeTracking.implement), + }; + } else if (ct && typeof ct === 'object' && !Array.isArray(ct)) { + changeTracking = { + feat: normalizeBool(ct.feat, DEFAULT_CFG.changeTracking.feat), + fix: normalizeBool(ct.fix, DEFAULT_CFG.changeTracking.fix), + implement: normalizeBool(ct.implement, DEFAULT_CFG.changeTracking.implement), + }; + } + const switchRaw = Object.prototype.hasOwnProperty.call(raw, 'switchAgentVerification') + ? raw.switchAgentVerification + : raw.subAgentVerification; + return { + subAgent: normalizeBool(raw.subAgent, DEFAULT_CFG.subAgent), + switchAgentVerification: normalizeBool( + switchRaw, + DEFAULT_CFG.switchAgentVerification, + ), + changeTracking, + }; +} + +function emit(lines) { + process.stdout.write( + JSON.stringify({ + hookSpecificOutput: { + hookEventName: 'SessionStart', + additionalContext: lines.join('\n'), + }, + }) + '\n', + ); +} + +function main() { + const configPath = path.resolve(process.cwd(), 'flow2spec.config.json'); + if (!fs.existsSync(configPath)) { + const cfg = { ...DEFAULT_CFG, changeTracking: { ...DEFAULT_CFG.changeTracking } }; + emit([ + '[flow2spec] 本会话未找到 flow2spec.config.json;f2s-* Skill 前仍必须尝试 Read("flow2spec.config.json"),缺失字段按默认值处理。', + `配置摘要:subAgent=${cfg.subAgent}, switchAgentVerification=${cfg.switchAgentVerification}, changeTracking=${JSON.stringify(cfg.changeTracking)}`, + ]); + return; + } + + try { + const cfg = normalizeCfg(JSON.parse(fs.readFileSync(configPath, 'utf8'))); + emit([ + '[flow2spec] SessionStart 配置摘要(仅作提醒,执行 f2s-* Skill 前仍必须 Read 磁盘文件):', + `subAgent=${cfg.subAgent}`, + `switchAgentVerification=${cfg.switchAgentVerification}`, + `changeTracking=${JSON.stringify(cfg.changeTracking)}`, + ]); + } catch (err) { + emit([ + `[flow2spec] flow2spec.config.json 解析失败:${err.message || String(err)}`, + '执行 f2s-* Skill 前必须先修复或 Read 该文件;无法读取时按缺失字段默认值处理。', + ]); + } +} + +main(); diff --git a/packages/core/templates/zh-CN/hooks/f2s-update-check.js b/packages/core/templates/zh-CN/hooks/f2s-update-check.js new file mode 100644 index 0000000..61785b8 --- /dev/null +++ b/packages/core/templates/zh-CN/hooks/f2s-update-check.js @@ -0,0 +1,187 @@ +#!/usr/bin/env node +'use strict'; +/** + * flow2spec SessionStart hook — 每天第一次对话时检查版本更新。 + * 比较本地知识库 manifest-routing.json 的 version 与 npm 最新版本: + * - 一致或本地更新 → 静默退出 + * - 落后 → 向 Agent 上下文注入一行提示(建议执行 f2s-kb-upgrade) + * 已检查过且无需升级则静默;已检查且仍需升级时,每个新会话继续注入提醒。 + * 由 flow2spec init 写入对应 agent 的 hooks/f2s-update-check.js。 + */ +const fs = require('fs'); +const path = require('path'); +const { execFileSync } = require('child_process'); + +const MANIFEST_PATH = path.join(process.cwd(), '.Knowledge', 'manifest-routing.json'); +const CACHE_DIR = path.join(process.cwd(), '.Knowledge'); +const CACHE_FILE = path.join(CACHE_DIR, 'update-check.json'); +const PACKAGE_NAME_PLACEHOLDER = '__FLOW2SPEC_' + 'PACKAGE_NAME__'; +const PACKAGE_NAME = '__FLOW2SPEC_PACKAGE_NAME__'; + +// ── 缓存 ──────────────────────────────────────────────────────────────────── + +function readCache() { + if (!fs.existsSync(CACHE_FILE)) return null; + try { + const d = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8')); + if (!d || typeof d !== 'object') return null; + const checkedAt = Number(d.checkedAt || 0); + if (!checkedAt) return null; + if (new Date(checkedAt).toDateString() !== new Date().toDateString()) return null; + return d; + } catch (_) { return null; } +} + +function getProjectName() { + try { + const pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8')); + if (pkg && pkg.name) return String(pkg.name); + } catch (_) {} + return path.basename(process.cwd()); +} + +function buildNotice(latestNpm, manifestVersion) { + const userMsg = `📦 [flow2spec] 当前项目「${getProjectName()}」知识库版本 v${manifestVersion},低于最新包版本 v${latestNpm}。可执行 f2s-kb-upgrade skill 对齐模板与路由。`; + return [ + `[flow2spec/agent-instruction] 你必须在本轮回复开头将下列升级提示原文转告用户,不得省略、不得仅在内部判断后跳过:`, + ``, + userMsg, + ].join('\n'); +} + +function emitNotice(notice) { + process.stdout.write( + JSON.stringify({ + additional_context: notice, + hookSpecificOutput: { + hookEventName: 'SessionStart', + additionalContext: notice, + }, + }) + '\n' + ); +} + +function writeCache(latestNpm, manifestVersion) { + try { + const needsUpgrade = cmpVer(manifestVersion, latestNpm) < 0; + fs.writeFileSync( + CACHE_FILE, + `${JSON.stringify({ + latestNpm, + manifestVersion, + needsUpgrade, + notice: needsUpgrade ? buildNotice(latestNpm, manifestVersion) : '', + checkedAt: Date.now(), + }, null, 2)}\n`, + 'utf8' + ); + } catch (_) {} +} + +function deleteCache() { + try { + if (fs.existsSync(CACHE_FILE)) fs.unlinkSync(CACHE_FILE); + } catch (_) {} +} + +// ── 版本比较 ───────────────────────────────────────────────────────────────── + +function parseVer(v) { + return String(v || '').replace(/^v/, '').split(/[.-]/).slice(0, 3).map((p) => { + const n = Number.parseInt(p, 10); + return Number.isFinite(n) ? n : 0; + }); +} + +/** a < b → 负数;a === b → 0;a > b → 正数 */ +function cmpVer(a, b) { + const av = parseVer(a), bv = parseVer(b); + for (let i = 0; i < 3; i++) { + const d = (av[i] || 0) - (bv[i] || 0); + if (d !== 0) return d; + } + return 0; +} + +// ── 读取 ───────────────────────────────────────────────────────────────────── + +function getManifestVersion() { + if (!fs.existsSync(MANIFEST_PATH)) return null; + try { + return JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8')).version || null; + } catch (_) { return null; } +} + +function getPackageName() { + if (PACKAGE_NAME && PACKAGE_NAME !== PACKAGE_NAME_PLACEHOLDER) { + return PACKAGE_NAME; + } + return '@double-coding/flow2spec'; +} + +function queryNpmLatest(pkgName) { + return execFileSync('npm', ['view', pkgName, 'version'], { + encoding: 'utf8', + timeout: 5000, + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); +} + +// ── 配置开关 ────────────────────────────────────────────────────────────────── + +function isEnabled() { + try { + const cfg = JSON.parse(fs.readFileSync( + path.join(process.cwd(), 'flow2spec.config.json'), 'utf8' + )); + const uc = cfg && cfg.updateCheck; + if (uc && typeof uc.enabled === 'boolean') return uc.enabled; + return true; + } catch (_) { return true; } +} + +// ── 主流程 ──────────────────────────────────────────────────────────────────── + +function main() { + if (process.env.CI || process.env.CONTINUOUS_INTEGRATION) return; + if (!isEnabled()) return; + const cache = readCache(); + if (cache) { + // 今天已检查过则不重复查 npm;若缓存显示仍需升级,每个新会话继续提醒。 + const needsUpgrade = cache.needsUpgrade === true || + cmpVer(cache.manifestVersion, cache.latestNpm) < 0; + if (needsUpgrade) { + const currentManifestVersion = getManifestVersion(); + if (currentManifestVersion && cache.latestNpm && + cmpVer(currentManifestVersion, cache.latestNpm) >= 0) { + deleteCache(); + return; + } + // SessionStart 进入新会话:缓存命中且仍需升级,直接 emit。 + const notice = buildNotice(cache.latestNpm, cache.manifestVersion); + emitNotice(notice); + } + return; + } + + const manifestVersion = getManifestVersion(); + if (!manifestVersion) return; // 无知识库,跳过 + + let latestNpm; + try { + const pkgName = getPackageName(); + latestNpm = queryNpmLatest(pkgName); + } catch (_) { + return; // 网络不通,静默退出,不写缓存(下次还会重试) + } + + // 写缓存(无论是否需要升级,今天不再重复检查) + writeCache(latestNpm, manifestVersion); + + if (cmpVer(manifestVersion, latestNpm) >= 0) return; // 已是最新 + + const notice = buildNotice(latestNpm, manifestVersion); + emitNotice(notice); +} + +main(); diff --git a/packages/core/templates/zh-CN/knowledge/index.md b/packages/core/templates/zh-CN/knowledge/index.md new file mode 100644 index 0000000..bce6b9d --- /dev/null +++ b/packages/core/templates/zh-CN/knowledge/index.md @@ -0,0 +1,71 @@ +# Flow2Spec Knowledge Index + +> **路径约定**:下文 **`.Knowledge/`**、**`manifest-routing.json`** 等路径均相对于**本仓库根目录**(即已运行 `flow2spec init` 的当前项目)。 + +本文件是 **人读导航**:主题说明、关联文档摘要、语义边界。 +**机读事实源** 以 `.Knowledge/manifest-routing.json` + `taskToTopicRules[].matcherPath` 指向的 `.Knowledge/matchers/*.json` 分片为准(不再使用 `.Knowledge/manifest-matchers.json`)。 + +--- + +## 推荐阅读顺序 + +1. `.Knowledge/manifest-routing.json`(任务路由、`topicPaths`、`topicDependencies`、`fallbackTopic`) +2. 按需:由 `matcherPath` 读取 `.Knowledge/matchers/.json`(`includeAny` 关键词) +3. 按需:本 `index.md`(主题语义与边界) +4. `.Knowledge/topics/.md`(执行约束与流程) +5. 按需:`.Knowledge/stock-docs/`、`.Knowledge/req-docs/` +6. 仍不足再下钻业务代码 + +--- + +## 主题一览 + +| 主题 | 路径 | 适用场景 | 关联文档(摘要) | +| --- | --- | --- | --- | +| implement-tech-design | `.Knowledge/topics/f2s-implement-tech-design.md` | 按技术方案实现代码 | req:[技术方案](.Knowledge/req-docs/<技术方案>.md)(必填) | +| f2s-doc-routing | `.Knowledge/topics/f2s-stock-docs-vs-req-docs.md` | stock-docs / req-docs 目录分工 | stock:[目录边界说明](.Knowledge/stock-docs/<目录边界说明>.md)(可选) | +| fallback-triage | `.Knowledge/topics/f2s-fallback-triage.md` | 未命中或低置信度:分诊与澄清 | stock:[路由分诊说明](.Knowledge/stock-docs/<分诊说明>.md)(可选) | +| config-precheck | `.Knowledge/topics/f2s-config-precheck.md` | 执行 `f2s-*` 前读 `flow2spec.config.json` / 编排开关 | Codex 长文:仓库根 `.codex/topics/f2s-config-check.md`;[路由摘要](topics/f2s-config-precheck.md) | +| f2s-task | `.Knowledge/topics/f2s-task.md` | 变更追踪、`.task/` 任务清单与跨会话续作 | 长文:配置根 `rules/f2s-task.*`;Codex:`.codex/topics/f2s-task.md` | +| f2s-req-plan | `.Knowledge/topics/f2s-req-plan.md` | 需求/方案规划与实现;始终维护 `.task/` | 技能:`skills/f2s-req-plan/SKILL.md`;依赖 `f2s-task` | +| flow2spec-dsh-adapter | `.Knowledge/topics/flow2spec-dsh-adapter.md` | `flow2spec init dsh` 与 DeepSeek Harness 项目技能发现 | 用户文档:`docs/使用说明.md`;实现:`lib/dshAgentsAdapter.js` | + +每主题保留 **1–3 条** 可点击摘要链接;全量路径对照写入 `.Knowledge/migration-report.md`(迁移场景)。 +其中 **`implement-tech-design`**、**`f2s-doc-routing`**、**`config-precheck`**、**`f2s-task`** 在 `topics/` 内为**路由摘要**;执行长文见配置根 **`rules/f2s-*.md(c)`**;使用 Codex 时见 **`.codex/AGENTS.md`**、**`.codex/topics/f2s-*.md`**(`f2s-config-check` 与 `AGENTS` 前置同源,按需打开)。**`f2s-knowledge-preflight`** 与 **`f2s-kb-feedback-closing`** 是普通问答首读 / 源码补答收口门禁,作为配置根规则 / Codex 专题长文生效,不写入 `topicPaths` 或 `taskToTopicRules`。 + +--- + +## 命中与执行(与统一入口一致) + +- **路由**:`taskToTopicRules` 给出任务 → 主题集合;**关键词**在 matcher 分片的 `includeAny`。 +- **依赖**:命中主主题前,按 `topicDependencies` 先读依赖主题。 +- **兜底**:`fallbackTopic` 指向分诊主题(如 `fallback-triage`),仅低置信度上下文,**不得**当作最终命中直接改代码。 +- **执行链**:`match → expand → verify → act`;`expand` 须含依赖展开,并保留次高候选做校验。 +- **全量补检索**:仅当无命中、候选分差过小、缺口检查失败,或用户明确要求「全量检查」时允许跨 matcher 补检索。 + +--- + +## 目录职责 + +| 目录 | 职责 | +| --- | --- | +| `topics/` | 专题规则与执行流程 | +| `matchers/` | matcher 分片(`matcherPath` 指向) | +| `stock-docs/` | 存量沉淀(架构、终稿等) | +| `req-docs/` | 需求与技术方案(驱动实现) | +| `template/` | 终稿与方案模版 | + +路由清单由 `f2s-*` 技能链路维护,不依赖额外 CLI 子命令。 + +--- + +## 常见缺口怎么处理(与统一入口一致) + +| 情况 | 你怎么做 | +| --- | --- | +| 有文档但没配到(1a) | 维护侧:`f2s-kb-build` / `f2s-kb-sync` / `f2s-kb-add` 补路由与 `includeAny`。执行侧:分诊主题澄清任务类型,**不**用全仓扫替代 manifest。 | +| 配到了但不够(1b) | 走依赖与次高候选 → `verify` 点名缺哪篇文档;仍缺则向用户要路径或补 `req-docs`。 | +| 库里没有(2) | 承认缺口 → 代码下钻或请用户补需求/方案文档。 | +| 反复读 manifest 费 token(2a) | 同一任务线内 routing 只当快照;只读命中项的单个 matcher;不遍历整个 `matchers/` 目录枚举;`index.md` 勿与 routing 循环互刷。 | + +**说明**:「路由/知识已更新」指 `f2s-*`(如 `f2s-kb-build`、`f2s-kb-sync`、`f2s-kb-add`、`f2s-kb-fix` 等)产出或手改 `manifest-routing` / `matchers` 分片;**`flow2spec init` 不撰写业务文档**,以模板补齐与配置根落盘为主,勿与知识库内容更新混为一谈。 diff --git a/packages/core/templates/zh-CN/knowledge/manifest-matchers.json b/packages/core/templates/zh-CN/knowledge/manifest-matchers.json new file mode 100644 index 0000000..b2555c3 --- /dev/null +++ b/packages/core/templates/zh-CN/knowledge/manifest-matchers.json @@ -0,0 +1,65 @@ +{ + "version": "1.0.0", + "generatedFrom": ".Knowledge/manifest-routing.json", + "matcherKey": "matcherId", + "sourceOfTruth": ".Knowledge/manifest-routing.json", + "matchers": { + "m-implement-from-spec": { + "includeAny": [ + "按技术方案实现", + "实现接口", + "需求文档开发" + ] + }, + "m-doc-routing": { + "includeAny": [ + "文档放哪", + "stock-docs", + "req-docs", + "目录约定" + ] + }, + "m-f2s-config-precheck": { + "includeAny": [ + "flow2spec.config.json", + "subAgent", + "switchAgentVerification", + "切换 agent 校验", + "技能前置", + "f2s-config-check", + "f2s-config-inject", + "changeTracking" + ] + }, + "m-change-tracking": { + "includeAny": [ + "changeTracking", + "变更追踪", + "任务追踪", + "任务清单", + ".task", + "续作", + "继续上次任务", + "todo.json", + "task.md", + "f2s-task", + "任务清单归档", + "跨会话" + ] + }, + "m-req-plan": { + "includeAny": [ + "f2s-req-plan", + "任务规划", + "任务清单设计", + "需求规划", + "创建任务清单", + "规划需求", + "req-plan", + "需求实现", + "按需求实现", + "按方案规划" + ] + } + } +} \ No newline at end of file diff --git a/packages/core/templates/zh-CN/knowledge/manifest-routing.json b/packages/core/templates/zh-CN/knowledge/manifest-routing.json new file mode 100644 index 0000000..41feb7f --- /dev/null +++ b/packages/core/templates/zh-CN/knowledge/manifest-routing.json @@ -0,0 +1,110 @@ +{ + "version": "3.1.5", + "projectRev": 2, + "knowledgeRoot": ".Knowledge", + "matcherKey": "matcherId", + "sourceOfTruth": ".Knowledge/manifest-routing.json", + "fallbackTopic": "fallback-triage", + "topicDependencies": { + "implement-tech-design": [ + "f2s-doc-routing" + ], + "f2s-req-plan": [ + "f2s-task" + ] + }, + "topicMetadata": { + "implement-tech-design": { + "primary": "policy", + "confidence": "manual" + }, + "f2s-doc-routing": { + "primary": "policy", + "confidence": "manual" + }, + "fallback-triage": { + "primary": "policy", + "confidence": "manual" + }, + "config-precheck": { + "primary": "config", + "tags": [ + "policy" + ], + "confidence": "manual" + }, + "f2s-task": { + "primary": "policy", + "confidence": "manual" + }, + "f2s-req-plan": { + "primary": "policy", + "confidence": "manual" + }, + "flow2spec-dsh-adapter": { + "primary": "feature", + "confidence": "inferred", + "tags": ["module"] + } + }, + "topicPaths": { + "implement-tech-design": ".Knowledge/topics/f2s-implement-tech-design.md", + "f2s-doc-routing": ".Knowledge/topics/f2s-stock-docs-vs-req-docs.md", + "fallback-triage": ".Knowledge/topics/f2s-fallback-triage.md", + "config-precheck": ".Knowledge/topics/f2s-config-precheck.md", + "f2s-task": ".Knowledge/topics/f2s-task.md", + "f2s-req-plan": ".Knowledge/topics/f2s-req-plan.md", + "flow2spec-dsh-adapter": ".Knowledge/topics/flow2spec-dsh-adapter.md" + }, + "taskToTopicRules": [ + { + "task": "f2s-config-precheck", + "matcherId": "m-f2s-config-precheck", + "matcherPath": ".Knowledge/matchers/m-f2s-config-precheck.json", + "topics": [ + "config-precheck" + ] + }, + { + "task": "implement-from-spec", + "matcherId": "m-implement-from-spec", + "matcherPath": ".Knowledge/matchers/m-implement-from-spec.json", + "topics": [ + "f2s-doc-routing", + "implement-tech-design" + ] + }, + { + "task": "doc-routing", + "matcherId": "m-doc-routing", + "matcherPath": ".Knowledge/matchers/m-doc-routing.json", + "topics": [ + "f2s-doc-routing" + ] + }, + { + "task": "change-tracking", + "matcherId": "m-change-tracking", + "matcherPath": ".Knowledge/matchers/m-change-tracking.json", + "topics": [ + "f2s-task" + ] + }, + { + "task": "req-plan", + "matcherId": "m-req-plan", + "matcherPath": ".Knowledge/matchers/m-req-plan.json", + "topics": [ + "f2s-req-plan" + ] + }, + { + "task": "flow2spec-dsh-adapter", + "matcherId": "m-flow2spec-dsh-adapter", + "matcherPath": ".Knowledge/matchers/m-flow2spec-dsh-adapter.json", + "topics": [ + "flow2spec-dsh-adapter" + ] + } + ] +} diff --git a/packages/core/templates/zh-CN/knowledge/matchers/m-change-tracking.json b/packages/core/templates/zh-CN/knowledge/matchers/m-change-tracking.json new file mode 100644 index 0000000..c1bf1f7 --- /dev/null +++ b/packages/core/templates/zh-CN/knowledge/matchers/m-change-tracking.json @@ -0,0 +1,20 @@ +{ + "id": "m-change-tracking", + "includeAny": [ + "changeTracking", + "变更追踪", + "任务追踪", + "任务清单", + ".task", + "续作", + "继续上次任务", + "todo.json", + "task.md", + "f2s-task", + "任务清单归档", + "跨会话", + "验收清单", + "acceptance.md", + "归档前验收" + ] +} diff --git a/packages/core/templates/zh-CN/knowledge/matchers/m-doc-routing.json b/packages/core/templates/zh-CN/knowledge/matchers/m-doc-routing.json new file mode 100644 index 0000000..a95e0cd --- /dev/null +++ b/packages/core/templates/zh-CN/knowledge/matchers/m-doc-routing.json @@ -0,0 +1,11 @@ +{ + "id": "m-doc-routing", + "version": "1.0.0", + "schema": "flow2spec.matcher.v1", + "includeAny": [ + "文档放哪", + "stock-docs", + "req-docs", + "目录约定" + ] +} diff --git a/packages/core/templates/zh-CN/knowledge/matchers/m-f2s-config-precheck.json b/packages/core/templates/zh-CN/knowledge/matchers/m-f2s-config-precheck.json new file mode 100644 index 0000000..287d590 --- /dev/null +++ b/packages/core/templates/zh-CN/knowledge/matchers/m-f2s-config-precheck.json @@ -0,0 +1,15 @@ +{ + "includeAny": [ + "flow2spec.config.json", + "subAgent", + "switchAgentVerification", + "切换 agent 校验", + "技能前置", + "f2s-config-check", + "f2s-config-inject", + "changeTracking" + ], + "id": "m-f2s-config-precheck", + "version": "1.0.0", + "schema": "flow2spec.matcher.v1" +} diff --git a/packages/core/templates/zh-CN/knowledge/matchers/m-flow2spec-dsh-adapter.json b/packages/core/templates/zh-CN/knowledge/matchers/m-flow2spec-dsh-adapter.json new file mode 100644 index 0000000..0ff8c89 --- /dev/null +++ b/packages/core/templates/zh-CN/knowledge/matchers/m-flow2spec-dsh-adapter.json @@ -0,0 +1,6 @@ +{ + "includeAny": ["DeepSeek Harness", "deepseek-harness", "dsh", "flow2spec init dsh", ".dsh/skills", ".dsh/topics", "Cordis 插件", "Harness 适配"], + "id": "m-flow2spec-dsh-adapter", + "version": "1.0.0", + "schema": "flow2spec.matcher.v1" +} diff --git a/packages/core/templates/zh-CN/knowledge/matchers/m-implement-from-spec.json b/packages/core/templates/zh-CN/knowledge/matchers/m-implement-from-spec.json new file mode 100644 index 0000000..7a55734 --- /dev/null +++ b/packages/core/templates/zh-CN/knowledge/matchers/m-implement-from-spec.json @@ -0,0 +1,10 @@ +{ + "id": "m-implement-from-spec", + "version": "1.0.0", + "schema": "flow2spec.matcher.v1", + "includeAny": [ + "按技术方案实现", + "实现接口", + "需求文档开发" + ] +} diff --git a/packages/core/templates/zh-CN/knowledge/matchers/m-req-plan.json b/packages/core/templates/zh-CN/knowledge/matchers/m-req-plan.json new file mode 100644 index 0000000..1e22fe1 --- /dev/null +++ b/packages/core/templates/zh-CN/knowledge/matchers/m-req-plan.json @@ -0,0 +1,15 @@ +{ + "id": "m-req-plan", + "includeAny": [ + "f2s-req-plan", + "任务规划", + "任务清单设计", + "需求规划", + "创建任务清单", + "规划需求", + "req-plan", + "需求实现", + "按需求实现", + "按方案规划" + ] +} diff --git "a/packages/core/templates/zh-CN/knowledge/template/\346\212\200\346\234\257\346\226\271\346\241\210\346\250\241\347\211\210.md" "b/packages/core/templates/zh-CN/knowledge/template/\346\212\200\346\234\257\346\226\271\346\241\210\346\250\241\347\211\210.md" new file mode 100644 index 0000000..65b2b7d --- /dev/null +++ "b/packages/core/templates/zh-CN/knowledge/template/\346\212\200\346\234\257\346\226\271\346\241\210\346\250\241\347\211\210.md" @@ -0,0 +1,89 @@ +> **主口径(统一知识库)**:技术方案模板与产物统一维护在 `/.Knowledge/template/` 与 `/.Knowledge/req-docs/`。 + +# 技术方案模版 + +> 供 **f2s-req-tech** 技能使用。主模板路径为 `.Knowledge/template/技术方案模版.md`;配置根不再写入 `template/` 副本。**章节为可选积木**:与需求无关的整节可省略,按需增加项目特有章节;章节顺序建议如下,可按实际调整。不要为了套模板强行生成接口、数据库、错误码或消息队列章节。 + +--- + +## 1. 文档标题 + +- 一级标题:`# <活动/需求名> 技术方案` + +--- + +## 2. 需求概述 + +- 二级标题:`## 需求概述` +- 用无序列表或短段落写清:背景、目标、范围、**明确不做什么**。 + +--- + +## 3. 重点问题概述 + +- 二级标题:`## 重点问题概述` +- 技术难点、并发/一致性/性能、与现有模块边界、风险与取舍(列表即可)。 +- **何时填**:存在需要决策的技术权衡时;无明显难点可省略。 + +--- + +## 4. 外部依赖与内部调用 + +- 二级标题:`## 外部依赖与内部调用` +- 简要列出:依赖的外部服务(HTTP/RPC/第三方 API/SDK 等);内部会调用的模块或方法(不必展开到每个参数)。 +- **何时填**:存在跨模块、跨服务调用时;纯单模块内部改动可省略。 + +--- + +## 5. 配置 + +- 二级标题:`## 配置` +- 按配置来源分子节(如 `### 环境变量`、`### 配置文件`、`### 功能开关`),内嵌示例并注释字段含义。 +- **何时填**:需要新增或修改配置项时;无新配置可省略。 + +--- + +## 6. 消息队列/事件总线(如有) + +- 二级标题:`## 消息队列/事件总线` +- 按场景分子节(如 `### xxx 流程`),说明生产/消费方、主题/队列名、触发时机、消费逻辑、幂等处理等。 +- **何时填**:涉及异步消息、事件驱动架构时;无消息队列可省略。 + +--- + +## 7. 交付单元 + +- 二级标题按实际交付形态命名(如 `## API 契约`、`## 组件设计`、`## 页面/交互`、`## 脚本/工具`、`## 服务逻辑`、`## 数据处理`)。 +- **每个交付单元一个三级标题**:`### <交付单元名>`(可括注路径或类型)。 +- **同一小节内按需包含**(顺序建议): + 1. **业务说明**(可选):前置条件、使用场景、注意事项。 + 2. **输入/触发**:参数签名、请求体、用户操作、事件、定时任务或脚本参数示例;说明可用列表或表格。 + 3. **输出/结果**:返回体、页面状态、组件 Props、落库结果、消息事件或文件产物说明。 + 4. **字段说明**(可选):表格 `| 字段 | 类型 | 说明 |`。 + 5. **处理流程**(按需):涉及业务逻辑、状态变更、跨模块调用或异常分支时必须写;纯结构说明或静态配置可省略。 +- **复用通用能力**的单元:写清输入/输出示例 + 「详见《xxx 技术方案》」即可,**处理流程**可简写为「同 xxx 逻辑」。 +- **禁止**:单独再开一章罗列所有交付单元流程;禁止在文末再整段重复每个单元的逐步流程(除非是全链路**一页级**串联,见下条)。 + +--- + +## 8. 调用/交互流程(可选、仅全景) + +- 二级标题:`## 调用流程` 或 `## 交互流程` +- **仅**写用户/系统维度的**调用顺序**(如:进页先加载配置,再触发提交,再展示结果),**不**重复各单元内部步骤。若无多单元串联必要,可整节省略。 + +--- + +## 9. 错误码/异常处理 + +- 二级标题:`## 错误码` 或 `## 异常处理` +- 表格:`| 错误码/异常类型 | 说明 | 处理建议 |`(与项目约定保持一致)。 +- **何时填**:存在明确的错误码体系或需要统一异常处理策略时;无则省略。 + +--- + +## 10. 数据模型/表设计 + +- 二级标题:`## 数据模型` 或 `## 表设计` +- 数据库:每个表 `### 表中文名 表名`,内嵌字段说明+索引说明。 +- 前端/状态:用类型定义或 TypeScript interface 示例。 +- **何时填**:涉及新建或修改数据结构时;纯逻辑改动可省略。 diff --git "a/packages/core/templates/zh-CN/knowledge/template/\347\273\210\347\250\277\346\250\241\347\211\210.md" "b/packages/core/templates/zh-CN/knowledge/template/\347\273\210\347\250\277\346\250\241\347\211\210.md" new file mode 100644 index 0000000..29815f2 --- /dev/null +++ "b/packages/core/templates/zh-CN/knowledge/template/\347\273\210\347\250\277\346\250\241\347\211\210.md" @@ -0,0 +1,102 @@ +> **主口径(统一知识库)**:终稿模板与终稿文档统一维护在 `.Knowledge/template/` 与 `.Knowledge/stock-docs/`。 + +# 终稿概述模版 + +> 本模板用于将「架构说明」「功能/技术方案」等文档整理为**终稿**形态,便于 **f2s-kb-build** 技能更新 `.Knowledge/topics`、`.Knowledge/index.md` 与(按需)路由清单(`manifest-routing` + `matchers/*.json`)。 +> 适用:后端服务、前端/客户端、全栈、产品与设计说明等,按需保留或省略章节。 +> **在 Flow2Spec 中**:主模板路径为 `.Knowledge/template/终稿模版.md`;配置根不再写入 `template/` 副本。 +> **执行 f2s-doc-final 技能时**:本模版仅作为**结构参考与写作提示**,不强制套用;转换以原文内容与逻辑为主,按需采纳章节建议。 + +--- + +## 核心概念 + +| 概念 | 说明 | +|------|------| +| (名词一) | 定义与用途。 | +| (名词二) | 定义与用途。 | + +用表格列出**术语、实体、关键 ID**,便于 AI 提炼到 Rules/Skills。若为架构说明,可包含目录约定、模块划分、公共能力入口等;若为功能方案,可包含领域实体、配置 key、接口/页面名称等。 + +--- + +## 状态与流转 + +(若有状态、阶段或生命周期则填写;若无可简述或省略。) + +- **状态 A**:含义;何时转为状态 B/C。 +- **状态 B**:含义;后续流转。 + +若无复杂状态,可写「主流程上的阶段」或「本说明无状态机,见关键流程」。 + +--- + +## 业务规则 + +- 规则一:约束、限购、时效、权限等。 +- 规则二:校验维度、失败条件、边界情况。 +- 规则三:与配置/开关/环境的对应关系。 + +写清**约束、校验、配置项**,便于生成 Rules 的「规则要点」。前后端、配置、数据一致性、错误处理等均可在此列出。 + +--- + +## 关键流程 + +1. **流程一**:步骤简述;入口(接口/API/页面/事件);结果。 +2. **流程二**:步骤简述;入口;结果。 +3. **流程三**:步骤简述;入口;结果。 + +按「用户侧或系统侧」的主流程写,便于 AI 提炼到 Skills 的「关键流程」。可包含:接口调用顺序、页面跳转、事件触发、后台任务等。 + +--- + +## 接口 / API / 页面(可选) + +(按文档类型择一或多选:后端写接口,前端写页面/组件与数据流,全栈可都写。) + +### 接口 / API 名称或路径 + +- **请求**:入参说明(体/查询/头)。 +- **返回**:出参说明;错误码或异常情况。 +- **内部调用**:项目内方法、服务或模块(若有)。 + +### 页面 / 组件 / 路由(若有) + +- **入口**:路由、入口组件或事件。 +- **依赖数据**:接口、Store、本地状态。 +- **产出**:页面/组件职责与关键交互。 + +--- + +## 配置 / 数据 / 错误(可选) + +(按需选写,不必全部出现。) + +- **配置**:配置项、key、必传/可选、含义;配置中心或环境变量约定。 +- **数据**:核心数据模型、表结构、字段说明;或前端 Store/状态结构;与业务的对应关系。 +- **错误码或异常**:code、场景、说明;或前端错误态、兜底逻辑。 + +--- + +## 实现位置与对接方式 + +- **实现位置**:代码/服务/仓库路径(如 `src/xxx/yyy`、服务名、仓库名),说明封装或实现所在位置。 +- **对接方式**:新功能/新业务如何接入:需做哪些配置、建表、注册路由、引用组件等;无需改封装时的最小接入步骤。 + +--- + +## 来源文件 + +> 生成本终稿时实际读取的原始路径,便于溯源与后续更新。 + +- `<路径1>` +- `<路径2>` + +--- + +### 使用说明 + +- 将上述括号内的占位替换为实际内容;与文档类型无关的章节可整节删除或标「(不适用)」。 +- 至少保留 **核心概念、业务规则、关键流程** 三个二级标题,其余按需增删。 +- 保存为 `.Knowledge/stock-docs/<方案名>_终稿.md` 后,按 **f2s-kb-build** 技能、以该路径为入参即可更新 `.Knowledge/topics`、`.Knowledge/index.md`,并在需要时更新路由清单。 diff --git "a/packages/core/templates/zh-CN/knowledge/template/\351\241\271\347\233\256\351\207\214\347\250\213\347\242\221\346\250\241\347\211\210.md" "b/packages/core/templates/zh-CN/knowledge/template/\351\241\271\347\233\256\351\207\214\347\250\213\347\242\221\346\250\241\347\211\210.md" new file mode 100644 index 0000000..4ebe2ae --- /dev/null +++ "b/packages/core/templates/zh-CN/knowledge/template/\351\241\271\347\233\256\351\207\214\347\250\213\347\242\221\346\250\241\347\211\210.md" @@ -0,0 +1,32 @@ +> **主口径**:模板在 `.Knowledge/template/项目里程碑模版.md`(由 `flow2spec init` 落地);生成物在 `.Knowledge/stock-docs/<范围名>里程碑.md`。 +> **执行 `f2s-doc-milestone`**:结构以本模版为准;内容仅来自 **req-docs、git log、.task** 与知识库主题,禁止臆造。 +> **阶段**:Mx **仅**功能/能力变更;**不得**单独成阶段的联调、测试、验收或纯环境运维;工程性改动并入对应功能阶段。 + +# (范围名)里程碑 + +> **范围**:(用户语义说明;未指定时写「整个项目」) +> **更新时间**:`YYYY-MM-DD` + +## 总览 + +| 阶段 | 时间 | 摘要 | +| --- | --- | --- | +| MN · (最新阶段标题) | YYYY-MM | (可验证的功能交付摘要;非联调/测试/验收) | +| … | … | … | +| M1 · (初始阶段标题) | YYYY-MM | … | + +## MN · (最新阶段标题) + +- (交付内容,逐条列出,可验证) + +## … + +(结构同 MN;总览表中每一行 Mx 须有对应二级标题;顺序均为最新在前。) + +## M1 · (初始阶段标题) + +- (交付内容,逐条列出,可验证) + +## 待确认 + +- (四源中功能/交付层面的缺口或不一致;无则写「无」) diff --git a/packages/core/templates/zh-CN/knowledge/topics/f2s-config-precheck.md b/packages/core/templates/zh-CN/knowledge/topics/f2s-config-precheck.md new file mode 100644 index 0000000..c8947f3 --- /dev/null +++ b/packages/core/templates/zh-CN/knowledge/topics/f2s-config-precheck.md @@ -0,0 +1,32 @@ +--- +id: config-precheck +revision: 0 +summary: "config-precheck(路由摘要)" +primary: config +confidence: manual +tags: [policy] +--- +# config-precheck(路由摘要) + +## 本主题作用 + +- 供 `manifest-routing.topicPaths` 锚定主题 id **`config-precheck`**。 +- 与执行任意 **`f2s-*` 技能**前读取项目根 **`flow2spec.config.json`**(`subAgent`、`switchAgentVerification`、`changeTracking`)相关;语义与仓库根 **`AGENTS.md`** 顶部、「统一入口」一致。 + +## 完整条令(按需,勿在 `.Knowledge` 再维护第二份正文) + +| 侧 | 路径 | +| --- | --- | +| Codex | 仓库根 `.codex/topics/f2s-config-check.md`(init 镜像,与模板同源);SessionStart:`.codex/hooks/f2s-config-session.js` | +| Cursor | 仓库根 `.cursor/rules/f2s-config-check.mdc`(`flow2spec init cursor`) | +| Claude | `.claude/rules/f2s-config-check.md`;SessionStart:`.claude/hooks/f2s-config-session.js`;PreToolUse 守门:`.claude/hooks/f2s-config-inject.js` | + +## 必备步骤 + +1. 用 **Read** 打开项目根 **`flow2spec.config.json`**(须在 `f2s-*` 技能正文任何步骤之前)。 +2. 仓库根 **`AGENTS.md`** 中 `{{FLOW2SPEC_PROJECT_CONFIG}}` 表仅说明字段语义;当前值以 **Read** 结果为准。 +3. `subAgent=true` 时,主 agent 必须在进入技能正文早期**显式判断**本次是否满足拆子前提 / 阈值;即使判断不拆,也必须输出不拆原因。SessionStart 摘要只负责提醒,不替代该判断。 + +## 禁止项 + +- 禁止在未读 **`flow2spec.config.json`** 的情况下进入 **`f2s-*`** 技能正文步骤(与 `AGENTS`、`.codex/topics/f2s-config-check.md` 一致);Claude / Codex 的 SessionStart 摘要与 Claude 的 PreToolUse 守门提示都不替代本次 Read。 diff --git a/packages/core/templates/zh-CN/knowledge/topics/f2s-fallback-triage.md b/packages/core/templates/zh-CN/knowledge/topics/f2s-fallback-triage.md new file mode 100644 index 0000000..f27ffaa --- /dev/null +++ b/packages/core/templates/zh-CN/knowledge/topics/f2s-fallback-triage.md @@ -0,0 +1,67 @@ +--- +id: fallback-triage +revision: 0 +summary: fallback-triage +primary: policy +confidence: manual +--- +# fallback-triage + +## 触发条件 + +以下任一成立时进入本主题: + +- `taskToTopicRules` 无命中 +- 无法确定路由到哪个主题(多个候选相近,关键词均为通用词,无领域词命中) +- 缺口检查失败(依赖主题或上下文文档缺失) + +> **进入本主题时**:`manifest-routing.json` 已在本任务线读取过,视为稳定快照,不再重读。直接基于已有路由结果分诊。 +> +> **本主题仅用于分诊,不作为最终命中依据,不直接实施业务改动。** + +--- + +## 分诊流程 + +### 第一步:判断路由是否命中 + +**已命中主题,但上下文不足**: + +1. 读取 `topicDependencies` 中的依赖主题,保留次高候选做补充校验 +2. 点名缺少哪份文档或哪段 topic +3. 向用户要具体文档路径,补齐后继续执行 +4. **不进行无门槛的跨 matcher 全量检索** + +**未命中任何主题**:进入第二步。 + +--- + +### 第二步:向用户确认领域覆盖情况 + +不由 Agent 自行推断,直接问用户: + +> 当前任务未命中路由。请确认:**这个领域的文档是否已录入知识库?** +> - 是 → 可能是路由词条缺失,建议执行 `f2s-kb-build` / `f2s-kb-sync` 补充路由后重试 +> - 否 → 知识库当前无此覆盖,可选择:下钻业务源码 / 补充 `req-docs` 后按方案实现 +> - 不确定 → 请检查 `.Knowledge/stock-docs/` 是否有相关文档,再告知 + +根据用户回答走对应出口,**不在无答案时自行猜测**。 + +--- + +## 出口路径 + +| 分诊结论 | 下一步 | +|----------|--------| +| 已命中主题,补齐上下文后 | 跳转至该 topic,按 `match → expand → verify → act` 执行 | +| 用户确认文档已录入,路由词条缺失 | 提示执行 `f2s-kb-build` / `f2s-kb-sync` 补路由,本次暂停或下钻源码 | +| 用户确认库中无覆盖 | 提供两条路:下钻源码 / 补充 `req-docs` 后实现 | +| 用户不确定,仍无法定位 | 停止执行,向用户说明原因,等待明确指令 | + +--- + +## 禁止项 + +- 禁止将本主题作为最终命中依据直接实施改动 +- 禁止跳过向用户确认,自行推断领域覆盖情况 +- 禁止在上下文不足时进行跨 matcher 全量补检索 diff --git a/packages/core/templates/zh-CN/knowledge/topics/f2s-implement-tech-design.md b/packages/core/templates/zh-CN/knowledge/topics/f2s-implement-tech-design.md new file mode 100644 index 0000000..6c13e8b --- /dev/null +++ b/packages/core/templates/zh-CN/knowledge/topics/f2s-implement-tech-design.md @@ -0,0 +1,29 @@ +--- +id: implement-tech-design +revision: 0 +summary: "implement-tech-design(路由摘要)" +dependsOn: [f2s-doc-routing] +primary: policy +confidence: manual +--- +# implement-tech-design(路由摘要) + +> **唯一长文**:Cursor / Claude 以配置根 **`rules/f2s-implement-tech-design.md(c)`** 为准。 +> **Codex**:不读 `rules/`,须执行 **`.codex/topics/f2s-implement-tech-design.md`**(由 `flow2spec init` 从模板 `rules` 自动镜像)中的等效约束。 + +## 本文件作用 + +- 供 `manifest-routing.topicPaths` 与 `index.md` 锚定主题 id **`implement-tech-design`**。 +- 仅保留**路径与角色**记忆点,避免与 `rules/` 双份维护长文。 + +## 路径与角色(须与规则一致) + +- 技术方案输入:`.Knowledge/req-docs/*.md`(及 PDF 经 `f2s-doc-pdf` 落入同目录的 MD)。 +- 存量沉淀:`.Knowledge/stock-docs/` — **不**作为「按方案写代码」的直接输入。 + +## 下一步读什么 + +| 环境 | 下一步 | +| --- | --- | +| Cursor / Claude | 打开或 @ **`rules/f2s-implement-tech-design`**,按其中步骤执行。 | +| Codex | 读 **`.codex/topics/f2s-implement-tech-design.md`**。 | diff --git a/packages/core/templates/zh-CN/knowledge/topics/f2s-req-plan.md b/packages/core/templates/zh-CN/knowledge/topics/f2s-req-plan.md new file mode 100644 index 0000000..bf3781e --- /dev/null +++ b/packages/core/templates/zh-CN/knowledge/topics/f2s-req-plan.md @@ -0,0 +1,35 @@ +--- +id: f2s-req-plan +revision: 0 +summary: "f2s-req-plan(路由摘要)" +dependsOn: [f2s-task] +primary: policy +confidence: manual +--- +# f2s-req-plan(路由摘要) + +> 长文见配置根 **`skills/f2s-req-plan/SKILL.md`**。 +> **`.task/` 真值源**:配置根 **`rules/f2s-task.*`**(Codex:`.codex/topics/f2s-task.md`)。 +> 设计背景(可选):[任务清单与变更追踪](../stock-docs/<任务清单说明>.md)。 + +## 依赖 + +执行本主题前须先读依赖主题 **`f2s-task`**(`manifest-routing.topicDependencies`)。 + +## 作用 + +从技术方案或需求描述出发:**续作分诊 → 草稿确认 → 按 f2s-task 落盘 → 实现 → 归档**。 + +1. 步骤 0:`flow2spec.config.json` + **`f2s-task` 全文** +2. `f2s-task`「任务开始」:检查 `todo.json` / keywords 续作 +3. 草稿确认(主 agent) +4. 落盘 `task.md` / `context.md` / `user-todos.md` / `todo.json`(`linkedSkill: f2s-req-plan`) +5. 实现并按步打钩;用户代办写 `user-todos.md` +6. 满足归档门禁后移入 `completed/-/` + +不依赖 `changeTracking`,但 **始终** 服从 `f2s-task`。 + +## 下一步 + +- 技能全文:`skills/f2s-req-plan/SKILL.md` +- 任务规则:`rules/f2s-task.*` 或 `.codex/topics/f2s-task.md` diff --git a/packages/core/templates/zh-CN/knowledge/topics/f2s-stock-docs-vs-req-docs.md b/packages/core/templates/zh-CN/knowledge/topics/f2s-stock-docs-vs-req-docs.md new file mode 100644 index 0000000..19ae76b --- /dev/null +++ b/packages/core/templates/zh-CN/knowledge/topics/f2s-stock-docs-vs-req-docs.md @@ -0,0 +1,32 @@ +--- +id: f2s-doc-routing +revision: 0 +summary: "f2s-doc-routing(路由摘要)" +primary: policy +confidence: manual +--- +# f2s-doc-routing(路由摘要) + +> **唯一长文**:Cursor / Claude 以配置根 **`rules/f2s-stock-docs-vs-req-docs.md(c)`** 为准。 +> **Codex**:不读 `rules/`,须执行 **`.codex/topics/f2s-stock-docs-vs-req-docs.md`**(由 `flow2spec init` 从模板 `rules` 自动镜像)中的等效约束。 + +## 本文件作用 + +- 供 `manifest-routing.topicPaths`、**`topicDependencies`** 与 `index.md` 锚定主题 id **`f2s-doc-routing`**。 +- 仅保留**目录分工**记忆点。 + +## 目录分工(须与规则一致) + +| 目录 | 用途 | +| --- | --- | +| `.Knowledge/stock-docs/` | 架构、终稿、沉淀;`f2s-kb-build` / `f2s-doc-final` 等优先落盘。 | +| `.Knowledge/req-docs/` | 需求澄清、**技术方案**、按方案实现时的 MD 输入。 | + +**原则**:按方案写代码只读 **`req-docs`**;不要把 **`stock-docs`** 当编码直接输入。 + +## 下一步读什么 + +| 环境 | 下一步 | +| --- | --- | +| Cursor / Claude | 打开或 @ **`rules/f2s-stock-docs-vs-req-docs`**。 | +| Codex | 读 **`.codex/topics/f2s-stock-docs-vs-req-docs.md`**。 | diff --git a/packages/core/templates/zh-CN/knowledge/topics/f2s-task.md b/packages/core/templates/zh-CN/knowledge/topics/f2s-task.md new file mode 100644 index 0000000..ed13c47 --- /dev/null +++ b/packages/core/templates/zh-CN/knowledge/topics/f2s-task.md @@ -0,0 +1,60 @@ +--- +id: f2s-task +revision: 0 +summary: "f2s-task(路由摘要)" +primary: policy +confidence: manual +--- +# f2s-task(路由摘要) + +> 长文见配置根 **`rules/f2s-task.*`**。 +> 体系化设计说明(可选):在 `stock-docs/` 自建任务清单说明后,于本主题或 `index.md` 中链接,例如 `../stock-docs/<任务清单说明>.md`。 + +## 作用 + +变更追踪规则(`alwaysApply: true`)。当对应技能的 `changeTracking.*` 为 `true` 时,技能执行前后自动创建、逐步更新、最终归档 `.task/` 下的任务清单,支持跨会话续作。 + +## 生效范围 + +| 配置项 | 对应技能 | +| --- | --- | +| `changeTracking.feat` | `f2s-kb-feat` | +| `changeTracking.fix` | `f2s-kb-fix` | +| `changeTracking.implement` | `f2s-implement-tech-design` | + +`f2s-req-plan` 不受配置约束,始终创建任务清单。 + +## 任务根 `TASK_ROOT`(多人) + +- 解析顺序:`collaboration.developerId`(config)→ git email/name → legacy `.task` +- 非 legacy 时目录为 `.task//…`;只读写当前 `TASK_ROOT`,禁止扫其他人的 todo(防串戏) +- `.Knowledge/` 仍全员共享 + +## 目录结构 + +``` +TASK_ROOT/ ← `.task` 或 `.task/` +├── todo.json ← 活跃任务索引(仅主 agent 写) +├── active// +│ ├── task.md ← checklist(执行步骤) +│ ├── context.md ← 涉及文件、文档链接 +│ ├── user-todos.md ← 须用户执行的代办(改库、配环境等) +│ └── acceptance.md ← 验收清单:task.md 全部 [x] 后、归档前生成 +└── completed/-/ + ├── task.md + ├── context.md + ├── user-todos.md + └── acceptance.md +``` + +用户代办**必须**落在与 `task.md` 同目录的 **`user-todos.md`**;归档前**必须**生成与 `task.md` 同目录的 **`acceptance.md`**(验收清单),二者职责分离。细则见配置根 **`rules/f2s-task.*`**。 + +## 跨会话续作 + +新会话先解析 `TASK_ROOT`;若存在该根下 `todo.json`,将用户首条消息与**仅该文件**内 `keywords` 匹配: +- 命中 → 展示剩余 checklist,摘要 user-todos / acceptance,加载 `linkedSkill`,提示是否继续 +- 无命中 → 不打扰 + +## 下一步 + +读配置根 `rules/f2s-task.*` 获取完整规则(目录结构、todo.json 格式、任务生命周期、Hook 配置)。 diff --git a/packages/core/templates/zh-CN/knowledge/topics/flow2spec-dsh-adapter.md b/packages/core/templates/zh-CN/knowledge/topics/flow2spec-dsh-adapter.md new file mode 100644 index 0000000..b1c0d94 --- /dev/null +++ b/packages/core/templates/zh-CN/knowledge/topics/flow2spec-dsh-adapter.md @@ -0,0 +1,16 @@ +--- +id: flow2spec-dsh-adapter +revision: 0 +summary: "DeepSeek Harness 项目级技能初始化与目录适配" +primary: feature +confidence: inferred +tags: [module] +--- +# DeepSeek Harness 适配 + +用于 `flow2spec init dsh`、DeepSeek Harness 技能发现、`.dsh/skills`、`.dsh/topics` 和根 `AGENTS.md` 入口问题。 + +- 技能写入 `.dsh/skills//SKILL.md`。 +- 规则长文镜像到 `.dsh/topics/*.md`,并写入 `.dsh/AGENTS.md` 目录指针。 +- 缺少根 `AGENTS.md` 时生成完整入口,已有入口不覆盖。 +- 原生 Cordis 插件属于后续路线图事项。 diff --git a/packages/core/templates/zh-CN/rules/f2s-config-check.md b/packages/core/templates/zh-CN/rules/f2s-config-check.md new file mode 100644 index 0000000..f42596f --- /dev/null +++ b/packages/core/templates/zh-CN/rules/f2s-config-check.md @@ -0,0 +1,50 @@ +--- +description: 执行任何 f2s-* 技能前强制读取 flow2spec.config.json,确定 subAgent 与 switchAgentVerification 实际值 +alwaysApply: true +--- + +> **任务路径**:凡 `.task/` 落盘与续作,**必须以 `rules/f2s-task` 解析的 `TASK_ROOT` 为准(`.task` 或 `.task/`;config → git → legacy)。下文若仍出现 `.task/todo.json` / `.task/active/`,均视为 **`TASK_ROOT/...` 的简写**。 + + +# f2s 技能前置强制步骤 + +**执行任何 `f2s-*` 技能的第一个动作,必须用 Read 工具读取项目根 `flow2spec.config.json`**,获取 `subAgent` 与 `switchAgentVerification` 的实际值,再决定后续编排方式。 + +``` +必须执行:Read("flow2spec.config.json") ← 技能正文任何步骤之前 +``` + +| 读取结果 | 行为 | +|---------|------| +| `subAgent: true` | 先显式判断当前技能是否满足拆子前提 / 规模阈值;满足时按技能 SKILL.md 的 B/C 模式派子 agent,并在回复或执行记录中写明「本次是否拆子、拆给谁、为什么」;不满足时主 agent 继续完成,但也必须输出不拆原因 | +| `subAgent: false` | 全部在主 agent 内完成,不得拆子 agent | +| `switchAgentVerification: true` | 子 agent 落盘的由主 agent 校验;主 agent 落盘的由子 agent 校验(须 subAgent=true 且已拆子任务) | +| `switchAgentVerification: false` | 落盘侧自验,不交叉 | +| 文件不存在 | 所有字段均视为 `false` | + +**Claude Code**:`f2s-config-session` 在 `SessionStart` 注入一次配置摘要;`f2s-config-inject` 在 `PreToolUse` 仅作为守门提示,提醒调用 `f2s-*` Skill 前首步必须 `Read("flow2spec.config.json")`。两者都**不替代**本条 Read 要求。 + +**Cursor**:配置读取仍走文本约束(本规则 `alwaysApply`),不依赖 hook 自动读取配置。 + +**Codex**:`SessionStart` 会注入一次配置摘要;进入 `f2s-*` Skill 正文前仍必须 `Read("flow2spec.config.json")`,且当 `subAgent=true` 时,主 agent **必须先显式判断**当前技能是否满足拆子前提 / 阈值,再决定是否派子;即使判断不拆,也必须输出不拆原因。Codex **没有** Claude 的 `PreToolUse Skill` 守门,不能把“拆子判断”留给隐式心证。 + +### changeTracking(变更追踪) + +| 字段 | 生效技能 | 行为 | +|------|---------|------| +| `changeTracking.feat: true` | `f2s-kb-feat` | **步骤 0 必须执行**:创建或续作 `.task/active/` 变更追踪任务 | +| `changeTracking.feat: false` | `f2s-kb-feat` | 步骤 0 跳过,不创建 `.task/` 目录 | +| `changeTracking.fix: true` | `f2s-kb-fix` | **步骤 0 必须执行**:创建或续作 `.task/active/` 变更追踪任务 | +| `changeTracking.fix: false` | `f2s-kb-fix` | 步骤 0 跳过,不创建 `.task/` 目录 | +| `changeTracking.implement: true` | `f2s-implement-tech-design` | **步骤 2.5 写入任务清单、步骤 2.6 随实现同步打钩 `task.md`、步骤 5 满足归档门禁后归档** | +| `changeTracking.implement: false` | `f2s-implement-tech-design` | 步骤 2.5、2.6 和步骤 5 的变更追踪部分跳过 | + +### intentRecognition(意图识别) + +| 字段 | 行为 | +|------|------| +| `intentRecognition: true` | 启用意图识别:高置信操作意图按 `rules/f2s-intent-routing.*` 自动进入对应 Skill;讨论 / 评估 / 低置信输入不得自动调用 | +| `intentRecognition: false` | 不启用自动分流;仅显式 `$f2s-*` / 明确要求执行某技能时进入对应 Skill | +| 字段不存在 | 视为 `false` | + +**禁止在未读该文件的情况下进入技能正文的任何执行步骤。** diff --git a/packages/core/templates/zh-CN/rules/f2s-flow2spec-unified-entry.md b/packages/core/templates/zh-CN/rules/f2s-flow2spec-unified-entry.md new file mode 100644 index 0000000..1d0e9b5 --- /dev/null +++ b/packages/core/templates/zh-CN/rules/f2s-flow2spec-unified-entry.md @@ -0,0 +1,117 @@ +--- +description: Flow2Spec 统一知识库入口,按 .Knowledge 渐进式读取 +alwaysApply: true +--- + +# Flow2Spec 统一入口规则 + +本项目知识库已统一到 `.Knowledge/`,请按以下顺序读取,避免无范围检索。 + +## 项目根 CLI 开关(必须按需读取) + +业务仓库**项目根** `flow2spec.config.json`(`flow2spec init` 在文件缺失时补齐)含布尔字段 **`subAgent`**、**`switchAgentVerification`**(**切换 agent 校验**),默认 `false`。执行任意 **`f2s-*` 技能**或与 Flow2Spec 初始化相关的说明前,须读取该文件;技能或规则中凡写「仅当 `subAgent` / `switchAgentVerification` 为 true」的步骤,**必须按文件实际值决定是否执行**;缺失字段或文件不存在时均视为 `false`。 + +> **`init` 与择路**:**`flow2spec init`** 会把统一入口写入当前仓库;**Cursor / Claude** 读取配置根 **`rules/f2s-flow2spec-unified-entry.*`**,**Codex** 读取 **`.codex/topics/f2s-flow2spec-unified-entry.md`**。两处正文同源,按当前工具读取对应入口即可;技能引「统一入口」时,在 **Codex** 以 **`.codex/topics/f2s-flow2spec-unified-entry.md`** 为准。 + +### 两字段语义(模板约定) + +- **`subAgent`**:`f2s-*` 技能若规定某步骤「用子 agent 执行」,则 **`true`** 时按技能使用子 agent,**`false`** 时在主 agent 内完成。用户可在对话中要求「**仅当**本项为 **`true`** 时,由主 agent **动态判断**哪些子任务适合交给子 agent」——**仅当配置为 `true` 时该要求有效**;配置为 `false` 时凡依赖拆子 agent 的该段说明**不生效**,全部在主 agent 完成。`subAgent=true` 时,主 agent 必须在技能正文前段显式判断本次是否拆子;即使判断不拆,也必须输出不拆原因。**各 `f2s-*` 在工作哪一阶段必须或建议使用子 agent** 由技能正文逐步约定;技能未写明时不默认拆子。 +- **`switchAgentVerification`(切换 agent 校验)**:落盘或变更后的**验证/复核**(对照清单、diff、自检)**不是**「一律在主 agent」;默认以**落盘侧所在 agent 为「当前 agent」**,在该会话内完成校验(**子 agent 落盘的就在子 agent 内验,主 agent 落盘的就在主 agent 内验**)。**仅当**① 配置 **`switchAgentVerification` 为 `true`**,**且** ② **当前 `f2s-*` 技能正文**对该步骤**明确写出**「当 **`switchAgentVerification`** 为 **`true`**」时,才启用**交叉校验**:**子 agent 落盘的 → 由主 agent 校验**;**主 agent 落盘的 → 由子 agent 校验**(**须**已存在子 agent 会话,即 **`subAgent` 为 `true`** 且实际拆出子任务;若 **`subAgent` 为 `false`**,无子侧可承接,**「主落盘→子验」不发生**,校验**全部在主 agent 内**完成)。配置为 `false`、或技能未写依赖本项、或用户仅泛泛要求「给对方验」的:**不**启用交叉,仍在**落盘侧 agent**内完成验证。 + +### Git worktree 与子任务工作目录卫生(`subAgent: true` 或并行子任务时必读) + +部分环境会为子 agent / 并行尝试创建 **独立 `git worktree`** 或等价隔离目录。规则如下: + +1. **谁创建谁收尾**:子侧创建则子侧在返回前尽量清理;若子会话已结束无法清理,**主 agent 合并结果后**必须执行清理,**禁止**依赖「稍后自动回收」。 +2. **收尾动作(必须)**:对**仅为本次子任务**添加的 worktree,在合并或丢弃该子任务结果后执行 `git worktree remove `(工作区干净仍失败时再用 `git worktree remove --force `,**须确认**该路径无他人未提交修改);随后 `git worktree list` 自检,**禁止**留下已知孤儿路径。 +3. **中断 / 用户换题前**:若本会话曾添加 worktree,在结束前**必须**完成上述移除或在 `task.md`「## 备注」写明残留路径与删除命令,并视情况写入 **`user-todos.md`** 请用户本地执行(见 `f2s-task`)。 +4. **禁止**:子任务已结束、主分支已继续开发,仍长期保留仅用于尝试的 worktree 目录(易造成混淆提交、磁盘堆积)。 + +## 读取顺序(必须) + +1. 先读 `.Knowledge/manifest-routing.json`,优先按 `taskToTopicRules` 路由;按需根据 `matcherPath` 读取 matcher 分片获取 `includeAny` 关键词;无法命中时进入补召回阶段。 + - 若命中主题在 `topicDependencies` 中存在依赖,先读依赖主题,再读主主题。 + - 路由清单仅通过 `f2s-*` 技能流程维护,不依赖额外 CLI 子命令。 +2. `.Knowledge/index.md` 按需读取,仅用于确认主题语义与边界。 +3. 再读 `.Knowledge/topics/.md`(**路由摘要**:主题 id、路径约定、下一步指针);若主题为 **`implement-tech-design`** 或 **`f2s-doc-routing`**,**必须继续读取**配置根 **`rules/f2s-implement-tech-design.*` / `rules/f2s-stock-docs-vs-req-docs.*` 全文**作为执行依据(`.Knowledge/topics` 内同名文件不重复长文)。 +4. 若需要背景,再读 `.Knowledge/stock-docs/.md`。 +5. 仅在前四步不足时下钻业务源码。 +6. 命中后必须执行 `match -> expand -> verify -> act`: + - `match`:先取主候选; + - `expand`:展开 `topicDependencies`,并保留次高候选做补充校验; + - `verify`:执行前做缺口检查(关键主题/边界/上下文是否缺失); + - `act`:仅在置信度足够时执行;低置信度必须先澄清。 +7. 仅在以下条件之一成立时,允许执行跨 matcher 全量补检索(top-k): + - `taskToTopicRules` 无命中; + - 主候选与次候选分差过小(低置信度); + - 缺口检查失败(关键主题/依赖/上下文缺失); + - 用户明确要求“全量检查/不要遗漏”。 + +## 任务分流 + +- 技术方案实现:先读 `.Knowledge/topics/f2s-implement-tech-design.md`(摘要),再读 **`rules/f2s-implement-tech-design.*` 全文**;需求文档默认位于 `.Knowledge/req-docs/`。 +- 目录边界判断:先读 `.Knowledge/topics/f2s-stock-docs-vs-req-docs.md`(摘要),再读 **`rules/f2s-stock-docs-vs-req-docs.*` 全文**。 + +## 机读事实源口径(规则层) + +- `taskToTopicRules`:任务路由第一优先级。 +- `taskToTopicRules[].matcherPath`:匹配词分片直链路径,按需读取单个 matcher 文件。 +- `taskToTopicRules[].matcherId`:matcher 的稳定标识,需与 matcher 分片内 `id` 一致。 +- `topicDependencies`:主主题命中后先加载依赖主题。 +- `topicMetadata`:主题治理元数据,只影响阅读预期,不参与 matcher 命中,不决定是否读取 topic,不改变执行强制性;执行强制性始终以 `AGENTS.md`、rules、skills 与 topic 正文中的明确要求为准。读到 `topicMetadata[topicId].primary` / `tags` 时:`config` 关注配置项、开关、默认值、初始化参数;`policy` 优先检查正文中的必须/禁止/门禁/流程约束;`feature` 作为已落地业务/产品能力背景;`module` 作为目录、包、模块边界与工程结构背景。`confidence` 仅允许 `manual` / `inferred`;无明确分类证据时不写 metadata。 +- `matcherPath(includeAny)`:任务关键词匹配词表。 +- `fallbackTopic`:任务与关键词都未命中时必须读取,但仅作低置信度兜底,不是最终执行依据。 +- `.Knowledge/manifest-routing.json + matcherPath 分片文件` 是机读事实源(关键词仅在 `matchers/*.json`)。 +- `.Knowledge/index.md` 不是机读事实源,仅作人读导航与语义边界校验。 +- 进入 `fallbackTopic` 后,必须先补召回或澄清,再决定是否执行改动。 + +## 知识缺口与对策(分场景) + +| 情况 | 对策 | +| --- | --- | +| **1a 库里有文档但未配路由** | 用 `f2s-kb-build` / `f2s-kb-sync` / `f2s-kb-add` 补 `taskToTopicRules`、`matcherPath` 分片、`topicPaths`;扩充 `includeAny` 覆盖用户常用说法。Agent 侧:走 `fallbackTopic` 分诊并提示「需补路由」,**不**靠全仓扫文件代替配置。 | +| **1b 命中了但上下文不够** | 先 `expand`(`topicDependencies` + 次高候选),再 `verify` 点名缺哪份 `stock-docs`/`req-docs` 或哪段 topic;仍不足则 **向用户要文档或路径**,不要无门槛跨 matcher 全量补检索。**Agent 若需下钻源码**:须先对用户做**可见的缺口说明**(已读 KB、缺什么、拟读哪 1~2 个文件),见 **`f2s-knowledge-preflight`**「缺口闸门」;**禁止**无说明地连续 `Grep`/乱序探源。 | +| **2 库里没有对应文档** | 一次读完 routing + 已命中 matcher + 相关 topic 后,在回复中 **明确承认知识库无覆盖**,再选:下钻业务代码 / 请用户补充 `req-docs` 或 PRD。**禁止**用反复读清单假装「再找一遍就会有」。**下钻源码前**同样须满足 **`f2s-knowledge-preflight`**「缺口闸门」的可见说明。 | +| **2a 反复读清单耗 token** | **同一任务线内** `manifest-routing.json` 视为稳定快照:再次全文读取须说明理由(例如用户声明已通过 `f2s-kb-build` / `f2s-kb-sync` / `f2s-kb-add` 等更新路由或知识、或**手动编辑**了 manifest/matcher)。**勿将**仅执行 **`flow2spec init`** 等同于「业务知识库已更新」:`init` 以配置根落盘、目录补齐与包级路由结构对齐为主;**stock-docs / req-docs、topics 路由摘要、matchers 词条**由 **`f2s-*` 技能流程**维护;`init` 会把规则写入配置根 **`rules/*`**(或等价扩展名),并为 Codex 写入 **`.codex/topics/*.md`**。只读 **当前规则对应的单个** `matcherPath`;不要为枚举而遍历整个 `matchers/` 目录。`index.md` 仅在需核对主题语义时打开,禁止与 manifest 交替「刷清单」。 | + +### 知识缺口的执行层要点(避免「表里有写、行为没做」) + +- **「向用户说明」「明确承认无覆盖」必须是用户可见的自然语言**,不得仅在内部分析或工具链中隐含带过;细则与停步条件见 **`f2s-knowledge-preflight`**(缺口闸门、探索次数上限)。 +- **禁止**在命中 **1b / 2** 后,未做上述可见说明便进入「多文件 + 依赖目录」的链式探源;每出现一个新的「入口符号」就再 `Grep` 一轮,属于典型反模式。 +- **HTTP 状态、错误正文、重定向与否**等事实,**不得以训练数据或他库经验代答**;须以当前仓库内**本次已读到的实现**为准。 +- 普通问答下钻源码并据此补答时,先按 **`f2s-knowledge-preflight`** 完成首读与缺口闸门,再按 **`f2s-kb-feedback-closing`** 完成最终知识库补充建议收口;只提示,不自动落盘。 + +## 知识库落盘文风(全局,写 stock-docs / topics / index 时适用) + +**肯定式优先**:表述正确信息时,直接说"是什么 / 在哪里 / 怎么做",禁止用"不是 X / 非 X / 不再是 X"来传达——即使旧描述是错的,否定旧版也会让读者在脑中锚定错误前提。 + +- 错:`随包 import,非 window 注入` +- 对:`通过 import { <符号> } from '<包名>' 引入` + +**例外(应显式否定)**:A、B 两种做法在逻辑上均正确,但项目已做出**排他性选择**时,须写出「不用 B」——不说清楚,读者无法判断 B 是否仍可选。 + +## 知识库版本自检(hook 自动触发;每日首次,仅 updateCheck.enabled=true 时) + +各已初始化客户端在支持时使用自身的启动 / 更新机制,具体以生成的客户端入口为准。不提供 hooks 的客户端继续通过生成的 rules、skills、`AGENTS.md` 或 topics 镜像工作。版本检查脚本在客户端支持时完成版本比对并注入升级提示;项目级技能发现客户端通过 `flow2spec init dsh` 使用 `.dsh/skills/` 与 `.dsh/topics/`。 + +**规则层双保险**(与脚本缓存互为备份): + +1. 读 `flow2spec.config.json` → 若 `updateCheck.enabled` 不为 `true`,跳过,不做任何提示。 +2. 读 `.Knowledge/update-check.json` → 若文件存在且 `checkedAt` 与今日为同一自然日(`new Date(checkedAt).toDateString() === new Date().toDateString()`),不重复查 npm;但若 `needsUpgrade=true` 或 `latestNpm > manifestVersion`,本会话首次回复用户时仍须提醒执行 `f2s-kb-upgrade`;若当前 `.Knowledge/manifest-routing.json.version` 已不低于 `latestNpm`,删除该缓存并不再提示。 +3. 上述两步均未跳过时:执行当前 agent 配置根下的更新检测脚本(Claude:`node .claude/hooks/f2s-update-check.js`;Cursor:`node .cursor/hooks/f2s-update-check.js`;Codex:`node .codex/hooks/f2s-update-check.js`),解析标准输出的 JSON: + - 若含 `hookSpecificOutput.additionalContext`:**告知用户**该内容(建议执行 `f2s-kb-upgrade` skill)。 + - 无输出或解析失败:静默,不提示。 +4. 以上步骤出现任何错误,静默跳过,不影响正常对话。 + +## 主题创作(Topic Authoring)指针 + +新增或修改 `.Knowledge/topics/.md`、调整 `manifest-routing.topicDependencies`、删除 / 迁移 topic 时,**创作侧** 准则以 **`rules/f2s-topic-authoring.*`** 为单一事实源(**Cursor/Claude**:`rules/f2s-topic-authoring.mdc`;**Codex**:`.codex/topics/f2s-topic-authoring.md`)。本入口为**消费侧**(如何按已有 topic 路由 / 读取 / 兜底),与之并存;硬冲突时以本入口为准。`f2s-kb-build` / `f2s-kb-add` / `f2s-kb-feat` / `f2s-kb-fix` / `f2s-kb-sync` / `f2s-kb-migrate` / `f2s-kb-rm` 在涉及 topic 落盘前须 Read 该条全文。 + +## 禁止项 + +- **下发内容中性约束**:技能、规则、知识正文中的示例须**中性**——勿写特定业务域名称、单一组织 npm 包名、仅 Flow2Spec 产品仓存在的 `docs/` 路径;用 `<能力>`、`src/<模块>/` 等占位。 +- 使用 `git worktree` 或隔离目录跑子任务后,**禁止**在未 `git worktree remove` / 未交接删除命令的情况下结束会话(见上文「Git worktree 与子任务工作目录卫生」)。 +- 未查看 `.Knowledge/manifest-routing.json` 前,禁止进行全仓无范围扫描;`.Knowledge/index.md` 在需确认主题语义时再读,禁止与 manifest 交替重复读取以代替决策。 +- 禁止把 `stock-docs` 作为直接编码输入文档;按方案实现应使用 `req-docs`。 +- 禁止把 `fallbackTopic` 当作最终命中直接实施改动。 +- 禁止在不满足触发门槛时执行跨 matcher 全量补检索。 diff --git a/packages/core/templates/zh-CN/rules/f2s-implement-tech-design.md b/packages/core/templates/zh-CN/rules/f2s-implement-tech-design.md new file mode 100644 index 0000000..ea6a7f0 --- /dev/null +++ b/packages/core/templates/zh-CN/rules/f2s-implement-tech-design.md @@ -0,0 +1,147 @@ +--- +description: 当用户要求根据技术方案文档实现可运行交付物时,按本规则执行(读文档、列任务、确认、实现、待完成列表与提醒)。用户会在对话中提供技术方案文档路径(MD 或 PDF);若为 PDF,先按 f2s-doc-pdf 转为 MD 再继续。 +globs: + - "**/.Knowledge/req-docs/**/*.md" +alwaysApply: false +--- + +> **任务路径**:凡 `.task/` 落盘与续作,**必须以 `rules/f2s-task` 解析的 `TASK_ROOT` 为准(`.task` 或 `.task/`;config → git → legacy)。下文若仍出现 `.task/todo.json` / `.task/active/`,均视为 **`TASK_ROOT/...` 的简写**。 + + +> **唯一长文**:本文件为 **implement-tech-design** 的完整执行条令。`.Knowledge/topics/f2s-implement-tech-design.md` 仅为路由摘要;**Codex** 读取 `.codex/topics/f2s-implement-tech-design.md`(由 `flow2spec init` 从本文件自动镜像)作为等效条令。 + +> 执行口径:统一知识库路径为 `/.Knowledge/`。下文所有路径均按 `.Knowledge` 约定解释。 + +# 基于技术方案实现交付物(通用) + +当用户要求根据**技术方案文档**实现可运行交付物时(用户会提供文档路径,如 `.Knowledge/req-docs/xxx.md` 或 PDF),按以下约定执行。 + +**目录约定**:`.Knowledge/req-docs/` 放“用于实现”的技术方案;`.Knowledge/stock-docs/` 放沉淀文档,不作为直接编码输入。 + +**触发说明**:本规则在打开 `req-docs` 下 `.md` 时自动加载(`**/req-docs/**/*.md`)。若对话前未打开技术方案,可在对话中 @ 本规则后再提供路径。 + +- 若用户提供的是 PDF:先执行 `f2s-doc-pdf`,将 PDF 转为 `.Knowledge/req-docs/` 下 MD,再继续。 +- 若用户提供的是 MD/文本:直接读取并进入实现流程。 + +--- + +## 一、目标与原则 + +- **目标**:基于技术方案实现可运行交付物,并与项目现有约定保持一致。交付物可以是前端页面/组件、后端接口/服务、数据处理逻辑、任务编排、脚本与配置等(按方案实际范围裁剪)。 +- **原则**: + 1. **先列任务再动手**:先输出「实现任务列表」,再提问与实现。 + 2. **先读后做**:先完整理解方案、边界、依赖、验收标准,再编码。 + 3. **对齐项目约定**:目录、命名、依赖、封装方式、错误处理与项目既有风格一致。 + 4. **缺项即问**:文档未明确的关键决策先向用户确认;未回复项进入待完成列表。 + 5. **实现后可执行**:必须给出验证方式与外部待办,确保用户可落地验收。 + +--- + +## 二、方案要素与实现映射(通用) + +| 技术方案内容 | 实现动作(按项目约定落地) | +| --- | --- | +| 需求目标 / 范围 / 非目标 | 明确本次实现边界,避免超范围开发。 | +| 关键流程 / 状态流转 / 时序 | 实现主流程与分支,关键判断处加简短注释。 | +| 数据结构 / 协议 / 字段约束 | 落地类型定义、模型、校验器或契约层。 | +| 接口 / 事件 / 消息 | 实现调用入口、事件处理、订阅或回调(按方案涉及项选择)。 | +| 页面 / 组件 / 交互 | 实现 UI 结构、状态管理、交互流程与容错提示(若方案涉及)。 | +| 配置 / 开关 / 环境差异 | 在项目约定位置注册并读取,补齐默认值和降级策略。 | +| 错误码 / 异常策略 / 重试 | 统一错误返回与日志策略,保持与现有封装一致。 | +| 发布 / 路由 / 权限 / 任务调度 | 实现对应代码并提醒用户完成平台侧配置(若方案涉及)。 | + +### 流程图处理(重要) + +- 若流程图是 PDF/图片且无文字步骤,先向用户索要文字版流程或补充文档; +- 若已有文字步骤,严格按顺序和分支实现; +- 无法确认分支时先提问,或按默认策略实现并写入待完成列表。 + +--- + +## 三、执行步骤 + +### 步骤 1:输入标准化 + +- PDF 输入:先执行 `f2s-doc-pdf`,得到 `.Knowledge/req-docs/*.md`。 +- MD/文本输入:直接读取。 + +### 步骤 2:理解方案与上下文 + +1. 读取技术方案全文,提取:目标、范围、流程、接口/交互、数据、配置、依赖、验收条件。 +2. 读取项目约定(如 README、`.Knowledge/stock-docs/`、架构说明、既有模块)以对齐实现风格。 +3. 若流程图缺文字说明,先记录缺口,进入步骤 3 一并向用户确认。 + +### 步骤 2.5:先输出实现任务列表(必做) + +在提问或编码前,必须先输出任务列表(可按方案裁剪): + +```markdown +## 实现任务列表(基于《xxx》技术方案) + +| 序号 | 任务项 | 说明 | +| --- | --- | --- | +| 1 | 核心结构与数据契约 | 落地类型/模型/校验规则,明确输入输出。 | +| 2 | 业务流程实现 | 按流程图/文字步骤实现主链路与分支。 | +| 3 | 对外能力接入 | 接口/事件/页面交互等对外入口实现。 | +| 4 | 配置与异常处理 | 配置注册、错误处理、重试/降级策略。 | +| 5 | 验证与收尾 | 自测说明、待完成列表、平台侧提醒。 | +``` + +若 `changeTracking.implement: true`,在输出任务列表后,按 `f2s-task` 规则将本清单写入 `.task/active//task.md`。 + +### 步骤 2.6:变更追踪与 `task.md` / `user-todos.md` 同步(仅当 `changeTracking.implement: true`) + +- 每完成实现任务列表中一项对应工作,**同一会话内**用 `Edit` 更新 `.task/active//task.md` 中对应 `[ ]`→`[x]`,禁止积压到收尾、禁止口头完成代替写盘(见 `f2s-task`「执行中」「中断与会话结束」)。 +- 执行过程中每出现**须用户执行**的项(改库、配环境等),**同会话内**追加到 `.task/active//user-todos.md`(见 `f2s-task`「user-todos.md」)。 + +### 步骤 3:实现前提问(必做,不可跳过) + +进入编码前,必须一次性列出未明确项并请用户确认。常见问题: + +- **范围与验收**:本次必须交付什么,哪些明确不做; +- **技术边界**:实现在哪个模块/端(前端、后端、脚本、数据任务等); +- **依赖与契约**:外部接口、消息协议、数据源、鉴权方式; +- **配置与环境**:配置 key、环境差异、默认值与灰度策略; +- **流程图缺口**:分支条件、失败回退、超时与重试策略; +- **发布约束**:路由、权限、调度、部署步骤是否已具备。 + +若用户未回复某项:按合理默认或占位实现,并在待完成列表中标注“需用户确认”。 + +### 步骤 4:按任务列表实现 + +按方案与项目实际裁剪顺序,建议: + +1. 先落地数据/契约与公共抽象; +2. 再实现主流程与核心能力; +3. 再接入入口层(接口/页面/事件/任务); +4. 最后补齐配置、异常处理、日志与测试辅助。 + +要求:复用现有依赖与封装;与项目命名/目录/风格一致;关键分支要可读、可维护。 + +### 步骤 5:收尾输出(必做) + +1. **待完成列表(必须)**:列出所有待用户或平台补齐项; +2. **实现后提醒清单(必须)**:按实际涉及内容提醒配置、依赖、数据、发布、权限、调度等; +3. **验证建议(建议)**:给出最小可执行验证步骤(本地、测试环境或回归路径)。 +4. **用户代办落盘(仅当 `changeTracking.implement: true`)**:将步骤 5 第 1–2 点中**须用户亲自执行**的条目(改库脚本、配置、审批等)**同步追加**到 `.task/active//user-todos.md`(若尚无该文件则先创建,见 `f2s-task`);禁止仅出现在对话或方案尾部的列表而不写入该文件。 +5. 若 `changeTracking.implement: true`:**先确认** `task.md`「步骤」已全部 `[x]`(或备注已记录取消项),满足 `f2s-task` 归档门禁后,再将 `.task/active//` 移至 `.task/completed/-/`,并从 `todo.json` 删除对应条目;禁止在仍有 `[ ]` 时归档。 + +--- + +## 四、可选补充 + +- 若方案命名不明确,可先给出命名建议并请用户确认; +- 若方案跨度大,可按“最小可用版本 -> 增量迭代”拆分阶段交付; +- 若用户希望沉淀知识库,可提醒后续用 `f2s-kb-build` 同步主题与路由。 + +--- + +## 五、约束与小结 + +- PDF 必须先转 MD,再进入实现流程; +- 不得跳过步骤 2.5(任务列表)与步骤 3(实现前提问)直接编码; +- 若 `changeTracking.implement: true`:不得跳过步骤 2.6(随实现进度写回 `task.md` checkbox,并追加 `user-todos.md`);归档须满足 `f2s-task` 归档门禁; +- 输出中必须包含待完成列表与实现后提醒清单;若 `changeTracking.implement: true`,其中用户侧项须同步写入 `user-todos.md`; +- 内容保持通用,不预设“仅后端”场景,按方案实际范围裁剪实现对象。 + +完成时可用一句话总结:已基于《xxx》技术方案完成本轮实现并给出待完成与验证建议,请按清单补齐平台与环境侧配置后验收。 diff --git a/packages/core/templates/zh-CN/rules/f2s-intent-routing.md b/packages/core/templates/zh-CN/rules/f2s-intent-routing.md new file mode 100644 index 0000000..869ef6a --- /dev/null +++ b/packages/core/templates/zh-CN/rules/f2s-intent-routing.md @@ -0,0 +1,90 @@ +--- +description: 意图识别:高置信操作意图自动进入对应 f2s-* Skill,由 intentRecognition 开关控制 +alwaysApply: true +--- + +# f2s 意图识别路由 + +## 前置 + +**执行本条前必须读 `flow2spec.config.json`**: + +- `intentRecognition: true` → 继续执行本条 +- `intentRecognition: false` 或字段不存在 → **跳过本条全部逻辑**,不做任何自动调用 + +## 优先级 + +1. 用户显式 `$f2s-*` 命令最高优先级,按显式命令执行。 +2. 用户明确说「只讨论 / 先别改 / 不要执行 / 先评估 / 先聊方案」时,禁止自动调用 Skill。 +3. 当前已进入某个 `f2s-*` 流程时,保持当前流程;不得自动切到其他流程,除非用户明确说「停止当前流程,改走 X」。 +4. **需求不完整禁自动进入撰写类技能**:用户要求改代码但需求不完整时,优先 `f2s-req-clarify`,不得直接进入 `f2s-kb-feat` / `f2s-kb-fix`;同理,用户要"出方案 / 生成技术方案"但需求含明显未决问题时,优先 `f2s-req-clarify`,**不得**直接进入 `f2s-req-tech`;用户要"拆任务 / 实现"但方案尚未落盘时,优先 `f2s-req-tech`,**不得**直接进入 `f2s-req-plan` / `implement-tech-design`。 +5. **过程编排型技能落盘后本轮不自动衔接下一技能(一处允许的单跳例外)**:`f2s-req-clarify` / `f2s-req-tech` / `f2s-req-plan` / `f2s-doc-*` 完成落盘后,**本轮**默认只输出"文档已就绪 + 下一步指引"一行提示即停止;**下一技能须由用户在新一轮明确触发**再由本条分流。**唯一允许的同轮单跳**:`f2s-req-clarify` 澄清文档落盘后直接自动衔接 `f2s-req-tech`(详见 `skills/f2s-req-clarify/SKILL.md` 结束段),此后不得再跳;`f2s-req-tech` 落盘后不得自动衔接 `f2s-req-plan` / `implement-tech-design`。 +6. 用户只是在询问、比较、评估、解释时,不调用 Skill。 +7. 低置信度或多意图冲突时,先用一句话说明候选分流并反问,不调用 Skill。 + +## 意图 → Skill 映射 + +用户输入**明确触发**以下操作意图,且不违反上文优先级时,Agent 可直接进入对应 Skill,不需要等用户二次确认: + +| 意图信号(示例) | 调用 Skill | +|----------------|-----------| +| 需求澄清、PRD 澄清、帮我理清需求、澄清一下 | `f2s-req-clarify` | +| 生成技术方案、出方案、技术设计 | `f2s-req-tech` | +| 提交代码、git commit、帮我提交、快捷提交 | `f2s-git-commit` | +| 新增能力、加功能、f2s-kb-feat | `f2s-kb-feat` | +| 修正实现规则、规则错了、f2s-kb-fix | `f2s-kb-fix` | +| 任务规划、创建任务 | `f2s-req-plan` | +| 知识库同步、全局同步、已实现能力同步 | `f2s-kb-sync` | +| 已有能力进知识库、多文件生成上下文 | `f2s-kb-add` | +| 新增规则、口述规则、把这条记到知识库 | `f2s-kb-addRules` | +| 生成项目上下文、终稿生成上下文 | `f2s-kb-build` | +| 合并上下文冲突、解决知识库冲突 | `f2s-kb-merge` | +| 知识库迁移、旧版迁移 | `f2s-kb-migrate` | +| 删除项目上下文 | `f2s-kb-rm` | +| 知识库模板升级、知识库升级、一键升级迁移 | `f2s-kb-upgrade` | +| 项目架构说明、架构初稿 | `f2s-doc-arch` | +| 转成终稿模版、f2s-doc-final | `f2s-doc-final` | +| 生成项目里程碑、里程碑 | `f2s-doc-milestone` | +| PDF 转 MD| `f2s-doc-pdf` | + +## 判断边界 + +**调用**:用户明确发起操作意图,且置信度高。 + +- "帮我做需求澄清" → 调用 `f2s-req-clarify` +- "生成一份技术方案" → 调用 `f2s-req-tech` +- "修复这个 bug,表现是 X,期望是 Y" → 调用 `f2s-kb-fix` +- "新增这个配置开关,默认 false,影响范围是 X" → 调用 `f2s-kb-feat` + +**不调用**:用户在询问或讨论,而非发起操作。 + +- "这个需求需要澄清吗?" → 先回答问题 +- "技术方案一般怎么写?" → 先回答问题 +- "f2s-req-tech 是干什么的?" → 先回答问题 +- "我们讨论一下这个能力怎么做" → 先讨论,不进入实现 +- "我想加一个能力,但还没想清楚" → 走澄清或反问,不进入 feat + +**判断依据**:有无明确的「帮我做 X」「执行 X」「开始 X」等动作性语义;仅询问、讨论、评估不触发。 + +## 分流说明 + +自动进入 Skill 前,先用一句话说明分流原因: + +```text +我按 处理:<一句话原因>。 +``` + +低置信度时只输出候选与反问: + +```text +这可能是 ,当前缺 <关键信息>,先确认后再进入流程。 +``` + +## 禁止项 + +- 在 `intentRecognition` 未读取或为 `false` 时自动调用任何 Skill +- 把询问类输入误判为操作意图 +- 在需求澄清未结束时自动跳到 feat/fix/plan/tech +- 在技术方案未落盘时自动跳到 `f2s-req-plan` / `implement-tech-design` +- 在过程编排型技能(`f2s-req-clarify` / `f2s-req-tech` / `f2s-req-plan` / `f2s-doc-*`)落盘的**同一轮**内自动衔接下一 `f2s-*` 技能(**唯一例外**:`f2s-req-clarify` → `f2s-req-tech` 单跳;`f2s-req-tech` 落盘后不得再自动衔接) +- 在当前流程未结束时自动切换到另一个 Skill diff --git a/packages/core/templates/zh-CN/rules/f2s-karpathy-guidelines.md b/packages/core/templates/zh-CN/rules/f2s-karpathy-guidelines.md new file mode 100644 index 0000000..e3eea32 --- /dev/null +++ b/packages/core/templates/zh-CN/rules/f2s-karpathy-guidelines.md @@ -0,0 +1,77 @@ +--- +description: Karpathy 式编码行为准则:先澄清假设、极简实现、只改必要处、用可验证目标驱动执行。与 f2s-* 规则并存;流程类硬约束以 f2s 为准。 +alwaysApply: true +--- + +# Karpathy 式编码行为准则 + +> 与项目内 Flow2Spec / `f2s-*` 规则**并行**;若某条与 f2s 强制步骤冲突,**以 f2s 与项目约定为准**。 + +用于减少常见「模型写代码」失误的行为约定。 + +**取舍:** 这些准则偏向**稳妥而非一味求快**;对明显琐碎的修改(如单行笔误)可自行把握,不必条条刻板执行。 + +## 1. 先想清楚再写代码 + +**不要默认、不要藏困惑、把权衡摆到台面上。** + +动手实现前: + +- **假设要说清楚**;不确定就问,不要猜。 +- **有多种理解时并列说明**,不要悄悄选一种就跑。 +- **若有更简单做法**,主动提出;该反对时要反对。 +- **说不清就停**:点名哪里困惑,再向用户要信息。 + +## 2. 简单优先 + +**用最少代码解决问题,不做臆测性扩展。** + +- 不要超出需求加功能。 +- 不要为只用一次的代码抽抽象。 +- 不要加未被要求的「灵活性」「可配置」。 +- 不要为几乎不可能的场景堆错误处理。 +- 若写了 200 行其实 50 行就够,**重写**。 + +自问:「资深工程师会不会觉得过度设计?」若是,就简化。 + +## 3. 手术式修改 + +**只动该动的;只收拾自己弄乱的。** + +改已有代码时: + +- 不要顺手「优化」相邻代码、注释或格式。 +- 不要重构没坏的东西。 +- **风格对齐现有代码**,即使你个人偏好不同。 +- 若发现与任务无关的死代码,**可以提一嘴,不要擅自删**。 + +若你的改动产生了孤儿引用/变量: + +- **删掉因你这次改动而不再使用的** import、变量、函数。 +- **不要**在用户未要求时删除**原本就存在**的死代码。 + +检验标准:**每一行改动都能追溯到用户的明确诉求。** + +## 4. 目标驱动执行 + +**先定义成功标准,再循环直到可验证地达成。** + +把任务变成可验证目标,例如: + +- 「加校验」→「先写非法入参测试,再改到通过」 +- 「修 bug」→「先写能复现的测试,再改到通过」 +- 「重构 X」→「前后测试套件均通过」 + +多步骤任务可写简短计划: + +``` +1. [步骤] → 验证:[检查方式] +2. [步骤] → 验证:[检查方式] +3. [步骤] → 验证:[检查方式] +``` + +成功标准越具体,越能独立迭代;含糊的「跑通就行」会逼出反复追问。 + +--- + +**准则在起作用的信号:** diff 里无关改动变少、因过度设计返工变少、**澄清问题出现在实现之前**而不是做错之后。 diff --git a/packages/core/templates/zh-CN/rules/f2s-kb-feedback-closing.md b/packages/core/templates/zh-CN/rules/f2s-kb-feedback-closing.md new file mode 100644 index 0000000..857f904 --- /dev/null +++ b/packages/core/templates/zh-CN/rules/f2s-kb-feedback-closing.md @@ -0,0 +1,115 @@ +--- +description: 普通问答读取源码后的知识库补充建议收口规则;只提示 f2s-kb-distill,不自动落盘 +--- +# Flow2Spec 知识库反馈收口 + +本条专管普通问答读取业务源码后的知识库补充建议。只决定最终回答是否需要追加一条极简提示。 + +## 适用范围 + +仅当同时满足以下条件时执行: + +- 本轮是**普通问答 / 排查 / 解释**; +- 本轮**未进入** `f2s-*` 技能、`implement-tech-design`、`f2s-git-commit` 或其他已有后续流程; +- 本轮读取过业务源码,且最终答案引用了源码事实。 + +**禁止**:以下两类情形不得输出本规则 case 1~4 中**任何一个**收口块—— + +1. **本轮已进入 `f2s-kb-distill`**:`f2s-kb-distill` 本身就是把本轮知识入库的技能,再贴自己的入库提示既冗余又自指。 +2. **本轮进入过程编排型技能**:`f2s-req-clarify` / `f2s-req-tech` / `f2s-req-plan` / `f2s-doc-arch` / `f2s-doc-final` / `f2s-doc-milestone` / `f2s-doc-pdf`。这些技能的产物是**面向本次交付的 `.Knowledge/req-docs/*`、`docs/*` 或任务规划物**,读源码是为了产出这些产物本身,不是"顺手补一条通用知识"。哪怕读了源码并将事实写进了澄清 / 方案 / 规划文档,也**不追加** distill 提示(澄清 / 方案文档本身归 `req-docs`,不是 `topics` / `stock-docs` 的入库对象;规划物随任务归档;文档类技能已有各自的落盘目标)。 + +其他 `f2s-kb-*` 技能(如 `f2s-kb-feat` / `f2s-kb-fix` / `f2s-kb-sync` 等)跑完之后**仍按四 case 正常判定**:若本轮回答里包含**主路径之外**、本次 SKILL **未入库**的可复用知识事实(典型场景:修 bug 时顺带读了另一模块源码、回答了与本次 SKILL 主体无关的衍生追问),照常输出收口块;agent 据本轮实际写入情况判断,不一刀切。 + +## 判断时机与依据 + +**判断时机**:在生成最终回答后,基于回答实际包含的知识内容判断,而非读取过程。 + +**判断依据**: +- 最终回答中补充了哪些 KB 未写或不够细的知识 +- 这些知识是否属于"可复用知识事实" +- 而非:读取过程中接触到的所有文件/信息 + +**可复用知识事实**包括: +- 核心机制(如:缓存语义、重试策略、降级逻辑) +- 状态流转(如:订单状态机、会话生命周期) +- 返回值 / 错误码契约(如:HTTP 状态码语义、业务错误码含义) +- 配置开关影响(如:开关 X 影响行为 Y) +- 失败回退策略(如:主路径失败时的降级方案) +- 模块边界或调用约定(如:模块 A 调用模块 B 的契约) +- 数据模型与字段语义(如:关键字段的业务含义) + +**仅作证据,不触发同步**的包括: +- 行号(如:`client.py:51`) +- 函数名(如:`send_message_to_session()`) +- 代码片段(用于演示的具体实现代码) +- 调用路径(如:`A → B → C` 的调用链) +- 为了回答用户追问而展开的局部实现 +- 对 topic 已写事实做源码核验(KB 已写清楚,源码只是印证) + +## 机械门禁 + +- 读完首个业务源码文件后,视为本轮已触发 `sourceFallbackUsed=true`。 +- `sourceFallbackUsed=true` 且最终答案引用源码事实时,发出回答前必须执行本条四 case 自检。 +- **四 case 必须显式表态**:每轮收口必须从 case 1~4 中选一个**明确输出对应块**,不允许悄悄跳过整个收口流程。 +- 判定逻辑: + - topic 命中 + 最终回答补充了"可复用知识事实" → 走 **case 2** + - topic 未命中 + 最终回答补充了"可复用知识事实" → 走 **case 1** + - topic 命中 + 最终回答仅包含"证据性内容"(行号/函数名/调用路径) + KB 已写清核心事实 → 走 **case 4** + - 若下钻前说明的是**机制/契约/流程类知识缺口**,答后走 **case 2** + - 若下钻前说明的只是**证据/源码位置/行号/实现出处缺口**,且 topic 已覆盖核心事实,可走 **case 4** + +## 四种收口 + +1. **KB 未覆盖 + 源码找到答案**:在答案末尾追加: + ```md + > 💡 可用 `f2s-kb-distill` 将本轮知识入库 + > + > **本轮将入库**:<一句话概要,点名「是什么能力 / 哪个模块 / 哪类知识」,例如:模块 X 的重试机制(首次入库)> + ``` + **判定条件**:没有 topic 覆盖该能力 / 模块 / 问题域,且最终回答补充了可复用知识事实。 + +2. **KB 已覆盖但不够细 + 源码补齐答案**:在答案末尾追加: + ```md + > 💡 可用 `f2s-kb-distill` 将本轮知识入库 + > + > **本轮将入库**:<一句话概要,点名「补到哪个 topic 的哪段」,例如:补充 `` 的「失败回退逻辑」一节> + ``` + **判定条件**:已有 topic 覆盖方向但缺少细节,且最终回答补充了可复用知识事实(核心机制、状态流转、契约等)。 + +3. **KB 与源码不一致**:以源码事实回答,并在答案末尾追加: + ```md + > 💡 可用 `f2s-kb-distill` 将本轮知识入库 + > + > **本轮将入库**:<一句话概要,点名「修正 `` 的哪条与源码不符的描述」> + ``` + +4. **KB 已完整覆盖,源码仅核验**:在答案末尾追加: + ```md + > **知识库已覆盖**:本轮答案核心事实已由 `` 完整提供,源码读取仅作核验。 + ``` + **判定条件**: + - KB 相关 topic 已写明本问题核心答案(机制、流转、契约等可复用知识事实) + - 本轮最终回答未引入 KB 之外的新的可复用知识事实 + - 回答中引用源码仅作为证据(行号、函数名、调用路径)或核验 KB 已写内容 + - 若下钻前说明缺口时提到的是机制/契约/流程类知识缺口,禁止走 case 4 + +> **概要要求(case 1~3 必填)**:必须一句话写明"这次跑 distill 会把什么入库"——能力 / 模块名 + 知识类型(机制 / 流转 / 契约 / 配置等)+ 是首次入库还是补充某 topic。**禁止**只贴命令不写概要;用户看了概要才能判断要不要接着跑 distill。 + +## case 1 和 case 2 的边界 + +- **case 1**(`f2s-kb-distill` 未覆盖场景):没有 topic 覆盖该能力 / 模块 / 问题域 + - 示例:用户问"模块 X 的重试机制",但 manifest 中没有任何 topic 与模块 X 相关 + - 示例:用户问"新功能 Y 的实现",KB 中完全没有功能 Y 的文档或 topic + +- **case 2**(`f2s-kb-distill` 补充场景):已有 topic 覆盖方向,但缺少细节 + - 示例:topic 写了"缓存优先策略",但没写具体的失败回退逻辑 + - 示例:topic 写了"动作链判定",但没写具体的状态检查方式 + +这样能避免"已有 topic 还建议 add"的误判。 + +## 输出格式 + +- case 1~3:输出一个 Markdown 引用块,依次写 `f2s-kb-distill` 命令 + 一行空行 + **本轮将入库**概要(一句话,见上文「概要要求」)。 +- case 4:输出一个 Markdown 引用块,标明"知识库已覆盖"+ 关联 topicId。 +- 禁止省略本块;禁止输出已读 KB 路径列表、覆盖对照表、原因解释或多行背景。 +- 只提示,不自动执行 `f2s-kb-distill`。 diff --git a/packages/core/templates/zh-CN/rules/f2s-knowledge-preflight.md b/packages/core/templates/zh-CN/rules/f2s-knowledge-preflight.md new file mode 100644 index 0000000..fb6b8dc --- /dev/null +++ b/packages/core/templates/zh-CN/rules/f2s-knowledge-preflight.md @@ -0,0 +1,72 @@ +--- +description: 普通提问也须先读 .Knowledge 机读路由,再搜代码;硬约束首工具调用 +alwaysApply: true +--- + +# Flow2Spec 知识库首读(KB Preflight) + +本条与 `f2s-flow2spec-unified-entry` **并存**;凡涉及**当前仓库**内实现、配置、排错与 Flow2Spec 知识路由的回答,**以本条约束「何时必须先读磁盘上的知识库」为准**。统一入口中的读取顺序在**满足本条之后**继续适用。 + +## 适用范围(须执行首读) + +用户问题若可能依赖下列任一类信息,即视为「须先走知识库」: + +- 当前仓库中的**实现代码**、目录与模块约定、构建/部署/运行时行为、`.Knowledge/`、`f2s-*` 技能、`manifest-routing` 所描述的主题路由等; +- 用户未明确声明「与当前仓库无关」、但语境明显依赖本仓库事实时。 + +## 硬约束:首工具调用 + +在给出实质性结论或修改建议之前: + +1. **在本轮用户消息下**,若尚未用工具读取过 **`.Knowledge/manifest-routing.json`**,则 **第一个** 使用的代码/知识库类工具 **必须** 为: + + `Read` → 路径 **`.Knowledge/manifest-routing.json`**(项目根相对路径,与统一入口一致)。 + +2. 读完 manifest 后,再按 `taskToTopicRules` / `matcherPath` **按需** `Read` **单个** matcher 分片与 **`.Knowledge/topics/.md`**(及 `topicDependencies`),然后才允许对 **除 `.Knowledge/` 以外的业务源码路径** 使用 `SemanticSearch`、`Grep` 或 `Read`。 + +3. **禁止**:在未执行步骤 1 的情况下,用「凭记忆/凭训练数据」直接断言本仓库特有的路径、配置或行为;若 manifest 或 topic 已明确覆盖,**须以 KB 为准**,源码用于印证或补全 KB 未写细节。 + +4. **回答末尾(简短一行即可)**:注明本轮依据的 KB 路径(例如「已读 manifest + `topics/.md`」);若 manifest 无命中且已读 `fallbackTopic` 对应 topic,写明「走 fallback 分诊」。 + +## 可跳过首读(极少数) + +- 用户仅询问 **IDE/编辑器本身**用法、且与当前仓库目录无关; +- 用户给出 **绝对路径 + 明确指令**(例如「只把该行改为 x」)且与业务知识无关的纯机械编辑; +- **同一会话内**已对当前工作区执行过 `Read(".Knowledge/manifest-routing.json")` 且用户未要求「重新路由/全量检查」的**直接续问**:可在回答首句写「manifest 已读本会话,沿用上次路由」,**不再重复 Read** manifest。 + +## 回答收口检查(源码补答后) + +普通问答读取业务源码后的知识库补充建议,以 **`f2s-kb-feedback-closing`** 为单一规则源。本条只保留触发关系:读完首个业务源码文件后,视为本轮已触发 `sourceFallbackUsed=true`;若最终答案引用源码事实,发出回答前必须执行 `f2s-kb-feedback-closing` 的四 case 自检。若本轮已经进入 `f2s-*` 技能、`implement-tech-design`、`f2s-git-commit` 或其他已有后续流程,不重复提示。 + +与 **`f2s-flow2spec-unified-entry`** 中 **「知识缺口与对策」** 一致;命中 **1b(命中但上下文不够)** 或 **2(库里没有对应文档)** 时,还须遵守: + +1. **先对用户说明,再扩工具**:在已读 `manifest-routing.json` 与应读的 `topics/*.md`(及依赖 topic)之后,若仍**无法仅凭 KB** 精确回答用户问题,**必须先**用自然语言说明:**已读哪些 KB 路径**、**仍缺哪类信息**、**你打算只读哪 1~2 个源码文件**或**请用户补哪篇文档**;不得沉默地连续堆叠「再找入口」式探索。 +2. **探索次数上限**:在未向用户发出上述缺口说明前,**禁止**连续发起 **4 次及以上**仅以扩大搜索面为目的的 `Grep` / 无明确目标的 `SemanticSearch`。说明并获用户默许(或问题明确要求追到底)后,再有序下钻。 +3. **单点下钻优先**:若仅需确认行为细节,应优先 **Read 一个**与问题最相关的实现文件并据此作答;**禁止**为同一子问题在无新假设的情况下链式深入第三方依赖目录中多文件,除非用户明确要求通读依赖。 + +### 缺口闸门(针对「规则有写、执行跳过」) + +当且仅当已读完 **manifest + 应读 topics** 后,仍判定为 **1b / 2**、且**下一步**要使用指向**业务源码树**(非 `.Knowledge/`)的 `Read` / `Grep` / `SemanticSearch` 时: + +- **必须先**产出一段**终端用户可见**的自然语言(可与简短结论同屏),且至少包含:**(a)** 已读的 KB 路径;**(b)** 缺口一句(topic 缺哪类信息或库中无文档);**(c)** 拟打开的 **1~2 个具体文件路径**或征询用户是否先补 `req-docs`/stock-docs。 +- **禁止**在**从未**输出过满足 (a)(b)(c) 的可见说明的情况下,连续发起多轮仅用于「再找入口」的源码侧工具调用(此即「规则已写但未先做缺口说明」的执行层遗漏)。 +- 下钻后给出**行为、状态码、错误文案**等事实时,**须以本次实际读到的源码与契约为准**;不得凭推测或与当前仓库无关的外部项目经验填写。 + +上述 (a)(b)(c) 与 **`f2s-flow2spec-unified-entry`** 表中「向用户要文档或路径」「明确承认知识库无覆盖」**同一语义**:不得用「已在内心判定为 1b」代替**已对用户写出**的缺口说明。 + +### 与执行环境的交互(权限、确认类噪音) + +在带沙箱或权限门控的 IDE 中,**短时间连续**发起多轮 `Grep` / `SemanticSearch` / 大范围读盘,常表现为**对同类权限或确认的重复询问**。这与 Flow2Spec 规则是否写明无直接关系,多由**探索链过长、无停止条件**放大。遵守本节「缺口闸门」「探索次数上限」与下文「检索体积与作答节奏」、优先单文件 `Read`,可显著减少此类打扰。 + +## 检索体积与作答节奏(降低多轮扫描、体感变慢) + +本节针对 **Codex / 终端 IDE** 等环境中「一次问答多轮 `grep`、输出量巨大、总耗时长」的常见根因,与「首读 manifest」**不冲突**:仍须先 `Read` manifest,再收窄搜索面。 + +1. **`Grep` / 文本搜索范围**:在已读命中 topic 且其中给出**具体文件或目录路径**时,搜索**不得**大于该路径;无路径时再缩到**单一**最可能目录(如 `src/utils/` 或 `src/functions/<活动目录>/`)。**禁止**在无用户明确要求「全仓检查」且未满足统一入口「全量补检索触发门槛」时,对 **`src/` 根、`src/functions` 全域、`.Knowledge` 等多处并列**做一次超大范围扫描。 +2. **大命中量时**:若单次搜索命中行数明显过多,应**停止扩大关键词或路径**,改为优先 **`Read` topic 或 stock-docs 已点名的 1~2 个主文件**;仍不足再缩小模式或目录做**第二次**窄 `Grep`。 +3. **两阶段作答**:用户未明确要求「列出全部实现细节 / 通读依赖 / 审计全链路」时,在 manifest + 应读 topics(及 stock/req 中 topic 已指明的材料)已足以形成结论时,**可先输出对用户有用的短答案**;实现细节、额外文件清单仅在用户追问依据或「展开」时再下钻,**禁止**仅为自验完备而拉长探索链。 +4. **避免重复读盘**:本会话内已对某文件做过**全文** `Read` 且无新用户指令或新假设时,**禁止**再次对该文件发起等价全文 `Read`。 + +## 对 Agent 自检 + +若你发现自己在回答当前仓库相关问题时尚未 `Read` manifest,**须立即停止补写**,先 `Read` manifest 与命中 topic,再更正或续答。若本轮读取过业务源码并引用源码事实,发出回答前须继续执行 `f2s-kb-feedback-closing`。 diff --git a/packages/core/templates/zh-CN/rules/f2s-stock-docs-vs-req-docs.md b/packages/core/templates/zh-CN/rules/f2s-stock-docs-vs-req-docs.md new file mode 100644 index 0000000..9f47014 --- /dev/null +++ b/packages/core/templates/zh-CN/rules/f2s-stock-docs-vs-req-docs.md @@ -0,0 +1,16 @@ +--- +description: 区分 .Knowledge/stock-docs(存量上下文)与 .Knowledge/req-docs(需求与技术方案);禁止混用路径与链出目标 +globs: + - "**/.Knowledge/stock-docs/**/*.md" + - "**/.Knowledge/req-docs/**/*.md" +alwaysApply: false +--- + +> **唯一长文**:本文件为 **f2s-doc-routing** 的完整约定。`.Knowledge/topics/f2s-stock-docs-vs-req-docs.md` 仅为路由摘要;**Codex** 读取 `.codex/topics/f2s-stock-docs-vs-req-docs.md`(由 `flow2spec init` 从本文件自动镜像)作为等效条令。 + +# stock-docs 与 req-docs + +- **`.Knowledge/stock-docs/`**:PDF/初稿/终稿/架构说明等**存量源文档**;`f2s-kb-build`、`f2s-doc-final`、`f2s-doc-arch`、`f2s-kb-add` 的文档落盘优先在此。`sourceDoc` 统一写 `.Knowledge/stock-docs/<文件名>.md`。 +- **`.Knowledge/req-docs/`**:需求澄清、技术方案(前后端/数据/任务等)、`f2s-doc-pdf` 输出的「按方案实现」MD;`implement-tech-design` 的触发范围为 `.Knowledge/req-docs/**/*.md`。 + +完整约定见本规则与 **`skills/f2s-doc-routing/SKILL.md`**;`.Knowledge/topics/f2s-stock-docs-vs-req-docs.md` 为路由摘要。 diff --git a/packages/core/templates/zh-CN/rules/f2s-task.md b/packages/core/templates/zh-CN/rules/f2s-task.md new file mode 100644 index 0000000..6981ece --- /dev/null +++ b/packages/core/templates/zh-CN/rules/f2s-task.md @@ -0,0 +1,307 @@ +--- +name: f2s-task +description: > + 变更追踪:代码变更时自动创建并维护 .task/ 下的任务清单,支持跨会话续作。 + 仅当 flow2spec.config.json 中 changeTracking.feat / fix / implement 之一为 true 时,对应技能生效。 + 触发词:changeTracking、任务追踪、变更追踪、续作、继续上次任务 +alwaysApply: true +--- + +# f2s-task(变更追踪规则) + +## 生效条件 + +各技能按自身子项判断: + +- `f2s-kb-feat`:读 `changeTracking.feat` +- `f2s-kb-fix`:读 `changeTracking.fix` +- `f2s-implement-tech-design`:读 `changeTracking.implement` + +若对应子项为 `false` 或字段不存在,**该技能内的变更追踪步骤不执行**,直接跳过。 + +> `f2s-req-plan` 命令不受此条件约束,始终执行(见 `skills/f2s-req-plan/SKILL.md`)。 + +## 多人协作与任务根 `TASK_ROOT`(必须先解析) + +进入本规则任何「读/写 `.task`」步骤前,**必须** `Read("flow2spec.config.json")`,并解析 **`TASK_ROOT`**(本会话内固定,禁止中途改 id): + +| 条件 | `TASK_ROOT` | `developerId` 来源 | +| --- | --- | --- | +| `collaboration.enabled === false` | `.task` | legacy(强制单人根) | +| `collaboration.developerId` 非空(trim 后) | `.task/` | **config** | +| 否则能读到 `git config user.email` | `.task/` | **git-email** | +| 否则能读到 `git config user.name` | `.task/` | **git-name** | +| 仍无 | `.task` | **legacy**(单人旧布局;须在回复中提示:建议配置 `collaboration.developerId`) | + +**sanitize**:小写;若含 `@` 只取本地部分;非 `[a-z0-9]` 收成 `-`;去首尾 `-`;长度 1–64,否则视为无 id。 + +**路径一律用 `TASK_ROOT` 前缀**(下面凡写 `TASK_ROOT/...` 均指解析结果): + +- 索引:`TASK_ROOT/todo.json` +- 进行中:`TASK_ROOT/active//` +- 已完成:`TASK_ROOT/completed/-/` + +**防串戏(硬约束)**: + +1. **只**读写当前会话的 `TASK_ROOT`;**禁止**为续作/匹配去遍历 `.task/*/todo.json` 或其他 developer 目录。 +2. keywords 匹配范围 **仅** 当前 `TASK_ROOT/todo.json` 内条目。 +3. 创建任务时 `todo.json` 的 `folder` 必须写成当前 `TASK_ROOT/active//`(posix 相对路径)。 +4. 可选在任务开始时向用户回显一行:`[task] developerId= TASK_ROOT=`。 +5. **`.Knowledge/` 仍为全员共享**;本规则不按人拆分知识库。 + +> 实现参考(CLI/工具):包内 `lib/developerId.js` 的 `resolveDeveloperContext` / `taskRootFor`(与上表同口径)。 + +## f2s-req-plan 调用时的绑定 + +执行 **`f2s-req-plan`**(或续作命中 `linkedSkill: "f2s-req-plan"`)时: + +- **不受** `changeTracking.feat` / `fix` / `implement` 限制,但 **必须** 按本规则「任务开始 / 执行中 / 中断与会话结束 / 任务完成 / 新会话续作」维护 **`TASK_ROOT`** 下任务树; +- 技能 **步骤 0** 须 `Read` 本规则全文(**Cursor/Claude**:`rules/f2s-task.*`;**Codex**:`.codex/topics/f2s-task.md`); +- 落盘、打钩、归档、`user-todos.md` / `acceptance.md` 格式 **以本规则为准**;技能正文不得省略 `todo.json` / `user-todos.md` / `acceptance.md`,不得改写归档目录命名(`-`)。 + +## 目录结构 + +``` +TASK_ROOT/ ← `.task` 或 `.task/` +├── todo.json ← 活跃任务索引,仅主 agent 写 +├── active/ +│ └── / +│ ├── task.md ← checklist(执行步骤) +│ ├── context.md ← 涉及文件路径、相关资料链接 +│ ├── user-todos.md ← 须用户执行的代办(改库、配环境等),见下文 +│ └── acceptance.md ← 验收清单:task.md 全部 [x] 后、归档前生成,见下文 +└── completed/ + └── -/ + ├── task.md + ├── context.md + ├── user-todos.md ← 随任务一并归档,便于验收后逐项消项 + └── acceptance.md ← 随任务一并归档,便于用户最终核对 +``` + +**归档目录命名**:`completed/` 下文件夹名为 **`-`**(**本地日历日期 8 位在前**,`` 与 `active/` 下一致、为 snake_case;便于按时间排序)。**新归档一律使用本格式**;仓库中已有的旧式 `-` 目录可保留,择机人工重命名即可。 + +**从单人布局迁移**:若磁盘仍有根级 `.task/active/` 与 `.task/todo.json`,而当前已解析出非 legacy 的 `TASK_ROOT=.task/`,可在用户确认后将旧 `active/*` 与条目迁入新根(一次性);未确认前 **不要** 自动挪动他人可能共用的根目录。 + +## todo.json 结构 + +```json +[ + { + "name": "任务名称", + "folder": "TASK_ROOT/active//", + "keywords": ["关键词1", "关键词2"], + "linkedSkill": "f2s-kb-fix", + "createdAt": "YYYY-MM-DD", + "assignee": "" + } +] +``` + +`folder` 落盘时须写成真实相对路径(例如 `.task/alice/active/fix_foo/` 或 legacy 的 `.task/active/fix_foo/`)。`assignee` 建议写入当前 `developerId`(legacy 可写 `"legacy"` 或省略)。 + +**写权约束**:`todo.json` 仅由主 agent 写,禁止子 agent 修改。 + +## 任务开始(代码变更前) + +0. 按上文解析并固定 **`TASK_ROOT`**(及 developerId / legacy)。 +1. 检查 `TASK_ROOT/todo.json` 是否存在活跃任务。 +2. 将用户输入与**该文件内**各条目 `keywords` 匹配(**禁止**读取其他 `TASK_ROOT`): + - 命中一个 → 加载对应 `task.md`、`context.md`,**若存在** `user-todos.md` 则一并加载,展示剩余清单与未消用户代办 + - 命中多个 → 列出候选,让用户选择 + - 无命中 → 确认任务名称后创建新任务 +3. 创建新任务(无命中时): + a. 确认任务名称(snake_case,简短描述变更内容) + b. 在 `TASK_ROOT/active//` 创建文件夹 + c. 将本次工作步骤写入 `task.md` + d. 将涉及文件路径和相关资料链接写入 `context.md` + e. **创建 `user-todos.md`**(固定文件名,与 `task.md` 同目录):见下文「`user-todos.md` 格式与写盘义务」;尚无代办时可写入占位说明 + f. 在 `TASK_ROOT/todo.json` 新增条目(仅主 agent 写;`folder` 指向本任务目录) + +## 执行中 + +- 每完成一个步骤,**立即**用 `Edit` / `Write` 将 `task.md` 中对应 checkbox 由 `[ ]` 改为 `[x]`(与代码改动同等对待,**禁止**仅靠会话内口头宣称「已完成」代替磁盘更新) +- 禁止批量勾选或跳步 +- **用户代办须落盘**:凡须任务责任人(用户)在本机、数据库、配置平台或流程上完成的项(例如执行 DDL/DML、填密钥、点审批、发版、补数据),**同一会话内**追加写入 `user-todos.md`(`Edit` 追加小节或列表项),**禁止**仅在对话里交代而不写入该文件;可与对话摘要并存,以磁盘文件为交接真值 + +## 中断与会话结束(硬约束) + +- **长记忆以 `task.md` 的 checkbox 为真值**:下一会话通过「首个仍为 `[ ]` 的步骤」定位进度;未写盘则续作失真。 +- 本会话内每真实完成 `task.md` 所列一步:**当步**打钩,不得积压到归档前一次性勾选。 +- 若用户结束对话、工具流中断、或预计无法继续:在结束前至少打钩**已真实完成**的步骤,并在「## 备注」写明阻塞原因或「下一会话从步骤 N 继续」;**禁止**在未更新 `task.md` 的情况下直接结束(否则等同丢失进度信号)。 +- 中断前若本会话已识别出**用户代办**:**必须**写入或追加到 `user-todos.md`,避免下一会话丢失「交给用户的事」。 +- 若本会话为子任务创建过 **`git worktree`** 或等价隔离目录:结束前按 **`f2s-flow2spec-unified-entry`**「Git worktree 与子任务工作目录卫生」完成移除或写明残留路径与删除命令(必要时写入 `user-todos.md`)。 + +## 任务完成 + +**归档门禁(须先于移动目录自检)**: + +- 将目录移入 `completed/` **当且仅当** `task.md` 的「## 步骤」下,与本次交付相关的条目**全部为 `[x]`**(或用户明确取消的项已在「## 备注」说明,且对应列表项已改为 `[x]` / 已删除该项并注明取消)。 +- `task.md` 全部 `[x]` 后、移动目录前,**必须**已生成或更新 `acceptance.md`(见下文「acceptance.md 格式与写盘义务」);缺失 `acceptance.md` 或仍为创建任务时的占位说明 → 视为门禁未过,禁止归档。 +- 若仍存在 `[ ]`:**禁止**移动 `active` → `completed/`、**禁止**从 `todo.json` 删除该条目;应先回到「执行中」补完或改清单后再归档。 + +完成上述门禁后: + +1. 将 `TASK_ROOT/active//` 整体移至 `TASK_ROOT/completed/-/` +2. 从 `TASK_ROOT/todo.json` 删除该条目 +3. 若 `todo.json` 变为空数组,删除该文件 + +## 新会话续作 + +新会话开始时,先解析 **`TASK_ROOT`**;若存在 `TASK_ROOT/todo.json`: + +1. **仅**读取该文件中的活跃任务(禁止合并其他 developer 的 todo) +2. 将用户首条消息与各条目 `keywords` 匹配 +3. 命中则展示剩余 checklist,**若存在 `user-todos.md` 则摘要其中仍为 `- [ ]` 的用户代办**;**若存在 `acceptance.md` 则提示其当前形态**(占位 / 已成稿;归档前必须成稿);提示「检测到未完成任务,是否继续?」 +4. 用户确认后:**若 `linkedSkill` 非空,先加载对应技能规则文件(配置根 `skills//SKILL.md`)作为执行上下文**,再按 `task.md` 剩余步骤继续——技能的落盘约束、文风规则、自检清单全部生效,与首次调用一致 +5. 无命中则不打扰,正常响应 + +**孤儿 `active/`(`todo.json` 缺失或损坏)**:若**当前 `TASK_ROOT`** 下仍存在 `active//` 且其中 `task.md` 含未勾选步骤,应 `Read` 该 `task.md` 并提示用户是否续作;续作前宜按「任务开始」一节恢复或补写 `todo.json`(仅主 agent)。**禁止**扫描其他 `.task//active/` 当作孤儿续作。 + +## task.md 格式 + +```markdown +# <任务名> + +## 步骤 +- [ ] 步骤1 +- [ ] 步骤2 +- [x] 步骤3(已完成) + +## 备注 +<执行中的发现、决策等> +``` + +## context.md 格式 + +```markdown +# <任务名> 上下文 + +## 涉及文件 +- `src/<模块>/callback.js` +- `src/<模块>/retry.js` + +## 相关资料 +- `.Knowledge/req-docs/<能力>-spec.md` +- `.Knowledge/stock-docs/<能力>-arch.md` + +## 用户代办清单 +- 见同目录 `user-todos.md`(须用户执行的项统一写在该文件,勿仅在对话中罗列) + +## 验收 +- 见同目录 `acceptance.md`(task.md 全部 `[x]` 后、归档前生成) +``` + +## user-todos.md 格式与写盘义务 + +**路径**:`TASK_ROOT/active//user-todos.md`(归档后位于 `TASK_ROOT/completed/-/user-todos.md`)。**固定文件名** `user-todos.md`,便于 Hook 与脚本引用。 + +**用途**:汇总 **Agent 无法代劳**、必须由用户(或持权人在平台)完成的项,例如: + +- 在指定环境执行 SQL / 迁移脚本(可引用 `req-docs` 或仓库内 `.sql` 路径) +- 配置中心 / 环境变量 / 密钥 / 白名单 +- 发布、审批、工单、外部系统开关 + +**写盘义务**: + +1. **创建任务时**(`f2s-task`「任务开始」步骤 3.e):创建该文件;可含简短说明 + 空列表。 +2. **执行中**:每出现一类新的用户代办,**当次**追加(推荐按日期分二级标题 `## YYYY-MM-DD`,下列 `- [ ]` 可勾选项或步骤编号)。 +3. **与 `task.md` 分工**:`task.md` 管 Agent 侧步骤 checkbox;`user-todos.md` 管用户侧待办;**勿**把「仅用户可执行」的长操作说明只写在 `task.md` 步骤正文代替本文件。 +4. **续作**:加载任务时 `Read` 本文件,向用户展示仍未勾选的 `- [ ]` 项(若有)。 + +**示例结构**: + +```markdown +# 用户代办清单 + +> Agent 追加;用户完成后可将对应 `- [ ]` 改为 `- [x]` 或删除该行。 + +## 2026-05-09 + +- [ ] 在目标环境执行:`.Knowledge/req-docs/xxx.sql`(先备份) +- [ ] 在配置中心打开功能开关 `feature.foo.enabled` + +## 2026-05-10 + +- [ ] 生产发版后回写实际版本号到本文档备注 +``` + +## acceptance.md 格式与写盘义务 + +**路径**:`TASK_ROOT/active//acceptance.md`(归档后位于 `TASK_ROOT/completed/-/acceptance.md`)。**固定文件名** `acceptance.md`,与 `task.md` / `user-todos.md` 同目录。 + +**用途**:Agent 在 `task.md` 全部 `[x]` 后、归档前,依据本次实际交付沉淀的**验收清单**:用户照单逐项核对就能确认「这次任务真的做完了」。与 `user-todos.md` **职责分离**: + +| 文件 | 谁在做 | 内容焦点 | +| --- | --- | --- | +| `task.md` | Agent | 实现步骤的进度 checkbox | +| `user-todos.md` | 用户 | **代办**:Agent 做不了、必须用户在外部(库 / 平台 / 审批)执行的事 | +| `acceptance.md` | 用户 | **验收**:本轮 Agent 已交付项,用户核对是否真的可用 | + +**生效范围**:凡使用 `.task/` 的任务均生成(自动模式 `changeTracking.feat` / `fix` / `implement` 命中、以及显式模式 `f2s-req-plan`);不区分技能。 + +**写盘义务**: + +1. **创建任务时**(`f2s-task`「任务开始」步骤 3.e 之后):**可同时创建** `acceptance.md` 并写占位说明(如「task.md 全部 `[x]` 后由 Agent 在此填入验收清单」);尚未实现时**不得**预先写入验收点,避免与最终交付脱节。 +2. **执行中**:原则上**不写**;若交付边界发生重大变化,可在「## 备注」一行记录,最终成稿时再统一整理。 +3. **task.md 全部 `[x]` 后、归档前**(**必写**):Agent 基于本次实际改动整理为正式验收清单;占位说明须被替换为成稿。**这是归档门禁**(见「任务完成」)。 +4. **续作**:加载任务时 `Read` 本文件,向用户展示当前形态(占位 / 已成稿)。 + +**内容形态**:可勾选 `- [ ]` 列表 + 验收方式。每项形如: + +```markdown +- [ ] <验收点:交付了什么>(验收方式:<查看哪份文件 / 跑哪条命令 / 看哪个页面>) +``` + +按交付域分二级标题分组(如 `## 代码`、`## 规则与知识库`、`## 任务清单本体`)。**勿**重复列 `task.md` 的执行步骤;**勿**把 `user-todos.md` 中「用户代办」搬入此处。 + +**示例结构**: + +```markdown +# 验收清单 + +> Agent 整理;用户核对后可将对应 `- [ ]` 改为 `- [x]`。 + +## 代码 + +- [ ] `src/<模块>/<文件>.ts`:<改动点>(验收方式:阅读该文件 / 跑 `npm test -- <文件>`) + +## 规则与知识库 + +- [ ] `.Knowledge/topics/.md`:<新增/修订说明>(验收方式:打开该文件确认章节齐全) +- [ ] `.Knowledge/manifest-routing.json`:<是否变更与原因>(验收方式:阅读对应字段) + +## 任务清单本体 + +- [ ] `TASK_ROOT/completed/-/` 目录齐全:`task.md` / `context.md` / `user-todos.md` / `acceptance.md` +- [ ] `TASK_ROOT/todo.json` 已删除对应条目(或文件已删除,若数组变空) +``` + +## 推荐 Hook 配置(Claude Code) + +在项目 `.claude/settings.json` 中添加,每次文件变更前将活跃任务注入上下文(示例为 **legacy** 单根;多人请改为当前 `TASK_ROOT/todo.json`,或在命令内按 `flow2spec.config.json` + git 解析路径): + +```json +{ + "hooks": { + "PreToolUse": [{ + "matcher": "Edit|Write", + "hooks": [{ + "type": "command", + "command": "node -e \"try{const f='.task/todo.json',fs=require('fs');if(fs.existsSync(f)){const t=JSON.parse(fs.readFileSync(f,'utf8'));if(t.length)console.log('[task] 活跃任务: '+t.map(x=>x.name).join(', '))}}catch(e){}\" 2>/dev/null || true" + }] + }] + } +} +``` + +## 禁止项 + +- 禁止子 agent 写入 `todo.json` +- 禁止在所有步骤完成前将任务移至 `completed/` +- 禁止批量勾选 checkbox(必须逐步勾选) +- 禁止在 `changeTracking.feat` / `changeTracking.fix` / `changeTracking.implement` 均为 `false` 或字段不存在时创建任务目录(`f2s-req-plan` 不受此约束) +- 禁止在已使用任务清单的流程中,将「须用户执行的代办」**仅**写在对话或仅写在 `task.md` 而**不**追加到 `user-todos.md`(无代办时文件可保持占位说明) +- 禁止在 `acceptance.md` 仍为占位说明、或缺失该文件时归档;禁止把 `user-todos.md`(用户代办)与 `acceptance.md`(用户验收)合并写入同一文件 +- 禁止在任务实现完成前预先写入具体验收点(仅可写占位说明),避免与实际交付脱节 +- **禁止**为匹配/续作遍历其他 developer 的 `.task//` 或合并多人 `todo.json` +- **禁止**在未解析 `TASK_ROOT` 的情况下默认写入仓库根 `.task/active/`(除非当前解析结果即为 legacy `.task`) diff --git a/packages/core/templates/zh-CN/rules/f2s-topic-authoring.md b/packages/core/templates/zh-CN/rules/f2s-topic-authoring.md new file mode 100644 index 0000000..b2ac24b --- /dev/null +++ b/packages/core/templates/zh-CN/rules/f2s-topic-authoring.md @@ -0,0 +1,127 @@ +--- +description: Flow2Spec 主题创作准则:topic 命名 / 骨架 / topicMetadata / topicDependencies 判定 / rule 是否需建对应 topic / 写盘权属指针 +alwaysApply: false +--- +# Flow2Spec 主题创作准则(Topic Authoring) + +本条为 **创作侧** 单一事实源;凡 `f2s-*` 技能在新增或修改 `.Knowledge/topics/.md`、调整 `manifest-routing.topicMetadata` / `manifest-routing.topicDependencies`、删除 / 迁移 topic 时,**必须先 Read 本条全文**,再按对应 SKILL 的步骤继续。与 `f2s-flow2spec-unified-entry`(消费侧)**并存**;硬冲突时以统一入口为准。 + +## 适用范围 + +满足下列任一即「触达本条」: + +- 新增或重写 `.Knowledge/topics/.md`; +- 修改既有 topic 的标题 / 适用场景 / 关键流程边界; +- 新增、删除或调整 `manifest-routing.topicMetadata`; +- 在 `manifest-routing.topicDependencies` 中新增、删除或调整依赖边; +- 在 `taskToTopicRules[].topics` 中新增引用某个 topic id; +- 删除或迁移 topic(`f2s-kb-rm` / `f2s-kb-migrate` / `f2s-kb-upgrade`)。 + +## 1. topic 命名 + +- **id**:`kebab-case`,与 `manifest-routing.topicPaths` 的 key 一致。 +- **文件名**:`.Knowledge/topics/.md`;若该 topic 与同名 `f2s-*` 技能 / 规则强绑定(如 `f2s-task` / `f2s-req-plan`),文件名可加 `f2s-` 前缀以示同源。 +- **不要**:版本后缀(`-v2` / `-new`)、个人花名、与 `index.md` 行级标题冲突的同义词。 + +## 2. topic 定位与正文骨架 + +**topic 的定位**:可执行路由摘要 + 关键边界。topic 可以包含必要的边界说明、关键流程步骤、禁止项、配置摘要——Agent 读完即可执行或判断是否需要继续下钻;**不应承载**完整实现细节、长文背景或可在 stock-doc 里查的原始内容。stock-doc 承载完整背景与长文细节,topic 指向它。 + +**长文背景引用的目录边界(硬约束)**:topic 中「详细背景 / 相关资料 / 长文来源 / 参考文档」等**指向长文源**的引用槽位,**只允许**指向 `.Knowledge/stock-docs/*_终稿.md` 或已被归档为长文事实的 `stock-docs/*`;**禁止**把这类槽位挂到 `.Knowledge/req-docs/*`(含澄清 / 技术方案 / SQL / PRD 等)——`req-docs` 是本次交付的**临时输入**,用完会随任务归档或迁移,作为 topic 的长文事实源是悬空引用。若同步 / 新建 topic 时相应 `stock-docs/*_终稿.md` 尚未生成,**必须先触发 `f2s-doc-final` 沉淀终稿**(或与用户确认由手写补齐),再让 topic 指向终稿;不得跳过终稿直接把 topic 挂在 `req-docs` 上。**允许**:topic 正文可**短引**方案里的一句结论或一个字段名作为佐证(如「见 `.Knowledge/req-docs/xxx_技术方案.md`」的偶发点引),但**长文背景槽位**("详细背景 / 相关资料"整节)仍须指向 stock-doc。 + +每个 topic 至少包含: + +1. **标题与一句话意图**(一行写清"该 topic 解决什么"); +2. **适用场景 / 触发词**(与对应 `matchers/.json` `includeAny` 语义一致); +3. **核心规则 / 流程**(可执行知识;步骤须可由 Agent 复现); +4. **依赖声明**(若 `topicDependencies` 中存在依赖项,正文须显式写一句「执行前须先读依赖主题 ``」,参考 `topics/f2s-req-plan.md` 首段写法); +5. **边界与禁止项**(避免膨胀到隔壁 topic); +6. **长文背景 / 详细资料引用**(如需承载业务背景):只列 `.Knowledge/stock-docs/*_终稿.md` 的可点击 Markdown 链接(1–3 条即可);**禁止**直接列 `.Knowledge/req-docs/*` 作为长文背景来源;无对应终稿时**先生成终稿**再回填此小节。 + +## 3. topicMetadata 判定准则 + +`topicMetadata` 是治理元数据,只影响盘点、审计和阅读预期;不参与 matcher 命中,不决定是否读取 topic,不改变执行强制性。执行强制性以 `AGENTS.md`、rules、skills 与 topic 正文明确要求为准。 + +字段: + +- `primary`:主分类,单值,取 `feature` / `module` / `config` / `policy`。 +- `tags`:可选,数组,取值范围同 `primary`,不得与 `primary` 重复。用于描述 topic 同时包含的次要性质,仅作审计/阅读预期,不参与路由或执行。 +- `confidence`:取 `manual` / `inferred`。 + +判定: + +1. `topicMetadata` key 必须存在于 `topicPaths`;仅给已存在或本次确认创建的 topicId 写入。 +2. `primary` 取 topic 最核心的性质:读 topic 正文,判断其主要内容属于哪个类型,写入 `primary`。 +3. `config`:配置项、开关、默认值、初始化参数;仅当这些内容构成 topic 的主要语义时才可作为 `primary`。 +4. `policy`:流程、规则、约束、门禁、禁止项、agent 编排、技能步骤;仅当这些内容构成 topic 的主要语义时才可作为 `primary`。。 +5. `feature`:已落地业务 / 产品能力。 +6. `module`:公共能力、公共包、模块边界与工程结构 +7. topic 同时覆盖多个性质时,最主要性质写 `primary`,其余明确成立的性质写 `tags`(可选数组,元素取值同 `primary`,不得与 `primary` 重复)。 +8. `manual` 仅用于用户或维护者明确确认分类值;有明确证据但未人工确认分类值时写 `inferred`。证据不足时**不写 metadata**,但须在摘要中列出推断方向与依据(如「建议 policy,正文含多处强制约束」),供用户确认后手动补写 `manual`。**禁止仅凭 topicId 名称推断分类,必须 Read topic 正文后再判断。** **`inferred` 不需要用户事先同意即可直接落盘**:证据足够时按本条直接写入;只有当用户/维护者主动指定分类、或证据矛盾需要决断时,才升级为 `manual`。把 `inferred` 当作"待用户同意"是常见误读,等同于把"有依据的自动归类"硬变成"必须人工确认",与本条第 7 项允许 `inferred` 落盘的语义冲突。 + +禁止:为了分类创建、重命名、拆分 topic;在 topic markdown 正文或 `index.md` 中重复写分类块。 + +## 4. topicDependencies 判定准则 + +设当前主题为 A、候选依赖为 B。**四问命中任一即声明 `A → B`**: + +1. **前置规则强引用**:A 的执行步骤**显式提到** B 的术语 / 产物 / 落盘约束(例:`f2s-req-plan` 要求「按 `f2s-task` 维护 `.task/`」)。 +2. **缺 B 必出错**:仅读 A 不读 B 能否产出对的结果?答否——典型为 A 写"怎么做"、B 写"在哪做 / 用哪份输入"。 +3. **共享落盘目标**:A、B 写同一组文件且 B 定义写盘格式(如 `.task/`、`.Knowledge/topics/`)。 +4. **fallback 跳转 B**:A 自身覆盖不全,按现有约定回落 B 兜底。 + +**反向排除**(避免依赖膨胀): + +- 仅术语相邻(都谈"知识库")→ 不写依赖,靠 `index.md` 语义边界即可。 +- 跨主题信息互查(A 想"了解一下" B)→ 不写依赖,靠 `taskToTopicRules` 次高候选 + `expand` 补召回。 +- **概述 → 详情导航**:大功能主 topic 与其子模块 topic 之间是"关联/导航"关系,不是强前置依赖——子模块 topic 通过各自的 matcher 独立命中,不写 `A → B`;主 topic 正文里写子模块 stock-doc 的可点击链接作为导航入口。 +- **传递依赖不重复声明**:若 `A→B`、`B→C` 已成立,禁止再写 `A→C`(读 B 时会自然带上 C)。 + +**DAG 与最小化**:`topicDependencies` 必须是 DAG,禁止环;保持最小边集。 + +**判定时机**:终稿与新 / 改 topic 落盘后,扫正文中**反引号引用的其他 topic id 与规则文件名**,逐个套四问;命中即写入 `manifest-routing.topicDependencies`,**并在新 topic 正文显式写依赖声明**(见骨架第 4 条)。 + +## 5. 大功能拆分策略 + +当一个业务功能体量较大时,推荐「主 topic + 子 topic」结构,而非单个大 topic。 + +**何时拆分(软约束,满足任一评估是否需拆)**: + +- 对应 stock-doc 超过 **300–500 行**:建议评估拆分,不强制阻断; +- matcher `includeAny` 超过 **12 个**:主题过宽信号; +- topic 正文包含超过 **3 个不相干职责域**的二级标题; +- `f2s-kb-upgrade` 审计时发现同一 topic 被多种不相干任务类型反复命中。 + +**拆分方式**: + +- **主 topic**(`primary: feature`):写业务闭环、入口边界、子模块索引,正文里用可点击 stock-doc 链接指向各细节文档;不写子模块的实现细节。 +- **子模块 topic**:按实际语义各自写 `feature` / `module` / `config` / `policy`,不预设类型;各自拥有独立 matcher,通过细分触发词独立命中。 +- **stock-doc**:允许长文;超过阈值时建议拆成多份 focused stock-doc(如 `<功能名>-业务规则_终稿.md`、`<功能名>-数据模型_终稿.md`),每份对应一个子 topic。 + +**不要做的事**: + +- 不用 `topicDependencies` 表达"概述 → 详情"导航关系(见第 4 节反向排除); +- 不为拆分而强行制造子 topic,若子模块本身不会被独立路由命中,不必建 topic。 + +## 6. rule 是否需新建对应 topic + +判据:**该 rule 是否会作为用户任务路由命中**。 + +- **会**(用户问 / 输入会触发该规则的执行)→ 须在 `.Knowledge/topics/` 建对应路由摘要,并在 `taskToTopicRules` 配置入口。例:`f2s-task`(变更追踪用户场景命中)、`f2s-implement-tech-design`("按方案实现"用户场景命中)。 +- **不会**(仅被其他规则 / SKILL 内部引用,用户不会直接发起)→ **不建** topic。例:`f2s-knowledge-preflight`、`f2s-karpathy-guidelines`、`f2s-config-check`、本条 `f2s-topic-authoring`。 + +误区:「重要的规则就该有 topic」——重要不等于"用户路由命中";让消费方 SKILL 在正文里直接 `Read rules/.*` 全文即可,无需走 manifest 路由。 + +## 7. 写盘权属(指针) + +`manifest-routing.json` / `.Knowledge/index.md` / `.Knowledge/topics/*.md` 的写权约束**以 `f2s-flow2spec-unified-entry` 与各 SKILL 内「写权硬约束」为准**,本条不复述;遇分歧以统一入口与对应 SKILL 为准。 + +## 禁止项 + +- 在未读本条的情况下新增 / 修改 topic 或 `topicDependencies`。 +- 为补分类单独创建、重命名或拆分 topic。 +- 在 topic 正文或 `index.md` 中写 `## 概念分类` 等 metadata 副本。 +- 把"重要的规则"硬塞进 `taskToTopicRules`(参见第 4 条)。 +- 用 `topicDependencies` 表达"信息相关"(应通过 `index.md` 语义边界 + matcher 关键词补召回,而非依赖边)。 +- 在 `topicDependencies` 中写传递冗余边或形成环。 +- **在 topic 的「长文背景 / 详细资料 / 相关资料 / 长文来源 / 参考文档」等指向长文源的整节引用槽位里,列出 `.Knowledge/req-docs/*`**(含澄清 / 技术方案 / SQL / PRD)。此类槽位只允许指向 `.Knowledge/stock-docs/*_终稿.md`;无终稿时须先生成再回填。短句/佐证式的偶发点引不受此约束。 \ No newline at end of file diff --git a/packages/core/templates/zh-CN/skills/f2s-doc-arch/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-doc-arch/SKILL.md new file mode 100644 index 0000000..23b7c76 --- /dev/null +++ b/packages/core/templates/zh-CN/skills/f2s-doc-arch/SKILL.md @@ -0,0 +1,128 @@ +--- +name: f2s-doc-arch +description: 根据用户说明或文档(或扫描代码)生成项目架构说明初稿,无固定格式,描述清楚即可;触发:项目架构说明、f2s-doc-arch、架构初稿 +--- +> 执行口径:本技能产物默认写入 `.Knowledge/stock-docs/`,后续由知识库技能链(如 `f2s-doc-final`、`f2s-kb-build`)同步到 `.Knowledge/topics/index/manifest`。 + +## 编排(主 / 子 agent) + +- `subAgent` / `switchAgentVerification` 两字段语义以统一入口为唯一事实源:**Cursor/Claude** 读配置根 `rules/f2s-flow2spec-unified-entry.*`;**Codex** 读 `.codex/topics/f2s-flow2spec-unified-entry.md`(与上同源,`flow2spec init` 镜像)。本节不复述。 +- 当 `subAgent=true` 时,从以下两种子策略择一: + - **B 模式(默认,单轮并行)**:主先产出「inventory(入口 + 核心模块名,主手写)」+「扫描契约(可读路径 / 禁扫目录 / 统一产出字段)」→ 子 agent 并行只读扫表 → 主一轮合并去重 → 写 `stock-docs` 初稿 → 用户确认与验收在主 agent 内完成。 + - **C 模式(多轮纠偏)**:切换判据为以下任一 —— 多 workspace / monorepo、目录极深或源路径 > 20 条、首轮子表矛盾或空洞明显、多源叙述重合 / 矛盾严重。 +- **子交付硬约束**:子 agent 不得自行裁剪目录范围,必须按主手写 inventory 执行;子交付按「子交付 YAML schema」(字段:`source` / `scope` / `cross_refs` / `pending`),禁止散文式回传。 +- **写权硬约束**:`.Knowledge/index.md` / `manifest-routing.json` 恒由主 agent 落盘,子 agent 不得触碰。 +- 落盘侧自验;本 SKILL 不绑定交叉校验。 + +# 生成项目架构说明(初稿) + +本技能用于**帮助用户生成项目架构的文档说明**,产出形态类似**初稿**:无固定格式规范,以**描述清楚**为目标。用户可提供纯文字说明、已有文档,或在不提供时由 AI 扫描代码生成(不推荐,仅作兜底)。 + +**与 f2s-kb-add 的分工**:本技能**只**负责「架构说明类**初稿**」这一环,默认**不**在同一技能内写终稿、不直接执行 **f2s-kb-build**。若用户在工作中要把**已做好的能力**依据多份相关文件路径**一次**解析进知识库(初稿→终稿→topics/index/manifest),应使用 **`f2s-kb-add`**,**勿用本技能冒充该流程**。 + +--- + +## 入参(均可选) + +| 参数 | 说明 | +| -------------- | -------------------------- | +| **第一个参数** | 可选。可为以下之一:**一段纯文字说明**(直接写在命令后)、**本地文档路径**(如 `.Knowledge/stock-docs/xxx.md`、`.Knowledge/req-docs/README.md`、`README.md`)。不传则进入「无输入」流程。 | +| **第二个参数** | 可选。输出文件路径;若不传,默认写入 `.Knowledge/stock-docs/架构说明_初稿.md`(项目名可从 package.json 的 name 或目录名推断,做合法文件名处理)。 | + +**注意**:不传任何说明或文档时,将使用 **AI 扫描项目代码与目录** 生成架构说明初稿,**不保证质量**。执行时**必须先提示用户**:「是否确认不传递参数,仍使用 AI 扫描代码生成?(不保证质量)」,仅当用户明确确认后才继续。 + +--- + +## 执行流程 + +### 1. 若用户提供了说明或文档 + +1. **读取与理解** + - 若第一参数是**文档路径**:在配置根的父目录下按路径读取该文件内容(支持 .md、.txt 等文本格式)。 + - 若第一参数是**纯文字说明**:直接以用户输入为「用户说明」。 +2. **结合项目补充** + - 根据用户说明中的**代码路径、模块名、入口**等线索,结合配置根的父目录下的实际目录结构、关键文件(如 package.json、入口文件、配置文件)进行**归纳与补全**。 + - 若用户说明较宽泛(如只说了「一个后台系统」),**主动引导**用户补充:主要代码路径、模块/包划分、入口与启动方式、与外部系统的边界等,便于生成更贴合的架构说明。 +3. **生成初稿** + - 若启用拆子(B 模式),子 agent 必须按主手写 inventory 执行扫描,交付遵循子交付 YAML schema。 + - 产出一份**项目架构说明**:可包含但不限于:项目定位、技术栈、目录/模块划分、关键路径与入口、配置与部署要点、与文档产物阶段的对应说明(若适用)。 + - **无固定格式**:采用清晰的标题与段落即可,不强制套用《终稿模版》。 +4. **输出** + - 默认写入 `.Knowledge/stock-docs/架构说明_初稿.md`;若用户传入第二参数则写入该路径。 + - 若目录不存在则先创建。 + +### 2. 若用户未提供任何说明或文档 + +1. **提醒并确认** + - 明确说明:「**未收到任何参数。** 不传递说明或文档时,将使用 AI 扫描项目代码与目录生成架构说明初稿,**不保证质量**,且易遗漏重点、难以区分主次。建议先提供一段简要说明或已有文档(如 README、设计 doc)再执行本技能。」 + - **必须询问用户**:「是否确认不传递参数,仍使用 AI 扫描代码生成?(不保证质量)」 + - 仅当用户**明确确认**(如回复「确认」「是」「直接扫描」等)后,才继续步骤 2;若用户未确认或表示取消,则不再执行扫描与生成。 +2. **扫描与生成** + - 基于配置根的父目录:列出主要目录与代表性文件(可结合 package.json、常见入口与配置文件名),归纳出「目录结构、疑似模块、入口与配置」等。 + - 生成一份**架构说明初稿**,并在文中注明「本初稿由扫描项目结构生成,建议结合业务说明与代码细节进一步补充」。 +3. **输出** + - 同上,默认 `.Knowledge/stock-docs/架构说明_初稿.md`,或用户指定的第二参数。 + +--- + +## 引导与迭代 + +- 用户说明若**范围较大**(如「整个中台」),可提示:建议补充**主要代码路径、子模块/包名、对外入口、依赖关系**等,并可在本次或后续对话中分批补充,再重新执行本技能更新初稿。 + +## 大功能拆分建议 + +扫描或理解完源码/说明后,若识别出以下任一信号,须在初稿**末尾**输出「拆分建议」段落,供用户参考(不阻断生成): + +- 源码总量超过 **~5000 行**,或涉及文件超过 **20 个**; +- 能明显识别出 **3 个以上不相干职责域**(如接口层 / 核心规则 / 数据模型 / 外部依赖各自独立); +- 用户说明本身已提到「多个子模块」或「多个功能」。 + +**拆分建议格式**(写在初稿末尾,独立节): + +``` +## 拆分建议 + +当前功能体量较大,建议拆成多份 focused stock-doc,各自对应一个独立 topic: + +| 建议文档 | 主要内容 | 建议 topic primary | +|---|---|---| +| <功能名>-概述_初稿.md | 入口边界、子模块关系、快速索引 | feature | +| <功能名>-业务规则_初稿.md | 核心流程、门禁、状态机 | policy | +| <功能名>-数据模型_初稿.md | 表结构、枚举、模型约定 | module | +| <功能名>-外部依赖_初稿.md | SOA/QMQ/Redis/风控封装 | config | + +拆分后各子 topic 通过各自 matcher 独立命中,主 topic 正文写导航链接; +不通过 topicDependencies 串联"概述 → 详情"(见 f2s-topic-authoring 第 5 节)。 +``` + +用户可选择:**A) 按拆分建议分别执行 `f2s-doc-arch`**(推荐),或 **B) 继续用当前单份初稿**进入后续流程。 + +## 完成后的下一步(硬约束) + +本技能**只产出初稿**;结束时须按下列顺序引导,**禁止**让用户跳过终稿直接 `f2s-kb-build`: + +1. 告知初稿路径,建议用户先审阅、补充内容。 +2. **下一步必须为 `f2s-doc-final`**:以初稿路径为入参,产出 `.Knowledge/stock-docs/<方案名>_终稿.md`(《终稿模版》规范格式)。 +3. **仅在终稿落盘后**再引导 **`f2s-kb-build`**,且入参须为终稿路径(含 `_终稿` 或由 `f2s-doc-final` 刚生成)。 +4. **禁止**在完成回复中单独写「请执行 `f2s-kb-build`」且入参指向 `*_初稿.md`;**禁止**将 `f2s-kb-build` 与 `f2s-doc-final` 并列成「二选一」。 +5. **唯一例外**:用户**明确要求**跳过终稿、且初稿已人工符合终稿模版——须先说明跳过终稿的风险,再允许指向 `f2s-kb-build`。 + +**完成回复模板**(须同时包含 `f2s-doc-final` 与 `f2s-kb-build`,且 ctx-build 在终稿之后): + +> 已生成架构说明初稿:`<初稿路径>`。请先审阅修改;下一步请执行 **`f2s-doc-final <初稿路径>`** 转为终稿,再执行 **`f2s-kb-build <终稿路径>`** 同步知识路由主题与索引。 + +--- + +## 路径与输出约定 + +- 所有路径均相对于**配置根的父目录**。 +- **默认输出**:`.Knowledge/stock-docs/架构说明_初稿.md`;项目名取自 `package.json` 的 `name`(去掉 scope 与非法字符)或当前目录名。 +- 若用户传入第二参数为输出路径,则优先使用该路径;若目录不存在则先创建。 + +--- + +## 约束与注意 + +- **不强制格式**:本技能产出为「架构说明初稿」,以描述清楚为主,不要求符合《终稿模版》或固定章节结构。 +- **无参数时必须确认**:用户未传任何参数时,必须先提示「是否确认不传递参数,仍使用 AI 扫描代码生成?(不保证质量)」,仅当用户明确确认后才执行扫描与生成。 +- 完成后按上文「完成回复模板」总结:初稿路径 + **必须先 `f2s-doc-final` 再 `f2s-kb-build`**;不得仅推荐 build。 diff --git a/packages/core/templates/zh-CN/skills/f2s-doc-final/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-doc-final/SKILL.md new file mode 100644 index 0000000..27d804f --- /dev/null +++ b/packages/core/templates/zh-CN/skills/f2s-doc-final/SKILL.md @@ -0,0 +1,92 @@ +--- +name: f2s-doc-final +description: 将 PDF 或 MD 转为《终稿模版》规范格式,便于后续用 f2s-kb-build 同步 topics/index/manifest;触发:f2s-doc-final、转成概述模板、终稿模版 +--- + +> 执行口径:初稿/终稿统一写入 `.Knowledge/stock-docs/`;模板优先读取 `.Knowledge/template/终稿模版.md`。 + +## 编排(主 / 子 agent) + +- `subAgent` / `switchAgentVerification` 两字段语义以统一入口为唯一事实源:**Cursor/Claude** 读配置根 `rules/f2s-flow2spec-unified-entry.*`;**Codex** 读 `.codex/topics/f2s-flow2spec-unified-entry.md`(与上同源,`flow2spec init` 镜像)。本节不复述。 +- **默认不拆子**:MD / PDF → 终稿模版的连贯性最好,由主会话一气呵成完成理解、套模版与定稿。 +- **可选拆子**(仅当 `subAgent=true` 且大体量 / 多文件,阈值:PDF **> 50 页** 或 **> ~5MB 文本**):子 agent 做「套模版、排版与结构搬运」**草稿**;主 agent 对照终稿模版、识别缺口并向用户追问、与用户对齐并**定稿 / 验收**;**子 agent 不得单独宣称终稿已合规**。 +- 不为「格式转换可独立」默认拆子:终稿合规依赖模版语义 + 业务表述,主侧验收成本通常仍在。 +- 校验:落盘侧 agent 自验,本 SKILL 不绑定交叉校验。 + +# 将 PDF 或 MD 转换为《终稿模版》规范格式(spec → context) + +用户会在本技能后附带**至少一个参数**:**第一个参数**为本地 **PDF 文件路径**或 **Markdown 文件路径**(必填);**第二个参数**(可选)为输出文件路径,若提供则覆盖默认输出位置。请根据文件类型按下列流程执行,输出便于后续由 **f2s-kb-build** 技能消费的终稿风格 Markdown 文档。 + +**终稿模版仅作提示**:若存在 `.Knowledge/template/终稿模版.md`,可读取作为结构参考;不强制套用。 + +## 内嵌模板结构(当项目内无 `.Knowledge/template/终稿模版.md` 时使用) + +规范要求: + +- **一级标题**:方案名(如 `# xxx 技术方案设计`)。 +- **二级标题至少包含**:`## 核心概念`、`## 业务规则`、`## 关键流程`;其余可按需增删(如 状态与流转、接口、配置/表设计/错误码、实现位置与对接方式)。 +- **核心概念**:用表格列出术语、实体、关键 ID(列:概念、说明)。 +- **状态与流转**:若有状态机,用列表写状态及流转;若无可简述或省略。 +- **业务规则**:列表写约束、校验、配置项。 +- **关键流程**:按「用户侧或系统侧」主流程,列表写流程名、步骤简述、入口接口/方法、结果。 +- **可选章节**:接口、配置/表设计/错误码、实现位置与对接方式,按需保留并填写。 + +--- + +## 流程一:用户传入的是 Markdown(.md) + +1. **读取**用户传入的 `.md` 文件内容。 +2. **参考格式**(不强制):若存在 `.Knowledge/template/终稿模版.md`,可读取作为结构提示;否则可参考下方内嵌模板结构。 +3. **分析与转换**: + - 理解原文主题与结构,提炼「方案名」「核心概念」「业务规则」「关键流程」及与原文相关的其他章节(如状态与流转、接口、配置/表设计/错误码、实现位置等)。 + - 将内容重组为结构清晰的终稿风格 Markdown:一级标题为方案名;建议至少包含 核心概念、业务规则、关键流程 三个二级标题,其余按原文有无与需要增删;表格/列表格式可参考模版,不必完全一致。 + - 若原文缺少某节,可标「(待补充)」或根据原文推断补全;若原文结构已清晰,可保留原文章节命名。 +4. **输出**: + - 默认写入 `.Knowledge/stock-docs/<方案名>_终稿.md`(最终产物带 `_终稿` 标识)。 + - 若用户希望指定输出路径,可在命令后附带第二个参数作为输出路径;否则用默认。 +5. **回复**:告知用户已生成 `.Knowledge/stock-docs/<方案名>_终稿.md`,并提示可按 `f2s-kb-build` 继续同步 `.Knowledge/topics`、`.Knowledge/index.md`(必要时 `manifest`)。 + +--- + +## 流程二:用户传入的是 PDF(.pdf) + +分两步完成:**先 PDF → 初稿 MD,用户确认后再 初稿 MD → 模板格式 MD**。 + +### 步骤 A:首次执行(传入 PDF 路径) + +1. **尝试读取 PDF**:按用户传入路径读取 PDF(可为绝对路径,或相对项目根;如 `.Knowledge/stock-docs/xxx.pdf`)。 + - 若当前环境可解析 PDF 文本:提取正文,转为 Markdown 初稿(保留标题层级、列表、段落,表格若可识别则保留)。 + - 若无法直接读取 PDF(如仅能拿到二进制):回复用户可将 PDF 内容转存为 `.Knowledge/stock-docs/xxx.md` 后再执行。 +2. **生成初稿**: + - 将提取出的内容保存为 `.Knowledge/stock-docs/<方案名>_初稿.md`(方案名可从 PDF 文件名或首标题推断)。 + - 在回复中**展示初稿的全文或主要结构**,并明确说明: + - 「初稿已保存为 `.Knowledge/stock-docs/<方案名>_初稿.md`,请检查并修改。」 + - 「确认无误后,请执行:`f2s-doc-final .Knowledge/stock-docs/<方案名>_初稿.md`。」 +3. **本轮不进行模板格式转换**,仅完成 PDF → 初稿 MD。 + +### 步骤 B:用户确认后再次执行(传入初稿 .md 路径) + +当用户**再次执行本技能并传入初稿的 .md 路径**(如 `.Knowledge/stock-docs/技术方案设计_初稿.md`)时: + +- 按 **「流程一:用户传入的是 Markdown」** 的步骤 2~5 执行:读取格式规范 → 分析与转换 → 输出为模板格式。 +- **输出建议**:生成 `.Knowledge/stock-docs/<方案名>_终稿.md`。 +- **回复**:告知已生成规范版,并提示可按 `f2s-kb-build` 继续同步 `.Knowledge/topics` 与索引。 + +--- + +## 路径与输出约定 + +- 所有路径均相对于项目根;初稿/终稿统一放在 `.Knowledge/stock-docs/`。 +- **输入**:第一个参数为文件路径(必填),如 `.Knowledge/stock-docs/方案.pdf` 或 `.Knowledge/stock-docs/方案_初稿.md`;第二个参数可选。 +- **输出**: + - PDF 首次:`.Knowledge/stock-docs/<方案名>_初稿.md` + - MD 或初稿 MD:`.Knowledge/stock-docs/<方案名>_终稿.md` +- 若 `.Knowledge/stock-docs/` 目录不存在,先创建再写入。 + +--- + +## 约束与注意 + +- 转换时**不要照抄原文**,要按模板**提炼、归纳、补全**,使核心概念、业务规则、关键流程清晰可查。 +- 建议(不强制)保留 **核心概念、业务规则、关键流程** 三个二级标题;其余章节按原文与需求增删,终稿模版仅作提示,不强制套用。 +- 完成后一句话总结:已生成初稿/终稿路径,并说明下一步可用 `f2s-kb-build` 同步知识路由主题与索引。 diff --git a/packages/core/templates/zh-CN/skills/f2s-doc-milestone/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-doc-milestone/SKILL.md new file mode 100644 index 0000000..f4789bc --- /dev/null +++ b/packages/core/templates/zh-CN/skills/f2s-doc-milestone/SKILL.md @@ -0,0 +1,148 @@ +--- +name: f2s-doc-milestone +description: 据 req-docs、git log、.task 与知识库主题语义生成里程碑(《项目里程碑模版》);触发:f2s-doc-milestone、生成项目里程碑、里程碑。命令后可附语义化范围。本技能固定子 agent 生成、主 agent 验证,不受 flow2spec.config 编排开关影响 +--- + +> **任务路径**:凡 `.task/` 落盘与续作,**必须以 `rules/f2s-task` 解析的 `TASK_ROOT` 为准(`.task` 或 `.task/`;config → git → legacy)。下文若仍出现 `.task/todo.json` / `.task/active/`,均视为 **`TASK_ROOT/...` 的简写**。 + + +> 执行口径:读 `.Knowledge/template/项目里程碑模版.md`;落盘 **仅** `.Knowledge/stock-docs/<范围名>里程碑.md`(无第二路径参数)。 + +## 编排(固定,不受项目配置影响) + +**本技能不受** `flow2spec.config.json` 中 **`subAgent`**、**`switchAgentVerification`**(及旧键 `subAgentVerification`)**影响**:无论其为 `true` 或 `false`,**一律**按下述分工执行,**禁止**因配置改为「全主会话」或「子 agent 自验即结束」。 + +| 角色 | 步骤 | 职责 | +| --- | --- | --- | +| **主 agent** | 0、3、4 | 读模版与知识库主题索引、解析范围、派子、**验证**、必要时修订、回复用户 | +| **子 agent** | 1、2 | 采集四源、套模版、**Write 初稿** | + +1. **主 agent**:步骤 0 → 下发「采集契约」→ 子 agent 步骤 1–2 落盘初稿。 +2. **主 agent**:步骤 3 对照四源与「重要节点清单」验证(不全文重写;补缺、纠偏、「待确认」)→ 步骤 4 回复。 +3. 子 agent **禁止**宣称「里程碑已验收完成」;终稿以主 agent 验证后为准。 + +> 步骤 0 仍 **`Read("flow2spec.config.json")`**(满足 `f2s-config-check` 前置),但**不得**用其中的 `subAgent` / `switchAgentVerification` 改变本技能编排。 + +**子 agent 采集契约(主 agent 派子前写入 prompt)** + +| 字段 | 内容 | +| --- | --- | +| `scope` | 用户语义范围一句 | +| `outputPath` | `stock-docs/<范围名>里程碑.md` | +| `sources` | 见下文「四源」;**须含知识库主题语义** | +| `template` | `.Knowledge/template/项目里程碑模版.md`(不写模版顶部说明 blockquote) | +| `delivery` | 完整 Markdown,可直接 `Write` 至 `outputPath` | +| `stagePolicy` | 见下文「阶段粒度」;契约中须复述一句 | + +## 阶段粒度(必须,写入契约) + +里程碑 **Mx 仅记录功能/能力变更**:当前仓库(或用户指定范围内)**已落地或可核验**的交付,例如模块/接口/数据模型/领域行为/知识库路由等,且须在四源中有依据。 + +**不得**单独占一行总览或独立 `## Mx ·` 的阶段类型(无四源交付支撑时禁止臆造;有交付也不得拆成「纯测试/纯联调」阶段): + +- 联调、集成测试、UAT、回归、验收、提测、上线检查(仅过程、无功能 diff) +- 仅环境/运维动作(执行 DDL、填配置、发版窗口、跨仓排期)且**无**本范围功能交付 +- 以「稳定化 / 工程化 / 收尾」为名、实质仅为上述过程性工作的阶段 + +**合并规则**:同一次能力迭代内的工程性改动(如 id 类型对齐、分页格式、锁与并发)**并入**对应功能阶段正文,不另起「联调 / 测试 / 验收」阶段。 + +**缺口处理**:四源仅提及待联调、待验收、环境待补齐而无本范围功能交付 → **不写**对应 Mx;可在 **待确认** 列一句,**禁止**用「计划项」填充总览表。 + +## 四源(采集与验证均须覆盖) + +| 源 | 读什么 | 里程碑里怎么用 | +| --- | --- | --- | +| **req-docs** | 范围内 `.Knowledge/req-docs/*.md` | 需求/方案节点、交付摘要 | +| **git** | `git log --no-merges`、`git tag -l`、`package.json` 版本 | 时间线、大版本/tag、提交锚点 | +| **`.task`** | `todo.json`、`active/`、`completed/` 下 `task.md` 等 | 任务闭环、已交付步骤 | +| **知识库主题(语义)** | 见下「主题索源」 | 与 index/manifest 已登记能力对齐,避免漏写「库里已有语义」的阶段 | + +### 主题索源(知识库语义,主 agent 步骤 0 须读;子 agent 步骤 1 须读) + +1. **`Read(".Knowledge/manifest-routing.json")`**:提取 `topicPaths`、`taskToTopicRules`(及与范围相关的 `topicDependencies`)。 +2. **`Read(".Knowledge/index.md")`**:至少「**主题一览**」表(主题 id、适用场景、关联文档摘要)。 +3. **按需 `Read` `.Knowledge/topics/.md`**:与范围或 manifest 命中相关的摘要(**禁止**为枚举遍历整个 `topics/`;仅读 manifest/index 已点名的主题,通常 ≤ 全表行数)。 +4. 将主题语义归纳为「能力/场景节点」列表,供子 agent 写入契约;里程碑阶段须能覆盖或于「待确认」说明与某主题相关的缺口。 + +> **索源内容仅用于采集与验证,禁止写入生成文档**;生成文档不含「索源」行、topic 路径或 manifest 内部名称。 + +## 入参(仅一个,可选) + +命令名之后可跟**一段语义化范围**(自然语言): + +| 用户意图 | 示例 | 落盘文件名 | +| --- | --- | --- | +| 整个项目(默认) | 不传 / `整个项目` / `全项目` | `项目里程碑.md` | +| 某一需求或能力 | `回调改造` / `登录模块` | `<简述>里程碑.md` | + +**文件名规则**:后缀 `里程碑.md`;整个项目 → 前缀 `项目`;单一需求 → 语义或 req 标题简述(≤ 20 字)。 + +**范围收窄**:在四源上按关键词、路径、日期过滤;未传范围则四源全量可追溯(主题索源读 index 全表 + manifest,topics 按需展开)。 + +## 步骤 0:前置(主 agent) + +1. **`Read("flow2spec.config.json")`**(不采纳其 `subAgent` / `switchAgentVerification` 编排本技能) +2. **`Read(".Knowledge/template/项目里程碑模版.md")`** +3. **主题索源**(见上:manifest → index 主题一览 → 按需 topics 摘要) +4. 解析范围 → 确定默认路径 **`stock-docs/<范围名>里程碑.md`**。 +5. **相似文件检查(落盘前必做)**:列出 `.Knowledge/stock-docs/` 下已有 `*里程碑*.md`(含 `*里程碑.md`)。若存在与本次**目标路径相同**或**语义相近**的文件(例如同为「整个项目」的 `项目里程碑.md` 与另一份全项目里程碑、或前缀/范围关键词高度重叠),**须先询问用户**,**禁止**静默覆盖或擅自另存: + - **覆盖**:沿用原路径,子 agent 写入时覆盖该文件(验证后仍以该路径为终稿)。 + - **另生成一份**:改用新路径(建议:范围简述 + `_YYYYMMDD` + `里程碑.md`,或用户指定的 `<简述>里程碑.md`),并在契约中更新 `outputPath`。 + - 无相似文件,或仅有一个且与目标路径完全一致且用户本轮已明确要「重新生成/覆盖」→ 可不再追问,按默认路径继续。 +6. 向用户复述:范围、**最终** `outputPath`、已读主题数量;若做了相似文件询问,待用户选择后再继续。 +7. 组装「采集契约」(含最终 `outputPath`、主题节点列表)并 **派子 agent** 执行步骤 1–2 + +## 步骤 1:采集索源(子 agent) + +- 按契约完成 **四源** 采集;git **须** 对照 tag 与主版本/semver 跃迁(以本仓库 `git tag` / `package.json` 为准)。 +- 主题语义:核对 manifest/index 中能力与 git/req/task 是否同窗出现;暂无法对齐的记入内部备注供「待确认」。 + +索源为空:仍生成文档,「待确认」说明缺口;**禁止**训练数据填交付。 + +## 步骤 2:套模版并落盘(子 agent) + +**生成原则:面向读者,不暴露内部信息。** + +1. 文首只写:标题 `# (范围名)里程碑`、范围、更新时间。**不写** 索源行、topic 路径、manifest 内部名称、commit hash、npm 发布状态、环境状态等任何内部信息。 +2. **阶段倒序**:总览表与各 `## Mx ·` 均按**最新在前**排列(MN → … → M1);每阶段标题体现功能变更,不得用「联调 / 测试 / 验收」命名(见「阶段粒度」)。 +3. 每阶段正文:仅列**已交付的功能点**,每条一行,可验证;不写时间细节、过程说明或背景铺垫。 +4. **待确认**:只列功能/交付层面的缺口或不一致;**禁止**写内部运维/发布/环境状态。若无缺口写「无」。 +5. 不写模版顶部说明 blockquote。 +6. **`Write`** 至 `outputPath`。 + +## 步骤 3:验证(主 agent,须执行) + +子 agent 落盘后 **必须**验证:**重要节点**是否错误、遗漏或合并过度。 + +1. **重读四源要点**:git tag/commit、req/task、**index 主题一览 + 已读 topics** 与文稿对照。 +2. **对照「重要节点清单」**: + +| 类别 | 检查什么 | +| --- | --- | +| 版本 / tag | 四源中的 major tag、`package.json` 版本跃迁是否在总览或 Mx 中体现 | +| 路线/架构转折 | 四源中出现的目录重组、技术路线替换等重大变更是否单独或合并体现 | +| 功能交付 | req/git/task 中可核验的能力是否在 Mx 中有对应阶段 | +| **知识库主题** | 若存在 manifest/index:与范围相关的主题是否覆盖或列入「待确认」 | +| 任务闭环 | 若存在 `.task/`:已归档任务是否在相关 Mx 中体现 | +| 依据可追溯 | 每 Mx 交付能否在四源中找到 | +| 时间线 | 先后合理;同窗多版本是否需拆分 | +| **排序** | 总览表与各 Mx 是否均为最新在前;若不是则调整 | +| **阶段粒度** | 是否存在仅联调/测试/验收/环境而无功能交付的 Mx;若有 **删除或并入** 相邻功能阶段 | +| **内部信息** | 文档中是否含索源行、commit hash、topic 路径、npm/环境状态等内部信息;若有**删除** | + +3. 遗漏 → 补 Mx(**须为功能变更**);错误 → 按四源修正;无法确认 → 「待确认」(**禁止**用假 Mx 代替)。 +4. 验证或修订完成后方可步骤 4。 + +## 步骤 4:回复(主 agent) + +落盘路径、阶段数、验证结论(一句)、「待确认」摘要。 + +## 禁止项 + +- 禁止用 `subAgent` / `switchAgentVerification` 跳过子生成或跳过主验证。 +- 禁止在 `stock-docs/` 已存在**相似里程碑**且用户未选择「覆盖 / 另生成一份」前派子 agent 或 `Write`。 +- 禁止第二参数改输出路径(路径由范围 + 相似文件询问结果确定);禁止写入 `req-docs`。 +- 禁止未读四源写交付;禁止子 agent 未经验证即宣称完成。 +- 禁止遍历整个 `matchers/` 或全仓 topics 代替「manifest + index + 按需 topics」。 +- 禁止用训练数据或其它项目的里程碑结构替代**当前仓库**四源;禁止代写与本次 `outputPath` 无关的其它 `stock-docs` 文档。 +- 禁止单独设立联调 / 集成测试 / UAT / 验收 / 纯环境运维类 Mx;禁止在无四源功能交付时写「计划项」阶段。 diff --git a/packages/core/templates/zh-CN/skills/f2s-doc-pdf/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-doc-pdf/SKILL.md new file mode 100644 index 0000000..170a320 --- /dev/null +++ b/packages/core/templates/zh-CN/skills/f2s-doc-pdf/SKILL.md @@ -0,0 +1,70 @@ +--- +name: f2s-doc-pdf +description: 将 PDF 技术方案转为 Markdown 并保存到 req-docs,可补全流程说明;触发:PDF转MD、按方案实现前的 PDF +--- + +> 执行口径:技术方案文档统一落在 `.Knowledge/req-docs/`;规则能力仍由配置根 `rules/skills` 加载。 + +## 编排(主 / 子 agent) + +- 两字段(`subAgent` / `switchAgentVerification`)语义以统一入口为唯一事实源:**Cursor/Claude** 读配置根 `rules/f2s-flow2spec-unified-entry.*`;**Codex** 读 `.codex/topics/f2s-flow2spec-unified-entry.md`(与上同源,`flow2spec init` 镜像)。本文不复述。 +- **默认不拆子**:追问-落盘必须在主 agent 内完成(子 agent 无法向用户追问)。 +- **可选拆子**:仅当 `subAgent=true` 且 PDF 规模超阈值(**> 50 页 或 > ~5MB 文本**)时启用;子 agent 仅负责 PDF→MD 首稿并落盘 `.Knowledge/req-docs/<名>.md`,**不向用户追问、不写「流程说明」章节**;主 agent 接手后续追问与流程图补写。 +- 校验默认由落盘侧 agent 自验;本 SKILL 不绑定交叉校验。 + +# 将 PDF 技术方案文档转为 Markdown(并补全流程说明) + +用户会在本技能后附带**一个参数**:**PDF 技术方案文档的本地路径**(如 `~/Downloads/技术方案.pdf`,或 `.Knowledge/req-docs/某草稿.pdf`)。请按以下步骤执行,将 PDF 转为 Markdown 并保存到 `.Knowledge/req-docs/`,必要时引导用户补全流程说明。 + +## 步骤 1:读取 PDF 并转为 Markdown + +1. 若启用拆子(PDF > 50 页 或 > ~5MB),子 agent 仅负责 PDF→MD 首稿并落盘 `req-docs/<名>.md`,不追问、不写流程说明;主 agent 接手后续步骤。**读取**用户传入的 PDF 文件,提取其中的**文字内容**(表格、章节、列表、代码块等尽量保留结构),整理为 Markdown 格式。 +2. **保存到** `.Knowledge/req-docs/`,推荐路径:`.Knowledge/req-docs/<方案名>.md`。文件名为原 PDF 文件名去掉 `.pdf` 后加 `.md`。 +3. 若目录不存在,先创建再写入。 +4. 保存后告知用户:「已将该 PDF 转为 Markdown 并保存为 `xxx.md`。」 + +--- + +## 步骤 2:向用户提问获取流程图(可选但推荐) + +PDF 内嵌的**流程图**无法被直接解析为步骤与分支,若需按图实现代码,需用户配合提供。 + +1. 向用户说明:「文档中可能包含流程图,我无法从 PDF 中解析图中的步骤与分支。若您后续会根据技术方案实现代码(见 `implement-tech-design` 规则),建议补全流程说明: + - **方式一**:将相关流程图以**图片形式**发到本次对话中,我将解析后以文字形式写入上述 MD; + - **方式二**:直接以**文字描述**每个接口/流程的步骤(如:1. 是否登录 2. 查某表 3. 判断某字段 → 返回结果),我将原样写入上述 MD。 + 若文档无流程图或暂不提供,可回复「跳过」,我将结束本技能。」 +2. **若用户回复「跳过」或明确表示无需流程说明**:告知用户「在对话中提供上述 MD 路径并说明按技术方案实现代码,我将按 `implement-tech-design` 规则执行。」并结束。 +3. **若用户提供流程图(图片或文字)**:进入步骤 3。 + +--- + +## 步骤 3:将流程说明写入该 MD + +1. 若用户提供的是**图片**:解析图片中的步骤、判断分支与返回,整理为文字步骤。 +2. 若用户提供的是**文字**:直接采用。 +3. 在该 MD 文件末尾(或新增「流程说明」章节)**追加**流程内容,格式示例: + +```markdown +## 流程说明(由用户提供 / 由流程图解析) + +### 示例接口 A +1. 前端发起请求 +2. 后端:查询某表最后一条记录 +3. 判断:是否有某 ID?是 → 返回 true,否 → 返回 false +4. 返回结果 + +### 示例接口 B +1. 是否登录 → 否 返回 401 +2. 是否过期 → 是 返回 403 +… +``` + +1. 保存后告知用户:「流程说明已写入 `xxx.md`。接下来请在对话中提供该 MD 路径并说明要按技术方案实现代码,我将按 `implement-tech-design` 规则执行。」 + +--- + +## 约束与小结 + +- **路径**:用户传入的 PDF 路径可为绝对路径或相对项目根。输出 MD 建议保存在 `.Knowledge/req-docs/<方案名>.md`(`req-docs` 放实现文档,`stock-docs` 放知识沉淀源文档)。 +- **本技能仅负责**:PDF → Markdown 转换 + 可选流程说明补全;不执行代码实现。完成后可提示用户:在对话中提供生成的 MD 路径并说明按技术方案实现,AI 将按 **f2s-implement-tech-design.mdc** 执行。 + diff --git a/packages/core/templates/zh-CN/skills/f2s-git-commit/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-git-commit/SKILL.md new file mode 100644 index 0000000..286d389 --- /dev/null +++ b/packages/core/templates/zh-CN/skills/f2s-git-commit/SKILL.md @@ -0,0 +1,251 @@ +--- +name: f2s-git-commit +description: 代码写完后提交 Git:默认检查变更与知识库覆盖;用户明确要求“快捷提交”时跳过知识库覆盖检查;**改动全为纯文档 / 知识库自身**或**近 30 分钟内已跑过 kb-sync/kb-feat/kb-fix** 时自动跳过覆盖检查;生成带 emoji 首行的提交说明后**可直接 commit**(须在当条回复展示首行,不要求用户单独确认 commit);**git pull 类拉取须用户先确认**。触发:f2s-git-commit、提交代码、快捷提交、git commit、帮我提交 +--- + +> 执行口径:本技能代用户执行 git 操作;不使用 `git add -A` / `git add .`,不跳过 hooks(`--no-verify`),不自动 push。**`git pull` / `git fetch` 合并入本地前必须取得用户对「拉取」的明确确认**;`git commit` 不要求单独一轮「确认」交互(见步骤 3–4)。用户明确要求“快捷提交”时,仅跳过步骤 2 知识库覆盖检查,其余安全步骤照常执行。 + +## 编排(主 / 子 agent) + +- `subAgent` / `switchAgentVerification` 语义以统一入口为唯一事实源:**Cursor/Claude** 读配置根 `rules/f2s-flow2spec-unified-entry.*`;**Codex** 读 `.codex/topics/f2s-flow2spec-unified-entry.md`。 +- 本技能全程在主 agent 完成(**pull 的确认**不可下放子 agent;`git commit` 不要求单独一轮用户确认,见步骤 3–4)。 + +# f2s-git-commit(提交代码) + +## 强制流程 + +### 快捷提交模式 + +当用户本轮明确说出 **“快捷提交”**、**“快速提交”** 或 **“quick commit”** 时,进入快捷提交模式: + +- 跳过 **步骤 2:知识库覆盖检查**,不读取 `.Knowledge/topics/` / `.Knowledge/stock-docs/` 做覆盖判断。 +- 不提示用户先运行 `f2s-kb-sync` / `f2s-kb-feat`。 +- **不跳过**步骤 1 的变更读取与冲突标记检查。 +- **不跳过**步骤 3 的提交信息生成与展示。 +- **不跳过**步骤 4 的精确 `git add <文件列表>`、正常 `git commit` 与 git hooks。 +- **不得**因快捷提交使用 `git add -A` / `git add .` / `--no-verify` / 自动 push。 + +### 步骤 1:读取变更(只读) + +```bash +git status --short +git diff HEAD +``` + +- 从 `git status --short` 区分三类文件: + - **Staged**:已 `git add`,前缀为 `M `、`A `、`D `(首列非空) + - **Unstaged**:已追踪但未 add,前缀为 ` M`、` D`(次列非空) + - **Untracked**:`??` 前缀,新文件尚未追踪 +- 若三类均为空(nothing to commit),直接告知用户并结束。 + +**冲突检查(必须,先于一切)**: + +扫描所有变更文件内容,若任意文件包含 `<<<<<<<`、`=======`、`>>>>>>>` 冲突标记,立即终止并提示: + +``` +❌ 检测到未解决的 merge conflict: + - <文件路径> + +请先解决冲突后再提交。 +``` + +### 步骤 2:知识库覆盖检查(默认必须;三种情况可跳过) + +若处于**快捷提交模式**,本步骤直接跳过,并在步骤 5 收尾提示中说明“已按快捷提交跳过知识库覆盖检查”。 + +**先判断 `.Knowledge/` 是否存在:** + +- 若 `.Knowledge/manifest-routing.json` 不存在:跳过本步骤,在步骤 5 收尾提示「项目尚未初始化 Flow2Spec 知识库,建议运行 flow2spec init」,继续步骤 3。 + +**跳过判定 A:改动纯文档 / 知识库自身**(进入覆盖检查前先判定) + +若步骤 1 收集到的 pending 文件路径**全部**命中以下模式,直接跳过本步骤(在步骤 5 说明「本次改动纯文档,已跳过覆盖检查」): + +- `.Knowledge/**`(改的就是知识库自己,检自己无意义) +- `docs/**` / `docs/en/**` +- `README*.md` / `LICENSE` / `CHANGELOG*` +- `.claude/**` / `.cursor/**` / `.codex/**`(agent 配置根,由 flow2spec init 分发,与业务能力覆盖无关) +- `presentations/**` / `assets/**` / 其他纯静态资源 + +**任一**文件落在 `src/` / `lib/` / `cli.js` / `templates/` / 业务代码目录时,本捷径**不生效**,继续走覆盖检查。 + +**跳过判定 B:近期已同步过知识库** + +读取 `.Knowledge/.last-sync.json`(若不存在直接跳过本判定): + +```json +{ + "syncedAt": "2026-08-04T10:30:00.000Z", + "skill": "f2s-kb-sync", + "developerId": "<可选>" +} +``` + +- 若 `Date.now() - Date.parse(syncedAt) < 30 * 60 * 1000`(30 分钟内)→ 直接跳过本步骤,在步骤 5 说明「近 30 分钟内已跑过 ,已跳过覆盖检查」。 +- 若时间戳过期或文件损坏 → 忽略,正常走覆盖检查。 +- 该文件由 `f2s-kb-sync` / `f2s-kb-feat` / `f2s-kb-fix` / `f2s-kb-add` / `f2s-kb-addRules` / `f2s-kb-distill` 等**知识库写入类技能**在成功完成后写入,`f2s-git-commit` **只读**不写。 +- 用户显式说「重新检查一次覆盖」/「不要跳过覆盖检查」→ 本判定失效,强制走覆盖检查。 + +**存在时执行覆盖检查:** + +**先执行 KB 自动合并预检(必须,不让用户手动跑命令):** + +1. Agent 在本步骤内部执行 `flow2spec kb check --json` 与 `flow2spec kb status --json`,或使用等价的内置 KB 引擎能力;不得把这些命令变成用户要手动执行的提交前置工作。 +2. 若 `check` 返回知识库结构错误、matcher 缺失、routing drift 等健康问题:终止本次 commit,报告具体问题与建议修复动作;不要把损坏的知识库一起提交。 +3. 若 `status.tasks` 中存在当前 developer 任务根下的 `kb-delta.json`: + - 能唯一定位当前任务线且 `mergeable=true`:自动执行 `plan → apply → build → check`(CLI 或等价内置能力均可),并把被写入的 `.Knowledge/**` 文件纳入本次提交文件列表。 + - `mergeable=false`、delta 解析失败,或存在多个 active delta 且无法判断哪个属于本次提交:停止自动写入,列出 `topic / reason / deltaPath`,提示用户需要语义合并或选择任务线;不得猜测合并。 +4. 若没有当前任务线的 active `kb-delta.json`,才进入下面的粗粒度覆盖判断。 + +**没有可自动应用的 delta 时,执行粗粒度覆盖检查:** + +1. 从 `git diff HEAD` 及 untracked 文件路径推断本次变更涉及的**功能模块**(以仓库内目录/包名为准,勿臆测未出现的业务名)。 +2. 读取 `.Knowledge/topics/` 目录列表与 `.Knowledge/stock-docs/` 目录列表。 +3. 对比步骤 1 推断出的功能模块,判断对应文档是否已在知识库中登记。 +4. 得出结论:**已覆盖 / 部分覆盖 / 未覆盖**。 + +> 判断粗粒度即可:有对应 topic 或 stock-docs 文档即视为已覆盖;若知识库为空或找不到相关文档则视为未覆盖。 + +**未覆盖或部分覆盖时(必须提示):** + +``` +⚠️ 本次变更涉及以下能力尚未入知识库: + - <能力描述> + +建议在提交前同步知识库,可选: + A) 现在运行 f2s-kb-sync 补录,完成后自动继续提交流程 + B) 先提交,稍后手动补录(输入 B 确认) + C) 取消本次提交(输入 C) +``` + +- 选 **A**:提示用户运行 `f2s-kb-sync` 或 `f2s-kb-feat` 补录;用户补录完成后在**同一会话声明已补录**或**再次触发本技能**时,从步骤 1 或步骤 3 继续(**不要求**为「继续 commit」单独打字确认,与步骤 3–4 一致)。 +- 选 **B**:记录未覆盖能力描述,在步骤 5 收尾提示中输出。 +- 选 **C**:终止本技能。 + +### 步骤 3:生成提交信息草稿(必须) + +读取 `git diff HEAD`(内容过长时取前 300 行),基于实际变更内容生成提交信息。 + +#### 首行格式(必须):类型图标 + Conventional Commits + +**首行**须同时满足: + +1. **以一个 emoji 开头**(与下表 `type` 对应,**禁止**用多个装饰 emoji 堆叠)。 +2. 紧跟 **一个 ASCII 空格**,再写 **小写 `type`**、英文冒号 `:`、**一个空格**、**中文或英文简述**。 +3. **可选 scope**:使用 Conventional 的 `type(scope):`,紧跟在 `type` 之后、冒号之前,例如 `🐛 fix(auth): 修复登录态丢失`。 +4. 首行总长度建议 **≤ 72 个字符**(含 emoji;过宽时优先缩短描述)。 + +**推荐模板(单行)**: + +```text + [(scope)]: <简述> +``` + +无 scope 时省略括号段,例如:`🚀 feat: 简述`。 + +**`type` → 首字符 emoji(固定选用下表,便于检索与发布说明)**: + +| `type` | emoji | 典型场景 | +|--------|--------|----------| +| `feat` | 🚀 | 新功能、对用户可见的能力增量 | +| `fix` | 🐛 | 缺陷修复、线上/测试问题 | +| `docs` | 📚 | 仅文档、注释、README、知识库正文类 | +| `style` | 💄 | 纯格式、缩进、分号等不改变行为的排版 | +| `refactor` | ♻️ | 重构、改名、无行为变化的结构调整 | +| `perf` | ⚡ | 性能优化 | +| `test` | 🧪 | 测试用例、测试桩、快照 | +| `build` | 🏗️ | 打包、依赖、编译脚本、artifact | +| `ci` | 👷 | CI 配置、流水线、自动化脚本 | +| `chore` | 🔧 | 杂项、工具脚本、非 build/ci 的维护性改动 | +| `revert` | ↩️ | 回滚某次提交 | + +**示例**: + +```text +🚀 feat: 支持xxx活动缓存预热 +🐛 fix(coupon): 领券窗口边界条件错误 +📚 docs: 补充公共模块 QConfig 说明 +♻️ refactor: 提取拼团校验为独立函数 +🔧 chore: 升级 ESLint 配置 +``` + +**正文(可选)**:第二行起可为列表或段落,**不要求**每行再加 emoji;若需条目,用 `- ` 即可。 + +**用户已给出首行时**:若已含上表之一且 emoji 与 `type` 一致,**尊重用户文案**;若仅有 `type:` 无 emoji,**须补全 emoji** 再进入步骤 4。 + +**与 `git commit` 的确认策略(必须)**: + +- 在**同一条 assistant 回复**中:**先**展示拟提交说明的**首行**(及可选正文),**随后立即**执行步骤 4(`git add` 逐项 + `git commit`)。**不要求**用户再回复「确认」才允许 commit。 +- 若用户在该轮对话中**已先写明**提交说明且合规,可直接使用并进入步骤 4,仍须在执行前**复述首行**再 commit。 +- 用户若明确表示「改提交说明 / 换一个 type」:改稿后仍在本策略下**展示即提交**,不增加「请回复确认」门槛。 + +### 步骤 4:执行提交(展示说明后立即执行) + +根据步骤 1 的三类文件分别处理: + +```bash +# 1. Unstaged 文件:需先 add +git add + +# 2. Untracked 文件:需先 add +git add + +# 3. Staged 文件:已 add,无需重复操作 + +# 执行提交 +git commit -m "<步骤 3 定稿的完整提交信息>" +``` + +- 禁止使用 `git add -A` / `git add .`,仅 add 步骤 1 中明确列出的文件。 +- 若 pre-commit hook 失败:输出完整错误信息,提示用户修复后重新触发本技能,**不**使用 `--no-verify` 绕过。 +- 若 commit 成功:读取 commit hash(`git rev-parse --short HEAD`)并进入步骤 5。 + +### 步骤 5:收尾提示 + +``` +✅ commit 完成 + <提交信息首行> + +[若步骤 2 选了 B] +📌 提醒:以下能力仍未入知识库,建议在合并前补录: + - <能力描述> + 可运行:f2s-kb-sync 或 f2s-kb-feat + +[若跳过了步骤 2(.Knowledge 不存在)] +💡 项目尚未初始化 Flow2Spec 知识库,如需接入可运行:flow2spec init + +[若快捷提交跳过了步骤 2] +⚡ 已按快捷提交跳过知识库覆盖检查。 + +[若命中跳过判定 A:改动纯文档] +📄 本次改动纯文档 / 知识库自身,已跳过覆盖检查。 + +[若命中跳过判定 B:近 30 分钟内已同步] +🔄 近 30 分钟内已跑过 ,已跳过覆盖检查(.Knowledge/.last-sync.json)。 +``` + +## 约束 + +- 禁止使用 `git add -A` / `git add .`,只 add 已确认的变更文件。 +- 禁止 `--no-verify`,hook 失败须修复后重试。 +- 禁止 `--amend` 已推送的 commit,除非用户明确要求。 +- 禁止自动 push,commit 完成后停止。 +- 默认模式下知识库未覆盖时必须提示,但最终是否补录由用户决定(选 B 不阻塞);快捷提交模式下跳过知识库覆盖检查,不提示补录选项。 +- **`git pull` / `git pull --rebase` / 会改写当前分支工作区内容的 `git fetch` 后续合并操作**:**必须**先向用户说明目的与风险,**取得用户对「拉取」的明确确认**(如用户回复「确认 pull」)后再执行;**禁止**为 commit 而顺带静默 pull。 +- **`git commit`**:**不要求**用户单独回复「确认」;但**禁止完全不展示**拟提交首行就执行 commit(须在当条回复中可见首行后再执行)。 +- 提交信息**首行**须符合步骤 3 的 **emoji + type** 格式(用户已合规给出时可保留)。 +- 存在 merge conflict 标记时必须终止,不得继续。 + +## 完成后自检 + +1. 步骤 1 是否检查了 merge conflict(必须为是)。 +2. 是否区分了 staged / unstaged / untracked 三类文件(必须为是)。 +3. 是否用了 `git add -A` / `git add .`(必须为否)。 +4. 知识库检查是否执行或有明确跳过理由(快捷提交 / `.Knowledge` 不存在)(必须为是);若存在 active `kb-delta.json`,是否已自动 plan/apply/build/check 或明确报告冲突(必须为是)。 +5. 步骤 3 是否基于 `git diff` 实际内容生成提交信息(必须为是,而非仅 `--stat`)。 +6. 执行 commit 前是否在当条回复中**展示了拟提交首行**(必须为是);**不得**要求用户单独「确认 commit」才执行(与策略一致)。 +7. 提交信息**首行**是否为 ` [(scope)]: <简述>` 且 emoji 与 type 与上表一致(合并 revert 等例外须在展示中说明)。 +8. 若 pre-commit 失败,是否跳过了 hook(必须为否)。 +9. 若步骤 2 选 B,收尾提示是否包含未补录提醒。 +10. 若步骤 2 选 A,是否在用户补录或再次触发后继续流程(**不要求**为继续 commit 单独要确认)。 +11. 若本流程中曾需要 `git pull`:是否在执行前取得用户对 **pull** 的明确确认(必须为是);未涉及 pull 则标 N/A。 diff --git a/packages/core/templates/zh-CN/skills/f2s-kb-add/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-kb-add/SKILL.md new file mode 100644 index 0000000..eb323a4 --- /dev/null +++ b/packages/core/templates/zh-CN/skills/f2s-kb-add/SKILL.md @@ -0,0 +1,132 @@ +--- +name: f2s-kb-add +description: 工作中把已落地能力解析进知识库(多文件聚合):初稿→终稿→topics/index/manifest;触发:f2s-kb-add、已有能力进知识库、多文件生成上下文 +--- + +> 执行口径:本技能只维护 `.Knowledge`,不改配置根 `rules/skills`。 + +## 编排(主 / 子 agent) + +- `subAgent` / `switchAgentVerification` 两字段语义以统一入口为唯一事实源:**Cursor/Claude** 读配置根 `rules/f2s-flow2spec-unified-entry.*`;**Codex** 读 `.codex/topics/f2s-flow2spec-unified-entry.md`(与上同源,`flow2spec init` 镜像)。 +- 默认不拆子:主会话全流程完成;低于阈值时拆子收益低于 context 切换成本。 +- 拆子阈值(仅当 `subAgent=true` 且任一满足):① 输入路径 ≥ 5;② 单源文件 > ~3000 行;③ 多路径总量 > ~10000 行。 +- **拆子策略(仅在达到拆子阈值且 `subAgent=true` 时启用)**: + - **B 模式(默认,单轮并行)**:主先产出「inventory(待解析源文档路径清单 + 核心能力名,主手写,禁止子 agent 自行增删)」+「扫描契约(每个源读哪些章节 / 行号范围、禁扫目录、统一产出字段与表头)」→ 子 agent 并行只读按表填写 → 主一轮合并 + 去重 → 写 `.Knowledge/stock-docs/<方案名>_初稿.md` → 主做用户确认与验收。适合源边界较清晰、中等规模、希望尽快出一版。 + - **C 模式(大仓 / 高风险,多轮纠偏)**:在 B 之前或替代 B 首轮 —— 主先做 inventory → 子并行交表 → 主专做一轮**对表**(标重合 / 矛盾 / 缺依赖 / 跨源边界)→ 必要时对矛盾点补派小任务或主自读关键点 → 最后主写 / 改定稿。适合多 workspace / monorepo、目录极深、源路径 > 20 条、首轮子表矛盾或空洞明显、多源叙述重合或矛盾严重的场景。 + - **切换判据**(任一成立即切到 C):多 workspace / monorepo;目录极深或源路径 > 20 条;首轮子表矛盾 / 空洞明显;多源叙述重合 / 矛盾严重。 +- **子交付硬约束**:子 agent 不得自行裁剪源路径范围,必须按主手写 inventory 执行;交付按「子交付 YAML schema」(字段:`source` / `scope` / `capabilities` / `cross_refs` / `pending`),禁止散文式回传;子不得写 `manifest-routing.json` / `.Knowledge/index.md`;子不得单独宣布「已进知识库」。 +- 主必控:重合判定、终稿定稿、`f2s-kb-build` 调度、整体验收。 +- 写权硬约束:`manifest-routing.json` 与 `.Knowledge/index.md` 恒由主 agent 落盘。 +- 落盘侧自验。 + +# f2s-kb-add:多文件聚合 -> 初稿 -> 终稿 -> 知识路由同步 + +## 使用时机 + +- 某能力已在代码中落地,但信息分散在多个文件,需沉淀为可检索知识。 +- 与 `f2s-doc-arch` 区分:`doc-arch` 产出架构初稿;`doc-add` 产出“已落地能力”知识沉淀链路。 + +## 输入 + +| 参数 | 必填 | 说明 | +| --- | --- | --- | +| 文件路径列表 | 是 | 一个或多个路径(空格/换行/`@`);支持源码、配置、文档 | +| 方案名 | 否 | 用于生成 `<方案名>_初稿.md`、`<方案名>_终稿.md` | +| 初稿/终稿路径 | 否 | 默认放 `.Knowledge/stock-docs/` | + +无有效路径时中止并要求用户补充。 + +## 步骤 0:重合判定(重要) + +执行前先对照: + +- `.Knowledge/index.md` +- `.Knowledge/topics/*.md` +- `.Knowledge/stock-docs/*.md` + +若已有同主题沉淀,优先原位更新,避免重复主题和重复索引行。 + +## 步骤 0.5:多模块检测(输入路径 ≥ 2 时必须执行) + +1. **目录聚合**:按路径中的功能层目录(如 `src/<模块名>/`、顶层目录名)对文件分组。 +2. **判定规则**(满足任一即判定为「多模块」): + - 文件分属 ≥ 2 个不同顶层功能目录(如 `auth/`、`payment/`); + - 用户在输入中明确提及「多个功能 / 不同模块 / 分别处理」等; + - 文件名前缀明显不同且无共同父目录。 +3. **单模块(未触发判定)**:不中断,继续步骤 1,按现有单输出逻辑生成 `<方案名>_初稿.md`。 +4. **多模块(触发判定)**:**暂停**,向用户展示分组结果,并询问: + - **方案 A(推荐)**:按模块分别生成知识文件 → 每组独立走步骤 1→2→3→4,各自产出 `<模块名>_初稿.md` / `<模块名>_终稿.md`; + - **方案 B(合并)**:忽略模块边界,合并生成一份 `<方案名>_初稿.md`(原有行为)。 + - **禁止**在未获用户明确选择前默认走方案 B 继续执行。 +5. **单模块但 stock-doc 体量大**:若单份输入文档或聚合后的源码超过 **300–500 行**,或涵盖 **3 个以上不相干职责域**,建议向用户提示"可拆成多份 focused stock-doc,各自对应独立 topic";用户确认继续则不阻断,但在输出摘要中记录"建议后续拆分"。 + +## 步骤 1:适度深度解析 + +- 小文件通读; +- 大文件优先结构与关键片段(导出、接口、配置、流程); +- 不确定内容显式标注”待确认”,禁止编造。 +- 若任一拆子阈值满足(输入路径 ≥ 5 / 单源 > ~3000 行 / 多路径总量 > ~10000 行)且 `subAgent=true`,按 B 模式(默认)或 C 模式(达成切换判据时)拆子并行只读扫描;否则主全流程。**启用拆子时,子 agent 必须按主手写 inventory 与扫描契约执行,不得自行增删源路径。** + +## 步骤 2:生成初稿 + +- 默认输出:`.Knowledge/stock-docs/<方案名>_初稿.md` +- 初稿建议结构: + - 概述 + - 来源清单(含不可读文件) + - 分模块归纳 + - 交叉关系 + - 待确认项 + +## 步骤 3:生成终稿 + +- 参考 `.Knowledge/template/终稿模版.md` +- 输出:`.Knowledge/stock-docs/<方案名>_终稿.md` +- **必须填写 `## 来源文件` 小节**,列出步骤 1 实际读取的原始源文件路径 +- 若用户要求”先审初稿”,则停在初稿并等待确认 + +## 步骤 4:同步知识路由 + +基于终稿调用 `f2s-kb-build` 口径,更新: + +- `.Knowledge/topics/` +- `.Knowledge/index.md` +- 路由清单(必要时) +- `manifest-routing.json.topicMetadata`(按需):仅给已存在或本次确认创建的 topicId 写入 `primary` / `tags` / `confidence`;`tags` 可省略,且不得与 `primary` 重复。分类只用于治理、审计和阅读预期,不参与路由或执行强制性;证据不足时不写 metadata,并在摘要列为待确认;不得为了分类单独创建、重命名或拆分 topic。 + +> **创作侧准则**:本步骤会触发新增 / 修改 topic 与 `topicDependencies`,**须先 Read** `rules/f2s-topic-authoring.*` 全文(**Cursor/Claude**:`rules/f2s-topic-authoring.mdc`;**Codex**:`.codex/topics/f2s-topic-authoring.md`),再调用 `f2s-kb-build` 口径同步。 + +## 输出摘要(必须) + +1. 初稿/终稿路径 +2. 更新的 topic/index/路由清单 路径 +3. 未完成项与原因(如路径无效、信息不足) + +## 复杂场景示例 + +用户输入 6 个文件(代码、配置、旧文档混合),其中 2 个路径不可读。 + +- 先继续处理可读文件,初稿中明确列出不可读路径和缺口,不因部分失败中断全流程。 +- 若发现已有 `.Knowledge/stock-docs/<能力名>_终稿.md`:优先在该终稿上修订,而不是新建重复终稿。 +- 用户要求”先审初稿”:必须停在初稿,等待确认后再生成终稿并进入 `f2s-kb-build` 同步。 + +用户输入 3 个文件:`src/auth/login.ts`、`src/payment/checkout.ts`、`src/notification/email.ts`。 + +- 步骤 0.5 检测到文件分属 `auth/`、`payment/`、`notification/` 三个不同顶层功能目录,判定为「多模块」。 +- 向用户展示分组:`auth` 组 1 个文件、`payment` 组 1 个文件、`notification` 组 1 个文件;询问方案 A(分别生成)或方案 B(合并)。 +- 用户选方案 A:按 `auth`、`payment`、`notification` 三组各走步骤 1→2→3→4,分别产出 `auth_初稿.md`、`payment_初稿.md`、`notification_初稿.md`。 +- **禁止**在用户选择前直接合并三个模块生成 `综合_初稿.md`。 + +## 约束 + +- 终稿 `sourceDoc` 仅指向 `.Knowledge/stock-docs/*` +- 不改配置根 `rules/skills` +- 同主题优先更新,不平行新建重复知识 +- `manifest-routing.json` 与 `.Knowledge/index.md` 恒由主 agent 落盘(写权硬约束),子 agent 不得触碰 + +## 完成后自检 + +1. 初稿/终稿路径是否落在 `.Knowledge/stock-docs/`。 +2. 同主题是否避免重复新建。 +3. topic/index/manifest 是否与终稿语义一致。 +4. 若写入 `topicMetadata`:是否只覆盖已存在或本次已创建的 topicId;`primary` / `tags` / `confidence` 是否合法;是否避免类型前缀命名与重命名。 +5. 输入路径 ≥ 2 时,步骤 0.5 是否执行了多模块检测;若判定为多模块,是否向用户展示了分组并等待了明确选择,未默认合并输出。 diff --git a/packages/core/templates/zh-CN/skills/f2s-kb-addRules/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-kb-addRules/SKILL.md new file mode 100644 index 0000000..93bc55f --- /dev/null +++ b/packages/core/templates/zh-CN/skills/f2s-kb-addRules/SKILL.md @@ -0,0 +1,168 @@ +--- +name: f2s-kb-addRules +description: 把用户口述的规则沉淀进知识库,自动判定「新建主题 / 并入存量主题」并同步路由;不写代码、不创建 .task/;触发:f2s-kb-addRules、新增规则、口述规则、把这条记到知识库 +--- + +> **任务路径**:凡 `.task/` 落盘与续作,**必须以 `rules/f2s-task` 解析的 `TASK_ROOT` 为准(`.task` 或 `.task/`;config → git → legacy)。下文若仍出现 `.task/todo.json` / `.task/active/`,均视为 **`TASK_ROOT/...` 的简写**。 + + +> 执行口径:本技能只维护 `.Knowledge`(`topics/index/manifest-routing/matchers` 分片),不改配置根 `rules/skills`,不动业务代码,不创建 `.task/`(口述规则属于元配置变更,不是业务变更追踪)。 + +# f2s-kb-addRules:用户口述规则进知识库 + +## 与既有技能的边界 + +- 与 `f2s-kb-feat` 区分:`f2s-kb-feat` 强绑「代码实现 + KB 同步」,命中 `changeTracking.feat` 会创建 `.task/`;本技能**只沉淀规则**,不改代码、不追踪任务。 +- 与 `f2s-kb-build` 区分:`f2s-kb-build` 输入是 `.Knowledge/stock-docs/_终稿.md`;本技能输入是**用户当场口述的规则文本**。 +- 与 `f2s-kb-add` 区分:`f2s-kb-add` 输入是「多文件源码 / 配置」聚合到 stock-docs;本技能跳过 stock-docs,直接落 topic。 + +## 编排(主 / 子 agent) + +- `subAgent` / `switchAgentVerification` 语义以统一入口为唯一事实源(**Cursor/Claude** 读 `rules/f2s-flow2spec-unified-entry.*`;**Codex** 读 `.codex/topics/f2s-flow2spec-unified-entry.md`)。本 SKILL 不复述。 +- 默认主 agent 全流程执行——口述规则单条短文,拆子收益低于 context 切换成本。 +- **写权硬约束**:`.Knowledge/manifest-routing.json` / `.Knowledge/index.md` 恒由主 agent 落盘。 +- 落盘侧自验。 + +## 输入 + +- 一条或一段用户口述的规则文本(自由文本即可,无固定格式)。 +- 用户**不需要**指定目标主题、文件名、`alwaysApply` 等参数;由本技能判定与提议。 + +## 强制前置:Read 创作侧准则 + +执行任何步骤前,**须先 Read** `rules/f2s-topic-authoring.*` 全文(**Cursor/Claude**:`rules/f2s-topic-authoring.mdc`;**Codex**:`.codex/topics/f2s-topic-authoring.md`),后续命名 / 骨架 / 依赖判定 / DAG 最小化 / 写盘权属均以该条为准。 + +## 步骤 1:意图归一 + +把用户口述文本归一为可落盘的"规则单元": + +- 抽取**约束句式**("做 X 时必须 / 禁止 / 优先 Y")或**流程描述**("X 的处理顺序是 A→B→C"); +- 标识规则**适用场景**(触发条件、文件路径范围、生命周期阶段等); +- 不替用户引申、不补未说的边界——口述什么写什么,模糊处保留并在步骤 3 询问。 + +## 步骤 2:扫存量主题(必须) + +- Read `.Knowledge/manifest-routing.json` 取 `topicPaths` 全集; +- Read `.Knowledge/index.md` 主题表,按主题 id + 一句话意图扫一遍; +- 必要时按规则正文中的**关键词**逐个 Read 候选 `topics/.md` 头部 10–30 行(不要全文加载所有 topic); +- 输出**候选清单**(重合度高 → 低,至多 3 个)作为步骤 3 的输入。 + +## 步骤 3:新建 vs 并入判定(必须,与用户确认) + +向用户**展示候选**,按下列分支提议: + +- **高重合**(口述规则明显是某存量主题的细化 / 补充 / 例外)→ 提议「**并入** `topics/.md`」,并指出拟插入位置(章节名 / 段落锚点)。 +- **无重合 / 低重合**(找不到合适宿主)→ 提议「**新建** `topics/<新 id>.md`」;新 id 由本技能按规则正文生成 **kebab-case**,遵循 `f2s-topic-authoring` 命名约束(无版本后缀、无个人花名、与 `index.md` 既有标题不冲突)。 +- **跨多个主题**(一条口述同时约束 ≥2 个主题)→ **暂停**,向用户呈现拆分选项: + - 选项 A:拆为 ≥2 条规则单元,分别并入对应主题; + - 选项 B:选主归并到一个主题,其它主题以一行交叉引用提示; + - 选项 C:新建一个**总纲性**主题统辖,旧主题加引用——仅在该规则确实横切多个领域时使用。 + +> 用户未确认前**禁止**落盘 `topics/` / `manifest-routing.json` / `index.md`。 + +## 步骤 4:落盘(用户确认后执行) + +### 4a. 写 `topics/.md` + +- **新建**:按 `f2s-topic-authoring` 第 2 节"topic 正文骨架"五点逐项写入(标题与一句话意图 / 适用场景 / 核心规则 / 依赖声明 / 边界与禁止项); +- **并入**:在用户确认的章节 / 段落处**手术式插入**——只增加与本次规则直接相关的句段,禁止整文件重写或借机重述背景; +- 行文遵守 `f2s-flow2spec-unified-entry`「知识库落盘文风」**肯定式优先**;排他性选择例外。 + +### 4b. `topicDependencies` 判定(必须) + +按 `f2s-topic-authoring` 第 4 节四问 + 反向排除 + DAG 最小化,扫新写正文中**反引号引用的其他 topic id / 规则文件名**,逐个判定是否声明依赖: + +- 命中 → 在 `manifest-routing.topicDependencies` 增加边,**且**在新 / 改 topic 正文显式写一句「执行前须先读依赖主题 ``」; +- 未命中 → 不写依赖,靠 `taskToTopicRules` 次高候选 + `expand` 补召回。 + +并入存量主题时,若仅是细化既有规则、未引入对新 topic 的强引用,**通常不需要**新增依赖边。 + +### 4c. 同步路由(仅主 agent 落盘) + +- **新建主题**: + - 补 `manifest-routing.topicPaths`:` -> .Knowledge/topics/.md`; + - 按需补 `manifest-routing.topicMetadata`:口述规则主题通常为 `{ "primary": "policy", "confidence": "inferred" }`;用户明确确认分类可写 `manual`;如同时包含配置项 / 模块 / 能力性质,可写入不与 `primary` 重复的 `tags`;证据不足则不写 metadata,并在摘要列为待确认。分类只用于治理、审计和阅读预期,不参与路由命中或执行强制性; + - 视情况补 `taskToTopicRules[]`——**仅当**该规则会作为**用户任务路由命中**(参见 `f2s-topic-authoring` 第 5 节判据)才补;纯被其它规则 / SKILL 引用的内部规则**不进** `taskToTopicRules`; + - 若补了 `taskToTopicRules[]`,须新建 `.Knowledge/matchers/.json`,从用户口述中抽取 `includeAny` 关键词(用户原话 + 1–2 个明显近义说法,宁缺勿滥); +- **并入存量主题**: + - `topicPaths` 不变; + - 可按需补齐该 topic 的 `topicMetadata`,但不得为了分类创建、重命名或拆分 topic; + - 仅当口述规则**新增了触发场景**时,最小更新对应 `matchers/.json` 的 `includeAny`;否则不动 matcher。 + +### 4d. 更新 `index.md` + +- 新建主题:在主题表新增一行(同主题单行原则);「关联文档(摘要)」列填「无」或「待补充」(口述规则通常无 stock-docs / req-docs 锚定文档),禁止留空; +- 并入存量主题:仅在主题意图发生变化时更新该行的「主题意图」摘要列,否则不动。 + +## 步骤 5:输出摘要(必须) + +```markdown +## 规则捕获结果 + +### 口述规则 +> <用户原文,1–3 行> + +### 落盘决策 +- 模式:新建 / 并入 / 跨主题拆分 +- 目标:.Knowledge/topics/.md(章节:<可选>) + +### 知识库变更 +- .Knowledge/topics/.md:<新增 / 修订说明> +- .Knowledge/manifest-routing.json: +- .Knowledge/matchers/.json:<是否更新 includeAny 与原因> +- .Knowledge/index.md:<是否更新与原因> + +### 待用户后续 +- <如无 taskToTopicRules,提示"该规则当前不会被任务路由命中,需要时可补";其它跟进项一并列出> +``` + +## 约束 + +- 不写代码、不动配置根 `rules/skills`、不创建 `.task/`。 +- 用户未确认「新建 / 并入 / 跨主题拆分」前禁止落盘。 +- 同主题优先并入,避免新建近似主题(参见 `f2s-topic-authoring` 命名"不要"项)。 +- `manifest-routing.json` 与 `.Knowledge/index.md` 恒由主 agent 落盘(写权硬约束)。 +- 路由清单仅做最小改动,不重写无关字段。 +- 行文遵守统一入口「知识库落盘文风」与单文件篇幅软约束(口述规则通常 ≤ 30 行新增正文足矣)。 + +## 复杂场景示例 + +**场景 A:高重合并入** + +用户口述:「写 commit message 时,第一行必须中文 emoji 开头」。 +扫描发现已存在 `topics/f2s-git-commit.md`(描述 git commit 流程)。 +- 步骤 3 提议:**并入** `topics/f2s-git-commit.md` 的「commit 文风」章节; +- 步骤 4a 在该章节追加规则段,不改其他章节; +- 步骤 4b 不新增依赖; +- 步骤 4c manifest 不动,仅在该 topic 对应 matcher(若存在)中补 1–2 个关键词; +- 步骤 4d index 不动。 + +**场景 B:新建主题** + +用户口述:「所有面向用户的错误提示必须以动词开头,如『重试』『检查 X』而非『错误:X 失败』」。 +扫描未找到合适宿主。 +- 步骤 3 提议:**新建** `topics/error-message-style.md`; +- 步骤 4a 按骨架写入; +- 步骤 4b 评估是否依赖 i18n / 文案规范类既有 topic,命中则声明; +- 步骤 4c 判断「用户日常对话中是否会触发"错误提示文案"任务路由」——若会,补 `taskToTopicRules` + 新建 matcher;若仅作为内部规范被其它 SKILL 引用,则**不进** `taskToTopicRules`; +- 步骤 4d index 新增一行。 + +**场景 C:跨主题拆分** + +用户口述:「按方案实现时不能边写边改文档;提交 PR 时必须先跑测试」。 +明显涉及 `f2s-implement-tech-design`(实现纪律)和 `f2s-git-commit`(提交流程)两个主题。 +- 步骤 3 暂停,呈现 A / B / C 三选项; +- 用户选 A → 拆为两条规则单元,分别并入两个主题; +- 步骤 4 在两个 topic 中分别落盘,输出摘要列出两条变更。 + +## 完成后自检 + +1. 是否在落盘前 Read 了 `rules/f2s-topic-authoring.*` 全文。 +2. 是否在用户未确认「新建 / 并入 / 跨主题拆分」前提前落盘(必须为否)。 +3. 新建 topic:`topicPaths` 是否补全;正文是否含五点骨架;`taskToTopicRules` 与 matcher 的 `includeAny` 是否符合「rule 是否需建对应 topic 路由」判据。 +4. 若写入 `topicMetadata`:key 是否存在于 `topicPaths`;`primary` / `tags` / `confidence` 是否合法;是否未因分类改 topicId / 文件名。 +5. 并入 topic:是否仅做手术式插入;是否未借机改写无关章节。 +6. `topicDependencies` 是否经四问判定;是否引入冗余传递边或环。 +7. `index.md` 与 `topics/` 文件集合是否一一对应;新建主题是否补「关联文档(摘要)」列。 +8. 是否未触碰配置根 `rules/skills`;是否未创建 `.task/`。 +9. 输出摘要是否齐全(口述原文 / 决策 / 变更 / 待跟进)。 diff --git a/packages/core/templates/zh-CN/skills/f2s-kb-build/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-kb-build/SKILL.md new file mode 100644 index 0000000..bda9c7b --- /dev/null +++ b/packages/core/templates/zh-CN/skills/f2s-kb-build/SKILL.md @@ -0,0 +1,113 @@ +--- +name: f2s-kb-build +description: 根据 .Knowledge/stock-docs 文档生成知识路由主题与索引;触发:生成项目上下文、f2s-kb-build、终稿生成上下文 +--- + +> 执行口径:本技能只维护 `.Knowledge`(`topics/index/manifest-routing/matchers` 分片),不改配置根 `rules/skills`。不再维护 `.Knowledge/manifest-matchers.json`(已废弃聚合文件;`flow2spec init` 会删除遗留副本)。 + +# 根据文档生成项目上下文(topics/index/路由清单) + +## 编排(主 / 子 agent) + +- 两字段(`subAgent` / `switchAgentVerification`)语义以统一入口为唯一事实源:**Cursor/Claude** 读配置根 `rules/f2s-flow2spec-unified-entry.*`;**Codex** 读 `.codex/topics/f2s-flow2spec-unified-entry.md`(与上同源,`flow2spec init` 镜像)。本 SKILL 不复述。 +- **首选分支(小变更 → 主全流程)**:当本次改动 **≤ 2 个新 / 改主题**,**且 ≤ 1 个新 matcher**,**且无跨主题批量引用调整** 时,全流程在主 agent 完成,不拆子。 +- **中大变更分支**(`subAgent=true` 且超出上述阈值): + - 主 agent 在主会话中列出**文件级契约**:子 A 只写 `.Knowledge/topics/.md`,子 B 只写 `.Knowledge/matchers/.json`,路径互不重叠; + - 子 agent 仅落盘契约内文件,不跨边界; + - **主 agent 单点**编辑 `.Knowledge/manifest-routing.json` / `.Knowledge/index.md`(补 `taskToTopicRules`、`topicPaths`、`matcherPath`、`topicDependencies`、`topicMetadata`); + - 主 agent 做整体验收。 +- **不推荐**:单个子 agent 同时改 manifest / index / 多份 topics / matchers;以及「子 A 写、子 B 验」。 +- **「一子写、主验」**:仅在交付边界极窄(例如只产出 1 个新 matcher 分片草稿,manifest 引用仍由主写)时可接受。 +- **写权硬约束**:`.Knowledge/manifest-routing.json`(含 `topicMetadata`)/ `.Knowledge/index.md` **恒由主 agent 落盘**,子 agent 不得触碰。 +- 默认落盘侧 agent 自验;本 SKILL 不绑定交叉校验。 + +## 输入 + +- 接收一个参数:URL 或本地路径。 +- 本地路径必须位于 `.Knowledge/stock-docs/`。 +- **须为终稿**:推荐文件名含 `_终稿.md`,或已由 **`f2s-doc-final`** 规范化;**禁止**以 `f2s-doc-arch` 产出的 `*_初稿.md` 作为入参直接执行本技能。 +- 若入参路径含 **`_初稿`**、或用户刚完成架构初稿尚未执行 `f2s-doc-final`:**停止**,回复须先执行 **`f2s-doc-final <初稿路径>`**,待终稿落盘后再以终稿路径调用本技能。 +- 若传入 `.Knowledge/req-docs/`,提示用户先整理为 `stock-docs` 终稿后再执行。 + +## 生成原则 + +1. **拆解**:文档较长或包含多块独立能力时,拆分为多个 topic;避免把无关能力塞到同一主题。 +2. **分工**: + - `topics/`:规则与流程正文(可执行知识) + - `index.md`:主题索引与语义说明(人读入口) + - `manifest-routing.json` + `taskToTopicRules[].matcherPath` 指向的 `matchers/*.json`:任务路由与关键词词表(机读入口) + +## 步骤 1:获取文档内容 + +- URL:抓取正文;无法访问时提示用户先落地到 `.Knowledge/stock-docs/*.md`。 +- 本地路径:读取 Markdown 文档,提炼主题与能力边界。 + +## 步骤 2:语义分析(必须) + +从文档中提炼: + +- 主题名与主题意图(可形成 topic id) +- 核心概念与关键流程 +- 业务规则与边界条件 +- 任务触发词(写入对应 `matchers/.json` 的 `includeAny`) +- 与现有主题的依赖关系(用于 `topicDependencies`) + +> **创作侧准则**:本步骤涉及新增 / 修改 topic 与 `topicDependencies`,**须先 Read** `rules/f2s-topic-authoring.*` 全文(**Cursor/Claude**:`rules/f2s-topic-authoring.mdc`;**Codex**:`.codex/topics/f2s-topic-authoring.md`),再继续步骤 3 / 步骤 5。命名、骨架、依赖判定、DAG 最小化、判定时机均以该条为准,本 SKILL 不复述。 + +> **拆分评估**:若输入 stock-doc 超过 **300–500 行**,或语义分析后发现覆盖 **3 个以上不相干职责域**,须在输出摘要中说明:建议拆成多份 focused stock-doc(各自对应一个独立 topic),用户确认后再分批执行;若用户选择继续生成单个大 topic,不阻断,但在摘要中记录"主题偏大,建议后续拆分"。大功能主 topic 写业务闭环/入口/子模块 stock-doc 导航链接;子模块 topic 各自独立命中,**不通过 `topicDependencies` 串联概述与详情**。 + +## 步骤 3:写入 topics + +- 目标路径:`.Knowledge/topics/.md` +- 若已存在同主题:优先增量更新,避免重复主题。 +- 若为新主题:新增文件并补充清晰标题、适用场景、规则与流程。 + +## 步骤 4:更新 index + +- 更新 `.Knowledge/index.md` 的主题路由表。 +- 保证“同主题单行”。 +- 主题路由表需维护“关联文档(摘要)”列:每个主题补充 1-3 条关键文档**可点击 Markdown 链接**(格式:`[标题](相对路径)`,优先 `stock-docs/req-docs`)。 +- 若某主题暂无可公开文档,写“无”或“待补充”,禁止留空导致歧义。 +- 若新增/删除主题,索引同步调整,避免孤儿路径。 + +## 步骤 5:更新路由清单(按需) + +- 本步骤由主 agent 落盘(写权硬约束),子 agent 不得执行。 +- 更新 `manifest-routing.topicPaths`(topicId -> topic 文件路径) +- 更新 `manifest-routing.taskToTopicRules[]`(任务到主题集合 + matcherId) +- 更新 `manifest-routing.topicDependencies`(先读依赖后读主主题) +- 更新 `manifest-routing.topicMetadata`(按需):仅给已存在或本次确认创建的 topicId 写入 `{ "primary": "feature|module|config|policy", "tags": ["..."], "confidence": "manual|inferred" }`;`tags` 可省略,且不得与 `primary` 重复。分类只用于治理、审计和阅读预期,不参与路由命中或执行强制性。新建 topic 时有明确证据可写 `inferred`;用户确认后才写 `manual`;证据不足时不写 metadata,并在摘要列为待确认。不得为了分类创建、重命名或拆分 topic。 +- 更新 `matchers/.json` 的 `includeAny`(关键词词表;路径须与 `taskToTopicRules[].matcherPath` 一致) +- 校验 `fallbackTopic`、`topicPaths`、`matcherId` 引用有效 +- 仅做最小改动,不重写无关字段 + +## 路径与引用约束 + +- `sourceDoc` 或文档引用统一指向 `.Knowledge/stock-docs/<文件名>.md` +- 禁止把 `.Knowledge/req-docs/` 作为 topic 的 `sourceDoc` +- 禁止改写配置根 `rules/skills` + +## 输出摘要(必须) + +- 新增/更新的 topic 文件 +- `index` 更新项 +- 路由清单更新项(如有) +- 失败或跳过项及原因 + +## 复杂场景示例 + +用户输入:`f2s-kb-build .Knowledge/stock-docs/<能力>_终稿.md`,且现有 `topics/<能力>.md` 已存在。 + +- 若新文档与现有 `<能力>` 主题高度重合:原位更新 `topics/<能力>.md`,不要新建 `<能力>-v2.md`。 +- 若新文档新增子能力:可新增 `topics/<能力>-<子域>.md`,并在 `manifest-routing.topicDependencies` 中声明依赖关系。 +- 更新后同步 `index` 与路由清单,确保 `topicPaths`、`fallbackTopic`、`matcherId` 仍有效。 + +## 完成后自检 + +1. `.Knowledge/topics/*.md` 与 `manifest-routing.topicPaths` 一一对应。 +2. `index.md` 主题表与 topics 文件集合一致,且每个主题都包含“关联文档(摘要)”。 +3. 每个 `taskToTopicRules[].matcherPath` 文件存在且其中 `id` 与 `matcherId` 一致。 +4. 若写入 `topicMetadata`:key 是否均存在于 `topicPaths`;`primary` / `tags` / `confidence` 是否合法;`tags` 是否未与 `primary` 重复;是否未因分类改 topicId / 文件名。 +5. 未触碰配置根 `rules/skills`。 +6. 中大变更时是否按文件级契约拆子(子 A / 子 B 路径互不重叠)。 +7. `manifest-routing.json` / `.Knowledge/index.md` 由主 agent 单点落盘,无子 agent 越权写入。 diff --git a/packages/core/templates/zh-CN/skills/f2s-kb-distill/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-kb-distill/SKILL.md new file mode 100644 index 0000000..c2f923b --- /dev/null +++ b/packages/core/templates/zh-CN/skills/f2s-kb-distill/SKILL.md @@ -0,0 +1,387 @@ +--- +name: f2s-kb-distill +description: 从问答过程中提取可复用知识事实并自动入库;根据下钻深度与命中主题判断新增主题或补充既有主题;触发:f2s-kb-distill、问答知识提取、从对话中提取知识 +--- + +> **任务路径**:凡 `.task/` 落盘与续作,**必须以 `rules/f2s-task` 解析的 `TASK_ROOT` 为准(`.task` 或 `.task/`;config → git → legacy)。下文若仍出现 `.task/todo.json` / `.task/active/`,均视为 **`TASK_ROOT/...` 的简写**。 + + +> 执行口径:本技能只维护 `.Knowledge`,默认不改配置根 `rules/skills`。 + +## KB 自动合并协议(必须) + +本技能不得把“人工执行命令”作为用户流程。用户触发本技能后,由 agent 自己完成知识候选生成、合并、构建与校验: + +1. 若本轮存在可沉淀知识,先在当前任务上下文中形成 `kb-delta` 草稿,记录 `taskId`、`developerId`、`baseRevisions`、`changes` 与证据摘要;没有显式任务目录时可在内存中形成等价对象,不强制为了本技能创建 `.task`。`changes` 可使用 `appendBody` / `replaceBody` / `updateFrontmatter`;确需新主题时使用 `createTopic`,并可携带 `taskRule` 与 `matcher` 让路由一并接入。 +2. 写入 `.Knowledge` 前,必须用 `flow2spec kb plan ` 或等价内部能力预演;若 topic revision 不一致,停止自动写入,转入语义合并说明。 +3. 可自动合并时,由 agent 调用 `flow2spec kb apply ` 或等价内部能力写入 topic,并随后执行 `flow2spec kb build` 与 `flow2spec kb check`。 +4. 用户只看到“知识库已同步 / 有语义冲突需确认 / 已跳过入库及原因”,不要求用户手动执行 `kb plan/apply/build/check`。 + +## 编排(主 / 子 agent) + +- `subAgent` / `switchAgentVerification` 两字段语义以统一入口为唯一事实源:**Cursor/Claude** 读配置根 `rules/f2s-flow2spec-unified-entry.*`;**Codex** 读 `.codex/topics/f2s-flow2spec-unified-entry.md`(与上同源,`flow2spec init` 镜像)。 +- 本技能默认不拆子:问答知识提取是单轮聚焦任务,由主 agent 全流程完成效率更高。 +- 写权硬约束:`manifest-routing.json` 与 `.Knowledge/index.md` 恒由主 agent 单点落盘。 +- 校验:落盘侧自验。 + +# f2s-kb-distill:问答驱动的知识提取与入库 + +## 使用时机 + +- 用户提问 → agent 下钻源码回答 → 需要将发现的知识沉淀到 KB +- 通常由 `f2s-kb-feedback-closing` 规则自动建议,也可用户主动调用 +- 与 `f2s-kb-sync` 区分:`sync` 适合批量同步多个能力;`distill` 专注单次问答的知识提取 + +## 输入 + +| 参数 | 必填 | 说明 | +| --- | --- | --- | +| 用户问题 | 自动获取 | 上一轮用户的提问(自动从对话历史提取) | +| agent 回答 | 自动获取 | 上一轮 agent 的回答内容(自动从对话历史提取) | +| 命中主题 | 可选 | 如由 `f2s-kb-feedback-closing` 触发,会携带命中的 topicId | +| 下钻文件 | 自动分析 | 从回答中提取引用的文件/函数(自动分析) | + +无有效问答上下文时中止并提示用户。 + +## 执行挡位(轻量 / 严格,agent 自动判) + +`f2s-kb-distill` 只有**一个**入口(无 `--fast` 参数),进入流程第一件事是**判挡**: + +### 判挡依据(4 个维度,全满足才走轻量挡) + +| 维度 | 取值方式 | 走「轻量挡」的条件 | +| --- | --- | --- | +| 上游 `f2s-kb-feedback-closing` case | 看本轮 / 上一轮 agent 回答末尾的收口块 | **case 2 或 case 3**(case 1 / 无收口 → 严格挡) | +| 本轮 Read 业务源码文件数 | agent 回顾本轮自己的工具调用 | **≤ 3 个** | +| 本轮回答引用的函数 / 类名数 | 数回答里反引号包裹的 `xxx()` / 类名 | **≤ 5 个** | +| 用户追问是否否定上游结论 | 看用户最新输入是否含"不对 / 重新分析 / 那条不准"等 | **否** | + +**4 项全满足** → **轻量挡**:跳过步骤 2.1(量化打分)/ 2.4(既有 topic 描述程度评估)/ 步骤 3(决策矩阵)/ 步骤 4.1 的「读近邻 topic 风格对齐」;直接采上游「本轮将入库:<概要>」做策略与目标 topicId 判定,进入步骤 4 生成内容。 + +**任一不满足** → **严格挡**:跑完整 6 步。 + +> **业务源码定义**:路径**不在** `.claude/` / `.cursor/` / `.codex/` / `.Knowledge/` / `.task/` 这 5 个目录下的 Read 才计数;规则文件 / topic / config / 任务清单都不算。 + +### 为什么这几个维度够(设计意图) + +- **case 类型**挡掉「新增 topic」场景:新建必须读近邻 topic 学风格、必须配 matcher `includeAny`、必须更新 `taskToTopicRules`,跳不得; +- **Read 文件数 + 函数引用数**反映本轮知识够不够"轻":轻量补充才能跳决策,深度入库(多文件 + 多函数)跳了会撞「描述程度差 ≥ 2 级」事故; +- **用户否定信号**兜底"上游 case 判错了"的边界。 + +### 步骤 5 / 6 永不省 + +无论哪一挡,**步骤 5 路由 / matcher / index 同步**与**步骤 6 落盘 + 自检**都跑完整版——这是入库正确性的硬约束。 + +## 强制流程(不可颠倒) + +### 步骤 0:读取配置与规则 + +1. 读取 `flow2spec.config.json`(获取 `subAgent` / `switchAgentVerification`) +2. 读取 `.codex/topics/f2s-kb-feedback-closing.md`(获取"可复用知识事实"定义) +3. 读取 `.codex/topics/f2s-topic-authoring.md`(获取 topic 创作准则) + +### 步骤 1:提取问答上下文 + +从上一轮对话中提取: + +1. **用户问题**:原始问题文本 +2. **agent 回答**:完整回答内容 +3. **命中主题**:如果 `f2s-kb-feedback-closing` 已分析,提取命中的 topicId;否则根据问题重新路由 +4. **下钻文件**:从回答中提取所有引用的文件路径、函数名、行号 +5. **引用源码**:提取回答中引用的代码片段 + +### 步骤 2:分析下钻深度与知识性质 + +> ****轻量挡**跳过**:本步骤的 2.1 / 2.4 整段跳过;2.2(提取知识事实)必须执行;2.3(本次知识描述深度)改为**简短标注**(一行写"摘要级 / 详细级 / 实现级"即可,不再多维评估)。 + +#### 2.1 计算下钻深度得分 + +累加以下指标(每项 0-10 分,总分 0-50): + +- **读取文件数**: + - 0 个文件:0 分 + - 1-2 个文件:3 分 + - 3-5 个文件:7 分 + - 6+ 个文件:10 分 + +- **分段读取次数**(同一文件多次读取不同行范围): + - 0-1 次:0 分 + - 2-4 次:3 分 + - 5-8 次:7 分 + - 9+ 次:10 分 + +- **函数/类引用数**: + - 0-2 个:0 分 + - 3-5 个:3 分 + - 6-10 个:7 分 + - 11+ 个:10 分 + +- **代码片段长度**: + - 0-50 行:0 分 + - 51-150 行:3 分 + - 151-300 行:7 分 + - 301+ 行:10 分 + +- **回答篇幅**: + - 0-200 字:0 分 + - 201-500 字:3 分 + - 501-1000 字:7 分 + - 1001+ 字:10 分 + +**下钻深度分级**: +- **浅**(0-15 分):简单问答,少量源码引用 +- **中**(16-30 分):中等复杂度,多文件查阅 +- **深**(31-50 分):深度探索,大量源码分析 + +#### 2.2 提取可复用知识事实 + +从回答中提取以下类型的知识(参考 `f2s-kb-feedback-closing`): + +- 核心机制(缓存语义、重试策略、降级逻辑) +- 状态流转(状态机、生命周期) +- 返回值/错误码契约 +- 配置开关影响 +- 失败回退策略 +- 模块边界或调用约定 +- 数据模型与字段语义 + +**提取结果**: +- 每条知识事实包含:类型、描述、来源(文件:行号) +- 按重要性排序 + +#### 2.3 判断本次提取知识的描述深度 + +评估提取出的知识事实的详细程度(与长度无关,看内容特征): + +- **摘要级**:只有结论性描述("是什么"、"做什么"),无条件、流程、函数细节 + - 示例:`缓存优先、失败回退 OCR` +- **详细级**:包含机制说明、流程步骤、关键判断条件("当X时"、"如果Y则"、"先...再...") + - 示例:`缓存优先:坐标缓存命中时直接使用;失败回退条件:弹窗未消失、坐标超出边界` +- **实现级**:包含函数调用关系、状态转换细节、边界条件处理、代码示例 + - 示例:`缓存读取:调用 _get_cached_point_in_bounds("chat.input"),返回 None 时回退;失败判定:VisualSearchPopup.find(timeout=0.08) is None` + +#### 2.4 评估既有 topic 的描述程度(仅当"有命中"时) + +如果命中了既有 topic,需要评估它的描述程度(与长度无关): + +1. **读取目标 topic 内容** +2. **随机抽取 3-5 条内容**(不同段落) +3. **判断每条的描述程度**: + - **摘要级特征**:只说"是什么"、"做什么",列举式,无条件/流程/函数细节 + - **详细级特征**:包含"当X时"、"如果Y则"、"先...再..."、判断条件、机制说明 + - **实现级特征**:包含函数名 `xxx()`、类名、文件路径、参数、代码示例、状态转换逻辑 +4. **大部分条目的级别 = topic 的整体描述程度** + +**判断示例**: + +| Topic 内容 | 判定 | 原因 | +|-----------|------|------| +| `- 缓存优先、失败回退 OCR`
`- 发送消息动作链判定` | 摘要级 | 只说"做什么",无细节 | +| `- 缓存优先:坐标缓存命中时直接使用`
`- 失败回退:弹窗未消失时清除缓存并重新 OCR` | 详细级 | 有条件说明("当...时") | +| `- 缓存读取:_get_cached_point_in_bounds("chat.input")`
`- 失败判定:VisualSearchPopup.find(timeout=0.08) is None` | 实现级 | 有函数名、参数 | + +**重要**:一个 300+ 行的 topic,如果每条都是"模块 X:负责 YYY",仍然是摘要级;一个 50 行的 topic,如果每条都有"条件判断 + 函数调用",就是实现级。 + +### 步骤 3:决策入库策略 + +> ****轻量挡**跳过整段决策矩阵**:直接采用上游 `f2s-kb-feedback-closing`「本轮将入库:<概要>」给出的结论: +> - 概要含「补充 / 补到 / 补齐 `` 的某段」→ 策略 = **补充既有 topic**,目标 topicId = 概要点名的 topic; +> - 概要含「首次入库」「新增 `<能力>`」「新增 `<模块>`」→ 策略 = **新增 topic**(默认小型 topic;下钻深 + 模块独立时升级为「新增独立模块 topic」由步骤 4.3 内部判断); +> - 概要含「修正 `` 的某条」→ 策略 = **补充既有 topic**(覆盖式追加,原表述在生成内容时改写)。 + +根据以下决策矩阵判断: + +| 下钻深度 | 命中主题情况 | 既有 topic 描述程度 | 本次知识描述深度 | 策略 | +|---------|------------|------------------|----------------|------| +| 浅 | 有命中 | 摘要级 | 摘要级 | **补充既有 topic**(追加简短说明) | +| 浅 | 有命中 | 摘要级/详细级 | 详细级 | **补充既有 topic**(追加详细段落) | +| 浅 | 有命中 | 摘要级 | 实现级 | **新增子主题**(既有太简短,本次太详细) | +| 浅 | 无命中 | - | 任意 | **新增 topic**(小型 topic) | +| 中 | 有命中 | 摘要级 | 摘要级/详细级 | **补充既有 topic**(追加详细段落) | +| 中 | 有命中 | 摘要级 | 实现级 | **新增子主题**(差距 ≥ 2 级) | +| 中 | 有命中 | 详细级/实现级 | 详细级/实现级 | **补充既有 topic**(级别匹配) | +| 中 | 无命中 | - | 任意 | **新增 topic**(中型 topic) | +| 深 | 有命中 | 摘要级 | 任意 | **新增子主题**(独立 topic + stock-doc) | +| 深 | 有命中 | 详细级/实现级 | 详细级/实现级 | **补充既有 topic** 或 **新增子主题**(根据语义聚焦度判断) | +| 深 | 无命中 | - | 任意 | **新增独立模块 topic**(完整 topic + stock-doc) | + +**决策关键**: +- **描述程度差距 ≥ 2 级**(摘要 vs 实现)→ 强制新增子主题,避免风格不协调 +- **描述程度差距 = 1 级**(摘要 vs 详细,或详细 vs 实现)→ 可以追加,但要写详细段落 +- **描述程度匹配**(同级)→ 正常追加 +- **下钻深度 ≥ 深** → 倾向新增子主题,除非既有 topic 已经很详细且语义完全重合 + +**决策输出**: +- 策略类型:`补充既有 topic` / `新增子主题` / `新增独立模块 topic` +- 目标 topicId:既有 topic 的 id 或新 topic 的建议 id +- 更新内容:要追加的内容或新 topic 的结构 +- 描述程度匹配度:同级 / 差 1 级 / 差 ≥ 2 级 + +### 步骤 4:生成知识内容 + +> **创作侧准则**:本步骤会触发新增 / 修改 topic 与可能的 `topicDependencies`,须遵循已读取的 `f2s-topic-authoring` 准则。 + +#### 4.1 补充既有 topic + +如果策略是"补充既有 topic": + +1. 读取目标 topic 当前内容 +2. 读取近邻 2-3 个 topic 的风格样例(用于风格对齐) + ****轻量挡**跳过**:不读近邻 topic,仅参考目标 topic 自身的列表 / 段落形态保持一致即可。 +3. 生成要追加的内容: + - **位置**:找到最相关的段落,在其后追加 + - **格式**:保持与既有 topic 一致的列表/段落风格 + - **长度**:根据知识描述深度决定: + - 摘要级:1-3 行 + - 详细级:5-10 行,包含机制说明 + - 实现级:10-20 行,包含流程步骤与关键函数 +4. 追加内容示例: + ```markdown + - 【机制】缓存只用于坐标定位与快速路径;缓存命中不等同于步骤完成 + - 【判定】添加好友在缓存点击后仍通过窗口出现、资料页状态、提交后状态分类判断进度 + - 【边界】发送消息在按 Enter 后返回成功,当前无 OCR 校验消息气泡或发送状态 + ``` + +#### 4.2 新增子主题 + +如果策略是"新增子主题": + +1. 生成新 topicId(基于父 topic + 聚焦点): + - 例如:`wxautocontrol-architecture` → `wxautocontrol-completion-detection` +2. 创建新 topic 内容: + - 标题与一句话意图 + - 适用场景/触发词 + - 核心机制详述(从提取的知识事实生成) + - 依赖声明(依赖父 topic) + - 边界与禁止项 +3. 同步更新父 topic: + - 在相关段落追加指向子 topic 的链接 + - 说明子 topic 的聚焦点 +4. 更新 `topicDependencies`: + - 添加 `子 topic → 父 topic` 的依赖边 + +#### 4.3 新增独立模块 topic + +如果策略是"新增独立模块 topic": + +1. 生成新 topicId(基于模块名或问题域) +2. 判断是否需要创建 stock-doc: + - 下钻深度 ≥ 深:创建 stock-doc(`_终稿.md`) + - 下钻深度 < 深:仅创建 topic,不创建 stock-doc +3. 如果创建 stock-doc: + - 结构:概述、核心机制、来源文件、关键函数与流程 + - 内容:基于提取的知识事实与引用的代码片段生成 + - 长度:根据下钻深度,100-500 行 +4. 创建 topic: + - 如有 stock-doc,topic 作为摘要 + 指针 + - 如无 stock-doc,topic 包含完整的机制说明 + +### 步骤 5:同步路由与索引 + +#### 5.1 更新 manifest 与 matcher + +- 如果新增 topic: + - 在 `manifest-routing.json.topicPaths` 中添加条目 + - 创建对应的 `matchers/.json`,包含: + - 从用户问题中提取的关键词 + - 从回答中提取的术语 + - 建议 `includeAny`:5-10 个触发词 + - 在 `taskToTopicRules` 中添加路由规则 + +- 如果更新既有 topic: + - 检查 matcher 是否需要补充新的触发词 + - 从用户问题中提取未覆盖的关键词,追加到 `includeAny` + +#### 5.2 更新 index.md + +- 如果新增 topic: + - 在 `.Knowledge/index.md` 中添加新条目 + - 格式:`- **[topic 标题](topics/.md)** - 一句话说明 | 关联文档:[终稿](stock-docs/.md)`(如有) +- 如果更新既有 topic: + - 检查 index 中的描述是否需要更新 + - 如果新增了 stock-doc,更新"关联文档"列 + +#### 5.3 处理 topicMetadata(可选) + +如果有明确证据,写入 `topicMetadata`: + +- 从提取的知识事实判断 `primary` 类型: + - 核心机制/状态流转/失败回退 → `policy` + - 配置开关影响 → `config` + - 模块边界/调用约定 → `module` + - 已落地能力/业务逻辑 → `feature` +- `confidence` 设为 `inferred` +- 无明确证据时不写,在输出摘要中列为"未分类" + +### 步骤 6:落盘与自检 + +按以下顺序落盘: + +1. 如果有 stock-doc:写入 `.Knowledge/stock-docs/.md` +2. 写入或更新 `.Knowledge/topics/.md` +3. 更新 `.Knowledge/manifest-routing.json` +4. 更新 `.Knowledge/matchers/.json` +5. 更新 `.Knowledge/index.md` + +自检清单: + +1. topic 内容是否包含了提取的核心知识事实 +2. 新增 topic 是否在 index.md 中有对应条目 +3. manifest 中的 topicPaths / taskToTopicRules 是否引用有效路径 +4. matcher 的 includeAny 是否覆盖用户问题的关键词 +5. 如果新增子主题,topicDependencies 是否正确设置 +6. 追加内容是否保持了既有 topic 的风格(如已读近邻 topic) + +## 输出摘要格式 + +```markdown +## 知识提取与入库结果 + +- 执行挡位:`严格挡(完整流程)` / `轻量挡(已跳过 2.1 / 2.4 / 3 / 4.1)` / `轻量挡 → 严格挡(降级原因:<原因>)` + +### 问答分析 +- 用户问题:<问题摘要> +- 命中主题: +- 下钻深度:<浅/中/深> (<得分>) ← **轻量挡**:`未评估` +- 知识描述深度:<摘要级/详细级/实现级> + +### 提取的知识事实 +- 【核心机制】<描述> (来源:<文件:行号>) +- 【状态流转】<描述> (来源:<文件:行号>) +- ... + +### 入库策略 +- 策略:<补充既有 topic / 新增子主题 / 新增独立模块 topic> +- 目标 topic: +- 操作说明:<追加内容 / 新建 topic + stock-doc> + +### 已修改文件 +- .Knowledge/topics/.md:<修改说明> +- .Knowledge/index.md:<修改说明或"未改动"> +- .Knowledge/manifest-routing.json:<修改说明或"未改动"> +- .Knowledge/matchers/.json:<修改说明或"未改动"> +- .Knowledge/stock-docs/.md:<修改说明或"未改动"> + +### 验证建议 +- 下次遇到类似问题"<问题>"时,应命中 topic: +- 建议验证触发词:<关键词列表> +``` + +## 约束 + +- 只维护 `.Knowledge`,不改配置根 `rules/skills` +- 不需要用户确认(问答已验证知识的正确性) +- 保持轻量,单次问答的知识提取在 30 秒内完成 +- 避免过度拆分:除非下钻深度 ≥ 深且知识描述深度 ≥ 详细级,否则优先补充既有 topic +- 生成的 matcher includeAny 应覆盖用户实际会用的表述,不只是技术术语 + +## 完成后自检 + +1. 是否正确分析了下钻深度与知识描述深度(**轻量挡**:是否正确从上游概要解析出策略与目标 topicId) +2. 是否提取了所有"可复用知识事实"(参考 `f2s-kb-feedback-closing` 定义) +3. 入库策略是否符合决策矩阵(**轻量挡**:是否与上游概要点名的 case 一致) +4. 新增或更新的 topic 是否在 index.md 中有条目 +5. manifest / matcher 是否正确配置路由规则 +6. 生成内容是否保持了既有 topic 的风格(**轻量挡**:是否至少与目标 topic 自身列表/段落形态对齐) +7. **轻量挡专项**:摘要顶部是否写明「调用模式」;若中途降级,是否注明降级原因 +8. 本次回复末尾**没有**追加 `f2s-kb-feedback-closing` 的 case 1~4 任何一种收口块(必须为是;本技能就是 distill 入库本身,自指地再贴一遍提示既冗余又会让用户误以为没入库——`f2s-kb-feedback-closing`「适用范围」对此有专项禁令) diff --git a/packages/core/templates/zh-CN/skills/f2s-kb-feat/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-kb-feat/SKILL.md new file mode 100644 index 0000000..b7c5560 --- /dev/null +++ b/packages/core/templates/zh-CN/skills/f2s-kb-feat/SKILL.md @@ -0,0 +1,115 @@ +--- +name: f2s-kb-feat +description: 新增能力时补全实现与知识库;已实现则仅同步知识库;触发:f2s-kb-feat、新增能力 +--- + +> **任务路径**:凡 `.task/` 落盘与续作,**必须以 `rules/f2s-task` 解析的 `TASK_ROOT` 为准(`.task` 或 `.task/`;config → git → legacy)。下文若仍出现 `.task/todo.json` / `.task/active/`,均视为 **`TASK_ROOT/...` 的简写**。 + + +> 执行口径:`f2s-kb-feat` 默认同步 `.Knowledge`,无需用户额外提出"请同步知识库"。 + +## KB 自动合并协议(必须) + +本技能不得把“人工执行命令”作为用户流程。代码实现完成或确认已有实现后,由 agent 自己完成知识候选生成、合并、构建与校验: + +1. 将本次能力变更转换为 `kb-delta` 草稿,记录 `taskId`、`developerId`、`baseRevisions`、`changes` 与实现证据;若 `changeTracking.feat=true` 且已有任务目录,可把 delta 落在当前 `TASK_ROOT/active//kb-delta.json`,否则可在内存中形成等价对象。`changes` 可使用 `appendBody` / `replaceBody` / `updateFrontmatter`;确需新主题时使用 `createTopic`,并可携带 `taskRule` 与 `matcher` 让路由一并接入。 +2. 写入 `.Knowledge` 前,必须用 `flow2spec kb plan ` 或等价内部能力预演;若 topic revision 不一致,停止自动写入,转入语义合并说明。 +3. 可自动合并时,由 agent 调用 `flow2spec kb apply ` 或等价内部能力写入 topic,并随后执行 `flow2spec kb build` 与 `flow2spec kb check`。 +4. 用户只看到“能力与知识库已同步 / 有语义冲突需确认 / 已跳过入库及原因”,不要求用户手动执行 `kb plan/apply/build/check`。 + +## 编排(主 / 子 agent) + +- `subAgent` 与 `switchAgentVerification` 的语义以统一入口为唯一事实源:**Cursor/Claude** 读配置根 `rules/f2s-flow2spec-unified-entry.*`;**Codex** 读 `.codex/topics/f2s-flow2spec-unified-entry.md`(与上同源,`flow2spec init` 镜像)。本处不复述。 +- **代码子包**(新增 / 修改实现代码):`subAgent=true` 时可外包给子 agent 执行。 +- **文档子包**(rules / skills / topics / stock-docs 文风类改动):默认不拆,由主 agent 写,以保证「现行真值覆盖 / 篇幅上限 / 禁历史否定堆砌」等文风合规。 +- 若确需外包文档改动:子侧只输出「原位替换 diff」(before / after 小段),不得整文件重写;主合并落盘。 +- **写权硬约束**:`manifest-routing.json` / `.Knowledge/index.md` 恒由主 agent 落盘,子 agent 不得触碰。 +- 落盘侧自验。 + +# /新增能力(f2s-kb-feat) + +## 输入 + +- 用户描述新增能力、场景、边界、可选路径。 + +## 步骤 + +**步骤 0:变更追踪(仅当 `changeTracking.feat: true`)** + +执行前读取 `flow2spec.config.json`,若 `changeTracking.feat: true`: + +- 检查 `.task/todo.json` 是否存在活跃任务,将用户描述与 `keywords` 匹配。 +- 命中 → 加载对应 `task.md`,展示剩余清单,在已有任务中继续。 +- 无命中 → 创建新任务(见 `f2s-task` 规则),将步骤 1–4 写入 `task.md` 作为任务 checklist。 +- **执中必写盘**:每完成 `task.md` 中一步,**同一会话内**立即 `Edit` 将该步 `[ ]`→`[x]`;禁止把打钩积压到「收尾/归档」一步、禁止口头完成代替写盘(见 `f2s-task`「中断与会话结束」「归档门禁」)。 +- **用户代办**:凡须用户改库、配环境、点平台等项,**同会话内**追加写入 `.task/active//user-todos.md`(见 `f2s-task`);新建任务时若尚无代办,仍应创建该文件(可占位)。 + +1. 判断能力状态:未实现 / 部分实现 / 已实现。 +2. 补齐代码实现(已实现则跳过此步)。 +3. 同步知识库(默认执行): + - `.Knowledge/stock-docs/`:能力说明与使用方式 + - `.Knowledge/topics/`:新增/修订主题规则与流程 + - `.Knowledge/index.md`:主题索引 + - 路由清单:路由、依赖或 `topicMetadata` 变化时最小更新 + - **创作侧准则**:本步若新增 / 修改 topic、`topicMetadata` 或 `topicDependencies`,须先 Read `rules/f2s-topic-authoring.*` 全文(**Cursor/Claude**:`rules/f2s-topic-authoring.mdc`;**Codex**:`.codex/topics/f2s-topic-authoring.md`),再落盘。 +4. 输出摘要(能力点、实现、知识库变更)。 + +## 输出摘要格式(建议) + +```markdown +## 新增能力:<能力名> + +### 能力范围 +- <能力点1> +- <能力点2> + +### 实现 +- <文件路径>:<改动说明>(若未改代码则写"已有实现") + +### 知识库 +- .Knowledge/stock-docs/<文件>.md:<新增/修订说明> +- .Knowledge/topics/.md:<新增/修订说明> +- .Knowledge/index.md:<更新说明> +- .Knowledge/manifest-routing.json:<是否更新与原因> +- .Knowledge/matchers/.json:<是否更新 includeAny 与原因> +``` + +## 复杂场景示例 + +用户要求"新增失败重试队列能力",且代码中已有半成品实现。 + +- 先判断为"部分实现",补齐缺口代码而非重做整模块。 +- 同步新增或修订 `topics/retry-queue.md`,并更新 `index` 入口说明。 +- 若该能力需任务路由命中(如"重试队列改造"),补充 `manifest.taskToTopicRules`。 + +## 约束 + +- 与旧约定冲突时:**改写到当前真值**,不要另起「(不再与某 X 有关)」等历史否定句。 +- 与现有主题重合时优先原位更新。 +- 至少落一处知识库更新,避免"代码有了但不可检索"。 +- 不改配置根 `rules/skills`。 +- 文档子包默认不拆;必要外包子侧仅出 before/after diff 片段,主合并落盘;`manifest-routing.json` / `.Knowledge/index.md` 恒主落盘(写权硬约束)。 + +## 知识库落盘文风(必须,防赘述) + +写 `stock-docs` / `topics` / `index` 时遵守: + +1. **增量最小**:只追加或改写与**本次能力**直接相关的句段;禁止因「同步知识库」而全文重述背景、需求复述、与实现无关的教程式铺垫。 +2. **肯定式优先(见统一入口「知识库落盘文风」)**:直接写出正确描述,禁止用否定旧版来传达新约定;排他性选择除外。 +3. **不重复叙事**:同一事实在 `stock-docs` 与 `topics` **不要各写一长篇**;择一处写清可执行约定,另一处用短段落 + 链接指向,或仅列要点与引用路径。 +4. **条文化优先**:`topics` 以规则、边界、步骤、错误与配置要点为主;能用列表/表格表达的不用长段落。 +5. **篇幅上限(软约束)**:单次同步中,对**同一文件**的新增正文合计不宜超过约 **80 行**(不含代码块行);超出则拆分为新 topic、或先写「摘要 + 详见代码路径/另一文档」,禁止单文件堆叠重复说明。 +6. **`index.md`**:只改与本次主题相关的行/表项,禁止整表或整节复制粘贴式刷新。 +7. **禁止**:重复解释 Flow2Spec 目录分工、重复贴用户对话全文、与本次 diff 无关的「历史回顾」大段。 + +## 完成后自检 + +1. 能力描述与代码实现是否一致。 +2. 新增能力是否可通过 topic 被检索。 +3. `index` 与 `manifest` 是否同步更新。 +4. 若写入 `topicMetadata`:key 是否存在于 `topicPaths`;`primary` / `tags` / `confidence` 是否合法;是否未因分类创建、重命名或拆分 topic。 +5. 知识库变更是否可再压缩:删掉与本次变更无关的套话后,规则与链接是否仍完整。 +6. 是否仍存在「否定旧版 / 不再与某物有关」类赘句:若现行规则已写清,此类句应删或并入用户要求的迁移小节。 +7. 子 agent 未整文件重写文档;manifest / index 由主 agent 单点落盘。 +8. 若 `changeTracking.feat: true`:`task.md`「步骤」已全部 `[x]`(或备注已记录取消项)后,才将 `.task/active//` 归档至 `completed/` 并从 `todo.json` 删除对应条目;禁止在仍有 `[ ]` 时移动目录(与 `f2s-task` 归档门禁一致)。 +9. 若 `changeTracking.feat: true`:`user-todos.md` 已存在;有用户代办时内容已与会话结论一致。 diff --git a/packages/core/templates/zh-CN/skills/f2s-kb-fix/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-kb-fix/SKILL.md new file mode 100644 index 0000000..dbed698 --- /dev/null +++ b/packages/core/templates/zh-CN/skills/f2s-kb-fix/SKILL.md @@ -0,0 +1,112 @@ +--- +name: f2s-kb-fix +description: 根据用户指出的实现或规则错误修正代码,并默认同步知识库;触发:f2s-kb-fix、修正实现规则 +--- + +> **任务路径**:凡 `.task/` 落盘与续作,**必须以 `rules/f2s-task` 解析的 `TASK_ROOT` 为准(`.task` 或 `.task/`;config → git → legacy)。下文若仍出现 `.task/todo.json` / `.task/active/`,均视为 **`TASK_ROOT/...` 的简写**。 + + +> 执行口径:`f2s-kb-fix` 默认"修代码 + 同步 `.Knowledge`",无需用户额外要求"请同步知识库"。 + +## KB 自动合并协议(必须) + +本技能不得把“人工执行命令”作为用户流程。修复完成后,由 agent 自己完成知识候选生成、合并、构建与校验: + +1. 将本次修复后的正确规则/实现边界转换为 `kb-delta` 草稿,记录 `taskId`、`developerId`、`baseRevisions`、`changes` 与修复证据;若 `changeTracking.fix=true` 且已有任务目录,可把 delta 落在当前 `TASK_ROOT/active//kb-delta.json`,否则可在内存中形成等价对象。`changes` 可使用 `appendBody` / `replaceBody` / `updateFrontmatter`;确需新主题时使用 `createTopic`,并可携带 `taskRule` 与 `matcher` 让路由一并接入。 +2. 写入 `.Knowledge` 前,必须用 `flow2spec kb plan ` 或等价内部能力预演;若 topic revision 不一致,停止自动写入,转入语义合并说明。 +3. 可自动合并时,由 agent 调用 `flow2spec kb apply ` 或等价内部能力写入 topic,并随后执行 `flow2spec kb build` 与 `flow2spec kb check`。 +4. 用户只看到“修复与知识库已同步 / 有语义冲突需确认 / 已跳过入库及原因”,不要求用户手动执行 `kb plan/apply/build/check`。 + +## 编排(主 / 子 agent) + +- 两字段(`subAgent` / `switchAgentVerification`)语义以统一入口为唯一事实源:**Cursor/Claude** 读配置根 `rules/f2s-flow2spec-unified-entry.*`;**Codex** 读 `.codex/topics/f2s-flow2spec-unified-entry.md`(与上同源,`flow2spec init` 镜像)。本处不复述。 +- 代码子包(bug 修复类实现代码):`subAgent=true` 时可外包给子 agent 执行。 +- 文档子包(rules / skills / topics / stock-docs 等文风类改动):默认不拆,由主 agent 直接编写,以保证「现行真值覆盖 / 篇幅上限 / 禁历史否定堆砌」等文风合规。 +- 若确需外包文档改动:子侧**只输出「原位替换 diff」**(before / after 小段),**不得整文件重写**;由主 agent 合并落盘。 +- 写权硬约束:`manifest-routing.json` / `.Knowledge/index.md` 恒由主 agent 落盘,子 agent 不得触碰。 +- 落盘侧自验。 + +# /修正能力(f2s-kb-fix) + +## 输入 + +- 用户描述违规点、正确写法、可选范围。 + +## 步骤 + +**步骤 0:变更追踪(仅当 `changeTracking.fix: true`)** + +执行前读取 `flow2spec.config.json`,若 `changeTracking.fix: true`: + +- 检查 `.task/todo.json` 是否存在活跃任务,将用户描述与 `keywords` 匹配。 +- 命中 → 加载对应 `task.md`,展示剩余清单,在已有任务中继续。 +- 无命中 → 创建新任务(见 `f2s-task` 规则),将步骤 1–4 写入 `task.md` 作为任务 checklist。 +- **执中必写盘**:每完成 `task.md` 中一步,**同一会话内**立即 `Edit` 将该步 `[ ]`→`[x]`;禁止积压打钩或口头完成代替写盘(见 `f2s-task`「中断与会话结束」「归档门禁」)。 +- **用户代办**:凡须用户改库、配环境、回归验证等项,**同会话内**追加写入 `.task/active//user-todos.md`(见 `f2s-task`);新建任务时若无代办可写占位说明。 + +1. 明确违规点与影响范围(不清先追问)。 +2. 修复代码实现。 +3. 同步知识库(默认执行): + - `.Knowledge/stock-docs/`:修订约定说明 + - `.Knowledge/topics/`:修订对应主题规则/流程 + - `.Knowledge/index.md`:更新主题索引 + - 路由清单:若路由、依赖或 `topicMetadata` 受影响则最小更新 + - **创作侧准则**:本步若新增 / 修改 topic、`topicMetadata` 或 `topicDependencies`,须先 Read `rules/f2s-topic-authoring.*` 全文(**Cursor/Claude**:`rules/f2s-topic-authoring.mdc`;**Codex**:`.codex/topics/f2s-topic-authoring.md`),再落盘。 +4. 输出摘要(代码改动 + 知识库改动)。 + +## 输出摘要格式(建议) + +```markdown +## 修正结果:<约定简述> + +### 代码 +- <文件路径>:<改动说明> + +### 知识库 +- .Knowledge/stock-docs/<文件>.md:<新增/修订说明> +- .Knowledge/topics/.md:<新增/修订说明> +- .Knowledge/index.md:<更新说明> +- .Knowledge/manifest-routing.json:<是否更新与原因> +- .Knowledge/matchers/.json:<是否更新与原因> +``` + +## 复杂场景示例 + +用户指出「某回调接口幂等实现错误」,但未给明确文件范围。 + +- 先按最小可行范围修复已定位的回调处理链路,并在摘要中说明"可继续扩展全仓同类修复"。 +- 同步更新 `topics` 中幂等规则段落,避免后续再次生成错误实现。 +- 若该修复影响任务路由(例如新增"幂等修复"主题),再最小更新 `manifest`。 + +## 约束 + +- 与旧约定冲突时:**改写到当前真值**,不要叠写「(不再与某 X 有关)」等对照旧版的赘句。 +- 同主题优先原位更新。 +- 范围不明时按最小可行范围修复并说明。 +- 不改配置根 `rules/skills`。 +- 文档子包默认不拆;必要外包子侧仅出 before/after diff 片段,主合并落盘;`manifest-routing.json` / `.Knowledge/index.md` 恒主落盘(写权硬约束)。 + +## 知识库落盘文风(必须,防赘述) + +写 `stock-docs` / `topics` / `index` 时遵守: + +1. **增量最小**:只改与**本次修复**直接相关的段落或列表项;禁止借机重述整份方案、整段历史背景或与修复无关的说明。 +2. **肯定式优先(见统一入口「知识库落盘文风」)**:直接写出正确描述,禁止用否定旧版来传达新约定;排他性选择除外。 +3. **不重复叙事**:`stock-docs` 与 `topics` 不就同一修复各写长篇;一处写清「错因 / 正确约定 / 注意点」,另一处简短引用或链到该段。 +4. **条文化优先**:以「错误表现 → 根因 → 正确行为 / 边界」为序的短列表为主,避免散文式展开。 +5. **篇幅上限(软约束)**:单次同步中,对**同一文件**的新增或替换正文合计不宜超过约 **60 行**(不含代码块行);超出则只保留与修复相关的最小说明,其余用「见提交/见某路径」代替。 +6. **`index.md`**:仅更新受影响的索引行或摘要列,禁止无关整表重写。 +7. **禁止**:重复粘贴用户报错全文(可摘一行标识 + 链接)、重复解释 Flow2Spec 用法。 + +## 完成后自检 + +1. 代码修复是否覆盖用户点名范围。 +2. 主题文档是否与修复后的实现一致。 +3. `index` 是否指向正确主题。 +4. 若更新了 `manifest`,路由字段是否仍可解析。 +5. 若写入 `topicMetadata`:key 是否存在于 `topicPaths`;`primary` / `tags` / `confidence` 是否合法;是否未因分类创建、重命名或拆分 topic。 +6. 知识库变更是否可再压缩:删套话后约定是否仍清晰。 +7. 是否仍存在「否定旧版 / 不再与某物有关」类赘句:现行规则已写清则应删。 +8. 子 agent 未整文件重写文档;manifest / index 由主 agent 单点落盘。 +9. 若 `changeTracking.fix: true`:`task.md`「步骤」已全部 `[x]`(或备注已记录取消项)后,才归档至 `completed/` 并从 `todo.json` 删除对应条目;禁止在仍有 `[ ]` 时移动目录(与 `f2s-task` 归档门禁一致)。 +10. 若 `changeTracking.fix: true`:`user-todos.md` 已存在;有用户代办时内容已与会话结论一致。 diff --git a/packages/core/templates/zh-CN/skills/f2s-kb-merge/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-kb-merge/SKILL.md new file mode 100644 index 0000000..0cc79d5 --- /dev/null +++ b/packages/core/templates/zh-CN/skills/f2s-kb-merge/SKILL.md @@ -0,0 +1,80 @@ +--- +name: f2s-kb-merge +description: 解决 Git 合并后编辑器上下文冲突;可选传入冲突文件;实现侧冲突仅罗列待用户确认;触发:合并上下文冲突、f2s-kb-merge +--- + +## 编排(主 / 子 agent) + +- 两字段(`subAgent` / `switchAgentVerification`)语义以统一入口为唯一事实源:**Cursor/Claude** 读配置根 `rules/f2s-flow2spec-unified-entry.*`;**Codex** 读 `.codex/topics/f2s-flow2spec-unified-entry.md`(与上同源,`flow2spec init` 镜像)。本技能不复述。 +- **子 agent 职责**(仅当 `subAgent=true`):只做**冲突扫描 + 按类别对照表**,每条包含五字段 —— `file` / `category`(文档索引 / 总览规则 / 模块规则 / 技能 / 说明文档 / 实现类 / 依赖元数据)/ `ours_summary` / `theirs_summary` / `recommendation`(并集 / 保留某侧 / 并入必须项 / 待用户选)。 +- **子 agent 不出成品合并稿**,避免主 agent 二次重写。 +- **主 agent 职责**:按策略落盘 + 实现类决策 + 验收。 +- 默认落盘侧自验,本技能不绑定交叉校验。 + +# /合并上下文冲突(f2s-kb-merge) + +在 **rebase / merge** 后出现 `<<<<<<<` / `=======` / `>>>>>>>` 时,优先**自动合并「AI 与开发者上下文」相关文件**,保证索引、规则、技能与说明文档互相对齐;**涉及可执行实现、部署或依赖声明的冲突不擅自合并**,需**向用户展示双方差异并等待确认**后再改。 + +## 传参(可选) + +- **不传参**:在工作区内**自行检索**仍存在冲突标记的文件,再按本技能分类与策略处理(含全量扫描后的摘要)。 +- **传参**:用户可指定**一个或多个仍含冲突的文件**(随消息 @ 文件或列出路径均可)。助手**优先只处理这些文件**中的冲突;若其中含「禁止自动合并」类别,仍只罗列差异与建议,**不擅自写入**。指定文件处理完毕后,可询问用户是否需要对工作区做**补充扫描**。 + +## 适用范围(可自动合并) + +以下**类别**内的冲突,按本技能**合并策略**处理,**无需**逐行征求确认(除非两侧表述**互斥**且无法判断应以何为准): + +| 类别 | 说明 | +| -------------------- | -------------------------------------------------------------------- | +| 文档索引 | 承载「文档 ↔ 规则 / 技能」映射的索引表文件 | +| 项目总览规则 | 规则目录中的总入口文件 | +| 模块规则 | 同套规则目录下的其余规则片段 | +| 技能 | 技能目录下的 SKILL 说明文件 | +| 上下文说明文档 | 与规则、技能配套的说明类 Markdown | +| 索引联动的纯说明文档 | 由项目约定存放、仅被索引或规则引用、**不含可执行实现语义**的说明文档 | + +## 禁止自动合并(须用户确认) + +以下冲突**不得**在未获用户明确选择前合并: + +- **应用或服务实现源码**(业务逻辑、接口实现、数据访问等) +- **会改变对外暴露行为**的配置(路由、函数注册、中间件链、运行入口等) +- **依赖与构建元数据**(依赖声明、锁文件、构建与部署脚本等) +- **集中维护外部资源清单的实现模块**:若两侧**条目集合或注册内容不同**,属运行行为差异,须用户确认保留范围(助手可建议「并集 + 去重」,**待用户同意**后再写入) + +**处理方式**:列出冲突文件、简述两侧意图,给出推荐方案,**请用户选定**后再改上述范围中的文件。 + +## 合并策略(上下文类) + +1. **删除所有** Git 冲突标记(`<<<<<<<` / `=======` / `>>>>>>>`),不得残留。 +2. **索引表** + - 同一索引行的 **Rules / Skills / 链接列**:做**并集**,路径去重、空格分隔。 + - 仅在一侧出现的**独立索引行**:合并后**保留**,避免丢失条目。 +3. **总览规则** + - 同一主题下多条 bullet:合并为**信息完整的单条或并列多条**,**不丢弃**任一侧独有的约束或引用。 +4. **长文档中的表格** + - 描述**不同维度能力**的行:**并集保留**。 + - 描述**同一主题**的重复行:合并为**一条**连贯表述,涵盖两侧要点。 +5. **rules / skills** + - 优先保留**更具体、约束更清晰**的表述;另一侧独有的**必须 / 禁止**条款**并入**,避免规则回退。 +6. **链接与路径** + - 统一为仓库内可解析的相对路径,并与总览规则中的索引入口一致。 + +## 执行步骤 + +1. **确定范围**:若用户已指定冲突文件,仅以这些文件为范围;否则全工作区检索冲突标记(或结合 IDE 冲突列表)。再按**适用范围**分类。 + - 若启用拆子,子 agent 按子交付对照表 schema(`file` / `category` / `ours_summary` / `theirs_summary` / `recommendation` 五字段)产出分类表;主 agent 接手后续落盘 / 决策 / 验收步骤。 +2. **上下文类**:按合并策略直接修改并保存。 +3. **实现类**:只输出对比摘要与建议,**不修改文件**直至用户确认。 +4. **输出摘要**(Markdown):已解决文件 + 要点;待确认文件 + 两侧差异 + 建议。 +5. 对**已处理文件**再次确认**无**冲突标记残留;若未做全量扫描,可提示用户是否补充扫描。 + +## 与相关命令的关系 + +- **`/修正实现规则`(f2s-kb-fix)**:用户已指明问题点后的**定向修正**与文档/规则同步。 +- **本技能**:合并产生的**批量冲突**,侧重**编辑器上下文与说明文档**同**实现侧**分离处理。 + +## 何时使用 + +- merge / rebase 后,**规则 / 技能 / 索引 / 配套说明文档**出现冲突(可全量处理,也可只处理用户指定的冲突文件)。 +- 需要一次性对齐「索引 ↔ 规则 ↔ 技能 ↔ 说明文档」,且**避免误合并实现或部署相关改动**时。 diff --git a/packages/core/templates/zh-CN/skills/f2s-kb-migrate/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-kb-migrate/SKILL.md new file mode 100644 index 0000000..72ba035 --- /dev/null +++ b/packages/core/templates/zh-CN/skills/f2s-kb-migrate/SKILL.md @@ -0,0 +1,358 @@ +--- +name: f2s-kb-migrate +description: 旧版知识库一次性迁到 `.Knowledge`:以配置根 `docs-index.md` + 规则统一入口(旧版 `rules/main.md(c)` 或新版包 `rules/f2s-flow2spec-unified-entry.md(c)`)为主索引线索,全量处理业务 `rules/` 与业务 `skills/`(排除 `f2s-*` 包技能),并全量迁移 `stock-docs`/`req-docs`;**迁移验收后必选**落盘 `.Knowledge/migration-report.md`(迁移对照表 + 拟删除路径列表);**收尾必选**删除已迁旧的 `rules/`、已迁业务 `skills/`、旧版 `docs-index.md`/`index-doc.md`;用户只**核对/修订删除清单(排除项)**;触发:f2s-kb-migrate、知识库迁移、旧版迁移 +--- + +> 执行口径:这是 `f2s-*` 技能流程,不是 CLI 子命令。迁移目标包含: +> 1) 结构层:`.Knowledge/topics`、`.Knowledge/index.md`、`.Knowledge/manifest-routing.json`、`.Knowledge/matchers/*.json` +> 2) 文档层:`.Knowledge/stock-docs`、`.Knowledge/req-docs` +> +> **硬边界**:`skills/f2s-*`(各 agent 配置根下)属于 Flow2Spec 包技能/执行层能力,**不得**写入 `.Knowledge`(含 `topics/stock-docs/req-docs`),也不得作为“业务技能迁移”的源;**不得**在本流程中删除(版本对齐走 `flow2spec init` / 包升级)。 +> +> **基线规则保留清单(不得删除)**:`rules/f2s-flow2spec-unified-entry.md(c)`、`rules/f2s-implement-tech-design.md(c)`、`rules/f2s-stock-docs-vs-req-docs.md(c)`。 + +## 编排(主 / 子 agent) + +- 两字段(`subAgent` / `switchAgentVerification`)语义以统一入口为唯一事实源:**Cursor/Claude** 读配置根 `rules/f2s-flow2spec-unified-entry.*`;**Codex** 读 `.codex/topics/f2s-flow2spec-unified-entry.md`(与上同源,`flow2spec init` 镜像)。本节不复述。 +- **子 agent 职责**(仅当 `subAgent=true`):在主给定清单下做搬运工作、生成 `migration-report.md` 的**草案片段**;产出一律以 patch 形式提交,由主 agent 合并落盘。 +- **主必控**: + - `.Knowledge/.migrate-state.json` **写权归主**(状态机事实源,主 / 子抢写会致队列错位); + - `migration-report.md` 的 **「删除执行记录」** 小节恒由主 agent 追加; + - **删除清单确认**与闭环收尾必主完成。 +- **写权硬约束**:`manifest-routing.json` / `.Knowledge/index.md` / `.Knowledge/.migrate-state.json` / 迁移报告「删除执行记录」均恒由主 agent 落盘。 +- 默认落盘侧自验;本 SKILL 不绑定交叉校验。 + +# f2s-kb-migrate(旧版知识库 -> 新版知识库) + +## 与 `f2s-kb-upgrade` 为何并存 + +| 技能 | 解决的问题 | +| --- | --- | +| **本技能 `f2s-kb-migrate`** | **一次性结构搬家**:旧索引(`docs-index.md` / `index-doc.md`)、`rules/main.md(c)`、业务 `skills/`、散落 `stock-docs`/`req-docs` → **`.Knowledge`**,并处理删除清单与 `migration-report.md`。 | +| **`f2s-kb-upgrade`** | **知识库模板升级技能(唯一「升级」口径)**:按 **`skills/f2s-kb-upgrade/SKILL.md`** 全文执行;其中代跑 **`flow2spec init`** 以对齐 **`manifest-routing` + `matchers/`** 与各 agent **`rules`/`skills`**;含 **V1 / 现行库(V2+)** 分流(旧项目须 **migrate 后再跑本技能**;**V2+ 含 npm v3.x 等已上 `.Knowledge` 的项目**,见 `f2s-kb-upgrade` 步骤 0)。 | + +- **迁移验收、删除清单确认完成后**:应提醒或代用户执行 **`f2s-kb-upgrade` 技能全文**(其中 **步骤 2** 会代跑 **`flow2spec init`**),把 Flow2Spec 包版本、路由分片与配置根产物对齐到当前包。**勿**让用户以为「单独执行 `init`」即完成知识库模板升级。 +- **已在稳定使用 `.Knowledge` 且无旧索引负担的项目**:不要重复跑本技能;日常包/模板对齐走 **`f2s-kb-upgrade`** 技能即可(不是只跑 `init`)。 + +**为何各 agent 下都有同名 `SKILL.md`?** 各工具只读各自配置根下的 `skills/`;`flow2spec init` 会向所选 agent **同步**当前语言对应的技能内容。 + +## 本命令做什么(对外口径) + +把旧版“散落在配置根的文档索引 + 规则 + 业务技能 + stock/req 文档树”,**整体搬迁并改写到新版 `.Knowledge`**,完成后再做**旧版入口与旧版业务产物清理**,实现与旧版知识库组织方式的切割。 + +必须覆盖的对象: + +1. **索引入口**:配置根 `docs-index.md`(兼容 `index-doc.md`)中声明/映射到的业务文档与规则线索。 +2. **规则入口**:`rules/main.md` / `rules/main.mdc`(旧版常见)或 `rules/f2s-flow2spec-unified-entry.md` / `rules/f2s-flow2spec-unified-entry.mdc`(兼容历史命名 `rules/flow2spec-unified-entry.md(c)`)中声明/引用的规则集合(以及 `rules/` 下其它业务规则文件)。 +3. **业务技能**:各 agent 配置根 `skills/` 下除 `f2s-*` 以外的业务技能目录(全量盘点)。 +4. **文档树**:旧版 `stock-docs/`、`req-docs/`(或同义目录)**全量**迁入 `.Knowledge` 对应目录。 + +对“索引未覆盖”的对象: + +- 先输出候选清单(路径 + 推断理由:命名/目录/引用关系)。 +- **默认必须让用户确认**是否纳入迁移;仅当证据非常充分(例如被 `rules/main` / `f2s-flow2spec-unified-entry` 显式引用、或被已索引文档明确引用)才允许 Agent 自行判定纳入,并在迁移摘要中写明判定依据。 + +迁移完成后的清理(**必选收尾**;且迁移结果无失败、无待确认项;**`skills/f2s-*` 永不删除**): + +- **必须执行**:删除旧版 **`rules/` 中已迁移业务规则文件**(含 `main.md(c)` 若仅作为旧入口),但**不得删除**基线规则保留清单中的 3 个 `f2s-*` 根规则文件。 +- **必须执行**:删除旧版 **业务** `skills/` 下**已迁移**的子目录(**排除** `f2s-*`;若某目录下仍有未迁完项则不得删该目录,须先补齐或从清单剔除)。 +- **必须执行**:删除旧版入口 **`docs-index.md`**(兼容 **`index-doc.md`**),避免与 `.Knowledge/index.md` 双入口并存。 +- **默认一并列入删除子清单**(用户可在清单中排除):旧版 **`stock-docs/`**、**`req-docs/`** 源目录(仅当对应文档层迁移验收通过、无失败/无待确认项时执行实际删除)。 + +**用户确认的含义(重要)**: + +- **不是**询问「要不要做清理」;清理是流程的一部分。 +- **而是**输出**默认全选的「删除路径清单」**(规则文件逐条、业务 skill 目录逐条、索引文件名、以及可选的旧文档根目录),请用户**核对**;用户只能: + - 回复「**确认清单**」按当前清单执行删除;或 + - 回复「**排除:<路径…>**」从清单中移除指定项后再执行(移除项须写入 `.migrate-state.json` 的 `notes[]` 并说明原因)。 +- 若用户要求**暂缓删除某路径**,须在清单中保留该项并结束本轮清理(状态文件 `status=paused`),**不得**假装已完成迁移闭环。 + +## 适用场景 + +- 项目仍在使用旧版知识组织(`docs-index.md` / `index-doc.md` + `rules/main.md(c)` 或 `rules/f2s-flow2spec-unified-entry.md(c)`(兼容旧 `flow2spec-unified-entry.md(c)`)+ 业务 `skills/` + 散落 `stock-docs`/`req-docs`)。 +- 希望迁移到新版 `.Knowledge`,并且按主题逐个确认,避免一次性大改。 +- 需要 **req-docs / stock-docs 全量** 迁入 `.Knowledge`,并与旧版知识库目录/表述做切割(路径、索引、主题文案统一到新架构口径)。 + +## 输入 + +- 可选输入: + - 旧版规则统一入口路径:`rules/main.md` / `rules/main.mdc` 和/或 `rules/f2s-flow2spec-unified-entry.md` / `rules/f2s-flow2spec-unified-entry.mdc`(兼容旧 `rules/flow2spec-unified-entry.md(c)`) + - 旧版 `index-doc.md`(或 `docs-index.md`)路径 + - 旧版存量文档目录(如 `stock-docs/`、`docs/stock/`) + - 旧版需求文档目录(如 `req-docs/`、`docs/req/`) + - 迁移范围(全部主题 / 指定主题) +- 不提供时,先在仓库中定位上述文件并向用户确认。 + +## 断点续迁状态文件(必须启用) + +- 状态文件路径:`.Knowledge/.migrate-state.json` +- 作用:记录迁移进度,支持会话中断后恢复,不重复迁移已完成项。 +- 初始化时机:用户确认“开始迁移”后立即创建。 +- 结束时机: + - 全部迁移完成且用户确认结束:删除状态文件。 + - 用户主动“停止”:保留状态文件,等待下次恢复。 +- `.migrate-state.json` 只由主 agent 写;子 agent 以 patch 片段提交由主合并(写权硬约束)。 + +建议字段(最小集): + +```json +{ + "version": "1", + "status": "running", + "currentStage": "inventory|orphans|topics|stock-docs|req-docs|cleanup", + "topicQueue": [], + "topicDone": [], + "bizRuleQueue": [], + "bizRuleDone": [], + "bizSkillQueue": [], + "bizSkillDone": [], + "stockQueue": [], + "stockDone": [], + "reqQueue": [], + "reqDone": [], + "pendingManual": [], + "failed": [], + "notes": [], + "updatedAt": "ISO-8601" +} +``` + +更新规则(必须执行): + +1. 每完成 1 个主题、1 个业务技能目录、1 个业务规则文件或 1 个文档文件后,立即落盘更新状态文件。 +2. 收到“重试 ”时,先回滚该项状态,再执行重试。 +3. 收到“继续”时,优先读取状态文件,从未完成队列继续。 +4. 收到“停止”时,写入 `status=paused` 并结束本轮。 +5. 收到恢复请求时,先展示状态摘要(当前阶段、剩余数量、失败/待确认项)并等待用户确认继续。 + +## 强制流程(分阶段执行) + +### 步骤 1:读取旧版映射 + +1. 读取 `docs-index.md`(兼容 `index-doc.md`),提取“业务文档 -> 规则/主题”映射(**主索引**)。 +2. 读取 **`rules/main.md`(兼容 `main.mdc`)** 或 **`rules/f2s-flow2spec-unified-entry.md`(兼容 `f2s-flow2spec-unified-entry.mdc`;兼容旧 `flow2spec-unified-entry.md(c)`)**(二者通常只存在其一),提取模块/主题目录线索(**与索引交叉校验**)。 +3. **全量盘点业务规则文件**:扫描 `rules/` 下除以下文件外的业务规则文件,建立 `bizRuleQueue`(去重): + - 统一入口:`main.md(c)`、`f2s-flow2spec-unified-entry.md(c)`、`flow2spec-unified-entry.md(c)`(兼容旧命名) + - 基线保留:`f2s-implement-tech-design.md(c)`、`f2s-stock-docs-vs-req-docs.md(c)` +4. **全量盘点业务技能**:扫描各 agent 配置根 `skills/` 目录,**排除** `f2s-*`,其余目录一律进入 `bizSkillQueue`(去重)。 +5. 扫描旧版 `stock-docs` 与 `req-docs` 候选来源目录(若存在)。 +6. 生成待迁移清单并展示给用户确认: + - 主题清单(去重、排序) + - 业务规则文件清单(`bizRuleQueue`) + - 业务技能目录清单(`bizSkillQueue`) + - `stock-docs` 文件清单 + - `req-docs` 文件清单 +7. 文档分类口径(必须明确): + - 来源路径命中 `stock-docs`(含同义目录如 `docs/stock`) -> 迁移到 `.Knowledge/stock-docs` + - 来源路径命中 `req-docs`(含同义目录如 `docs/req`) -> 迁移到 `.Knowledge/req-docs` + - 无法判定的文件 -> 列入“待人工确认清单”,未确认前不迁移 +8. 计算“索引外候选”(`orphans`): + - `bizRuleQueue` 中未被 `docs-index` / 统一入口(`rules/main` 或 `f2s-flow2spec-unified-entry`)覆盖的文件 + - `bizSkillQueue` 中未被索引映射覆盖的目录 + - 对每一项默认要求用户确认是否迁移;仅在高置信引用场景允许 Agent 自判纳入,并将依据追加写入状态文件 `notes[]`(不得破坏 JSON 可解析性)。 +9. 用户确认清单后,初始化状态文件并写入队列(inventory/orphans/topics/stock/req)。 + +### 步骤 2:逐主题迁移(结构层核心) + +对每个主题按以下顺序执行: + +1. 汇总该主题旧资料: + - 相关 `rules/*.md(c)`(业务规则) + - 相关 **业务** `skills/<非 f2s-*>`(将其内容合并进主题叙述/流程,不复制为 `.Knowledge` 下的技能文件) + - 索引映射中的**业务文档**路径 + - **不得**包含 `skills/f2s-*` 下任何文件 +2. 生成或更新 `.Knowledge/topics/.md`: + - 正文表述统一为新架构口径(`.Knowledge` 分层、`manifest` 路由、`stock-docs`/`req-docs` 分工)。 + - 去除旧版独有路径/术语(如旧 `docs-index` 根路径、旧散落目录名),改为指向 `.Knowledge/...` 或相对 `.Knowledge` 的稳定路径。 + - **创作侧准则**:本步生成 / 重写 topic 或调整 `topicMetadata` / `topicDependencies`,须先 Read `rules/f2s-topic-authoring.*` 全文(**Cursor/Claude**:`rules/f2s-topic-authoring.mdc`;**Codex**:`.codex/topics/f2s-topic-authoring.md`),再落盘。 +3. 更新 `.Knowledge/index.md` 的主题索引行,并同步维护“关联文档(摘要)”列(每主题 1-3 条关键 `stock-docs/req-docs` **可点击 Markdown 链接**,格式:`[标题](相对路径)`)。 +4. 按需更新路由清单: + - `.Knowledge/manifest-routing.json`:`topicPaths`、`taskToTopicRules[]`、`topicDependencies`、`topicMetadata`、`fallbackTopic` + - `.Knowledge/matchers/.json`:`includeAny`(与 `manifest-routing.taskToTopicRules[].matcherPath` 一致) +5. 输出本主题迁移摘要并**暂停**,提示用户: + - 回复“继续”迁移下一个主题 + - 或回复“停止”终止本轮 + - 或回复“重试 ”重做当前主题 + +> 未收到“继续”前,不得迁移下一个主题。 +> 每完成一个主题,必须先更新状态文件再进入等待。 + +### 步骤 3:迁移 `stock-docs`(文档层) + +当步骤 2 全部完成后,执行: + +1. 按“来源目录相对路径”迁移到 `.Knowledge/stock-docs/`,不做平铺。 +2. 默认场景视为在旧版仓库首次迁移到新版知识库,目标路径按“不存在”执行。 +3. 每迁移 1 个文件输出一次结果并暂停,等待“继续 / 停止 / 重试 <文件>”。 +4. 全部完成后输出 `stock-docs` 子摘要(成功/失败/待确认)。 + +> 未收到“继续”前,不得迁移下一个文件。 +> 每完成一个文件,必须先更新状态文件再进入等待。 + +### 步骤 4:迁移 `req-docs`(文档层) + +当 `stock-docs` 阶段完成后,执行: + +1. 按“来源目录相对路径”迁移到 `.Knowledge/req-docs/`,不做平铺。 +2. 默认场景视为在旧版仓库首次迁移到新版知识库,目标路径按“不存在”执行。 +3. 每迁移 1 个文件输出一次结果并暂停,等待“继续 / 停止 / 重试 <文件>”。 +4. 全部完成后输出 `req-docs` 子摘要(成功/失败/待确认)。 + +> 未收到“继续”前,不得迁移下一个文件。 +> 每完成一个文件,必须先更新状态文件再进入等待。 + +### 步骤 5:全部迁移完成后的收尾(必选:迁移报告落盘 + 删除清单确认) + +当主题(步骤 2)与文档层 `stock-docs` / `req-docs`(步骤 3–4)**全部验收通过**(无失败、无阻塞性待确认项,或已在报告中单列)后,按顺序执行以下子步骤。 + +#### 5.0 迁移报告(必选:写入项目 Markdown) + +1. **必须**在项目仓库中创建或覆盖文件:**`.Knowledge/migration-report.md`**(相对项目根;与 `.Knowledge` 同库,便于评审与留痕)。 +2. 报告正文须至少包含两大块(可用表格或分级列表,路径一律用**相对项目根**的 POSIX 风格): + - **「迁移对照表」**: + - **主题**:每个已迁移 `topic` → 旧侧来源(对应 `rules/*.md(c)`、业务 `skills/`、`docs-index` 映射行摘要)→ 新路径 `.Knowledge/topics/.md`;并注明本次是否改写了 `.Knowledge/index.md` / 路由清单相关字段。 + - **`stock-docs`**:每条 **源路径 → `.Knowledge/stock-docs/...` 目标路径**(含跳过的文件及原因,若无则写「无」)。 + - **`req-docs`**:同上。 + - **「拟删除路径清单」**:与下文步骤 5.2 中向用户展示的**默认全选删除清单**逐项一致(`rules/` 下每个文件、业务 `skills/` 下每个待删目录、`docs-index`/`index-doc`、以及可选列入的旧 `stock-docs/`/`req-docs/` 根目录);每条建议用 `- [ ] <路径>`,便于人类勾选核对。 +3. 若用户随后在步骤 5.2 中发出 **「排除:<路径…>」**,须在**执行物理删除前**更新同一文件:追加或在「用户排除项」小节中写明排除路径与原因,并同步更新「拟删除路径清单」勾选状态或列表,使**磁盘上的报告与最终删除集合一致**。 +4. 在步骤 5.2 第 3 步按最终清单**执行完物理删除后**,须在**同一文件末尾**追加小节 **`## 删除执行记录`**(含执行时间、实际已删路径列表;未删项注明原因与 `status=paused` 等),不得仅留在对话里。 +5. 迁移报告的「删除执行记录」小节恒由主 agent 追加,子 agent 不得直接写入(写权硬约束)。 + +> **禁止**:未完成 `.Knowledge/migration-report.md` 落盘即进入物理删除或结束本轮迁移闭环。 + +#### 5.1 总摘要(对话内,可与报告摘要一致) + +- 已迁移主题列表 +- 新增/更新的 `.Knowledge` 文件 +- 已迁移 `stock-docs` 文件 +- 已迁移 `req-docs` 文件 +- 未迁移或失败项 + +#### 5.2 必选清理阶段(删除清单确认,不得跳过) + +1. 输出**默认全选**的「**删除路径清单**」(须与 `migration-report.md` 中「拟删除路径清单」同源),至少包含: + - 旧版 **`rules/`** 下每个将删除的**业务规则**文件路径(可含 `main.md(c)`;**不含**基线保留清单中的 `f2s-*` 根规则) + - 旧版 **业务** `skills/` 下每个将删除的子目录路径(**不含** `f2s-*`) + - 旧版 **`docs-index.md` / `index-doc.md`** + - (可选子清单)旧版 **`stock-docs/`**、**`req-docs/`** 根目录:仅当文档迁移验收通过且无待确认项时列入;用户可排除。 +2. 等待用户回复 **「确认清单」** 或 **「排除:<路径…>」** 更新清单;**禁止**使用「是否执行清理」类二选一提问。 +3. 按**最终清单**执行删除;**不得**删除清单外的路径;**不得**删除 **`skills/f2s-*`**。 +4. 收尾完成后处理状态文件: + - 本轮完整完成:删除 `.Knowledge/.migrate-state.json` + - 本轮暂停/中止:保留 `.Knowledge/.migrate-state.json`(`status=paused`),并记录未删路径与原因 + +## 输出摘要格式(建议) + +```markdown +## 主题迁移完成: + +### 来源 +- rules: <旧路径...> +- 业务文档: <索引映射中的文档路径...> +- 映射: + +### 已写入 +- .Knowledge/topics/.md +- .Knowledge/index.md(更新 行) +- .Knowledge/manifest-routing.json(更新字段:...) +- .Knowledge/matchers/.json(更新 `includeAny` 等:...) + +### 下一步 +- 回复“继续”迁移下一个主题 +- 回复“停止”结束迁移 +``` + +```markdown +## 文档迁移完成:/ + +### 来源 +- source: <旧路径...> + +### 已写入 +- .Knowledge// + +### 下一步 +- 回复“继续”迁移下一个文件 +- 回复“停止”结束迁移 +``` + +## 约束 + +- 必须逐主题确认,不可批量跳过确认直接全量迁移。 +- `stock-docs` / `req-docs` 必须逐文件确认,不可无确认批量迁移。 +- 文档迁移必须保留来源目录相对路径,不可平铺为单层文件名。 +- **`f2s-*` 技能不得进入 `.Knowledge`,不得在主题迁移中合并进 `topics`。** +- **业务** `skills/`(非 `f2s-*`)必须纳入全量盘点;索引未覆盖项默认必须用户确认后才可迁移。 +- 未完成全部主题前,禁止删除旧业务 `rules/` 与**非 `f2s-*`** 的旧业务 `skills/`;基线保留清单中的 `f2s-*` 根规则文件始终不得删除。 +- 未完成文档迁移前,禁止删除旧文档目录。 +- 删除旧目录前必须完成「**删除路径清单**」核对(允许排除项),**禁止**用「是否清理」替代清单确认。 +- 迁移过程只改 `.Knowledge` 与(**最终删除清单**确认后)对清单内旧路径的删除,不改业务代码。 +- 必须维护 `.Knowledge/.migrate-state.json`,禁止只在内存中维护迁移进度。 +- 主题与文档层迁移验收通过后,**必须先**写入 `.Knowledge/migration-report.md`(含迁移对照表与拟删除路径清单),再进入物理删除;报告与对话内删除清单须同源可追溯。 +- `.migrate-state.json` / `migration-report.md` 的删除执行记录 / `manifest-routing.json` / `.Knowledge/index.md` 均恒主落盘。 + +## 迁移报告模板(落盘 `migration-report.md` 时建议结构) + +以下骨架可直接复制后填空;路径均为相对项目根。 + +```markdown +# 知识库迁移报告 + +- **生成时间(ISO-8601)**:<...> +- **配置根(如 `.cursor/`)**:<...> + +## 迁移对照表 + +### 主题(旧来源 → 新路径) + +| topic ID | 旧 rules / 旧业务 skills / 索引线索 | 新路径 | +| --- | --- | --- | +| | <...> | `.Knowledge/topics/.md` | + +### stock-docs(源 → 目标) + +| 源路径 | 目标路径 | 备注 | +| --- | --- | --- | +| <...> | `.Knowledge/stock-docs/...` | 成功 / 跳过原因 | + +### req-docs(源 → 目标) + +| 源路径 | 目标路径 | 备注 | +| --- | --- | --- | +| <...> | `.Knowledge/req-docs/...` | 成功 / 跳过原因 | + +## 拟删除路径清单(默认全选;与对话内清单一致) + +- [ ] `<路径>`(`rules/` 下逐文件) +- [ ] `<路径>`(业务 `skills/`,不含 `f2s-*`) +- [ ] `.cursor/docs-index.md`(或实际路径) +- [ ] (可选)旧 `stock-docs/` / `req-docs/` 根目录 + +## 用户排除项(如有) + +- (无则写「无」) + +## 失败或未迁移项(如有) + +- (无则写「无」) + +## 删除执行记录 + +(仅在执行物理删除后追加:时间、已删列表、未删及原因) +``` + +## 完成后自检 + +1. 主题总数是否与旧映射总数对齐(允许用户显式跳过)。 +2. `manifest.topics[].path` 是否都存在。 +3. `index` 是否可定位到每个已迁移主题。 +4. `topicMetadata` 是否只引用 `topicPaths` 已存在 topicId;`primary` / `tags` / `confidence` 是否合法。 +5. `.Knowledge/stock-docs`、`.Knowledge/req-docs` 是否与确认迁移清单一致。 +6. 待人工确认清单是否已清空;未清空则禁止删除旧文档目录。 +7. 旧业务 `rules/`、**非 `f2s-*`** 的旧业务 `skills/`、旧版索引及(若列入清单)旧文档目录是否已按**最终删除清单**执行删除;基线保留清单中的 3 个 `f2s-*` 根规则是否仍保留。 +8. 旧版入口 `docs-index.md` / `index-doc.md` 与 `rules/main.md(c)` 是否已按清单删除(且 `.Knowledge` 已可替代其职责),或是否因用户排除而**明确保留**并写入 `notes[]`。 +9. 状态文件是否与迁移结果一致(完成则删除,暂停则保留且 `status=paused`)。 +10. `.Knowledge/index.md` 是否已为每个主题同步“关联文档(摘要)”列(可写“无”,但不得留空)。 +11. `skills/f2s-*` 是否未被误删、未被写入 `.Knowledge`。 +12. `.Knowledge/migration-report.md` 是否已落盘且包含 **迁移对照表**、**拟删除路径清单**;若已执行删除,是否已追加 **「删除执行记录」** 并与实际磁盘状态一致。 +13. 状态机文件与删除执行记录未被子 agent 越权写入;manifest / index 由主 agent 单点落盘。 diff --git a/packages/core/templates/zh-CN/skills/f2s-kb-rm/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-kb-rm/SKILL.md new file mode 100644 index 0000000..6981d9c --- /dev/null +++ b/packages/core/templates/zh-CN/skills/f2s-kb-rm/SKILL.md @@ -0,0 +1,61 @@ +--- +name: f2s-kb-rm +description: 删除某 stock-docs 文档对应的知识主题与索引映射;触发:删除项目上下文、f2s-kb-rm +--- + +> 执行口径:仅维护 `.Knowledge`,不改配置根 `rules/skills`。 + +## 编排(主 / 子 agent) + +- 两字段(`subAgent` / `switchAgentVerification`)语义以统一入口为唯一事实源:**Cursor/Claude** 读配置根 `rules/f2s-flow2spec-unified-entry.*`;**Codex** 读 `.codex/topics/f2s-flow2spec-unified-entry.md`(与上同源,`flow2spec init` 镜像)。不在此复述。 +- 默认主 agent 全流程执行(单点删除拆子收益低)。 +- 拆子阈值:仅当 `subAgent=true` 且**批量删除一次 ≥ 5 主题**时,才拆子执行删除与清引用。 +- 主必控:范围确认、`fallbackTopic` 重指。 +- 写权硬约束:`manifest-routing.json` 与 `.Knowledge/index.md` 恒由主 agent 落盘。 +- 验证:默认落盘侧自验;本 SKILL 不绑定交叉校验。 + +# 删除文档对应的项目上下文 + +## 输入 + +- 一个参数:`.Knowledge/stock-docs/<文件名>.md` 路径,或可匹配文件名片段。 + +## 执行步骤 + +1. 读取 `.Knowledge/index.md`,匹配目标文档相关主题。 +2. 删除对应 `.Knowledge/topics/.md` 文件。 +3. 从 `.Knowledge/index.md` 移除匹配项并写回。 +4. 更新路由清单: + - `.Knowledge/manifest-routing.json`:移除失效 `topicPaths`、`taskToTopicRules`、`topicDependencies`、`topicMetadata` 引用 + - 对应 `matchers/.json`:移除失效规则或 `includeAny` 词条(与已删 `task`/`matcherId` 对齐) + - 若删除了 `fallbackTopic`,必须指定新的兜底主题 + - **创作侧准则**:本步会调整 `topicDependencies`(删除被依赖主题或孤儿边),须先 Read `rules/f2s-topic-authoring.*` 全文(**Cursor/Claude**:`rules/f2s-topic-authoring.mdc`;**Codex**:`.codex/topics/f2s-topic-authoring.md`),核对 DAG 与最小化约束后再落盘。 + +## 输出摘要(必须) + +- 已删除的 topic 文件列表 +- `.Knowledge/index.md` 删除的条目 +- 路由清单调整的字段 +- 未执行项(若有) + +## 复杂场景示例 + +用户输入文件名片段「回调」,匹配到 2 个主题文档。 + +- 先列出两个候选并要求用户确认删除范围,避免误删。 +- 删除后同步清理路由清单失效引用;若删到了 `fallbackTopic`,必须先指定新的兜底主题再落盘。 +- 最终摘要中写清:删除了哪些 topic、保留了哪些 topic、为什么。 + +## 约束 + +- 匹配多义时先询问用户确认。 +- 仅删除命中主题,不影响其它主题。 +- `manifest-routing.json` 与 `.Knowledge/index.md` 恒由主 agent 落盘(写权硬约束);范围确认与 `fallbackTopic` 重指不可下放给子 agent。 + +## 完成后自检 + +1. 被删 topic 是否仍被 `manifest` 引用(必须为否)。 +2. `index` 是否仍存在失效主题路径(必须为否)。 +3. `topicMetadata` 是否仍引用已删除 topic(必须为否)。 +4. `fallbackTopic` 是否仍有效。 +5. 未在低于拆子阈值(< 5 主题)时强行拆子;manifest / index 由主单点落盘。 diff --git a/packages/core/templates/zh-CN/skills/f2s-kb-sync/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-kb-sync/SKILL.md new file mode 100644 index 0000000..6d69ea2 --- /dev/null +++ b/packages/core/templates/zh-CN/skills/f2s-kb-sync/SKILL.md @@ -0,0 +1,160 @@ +--- +name: f2s-kb-sync +description: 可显式给出能力或零输入推断;先输出知识库更新大纲,确认后写入 topics/index/manifest;触发:f2s-kb-sync、全局同步、知识库同步、已实现能力 +--- + +> 执行口径:本技能只维护 `.Knowledge`,默认不改配置根 `rules/skills`。 + +## KB 自动合并协议(必须) + +本技能不得把“人工执行命令”作为用户流程。用户确认同步大纲后,由 agent 自己完成知识候选生成、合并、构建与校验: + +1. 将已确认的大纲转换为一个或多个 `kb-delta` 草稿,记录 `taskId`、`developerId`、`baseRevisions`、`changes` 与证据摘要;没有显式任务目录时可在内存中形成等价对象,不强制为了本技能创建 `.task`。`changes` 可使用 `appendBody` / `replaceBody` / `updateFrontmatter`;确需新主题时使用 `createTopic`,并可携带 `taskRule` 与 `matcher` 让路由一并接入。 +2. 写入 `.Knowledge` 前,必须用 `flow2spec kb plan ` 或等价内部能力预演;若 topic revision 不一致,停止自动写入,转入语义合并说明。 +3. 可自动合并时,由 agent 调用 `flow2spec kb apply ` 或等价内部能力写入 topic,并随后执行 `flow2spec kb build` 与 `flow2spec kb check`。 +4. 用户只看到“知识库已同步 / 有语义冲突需确认 / 已跳过入库及原因”,不要求用户手动执行 `kb plan/apply/build/check`。 + +## 编排(主 / 子 agent) + +- 两字段(`subAgent` / `switchAgentVerification`)语义以统一入口为唯一事实源:**Cursor/Claude** 读配置根 `rules/f2s-flow2spec-unified-entry.*`;**Codex** 读 `.codex/topics/f2s-flow2spec-unified-entry.md`(与上同源,`flow2spec init` 镜像)。 +- 步骤 1(素材汇总):`subAgent=true` 时可拆子并行,仅只读汇总,不得落盘。 +- 步骤 2(大纲 + 用户确认):必主 agent 完成,确认权不可下放子 agent。 +- 步骤 3(落盘):`subAgent=true` 时可按已确认大纲拆子逐项落盘;硬约束:子落盘前必须前置加载近邻 2–3 个主题的开头摘要,做叙事风格对齐。 +- 写权硬约束:`manifest-routing.json` 与 `.Knowledge/index.md` 恒由主 agent 单点落盘,禁止下放。 +- 校验:默认落盘侧 agent 自验;本 SKILL 不绑定交叉校验。 + +# f2s-kb-sync(先大纲后写入) + +## 输入(可选) + +1. 用户显式给出“已实现能力列表” +2. 零输入:由 Agent 基于当前上下文推断 +3. 辅助材料:`@` 文件、需求文档、架构说明等 + +## 强制流程(不可颠倒) + +### 步骤 1:收集素材(只读) + +- 汇总用户目标、范围、优先级 +- 汇总已实现能力(用户指定 + Agent 推断) +- 对照现有知识库: + - `.Knowledge/topics/` + - `.Knowledge/index.md` + - `.Knowledge/manifest-routing.json` + - `.Knowledge/matchers/*.json`(与路由中 `matcherPath` 对应的分片) + - `.Knowledge/stock-docs/` +- **主题粒度扫描**:对已有 topic 粗扫以下信号,命中时在步骤 2 大纲中列为"建议拆分"(不阻断同步流程): + - 对应 stock-doc 超过 **300–500 行**; + - `includeAny` 词数超过 **12 个**; + - topic 正文包含超过 **3 个不相干职责域**的二级标题。 + +### 步骤 2:输出《更新大纲》(必须) + +大纲至少包含: + +1. 同步目标 +2. 能力清单(用户指定 / Agent 推断 / 合并结果) +3. 信息来源 +4. 拟改文件清单(精确到路径) +5. 主题同步计划:说明每个能力是"更新已有主题"还是"创建新主题",并列出 topicId、topic 文件、index 行、manifest/matcher 变更;如涉及 `topicMetadata`,列出 `primary` / `tags` / `confidence` 候选和证据;无明确证据时写"不分类 / 暂不写入" +6. **终稿沉淀计划(硬约束)**:对每一个"新建 / 更新"的 topic,判断其「长文背景 / 详细资料」引用槽位是否已有对应 `.Knowledge/stock-docs/*_终稿.md`: + - **已有** → 直接引用; + - **没有但本次同步的能力已经代码落地** → 大纲**必须列出**"待生成 `stock-docs/<能力名>_终稿.md`",并注明沉淀来源(对应 `req-docs/*_技术方案.md` + 已实现代码 + 澄清文档),由本 SKILL 步骤 3 之前先触发 `f2s-doc-final` 沉淀(或由用户确认后手写),**再**让 topic 指向终稿; + - **能力仍在 req-docs 待实现阶段、尚无代码** → topic「长文背景」小节暂写占位说明「待代码落地后由 `f2s-doc-final` 生成 stock-doc 终稿」,**禁止**在此槽位直接列 `req-docs/*`。 + - 依据见 `rules/f2s-topic-authoring.*`「长文背景引用的目录边界(硬约束)」。 +7. 不改动范围 +8. 等待用户确认提示 + +> 未确认前禁止落盘修改。 + +### 步骤 2.5:终稿沉淀(若步骤 2 列出待生成终稿) + +用户确认大纲后、`.Knowledge/topics/` 落盘前,先按大纲第 6 项**逐个沉淀 `stock-docs/*_终稿.md`**: + +- 优先调用 **`f2s-doc-final`**(在同一会话内直接进入,不需要用户重新触发); +- 或按 `.Knowledge/template/`(若有终稿模版)手写并落盘; +- 沉淀完成、终稿路径确定后,再进入步骤 3 让 topic 的「长文背景」小节指向该终稿。 + +**禁止**:跳过本步直接落 topic,把 `req-docs/*` 挂进 topic 的「长文背景 / 相关资料」整节槽位。 + +### 步骤 3:确认后写入 + +> 硬约束:若启用拆子,子 agent 落盘前必须读取近邻 2–3 个主题的开头摘要,确保叙事风格一致;`manifest-routing.json` 与 `.Knowledge/index.md` 由主 agent 单点落盘,子 agent 无写权。 +> +> **创作侧准则**:本步若新增 / 修改 topic、`topicMetadata` 或 `topicDependencies`,须先 Read `rules/f2s-topic-authoring.*` 全文(**Cursor/Claude**:`rules/f2s-topic-authoring.mdc`;**Codex**:`.codex/topics/f2s-topic-authoring.md`),再落盘。 + +按大纲逐项更新: + +- `.Knowledge/topics/*.md` +- `.Knowledge/index.md`(同步主题路由表的“关联文档(摘要)”列) +- 路由清单(按需);若创建新 topic,须同步 `topicPaths`、必要的 `taskToTopicRules` / matcher 分片;可在证据明确时写 `topicMetadata`,但分类只用于治理、审计和阅读预期,不参与路由命中或执行强制性,不得为了分类创建、重命名或拆分 topic +- `.Knowledge/stock-docs/*.md`(按需补充索源文档) + +### 步骤 4:收尾摘要 + +- 列出已修改路径与目的 +- 列出未执行项与原因 + +### 步骤 5:写入同步时间戳(必须,f2s-git-commit 依赖) + +本技能成功完成写入后(步骤 3 有实际文件落盘时),由主 agent 落盘 `.Knowledge/.last-sync.json`,格式: + +```json +{ + "syncedAt": "", + "skill": "f2s-kb-sync", + "developerId": "<按 f2s-task 规则解析的 developerId,legacy 时可省略>" +} +``` + +- 该文件由 `f2s-git-commit` 在**默认覆盖检查**前读取,若 `syncedAt` 距今 < 30 分钟则跳过覆盖检查,避免刚同步完知识库又被要求同步一次。 +- **写入时机**:仅在本轮真正写盘(步骤 3 有 topic / index / manifest / stock-docs 变更)时才写;纯"读一遍 kb 什么也没改"的场景**不**写。 +- 覆盖式写入,不追加历史。 +- 落盘失败(磁盘只读、权限不足等)不阻塞本技能主流程,在收尾摘要中列一行 warning 即可。 +- 同类知识库写入技能(`f2s-kb-feat` / `f2s-kb-fix` / `f2s-kb-add` / `f2s-kb-addRules` / `f2s-kb-distill`)成功写盘后也应遵守相同约定,`skill` 字段填自己的 id。 + +## 输出摘要格式(建议) + +```markdown +## 知识库同步结果 + +### 已确认能力范围 +- <能力1> +- <能力2> + +### 已修改文件 +- .Knowledge/topics/.md:<修改说明> +- .Knowledge/index.md:<修改说明> +- .Knowledge/manifest-routing.json:<修改说明或“未改动”> +- .Knowledge/matchers/.json:<修改说明或“未改动”> +- .Knowledge/stock-docs/.md:<修改说明或“未改动”> + +### 未执行项 +- <项>:<原因> +``` + +## 复杂场景示例 + +用户仅说“/f2s-kb-sync 同步一下”,未给能力清单。 + +- 步骤 1 先做最小推断(例如从 `git diff` / 目录名归纳 1~2 个能力域),并给出推断依据。 +- 步骤 2 必须输出大纲并等待“确认”;未确认前禁止写入任何 `.Knowledge` 文件。 +- 用户确认后只执行大纲内条目;若用户中途缩小范围,未执行项写入收尾摘要。 + +## 约束 + +- 先大纲,后写入 +- 小步增补,避免整文件重写 +- 同主题优先原位更新 +- `index.md` 每个主题需包含 `stock-docs/req-docs` 的摘要级**可点击 Markdown 链接**(格式:`[标题](相对路径)`,1-3 条,允许写“无”) +- 不改配置根 `rules/skills` + +## 完成后自检 + +1. 是否存在未确认即写入(必须为否)。 +2. topic 文件与 index 行是否一一对应,且"关联文档(摘要)"已同步更新。 +3. manifest 中 `topics` / `taskToTopicRules` / `topicDependencies` 是否仍引用有效路径。 +4. 若写入 `topicMetadata`:key 是否均存在于 `topicPaths`;`primary` / `tags` / `confidence` 是否合法;是否避免类型前缀命名。 +5. 是否误改配置根 `rules/skills`(必须为否)。 +6. 步骤 2 大纲 + 用户确认未下放子 agent;步骤 3 子落盘前已加载近邻 2–3 主题摘要;manifest / index 由主单点落盘。 +7. **每个新建 / 更新的 topic**,其「长文背景 / 详细资料 / 相关资料 / 长文来源 / 参考文档」整节引用槽位**是否仅指向 `.Knowledge/stock-docs/*_终稿.md`**(或已定型的 stock-doc);**不得**直接列 `.Knowledge/req-docs/*` 作为长文事实源。若代码已落地但对应终稿尚缺,是否已在步骤 2.5 完成沉淀。 diff --git a/packages/core/templates/zh-CN/skills/f2s-kb-upgrade/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-kb-upgrade/SKILL.md new file mode 100644 index 0000000..3cc064d --- /dev/null +++ b/packages/core/templates/zh-CN/skills/f2s-kb-upgrade/SKILL.md @@ -0,0 +1,363 @@ +--- +name: f2s-kb-upgrade +description: 知识库模板升级技能(仅指本 SKILL):**流程分流 V1** 须先 f2s-kb-migrate 再在流程内代跑 flow2spec init;**现行库(流程代号 V2+,含已用 .Knowledge 的 Flow2Spec npm v3.x 等项目)** 则代跑 init 以对齐 manifest-routing + matchers 分片(包内 `manifest-matchers.json` 仅作 init 合并种子,不落盘 .Knowledge)。触发:f2s-kb-upgrade、一键升级迁移、旧项目升级、知识库模板升级。注意:不要把单独的 flow2spec init 称作「升级命令」;**V1/V2+ 为技能内分流代号,不等于 npm 包主版本号**。 +--- + +> 执行口径:本技能用于「代替用户跑 shell」完成 **按本 SKILL 定义的** Flow2Spec **模板与配置根对齐**;其中一步会代跑 **`flow2spec init`**,但 **`init` 不是「升级命令」**,**升级命令 / 知识库升级** 仅指 **`f2s-kb-upgrade` 本技能全流程**。 + +# f2s-kb-upgrade(知识库模板升级技能) + +**术语(必须)**:**「升级」「升级命令」「知识库升级」** 仅指按本文件 **`f2s-kb-upgrade`** 执行的完整技能流程。**`flow2spec init`** 是 CLI **初始化/落盘**命令;本技能 **步骤 2** 会代跑它,**禁止**把用户单独执行的 `init` 或 CLI 帮助里的 `init` 表述为「升级命令」。 + +## 边界(避免误区) + +- **`flow2spec init` 不写业务知识**:不替代 `f2s-kb-add`、`f2s-kb-fix`、`f2s-kb-feat`、`f2s-kb-sync`、`f2s-kb-build` 等对 `stock-docs` / `req-docs` / `topics` 正文与业务向路由词条的维护。 +- 本技能跑通的是 **包版本下的目录、模板占位、路由结构对齐**;用户若说「把新能力写进知识库」,应引导 **`f2s-kb-sync` / `f2s-kb-add`** 等,而非仅 `f2s-kb-upgrade`。 +- 本技能负责存量 `topicMetadata` 审计:`primary` / `tags` 仅用于治理、审计、盘点和阅读预期,不参与路由命中或执行强制性;执行强制性仍以 `AGENTS.md`、rules、skills 与 topic 正文为准。 + +## 包侧发版纪律(`projectRev` 必须正确 bump) + +**字段位置**:`templates/{zh-CN,en-US}/knowledge/manifest-routing.json` 的根级整数字段 `projectRev`(起始 `1`)。 + +**字段写入语义(必读)**: +- **包侧**:维护者按下文规则手动 bump(包模板自身的 `projectRev` 永远是最新值)。 +- **项目侧**(落盘到 `.Knowledge/manifest-routing.json`): + - **首次 init**:项目 `.Knowledge/manifest-routing.json` 不存在 → `init` 把模板原值落盘,等同首次落地即基线对齐。 + - **后续 init**:项目 `.Knowledge/manifest-routing.json` 已存在 → `init` **不再覆盖该字段**(保留项目原值);该字段只由本技能完整流程末尾 3b 写入(见步骤 3b「回写 `projectRev`」)。 + - 这使「项目侧 `projectRev`」语义清晰:**「本项目已基线对齐到的包模板修订号」**,而非"上次 init 时碰到的"。 + +**必须 bump 的修改**(每次发版至少 `+1`): +- 包模板 `templates//knowledge/topics/.md` 任一文件的**正文**修改、新增、删除或改名; +- 包模板 `templates//knowledge/matchers/.json` 的 `includeAny` 词条、`id` 或新增 / 删除 matcher 文件; +- 包模板 `templates//knowledge/manifest-routing.json` 的 `topicPaths` / `taskToTopicRules` / `topicDependencies` / `fallbackTopic` / `topicMetadata` 任一段修改; +- 包模板 `templates//knowledge/index.md` 「主题一览」节或包级章节修改。 + +**不需要 bump 的修改**: +- 包源码(`lib/`、`cli.js`、`scripts/`)、`AGENTS.md`、`README*` 文档; +- `templates//flow2spec.config.json` 默认值; +- `templates//rules/*` / `templates//skills/*` 仅规则与技能正文修改(这些与主题层无关,无需触发完整流程)。 + +**判定准则一句话**:模板里 `knowledge/` 目录下 topic / matcher / manifest / index 任一**主题层产物**变了 → 必 bump;否则不动。漏 bump 会让用户的 `f2s-kb-upgrade` 跑快速路径,错过包带来的主题变更。 + +## 编排(主 / 子 agent) + +- 两字段(`subAgent` / `switchAgentVerification`)语义以统一入口为唯一事实源:**Cursor/Claude** 读配置根 `rules/f2s-flow2spec-unified-entry.*`;**Codex** 读 `.codex/topics/f2s-flow2spec-unified-entry.md`(与上同源,`flow2spec init` 镜像)。本节不复述。 +- **子 agent 职责**(仅当 `subAgent=true`):代跑 `flow2spec init` 等 shell 命令;仅承接命令执行,不承担知识库正文落盘。 +- **主必控**(主 agent 不可下放): + 1. **版本分流**:**V1** 先走 `f2s-kb-migrate` 再进入本技能;**现行库(V2+)** 直接进入 `init` 流程(含 Flow2Spec **npm v3.x** 等,只要已满足步骤 0 中「现行库」条件,均走此支,**勿**因主版本为 3 再单独设一套流程)。 + 2. **`init` 后重读**:从磁盘重读 `f2s-kb-upgrade/SKILL.md`,对比标识是否变化。 + 3. **整技能重跑**:SKILL 有变化时,按新版字面从头再跑一轮,直至连续两轮无变化。 + 4. **步骤 3b 融合**:`.Knowledge/index.md` 的维护区保留 + 包版对齐融合由主 agent 执行。 + 5. **校验摘要**:校验结论与输出摘要由主 agent 汇总。 +- **写权硬约束**:`.Knowledge/index.md` **只由主 agent 落盘**,子 agent **不得触碰**;`manifest-routing.json` 同属主落盘。 +- 本 SKILL 不绑定交叉校验;落盘侧自验。 + +## 与 `f2s-kb-migrate` 为何并存 + +| 技能 | 解决的问题 | +| --- | --- | +| **`f2s-kb-migrate`** | **结构搬家**:`docs-index.md` / `index-doc.md`、`rules/main.md(c)`、业务 `skills/`、散落 `stock-docs`/`req-docs` → **迁入 `.Knowledge`**,落盘 `migration-report.md`、删除清单需用户确认。不代跑 npm 包升级。 | +| **本技能 `f2s-kb-upgrade`** | **包与模板对齐**:代跑 **`flow2spec init`**,合并 **`manifest-routing.json`** 与 **`matchers/*.json`**,刷新各 agent **`rules`/`skills`**(或 Codex **`AGENTS.md`**);`init` 另将当前语言的 **`index.md` → `.Knowledge/template/index.template.md`** 作对照快照,**`.Knowledge/index.md`** 由步骤 3b **diff 对齐**,init **不**自动改其正文。 | + +- **旧项目一键闭环**:**先 `f2s-kb-migrate`** → **再本技能**(`init`)。禁止仅用 `init` 代替完整迁移。 +- **已是新版 `.Knowledge` 的项目**:**只跑本技能**,勿重复 migrate。 + +**为何每个已配置客户端目录下都有一份同名 `SKILL.md`?** +各客户端只加载**自身配置根**下的 `skills/`。`flow2spec init` 会向所选 agent 目录**同步落盘**当前语言对应的技能内容。 + +## 目标 + +当用户说「帮我升级知识库模板 / 跑 f2s-kb-upgrade / 同步最新 Flow2Spec」时,Agent **按本技能 `f2s-kb-upgrade` 全文流程执行**(含代跑 `flow2spec init`、清理、校验、摘要);**勿**把仅执行 `init` 等同于完成本技能。 + +## 默认行为 + +1. 本技能步骤 2 代跑 **`flow2spec init`** 时,默认 **增量落盘**(不带 `--reset-knowledge`)。 +2. 仅当用户明确要求「覆盖重置」时,才在 `init` 末尾追加 `--reset-knowledge`。 +3. 优先写入用户指定的 agent;未指定时使用包的默认客户端选择。 + +## init 与技能自更新(必须) + +本技能在 **步骤 2** 会执行 **`flow2spec init`**;`init` 会把当前语言对应的技能内容同步到各 agent **配置根**,因此 **`init` 成功结束后**,本仓库里的 **`skills/f2s-kb-upgrade/SKILL.md`** **可能被新版本覆盖**,与当前对话里已缓存的旧说明不一致。 + +**闭环(防旧条令)**: + +1. **`init` 前**(推荐):记下当前配置根内 **`skills/f2s-kb-upgrade/SKILL.md`** 的标识(如 `mtime`、文件大小或正文 hash)。 +2. **`init` 成功结束后**:**重新读取磁盘上** 该 **`SKILL.md` 全文**(Cursor:`.cursor/skills/f2s-kb-upgrade/SKILL.md`;Claude:`.claude/skills/...`;Codex:`.codex/skills/...`,与本次 `init` 写入的 agent 一致)。 +3. **若相对步骤 1 有变化**(或刚升级 Flow2Spec 包、无法确认是否无变):**必须以最新 SKILL 为准**,按新版字面**重跑评估与落盘**(即从下文「步骤 2c」开始:重新读 `projectRev` / `pkgRev`、按新版判定表决定快速路径或完整流程、跑步骤 3 / 3a / 3b / 4 / 5)。**重跑时不再次执行 `flow2spec init`**——本轮已在步骤 2 跑过;再 init 不会带来新信息,反而会让 SKILL 自更新闭环陷入循环。可循环至**连续两轮**读到的 SKILL **无变化**,或用户明确要求停止。 +4. **若无变化**:继续执行步骤 2c 及以后。 + +> **快速路径例外**:若步骤 2c 判定为「快速路径」(`projectRev == pkgRev`,主题层未变),即便 SKILL.md 字面有变化,也**不要求**按新版重跑——重跑后仍会再次判定为快速路径,徒增开销。仅在「完整流程」分支下保留闭环。 + +> 口径:**本技能步骤 2 执行 `init` 后** → 再读最新 `f2s-kb-upgrade/SKILL.md` → 有变 + 走完整流程时才**按新版字面从步骤 2c 起重跑**(**不再次 init**);不要仅凭会话记忆执行 **本技能**。 + +## 强制流程 + +### 步骤 -1:全局 flow2spec 版本预检(必须,先于一切,主 agent 前台探测) + +**目的**:让「能用全局 `flow2spec` 就用全局」,只在**没装**或**版本过旧**时才动手升级;已装且已是 latest 时**完全跳过**升级动作,同时决定步骤 2 命令的**默认形态**(用 `flow2spec init` 还是 `npx @latest init`)。 + +**动作**:主 agent 在进入步骤 0 **之前**,**顺序、前台**执行以下 3 条探测(都是纯查询,无副作用,秒级返回;无需拆子 agent): + +```bash +# 1. 探测本机全局是否装了 flow2spec +flow2spec --version 2>/dev/null || echo __F2S_NOT_INSTALLED__ +# 2. 查询 npm 上 latest 版本号(网络受限时可能失败,允许失败) +npm view @double-coding/flow2spec version 2>/dev/null || echo __F2S_NPM_UNREACHABLE__ +# 3. (备用)若第 1 步返回 __F2S_NOT_INSTALLED__,用来确认 npx 可用 +command -v npx >/dev/null 2>&1 && echo __NPX_OK__ || echo __NPX_MISSING__ +``` + +**判定 3 分支**(按结果选一条,写入本轮上下文并影响步骤 2 与步骤 5 摘要): + +| 情况 | 判定条件 | 行动 | 步骤 2 命令默认形态 | +| --- | --- | --- | --- | +| **A. 已装且是 latest** | 第 1 步返回版本号 `V`,第 2 步返回版本号 `L`,且 `V === L` | **完全跳过升级**,本轮不派子 agent、不跑 `npm i -g` | **`flow2spec init `**(用全局) | +| **B. 已装但落后** | 第 1 步返回版本号 `V`,第 2 步返回版本号 `L`,且 `V !== L`(`V < L` 或 semver 不等) | **派独立子 agent 后台跑** `npm i -g @double-coding/flow2spec@latest`(fire-and-forget,不等待,不阻塞主流程);本轮步骤 2 仍用 `npx @latest` 保证本次拿到 latest 模板 | **`npx @double-coding/flow2spec@latest init `** | +| **C. 未装 or 版本无法确认** | 第 1 步命中 `__F2S_NOT_INSTALLED__`,或第 2 步命中 `__F2S_NPM_UNREACHABLE__` 且第 1 步也未拿到版本号 | 若 A 情况「已装 latest」不成立且**未装**:派独立子 agent 后台跑 `npm i -g ...@latest`(同 B);若第 2 步失败但第 1 步已装某版本:视作 B 且无法比对 latest,**不派**升级、仅提示「latest 未知,保守用 npx」 | **`npx @double-coding/flow2spec@latest init `** | + +**编排(必须)**: + +- **A 分支**:主 agent 直接跳过所有升级动作,**不派**子 agent;本轮步骤 2 命令首选 `flow2spec init`。 +- **B / C 分支**:若确需升级(未装或版本落后),派**独立子 agent** fire-and-forget 执行 `npm i -g @double-coding/flow2spec@latest`,**不等待完成**、**不阻塞**主流程;成败均不进入 SKILL 结论。该派子**强制**执行,**不受** `flow2spec.config.json.subAgent` 字段约束(全局 npm 装包不属业务拆分范畴)。 +- **写权**:子 agent 仅执行该 shell,**不**触碰 `.Knowledge` / `manifest-routing.json` / `index.md` 等任何项目文件;写权硬约束不变。 +- **探测失败兜底**:若 3 条探测全部失败(无 shell 权限、极端受限环境),按 C 分支处理并用 `npx @latest`;此时也可以直接放弃步骤 -1、把升级留给 `cli.js` 的 `maybeAutoUpdateGlobalInstall()` 收尾兜底。 + +**与 cli.js 的关系**: + +- `cli.js` 内 `maybeAutoUpdateGlobalInstall()` 是 `init` 收尾兜底逻辑,**与本步不冲突**:本步在前台 init 之前完成探测/派工,cli 那段在 init 收尾时再兜一次;两次都成功就是 no-op,第一次失败第二次还能补救。 + +### 步骤 0:版本判定与分流(必须,先于 init) + +> **命名说明**:下文 **「V1」「现行库(V2+)」** 为本技能**流程分流代号**。**npm 包为 v3.x、v4.x…** 且仓库**已**是 `.Knowledge` + `manifest-routing` 形态时,仍走 **「现行库(V2+)」** 支(仅 `init` 对齐),**不要**把 npm 主版本数字当成这里的「V2」字面限制。 + +**V1 — 旧版知识组织(须先迁移再 init)** +命中**任一**强信号则按 V1: + +- 配置根仍有 **`docs-index.md` 或 `index-doc.md`**,且主要仍经 **`rules/main.md` / `rules/main.mdc`** 收口;或 +- 业务 **`stock-docs` / `req-docs` 与规则、业务 skills** 仍以配置根旧树为主,**未**稳定落在 `.Knowledge`。 + +**动作**:先按 **`f2s-kb-migrate`** 全流程执行(含 `migration-report`、删除清单确认),**再**进入步骤 1–5 执行 `flow2spec init`。 + +**现行库(V2+)— 已上 `.Knowledge` + 新版路由(仅包级 / 形态对齐)** +同时满足: + +- 存在 **`.Knowledge/manifest-routing.json`**,且 **`topicPaths` / `taskToTopicRules`** 可用; +- 业务文档已以 **`.Knowledge/stock-docs`、`req-docs`、`topics`** 为主(可与 V1 刚结束状态衔接)。 + +**历史口径**:若仓库里仍有遗留 **单文件 `manifest.json`**,**不得**再当作机读事实源;机读以 **`manifest-routing.json` + `matcherPath` 指向的 `matchers/*.json`** 为准,`init` 负责与模板**合并 / 回填分片**。 + +**动作**:直接进入步骤 1–5;**无需** migrate,除非用户明确要求重做迁移。 + +### 步骤 1:确认本技能内 `init` 模式(必须) + +- 若用户未明确「覆盖重置」,本技能步骤 2 默认 **增量 `init`**。 +- 若用户提到「全部按模板覆盖/重置」,二次确认后再使用 `--reset-knowledge`。 +- **locale 规则**:普通升级沿用项目 `flow2spec.config.json.locale`;字段不存在时按 `zh-CN` 补齐。禁止在本技能中顺手切换语言;只有用户显式要求 `--locale en-US` / `--locale zh-CN` 时才传入对应参数。 + +### 步骤 2:执行命令(代用户跑 shell) + +**步骤 2 开始前**:读取项目侧 **`.Knowledge/manifest-routing.json`** 的 `projectRev` 字段(**字段不存在则记为 `null`**),将该值记为 **`projectRev`**。`projectRev` 表示**「本项目已基线对齐到的包模板修订号」**(由本技能完整流程跑完步骤 3 / 3a / 3b 后写入;首次 init 时 init 会以模板值落盘);**`init` 在 manifest 已存在时不再覆盖该字段**,因此 `projectRev` 反映的是本项目最近一次完整流程对齐到的版本,而非"上次 init 时包带过来的"。`projectRev` 将用于步骤 2c 与 `pkgRev` 对比。 + +在目标项目根目录执行以下命令(**按步骤 -1 的分支结论选默认形态**): + +1. **步骤 -1 判定为 A(已装且是 latest)**:直接用全局 CLI(**首选**): + - `flow2spec init ` +2. **步骤 -1 判定为 B/C(未装 / 落后 / latest 未知)**:拉 npm latest 跑(**保证本次拿到最新模板**): + - `npx @double-coding/flow2spec@latest init ` +3. 覆盖重置时: + - 在上述命令末尾追加 `--reset-knowledge` +4. 用户显式要求切换模板语言时: + - 在上述命令末尾追加 `--locale ` +5. **手动 override**:若用户明确说「就用全局」或「就用 npx」,按用户意愿选定;不再走步骤 -1 分支自动匹配。 + +> `` 示例:`cursor claude codex`。 + +> **辅助命令(用户可自查)**:`flow2spec --version` 看当前全局版本;`flow2spec update` 触发 CLI 内置的自更新。这两条**不**替代本 SKILL 的完整流程——它们只是「让全局 CLI 保鲜」,主题层对齐仍须走本 SKILL 步骤 2 及以后。 + +**步骤 2 完成后**:立刻执行上文 **「init 与技能自更新」**:重读 **`skills/f2s-kb-upgrade/SKILL.md`**;若有更新则**按新版字面从步骤 2c 起重跑**(**不再次 init**;避免用旧版 SKILL 做后续校验)。 + +### 步骤 2c:主题层变更判定(必须,决定走快速路径或完整流程) + +**目的**:包升级若**未带主题层变更**(topic / matcher / index 模板正文未改),跳过步骤 3 / 3a / 3b 与"整技能重跑"闭环,直接进入步骤 4 轻量校验。仅当包侧明确 bump 了 `projectRev` 才走完整流程。 + +**判定方法**: + +1. **`init` 跑完后**,从**项目侧 manifest**取 `pkgRev`。**口径**:直接 `Read` 项目根 **`.Knowledge/manifest-routing.json`** 的 **`pkgRev`** 顶层字段。该字段由本次 `init` 写入,记录"本次 init 用的包模板 projectRev"——是包侧最新值,与同文件里的 `projectRev`(= `projectRev`,"本项目已基线对齐到的包模板修订号")形成「包侧 / 项目侧」对照,无需新增文件。 + + - 字段存在且为整数 → `pkgRev = <整数>`; + - 字段缺失或非整数 → `pkgRev = null`(包模板自身未声明 `projectRev`); + - 项目侧 manifest 文件本身缺失 → 不在本步处理,应在步骤 2 / 步骤 1 自检阶段就报错。 + +2. 比对 `projectRev`(步骤 2 开始前记录)与 `pkgRev`: + +| `projectRev` | `pkgRev` | 判定 | 后续 | +| --- | --- | --- | --- | +| 任意值 | `null` | **完整流程**(包未声明字段,走旧逻辑兜底) | 走完整步骤 3 / 3a / 3b | +| `null` | 任意整数 | **完整流程**(项目首次接入或老项目升级,需走完整流程做基线对齐) | 走完整步骤 3 / 3a / 3b | +| 整数 X | 整数 X(相等) | **快速路径**(主题层未变) | **跳过** 步骤 3 / 3a / 3b 及"整技能重跑"闭环,**直接进入步骤 4** | +| 整数 X | 整数 Y(不等) | **完整流程**(包带来主题层变更) | 走完整步骤 3 / 3a / 3b | + +3. **`--reset-knowledge` 例外**:用户显式 reset 时,**强制走完整流程**,忽略本步判定(reset 必须走完整 3b 重建)。 + +4. **本步判定结论必须写入步骤 5 摘要**,形如「`projectRev`:项目 `X` vs 包 `Y` → 快速路径 / 完整流程 / 字段缺失走兜底」。 + +> **盲点声明**:本判定只看 `projectRev`,**信任包侧维护者在改了 topic / matcher 模板正文时按规矩 bump**。若包侧未守纪律,可能漏判;用户主观觉得不对时可显式追加 `--full` 语义(口头要求"完整流程"即可),技能侧应忽略快速路径直接走完整流程。 + +### 步骤 3:旧主题模板清理与引用修复(若存在则必须执行) + +> **快速路径跳过**:若步骤 2c 判定为「快速路径」,**本步骤整段跳过**,直接进入步骤 4。仅在「完整流程」时执行以下内容。 + +**本技能步骤 2** `flow2spec init` 成功后,先执行「旧文件清理 + 引用修复」: + +> **skill 目录自动对齐**:`flow2spec init` 现已自动删除配置根 `skills/` 中当前版本不再提供的旧目录(重命名/删除的 skill 如 `f2s-ctx-build`、`f2s-doc-add`、`f2s-rule-capture`、`stock-docs-vs-req-docs` 等),**无需 Agent 手动清理**。 + +1. 清理旧命名主题文件(仅在文件存在时删除,均为无 `f2s-` 前缀的旧版遗留): + - `.Knowledge/topics/flow2spec-architecture.md` + - `.Knowledge/topics/implement-tech-design.md` +2. 修复引用(仅在文件存在时更新;**`.Knowledge/index.md` 正文不由 init 改写**,见步骤 3b): + - `.Knowledge/index.md`(按需人工或技能侧改路径/段落) + - `.Knowledge/manifest-routing.json` +3. 引用更新目标(确认使用新名): + - `.Knowledge/topics/f2s-flow2spec-architecture.md` + - `.Knowledge/topics/f2s-implement-tech-design.md` + - `.Knowledge/topics/f2s-stock-docs-vs-req-docs.md` + +> 口径:只清理”旧命名主题文件”,不删除带 `f2s-` 前缀的现行主题文件。 + +### 步骤 3a:`topicMetadata` 存量审计(必须执行) + +> **快速路径跳过**:若步骤 2c 判定为「快速路径」,**本步骤整段跳过**。仅在「完整流程」时执行。 + +1. 读取 `.Knowledge/manifest-routing.json`,以 `topicPaths` 为主题全集。 +2. 校验 `topicMetadata`:key 必须存在于 `topicPaths`;`primary` 仅允许 `feature` / `module` / `config` / `policy`;`tags` 若存在须为数组,元素取值同 `primary` 且不得与 `primary` 重复;`confidence` 仅允许 `manual` / `inferred`。 +3. 对 `topicPaths` 中缺少 metadata 的主题做分类分析:**必须 Read 对应 `.Knowledge/topics/.md` 正文**,禁止仅凭 topicId 名称推断。证据明确则写入 `inferred`;证据不足时**不写 metadata**,但须在摘要中列出推断方向与依据(如「建议 policy,正文含多处强制约束」),供用户确认后手动补写 `manual`。 +4. 分类判断以 `f2s-topic-authoring` 准则第 3 节为准,Agent 基于 topic 正文判断主要性质,写 `primary`;同时覆盖多个性质时其余写 `tags`(可选)。 +5. 禁止因为补分类创建、重命名或拆分 topic。 +6. **主题粒度审计**(不阻断升级,仅列入摘要):逐项检查,命中任一信号时在步骤 5 摘要中列为「建议拆分」: + - 对应 stock-doc 超过 **300–500 行**; + - `includeAny` 词数超过 **12 个**; + - topic 正文包含超过 **3 个不相干职责域**的二级标题; + - 该 topic 同时被多种不相干任务类型频繁命中(可从 `taskToTopicRules` 和 matcher 词宽度判断)。 +7. **旧 topic frontmatter 自动补齐**:完整流程中必须由 agent 自行执行 `flow2spec kb build --fix-topics`(或等价内部能力),为缺少 frontmatter / `revision` 的存量 topic 补 `id`、`revision`、`summary`,并按 `manifest-routing.json` 补 `dependsOn` / `primary` / `confidence` / `tags`。随后执行 `flow2spec kb check --strict`;若 strict 失败,停止并在摘要中列出具体 topic / reason。不得要求用户手动逐个 topic 添加头部。 + +### 步骤 3b:`index.md` 融合与 `template/index.template.md`(必须执行) + +> **快速路径跳过**:若步骤 2c 判定为「快速路径」,**本步骤整段跳过**(包模板的「主题一览」节未变 → 现有 `index.md` 仍是对的)。仅在「完整流程」时执行。 + +> **范围**:本条「融合」**仅在本技能内由 Agent 落盘 `.Knowledge/index.md`**;**不要求、也不假设**修改 Flow2Spec 包内 **`cli.js` / `lib/init.js`** 等 JS。`init` 行为仍以仓库现行为准(仅复制快照等)。 + +**`flow2spec init` 在本流程中的角色**:把当前语言的 `index.md` 快照复制到 **`.Knowledge/template/index.template.md`**,作为**包版外壳对照**;**不**替代本步骤对 **`index.md`** 的融合书写。 + +#### 融合规则(必须遵守) + +0. **写权归属**:本步骤的 `.Knowledge/index.md` 融合恒由主 agent 执行并落盘;子 agent 不得直接写入(写权硬约束)。 +1. **对照源** + - **包版全文**:**`.Knowledge/template/index.template.md`**。 + - **项目现状**:**`.Knowledge/index.md`**。 + +2. **项目自身维护区(锚点:`.Knowledge/template/index.template.md` 中的 `## 主题一览`)** + - 以 `.Knowledge/template/index.template.md` 为参照:**从二级标题 `## 主题一览` 起**,**直至本节结束**:即到 **紧挨在 `## 命中与执行`(含括号说明)之前的那个 `---` 之前**的整块内容(含「主题一览」下的表格、节内说明段落等)。 + - 该整块 **必须保留来自当前项目 `.Knowledge/index.md` 的正文**(由业务与 **f2s-*** 维护);**禁止**用包模板同一段落**整体替换**覆盖(避免丢失业务主题行与摘要列)。 + - **允许**在该块内做**最小必要修补**:例如为新增的 `topicPaths` 主题**补行**、按 **`manifest-routing.json` 的 `topicPaths`** 改正「路径」列、与快照对比后补上新增的表格列说明——仍以保留项目已有行为主。 + +3. **必须与包模板一致的部分** + - **上述维护区之外**的所有内容(含 **`## 主题一览` 之前**从文件开头到该节前、以及 **`## 命中与执行` 及之后**直到文件结尾):须与 **`.Knowledge/template/index.template.md`** 中对应段落 **一致**(以包版为准;diff 后以模板覆盖项目侧旧文)。 + +4. **产出** + - 将融合后的完整 **`index.md`** 写回 **`.Knowledge/index.md`**。 + - **diff** 结论与是否改动写入步骤 5 摘要。 + +5. **与 `--reset-knowledge` 的关系** + - 若用户已 `reset`,`.Knowledge/index.md` 可能被模板整文件覆盖,仍须按本条 **2** 从备份或版本控制恢复「主题一览」块后再与包外壳做 **3** 的合并(若仓库无备份,则按 `topicPaths` + 快照**重建**主题表并让用户确认)。 + +#### 完整流程末尾:回写 `projectRev`(必须) + +完整流程跑完上述步骤 3 / 3a / 3b 之后(**仅完整流程,快速路径不执行**),由主 agent 把项目侧 **`.Knowledge/manifest-routing.json`** 的 `projectRev` 字段**改写为 `pkgRev`**(步骤 2c 取到的整数;若 `pkgRev` 为 `null` 则**不动**该字段): + +- 这是 `projectRev` 的**唯一**写入路径(除首次 init 模板默写之外); +- 下一次 `f2s-kb-upgrade` 据此判定 `projectRev == pkgRev` 走快速路径,避免重复跑 3 / 3a / 3b; +- 写入与 `manifest-routing.json` 其余字段同属主 agent 写权(写权硬约束)。 + +### 步骤 4:校验本技能执行结果(必须) + +至少校验: + +1. 步骤 2 的 `flow2spec init` 是否成功退出(exit code = 0)。 +2. init 输出是否包含 **路由清单与 `.Knowledge` 的结论**(已对齐/已最新/reset 覆盖等),以及 **`index.template.md` 已复制** 一行(若包内缺 `index.md` 则无此行)。 +3. `manifest-routing` 与各 `matcherPath` 分片是否可解析,且 `topicPaths` / `matcherId` 引用均有效。 +4. 存在 **`.Knowledge/template/index.template.md`**;已按步骤 **3b** 完成 **`index.md` 融合**(维护区保留 + 其余与包版一致)或写明待用户处理原因。 +5. 配置根产物是否存在: + - Cursor/Claude:`rules/`、`skills/` + - Codex:`.codex/AGENTS.md`、`skills/` +6. 本技能成功完成后,删除 `.Knowledge/update-check.json`(若存在),让下一次新会话重新检测并清除旧升级提示;若删除失败,在步骤 5 摘要中写明。 + +### 步骤 5:输出结果摘要(必须) + +输出以下信息: + +- **步骤 -1 全局版本预检**:分支结论(`A 已装且是 latest(跳过升级) / B 已装但落后(已派子 agent 后台升级) / C 未装或 latest 未知(已派或提示)`)+ 当前全局版本 + npm latest 版本(若拿到) +- 执行命令(含 agent 与是否 reset) +- 是否成功 +- **`projectRev` 判定**:`projectRev` X vs `pkgRev` Y → 快速路径 / 完整流程 / 字段缺失走兜底(步骤 2c) +- 旧主题模板清理结论(删了哪些 / 哪些本就不存在;**快速路径下:未执行**) +- `index/manifest` 引用修复结论(**快速路径下:未执行**) +- **index**:`index.template.md` 是否已生成;**`index.md` 融合**是否完成(锚点 **18–19「主题一览」节**保留、其余与包版一致)及 `topicPaths` / diff 结论(步骤 3b;**快速路径下:未执行**) +- **`projectRev` 回写**:完整流程跑完后是否已把项目侧 `projectRev` 改写为 `pkgRev`(步骤 3b 末「回写 `projectRev`」;**快速路径下:未执行**) +- **SKILL 自更新**:`init` 后是否重读 `f2s-kb-upgrade/SKILL.md`;是否因文件变化**按新版字面从步骤 2c 起重跑**及轮次(**不再次 init**;见「init 与技能自更新」;**快速路径下:跳过该闭环**) +- manifest / matchers 对齐结论(随 init 输出) +- 关键文件校验结论 +- `.Knowledge/update-check.json` 清理结论(已删除 / 不存在 / 删除失败) +- 如失败,给出下一步可执行修复建议 + +## 输出摘要模板(建议) + +```markdown +## f2s-kb-upgrade 执行结果 + +- **步骤 -1 全局版本预检**:`A 已装且是 latest(跳过升级) / B 已装但落后(已派子 agent 后台升级 npm i -g) / C 未装或 latest 未知(已派 / 保守用 npx)`;当前版本=``,latest=`` +- 本技能内代跑命令:`<实际执行的 flow2spec init ... 或 npx @latest init ...>` +- init 模式:`增量` / `覆盖重置(--reset-knowledge)` +- 执行结果:`成功` / `失败` +- **主题层判定**:`projectRev=` vs `pkgRev=` → `快速路径(已跳过 3/3a/3b)` / `完整流程` / `字段缺失走兜底` + +### 核心校验 +- 旧主题文件:`已清理` / `无需清理` / `快速路径下未执行` +- 引用修复:`已更新` / `已一致` / `快速路径下未执行` +- **index(快照 + 融合)**:`快照已复制` / `index.md 已融合` / `快速路径下未执行` / `待处理(见备注)` +- **topicMetadata(存量审计)**:`已补齐` / `待用户确认` / `快速路径下未执行`;列出新增 / 修正 / 删除的 topicId +- **topic frontmatter**:`已自动补齐 N 个` / `已完整无需补齐` / `strict 校验失败` / `快速路径下未执行` +- **f2s-kb-upgrade SKILL**:`init 后无变化` / `已按新版从 2c 起重跑 N 轮(不再次 init)` / `快速路径下跳过该闭环` / `待确认` +- **`projectRev` 回写**:`已写入项目 manifest(值=pkgRev)` / `快速路径下未执行` / `pkgRev=null 未动` +- manifest-routing / matchers 分片:`已与模板对齐` / `已是最新` / `reset 覆盖` +- topics.path:`全部存在` / `存在缺失(见下)` +- agent 产物:`通过` / `异常(见下)` +- update-check 缓存:`已删除` / `不存在` / `删除失败` + +### 备注 +- <失败原因或后续建议> +``` + +## 约束 + +- 不把“请用户自行运行命令”作为默认方案;优先由 Agent 直接执行。 +- 未经明确同意,不执行 `--reset-knowledge`。 +- 不修改业务代码;仅按 **本技能 `f2s-kb-upgrade`** 流程与结果做校验。 +- 步骤 3b `.Knowledge/index.md` 融合与 `manifest-routing.json` 均恒由主 agent 落盘(写权硬约束);子 agent 仅可代跑 shell 命令。 + +## 完成后自检 + +1. 是否已做 **步骤 -1**:在进入步骤 0 前**已顺序前台执行 3 条探测**(`flow2spec --version` / `npm view ... version` / `npx` 可用性),并按 A/B/C 分支得出结论;仅在 B/C 时才**派独立子 agent**后台跑 `npm i -g @double-coding/flow2spec@latest`(不等待),A 分支**未派**任何升级动作;步骤 2 命令默认形态是否随分支选定(A→`flow2spec init`,B/C→`npx @latest init`);摘要中已写清分支与版本对比。 +2. 是否已做 **步骤 0**:V1 未跳过 migrate、**现行库(V2+)** 未误跑 migrate。 +3. 是否在 **步骤 2 开始前** 记录了项目侧 `projectRev`(`projectRev`),并在 **步骤 2 的 `init` 之后** 重读 `pkgRev`、执行 **步骤 2c** 判定。 +4. 是否在 **步骤 2 的 `init` 之后**重读过 **`f2s-kb-upgrade/SKILL.md`**:完整流程下有变化必须**按新版字面从步骤 2c 起重跑**(**不再次 init**);快速路径下可跳过该闭环(见「init 与技能自更新」「快速路径例外」)。 +5. 是否已实际执行 shell 命令(而非只给建议)。 +6. 是否明确标注增量 or reset 模式。 +7. **完整流程时**:是否已处理旧主题文件清理与 `index/manifest` 引用修复(步骤 3)。 +8. **完整流程时**:是否已执行 **步骤 3a**:审计 `topicMetadata`,确保无孤儿 key / 非法 primary / 非法 confidence;缺失旧主题已按证据补 `inferred` 或列为待确认。 +9. **完整流程时**:是否已执行 `flow2spec kb build --fix-topics` 或等价内部能力,并随后执行 `flow2spec kb check --strict`,确保存量 topic 已具备 `revision`。 +10. **完整流程时**:是否已执行 **步骤 3b**:**融合** `index.md`(**主题一览**节起至命中与执行前为项目维护区,其余同包版),并核对 `topicPaths`;**完整流程末尾**是否已**回写** 项目侧 `projectRev = pkgRev`(`pkgRev=null` 则保留原值)。 +11. **快速路径时**:步骤 3 / 3a / 3b 是否真的跳过(未做无关扫描),摘要中明确标注「快速路径下未执行」。 +12. 是否输出了 manifest 与关键路径校验结果。 +13. 若失败,是否给出下一步具体命令建议。 +14. 步骤 3b 的 `index.md` 融合由主 agent 完成并落盘,无子 agent 越权写入(仅在完整流程时适用)。 +15. 成功升级后是否删除 `.Knowledge/update-check.json`,避免当天新会话继续提示旧升级信息。 diff --git a/packages/core/templates/zh-CN/skills/f2s-req-clarify/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-req-clarify/SKILL.md new file mode 100644 index 0000000..9a23245 --- /dev/null +++ b/packages/core/templates/zh-CN/skills/f2s-req-clarify/SKILL.md @@ -0,0 +1,32 @@ +--- +name: f2s-req-clarify +description: 针对 PRD/需求反问直到清楚,再可用 f2s-req-tech 出技术方案;触发:需求澄清、PRD 澄清 +--- + +## 编排(主 / 子 agent) + +- `subAgent` / `switchAgentVerification` 两字段语义以统一入口为唯一事实源:**Cursor/Claude** 读配置根 `rules/f2s-flow2spec-unified-entry.*`;**Codex** 读 `.codex/topics/f2s-flow2spec-unified-entry.md`(与上同源,`flow2spec init` 镜像)。本技能不复述。 +- 本技能默认**不拆子**:无论 `subAgent` 真值,澄清流程全程在主会话进行(追问与用户对齐强依赖连续同会话,拆子必断上下文)。 +- 校验口径为**落盘侧自验**,本技能不绑定交叉校验。 + +# 需求澄清 + +> 执行口径:澄清文档统一落盘到 `.Knowledge/req-docs/`。 + +**入参**:可选。PRD 全文、需求描述或文档路径(如 `.Knowledge/req-docs/xxx.md`);不传则按当前对话内容澄清。后续回复可补需求条件。 + +**行为**:找出需求中的模糊表述、未定义概念、缺失信息、矛盾、与实现相关但未说明的点 → 分组、具体可答地反问 → 根据回答迭代追问,直到流程、边界、异常、关键概念无歧义。不替用户做业务假设,不清楚就问。 + +**结束(澄清文档落盘 → 自动衔接技术方案)**:当信息已足够清晰时,必须输出一份可直接落盘的「需求澄清文档」(Markdown)。文档至少包含:背景与目标、范围(包含/不包含)、关键流程、边界与异常、关键概念定义、验收标准、未决问题(如有)。建议保存到 `.Knowledge/req-docs/`(推荐命名 `<能力名>_需求澄清.md`)。 + +**澄清文档落盘后本技能同轮自动衔接 `f2s-req-tech`**:以刚落盘的澄清文档路径为输入直接进入技术方案生成,无需等用户再次触发;进入前给用户一行提示「澄清文档已就绪:`<路径>`;正在按 `f2s-req-tech` 生成技术方案」,然后继续。 + +**例外——停在澄清、不自动衔接技术方案**(任一命中即停): +- 澄清文档「未决问题」小节仍有影响方案结构的关键项未回答(如库/表/接口/状态机主契约缺定义),此时输出一段说明列出待答项,等用户回答后再落盘并衔接; +- 用户在澄清过程中明确说「先只出澄清 / 别急着做方案 / 先讨论」等停步语; +- 用户显式指定了不同的后续动作(如「澄清完就停」「先给我拆任务」)。 + +**禁止**: +- 在澄清文档尾部或紧随其后追加 `f2s-kb-distill` 收口提示(见 `rules/f2s-kb-feedback-closing.*` 禁止段——过程编排型技能落盘不触发 distill); +- 未落盘澄清文档就自动衔接 `f2s-req-tech`(自动衔接的前提是磁盘上已有澄清文档路径); +- 越级自动衔接 `f2s-req-plan` / `implement-tech-design` / 其他 `f2s-*` 技能(同轮只允许接到 `f2s-req-tech` 一步,后续仍须用户新一轮触发)。 diff --git a/packages/core/templates/zh-CN/skills/f2s-req-plan/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-req-plan/SKILL.md new file mode 100644 index 0000000..0232fd5 --- /dev/null +++ b/packages/core/templates/zh-CN/skills/f2s-req-plan/SKILL.md @@ -0,0 +1,150 @@ +--- +name: f2s-req-plan +description: 根据技术方案/需求描述/变更描述规划并实现任务;始终按 f2s-task 维护 .task/;支持子 agent 并行实现;触发:f2s-req-plan、创建任务、任务规划、我需要任务清单 +--- + +> **任务路径**:凡 `.task/` 落盘与续作,**必须以 `rules/f2s-task` 解析的 `TASK_ROOT` 为准(`.task` 或 `.task/`;config → git → legacy)。下文若仍出现 `.task/todo.json` / `.task/active/`,均视为 **`TASK_ROOT/...` 的简写**。 + + +# 需求任务规划与实现(f2s-req-plan) + +从需求/技术方案出发,完整覆盖「规划 → 实现」链路。**不依赖** `changeTracking.*`,但 **`.task/` 全生命周期必须以 `f2s-task` 为唯一真值源**(目录、格式、续作、打钩、归档、user-todos)。知识库同步由用户后续按需调用 `f2s-kb-feat` / `f2s-kb-sync`。 + +## 与 f2s-task 的关系(硬约束) + +| 项 | 说明 | +| --- | --- | +| **真值源** | 配置根 **`rules/f2s-task.*`**(`alwaysApply: true`);Codex 读 **`.codex/topics/f2s-task.md`**(init 镜像,与 rules 同源) | +| **本技能职责** | 规划草稿、实现代码、子 agent 编排;**不得**自定 `.task/` 结构或弱化打钩/归档 | +| **与 changeTracking** | `f2s-req-plan` **不受** `changeTracking.feat/fix/implement` 约束,**始终**走任务清单;见 `f2s-task`「生效条件」 | + +**所有已为项目初始化的客户端都必须读取 `f2s-task` 全文(步骤 0 必做,先于下文任何步骤)**。请从当前客户端生成的 rules、`AGENTS.md` 或 topics 入口读取,不得用本技能摘要代替全文。 + +## 编排(主 / 子 agent) + +- `subAgent` / `switchAgentVerification` 以统一入口为唯一事实源:**Cursor/Claude** → `rules/f2s-flow2spec-unified-entry.*`;**Codex** → `.codex/topics/f2s-flow2spec-unified-entry.md`。 +- **步骤 1(续作分诊 + 解析)**:主 agent 必做 `f2s-task`「任务开始」1–2;解析文档可拆子 agent(只读)。 +- **步骤 2(草稿确认)**:必须主 agent;未确认前禁止创建 `.task/` 或写业务代码。 +- **步骤 3(落盘)**:按 `f2s-task`「任务开始」3.a–3.f;`todo.json` **仅主 agent**;`task.md` / `context.md` / `user-todos.md` 初稿可子 agent,`user-todos.md` 执行中追加由主 agent 合并。 +- **步骤 4(实现)**:子 agent 只写业务代码;**禁止**子 agent 写 `todo.json`、改 `task.md` checkbox;打钩由主 agent 在合并后当步完成。 +- **步骤 5(归档)**:主 agent;**仅**满足 `f2s-task`「任务完成」归档门禁后执行。 +- worktree 卫生见 `f2s-flow2spec-unified-entry`;中断/结束见 `f2s-task`「中断与会话结束」。 + +## 输入(任选其一) + +- 技术方案路径(`.Knowledge/req-docs/*.md` 或 PDF) +- 需求 / 变更描述(自由文本) + +## 步骤 + +### 步骤 0:前置(强制,任何步骤之前) + +1. **`Read("flow2spec.config.json")`**(项目根;缺失字段视为 `false`)。 +2. **`Read` 当前客户端生成入口中的 `f2s-task` 全文**(不得跳过;不得仅用本 SKILL 摘要代替)。 +3. 按读到的 `subAgent` / `switchAgentVerification` 决定下文是否拆子 agent、是否交叉校验。 + +### 步骤 1:续作分诊 + 解析输入 + +#### 1a. 续作分诊(`f2s-task`「任务开始」1–2,主 agent) + +1. 若存在 **`.task/todo.json`**,`Read` 并将**用户本条输入**与各条目 **`keywords`** 匹配。 +2. **命中 1 个** → `Read` 对应 `task.md`、`context.md`;若存在则 `Read` **`user-todos.md`**;向用户展示剩余 checklist 与未勾用户代办;询问是否**续作**该任务。 + - 用户确认续作 → **加载本 SKILL 全文**(`linkedSkill` 应为 `f2s-req-plan`),从 `task.md` 首个 `[ ]` 继续;**禁止**新建重复 `active/` 目录;**跳至步骤 4**(若仍需补充规划,先在「## 备注」记录后再实现)。 + - 用户明确要**新任务** → 进入 1b。 +3. **命中多个** → 列出候选,让用户选择续作哪一个或新建。 +4. **无命中** → 检查**孤儿 `active/`**(`f2s-task`):若有未归档且含 `[ ]` 的 `task.md`,提示是否续作或恢复 `todo.json`;否则进入 1b。 +5. **无 `todo.json`** → 进入 1b。 + +#### 1b. 解析输入(新任务或待草稿) + +`subAgent=true` 时可拆子 agent 并行只读: + +- 读取方案/需求全文,提取目标、范围、工作项、涉及文件 +- 读取 `.Knowledge/stock-docs/` 等对齐上下文 +- PDF 先 `f2s-doc-pdf` 转 MD + +子 agent 只交「解析摘要」;`subAgent=false` 时主 agent 完成。→ **步骤 2**。 + +### 步骤 2:输出草稿并确认(必须主 agent) + +主 agent 输出: + +1. **任务名称**(snake_case) +2. **实现清单草稿**(每步可 checkbox,将写入 `task.md` 的「## 步骤」) +3. **涉及文件列表**(将写入 `context.md`) +4. **建议 `keywords`**(2–5 个,供 `todo.json` 续作匹配) +5. **等待用户确认** + +> **未确认前**禁止:创建 `.task/`、写 `todo.json`、写业务代码。 + +### 步骤 3:落盘任务清单(`f2s-task`「任务开始」3.a–3.f) + +用户确认后,**严格按 `f2s-task` 执行**(格式以该规则正文为准,不得省略文件): + +| 子步 | 动作 | 写权 | +| --- | --- | --- | +| 3.a | 确认 ``(snake_case) | 主 | +| 3.b | 创建 `.task/active//` | 主或子(初稿) | +| 3.c | 写入 **`task.md`**:`# 任务名` + `## 步骤` + `- [ ]` 列表 + 空 `## 备注` | 主或子 | +| 3.d | 写入 **`context.md`**:涉及文件、`.Knowledge` 资料链接;用户代办指向 `user-todos.md` | 主或子 | +| 3.e | 创建 **`user-todos.md`**(固定文件名;无代办时写占位说明) | 主或子 | +| 3.f | **`todo.json` 新增条目**:`name`、`folder`、`keywords`(含步骤 2 建议词)、`linkedSkill: "f2s-req-plan"`、`createdAt` | **仅主 agent** | + +**禁止**:只建 `task.md` 不写 `todo.json`;省略 `user-todos.md`;使用 `completed/-` 旧式归档名。 + +### 步骤 4:实现代码 + +遵守 `f2s-task`「**执行中**」「**中断与会话结束**」: + +- 按 `task.md` 顺序实现;**每真实完成一步**,主 agent **立即** `Edit` 该步 `[ ]` → `[x]`(禁止批量勾选、禁止仅口头完成)。 +- 凡须用户改库/配环境/审批等,**同会话**追加 **`user-todos.md`**(按日期分节);禁止只写在对话或 `task.md` 正文。 +- `subAgent=true`:子 agent 只改业务源码;回报后由主 agent 打钩与写 `user-todos.md`。 +- 合并子 agent 后清理 **git worktree**(见统一入口)。 + +### 步骤 5:归档任务(`f2s-task`「任务完成」) + +**归档门禁**(自检通过后才移动目录): + +- `task.md`「## 步骤」中与本次交付相关项 **全部为 `[x]`**(取消项已在「## 备注」说明)。 +- 仍有 `[ ]` → **禁止**移入 `completed/`、**禁止**删 `todo.json` 条目。 + +通过后: + +1. `.task/active//` → `.task/completed/-/`(**日期 8 位在前**) +2. 从 `todo.json` 删除该条;空数组则删文件 +3. `user-todos.md` 随目录一并归档 + +### 步骤 6:输出摘要 + +```markdown +## f2s-req-plan 完成:<任务名> + +### 实现 +- <文件路径>:<改动说明> + +### 任务清单 +- 已归档:`.task/completed/-/`(或仍 active 时写明路径与剩余 `[ ]`) + +### 待办(知识库) +- 可后续调用 f2s-kb-sync / f2s-kb-feat + +### 用户代办 +- 见 `user-todos.md`(归档后在 completed 同路径) +``` + +## 约束 + +- **步骤 0**:必须先 `Read` `flow2spec.config.json` + 当前客户端入口中的 **`f2s-task` 全文** +- **`.task/`**:一律服从 `f2s-task`;本 SKILL 不得与之冲突 +- 不依赖 `changeTracking`,但**始终**创建并维护任务清单(除非续作已有 active 任务) +- 步骤 2 必须主 agent;未确认禁止落盘 +- `todo.json` 仅主 agent;子 agent 禁止写入 +- 禁止批量勾选;禁止跳过 `user-todos.md` + +## 完成后自检 + +1. 是否已读 **`f2s-task` 全文** 且落盘格式与其一致。 +2. `task.md` 步骤是否均已磁盘 `[x]`(非口头)。 +3. 归档门禁满足时目录在 `completed/-/`,`todo.json` 已更新。 +4. `user-todos.md` 与会话中用户代办一致(无则占位)。 +5. worktree 已清理或已交接删除命令(N/A 则注明)。 diff --git a/packages/core/templates/zh-CN/skills/f2s-req-tech/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-req-tech/SKILL.md new file mode 100644 index 0000000..88bacaf --- /dev/null +++ b/packages/core/templates/zh-CN/skills/f2s-req-tech/SKILL.md @@ -0,0 +1,82 @@ +--- +name: f2s-req-tech +description: 根据澄清后的需求基于项目知识库/Skills/Rules 生成技术方案文档;触发:生成技术方案、技术方案、f2s-req-tech +--- +> 执行口径:业务文档统一在 `/.Knowledge/`,本技能只产出 `.Knowledge/req-docs` 方案文档并参考 `.Knowledge` 内知识,不修改配置根 `rules/skills`。 + +## 编排(主 / 子 agent) + +- 两字段(`subAgent` / `switchAgentVerification`)语义以统一入口为唯一事实源:**Cursor/Claude** 读配置根 `rules/f2s-flow2spec-unified-entry.*`;**Codex** 读 `.codex/topics/f2s-flow2spec-unified-entry.md`(与上同源,`flow2spec init` 镜像)。本技能不复述。 +- **拆子前提(硬约束)**:当 `subAgent=true` 时,主 agent **必须先**抽取一份「项目约定摘要」作为子 agent 的强制上下文,覆盖:对外契约规范、错误与返回约定、异步/集成规范、数据与存储约定、工程结构、模块边界,合计 **< 80 行**。若未做该前置,**不拆子**——验收返工成本 > 拆子收益,强行拆子得不偿失。 +- **子职责**:多源只读(`.Knowledge/topics`、`stock-docs`、澄清后的 `req-docs`、模版)+ 按 `.Knowledge/template/技术方案模版.md` 写 `req-docs` 方案初稿。 +- **主职责**:契约定稿、对照模版与澄清文档验收、处理交付单元/流程一致性。 +- **校验**:默认落盘侧自验;本技能不绑定交叉校验。 + +# 根据需求生成技术方案文档 + +用户在对话中提供**已澄清的需求**(或需求摘要、PRD 路径),并可选择附带**需求条件**(如范围限定、必须/禁止使用的技术、端侧限定、优先级等)。你需要基于业务知识文档(`.Knowledge/`)和当前 agent 已加载的 rules/skills,输出一份可直接用于实现的技术方案文档。 + +**用途**:本技能产出的技术方案**供后续代码实现使用**,开发按该文档实现功能。不限于后端,适用于后端、前端、全栈、移动端、脚本工具等任意场景。不用于生成 Rules/Skills。 + +**结构范本**:技术方案按 `.Knowledge/template/技术方案模版.md` 中的**可选积木**按需组装输出。**不要硬套固定章节**:只写本次实现真正需要的交付单元、数据结构、配置、依赖、流程或异常处理;每个交付单元小节内同时说明契约/输入输出与必要处理流程,避免再单独拆「接口及流程说明」「关联调用流程」「流程说明」等大章重复描述同一单元。 + +--- + +## 输入 + +- **第一参数(必填)**:澄清后的需求描述,或**需求/PRD 文档路径**(如 `.Knowledge/req-docs/xxx.md`、`.Knowledge/stock-docs/需求_终稿.md`)。 +- **后续参数或用户补充(可选)**:需求条件与约束,例如: + - 范围(只做某模块、某端) + - 必须/禁止使用的技术栈、接口风格 + - 与现有某模块的边界 + - 性能、安全、合规要求 + +--- + +## 输出结构 + +生成文档时,**先读取 `.Knowledge/template/技术方案模版.md`** 作为结构参考,按需选用其中的章节积木;与需求无关的整节可省略,也可根据项目实际增加未列出的章节。 + +--- + +## 拆子前置(可选,仅当 `subAgent=true`) + +主 agent 在拆子前,必须产出「项目约定摘要」作为子 agent 的**强制输入**,否则**不拆子**。摘要篇幅上限 **< 80 行**,必须包含以下 6 类条款(技术栈无关,按项目实际填具体值): + +1. **对外契约规范**:接口 / 事件 / 消息 / 组件 / 脚本入口的命名、版本、鉴权、分页、通用返回字段等契约约定。 +2. **错误与返回约定**:错误码体系来源、前缀 / 分段规则、必选字段(如 code / message / data)、状态分层。 +3. **异步 / 集成规范**:消息队列 / 事件总线 / 定时任务 / 外部服务调用的命名、消费者组织、重试与幂等约定。 +4. **数据与存储约定**:库 / 表 / 字段 / 缓存 / 文件 / 搜索等命名规范、主键 / 索引 / 时间字段约定、分库分表策略(若有)。 +5. **工程结构**:模块分层(如 controller / service / dao / domain,或前端的 pages / components / hooks / store,或等价命名)与包路径 / 目录约定。 +6. **模块边界**:本方案涉及的既有模块与其他模块的调用 / 数据边界。 + +未完成该前置即拆子,视为违反硬约束;摘要完成后方可将子任务交付子 agent。 + +--- + +## 步骤 + +1. **澄清完备性前置门禁(硬约束)**:进入撰写前必须先判定当前需求是否**已澄清**: + - **已澄清**判据(满足其一即可):① **本轮由 `f2s-req-clarify` 自动衔接进入**,且澄清文档已落盘并作为输入路径传入(这是首选路径——用户可直接从 `f2s-req-clarify` 一路走到方案,同轮完成);② 用户显式提供 `.Knowledge/req-docs/*_需求澄清.md` 或等价澄清文档路径;③ 用户显式声明"已澄清 / 需求已明确 / 直接出方案";④ 用户提供的输入本身即完整 PRD(含范围、关键流程、边界、验收标准),且**当轮**不含明显未定义概念或矛盾。 + - **未澄清**信号(任一命中即视为未澄清):需求描述含"我理解为 / 我打算 / 大概 / 应该 / 待定 / 还没确定"等模糊语;接口 / 表 / 状态机 / 与既有模块的联动只给了"要做什么"未给"怎么算完";用户输入中已被 agent 或用户自己列出但未回答的 3 个及以上关键问题;且**本轮不是**从 `f2s-req-clarify` 衔接进入。 + - **未澄清则改走 clarify**:**禁止**在同一轮内直接进入撰写;应先转入 `f2s-req-clarify` 完成澄清落盘,然后按其自动衔接规则**回到本技能同轮继续**(这是设计的直连路径,不打断用户)。如无法转入 clarify(例如用户明确说"先只做技术方案 / 别做澄清"),列 3~6 条最影响方案落笔的澄清问题清单等用户回答,**不生成方案**。 +2. **读取需求**:从用户提供的路径或正文(或 `f2s-req-clarify` 衔接传入的澄清文档路径)获取需求内容;若有需求条件,一并纳入。 +3. **加载项目上下文**:主动读取并运用: + - `.Knowledge/topics/` 下与本次需求相关的主题规则/流程; + - `.Knowledge/stock-docs/` 下的背景文档与历史技术方案; + - **结构对照 `.Knowledge/template/技术方案模版.md`**。 +4. **对齐项目约定**:命名规范、目录结构、配置约定、消息队列、错误码、数据模型等与现有项目一致。 +5. **撰写文档**:按 `.Knowledge/template/技术方案模版.md` 按需选用章节积木书写;交付单元涉及行为逻辑时,在同一小节写清处理流程,避免交付物与流程两张皮。若启用拆子,子 agent 以「项目约定摘要」+ 澄清文档为强制输入,禁止自行扩展读取范围。 +6. **输出位置**:默认 `.Knowledge/req-docs/<方案名>_技术方案.md`;若用户指定路径则用该路径。 +7. **收口停步(硬约束)**:技术方案落盘后**只输出一行提示**「技术方案已就绪:`<路径>`;如需继续,可用 `f2s-req-plan` 拆任务、`implement-tech-design` 落地」,然后**停止**。**禁止**: + - 在同一轮内自动衔接 `f2s-req-plan` / `implement-tech-design` / 任何后续 `f2s-*` 技能(`f2s-req-clarify` → `f2s-req-tech` 是允许的**单跳**衔接,方案之后须由用户在**新一轮**触发下一步); + - 在方案文档尾部或紧随其后追加 `f2s-kb-distill` 收口提示(见 `rules/f2s-kb-feedback-closing.*` 禁止段——过程编排型技能落盘不触发 distill); + - 主动列"下一步 A/B/C 选一个"式路径清单诱导用户立即进入下一技能。 + +--- + +## 约束 + +- 所有路径相对于项目根目录(与 `.Knowledge` 同级)。 +- 不臆造与项目不符的约定;不确定时标注「待与项目约定确认」。 +- **原则**:交付单元小节按需包含契约(输入/输出)与处理流程,二者不拆章重复;结构以 `.Knowledge/template/技术方案模版.md` 为参考,按需取用,不硬套。 diff --git a/scripts/git-tag-version.js b/scripts/git-tag-version.js index b75a9ed..eb71c90 100644 --- a/scripts/git-tag-version.js +++ b/scripts/git-tag-version.js @@ -4,11 +4,23 @@ const { execFileSync } = require('child_process'); const path = require('path'); -const pkg = require(path.join(process.cwd(), 'package.json')); -const version = String(pkg.version || '').trim(); +const rootPkg = require(path.join(process.cwd(), 'package.json')); +const corePkg = require(path.join(process.cwd(), 'packages', 'core', 'package.json')); +const cliPkg = require(path.join(process.cwd(), 'packages', 'cli', 'package.json')); +const version = String(corePkg.version || '').trim(); if (!version) { - console.error('package.json version is empty'); + console.error('packages/core/package.json version is empty'); + process.exit(1); +} + +if (String(rootPkg.version || '').trim() !== version || String(cliPkg.version || '').trim() !== version) { + console.error('workspace package versions must match before tagging'); + process.exit(1); +} + +if (cliPkg.dependencies?.['@double-coding/flow2spec-core'] !== version) { + console.error('CLI Core dependency must match the release version before tagging'); process.exit(1); } diff --git a/scripts/test-core-api.js b/scripts/test-core-api.js new file mode 100644 index 0000000..6b963f9 --- /dev/null +++ b/scripts/test-core-api.js @@ -0,0 +1,41 @@ +"use strict"; + +const assert = require("assert"); +const fs = require("fs"); +const os = require("os"); +const path = require("path"); +const core = require("@double-coding/flow2spec-core"); + +async function main() { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "flow2spec-core-")); + const events = []; + const api = core.createFlow2Spec({ + cwd: tempDir, + onProgress: (event) => events.push(event), + }); + + assert.strictEqual(typeof api.project.init, "function"); + assert.strictEqual(typeof api.knowledge.check, "function"); + assert.strictEqual(core.getCapabilities().schema, "flow2spec.capabilities.v1"); + + await api.project.init({ mode: "native-host", integrations: ["dsh"], locale: "zh-CN" }); + assert.ok(fs.existsSync(path.join(tempDir, ".Knowledge"))); + assert.ok(fs.existsSync(path.join(tempDir, "flow2spec.config.json"))); + assert.ok(!fs.existsSync(path.join(tempDir, ".dsh"))); + + const report = api.knowledge.check({ strict: true }); + assert.strictEqual(report.ok, true); + const route = api.routing.match({ task: "flow2spec-dsh-adapter", request: "DeepSeek Harness" }); + const expanded = api.routing.expand(route); + assert.ok(expanded.topics.includes("flow2spec-dsh-adapter")); + assert.strictEqual(api.routing.verify(expanded).ok, true); + assert.ok(api.resources.listSkills().some((file) => file.endsWith("f2s-kb-sync/SKILL.md"))); + assert.ok(Array.isArray(events)); + + console.log("test-core-api: ok"); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/scripts/test-package-install.js b/scripts/test-package-install.js new file mode 100644 index 0000000..48928b5 --- /dev/null +++ b/scripts/test-package-install.js @@ -0,0 +1,46 @@ +"use strict"; + +const assert = require("assert"); +const fs = require("fs"); +const os = require("os"); +const path = require("path"); +const { execFileSync } = require("child_process"); + +function run(command, args, options = {}) { + const executable = process.platform === "win32" && command === "npm" ? "npm.cmd" : command; + return execFileSync(executable, args, { + cwd: path.resolve(__dirname, ".."), + encoding: "utf8", + stdio: "pipe", + shell: process.platform === "win32", + ...options, + }); +} + +const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "flow2spec-pack-")); +run("npm", ["pack", "--workspace", "@double-coding/flow2spec-core", "--pack-destination", tempDir]); +run("npm", ["pack", "--workspace", "@double-coding/flow2spec", "--pack-destination", tempDir]); + +const tarballs = fs.readdirSync(tempDir).filter((file) => file.endsWith(".tgz")); +assert.strictEqual(tarballs.length, 2, "expected Core and CLI tarballs"); +run("npm", [ + "install", + "--ignore-scripts", + "--no-audit", + "--no-fund", + ...tarballs.map((file) => path.join(tempDir, file)), +], { cwd: tempDir }); + +const coreProbe = path.join(tempDir, "core-probe.js"); +fs.writeFileSync( + coreProbe, + "const core = require('@double-coding/flow2spec-core');\n" + + "if (core.getCapabilities().protocolVersion !== 1) process.exit(1);\n", + "utf8", +); +const coreCheck = run(process.execPath, [coreProbe], { cwd: tempDir }); +assert.strictEqual(coreCheck, ""); +const cliPath = path.join(tempDir, "node_modules", "@double-coding", "flow2spec", "cli.js"); +assert.ok(fs.existsSync(cliPath)); +run(process.execPath, [cliPath, "--help"], { cwd: tempDir }); +console.log("test-package-install: ok");