@jspg-ai/coding-bb 0.0.3-beta.2 → 0.0.3-beta.20

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 (101) hide show
  1. package/cbb/dev-standards/rules/cbb-ai-behavior.md +128 -116
  2. package/cbb/lib/install/claude-code.js +1 -3
  3. package/cbb/lib/install/init.js +344 -127
  4. package/cbb/lib/install/opencode.js +35 -31
  5. package/cbb/lib/install/qoder.js +1 -3
  6. package/cbb/lib/install/rule-rewrite.js +27 -0
  7. package/cbb/lib/install/setup-decision.js +48 -0
  8. package/cbb/lib/install/trae.js +43 -0
  9. package/cbb/lib/install/workbuddy.js +36 -0
  10. package/cbb/lib/openspec/index.js +337 -554
  11. package/cbb/lib/superpowers/index.js +246 -265
  12. package/cbb/lib/utils/check-update.js +12 -1
  13. package/cbb/lib/utils/gitignore.js +9 -2
  14. package/cbb/lib/utils/settings.js +17 -4
  15. package/cbb/lib/utils/tar.js +92 -0
  16. package/cbb/lib/utils/upstream.js +90 -0
  17. package/cbb/worktrees/skills/cbb-worktree-close/SKILL.md +32 -23
  18. package/cbb/worktrees/skills/cbb-worktree-close/scripts/remove-worktrees.js +2 -2
  19. package/cbb/worktrees/skills/cbb-worktree-init/SKILL.md +36 -28
  20. package/cbb/worktrees/skills/cbb-worktree-init/scripts/create-worktrees.js +15 -37
  21. package/cbb/worktrees/skills/cbb-worktree-init/scripts/update-gitignore.js +25 -9
  22. package/cbb/worktrees/skills/cbb-worktree-push/SKILL.md +108 -31
  23. package/cbb/worktrees/skills/cbb-worktree-push/scripts/make-mr-urls.js +211 -0
  24. package/config/openspec/schemas/spec-driven/schema.yaml +40 -30
  25. package/config/openspec/schemas/spec-driven/templates/design.md +0 -18
  26. package/config/openspec/schemas/spec-driven/templates/proposal.md +4 -3
  27. package/config/upstream-mirrors.json +12 -0
  28. package/config/workspace-agents.sample.md +45 -39
  29. package/openspec/.version +2 -3
  30. package/openspec/commands/apply.md +189 -175
  31. package/openspec/commands/archive.md +237 -216
  32. package/openspec/commands/bulk-archive.md +355 -327
  33. package/openspec/commands/continue.md +116 -105
  34. package/openspec/commands/explore.md +230 -199
  35. package/openspec/commands/ff.md +115 -104
  36. package/openspec/commands/new.md +74 -63
  37. package/openspec/commands/onboard.md +557 -548
  38. package/openspec/commands/propose.md +161 -150
  39. package/openspec/commands/sync.md +277 -249
  40. package/openspec/commands/update.md +92 -80
  41. package/openspec/commands/verify.md +175 -162
  42. package/openspec/skills/openspec-apply-change/SKILL.md +20 -5
  43. package/openspec/skills/openspec-archive-change/SKILL.md +30 -8
  44. package/openspec/skills/openspec-bulk-archive-change/SKILL.md +36 -6
  45. package/openspec/skills/openspec-continue-change/SKILL.md +14 -2
  46. package/openspec/skills/openspec-explore/SKILL.md +21 -9
  47. package/openspec/skills/openspec-ff-change/SKILL.md +14 -2
  48. package/openspec/skills/openspec-new-change/SKILL.md +13 -1
  49. package/openspec/skills/openspec-onboard/SKILL.md +49 -39
  50. package/openspec/skills/openspec-propose/SKILL.md +15 -3
  51. package/openspec/skills/openspec-sync-specs/SKILL.md +31 -2
  52. package/openspec/skills/openspec-update-change/SKILL.md +27 -14
  53. package/openspec/skills/openspec-verify-change/SKILL.md +17 -3
  54. package/package.json +2 -2
  55. package/superpowers/.version +4 -4
  56. package/superpowers/skills/brainstorming/SKILL.md +47 -12
  57. package/superpowers/skills/brainstorming/scripts/frame-template.html +213 -213
  58. package/superpowers/skills/brainstorming/scripts/server.cjs +723 -723
  59. package/superpowers/skills/brainstorming/visual-companion.md +6 -6
  60. package/superpowers/skills/diagnosing-superpowers/SKILL.md +120 -0
  61. package/superpowers/skills/diagnosing-superpowers/prompts/analyst-common.md +38 -0
  62. package/superpowers/skills/diagnosing-superpowers/prompts/cost-and-time.md +28 -0
  63. package/superpowers/skills/diagnosing-superpowers/prompts/plan-adherence.md +29 -0
  64. package/superpowers/skills/diagnosing-superpowers/prompts/quality-evidence.md +26 -0
  65. package/superpowers/skills/diagnosing-superpowers/prompts/repeated-work.md +30 -0
  66. package/superpowers/skills/diagnosing-superpowers/prompts/request-conflicts.md +20 -0
  67. package/superpowers/skills/diagnosing-superpowers/prompts/scrub-audit.md +33 -0
  68. package/superpowers/skills/diagnosing-superpowers/prompts/scrub.md +29 -0
  69. package/superpowers/skills/diagnosing-superpowers/prompts/similar-session.md +38 -0
  70. package/superpowers/skills/diagnosing-superpowers/prompts/skill-timeline.md +30 -0
  71. package/superpowers/skills/diagnosing-superpowers/prompts/stumbles.md +28 -0
  72. package/superpowers/skills/diagnosing-superpowers/references/context-safety.md +22 -0
  73. package/superpowers/skills/diagnosing-superpowers/references/github-issues.md +47 -0
  74. package/superpowers/skills/diagnosing-superpowers/references/redaction-policy.md +34 -0
  75. package/superpowers/skills/diagnosing-superpowers/references/session-discovery.md +31 -0
  76. package/superpowers/skills/diagnosing-superpowers/templates/bundle-README.md +77 -0
  77. package/superpowers/skills/diagnosing-superpowers/templates/case.md +64 -0
  78. package/superpowers/skills/diagnosing-superpowers/templates/issue.md +51 -0
  79. package/superpowers/skills/diagnosing-superpowers/templates/report.md +82 -0
  80. package/superpowers/skills/executing-plans/SKILL.md +350 -41
  81. package/superpowers/skills/executing-plans/scripts/task-done +52 -0
  82. package/superpowers/skills/executing-plans/scripts/task-start +28 -0
  83. package/superpowers/skills/requesting-code-review/SKILL.md +1 -1
  84. package/superpowers/skills/requesting-code-review/code-reviewer.md +17 -0
  85. package/superpowers/skills/subagent-driven-development/SKILL.md +18 -18
  86. package/superpowers/skills/subagent-driven-development/re-review-prompt.md +1 -1
  87. package/superpowers/skills/subagent-driven-development/scripts/review-package +53 -46
  88. package/superpowers/skills/subagent-driven-development/scripts/sdd-workspace +82 -40
  89. package/superpowers/skills/subagent-driven-development/scripts/task-brief +43 -41
  90. package/superpowers/skills/subagent-driven-development/task-reviewer-prompt.md +2 -2
  91. package/superpowers/skills/systematic-debugging/root-cause-tracing.md +1 -1
  92. package/superpowers/skills/test-driven-development/SKILL.md +10 -0
  93. package/superpowers/skills/using-superpowers/SKILL.md +2 -0
  94. package/superpowers/skills/using-superpowers/references/claude-code-tools.md +29 -0
  95. package/superpowers/skills/using-superpowers/references/muse-tools.md +35 -0
  96. package/superpowers/skills/writing-plans/SKILL.md +30 -9
  97. package/superpowers/skills/writing-skills/SKILL.md +4 -2
  98. package/superpowers/skills/writing-skills/graphviz-conventions.dot +171 -171
  99. package/cbb/worktrees/commands/worktree-close.md +0 -64
  100. package/cbb/worktrees/commands/worktree-init.md +0 -51
  101. package/cbb/worktrees/commands/worktree-push.md +0 -42
@@ -4,7 +4,8 @@
4
4
  * OpenSpec Shared Standards 安装脚本
5
5
  *
6
6
  * 将AI 编码规范与工作流套件安装到目标项目中。
7
- * 支持工具:Claude Code (.claude/rules/)、Qoder (.qoder/rules/)、opencode (.opencode/rules/)
7
+ * 支持工具:Claude Code (.claude/rules/)、Qoder (.qoder/rules/)、opencode (.opencode/rules/)、
8
+ * WorkBuddy (.workbuddy/rules/)、TraeWork/TraeCode 中国版 (.trae/rules/,用户级 hook 在 ~/.trae-cn)
8
9
  *
9
10
  * 使用方式:
10
11
  * npm install -g @jspg-ai/coding-bb # 首次使用
@@ -39,6 +40,7 @@ function execCmd(file, args, opts = {}) {
39
40
  }
40
41
 
41
42
  const SHARED_STANDARDS_DIR = path.join(__dirname, '..', '..', '..');
43
+ const setupDecision = require('./setup-decision');
42
44
 
43
45
  // 目标项目根目录:默认取当前 cwd,init() 可在调用前覆盖。
44
46
  // 注意:使用 let 而非 const 是为了让 cbb setup 能在拉好仓库后把目标目录
@@ -51,14 +53,17 @@ const MANIFEST_FILE = path.join(MANIFEST_DIR, '.managed-by-cbb');
51
53
  const LAST_TOOLS_FILE = path.join(MANIFEST_DIR, '.last-tools');
52
54
 
53
55
  // 适配器
56
+ // 适配器(数组顺序 = 工具选择面板的显示顺序)
54
57
  const adapters = [
55
- require('./claude-code'),
56
- require('./qoder'),
57
58
  require('./opencode'),
59
+ require('./workbuddy'),
60
+ require('./trae'),
61
+ require('./qoder'),
62
+ require('./claude-code'),
58
63
  ];
59
64
 
60
- // superpowers 部署白名单:仅装 openspec 工作流硬依赖 + 方法论基底(源仓库仍保留全量 14 个,
61
- // 供 cbb update --upstream-superpowers 维护;砍掉的与 openspec 工作流重叠或属低频场景)
65
+ // superpowers 部署白名单:仅装 openspec 工作流硬依赖 + 方法论基底(源仓库仍保留全量 15 个,
66
+ // 供 cbbm update --upstream-superpowers 维护;砍掉的与 openspec 工作流重叠或属低频场景)
62
67
  const SUPERPOWERS_WHITELIST = [
63
68
  'writing-plans', // openspec apply:生成 plan.md(schema.yaml 引用)
64
69
  'subagent-driven-development', // openspec apply:执行 plan.md(schema.yaml 引用)
@@ -68,8 +73,9 @@ const SUPERPOWERS_WHITELIST = [
68
73
  'test-driven-development', // TDD 计划结构 + TDD Evidence 方法论基底
69
74
  ];
70
75
 
71
- // worktree 扩展 skills 部署白名单:init / close / push 三件套
72
- // (单层安装:业务空间根是唯一安装点,worktree 管理入口 init 同样部署在根)
76
+ // worktree 管理 skill 清单:init / close / push 三件套(SKILL.md + scripts)。
77
+ // 按 skill 形态部署到各工具的 skillsDir,使这三个能力可被 AI 在对话中自动触发
78
+ // (description 驱动),同时工具也会把它们暴露为斜杠命令供用户手动指定。
73
79
  const WORKTREE_SKILLS = ['cbb-worktree-init', 'cbb-worktree-close', 'cbb-worktree-push'];
74
80
 
75
81
  // 装到空间的 tools skills:当前不分发。
@@ -83,9 +89,38 @@ const OPENSPEC_COMMANDS_EXCLUDE = ['onboard.md'];
83
89
  // settings.json hook 工具
84
90
  const settingsManager = require('../utils/settings');
85
91
 
92
+ /** 适配器的 hook 方言参数(Trae 需顶层 version 字段等差异) */
93
+ function hookOpts(adapter) {
94
+ return adapter.hookDialect ? { dialect: adapter.hookDialect } : {};
95
+ }
96
+
97
+ // 工具历史名迁移:beta.16 及以前为 'codebuddy'(CodeBuddy 目录模型),
98
+ // beta.17 起统一为 'workbuddy'。存量 .last-tools / 清单里的旧名读取时归一,
99
+ // 下次安装由 saveLastTools 以新名落盘。
100
+ const LEGACY_TOOL_NAMES = { codebuddy: 'workbuddy' };
101
+
102
+ /** 工具名集合归一(旧名 → 新名);null 透传 */
103
+ function normalizeToolSet(set) {
104
+ if (!set) return set;
105
+ return new Set([...set].map(t => LEGACY_TOOL_NAMES[t] || t));
106
+ }
107
+
108
+ /**
109
+ * 迁移清理:移除本包写在旧用户级 settings 里的 hook(如 WorkBuddy 适配修正:
110
+ * ~/.codebuddy/settings.json → ~/.workbuddy/settings.json)。
111
+ * removeHooks 只删 _managedBy: cbb 标记的条目,文件清空则删除。返回 true 表示有改动。
112
+ */
113
+ function cleanLegacySettings(adapter) {
114
+ if (!adapter.legacySettingsFile || !adapter.hookEvents) return false;
115
+ return settingsManager.removeHooks(adapter.legacySettingsFile, adapter.hookEvents, hookOpts(adapter));
116
+ }
117
+
86
118
  // 清单驱动的文件清理决策(保护用户文档 CLAUDE.md / AGENTS.md 等)
87
119
  const { pathsToDelete, protectedKeptPaths } = require('./cleanup');
88
120
 
121
+ // 规则 frontmatter 改写(Trae/WorkBuddy 需 alwaysApply: true)
122
+ const { applyRuleRewrites } = require('./rule-rewrite');
123
+
89
124
  // .gitignore 条目决策(纯函数:检测/写入/卸载移除)
90
125
  const gitignore = require('../utils/gitignore');
91
126
 
@@ -184,7 +219,7 @@ function readLastTools() {
184
219
  if (!fs.existsSync(filePath)) return null;
185
220
  try {
186
221
  const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
187
- return new Set(data.tools || []);
222
+ return normalizeToolSet(new Set(data.tools || []));
188
223
  } catch (_) {
189
224
  return null;
190
225
  }
@@ -209,6 +244,39 @@ function removePath(targetPath) {
209
244
  }
210
245
  }
211
246
 
247
+ /**
248
+ * 清理因删除文件/目录而变空的祖先目录(从深到浅,止于 TARGET_DIR)。
249
+ *
250
+ * 删除清单路径只删到该路径本身,其父目录会留空(如改名后残留的空 `.cbb/skills/`)。
251
+ * 此处统一收尾,升级与卸载共用。仅删除「存在且为空」的目录,非空目录(含用户文件)
252
+ * 一律保留。
253
+ *
254
+ * @param {Iterable<string>} relativePaths 已删除的相对路径(正斜杠)
255
+ * @returns {string[]} 实际删除的空目录相对路径
256
+ */
257
+ function cleanupEmptyParentDirs(relativePaths) {
258
+ const dirsToCheck = new Set();
259
+ for (const relativePath of relativePaths) {
260
+ let dir = path.dirname(relativePath);
261
+ while (dir && dir !== '.') {
262
+ dirsToCheck.add(dir);
263
+ dir = path.dirname(dir);
264
+ }
265
+ }
266
+
267
+ const removed = [];
268
+ [...dirsToCheck]
269
+ .sort((a, b) => b.split('/').length - a.split('/').length)
270
+ .forEach(dir => {
271
+ const fullPath = path.join(TARGET_DIR, dir);
272
+ if (fs.existsSync(fullPath) && fs.readdirSync(fullPath).length === 0) {
273
+ removePath(fullPath);
274
+ removed.push(dir);
275
+ }
276
+ });
277
+ return removed;
278
+ }
279
+
212
280
  /**
213
281
  * 安全删除目标路径(文件或目录)
214
282
  * 在清单中 → 直接删除(本包旧版本)
@@ -306,7 +374,8 @@ function deployCoreRules(adapter, isUpgrade, managedPaths) {
306
374
  // 同名文件存在时直接覆盖
307
375
  removePath(destPath);
308
376
 
309
- fs.writeFileSync(destPath, fs.readFileSync(srcPath, 'utf-8'));
377
+ const content = applyRuleRewrites(adapter, fs.readFileSync(srcPath, 'utf-8'));
378
+ fs.writeFileSync(destPath, content);
310
379
  }
311
380
  });
312
381
 
@@ -315,56 +384,117 @@ function deployCoreRules(adapter, isUpgrade, managedPaths) {
315
384
  // ── opencode instructions 登记 ────────────────────────────────
316
385
 
317
386
  /**
318
- * opencode 无原生 rules 目录:把 .opencode/rules/*.md 的 glob 登记进项目根
319
- * opencode.json 的 instructions(官方支持的加载方式)。
387
+ * opencode 无原生 rules 目录:把 rules glob 登记进 .opencode/opencode.json 的
388
+ * instructions(opencode V2 支持的配置位置;随 .opencode gitignore,按机器生成)。
389
+ * instructions 相对路径的解析基准(配置文件目录 vs 项目根)文档未写死,
390
+ * 故登记双 glob 对冲:任一基准下恰有一个命中,另一个空匹配。
320
391
  * - 文件不存在:创建(记入清单,卸载时整体删除)
321
- * - 文件存在:仅追加 pattern(幂等);若清单显示该文件本由本包创建,则续登
322
- * newPaths 防止升级清理误删;JSONC(含注释)解析失败则只提示不改写
392
+ * - 文件存在:幂等补齐缺失的 glob(保留用户已有 instructions)
393
+ * - JSONC(含注释)解析失败则只提示不改写
394
+ * 旧版登记在项目根 opencode.json,由 migrateLegacyInstructions 负责迁移清理。
323
395
  */
324
396
  function injectInstructions(adapter, newPaths, managedPaths) {
325
- const cfgPath = path.join(TARGET_DIR, 'opencode.json');
326
- const pattern = adapter.instructionsPattern;
397
+ const cfgPath = path.join(TARGET_DIR, adapter.instructionsFile);
398
+ const patterns = adapter.instructionsPatterns;
399
+ let cfg = {};
400
+ if (fs.existsSync(cfgPath)) {
401
+ try {
402
+ cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
403
+ } catch (_) {
404
+ log(`${adapter.instructionsFile} 解析失败(可能含注释),未自动登记。请手动在 instructions 中加入 "${patterns.join('", "')}"`, 'warn');
405
+ return;
406
+ }
407
+ if (!cfg || typeof cfg !== 'object') cfg = {};
408
+ }
409
+ const list = Array.isArray(cfg.instructions) ? cfg.instructions : [];
410
+ const merged = [...list];
411
+ let added = false;
412
+ patterns.forEach(p => {
413
+ if (!merged.includes(p)) {
414
+ merged.push(p);
415
+ added = true;
416
+ }
417
+ });
327
418
  if (!fs.existsSync(cfgPath)) {
328
- const cfg = { $schema: 'https://opencode.ai/config.json', instructions: [pattern] };
419
+ cfg.instructions = merged;
420
+ if (!cfg.$schema) cfg.$schema = 'https://opencode.ai/config.json';
421
+ fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
329
422
  fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
330
- newPaths.add('opencode.json');
331
- log(`生成 opencode.json 并登记 rules 加载(instructions: ${pattern})`, 'success');
332
- return;
423
+ newPaths.add(adapter.instructionsFile);
424
+ log(`生成 ${adapter.instructionsFile} 并登记 rules 加载(instructions: ${patterns.join(' | ')})`, 'success');
425
+ } else if (added) {
426
+ cfg.instructions = merged;
427
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
428
+ log(`已在 ${adapter.instructionsFile} instructions 中登记 rules glob`, 'success');
333
429
  }
430
+ // 本包此前创建的文件:续登清单,避免升级清理误删
431
+ if (managedPaths.has(adapter.instructionsFile)) newPaths.add(adapter.instructionsFile);
432
+ }
433
+
434
+ /**
435
+ * 旧版(≤0.0.3-beta.2)把 rules glob 登记在项目根 opencode.json;迁移为
436
+ * .opencode/opencode.json 后需把旧 pattern 从根文件摘除(用户其余配置保留)。
437
+ * 根文件若由本包创建(在清单中),升级时由清单差集自动整体删除,此处只摘 pattern。
438
+ */
439
+ function migrateLegacyInstructions(adapter) {
440
+ const legacyPattern = adapter.legacyInstructionsPattern;
441
+ if (!legacyPattern) return;
442
+ const cfgPath = path.join(TARGET_DIR, 'opencode.json');
443
+ if (!fs.existsSync(cfgPath)) return;
334
444
  let cfg;
335
445
  try {
336
446
  cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
337
447
  } catch (_) {
338
- log(`opencode.json 解析失败(可能含注释),未自动登记。请手动在 instructions 中加入 "${pattern}"`, 'warn');
448
+ log('根 opencode.json 解析失败(可能含注释),未自动迁移旧登记。可手动移除 instructions 中的 "' + legacyPattern + '"', 'warn');
339
449
  return;
340
450
  }
341
- const list = Array.isArray(cfg.instructions) ? cfg.instructions : [];
342
- if (!list.includes(pattern)) {
343
- cfg.instructions = [...list, pattern];
451
+ if (Array.isArray(cfg.instructions) && cfg.instructions.includes(legacyPattern)) {
452
+ cfg.instructions = cfg.instructions.filter(p => p !== legacyPattern);
453
+ if (cfg.instructions.length === 0) delete cfg.instructions;
344
454
  fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
345
- log(`已在 opencode.json instructions 中登记 ${pattern}`, 'success');
455
+ log('已从根 opencode.json 移除旧版 rules 登记(迁移至 .opencode/opencode.json)', 'success');
346
456
  }
347
- // 本包此前创建的文件:续登清单,避免升级清理误删
348
- if (managedPaths.has('opencode.json')) newPaths.add('opencode.json');
349
457
  }
350
458
 
351
- /** 卸载时移除本包登记的 instructions pattern(用户创建的 opencode.json 只删 pattern 不删文件) */
459
+ /** 卸载时移除本包登记的 instructions(用户自建配置只摘本包 glob,不删文件) */
352
460
  function removeInstructionsRegistrations() {
353
461
  adapters.forEach(adapter => {
354
- if (!adapter.instructionsPattern) return;
355
- const cfgPath = path.join(TARGET_DIR, 'opencode.json');
356
- if (!fs.existsSync(cfgPath)) return;
357
- let cfg;
358
- try {
359
- cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
360
- } catch (_) {
361
- return; // 不可解析的文件不动
462
+ if (!adapter.instructionsFile) return;
463
+ const patterns = adapter.instructionsPatterns;
464
+ // 新位置:.opencode/opencode.json(cbb 创建的整体文件由清单删除兜底)
465
+ const cfgPath = path.join(TARGET_DIR, adapter.instructionsFile);
466
+ if (fs.existsSync(cfgPath)) {
467
+ let cfg;
468
+ try {
469
+ cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
470
+ } catch (_) {
471
+ cfg = null; // 不可解析的文件不动
472
+ }
473
+ if (cfg && Array.isArray(cfg.instructions)) {
474
+ const filtered = cfg.instructions.filter(p => !patterns.includes(p));
475
+ if (filtered.length !== cfg.instructions.length) {
476
+ if (filtered.length === 0) delete cfg.instructions;
477
+ else cfg.instructions = filtered;
478
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
479
+ log(`${adapter.instructionsFile}: 移除本包登记的 instructions,保留用户配置`, 'success');
480
+ }
481
+ }
362
482
  }
363
- if (Array.isArray(cfg.instructions) && cfg.instructions.includes(adapter.instructionsPattern)) {
364
- cfg.instructions = cfg.instructions.filter(p => p !== adapter.instructionsPattern);
365
- if (cfg.instructions.length === 0) delete cfg.instructions;
366
- fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
367
- log('opencode.json: 移除本包登记的 instructions,保留用户配置', 'success');
483
+ // 旧位置:项目根 opencode.json(未升级就卸载的老空间,或迁移残留)
484
+ const legacyPath = path.join(TARGET_DIR, 'opencode.json');
485
+ if (adapter.legacyInstructionsPattern && fs.existsSync(legacyPath)) {
486
+ let legacyCfg;
487
+ try {
488
+ legacyCfg = JSON.parse(fs.readFileSync(legacyPath, 'utf-8'));
489
+ } catch (_) {
490
+ return; // 不可解析的文件不动
491
+ }
492
+ if (Array.isArray(legacyCfg.instructions) && legacyCfg.instructions.includes(adapter.legacyInstructionsPattern)) {
493
+ legacyCfg.instructions = legacyCfg.instructions.filter(p => p !== adapter.legacyInstructionsPattern);
494
+ if (legacyCfg.instructions.length === 0) delete legacyCfg.instructions;
495
+ fs.writeFileSync(legacyPath, JSON.stringify(legacyCfg, null, 2) + '\n');
496
+ log('opencode.json: 移除本包登记的旧版 instructions,保留用户配置', 'success');
497
+ }
368
498
  }
369
499
  });
370
500
  }
@@ -499,13 +629,59 @@ function deployCommandFiles(adapter, srcDir, destDir, prefix, filter) {
499
629
  const srcPath = path.join(srcDir, f);
500
630
  const destPath = path.join(destDir, prefix + f);
501
631
  removePath(destPath);
502
- const content = applyContentRewrites(adapter, fs.readFileSync(srcPath, 'utf-8'));
632
+ const content = rewriteCommandName(adapter, sanitizeCommandFrontmatter(applyContentRewrites(adapter, fs.readFileSync(srcPath, 'utf-8'))));
503
633
  fs.writeFileSync(destPath, content);
504
634
  });
505
635
 
506
636
  return { count: commandFiles.length, names: commandFiles.map(f => prefix + f) };
507
637
  }
508
638
 
639
+ /**
640
+ * 命令 frontmatter 消毒:opencode 新版校验命令配置(description 必须为 string | undefined),
641
+ * 上游个别命令(openspec/commands/update.md)的 description 为空值(YAML 解析为 null),
642
+ * 会导致 opencode 拒绝加载整个项目配置。此处对部署副本回填正文首句作为描述
643
+ * (超过 120 字截断),上游源文件不动(CLI 接管区)。
644
+ */
645
+ function sanitizeCommandFrontmatter(content) {
646
+ const lines = content.split('\n');
647
+ if ((lines[0] || '').trim() !== '---') return content;
648
+ let end = -1;
649
+ for (let i = 1; i < lines.length; i++) {
650
+ if ((lines[i] || '').trim() === '---') { end = i; break; }
651
+ }
652
+ if (end === -1) return content;
653
+ let sentence = '';
654
+ for (let i = end + 1; i < lines.length; i++) {
655
+ const t = (lines[i] || '').trim();
656
+ if (t) { sentence = t; break; }
657
+ }
658
+ let changed = false;
659
+ for (let i = 1; i < end; i++) {
660
+ if (/^description:\s*(null|~)?\s*\r?$/.test(lines[i])) {
661
+ if (sentence) {
662
+ if (sentence.length > 120) sentence = sentence.slice(0, 117) + '...';
663
+ lines[i] = 'description: ' + JSON.stringify(sentence) + (lines[i].endsWith('\r') ? '\r' : '');
664
+ } else {
665
+ lines[i] = null; // 无正文可回填则移除(undefined 同样合法)
666
+ }
667
+ changed = true;
668
+ }
669
+ }
670
+ return changed ? lines.filter(l => l !== null).join('\n') : content;
671
+ }
672
+
673
+ /**
674
+ * opencode 命令显示名取自 frontmatter name,上游为大写空格风格("OPSX: Propose"),
675
+ * 与文档写法(/opsx-propose)不一致;部署副本统一重写为 kebab-case。
676
+ */
677
+ function rewriteCommandName(adapter, content) {
678
+ if (!adapter.commandNameKebab) return content;
679
+ return content.replace(/^name:\s*"?([A-Za-z]+):\s*([^"\r\n]+?)"?\s*\r?$/m, (m0, ns, rest) => {
680
+ const kebab = (ns + '-' + rest).toLowerCase().replace(/\s+/g, '-');
681
+ return `name: "${kebab}"`;
682
+ });
683
+ }
684
+
509
685
  /** 官方 OpenSpec 命令(openspec/commands/ → adapter.commandsDir) */
510
686
  function deployCommands(adapter, isUpgrade, managedPaths, srcDir, filter) {
511
687
  if (!adapter.commandsDir) return { count: 0, names: [] };
@@ -513,13 +689,6 @@ function deployCommands(adapter, isUpgrade, managedPaths, srcDir, filter) {
513
689
  adapter, srcDir, path.join(TARGET_DIR, adapter.commandsDir), adapter.commandPrefix || '', filter);
514
690
  }
515
691
 
516
- /** worktree 管理命令(cbb/worktrees/commands/ → adapter.worktreeCommandsDir) */
517
- function deployWorktreeCommands(adapter, srcDir, filter) {
518
- if (!adapter.worktreeCommandsDir) return { count: 0, names: [] };
519
- return deployCommandFiles(
520
- adapter, srcDir, path.join(TARGET_DIR, adapter.worktreeCommandsDir), adapter.worktreeCommandPrefix || '', filter);
521
- }
522
-
523
692
  // ── 设置 OpenSpec 工作流 ──────────────────────────────────
524
693
 
525
694
  /**
@@ -837,12 +1006,15 @@ async function init(targetDir, options = {}) {
837
1006
  // 选择优先级:调用方预设(cbb setup)> .last-tools(非交互沿用)> 交互选择;无记录才回退全选
838
1007
  const isNonInteractive = yes || !process.stdin.isTTY;
839
1008
  let selectedAdapters;
1009
+ let fromRecord = false; // 选择来自预设或 .last-tools 记录(需落盘归一化后的新工具名)
840
1010
  if (presetAdapters && presetAdapters.length > 0) {
841
1011
  selectedAdapters = presetAdapters;
1012
+ fromRecord = true;
842
1013
  } else if (isNonInteractive) {
843
1014
  const lastTools = readLastTools();
844
1015
  const reused = lastTools ? adapters.filter(a => lastTools.has(a.name)) : [];
845
1016
  selectedAdapters = reused.length > 0 ? reused : adapters;
1017
+ fromRecord = reused.length > 0;
846
1018
  if (yes && process.stdin.isTTY) {
847
1019
  log(reused.length > 0
848
1020
  ? `--yes 非交互模式,沿用上次工具选择:${reused.map(a => a.displayName).join('、')}`
@@ -851,8 +1023,9 @@ async function init(targetDir, options = {}) {
851
1023
  } else {
852
1024
  selectedAdapters = await selectTools(isUpgrade);
853
1025
  }
854
- // 业务空间根记录本次选择,供 cbb update 等非交互场景沿用
855
- if (isWorkspaceRoot && presetAdapters && presetAdapters.length > 0) {
1026
+ // 业务空间根记录本次选择,供 cbb update 等非交互场景沿用;
1027
+ // 沿用旧记录时同样落盘,使历史工具名(codebuddy → workbuddy)归一化自愈
1028
+ if (isWorkspaceRoot && fromRecord) {
856
1029
  saveLastTools(selectedAdapters);
857
1030
  }
858
1031
 
@@ -871,9 +1044,10 @@ async function init(targetDir, options = {}) {
871
1044
  newPaths.add(`${adapter.rulesDir}/cbb-coding-rule.md`);
872
1045
  newPaths.add(`${adapter.rulesDir}/cbb-priority.md`);
873
1046
  newPaths.add(`${adapter.rulesDir}/cbb-ai-behavior.md`);
874
- // opencode:rules glob 登记进项目根 opencode.json instructions
875
- if (adapter.instructionsPattern) {
1047
+ // opencode:rules glob 登记进 .opencode/opencode.json instructions,并迁移根文件旧登记
1048
+ if (adapter.instructionsFile) {
876
1049
  injectInstructions(adapter, newPaths, managedPaths);
1050
+ migrateLegacyInstructions(adapter);
877
1051
  }
878
1052
  });
879
1053
  log(`核心规则 → ${selectedAdapters.map(a => a.rulesDir).join(', ')} (2 个文件)`, 'success');
@@ -883,11 +1057,10 @@ async function init(targetDir, options = {}) {
883
1057
  if (skillsAdapters.length > 0) {
884
1058
  console.log(`\n 🔧 ${action}按需加载 Skills...`);
885
1059
  const standardsSkillsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'dev-standards', 'skills');
886
- const openspecExtendsSkillsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'worktrees', 'skills');
887
1060
  const extendsSkillsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'tools');
888
1061
 
889
1062
  const skillsDests = skillsAdapters.map(a => a.skillsDir);
890
- let stdCount = 0, osCount = 0, extCount = 0;
1063
+ let stdCount = 0, extCount = 0;
891
1064
  skillsAdapters.forEach(adapter => {
892
1065
  // 通用编码规范 Skills(cbb/dev-standards/skills)
893
1066
  const stdResult = deploySkills(adapter, isUpgrade, managedPaths, standardsSkillsSrc);
@@ -897,12 +1070,6 @@ async function init(targetDir, options = {}) {
897
1070
  }
898
1071
  // 官方 OpenSpec Skills(openspec/skills/)不部署——工作流入口统一为 /opsx:* 斜杠命令
899
1072
  //(openspec/commands/ 在下方 Commands 段部署),避免双形态重复
900
- // worktree 扩展 Skills:init / close / push 三件套(单层安装,空间根是唯一安装点)
901
- const osExtResult = deploySkills(adapter, isUpgrade, managedPaths, openspecExtendsSkillsSrc, WORKTREE_SKILLS);
902
- if (osExtResult.count > 0) {
903
- osCount = osExtResult.count;
904
- osExtResult.names.forEach(name => newPaths.add(`${adapter.skillsDir}/${name}`));
905
- }
906
1073
  // 工具 Skills:当前不分发(恢复方式见 TOOLS_SKILLS 注释)
907
1074
  const extResult = deploySkills(adapter, isUpgrade, managedPaths, extendsSkillsSrc, TOOLS_SKILLS);
908
1075
  if (extResult.count > 0) {
@@ -911,9 +1078,22 @@ async function init(targetDir, options = {}) {
911
1078
  }
912
1079
  });
913
1080
  if (stdCount > 0) log(`规范 Skills → ${skillsDests.join(', ')} (${stdCount} 个)`, 'success');
914
- if (osCount > 0) log(`worktree Skills → ${skillsDests.join(', ')} (${osCount} 个)`, 'success');
915
1081
  if (extCount > 0) log(`扩展 Skills → ${skillsDests.join(', ')} (${extCount} 个)`, 'success');
916
1082
 
1083
+ // 部署 worktree 管理 Skills(init / close / push;skill 形态,可被对话自动触发)
1084
+ const worktreeSkillsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'worktrees', 'skills');
1085
+ let wtCount = 0;
1086
+ if (fs.existsSync(worktreeSkillsSrc)) {
1087
+ skillsAdapters.forEach(adapter => {
1088
+ const wtResult = deploySkills(adapter, isUpgrade, managedPaths, worktreeSkillsSrc, WORKTREE_SKILLS);
1089
+ if (wtResult.count > 0) {
1090
+ wtCount = wtResult.count;
1091
+ wtResult.names.forEach(name => newPaths.add(`${adapter.skillsDir}/${name}`));
1092
+ }
1093
+ });
1094
+ if (wtCount > 0) log(`Worktree Skills → ${skillsDests.join(', ')} (${wtCount} 个)`, 'success');
1095
+ }
1096
+
917
1097
  // 部署 Superpowers Skills(白名单:openspec 工作流依赖 + 方法论基底)
918
1098
  const superpowersSkillsSrc = superpowers.SUPERPOWERS_SKILLS_DIR;
919
1099
  let spCount = 0;
@@ -935,18 +1115,16 @@ async function init(targetDir, options = {}) {
935
1115
  await detectUserLevelSuperpowers(selectedAdapters, yes);
936
1116
  }
937
1117
 
938
- // 安装 Commands(官方 11 个 → /opsx:*,onboard 不分发;worktree 管理 → /cbb:worktree-*,init/close/push 三件套)
1118
+ // 安装 Commands(官方 11 个 → /opsx:*,onboard 不分发)
939
1119
  const commandsAdapters = selectedAdapters.filter(a => a.commandsDir);
940
1120
  if (commandsAdapters.length > 0) {
941
1121
  console.log(`\n ⚡ ${action} Commands...`);
942
1122
  const commandsBaseSrc = path.join(SHARED_STANDARDS_DIR, 'openspec', 'commands');
943
- const commandsExtendsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'worktrees', 'commands');
944
- const cmdFilter = ['worktree-init.md', 'worktree-close.md', 'worktree-push.md'];
945
1123
  const openspecCmdInclude = fs.readdirSync(commandsBaseSrc)
946
1124
  .filter(f => f.endsWith('.md') && !OPENSPEC_COMMANDS_EXCLUDE.includes(f));
947
1125
 
948
1126
  const cmdDests = [...new Set(commandsAdapters
949
- .flatMap(a => [a.commandsDir, a.worktreeCommandsDir])
1127
+ .map(a => a.commandsDir)
950
1128
  .filter(Boolean))];
951
1129
  let cmdCount = 0;
952
1130
  commandsAdapters.forEach(adapter => {
@@ -957,12 +1135,6 @@ async function init(targetDir, options = {}) {
957
1135
  adapterCmdCount += baseResult.count;
958
1136
  baseResult.names.forEach(name => newPaths.add(`${adapter.commandsDir}/${name}`));
959
1137
  }
960
- // worktree 管理 Commands(cbb/worktrees/commands/)
961
- const extResult = deployWorktreeCommands(adapter, commandsExtendsSrc, cmdFilter);
962
- if (extResult.count > 0) {
963
- adapterCmdCount += extResult.count;
964
- extResult.names.forEach(name => newPaths.add(`${adapter.worktreeCommandsDir}/${name}`));
965
- }
966
1138
  cmdCount = Math.max(cmdCount, adapterCmdCount);
967
1139
  });
968
1140
  if (cmdCount > 0) log(`Commands → ${cmdDests.join(', ')} (${cmdCount} 个)`, 'success');
@@ -1001,11 +1173,15 @@ async function init(targetDir, options = {}) {
1001
1173
  const settingsPath = adapter.settingsFileIsUserLevel
1002
1174
  ? adapter.settingsFile
1003
1175
  : path.join(TARGET_DIR, adapter.settingsFile);
1004
- settingsManager.addHooks(settingsPath, command, adapter.hookEvents);
1176
+ settingsManager.addHooks(settingsPath, command, adapter.hookEvents, hookOpts(adapter));
1005
1177
  // 清理旧版本遗留的 SessionStart hook(现已仅安装 UserPromptSubmit)
1006
- if (settingsManager.removeLegacyHooks(settingsPath)) {
1178
+ if (settingsManager.removeLegacyHooks(settingsPath, hookOpts(adapter))) {
1007
1179
  log(`清理 ${adapter.displayName} 遗留的 SessionStart hook`, 'success');
1008
1180
  }
1181
+ // 迁移:清理旧用户级 settings(如 ~/.codebuddy → ~/.workbuddy)
1182
+ if (cleanLegacySettings(adapter)) {
1183
+ log(`迁移清理旧用户级配置 ${adapter.legacySettingsFile}`, 'success');
1184
+ }
1009
1185
  if (!adapter.settingsFileIsUserLevel) newPaths.add(adapter.settingsFile);
1010
1186
  });
1011
1187
  log(`版本检查 hook → ${hookAdapters.map(a => a.displayName).join(', ')} (${hookAdapters[0].hookEvents.join(', ')})`, 'success');
@@ -1028,6 +1204,8 @@ async function init(targetDir, options = {}) {
1028
1204
  if (oldPaths.length > MAX_DISPLAY) {
1029
1205
  log(` ... 还有 ${oldPaths.length - MAX_DISPLAY} 个`, 'dim');
1030
1206
  }
1207
+ // 收尾:清掉因此变空的祖先目录(如改名后残留的空 .cbb/skills/)
1208
+ cleanupEmptyParentDirs(oldPaths).forEach(dir => log(` 清理空目录 ${dir}/`, 'dim'));
1031
1209
  }
1032
1210
 
1033
1211
  const kept = protectedKeptPaths(managedPaths, newPaths);
@@ -1098,28 +1276,41 @@ async function init(targetDir, options = {}) {
1098
1276
  // ── cbb setup 命令 ──────────────────────────────────────
1099
1277
 
1100
1278
  /**
1101
- * 若 targetDir 已安装,比较已装版本与当前包版本决定是否继续安装。
1102
- * - 版本一致 → 返回 'skip'(不询问、不重复安装)
1103
- * - 版本不同 → 返回 'install'(setup 已整体确认过,直接升级到当前包版本,不再二次询问)
1104
- * - 未安装 → 返回 'install'(不提示,直接继续)
1279
+ * 若 targetDir 已安装,比较「已装版本 vs 当前包版本」与「本次工具选择 vs .last-tools」
1280
+ * 决定是否继续安装(判定纯逻辑见 setup-decision.js)。
1281
+ * - 版本不同 → 'install'(setup 已整体确认过,直接升级到当前包版本,不再二次询问)
1282
+ * - 版本一致但本次选择含 .last-tools 之外的新工具(或无记录)→ 'install'
1283
+ * (beta.14 及以前的 bug:版本一致即整体跳过,新工具的空间级目录永不安装)
1284
+ * - 版本一致且所选 ⊆ .last-tools → 'skip'(不询问、不重复安装)
1105
1285
  *
1106
1286
  * 注:只比较「已装版本 vs 当前包版本」——安装动作只能落盘当前包的产物,
1107
1287
  * registry 是否有更新由用户级版本检查 hook 提醒,不在此处联网查询。
1288
+ * @param {string} targetDir
1289
+ * @param {Array<{name:string}>} [selectedAdapters] 本次 setup 选中的工具
1108
1290
  * @returns {'install'|'skip'}
1109
1291
  */
1110
- function decideUpgradeIfInstalled(targetDir) {
1292
+ function decideUpgradeIfInstalled(targetDir, selectedAdapters) {
1111
1293
  const installed = version.readInstalledVersion(targetDir);
1112
1294
  if (!installed) return 'install'; // 未安装:不提示,直接继续
1113
1295
 
1114
1296
  const pkg = require(path.join(SHARED_STANDARDS_DIR, 'package.json'));
1297
+ const decision = setupDecision.decideSetupAction({
1298
+ sameVersion: version.compareVersions(installed, pkg.version) === 0,
1299
+ lastTools: normalizeToolSet(setupDecision.readLastToolsFrom(targetDir)),
1300
+ selectedNames: (selectedAdapters || []).map(a => a.name),
1301
+ });
1115
1302
 
1116
- // 版本未变化 → 不处理(不询问、不重复安装)
1117
- if (version.compareVersions(installed, pkg.version) === 0) {
1303
+ if (decision.action === 'skip') {
1118
1304
  step('✓', '版本', `v${installed}(已是最新,跳过)`);
1119
1305
  return 'skip';
1120
1306
  }
1121
-
1122
- step('✓', '版本', `v${installed} → v${pkg.version}`);
1307
+ if (decision.reason === 'new-tools') {
1308
+ step('✓', '版本', `v${installed}(版本一致,新增工具 ${decision.newTools.join('、')} → 继续安装)`);
1309
+ } else if (decision.reason === 'no-tools-record') {
1310
+ step('✓', '版本', `v${installed}(无工具选择记录 → 继续安装)`);
1311
+ } else {
1312
+ step('✓', '版本', `v${installed} → v${pkg.version}`);
1313
+ }
1123
1314
  return 'install';
1124
1315
  }
1125
1316
 
@@ -1410,8 +1601,8 @@ async function initSingleWorkspace(w, opts = {}) {
1410
1601
  console.log(` ${c('gray', `${names} → 编辑 workspace-config.json 填入真实仓库地址`)}`);
1411
1602
  }
1412
1603
 
1413
- // 5. 若已安装:版本未变化直接跳过;有新版本直接升级(setup 已整体确认过,不再二次询问)
1414
- const decision = decideUpgradeIfInstalled(targetDir);
1604
+ // 5. 若已安装:版本一致且无新增工具才跳过;否则继续安装/升级(setup 已整体确认过,不再二次询问)
1605
+ const decision = decideUpgradeIfInstalled(targetDir, opts.selectedAdapters);
1415
1606
  if (decision === 'skip') {
1416
1607
  return { ok: true, subdir: subdirName, upToDate: true };
1417
1608
  }
@@ -1612,8 +1803,9 @@ async function setupUserLevelComponents(targetAdapters = adapters) {
1612
1803
  if (hookAdapters.length > 0) {
1613
1804
  const command = settingsManager.getCheckUpdateCommand();
1614
1805
  hookAdapters.forEach(adapter => {
1615
- settingsManager.addHooks(adapter.settingsFile, command, adapter.hookEvents);
1616
- settingsManager.removeLegacyHooks(adapter.settingsFile);
1806
+ settingsManager.addHooks(adapter.settingsFile, command, adapter.hookEvents, hookOpts(adapter));
1807
+ settingsManager.removeLegacyHooks(adapter.settingsFile, hookOpts(adapter));
1808
+ cleanLegacySettings(adapter);
1617
1809
  });
1618
1810
  summaryParts.push(`版本检查 hook → ${hookAdapters.map(a => a.displayName).join('、')}`);
1619
1811
  }
@@ -1729,9 +1921,12 @@ async function uninstallUserLevelComponents() {
1729
1921
  // 移除版本检查 hook(保留用户其余配置)
1730
1922
  adapters.forEach(adapter => {
1731
1923
  if (!adapter.settingsFile || !adapter.hookEvents) return;
1732
- if (settingsManager.removeHooks(adapter.settingsFile, adapter.hookEvents)) {
1924
+ if (settingsManager.removeHooks(adapter.settingsFile, adapter.hookEvents, hookOpts(adapter))) {
1733
1925
  log(`${adapter.settingsFile}: 移除版本检查 hook,保留用户配置`, 'success');
1734
1926
  }
1927
+ if (cleanLegacySettings(adapter)) {
1928
+ log(`${adapter.legacySettingsFile}: 移除版本检查 hook(旧路径),保留用户配置`, 'success');
1929
+ }
1735
1930
  });
1736
1931
 
1737
1932
  removePath(USER_MANIFEST_FILE);
@@ -1760,7 +1955,7 @@ async function uninstall() {
1760
1955
  // 是否同时卸载用户级组件(工具 skills + 版本检查 hook,作用于 ~/.claude 等用户目录)
1761
1956
  let removeUserLevel = false;
1762
1957
  if (fs.existsSync(USER_MANIFEST_FILE)) {
1763
- const answerUser = await ask('是否同时卸载用户级组件(~/.claude、~/.qoder 的版本检查 hook)?(y/N): ');
1958
+ const answerUser = await ask('是否同时卸载用户级组件(~/.claude、~/.qoder、~/.workbuddy、~/.trae-cn 的版本检查 hook)?(y/N): ');
1764
1959
  removeUserLevel = answerUser.toLowerCase() === 'y' || answerUser.toLowerCase() === 'yes';
1765
1960
  }
1766
1961
 
@@ -1779,11 +1974,11 @@ async function uninstall() {
1779
1974
  : path.join(TARGET_DIR, adapter.settingsFile);
1780
1975
  // 项目级:需在清单中才处理
1781
1976
  if (!adapter.settingsFileIsUserLevel && !managedPaths.has(adapter.settingsFile)) return;
1782
- if (removeUserLevel && settingsManager.removeHooks(settingsPath, adapter.hookEvents)) {
1977
+ if (removeUserLevel && settingsManager.removeHooks(settingsPath, adapter.hookEvents, hookOpts(adapter))) {
1783
1978
  log(`${adapter.settingsFile}: 移除版本检查 hook,保留用户配置`, 'success');
1784
1979
  }
1785
1980
  // 清理旧版本遗留的 SessionStart hook(无版本差异,无条件清理)
1786
- if (settingsManager.removeLegacyHooks(settingsPath)) {
1981
+ if (settingsManager.removeLegacyHooks(settingsPath, hookOpts(adapter))) {
1787
1982
  log(`${adapter.settingsFile}: 清理遗留 SessionStart hook`, 'success');
1788
1983
  }
1789
1984
  skippedPaths.add(adapter.settingsFile);
@@ -1808,22 +2003,7 @@ async function uninstall() {
1808
2003
  });
1809
2004
 
1810
2005
  // 清理可能变空的父目录(从深到浅)
1811
- const dirsToCheck = new Set();
1812
- managedPaths.forEach(relativePath => {
1813
- let dir = path.dirname(relativePath);
1814
- while (dir && dir !== '.') {
1815
- dirsToCheck.add(dir);
1816
- dir = path.dirname(dir);
1817
- }
1818
- });
1819
- // 按深度排序,深层先处理
1820
- [...dirsToCheck].sort((a, b) => b.split('/').length - a.split('/').length).forEach(dir => {
1821
- const fullPath = path.join(TARGET_DIR, dir);
1822
- if (fs.existsSync(fullPath) && fs.readdirSync(fullPath).length === 0) {
1823
- removePath(fullPath);
1824
- log(`清理空目录 ${dir}/`, 'success');
1825
- }
1826
- });
2006
+ cleanupEmptyParentDirs(managedPaths).forEach(dir => log(`清理空目录 ${dir}/`, 'success'));
1827
2007
 
1828
2008
  // 删除清单文件和 .va 目录
1829
2009
  removePath(manifestDir);
@@ -1841,11 +2021,50 @@ async function uninstall() {
1841
2021
  console.log(`\n✅ 卸载完成!共移除 ${removed} 个文件/目录\n`);
1842
2022
  }
1843
2023
 
2024
+ // ── 上游版本检查 ──────────────────────────────────────────────
2025
+
2026
+ /**
2027
+ * 检查 openspec / superpowers 是否有上游新版本(只读,不改动任何文件)。
2028
+ * 供 cbbm check-upstream 调用。
2029
+ */
2030
+ async function checkUpstream() {
2031
+ const results = await Promise.allSettled([
2032
+ openspec.checkUpstream().then(r => ({ name: 'openspec', ...r })),
2033
+ superpowers.checkUpstream().then(r => ({ name: 'superpowers', ...r })),
2034
+ ]);
2035
+
2036
+ let outdated = false;
2037
+ for (const r of results) {
2038
+ if (r.status === 'rejected') {
2039
+ log(`检查失败: ${r.reason.message}`, 'error');
2040
+ continue;
2041
+ }
2042
+ const { name, current, latest, outdated: isOutdated } = r.value;
2043
+ if (!latest) {
2044
+ log(`${name}: 无法获取上游版本`, 'warn');
2045
+ continue;
2046
+ }
2047
+ if (!current) {
2048
+ log(`${name}: 本地无 pin(上游最新 ${latest})`, 'warn');
2049
+ outdated = true;
2050
+ } else if (isOutdated) {
2051
+ log(`${name}: ${current} → ${latest}(有新版本)`, 'warn');
2052
+ outdated = true;
2053
+ } else {
2054
+ log(`${name}: ${current} 已是最新`, 'success');
2055
+ }
2056
+ }
2057
+
2058
+ if (outdated) {
2059
+ log('升级方式:修改对应 .version 的 pin 后,执行 cbbm update --upstream-openspec / --upstream-superpowers', 'info');
2060
+ }
2061
+ }
2062
+
1844
2063
  // ── CLI 入口 ──────────────────────────────────────────────
1845
2064
 
1846
2065
  // 入口身份:cbb(用户命令) / cbbm(内部命令,由流程、hook、Agent 技能调用)
1847
2066
  const ENTRY = process.env.CBB_ENTRY === 'cbbm' ? 'cbbm' : 'cbb';
1848
- const INTERNAL_COMMANDS = ['check-update', 'wiki', 'wp'];
2067
+ const INTERNAL_COMMANDS = ['check-update', 'check-upstream', 'wiki', 'wp'];
1849
2068
  const UPSTREAM_FLAGS = ['--upstream-superpowers', '--upstream-openspec'];
1850
2069
 
1851
2070
  const args = process.argv.slice(2);
@@ -1884,11 +2103,11 @@ switch (command) {
1884
2103
  const upstreamSuperpowers = restArgs.includes('--upstream-superpowers');
1885
2104
  const upstreamOpenspec = restArgs.includes('--upstream-openspec');
1886
2105
  if (upstreamSuperpowers || upstreamOpenspec) {
1887
- // 维护者选项:从 GitHub 拉取上游原生内容到本包源仓库(发布流程用)
2106
+ // 维护者选项:按 .version 的 pin 重新拉取上游原生内容到本包源仓库(发布流程用)
1888
2107
  if (upstreamSuperpowers) {
1889
2108
  superpowers.fetchLatestSkills().then(result => {
1890
2109
  if (result.skipped) {
1891
- log(`superpowers skills 已是最新 (${result.sha.slice(0, 8)})`, 'success');
2110
+ log(`superpowers skills 已是最新 (${result.tag})`, 'success');
1892
2111
  }
1893
2112
  const validation = superpowers.validateSkills();
1894
2113
  if (!validation.valid) {
@@ -1904,22 +2123,13 @@ switch (command) {
1904
2123
  }
1905
2124
  if (upstreamOpenspec) {
1906
2125
  openspec.fetchLatestOpenSpec().then(result => {
1907
- if (!result.skipped) {
1908
- const validation = openspec.validateDeployed();
1909
- if (!validation.valid) {
1910
- log(`部署不完整,缺失: ${validation.missing.join(', ')}`, 'error');
1911
- process.exit(1);
1912
- }
1913
- log(`openspec 工作流完整性校验通过`, 'success');
1914
- } else {
1915
- const validation = openspec.validateDeployed();
1916
- if (!validation.valid) {
1917
- log(`本地 openspec 工作流不完整,请重新运行 cbb update --upstream-openspec:`, 'error');
1918
- for (const m of validation.missing) log(` - 缺失 ${m}`, 'error');
1919
- process.exit(1);
1920
- }
1921
- log(`openspec 工作流完整性校验通过`, 'success');
2126
+ const validation = openspec.validateDeployed();
2127
+ if (!validation.valid) {
2128
+ log(`部署不完整,缺失: ${validation.missing.join(', ')}`, 'error');
2129
+ for (const m of validation.missing) log(` - 缺失 ${m}`, 'error');
2130
+ process.exit(1);
1922
2131
  }
2132
+ log(`openspec 工作流完整性校验通过`, 'success');
1923
2133
  }).catch(err => {
1924
2134
  log(`更新失败: ${err.message}`, 'error');
1925
2135
  process.exit(1);
@@ -1942,6 +2152,12 @@ switch (command) {
1942
2152
  case 'check-update':
1943
2153
  require('../utils/check-update.js');
1944
2154
  break;
2155
+ case 'check-upstream':
2156
+ checkUpstream().catch(err => {
2157
+ log(err.message, 'error');
2158
+ process.exit(1);
2159
+ });
2160
+ break;
1945
2161
  case 'wp':
1946
2162
  require('../wiki/index.js').publish(restArgs);
1947
2163
  break;
@@ -1973,11 +2189,12 @@ function showHelp() {
1973
2189
  @jspg-ai/coding-bb v${pkg.version} — cbbm:内部命令(由流程 / hook / Agent 技能调用)
1974
2190
 
1975
2191
  cbbm check-update 版本检查(用户级 hook 每日自动触发)
2192
+ cbbm check-upstream 检查 openspec / superpowers 是否有上游新版本
1976
2193
  cbbm wiki / cbbm wp Wiki 查询与发布(AI 通过技能调用)
1977
2194
 
1978
- 维护者选项(拉取上游原生内容到本包源仓库):
1979
- cbbm update --upstream-superpowers 从 GitHub 拉取最新 superpowers skills
1980
- cbbm update --upstream-openspec 从 GitHub 拉取最新 OpenSpec 工作流
2195
+ 维护者选项(按 .version 的 pin 重新拉取上游原生内容到本包源仓库):
2196
+ cbbm update --upstream-superpowers 按 pin tag 拉取 superpowers skills(走镜像)
2197
+ cbbm update --upstream-openspec 按 pin 版本从 npmmirror 渲染 OpenSpec 工作流
1981
2198
  `);
1982
2199
  return;
1983
2200
  }