@jspg-ai/coding-bb 0.0.2-beta.29 → 0.0.3-beta.12

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 (107) hide show
  1. package/cbb/bin/cbbm.js +1 -1
  2. package/cbb/dev-standards/rules/cbb-ai-behavior.md +118 -103
  3. package/cbb/lib/install/claude-code.js +1 -3
  4. package/cbb/lib/install/codebuddy.js +30 -0
  5. package/cbb/lib/install/init.js +354 -305
  6. package/cbb/lib/install/opencode.js +35 -31
  7. package/cbb/lib/install/qoder.js +1 -3
  8. package/cbb/lib/install/rule-rewrite.js +27 -0
  9. package/cbb/lib/install/trae.js +33 -0
  10. package/cbb/lib/install/workspaces.js +24 -0
  11. package/cbb/lib/openspec/index.js +337 -554
  12. package/cbb/lib/superpowers/index.js +246 -265
  13. package/cbb/lib/utils/check-update.js +9 -2
  14. package/cbb/lib/utils/gitignore.js +2 -0
  15. package/cbb/lib/utils/settings.js +17 -4
  16. package/cbb/lib/utils/tar.js +92 -0
  17. package/cbb/lib/utils/upstream.js +90 -0
  18. package/cbb/worktrees/skills/cbb-worktree-close/SKILL.md +33 -24
  19. package/cbb/worktrees/skills/cbb-worktree-close/scripts/check-env.js +4 -4
  20. package/cbb/worktrees/skills/cbb-worktree-close/scripts/remove-worktrees.js +2 -2
  21. package/cbb/worktrees/skills/cbb-worktree-init/SKILL.md +61 -78
  22. package/cbb/worktrees/skills/cbb-worktree-init/scripts/check-env-deep.js +3 -33
  23. package/cbb/worktrees/skills/cbb-worktree-init/scripts/check-env.js +5 -14
  24. package/cbb/worktrees/skills/cbb-worktree-init/scripts/create-worktrees.js +15 -37
  25. package/cbb/worktrees/skills/cbb-worktree-init/scripts/update-gitignore.js +25 -9
  26. package/cbb/worktrees/skills/cbb-worktree-push/SKILL.md +67 -31
  27. package/config/openspec/schemas/spec-driven/schema.yaml +29 -30
  28. package/config/openspec/schemas/spec-driven/templates/design.md +0 -18
  29. package/config/openspec/schemas/spec-driven/templates/proposal.md +4 -3
  30. package/config/upstream-mirrors.json +12 -0
  31. package/config/workspace-agents.sample.md +17 -13
  32. package/config/workspaces.json +5 -0
  33. package/openspec/.version +2 -3
  34. package/openspec/commands/apply.md +189 -175
  35. package/openspec/commands/archive.md +237 -216
  36. package/openspec/commands/bulk-archive.md +355 -327
  37. package/openspec/commands/continue.md +116 -105
  38. package/openspec/commands/explore.md +230 -199
  39. package/openspec/commands/ff.md +115 -104
  40. package/openspec/commands/new.md +74 -63
  41. package/openspec/commands/onboard.md +557 -548
  42. package/openspec/commands/propose.md +161 -150
  43. package/openspec/commands/sync.md +277 -249
  44. package/openspec/commands/update.md +92 -80
  45. package/openspec/commands/verify.md +175 -162
  46. package/openspec/skills/openspec-apply-change/SKILL.md +20 -5
  47. package/openspec/skills/openspec-archive-change/SKILL.md +30 -8
  48. package/openspec/skills/openspec-bulk-archive-change/SKILL.md +36 -6
  49. package/openspec/skills/openspec-continue-change/SKILL.md +14 -2
  50. package/openspec/skills/openspec-explore/SKILL.md +21 -9
  51. package/openspec/skills/openspec-ff-change/SKILL.md +14 -2
  52. package/openspec/skills/openspec-new-change/SKILL.md +13 -1
  53. package/openspec/skills/openspec-onboard/SKILL.md +49 -39
  54. package/openspec/skills/openspec-propose/SKILL.md +15 -3
  55. package/openspec/skills/openspec-sync-specs/SKILL.md +31 -2
  56. package/openspec/skills/openspec-update-change/SKILL.md +27 -14
  57. package/openspec/skills/openspec-verify-change/SKILL.md +17 -3
  58. package/package.json +2 -2
  59. package/superpowers/.version +4 -4
  60. package/superpowers/skills/brainstorming/SKILL.md +47 -12
  61. package/superpowers/skills/brainstorming/scripts/frame-template.html +213 -213
  62. package/superpowers/skills/brainstorming/scripts/server.cjs +723 -723
  63. package/superpowers/skills/brainstorming/visual-companion.md +6 -6
  64. package/superpowers/skills/diagnosing-superpowers/SKILL.md +120 -0
  65. package/superpowers/skills/diagnosing-superpowers/prompts/analyst-common.md +38 -0
  66. package/superpowers/skills/diagnosing-superpowers/prompts/cost-and-time.md +28 -0
  67. package/superpowers/skills/diagnosing-superpowers/prompts/plan-adherence.md +29 -0
  68. package/superpowers/skills/diagnosing-superpowers/prompts/quality-evidence.md +26 -0
  69. package/superpowers/skills/diagnosing-superpowers/prompts/repeated-work.md +30 -0
  70. package/superpowers/skills/diagnosing-superpowers/prompts/request-conflicts.md +20 -0
  71. package/superpowers/skills/diagnosing-superpowers/prompts/scrub-audit.md +33 -0
  72. package/superpowers/skills/diagnosing-superpowers/prompts/scrub.md +29 -0
  73. package/superpowers/skills/diagnosing-superpowers/prompts/similar-session.md +38 -0
  74. package/superpowers/skills/diagnosing-superpowers/prompts/skill-timeline.md +30 -0
  75. package/superpowers/skills/diagnosing-superpowers/prompts/stumbles.md +28 -0
  76. package/superpowers/skills/diagnosing-superpowers/references/context-safety.md +22 -0
  77. package/superpowers/skills/diagnosing-superpowers/references/github-issues.md +47 -0
  78. package/superpowers/skills/diagnosing-superpowers/references/redaction-policy.md +34 -0
  79. package/superpowers/skills/diagnosing-superpowers/references/session-discovery.md +31 -0
  80. package/superpowers/skills/diagnosing-superpowers/templates/bundle-README.md +77 -0
  81. package/superpowers/skills/diagnosing-superpowers/templates/case.md +64 -0
  82. package/superpowers/skills/diagnosing-superpowers/templates/issue.md +51 -0
  83. package/superpowers/skills/diagnosing-superpowers/templates/report.md +82 -0
  84. package/superpowers/skills/executing-plans/SKILL.md +350 -41
  85. package/superpowers/skills/executing-plans/scripts/task-done +52 -0
  86. package/superpowers/skills/executing-plans/scripts/task-start +28 -0
  87. package/superpowers/skills/requesting-code-review/SKILL.md +1 -1
  88. package/superpowers/skills/requesting-code-review/code-reviewer.md +17 -0
  89. package/superpowers/skills/subagent-driven-development/SKILL.md +18 -18
  90. package/superpowers/skills/subagent-driven-development/re-review-prompt.md +1 -1
  91. package/superpowers/skills/subagent-driven-development/scripts/review-package +53 -46
  92. package/superpowers/skills/subagent-driven-development/scripts/sdd-workspace +82 -40
  93. package/superpowers/skills/subagent-driven-development/scripts/task-brief +43 -41
  94. package/superpowers/skills/subagent-driven-development/task-reviewer-prompt.md +2 -2
  95. package/superpowers/skills/systematic-debugging/root-cause-tracing.md +1 -1
  96. package/superpowers/skills/test-driven-development/SKILL.md +10 -0
  97. package/superpowers/skills/using-superpowers/SKILL.md +2 -0
  98. package/superpowers/skills/using-superpowers/references/claude-code-tools.md +29 -0
  99. package/superpowers/skills/using-superpowers/references/muse-tools.md +35 -0
  100. package/superpowers/skills/writing-plans/SKILL.md +30 -9
  101. package/superpowers/skills/writing-skills/SKILL.md +4 -2
  102. package/superpowers/skills/writing-skills/graphviz-conventions.dot +171 -171
  103. package/cbb/worktrees/commands/worktree-close.md +0 -64
  104. package/cbb/worktrees/commands/worktree-init.md +0 -55
  105. package/cbb/worktrees/commands/worktree-push.md +0 -42
  106. package/cbb/worktrees/skills/cbb-worktree-init/scripts/auto-open.js +0 -136
  107. package/cbb/worktrees/skills/cbb-worktree-init/scripts/install-ai.js +0 -177
@@ -4,12 +4,13 @@
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/CodeBuddy (.codebuddy/rules/)、TraeWork/TraeCode (.trae/rules/)
8
9
  *
9
10
  * 使用方式:
10
11
  * npm install -g @jspg-ai/coding-bb # 首次使用
11
- * npm install -g @jspg-ai/coding-bb
12
- * cd your-project && cbbm init
12
+ * cbb setup # 初始化业务空间(空间根全量安装)
13
+ * cbb update # 就地升级已安装目录
13
14
  */
14
15
 
15
16
  const fs = require('fs');
@@ -40,7 +41,7 @@ function execCmd(file, args, opts = {}) {
40
41
 
41
42
  const SHARED_STANDARDS_DIR = path.join(__dirname, '..', '..', '..');
42
43
 
43
- // 目标项目根目录:默认取当前 cwd,init() / initWorkspace() 可在调用前覆盖。
44
+ // 目标项目根目录:默认取当前 cwd,init() 可在调用前覆盖。
44
45
  // 注意:使用 let 而非 const 是为了让 cbb setup 能在拉好仓库后把目标目录
45
46
  // 切到仓库根,再复用 init() 的完整安装流程,而无需把 TARGET_DIR 透传到所有 helper。
46
47
  let TARGET_DIR = process.cwd();
@@ -51,14 +52,17 @@ const MANIFEST_FILE = path.join(MANIFEST_DIR, '.managed-by-cbb');
51
52
  const LAST_TOOLS_FILE = path.join(MANIFEST_DIR, '.last-tools');
52
53
 
53
54
  // 适配器
55
+ // 适配器(数组顺序 = 工具选择面板的显示顺序)
54
56
  const adapters = [
55
- require('./claude-code'),
56
- require('./qoder'),
57
57
  require('./opencode'),
58
+ require('./codebuddy'),
59
+ require('./trae'),
60
+ require('./qoder'),
61
+ require('./claude-code'),
58
62
  ];
59
63
 
60
- // superpowers 部署白名单:仅装 openspec 工作流硬依赖 + 方法论基底(源仓库仍保留全量 14 个,
61
- // 供 cbb update --upstream-superpowers 维护;砍掉的与 openspec 工作流重叠或属低频场景)
64
+ // superpowers 部署白名单:仅装 openspec 工作流硬依赖 + 方法论基底(源仓库仍保留全量 15 个,
65
+ // 供 cbbm update --upstream-superpowers 维护;砍掉的与 openspec 工作流重叠或属低频场景)
62
66
  const SUPERPOWERS_WHITELIST = [
63
67
  'writing-plans', // openspec apply:生成 plan.md(schema.yaml 引用)
64
68
  'subagent-driven-development', // openspec apply:执行 plan.md(schema.yaml 引用)
@@ -68,20 +72,33 @@ const SUPERPOWERS_WHITELIST = [
68
72
  'test-driven-development', // TDD 计划结构 + TDD Evidence 方法论基底
69
73
  ];
70
74
 
71
- // worktree 扩展 skills 部署到沙箱的白名单:init 只在 workspace 根执行
72
- // (由 installWorkspaceConfig 部署),沙箱内只需 push(推送)与 close(关闭)
73
- const SANDBOX_WORKTREE_SKILLS = ['cbb-worktree-push', 'cbb-worktree-close'];
75
+ // worktree 管理 skill 清单:init / close / push 三件套(SKILL.md + scripts)。
76
+ // skill 形态部署到各工具的 skillsDir,使这三个能力可被 AI 在对话中自动触发
77
+ // (description 驱动),同时工具也会把它们暴露为斜杠命令供用户手动指定。
78
+ const WORKTREE_SKILLS = ['cbb-worktree-init', 'cbb-worktree-close', 'cbb-worktree-push'];
79
+
80
+ // 装到空间的 tools skills:当前不分发。
81
+ // cbb-design-to-wiki(Wiki 发布)与 cbb-wiki-ops 保留在源仓库,需要时加入本清单即恢复分发;Java 工具不装。
82
+ const TOOLS_SKILLS = [];
74
83
 
75
- // 装到沙箱的 tools skills:仅 cbb-design-to-wiki(openspec 设计阶段动作)。
76
- // cbb-wiki-ops 当前不分发(保留在源仓库,有用户需要时再加入 USER_LEVEL_SKILLS);Java 工具不装。
77
- const SANDBOX_TOOLS_SKILLS = ['cbb-design-to-wiki'];
84
+ // 不随安装分发的官方 OpenSpec 命令(源仓库保留,仍由 CLI 接管升级):
85
+ // onboard 为新手引导教程,团队入门已由文档覆盖;sync archive 流程承重件,保留分发。
86
+ const OPENSPEC_COMMANDS_EXCLUDE = ['onboard.md'];
78
87
 
79
88
  // settings.json hook 工具
80
89
  const settingsManager = require('../utils/settings');
81
90
 
91
+ /** 适配器的 hook 方言参数(Trae 需顶层 version 字段等差异) */
92
+ function hookOpts(adapter) {
93
+ return adapter.hookDialect ? { dialect: adapter.hookDialect } : {};
94
+ }
95
+
82
96
  // 清单驱动的文件清理决策(保护用户文档 CLAUDE.md / AGENTS.md 等)
83
97
  const { pathsToDelete, protectedKeptPaths } = require('./cleanup');
84
98
 
99
+ // 规则 frontmatter 改写(Trae/CodeBuddy 需 alwaysApply: true)
100
+ const { applyRuleRewrites } = require('./rule-rewrite');
101
+
85
102
  // .gitignore 条目决策(纯函数:检测/写入/卸载移除)
86
103
  const gitignore = require('../utils/gitignore');
87
104
 
@@ -109,6 +126,18 @@ function detectExistingInstall() {
109
126
  return fs.existsSync(path.join(TARGET_DIR, MANIFEST_FILE));
110
127
  }
111
128
 
129
+ /**
130
+ * 判定目标目录是否为业务空间根:workspace-config.json 存在且 .git 是目录。
131
+ * 需求工作树内也有 workspace-config.json(tracked 副本),但其 .git 是文件(gitdir 指针),
132
+ * 必须以 .git 形态排除——避免「在 worktree 里误跑 cbb update」被当作空间根
133
+ * (误装 worktree 管理入口、在错误位置报旧导航提示)。
134
+ */
135
+ function detectWorkspaceRoot() {
136
+ if (!fs.existsSync(path.join(TARGET_DIR, 'workspace-config.json'))) return false;
137
+ const gitPath = path.join(TARGET_DIR, '.git');
138
+ return fs.existsSync(gitPath) && fs.statSync(gitPath).isDirectory();
139
+ }
140
+
112
141
  // ── 清单管理 ──────────────────────────────────────────────
113
142
 
114
143
  /** 读取清单,返回 Set<path> */
@@ -193,6 +222,39 @@ function removePath(targetPath) {
193
222
  }
194
223
  }
195
224
 
225
+ /**
226
+ * 清理因删除文件/目录而变空的祖先目录(从深到浅,止于 TARGET_DIR)。
227
+ *
228
+ * 删除清单路径只删到该路径本身,其父目录会留空(如改名后残留的空 `.cbb/skills/`)。
229
+ * 此处统一收尾,升级与卸载共用。仅删除「存在且为空」的目录,非空目录(含用户文件)
230
+ * 一律保留。
231
+ *
232
+ * @param {Iterable<string>} relativePaths 已删除的相对路径(正斜杠)
233
+ * @returns {string[]} 实际删除的空目录相对路径
234
+ */
235
+ function cleanupEmptyParentDirs(relativePaths) {
236
+ const dirsToCheck = new Set();
237
+ for (const relativePath of relativePaths) {
238
+ let dir = path.dirname(relativePath);
239
+ while (dir && dir !== '.') {
240
+ dirsToCheck.add(dir);
241
+ dir = path.dirname(dir);
242
+ }
243
+ }
244
+
245
+ const removed = [];
246
+ [...dirsToCheck]
247
+ .sort((a, b) => b.split('/').length - a.split('/').length)
248
+ .forEach(dir => {
249
+ const fullPath = path.join(TARGET_DIR, dir);
250
+ if (fs.existsSync(fullPath) && fs.readdirSync(fullPath).length === 0) {
251
+ removePath(fullPath);
252
+ removed.push(dir);
253
+ }
254
+ });
255
+ return removed;
256
+ }
257
+
196
258
  /**
197
259
  * 安全删除目标路径(文件或目录)
198
260
  * 在清单中 → 直接删除(本包旧版本)
@@ -290,7 +352,8 @@ function deployCoreRules(adapter, isUpgrade, managedPaths) {
290
352
  // 同名文件存在时直接覆盖
291
353
  removePath(destPath);
292
354
 
293
- fs.writeFileSync(destPath, fs.readFileSync(srcPath, 'utf-8'));
355
+ const content = applyRuleRewrites(adapter, fs.readFileSync(srcPath, 'utf-8'));
356
+ fs.writeFileSync(destPath, content);
294
357
  }
295
358
  });
296
359
 
@@ -299,56 +362,117 @@ function deployCoreRules(adapter, isUpgrade, managedPaths) {
299
362
  // ── opencode instructions 登记 ────────────────────────────────
300
363
 
301
364
  /**
302
- * opencode 无原生 rules 目录:把 .opencode/rules/*.md glob 登记进项目根
303
- * opencode.json 的 instructions(官方支持的加载方式)。
365
+ * opencode 无原生 rules 目录:把 rules glob 登记进 .opencode/opencode.json
366
+ * instructions(opencode V2 支持的配置位置;随 .opencode gitignore,按机器生成)。
367
+ * instructions 相对路径的解析基准(配置文件目录 vs 项目根)文档未写死,
368
+ * 故登记双 glob 对冲:任一基准下恰有一个命中,另一个空匹配。
304
369
  * - 文件不存在:创建(记入清单,卸载时整体删除)
305
- * - 文件存在:仅追加 pattern(幂等);若清单显示该文件本由本包创建,则续登
306
- * newPaths 防止升级清理误删;JSONC(含注释)解析失败则只提示不改写
370
+ * - 文件存在:幂等补齐缺失的 glob(保留用户已有 instructions)
371
+ * - JSONC(含注释)解析失败则只提示不改写
372
+ * 旧版登记在项目根 opencode.json,由 migrateLegacyInstructions 负责迁移清理。
307
373
  */
308
374
  function injectInstructions(adapter, newPaths, managedPaths) {
309
- const cfgPath = path.join(TARGET_DIR, 'opencode.json');
310
- const pattern = adapter.instructionsPattern;
375
+ const cfgPath = path.join(TARGET_DIR, adapter.instructionsFile);
376
+ const patterns = adapter.instructionsPatterns;
377
+ let cfg = {};
378
+ if (fs.existsSync(cfgPath)) {
379
+ try {
380
+ cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
381
+ } catch (_) {
382
+ log(`${adapter.instructionsFile} 解析失败(可能含注释),未自动登记。请手动在 instructions 中加入 "${patterns.join('", "')}"`, 'warn');
383
+ return;
384
+ }
385
+ if (!cfg || typeof cfg !== 'object') cfg = {};
386
+ }
387
+ const list = Array.isArray(cfg.instructions) ? cfg.instructions : [];
388
+ const merged = [...list];
389
+ let added = false;
390
+ patterns.forEach(p => {
391
+ if (!merged.includes(p)) {
392
+ merged.push(p);
393
+ added = true;
394
+ }
395
+ });
311
396
  if (!fs.existsSync(cfgPath)) {
312
- const cfg = { $schema: 'https://opencode.ai/config.json', instructions: [pattern] };
397
+ cfg.instructions = merged;
398
+ if (!cfg.$schema) cfg.$schema = 'https://opencode.ai/config.json';
399
+ fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
313
400
  fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
314
- newPaths.add('opencode.json');
315
- log(`生成 opencode.json 并登记 rules 加载(instructions: ${pattern})`, 'success');
316
- return;
401
+ newPaths.add(adapter.instructionsFile);
402
+ log(`生成 ${adapter.instructionsFile} 并登记 rules 加载(instructions: ${patterns.join(' | ')})`, 'success');
403
+ } else if (added) {
404
+ cfg.instructions = merged;
405
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
406
+ log(`已在 ${adapter.instructionsFile} instructions 中登记 rules glob`, 'success');
317
407
  }
408
+ // 本包此前创建的文件:续登清单,避免升级清理误删
409
+ if (managedPaths.has(adapter.instructionsFile)) newPaths.add(adapter.instructionsFile);
410
+ }
411
+
412
+ /**
413
+ * 旧版(≤0.0.3-beta.2)把 rules glob 登记在项目根 opencode.json;迁移为
414
+ * .opencode/opencode.json 后需把旧 pattern 从根文件摘除(用户其余配置保留)。
415
+ * 根文件若由本包创建(在清单中),升级时由清单差集自动整体删除,此处只摘 pattern。
416
+ */
417
+ function migrateLegacyInstructions(adapter) {
418
+ const legacyPattern = adapter.legacyInstructionsPattern;
419
+ if (!legacyPattern) return;
420
+ const cfgPath = path.join(TARGET_DIR, 'opencode.json');
421
+ if (!fs.existsSync(cfgPath)) return;
318
422
  let cfg;
319
423
  try {
320
424
  cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
321
425
  } catch (_) {
322
- log(`opencode.json 解析失败(可能含注释),未自动登记。请手动在 instructions 中加入 "${pattern}"`, 'warn');
426
+ log('根 opencode.json 解析失败(可能含注释),未自动迁移旧登记。可手动移除 instructions 中的 "' + legacyPattern + '"', 'warn');
323
427
  return;
324
428
  }
325
- const list = Array.isArray(cfg.instructions) ? cfg.instructions : [];
326
- if (!list.includes(pattern)) {
327
- cfg.instructions = [...list, pattern];
429
+ if (Array.isArray(cfg.instructions) && cfg.instructions.includes(legacyPattern)) {
430
+ cfg.instructions = cfg.instructions.filter(p => p !== legacyPattern);
431
+ if (cfg.instructions.length === 0) delete cfg.instructions;
328
432
  fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
329
- log(`已在 opencode.json instructions 中登记 ${pattern}`, 'success');
433
+ log('已从根 opencode.json 移除旧版 rules 登记(迁移至 .opencode/opencode.json)', 'success');
330
434
  }
331
- // 本包此前创建的文件:续登清单,避免升级清理误删
332
- if (managedPaths.has('opencode.json')) newPaths.add('opencode.json');
333
435
  }
334
436
 
335
- /** 卸载时移除本包登记的 instructions pattern(用户创建的 opencode.json 只删 pattern 不删文件) */
437
+ /** 卸载时移除本包登记的 instructions(用户自建配置只摘本包 glob,不删文件) */
336
438
  function removeInstructionsRegistrations() {
337
439
  adapters.forEach(adapter => {
338
- if (!adapter.instructionsPattern) return;
339
- const cfgPath = path.join(TARGET_DIR, 'opencode.json');
340
- if (!fs.existsSync(cfgPath)) return;
341
- let cfg;
342
- try {
343
- cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
344
- } catch (_) {
345
- return; // 不可解析的文件不动
440
+ if (!adapter.instructionsFile) return;
441
+ const patterns = adapter.instructionsPatterns;
442
+ // 新位置:.opencode/opencode.json(cbb 创建的整体文件由清单删除兜底)
443
+ const cfgPath = path.join(TARGET_DIR, adapter.instructionsFile);
444
+ if (fs.existsSync(cfgPath)) {
445
+ let cfg;
446
+ try {
447
+ cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
448
+ } catch (_) {
449
+ cfg = null; // 不可解析的文件不动
450
+ }
451
+ if (cfg && Array.isArray(cfg.instructions)) {
452
+ const filtered = cfg.instructions.filter(p => !patterns.includes(p));
453
+ if (filtered.length !== cfg.instructions.length) {
454
+ if (filtered.length === 0) delete cfg.instructions;
455
+ else cfg.instructions = filtered;
456
+ fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
457
+ log(`${adapter.instructionsFile}: 移除本包登记的 instructions,保留用户配置`, 'success');
458
+ }
459
+ }
346
460
  }
347
- if (Array.isArray(cfg.instructions) && cfg.instructions.includes(adapter.instructionsPattern)) {
348
- cfg.instructions = cfg.instructions.filter(p => p !== adapter.instructionsPattern);
349
- if (cfg.instructions.length === 0) delete cfg.instructions;
350
- fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
351
- log('opencode.json: 移除本包登记的 instructions,保留用户配置', 'success');
461
+ // 旧位置:项目根 opencode.json(未升级就卸载的老空间,或迁移残留)
462
+ const legacyPath = path.join(TARGET_DIR, 'opencode.json');
463
+ if (adapter.legacyInstructionsPattern && fs.existsSync(legacyPath)) {
464
+ let legacyCfg;
465
+ try {
466
+ legacyCfg = JSON.parse(fs.readFileSync(legacyPath, 'utf-8'));
467
+ } catch (_) {
468
+ return; // 不可解析的文件不动
469
+ }
470
+ if (Array.isArray(legacyCfg.instructions) && legacyCfg.instructions.includes(adapter.legacyInstructionsPattern)) {
471
+ legacyCfg.instructions = legacyCfg.instructions.filter(p => p !== adapter.legacyInstructionsPattern);
472
+ if (legacyCfg.instructions.length === 0) delete legacyCfg.instructions;
473
+ fs.writeFileSync(legacyPath, JSON.stringify(legacyCfg, null, 2) + '\n');
474
+ log('opencode.json: 移除本包登记的旧版 instructions,保留用户配置', 'success');
475
+ }
352
476
  }
353
477
  });
354
478
  }
@@ -483,13 +607,59 @@ function deployCommandFiles(adapter, srcDir, destDir, prefix, filter) {
483
607
  const srcPath = path.join(srcDir, f);
484
608
  const destPath = path.join(destDir, prefix + f);
485
609
  removePath(destPath);
486
- const content = applyContentRewrites(adapter, fs.readFileSync(srcPath, 'utf-8'));
610
+ const content = rewriteCommandName(adapter, sanitizeCommandFrontmatter(applyContentRewrites(adapter, fs.readFileSync(srcPath, 'utf-8'))));
487
611
  fs.writeFileSync(destPath, content);
488
612
  });
489
613
 
490
614
  return { count: commandFiles.length, names: commandFiles.map(f => prefix + f) };
491
615
  }
492
616
 
617
+ /**
618
+ * 命令 frontmatter 消毒:opencode 新版校验命令配置(description 必须为 string | undefined),
619
+ * 上游个别命令(openspec/commands/update.md)的 description 为空值(YAML 解析为 null),
620
+ * 会导致 opencode 拒绝加载整个项目配置。此处对部署副本回填正文首句作为描述
621
+ * (超过 120 字截断),上游源文件不动(CLI 接管区)。
622
+ */
623
+ function sanitizeCommandFrontmatter(content) {
624
+ const lines = content.split('\n');
625
+ if ((lines[0] || '').trim() !== '---') return content;
626
+ let end = -1;
627
+ for (let i = 1; i < lines.length; i++) {
628
+ if ((lines[i] || '').trim() === '---') { end = i; break; }
629
+ }
630
+ if (end === -1) return content;
631
+ let sentence = '';
632
+ for (let i = end + 1; i < lines.length; i++) {
633
+ const t = (lines[i] || '').trim();
634
+ if (t) { sentence = t; break; }
635
+ }
636
+ let changed = false;
637
+ for (let i = 1; i < end; i++) {
638
+ if (/^description:\s*(null|~)?\s*\r?$/.test(lines[i])) {
639
+ if (sentence) {
640
+ if (sentence.length > 120) sentence = sentence.slice(0, 117) + '...';
641
+ lines[i] = 'description: ' + JSON.stringify(sentence) + (lines[i].endsWith('\r') ? '\r' : '');
642
+ } else {
643
+ lines[i] = null; // 无正文可回填则移除(undefined 同样合法)
644
+ }
645
+ changed = true;
646
+ }
647
+ }
648
+ return changed ? lines.filter(l => l !== null).join('\n') : content;
649
+ }
650
+
651
+ /**
652
+ * opencode 命令显示名取自 frontmatter name,上游为大写空格风格("OPSX: Propose"),
653
+ * 与文档写法(/opsx-propose)不一致;部署副本统一重写为 kebab-case。
654
+ */
655
+ function rewriteCommandName(adapter, content) {
656
+ if (!adapter.commandNameKebab) return content;
657
+ return content.replace(/^name:\s*"?([A-Za-z]+):\s*([^"\r\n]+?)"?\s*\r?$/m, (m0, ns, rest) => {
658
+ const kebab = (ns + '-' + rest).toLowerCase().replace(/\s+/g, '-');
659
+ return `name: "${kebab}"`;
660
+ });
661
+ }
662
+
493
663
  /** 官方 OpenSpec 命令(openspec/commands/ → adapter.commandsDir) */
494
664
  function deployCommands(adapter, isUpgrade, managedPaths, srcDir, filter) {
495
665
  if (!adapter.commandsDir) return { count: 0, names: [] };
@@ -497,13 +667,6 @@ function deployCommands(adapter, isUpgrade, managedPaths, srcDir, filter) {
497
667
  adapter, srcDir, path.join(TARGET_DIR, adapter.commandsDir), adapter.commandPrefix || '', filter);
498
668
  }
499
669
 
500
- /** worktree 管理命令(cbb/worktrees/commands/ → adapter.worktreeCommandsDir) */
501
- function deployWorktreeCommands(adapter, srcDir, filter) {
502
- if (!adapter.worktreeCommandsDir) return { count: 0, names: [] };
503
- return deployCommandFiles(
504
- adapter, srcDir, path.join(TARGET_DIR, adapter.worktreeCommandsDir), adapter.worktreeCommandPrefix || '', filter);
505
- }
506
-
507
670
  // ── 设置 OpenSpec 工作流 ──────────────────────────────────
508
671
 
509
672
  /**
@@ -801,7 +964,7 @@ async function detectUserLevelSuperpowers(selectedAdapters, yes = false) {
801
964
  // ── 主流程 ────────────────────────────────────────────────
802
965
 
803
966
  async function init(targetDir, options = {}) {
804
- const { yes = false } = options;
967
+ const { yes = false, selectedAdapters: presetAdapters = null } = options;
805
968
 
806
969
  // 允许调用方指定目标目录(cbb setup 在拉好仓库后切到仓库根调用)。
807
970
  // 不传则保持模块级 TARGET_DIR(即调用 init 时的 cwd)。
@@ -812,16 +975,33 @@ async function init(targetDir, options = {}) {
812
975
  const action = isUpgrade ? '升级' : '安装';
813
976
  const managedPaths = readManifest();
814
977
  const newPaths = new Set();
978
+ const isWorkspaceRoot = detectWorkspaceRoot();
815
979
 
816
980
  console.log(`\n 🚀 ${action} @jspg-ai/coding-bb...\n`);
817
981
 
818
982
  // 先选适配器:后续的前置依赖检测仅针对所选适配器,避免误导(如选了 Qoder 却提示安装 Claude Code 的 Superpowers)
819
983
  // 非交互模式判断:显式 -y/--yes 或 stdin 非 TTY(pipe/ignore)均视为非交互
984
+ // 选择优先级:调用方预设(cbb setup)> .last-tools(非交互沿用)> 交互选择;无记录才回退全选
820
985
  const isNonInteractive = yes || !process.stdin.isTTY;
821
- if (yes && process.stdin.isTTY) {
822
- log('--yes 非交互模式,已默认选中所有适配器', 'info');
986
+ let selectedAdapters;
987
+ if (presetAdapters && presetAdapters.length > 0) {
988
+ selectedAdapters = presetAdapters;
989
+ } else if (isNonInteractive) {
990
+ const lastTools = readLastTools();
991
+ const reused = lastTools ? adapters.filter(a => lastTools.has(a.name)) : [];
992
+ selectedAdapters = reused.length > 0 ? reused : adapters;
993
+ if (yes && process.stdin.isTTY) {
994
+ log(reused.length > 0
995
+ ? `--yes 非交互模式,沿用上次工具选择:${reused.map(a => a.displayName).join('、')}`
996
+ : '--yes 非交互模式,无上次选择记录,已默认选中所有适配器', 'info');
997
+ }
998
+ } else {
999
+ selectedAdapters = await selectTools(isUpgrade);
1000
+ }
1001
+ // 业务空间根记录本次选择,供 cbb update 等非交互场景沿用
1002
+ if (isWorkspaceRoot && presetAdapters && presetAdapters.length > 0) {
1003
+ saveLastTools(selectedAdapters);
823
1004
  }
824
- const selectedAdapters = isNonInteractive ? adapters : await selectTools(isUpgrade);
825
1005
 
826
1006
  // 前置依赖检测(放在选适配器之后,只检测与所选工具相关的依赖)
827
1007
  const depProgress = progress('依赖');
@@ -838,9 +1018,10 @@ async function init(targetDir, options = {}) {
838
1018
  newPaths.add(`${adapter.rulesDir}/cbb-coding-rule.md`);
839
1019
  newPaths.add(`${adapter.rulesDir}/cbb-priority.md`);
840
1020
  newPaths.add(`${adapter.rulesDir}/cbb-ai-behavior.md`);
841
- // opencode:rules glob 登记进项目根 opencode.json instructions
842
- if (adapter.instructionsPattern) {
1021
+ // opencode:rules glob 登记进 .opencode/opencode.json instructions,并迁移根文件旧登记
1022
+ if (adapter.instructionsFile) {
843
1023
  injectInstructions(adapter, newPaths, managedPaths);
1024
+ migrateLegacyInstructions(adapter);
844
1025
  }
845
1026
  });
846
1027
  log(`核心规则 → ${selectedAdapters.map(a => a.rulesDir).join(', ')} (2 个文件)`, 'success');
@@ -850,11 +1031,10 @@ async function init(targetDir, options = {}) {
850
1031
  if (skillsAdapters.length > 0) {
851
1032
  console.log(`\n 🔧 ${action}按需加载 Skills...`);
852
1033
  const standardsSkillsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'dev-standards', 'skills');
853
- const openspecExtendsSkillsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'worktrees', 'skills');
854
1034
  const extendsSkillsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'tools');
855
1035
 
856
1036
  const skillsDests = skillsAdapters.map(a => a.skillsDir);
857
- let stdCount = 0, osCount = 0, extCount = 0;
1037
+ let stdCount = 0, extCount = 0;
858
1038
  skillsAdapters.forEach(adapter => {
859
1039
  // 通用编码规范 Skills(cbb/dev-standards/skills)
860
1040
  const stdResult = deploySkills(adapter, isUpgrade, managedPaths, standardsSkillsSrc);
@@ -864,23 +1044,30 @@ async function init(targetDir, options = {}) {
864
1044
  }
865
1045
  // 官方 OpenSpec Skills(openspec/skills/)不部署——工作流入口统一为 /opsx:* 斜杠命令
866
1046
  //(openspec/commands/ 在下方 Commands 段部署),避免双形态重复
867
- // worktree 扩展 Skills:沙箱内只需 push / close(init 由 workspace 根的子集安装)
868
- const osExtResult = deploySkills(adapter, isUpgrade, managedPaths, openspecExtendsSkillsSrc, SANDBOX_WORKTREE_SKILLS);
869
- if (osExtResult.count > 0) {
870
- osCount = osExtResult.count;
871
- osExtResult.names.forEach(name => newPaths.add(`${adapter.skillsDir}/${name}`));
872
- }
873
- // 工具 Skills:仅 cbb-design-to-wiki(设计文档发布属 openspec 设计阶段动作)
874
- const extResult = deploySkills(adapter, isUpgrade, managedPaths, extendsSkillsSrc, SANDBOX_TOOLS_SKILLS);
1047
+ // 工具 Skills:当前不分发(恢复方式见 TOOLS_SKILLS 注释)
1048
+ const extResult = deploySkills(adapter, isUpgrade, managedPaths, extendsSkillsSrc, TOOLS_SKILLS);
875
1049
  if (extResult.count > 0) {
876
1050
  extCount = extResult.count;
877
1051
  extResult.names.forEach(name => newPaths.add(`${adapter.skillsDir}/${name}`));
878
1052
  }
879
1053
  });
880
1054
  if (stdCount > 0) log(`规范 Skills → ${skillsDests.join(', ')} (${stdCount} 个)`, 'success');
881
- if (osCount > 0) log(`worktree Skills → ${skillsDests.join(', ')} (${osCount} 个)`, 'success');
882
1055
  if (extCount > 0) log(`扩展 Skills → ${skillsDests.join(', ')} (${extCount} 个)`, 'success');
883
1056
 
1057
+ // 部署 worktree 管理 Skills(init / close / push;skill 形态,可被对话自动触发)
1058
+ const worktreeSkillsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'worktrees', 'skills');
1059
+ let wtCount = 0;
1060
+ if (fs.existsSync(worktreeSkillsSrc)) {
1061
+ skillsAdapters.forEach(adapter => {
1062
+ const wtResult = deploySkills(adapter, isUpgrade, managedPaths, worktreeSkillsSrc, WORKTREE_SKILLS);
1063
+ if (wtResult.count > 0) {
1064
+ wtCount = wtResult.count;
1065
+ wtResult.names.forEach(name => newPaths.add(`${adapter.skillsDir}/${name}`));
1066
+ }
1067
+ });
1068
+ if (wtCount > 0) log(`Worktree Skills → ${skillsDests.join(', ')} (${wtCount} 个)`, 'success');
1069
+ }
1070
+
884
1071
  // 部署 Superpowers Skills(白名单:openspec 工作流依赖 + 方法论基底)
885
1072
  const superpowersSkillsSrc = superpowers.SUPERPOWERS_SKILLS_DIR;
886
1073
  let spCount = 0;
@@ -902,33 +1089,26 @@ async function init(targetDir, options = {}) {
902
1089
  await detectUserLevelSuperpowers(selectedAdapters, yes);
903
1090
  }
904
1091
 
905
- // 安装 Commands(官方 12 个 → /opsx:*;worktree 管理 → /cbb:worktree-*,
906
- // 仅装 close/push,init 只在 workspace 根使用,由 installWorkspaceConfig 部署)
1092
+ // 安装 Commands(官方 11 个 → /opsx:*,onboard 不分发)
907
1093
  const commandsAdapters = selectedAdapters.filter(a => a.commandsDir);
908
1094
  if (commandsAdapters.length > 0) {
909
1095
  console.log(`\n ⚡ ${action} Commands...`);
910
1096
  const commandsBaseSrc = path.join(SHARED_STANDARDS_DIR, 'openspec', 'commands');
911
- const commandsExtendsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'worktrees', 'commands');
912
- const cmdFilter = ['worktree-close.md', 'worktree-push.md'];
1097
+ const openspecCmdInclude = fs.readdirSync(commandsBaseSrc)
1098
+ .filter(f => f.endsWith('.md') && !OPENSPEC_COMMANDS_EXCLUDE.includes(f));
913
1099
 
914
1100
  const cmdDests = [...new Set(commandsAdapters
915
- .flatMap(a => [a.commandsDir, a.worktreeCommandsDir])
1101
+ .map(a => a.commandsDir)
916
1102
  .filter(Boolean))];
917
1103
  let cmdCount = 0;
918
1104
  commandsAdapters.forEach(adapter => {
919
1105
  let adapterCmdCount = 0;
920
- // 官方 Commands(openspec/commands/)— 始终部署
921
- const baseResult = deployCommands(adapter, isUpgrade, managedPaths, commandsBaseSrc);
1106
+ // 官方 Commands(openspec/commands/)— 部署除不分发清单外的全部
1107
+ const baseResult = deployCommands(adapter, isUpgrade, managedPaths, commandsBaseSrc, openspecCmdInclude);
922
1108
  if (baseResult.count > 0) {
923
1109
  adapterCmdCount += baseResult.count;
924
1110
  baseResult.names.forEach(name => newPaths.add(`${adapter.commandsDir}/${name}`));
925
1111
  }
926
- // worktree 管理 Commands(cbb/worktrees/commands/)
927
- const extResult = deployWorktreeCommands(adapter, commandsExtendsSrc, cmdFilter);
928
- if (extResult.count > 0) {
929
- adapterCmdCount += extResult.count;
930
- extResult.names.forEach(name => newPaths.add(`${adapter.worktreeCommandsDir}/${name}`));
931
- }
932
1112
  cmdCount = Math.max(cmdCount, adapterCmdCount);
933
1113
  });
934
1114
  if (cmdCount > 0) log(`Commands → ${cmdDests.join(', ')} (${cmdCount} 个)`, 'success');
@@ -948,6 +1128,15 @@ async function init(targetDir, options = {}) {
948
1128
  setupOpenSpec(isUpgrade, managedPaths);
949
1129
  newPaths.add('openspec/config.yaml');
950
1130
 
1131
+ // 业务空间根专属:空间导航 AGENTS.md(仅缺失时生成,永不覆盖)+ 旧版导航文案提示
1132
+ let agentsMdState = null;
1133
+ if (isWorkspaceRoot) {
1134
+ agentsMdState = workspaces.ensureWorkspaceAgentsMd(TARGET_DIR);
1135
+ if (workspaces.detectLegacyAgentsMd(TARGET_DIR)) {
1136
+ log('空间根 AGENTS.md 仍是旧版导航文案(单层安装改造前模板)。建议删除该文件后重跑 cbb update 重新生成(仅缺失时生成,不会覆盖你的增补内容)', 'warn');
1137
+ }
1138
+ }
1139
+
951
1140
  // 写入自动版本检查 hook
952
1141
  const hookAdapters = selectedAdapters.filter(a => a.settingsFile && a.hookEvents);
953
1142
  if (hookAdapters.length > 0) {
@@ -958,9 +1147,9 @@ async function init(targetDir, options = {}) {
958
1147
  const settingsPath = adapter.settingsFileIsUserLevel
959
1148
  ? adapter.settingsFile
960
1149
  : path.join(TARGET_DIR, adapter.settingsFile);
961
- settingsManager.addHooks(settingsPath, command, adapter.hookEvents);
1150
+ settingsManager.addHooks(settingsPath, command, adapter.hookEvents, hookOpts(adapter));
962
1151
  // 清理旧版本遗留的 SessionStart hook(现已仅安装 UserPromptSubmit)
963
- if (settingsManager.removeLegacyHooks(settingsPath)) {
1152
+ if (settingsManager.removeLegacyHooks(settingsPath, hookOpts(adapter))) {
964
1153
  log(`清理 ${adapter.displayName} 遗留的 SessionStart hook`, 'success');
965
1154
  }
966
1155
  if (!adapter.settingsFileIsUserLevel) newPaths.add(adapter.settingsFile);
@@ -985,6 +1174,8 @@ async function init(targetDir, options = {}) {
985
1174
  if (oldPaths.length > MAX_DISPLAY) {
986
1175
  log(` ... 还有 ${oldPaths.length - MAX_DISPLAY} 个`, 'dim');
987
1176
  }
1177
+ // 收尾:清掉因此变空的祖先目录(如改名后残留的空 .cbb/skills/)
1178
+ cleanupEmptyParentDirs(oldPaths).forEach(dir => log(` 清理空目录 ${dir}/`, 'dim'));
988
1179
  }
989
1180
 
990
1181
  const kept = protectedKeptPaths(managedPaths, newPaths);
@@ -1015,7 +1206,7 @@ async function init(targetDir, options = {}) {
1015
1206
  console.log(`\n ✅ ${action}完成!\n`);
1016
1207
 
1017
1208
  if (yes) {
1018
- console.log(c('dim', ' (使用 --yes 非交互模式,已默认选中所有适配器)'));
1209
+ console.log(c('dim', ` (使用 --yes 非交互模式,工具:${selectedAdapters.map(a => a.displayName).join('、')})`));
1019
1210
  }
1020
1211
 
1021
1212
  // 适配器映射(单行)
@@ -1045,161 +1236,13 @@ async function init(targetDir, options = {}) {
1045
1236
  if (hasSkills) extras.push('Skills');
1046
1237
  if (hasCommands) extras.push('Commands');
1047
1238
  if (hasHooks) extras.push('版本检查 hook');
1239
+ if (agentsMdState === 'created') extras.push('AGENTS.md(空间导航)');
1048
1240
  if (extras.length > 0) {
1049
1241
  console.log(` ${extras.join(' / ')} 已配置`);
1050
1242
  }
1051
1243
  console.log('');
1052
1244
  }
1053
1245
 
1054
- /**
1055
- * 为 workspace 根目录安装 AI 配置子集(由 cbb setup 调用)。
1056
- *
1057
- * 只装 worktree 生命周期管理(init/close/push)+ openspec 运行时配置 + 空间导航
1058
- * AGENTS.md(仅缺失时生成,永不覆盖),不装 base 工作流、编码规范、superpowers、
1059
- * 版本检查 hook--这些属于 sandbox worktree 内的开发配置,由 worktree 内
1060
- * `cbbm init --yes` 完整模式安装。在 workspace 根目录
1061
- * (主分支)装 base 工作流会诱导绕过 worktree 直接在主分支做需求开发。
1062
- */
1063
- async function installWorkspaceConfig(targetDir, options = {}) {
1064
- const { yes = false, selectedAdapters: presetAdapters = null } = options;
1065
- if (targetDir) TARGET_DIR = targetDir;
1066
-
1067
- const isUpgrade = detectExistingInstall();
1068
- const managedPaths = readManifest();
1069
- const newPaths = new Set();
1070
-
1071
- let selectedAdapters;
1072
- if (presetAdapters && presetAdapters.length > 0) {
1073
- // cbb setup 场景:上层已交互选定适配器,持久化供 cbb update 预选
1074
- selectedAdapters = presetAdapters;
1075
- saveLastTools(selectedAdapters);
1076
- } else {
1077
- const isNonInteractive = yes || !process.stdin.isTTY;
1078
- if (isNonInteractive) {
1079
- // 非交互(cbb update / 管道运行):沿用上次工具选择,避免误装未使用的适配器
1080
- const lastTools = readLastTools();
1081
- const reused = lastTools ? adapters.filter(a => lastTools.has(a.name)) : [];
1082
- if (reused.length > 0) {
1083
- selectedAdapters = reused;
1084
- } else {
1085
- selectedAdapters = adapters;
1086
- }
1087
- } else {
1088
- selectedAdapters = await selectTools(isUpgrade);
1089
- }
1090
- }
1091
- // 只装 worktree 生命周期管理 Skills(init/close/push)
1092
- const skillsAdapters = selectedAdapters.filter(a => a.skillsDir);
1093
- const commandsAdapters = selectedAdapters.filter(a => a.worktreeCommandsDir);
1094
- let skillCount = 0;
1095
- let cmdCount = 0;
1096
- let adapterRoots = [];
1097
-
1098
- // 「AI 配置」阶段心跳:检查依赖 → 部署 skills/commands → 写入 openspec 配置 → 生成导航。
1099
- // 其中 npm 查询/安装最长可达分钟级,必须让用户看到进程仍活着;任何异常先清掉进度行。
1100
- const cfgProgress = progress('AI 配置');
1101
- let agentsMdState = 'exists';
1102
- let openspecRes;
1103
- try {
1104
- await ensureOpenSpecCli(t => cfgProgress.update(t));
1105
-
1106
- if (skillsAdapters.length > 0) {
1107
- cfgProgress.update('部署 skills');
1108
- const openspecExtendsSkillsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'worktrees', 'skills');
1109
- const osExtFilter = ['cbb-worktree-init', 'cbb-worktree-close', 'cbb-worktree-push'];
1110
- skillsAdapters.forEach(adapter => {
1111
- const r = deploySkills(adapter, isUpgrade, managedPaths, openspecExtendsSkillsSrc, osExtFilter);
1112
- if (r.count > 0) {
1113
- skillCount = Math.max(skillCount, r.count);
1114
- r.names.forEach(name => newPaths.add(`${adapter.skillsDir}/${name}`));
1115
- }
1116
- });
1117
- adapterRoots = skillsAdapters.map(a => a.skillsDir.split('/')[0]);
1118
- }
1119
-
1120
- // 只装 worktree 管理 Commands(cbb:worktree-init/close/push)
1121
- if (commandsAdapters.length > 0) {
1122
- cfgProgress.update('部署 worktree 命令');
1123
- const commandsExtendsSrc = path.join(SHARED_STANDARDS_DIR, 'cbb', 'worktrees', 'commands');
1124
- const cmdFilter = ['worktree-init.md', 'worktree-close.md', 'worktree-push.md'];
1125
- commandsAdapters.forEach(adapter => {
1126
- const r = deployWorktreeCommands(adapter, commandsExtendsSrc, cmdFilter);
1127
- if (r.count > 0) {
1128
- cmdCount = Math.max(cmdCount, r.count);
1129
- r.names.forEach(name => newPaths.add(`${adapter.worktreeCommandsDir}/${name}`));
1130
- }
1131
- });
1132
- if (adapterRoots.length === 0) adapterRoots = commandsAdapters.map(a => a.worktreeCommandsDir.split('/')[0]);
1133
- }
1134
-
1135
- // 设置 OpenSpec 工作流(config.yaml + schemas)
1136
- cfgProgress.update('写入 openspec 配置');
1137
- const schemasSrcs = [
1138
- path.join(SHARED_STANDARDS_DIR, 'config', 'openspec', 'schemas'),
1139
- ];
1140
- schemasSrcs.forEach(src => {
1141
- if (fs.existsSync(src)) {
1142
- fs.readdirSync(src).filter(f2 => fs.statSync(path.join(src, f2)).isDirectory())
1143
- .forEach(name => newPaths.add(`openspec/schemas/${name}`));
1144
- }
1145
- });
1146
- openspecRes = setupOpenSpec(isUpgrade, managedPaths);
1147
- newPaths.add('openspec/config.yaml');
1148
-
1149
- // 空间导航文件 AGENTS.md:仅缺失时生成,永不覆盖(不登记清单,卸载不删)
1150
- cfgProgress.update('生成空间导航');
1151
- agentsMdState = workspaces.ensureWorkspaceAgentsMd(TARGET_DIR);
1152
- } finally {
1153
- cfgProgress.done();
1154
- }
1155
-
1156
- // 升级时清理旧路径(workspace 曾装过的 base 工作流等不属于此子集,删除)
1157
- const cleaned = [];
1158
- const keptDocs = [];
1159
- if (isUpgrade) {
1160
- const oldPaths = pathsToDelete(managedPaths, newPaths);
1161
- oldPaths.forEach(oldPath => {
1162
- const targetPath = path.join(TARGET_DIR, oldPath);
1163
- if (fs.existsSync(targetPath)) {
1164
- removePath(targetPath);
1165
- cleaned.push(oldPath);
1166
- }
1167
- });
1168
- keptDocs.push(...protectedKeptPaths(managedPaths, newPaths));
1169
- (skillsAdapters || []).forEach(adapter => {
1170
- const legacyNodeModules = path.join(TARGET_DIR, adapter.skillsDir, 'node_modules');
1171
- if (fs.existsSync(legacyNodeModules)) {
1172
- removePath(legacyNodeModules);
1173
- cleaned.push(`${adapter.skillsDir}/node_modules`);
1174
- }
1175
- });
1176
- }
1177
-
1178
- writeManifest(newPaths, selectedAdapters);
1179
- updateGitignore();
1180
-
1181
- // 汇总输出(一行主结果 + 可选清理/保留明细)
1182
- const rootLabel = [...new Set(adapterRoots)].join('/');
1183
- const parts = [];
1184
- if (skillCount > 0) parts.push(`${rootLabel} skills×${skillCount}`);
1185
- if (cmdCount > 0) parts.push(`commands×${cmdCount}`);
1186
- if (openspecRes.installed.length > 0) {
1187
- parts.push(`openspec ${openspecRes.defaultSchema || openspecRes.installed.join('/')}`);
1188
- }
1189
- if (agentsMdState === 'created') parts.push('AGENTS.md(空间导航)');
1190
- const configLabel = 'AI 配置';
1191
- step('✓', configLabel, parts.join(' · '));
1192
- if (cleaned.length > 0) {
1193
- const MAX_DISPLAY = 3;
1194
- const preview = cleaned.slice(0, MAX_DISPLAY).join('、');
1195
- step('!', '清理残留', `${cleaned.length} 个(${preview}${cleaned.length > MAX_DISPLAY ? ' 等' : ''})`,
1196
- { iconColor: 'yellow' });
1197
- }
1198
- if (keptDocs.length > 0) {
1199
- step('!', '保留文档', `${keptDocs.join('、')}(本包已不再管理)`, { iconColor: 'yellow' });
1200
- }
1201
- }
1202
-
1203
1246
  // ── cbb setup 命令 ──────────────────────────────────────
1204
1247
 
1205
1248
  /**
@@ -1521,9 +1564,9 @@ async function initSingleWorkspace(w, opts = {}) {
1521
1564
  return { ok: true, subdir: subdirName, upToDate: true };
1522
1565
  }
1523
1566
 
1524
- // 6. 安装 workspace AI 配置子集(worktree 管理技能/命令 + openspec 配置,不含 base 工作流)
1567
+ // 6. 安装空间 AI 配置(单层全量:开发配置 + worktree 管理,统一安装器自动识别空间根)
1525
1568
  try {
1526
- await installWorkspaceConfig(targetDir, { yes: true, selectedAdapters: opts.selectedAdapters });
1569
+ await init(targetDir, { yes: true, selectedAdapters: opts.selectedAdapters });
1527
1570
  return { ok: true, subdir: subdirName };
1528
1571
  } catch (e) {
1529
1572
  step('✗', 'AI 配置', e.message, { iconColor: 'red', labelColor: 'red', valueColor: 'red' });
@@ -1717,8 +1760,8 @@ async function setupUserLevelComponents(targetAdapters = adapters) {
1717
1760
  if (hookAdapters.length > 0) {
1718
1761
  const command = settingsManager.getCheckUpdateCommand();
1719
1762
  hookAdapters.forEach(adapter => {
1720
- settingsManager.addHooks(adapter.settingsFile, command, adapter.hookEvents);
1721
- settingsManager.removeLegacyHooks(adapter.settingsFile);
1763
+ settingsManager.addHooks(adapter.settingsFile, command, adapter.hookEvents, hookOpts(adapter));
1764
+ settingsManager.removeLegacyHooks(adapter.settingsFile, hookOpts(adapter));
1722
1765
  });
1723
1766
  summaryParts.push(`版本检查 hook → ${hookAdapters.map(a => a.displayName).join('、')}`);
1724
1767
  }
@@ -1733,13 +1776,16 @@ async function setupUserLevelComponents(targetAdapters = adapters) {
1733
1776
  }
1734
1777
 
1735
1778
  /**
1736
- * cbb update:按现有安装模式升级当前目录内容 + 用户级组件。
1779
+ * cbb update:就地升级当前目录内容 + 用户级组件。
1737
1780
  *
1738
- * 安装模式判定:清单含 rules 路径 → 完整模式(worktree 沙箱/单项目),
1739
- * 否则为 workspace 子集模式(空间根,只装 worktree 管理 + openspec 配置)。
1781
+ * 单层安装:统一安装器按空间根判定(workspace-config.json + .git 目录)补齐
1782
+ * worktree 管理入口与空间导航;旧子集清单升级时由清单差集机制自动补齐全量内容。
1740
1783
  */
1741
1784
  async function updateInstalled() {
1742
- await setupUserLevelComponents();
1785
+ // 用户级组件沿用本目录的工具选择(.last-tools),无记录或为空才回退全量
1786
+ const lastTools = readLastTools();
1787
+ const reused = lastTools ? adapters.filter(a => lastTools.has(a.name)) : [];
1788
+ await setupUserLevelComponents(reused.length > 0 ? reused : adapters);
1743
1789
 
1744
1790
  const manifest = readManifest();
1745
1791
  if (manifest.size === 0) {
@@ -1747,17 +1793,7 @@ async function updateInstalled() {
1747
1793
  process.exit(1);
1748
1794
  }
1749
1795
 
1750
- const isFullInstall = [...manifest].some(p =>
1751
- adapters.some(a => p.startsWith(`${a.rulesDir}/`))
1752
- );
1753
-
1754
- if (isFullInstall) {
1755
- log('检测到完整模式安装,升级开发配置(rules + skills + commands + 工作流)...', 'info');
1756
- await init(undefined, {});
1757
- } else {
1758
- log('检测到 workspace 子集模式,升级 worktree 管理 + openspec 配置...', 'info');
1759
- await installWorkspaceConfig(undefined, {});
1760
- }
1796
+ await init(undefined, {});
1761
1797
  }
1762
1798
 
1763
1799
  // ── 卸载 ──────────────────────────────────────────────────
@@ -1841,7 +1877,7 @@ async function uninstallUserLevelComponents() {
1841
1877
  // 移除版本检查 hook(保留用户其余配置)
1842
1878
  adapters.forEach(adapter => {
1843
1879
  if (!adapter.settingsFile || !adapter.hookEvents) return;
1844
- if (settingsManager.removeHooks(adapter.settingsFile, adapter.hookEvents)) {
1880
+ if (settingsManager.removeHooks(adapter.settingsFile, adapter.hookEvents, hookOpts(adapter))) {
1845
1881
  log(`${adapter.settingsFile}: 移除版本检查 hook,保留用户配置`, 'success');
1846
1882
  }
1847
1883
  });
@@ -1872,7 +1908,7 @@ async function uninstall() {
1872
1908
  // 是否同时卸载用户级组件(工具 skills + 版本检查 hook,作用于 ~/.claude 等用户目录)
1873
1909
  let removeUserLevel = false;
1874
1910
  if (fs.existsSync(USER_MANIFEST_FILE)) {
1875
- const answerUser = await ask('是否同时卸载用户级组件(~/.claude、~/.qoder 的版本检查 hook)?(y/N): ');
1911
+ const answerUser = await ask('是否同时卸载用户级组件(~/.claude、~/.qoder、~/.codebuddy、~/.trae-cn 的版本检查 hook)?(y/N): ');
1876
1912
  removeUserLevel = answerUser.toLowerCase() === 'y' || answerUser.toLowerCase() === 'yes';
1877
1913
  }
1878
1914
 
@@ -1891,11 +1927,11 @@ async function uninstall() {
1891
1927
  : path.join(TARGET_DIR, adapter.settingsFile);
1892
1928
  // 项目级:需在清单中才处理
1893
1929
  if (!adapter.settingsFileIsUserLevel && !managedPaths.has(adapter.settingsFile)) return;
1894
- if (removeUserLevel && settingsManager.removeHooks(settingsPath, adapter.hookEvents)) {
1930
+ if (removeUserLevel && settingsManager.removeHooks(settingsPath, adapter.hookEvents, hookOpts(adapter))) {
1895
1931
  log(`${adapter.settingsFile}: 移除版本检查 hook,保留用户配置`, 'success');
1896
1932
  }
1897
1933
  // 清理旧版本遗留的 SessionStart hook(无版本差异,无条件清理)
1898
- if (settingsManager.removeLegacyHooks(settingsPath)) {
1934
+ if (settingsManager.removeLegacyHooks(settingsPath, hookOpts(adapter))) {
1899
1935
  log(`${adapter.settingsFile}: 清理遗留 SessionStart hook`, 'success');
1900
1936
  }
1901
1937
  skippedPaths.add(adapter.settingsFile);
@@ -1920,22 +1956,7 @@ async function uninstall() {
1920
1956
  });
1921
1957
 
1922
1958
  // 清理可能变空的父目录(从深到浅)
1923
- const dirsToCheck = new Set();
1924
- managedPaths.forEach(relativePath => {
1925
- let dir = path.dirname(relativePath);
1926
- while (dir && dir !== '.') {
1927
- dirsToCheck.add(dir);
1928
- dir = path.dirname(dir);
1929
- }
1930
- });
1931
- // 按深度排序,深层先处理
1932
- [...dirsToCheck].sort((a, b) => b.split('/').length - a.split('/').length).forEach(dir => {
1933
- const fullPath = path.join(TARGET_DIR, dir);
1934
- if (fs.existsSync(fullPath) && fs.readdirSync(fullPath).length === 0) {
1935
- removePath(fullPath);
1936
- log(`清理空目录 ${dir}/`, 'success');
1937
- }
1938
- });
1959
+ cleanupEmptyParentDirs(managedPaths).forEach(dir => log(`清理空目录 ${dir}/`, 'success'));
1939
1960
 
1940
1961
  // 删除清单文件和 .va 目录
1941
1962
  removePath(manifestDir);
@@ -1953,11 +1974,50 @@ async function uninstall() {
1953
1974
  console.log(`\n✅ 卸载完成!共移除 ${removed} 个文件/目录\n`);
1954
1975
  }
1955
1976
 
1977
+ // ── 上游版本检查 ──────────────────────────────────────────────
1978
+
1979
+ /**
1980
+ * 检查 openspec / superpowers 是否有上游新版本(只读,不改动任何文件)。
1981
+ * 供 cbbm check-upstream 调用。
1982
+ */
1983
+ async function checkUpstream() {
1984
+ const results = await Promise.allSettled([
1985
+ openspec.checkUpstream().then(r => ({ name: 'openspec', ...r })),
1986
+ superpowers.checkUpstream().then(r => ({ name: 'superpowers', ...r })),
1987
+ ]);
1988
+
1989
+ let outdated = false;
1990
+ for (const r of results) {
1991
+ if (r.status === 'rejected') {
1992
+ log(`检查失败: ${r.reason.message}`, 'error');
1993
+ continue;
1994
+ }
1995
+ const { name, current, latest, outdated: isOutdated } = r.value;
1996
+ if (!latest) {
1997
+ log(`${name}: 无法获取上游版本`, 'warn');
1998
+ continue;
1999
+ }
2000
+ if (!current) {
2001
+ log(`${name}: 本地无 pin(上游最新 ${latest})`, 'warn');
2002
+ outdated = true;
2003
+ } else if (isOutdated) {
2004
+ log(`${name}: ${current} → ${latest}(有新版本)`, 'warn');
2005
+ outdated = true;
2006
+ } else {
2007
+ log(`${name}: ${current} 已是最新`, 'success');
2008
+ }
2009
+ }
2010
+
2011
+ if (outdated) {
2012
+ log('升级方式:修改对应 .version 的 pin 后,执行 cbbm update --upstream-openspec / --upstream-superpowers', 'info');
2013
+ }
2014
+ }
2015
+
1956
2016
  // ── CLI 入口 ──────────────────────────────────────────────
1957
2017
 
1958
2018
  // 入口身份:cbb(用户命令) / cbbm(内部命令,由流程、hook、Agent 技能调用)
1959
2019
  const ENTRY = process.env.CBB_ENTRY === 'cbbm' ? 'cbbm' : 'cbb';
1960
- const INTERNAL_COMMANDS = ['init', 'check-update', 'wiki', 'wp'];
2020
+ const INTERNAL_COMMANDS = ['check-update', 'check-upstream', 'wiki', 'wp'];
1961
2021
  const UPSTREAM_FLAGS = ['--upstream-superpowers', '--upstream-openspec'];
1962
2022
 
1963
2023
  const args = process.argv.slice(2);
@@ -1996,11 +2056,11 @@ switch (command) {
1996
2056
  const upstreamSuperpowers = restArgs.includes('--upstream-superpowers');
1997
2057
  const upstreamOpenspec = restArgs.includes('--upstream-openspec');
1998
2058
  if (upstreamSuperpowers || upstreamOpenspec) {
1999
- // 维护者选项:从 GitHub 拉取上游原生内容到本包源仓库(发布流程用)
2059
+ // 维护者选项:按 .version 的 pin 重新拉取上游原生内容到本包源仓库(发布流程用)
2000
2060
  if (upstreamSuperpowers) {
2001
2061
  superpowers.fetchLatestSkills().then(result => {
2002
2062
  if (result.skipped) {
2003
- log(`superpowers skills 已是最新 (${result.sha.slice(0, 8)})`, 'success');
2063
+ log(`superpowers skills 已是最新 (${result.tag})`, 'success');
2004
2064
  }
2005
2065
  const validation = superpowers.validateSkills();
2006
2066
  if (!validation.valid) {
@@ -2016,22 +2076,13 @@ switch (command) {
2016
2076
  }
2017
2077
  if (upstreamOpenspec) {
2018
2078
  openspec.fetchLatestOpenSpec().then(result => {
2019
- if (!result.skipped) {
2020
- const validation = openspec.validateDeployed();
2021
- if (!validation.valid) {
2022
- log(`部署不完整,缺失: ${validation.missing.join(', ')}`, 'error');
2023
- process.exit(1);
2024
- }
2025
- log(`openspec 工作流完整性校验通过`, 'success');
2026
- } else {
2027
- const validation = openspec.validateDeployed();
2028
- if (!validation.valid) {
2029
- log(`本地 openspec 工作流不完整,请重新运行 cbb update --upstream-openspec:`, 'error');
2030
- for (const m of validation.missing) log(` - 缺失 ${m}`, 'error');
2031
- process.exit(1);
2032
- }
2033
- log(`openspec 工作流完整性校验通过`, 'success');
2079
+ const validation = openspec.validateDeployed();
2080
+ if (!validation.valid) {
2081
+ log(`部署不完整,缺失: ${validation.missing.join(', ')}`, 'error');
2082
+ for (const m of validation.missing) log(` - 缺失 ${m}`, 'error');
2083
+ process.exit(1);
2034
2084
  }
2085
+ log(`openspec 工作流完整性校验通过`, 'success');
2035
2086
  }).catch(err => {
2036
2087
  log(`更新失败: ${err.message}`, 'error');
2037
2088
  process.exit(1);
@@ -2045,14 +2096,6 @@ switch (command) {
2045
2096
  }
2046
2097
  break;
2047
2098
  }
2048
- case 'init': {
2049
- const yes = restArgs.includes('-y') || restArgs.includes('--yes');
2050
- init(undefined, { yes }).catch(err => {
2051
- log(err.message, 'error');
2052
- process.exit(1);
2053
- });
2054
- break;
2055
- }
2056
2099
  case 'uninstall':
2057
2100
  uninstall().catch(err => {
2058
2101
  log(err.message, 'error');
@@ -2062,6 +2105,12 @@ switch (command) {
2062
2105
  case 'check-update':
2063
2106
  require('../utils/check-update.js');
2064
2107
  break;
2108
+ case 'check-upstream':
2109
+ checkUpstream().catch(err => {
2110
+ log(err.message, 'error');
2111
+ process.exit(1);
2112
+ });
2113
+ break;
2065
2114
  case 'wp':
2066
2115
  require('../wiki/index.js').publish(restArgs);
2067
2116
  break;
@@ -2092,13 +2141,13 @@ function showHelp() {
2092
2141
  console.log(`
2093
2142
  @jspg-ai/coding-bb v${pkg.version} — cbbm:内部命令(由流程 / hook / Agent 技能调用)
2094
2143
 
2095
- cbbm init --yes worktree 沙箱内安装开发配置(/cbb:worktree-init Step 4 自动调用)
2096
2144
  cbbm check-update 版本检查(用户级 hook 每日自动触发)
2145
+ cbbm check-upstream 检查 openspec / superpowers 是否有上游新版本
2097
2146
  cbbm wiki / cbbm wp Wiki 查询与发布(AI 通过技能调用)
2098
2147
 
2099
- 维护者选项(拉取上游原生内容到本包源仓库):
2100
- cbbm update --upstream-superpowers GitHub 拉取最新 superpowers skills
2101
- cbbm update --upstream-openspec GitHub 拉取最新 OpenSpec 工作流
2148
+ 维护者选项(按 .version 的 pin 重新拉取上游原生内容到本包源仓库):
2149
+ cbbm update --upstream-superpowers pin tag 拉取 superpowers skills(走镜像)
2150
+ cbbm update --upstream-openspec pin 版本从 npmmirror 渲染 OpenSpec 工作流
2102
2151
  `);
2103
2152
  return;
2104
2153
  }