Skip to content

Commit b7ddd3b

Browse files
committed
fix(coding-agent): carry the monitor wait redirect on the bash surface, dedup terminal guidance
The PTY bash description now routes waits on observable state to the monitor tool (the surface where a model actually types sleep 30; same cross-tool routing pattern as the grep->rg snippet rule). Monitor promptGuidelines collapse to the single when-to-use decision rule and the terminal section keeps mechanics only, so each aspect is stated once. The stale-wait-idioms gate now loads the bash description and the bash-timeout prompt as surfaces, asserts the bash->monitor redirect, and bans tmux teaching outside negative guidance.
1 parent 88fc3ed commit b7ddd3b

5 files changed

Lines changed: 76 additions & 18 deletions

File tree

packages/coding-agent/src/core/extensions/builtin/terminal/changes.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,33 @@
33
The persistent-terminal tool suite (`bash` swapped to PTY-backed + `bash_output`,
44
`kill_bash`, `bash_input`, `bash_resize`). Backed by `@earendil-works/pi-pty`.
55

6+
## Wait-discipline routing: bash surface redirect + guidance dedup (2026-07-28)
7+
8+
### What changed
9+
10+
- `tools/bash.ts`: the PTY `bash` tool description now carries the wait redirect — waiting on
11+
observable state (a build finishing, a server coming up, a log line) is never a sleep/poll
12+
loop; subscribe with the `monitor` tool instead. The bash surface is where a model actually
13+
types `sleep 30`, and cross-tool routing in the misused tool's description follows the same
14+
pattern as the grep→rg snippet rule (`test/bash-prompt-snippet.test.ts`). The upstream core
15+
bash (`src/core/tools/bash.ts`) is deliberately untouched: its toolset has no monitor, and
16+
guidance must never name a tool the toolset lacks.
17+
- `tools/monitor.ts`: promptGuidelines collapsed to the single when-to-use decision rule. The
18+
command-shaping sentence duplicated the TERMINAL_PROMPT_SECTION bullet near-verbatim; each
19+
aspect is now stated once (decision rule → Tool Guidelines; mechanics → terminal section;
20+
redirect → bash schema; long-run routing → bash-timeout policy).
21+
- `prompt.ts`: the monitor bullet dropped its embedded when-to-use sentence (kept as the monitor
22+
tool's guideline) and keeps the subscribe framing plus shaping/filtering/rearm mechanics.
23+
- `test/prompt-surface-stale-wait-idioms.test.ts`: the consistency gate now also loads the PTY
24+
bash description and the bash-timeout prompt section as surfaces; new gates assert the bash
25+
description routes waits to monitor, and that no agent-facing terminal surface teaches tmux as
26+
the backgrounding mechanism (negative guidance like "do NOT use tmux" stays allowed).
27+
28+
### Expected merge conflict zones on next upstream sync
29+
30+
- LOW: `tools/bash.ts` description string, `tools/monitor.ts` promptGuidelines, `prompt.ts`
31+
monitor bullet, gate-test surface list (all fork-owned).
32+
633
## Monitor flat schema + subscribe-not-poll prompt (2026-07-27)
734

835
### What changed

packages/coding-agent/src/core/extensions/builtin/terminal/prompt.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,11 @@ manual \`&\` backgrounding — use the built-in session tools:
1414
\`view: "screen"\`. Completion arrives as a notification carrying the exit code and output
1515
tail — peeking is for steering, never for waiting.
1616
- \`monitor({ description, command, filter?, timeout_ms?, persistent? })\` subscribes you to a
17-
command: its stdout lines arrive as injected events while you keep working. Any wait on
18-
observable state (CI checks, builds, log patterns, deploys) is a monitor, never a foreground
19-
sleep/poll loop. Shape the command by notifications needed: exit-on-condition for one
20-
completion event; emit-per-occurrence (\`tail -f | grep --line-buffered\`, a polling loop
21-
inside the command) for a stream. Filter noise at the command source, stop with \`kill_bash\`,
22-
and use \`monitor({ action: "rearm", bash_id })\` only after a wake-budget pause.
17+
command: its stdout lines arrive as injected events while you keep working. Shape the command
18+
by notifications needed: exit-on-condition for one completion event; emit-per-occurrence
19+
(\`tail -f | grep --line-buffered\`, a polling loop inside the command) for a stream. Filter
20+
noise at the command source, stop with \`kill_bash\`, and use
21+
\`monitor({ action: "rearm", bash_id })\` only after a wake-budget pause.
2322
- \`bash_input({ bash_id, input, keys, submit })\` sends stdin or named keys (e.g.
2423
\`["ctrl+c"]\`, \`["enter"]\`) to steer a REPL or interrupt a process.
2524
- \`bash_resize({ bash_id, cols, rows })\` resizes the PTY so full-screen programs reflow.

packages/coding-agent/src/core/extensions/builtin/terminal/tools/bash.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ export function createPtyBashTool(ctx: TerminalToolContext) {
285285
name: TERMINAL_BASH_TOOL,
286286
label: "bash",
287287
description:
288-
"Execute a shell command in a persistent PTY-backed session. Set run_in_background:true for long-lived or interactive sessions; steer them with bash_input, snapshot with bash_output, tear down with kill_bash. Foreground timeout is a kill deadline in seconds.",
288+
"Execute a shell command in a persistent PTY-backed session. Set run_in_background:true for long-lived or interactive sessions; steer them with bash_input, snapshot with bash_output, tear down with kill_bash. To wait on observable state (a build finishing, a server coming up, a log line), never run sleep or poll loops — subscribe with the monitor tool instead. Foreground timeout is a kill deadline in seconds.",
289289
promptSnippet: "Run shell commands; run_in_background:true for long-lived/interactive PTY sessions",
290290
promptGuidelines: ["Inspect PI_* environment variables for current model and session details."],
291291
parameters: ptyBashSchema,

packages/coding-agent/src/core/extensions/builtin/terminal/tools/monitor.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,6 @@ export function createMonitorTool(ctx: TerminalToolContext) {
115115
promptSnippet: "Subscribe to a command's stdout lines as injected events instead of polling",
116116
promptGuidelines: [
117117
"Waiting on observable state (CI checks, builds, log patterns, deploys) means a monitor, never a foreground sleep/poll loop.",
118-
"Shape the command by notifications needed: exit-on-condition for one completion event; emit-per-occurrence (`tail -f | grep --line-buffered`, a polling loop inside the command) for a stream. Filter noise at the source.",
119118
],
120119
parameters: monitorSchema,
121120
renderCall: renderMonitorCall,

packages/coding-agent/test/prompt-surface-stale-wait-idioms.test.ts

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import { readFileSync } from "node:fs";
22
import { join } from "node:path";
33
import { fileURLToPath } from "node:url";
44
import { describe, expect, it } from "vitest";
5+
import {
6+
buildBashTimeoutPrompt,
7+
resolveBashTimeoutDefaults,
8+
} from "../src/core/extensions/builtin/bash-timeout/timeout.ts";
59
import { TERMINAL_PROMPT_SECTION } from "../src/core/extensions/builtin/terminal/prompt.ts";
10+
import { createPtyBashTool } from "../src/core/extensions/builtin/terminal/tools/bash.ts";
611
import {
712
BASH_OUTPUT_WAIT_REMOVED_GUIDANCE,
813
createBashOutputTool,
@@ -23,22 +28,42 @@ const CODING_AGENT_ROOT = join(REPO_ROOT, "packages", "coding-agent");
2328
/** Lines mentioning wait_for are allowed only when they are removal guidance. */
2429
const GHOST_GUIDANCE_MARKER = /removed|no longer/i;
2530

31+
/** Lines mentioning tmux are allowed only when they steer away from it. */
32+
const NEGATIVE_GUIDANCE_MARKER = /do not|don't|never/i;
33+
2634
function surfaceLines(name: string, text: string): Array<{ name: string; line: number; text: string }> {
2735
return text.split("\n").map((line, index) => ({ name, line: index + 1, text: line }));
2836
}
2937

30-
function loadSurfaces(): Array<{ name: string; line: number; text: string }> {
31-
const stubCtx = {
38+
function stubTerminalCtx(): TerminalToolContext {
39+
return {
3240
manager: { get: () => undefined },
3341
cwd: process.cwd(),
3442
defaultCols: 120,
3543
defaultRows: 40,
3644
getEnv: () => process.env,
3745
} as unknown as TerminalToolContext;
46+
}
47+
48+
/** The prompt surfaces the model actually sees (system-prompt sections + tool schemas). */
49+
function agentFacingSurfaces(): Array<{ name: string; line: number; text: string }> {
50+
const stubCtx = stubTerminalCtx();
51+
return [
52+
...surfaceLines("terminal/prompt.ts TERMINAL_PROMPT_SECTION", TERMINAL_PROMPT_SECTION),
53+
...surfaceLines("bash-timeout prompt section", buildBashTimeoutPrompt(resolveBashTimeoutDefaults({}))),
54+
...surfaceLines("terminal bash tool description", createPtyBashTool(stubCtx).description),
55+
...surfaceLines("bash_output tool description", createBashOutputTool(stubCtx).description),
56+
];
57+
}
58+
59+
function loadSurfaces(): Array<{ name: string; line: number; text: string }> {
60+
const stubCtx = stubTerminalCtx();
3861
const bashOutput = createBashOutputTool(stubCtx);
3962

4063
return [
4164
...surfaceLines("terminal/prompt.ts TERMINAL_PROMPT_SECTION", TERMINAL_PROMPT_SECTION),
65+
...surfaceLines("bash-timeout prompt section", buildBashTimeoutPrompt(resolveBashTimeoutDefaults({}))),
66+
...surfaceLines("terminal bash tool description", createPtyBashTool(stubCtx).description),
4267
...surfaceLines("bash_output tool description", bashOutput.description),
4368
...surfaceLines("bash_output promptSnippet", bashOutput.promptSnippet ?? ""),
4469
...surfaceLines(
@@ -69,15 +94,23 @@ describe("stale wait-idiom consistency gate", () => {
6994
expect(TERMINAL_PROMPT_SECTION).not.toContain("wait_for");
7095
});
7196

97+
it("the bash tool surface routes waits to the monitor tool", () => {
98+
const bash = createPtyBashTool(stubTerminalCtx());
99+
expect(bash.description).toContain("monitor");
100+
});
101+
102+
it("no agent-facing terminal surface teaches tmux as the backgrounding mechanism", () => {
103+
const violations = agentFacingSurfaces().filter(
104+
({ text }) => /tmux/i.test(text) && !NEGATIVE_GUIDANCE_MARKER.test(text),
105+
);
106+
expect(
107+
violations.map(({ name, line, text }) => `${name}:${line}: ${text.trim()}`),
108+
"tmux taught as a backgrounding/wait mechanism outside negative guidance",
109+
).toEqual([]);
110+
});
111+
72112
it("the bash_output tool surface no longer advertises blocking", () => {
73-
const stubCtx = {
74-
manager: { get: () => undefined },
75-
cwd: process.cwd(),
76-
defaultCols: 120,
77-
defaultRows: 40,
78-
getEnv: () => process.env,
79-
} as unknown as TerminalToolContext;
80-
const bashOutput = createBashOutputTool(stubCtx);
113+
const bashOutput = createBashOutputTool(stubTerminalCtx());
81114
expect(bashOutput.description.toLowerCase()).not.toContain("block until");
82115
expect(bashOutput.description).not.toContain("wait_for");
83116
expect(bashOutput.promptSnippet ?? "").not.toContain("wait_for");

0 commit comments

Comments
 (0)