diff --git a/.Knowledge/matchers/m-flow2spec-core-package.json b/.Knowledge/matchers/m-flow2spec-core-package.json index f279b76..3dc4acb 100644 --- a/.Knowledge/matchers/m-flow2spec-core-package.json +++ b/.Knowledge/matchers/m-flow2spec-core-package.json @@ -9,7 +9,18 @@ "createFlow2Spec", "legacy shim", "模板真源", - "CLI 依赖 core" + "CLI 依赖 core", + "独立版本", + "Core-only", + "Template Version", + "version:set:cli", + "version:set:core", + "version:set:template", + "version:check", + "独立发布", + "core-v", + "cli-v", + "Core 更新检测" ], "id": "m-flow2spec-core-package", "version": "1.0.0", diff --git a/.Knowledge/topics/f2s-dev-workflow-constraints.md b/.Knowledge/topics/f2s-dev-workflow-constraints.md index 2675670..880ae40 100644 --- a/.Knowledge/topics/f2s-dev-workflow-constraints.md +++ b/.Knowledge/topics/f2s-dev-workflow-constraints.md @@ -1,72 +1,41 @@ --- id: f2s-dev-workflow-constraints -revision: 2 -summary: "f2s-dev-workflow-constraints(路由摘要)" +revision: 3 +summary: "Flow2Spec 本仓的模板真源、配置根、版本发布与分发边界" primary: policy confidence: inferred --- # f2s-dev-workflow-constraints(路由摘要) -> **仅适用于 Flow2Spec 本仓自身**。**不给下游使用**——本 topic 与对应 rules / skill 都**只**存在于本仓,**不进 `templates/`**。 +## 适用场景 -## 作用 +Flow2Spec 本仓开发时判断应改 Core templates、配置根、本仓知识库还是发布脚本;普通下游仓不使用本主题。 -约束在 Flow2Spec 本仓内开发时的写盘边界与分发口径,避免「手改配置根被 init 覆盖」「未经用户同意主动跑 init」「手改 core templates 副本」。 +## 长文规则 -## 长文位置(本仓配置根手写,不进 templates) +- Claude:`.claude/rules/repo-dev-workflow-constraints.md` +- Cursor:`.cursor/rules/repo-dev-workflow-constraints.mdc` +- Codex:`.codex/topics/repo-dev-workflow-constraints.md` -- **Cursor**:`.cursor/rules/repo-dev-workflow-constraints.mdc` -- **Claude**:`.claude/rules/repo-dev-workflow-constraints.md` -- **Codex**:`.codex/topics/repo-dev-workflow-constraints.md` +三份是同源手写镜像,执行时读取当前客户端对应版本。 -三份为同源手写副本;执行时按当前 agent 打开对应端。 +## 核心约束 -## 核心约束(一句一条) +1. 下游会使用的 Rule、Skill、Hook、AGENTS、配置与知识模板只改 `packages/core/templates/{zh-CN,en-US}/`;该目录是受 Git 管理的唯一模板真源。 +2. 配置根中有 Core template 对应源的文件属于 init 产物,不直接手改。 +3. 本仓专属 `repo-*` Rule/Skill 不进入 Core templates,直接同步三端配置根与本仓知识库。 +4. Agent 不主动运行 `flow2spec init` / `npm run sync:agents`,由用户明确触发分发。 +5. 根 `lib/`、根 `templates/` 与模板复制脚本不再使用。 -1. **只改 `templates/`**:所有下游会用到的规则/技能/主题/matcher/manifest 模板改动,只落 `templates/zh-CN/` 与 `templates/en-US/`(workspace 根)。 -2. **不改配置根(下游会用到的产物)**:`.claude/rules/` 等中在 `templates/` 有对应源的文件属 `init` 产物,手改会被覆盖。 -3. **本仓专属手写例外**:`f2s-dev-workflow-constraints`、`repo-dev-check` 等**只**在本仓的规则/技能,直接落配置根,**不进** `templates/`。 -4. **用户驱动分发**:Agent **不主动**跑 `flow2spec init` / `npm run sync:agents`;由用户执行。 -5. **不改 `packages/core/templates/`**:它是 `scripts/sync-core-templates.js` 从根 `templates/` 自动生成的 npm 发布物副本,已进 `.gitignore`,下次同步会被覆盖。 +## 版本与发布 -## 模板双份关系(重要) - -- **根 `templates/`**:唯一手写事实源,进 git,日常改这里。 -- **`packages/core/templates/`**:npm 发布副本,不进 git,由 `scripts/sync-core-templates.js` 生成;运行时 `packages/core/index.js` 通过 `__dirname` 读它,因此 core 包必须自带这份(否则用户 `npm install` 后 `flow2spec init` 会 `ENOENT`)。 -- 自动同步触发点:`sync:agents` / `test` / `pack:check` 静默同步;`packages/core` 的 `prepack` 在 `npm pack` / `npm publish` 前同步;`sync:core-templates:check` 做纯校验。 - -## 分发命令(用户执行) - -```bash -npm run sync:agents -# 或 -node ./cli.js init codex claude cursor -# 或全局装了 -flow2spec init codex claude cursor -``` - -## 自查技能 - -`repo-dev-check` 用于本仓提交前自查写盘边界与分发口径。触发词见该 SKILL。 - -## 适用场景 / 触发词 - -- 用户提到「templates vs 配置根」「模板 vs 落盘」 -- 用户说「不要改配置根 / 只改 templates / 我来跑 init / 我会自己 sync:agents」 -- Agent 打算改配置根下与 `templates/` 有对应源的文件时须先读本 topic 与对应 rules 长文 -- 用户问「根 templates 与 core templates 是什么关系」「core templates 冗余吗」 -- 分发口径澄清 +- CLI、Core、Template、Protocol 独立版本;CLI 用 caret range 约束 Core。 +- Core-only 兼容更新不升 Template Version,也不触发知识库升级。 +- `core-vX.Y.Z` 与 `cli-vX.Y.Z` 分别发布对应包。 +- 发布前执行版本、打包、tarball 安装与 README 门禁。 ## 边界与禁止项 -- **仅本仓适用**:下游项目不承担本 topic 约束,不 `Read` 也不生效。 -- **不写下游可见位置**:本 topic 与关联 rules/skill 一律不落 `templates/`;`f2s-kb-upgrade` 步骤 -1 / 步骤 2 的 init 分发**不带**这些文件到下游。 -- `LOCAL_CONTEXT.md`、`.claude/memory/` 为本地不入库文件,不视为配置根。 -- **禁止手改 `packages/core/templates/`**——它由脚本从根 templates 生成,下次同步会覆盖。 - -## npm workspace 发布门禁 - -- 主 CLI 包 `@double-coding/flow2spec` 的 `packages/cli/README.md` 与根 `README.md` 保持完全一致;Core 包维护面向程序化调用的独立 README。 -- Core、CLI 与 workspace 根版本保持一致,CLI 固定依赖同版本 Core。 -- 发布前运行 `npm run pack:check` 与 `node scripts/test-package-install.js`,并从 CLI tarball 校验 README,确保 npm 页面保留完整产品文档。 -- `packages/core/templates/` 由 `scripts/sync-core-templates.js` 生成,`prepack` 自动触发,tarball 内始终包含最新模板;CI/pre-release 建议跑 `npm run sync:core-templates:check` 做漂移检查。 +- 本主题和长文规则只服务 Flow2Spec 本仓,不进入下游模板。 +- 禁止恢复双模板根或版本锁步模型。 +- 禁止用 Core Version 覆盖 `manifest-routing.json.version`;该字段表示 Template Version。 diff --git a/.Knowledge/topics/f2s-init-defaults.md b/.Knowledge/topics/f2s-init-defaults.md index b44cb0e..10827c3 100644 --- a/.Knowledge/topics/f2s-init-defaults.md +++ b/.Knowledge/topics/f2s-init-defaults.md @@ -1,6 +1,6 @@ --- id: flow2spec-init-defaults -revision: 0 +revision: 1 summary: "flow2spec-init-defaults(路由摘要)" primary: config confidence: inferred @@ -18,10 +18,10 @@ tags: [policy] | 落点 | 角色 | 路径 | | --- | --- | --- | -| `DEFAULTS` 常量 | `loadFlow2specConfig` 兜底值,缺字段时回填 | `lib/flow2specConfig.js` | -| `CONFIG_FIELDS[].default` | init 交互问答按回车的默认值 | `lib/flow2specConfig.js` | -| 包模板 `flow2spec.config.json` | 新项目首次落盘的整文件模板(按 `locale` 选择) | `templates/{zh-CN,en-US}/flow2spec.config.json` | -| `renderProjectConfigBlock()` | Codex `AGENTS.md` 中字段语义表的「init 默认」列 | `lib/codexAgentsAdapter.js` | +| `DEFAULTS` 常量 | `loadFlow2specConfig` 兜底值,缺字段时回填 | `packages/core/lib/flow2specConfig.js` | +| `CONFIG_FIELDS[].default` | init 交互问答按回车的默认值 | `packages/core/lib/flow2specConfig.js` | +| 包模板 `flow2spec.config.json` | 新项目首次落盘的整文件模板(按 `locale` 选择) | `packages/core/templates/{zh-CN,en-US}/flow2spec.config.json` | +| `renderProjectConfigBlock()` | Codex `AGENTS.md` 中字段语义表的「init 默认」列 | `packages/core/lib/codexAgentsAdapter.js` | 不一致的后果: @@ -35,7 +35,7 @@ tags: [policy] | 字段 | 类型 | 当前默认值 | 语义 | | --- | --- | --- | --- | -| `locale` | `"zh-CN"` / `"en-US"` | `"zh-CN"` | 模板语言;决定首次落盘读哪份 `templates//` | +| `locale` | `"zh-CN"` / `"en-US"` | `"zh-CN"` | 模板语言;决定首次落盘读哪份 Core package template | | `subAgent` | boolean | `true` | 是否允许技能拆子 agent;`false` 时主 agent 全流程;详见 `f2s-flow2spec-unified-entry` | | `switchAgentVerification` | boolean | `true` | 切换 agent 校验;`true` 且技能正文绑定时启用交叉校验;旧键 `subAgentVerification` 仍兼容 | | `intentRecognition` | boolean | `true` | 高置信操作意图是否按 `f2s-intent-routing` 自动进入对应 `f2s-*` 技能 | @@ -44,7 +44,7 @@ tags: [policy] | `changeTracking.implement` | boolean | `true` | `f2s-implement-tech-design` 是否走 `.task/` 变更追踪 | | `updateCheck.enabled` | boolean | `true` | 是否启用每日版本更新提示 | -「当前默认值」一栏以包模板 `templates/zh-CN/flow2spec.config.json` 为锚,作变更前先核对该文件。 +「当前默认值」一栏以包模板 `packages/core/templates/zh-CN/flow2spec.config.json` 为锚,作变更前先核对该文件。 ### 默认值变更记录 @@ -69,20 +69,14 @@ tags: [policy] 含义:升级 npm 包后新增的字段,会以「当前 `CONFIG_FIELDS` 默认值」补齐到老项目;如新版本默认值翻面(例如某字段由 `false` 改为 `true`),**只影响**升级时**仍缺该字段**的老项目,已写过该字段的项目不变。 -### 跑 `f2s-kb-upgrade` 时怎么让全局 flow2spec 保持最新 +### 跑 `f2s-kb-upgrade` 时的版本预检 -`f2s-kb-upgrade` SKILL 的「步骤 -1」(先于一切)**先做前台探测再决定是否升级**:主 agent 顺序跑 `flow2spec --version` + `npm view @double-coding/flow2spec version` + `command -v npx`,按 3 分支处理: +步骤 -1 执行 `flow2spec version` 与 `flow2spec update --check`,分别读取 CLI、Core、Core Range、Template、Protocol 和 npm 最新 Core/Template: -- **A. 已装且是 latest** → **完全跳过**升级;步骤 2 命令首选 `flow2spec init `。 -- **B. 已装但落后** → 派**独立子 agent** 后台跑 `npm i -g @double-coding/flow2spec@latest`(fire-and-forget,不等待,不阻塞主流程);步骤 2 命令用 `npx @double-coding/flow2spec@latest init ` 保证本次拿到 latest 模板。 -- **C. 未装 / latest 未知** → 同 B 处理;探测全部失败时可放弃步骤 -1,由 cli.js 兜底。 - -口径: - -- **仅** B/C 时才派子 agent;派子**不受** `flow2spec.config.json.subAgent` 字段约束(全局 npm 装包不属业务拆分范畴); -- 派子是 fire-and-forget,结果不进入 SKILL 结论摘要——失败也只是"下次再升一次"; -- 与 `cli.js` 内 `maybeAutoUpdateGlobalInstall()` 不冲突:前者是 SKILL 入口按需异步派工,后者是 `init` 收尾兜底;两次都成功就是 no-op。 -- 用户自查用 `flow2spec --version`(当前全局版本)和 `flow2spec update`(CLI 内置自更新)。 +- Core-only 更新:`flow2spec update --core` 后幂等 init 刷新 Hook,不进入完整知识库升级。 +- Template 更新且 Core 兼容:更新 Core 后继续 init 与 `projectRev` / `pkgRev` 分流。 +- 本地版本不可用或 Core 超出范围:显式组合 latest CLI/Core 执行 init,避免 npx 复用旧 Core。 +- `manifest-routing.json.version` 表示 Template Version,不能与 Core Version 混用。 ## init 不动哪些目录 diff --git a/.Knowledge/topics/flow2spec-core-package.md b/.Knowledge/topics/flow2spec-core-package.md index 381872a..4cee7ac 100644 --- a/.Knowledge/topics/flow2spec-core-package.md +++ b/.Knowledge/topics/flow2spec-core-package.md @@ -1,7 +1,7 @@ --- id: flow2spec-core-package -revision: 0 -summary: "@double-coding/flow2spec-core 的职责边界:核心实现、发包模板真源与原生插件 API" +revision: 2 +summary: "Core/CLI 所有权、公共 API、独立版本、独立发布与更新兼容契约" primary: module confidence: inferred --- @@ -9,19 +9,47 @@ confidence: inferred ## 适用场景 -回答或维护 npm workspace 包结构、`flow2spec-core` 的职责、CLI 与 core 的依赖关系、原生插件 API、发包模板真源位置、根目录 legacy shim。 +维护 npm workspace 包结构、Core 公共 API、CLI/Core 依赖、模板真源、独立版本、发布 tag、更新检测与兼容范围。 -## 核心事实 +## 所有权 -- workspace 三层:根 `flow2spec-workspace`(private)+ `packages/core`(`@double-coding/flow2spec-core`)+ `packages/cli`(`@double-coding/flow2spec`);三者版本保持一致,CLI 对 core 的依赖固定为同一版本。 -- **core 承载全部核心实现**(`packages/core/lib/`):`init` 落盘引擎、各端适配器(`claudeRulesAdapter` / `claudeSettingsAdapter` / `codexAgentsAdapter` / `dshAgentsAdapter`)、知识库引擎与路由(`knowledgeEngine` / `routing`)、`updateCheck`、`doctor`、`developerId`、`flow2specConfig`、`resources`。 -- **npm 发布的模板真源**在 `packages/core/templates/{zh-CN,en-US}/`(`package.json` 的 `files` 含 `templates`);根 `templates/` 是本仓同步副本,模板改动须双根同步(写盘边界见 `f2s-dev-workflow-constraints`)。 -- **原生插件 API**:`createFlow2Spec({ cwd })` 暴露 `resources.skillCatalog()`(宿主适配的结构化 Skill 清单)、`resources.unifiedEntry()`(宿主适配的统一入口)、`update.check()`(复用 `.Knowledge/update-check.json` 每日缓存);`capabilities.json` 的 `protocolVersion` 供插件启动时做兼容校验;`index.d.ts` 导出完整公共契约类型。 -- **CLI 是薄壳**:`packages/cli/cli.js` 只做命令行解析并转调 core;根 `cli.js` 一行转发 `require("./packages/cli/cli.js")`。 -- **根 `lib/*.js` 为 legacy shim**:逐文件 `module.exports = require("@double-coding/flow2spec-core").legacy.`,仅保留拆包前旧引用路径的兼容。 -- 普通用户只装 CLI 或原生插件(如 DeepSeek Harness 的 `@double-coding/flow2spec-deepseek-harness`),core 作为依赖按锁定版本自动带上。 +- 根 private workspace 只负责开发编排,不作为 Core 运行时依赖声明位置。 +- `packages/core/lib/` 承载核心实现;`packages/core/templates/{zh-CN,en-US}/` 是受 Git 管理的唯一模板真源并随 Core tarball 发布。 +- 根 `lib/`、根 `templates/` 与 `scripts/sync-core-templates.js` 均不存在。 +- CLI 是薄壳,只通过 `@double-coding/flow2spec-core` 的公共 API 工作;当前运行时范围为 `^3.5.0`。 -## 边界与注意 +## 公共 API -- 本仓开发态下 `packages/cli` → core 的模块解析依赖根 `node_modules/@double-coding/` 的 workspace 软链;fresh clone 或拆包合入后未执行 `npm install` 时会报 `Cannot find module '@double-coding/flow2spec-core'`。下游 npm 安装不受影响(`scripts/test-package-install.js` 从 tarball 真装验证)。 -- `packages/core/README.md` 面向 CLI 与原生插件开发者独立维护;`packages/cli/README.md` 必须与根 `README.md` 完全一致(发布门禁见 `f2s-dev-workflow-constraints`)。 +- `createFlow2Spec({ cwd })` 暴露项目初始化、配置、路由、知识库、协作、doctor、resources 与 update API。 +- `getVersions()` 返回 `coreVersion`、`templateVersion`、`protocolVersion`。 +- `capabilities.json.protocolVersion` 表示公共协议兼容级别,不表示包版本或模板版本。 +- `legacy` 暂时保留 Core 包内的旧消费兼容面;CLI 和仓库测试不再依赖根 shim。 + +## 独立版本 + +```text +CLI Version packages/cli/package.json +Core Version packages/core/package.json +Template Version packages/core/package.json.templateVersion +Protocol Version packages/core/capabilities.json.protocolVersion +``` + +- `version:set:cli` 只更新 CLI,可显式提高 `--core-range`。 +- `version:set:core` 只更新 Core,并拒绝落到当前 CLI 范围之外。 +- `version:set:template` 更新 Core 元数据及中英文 `manifest-routing.json.version`。 +- `version:check` 校验 semver range、lockfile、双语 Template Version、Protocol Version 与 release tag。 + +## 发布与更新 + +- `core-vX.Y.Z` 只发布 Core;`cli-vX.Y.Z` 只发布 CLI。同时发布时先 Core 后 CLI。 +- `flow2spec version` 展示 CLI/Core/Core Range/Template/Protocol。 +- `flow2spec update --check|--cli|--core` 分别检查、更新 CLI、更新兼容 Core。 +- Hook 与 `update.check()` 同时返回 Core 与 Template 状态。 +- Core-only 更新:更新 Core 后幂等 init 刷新 Hook,不进入 `f2s-kb-upgrade`。 +- Template 更新:更新 Core、执行 init,再由 `projectRev` / `pkgRev` 决定是否进入完整知识库升级。 + +## 边界 + +- Core 新版必须落在 CLI caret range 内;超出范围先升级 CLI。 +- `.Knowledge/manifest-routing.json.version` 表示 Template Version,不能与 Core Version 混用。 +- 包安装验收使用两包 tarball,并验证 Core templates、类型声明、CLI README 与启动行为。 diff --git a/.claude/rules/repo-dev-workflow-constraints.md b/.claude/rules/repo-dev-workflow-constraints.md index c754936..7e64548 100644 --- a/.claude/rules/repo-dev-workflow-constraints.md +++ b/.claude/rules/repo-dev-workflow-constraints.md @@ -1,152 +1,89 @@ --- -description: Flow2Spec 项目开发纪律(仅本仓适用):修改本仓时只改 templates/,配置根由 flow2spec init / sync:agents 分发,禁止手改配置根 +description: Flow2Spec 本仓开发纪律:Core 持有模板真源,配置根由 init 分发,CLI/Core/Template 独立版本与发布 --- # Flow2Spec 项目开发纪律(Dev Workflow Constraints) -> **仅适用于 Flow2Spec 本仓自身的开发**。 +> **仅适用于 Flow2Spec 本仓自身开发,不下发给普通业务仓。** > -> **不给下游使用**:因此本规则 / 对应 skill / 对应 topic 摘要**只**存在于本仓的配置根(`.claude/` / `.cursor/` / `.codex/`)与本仓 `.Knowledge/topics/`,**不落 `templates/`**——`flow2spec init` 不会把本文件分发给下游项目。 -> -> **单一事实源**:本文件为 **`f2s-dev-workflow-constraints`** 的完整约定。 +> 三端同源手写镜像: > - Claude:`.claude/rules/repo-dev-workflow-constraints.md` > - Cursor:`.cursor/rules/repo-dev-workflow-constraints.mdc` > - Codex:`.codex/topics/repo-dev-workflow-constraints.md` -> - 本仓路由摘要:`.Knowledge/topics/f2s-dev-workflow-constraints.md` -> -> **命名对齐**:**配置根文件名**统一用 **`repo-*`** 前缀(避开 `init` 的 `f2s-` 自清理触发面);**topic id / 路由概念名**保留 **`f2s-dev-workflow-constraints`**(`manifest-routing.topicPaths` / `topicDependencies` / `topicMetadata` / matcher 已登记的稳定 id,改会连锁破坏路由并让历史里程碑失真)。两处字面不同,指向**同一条规则**。 -> -> 三端配置根内容为同源手写副本;本仓内改动须**三端同步落盘**。 - -## 模板双份关系(重要背景) +> - 路由摘要:`.Knowledge/topics/f2s-dev-workflow-constraints.md` -本仓有两份 templates 目录,职责不同,**不能手工双写**: +## 所有权边界 -| 位置 | 角色 | 谁写 | +| 位置 | 角色 | 写入方式 | | --- | --- | --- | -| workspace 根 `templates/` | **唯一手写事实源**;日常改模板只改这里 | **人** / Agent | -| `packages/core/templates/` | **npm 发布物副本**;`@double-coding/flow2spec-core` 打包时带上,运行时被 `packages/core/index.js` 用 `path.join(__dirname, "templates", ...)` 读取;不进 git | **`scripts/sync-core-templates.js` 自动生成** | - -为什么 core 必须有独立副本:`npm install @double-coding/flow2spec-core` 装到用户机器上时,只有 `packages/core/` 目录进入 tarball——workspace 根 templates 不在其中。core 包内必须自带一份 templates,否则用户 `flow2spec init` 会 `ENOENT` 崩溃。 - -自动同步链路: - -- `packages/core/templates/` 已加入 `.gitignore`,不进 git。 -- `packages/core/package.json.scripts.prepack` 调用 `sync-core-templates.js`——`npm pack` / `npm publish` 前自动从根 templates 同步到 core templates,保证 tarball 完整。 -- 根 `package.json` 中 `sync:agents` / `test` / `pack:check` 都先跑一次同步(`--quiet` 静默模式),本地 dev 也不会碰到"core/templates 找不到"的启动错。 -- 想快速核对是否漂移:`npm run sync:core-templates:check`(不写盘,纯校验,退出码非零即有漂移)。 -- 想手动重新同步:`npm run sync:core-templates`。 +| `packages/core/templates/{zh-CN,en-US}/` | 下游 Rule、Skill、Hook、知识模板的唯一真源;随 Core tarball 发布 | 人或 Agent 直接维护并提交 Git | +| `.claude/` / `.cursor/` / `.codex/` / 根 `AGENTS.md` | 本仓消费模板后的配置根产物 | 用户明确要求后由 `flow2spec init` / `npm run sync:agents` 分发 | +| `.Knowledge/` | 本仓共享知识库 | 按 topic/skill 写权直接维护 | +| 本仓专属 `repo-*` Rule/Skill | 只服务本仓,不进入 Core templates | 三端配置根手写镜像 | -## 硬约束一览 +根 `templates/` 与根 `lib/` 均不再存在:模板和核心实现分别由 `packages/core/templates/`、`packages/core/lib/` 单独持有。`packages/core/templates/` 受 Git 管理,不依赖复制脚本、`.gitignore` 例外或 `prepack` 同步。 -| 约束 | 内容 | -| --- | --- | -| **1. 只改 `templates/` 与本仓知识库** | Flow2Spec 本仓下要给**下游用**的内容——规则 / 技能 / manifest / matchers / topics / knowledge 模板改动——**只落**在 `templates/zh-CN/` 与 `templates/en-US/` 下(含 `rules/`、`skills/`、`knowledge/`、`AGENTS.md`、`flow2spec.config.json` 模板)。**本仓自身**的 `.Knowledge/` 与配置根**下游用不到**的内容(如本规则、`repo-dev-check` skill)**直接**落配置根 + 本仓 `.Knowledge/topics/`。 | -| **2. 不改配置根(下游会用到的规则/技能)** | **禁止**直接编辑 `.claude/rules/` / `.claude/skills/` / `.cursor/rules/` / `.cursor/skills/` / `.codex/skills/` / `.codex/topics/` 中**由 `templates/` 派生**的文件、以及根目录 `AGENTS.md`——这些是 `flow2spec init` 的产物,手改会在下次 `init` / `sync:agents` 时被静默覆盖。**例外**:本规则、`repo-dev-check` skill 之类**只存在于本仓配置根**、**从不写入 `templates/`** 的文件不受此约束(它们本来就是配置根的原生手写内容)。 | -| **3. 用户驱动分发** | 模板改完后,Agent **不主动**跑 `flow2spec init` / `sync:agents`。默认交给用户执行;如果用户明确说「帮我 init」/「跑 sync」,才代跑。 | +## 硬约束 -## 为什么这么分 +1. 下游会使用的 Rule、Skill、Hook、AGENTS、配置与知识模板,只改 `packages/core/templates/zh-CN/` 和 `packages/core/templates/en-US/`;双语版本保持语义一致。 +2. 不直接编辑配置根中由 Core templates 派生的文件。它们会在后续 `init` 中被覆盖。 +3. 本规则、`repo-dev-check` 等本仓专属内容不进 Core templates,直接同步三端手写镜像与本仓知识库。 +4. Agent 不主动执行 `flow2spec init` / `npm run sync:agents`;只有用户明确要求分发时才执行。 +5. 根 `lib/` 不作为兼容入口。CLI 通过 `createFlow2Spec()` 等 Core 公共 API 工作;Core 内部测试可按需引用 `packages/core/lib/`。 -Flow2Spec 有一个让人容易混乱的特点:**它自己就是自己的第一位用户**——本仓开发时也遵循 Flow2Spec 规则,本仓的 `.claude/` / `.cursor/` / `.codex/` 是 `flow2spec init` 从 `templates/` 派生出来的产物。 +## 修改判断 -这就出现两类约束: +- 通用能力:改 `packages/core/templates/{locale}/...`,必要时同步 Core 实现与公开文档。 +- 本仓知识:改 `.Knowledge/topics/`、matcher、manifest/index;触达 topic 时先读 `f2s-topic-authoring`。 +- 本仓专属纪律:改三端 `repo-dev-workflow-constraints` 镜像,不写入 Core templates。 +- 配置根派生产物:不手改,也不为预览主动执行 init。 -- **A 类:下游也要遵守的通用规则**(比如 `f2s-git-commit`、`f2s-task`、`f2s-implement-tech-design`、`f2s-flow2spec-unified-entry` 等)——**放 `templates/`**,`init` 时分发到所有下游项目。 -- **B 类:只有 Flow2Spec 本仓自己需要的开发纪律**(比如「不要手改配置根」「用户来跑 init」)——**只放本仓配置根 + 本仓 `.Knowledge/topics/`**,`init` **不带**到下游。 +## 版本模型 -如果本规则写进 `templates/`,下游项目 `init` 时会拿到「不要手改配置根」这条——但下游的配置根就是它的运行时事实源,这条约束对它**毫无意义**且**会产生误导**。所以本规则严格保持在本仓内。 - -从数据流看: - -``` -本仓 templates/ ─┐ - ├── rules/ │ → flow2spec init → 三端配置根(同时供本仓消费 + 下游消费) - ├── skills/ │ - └── knowledge/ ─┘ - -本仓配置根手写内容(本文件、repo-dev-check skill) - └── 不进 templates,只服务本仓开发 +```text +CLI Version packages/cli/package.json +Core Version packages/core/package.json +Template Version packages/core/package.json.templateVersion +Protocol Version packages/core/capabilities.json.protocolVersion ``` -## 目录清单 - -### ✅ Agent / 人工都可以改 - -**通用(进 templates,会分发到下游)**: -- `templates/zh-CN/**` 与 `templates/en-US/**`:包括 - - `rules/*.md`(Cursor 端 `.mdc` 由 init 转换) - - `skills//SKILL.md` - - `AGENTS.md`(Codex 根条令模板) - - `flow2spec.config.json`(首次 init 模板) - - `knowledge/index.md` / `knowledge/topics/*.md` / `knowledge/matchers/*.json` / `knowledge/manifest-*.json` +- CLI 对 Core 使用运行时依赖 caret range(当前为 `^3.5.0`);Core 必须落在该范围内。 +- Core 兼容修复/新增 API 可只升 Core;Template Version 不变,不触发知识库升级。 +- Rule、Skill、Hook、知识模板变化时升 Core,并显式执行 `version:set:template`。 +- CLI 开始调用新版 Core API 时,升 CLI 并显式提高最低 Core range。 +- Protocol Version 只在公共协议不兼容时调整。 +- 根 private workspace version 不参与 npm 发布匹配。 -**本仓专属(不进 templates)**: -- `.Knowledge/topics/*.md`、`.Knowledge/index.md`、`.Knowledge/manifest-routing.json`、`.Knowledge/matchers/*.json`、`.Knowledge/stock-docs/*.md`、`.Knowledge/req-docs/*.md`:本仓自身知识库 -- **本仓专属规则 / 技能**:`.claude/rules/repo-dev-workflow-constraints.md`、`.cursor/rules/repo-dev-workflow-constraints.mdc`、`.codex/topics/repo-dev-workflow-constraints.md`、`.claude/skills/repo-dev-check/`、`.cursor/skills/repo-dev-check/`、`.codex/skills/repo-dev-check/`——这些**从不进 `templates/`**,直接手写落三端配置根 -- `docs/*.md` / `docs/en/*.md`、`lib/*.js`、`cli.js`、`scripts/*`、`package.json`、`README*.md` - -### ❌ Agent 禁止改(下游会用到的 init 产物) - -- `.claude/rules/`、`.claude/skills/` 中**由 `templates/` 派生**的文件(`f2s-config-check`、`f2s-flow2spec-unified-entry`、`f2s-task`、`f2s-kb-*` 等) -- `.claude/hooks/*.js`、`.claude/settings.json`(hook 段由 init 写) -- `.cursor/rules/*.mdc`、`.cursor/skills/*/SKILL.md`(除本仓专属条目外) -- `.cursor/hooks.json`、`.cursor/hooks/*.js` -- `.codex/topics/` 中**由 `templates/rules/` 镜像**的文件、`.codex/skills/` 中**由 `templates/skills/` 派生**的文件 -- `.codex/AGENTS.md`(指针文件)、`.codex/hooks.json`、`.codex/hooks/*.js` -- 根目录 `AGENTS.md`(Codex 完整条令,由 `buildCodexAgentsMd` 从 templates 拼装) - -**判定捷径**:如果一份文件在 `templates/{zh-CN,en-US}/` 下有对应源,就是「下游会用到的 init 产物」,禁止手改配置根版;否则就是「本仓专属手写内容」,可以直接改配置根版。 - -### 例外:本地不入库文件 - -- `.claude/memory/`:本地记忆,会话运行时状态,不进版本,不受约束 -- `LOCAL_CONTEXT.md`:本地上下文,不视为配置根 - -## Agent 行为准则 - -1. **判断改动性质**: - - 若是**下游也要用的规则 / 技能 / 主题模板**——路径必须落 **`templates/zh-CN/` + `templates/en-US/`**(zh/en 都改)。**禁止**用 `Edit` / `Write` 直接改配置根同名文件(哪怕当前工具的配置根就在旁边)。 - - 若是**本仓专属开发纪律 / 自查技能**(本文件、`repo-dev-check` 等)——直接手写 `.claude/` + `.cursor/` + `.codex/` 三端配置根 + `.Knowledge/topics/` 摘要。**不进 templates**。 -2. **用户指名要改 `.claude/rules/xxx.md`(且 xxx 在 templates 里有源)**:先澄清「这是 init 产物;改了会被覆盖;正确做法是改 `templates/{zh-CN,en-US}/rules/xxx.md`,然后跑 `npm run sync:agents` 或 `flow2spec init` 分发」。用户明确「就一次性临时改配置根 / 我知道会被覆盖」时才照办。 -3. **改完后**:**不主动**跑 `flow2spec init` / `sync:agents`。回复里点名「**已改 templates;请你执行 `npm run sync:agents` 或 `node ./cli.js init codex claude cursor` 分发到配置根**」。用户明确让代跑时才代跑。 -4. **发现「配置根有改动、templates 没改」**:先判定是不是本仓专属手写内容——是的话正常;如果 templates 里有对应源,多半是漂移,把配置根改动**回填**到 `templates/*/` 对应位置再由 `init` 从模板重新分发。 - -## 分发命令 - -从最短到最全: +版本命令: ```bash -# package.json 里配好的脚本 -npm run sync:agents - -# 直接调本仓 CLI(本地开发常用) -node ./cli.js init codex claude cursor - -# 全局装了 flow2spec 时 -flow2spec init codex claude cursor +npm run version:set:cli -- [--core-range ^x.y.z] +npm run version:set:core -- +npm run version:set:template -- +npm run version:check ``` -三条命令都会:从 `templates/{locale}/` 派生 → 写入配置根 `.claude/` / `.cursor/` / `.codex/` / 根 `AGENTS.md` / `.Knowledge/`(如首次)。当前 locale 取项目根 `flow2spec.config.json.locale`(本仓默认 `zh-CN`)。 - -**注意**:`init` **不会**动本仓专属手写文件(本规则、`repo-dev-check` skill)——这些文件在 `templates/` 里没有对应源,`init` 找不到就不写,用户手写的原始版本得以保留。 - -## npm workspace 发布门禁 +## 发布门禁 -- `@double-coding/flow2spec` 是面向普通用户的主包,`packages/cli/README.md` 必须与根 `README.md` 完全一致;根 README 变更后执行 `npm run sync:package-readme`。 -- `@double-coding/flow2spec-core` 面向 CLI 与原生插件开发者,维护独立的 `packages/core/README.md`,不复制主包的用户文档。 -- Core、CLI 与 workspace 根版本保持一致,CLI 对 Core 的依赖固定为同一版本。 -- 发布前执行 `npm run pack:check` 与 `node scripts/test-package-install.js`;安装测试必须从 CLI tarball 解出 `package/README.md` 并与根 README 比较,防止 npm 页面退化为占位说明。 -- **core templates 门禁**:`packages/core/templates/` 是 npm 发布物副本,由 `scripts/sync-core-templates.js` 从根 `templates/` 生成,进入 `.gitignore` 不入库。`packages/core/package.json.scripts.prepack` 会在 `npm pack` / `npm publish` 前自动同步;`npm run sync:core-templates:check` 可用来手动核对是否漂移(CI 也建议跑一遍)。**禁止**在 `packages/core/templates/` 里直接手改文件——下次同步会覆盖;改模板一律回到根 `templates/`。 +- `core-vX.Y.Z` 只测试、打包并发布 `@double-coding/flow2spec-core`。 +- `cli-vX.Y.Z` 只测试、打包并发布 `@double-coding/flow2spec`。 +- 同时发布时先 Core 后 CLI;禁止发布没有版本变化的包。 +- CLI README 与根 README 保持一致;Core README 独立维护。 +- 发布前运行 `npm run version:check`、`npm run pack:check`、`node scripts/test-package-install.js`。 +- `packages/core/templates/` 必须直接进入 Core tarball;不存在模板复制或漂移检查步骤。 -## 与其他规则的关系 +## 更新语义 -- **`f2s-flow2spec-unified-entry`**:讲「消费侧读取顺序」;本文件讲「本仓开发时的生产侧写盘边界」。两者并存不冲突。 -- **`f2s-topic-authoring`**:新增 / 改 `.Knowledge/topics/.md` 时先读该规则;对**下游用**的 topic 要落 `templates/*/knowledge/topics/`,对**本仓专属**的 topic 直接落 `.Knowledge/topics/`。 +- `flow2spec version` 展示 CLI、Core、Core Range、Template、Protocol。 +- `flow2spec update --check|--cli|--core` 分别检查、更新 CLI、更新兼容 Core。 +- Hook 同时比较 Core Version 与 Template Version。 +- Core 变化且 Template 不变:更新 Core 并执行一次幂等 init 刷新 Hook,不进入 `f2s-kb-upgrade`。 +- Template 变化:更新 Core、执行 init,再按 `projectRev` / `pkgRev` 判断是否进入 `f2s-kb-upgrade`。 ## 禁止项 -- 禁止 Agent 用 `Edit` / `Write` 直接改**在 `templates/` 里有对应源**的配置根文件(`.claude/rules/*`、`.claude/skills/*`、`.cursor/rules/*`、`.cursor/skills/*`、`.codex/topics/*`、`.codex/skills/*`、根 `AGENTS.md`)——即便当前工具的配置根就是它们。 -- 禁止把**本仓专属**规则 / 技能 / 主题写入 `templates/`(下游用不到,会造成噪音)。 -- 禁止 Agent 主动跑 `flow2spec init` / `npm run sync:agents` 分发;须由用户明确得令。 -- 禁止把本纪律当作「使用 flow2spec 的下游项目」的通用约束——**本文件仅针对 Flow2Spec 本仓自身的开发**。 -- **禁止手改 `packages/core/templates/`**——它是 `scripts/sync-core-templates.js` 从根 `templates/` 生成的副本,下次同步会被覆盖;改模板只改根 `templates/`。 +- 禁止恢复根 `templates/`、根 `lib/` 或 `sync-core-templates.js` 双根模型。 +- 禁止把 CLI、Core、Template 重新锁步成同一版本。 +- 禁止使用单一 `vX.Y.Z` tag 同时发布两个 npm 包。 +- 禁止把 Core 代码版本与 `.Knowledge/manifest-routing.json.version` 直接比较来决定知识库升级。 +- 禁止把本规则下发给普通业务仓。 diff --git a/.codex/topics/repo-dev-workflow-constraints.md b/.codex/topics/repo-dev-workflow-constraints.md index c754936..7e64548 100644 --- a/.codex/topics/repo-dev-workflow-constraints.md +++ b/.codex/topics/repo-dev-workflow-constraints.md @@ -1,152 +1,89 @@ --- -description: Flow2Spec 项目开发纪律(仅本仓适用):修改本仓时只改 templates/,配置根由 flow2spec init / sync:agents 分发,禁止手改配置根 +description: Flow2Spec 本仓开发纪律:Core 持有模板真源,配置根由 init 分发,CLI/Core/Template 独立版本与发布 --- # Flow2Spec 项目开发纪律(Dev Workflow Constraints) -> **仅适用于 Flow2Spec 本仓自身的开发**。 +> **仅适用于 Flow2Spec 本仓自身开发,不下发给普通业务仓。** > -> **不给下游使用**:因此本规则 / 对应 skill / 对应 topic 摘要**只**存在于本仓的配置根(`.claude/` / `.cursor/` / `.codex/`)与本仓 `.Knowledge/topics/`,**不落 `templates/`**——`flow2spec init` 不会把本文件分发给下游项目。 -> -> **单一事实源**:本文件为 **`f2s-dev-workflow-constraints`** 的完整约定。 +> 三端同源手写镜像: > - Claude:`.claude/rules/repo-dev-workflow-constraints.md` > - Cursor:`.cursor/rules/repo-dev-workflow-constraints.mdc` > - Codex:`.codex/topics/repo-dev-workflow-constraints.md` -> - 本仓路由摘要:`.Knowledge/topics/f2s-dev-workflow-constraints.md` -> -> **命名对齐**:**配置根文件名**统一用 **`repo-*`** 前缀(避开 `init` 的 `f2s-` 自清理触发面);**topic id / 路由概念名**保留 **`f2s-dev-workflow-constraints`**(`manifest-routing.topicPaths` / `topicDependencies` / `topicMetadata` / matcher 已登记的稳定 id,改会连锁破坏路由并让历史里程碑失真)。两处字面不同,指向**同一条规则**。 -> -> 三端配置根内容为同源手写副本;本仓内改动须**三端同步落盘**。 - -## 模板双份关系(重要背景) +> - 路由摘要:`.Knowledge/topics/f2s-dev-workflow-constraints.md` -本仓有两份 templates 目录,职责不同,**不能手工双写**: +## 所有权边界 -| 位置 | 角色 | 谁写 | +| 位置 | 角色 | 写入方式 | | --- | --- | --- | -| workspace 根 `templates/` | **唯一手写事实源**;日常改模板只改这里 | **人** / Agent | -| `packages/core/templates/` | **npm 发布物副本**;`@double-coding/flow2spec-core` 打包时带上,运行时被 `packages/core/index.js` 用 `path.join(__dirname, "templates", ...)` 读取;不进 git | **`scripts/sync-core-templates.js` 自动生成** | - -为什么 core 必须有独立副本:`npm install @double-coding/flow2spec-core` 装到用户机器上时,只有 `packages/core/` 目录进入 tarball——workspace 根 templates 不在其中。core 包内必须自带一份 templates,否则用户 `flow2spec init` 会 `ENOENT` 崩溃。 - -自动同步链路: - -- `packages/core/templates/` 已加入 `.gitignore`,不进 git。 -- `packages/core/package.json.scripts.prepack` 调用 `sync-core-templates.js`——`npm pack` / `npm publish` 前自动从根 templates 同步到 core templates,保证 tarball 完整。 -- 根 `package.json` 中 `sync:agents` / `test` / `pack:check` 都先跑一次同步(`--quiet` 静默模式),本地 dev 也不会碰到"core/templates 找不到"的启动错。 -- 想快速核对是否漂移:`npm run sync:core-templates:check`(不写盘,纯校验,退出码非零即有漂移)。 -- 想手动重新同步:`npm run sync:core-templates`。 +| `packages/core/templates/{zh-CN,en-US}/` | 下游 Rule、Skill、Hook、知识模板的唯一真源;随 Core tarball 发布 | 人或 Agent 直接维护并提交 Git | +| `.claude/` / `.cursor/` / `.codex/` / 根 `AGENTS.md` | 本仓消费模板后的配置根产物 | 用户明确要求后由 `flow2spec init` / `npm run sync:agents` 分发 | +| `.Knowledge/` | 本仓共享知识库 | 按 topic/skill 写权直接维护 | +| 本仓专属 `repo-*` Rule/Skill | 只服务本仓,不进入 Core templates | 三端配置根手写镜像 | -## 硬约束一览 +根 `templates/` 与根 `lib/` 均不再存在:模板和核心实现分别由 `packages/core/templates/`、`packages/core/lib/` 单独持有。`packages/core/templates/` 受 Git 管理,不依赖复制脚本、`.gitignore` 例外或 `prepack` 同步。 -| 约束 | 内容 | -| --- | --- | -| **1. 只改 `templates/` 与本仓知识库** | Flow2Spec 本仓下要给**下游用**的内容——规则 / 技能 / manifest / matchers / topics / knowledge 模板改动——**只落**在 `templates/zh-CN/` 与 `templates/en-US/` 下(含 `rules/`、`skills/`、`knowledge/`、`AGENTS.md`、`flow2spec.config.json` 模板)。**本仓自身**的 `.Knowledge/` 与配置根**下游用不到**的内容(如本规则、`repo-dev-check` skill)**直接**落配置根 + 本仓 `.Knowledge/topics/`。 | -| **2. 不改配置根(下游会用到的规则/技能)** | **禁止**直接编辑 `.claude/rules/` / `.claude/skills/` / `.cursor/rules/` / `.cursor/skills/` / `.codex/skills/` / `.codex/topics/` 中**由 `templates/` 派生**的文件、以及根目录 `AGENTS.md`——这些是 `flow2spec init` 的产物,手改会在下次 `init` / `sync:agents` 时被静默覆盖。**例外**:本规则、`repo-dev-check` skill 之类**只存在于本仓配置根**、**从不写入 `templates/`** 的文件不受此约束(它们本来就是配置根的原生手写内容)。 | -| **3. 用户驱动分发** | 模板改完后,Agent **不主动**跑 `flow2spec init` / `sync:agents`。默认交给用户执行;如果用户明确说「帮我 init」/「跑 sync」,才代跑。 | +## 硬约束 -## 为什么这么分 +1. 下游会使用的 Rule、Skill、Hook、AGENTS、配置与知识模板,只改 `packages/core/templates/zh-CN/` 和 `packages/core/templates/en-US/`;双语版本保持语义一致。 +2. 不直接编辑配置根中由 Core templates 派生的文件。它们会在后续 `init` 中被覆盖。 +3. 本规则、`repo-dev-check` 等本仓专属内容不进 Core templates,直接同步三端手写镜像与本仓知识库。 +4. Agent 不主动执行 `flow2spec init` / `npm run sync:agents`;只有用户明确要求分发时才执行。 +5. 根 `lib/` 不作为兼容入口。CLI 通过 `createFlow2Spec()` 等 Core 公共 API 工作;Core 内部测试可按需引用 `packages/core/lib/`。 -Flow2Spec 有一个让人容易混乱的特点:**它自己就是自己的第一位用户**——本仓开发时也遵循 Flow2Spec 规则,本仓的 `.claude/` / `.cursor/` / `.codex/` 是 `flow2spec init` 从 `templates/` 派生出来的产物。 +## 修改判断 -这就出现两类约束: +- 通用能力:改 `packages/core/templates/{locale}/...`,必要时同步 Core 实现与公开文档。 +- 本仓知识:改 `.Knowledge/topics/`、matcher、manifest/index;触达 topic 时先读 `f2s-topic-authoring`。 +- 本仓专属纪律:改三端 `repo-dev-workflow-constraints` 镜像,不写入 Core templates。 +- 配置根派生产物:不手改,也不为预览主动执行 init。 -- **A 类:下游也要遵守的通用规则**(比如 `f2s-git-commit`、`f2s-task`、`f2s-implement-tech-design`、`f2s-flow2spec-unified-entry` 等)——**放 `templates/`**,`init` 时分发到所有下游项目。 -- **B 类:只有 Flow2Spec 本仓自己需要的开发纪律**(比如「不要手改配置根」「用户来跑 init」)——**只放本仓配置根 + 本仓 `.Knowledge/topics/`**,`init` **不带**到下游。 +## 版本模型 -如果本规则写进 `templates/`,下游项目 `init` 时会拿到「不要手改配置根」这条——但下游的配置根就是它的运行时事实源,这条约束对它**毫无意义**且**会产生误导**。所以本规则严格保持在本仓内。 - -从数据流看: - -``` -本仓 templates/ ─┐ - ├── rules/ │ → flow2spec init → 三端配置根(同时供本仓消费 + 下游消费) - ├── skills/ │ - └── knowledge/ ─┘ - -本仓配置根手写内容(本文件、repo-dev-check skill) - └── 不进 templates,只服务本仓开发 +```text +CLI Version packages/cli/package.json +Core Version packages/core/package.json +Template Version packages/core/package.json.templateVersion +Protocol Version packages/core/capabilities.json.protocolVersion ``` -## 目录清单 - -### ✅ Agent / 人工都可以改 - -**通用(进 templates,会分发到下游)**: -- `templates/zh-CN/**` 与 `templates/en-US/**`:包括 - - `rules/*.md`(Cursor 端 `.mdc` 由 init 转换) - - `skills//SKILL.md` - - `AGENTS.md`(Codex 根条令模板) - - `flow2spec.config.json`(首次 init 模板) - - `knowledge/index.md` / `knowledge/topics/*.md` / `knowledge/matchers/*.json` / `knowledge/manifest-*.json` +- CLI 对 Core 使用运行时依赖 caret range(当前为 `^3.5.0`);Core 必须落在该范围内。 +- Core 兼容修复/新增 API 可只升 Core;Template Version 不变,不触发知识库升级。 +- Rule、Skill、Hook、知识模板变化时升 Core,并显式执行 `version:set:template`。 +- CLI 开始调用新版 Core API 时,升 CLI 并显式提高最低 Core range。 +- Protocol Version 只在公共协议不兼容时调整。 +- 根 private workspace version 不参与 npm 发布匹配。 -**本仓专属(不进 templates)**: -- `.Knowledge/topics/*.md`、`.Knowledge/index.md`、`.Knowledge/manifest-routing.json`、`.Knowledge/matchers/*.json`、`.Knowledge/stock-docs/*.md`、`.Knowledge/req-docs/*.md`:本仓自身知识库 -- **本仓专属规则 / 技能**:`.claude/rules/repo-dev-workflow-constraints.md`、`.cursor/rules/repo-dev-workflow-constraints.mdc`、`.codex/topics/repo-dev-workflow-constraints.md`、`.claude/skills/repo-dev-check/`、`.cursor/skills/repo-dev-check/`、`.codex/skills/repo-dev-check/`——这些**从不进 `templates/`**,直接手写落三端配置根 -- `docs/*.md` / `docs/en/*.md`、`lib/*.js`、`cli.js`、`scripts/*`、`package.json`、`README*.md` - -### ❌ Agent 禁止改(下游会用到的 init 产物) - -- `.claude/rules/`、`.claude/skills/` 中**由 `templates/` 派生**的文件(`f2s-config-check`、`f2s-flow2spec-unified-entry`、`f2s-task`、`f2s-kb-*` 等) -- `.claude/hooks/*.js`、`.claude/settings.json`(hook 段由 init 写) -- `.cursor/rules/*.mdc`、`.cursor/skills/*/SKILL.md`(除本仓专属条目外) -- `.cursor/hooks.json`、`.cursor/hooks/*.js` -- `.codex/topics/` 中**由 `templates/rules/` 镜像**的文件、`.codex/skills/` 中**由 `templates/skills/` 派生**的文件 -- `.codex/AGENTS.md`(指针文件)、`.codex/hooks.json`、`.codex/hooks/*.js` -- 根目录 `AGENTS.md`(Codex 完整条令,由 `buildCodexAgentsMd` 从 templates 拼装) - -**判定捷径**:如果一份文件在 `templates/{zh-CN,en-US}/` 下有对应源,就是「下游会用到的 init 产物」,禁止手改配置根版;否则就是「本仓专属手写内容」,可以直接改配置根版。 - -### 例外:本地不入库文件 - -- `.claude/memory/`:本地记忆,会话运行时状态,不进版本,不受约束 -- `LOCAL_CONTEXT.md`:本地上下文,不视为配置根 - -## Agent 行为准则 - -1. **判断改动性质**: - - 若是**下游也要用的规则 / 技能 / 主题模板**——路径必须落 **`templates/zh-CN/` + `templates/en-US/`**(zh/en 都改)。**禁止**用 `Edit` / `Write` 直接改配置根同名文件(哪怕当前工具的配置根就在旁边)。 - - 若是**本仓专属开发纪律 / 自查技能**(本文件、`repo-dev-check` 等)——直接手写 `.claude/` + `.cursor/` + `.codex/` 三端配置根 + `.Knowledge/topics/` 摘要。**不进 templates**。 -2. **用户指名要改 `.claude/rules/xxx.md`(且 xxx 在 templates 里有源)**:先澄清「这是 init 产物;改了会被覆盖;正确做法是改 `templates/{zh-CN,en-US}/rules/xxx.md`,然后跑 `npm run sync:agents` 或 `flow2spec init` 分发」。用户明确「就一次性临时改配置根 / 我知道会被覆盖」时才照办。 -3. **改完后**:**不主动**跑 `flow2spec init` / `sync:agents`。回复里点名「**已改 templates;请你执行 `npm run sync:agents` 或 `node ./cli.js init codex claude cursor` 分发到配置根**」。用户明确让代跑时才代跑。 -4. **发现「配置根有改动、templates 没改」**:先判定是不是本仓专属手写内容——是的话正常;如果 templates 里有对应源,多半是漂移,把配置根改动**回填**到 `templates/*/` 对应位置再由 `init` 从模板重新分发。 - -## 分发命令 - -从最短到最全: +版本命令: ```bash -# package.json 里配好的脚本 -npm run sync:agents - -# 直接调本仓 CLI(本地开发常用) -node ./cli.js init codex claude cursor - -# 全局装了 flow2spec 时 -flow2spec init codex claude cursor +npm run version:set:cli -- [--core-range ^x.y.z] +npm run version:set:core -- +npm run version:set:template -- +npm run version:check ``` -三条命令都会:从 `templates/{locale}/` 派生 → 写入配置根 `.claude/` / `.cursor/` / `.codex/` / 根 `AGENTS.md` / `.Knowledge/`(如首次)。当前 locale 取项目根 `flow2spec.config.json.locale`(本仓默认 `zh-CN`)。 - -**注意**:`init` **不会**动本仓专属手写文件(本规则、`repo-dev-check` skill)——这些文件在 `templates/` 里没有对应源,`init` 找不到就不写,用户手写的原始版本得以保留。 - -## npm workspace 发布门禁 +## 发布门禁 -- `@double-coding/flow2spec` 是面向普通用户的主包,`packages/cli/README.md` 必须与根 `README.md` 完全一致;根 README 变更后执行 `npm run sync:package-readme`。 -- `@double-coding/flow2spec-core` 面向 CLI 与原生插件开发者,维护独立的 `packages/core/README.md`,不复制主包的用户文档。 -- Core、CLI 与 workspace 根版本保持一致,CLI 对 Core 的依赖固定为同一版本。 -- 发布前执行 `npm run pack:check` 与 `node scripts/test-package-install.js`;安装测试必须从 CLI tarball 解出 `package/README.md` 并与根 README 比较,防止 npm 页面退化为占位说明。 -- **core templates 门禁**:`packages/core/templates/` 是 npm 发布物副本,由 `scripts/sync-core-templates.js` 从根 `templates/` 生成,进入 `.gitignore` 不入库。`packages/core/package.json.scripts.prepack` 会在 `npm pack` / `npm publish` 前自动同步;`npm run sync:core-templates:check` 可用来手动核对是否漂移(CI 也建议跑一遍)。**禁止**在 `packages/core/templates/` 里直接手改文件——下次同步会覆盖;改模板一律回到根 `templates/`。 +- `core-vX.Y.Z` 只测试、打包并发布 `@double-coding/flow2spec-core`。 +- `cli-vX.Y.Z` 只测试、打包并发布 `@double-coding/flow2spec`。 +- 同时发布时先 Core 后 CLI;禁止发布没有版本变化的包。 +- CLI README 与根 README 保持一致;Core README 独立维护。 +- 发布前运行 `npm run version:check`、`npm run pack:check`、`node scripts/test-package-install.js`。 +- `packages/core/templates/` 必须直接进入 Core tarball;不存在模板复制或漂移检查步骤。 -## 与其他规则的关系 +## 更新语义 -- **`f2s-flow2spec-unified-entry`**:讲「消费侧读取顺序」;本文件讲「本仓开发时的生产侧写盘边界」。两者并存不冲突。 -- **`f2s-topic-authoring`**:新增 / 改 `.Knowledge/topics/.md` 时先读该规则;对**下游用**的 topic 要落 `templates/*/knowledge/topics/`,对**本仓专属**的 topic 直接落 `.Knowledge/topics/`。 +- `flow2spec version` 展示 CLI、Core、Core Range、Template、Protocol。 +- `flow2spec update --check|--cli|--core` 分别检查、更新 CLI、更新兼容 Core。 +- Hook 同时比较 Core Version 与 Template Version。 +- Core 变化且 Template 不变:更新 Core 并执行一次幂等 init 刷新 Hook,不进入 `f2s-kb-upgrade`。 +- Template 变化:更新 Core、执行 init,再按 `projectRev` / `pkgRev` 判断是否进入 `f2s-kb-upgrade`。 ## 禁止项 -- 禁止 Agent 用 `Edit` / `Write` 直接改**在 `templates/` 里有对应源**的配置根文件(`.claude/rules/*`、`.claude/skills/*`、`.cursor/rules/*`、`.cursor/skills/*`、`.codex/topics/*`、`.codex/skills/*`、根 `AGENTS.md`)——即便当前工具的配置根就是它们。 -- 禁止把**本仓专属**规则 / 技能 / 主题写入 `templates/`(下游用不到,会造成噪音)。 -- 禁止 Agent 主动跑 `flow2spec init` / `npm run sync:agents` 分发;须由用户明确得令。 -- 禁止把本纪律当作「使用 flow2spec 的下游项目」的通用约束——**本文件仅针对 Flow2Spec 本仓自身的开发**。 -- **禁止手改 `packages/core/templates/`**——它是 `scripts/sync-core-templates.js` 从根 `templates/` 生成的副本,下次同步会被覆盖;改模板只改根 `templates/`。 +- 禁止恢复根 `templates/`、根 `lib/` 或 `sync-core-templates.js` 双根模型。 +- 禁止把 CLI、Core、Template 重新锁步成同一版本。 +- 禁止使用单一 `vX.Y.Z` tag 同时发布两个 npm 包。 +- 禁止把 Core 代码版本与 `.Knowledge/manifest-routing.json.version` 直接比较来决定知识库升级。 +- 禁止把本规则下发给普通业务仓。 diff --git a/.cursor/rules/repo-dev-workflow-constraints.mdc b/.cursor/rules/repo-dev-workflow-constraints.mdc index c754936..7e64548 100644 --- a/.cursor/rules/repo-dev-workflow-constraints.mdc +++ b/.cursor/rules/repo-dev-workflow-constraints.mdc @@ -1,152 +1,89 @@ --- -description: Flow2Spec 项目开发纪律(仅本仓适用):修改本仓时只改 templates/,配置根由 flow2spec init / sync:agents 分发,禁止手改配置根 +description: Flow2Spec 本仓开发纪律:Core 持有模板真源,配置根由 init 分发,CLI/Core/Template 独立版本与发布 --- # Flow2Spec 项目开发纪律(Dev Workflow Constraints) -> **仅适用于 Flow2Spec 本仓自身的开发**。 +> **仅适用于 Flow2Spec 本仓自身开发,不下发给普通业务仓。** > -> **不给下游使用**:因此本规则 / 对应 skill / 对应 topic 摘要**只**存在于本仓的配置根(`.claude/` / `.cursor/` / `.codex/`)与本仓 `.Knowledge/topics/`,**不落 `templates/`**——`flow2spec init` 不会把本文件分发给下游项目。 -> -> **单一事实源**:本文件为 **`f2s-dev-workflow-constraints`** 的完整约定。 +> 三端同源手写镜像: > - Claude:`.claude/rules/repo-dev-workflow-constraints.md` > - Cursor:`.cursor/rules/repo-dev-workflow-constraints.mdc` > - Codex:`.codex/topics/repo-dev-workflow-constraints.md` -> - 本仓路由摘要:`.Knowledge/topics/f2s-dev-workflow-constraints.md` -> -> **命名对齐**:**配置根文件名**统一用 **`repo-*`** 前缀(避开 `init` 的 `f2s-` 自清理触发面);**topic id / 路由概念名**保留 **`f2s-dev-workflow-constraints`**(`manifest-routing.topicPaths` / `topicDependencies` / `topicMetadata` / matcher 已登记的稳定 id,改会连锁破坏路由并让历史里程碑失真)。两处字面不同,指向**同一条规则**。 -> -> 三端配置根内容为同源手写副本;本仓内改动须**三端同步落盘**。 - -## 模板双份关系(重要背景) +> - 路由摘要:`.Knowledge/topics/f2s-dev-workflow-constraints.md` -本仓有两份 templates 目录,职责不同,**不能手工双写**: +## 所有权边界 -| 位置 | 角色 | 谁写 | +| 位置 | 角色 | 写入方式 | | --- | --- | --- | -| workspace 根 `templates/` | **唯一手写事实源**;日常改模板只改这里 | **人** / Agent | -| `packages/core/templates/` | **npm 发布物副本**;`@double-coding/flow2spec-core` 打包时带上,运行时被 `packages/core/index.js` 用 `path.join(__dirname, "templates", ...)` 读取;不进 git | **`scripts/sync-core-templates.js` 自动生成** | - -为什么 core 必须有独立副本:`npm install @double-coding/flow2spec-core` 装到用户机器上时,只有 `packages/core/` 目录进入 tarball——workspace 根 templates 不在其中。core 包内必须自带一份 templates,否则用户 `flow2spec init` 会 `ENOENT` 崩溃。 - -自动同步链路: - -- `packages/core/templates/` 已加入 `.gitignore`,不进 git。 -- `packages/core/package.json.scripts.prepack` 调用 `sync-core-templates.js`——`npm pack` / `npm publish` 前自动从根 templates 同步到 core templates,保证 tarball 完整。 -- 根 `package.json` 中 `sync:agents` / `test` / `pack:check` 都先跑一次同步(`--quiet` 静默模式),本地 dev 也不会碰到"core/templates 找不到"的启动错。 -- 想快速核对是否漂移:`npm run sync:core-templates:check`(不写盘,纯校验,退出码非零即有漂移)。 -- 想手动重新同步:`npm run sync:core-templates`。 +| `packages/core/templates/{zh-CN,en-US}/` | 下游 Rule、Skill、Hook、知识模板的唯一真源;随 Core tarball 发布 | 人或 Agent 直接维护并提交 Git | +| `.claude/` / `.cursor/` / `.codex/` / 根 `AGENTS.md` | 本仓消费模板后的配置根产物 | 用户明确要求后由 `flow2spec init` / `npm run sync:agents` 分发 | +| `.Knowledge/` | 本仓共享知识库 | 按 topic/skill 写权直接维护 | +| 本仓专属 `repo-*` Rule/Skill | 只服务本仓,不进入 Core templates | 三端配置根手写镜像 | -## 硬约束一览 +根 `templates/` 与根 `lib/` 均不再存在:模板和核心实现分别由 `packages/core/templates/`、`packages/core/lib/` 单独持有。`packages/core/templates/` 受 Git 管理,不依赖复制脚本、`.gitignore` 例外或 `prepack` 同步。 -| 约束 | 内容 | -| --- | --- | -| **1. 只改 `templates/` 与本仓知识库** | Flow2Spec 本仓下要给**下游用**的内容——规则 / 技能 / manifest / matchers / topics / knowledge 模板改动——**只落**在 `templates/zh-CN/` 与 `templates/en-US/` 下(含 `rules/`、`skills/`、`knowledge/`、`AGENTS.md`、`flow2spec.config.json` 模板)。**本仓自身**的 `.Knowledge/` 与配置根**下游用不到**的内容(如本规则、`repo-dev-check` skill)**直接**落配置根 + 本仓 `.Knowledge/topics/`。 | -| **2. 不改配置根(下游会用到的规则/技能)** | **禁止**直接编辑 `.claude/rules/` / `.claude/skills/` / `.cursor/rules/` / `.cursor/skills/` / `.codex/skills/` / `.codex/topics/` 中**由 `templates/` 派生**的文件、以及根目录 `AGENTS.md`——这些是 `flow2spec init` 的产物,手改会在下次 `init` / `sync:agents` 时被静默覆盖。**例外**:本规则、`repo-dev-check` skill 之类**只存在于本仓配置根**、**从不写入 `templates/`** 的文件不受此约束(它们本来就是配置根的原生手写内容)。 | -| **3. 用户驱动分发** | 模板改完后,Agent **不主动**跑 `flow2spec init` / `sync:agents`。默认交给用户执行;如果用户明确说「帮我 init」/「跑 sync」,才代跑。 | +## 硬约束 -## 为什么这么分 +1. 下游会使用的 Rule、Skill、Hook、AGENTS、配置与知识模板,只改 `packages/core/templates/zh-CN/` 和 `packages/core/templates/en-US/`;双语版本保持语义一致。 +2. 不直接编辑配置根中由 Core templates 派生的文件。它们会在后续 `init` 中被覆盖。 +3. 本规则、`repo-dev-check` 等本仓专属内容不进 Core templates,直接同步三端手写镜像与本仓知识库。 +4. Agent 不主动执行 `flow2spec init` / `npm run sync:agents`;只有用户明确要求分发时才执行。 +5. 根 `lib/` 不作为兼容入口。CLI 通过 `createFlow2Spec()` 等 Core 公共 API 工作;Core 内部测试可按需引用 `packages/core/lib/`。 -Flow2Spec 有一个让人容易混乱的特点:**它自己就是自己的第一位用户**——本仓开发时也遵循 Flow2Spec 规则,本仓的 `.claude/` / `.cursor/` / `.codex/` 是 `flow2spec init` 从 `templates/` 派生出来的产物。 +## 修改判断 -这就出现两类约束: +- 通用能力:改 `packages/core/templates/{locale}/...`,必要时同步 Core 实现与公开文档。 +- 本仓知识:改 `.Knowledge/topics/`、matcher、manifest/index;触达 topic 时先读 `f2s-topic-authoring`。 +- 本仓专属纪律:改三端 `repo-dev-workflow-constraints` 镜像,不写入 Core templates。 +- 配置根派生产物:不手改,也不为预览主动执行 init。 -- **A 类:下游也要遵守的通用规则**(比如 `f2s-git-commit`、`f2s-task`、`f2s-implement-tech-design`、`f2s-flow2spec-unified-entry` 等)——**放 `templates/`**,`init` 时分发到所有下游项目。 -- **B 类:只有 Flow2Spec 本仓自己需要的开发纪律**(比如「不要手改配置根」「用户来跑 init」)——**只放本仓配置根 + 本仓 `.Knowledge/topics/`**,`init` **不带**到下游。 +## 版本模型 -如果本规则写进 `templates/`,下游项目 `init` 时会拿到「不要手改配置根」这条——但下游的配置根就是它的运行时事实源,这条约束对它**毫无意义**且**会产生误导**。所以本规则严格保持在本仓内。 - -从数据流看: - -``` -本仓 templates/ ─┐ - ├── rules/ │ → flow2spec init → 三端配置根(同时供本仓消费 + 下游消费) - ├── skills/ │ - └── knowledge/ ─┘ - -本仓配置根手写内容(本文件、repo-dev-check skill) - └── 不进 templates,只服务本仓开发 +```text +CLI Version packages/cli/package.json +Core Version packages/core/package.json +Template Version packages/core/package.json.templateVersion +Protocol Version packages/core/capabilities.json.protocolVersion ``` -## 目录清单 - -### ✅ Agent / 人工都可以改 - -**通用(进 templates,会分发到下游)**: -- `templates/zh-CN/**` 与 `templates/en-US/**`:包括 - - `rules/*.md`(Cursor 端 `.mdc` 由 init 转换) - - `skills//SKILL.md` - - `AGENTS.md`(Codex 根条令模板) - - `flow2spec.config.json`(首次 init 模板) - - `knowledge/index.md` / `knowledge/topics/*.md` / `knowledge/matchers/*.json` / `knowledge/manifest-*.json` +- CLI 对 Core 使用运行时依赖 caret range(当前为 `^3.5.0`);Core 必须落在该范围内。 +- Core 兼容修复/新增 API 可只升 Core;Template Version 不变,不触发知识库升级。 +- Rule、Skill、Hook、知识模板变化时升 Core,并显式执行 `version:set:template`。 +- CLI 开始调用新版 Core API 时,升 CLI 并显式提高最低 Core range。 +- Protocol Version 只在公共协议不兼容时调整。 +- 根 private workspace version 不参与 npm 发布匹配。 -**本仓专属(不进 templates)**: -- `.Knowledge/topics/*.md`、`.Knowledge/index.md`、`.Knowledge/manifest-routing.json`、`.Knowledge/matchers/*.json`、`.Knowledge/stock-docs/*.md`、`.Knowledge/req-docs/*.md`:本仓自身知识库 -- **本仓专属规则 / 技能**:`.claude/rules/repo-dev-workflow-constraints.md`、`.cursor/rules/repo-dev-workflow-constraints.mdc`、`.codex/topics/repo-dev-workflow-constraints.md`、`.claude/skills/repo-dev-check/`、`.cursor/skills/repo-dev-check/`、`.codex/skills/repo-dev-check/`——这些**从不进 `templates/`**,直接手写落三端配置根 -- `docs/*.md` / `docs/en/*.md`、`lib/*.js`、`cli.js`、`scripts/*`、`package.json`、`README*.md` - -### ❌ Agent 禁止改(下游会用到的 init 产物) - -- `.claude/rules/`、`.claude/skills/` 中**由 `templates/` 派生**的文件(`f2s-config-check`、`f2s-flow2spec-unified-entry`、`f2s-task`、`f2s-kb-*` 等) -- `.claude/hooks/*.js`、`.claude/settings.json`(hook 段由 init 写) -- `.cursor/rules/*.mdc`、`.cursor/skills/*/SKILL.md`(除本仓专属条目外) -- `.cursor/hooks.json`、`.cursor/hooks/*.js` -- `.codex/topics/` 中**由 `templates/rules/` 镜像**的文件、`.codex/skills/` 中**由 `templates/skills/` 派生**的文件 -- `.codex/AGENTS.md`(指针文件)、`.codex/hooks.json`、`.codex/hooks/*.js` -- 根目录 `AGENTS.md`(Codex 完整条令,由 `buildCodexAgentsMd` 从 templates 拼装) - -**判定捷径**:如果一份文件在 `templates/{zh-CN,en-US}/` 下有对应源,就是「下游会用到的 init 产物」,禁止手改配置根版;否则就是「本仓专属手写内容」,可以直接改配置根版。 - -### 例外:本地不入库文件 - -- `.claude/memory/`:本地记忆,会话运行时状态,不进版本,不受约束 -- `LOCAL_CONTEXT.md`:本地上下文,不视为配置根 - -## Agent 行为准则 - -1. **判断改动性质**: - - 若是**下游也要用的规则 / 技能 / 主题模板**——路径必须落 **`templates/zh-CN/` + `templates/en-US/`**(zh/en 都改)。**禁止**用 `Edit` / `Write` 直接改配置根同名文件(哪怕当前工具的配置根就在旁边)。 - - 若是**本仓专属开发纪律 / 自查技能**(本文件、`repo-dev-check` 等)——直接手写 `.claude/` + `.cursor/` + `.codex/` 三端配置根 + `.Knowledge/topics/` 摘要。**不进 templates**。 -2. **用户指名要改 `.claude/rules/xxx.md`(且 xxx 在 templates 里有源)**:先澄清「这是 init 产物;改了会被覆盖;正确做法是改 `templates/{zh-CN,en-US}/rules/xxx.md`,然后跑 `npm run sync:agents` 或 `flow2spec init` 分发」。用户明确「就一次性临时改配置根 / 我知道会被覆盖」时才照办。 -3. **改完后**:**不主动**跑 `flow2spec init` / `sync:agents`。回复里点名「**已改 templates;请你执行 `npm run sync:agents` 或 `node ./cli.js init codex claude cursor` 分发到配置根**」。用户明确让代跑时才代跑。 -4. **发现「配置根有改动、templates 没改」**:先判定是不是本仓专属手写内容——是的话正常;如果 templates 里有对应源,多半是漂移,把配置根改动**回填**到 `templates/*/` 对应位置再由 `init` 从模板重新分发。 - -## 分发命令 - -从最短到最全: +版本命令: ```bash -# package.json 里配好的脚本 -npm run sync:agents - -# 直接调本仓 CLI(本地开发常用) -node ./cli.js init codex claude cursor - -# 全局装了 flow2spec 时 -flow2spec init codex claude cursor +npm run version:set:cli -- [--core-range ^x.y.z] +npm run version:set:core -- +npm run version:set:template -- +npm run version:check ``` -三条命令都会:从 `templates/{locale}/` 派生 → 写入配置根 `.claude/` / `.cursor/` / `.codex/` / 根 `AGENTS.md` / `.Knowledge/`(如首次)。当前 locale 取项目根 `flow2spec.config.json.locale`(本仓默认 `zh-CN`)。 - -**注意**:`init` **不会**动本仓专属手写文件(本规则、`repo-dev-check` skill)——这些文件在 `templates/` 里没有对应源,`init` 找不到就不写,用户手写的原始版本得以保留。 - -## npm workspace 发布门禁 +## 发布门禁 -- `@double-coding/flow2spec` 是面向普通用户的主包,`packages/cli/README.md` 必须与根 `README.md` 完全一致;根 README 变更后执行 `npm run sync:package-readme`。 -- `@double-coding/flow2spec-core` 面向 CLI 与原生插件开发者,维护独立的 `packages/core/README.md`,不复制主包的用户文档。 -- Core、CLI 与 workspace 根版本保持一致,CLI 对 Core 的依赖固定为同一版本。 -- 发布前执行 `npm run pack:check` 与 `node scripts/test-package-install.js`;安装测试必须从 CLI tarball 解出 `package/README.md` 并与根 README 比较,防止 npm 页面退化为占位说明。 -- **core templates 门禁**:`packages/core/templates/` 是 npm 发布物副本,由 `scripts/sync-core-templates.js` 从根 `templates/` 生成,进入 `.gitignore` 不入库。`packages/core/package.json.scripts.prepack` 会在 `npm pack` / `npm publish` 前自动同步;`npm run sync:core-templates:check` 可用来手动核对是否漂移(CI 也建议跑一遍)。**禁止**在 `packages/core/templates/` 里直接手改文件——下次同步会覆盖;改模板一律回到根 `templates/`。 +- `core-vX.Y.Z` 只测试、打包并发布 `@double-coding/flow2spec-core`。 +- `cli-vX.Y.Z` 只测试、打包并发布 `@double-coding/flow2spec`。 +- 同时发布时先 Core 后 CLI;禁止发布没有版本变化的包。 +- CLI README 与根 README 保持一致;Core README 独立维护。 +- 发布前运行 `npm run version:check`、`npm run pack:check`、`node scripts/test-package-install.js`。 +- `packages/core/templates/` 必须直接进入 Core tarball;不存在模板复制或漂移检查步骤。 -## 与其他规则的关系 +## 更新语义 -- **`f2s-flow2spec-unified-entry`**:讲「消费侧读取顺序」;本文件讲「本仓开发时的生产侧写盘边界」。两者并存不冲突。 -- **`f2s-topic-authoring`**:新增 / 改 `.Knowledge/topics/.md` 时先读该规则;对**下游用**的 topic 要落 `templates/*/knowledge/topics/`,对**本仓专属**的 topic 直接落 `.Knowledge/topics/`。 +- `flow2spec version` 展示 CLI、Core、Core Range、Template、Protocol。 +- `flow2spec update --check|--cli|--core` 分别检查、更新 CLI、更新兼容 Core。 +- Hook 同时比较 Core Version 与 Template Version。 +- Core 变化且 Template 不变:更新 Core 并执行一次幂等 init 刷新 Hook,不进入 `f2s-kb-upgrade`。 +- Template 变化:更新 Core、执行 init,再按 `projectRev` / `pkgRev` 判断是否进入 `f2s-kb-upgrade`。 ## 禁止项 -- 禁止 Agent 用 `Edit` / `Write` 直接改**在 `templates/` 里有对应源**的配置根文件(`.claude/rules/*`、`.claude/skills/*`、`.cursor/rules/*`、`.cursor/skills/*`、`.codex/topics/*`、`.codex/skills/*`、根 `AGENTS.md`)——即便当前工具的配置根就是它们。 -- 禁止把**本仓专属**规则 / 技能 / 主题写入 `templates/`(下游用不到,会造成噪音)。 -- 禁止 Agent 主动跑 `flow2spec init` / `npm run sync:agents` 分发;须由用户明确得令。 -- 禁止把本纪律当作「使用 flow2spec 的下游项目」的通用约束——**本文件仅针对 Flow2Spec 本仓自身的开发**。 -- **禁止手改 `packages/core/templates/`**——它是 `scripts/sync-core-templates.js` 从根 `templates/` 生成的副本,下次同步会被覆盖;改模板只改根 `templates/`。 +- 禁止恢复根 `templates/`、根 `lib/` 或 `sync-core-templates.js` 双根模型。 +- 禁止把 CLI、Core、Template 重新锁步成同一版本。 +- 禁止使用单一 `vX.Y.Z` tag 同时发布两个 npm 包。 +- 禁止把 Core 代码版本与 `.Knowledge/manifest-routing.json.version` 直接比较来决定知识库升级。 +- 禁止把本规则下发给普通业务仓。 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ff18cd..2139b3c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,9 @@ jobs: - name: Install workspace dependencies run: npm ci --ignore-scripts + - name: Verify workspace versions + run: npm run version:check + - name: Run CLI tests run: npm test diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml index 0340e08..6924c77 100644 --- a/.github/workflows/publish-npm.yml +++ b/.github/workflows/publish-npm.yml @@ -15,7 +15,7 @@ concurrency: jobs: publish: - name: Publish Flow2Spec workspace packages + name: Publish tagged Flow2Spec package runs-on: ubuntu-latest steps: - name: Check out release commit @@ -42,25 +42,8 @@ jobs: - name: Install workspace dependencies run: npm ci --ignore-scripts - - name: Verify tag matches package version - shell: bash - run: | - 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 workspace package version $package_version." >&2 - exit 1 - fi + - name: Verify workspace and release tag versions + run: npm run version:check -- --tag "$GITHUB_REF_NAME" - name: Run tests run: npm test @@ -69,7 +52,9 @@ jobs: run: npm run pack:check - name: Publish Core package with provenance + if: startsWith(github.event.release.tag_name, 'core-v') run: npm publish --workspace @double-coding/flow2spec-core --access public --provenance - name: Publish CLI package with provenance + if: startsWith(github.event.release.tag_name, 'cli-v') run: npm publish --workspace @double-coding/flow2spec --access public --provenance diff --git a/.gitignore b/.gitignore index 4141039..73f37d2 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,3 @@ output/playwright/ node_modules/ .astro/ .Knowledge/.last-sync.json - -# Core npm 发布物副本:由 scripts/sync-core-templates.js 从根 templates/ 生成 -# 唯一手写事实源是 workspace 根 templates/ -packages/core/templates/ diff --git "a/docs/Flow2Spec\345\237\272\347\241\200\344\273\213\347\273\215.md" "b/docs/Flow2Spec\345\237\272\347\241\200\344\273\213\347\273\215.md" index 29b2982..d9ba445 100644 --- "a/docs/Flow2Spec\345\237\272\347\241\200\344\273\213\347\273\215.md" +++ "b/docs/Flow2Spec\345\237\272\347\241\200\344\273\213\347\273\215.md" @@ -256,7 +256,7 @@ flowchart TD **提交前知识覆盖检查**:`f2s-git-commit` 会在提交前检查 diff、冲突标记、暂存范围,也会检查本轮变更是否需要同步知识库,让"改代码后忘记更新知识"在提交前能被发现。 -**模板与路由升级检测**:Flow2Spec 启动时会检测 `.Knowledge/` 结构版本,如果落后于 npm 包版本,Agent 会提示执行 `f2s-kb-upgrade`,确保项目知识结构与工具版本保持同步。 +**模板与路由升级检测**:Flow2Spec 启动时分别检测 Core Version 与 Template Version。Core-only 更新只刷新运行时与 Hook;Template Version 落后时,Agent 才在 init 后按 `projectRev` / `pkgRev` 决定是否执行 `f2s-kb-upgrade`。 --- diff --git a/docs/en/Flow2Spec-Introduction.md b/docs/en/Flow2Spec-Introduction.md index a493558..0476e14 100644 --- a/docs/en/Flow2Spec-Introduction.md +++ b/docs/en/Flow2Spec-Introduction.md @@ -256,7 +256,7 @@ Every step in this chain leaves a trackable asset: requirements in `req-docs/`, **Pre-commit knowledge coverage check**: `f2s-git-commit` checks the diff, conflict markers, and staging scope before committing, and checks whether the round's changes require a knowledge base sync — catching "changed code, forgot to update knowledge" before the commit lands. -**Template and routing upgrade detection**: At startup, Flow2Spec checks whether `.Knowledge/` is behind the npm package version and, if so, prompts `f2s-kb-upgrade` to keep the project's knowledge structure aligned with the tool version. +**Template and routing upgrade detection**: At startup, Flow2Spec checks Core Version and Template Version independently. Core-only updates refresh the runtime and Hook; when Template Version is behind, the Agent uses `projectRev` / `pkgRev` after init to decide whether `f2s-kb-upgrade` is required. --- diff --git a/docs/en/commands-reference.md b/docs/en/commands-reference.md index 5152cbc..014db04 100644 --- a/docs/en/commands-reference.md +++ b/docs/en/commands-reference.md @@ -505,12 +505,12 @@ The table above lists conversation-triggered `/f2s-*` skills. The repository als **Purpose**: Knowledge base template upgrade. Aligns manifest-routing and matchers shards. -**How It Works**: Uses "version branching + delegated init" — detect whether the current knowledge base is V1 (legacy structure) or V2+ (already has `.Knowledge`): V1 no longer has built-in migration support — the skill stops and tells the user how to proceed (one-time migration with a historical package version, or manual move into `.Knowledge`); V2+ runs `flow2spec init` directly for incremental package alignment (new templates, manifest schema upgrades, matcher shard format alignment). After upgrade, re-read SKILL.md to see if certain steps must be re-run. Unlike a standalone `init`, `kb-upgrade` includes version routing and re-run logic; `init` alone is a one-shot structural fill-in. For **non-topic version updates** (`projectRev == pkgRev` after init), the agent may directly run `flow2spec init` on the user's behalf without entering this skill's full flow. +**How It Works**: First use `flow2spec version` / `flow2spec update --check` to judge Core and Template independently, then detect whether the knowledge base is V1 (legacy) or V2+ (`.Knowledge`). Core-only updates refresh Core and the Hook; Template updates run init and use `projectRev` / `pkgRev` to choose the fast path or full flow. V1 no longer has built-in migration support. **Use Cases**: -- After a `flow2spec` package version upgrade, upgrade the project knowledge base template +- After Template Version changes, align the project knowledge-base templates - Upgrade an old project to the latest structure -- When interactive `flow2spec version` / `flow2spec init` detects a newer npm version, the CLI prompts you to run `flow2spec update`, then execute this skill in the Agent conversation +- When the Hook reports `templateUpdateAvailable=true`, execute this skill in the Agent conversation; Core-only updates do not enter it - When Cursor detects an older knowledge-base version through `.cursor/hooks.json` on `sessionStart`, it prompts you to execute this skill - When Codex detects an older knowledge-base version through `.codex/hooks.json` on `SessionStart`, it prompts you to execute this skill (new or changed hooks must be trusted through `/hooks` first) diff --git a/docs/en/usage-guide.md b/docs/en/usage-guide.md index 47bc4c5..0debed3 100644 --- a/docs/en/usage-guide.md +++ b/docs/en/usage-guide.md @@ -130,12 +130,12 @@ f2s-kb-merge # Resolve context conflicts after Git merges ### Cross-Version Knowledge Base Upgrade ``` -Non-topic version update (projectRev == pkgRev after init): the agent runs flow2spec init directly — done -Topic-layer changes (projectRev != pkgRev): full f2s-kb-upgrade flow (Current V2+: already has .Knowledge; includes npm v3.x projects, etc.; see skill step 0) +Core-only update (Template Version unchanged): update Core and run one idempotent init to refresh the Hook; do not enter f2s-kb-upgrade +Template update: after init, projectRev == pkgRev takes the fast path; a difference enters the full f2s-kb-upgrade flow Legacy layout (V1): built-in migration removed; use a historical package version (@3.4.x or earlier) for a one-time migration, or move into .Knowledge manually ``` -In interactive terminals, the Flow2Spec CLI checks the latest npm version with a cache when running `flow2spec version` / `flow2spec init`. If a newer version exists, it prompts you to run `flow2spec update`; for non-topic version updates the agent may directly run `flow2spec init` to complete the alignment, and only when the update includes topic-layer changes execute `f2s-kb-upgrade` in the Agent conversation to align the project knowledge templates, manifest/matchers, and agent config roots. Failed update checks are skipped silently and do not affect the current command; checks are disabled in `CI`, non-TTY sessions, or when `FLOW2SPEC_SKIP_UPDATE_CHECK=1` is set. +`flow2spec version` shows CLI, Core, Core Range, Template, and Protocol. `flow2spec update --check|--cli|--core` checks updates, updates CLI, or updates a compatible Core. SessionStart Hooks compare Core and Template independently: Core-only updates do not trigger knowledge upgrade; Template updates use `projectRev` / `pkgRev` after init to decide whether to run `f2s-kb-upgrade`. Failed checks are skipped silently; CLI self-checks do not interrupt `CI` or runs with `FLOW2SPEC_SKIP_UPDATE_CHECK=1`. After `flow2spec init codex`, Codex projects include `.codex/hooks.json`, `.codex/hooks/f2s-config-session.js`, and `.codex/hooks/f2s-update-check.js`. On Codex `SessionStart` for `startup|resume`, the first script injects one configuration summary and the second checks the knowledge-base version automatically. When the hook is first generated or changed, trust it through `/hooks` in Codex. Set `updateCheck.enabled=false` in `flow2spec.config.json` to skip only the version check. @@ -143,7 +143,7 @@ Prefer the native Cordis plugin `@double-coding/flow2spec-deepseek-harness` from After `flow2spec init cursor`, Cursor projects include `.cursor/hooks.json` and `.cursor/hooks/f2s-update-check.js`. The hook runs on Cursor `sessionStart` and injects upgrade reminders through `additional_context`. Set `updateCheck.enabled=false` in `flow2spec.config.json` to skip the check. -After `flow2spec init claude`, Claude projects include `.claude/settings.json`, `.claude/hooks/f2s-config-session.js`, `.claude/hooks/f2s-config-inject.js`, and `.claude/hooks/f2s-update-check.js`: `SessionStart` injects the configuration summary and checks the knowledge-base version, while `PreToolUse Skill` only guards `f2s-*` Skill calls. The version-check script injects an agent-instruction notice through `additional_context`, requiring the agent to relay the message verbatim to the user. The notice format is: "Current project `` knowledge-base version v, lower than latest package version v. Run the f2s-kb-upgrade skill to align templates and routing." If today's cache still flags a needed upgrade, every new session re-injects the reminder; after a successful upgrade, `f2s-kb-upgrade` clears `.Knowledge/update-check.json` so stale reminders disappear. +After `flow2spec init claude`, Claude projects include `.claude/settings.json`, `.claude/hooks/f2s-config-session.js`, `.claude/hooks/f2s-config-inject.js`, and `.claude/hooks/f2s-update-check.js`: `SessionStart` injects the configuration summary and checks Core/Template independently, while `PreToolUse Skill` only guards `f2s-*` Skill calls. The Hook injects the relevant Core-only or Template-update action through `additional_context`; an unresolved same-day cache is reminded again, and completion clears `.Knowledge/update-check.json`. --- diff --git "a/docs/\344\275\277\347\224\250\350\257\264\346\230\216.md" "b/docs/\344\275\277\347\224\250\350\257\264\346\230\216.md" index e0b5c3d..7127d6d 100644 --- "a/docs/\344\275\277\347\224\250\350\257\264\346\230\216.md" +++ "b/docs/\344\275\277\347\224\250\350\257\264\346\230\216.md" @@ -117,12 +117,12 @@ f2s-kb-merge # Git 合并后解决上下文冲突 ### 知识库跨版本升级 ``` -非主题版本更新(init 后 projectRev == pkgRev):agent 直接代跑 flow2spec init 即完成 -主题层变更(projectRev != pkgRev):f2s-kb-upgrade 完整流程(现行库 V2+:已有 .Knowledge;含 npm v3.x 等,见技能步骤 0) +Core-only 更新(Template Version 不变):更新 Core 后幂等 init 刷新 Hook,不进入 f2s-kb-upgrade +Template 更新:init 后按 projectRev == pkgRev 走快速路径,不等时进入 f2s-kb-upgrade 完整流程 旧版布局(流程 V1):不再内置迁移;用历史版本包(@3.4.x 及更早)一次性迁移或手动迁入 .Knowledge ``` -Flow2Spec CLI 在交互式执行 `flow2spec version` / `flow2spec init` 时会按缓存检查 npm 最新版本;若发现新版本,会提示先执行 `flow2spec update`;非主题版本更新可由 agent 直接代跑 `flow2spec init` 完成对齐,仅当更新包含主题层变更时再在 Agent 对话中执行 `f2s-kb-upgrade`,用于对齐项目知识库模板、manifest/matchers 与配置根产物。更新检查失败会静默跳过,不影响当前命令;`CI`、非 TTY 或设置 `FLOW2SPEC_SKIP_UPDATE_CHECK=1` 时不检查。 +`flow2spec version` 展示 CLI、Core、Core Range、Template、Protocol;`flow2spec update --check|--cli|--core` 分别检查、更新 CLI、更新兼容 Core。SessionStart Hook 同时检查 Core 与 Template:Core-only 更新不触发知识库升级;Template 更新才在 init 后按 `projectRev` / `pkgRev` 决定是否执行 `f2s-kb-upgrade`。更新检查失败会静默跳过,不影响当前命令;`CI` 或设置 `FLOW2SPEC_SKIP_UPDATE_CHECK=1` 时 CLI 自检不打扰当前流程。 Codex 项目执行 `flow2spec init codex` 后会写入 `.codex/hooks.json`、`.codex/hooks/f2s-config-session.js` 与 `.codex/hooks/f2s-update-check.js`:前者在 Codex `SessionStart` 的 `startup|resume` 事件注入一次配置摘要,后者自动检查知识库版本;首次生成或 hook 内容变化后,需要在 Codex 中通过 `/hooks` 信任该项目 hook。`flow2spec.config.json` 中 `updateCheck.enabled=false` 时仅跳过版本检查,不影响配置摘要注入。 @@ -130,7 +130,7 @@ DeepSeek Harness 优先安装原生 Cordis 插件 `@double-coding/flow2spec-deep Cursor 项目执行 `flow2spec init cursor` 后会写入 `.cursor/hooks.json` 与 `.cursor/hooks/f2s-update-check.js`,在 Cursor `sessionStart` 自动检查知识库版本;脚本通过 `additional_context` 把升级提示注入会话。`flow2spec.config.json` 中 `updateCheck.enabled=false` 时跳过检查。 -Claude 项目执行 `flow2spec init claude` 后会写入 `.claude/settings.json`、`.claude/hooks/f2s-config-session.js`、`.claude/hooks/f2s-config-inject.js` 与 `.claude/hooks/f2s-update-check.js`:`SessionStart` 注入配置摘要并检查知识库版本,`PreToolUse Skill` 仅在调用 `f2s-*` Skill 前做守门提示。版本检查脚本通过 `additional_context` 注入命令式升级提示(agent-instruction 文案要求 agent 必须原文转告用户),格式为:"当前项目「<项目名>」知识库版本 v<当前版本>,低于最新包版本 v<最新版本>。可执行 f2s-kb-upgrade skill 对齐模板与路由。"若当天缓存仍标记需升级,新会话每次都会重新注入提示;升级成功后 `f2s-kb-upgrade` 会清理 `.Knowledge/update-check.json`,避免旧提示残留。 +Claude 项目执行 `flow2spec init claude` 后会写入 `.claude/settings.json`、`.claude/hooks/f2s-config-session.js`、`.claude/hooks/f2s-config-inject.js` 与 `.claude/hooks/f2s-update-check.js`:`SessionStart` 注入配置摘要并分别检查 Core/Template,`PreToolUse Skill` 仅在调用 `f2s-*` Skill 前做守门提示。Hook 通过 `additional_context` 注入对应的 Core-only 或 Template 更新动作;当天缓存仍标记需更新时,新会话会继续提醒,完成更新后清理 `.Knowledge/update-check.json`。 --- diff --git "a/docs/\345\221\275\344\273\244\350\257\264\346\230\216.md" "b/docs/\345\221\275\344\273\244\350\257\264\346\230\216.md" index 749a052..9e66d46 100644 --- "a/docs/\345\221\275\344\273\244\350\257\264\346\230\216.md" +++ "b/docs/\345\221\275\344\273\244\350\257\264\346\230\216.md" @@ -587,14 +587,14 @@ **作用**:知识库模板升级。对齐 manifest-routing + matchers 分片。 -**工作原理**:通过「版本分流 + init 代跑」实现升级——先检测当前知识库属于 V1(旧结构)还是 V2+(已有 `.Knowledge`):V1 已不再内置迁移支持,技能停止并告知用户处理方式(用历史版本包一次性迁移,或手动迁入 `.Knowledge`);V2+ 直接代跑 `flow2spec init` 进行包级结构增量对齐(补齐新模板、升级 manifest schema、对齐 matchers 分片格式)。升级后重读 SKILL.md 判断是否需要重跑某些步骤。与单独 `init` 的区别:`kb-upgrade` 包含版本判断和重跑逻辑,`init` 仅做一次性结构补齐;**非主题版本更新**(init 后 `projectRev == pkgRev`)可由 agent 直接代跑 `flow2spec init` 完成,无需进入本技能完整流程。 +**工作原理**:先用 `flow2spec version` / `flow2spec update --check` 分开判断 Core 与 Template,再检测当前知识库属于 V1(旧结构)还是 V2+(已有 `.Knowledge`)。Core-only 更新只刷新 Core 与 Hook;Template 更新才代跑 init,并按 `projectRev` / `pkgRev` 选择快速路径或完整流程。V1 不再内置迁移支持。 **使用场景**: -- flow2spec 包版本升级后,升级项目知识库模板 +- Template Version 升级后,对齐项目知识库模板 - 旧项目升级到最新结构 - 项目使用 `locale` 模板语言时,默认沿用 `flow2spec.config.json.locale`;未配置则按 `zh-CN` 补齐 -- CLI 在交互式 `flow2spec version` / `flow2spec init` 中发现 npm 有新版本后,会提示先 `flow2spec update`,再在 Agent 对话中执行本技能 +- Hook 检测到 `templateUpdateAvailable=true` 后,在 Agent 对话中执行本技能;Core-only 更新不进入本技能 - Cursor 通过 `.cursor/hooks.json` 在 `sessionStart` 自动检测到知识库版本落后后,会提示执行本技能 - Codex 通过 `.codex/hooks.json` 在 `SessionStart` 自动检测到知识库版本落后后,会提示执行本技能(首次或变更后需在 Codex `/hooks` 中信任) diff --git a/lib/agents.js b/lib/agents.js deleted file mode 100644 index 8fe9444..0000000 --- a/lib/agents.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("@double-coding/flow2spec-core").legacy.agents; diff --git a/lib/claudeRulesAdapter.js b/lib/claudeRulesAdapter.js deleted file mode 100644 index 73ea2d2..0000000 --- a/lib/claudeRulesAdapter.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("@double-coding/flow2spec-core").legacy.claudeRulesAdapter; diff --git a/lib/claudeSettingsAdapter.js b/lib/claudeSettingsAdapter.js deleted file mode 100644 index bf471c3..0000000 --- a/lib/claudeSettingsAdapter.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("@double-coding/flow2spec-core").legacy.claudeSettingsAdapter; diff --git a/lib/codexAgentsAdapter.js b/lib/codexAgentsAdapter.js deleted file mode 100644 index d570649..0000000 --- a/lib/codexAgentsAdapter.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("@double-coding/flow2spec-core").legacy.codexAgentsAdapter; diff --git a/lib/developerId.js b/lib/developerId.js deleted file mode 100644 index 154ff1d..0000000 --- a/lib/developerId.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("@double-coding/flow2spec-core").legacy.developerId; diff --git a/lib/doctor.js b/lib/doctor.js deleted file mode 100644 index 17ea889..0000000 --- a/lib/doctor.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("@double-coding/flow2spec-core").legacy.doctor; diff --git a/lib/dshAgentsAdapter.js b/lib/dshAgentsAdapter.js deleted file mode 100644 index 800996b..0000000 --- a/lib/dshAgentsAdapter.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("@double-coding/flow2spec-core").legacy.dshAgentsAdapter; diff --git a/lib/flow2specConfig.js b/lib/flow2specConfig.js deleted file mode 100644 index 6de2f56..0000000 --- a/lib/flow2specConfig.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("@double-coding/flow2spec-core").legacy.config; diff --git a/lib/init.js b/lib/init.js deleted file mode 100644 index d3ee21a..0000000 --- a/lib/init.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("@double-coding/flow2spec-core").legacy.init; diff --git a/lib/knowledgeEngine.js b/lib/knowledgeEngine.js deleted file mode 100644 index cccd203..0000000 --- a/lib/knowledgeEngine.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("@double-coding/flow2spec-core").legacy.knowledgeEngine; diff --git a/package-lock.json b/package-lock.json index 8bdf277..e26ce97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,6 @@ "packages/core", "packages/cli" ], - "dependencies": { - "@double-coding/flow2spec-core": "^3.5.0" - }, "engines": { "node": ">=16" } @@ -31,7 +28,7 @@ "version": "3.5.0", "license": "ISC", "dependencies": { - "@double-coding/flow2spec-core": "3.5.0" + "@double-coding/flow2spec-core": "^3.5.0" }, "bin": { "flow2spec": "cli.js" diff --git a/package.json b/package.json index 053ae1d..a24cd31 100644 --- a/package.json +++ b/package.json @@ -11,17 +11,17 @@ "node": ">=16" }, "scripts": { - "test": "node scripts/sync-core-templates.js --quiet && node cli.js --help && node cli.js kb check && node scripts/sync-package-readme.js --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/sync-core-templates.js --quiet && node scripts/test-core-api.js", - "test:cli": "node scripts/sync-core-templates.js --quiet && node cli.js --help && node cli.js doctor --json", - "sync:core-templates": "node scripts/sync-core-templates.js", - "sync:core-templates:check": "node scripts/sync-core-templates.js --check", - "sync:agents": "node scripts/sync-core-templates.js --quiet && node cli.js init cursor claude codex", + "test": "node cli.js --help && node cli.js kb check && node scripts/sync-package-readme.js --check && node scripts/test-workspace-version.js && node scripts/test-cli-update.js && 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", "sync:package-readme": "node scripts/sync-package-readme.js", - "pack:check": "node scripts/sync-core-templates.js --quiet && node scripts/sync-package-readme.js --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" - }, - "dependencies": { - "@double-coding/flow2spec-core": "^3.5.0" + "pack:check": "npm run version:check && node scripts/sync-package-readme.js --check && npm pack --workspace @double-coding/flow2spec-core --dry-run && npm pack --workspace @double-coding/flow2spec --dry-run", + "tag:version:cli": "node scripts/git-tag-version.js cli", + "tag:version:core": "node scripts/git-tag-version.js core", + "version:set:cli": "node scripts/workspace-version.js set-cli", + "version:set:core": "node scripts/workspace-version.js set-core", + "version:set:template": "node scripts/workspace-version.js set-template", + "version:check": "node scripts/workspace-version.js check" } } diff --git a/packages/cli/cli.js b/packages/cli/cli.js index 8ae7bc9..a5645d4 100755 --- a/packages/cli/cli.js +++ b/packages/cli/cli.js @@ -4,29 +4,34 @@ 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"); + createFlow2Spec, + getCapabilities, + getVersions, +} = require("@double-coding/flow2spec-core"); const { execFileSync } = require("child_process"); const args = process.argv.slice(2); const sub = args[0]; +const CONFIG_FILENAME = "flow2spec.config.json"; +const CORE_PACKAGE = "@double-coding/flow2spec-core"; +const flow2specForMetadata = createFlow2Spec({ cwd: process.cwd() }); +const AGENTS = flow2specForMetadata.project.agents(); +const SUPPORTED_LOCALES = flow2specForMetadata.config.supportedLocales(); + +function createApi(cwd = process.cwd()) { + return createFlow2Spec({ cwd }); +} + const agentList = Object.entries(AGENTS) .map(([id, { label }]) => `${id}(${label})`) .join(", "); const pkg = require("./package.json"); +const coreVersions = getVersions(); +const coreRange = pkg.dependencies?.[CORE_PACKAGE] || ""; const UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1000; @@ -51,6 +56,41 @@ function compareVersions(a, b) { return 0; } +function satisfiesCoreRange(version, range) { + const match = /^\^(\d+)\.(\d+)\.(\d+)/.exec(String(range || "")); + if (!match || compareVersions(version, `${match[1]}.${match[2]}.${match[3]}`) < 0) return false; + const [major, minor, patch] = match.slice(1).map(Number); + const upper = major > 0 + ? `${major + 1}.0.0` + : minor > 0 + ? `0.${minor + 1}.0` + : `0.0.${patch + 1}`; + return compareVersions(version, upper) < 0; +} + +function runCommandSync(command, commandArgs, options = {}) { + return execFileSync(command, commandArgs, { + windowsHide: true, + shell: process.platform === "win32", + ...options, + }); +} + +function queryLatestCoreMetadata() { + const output = runCommandSync( + "npm", + ["view", CORE_PACKAGE, "version", "templateVersion", "--json", "--registry=https://registry.npmjs.org"], + { encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"] }, + ); + const metadata = JSON.parse(output); + return { + version: typeof metadata === "string" ? metadata : metadata.version, + templateVersion: typeof metadata === "string" + ? metadata + : metadata.templateVersion || metadata.version, + }; +} + function updateCheckCacheFile() { const safeName = String(pkg.name || "flow2spec").replace(/[^a-z0-9_.-]+/gi, "_"); return path.join(os.homedir(), ".flow2spec", `${safeName}-update-check.json`); @@ -88,7 +128,7 @@ function writeUpdateCheckCache(latest) { function queryLatestPackageVersion() { const cached = readUpdateCheckCache(); if (cached?.latest) return cached.latest; - const latest = execFileSync("npm", ["view", pkg.name, "version"], { + const latest = runCommandSync("npm", ["view", pkg.name, "version"], { encoding: "utf8", timeout: 2000, stdio: ["ignore", "pipe", "ignore"], @@ -116,7 +156,7 @@ function getGlobalInstalledVersion() { if (!pkg.name) return null; let globalRoot; try { - globalRoot = execFileSync("npm", ["root", "-g"], { + globalRoot = runCommandSync("npm", ["root", "-g"], { encoding: "utf8", timeout: 2000, stdio: ["ignore", "pipe", "ignore"], @@ -166,27 +206,21 @@ function maybeAutoUpdateGlobalInstall() { console.log(` ↻ 检测到全局 ${pkg.name}@${installed},正在升级到 v${latest}...`); try { - execFileSync("npm", ["install", "-g", `${pkg.name}@latest`], { + runCommandSync("npm", ["install", "-g", `${pkg.name}@latest`], { stdio: "inherit", }); console.log(`✓ 全局 ${pkg.name} 已升级到 v${latest}`); } catch (e) { console.error(` ⚠ 全局升级失败:${e.message || e} - 可手动执行: flow2spec update`); + 可手动执行: flow2spec update --cli`); } } -function printKnowledgeUpgradeHint(latest) { +function printCliUpdateHint(latest) { console.log(` -⚠ Flow2Spec 有新版本 v${latest}(当前 v${pkg.version}) -建议先更新包: - flow2spec update - -更新后请在 Agent 对话中执行: - f2s-kb-upgrade - -用于对齐项目知识库模板、manifest/matchers 与配置根产物;不要把单独 flow2spec init 当作知识库升级。 +⚠ Flow2Spec CLI 有新版本 v${latest}(当前 v${pkg.version}) +执行:flow2spec update --cli `); } @@ -195,7 +229,7 @@ function maybePrintUpdateNotice() { try { const latest = queryLatestPackageVersion(); if (latest && compareVersions(latest, pkg.version) > 0) { - printKnowledgeUpgradeHint(latest); + printCliUpdateHint(latest); } } catch { // 静默跳过,不能因为网络或 npm registry 影响主命令。 @@ -206,6 +240,24 @@ function printJson(data) { console.log(`${JSON.stringify(data, null, 2)}\n`); } +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"); +} + function printKnowledgeHelp() { console.log(` Flow2Spec KB - knowledge collaboration engine @@ -237,8 +289,10 @@ Flow2Spec - 统一知识库工作流(AI 配置入口) v${pkg.version} flow2spec config 打印项目根 ${CONFIG_FILENAME} 的解析结果(缺省值合并后) flow2spec doctor [--json] 只读检查运行环境、项目初始化、协作上下文与知识库健康 flow2spec kb 知识库协作引擎:status / check / plan / apply / build - flow2spec version 显示当前 flow2spec 版本 - flow2spec update 更新 flow2spec 到最新版本;更新后提示执行 f2s-kb-upgrade + flow2spec version 显示 CLI / Core / Template / Protocol 版本 + flow2spec update --check 检查 CLI 与 Core 更新 + flow2spec update --cli 更新 CLI + flow2spec update --core 在兼容范围内更新 Core flow2spec --help 显示本说明 agent(可多个,空格分隔;省略时交互选择): @@ -274,34 +328,82 @@ if (sub === "--help" || sub === "-h" || !sub) { } if (sub === "version" || sub === "--version" || sub === "-v") { - console.log(`flow2spec v${pkg.version}`); + console.log([ + `Flow2Spec CLI: ${pkg.version}`, + `Flow2Spec Core: ${coreVersions.coreVersion}`, + `Core Range: ${coreRange}`, + `Template Version: ${coreVersions.templateVersion}`, + `Protocol Version: ${getCapabilities().protocolVersion}`, + ].join("\n")); maybePrintUpdateNotice(); process.exit(0); } if (sub === "update") { - console.log(`当前版本: v${pkg.version}`); - console.log("正在检查最新版本..."); + const updateFlags = args.slice(1); + const allowedUpdateFlags = new Set(["--check", "--cli", "--core"]); + if (updateFlags.length > 1 || updateFlags.some((flag) => !allowedUpdateFlags.has(flag))) { + console.error("用法: flow2spec update --check|--cli|--core"); + process.exit(1); + } + const mode = updateFlags[0] || "--check"; try { - const latest = execFileSync("npm", ["view", pkg.name, "version"], { + const latestCli = runCommandSync("npm", ["view", pkg.name, "version"], { encoding: "utf8", + timeout: 5000, + stdio: ["ignore", "pipe", "ignore"], }).trim(); - if (compareVersions(latest, pkg.version) <= 0) { - console.log(`当前版本不低于 npm 最新版本 v${latest}`); + const latestCore = queryLatestCoreMetadata(); + const coreCompatible = satisfiesCoreRange(latestCore.version, coreRange); + + if (mode === "--check") { + console.log([ + `CLI: ${pkg.version} -> ${latestCli}`, + `Core: ${coreVersions.coreVersion} -> ${latestCore.version}`, + `Template: ${coreVersions.templateVersion} -> ${latestCore.templateVersion}`, + `Range: ${coreRange} (${coreCompatible ? "compatible" : "incompatible"})`, + ].join("\n")); 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 当作知识库升级。 -`); + if (mode === "--cli") { + if (compareVersions(latestCli, pkg.version) <= 0) { + console.log(`CLI 已是最新版本 v${pkg.version}`); + process.exit(0); + } + if (getGlobalInstalledVersion()) { + runCommandSync("npm", ["install", "-g", `${pkg.name}@latest`], { stdio: "inherit" }); + console.log(`\n✓ CLI 已更新到 v${latestCli}`); + } else { + runCommandSync("npx", ["--yes", `${pkg.name}@latest`, "version"], { stdio: "inherit" }); + console.log("\n✓ 当前为 npx 场景;已用 latest CLI 启动并验证,无需写入全局安装。"); + } + process.exit(0); + } + + if (!coreCompatible) { + console.error(`Core v${latestCore.version} 超出当前 CLI 兼容范围 ${coreRange};请先更新支持该 Core 的 CLI。`); + process.exit(1); + } + if (compareVersions(latestCore.version, coreVersions.coreVersion) <= 0) { + console.log(`Core 已是最新兼容版本 v${coreVersions.coreVersion}`); + process.exit(0); + } + if (getGlobalInstalledVersion()) { + runCommandSync("npm", ["install", "-g", `${CORE_PACKAGE}@${latestCore.version}`], { stdio: "inherit" }); + console.log(`\n✓ Core 已更新到 v${latestCore.version}`); + } else { + runCommandSync( + "npx", + ["--yes", "--package", `${pkg.name}@${pkg.version}`, "--package", `${CORE_PACKAGE}@${latestCore.version}`, "flow2spec", "version"], + { stdio: "inherit" }, + ); + console.log("\n✓ 当前为 npx 场景;已显式安装并验证 CLI/Core 组合,后续 init 请继续使用相同的 --package 组合。"); + } + console.log(latestCore.templateVersion === coreVersions.templateVersion + ? "Template Version 未变化,无需执行 f2s-kb-upgrade。" + : "Template Version 已变化;请运行 init,并仅在 projectRev 与 pkgRev 不等时进入 f2s-kb-upgrade。" + ); } catch (e) { console.error("更新失败:", e.message || e); process.exit(1); @@ -313,7 +415,7 @@ if (sub === "config") { const cwd = process.cwd(); const abs = path.join(cwd, CONFIG_FILENAME); try { - const cfg = loadFlow2specConfig(cwd); + const cfg = createApi(cwd).config.load(); console.log(JSON.stringify({ configPath: abs, ...cfg }, null, 2)); } catch (e) { console.error(e.message || e); @@ -339,7 +441,7 @@ if (sub === "doctor") { console.error(`doctor 不支持参数:${unknown.join(" ")}`); process.exit(1); } - const report = runDoctor(process.cwd()); + const report = createApi(process.cwd()).doctor.run(); if (doctorArgs.includes("--json")) { printJson(report); } else { @@ -354,6 +456,7 @@ if (sub === "kb") { const kbPositionals = args.slice(2).filter((arg) => !String(arg || "").startsWith("--")); const cwd = process.cwd(); const jsonOut = kbFlags.has("--json"); + const flow2spec = createApi(cwd); try { if (!kbSub || kbSub === "--help" || kbSub === "-h") { @@ -362,7 +465,7 @@ if (sub === "kb") { } if (kbSub === "status") { - const report = knowledgeEngine.summarizeKnowledgeState(cwd); + const report = flow2spec.knowledge.status(); if (jsonOut) { printJson(report); } else { @@ -390,17 +493,11 @@ if (sub === "kb") { if (kbSub === "check") { const strict = kbFlags.has("--strict"); - const graph = knowledgeEngine.loadKnowledgeGraph(cwd); - const validation = knowledgeEngine.validateKnowledgeGraph(graph, { + const validation = flow2spec.knowledge.check({ strictRevision: strict, }); - const normalized = knowledgeEngine.normalizeRoutingWithGraph( - graph, - ); - const routingDrift = - knowledgeEngine.stableStringify(normalized.routing) !== - knowledgeEngine.stableStringify(graph.routing); - const report = knowledgeEngine.summarizeKnowledgeState(cwd); + const report = flow2spec.knowledge.status(); + const routingDrift = report.routingDrift; const ok = validation.issues.length === 0 && !routingDrift && @@ -441,10 +538,8 @@ if (sub === "kb") { } 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 plan = flow2spec.knowledge.plan({ deltaFile: deltaPath }); const result = { ok: plan.mergeable, deltaPath, @@ -466,7 +561,8 @@ if (sub === "kb") { } process.exit(result.ok ? 0 : 1); } - const result = knowledgeEngine.applyKnowledgeDelta(cwd, deltaPath, { + const result = flow2spec.knowledge.apply({ + deltaFile: deltaPath, dryRun, }); if (jsonOut) { @@ -481,7 +577,7 @@ if (sub === "kb") { } if (kbSub === "build") { - const result = knowledgeEngine.buildKnowledgeGraph(cwd, { + const result = flow2spec.knowledge.build({ writeTopicFrontmatter: kbFlags.has("--fix-topics"), }); if (jsonOut) { @@ -539,7 +635,6 @@ if (sub === "init") { console.error(`不支持的 locale:${cliLocale}。可选:${SUPPORTED_LOCALES.join(", ")}`); process.exit(1); } - if (cliLocale) cliLocale = normalizeLocale(cliLocale); const cwd = process.cwd(); @@ -706,7 +801,7 @@ if (sub === "init") { async function collectInitOptions() { const needAgentPrompt = agentArgs.length === 0 && !skipPrompts; - const missingFields = getMissingConfigFields(cwd); + const missingFields = createApi(cwd).config.missingFields(); const needConfigPrompt = missingFields.length > 0; // 没有任何需要处理的事情 @@ -750,7 +845,7 @@ if (sub === "init") { if (needConfigPrompt) { if (needAgentPrompt) process.stdout.write("\n"); - const isFirstTime = missingFields.length === CONFIG_FIELDS.length; + const isFirstTime = !fs.existsSync(path.join(cwd, CONFIG_FILENAME)); process.stdout.write( ` 配置 ${CONFIG_FILENAME}${isFirstTime ? "(首次创建)" : "(补充新增字段)"}:\n\n`, ); @@ -783,7 +878,12 @@ if (sub === "init") { collectInitOptions() .then(({ configValues, chosenAgents }) => - runInit(cwd, chosenAgents, { overwriteKnowledge, configValues, locale: cliLocale }), + createApi(cwd).project.init({ + integrations: chosenAgents, + overwriteKnowledge, + configValues, + locale: cliLocale, + }), ) .then(({ ids, knowledgeResult, routingUpgrade, indexSnapshot, gitignoreResult, projectConfig, locale, claudeHooksResult }) => { const lines = ids.map((id) => { diff --git a/packages/cli/lib/agents.js b/packages/cli/lib/agents.js deleted file mode 100644 index 8fe9444..0000000 --- a/packages/cli/lib/agents.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("@double-coding/flow2spec-core").legacy.agents; diff --git a/packages/cli/lib/doctor.js b/packages/cli/lib/doctor.js deleted file mode 100644 index 17ea889..0000000 --- a/packages/cli/lib/doctor.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("@double-coding/flow2spec-core").legacy.doctor; diff --git a/packages/cli/lib/flow2specConfig.js b/packages/cli/lib/flow2specConfig.js deleted file mode 100644 index 6de2f56..0000000 --- a/packages/cli/lib/flow2specConfig.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("@double-coding/flow2spec-core").legacy.config; diff --git a/packages/cli/lib/init.js b/packages/cli/lib/init.js deleted file mode 100644 index d3ee21a..0000000 --- a/packages/cli/lib/init.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("@double-coding/flow2spec-core").legacy.init; diff --git a/packages/cli/lib/knowledgeEngine.js b/packages/cli/lib/knowledgeEngine.js deleted file mode 100644 index cccd203..0000000 --- a/packages/cli/lib/knowledgeEngine.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("@double-coding/flow2spec-core").legacy.knowledgeEngine; diff --git a/packages/cli/package.json b/packages/cli/package.json index 758ae18..579a48a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -16,11 +16,10 @@ }, "files": [ "cli.js", - "lib", "README.md" ], "dependencies": { - "@double-coding/flow2spec-core": "3.5.0" + "@double-coding/flow2spec-core": "^3.5.0" }, "publishConfig": { "access": "public", diff --git a/packages/core/README.md b/packages/core/README.md index 40cf9be..e8c1ce4 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -7,7 +7,7 @@ Flow2Spec 的程序化核心能力包,供 CLI 和原生开发工具插件共 ## 原生插件 API ```js -const { createFlow2Spec } = require("@double-coding/flow2spec-core"); +const { createFlow2Spec, getVersions } = require("@double-coding/flow2spec-core"); const flow2spec = createFlow2Spec({ cwd: process.cwd() }); const skills = flow2spec.resources.skillCatalog({ host: "dsh", locale: "zh-CN" }); @@ -17,11 +17,14 @@ const entry = flow2spec.resources.unifiedEntry({ projectConfig: flow2spec.config.load(), }); const update = await flow2spec.update.check(); +const versions = getVersions(); ``` - `resources.skillCatalog()` 返回带正文和关联规则资源的结构化 Skill 清单。Core 根据宿主适配规则与 Skill 路径,插件不需要重写其他客户端目录。 - `resources.unifiedEntry()` 返回宿主适配后的统一入口,可附带当前项目配置摘要。 -- `update.check()` 复用 `.Knowledge/update-check.json` 的每日缓存与版本比较语义;网络不可用时返回 `unavailable`,不会阻断宿主。 +- `project.agents()` 返回可用于 `project.init()` 的客户端集成元数据;`config.supportedLocales()` 返回包内模板语言列表。 +- `getVersions()` 返回独立的 Core、Template 与 Protocol Version。 +- `update.check()` 复用 `.Knowledge/update-check.json` 的每日缓存,分别返回 Core 与 Template 更新状态;网络不可用时返回 `unavailable`,不会阻断宿主。 - `capabilities.json` 的 `protocolVersion` 用于插件启动时执行能力兼容校验。 包通过 `index.d.ts` 导出完整公共契约类型。普通 CLI 用户继续使用 `npx @double-coding/flow2spec init`,无需直接调用这些 API。 diff --git a/packages/core/capabilities.json b/packages/core/capabilities.json index 675136e..42237ae 100644 --- a/packages/core/capabilities.json +++ b/packages/core/capabilities.json @@ -5,7 +5,9 @@ "capabilities": [ { "id": "project.init", "api": "project.init", "since": "3.3.0" }, { "id": "project.inspect", "api": "project.inspect", "since": "3.3.0" }, + { "id": "project.agents", "api": "project.agents", "since": "3.5.0" }, { "id": "config.load", "api": "config.load", "since": "3.3.0" }, + { "id": "config.supported-locales", "api": "config.supportedLocales", "since": "3.5.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" }, diff --git a/packages/core/index.d.ts b/packages/core/index.d.ts index 317503c..96b8f00 100644 --- a/packages/core/index.d.ts +++ b/packages/core/index.d.ts @@ -55,6 +55,11 @@ export interface ProjectInspection { config: Flow2SpecProjectConfig; } +export interface Flow2SpecAgent { + root: string; + label: string; +} + export interface RoutingRule { task?: string; matcherId?: string; @@ -200,6 +205,12 @@ export interface CapabilityManifest { capabilities: CapabilityDefinition[]; } +export interface Flow2SpecVersions { + coreVersion: string; + templateVersion: string; + protocolVersion: number; +} + export interface HostResourceOptions { host: Flow2SpecHost; locale?: Flow2SpecLocale; @@ -239,8 +250,14 @@ export interface UpdateCheckResult { checked: boolean; fromCache: boolean; packageName: string; + currentCoreVersion: string; + currentTemplateVersion: string; manifestVersion: string | null; + latestCoreVersion: string | null; + latestTemplateVersion: string | null; latestVersion: string | null; + coreUpdateAvailable: boolean; + templateUpdateAvailable: boolean; needsUpgrade: boolean; notice: string; checkedAt: number | null; @@ -256,10 +273,12 @@ export interface Flow2SpecApi { project: { init(options?: ProjectInitOptions): Promise; inspect(): ProjectInspection; + agents(): Record; }; config: { load(): Flow2SpecProjectConfig; missingFields(): unknown[]; + supportedLocales(): Flow2SpecLocale[]; }; routing: { graph(): unknown; @@ -323,5 +342,6 @@ export class Flow2SpecError extends Error { export function createFlow2Spec(options?: CreateFlow2SpecOptions): Flow2SpecApi; export function getCapabilities(): CapabilityManifest; +export function getVersions(): Flow2SpecVersions; export const resourcesRoot: string; export const legacy: Record; diff --git a/packages/core/index.js b/packages/core/index.js index f9ec5f8..1445686 100644 --- a/packages/core/index.js +++ b/packages/core/index.js @@ -16,6 +16,7 @@ const routing = require("./lib/routing"); const hostResources = require("./lib/resources"); const updateCheck = require("./lib/updateCheck"); const capabilities = require("./capabilities.json"); +const packageMetadata = require("./package.json"); class Flow2SpecError extends Error { constructor(code, message, details = {}, options = {}) { @@ -113,10 +114,14 @@ function createFlow2Spec(options = {}) { project: { init: (initOptions = {}) => init(cwd, initOptions.integrations || [], initOptions), inspect: () => ({ cwd, config: config.loadFlow2specConfig(cwd) }), + agents: () => Object.fromEntries( + Object.entries(agents.AGENTS).map(([id, metadata]) => [id, { ...metadata }]), + ), }, config: { load: () => config.loadFlow2specConfig(cwd), missingFields: () => config.getMissingConfigFields(cwd), + supportedLocales: () => [...config.SUPPORTED_LOCALES], }, routing: { graph: () => knowledgeEngine.loadKnowledgeGraph(cwd), @@ -177,6 +182,11 @@ module.exports = { Flow2SpecError, createFlow2Spec, getCapabilities: () => capabilities, + getVersions: () => ({ + coreVersion: packageMetadata.version, + templateVersion: packageMetadata.templateVersion, + protocolVersion: capabilities.protocolVersion, + }), resourcesRoot: __dirname, legacy: { AGENTS: agents.AGENTS, diff --git a/packages/core/lib/claudeSettingsAdapter.js b/packages/core/lib/claudeSettingsAdapter.js index 44d9d9e..afdc2fd 100644 --- a/packages/core/lib/claudeSettingsAdapter.js +++ b/packages/core/lib/claudeSettingsAdapter.js @@ -169,16 +169,17 @@ function copyHookScript(claudeRoot, templatesDir, scriptName) { 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); - } + let packageMetadata = {}; + try { + const packageDir = findPackageJsonDir(templatesDir); + packageMetadata = JSON.parse( + fs.readFileSync(path.join(packageDir || path.join(templatesDir, '..'), 'package.json'), 'utf8'), + ); + } catch (_) {} + body = body + .replace(/__FLOW2SPEC_PACKAGE_NAME__/g, packageMetadata.name || '@double-coding/flow2spec-core') + .replace(/__FLOW2SPEC_CORE_VERSION__/g, packageMetadata.version || '0.0.0') + .replace(/__FLOW2SPEC_TEMPLATE_VERSION__/g, packageMetadata.templateVersion || '0.0.0'); fs.writeFileSync(path.join(hooksDir, scriptName), body, 'utf8'); return { written: true }; } diff --git a/packages/core/lib/init.js b/packages/core/lib/init.js index 23de9fe..fafb48e 100644 --- a/packages/core/lib/init.js +++ b/packages/core/lib/init.js @@ -172,12 +172,12 @@ function findPackageJsonDir(startDir) { return null; } -function readPackageName(templatesDir) { +function readPackageMetadata(templatesDir) { try { const packageDir = findPackageJsonDir(templatesDir); - return readJson(path.join(packageDir || path.join(templatesDir, ".."), "package.json")).name; + return readJson(path.join(packageDir || path.join(templatesDir, ".."), "package.json")); } catch (_) { - return "@double-coding/flow2spec"; + return {}; } } @@ -199,7 +199,11 @@ function writeHookScriptWithPackageName(destDir, templatesDir, 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)); + const packageMetadata = readPackageMetadata(templatesDir); + body = body + .replace(/__FLOW2SPEC_PACKAGE_NAME__/g, packageMetadata.name || "@double-coding/flow2spec-core") + .replace(/__FLOW2SPEC_CORE_VERSION__/g, packageMetadata.version || "0.0.0") + .replace(/__FLOW2SPEC_TEMPLATE_VERSION__/g, packageMetadata.templateVersion || "0.0.0"); fs.writeFileSync(path.join(destDir, scriptName), body, "utf8"); return { written: true }; } @@ -462,7 +466,7 @@ function mergeTopicMetadata(templateMetadata, existingMetadata, topicPaths) { return out; } -function buildMergedRouting(templateRouting, existingRouting, pkgVersion, isFirstInit = false) { +function buildMergedRouting(templateRouting, existingRouting, templateVersion, isFirstInit = false) { const mergedTaskRules = unionByKey( templateRouting.taskToTopicRules, existingRouting.taskToTopicRules, @@ -487,7 +491,7 @@ function buildMergedRouting(templateRouting, existingRouting, pkgVersion, isFirs ); const knownMerged = { - version: pkgVersion || templateRouting.version || existingRouting.version, + version: templateVersion || templateRouting.version || existingRouting.version, knowledgeRoot: existingRouting.knowledgeRoot || templateRouting.knowledgeRoot, generatedFrom: @@ -731,14 +735,14 @@ function finalizePkgRev(cwd, templatesDir) { } catch { return { written: false, pkgRev: null }; } - // 顺便用本包版本号覆盖 manifest.version——reset 路径直接 cp 模板会留下模板里的占位版本号 + // reset 路径直接复制模板时,用 Core 包内独立模板版本覆盖 manifest.version。 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; + const templateVersion = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8")).templateVersion; + if (typeof templateVersion === "string" && routing.version !== templateVersion) { + routing.version = templateVersion; changed = true; } } @@ -803,14 +807,14 @@ function upgradeKnowledgeRoutingAndMatchers(cwd, templatesDir, options = {}) { const existingRouting = hadRouting ? readJson(routingPath) : {}; const existingMatchers = hadMatchers ? readJson(matchersPath) : {}; - // 读包版本号,用于写入 manifest-routing.json 的 version 字段 - let pkgVersion; + // 读取 Core 包内独立模板版本,用于写入 manifest-routing.json.version。 + let templateVersion; try { const packageDir = findPackageJsonDir(templatesDir); - pkgVersion = readJson(path.join(packageDir || path.join(templatesDir, ".."), "package.json")).version; + templateVersion = readJson(path.join(packageDir || path.join(templatesDir, ".."), "package.json")).templateVersion; } catch (_) {} - const mergedRouting = buildMergedRouting(templateRouting, existingRouting, pkgVersion, !hadRouting); + const mergedRouting = buildMergedRouting(templateRouting, existingRouting, templateVersion, !hadRouting); const mergedMatchers = buildMergedMatchers( templateMatchers, existingMatchers, diff --git a/packages/core/lib/resources.js b/packages/core/lib/resources.js index 7003e0b..799dfea 100644 --- a/packages/core/lib/resources.js +++ b/packages/core/lib/resources.js @@ -141,7 +141,7 @@ function adaptNativeDshEntry(content, locale) { "## Topic Authoring Pointer", `## Knowledge Base Version Check -The native host calls Core \`update.check()\` on session start. The API respects the project \`updateCheck.enabled\` switch and the daily \`.Knowledge/update-check.json\` cache. A notice is displayed when the published Core is newer than the project knowledge version. This check only detects and reports updates; it does not replace the configuration-read or knowledge-routing gates.`, +The native host calls Core \`update.check()\` on session start. The API respects the project \`updateCheck.enabled\` switch and the daily \`.Knowledge/update-check.json\` cache. It reports Core Version and Template Version independently: Core-only updates refresh the runtime and Hook without entering knowledge upgrade, while Template updates continue through init and the projectRev/pkgRev gate. This check does not replace configuration-read or knowledge-routing gates.`, ); } return replaceSection( @@ -150,7 +150,7 @@ The native host calls Core \`update.check()\` on session start. The API respects "## 主题创作", `## 知识库版本自检 -原生宿主在会话启动时调用 Core \`update.check()\`。该 API 服从项目的 \`updateCheck.enabled\` 开关并复用每日 \`.Knowledge/update-check.json\` 缓存;Core 发布版本高于项目知识版本时,由宿主展示升级提示。版本检查只负责检测与提醒,不替代配置前置读取和知识路由门禁。`, +原生宿主在会话启动时调用 Core \`update.check()\`。该 API 服从项目的 \`updateCheck.enabled\` 开关并复用每日 \`.Knowledge/update-check.json\` 缓存,分别报告 Core Version 与 Template Version:Core-only 更新只刷新运行时与 Hook,Template 更新再进入 init 和 projectRev/pkgRev 门禁。版本检查不替代配置前置读取和知识路由门禁。`, ); } diff --git a/packages/core/lib/updateCheck.js b/packages/core/lib/updateCheck.js index cc4fe87..65a5482 100644 --- a/packages/core/lib/updateCheck.js +++ b/packages/core/lib/updateCheck.js @@ -3,20 +3,16 @@ const fs = require("fs"); const path = require("path"); const { execFile } = require("child_process"); +const packageMetadata = require("../package.json"); const DEFAULT_PACKAGE_NAME = "@double-coding/flow2spec-core"; -const KNOWLEDGE_ROOT = ".Knowledge"; const CACHE_FILENAME = "update-check.json"; function parseVersion(version) { - return String(version || "") - .replace(/^v/, "") - .split(/[.-]/) - .slice(0, 3) - .map((part) => { - const number = Number.parseInt(part, 10); - return Number.isFinite(number) ? number : 0; - }); + return String(version || "").replace(/^v/, "").split(/[.-]/).slice(0, 3).map((part) => { + const number = Number.parseInt(part, 10); + return Number.isFinite(number) ? number : 0; + }); } function compareVersions(left, right) { @@ -41,11 +37,7 @@ function assertNotAborted(signal) { } function readJson(filePath) { - try { - return JSON.parse(fs.readFileSync(filePath, "utf8")); - } catch (_) { - return null; - } + try { return JSON.parse(fs.readFileSync(filePath, "utf8")); } catch (_) { return null; } } function sameLocalDay(timestamp, now) { @@ -54,23 +46,27 @@ function sameLocalDay(timestamp, now) { } function projectName(cwd) { - const pkg = readJson(path.join(cwd, "package.json")); - return pkg?.name ? String(pkg.name) : path.basename(cwd); + return readJson(path.join(cwd, "package.json"))?.name || path.basename(cwd); } -function buildNotice({ cwd, locale, manifestVersion, latestVersion }) { - if (locale === "en-US") { - return `[flow2spec] The project "${projectName(cwd)}" knowledge version is v${manifestVersion}; Core v${latestVersion} is available. The agent may run flow2spec init directly to complete the update; run the f2s-kb-upgrade skill only when the update includes topic-layer changes (projectRev != pkgRev after init).`; +function buildNotice({ cwd, locale, state }) { + const summary = `Core v${state.currentCoreVersion} -> v${state.latestCoreVersion}, Template v${state.manifestVersion} -> v${state.latestTemplateVersion}`; + if (state.templateUpdateAvailable) { + return locale === "en-US" + ? `[flow2spec] Project "${projectName(cwd)}" has a template update (${summary}). Update Core, run flow2spec init, then use f2s-kb-upgrade only if projectRev differs from pkgRev.` + : `[flow2spec] 当前项目「${projectName(cwd)}」有模板更新(${summary})。请更新 Core 后执行 flow2spec init;仅当 projectRev 与 pkgRev 不等时再执行 f2s-kb-upgrade。`; } - return `[flow2spec] 当前项目「${projectName(cwd)}」知识版本为 v${manifestVersion},Core 最新版本为 v${latestVersion}。可由 agent 直接代跑 flow2spec init 完成更新;仅当更新包含主题层变更(init 后 projectRev 与 pkgRev 不等)时再执行 f2s-kb-upgrade skill。`; + return locale === "en-US" + ? `[flow2spec] Project "${projectName(cwd)}" has a Core-only update (${summary}). Update Core and run one idempotent flow2spec init; do not run f2s-kb-upgrade.` + : `[flow2spec] 当前项目「${projectName(cwd)}」仅有 Core 程序更新(${summary})。请更新 Core 并执行一次幂等 flow2spec init;无需执行 f2s-kb-upgrade。`; } -function queryLatestVersion(packageName, options = {}) { +function queryLatestMetadata(packageName, options = {}) { const npmExecutable = process.platform === "win32" ? "npm.cmd" : "npm"; return new Promise((resolve, reject) => { execFile( npmExecutable, - ["view", packageName, "version", "--registry=https://registry.npmjs.org"], + ["view", packageName, "version", "templateVersion", "--json", "--registry=https://registry.npmjs.org"], { encoding: "utf8", timeout: options.timeout || 5000, @@ -87,20 +83,52 @@ function queryLatestVersion(packageName, options = {}) { reject(error); return; } - resolve(String(stdout || "").trim()); + try { + const metadata = JSON.parse(String(stdout || "").trim()); + const latestCoreVersion = typeof metadata === "string" ? metadata : metadata.version; + const latestTemplateVersion = typeof metadata === "string" + ? metadata + : metadata.templateVersion || metadata.version; + resolve({ latestCoreVersion, latestTemplateVersion }); + } catch (parseError) { + reject(parseError); + } }, ); }); } +function buildState(manifestVersion, metadata) { + const currentCoreVersion = packageMetadata.version; + const currentTemplateVersion = packageMetadata.templateVersion; + const coreUpdateAvailable = compareVersions(currentCoreVersion, metadata.latestCoreVersion) < 0; + const templateUpdateAvailable = compareVersions(manifestVersion, metadata.latestTemplateVersion) < 0; + return { + currentCoreVersion, + currentTemplateVersion, + manifestVersion, + latestCoreVersion: metadata.latestCoreVersion, + latestTemplateVersion: metadata.latestTemplateVersion, + coreUpdateAvailable, + templateUpdateAvailable, + needsUpgrade: coreUpdateAvailable || templateUpdateAvailable, + }; +} + function result(status, values = {}) { return { status, checked: false, fromCache: false, packageName: values.packageName || DEFAULT_PACKAGE_NAME, + currentCoreVersion: values.currentCoreVersion || packageMetadata.version, + currentTemplateVersion: values.currentTemplateVersion || packageMetadata.templateVersion, manifestVersion: values.manifestVersion || null, - latestVersion: values.latestVersion || null, + latestCoreVersion: values.latestCoreVersion || null, + latestTemplateVersion: values.latestTemplateVersion || null, + latestVersion: values.latestCoreVersion || values.latestVersion || null, + coreUpdateAvailable: false, + templateUpdateAvailable: false, needsUpgrade: status === "upgrade-available", notice: values.notice || "", checkedAt: values.checkedAt || null, @@ -120,105 +148,57 @@ async function checkUpdate(cwd, config, options = {}) { return result("skipped", { packageName, reason: "continuous-integration" }); } - const knowledgeDir = path.join(cwd, KNOWLEDGE_ROOT); - const manifestPath = path.join(knowledgeDir, "manifest-routing.json"); + const knowledgeDir = path.join(cwd, ".Knowledge"); const cachePath = path.join(knowledgeDir, CACHE_FILENAME); - const manifestVersion = readJson(manifestPath)?.version || null; - if (!manifestVersion) { - return result("skipped", { packageName, reason: "manifest-missing" }); - } + const manifestVersion = readJson(path.join(knowledgeDir, "manifest-routing.json"))?.version || null; + if (!manifestVersion) return result("skipped", { packageName, reason: "manifest-missing" }); - const now = Date.now(); const cache = readJson(cachePath); - const cachePackageMatches = cache?.packageName - ? cache.packageName === packageName - : packageName === DEFAULT_PACKAGE_NAME; - if (!options.force && cache && cachePackageMatches && sameLocalDay(cache.checkedAt, now)) { - const latestVersion = cache.latestVersion || cache.latestNpm || null; - if (latestVersion && compareVersions(manifestVersion, latestVersion) >= 0) { - try { - fs.rmSync(cachePath, { force: true }); - } catch (_) {} - return result("current", { - checked: true, - fromCache: true, - packageName, - manifestVersion, - latestVersion, - checkedAt: Number(cache.checkedAt), - }); + let metadata; + let fromCache = false; + if (!options.force && cache && sameLocalDay(cache.checkedAt, Date.now())) { + metadata = { + latestCoreVersion: cache.latestCoreVersion || cache.latestVersion || cache.latestNpm, + latestTemplateVersion: cache.latestTemplateVersion || cache.latestNpm || cache.latestVersion, + }; + fromCache = true; + } else { + try { + metadata = await queryLatestMetadata(packageName, options); + } catch (error) { + if (error?.code === "F2S_ABORTED") throw error; + return result("unavailable", { packageName, manifestVersion, reason: "registry-unavailable" }); } - const needsUpgrade = - Boolean(latestVersion) && - (cache.needsUpgrade === true || compareVersions(manifestVersion, latestVersion) < 0); - return result(needsUpgrade ? "upgrade-available" : "current", { - checked: true, - fromCache: true, - packageName, - manifestVersion, - latestVersion, - needsUpgrade, - notice: needsUpgrade - ? buildNotice({ cwd, locale, manifestVersion, latestVersion }) - : "", - checkedAt: Number(cache.checkedAt), - }); - } - - let latestVersion; - try { - latestVersion = await queryLatestVersion(packageName, { - signal: options.signal, - timeout: options.timeout, - }); - } catch (error) { - if (error?.code === "F2S_ABORTED") throw error; - return result("unavailable", { - packageName, - manifestVersion, - reason: "registry-unavailable", - }); } assertNotAborted(options.signal); - if (!latestVersion) { - return result("unavailable", { - packageName, - manifestVersion, - reason: "empty-registry-version", - }); + if (!metadata.latestCoreVersion || !metadata.latestTemplateVersion) { + return result("unavailable", { packageName, manifestVersion, reason: "empty-registry-version" }); } - const needsUpgrade = compareVersions(manifestVersion, latestVersion) < 0; - const notice = needsUpgrade - ? buildNotice({ cwd, locale, manifestVersion, latestVersion }) - : ""; - const checkedAt = Date.now(); - try { - fs.mkdirSync(knowledgeDir, { recursive: true }); - fs.writeFileSync( - cachePath, - `${JSON.stringify( - { - packageName, - latestVersion, - latestNpm: latestVersion, - manifestVersion, - needsUpgrade, - notice, - checkedAt, - }, - null, - 2, - )}\n`, - "utf8", - ); - } catch (_) {} - return result(needsUpgrade ? "upgrade-available" : "current", { + const state = buildState(manifestVersion, metadata); + const notice = state.needsUpgrade ? buildNotice({ cwd, locale, state }) : ""; + const checkedAt = fromCache ? Number(cache.checkedAt) : Date.now(); + if (!fromCache) { + try { + fs.mkdirSync(knowledgeDir, { recursive: true }); + fs.writeFileSync(cachePath, `${JSON.stringify({ + packageName, + ...state, + latestNpm: state.latestTemplateVersion, + notice, + checkedAt, + }, null, 2)}\n`, "utf8"); + } catch (_) {} + } + if (!state.needsUpgrade) { + try { fs.rmSync(cachePath, { force: true }); } catch (_) {} + } + return result(state.needsUpgrade ? "upgrade-available" : "current", { checked: true, + fromCache, packageName, - manifestVersion, - latestVersion, - needsUpgrade, + ...state, + latestVersion: state.latestCoreVersion, notice, checkedAt, }); @@ -228,4 +208,5 @@ module.exports = { DEFAULT_PACKAGE_NAME, compareVersions, checkUpdate, + queryLatestMetadata, }; diff --git a/packages/core/package.json b/packages/core/package.json index 5099263..f995495 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,7 @@ { "name": "@double-coding/flow2spec-core", "version": "3.5.0", + "templateVersion": "3.5.0", "description": "Flow2Spec Core APIs, knowledge engine, project initialization and shared resources", "homepage": "https://github.com/double-coding-lab/Flow2Spec#readme", "repository": { @@ -22,9 +23,6 @@ "access": "public", "registry": "https://registry.npmjs.org/" }, - "scripts": { - "prepack": "node ../../scripts/sync-core-templates.js --quiet" - }, "engines": { "node": ">=16" }, diff --git a/templates/en-US/AGENTS.codex-stub.md b/packages/core/templates/en-US/AGENTS.codex-stub.md similarity index 100% rename from templates/en-US/AGENTS.codex-stub.md rename to packages/core/templates/en-US/AGENTS.codex-stub.md diff --git a/templates/en-US/AGENTS.md b/packages/core/templates/en-US/AGENTS.md similarity index 100% rename from templates/en-US/AGENTS.md rename to packages/core/templates/en-US/AGENTS.md diff --git a/templates/en-US/flow2spec.config.json b/packages/core/templates/en-US/flow2spec.config.json similarity index 100% rename from templates/en-US/flow2spec.config.json rename to packages/core/templates/en-US/flow2spec.config.json diff --git a/templates/en-US/hooks/f2s-config-inject.js b/packages/core/templates/en-US/hooks/f2s-config-inject.js similarity index 100% rename from templates/en-US/hooks/f2s-config-inject.js rename to packages/core/templates/en-US/hooks/f2s-config-inject.js diff --git a/templates/en-US/hooks/f2s-config-session.js b/packages/core/templates/en-US/hooks/f2s-config-session.js similarity index 100% rename from templates/en-US/hooks/f2s-config-session.js rename to packages/core/templates/en-US/hooks/f2s-config-session.js 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..a1b77d3 --- /dev/null +++ b/packages/core/templates/en-US/hooks/f2s-update-check.js @@ -0,0 +1,156 @@ +#!/usr/bin/env node +'use strict'; + +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_FILE = path.join(process.cwd(), '.Knowledge', 'update-check.json'); +const PACKAGE_NAME_PLACEHOLDER = '__FLOW2SPEC_' + 'PACKAGE_NAME__'; +const PACKAGE_NAME = '__FLOW2SPEC_PACKAGE_NAME__'; +const GENERATED_CORE_VERSION = '__FLOW2SPEC_CORE_VERSION__'; +const GENERATED_TEMPLATE_VERSION = '__FLOW2SPEC_TEMPLATE_VERSION__'; + +function readJson(file) { + try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch (_) { return null; } +} + +function parseVer(value) { + return String(value || '').replace(/^v/, '').split(/[.-]/).slice(0, 3).map((part) => { + const number = Number.parseInt(part, 10); + return Number.isFinite(number) ? number : 0; + }); +} + +function cmpVer(left, right) { + const a = parseVer(left), b = parseVer(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 getProjectName() { + return readJson(path.join(process.cwd(), 'package.json'))?.name || path.basename(process.cwd()); +} + +function getManifestVersion() { + return readJson(MANIFEST_PATH)?.version || null; +} + +function getPackageName() { + return PACKAGE_NAME && PACKAGE_NAME !== PACKAGE_NAME_PLACEHOLDER + ? PACKAGE_NAME + : '@double-coding/flow2spec-core'; +} + +function isEnabled() { + const config = readJson(path.join(process.cwd(), 'flow2spec.config.json')); + return config?.updateCheck?.enabled !== false; +} + +function readCache() { + const cache = readJson(CACHE_FILE); + if (!cache?.checkedAt) return null; + return new Date(cache.checkedAt).toDateString() === new Date().toDateString() ? cache : null; +} + +function queryLatestMetadata(packageName) { + const output = execFileSync( + 'npm', + ['view', packageName, 'version', 'templateVersion', '--json', '--registry=https://registry.npmjs.org'], + { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] }, + ); + const metadata = JSON.parse(output); + const latestCoreVersion = typeof metadata === 'string' ? metadata : metadata.version; + const latestTemplateVersion = typeof metadata === 'string' + ? metadata + : metadata.templateVersion || metadata.version; + return { latestCoreVersion, latestTemplateVersion }; +} + +function updateState(metadata, manifestVersion) { + const coreUpdateAvailable = cmpVer(GENERATED_CORE_VERSION, metadata.latestCoreVersion) < 0; + const templateUpdateAvailable = cmpVer(manifestVersion, metadata.latestTemplateVersion) < 0; + return { + packageName: getPackageName(), + generatedByCoreVersion: GENERATED_CORE_VERSION, + generatedTemplateVersion: GENERATED_TEMPLATE_VERSION, + manifestVersion, + ...metadata, + coreUpdateAvailable, + templateUpdateAvailable, + needsUpgrade: coreUpdateAvailable || templateUpdateAvailable, + }; +} + +function buildNotice(state) { + const project = getProjectName(); + const versionSummary = `Core v${state.generatedByCoreVersion} -> v${state.latestCoreVersion}, Template v${state.manifestVersion} -> v${state.latestTemplateVersion}`; + if (state.templateUpdateAvailable) { + return [ + '[flow2spec/agent-instruction] At the start of this response, relay this update notice to the user:', + '', + `📦 [flow2spec] Project "${project}" has a template update (${versionSummary}).`, + '', + '[flow2spec/agent-instruction] You may run `flow2spec update --core`, then `flow2spec init `. After init, read `.Knowledge/manifest-routing.json`: if projectRev equals pkgRev, delete `.Knowledge/update-check.json`; otherwise continue with f2s-kb-upgrade from step 2c.', + ].join('\n'); + } + return [ + '[flow2spec/agent-instruction] At the start of this response, relay this update notice to the user:', + '', + `📦 [flow2spec] Project "${project}" has a Core-only update (${versionSummary}); the template version is unchanged.`, + '', + '[flow2spec/agent-instruction] You may run `flow2spec update --core`, then one idempotent `flow2spec init ` to refresh the Hook. Do not enter f2s-kb-upgrade when Template Version is unchanged; delete `.Knowledge/update-check.json` afterwards.', + ].join('\n'); +} + +function emitNotice(notice) { + process.stdout.write(JSON.stringify({ + additional_context: notice, + hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: notice }, + }) + '\n'); +} + +function writeCache(state) { + try { + fs.writeFileSync(CACHE_FILE, `${JSON.stringify({ + ...state, + latestNpm: state.latestTemplateVersion, + notice: state.needsUpgrade ? buildNotice(state) : '', + checkedAt: Date.now(), + }, null, 2)}\n`, 'utf8'); + } catch (_) {} +} + +function deleteCache() { + try { fs.rmSync(CACHE_FILE, { force: true }); } catch (_) {} +} + +function main() { + if (process.env.CI || process.env.CONTINUOUS_INTEGRATION || !isEnabled()) return; + const manifestVersion = getManifestVersion(); + if (!manifestVersion) return; + + const cached = readCache(); + if (cached) { + const state = updateState({ + latestCoreVersion: cached.latestCoreVersion, + latestTemplateVersion: cached.latestTemplateVersion || cached.latestNpm, + }, manifestVersion); + if (!state.needsUpgrade) deleteCache(); + else emitNotice(buildNotice(state)); + return; + } + + let metadata; + try { metadata = queryLatestMetadata(getPackageName()); } catch (_) { return; } + if (!metadata.latestCoreVersion || !metadata.latestTemplateVersion) return; + const state = updateState(metadata, manifestVersion); + writeCache(state); + if (state.needsUpgrade) emitNotice(buildNotice(state)); +} + +main(); diff --git a/templates/en-US/knowledge/index.md b/packages/core/templates/en-US/knowledge/index.md similarity index 100% rename from templates/en-US/knowledge/index.md rename to packages/core/templates/en-US/knowledge/index.md diff --git a/templates/en-US/knowledge/manifest-matchers.json b/packages/core/templates/en-US/knowledge/manifest-matchers.json similarity index 100% rename from templates/en-US/knowledge/manifest-matchers.json rename to packages/core/templates/en-US/knowledge/manifest-matchers.json diff --git a/templates/en-US/knowledge/manifest-routing.json b/packages/core/templates/en-US/knowledge/manifest-routing.json similarity index 100% rename from templates/en-US/knowledge/manifest-routing.json rename to packages/core/templates/en-US/knowledge/manifest-routing.json diff --git a/templates/en-US/knowledge/matchers/m-change-tracking.json b/packages/core/templates/en-US/knowledge/matchers/m-change-tracking.json similarity index 100% rename from templates/en-US/knowledge/matchers/m-change-tracking.json rename to packages/core/templates/en-US/knowledge/matchers/m-change-tracking.json diff --git a/templates/en-US/knowledge/matchers/m-doc-routing.json b/packages/core/templates/en-US/knowledge/matchers/m-doc-routing.json similarity index 100% rename from templates/en-US/knowledge/matchers/m-doc-routing.json rename to packages/core/templates/en-US/knowledge/matchers/m-doc-routing.json diff --git a/templates/en-US/knowledge/matchers/m-f2s-config-precheck.json b/packages/core/templates/en-US/knowledge/matchers/m-f2s-config-precheck.json similarity index 100% rename from templates/en-US/knowledge/matchers/m-f2s-config-precheck.json rename to packages/core/templates/en-US/knowledge/matchers/m-f2s-config-precheck.json diff --git a/templates/en-US/knowledge/matchers/m-flow2spec-dsh-adapter.json b/packages/core/templates/en-US/knowledge/matchers/m-flow2spec-dsh-adapter.json similarity index 100% rename from templates/en-US/knowledge/matchers/m-flow2spec-dsh-adapter.json rename to packages/core/templates/en-US/knowledge/matchers/m-flow2spec-dsh-adapter.json diff --git a/templates/en-US/knowledge/matchers/m-implement-from-spec.json b/packages/core/templates/en-US/knowledge/matchers/m-implement-from-spec.json similarity index 100% rename from templates/en-US/knowledge/matchers/m-implement-from-spec.json rename to packages/core/templates/en-US/knowledge/matchers/m-implement-from-spec.json diff --git a/templates/en-US/knowledge/matchers/m-req-plan.json b/packages/core/templates/en-US/knowledge/matchers/m-req-plan.json similarity index 100% rename from templates/en-US/knowledge/matchers/m-req-plan.json rename to packages/core/templates/en-US/knowledge/matchers/m-req-plan.json diff --git a/templates/en-US/knowledge/template/final-overview-template.md b/packages/core/templates/en-US/knowledge/template/final-overview-template.md similarity index 100% rename from templates/en-US/knowledge/template/final-overview-template.md rename to packages/core/templates/en-US/knowledge/template/final-overview-template.md diff --git a/templates/en-US/knowledge/template/project-milestone-template.md b/packages/core/templates/en-US/knowledge/template/project-milestone-template.md similarity index 100% rename from templates/en-US/knowledge/template/project-milestone-template.md rename to packages/core/templates/en-US/knowledge/template/project-milestone-template.md diff --git a/templates/en-US/knowledge/template/technical-spec-template.md b/packages/core/templates/en-US/knowledge/template/technical-spec-template.md similarity index 100% rename from templates/en-US/knowledge/template/technical-spec-template.md rename to packages/core/templates/en-US/knowledge/template/technical-spec-template.md diff --git a/templates/en-US/knowledge/topics/f2s-config-precheck.md b/packages/core/templates/en-US/knowledge/topics/f2s-config-precheck.md similarity index 100% rename from templates/en-US/knowledge/topics/f2s-config-precheck.md rename to packages/core/templates/en-US/knowledge/topics/f2s-config-precheck.md diff --git a/templates/en-US/knowledge/topics/f2s-fallback-triage.md b/packages/core/templates/en-US/knowledge/topics/f2s-fallback-triage.md similarity index 100% rename from templates/en-US/knowledge/topics/f2s-fallback-triage.md rename to packages/core/templates/en-US/knowledge/topics/f2s-fallback-triage.md diff --git a/templates/en-US/knowledge/topics/f2s-implement-tech-design.md b/packages/core/templates/en-US/knowledge/topics/f2s-implement-tech-design.md similarity index 100% rename from templates/en-US/knowledge/topics/f2s-implement-tech-design.md rename to packages/core/templates/en-US/knowledge/topics/f2s-implement-tech-design.md diff --git a/templates/en-US/knowledge/topics/f2s-req-plan.md b/packages/core/templates/en-US/knowledge/topics/f2s-req-plan.md similarity index 100% rename from templates/en-US/knowledge/topics/f2s-req-plan.md rename to packages/core/templates/en-US/knowledge/topics/f2s-req-plan.md diff --git a/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 similarity index 100% rename from templates/en-US/knowledge/topics/f2s-stock-docs-vs-req-docs.md rename to packages/core/templates/en-US/knowledge/topics/f2s-stock-docs-vs-req-docs.md diff --git a/templates/en-US/knowledge/topics/f2s-task.md b/packages/core/templates/en-US/knowledge/topics/f2s-task.md similarity index 100% rename from templates/en-US/knowledge/topics/f2s-task.md rename to packages/core/templates/en-US/knowledge/topics/f2s-task.md diff --git a/templates/en-US/knowledge/topics/flow2spec-dsh-adapter.md b/packages/core/templates/en-US/knowledge/topics/flow2spec-dsh-adapter.md similarity index 100% rename from templates/en-US/knowledge/topics/flow2spec-dsh-adapter.md rename to packages/core/templates/en-US/knowledge/topics/flow2spec-dsh-adapter.md diff --git a/templates/en-US/rules/f2s-config-check.md b/packages/core/templates/en-US/rules/f2s-config-check.md similarity index 100% rename from templates/en-US/rules/f2s-config-check.md rename to packages/core/templates/en-US/rules/f2s-config-check.md diff --git a/templates/en-US/rules/f2s-flow2spec-unified-entry.md b/packages/core/templates/en-US/rules/f2s-flow2spec-unified-entry.md similarity index 94% rename from templates/en-US/rules/f2s-flow2spec-unified-entry.md rename to packages/core/templates/en-US/rules/f2s-flow2spec-unified-entry.md index 928bdeb..bda6d66 100644 --- a/templates/en-US/rules/f2s-flow2spec-unified-entry.md +++ b/packages/core/templates/en-US/rules/f2s-flow2spec-unified-entry.md @@ -97,9 +97,9 @@ Each initialized client uses its own startup/update mechanism when supported; th **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 relay the upgrade notice, and the agent may **directly run** `flow2spec init ` on the user's behalf to complete the update — after init, read `.Knowledge/manifest-routing.json`: if `projectRev` equals `pkgRev` (a non-topic version update), the update is complete, delete the cache; if they differ (topic-layer changes included), run the full `f2s-kb-upgrade` flow (starting from its step 2c, without re-running init). If the current `.Knowledge/manifest-routing.json.version` is already no lower than `latestNpm`, delete that cache and stop reminding. +2. Read `.Knowledge/update-check.json` -> if the file exists and `checkedAt` is on the same local calendar day, do not query npm again. When `coreUpdateAvailable=true`, the agent may run `flow2spec update --core`. If `templateUpdateAvailable=false`, run one idempotent `flow2spec init ` to refresh the Hook, delete the cache, and do not enter `f2s-kb-upgrade`. If `templateUpdateAvailable=true`, update Core, run init, then use `projectRev` / `pkgRev` to choose the fast path or full flow. `.Knowledge/manifest-routing.json.version` is Template Version and must not be compared directly with Core Version. 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 and follow its agent-instruction (for non-topic version updates the agent may run `flow2spec init` directly; run the `f2s-kb-upgrade` skill only for topic-layer changes). + - If it contains `hookSpecificOutput.additionalContext`: **tell the user** that content and follow its separate Core-only or Template-update instructions. - If there is no output or parsing fails: stay silent. 4. If any step errors, silently skip it and do not affect normal conversation. diff --git a/templates/en-US/rules/f2s-implement-tech-design.md b/packages/core/templates/en-US/rules/f2s-implement-tech-design.md similarity index 100% rename from templates/en-US/rules/f2s-implement-tech-design.md rename to packages/core/templates/en-US/rules/f2s-implement-tech-design.md diff --git a/templates/en-US/rules/f2s-intent-routing.md b/packages/core/templates/en-US/rules/f2s-intent-routing.md similarity index 100% rename from templates/en-US/rules/f2s-intent-routing.md rename to packages/core/templates/en-US/rules/f2s-intent-routing.md diff --git a/templates/en-US/rules/f2s-karpathy-guidelines.md b/packages/core/templates/en-US/rules/f2s-karpathy-guidelines.md similarity index 100% rename from templates/en-US/rules/f2s-karpathy-guidelines.md rename to packages/core/templates/en-US/rules/f2s-karpathy-guidelines.md diff --git a/templates/en-US/rules/f2s-kb-feedback-closing.md b/packages/core/templates/en-US/rules/f2s-kb-feedback-closing.md similarity index 100% rename from templates/en-US/rules/f2s-kb-feedback-closing.md rename to packages/core/templates/en-US/rules/f2s-kb-feedback-closing.md diff --git a/templates/en-US/rules/f2s-knowledge-preflight.md b/packages/core/templates/en-US/rules/f2s-knowledge-preflight.md similarity index 100% rename from templates/en-US/rules/f2s-knowledge-preflight.md rename to packages/core/templates/en-US/rules/f2s-knowledge-preflight.md diff --git a/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 similarity index 100% rename from templates/en-US/rules/f2s-stock-docs-vs-req-docs.md rename to packages/core/templates/en-US/rules/f2s-stock-docs-vs-req-docs.md diff --git a/templates/en-US/rules/f2s-task.md b/packages/core/templates/en-US/rules/f2s-task.md similarity index 100% rename from templates/en-US/rules/f2s-task.md rename to packages/core/templates/en-US/rules/f2s-task.md diff --git a/templates/en-US/rules/f2s-topic-authoring.md b/packages/core/templates/en-US/rules/f2s-topic-authoring.md similarity index 100% rename from templates/en-US/rules/f2s-topic-authoring.md rename to packages/core/templates/en-US/rules/f2s-topic-authoring.md diff --git a/templates/en-US/skills/f2s-doc-arch/SKILL.md b/packages/core/templates/en-US/skills/f2s-doc-arch/SKILL.md similarity index 100% rename from templates/en-US/skills/f2s-doc-arch/SKILL.md rename to packages/core/templates/en-US/skills/f2s-doc-arch/SKILL.md diff --git a/templates/en-US/skills/f2s-doc-final/SKILL.md b/packages/core/templates/en-US/skills/f2s-doc-final/SKILL.md similarity index 100% rename from templates/en-US/skills/f2s-doc-final/SKILL.md rename to packages/core/templates/en-US/skills/f2s-doc-final/SKILL.md diff --git a/templates/en-US/skills/f2s-doc-milestone/SKILL.md b/packages/core/templates/en-US/skills/f2s-doc-milestone/SKILL.md similarity index 100% rename from templates/en-US/skills/f2s-doc-milestone/SKILL.md rename to packages/core/templates/en-US/skills/f2s-doc-milestone/SKILL.md diff --git a/templates/en-US/skills/f2s-doc-pdf/SKILL.md b/packages/core/templates/en-US/skills/f2s-doc-pdf/SKILL.md similarity index 100% rename from templates/en-US/skills/f2s-doc-pdf/SKILL.md rename to packages/core/templates/en-US/skills/f2s-doc-pdf/SKILL.md diff --git a/templates/en-US/skills/f2s-git-commit/SKILL.md b/packages/core/templates/en-US/skills/f2s-git-commit/SKILL.md similarity index 100% rename from templates/en-US/skills/f2s-git-commit/SKILL.md rename to packages/core/templates/en-US/skills/f2s-git-commit/SKILL.md diff --git a/templates/en-US/skills/f2s-kb-add/SKILL.md b/packages/core/templates/en-US/skills/f2s-kb-add/SKILL.md similarity index 100% rename from templates/en-US/skills/f2s-kb-add/SKILL.md rename to packages/core/templates/en-US/skills/f2s-kb-add/SKILL.md diff --git a/templates/en-US/skills/f2s-kb-addRules/SKILL.md b/packages/core/templates/en-US/skills/f2s-kb-addRules/SKILL.md similarity index 100% rename from templates/en-US/skills/f2s-kb-addRules/SKILL.md rename to packages/core/templates/en-US/skills/f2s-kb-addRules/SKILL.md diff --git a/templates/en-US/skills/f2s-kb-build/SKILL.md b/packages/core/templates/en-US/skills/f2s-kb-build/SKILL.md similarity index 100% rename from templates/en-US/skills/f2s-kb-build/SKILL.md rename to packages/core/templates/en-US/skills/f2s-kb-build/SKILL.md diff --git a/templates/en-US/skills/f2s-kb-distill/SKILL.md b/packages/core/templates/en-US/skills/f2s-kb-distill/SKILL.md similarity index 100% rename from templates/en-US/skills/f2s-kb-distill/SKILL.md rename to packages/core/templates/en-US/skills/f2s-kb-distill/SKILL.md diff --git a/templates/en-US/skills/f2s-kb-feat/SKILL.md b/packages/core/templates/en-US/skills/f2s-kb-feat/SKILL.md similarity index 100% rename from templates/en-US/skills/f2s-kb-feat/SKILL.md rename to packages/core/templates/en-US/skills/f2s-kb-feat/SKILL.md diff --git a/templates/en-US/skills/f2s-kb-fix/SKILL.md b/packages/core/templates/en-US/skills/f2s-kb-fix/SKILL.md similarity index 100% rename from templates/en-US/skills/f2s-kb-fix/SKILL.md rename to packages/core/templates/en-US/skills/f2s-kb-fix/SKILL.md diff --git a/templates/en-US/skills/f2s-kb-merge/SKILL.md b/packages/core/templates/en-US/skills/f2s-kb-merge/SKILL.md similarity index 100% rename from templates/en-US/skills/f2s-kb-merge/SKILL.md rename to packages/core/templates/en-US/skills/f2s-kb-merge/SKILL.md diff --git a/templates/en-US/skills/f2s-kb-rm/SKILL.md b/packages/core/templates/en-US/skills/f2s-kb-rm/SKILL.md similarity index 100% rename from templates/en-US/skills/f2s-kb-rm/SKILL.md rename to packages/core/templates/en-US/skills/f2s-kb-rm/SKILL.md diff --git a/templates/en-US/skills/f2s-kb-sync/SKILL.md b/packages/core/templates/en-US/skills/f2s-kb-sync/SKILL.md similarity index 100% rename from templates/en-US/skills/f2s-kb-sync/SKILL.md rename to packages/core/templates/en-US/skills/f2s-kb-sync/SKILL.md diff --git a/templates/en-US/skills/f2s-kb-upgrade/SKILL.md b/packages/core/templates/en-US/skills/f2s-kb-upgrade/SKILL.md similarity index 83% rename from templates/en-US/skills/f2s-kb-upgrade/SKILL.md rename to packages/core/templates/en-US/skills/f2s-kb-upgrade/SKILL.md index 6a0f366..7640eb2 100644 --- a/templates/en-US/skills/f2s-kb-upgrade/SKILL.md +++ b/packages/core/templates/en-US/skills/f2s-kb-upgrade/SKILL.md @@ -12,12 +12,12 @@ description: Knowledge-base template upgrade skill (this SKILL only): **current ## 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 completes **directory, template placeholder, and routing-structure alignment under Template Version**. Core code version and Template Version are independent. 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 location**: root-level integer field `projectRev` in the Core package's `knowledge/manifest-routing.json` template (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). @@ -27,15 +27,15 @@ description: Knowledge-base template upgrade skill (this SKILL only): **current - 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`. +- Any body change / addition / deletion / rename of `knowledge/topics/.md`; +- `includeAny` entries, `id` changes, or addition / deletion of any `knowledge/matchers/.json`; +- Any change in `topicPaths` / `taskToTopicRules` / `topicDependencies` / `fallbackTopic` / `topicMetadata` of `knowledge/manifest-routing.json`; +- Any change in the "topic overview" section or package-level sections of `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). +- `flow2spec.config.json` default values; +- `rules/*` / `skills/*` rule/skill body changes (unrelated to the topic layer; no need to trigger the full flow, but Template Version must still be raised explicitly so the Hook distributes the new content). **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. @@ -54,11 +54,12 @@ description: Knowledge-base template upgrade skill (this SKILL only): **current ## Non-topic version updates: the agent may run `init` directly (no need to enter this skill) -When the version check reports "knowledge-base version is lower than the latest package version", the agent may **directly run** `flow2spec init ` on the user's behalf to complete the update: +When the version check reports a Core update with `templateUpdateAvailable=false`, the agent may **update Core and directly run** `flow2spec init `: - After `init`, read the project-side `.Knowledge/manifest-routing.json`: if `projectRev` **equals** `pkgRev` (a non-topic version update) -> the update is complete, delete `.Knowledge/update-check.json`, **no need to enter this skill**; - If they **differ** (topic-layer changes included) -> enter this skill's full flow (judge from step 2c, without re-running `init`). - This path uses the same judgment as this skill's step 2c "fast path"; when the user explicitly asks for a "full flow / reset", still execute this skill in full. +- `.Knowledge/manifest-routing.json.version` is Template Version; never compare it directly with Core Version. ## Legacy layouts (V1) no longer have built-in migration @@ -97,39 +98,24 @@ This skill executes **`flow2spec init`** in **step 2**. `init` syncs the current ## Mandatory Flow -### Step -1: Global flow2spec Version Preflight (Required, Before Everything, Foreground Probe by Main Agent) +### Step -1: CLI/Core/Template Version Preflight (Required, Before Everything) -**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): +The main agent runs these read-only probes in the foreground: ```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__ +flow2spec version +flow2spec update --check ``` -**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. +Record CLI Version, Core Version, Core Range, Template Version, Protocol Version, and the latest npm Core/Template values: -**Relation to cli.js**: +| Case | Action | Default step 2 command | +| --- | --- | --- | +| **A. Template is current** | If only Core changed, run `flow2spec update --core`, then one idempotent init to refresh the Hook, clear the cache, and **stop this skill**. If Core is also current, stop immediately. | `flow2spec init ` | +| **B. Template changed and latest Core is inside Core Range** | Run `flow2spec update --core`, then continue to step 0. A compatible Core update does not require a CLI release or upgrade. | `flow2spec init ` | +| **C. Not installed, unknown, or latest Core is outside the range** | Use an explicit latest CLI/Core combination so npx cannot reuse an old Core. If latest Core still is not compatible, upgrade to a CLI that supports it first. | `npx --yes --package @latest --package @latest flow2spec init ` | -- `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. +If the preflight fails, case C is allowed as fallback, but never treat Core Version as Template Version. This step does not mandate a sub-agent or a background global install. ### Step 0: Version Judgment and Branching (Required, Before init) @@ -165,10 +151,10 @@ Both conditions are met: 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**): +1. **Step -1 returned A/B (local CLI/Core is usable)**: use the current CLI: - `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 ` +2. **Step -1 returned C**: explicitly combine latest CLI/Core so this session gets the latest Core and templates: + - `npx --yes --package @latest --package @latest flow2spec init ` 3. For overwrite reset: - Append `--reset-knowledge` to the above command. 4. If the user explicitly requests a template-language switch: @@ -177,7 +163,7 @@ Run one of the following in the target project root (**choose the default form b > `` 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. +> **Helper commands (user self-inspection)**: `flow2spec version` shows the five version dimensions; `flow2spec update --check|--cli|--core` checks updates, updates CLI, or updates a compatible Core. These commands do not replace this skill's full flow after Template Version changes. **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). @@ -320,7 +306,7 @@ Output: ## 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: `` +- 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` @@ -351,7 +337,7 @@ Output: ## 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. +1. **Step -1** was performed: `flow2spec version` and `flow2spec update --check` recorded CLI/Core/Core Range/Template/Protocol; Core-only updates refreshed Core/Hook and stopped, while Template updates selected the current CLI or an explicit latest CLI/Core combination through A/B/C. 2. **Step 0** was performed: on V1 the skill stopped and told the user how to proceed, and **current repositories (V2+)** entered the `init` flow normally. 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"). diff --git a/templates/en-US/skills/f2s-req-clarify/SKILL.md b/packages/core/templates/en-US/skills/f2s-req-clarify/SKILL.md similarity index 100% rename from templates/en-US/skills/f2s-req-clarify/SKILL.md rename to packages/core/templates/en-US/skills/f2s-req-clarify/SKILL.md diff --git a/templates/en-US/skills/f2s-req-plan/SKILL.md b/packages/core/templates/en-US/skills/f2s-req-plan/SKILL.md similarity index 100% rename from templates/en-US/skills/f2s-req-plan/SKILL.md rename to packages/core/templates/en-US/skills/f2s-req-plan/SKILL.md diff --git a/templates/en-US/skills/f2s-req-tech/SKILL.md b/packages/core/templates/en-US/skills/f2s-req-tech/SKILL.md similarity index 100% rename from templates/en-US/skills/f2s-req-tech/SKILL.md rename to packages/core/templates/en-US/skills/f2s-req-tech/SKILL.md diff --git a/templates/zh-CN/AGENTS.codex-stub.md b/packages/core/templates/zh-CN/AGENTS.codex-stub.md similarity index 100% rename from templates/zh-CN/AGENTS.codex-stub.md rename to packages/core/templates/zh-CN/AGENTS.codex-stub.md diff --git a/templates/zh-CN/AGENTS.md b/packages/core/templates/zh-CN/AGENTS.md similarity index 100% rename from templates/zh-CN/AGENTS.md rename to packages/core/templates/zh-CN/AGENTS.md diff --git a/templates/zh-CN/flow2spec.config.json b/packages/core/templates/zh-CN/flow2spec.config.json similarity index 100% rename from templates/zh-CN/flow2spec.config.json rename to packages/core/templates/zh-CN/flow2spec.config.json diff --git a/templates/zh-CN/hooks/f2s-config-inject.js b/packages/core/templates/zh-CN/hooks/f2s-config-inject.js similarity index 100% rename from templates/zh-CN/hooks/f2s-config-inject.js rename to packages/core/templates/zh-CN/hooks/f2s-config-inject.js diff --git a/templates/zh-CN/hooks/f2s-config-session.js b/packages/core/templates/zh-CN/hooks/f2s-config-session.js similarity index 100% rename from templates/zh-CN/hooks/f2s-config-session.js rename to packages/core/templates/zh-CN/hooks/f2s-config-session.js 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..b28b717 --- /dev/null +++ b/packages/core/templates/zh-CN/hooks/f2s-update-check.js @@ -0,0 +1,156 @@ +#!/usr/bin/env node +'use strict'; + +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_FILE = path.join(process.cwd(), '.Knowledge', 'update-check.json'); +const PACKAGE_NAME_PLACEHOLDER = '__FLOW2SPEC_' + 'PACKAGE_NAME__'; +const PACKAGE_NAME = '__FLOW2SPEC_PACKAGE_NAME__'; +const GENERATED_CORE_VERSION = '__FLOW2SPEC_CORE_VERSION__'; +const GENERATED_TEMPLATE_VERSION = '__FLOW2SPEC_TEMPLATE_VERSION__'; + +function readJson(file) { + try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch (_) { return null; } +} + +function parseVer(value) { + return String(value || '').replace(/^v/, '').split(/[.-]/).slice(0, 3).map((part) => { + const number = Number.parseInt(part, 10); + return Number.isFinite(number) ? number : 0; + }); +} + +function cmpVer(left, right) { + const a = parseVer(left), b = parseVer(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 getProjectName() { + return readJson(path.join(process.cwd(), 'package.json'))?.name || path.basename(process.cwd()); +} + +function getManifestVersion() { + return readJson(MANIFEST_PATH)?.version || null; +} + +function getPackageName() { + return PACKAGE_NAME && PACKAGE_NAME !== PACKAGE_NAME_PLACEHOLDER + ? PACKAGE_NAME + : '@double-coding/flow2spec-core'; +} + +function isEnabled() { + const config = readJson(path.join(process.cwd(), 'flow2spec.config.json')); + return config?.updateCheck?.enabled !== false; +} + +function readCache() { + const cache = readJson(CACHE_FILE); + if (!cache?.checkedAt) return null; + return new Date(cache.checkedAt).toDateString() === new Date().toDateString() ? cache : null; +} + +function queryLatestMetadata(packageName) { + const output = execFileSync( + 'npm', + ['view', packageName, 'version', 'templateVersion', '--json', '--registry=https://registry.npmjs.org'], + { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] }, + ); + const metadata = JSON.parse(output); + const latestCoreVersion = typeof metadata === 'string' ? metadata : metadata.version; + const latestTemplateVersion = typeof metadata === 'string' + ? metadata + : metadata.templateVersion || metadata.version; + return { latestCoreVersion, latestTemplateVersion }; +} + +function updateState(metadata, manifestVersion) { + const coreUpdateAvailable = cmpVer(GENERATED_CORE_VERSION, metadata.latestCoreVersion) < 0; + const templateUpdateAvailable = cmpVer(manifestVersion, metadata.latestTemplateVersion) < 0; + return { + packageName: getPackageName(), + generatedByCoreVersion: GENERATED_CORE_VERSION, + generatedTemplateVersion: GENERATED_TEMPLATE_VERSION, + manifestVersion, + ...metadata, + coreUpdateAvailable, + templateUpdateAvailable, + needsUpgrade: coreUpdateAvailable || templateUpdateAvailable, + }; +} + +function buildNotice(state) { + const project = getProjectName(); + const versionSummary = `Core v${state.generatedByCoreVersion} -> v${state.latestCoreVersion},Template v${state.manifestVersion} -> v${state.latestTemplateVersion}`; + if (state.templateUpdateAvailable) { + return [ + '[flow2spec/agent-instruction] 你必须在本轮回复开头转告用户以下更新提示:', + '', + `📦 [flow2spec] 当前项目「${project}」有模板更新(${versionSummary})。`, + '', + '[flow2spec/agent-instruction] 可先执行 `flow2spec update --core`,再执行 `flow2spec init <已初始化的 agents>`。init 后读取 `.Knowledge/manifest-routing.json`:projectRev 与 pkgRev 相等则更新完成并删除 `.Knowledge/update-check.json`;不等时再执行 f2s-kb-upgrade(可从步骤 2c 继续)。', + ].join('\n'); + } + return [ + '[flow2spec/agent-instruction] 你必须在本轮回复开头转告用户以下更新提示:', + '', + `📦 [flow2spec] 当前项目「${project}」仅有 Core 程序更新(${versionSummary}),模板版本未变化。`, + '', + '[flow2spec/agent-instruction] 可执行 `flow2spec update --core`,随后执行一次幂等 `flow2spec init <已初始化的 agents>` 刷新 Hook。Template Version 未变化,不进入 f2s-kb-upgrade;完成后删除 `.Knowledge/update-check.json`。', + ].join('\n'); +} + +function emitNotice(notice) { + process.stdout.write(JSON.stringify({ + additional_context: notice, + hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: notice }, + }) + '\n'); +} + +function writeCache(state) { + try { + fs.writeFileSync(CACHE_FILE, `${JSON.stringify({ + ...state, + latestNpm: state.latestTemplateVersion, + notice: state.needsUpgrade ? buildNotice(state) : '', + checkedAt: Date.now(), + }, null, 2)}\n`, 'utf8'); + } catch (_) {} +} + +function deleteCache() { + try { fs.rmSync(CACHE_FILE, { force: true }); } catch (_) {} +} + +function main() { + if (process.env.CI || process.env.CONTINUOUS_INTEGRATION || !isEnabled()) return; + const manifestVersion = getManifestVersion(); + if (!manifestVersion) return; + + const cached = readCache(); + if (cached) { + const state = updateState({ + latestCoreVersion: cached.latestCoreVersion, + latestTemplateVersion: cached.latestTemplateVersion || cached.latestNpm, + }, manifestVersion); + if (!state.needsUpgrade) deleteCache(); + else emitNotice(buildNotice(state)); + return; + } + + let metadata; + try { metadata = queryLatestMetadata(getPackageName()); } catch (_) { return; } + if (!metadata.latestCoreVersion || !metadata.latestTemplateVersion) return; + const state = updateState(metadata, manifestVersion); + writeCache(state); + if (state.needsUpgrade) emitNotice(buildNotice(state)); +} + +main(); diff --git a/templates/zh-CN/knowledge/index.md b/packages/core/templates/zh-CN/knowledge/index.md similarity index 100% rename from templates/zh-CN/knowledge/index.md rename to packages/core/templates/zh-CN/knowledge/index.md diff --git a/templates/zh-CN/knowledge/manifest-matchers.json b/packages/core/templates/zh-CN/knowledge/manifest-matchers.json similarity index 100% rename from templates/zh-CN/knowledge/manifest-matchers.json rename to packages/core/templates/zh-CN/knowledge/manifest-matchers.json diff --git a/templates/zh-CN/knowledge/manifest-routing.json b/packages/core/templates/zh-CN/knowledge/manifest-routing.json similarity index 100% rename from templates/zh-CN/knowledge/manifest-routing.json rename to packages/core/templates/zh-CN/knowledge/manifest-routing.json diff --git a/templates/zh-CN/knowledge/matchers/m-change-tracking.json b/packages/core/templates/zh-CN/knowledge/matchers/m-change-tracking.json similarity index 100% rename from templates/zh-CN/knowledge/matchers/m-change-tracking.json rename to packages/core/templates/zh-CN/knowledge/matchers/m-change-tracking.json diff --git a/templates/zh-CN/knowledge/matchers/m-doc-routing.json b/packages/core/templates/zh-CN/knowledge/matchers/m-doc-routing.json similarity index 100% rename from templates/zh-CN/knowledge/matchers/m-doc-routing.json rename to packages/core/templates/zh-CN/knowledge/matchers/m-doc-routing.json diff --git a/templates/zh-CN/knowledge/matchers/m-f2s-config-precheck.json b/packages/core/templates/zh-CN/knowledge/matchers/m-f2s-config-precheck.json similarity index 100% rename from templates/zh-CN/knowledge/matchers/m-f2s-config-precheck.json rename to packages/core/templates/zh-CN/knowledge/matchers/m-f2s-config-precheck.json diff --git a/templates/zh-CN/knowledge/matchers/m-flow2spec-dsh-adapter.json b/packages/core/templates/zh-CN/knowledge/matchers/m-flow2spec-dsh-adapter.json similarity index 100% rename from templates/zh-CN/knowledge/matchers/m-flow2spec-dsh-adapter.json rename to packages/core/templates/zh-CN/knowledge/matchers/m-flow2spec-dsh-adapter.json diff --git a/templates/zh-CN/knowledge/matchers/m-implement-from-spec.json b/packages/core/templates/zh-CN/knowledge/matchers/m-implement-from-spec.json similarity index 100% rename from templates/zh-CN/knowledge/matchers/m-implement-from-spec.json rename to packages/core/templates/zh-CN/knowledge/matchers/m-implement-from-spec.json diff --git a/templates/zh-CN/knowledge/matchers/m-req-plan.json b/packages/core/templates/zh-CN/knowledge/matchers/m-req-plan.json similarity index 100% rename from templates/zh-CN/knowledge/matchers/m-req-plan.json rename to packages/core/templates/zh-CN/knowledge/matchers/m-req-plan.json diff --git "a/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" similarity index 100% rename from "templates/zh-CN/knowledge/template/\346\212\200\346\234\257\346\226\271\346\241\210\346\250\241\347\211\210.md" rename to "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" diff --git "a/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" similarity index 100% rename from "templates/zh-CN/knowledge/template/\347\273\210\347\250\277\346\250\241\347\211\210.md" rename to "packages/core/templates/zh-CN/knowledge/template/\347\273\210\347\250\277\346\250\241\347\211\210.md" diff --git "a/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" similarity index 100% rename from "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" rename to "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" diff --git a/templates/zh-CN/knowledge/topics/f2s-config-precheck.md b/packages/core/templates/zh-CN/knowledge/topics/f2s-config-precheck.md similarity index 100% rename from templates/zh-CN/knowledge/topics/f2s-config-precheck.md rename to packages/core/templates/zh-CN/knowledge/topics/f2s-config-precheck.md diff --git a/templates/zh-CN/knowledge/topics/f2s-fallback-triage.md b/packages/core/templates/zh-CN/knowledge/topics/f2s-fallback-triage.md similarity index 100% rename from templates/zh-CN/knowledge/topics/f2s-fallback-triage.md rename to packages/core/templates/zh-CN/knowledge/topics/f2s-fallback-triage.md diff --git a/templates/zh-CN/knowledge/topics/f2s-implement-tech-design.md b/packages/core/templates/zh-CN/knowledge/topics/f2s-implement-tech-design.md similarity index 100% rename from templates/zh-CN/knowledge/topics/f2s-implement-tech-design.md rename to packages/core/templates/zh-CN/knowledge/topics/f2s-implement-tech-design.md diff --git a/templates/zh-CN/knowledge/topics/f2s-req-plan.md b/packages/core/templates/zh-CN/knowledge/topics/f2s-req-plan.md similarity index 100% rename from templates/zh-CN/knowledge/topics/f2s-req-plan.md rename to packages/core/templates/zh-CN/knowledge/topics/f2s-req-plan.md diff --git a/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 similarity index 100% rename from templates/zh-CN/knowledge/topics/f2s-stock-docs-vs-req-docs.md rename to packages/core/templates/zh-CN/knowledge/topics/f2s-stock-docs-vs-req-docs.md diff --git a/templates/zh-CN/knowledge/topics/f2s-task.md b/packages/core/templates/zh-CN/knowledge/topics/f2s-task.md similarity index 100% rename from templates/zh-CN/knowledge/topics/f2s-task.md rename to packages/core/templates/zh-CN/knowledge/topics/f2s-task.md diff --git a/templates/zh-CN/knowledge/topics/flow2spec-dsh-adapter.md b/packages/core/templates/zh-CN/knowledge/topics/flow2spec-dsh-adapter.md similarity index 100% rename from templates/zh-CN/knowledge/topics/flow2spec-dsh-adapter.md rename to packages/core/templates/zh-CN/knowledge/topics/flow2spec-dsh-adapter.md diff --git a/templates/zh-CN/rules/f2s-config-check.md b/packages/core/templates/zh-CN/rules/f2s-config-check.md similarity index 100% rename from templates/zh-CN/rules/f2s-config-check.md rename to packages/core/templates/zh-CN/rules/f2s-config-check.md diff --git a/templates/zh-CN/rules/f2s-flow2spec-unified-entry.md b/packages/core/templates/zh-CN/rules/f2s-flow2spec-unified-entry.md similarity index 94% rename from templates/zh-CN/rules/f2s-flow2spec-unified-entry.md rename to packages/core/templates/zh-CN/rules/f2s-flow2spec-unified-entry.md index f28b98a..5c8b409 100644 --- a/templates/zh-CN/rules/f2s-flow2spec-unified-entry.md +++ b/packages/core/templates/zh-CN/rules/f2s-flow2spec-unified-entry.md @@ -97,9 +97,9 @@ alwaysApply: true **规则层双保险**(与脚本缓存互为备份): 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`,本会话首次回复用户时仍须转告升级提示,并可**直接替用户执行** `flow2spec init <已初始化的 agents>` 完成更新——init 后读 `.Knowledge/manifest-routing.json`:`projectRev` 与 `pkgRev` 相等(非主题版本更新)即完成并删除该缓存;不等(含主题层变更)再执行 `f2s-kb-upgrade` 完整流程(可从其步骤 2c 起,不重复 init);若当前 `.Knowledge/manifest-routing.json.version` 已不低于 `latestNpm`,删除该缓存并不再提示。 +2. 读 `.Knowledge/update-check.json` → 若文件存在且 `checkedAt` 与今日为同一自然日,不重复查 npm;`coreUpdateAvailable=true` 时可执行 `flow2spec update --core`。若 `templateUpdateAvailable=false`,随后只执行一次幂等 `flow2spec init <已初始化的 agents>` 刷新 Hook,删除缓存,不进入 `f2s-kb-upgrade`;若 `templateUpdateAvailable=true`,更新 Core 后执行 init,再以 `projectRev` / `pkgRev` 判定快速路径或完整流程。`.Knowledge/manifest-routing.json.version` 表示 Template Version,禁止与 Core Version 直接比较。 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`:**告知用户**该内容,并按其中 agent-instruction 执行(非主题版本更新可直接代跑 `flow2spec init`;主题层变更再执行 `f2s-kb-upgrade` skill)。 + - 若含 `hookSpecificOutput.additionalContext`:**告知用户**该内容,并按其中 agent-instruction 分别处理 Core-only 与 Template 更新。 - 无输出或解析失败:静默,不提示。 4. 以上步骤出现任何错误,静默跳过,不影响正常对话。 diff --git a/templates/zh-CN/rules/f2s-implement-tech-design.md b/packages/core/templates/zh-CN/rules/f2s-implement-tech-design.md similarity index 100% rename from templates/zh-CN/rules/f2s-implement-tech-design.md rename to packages/core/templates/zh-CN/rules/f2s-implement-tech-design.md diff --git a/templates/zh-CN/rules/f2s-intent-routing.md b/packages/core/templates/zh-CN/rules/f2s-intent-routing.md similarity index 100% rename from templates/zh-CN/rules/f2s-intent-routing.md rename to packages/core/templates/zh-CN/rules/f2s-intent-routing.md diff --git a/templates/zh-CN/rules/f2s-karpathy-guidelines.md b/packages/core/templates/zh-CN/rules/f2s-karpathy-guidelines.md similarity index 100% rename from templates/zh-CN/rules/f2s-karpathy-guidelines.md rename to packages/core/templates/zh-CN/rules/f2s-karpathy-guidelines.md diff --git a/templates/zh-CN/rules/f2s-kb-feedback-closing.md b/packages/core/templates/zh-CN/rules/f2s-kb-feedback-closing.md similarity index 100% rename from templates/zh-CN/rules/f2s-kb-feedback-closing.md rename to packages/core/templates/zh-CN/rules/f2s-kb-feedback-closing.md diff --git a/templates/zh-CN/rules/f2s-knowledge-preflight.md b/packages/core/templates/zh-CN/rules/f2s-knowledge-preflight.md similarity index 100% rename from templates/zh-CN/rules/f2s-knowledge-preflight.md rename to packages/core/templates/zh-CN/rules/f2s-knowledge-preflight.md diff --git a/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 similarity index 100% rename from templates/zh-CN/rules/f2s-stock-docs-vs-req-docs.md rename to packages/core/templates/zh-CN/rules/f2s-stock-docs-vs-req-docs.md diff --git a/templates/zh-CN/rules/f2s-task.md b/packages/core/templates/zh-CN/rules/f2s-task.md similarity index 100% rename from templates/zh-CN/rules/f2s-task.md rename to packages/core/templates/zh-CN/rules/f2s-task.md diff --git a/templates/zh-CN/rules/f2s-topic-authoring.md b/packages/core/templates/zh-CN/rules/f2s-topic-authoring.md similarity index 100% rename from templates/zh-CN/rules/f2s-topic-authoring.md rename to packages/core/templates/zh-CN/rules/f2s-topic-authoring.md diff --git a/templates/zh-CN/skills/f2s-doc-arch/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-doc-arch/SKILL.md similarity index 100% rename from templates/zh-CN/skills/f2s-doc-arch/SKILL.md rename to packages/core/templates/zh-CN/skills/f2s-doc-arch/SKILL.md diff --git a/templates/zh-CN/skills/f2s-doc-final/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-doc-final/SKILL.md similarity index 100% rename from templates/zh-CN/skills/f2s-doc-final/SKILL.md rename to packages/core/templates/zh-CN/skills/f2s-doc-final/SKILL.md diff --git a/templates/zh-CN/skills/f2s-doc-milestone/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-doc-milestone/SKILL.md similarity index 100% rename from templates/zh-CN/skills/f2s-doc-milestone/SKILL.md rename to packages/core/templates/zh-CN/skills/f2s-doc-milestone/SKILL.md diff --git a/templates/zh-CN/skills/f2s-doc-pdf/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-doc-pdf/SKILL.md similarity index 100% rename from templates/zh-CN/skills/f2s-doc-pdf/SKILL.md rename to packages/core/templates/zh-CN/skills/f2s-doc-pdf/SKILL.md diff --git a/templates/zh-CN/skills/f2s-git-commit/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-git-commit/SKILL.md similarity index 100% rename from templates/zh-CN/skills/f2s-git-commit/SKILL.md rename to packages/core/templates/zh-CN/skills/f2s-git-commit/SKILL.md diff --git a/templates/zh-CN/skills/f2s-kb-add/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-kb-add/SKILL.md similarity index 100% rename from templates/zh-CN/skills/f2s-kb-add/SKILL.md rename to packages/core/templates/zh-CN/skills/f2s-kb-add/SKILL.md diff --git a/templates/zh-CN/skills/f2s-kb-addRules/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-kb-addRules/SKILL.md similarity index 100% rename from templates/zh-CN/skills/f2s-kb-addRules/SKILL.md rename to packages/core/templates/zh-CN/skills/f2s-kb-addRules/SKILL.md diff --git a/templates/zh-CN/skills/f2s-kb-build/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-kb-build/SKILL.md similarity index 100% rename from templates/zh-CN/skills/f2s-kb-build/SKILL.md rename to packages/core/templates/zh-CN/skills/f2s-kb-build/SKILL.md diff --git a/templates/zh-CN/skills/f2s-kb-distill/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-kb-distill/SKILL.md similarity index 100% rename from templates/zh-CN/skills/f2s-kb-distill/SKILL.md rename to packages/core/templates/zh-CN/skills/f2s-kb-distill/SKILL.md diff --git a/templates/zh-CN/skills/f2s-kb-feat/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-kb-feat/SKILL.md similarity index 100% rename from templates/zh-CN/skills/f2s-kb-feat/SKILL.md rename to packages/core/templates/zh-CN/skills/f2s-kb-feat/SKILL.md diff --git a/templates/zh-CN/skills/f2s-kb-fix/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-kb-fix/SKILL.md similarity index 100% rename from templates/zh-CN/skills/f2s-kb-fix/SKILL.md rename to packages/core/templates/zh-CN/skills/f2s-kb-fix/SKILL.md diff --git a/templates/zh-CN/skills/f2s-kb-merge/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-kb-merge/SKILL.md similarity index 100% rename from templates/zh-CN/skills/f2s-kb-merge/SKILL.md rename to packages/core/templates/zh-CN/skills/f2s-kb-merge/SKILL.md diff --git a/templates/zh-CN/skills/f2s-kb-rm/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-kb-rm/SKILL.md similarity index 100% rename from templates/zh-CN/skills/f2s-kb-rm/SKILL.md rename to packages/core/templates/zh-CN/skills/f2s-kb-rm/SKILL.md diff --git a/templates/zh-CN/skills/f2s-kb-sync/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-kb-sync/SKILL.md similarity index 100% rename from templates/zh-CN/skills/f2s-kb-sync/SKILL.md rename to packages/core/templates/zh-CN/skills/f2s-kb-sync/SKILL.md diff --git a/templates/zh-CN/skills/f2s-kb-upgrade/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-kb-upgrade/SKILL.md similarity index 82% rename from templates/zh-CN/skills/f2s-kb-upgrade/SKILL.md rename to packages/core/templates/zh-CN/skills/f2s-kb-upgrade/SKILL.md index 8b94ed1..2ae6011 100644 --- a/templates/zh-CN/skills/f2s-kb-upgrade/SKILL.md +++ b/packages/core/templates/zh-CN/skills/f2s-kb-upgrade/SKILL.md @@ -12,12 +12,12 @@ description: 知识库模板升级技能(仅指本 SKILL):**现行库( ## 边界(避免误区) - **`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`。 +- 本技能跑通的是 **Template Version 下的目录、模板占位、路由结构对齐**;Core 代码版本与 Template Version 独立。用户若说「把新能力写进知识库」,应引导 **`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`)。 +**字段位置**:Core 包模板 `knowledge/manifest-routing.json` 的根级整数字段 `projectRev`(起始 `1`)。 **字段写入语义(必读)**: - **包侧**:维护者按下文规则手动 bump(包模板自身的 `projectRev` 永远是最新值)。 @@ -27,15 +27,15 @@ description: 知识库模板升级技能(仅指本 SKILL):**现行库( - 这使「项目侧 `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` 「主题一览」节或包级章节修改。 +- 包模板 `knowledge/topics/.md` 任一文件的**正文**修改、新增、删除或改名; +- 包模板 `knowledge/matchers/.json` 的 `includeAny` 词条、`id` 或新增 / 删除 matcher 文件; +- 包模板 `knowledge/manifest-routing.json` 的 `topicPaths` / `taskToTopicRules` / `topicDependencies` / `fallbackTopic` / `topicMetadata` 任一段修改; +- 包模板 `knowledge/index.md` 「主题一览」节或包级章节修改。 **不需要 bump 的修改**: - 包源码(`lib/`、`cli.js`、`scripts/`)、`AGENTS.md`、`README*` 文档; -- `templates//flow2spec.config.json` 默认值; -- `templates//rules/*` / `templates//skills/*` 仅规则与技能正文修改(这些与主题层无关,无需触发完整流程)。 +- `flow2spec.config.json` 默认值; +- `rules/*` / `skills/*` 仅规则与技能正文修改(这些与主题层无关,无需触发完整流程,但仍须显式提升 Template Version 以便 Hook 分发新版内容)。 **判定准则一句话**:模板里 `knowledge/` 目录下 topic / matcher / manifest / index 任一**主题层产物**变了 → 必 bump;否则不动。漏 bump 会让用户的 `f2s-kb-upgrade` 跑快速路径,错过包带来的主题变更。 @@ -54,11 +54,12 @@ description: 知识库模板升级技能(仅指本 SKILL):**现行库( ## 非主题版本更新:agent 可直接代跑 `init`(无需进入本技能) -版本检查提示「知识库版本低于最新包版本」时,agent 可**直接替用户执行** `flow2spec init <已初始化的 agents>` 完成更新: +版本检查提示 Core 更新但 `templateUpdateAvailable=false` 时,agent 可**直接替用户更新 Core 并执行** `flow2spec init <已初始化的 agents>`: - `init` 后读项目侧 `.Knowledge/manifest-routing.json`:`projectRev` 与 `pkgRev` **相等**(非主题版本更新)→ 更新完成,删除 `.Knowledge/update-check.json`,**无需进入本技能**; - 两者**不等**(包含主题层变更)→ 进入本技能完整流程(从步骤 2c 起判定,不重复 `init`)。 - 该路径与本技能步骤 2c「快速路径」同一判定口径;用户显式要求「完整流程 / 覆盖重置」时仍按本技能全文执行。 +- `.Knowledge/manifest-routing.json.version` 表示 Template Version;禁止拿它与 Core Version 直接比较。 ## 旧版布局(V1)不再内置迁移 @@ -97,39 +98,24 @@ description: 知识库模板升级技能(仅指本 SKILL):**现行库( ## 强制流程 -### 步骤 -1:全局 flow2spec 版本预检(必须,先于一切,主 agent 前台探测) +### 步骤 -1:CLI/Core/Template 版本预检(必须,先于一切) -**目的**:让「能用全局 `flow2spec` 就用全局」,只在**没装**或**版本过旧**时才动手升级;已装且已是 latest 时**完全跳过**升级动作,同时决定步骤 2 命令的**默认形态**(用 `flow2spec init` 还是 `npx @latest init`)。 - -**动作**:主 agent 在进入步骤 0 **之前**,**顺序、前台**执行以下 3 条探测(都是纯查询,无副作用,秒级返回;无需拆子 agent): +主 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__ +flow2spec version +flow2spec update --check ``` -**判定 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 Version、Core Version、Core Range、Template Version、Protocol Version,以及 npm 最新 Core/Template: -**与 cli.js 的关系**: +| 情况 | 行动 | 步骤 2 默认命令 | +| --- | --- | --- | +| **A. Template 已是最新** | 若只有 Core 更新,执行 `flow2spec update --core` 后幂等 init 刷新 Hook,删除缓存并**停止本技能**;Core 也最新则直接停止 | `flow2spec init ` | +| **B. Template 有更新且最新 Core 落在当前 Core Range** | 执行 `flow2spec update --core`,继续步骤 0;CLI 无需因 Core 兼容更新而发版或升级 | `flow2spec init ` | +| **C. 未安装、版本未知或最新 Core 超出范围** | 使用显式 latest CLI/Core 组合,避免 npx 复用旧 Core;若最新 Core 仍不兼容,先升级支持它的 CLI | `npx --yes --package @latest --package @latest flow2spec init ` | -- `cli.js` 内 `maybeAutoUpdateGlobalInstall()` 是 `init` 收尾兜底逻辑,**与本步不冲突**:本步在前台 init 之前完成探测/派工,cli 那段在 init 收尾时再兜一次;两次都成功就是 no-op,第一次失败第二次还能补救。 +预检失败时允许回退 C,但不得把 Core Version 当作 Template Version。此步骤不强制创建子 agent,也不后台安装全局包。 ### 步骤 0:版本判定与分流(必须,先于 init) @@ -165,10 +151,10 @@ command -v npx >/dev/null 2>&1 && echo __NPX_OK__ || echo __NPX_MISSING__ 在目标项目根目录执行以下命令(**按步骤 -1 的分支结论选默认形态**): -1. **步骤 -1 判定为 A(已装且是 latest)**:直接用全局 CLI(**首选**): +1. **步骤 -1 判定为 A/B(本地 CLI/Core 可用)**:直接使用当前 CLI: - `flow2spec init ` -2. **步骤 -1 判定为 B/C(未装 / 落后 / latest 未知)**:拉 npm latest 跑(**保证本次拿到最新模板**): - - `npx @double-coding/flow2spec@latest init ` +2. **步骤 -1 判定为 C**:显式组合 latest CLI/Core,保证本次拿到最新 Core 与模板: + - `npx --yes --package @latest --package @latest flow2spec init ` 3. 覆盖重置时: - 在上述命令末尾追加 `--reset-knowledge` 4. 用户显式要求切换模板语言时: @@ -177,7 +163,7 @@ command -v npx >/dev/null 2>&1 && echo __NPX_OK__ || echo __NPX_MISSING__ > `` 示例:`cursor claude codex`。 -> **辅助命令(用户可自查)**:`flow2spec --version` 看当前全局版本;`flow2spec update` 触发 CLI 内置的自更新。这两条**不**替代本 SKILL 的完整流程——它们只是「让全局 CLI 保鲜」,主题层对齐仍须走本 SKILL 步骤 2 及以后。 +> **辅助命令(用户可自查)**:`flow2spec version` 查看五维版本;`flow2spec update --check|--cli|--core` 分别检查、更新 CLI、更新兼容 Core。这些命令不替代 Template Version 变化后的本技能完整流程。 **步骤 2 完成后**:立刻执行上文 **「init 与技能自更新」**:重读 **`skills/f2s-kb-upgrade/SKILL.md`**;若有更新则**按新版字面从步骤 2c 起重跑**(**不再次 init**;避免用旧版 SKILL 做后续校验)。 @@ -320,7 +306,7 @@ command -v npx >/dev/null 2>&1 && echo __NPX_OK__ || echo __NPX_MISSING__ ## f2s-kb-upgrade 执行结果 - **步骤 -1 全局版本预检**:`A 已装且是 latest(跳过升级) / B 已装但落后(已派子 agent 后台升级 npm i -g) / C 未装或 latest 未知(已派 / 保守用 npx)`;当前版本=``,latest=`` -- 本技能内代跑命令:`<实际执行的 flow2spec init ... 或 npx @latest init ...>` +- 本技能内代跑命令:`<实际执行的 flow2spec init ... 或显式 latest CLI/Core 组合 init ...>` - init 模式:`增量` / `覆盖重置(--reset-knowledge)` - 执行结果:`成功` / `失败` - **主题层判定**:`projectRev=` vs `pkgRev=` → `快速路径(已跳过 3/3a/3b)` / `完整流程` / `字段缺失走兜底` @@ -351,7 +337,7 @@ command -v npx >/dev/null 2>&1 && echo __NPX_OK__ || echo __NPX_MISSING__ ## 完成后自检 -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`);摘要中已写清分支与版本对比。 +1. 是否已做 **步骤 -1**:执行 `flow2spec version` 与 `flow2spec update --check`,记录 CLI/Core/Core Range/Template/Protocol;Core-only 更新是否直接刷新 Core/Hook 并停止,Template 更新是否按 A/B/C 选择当前 CLI 或显式 latest CLI/Core 组合。 2. 是否已做 **步骤 0**:V1 已停止执行并告知用户处理方式、**现行库(V2+)** 正常进入 `init` 流程。 3. 是否在 **步骤 2 开始前** 记录了项目侧 `projectRev`(`projectRev`),并在 **步骤 2 的 `init` 之后** 重读 `pkgRev`、执行 **步骤 2c** 判定。 4. 是否在 **步骤 2 的 `init` 之后**重读过 **`f2s-kb-upgrade/SKILL.md`**:完整流程下有变化必须**按新版字面从步骤 2c 起重跑**(**不再次 init**);快速路径下可跳过该闭环(见「init 与技能自更新」「快速路径例外」)。 diff --git a/templates/zh-CN/skills/f2s-req-clarify/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-req-clarify/SKILL.md similarity index 100% rename from templates/zh-CN/skills/f2s-req-clarify/SKILL.md rename to packages/core/templates/zh-CN/skills/f2s-req-clarify/SKILL.md diff --git a/templates/zh-CN/skills/f2s-req-plan/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-req-plan/SKILL.md similarity index 100% rename from templates/zh-CN/skills/f2s-req-plan/SKILL.md rename to packages/core/templates/zh-CN/skills/f2s-req-plan/SKILL.md diff --git a/templates/zh-CN/skills/f2s-req-tech/SKILL.md b/packages/core/templates/zh-CN/skills/f2s-req-tech/SKILL.md similarity index 100% rename from templates/zh-CN/skills/f2s-req-tech/SKILL.md rename to packages/core/templates/zh-CN/skills/f2s-req-tech/SKILL.md diff --git a/scripts/git-tag-version.js b/scripts/git-tag-version.js index eb71c90..c074799 100644 --- a/scripts/git-tag-version.js +++ b/scripts/git-tag-version.js @@ -2,29 +2,16 @@ 'use strict'; const { execFileSync } = require('child_process'); -const path = require('path'); +const { checkWorkspaceVersion } = require('./workspace-version'); -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('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'); +const target = process.argv[2]; +if (!['cli', 'core'].includes(target)) { + console.error('usage: git-tag-version.js '); 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); -} - -const tag = version.startsWith('v') ? version : `v${version}`; +const versions = checkWorkspaceVersion({ rootDir: process.cwd() }); +const version = target === 'cli' ? versions.cliVersion : versions.coreVersion; +const tag = `${target}-v${version}`; try { execFileSync('git', ['rev-parse', '-q', '--verify', `refs/tags/${tag}`], { diff --git a/scripts/sync-core-templates.js b/scripts/sync-core-templates.js deleted file mode 100755 index 3cc8c7a..0000000 --- a/scripts/sync-core-templates.js +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env node -/** - * sync-core-templates.js - * ----------------------------------------------------------------------------- - * 把 workspace 根 templates/ 镜像到 packages/core/templates/。 - * - * 背景: - * - workspace 根 templates/ 是唯一手写事实源(日常改的入口)。 - * - packages/core/templates/ 是 @double-coding/flow2spec-core 的 npm 发布物副本: - * * packages/core/index.js 在 `path.join(__dirname, "templates", ...)` 处读取, - * * packages/core/package.json.files 里必须列出 "templates", - * * 用户 `npm install` 后必须能拿到这份副本,否则 `flow2spec init` 会 ENOENT 崩溃。 - * - 为了避免"手工维护两份"漂移,packages/core/templates/ 从 git 忽略, - * 由本脚本在 dev / test / prepack 前自动生成。 - * - * 用法: - * node scripts/sync-core-templates.js # 常规同步 - * node scripts/sync-core-templates.js --check # 只检查是否已同步,不写盘(CI / pack:check) - */ - -"use strict"; - -const fs = require("fs"); -const path = require("path"); - -const repoRoot = path.resolve(__dirname, ".."); -const src = path.join(repoRoot, "templates"); -const dest = path.join(repoRoot, "packages", "core", "templates"); - -const args = process.argv.slice(2); -const CHECK_ONLY = args.includes("--check"); -const QUIET = args.includes("--quiet"); - -function log(msg) { - if (!QUIET) console.log(msg); -} - -function die(msg, code = 1) { - console.error(`[sync-core-templates] ${msg}`); - process.exit(code); -} - -if (!fs.existsSync(src)) { - die(`source not found: ${path.relative(repoRoot, src)}`); -} - -if (!fs.statSync(src).isDirectory()) { - die(`source is not a directory: ${path.relative(repoRoot, src)}`); -} - -/** - * 递归列出目录下的所有相对路径(POSIX),用于精确 diff。 - */ -function listRelativeFiles(root) { - const result = []; - function walk(current, rel) { - const entries = fs.readdirSync(current, { withFileTypes: true }); - for (const entry of entries) { - if (entry.name === ".DS_Store") continue; - const absChild = path.join(current, entry.name); - const relChild = rel ? path.posix.join(rel, entry.name) : entry.name; - if (entry.isDirectory()) { - walk(absChild, relChild); - } else if (entry.isFile()) { - result.push(relChild); - } - } - } - if (fs.existsSync(root)) walk(root, ""); - return result.sort(); -} - -function bytesEqual(a, b) { - const bufA = fs.readFileSync(a); - const bufB = fs.readFileSync(b); - if (bufA.length !== bufB.length) return false; - return bufA.equals(bufB); -} - -function computeDrift() { - const srcFiles = new Set(listRelativeFiles(src)); - const destFiles = new Set(listRelativeFiles(dest)); - const only_in_src = []; - const only_in_dest = []; - const different = []; - for (const f of srcFiles) { - if (!destFiles.has(f)) { - only_in_src.push(f); - } else { - const a = path.join(src, f); - const b = path.join(dest, f); - if (!bytesEqual(a, b)) different.push(f); - } - } - for (const f of destFiles) { - if (!srcFiles.has(f)) only_in_dest.push(f); - } - return { only_in_src, only_in_dest, different }; -} - -if (CHECK_ONLY) { - const drift = computeDrift(); - const total = drift.only_in_src.length + drift.only_in_dest.length + drift.different.length; - if (total === 0) { - log("sync-core-templates: in sync"); - process.exit(0); - } - console.error("[sync-core-templates] drift detected between templates/ and packages/core/templates/:"); - if (drift.only_in_src.length) { - console.error(` only in root templates/ (${drift.only_in_src.length}):`); - drift.only_in_src.slice(0, 20).forEach((f) => console.error(` + ${f}`)); - if (drift.only_in_src.length > 20) console.error(` ...(${drift.only_in_src.length - 20} more)`); - } - if (drift.only_in_dest.length) { - console.error(` only in core templates/ (${drift.only_in_dest.length}):`); - drift.only_in_dest.slice(0, 20).forEach((f) => console.error(` - ${f}`)); - if (drift.only_in_dest.length > 20) console.error(` ...(${drift.only_in_dest.length - 20} more)`); - } - if (drift.different.length) { - console.error(` content differs (${drift.different.length}):`); - drift.different.slice(0, 20).forEach((f) => console.error(` ~ ${f}`)); - if (drift.different.length > 20) console.error(` ...(${drift.different.length - 20} more)`); - } - console.error("run: node scripts/sync-core-templates.js"); - process.exit(1); -} - -// 常规同步:先清空 dest,再全量拷贝(避免留下 src 已删除的文件)。 -if (fs.existsSync(dest)) { - fs.rmSync(dest, { recursive: true, force: true }); -} -fs.mkdirSync(dest, { recursive: true }); - -const srcFiles = listRelativeFiles(src); -let count = 0; -for (const rel of srcFiles) { - const from = path.join(src, rel); - const to = path.join(dest, rel); - fs.mkdirSync(path.dirname(to), { recursive: true }); - fs.copyFileSync(from, to); - count += 1; -} - -log(`sync-core-templates: ${count} files copied from templates/ → packages/core/templates/`); diff --git a/scripts/sync-cursor-plugin.sh b/scripts/sync-cursor-plugin.sh index 5043f95..a525bfd 100755 --- a/scripts/sync-cursor-plugin.sh +++ b/scripts/sync-cursor-plugin.sh @@ -3,7 +3,7 @@ set -euo pipefail # ───────────────────────────────────────────────────────────────────────────── # sync-cursor-plugin.sh -# 将当前分支(main / dev_v3 等)的 templates/ 内容同步到 +# 将当前分支(main / dev_v3 等)的 packages/core/templates/ 内容同步到 # feat/cursor-directory-plugin 分支的插件根结构。 # # 用法: @@ -49,15 +49,15 @@ echo "🔧 创建 worktree → $WORKTREE_DIR" git worktree add "$WORKTREE_DIR" "$PLUGIN_BRANCH" --quiet # ─── 同步映射 ────────────────────────────────────────────────────────────── -# 源 (templates/) → 目标 (插件根) -# templates/rules/ → rules/ -# templates/skills/ → skills/ -# templates/hooks/ → scripts/ -# templates/knowledge/ → knowledge/ -# templates/flow2spec.config.json → flow2spec.config.json +# 源 (packages/core/templates/) → 目标 (插件根) +# packages/core/templates/rules/ → rules/ +# packages/core/templates/skills/ → skills/ +# packages/core/templates/hooks/ → scripts/ +# packages/core/templates/knowledge/ → knowledge/ +# packages/core/templates/flow2spec.config.json → flow2spec.config.json REPO_ROOT=$(git rev-parse --show-toplevel) -SRC="$REPO_ROOT/templates" +SRC="$REPO_ROOT/packages/core/templates" sync_dir() { local src="$1" dest="$2" diff --git a/scripts/test-cli-update.js b/scripts/test-cli-update.js new file mode 100644 index 0000000..b70f9f3 --- /dev/null +++ b/scripts/test-cli-update.js @@ -0,0 +1,59 @@ +"use strict"; + +const assert = require("assert"); +const fs = require("fs"); +const os = require("os"); +const path = require("path"); +const { spawnSync } = require("child_process"); + +const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "flow2spec-cli-update-")); +const cliPath = path.resolve(__dirname, "..", "cli.js"); + +if (process.platform === "win32") { + fs.writeFileSync(path.join(tempDir, "npm.cmd"), [ + "@echo off", + "if \"%2\"==\"@double-coding/flow2spec-core\" (", + " echo {\"version\":\"3.6.0\",\"templateVersion\":\"3.5.0\"}", + ") else (", + " echo 3.5.1", + ")", + "", + ].join("\r\n"), "utf8"); +} else { + const npmPath = path.join(tempDir, "npm"); + fs.writeFileSync(npmPath, [ + "#!/usr/bin/env sh", + "if [ \"$2\" = \"@double-coding/flow2spec-core\" ]; then", + " echo '{\"version\":\"3.6.0\",\"templateVersion\":\"3.5.0\"}'", + "else", + " echo '3.5.1'", + "fi", + "", + ].join("\n"), { encoding: "utf8", mode: 0o755 }); +} + +const env = { + ...process.env, + PATH: `${tempDir}${path.delimiter}${process.env.PATH || ""}`, + FLOW2SPEC_SKIP_UPDATE_CHECK: "1", +}; +const check = spawnSync(process.execPath, [cliPath, "update", "--check"], { + cwd: path.resolve(__dirname, ".."), + env, + encoding: "utf8", +}); +assert.strictEqual(check.status, 0, check.stderr); +assert.match(check.stdout, /CLI:\s+3\.5\.0 -> 3\.5\.1/); +assert.match(check.stdout, /Core:\s+3\.5\.0 -> 3\.6\.0/); +assert.match(check.stdout, /Template:\s+3\.5\.0 -> 3\.5\.0/); +assert.match(check.stdout, /\^3\.5\.0 \(compatible\)/); + +const invalid = spawnSync(process.execPath, [cliPath, "update", "--unknown"], { + cwd: path.resolve(__dirname, ".."), + env, + encoding: "utf8", +}); +assert.strictEqual(invalid.status, 1); +assert.match(invalid.stderr, /update --check\|--cli\|--core/); + +console.log("test-cli-update: ok"); diff --git a/scripts/test-core-api.js b/scripts/test-core-api.js index 2356b6b..eaac8b6 100644 --- a/scripts/test-core-api.js +++ b/scripts/test-core-api.js @@ -15,9 +15,23 @@ async function main() { }); assert.strictEqual(typeof api.project.init, "function"); + assert.deepStrictEqual(Object.keys(api.project.agents()), ["cursor", "claude", "codex", "dsh"]); + assert.deepStrictEqual(api.config.supportedLocales(), ["zh-CN", "en-US"]); assert.strictEqual(typeof api.knowledge.check, "function"); assert.strictEqual(core.getCapabilities().schema, "flow2spec.capabilities.v1"); assert.strictEqual(core.getCapabilities().protocolVersion, 2); + assert.deepStrictEqual(core.getVersions(), { + coreVersion: "3.5.0", + templateVersion: "3.5.0", + protocolVersion: 2, + }); + assert.deepStrictEqual( + core + .getCapabilities() + .capabilities.filter((capability) => capability.since === "3.5.0") + .map((capability) => capability.id), + ["project.agents", "config.supported-locales"], + ); assert.deepStrictEqual( core .getCapabilities() @@ -96,7 +110,22 @@ async function main() { assert.strictEqual(update.status, "upgrade-available"); assert.strictEqual(update.fromCache, true); assert.strictEqual(update.latestVersion, "9.0.0"); + assert.strictEqual(update.templateUpdateAvailable, true); assert.ok(update.notice.includes("f2s-kb-upgrade")); + + fs.writeFileSync( + cachePath, + `${JSON.stringify({ + latestCoreVersion: "3.6.0", + latestTemplateVersion: "1.0.0", + checkedAt: Date.now(), + })}\n`, + "utf8", + ); + const coreOnlyUpdate = await api.update.check(); + assert.strictEqual(coreOnlyUpdate.coreUpdateAvailable, true); + assert.strictEqual(coreOnlyUpdate.templateUpdateAvailable, false); + assert.ok(coreOnlyUpdate.notice.includes("无需执行 f2s-kb-upgrade")); } finally { if (originalCI === undefined) delete process.env.CI; else process.env.CI = originalCI; diff --git a/scripts/test-developer-id.js b/scripts/test-developer-id.js index 6e54685..cbfd694 100644 --- a/scripts/test-developer-id.js +++ b/scripts/test-developer-id.js @@ -6,7 +6,7 @@ const { resolveDeveloperContext, taskRootFor, HASH_FALLBACK_PREFIX, -} = require("../lib/developerId"); +} = require("../packages/core/lib/developerId"); // ---------- sanitizeDeveloperId ---------- assert.strictEqual(sanitizeDeveloperId("Alice"), "alice"); diff --git a/scripts/test-doctor.js b/scripts/test-doctor.js index b541a58..994b8b1 100644 --- a/scripts/test-doctor.js +++ b/scripts/test-doctor.js @@ -8,7 +8,7 @@ const { runDoctor, formatDoctorReport, satisfiesNodeEngine, -} = require("../lib/doctor"); +} = require("../packages/core/lib/doctor"); function write(file, content = "") { fs.mkdirSync(path.dirname(file), { recursive: true }); diff --git a/scripts/test-dsh-init.js b/scripts/test-dsh-init.js index 6b7095f..dc7490b 100644 --- a/scripts/test-dsh-init.js +++ b/scripts/test-dsh-init.js @@ -3,7 +3,7 @@ const fs = require("fs"); const os = require("os"); const path = require("path"); -const runInit = require("../lib/init"); +const runInit = require("../packages/core/lib/init"); (async () => { for (const locale of ["zh-CN", "en-US"]) { diff --git a/scripts/test-init-gitignore.js b/scripts/test-init-gitignore.js index 2985f00..3278d40 100644 --- a/scripts/test-init-gitignore.js +++ b/scripts/test-init-gitignore.js @@ -3,7 +3,7 @@ const fs = require("fs"); const os = require("os"); const path = require("path"); -const runInit = require("../lib/init"); +const runInit = require("../packages/core/lib/init"); (async () => { const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "flow2spec-init-")); @@ -11,7 +11,7 @@ const runInit = require("../lib/init"); const gitignorePath = path.join(tmpRoot, ".gitignore"); fs.writeFileSync(gitignorePath, "node_modules/\n", "utf8"); - const first = await runInit(tmpRoot, ["cursor"], { + const first = await runInit(tmpRoot, ["cursor", "claude", "codex"], { configValues: { locale: "zh-CN", }, @@ -27,6 +27,17 @@ const runInit = require("../lib/init"); assert(afterFirst.includes("# Flow2Spec local state")); assert(afterFirst.includes(".task/")); assert(afterFirst.includes(".Knowledge/update-check.json")); + for (const agentRoot of [".cursor", ".claude", ".codex"]) { + const hook = fs.readFileSync( + path.join(tmpRoot, agentRoot, "hooks", "f2s-update-check.js"), + "utf8", + ); + assert(hook.includes("const GENERATED_CORE_VERSION = '3.5.0';")); + assert(hook.includes("const GENERATED_TEMPLATE_VERSION = '3.5.0';")); + assert(!hook.includes("__FLOW2SPEC_PACKAGE_NAME__")); + assert(!hook.includes("__FLOW2SPEC_CORE_VERSION__")); + assert(!hook.includes("__FLOW2SPEC_TEMPLATE_VERSION__")); + } const second = await runInit(tmpRoot, ["cursor"], { configValues: { diff --git a/scripts/test-knowledge-engine.js b/scripts/test-knowledge-engine.js index 777b813..f0b04bc 100644 --- a/scripts/test-knowledge-engine.js +++ b/scripts/test-knowledge-engine.js @@ -3,7 +3,7 @@ const fs = require("fs"); const os = require("os"); const path = require("path"); -const engine = require("../lib/knowledgeEngine"); +const engine = require("../packages/core/lib/knowledgeEngine"); function writeJson(filePath, data) { fs.mkdirSync(path.dirname(filePath), { recursive: true }); diff --git a/scripts/test-package-install.js b/scripts/test-package-install.js index 1182536..b65a4fb 100644 --- a/scripts/test-package-install.js +++ b/scripts/test-package-install.js @@ -46,8 +46,9 @@ run("npm", [ const coreProbe = path.join(tempDir, "core-probe.js"); fs.writeFileSync( coreProbe, - "const core = require('@double-coding/flow2spec-core');\n" + + "const core = require('@double-coding/flow2spec-core');\n" + "if (core.getCapabilities().protocolVersion !== 2) process.exit(1);\n" + + "if (core.getVersions().templateVersion !== '3.5.0') process.exit(1);\n" + "if (typeof core.createFlow2Spec({ cwd: process.cwd() }).resources.skillCatalog !== 'function') process.exit(1);\n", "utf8", ); diff --git a/scripts/test-template-knowledge.js b/scripts/test-template-knowledge.js index 3668296..f166e7c 100644 --- a/scripts/test-template-knowledge.js +++ b/scripts/test-template-knowledge.js @@ -3,7 +3,7 @@ const fs = require("fs"); const os = require("os"); const path = require("path"); -const engine = require("../lib/knowledgeEngine"); +const engine = require("../packages/core/lib/knowledgeEngine"); const repoRoot = path.resolve(__dirname, ".."); const locales = ["zh-CN", "en-US"]; @@ -11,7 +11,7 @@ const locales = ["zh-CN", "en-US"]; for (const locale of locales) { const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), `flow2spec-template-${locale}-`)); try { - const source = path.join(repoRoot, "templates", locale, "knowledge"); + const source = path.join(repoRoot, "packages", "core", "templates", locale, "knowledge"); const target = path.join(tmpRoot, ".Knowledge"); fs.cpSync(source, target, { recursive: true }); diff --git a/scripts/test-workspace-version.js b/scripts/test-workspace-version.js new file mode 100644 index 0000000..1eaaeec --- /dev/null +++ b/scripts/test-workspace-version.js @@ -0,0 +1,72 @@ +"use strict"; + +const assert = require("assert"); +const fs = require("fs"); +const os = require("os"); +const path = require("path"); +const { + checkWorkspaceVersion, + normalizeCoreRange, + normalizeVersion, + satisfiesCaret, + setCliVersion, + setCoreVersion, + setTemplateVersion, +} = require("./workspace-version"); + +const repoRoot = path.resolve(__dirname, ".."); +const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "flow2spec-version-")); + +for (const relativePath of [ + "package.json", + "package-lock.json", + "packages/core/package.json", + "packages/core/capabilities.json", + "packages/core/templates/zh-CN/knowledge/manifest-routing.json", + "packages/core/templates/en-US/knowledge/manifest-routing.json", + "packages/cli/package.json", +]) { + const source = path.join(repoRoot, relativePath); + const target = path.join(tempRoot, relativePath); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.copyFileSync(source, target); +} + +assert.strictEqual(normalizeVersion("v4.1.0-beta.2"), "4.1.0-beta.2"); +assert.throws(() => normalizeVersion("4.01.0"), /invalid semantic version/); +assert.strictEqual(normalizeCoreRange("^3.5.0"), "^3.5.0"); +assert.throws(() => normalizeCoreRange(">3.5.0"), /caret semantic range/); +assert.strictEqual(satisfiesCaret("3.6.0", "^3.5.0"), true); +assert.strictEqual(satisfiesCaret("4.0.0", "^3.5.0"), false); + +setCoreVersion("3.6.0", { rootDir: tempRoot }); +setCliVersion("3.5.1", { rootDir: tempRoot, coreRange: "^3.6.0" }); +setTemplateVersion("3.5.2", { rootDir: tempRoot }); +assert.deepStrictEqual(checkWorkspaceVersion({ rootDir: tempRoot, tag: "core-v3.6.0" }), { + cliVersion: "3.5.1", + coreVersion: "3.6.0", + templateVersion: "3.5.2", + coreRange: "^3.6.0", + protocolVersion: 2, +}); +assert.deepStrictEqual(checkWorkspaceVersion({ rootDir: tempRoot, tag: "cli-v3.5.1" }), { + cliVersion: "3.5.1", + coreVersion: "3.6.0", + templateVersion: "3.5.2", + coreRange: "^3.6.0", + protocolVersion: 2, +}); +assert.throws(() => checkWorkspaceVersion({ rootDir: tempRoot, tag: "v3.6.0" }), /release tag must match/); +assert.throws(() => setCoreVersion("4.0.0", { rootDir: tempRoot }), /outside the CLI dependency range/); + +const corePackage = require(path.join(tempRoot, "packages/core/package.json")); +const cliPackage = require(path.join(tempRoot, "packages/cli/package.json")); +const lockfile = require(path.join(tempRoot, "package-lock.json")); +assert.strictEqual(corePackage.version, "3.6.0"); +assert.strictEqual(corePackage.templateVersion, "3.5.2"); +assert.strictEqual(cliPackage.version, "3.5.1"); +assert.strictEqual(cliPackage.dependencies["@double-coding/flow2spec-core"], "^3.6.0"); +assert.strictEqual(lockfile.packages["packages/core"].version, "3.6.0"); +assert.strictEqual(lockfile.packages["packages/cli"].version, "3.5.1"); + +console.log("test-workspace-version: ok"); diff --git a/scripts/workspace-version.js b/scripts/workspace-version.js new file mode 100644 index 0000000..9817588 --- /dev/null +++ b/scripts/workspace-version.js @@ -0,0 +1,290 @@ +#!/usr/bin/env node +"use strict"; + +const fs = require("fs"); +const path = require("path"); + +const CORE_PACKAGE = "@double-coding/flow2spec-core"; +const SEMVER_SOURCE = "(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?"; +const SEMVER_PATTERN = new RegExp(`^${SEMVER_SOURCE}$`); +const CORE_RANGE_PATTERN = new RegExp(`^\\^(${SEMVER_SOURCE})$`); + +function readJson(filePath) { + return JSON.parse(fs.readFileSync(filePath, "utf8")); +} + +function writeJson(filePath, value) { + fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8"); +} + +function workspacePaths(rootDir) { + return { + root: path.join(rootDir, "package.json"), + core: path.join(rootDir, "packages", "core", "package.json"), + cli: path.join(rootDir, "packages", "cli", "package.json"), + lock: path.join(rootDir, "package-lock.json"), + capabilities: path.join(rootDir, "packages", "core", "capabilities.json"), + templateManifests: [ + path.join(rootDir, "packages", "core", "templates", "zh-CN", "knowledge", "manifest-routing.json"), + path.join(rootDir, "packages", "core", "templates", "en-US", "knowledge", "manifest-routing.json"), + ], + }; +} + +function loadWorkspace(rootDir) { + const paths = workspacePaths(rootDir); + return { + paths, + root: readJson(paths.root), + core: readJson(paths.core), + cli: readJson(paths.cli), + lock: readJson(paths.lock), + capabilities: readJson(paths.capabilities), + templateManifests: paths.templateManifests.map(readJson), + }; +} + +function normalizeVersion(input) { + const raw = String(input || "").trim(); + const version = raw.startsWith("v") || raw.startsWith("V") ? raw.slice(1) : raw; + if (!SEMVER_PATTERN.test(version)) { + throw new Error(`invalid semantic version: ${raw || ""}`); + } + return version; +} + +function parseVersion(input) { + const version = normalizeVersion(input); + const [versionAndPrerelease] = version.split("+", 1); + const separator = versionAndPrerelease.indexOf("-"); + const core = separator === -1 ? versionAndPrerelease : versionAndPrerelease.slice(0, separator); + return { + version, + numbers: core.split(".").map(Number), + prerelease: separator === -1 ? "" : versionAndPrerelease.slice(separator + 1), + }; +} + +function compareVersions(left, right) { + const a = parseVersion(left); + const b = parseVersion(right); + for (let index = 0; index < 3; index += 1) { + const difference = a.numbers[index] - b.numbers[index]; + if (difference !== 0) return difference; + } + if (a.prerelease === b.prerelease) return 0; + if (!a.prerelease) return 1; + if (!b.prerelease) return -1; + return a.prerelease.localeCompare(b.prerelease, "en", { numeric: true }); +} + +function normalizeCoreRange(input) { + const raw = String(input || "").trim(); + const match = CORE_RANGE_PATTERN.exec(raw); + if (!match) throw new Error(`Core dependency must use a caret semantic range, received: ${raw || ""}`); + return `^${normalizeVersion(match[1])}`; +} + +function satisfiesCaret(versionInput, rangeInput) { + const version = parseVersion(versionInput); + const minimum = parseVersion(normalizeCoreRange(rangeInput).slice(1)); + if (compareVersions(version.version, minimum.version) < 0) return false; + const [major, minor, patch] = minimum.numbers; + let upper; + if (major > 0) upper = `${major + 1}.0.0`; + else if (minor > 0) upper = `0.${minor + 1}.0`; + else upper = `0.0.${patch + 1}`; + return compareVersions(version.version, upper) < 0; +} + +function collectVersionErrors(workspace, tag) { + const { root, core, cli, lock, capabilities, templateManifests } = workspace; + const cliVersion = String(cli.version || "").trim(); + const coreVersion = String(core.version || "").trim(); + const templateVersion = String(core.templateVersion || "").trim(); + const coreRange = String(cli.dependencies?.[CORE_PACKAGE] || "").trim(); + const protocolVersion = capabilities.protocolVersion; + const errors = []; + const expect = (actual, wanted, label) => { + if (actual !== wanted) errors.push(`${label}: expected ${wanted}, received ${actual || ""}`); + }; + + for (const [label, value] of [ + ["CLI version", cliVersion], + ["Core version", coreVersion], + ["Template version", templateVersion], + ["workspace version", String(root.version || "").trim()], + ]) { + try { + normalizeVersion(value); + } catch (error) { + errors.push(`${label}: ${error.message}`); + } + } + + try { + normalizeCoreRange(coreRange); + if (!satisfiesCaret(coreVersion, coreRange)) { + errors.push(`Core ${coreVersion} is outside the CLI dependency range ${coreRange}`); + } + } catch (error) { + errors.push(`packages/cli/package.json dependency ${CORE_PACKAGE}: ${error.message}`); + } + + if (!Number.isInteger(protocolVersion) || protocolVersion < 1) { + errors.push(`capabilities.json protocolVersion must be a positive integer, received ${protocolVersion}`); + } + + expect(String(lock.version || "").trim(), String(root.version || "").trim(), "package-lock.json version"); + expect(String(lock.packages?.[""]?.version || "").trim(), String(root.version || "").trim(), "package-lock.json root version"); + expect(String(lock.packages?.["packages/core"]?.version || "").trim(), coreVersion, "package-lock.json Core version"); + expect(String(lock.packages?.["packages/cli"]?.version || "").trim(), cliVersion, "package-lock.json CLI version"); + expect(String(lock.packages?.["packages/cli"]?.dependencies?.[CORE_PACKAGE] || "").trim(), coreRange, `package-lock.json CLI dependency ${CORE_PACKAGE}`); + if (lock.packages?.[""]?.dependencies?.[CORE_PACKAGE]) errors.push(`package-lock.json root must not depend on ${CORE_PACKAGE}`); + if (root.dependencies?.[CORE_PACKAGE]) errors.push(`package.json root must not depend on ${CORE_PACKAGE}`); + + templateManifests.forEach((manifest, index) => { + expect(String(manifest.version || "").trim(), templateVersion, `${workspace.paths.templateManifests[index]} version`); + }); + + if (tag) { + const match = /^(cli|core)-v(.+)$/.exec(String(tag).trim()); + if (!match) { + errors.push(`release tag must match cli-v or core-v, received ${tag}`); + } else { + let tagVersion = ""; + try { + tagVersion = normalizeVersion(match[2]); + } catch (error) { + errors.push(`release tag: ${error.message}`); + } + if (tagVersion) expect(tagVersion, match[1] === "cli" ? cliVersion : coreVersion, `${match[1]} release tag version`); + } + } + + return { errors, cliVersion, coreVersion, templateVersion, coreRange, protocolVersion }; +} + +function checkWorkspaceVersion(options = {}) { + const rootDir = path.resolve(options.rootDir || path.join(__dirname, "..")); + const result = collectVersionErrors(loadWorkspace(rootDir), options.tag); + if (result.errors.length > 0) { + throw new Error(`workspace version check failed:\n- ${result.errors.join("\n- ")}`); + } + const { errors, ...versions } = result; + return versions; +} + +function setCliVersion(input, options = {}) { + const rootDir = path.resolve(options.rootDir || path.join(__dirname, "..")); + const version = normalizeVersion(input); + const workspace = loadWorkspace(rootDir); + const coreRange = options.coreRange + ? normalizeCoreRange(options.coreRange) + : normalizeCoreRange(workspace.cli.dependencies?.[CORE_PACKAGE]); + if (!satisfiesCaret(workspace.core.version, coreRange)) { + throw new Error(`Core ${workspace.core.version} is outside the requested CLI dependency range ${coreRange}`); + } + workspace.cli.version = version; + workspace.cli.dependencies[CORE_PACKAGE] = coreRange; + workspace.lock.packages["packages/cli"].version = version; + workspace.lock.packages["packages/cli"].dependencies[CORE_PACKAGE] = coreRange; + writeJson(workspace.paths.cli, workspace.cli); + writeJson(workspace.paths.lock, workspace.lock); + checkWorkspaceVersion({ rootDir }); + return { cliVersion: version, coreRange }; +} + +function setCoreVersion(input, options = {}) { + const rootDir = path.resolve(options.rootDir || path.join(__dirname, "..")); + const version = normalizeVersion(input); + const workspace = loadWorkspace(rootDir); + const coreRange = normalizeCoreRange(workspace.cli.dependencies?.[CORE_PACKAGE]); + if (!satisfiesCaret(version, coreRange)) { + throw new Error(`Core ${version} is outside the CLI dependency range ${coreRange}; update the CLI range first`); + } + workspace.core.version = version; + workspace.lock.packages["packages/core"].version = version; + writeJson(workspace.paths.core, workspace.core); + writeJson(workspace.paths.lock, workspace.lock); + checkWorkspaceVersion({ rootDir }); + return { coreVersion: version }; +} + +function setTemplateVersion(input, options = {}) { + const rootDir = path.resolve(options.rootDir || path.join(__dirname, "..")); + const version = normalizeVersion(input); + const workspace = loadWorkspace(rootDir); + workspace.core.templateVersion = version; + workspace.templateManifests.forEach((manifest) => { + manifest.version = version; + }); + writeJson(workspace.paths.core, workspace.core); + workspace.paths.templateManifests.forEach((manifestPath, index) => { + writeJson(manifestPath, workspace.templateManifests[index]); + }); + checkWorkspaceVersion({ rootDir }); + return { templateVersion: version }; +} + +function readOption(args, name) { + const index = args.indexOf(name); + if (index === -1) return undefined; + if (!args[index + 1]) throw new Error(`${name} requires a value`); + return args[index + 1]; +} + +function versionArgument(args, usage) { + const value = args.find((arg, index) => arg !== "--" && (index === 0 || args[index - 1] !== "--core-range")); + if (!value) throw new Error(usage); + return value; +} + +function main(args = process.argv.slice(2)) { + const command = args[0]; + const rest = args.slice(1); + if (command === "check") { + const result = checkWorkspaceVersion({ tag: readOption(rest, "--tag") }); + console.log(`workspace versions are consistent: CLI ${result.cliVersion}, Core ${result.coreVersion}, Template ${result.templateVersion}, Protocol ${result.protocolVersion}`); + return; + } + if (command === "set-cli") { + const result = setCliVersion(versionArgument(rest, "usage: npm run version:set:cli -- [--core-range ^x.y.z]"), { + coreRange: readOption(rest, "--core-range"), + }); + console.log(`CLI version updated: ${result.cliVersion} (Core ${result.coreRange})`); + return; + } + if (command === "set-core") { + const result = setCoreVersion(versionArgument(rest, "usage: npm run version:set:core -- ")); + console.log(`Core version updated: ${result.coreVersion}`); + return; + } + if (command === "set-template") { + const result = setTemplateVersion(versionArgument(rest, "usage: npm run version:set:template -- ")); + console.log(`Template version updated: ${result.templateVersion}`); + return; + } + throw new Error("usage: workspace-version.js [version]"); +} + +if (require.main === module) { + try { + main(); + } catch (error) { + console.error(error.message); + process.exitCode = 1; + } +} + +module.exports = { + checkWorkspaceVersion, + collectVersionErrors, + compareVersions, + normalizeCoreRange, + normalizeVersion, + satisfiesCaret, + setCliVersion, + setCoreVersion, + setTemplateVersion, +}; diff --git a/templates/en-US/hooks/f2s-update-check.js b/templates/en-US/hooks/f2s-update-check.js deleted file mode 100644 index 9b6960e..0000000 --- a/templates/en-US/hooks/f2s-update-check.js +++ /dev/null @@ -1,189 +0,0 @@ -#!/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 a notice into Agent context (the agent may run flow2spec init directly; use f2s-kb-upgrade only for topic-layer changes) - * 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}. I can run flow2spec init on your behalf to complete the update; if this update includes topic-layer changes, run the full f2s-kb-upgrade flow afterwards.`; - 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, - ``, - `[flow2spec/agent-instruction] After relaying, you may directly run \`flow2spec init \` on the user's behalf to complete the update; the user does not need to run f2s-kb-upgrade manually. After init succeeds, read .Knowledge/manifest-routing.json: if projectRev equals pkgRev (a non-topic version update), the update is complete — delete .Knowledge/update-check.json; if they differ (the update includes topic-layer changes), continue with the full f2s-kb-upgrade skill flow (you may start from its step 2c without re-running init).`, - ].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/templates/zh-CN/hooks/f2s-update-check.js b/templates/zh-CN/hooks/f2s-update-check.js deleted file mode 100644 index 2f6f4ed..0000000 --- a/templates/zh-CN/hooks/f2s-update-check.js +++ /dev/null @@ -1,189 +0,0 @@ -#!/usr/bin/env node -'use strict'; -/** - * flow2spec SessionStart hook — 每天第一次对话时检查版本更新。 - * 比较本地知识库 manifest-routing.json 的 version 与 npm 最新版本: - * - 一致或本地更新 → 静默退出 - * - 落后 → 向 Agent 上下文注入提示(agent 可直接代跑 flow2spec init;主题层变更再走 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}。我可以直接代跑 flow2spec init 完成更新;若本次更新包含主题层变更,再走 f2s-kb-upgrade 完整流程。`; - return [ - `[flow2spec/agent-instruction] 你必须在本轮回复开头将下列升级提示原文转告用户,不得省略、不得仅在内部判断后跳过:`, - ``, - userMsg, - ``, - `[flow2spec/agent-instruction] 转告后可直接替用户执行 \`flow2spec init <已初始化的 agents,如 claude cursor codex>\` 完成更新,无需用户手动执行 f2s-kb-upgrade。init 成功后读取 .Knowledge/manifest-routing.json:若 projectRev 与 pkgRev 相等(非主题版本更新),更新已完成,删除 .Knowledge/update-check.json;若两者不等(包含主题层变更),继续执行 f2s-kb-upgrade skill 完整流程(可从其步骤 2c 起,不必重复 init)。`, - ].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();