worktree-bay 4.0.4 → 4.2.0
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/README.md +5 -2
- package/dist/cli.js +14 -6
- package/dist/commands/add.js +6 -6
- package/dist/commands/claim.js +4 -2
- package/dist/commands/logs.js +40 -0
- package/dist/commands/ls.js +26 -10
- package/dist/engine.js +21 -8
- package/dist/mcp.js +14 -10
- package/dist/proc.js +14 -1
- package/dist/slots.js +50 -12
- package/package.json +1 -1
- package/skill.md +12 -7
package/README.md
CHANGED
|
@@ -30,7 +30,8 @@ npm i -g worktree-bay
|
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
32
|
# 一条命令起整个功能:自动占槽 + 在 api/lms 上开 worktree(分支默认 = 功能名)
|
|
33
|
-
|
|
33
|
+
# -d 记录这个槽的用途,写进槽账本,ls 会显示、重入时参考(up/add/claim 都支持)
|
|
34
|
+
worktree-bay up drill-fix api lms -d "修演练页 bug"
|
|
34
35
|
|
|
35
36
|
# 看占用
|
|
36
37
|
worktree-bay ls
|
|
@@ -48,6 +49,8 @@ worktree-bay gc
|
|
|
48
49
|
> 运行体随起随停(不动 worktree/代码):`worktree-bay stop drill-fix` 停掉(docker 容器 + dev server 一起)、`start` 起回来、`restart` 重启。
|
|
49
50
|
>
|
|
50
51
|
> 更细的控制:`claim <feature>` 单独占槽、`add <feature> <service> [branch]` 单加一个服务(branch 可自定义,省略则用功能名)、`down <feature> <service>` 只拆某个服务(省略服务则拆整功能)。
|
|
52
|
+
>
|
|
53
|
+
> dev server 起不来或报错?`worktree-bay logs <feature>`(`--tail N` 调行数、`--prev` 看上一轮)直接看日志尾部排障——日志每次启动滚动,当前文件只含本轮。
|
|
51
54
|
|
|
52
55
|
## 配置
|
|
53
56
|
|
|
@@ -132,7 +135,7 @@ worktree-bay completion install
|
|
|
132
135
|
}
|
|
133
136
|
```
|
|
134
137
|
|
|
135
|
-
> 服务在哪个工作区目录启动,就用哪个目录的 `worktree-bay.config.json`(cwd 自动向上查找,**无需写死路径**;也可设 `WORKTREE_BAY_CONFIG`)。暴露的工具:`worktree_bay_doctor / ls / up / claim / add / path / run / start / stop / restart / down / gc / init / skill`(`doctor` 列出全部服务名,`ls` JSON 返回各 worktree 路径与 `▸run`,`path` 给某功能某服务目录,`start/stop/restart` 控制运行体,`down` 可只拆单个服务,`skill` 取完整指南)。MCP 调用时自动以非交互模式运行(输出不含颜色/进度控制符)。
|
|
138
|
+
> 服务在哪个工作区目录启动,就用哪个目录的 `worktree-bay.config.json`(cwd 自动向上查找,**无需写死路径**;也可设 `WORKTREE_BAY_CONFIG`)。暴露的工具:`worktree_bay_doctor / ls / up / claim / add / path / run / start / stop / restart / down / logs / gc / init / skill`(`doctor` 列出全部服务名,`ls` JSON 返回各 worktree 路径与 `▸run`,`path` 给某功能某服务目录,`start/stop/restart` 控制运行体,`down` 可只拆单个服务,`logs` 看 dev server 日志排障,`skill` 取完整指南)。MCP 调用时自动以非交互模式运行(输出不含颜色/进度控制符)。
|
|
136
139
|
|
|
137
140
|
## 许可证
|
|
138
141
|
|
package/dist/cli.js
CHANGED
|
@@ -8,6 +8,7 @@ import { addCommand, upCommand } from './commands/add.js';
|
|
|
8
8
|
import { runCommand, shCommand, pathCommand } from './commands/passthrough.js';
|
|
9
9
|
import { rmCommand } from './commands/rm.js';
|
|
10
10
|
import { startCommand, stopCommand, restartCommand } from './commands/lifecycle.js';
|
|
11
|
+
import { logsCommand } from './commands/logs.js';
|
|
11
12
|
import { gcCommand } from './commands/gc.js';
|
|
12
13
|
import { doctorCommand } from './commands/doctor.js';
|
|
13
14
|
import { complete, completionCommand, installCompletion } from './commands/completion.js';
|
|
@@ -52,9 +53,10 @@ program.command('init').description(t('在当前工作区生成 worktree-bay.con
|
|
|
52
53
|
catch (e) {
|
|
53
54
|
die(e.message);
|
|
54
55
|
} });
|
|
55
|
-
program.command('claim <feature>').description(t('
|
|
56
|
-
.
|
|
57
|
-
|
|
56
|
+
program.command('claim <feature> [description]').description(t('为功能占一个槽位(打印各服务在该槽的端口);可选 description 记录这个槽是干嘛的,供重入参考', 'claim a slot for a feature (prints each service\'s port in that slot); optional description records what this slot is for, as a re-entry hint'))
|
|
57
|
+
.option('-d, --description <text>', t('这个槽的介绍/用途(也可用位置参数传)', 'what this slot is for (can also be passed as the positional arg)'))
|
|
58
|
+
.action(async (f, desc, o) => { try {
|
|
59
|
+
await claimCommand(loadConfig(process.cwd()), f, o.description ?? desc);
|
|
58
60
|
}
|
|
59
61
|
catch (e) {
|
|
60
62
|
die(e.message);
|
|
@@ -66,8 +68,9 @@ program.command('path <feature> <service>').description(t('打印某功能某服
|
|
|
66
68
|
program.command('doctor').description(t('体检:git/配置/各服务仓是否就绪', 'health check: git / config / each service repo readiness'))
|
|
67
69
|
.action(() => sync(doctorCommand));
|
|
68
70
|
program.command('up <feature> <services...>').description(t('一条命令为功能起多个服务(自动 claim + 各服务默认分支 = 功能名)', 'bring up multiple services for a feature (auto-claim + branch defaults to feature name)'))
|
|
69
|
-
.
|
|
70
|
-
|
|
71
|
+
.option('-d, --description <text>', t('这个槽的介绍/用途,写进槽账本供重入参考', 'what this slot is for; recorded in the slot ledger as a re-entry hint'))
|
|
72
|
+
.action(async (f, services, o) => { try {
|
|
73
|
+
await upCommand(loadConfig(process.cwd()), f, services, undefined, o.description);
|
|
71
74
|
}
|
|
72
75
|
catch (e) {
|
|
73
76
|
die(e.message);
|
|
@@ -75,8 +78,9 @@ catch (e) {
|
|
|
75
78
|
program.command('add <feature> <service> [branch] [base]').description(t('为功能在某服务开 worktree(branch 默认 = 功能名)', 'open a worktree for a feature on one service (branch defaults to feature name)'))
|
|
76
79
|
.option('--branch <branch>', t('要创建的分支名(默认 = 功能名)', 'branch to create (default = feature name)'))
|
|
77
80
|
.option('--base <base>', t('分支基点(默认 = origin/<主分支>)', 'base ref for the branch (default = origin/<main>)'))
|
|
81
|
+
.option('-d, --description <text>', t('这个槽的介绍/用途,写进槽账本供重入参考', 'what this slot is for; recorded in the slot ledger as a re-entry hint'))
|
|
78
82
|
.action(async (f, s, b, base, o) => { try {
|
|
79
|
-
await addCommand(loadConfig(process.cwd()), f, s, o.branch ?? b, o.base ?? base);
|
|
83
|
+
await addCommand(loadConfig(process.cwd()), f, s, o.branch ?? b, o.base ?? base, o.description);
|
|
80
84
|
}
|
|
81
85
|
catch (e) {
|
|
82
86
|
die(e.message);
|
|
@@ -113,6 +117,10 @@ program.command('down <feature> [services...]').description(t('拆除功能的 w
|
|
|
113
117
|
catch (e) {
|
|
114
118
|
die(e.message);
|
|
115
119
|
} });
|
|
120
|
+
program.command('logs <feature> [services...]').description(t('看功能各服务 dev server 日志尾部(排障 dev server 起不来/报错);省略 services = 全部', 'tail each service\'s dev server log (debug dev servers that won\'t start / error out); omit services = all'))
|
|
121
|
+
.option('--tail <n>', t('显示末尾多少行(默认 40)', 'how many trailing lines (default 40)'))
|
|
122
|
+
.option('--prev', t('看上一轮启动的日志(.prev)', 'show the previous run\'s log (.prev)'))
|
|
123
|
+
.action((f, s, o) => sync((c) => logsCommand(c, f, s ?? [], { tail: o.tail ? Number(o.tail) : undefined, prev: !!o.prev })));
|
|
116
124
|
program.command('gc').description(t('合并感知回收(默认 dry-run)', 'merge-aware reclaim (dry-run by default)')).option('--apply', t('实际执行回收', 'actually perform the reclaim'))
|
|
117
125
|
.action(async (o) => { try {
|
|
118
126
|
await gcCommand(loadConfig(process.cwd()), !!o.apply);
|
package/dist/commands/add.js
CHANGED
|
@@ -9,17 +9,17 @@ import { mainBranch } from '../git.js';
|
|
|
9
9
|
import { log } from '../util/log.js';
|
|
10
10
|
import { color as c } from '../util/color.js';
|
|
11
11
|
import { t } from '../i18n.js';
|
|
12
|
-
export function resolveAdd(cfg, feature, service, branch) {
|
|
12
|
+
export function resolveAdd(cfg, feature, service, branch, description) {
|
|
13
13
|
if (!cfg.services[service])
|
|
14
14
|
throw new Error(t(`未知服务「${service}」。运行 \`worktree-bay doctor\` 查看配置里有哪些服务。`, `unknown service "${service}". Run \`worktree-bay doctor\` to see configured services.`));
|
|
15
|
-
const slot = claim(cfg, feature);
|
|
15
|
+
const slot = claim(cfg, feature, { branch, description });
|
|
16
16
|
const slug = worktreeDirName(slot, slugify(branch));
|
|
17
17
|
return { service, slot, slug, dir: path.join(repoPath(cfg, service), '.worktrees', slug), repo: repoPath(cfg, service) };
|
|
18
18
|
}
|
|
19
|
-
export async function addCommand(cfg, feature, service, branch, base) {
|
|
19
|
+
export async function addCommand(cfg, feature, service, branch, base, description) {
|
|
20
20
|
const br = branch || feature; // 默认分支 = 功能名
|
|
21
21
|
await withLock(cfg.workspaceRoot, async () => {
|
|
22
|
-
const p = resolveAdd(cfg, feature, service, br);
|
|
22
|
+
const p = resolveAdd(cfg, feature, service, br, description);
|
|
23
23
|
const sp = cfg.services[service];
|
|
24
24
|
const ctxBase = { cfg, service, sp, slot: p.slot, slug: p.slug, dir: p.dir, repo: p.repo };
|
|
25
25
|
const ctx = { ...ctxBase, vars: buildVars(cfg, ctxBase) };
|
|
@@ -45,7 +45,7 @@ export async function addCommand(cfg, feature, service, branch, base) {
|
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
// up: 一条命令为功能批量起多个服务(claim 自动 + 各服务默认分支)。每个服务由 addCommand 自己打印简洁标题。
|
|
48
|
-
export async function upCommand(cfg, feature, services, base) {
|
|
48
|
+
export async function upCommand(cfg, feature, services, base, description) {
|
|
49
49
|
for (const service of services)
|
|
50
|
-
await addCommand(cfg, feature, service, undefined, base);
|
|
50
|
+
await addCommand(cfg, feature, service, undefined, base, description);
|
|
51
51
|
}
|
package/dist/commands/claim.js
CHANGED
|
@@ -4,9 +4,11 @@ import { portOf } from '../ports.js';
|
|
|
4
4
|
import { log } from '../util/log.js';
|
|
5
5
|
import { color as c } from '../util/color.js';
|
|
6
6
|
import { t } from '../i18n.js';
|
|
7
|
-
export async function claimCommand(cfg, feature) {
|
|
8
|
-
const slot = await withLock(cfg.workspaceRoot, async () => claim(cfg, feature));
|
|
7
|
+
export async function claimCommand(cfg, feature, description) {
|
|
8
|
+
const slot = await withLock(cfg.workspaceRoot, async () => claim(cfg, feature, { description }));
|
|
9
9
|
log(c.bold(c.cyan(t(`功能 "${feature}" → 槽 ${slot}`, `feature "${feature}" → slot ${slot}`))));
|
|
10
|
+
if (description)
|
|
11
|
+
log(c.dim(' ' + description));
|
|
10
12
|
for (const [n, sp] of Object.entries(cfg.services))
|
|
11
13
|
log(` ${n.padEnd(8)} ${portOf(sp.port, slot)}`);
|
|
12
14
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { scanOccupancy, slotOfFeature } from '../slots.js';
|
|
3
|
+
import { logPath, readLogTail } from '../proc.js';
|
|
4
|
+
import { log } from '../util/log.js';
|
|
5
|
+
import { color as c } from '../util/color.js';
|
|
6
|
+
import { t } from '../i18n.js';
|
|
7
|
+
// 看某功能各服务 dev server(配了 start 的)托管日志的尾部。排障 dev server 起不来/报错时用,
|
|
8
|
+
// 省得自己拼 .worktree-bay/logs/<slug>-<service>.log 路径再读整个大文件。
|
|
9
|
+
// 日志每次启动滚动(startDetached),当前文件只含本轮;--prev 看上一轮(.prev)。
|
|
10
|
+
export function logsCommand(cfg, feature, services = [], opts = {}) {
|
|
11
|
+
for (const s of services)
|
|
12
|
+
if (!cfg.services[s])
|
|
13
|
+
throw new Error(t(`未知服务「${s}」。运行 \`worktree-bay doctor\` 查看配置里有哪些服务。`, `unknown service "${s}". Run \`worktree-bay doctor\` to see configured services.`));
|
|
14
|
+
const slot = slotOfFeature(cfg, feature);
|
|
15
|
+
if (slot === undefined) {
|
|
16
|
+
log(c.dim(t(`功能「${feature}」未占槽,没有可看的日志。`, `feature "${feature}" has no slot; no logs to show.`)));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
let occ = scanOccupancy(cfg).get(slot) ?? [];
|
|
20
|
+
if (services.length)
|
|
21
|
+
occ = occ.filter((o) => services.includes(o.service));
|
|
22
|
+
occ = occ.filter((o) => cfg.services[o.service].start); // 只有配了 start 的服务才有托管日志
|
|
23
|
+
if (!occ.length) {
|
|
24
|
+
log(c.dim(t('没有可看日志的运行体(这些服务未配置 start dev server)。', 'no logs available (those services have no start dev server configured).')));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const tail = opts.tail ?? 40;
|
|
28
|
+
for (const o of occ) {
|
|
29
|
+
const file = logPath(cfg.workspaceRoot, o.slug, o.service) + (opts.prev ? '.prev' : '');
|
|
30
|
+
log(c.bold(c.cyan(o.service)) + c.dim(' ' + file));
|
|
31
|
+
if (!fs.existsSync(file)) {
|
|
32
|
+
log(c.dim(t(opts.prev ? ' (无上一轮日志)' : ' (暂无日志)', opts.prev ? ' (no previous-run log)' : ' (no log yet)')));
|
|
33
|
+
log('');
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const body = readLogTail(file, tail);
|
|
37
|
+
log(body || c.dim(t(' (日志为空)', ' (log is empty)')));
|
|
38
|
+
log('');
|
|
39
|
+
}
|
|
40
|
+
}
|
package/dist/commands/ls.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { scanOccupancy,
|
|
1
|
+
import { scanOccupancy, readSlots } from '../slots.js';
|
|
2
2
|
import { portOf } from '../ports.js';
|
|
3
3
|
import { log } from '../util/log.js';
|
|
4
4
|
import { pidOnPort } from '../proc.js';
|
|
@@ -9,22 +9,38 @@ import { t } from '../i18n.js';
|
|
|
9
9
|
function running(port) { return !!pidOnPort(port); }
|
|
10
10
|
export function renderSlots(cfg) {
|
|
11
11
|
const occ = scanOccupancy(cfg);
|
|
12
|
-
const
|
|
13
|
-
const slots = new Set([...occ.keys(), ...Object.keys(
|
|
12
|
+
const metas = readSlots(cfg);
|
|
13
|
+
const slots = new Set([...occ.keys(), ...Object.keys(metas).map(Number)]);
|
|
14
14
|
const lines = [];
|
|
15
15
|
for (const n of [...slots].sort((a, b) => a - b)) {
|
|
16
|
+
const meta = metas[String(n)];
|
|
16
17
|
const svc = (occ.get(n) ?? []).map((o) => { const p = portOf(cfg.services[o.service].port, n); const dot = running(p) ? c.green('●') : c.dim('●'); return `${dot}${o.service}@${p}`; });
|
|
17
|
-
lines.push(`${c.bold(c.cyan(String(n)))}${c.dim(':')} ${c.bold(
|
|
18
|
+
lines.push(`${c.bold(c.cyan(String(n)))}${c.dim(':')} ${c.bold(meta?.feature ?? t('(未命名)', '(unnamed)'))} [${svc.join(', ') || c.dim(t('无 worktree', 'no worktree'))}]`);
|
|
19
|
+
if (meta) { // 副行:介绍 + 分支(异于功能名时) + 创建日期,给重入/总览更多上下文
|
|
20
|
+
const bits = [];
|
|
21
|
+
if (meta.description)
|
|
22
|
+
bits.push(meta.description);
|
|
23
|
+
if (meta.branch && meta.branch !== meta.feature)
|
|
24
|
+
bits.push(t(`分支 ${meta.branch}`, `branch ${meta.branch}`));
|
|
25
|
+
if (meta.createdAt)
|
|
26
|
+
bits.push(meta.createdAt.slice(0, 10));
|
|
27
|
+
if (bits.length)
|
|
28
|
+
lines.push(' ' + c.dim(bits.join(' · ')));
|
|
29
|
+
}
|
|
18
30
|
}
|
|
19
31
|
return lines.join('\n') || t('(无槽位在用)', '(no slots in use)');
|
|
20
32
|
}
|
|
21
33
|
export function slotsData(cfg) {
|
|
22
34
|
const occ = scanOccupancy(cfg);
|
|
23
|
-
const
|
|
24
|
-
const slots = new Set([...occ.keys(), ...Object.keys(
|
|
25
|
-
return [...slots].sort((a, b) => a - b).map((n) =>
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
const metas = readSlots(cfg);
|
|
36
|
+
const slots = new Set([...occ.keys(), ...Object.keys(metas).map(Number)]);
|
|
37
|
+
return [...slots].sort((a, b) => a - b).map((n) => {
|
|
38
|
+
const meta = metas[String(n)];
|
|
39
|
+
return {
|
|
40
|
+
slot: n, feature: meta?.feature ?? null, branch: meta?.branch ?? null,
|
|
41
|
+
description: meta?.description ?? null, createdAt: meta?.createdAt ?? null,
|
|
42
|
+
services: (occ.get(n) ?? []).map((o) => { const p = portOf(cfg.services[o.service].port, n); return { service: o.service, port: p, dir: o.dir, running: running(p) }; }),
|
|
43
|
+
};
|
|
44
|
+
});
|
|
29
45
|
}
|
|
30
46
|
export function lsCommand(cfg, json = false) { log(json ? JSON.stringify(slotsData(cfg), null, 2) : renderSlots(cfg)); }
|
package/dist/engine.js
CHANGED
|
@@ -26,6 +26,26 @@ export function mergeEnvText(text, kv) {
|
|
|
26
26
|
}
|
|
27
27
|
return out.join('\n');
|
|
28
28
|
}
|
|
29
|
+
// 把 env 规格渲染并合并进 worktree 的 dotenv 文件。幂等:合并后内容与现有文件一致就【跳过写入】,
|
|
30
|
+
// 避免无谓刷新 mtime 触发 dev server 的 .env 文件 watcher(如 vite)抖动重启——重跑 up/add(值已正确)很常见,
|
|
31
|
+
// 运行中的前端被反复重启会偶发解析失败。返回实际写入的文件名(便于观测/测试)。
|
|
32
|
+
export function writeEnvFiles(dir, env, vars) {
|
|
33
|
+
const written = [];
|
|
34
|
+
for (const [file, kv] of Object.entries(env ?? {})) {
|
|
35
|
+
const fp = path.join(dir, file);
|
|
36
|
+
const exists = fs.existsSync(fp);
|
|
37
|
+
const cur = exists ? fs.readFileSync(fp, 'utf8') : '';
|
|
38
|
+
const rendered = {};
|
|
39
|
+
for (const [k, v] of Object.entries(kv))
|
|
40
|
+
rendered[k] = renderTemplate(v, vars);
|
|
41
|
+
const next = mergeEnvText(cur, rendered);
|
|
42
|
+
if (!exists || next !== cur) {
|
|
43
|
+
fs.writeFileSync(fp, next);
|
|
44
|
+
written.push(file);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return written;
|
|
48
|
+
}
|
|
29
49
|
export function resolveUpstreamBase(cfg, slot, up, materialized) {
|
|
30
50
|
return materialized ? `http://localhost:${portOf(cfg.services[up.service].port, slot)}` : up.fallback;
|
|
31
51
|
}
|
|
@@ -55,14 +75,7 @@ export async function bringUp(ctx, base, branch) {
|
|
|
55
75
|
warn(t(`⚠ ${lock} 与主 checkout 不一致,拷来的依赖可能版本错位;建议把该服务的 copy 去掉、改用 setup 跑安装命令。`, `⚠ ${lock} differs from the main checkout; copied dependencies may be the wrong version. Consider dropping copy for this service and installing via setup instead.`));
|
|
56
76
|
}
|
|
57
77
|
}
|
|
58
|
-
|
|
59
|
-
const fp = path.join(dir, file);
|
|
60
|
-
const cur = fs.existsSync(fp) ? fs.readFileSync(fp, 'utf8') : '';
|
|
61
|
-
const rendered = {};
|
|
62
|
-
for (const [k, v] of Object.entries(kv))
|
|
63
|
-
rendered[k] = renderTemplate(v, vars);
|
|
64
|
-
fs.writeFileSync(fp, mergeEnvText(cur, rendered));
|
|
65
|
-
}
|
|
78
|
+
writeEnvFiles(dir, sp.env, vars);
|
|
66
79
|
if (sp.setup) {
|
|
67
80
|
const cmd = renderTemplate(sp.setup, vars);
|
|
68
81
|
const r = await runShellLive(cmd, { cwd: dir }, t(`setup:${cmd}`, `setup: ${cmd}`));
|
package/dist/mcp.js
CHANGED
|
@@ -26,8 +26,9 @@ export const INSTRUCTIONS = `worktree-bay 是「功能 = 槽位」的并行开
|
|
|
26
26
|
|
|
27
27
|
要点:
|
|
28
28
|
- 一个功能从头到尾用同一个功能名(= 默认分支名)。
|
|
29
|
+
- 每个新任务都用一个【新的功能名】调 worktree_bay_up,让工具自动占一个空槽——【不要】去 worktree_bay_ls 挑一个现成的槽来复用。ls 是给你看占用情况的,不是用来选槽复用的;复用别的功能已占的槽会破坏隔离、互相污染。唯一例外:你是在【继续同一个功能】之前没跑完的工作(功能名相同),这时 up 会幂等复用它自己的槽。起 up / claim 时【带上 description】写明这个槽是干嘛的(记进槽账本、ls 会显示、ls --json 含 description/branch/createdAt),方便你之后重入时判断某个槽在做什么。
|
|
29
30
|
- 只起「实际要改」的服务,不要全起;不知道有哪些服务名先调 worktree_bay_doctor。
|
|
30
|
-
- 拿不准当前状态先调 worktree_bay_ls
|
|
31
|
+
- 拿不准当前状态先调 worktree_bay_ls;dev server 起不来或报错就调 worktree_bay_logs 看日志尾部排障。
|
|
31
32
|
- worktree_bay_gc 默认只读(dry-run 列建议),apply=true 才真删,且只删「已合并到主分支且工作区干净」的,保守不误删。
|
|
32
33
|
- worktree_bay_init 在新工作区生成配置骨架(已存在则不覆盖);worktree_bay_claim 只占槽并打印端口、不建 worktree(一般直接用 up 即可)。
|
|
33
34
|
- 要写/改 worktree-bay.config.json、或拿不准任何命令/参数/配置细节,先调 worktree_bay_skill 取完整指南(每个配置原语、模板变量、校验规则、完整示例)。`;
|
|
@@ -37,15 +38,15 @@ export const TOOLS = [
|
|
|
37
38
|
inputSchema: { type: 'object', properties: {} }, toArgs: () => ['doctor'] },
|
|
38
39
|
{ name: 'worktree_bay_ls', description: '列出所有功能槽位与占用(JSON:每槽的功能名、已起服务及端口、各 worktree 绝对路径),用于总览当前并行开发状态',
|
|
39
40
|
inputSchema: { type: 'object', properties: {} }, toArgs: () => ['ls', '--json'] },
|
|
40
|
-
{ name: 'worktree_bay_up', description: '为一个功能一次性起多个服务(自动占槽 + 各服务开 worktree
|
|
41
|
-
inputSchema: { type: 'object', properties: { feature: str, services: { type: 'array', items: str } }, required: ['feature', 'services'] },
|
|
42
|
-
toArgs: (a) => ['up', String(a.feature), ...(a.services ?? [])] },
|
|
43
|
-
{ name: 'worktree_bay_claim', description: '只为功能占一个槽位并打印各服务在该槽的端口,不开 worktree(一般直接用 up
|
|
44
|
-
inputSchema: { type: 'object', properties: { feature: str }, required: ['feature'] },
|
|
45
|
-
toArgs: (a) => ['claim', String(a.feature)] },
|
|
46
|
-
{ name: 'worktree_bay_add', description: '为功能在单个服务上开 worktree(branch
|
|
47
|
-
inputSchema: { type: 'object', properties: { feature: str, service: str, branch: str }, required: ['feature', 'service'] },
|
|
48
|
-
toArgs: (a) => ['add', String(a.feature), String(a.service), ...(a.branch ? [String(a.branch)] : [])] },
|
|
41
|
+
{ name: 'worktree_bay_up', description: '为一个功能一次性起多个服务(自动占槽 + 各服务开 worktree,分支默认=功能名,前端自动接同槽后端)。并行开发新功能首选。【强烈建议带 description】写明这个槽是干嘛的,记进槽账本、`ls` 会显示,供之后重入参考。',
|
|
42
|
+
inputSchema: { type: 'object', properties: { feature: str, services: { type: 'array', items: str }, description: str }, required: ['feature', 'services'] },
|
|
43
|
+
toArgs: (a) => ['up', String(a.feature), ...(a.services ?? []), ...(a.description ? ['--description', String(a.description)] : [])] },
|
|
44
|
+
{ name: 'worktree_bay_claim', description: '只为功能占一个槽位并打印各服务在该槽的端口,不开 worktree(一般直接用 up 即可;需要先预览端口/预约槽时用)。description 记录这个槽的用途供重入参考。',
|
|
45
|
+
inputSchema: { type: 'object', properties: { feature: str, description: str }, required: ['feature'] },
|
|
46
|
+
toArgs: (a) => ['claim', String(a.feature), ...(a.description ? ['--description', String(a.description)] : [])] },
|
|
47
|
+
{ name: 'worktree_bay_add', description: '为功能在单个服务上开 worktree(branch 省略则用功能名)。description 记录这个槽的用途供重入参考。',
|
|
48
|
+
inputSchema: { type: 'object', properties: { feature: str, service: str, branch: str, description: str }, required: ['feature', 'service'] },
|
|
49
|
+
toArgs: (a) => ['add', String(a.feature), String(a.service), ...(a.branch ? [String(a.branch)] : []), ...(a.description ? ['--description', String(a.description)] : [])] },
|
|
49
50
|
{ name: 'worktree_bay_path', description: '打印某功能某服务的 worktree 绝对路径——up 之后用它定位代码目录,再进去改文件',
|
|
50
51
|
inputSchema: { type: 'object', properties: { feature: str, service: str }, required: ['feature', 'service'] },
|
|
51
52
|
toArgs: (a) => ['path', String(a.feature), String(a.service)] },
|
|
@@ -64,6 +65,9 @@ export const TOOLS = [
|
|
|
64
65
|
{ name: 'worktree_bay_down', description: '拆除 worktree:省略 services 拆整个功能(所有服务),给 services 只拆这些服务(默认查脏/未推保护,force=true 强删)',
|
|
65
66
|
inputSchema: { type: 'object', properties: { feature: str, services: { type: 'array', items: str }, force: { type: 'boolean' } }, required: ['feature'] },
|
|
66
67
|
toArgs: (a) => ['down', String(a.feature), ...(a.services ?? []), ...(a.force ? ['-f'] : [])] },
|
|
68
|
+
{ name: 'worktree_bay_logs', description: '看功能各服务 dev server 的日志尾部——dev server 起不来/报错时排障用,免得自己拼日志路径。services 省略=全部;tail 指定行数(默认 40);prev=true 看上一轮启动的日志(每次启动会滚动)。',
|
|
69
|
+
inputSchema: { type: 'object', properties: { feature: str, services: { type: 'array', items: str }, tail: { type: 'number' }, prev: { type: 'boolean' } }, required: ['feature'] },
|
|
70
|
+
toArgs: (a) => ['logs', String(a.feature), ...(a.services ?? []), ...(a.tail ? ['--tail', String(a.tail)] : []), ...(a.prev ? ['--prev'] : [])] },
|
|
67
71
|
{ name: 'worktree_bay_gc', description: '合并感知回收:默认 dry-run 只列建议,apply=true 才实际删除「已合并且干净」的功能',
|
|
68
72
|
inputSchema: { type: 'object', properties: { apply: { type: 'boolean' } } },
|
|
69
73
|
toArgs: (a) => ['gc', ...(a.apply ? ['--apply'] : [])] },
|
package/dist/proc.js
CHANGED
|
@@ -34,6 +34,10 @@ export function readLogTail(file, lines = 15) {
|
|
|
34
34
|
return '';
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
// 某 worktree dev server 的日志路径(startDetached 写、logs 命令读,单一来源避免两处拼接漂移)。
|
|
38
|
+
export function logPath(ws, slug, service) {
|
|
39
|
+
return path.join(ws, '.worktree-bay', 'logs', `${slug}-${service}.log`);
|
|
40
|
+
}
|
|
37
41
|
// 找出监听某端口的进程 pid(shell/pnpm 等中间层会让记录的 pid 漂移,按端口查最可靠)。
|
|
38
42
|
export function pidOnPort(port) {
|
|
39
43
|
if (process.platform === 'win32') {
|
|
@@ -126,7 +130,16 @@ export function stopUnrecordedOnPort(ws, dir, port) {
|
|
|
126
130
|
export function startDetached(ws, dir, service, slug, port, cmd) {
|
|
127
131
|
const logDir = path.join(ws, '.worktree-bay', 'logs');
|
|
128
132
|
fs.mkdirSync(logDir, { recursive: true });
|
|
129
|
-
const log =
|
|
133
|
+
const log = logPath(ws, slug, service);
|
|
134
|
+
// 每次启动滚动日志:上一轮留一份 .prev,当前日志只含本次运行,排障时不被跨会话的历史淹没。
|
|
135
|
+
// (fs.rename 跨平台覆盖已存在目标;失败也不阻断启动。)
|
|
136
|
+
try {
|
|
137
|
+
if (fs.existsSync(log) && fs.statSync(log).size > 0)
|
|
138
|
+
fs.renameSync(log, log + '.prev');
|
|
139
|
+
}
|
|
140
|
+
catch { /* 滚动失败忽略 */ }
|
|
141
|
+
// 启动头:标清「本次启动」的时间与命令,作为日志里这一轮运行的起点。
|
|
142
|
+
fs.writeFileSync(log, `===== worktree-bay start ${new Date().toISOString()} :: ${cmd} =====\n`);
|
|
130
143
|
const fd = fs.openSync(log, 'a');
|
|
131
144
|
// 后台启动、CLI 退出后仍存活、且不弹窗:
|
|
132
145
|
// - Windows:不要 detached(detached 会新开控制台窗口、且与 windowsHide 冲突);用 windowsHide 抑制窗口,
|
package/dist/slots.js
CHANGED
|
@@ -23,28 +23,66 @@ export function scanOccupancy(cfg) {
|
|
|
23
23
|
return map;
|
|
24
24
|
}
|
|
25
25
|
function labelPath(cfg) { return path.join(cfg.workspaceRoot, '.worktree-bay-slots.json'); }
|
|
26
|
-
export function
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
export function readSlots(cfg) {
|
|
27
|
+
const p = labelPath(cfg);
|
|
28
|
+
if (!fs.existsSync(p))
|
|
29
|
+
return {};
|
|
30
|
+
const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
31
|
+
const out = {};
|
|
32
|
+
for (const [k, v] of Object.entries(raw))
|
|
33
|
+
out[k] = typeof v === 'string' ? { feature: v } : v;
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
function save(cfg, store) { fs.writeFileSync(labelPath(cfg), JSON.stringify(store, null, 2) + '\n'); }
|
|
37
|
+
// 兼容旧调用:slot → 功能名(completion/ls 等仍按字符串用)。
|
|
38
|
+
export function readLabels(cfg) { const o = {}; for (const [k, v] of Object.entries(readSlots(cfg)))
|
|
39
|
+
o[k] = v.feature; return o; }
|
|
40
|
+
export function removeLabel(cfg, slot) { const s = readSlots(cfg); delete s[String(slot)]; save(cfg, s); }
|
|
41
|
+
export function slotOfFeature(cfg, f) { for (const [k, v] of Object.entries(readSlots(cfg)))
|
|
42
|
+
if (v.feature === f)
|
|
32
43
|
return Number(k); return undefined; }
|
|
33
44
|
export function freeSlot(cfg) {
|
|
34
45
|
const occ = scanOccupancy(cfg);
|
|
35
|
-
const
|
|
46
|
+
const s = readSlots(cfg);
|
|
36
47
|
for (let n = 1; n <= cfg.maxSlots; n++)
|
|
37
|
-
if (!occ.has(n) &&
|
|
48
|
+
if (!occ.has(n) && s[String(n)] === undefined)
|
|
38
49
|
return n;
|
|
39
50
|
throw new Error(t(`没有空闲槽位(1..${cfg.maxSlots} 全部占用)。用 \`worktree-bay gc\` 回收已合并的,或 \`worktree-bay down <功能>\` 拆掉用完的,或调大配置里的 maxSlots。`, `no free slot (1..${cfg.maxSlots} all taken). Reclaim merged ones with \`worktree-bay gc\`, tear down finished ones with \`worktree-bay down <feature>\`, or raise maxSlots in your config.`));
|
|
40
51
|
}
|
|
41
|
-
|
|
42
|
-
|
|
52
|
+
// 占槽。首次认领写入 { feature, branch?, description?, createdAt };已占同名功能则【补全/更新】非空的
|
|
53
|
+
// branch / description(保留首次的 createdAt)——所以同一功能重入 up/claim 带上新 --description 即可改介绍。
|
|
54
|
+
export function claim(cfg, f, meta = {}) {
|
|
55
|
+
const store = readSlots(cfg);
|
|
56
|
+
const existing = slotOfFeature(cfg, f);
|
|
57
|
+
if (existing !== undefined) {
|
|
58
|
+
const cur = store[String(existing)];
|
|
59
|
+
const next = { ...cur };
|
|
60
|
+
if (meta.branch)
|
|
61
|
+
next.branch = meta.branch;
|
|
62
|
+
if (meta.description)
|
|
63
|
+
next.description = meta.description;
|
|
64
|
+
if (JSON.stringify(next) !== JSON.stringify(cur)) {
|
|
65
|
+
store[String(existing)] = next;
|
|
66
|
+
save(cfg, store);
|
|
67
|
+
}
|
|
68
|
+
return existing;
|
|
69
|
+
}
|
|
70
|
+
const n = freeSlot(cfg);
|
|
71
|
+
const m = { feature: f };
|
|
72
|
+
if (meta.branch)
|
|
73
|
+
m.branch = meta.branch;
|
|
74
|
+
if (meta.description)
|
|
75
|
+
m.description = meta.description;
|
|
76
|
+
m.createdAt = new Date().toISOString();
|
|
77
|
+
store[String(n)] = m;
|
|
78
|
+
save(cfg, store);
|
|
79
|
+
return n;
|
|
80
|
+
}
|
|
43
81
|
export function pruneEmptyLabels(cfg) {
|
|
44
82
|
const occ = scanOccupancy(cfg);
|
|
45
83
|
const removed = [];
|
|
46
|
-
for (const [k, v] of Object.entries(
|
|
84
|
+
for (const [k, v] of Object.entries(readSlots(cfg)))
|
|
47
85
|
if (!occ.has(Number(k)))
|
|
48
|
-
removed.push({ slot: Number(k), feature: v });
|
|
86
|
+
removed.push({ slot: Number(k), feature: v.feature });
|
|
49
87
|
return removed;
|
|
50
88
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "worktree-bay",
|
|
3
|
-
"version": "4.0
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"description": "Per-feature git worktree + port slots for parallel multi-service development: auto deps, env wiring, frontend-to-backend, merge-aware reclaim, plus an MCP server for AI agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"git",
|
package/skill.md
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
- 同槽的前端服务自动把 api base 指向同槽的后端端口。
|
|
10
10
|
- 槽位占用从文件系统派生(看 `<repo>/.worktrees/s<N>-*` 是否存在),删了 worktree 槽自动空出。
|
|
11
11
|
|
|
12
|
+
> **每个新任务都用一个【新功能名】认领新槽**(`up <新功能名> ...`),让工具自动占一个空槽。**不要先 `ls` 再去挑一个现成的槽来复用**——`ls` 是给你看占用情况的,不是用来选槽复用的;复用别的功能已占的槽会破坏隔离、互相污染数据。唯一例外:你在**继续同一个功能**之前没跑完的工作(用同一功能名),此时 `up` 会幂等复用它自己的槽。起槽时给 `up`/`claim`/`add` 带 `-d/--description` 写明这个槽是干嘛的——记进槽账本、`ls` 会显示、`ls --json` 含 `description`/`branch`/`createdAt`,方便日后重入判断某个槽在做什么。
|
|
13
|
+
|
|
12
14
|
---
|
|
13
15
|
|
|
14
16
|
## 安装
|
|
@@ -28,9 +30,9 @@ worktree-bay completion install # 一键装 shell 补全(可选)
|
|
|
28
30
|
|---|---|
|
|
29
31
|
| `worktree-bay init` | 在当前工作区生成 `worktree-bay.config.json`(扫描子 git 仓预填服务) |
|
|
30
32
|
| `worktree-bay doctor` | 体检:git 是否可用、配置是否有效、各服务仓是否就绪 |
|
|
31
|
-
| `worktree-bay up <feature> <service...>` | **最常用**:一条命令为功能起多个服务(自动占槽 + 各服务开 worktree,分支默认 =
|
|
32
|
-
| `worktree-bay claim <feature
|
|
33
|
-
| `worktree-bay add <feature> <service> [branch] [base]` | 为功能在单个服务开 worktree。`branch` 省略 = 功能名;`base` 省略 = `origin
|
|
33
|
+
| `worktree-bay up <feature> <service...>` | **最常用**:一条命令为功能起多个服务(自动占槽 + 各服务开 worktree,分支默认 = 功能名)。`-d/--description` 记录这个槽的用途(写进槽账本,`ls` 显示,重入参考) |
|
|
34
|
+
| `worktree-bay claim <feature> [description]` | 只占一个槽、打印各服务在该槽的端口(不开 worktree)。可选 `description`(位置参数或 `-d`)记录槽用途 |
|
|
35
|
+
| `worktree-bay add <feature> <service> [branch] [base]` | 为功能在单个服务开 worktree。`branch` 省略 = 功能名;`base` 省略 = `origin/<主分支>`;`-d/--description` 记录槽用途 |
|
|
34
36
|
| `worktree-bay ls [--json]` | 列出所有槽位:功能名、已起服务及端口;`--json` 输出结构化数据(含 worktree 绝对路径,便于脚本/AI 消费)。合并状态由 `gc` 判定,`ls` 不查(避免每次都 `git fetch`) |
|
|
35
37
|
| `worktree-bay path <feature> <service>` | 打印某服务 worktree 的绝对路径(可 `cd $(worktree-bay path f api)`) |
|
|
36
38
|
| `worktree-bay run <feature> <service> <name> [args...]` | 在某服务运行体里跑配置的 `run.<name>`(如 test),透传 args |
|
|
@@ -39,6 +41,7 @@ worktree-bay completion install # 一键装 shell 补全(可选)
|
|
|
39
41
|
| `worktree-bay stop <feature> [services...]` | 停止功能的运行体(停 docker + 杀 node dev server);省略 = 全部,可列多个。保留 worktree |
|
|
40
42
|
| `worktree-bay restart <feature> [services...]` | 重启运行体(停掉再起);省略 = 全部,可列多个 |
|
|
41
43
|
| `worktree-bay down <feature> [services...]` | 拆除 worktree(停运行体 + teardown + 删 worktree);**省略 services = 整功能**,也可列多个只拆这些。默认查脏/未推保护,`-f` 强删 |
|
|
44
|
+
| `worktree-bay logs <feature> [services...]` | 看各服务 dev server 日志尾部(排障 dev server 起不来/报错);省略 services = 全部。`--tail N` 行数(默认 40)、`--prev` 看上一轮启动的日志 |
|
|
42
45
|
| `worktree-bay gc [--apply]` | 合并感知回收:默认 dry-run 只列建议,`--apply` 才删「已合并且干净」的 |
|
|
43
46
|
| `worktree-bay completion <install\|bash\|zsh\|fish>` | `install` 一键装进 shell;或打印补全脚本 |
|
|
44
47
|
| `worktree-bay mcp` | 启动 MCP 服务(stdio,轻量脚本,客户端按需 spawn),供 AI 调用 |
|
|
@@ -81,7 +84,7 @@ worktree-bay gc # 回收已合并的
|
|
|
81
84
|
| `repo` | | string | 仓库目录名(相对 workspaceRoot),**默认 = 服务名** |
|
|
82
85
|
| `vars` | | object | 自定义模板变量,值里可引用基础变量,如 `{ "project": "myapi-{slug}" }` |
|
|
83
86
|
| `copy` | | string[] | 挂入时从主 checkout **递归拷贝**到 worktree 的文件/目录(含依赖目录,如 `vendor`)。含符号链接也安全(会跟随拷目标内容) |
|
|
84
|
-
| `env` | | object | dotenv 注入:`{ "文件名": { "KEY": "值模板" } }
|
|
87
|
+
| `env` | | object | dotenv 注入:`{ "文件名": { "KEY": "值模板" } }`,把键值**合并**进该文件(保留其它行,文件不存在则建)。**幂等**:合并结果与现有内容一致就跳过写入、不刷新 mtime——避免重跑 `up`/`add` 时无谓触发前端 `.env` watcher(如 vite)抖动重启 |
|
|
85
88
|
| `upstream` | | object | 声明依赖的上游服务:`{ "service": "api", "fallback": "http://localhost:6001" }`,产出 `{upstreamBase}` 变量 |
|
|
86
89
|
| `setup` | | string | 挂入后执行的 shell 命令(创建/装好运行体,如 `docker compose up -d`、`pnpm install`)。`up` 时跑 |
|
|
87
90
|
| `teardown` | | string | 拆除时执行的 shell 命令(销毁运行体,如 `docker compose down -v`)。`down`/`rm`/`gc` 时跑 |
|
|
@@ -162,8 +165,8 @@ worktree-bay gc # 回收已合并的
|
|
|
162
165
|
## 工作原理要点
|
|
163
166
|
|
|
164
167
|
- **全命令幂等**:`up / down / start / stop / restart` 重复执行都收敛到同一目标态、不报错——`up` 重入=复用 worktree + 恢复运行体;`start`/`stop` 已在目标态则跳过/仍逐服务给状态;`down` 对未占槽或已拆的服务是友好 no-op。仅「未知服务名」(typo,不在 config)才报错。
|
|
165
|
-
- **占用真相 = 文件系统**:扫各服务 `.worktrees/s<N>-*`;`.worktree-bay-slots.json`
|
|
166
|
-
- **dev server 托管**:`start` 进程后台 detach 启动、日志落 `.worktree-bay/logs/`、按端口追踪真实 pid
|
|
168
|
+
- **占用真相 = 文件系统**:扫各服务 `.worktrees/s<N>-*`;`.worktree-bay-slots.json` 是槽位元数据账本(每槽记 `feature` / `branch` / `description` / `createdAt`;旧的纯字符串值自动兼容为 `{feature}`),属预约标记,真正占用仍以 worktree 是否存在为准。
|
|
169
|
+
- **dev server 托管**:`start` 进程后台 detach 启动、日志落 `.worktree-bay/logs/`、按端口追踪真实 pid。日志**每次启动滚动**(上一轮存一份 `.prev`,当前文件只含本轮 + 一行启动头,排障不被跨会话历史淹没;用 `worktree-bay logs <feature>` 直接看尾部,免拼路径)。`start` 会**阻塞到约定端口被监听才返回**(给 vite 冷启动留 ~25s),所以命令返回即代表就绪、`ls` 行首 `●` 绿即可开工;超时不算失败(可能仍在编译/重启),会提示去看日志。`stop`/`down` 按端口可靠停。
|
|
167
170
|
- **运行状态判断 = 端口**:`ls`/`start`/`stop` 判「在不在跑」全用 `pidOnPort`(netstat/lsof,与 ls 同源),不用 connect 探测(docker 发布端口两者会不一致)、也不只看 pid 账本(dir 形态会漂移、docker 无账本记录)。`start` 端口已在监听就跳过、不再误报「恢复」。
|
|
168
171
|
- **stop 严格停「本目录+本端口+本进程」**:① 优先用启动账本(dir 已规范化匹配:相对/绝对/大小写都认);② 账本缺失时**校验后才杀**——Linux/macOS 比对进程 `cwd` 是否为本 worktree,Windows 取不到 cwd 则核对命令行含 `--port <本端口>`(端口按槽唯一,等价确证);都确证不了就**不动它**并如实报告(绝不凭端口盲杀)。每个服务都有状态行:已停 / 端口空闲 / 无法确认未停。
|
|
169
172
|
- **并发安全**:`claim/add/up/rm/down/gc` 全程持工作区原子锁。
|
|
@@ -174,7 +177,9 @@ worktree-bay gc # 回收已合并的
|
|
|
174
177
|
|
|
175
178
|
## 给 AI(MCP)
|
|
176
179
|
|
|
177
|
-
`worktree-bay mcp` 暴露工具:`worktree_bay_doctor / ls / up / claim / add / path / run / start / stop / restart / down / gc / init / skill`。`doctor` 列出全部服务(AI 借此得知服务名);`ls` 以 JSON 返回 `[{slot, feature(可 null), services:[{service, port, dir, running}]}]`(含各 worktree 绝对路径、布尔 `running`);`path` 给某功能某服务的 worktree 目录;`start/stop/restart` 控制运行体(docker+node);`down` 省略 services 拆整功能、给 services
|
|
180
|
+
`worktree-bay mcp` 暴露工具:`worktree_bay_doctor / ls / up / claim / add / path / run / start / stop / restart / down / logs / gc / init / skill`。`doctor` 列出全部服务(AI 借此得知服务名);`ls` 以 JSON 返回 `[{slot, feature(可 null), services:[{service, port, dir, running}]}]`(含各 worktree 绝对路径、布尔 `running`);`path` 给某功能某服务的 worktree 目录;`start/stop/restart` 控制运行体(docker+node);`down` 省略 services 拆整功能、给 services 只拆这些;`logs` 看 dev server 日志尾部排障。要写或修改 `worktree-bay.config.json`、或拿不准命令/配置细节时,调用 `worktree_bay_skill` 取本指南全文。
|
|
181
|
+
|
|
182
|
+
**给 AI 的硬约束**:每个新任务用一个【新功能名】`up` 认领新槽,让工具自动占空槽;**不要用 `ls` 去挑现成的槽复用**(破坏隔离、污染数据)。只有继续同一功能未完成的工作才用同名 `up`(幂等复用自己的槽)。dev server 起不来/报错先 `logs` 看日志,别瞎猜。
|
|
178
183
|
|
|
179
184
|
## 常见坑
|
|
180
185
|
|