Skip to content

Commit 723ee85

Browse files
committed
pi-bash-bg: stop replacing the bash tool so shellCommandPrefix keeps working
createBashTool(process.cwd()) + pi.registerTool(...) was used to append a background-job note to the tool description, but the freshly-constructed bash tool shadowed pi's built-in one in the registry, silently dropping the shellCommandPrefix, shellPath, and spawnHook that pi wires onto the built-in tool. Any user with "shellCommandPrefix": "..." in settings saw their prefix stop running as soon as pi-bash-bg was loaded. Move the guidance into the system prompt via before_agent_start so the built-in bash tool is left untouched. Add a changeset. chore(bash-bg): shorten changeset summary wip: simplify bash-bg system prompt wip
1 parent 138acb8 commit 723ee85

4 files changed

Lines changed: 99 additions & 29 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"pi-bash-bg": patch
3+
---
4+
5+
Stop replacing pi's built-in `bash` tool so `shellCommandPrefix`, `shellPath`, and `spawnHook` keep working; background-job guidance is now injected into the system prompt instead.

packages/bash-bg/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ The bash tool pipes stdout/stderr from the spawned shell. When a command backgro
88

99
## Solution
1010

11-
This extension intercepts bash tool calls, parses the command with [@aliou/sh](https://github.com/nicolo-ribaudo/sh) to detect background processes (`stmt.background === true`), appends background-job guidance to the bash tool description, and rewrites the command to:
11+
This extension intercepts bash tool calls, parses the command with [@aliou/sh](https://github.com/nicolo-ribaudo/sh) to detect background processes (`stmt.background === true`), appends background-job guidance to the system prompt, and rewrites the command to:
1212

1313
1. **Redirect output** to temp log files with human-readable names based on the command label, so background processes release the pipes
1414
2. **Add `disown`** to detach from job control (if not already present)
@@ -78,9 +78,9 @@ echo "[bg] pid=$! label=npm start log=/tmp/pi-bg-npm-start-3.log"
7878

7979
If the command already has `disown` after the `&`, no duplicate is added.
8080

81-
### Bash tool description
81+
### System prompt guidance
8282

83-
The extension appends a short background-job summary to the bash tool description, so the model sees it directly in tool metadata.
83+
The extension appends a short background-job section to the system prompt via the `before_agent_start` hook, so the model sees how to use `command &`, where to find log files, and how to stop processes. The built-in `bash` tool definition is left untouched so pi's `shellCommandPrefix`, `shellPath`, and any `spawnHook` wiring continue to apply.
8484

8585
## Supported patterns
8686

packages/bash-bg/src/index.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,20 @@
1515
*/
1616

1717
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
18-
import { createBashTool, isToolCallEventType } from "@mariozechner/pi-coding-agent";
18+
import { isToolCallEventType } from "@mariozechner/pi-coding-agent";
1919
import { detectBackground } from "./detect.js";
2020
import { rewriteCommand } from "./rewrite.js";
2121

2222
export { detectBackground, type BgStatement, type DetectResult } from "./detect.js";
2323
export { rewriteCommand, findBgOperatorPositions, type BgProcessInfo, type RewriteResult } from "./rewrite.js";
2424

25-
const BASH_DESCRIPTION_APPENDIX =
26-
"Background jobs continue running after the command returns. Their output is captured to a log file even without explicit redirection. The PID and log path will be returned.";
25+
export const BASH_BG_SYSTEM_PROMPT_SECTION = [
26+
"## Background jobs",
27+
"",
28+
"`command &` returns immediately; output is captured to a log file shown in the `[bg] pid=<PID> label=<LABEL> log=<PATH>` line.",
29+
].join("\n");
2730

2831
export default function (pi: ExtensionAPI) {
29-
const bashTool = createBashTool(process.cwd());
30-
pi.registerTool({
31-
...bashTool,
32-
description: `${bashTool.description} ${BASH_DESCRIPTION_APPENDIX}`,
33-
});
34-
3532
pi.on("tool_call", async (event) => {
3633
if (!isToolCallEventType("bash", event)) return;
3734

@@ -42,4 +39,15 @@ export default function (pi: ExtensionAPI) {
4239
const { command: rewritten } = rewriteCommand(command, bgStatements);
4340
event.input.command = rewritten;
4441
});
42+
43+
pi.on("before_agent_start", async (event) => {
44+
// Inject background-job guidance into the system prompt once per turn.
45+
// We avoid re-registering the bash tool because that would drop the
46+
// commandPrefix/shellPath/spawnHook options that pi's built-in bash
47+
// tool is constructed with, silently breaking `shellCommandPrefix`.
48+
if (event.systemPrompt.includes(BASH_BG_SYSTEM_PROMPT_SECTION)) return;
49+
return {
50+
systemPrompt: `${event.systemPrompt}\n\n${BASH_BG_SYSTEM_PROMPT_SECTION}`,
51+
};
52+
});
4553
}
Lines changed: 74 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,82 @@
1-
import type { ExtensionAPI, ToolDefinition } from "@mariozechner/pi-coding-agent";
2-
import { describe, expect, it, vi } from "vitest";
3-
import extension from "../src/index.js";
1+
import type {
2+
BeforeAgentStartEvent,
3+
ExtensionAPI,
4+
ExtensionHandler,
5+
ToolDefinition,
6+
} from "@mariozechner/pi-coding-agent";
7+
import { describe, expect, it } from "vitest";
8+
import extension, { BASH_BG_SYSTEM_PROMPT_SECTION } from "../src/index.js";
9+
10+
type HandlerMap = Map<string, ExtensionHandler<never>[]>;
11+
12+
function makeFakePi(): { pi: ExtensionAPI; handlers: HandlerMap; tools: ToolDefinition[] } {
13+
const handlers: HandlerMap = new Map();
14+
const tools: ToolDefinition[] = [];
15+
const pi = {
16+
on(event: string, handler: ExtensionHandler<never>) {
17+
const list = handlers.get(event) ?? [];
18+
list.push(handler);
19+
handlers.set(event, list);
20+
},
21+
registerTool(tool: ToolDefinition) {
22+
tools.push(tool);
23+
},
24+
} as unknown as ExtensionAPI;
25+
return { pi, handlers, tools };
26+
}
427

528
describe("pi-bash-bg extension", () => {
6-
it("appends background job guidance to the bash tool description", () => {
7-
const tools: ToolDefinition[] = [];
8-
const pi = {
9-
registerTool(tool) {
10-
tools.push(tool);
11-
},
12-
on: vi.fn(),
13-
} as unknown as ExtensionAPI;
29+
it("does not re-register the bash tool (preserves pi's commandPrefix/shellPath/spawnHook)", () => {
30+
const { pi, tools } = makeFakePi();
31+
extension(pi);
32+
expect(tools.find((tool) => tool.name === "bash")).toBeUndefined();
33+
// The extension must not register any tool; it only hooks events.
34+
expect(tools).toHaveLength(0);
35+
});
36+
37+
it("registers tool_call and before_agent_start handlers", () => {
38+
const { pi, handlers } = makeFakePi();
39+
extension(pi);
40+
expect(handlers.get("tool_call")?.length).toBe(1);
41+
expect(handlers.get("before_agent_start")?.length).toBe(1);
42+
});
1443

44+
it("appends background-job guidance to the system prompt", async () => {
45+
const { pi, handlers } = makeFakePi();
1546
extension(pi);
47+
const handler = handlers.get("before_agent_start")?.[0];
48+
expect(handler).toBeDefined();
49+
50+
const event: BeforeAgentStartEvent = {
51+
type: "before_agent_start",
52+
prompt: "hello",
53+
systemPrompt: "You are a coding agent.",
54+
systemPromptOptions: {} as BeforeAgentStartEvent["systemPromptOptions"],
55+
};
56+
const result = await (handler as (e: BeforeAgentStartEvent) => Promise<{ systemPrompt?: string } | undefined>)(
57+
event,
58+
);
1659

17-
const bashTool = tools.find((tool) => tool.name === "bash");
18-
expect(bashTool).toBeDefined();
19-
expect(bashTool?.description).toContain("Background jobs continue running after the command returns.");
20-
expect(bashTool?.description).toContain(
21-
"Their output is captured to a log file even without explicit redirection.",
60+
expect(result?.systemPrompt).toBeDefined();
61+
expect(result?.systemPrompt).toContain("You are a coding agent.");
62+
expect(result?.systemPrompt).toContain(BASH_BG_SYSTEM_PROMPT_SECTION);
63+
expect(result?.systemPrompt).toContain("## Background jobs");
64+
expect(result?.systemPrompt).toContain("[bg] pid=");
65+
});
66+
67+
it("does not duplicate the guidance if it is already present", async () => {
68+
const { pi, handlers } = makeFakePi();
69+
extension(pi);
70+
const handler = handlers.get("before_agent_start")?.[0];
71+
const event: BeforeAgentStartEvent = {
72+
type: "before_agent_start",
73+
prompt: "hello",
74+
systemPrompt: `You are a coding agent.\n\n${BASH_BG_SYSTEM_PROMPT_SECTION}`,
75+
systemPromptOptions: {} as BeforeAgentStartEvent["systemPromptOptions"],
76+
};
77+
const result = await (handler as (e: BeforeAgentStartEvent) => Promise<{ systemPrompt?: string } | undefined>)(
78+
event,
2279
);
23-
expect(bashTool?.description).toContain("The PID and log path will be returned.");
80+
expect(result).toBeUndefined();
2481
});
2582
});

0 commit comments

Comments
 (0)