@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.
- package/CHANGELOG.md +12 -0
- package/README.md +2 -1
- package/package.json +8 -5
- package/packages/herdr-subagents/CHANGELOG.md +7 -0
- package/packages/herdr-subagents/README.md +4 -2
- package/packages/herdr-subagents/TESTING.md +6 -1
- package/packages/herdr-subagents/extensions/herdr-subagents.ts +83 -19
- package/packages/herdr-subagents/src/batch-runner.ts +3 -1
- package/packages/herdr-subagents/src/dispatcher.ts +8 -1
- package/packages/herdr-subagents/tests/extension-contract.ts +198 -2
- package/packages/herdr-subagents/tests/run-tests.mjs +25 -0
- package/packages/scheduler/CHANGELOG.md +10 -0
- package/packages/scheduler/README.md +59 -0
- package/packages/scheduler/TESTING.md +19 -0
- package/packages/scheduler/docs/demo.gif +0 -0
- package/packages/scheduler/docs/screenshot.png +0 -0
- package/packages/scheduler/extensions/scheduler.ts +206 -0
- package/packages/scheduler/package.json +25 -0
- package/packages/scheduler/src/scheduler.ts +173 -0
- package/packages/scheduler/tests/scheduler-core.mjs +70 -0
- package/packages/scheduler/tests/scheduler-extension.mjs +117 -0
- package/packages/worktree/CHANGELOG.md +7 -0
- package/packages/worktree/README.md +18 -3
- package/packages/worktree/TESTING.md +5 -2
- package/packages/worktree/bin/pi-worktree +118 -23
- package/packages/worktree/bin/pi-worktree-completion.bash +2 -2
- package/packages/worktree/extensions/worktree.ts +173 -0
- package/packages/worktree/package.json +37 -8
- package/packages/worktree/skills/pi-worktree/SKILL.md +11 -6
- package/packages/worktree/tests/extension.test.mjs +114 -0
- 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'
|