@maxiaochao/pi-toolkit 0.10.24 → 0.11.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 CHANGED
@@ -1,3 +1,8 @@
1
+ ## 2026-10-12 - v0.11.0
2
+
3
+ - bundle worktree v0.4.0: Replace CLI start and prepare with the create-only command
4
+ - Replace `pi-worktree start` and `pi-worktree prepare` with the create-only `pi-worktree create` CLI; `/worktree start` and `enter_worktree` continue to switch the current Pi session.
5
+
1
6
  ## 2026-10-12 - v0.10.24
2
7
 
3
8
  - bundle herdr-subagents v0.4.10: Stabilize concurrency checks and cancellation synchronization on slower CI runners.
@@ -5,6 +10,7 @@
5
10
  ## 2026-10-12 - v0.10.23
6
11
 
7
12
  - bundle herdr-subagents v0.4.9: Stabilize concurrency checks on slower CI runners.
13
+
8
14
  ## 2026-10-12 - v0.10.22
9
15
 
10
16
  - bundle herdr-subagents v0.4.8: make blocking behave identically in serial and parallel runs and let respond continue a named failed task from its saved session
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maxiaochao/pi-toolkit",
3
- "version": "0.10.24",
3
+ "version": "0.11.0",
4
4
  "description": "Personal pi coding agent toolkit: scheduler, /btw, /cache_export, apply_patch, browser automation, and bundled theme/skills.",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -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
@@ -1,6 +1,6 @@
1
1
  # @maxiaochao/pi-worktree
2
2
 
3
- `pi-worktree` CLI、Pi Extension 与配套 skill。它既能启动独立 Pi,也能让用户或 Agent 在当前 Pi 界面中切换 worktree,并保留完整对话。
3
+ `pi-worktree` CLI、Pi Extension 与配套 skill。CLI 负责管理 worktree;Extension 让用户或 Agent 在当前 Pi 界面中切换,并保留完整对话。
4
4
 
5
5
  ![Worktree 在真实临时 Git 仓库中的运行截图](docs/screenshot.png)
6
6
 
@@ -13,14 +13,14 @@
13
13
  ![Worktree 隔离流程演示](docs/demo.gif)
14
14
 
15
15
  ```bash
16
- pi-worktree start feature -p "implement the feature"
17
- pi-worktree prepare --json feature
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
- `start` 从当前分支创建或复用 `.worktrees/feature`,记录 base,并在该目录启动 Pi。`prepare` 复用同一创建逻辑,但只返回目标,不启动嵌套 Pi。
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 start <name> [pi args...]
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
- `prepare --json` 是 Extension 和其他宿主使用的幂等机器接口,返回 `created`、`attached`、`reused` 或 `already-active`,以及绝对路径、分支和 dirty 状态。已有 worktree 会复用;已存在但未挂载的同名分支会挂载;stale 注册、非法名称和路径冲突会显式失败。
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)。
@@ -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 start` 交互流程。
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
- - `start` 从当前分支创建 `.worktrees/<name>` 并记录 base。
14
- - `prepare --json` 覆盖 created、reused、already-active、dirty 和非法名称安全失败。
13
+ - `create` 从当前分支创建 `.worktrees/<name>` 并记录 base。
14
+ - `create --json` 覆盖 created、reused、already-active、dirty 和非法名称安全失败。
15
15
  - `/worktree start` 把历史 fork 到 cwd 正确的新 session,再调用 `switchSession`。
16
- - `enter_worktree` 串行准备目标,在 `agent_settled` 后触发内部命令,并 fork 当前 session 后调用 `switchSession`。
16
+ - `enter_worktree` 串行创建或复用目标,在 `agent_settled` 后触发内部命令,并 fork 当前 session 后调用 `switchSession`。
17
17
  - Agent 轮被中止时不触发切换;用户取消 session replacement 时删除派生 session。
18
18
  - 自定义 session 目录在 fork 后保持不变。
19
- - 假 Pi 收到正确工作目录和参数。
19
+ - 已删除的 CLI `start` 和 `prepare` 会显式失败。
20
20
  - `list` 标记主 checkout 和子 worktree。
21
21
  - `info` 报告 clean、branch、base 和 ahead。
22
22
  - postinstall 把可执行 CLI 与 completion 复制到隔离 HOME,并保持执行权限。
@@ -1,10 +1,8 @@
1
1
  #!/usr/bin/env bash
2
- # pi-worktree: create/reuse a git worktree, then launch pi inside it.
2
+ # pi-worktree: create, inspect, and clean up git worktrees.
3
3
  #
4
4
  # Usage:
5
- # pi-worktree start <name> [pi args...] # enter worktree <name> and start pi
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 start $branch' will reuse it as-is"
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
- prepare_wt() {
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
- PREPARE_STATUS="already-active"
251
- PREPARE_DIR=$(pwd -P)
252
- PREPARE_BRANCH="$current_branch"
253
- PREPARE_DIRTY=false
254
- if [[ -n "$(git status --porcelain)" ]]; then PREPARE_DIRTY=true; fi
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
- PREPARE_STATUS="reused"
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
- PREPARE_STATUS="attached"
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
- PREPARE_STATUS="created"
308
+ CREATE_STATUS="created"
311
309
  fi
312
310
  fi
313
311
 
314
- PREPARE_DIR=$(cd "$dir" && pwd -P)
315
- PREPARE_BRANCH=$(git -C "$dir" branch --show-current)
316
- PREPARE_DIRTY=false
317
- if [[ -n "$(git -C "$dir" status --porcelain)" ]]; then PREPARE_DIRTY=true; fi
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
- print_prepare_json() {
321
- PI_WORKTREE_STATUS="$PREPARE_STATUS" \
322
- PI_WORKTREE_PATH="$PREPARE_DIR" \
323
- PI_WORKTREE_BRANCH="$PREPARE_BRANCH" \
324
- PI_WORKTREE_DIRTY="$PREPARE_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
- cmd_prepare() {
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 prepare [--base <ref>] [--json] <name>"
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 prepare option: $1" ;;
335
+ --*) die "unknown create option: $1" ;;
338
336
  *) break ;;
339
337
  esac
340
338
  done
341
- [[ $# -eq 1 ]] || die "usage: pi-worktree prepare [--base <ref>] [--json] <name>"
342
- prepare_wt "$1"
339
+ [[ $# -eq 1 ]] || die "usage: pi-worktree create [--base <ref>] [--json] <name>"
340
+ create_wt "$1"
343
341
  if [[ "$json" == 1 ]]; then
344
- print_prepare_json
342
+ print_create_json
345
343
  else
346
- ok "$PREPARE_STATUS worktree: $PREPARE_DIR ($PREPARE_BRANCH)"
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
- prepare) shift; cmd_prepare "$@" ;;
383
- start) shift
384
- BASE_OVERRIDE=""
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="start prepare list ls info out remove rm prune -l --list -i --info -o --out -r --remove -p --prune -h --help"
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 (to enter)
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|start|prepare)
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
- # after a name: default (file) completion for pi args
36
+ # no command-specific completion
37
37
  COMPREPLY=()
38
38
  ;;
39
39
  esac
Binary file
@@ -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 WorktreePrepareResult {
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 WorktreePrepareResult {
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 parsePrepareOutput(stdout: string): WorktreePrepareResult {
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 prepare 没有返回结果");
55
- const result = JSON.parse(line) as Partial<WorktreePrepareResult>;
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 prepare 返回了无效结果");
62
+ throw new Error("pi-worktree create 返回了无效结果");
63
63
  }
64
- return result as WorktreePrepareResult;
64
+ return result as WorktreeCreateResult;
65
65
  }
66
66
 
67
- async function prepareWorktree(
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<WorktreePrepareResult> {
74
- const args = ["prepare", "--json"];
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 prepare 失败(退出码 ${result.code})`);
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 parsePrepareOutput(result.stdout);
82
+ return parseCreateOutput(result.stdout);
83
83
  }
84
84
 
85
85
  export default function worktreeExtension(pi: ExtensionAPI) {
86
- let pendingSwitch: WorktreePrepareResult | undefined;
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 prepared: WorktreePrepareResult;
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
- prepared = requestedSwitch;
130
+ created = requestedSwitch;
131
131
  } else {
132
132
  const { name, base } = parseCommandArgs(input);
133
- prepared = await prepareWorktree(pi, ctx.cwd, name, base);
133
+ created = await createWorktree(pi, ctx.cwd, name, base);
134
134
  }
135
- if (resolve(ctx.cwd) === resolve(prepared.path)) {
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:${prepared.path}`, "info");
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, prepared.path, sessionDir);
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 = prepared.dirty ? "(已有未提交改动)" : "";
165
+ const dirtyNote = created.dirty ? "(已有未提交改动)" : "";
166
166
  await next.sendMessage({
167
167
  customType: "worktree-switch",
168
- content: `✓ 已进入 worktree:${prepared.path}\nBranch: ${prepared.branch}${dirtyNote}\n\n在worktree继续任务`,
168
+ content: `✓ 已进入 worktree:${created.path}\nBranch: ${created.branch}${dirtyNote}\n\n在worktree继续任务`,
169
169
  display: true,
170
- details: prepared,
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 prepared: WorktreePrepareResult;
217
+ let created: WorktreeCreateResult;
218
218
  try {
219
- prepared = await prepareWorktree(pi, ctx.cwd, params.name, params.base, signal);
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(prepared.path);
225
+ const alreadyActive = resolve(ctx.cwd) === resolve(created.path);
226
226
  const result: EnterWorktreeResult = {
227
227
  switch: alreadyActive ? "already-active" : "scheduled",
228
- ...prepared,
228
+ ...created,
229
229
  };
230
230
  if (alreadyActive) transitionReserved = false;
231
- else pendingSwitch = prepared;
232
- const dirtyNote = prepared.dirty ? " 目标 worktree 已有未提交改动。" : "";
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:${prepared.path}.${dirtyNote}`
238
- : `Worktree ${prepared.status}: ${prepared.path}.${dirtyNote} 当前 Agent run 结束后会自动切换到该目录;不要继续调用仓库工具。`,
237
+ ? `已经位于 worktree:${created.path}.${dirtyNote}`
238
+ : `Worktree ${created.status}: ${created.path}.${dirtyNote} 当前 Agent run 结束后会自动切换到该目录;不要继续调用仓库工具。`,
239
239
  }],
240
240
  details: result,
241
241
  structuredContent: result,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maxiaochao/pi-worktree",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "Git worktree CLI, Pi extension, and skill for isolated coding tasks.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -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 prepare [--base <ref>] [--json] <name>` | Create/reuse without launching Pi; `--json` is the machine interface. |
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>`. Use the CLI `start` only when a separate Pi process is wanted.
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
- - `pi-worktree start` **execs a new `pi`** in the worktree, and `out` execs a shell. Use this only for an intentionally separate process/session.
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>`.
@@ -140,7 +140,7 @@ test("public /worktree forwards --base to the CLI and switches", async () => {
140
140
  try {
141
141
  await f.handlers["command:worktree"]("start feature --base main", f.ctx);
142
142
  assert.match(f.execCalls[0].command, /packages\/worktree\/bin\/pi-worktree$/);
143
- assert.deepEqual(f.execCalls[0].args, ["prepare", "--json", "--base", "main", "feature"]);
143
+ assert.deepEqual(f.execCalls[0].args, ["create", "--json", "--base", "main", "feature"]);
144
144
  assert.equal(f.execCalls[0].options.cwd, f.cwd);
145
145
  assert.ok(f.switchedTo());
146
146
  } finally {
@@ -159,7 +159,7 @@ test("aborted agent settlement discards the scheduled switch", async () => {
159
159
  }
160
160
  });
161
161
 
162
- test("a user command cannot race an Agent tool that is still preparing", async () => {
162
+ test("a user command cannot race an Agent tool that is still creating", async () => {
163
163
  const f = await fixture({ deferExec: true });
164
164
  try {
165
165
  const toolResult = f.handlers.tool.execute(
@@ -5,14 +5,6 @@ PACKAGE_DIR=$(cd "$(dirname "$0")/.." && pwd)
5
5
  ROOT=$(mktemp -d)
6
6
  trap 'rm -rf "$ROOT"' EXIT
7
7
 
8
- mkdir -p "$ROOT/bin"
9
- cat > "$ROOT/bin/pi" <<'EOF'
10
- #!/usr/bin/env bash
11
- printf '%s\n' "$PWD" > "${PI_WORKTREE_TEST_OUTPUT}.cwd"
12
- printf '%s\n' "$@" > "${PI_WORKTREE_TEST_OUTPUT}.args"
13
- EOF
14
- chmod +x "$ROOT/bin/pi"
15
-
16
8
  mkdir "$ROOT/repo"
17
9
  cd "$ROOT/repo"
18
10
  git init -q -b main
@@ -23,15 +15,11 @@ printf 'base\n' > tracked.txt
23
15
  git add .gitignore tracked.txt
24
16
  git commit -qm initial
25
17
 
26
- export PATH="$ROOT/bin:$PATH"
27
- export PI_WORKTREE_TEST_OUTPUT="$ROOT/pi"
28
- bash "$PACKAGE_DIR/bin/pi-worktree" start feature --model fake/model
18
+ feature_json=$(bash "$PACKAGE_DIR/bin/pi-worktree" create --json feature)
29
19
 
30
20
  test -d .worktrees/feature
31
21
  test "$(git -C .worktrees/feature branch --show-current)" = feature
32
- test "$(cat "$ROOT/pi.cwd")" = "$ROOT/repo/.worktrees/feature"
33
- grep -qx -- '--model' "$ROOT/pi.args"
34
- grep -qx -- 'fake/model' "$ROOT/pi.args"
22
+ node -e 'if (JSON.parse(process.argv[1]).status !== "created") process.exit(1)' "$feature_json"
35
23
 
36
24
  bash "$PACKAGE_DIR/bin/pi-worktree" list > "$ROOT/list.txt"
37
25
  grep -q 'main checkout' "$ROOT/list.txt"
@@ -42,14 +30,14 @@ grep -q 'status: clean' "$ROOT/info.txt"
42
30
  grep -q 'branch: feature' "$ROOT/info.txt"
43
31
  grep -q 'based on:' "$ROOT/info.txt"
44
32
 
45
- bash "$PACKAGE_DIR/bin/pi-worktree" prepare --json feature > "$ROOT/reused.json"
33
+ bash "$PACKAGE_DIR/bin/pi-worktree" create --json feature > "$ROOT/reused.json"
46
34
  node -e '
47
35
  const fs = require("node:fs");
48
36
  const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
49
37
  if (value.status !== "reused" || value.branch !== "feature" || value.dirty !== false) process.exit(1);
50
38
  ' "$ROOT/reused.json"
51
39
 
52
- bash "$PACKAGE_DIR/bin/pi-worktree" prepare --json prepared > "$ROOT/created.json"
40
+ bash "$PACKAGE_DIR/bin/pi-worktree" create --json prepared > "$ROOT/created.json"
53
41
  node -e '
54
42
  const fs = require("node:fs");
55
43
  const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
@@ -57,7 +45,7 @@ node -e '
57
45
  ' "$ROOT/created.json"
58
46
 
59
47
  mkdir -p nested/path
60
- (cd nested/path && bash "$PACKAGE_DIR/bin/pi-worktree" prepare --json from-subdir) > "$ROOT/subdir.json"
48
+ (cd nested/path && bash "$PACKAGE_DIR/bin/pi-worktree" create --json from-subdir) > "$ROOT/subdir.json"
61
49
  node -e '
62
50
  const fs = require("node:fs");
63
51
  const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
@@ -65,50 +53,50 @@ node -e '
65
53
  ' "$ROOT/subdir.json" "$ROOT/repo"
66
54
 
67
55
  git branch attachable
68
- bash "$PACKAGE_DIR/bin/pi-worktree" prepare --json attachable > "$ROOT/attached.json"
56
+ bash "$PACKAGE_DIR/bin/pi-worktree" create --json attachable > "$ROOT/attached.json"
69
57
  node -e '
70
58
  const fs = require("node:fs");
71
59
  const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
72
60
  if (value.status !== "attached" || value.branch !== "attachable") process.exit(1);
73
61
  ' "$ROOT/attached.json"
74
62
 
75
- bash "$PACKAGE_DIR/bin/pi-worktree" prepare --json stale > /dev/null
63
+ bash "$PACKAGE_DIR/bin/pi-worktree" create --json stale > /dev/null
76
64
  rm -rf .worktrees/stale
77
- if bash "$PACKAGE_DIR/bin/pi-worktree" prepare --json stale >/dev/null 2>&1; then
78
- echo "prepare silently replaced a stale worktree registration" >&2
65
+ if bash "$PACKAGE_DIR/bin/pi-worktree" create --json stale >/dev/null 2>&1; then
66
+ echo "create silently replaced a stale worktree registration" >&2
79
67
  exit 1
80
68
  fi
81
69
  git worktree prune
82
70
 
83
71
  git worktree add .worktrees/wrong -b another-branch >/dev/null
84
- if bash "$PACKAGE_DIR/bin/pi-worktree" prepare --json wrong >/dev/null 2>&1; then
85
- echo "prepare reused a path checked out on the wrong branch" >&2
72
+ if bash "$PACKAGE_DIR/bin/pi-worktree" create --json wrong >/dev/null 2>&1; then
73
+ echo "create reused a path checked out on the wrong branch" >&2
86
74
  exit 1
87
75
  fi
88
76
 
89
77
  mkdir .worktrees/main
90
- if bash "$PACKAGE_DIR/bin/pi-worktree" prepare --json main >/dev/null 2>&1; then
91
- echo "prepare reused an ordinary directory as a worktree"
78
+ if bash "$PACKAGE_DIR/bin/pi-worktree" create --json main >/dev/null 2>&1; then
79
+ echo "create reused an ordinary directory as a worktree"
92
80
  exit 1
93
81
  fi
94
82
 
95
83
  printf 'changed\n' >> .worktrees/prepared/tracked.txt
96
- bash "$PACKAGE_DIR/bin/pi-worktree" prepare --json prepared > "$ROOT/dirty.json"
84
+ bash "$PACKAGE_DIR/bin/pi-worktree" create --json prepared > "$ROOT/dirty.json"
97
85
  node -e '
98
86
  const fs = require("node:fs");
99
87
  const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
100
88
  if (value.status !== "reused" || value.dirty !== true) process.exit(1);
101
89
  ' "$ROOT/dirty.json"
102
90
 
103
- (cd .worktrees/prepared && bash "$PACKAGE_DIR/bin/pi-worktree" prepare --json prepared) > "$ROOT/active.json"
91
+ (cd .worktrees/prepared && bash "$PACKAGE_DIR/bin/pi-worktree" create --json prepared) > "$ROOT/active.json"
104
92
  node -e '
105
93
  const fs = require("node:fs");
106
94
  const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
107
95
  if (value.status !== "already-active" || value.dirty !== true) process.exit(1);
108
96
  ' "$ROOT/active.json"
109
97
 
110
- if bash "$PACKAGE_DIR/bin/pi-worktree" prepare --json ../escape >/dev/null 2>&1; then
111
- echo "prepare accepted an unsafe worktree name" >&2
98
+ if bash "$PACKAGE_DIR/bin/pi-worktree" create --json ../escape >/dev/null 2>&1; then
99
+ echo "create accepted an unsafe worktree name" >&2
112
100
  exit 1
113
101
  fi
114
102
 
@@ -122,11 +110,19 @@ mkdir "$ROOT/symlink-repo" "$ROOT/outside-worktrees"
122
110
  git add tracked.txt
123
111
  git commit -qm initial
124
112
  ln -s "$ROOT/outside-worktrees" .worktrees
125
- if bash "$PACKAGE_DIR/bin/pi-worktree" prepare --json escaped >/dev/null 2>&1; then
126
- echo "prepare followed a symlinked .worktrees directory" >&2
113
+ if bash "$PACKAGE_DIR/bin/pi-worktree" create --json escaped >/dev/null 2>&1; then
114
+ echo "create followed a symlinked .worktrees directory" >&2
127
115
  exit 1
128
116
  fi
129
117
  )
130
118
 
131
- bash "$PACKAGE_DIR/bin/pi-worktree" --help | grep -q 'pi-worktree start'
119
+ bash "$PACKAGE_DIR/bin/pi-worktree" --help | grep -q 'pi-worktree create'
120
+ if bash "$PACKAGE_DIR/bin/pi-worktree" prepare feature >/dev/null 2>&1; then
121
+ echo "removed prepare command is still accepted" >&2
122
+ exit 1
123
+ fi
124
+ if bash "$PACKAGE_DIR/bin/pi-worktree" start feature >/dev/null 2>&1; then
125
+ echo "removed start command is still accepted" >&2
126
+ exit 1
127
+ fi
132
128
  printf 'PASS worktree CLI integration cases\n'