@oh-my-pi/pi-coding-agent 14.5.3 → 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 (68) hide show
  1. package/CHANGELOG.md +44 -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 +103 -8
  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 +1057 -841
  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 +7 -7
  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/utils/ui-helpers.ts +31 -7
  33. package/src/prompts/agents/explore.md +1 -1
  34. package/src/prompts/agents/librarian.md +2 -2
  35. package/src/prompts/agents/plan.md +2 -2
  36. package/src/prompts/agents/reviewer.md +1 -1
  37. package/src/prompts/agents/task.md +2 -2
  38. package/src/prompts/system/plan-mode-active.md +1 -1
  39. package/src/prompts/system/system-prompt.md +34 -31
  40. package/src/prompts/tools/apply-patch.md +0 -2
  41. package/src/prompts/tools/atom.md +81 -63
  42. package/src/prompts/tools/bash.md +7 -4
  43. package/src/prompts/tools/checkpoint.md +1 -1
  44. package/src/prompts/tools/find.md +6 -1
  45. package/src/prompts/tools/hashline.md +10 -11
  46. package/src/prompts/tools/patch.md +13 -13
  47. package/src/prompts/tools/read.md +4 -4
  48. package/src/prompts/tools/replace.md +3 -3
  49. package/src/prompts/tools/{grep.md → search.md} +4 -4
  50. package/src/sdk.ts +19 -9
  51. package/src/session/agent-session.ts +65 -0
  52. package/src/system-prompt.ts +15 -5
  53. package/src/task/executor.ts +5 -0
  54. package/src/task/index.ts +10 -1
  55. package/src/tools/ast-edit.ts +4 -6
  56. package/src/tools/ast-grep.ts +4 -6
  57. package/src/tools/bash.ts +1 -1
  58. package/src/tools/file-recorder.ts +6 -6
  59. package/src/tools/find.ts +11 -13
  60. package/src/tools/index.ts +7 -7
  61. package/src/tools/path-utils.ts +31 -4
  62. package/src/tools/read.ts +12 -6
  63. package/src/tools/renderers.ts +2 -2
  64. package/src/tools/{grep.ts → search.ts} +32 -40
  65. package/src/tools/write.ts +8 -4
  66. package/src/web/search/index.ts +1 -1
  67. package/src/edit/block.ts +0 -308
  68. package/src/edit/indent.ts +0 -150
@@ -1,76 +1,94 @@
1
- Applies precise file edits using anchors (line+hash).
1
+ Your patch language is a compact, file-oriented edit format.
2
2
 
3
- <ops>
4
- Each call **MUST** have shape `{path:"a.ts",edits:[…]}`. `path` is the default file; you **MAY** override it per edit with `loc:"b.ts:160sr"`.
5
- Each edit **MUST** have exactly one `loc` and **MUST** include one or more verbs.
6
-
7
- # Locators
8
- - `"A"` targets one anchored line. `"$"` targets the whole file: `pre` = BOF, `post` = EOF, `sed` = every line.
9
- - Bracketed locators are **`splice` only** and select a balanced region around anchor `A`.
10
- - `"(A)"` = block body. `"[A]"` = whole block/node.
11
- - `"[A"` / `"(A"` = tail after/including anchor, closer excluded.
12
- - `"A]"` / `"A)"` = head through/before anchor, opener excluded.
13
- - Anchor bracketed forms on a body line of the intended block, not the opener line.
14
- - Do not use bracketed locators on files that do not currently parse.
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`).
15
5
 
16
- # Verbs
17
- - `splice:[…]` replaces the anchored line, or the bracketed region. `[]` deletes; `[""]` makes a blank line.
18
- - `pre:[…]` inserts before the anchor, or BOF with `loc:"$"`.
19
- - `post:[…]` inserts after the anchor, or EOF with `loc:"$"`.
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)
20
16
  </ops>
21
17
 
22
- <splice>
23
- Replaces the anchored line, or the bracketed region.
24
- - `[]` deletes. `[""]` leaves a blank line.
25
- - For bracketed `splice`, write body at column 0, it will be re-indented.
26
- - Do not use bracketed `splice` on broken files, or for single line edits.
27
- </splice>
28
-
29
- <sed>
30
- Use for tiny inline edits: names, operators, literals.
31
- - Keep `pat` as short as possible, it does not have to be unique.
32
- - `g:false` by default; set to replace all instead of first.
33
- </sed>
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>
34
25
 
35
- <examples>
36
- ```ts title="a.ts"
37
- {{hline 1 "const FALLBACK = \"guest\";"}}
26
+ <case file="a.ts">
27
+ {{hline 1 "const DEF = \"guest\";"}}
38
28
  {{hline 2 ""}}
39
29
  {{hline 3 "export function label(name) {"}}
40
- {{hline 4 "\tconst clean = name || FALLBACK;"}}
41
- {{hline 5 "\treturn clean.trim().toLowerCase();"}}
30
+ {{hline 4 "\tconst clean = name || DEF;"}}
31
+ {{hline 5 "\treturn clean.trim();"}}
42
32
  {{hline 6 "}"}}
43
- ```
33
+ </case>
34
+
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();
44
81
 
45
- # Single-line replacement:
46
- `{path:"a.ts",edits:[{loc:{{href 1 "const FALLBACK = \"guest\";"}},splice:["const FALLBACK = \"anonymous\";"]}]}`
47
- # Small token edit: prefer `sed`:
48
- `{path:"a.ts",edits:[{loc:{{href 5 "\treturn clean.trim().toLowerCase();"}},sed:{pat:"toLowerCase",rep:"toUpperCase"}}]}`
49
- # Insert before / after an anchor:
50
- `{path:"a.ts",edits:[{loc:{{href 5 "\treturn clean.trim().toLowerCase();"}},pre:["\tif (!clean) return FALLBACK;"],post:["\t// normalized label"]}]}`
51
- # Delete a line vs make it blank:
52
- `{path:"a.ts",edits:[{loc:{{href 2 ""}},splice:[]}]}`
53
- `{path:"a.ts",edits:[{loc:{{href 2 ""}},splice:[""]}]}`
54
- # File edges:
55
- `{path:"a.ts",edits:[{loc:"$",pre:["// Copyright (c) 2026",""]}]}`
56
- `{path:"a.ts",edits:[{loc:"$",post:["","export { FALLBACK };"]}]}`
57
- # Cross-file override:
58
- `{path:"a.ts",edits:[{loc:{{href 1 "const FALLBACK = \"guest\";" "config.ts:" ""}},splice:["const FALLBACK = \"anonymous\";"]}]}`
59
- # Body replacement: use bracketed `splice`, write body at column 0:
60
- `{path:"a.ts",edits:[{loc:{{href 4 "\tconst clean = name || FALLBACK;" "(" ")"}},splice:["if (name == null) return FALLBACK;","const clean = String(name).trim();","return clean || FALLBACK;"]}]}`
61
- # Whole function replacement: anchor on a body line:
62
- `{path:"a.ts",edits:[{loc:{{href 5 "\treturn clean.trim().toLowerCase();" "[" "]"}},splice:["export function label(name) {","\treturn String(name ?? FALLBACK).trim().toLowerCase();","}"]}]}`
63
- # WRONG: bare-anchor `splice` does not own neighboring lines:
64
- `{path:"a.ts",edits:[{loc:{{href 4 "\tconst clean = name || FALLBACK;"}},splice:["\tconst clean = String(name ?? FALLBACK).trim();","\treturn clean.toLowerCase();"]}]}`
65
- This replaces only line 4. Original line 5 still shifts down, so the function now has two returns.
66
- # RIGHT: use a body edit for that rewrite:
67
- `{path:"a.ts",edits:[{loc:{{href 4 "\tconst clean = name || FALLBACK;" "(" ")"}},splice:["const clean = String(name ?? FALLBACK).trim();","return clean.toLowerCase();"]}]}`
82
+ # Wrong: do not replace by deleting then adding
83
+ ---a.ts
84
+ -{{hrefr 5}}
85
+ +{{hrefr 5}}= return clean.trim().toUpperCase();
68
86
  </examples>
69
87
 
70
88
  <critical>
71
- - You **MUST** copy full anchors exactly from a read op (e.g. `160sr`); you **MUST NOT** send only the 2-letter suffix.
72
- - You **MUST** make the minimum exact edit; you **MUST NOT** reformat unrelated code.
73
- - A bare anchor **MUST** target one line only; you **MUST** use bracketed `splice` for balanced block rewrites.
74
- - You **MUST NOT** include unchanged adjacent lines in `splice`/`pre`/`post`; they shift and duplicate.
75
- - For bracketed `splice`, replacement braces **MUST** be balanced for the selected region.
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.
76
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>
@@ -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";
@@ -263,6 +264,10 @@ export interface AgentSessionConfig {
263
264
  obfuscator?: SecretObfuscator;
264
265
  /** Logical owner for retained Python kernels created by this session. */
265
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;
266
271
  }
267
272
 
268
273
  /** Options for AgentSession.prompt() */
@@ -478,6 +483,9 @@ export class AgentSession {
478
483
  // Drained into history (via emitExternalEvent) once the recipient becomes idle.
479
484
  #pendingBackgroundExchanges: CustomMessage[][] = [];
480
485
  #scheduledBackgroundExchangeFlush = false;
486
+ // Agent identity + registry for IRC relay forwarding to the main session UI.
487
+ #agentId: string | undefined;
488
+ #agentRegistry: AgentRegistry | undefined;
481
489
  // Extension system
482
490
  #extensionRunner: ExtensionRunner | undefined = undefined;
483
491
  #turnIndex = 0;
@@ -611,6 +619,8 @@ export class AgentSession {
611
619
  );
612
620
  this.#ttsrManager = config.ttsrManager;
613
621
  this.#obfuscator = config.obfuscator;
622
+ this.#agentId = config.agentId;
623
+ this.#agentRegistry = config.agentRegistry;
614
624
  this.agent.setAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
615
625
  const event: AgentEvent = {
616
626
  type: "message_update",
@@ -5999,6 +6009,13 @@ export class AgentSession {
5999
6009
  timestamp: incomingTimestamp,
6000
6010
  };
6001
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
+ });
6002
6019
 
6003
6020
  if (!awaitReply) {
6004
6021
  this.#queueBackgroundExchangeInjection([incomingRecord]);
@@ -6024,11 +6041,59 @@ export class AgentSession {
6024
6041
  timestamp: Date.now(),
6025
6042
  };
6026
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
+ });
6027
6051
  this.#queueBackgroundExchangeInjection([incomingRecord, replyRecord]);
6028
6052
 
6029
6053
  return { replyText };
6030
6054
  }
6031
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
+
6032
6097
  /**
6033
6098
  * Run a single ephemeral side-channel turn against this session's current
6034
6099
  * model + system prompt + history. No tools are used; the side request
@@ -384,6 +384,8 @@ export async function loadSystemPromptFiles(options: LoadContextFilesOptions = {
384
384
  export interface SystemPromptToolMetadata {
385
385
  label: string;
386
386
  description: string;
387
+ /** Tool name the model sees on the provider wire. Defaults to the internal tool name. */
388
+ wireName?: string;
387
389
  }
388
390
 
389
391
  export function buildSystemPromptToolMetadata(
@@ -394,12 +396,16 @@ export function buildSystemPromptToolMetadata(
394
396
  Array.from(tools.entries(), ([name, tool]) => {
395
397
  const toolRecord = tool as AgentTool & { label?: string; description?: string };
396
398
  const override = overrides[name];
399
+ const wireName =
400
+ override?.wireName ??
401
+ (typeof toolRecord.customWireName === "string" ? toolRecord.customWireName : undefined);
397
402
  return [
398
403
  name,
399
404
  {
400
405
  label: override?.label ?? (typeof toolRecord.label === "string" ? toolRecord.label : ""),
401
406
  description:
402
407
  override?.description ?? (typeof toolRecord.description === "string" ? toolRecord.description : ""),
408
+ wireName,
403
409
  },
404
410
  ] as const;
405
411
  }),
@@ -570,14 +576,17 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
570
576
  }
571
577
  }
572
578
 
573
- // Build tool descriptions for system prompt rendering
579
+ // Build tool descriptions for system prompt rendering.
580
+ const toolPromptNames = new Map<string, string>(toolNames.map(name => [name, tools?.get(name)?.wireName ?? name]));
581
+ const toolRefs = Object.fromEntries(toolPromptNames.entries());
574
582
  const toolInfo = toolNames.map(name => ({
575
- name,
583
+ name: toolPromptNames.get(name) ?? name,
584
+ internalName: name,
576
585
  label: tools?.get(name)?.label ?? "",
577
586
  description: tools?.get(name)?.description ?? "",
578
587
  }));
579
588
 
580
- // Filter skills to only include those with read tool
589
+ // Filter skills to only include those with read tool.
581
590
  const hasRead = tools?.has("read");
582
591
  const filteredSkills = hasRead ? skills : [];
583
592
 
@@ -589,6 +598,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
589
598
  const injectedAlwaysApplyRules = dedupeAlwaysApplyRules(alwaysApplyRules, promptSources);
590
599
 
591
600
  const environment = await logger.time("getEnvironmentInfo", getEnvironmentInfo);
601
+ const reportToolIssueToolName = toolPromptNames.get("report_tool_issue") ?? "report_tool_issue";
592
602
  const data = {
593
603
  systemPromptCustomization: effectiveSystemPromptCustomization,
594
604
  customPrompt: resolvedCustomPrompt,
@@ -596,6 +606,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
596
606
  tools: toolNames,
597
607
  toolInfo,
598
608
  repeatToolDescriptions,
609
+ toolRefs,
599
610
  environment,
600
611
  contextFiles,
601
612
  agentsMdSearch,
@@ -617,8 +628,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
617
628
 
618
629
  // When autoqa is active the report_tool_issue tool is in the tool set — nudge the agent.
619
630
  if (toolNames.includes("report_tool_issue")) {
620
- rendered +=
621
- "\n\n<critical>\nThe `report_tool_issue` tool is available for automated QA. If ANY tool you call returns output that is unexpected, incorrect, malformed, or otherwise inconsistent with what you anticipated given the tool's described behavior and your parameters, call `report_tool_issue` with the tool name and a concise description of the discrepancy. Do not hesitate to report — false positives are acceptable.\n</critical>";
631
+ rendered += `\n\n<critical>\nThe \`${reportToolIssueToolName}\` tool is available for automated QA. If ANY tool you call returns output that is unexpected, incorrect, malformed, or otherwise inconsistent with what you anticipated given the tool's described behavior and your parameters, call \`${reportToolIssueToolName}\` with the tool name and a concise description of the discrepancy. Do not hesitate to report — false positives are acceptable.\n</critical>`;
622
632
  }
623
633
 
624
634
  return rendered;