@oh-my-pi/pi-coding-agent 14.5.2 → 14.5.5

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.
Files changed (69) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/examples/extensions/plan-mode.ts +1 -1
  3. package/examples/sdk/README.md +1 -1
  4. package/package.json +7 -7
  5. package/src/config/prompt-templates.ts +104 -6
  6. package/src/config/settings-schema.ts +14 -13
  7. package/src/config/settings.ts +1 -1
  8. package/src/cursor.ts +4 -4
  9. package/src/edit/index.ts +111 -109
  10. package/src/edit/line-hash.ts +33 -3
  11. package/src/edit/modes/apply-patch.ts +6 -4
  12. package/src/edit/modes/atom.lark +27 -0
  13. package/src/edit/modes/atom.ts +1094 -642
  14. package/src/edit/modes/hashline.ts +9 -10
  15. package/src/edit/modes/patch.ts +23 -19
  16. package/src/edit/modes/replace.ts +19 -15
  17. package/src/edit/renderer.ts +65 -8
  18. package/src/edit/streaming.ts +47 -77
  19. package/src/extensibility/extensions/types.ts +11 -11
  20. package/src/extensibility/hooks/types.ts +6 -6
  21. package/src/lsp/edits.ts +8 -5
  22. package/src/lsp/index.ts +4 -4
  23. package/src/lsp/utils.ts +13 -43
  24. package/src/mcp/discoverable-tool-metadata.ts +1 -1
  25. package/src/mcp/manager.ts +3 -3
  26. package/src/mcp/tool-bridge.ts +4 -4
  27. package/src/memories/index.ts +1 -1
  28. package/src/modes/acp/acp-event-mapper.ts +1 -1
  29. package/src/modes/components/session-observer-overlay.ts +1 -1
  30. package/src/modes/components/settings-defs.ts +3 -3
  31. package/src/modes/components/tree-selector.ts +2 -2
  32. package/src/modes/controllers/event-controller.ts +12 -0
  33. package/src/modes/utils/ui-helpers.ts +31 -7
  34. package/src/prompts/agents/explore.md +1 -1
  35. package/src/prompts/agents/librarian.md +2 -2
  36. package/src/prompts/agents/plan.md +2 -2
  37. package/src/prompts/agents/reviewer.md +1 -1
  38. package/src/prompts/agents/task.md +2 -2
  39. package/src/prompts/system/plan-mode-active.md +1 -1
  40. package/src/prompts/system/system-prompt.md +34 -31
  41. package/src/prompts/tools/apply-patch.md +0 -2
  42. package/src/prompts/tools/atom.md +88 -97
  43. package/src/prompts/tools/bash.md +7 -4
  44. package/src/prompts/tools/checkpoint.md +1 -1
  45. package/src/prompts/tools/find.md +6 -1
  46. package/src/prompts/tools/hashline.md +10 -11
  47. package/src/prompts/tools/patch.md +13 -13
  48. package/src/prompts/tools/read.md +5 -5
  49. package/src/prompts/tools/replace.md +3 -3
  50. package/src/prompts/tools/{grep.md → search.md} +4 -4
  51. package/src/sdk.ts +19 -9
  52. package/src/session/agent-session.ts +69 -1
  53. package/src/system-prompt.ts +15 -5
  54. package/src/task/executor.ts +5 -0
  55. package/src/task/index.ts +10 -1
  56. package/src/tools/ast-edit.ts +27 -50
  57. package/src/tools/ast-grep.ts +22 -48
  58. package/src/tools/bash.ts +1 -1
  59. package/src/tools/file-recorder.ts +6 -6
  60. package/src/tools/find.ts +11 -13
  61. package/src/tools/grouped-file-output.ts +96 -0
  62. package/src/tools/index.ts +7 -7
  63. package/src/tools/path-utils.ts +31 -4
  64. package/src/tools/read.ts +12 -6
  65. package/src/tools/renderers.ts +2 -2
  66. package/src/tools/{grep.ts → search.ts} +43 -86
  67. package/src/tools/todo-write.ts +0 -1
  68. package/src/tools/write.ts +8 -4
  69. package/src/web/search/index.ts +1 -1
@@ -1,5 +1,3 @@
1
- ## `apply_patch`
2
-
3
1
  Use the `apply_patch` shell command to edit files.
4
2
  Your patch language is a stripped‑down, file‑oriented diff format designed to be easy to parse and safe to apply. You can think of it as a high‑level envelope:
5
3
 
@@ -1,103 +1,94 @@
1
- Applies precise file edits using full anchors from `read` output (for example `160sr`).
2
-
3
- Read the file first. Copy the full anchors exactly as shown by `read`.
4
-
5
- <operations>
6
- **Top level**: `{ path, edits: […] }` — `path` is shared by all entries. You may still override the file inside `loc` with forms like `other.ts:160sr`.
7
-
8
- Each entry has one shared locator plus one or more verbs:
9
- - `loc: "160sr"` single anchored line
10
- - `loc: "$"` whole file: `pre` prepends, `post` appends, `sed` substitutes across every line
11
- - `loc: "a.ts:160sr"` — cross-file override inside the locator
12
-
13
- Verbs:
14
- - `splice: […]`: lines are spliced in at the anchor.
15
- - `pre: […]`: prepend before the anchor (or at BOF if `loc=$`)
16
- - `post: […]`: append after the anchor (or at EOF if `loc=$`)
17
- - `sed: { pat, rep, g?, F? }` — structured find/replace on the anchor line. **Prefer this over `splice` for token-level changes**
18
- - `pat`: pattern to find (regex by default)
19
- - `rep`: replacement (regex back-refs like `$1`, `$&` available)
20
- - `g`: global replace every occurrence (default `false`; pass `true` to replace all)
21
- - `F`: literal — treat `pat` as a literal substring (no regex). Use this whenever `pat` contains `||`, `.`, `(`, `?`, `\`, etc. you mean literally.
22
- You **MUST** keep `pat` as short as possible.
23
-
24
- Combination rules:
25
- - On a single-anchor `loc`, you may combine `pre`, `splice`, and `post` in the same entry.
26
- - `splice: []` on a single-anchor `loc` deletes that line.
27
- - `splice:[""]` is **not** delete — it replaces the line with a blank line.
28
- </operations>
29
-
30
- <examples>
31
- All examples below reference the same file:
32
-
33
- ```ts title="a.ts"
34
- {{hline 1 "const tag = \"BAD\";"}}
1
+ Your patch language is a compact, file-oriented edit format.
2
+
3
+ When emitting a patch, the first non-blank line **MUST** be `---PATH`.
4
+ A Lid is the anchor emitted in read/grep etc. (line number + id, e.g. `5th`).
5
+
6
+ <ops>
7
+ ---PATH start editing PATH with cursor at EOF
8
+ !rm delete PATH
9
+ !mv X move file to X
10
+ $ move cursor to BOF
11
+ ^ move cursor to EOF
12
+ @Lid move cursor after Lid
13
+ +X insert X at the cursor; `+` alone inserts a blank line
14
+ Lid=X replace whole line with X; `Lid=` blanks it out
15
+ -Lid delete line (repeat for multi)
16
+ </ops>
17
+
18
+ <rules>
19
+ - You may have multiple `---PATH` sections to edit multiple files at once.
20
+ - Ops starting with `$` / `^` / `@Lid` do not alter lines; you must still issue an op like `+` afterwards.
21
+ - Consecutive `+X` ops insert consecutive lines.
22
+ - `Lid=X` replaces the whole line. X must be the complete new line, not a fragment.
23
+ - To rewrite multiple adjacent lines, delete each with `-Lid` then emit the new content as `+TEXT` lines. Do not stack `Lid=X` over a contiguous range — it requires the new block to match the old line count and silently corrupts the file when they differ.
24
+ </rules>
25
+
26
+ <case file="a.ts">
27
+ {{hline 1 "const DEF = \"guest\";"}}
35
28
  {{hline 2 ""}}
36
- {{hline 3 "function beta(x) {"}}
37
- {{hline 4 "\tif (x) {"}}
38
- {{hline 5 "\t\treturn parse(data) || fallback;"}}
39
- {{hline 6 "\t}"}}
40
- {{hline 7 "\treturn null;"}}
41
- {{hline 8 "}"}}
42
- ```
43
-
44
- # Replace a line with `splice`
45
- `{path:"a.ts",edits:[{loc:{{href 1 "const tag = \"BAD\";"}},splice:["const tag = \"OK\";"]}]}`
46
-
47
- # Combine `pre` + `splice` + `post` in one entry
48
- `{path:"a.ts",edits:[{loc:{{href 4 "\tif (x) {"}},pre:["\tvalidate();"],splice:["\tif (!x) {"],post:["\t\tlog();"]}]}`
29
+ {{hline 3 "export function label(name) {"}}
30
+ {{hline 4 "\tconst clean = name || DEF;"}}
31
+ {{hline 5 "\treturn clean.trim();"}}
32
+ {{hline 6 "}"}}
33
+ </case>
49
34
 
50
- # Delete a line with `splice: []`
51
- `{path:"a.ts",edits:[{loc:{{href 7 "\treturn null;"}},splice:[]}]}`
52
-
53
- # Preserve a blank line with `splice:[""]`
54
- `{path:"a.ts",edits:[{loc:{{href 2 ""}},splice:[""]}]}`
55
-
56
- # Insert before / after a line
57
- `{path:"a.ts",edits:[{loc:{{href 3 "function beta(x) {"}},pre:["function gamma() {","\tvalidate();","}",""]}]}`
58
-
59
- # Substitute one token with `sed` (regex) — preferred for token-level edits
60
- Use the smallest `pat` that uniquely identifies the change.
61
- `{path:"a.ts",edits:[{loc:{{href 5 "\t\treturn parse(data) || fallback;"}},sed:{pat:"\\|\\|",rep:"??"}}]}`
62
-
63
- # Substitute literal text — set `F:true` so `pat` is not parsed as regex
64
- `{path:"a.ts",edits:[{loc:{{href 5 "\t\treturn parse(data) || fallback;"}},sed:{pat:"data",rep:"input",F:true}}]}`
65
-
66
- # Comment out a line by capturing the whole content with a regex
67
- Use `$&` (the entire match) inside `rep` to keep the original text and prepend `// `.
68
- `{path:"a.ts",edits:[{loc:{{href 7 "\treturn null;"}},sed:{pat:".+",rep:"// $&"}}]}`
69
-
70
- # Prepend / append at file edges
71
- `{path:"a.ts",edits:[{loc:"$",pre:["// Copyright (c) 2026",""]}]}`
72
- `{path:"a.ts",edits:[{loc:"$",post:["","export const VERSION = \"1.0.0\";"]}]}`
73
-
74
- # Cross-file override inside `loc`
75
- `{path:"a.ts",edits:[{loc:"b.ts:{{href 1 "const tag = \"BAD\";"}}",splice:["const tag = \"OK\";"]}]}`
76
-
77
- # WRONG: retyping unchanged neighbors inside `splice` duplicates them
78
- `{path:"a.ts",edits:[{loc:{{href 4 "\tif (x) {"}},splice:["\tif (x && ready) {","\t\treturn parse(data) ?? fallback;","\t\t//unreachable"]}]}`
79
- The 2nd array element matches existing line 5, which is **not** overwritten, it shifts, so return statement ends up duplicated.
80
-
81
- # RIGHT: split into separate edits
82
- - `{path:"a.ts",edits:[{loc:{{href 4 "\tif (x) {"}},sed:{pat:"x",rep:"x && ready",g:false}},{loc:{{href 5 "\t\treturn parse(data) ?? fallback;"}},post:["\t\t//unreachable"]}]}`
83
- OR
84
- - `{path:"a.ts",edits:[{loc:{{href 4 "\tif (x) {"}},splice:["\tif (x && ready) {"]},{loc:{{href 5 "\t\treturn parse(data) ?? fallback;"}},splice:["\t\treturn parse(data) ?? fallback;","\t\t//unreachable"]}]}`
35
+ <examples>
36
+ # Replace line
37
+ ---a.ts
38
+ {{hrefr 5}}= return clean.trim().toUpperCase();
39
+
40
+ # Rewrite multiple adjacent lines (delete the old, insert the new)
41
+ ---a.ts
42
+ -{{hrefr 3}}
43
+ -{{hrefr 4}}
44
+ -{{hrefr 5}}
45
+ -{{hrefr 6}}
46
+ +export function label(name: string): string {
47
+ + return (name || DEF).trim().toUpperCase();
48
+ +}
49
+
50
+ # Append after
51
+ ---a.ts
52
+ @{{hrefr 4}}
53
+ + const suffix = "";
54
+
55
+ # Delete a line
56
+ ---a.ts
57
+ -{{hrefr 2}}
58
+
59
+ # Prepend and append
60
+ ---a.ts
61
+ $
62
+ +// Copyright (c) 2026
63
+ +
64
+ ^
65
+ +export { DEF };
66
+
67
+ # File ops
68
+ ---a.ts
69
+ !rm
70
+ ---b.ts
71
+ !mv a.ts
72
+
73
+ # Wrong: `@Lid=TEXT` is not replacement syntax
74
+ ---a.ts
75
+ @{{hrefr 5}}= return clean.trim().toUpperCase();
76
+
77
+ # Wrong: do not split `Lid=TEXT` across lines
78
+ ---a.ts
79
+ {{hrefr 5}}=
80
+ return clean.trim().toUpperCase();
81
+
82
+ # Wrong: do not replace by deleting then adding
83
+ ---a.ts
84
+ -{{hrefr 5}}
85
+ +{{hrefr 5}}= return clean.trim().toUpperCase();
85
86
  </examples>
86
87
 
87
88
  <critical>
88
- - Make the minimum exact edit.
89
- - Copy the full anchors exactly as shown by `read/grep` (for example `160sr`, not just `sr`).
90
- - `loc` chooses the target. Verbs describe what to do there.
91
- - On a single-anchor `loc`, you may combine `pre`, `splice`, and `post`.
92
- - `loc:"$"` operates on the whole file: `pre` prepends, `post` appends, `sed` runs across every line.
93
- - `splice: []` deletes the anchored line. `splice:[""]` preserves a blank line.
94
- - Within a single request you may submit edits in any order — the runtime applies them bottom-up so they don't shift each other. After any request that mutates a file, anchors below the mutation are stale on disk; re-read before issuing more edits to that file.
95
- - `splice` operations target the current file content only. Do not try to reference old line text after the file has changed.
96
- - For **small** in-line edits (renaming a token, flipping an operator, tweaking a literal), prefer `sed` over `splice`. The `loc` anchor already pins the line — repeating the entire line in a `splice` array invites hallucinated content. Use the smallest `pat` that uniquely identifies the change on that line; do not pad it with surrounding text just to feel safe. When `pat` contains regex metacharacters you mean literally (e.g. `||`, `.`, `(`, `?`, `\`), set `F:true` to disable regex. `g` is `false` by default — pass `g:true` to replace every occurrence. For multi-line restructuring (wrapping logic, adding new branches, inserting blocks), use `splice`/`pre`/`post` — do **not** stretch `sed` into a rewrite tool.
97
- - When you do use `splice`, re-read the anchored line first and copy it verbatim, changing only the required token(s). Anchor identity does not verify line content, so a hallucinated replacement will silently corrupt the file.
98
- - Anchors are pin points, not region markers. One anchor pins exactly one line. If your change touches N distinct source lines, that is N edits with N anchors — not one big `splice` array intended to cover the whole region. `splice` cannot "replace lines 4 through 7"; it can only splice content in at one anchor.
99
- - You **MUST NOT** include lines in `splice`/`pre`/`post` that already exist immediately adjacent to the anchor in the current file. `splice` does not overwrite the lines below — they shift down — so any neighbor you re-type in your array becomes a duplicate. If your intended replacement contains content that is already on neighboring source lines, split into multiple edits at each real change site instead of one fat `splice`.
100
- - Before issuing a multi-line `splice`, mentally diff each array element against the current file lines at and just below the anchor. Any element that matches a line within ~5 lines of the anchor will become a duplicate after the splice. If you find a match, drop that element and use a separate edit (or `pre`/`post`) at the real change point.
101
- - Text content must be literal file content with matching indentation. If the file uses tabs, use real tabs.
102
- - You **MUST NOT** use this tool to reformat or clean up unrelated code.
89
+ - Copy Lids **EXACTLY** from prior tool output. Never guess, shorten, or omit the letters.
90
+ - Only emit lines that change. Never repeat unchanged context anchors imply it.
91
+ - This is **NOT** unified diff. Never send `@@`, `-OLD` / `+NEW` pairs, or unchanged context.
92
+ - Never split `Lid=TEXT` across two physical lines.
93
+ - Never stack `Lid=X` over a contiguous range. Use `-Lid`+`+TEXT` for block rewrites.
103
94
  </critical>
@@ -29,22 +29,25 @@ Returns output and exit code.
29
29
  </output>
30
30
 
31
31
  <critical>
32
- You **MUST NOT** use bash for file operations where specialized tools exist:
32
+ You **MUST** use specialized tools instead of bash for any file, directory, or text-search operation. Do **NOT** use Bash to run commands when a relevant dedicated tool is provided — dedicated tools are faster, render diffs, respect `.gitignore`, and let the user review your work. Bash commands matching the patterns below are intercepted and blocked at runtime.
33
33
 
34
34
  |Instead of (WRONG)|Use (CORRECT)|
35
35
  |---|---|
36
36
  |`cat file`, `head -n N file`|`read(path="file", limit=N)`|
37
37
  |`cat -n file \|sed -n '50,150p'`|`read(path="file", offset=50, limit=100)`|
38
- {{#if hasGrep}}|`grep -A 20 'pat' file`|`grep(pattern="pat", path="file", post=20)`|
39
- |`grep -rn 'pat' dir/`|`grep(pattern="pat", path="dir/")`|
40
- |`rg 'pattern' dir/`|`grep(pattern="pattern", path="dir/")`|{{/if}}
38
+ {{#if hasSearch}}|`grep -A 20 'pat' file`|`search(pattern="pat", path="file", post=20)`|
39
+ |`grep -rn 'pat' dir/`|`search(pattern="pat", path="dir/")`|
40
+ |`rg 'pattern' dir/`|`search(pattern="pattern", path="dir/")`|{{/if}}
41
41
  {{#if hasFind}}|`find dir -name '*.ts'`|`find(pattern="dir/**/*.ts")`|{{/if}}
42
42
  |`ls dir/`|`read(path="dir/")`|
43
43
  |`cat <<'EOF' > file`|`write(path="file", content="…")`|
44
44
  |`sed -i 's/old/new/' file`|`edit(path="file", edits=[…])`|
45
45
  {{#if hasAstEdit}}|`sed -i 's/oldFn(/newFn(/' src/*.ts`|`ast_edit({ops:[{pat:"oldFn($$$A)", out:"newFn($$$A)"}], path:"src/"})`|{{/if}}
46
+ - You **MUST NOT** create files with `cat <<EOF`, `echo > file`, or `printf > file`. Use `write` — heredoc content cannot be cached for permission reuse, every revision triggers a fresh review, and there is no diff. This is the most-violated rule.
47
+ - You **MUST NOT** read line ranges with `sed -n 'A,Bp'`, `awk 'NR≥A && NR≤B'`, or `head | tail` pipelines. Use `read` with `offset`/`limit` (or `sel` if available).
46
48
  {{#if hasAstGrep}}- You **MUST** use `ast_grep` for structural code search instead of bash `grep`/`awk`/`perl` pipelines{{/if}}
47
49
  {{#if hasAstEdit}}- You **MUST** use `ast_edit` for structural rewrites instead of bash `sed`/`awk`/`perl` pipelines{{/if}}
48
50
  - You **MUST NOT** use `2>&1` or `2>/dev/null` — stdout and stderr are already merged
49
51
  - You **MUST NOT** use `| head -n 50` or `| tail -n 100` — use `head`/`tail` parameters instead
52
+ - If you catch yourself typing `cat`, `head`, `tail`, `less`, `more`, `ls`, `grep`, `rg`, `find`, `fd`, `sed -i`, `awk -i`, or a heredoc redirect inside a Bash call, stop and switch to the dedicated tool. There is no scenario where bash is preferable for these operations.
50
53
  </critical>
@@ -1,6 +1,6 @@
1
1
  Creates a context checkpoint before exploratory work so you can later rewind and keep only a concise report.
2
2
 
3
- Use this when you need to investigate with many intermediate tool calls (read/grep/find/lsp/etc.) and want to minimize context cost afterward.
3
+ Use this when you need to investigate with many intermediate tool calls (read/search/find/lsp/etc.) and want to minimize context cost afterward.
4
4
 
5
5
  Rules:
6
6
  - You **MUST** call `rewind` before yielding after starting a checkpoint.
@@ -14,5 +14,10 @@ Matching file paths sorted by modification time (most recent first). Truncated a
14
14
  </examples>
15
15
 
16
16
  <avoid>
17
- For open-ended searches requiring multiple rounds of globbing and grepping, you **MUST** use Task tool instead.
17
+ For open-ended searches requiring multiple rounds of globbing and searching, you **MUST** use Task tool instead.
18
18
  </avoid>
19
+
20
+ <critical>
21
+ - You **MUST** use the built-in Find tool for every file-name lookup. Do **NOT** shell out to `find`, `fd`, `locate`, `ls`, or `git ls-files` via Bash — they ignore `.gitignore`, blow past result limits, and waste tokens.
22
+ - If you catch yourself typing `find -name`, `fd`, or `ls **/*.ext` in a Bash command, stop and re-issue the lookup through the Find tool with a glob pattern instead.
23
+ </critical>
@@ -5,10 +5,9 @@ Read the file first. Copy the full anchors exactly as shown by `read`.
5
5
  <operations>
6
6
  **Top level**
7
7
  - `edits` — array of edit entries
8
- - `path` (optional) — default file path used when an entry omits its own `path`. Lets you share the path across many edits in one request.
8
+ - `path` (required) — file path for all edits in this request
9
9
 
10
- **Edit entry**: `{ path?, loc, content }`
11
- - `path` — file path (omit to fall back to the request-level `path`)
10
+ **Edit entry**: `{ loc, content }`
12
11
  - `loc` — where to apply the edit (see below)
13
12
  - `content` — replacement/inserted lines (`string[]`, one element per line; `null` to delete)
14
13
 
@@ -44,24 +43,24 @@ All examples below reference the same file:
44
43
 
45
44
  # Replace a block body
46
45
  Replace only the catch body. Do not target the shared boundary line `} catch (err) {`.
47
- `{edits:[{path:"a.ts",loc:{range:{pos:{{href 15 "\t\tconsole.error(err);"}},end:{{href 16 "\t\treturn null;"}}}},content:["\t\tif (isEnoent(err)) return null;","\t\tthrow err;"]}]}`
46
+ `{path:"a.ts",edits:[{loc:{range:{pos:{{href 15}},end:{{href 16}}}},content:["\t\tif (isEnoent(err)) return null;","\t\tthrow err;"]}]}`
48
47
  # Replace whole block including closing brace
49
- Replace `alpha`'s entire body including the closing `}`. `end` **MUST** be {{href 7 "}"}} because `content` includes `}`.
50
- `{edits:[{path:"a.ts",loc:{range:{pos:{{href 6 "\tlog();"}},end:{{href 7 "}"}}}},content:["\tvalidate();","\tlog();","}"]}]}`
51
- **Wrong**: `end: {{href 6 "\tlog();"}}` — line 7 (`}`) survives AND content emits `}`, producing two closing braces.
48
+ Replace `alpha`'s entire body including the closing `}`. `end` **MUST** be {{href 7}} because `content` includes `}`.
49
+ `{path:"a.ts",edits:[{loc:{range:{pos:{{href 6}},end:{{href 7}}}},content:["\tvalidate();","\tlog();","}"]}]}`
50
+ **Wrong**: `end: {{href 6}}` — line 7 (`}`) survives AND content emits `}`, producing two closing braces.
52
51
  # Replace one line
53
52
  Single-line replace uses `pos == end`.
54
- `{edits:[{path:"a.ts",loc:{range:{pos:{{href 2 "const timeout = 5000;"}},end:{{href 2 "const timeout = 5000;"}}}},content:["const timeout = 30_000;"]}]}`
53
+ `{path:"a.ts",edits:[{loc:{range:{pos:{{href 2}},end:{{href 2}}}},content:["const timeout = 30_000;"]}]}`
55
54
  # Delete a range
56
- `{edits:[{path:"a.ts",loc:{range:{pos:{{href 10 "\t// TODO: remove after migration"}},end:{{href 11 "\tlegacy();"}}}},content:null}]}`
55
+ `{path:"a.ts",edits:[{loc:{range:{pos:{{href 10}},end:{{href 11}}}},content:null}]}`
57
56
  # Insert before a sibling
58
57
  When adding a sibling declaration, prefer `prepend` on the next declaration.
59
- `{edits:[{path:"a.ts",loc:{prepend:{{href 9 "function beta() {"}}},content:["function gamma() {","\tvalidate();","}",""]}]}`
58
+ `{path:"a.ts",edits:[{loc:{prepend:{{href 9}}},content:["function gamma() {","\tvalidate();","}",""]}]}`
60
59
  </examples>
61
60
 
62
61
  <critical>
63
62
  - Make the minimum exact edit.
64
- - Copy the full anchors exactly as shown by `read/grep` (for example `160sr`, not just `sr`).
63
+ - Copy the full anchors exactly as shown by `read/search` (for example `160sr`, not just `sr`).
65
64
  - `range` requires both `pos` and `end`.
66
65
  - **Closing-delimiter check**: when your replacement `content` ends with a closing delimiter (`}`, `*/`, `)`, `]`), compare it against the line immediately after `end` in the file. If they match, extend `end` to include that line — otherwise the original delimiter survives and `content` adds a second copy.
67
66
  - For a range, replace only the body or the whole range — don't split range boundaries.
@@ -18,19 +18,19 @@ When editing structured blocks (nested braces, tags, indented regions), include
18
18
 
19
19
  <parameters>
20
20
  ```ts
21
- // Input is { edits: Entry[] } where Entry is one of:
21
+ // Input is { path: string, edits: Entry[] }. `path` is required and applies to every entry.
22
22
  type Entry =
23
- // Diff is one or more hunks in the same file.
23
+ // Diff is one or more hunks for the top-level path.
24
24
  // - Each hunk begins with "@@" (anchor optional).
25
25
  // - Each hunk body only has lines starting with ' ' | '+' | '-'.
26
26
  // - Each hunk includes at least one change (+ or -).
27
- | { path: string, op: "update", diff: string }
27
+ | { op: "update", diff: string }
28
28
  // Diff is full file content, no prefixes.
29
- | { path: string, op: "create", diff: string }
29
+ | { op: "create", diff: string }
30
30
  // No diff for delete.
31
- | { path: string, op: "delete" }
32
- // New path for update+move.
33
- | { path: string, op: "update", rename: string, diff: string }
31
+ | { op: "delete" }
32
+ // New path for update+move from the top-level path.
33
+ | { op: "update", rename: string, diff: string }
34
34
  ```
35
35
  </parameters>
36
36
 
@@ -52,15 +52,15 @@ Returns success/failure; on failure, error message indicates:
52
52
 
53
53
  <examples>
54
54
  # Create
55
- `edit {"edits":[{"path":"hello.txt","op":"create","diff":"Hello\n"}]}`
55
+ `edit {"path":"hello.txt","edits":[{"op":"create","diff":"Hello\n"}]}`
56
56
  # Update
57
- `edit {"edits":[{"path":"src/app.py","op":"update","diff":"@@ def greet():\n def greet():\n-print('Hi')\n+print('Hello')\n"}]}`
57
+ `edit {"path":"src/app.py","edits":[{"op":"update","diff":"@@ def greet():\n def greet():\n-print('Hi')\n+print('Hello')\n"}]}`
58
58
  # Rename
59
- `edit {"edits":[{"path":"src/app.py","op":"update","rename":"src/main.py","diff":"@@\n …\n"}]}`
59
+ `edit {"path":"src/app.py","edits":[{"op":"update","rename":"src/main.py","diff":"@@\n …\n"}]}`
60
60
  # Delete
61
- `edit {"edits":[{"path":"obsolete.txt","op":"delete"}]}`
62
- # Multi-file
63
- `edit {"edits":[{"path":"src/types.ts","op":"update","diff":"@@\n-old\n+new\n"},{"path":"src/index.ts","op":"update","diff":"@@\n-old\n+new\n"}]}`
61
+ `edit {"path":"obsolete.txt","edits":[{"op":"delete"}]}`
62
+ # Multiple entries
63
+ All entries in one call apply to the top-level `path`; use separate calls for different files.
64
64
  </examples>
65
65
 
66
66
  <avoid>
@@ -23,7 +23,7 @@ The `read` tool is multi-purpose and more capable than it looks — inspects fil
23
23
  # Filesystem
24
24
  - Reading a directory path returns a list of dirents.
25
25
  {{#if IS_HASHLINE_MODE}}
26
- - Reading a file returns lines prefixed with anchors (line # .. hash .. | .. line content): `41th|def alpha():`
26
+ - Reading a file returns lines prefixed with anchors (line+hash): `41th|def alpha():`
27
27
  {{else}}
28
28
  {{#if IS_LINE_NUMBER_MODE}}
29
29
  - Reading a file returns lines prefixed with line numbers: `41|def alpha():`
@@ -50,9 +50,9 @@ Extracts content from web pages, GitHub issues/PRs, Stack Overflow, Wikipedia, R
50
50
  </instruction>
51
51
 
52
52
  <critical>
53
- - You **MUST** use `read` for all file, directory, archive, and URL reads; never cat/head/ls/tar/unzip/curl, etc.
54
- You **MUST** prefer `read` over a browser/puppeteer tool for fetching URL content; only use a browser if this method fails to deliver reasonable content.
55
- - You **MUST** always include the `path` parameter.
56
- - For specific line ranges, use `sel`.
53
+ - You **MUST** use `read` for every file, directory, archive, and URL read. `cat`, `head`, `tail`, `less`, `more`, `ls`, `tar`, `unzip`, `curl`, and `wget` are **FORBIDDEN** for inspection — any such Bash call is a bug, regardless of how short or convenient it looks.
54
+ - You **MUST** prefer `read` over a browser/puppeteer tool for fetching URL content; only use a browser if `read` fails to deliver reasonable content.
55
+ - You **MUST** always include the `path` parameter — never call `read` with an empty argument object `{}`.
56
+ - For specific line ranges, use `sel` (e.g. `sel="50-200"`, `sel="50+150"`) — do **NOT** reach for `sed -n`, `awk NR`, or `head`/`tail` pipelines.
57
57
  - You **MAY** use `sel` with URL reads; the tool paginates cached fetched output.
58
58
  </critical>
@@ -1,9 +1,9 @@
1
1
  Performs string replacements in files with fuzzy whitespace matching.
2
2
 
3
3
  <instruction>
4
- - You **MUST** use the smallest edit that uniquely identifies the change
5
- - If `old_text` not unique, you **MUST** expand to include more context or use `all: true` to replace all occurrences
6
- - Fuzzy matching handles minor whitespace/indentation differences automatically
4
+ - Params **MUST** be `{ path, edits }`; `path` is required at the top level and applies to every replacement
5
+ - You **MUST** use the smallest `old_text` that uniquely identifies the change
6
+ - If `old_text` is not unique, you **MUST** expand it with more context or use `all: true` to replace all occurrences
7
7
  - You **SHOULD** prefer editing existing files over creating new ones
8
8
  </instruction>
9
9
 
@@ -17,8 +17,8 @@ Searches files using powerful regex matching.
17
17
  </output>
18
18
 
19
19
  <critical>
20
- - You **MUST** use the built-in Grep tool for any content search. Do **NOT** shell out to `grep`, `rg`, `ripgrep`, `ag`, `ack`, `git grep`, `awk`, `sed`-for-search, or any other CLI search via Bash — even for a single match, even "just to check quickly", even piped through other commands.
21
- - Bash `grep`/`rg` loses `.gitignore` semantics, bypasses result limits, and wastes tokens. The Grep tool is faster, structured, and already wired into the workspace — there is no scenario where Bash search is preferable.
22
- - If you catch yourself typing `grep`, `rg`, or `| grep` in a Bash command, stop and re-issue the search through the Grep tool instead.
23
- - If the search is open-ended, requiring multiple rounds, you **MUST** use the Task tool with the explore subagent instead of chaining Grep calls yourself.
20
+ - You **MUST** use the built-in `search` tool for any content search. Do **NOT** shell out to `grep`, `rg`, `ripgrep`, `ag`, `ack`, `git grep`, `awk`, `sed`-for-search, or any other CLI search via Bash — even for a single match, even "just to check quickly", even piped through other commands.
21
+ - Bash `grep`/`rg` loses `.gitignore` semantics, bypasses result limits, and wastes tokens. The `search` tool is faster, structured, and already wired into the workspace — there is no scenario where Bash search is preferable.
22
+ - If you catch yourself typing `grep`, `rg`, or `| grep` in a Bash command, stop and re-issue the lookup through the `search` tool instead.
23
+ - If the search is open-ended, requiring multiple rounds, you **MUST** use the Task tool with the explore subagent instead of chaining `search` calls yourself.
24
24
  </critical>
package/src/sdk.ts CHANGED
@@ -66,6 +66,7 @@ import {
66
66
  InternalUrlRouter,
67
67
  JobsProtocolHandler,
68
68
  LocalProtocolHandler,
69
+ type LocalProtocolOptions,
69
70
  McpProtocolHandler,
70
71
  MemoryProtocolHandler,
71
72
  PiProtocolHandler,
@@ -111,7 +112,6 @@ import {
111
112
  discoverStartupLspServers,
112
113
  EditTool,
113
114
  FindTool,
114
- GrepTool,
115
115
  getSearchTools,
116
116
  HIDDEN_TOOLS,
117
117
  isSearchProviderPreference,
@@ -121,10 +121,12 @@ import {
121
121
  ReadTool,
122
122
  ResolveTool,
123
123
  renderSearchToolBm25Description,
124
+ SearchTool,
124
125
  setPreferredImageProvider,
125
126
  setPreferredSearchProvider,
126
127
  type Tool,
127
128
  type ToolSession,
129
+ WebSearchTool,
128
130
  WriteTool,
129
131
  warmupLspServers,
130
132
  } from "./tools";
@@ -226,6 +228,9 @@ export interface CreateAgentSessionOptions {
226
228
  /** Session manager. Default: session stored under the configured agentDir sessions root */
227
229
  sessionManager?: SessionManager;
228
230
 
231
+ /** Override local:// protocol options for subagent local:// sharing. Default: uses the session's own artifacts dir and session ID. */
232
+ localProtocolOptions?: LocalProtocolOptions;
233
+
229
234
  /** Settings instance. Default: Settings.init({ cwd, agentDir }) */
230
235
  settings?: Settings;
231
236
 
@@ -271,13 +276,14 @@ export {
271
276
  createTools,
272
277
  EditTool,
273
278
  FindTool,
274
- GrepTool,
275
279
  HIDDEN_TOOLS,
276
280
  loadSshTool,
277
281
  PythonTool,
278
282
  ReadTool,
279
283
  ResolveTool,
284
+ SearchTool,
280
285
  type ToolSession,
286
+ WebSearchTool,
281
287
  WriteTool,
282
288
  };
283
289
 
@@ -996,10 +1002,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
996
1002
  }),
997
1003
  );
998
1004
  internalRouter.register(
999
- new LocalProtocolHandler({
1000
- getArtifactsDir,
1001
- getSessionId: () => sessionManager.getSessionId(),
1002
- }),
1005
+ new LocalProtocolHandler(
1006
+ options.localProtocolOptions ?? {
1007
+ getArtifactsDir,
1008
+ getSessionId: () => sessionManager.getSessionId(),
1009
+ },
1010
+ ),
1003
1011
  );
1004
1012
  internalRouter.register(
1005
1013
  new SkillProtocolHandler({
@@ -1386,7 +1394,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1386
1394
  ? requestedActiveToolNames
1387
1395
  : requestedActiveToolNames.filter(name => !defaultInactiveToolNames.has(name));
1388
1396
  const explicitlyRequestedMCPToolNames = options.toolNames
1389
- ? requestedActiveToolNames.filter(name => name.startsWith("mcp_"))
1397
+ ? requestedActiveToolNames.filter(name => name.startsWith("mcp__"))
1390
1398
  : [];
1391
1399
  const discoveryDefaultServers = new Set(
1392
1400
  (settings.get("mcp.discoveryDefaultServers") ?? []).map(serverName => serverName.trim()).filter(Boolean),
@@ -1412,7 +1420,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1412
1420
  : [...new Set([...restoredSelectedMCPToolNames, ...defaultSelectedMCPToolNames])];
1413
1421
  initialToolNames = [
1414
1422
  ...new Set([
1415
- ...initialRequestedActiveToolNames.filter(name => !name.startsWith("mcp_")),
1423
+ ...initialRequestedActiveToolNames.filter(name => !name.startsWith("mcp__")),
1416
1424
  ...initialSelectedMCPToolNames,
1417
1425
  ]),
1418
1426
  ];
@@ -1424,7 +1432,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1424
1432
  ...registeredTools.filter(t => !t.definition.defaultInactive).map(t => t.definition.name),
1425
1433
  ];
1426
1434
  for (const name of alwaysInclude) {
1427
- if (mcpDiscoveryEnabled && name.startsWith("mcp_")) {
1435
+ if (mcpDiscoveryEnabled && name.startsWith("mcp__")) {
1428
1436
  continue;
1429
1437
  }
1430
1438
  if (toolRegistry.has(name) && !initialToolNames.includes(name)) {
@@ -1601,6 +1609,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1601
1609
  ttsrManager,
1602
1610
  obfuscator,
1603
1611
  asyncJobManager,
1612
+ agentId: resolvedAgentId,
1613
+ agentRegistry,
1604
1614
  });
1605
1615
  hasSession = true;
1606
1616
 
@@ -130,6 +130,7 @@ import planModeToolDecisionReminderPrompt from "../prompts/system/plan-mode-tool
130
130
  type: "text",
131
131
  };
132
132
  import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { type: "text" };
133
+ import { type AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
133
134
  import { deobfuscateSessionContext, type SecretObfuscator } from "../secrets/obfuscator";
134
135
  import { resolveThinkingLevelForModel, toReasoningEffort } from "../thinking";
135
136
  import { assertEditableFile } from "../tools/auto-generated-guard";
@@ -196,7 +197,8 @@ export type AgentSessionEvent =
196
197
  | { type: "retry_fallback_succeeded"; model: string; role: string }
197
198
  | { type: "ttsr_triggered"; rules: Rule[] }
198
199
  | { type: "todo_reminder"; todos: TodoItem[]; attempt: number; maxAttempts: number }
199
- | { type: "todo_auto_clear" };
200
+ | { type: "todo_auto_clear" }
201
+ | { type: "irc_message"; message: CustomMessage };
200
202
 
201
203
  /** Listener function for agent session events */
202
204
  export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
@@ -262,6 +264,10 @@ export interface AgentSessionConfig {
262
264
  obfuscator?: SecretObfuscator;
263
265
  /** Logical owner for retained Python kernels created by this session. */
264
266
  pythonKernelOwnerId?: string;
267
+ /** Agent identity (registry id like "0-Main" or "3-Alice") used for IRC routing. */
268
+ agentId?: string;
269
+ /** Shared agent registry (for forwarding IRC observations to the main session UI). */
270
+ agentRegistry?: AgentRegistry;
265
271
  }
266
272
 
267
273
  /** Options for AgentSession.prompt() */
@@ -477,6 +483,9 @@ export class AgentSession {
477
483
  // Drained into history (via emitExternalEvent) once the recipient becomes idle.
478
484
  #pendingBackgroundExchanges: CustomMessage[][] = [];
479
485
  #scheduledBackgroundExchangeFlush = false;
486
+ // Agent identity + registry for IRC relay forwarding to the main session UI.
487
+ #agentId: string | undefined;
488
+ #agentRegistry: AgentRegistry | undefined;
480
489
  // Extension system
481
490
  #extensionRunner: ExtensionRunner | undefined = undefined;
482
491
  #turnIndex = 0;
@@ -610,6 +619,8 @@ export class AgentSession {
610
619
  );
611
620
  this.#ttsrManager = config.ttsrManager;
612
621
  this.#obfuscator = config.obfuscator;
622
+ this.#agentId = config.agentId;
623
+ this.#agentRegistry = config.agentRegistry;
613
624
  this.agent.setAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
614
625
  const event: AgentEvent = {
615
626
  type: "message_update",
@@ -5997,6 +6008,14 @@ export class AgentSession {
5997
6008
  attribution: "agent",
5998
6009
  timestamp: incomingTimestamp,
5999
6010
  };
6011
+ void this.#emitSessionEvent({ type: "irc_message", message: incomingRecord });
6012
+ this.#forwardIrcRelayToMain({
6013
+ from: args.from,
6014
+ to: this.#agentId ?? "?",
6015
+ body: args.message,
6016
+ kind: "message",
6017
+ timestamp: incomingTimestamp,
6018
+ });
6000
6019
 
6001
6020
  if (!awaitReply) {
6002
6021
  this.#queueBackgroundExchangeInjection([incomingRecord]);
@@ -6021,11 +6040,60 @@ export class AgentSession {
6021
6040
  attribution: "agent",
6022
6041
  timestamp: Date.now(),
6023
6042
  };
6043
+ void this.#emitSessionEvent({ type: "irc_message", message: replyRecord });
6044
+ this.#forwardIrcRelayToMain({
6045
+ from: this.#agentId ?? "?",
6046
+ to: args.from,
6047
+ body: replyText,
6048
+ kind: "reply",
6049
+ timestamp: replyRecord.timestamp,
6050
+ });
6024
6051
  this.#queueBackgroundExchangeInjection([incomingRecord, replyRecord]);
6025
6052
 
6026
6053
  return { replyText };
6027
6054
  }
6028
6055
 
6056
+ /**
6057
+ * Forward an IRC exchange observation to the main agent's session UI so the
6058
+ * user can see every IRC conversation in the main transcript, even when the
6059
+ * main agent is not a direct participant. The relay record is display-only:
6060
+ * it is NOT injected into the main agent's persisted history.
6061
+ */
6062
+ #forwardIrcRelayToMain(args: {
6063
+ from: string;
6064
+ to: string;
6065
+ body: string;
6066
+ kind: "message" | "reply";
6067
+ timestamp: number;
6068
+ }): void {
6069
+ const registry = this.#agentRegistry;
6070
+ if (!registry) return;
6071
+ // If this session is the main agent, the local emit already reached the main UI.
6072
+ if (this.#agentId === MAIN_AGENT_ID) return;
6073
+ const mainRef = registry.get(MAIN_AGENT_ID);
6074
+ const mainSession = mainRef?.session;
6075
+ if (!mainSession || mainSession === this) return;
6076
+ const arrow = args.kind === "reply" ? "\u2192 (auto)" : "\u2192";
6077
+ const relayRecord: CustomMessage = {
6078
+ role: "custom",
6079
+ customType: "irc:relay",
6080
+ content: `[IRC \`${args.from}\` ${arrow} \`${args.to}\`]\n\n${args.body}`,
6081
+ display: true,
6082
+ details: { from: args.from, to: args.to, body: args.body, kind: args.kind },
6083
+ attribution: "agent",
6084
+ timestamp: args.timestamp,
6085
+ };
6086
+ mainSession.emitIrcRelayObservation(relayRecord);
6087
+ }
6088
+
6089
+ /**
6090
+ * Emit an IRC relay observation event on this session for UI rendering only.
6091
+ * Does not persist the record to history. Public so other sessions can forward.
6092
+ */
6093
+ emitIrcRelayObservation(record: CustomMessage): void {
6094
+ void this.#emitSessionEvent({ type: "irc_message", message: record });
6095
+ }
6096
+
6029
6097
  /**
6030
6098
  * Run a single ephemeral side-channel turn against this session's current
6031
6099
  * model + system prompt + history. No tools are used; the side request