这篇只记录我从第二版配置后变化,第一次安装 Pi 的佬友可以先看前两版。
Pi 从零安装到入门配置:Windows 实战教程 - 开发调优 - LINUX DO
Pi Windows 工作流第二版:从第一版配置升级到现在有哪些改变? - 开发调优 - LINUX DO
package 和 Pi 本体更新很快,升级前仍建议先看各自的 release 和 README。
本版编写基线:Pi 0.84.1
变动范围与原因
继第二版发布继续使用一段时间后,又发现了一些实际影响工作流的问题:
- 压缩扩展会同时处理同一个 hook
- LSP 的默认格式化选择和项目不一致,且 LSP 过重拖慢 Pi 加载与 Agent 工作
- Plan 和 subagents协调下的普通子代理角色又可能拥有写入能力的问题
- MCP 还经历了从其他 CLI 迁移、连接生命周期误判和 SDK 版本不兼容
于是乎借着 Pi 0.84.0 更新,继续记录一波现在的配置,没有被本版提到的包继续沿用第二版配置。
上下文与压缩
移除了 pi-observational-memory 和 nano-context。
前者 pi-observational-memory 与 pi-ultra-compact 都注册 session_before_compact,后加载的自定义压缩结果可能覆盖前一个结果;而且从其他佬友体验和设计上来说它偏向长期记忆会导致上下文污染,衡量过后我还是用 pi-ultra-compact 吧,毕竟我的工作流里不需要长期记忆,只需要一个上下文压缩。
后者 nano-context 只提供额外的 context segmented bar,Pi 0.84 的 fullscreen 和现有 Footer 已经能显示我需要的状态,因此一并移除。
最终我现在的 Pi 最终保留了三层职责。
- Pi 原生 compaction:决定何时压缩以及为输出保留多少上下文
context-mode 负责把大段工具结果放到可回读的外部索引
pi-ultra-compact 提供 /ultracompact 和分级预压缩。
这样每个组件处理不同的数据,不再让两个 memory 扩展同时改写同一份压缩结果。
Pi 0.84 的 TUI 与 Markdown
原生能力
Pi 0.84 完善了 TUI 与 Markdown的原生能力,相关设置:
{
"tuiMode": "fullscreen",
"fullscreenScrollbar": "auto",
"markdown": {
"mermaid": "final",
"codeBlockIndent": " "
}
}
fullscreen 现在由 Pi 负责布局、滚动条和固定位置组件,代码块使用两个空格的缩进。
同时 Pi 0.84 修复了 Windows 下 Shift+Enter 的检测,支持最终消息中的 Mermaid 和终端内 LaTeX,并增加了 Ctrl+X:普通 transcript 复制最近一条 assistant 消息,/tree 复制当前选中的消息。
经过我实测,Pi 之前频繁出现反复滚动和终端跳顶 BUG ,随着 0.84 的完善终于解决了
键位
顺手分享一下我的快捷键设置:
当前 keybindings.json
{
"tui.editor.cursorUp": ["up", "ctrl+p", "alt+k"],
"tui.editor.cursorDown": ["down", "ctrl+n", "alt+j"],
"tui.editor.cursorLeft": ["left", "ctrl+b", "alt+h"],
"tui.editor.cursorRight": ["right", "ctrl+f", "alt+l"],
"tui.editor.cursorWordLeft": ["alt+left", "alt+b"],
"tui.editor.cursorWordRight": ["alt+right", "alt+f", "alt+w"],
"tui.editor.deleteCharForward": ["delete", "ctrl+d"],
"tui.editor.deleteCharBackward": ["backspace", "ctrl+h"],
"tui.editor.deleteWordBackward": ["ctrl+w", "alt+backspace"],
"tui.input.newLine": ["shift+enter", "ctrl+j", "alt+enter"]
}
Zentui 和 tool display 的边界
主题这方面,每个人的审美不一样我就不放了,主要说一下我遇到的一些坑:
pi-tool-display 和 pi-di18n 都会默认注册内置工具名,直接同时加载会报 Tool read/edit/write/bash conflicts。
最终我跟 TUI、主题、美化相关的配置分层:
pi-di18n 保留 read/edit/write/bash 的中文化
- tool-display 只接管
grep/find/ls 和摘要展示。
pi-zentui 负责 editor、用户消息、Starship Footer、颜色、图标和扩展状态,
- Pi 负责布局、滚动条和固定位置组件。
pi-markdown-preview 提供浏览器、PDF 和独立预览。
LSP 从 pi-lens 换成 pi-lsp
我最初安装 pi-lens,是想让 Pi 的 Python 诊断和 VS Code 使用同一套 Black/Pyright 习惯。实际使用时,它的 LSP 会在 Pyright/Jedi 间回退,formatter 选择偏向 Ruff,Black 的发现路径也受项目虚拟环境限制
我能在项目配置里调整格式化时机,却不能可靠地把全局服务器和 formatter 优先级改成自己的组合
而且用下来发现 pi-lens 还负责格式化行为,导致整个 Pi 和 agents 工作流程过程中非常的慢,包括加载慢,而且 agents 还会左右脑互博,我要求用最小代码修改原则修改的时候,它会把自动格式化行为当成不必要变动硬要去掉,然后反复陷入死循环。
最终我改用 @narumitw/pi-lsp 。
pi-lsp 的职责是按文件扩展名路由语言服务器,提供 lsp_diagnostics 和 lsp_fix;它已经移除 lsp_format,所以格式化仍由项目的 Black、Prettier 等命令负责。配置优先级是受信任项目的 .pi/pi-lsp.json、用户级 ~/.pi/agent/pi-lsp.json、内置默认值;显式配置会替换内置 server map,新增语言时要在同一个文件里补齐需要的 server。
当前 pi-lsp.json
{
"timeout": 30000,
"servers": {
"ruff": {
"command": ["ruff", "server"],
"extensions": [".py", ".pyi"],
"skipDirectories": [
".git",
".mypy_cache",
".pytest_cache",
".ruff_cache",
".venv",
"__pycache__",
"node_modules"
]
}
}
}
MCP 更新
最近 MCP 2026-07-28 文档 更新了 MCP 协议配置,正好借着 MCP 协议更新把相关的MCP/配置/插件更新一下,给 Context7 和 DeepWiki 这两个 HTTP server 增加 protocolVersion: "auto",让 adapter 与服务协商版本;我是不喜欢用需要自己注册 key 的服务的,最后保留七个不需要我填写 key 的 MCP:
名称 |
类型 |
生命周期 |
我用它做什么 |
|---|
sequential-thinking |
stdio |
eager |
因果分析和方案比较 |
context7-1 |
Streamable HTTP |
eager |
查询官方库/API 文档 |
mcp-deepwiki |
Streamable HTTP |
eager |
阅读 GitHub 仓库资料 |
mcp-server-time |
stdio |
eager |
时间和时区换算 |
shrimp-task-manager |
stdio |
eager |
跟踪有依赖的长任务 |
playwright |
stdio |
lazy |
浏览器行为和页面证据 |
chrome-devtools-mcp |
stdio |
lazy |
Chrome 网络、性能和 trace |
Plan Mode
Plan 的生命周期
Plan Mode 这个插件最近的更新迭代变化也比较大值得单独说一下。
现在进入 /plan 后,Pi 会根据当前工具选择建立一个受限父会话。这个阶段可以读取文件、搜索内容,并执行通过 Plan 自己命令检查的 Bash;完成 plan_mode_complete 后,Pi 才显示实施菜单。选择 Implement here 或 Start fresh and implement 会关闭 Plan,再恢复完整工具。
当前我的 Plan 配置
当前 pi-plan-mode.json
{
"thinkingLevel": "inherit",
"defaultPlanTools": [
"read",
"bash",
"grep",
"find",
"ls",
"subagent"
],
"implementationPlanRetention": "clear-after-first-run",
"defaultPlanExportPath": "PLAN.md",
"safeSubcommands": {
"git": [
"status",
"log",
"diff",
"show",
"branch",
"remote",
"ls-files",
"grep",
"rev-parse",
"blame",
"describe",
"merge-base",
"ls-tree",
"cat-file"
]
}
}
我保留 bash 是为了在 Plan 里查看 Git 历史和项目状态,但 safeSubcommands.git 只增补经过验证的 Git 子命令,不是任意 shell 白名单。权限扩展里的 bash.*: allow 只影响普通会话的询问,Plan 仍会先做自己的 fail-closed 命令解析。复合命令、重定向、命令替换、后台任务、外部 pager、textconv 和 filter 仍可能被拒绝。
只读子代理:角色选择和能力上限
我一开始只在 Plan 工具里勾选 subagent,再用提示词要求子代理“只读”。这不能改变普通角色的工具面,worker 仍然可能带有 bash、edit 或 write。把全局权限改成 ask 或禁用 worker 又会破坏普通开发,所以我把限制放在 Plan 会话,而不是全局。
第二版曾使用 allowedPlanSubagents。它确实能限制角色名,但 Plan 插件需要理解 pi-subagents 的 action、并行任务和管理命令;{"action":"list"} 这类不含角色名的只读调用也会在到达 subagents 前被拒绝。上游后来移除了这条 schema-specific 逻辑。当前使用 @narumitw/pi-plan-mode ,不再支持配置 allowedPlanSubagents 只负责自己的工具和 Git 校验,而 pi-subagents 也只负责角色发现与执行。
我补了一层本地 plan-subagent-ceiling bridge,位置是用户扩展目录下的 extensions/plan-subagent-ceiling/index.ts。它通过 Pi 的 getAgentDir() 找到用户包目录,用 createRequire 和 jiti 动态加载 pi-subagents/capability-ceiling,不改 node_modules,也不解析 subagent 的 action 字段。进入 Plan 时,它向当前 session 注册三个允许角色和四个允许工具;Plan 工具不再 active 时或 session 关闭时调用 dispose()。如果 bridge 无法加载,只有 Plan 下的 subagent 调用被阻止,普通会话的 worker 不受影响。
三个角色的 frontmatter 是第二层边界:tools 只有 read, grep, find, ls,extensions 为空,inheritSkills 为 false,并用 agent 级 permission 默认拒绝其他工具。denyExtensions: true 使它们不加载 MCP 和普通扩展。acceptanceRole: read-only 只描述交付语义,实际能力由工具白名单和 ceiling 决定。pi-subagents 新版不接受 permission.bash,Bash 策略交给 Plan limited-shell 和 Pi guard;三个角色没有 Bash 工具,因此不再写无效的 bash: deny。
权限依旧使用 @gotgenes/pi-permission-system 控制。
普通角色和 Plan 角色
普通角色保留完整工作流:worker 可以 bash/edit/write,其他常规角色按任务使用读工具和必要的 Bash。Plan 角色只用于当前父会话的证据收集:
角色 |
模型/思考 |
工具 |
用途 |
|---|
plan-scout |
Luna / low |
read, grep, find, ls |
快速盘点入口、调用链和事实源 |
plan-researcher |
Luna / medium |
read, grep, find, ls |
跨文件技术调查,默认超时 15 分钟 |
plan-reviewer |
Terra / medium |
read, grep, find, ls |
对完整方案或完整 diff 做一次集中复查 |
AGENTS 变动
在写第二版教程后,许多佬友说 AGENTS.md 现在的 模型能力很强了不需要约束那么多,一百行左右就行了。
实测下来只能说大体来说确实如此,实际上一些约束在 Windows 上还是存在必要的,不然 GPT 该被 powershell 绊还是绊。
我实测下来有两点还是必须仔细写清楚 GPT 才会遵循,一个是静态核查,不写清楚老爱自己给你写 test 脚本,运行代码测试。一个就是 powershell用法。
最终经过我的实测,现在我的 AGENTS.md 也压缩到200行以内了,相较之前减少了很多了。
AGENTS.md.txt (15.3 KB)

本轮相关配置一览
subagent/config.json
{
"toolDescriptionMode": "compact",
"asyncByDefault": true,
"inlineToolDisplay": "summary",
"waitTool": {
"enabled": true
},
"forceTopLevelAsync": false,
"globalConcurrencyLimit": 6,
"parallel": {
"maxTasks": 6,
"concurrency": 6
},
"maxSubagentSpawnsPerSession": 24,
"maxSubagentDepth": 1,
"completionBatch": {
"enabled": true,
"debounceMs": 150,
"maxWaitMs": 1000,
"stragglerDebounceMs": 75,
"stragglerMaxWaitMs": 400,
"stragglerWindowMs": 2000
},
"worktreeBaseDir": "<ABSOLUTE_WINDOWS_WORKTREE_DIR>"
}
asyncByDefault 是为了解决长时间 foreground 子代理阻塞父会话;它不意味着每个任务都要并发。完整链路内连续工作由主线程完成,默认只在链路结束后做一次集中只读审查。跨文件研究若预计超过两分钟,父调用要显式传 timeoutMs: 900000;调用方传入的值会覆盖角色 frontmatter 的默认值。
Plan 专用 agent 的共同 frontmatter
---
name: <plan-scout|plan-researcher|plan-reviewer>
description: Read-only investigation or review for a planning session.
tools: read, grep, find, ls
extensions:
model: <openai/gpt-5.6-luna|openai/gpt-5.6-terra>
thinking: <low|medium>
systemPromptMode: replace
inheritProjectContext: true
inheritSkills: false
defaultContext: fresh
acceptanceRole: read-only
permission:
"*": deny
read: allow
grep: allow
find: allow
ls: allow
edit: deny
write: deny
mcp: deny
skill: deny
external_directory: deny
special: deny
---
plan-scout 使用 Luna/low,plan-researcher 使用 Luna/medium,plan-reviewer 使用 Terra/medium;只有 plan-researcher 当前显式保留 timeoutMs: 900000,其余角色可按需要继承默认值。tools 白名单和 capability ceiling 才是实际边界;acceptanceRole 只描述完成语义。当前 pi-subagents 不支持 permission.bash,不要再添加该字段。
plan-subagent-ceiling bridge 的适配扩展
import { createRequire } from "node:module";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { getAgentDir, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
const PLAN_TOOLS = new Set(["plan_mode_question", "plan_mode_complete"]);
const ALLOWED_AGENTS = ["plan-scout", "plan-researcher", "plan-reviewer"];
const ALLOWED_TOOLS = ["read", "grep", "find", "ls"];
type CeilingHandle = { dispose(): void };
type RegisterCapabilityCeiling = (options: {
sessionId: string;
source: string;
ceiling: {
allowedAgents: string[];
allowedTools: string[];
denyExtensions: boolean;
};
}) => CeilingHandle;
type CapabilityCeilingModule = {
registerSubagentCapabilityCeiling?: RegisterCapabilityCeiling;
};
type JitiLoader = {
import(specifier: string): Promise<unknown>;
};
let subagentPackageLoader: JitiLoader | undefined;
async function loadCapabilityCeilingModule(): Promise<CapabilityCeilingModule> {
if (!subagentPackageLoader) {
const agentDir = getAgentDir();
const requireFromAgentPackages = createRequire(join(agentDir, "npm", "package.json"));
const { createJiti } = requireFromAgentPackages("jiti") as {
createJiti?: (id: string, options?: { moduleCache?: boolean }) => JitiLoader;
};
if (typeof createJiti !== "function") {
throw new Error("Pi user package loader is unavailable");
}
subagentPackageLoader = createJiti(
pathToFileURL(join(agentDir, "npm", "package.json")).href,
{ moduleCache: false },
);
}
return (await subagentPackageLoader.import("pi-subagents/capability-ceiling")) as CapabilityCeilingModule;
}
export default function planSubagentCeiling(pi: ExtensionAPI) {
let handle: CeilingHandle | undefined;
let sessionId: string | undefined;
let syncPromise: Promise<boolean> | undefined;
const isPlanActive = () => {
try {
return pi.getActiveTools().some((tool) => PLAN_TOOLS.has(tool));
} catch {
return false;
}
};
const clear = () => {
handle?.dispose();
handle = undefined;
sessionId = undefined;
};
const sync = (ctx: ExtensionContext): Promise<boolean> => {
if (syncPromise) return syncPromise;
const current = (async () => {
if (!isPlanActive()) {
clear();
return true;
}
const currentSessionId = ctx.sessionManager.getSessionId();
if (!currentSessionId) return false;
if (handle && sessionId === currentSessionId) return true;
clear();
try {
const module = await loadCapabilityCeilingModule();
if (typeof module.registerSubagentCapabilityCeiling !== "function") return false;
handle = module.registerSubagentCapabilityCeiling({
sessionId: currentSessionId,
source: "local-plan-subagent-ceiling",
ceiling: {
allowedAgents: ALLOWED_AGENTS,
allowedTools: ALLOWED_TOOLS,
denyExtensions: true,
},
});
sessionId = currentSessionId;
return true;
} catch {
clear();
return false;
}
})();
syncPromise = current;
return current.finally(() => {
if (syncPromise === current) syncPromise = undefined;
});
};
pi.on("session_start", async (_event, ctx) => {
await sync(ctx);
});
pi.on("before_agent_start", async (_event, ctx) => {
await sync(ctx);
});
pi.on("tool_call", async (event, ctx) => {
if (event.toolName !== "subagent") return;
if (!isPlanActive()) {
await sync(ctx);
return;
}
if (await sync(ctx)) return;
return {
block: true,
reason:
"Plan-mode subagent delegation is blocked because the local capability-ceiling bridge could not load pi-subagents/capability-ceiling.",
};
});
pi.on("session_shutdown", () => {
clear();
});
}
我实际的 loadWithJiti 使用 getAgentDir()、createRequire(<agentDir>/npm/package.json) 和用户包目录中的 jiti,再加载 pi-subagents/capability-ceiling。上面的代码只展示边界和事件,pi、handle 和加载器由扩展外层初始化;bridge 在 session_start、before_agent_start 和 subagent 调用前同步 ceiling;Plan 工具不再 active 时释放,session_shutdown 也会调用 dispose()。它不读取 action: list/parallel/...,因此不会重新把两个官方扩展绑在一起;加载失败时只阻止 Plan 下的 subagent,并返回原因。
pi-permission-system 配置
{
"permission": {
"*": "allow",
"read": "allow",
"grep": "allow",
"find": "allow",
"ls": "allow",
"external_directory": { "*": "ask", "~/.pi/agent/*": "allow" },
"path": {
"*": "allow",
"*.env": "deny",
"~/.ssh/*": "deny",
"~/.pi/agent/auth.json": "deny"
},
"bash": {
"*": "allow",
"Remove-Item *": "ask",
"Set-Content *": "ask",
"git diff *": "allow",
"git log *": "allow",
"git show *": "allow",
"git rev-parse *": "allow"
}
}
}
pi-tool-display 的冲突规避配置
{
"enabled": true,
"registerToolOverrides": {
"read": false,
"grep": true,
"find": true,
"ls": true,
"bash": false,
"edit": false,
"write": false
},
"readOutputMode": "summary",
"searchOutputMode": "count",
"mcpOutputMode": "summary",
"bashOutputMode": "opencode",
"diffViewMode": "auto",
"diffWordWrap": true
}
mcp.json 参考配置
{
"settings": {
"directTools": false
},
"mcpServers": {
"sequential-thinking": {
"command": "cmd",
"args": ["/c", "npx", "-y", "@modelcontextprotocol/server-sequential-thinking"],
"lifecycle": "eager",
"directTools": true
},
"context7-1": {
"url": "<CONTEXT7_MCP_URL>",
"protocolVersion": "auto",
"lifecycle": "eager",
"directTools": true
},
"playwright": {
"command": "cmd",
"args": ["/c", "npx", "-y", "@playwright/mcp@latest"],
"lifecycle": "lazy"
},
"mcp-server-time": {
"command": "uvx",
"args": ["--with", "mcp>=1.23,<2", "mcp-server-time", "--local-timezone=Asia/Shanghai"],
"lifecycle": "eager",
"directTools": true
},
"shrimp-task-manager": {
"command": "cmd",
"args": ["/c", "npx", "-y", "mcp-shrimp-task-manager"],
"lifecycle": "eager",
"directTools": true,
"env": {
"DATA_DIR": "<MCP_DATA_DIR>",
"ENABLE_GUI": "false",
"TEMPLATES_USE": "en"
}
},
"mcp-deepwiki": {
"url": "<DEEPWIKI_MCP_URL>",
"protocolVersion": "auto",
"lifecycle": "eager",
"directTools": true
},
"chrome-devtools-mcp": {
"command": "cmd",
"args": ["/c", "npx", "-y", "chrome-devtools-mcp@latest"],
"lifecycle": "lazy"
}
}
}