@oh-my-pi/pi-coding-agent 14.4.1 → 14.4.3

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 (62) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/package.json +7 -7
  3. package/src/cli.ts +0 -1
  4. package/src/config/prompt-templates.ts +0 -30
  5. package/src/config/settings-schema.ts +26 -36
  6. package/src/config/settings.ts +1 -1
  7. package/src/edit/index.ts +1 -53
  8. package/src/edit/line-hash.ts +0 -53
  9. package/src/edit/modes/atom.ts +82 -47
  10. package/src/edit/modes/hashline.ts +6 -8
  11. package/src/edit/renderer.ts +6 -8
  12. package/src/edit/streaming.ts +90 -114
  13. package/src/export/html/template.generated.ts +1 -1
  14. package/src/export/html/template.js +10 -15
  15. package/src/internal-urls/docs-index.generated.ts +1 -2
  16. package/src/modes/components/settings-defs.ts +0 -5
  17. package/src/modes/components/tool-execution.ts +2 -5
  18. package/src/modes/controllers/btw-controller.ts +17 -105
  19. package/src/modes/controllers/todo-command-controller.ts +537 -0
  20. package/src/modes/interactive-mode.ts +35 -9
  21. package/src/modes/types.ts +2 -0
  22. package/src/modes/utils/ui-helpers.ts +17 -0
  23. package/src/prompts/system/irc-incoming.md +8 -0
  24. package/src/prompts/system/subagent-system-prompt.md +8 -0
  25. package/src/prompts/tools/ast-grep.md +1 -1
  26. package/src/prompts/tools/atom.md +37 -26
  27. package/src/prompts/tools/bash.md +2 -2
  28. package/src/prompts/tools/grep.md +2 -5
  29. package/src/prompts/tools/irc.md +49 -0
  30. package/src/prompts/tools/job.md +11 -0
  31. package/src/prompts/tools/read.md +12 -13
  32. package/src/prompts/tools/task.md +1 -1
  33. package/src/prompts/tools/todo-write.md +14 -5
  34. package/src/registry/agent-registry.ts +139 -0
  35. package/src/sdk.ts +35 -0
  36. package/src/session/agent-session.ts +217 -5
  37. package/src/session/streaming-output.ts +1 -1
  38. package/src/slash-commands/builtin-registry.ts +24 -0
  39. package/src/task/executor.ts +14 -0
  40. package/src/tools/bash.ts +1 -1
  41. package/src/tools/fetch.ts +18 -6
  42. package/src/tools/fs-cache-invalidation.ts +0 -5
  43. package/src/tools/grep.ts +4 -124
  44. package/src/tools/index.ts +12 -6
  45. package/src/tools/irc.ts +258 -0
  46. package/src/tools/job.ts +489 -0
  47. package/src/tools/match-line-format.ts +7 -6
  48. package/src/tools/output-meta.ts +1 -1
  49. package/src/tools/read.ts +36 -126
  50. package/src/tools/renderers.ts +2 -0
  51. package/src/tools/todo-write.ts +243 -12
  52. package/src/utils/edit-mode.ts +1 -2
  53. package/src/utils/file-display-mode.ts +0 -3
  54. package/src/cli/read-cli.ts +0 -67
  55. package/src/commands/read.ts +0 -33
  56. package/src/edit/modes/chunk.ts +0 -832
  57. package/src/prompts/tools/cancel-job.md +0 -5
  58. package/src/prompts/tools/chunk-edit.md +0 -158
  59. package/src/prompts/tools/poll.md +0 -5
  60. package/src/prompts/tools/read-chunk.md +0 -73
  61. package/src/tools/cancel-job.ts +0 -95
  62. package/src/tools/poll-tool.ts +0 -173
@@ -7,23 +7,22 @@ Read the file first. Copy the full anchors exactly as shown by `read`.
7
7
 
8
8
  Each entry has one shared locator plus one or more verbs:
9
9
  - `loc: "160sr"` — single anchored line
10
- - `loc: "^"` — beginning of file (only valid with `pre`)
11
- - `loc: "$"` — end of file (only valid with `post`)
10
+ - `loc: "$"` — whole file: `pre` prepends, `post` appends, `sed` substitutes across every line
12
11
  - `loc: "a.ts:160sr"` — cross-file override inside the locator
13
12
 
14
13
  Verbs:
15
- - `set: [""]` replace the anchor line
16
- - `pre: [""]` insert before the anchor line (or at BOF when `loc:"^"`)
17
- - `post: [""]` insert after the anchor line (or at EOF when `loc:"$"`)
18
- - `sed: "s/foo/bar/"` — sed-style substitution applied to the anchor line. **Prefer this over `set` for token-level changes**
19
- Flags: `g` (all occurrences), `i` (case-insensitive), `F` (literal/fixed-string, no regex).
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: "s/foo/bar/"` — sed-style substitution applied to the anchor line. **Prefer this over `splice` for token-level changes**
18
+ Flags: `g` (all occurrences), `i` (case-insensitive), `F` (literal).
20
19
  Delimiter is whatever character follows `s`.
21
20
  You **MUST** keep the pattern as short as possible.
22
21
 
23
22
  Combination rules:
24
- - On a single-anchor `loc`, you may combine `pre`, `set`, and `post` in the same entry.
25
- - `set: []` on a single-anchor `loc` deletes that line.
26
- - `set:[""]` is **not** delete — it replaces the line with a blank line.
23
+ - On a single-anchor `loc`, you may combine `pre`, `splice`, and `post` in the same entry.
24
+ - `splice: []` on a single-anchor `loc` deletes that line.
25
+ - `splice:[""]` is **not** delete — it replaces the line with a blank line.
27
26
  </operations>
28
27
 
29
28
  <examples>
@@ -40,17 +39,17 @@ All examples below reference the same file:
40
39
  {{hline 8 "}"}}
41
40
  ```
42
41
 
43
- # Replace a line with `set`
44
- `{path:"a.ts",edits:[{loc:{{href 1 "const tag = \"BAD\";"}},set:["const tag = \"OK\";"]}]}`
42
+ # Replace a line with `splice`
43
+ `{path:"a.ts",edits:[{loc:{{href 1 "const tag = \"BAD\";"}},splice:["const tag = \"OK\";"]}]}`
45
44
 
46
- # Combine `pre` + `set` + `post` in one entry
47
- `{path:"a.ts",edits:[{loc:{{href 4 "\tif (x) {"}},pre:["\tvalidate();"],set:["\tif (!x) {"],post:["\t\tlog();"]}]}`
45
+ # Combine `pre` + `splice` + `post` in one entry
46
+ `{path:"a.ts",edits:[{loc:{{href 4 "\tif (x) {"}},pre:["\tvalidate();"],splice:["\tif (!x) {"],post:["\t\tlog();"]}]}`
48
47
 
49
- # Delete a line with `set: []`
50
- `{path:"a.ts",edits:[{loc:{{href 7 "\treturn null;"}},set:[]}]}`
48
+ # Delete a line with `splice: []`
49
+ `{path:"a.ts",edits:[{loc:{{href 7 "\treturn null;"}},splice:[]}]}`
51
50
 
52
- # Preserve a blank line with `set:[""]`
53
- `{path:"a.ts",edits:[{loc:{{href 2 ""}},set:[""]}]}`
51
+ # Preserve a blank line with `splice:[""]`
52
+ `{path:"a.ts",edits:[{loc:{{href 2 ""}},splice:[""]}]}`
54
53
 
55
54
  # Insert before / after a line
56
55
  `{path:"a.ts",edits:[{loc:{{href 3 "function beta(x) {"}},pre:["function gamma() {","\tvalidate();","}",""]}]}`
@@ -64,24 +63,36 @@ Use the `F` flag to disable regex; the delimiter can be any non-alphanumeric cha
64
63
  `{path:"a.ts",edits:[{loc:{{href 5 "\t\treturn parse(data) || fallback;"}},sed:"s|data|input|gF"}]}`
65
64
 
66
65
  # Prepend / append at file edges
67
- `{path:"a.ts",edits:[{loc:"^",pre:["// Copyright (c) 2026",""]}]}`
66
+ `{path:"a.ts",edits:[{loc:"$",pre:["// Copyright (c) 2026",""]}]}`
68
67
  `{path:"a.ts",edits:[{loc:"$",post:["","export const VERSION = \"1.0.0\";"]}]}`
69
68
 
70
69
  # Cross-file override inside `loc`
71
- `{path:"a.ts",edits:[{loc:"b.ts:{{href 1 "const tag = \"BAD\";"}}",set:["const tag = \"OK\";"]}]}`
70
+ `{path:"a.ts",edits:[{loc:"b.ts:{{href 1 "const tag = \"BAD\";"}}",splice:["const tag = \"OK\";"]}]}`
71
+
72
+ # WRONG: retyping unchanged neighbors inside `splice` duplicates them
73
+ `{path:"a.ts",edits:[{loc:{{href 4 "\tif (x) {"}},splice:["\tif (x && ready) {","\t\treturn parse(data) ?? fallback;","\t\t//unreachable"]}]}`
74
+ The 2nd array element matches existing line 5, which is **not** overwritten, it shifts, so return statement ends up duplicated.
75
+
76
+ # RIGHT: split into separate edits
77
+ - `{path:"a.ts",edits:[{loc:{{href 4 "\tif (x) {"}},sed:"s/x/x \\&\\& ready/"},{loc:{{href 5 "\t\treturn parse(data) ?? fallback;"}},post:["\t\t//unreachable"]}]}`
78
+ OR
79
+ - `{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"]}]}`
72
80
  </examples>
73
81
 
74
82
  <critical>
75
83
  - Make the minimum exact edit.
76
84
  - Copy the full anchors exactly as shown by `read/grep` (for example `160sr`, not just `sr`).
77
85
  - `loc` chooses the target. Verbs describe what to do there.
78
- - On a single-anchor `loc`, you may combine `pre`, `set`, and `post`.
79
- - `loc:"^"` only supports `pre`. `loc:"$"` only supports `post`.
80
- - `set: []` deletes the anchored line. `set:[""]` preserves a blank line.
86
+ - On a single-anchor `loc`, you may combine `pre`, `splice`, and `post`.
87
+ - `loc:"$"` operates on the whole file: `pre` prepends, `post` appends, `sed` runs across every line.
88
+ - `splice: []` deletes the anchored line. `splice:[""]` preserves a blank line.
81
89
  - 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.
82
- - `set` operations target the current file content only. Do not try to reference old line text after the file has changed.
83
- - For token-level edits, prefer `sed` over `set`. The `loc` anchor already pins the line — repeating the entire line in a `set` array invites hallucinated content. Use the smallest `sed` pattern that uniquely identifies the change on that line; do not pad it with surrounding text just to feel safe.
84
- - When you do use `set`, 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.
90
+ - `splice` operations target the current file content only. Do not try to reference old line text after the file has changed.
91
+ - 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 `sed` pattern that uniquely identifies the change on that line; do not pad it with surrounding text just to feel safe. For multi-line restructuring (wrapping logic, adding new branches, inserting blocks), use `splice`/`pre`/`post` — do **not** stretch `sed` into a rewrite tool.
92
+ - 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.
93
+ - 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.
94
+ - 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`.
95
+ - 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.
85
96
  - Text content must be literal file content with matching indentation. If the file uses tabs, use real tabs.
86
97
  - You **MUST NOT** use this tool to reformat or clean up unrelated code.
87
98
  </critical>
@@ -14,10 +14,10 @@ Executes bash command in shell session for terminal operations like git, bun, ca
14
14
  - Long-running non-PTY commands may auto-background after ~{{autoBackgroundThresholdSeconds}}s and continue as background jobs.
15
15
  {{/if}}
16
16
  {{#if asyncEnabled}}
17
- - Inspect background jobs with `read jobs://` (`read jobs://<job-id>` for detail). To wait for results, call `poll` — do NOT poll `read jobs://` in a loop or yield and hope for delivery.
17
+ - Inspect background jobs with `read jobs://` (`read jobs://<job-id>` for detail). To wait for results, call `job` (with `poll`) — do NOT poll `read jobs://` in a loop or yield and hope for delivery.
18
18
  {{else}}
19
19
  {{#if autoBackgroundEnabled}}
20
- - For auto-backgrounded jobs, inspect with `read jobs://` and call `poll` to wait — do NOT poll in a loop.
20
+ - For auto-backgrounded jobs, inspect with `read jobs://` and call `job` (with `poll`) to wait — do NOT poll in a loop.
21
21
  {{/if}}
22
22
  {{/if}}
23
23
  </instruction>
@@ -8,20 +8,17 @@ Searches files using powerful regex matching.
8
8
 
9
9
  <output>
10
10
  {{#if IS_HASHLINE_MODE}}
11
- - Text output is anchor-prefixed: `123th>content` (match) or `123th:content` (context). The 2-letter ID is a content fingerprint.
11
+ - Text output is anchor-prefixed: `*123th|content` (match) or ` 123th|content` (context, leading space). The 2-letter ID is a content fingerprint.
12
12
  {{else}}
13
13
  {{#if IS_LINE_NUMBER_MODE}}
14
14
  - Text output is line-number-prefixed
15
15
  {{/if}}
16
16
  {{/if}}
17
- {{#if IS_CHUNK_MODE}}
18
- - Text output is chunk-path-prefixed: `path:sel>123|content`
19
- {{/if}}
20
17
  </output>
21
18
 
22
19
  <critical>
23
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.
24
- - Bash `grep`/`rg` returns raw text without chunk paths, 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.
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.
25
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.
26
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.
27
24
  </critical>
@@ -0,0 +1,49 @@
1
+ Sends short text messages to other live agents in this process and receives their prose replies.
2
+
3
+ <instruction>
4
+ - The main agent is addressable as `0-Main`. Subagents reuse their task id (e.g. `0-AuthLoader`).
5
+ - `op: "list"` returns the current set of visible peers. Use it before sending if you are not sure who is live.
6
+ - `op: "send"` delivers `message` to `to`. `to` may be a specific id or `"all"` to broadcast.
7
+ - The recipient generates the reply via an ephemeral side-channel turn that uses their current model, system prompt, and history — it does **not** wait for the recipient's main loop to be free, so it is safe to IRC an agent that is currently inside a long-running tool call.
8
+ - The exchange (incoming question + auto-reply) is queued for injection into the recipient's persisted history; the recipient sees it on its next turn and can follow up if needed.
9
+ </instruction>
10
+
11
+ <when_to_use>
12
+ You **SHOULD** reach for `irc` proactively when continuing alone is wasteful or wrong. When in doubt, prefer messaging.
13
+ - **Unexpected state.** You hit something the original task did not describe — a missing file, a config that contradicts the assignment, an API behaving differently than you were told, a tool failing in a way that suggests the spec is wrong. DM `0-Main` (or the spawning agent) for guidance instead of guessing.
14
+ - **Blocked by another agent.** A peer holds the file/branch/resource you need, has already started the change you are about to make, or owns a decision you depend on. DM that peer (or broadcast to discover who) before duplicating or stepping on work.
15
+ - **Decision points outside your scope.** A genuine fork in the road that the assignment did not pre-decide (e.g. which of two viable APIs to use, whether to refactor adjacent code). Ask the requester rather than picking unilaterally.
16
+ - **Coordination opportunities.** You realize a peer's in-flight work would benefit from yours, or vice-versa.
17
+
18
+ Do **not** use `irc` for: routine progress updates, things you can verify with a tool call, or questions whose answer is already in your assignment / repo / docs.
19
+ </when_to_use>
20
+
21
+ <etiquette>
22
+ These rules apply to both sending and replying.
23
+ - **Plain prose only.** Do not send structured JSON status payloads (e.g. `{"type":"task_completed",…}`). Write a normal sentence: "Done with the auth refactor — left a TODO in `src/server/auth.ts` for the rate limiter."
24
+ - **Do not quote the message you are replying to.** The sender already saw it; the TUI already renders it. Lead with the answer.
25
+ - **Use IRC, not terminal tools, to learn about peers.** Do not `grep` artifacts, read other sessions' JSONL files, or shell-poke around to figure out what another agent is doing. DM them — they have the live answer and you do not.
26
+ - **One round-trip is enough.** Replies arrive synchronously when the recipient is reachable. Do not follow up with "did you get my message?" — they did. If `delivered` is empty or the result was `failed`, the peer is unavailable; move on or report the blocker, do not retry in a loop.
27
+ - **Stay terse.** A DM is a chat message, not a memo. One question per send when you can. Share file paths and artifacts via `local://` / `memory://` / `artifact://` URLs instead of pasting blobs.
28
+ - **Address peers by id.** Use the exact id from `op: "list"` (e.g. `0-AuthLoader`, `0-Main`). Do not invent friendly names.
29
+ - **Do not IRC for things a tool would answer.** If a `read`, `grep`, or build command would resolve the question, do that first.
30
+ - **When you receive an IRC message, answer it before continuing.** The recipient injects the question + your auto-reply into your history; address it directly, do not repeat it back to the user.
31
+ </etiquette>
32
+
33
+ <output>
34
+ - `send`: returns each recipient that received the message and any prose replies that arrived.
35
+ - `list`: returns peers and channels visible to the caller.
36
+ </output>
37
+
38
+ <examples>
39
+ # List peers
40
+ `{"op": "list"}`
41
+ # Direct message to the main agent (waits for prose reply)
42
+ `{"op": "send", "to": "0-Main", "message": "Should I prefer JWT or session cookies for the auth flow?"}`
43
+ # Unexpected state — ask the originator
44
+ `{"op": "send", "to": "0-Main", "message": "Assignment says edit src/auth/jwt.ts but the file does not exist. Is the new path src/server/auth/jwt.ts?"}`
45
+ # Blocked by a peer — ask them directly
46
+ `{"op": "send", "to": "0-AuthLoader", "message": "Are you still touching src/server/auth.ts? I need to add a 401 path; OK to proceed or should I wait?"}`
47
+ # Broadcast to discover who owns something (no replies, just informs them)
48
+ `{"op": "send", "to": "all", "message": "About to refactor src/server/middleware/*. Anyone already in there?", "awaitReply": false}`
49
+ </examples>
@@ -0,0 +1,11 @@
1
+ Manages background jobs: poll to wait for completion, cancel to stop running jobs.
2
+
3
+ You **MUST** use the `job` tool (in a loop, if necessary) instead of manually reading in a loop or issuing sleep commands.
4
+
5
+ Pass `poll` to wait for one or more background jobs to finalize. If the timeout elapses before any job changes state, it returns the current snapshot (still-running jobs and any already-completed deliveries) without erroring — call `job` again to keep waiting. Calling with no `poll` and no `cancel` waits on every running background job.
6
+
7
+ You **MUST NOT** poll the same job repeatedly without evidence of progress. Between calls, inspect `read jobs://<id>` to confirm new output or activity. If a job is stalled, has hung, or is producing nothing useful, cancel it via `cancel` and try a different approach instead of waiting indefinitely.
8
+
9
+ Pass `cancel` to stop one or more running background jobs (started via async tool execution or bash auto-backgrounding). You **SHOULD** cancel jobs that are no longer needed or stuck. You **MAY** inspect jobs first with `read jobs://` or `read jobs://<job-id>`.
10
+
11
+ `poll` and `cancel` may be combined in a single call: cancellations apply first, then polling waits on the remaining ids. When only `cancel` is provided the call returns immediately without waiting.
@@ -15,19 +15,18 @@ The `read` tool is multi-purpose and more capable than it looks — inspects fil
15
15
  |`sel` value|Behavior|
16
16
  |---|---|
17
17
  |*(omitted)*|Read full file (up to {{DEFAULT_LIMIT}} lines)|
18
- |`L50`|Read from line 50 onward (shorthand for L50 to EOF)|
19
- |`L50-L120`|Read lines 50 through 120|
20
- |`L20-L20`|Read exactly one line|
21
- |`raw`|Skip line-numbering / hashline / chunking; return file content as plain text. For URLs: untouched HTML.|
22
-
23
- Max {{DEFAULT_MAX_LINES}} lines per call.
18
+ |`50`|Read from line 50 onward|
19
+ |`50-200`|Read lines 50-200|
20
+ |`50+150`|Read 150 lines starting at line 50|
21
+ |`20+1`|Read exactly one line|
24
22
 
25
23
  # Filesystem
24
+ - Reading a directory path returns a list of dirents.
26
25
  {{#if IS_HASHLINE_MODE}}
27
- - Reading from FS returns lines prefixed with anchors: `41th|def alpha():` (line number, 2-letter ID, pipe, then content)
26
+ - Reading a file returns lines prefixed with anchors (line # .. hash .. | .. line content): `41th|def alpha():`
28
27
  {{else}}
29
28
  {{#if IS_LINE_NUMBER_MODE}}
30
- - Reading from FS returns lines prefixed with line numbers: `41:def alpha():`
29
+ - Reading a file returns lines prefixed with line numbers: `41|def alpha():`
31
30
  {{/if}}
32
31
  {{/if}}
33
32
 
@@ -47,13 +46,13 @@ For `.sqlite`, `.sqlite3`, `.db`, `.db3`:
47
46
  - `file.db?q=SELECT …` — read-only SELECT query
48
47
 
49
48
  # URLs
50
- Extracts content from web pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom feeds, JSON endpoints, PDFs at URLs, and similar text-based resources. Returns clean reader-mode text/markdown — no browser required. Use `sel="raw"` for untouched HTML; `timeout` to override the default request timeout. You **SHOULD** prefer `read` over a browser/puppeteer tool for fetching URL content; only use a browser when the page requires JS execution, authentication, or interactive actions (clicks, forms, scrolling).
49
+ Extracts content from web pages, GitHub issues/PRs, Stack Overflow, Wikipedia, Reddit, NPM, arXiv, RSS/Atom feeds, JSON endpoints, PDFs at URLs, and similar text-based resources. Returns clean reader-mode text/markdown — no browser required. Use `sel="raw"` for untouched HTML; `timeout` to override the default request timeout.
51
50
  </instruction>
52
51
 
53
52
  <critical>
54
- - You **MUST** use `read` (never bash `cat`/`head`/`tail`/`less`/`more`/`ls`/`tar`/`unzip`/`curl`/`wget`) for all file, directory, archive, and URL reads.
55
- - You **MUST NOT** reach for a browser/puppeteer tool to fetch static web content `read` handles HTML, PDFs, JSON, feeds, and docs directly. Reserve browser tools for JS-heavy pages or interactive flows.
56
- - You **MUST** always include the `path` parameter; never call with `{}`.
57
- - For specific line ranges, use `sel`: `read(path="file", sel="L50-L150")` — not `cat -n file | sed`.
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`.
58
57
  - You **MAY** use `sel` with URL reads; the tool paginates cached fetched output.
59
58
  </critical>
@@ -2,7 +2,7 @@ Launches subagents to parallelize workflows.
2
2
 
3
3
  {{#if asyncEnabled}}
4
4
  - Use `read jobs://` to inspect state; `read jobs://<job_id>` for detail.
5
- - Use the `poll` tool to wait until completion. You **MUST NOT** poll `read jobs://` in a loop.
5
+ - Use the `job` tool (with `poll`) to wait until completion. You **MUST NOT** poll `read jobs://` in a loop.
6
6
  {{/if}}
7
7
 
8
8
  {{#if defaultMode}}
@@ -23,11 +23,12 @@ Pass an object with an `ops` array:
23
23
 
24
24
  |Field|Type|When to use|
25
25
  |---|---|---|
26
- |`op`|string|Required. One of `replace`, `start`, `done`, `rm`, `drop`, `append`|
26
+ |`op`|string|Required. One of `replace`, `start`, `done`, `rm`, `drop`, `append`, `note`|
27
27
  |`task`|string|Task id for `start`, or a task target for `done` / `rm` / `drop`|
28
28
  |`phase`|string|Phase target for `done` / `rm` / `drop`, or append destination for `append`|
29
29
  |`items`|{id, label}[]|Required for `append`. If the phase does not exist, it is created at the end|
30
30
  |`phases`|Phase[]|Only for `replace`. Keeps initial phased setup available for harness bootstrap and full restructures|
31
+ |`text`|string|Required for `note`. The note text appended to `task.notes` (which is a list, joined with newlines on render)|
31
32
 
32
33
  ## Semantics
33
34
  - `start`: requires `task`; sets that task to `in_progress`
@@ -36,6 +37,7 @@ Pass an object with an `ops` array:
36
37
  - `drop`: marks one task, one phase, or all tasks abandoned
37
38
  - `append`: appends `items` to `phase`; creates the phase if missing
38
39
  - `replace`: replaces the full todo list
40
+ - `note`: append `text` as a new note attached to `task`. Notes are append-only context the user added; they only render to you when the task is `in_progress`. Other tasks display only a `+N` marker. Use this when you want to leave a follow-up reminder for yourself when you reach a later task.
39
41
 
40
42
  If `done`, `rm`, or `drop` omits both `task` and `phase`, it applies to all tasks.
41
43
 
@@ -43,6 +45,11 @@ If `done`, `rm`, or `drop` omits both `task` and `phase`, it applies to all task
43
45
  - `label`: Short label (5-10 words). What is being done, not how.
44
46
  - `replace` task `content` should stay short and specific.
45
47
 
48
+ ## Phase Anatomy
49
+ - `name`: Short, human-readable noun phrase (1-3 words). Capitalize naturally.
50
+ - Always prefix with a roman-numeral ordinal (`I.`, `II.`, `III.`, `IV.`, …) to convey ordering — e.g. `I. Foundation`, `II. Auth`, `III. Routing`. Single-phase plans use `I.` too.
51
+ - You **MUST NOT** use snake_case, `Phase1_*`, arabic numerals (`1.`), or letter prefixes (`A.`) — they render as ugly identifiers.
52
+
46
53
  ## Rules
47
54
  - Mark tasks done immediately after finishing — never defer.
48
55
  - Complete phases in order — do not skip ahead while earlier ones are pending.
@@ -59,18 +66,20 @@ Create a todo list when:
59
66
  </conditions>
60
67
 
61
68
  <examples>
62
- # Initial setup
63
- `{"ops":[{"op":"replace","phases":[{"name":"Investigation","tasks":[{"content":"Read source"},{"content":"Map callsites"}]},{"name":"Implementation","tasks":[{"content":"Apply fix"},{"content":"Run tests"}]}]}]}`
69
+ # Initial setup (multi-phase)
70
+ `{"ops":[{"op":"replace","phases":[{"name":"I. Foundation","tasks":[{"content":"Scaffold crate"},{"content":"Wire workspace"}]},{"name":"II. Auth","tasks":[{"content":"Port credential store"},{"content":"Wire OAuth providers"}]},{"name":"III. Verification","tasks":[{"content":"Run cargo test"}]}]}]}`
71
+ # Initial setup (single phase — still prefixed)
72
+ `{"ops":[{"op":"replace","phases":[{"name":"I. Implementation","tasks":[{"content":"Apply fix"},{"content":"Run tests"}]}]}]}`
64
73
  # Complete one task
65
74
  `{"ops":[{"op":"done","task":"task-2"}]}`
66
75
  # Complete a whole phase
67
- `{"ops":[{"op":"done","phase":"Implementation"}]}`
76
+ `{"ops":[{"op":"done","phase":"II. Auth"}]}`
68
77
  # Remove all tasks
69
78
  `{"ops":[{"op":"rm"}]}`
70
79
  # Drop one task
71
80
  `{"ops":[{"op":"drop","task":"task-7"}]}`
72
81
  # Append tasks to a phase
73
- `{"ops":[{"op":"append","phase":"Implementation","items":[{"id":"task-8","label":"Handle retries"},{"id":"task-9","label":"Run tests"}]}]}`
82
+ `{"ops":[{"op":"append","phase":"II. Auth","items":[{"id":"task-8","label":"Handle retries"},{"id":"task-9","label":"Run tests"}]}]}`
74
83
  </examples>
75
84
 
76
85
  <avoid>
@@ -0,0 +1,139 @@
1
+ /**
2
+ * AgentRegistry - Process-global registry of live AgentSession instances.
3
+ *
4
+ * Tracks every alive agent (the main session plus every subagent) so the
5
+ * `irc` tool can address peers by id. Sessions are registered explicitly at
6
+ * creation and removed when the owner releases them.
7
+ */
8
+
9
+ import type { AgentSession } from "../session/agent-session";
10
+
11
+ export const MAIN_AGENT_ID = "0-Main";
12
+
13
+ export type AgentStatus = "running" | "idle" | "completed" | "aborted";
14
+ export type AgentKind = "main" | "sub";
15
+
16
+ export interface AgentRef {
17
+ id: string;
18
+ displayName: string;
19
+ kind: AgentKind;
20
+ parentId?: string;
21
+ status: AgentStatus;
22
+ session: AgentSession | null;
23
+ sessionFile: string | null;
24
+ createdAt: number;
25
+ lastActivity: number;
26
+ }
27
+
28
+ export type RegistryEvent =
29
+ | { type: "registered"; ref: AgentRef }
30
+ | { type: "status_changed"; ref: AgentRef }
31
+ | { type: "removed"; ref: AgentRef };
32
+
33
+ type RegistryListener = (event: RegistryEvent) => void;
34
+
35
+ export interface RegisterInput {
36
+ id: string;
37
+ displayName: string;
38
+ kind: AgentKind;
39
+ parentId?: string;
40
+ session: AgentSession | null;
41
+ sessionFile?: string | null;
42
+ status?: AgentStatus;
43
+ }
44
+
45
+ export class AgentRegistry {
46
+ static #global: AgentRegistry | undefined;
47
+
48
+ static global(): AgentRegistry {
49
+ if (!AgentRegistry.#global) {
50
+ AgentRegistry.#global = new AgentRegistry();
51
+ }
52
+ return AgentRegistry.#global;
53
+ }
54
+
55
+ /** Reset the global registry. Test-only. */
56
+ static resetGlobalForTests(): void {
57
+ AgentRegistry.#global = new AgentRegistry();
58
+ }
59
+
60
+ readonly #refs = new Map<string, AgentRef>();
61
+ readonly #listeners = new Set<RegistryListener>();
62
+
63
+ register(input: RegisterInput): AgentRef {
64
+ const now = Date.now();
65
+ const ref: AgentRef = {
66
+ id: input.id,
67
+ displayName: input.displayName,
68
+ kind: input.kind,
69
+ parentId: input.parentId,
70
+ status: input.status ?? "running",
71
+ session: input.session,
72
+ sessionFile: input.sessionFile ?? null,
73
+ createdAt: now,
74
+ lastActivity: now,
75
+ };
76
+ this.#refs.set(ref.id, ref);
77
+ this.#emit({ type: "registered", ref });
78
+ return ref;
79
+ }
80
+
81
+ setStatus(id: string, status: AgentStatus): void {
82
+ const ref = this.#refs.get(id);
83
+ if (!ref || ref.status === status) return;
84
+ ref.status = status;
85
+ ref.lastActivity = Date.now();
86
+ this.#emit({ type: "status_changed", ref });
87
+ }
88
+
89
+ attachSession(id: string, session: AgentSession): void {
90
+ const ref = this.#refs.get(id);
91
+ if (!ref) return;
92
+ ref.session = session;
93
+ ref.lastActivity = Date.now();
94
+ }
95
+
96
+ detachSession(id: string): void {
97
+ const ref = this.#refs.get(id);
98
+ if (!ref) return;
99
+ ref.session = null;
100
+ }
101
+
102
+ unregister(id: string): void {
103
+ const ref = this.#refs.get(id);
104
+ if (!ref) return;
105
+ this.#refs.delete(id);
106
+ this.#emit({ type: "removed", ref });
107
+ }
108
+
109
+ get(id: string): AgentRef | undefined {
110
+ return this.#refs.get(id);
111
+ }
112
+
113
+ list(): AgentRef[] {
114
+ return [...this.#refs.values()];
115
+ }
116
+
117
+ /**
118
+ * Returns every alive agent (running | idle) except the caller.
119
+ * Flat namespace: every agent can see every other agent.
120
+ */
121
+ listVisibleTo(id: string): AgentRef[] {
122
+ return this.list().filter(ref => ref.id !== id && (ref.status === "running" || ref.status === "idle"));
123
+ }
124
+
125
+ onChange(listener: RegistryListener): () => void {
126
+ this.#listeners.add(listener);
127
+ return () => this.#listeners.delete(listener);
128
+ }
129
+
130
+ #emit(event: RegistryEvent): void {
131
+ for (const listener of this.#listeners) {
132
+ try {
133
+ listener(event);
134
+ } catch {
135
+ // listeners must not break the dispatch loop
136
+ }
137
+ }
138
+ }
139
+ }
package/src/sdk.ts CHANGED
@@ -83,6 +83,7 @@ import {
83
83
  } from "./mcp/discoverable-tool-metadata";
84
84
  import { buildMemoryToolDeveloperInstructions, getMemoryRoot, startMemoryStartupTask } from "./memories";
85
85
  import asyncResultTemplate from "./prompts/tools/async-result.md" with { type: "text" };
86
+ import { AgentRegistry, MAIN_AGENT_ID } from "./registry/agent-registry";
86
87
  import {
87
88
  collectEnvSecrets,
88
89
  deobfuscateSessionContext,
@@ -213,6 +214,12 @@ export interface CreateAgentSessionOptions {
213
214
  requireYieldTool?: boolean;
214
215
  /** Task recursion depth (for subagent sessions). Default: 0 */
215
216
  taskDepth?: number;
217
+ /** Pre-allocated agent identity for IRC routing. Default: "0-Main" for top-level, parentTaskPrefix-derived for sub. */
218
+ agentId?: string;
219
+ /** Display name for the agent in IRC. Default: "main" or "sub". */
220
+ agentDisplayName?: string;
221
+ /** Optional shared agent registry for IRC routing. Default: AgentRegistry.global(). */
222
+ agentRegistry?: AgentRegistry;
216
223
  /** Parent task ID prefix for nested artifact naming (e.g., "6-Extensions") */
217
224
  parentTaskPrefix?: string;
218
225
 
@@ -896,6 +903,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
896
903
  })
897
904
  : undefined;
898
905
 
906
+ const agentRegistry = options.agentRegistry ?? AgentRegistry.global();
907
+ const resolvedAgentId = options.agentId ?? options.parentTaskPrefix ?? MAIN_AGENT_ID;
908
+ const resolvedAgentDisplayName =
909
+ options.agentDisplayName ?? ((options.taskDepth ?? 0) > 0 || options.parentTaskPrefix ? "sub" : "main");
899
910
  const pythonKernelOwnerId = `agent-session:${Snowflake.next()}`;
900
911
 
901
912
  try {
@@ -929,6 +940,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
929
940
  trackPythonExecution: (execution, abortController) =>
930
941
  session ? session.trackPythonExecution(execution, abortController) : execution,
931
942
  getSessionId: () => sessionManager.getSessionId?.() ?? null,
943
+ getAgentId: () => resolvedAgentId,
944
+ agentRegistry,
932
945
  getSessionSpawns: () => options.spawns ?? "*",
933
946
  getModelString: () => (hasExplicitModel && model ? formatModelString(model) : undefined),
934
947
  getActiveModelString,
@@ -1591,6 +1604,28 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1591
1604
  });
1592
1605
  hasSession = true;
1593
1606
 
1607
+ // Register this session in the global agent registry so other agents can
1608
+ // address it via the irc tool. Wrap dispose to unregister on teardown.
1609
+ agentRegistry.register({
1610
+ id: resolvedAgentId,
1611
+ displayName: resolvedAgentDisplayName,
1612
+ kind: (options.taskDepth ?? 0) > 0 || options.parentTaskPrefix ? "sub" : "main",
1613
+ parentId: options.parentTaskPrefix,
1614
+ session,
1615
+ sessionFile: sessionManager.getSessionFile() ?? null,
1616
+ status: "running",
1617
+ });
1618
+ {
1619
+ const originalDispose = session.dispose.bind(session);
1620
+ session.dispose = async () => {
1621
+ try {
1622
+ await originalDispose();
1623
+ } finally {
1624
+ agentRegistry.unregister(resolvedAgentId);
1625
+ }
1626
+ };
1627
+ }
1628
+
1594
1629
  if (model?.api === "openai-codex-responses") {
1595
1630
  const codexModel = model;
1596
1631
  const codexTransport = getOpenAICodexTransportDetails(codexModel, {