@jspg-ai/coding-bb 0.0.5-beta.2 → 0.0.5-beta.4

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.
@@ -10,7 +10,7 @@ trigger: always_on
10
10
 
11
11
  - 就地切分支:根 + 各涉及应用 `.codespace/<app>/` 同名 `checkout -b`(基线 = `origin/<main>`,经 `cbb-local-mode` 技能执行)
12
12
  - 当前会话原地开发,相对路径原样生效;**一次只承载一个需求**(独占本地检出;期间并行新需求走 worktree 模式,互不干扰)
13
- - 收尾(收尾合并门禁,ADR 0007):`merge-branches.js --check` 判定已合并;未合并须三选一(提 MR / 直合并 `--allow-checkout` / 确认废弃)后方可收尾;各参与仓库 `checkout main && pull && 删分支`
13
+ - 收尾(收尾合并门禁,ADR 0007):`merge-branches.js --check` 判定已合并;未合并须三选一(提 MR / 直合并(临时 worktree 零接触,ADR 0008) / 确认废弃)后方可收尾;交付完成后各参与仓库回主干并删分支(`return-to-trunk.js` + `delete-branches.js`,主干名经远端 HEAD 探测,兼容 main / master)
14
14
 
15
15
  **模式二 · worktree 模式(并行 / 隔离):**
16
16
 
@@ -9,9 +9,17 @@ require('./silence-popup');
9
9
  *
10
10
  * 用法:
11
11
  * node deprecate-branches.js <ws_root> <branch> [--apps a,b]
12
+ * node deprecate-branches.js <ws_root> <branch> [--apps a,b] --check # 仅检查,不重命名
13
+ *
14
+ * 模式:
15
+ * --check 只读检查:逐仓库报远端分支存在性与本地未推送提交数
16
+ * (ls-remote 取远端权威 hash 与本地 <branch> 比对)。
17
+ * 用途:废弃重命名只覆盖远端已推送的部分——本地 ahead 提交
18
+ * 不会被带进 <branch>-deprecated,执行前须提示用户决策。
19
+ * 默认 执行远端重命名 <branch> → <branch>-deprecated
12
20
  *
13
21
  * 退出码:0 全部成功(含 skipped)/ 1 参数错误 / 2 存在失败
14
- * 末行:DEPRECATE_RESULT [...]
22
+ * 末行:DEPRECATE_RESULT [...] 或 DEPRECATE_CHECK_RESULT [...]
15
23
  */
16
24
 
17
25
  const { execSync } = require('child_process');
@@ -44,6 +52,51 @@ function loadApps(wsRoot) {
44
52
  }
45
53
  }
46
54
 
55
+ /**
56
+ * 检查单个仓库(--check,只读):
57
+ * unpushed: 0 = 已全部推送;>0 = 本地领先远端 N 个提交;'all' = 远端无该分支(提交全部未推送)
58
+ */
59
+ function checkRepo(name, repo, branch) {
60
+ const rec = { name, status: 'skipped', remote_exists: false, unpushed: 0, detail: null };
61
+ if (!fs.existsSync(path.join(repo, '.git'))) {
62
+ rec.detail = 'no-repo';
63
+ return rec;
64
+ }
65
+ const remote = run('git remote', repo);
66
+ if (!remote.ok || !remote.out) {
67
+ rec.detail = 'no-remote';
68
+ return rec;
69
+ }
70
+ const r = remote.out.split(/\r?\n/)[0];
71
+ if (!run('git rev-parse --verify --quiet "refs/heads/' + branch + '"', repo).ok) {
72
+ rec.detail = 'no-local-branch';
73
+ return rec;
74
+ }
75
+ const ls = run('git ls-remote --heads "' + r + '" "refs/heads/' + branch + '"', repo);
76
+ if (!ls.ok) {
77
+ rec.status = 'failed';
78
+ rec.detail = 'ls-remote-failed';
79
+ return rec;
80
+ }
81
+ if (!ls.out) {
82
+ rec.status = 'ok';
83
+ rec.unpushed = 'all';
84
+ rec.detail = 'remote-has-no-branch';
85
+ return rec;
86
+ }
87
+ const remoteHash = ls.out.split(/\r?\n/)[0].split(/\s+/)[0];
88
+ const cnt = run('git rev-list --count "' + remoteHash + '..refs/heads/' + branch + '"', repo);
89
+ if (!cnt.ok) {
90
+ rec.status = 'failed';
91
+ rec.detail = 'rev-list-failed';
92
+ return rec;
93
+ }
94
+ rec.status = 'ok';
95
+ rec.remote_exists = true;
96
+ rec.unpushed = parseInt(cnt.out, 10) || 0;
97
+ return rec;
98
+ }
99
+
47
100
  /** 对单个仓库执行废弃重命名 */
48
101
  function deprecateRepo(name, repo, branch) {
49
102
  const newName = branch + '-deprecated';
@@ -95,6 +148,7 @@ function main() {
95
148
  const appFilter = appsIdx >= 0
96
149
  ? new Set((args[appsIdx + 1] || '').split(',').map(s => s.trim()).filter(Boolean))
97
150
  : null;
151
+ const checkOnly = args.includes('--check');
98
152
 
99
153
  const repos = [{ name: 'workspace', repo: wsRoot }];
100
154
  for (const app of loadApps(wsRoot)) {
@@ -102,6 +156,17 @@ function main() {
102
156
  repos.push({ name: app.name, repo: path.join(wsRoot, '.codespace', app.name) });
103
157
  }
104
158
 
159
+ if (checkOnly) {
160
+ const results = repos.map(rp => checkRepo(rp.name, rp.repo, branch));
161
+ results.forEach(r => {
162
+ const up = r.status === 'ok' && r.unpushed !== 0 ? ' [unpushed: ' + r.unpushed + ']' : '';
163
+ console.log(r.status.toUpperCase() + ' ' + r.name + (r.detail ? ' — ' + r.detail : '') + up);
164
+ });
165
+ console.log('---');
166
+ console.log('DEPRECATE_CHECK_RESULT', JSON.stringify({ branch, results }));
167
+ process.exit(results.some(r => r.status === 'failed') ? 2 : 0);
168
+ }
169
+
105
170
  const results = repos.map(rp => deprecateRepo(rp.name, rp.repo, branch));
106
171
 
107
172
  results.forEach(r => {
@@ -116,4 +181,4 @@ function main() {
116
181
 
117
182
  if (require.main === module) main();
118
183
 
119
- module.exports = { run, loadApps, deprecateRepo };
184
+ module.exports = { run, loadApps, checkRepo, deprecateRepo };
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env node
2
+ require('./silence-popup');
3
+ /**
4
+ * 收尾回主干 —— 交付完成后把各参与仓库的本地检出切回主干并追平(ADR 0007 / 0008)
5
+ *
6
+ * 背景:merge-branches.js 统一零接触通道后,合并在临时 worktree 内完成、
7
+ * 不触碰本地检出(本地主干检出不随交付前移)。本地模式收尾需把各仓库
8
+ * 本地检出从需求分支切回主干——本脚本替代曾经写死 `git checkout main`
9
+ * 的裸命令:主干名逐仓库经远端 HEAD 探测(与 check-local-state.js 同源,
10
+ * 兼容 master 等),不再假设主干叫 main。
11
+ *
12
+ * 用法:
13
+ * node return-to-trunk.js <ws_root> [--apps a,b] [--dry-run]
14
+ *
15
+ * 行为(逐仓库,相互独立、失败不停链):
16
+ * 1. 目录无 .git → SKIP no-repo
17
+ * 2. git fetch --quiet origin(失败仅降级,后续判定退回本地主干)
18
+ * 3. 主干名 detectMain:远端 HEAD → 本地 main → 本地 master → origin/main → origin/master
19
+ * → FAIL cannot-detect-main
20
+ * 4. 当前检出 ≠ 主干:
21
+ * - 检出 dirty(含未跟踪)→ FAIL dirty-worktree(列未提交项数;不自动 stash,交用户决策)
22
+ * - 本地有主干分支 → git checkout <主干>;仅远端有 → git checkout -b <主干> --track origin/<主干>
23
+ * 5. 追平远端:origin/<主干> 存在 → git pull --ff-only origin <主干>
24
+ * (成功详情注记 `pulled origin/<主干>` / `up-to-date`;本地主干与 origin 分叉 → FAIL
25
+ * pull-failed,交用户决策;远端无主干 → 注记跳过)
26
+ * 6. 已在主干 → 跳过切回,仍追平(幂等)
27
+ *
28
+ * 退出码:0 全部成功(含 no-repo / 已在主干)/ 1 参数错误 / 2 任一仓库失败
29
+ * 末行:TRUNK_RESULT [...]
30
+ */
31
+
32
+ const { execSync } = require('child_process');
33
+ const fs = require('fs');
34
+ const path = require('path');
35
+
36
+ function run(cmd, cwd) {
37
+ try {
38
+ return {
39
+ ok: true,
40
+ out: execSync(cmd, {
41
+ cwd,
42
+ encoding: 'utf-8',
43
+ stdio: ['ignore', 'pipe', 'pipe'],
44
+ timeout: 120000,
45
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
46
+ }).trim(),
47
+ };
48
+ } catch (e) {
49
+ return { ok: false, err: (e.stderr || e.message || String(e)).trim() };
50
+ }
51
+ }
52
+
53
+ function hasRef(repoPath, ref) {
54
+ return run('git rev-parse --verify --quiet "' + ref + '"', repoPath).ok;
55
+ }
56
+
57
+ function loadApps(wsRoot) {
58
+ try {
59
+ const config = JSON.parse(fs.readFileSync(path.join(wsRoot, 'CBB-WORKSPACE.json'), 'utf-8'));
60
+ return Array.isArray(config.apps) ? config.apps : [];
61
+ } catch (e) {
62
+ return [];
63
+ }
64
+ }
65
+
66
+ /** 主干名:symbolic-ref 远端 HEAD → 本地 main/master → 远端跟踪 origin/main|master → 空串(与 check-local-state.js 同源,回退链更强) */
67
+ function detectMain(repoPath) {
68
+ const remote = run('git remote', repoPath);
69
+ if (remote.ok && remote.out) {
70
+ const r = remote.out.split(/\r?\n/)[0];
71
+ const symbolic = run('git symbolic-ref "refs/remotes/' + r + '/HEAD"', repoPath);
72
+ if (symbolic.ok && symbolic.out) return symbolic.out.replace('refs/remotes/' + r + '/', '');
73
+ }
74
+ if (hasRef(repoPath, 'refs/heads/main')) return 'main';
75
+ if (hasRef(repoPath, 'refs/heads/master')) return 'master';
76
+ if (hasRef(repoPath, 'refs/remotes/origin/main')) return 'main';
77
+ if (hasRef(repoPath, 'refs/remotes/origin/master')) return 'master';
78
+ return '';
79
+ }
80
+
81
+ /** 检出未提交项数(含未跟踪,不含 ignored);-1 = 状态不可读 */
82
+ function countUncommitted(repoPath) {
83
+ const s = run('git status --porcelain', repoPath);
84
+ if (!s.ok) return -1;
85
+ return s.out ? s.out.split(/\r?\n/).filter(Boolean).length : 0;
86
+ }
87
+
88
+ function main() {
89
+ const args = process.argv.slice(2);
90
+ const wsRoot = args[0];
91
+ if (!wsRoot || wsRoot.indexOf('--') === 0) {
92
+ console.error('Usage: node return-to-trunk.js <ws_root> [--apps a,b] [--dry-run]');
93
+ process.exit(1);
94
+ }
95
+ const appsIdx = args.indexOf('--apps');
96
+ const appFilter = appsIdx >= 0
97
+ ? new Set((args[appsIdx + 1] || '').split(',').map(s => s.trim()).filter(Boolean))
98
+ : null;
99
+ const dryRun = args.includes('--dry-run');
100
+
101
+ const repos = [{ name: 'workspace', repo: wsRoot }];
102
+ for (const app of loadApps(wsRoot)) {
103
+ if (appFilter && !appFilter.has(app.name)) continue;
104
+ repos.push({ name: app.name, repo: path.join(wsRoot, '.codespace', app.name) });
105
+ }
106
+
107
+ const results = [];
108
+ let failed = false;
109
+
110
+ for (const rp of repos) {
111
+ const rec = { name: rp.name, status: 'ok', main: null, switched: false, uncommitted: 0, reason: null, detail: null };
112
+ if (!fs.existsSync(path.join(rp.repo, '.git'))) {
113
+ rec.status = 'no-repo';
114
+ results.push(rec);
115
+ continue;
116
+ }
117
+ run('git fetch --quiet origin', rp.repo);
118
+ const main = detectMain(rp.repo);
119
+ if (!main) {
120
+ rec.status = 'failed';
121
+ rec.reason = 'cannot-detect-main';
122
+ results.push(rec);
123
+ failed = true;
124
+ continue;
125
+ }
126
+ rec.main = main;
127
+ rec.uncommitted = countUncommitted(rp.repo);
128
+ const current = run('git branch --show-current', rp.repo);
129
+
130
+ if (current.ok && current.out !== main) {
131
+ // detached(out 为空)也需切回——「不在主干」含 detached
132
+ if (dryRun) {
133
+ rec.reason = 'would-switch (from ' + current.out + ')';
134
+ results.push(rec);
135
+ continue;
136
+ }
137
+ if (rec.uncommitted > 0) {
138
+ rec.status = 'failed';
139
+ rec.reason = 'dirty-worktree';
140
+ rec.detail = '检出 ' + current.out + ' 上有 ' + rec.uncommitted + ' 项未提交改动,不自动 stash——先处理再重跑';
141
+ results.push(rec);
142
+ failed = true;
143
+ continue;
144
+ }
145
+ const co = hasRef(rp.repo, 'refs/heads/' + main)
146
+ ? run('git checkout --quiet "' + main + '"', rp.repo)
147
+ : run('git checkout -b "' + main + '" --track origin/' + main, rp.repo);
148
+ if (!co.ok) {
149
+ rec.status = 'failed';
150
+ rec.reason = 'checkout-failed';
151
+ rec.detail = (co.err || '').split(/\r?\n/)[0];
152
+ results.push(rec);
153
+ failed = true;
154
+ continue;
155
+ }
156
+ rec.switched = true;
157
+ } else if (dryRun) {
158
+ rec.reason = 'already-on-trunk';
159
+ results.push(rec);
160
+ continue;
161
+ }
162
+
163
+ if (hasRef(rp.repo, 'refs/remotes/origin/' + main)) {
164
+ const before = run('git rev-parse --quiet HEAD', rp.repo);
165
+ const pull = run('git pull --ff-only --quiet origin "' + main + '"', rp.repo);
166
+ if (!pull.ok) {
167
+ rec.status = 'failed';
168
+ rec.reason = 'pull-failed';
169
+ rec.detail = (pull.err || '').split(/\r?\n/)[0];
170
+ failed = true;
171
+ } else {
172
+ const after = run('git rev-parse --quiet HEAD', rp.repo);
173
+ rec.detail = before.ok && after.ok && before.out !== after.out
174
+ ? 'pulled origin/' + main
175
+ : 'up-to-date';
176
+ }
177
+ } else if (!rec.reason) {
178
+ rec.reason = 'no-remote-trunk';
179
+ }
180
+ results.push(rec);
181
+ }
182
+
183
+ results.forEach(r => {
184
+ console.log(r.status.toUpperCase() + ' ' + r.name + (r.main ? ' → ' + r.main : '') + (r.reason ? ': ' + r.reason : '') + (r.detail ? ' — ' + r.detail : ''));
185
+ });
186
+ console.log('---');
187
+ console.log('TRUNK_RESULT', JSON.stringify({ results }));
188
+ process.exit(failed ? 2 : 0);
189
+ }
190
+
191
+ if (require.main === module) main();
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: cbb-local-mode
3
- description: "业务空间本地模式开发:空间根与各涉及应用在 .codespace/<app>/ 就地建同名分支,当前会话直接开发(不建 worktree、不换目录)。触发:用户说'本地模式'/'就地开发'/'不走 worktree'/'在这个窗口开发',或双模式选择时用户选本地模式。覆盖开工(状态自检→布局确认→切分支)、开发、提交推送(转 cbb-worktree-push)、收尾(收尾合并门禁 ADR 0007:MR / 直合并 / 确认废弃后回 main + 删分支)、升级 worktree(重跑 cbb-worktree-init)。一次只承载一个需求。"
3
+ description: "业务空间本地模式开发:空间根与各涉及应用在 .codespace/<app>/ 就地建同名分支,当前会话直接开发(不建 worktree、不换目录)。触发:用户说'本地模式'/'就地开发'/'不走 worktree'/'在这个窗口开发',或双模式选择时用户选本地模式。覆盖开工(状态自检→布局确认→切分支)、开发、提交推送(转 cbb-worktree-push)、收尾(收尾合并门禁 ADR 0007:MR / 直合并 / 确认废弃;交付完成回主干 + 删分支,废弃保留本地分支)、升级 worktree(重跑 cbb-worktree-init)。一次只承载一个需求。"
4
4
  ---
5
5
 
6
6
  # Local Mode(本地模式)
@@ -88,40 +88,77 @@ MR 合并、验收通过后回到本会话收尾。**与 worktree 模式同门
88
88
  node "<skill_dir>/scripts/merge-branches.js" "<ws_root>" "<需求名>" --apps "<app1,app2>" --check
89
89
  ```
90
90
 
91
- - 解读末行 `MERGE_CHECK_RESULT`:每仓库 `merged`(含 `origin/main` / `main-via-release-*`)/ `unmerged` / `no-branch`
92
- - 全部 `merged` / `no-branch`(退出码 0)→ 直接进 5.4
93
- - 存在 `unmerged`(退出码 2)→ 进 5.3
91
+ - 解读末行 `MERGE_CHECK_RESULT`:每仓库 `merged`(含 `origin/main` / `main-via-release-*`)/ `unmerged` / `no-branch`(该仓库无此本地分支,不参与放行)/ `no-repo`(目录缺失,不参与放行)/ `failed`(主干探测或 fetch 失败)
92
+ - 全部 `merged` / `no-branch` / `no-repo`(退出码 0)→ 直接进 5.4
93
+ - 存在 `unmerged`(退出码 2 且状态为 `unmerged`)→ 进 5.3
94
+ - 存在 `failed`(退出码 2,**与 `unmerged` 撞码,先看状态再行动**)→ 这不是"未合并":报告失败仓库与原因(`cannot-detect-main` / `fetch-failed`),修复环境(远端可达性 / 远端默认分支设置)后重跑 5.2;**禁止按未合并走 5.3 门禁**
94
95
 
95
96
  **5.3 门禁三选一**(`AskUserQuestion`,同 close Step 3.4):
96
97
 
97
98
  - **提 MR** → **中止收尾**,引导用户运行 `cbb-worktree-push`(说"提交并推送")生成 MR 预填链接(其 Step 7:目标分支候选交互 + `make-mr-urls.js`)→ 合并完重跑本步
98
- - **直合并** → `merge-branches.js`(临时 worktree 合并,**不碰本地检出**——被需求分支占用也无需处理;交付后本地主干检出不随之前移,由 5.4 的 checkout main + pull 追平):
99
+ - **直合并** → `merge-branches.js`(临时 worktree 合并,**不碰本地检出**——被需求分支占用也无需处理;交付后本地主干检出不随之前移,由 5.4 的 `return-to-trunk.js` 追平):
99
100
 
100
101
  ```bash
101
102
  node "<skill_dir>/scripts/merge-branches.js" "<ws_root>" "<需求名>" --apps "<app1,app2>"
102
103
  ```
103
104
 
104
105
  `need-mr`(远端主干受保护)→ 改用提 MR;`failed` → 输出明细、修复后重跑
105
- - **确认废弃** → `deprecate-branches.js`(远程重命名 `<需求名>-deprecated`;本地分支保留原名)
106
+ - **确认废弃** → 三步:检查未推送提交 → 二次确认 → 执行。**废弃后仍进 5.4 步骤 1 回主干,但跳过步骤 2(本地分支保留原名,不调用 `delete-branches.js`)**。
106
107
 
107
- **5.4 交付完成 → 各仓库回主干并删分支**:
108
+ 1. 检查未推送提交(废弃重命名只覆盖远端已推送的部分,本地 ahead 提交不会进 `<需求名>-deprecated`):
109
+
110
+ ```bash
111
+ node "<skill_dir>/scripts/deprecate-branches.js" "<ws_root>" "<需求名>" --apps "<app1,app2>" --check
112
+ ```
113
+
114
+ **解读末行 `DEPRECATE_CHECK_RESULT`**:每仓库 `unpushed`(本地领先远端的提交数;`all` = 远端无该分支、提交全部未推送;`0` = 已全部推送)。存在 `unpushed > 0` / `all` 时用 `AskUserQuestion`:
115
+
116
+ ```
117
+ ⚠️ 以下仓库存在未推送提交(将只保留在本地分支,不进 <需求名>-deprecated):
118
+ - workspace: 3 个 commit
119
+
120
+ 请选择处理方式:
121
+ [先推送](推荐)调 cbb-worktree-push 推送全部提交后回到本步
122
+ [继续] 不推送直接废弃(这些提交只存在于被保留的本地分支)
123
+ [中止] 终止流程,我先手动处理
124
+ ```
125
+
126
+ 2. 二次确认(远程重命名不可逆,`AskUserQuestion` 固定选项):确认废弃 / 中止
127
+ 3. 执行:
128
+
129
+ ```bash
130
+ node "<skill_dir>/scripts/deprecate-branches.js" "<ws_root>" "<需求名>" --apps "<app1,app2>"
131
+ ```
132
+
133
+ `failed` → 报告并终止;完成后进 5.4(**仅步骤 1**)
134
+
135
+ **5.4 交付完成 → 各仓库回主干并删分支**(脚本化;主干名逐仓库经远端 HEAD 探测,兼容 main / master。**废弃出口仅执行步骤 1**——分支保留、不删):
136
+
137
+ 1. 各仓库切回主干并追平远端(替代原裸 git 三连;不自动 stash,失败即报告):
108
138
 
109
139
  ```bash
110
- git -C "<仓库>" checkout main && git -C "<仓库>" pull && git -C "<仓库>" branch -d "<需求名>"
140
+ node "<skill_dir>/scripts/return-to-trunk.js" "<ws_root>" --apps "<app1,app2>"
111
141
  ```
112
142
 
113
- - 各仓库本地分支删除后,远程分支删除由共享脚本完成(删除前输出将删清单):
143
+ **解读末行 `TRUNK_RESULT`**:
144
+ - 退出码 0 → 全部已回主干并追平(detail 注记 `pulled origin/<主干>` = 本次有追平;`up-to-date` = 本就最新)
145
+ - 某仓库 `dirty-worktree` → 检出上有未提交改动(回归 5.1 前置,先处理再重跑);**不自动 stash**
146
+ - 某仓库 `pull-failed`(本地主干与 origin 分叉)→ 输出详情,交用户决策
147
+ - 某仓库 `cannot-detect-main` → 检查该仓库远端与主干分支(`fetch-failed` 同理)
148
+ - 幂等:已在主干的仓库仅追平远端,重复执行无害
149
+
150
+ 2. 本地 + 远程需求分支删除(**仅交付完成出口执行**——MR 已合入 / 直合并成功;**废弃出口跳过本步**,本地分支保留原名。本地删除含 squash 合并的 `-d` → `-D` 兜底;删除前输出将删清单):
114
151
 
115
152
  ```bash
116
153
  node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<需求名>" local_remote
117
154
  ```
118
- - `checkout` 被 hook 拦(说明 main 落后或本地 main 有分叉)→ 先 `pull --ff-only`,再删分支
155
+
119
156
  - openspec change 归档在合并前执行(产物在分支上随交付合入)
120
- - 全部仓库回 main 后向用户报告"本地模式已收尾,空间回到干净态"
157
+ - 全部仓库回主干(交付完成出口含分支删除)后,向用户报告"本地模式已收尾,空间回到干净态"
121
158
 
122
159
  ## 升级 worktree(中途要并行 / 要隔离)
123
160
 
124
- **直接重跑 `cbb-worktree-init`(幂等)**:分支已存在自动复用(reused),worktree 从分支创建——本地阶段的提交**随身带走**,零成本升级。升级前提:本地检出先回 main(`git checkout main`,未提交改动先 commit 或用户决策),否则 worktree 检出与本地检出冲突。
161
+ **直接重跑 `cbb-worktree-init`(幂等)**:分支已存在自动复用(reused),worktree 从分支创建——本地阶段的提交**随身带走**,零成本升级。升级前提:本地检出先回主干(跑 5.4 的 `return-to-trunk.js`,主干名以远端 HEAD 为准;未提交改动先 commit 或用户决策),否则 worktree 检出与本地检出冲突。
125
162
 
126
163
  ---
127
164
 
@@ -136,9 +173,9 @@ node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<需求名>" local_re
136
173
  | commit 被 hook 拦(main 载荷) | 大声报错是预期行为:回分支重做;禁止自动 --no-verify |
137
174
  | 误在 main 检出上写了文件 | `git checkout -b <需求名>` 后改动随分支走;已 staged 则 unstage 后再切 |
138
175
  | 提交推送 | 说"提交并推送" → cbb-worktree-push(本地布局自动发现) |
139
- | 收尾 | 收尾合并门禁(ADR 0007):`merge-branches.js --check` 判已合并 → 全合并则各仓库 checkout main + pull + 删分支;否则三选一(MR / 直合并(临时 worktree) / 确认废弃) |
176
+ | 收尾 | 收尾合并门禁(ADR 0007):`merge-branches.js --check` 判已合并 → 全合并则 `return-to-trunk.js` 回主干 + `delete-branches.js` 删分支(主干名探测);否则三选一(MR / 直合并(临时 worktree) / 确认废弃(仅回主干不删分支)) |
140
177
  | 要并行新需求 | 转 worktree 模式(init 分支从 origin/main 建,不占本地检出);本地需求继续占用根 |
141
- | 升级隔离 | 重跑 cbb-worktree-init(提交随身带走);先回 main 腾检出 |
178
+ | 升级隔离 | 重跑 cbb-worktree-init(提交随身带走);先回主干腾检出(return-to-trunk.js) |
142
179
 
143
180
  ---
144
181
 
@@ -161,8 +198,13 @@ node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<需求名>" local_re
161
198
 
162
199
  ### 收尾忘记 pull
163
200
 
164
- - **问题:** `checkout main` 后直接删分支,本地 main 陈旧,下次 `start-local` 从旧基线建分支
165
- - **修复:** 收尾固定三连:checkout main && pull && branch -d
201
+ - **问题:** 回主干后未追平远端,本地主干陈旧,下次 `start-local` 从旧基线建分支
202
+ - **修复:** 收尾固定两步脚本:`return-to-trunk.js`(切回主干 + `pull --ff-only` 追平)→ `delete-branches.js` 删分支
203
+
204
+ ### 废弃出口误跑删分支
205
+
206
+ - **问题:** 选"确认废弃"后照 5.4 全跑,`delete-branches.js` 对未合并的废弃分支 `-D` 强删——远端 `<需求名>-deprecated` 只含已推送部分,本地领先提交只剩 reflog 可捞
207
+ - **修复:** 废弃出口仅执行 5.4 步骤 1(回主干 + 追平);本地分支保留原名,**不调用** `delete-branches.js`
166
208
 
167
209
  ### 中途插单静默混入
168
210
 
@@ -189,6 +231,7 @@ node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<需求名>" local_re
189
231
  - 布局展示 + 用户确认后才切分支
190
232
  - 切分支统一走 `start-local.js`(基线 = `<remote>/<main>`,不取 HEAD)
191
233
  - 收尾先过门禁判定(`merge-branches.js --check`);未合并必须三选一后收尾
192
- - 直合并走 `merge-branches.js`(临时 worktree,零接触;本地检出由 5.4 收尾三连切回主干)
193
- - 收尾三连(checkout main / pull / 删分支)完成后报告干净态
234
+ - 直合并走 `merge-branches.js`(临时 worktree,零接触;本地检出由 5.4 `return-to-trunk.js` 切回主干)
235
+ - 废弃出口:先 `deprecate-branches.js --check` 查未推送提交(有 ahead 须用户决策)→ 二次确认后执行;废弃后**仅回主干**(5.4 步骤 1),不删本地分支
236
+ - 收尾脚本化(`return-to-trunk.js` 回主干 + 交付完成出口 `delete-branches.js` 删分支)完成后报告干净态
194
237
  - 所有写操作前的分支确认(当前检出 = 需求分支)
@@ -9,9 +9,17 @@ require('./silence-popup');
9
9
  *
10
10
  * 用法:
11
11
  * node deprecate-branches.js <ws_root> <branch> [--apps a,b]
12
+ * node deprecate-branches.js <ws_root> <branch> [--apps a,b] --check # 仅检查,不重命名
13
+ *
14
+ * 模式:
15
+ * --check 只读检查:逐仓库报远端分支存在性与本地未推送提交数
16
+ * (ls-remote 取远端权威 hash 与本地 <branch> 比对)。
17
+ * 用途:废弃重命名只覆盖远端已推送的部分——本地 ahead 提交
18
+ * 不会被带进 <branch>-deprecated,执行前须提示用户决策。
19
+ * 默认 执行远端重命名 <branch> → <branch>-deprecated
12
20
  *
13
21
  * 退出码:0 全部成功(含 skipped)/ 1 参数错误 / 2 存在失败
14
- * 末行:DEPRECATE_RESULT [...]
22
+ * 末行:DEPRECATE_RESULT [...] 或 DEPRECATE_CHECK_RESULT [...]
15
23
  */
16
24
 
17
25
  const { execSync } = require('child_process');
@@ -44,6 +52,51 @@ function loadApps(wsRoot) {
44
52
  }
45
53
  }
46
54
 
55
+ /**
56
+ * 检查单个仓库(--check,只读):
57
+ * unpushed: 0 = 已全部推送;>0 = 本地领先远端 N 个提交;'all' = 远端无该分支(提交全部未推送)
58
+ */
59
+ function checkRepo(name, repo, branch) {
60
+ const rec = { name, status: 'skipped', remote_exists: false, unpushed: 0, detail: null };
61
+ if (!fs.existsSync(path.join(repo, '.git'))) {
62
+ rec.detail = 'no-repo';
63
+ return rec;
64
+ }
65
+ const remote = run('git remote', repo);
66
+ if (!remote.ok || !remote.out) {
67
+ rec.detail = 'no-remote';
68
+ return rec;
69
+ }
70
+ const r = remote.out.split(/\r?\n/)[0];
71
+ if (!run('git rev-parse --verify --quiet "refs/heads/' + branch + '"', repo).ok) {
72
+ rec.detail = 'no-local-branch';
73
+ return rec;
74
+ }
75
+ const ls = run('git ls-remote --heads "' + r + '" "refs/heads/' + branch + '"', repo);
76
+ if (!ls.ok) {
77
+ rec.status = 'failed';
78
+ rec.detail = 'ls-remote-failed';
79
+ return rec;
80
+ }
81
+ if (!ls.out) {
82
+ rec.status = 'ok';
83
+ rec.unpushed = 'all';
84
+ rec.detail = 'remote-has-no-branch';
85
+ return rec;
86
+ }
87
+ const remoteHash = ls.out.split(/\r?\n/)[0].split(/\s+/)[0];
88
+ const cnt = run('git rev-list --count "' + remoteHash + '..refs/heads/' + branch + '"', repo);
89
+ if (!cnt.ok) {
90
+ rec.status = 'failed';
91
+ rec.detail = 'rev-list-failed';
92
+ return rec;
93
+ }
94
+ rec.status = 'ok';
95
+ rec.remote_exists = true;
96
+ rec.unpushed = parseInt(cnt.out, 10) || 0;
97
+ return rec;
98
+ }
99
+
47
100
  /** 对单个仓库执行废弃重命名 */
48
101
  function deprecateRepo(name, repo, branch) {
49
102
  const newName = branch + '-deprecated';
@@ -95,6 +148,7 @@ function main() {
95
148
  const appFilter = appsIdx >= 0
96
149
  ? new Set((args[appsIdx + 1] || '').split(',').map(s => s.trim()).filter(Boolean))
97
150
  : null;
151
+ const checkOnly = args.includes('--check');
98
152
 
99
153
  const repos = [{ name: 'workspace', repo: wsRoot }];
100
154
  for (const app of loadApps(wsRoot)) {
@@ -102,6 +156,17 @@ function main() {
102
156
  repos.push({ name: app.name, repo: path.join(wsRoot, '.codespace', app.name) });
103
157
  }
104
158
 
159
+ if (checkOnly) {
160
+ const results = repos.map(rp => checkRepo(rp.name, rp.repo, branch));
161
+ results.forEach(r => {
162
+ const up = r.status === 'ok' && r.unpushed !== 0 ? ' [unpushed: ' + r.unpushed + ']' : '';
163
+ console.log(r.status.toUpperCase() + ' ' + r.name + (r.detail ? ' — ' + r.detail : '') + up);
164
+ });
165
+ console.log('---');
166
+ console.log('DEPRECATE_CHECK_RESULT', JSON.stringify({ branch, results }));
167
+ process.exit(results.some(r => r.status === 'failed') ? 2 : 0);
168
+ }
169
+
105
170
  const results = repos.map(rp => deprecateRepo(rp.name, rp.repo, branch));
106
171
 
107
172
  results.forEach(r => {
@@ -116,4 +181,4 @@ function main() {
116
181
 
117
182
  if (require.main === module) main();
118
183
 
119
- module.exports = { run, loadApps, deprecateRepo };
184
+ module.exports = { run, loadApps, checkRepo, deprecateRepo };
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env node
2
+ require('./silence-popup');
3
+ /**
4
+ * 收尾回主干 —— 交付完成后把各参与仓库的本地检出切回主干并追平(ADR 0007 / 0008)
5
+ *
6
+ * 背景:merge-branches.js 统一零接触通道后,合并在临时 worktree 内完成、
7
+ * 不触碰本地检出(本地主干检出不随交付前移)。本地模式收尾需把各仓库
8
+ * 本地检出从需求分支切回主干——本脚本替代曾经写死 `git checkout main`
9
+ * 的裸命令:主干名逐仓库经远端 HEAD 探测(与 check-local-state.js 同源,
10
+ * 兼容 master 等),不再假设主干叫 main。
11
+ *
12
+ * 用法:
13
+ * node return-to-trunk.js <ws_root> [--apps a,b] [--dry-run]
14
+ *
15
+ * 行为(逐仓库,相互独立、失败不停链):
16
+ * 1. 目录无 .git → SKIP no-repo
17
+ * 2. git fetch --quiet origin(失败仅降级,后续判定退回本地主干)
18
+ * 3. 主干名 detectMain:远端 HEAD → 本地 main → 本地 master → origin/main → origin/master
19
+ * → FAIL cannot-detect-main
20
+ * 4. 当前检出 ≠ 主干:
21
+ * - 检出 dirty(含未跟踪)→ FAIL dirty-worktree(列未提交项数;不自动 stash,交用户决策)
22
+ * - 本地有主干分支 → git checkout <主干>;仅远端有 → git checkout -b <主干> --track origin/<主干>
23
+ * 5. 追平远端:origin/<主干> 存在 → git pull --ff-only origin <主干>
24
+ * (成功详情注记 `pulled origin/<主干>` / `up-to-date`;本地主干与 origin 分叉 → FAIL
25
+ * pull-failed,交用户决策;远端无主干 → 注记跳过)
26
+ * 6. 已在主干 → 跳过切回,仍追平(幂等)
27
+ *
28
+ * 退出码:0 全部成功(含 no-repo / 已在主干)/ 1 参数错误 / 2 任一仓库失败
29
+ * 末行:TRUNK_RESULT [...]
30
+ */
31
+
32
+ const { execSync } = require('child_process');
33
+ const fs = require('fs');
34
+ const path = require('path');
35
+
36
+ function run(cmd, cwd) {
37
+ try {
38
+ return {
39
+ ok: true,
40
+ out: execSync(cmd, {
41
+ cwd,
42
+ encoding: 'utf-8',
43
+ stdio: ['ignore', 'pipe', 'pipe'],
44
+ timeout: 120000,
45
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
46
+ }).trim(),
47
+ };
48
+ } catch (e) {
49
+ return { ok: false, err: (e.stderr || e.message || String(e)).trim() };
50
+ }
51
+ }
52
+
53
+ function hasRef(repoPath, ref) {
54
+ return run('git rev-parse --verify --quiet "' + ref + '"', repoPath).ok;
55
+ }
56
+
57
+ function loadApps(wsRoot) {
58
+ try {
59
+ const config = JSON.parse(fs.readFileSync(path.join(wsRoot, 'CBB-WORKSPACE.json'), 'utf-8'));
60
+ return Array.isArray(config.apps) ? config.apps : [];
61
+ } catch (e) {
62
+ return [];
63
+ }
64
+ }
65
+
66
+ /** 主干名:symbolic-ref 远端 HEAD → 本地 main/master → 远端跟踪 origin/main|master → 空串(与 check-local-state.js 同源,回退链更强) */
67
+ function detectMain(repoPath) {
68
+ const remote = run('git remote', repoPath);
69
+ if (remote.ok && remote.out) {
70
+ const r = remote.out.split(/\r?\n/)[0];
71
+ const symbolic = run('git symbolic-ref "refs/remotes/' + r + '/HEAD"', repoPath);
72
+ if (symbolic.ok && symbolic.out) return symbolic.out.replace('refs/remotes/' + r + '/', '');
73
+ }
74
+ if (hasRef(repoPath, 'refs/heads/main')) return 'main';
75
+ if (hasRef(repoPath, 'refs/heads/master')) return 'master';
76
+ if (hasRef(repoPath, 'refs/remotes/origin/main')) return 'main';
77
+ if (hasRef(repoPath, 'refs/remotes/origin/master')) return 'master';
78
+ return '';
79
+ }
80
+
81
+ /** 检出未提交项数(含未跟踪,不含 ignored);-1 = 状态不可读 */
82
+ function countUncommitted(repoPath) {
83
+ const s = run('git status --porcelain', repoPath);
84
+ if (!s.ok) return -1;
85
+ return s.out ? s.out.split(/\r?\n/).filter(Boolean).length : 0;
86
+ }
87
+
88
+ function main() {
89
+ const args = process.argv.slice(2);
90
+ const wsRoot = args[0];
91
+ if (!wsRoot || wsRoot.indexOf('--') === 0) {
92
+ console.error('Usage: node return-to-trunk.js <ws_root> [--apps a,b] [--dry-run]');
93
+ process.exit(1);
94
+ }
95
+ const appsIdx = args.indexOf('--apps');
96
+ const appFilter = appsIdx >= 0
97
+ ? new Set((args[appsIdx + 1] || '').split(',').map(s => s.trim()).filter(Boolean))
98
+ : null;
99
+ const dryRun = args.includes('--dry-run');
100
+
101
+ const repos = [{ name: 'workspace', repo: wsRoot }];
102
+ for (const app of loadApps(wsRoot)) {
103
+ if (appFilter && !appFilter.has(app.name)) continue;
104
+ repos.push({ name: app.name, repo: path.join(wsRoot, '.codespace', app.name) });
105
+ }
106
+
107
+ const results = [];
108
+ let failed = false;
109
+
110
+ for (const rp of repos) {
111
+ const rec = { name: rp.name, status: 'ok', main: null, switched: false, uncommitted: 0, reason: null, detail: null };
112
+ if (!fs.existsSync(path.join(rp.repo, '.git'))) {
113
+ rec.status = 'no-repo';
114
+ results.push(rec);
115
+ continue;
116
+ }
117
+ run('git fetch --quiet origin', rp.repo);
118
+ const main = detectMain(rp.repo);
119
+ if (!main) {
120
+ rec.status = 'failed';
121
+ rec.reason = 'cannot-detect-main';
122
+ results.push(rec);
123
+ failed = true;
124
+ continue;
125
+ }
126
+ rec.main = main;
127
+ rec.uncommitted = countUncommitted(rp.repo);
128
+ const current = run('git branch --show-current', rp.repo);
129
+
130
+ if (current.ok && current.out !== main) {
131
+ // detached(out 为空)也需切回——「不在主干」含 detached
132
+ if (dryRun) {
133
+ rec.reason = 'would-switch (from ' + current.out + ')';
134
+ results.push(rec);
135
+ continue;
136
+ }
137
+ if (rec.uncommitted > 0) {
138
+ rec.status = 'failed';
139
+ rec.reason = 'dirty-worktree';
140
+ rec.detail = '检出 ' + current.out + ' 上有 ' + rec.uncommitted + ' 项未提交改动,不自动 stash——先处理再重跑';
141
+ results.push(rec);
142
+ failed = true;
143
+ continue;
144
+ }
145
+ const co = hasRef(rp.repo, 'refs/heads/' + main)
146
+ ? run('git checkout --quiet "' + main + '"', rp.repo)
147
+ : run('git checkout -b "' + main + '" --track origin/' + main, rp.repo);
148
+ if (!co.ok) {
149
+ rec.status = 'failed';
150
+ rec.reason = 'checkout-failed';
151
+ rec.detail = (co.err || '').split(/\r?\n/)[0];
152
+ results.push(rec);
153
+ failed = true;
154
+ continue;
155
+ }
156
+ rec.switched = true;
157
+ } else if (dryRun) {
158
+ rec.reason = 'already-on-trunk';
159
+ results.push(rec);
160
+ continue;
161
+ }
162
+
163
+ if (hasRef(rp.repo, 'refs/remotes/origin/' + main)) {
164
+ const before = run('git rev-parse --quiet HEAD', rp.repo);
165
+ const pull = run('git pull --ff-only --quiet origin "' + main + '"', rp.repo);
166
+ if (!pull.ok) {
167
+ rec.status = 'failed';
168
+ rec.reason = 'pull-failed';
169
+ rec.detail = (pull.err || '').split(/\r?\n/)[0];
170
+ failed = true;
171
+ } else {
172
+ const after = run('git rev-parse --quiet HEAD', rp.repo);
173
+ rec.detail = before.ok && after.ok && before.out !== after.out
174
+ ? 'pulled origin/' + main
175
+ : 'up-to-date';
176
+ }
177
+ } else if (!rec.reason) {
178
+ rec.reason = 'no-remote-trunk';
179
+ }
180
+ results.push(rec);
181
+ }
182
+
183
+ results.forEach(r => {
184
+ console.log(r.status.toUpperCase() + ' ' + r.name + (r.main ? ' → ' + r.main : '') + (r.reason ? ': ' + r.reason : '') + (r.detail ? ' — ' + r.detail : ''));
185
+ });
186
+ console.log('---');
187
+ console.log('TRUNK_RESULT', JSON.stringify({ results }));
188
+ process.exit(failed ? 2 : 0);
189
+ }
190
+
191
+ if (require.main === module) main();
@@ -9,9 +9,17 @@ require('./silence-popup');
9
9
  *
10
10
  * 用法:
11
11
  * node deprecate-branches.js <ws_root> <branch> [--apps a,b]
12
+ * node deprecate-branches.js <ws_root> <branch> [--apps a,b] --check # 仅检查,不重命名
13
+ *
14
+ * 模式:
15
+ * --check 只读检查:逐仓库报远端分支存在性与本地未推送提交数
16
+ * (ls-remote 取远端权威 hash 与本地 <branch> 比对)。
17
+ * 用途:废弃重命名只覆盖远端已推送的部分——本地 ahead 提交
18
+ * 不会被带进 <branch>-deprecated,执行前须提示用户决策。
19
+ * 默认 执行远端重命名 <branch> → <branch>-deprecated
12
20
  *
13
21
  * 退出码:0 全部成功(含 skipped)/ 1 参数错误 / 2 存在失败
14
- * 末行:DEPRECATE_RESULT [...]
22
+ * 末行:DEPRECATE_RESULT [...] 或 DEPRECATE_CHECK_RESULT [...]
15
23
  */
16
24
 
17
25
  const { execSync } = require('child_process');
@@ -44,6 +52,51 @@ function loadApps(wsRoot) {
44
52
  }
45
53
  }
46
54
 
55
+ /**
56
+ * 检查单个仓库(--check,只读):
57
+ * unpushed: 0 = 已全部推送;>0 = 本地领先远端 N 个提交;'all' = 远端无该分支(提交全部未推送)
58
+ */
59
+ function checkRepo(name, repo, branch) {
60
+ const rec = { name, status: 'skipped', remote_exists: false, unpushed: 0, detail: null };
61
+ if (!fs.existsSync(path.join(repo, '.git'))) {
62
+ rec.detail = 'no-repo';
63
+ return rec;
64
+ }
65
+ const remote = run('git remote', repo);
66
+ if (!remote.ok || !remote.out) {
67
+ rec.detail = 'no-remote';
68
+ return rec;
69
+ }
70
+ const r = remote.out.split(/\r?\n/)[0];
71
+ if (!run('git rev-parse --verify --quiet "refs/heads/' + branch + '"', repo).ok) {
72
+ rec.detail = 'no-local-branch';
73
+ return rec;
74
+ }
75
+ const ls = run('git ls-remote --heads "' + r + '" "refs/heads/' + branch + '"', repo);
76
+ if (!ls.ok) {
77
+ rec.status = 'failed';
78
+ rec.detail = 'ls-remote-failed';
79
+ return rec;
80
+ }
81
+ if (!ls.out) {
82
+ rec.status = 'ok';
83
+ rec.unpushed = 'all';
84
+ rec.detail = 'remote-has-no-branch';
85
+ return rec;
86
+ }
87
+ const remoteHash = ls.out.split(/\r?\n/)[0].split(/\s+/)[0];
88
+ const cnt = run('git rev-list --count "' + remoteHash + '..refs/heads/' + branch + '"', repo);
89
+ if (!cnt.ok) {
90
+ rec.status = 'failed';
91
+ rec.detail = 'rev-list-failed';
92
+ return rec;
93
+ }
94
+ rec.status = 'ok';
95
+ rec.remote_exists = true;
96
+ rec.unpushed = parseInt(cnt.out, 10) || 0;
97
+ return rec;
98
+ }
99
+
47
100
  /** 对单个仓库执行废弃重命名 */
48
101
  function deprecateRepo(name, repo, branch) {
49
102
  const newName = branch + '-deprecated';
@@ -95,6 +148,7 @@ function main() {
95
148
  const appFilter = appsIdx >= 0
96
149
  ? new Set((args[appsIdx + 1] || '').split(',').map(s => s.trim()).filter(Boolean))
97
150
  : null;
151
+ const checkOnly = args.includes('--check');
98
152
 
99
153
  const repos = [{ name: 'workspace', repo: wsRoot }];
100
154
  for (const app of loadApps(wsRoot)) {
@@ -102,6 +156,17 @@ function main() {
102
156
  repos.push({ name: app.name, repo: path.join(wsRoot, '.codespace', app.name) });
103
157
  }
104
158
 
159
+ if (checkOnly) {
160
+ const results = repos.map(rp => checkRepo(rp.name, rp.repo, branch));
161
+ results.forEach(r => {
162
+ const up = r.status === 'ok' && r.unpushed !== 0 ? ' [unpushed: ' + r.unpushed + ']' : '';
163
+ console.log(r.status.toUpperCase() + ' ' + r.name + (r.detail ? ' — ' + r.detail : '') + up);
164
+ });
165
+ console.log('---');
166
+ console.log('DEPRECATE_CHECK_RESULT', JSON.stringify({ branch, results }));
167
+ process.exit(results.some(r => r.status === 'failed') ? 2 : 0);
168
+ }
169
+
105
170
  const results = repos.map(rp => deprecateRepo(rp.name, rp.repo, branch));
106
171
 
107
172
  results.forEach(r => {
@@ -116,4 +181,4 @@ function main() {
116
181
 
117
182
  if (require.main === module) main();
118
183
 
119
- module.exports = { run, loadApps, deprecateRepo };
184
+ module.exports = { run, loadApps, checkRepo, deprecateRepo };
@@ -367,7 +367,7 @@ node "<skill_dir>/scripts/junction-ai-config.js" "<ws_root>" "<wt_path>"
367
367
  | 空间新增了应用(编辑 `CBB-WORKSPACE.json` 后) | Step 1 拉取新应用代码 → Step 3.3 为新应用建分支 → Step 3.5 在 `.codespace/` 容器内创建缺失的 app worktree → Step 3.6 补 junction |
368
368
  | 上次 init 中途失败 | 已存在的分支 / worktree 自动复用,`.gitignore` 处理自动跳过,仅补齐缺失部分 |
369
369
  | 应用从 config 移除 | 已创建的分支 / worktree **不会**自动删除;如需清理用 `cbb-worktree-close` skill 关闭后重建 |
370
- | **本地模式升级 worktree**(ADR 0002) | Step 3.3 分支已存在 → reused(本地阶段提交随身带走);先 `git checkout main` 腾出各仓库本地检出,再重跑本技能 |
370
+ | **本地模式升级 worktree**(ADR 0002) | Step 3.3 分支已存在 → reused(本地阶段提交随身带走);先把各仓库本地检出切回主干(主干名以远端 HEAD 为准,勿写死 main),再重跑本技能 |
371
371
  | 本地模式进行中重跑 | 安全:分支基线取 `origin/main`(非 HEAD),不受根/应用检出状态影响;fetch 不动工作区 |
372
372
 
373
373
  > ⓘ 本技能**零远端副作用**(lazy push):不存在"重跑推送开发中提交"的风险面——推送只发生在 `cbb-worktree-push`。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jspg-ai/coding-bb",
3
- "version": "0.0.5-beta.2",
3
+ "version": "0.0.5-beta.4",
4
4
  "description": "整合业界热门且高价值的工具、框架与技能,为 AI CODING AGENT 提供统一的行为准则与工作流,辅助开发者将需求高效落地为符合规范的代码",
5
5
  "main": "cbb/lib/install/init.js",
6
6
  "bin": {
@@ -20,7 +20,7 @@
20
20
  "scripts": {
21
21
  "sync:shared": "node scripts/sync-shared.js",
22
22
  "pretest": "node scripts/sync-shared.js --check",
23
- "test": "node test/lib/install/cleanup.test.js && node test/lib/utils/settings.test.js && node test/lib/utils/gitignore.test.js && node test/lib/utils/githook.test.js && node test/lib/utils/version.test.js && node test/lib/install/workspaces.test.js && node test/lib/utils/checkbox.test.js && node test/lib/utils/output.test.js && node test/lib/install/cli-help.test.js && node test/lib/install/adapters.test.js && node test/lib/install/tool-selection.test.js && node test/lib/install/agents-nav.test.js && node test/lib/install/command-skill.test.js && node test/lib/utils/plugin-hook.test.js && node test/lib/wiki/wiki-publish.test.js && node test/lib/wiki/wiki-parse-args.test.js && node test/lib/wiki/wiki-split.test.js && node test/lib/superpowers/superpowers.test.js && node test/lib/external/external.test.js && node test/worktrees/push-branches.test.js && node test/worktrees/commit-worktrees.test.js && node test/worktrees/push-worktrees.test.js && node test/worktrees/check-env-deep.test.js && node test/worktrees/create-worktrees.test.js && node test/worktrees/make-mr-urls.test.js && node test/lib/install/setup-decision.test.js && node test/lib/openspec/protected-paths.test.js && node test/worktrees/merge-branches.test.js && node test/worktrees/deprecate-branches.test.js && node test/worktrees/delete-branches.test.js && node test/worktrees/skill-refs.test.js"
23
+ "test": "node test/lib/install/cleanup.test.js && node test/lib/utils/settings.test.js && node test/lib/utils/gitignore.test.js && node test/lib/utils/githook.test.js && node test/lib/utils/version.test.js && node test/lib/install/workspaces.test.js && node test/lib/utils/checkbox.test.js && node test/lib/utils/output.test.js && node test/lib/install/cli-help.test.js && node test/lib/install/adapters.test.js && node test/lib/install/tool-selection.test.js && node test/lib/install/agents-nav.test.js && node test/lib/install/command-skill.test.js && node test/lib/utils/plugin-hook.test.js && node test/lib/wiki/wiki-publish.test.js && node test/lib/wiki/wiki-parse-args.test.js && node test/lib/wiki/wiki-split.test.js && node test/lib/superpowers/superpowers.test.js && node test/lib/external/external.test.js && node test/worktrees/push-branches.test.js && node test/worktrees/commit-worktrees.test.js && node test/worktrees/push-worktrees.test.js && node test/worktrees/check-env-deep.test.js && node test/worktrees/create-worktrees.test.js && node test/worktrees/make-mr-urls.test.js && node test/lib/install/setup-decision.test.js && node test/lib/openspec/protected-paths.test.js && node test/worktrees/merge-branches.test.js && node test/worktrees/deprecate-branches.test.js && node test/worktrees/delete-branches.test.js && node test/worktrees/return-to-trunk.test.js && node test/worktrees/skill-refs.test.js"
24
24
  },
25
25
  "keywords": [
26
26
  "cbb",