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

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 (34) hide show
  1. package/cbb/lib/external/index.js +228 -0
  2. package/cbb/lib/install/init.js +70 -7
  3. package/cbb/worktrees/skills/cbb-local-mode/SKILL.md +1 -1
  4. package/cbb/worktrees/skills/cbb-worktree-init/SKILL.md +1 -1
  5. package/cbb/worktrees/skills/cbb-worktree-push/SKILL.md +32 -40
  6. package/config/upstream-mirrors.json +5 -0
  7. package/external/frontend-design/.version +6 -0
  8. package/external/frontend-design/LICENSE.txt +177 -0
  9. package/external/frontend-design/SKILL.md +71 -0
  10. package/openspec/.version +2 -2
  11. package/openspec/commands/archive.md +15 -8
  12. package/openspec/commands/bulk-archive.md +13 -6
  13. package/openspec/commands/continue.md +0 -1
  14. package/openspec/commands/explore.md +1 -1
  15. package/openspec/commands/ff.md +1 -1
  16. package/openspec/commands/onboard.md +9 -4
  17. package/openspec/commands/update.md +11 -5
  18. package/openspec/commands/verify.md +57 -26
  19. package/openspec/skills/openspec-apply-change/SKILL.md +1 -1
  20. package/openspec/skills/openspec-archive-change/SKILL.md +16 -9
  21. package/openspec/skills/openspec-bulk-archive-change/SKILL.md +14 -7
  22. package/openspec/skills/openspec-continue-change/SKILL.md +1 -2
  23. package/openspec/skills/openspec-explore/SKILL.md +2 -2
  24. package/openspec/skills/openspec-ff-change/SKILL.md +2 -2
  25. package/openspec/skills/openspec-new-change/SKILL.md +1 -1
  26. package/openspec/skills/openspec-onboard/SKILL.md +10 -5
  27. package/openspec/skills/openspec-propose/SKILL.md +1 -1
  28. package/openspec/skills/openspec-sync-specs/SKILL.md +1 -1
  29. package/openspec/skills/openspec-update-change/SKILL.md +12 -6
  30. package/openspec/skills/openspec-verify-change/SKILL.md +59 -27
  31. package/package.json +3 -2
  32. package/superpowers/.version +3 -3
  33. package/superpowers/skills/writing-plans/SKILL.md +32 -20
  34. package/superpowers/skills/writing-plans/plan-document-reviewer-prompt.md +0 -49
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * External 小型外部依赖管理模块
5
+ *
6
+ * 管理顶层 external/ 目录下的零散第三方依赖(当前为 frontend-design skill,
7
+ * 来源 anthropics/skills)。与 superpowers / openspec 的整仓 pin 不同,
8
+ * 这里是「一依赖一目录、pin 文件放依赖自己的子目录内」的轻量形态:
9
+ *
10
+ * external/<dep-name>/
11
+ * ├── .version # pin:{ repo, ref(commit sha), path(tarball 内子路径), updatedAt }
12
+ * └── ... # 上游 path 子目录的内容(如 SKILL.md + LICENSE.txt)
13
+ *
14
+ * 新增外部依赖 = 在 external/ 下新建子目录并写入 .version,无需改本模块。
15
+ *
16
+ * 网络:GitHub 直连可能被墙 / 被本机 DNS 劫持,走 config/upstream-mirrors.json
17
+ * 的 'external' 镜像前缀列表(按顺序尝试,首个成功即用;空字符串 = 直连)。
18
+ *
19
+ * 升级方式:cbbm update --upstream-external [name](按 .version 的 ref 幂等重拉,省略 name = 全部)
20
+ * 版本检查:cbbm check-upstream(对比 pinned sha 与上游 main 该 path 的最新提交)
21
+ */
22
+
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+ const os = require('os');
26
+
27
+ const SHARED_STANDARDS_DIR = path.join(__dirname, '..', '..', '..');
28
+
29
+ // External 依赖根目录
30
+ const EXTERNAL_DIR = path.join(SHARED_STANDARDS_DIR, 'external');
31
+ const VERSION_FILE_NAME = '.version';
32
+ // skill 型依赖的完整性标记(上游 path 内容含 SKILL.md 时以其校验完整性)
33
+ const SKILL_MARKER = 'SKILL.md';
34
+
35
+ const { log } = require('../utils/output');
36
+ const { extractTarGz } = require('../utils/tar');
37
+ const { loadMirrors, fetchWithMirrors } = require('../utils/upstream');
38
+
39
+ // ── 依赖发现与 pin 读取 ──────────────────────────────────────
40
+
41
+ /**
42
+ * 读取依赖目录内的 .version pin。字段不全 / JSON 损坏 / 文件缺失返回 null。
43
+ * @param {string} dir
44
+ * @returns {{repo: string, ref: string, path: string, updatedAt?: string}|null}
45
+ */
46
+ function readPin(dir) {
47
+ const file = path.join(dir, VERSION_FILE_NAME);
48
+ if (!fs.existsSync(file)) return null;
49
+ try {
50
+ const data = JSON.parse(fs.readFileSync(file, 'utf-8'));
51
+ if (!data || typeof data.repo !== 'string' || !data.repo.includes('/') ||
52
+ typeof data.ref !== 'string' || !data.ref ||
53
+ typeof data.path !== 'string' || !data.path) {
54
+ return null;
55
+ }
56
+ return data;
57
+ } catch (_) {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * 扫描 external/ 下各依赖子目录,返回全部依赖(pin 无效的目录跳过并告警)。
64
+ * @returns {Array<{name: string, dir: string, pin: object}>}
65
+ */
66
+ function listDeps() {
67
+ if (!fs.existsSync(EXTERNAL_DIR)) return [];
68
+ const deps = [];
69
+ for (const entry of fs.readdirSync(EXTERNAL_DIR)) {
70
+ const dir = path.join(EXTERNAL_DIR, entry);
71
+ if (!fs.statSync(dir).isDirectory()) continue;
72
+ const pin = readPin(dir);
73
+ if (!pin) {
74
+ log(`external/${entry}: 缺少有效 .version pin({ repo, ref, path }),跳过`, 'warn');
75
+ continue;
76
+ }
77
+ deps.push({ name: entry, dir, pin });
78
+ }
79
+ return deps;
80
+ }
81
+
82
+ /**
83
+ * 依赖内容完整性:skill 型(上游含 SKILL.md)要求本地 SKILL.md 存在;
84
+ * 其余类型要求除 .version 外至少有一个条目。
85
+ * @param {string} dir
86
+ * @returns {boolean}
87
+ */
88
+ function validateDepDir(dir) {
89
+ if (!fs.existsSync(dir)) return false;
90
+ if (fs.existsSync(path.join(dir, SKILL_MARKER))) return true;
91
+ return fs.readdirSync(dir).some(f => f !== VERSION_FILE_NAME);
92
+ }
93
+
94
+ // ── 上游拉取 ──────────────────────────────────────────────────
95
+
96
+ /** tarball 下载地址(按镜像前缀拼接;ref = commit sha,上游无 skill 粒度 tag) */
97
+ function tarballUrl(mirror, pin) {
98
+ return `${mirror}https://api.github.com/repos/${pin.repo}/tarball/${pin.ref}`;
99
+ }
100
+
101
+ /**
102
+ * 按 .version 的 ref 下载 tarball,解压出 pin.path 子目录,内容覆盖到 external/<name>/。
103
+ * 幂等:pin 一致且内容完整 → 跳过重拉。.version 由本模块改写,不被上游内容覆盖。
104
+ * @param {string[]} [names] - 指定依赖名;省略 = 全部
105
+ * @returns {Promise<Array<{name: string, ref: string, skipped: boolean, error?: string}>>}
106
+ */
107
+ async function fetchDeps(names) {
108
+ const all = listDeps();
109
+ let deps = all;
110
+ if (names && names.length > 0) {
111
+ const known = new Set(all.map(d => d.name));
112
+ const unknown = names.filter(n => !known.has(n));
113
+ if (unknown.length > 0) {
114
+ throw new Error(`未知的 external 依赖: ${unknown.join(', ')}(现有: ${[...known].join(', ') || '无'})`);
115
+ }
116
+ deps = all.filter(d => names.includes(d.name));
117
+ }
118
+
119
+ const results = [];
120
+ for (const dep of deps) {
121
+ try {
122
+ results.push(await fetchDep(dep));
123
+ } catch (err) {
124
+ results.push({ name: dep.name, ref: dep.pin.ref, skipped: false, error: err.message });
125
+ }
126
+ }
127
+ return results;
128
+ }
129
+
130
+ /**
131
+ * 拉取单个依赖。
132
+ * @param {{name: string, dir: string, pin: object}} dep
133
+ * @returns {Promise<{name: string, ref: string, skipped: boolean}>}
134
+ */
135
+ async function fetchDep(dep) {
136
+ const { name, dir, pin } = dep;
137
+
138
+ // pin 一致且内容完整 → 幂等跳过
139
+ if (validateDepDir(dir)) {
140
+ log(`external/${name} 已按 pin ${pin.ref.slice(0, 12)} 就位,跳过重拉`, 'success');
141
+ return { name, ref: pin.ref, skipped: true };
142
+ }
143
+ log(`external/${name} 内容不完整,按 pin ${pin.ref.slice(0, 12)} 拉取...`, 'info');
144
+
145
+ const mirrors = loadMirrors('external');
146
+ log(`正在获取 ${pin.repo}@${pin.ref.slice(0, 12)}(${mirrors.map(m => m || '直连').join(' → ')})...`, 'info');
147
+ const { buffer, mirror } = await fetchWithMirrors(mirrors, m => tarballUrl(m, pin));
148
+ log(`已从 ${mirror || '(直连)'} 下载 ${(buffer.length / 1024).toFixed(0)} KB`, 'info');
149
+
150
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cbb-ext-'));
151
+ try {
152
+ extractTarGz(buffer, tmpDir);
153
+
154
+ // GitHub tarball 顶层目录名格式:<owner>-<repo>-<short-sha>
155
+ const topPrefix = `${pin.repo.replace('/', '-')}-`;
156
+ const tops = fs.readdirSync(tmpDir).filter(f => {
157
+ const p = path.join(tmpDir, f);
158
+ return fs.statSync(p).isDirectory() && f.startsWith(topPrefix);
159
+ });
160
+ if (tops.length === 0) throw new Error(`解压后未找到 ${topPrefix}* 目录`);
161
+
162
+ // pin.path 是 tarball 内相对仓库根的子路径(如 skills/frontend-design)
163
+ const source = path.join(tmpDir, tops[0], pin.path);
164
+ if (!fs.existsSync(source)) {
165
+ throw new Error(`上游 ${pin.repo}@${pin.ref.slice(0, 12)} 中不存在路径 ${pin.path}(ref 或 path 配置有误)`);
166
+ }
167
+
168
+ // 覆盖内容到 external/<name>/(cpSync 合并,.version 保留)
169
+ fs.cpSync(source, dir, { recursive: true });
170
+ if (!validateDepDir(dir)) throw new Error('拉取后完整性校验未通过');
171
+
172
+ // 刷新 pin 时间戳(ref 不变)
173
+ fs.writeFileSync(
174
+ path.join(dir, VERSION_FILE_NAME),
175
+ JSON.stringify({ ...pin, updatedAt: new Date().toISOString() }, null, 2) + '\n'
176
+ );
177
+
178
+ log(`external/${name} 已更新: ${pin.repo}@${pin.ref.slice(0, 12)}`, 'success');
179
+ return { name, ref: pin.ref, skipped: false };
180
+ } finally {
181
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) { /* ignore */ }
182
+ }
183
+ }
184
+
185
+ /**
186
+ * 检查全部依赖上游是否有新提交(只读,不改动任何文件)。
187
+ * 对比 pinned sha 与上游 main 分支 pin.path 的最新提交 sha
188
+ * (commits?path= 只统计该子路径的变更,仓库其他目录的提交不告警)。
189
+ * 单依赖失败不阻塞其他依赖(返回 {name, error} 条目)。
190
+ * @returns {Promise<Array<{name: string, current: string, latest: string, outdated: boolean}|{name: string, error: string}>>}
191
+ */
192
+ async function checkUpstream() {
193
+ const deps = listDeps();
194
+ const results = await Promise.allSettled(deps.map(async dep => {
195
+ const mirrors = loadMirrors('external');
196
+ const { buffer } = await fetchWithMirrors(mirrors, m =>
197
+ `${m}https://api.github.com/repos/${dep.pin.repo}/commits?path=${encodeURIComponent(dep.pin.path)}&sha=main&per_page=1`
198
+ );
199
+ const data = JSON.parse(buffer.toString('utf-8'));
200
+ const latest = Array.isArray(data) && data[0] ? data[0].sha : null;
201
+ return {
202
+ name: dep.name,
203
+ current: dep.pin.ref,
204
+ latest,
205
+ outdated: Boolean(latest && latest !== dep.pin.ref),
206
+ };
207
+ }));
208
+ return results.map((r, i) => r.status === 'fulfilled'
209
+ ? r.value
210
+ : { name: deps[i].name, error: r.reason.message });
211
+ }
212
+
213
+ // ── 导出 ──────────────────────────────────────────────────────
214
+
215
+ module.exports = {
216
+ // 常量
217
+ EXTERNAL_DIR,
218
+ VERSION_FILE_NAME,
219
+ SKILL_MARKER,
220
+
221
+ // 核心函数
222
+ listDeps,
223
+ readPin,
224
+ validateDepDir,
225
+ fetchDeps,
226
+ checkUpstream,
227
+ tarballUrl,
228
+ };
@@ -84,6 +84,11 @@ const WORKTREE_SKILLS = ['cbb-worktree-init', 'cbb-worktree-close', 'cbb-worktre
84
84
  // cbb-design-to-wiki(Wiki 发布)与 cbb-wiki-ops 保留在源仓库,需要时加入本清单即恢复分发;Java 工具不装。
85
85
  const TOOLS_SKILLS = [];
86
86
 
87
+ // external 外部依赖 skills 白名单:第三方上游 pin vendor 的 skill(源在 external/<name>/,
88
+ // 各子目录自带 .version pin,供 cbbm update --upstream-external 维护)。
89
+ // 非 skill 类外部依赖不走部署通道,不会进本清单。
90
+ const EXTERNAL_SKILLS = ['frontend-design'];
91
+
87
92
  // 不随安装分发的官方 OpenSpec 命令(源仓库保留,仍由 CLI 接管升级):
88
93
  // onboard 为新手引导教程,团队入门已由文档覆盖;sync 是 archive 流程承重件,保留分发。
89
94
  const OPENSPEC_COMMANDS_EXCLUDE = ['onboard.md'];
@@ -156,6 +161,9 @@ const superpowers = require('../superpowers');
156
161
  // OpenSpec 工作流管理(内置集成 + cbb update --upstream-openspec)
157
162
  const openspec = require('../openspec');
158
163
 
164
+ // External 外部依赖管理(内置集成 + cbbm update --upstream-external)
165
+ const external = require('../external');
166
+
159
167
  // ── 颜色与输出辅助 ──────────────────────────────────────
160
168
 
161
169
  const { COLOR, COLOR_ENABLED, c, log, section, step, padVisual, progress, LOG_STYLES } = require('../utils/output');
@@ -1156,6 +1164,22 @@ async function init(targetDir, options = {}) {
1156
1164
  log('可运行 cbb update --upstream-superpowers 下载最新 skills', 'warn');
1157
1165
  }
1158
1166
 
1167
+ // 部署 external 外部依赖 Skills(第三方上游 pin vendor,白名单控制)
1168
+ const externalSkillsSrc = external.EXTERNAL_DIR;
1169
+ let exCount = 0;
1170
+ if (fs.existsSync(externalSkillsSrc)) {
1171
+ skillsAdapters.forEach(adapter => {
1172
+ const exResult = deploySkills(adapter, isUpgrade, managedPaths, externalSkillsSrc, EXTERNAL_SKILLS);
1173
+ if (exResult.count > 0) {
1174
+ exCount = exResult.count;
1175
+ exResult.names.forEach(name => newPaths.add(`${adapter.skillsDir}/${name}`));
1176
+ }
1177
+ });
1178
+ if (exCount > 0) log(`External Skills → ${skillsDests.join(', ')} (${exCount}/${EXTERNAL_SKILLS.length} 个,白名单)`, 'success');
1179
+ } else {
1180
+ log('external/ 目录不存在,跳过 External Skills 部署', 'warn');
1181
+ }
1182
+
1159
1183
  // 检测用户级 superpowers 安装,避免与项目级重复导致 skill 冲突
1160
1184
  await detectUserLevelSuperpowers(selectedAdapters, yes);
1161
1185
  }
@@ -2164,13 +2188,14 @@ async function uninstall() {
2164
2188
  // ── 上游版本检查 ──────────────────────────────────────────────
2165
2189
 
2166
2190
  /**
2167
- * 检查 openspec / superpowers 是否有上游新版本(只读,不改动任何文件)。
2191
+ * 检查 openspec / superpowers / external 是否有上游新版本(只读,不改动任何文件)。
2168
2192
  * 供 cbbm check-upstream 调用。
2169
2193
  */
2170
2194
  async function checkUpstream() {
2171
2195
  const results = await Promise.allSettled([
2172
2196
  openspec.checkUpstream().then(r => ({ name: 'openspec', ...r })),
2173
2197
  superpowers.checkUpstream().then(r => ({ name: 'superpowers', ...r })),
2198
+ external.checkUpstream().then(list => ({ name: 'external', list })),
2174
2199
  ]);
2175
2200
 
2176
2201
  let outdated = false;
@@ -2179,6 +2204,22 @@ async function checkUpstream() {
2179
2204
  log(`检查失败: ${r.reason.message}`, 'error');
2180
2205
  continue;
2181
2206
  }
2207
+ // external 是多依赖聚合结果,逐依赖渲染
2208
+ if (r.value.name === 'external') {
2209
+ for (const item of r.value.list) {
2210
+ if (item.error) {
2211
+ log(`external/${item.name}: 检查失败: ${item.error}`, 'error');
2212
+ } else if (!item.latest) {
2213
+ log(`external/${item.name}: 无法获取上游版本`, 'warn');
2214
+ } else if (item.outdated) {
2215
+ log(`external/${item.name}: ${item.current.slice(0, 12)} → ${item.latest.slice(0, 12)}(上游有新提交)`, 'warn');
2216
+ outdated = true;
2217
+ } else {
2218
+ log(`external/${item.name}: ${item.current.slice(0, 12)} 已是最新`, 'success');
2219
+ }
2220
+ }
2221
+ continue;
2222
+ }
2182
2223
  const { name, current, latest, outdated: isOutdated } = r.value;
2183
2224
  if (!latest) {
2184
2225
  log(`${name}: 无法获取上游版本`, 'warn');
@@ -2196,7 +2237,7 @@ async function checkUpstream() {
2196
2237
  }
2197
2238
 
2198
2239
  if (outdated) {
2199
- log('升级方式:修改对应 .version 的 pin 后,执行 cbbm update --upstream-openspec / --upstream-superpowers', 'info');
2240
+ log('升级方式:修改对应 .version 的 pin 后,执行 cbbm update --upstream-openspec / --upstream-superpowers / --upstream-external', 'info');
2200
2241
  }
2201
2242
  }
2202
2243
 
@@ -2205,7 +2246,7 @@ async function checkUpstream() {
2205
2246
  // 入口身份:cbb(用户命令) / cbbm(内部命令,由流程、hook、Agent 技能调用)
2206
2247
  const ENTRY = process.env.CBB_ENTRY === 'cbbm' ? 'cbbm' : 'cbb';
2207
2248
  const INTERNAL_COMMANDS = ['check-update', 'check-upstream', 'wiki', 'wp'];
2208
- const UPSTREAM_FLAGS = ['--upstream-superpowers', '--upstream-openspec'];
2249
+ const UPSTREAM_FLAGS = ['--upstream-superpowers', '--upstream-openspec', '--upstream-external'];
2209
2250
 
2210
2251
  const args = process.argv.slice(2);
2211
2252
  const command = args[0];
@@ -2213,11 +2254,11 @@ const restArgs = args.slice(1);
2213
2254
  const hasUpstreamFlag = restArgs.some(a => UPSTREAM_FLAGS.includes(a));
2214
2255
 
2215
2256
  if (ENTRY === 'cbb' && command === 'update' && hasUpstreamFlag) {
2216
- log('维护者拉源选项已迁移到 cbbm:cbbm update --upstream-superpowers / --upstream-openspec', 'error');
2257
+ log('维护者拉源选项已迁移到 cbbm:cbbm update --upstream-superpowers / --upstream-openspec / --upstream-external', 'error');
2217
2258
  process.exit(1);
2218
2259
  }
2219
2260
  if (ENTRY === 'cbbm' && command === 'update' && !hasUpstreamFlag) {
2220
- log('cbbm update 仅用于维护者拉源(--upstream-superpowers / --upstream-openspec)', 'error');
2261
+ log('cbbm update 仅用于维护者拉源(--upstream-superpowers / --upstream-openspec / --upstream-external)', 'error');
2221
2262
  log('升级已安装内容请使用 cbb update', 'warn');
2222
2263
  process.exit(1);
2223
2264
  }
@@ -2242,7 +2283,8 @@ switch (command) {
2242
2283
  case 'update': {
2243
2284
  const upstreamSuperpowers = restArgs.includes('--upstream-superpowers');
2244
2285
  const upstreamOpenspec = restArgs.includes('--upstream-openspec');
2245
- if (upstreamSuperpowers || upstreamOpenspec) {
2286
+ const upstreamExternal = restArgs.includes('--upstream-external');
2287
+ if (upstreamSuperpowers || upstreamOpenspec || upstreamExternal) {
2246
2288
  // 维护者选项:按 .version 的 pin 重新拉取上游原生内容到本包源仓库(发布流程用)
2247
2289
  if (upstreamSuperpowers) {
2248
2290
  superpowers.fetchLatestSkills().then(result => {
@@ -2275,6 +2317,26 @@ switch (command) {
2275
2317
  process.exit(1);
2276
2318
  });
2277
2319
  }
2320
+ if (upstreamExternal) {
2321
+ // 可选依赖名参数:cbbm update --upstream-external [name](省略 = 全部)
2322
+ const extNames = [];
2323
+ for (let i = 0; i < restArgs.length; i++) {
2324
+ if (restArgs[i] === '--upstream-external' && restArgs[i + 1] && !restArgs[i + 1].startsWith('-')) {
2325
+ extNames.push(restArgs[i + 1]);
2326
+ }
2327
+ }
2328
+ external.fetchDeps(extNames).then(results => {
2329
+ const failed = results.filter(r => r.error);
2330
+ if (failed.length > 0) {
2331
+ failed.forEach(r => log(`external/${r.name}: 更新失败: ${r.error}`, 'error'));
2332
+ process.exit(1);
2333
+ }
2334
+ log('external 外部依赖完整性校验通过', 'success');
2335
+ }).catch(err => {
2336
+ log(`更新失败: ${err.message}`, 'error');
2337
+ process.exit(1);
2338
+ });
2339
+ }
2278
2340
  } else {
2279
2341
  updateInstalled().catch(err => {
2280
2342
  log(err.message, 'error');
@@ -2329,12 +2391,13 @@ function showHelp() {
2329
2391
  @jspg-ai/coding-bb v${pkg.version} — cbbm:内部命令(由流程 / hook / Agent 技能调用)
2330
2392
 
2331
2393
  cbbm check-update 版本检查(用户级 hook 每日自动触发)
2332
- cbbm check-upstream 检查 openspec / superpowers 是否有上游新版本
2394
+ cbbm check-upstream 检查 openspec / superpowers / external 是否有上游新版本
2333
2395
  cbbm wiki / cbbm wp Wiki 查询与发布(AI 通过技能调用)
2334
2396
 
2335
2397
  维护者选项(按 .version 的 pin 重新拉取上游原生内容到本包源仓库):
2336
2398
  cbbm update --upstream-superpowers 按 pin tag 拉取 superpowers skills(走镜像)
2337
2399
  cbbm update --upstream-openspec 按 pin 版本从 npmmirror 渲染 OpenSpec 工作流
2400
+ cbbm update --upstream-external [名] 按 pin sha 拉取 external 外部依赖(省略名 = 全部,走镜像)
2338
2401
  `);
2339
2402
  return;
2340
2403
  }
@@ -73,7 +73,7 @@ node "<skill_dir>/scripts/start-local.js" "<ws_root>" "<branch>" --apps "<app1>,
73
73
 
74
74
  ## Step 4:提交推送(转 cbb-worktree-push)
75
75
 
76
- 对话中说"提交并推送"即触发 `cbb-worktree-push`——其仓库发现已支持**本地布局**(根 + 持有同名分支的 `.codespace/<app>/`),dry-run 预览 → 用户确认 commit 信息 → 批量 commit → push → MR 预填链接,全流程与 worktree 模式一致。
76
+ 对话中说"提交并推送"即触发 `cbb-worktree-push`——其仓库发现已支持**本地布局**(根 + 持有同名分支的 `.codespace/<app>/`),dry-run 预览(含 agent 生成的 commit 信息)→ 批量 commit → push → MR 预填链接,全流程与 worktree 模式一致。
77
77
 
78
78
  ## Step 5:收尾(MR 合并后)
79
79
 
@@ -265,7 +265,7 @@ node "<skill_dir>/scripts/create-branches.js" "<ws_root>" "<branch>" [--apps "ap
265
265
 
266
266
  ### 3.4 分支就位(lazy push,不推远端)
267
267
 
268
- **本技能不执行任何远端推送(ADR 0002 lazy push)**:分支创建止步于本地。远端副作用收敛到 `cbb-worktree-push` 唯一出口(dry-run 预览 → 用户确认 commit 信息 → push → MR 链接)——推送类失败(认证/网络/保护分支)在 push 技能内暴露并处理,不在创建流程中阻塞。
268
+ **本技能不执行任何远端推送(ADR 0002 lazy push)**:分支创建止步于本地。远端副作用收敛到 `cbb-worktree-push` 唯一出口(dry-run 预览 → agent 生成 commit 信息并提交 → push → MR 链接)——推送类失败(认证/网络/保护分支)在 push 技能内暴露并处理,不在创建流程中阻塞。
269
269
 
270
270
  > 旧版在本步骤推送空分支建立 upstream(`push-branches.js`),自 0.0.3-beta.22 起移除:避免每个需求在远端留下空分支尸体;`push-worktrees.js` 首次推送时自动建立 upstream,行为等价。
271
271
 
@@ -21,7 +21,7 @@ description: "提交并推送当前需求的所有关联仓库(跨 worktree
21
21
  - **本技能支持两层执行位置**(与 `cbb-worktree-close` 对称):
22
22
  - 工作空间根目录(`WS_ROOT/`)
23
23
  - worktree 内部(`WS_ROOT/.worktrees/worktree-<需求名>/` 或其子目录)
24
- - 输入约定:**自由文本**(commit 信息、需求名等)直接在对话中提问并等待回复;**固定选项多选**(如 "应用统一信息 vs 分别信息")使用 `AskUserQuestion`
24
+ - 输入约定:**自由文本**(需求名等)直接在对话中提问并等待回复;**固定选项多选**使用 `AskUserQuestion`。**例外:commit 信息由 agent 自行生成,不提问、不等待确认**
25
25
 
26
26
  ---
27
27
 
@@ -56,7 +56,7 @@ node "<skill_dir>/scripts/<name>.js" <参数>
56
56
  |------|------|------|
57
57
  | **A. cbb 工作空间根** | cwd(或其 git 顶层)含 `CBB-WORKSPACE.json`;`find-workspace-root.js` 退出码 0 | 走 **Step 1 → 6** 的跨 worktree 批量流程 |
58
58
  | **B. worktree 内** | cwd 命中 `<ws_root>/.worktrees/worktree-<需求名>/` | 用 cwd 反推 `<需求名>`,走 **Step 1 → 6** 批量流程 |
59
- | **C. 普通 git 仓库** | 在 git 仓库内,但无 `CBB-WORKSPACE.json`(`find-workspace-root.js` 退出码 1) | **降级为单仓库提交推送**:不调 worktree 脚本,按常规 `git add -u` → 向用户询问 commit 信息 → `git commit` → `git push`(保留本 skill 的"必问 commit 信息"约束) |
59
+ | **C. 普通 git 仓库** | 在 git 仓库内,但无 `CBB-WORKSPACE.json`(`find-workspace-root.js` 退出码 1) | **降级为单仓库提交推送**:不调 worktree 脚本,按常规 `git add -u` → agent 生成 commit 信息 → `git commit` → `git push`(信息生成规则与 A/B 场景一致:精炼最简、5–50 字符) |
60
60
  | **D. 非 git 目录** | `git rev-parse --is-inside-work-tree` 失败 | 说明"当前目录不是 git 仓库",终止,不做任何操作 |
61
61
 
62
62
  **判场景与定位(A/B 场景):**
@@ -74,9 +74,9 @@ node "<skill_dir>/scripts/find-workspace-root.js"
74
74
  **场景 C 降级流程**(普通仓库误触):
75
75
 
76
76
  1. `git status` 报告改动现状
77
- 2. 直接对话询问 commit 信息(**禁止**自动生成)
77
+ 2. agent 基于改动内容生成 commit 信息(精炼最简,5–50 字符;规则见 Step 3)
78
78
  3. `git add -u`(默认不含未追踪文件;用户明确要求时才 `git add -A`)
79
- 4. `git commit -m "<用户提供的信息>"`
79
+ 4. `git commit -m "<agent 生成的信息>"`
80
80
  5. `git push`(无 upstream 时先询问用户是否 `-u`)
81
81
  6. 汇报结果;**不要**提及 worktree 批量流程
82
82
 
@@ -111,36 +111,27 @@ node "<skill_dir>/scripts/commit-worktrees.js" "<ws_root>" "<branch>" --message
111
111
  - `skipped (skip-repo)`:用户指定跳过
112
112
  - `failed`:错误
113
113
 
114
- > ⓘ **dry-run 必跑**:让用户确认将要 commit 的内容,避免误纳 `.idea/` 等被 git 忽略的文件(虽然 `git add -u` 已天然排除,但 dry-run 仍是最后的兜底确认)。
114
+ > ⓘ **dry-run 必跑**:把将要 commit 的文件清单与 agent 生成的 commit 信息一并展示给用户(透明可见,**不停顿等待回复**),避免误纳 `.idea/` 等被 git 忽略的文件(虽然 `git add -u` 已天然排除,但 dry-run 仍是最后兜底)。
115
115
 
116
116
  ---
117
117
 
118
- ## Step 3:收集 commit 信息(对话中提问)
118
+ ## Step 3:生成 commit 信息(agent 自主)
119
119
 
120
- AI agent 在执行真实 commit 前**必须**在对话中向用户确认 commit 信息。**禁止**自动生成。
120
+ AI agent 在执行 commit 前**自行生成** commit 信息,**不向用户提问、不等待确认**。
121
121
 
122
- **问询模板**(直接对话提问,不用 `AskUserQuestion`):
122
+ **生成规则(硬约束)**:
123
123
 
124
- ```
125
- 检测到以下 worktree 仓库有未提交改动:
126
- - workspace: 5 个文件(CBB-WORKSPACE.json, openspec/changes/foo/...)
127
- - ep-foo-app: 12 个文件
128
- - ep-bar-app: 3 个文件
129
-
130
- 请提供 commit 信息:
131
- [1] 所有仓库用同一条信息(推荐简单场景)
132
- [2] 每仓库分别提供(推荐多仓库改不同东西)
133
-
134
- 回复示例(统一):
135
- feat(<branch>): 实现客户运费逻辑
136
-
137
- 回复示例(分别):
138
- workspace: chore(<branch>): 同步 openspec 标准
139
- ep-foo-app: feat(<branch>): 实现 saveClientExpectFreight
140
- ep-bar-app: refactor(<branch>): 拆分 PlanService
141
- ```
124
+ | 规则 | 要求 |
125
+ |------|------|
126
+ | 长度 | **5–50 字符**(按字符计:1 个中文字 = 1,1 个英文字母 / 空格 / 标点 = 1);超出范围不得提交 |
127
+ | 风格 | 精炼、最简——一句话说清"改了什么",不写缘由、不换行、不堆砌 |
128
+ | 依据 | 以 Step 2 dry-run 的实际改动 + `openspec/changes/<branch>/tasks.md` 已完成 task 为准;禁止凭空编造 |
129
+ | 格式 | 沿用 Conventional Commits 前缀(`feat` / `fix` / `refactor` / `chore` / `docs` 等)+ 需求名,如 `feat(<branch>): 实现客户运费逻辑` |
130
+ | 多仓库 | 各仓库改动语义不同时**逐仓库分别生成**(经 `--per-repo`),禁止硬塞同一条 |
131
+
132
+ **展示时机**:生成的每条信息在 Step 2 dry-run 预览中随待提交文件一并输出(透明可见),但**不停顿等待回复**,直接进入 Step 4。
142
133
 
143
- **自动建议**(仅作辅助):agent 可以从 `openspec/changes/<branch>/tasks.md` 已完成的 task 描述生成建议,但**必须**由用户确认或修改后才能传给脚本。
134
+ > ⓘ 用户若在对话中显式给出 commit 信息,直接采用(不覆盖用户意图);未给出时按上述规则自行生成。
144
135
 
145
136
  ---
146
137
 
@@ -163,7 +154,7 @@ node "<skill_dir>/scripts/commit-worktrees.js" "<ws_root>/.worktrees/worktree-<b
163
154
  **选项**:
164
155
  | 选项 | 说明 |
165
156
  |------|------|
166
- | `--message / -m` | 必填(除非 --per-repo 完全覆盖)。commit 信息 |
157
+ | `--message / -m` | 必填(除非 --per-repo 完全覆盖)。commit 信息(agent 生成,5–50 字符) |
167
158
  | `--per-repo "<name>:msg;<name>:msg"` | 覆盖特定仓库的 commit 信息 |
168
159
  | `--untracked` | 同时 stage 未追踪文件(默认 `git add -u`,加此项时 `git add -A`) |
169
160
  | `--allow-empty` | 允许空提交 |
@@ -283,11 +274,11 @@ node "<skill_dir>/scripts/make-mr-urls.js" "<ws_root>" "<branch>" "<target>" --o
283
274
  |------|----------|
284
275
  | **任何触发** | **先做 Step 0 场景识别**,再决定策略 |
285
276
  | 场景 A:cwd 在 cbb 工作空间根 | find-workspace-root 成功 → 多个 worktree 时让用户选 |
286
- | 场景 B:cwd 在 worktree 内 | 自动用 cwd 反推分支,扫描后进入询问 commit 信息 |
277
+ | 场景 B:cwd 在 worktree 内 | 自动用 cwd 反推分支,扫描后 agent 生成 commit 信息 |
287
278
  | 场景 C:cwd 在普通 git 仓库(无 CBB-WORKSPACE.json) | **降级为单仓库提交推送**,不调用 worktree 脚本 |
288
279
  | 场景 D:cwd 非 git 目录 | 说明情况并终止,不做任何操作 |
289
- | commit 信息缺失 | 必须询问用户;禁止自动生成(可基于 apply task 上下文给建议) |
290
- | 多仓库需不同 commit 信息 | 用 `--per-repo "ep-foo:m1;ep-bar:m2"`,必须由用户逐条提供 |
280
+ | commit 信息 | agent 自行生成(5–50 字符,精炼最简),不询问用户;dry-run 预览时一并展示 |
281
+ | 多仓库改动语义不同 | agent 逐仓库分别生成,用 `--per-repo "ep-foo:msg;ep-bar:msg"` |
291
282
  | 仓库干净 | commit-worktrees 自动 skipped,只 push 不 commit |
292
283
  | 仓库已有 ahead commit | commit-worktrees 自动 skipped(不重复 commit),只 push |
293
284
  | 未追踪文件 | 默认不纳入(避免误纳 .idea);用户明确要求时加 `--untracked` |
@@ -308,10 +299,10 @@ node "<skill_dir>/scripts/make-mr-urls.js" "<ws_root>" "<branch>" "<target>" --o
308
299
  - **问题:** 用户在普通仓库说"提交代码",技能被 description 匹配触发,直接跑 `find-workspace-root.js` 失败并报错终止
309
300
  - **修复:** Step 0 先判场景;场景 C(普通 git 仓库)降级为单仓库提交推送,不调用 worktree 脚本、不提及批量流程
310
301
 
311
- ### 自动生成 commit 信息不让用户确认
302
+ ### commit 信息超长或空洞
312
303
 
313
- - **问题:** agent 拍脑袋写 "feat: xxx",与用户实际意图不符
314
- - **修复:** Step 3 必须**直接对话提问**让用户输入或确认
304
+ - **问题:** agent 生成的 commit 信息过长(罗列全部改动、写缘由)或过于笼统("更新代码"),与"5–50 字符、精炼最简"不符
305
+ - **修复:** Step 3 按规则生成——以实际 diff / 已完成 task 为依据,一句话说清改了什么,长度落在 5–50 字符
315
306
 
316
307
  ### 误纳未追踪文件
317
308
 
@@ -328,10 +319,10 @@ node "<skill_dir>/scripts/make-mr-urls.js" "<ws_root>" "<branch>" "<target>" --o
328
319
  - **问题:** apply 实现完 → 用户关掉 IDE → 调 close-worktree → ahead 提交永久丢失
329
320
  - **修复:** 强烈建议 apply 完成后立即调本 skill 推送;或 close-worktree Step 3.4 兜底
330
321
 
331
- ### 多仓库用同一条 commit 信息
322
+ ### 多仓库硬塞同一条 commit 信息
332
323
 
333
324
  - **问题:** workspace 改了 CBB-WORKSPACE.json,ep-foo-app 改了 Java 代码,硬塞同一条 "feat: 实现 XX" 信息不准确
334
- - **修复:** 用 `--per-repo` 模式,让用户逐仓库提供
325
+ - **修复:** agent 逐仓库生成各自的 commit 信息,用 `--per-repo` 传入
335
326
 
336
327
  ### push 失败时跳过失败仓库直接继续
337
328
 
@@ -349,7 +340,7 @@ node "<skill_dir>/scripts/make-mr-urls.js" "<ws_root>" "<branch>" "<target>" --o
349
340
 
350
341
  **绝不允许**
351
342
 
352
- - 自动生成 commit 信息不让用户确认
343
+ - commit 信息超出 5–50 字符范围 / 空洞无意义 / 与改动不符
353
344
  - 默认 `git add -A` 误纳未追踪文件
354
345
  - push 失败后跳过失败仓库直接继续
355
346
  - 远程分支已存在时脚本自动 set-upstream-to
@@ -363,9 +354,10 @@ node "<skill_dir>/scripts/make-mr-urls.js" "<ws_root>" "<branch>" "<target>" --o
363
354
  - 严格按 Step 0 → 1 → 2 → 3 → 4 → 5 → 6 顺序执行(场景 C/D 除外,按 Step 0 降级策略)
364
355
  - 所有脚本调用以 `node "<skill_dir>/scripts/<name>.js"` 形式发起(`<skill_dir>` = 本 SKILL.md 所在目录)
365
356
  - 执行脚本时 cwd 保持在工作空间根;场景 C 降级时按普通 git 流程
366
- - Step 2 dry-run 必跑,让用户看到将要 commit 的内容
367
- - Step 3 必问用户 commit 信息(自由文本),禁止自动生成
368
- - commit 信息涉及业务语义时(feature/refactor/fix 等)必须从 apply task 上下文生成建议并由用户确认
357
+ - Step 2 dry-run 必跑,把待提交文件清单与 agent 生成的 commit 信息一并展示给用户
358
+ - Step 3 由 agent 自行生成 commit 信息(5–50 字符、精炼最简),不向用户提问、不等待确认;生成结果在 dry-run 预览中展示
359
+ - commit 信息必须依据实际 diff / 已完成 task 生成,沿用 Conventional Commits 前缀(feature/refactor/fix 等语义)
360
+ - 多个仓库改动语义不同时必须逐仓库分别生成(`--per-repo`),禁止硬塞同一条
369
361
  - commit-worktrees / push-worktrees 退出码 2 时必须阻塞流程
370
362
  - 远程分支已存在时 AI 必须先输出 commit 元信息再询问
371
363
  - push 成功后必须走 Step 7 MR 提案(AskUserQuestion 固定选项;用户选"暂不提"则正常结束)
@@ -8,5 +8,10 @@
8
8
  "openspec": [
9
9
  "https://registry.npmmirror.com",
10
10
  "https://registry.npmjs.org"
11
+ ],
12
+ "external": [
13
+ "https://gh-proxy.com/",
14
+ "https://ghfast.top/",
15
+ ""
11
16
  ]
12
17
  }
@@ -0,0 +1,6 @@
1
+ {
2
+ "repo": "anthropics/skills",
3
+ "ref": "41bbe19d1a1a7eaab5e7bb9050a417e5c6cffc8f",
4
+ "path": "skills/frontend-design",
5
+ "updatedAt": "2026-09-28T07:10:02.694Z"
6
+ }