@maxiaochao/pi-toolkit 0.13.4 → 0.13.6
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 +14 -0
- package/README.md +4 -0
- package/package.json +1 -1
- package/packages/background-tasks/CHANGELOG.md +12 -0
- package/packages/background-tasks/README.md +37 -6
- package/packages/background-tasks/TESTING.md +9 -2
- package/packages/background-tasks/docs/demo.gif +0 -0
- package/packages/background-tasks/docs/screenshot.png +0 -0
- package/packages/background-tasks/extensions/background-tasks.ts +88 -8
- package/packages/background-tasks/package.json +2 -2
- package/packages/background-tasks/tests/compaction.mjs +167 -0
- package/packages/background-tasks/tests/extension.mjs +309 -9
- package/packages/background-tasks/tests/fixtures/notifications.ts +77 -0
- package/packages/background-tasks/tests/fixtures/provider.ts +14 -4
- package/packages/background-tasks/tests/interruptions.mjs +140 -0
- package/packages/background-tasks/tests/notifications.mjs +80 -0
- package/packages/background-tasks/tests/tui-smoke.py +8 -4
- package/packages/herdr-subagents/CHANGELOG.md +6 -0
- package/packages/herdr-subagents/README.md +4 -0
- package/packages/herdr-subagents/TESTING.md +4 -0
- package/packages/herdr-subagents/extensions/subagent-report.ts +2 -1
- package/packages/herdr-subagents/package.json +1 -1
- package/packages/herdr-subagents/src/live-agent.ts +5 -2
- package/packages/herdr-subagents/tests/config.mjs +8 -0
- package/packages/herdr-subagents/tests/core-state-machine.mjs +36 -0
- package/packages/herdr-subagents/tests/extension-sources-load.mjs +25 -0
- package/packages/herdr-subagents/tests/run-tests.mjs +1 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { test } from "node:test";
|
|
8
|
+
import { piBinary } from "./runtime.mjs";
|
|
9
|
+
|
|
10
|
+
test("native Codemode receives start guidance and exposes it to the model", () => {
|
|
11
|
+
const directory = mkdtempSync(join(tmpdir(), "pi-bg-guidance-"));
|
|
12
|
+
try {
|
|
13
|
+
const events = join(directory, "events.jsonl");
|
|
14
|
+
const output = execFileSync(piBinary, [
|
|
15
|
+
"--no-extensions", "--no-skills", "--no-prompt-templates", "--no-themes", "--no-context-files", "--no-session", "--offline",
|
|
16
|
+
"-e", "builtin:codemode", "-e", fileURLToPath(new URL("../extensions/background-tasks.ts", import.meta.url)),
|
|
17
|
+
"-e", fileURLToPath(new URL("fixtures/provider.ts", import.meta.url)),
|
|
18
|
+
"--tools", "codemode,background_task", "--model", "bg-fixture/deterministic", "-p", "SMOKE_GUIDANCE",
|
|
19
|
+
], { cwd: directory, encoding: "utf8", timeout: 30000,
|
|
20
|
+
env: { ...process.env, PI_CODING_AGENT_DIR: directory, PI_OFFLINE: "1", BG_SMOKE_EVENTS: events }, stdio: ["ignore", "pipe", "pipe"] });
|
|
21
|
+
assert.match(output, /Main agent is ready/);
|
|
22
|
+
const records = readFileSync(events, "utf8").trim().split("\n").map((line) => JSON.parse(line));
|
|
23
|
+
const started = records.find((entry) => entry.event === "tool_end" && entry.name === "background_task");
|
|
24
|
+
assert.equal(started.isError, false);
|
|
25
|
+
const guidance = started.result.structuredContent.guidance;
|
|
26
|
+
assert.equal(guidance, "完成会自动通知。现在不要查询等待;继续其他独立工作,没有其他工作就结束当前轮,收到通知后再取结果。");
|
|
27
|
+
const codemode = records.find((entry) => entry.event === "tool_end" && entry.name === "codemode");
|
|
28
|
+
assert.equal(codemode.isError, false);
|
|
29
|
+
assert.ok(codemode.result.content.some((block) => block.type === "text" && block.text.includes(guidance)));
|
|
30
|
+
const requests = records.filter((entry) => entry.event === "request");
|
|
31
|
+
assert.ok(requests[0].backgroundDescription.includes(guidance));
|
|
32
|
+
assert.ok(requests[1].text.includes(guidance), "the next model request sees guidance printed from the programmatic result");
|
|
33
|
+
} finally { rmSync(directory, { recursive: true, force: true }); }
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
for (const mode of ["codemode", "direct"]) {
|
|
37
|
+
for (const reads of [0, 1, 4]) {
|
|
38
|
+
test(`real Pi ${mode} merges four completions before the next model call, excluding ${reads} consumed results`, () => {
|
|
39
|
+
const directory = mkdtempSync(join(tmpdir(), "pi-bg-notifications-"));
|
|
40
|
+
try {
|
|
41
|
+
const events = join(directory, "events.jsonl");
|
|
42
|
+
writeFileSync(join(directory, "settings.json"), JSON.stringify({ quietStartup: true, followUpMode: "one-at-a-time", steeringMode: "one-at-a-time" }));
|
|
43
|
+
const output = execFileSync(piBinary, [
|
|
44
|
+
"--no-extensions", "--no-skills", "--no-prompt-templates", "--no-themes", "--no-context-files", "--no-session", "--offline",
|
|
45
|
+
"-e", "builtin:codemode", "-e", process.env.BG_TEST_EXTENSION ?? fileURLToPath(new URL("../extensions/background-tasks.ts", import.meta.url)),
|
|
46
|
+
"-e", fileURLToPath(new URL("fixtures/notifications.ts", import.meta.url)),
|
|
47
|
+
"--tools", "codemode,background_task,synchronous_subagent_fixture", "--model", "bg-notifications/deterministic", "--mode", "json", "-p", "Run the synchronous subagent scenario",
|
|
48
|
+
], { cwd: directory, encoding: "utf8", timeout: 30000, env: { ...process.env, PI_CODING_AGENT_DIR: directory, PI_OFFLINE: "1", BG_NOTIFICATION_EVENTS: events, BG_NOTIFICATION_READS: String(reads), BG_NOTIFICATION_MODE: mode }, stdio: ["ignore", "pipe", "pipe"] });
|
|
49
|
+
assert.doesNotMatch(output, /Failed to load|Error loading|Fixture shell jobs did not finish/);
|
|
50
|
+
const records = readFileSync(events, "utf8").trim().split("\n").map((line) => JSON.parse(line));
|
|
51
|
+
const returned = records.find((entry) => entry.event === "returned");
|
|
52
|
+
assert.ok(returned, `the blocking tool finished all real shell jobs\n${output}`);
|
|
53
|
+
assert.equal(returned.tasks.length, 4);
|
|
54
|
+
assert.ok(returned.tasks.every((task) => task.status === "failed" && task.exit_code === 7));
|
|
55
|
+
const transcript = output.trim().split("\n").map((line) => JSON.parse(line));
|
|
56
|
+
const notifications = transcript.filter((entry) =>
|
|
57
|
+
(entry.type === "entry_appended" && entry.entry.customType === "background-task-finished") ||
|
|
58
|
+
(entry.type === "message_start" && entry.message.customType === "background-task-finished")
|
|
59
|
+
);
|
|
60
|
+
assert.equal(notifications.length, reads === 4 ? 0 : 1);
|
|
61
|
+
if (reads !== 4) {
|
|
62
|
+
const message = notifications[0].entry ?? notifications[0].message;
|
|
63
|
+
const tasks = message.details.tasks;
|
|
64
|
+
assert.equal(tasks.length, 4 - reads);
|
|
65
|
+
assert.ok(tasks.every((task) => !returned.consumed.includes(task.id)));
|
|
66
|
+
const toolEnd = transcript.findIndex((entry) => entry.type === "tool_execution_end" && entry.toolName === "synchronous_subagent_fixture");
|
|
67
|
+
assert.ok(toolEnd >= 0 && transcript.indexOf(notifications[0]) > toolEnd, "the notification cannot interrupt the synchronous tool");
|
|
68
|
+
assert.match(message.content, /Ended at: \d{4}-/);
|
|
69
|
+
}
|
|
70
|
+
const requests = records.filter((entry) => entry.event === "request");
|
|
71
|
+
assert.equal(requests.length, mode === "direct" ? 3 : 2, "no extra model turns after the final report");
|
|
72
|
+
const nextContext = JSON.stringify(requests.at(-1).messages);
|
|
73
|
+
assert.equal((nextContext.match(/\[Background task finished\]/g) ?? []).length, reads === 4 ? 0 : 1);
|
|
74
|
+
assert.doesNotMatch(nextContext, /FINAL_REPORT/, "completion is delivered before, not after, the final report");
|
|
75
|
+
const contextText = requests.at(-1).messages.flatMap((message) => typeof message.content === "string" ? [message.content] : (message.content ?? []).filter((block) => block.type === "text").map((block) => block.text)).join("\n");
|
|
76
|
+
for (const task of returned.tasks.filter((task) => !returned.consumed.includes(task.id))) assert.ok(contextText.includes(task.id));
|
|
77
|
+
} finally { rmSync(directory, { recursive: true, force: true }); }
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -138,12 +138,15 @@ try:
|
|
|
138
138
|
shot('running')
|
|
139
139
|
send('/bg output ' + first['id'])
|
|
140
140
|
send('SMOKE_CANCEL')
|
|
141
|
-
wait(lambda: bool(finished('cancelled')), 'cancel
|
|
142
|
-
wait(lambda: bool(finished('succeeded')), 'successful background
|
|
141
|
+
wait(lambda: bool(finished('cancelled')), 'cancel notification')
|
|
142
|
+
wait(lambda: bool(finished('succeeded')), 'successful background notification')
|
|
143
143
|
shot('completed')
|
|
144
144
|
send('SMOKE_OUTPUT')
|
|
145
145
|
wait(lambda: any(e.get('result', {}).get('structuredContent', {}).get('output') == 'BUILD SUCCESSFUL\n' for e in tools()), 'explicit output query')
|
|
146
|
-
wait(lambda: '
|
|
146
|
+
wait(lambda: 'BACKGROUND_NOTIFICATION_RECEIVED' in raw, 'main agent automatic response')
|
|
147
|
+
pump(0.5)
|
|
148
|
+
notifications = [e for e in events() if e.get('event') == 'custom' and e.get('type') == 'background-task-finished']
|
|
149
|
+
assert len(notifications) == 2, 'Duplicate persisted busy/idle notifications'
|
|
147
150
|
pump(5.3)
|
|
148
151
|
shot('history')
|
|
149
152
|
|
|
@@ -174,7 +177,8 @@ try:
|
|
|
174
177
|
(OUT / 'events.jsonl').write_text('\n'.join(json.dumps(e) for e in records) + '\n')
|
|
175
178
|
(OUT / 'result.json').write_text(json.dumps({'passed': True, 'screenshots': screens,
|
|
176
179
|
'checks': ['two jobs', 'third rejected', 'Codemode returns while jobs live', 'Scheduler coexistence', '/bg output',
|
|
177
|
-
'cancel notification', 'success notification', 'main agent
|
|
180
|
+
'cancel notification', 'success notification', 'main agent notification', 'no duplicate persisted notifications',
|
|
181
|
+
'5-second terminal row', 'reload cleanup', 'new-session cleanup', 'quit cleanup']}, indent=2))
|
|
178
182
|
except Exception:
|
|
179
183
|
(OUT / 'error.txt').write_text(traceback.format_exc())
|
|
180
184
|
raise
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## 2026-10-22 - v0.7.4
|
|
2
|
+
|
|
3
|
+
- preserve Codex patch editing in child allowlists and hide the parent shortcut footer hint
|
|
4
|
+
- Hide the child parent-navigation footer hint and clear its legacy status on reload, while retaining `Alt+P`, `/parent`, and editor/plugin status.
|
|
5
|
+
- Permit `apply_patch` in the persistent CLI allowlist only for Personas that allow `edit`; test actual Pi startup selection, read-only limits and native fallback instead of relying on mocked tool lists or status labels.
|
|
6
|
+
|
|
1
7
|
## 2026-10-22 - v0.7.3
|
|
2
8
|
|
|
3
9
|
- keep Persona selection lists concise
|
|
@@ -48,6 +48,10 @@ The tools and command are registered only when Pi is running inside Herdr with a
|
|
|
48
48
|
|
|
49
49
|
Each task starts a single-task interactive Pi process in its own Herdr pane. Herdr first confirms the interactive Pi is ready, then the prompt is delivered exactly once. Prompt acknowledgement never decides task completion. Panes remain ordinary Herdr panes: they can be focused and inspected while work is running or after it settles. An early-finishing child stays at the Pi UI while sibling tasks continue. After the whole run settles, cleanup closes an unfocused run tab immediately or waits for the user to leave a focused run tab.
|
|
50
50
|
|
|
51
|
+
Use `Alt+P` or `/parent` in a child to focus its parent Agent pane. The shortcut has no persistent footer hint. Other extensions' status text, such as Codex Edit's `editor: apply_patch`, remains visible when those extensions are loaded and their editing tools are active. Changing Persona extensions does not change an already running child.
|
|
52
|
+
|
|
53
|
+
For a Persona whose `tools` includes `edit`, the child CLI also permits `apply_patch` so a trusted editing extension can switch protocols. This does not load an extension or enable both editors by itself. Personas without `edit` do not receive this permission, and a child without a patch extension keeps native `edit`.
|
|
54
|
+
|
|
51
55
|
Cleanup belongs to the Pi session that created the run. Starting a new Pi session never scans historical runs or closes their recorded Herdr tabs. If Pi terminates before shutdown cleanup runs, any surviving Subagent tab remains visible for explicit inspection or cleanup.
|
|
52
56
|
|
|
53
57
|
## Result protocol
|
|
@@ -29,6 +29,8 @@ It verifies:
|
|
|
29
29
|
- immutable terminal task records;
|
|
30
30
|
- idempotent duplicate reports and rejection of conflicting replacements;
|
|
31
31
|
- keeping a settled child alive for focus-aware cleanup instead of force-exiting its TUI;
|
|
32
|
+
- clearing the legacy parent shortcut footer hint while preserving other extension status, with `Alt+P` and `/parent` still focusing the exact parent pane;
|
|
33
|
+
- adding `apply_patch` to the child CLI allowlist only when the Persona permits `edit`;
|
|
32
34
|
- preserving the configured smoke model, thinking level, and authentication extensions while isolating unrelated settings;
|
|
33
35
|
- project-over-global smoke configuration, explicit extension clearing, model overrides, and local-source paths after moving to a temporary directory.
|
|
34
36
|
|
|
@@ -40,6 +42,8 @@ Run extension source loading. The direct-Pi fixture resolves global `defaultExte
|
|
|
40
42
|
npm run load-test --workspace @maxiaochao/pi-herdr-subagents
|
|
41
43
|
```
|
|
42
44
|
|
|
45
|
+
The native CLI fixture also exercises an extension that switches `edit` to `apply_patch`. It checks the final active tools, not just status text: writable children retain the selected patch tool, read-only/no-editor children do not gain it, and children without the patch extension retain native `edit`. This deterministic probe does not call a model or test Codex Edit's parser.
|
|
46
|
+
|
|
43
47
|
Run the real Herdr smoke test from a Herdr session:
|
|
44
48
|
|
|
45
49
|
```sh
|
|
@@ -96,7 +96,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
96
96
|
async handler(ctx) { await returnToParent(pi, ctx); },
|
|
97
97
|
});
|
|
98
98
|
pi.on("session_start", async (_event, ctx) => {
|
|
99
|
-
|
|
99
|
+
// Remove the legacy hint on reload without touching editor/plugin status.
|
|
100
|
+
ctx.ui.setStatus("herdr-subagents-parent", undefined);
|
|
100
101
|
});
|
|
101
102
|
pi.registerTool({
|
|
102
103
|
name: "subagent_report",
|
|
@@ -42,8 +42,11 @@ export function childPiArgs(task: LiveTaskRecord, systemPromptFile: string): str
|
|
|
42
42
|
for (const extension of task.extensions ?? []) args.push("--extension", extension);
|
|
43
43
|
if (task.model) args.push("--model", task.model);
|
|
44
44
|
if (task.thinking) args.push("--thinking", task.thinking);
|
|
45
|
-
if (task.tools?.length)
|
|
46
|
-
|
|
45
|
+
if (task.tools?.length) {
|
|
46
|
+
// Pi keeps this allowlist after startup, including editor protocol switches.
|
|
47
|
+
const editorTools = task.tools.includes("edit") ? ["apply_patch"] : [];
|
|
48
|
+
args.push("--tools", [...new Set([...task.tools, ...editorTools, "codemode", "subagent_report"])].join(","));
|
|
49
|
+
} else args.push("--tools", "+codemode");
|
|
47
50
|
for (const skill of task.skills ?? []) args.push("--skill", skill);
|
|
48
51
|
return args;
|
|
49
52
|
}
|
|
@@ -108,6 +108,14 @@ try {
|
|
|
108
108
|
assert.ok(clearedArgs.some((arg) => arg.endsWith("/extensions/subagent-report.ts")));
|
|
109
109
|
assert.ok(!clearedArgs.includes("./updated.ts"));
|
|
110
110
|
assert.deepEqual(selected.extensions, ["./updated.ts"]);
|
|
111
|
+
console.log("PASS defaults reach child arguments and control listings, reload per call, and preserve mandatory extensions");
|
|
112
|
+
|
|
113
|
+
const editorArgs = childPiArgs({ id: "editor", sessionFile: join(root, "editor.jsonl"), tools: ["read", "edit", "write"] }, join(root, "system.md"));
|
|
114
|
+
assert.deepEqual(editorArgs[editorArgs.indexOf("--tools") + 1].split(","), ["read", "edit", "write", "apply_patch", "codemode", "subagent_report"]);
|
|
115
|
+
const readonlyArgs = childPiArgs({ id: "readonly", sessionFile: join(root, "readonly.jsonl"), tools: ["read", "bash"] }, join(root, "system.md"));
|
|
116
|
+
assert.ok(!readonlyArgs[readonlyArgs.indexOf("--tools") + 1].split(",").includes("apply_patch"));
|
|
117
|
+
console.log("PASS edit-capable children allow the patch editor without adding it to read-only tool lists");
|
|
118
|
+
|
|
111
119
|
console.log("PASS concise Persona listings preserve diagnostics and detailed metadata; defaults reach child arguments and reload per call");
|
|
112
120
|
} finally {
|
|
113
121
|
rmSync(root, { recursive: true, force: true });
|
|
@@ -291,6 +291,42 @@ assert.throws(() => applyTaskStatus(terminal, "running"), /terminal tasks are im
|
|
|
291
291
|
assert.match(notifications[1], /: (?:package|global|project), (?:read|write), model=.*thinking=.*tools=.*skills=.*extensions=/, "User-facing inspection must retain detailed configuration fields");
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
{
|
|
295
|
+
const previousParent = process.env.PI_HERDR_SUBAGENTS_PARENT_PANE_ID;
|
|
296
|
+
process.env.PI_HERDR_SUBAGENTS_PARENT_PANE_ID = "test-workspace:parent";
|
|
297
|
+
const handlers = new Map();
|
|
298
|
+
const commands = new Map();
|
|
299
|
+
const shortcuts = new Map();
|
|
300
|
+
const calls = [];
|
|
301
|
+
const statuses = new Map([
|
|
302
|
+
["herdr-subagents-parent", "alt+p parent"],
|
|
303
|
+
["codex-edit", "editor: apply_patch"],
|
|
304
|
+
]);
|
|
305
|
+
const ctx = { ui: { notify() {}, setStatus(key, value) { statuses.set(key, value); } } };
|
|
306
|
+
try {
|
|
307
|
+
registerSubagentReport({
|
|
308
|
+
registerTool() {},
|
|
309
|
+
registerCommand(name, command) { commands.set(name, command); },
|
|
310
|
+
registerShortcut(key, shortcut) { shortcuts.set(key, shortcut); },
|
|
311
|
+
on(name, handler) { handlers.set(name, handler); },
|
|
312
|
+
events: { on() {} },
|
|
313
|
+
async exec(command, args) { calls.push([command, args]); return { code: 0, stdout: "", stderr: "" }; },
|
|
314
|
+
});
|
|
315
|
+
await handlers.get("session_start")({}, ctx);
|
|
316
|
+
assert.equal(statuses.get("herdr-subagents-parent"), undefined, "child startup clears the old parent shortcut hint");
|
|
317
|
+
assert.equal(statuses.get("codex-edit"), "editor: apply_patch", "child navigation must not overwrite editor status");
|
|
318
|
+
await commands.get("parent").handler("", ctx);
|
|
319
|
+
await shortcuts.get("alt+p").handler(ctx);
|
|
320
|
+
assert.deepEqual(calls, [
|
|
321
|
+
["herdr", ["agent", "focus", "test-workspace:parent"]],
|
|
322
|
+
["herdr", ["agent", "focus", "test-workspace:parent"]],
|
|
323
|
+
], "the parent command and shortcut must remain functional without a footer hint");
|
|
324
|
+
} finally {
|
|
325
|
+
if (previousParent === undefined) delete process.env.PI_HERDR_SUBAGENTS_PARENT_PANE_ID;
|
|
326
|
+
else process.env.PI_HERDR_SUBAGENTS_PARENT_PANE_ID = previousParent;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
294
330
|
{
|
|
295
331
|
const directory = join(root, "report-idempotency");
|
|
296
332
|
mkdirSync(directory, { recursive: true });
|
|
@@ -19,6 +19,15 @@ try {
|
|
|
19
19
|
}));
|
|
20
20
|
writeFileSync(join(packageDirectory, "extension.ts"), `export default function (pi) {
|
|
21
21
|
pi.registerCommand("package-extension", { description: "Package extension loaded", handler() {} });
|
|
22
|
+
pi.registerTool({
|
|
23
|
+
name: "apply_patch", label: "Patch editor probe", description: "Editing protocol selection probe",
|
|
24
|
+
parameters: { type: "object", properties: {}, additionalProperties: false },
|
|
25
|
+
async execute() { return { content: [], details: {} }; },
|
|
26
|
+
});
|
|
27
|
+
pi.on("session_start", () => {
|
|
28
|
+
const active = pi.getActiveTools();
|
|
29
|
+
if (active.includes("edit")) pi.setActiveTools([...active.filter((name) => name !== "edit"), "apply_patch"]);
|
|
30
|
+
});
|
|
22
31
|
}`);
|
|
23
32
|
writeFileSync(join(packageDirectory, "skills", "package-skill", "SKILL.md"), "---\nname: package-skill\ndescription: Package skill loading probe\n---\n# Probe\n");
|
|
24
33
|
writeFileSync(join(packageDirectory, "prompts", "package-prompt.md"), "Package prompt loading probe.");
|
|
@@ -58,6 +67,22 @@ try {
|
|
|
58
67
|
for (const command of ["file-extension", "package-extension", "skill:package-skill", "package-prompt", "parent"]) assert.ok(loaded.commands.includes(command), `Missing ${command}: ${JSON.stringify(loaded)}`);
|
|
59
68
|
assert.deepEqual([...loaded.tools].sort(), ["read", "subagent_report"]);
|
|
60
69
|
console.log("PASS real Pi loads defaultExtensions file and package sources, including package skills/prompts despite discovery flags, without broadening the direct-Pi tool allowlist");
|
|
70
|
+
|
|
71
|
+
for (const [id, tools, extensions, expected] of [
|
|
72
|
+
["patch-editor", ["read", "edit", "write"], analyst.extensions, ["read", "write", "apply_patch", "subagent_report"]],
|
|
73
|
+
["no-editor", ["read", "write"], analyst.extensions, ["read", "write", "subagent_report"]],
|
|
74
|
+
["native-editor", ["read", "edit", "write"], [fileExtension, probe], ["read", "edit", "write", "subagent_report"]],
|
|
75
|
+
]) {
|
|
76
|
+
const childArgs = childPiArgs({ id, sessionFile: join(root, `${id}.jsonl`), tools, extensions }, join(root, "system.md"));
|
|
77
|
+
const builtin = childArgs.indexOf("builtin:codemode");
|
|
78
|
+
childArgs.splice(builtin - 1, 2);
|
|
79
|
+
const child = spawnSync(process.env.PI_TEST_BINARY?.trim() || "pi", [...childArgs, "--offline"], { cwd: root, env, encoding: "utf8", timeout: 30_000 });
|
|
80
|
+
assert.equal(child.error, undefined, String(child.error));
|
|
81
|
+
assert.equal(child.status, 0, child.stderr || child.stdout);
|
|
82
|
+
const actual = JSON.parse(readFileSync(resultPath, "utf8"));
|
|
83
|
+
assert.deepEqual([...actual.tools].sort(), [...expected].sort(), `${id}: the real CLI must preserve the selected editing protocol`);
|
|
84
|
+
}
|
|
85
|
+
console.log("PASS real Pi CLI permits apply_patch for edit-capable children, preserves native fallback, and denies it to read-only/no-editor children");
|
|
61
86
|
} finally {
|
|
62
87
|
rmSync(root, { recursive: true, force: true });
|
|
63
88
|
}
|
|
@@ -430,7 +430,7 @@ try {
|
|
|
430
430
|
await childEvents.get("session_start")?.({}, childContext);
|
|
431
431
|
await childCommands.get("parent")?.handler("", childContext);
|
|
432
432
|
await childShortcuts.get("alt+p")?.handler(childContext);
|
|
433
|
-
check("child command and shortcut return to the exact parent Agent pane", childExecCalls.length === 2 && childExecCalls.every(([command, args]) => command === "herdr" && args.join(",") === "agent,focus,w1:p-parent") && childStatuses.
|
|
433
|
+
check("child command and shortcut return to the exact parent Agent pane without a footer hint", childExecCalls.length === 2 && childExecCalls.every(([command, args]) => command === "herdr" && args.join(",") === "agent,focus,w1:p-parent") && childStatuses.every(([, value]) => value === undefined));
|
|
434
434
|
const reportToolDirectory = join(root, "child-report-tool");
|
|
435
435
|
const reportAttemptId = "11111111-1111-4111-8111-111111111111";
|
|
436
436
|
mkdirSync(reportToolDirectory, { recursive: true });
|