diff --git a/Cargo.lock b/Cargo.lock index b61f52485..3e625b4af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -283,9 +283,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "bitvec" @@ -464,9 +464,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.3" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" +checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" dependencies = [ "memchr", "regex-automata", @@ -669,9 +669,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.1" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011" dependencies = [ "clap_builder", "clap_derive", @@ -679,9 +679,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" dependencies = [ "anstream", "anstyle", @@ -929,9 +929,9 @@ dependencies = [ [[package]] name = "ctor" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb22e947478ccf9dc44d8922042c677a63fbb88f2cb468521d1145816e5087cb" +checksum = "a394189d59f9befacce833f337f7b1eca5e9a91221bcdd4d28e0114d96e597b3" [[package]] name = "darling" @@ -1100,7 +1100,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2", ] @@ -1598,9 +1598,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "globset" -version = "0.4.18" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +checksum = "e47d37d2ae4464254884b60ab7071be2b876a9c35b696bd018ddcc76847309cd" dependencies = [ "aho-corasick", "bstr", @@ -1625,18 +1625,18 @@ dependencies = [ [[package]] name = "grep-matcher" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36d7b71093325ab22d780b40d7df3066ae4aebb518ba719d38c697a8228a8023" +checksum = "f9417543f4870fc8f1c8e1af870afae2431007626d9e703fce6471c468d33847" dependencies = [ "memchr", ] [[package]] name = "grep-pcre2" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "667bb90a3ee51f8f6e2aadf1d0f106ea42efca4cf28f0e71f0fdec1137d7f9b0" +checksum = "b752bb6f57dd3716e55b63a33ca05b5825c3d66cd427468874a5564af6ca365c" dependencies = [ "grep-matcher", "log", @@ -1673,9 +1673,9 @@ dependencies = [ [[package]] name = "grep-searcher" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac63295322dc48ebb20a25348147905d816318888e64f531bfc2a2bc0577dc34" +checksum = "72348823a0eafc4bc2e9051064f28b5b42cc100b571b3a35d67918d711efcbc6" dependencies = [ "bstr", "encoding_rs", @@ -2108,9 +2108,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "ignore" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2adf14691c72bcfc1058740436a35bdd3ae9c07d1a941ef00b749e9ea16aefa7" +checksum = "d4ffa3a0547a138e59ddd6fa3b7c672ed47e6ad6a3cd177984ff1116aa5ba742" dependencies = [ "crossbeam-deque", "globset", @@ -2213,7 +2213,7 @@ version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "153be1941a183ec9ccd095ddbe17a8b8d435ef6c76e9e02451b933c3999af2c8" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "inotify-sys", "libc", ] @@ -2438,7 +2438,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "libc", ] @@ -2620,7 +2620,7 @@ version = "3.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6826e5ddc15589b2d68c8ad5321c18e85d40488e93e32962f362e572669bccf6" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "ctor", "futures", "napi-build", @@ -2695,7 +2695,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cfg-if", "cfg_aliases 0.1.1", "libc", @@ -2707,7 +2707,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cfg-if", "cfg_aliases 0.2.1", "libc", @@ -2719,7 +2719,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cfg-if", "cfg_aliases 0.2.1", "libc", @@ -2762,7 +2762,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "fsevent-sys", "inotify", "kqueue", @@ -2780,7 +2780,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", ] [[package]] @@ -2852,7 +2852,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2", "objc2-core-graphics", "objc2-foundation", @@ -2864,7 +2864,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "dispatch2", "objc2", ] @@ -2875,7 +2875,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "dispatch2", "objc2", "objc2-core-foundation", @@ -2894,7 +2894,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2", "objc2-core-foundation", ] @@ -2905,7 +2905,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2", "objc2-core-foundation", ] @@ -2937,7 +2937,7 @@ version = "6.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "libc", "once_cell", "onig_sys", @@ -3264,7 +3264,7 @@ dependencies = [ [[package]] name = "pi-ast" -version = "16.5.2" +version = "17.0.1" dependencies = [ "anyhow", "ast-grep-core", @@ -3333,7 +3333,7 @@ dependencies = [ [[package]] name = "pi-iso" -version = "16.5.2" +version = "17.0.1" dependencies = [ "async-trait", "libc", @@ -3345,7 +3345,7 @@ dependencies = [ [[package]] name = "pi-natives" -version = "16.5.2" +version = "17.0.1" dependencies = [ "anyhow", "arboard", @@ -3398,7 +3398,7 @@ dependencies = [ [[package]] name = "pi-shell" -version = "16.5.2" +version = "17.0.1" dependencies = [ "anyhow", "brush-builtins", @@ -3482,7 +3482,7 @@ dependencies = [ [[package]] name = "pi-walker" -version = "16.5.2" +version = "17.0.1" dependencies = [ "dashmap", "globset", @@ -3551,7 +3551,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "crc32fast", "fdeflate", "flate2", @@ -3645,7 +3645,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25485360a54d6861439d60facef26de713b1e126bf015ec8f98239467a2b82f7" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "chrono", "flate2", "procfs-core", @@ -3658,7 +3658,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6401bf7b6af22f78b563665d15a22e9aef27775b79b149a66ca022468a4e405" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "chrono", "hex", ] @@ -3822,7 +3822,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", ] [[package]] @@ -3847,9 +3847,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -3859,9 +3859,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.15" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" dependencies = [ "aho-corasick", "memchr", @@ -3970,7 +3970,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "errno", "libc", "linux-raw-sys", @@ -4291,9 +4291,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.118" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", @@ -6007,9 +6007,9 @@ checksum = "0bb6d972f580f8223cb7052d8580aea2b7061e368cf476de32ea9457b19459ed" [[package]] name = "uuid" -version = "1.23.5" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea5fab0d6c3c01ae70085a09cb03d4c7a1d6314e2b3e075392783396d724ca0a" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ "js-sys", "wasm-bindgen", @@ -6153,7 +6153,7 @@ version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "rustix", "wayland-backend", "wayland-scanner", @@ -6165,7 +6165,7 @@ version = "0.32.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "wayland-backend", "wayland-client", "wayland-scanner", @@ -6177,7 +6177,7 @@ version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "wayland-backend", "wayland-client", "wayland-protocols", @@ -6710,9 +6710,9 @@ dependencies = [ [[package]] name = "xxhash-rust" -version = "0.8.16" +version = "0.8.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d93c89cdc2d3a63c3ec48ffe926931bdc069eafa8e4402fe6d8f790c9d1e576" +checksum = "985eec839aaf2a1270af8f4ebcf63cf9401cfd90f0902f97c28d9f104ffbde72" [[package]] name = "yaml-rust" diff --git a/Cargo.toml b/Cargo.toml index 23ac6f76c..a1a2f13ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ exclude = ["crates/vendor/brush-core", "crates/vendor/brush-builtins"] resolver = "3" [workspace.package] -version = "16.5.2" +version = "17.0.1" edition = "2024" license = "MIT" authors = ["Can Boluk"] diff --git a/README.md b/README.md index a9d03c0f5..21644dd07 100644 --- a/README.md +++ b/README.md @@ -219,7 +219,7 @@ Stealth's on by default, so pages see a normal user instead of a headless bot. T ## Whatever the task needs, _it's already in the box_. -32 tools live in the same namespace as `read` and `bash`. Pin the active set with `--tools read,edit,bash,…` and the rest stay hidden but indexed — `search_tool_bm25` pulls them back in mid-session when `tools.discoveryMode` says so. +32 tools live in the same namespace as `read` and `bash`. Pin the active set with `--tools read,edit,bash,…`; rarely used discoverable tools stay behind `xd://` devices. `read xd://` lists them, and `write xd://` runs one when `tools.xdev` is enabled. **Files & search** @@ -245,9 +245,8 @@ Stealth's on by default, so pages see a normal user instead of a headless bot. T **Coordination** - `task` — fan out subagents in parallel, optionally workspace-isolated. -- `irc` — short prose between live agents in this process. +- `hub` — message live agents, wait on or cancel background jobs, and supervise long-running processes. - `todo` — ordered mutations over the session todo list with phase tracking. -- `job` — wait on or cancel background jobs. - `ask` — structured follow-up questions for interactive runs. **Outside the box** @@ -270,12 +269,21 @@ Stealth's on by default, so pages see a normal user instead of a headless bot. T **Misc** - `resolve` — apply or discard a queued preview action. -- `search_tool_bm25` — BM25 over the hidden tool index; activates top matches mid-session. -Setting-gated, off by default: `github`, `inspect_image`, `tts`, `checkpoint`, `rewind`, `search_tool_bm25`, `retain`, `recall`, `reflect`. Flip them on once, scoped per project. +Setting-gated, off by default: `github`, `inspect_image`, `tts`, `checkpoint`, `rewind`, `retain`, `recall`, `reflect`. Flip them on once, scoped per project. [Full reference →](https://omp.sh/docs/tools) +### Prompt controls + +Three standalone, lowercase words opt a turn into specialized agent behavior: + +- `ultrathink` — request careful multi-step reasoning and the highest supported automatic thinking effort. +- `orchestrate` — run substantial independent work through parallel subagents and verify each phase. +- `workflowz` — build a deterministic multi-subagent workflow with the active `task` tool. + +They trigger only in prose, not inside code spans, fenced code blocks, XML/HTML sections, identifiers, or paths. See [Magic keywords](docs/magic-keywords.md) for exact matching rules and configuration. + ## Forty-plus providers, hundreds of models, _one /model away_. Roles route work by intent. `default` for normal turns. `smol` for cheap subagent fan-out. `slow` for deep reasoning. `plan` for plan mode. `commit` for changelogs. Override at launch with `--smol`, `--slow`, or `--plan`; cycle through the configured models for the active role with `Ctrl+P`. Swap the active model mid-session with the `/model` slash command. diff --git a/bun.lock b/bun.lock index 38a267324..eaf91dd7e 100644 --- a/bun.lock +++ b/bun.lock @@ -21,7 +21,7 @@ }, "packages/agent": { "name": "@oh-my-pi/pi-agent-core", - "version": "16.5.2", + "version": "17.0.1", "dependencies": { "@oh-my-pi/pi-ai": "catalog:", "@oh-my-pi/pi-catalog": "catalog:", @@ -39,7 +39,7 @@ }, "packages/ai": { "name": "@oh-my-pi/pi-ai", - "version": "16.5.2", + "version": "17.0.1", "dependencies": { "@bufbuild/protobuf": "catalog:", "@oh-my-pi/pi-catalog": "catalog:", @@ -55,7 +55,7 @@ }, "packages/catalog": { "name": "@oh-my-pi/pi-catalog", - "version": "16.5.2", + "version": "17.0.1", "dependencies": { "@bufbuild/protobuf": "catalog:", "@oh-my-pi/pi-utils": "catalog:", @@ -69,7 +69,7 @@ }, "packages/coding-agent": { "name": "@oh-my-pi/pi-coding-agent", - "version": "16.5.2", + "version": "17.0.1", "bin": { "omp": "src/cli.ts", }, @@ -114,6 +114,7 @@ }, "devDependencies": { "@types/bun": "catalog:", + "marked": "catalog:", }, "optionalDependencies": { "@huggingface/transformers": "catalog:", @@ -138,7 +139,7 @@ }, "packages/hashline": { "name": "@oh-my-pi/hashline", - "version": "16.5.2", + "version": "17.0.1", "dependencies": { "diff": "catalog:", "lru-cache": "catalog:", @@ -181,7 +182,7 @@ }, "packages/mnemopi": { "name": "@oh-my-pi/pi-mnemopi", - "version": "16.5.2", + "version": "17.0.1", "bin": { "mnemopi": "src/cli.ts", }, @@ -207,7 +208,7 @@ }, "packages/natives": { "name": "@oh-my-pi/pi-natives", - "version": "16.5.2", + "version": "17.0.1", "devDependencies": { "@napi-rs/cli": "catalog:", "@types/bun": "catalog:", @@ -215,7 +216,7 @@ }, "packages/snapcompact": { "name": "@oh-my-pi/snapcompact", - "version": "16.5.2", + "version": "17.0.1", "dependencies": { "@oh-my-pi/pi-ai": "catalog:", "@oh-my-pi/pi-natives": "catalog:", @@ -228,7 +229,7 @@ }, "packages/stats": { "name": "@oh-my-pi/omp-stats", - "version": "16.5.2", + "version": "17.0.1", "bin": { "omp-stats": "./src/index.ts", }, @@ -249,12 +250,13 @@ "@types/bun": "catalog:", "@types/react": "catalog:", "@types/react-dom": "catalog:", + "linkedom": "catalog:", "postcss": "catalog:", }, }, "packages/swarm-extension": { "name": "@oh-my-pi/swarm-extension", - "version": "16.5.2", + "version": "17.0.1", "bin": { "omp-swarm": "src/cli.ts", }, @@ -270,7 +272,7 @@ }, "packages/tui": { "name": "@oh-my-pi/pi-tui", - "version": "16.5.2", + "version": "17.0.1", "dependencies": { "@oh-my-pi/pi-natives": "catalog:", "@oh-my-pi/pi-utils": "catalog:", @@ -308,7 +310,7 @@ }, "packages/utils": { "name": "@oh-my-pi/pi-utils", - "version": "16.5.2", + "version": "17.0.1", "dependencies": { "@oh-my-pi/pi-natives": "catalog:", "handlebars": "catalog:", @@ -321,7 +323,7 @@ }, "packages/wire": { "name": "@oh-my-pi/pi-wire", - "version": "16.5.2", + "version": "17.0.1", "devDependencies": { "@types/bun": "catalog:", }, @@ -362,18 +364,18 @@ "@huggingface/transformers": "^4.2.0", "@mozilla/readability": "^0.6.0", "@napi-rs/cli": "3.7.2", - "@oh-my-pi/hashline": "16.5.2", - "@oh-my-pi/omp-stats": "16.5.2", - "@oh-my-pi/pi-agent-core": "16.5.2", - "@oh-my-pi/pi-ai": "16.5.2", - "@oh-my-pi/pi-catalog": "16.5.2", - "@oh-my-pi/pi-coding-agent": "16.5.2", - "@oh-my-pi/pi-mnemopi": "16.5.2", - "@oh-my-pi/pi-natives": "16.5.2", - "@oh-my-pi/pi-tui": "16.5.2", - "@oh-my-pi/pi-utils": "16.5.2", - "@oh-my-pi/pi-wire": "16.5.2", - "@oh-my-pi/snapcompact": "16.5.2", + "@oh-my-pi/hashline": "17.0.1", + "@oh-my-pi/omp-stats": "17.0.1", + "@oh-my-pi/pi-agent-core": "17.0.1", + "@oh-my-pi/pi-ai": "17.0.1", + "@oh-my-pi/pi-catalog": "17.0.1", + "@oh-my-pi/pi-coding-agent": "17.0.1", + "@oh-my-pi/pi-mnemopi": "17.0.1", + "@oh-my-pi/pi-natives": "17.0.1", + "@oh-my-pi/pi-tui": "17.0.1", + "@oh-my-pi/pi-utils": "17.0.1", + "@oh-my-pi/pi-wire": "17.0.1", + "@oh-my-pi/snapcompact": "17.0.1", "@opentelemetry/api": "^1.9.1", "@opentelemetry/context-async-hooks": "^2.7.1", "@opentelemetry/exporter-trace-otlp-proto": "^0.220.0", @@ -1358,7 +1360,7 @@ "mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], - "nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="], + "nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], @@ -1402,7 +1404,7 @@ "platform": ["platform@1.3.6", "", {}, "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="], - "postcss": ["postcss@8.5.17", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-J7EF+8X+CzRPaJPOv9Ck2wNWJvGnnl3PcNPAdGg6GTLjyVpyQ0yATMSXRFRV01BviT/9Gwuc3rjEyJbDJG9a4w=="], + "postcss": ["postcss@8.5.18", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-xdB1oSLHbz1vRWgCDalrCqEFTWzFlhqFC5tIHLMOSUIjhm3XXQ1qrFy8S/ESr1JYRRXqM3c1QFiMZUJdUTqyMQ=="], "prettier": ["prettier@3.9.5", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg=="], @@ -1672,7 +1674,7 @@ "fastembed/onnxruntime-node/onnxruntime-common": ["onnxruntime-common@1.21.0", "", {}, "sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ=="], - "fastembed/onnxruntime-node/tar": ["tar@7.5.19", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw=="], + "fastembed/onnxruntime-node/tar": ["tar@7.5.20", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-9FcyK4PA6+WbzlTM9WhQm6vB5W7cP7dUiPsv1g7YDwEQnQ1CGpK3MGlKk/ITVWMk05kHZuBhmVhiv8LZoy/PFQ=="], "htmlparser2/domutils/dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], diff --git a/crates/pi-natives/src/lib.rs b/crates/pi-natives/src/lib.rs index 64e10793b..d23ca71a2 100644 --- a/crates/pi-natives/src/lib.rs +++ b/crates/pi-natives/src/lib.rs @@ -248,7 +248,7 @@ fn create_windows_napi_tokio_runtime() -> Option { /// MUST stay in sync with `VERSION_SENTINEL_EXPORT` in /// `packages/natives/native/index.js` (which derives the name from /// `package.json#version`). -#[napi(js_name = "__piNativesV16_5_2")] +#[napi(js_name = "__piNativesV17_0_1")] pub const fn pi_natives_version_sentinel() {} /// Native module entry point: install crash diagnostics before any tool can diff --git a/crates/pi-natives/src/shell.rs b/crates/pi-natives/src/shell.rs index ccc0e07dd..4b80406f4 100644 --- a/crates/pi-natives/src/shell.rs +++ b/crates/pi-natives/src/shell.rs @@ -558,4 +558,31 @@ mod tests { .expect("shell run should return"); assert!(result.cancelled); } + + #[tokio::test(flavor = "multi_thread")] + async fn timeout_drains_pipeline_output_before_stopping_reader() { + let shell = CoreShell::new(None); + let (tx, rx) = flume::unbounded::(); + let result = shell + .run( + CoreShellRunOptions { + command: "yes x | tail -5".to_string(), + cwd: None, + env: None, + timeout_ms: Some(50), + }, + Some(tx), + CancelToken::new(Some(50)), + ) + .await + .expect("shell run"); + + let mut output = String::new(); + while let Ok(chunk) = rx.recv_async().await { + output.push_str(&chunk); + } + + assert!(result.timed_out); + assert_eq!(output.lines().filter(|line| *line == "x").count(), 5); + } } diff --git a/crates/pi-shell/src/coreutils.rs b/crates/pi-shell/src/coreutils.rs index caf7c064c..b18247e80 100644 --- a/crates/pi-shell/src/coreutils.rs +++ b/crates/pi-shell/src/coreutils.rs @@ -9,7 +9,7 @@ use std::{ collections::HashMap, - ffi::OsString, + ffi::{OsStr, OsString}, io::{self, Read, Write}, panic::catch_unwind, sync::{ @@ -18,6 +18,8 @@ use std::{ }, }; +#[cfg(unix)] +use brush_core::ShellFd; use brush_core::{ Error, builtins::{BoxFuture, ContentOptions, ContentType, Registration}, @@ -31,6 +33,34 @@ use brush_core::{ /// command name at index 0) and returns a process-style exit code. type UutilRun = fn(Vec) -> i32; +#[cfg(unix)] +fn process_substitution_fd(arg: &OsStr) -> Option { + let fd = arg.to_str()?.strip_prefix("/dev/fd/")?.parse().ok()?; + (fd > OpenFiles::STDERR_FD).then_some(fd) +} + +#[cfg(unix)] +fn materialize_process_substitution_fds( + context: &ExecutionContext<'_, SE>, + argv: &mut [OsString], +) -> Result, Error> { + use std::os::fd::AsRawFd as _; + + let mut fds = Vec::new(); + for arg in argv { + let Some(shell_fd) = process_substitution_fd(arg) else { + continue; + }; + let Some(file) = context.try_fd(shell_fd) else { + continue; + }; + let fd = file.try_borrow_as_fd()?.try_clone_to_owned()?; + *arg = OsString::from(format!("/dev/fd/{}", fd.as_raw_fd())); + fds.push(fd); + } + Ok(fds) +} + /// Drives a patched uutils utility to completion under a [`pi_uutils_ctx`] /// scope derived from the command execution context. async fn run_uutil( @@ -76,14 +106,18 @@ async fn run_uutil( // brush passes the command name as the first `CommandArg`, which is exactly // the argv[0] uutils' argument parsing expects. - let argv: Vec = args + let mut argv: Vec = args .iter() .map(|arg| OsString::from(arg.to_string())) .collect(); + #[cfg(unix)] + let process_substitution_fds = materialize_process_substitution_fds(&context, &mut argv)?; drop(context); let mut handle = tokio::task::spawn_blocking(move || { + #[cfg(unix)] + let _process_substitution_fds = process_substitution_fds; let stdin: Box = match stdin { Some(file) => Box::new(file), None => Box::new(io::empty()), diff --git a/crates/pi-shell/src/fd.rs b/crates/pi-shell/src/fd.rs index 93ffbbced..021b8856f 100644 --- a/crates/pi-shell/src/fd.rs +++ b/crates/pi-shell/src/fd.rs @@ -647,6 +647,10 @@ fn run_fd_sync( 0 } }, + // A closed downstream reader (`fd … | head`) surfaces as BrokenPipe on + // stdout writes. Real fd dies silently from SIGPIPE; mirror that with + // exit 141 (128+SIGPIPE) and no diagnostic. + Err(err) if err.kind() == io::ErrorKind::BrokenPipe => 141, Err(err) => { let _ = writeln!(stderr, "fd: {err}"); 2 @@ -1650,7 +1654,7 @@ mod tests { use brush_core::openfiles::OpenFile; use clap::Parser; - use super::{FdCli, cancel_heartbeat, search}; + use super::{FdCli, cancel_heartbeat, run_fd_sync, search}; static COUNTER: AtomicUsize = AtomicUsize::new(0); @@ -1803,4 +1807,28 @@ mod tests { assert!(err.is_empty(), "stderr should stay clean on success: {err:?}"); let _ = fs::remove_dir_all(&tree); } + + #[test] + fn broken_pipe_on_stdout_is_silent_and_exits_141() { + // Regression: `fd … | head` printed "fd: Broken pipe (os error 32)" + // when the downstream builtin closed the read end early. Real fd dies + // silently from SIGPIPE; the builtin must map BrokenPipe to exit 141 + // with no stderr diagnostic. + let tree = seeded_tree("epipe"); + let (reader, writer) = std::io::pipe().expect("pipe"); + drop(reader); // downstream reader (e.g. `head`) already exited + let mut stdout = OpenFile::from(writer); + let (stderr_capture, stderr_file) = capture_file("stderr-epipe"); + let mut stderr = OpenFile::from(stderr_file); + let cancelled = AtomicBool::new(false); + let argv = ["fd", "haystack", tree.to_str().expect("utf8 path")] + .map(std::ffi::OsString::from) + .to_vec(); + let code = run_fd_sync(argv, tree.clone(), &mut stdout, &mut stderr, &cancelled); + drop(stderr); + assert_eq!(code, 141, "BrokenPipe must map to 128+SIGPIPE"); + let err = read_all(&stderr_capture); + assert!(err.is_empty(), "stderr must stay clean on a broken pipe: {err:?}"); + let _ = fs::remove_dir_all(&tree); + } } diff --git a/crates/pi-shell/src/minimizer/filters/mod.rs b/crates/pi-shell/src/minimizer/filters/mod.rs index d68963a89..3a0e21489 100644 --- a/crates/pi-shell/src/minimizer/filters/mod.rs +++ b/crates/pi-shell/src/minimizer/filters/mod.rs @@ -376,6 +376,7 @@ fn uv_wrapper_tool<'a>(ctx: &'a MinimizerCtx<'_>) -> Option<&'a str> { /// is already a single flag token and needs no entry here. const WRAPPER_VALUE_OPTIONS: &[&str] = &[ // uv run + "--extra", "--with", "--with-requirements", "--with-editable", @@ -633,7 +634,7 @@ mod tests { #[test] fn uv_run_pytest_routes_to_python_filter() { let config = MinimizerConfig::default(); - let context = ctx("uv", Some("run"), "uv run pytest", &config); + let context = ctx("uv", Some("run"), "uv run --extra turso pytest", &config); let input = "============================= test session starts \ ==============================\ncollected 2 items\n\na.py .\nb.py \ F\n\n=================================== FAILURES \ diff --git a/crates/pi-shell/src/shell.rs b/crates/pi-shell/src/shell.rs index f2254a258..e30c98a6c 100644 --- a/crates/pi-shell/src/shell.rs +++ b/crates/pi-shell/src/shell.rs @@ -1141,11 +1141,16 @@ async fn run_shell_command_once( } } }); + // Let pipeline consumers flush output after cancellation kills their + // producers. The outer run cancellation remains bounded, and this delayed + // fallback still releases readers whose writers never close. + const CANCEL_READER_GRACE: Duration = Duration::from_millis(500); let cancel_bridge = tokio::spawn({ let cancel_token = cancel_token.clone(); let reader_cancel = reader_cancel.clone(); async move { cancel_token.cancelled().await; + time::sleep(CANCEL_READER_GRACE).await; reader_cancel.cancel(); } }); @@ -3215,6 +3220,20 @@ mod tests { path } + #[cfg(unix)] + #[tokio::test(flavor = "multi_thread")] + async fn uutils_diff_reads_process_substitution_fds() { + let (result, output) = time::timeout( + Duration::from_secs(5), + run_command_capture("diff <(echo a) <(echo b)", None, None, CancelToken::default()), + ) + .await + .expect("process substitution should not hang"); + + assert_eq!(result.exit_code, Some(1)); + assert!(output.contains("-a\n+b\n"), "diff output missing changed lines: {output:?}"); + } + #[cfg(unix)] fn printf_minimizer( settings_path: &std::path::Path, diff --git a/crates/pi-uu-grep/src/lib.rs b/crates/pi-uu-grep/src/lib.rs index 20a0d2c79..4aba22add 100644 --- a/crates/pi-uu-grep/src/lib.rs +++ b/crates/pi-uu-grep/src/lib.rs @@ -13,6 +13,7 @@ mod rg; use std::{ + borrow::Cow, ffi::{OsStr, OsString}, fs::File, io::{self, BufWriter, Read, Write}, @@ -603,6 +604,76 @@ fn escape_literal(pat: &str) -> String { out } +/// Translate GNU BRE `\|` alternation into the syntax accepted by +/// `grep-regex`, without rewriting escaped pipes inside character classes. +fn normalize_basic_alternation(pattern: &str) -> Cow<'_, str> { + let bytes = pattern.as_bytes(); + let mut output = None; + let mut copied = 0; + let mut index = 0; + let mut in_class = false; + + while index < bytes.len() { + if bytes[index] == b'\\' { + let run_start = index; + while index < bytes.len() && bytes[index] == b'\\' { + index += 1; + } + let slash_count = index - run_start; + if !in_class && slash_count % 2 == 1 && index < bytes.len() && bytes[index] == b'|' { + let normalized = output.get_or_insert_with(|| String::with_capacity(pattern.len())); + normalized.push_str(&pattern[copied..index - 1]); + normalized.push('|'); + copied = index + 1; + index += 1; + continue; + } + if slash_count % 2 == 1 && index < bytes.len() { + index += 1; + } + continue; + } + + match bytes[index] { + b'[' if !in_class => in_class = true, + b']' if in_class => in_class = false, + _ => {}, + } + index += 1; + } + + if let Some(mut normalized) = output { + normalized.push_str(&pattern[copied..]); + Cow::Owned(normalized) + } else { + Cow::Borrowed(pattern) + } +} + +fn build_default_matcher>( + builder: &RegexMatcherBuilder, + patterns: &[P], +) -> Result { + let error = match builder.build_many(patterns) { + Ok(matcher) => return Ok(matcher), + Err(error) => error, + }; + let sanitized: Vec = patterns + .iter() + .map(|pattern| { + let pattern = pattern.as_ref(); + if builder.build(pattern).is_ok() { + pattern.to_owned() + } else { + escape_literal(pattern) + } + }) + .collect(); + builder + .build_many(&sanitized) + .map_err(|_| error.to_string()) +} + /// Compile all patterns using the last-selected matcher mode. fn build_matcher( patterns: &[String], @@ -644,28 +715,18 @@ fn build_matcher( .map_err(|error| error.to_string()); } - match builder.build_many(patterns) { - Ok(matcher) => Ok(CompiledMatcher::Rust(matcher)), - Err(error) if mode == MatchMode::Default => { - // The historical builtin accepts ERE syntax by default but falls - // back to literals per malformed alternative. - let sanitized: Vec = patterns - .iter() - .map(|pattern| { - if builder.build(pattern).is_ok() { - pattern.clone() - } else { - escape_literal(pattern) - } - }) - .collect(); - builder - .build_many(&sanitized) - .map(CompiledMatcher::Rust) - .map_err(|_| error.to_string()) - }, - Err(error) => Err(error.to_string()), + if mode == MatchMode::Default { + let normalized: Vec<_> = patterns + .iter() + .map(|pattern| normalize_basic_alternation(pattern)) + .collect(); + return build_default_matcher(&builder, &normalized).map(CompiledMatcher::Rust); } + + builder + .build_many(patterns) + .map(CompiledMatcher::Rust) + .map_err(|error| error.to_string()) } /// A search sink that renders GNU-compatible records and tracks selection. @@ -1619,6 +1680,16 @@ mod tests { assert!(!stdout.contains("bar")); } + #[test] + fn default_mode_supports_gnu_basic_alternation() { + let input = "\"tools.xdev\": {}\n\"tools.toolbox\": {}\n\"tools.other\": {}\n"; + let (code, stdout, stderr) = run_grep(&["-c", r"tools.xdev\|tools.toolbox"], input); + + assert_eq!(code, 0, "{stderr}"); + assert!(stderr.is_empty(), "{stderr}"); + assert_eq!(stdout, "2\n"); + } + #[test] fn multi_pattern_keeps_valid_alternative_as_regex() { // Per-pattern fallback: valid `fo+` stays a regex while `bar)` is escaped. diff --git a/crates/vendor/uu-tail/src/tail.rs b/crates/vendor/uu-tail/src/tail.rs index 83ae6e229..4b5c596c4 100644 --- a/crates/vendor/uu-tail/src/tail.rs +++ b/crates/vendor/uu-tail/src/tail.rs @@ -633,16 +633,12 @@ fn unbounded_tail(reader: &mut BufReader, settings: &Settings) -> UR }, _ => {}, } - #[cfg(not(target_os = "windows"))] + // pi-uutils: upstream emulates Unix SIGPIPE on Windows by calling + // `std::process::exit(13)` on a broken-pipe flush. That would kill the + // long-lived host shell process. An in-process builtin must never + // `process::exit`; let the broken pipe surface as a normal `io::Error` and + // propagate to the caller, matching every other pi-uutils builtin. writer.flush()?; - - // SIGPIPE is not available on Windows. - #[cfg(target_os = "windows")] - writer.flush().inspect_err(|err| { - if err.kind() == ErrorKind::BrokenPipe { - std::process::exit(13); - } - })?; Ok(()) } @@ -855,4 +851,47 @@ mod tests { assert_ne!(code, 0, "broken pipe must surface as a non-zero exit, not a panic"); } + + #[test] + fn unbounded_tail_broken_pipe_does_not_abort() { + use std::{ + collections::HashMap, + ffi::OsString, + io::{self, Cursor, ErrorKind, Write}, + sync::{Arc, atomic::AtomicBool}, + }; + + // Same broken-pipe consumer as above, but here stdin is a plain reader + // so `tail_stdin` always takes the streaming `unbounded_tail` path — the + // one the reported repro (`seq ... | tail -n 3 | head -n 0`) exercises, + // and where the Windows SIGPIPE emulation used to `std::process::exit`. + struct BrokenPipeWriter; + impl Write for BrokenPipeWriter { + fn write(&mut self, _buf: &[u8]) -> io::Result { + Err(io::Error::new(ErrorKind::BrokenPipe, "Broken pipe")) + } + + fn flush(&mut self) -> io::Result<()> { + Err(io::Error::new(ErrorKind::BrokenPipe, "Broken pipe")) + } + } + + let input = b"1\n2\n3\n4\n5\n".to_vec(); + let io = pi_uutils_ctx::ScopeIo { + stdin: Box::new(Cursor::new(input)), + stdin_fd: None, + stdin_is_search_input: false, + stdout: Box::new(BrokenPipeWriter), + stderr: Box::new(io::sink()), + cwd: std::env::temp_dir(), + env: HashMap::new(), + cancel: Arc::new(AtomicBool::new(false)), + }; + + let code = pi_uutils_ctx::scope(io, || { + crate::run(vec![OsString::from("tail"), OsString::from("-n"), OsString::from("3")]) + }); + + assert_ne!(code, 0, "broken pipe must surface as a non-zero exit, not process::exit"); + } } diff --git a/docs/advisor-watchdog.md b/docs/advisor-watchdog.md index a4881fa5c..65870bf11 100644 --- a/docs/advisor-watchdog.md +++ b/docs/advisor-watchdog.md @@ -80,7 +80,7 @@ Every advisor has the `advise` tool for surfacing notes into the primary transcr - `grep` - `glob` -A `WATCHDOG.yml` roster entry may broaden this with `tools: [...]`, selecting any subset of the built-in pool the session actually built (a factory that returned `null`, e.g. `lsp` with no matching servers, is absent). Grantable tools include mutating ones: `edit`, `write`, `bash`, `eval`, `browser`, `debug`, `ast_edit`, `task`, `job`, and the memory tools. Tool names outside [`BUILTIN_TOOL_NAMES`](../packages/coding-agent/src/tools/builtin-names.ts) are dropped with a warning. +A `WATCHDOG.yml` roster entry may broaden this with `tools: [...]`, selecting any subset of the built-in pool the session actually built (a factory that returned `null`, e.g. `lsp` with no matching servers, is absent). Grantable tools include mutating ones: `edit`, `write`, `bash`, `eval`, `browser`, `debug`, `ast_edit`, `task`, `hub`, and the memory tools. Tool names outside [`BUILTIN_TOOL_NAMES`](../packages/coding-agent/src/tools/builtin-names.ts) are dropped with a warning. Advisor grants are not routed through the primary agent's approval wrapper. The advisor pool is built from the built-in tool factories against its own `-advisor` `ToolSession` and then filtered by `WATCHDOG.yml`; it is not the primary `toolRegistry` wrapped with `ExtensionToolWrapper`. Granting write- or exec-tier tools therefore lets the advisor invoke those tools directly, subject to the tool's own runtime guards but not to `tools.approvalMode` / `tools.approval.` prompts. Keep mutating grants narrow and trusted. @@ -233,7 +233,7 @@ Fields: - `instructions` (top level): shared prompt prepended to every advisor's system prompt alongside `WATCHDOG.md`. Concatenated across all discovered `WATCHDOG.yml` files. - `advisors[].name`: human label; slugified for the session id and the `/__advisor.jsonl` filename. Duplicate slugs across files are resolved by the same specificity rule as `WATCHDOG.md` discovery (project leaf > project ancestor > user). - `advisors[].model`: optional model selector with optional `:level` thinking suffix (e.g. `x-ai/grok-code-fast:high`). Omitted → the advisor uses `modelRoles.advisor`. -- `advisors[].tools`: optional list of built-in tool names to grant. Omitted or empty → the default `read`/`grep`/`glob` subset. Any name in [`BUILTIN_TOOL_NAMES`](../packages/coding-agent/src/tools/builtin-names.ts) is accepted, including mutating tools (`edit`, `write`, `bash`, `eval`, `browser`, `debug`, `ast_edit`, `task`, `job`, and the memory tools). Legacy aliases (`search`→`grep`, `find`→`glob`) are normalized. Unknown names are dropped with a warning. See [Tools and isolation](#tools-and-isolation) for the safety implications of granting mutating tools. +- `advisors[].tools`: optional list of built-in tool names to grant. Omitted or empty → the default `read`/`grep`/`glob` subset. Any name in [`BUILTIN_TOOL_NAMES`](../packages/coding-agent/src/tools/builtin-names.ts) is accepted, including mutating tools (`edit`, `write`, `bash`, `eval`, `browser`, `debug`, `ast_edit`, `task`, `hub`, and the memory tools). Legacy aliases (`search`→`grep`, `find`→`glob`) are normalized. Unknown names are dropped with a warning. See [Tools and isolation](#tools-and-isolation) for the safety implications of granting mutating tools. - `advisors[].instructions`: this advisor's specialization, appended after the shared baseline. Both instruction fields expand `@path` imports like `WATCHDOG.md`. ### Discovery locations @@ -277,4 +277,4 @@ Why a file: The file follows session switches: on `/new`, resume/switch, and branch the recorder reopens at the new session's path on the next advisor turn; before a `/drop` deletes the old artifacts dir the recorder feed is detached and drained so a queued write cannot recreate the deleted file. The on-disk log is append-only and independent of the in-memory context — re-primes and compaction never truncate it. -The advisor is never a peer. The `advisor`-kind registry ref is excluded from every agent-facing surface — the `irc` peer roster and broadcast targets, the subagent peer prompt, and the `history://` index/lookup/completions — and cannot be messaged (`irc send` and collab chat refuse it) or revived/killed from the Agent Hub or collab. It is not addressable as a peer, regardless of what tools it has been granted. +The advisor is never a peer. The `advisor`-kind registry ref is excluded from every agent-facing surface — the `hub` peer roster and broadcast targets, the subagent peer prompt, and the `history://` index/lookup/completions — and cannot be messaged (`hub` send and collab chat refuse it) or revived/killed from the Agent Hub or collab. It is not addressable as a peer, regardless of what tools it has been granted. diff --git a/docs/compaction.md b/docs/compaction.md index 186d53054..3d9790691 100644 --- a/docs/compaction.md +++ b/docs/compaction.md @@ -164,7 +164,7 @@ If pruning changes entries, session storage is rewritten and agent message state ### Useless-result elision -Tools can flag a finished result as contextually useless — a search with zero matches, a `job` poll that timed out with everything still running, an empty `irc` inbox drain. The flag originates on the tool result (`AgentToolResult.useless`, set via `ToolResultBuilder.useless()` or directly on the returned object), is copied by the agent loop onto the persisted `ToolResultMessage` (never together with `isError` — errors always win), and is consumed in three places: +Tools can flag a finished result as contextually useless — a search with zero matches, a `hub` wait that timed out with everything still running, an empty `hub` inbox drain. The flag originates on the tool result (`AgentToolResult.useless`, set via `ToolResultBuilder.useless()` or directly on the returned object), is copied by the agent loop onto the persisted `ToolResultMessage` (never together with `isError` — errors always win), and is consumed in three places: - **Per-turn stale-result pass** (`pruneSupersededToolResults`, gated by `compaction.dropUseless`, default on): flagged results are blanked to the exact placeholder `[Uneventful result elided]` (`USELESS_NOTICE`) with the same cache-aware timing as superseded reads — only when the suffix after the candidate is small (≤ ~8k tokens) or the session has idled past the provider prompt-cache lifetime. Results smaller than the notice itself are never blanked (no savings), and protected tools are exempt. - **Threshold prune** (`pruneToolOutputs`): flagged results bypass the protect-recent window, same as superseded reads, and receive `USELESS_NOTICE` instead of the token-count placeholder. diff --git a/docs/custom-tools.md b/docs/custom-tools.md index 9f74137fd..9c6d63e0c 100644 --- a/docs/custom-tools.md +++ b/docs/custom-tools.md @@ -129,7 +129,7 @@ From `types.ts` and `loader.ts`: - `typebox`: zod-backed compatibility shim for legacy TypeBox-style schemas - `zod`: injected `zod/v4` module (canonical for new schemas) - `pi`: injected `@oh-my-pi/pi-coding-agent` exports -- `pushPendingAction(action)`: register a preview action for hidden `resolve` tool (`docs/resolve-tool-runtime.md`) +- `pushPendingAction(action)`: register a preview action finalized via plain-text writes to `/xdev/resolve` or `/xdev/reject` (`docs/resolve-tool-runtime.md`) Loader starts with a no-op UI context and requires host code to call `setUIContext(...)` when real UI is ready. ## Execution contract and typing diff --git a/docs/environment-variables.md b/docs/environment-variables.md index adf3d310c..cb34ed6b8 100644 --- a/docs/environment-variables.md +++ b/docs/environment-variables.md @@ -407,7 +407,7 @@ These are read as runtime signals; they are usually set by the terminal/OS rathe | `PI_NO_DECCARA` | If set (truthy), disables Kitty DECCARA rectangular-SGR background fills (forces padded-string rendering) | | `PI_DEBUG_REDRAW` | If `1`, enables redraw debug logging | | `PI_FORCE_IMAGE_PROTOCOL` | Forces terminal image protocol detection (`kitty`, `iterm2`/`iterm`, `sixel`, `none`) | -| `PI_TUI_RESIZE_IN_PLACE` | `1`/`true` force in-place resize (no alt-screen borrow, no ED3 rewrap); `0`/`false` force the alt-screen fast path. Default-on for Warp, which re-reports its size on alt-screen toggles | +| `PI_TUI_RESIZE_IN_PLACE` | `1`/`true` preserves terminal-managed history and repaints after resize settle; `0`/`false` uses viewport-only drag paints followed by one ED3 history rewrap. Neither path switches terminal buffers. Default-on for Warp and multiplexers | --- diff --git a/docs/extensions.md b/docs/extensions.md index 786f66284..0da8c250e 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -160,6 +160,30 @@ Handlers and tool `execute` receive `ctx` with: - `shutdown()` - `getSystemPrompt()` - `memory` (optional structured memory runtime — status/search/save across the configured backend) +- `setInterval(fn, ms, ...args)` / `setTimeout(fn, ms, ...args)` / `clearTimer(timer)` — managed timers (see below) + +### Background work (`ctx.setInterval` / `ctx.setTimeout`) + +Extensions run **in-process with no isolation**. A raw `setInterval`/`setTimeout`/detached-promise callback that throws runs outside the handler-dispatch try/catch, surfaces as a process-level `uncaughtException`, and the global postmortem handler treats it as fatal — **the whole session is torn down**, not just the offending extension. + +Use `ctx.setInterval` / `ctx.setTimeout` for any periodic or deferred background work. They mirror the platform signatures but: + +- run the callback with the same isolation as handler dispatch — a synchronous throw or a rejected promise is logged and reported through the extension error channel, and the session keeps running; +- return a handle you can pass to `ctx.clearTimer(handle)`; +- are `unref`'d (never keep the process alive on their own) and are cleared automatically on `session_shutdown`. + +```ts +pi.on("session_start", async (_event, ctx) => { + const timer = ctx.setInterval(() => { + // A throw here is contained — it will not crash the session. + ctx.ui.notify("tick", "info"); + }, 60_000); + // Optional: clear it yourself; otherwise it is cleared on shutdown. + pi.on("session_shutdown", () => ctx.clearTimer(timer)); +}); +``` + +If you use raw `setInterval`/`setTimeout` or detached promises instead, you own the isolation: wrap the callback body in your own `try/catch` (an unhandled throw will take down the session) and clear the timer on `session_shutdown`. ### Model selection (`ctx.models`) diff --git a/docs/magic-keywords.md b/docs/magic-keywords.md new file mode 100644 index 000000000..19750d4da --- /dev/null +++ b/docs/magic-keywords.md @@ -0,0 +1,46 @@ +# Magic keywords + +Magic keywords are standalone words in a user prompt that add a hidden instruction for that turn. They are enabled by default and glow in the editor when `omp` recognizes them. + +## Keywords + +| Keyword | Effect | +|---|---| +| `ultrathink` | Asks the agent to reason carefully through a multi-step task. When automatic thinking is active, it also selects the highest reasoning effort supported by the current model for that turn. | +| `orchestrate` | Switches the agent to the multi-agent orchestration contract: scope the full task, delegate substantial independent work in parallel, verify each phase, and continue until the request is complete. | +| `workflowz` | Asks the agent to build and run a deterministic multi-subagent workflow with the `task` tool. It is intended for broad research, reviews, migrations, or other work that benefits from parallel coverage. The keyword only adds its instruction when `task` is available in the active tool set. | + +Use the keyword anywhere in the prose of the prompt: + +```text +ultrathink about the failure modes before changing this API + +orchestrate the migration described in docs/plan.md + +workflowz an adversarial review of the authentication changes +``` + +## Matching rules + +Matching is deliberate so source code and paths do not accidentally change agent behavior: + +- Use the exact lowercase spelling. `Ultrathink`, `Orchestrate`, and `Workflowz` do not trigger. +- The keyword must be standalone. Sentence punctuation may touch it, but identifiers, inflections, paths, and file extensions do not match. For example, `orchestrate,` matches; `orchestrated` and `orchestrate.ts` do not. +- Fenced code blocks, inline code spans, and XML/HTML sections are ignored. +- The instruction applies to the user turn containing the keyword. The highlighted word remains part of the visible prompt; the added instruction is hidden. + +## Configuration + +Open `/settings` and use **Interaction → Magic Keywords**, or change the settings from a shell: + +```bash +# Disable every magic keyword +omp config set magicKeywords.enabled false + +# Disable one keyword while leaving the others enabled +omp config set magicKeywords.ultrathink false +omp config set magicKeywords.orchestrate false +omp config set magicKeywords.workflow false +``` + +All four settings default to `true`. Run `omp config list` to inspect every available setting and its current value. See [Settings](./settings.md) for configuration scopes, precedence, and project-local overrides. diff --git a/docs/resolve-tool-runtime.md b/docs/resolve-tool-runtime.md index 6aadf6168..97001b01e 100644 --- a/docs/resolve-tool-runtime.md +++ b/docs/resolve-tool-runtime.md @@ -1,145 +1,53 @@ -# Resolve tool runtime internals +# Resolution devices runtime -This document explains how preview/apply workflows are modeled in coding-agent and how built-in or custom tools can participate via the pending-invoker registry and `pushPendingAction`. (Pending previews live in a separate non-forcing registry inside `ToolChoiceQueue`; only genuine hard forces use the consuming directive queue.) +Pending previews and plan approval no longer use a `resolve` tool. They finalize through three plain-text writes handled by `packages/coding-agent/src/tools/resolve.ts`: -## Scope and key files +- `/xdev/resolve` — apply the pending staged preview; body = reason text +- `/xdev/reject` — discard the pending staged preview; body = reason text +- `/xdev/propose` — submit the plan for approval while plan mode is active; body = the plan slug/title (`` for `local://-plan.md`) -- [`src/tools/resolve.ts`](../packages/coding-agent/src/tools/resolve.ts) -- [`src/tools/ast-edit.ts`](../packages/coding-agent/src/tools/ast-edit.ts) -- [`src/extensibility/custom-tools/types.ts`](../packages/coding-agent/src/extensibility/custom-tools/types.ts) -- [`src/extensibility/custom-tools/loader.ts`](../packages/coding-agent/src/extensibility/custom-tools/loader.ts) -- [`src/sdk.ts`](../packages/coding-agent/src/sdk.ts) +## Preview flows -## What `resolve` does +Preview producers call `queueResolveHandler(...)` with `apply(reason)` and optional `reject(reason)` callbacks. That registers a non-forcing pending invoker in `ToolChoiceQueue`. -`resolve` is a hidden tool that finalizes a pending preview action. +While a preview is pending, `AgentSession.nextToolChoiceDirective()` returns a soft requirement: -- `action: "apply"` executes the queued action's `apply(reason, extra)` callback and returns that result with resolve metadata. -- `action: "discard"` invokes `reject(reason, extra)` if provided; otherwise returns `Discarded: