feat: 接入 Pi 内置 codemode,并修复全面审查发现的问题

- codemode:Tool Search 常驻(alwaysEnabled)、权限 allow(嵌套调用仍逐个过权限门)、
  tools.jsonc 登记使用规则;RTK 跳过 parentToolCallId 嵌套调用,避免丢失 bash
  structuredContent;Tool Search 目录过滤 hidden / codemode(script-only)exposure;
  扩展加载测试加载 builtin:codemode 并断言其常驻激活
- 搜索 key 不再经命令行参数传递:search-auth.mjs 改为 --set-stdin(先读校验再加锁),
  configure.sh 交互输入经管道、非交互用 --<provider> -
- CodeGraph 客户端监听子进程 stdin error,避免 EPIPE 未处理导致崩溃
- 移除 PI_TOOL_SEARCH/HERMES_MEMORY/RTK_CONFIG_PATH 环境变量入口,测试改用工厂 configPath 注入
- pi-subagents 固定 vitest / coverage-istanbul 4.1.10,修复 npm ls 版本冲突
- 补 pi-web-search / pi-worktree UPSTREAM.md 与 pi-minimal-footer AGENTS.md,
  files 白名单补齐并加测试;.codegraph/.gitignore 移出跟踪
- detect_kitty 优先识别官方安装目录

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-21 23:18:34 -07:00
co-authored by Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent 56cbc83296
commit f9a7f743dd
46 changed files with 585 additions and 198 deletions
+1
View File
@@ -5,6 +5,7 @@
- RTK 默认只压缩非搜索输出,包括本地 `bash` 与远端 `ssh_bash` 的 ANSI 清理、测试聚合、构建过滤、Git 压缩、Lint 聚合;兜底截断默认关闭,新结果的展示截断由 pi-overflow 统一负责;`ssh_bash` 只复用输出处理,远端命令不得进入 RTK rewrite。
- RTK 命令改写默认关闭(`commandRewritingEnabled` 默认 `false`);默认安装不依赖系统 `rtk` CLI,只有用户主动开启命令改写时才需要 `rtk rewrite`,其命令支持策略由已安装的 `rtk rewrite` 决定,不在扩展内重复维护规则。
- FFF override 独占搜索工具与搜索输出;RTK 不得处理本地或远端搜索工具的调用/结果(含 `grep` 工具结果),也不得通过默认命令改写接管 `rg`、`grep`、`find`、`fd`。
- RTK 不处理带 `parentToolCallId` 的嵌套调用(codemode 脚本内调用),否则会丢失 bash 的 structuredContent。
- `readCompaction.enabled`、`sourceCodeFilteringEnabled` 和 `smartTruncate.enabled` 当前均默认 `false`,源码读取保持原样;未经用户明确决定,不因节省上下文而改变这些默认值。若以后开启 read 压缩,优先考虑 `readCompaction + smartTruncate`,源码过滤仍独立评估;必须保留精确 `offset/limit` 读取、短文件和行锚点的完整性。
- read 压缩属于有损能力。任何默认值调整都必须同时说明哪些内容可能被省略、精确读取如何恢复原文,以及对 `edit` 文本匹配和排障证据的影响。
- 工具输出压缩可能损失证据。排障和审计相关改动应优先保证原始输出可恢复,并覆盖锚点完整性和截断边界。
+5 -5
View File
@@ -1,6 +1,6 @@
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
import { dirname } from "node:path";
import { resolveConfigPath } from "./constants.js";
import { CONFIG_PATH } from "./constants.js";
import { toRecord } from "./record-utils.js";
import {
DEFAULT_RTK_INTEGRATION_CONFIG,
@@ -151,7 +151,7 @@ export function normalizeRtkIntegrationConfig(raw: unknown): RtkIntegrationConfi
};
}
export function ensureConfigExists(configPath = resolveConfigPath()): EnsureConfigResult {
export function ensureConfigExists(configPath = CONFIG_PATH): EnsureConfigResult {
if (existsSync(configPath)) {
return { created: false };
}
@@ -169,7 +169,7 @@ export function ensureConfigExists(configPath = resolveConfigPath()): EnsureConf
}
}
export function loadRtkIntegrationConfig(configPath = resolveConfigPath()): ConfigLoadResult {
export function loadRtkIntegrationConfig(configPath = CONFIG_PATH): ConfigLoadResult {
if (!existsSync(configPath)) {
return { config: structuredClone(DEFAULT_RTK_INTEGRATION_CONFIG) };
}
@@ -189,7 +189,7 @@ export function loadRtkIntegrationConfig(configPath = resolveConfigPath()): Conf
export function saveRtkIntegrationConfig(
config: RtkIntegrationConfig,
configPath = resolveConfigPath(),
configPath = CONFIG_PATH,
): ConfigSaveResult {
const normalized = normalizeRtkIntegrationConfig(config);
const tmpPath = `${configPath}.tmp`;
@@ -218,6 +218,6 @@ export function saveRtkIntegrationConfig(
}
}
export function getRtkIntegrationConfigPath(configPath = resolveConfigPath()): string {
export function getRtkIntegrationConfigPath(configPath = CONFIG_PATH): string {
return configPath;
}
-8
View File
@@ -9,11 +9,3 @@ export const EXTENSION_NAME = "pi-rtk-optimizer";
export const CONFIG_PATH = fileURLToPath(
new URL("../../config/rtk-optimizer.json", import.meta.url),
);
/**
* Resolve the effective config path. `PI_RTK_CONFIG_PATH` relocates the file
* (tests point it at a temp file; it is never a persisted config value).
*/
export function resolveConfigPath(): string {
return process.env.PI_RTK_CONFIG_PATH ?? CONFIG_PATH;
}
+65 -3
View File
@@ -36,16 +36,15 @@ mock.module("@earendil-works/pi-tui", {
const indexModule = await import("./index.ts");
const { createBoundedNoticeTracker, shouldInjectSourceFilterTroubleshootingNote } = indexModule;
const rtkIntegrationExtension = indexModule.default;
const { DEFAULT_RTK_INTEGRATION_CONFIG } = await import("./types.ts");
// Tests must never write the bundle's config/ file: relocate the config to a
// throwaway temp file before any config-store call resolves its path.
// throwaway temp file via the extension factory's configPath option.
const TEST_CONFIG_PATH = join(
mkdtempSync(join(tmpdir(), "my-pi-rtk-test-")),
"rtk-optimizer.json",
);
process.env.PI_RTK_CONFIG_PATH = TEST_CONFIG_PATH;
const rtkIntegrationExtension = indexModule.createRtkOptimizerExtension({ configPath: TEST_CONFIG_PATH });
function writeTestConfig(commandRewritingEnabled: boolean): void {
writeFileSync(
@@ -401,6 +400,69 @@ await runTest("tool_result lifecycle merges compaction metadata with existing de
assert.equal(notifications.length, 0);
});
await runTest("nested tool calls and results (parentToolCallId) are left untouched", async () => {
writeTestConfig(true);
const handlers: Record<string, ExtensionHandler> = {};
const notifications: Notification[] = [];
let rewriteCalls = 0;
rtkIntegrationExtension({
exec: async (_command: string, args: string[]) => {
if (args[0] === "--version") {
return { code: 0, stdout: "rtk 1.0.0", stderr: "" };
}
if (args[0] === "rewrite") {
rewriteCalls += 1;
return { code: 3, stdout: "rtk git status", stderr: "" };
}
return { code: 1, stdout: "", stderr: "unexpected" };
},
on(eventName: string, handler: ExtensionHandler) {
handlers[eventName] = handler;
},
registerCommand() {},
} as never);
const toolCallHandler = handlers.tool_call;
const toolResultHandler = handlers.tool_result;
assert.ok(toolCallHandler);
assert.ok(toolResultHandler);
// A codemode script's nested bash result must survive intact: rewriting
// content without structuredContent would drop bash's outputSchema payload.
const nestedResult = await toolResultHandler(
{
toolName: "bash",
parentToolCallId: "codemode-1",
input: { command: "printf TODO" },
content: [{ type: "text", text: "\x1B[31msrc/a.ts\n 1: TODO\x1B[0m\n" }],
},
createNotificationContext(notifications),
);
assert.ok(!nestedResult || !("content" in nestedResult));
const nestedCall = {
toolName: "bash",
parentToolCallId: "codemode-1",
input: { command: "git status" },
};
await toolCallHandler(nestedCall, createNotificationContext(notifications));
assert.equal(nestedCall.input.command, "git status");
assert.equal(rewriteCalls, 0);
const directResult = await toolResultHandler(
{
toolName: "bash",
input: { command: "printf TODO" },
content: [{ type: "text", text: "\x1B[31msrc/a.ts\n 1: TODO\x1B[0m\n" }],
},
createNotificationContext(notifications),
);
assert.ok(directResult && "content" in directResult);
assert.equal(firstText(directResult.content), "src/a.ts\n 1: TODO\n");
writeTestConfig(false);
});
await runTest("tool_call surfaces RTK rewrite errors through existing UI warning path", async () => {
writeTestConfig(true);
const handlers: Record<string, (event: Record<string, unknown>, ctx: Record<string, unknown>) => Promise<Record<string, unknown> | void>> = {};
+28 -7
View File
@@ -8,7 +8,7 @@ import {
} from "./config-store.js";
import { computeRewriteDecision } from "./command-rewriter.js";
import { registerRtkIntegrationCommand } from "./command-register.js";
import { EXTENSION_NAME } from "./constants.js";
import { CONFIG_PATH, EXTENSION_NAME } from "./constants.js";
import { createLazyModuleLoader } from "./lazy-module-loader.js";
import { clearOutputMetrics, getOutputMetricsSummary } from "./output-metrics.js";
import type { ToolResultCompactionMetadata } from "./output-compactor.js";
@@ -103,8 +103,12 @@ export function createBoundedNoticeTracker(maxEntries: number): BoundedNoticeTra
};
}
export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
const initialLoad = loadRtkIntegrationConfig();
export interface RtkOptimizerExtensionOptions {
configPath?: string;
}
function rtkIntegrationExtension(pi: ExtensionAPI, configPath: string): void {
const initialLoad = loadRtkIntegrationConfig(configPath);
let config: RtkIntegrationConfig = initialLoad.config;
if (!config.enabled) {
return;
@@ -200,12 +204,12 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
};
const refreshConfig = async (ctx?: ExtensionContext | ExtensionCommandContext): Promise<void> => {
const ensured = ensureConfigExists();
const ensured = ensureConfigExists(configPath);
if (ensured.error && ctx) {
warnOnce(ctx, ensured.error);
}
const loaded = loadRtkIntegrationConfig();
const loaded = loadRtkIntegrationConfig(configPath);
config = loaded.config;
pendingLoadWarning = loaded.warning;
await refreshRuntimeStatus();
@@ -218,7 +222,7 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
const setConfig = (next: RtkIntegrationConfig, ctx: ExtensionCommandContext): void => {
config = normalizeRtkIntegrationConfig(next);
const saved = saveRtkIntegrationConfig(config);
const saved = saveRtkIntegrationConfig(config, configPath);
if (!saved.success && saved.error) {
ctx.ui.notify(saved.error, "error");
}
@@ -310,7 +314,7 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
const controller = {
getConfig: () => config,
setConfig,
getConfigPath: getRtkIntegrationConfigPath,
getConfigPath: () => getRtkIntegrationConfigPath(configPath),
getRuntimeStatus: () => runtimeStatus,
refreshRuntimeStatus,
getMetricsSummary: getOutputMetricsSummary,
@@ -396,6 +400,10 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
});
pi.on("tool_call", async (event, ctx) => {
if (event.parentToolCallId) {
return {};
}
if (!config.enabled || !config.commandRewritingEnabled) {
return {};
}
@@ -455,6 +463,10 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
});
pi.on("tool_result", async (event, ctx) => {
if (event.parentToolCallId) {
return {};
}
if (!config.enabled || !config.outputCompaction.enabled) {
return {};
}
@@ -485,3 +497,12 @@ export default function rtkIntegrationExtension(pi: ExtensionAPI): void {
}
});
}
export function createRtkOptimizerExtension(
options?: RtkOptimizerExtensionOptions,
): (pi: ExtensionAPI) => void {
const configPath = options?.configPath ?? CONFIG_PATH;
return (pi) => rtkIntegrationExtension(pi, configPath);
}
export default createRtkOptimizerExtension();
+1
View File
@@ -76,6 +76,7 @@ declare module "@earendil-works/pi-coding-agent" {
input: Record<string, unknown>;
content: Array<Record<string, unknown>>;
details?: unknown;
parentToolCallId?: string;
}
export interface BashToolCallEvent {