@oh-my-pi/pi-coding-agent 13.9.5 → 13.9.11

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 CHANGED
@@ -2,6 +2,51 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [13.9.10] - 2026-03-08
6
+ ### Added
7
+
8
+ - Added `env` parameter to bash tool to pass environment variables safely without shell re-parsing, preventing quote and special character bugs with multiline or untrusted values
9
+ - Added support for rendering partial `env` assignments in command preview while tool arguments are still streaming
10
+ - Added `env` support to the bash tool so commands can reference safe shell variables without inline quoting bugs for multiline or quote-heavy values
11
+
12
+ ### Changed
13
+
14
+ - Changed bash tool to display environment variable assignments in command preview when `env` parameter is used
15
+
16
+ ## [13.9.8] - 2026-03-08
17
+ ### Added
18
+
19
+ - Added docs.rs scraper for extracting Rust crate documentation from rustdoc JSON, including support for modules, functions, structs, traits, enums, and other Rust items with caching
20
+
21
+ ## [13.9.7] - 2026-03-08
22
+
23
+ ### Added
24
+
25
+ - Added `skipPostPromptRecoveryWait` option to handoff operations to defer recovery work until after handoff completion
26
+ - Added deferred auto-compaction scheduling to allow threshold-triggered handoffs to complete while the original prompt is still unwinding
27
+
28
+ ### Changed
29
+
30
+ - Extracted handoff document template to dedicated prompt file for improved maintainability and template variable support
31
+ - Changed handoff prompt generation to use template rendering with support for custom focus instructions
32
+ - Refactored internal prompt-in-flight tracking from boolean flag to counter to properly handle nested prompt operations
33
+ - Moved llms.txt endpoint discovery to fallback strategy when rendered page content is low quality, prioritizing page-specific content over site-wide files
34
+ - Enhanced llms.txt endpoint detection to scope candidates to the requested URL path, searching section-specific files before site-wide ones
35
+
36
+ ## [13.9.6] - 2026-03-08
37
+
38
+ ### Added
39
+
40
+ - Added `glob` parameter to `ast_grep` and `ast_edit` tools for additional glob filtering relative to the `path` parameter
41
+ - Added `combineSearchGlobs` utility function to merge glob patterns from `path` and `glob` parameters
42
+
43
+ ### Changed
44
+
45
+ - Renamed `patterns` parameter to `pat` in `ast_grep` tool for consistency
46
+ - Renamed `selector` parameter to `sel` in `ast_grep` and `ast_edit` tools for brevity
47
+ - Updated tool documentation with expanded guidance on AST pattern syntax, metavariable usage, and contextual matching strategies
48
+ - Updated `grep` tool to combine glob patterns from `path` and `glob` parameters instead of throwing an error when both are provided
49
+
5
50
  ## [13.9.4] - 2026-03-07
6
51
  ### Added
7
52
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "13.9.5",
4
+ "version": "13.9.11",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -41,12 +41,12 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@mozilla/readability": "^0.6",
44
- "@oh-my-pi/omp-stats": "13.9.5",
45
- "@oh-my-pi/pi-agent-core": "13.9.5",
46
- "@oh-my-pi/pi-ai": "13.9.5",
47
- "@oh-my-pi/pi-natives": "13.9.5",
48
- "@oh-my-pi/pi-tui": "13.9.5",
49
- "@oh-my-pi/pi-utils": "13.9.5",
44
+ "@oh-my-pi/omp-stats": "13.9.11",
45
+ "@oh-my-pi/pi-agent-core": "13.9.11",
46
+ "@oh-my-pi/pi-ai": "13.9.11",
47
+ "@oh-my-pi/pi-natives": "13.9.11",
48
+ "@oh-my-pi/pi-tui": "13.9.11",
49
+ "@oh-my-pi/pi-utils": "13.9.11",
50
50
  "@sinclair/typebox": "^0.34",
51
51
  "@xterm/headless": "^6.0",
52
52
  "ajv": "^8.18",
@@ -478,7 +478,7 @@ export class ToolExecutionComponent extends Container {
478
478
  },
479
479
  this.#renderState,
480
480
  theme,
481
- this.#args, // Pass args for tools that need them
481
+ this.#getCallArgsForRender(),
482
482
  );
483
483
  if (resultComponent) {
484
484
  this.#contentBox.addChild(ensureInvalidate(resultComponent));
@@ -560,10 +560,14 @@ export class ToolExecutionComponent extends Container {
560
560
  return Math.max(1, Math.min(maxSeconds, value));
561
561
  };
562
562
 
563
- if (this.#toolName === "bash" && this.#result) {
564
- // Pass raw output and expanded state - renderer handles width-aware truncation
565
- const output = this.#getTextOutput().trimEnd();
566
- context.output = output;
563
+ if (this.#toolName === "bash") {
564
+ // Bash needs render context even before a result exists. The renderer uses the pending-call args
565
+ // plus this context to keep the inline command preview visible while tool-call JSON is still streaming.
566
+ if (this.#result) {
567
+ // Pass raw output and expanded state - renderer handles width-aware truncation
568
+ const output = this.#getTextOutput().trimEnd();
569
+ context.output = output;
570
+ }
567
571
  context.expanded = this.#expanded;
568
572
  context.previewLines = BASH_DEFAULT_PREVIEW_LINES;
569
573
  context.timeout = normalizeTimeoutSeconds(this.#args?.timeout, 3600);
@@ -184,13 +184,19 @@ export class EventController {
184
184
  continue;
185
185
  }
186
186
 
187
+ // Preserve the raw partial JSON for renderers that need to surface fields before the JSON object closes.
188
+ // Bash uses this to show inline env assignments during streaming instead of popping them in at completion.
189
+ const renderArgs =
190
+ "partialJson" in content
191
+ ? { ...content.arguments, __partialJson: content.partialJson }
192
+ : content.arguments;
187
193
  if (!this.ctx.pendingTools.has(content.id)) {
188
194
  this.#resetReadGroup();
189
195
  this.ctx.chatContainer.addChild(new Text("", 0, 0));
190
196
  const tool = this.ctx.session.getToolByName(content.name);
191
197
  const component = new ToolExecutionComponent(
192
198
  content.name,
193
- content.arguments,
199
+ renderArgs,
194
200
  {
195
201
  showImages: settings.get("terminal.showImages"),
196
202
  editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
@@ -206,7 +212,7 @@ export class EventController {
206
212
  } else {
207
213
  const component = this.ctx.pendingTools.get(content.id);
208
214
  if (component) {
209
- component.updateArgs(content.arguments, content.id);
215
+ component.updateArgs(renderArgs, content.id);
210
216
  }
211
217
  }
212
218
  }
@@ -272,9 +272,13 @@ export class UiHelpers {
272
272
 
273
273
  readGroup = null;
274
274
  const tool = this.ctx.session.getToolByName(content.name);
275
+ const renderArgs =
276
+ "partialJson" in content
277
+ ? { ...content.arguments, __partialJson: content.partialJson }
278
+ : content.arguments;
275
279
  const component = new ToolExecutionComponent(
276
280
  content.name,
277
- content.arguments,
281
+ renderArgs,
278
282
  {
279
283
  showImages: settings.get("terminal.showImages"),
280
284
  editFuzzyThreshold: settings.get("edit.fuzzyThreshold"),
@@ -99,6 +99,7 @@ Given a task, you rapidly investigate the codebase and return structured finding
99
99
  <directives>
100
100
  - You **MUST** use tools for broad pattern matching / code search as much as possible.
101
101
  - You **SHOULD** invoke tools in parallel when possible—this is a short investigation, and you are supposed to finish in a few seconds.
102
+ - If a search returns empty results, you **MUST** try at least one alternate strategy (different pattern, broader path, or AST search) before concluding the target doesn't exist.
102
103
  </directives>
103
104
 
104
105
  <thoroughness>
@@ -111,6 +111,8 @@ Before acting, determine what kind of question this is:
111
111
  - If you discover undocumented behavior or gotchas, you **MUST** populate `caveats`.
112
112
  - When local `node_modules` has the package, you **SHOULD** prefer it over cloning — it reflects the version the project actually uses.
113
113
  - You **SHOULD** use `web_search` to find the canonical repo URL and to check for known issues, but the definitive answer **MUST** come from reading source code.
114
+ - If a search or lookup returns empty or unexpectedly few results, you **MUST** try at least 2 fallback strategies (broader query, alternate path, different source) before concluding nothing exists.
115
+ - If the package is absent from local `node_modules` and cloning fails, you **MUST** fall back to `web_search` for official API documentation before reporting failure.
114
116
  </directives>
115
117
 
116
118
  <critical>
@@ -0,0 +1,46 @@
1
+ <critical>
2
+ Write a comprehensive handoff document for another instance of yourself.
3
+ The handoff **MUST** be sufficient for seamless continuation without access to this conversation.
4
+ Output ONLY the handoff document. No preamble, no commentary, no wrapper text.
5
+ </critical>
6
+
7
+ <instruction>
8
+ Capture exact technical state, not abstractions.
9
+ Include concrete file paths, symbol names, commands run, test results, observed failures, decisions made, and any partial work that materially affects the next step.
10
+ </instruction>
11
+
12
+ <output>
13
+ Use exactly this structure:
14
+
15
+ ## Goal
16
+ [What the user is trying to accomplish]
17
+
18
+ ## Constraints & Preferences
19
+ - [Any constraints, preferences, or requirements mentioned]
20
+
21
+ ## Progress
22
+ ### Done
23
+ - [x] [Completed tasks with specifics]
24
+
25
+ ### In Progress
26
+ - [ ] [Current work if any]
27
+
28
+ ### Pending
29
+ - [ ] [Tasks mentioned but not started]
30
+
31
+ ## Key Decisions
32
+ - **[Decision]**: [Rationale]
33
+
34
+ ## Critical Context
35
+ - [Code snippets, file paths, function/type names, error messages, or data essential to continue]
36
+ - [Repository state if relevant]
37
+
38
+ ## Next Steps
39
+ 1. [What should happen next]
40
+ </output>
41
+
42
+ {{#if additionalFocus}}
43
+ <instruction>
44
+ Additional focus: {{additionalFocus}}
45
+ </instruction>
46
+ {{/if}}
@@ -29,13 +29,7 @@ Your result **MUST** match this TypeScript interface:
29
29
  {{/if}}
30
30
 
31
31
  {{SECTION_SEPERATOR "Giving Up"}}
32
- If you cannot complete the assignment, you **MUST** call `submit_result` exactly once with `result.error` describing what you tried and the exact blocker.
32
+ Giving up is a last resort. If truly blocked, you **MUST** call `submit_result` exactly once with `result.error` describing what you tried and the exact blocker.
33
+ You **MUST NOT** give up due to uncertainty, missing information obtainable via tools or repo context, or needing a design decision you can derive yourself.
33
34
 
34
- Giving up is a last resort.
35
- You **MUST NOT** give up due to uncertainty or missing information obtainable via tools or repo context.
36
- You **MUST NOT** give up due to requiring a design, you can derive that yourself, more than capable of that.
37
-
38
- Proceed with the best approach using the most reasonable option.
39
-
40
- You **MUST** keep going until this ticket is closed.
41
- This matters.
35
+ You **MUST** keep going until this ticket is closed. This matters.
@@ -313,8 +313,9 @@ Today is '{{date}}', and your work begins now. Get it right.
313
313
  - You **MUST** use the most specialized tool, **NEVER** `cat` if there's tool.bash, `rg/grep`:tool.grep, `find`:tool.find, `sed`:tool.edit…
314
314
  - Every turn **MUST** materially advance the deliverable.
315
315
  - You **MUST** default to action. You **MUST NOT** ask for confirmation to continue work. If you hit an error, you **MUST** fix it. If you know the next step, you **MUST** take it. The user will intervene if needed.
316
- - You **MUST NOT** make speculative edits before understanding the surrounding design.
317
316
  - You **MUST** default to informed action. You **MUST NOT** ask for confirmation to continue work. If you hit an error, you **MUST** fix it. If you know the next step, you **MUST** take it. The user will intervene if needed.
317
+ - You **MUST NOT** make speculative edits before understanding the surrounding design.
318
+ - You **MUST NOT** stop calling tools to save round-trips when the task is incomplete. Completeness beats efficiency.
318
319
  - You **MUST NOT** ask when the answer may be obtained from available tools or repo context/files.
319
320
  - You **MUST** verify the effect. When a task involves a behavioral change, you **MUST** confirm the change is observable before yielding: run the specific test, command, or scenario that covers your change.
320
321
  </critical>
@@ -2,10 +2,16 @@ Performs structural AST-aware rewrites via native ast-grep.
2
2
 
3
3
  <instruction>
4
4
  - Use for codemods and structural rewrites where plain text replace is unsafe
5
- - Narrow scope with `path` before replacing (`path` accepts files, directories, or glob patterns)
6
- - Default to language-scoped rewrites in mixed repositories: set `lang` and keep `path` narrow
7
- - Treat parse issues as a scoping signal: tighten `path`/`lang` before retrying
5
+ - Narrow scope with `path` before replacing (`path` accepts files, directories, or glob patterns; use `glob` for an additional filter relative to `path`)
6
+ - Default to language-scoped rewrites in mixed repositories: set `lang` and keep `path`/`glob` narrow
7
+ - Treat parse issues as a scoping or pattern-shape signal: tighten `path`/`lang`, or rewrite the pattern into valid syntax before retrying
8
8
  - Metavariables captured in each rewrite pattern (`$A`, `$$$ARGS`) are substituted into that entry's rewrite template
9
+ - For variadic captures, use `$$$NAME` (not `$$NAME`)
10
+ - Rewrite patterns must parse as valid AST for the target language; if a method or declaration does not parse standalone, wrap it in valid context or switch to a contextual `sel`
11
+ - When using contextual `sel`, the match and replacement target the selected node, not the outer wrapper you used to make the pattern parse
12
+ - For TypeScript declarations and methods, prefer patterns that tolerate annotations you do not care about, e.g. `async function $NAME($$$ARGS): $_ { $$$BODY }` or `class $_ { method($$$ARGS): $_ { $$$BODY } }`
13
+ - Metavariables must be the sole content of an AST node; partial-text metavariables like `prefix$VAR` or `"hello $NAME"` do NOT work in patterns or rewrites
14
+ - To delete matched code, use an empty `out` string: `{"pat":"console.log($$$)","out":""}`
9
15
  - Each matched rewrite is a 1:1 structural substitution; you cannot split one capture into multiple nodes or merge multiple captures into one node
10
16
  </instruction>
11
17
 
@@ -17,14 +23,23 @@ Performs structural AST-aware rewrites via native ast-grep.
17
23
  <examples>
18
24
  - Rename a call site across a directory:
19
25
  `{"ops":[{"pat":"oldApi($$$ARGS)","out":"newApi($$$ARGS)"}],"lang":"typescript","path":"src/"}`
20
- - Multi-op codemod:
21
- `{"ops":[{"pat":"require($A)","out":"import $A"},{"pat":"module.exports = $E","out":"export default $E"}],"lang":"javascript","path":"src/"}`
26
+ - Delete all matching calls (empty `out` removes the matched node):
27
+ `{"ops":[{"pat":"console.log($$$ARGS)","out":""}],"lang":"typescript","path":"src/"}`
28
+ - Rewrite an import source path:
29
+ `{"ops":[{"pat":"import { $$$IMPORTS } from \"old-package\"","out":"import { $$$IMPORTS } from \"new-package\""}],"lang":"typescript","path":"src/"}`
30
+ - Modernize to optional chaining (same metavariable enforces identity):
31
+ `{"ops":[{"pat":"$A && $A()","out":"$A?.()"}],"lang":"typescript","path":"src/"}`
22
32
  - Swap two arguments using captures:
23
33
  `{"ops":[{"pat":"assertEqual($A, $B)","out":"assertEqual($B, $A)"}],"lang":"typescript","path":"tests/"}`
34
+ - Rename a TypeScript function declaration while tolerating any return type annotation:
35
+ `{"ops":[{"pat":"async function fetchData($$$ARGS): $_ { $$$BODY }","out":"async function loadData($$$ARGS): $_ { $$$BODY }"}],"sel":"function_declaration","lang":"typescript","path":"src/api.ts"}`
36
+ - Convert Python print calls to logging:
37
+ `{"ops":[{"pat":"print($$$ARGS)","out":"logger.info($$$ARGS)"}],"lang":"python","path":"src/"}`
24
38
  </examples>
25
39
 
26
40
  <critical>
27
41
  - `ops` **MUST** contain at least one concrete `{ pat, out }` entry
28
42
  - If the path pattern spans multiple languages, set `lang` explicitly for deterministic rewrites
43
+ - Parse issues mean the rewrite request is malformed or mis-scoped; do not assume a clean no-op until the pattern parses successfully
29
44
  - For one-off local text edits, prefer the Edit tool instead of AST edit
30
45
  </critical>
@@ -2,14 +2,22 @@ Performs structural code search using AST matching via native ast-grep.
2
2
 
3
3
  <instruction>
4
4
  - Use this when syntax shape matters more than raw text (calls, declarations, specific language constructs)
5
- - Prefer a precise `path` scope to keep results targeted and deterministic (`path` accepts files, directories, or glob patterns)
6
- - Default to language-scoped search in mixed repositories: pair `path` glob + explicit `lang` to avoid parse-noise from non-source files
7
- - `patterns` is required and must include at least one non-empty AST pattern; `lang` is optional (`lang` is inferred per file extension when omitted)
5
+ - Prefer a precise `path` scope to keep results targeted and deterministic (`path` accepts files, directories, or glob patterns; use `glob` for an additional filter relative to `path`)
6
+ - Default to language-scoped search in mixed repositories: pair `path` + `glob` + explicit `lang` to avoid parse-noise from non-source files
7
+ - `pat` is required and must include at least one non-empty AST pattern; `lang` is optional (`lang` is inferred per file extension when omitted)
8
8
  - Multiple patterns run in one native pass; results are merged and then `offset`/`limit` are applied to the combined match set
9
- - Use `selector` only for contextual pattern mode; otherwise provide direct patterns
9
+ - Use `sel` only for contextual pattern mode; otherwise provide direct patterns
10
+ - In contextual pattern mode, results are returned for the selected node (`sel`), not the outer wrapper used to make the pattern parse
10
11
  - For variadic arguments/fields, use `$$$NAME` (not `$$NAME`)
12
+ - Patterns must parse as a single valid AST node for the target language; if a bare pattern fails, wrap it in valid context or use `sel`
11
13
  - Patterns match AST structure, not text — whitespace/formatting differences are ignored
12
14
  - When the same metavariable appears multiple times, all occurrences must match identical code
15
+ - For TypeScript declarations and methods, prefer shapes that tolerate annotations you do not care about, e.g. `async function $NAME($$$ARGS): $_ { $$$BODY }` or `class $_ { method($$$ARGS): $_ { $$$BODY } }` instead of omitting the return type entirely
16
+ - Metavariables must be the sole content of an AST node; partial-text metavariables like `prefix$VAR`, `"hello $NAME"`, or `a $OP b` do NOT work — match the whole node instead
17
+ - `$$$` captures are lazy (non-greedy): they stop when the next element in the pattern can match; place the most specific node after `$$$` to control where capture ends
18
+ - `$_` is a non-capturing wildcard (matches any single node without binding); use it when you need to tolerate a node but don't need its value
19
+ - Search the right declaration form before concluding absence: top-level function, class method, and variable-assigned function are different AST shapes
20
+ - If you only need to prove a symbol exists, prefer a looser contextual search such as `pat: ["executeBash"]` with `sel: "identifier"`
13
21
  </instruction>
14
22
 
15
23
  <output>
@@ -19,16 +27,25 @@ Performs structural code search using AST matching via native ast-grep.
19
27
 
20
28
  <examples>
21
29
  - Find all console logging calls in one pass (multi-pattern, scoped):
22
- `{"patterns":["console.log($$$)","console.error($$$)"],"lang":"typescript","path":"src/"}`
23
- - Capture and inspect metavariable bindings from a pattern:
24
- `{"patterns":["require($MOD)"],"lang":"javascript","path":"src/"}`
30
+ `{"pat":["console.log($$$)","console.error($$$)"],"lang":"typescript","path":"src/"}`
31
+ - Find all named imports from a specific package:
32
+ `{"pat":["import { $$$IMPORTS } from \"react\""],"lang":"typescript","path":"src/"}`
33
+ - Match arrow functions assigned to a const (different AST shape than function declarations):
34
+ `{"pat":["const $NAME = ($$$ARGS) => $BODY"],"lang":"typescript","path":"src/utils/"}`
35
+ - Match any method call on an object using wildcard `$_` (ignores method name):
36
+ `{"pat":["logger.$_($$$ARGS)"],"lang":"typescript","path":"src/"}`
25
37
  - Contextual pattern with selector — match only the identifier `foo`, not the whole call:
26
- `{"patterns":["foo()"],"selector":"identifier","lang":"typescript","path":"src/utils.ts"}`
38
+ `{"pat":["foo()"],"sel":"identifier","lang":"typescript","path":"src/utils.ts"}`
39
+ - Match a TypeScript function declaration without caring about its exact return type:
40
+ `{"pat":["async function processItems($$$ARGS): $_ { $$$BODY }"],"sel":"function_declaration","lang":"typescript","path":"src/worker.ts"}`
41
+ - Loosest existence check for a symbol in one file:
42
+ `{"pat":["processItems"],"sel":"identifier","lang":"typescript","path":"src/worker.ts"}`
27
43
  </examples>
28
44
 
29
45
  <critical>
30
- - `patterns` is required
46
+ - `pat` is required
31
47
  - Set `lang` explicitly to constrain matching when path pattern spans mixed-language trees
32
48
  - Avoid repo-root AST scans when the target is language-specific; narrow `path` first
49
+ - Treat parse issues as query failure, not evidence of absence: repair the pattern or tighten `path`/`glob`/`lang` before concluding "no matches"
33
50
  - If exploration is broad/open-ended across subsystems, use Task tool with explore subagent first
34
51
  </critical>
@@ -2,6 +2,8 @@ Executes bash command in shell session for terminal operations like git, bun, ca
2
2
 
3
3
  <instruction>
4
4
  - You **MUST** use `cwd` parameter to set working directory instead of `cd dir && …`
5
+ - Prefer `env: { NAME: "…" }` for multiline, quote-heavy, or untrusted values instead of inlining them into shell syntax; reference them from the command as `$NAME`
6
+ - Quote variable expansions like `"$NAME"` to preserve exact content and avoid shell parsing bugs
5
7
  - PTY mode is opt-in: set `pty: true` only when command expects a real terminal (for example `sudo`, `ssh` where you need input from the user); default is `false`
6
8
  - You **MUST** use `;` only when later commands should run regardless of earlier failures
7
9
  - `skill://` URIs are auto-resolved to filesystem paths before execution
@@ -89,6 +89,7 @@ import { getCurrentThemeName, theme } from "../modes/theme/theme";
89
89
  import { normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../patch";
90
90
  import type { PlanModeState } from "../plan-mode/state";
91
91
  import autoHandoffThresholdFocusPrompt from "../prompts/system/auto-handoff-threshold-focus.md" with { type: "text" };
92
+ import handoffDocumentPrompt from "../prompts/system/handoff-document.md" with { type: "text" };
92
93
  import planModeActivePrompt from "../prompts/system/plan-mode-active.md" with { type: "text" };
93
94
  import planModeReferencePrompt from "../prompts/system/plan-mode-reference.md" with { type: "text" };
94
95
  import planModeToolDecisionReminderPrompt from "../prompts/system/plan-mode-tool-decision-reminder.md" with {
@@ -260,6 +261,7 @@ export interface HandoffResult {
260
261
  interface HandoffOptions {
261
262
  autoTriggered?: boolean;
262
263
  signal?: AbortSignal;
264
+ skipPostPromptRecoveryWait?: boolean;
263
265
  }
264
266
 
265
267
  /** Internal marker for hook messages queued through the agent loop */
@@ -397,7 +399,7 @@ export class AgentSession {
397
399
  #streamingEditAbortTriggered = false;
398
400
  #streamingEditCheckedLineCounts = new Map<string, number>();
399
401
  #streamingEditFileCache = new Map<string, string>();
400
- #promptInFlight = false;
402
+ #promptInFlightCount = 0;
401
403
  #obfuscator: SecretObfuscator | undefined;
402
404
  #pendingActionStore: PendingActionStore | undefined;
403
405
  #checkpointState: CheckpointState | undefined = undefined;
@@ -1560,7 +1562,7 @@ export class AgentSession {
1560
1562
 
1561
1563
  /** Whether agent is currently streaming a response */
1562
1564
  get isStreaming(): boolean {
1563
- return this.agent.state.isStreaming || this.#promptInFlight;
1565
+ return this.agent.state.isStreaming || this.#promptInFlightCount > 0;
1564
1566
  }
1565
1567
 
1566
1568
  /** Wait until streaming and deferred recovery work are fully settled. */
@@ -1999,7 +2001,7 @@ export class AgentSession {
1999
2001
  skipPostPromptRecoveryWait?: boolean;
2000
2002
  },
2001
2003
  ): Promise<void> {
2002
- this.#promptInFlight = true;
2004
+ this.#promptInFlightCount++;
2003
2005
  const generation = this.#promptGeneration;
2004
2006
  try {
2005
2007
  // Flush any pending bash messages before the new prompt
@@ -2109,7 +2111,7 @@ export class AgentSession {
2109
2111
  await this.#waitForPostPromptRecovery();
2110
2112
  }
2111
2113
  } finally {
2112
- this.#promptInFlight = false;
2114
+ this.#promptInFlightCount = Math.max(0, this.#promptInFlightCount - 1);
2113
2115
  }
2114
2116
  }
2115
2117
 
@@ -2508,11 +2510,10 @@ export class AgentSession {
2508
2510
  this.#cancelPostPromptTasks();
2509
2511
  this.agent.abort();
2510
2512
  await this.agent.waitForIdle();
2511
- // Clear promptInFlight: waitForIdle resolves when the agent loop's finally
2512
- // block runs (#resolveRunningPrompt), but #promptWithMessage's finally
2513
- // (#promptInFlight = false) fires on a later microtask. Without this,
2514
- // isStreaming stays true and a subsequent prompt() throws.
2515
- this.#promptInFlight = false;
2513
+ // Clear prompt-in-flight state: waitForIdle resolves when the agent loop's finally
2514
+ // block runs, but nested prompt setup/finalizers may still be unwinding. Without this,
2515
+ // a subsequent prompt() can incorrectly observe the session as busy after an abort.
2516
+ this.#promptInFlightCount = 0;
2516
2517
  }
2517
2518
 
2518
2519
  /**
@@ -3194,41 +3195,9 @@ export class AgentSession {
3194
3195
  }
3195
3196
 
3196
3197
  // Build the handoff prompt
3197
- let handoffPrompt = `Write a comprehensive handoff document that will allow another instance of yourself to seamlessly continue this work. The document should capture everything needed to resume without access to this conversation.
3198
-
3199
- Use this format:
3200
-
3201
- ## Goal
3202
- [What the user is trying to accomplish]
3203
-
3204
- ## Constraints & Preferences
3205
- - [Any constraints, preferences, or requirements mentioned]
3206
-
3207
- ## Progress
3208
- ### Done
3209
- - [x] [Completed tasks with specifics]
3210
-
3211
- ### In Progress
3212
- - [ ] [Current work if any]
3213
-
3214
- ### Pending
3215
- - [ ] [Tasks mentioned but not started]
3216
-
3217
- ## Key Decisions
3218
- - **[Decision]**: [Rationale]
3219
-
3220
- ## Critical Context
3221
- - [Code snippets, file paths, error messages, or data essential to continue]
3222
- - [Repository state if relevant]
3223
-
3224
- ## Next Steps
3225
- 1. [What should happen next]
3226
-
3227
- Be thorough - include exact file paths, function names, error messages, and technical details. Output ONLY the handoff document, no other text.`;
3228
-
3229
- if (customInstructions) {
3230
- handoffPrompt += `\n\nAdditional focus: ${customInstructions}`;
3231
- }
3198
+ const handoffPrompt = renderPromptTemplate(handoffDocumentPrompt, {
3199
+ additionalFocus: customInstructions,
3200
+ });
3232
3201
 
3233
3202
  // Create a promise that resolves when the agent completes
3234
3203
  let handoffText: string | undefined;
@@ -3273,11 +3242,16 @@ Be thorough - include exact file paths, function names, error messages, and tech
3273
3242
  if (handoffSignal.aborted) {
3274
3243
  throw new Error("Handoff cancelled");
3275
3244
  }
3276
- await this.prompt(handoffPrompt, {
3277
- expandPromptTemplates: false,
3278
- synthetic: true,
3279
- skipCompactionCheck: true,
3280
- });
3245
+ await this.#promptWithMessage(
3246
+ {
3247
+ role: "developer",
3248
+ content: [{ type: "text", text: handoffPrompt }],
3249
+ attribution: "agent",
3250
+ timestamp: Date.now(),
3251
+ },
3252
+ handoffPrompt,
3253
+ { skipCompactionCheck: true, skipPostPromptRecoveryWait: options?.skipPostPromptRecoveryWait },
3254
+ );
3281
3255
  await completionPromise;
3282
3256
 
3283
3257
  if (handoffCancelled || handoffSignal.aborted) {
@@ -3730,11 +3704,22 @@ Be thorough - include exact file paths, function names, error messages, and tech
3730
3704
  /**
3731
3705
  * Internal: Run auto-compaction with events.
3732
3706
  */
3733
- async #runAutoCompaction(reason: "overflow" | "threshold", willRetry: boolean): Promise<void> {
3707
+ async #runAutoCompaction(reason: "overflow" | "threshold", willRetry: boolean, deferred = false): Promise<void> {
3734
3708
  const compactionSettings = this.settings.getGroup("compaction");
3735
-
3736
3709
  if (!compactionSettings.enabled || compactionSettings.strategy === "off") return;
3737
3710
  const generation = this.#promptGeneration;
3711
+ if (!deferred && reason !== "overflow" && compactionSettings.strategy === "handoff") {
3712
+ this.#schedulePostPromptTask(
3713
+ async signal => {
3714
+ await Promise.resolve();
3715
+ if (signal.aborted) return;
3716
+ await this.#runAutoCompaction(reason, willRetry, true);
3717
+ },
3718
+ { generation },
3719
+ );
3720
+ return;
3721
+ }
3722
+
3738
3723
  let action: "context-full" | "handoff" =
3739
3724
  compactionSettings.strategy === "handoff" && reason !== "overflow" ? "handoff" : "context-full";
3740
3725
  await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
@@ -3750,6 +3735,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
3750
3735
  const handoffResult = await this.handoff(handoffFocus, {
3751
3736
  autoTriggered: true,
3752
3737
  signal: this.#autoCompactionAbortController.signal,
3738
+ skipPostPromptRecoveryWait: true,
3753
3739
  });
3754
3740
  if (!handoffResult) {
3755
3741
  const aborted = this.#autoCompactionAbortController.signal.aborted;
@@ -4028,15 +4014,25 @@ Be thorough - include exact file paths, function names, error messages, and tech
4028
4014
  await this.#emitSessionEvent({ type: "auto_compaction_end", action, result, aborted: false, willRetry });
4029
4015
 
4030
4016
  if (!willRetry && compactionSettings.autoContinue !== false) {
4031
- await this.#promptWithMessage(
4032
- {
4033
- role: "developer",
4034
- content: [{ type: "text", text: "Continue if you have next steps." }],
4035
- attribution: "agent",
4036
- timestamp: Date.now(),
4017
+ const continuePrompt = async () => {
4018
+ await this.#promptWithMessage(
4019
+ {
4020
+ role: "developer",
4021
+ content: [{ type: "text", text: "Continue if you have next steps." }],
4022
+ attribution: "agent",
4023
+ timestamp: Date.now(),
4024
+ },
4025
+ "Continue if you have next steps.",
4026
+ { skipPostPromptRecoveryWait: true },
4027
+ );
4028
+ };
4029
+ this.#schedulePostPromptTask(
4030
+ async signal => {
4031
+ await Promise.resolve();
4032
+ if (signal.aborted) return;
4033
+ await continuePrompt();
4037
4034
  },
4038
- "Continue if you have next steps.",
4039
- { skipPostPromptRecoveryWait: true },
4035
+ { generation },
4040
4036
  );
4041
4037
  }
4042
4038
 
@@ -14,7 +14,7 @@ import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, t
14
14
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
15
15
  import type { ToolSession } from ".";
16
16
  import type { OutputMeta } from "./output-meta";
17
- import { hasGlobPathChars, parseSearchPath, resolveToCwd } from "./path-utils";
17
+ import { combineSearchGlobs, hasGlobPathChars, parseSearchPath, resolveToCwd } from "./path-utils";
18
18
  import {
19
19
  dedupeParseErrors,
20
20
  formatCount,
@@ -38,7 +38,8 @@ const astEditSchema = Type.Object({
38
38
  }),
39
39
  lang: Type.Optional(Type.String({ description: "Language override" })),
40
40
  path: Type.Optional(Type.String({ description: "File, directory, or glob pattern to rewrite (default: cwd)" })),
41
- selector: Type.Optional(Type.String({ description: "Optional selector for contextual pattern mode" })),
41
+ glob: Type.Optional(Type.String({ description: "Optional glob filter relative to path" })),
42
+ sel: Type.Optional(Type.String({ description: "Optional selector for contextual pattern mode" })),
42
43
  limit: Type.Optional(Type.Number({ description: "Max total replacements" })),
43
44
  });
44
45
 
@@ -98,7 +99,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
98
99
  const maxFiles = parseInt(process.env.PI_MAX_AST_FILES ?? "", 10) || 1000;
99
100
 
100
101
  let searchPath: string | undefined;
101
- let globFilter: string | undefined;
102
+ let globFilter = params.glob?.trim() || undefined;
102
103
  const rawPath = params.path?.trim();
103
104
  if (rawPath) {
104
105
  const internalRouter = this.session.internalRouter;
@@ -114,7 +115,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
114
115
  } else {
115
116
  const parsedPath = parseSearchPath(rawPath);
116
117
  searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
117
- globFilter = parsedPath.glob;
118
+ globFilter = combineSearchGlobs(parsedPath.glob, globFilter);
118
119
  }
119
120
  }
120
121
 
@@ -133,7 +134,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
133
134
  lang: params.lang?.trim(),
134
135
  path: resolvedSearchPath,
135
136
  glob: globFilter,
136
- selector: params.selector?.trim(),
137
+ selector: params.sel?.trim(),
137
138
  dryRun: true,
138
139
  maxReplacements,
139
140
  maxFiles,
@@ -277,7 +278,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
277
278
  lang: params.lang?.trim(),
278
279
  path: resolvedSearchPath,
279
280
  glob: globFilter,
280
- selector: params.selector?.trim(),
281
+ selector: params.sel?.trim(),
281
282
  dryRun: false,
282
283
  maxReplacements,
283
284
  maxFiles,
@@ -320,7 +321,7 @@ interface AstEditRenderArgs {
320
321
  ops?: Array<{ pat?: string; out?: string }>;
321
322
  lang?: string;
322
323
  path?: string;
323
- selector?: string;
324
+ sel?: string;
324
325
  limit?: number;
325
326
  }
326
327