feat(prompt): 重新注册 bash,把"不要用 bash 搜索"写进工具描述

真实会话里模型仍习惯性先调 bash grep,被拒后才改用 grep 工具;系统提示词里已有 4 处
提醒,但模型下调用决定时看的是工具 schema 描述,而 Pi 内置 bash 描述对此只字未提。

- 新增 extensions/bash-override.ts:用 Pi 的 createBashToolDefinition 重新注册 bash,
  执行、输出结构与 codemode 说明完全沿用上游;shellPath/commandPrefix 按 ctx.cwd
  经 SettingsManager 惰性解析,与内置一致
- tools.jsonc 新增 descriptionAppend 字段,由 prompt-guard 注册时拼接到上游描述后
- bash 增加命令→工具对照规则(grep -n / grep -rl / find -name),并提示
  文件清单用 $(git ls-files) 传参、不要接 xargs(会触发权限审核)

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
云服务部-叶林立
2026-10-22 01:20:52 -07:00
co-authored by Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent 2b21163a18
commit 87940da51a
11 changed files with 226 additions and 28 deletions
+48 -2
View File
@@ -21,6 +21,7 @@ import {
stripJsoncComments,
validateRoutingConfig,
validateSectionsConfig,
validateToolCatalog,
type MutablePromptOptions,
type RoutingRule,
} from "../extensions/system-prompt-config.ts";
@@ -28,7 +29,7 @@ import systemPromptExtension, {
createSystemPromptComposer,
getSystemPromptDumpPath,
} from "../extensions/system-prompt.ts";
import { guardExtensionApi } from "../extensions/prompt-guard.ts";
import { catalogDescription, guardExtensionApi } from "../extensions/prompt-guard.ts";
import { BUNDLE_GROUP_DEFINITIONS } from "../pi-tool-search/extensions/bundle-groups.ts";
import { BUNDLE_TOOL_SEARCH_DEFAULTS } from "../pi-tool-search/extensions/config.ts";
@@ -93,6 +94,20 @@ test("routing scope validation: main/subagent accepted, other values rejected",
);
});
test("tools.jsonc validation: descriptionAppend accepted as a string, rejected otherwise", () => {
assert.doesNotThrow(() =>
validateToolCatalog({ version: 1, tools: { bash: { descriptionAppend: "extra" } } }),
);
assert.throws(
() => validateToolCatalog({ version: 1, tools: { bash: { descriptionAppend: 5 } } }),
/descriptionAppend must be a string/,
);
assert.throws(
() => validateToolCatalog({ version: 1, tools: { bash: { descriptionAppend: true } } }),
/descriptionAppend must be a string/,
);
});
test("sections.jsonc validation: subagentExclude defaults to [] and rejects bad shapes", () => {
assert.deepEqual(validateSectionsConfig({ version: 1, order: ["tool_routing"] }), {
version: 1,
@@ -608,7 +623,38 @@ test("prompt guard applies catalog description overrides at registration", () =>
const override = loadToolCatalog().tools.edit;
assert.ok(override && !("inherit" in override) && typeof override.description === "string");
assert.equal(tools[0].description, override.description);
assert.equal(tools[1].description, "upstream grep", "inherit entries keep the upstream description");
assert.equal(tools[1].description, "upstream grep", "entries without description fields keep the upstream description");
});
test("prompt guard appends the catalog descriptionAppend to the upstream description", () => {
const { pi, tools } = makeRecordingPi();
const guarded = guardExtensionApi(pi as unknown as ExtensionAPI);
guarded.registerTool({ name: "bash", description: "upstream bash" } as never);
guarded.registerTool({ name: "read", description: "upstream read" } as never);
const bashEntry = loadToolCatalog().tools.bash;
assert.ok(bashEntry && !("inherit" in bashEntry) && typeof bashEntry.descriptionAppend === "string");
assert.equal(tools[0].description, `upstream bash\n\n${bashEntry.descriptionAppend}`);
assert.equal(tools[1].description, "upstream read", "inherit entries keep the upstream description");
});
test("catalogDescription composes override, append, and inherit", () => {
assert.equal(
catalogDescription("upstream", { description: "override", descriptionAppend: "append" }),
"override\n\nappend",
"an append sits on top of the description override when both are set",
);
assert.equal(
catalogDescription("upstream \n", { descriptionAppend: "append" }),
"upstream\n\nappend",
"trailing whitespace on the base is trimmed before the blank line",
);
assert.equal(
catalogDescription("upstream", { inherit: true }),
"upstream",
"inherit entries are untouched",
);
});
test("prompt guard strips a returned systemPrompt but keeps other fields", async () => {