@jspg-ai/coding-bb 0.0.5-beta.3 → 0.0.5-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -34,15 +34,8 @@ trigger: always_on
34
34
 
35
35
  **只改必须改的,只清理自己造成的烂摊子。**
36
36
 
37
- 编辑已有代码时:
38
- - 不顺手"改进"相邻代码、注释或格式
39
- - 不重构没坏的东西
40
- - 匹配现有风格,即使你更习惯另一种写法
41
- - 发现无关的死代码,口头提及即可,不要删除
42
-
43
- 当你的改动产生孤儿代码时:
44
- - 删除因你的改动而不再使用的 import / 变量 / 函数
45
- - 不删除已有的死代码,除非用户要求
37
+ - 编辑已有代码:不顺手"改进"相邻代码/注释/格式,不重构没坏的东西,匹配现有风格(即使你更习惯另一种写法);发现无关死代码只口头提及,不删除
38
+ - 改动产生孤儿代码:删除因你改动而不再使用的 import / 变量 / 函数;不碰原有死代码,除非用户要求
46
39
 
47
40
  **检验标准:** 每一行改动都应该能追溯到用户的需求。
48
41
 
@@ -50,20 +43,7 @@ trigger: always_on
50
43
 
51
44
  **定义成功标准,循环直到验证通过。**
52
45
 
53
- 将命令式任务转换为可验证的目标:
54
-
55
- | 而非… | 转换为… |
56
- |--------|---------|
57
- | "加个校验" | "先写无效输入的测试,再让测试通过" |
58
- | "修这个 bug" | "先写能复现的测试,再修到测试通过" |
59
- | "重构 X" | "确保重构前后测试全部通过" |
60
-
61
- 多步任务先给简短计划:
62
- ```
63
- 1. [步骤] → 验证: [检查项]
64
- 2. [步骤] → 验证: [检查项]
65
- 3. [步骤] → 验证: [检查项]
66
- ```
46
+ 将命令式任务转换为可验证的目标——"加个校验"→"先写无效输入的测试再让通过";"修这个 bug"→"先写复现测试再修到通过";"重构 X"→"确保重构前后测试全部通过"。多步任务先给简短计划(步骤 → 验证项)。
67
47
 
68
48
  清晰的成功标准让 AI 可以独立循环验证,模糊的标准("搞一下")需要反复澄清。
69
49
 
@@ -92,13 +72,7 @@ trigger: always_on
92
72
 
93
73
  **任何实现动作前,先陈述方案并获得用户批准。**
94
74
 
95
- - 动手前说出打算怎么做(方案、涉及文件、验证方式),等用户确认
96
- - 仪式随任务缩放:小改动两三句话说清即可;大改动走 openspec 工作流(propose → specs → design)
97
- - "太简单不需要方案"是最危险的念头——简单意味着方案短,而不是没有方案
98
- - 中途发现复杂度超出预期:停下来重新说明,不要闷头扩大改动范围
99
-
100
- **检验标准:** 用户从未因"AI 直接改了代码没打招呼"而感到意外。
101
-
102
- ---
75
+ - 动手前说出打算怎么做(方案、涉及文件、验证方式),等用户确认;仪式随任务缩放:小改动两三句话说清,大改动走 openspec 工作流(propose → specs → design)
76
+ - "太简单不需要方案"是最危险的念头——简单意味着方案短,而不是没有方案;中途发现复杂度超出预期:停下来重新说明,不要闷头扩大改动范围
103
77
 
104
- **这些准则生效的标志:** diff 中不必要的改动减少、不会因过度设计而重写、澄清问题在实现前而非出错后提出。
78
+ **检验标准:** 用户从未因"AI 直接改了代码没打招呼"而感到意外。
@@ -8,7 +8,7 @@ trigger: always_on
8
8
  - 方法禁止超过 **100 行**,超过必须拆分为语义明确的子方法
9
9
  - 主方法只做步骤编排,涉及业务规则判断、状态变更或外部调用的循环/分支逻辑必须抽取为独立方法;仅做参数校验、数据组装、简单赋值等非业务逻辑可保留
10
10
  - 命名贴合业务含义,同一工程避免同名类,同一概念命名一致,禁止望文不知义的缩写(反例:`aeh` 表示"运费估算处理")
11
- - 类注释必须包含:说明、作者、版本、引入版本、相关类
11
+ - 类注释必须包含:说明、作者、相关类
12
12
  - 公共方法(public、REST、RPC)注释必须包含:入参要求和取值范围、出参结果、异常场景;私有/内部方法只要求写核心功能说明
13
13
  - 数据模型必须使用对应后缀:`Request`(入参)、`Response`(出参)、`DTO`(服务间传输)、`PO`(持久化对象)、`Param`(查询/操作参数)
14
14
 
@@ -28,7 +28,6 @@ trigger: always_on
28
28
  - 密码、密钥等敏感信息必须存储在 Lion 配置中心,禁止硬编码
29
29
  - 日志中禁止输出未脱敏的敏感信息
30
30
  - 后台敏感操作(删除/修改)必须记录审计日志:操作人、时间、操作内容,有请求上下文时记录来源 IP
31
- - 三方资源限制使用频率
32
31
 
33
32
  ## 五. 数据库规范
34
33
 
@@ -31,20 +31,13 @@ trigger: always_on
31
31
  2. **识别架构模式** — 确认项目使用的分包模式、命名约定、注解风格
32
32
  3. **保持一致性** — 新代码遵循项目风格,而非套用全局规范
33
33
 
34
- > 目录内风格不一致时,以目录内多数文件的风格为准;仍无法判断时,以项目显式规范为准。
35
-
36
34
  ### 冲突处理
37
35
 
38
36
  | 场景 | 处理方式 |
39
37
  |------|----------|
40
- | 项目已有分层架构 | 沿用现有分层,不强制统一为某种架构 |
41
- | 项目已有命名风格 | 保持统一,不自创新风格 |
42
- | 项目已用某中间件 | 继续使用,不强行切换 |
43
- | 项目内存在多种风格混用 | 以当前文件风格为准,不引入第三种风格;当前文件无明确风格时以目录内多数文件风格为准 |
44
- | 项目无先例可循(如首次使用 Redis、首次写 MQ 消费者) | 使用本规范库作为默认起点 |
45
- | 全局规范更新 | 以项目实际安装的版本为准,不自动覆盖 |
46
- | 全新项目 | 使用本规范库作为起点 |
47
- | 已有项目中的新模块 | 沿用项目现有约定,不另起风格 |
38
+ | 项目已有约定(分层架构 / 命名风格 / 中间件选型 / 新模块) | 一律沿用,不强制统一、不自创风格、不强行切换 |
39
+ | 项目内存在多种风格混用 | 以当前文件风格为准,不引入第三种风格;当前文件无明确风格时取目录内多数文件风格,仍有歧义以项目显式规范为准 |
40
+ | 项目无先例可循 / 全新项目(如首次使用 Redis、首次写 MQ 消费者) | 使用本规范库作为默认起点 |
48
41
 
49
42
  > **显式规范与代码风格冲突时的处理**:显式规范优先用于新代码;不要求对已有代码做大规模 reformat。判断标准:涉及命名约定、架构分层、中间件选型等**结构性决策**时以显式规范为准;仅涉及缩进、换行等**格式细节**时以当前文件风格为准。
50
43
 
@@ -4,35 +4,28 @@ trigger: always_on
4
4
 
5
5
  # 业务空间开发纪律(双模式,用户选择制)
6
6
 
7
- **仅当处于业务空间体系(空间根、`.worktrees/` 工作树、及其中各应用检出)时生效。空间支持两种开发模式,能力对称(都能修改 workspace 内容与应用代码、都强制分支隔离、交付均有 MR 与直合并两条通道,ADR 0007/0008),由用户显式选择,agent 只给推荐不代决。**
7
+ **仅当处于业务空间体系(空间根、`.worktrees/` 工作树、及其中各应用检出)时生效。空间支持本地模式 / worktree 模式两种开发模式,能力对称(ADR 0007/0008),由用户显式选择,agent 只给推荐不代决。执行细则以技能为唯一出处:本地模式 `cbb-local-mode`;worktree 全流程 `cbb-worktree-init` / `cbb-worktree-push` / `cbb-worktree-close`。**
8
8
 
9
- **模式一 · 本地模式(单线程快改,当前会话就地干):**
9
+ **模式一 · 本地模式(单线程快改,当前会话就地干):** 根 + 各涉及应用 `.codespace/<app>/` 就地切同名分支(基线 = `origin/<主干>`),当前会话原地开发,相对路径原样生效;**一次只承载一个需求**(独占本地检出;并行新需求走 worktree 模式,互不干扰)。收尾受下「收尾合并门禁」约束。
10
10
 
11
- - 就地切分支:根 + 各涉及应用 `.codespace/<app>/` 同名 `checkout -b`(基线 = `origin/<main>`,经 `cbb-local-mode` 技能执行)
12
- - 当前会话原地开发,相对路径原样生效;**一次只承载一个需求**(独占本地检出;期间并行新需求走 worktree 模式,互不干扰)
13
- - 收尾(收尾合并门禁,ADR 0007):`merge-branches.js --check` 判定已合并;未合并须三选一(提 MR / 直合并(临时 worktree 零接触,ADR 0008) / 确认废弃)后方可收尾;交付完成后各参与仓库回主干并删分支(`return-to-trunk.js` + `delete-branches.js`,主干名经远端 HEAD 探测,兼容 main / master)
14
-
15
- **模式二 · worktree 模式(并行 / 隔离):**
16
-
17
- - `cbb-worktree-init` 建隔离 worktree(AI 配置内容子目录已 junction 就位),**开发会话开进 `.worktrees/worktree-<需求名>/`**,可多需求并行;app 检出收拢在 `<wt>/.codespace/<app>/`——**镜像空间根布局**(`.codespace/<app>/...` 相对路径两模式通用,ADR 0003)
18
- - 根会话保留作控制面(worktree 生灭、`cbb update` 等空间级操作);**会话形态按工具定,能自动则 agent 亲自做**:OpenCode 桌面端 = agent 自动把会话切进/切出 worktree(`session_move`,上下文保留、不列入用户待办,空间根路径在案供回切);无会话搬移能力的桌面端 = 用户手动切根;CLI/TUI = 新开会话进 worktree
11
+ **模式二 · worktree 模式(并行 / 隔离):** `cbb-worktree-init` 建隔离 worktree,**开发会话开进 `.worktrees/worktree-<需求名>/`**,可多需求并行;app 检出收拢在 `<wt>/.codespace/<app>/`——**镜像空间根布局**(`.codespace/<app>/...` 相对路径两模式通用,ADR 0003)。根会话保留作控制面(worktree 生灭、`cbb update` 等空间级操作);会话切进/切出的工具差异与细则见 init / close 技能。
19
12
 
20
13
  **基准与防线(两模式共同,ADR 0002):**
21
14
 
22
- - **基准 = `origin/main`(唯一真相源);`.codespace/<app>/` 的检出 = 视图**——本地模式可暂态被需求分支占用,收尾恢复
23
- - **严禁直接在任何 main 检出上提交载荷**:分支感知 pre-commit hook 机械拦截(workspace 仓库仅放行控制面白名单:`CBB-WORKSPACE.json` / `.gitignore` / `AGENTS.md` / `openspec/config.yaml` / `openspec/schemas/**`;应用仓库 main 全拦;**merge 提交(`MERGE_HEAD` 存在)放行**,见 ADR 0007)——**被拦是防线生效,不是故障**;`--no-verify` 绕过仅限用户显式要求
24
- - 需求分支推送唯一出口 = `cbb-worktree-push`(init 创建流程零远端操作,lazy push);推送后可选提 MR 或**直接合并**(`merge-branches.js`,主干 / 测试分支,ADR 0008);收尾合并门禁同源(ADR 0007)
25
- - **收尾合并门禁(ADR 0007)**:两模式收尾均须每仓库"已合并"(`safety-check` / `merge-branches.js --check` 判定);未合并三选一——提 MR / 直合并 / 确认废弃(远程重命名 `<分支>-deprecated`);交付完成自动删本地+远程分支
15
+ - **基准 = `origin/<主干>`(唯一真相源);`.codespace/<app>/` 的检出 = 视图**——本地模式可暂态被需求分支占用,收尾恢复
16
+ - **严禁直接在任何主干检出上提交载荷**:分支感知 pre-commit hook 机械拦截(workspace 仓库仅放行控制面白名单:`CBB-WORKSPACE.json` / `.gitignore` / `AGENTS.md` / `openspec/config.yaml` / `openspec/schemas/**`;应用仓库主干全拦;**merge 提交(`MERGE_HEAD` 存在)放行**,见 ADR 0007)——**被拦是防线生效,不是故障**;`--no-verify` 绕过仅限用户显式要求
17
+ - 需求分支推送唯一出口 = `cbb-worktree-push`(init 创建流程零远端操作,lazy push);推送后可选提 MR 或**直接合并**(`merge-branches.js`,主干 / 测试分支,ADR 0008)
18
+ - **收尾合并门禁(ADR 0007)**:两模式收尾均须每仓库"已合并"(`safety-check` / `merge-branches.js --check` 判定);未合并三选一——提 MR / 直合并 / 确认废弃(远程重命名 `<分支>-deprecated`);交付完成回主干并自动删本地+远程分支,**废弃出口保留本地分支**(细则见 cbb-local-mode Step 5 / cbb-worktree-close Step 3.4)
26
19
  - **模式误判零代价**:本地模式中途要并行/隔离 → 重跑 `cbb-worktree-init`(分支已存在自动复用,提交随身带走)
27
20
 
28
- **五时刻对齐(agent 必须遵守):**
21
+ **五时刻对齐(agent 必须遵守;1–3 的细则见 `cbb-local-mode` 技能):**
29
22
 
30
- 1. **开工前——状态自检**(`cbb-local-mode` 的 `check-local-state.js`):干净态零交互直接开工;**遗留态(别的需求未收尾,任一仓库检出在非主干分支)必须让用户四选一**(继续 / 先收尾 / 升级 worktree / 丢弃),不得自行决定
23
+ 1. **开工前——状态自检**:干净态零交互直接开工;**遗留态(别的需求未收尾,任一仓库检出在非主干分支)必须让用户四选一**(继续 / 先收尾 / 升级 worktree / 丢弃),不得自行决定
31
24
  2. **开工时——布局确认**:向用户展示参与仓库与分支名,确认后执行
32
25
  3. **进行中——插单归属**:用户插入非本需求的改动,先确认归属(并入 / 收尾后处理 / 转 worktree),不得静默混入
33
- 4. **打包前——主干对齐**(任何构建打包动作前):对产物所涉及的仓库(按代码来源判定:哪个仓库的代码被编进产物就同步哪个,与产物存放位置无关;多仓聚合产物则全部同步)先对齐再打包——① `git fetch <远端>`;② 在当前需求分支执行 `git merge <远端>/<主干>`(远端通常为 `origin`,主干名以远端 HEAD 为准,常见 main / master;在 main 检出上打包时改为 `git pull`;工作区未提交修改阻碍合并时,先提交或暂存再对齐);③ 冲突未解决时**禁止打包**,停下报告用户;fetch/merge 失败 → 报告并等待用户指示;④ 对齐完成后(由此产生的 merge 提交属正常)再执行打包命令(`mvn package` / `npm run build` / 前端 bundle / 生成部署包等凡产出构建产物者);产物目录须已被 `.gitignore` 忽略(未忽略先补 ignore——该修改属普通变更,随需求分支提交)。产物基线 = 主干最新,避免基于过时分支打包导致的集成返工;**不 rebase、不 force push**,merge 提交随下次 push 一并推送
34
- 5. **打包后——产物收集与留存**:打包完成后把各仓库产物**复制**到 `<空间根>/.build/<需求名>/<应用名>/`(原件保持原位;需求名 = 分支名;workspace 仓库产物用 `workspace` 子目录;目标目录已存在时先向用户确认清理或覆盖;`.build/` 由 cbb 写入空间根 `.gitignore`)——产物按需求聚合留存,worktree 关闭后不丢失;打包时顺带报告 `.build/` 积压现状(需求目录数与总体积),建议清理已交付或已废弃需求的产物目录,经用户逐项确认后执行、**永不自动删除**
26
+ 4. **打包前——主干对齐**(任何构建打包动作前):产物涉及哪些仓库(按代码来源判定,与产物存放位置无关;多仓聚合则全部)就先同步哪些——① `git fetch <远端>`;② 在当前需求分支 `git merge <远端>/<主干>`(主干名以远端 HEAD 为准;在主干检出上打包改为 `git pull`;未提交修改阻碍合并时先提交或暂存);③ 冲突未解决**禁止打包**,停下报告用户;fetch/merge 失败 → 报告并等待指示;④ 对齐完成(merge 提交属正常)再打包;产物目录须已被 `.gitignore` 忽略(未忽略先补,随需求分支提交)。产物基线 = 主干最新;**不 rebase、不 force push**,merge 提交随下次 push 一并推送
27
+ 5. **打包后——产物收集与留存**:把各仓库产物**复制**到 `<空间根>/.build/<需求名>/<应用名>/`(原件原位;需求名 = 分支名;workspace 用 `workspace` 子目录;目标已存在先向用户确认清理或覆盖;`.build/` 由 cbb 写入空间根 `.gitignore`)——按需求聚合留存,worktree 关闭后不丢失;打包时顺带报告 `.build/` 积压现状(需求目录数与总体积),建议清理已交付/废弃需求的产物目录,经用户逐项确认后执行、**永不自动删除**
35
28
 
36
- **读基准代码约定:** 本地模式进行中,工作区是开发态而非基准——要读"main 现状"先确认检出分支,或直接 `git show main:<path>` / `git diff main`;不要默认工作区即基准。
29
+ **读基准代码约定:** 本地模式进行中,工作区是开发态而非基准——读"主干现状"须从主干显式获取;不要默认工作区即基准(细则见 `cbb-local-mode` 技能)。
37
30
 
38
- **检验标准:** 任何 main 检出上无载荷提交(hook 兜底,git log 可机械审计;merge 提交除外);本地模式收尾后全部仓库回主干且 clean;worktree 模式的**变更(代码改动)**只出现在需求工作树内(打包产物按第 5 条收集到空间根 `.build/`,不在此限);需求分支只经 push 技能产生;**未合并且非废弃的分支无法通过收尾(门禁兜底)**。
31
+ **检验标准:** 任何主干检出上无载荷提交(hook 兜底,git log 可机械审计;merge 提交除外);本地模式收尾后全部仓库回主干且 clean;worktree 模式的变更(代码改动)只出现在需求工作树内(打包产物按第 5 条收集到 `.build/`,不在此限);需求分支只经 push 技能产生;**未合并且非废弃的分支无法通过收尾(门禁兜底)**。
@@ -289,6 +289,7 @@ public interface OrderConverter {
289
289
  - 二方、三方调用必须通过防腐层(Repo),面向失败编程,考虑网络抖动、服务劣化等异常
290
290
  - 所有二方、三方调用必须明确设置超时时间
291
291
  - 三方调用发生错误时,对错误响应编码分类打点,必须打印日志
292
+ - 第三方资源有限频约束:调用前确认频率上限(QPS / 日配额),以缓存 / 批量合并 / 异步削峰控制调用量;接近或超限时必须有降级路径
292
293
 
293
294
  #### IO 优化
294
295
 
@@ -9,9 +9,17 @@ require('./silence-popup');
9
9
  *
10
10
  * 用法:
11
11
  * node deprecate-branches.js <ws_root> <branch> [--apps a,b]
12
+ * node deprecate-branches.js <ws_root> <branch> [--apps a,b] --check # 仅检查,不重命名
13
+ *
14
+ * 模式:
15
+ * --check 只读检查:逐仓库报远端分支存在性与本地未推送提交数
16
+ * (ls-remote 取远端权威 hash 与本地 <branch> 比对)。
17
+ * 用途:废弃重命名只覆盖远端已推送的部分——本地 ahead 提交
18
+ * 不会被带进 <branch>-deprecated,执行前须提示用户决策。
19
+ * 默认 执行远端重命名 <branch> → <branch>-deprecated
12
20
  *
13
21
  * 退出码:0 全部成功(含 skipped)/ 1 参数错误 / 2 存在失败
14
- * 末行:DEPRECATE_RESULT [...]
22
+ * 末行:DEPRECATE_RESULT [...] 或 DEPRECATE_CHECK_RESULT [...]
15
23
  */
16
24
 
17
25
  const { execSync } = require('child_process');
@@ -44,6 +52,51 @@ function loadApps(wsRoot) {
44
52
  }
45
53
  }
46
54
 
55
+ /**
56
+ * 检查单个仓库(--check,只读):
57
+ * unpushed: 0 = 已全部推送;>0 = 本地领先远端 N 个提交;'all' = 远端无该分支(提交全部未推送)
58
+ */
59
+ function checkRepo(name, repo, branch) {
60
+ const rec = { name, status: 'skipped', remote_exists: false, unpushed: 0, detail: null };
61
+ if (!fs.existsSync(path.join(repo, '.git'))) {
62
+ rec.detail = 'no-repo';
63
+ return rec;
64
+ }
65
+ const remote = run('git remote', repo);
66
+ if (!remote.ok || !remote.out) {
67
+ rec.detail = 'no-remote';
68
+ return rec;
69
+ }
70
+ const r = remote.out.split(/\r?\n/)[0];
71
+ if (!run('git rev-parse --verify --quiet "refs/heads/' + branch + '"', repo).ok) {
72
+ rec.detail = 'no-local-branch';
73
+ return rec;
74
+ }
75
+ const ls = run('git ls-remote --heads "' + r + '" "refs/heads/' + branch + '"', repo);
76
+ if (!ls.ok) {
77
+ rec.status = 'failed';
78
+ rec.detail = 'ls-remote-failed';
79
+ return rec;
80
+ }
81
+ if (!ls.out) {
82
+ rec.status = 'ok';
83
+ rec.unpushed = 'all';
84
+ rec.detail = 'remote-has-no-branch';
85
+ return rec;
86
+ }
87
+ const remoteHash = ls.out.split(/\r?\n/)[0].split(/\s+/)[0];
88
+ const cnt = run('git rev-list --count "' + remoteHash + '..refs/heads/' + branch + '"', repo);
89
+ if (!cnt.ok) {
90
+ rec.status = 'failed';
91
+ rec.detail = 'rev-list-failed';
92
+ return rec;
93
+ }
94
+ rec.status = 'ok';
95
+ rec.remote_exists = true;
96
+ rec.unpushed = parseInt(cnt.out, 10) || 0;
97
+ return rec;
98
+ }
99
+
47
100
  /** 对单个仓库执行废弃重命名 */
48
101
  function deprecateRepo(name, repo, branch) {
49
102
  const newName = branch + '-deprecated';
@@ -95,6 +148,7 @@ function main() {
95
148
  const appFilter = appsIdx >= 0
96
149
  ? new Set((args[appsIdx + 1] || '').split(',').map(s => s.trim()).filter(Boolean))
97
150
  : null;
151
+ const checkOnly = args.includes('--check');
98
152
 
99
153
  const repos = [{ name: 'workspace', repo: wsRoot }];
100
154
  for (const app of loadApps(wsRoot)) {
@@ -102,6 +156,17 @@ function main() {
102
156
  repos.push({ name: app.name, repo: path.join(wsRoot, '.codespace', app.name) });
103
157
  }
104
158
 
159
+ if (checkOnly) {
160
+ const results = repos.map(rp => checkRepo(rp.name, rp.repo, branch));
161
+ results.forEach(r => {
162
+ const up = r.status === 'ok' && r.unpushed !== 0 ? ' [unpushed: ' + r.unpushed + ']' : '';
163
+ console.log(r.status.toUpperCase() + ' ' + r.name + (r.detail ? ' — ' + r.detail : '') + up);
164
+ });
165
+ console.log('---');
166
+ console.log('DEPRECATE_CHECK_RESULT', JSON.stringify({ branch, results }));
167
+ process.exit(results.some(r => r.status === 'failed') ? 2 : 0);
168
+ }
169
+
105
170
  const results = repos.map(rp => deprecateRepo(rp.name, rp.repo, branch));
106
171
 
107
172
  results.forEach(r => {
@@ -116,4 +181,4 @@ function main() {
116
181
 
117
182
  if (require.main === module) main();
118
183
 
119
- module.exports = { run, loadApps, deprecateRepo };
184
+ module.exports = { run, loadApps, checkRepo, deprecateRepo };
@@ -21,7 +21,8 @@ require('./silence-popup');
21
21
  * - 检出 dirty(含未跟踪)→ FAIL dirty-worktree(列未提交项数;不自动 stash,交用户决策)
22
22
  * - 本地有主干分支 → git checkout <主干>;仅远端有 → git checkout -b <主干> --track origin/<主干>
23
23
  * 5. 追平远端:origin/<主干> 存在 → git pull --ff-only origin <主干>
24
- * (本地主干与 origin 分叉 → FAIL pull-failed,交用户决策;远端无主干 → 注记跳过)
24
+ * (成功详情注记 `pulled origin/<主干>` / `up-to-date`;本地主干与 origin 分叉 → FAIL
25
+ * pull-failed,交用户决策;远端无主干 → 注记跳过)
25
26
  * 6. 已在主干 → 跳过切回,仍追平(幂等)
26
27
  *
27
28
  * 退出码:0 全部成功(含 no-repo / 已在主干)/ 1 参数错误 / 2 任一仓库失败
@@ -160,12 +161,18 @@ function main() {
160
161
  }
161
162
 
162
163
  if (hasRef(rp.repo, 'refs/remotes/origin/' + main)) {
164
+ const before = run('git rev-parse --quiet HEAD', rp.repo);
163
165
  const pull = run('git pull --ff-only --quiet origin "' + main + '"', rp.repo);
164
166
  if (!pull.ok) {
165
167
  rec.status = 'failed';
166
168
  rec.reason = 'pull-failed';
167
169
  rec.detail = (pull.err || '').split(/\r?\n/)[0];
168
170
  failed = true;
171
+ } else {
172
+ const after = run('git rev-parse --quiet HEAD', rp.repo);
173
+ rec.detail = before.ok && after.ok && before.out !== after.out
174
+ ? 'pulled origin/' + main
175
+ : 'up-to-date';
169
176
  }
170
177
  } else if (!rec.reason) {
171
178
  rec.reason = 'no-remote-trunk';
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: cbb-local-mode
3
- description: "业务空间本地模式开发:空间根与各涉及应用在 .codespace/<app>/ 就地建同名分支,当前会话直接开发(不建 worktree、不换目录)。触发:用户说'本地模式'/'就地开发'/'不走 worktree'/'在这个窗口开发',或双模式选择时用户选本地模式。覆盖开工(状态自检→布局确认→切分支)、开发、提交推送(转 cbb-worktree-push)、收尾(收尾合并门禁 ADR 0007:MR / 直合并 / 确认废弃后回主干 + 删分支)、升级 worktree(重跑 cbb-worktree-init)。一次只承载一个需求。"
3
+ description: "业务空间本地模式开发:空间根与各涉及应用在 .codespace/<app>/ 就地建同名分支,当前会话直接开发(不建 worktree、不换目录)。触发:用户说'本地模式'/'就地开发'/'不走 worktree'/'在这个窗口开发',或双模式选择时用户选本地模式。覆盖开工(状态自检→布局确认→切分支)、开发、提交推送(转 cbb-worktree-push)、收尾(收尾合并门禁 ADR 0007:MR / 直合并 / 确认废弃;交付完成回主干 + 删分支,废弃保留本地分支)、升级 worktree(重跑 cbb-worktree-init)。一次只承载一个需求。"
4
4
  ---
5
5
 
6
6
  # Local Mode(本地模式)
@@ -103,9 +103,36 @@ node "<skill_dir>/scripts/merge-branches.js" "<ws_root>" "<需求名>" --apps "<
103
103
  ```
104
104
 
105
105
  `need-mr`(远端主干受保护)→ 改用提 MR;`failed` → 输出明细、修复后重跑
106
- - **确认废弃** → `deprecate-branches.js`(远程重命名 `<需求名>-deprecated`;本地分支保留原名)
106
+ - **确认废弃** → 三步:检查未推送提交 → 二次确认 → 执行。**废弃后仍进 5.4 步骤 1 回主干,但跳过步骤 2(本地分支保留原名,不调用 `delete-branches.js`)**。
107
107
 
108
- **5.4 交付完成 → 各仓库回主干并删分支**(脚本化;主干名逐仓库经远端 HEAD 探测,兼容 main / master):
108
+ 1. 检查未推送提交(废弃重命名只覆盖远端已推送的部分,本地 ahead 提交不会进 `<需求名>-deprecated`):
109
+
110
+ ```bash
111
+ node "<skill_dir>/scripts/deprecate-branches.js" "<ws_root>" "<需求名>" --apps "<app1,app2>" --check
112
+ ```
113
+
114
+ **解读末行 `DEPRECATE_CHECK_RESULT`**:每仓库 `unpushed`(本地领先远端的提交数;`all` = 远端无该分支、提交全部未推送;`0` = 已全部推送)。存在 `unpushed > 0` / `all` 时用 `AskUserQuestion`:
115
+
116
+ ```
117
+ ⚠️ 以下仓库存在未推送提交(将只保留在本地分支,不进 <需求名>-deprecated):
118
+ - workspace: 3 个 commit
119
+
120
+ 请选择处理方式:
121
+ [先推送](推荐)调 cbb-worktree-push 推送全部提交后回到本步
122
+ [继续] 不推送直接废弃(这些提交只存在于被保留的本地分支)
123
+ [中止] 终止流程,我先手动处理
124
+ ```
125
+
126
+ 2. 二次确认(远程重命名不可逆,`AskUserQuestion` 固定选项):确认废弃 / 中止
127
+ 3. 执行:
128
+
129
+ ```bash
130
+ node "<skill_dir>/scripts/deprecate-branches.js" "<ws_root>" "<需求名>" --apps "<app1,app2>"
131
+ ```
132
+
133
+ `failed` → 报告并终止;完成后进 5.4(**仅步骤 1**)
134
+
135
+ **5.4 交付完成 → 各仓库回主干并删分支**(脚本化;主干名逐仓库经远端 HEAD 探测,兼容 main / master。**废弃出口仅执行步骤 1**——分支保留、不删):
109
136
 
110
137
  1. 各仓库切回主干并追平远端(替代原裸 git 三连;不自动 stash,失败即报告):
111
138
 
@@ -114,20 +141,20 @@ node "<skill_dir>/scripts/return-to-trunk.js" "<ws_root>" --apps "<app1,app2>"
114
141
  ```
115
142
 
116
143
  **解读末行 `TRUNK_RESULT`**:
117
- - 退出码 0 → 全部已回主干并追平
144
+ - 退出码 0 → 全部已回主干并追平(detail 注记 `pulled origin/<主干>` = 本次有追平;`up-to-date` = 本就最新)
118
145
  - 某仓库 `dirty-worktree` → 检出上有未提交改动(回归 5.1 前置,先处理再重跑);**不自动 stash**
119
146
  - 某仓库 `pull-failed`(本地主干与 origin 分叉)→ 输出详情,交用户决策
120
147
  - 某仓库 `cannot-detect-main` → 检查该仓库远端与主干分支(`fetch-failed` 同理)
121
148
  - 幂等:已在主干的仓库仅追平远端,重复执行无害
122
149
 
123
- 2. 本地 + 远程需求分支删除(本地删除含 squash 合并的 `-d` → `-D` 兜底;删除前输出将删清单):
150
+ 2. 本地 + 远程需求分支删除(**仅交付完成出口执行**——MR 已合入 / 直合并成功;**废弃出口跳过本步**,本地分支保留原名。本地删除含 squash 合并的 `-d` → `-D` 兜底;删除前输出将删清单):
124
151
 
125
152
  ```bash
126
153
  node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<需求名>" local_remote
127
154
  ```
128
155
 
129
156
  - openspec change 归档在合并前执行(产物在分支上随交付合入)
130
- - 全部仓库回主干且分支删除后,向用户报告"本地模式已收尾,空间回到干净态"
157
+ - 全部仓库回主干(交付完成出口含分支删除)后,向用户报告"本地模式已收尾,空间回到干净态"
131
158
 
132
159
  ## 升级 worktree(中途要并行 / 要隔离)
133
160
 
@@ -146,7 +173,7 @@ node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<需求名>" local_re
146
173
  | commit 被 hook 拦(main 载荷) | 大声报错是预期行为:回分支重做;禁止自动 --no-verify |
147
174
  | 误在 main 检出上写了文件 | `git checkout -b <需求名>` 后改动随分支走;已 staged 则 unstage 后再切 |
148
175
  | 提交推送 | 说"提交并推送" → cbb-worktree-push(本地布局自动发现) |
149
- | 收尾 | 收尾合并门禁(ADR 0007):`merge-branches.js --check` 判已合并 → 全合并则 `return-to-trunk.js` 回主干 + `delete-branches.js` 删分支(主干名探测);否则三选一(MR / 直合并(临时 worktree) / 确认废弃) |
176
+ | 收尾 | 收尾合并门禁(ADR 0007):`merge-branches.js --check` 判已合并 → 全合并则 `return-to-trunk.js` 回主干 + `delete-branches.js` 删分支(主干名探测);否则三选一(MR / 直合并(临时 worktree) / 确认废弃(仅回主干不删分支)) |
150
177
  | 要并行新需求 | 转 worktree 模式(init 分支从 origin/main 建,不占本地检出);本地需求继续占用根 |
151
178
  | 升级隔离 | 重跑 cbb-worktree-init(提交随身带走);先回主干腾检出(return-to-trunk.js) |
152
179
 
@@ -174,6 +201,11 @@ node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<需求名>" local_re
174
201
  - **问题:** 回主干后未追平远端,本地主干陈旧,下次 `start-local` 从旧基线建分支
175
202
  - **修复:** 收尾固定两步脚本:`return-to-trunk.js`(切回主干 + `pull --ff-only` 追平)→ `delete-branches.js` 删分支
176
203
 
204
+ ### 废弃出口误跑删分支
205
+
206
+ - **问题:** 选"确认废弃"后照 5.4 全跑,`delete-branches.js` 对未合并的废弃分支 `-D` 强删——远端 `<需求名>-deprecated` 只含已推送部分,本地领先提交只剩 reflog 可捞
207
+ - **修复:** 废弃出口仅执行 5.4 步骤 1(回主干 + 追平);本地分支保留原名,**不调用** `delete-branches.js`
208
+
177
209
  ### 中途插单静默混入
178
210
 
179
211
  - **问题:** 用户插的"顺手改一下"与本需求改动混在同一 commit,评审困难
@@ -200,5 +232,6 @@ node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<需求名>" local_re
200
232
  - 切分支统一走 `start-local.js`(基线 = `<remote>/<main>`,不取 HEAD)
201
233
  - 收尾先过门禁判定(`merge-branches.js --check`);未合并必须三选一后收尾
202
234
  - 直合并走 `merge-branches.js`(临时 worktree,零接触;本地检出由 5.4 `return-to-trunk.js` 切回主干)
203
- - 收尾脚本化(`return-to-trunk.js` 回主干 + `delete-branches.js` 删分支)完成后报告干净态
235
+ - 废弃出口:先 `deprecate-branches.js --check` 查未推送提交(有 ahead 须用户决策)→ 二次确认后执行;废弃后**仅回主干**(5.4 步骤 1),不删本地分支
236
+ - 收尾脚本化(`return-to-trunk.js` 回主干 + 交付完成出口 `delete-branches.js` 删分支)完成后报告干净态
204
237
  - 所有写操作前的分支确认(当前检出 = 需求分支)
@@ -9,9 +9,17 @@ require('./silence-popup');
9
9
  *
10
10
  * 用法:
11
11
  * node deprecate-branches.js <ws_root> <branch> [--apps a,b]
12
+ * node deprecate-branches.js <ws_root> <branch> [--apps a,b] --check # 仅检查,不重命名
13
+ *
14
+ * 模式:
15
+ * --check 只读检查:逐仓库报远端分支存在性与本地未推送提交数
16
+ * (ls-remote 取远端权威 hash 与本地 <branch> 比对)。
17
+ * 用途:废弃重命名只覆盖远端已推送的部分——本地 ahead 提交
18
+ * 不会被带进 <branch>-deprecated,执行前须提示用户决策。
19
+ * 默认 执行远端重命名 <branch> → <branch>-deprecated
12
20
  *
13
21
  * 退出码:0 全部成功(含 skipped)/ 1 参数错误 / 2 存在失败
14
- * 末行:DEPRECATE_RESULT [...]
22
+ * 末行:DEPRECATE_RESULT [...] 或 DEPRECATE_CHECK_RESULT [...]
15
23
  */
16
24
 
17
25
  const { execSync } = require('child_process');
@@ -44,6 +52,51 @@ function loadApps(wsRoot) {
44
52
  }
45
53
  }
46
54
 
55
+ /**
56
+ * 检查单个仓库(--check,只读):
57
+ * unpushed: 0 = 已全部推送;>0 = 本地领先远端 N 个提交;'all' = 远端无该分支(提交全部未推送)
58
+ */
59
+ function checkRepo(name, repo, branch) {
60
+ const rec = { name, status: 'skipped', remote_exists: false, unpushed: 0, detail: null };
61
+ if (!fs.existsSync(path.join(repo, '.git'))) {
62
+ rec.detail = 'no-repo';
63
+ return rec;
64
+ }
65
+ const remote = run('git remote', repo);
66
+ if (!remote.ok || !remote.out) {
67
+ rec.detail = 'no-remote';
68
+ return rec;
69
+ }
70
+ const r = remote.out.split(/\r?\n/)[0];
71
+ if (!run('git rev-parse --verify --quiet "refs/heads/' + branch + '"', repo).ok) {
72
+ rec.detail = 'no-local-branch';
73
+ return rec;
74
+ }
75
+ const ls = run('git ls-remote --heads "' + r + '" "refs/heads/' + branch + '"', repo);
76
+ if (!ls.ok) {
77
+ rec.status = 'failed';
78
+ rec.detail = 'ls-remote-failed';
79
+ return rec;
80
+ }
81
+ if (!ls.out) {
82
+ rec.status = 'ok';
83
+ rec.unpushed = 'all';
84
+ rec.detail = 'remote-has-no-branch';
85
+ return rec;
86
+ }
87
+ const remoteHash = ls.out.split(/\r?\n/)[0].split(/\s+/)[0];
88
+ const cnt = run('git rev-list --count "' + remoteHash + '..refs/heads/' + branch + '"', repo);
89
+ if (!cnt.ok) {
90
+ rec.status = 'failed';
91
+ rec.detail = 'rev-list-failed';
92
+ return rec;
93
+ }
94
+ rec.status = 'ok';
95
+ rec.remote_exists = true;
96
+ rec.unpushed = parseInt(cnt.out, 10) || 0;
97
+ return rec;
98
+ }
99
+
47
100
  /** 对单个仓库执行废弃重命名 */
48
101
  function deprecateRepo(name, repo, branch) {
49
102
  const newName = branch + '-deprecated';
@@ -95,6 +148,7 @@ function main() {
95
148
  const appFilter = appsIdx >= 0
96
149
  ? new Set((args[appsIdx + 1] || '').split(',').map(s => s.trim()).filter(Boolean))
97
150
  : null;
151
+ const checkOnly = args.includes('--check');
98
152
 
99
153
  const repos = [{ name: 'workspace', repo: wsRoot }];
100
154
  for (const app of loadApps(wsRoot)) {
@@ -102,6 +156,17 @@ function main() {
102
156
  repos.push({ name: app.name, repo: path.join(wsRoot, '.codespace', app.name) });
103
157
  }
104
158
 
159
+ if (checkOnly) {
160
+ const results = repos.map(rp => checkRepo(rp.name, rp.repo, branch));
161
+ results.forEach(r => {
162
+ const up = r.status === 'ok' && r.unpushed !== 0 ? ' [unpushed: ' + r.unpushed + ']' : '';
163
+ console.log(r.status.toUpperCase() + ' ' + r.name + (r.detail ? ' — ' + r.detail : '') + up);
164
+ });
165
+ console.log('---');
166
+ console.log('DEPRECATE_CHECK_RESULT', JSON.stringify({ branch, results }));
167
+ process.exit(results.some(r => r.status === 'failed') ? 2 : 0);
168
+ }
169
+
105
170
  const results = repos.map(rp => deprecateRepo(rp.name, rp.repo, branch));
106
171
 
107
172
  results.forEach(r => {
@@ -116,4 +181,4 @@ function main() {
116
181
 
117
182
  if (require.main === module) main();
118
183
 
119
- module.exports = { run, loadApps, deprecateRepo };
184
+ module.exports = { run, loadApps, checkRepo, deprecateRepo };
@@ -21,7 +21,8 @@ require('./silence-popup');
21
21
  * - 检出 dirty(含未跟踪)→ FAIL dirty-worktree(列未提交项数;不自动 stash,交用户决策)
22
22
  * - 本地有主干分支 → git checkout <主干>;仅远端有 → git checkout -b <主干> --track origin/<主干>
23
23
  * 5. 追平远端:origin/<主干> 存在 → git pull --ff-only origin <主干>
24
- * (本地主干与 origin 分叉 → FAIL pull-failed,交用户决策;远端无主干 → 注记跳过)
24
+ * (成功详情注记 `pulled origin/<主干>` / `up-to-date`;本地主干与 origin 分叉 → FAIL
25
+ * pull-failed,交用户决策;远端无主干 → 注记跳过)
25
26
  * 6. 已在主干 → 跳过切回,仍追平(幂等)
26
27
  *
27
28
  * 退出码:0 全部成功(含 no-repo / 已在主干)/ 1 参数错误 / 2 任一仓库失败
@@ -160,12 +161,18 @@ function main() {
160
161
  }
161
162
 
162
163
  if (hasRef(rp.repo, 'refs/remotes/origin/' + main)) {
164
+ const before = run('git rev-parse --quiet HEAD', rp.repo);
163
165
  const pull = run('git pull --ff-only --quiet origin "' + main + '"', rp.repo);
164
166
  if (!pull.ok) {
165
167
  rec.status = 'failed';
166
168
  rec.reason = 'pull-failed';
167
169
  rec.detail = (pull.err || '').split(/\r?\n/)[0];
168
170
  failed = true;
171
+ } else {
172
+ const after = run('git rev-parse --quiet HEAD', rp.repo);
173
+ rec.detail = before.ok && after.ok && before.out !== after.out
174
+ ? 'pulled origin/' + main
175
+ : 'up-to-date';
169
176
  }
170
177
  } else if (!rec.reason) {
171
178
  rec.reason = 'no-remote-trunk';
@@ -9,9 +9,17 @@ require('./silence-popup');
9
9
  *
10
10
  * 用法:
11
11
  * node deprecate-branches.js <ws_root> <branch> [--apps a,b]
12
+ * node deprecate-branches.js <ws_root> <branch> [--apps a,b] --check # 仅检查,不重命名
13
+ *
14
+ * 模式:
15
+ * --check 只读检查:逐仓库报远端分支存在性与本地未推送提交数
16
+ * (ls-remote 取远端权威 hash 与本地 <branch> 比对)。
17
+ * 用途:废弃重命名只覆盖远端已推送的部分——本地 ahead 提交
18
+ * 不会被带进 <branch>-deprecated,执行前须提示用户决策。
19
+ * 默认 执行远端重命名 <branch> → <branch>-deprecated
12
20
  *
13
21
  * 退出码:0 全部成功(含 skipped)/ 1 参数错误 / 2 存在失败
14
- * 末行:DEPRECATE_RESULT [...]
22
+ * 末行:DEPRECATE_RESULT [...] 或 DEPRECATE_CHECK_RESULT [...]
15
23
  */
16
24
 
17
25
  const { execSync } = require('child_process');
@@ -44,6 +52,51 @@ function loadApps(wsRoot) {
44
52
  }
45
53
  }
46
54
 
55
+ /**
56
+ * 检查单个仓库(--check,只读):
57
+ * unpushed: 0 = 已全部推送;>0 = 本地领先远端 N 个提交;'all' = 远端无该分支(提交全部未推送)
58
+ */
59
+ function checkRepo(name, repo, branch) {
60
+ const rec = { name, status: 'skipped', remote_exists: false, unpushed: 0, detail: null };
61
+ if (!fs.existsSync(path.join(repo, '.git'))) {
62
+ rec.detail = 'no-repo';
63
+ return rec;
64
+ }
65
+ const remote = run('git remote', repo);
66
+ if (!remote.ok || !remote.out) {
67
+ rec.detail = 'no-remote';
68
+ return rec;
69
+ }
70
+ const r = remote.out.split(/\r?\n/)[0];
71
+ if (!run('git rev-parse --verify --quiet "refs/heads/' + branch + '"', repo).ok) {
72
+ rec.detail = 'no-local-branch';
73
+ return rec;
74
+ }
75
+ const ls = run('git ls-remote --heads "' + r + '" "refs/heads/' + branch + '"', repo);
76
+ if (!ls.ok) {
77
+ rec.status = 'failed';
78
+ rec.detail = 'ls-remote-failed';
79
+ return rec;
80
+ }
81
+ if (!ls.out) {
82
+ rec.status = 'ok';
83
+ rec.unpushed = 'all';
84
+ rec.detail = 'remote-has-no-branch';
85
+ return rec;
86
+ }
87
+ const remoteHash = ls.out.split(/\r?\n/)[0].split(/\s+/)[0];
88
+ const cnt = run('git rev-list --count "' + remoteHash + '..refs/heads/' + branch + '"', repo);
89
+ if (!cnt.ok) {
90
+ rec.status = 'failed';
91
+ rec.detail = 'rev-list-failed';
92
+ return rec;
93
+ }
94
+ rec.status = 'ok';
95
+ rec.remote_exists = true;
96
+ rec.unpushed = parseInt(cnt.out, 10) || 0;
97
+ return rec;
98
+ }
99
+
47
100
  /** 对单个仓库执行废弃重命名 */
48
101
  function deprecateRepo(name, repo, branch) {
49
102
  const newName = branch + '-deprecated';
@@ -95,6 +148,7 @@ function main() {
95
148
  const appFilter = appsIdx >= 0
96
149
  ? new Set((args[appsIdx + 1] || '').split(',').map(s => s.trim()).filter(Boolean))
97
150
  : null;
151
+ const checkOnly = args.includes('--check');
98
152
 
99
153
  const repos = [{ name: 'workspace', repo: wsRoot }];
100
154
  for (const app of loadApps(wsRoot)) {
@@ -102,6 +156,17 @@ function main() {
102
156
  repos.push({ name: app.name, repo: path.join(wsRoot, '.codespace', app.name) });
103
157
  }
104
158
 
159
+ if (checkOnly) {
160
+ const results = repos.map(rp => checkRepo(rp.name, rp.repo, branch));
161
+ results.forEach(r => {
162
+ const up = r.status === 'ok' && r.unpushed !== 0 ? ' [unpushed: ' + r.unpushed + ']' : '';
163
+ console.log(r.status.toUpperCase() + ' ' + r.name + (r.detail ? ' — ' + r.detail : '') + up);
164
+ });
165
+ console.log('---');
166
+ console.log('DEPRECATE_CHECK_RESULT', JSON.stringify({ branch, results }));
167
+ process.exit(results.some(r => r.status === 'failed') ? 2 : 0);
168
+ }
169
+
105
170
  const results = repos.map(rp => deprecateRepo(rp.name, rp.repo, branch));
106
171
 
107
172
  results.forEach(r => {
@@ -116,4 +181,4 @@ function main() {
116
181
 
117
182
  if (require.main === module) main();
118
183
 
119
- module.exports = { run, loadApps, deprecateRepo };
184
+ module.exports = { run, loadApps, checkRepo, deprecateRepo };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jspg-ai/coding-bb",
3
- "version": "0.0.5-beta.3",
3
+ "version": "0.0.5-beta.5",
4
4
  "description": "整合业界热门且高价值的工具、框架与技能,为 AI CODING AGENT 提供统一的行为准则与工作流,辅助开发者将需求高效落地为符合规范的代码",
5
5
  "main": "cbb/lib/install/init.js",
6
6
  "bin": {