@jspg-ai/coding-bb 0.0.3-beta.30 → 0.0.3-beta.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/cbb/lib/external/index.js +228 -0
  2. package/cbb/lib/install/claude-code.js +3 -3
  3. package/cbb/lib/install/init.js +125 -30
  4. package/cbb/lib/install/minimax.js +2 -2
  5. package/cbb/lib/install/tool-selection.js +8 -30
  6. package/cbb/worktrees/skills/cbb-local-mode/SKILL.md +1 -1
  7. package/cbb/worktrees/skills/cbb-worktree-init/SKILL.md +1 -1
  8. package/cbb/worktrees/skills/cbb-worktree-push/SKILL.md +32 -40
  9. package/config/upstream-mirrors.json +5 -0
  10. package/external/frontend-design/.version +6 -0
  11. package/external/frontend-design/LICENSE.txt +177 -0
  12. package/external/frontend-design/SKILL.md +71 -0
  13. package/openspec/.version +2 -2
  14. package/openspec/commands/archive.md +15 -8
  15. package/openspec/commands/bulk-archive.md +13 -6
  16. package/openspec/commands/continue.md +0 -1
  17. package/openspec/commands/explore.md +1 -1
  18. package/openspec/commands/ff.md +1 -1
  19. package/openspec/commands/onboard.md +9 -4
  20. package/openspec/commands/update.md +11 -5
  21. package/openspec/commands/verify.md +57 -26
  22. package/openspec/skills/openspec-apply-change/SKILL.md +1 -1
  23. package/openspec/skills/openspec-archive-change/SKILL.md +16 -9
  24. package/openspec/skills/openspec-bulk-archive-change/SKILL.md +14 -7
  25. package/openspec/skills/openspec-continue-change/SKILL.md +1 -2
  26. package/openspec/skills/openspec-explore/SKILL.md +2 -2
  27. package/openspec/skills/openspec-ff-change/SKILL.md +2 -2
  28. package/openspec/skills/openspec-new-change/SKILL.md +1 -1
  29. package/openspec/skills/openspec-onboard/SKILL.md +10 -5
  30. package/openspec/skills/openspec-propose/SKILL.md +1 -1
  31. package/openspec/skills/openspec-sync-specs/SKILL.md +1 -1
  32. package/openspec/skills/openspec-update-change/SKILL.md +12 -6
  33. package/openspec/skills/openspec-verify-change/SKILL.md +59 -27
  34. package/package.json +3 -2
  35. package/superpowers/.version +3 -3
  36. package/superpowers/skills/writing-plans/SKILL.md +32 -20
  37. package/superpowers/skills/writing-plans/plan-document-reviewer-prompt.md +0 -49
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * External 小型外部依赖管理模块
5
+ *
6
+ * 管理顶层 external/ 目录下的零散第三方依赖(当前为 frontend-design skill,
7
+ * 来源 anthropics/skills)。与 superpowers / openspec 的整仓 pin 不同,
8
+ * 这里是「一依赖一目录、pin 文件放依赖自己的子目录内」的轻量形态:
9
+ *
10
+ * external/<dep-name>/
11
+ * ├── .version # pin:{ repo, ref(commit sha), path(tarball 内子路径), updatedAt }
12
+ * └── ... # 上游 path 子目录的内容(如 SKILL.md + LICENSE.txt)
13
+ *
14
+ * 新增外部依赖 = 在 external/ 下新建子目录并写入 .version,无需改本模块。
15
+ *
16
+ * 网络:GitHub 直连可能被墙 / 被本机 DNS 劫持,走 config/upstream-mirrors.json
17
+ * 的 'external' 镜像前缀列表(按顺序尝试,首个成功即用;空字符串 = 直连)。
18
+ *
19
+ * 升级方式:cbbm update --upstream-external [name](按 .version 的 ref 幂等重拉,省略 name = 全部)
20
+ * 版本检查:cbbm check-upstream(对比 pinned sha 与上游 main 该 path 的最新提交)
21
+ */
22
+
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+ const os = require('os');
26
+
27
+ const SHARED_STANDARDS_DIR = path.join(__dirname, '..', '..', '..');
28
+
29
+ // External 依赖根目录
30
+ const EXTERNAL_DIR = path.join(SHARED_STANDARDS_DIR, 'external');
31
+ const VERSION_FILE_NAME = '.version';
32
+ // skill 型依赖的完整性标记(上游 path 内容含 SKILL.md 时以其校验完整性)
33
+ const SKILL_MARKER = 'SKILL.md';
34
+
35
+ const { log } = require('../utils/output');
36
+ const { extractTarGz } = require('../utils/tar');
37
+ const { loadMirrors, fetchWithMirrors } = require('../utils/upstream');
38
+
39
+ // ── 依赖发现与 pin 读取 ──────────────────────────────────────
40
+
41
+ /**
42
+ * 读取依赖目录内的 .version pin。字段不全 / JSON 损坏 / 文件缺失返回 null。
43
+ * @param {string} dir
44
+ * @returns {{repo: string, ref: string, path: string, updatedAt?: string}|null}
45
+ */
46
+ function readPin(dir) {
47
+ const file = path.join(dir, VERSION_FILE_NAME);
48
+ if (!fs.existsSync(file)) return null;
49
+ try {
50
+ const data = JSON.parse(fs.readFileSync(file, 'utf-8'));
51
+ if (!data || typeof data.repo !== 'string' || !data.repo.includes('/') ||
52
+ typeof data.ref !== 'string' || !data.ref ||
53
+ typeof data.path !== 'string' || !data.path) {
54
+ return null;
55
+ }
56
+ return data;
57
+ } catch (_) {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * 扫描 external/ 下各依赖子目录,返回全部依赖(pin 无效的目录跳过并告警)。
64
+ * @returns {Array<{name: string, dir: string, pin: object}>}
65
+ */
66
+ function listDeps() {
67
+ if (!fs.existsSync(EXTERNAL_DIR)) return [];
68
+ const deps = [];
69
+ for (const entry of fs.readdirSync(EXTERNAL_DIR)) {
70
+ const dir = path.join(EXTERNAL_DIR, entry);
71
+ if (!fs.statSync(dir).isDirectory()) continue;
72
+ const pin = readPin(dir);
73
+ if (!pin) {
74
+ log(`external/${entry}: 缺少有效 .version pin({ repo, ref, path }),跳过`, 'warn');
75
+ continue;
76
+ }
77
+ deps.push({ name: entry, dir, pin });
78
+ }
79
+ return deps;
80
+ }
81
+
82
+ /**
83
+ * 依赖内容完整性:skill 型(上游含 SKILL.md)要求本地 SKILL.md 存在;
84
+ * 其余类型要求除 .version 外至少有一个条目。
85
+ * @param {string} dir
86
+ * @returns {boolean}
87
+ */
88
+ function validateDepDir(dir) {
89
+ if (!fs.existsSync(dir)) return false;
90
+ if (fs.existsSync(path.join(dir, SKILL_MARKER))) return true;
91
+ return fs.readdirSync(dir).some(f => f !== VERSION_FILE_NAME);
92
+ }
93
+
94
+ // ── 上游拉取 ──────────────────────────────────────────────────
95
+
96
+ /** tarball 下载地址(按镜像前缀拼接;ref = commit sha,上游无 skill 粒度 tag) */
97
+ function tarballUrl(mirror, pin) {
98
+ return `${mirror}https://api.github.com/repos/${pin.repo}/tarball/${pin.ref}`;
99
+ }
100
+
101
+ /**
102
+ * 按 .version 的 ref 下载 tarball,解压出 pin.path 子目录,内容覆盖到 external/<name>/。
103
+ * 幂等:pin 一致且内容完整 → 跳过重拉。.version 由本模块改写,不被上游内容覆盖。
104
+ * @param {string[]} [names] - 指定依赖名;省略 = 全部
105
+ * @returns {Promise<Array<{name: string, ref: string, skipped: boolean, error?: string}>>}
106
+ */
107
+ async function fetchDeps(names) {
108
+ const all = listDeps();
109
+ let deps = all;
110
+ if (names && names.length > 0) {
111
+ const known = new Set(all.map(d => d.name));
112
+ const unknown = names.filter(n => !known.has(n));
113
+ if (unknown.length > 0) {
114
+ throw new Error(`未知的 external 依赖: ${unknown.join(', ')}(现有: ${[...known].join(', ') || '无'})`);
115
+ }
116
+ deps = all.filter(d => names.includes(d.name));
117
+ }
118
+
119
+ const results = [];
120
+ for (const dep of deps) {
121
+ try {
122
+ results.push(await fetchDep(dep));
123
+ } catch (err) {
124
+ results.push({ name: dep.name, ref: dep.pin.ref, skipped: false, error: err.message });
125
+ }
126
+ }
127
+ return results;
128
+ }
129
+
130
+ /**
131
+ * 拉取单个依赖。
132
+ * @param {{name: string, dir: string, pin: object}} dep
133
+ * @returns {Promise<{name: string, ref: string, skipped: boolean}>}
134
+ */
135
+ async function fetchDep(dep) {
136
+ const { name, dir, pin } = dep;
137
+
138
+ // pin 一致且内容完整 → 幂等跳过
139
+ if (validateDepDir(dir)) {
140
+ log(`external/${name} 已按 pin ${pin.ref.slice(0, 12)} 就位,跳过重拉`, 'success');
141
+ return { name, ref: pin.ref, skipped: true };
142
+ }
143
+ log(`external/${name} 内容不完整,按 pin ${pin.ref.slice(0, 12)} 拉取...`, 'info');
144
+
145
+ const mirrors = loadMirrors('external');
146
+ log(`正在获取 ${pin.repo}@${pin.ref.slice(0, 12)}(${mirrors.map(m => m || '直连').join(' → ')})...`, 'info');
147
+ const { buffer, mirror } = await fetchWithMirrors(mirrors, m => tarballUrl(m, pin));
148
+ log(`已从 ${mirror || '(直连)'} 下载 ${(buffer.length / 1024).toFixed(0)} KB`, 'info');
149
+
150
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cbb-ext-'));
151
+ try {
152
+ extractTarGz(buffer, tmpDir);
153
+
154
+ // GitHub tarball 顶层目录名格式:<owner>-<repo>-<short-sha>
155
+ const topPrefix = `${pin.repo.replace('/', '-')}-`;
156
+ const tops = fs.readdirSync(tmpDir).filter(f => {
157
+ const p = path.join(tmpDir, f);
158
+ return fs.statSync(p).isDirectory() && f.startsWith(topPrefix);
159
+ });
160
+ if (tops.length === 0) throw new Error(`解压后未找到 ${topPrefix}* 目录`);
161
+
162
+ // pin.path 是 tarball 内相对仓库根的子路径(如 skills/frontend-design)
163
+ const source = path.join(tmpDir, tops[0], pin.path);
164
+ if (!fs.existsSync(source)) {
165
+ throw new Error(`上游 ${pin.repo}@${pin.ref.slice(0, 12)} 中不存在路径 ${pin.path}(ref 或 path 配置有误)`);
166
+ }
167
+
168
+ // 覆盖内容到 external/<name>/(cpSync 合并,.version 保留)
169
+ fs.cpSync(source, dir, { recursive: true });
170
+ if (!validateDepDir(dir)) throw new Error('拉取后完整性校验未通过');
171
+
172
+ // 刷新 pin 时间戳(ref 不变)
173
+ fs.writeFileSync(
174
+ path.join(dir, VERSION_FILE_NAME),
175
+ JSON.stringify({ ...pin, updatedAt: new Date().toISOString() }, null, 2) + '\n'
176
+ );
177
+
178
+ log(`external/${name} 已更新: ${pin.repo}@${pin.ref.slice(0, 12)}`, 'success');
179
+ return { name, ref: pin.ref, skipped: false };
180
+ } finally {
181
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) { /* ignore */ }
182
+ }
183
+ }
184
+
185
+ /**
186
+ * 检查全部依赖上游是否有新提交(只读,不改动任何文件)。
187
+ * 对比 pinned sha 与上游 main 分支 pin.path 的最新提交 sha
188
+ * (commits?path= 只统计该子路径的变更,仓库其他目录的提交不告警)。
189
+ * 单依赖失败不阻塞其他依赖(返回 {name, error} 条目)。
190
+ * @returns {Promise<Array<{name: string, current: string, latest: string, outdated: boolean}|{name: string, error: string}>>}
191
+ */
192
+ async function checkUpstream() {
193
+ const deps = listDeps();
194
+ const results = await Promise.allSettled(deps.map(async dep => {
195
+ const mirrors = loadMirrors('external');
196
+ const { buffer } = await fetchWithMirrors(mirrors, m =>
197
+ `${m}https://api.github.com/repos/${dep.pin.repo}/commits?path=${encodeURIComponent(dep.pin.path)}&sha=main&per_page=1`
198
+ );
199
+ const data = JSON.parse(buffer.toString('utf-8'));
200
+ const latest = Array.isArray(data) && data[0] ? data[0].sha : null;
201
+ return {
202
+ name: dep.name,
203
+ current: dep.pin.ref,
204
+ latest,
205
+ outdated: Boolean(latest && latest !== dep.pin.ref),
206
+ };
207
+ }));
208
+ return results.map((r, i) => r.status === 'fulfilled'
209
+ ? r.value
210
+ : { name: deps[i].name, error: r.reason.message });
211
+ }
212
+
213
+ // ── 导出 ──────────────────────────────────────────────────────
214
+
215
+ module.exports = {
216
+ // 常量
217
+ EXTERNAL_DIR,
218
+ VERSION_FILE_NAME,
219
+ SKILL_MARKER,
220
+
221
+ // 核心函数
222
+ listDeps,
223
+ readPin,
224
+ validateDepDir,
225
+ fetchDeps,
226
+ checkUpstream,
227
+ tarballUrl,
228
+ };
@@ -4,8 +4,8 @@
4
4
  * 将编码规范分发到 .claude/rules/ 目录
5
5
  *
6
6
  * 退市(selectable: false):团队当前不使用 Claude Code,不在安装面板提供。
7
- * 适配器代码与全部能力保留(含用户级 settings hook);先前已安装的空间按
8
- * .last-tools 记录**粘性维护**(见 tool-selection.js),内容不被清理。
7
+ * 适配器代码与全部能力保留(含用户级 settings hook);旧记录中的遗留内容
8
+ * 不在勾选范围时随清单差集与用户级差集清理移除(ADR 0006)。
9
9
  * 恢复入口:删掉 selectable 即可重新出现在面板(决策:不留参数开关)。
10
10
  */
11
11
 
@@ -22,6 +22,6 @@ module.exports = {
22
22
  settingsFile: path.join(os.homedir(), '.claude', 'settings.json'),
23
23
  settingsFileIsUserLevel: true,
24
24
  hookEvents: ['UserPromptSubmit'],
25
- // 退市:不在安装面板提供(代码保留,存量空间粘性维护)
25
+ // 退市:不在安装面板提供(代码保留;旧记录遗留随清单/用户级差集清理,ADR 0006)
26
26
  selectable: false,
27
27
  };
@@ -84,6 +84,11 @@ const WORKTREE_SKILLS = ['cbb-worktree-init', 'cbb-worktree-close', 'cbb-worktre
84
84
  // cbb-design-to-wiki(Wiki 发布)与 cbb-wiki-ops 保留在源仓库,需要时加入本清单即恢复分发;Java 工具不装。
85
85
  const TOOLS_SKILLS = [];
86
86
 
87
+ // external 外部依赖 skills 白名单:第三方上游 pin vendor 的 skill(源在 external/<name>/,
88
+ // 各子目录自带 .version pin,供 cbbm update --upstream-external 维护)。
89
+ // 非 skill 类外部依赖不走部署通道,不会进本清单。
90
+ const EXTERNAL_SKILLS = ['frontend-design'];
91
+
87
92
  // 不随安装分发的官方 OpenSpec 命令(源仓库保留,仍由 CLI 接管升级):
88
93
  // onboard 为新手引导教程,团队入门已由文档覆盖;sync 是 archive 流程承重件,保留分发。
89
94
  const OPENSPEC_COMMANDS_EXCLUDE = ['onboard.md'];
@@ -100,7 +105,7 @@ const agentsNav = require('./agents-nav');
100
105
  // 命令 → skill 形态转换(commandsAsSkills 适配器,当前仅 MiniMax Code)
101
106
  const commandSkill = require('./command-skill');
102
107
 
103
- // 工具选择策略(退市工具不在面板提供 + 存量粘性保留)
108
+ // 工具选择策略(退市工具不在面板提供;勾选范围即权威,ADR 0006)
104
109
  const toolSelection = require('./tool-selection');
105
110
 
106
111
  /** 适配器的 hook 方言参数(Trae 需顶层 version 字段等差异) */
@@ -156,6 +161,9 @@ const superpowers = require('../superpowers');
156
161
  // OpenSpec 工作流管理(内置集成 + cbb update --upstream-openspec)
157
162
  const openspec = require('../openspec');
158
163
 
164
+ // External 外部依赖管理(内置集成 + cbbm update --upstream-external)
165
+ const external = require('../external');
166
+
159
167
  // ── 颜色与输出辅助 ──────────────────────────────────────
160
168
 
161
169
  const { COLOR, COLOR_ENABLED, c, log, section, step, padVisual, progress, LOG_STYLES } = require('../utils/output');
@@ -376,10 +384,7 @@ async function selectTools(isUpgrade) {
376
384
  .map(i => selectable[i]);
377
385
  }
378
386
 
379
- // 退市但该空间旧记录里有的工具:粘性保留(继续维护,不因退市清删其内容)
380
- result = toolSelection.carryOverRetired(adapters, result, lastTools);
381
-
382
- // 保存本次选择(含粘性保留项,避免记录丢失导致下次被清理),供下次运行时预选
387
+ // 保存本次选择(勾选范围即权威,供下次运行时预选)
383
388
  saveLastTools(result);
384
389
  return result;
385
390
  }
@@ -1046,7 +1051,8 @@ async function init(targetDir, options = {}) {
1046
1051
  fromRecord = true;
1047
1052
  } else if (isNonInteractive) {
1048
1053
  const lastTools = readLastTools();
1049
- const reused = lastTools ? adapters.filter(a => lastTools.has(a.name)) : [];
1054
+ // 旧记录只用于预选可选项——历史遗留的退市工具不并回(ADR 0006:勾选范围即权威)
1055
+ const reused = lastTools ? toolSelection.selectableAdapters(adapters).filter(a => lastTools.has(a.name)) : [];
1050
1056
  selectedAdapters = reused.length > 0 ? reused : toolSelection.selectableAdapters(adapters);
1051
1057
  fromRecord = reused.length > 0;
1052
1058
  if (yes && process.stdin.isTTY) {
@@ -1058,19 +1064,10 @@ async function init(targetDir, options = {}) {
1058
1064
  selectedAdapters = await selectTools(isUpgrade);
1059
1065
  }
1060
1066
 
1061
- // 退市工具粘性保留(面板不出现,但本空间旧记录里有 → 继续维护,内容不被清理)。
1062
- // 覆盖面板外入口:setup 预设(presetAdapters)与非交互记录沿用;交互面板已在 selectTools 内并过。
1063
- const lastToolsForDir = readLastTools();
1064
- const withRetired = toolSelection.carryOverRetired(adapters, selectedAdapters, lastToolsForDir);
1065
- const retiredKept = toolSelection.retiredNames(withRetired);
1066
- if (retiredKept.length > 0) {
1067
- log(`保留上次选择的退市工具:${retiredKept.join('、')}(当前不在安装面板提供,内容继续维护)`, 'dim');
1068
- }
1069
- selectedAdapters = withRetired;
1067
+ // 退市工具不粘性保留(ADR 0006):选择集即权威,旧记录里的退市工具随清单差集清理
1070
1068
 
1071
- // 业务空间根记录本次选择,供 cbb update 等非交互场景沿用;
1072
- // 沿用旧记录时同样落盘,使历史工具名(codebuddy → workbuddy)归一化自愈
1073
- if (isWorkspaceRoot && fromRecord) {
1069
+ // 本次选择落盘(沿用旧记录时同样落盘:归一化历史工具名 + 剔除退市遗留记录)
1070
+ if (fromRecord) {
1074
1071
  saveLastTools(selectedAdapters);
1075
1072
  }
1076
1073
 
@@ -1156,6 +1153,22 @@ async function init(targetDir, options = {}) {
1156
1153
  log('可运行 cbb update --upstream-superpowers 下载最新 skills', 'warn');
1157
1154
  }
1158
1155
 
1156
+ // 部署 external 外部依赖 Skills(第三方上游 pin vendor,白名单控制)
1157
+ const externalSkillsSrc = external.EXTERNAL_DIR;
1158
+ let exCount = 0;
1159
+ if (fs.existsSync(externalSkillsSrc)) {
1160
+ skillsAdapters.forEach(adapter => {
1161
+ const exResult = deploySkills(adapter, isUpgrade, managedPaths, externalSkillsSrc, EXTERNAL_SKILLS);
1162
+ if (exResult.count > 0) {
1163
+ exCount = exResult.count;
1164
+ exResult.names.forEach(name => newPaths.add(`${adapter.skillsDir}/${name}`));
1165
+ }
1166
+ });
1167
+ if (exCount > 0) log(`External Skills → ${skillsDests.join(', ')} (${exCount}/${EXTERNAL_SKILLS.length} 个,白名单)`, 'success');
1168
+ } else {
1169
+ log('external/ 目录不存在,跳过 External Skills 部署', 'warn');
1170
+ }
1171
+
1159
1172
  // 检测用户级 superpowers 安装,避免与项目级重复导致 skill 冲突
1160
1173
  await detectUserLevelSuperpowers(selectedAdapters, yes);
1161
1174
  }
@@ -1847,8 +1860,10 @@ function printSetupSummary(selected, selectedAdapters, parentDir) {
1847
1860
  /**
1848
1861
  * 安装用户级组件(与项目无关的内容,一次安装全局可用,幂等)。
1849
1862
  *
1850
- * - 清单:~/.cbb/.user-components 记录本包管理的用户级路径
1863
+ * - 清单:~/.cbb/.user-components 只记录**本次实装**的用户级路径
1851
1864
  * - 内容:check-update hook(settings 由 settingsManager 幂等管理)
1865
+ * - 差集清理(ADR 0006):不在本次目标集合内的适配器,其清单遗留路径、
1866
+ * 本包版本检查 hook、本地插件一并移除(仅删本包标记项,不动用户自装内容)
1852
1867
  */
1853
1868
  const USER_MANIFEST_FILE = path.join(os.homedir(), '.cbb', '.user-components');
1854
1869
  // 装到用户级的工具 skills(与具体项目无关,任何会话都可能用到)。
@@ -1860,15 +1875,18 @@ async function setupUserLevelComponents(targetAdapters = toolSelection.selectabl
1860
1875
  const srcDir = path.join(SHARED_STANDARDS_DIR, 'cbb', 'tools');
1861
1876
  if (!fs.existsSync(srcDir)) return;
1862
1877
 
1863
- const managed = new Set();
1878
+ // 旧清单仅作差集清理基准,不再并入 managed(ADR 0006:勾选范围即权威,
1879
+ // 本次未实装的路径由下方差集移除,而非累积保留)
1880
+ let oldPaths = [];
1864
1881
  const manifestDir = path.dirname(USER_MANIFEST_FILE);
1865
1882
  fs.mkdirSync(manifestDir, { recursive: true });
1866
1883
  if (fs.existsSync(USER_MANIFEST_FILE)) {
1867
1884
  try {
1868
1885
  const data = JSON.parse(fs.readFileSync(USER_MANIFEST_FILE, 'utf-8'));
1869
- (data.paths || []).forEach(p => managed.add(p));
1886
+ oldPaths = data.paths || [];
1870
1887
  } catch (_) { /* ignore parse errors */ }
1871
1888
  }
1889
+ const managed = new Set();
1872
1890
 
1873
1891
  const summaryParts = [];
1874
1892
 
@@ -1915,6 +1933,43 @@ async function setupUserLevelComponents(targetAdapters = toolSelection.selectabl
1915
1933
  summaryParts.push(`版本检查插件 → ${pluginHookAdapters.map(a => a.displayName).join('、')}`);
1916
1934
  }
1917
1935
 
1936
+ // ── 用户级差集清理(ADR 0006:不在本次勾选范围内的工具,用户级组件一并移除)──
1937
+ const targetNames = new Set(targetAdapters.map(a => a.name));
1938
+
1939
+ // 1) 清单路径差集:旧有新无 → 删除(仅本包管理路径,不碰用户自装内容)
1940
+ const removedPaths = [];
1941
+ oldPaths.forEach(relPath => {
1942
+ if (!managed.has(relPath)) {
1943
+ const target = path.join(home, relPath);
1944
+ if (fs.existsSync(target)) {
1945
+ removePath(target);
1946
+ removedPaths.push(relPath);
1947
+ }
1948
+ }
1949
+ });
1950
+ if (removedPaths.length > 0) {
1951
+ summaryParts.push(`移除清单遗留 ×${removedPaths.length}`);
1952
+ }
1953
+
1954
+ // 2) 非目标适配器:摘除本包版本检查 hook(仅删 _managedBy: cbb 标记项,保留用户配置)
1955
+ // + 本地插件目录
1956
+ const removedNames = [];
1957
+ adapters.filter(a => !targetNames.has(a.name)).forEach(adapter => {
1958
+ let touched = false;
1959
+ if (adapter.settingsFile && adapter.hookEvents) {
1960
+ if (settingsManager.removeHooks(adapter.settingsFile, adapter.hookEvents, hookOpts(adapter))) touched = true;
1961
+ if (cleanLegacySettings(adapter)) touched = true;
1962
+ }
1963
+ if (adapter.pluginHook &&
1964
+ pluginHookManager.remove({ dataDir: adapter.resolveDataDir(), ...adapter.pluginHook })) {
1965
+ touched = true;
1966
+ }
1967
+ if (touched) removedNames.push(adapter.displayName);
1968
+ });
1969
+ if (removedNames.length > 0) {
1970
+ summaryParts.push(`清理 ${removedNames.join('、')} 的用户级组件`);
1971
+ }
1972
+
1918
1973
  if (summaryParts.length > 0) step('✓', '用户级组件', summaryParts.join(' · '));
1919
1974
 
1920
1975
  const pkg = require(path.join(SHARED_STANDARDS_DIR, 'package.json'));
@@ -1931,9 +1986,10 @@ async function setupUserLevelComponents(targetAdapters = toolSelection.selectabl
1931
1986
  * worktree 管理入口与空间导航;旧子集清单升级时由清单差集机制自动补齐全量内容。
1932
1987
  */
1933
1988
  async function updateInstalled() {
1934
- // 用户级组件沿用本目录的工具选择(.last-tools),无记录或为空才回退全量
1989
+ // 用户级组件沿用本目录的工具选择(.last-tools),无记录或为空才回退全量。
1990
+ // 旧记录只预选可选项——历史遗留的退市工具不并回(ADR 0006)
1935
1991
  const lastTools = readLastTools();
1936
- const reused = lastTools ? adapters.filter(a => lastTools.has(a.name)) : [];
1992
+ const reused = lastTools ? toolSelection.selectableAdapters(adapters).filter(a => lastTools.has(a.name)) : [];
1937
1993
  await setupUserLevelComponents(reused.length > 0 ? reused : toolSelection.selectableAdapters(adapters));
1938
1994
 
1939
1995
  const manifest = readManifest();
@@ -2164,13 +2220,14 @@ async function uninstall() {
2164
2220
  // ── 上游版本检查 ──────────────────────────────────────────────
2165
2221
 
2166
2222
  /**
2167
- * 检查 openspec / superpowers 是否有上游新版本(只读,不改动任何文件)。
2223
+ * 检查 openspec / superpowers / external 是否有上游新版本(只读,不改动任何文件)。
2168
2224
  * 供 cbbm check-upstream 调用。
2169
2225
  */
2170
2226
  async function checkUpstream() {
2171
2227
  const results = await Promise.allSettled([
2172
2228
  openspec.checkUpstream().then(r => ({ name: 'openspec', ...r })),
2173
2229
  superpowers.checkUpstream().then(r => ({ name: 'superpowers', ...r })),
2230
+ external.checkUpstream().then(list => ({ name: 'external', list })),
2174
2231
  ]);
2175
2232
 
2176
2233
  let outdated = false;
@@ -2179,6 +2236,22 @@ async function checkUpstream() {
2179
2236
  log(`检查失败: ${r.reason.message}`, 'error');
2180
2237
  continue;
2181
2238
  }
2239
+ // external 是多依赖聚合结果,逐依赖渲染
2240
+ if (r.value.name === 'external') {
2241
+ for (const item of r.value.list) {
2242
+ if (item.error) {
2243
+ log(`external/${item.name}: 检查失败: ${item.error}`, 'error');
2244
+ } else if (!item.latest) {
2245
+ log(`external/${item.name}: 无法获取上游版本`, 'warn');
2246
+ } else if (item.outdated) {
2247
+ log(`external/${item.name}: ${item.current.slice(0, 12)} → ${item.latest.slice(0, 12)}(上游有新提交)`, 'warn');
2248
+ outdated = true;
2249
+ } else {
2250
+ log(`external/${item.name}: ${item.current.slice(0, 12)} 已是最新`, 'success');
2251
+ }
2252
+ }
2253
+ continue;
2254
+ }
2182
2255
  const { name, current, latest, outdated: isOutdated } = r.value;
2183
2256
  if (!latest) {
2184
2257
  log(`${name}: 无法获取上游版本`, 'warn');
@@ -2196,7 +2269,7 @@ async function checkUpstream() {
2196
2269
  }
2197
2270
 
2198
2271
  if (outdated) {
2199
- log('升级方式:修改对应 .version 的 pin 后,执行 cbbm update --upstream-openspec / --upstream-superpowers', 'info');
2272
+ log('升级方式:修改对应 .version 的 pin 后,执行 cbbm update --upstream-openspec / --upstream-superpowers / --upstream-external', 'info');
2200
2273
  }
2201
2274
  }
2202
2275
 
@@ -2205,7 +2278,7 @@ async function checkUpstream() {
2205
2278
  // 入口身份:cbb(用户命令) / cbbm(内部命令,由流程、hook、Agent 技能调用)
2206
2279
  const ENTRY = process.env.CBB_ENTRY === 'cbbm' ? 'cbbm' : 'cbb';
2207
2280
  const INTERNAL_COMMANDS = ['check-update', 'check-upstream', 'wiki', 'wp'];
2208
- const UPSTREAM_FLAGS = ['--upstream-superpowers', '--upstream-openspec'];
2281
+ const UPSTREAM_FLAGS = ['--upstream-superpowers', '--upstream-openspec', '--upstream-external'];
2209
2282
 
2210
2283
  const args = process.argv.slice(2);
2211
2284
  const command = args[0];
@@ -2213,11 +2286,11 @@ const restArgs = args.slice(1);
2213
2286
  const hasUpstreamFlag = restArgs.some(a => UPSTREAM_FLAGS.includes(a));
2214
2287
 
2215
2288
  if (ENTRY === 'cbb' && command === 'update' && hasUpstreamFlag) {
2216
- log('维护者拉源选项已迁移到 cbbm:cbbm update --upstream-superpowers / --upstream-openspec', 'error');
2289
+ log('维护者拉源选项已迁移到 cbbm:cbbm update --upstream-superpowers / --upstream-openspec / --upstream-external', 'error');
2217
2290
  process.exit(1);
2218
2291
  }
2219
2292
  if (ENTRY === 'cbbm' && command === 'update' && !hasUpstreamFlag) {
2220
- log('cbbm update 仅用于维护者拉源(--upstream-superpowers / --upstream-openspec)', 'error');
2293
+ log('cbbm update 仅用于维护者拉源(--upstream-superpowers / --upstream-openspec / --upstream-external)', 'error');
2221
2294
  log('升级已安装内容请使用 cbb update', 'warn');
2222
2295
  process.exit(1);
2223
2296
  }
@@ -2242,7 +2315,8 @@ switch (command) {
2242
2315
  case 'update': {
2243
2316
  const upstreamSuperpowers = restArgs.includes('--upstream-superpowers');
2244
2317
  const upstreamOpenspec = restArgs.includes('--upstream-openspec');
2245
- if (upstreamSuperpowers || upstreamOpenspec) {
2318
+ const upstreamExternal = restArgs.includes('--upstream-external');
2319
+ if (upstreamSuperpowers || upstreamOpenspec || upstreamExternal) {
2246
2320
  // 维护者选项:按 .version 的 pin 重新拉取上游原生内容到本包源仓库(发布流程用)
2247
2321
  if (upstreamSuperpowers) {
2248
2322
  superpowers.fetchLatestSkills().then(result => {
@@ -2275,6 +2349,26 @@ switch (command) {
2275
2349
  process.exit(1);
2276
2350
  });
2277
2351
  }
2352
+ if (upstreamExternal) {
2353
+ // 可选依赖名参数:cbbm update --upstream-external [name](省略 = 全部)
2354
+ const extNames = [];
2355
+ for (let i = 0; i < restArgs.length; i++) {
2356
+ if (restArgs[i] === '--upstream-external' && restArgs[i + 1] && !restArgs[i + 1].startsWith('-')) {
2357
+ extNames.push(restArgs[i + 1]);
2358
+ }
2359
+ }
2360
+ external.fetchDeps(extNames).then(results => {
2361
+ const failed = results.filter(r => r.error);
2362
+ if (failed.length > 0) {
2363
+ failed.forEach(r => log(`external/${r.name}: 更新失败: ${r.error}`, 'error'));
2364
+ process.exit(1);
2365
+ }
2366
+ log('external 外部依赖完整性校验通过', 'success');
2367
+ }).catch(err => {
2368
+ log(`更新失败: ${err.message}`, 'error');
2369
+ process.exit(1);
2370
+ });
2371
+ }
2278
2372
  } else {
2279
2373
  updateInstalled().catch(err => {
2280
2374
  log(err.message, 'error');
@@ -2329,12 +2423,13 @@ function showHelp() {
2329
2423
  @jspg-ai/coding-bb v${pkg.version} — cbbm:内部命令(由流程 / hook / Agent 技能调用)
2330
2424
 
2331
2425
  cbbm check-update 版本检查(用户级 hook 每日自动触发)
2332
- cbbm check-upstream 检查 openspec / superpowers 是否有上游新版本
2426
+ cbbm check-upstream 检查 openspec / superpowers / external 是否有上游新版本
2333
2427
  cbbm wiki / cbbm wp Wiki 查询与发布(AI 通过技能调用)
2334
2428
 
2335
2429
  维护者选项(按 .version 的 pin 重新拉取上游原生内容到本包源仓库):
2336
2430
  cbbm update --upstream-superpowers 按 pin tag 拉取 superpowers skills(走镜像)
2337
2431
  cbbm update --upstream-openspec 按 pin 版本从 npmmirror 渲染 OpenSpec 工作流
2432
+ cbbm update --upstream-external [名] 按 pin sha 拉取 external 外部依赖(省略名 = 全部,走镜像)
2338
2433
  `);
2339
2434
  return;
2340
2435
  }
@@ -19,7 +19,7 @@
19
19
  * 退市(selectable: false):实测 MiniMax Code 不支持目录级 skill 与 command
20
20
  * (无项目级 commands 目录,skill 亦需插件形态承载),当前分发形态不可用。
21
21
  * 适配器代码与全部能力保留(备将来改走插件形态),但不在安装面板提供;
22
- * 已安装过(beta.29)的空间按 .last-tools 记录**粘性维护**,内容不被清理。
22
+ * 旧记录中的遗留内容不在勾选范围时随清单差集与用户级差集清理移除(ADR 0006)。
23
23
  */
24
24
 
25
25
  const path = require('path');
@@ -55,6 +55,6 @@ module.exports = {
55
55
  timeout: 10,
56
56
  },
57
57
  resolveDataDir,
58
- // 退市:不在安装面板提供(代码保留,存量空间粘性维护)
58
+ // 退市:不在安装面板提供(代码保留;旧记录遗留随清单/用户级差集清理,ADR 0006)
59
59
  selectable: false,
60
60
  };
@@ -1,20 +1,18 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * 工具选择策略:安装面板的可见集合 + 退市工具的粘性保留。
4
+ * 工具选择策略:安装面板的可见集合(未退市适配器)。
5
5
  *
6
6
  * 背景(2026-09-28 决策):MiniMax Code 实测不支持目录级 skill 与 command、
7
7
  * Claude Code 团队当前未用,两者**退市**——适配器与全部能力保留(见各适配器
8
8
  * 文件头),仅不在 `cbb setup` / `cbb update` 的安装面板提供。
9
9
  *
10
- * 两条规则:
11
- * 1. **可选项**(selectableAdapters):面板项、非交互全选兜底、setup 默认预选、
12
- * 用户级组件新装,一律只用未退市适配器(`selectable !== false`);
13
- * 2. **粘性保留**(carryOverRetired):某空间的 `.last-tools` 记录过退市工具
14
- * (即确曾安装),则继续纳入本次安装/升级集合——面板不出现,内容不因退市
15
- * 被清单差集清删;`.last-tools` 一并保持该记录,避免记录丢失导致下次清理。
16
- *
17
- * 恢复入口:删掉适配器的 `selectable: false` 即回到面板(决策:不留参数开关)。
10
+ * 退市工具的取舍(ADR 0006,反转 beta.30 的粘性保留决策):
11
+ * - 退市工具**不进**选择集:面板、非交互沿用、setup 预设、用户级组件,
12
+ * 一律只用未退市适配器(`selectable !== false`);
13
+ * - 勾选范围即权威:`.last-tools` 只如实记录本次实际选择,不在范围内的
14
+ * 工具内容(含用户级 hook / 插件)由清单差集与用户级差集清理移除;
15
+ * - 恢复入口:删掉适配器的 `selectable: false` 即回到面板。
18
16
  */
19
17
 
20
18
  /** 面板/默认可选适配器(未标 selectable: false 的全部) */
@@ -22,24 +20,4 @@ function selectableAdapters(adapters) {
22
20
  return adapters.filter(a => a.selectable !== false);
23
21
  }
24
22
 
25
- /**
26
- * 把「本空间旧记录中的退市工具」并入选择结果(按注册表顺序追加,去重)。
27
- * @param {Array} adapters 注册表全量
28
- * @param {Array} selected 本次选择(面板 / 预设 / 记录沿用)
29
- * @param {Set<string>|null} lastTools 该空间的旧工具记录(.last-tools)
30
- * @returns {Array} 并入后的有序数组(无退市工具需保留时原样返回)
31
- */
32
- function carryOverRetired(adapters, selected, lastTools) {
33
- if (!lastTools || lastTools.size === 0) return selected;
34
- const selectedNames = new Set(selected.map(a => a.name));
35
- const carried = adapters.filter(a =>
36
- a.selectable === false && lastTools.has(a.name) && !selectedNames.has(a.name));
37
- return carried.length > 0 ? selected.concat(carried) : selected;
38
- }
39
-
40
- /** 结果集中退市工具的显示名(供安装输出提示,如「保留上次选择的 Claude Code」) */
41
- function retiredNames(selected) {
42
- return selected.filter(a => a.selectable === false).map(a => a.displayName);
43
- }
44
-
45
- module.exports = { selectableAdapters, carryOverRetired, retiredNames };
23
+ module.exports = { selectableAdapters };
@@ -73,7 +73,7 @@ node "<skill_dir>/scripts/start-local.js" "<ws_root>" "<branch>" --apps "<app1>,
73
73
 
74
74
  ## Step 4:提交推送(转 cbb-worktree-push)
75
75
 
76
- 对话中说"提交并推送"即触发 `cbb-worktree-push`——其仓库发现已支持**本地布局**(根 + 持有同名分支的 `.codespace/<app>/`),dry-run 预览 → 用户确认 commit 信息 → 批量 commit → push → MR 预填链接,全流程与 worktree 模式一致。
76
+ 对话中说"提交并推送"即触发 `cbb-worktree-push`——其仓库发现已支持**本地布局**(根 + 持有同名分支的 `.codespace/<app>/`),dry-run 预览(含 agent 生成的 commit 信息)→ 批量 commit → push → MR 预填链接,全流程与 worktree 模式一致。
77
77
 
78
78
  ## Step 5:收尾(MR 合并后)
79
79
 
@@ -265,7 +265,7 @@ node "<skill_dir>/scripts/create-branches.js" "<ws_root>" "<branch>" [--apps "ap
265
265
 
266
266
  ### 3.4 分支就位(lazy push,不推远端)
267
267
 
268
- **本技能不执行任何远端推送(ADR 0002 lazy push)**:分支创建止步于本地。远端副作用收敛到 `cbb-worktree-push` 唯一出口(dry-run 预览 → 用户确认 commit 信息 → push → MR 链接)——推送类失败(认证/网络/保护分支)在 push 技能内暴露并处理,不在创建流程中阻塞。
268
+ **本技能不执行任何远端推送(ADR 0002 lazy push)**:分支创建止步于本地。远端副作用收敛到 `cbb-worktree-push` 唯一出口(dry-run 预览 → agent 生成 commit 信息并提交 → push → MR 链接)——推送类失败(认证/网络/保护分支)在 push 技能内暴露并处理,不在创建流程中阻塞。
269
269
 
270
270
  > 旧版在本步骤推送空分支建立 upstream(`push-branches.js`),自 0.0.3-beta.22 起移除:避免每个需求在远端留下空分支尸体;`push-worktrees.js` 首次推送时自动建立 upstream,行为等价。
271
271