@jspg-ai/coding-bb 0.0.3-beta.9 → 0.0.5-beta.1

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 (109) hide show
  1. package/README.md +1 -0
  2. package/cbb/dev-standards/rules/cbb-ai-behavior.md +0 -15
  3. package/cbb/dev-standards/rules/cbb-workspace-discipline.md +38 -0
  4. package/cbb/lib/external/index.js +228 -0
  5. package/cbb/lib/install/agents-nav.js +92 -0
  6. package/cbb/lib/install/claude-code.js +8 -1
  7. package/cbb/lib/install/command-skill.js +105 -0
  8. package/cbb/lib/install/init.js +399 -129
  9. package/cbb/lib/install/minimax.js +60 -0
  10. package/cbb/lib/install/rule-rewrite.js +1 -1
  11. package/cbb/lib/install/setup-decision.js +92 -0
  12. package/cbb/lib/install/tool-selection.js +23 -0
  13. package/cbb/lib/install/trae.js +18 -8
  14. package/cbb/lib/install/workbuddy.js +36 -0
  15. package/cbb/lib/install/workspaces.js +15 -61
  16. package/cbb/lib/utils/check-update.js +13 -5
  17. package/cbb/lib/utils/githook.js +230 -0
  18. package/cbb/lib/utils/gitignore.js +17 -4
  19. package/cbb/lib/utils/plugin-hook.js +92 -0
  20. package/cbb/worktrees/_shared/scripts/check-local-state.js +114 -0
  21. package/cbb/worktrees/_shared/scripts/check-unarchived.js +77 -0
  22. package/cbb/worktrees/_shared/scripts/delete-branches.js +147 -0
  23. package/cbb/worktrees/_shared/scripts/deprecate-branches.js +119 -0
  24. package/cbb/worktrees/_shared/scripts/find-target-worktree.js +148 -96
  25. package/cbb/worktrees/_shared/scripts/find-workspace-root.js +8 -8
  26. package/cbb/worktrees/_shared/scripts/install-git-hook.js +70 -0
  27. package/cbb/worktrees/_shared/scripts/junction-ai-config.js +192 -0
  28. package/cbb/worktrees/_shared/scripts/merge-branches.js +580 -0
  29. package/cbb/worktrees/_shared/scripts/push-core.js +1 -1
  30. package/cbb/worktrees/_shared/scripts/silence-popup.js +1 -1
  31. package/cbb/worktrees/_shared/scripts/start-local.js +180 -0
  32. package/cbb/worktrees/skills/cbb-local-mode/SKILL.md +194 -0
  33. package/cbb/worktrees/skills/cbb-local-mode/scripts/check-local-state.js +114 -0
  34. package/cbb/worktrees/skills/cbb-local-mode/scripts/delete-branches.js +147 -0
  35. package/cbb/worktrees/skills/cbb-local-mode/scripts/deprecate-branches.js +119 -0
  36. package/cbb/worktrees/skills/cbb-local-mode/scripts/install-git-hook.js +70 -0
  37. package/cbb/worktrees/skills/cbb-local-mode/scripts/merge-branches.js +580 -0
  38. package/cbb/worktrees/skills/cbb-local-mode/scripts/silence-popup.js +23 -0
  39. package/cbb/worktrees/skills/cbb-local-mode/scripts/start-local.js +180 -0
  40. package/cbb/worktrees/skills/cbb-worktree-close/SKILL.md +134 -81
  41. package/cbb/worktrees/skills/cbb-worktree-close/scripts/check-env.js +3 -3
  42. package/cbb/worktrees/skills/cbb-worktree-close/scripts/check-unarchived.js +77 -48
  43. package/cbb/worktrees/skills/cbb-worktree-close/scripts/delete-branches.js +147 -136
  44. package/cbb/worktrees/skills/cbb-worktree-close/scripts/deprecate-branches.js +119 -0
  45. package/cbb/worktrees/skills/cbb-worktree-close/scripts/discover-apps.js +14 -5
  46. package/cbb/worktrees/skills/cbb-worktree-close/scripts/find-target-worktree.js +148 -96
  47. package/cbb/worktrees/skills/cbb-worktree-close/scripts/find-workspace-root.js +8 -8
  48. package/cbb/worktrees/skills/cbb-worktree-close/scripts/junction-ai-config.js +192 -0
  49. package/cbb/worktrees/skills/cbb-worktree-close/scripts/merge-branches.js +580 -0
  50. package/cbb/worktrees/skills/cbb-worktree-close/scripts/remove-worktrees.js +44 -9
  51. package/cbb/worktrees/skills/cbb-worktree-close/scripts/safety-check.js +15 -4
  52. package/cbb/worktrees/skills/cbb-worktree-close/scripts/silence-popup.js +1 -1
  53. package/cbb/worktrees/skills/cbb-worktree-init/SKILL.md +131 -120
  54. package/cbb/worktrees/skills/cbb-worktree-init/scripts/check-env.js +1 -1
  55. package/cbb/worktrees/skills/cbb-worktree-init/scripts/check-repos.js +2 -2
  56. package/cbb/worktrees/skills/cbb-worktree-init/scripts/create-branches.js +33 -8
  57. package/cbb/worktrees/skills/cbb-worktree-init/scripts/create-worktrees.js +60 -46
  58. package/cbb/worktrees/skills/cbb-worktree-init/scripts/install-git-hook.js +70 -0
  59. package/cbb/worktrees/skills/cbb-worktree-init/scripts/junction-ai-config.js +192 -0
  60. package/cbb/worktrees/skills/cbb-worktree-init/scripts/parse-config.js +7 -7
  61. package/cbb/worktrees/skills/cbb-worktree-init/scripts/push-branches.js +3 -3
  62. package/cbb/worktrees/skills/cbb-worktree-init/scripts/push-core.js +1 -1
  63. package/cbb/worktrees/skills/cbb-worktree-init/scripts/silence-popup.js +1 -1
  64. package/cbb/worktrees/skills/cbb-worktree-init/scripts/sync-repos.js +11 -7
  65. package/cbb/worktrees/skills/cbb-worktree-init/scripts/update-gitignore.js +17 -5
  66. package/cbb/worktrees/skills/cbb-worktree-push/SKILL.md +213 -56
  67. package/cbb/worktrees/skills/cbb-worktree-push/scripts/check-unarchived.js +77 -0
  68. package/cbb/worktrees/skills/cbb-worktree-push/scripts/commit-worktrees.js +70 -12
  69. package/cbb/worktrees/skills/cbb-worktree-push/scripts/find-target-worktree.js +148 -96
  70. package/cbb/worktrees/skills/cbb-worktree-push/scripts/find-workspace-root.js +8 -8
  71. package/cbb/worktrees/skills/cbb-worktree-push/scripts/make-mr-urls.js +211 -0
  72. package/cbb/worktrees/skills/cbb-worktree-push/scripts/merge-branches.js +580 -0
  73. package/cbb/worktrees/skills/cbb-worktree-push/scripts/push-core.js +1 -1
  74. package/cbb/worktrees/skills/cbb-worktree-push/scripts/push-worktrees.js +67 -20
  75. package/cbb/worktrees/skills/cbb-worktree-push/scripts/silence-popup.js +1 -1
  76. package/config/{workspace-config.sample.json → CBB-WORKSPACE.sample.json} +1 -1
  77. package/config/openspec/schemas/spec-driven/schema.yaml +20 -0
  78. package/config/upstream-mirrors.json +5 -0
  79. package/external/frontend-design/.version +6 -0
  80. package/external/frontend-design/LICENSE.txt +177 -0
  81. package/external/frontend-design/SKILL.md +71 -0
  82. package/openspec/.version +2 -2
  83. package/openspec/commands/archive.md +15 -8
  84. package/openspec/commands/bulk-archive.md +13 -6
  85. package/openspec/commands/continue.md +0 -1
  86. package/openspec/commands/explore.md +1 -1
  87. package/openspec/commands/ff.md +1 -1
  88. package/openspec/commands/onboard.md +9 -4
  89. package/openspec/commands/update.md +11 -5
  90. package/openspec/commands/verify.md +57 -26
  91. package/openspec/skills/openspec-apply-change/SKILL.md +1 -1
  92. package/openspec/skills/openspec-archive-change/SKILL.md +16 -9
  93. package/openspec/skills/openspec-bulk-archive-change/SKILL.md +14 -7
  94. package/openspec/skills/openspec-continue-change/SKILL.md +1 -2
  95. package/openspec/skills/openspec-explore/SKILL.md +2 -2
  96. package/openspec/skills/openspec-ff-change/SKILL.md +2 -2
  97. package/openspec/skills/openspec-new-change/SKILL.md +1 -1
  98. package/openspec/skills/openspec-onboard/SKILL.md +10 -5
  99. package/openspec/skills/openspec-propose/SKILL.md +1 -1
  100. package/openspec/skills/openspec-sync-specs/SKILL.md +1 -1
  101. package/openspec/skills/openspec-update-change/SKILL.md +12 -6
  102. package/openspec/skills/openspec-verify-change/SKILL.md +59 -27
  103. package/package.json +3 -2
  104. package/superpowers/.version +3 -3
  105. package/superpowers/skills/writing-plans/SKILL.md +32 -20
  106. package/cbb/lib/install/codebuddy.js +0 -30
  107. package/cbb/worktrees/skills/cbb-worktree-init/scripts/find-workspace-root.js +0 -67
  108. package/config/workspace-agents.sample.md +0 -41
  109. package/superpowers/skills/writing-plans/plan-document-reviewer-prompt.md +0 -49
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 本地模式开工(ADR 0002):根 + 指定应用就地切需求分支。
4
+ *
5
+ * 用法:node start-local.js <ws_root> <branch> [--apps app1,app2] [--all-apps]
6
+ *
7
+ * 行为:
8
+ * 1. 前置自检:根与所有 .codespace/<app>/ 若任一检出在非主干分支 → 遗留态,
9
+ * 退出码 2(agent 应升级为用户决策:继续 / 收尾 / 升级 / 丢弃,不在本脚本内处理)
10
+ * 2. 参与仓库 = workspace + 应用子集:
11
+ * - --apps a,b → 指定应用(本地模式推荐:从需求语境提取涉及应用)
12
+ * - --all-apps → CBB-WORKSPACE.json 全量应用
13
+ * - 缺省 → 仅 workspace(后续重跑幂等补齐,与 worktree-init 增补语义一致)
14
+ * 3. 每个仓库:
15
+ * - 当前已在 <branch> → reused
16
+ * - 分支存在(未检出)→ git checkout
17
+ * - 否则 → git checkout -b <branch> <remote>/<main>(基线不取 HEAD,ADR 0002;
18
+ * remote 不可用回退本地 main/master,再回退 HEAD)
19
+ * - dirty 仓库切换失败 → 报错退出(不 stash 兜底,交给用户决策)
20
+ * 4. 参与的应用仓库幂等补装提交防线 hook(cbb setup 已装,此处兜底)
21
+ *
22
+ * 幂等:重跑同一 branch 安全(reused / checkout 跳过)。
23
+ *
24
+ * 退出码:
25
+ * 0 - 全部切换成功
26
+ * 1 - 参数错误
27
+ * 2 - 遗留态 / 部分切换失败
28
+ */
29
+
30
+ const { execSync } = require('child_process');
31
+ const fs = require('fs');
32
+ const path = require('path');
33
+ const { installTo: installGitHook } = require('./install-git-hook');
34
+
35
+ const wsRoot = process.argv[2];
36
+ const branch = process.argv[3];
37
+
38
+ if (!wsRoot || !branch) {
39
+ console.error('Usage: node start-local.js <ws_root> <branch> [--apps a,b] [--all-apps]');
40
+ process.exit(1);
41
+ }
42
+
43
+ function run(cmd, cwd) {
44
+ try {
45
+ return { ok: true, out: execSync(cmd, { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }).toString().trim() };
46
+ } catch (e) {
47
+ return { ok: false, err: (e.stderr ? e.stderr.toString() : e.message || String(e)).trim() };
48
+ }
49
+ }
50
+
51
+ /** 主干名:symbolic-ref 远端 HEAD → 本地 main/master 回退 */
52
+ function detectMain(cwd) {
53
+ const remotes = run('git remote', cwd);
54
+ if (remotes.ok && remotes.out) {
55
+ const r = remotes.out.split(/\r?\n/)[0];
56
+ const sym = run('git symbolic-ref "refs/remotes/' + r + '/HEAD"', cwd);
57
+ if (sym.ok && sym.out) return { remote: r, main: sym.out.replace('refs/remotes/' + r + '/', '') };
58
+ }
59
+ if (run('git rev-parse --verify --quiet refs/heads/main', cwd).ok) return { remote: null, main: 'main' };
60
+ if (run('git rev-parse --verify --quiet refs/heads/master', cwd).ok) return { remote: null, main: 'master' };
61
+ return { remote: null, main: null };
62
+ }
63
+
64
+ function currentBranch(cwd) {
65
+ const r = run('git branch --show-current', cwd);
66
+ return r.ok ? r.out : '';
67
+ }
68
+
69
+ // ── 1. 前置自检:遗留态检测 ───────────────────────────────
70
+ // 「非主干」且「不是本次 branch」才是遗留(别的需求没收尾);
71
+ // 已检出在本次 branch 上 = 幂等补齐场景,放行(reused)。
72
+
73
+ function isOnMain(cwd) {
74
+ const info = detectMain(cwd);
75
+ return info.main ? currentBranch(cwd) === info.main : false;
76
+ }
77
+
78
+ function isOnTarget(cwd) {
79
+ return currentBranch(cwd) === branch || isOnMain(cwd);
80
+ }
81
+
82
+ const leftovers = [];
83
+ if (!isOnTarget(wsRoot)) leftovers.push({ name: 'workspace', branch: currentBranch(wsRoot) });
84
+ const codeDir = path.join(wsRoot, '.codespace');
85
+ if (fs.existsSync(codeDir)) {
86
+ for (const name of fs.readdirSync(codeDir).sort()) {
87
+ const appDir = path.join(codeDir, name);
88
+ if (fs.existsSync(path.join(appDir, '.git')) && !isOnTarget(appDir)) {
89
+ leftovers.push({ name, branch: currentBranch(appDir) });
90
+ }
91
+ }
92
+ }
93
+ if (leftovers.length > 0) {
94
+ console.error('❌ 遗留态:以下仓库检出在非主干分支,先由用户决策处理(继续/收尾/升级/丢弃):');
95
+ leftovers.forEach(l => console.error(' - ' + l.name + ': ' + l.branch));
96
+ console.log('---');
97
+ console.log('LOCAL_START_FAILED', JSON.stringify({ reason: 'leftover', leftovers }));
98
+ process.exit(2);
99
+ }
100
+
101
+ // ── 2. 参与仓库集合 ───────────────────────────────────────
102
+
103
+ let appNames = [];
104
+ const argApps = process.argv.indexOf('--apps');
105
+ const argAll = process.argv.includes('--all-apps');
106
+ if (argApps >= 0) {
107
+ const raw = process.argv[argApps + 1] || '';
108
+ appNames = raw.split(',').map(s => s.trim()).filter(Boolean);
109
+ } else if (argAll) {
110
+ try {
111
+ const config = JSON.parse(fs.readFileSync(path.join(wsRoot, 'CBB-WORKSPACE.json'), 'utf-8'));
112
+ appNames = (config.apps || []).map(a => a.name);
113
+ } catch (e) {
114
+ console.error('❌ 读取 CBB-WORKSPACE.json 失败:' + e.message);
115
+ process.exit(2);
116
+ }
117
+ }
118
+
119
+ const targets = [{ name: 'workspace', dir: wsRoot }];
120
+ for (const name of appNames) {
121
+ const appDir = path.join(wsRoot, '.codespace', name);
122
+ if (!fs.existsSync(path.join(appDir, '.git'))) {
123
+ console.error('❌ 应用仓库不存在:.codespace/' + name + '(请检查 CBB-WORKSPACE.json 或先跑 cbb setup)');
124
+ process.exit(2);
125
+ }
126
+ targets.push({ name, dir: appDir });
127
+ }
128
+
129
+ // ── 3. 逐仓库切换 ─────────────────────────────────────────
130
+
131
+ const results = [];
132
+ let hasFailure = false;
133
+
134
+ for (const t of targets) {
135
+ const cur = currentBranch(t.dir);
136
+ if (cur === branch) {
137
+ console.log('⏭️ ' + t.name + ': 已在 ' + branch + '(reused)');
138
+ results.push({ name: t.name, status: 'reused' });
139
+ continue;
140
+ }
141
+
142
+ const exists = run('git rev-parse --verify --quiet refs/heads/' + branch, t.dir).ok;
143
+ const info = detectMain(t.dir);
144
+ let cmd;
145
+ let base = 'HEAD';
146
+ if (exists) {
147
+ cmd = 'git checkout "' + branch + '"';
148
+ base = '(existing)';
149
+ } else {
150
+ if (info.remote && info.main) base = info.remote + '/' + info.main;
151
+ else if (info.main) base = info.main;
152
+ cmd = 'git checkout -b "' + branch + '" "' + base + '"';
153
+ }
154
+ const r = run(cmd, t.dir);
155
+ if (r.ok) {
156
+ console.log('✅ ' + t.name + ': ' + (exists ? 'checkout' : 'created from ' + base) + ' → ' + branch);
157
+ results.push({ name: t.name, status: exists ? 'switched' : 'created', base });
158
+ } else {
159
+ console.error('❌ ' + t.name + ': 切换失败 — ' + r.err);
160
+ results.push({ name: t.name, status: 'failed', error: r.err });
161
+ hasFailure = true;
162
+ }
163
+ }
164
+
165
+ // ── 4. 参与应用补装提交防线 hook(幂等)────────────────────
166
+
167
+ const hookResults = [];
168
+ for (const t of targets) {
169
+ if (t.name === 'workspace') continue; // 根仓库 hook 由 cbb setup 安装
170
+ hookResults.push({ name: t.name, status: installGitHook(t.dir) });
171
+ }
172
+
173
+ console.log('---');
174
+ console.log('LOCAL_START_RESULT', JSON.stringify({
175
+ branch,
176
+ results,
177
+ hooks: hookResults,
178
+ worktreeHint: '如需升级隔离:重跑 cbb-worktree-init(分支已存在自动复用,worktree 从分支创建,提交随身带走)',
179
+ }));
180
+ process.exit(hasFailure ? 2 : 0);
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: cbb-worktree-close
3
- description: "关闭/清理/删除需求工作空间的 worktree 和分支。触发:用户明确说关闭/清理/删除 worktree、结束隔离开发、合并完要清理。禁止在 cbb-worktree-init 执行后自动调用——只有用户主动要求关闭时才触发。"
3
+ description: "关闭/清理/删除需求工作空间的 worktree 和分支。内置收尾合并门禁(ADR 0007):未合并分支必须先经 MR 或直合并交付,或由用户确认废弃并重命名为 <分支>-deprecated,否则不允许关闭。触发:用户明确说关闭/清理/删除 worktree、结束隔离开发、合并完要清理。禁止在 cbb-worktree-init 执行后自动调用——只有用户主动要求关闭时才触发。"
4
4
  ---
5
5
 
6
6
  # Close Worktree(跨平台脚本化版本 v2.0)
@@ -12,12 +12,14 @@ description: "关闭/清理/删除需求工作空间的 worktree 和分支。触
12
12
  ## 核心原则
13
13
 
14
14
  - 本技能的所有原子操作封装为 `scripts/*.js` Node.js 脚本,**跨平台执行**
15
- - 本技能是 `cbb-worktree-init` 的逆操作:共用 `workspace-config.json`、共用 `.worktrees/worktree-<需求名>/` 物理直聚命名、共用统一的分支名(不加前缀)
16
- - App worktree **物理嵌套**在 workspace worktree 内(`.worktrees/worktree-<需求名>/<应用名>/`),但 git 上属于各自仓库的独立 worktree
15
+ - 本技能是 `cbb-worktree-init` 的逆操作:共用 `CBB-WORKSPACE.json`、共用 `.worktrees/worktree-<需求名>/` 物理直聚命名、共用统一的分支名(不加前缀)
16
+ - App worktree **物理嵌套**在 workspace worktree 的 `.codespace/` 容器内(`.worktrees/worktree-<需求名>/.codespace/<应用名>/`,镜像空间根布局,ADR 0003),但 git 上属于各自仓库的独立 worktree
17
17
  - 只操作 `.worktrees/worktree-<需求名>` 这一类精确路径,**绝不碰其它 worktree**(如 IDE/工具自动创建的 `~/.qoder/worktree/...` 会话 worktree)
18
- - 通过检查 `.worktrees/worktree-<需求名>/<应用名>/` **是否实际存在**来确定关联应用——创建时用户可能只关联了 `workspace-config.json` 中的部分应用,不能假设全部应用都关联
18
+ - 通过检查 `.worktrees/worktree-<需求名>/.codespace/<应用名>/` **是否实际存在**来确定关联应用——创建时用户可能只关联了 `CBB-WORKSPACE.json` 中的部分应用,不能假设全部应用都关联
19
19
  - **顺序硬约束:先移除所有 worktree,再删除分支。** 目标分支当前正检出于 worktree 中,先删分支会报 `error: Cannot delete branch '<分支>' checked out at '<worktree 路径>'`
20
- - **破坏性操作必须先报告、经用户确认才执行**:未提交修改、未合并/未推送的提交,绝不静默 `--force` 或 `-D` 丢弃
20
+ - **收尾合并门禁(ADR 0007)**:关闭前每个参与仓库必须为"已合并"(复用 `safety-check` 的 `merged` 判定,含 release 中转 / squash 兜底)。未合并时由用户在门禁处三选一——**提 MR**(人工浏览器合入)/ **直合并**(agent 执行 `merge-branches.js`,前置:已归档 + 已提交)/ **确认废弃**(`deprecate-branches.js` 把远程分支重命名为 `<分支>-deprecated`)。**硬门禁:未合并且非废弃一律不允许关闭**
21
+ - **合并成功后自动删分支(ADR 0007)**:交付完成(MR 已合并或直合并成功)后自动删各参与仓库的本地 + 远程 feature 分支(删除前输出将删清单);"废弃"出口则保留分支(仅远程重命名)
22
+ - **破坏性操作必须先报告、经用户确认才执行**:未提交修改、未确认废弃的分支,绝不静默 `--force` 或 `-D` 丢弃
21
23
  - 软性检查失败时报告详情,询问用户是否继续
22
24
  - 输入约定:凡是需要用户自由输入文本(如分支名、确认)的场景,**必须直接在对话中提问并等待用户回复**,禁止使用 `AskUserQuestion` 工具;`AskUserQuestion` 仅用于固定选项场景(选择归档哪些 change、是否丢弃未提交修改)
23
25
 
@@ -34,7 +36,7 @@ node "<skill_dir>/scripts/<name>.js" <参数>
34
36
  ```
35
37
 
36
38
  > - `<skill_dir>` = 本 SKILL.md 所在目录的绝对路径(AI 从读取路径即可推出)
37
- > - **cwd 必须保持在工作空间根目录**:脚本按 cwd 定位 `workspace-config.json` 与 git 仓库
39
+ > - **cwd 必须保持在工作空间根目录**:脚本按 cwd 定位 `CBB-WORKSPACE.json` 与 git 仓库
38
40
  > - 不要用 `./scripts/...` 裸相对路径(AI 的 cwd 不是技能目录,会解析失败)
39
41
 
40
42
  ---
@@ -62,9 +64,9 @@ node "<skill_dir>/scripts/find-workspace-root.js"
62
64
  - 退出码 0 + 末行(纯路径) → WS_ROOT 绝对路径
63
65
  - 退出码 1 → 未找到工作空间根目录,**终止流程**
64
66
 
65
- > ⓘ **为什么不用 workspace-config.json 存在性判断**:init-worktree 会把 workspace-config.json 提交到主分支,`git worktree add` 会自动 checkout 副本到 worktree 目录中。若用 workspace-config.json 存在性判断,在 worktree 内执行时会错误地把 worktree 目录当作 WS_ROOT。使用 `git worktree list` 的第一行(git 契约上始终为主仓库绝对路径)则不受该问题影响。
67
+ > ⓘ **为什么不用 CBB-WORKSPACE.json 存在性判断**:init-worktree 会把 CBB-WORKSPACE.json 提交到主分支,`git worktree add` 会自动 checkout 副本到 worktree 目录中。若用 CBB-WORKSPACE.json 存在性判断,在 worktree 内执行时会错误地把 worktree 目录当作 WS_ROOT。使用 `git worktree list` 的第一行(git 契约上始终为主仓库绝对路径)则不受该问题影响。
66
68
 
67
- > close-worktree 是 init-worktree 的逆操作,但本步骤的 WS_ROOT 定位逻辑与 init-worktree 不同:init-worktree 强制根目录执行(不需要处理 worktree 场景),close-worktree 支持 worktree 内执行(需要绕开 workspace-config.json 副本陷阱)。
69
+ > close-worktree 是 init-worktree 的逆操作,但本步骤的 WS_ROOT 定位逻辑与 init-worktree 不同:init-worktree 强制根目录执行(不需要处理 worktree 场景),close-worktree 支持 worktree 内执行(需要绕开 CBB-WORKSPACE.json 副本陷阱)。
68
70
 
69
71
  ---
70
72
 
@@ -97,26 +99,27 @@ node "<skill_dir>/scripts/find-target-worktree.js "<ws_root>"
97
99
  node "<skill_dir>/scripts/discover-apps.js "<ws_root>" "<branch>"
98
100
  ```
99
101
 
100
- 脚本自动从 `workspace-config.json` 读取应用列表,扫描 `.worktrees/worktree-<branch>/<应用名>/` 实际存在的子目录。
102
+ 脚本自动从 `CBB-WORKSPACE.json` 读取应用列表,扫描 `.worktrees/worktree-<branch>/.codespace/<应用名>/`(兼容 ≤beta.23 旧平铺 `<应用名>/`)实际存在的子目录。
101
103
 
102
104
  **解读末行 `DISCOVER_RESULT`**:
103
105
  - `workspace` 字段:workspace worktree 绝对路径(AI agent 记录供后续命令使用)
104
106
  - `apps` 数组:实际存在的应用 worktree 列表(每项含 `name`, `repo`, `side`, `wt_path`, `app_path`)
105
107
 
106
- > 说明:app worktree 物理嵌套在 workspace worktree 内,移除 workspace worktree 时嵌套的子目录会一并消失,但各 app 仓库的 git worktree 元数据(`.git/worktrees/`)仍需通过 `git worktree remove` 逐个清理。AI 配置只装在业务空间根(单层安装),worktree 内没有 AI 配置目录,无需特殊处理。
108
+ > 说明:app worktree 物理嵌套在 workspace worktree 的 `.codespace/` 容器内,移除 workspace worktree 时嵌套的子目录会一并消失,但各 app 仓库的 git worktree 元数据(`.git/worktrees/`)仍需通过 `git worktree remove` 逐个清理。AI 配置经 junction 链入 worktree(只读内容子目录指向空间根唯一物理副本),关闭时由 remove-worktrees.js 在物理删除前**显式拆除**(只解除链接、源文件零接触),见 4.2 与附录 `junction-ai-config.js`。
107
109
 
108
110
  ### 1.3 检查未归档 change
109
111
 
110
112
  ```bash
111
- node "<skill_dir>/scripts/check-unarchived.js "<ws_root>"
113
+ node "<skill_dir>/scripts/check-unarchived.js "<ws_root>" "<branch>"
112
114
  ```
113
115
 
114
- 脚本扫描 `openspec/changes/` 目录,筛选出非 `archive/` 子目录的 change 名称列表。
116
+ 脚本**双扫**(ADR 0002):空间根当前检出的 `openspec/changes/`(本地模式产物)与 `.worktrees/worktree-<branch>/openspec/changes/`(worktree 模式产物),返回未归档 change 名称合集。
115
117
 
116
118
  **解读末行 `UNARCHIVED_RESULT`**:
117
- - `changes`:未归档 change 名称数组(空数组 = 全部已归档)
118
- - 退出码 0 → 检查完成(无论是否有未归档 change)
119
- - 退出码 1 → `openspec/changes/` 目录不存在,视为无未归档 change
119
+ - `changes`:未归档 change 名称合集(空数组 = 两处全部已归档)
120
+ - `detail`:每个 change 的来源(`root` / `worktree` / `both`)——Step 3.2 归档与 Step 4.1 执行归档时按来源定位目录
121
+ - 退出码 0 → 检查完成(无论是否有未归档 change;目录不存在视为该处无 change)
122
+ - 退出码 1 → 参数错误
120
123
 
121
124
  > ⓘ 本检查为信息收集性质,不阻断流程。结果将在 Step 3 用户决策时展示。
122
125
 
@@ -211,70 +214,95 @@ AskUserQuestion({
211
214
 
212
215
  **归档执行逻辑**(用户选择归档后,在 Step 4 中依次对每个待归档 change 执行):
213
216
 
217
+ > 落点按 Step 1.3 `detail` 的来源决定:`root` → 在 `<ws_root>/openspec/changes/` 执行;`worktree` → 在 `.worktrees/worktree-<branch>/openspec/changes/` 执行;`both` → 先向用户确认以哪份为准(通常 worktree 内为最新)。
218
+
214
219
  1. `openspec status --change "<name>" --json` → 检查 artifacts 完成状态
215
220
  2. 读取 `tasks.md` → 检查任务完成状态
216
221
  3. 检查 `openspec/changes/<name>/specs/` → 判断 delta specs 是否需要同步
217
- 4. 执行归档(`mv openspec/changes/<name> openspec/changes/archive/YYYY-MM-DD-<name>`)
222
+ 4. 执行归档(`mv <来源目录>/openspec/changes/<name> <来源目录>/openspec/changes/archive/YYYY-MM-DD-<name>`)
218
223
 
219
224
  **逐项确认规则**:
220
225
  - 若某个 change 有 artifacts 或 tasks 未完成 → 对该 change 单独弹出确认:*"<name> 有 N 个任务未完成,仍要归档吗?"*
221
226
  - 若 delta specs 需要同步 → 对该 change 单独提示同步选项(同步 / 跳过同步直接归档)
222
227
  - 每个 change 的归档结果独立记录(✅ 已归档 / ⏭️ 跳过 / ❌ 失败)
223
228
 
224
- ### 3.3 分支处理方式(默认保留,不询问)
229
+ ### 3.3 未推送 commit 处理(仅当 2.4 任一 worktree `unpushed_count > 0` 时出现)
225
230
 
226
- 历史版本此步骤会询问"保留 / 删本地 / 删本地+远程",但绝大多数场景仅需移除 worktree、自动保留分支;频繁弹窗会打断 close-worktree 的批量清理节奏。改为 **默认 `MODE = keep`,不再触发 `AskUserQuestion`**。
231
+ **场景**:用户在 worktree 中做了 commit,但没有调 `cbb-worktree-push` 推送,本地有 ahead 提交。
227
232
 
228
- **默认行为**:
233
+ **报告**:
234
+ ```
235
+ ⚠️ 检测到未推送的提交(ahead of origin/<branch>):
236
+ - workspace: 3 个 commit
237
+ - ep-user-info-server: 1 个 commit
229
238
 
230
- - `MODE = keep` → 仅移除 worktree,**本地分支与远程分支都不动**
231
- - Step 2 安全评估结果(合并状态 / 远程存在性)仅用于内部决策展示,**不再弹出分支处理问卷**
239
+ 请选择处理方式:
240
+ [先推送](推荐)调 cbb-worktree-push 推送所有 ahead 提交
241
+ [继续] 不单独推送,直接进入合并门禁(走「直合并」时会随合并一并推送到主干)
242
+ [中止] 终止流程,我先手动处理
243
+ ```
232
244
 
233
- **逃生通道**(确需主动删分支时手动调用):
245
+ - 用户选择「先推送」→ AI 引导执行 `cbb-worktree-push` skill(或 `/cbb-worktree-push`),完成后**重新跑** Step 2 safety-check 确认 `unpushed_count = 0` 再回到本步骤
246
+ - 用户选择「继续」→ 进入 Step 3.4;注意:走「提 MR」出口时会因 feature 未推送而失败,需回到本步骤先推送
247
+ - 用户选择「中止」→ 终止流程
234
248
 
235
- 如用户在 Step 1.1 选定要关闭的 worktree 时同步要求"删除分支",或运行后需要清理本地/远程分支,可手动调用脚本:
249
+ > ⓘ 走「直合并」出口时,ahead 提交会被合并进主干并随 `git push origin <主干>` 一并推送,无需单独推送 feature 分支。
236
250
 
237
- ```bash
238
- # 仅删本地分支(已合并自动 -d,未合并自动 -D;含 squash 合并 -D 兜底)
239
- node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<branch>" local
251
+ ### 3.4 合并门禁决策(核心 · ADR 0007)
240
252
 
241
- # 删本地 + 远程分支
242
- node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<branch>" local_remote
243
- ```
253
+ 依据 Step 2 的 `merged` 判定(每个参与仓库 `merged !== null` 即已合并,含 `main` / `origin/main` / `main-via-release-*`):
244
254
 
245
- **调用前的强警告**(调用方需自评):
255
+ **情况 A:全部参与仓库已合并** → 无交互,直接进入 Step 4(合并成功后自动删分支)。
246
256
 
247
- - **未合并分支**(Step 2.2 `merged: null`):删除会永久丢失未合并的提交,必须二次确认
248
- - **远程分支**:删除后无法直接恢复,需确认其他仓库/同事是否还在引用
249
- - **推荐做法**:默认 `keep` 关闭 worktree 即可,确有必要再单独执行脚本;这样误删风险被显式手动动作兜住
257
+ **情况 B:存在未合并仓库** → 硬门禁触发,`AskUserQuestion` 三选一(**不允许"未合并且非废弃"地关闭**):
250
258
 
251
- > 合并代码到主分支属于另一项工作(参见 `finishing-a-development-branch`),本技能不代为合并;若用户其实想合并,引导其先完成合并再回来关闭。
259
+ ```
260
+ 以下仓库的需求分支尚未交付(未合并到主干):
261
+ - workspace(merged: null)
262
+ - ep-foo-app(merged: null)
263
+
264
+ 请选择交付方式:
265
+ [提 MR](推荐) 生成预填合并请求链接,由人工在浏览器合入
266
+ [直合并] 由 agent 直接合并到主干并推送远端(需你确认)
267
+ [确认废弃] 该分支不再交付,远程重命名为 <分支>-deprecated
268
+ ```
252
269
 
253
- ### 3.4 未推送 commit 处理(仅当 2.4 任一 worktree `unpushed_count > 0` 时出现)
270
+ **出口一 · 提 MR(推荐)**
254
271
 
255
- **场景**:
256
- - 用户在 worktree 中做了 commit,但**没有调** `cbb-worktree-push` 推送
257
- - 本地有 ahead 提交未推送到 origin
258
- - 一旦 close worktree + 后续走 `local` / `local_remote` 删分支,ahead 提交可能永久丢失(取决于上游 `delete-branches.js` 行为)
272
+ 1. 先确保 feature 已推送(Step 3.3 未推送时先推送)
273
+ 2. **引导用户运行 `cbb-worktree-push`**(说"提交并推送"):其 Step 7 会列出目标分支候选(`git branch -r` 现场候选,**禁止 AI 代决**)并生成 MR 预填链接(可选自动开浏览器);本技能不自带 MR 链接脚本
274
+ 3. **中止 close**:提示"合并完成后重跑本技能"。**不执行任何移除 / 删除**
259
275
 
260
- **报告**:
276
+ **出口二 · 直合并(`merge-branches.js`)**
277
+
278
+ - **前置(Q18)**:3.1 无未提交修改、3.2 已归档并提交——**合并前必须已归档 + 已提交**(两者先后不限)
279
+ - 执行(`--apps` 传 Step 1.2 发现的参与应用):
280
+
281
+ ```bash
282
+ node "<skill_dir>/scripts/merge-branches.js" "<ws_root>" "<branch>" --apps "<app1,app2>"
261
283
  ```
262
- ⚠️ 检测到未推送的提交(ahead of origin/<branch>):
263
- - workspace: 3 个 commit
264
- - ep-user-info-server: 1 个 commit
265
- - ep-baz-app: 2 个 commit
266
284
 
267
- 请选择处理方式:
268
- [先推送](推荐)调 cbb-worktree-push 推送所有 ahead 提交
269
- [接受丢失] 继续关闭,ahead 提交在删分支后永久丢失
270
- [中止] 终止流程,我先手动处理
285
+ - 两阶段:预检(主干可探测 / 主干不领先 origin / 分支存在;**检出未提交改动不拦截**)→ 执行(autostash 让路 → checkout 主干 → pull --ff-only → merge --no-ff → push origin 主干 → **自动恢复暂存**;执行失败也先恢复暂存再停)
286
+ - 解读末行 `MERGE_RESULT`:
287
+ - 退出码 0 → 交付完成,继续 Step 4
288
+ - `need-mr`(`branch-protected`:远端主干受保护)→ 远端保护是常态,**改用出口一**(不是故障)
289
+ - `failed`(含 `merge-failed` / `push-failed`)→ 输出失败明细,阻塞并询问用户(修复后重跑;重跑幂等)
290
+ - `failed` 且 `not-on-main`(空间根检出不是主干:被其他需求占用或异常)→ 先让该占用回到主干(收尾本地需求)或改用出口一;**不要用 `--allow-checkout`**(会切走占用者的工作区)
291
+ - `MERGED` 但含 `AUTOSTASH_PENDING`(退出码 2)→ **交付已完成**,只是检出上未提交改动恢复遇冲突(脚本按"后提交者解决"保留了 stash 条目与冲突标记):列出冲突文件,`AskUserQuestion` 二选一——**① agent 解决**(逐文件消除冲突标记,双侧意图都保留、拿不准处询问;完成后 `git stash drop` 清掉保留条目;**不代提交**)/ **② 用户手动**(给出冲突文件与 `git stash drop` 收尾指引,等用户确认);处理完再继续 Step 4。任何情况下**不得 drop 未处理的 stash 条目、不得 checkout 丢弃用户改动**
292
+
293
+ **出口三 · 确认废弃(`deprecate-branches.js`)**
294
+
295
+ - **二次确认**(远程重命名不可逆):`AskUserQuestion` 固定选项"确认废弃 / 中止"
296
+ - 执行:
297
+
298
+ ```bash
299
+ node "<skill_dir>/scripts/deprecate-branches.js" "<ws_root>" "<branch>" --apps "<app1,app2>"
271
300
  ```
272
301
 
273
- - 用户选择「先推送」→ AI 引导用户执行 `cbb-worktree-push` skill(或 `/cbb-worktree-push`),完成后**重新跑** Step 2 safety-check 确认 `unpushed_count = 0` 再回到本步骤
274
- - 用户选择「接受丢失」→ 必须二次确认(ahead 提交真的会丢),确认后继续 Step 4
275
- - 用户选择「中止」→ 终止流程
302
+ - 效果:远程存在该分支的仓库,重命名 `<分支>` → `<分支>-deprecated`(**保留历史、不删提交**);**本地分支保持原名不动**
303
+ - 解读末行 `DEPRECATE_RESULT`:`failed` → 报告并终止
276
304
 
277
- > ⓘ **为什么必须二次确认**:close-worktree 的默认 MODE=keep 不会删分支,ahead 提交仍在本地;只有用户主动走 `local` / `local_remote` 删分支模式才会真正丢。AI 必须先在报告中讲清楚后果,再询问用户决策。
305
+ > ⓘ 门禁判定复用 `safety-check` 的 `merged`:他人经 MR 或 `release-*` 中转合入后,重跑本技能即自动放行,无需重复交付。
278
306
 
279
307
  ---
280
308
 
@@ -302,11 +330,12 @@ node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<branch>" local_remot
302
330
  node "<skill_dir>/scripts/remove-worktrees.js "<ws_root>" "<branch>" "<force>"
303
331
  ```
304
332
 
305
- **三层降级策略**:`git worktree remove` 实际包含两个动作——①清理 git 元数据(`.git/worktrees/`)②删除物理目录。当 IDE 占用目录时 ② 会被 Windows 文件锁阻塞,但 ① 永远不受影响。脚本将这两个动作解耦:
333
+ **移除流程**:`git worktree remove` 实际包含两个动作——①清理 git 元数据(`.git/worktrees/`)②删除物理目录。当 IDE 占用目录时 ② 会被 Windows 文件锁阻塞,但 ① 永远不受影响。脚本将这两个动作解耦,并在物理删除前**先显式拆除 AI 配置 junction**(`junction-ai-config.js --remove`:只解除链接、目标在空间根内校验、源文件零接触):
306
334
 
307
- - **第 1 层(正常路径):** `git worktree remove` 一步完成元数据 + 物理目录清理
308
- - **第 2 层(解耦清理):** 当目录被锁时,先 `prune` 清理 git 元数据(始终成功),再 `rmdir`/`rm -rf` 尝试物理删除
309
- - **第 3 层(延迟清理):** Windows 下 IDE 持有排他句柄时触发。元数据已清理保证分支删除不受影响;物理目录留给 init-worktree 下次创建时自愈(见 init Step 4.0)
335
+ - **第 1 层(正常路径):** junction 拆除 → `git worktree remove` 一步完成元数据 + 物理目录清理,并**验证物理目录已真消失**
336
+ - **第 1.5 层(git 静默失败兜底):** 实测 Windows 上 `git worktree remove` 遇 junction 时可能**返回成功但物理目录残留**(本次修复前 junction 遗留的根因)——检测目录仍存在即自动走第 2 层
337
+ - **第 2 层(解耦清理):** 先 `prune` 清理 git 元数据(始终成功),再 `fs.rmSync`(Node 原生,对 reparse point 只删链接不穿透)尝试物理删除
338
+ - **第 3 层(延迟清理):** Windows 下 IDE 持有排他句柄时触发。元数据已清理保证分支删除不受影响;物理目录**不会被 init 自动删除**(0.0.3-beta.11 起 init 只 prune 元数据、绝不删目录,防误删活跃 worktree),需用户关闭 IDE 后手动删除,或重跑 close 补删
310
339
 
311
340
  **解读末行 `REMOVE_RESULT`**:
312
341
  - `has_locked: false` → 全部清理成功
@@ -320,48 +349,52 @@ node "<skill_dir>/scripts/remove-worktrees.js "<ws_root>" "<branch>" "<force>"
320
349
 
321
350
  所有 worktree 已成功移除后,如果当前 shell 的工作目录位于**已被关闭的 worktree 内**,需要主动让 IDE 窗口切回 `WS_ROOT`,避免 IDE 窗口指向已删除的目录。
322
351
 
323
- > ⓘ **本节是 close-worktree 专属逻辑**:与 init-worktree 的 Step 5 不共享、不交叉同步。init-worktree 负责"创建后**在新窗口打开** worktree",本节负责"关闭后切回根目录",两者职责独立。
352
+ > ⓘ **本节是 close-worktree 专属逻辑**:与 init-worktree 的 Step 5 不共享、不交叉同步。init-worktree 负责"创建后把开发会话开进 worktree",本节负责"关闭后切回根目录",两者职责独立。
324
353
 
325
354
  **AI agent 行为**:
326
- 1. 检查当前 cwd 是否在被关闭的 worktree 内
327
- 2. 仅当匹配时,调用对应 IDE 的 `--reuse-window` 命令切回 WS_ROOT:
355
+ 1. 检查当前会话 / IDE 窗口是否指向被关闭的 worktree
356
+ 2. **OpenCode 等支持会话搬移的工具**:agent 亲自经 code-mode 调用 `opencode.session_move` 把会话切回 `<ws_root>`——自动完成、上下文保留、**不列入用户待办**,切完核对工作目录
357
+ 3. **Qoder / Cursor / VS Code**:调用对应 IDE 的 `--reuse-window` 命令切回 WS_ROOT:
328
358
  - Qoder → `qoder --reuse-window "<ws_root>"`
329
359
  - Cursor → `cursor --reuse-window "<ws_root>"`
330
360
  - VS Code → `code --reuse-window "<ws_root>"`
331
- 3. **JetBrains 跳过主动切换**——`idea64 <path>` 会启动新进程或提示"打开/在新窗口打开",让用户手动操作
361
+ 4. **JetBrains 跳过主动切换**——`idea64 <path>` 会启动新进程或提示"打开/在新窗口打开",让用户手动操作
332
362
 
333
363
  **关键点**:
334
- - **只在 pwd 匹配被关闭的 worktree 时触发**——如果 IDE 窗口本身在 WS_ROOT 内,或会话位于其他目录,再切一次是多余的
364
+ - **只在会话 / 窗口匹配被关闭的 worktree 时触发**——如果已在 WS_ROOT 内,再切一次是多余的
365
+ - **OpenCode 会话切回是零成本自动动作**——`session_move` 复用当前会话(上下文保留、无新窗口),agent 亲自做,严禁推给用户
335
366
  - **仅切 Qoder / Cursor / VS Code**——这三者的 `--reuse-window` 是轻量级"切目录",不重启 IDE
336
367
  - **不会产生孤立窗口**——`--reuse-window` 是复用当前窗口切换目录,**不会打开新窗口**。即使 IDE 已开着指向 WS_ROOT 的其他窗口,本步骤也不会重复创建,仅是把当前指向已删除目录的窗口切走
337
- - **失败不致命**——`--reuse-window` 失败时仅警告,后续分支删除不受影响
368
+ - **失败不致命**——`session_move` / `--reuse-window` 失败时仅警告,后续分支删除不受影响
338
369
 
339
- ### 4.4 分支处理(默认跳过,按需手动调用)
370
+ ### 4.4 分支处理(合并成功后自动删除 · ADR 0007)
340
371
 
341
- 默认 `MODE = keep`(见 Step 3.3),本步骤**不主动调用** `delete-branches.js`,本地分支与远程分支均保留。
372
+ **按 Step 3.4 的门禁出口决定**:
342
373
 
343
- 如用户在 Step 1.1 选定 worktree 时已要求"删除分支",或运行后需要单独清理,可在此步骤**手动调用**:
374
+ - **交付完成**(情况 A 已合并,或出口二直合并成功)→ **自动删除**各参与仓库的本地 + 远程 feature 分支:
344
375
 
345
376
  ```bash
346
- node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<branch>" local
347
- # 或
348
377
  node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<branch>" local_remote
349
378
  ```
350
379
 
380
+ - **出口三废弃** → **保留分支**(远程已重命名为 `<分支>-deprecated`),**不调用** `delete-branches.js`
381
+
382
+ **删除前必做**:输出"即将删除的本地 / 远程分支清单"(逐仓库),透明可见。
383
+
351
384
  脚本内部对每个仓库:
352
385
  - **本地分支**:检测是否已合并(复用 safety-check 的合并判定算法)
353
386
  - 已合并 → `git branch -d`(安全删除)
354
387
  - 未合并 → `git branch -D`(强制删除)
355
388
  - squash 合并可能让 `-d` 失败 → 自动回退到 `-D`(squash 合并同样安全)
356
- - **远程分支**(仅 `local_remote` 模式):`git push <remote> --delete <branch>`
389
+ - **远程分支**(`local_remote` 模式):`git push <remote> --delete <branch>`
357
390
  - 分支不存在时 push --delete 会报错,按"已删除/本就无"处理即可
358
391
  - 远程引用已在 Step 2.0 主动 fetch 刷新并持久化在 .git,此处直接读取无需重复 fetch
359
392
 
360
- **解读末行 `BRANCH_DELETE_RESULT`**:
361
- - `mode`:keep / local / local_remote
362
- - `ok`:是否全部成功
393
+ **解读末行 `BRANCH_DELETE_RESULT`**:`mode`(keep / local / local_remote)、`ok`。
394
+
395
+ **失败处理(ADR 0007)**:清理阶段(删除分支)失败**报告并继续其余仓库,不回滚**——交付已达成,清理不净不该阻塞(与合并阶段的"失败即停"不同)。
363
396
 
364
- > 默认跳过分支删除是安全红线的取舍:未合并分支误删会永久丢失提交、远程分支误删会冲击其他仓库/同事。保留为显式手动动作可让调用方在执行前再做一次审视。
397
+ **顺序硬约束**:必须在 Step 4.2(移除 worktree)**之后**执行——分支正检出于 worktree,先删会报 `Cannot delete branch ... checked out at ...`。
365
398
 
366
399
  > 关于 `.gitignore`:创建时把 `.worktrees/` 加入 `.gitignore` 并**提交到了 feature 分支**(非主分支)。
367
400
  > - 分支未合并、被 `-D` 删除 → 这些 commit 随分支一起消失,主分支的 `.gitignore` 不受影响
@@ -381,6 +414,10 @@ git -C "<ws_root>" worktree list
381
414
 
382
415
  目标 worktree 应已不在列表中。
383
416
 
417
+ **产物留存检查(打包产物 · 不自动删除)**:检查 `<ws_root>/.build/<branch>/` 是否存在(`test -d`),并统计 `.build/` 全景(需求目录数与总体积,Node 或 `du -sh` 等价实现)。**关闭流程绝不自动删除 `.build/` 内容**(打包产物是交付后的取件来源,删错不可逆);用户明确表示不需要时,才按其指定范围清理。
418
+
419
+ **工作区回根交接(必须执行)**:若用户的开发会话工作在 worktree 内(当前会话工作目录 = 已删除的 `<wt_path>`),此刻目录已悬空——**OpenCode 等支持会话搬移的工具由 agent 自动 `session_move` 切回 `<ws_root>`,不列入用户待办**;无该能力的工具才提示用户把工作区切回 `<ws_root>`(打开文件夹 / Open Folder / 切换工作目录),并在最终报告中给出该绝对路径。
420
+
384
421
  ---
385
422
 
386
423
  ## 最终报告
@@ -388,17 +425,24 @@ git -C "<ws_root>" worktree list
388
425
  ```
389
426
  ✅ 工作空间 worktree 已关闭
390
427
  当前目录:<工作空间根目录绝对路径>
428
+ 交付方式:已合并(MR 合入)/ 直合并(agent 合并并推送主干)/ 确认废弃
391
429
  已移除 worktree:
392
430
  - .worktrees/worktree-feature-xxx(workspace)
393
- - .worktrees/worktree-feature-xxx/ep-user-info-server(嵌套,ep-user-info-server 仓库)
394
- - .worktrees/worktree-feature-xxx/ep-user-management-app(嵌套,ep-user-management-app 仓库)
431
+ - .worktrees/worktree-feature-xxx/.codespace/ep-user-info-server(嵌套,ep-user-info-server 仓库)
432
+ - .worktrees/worktree-feature-xxx/.codespace/ep-user-management-app(嵌套,ep-user-management-app 仓库)
395
433
  未提交修改:已丢弃(--force)/ 无
396
434
  未归档 change 处理:
397
435
  ✅ ai-call-send-im-msg → 已归档到 archive/2026-07-08-ai-call-send-im-msg
398
436
  ⏭️ fix-payment-timeout → 用户选择跳过
399
437
  (无未归档 change 时不显示本节)
400
- 分支处理:feature-xxx 已保留(默认行为;如需删除分支,手动调用 scripts/delete-branches.js)
438
+ 分支处理:
439
+ - 交付完成 → 已删除本地 + 远程分支:feature-xxx(逐仓库列出)
440
+ - 已废弃 → 远程已重命名为 feature-xxx-deprecated(本地分支保留原名)
401
441
  剩余工作空间 worktree:<列出仍存在的,或 "无" >
442
+ 产物留存:.build/<branch>/(已保留;不需要可告诉我清理)
443
+ .build 全景:共 N 个需求目录 / ~X MB(可顺带清理已交付或已废弃需求)
444
+ (产物留支行仅在该目录存在时显示)
445
+ 👉 仅当所用工具不支持自动切根、且你的窗口仍停在已删除的 worktree 时,请手动切回工作空间根:<ws_root 绝对路径>
402
446
  Workspace cleaned up.
403
447
  ```
404
448
 
@@ -417,18 +461,24 @@ git -C "<ws_root>" worktree list
417
461
  ### 绝不允许
418
462
 
419
463
  - 先删分支再删 worktree(分支正检出于 worktree,会报 `Cannot delete branch ... checked out at ...`)
420
- - 未经用户确认用 `--force` 丢弃未提交修改,或主动调用 `delete-branches.js` 对未合并分支执行 `-D` 强删
464
+ - **未合并且未确认为废弃时关闭 worktree**(违反 ADR 0007 硬门禁)
465
+ - 未经用户确认用 `--force` 丢弃未提交修改;或对未确认废弃的未合并分支执行 `-D` 强删
466
+ - 未经二次确认执行 `deprecate-branches.js`(远程重命名不可逆)
467
+ - 直合并前跳过"已归档 + 已提交"前置校验
421
468
  - 操作 `.worktrees/worktree-<需求名>` 以外的任何 worktree(如 `~/.qoder/worktree/...` 会话 worktree)
422
469
  - 移除/删除前跳过 Step 2 安全评估
423
470
  - 删除工作空间根目录的 AI 配置目录(`.claude/`、`.qoder/`、`.opencode/` 等)
471
+ - 未经用户确认删除 `.build/` 内的打包产物(留存目录的清理须用户逐项确认;关闭流程绝不自动删)
424
472
  - 用 `AskUserQuestion` 收集自由文本(分支名等直接对话提问)
425
473
  - 手动改任何仓库的 `.gitignore`(关闭流程不碰它)
426
474
  - 运行环境 node 不可用时强行继续 → 改为 fail-fast 提示用户
427
475
 
428
476
  ### 必须执行
429
477
 
430
- - 读 `workspace-config.json` 取应用名列表;以 `.worktrees/worktree-<需求名>/<应用名>/` 实际存在性确定关联清单
478
+ - 读 `CBB-WORKSPACE.json` 取应用名列表;以 `.worktrees/worktree-<需求名>/.codespace/<应用名>/` 实际存在性确定关联清单
431
479
  - 移除/删除前执行安全评估(未提交修改 / 是否合并 / 是否推远程)并报告
480
+ - **按 Step 3.4 门禁三选一交付**;未合并且未确认为废弃时**禁止关闭**
481
+ - 交付完成(自动删分支)前**先输出将删清单**;清理阶段失败报告并继续,不回滚
432
482
  - 严格按 Step 4.2(移除 worktree)→ Step 4.4(删分支)顺序
433
483
  - 移除 workspace worktree 前先把会话移出(`cd "<WS_ROOT>" &&` 或 `git -C` 绝对路径)
434
484
  - 收尾切回 WS 根目录、`pwd` 确认
@@ -438,7 +488,7 @@ git -C "<ws_root>" worktree list
438
488
  - **合并判定依赖刷新后的远程引用,故 Step 2.0 先主动 `git fetch`**:feature 多由他人经 MR 合入远端,本地引用否则会陈旧误判
439
489
  - **并行块用临时文件收结果、不用共享变量**:原 bash 版本中 `&` 后台任务跑在独立子 shell;Node.js 同步执行无此问题
440
490
  - **`.gitignore` 无需清理**:创建时的 `.worktrees/` 条目提交在 feature 分支上,删分支时随分支消失(未合并)或无害保留(已合并)
441
- - **AI 配置只装在空间根(单层安装)**:worktree 内没有 AI 配置目录,关闭时无需处理
491
+ - **AI 配置在 worktree 内是 junction(链接)**:物理副本只在空间根(单层安装)。**不可依赖「随目录树消失」**——Windows 上 `git worktree remove` 遇 junction 会静默失败、残留整个目录树(实测复现)。关闭流程在物理删除前用 Node 显式拆除(只解除链接,目标在空间根内校验,源文件零接触)
442
492
  - **部分 `worktree remove` 失败**:已移除的无需回退,未移除的报告详情让用户处理后重跑
443
493
  - **Windows IDE 目录锁的三层降级**:`git worktree remove` 内部做两件事——①清元数据 ②删目录。Windows 文件锁只阻塞 ②,不阻塞 ①
444
494
 
@@ -452,7 +502,10 @@ git -C "<ws_root>" worktree list
452
502
  | `find-workspace-root.js` | 定位工作空间根目录(共享脚本,源仓库真源在 `_shared/scripts/`) | 无 | 0/1 |
453
503
  | `find-target-worktree.js` | 目标 worktree 识别(共享脚本,源仓库真源在 `_shared/scripts/`) | ws_root | 0/1/2 |
454
504
  | `discover-apps.js` | 发现关联应用 worktree | ws_root, branch | 0/1/2 |
455
- | `check-unarchived.js` | 检查未归档 change | ws_root | 0/1 |
505
+ | `check-unarchived.js` | 检查未归档 change(双扫空间根 + worktree) | ws_root, branch | 0/1 |
456
506
  | `safety-check.js` | 安全评估(合并判定 + 未提交 + 远程) | ws_root, branch | 0/1 |
457
- | `remove-worktrees.js` | 移除 worktree(三层降级) | ws_root, branch, force | 0/1 |
458
- | `delete-branches.js` | 删除分支(按 mode) | ws_root, branch, mode | 0/1/2 |
507
+ | `remove-worktrees.js` | 移除 worktree(先拆 junction,三层降级 + 残留验证) | ws_root, branch, force | 0/1 |
508
+ | `junction-ai-config.js` | AI 配置 junction 创建 / `--remove` 拆除(共享脚本,源仓库真源在 `_shared/scripts/`) | ws_root, wt_path, [--remove] | 0/1/2 |
509
+ | `delete-branches.js` | 删除分支(按 mode;交付完成后由 Step 4.4 自动以 local_remote 调用) | ws_root, branch, mode | 0/1/2 |
510
+ | `merge-branches.js` | 收尾合并门禁·直合并(预检 → merge --no-ff → push 主干,共享脚本,真源在 `_shared/scripts/`) | ws_root, branch, --apps / --target / --allow-checkout / --dry-run / --check | 0/1/2 |
511
+ | `deprecate-branches.js` | 收尾合并门禁·废弃(远程分支重命名为 `<分支>-deprecated`,共享脚本) | ws_root, branch, --apps | 0/1/2 |
@@ -3,14 +3,14 @@ require('./silence-popup');
3
3
  /**
4
4
  * cbb-worktree-close 环境健康探活
5
5
  *
6
- * close-worktree 必须在工作空间根目录或 worktree 内执行,且需找到 workspace-config.json。
7
- * 与 init/extend 探活同构,但额外校验 workspace-config.json 存在性。
6
+ * close-worktree 必须在工作空间根目录或 worktree 内执行,且需找到 CBB-WORKSPACE.json。
7
+ * 与 init 探活同构,但额外校验 CBB-WORKSPACE.json 存在性。
8
8
  *
9
9
  * 退出码:
10
10
  * 0 - 探活通过
11
11
  * 1 - 致命错误(git 不可用)
12
12
  * 2 - 当前不在 git 仓库
13
- * 3 - 未找到 workspace-config.json
13
+ * 3 - 未找到 CBB-WORKSPACE.json
14
14
  */
15
15
 
16
16
  const { execSync } = require('child_process');