@jspg-ai/coding-bb 0.0.3-beta.33 → 0.0.3-beta.35
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.
- package/cbb/dev-standards/rules/cbb-ai-behavior.md +0 -33
- package/cbb/dev-standards/rules/cbb-workspace-discipline.md +36 -0
- package/cbb/lib/install/init.js +3 -2
- package/cbb/worktrees/_shared/scripts/delete-branches.js +147 -0
- package/cbb/worktrees/_shared/scripts/find-target-worktree.js +1 -1
- package/cbb/worktrees/_shared/scripts/find-workspace-root.js +1 -1
- package/cbb/worktrees/_shared/scripts/push-core.js +1 -1
- package/cbb/worktrees/_shared/scripts/silence-popup.js +1 -1
- package/cbb/worktrees/skills/cbb-local-mode/SKILL.md +9 -5
- package/cbb/worktrees/skills/cbb-local-mode/scripts/delete-branches.js +147 -0
- package/cbb/worktrees/skills/cbb-local-mode/scripts/silence-popup.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-close/SKILL.md +3 -2
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/check-env.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/delete-branches.js +147 -138
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/find-target-worktree.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/find-workspace-root.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/silence-popup.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-init/SKILL.md +13 -10
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/push-core.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/silence-popup.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-push/SKILL.md +1 -1
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/find-target-worktree.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/find-workspace-root.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/merge-branches.js +469 -0
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/push-core.js +1 -1
- package/cbb/worktrees/skills/cbb-worktree-push/scripts/silence-popup.js +1 -1
- package/config/workspace-agents.sample.md +1 -1
- package/package.json +2 -2
- package/cbb/worktrees/skills/cbb-worktree-init/scripts/find-workspace-root.js +0 -67
|
@@ -99,39 +99,6 @@ trigger: always_on
|
|
|
99
99
|
|
|
100
100
|
**检验标准:** 用户从未因"AI 直接改了代码没打招呼"而感到意外。
|
|
101
101
|
|
|
102
|
-
## 8. 业务空间开发模式(双模式,用户选择制)
|
|
103
|
-
|
|
104
|
-
**仅当工作目录是业务空间根(存在 `CBB-WORKSPACE.json` 且 `.git` 为目录)时生效。空间支持两种开发模式,能力对称(都能修改 workspace 内容与应用代码、都强制分支隔离、交付都走 MR),由用户显式选择,agent 只给推荐不代决。**
|
|
105
|
-
|
|
106
|
-
**模式一 · 本地模式(单线程快改,当前会话就地干):**
|
|
107
|
-
|
|
108
|
-
- 就地切分支:根 + 各涉及应用 `.codespace/<app>/` 同名 `checkout -b`(基线 = `origin/<main>`,经 `cbb-local-mode` 技能执行)
|
|
109
|
-
- 当前会话原地开发,相对路径原样生效;**一次只承载一个需求**(独占本地检出;期间并行新需求走 worktree 模式,互不干扰)
|
|
110
|
-
- 收尾(收尾合并门禁,ADR 0007):`merge-branches.js --check` 判定已合并;未合并须三选一(提 MR / 直合并 `--allow-checkout` / 确认废弃)后方可收尾;各参与仓库 `checkout main && pull && 删分支`
|
|
111
|
-
|
|
112
|
-
**模式二 · worktree 模式(并行 / 隔离):**
|
|
113
|
-
|
|
114
|
-
- `cbb-worktree-init` 建隔离 worktree(AI 配置内容子目录已 junction 就位),**开发会话开进 `.worktrees/worktree-<需求名>/`**,可多需求并行;app 检出收拢在 `<wt>/.codespace/<app>/`——**镜像空间根布局**(`.codespace/<app>/...` 相对路径两模式通用,ADR 0003)
|
|
115
|
-
- 根会话保留作控制面(worktree 生灭、`cbb update` 等空间级操作);**会话形态按工具定,能自动则 agent 亲自做**:OpenCode 桌面端 = agent 自动把会话切进/切出 worktree(`session_move`,上下文保留、不列入用户待办,空间根路径在案供回切);无会话搬移能力的桌面端 = 用户手动切根;CLI/TUI = 新开会话进 worktree
|
|
116
|
-
|
|
117
|
-
**基准与防线(两模式共同,ADR 0002):**
|
|
118
|
-
|
|
119
|
-
- **基准 = `origin/main`(唯一真相源);`.codespace/<app>/` 的检出 = 视图**——本地模式可暂态被需求分支占用,收尾恢复
|
|
120
|
-
- **严禁直接在任何 main 检出上提交载荷**:分支感知 pre-commit hook 机械拦截(workspace 仓库仅放行控制面白名单:`CBB-WORKSPACE.json` / `.gitignore` / `AGENTS.md` / `openspec/config.yaml` / `openspec/schemas/**`;应用仓库 main 全拦;**merge 提交(`MERGE_HEAD` 存在)放行**,见 ADR 0007)——**被拦是防线生效,不是故障**;`--no-verify` 绕过仅限用户显式要求
|
|
121
|
-
- 需求分支推送唯一出口 = `cbb-worktree-push`(init 创建流程零远端操作,lazy push);推送后可选提 MR 或**直接合并**(`merge-branches.js`,主干 / 测试分支,ADR 0008);收尾合并门禁同源(ADR 0007)
|
|
122
|
-
- **收尾合并门禁(ADR 0007)**:两模式收尾均须每仓库"已合并"(`safety-check` / `merge-branches.js --check` 判定);未合并三选一——提 MR / 直合并 / 确认废弃(远程重命名 `<分支>-deprecated`);交付完成自动删本地+远程分支
|
|
123
|
-
- **模式误判零代价**:本地模式中途要并行/隔离 → 重跑 `cbb-worktree-init`(分支已存在自动复用,提交随身带走)
|
|
124
|
-
|
|
125
|
-
**三时刻对齐(agent 必须遵守):**
|
|
126
|
-
|
|
127
|
-
1. **开工前——状态自检**(`cbb-local-mode` 的 `check-local-state.js`):干净态零交互直接开工;**遗留态(别的需求未收尾,任一仓库检出在非主干分支)必须让用户四选一**(继续 / 先收尾 / 升级 worktree / 丢弃),不得自行决定
|
|
128
|
-
2. **开工时——布局确认**:向用户展示参与仓库与分支名,确认后执行
|
|
129
|
-
3. **进行中——插单归属**:用户插入非本需求的改动,先确认归属(并入 / 收尾后处理 / 转 worktree),不得静默混入
|
|
130
|
-
|
|
131
|
-
**读基准代码约定:** 本地模式进行中,工作区是开发态而非基准——要读"main 现状"先确认检出分支,或直接 `git show main:<path>` / `git diff main`;不要默认工作区即基准。
|
|
132
|
-
|
|
133
|
-
**检验标准:** 任何 main 检出上无载荷提交(hook 兜底,git log 可机械审计;merge 提交除外);本地模式收尾后全部仓库回主干且 clean;worktree 模式的变更产物只出现在需求工作树内;需求分支只经 push 技能产生;**未合并且非废弃的分支无法通过收尾(门禁兜底)**。
|
|
134
|
-
|
|
135
102
|
---
|
|
136
103
|
|
|
137
104
|
**这些准则生效的标志:** diff 中不必要的改动减少、不会因过度设计而重写、澄清问题在实现前而非出错后提出。
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
trigger: always_on
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# 业务空间开发纪律(双模式,用户选择制)
|
|
6
|
+
|
|
7
|
+
**仅当工作目录是业务空间根(存在 `CBB-WORKSPACE.json` 且 `.git` 为目录)时生效。空间支持两种开发模式,能力对称(都能修改 workspace 内容与应用代码、都强制分支隔离、交付均有 MR 与直合并两条通道,ADR 0007/0008),由用户显式选择,agent 只给推荐不代决。**
|
|
8
|
+
|
|
9
|
+
**模式一 · 本地模式(单线程快改,当前会话就地干):**
|
|
10
|
+
|
|
11
|
+
- 就地切分支:根 + 各涉及应用 `.codespace/<app>/` 同名 `checkout -b`(基线 = `origin/<main>`,经 `cbb-local-mode` 技能执行)
|
|
12
|
+
- 当前会话原地开发,相对路径原样生效;**一次只承载一个需求**(独占本地检出;期间并行新需求走 worktree 模式,互不干扰)
|
|
13
|
+
- 收尾(收尾合并门禁,ADR 0007):`merge-branches.js --check` 判定已合并;未合并须三选一(提 MR / 直合并 `--allow-checkout` / 确认废弃)后方可收尾;各参与仓库 `checkout main && pull && 删分支`
|
|
14
|
+
|
|
15
|
+
**模式二 · worktree 模式(并行 / 隔离):**
|
|
16
|
+
|
|
17
|
+
- `cbb-worktree-init` 建隔离 worktree(AI 配置内容子目录已 junction 就位),**开发会话开进 `.worktrees/worktree-<需求名>/`**,可多需求并行;app 检出收拢在 `<wt>/.codespace/<app>/`——**镜像空间根布局**(`.codespace/<app>/...` 相对路径两模式通用,ADR 0003)
|
|
18
|
+
- 根会话保留作控制面(worktree 生灭、`cbb update` 等空间级操作);**会话形态按工具定,能自动则 agent 亲自做**:OpenCode 桌面端 = agent 自动把会话切进/切出 worktree(`session_move`,上下文保留、不列入用户待办,空间根路径在案供回切);无会话搬移能力的桌面端 = 用户手动切根;CLI/TUI = 新开会话进 worktree
|
|
19
|
+
|
|
20
|
+
**基准与防线(两模式共同,ADR 0002):**
|
|
21
|
+
|
|
22
|
+
- **基准 = `origin/main`(唯一真相源);`.codespace/<app>/` 的检出 = 视图**——本地模式可暂态被需求分支占用,收尾恢复
|
|
23
|
+
- **严禁直接在任何 main 检出上提交载荷**:分支感知 pre-commit hook 机械拦截(workspace 仓库仅放行控制面白名单:`CBB-WORKSPACE.json` / `.gitignore` / `AGENTS.md` / `openspec/config.yaml` / `openspec/schemas/**`;应用仓库 main 全拦;**merge 提交(`MERGE_HEAD` 存在)放行**,见 ADR 0007)——**被拦是防线生效,不是故障**;`--no-verify` 绕过仅限用户显式要求
|
|
24
|
+
- 需求分支推送唯一出口 = `cbb-worktree-push`(init 创建流程零远端操作,lazy push);推送后可选提 MR 或**直接合并**(`merge-branches.js`,主干 / 测试分支,ADR 0008);收尾合并门禁同源(ADR 0007)
|
|
25
|
+
- **收尾合并门禁(ADR 0007)**:两模式收尾均须每仓库"已合并"(`safety-check` / `merge-branches.js --check` 判定);未合并三选一——提 MR / 直合并 / 确认废弃(远程重命名 `<分支>-deprecated`);交付完成自动删本地+远程分支
|
|
26
|
+
- **模式误判零代价**:本地模式中途要并行/隔离 → 重跑 `cbb-worktree-init`(分支已存在自动复用,提交随身带走)
|
|
27
|
+
|
|
28
|
+
**三时刻对齐(agent 必须遵守):**
|
|
29
|
+
|
|
30
|
+
1. **开工前——状态自检**(`cbb-local-mode` 的 `check-local-state.js`):干净态零交互直接开工;**遗留态(别的需求未收尾,任一仓库检出在非主干分支)必须让用户四选一**(继续 / 先收尾 / 升级 worktree / 丢弃),不得自行决定
|
|
31
|
+
2. **开工时——布局确认**:向用户展示参与仓库与分支名,确认后执行
|
|
32
|
+
3. **进行中——插单归属**:用户插入非本需求的改动,先确认归属(并入 / 收尾后处理 / 转 worktree),不得静默混入
|
|
33
|
+
|
|
34
|
+
**读基准代码约定:** 本地模式进行中,工作区是开发态而非基准——要读"main 现状"先确认检出分支,或直接 `git show main:<path>` / `git diff main`;不要默认工作区即基准。
|
|
35
|
+
|
|
36
|
+
**检验标准:** 任何 main 检出上无载荷提交(hook 兜底,git log 可机械审计;merge 提交除外);本地模式收尾后全部仓库回主干且 clean;worktree 模式的变更产物只出现在需求工作树内;需求分支只经 push 技能产生;**未合并且非废弃的分支无法通过收尾(门禁兜底)**。
|
package/cbb/lib/install/init.js
CHANGED
|
@@ -396,7 +396,7 @@ function deployCoreRules(adapter, isUpgrade, managedPaths) {
|
|
|
396
396
|
const rulesDest = path.join(TARGET_DIR, adapter.rulesDir);
|
|
397
397
|
const action = isUpgrade ? '更新' : '安装';
|
|
398
398
|
|
|
399
|
-
const coreRules = ['cbb-coding-rule.md', 'cbb-priority.md', 'cbb-ai-behavior.md'];
|
|
399
|
+
const coreRules = ['cbb-coding-rule.md', 'cbb-priority.md', 'cbb-ai-behavior.md', 'cbb-workspace-discipline.md'];
|
|
400
400
|
|
|
401
401
|
// 确保目标目录存在
|
|
402
402
|
fs.mkdirSync(rulesDest, { recursive: true });
|
|
@@ -1086,13 +1086,14 @@ async function init(targetDir, options = {}) {
|
|
|
1086
1086
|
newPaths.add(`${adapter.rulesDir}/cbb-coding-rule.md`);
|
|
1087
1087
|
newPaths.add(`${adapter.rulesDir}/cbb-priority.md`);
|
|
1088
1088
|
newPaths.add(`${adapter.rulesDir}/cbb-ai-behavior.md`);
|
|
1089
|
+
newPaths.add(`${adapter.rulesDir}/cbb-workspace-discipline.md`);
|
|
1089
1090
|
// opencode:rules glob 登记进 .opencode/opencode.json instructions,并迁移根文件旧登记
|
|
1090
1091
|
if (adapter.instructionsFile) {
|
|
1091
1092
|
injectInstructions(adapter, newPaths, managedPaths);
|
|
1092
1093
|
migrateLegacyInstructions(adapter);
|
|
1093
1094
|
}
|
|
1094
1095
|
});
|
|
1095
|
-
log(`核心规则 → ${selectedAdapters.map(a => a.rulesDir).join(', ')} (
|
|
1096
|
+
log(`核心规则 → ${selectedAdapters.map(a => a.rulesDir).join(', ')} (4 个文件)`, 'success');
|
|
1096
1097
|
|
|
1097
1098
|
// 安装 Skills(支持 skills 的适配器)
|
|
1098
1099
|
const skillsAdapters = selectedAdapters.filter(a => a.skillsDir);
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
require('./silence-popup');
|
|
3
|
+
/**
|
|
4
|
+
* 删除需求分支(共享脚本,真源:cbb/worktrees/_shared/scripts/)
|
|
5
|
+
*
|
|
6
|
+
* 供 cbb-worktree-close(Step 4.4 交付完成后自动删本地 + 远程分支)与
|
|
7
|
+
* cbb-local-mode(Step 5.4 收尾删远程分支)调用。
|
|
8
|
+
*
|
|
9
|
+
* 用法:node delete-branches.js <ws_root> <branch> <mode> # mode: keep / local / local_remote
|
|
10
|
+
*
|
|
11
|
+
* 逐仓库执行:本地分支按合并判定选 -d / -D(squash 合并 -d 失败回退 -D);
|
|
12
|
+
* local_remote 模式追加 git push <remote> --delete。
|
|
13
|
+
* 清理阶段失败报告并继续其余仓库,不回滚(ADR 0007)。
|
|
14
|
+
*/
|
|
15
|
+
const { execSync } = require('child_process');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
const wsRoot = process.argv[2];
|
|
20
|
+
const branch = process.argv[3];
|
|
21
|
+
const mode = process.argv[4];
|
|
22
|
+
|
|
23
|
+
if (!wsRoot || !branch || !mode) {
|
|
24
|
+
console.error('Usage: node delete-branches.js <ws_root> <branch> <mode>');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (mode === 'keep') {
|
|
29
|
+
console.log('Keep branch, skip delete');
|
|
30
|
+
console.log('---');
|
|
31
|
+
console.log('BRANCH_DELETE_RESULT', JSON.stringify({ mode: 'keep', ok: true, deleted: [] }));
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (mode !== 'local' && mode !== 'local_remote') {
|
|
36
|
+
console.error('mode must be keep / local / local_remote');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function run(cmd, cwd, env) {
|
|
41
|
+
try {
|
|
42
|
+
return { ok: true, out: execSync(cmd, { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, ...(env || {}) } }).trim() };
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return { ok: false, err: (e.stderr || e.message || String(e)).trim() };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function detectMain(repoPath) {
|
|
49
|
+
const remote = run('git remote', repoPath);
|
|
50
|
+
if (!remote.ok || !remote.out) return 'main';
|
|
51
|
+
const r = remote.out.split(/\r?\n/)[0];
|
|
52
|
+
const symbolic = run('git symbolic-ref "refs/remotes/' + r + '/HEAD"', repoPath);
|
|
53
|
+
if (symbolic.ok && symbolic.out) {
|
|
54
|
+
return symbolic.out.replace('refs/remotes/' + r + '/', '');
|
|
55
|
+
}
|
|
56
|
+
return 'main';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function mergedTo(repoPath, from, to) {
|
|
60
|
+
// 0=merged, 1=unmerged, 2=to-ref-missing
|
|
61
|
+
if (!run('git rev-parse --verify --quiet "' + from + '"', repoPath).ok) return 1;
|
|
62
|
+
if (!run('git rev-parse --verify --quiet "' + to + '"', repoPath).ok) return 2;
|
|
63
|
+
if (run('git merge-base --is-ancestor "' + from + '" "' + to + '" && echo Y', repoPath).out === 'Y') return 0;
|
|
64
|
+
// 跨平台:不用 grep(Windows 无 grep),解析 git cherry 输出(+ 前缀 = 未合并提交)
|
|
65
|
+
const cherry = run('git cherry "' + to + '" "' + from + '"', repoPath);
|
|
66
|
+
if (!cherry.ok) return 1;
|
|
67
|
+
if (cherry.out.split(/\r?\n/).filter(Boolean).some(l => l.indexOf('+') === 0)) return 1;
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function mergedToMain(repoPath, branchName, main) {
|
|
72
|
+
if (mergedTo(repoPath, branchName, main) === 0) return true;
|
|
73
|
+
if (mergedTo(repoPath, branchName, 'origin/' + main) === 0) return true;
|
|
74
|
+
// release-* transit
|
|
75
|
+
const releaseList = run('git for-each-ref --format="%(refname:short)" "refs/heads/release-*" "refs/remotes/origin/release-*"', repoPath);
|
|
76
|
+
if (releaseList.ok && releaseList.out) {
|
|
77
|
+
const releases = releaseList.out.split(/\r?\n/).filter(Boolean).sort();
|
|
78
|
+
for (const rel of releases) {
|
|
79
|
+
if (mergedTo(repoPath, branchName, rel) === 0) {
|
|
80
|
+
if (mergedTo(repoPath, rel, main) === 0) return true;
|
|
81
|
+
if (mergedTo(repoPath, rel, 'origin/' + main) === 0) return true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function deleteBranch(repoPath, name) {
|
|
89
|
+
const checkLocal = run('git branch --list "' + branch + '"', repoPath);
|
|
90
|
+
if (checkLocal.ok && checkLocal.out) {
|
|
91
|
+
const main = detectMain(repoPath);
|
|
92
|
+
const merged = main ? mergedToMain(repoPath, branch, main) : false;
|
|
93
|
+
let flag = merged ? '-d' : '-D';
|
|
94
|
+
const del = run('git branch ' + flag + ' "' + branch + '"', repoPath);
|
|
95
|
+
if (del.ok) {
|
|
96
|
+
console.log('OK ' + name + ' local branch deleted (' + flag + ', ' + (merged ? 'merged' : 'unmerged') + ')');
|
|
97
|
+
} else {
|
|
98
|
+
if (flag === '-d') {
|
|
99
|
+
const fallback = run('git branch -D "' + branch + '"', repoPath);
|
|
100
|
+
if (fallback.ok) {
|
|
101
|
+
console.log('OK ' + name + ' local branch deleted (squash merge, fallback -D)');
|
|
102
|
+
} else {
|
|
103
|
+
console.error('FAIL ' + name + ' local branch delete failed');
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
console.error('FAIL ' + name + ' local branch delete failed');
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
console.log('Skip ' + name + ': no local branch ' + branch);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (mode === 'local_remote') {
|
|
116
|
+
const remote = run('git remote', repoPath);
|
|
117
|
+
if (remote.ok && remote.out) {
|
|
118
|
+
const r = remote.out.split(/\r?\n/)[0];
|
|
119
|
+
const pushDel = run('git push "' + r + '" --delete "' + branch + '"', repoPath, { GIT_TERMINAL_PROMPT: '0' });
|
|
120
|
+
if (pushDel.ok) {
|
|
121
|
+
console.log('OK ' + name + ' remote branch deleted');
|
|
122
|
+
} else {
|
|
123
|
+
console.log('Skip ' + name + ': remote has no ' + branch);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const configPath = path.join(wsRoot, 'CBB-WORKSPACE.json');
|
|
131
|
+
let config = {};
|
|
132
|
+
try { config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch (e) {}
|
|
133
|
+
|
|
134
|
+
const apps = config.apps || [];
|
|
135
|
+
|
|
136
|
+
console.log('=== Delete Workspace Branch ===');
|
|
137
|
+
deleteBranch(wsRoot, 'workspace');
|
|
138
|
+
|
|
139
|
+
console.log('=== Delete App Branches ===');
|
|
140
|
+
for (const app of apps) {
|
|
141
|
+
const appPath = path.join(wsRoot, '.codespace', app.name);
|
|
142
|
+
deleteBranch(appPath, app.name);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log('---');
|
|
146
|
+
console.log('BRANCH_DELETE_RESULT', JSON.stringify({ mode, ok: true }));
|
|
147
|
+
process.exit(0);
|
|
@@ -3,7 +3,7 @@ require('./silence-popup');
|
|
|
3
3
|
* openspec-shared 目标需求名识别
|
|
4
4
|
*
|
|
5
5
|
* 唯一真源:cbb/worktrees/_shared/scripts/find-target-worktree.js
|
|
6
|
-
* 由 scripts/sync-shared.js 展开到各 skill 的 scripts/ 下(
|
|
6
|
+
* 由 scripts/sync-shared.js 展开到各 skill 的 scripts/ 下(close / push)。
|
|
7
7
|
*
|
|
8
8
|
* 用法:node find-target-worktree.js <ws_root>
|
|
9
9
|
*
|
|
@@ -3,7 +3,7 @@ require('./silence-popup');
|
|
|
3
3
|
* openspec-shared 工作空间根目录定位
|
|
4
4
|
*
|
|
5
5
|
* 唯一真源:cbb/worktrees/_shared/scripts/find-workspace-root.js
|
|
6
|
-
* 由 scripts/sync-shared.js 展开到各 skill 的 scripts/ 下(
|
|
6
|
+
* 由 scripts/sync-shared.js 展开到各 skill 的 scripts/ 下(close / push)。
|
|
7
7
|
*
|
|
8
8
|
* 优先:git worktree list 第一行(主仓库根目录,不受 worktree 内 CBB-WORKSPACE.json 副本干扰)
|
|
9
9
|
* 兜底:当前目录含 CBB-WORKSPACE.json
|
|
@@ -3,7 +3,7 @@ require('./silence-popup');
|
|
|
3
3
|
* openspec-shared 推送核心模块
|
|
4
4
|
*
|
|
5
5
|
* 唯一真源:cbb/worktrees/_shared/scripts/push-core.js
|
|
6
|
-
* 由 scripts/sync-shared.js 展开到各 skill 的 scripts/ 下(init
|
|
6
|
+
* 由 scripts/sync-shared.js 展开到各 skill 的 scripts/ 下(init / push)。
|
|
7
7
|
*
|
|
8
8
|
* 公开 API:
|
|
9
9
|
* - tryExec(cmd, cwd, execImpl) 包装 execSync,统一返回 {ok, out|err}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// koffi is resolved from cbb global install (no local node_modules needed).
|
|
5
5
|
//
|
|
6
6
|
// 唯一真源:cbb/worktrees/_shared/scripts/silence-popup.js
|
|
7
|
-
// 由 scripts/sync-shared.js 展开到各 skill 的 scripts/ 下(init /
|
|
7
|
+
// 由 scripts/sync-shared.js 展开到各 skill 的 scripts/ 下(init / close / push / local-mode)。
|
|
8
8
|
// skill 内同目录引用:脚本第一行 `require('./silence-popup')`。
|
|
9
9
|
if (process.platform === 'win32') {
|
|
10
10
|
try {
|
|
@@ -5,7 +5,7 @@ description: "业务空间本地模式开发:空间根与各涉及应用在 .c
|
|
|
5
5
|
|
|
6
6
|
# Local Mode(本地模式)
|
|
7
7
|
|
|
8
|
-
业务空间的**就地开发**模式(ADR 0002):需求涉及的所有仓库(空间根 + 各应用的标准本地检出 `.codespace/<app>/`)统一切到同名分支,当前会话在原地直接开发。与 worktree 模式**能力对称**(都能改 workspace
|
|
8
|
+
业务空间的**就地开发**模式(ADR 0002):需求涉及的所有仓库(空间根 + 各应用的标准本地检出 `.codespace/<app>/`)统一切到同名分支,当前会话在原地直接开发。与 worktree 模式**能力对称**(都能改 workspace 内容与应用代码、都强制分支隔离、交付均有 MR 与直合并两条通道,ADR 0007/0008),差异仅是**无隔离目录、不可并行**——适合单线程快速推进的需求。
|
|
9
9
|
|
|
10
10
|
**启动时声明:** "我正在使用 cbb-local-mode 技能进行本地模式开发。"
|
|
11
11
|
|
|
@@ -42,7 +42,7 @@ node "<skill_dir>/scripts/check-local-state.js" "<ws_root>"
|
|
|
42
42
|
|
|
43
43
|
## Step 1:需求名与布局确认
|
|
44
44
|
|
|
45
|
-
1. 对话中询问需求名(kebab-case,同时作分支名;禁 `..` / 路径分隔符 /
|
|
45
|
+
1. 对话中询问需求名(kebab-case,同时作分支名;禁 `..` / 路径分隔符 / 空格):直接给名,或输入需求描述由 agent 推荐 3–5 个英文候选(`AskUserQuestion` 选定;不满意可直接回复自己的名字)
|
|
46
46
|
2. 从需求语境提取**涉及的应用子集**(改哪些 `.codespace/<app>`);纯 workspace 内容则仅根参与。展示布局向用户确认:
|
|
47
47
|
|
|
48
48
|
```
|
|
@@ -74,7 +74,7 @@ node "<skill_dir>/scripts/start-local.js" "<ws_root>" "<branch>" --apps "<app1>,
|
|
|
74
74
|
|
|
75
75
|
## Step 4:提交推送(转 cbb-worktree-push)
|
|
76
76
|
|
|
77
|
-
对话中说"提交并推送"即触发 `cbb-worktree-push`——其仓库发现已支持**本地布局**(根 + 持有同名分支的 `.codespace/<app>/`),dry-run 预览(含 agent 生成的 commit 信息)→ 批量 commit → push → MR
|
|
77
|
+
对话中说"提交并推送"即触发 `cbb-worktree-push`——其仓库发现已支持**本地布局**(根 + 持有同名分支的 `.codespace/<app>/`),dry-run 预览(含 agent 生成的 commit 信息)→ 批量 commit → push → Step 7 交付选择(提 MR 预填链接 / 直接合并 / 暂不处理,ADR 0008),全流程与 worktree 模式一致。
|
|
78
78
|
|
|
79
79
|
## Step 5:收尾(收尾合并门禁 · ADR 0007)
|
|
80
80
|
|
|
@@ -110,7 +110,11 @@ node "<skill_dir>/scripts/merge-branches.js" "<ws_root>" "<需求名>" --apps "<
|
|
|
110
110
|
git -C "<仓库>" checkout main && git -C "<仓库>" pull && git -C "<仓库>" branch -d "<需求名>"
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
-
-
|
|
113
|
+
- 各仓库本地分支删除后,远程分支删除由共享脚本完成(删除前输出将删清单):
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<需求名>" local_remote
|
|
117
|
+
```
|
|
114
118
|
- `checkout` 被 hook 拦(说明 main 落后或本地 main 有分叉)→ 先 `pull --ff-only`,再删分支
|
|
115
119
|
- openspec change 归档在合并前执行(产物在分支上随交付合入)
|
|
116
120
|
- 全部仓库回 main 后向用户报告"本地模式已收尾,空间回到干净态"
|
|
@@ -126,7 +130,7 @@ git -C "<仓库>" checkout main && git -C "<仓库>" pull && git -C "<仓库>" b
|
|
|
126
130
|
| 场景 | 处理方式 |
|
|
127
131
|
|------|----------|
|
|
128
132
|
| 任何触发 | 先跑 Step 0 状态自检(clean 零交互 / active 用户四选一) |
|
|
129
|
-
| 需求名 |
|
|
133
|
+
| 需求名 | 对话输入:直接给名,或给需求描述(agent 推荐 3–5 个英文候选供选);自由文本本身禁走 AskUserQuestion |
|
|
130
134
|
| 涉及应用 | 需求语境提取子集,`--apps` 传入;中途补齐重跑(幂等) |
|
|
131
135
|
| 切分支失败(dirty 冲突) | 报告详情,用户处理;禁止自动 stash |
|
|
132
136
|
| commit 被 hook 拦(main 载荷) | 大声报错是预期行为:回分支重做;禁止自动 --no-verify |
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
require('./silence-popup');
|
|
3
|
+
/**
|
|
4
|
+
* 删除需求分支(共享脚本,真源:cbb/worktrees/_shared/scripts/)
|
|
5
|
+
*
|
|
6
|
+
* 供 cbb-worktree-close(Step 4.4 交付完成后自动删本地 + 远程分支)与
|
|
7
|
+
* cbb-local-mode(Step 5.4 收尾删远程分支)调用。
|
|
8
|
+
*
|
|
9
|
+
* 用法:node delete-branches.js <ws_root> <branch> <mode> # mode: keep / local / local_remote
|
|
10
|
+
*
|
|
11
|
+
* 逐仓库执行:本地分支按合并判定选 -d / -D(squash 合并 -d 失败回退 -D);
|
|
12
|
+
* local_remote 模式追加 git push <remote> --delete。
|
|
13
|
+
* 清理阶段失败报告并继续其余仓库,不回滚(ADR 0007)。
|
|
14
|
+
*/
|
|
15
|
+
const { execSync } = require('child_process');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
const wsRoot = process.argv[2];
|
|
20
|
+
const branch = process.argv[3];
|
|
21
|
+
const mode = process.argv[4];
|
|
22
|
+
|
|
23
|
+
if (!wsRoot || !branch || !mode) {
|
|
24
|
+
console.error('Usage: node delete-branches.js <ws_root> <branch> <mode>');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (mode === 'keep') {
|
|
29
|
+
console.log('Keep branch, skip delete');
|
|
30
|
+
console.log('---');
|
|
31
|
+
console.log('BRANCH_DELETE_RESULT', JSON.stringify({ mode: 'keep', ok: true, deleted: [] }));
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (mode !== 'local' && mode !== 'local_remote') {
|
|
36
|
+
console.error('mode must be keep / local / local_remote');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function run(cmd, cwd, env) {
|
|
41
|
+
try {
|
|
42
|
+
return { ok: true, out: execSync(cmd, { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, ...(env || {}) } }).trim() };
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return { ok: false, err: (e.stderr || e.message || String(e)).trim() };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function detectMain(repoPath) {
|
|
49
|
+
const remote = run('git remote', repoPath);
|
|
50
|
+
if (!remote.ok || !remote.out) return 'main';
|
|
51
|
+
const r = remote.out.split(/\r?\n/)[0];
|
|
52
|
+
const symbolic = run('git symbolic-ref "refs/remotes/' + r + '/HEAD"', repoPath);
|
|
53
|
+
if (symbolic.ok && symbolic.out) {
|
|
54
|
+
return symbolic.out.replace('refs/remotes/' + r + '/', '');
|
|
55
|
+
}
|
|
56
|
+
return 'main';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function mergedTo(repoPath, from, to) {
|
|
60
|
+
// 0=merged, 1=unmerged, 2=to-ref-missing
|
|
61
|
+
if (!run('git rev-parse --verify --quiet "' + from + '"', repoPath).ok) return 1;
|
|
62
|
+
if (!run('git rev-parse --verify --quiet "' + to + '"', repoPath).ok) return 2;
|
|
63
|
+
if (run('git merge-base --is-ancestor "' + from + '" "' + to + '" && echo Y', repoPath).out === 'Y') return 0;
|
|
64
|
+
// 跨平台:不用 grep(Windows 无 grep),解析 git cherry 输出(+ 前缀 = 未合并提交)
|
|
65
|
+
const cherry = run('git cherry "' + to + '" "' + from + '"', repoPath);
|
|
66
|
+
if (!cherry.ok) return 1;
|
|
67
|
+
if (cherry.out.split(/\r?\n/).filter(Boolean).some(l => l.indexOf('+') === 0)) return 1;
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function mergedToMain(repoPath, branchName, main) {
|
|
72
|
+
if (mergedTo(repoPath, branchName, main) === 0) return true;
|
|
73
|
+
if (mergedTo(repoPath, branchName, 'origin/' + main) === 0) return true;
|
|
74
|
+
// release-* transit
|
|
75
|
+
const releaseList = run('git for-each-ref --format="%(refname:short)" "refs/heads/release-*" "refs/remotes/origin/release-*"', repoPath);
|
|
76
|
+
if (releaseList.ok && releaseList.out) {
|
|
77
|
+
const releases = releaseList.out.split(/\r?\n/).filter(Boolean).sort();
|
|
78
|
+
for (const rel of releases) {
|
|
79
|
+
if (mergedTo(repoPath, branchName, rel) === 0) {
|
|
80
|
+
if (mergedTo(repoPath, rel, main) === 0) return true;
|
|
81
|
+
if (mergedTo(repoPath, rel, 'origin/' + main) === 0) return true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function deleteBranch(repoPath, name) {
|
|
89
|
+
const checkLocal = run('git branch --list "' + branch + '"', repoPath);
|
|
90
|
+
if (checkLocal.ok && checkLocal.out) {
|
|
91
|
+
const main = detectMain(repoPath);
|
|
92
|
+
const merged = main ? mergedToMain(repoPath, branch, main) : false;
|
|
93
|
+
let flag = merged ? '-d' : '-D';
|
|
94
|
+
const del = run('git branch ' + flag + ' "' + branch + '"', repoPath);
|
|
95
|
+
if (del.ok) {
|
|
96
|
+
console.log('OK ' + name + ' local branch deleted (' + flag + ', ' + (merged ? 'merged' : 'unmerged') + ')');
|
|
97
|
+
} else {
|
|
98
|
+
if (flag === '-d') {
|
|
99
|
+
const fallback = run('git branch -D "' + branch + '"', repoPath);
|
|
100
|
+
if (fallback.ok) {
|
|
101
|
+
console.log('OK ' + name + ' local branch deleted (squash merge, fallback -D)');
|
|
102
|
+
} else {
|
|
103
|
+
console.error('FAIL ' + name + ' local branch delete failed');
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
console.error('FAIL ' + name + ' local branch delete failed');
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
console.log('Skip ' + name + ': no local branch ' + branch);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (mode === 'local_remote') {
|
|
116
|
+
const remote = run('git remote', repoPath);
|
|
117
|
+
if (remote.ok && remote.out) {
|
|
118
|
+
const r = remote.out.split(/\r?\n/)[0];
|
|
119
|
+
const pushDel = run('git push "' + r + '" --delete "' + branch + '"', repoPath, { GIT_TERMINAL_PROMPT: '0' });
|
|
120
|
+
if (pushDel.ok) {
|
|
121
|
+
console.log('OK ' + name + ' remote branch deleted');
|
|
122
|
+
} else {
|
|
123
|
+
console.log('Skip ' + name + ': remote has no ' + branch);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const configPath = path.join(wsRoot, 'CBB-WORKSPACE.json');
|
|
131
|
+
let config = {};
|
|
132
|
+
try { config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch (e) {}
|
|
133
|
+
|
|
134
|
+
const apps = config.apps || [];
|
|
135
|
+
|
|
136
|
+
console.log('=== Delete Workspace Branch ===');
|
|
137
|
+
deleteBranch(wsRoot, 'workspace');
|
|
138
|
+
|
|
139
|
+
console.log('=== Delete App Branches ===');
|
|
140
|
+
for (const app of apps) {
|
|
141
|
+
const appPath = path.join(wsRoot, '.codespace', app.name);
|
|
142
|
+
deleteBranch(appPath, app.name);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log('---');
|
|
146
|
+
console.log('BRANCH_DELETE_RESULT', JSON.stringify({ mode, ok: true }));
|
|
147
|
+
process.exit(0);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// koffi is resolved from cbb global install (no local node_modules needed).
|
|
5
5
|
//
|
|
6
6
|
// 唯一真源:cbb/worktrees/_shared/scripts/silence-popup.js
|
|
7
|
-
// 由 scripts/sync-shared.js 展开到各 skill 的 scripts/ 下(init /
|
|
7
|
+
// 由 scripts/sync-shared.js 展开到各 skill 的 scripts/ 下(init / close / push / local-mode)。
|
|
8
8
|
// skill 内同目录引用:脚本第一行 `require('./silence-popup')`。
|
|
9
9
|
if (process.platform === 'win32') {
|
|
10
10
|
try {
|
|
@@ -287,6 +287,7 @@ node "<skill_dir>/scripts/merge-branches.js" "<ws_root>" "<branch>" --apps "<app
|
|
|
287
287
|
- 退出码 0 → 交付完成,继续 Step 4
|
|
288
288
|
- `need-mr`(`branch-protected`:远端主干受保护)→ 远端保护是常态,**改用出口一**(不是故障)
|
|
289
289
|
- `failed`(含 `merge-failed` / `push-failed`)→ 输出失败明细,阻塞并询问用户(修复后重跑;重跑幂等)
|
|
290
|
+
- `failed` 且 `not-on-main`(空间根检出不是主干:被其他需求占用或异常)→ 先让该占用回到主干(收尾本地需求)或改用出口一;**不要用 `--allow-checkout`**(会切走占用者的工作区)
|
|
290
291
|
|
|
291
292
|
**出口三 · 确认废弃(`deprecate-branches.js`)**
|
|
292
293
|
|
|
@@ -493,9 +494,9 @@ git -C "<ws_root>" worktree list
|
|
|
493
494
|
| `find-workspace-root.js` | 定位工作空间根目录(共享脚本,源仓库真源在 `_shared/scripts/`) | 无 | 0/1 |
|
|
494
495
|
| `find-target-worktree.js` | 目标 worktree 识别(共享脚本,源仓库真源在 `_shared/scripts/`) | ws_root | 0/1/2 |
|
|
495
496
|
| `discover-apps.js` | 发现关联应用 worktree | ws_root, branch | 0/1/2 |
|
|
496
|
-
| `check-unarchived.js` | 检查未归档 change | ws_root | 0/1 |
|
|
497
|
+
| `check-unarchived.js` | 检查未归档 change(双扫空间根 + worktree) | ws_root, branch | 0/1 |
|
|
497
498
|
| `safety-check.js` | 安全评估(合并判定 + 未提交 + 远程) | ws_root, branch | 0/1 |
|
|
498
499
|
| `remove-worktrees.js` | 移除 worktree(三层降级) | ws_root, branch, force | 0/1 |
|
|
499
500
|
| `delete-branches.js` | 删除分支(按 mode;交付完成后由 Step 4.4 自动以 local_remote 调用) | ws_root, branch, mode | 0/1/2 |
|
|
500
|
-
| `merge-branches.js` | 收尾合并门禁·直合并(预检 → merge --no-ff → push 主干,共享脚本,真源在 `_shared/scripts/`) | ws_root, branch, --apps / --allow-checkout / --dry-run | 0/1/2 |
|
|
501
|
+
| `merge-branches.js` | 收尾合并门禁·直合并(预检 → merge --no-ff → push 主干,共享脚本,真源在 `_shared/scripts/`) | ws_root, branch, --apps / --target / --allow-checkout / --dry-run / --check | 0/1/2 |
|
|
501
502
|
| `deprecate-branches.js` | 收尾合并门禁·废弃(远程分支重命名为 `<分支>-deprecated`,共享脚本) | ws_root, branch, --apps | 0/1/2 |
|