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
@@ -24,10 +24,10 @@ const emit = async (event, data = {}) => {
try {
process.env.PI_CODING_AGENT_DIR = root;
process.env.PI_HERMES_MEMORY_CONFIG_PATH = join(root, "hermes-memory-config.json");
const configPath = join(root, "hermes-memory-config.json");
await mkdir(globalDir);
await mkdir(ctx.cwd);
await writeFile(process.env.PI_HERMES_MEMORY_CONFIG_PATH, JSON.stringify({
await writeFile(configPath, JSON.stringify({
lazyInitialization: lazy, memoryMode: "policy-only",
reviewEnabled: false, correctionDetection: false,
flushOnShutdown: false, flushOnCompact: false,
@@ -40,7 +40,10 @@ try {
}
const started = performance.now();
const { default: register } = await import(pathToFileURL(resolve(entry)).href);
const module = await import(pathToFileURL(resolve(entry)).href);
const register = module.createHermesMemoryExtension
? module.createHermesMemoryExtension({ configPath })
: module.default;
const imported = performance.now();
register({
on(event, handler) {
+1 -10
View File
@@ -89,16 +89,7 @@ export const DEFAULT_CONFIG_PATH = fileURLToPath(
new URL("../../config/pi-hermes-memory.json", import.meta.url),
);
/**
* Resolve the effective config path. `PI_HERMES_MEMORY_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_HERMES_MEMORY_CONFIG_PATH ?? DEFAULT_CONFIG_PATH;
}
export function loadConfig(configPath = resolveConfigPath()): MemoryConfig {
export function loadConfig(configPath = DEFAULT_CONFIG_PATH): MemoryConfig {
try {
if (fs.existsSync(configPath)) {
const raw = fs.readFileSync(configPath, "utf-8");
+16 -3
View File
@@ -52,7 +52,7 @@ import { registerPreviewContextCommand } from "./handlers/preview-context.js";
import { registerStandingPinCommand } from "./handlers/standing-pin.js";
import { StandingInstructions } from "./store/standing-instructions.js";
import { STANDING_FILE } from "./constants.js";
import { loadConfig } from "./config.js";
import { DEFAULT_CONFIG_PATH, loadConfig } from "./config.js";
import { shouldWarnAutoConsolidationFailure } from "./auto-consolidation-warning.js";
import { detectProject, detectProjectSkills } from "./project.js";
import { buildPromptContext } from "./prompt-context.js";
@@ -90,8 +90,12 @@ export function registerProjectSkillDiscoveryHandler(
});
}
export default function (pi: ExtensionAPI) {
const config = loadConfig();
export interface HermesMemoryExtensionOptions {
configPath?: string;
}
function hermesMemoryExtension(pi: ExtensionAPI, configPath: string) {
const config = loadConfig(configPath);
const lazy = config.lazyInitialization === true && config.memoryMode === "policy-only";
let sessionContext: ExtensionContext | undefined;
@@ -480,3 +484,12 @@ export default function (pi: ExtensionAPI) {
}
});
}
export function createHermesMemoryExtension(
options?: HermesMemoryExtensionOptions,
): (pi: ExtensionAPI) => void {
const configPath = options?.configPath ?? DEFAULT_CONFIG_PATH;
return (pi) => hermesMemoryExtension(pi, configPath);
}
export default createHermesMemoryExtension();
+4 -6
View File
@@ -8,10 +8,10 @@ import { after, beforeEach, afterEach, describe, it } from "node:test";
// Bind the entry point to a disposable agent root, never the user's memory.
const root = await fs.mkdtemp(path.join(os.tmpdir(), "pi-lazy-startup-"));
const previousRoot = process.env.PI_CODING_AGENT_DIR;
const previousConfigPath = process.env.PI_HERMES_MEMORY_CONFIG_PATH;
process.env.PI_CODING_AGENT_DIR = root;
process.env.PI_HERMES_MEMORY_CONFIG_PATH = path.join(root, "hermes-memory-config.json");
const { default: registerExtension } = await import("../src/index.js");
const configPath = path.join(root, "hermes-memory-config.json");
const { createHermesMemoryExtension } = await import("../src/index.js");
const registerExtension = createHermesMemoryExtension({ configPath });
const { MemoryStore } = await import("../src/store/memory-store.js");
const { DatabaseManager } = await import("../src/store/db.js");
const globalDir = path.join(root, "pi-hermes-memory");
@@ -24,7 +24,7 @@ let notifications: string[];
let ctx: any;
async function configure(overrides: Record<string, unknown> = {}) {
await fs.writeFile(path.join(root, "hermes-memory-config.json"), JSON.stringify({
await fs.writeFile(configPath, JSON.stringify({
lazyInitialization: true,
reviewEnabled: false, correctionDetection: false,
flushOnCompact: false, flushOnShutdown: false,
@@ -87,8 +87,6 @@ afterEach(async () => {
after(async () => {
if (previousRoot === undefined) delete process.env.PI_CODING_AGENT_DIR;
else process.env.PI_CODING_AGENT_DIR = previousRoot;
if (previousConfigPath === undefined) delete process.env.PI_HERMES_MEMORY_CONFIG_PATH;
else process.env.PI_HERMES_MEMORY_CONFIG_PATH = previousConfigPath;
await fs.rm(root, { recursive: true, force: true });
});
@@ -18,11 +18,11 @@ describe("session project memory rebinding", () => {
const launchDir = await fs.mkdtemp(path.join(os.tmpdir(), "pi-project-rebind-launch-"));
const targetDir = await fs.mkdtemp(path.join(os.tmpdir(), "pi-project-rebind-target-"));
const previousAgentRoot = process.env.PI_CODING_AGENT_DIR;
const previousConfigPath = process.env.PI_HERMES_MEMORY_CONFIG_PATH;
const configPath = path.join(agentRoot, "hermes-memory-config.json");
const previousCwd = process.cwd();
try {
await fs.writeFile(
path.join(agentRoot, "hermes-memory-config.json"),
configPath,
JSON.stringify({
memoryMode: "legacy-inject",
reviewEnabled: false,
@@ -47,10 +47,10 @@ describe("session project memory rebinding", () => {
);
process.env.PI_CODING_AGENT_DIR = agentRoot;
process.env.PI_HERMES_MEMORY_CONFIG_PATH = path.join(agentRoot, "hermes-memory-config.json");
process.chdir(launchDir);
// Import after setting PI_CODING_AGENT_DIR so AGENT_ROOT is test-local.
const { default: registerExtension } = await import("../src/index.js");
const { createHermesMemoryExtension } = await import("../src/index.js");
const registerExtension = createHermesMemoryExtension({ configPath });
const mockPi: MockPi = {
handlers: {},
tools: {},
@@ -103,8 +103,6 @@ describe("session project memory rebinding", () => {
process.chdir(previousCwd);
if (previousAgentRoot === undefined) delete process.env.PI_CODING_AGENT_DIR;
else process.env.PI_CODING_AGENT_DIR = previousAgentRoot;
if (previousConfigPath === undefined) delete process.env.PI_HERMES_MEMORY_CONFIG_PATH;
else process.env.PI_HERMES_MEMORY_CONFIG_PATH = previousConfigPath;
await fs.rm(agentRoot, { recursive: true, force: true });
await fs.rm(launchDir, { recursive: true, force: true });
await fs.rm(targetDir, { recursive: true, force: true });
@@ -24,10 +24,10 @@ import { after, beforeEach, describe, it } from "node:test";
// Bind the entry point to a disposable agent root, never the user's memory.
const root = await fs.mkdtemp(path.join(os.tmpdir(), "pi-prompt-injection-"));
const previousRoot = process.env.PI_CODING_AGENT_DIR;
const previousConfigPath = process.env.PI_HERMES_MEMORY_CONFIG_PATH;
process.env.PI_CODING_AGENT_DIR = root;
process.env.PI_HERMES_MEMORY_CONFIG_PATH = path.join(root, "hermes-memory-config.json");
const { default: registerExtension } = await import("../src/index.js");
const configPath = path.join(root, "hermes-memory-config.json");
const { createHermesMemoryExtension } = await import("../src/index.js");
const registerExtension = createHermesMemoryExtension({ configPath });
const globalDir = path.join(root, "pi-hermes-memory");
const cwd = path.join(root, "workspace");
@@ -68,7 +68,7 @@ let handlers: Record<string, Array<(event: any, ctx: any) => any>>;
let ctx: any;
async function configure(overrides: Record<string, unknown> = {}) {
await fs.writeFile(path.join(root, "hermes-memory-config.json"), JSON.stringify({
await fs.writeFile(configPath, JSON.stringify({
memoryMode: "policy-only",
memoryPolicyStyle: "full",
lazyInitialization: true,
@@ -115,8 +115,6 @@ beforeEach(async () => {
after(async () => {
if (previousRoot === undefined) delete process.env.PI_CODING_AGENT_DIR;
else process.env.PI_CODING_AGENT_DIR = previousRoot;
if (previousConfigPath === undefined) delete process.env.PI_HERMES_MEMORY_CONFIG_PATH;
else process.env.PI_HERMES_MEMORY_CONFIG_PATH = previousConfigPath;
try {
await fs.rm(root, { recursive: true, force: true });
} catch {