@maxiaochao/pi-toolkit 0.10.6 → 0.10.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +2 -1
  3. package/package.json +8 -5
  4. package/packages/herdr-subagents/CHANGELOG.md +7 -0
  5. package/packages/herdr-subagents/README.md +4 -2
  6. package/packages/herdr-subagents/TESTING.md +6 -1
  7. package/packages/herdr-subagents/extensions/herdr-subagents.ts +83 -19
  8. package/packages/herdr-subagents/src/batch-runner.ts +3 -1
  9. package/packages/herdr-subagents/src/dispatcher.ts +8 -1
  10. package/packages/herdr-subagents/tests/extension-contract.ts +198 -2
  11. package/packages/herdr-subagents/tests/run-tests.mjs +25 -0
  12. package/packages/scheduler/CHANGELOG.md +10 -0
  13. package/packages/scheduler/README.md +59 -0
  14. package/packages/scheduler/TESTING.md +19 -0
  15. package/packages/scheduler/docs/demo.gif +0 -0
  16. package/packages/scheduler/docs/screenshot.png +0 -0
  17. package/packages/scheduler/extensions/scheduler.ts +206 -0
  18. package/packages/scheduler/package.json +25 -0
  19. package/packages/scheduler/src/scheduler.ts +173 -0
  20. package/packages/scheduler/tests/scheduler-core.mjs +70 -0
  21. package/packages/scheduler/tests/scheduler-extension.mjs +117 -0
  22. package/packages/worktree/CHANGELOG.md +7 -0
  23. package/packages/worktree/README.md +18 -3
  24. package/packages/worktree/TESTING.md +5 -2
  25. package/packages/worktree/bin/pi-worktree +118 -23
  26. package/packages/worktree/bin/pi-worktree-completion.bash +2 -2
  27. package/packages/worktree/extensions/worktree.ts +173 -0
  28. package/packages/worktree/package.json +37 -8
  29. package/packages/worktree/skills/pi-worktree/SKILL.md +11 -6
  30. package/packages/worktree/tests/extension.test.mjs +114 -0
  31. package/packages/worktree/tests/run-tests.sh +86 -0
@@ -0,0 +1,114 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+
7
+ const root = await mkdtemp(join(tmpdir(), "pi-worktree-extension-"));
8
+ process.env.PI_CODING_AGENT_DIR = join(root, "agent");
9
+ const { default: register, WORKTREE_SWITCH_REQUEST } = await import("../extensions/worktree.ts");
10
+
11
+ test.after(async () => {
12
+ await rm(root, { recursive: true, force: true });
13
+ });
14
+
15
+ function setup(prepared) {
16
+ const commands = new Map();
17
+ const tools = new Map();
18
+ const pi = {
19
+ registerCommand(name, definition) { commands.set(name, definition); },
20
+ registerTool(definition) { tools.set(definition.name, definition); },
21
+ async exec(command, args, options) {
22
+ assert.match(command, /packages\/worktree\/bin\/pi-worktree$/);
23
+ assert.deepEqual(args, ["prepare", "--json", "--base", "main", "feature"]);
24
+ assert.equal(options.cwd, prepared.cwd);
25
+ return { code: 0, stdout: `${JSON.stringify(prepared.result)}\n`, stderr: "" };
26
+ },
27
+ };
28
+ register(pi);
29
+ return { commands, tools };
30
+ }
31
+
32
+ test("registers /worktree and switches a forked persistent session", async () => {
33
+ const cwd = join(root, "main");
34
+ const target = join(root, "main", ".worktrees", "feature");
35
+ await mkdir(target, { recursive: true });
36
+ const sourceSession = join(root, "source.jsonl");
37
+ await writeFile(sourceSession, `${JSON.stringify({ type: "session", version: 3, id: "source", timestamp: new Date().toISOString(), cwd })}\n`);
38
+ const prepared = { cwd, result: { status: "created", path: target, branch: "feature", dirty: false } };
39
+ const { commands } = setup(prepared);
40
+ const notifications = [];
41
+ let switchedFile;
42
+ await commands.get("worktree").handler("start feature --base main", {
43
+ cwd,
44
+ sessionManager: { getSessionFile: () => sourceSession },
45
+ waitForIdle: async () => {},
46
+ ui: { notify: (...args) => notifications.push(args) },
47
+ async switchSession(file, options) {
48
+ switchedFile = file;
49
+ await options.withSession({ ui: { notify: (...args) => notifications.push(args) } });
50
+ return { cancelled: false };
51
+ },
52
+ });
53
+
54
+ const header = JSON.parse((await readFile(switchedFile, "utf8")).split("\n")[0]);
55
+ assert.equal(header.cwd, target);
56
+ assert.equal(header.parentSession, sourceSession);
57
+ assert.deepEqual(notifications.at(-1), [`已切换到 feature:${target}`, "success"]);
58
+ });
59
+
60
+ test("removes the forked session when /worktree switching is cancelled", async () => {
61
+ const cwd = join(root, "cancel-main");
62
+ const target = join(cwd, ".worktrees", "feature");
63
+ await mkdir(target, { recursive: true });
64
+ const sourceSession = join(root, "cancel-source.jsonl");
65
+ await writeFile(sourceSession, `${JSON.stringify({ type: "session", version: 3, id: "cancel-source", timestamp: new Date().toISOString(), cwd })}\n`);
66
+ const prepared = { cwd, result: { status: "created", path: target, branch: "feature", dirty: false } };
67
+ const { commands } = setup(prepared);
68
+ const notifications = [];
69
+ let forkedFile;
70
+ await commands.get("worktree").handler("start feature --base main", {
71
+ cwd,
72
+ sessionManager: { getSessionFile: () => sourceSession },
73
+ waitForIdle: async () => {},
74
+ ui: { notify: (...args) => notifications.push(args) },
75
+ async switchSession(file) {
76
+ forkedFile = file;
77
+ return { cancelled: true };
78
+ },
79
+ });
80
+
81
+ await assert.rejects(readFile(forkedFile, "utf8"), { code: "ENOENT" });
82
+ assert.deepEqual(notifications.at(-1), ["worktree 会话切换已取消", "warning"]);
83
+ });
84
+
85
+ test("registers enter_worktree with a durable Harness switch request", async () => {
86
+ const cwd = join(root, "main");
87
+ const target = join(cwd, ".worktrees", "feature");
88
+ const sourceSession = join(root, "tool-source.jsonl");
89
+ const prepared = { cwd, result: { status: "reused", path: target, branch: "feature", dirty: true } };
90
+ const { tools } = setup(prepared);
91
+ const tool = tools.get("enter_worktree");
92
+ assert.equal(tool.exposure, "model-only");
93
+ assert.ok(tool.outputSchema);
94
+ const result = await tool.execute(
95
+ "call-1",
96
+ { name: "feature", base: "main" },
97
+ new AbortController().signal,
98
+ undefined,
99
+ { cwd, sessionManager: { getSessionFile: () => sourceSession } },
100
+ );
101
+
102
+ assert.equal(result.terminate, true);
103
+ assert.deepEqual(result.details, {
104
+ kind: WORKTREE_SWITCH_REQUEST,
105
+ version: 1,
106
+ action: "fork-and-switch",
107
+ sessionFile: sourceSession,
108
+ status: "reused",
109
+ path: target,
110
+ branch: "feature",
111
+ dirty: true,
112
+ });
113
+ assert.deepEqual(result.structuredContent, result.details);
114
+ });
@@ -42,5 +42,91 @@ grep -q 'status: clean' "$ROOT/info.txt"
42
42
  grep -q 'branch: feature' "$ROOT/info.txt"
43
43
  grep -q 'based on:' "$ROOT/info.txt"
44
44
 
45
+ bash "$PACKAGE_DIR/bin/pi-worktree" prepare --json feature > "$ROOT/reused.json"
46
+ node -e '
47
+ const fs = require("node:fs");
48
+ const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
49
+ if (value.status !== "reused" || value.branch !== "feature" || value.dirty !== false) process.exit(1);
50
+ ' "$ROOT/reused.json"
51
+
52
+ bash "$PACKAGE_DIR/bin/pi-worktree" prepare --json prepared > "$ROOT/created.json"
53
+ node -e '
54
+ const fs = require("node:fs");
55
+ const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
56
+ if (value.status !== "created" || value.branch !== "prepared" || !value.path.endsWith("/.worktrees/prepared")) process.exit(1);
57
+ ' "$ROOT/created.json"
58
+
59
+ mkdir -p nested/path
60
+ (cd nested/path && bash "$PACKAGE_DIR/bin/pi-worktree" prepare --json from-subdir) > "$ROOT/subdir.json"
61
+ node -e '
62
+ const fs = require("node:fs");
63
+ const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
64
+ if (value.path !== process.argv[2] + "/.worktrees/from-subdir") process.exit(1);
65
+ ' "$ROOT/subdir.json" "$ROOT/repo"
66
+
67
+ git branch attachable
68
+ bash "$PACKAGE_DIR/bin/pi-worktree" prepare --json attachable > "$ROOT/attached.json"
69
+ node -e '
70
+ const fs = require("node:fs");
71
+ const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
72
+ if (value.status !== "attached" || value.branch !== "attachable") process.exit(1);
73
+ ' "$ROOT/attached.json"
74
+
75
+ bash "$PACKAGE_DIR/bin/pi-worktree" prepare --json stale > /dev/null
76
+ 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
79
+ exit 1
80
+ fi
81
+ git worktree prune
82
+
83
+ 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
86
+ exit 1
87
+ fi
88
+
89
+ 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"
92
+ exit 1
93
+ fi
94
+
95
+ printf 'changed\n' >> .worktrees/prepared/tracked.txt
96
+ bash "$PACKAGE_DIR/bin/pi-worktree" prepare --json prepared > "$ROOT/dirty.json"
97
+ node -e '
98
+ const fs = require("node:fs");
99
+ const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
100
+ if (value.status !== "reused" || value.dirty !== true) process.exit(1);
101
+ ' "$ROOT/dirty.json"
102
+
103
+ (cd .worktrees/prepared && bash "$PACKAGE_DIR/bin/pi-worktree" prepare --json prepared) > "$ROOT/active.json"
104
+ node -e '
105
+ const fs = require("node:fs");
106
+ const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
107
+ if (value.status !== "already-active" || value.dirty !== true) process.exit(1);
108
+ ' "$ROOT/active.json"
109
+
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
112
+ exit 1
113
+ fi
114
+
115
+ mkdir "$ROOT/symlink-repo" "$ROOT/outside-worktrees"
116
+ (
117
+ cd "$ROOT/symlink-repo"
118
+ git init -q -b main
119
+ git config user.email test@example.com
120
+ git config user.name Test
121
+ printf 'base\n' > tracked.txt
122
+ git add tracked.txt
123
+ git commit -qm initial
124
+ 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
127
+ exit 1
128
+ fi
129
+ )
130
+
45
131
  bash "$PACKAGE_DIR/bin/pi-worktree" --help | grep -q 'pi-worktree start'
46
132
  printf 'PASS worktree CLI integration cases\n'