@maxiaochao/pi-worktree 0.3.2 → 0.4.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/CHANGELOG.md +5 -0
- package/README.md +6 -7
- package/TESTING.md +5 -5
- package/bin/pi-worktree +31 -66
- package/bin/pi-worktree-completion.bash +4 -4
- package/docs/demo.gif +0 -0
- package/docs/screenshot.png +0 -0
- package/extensions/worktree.ts +30 -30
- package/package.json +1 -1
- package/skills/pi-worktree/SKILL.md +3 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026-10-12 - v0.4.0
|
|
4
|
+
|
|
5
|
+
- Replace CLI start and prepare with the create-only command
|
|
6
|
+
- replace the CLI `start` and `prepare` commands with one idempotent `create` command that never launches Pi
|
|
7
|
+
|
|
3
8
|
## 2026-10-09 - v0.3.2
|
|
4
9
|
|
|
5
10
|
- continue Agent tasks automatically after switching into a worktree
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @maxiaochao/pi-worktree
|
|
2
2
|
|
|
3
|
-
`pi-worktree` CLI、Pi Extension 与配套 skill
|
|
3
|
+
`pi-worktree` CLI、Pi Extension 与配套 skill。CLI 负责管理 worktree;Extension 让用户或 Agent 在当前 Pi 界面中切换,并保留完整对话。
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|

|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
pi-worktree
|
|
17
|
-
pi-worktree
|
|
16
|
+
pi-worktree create feature
|
|
17
|
+
pi-worktree create --base main --json feature
|
|
18
18
|
pi-worktree info feature
|
|
19
19
|
pi-worktree out
|
|
20
20
|
pi-worktree remove feature
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
`
|
|
23
|
+
`create` 从当前分支(或显式 `--base`)创建或复用 `.worktrees/feature` 并记录 base,不启动 Pi。`--json` 提供相同操作的机器可读结果。
|
|
24
24
|
|
|
25
25
|
在交互式 Pi 中,用户可以保留当前界面和完整对话:
|
|
26
26
|
|
|
@@ -61,8 +61,7 @@ pi update npm:@maxiaochao/pi-worktree # 更新独立子包
|
|
|
61
61
|
## 命令
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
|
-
pi-worktree
|
|
65
|
-
pi-worktree prepare [--base <ref>] [--json] <name>
|
|
64
|
+
pi-worktree create [--base <ref>] [--json] <name>
|
|
66
65
|
pi-worktree list
|
|
67
66
|
pi-worktree info <name>
|
|
68
67
|
pi-worktree out
|
|
@@ -70,6 +69,6 @@ pi-worktree remove <name>
|
|
|
70
69
|
pi-worktree prune
|
|
71
70
|
```
|
|
72
71
|
|
|
73
|
-
`
|
|
72
|
+
`create` 是幂等接口;`create --json` 供 Extension 和其他宿主使用,返回 `created`、`attached`、`reused` 或 `already-active`,以及绝对路径、分支和 dirty 状态。已有 worktree 会复用;已存在但未挂载的同名分支会挂载;stale 注册、非法名称和路径冲突会显式失败。
|
|
74
73
|
|
|
75
74
|
Agent 用法见 `skills/pi-worktree/SKILL.md`。测试流程、夹具和结果见 [TESTING.md](TESTING.md)。
|
package/TESTING.md
CHANGED
|
@@ -4,19 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
1. `npm --prefix packages/worktree test`:在临时 Git 仓库中运行 CLI,并验证 Extension 注册、延迟命令桥接和 session 切换生命周期。
|
|
6
6
|
2. `npm pack ./packages/worktree --dry-run`:确认 CLI、completion、skill 和安装脚本进入 tarball。
|
|
7
|
-
3. 手工验收发布包的 `pi-worktree
|
|
7
|
+
3. 手工验收发布包的 `pi-worktree create` 创建与复用流程。
|
|
8
8
|
4. 确认 README 示例、真实临时 Git 仓库输出 `docs/screenshot.png` 和 `docs/demo.gif` 进入 tarball。
|
|
9
9
|
|
|
10
10
|
## 用例数据
|
|
11
11
|
|
|
12
12
|
- `--help` 输出命令说明。
|
|
13
|
-
- `
|
|
14
|
-
- `
|
|
13
|
+
- `create` 从当前分支创建 `.worktrees/<name>` 并记录 base。
|
|
14
|
+
- `create --json` 覆盖 created、reused、already-active、dirty 和非法名称安全失败。
|
|
15
15
|
- `/worktree start` 把历史 fork 到 cwd 正确的新 session,再调用 `switchSession`。
|
|
16
|
-
- `enter_worktree`
|
|
16
|
+
- `enter_worktree` 串行创建或复用目标,在 `agent_settled` 后触发内部命令,并 fork 当前 session 后调用 `switchSession`。
|
|
17
17
|
- Agent 轮被中止时不触发切换;用户取消 session replacement 时删除派生 session。
|
|
18
18
|
- 自定义 session 目录在 fork 后保持不变。
|
|
19
|
-
-
|
|
19
|
+
- 已删除的 CLI `start` 和 `prepare` 会显式失败。
|
|
20
20
|
- `list` 标记主 checkout 和子 worktree。
|
|
21
21
|
- `info` 报告 clean、branch、base 和 ahead。
|
|
22
22
|
- postinstall 把可执行 CLI 与 completion 复制到隔离 HOME,并保持执行权限。
|
package/bin/pi-worktree
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# pi-worktree: create
|
|
2
|
+
# pi-worktree: create, inspect, and clean up git worktrees.
|
|
3
3
|
#
|
|
4
4
|
# Usage:
|
|
5
|
-
# pi-worktree
|
|
6
|
-
# pi-worktree start --base <ref> <name> [pi args...] # branch from <ref> instead of current branch
|
|
7
|
-
# pi-worktree prepare [--base <ref>] [--json] <name> # create/reuse without launching pi
|
|
5
|
+
# pi-worktree create [--base <ref>] [--json] <name> # create/reuse without launching pi
|
|
8
6
|
# pi-worktree -l | --list # list worktrees (main checkout marked "main checkout")
|
|
9
7
|
# pi-worktree -i <name> # show base commit, ahead/behind, unique commits
|
|
10
8
|
# pi-worktree -o | --out # cd back to the main checkout
|
|
@@ -229,13 +227,13 @@ cmd_remove() {
|
|
|
229
227
|
if [[ "$ans" =~ ^[Yy]$ ]]; then
|
|
230
228
|
git branch -D "$branch" && ok "deleted branch: $branch"
|
|
231
229
|
else
|
|
232
|
-
warn "branch '$branch' kept; 'pi-worktree
|
|
230
|
+
warn "branch '$branch' kept; 'pi-worktree create $branch' will reuse it as-is"
|
|
233
231
|
fi
|
|
234
232
|
fi
|
|
235
233
|
fi
|
|
236
234
|
}
|
|
237
235
|
|
|
238
|
-
|
|
236
|
+
create_wt() {
|
|
239
237
|
local name="$1"
|
|
240
238
|
require_repo
|
|
241
239
|
validate_name "$name"
|
|
@@ -247,11 +245,11 @@ prepare_wt() {
|
|
|
247
245
|
current_branch=$(git branch --show-current)
|
|
248
246
|
[[ "$current_branch" == "$name" ]] \
|
|
249
247
|
|| die "already inside linked worktree '$current_branch'; return to the main checkout first"
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if [[ -n "$(git status --porcelain)" ]]; then
|
|
248
|
+
CREATE_STATUS="already-active"
|
|
249
|
+
CREATE_DIR=$(pwd -P)
|
|
250
|
+
CREATE_BRANCH="$current_branch"
|
|
251
|
+
CREATE_DIRTY=false
|
|
252
|
+
if [[ -n "$(git status --porcelain)" ]]; then CREATE_DIRTY=true; fi
|
|
255
253
|
return 0
|
|
256
254
|
fi
|
|
257
255
|
|
|
@@ -275,7 +273,7 @@ prepare_wt() {
|
|
|
275
273
|
|| die "$dir is not checked out on expected branch '$name'"
|
|
276
274
|
ensure_gitignore
|
|
277
275
|
ok "reusing existing worktree $dir"
|
|
278
|
-
|
|
276
|
+
CREATE_STATUS="reused"
|
|
279
277
|
else
|
|
280
278
|
local target_abs registered_path
|
|
281
279
|
target_abs="$dir"
|
|
@@ -292,7 +290,7 @@ prepare_wt() {
|
|
|
292
290
|
git worktree add "$dir" "$name"
|
|
293
291
|
fi
|
|
294
292
|
ok "checked out existing branch '$name'"
|
|
295
|
-
|
|
293
|
+
CREATE_STATUS="attached"
|
|
296
294
|
# record a best-effort base for -i display
|
|
297
295
|
local mb; mb=$(git merge-base main "$name" 2>/dev/null || true)
|
|
298
296
|
[[ -n "$mb" ]] && git config "branch.$name.base" "$(git rev-parse --short "$mb")" \
|
|
@@ -307,71 +305,46 @@ prepare_wt() {
|
|
|
307
305
|
git config "branch.$name.base" "$(git rev-parse --short "$base")"
|
|
308
306
|
git config "branch.$name.baseRef" "$base"
|
|
309
307
|
ok "created branch '$name' from $base @ $(git rev-parse --short "$base")"
|
|
310
|
-
|
|
308
|
+
CREATE_STATUS="created"
|
|
311
309
|
fi
|
|
312
310
|
fi
|
|
313
311
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if [[ -n "$(git -C "$dir" status --porcelain)" ]]; then
|
|
312
|
+
CREATE_DIR=$(cd "$dir" && pwd -P)
|
|
313
|
+
CREATE_BRANCH=$(git -C "$dir" branch --show-current)
|
|
314
|
+
CREATE_DIRTY=false
|
|
315
|
+
if [[ -n "$(git -C "$dir" status --porcelain)" ]]; then CREATE_DIRTY=true; fi
|
|
318
316
|
}
|
|
319
317
|
|
|
320
|
-
|
|
321
|
-
PI_WORKTREE_STATUS="$
|
|
322
|
-
PI_WORKTREE_PATH="$
|
|
323
|
-
PI_WORKTREE_BRANCH="$
|
|
324
|
-
PI_WORKTREE_DIRTY="$
|
|
318
|
+
print_create_json() {
|
|
319
|
+
PI_WORKTREE_STATUS="$CREATE_STATUS" \
|
|
320
|
+
PI_WORKTREE_PATH="$CREATE_DIR" \
|
|
321
|
+
PI_WORKTREE_BRANCH="$CREATE_BRANCH" \
|
|
322
|
+
PI_WORKTREE_DIRTY="$CREATE_DIRTY" \
|
|
325
323
|
node -e 'process.stdout.write(JSON.stringify({status:process.env.PI_WORKTREE_STATUS,path:process.env.PI_WORKTREE_PATH,branch:process.env.PI_WORKTREE_BRANCH,dirty:process.env.PI_WORKTREE_DIRTY==="true"})+"\n")'
|
|
326
324
|
}
|
|
327
325
|
|
|
328
|
-
|
|
326
|
+
cmd_create() {
|
|
329
327
|
BASE_OVERRIDE=""
|
|
330
328
|
local json=0
|
|
331
329
|
while [[ $# -gt 0 ]]; do
|
|
332
330
|
case "$1" in
|
|
333
331
|
--base)
|
|
334
|
-
shift; [[ $# -ge 1 ]] || die "usage: pi-worktree
|
|
332
|
+
shift; [[ $# -ge 1 ]] || die "usage: pi-worktree create [--base <ref>] [--json] <name>"
|
|
335
333
|
BASE_OVERRIDE="$1"; shift ;;
|
|
336
334
|
--json) json=1; QUIET=1; shift ;;
|
|
337
|
-
--*) die "unknown
|
|
335
|
+
--*) die "unknown create option: $1" ;;
|
|
338
336
|
*) break ;;
|
|
339
337
|
esac
|
|
340
338
|
done
|
|
341
|
-
[[ $# -eq 1 ]] || die "usage: pi-worktree
|
|
342
|
-
|
|
339
|
+
[[ $# -eq 1 ]] || die "usage: pi-worktree create [--base <ref>] [--json] <name>"
|
|
340
|
+
create_wt "$1"
|
|
343
341
|
if [[ "$json" == 1 ]]; then
|
|
344
|
-
|
|
342
|
+
print_create_json
|
|
345
343
|
else
|
|
346
|
-
ok "$
|
|
344
|
+
ok "$CREATE_STATUS worktree: $CREATE_DIR ($CREATE_BRANCH)"
|
|
347
345
|
fi
|
|
348
346
|
}
|
|
349
347
|
|
|
350
|
-
cd_wt() {
|
|
351
|
-
local name="$1"; shift
|
|
352
|
-
require_repo
|
|
353
|
-
|
|
354
|
-
# Already isolated? Preserve the original start behavior and launch Pi here.
|
|
355
|
-
if in_linked_worktree; then
|
|
356
|
-
ok "already in an isolated worktree ($(pwd)), launching pi here"
|
|
357
|
-
exec pi "$@"
|
|
358
|
-
fi
|
|
359
|
-
|
|
360
|
-
# Dirty workspace: uncommitted changes do NOT follow into the new worktree.
|
|
361
|
-
local dir; dir="$(main_checkout)/.worktrees/$name"
|
|
362
|
-
if [[ ! -d "$dir" ]] && { ! git diff --quiet || ! git diff --cached --quiet || [[ -n "$(git ls-files --others --exclude-standard)" ]]; }; then
|
|
363
|
-
warn "Uncommitted changes stay in THIS directory; they will NOT appear in the worktree:"
|
|
364
|
-
git status --short | head -20
|
|
365
|
-
read -rp "Continue? [y/N] " ans
|
|
366
|
-
[[ "$ans" =~ ^[Yy]$ ]] || die "aborted"
|
|
367
|
-
fi
|
|
368
|
-
|
|
369
|
-
prepare_wt "$name"
|
|
370
|
-
|
|
371
|
-
cd "$PREPARE_DIR"
|
|
372
|
-
exec pi "$@"
|
|
373
|
-
}
|
|
374
|
-
|
|
375
348
|
case "${1:-}" in
|
|
376
349
|
-l|--list|list|ls) shift; cmd_list "$@" ;;
|
|
377
350
|
-i|--info|info) shift; cmd_info "$@" ;;
|
|
@@ -379,15 +352,7 @@ case "${1:-}" in
|
|
|
379
352
|
-r|--remove|rm|remove) shift; cmd_remove "$@" ;;
|
|
380
353
|
-p|--prune|prune) shift; subcmd_prune ;;
|
|
381
354
|
-h|--help|help) sed -n '2,10p' "$0" | sed 's/^# \{0,1\}//' ;;
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
if [[ "${1:-}" == "--base" ]]; then
|
|
386
|
-
shift; [[ $# -ge 2 ]] || die "usage: pi-worktree start --base <ref> <name> [pi args...]"
|
|
387
|
-
BASE_OVERRIDE="$1"; shift
|
|
388
|
-
fi
|
|
389
|
-
[[ $# -ge 1 ]] || die "usage: pi-worktree start <name> [pi args...]"
|
|
390
|
-
name="$1"; shift; cd_wt "$name" "$@" ;;
|
|
391
|
-
"") die "usage: pi-worktree start <name> [pi args...] (or list / info / out / remove / prune)" ;;
|
|
392
|
-
*) die "unknown command: $1 (usage: pi-worktree start <name> [pi args...])" ;;
|
|
355
|
+
create) shift; cmd_create "$@" ;;
|
|
356
|
+
"") die "usage: pi-worktree create <name> (or list / info / out / remove / prune)" ;;
|
|
357
|
+
*) die "unknown command: $1 (usage: pi-worktree create <name>)" ;;
|
|
393
358
|
esac
|
|
@@ -4,7 +4,7 @@ _pi_worktree() {
|
|
|
4
4
|
cur="${COMP_WORDS[COMP_CWORD]}"
|
|
5
5
|
first="${COMP_WORDS[1]}"
|
|
6
6
|
|
|
7
|
-
local SUBS="
|
|
7
|
+
local SUBS="create list ls info out remove rm prune -l --list -i --info -o --out -r --remove -p --prune -h --help"
|
|
8
8
|
|
|
9
9
|
# existing worktree names (only linked worktrees, i.e. under .worktrees/)
|
|
10
10
|
local wts=""
|
|
@@ -17,13 +17,13 @@ _pi_worktree() {
|
|
|
17
17
|
fi
|
|
18
18
|
|
|
19
19
|
if (( COMP_CWORD == 1 )); then
|
|
20
|
-
# first word: subcommands, flags, and existing worktree names
|
|
20
|
+
# first word: subcommands, flags, and existing worktree names
|
|
21
21
|
COMPREPLY=($(compgen -W "$SUBS $wts" -- "$cur"))
|
|
22
22
|
return
|
|
23
23
|
fi
|
|
24
24
|
|
|
25
25
|
case "$first" in
|
|
26
|
-
-i|--info|info|-r|--remove|remove|rm|
|
|
26
|
+
-i|--info|info|-r|--remove|remove|rm|create)
|
|
27
27
|
# only the second word of info/remove completes worktree names;
|
|
28
28
|
# later words offer nothing (info/remove take no further args)
|
|
29
29
|
if (( COMP_CWORD == 2 )); then
|
|
@@ -33,7 +33,7 @@ _pi_worktree() {
|
|
|
33
33
|
fi
|
|
34
34
|
;;
|
|
35
35
|
*)
|
|
36
|
-
#
|
|
36
|
+
# no command-specific completion
|
|
37
37
|
COMPREPLY=()
|
|
38
38
|
;;
|
|
39
39
|
esac
|
package/docs/demo.gif
CHANGED
|
Binary file
|
package/docs/screenshot.png
CHANGED
|
Binary file
|
package/extensions/worktree.ts
CHANGED
|
@@ -7,14 +7,14 @@ import { Type } from "typebox";
|
|
|
7
7
|
const CLI_PATH = fileURLToPath(new URL("../bin/pi-worktree", import.meta.url));
|
|
8
8
|
const PENDING_SWITCH_ARGUMENT = "__enter_worktree_pending__";
|
|
9
9
|
|
|
10
|
-
export interface
|
|
10
|
+
export interface WorktreeCreateResult {
|
|
11
11
|
status: "created" | "attached" | "reused" | "already-active";
|
|
12
12
|
path: string;
|
|
13
13
|
branch: string;
|
|
14
14
|
dirty: boolean;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export interface EnterWorktreeResult extends
|
|
17
|
+
export interface EnterWorktreeResult extends WorktreeCreateResult {
|
|
18
18
|
switch: "scheduled" | "already-active";
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -49,41 +49,41 @@ function parseCommandArgs(input: string): { name: string; base?: string } {
|
|
|
49
49
|
return { name, base };
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function
|
|
52
|
+
function parseCreateOutput(stdout: string): WorktreeCreateResult {
|
|
53
53
|
const line = stdout.trim().split("\n").filter(Boolean).at(-1);
|
|
54
|
-
if (!line) throw new Error("pi-worktree
|
|
55
|
-
const result = JSON.parse(line) as Partial<
|
|
54
|
+
if (!line) throw new Error("pi-worktree create 没有返回结果");
|
|
55
|
+
const result = JSON.parse(line) as Partial<WorktreeCreateResult>;
|
|
56
56
|
if (
|
|
57
57
|
!["created", "attached", "reused", "already-active"].includes(String(result.status))
|
|
58
58
|
|| typeof result.path !== "string"
|
|
59
59
|
|| typeof result.branch !== "string"
|
|
60
60
|
|| typeof result.dirty !== "boolean"
|
|
61
61
|
) {
|
|
62
|
-
throw new Error("pi-worktree
|
|
62
|
+
throw new Error("pi-worktree create 返回了无效结果");
|
|
63
63
|
}
|
|
64
|
-
return result as
|
|
64
|
+
return result as WorktreeCreateResult;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
async function
|
|
67
|
+
async function createWorktree(
|
|
68
68
|
pi: ExtensionAPI,
|
|
69
69
|
cwd: string,
|
|
70
70
|
name: string,
|
|
71
71
|
base?: string,
|
|
72
72
|
signal?: AbortSignal,
|
|
73
|
-
): Promise<
|
|
74
|
-
const args = ["
|
|
73
|
+
): Promise<WorktreeCreateResult> {
|
|
74
|
+
const args = ["create", "--json"];
|
|
75
75
|
if (base) args.push("--base", base);
|
|
76
76
|
args.push(name);
|
|
77
77
|
const result = await pi.exec(CLI_PATH, args, { cwd, signal });
|
|
78
78
|
if (result.code !== 0) {
|
|
79
|
-
throw new Error(result.stderr.trim() || result.stdout.trim() || `pi-worktree
|
|
79
|
+
throw new Error(result.stderr.trim() || result.stdout.trim() || `pi-worktree create 失败(退出码 ${result.code})`);
|
|
80
80
|
}
|
|
81
81
|
if (signal?.aborted) throw new Error("worktree 创建已取消");
|
|
82
|
-
return
|
|
82
|
+
return parseCreateOutput(result.stdout);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
export default function worktreeExtension(pi: ExtensionAPI) {
|
|
86
|
-
let pendingSwitch:
|
|
86
|
+
let pendingSwitch: WorktreeCreateResult | undefined;
|
|
87
87
|
let pendingDispatchScheduled = false;
|
|
88
88
|
let transitionReserved = false;
|
|
89
89
|
|
|
@@ -108,7 +108,7 @@ export default function worktreeExtension(pi: ExtensionAPI) {
|
|
|
108
108
|
description: "创建或复用 worktree,并在当前界面切换过去",
|
|
109
109
|
handler: async (input, ctx) => {
|
|
110
110
|
const isPendingSwitch = input.trim() === PENDING_SWITCH_ARGUMENT;
|
|
111
|
-
let
|
|
111
|
+
let created: WorktreeCreateResult;
|
|
112
112
|
let targetSessionFile: string;
|
|
113
113
|
try {
|
|
114
114
|
if (!isPendingSwitch && transitionReserved) {
|
|
@@ -127,25 +127,25 @@ export default function worktreeExtension(pi: ExtensionAPI) {
|
|
|
127
127
|
|
|
128
128
|
await ctx.waitForIdle();
|
|
129
129
|
if (requestedSwitch) {
|
|
130
|
-
|
|
130
|
+
created = requestedSwitch;
|
|
131
131
|
} else {
|
|
132
132
|
const { name, base } = parseCommandArgs(input);
|
|
133
|
-
|
|
133
|
+
created = await createWorktree(pi, ctx.cwd, name, base);
|
|
134
134
|
}
|
|
135
|
-
if (resolve(ctx.cwd) === resolve(
|
|
135
|
+
if (resolve(ctx.cwd) === resolve(created.path)) {
|
|
136
136
|
if (isPendingSwitch) {
|
|
137
137
|
pendingSwitch = undefined;
|
|
138
138
|
pendingDispatchScheduled = false;
|
|
139
139
|
}
|
|
140
140
|
transitionReserved = false;
|
|
141
|
-
ctx.ui.notify(`已经位于 worktree:${
|
|
141
|
+
ctx.ui.notify(`已经位于 worktree:${created.path}`, "info");
|
|
142
142
|
return;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
const sessionDir = ctx.sessionManager.usesDefaultSessionDir()
|
|
146
146
|
? undefined
|
|
147
147
|
: ctx.sessionManager.getSessionDir();
|
|
148
|
-
const target = SessionManager.forkFrom(sourceSessionFile,
|
|
148
|
+
const target = SessionManager.forkFrom(sourceSessionFile, created.path, sessionDir);
|
|
149
149
|
targetSessionFile = target.getSessionFile() ?? "";
|
|
150
150
|
if (!targetSessionFile) throw new Error("无法为 worktree 创建持久会话");
|
|
151
151
|
} catch (error) {
|
|
@@ -162,12 +162,12 @@ export default function worktreeExtension(pi: ExtensionAPI) {
|
|
|
162
162
|
try {
|
|
163
163
|
switched = await ctx.switchSession(targetSessionFile, {
|
|
164
164
|
withSession: async (next) => {
|
|
165
|
-
const dirtyNote =
|
|
165
|
+
const dirtyNote = created.dirty ? "(已有未提交改动)" : "";
|
|
166
166
|
await next.sendMessage({
|
|
167
167
|
customType: "worktree-switch",
|
|
168
|
-
content: `✓ 已进入 worktree:${
|
|
168
|
+
content: `✓ 已进入 worktree:${created.path}\nBranch: ${created.branch}${dirtyNote}\n\n在worktree继续任务`,
|
|
169
169
|
display: true,
|
|
170
|
-
details:
|
|
170
|
+
details: created,
|
|
171
171
|
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
172
172
|
},
|
|
173
173
|
});
|
|
@@ -214,28 +214,28 @@ export default function worktreeExtension(pi: ExtensionAPI) {
|
|
|
214
214
|
}
|
|
215
215
|
transitionReserved = true;
|
|
216
216
|
|
|
217
|
-
let
|
|
217
|
+
let created: WorktreeCreateResult;
|
|
218
218
|
try {
|
|
219
|
-
|
|
219
|
+
created = await createWorktree(pi, ctx.cwd, params.name, params.base, signal);
|
|
220
220
|
if (signal.aborted) throw new Error("worktree 创建已取消");
|
|
221
221
|
} catch (error) {
|
|
222
222
|
transitionReserved = false;
|
|
223
223
|
throw error;
|
|
224
224
|
}
|
|
225
|
-
const alreadyActive = resolve(ctx.cwd) === resolve(
|
|
225
|
+
const alreadyActive = resolve(ctx.cwd) === resolve(created.path);
|
|
226
226
|
const result: EnterWorktreeResult = {
|
|
227
227
|
switch: alreadyActive ? "already-active" : "scheduled",
|
|
228
|
-
...
|
|
228
|
+
...created,
|
|
229
229
|
};
|
|
230
230
|
if (alreadyActive) transitionReserved = false;
|
|
231
|
-
else pendingSwitch =
|
|
232
|
-
const dirtyNote =
|
|
231
|
+
else pendingSwitch = created;
|
|
232
|
+
const dirtyNote = created.dirty ? " 目标 worktree 已有未提交改动。" : "";
|
|
233
233
|
return {
|
|
234
234
|
content: [{
|
|
235
235
|
type: "text",
|
|
236
236
|
text: alreadyActive
|
|
237
|
-
? `已经位于 worktree:${
|
|
238
|
-
: `Worktree ${
|
|
237
|
+
? `已经位于 worktree:${created.path}.${dirtyNote}`
|
|
238
|
+
: `Worktree ${created.status}: ${created.path}.${dirtyNote} 当前 Agent run 结束后会自动切换到该目录;不要继续调用仓库工具。`,
|
|
239
239
|
}],
|
|
240
240
|
details: result,
|
|
241
241
|
structuredContent: result,
|
package/package.json
CHANGED
|
@@ -15,9 +15,7 @@ Installed by the package postinstall hook to `~/.local/bin/pi-worktree`, with ba
|
|
|
15
15
|
|---|---|
|
|
16
16
|
| `enter_worktree({ name, base? })` | Agent entrypoint: create/reuse a worktree and automatically move this conversation there after the run settles. Call it alone and stop after it returns. |
|
|
17
17
|
| `/worktree start <name> [--base <ref>]` | User entrypoint: fork the current persistent session into the worktree and switch the current Pi UI. |
|
|
18
|
-
| `pi-worktree
|
|
19
|
-
| `pi-worktree start <name> [pi args...]` | Create or reuse `.worktrees/<name>`, then launch Pi inside it. Already inside a linked worktree, it just launches Pi there. |
|
|
20
|
-
| `pi-worktree start --base <ref> <name> [pi args...]` | Same, but branch from `<ref>` instead of the current branch. |
|
|
18
|
+
| `pi-worktree create [--base <ref>] [--json] <name>` | Idempotently create/reuse without launching Pi; `--json` is the machine interface. |
|
|
21
19
|
| `pi-worktree list` | All worktrees; the main checkout is marked `(main checkout)`, stale registrations are flagged. |
|
|
22
20
|
| `pi-worktree info <name>` | Clean/dirty status, branch and HEAD, base commit, ahead/behind `main`, unique commits. |
|
|
23
21
|
| `pi-worktree out` | Open a shell in the main checkout (for merging). |
|
|
@@ -27,7 +25,7 @@ Installed by the package postinstall hook to `~/.local/bin/pi-worktree`, with ba
|
|
|
27
25
|
## Isolating a task
|
|
28
26
|
|
|
29
27
|
1. **Survey** — `pi-worktree list`, plus `pi-worktree info <name>` when the name you want is already taken. Reuse is free; a removed worktree is gone for good, because `remove` deletes its branch.
|
|
30
|
-
2. **Enter** — as an Agent, call `enter_worktree` once and stop; the Extension switches automatically after settlement. A user can instead run `/worktree start <name>`.
|
|
28
|
+
2. **Enter** — as an Agent, call `enter_worktree` once and stop; the Extension switches automatically after settlement. A user can instead run `/worktree start <name>`.
|
|
31
29
|
3. **Work** — inside the worktree: commit there, and check `pi-worktree info <name>` before claiming progress. `ahead: N commits since base` is the evidence.
|
|
32
30
|
4. **Land** — `pi-worktree out`, then `git merge <name>` in the main checkout. Done when the worktree's unique commits are in the target branch.
|
|
33
31
|
5. **Clean up** — `pi-worktree remove <name>`. A merged branch goes without prompting; anything unmerged asks once.
|
|
@@ -36,6 +34,5 @@ Installed by the package postinstall hook to `~/.local/bin/pi-worktree`, with ba
|
|
|
36
34
|
|
|
37
35
|
- Prefer `enter_worktree` when it is available. After the tool returns, do not read or modify repository files; the Extension waits for `agent_settled`, then forks the session into the target cwd and switches automatically.
|
|
38
36
|
- `/worktree` is user-facing and cannot be invoked by the model. It requires a persisted session because it forks history into a session whose header owns the worktree cwd.
|
|
39
|
-
- `
|
|
40
|
-
- `prepare` is idempotent. It reports whether the target was created, attached, reused, or already active, and reports dirty reuse rather than discarding anything.
|
|
37
|
+
- `create` is idempotent and never launches Pi. It reports whether the target was created, attached, reused, or already active, and reports dirty reuse rather than discarding anything.
|
|
41
38
|
- `remove` asks before discarding dirty files or unmerged commits, and a non-interactive stdin (EOF) aborts safely. Answer explicitly when the removal is intended: `printf 'y\ny\n' | pi-worktree remove <name>`.
|