mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
Compare commits
2
Commits
833b9514d4
...
2b21163a18
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b21163a18 | ||
|
|
dae1a01f23 |
@@ -20,7 +20,7 @@
|
||||
| `pi-notify/` | Kitty OSC 99 完成与权限确认通知,精确聚焦来源窗口 |
|
||||
| `pi-compaction/` | 压力触发的观察遮蔽(Pi context_edit)、阈值整段摘要、effective token、native compaction |
|
||||
| `pi-overflow/` | 新工具结果统一截断:超阈值原文落盘 + 有界视图 + `overflow_query` |
|
||||
| `extensions/fff-override.ts` | FFF `override` 模式接管 `find`/`grep`/`multi_grep` 与 `@` 补全 |
|
||||
| `extensions/fff-override.ts` | FFF `override` 模式接管 `find`/`grep` 与 `@` 补全,`multi_grep` 按上游保持关闭 |
|
||||
| `extensions/codegraph.ts` | 原生 `codegraph_explore`,自有 stdio JSON-RPC 直连 `codegraph serve`(`codegraph-client.ts`) |
|
||||
| `extensions/hashline.ts` | 部署 `config/pi-hashline-edit.json` 后加载 pi-hashline-edit 覆盖 `read`/`edit`(`hashline-config.ts`) |
|
||||
| `extensions/permission-system.ts` | 直接加载仓库内 `pi-permission-system/src/index.ts` |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
个人维护的 Pi 扩展组合包。安装一次即可加载并配置:
|
||||
|
||||
- `@ff-labs/pi-fff@0.10.5`:使用官方 `override` 模式接管 `find`、`grep` 和 `multi_grep`。
|
||||
- `@ff-labs/pi-fff@0.10.5`:使用官方 `override` 模式接管 `find` 和 `grep`;上游默认关闭的 `multi_grep` 保持关闭。
|
||||
- 本仓库维护的 `pi-rtk-optimizer`:只处理 Bash/read 等输出压缩,默认不改写命令,也不处理搜索工具结果。
|
||||
- 本仓库自维护的 `pi-web-search`:对模型只暴露 `web_search` / `web_fetch` 两个工具;`web_search` 由当前主模型在 Tavily / Exa / Keenable 间路由,`web_fetch` 固定走 Exa,三家均以纯 `fetch` 直连(Exa 为 `mcp.exa.ai` 的 JSON-RPC,不经 MCP adapter)。
|
||||
- `extensions/codegraph.ts`:注册原生 `codegraph_explore` 工具,按索引根惰性启动常驻 `codegraph serve --mcp` 子进程,由组合包自有 stdio JSON-RPC 客户端直连(无 MCP adapter/SDK);serve 的文件监听自动同步索引,无结果缓存;未初始化项目经确认后才 `codegraph init`,拒绝记入会话不再重复询问。
|
||||
@@ -138,7 +138,7 @@ CodeGraph 的更新或遥测设置。
|
||||
|
||||
### 工具与搜索路由
|
||||
|
||||
`config/prompt/routing.jsonc` 定义跨工具路由规则,由 `extensions/system-prompt.ts` 在每轮开始前按当前激活工具渲染为 `<tool_routing>` 分区。代码结构、调用关系和待修改 symbol 优先使用 CodeGraph,定义、引用、类型和修改后诊断优先使用 LSP;仓库字面搜索先用 FFF `find` 缩小文件或目录范围,再在已收敛的路径中用 `grep`/`multi_grep` 获取行号。宽泛搜索命中大量结果、发生截断或达到上限时,应继续缩小路径、glob 或 pattern,而不是提高 limit 或输出全部结果。
|
||||
`config/prompt/routing.jsonc` 定义跨工具路由规则,由 `extensions/system-prompt.ts` 在每轮开始前按当前激活工具渲染为 `<tool_routing>` 分区。代码结构、调用关系和待修改 symbol 优先使用 CodeGraph,定义、引用、类型和修改后诊断优先使用 LSP;本地字面搜索按已知信息直接选工具:确切标识符或文本用 `grep` 字面模式并限定 path/exclude,多个命名变体合并为一次 `grep` 正则 alternation,文件名、路径片段或功能概念用 FFF `find` 模糊全路径匹配,然后只 `read` 命中区域。对搜索结果做提取、去重、计数或聚合时,在 codemode 脚本中调用 `tools.grep` / `tools.find` 并用 JavaScript 后处理;bash 中不得运行 `grep`/`rg`/`find`/`fd`/`git grep`(策略 deny 的 reason 指明替代工具),程序需要文件清单时传 `$(git ls-files '<glob>')`。宽泛搜索命中大量结果、发生截断或达到上限时,应继续缩小路径、glob 或 pattern,而不是提高 limit 或输出全部结果。
|
||||
|
||||
在线搜索默认按任务只选一个服务:关键词不确定、新闻/近期话题或需要较多候选来源时优先 Tavily;官方/API 文档、版本信息、论文和高确定性事实确认优先 Exa;中文政策、政府公告、中文网页、站点限定和日期筛选优先 Keenable。只有来源质量或不确定性确实需要时才跨服务复核,避免默认把三套结果全部塞进上下文。
|
||||
|
||||
@@ -211,7 +211,8 @@ Hashline 不负责压缩大文件;每行锚点本身还会增加少量 token
|
||||
|
||||
`extensions/fff-override.ts` 强制设置 FFF 官方的 `PI_FFF_MODE=override`(显式传入
|
||||
`--fff-mode` 仍按 FFF 官方优先级覆盖它)。FFF 替换
|
||||
Pi 的 `find` 和 `grep`,并提供 `multi_grep`;RTK 对所有 `grep` 工具结果直接跳过,
|
||||
Pi 的 `find` 和 `grep`;`multi_grep` 上游标注为有害、默认关闭,wrapper 在加载前
|
||||
删除 `PI_FFF_MULTIGREP` 环境变量,使其永远无法注册。RTK 对所有 `grep` 工具结果直接跳过,
|
||||
也默认关闭 Bash 命令改写,因此不会把 `rg`、`grep`、`find` 或 `fd` 改写到 RTK。
|
||||
|
||||
### 输出压缩归 RTK
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
"edit": "allow",
|
||||
"grep": "allow",
|
||||
"find": "allow",
|
||||
"multi_grep": "allow",
|
||||
"ls": "allow",
|
||||
"codegraph_explore": "allow",
|
||||
"lsp_diagnostics": "allow",
|
||||
@@ -103,10 +102,10 @@
|
||||
},
|
||||
"bash": {
|
||||
"*": "allow",
|
||||
"rg*": "deny",
|
||||
"grep*": "deny",
|
||||
"find*": "deny",
|
||||
"fd*": "deny",
|
||||
"rg*": { "action": "deny", "reason": "Bash content search is disabled. Locally use the grep tool (pattern + path/exclude; regex alternation for several names); to extract/dedupe/count matches, call tools.grep in codemode and process in JS; to filter command output, use overflow_query on the stored result. Over SSH use ssh_grep." },
|
||||
"grep*": { "action": "deny", "reason": "Bash content search is disabled. Locally use the grep tool (pattern + path/exclude; regex alternation for several names); to extract/dedupe/count matches, call tools.grep in codemode and process in JS; to filter command output, use overflow_query on the stored result. Over SSH use ssh_grep." },
|
||||
"find*": { "action": "deny", "reason": "Bash file search is disabled. Locally use the find tool (fuzzy path, or a glob in path such as 'src/**/*.ts'); to pass a file list to a program use $(git ls-files '<glob>'). Over SSH use ssh_find." },
|
||||
"fd*": { "action": "deny", "reason": "Bash file search is disabled. Locally use the find tool (fuzzy path, or a glob in path such as 'src/**/*.ts'); to pass a file list to a program use $(git ls-files '<glob>'). Over SSH use ssh_find." },
|
||||
"git *": "ask",
|
||||
"git status*": "allow",
|
||||
"git diff*": "allow",
|
||||
@@ -114,7 +113,7 @@
|
||||
"git show*": "allow",
|
||||
"git rev-parse*": "allow",
|
||||
"git ls-files*": "allow",
|
||||
"git grep*": "deny",
|
||||
"git grep*": { "action": "deny", "reason": "Bash content search is disabled. Locally use the grep tool (pattern + path/exclude; regex alternation for several names); to extract/dedupe/count matches, call tools.grep in codemode and process in JS; to filter command output, use overflow_query on the stored result. Over SSH use ssh_grep." },
|
||||
"git branch": "allow",
|
||||
"git branch --show-current*": "allow",
|
||||
"git remote -v": "allow",
|
||||
|
||||
@@ -16,16 +16,25 @@
|
||||
"text": "Use LSP for symbol definitions, references, hover/type information, and post-edit diagnostics when the language server supports the file."
|
||||
},
|
||||
{
|
||||
"whenAny": ["find", "grep", "multi_grep"],
|
||||
"text": "For literal search, narrow in stages instead of requesting a repository-wide dump:\n 1. Use find to identify likely files or directories. Constrain path/glob and keep the result limit small.\n 2. Use grep or multi_grep only inside the narrowed path or file to obtain matching line numbers. Start with a specific or literal pattern, a small context window, and a bounded limit.\n 3. Use read with offset/limit only for the exact matching region when full source is still needed."
|
||||
"whenAny": ["find", "grep"],
|
||||
"text": "Pick the local search tool by what you already know:\n - Exact identifier or text: grep with a literal pattern, constrained by path/exclude. Do not run find first just to locate it.\n - Several identifiers or naming variants: one grep with a regex alternation (e.g. user_id|userId|UserId) instead of several greps.\n - A filename, path fragment, or feature concept: find (fuzzy whole-path match; use a glob in path for exact names).\n - Then read only the matching region with offset/limit."
|
||||
},
|
||||
{
|
||||
"whenAny": ["find", "grep", "multi_grep"],
|
||||
"whenAny": ["find", "grep"],
|
||||
"text": "Do not run a broad search expected to return hundreds or thousands of files or matches. If results truncate or hit a limit, narrow the path, glob, or pattern instead of increasing the limit or dumping all results."
|
||||
},
|
||||
{
|
||||
"whenAll": ["codemode"],
|
||||
"whenAny": ["find", "grep"],
|
||||
"text": "To extract, deduplicate, count, or aggregate across search matches (what grep -o | sort -u | wc would do), call tools.grep / tools.find inside one codemode script and post-process the results in JavaScript."
|
||||
},
|
||||
{
|
||||
"whenAny": ["bash"],
|
||||
"text": "Use bash for builds, tests, git, package scripts, and other programs. Never search files by running grep, find, rg, or fd through bash; use the find/grep tools."
|
||||
"text": "Use bash for builds, tests, git, package scripts, and other programs. Never run grep, rg, find, fd, or git grep in bash — standalone, piped, inside a loop, or in $(…). Search files with the grep/find tools; when a program needs a file list (wc, node, tsc), pass $(git ls-files '<glob>')."
|
||||
},
|
||||
{
|
||||
"whenAll": ["bash", "overflow_query"],
|
||||
"text": "To filter a command's output, run it without | grep; if the result carries a [pi-overflow id=…] marker, search it with overflow_query instead of re-running."
|
||||
},
|
||||
{
|
||||
"whenAny": ["ssh_connect"],
|
||||
@@ -59,12 +68,15 @@
|
||||
},
|
||||
{
|
||||
"whenAny": ["read"],
|
||||
"text": "Use read only when exact source is needed or the file is small. Before editing, request the smallest useful offset/limit range; do not read a complete large file merely to copy, compare, hash, count, explore, or summarize it."
|
||||
"text": "Use read only when exact source is needed or the file is small. Before editing, request the smallest useful offset/limit range; do not read a complete large source file merely to copy, compare, hash, count, or explore it. Reading project overview docs such as a README in full is fine."
|
||||
},
|
||||
{
|
||||
"whenAll": ["read", "edit"],
|
||||
"text": "For existing-file changes, obtain fresh LINE#HASH anchors with a small read (find/grep output has no anchors), then use anchored edit operations. Reuse fresh anchors returned by successful edits for chained changes; re-read only when anchors are stale or the next target region was not shown."
|
||||
},
|
||||
{
|
||||
"text": "When a tool call is denied by policy, use the alternative named in the denial and continue the subtask; do not drop it or present unverified facts as checked."
|
||||
},
|
||||
{
|
||||
"text": "Stop searching once the current evidence is sufficient. Avoid repeating the same lookup through CodeGraph, find, grep, and read without a concrete information gap."
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ You locate code and answer "where is X / what references Y" questions for the de
|
||||
## Hard limits
|
||||
- Do not create, modify, move, copy, or delete files, including temporary files anywhere (also /tmp).
|
||||
- Use bash only for read-only commands such as git status, git log, git diff, git show, and ls. Never use output redirection, heredocs, or any command that changes system state.
|
||||
- Never search through bash (grep, rg, find, fd); use the find and grep tools.
|
||||
- Never search through bash (grep, rg, find, fd); use the find and grep tools. For several names use one grep with a regex alternation (a|b); when a program needs a file list, pass $(git ls-files '<glob>').
|
||||
|
||||
## Approach
|
||||
- Match the requested breadth: "quick" is one targeted lookup, "medium" covers a few related locations, "very thorough" searches multiple locations and naming conventions.
|
||||
|
||||
@@ -14,7 +14,7 @@ You explore the codebase and return an implementation plan to the delegating age
|
||||
## Hard limits
|
||||
- Do not create, modify, move, copy, or delete files, including temporary files anywhere (also /tmp).
|
||||
- Use bash only for read-only commands such as git status, git log, git diff, git show, and ls. Never use output redirection, heredocs, or any command that changes system state.
|
||||
- Never search through bash (grep, rg, find, fd); use the find and grep tools.
|
||||
- Never search through bash (grep, rg, find, fd); use the find and grep tools. For several names use one grep with a regex alternation (a|b); when a program needs a file list, pass $(git ls-files '<glob>').
|
||||
|
||||
## Process
|
||||
1. Restate the goal and the constraints you were given. Record anything ambiguous as an open question instead of guessing.
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"edit": {
|
||||
"guidelines": [
|
||||
"Use edit with LINE#HASH anchors from the latest read or edit result for all file changes; batch every change to one file into a single edit call.",
|
||||
"find/grep/multi_grep results carry plain line numbers, not LINE#HASH anchors; read the exact target range before editing.",
|
||||
"find/grep results carry plain line numbers, not LINE#HASH anchors; read the exact target range before editing.",
|
||||
"Never reuse anchors from an earlier read or edit response once a newer one exists for the same file; treat only the most recent response as carrying valid anchors.",
|
||||
"After a successful edit, the returned --- Anchors --- block replaces a re-read for nearby follow-up edits.",
|
||||
"On [E_STALE_ANCHOR], the error lists the stale refs and any content-matched candidate anchors; re-read the file to get current anchors before retrying."
|
||||
@@ -28,19 +28,18 @@
|
||||
"write": { "inherit": true },
|
||||
"ls": { "inherit": true },
|
||||
"powershell": { "inherit": true },
|
||||
"grep": { "inherit": true },
|
||||
"find": { "inherit": true },
|
||||
"multi_grep": { "inherit": true },
|
||||
"grep": { "snippet": "Search file contents via the FFF index (git-aware, smart-case, frecency-ranked, regex or literal); replaces bash grep/rg" },
|
||||
"find": { "snippet": "Find files by fuzzy path or glob via the FFF index; replaces bash find/fd" },
|
||||
"tool_search": { "inherit": true },
|
||||
"codemode": {
|
||||
"guidelines": [
|
||||
"Use codemode to batch two or more independent tool calls (Promise.allSettled), chain dependent lookups, or filter large output down to what you need; call a single tool directly instead of wrapping it in a script.",
|
||||
"Call edit, write, ssh_connect, ssh_cd, ask_user_question, and Agent directly, never from a codemode script.",
|
||||
"Inside scripts, search with tools.grep, tools.find, or tools.multi_grep; never run grep, find, rg, or fd through tools.bash."
|
||||
"Inside scripts, search with tools.grep or tools.find and do extraction, dedup, and counting in JavaScript; never run grep, find, rg, or fd through tools.bash."
|
||||
]
|
||||
},
|
||||
|
||||
"codegraph_explore": { "inherit": true },
|
||||
"codegraph_explore": { "snippet": "Explore code structure from the CodeGraph index: symbols, relationships, call flows, and their source" },
|
||||
"lsp_diagnostics": { "inherit": true },
|
||||
"lsp_definition": { "inherit": true },
|
||||
"lsp_references": { "inherit": true },
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
## 各 wrapper 约定
|
||||
|
||||
- `fff-override.ts`:强制 FFF 官方 `override` 模式,统一接管 `find`、`grep`、`multi_grep` 和 `@` 补全;FFF 独占本地字面搜索,远端服务器搜索由 `ssh_find` / `ssh_grep` 负责,RTK 不得接管搜索工具(见 `pi-rtk-optimizer/AGENTS.md`)。显式 CLI flag 仍遵循 FFF 官方优先级。
|
||||
- `fff-override.ts`:强制 FFF 官方 `override` 模式,统一接管 `find`、`grep` 和 `@` 补全;`multi_grep` 按上游保持关闭(加载前删除 `PI_FFF_MULTIGREP` 环境变量)。FFF 独占本地字面搜索,远端服务器搜索由 `ssh_find` / `ssh_grep` 负责,RTK 不得接管搜索工具(见 `pi-rtk-optimizer/AGENTS.md`)。显式 CLI flag 仍遵循 FFF 官方优先级。
|
||||
- `codegraph.ts` + `codegraph-client.ts`:注册原生 `codegraph_explore` 工具,按索引根惰性常驻 `codegraph serve --mcp -p <root>` 子进程,经组合包自有 stdio 逐行 JSON-RPC 客户端直连(不经 shell、不带 pi-mcp-adapter 或 MCP SDK);serve 进程内置文件监听自动同步索引,无结果缓存。子进程 10 分钟无调用自动关闭、会话结束统一关闭;调用中死亡换新客户端透明重试一次;其启动的上游 daemon(`.codegraph/daemon.sock`)退出后可多驻留 ≤5 分钟,属上游行为,组合包不管理。
|
||||
- CodeGraph 是机器级依赖,`install.sh`/`update.sh` 经官方安装器管理 `codegraph` CLI,组合包不内置其运行时。安装、升级和普通启动流程不得执行 `codegraph init`;只有 `codegraph_explore` 在未初始化项目上被调用、且用户在 `ctx.ui.confirm` 中明确确认后才可 `codegraph init` 当前目录,拒绝记入会话级 declined 不再重复询问;init 失败必须如实报告并让模型回退内置工具;不得改动 CodeGraph 更新或遥测设置。CLI 缺失、init 被拒或服务调用失败都不得抛出,必须以文本结果打回并引导模型改用 `find`/`grep`/`read`/LSP。已移除的 `plugin-init.ts`(原 `/plugin_init`)职责已并入本文件的惰性确认,不得恢复。
|
||||
- `hashline.ts` + `hashline-config.ts`:部署组合包权威 `config/pi-hashline-edit.json` 后加载 `pi-hashline-edit`;默认以 2 字符行哈希覆盖内置 `read` / `edit`,`grep` 与 `replaceText` 均关闭。它保证锚定编辑而不负责压缩,大文件上下文节省必须来自先定位/隔离分析、再做最小范围 `offset/limit` 读取。
|
||||
|
||||
@@ -5,5 +5,6 @@ import { guardExtensionApi } from "./prompt-guard.ts";
|
||||
/** Load FFF as the bundle's canonical `find` and `grep` implementation. */
|
||||
export default function fffOverrideExtension(pi: ExtensionAPI): void {
|
||||
process.env.PI_FFF_MODE = "override";
|
||||
delete process.env.PI_FFF_MULTIGREP;
|
||||
fffExtension(guardExtensionApi(pi));
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Changed
|
||||
|
||||
- **Descend into bash control-flow bodies when enumerating command units.** `for`/`while`/`until` loops, `if`/`elif`/`else` branches, `case` items, `{ … }` brace groups, function definitions, and `!`-negated commands were previously matched only as whole-statement text, so a denied command hidden in a loop body or behind `!` (e.g. `for d in */; do find "$d" …; done`, `! rm -rf x`) bypassed every bash pattern — as did a substitution inside a value (`x=$(grep foo f)`, `export X=$(find .)`). The enumerator still emits each enclosing statement as a unit and now also emits every condition, body, and negated command — plus substitutions hosted in value lists, case subjects, assignments, declarations, and other non-command positions — so the most restrictive decision wins; pure body containers (`do …; done`, `elif`/`else` clauses, `case` items) no longer produce their own units. Hard guards share the same enumeration and now reach those nested commands too.
|
||||
- **Replace the AutoReview text-JSON chat reviewer with a conversational tool-call reviewer.** The reviewer makes one independent model call per ask — a fixed review prompt, the rendered transcript, and a bounded JSON description of the planned action — and reaches its verdict only by calling one of two tools, `approve` or `reject`, each carrying a `reason`. A reply with no tool call counts as no verdict; multiple calls, unknown tools, and missing reasons count as invalid responses; neither is retried. An experimental TypeSafe Jev classifier rewrite explored in the working tree was dropped before release and never shipped.
|
||||
- **The review model follows the session by default.** With `autoReview.provider` and `autoReview.model` both unset, the reviewer calls the session's current model (`ctx.model`), resolved live per ask so a mid-session switch applies to the next request. Setting both pins a fixed reviewer; setting exactly one is a misconfiguration that warns once at `session_start` and defers every ask.
|
||||
- **A rejection defers instead of denying.** `reject` maps to `defer` and the model's `reason` is shown in the human permission prompt, labelled as model-generated. Every review failure — timeout, provider error, malformed or truncated reply, unresolved model — also defers with a fixed system-authored note. Pressing Esc during a review aborts it and denies outright, so no human dialog appears after the user already refused.
|
||||
|
||||
@@ -21,3 +21,7 @@ AutoReview 源码已整合进 `src/auto-review/`,不再保留或加载第二
|
||||
## 本地维护
|
||||
|
||||
本目录不是 submodule,不保留嵌套 `.git`、上游 `.pi` 本地状态、`dist/` 或构建产物。同步任一上游时必须从其源码仓库的明确 tag/commit 移植并与本地改动对照审查,不得整体覆盖,也不得使用 npm 预编译产物作为导入源。
|
||||
|
||||
## 本地改动
|
||||
|
||||
- Bash 命令枚举在保留整条控制流语句为独立 command unit 的基础上递归下降:`for`/`while`/`until`、`if`/`elif`/`else`、`case` 分支、`{ … }` 组合语句、函数定义与 `!` 取反命令体内的命令也按 bash pattern 逐条判定;赋值与声明值(`x=$(…)`、`export X=$(…)`)、值列表、case 词与 `[[ … ]]` 测试中的 `$(…)` 替换同样枚举,而 `do …; done`、`elif`/`else`、`case` 分支等纯容器不再单独产生 unit。藏在循环体、取反前缀或赋值载荷里的 `find`/`grep`/`rm` 等不再绕过 deny 或 hard guard。
|
||||
|
||||
@@ -370,7 +370,10 @@ This holds wherever the substitution appears, not only in argument position.
|
||||
A substitution in a **redirect target** (`echo hi > $(rm *.txt)`, `cat < <(rm c)`, ``echo hi 2> `rm d` ``) and one in an **interpolating heredoc body** (`cat <<EOF` with `$(rm e)` in the body) are evaluated the same way.
|
||||
A quoted heredoc delimiter (`<<'EOF'` or `<<"EOF"`) does not interpolate, so its body is literal text and nothing in it is evaluated as a command.
|
||||
The enclosing command is still matched without its redirect, so a rule like `npm install` keeps matching `npm install > out.txt`.
|
||||
Control-flow bodies (`if`/`while`/`for`/`case`) and `{ … }` brace groups are not descended into; their contents are matched as part of the enclosing statement's text.
|
||||
Control-flow statements (`for`/`while`/`until` loops, `if`/`elif`/`else` branches, `case` items, `{ … }` brace groups, and function definitions) are evaluated as a whole unit *and* descended into: every command in their conditions and bodies is matched against the bash patterns in addition to the enclosing statement's text.
|
||||
So `for d in src docs; do find "$d" -name '*.ts'; done` evaluates both the whole loop and `find "$d" -name '*.ts'`; if `find *` is denied, the whole invocation is denied.
|
||||
Non-command parts of a control-flow statement — the `for` value list, the `case` subject and patterns, `[[ … ]]` tests — are not matched as commands themselves, but substitutions nested inside them still are, so `for f in $(find …)` and `if [[ "$(rm e)" = x ]]` gate the inner command.
|
||||
Negation and shell bookkeeping get the same treatment: `! grep -q x f` evaluates both `! grep -q x f` and the inner `grep -q x f`, and a substitution inside a value — `x=$(grep foo f)`, `export X=$(find .)`, `local y=$(rg z)` — is gated as a command substitution even though the assignment or declaration itself runs no command.
|
||||
|
||||
A leading environment-variable assignment prefix is stripped before matching, so the rule gates the underlying command rather than the prefix.
|
||||
So `AWS_PROFILE=prod aws ec2 …` is matched as `aws ec2 …` — a `aws *` rule applies even though the invocation begins with `AWS_PROFILE=`.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
EXECUTION_HOST_TYPES,
|
||||
forEachNestedExecution,
|
||||
NESTED_EXECUTION_CONTEXTS,
|
||||
} from "#src/access-intent/bash/nested-execution";
|
||||
import type { TSNode } from "#src/access-intent/bash/parser";
|
||||
import {
|
||||
@@ -72,6 +73,62 @@ const COMMAND_ENUM_DESCEND = new Set([
|
||||
*/
|
||||
const COMMAND_ENUM_SKIP = new Set(["comment", "heredoc_end"]);
|
||||
|
||||
/**
|
||||
* Compound statement node types whose bodies really run commands:
|
||||
* `for`/`while`/`until` loops (tree-sitter-bash folds `until` into
|
||||
* `while_statement`), `if`/`case` statements, `{ … }` brace groups
|
||||
* (`compound_statement`), function definitions, and `!`-negated commands.
|
||||
*
|
||||
* These were once emitted whole without descending (#306 deferred that), which
|
||||
* let a gated command hide inside a loop body — a `for …; do find …; done`
|
||||
* matched only patterns starting with `for`. Each is now emitted whole AND
|
||||
* enumerated child-by-child; a `negated_command` emits `! grep …` whole and
|
||||
* then `grep …` so the `!` prefix cannot defeat a command pattern either.
|
||||
*/
|
||||
const CONTROL_FLOW_TYPES = new Set([
|
||||
"for_statement",
|
||||
"c_style_for_statement",
|
||||
"while_statement",
|
||||
"if_statement",
|
||||
"case_statement",
|
||||
"compound_statement",
|
||||
"function_definition",
|
||||
"negated_command",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Control-flow nodes that are pure body containers rather than statements in
|
||||
* their own right: `do …; done`, `elif`/`else` clauses, and `case` items.
|
||||
* They are descended exactly like {@link CONTROL_FLOW_TYPES} members but are
|
||||
* not emitted as whole units — nobody writes a command pattern against
|
||||
* `do …; done` or `a) find . ;;`, so emitting them only adds noise to prompts
|
||||
* and approval suggestions.
|
||||
*/
|
||||
const CONTROL_FLOW_CONTAINERS = new Set([
|
||||
"do_group",
|
||||
"elif_clause",
|
||||
"else_clause",
|
||||
"case_item",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Named children of a control-flow node that are statements in their own
|
||||
* right and get full command enumeration (whole-emit rules, wrapper
|
||||
* classification, substitution descent). Any other named child — words,
|
||||
* `variable_name`, a for-loop's value list, a case subject or pattern, a
|
||||
* `test_command` `[[ … ]]`, a comment — contributes only the nested
|
||||
* `$(…)`/`<(…)` executions it hosts.
|
||||
*/
|
||||
const CONTROL_FLOW_STATEMENT_TYPES = new Set([
|
||||
"command",
|
||||
"list",
|
||||
"pipeline",
|
||||
"redirected_statement",
|
||||
"subshell",
|
||||
...CONTROL_FLOW_TYPES,
|
||||
...CONTROL_FLOW_CONTAINERS,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Enumerate the command units of a bash program, in source order.
|
||||
*
|
||||
@@ -82,8 +139,19 @@ const COMMAND_ENUM_SKIP = new Set(["comment", "heredoc_end"]);
|
||||
* subshells (`( … )`) — emitting each inner command as its own unit *in
|
||||
* addition to* the enclosing command, since those inner commands really execute
|
||||
* (#306).
|
||||
* Control-flow bodies and `{ … }` brace groups are emitted whole without
|
||||
* descending (deferred).
|
||||
* Control-flow statements (loops, `if`/`elif`/`else`, `case`, `{ … }` groups,
|
||||
* function definitions, `!` negation) are likewise emitted whole and then
|
||||
* descended child-by-child: statement children (including nested control flow
|
||||
* and conditions like `if grep -q …; then`) are enumerated as commands, while
|
||||
* non-statement children (the for-loop value list, case subject/patterns,
|
||||
* `[[ … ]]` tests, names) contribute only the nested executions they host.
|
||||
* Pure body containers (`do …; done`, `elif`/`else` clauses, `case` items)
|
||||
* are descended without a whole-unit emit of their own.
|
||||
* Body commands keep the enclosing execution context — a loop inside `$(…)`
|
||||
* runs there, a top-level loop's body runs in the current shell.
|
||||
* Remaining named statements (`variable_assignment`, `declaration_command`,
|
||||
* `test_command`, …) emit whole and still report the nested executions they
|
||||
* host, so `x=$(grep foo f)` and `export X=$(find .)` gate the inner command.
|
||||
*
|
||||
* The enclosing command/subshell is always still emitted whole, so adding the
|
||||
* nested units can only ever produce a more-restrictive decision, never weaker.
|
||||
@@ -135,9 +203,57 @@ function collectCommandsInto(
|
||||
return;
|
||||
}
|
||||
|
||||
// Any other named statement (compound_statement `{ … }`, if/while/for/case,
|
||||
// function_definition): emit whole, do not descend — deferred (#306).
|
||||
if (CONTROL_FLOW_TYPES.has(node.type)) {
|
||||
out.push(makeUnit(node.text, context)); // never-weaker whole emit
|
||||
descendControlFlowChildren(node, context, out);
|
||||
return;
|
||||
}
|
||||
|
||||
if (CONTROL_FLOW_CONTAINERS.has(node.type)) {
|
||||
// Pure body container — enumerate its contents, emit no unit of its own.
|
||||
descendControlFlowChildren(node, context, out);
|
||||
return;
|
||||
}
|
||||
|
||||
// Any other named statement (variable_assignment, declaration_command,
|
||||
// test_command, …) keeps the whole-emit fallback, but a substitution in its
|
||||
// payload still really runs — `x=$(grep foo f)` or `export X=$(find .)` —
|
||||
// so hosted executions are enumerated in addition to the whole text.
|
||||
out.push(makeUnit(node.text, context));
|
||||
collectHostedCommands(node, out);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumerate inside a control-flow statement, keeping the enclosing context.
|
||||
*
|
||||
* Statement children (`command`, `list`, `pipeline`, `redirected_statement`,
|
||||
* `subshell`, or nested control flow such as a `do_group`)
|
||||
* get full enumeration. Other named children are scanned only for nested
|
||||
* executions: a `command_substitution` child directly (the `$(…)` in
|
||||
* `for f in $(…)` or `case $(…) in`) is a context node in its own right — the
|
||||
* strict-subtree {@link forEachNestedExecution} walk would not see it — so it
|
||||
* is descended with its own context, while words, `variable_name`, test and
|
||||
* expression subtrees go through the hosted-execution search.
|
||||
*/
|
||||
function descendControlFlowChildren(
|
||||
node: TSNode,
|
||||
context: BashCommandContext | undefined,
|
||||
out: BashCommand[],
|
||||
): void {
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const child = node.child(i);
|
||||
if (!child?.isNamed) continue;
|
||||
if (CONTROL_FLOW_STATEMENT_TYPES.has(child.type)) {
|
||||
collectCommandsInto(child, context, out);
|
||||
continue;
|
||||
}
|
||||
const nestedContext = NESTED_EXECUTION_CONTEXTS.get(child.type);
|
||||
if (nestedContext) {
|
||||
descendCommandChildren(child, nestedContext, out);
|
||||
} else {
|
||||
collectHostedCommands(child, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function makeUnit(
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
resetWarmBashParser,
|
||||
warmBashParser,
|
||||
} from "#src/access-intent/bash/parser";
|
||||
import { parseBashCommandsSync } from "#src/access-intent/bash/sync-commands";
|
||||
|
||||
describe("collectCommands — control-flow descent", () => {
|
||||
beforeEach(async () => {
|
||||
resetWarmBashParser();
|
||||
await warmBashParser();
|
||||
});
|
||||
afterEach(() => {
|
||||
resetWarmBashParser();
|
||||
});
|
||||
|
||||
it("emits a for-loop's body commands in addition to the whole statement", () => {
|
||||
expect(
|
||||
parseBashCommandsSync('for d in a b; do find "$d" -name x; done'),
|
||||
).toEqual([
|
||||
{ text: 'for d in a b; do find "$d" -name x; done' },
|
||||
{ text: 'find "$d" -name x' },
|
||||
]);
|
||||
});
|
||||
|
||||
it("emits a command substitution in the for value list as a nested context", () => {
|
||||
expect(
|
||||
parseBashCommandsSync('for f in $(find . -name x); do echo "$f"; done'),
|
||||
).toEqual([
|
||||
{ text: 'for f in $(find . -name x); do echo "$f"; done' },
|
||||
{ text: "find . -name x", context: "command_substitution" },
|
||||
{ text: 'echo "$f"' },
|
||||
]);
|
||||
});
|
||||
|
||||
it("emits the condition and body commands of a redirected while loop", () => {
|
||||
expect(
|
||||
parseBashCommandsSync('while read l; do grep x "$l"; done < list'),
|
||||
).toEqual([
|
||||
{ text: 'while read l; do grep x "$l"; done' },
|
||||
{ text: "read l" },
|
||||
{ text: 'grep x "$l"' },
|
||||
]);
|
||||
});
|
||||
|
||||
it("emits an if's condition, branch, and else commands", () => {
|
||||
expect(
|
||||
parseBashCommandsSync("if grep -q x f; then echo y; else rm z; fi"),
|
||||
).toEqual([
|
||||
{ text: "if grep -q x f; then echo y; else rm z; fi" },
|
||||
{ text: "grep -q x f" },
|
||||
{ text: "echo y" },
|
||||
{ text: "rm z" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("emits a case item's commands", () => {
|
||||
expect(parseBashCommandsSync("case $x in a) find . ;; esac")).toEqual([
|
||||
{ text: "case $x in a) find . ;; esac" },
|
||||
{ text: "find ." },
|
||||
]);
|
||||
});
|
||||
|
||||
it("emits a command substitution in the case subject as a nested context", () => {
|
||||
expect(
|
||||
parseBashCommandsSync("case $(rm x) in a) find . ;; esac"),
|
||||
).toEqual([
|
||||
{ text: "case $(rm x) in a) find . ;; esac" },
|
||||
{ text: "rm x", context: "command_substitution" },
|
||||
{ text: "find ." },
|
||||
]);
|
||||
});
|
||||
|
||||
it("descends a brace group", () => {
|
||||
expect(parseBashCommandsSync("{ grep x f; }")).toEqual([
|
||||
{ text: "{ grep x f; }" },
|
||||
{ text: "grep x f" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("descends a function definition's body", () => {
|
||||
expect(parseBashCommandsSync("f() { grep x f; }")).toEqual([
|
||||
{ text: "f() { grep x f; }" },
|
||||
{ text: "{ grep x f; }" },
|
||||
{ text: "grep x f" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("descends nested loops", () => {
|
||||
expect(
|
||||
parseBashCommandsSync("for a in 1; do for b in 2; do find .; done; done"),
|
||||
).toEqual([
|
||||
{ text: "for a in 1; do for b in 2; do find .; done; done" },
|
||||
{ text: "for b in 2; do find .; done" },
|
||||
{ text: "find ." },
|
||||
]);
|
||||
});
|
||||
|
||||
it("descends a c-style for loop", () => {
|
||||
expect(
|
||||
parseBashCommandsSync("for ((i=0;i<3;i++)); do echo i; done"),
|
||||
).toEqual([
|
||||
{ text: "for ((i=0;i<3;i++)); do echo i; done" },
|
||||
{ text: "echo i" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("emits no container units for a clean loop body", () => {
|
||||
expect(parseBashCommandsSync('for d in a b; do echo "$d"; done')).toEqual([
|
||||
{ text: 'for d in a b; do echo "$d"; done' },
|
||||
{ text: 'echo "$d"' },
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not emit a [[ ]] test as a separate command unit", () => {
|
||||
expect(parseBashCommandsSync("if [[ -f x ]]; then echo y; fi")).toEqual([
|
||||
{ text: "if [[ -f x ]]; then echo y; fi" },
|
||||
{ text: "echo y" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("catches a substitution hosted inside a [[ ]] test", () => {
|
||||
expect(
|
||||
parseBashCommandsSync('if [[ "$(rm x)" = y ]]; then echo y; fi'),
|
||||
).toEqual([
|
||||
{ text: 'if [[ "$(rm x)" = y ]]; then echo y; fi' },
|
||||
{ text: "rm x", context: "command_substitution" },
|
||||
{ text: "echo y" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectCommands — assignment, declaration, and negation", () => {
|
||||
beforeEach(async () => {
|
||||
resetWarmBashParser();
|
||||
await warmBashParser();
|
||||
});
|
||||
afterEach(() => {
|
||||
resetWarmBashParser();
|
||||
});
|
||||
|
||||
it("emits a substitution in a plain assignment value as a nested context", () => {
|
||||
expect(parseBashCommandsSync("x=$(grep foo f)")).toEqual([
|
||||
{ text: "x=$(grep foo f)" },
|
||||
{ text: "grep foo f", context: "command_substitution" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("emits a substitution in an export declaration value", () => {
|
||||
expect(parseBashCommandsSync("export X=$(find .)")).toEqual([
|
||||
{ text: "export X=$(find .)" },
|
||||
{ text: "find .", context: "command_substitution" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("emits a substitution in a local declaration inside a function body", () => {
|
||||
expect(parseBashCommandsSync("f() { local y=$(rg z); }")).toEqual([
|
||||
{ text: "f() { local y=$(rg z); }" },
|
||||
{ text: "{ local y=$(rg z); }" },
|
||||
{ text: "rg z", context: "command_substitution" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("emits a negated command's inner command as a normal unit", () => {
|
||||
expect(parseBashCommandsSync("! grep -q x f")).toEqual([
|
||||
{ text: "! grep -q x f" },
|
||||
{ text: "grep -q x f" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("emits a negated condition's inner command inside an if", () => {
|
||||
expect(parseBashCommandsSync("if ! grep -q x f; then :; fi")).toEqual([
|
||||
{ text: "if ! grep -q x f; then :; fi" },
|
||||
{ text: "! grep -q x f" },
|
||||
{ text: "grep -q x f" },
|
||||
{ text: ":" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -14,10 +14,13 @@ import { BashProgram } from "#src/access-intent/bash/program";
|
||||
import { resolveBashCommandCheck } from "#src/handlers/gates/bash-command";
|
||||
import { pathFlavorForPlatform } from "#src/path/path-flavor";
|
||||
import { PathNormalizer } from "#src/path-normalizer";
|
||||
import { PermissionResolver } from "#src/permission-resolver";
|
||||
import type { ScopedPermissionResolver } from "#src/permission-resolver";
|
||||
import type { PermissionState } from "#src/types";
|
||||
import { SessionRules } from "#src/session-rules";
|
||||
import type { DenyWithReason, PermissionState } from "#src/types";
|
||||
|
||||
import { makeCheckResult } from "#test/helpers/handler-fixtures";
|
||||
import { createInMemoryManager } from "#test/helpers/manager-harness";
|
||||
|
||||
/** Decision strength ordering: deny (2) > ask (1) > allow (0). */
|
||||
const STRENGTH: Record<PermissionState, number> = {
|
||||
@@ -146,3 +149,103 @@ describe("bash command gate — nested execution hosts do not weaken", () => {
|
||||
expect(await decide("cat <<'EOF'\n$(rm x)\nEOF", resolver)).toBe("allow");
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* The same totality property for control-flow bodies.
|
||||
*
|
||||
* A `for`/`while`/`if`/`case`/function was once emitted whole without
|
||||
* descending, so `for d in …; do find …; done` only ever matched patterns
|
||||
* starting with `for` — a `find*: deny` rule was bypassed. The enumerator now
|
||||
* emits the whole statement AND its body commands, so hosting a gated command
|
||||
* inside control flow cannot weaken its decision.
|
||||
*/
|
||||
describe("bash command gate — control-flow bodies do not weaken", () => {
|
||||
/** Real pattern matching over a bash map: `*` allow, the rest deny-with-reason. */
|
||||
function makePolicyResolver(
|
||||
bash: Record<string, PermissionState | DenyWithReason>,
|
||||
): ScopedPermissionResolver {
|
||||
const manager = createInMemoryManager({
|
||||
global: { permission: { "*": "allow", bash } },
|
||||
});
|
||||
return new PermissionResolver(manager, new SessionRules());
|
||||
}
|
||||
|
||||
async function checkWith(
|
||||
command: string,
|
||||
resolver: ScopedPermissionResolver,
|
||||
) {
|
||||
const program = await BashProgram.parse(
|
||||
command,
|
||||
new PathNormalizer(pathFlavorForPlatform(process.platform), "/cwd"),
|
||||
);
|
||||
return resolveBashCommandCheck(command, program.commands(), undefined, resolver);
|
||||
}
|
||||
|
||||
it("denies the reported repro: find inside a for-loop command substitution", async () => {
|
||||
const resolver = makePolicyResolver({
|
||||
"*": "allow",
|
||||
"find*": { action: "deny", reason: "use the find tool" },
|
||||
"grep*": { action: "deny", reason: "use the grep tool" },
|
||||
});
|
||||
|
||||
const result = await checkWith(
|
||||
'cd /Users/sia/code/my-pi && ls -1 extensions/ && echo "=== LOC by ext ===" && for d in pi-*/; do n=$(find "$d" -name \'*.ts\' -not -path \'*/node_modules/*\' -not -path \'*/dist/*\' -not -path \'*/test*\' | xargs wc -l 2>/dev/null | tail -1 | awk \'{print $1}\'); echo "$d $n"; done && echo "=== extensions ts ===" && wc -l extensions/*.ts | tail -3',
|
||||
resolver,
|
||||
);
|
||||
|
||||
expect(result.state).toBe("deny");
|
||||
expect(result.command).toBe(
|
||||
'find "$d" -name \'*.ts\' -not -path \'*/node_modules/*\' -not -path \'*/dist/*\' -not -path \'*/test*\'',
|
||||
);
|
||||
expect(result.matchedPattern).toBe("find*");
|
||||
expect(result.reason).toBe("use the find tool");
|
||||
expect(result.commandContext).toBe("command_substitution");
|
||||
});
|
||||
|
||||
it.each([
|
||||
'for d in a b; do find "$d" -name x; done',
|
||||
'while read l; do grep x "$l"; done < list',
|
||||
"if grep -q x f; then echo y; else echo n; fi",
|
||||
"case $x in a) find . ;; esac",
|
||||
"{ grep x f; }",
|
||||
"f() { grep x f; }",
|
||||
"for a in 1; do for b in 2; do find .; done; done",
|
||||
"x=$(grep foo f)",
|
||||
"export X=$(find .)",
|
||||
"f() { local y=$(rg z); }",
|
||||
"! grep -q x f",
|
||||
"if ! grep -q x f; then :; fi",
|
||||
])("denies a gated command inside `%s`", async (command) => {
|
||||
const resolver = makePolicyResolver({
|
||||
"*": "allow",
|
||||
"find*": "deny",
|
||||
"grep*": "deny",
|
||||
"rg*": "deny",
|
||||
});
|
||||
|
||||
expect((await checkWith(command, resolver)).state).toBe("deny");
|
||||
});
|
||||
|
||||
it("denies a hard-guard payload hidden in a loop body", async () => {
|
||||
const resolver = makePolicyResolver({
|
||||
"*": "allow",
|
||||
"rm -rf*": "deny",
|
||||
});
|
||||
|
||||
const result = await checkWith("for f in a; do rm -rf /; done", resolver);
|
||||
expect(result.state).toBe("deny");
|
||||
expect(result.matchedPattern).toBe("rm -rf*");
|
||||
});
|
||||
|
||||
it("allows a control-flow statement whose body commands are allowed", async () => {
|
||||
const resolver = makePolicyResolver({
|
||||
"*": "allow",
|
||||
"find*": "deny",
|
||||
"grep*": "deny",
|
||||
});
|
||||
|
||||
expect(
|
||||
(await checkWith('for d in a b; do echo "$d"; done', resolver)).state,
|
||||
).toBe("allow");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
autoReviewConfigFileSchema,
|
||||
normalizeAutoReviewConfig,
|
||||
} from "#src/auto-review/config";
|
||||
import { stripJsonComments } from "#src/config-loader";
|
||||
import { unifiedConfigSchema } from "#src/config-schema";
|
||||
|
||||
/** The bundle's authoritative policy seed, as shipped. */
|
||||
@@ -16,10 +17,10 @@ const SEED_PATH = fileURLToPath(
|
||||
|
||||
function readSeed(): Record<string, unknown> {
|
||||
const raw = readFileSync(SEED_PATH, "utf8");
|
||||
const stripped = raw
|
||||
.replace(/^\s*\/\/.*$/gm, "")
|
||||
.replace(/\/\*[\s\S]*?\*\//g, "");
|
||||
return JSON.parse(stripped) as Record<string, unknown>;
|
||||
// Parse with the same string-aware stripper the loader uses: a naive regex
|
||||
// strips `/*`-looking runs inside string values (e.g. a `'src/**/*.ts'`
|
||||
// glob in a deny reason) and corrupts the JSON.
|
||||
return JSON.parse(stripJsonComments(raw)) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
describe("bundled policy seed", () => {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
## Unreleased — my-pi maintained source
|
||||
|
||||
### Changed
|
||||
- Removed `multi_grep` from the `filesystem-navigation` group: the bundle wrapper keeps FFF's upstream `PI_FFF_MULTIGREP` opt-in disabled, so the tool is never registered and the group now covers only `ls`.
|
||||
- Pinned Pi's built-in `codemode` (exposure `model-only`) in `alwaysEnabled` so batching several tool calls through one script never depends on a `tool_search` activation; `platformToolPolicy` now also drops tools with `hidden` or `codemode` (script-only) exposure so they can never enter the catalog or the unknown-tools path.
|
||||
- Removed the `PI_TOOL_SEARCH_CONFIG_PATH` environment override; the extension reads the bundle `config/tool-search.json`, and tests inject a path through the `createToolSearchExtension({ configPath })` factory.
|
||||
- Removed the retired context-mode integration: the `ctx_execute` / `ctx_execute_file` / `ctx_batch_execute` pins left `alwaysEnabled` and the four `context-*` bundle groups were dropped. Large tool results are now bounded by pi-overflow and recoverable through the pinned `overflow_query`.
|
||||
|
||||
@@ -26,7 +26,7 @@ Run `/tool-search-status` to inspect the current catalog source, pinned count, d
|
||||
|
||||
| Group | Bundle tools |
|
||||
|---|---|
|
||||
| `filesystem-navigation` | `ls`, `multi_grep` |
|
||||
| `filesystem-navigation` | `ls` |
|
||||
| `ssh-connection` | `ssh_connect` |
|
||||
| `ssh-remote-files` | `ssh_read`, `ssh_write`, `ssh_edit` |
|
||||
| `ssh-remote-search` | `ssh_find`, `ssh_grep` |
|
||||
|
||||
@@ -25,11 +25,11 @@ export const BUNDLE_GROUP_DEFINITIONS: BundleGroupDefinition[] = [
|
||||
{
|
||||
id: "filesystem-navigation",
|
||||
title: "Filesystem navigation",
|
||||
summary: "Inspect directory layouts and run bounded multi-pattern literal searches.",
|
||||
useWhen: ["You need directory entries or several literal searches after narrowing a path"],
|
||||
summary: "Inspect directory layouts.",
|
||||
useWhen: ["You need the alphabetical directory entries of an unknown path"],
|
||||
avoidWhen: ["Code relationships or types are the real question"],
|
||||
keywords: ["files", "directory", "list", "multi grep", "目录", "文件", "多模式搜索"],
|
||||
tools: ["ls", "multi_grep"],
|
||||
keywords: ["files", "directory", "list", "目录", "文件"],
|
||||
tools: ["ls"],
|
||||
},
|
||||
{
|
||||
id: "ssh-connection",
|
||||
|
||||
@@ -16,7 +16,10 @@ import {
|
||||
import { CONFIG_PATH as RTK_CONFIG_PATH } from "../pi-rtk-optimizer/src/constants.ts";
|
||||
import { CONFIG_PATH as COMPACTION_CONFIG_PATH } from "../pi-compaction/src/config.ts";
|
||||
import { WORKTREE_CONFIG_PATH } from "../pi-worktree/src/settings.ts";
|
||||
import { resolvePreamblePath } from "../extensions/system-prompt-config.ts";
|
||||
import {
|
||||
resolvePreamblePath,
|
||||
stripJsoncComments,
|
||||
} from "../extensions/system-prompt-config.ts";
|
||||
|
||||
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const bundleConfigDir = join(repositoryRoot, "config");
|
||||
@@ -48,9 +51,9 @@ const BUNDLE_CONFIG_FILES = [
|
||||
"p10k.zsh",
|
||||
];
|
||||
|
||||
/** Strip JSONC comments (line and block) — same approach as the seed test. */
|
||||
/** Strip JSONC comments (line and block) without touching string literals. */
|
||||
function stripJsonc(raw: string): string {
|
||||
return raw.replace(/^\s*\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
||||
return stripJsoncComments(raw);
|
||||
}
|
||||
|
||||
test("every bundle config file exists and is non-empty", async () => {
|
||||
|
||||
@@ -343,4 +343,12 @@ test("all package extensions load together without global registration conflicts
|
||||
const codemode = probe.all.find((tool) => tool.name === "codemode");
|
||||
assert.equal(codemode?.exposure, "model-only", "Pi's built-in codemode must load with model-only exposure");
|
||||
assert.ok(probe.active.includes("codemode"), "Tool Search must pin codemode into the active set");
|
||||
assert.ok(
|
||||
probe.all.every((tool) => tool.name !== "multi_grep"),
|
||||
"FFF's upstream-flagged multi_grep must stay unregistered (fff-override deletes PI_FFF_MULTIGREP)",
|
||||
);
|
||||
assert.ok(
|
||||
!probe.active.includes("multi_grep"),
|
||||
"multi_grep must not appear in the active tool set",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -255,7 +255,7 @@ test("bundled routing rules select the same guidance the retired tool-routing ex
|
||||
const routing = loadRoutingRules();
|
||||
|
||||
const literal = renderRouting(routing.rules, ["find", "grep", "read"]);
|
||||
assert.match(literal, /Use find to identify likely files or directories/);
|
||||
assert.match(literal, /Pick the local search tool by what you already know/);
|
||||
assert.match(literal, /narrow the path, glob, or pattern instead of increasing the limit/);
|
||||
|
||||
const ssh = renderRouting(routing.rules, ["ssh_connect", "ssh_cd", "ssh_bash"]);
|
||||
@@ -267,7 +267,7 @@ test("bundled routing rules select the same guidance the retired tool-routing ex
|
||||
|
||||
const minimal = renderRouting(routing.rules, ["read"]);
|
||||
assert.doesNotMatch(minimal, /codegraph_explore first/);
|
||||
assert.doesNotMatch(minimal, /For literal search/);
|
||||
assert.doesNotMatch(minimal, /Pick the local search tool/);
|
||||
assert.doesNotMatch(minimal, /ssh_connect only when/);
|
||||
assert.match(minimal, /Stop searching once the current evidence is sufficient/);
|
||||
});
|
||||
@@ -397,7 +397,6 @@ test("composer splices the preamble, applies the catalog, and orders sections",
|
||||
read: "reads files",
|
||||
bash: "runs commands",
|
||||
edit: "edits",
|
||||
codegraph_explore: "explores",
|
||||
tool_search: "searches",
|
||||
},
|
||||
toolGuidelines: { edit: ["upstream edit rule"] },
|
||||
@@ -414,6 +413,11 @@ test("composer splices the preamble, applies the catalog, and orders sections",
|
||||
"the bundle preamble replaces Pi's default preamble",
|
||||
);
|
||||
assert.match(output, /<tool_routing>\n- Use codegraph_explore first/);
|
||||
assert.match(
|
||||
output,
|
||||
/<tools>[\s\S]*?- codegraph_explore: Explore code structure from the CodeGraph index/,
|
||||
"tools.jsonc must add a snippet for codegraph_explore, which has no upstream <tools> entry",
|
||||
);
|
||||
assert.match(output, /search files with the find\/grep tools, not bash/);
|
||||
assert.match(output, /LINE#HASH anchors/);
|
||||
assert.ok(
|
||||
|
||||
Reference in New Issue
Block a user