# pi-starter 能力与边界

> 这是唯一还维护的**对照**文档。基线：`v0.4.1`（`59c1e96`，2026-10-10）及其后的工作区。
> 对照对象：`pi-web-ui@0.99.0` 的后端（`server/` + 插件服务端 + CLI）。对方源码不在本仓库，规模数字来自当日阅读，未复测对方仓库。
> 我们这边的行为以本仓库源码为准。**规模与用例数不在这里手写**——见 [`README`](../README.md#numbers) 的 Numbers 表；**接口面不在任何手写文档里**——见 [`参考手册.md`](参考手册.md)（34 条 WS 命令 / 21 个服务端帧 / 47 条 REST 路由处理器 / 工具清单含能力与风险标签 / 全部环境变量 / 全部 npm 脚本）。三者（Numbers 表、参考手册、`docs/project_overview/` 全景页与名片）都由脚本从同一份 `scripts/visualization/metrics.mjs` 生成，`npm run docs:check` 在远端挡漂移。可执行门禁的组成见 [`CONTRIBUTING`](../CONTRIBUTING.md)：`npm run verify`（typecheck / lint:unused / test / test:web / docs:check / verify:audit / smoke / build / verify:embed）与 `npm run verify:all`（再加真进程 e2e）。
>
> 读法：第 2 节是对方的边界，第 3–4 节是我们现在有什么、和他们差在哪，第 5 节是下一步该长什么。第 7 节只是已删旧稿的来源备注。

---

## 0. 测试分层（先看这节，它决定你能信什么）

| 层 | 命令 | 覆盖什么 | 不覆盖什么 |
|---|---|---|---|
| 单元 | `npm test` | 纯函数、校验、状态机、编排层（用遵守 SDK 契约的替身） | 真实 SDK 调用、跨进程 |
| 传输 | `npm run smoke` | 真 HTTP + WS：帧顺序、pending 回放、origin、背压断连 | agent 组装、会话落盘 |
| **真 E2E** | `npm run e2e` | **真进程**：完整对话 → `SIGKILL` → 重启 → 会话从磁盘恢复 → 恢复后继续对话；**带工具调用恢复**（`tool_call` 与 tool 结果成对，断言打在出站请求上）；设置改完落盘 → 重启后真的生效；索引指向已删文件时无幽灵条目；停机后端口释放 | Windows 上无法投递可捕获的 SIGTERM（**显式 SKIP**，见下） |
| 门禁 | `npm run lint:unused` | 死代码（"声明了但没接线"就是靠它活下来的） | — |
| 自检 | `npm run verify:embed` | 打 `dist/` 跑，验嵌入路径 | — |

**关于 SKIP**：`npm run e2e` 会打印 `43 通过 / 1 跳过 / 0 失败（共 44）` 并**逐条列出跳过原因**（2026-10-10 在 Windows 实测）。Windows 上 Node 不向子进程投递可捕获的 `SIGTERM`（`child.kill` 走 `TerminateProcess`，实测退出码 `null`、handler 从不运行），所以真信号停机链路只能在 POSIX 上跑。为此优雅停机的**编排逻辑**（步骤顺序 / 单步失败隔离 / 幂等 / 兜底强退）已抽到 `src/graceful.ts` 并由 `graceful.test.ts` 全平台覆盖——不能因为平台限制就让它无人看守。

**不要用「条件不成立就 return」冒充通过。** 本仓库的符号链接用例曾这样写，结果在 Windows 上从未真正运行却显示通过；E2E 的「不是兜底强退」断言也曾在停机 handler 未运行时通过（假阳性）。跳过必须被打印、被统计、被解释——`src/files/service.test.ts` 里 denyNames 那条符号链接用例是最后一处裸 `return`，已改成 `t.skip(原因)`。

**单元层的平台跳过（Windows，逐条打印）**：权限位 2 例（Windows 的 `chmod` 只能置只读位，`0600` 与 `0644` 在 `stat.mode` 上不可区分，实测恒为 `0o666`）、文件符号链接 2 例（需要开发者模式或特权；junction 只对目录有效，换不成「字面名无害 → 真实目标是 `.env`」这个形状）。**功能断言不在跳过之列**——复制是否发生、内容是否真被读进 `auth.json`、路径与名单是否命中，全平台照跑。

**平台口径的不对称要当心**：真正生效的远端门禁只有 `.cnb.yml` 一套（单个 `node:24` Linux 容器）；`.github/workflows/ci.yml` 写的是 ubuntu / windows / macos 三 OS 矩阵，但本账号的 Actions 因计费跑不起来，所以它**一行都没跑**。结论不变而且更尖锐：Windows 专属的失败**不会被任何远端门禁看见**。本轮就是因此攒出了五处「本地根本跑不过、远端一直是绿」（生成器分隔符、`chmod` 语义、符号链接特权、两处 npm spawn），现已修到 `npm run verify` 在 Windows 上可通过。反过来也成立：**本地绿不等于远端绿**（`verify:audit` 就是为了让这条对称而加的；`docs:overview:check` 后来又在远端红过一次而本地全绿，成因是 CI 容器不继承开发机的 `core.quotePath`）。在 Windows 上开发时，任何新进门禁的步骤都要本机实跑一遍再宣布通过。

`npm run e2e` 不需要 API Key：把 `PI_CODING_AGENT_DIR` 指向临时目录，写一份指向本地假 OpenAI 兼容端点的 `models.json`（启动期不联网探测，`auth.json` 的 key 纯本地解析）。会话目录随之被隔离在临时目录内，**不会在用户真实的 `~/.pi` 下留垃圾**（已实测确认）。

这一层是**升级 SDK 时最该被回归保护**的：落盘时机、会话目录推导、索引与 jsonl 的对应关系，任何一处变了同进程测试都发现不了——因为同一个模块实例还在内存里。

---

## 1. 两边各自是什么

**pi-starter** 是垂直 Agent 的起步模板，不是编码助手。CLI、HTTP、库三种形态共用 `buildAgent`。业务从接口注入（工具、技能、扩展、人设），不改内核。运行时依赖尽量只留 SDK、Express、TypeBox、`ws`；环境变量用 Node 内置加载，数据库用 `node:sqlite`。

五条不要破的原则：

| 原则 | 含义 |
|---|---|
| 通用内核，不做成编码专用 | 按「任何垂直 Agent 都需要」取舍，不按「能改代码的驾驶舱」取舍 |
| 零重依赖优先 | 能用 Node 内置就不加包 |
| 可注入、可裁剪 | 能力是可替换的函数或参数，不长成万行单文件 |
| 默认安全 | 默认 loopback、编码工具默认关、`deny` 规则关不掉 |
| 核心逻辑可单测 | 评估、校验、协议翻译不碰网络、不调模型 |

**pi-web-ui** 是生产级 AI 编码驾驶舱：聊天、改代码、文件、终端、git，外加 Electron、桌面 sidecar、双引擎（pi + DeepSeek Harness）。`server/` 约 130 个 TS 文件，最大单文件 `agent-service.ts` 约 1.5 万行。它和我们同源，都坐在 `@earendil-works/pi-coding-agent` 上，所以能对照；它不是「脚手架再加几个工具」的量级。

它值得继承的是骨架思想，不是文件清单：服务端快照是唯一事实源、协议类型只有一份、权威状态和实时增量分成两条通道、审批用声明式规则、设置按字段校验。

---

## 2. 对方的边界：做到了什么，停在哪里

下面按他们自己的功能域写。每一域只回答两件事：他们做到哪一步，哪一部分是编码驾驶舱专用、我们不跟着长。

| 域 | 他们做到的程度 | 边界 / 不必照搬的部分 |
|---|---|---|
| 协议与传输 | WS 双向；`UiState` 快照 + `rev` 链；增量 delta 绕过背压；Origin/Host 校验；心跳；协议版本协商。客户端命令约 100 个 | 命令里大量是 `terminal_*` / `scm_*` / `plugin_*`。协议落地偏重，不是脚手架需要的面 |
| 会话编排 | 每连接多个对话，每个对话独立 runtime；上限按项目计；跨客户端过户有事务回滚；项目记住模型 | `agent-service.ts` 单文件约 1.5 万行。过户、项目维度是驾驶舱能力 |
| 工具 | 按名覆盖 SDK 内置工具；LSP、哈希补丁、eval、懒加载、看门狗、ActiveSet | 覆盖链直接改 SDK 私有字段 `_customTools`，升级就会碎。LSP / patch / Office 解析是改代码专用 |
| 审批 | 规则库自顶向下首个命中；约 10 条内置高危；allow / deny / ask；对话级三档免问；人可以改参后放行 | 规则匹配含 regex / glob / contains / prefix / outside_workspace。他们有 `plugin:` 档位，我们没有，我们多了 `capability` |
| 运行模式 | 计划模式（硬闸门 + 提示词，约 400 行）；审查委派；目标→执行→审查→修订（`goal-service.ts` 约 2500 行）；子代理 | 目标审查循环和子代理是产品循环，不是地基 |
| 终端与后台任务 | node-pty 七件套；Windows ConPTY 自愈（`terminals.ts` 约 2300 行）；bash 启动的监听进程可列出、停止 | 进程执行已有：`coding` 档的 `exec` / `exec_jobs` / `exec_stop`（cmd.exe 或 /bin/sh，超时、进程树、工作区 realpath）。交互式 PTY（vim / top、ConPTY）仍不进内核 |
| 文件与附件 | 文件 CRUD、预览、Range、上传；附件只给路径不灌进上下文；视觉模型转写；归档拒绝符号链接和设备文件 | 视觉桥、Office 解析、CAS 附件是编码/多模态驾驶舱的装备 |
| 模型 | 运行中切换；一个 provider 多把 key，原始 key 不出服务端；OAuth（`flowId + promptId`，令牌不进 wire）；自定义 provider 的增删和测连（`model-admin.ts` 约 1900 行） | 多密钥和「密钥不回传」值得学。1900 行的管理台不要整包搬 |
| 扩展 | MCP：外部 stdio 工具接入，改配置不用重启（约 600 行）。插件：市场、安装、两步授权、宿主 API、slot、定时任务（`plugins.ts` 约 4700 行） | MCP 覆盖「接外部工具」。插件市场是他们第二大子系统，放最后 |
| 上下文与提示词 | 按 token 分层裁剪、软上限、**主动压缩**（WS `compact_context` + `POST /context/compact`，可带"该保留什么"指令；三条前置校验给明确中文理由；压缩后作废投影/token 缓存防SDK 原地改写导致快照显示旧内容）、悬空 tool call 修复；提示词 11 个 token 可逐层覆盖 | 思想我们已经有一版（预算 + 模板）。目标审查循环是产品循环，不是地基 |
| 安全与运维 | 默认 loopback；token 鉴权；quiesce 拒绝新工作；控制 socket；更新检查；Electron；Docker / systemd / launchd | 我们刻意不做登录和多用户。鉴权、桌面端、发布体系是产品层 |
| 第二引擎 | DeepSeek Harness，JSON-RPC，和 pi 引擎并存（约 5000 行） | 与脚手架无关 |

他们**没有**、而我们有的，主要是：只读 SQL 工具、Markdown 知识库检索、类型化错误（`internal` 默认不把原文回给客户端）、`/metrics` 和脱敏日志。这些不要为了「对齐」删掉。

他们能力分布不均：最厚的是会话编排、插件、第二引擎、目标审查、终端、模型管理。会话持久化、SQL、知识库这类基础面他们反而薄。照着行数抄，会把失衡一起抄进来。

---

## 3. 我们现在有什么

骨架以 `c60b282` 为准。会话持久化是其后补上的，下面按当前代码写，不按那次提交。

**骨架（已经和对方同构，不再是缺口）**

- WebSocket 双向、快照 + `rev` 链、增量 delta、背压下丢快照、慢客户端持续背压会断开。
- `protocol.ts` 是消息类型单源，内置命令有编译期完备性断言。
- 多对话：每个对话独立 session，有上限和 LRU。可以改名、回退、编辑用户消息、按当前路径分叉。没有项目维度，没有跨客户端过户。
- Web 对话会落盘，重启后按会话 id 接回**本工作区、本脚手架自己写下的**那些。目录是 SDK 会话目录的兄弟目录，不和 `pi` CLI 的 `.jsonl` 混放。客户端只提交 id，不提交路径。另一个连接已经打开的对话不能被抢走。`inMemory: true` 时恢复会报错，不会静默变成空会话。
- 工具注册表带能力标签，运行中可以开关。不改 SDK 私有字段。
- 工具看门狗：超时中止；正在等人类的工具不计时。
- 审批：10 条内置高危规则，六种匹配器全部有（`regex` / `glob` / `contains` / `prefix` / `outside_workspace` / `capability`），三档策略，人可以改参后放行。`deny` 不能被「关掉审批」绕过。
- 上下文预算和提示词模板。层数比对方少。
- `run_start` / `run_end`（带 `stopReason`，能区分将重试和被中止）/ `tool_delta`。
- 传输：Origin/Host 校验、body 上限、安全响应头、限流、只读 SQL（CTE 里的写也会拦）。

**这一轮新接上的**

- 自定义 WS 命令：把 `commands` 传给 `attachWebSocket`。没有 `registerCommands()` 这个函数。`defineCommand()` 只在编译期给 `payload` 标类型，**运行时不校验**。未注册命令回错误帧，帧里列出的是已注册的自定义命令，不是内置全集。同名注册盖不住内置命令。
- HTTP：`createApp({ configure })` 在错误处理器之前注入业务路由。`internal` 不把密码、绝对路径、堆栈回给客户端。
- `addDisposer()`：停机时回收，单个清理失败不挡住其余的。
- 文件服务：列出、读、写、新建、改名、复制、删除、原始内容、上传。字面路径必须在工作目录内；符号链接用 `realpath` 再查一次，**解析失败就拒绝**。整条路径都不存在时（新建文件）只做字面校验，因为没有可逃逸的实体。
- 设置落盘：原子写。JSON 解析失败回落默认值。字段未知或类型不对时剔除并告警，服务仍能起来。通过 API 改设置仍然拒绝未知字段，而且每次 `patch()` 都写盘。
- 审批规则落盘：启动时读，非法规则跳过（`value` 必须是非空字符串，`outside_workspace` 除外），内置 `deny` 还在。**改动即写盘**——唯一写盘时机是「我们自己改动时」，因此停机不再写盘，运行期间用户手改规则文件**不会被停机覆盖**（原先停机写回会用启动时的内存副本抹掉那次手改）。规则库条数上限 500。

**对齐官方 Pi SDK（手写→官方切换 + 必要补齐）**

- 技能目录：只保留 SDK 的 `<available_skills>`（`buildSystemPrompt` 在含 `read` 时自动追加）。`agent.ts` 里那份 `formatSkillCatalog` 拼进 `{{skills}}` 的第二份清单已删除——`{{skills}}` 从默认顺序移除，但仍是已知 token（渲染为空），旧模板不报错。
- 提示词模板清单：走 `DefaultResourceLoader.getPrompts()`，不再自解 frontmatter（`loadPromptTemplates` 不在主入口导出）。`/skills`、`/prompt-templates` 与装载器同源，不漂移。
- 回退：`rollback_conversation` 可选 `summarize`/`instructions`，SDK 提供 `navigateTree` 时走官方树导航带分支摘要，否则回落 `branch()` + custom 标记。
- `prompt()` 传 `preflightResult`：预检拒绝回明确 `conflict`，不静默。`queue_update` 即时 `getState()`。
- 新增官方能力切片：`POST /chat?format=jsonl`（官方 `json.md` 事件词表的原始通道）；`custom-provider.example.ts`（官方 `pi.registerProvider`，api-key 型，默认不接线）；pi packages 经 `extraSkillPaths`/`extraPromptTemplatePaths`/`extraExtensions` 消费。
- **取舍原则（重要）**：第 3 条「切成官方」不是无条件的——只在不把已有能力做差的前提下切；官方更弱或覆盖不了需求时，保留自定义并显式记录原因。
- **模型管理 = 官方核心 + 更优自定义解析（有意保留的例外）**：列表/切换/换 Key 走的都是官方 `ModelRuntime`（`getAvailable()` / `session.setModel()` / `setRuntimeApiKey()`）；自定义的只是 `resolveModelRef` 这层「把 `provider/模型名` 解析成官方能认的引用」，支持带斜杠的模型 id、provider+model 拆开传、裸唯一 id、只接受已配 Key 的模型——官方 `resolveCliModel` 走 CLI 单体解析（all-registered-models、`provider/model[:thinking]`），覆盖不了这些语义，且 `setup.ts` 阶段没有 runtime。故这一段**不切**，属第 3 条的有意例外。
- 明确不适用（SDK 未提供或仅 TUI，不凭空造）：交互式 PTY、goal-review 循环、themes/TUI/`ctx.ui`/autocomplete、OAuth 交互式同意。（完整 `runRpcMode` 已改为下方官方接入，不再列不适用。）
- **隔离=官方部署层，不是库内置沙箱**：`guard` 是正则软闸门（零依赖、非沙箱，已实测可被命令替换/编码绕过）。官方规定的硬隔离靠 `containerization.md`（Docker / Gondolin micro-VM / OpenShell）或 `pi.registerTool` 工具路由扩展。本仓库不自己造沙箱，但根目录备了一份可直接用的 `Dockerfile`（非 root、只绑 127.0.0.1 发布），`src/extensions/sandbox.example.ts` 给工具路由接缝（默认不接线）；真要跑隔离得你自己上 Docker/QEMU。
- **补齐的官方能力面（本轮）**：模型轮换 `scopedModels`+`cycleModel`/`cycleThinkingLevel`（CLI `/cycle`、`POST /model/cycle`、WS `cycle_model`）；provider 鉴权状态 `getProviders`/`checkAuth`（`GET /providers`、`/info`）；`createEventBus` 暴露为 `BuiltAgent.eventBus`；会话标签 `appendLabelChange`/`getLabel`（WS `set_label` + 快照 `labels`）；`pi.registerCommand`+`pi.sendUserMessage`（`example-command.ts`，注：`AgentSession` 不暴露 sendUserMessage，故走扩展通道、不硬造内核命令）；AGENTS.md opt-in（`includeAgentsFiles`）；`registerProvider` 一等参数 `buildAgent({ providers })`；官方 RPC 模式 `runRpcMode`+`createAgentSessionRuntime`（`--mode rpc`，与 `buildAgent` 共享 `resourceLoaderOptions` 单一装配核心，不携 MCP/审批闸门）。另外一批低成本项也已一等化：`buildAgent({ commands })`（官方 `pi.registerCommand`）、`buildAgent({ excludeTools })`、`BuiltAgent.waitForIdle()`/`getThinkingLevel()`、`cycleModel(direction)`。
- **本地 RAG**：知识库检索可插拔（默认关键词零依赖）；`@huggingface/transformers` 进 `optionalDependencies`（进程内 embedding、首次自动下权重，`PI_EMBEDDINGS_HF_ENDPOINT` 可切镜像）；sqlite 向量库（`PI_KNOWLEDGE_VECTOR_STORE=sqlite`）+ 内容寻址 chunk id → 重启不重算。`npm run rag:smoke` 一键实机验证，环境不具备时 SKIP、不假绿。
- **HITL 反问（官方 `ctx.ui`，不闭门造车）**：`ask_user_question` 让智能体在回合中途向人类发问、阻塞等输入再继续。**官方 SDK 原生提供 `ExtensionUIContext`（`ctx.ui.input/select/confirm/editor`，带 `timeout`/`AbortSignal`）——但只给 TUI 与 RPC 子进程两种模式配了“谁来回答”**，本项目跑进程内 + WS 属官方未附带实现的模式，故只补一个 WS 版实现（逐字复刻官方 `rpc-mode` 的 `createDialogPromise`：`id`→resolver 挂起表，超时/断开/取消回官方默认值 undefined/false，fail-safe 不挂死）。`protocol.ts` 加 `extension_ui_request`/`extension_ui_response`，线形对齐官方 `RpcExtensionUIRequest/Response`。工具用 `defineTool` 调 `ctx.ui.*`，`ctx.hasUI` 为假时优雅降级让模型改用正常回复收尾；登记进 `allTools`（三档白名单 + `human.input` 能力），`session-hub.ts` 里那个早已存在的 `ask_user_question` 看门狗豁免占位至此有了真身。注入用 `buildAgent({ extensionUi })`→`extensionRunner.setUIContext`（**不是 `session.bindExtensions`**，见 §6）。**审批链路一行未动**——审批与反问共用同一“发出去、等回来、超时兜底”范式，但互不依赖：何时该问（规则匹配）SDK 不提供、保留自定义 `gate.ts`；怎么问人才用官方 `ctx.ui`。验证：`extension-ui-bridge.test.ts` 10 项单测 + `smoke` 4 项真 WS 闭环。**边界（诚实标注）**：前端已接（`web/src/components/HitlDialog.tsx` 挂进 `App.tsx`，走同一条 `extension_ui_request` / `extension_ui_response` 线协议）；UI 反问**不做快照持久化**（不像审批存 `pendingApproval`），断线重连期间的未答问题靠超时/断开兜底。
- **官方能力面补齐（第二轮，按接口文档逐节实证）**：拿 `官方SDK接口文档.md` 与代码对照，把官方提供、未接的能力补齐（字段名一律以 `dist/*.d.ts` 为准）：
  - **SDK 设置透传**：`buildAgent({ sdkSettings })` 把官方 `compaction{enabled,reserveTokens,keepRecentTokens}`/`retry{enabled,maxRetries,baseDelayMs}`/`images{autoResize,blockImages}`/`enabledModels` 交给官方 `SettingsManager`（create+applyOverrides）并注入 `createAgentSession` 与 loader——真正驱动自动压缩阈值/LLM 重试退避/图像降采屏蔽/模型白名单。**默认不配则不建 SettingsManager、行为逐字不变**。`config.ts` 新增 `resolveSdkSettings`/`resolveExtensionPaths`，`PI_COMPACTION_*`/`PI_RETRY_*`/`PI_IMAGES_*`/`PI_ENABLED_MODELS`/`PI_EXTENSION_PATHS` 驱动，server 与 CLI 同源。
  - **导入外部会话**：`SessionHub.importConversation()` 用官方 `SessionManager.forkFrom` 把外部 `.jsonl` 完整复制进本会话目录（全新 id）+ 登记索引，走既有 `openConversation` 接回（即官方 `importFromJsonl` 的底层原语）。新增 `POST /sessions/import`；目标文件同样过 `assertSessionFileAllowed` fail-closed。
  - **官方扩展路径**：`buildAgent({ extensionPaths })`→`additionalExtensionPaths`（与 extraExtensions 并列的第二条官方路；`noExtensions` 只关 `~/.pi` 扫描）。工具级：`ask_user_question` 设官方 `executionMode:"sequential"`；`renderCall/renderResult` 属 TUI 专属、无头不适用。
  - **扩展钩子接缝**：`provider-hooks.example.ts`（`before_provider_headers` 原地改头 / `before_provider_request` 返回替换 payload）、`input-resources.example.ts`（`input` transform/handled / `resources_discover` 动态贡献 skill/prompt 路径）——默认不接线、类型对齐官方事件、附行为测试。
  - **`tool_result` 结果脱敏接缝**：`tool-result-redaction.example.ts` 接官方五步管道第 5 步（`tool_result`），在结果**回传给模型前**按正则替掉密钥/路径回显——补上 `http/errors.ts`（只护 HTTP 响应）管不到的"发给模型的 tool 结果"这一面。默认不接线（会改变模型可见内容），按 `extraExtensions` 启用。
  - **Settings 透传扩面**：除上一轮的 `compaction`/`retry`/`images`/`enabledModels`，本轮又接上 `httpIdleTimeoutMs`（出站 provider 超时，区别于入站 `hardening.ts` 的服务器超时）/`websocketConnectTimeoutMs`/`steeringMode`/`followUpMode`/`thinkingBudgets`/`branchSummary`；枚举值非法即丢弃、不静默传 SDK。
  - **事件翻译补全**：`auto_retry_end`（官方重试结算事件，之前被 `default` 吞掉）现译成 notice："重试成功/最终失败"。

**入口上仍然在的**

- CLI 流式对话；HTTP 资源接口不调模型，`/chat` 仍是 SSE。
- 技能（仓库内 `SKILL.md`，不扫 `~/.pi`）、提示词模板（仓库内 `<name>.md`，走 SDK `additionalPromptTemplatePaths` + `noPromptTemplates`，与技能同规矩，`/name` 在 `session.prompt` 里展开）、Markdown 知识库（检索后端可插拔：默认关键词零依赖，`PI_KNOWLEDGE_RETRIEVAL=vector` + embedding 源切向量检索，`VectorStore` 接口可接 Qdrant/pgvector）、sqlite 只读查询。
- 模型目录、启动时选择、运行中 `setModel`（REST 和 WS 走同一个 hub）。多把 API 密钥已实现（原始值不出服务端）；没有 OAuth。
- 编码工具三档 `off` / `readonly` / `coding`，默认 `off`。`coding` 额外挂上进程执行（`exec` / `exec_jobs` / `exec_stop`），不是交互式终端。
- `npm run setup` 把 provider 写进 `~/.pi/agent/`，密钥文件权限收紧。不覆盖已有密钥，除非 `--force`。

**刻意不做**

登录、多用户、向量库、真沙箱、公网无鉴权暴露、交互式 PTY、插件市场、第二引擎、Electron、定时任务、支付、产品 UI。进程执行（跑一条命令、拿输出、后台任务）已在 `coding` 档，不在这张「不做」清单里。

非 loopback 必须自己加鉴权代理。本项目不内置登录，这是定位，不是遗漏。

---

## 4. 对照：他们有什么，我们有什么

| 能力 | 他们 | 我们 | 取舍 |
|---|---|---|---|
| 快照 / 协议 / 多对话 | 有，且带项目与过户 | 有，无项目、无过户 | 骨架对齐。过户先不做 |
| 工具注册 | 硬编码目录 + 改私有字段 | 数据驱动，走公开 API | 保持我们的，不要退回私有字段 |
| 看门狗 / 审批规则 | 有 | 有，匹配器已齐，且**有编辑接口 + 改动即落盘** | 对齐 |
| 轮次信号 | `agent_end` 汇成结束信号 | `run_start` / `run_end` / `tool_delta` / `turn_start` / `turn_end` 已有 | `entry_appended` 只刷新标题，没有对应协议帧 |
| 文件 | CRUD + 预览 + Range + 归档 + 视觉 | CRUD + 预览上限 + 符号链接 fail-closed | 手已经有了。视觉桥、归档、Office 先不做 |
| 模型 | 多密钥、OAuth、管理台 | 多密钥（原始值永不出服务端）+ 单一真源切换 | 不做 OAuth 与整座管理台 |
| 会话是否活过重启 | 有客户端状态落盘 | 设置能活过重启；**本工作区的 Web 对话能接回**；规则改动即落盘 | 会话只恢复自己写下的，不做过户 |
| MCP | 有，改配置可热生效 | 有，stdio 子进程 + 热生效 | 工具**只对新建会话生效**（见第 6 节硬约束） |
| 计划模式 | 有，约 400 行 | 有，会话级硬闸门 + 提示词软约束 | 按会话隔离，不做目标审查循环 |
| 子代理 | 有 | 有，不占主对话 LRU 额度 | 目标审查 / 审查委派仍不做 |
| 目标审查 / SCM / 插件 | 有，而且厚 | 无 | 等业务真的需要再做，不按他们的行数排期 |
| 后台任务 / PTY | 有，而且厚（ConPTY） | 进程执行有（列出、停止、超时杀进程树）；交互式 PTY 无 | 不把 vim / top 做进内核 |
| SQL / 知识库 / 指标 / 类型化错误 | 基本没有 | 有 | 保留 |

---

## 5. 下一步：我们自己要长什么

顺序按「脚手架缺了就接不进去」，不按对方文件有多大。

**1. Web 会话持久化** — 已接上

重启后能列出并打开这个工作区里、本脚手架自己写下的对话。打开命令是 `open_conversation`，只带会话 id。未加载的条目在列表里带 `dormant: true`，`switch_conversation` 切不过去。文件要等一轮里出现 assistant 消息才会被 SDK 写下，空连接不会进索引。不做跨客户端过户，不做按项目分库。

**2. 规则的编辑入口，以及一改就写盘** — 已完成

`/approval/rules` 四个端点：`GET` 查看生效全集、`PUT` 整体替换用户规则、`POST` 新增/覆盖单条、`DELETE /:id` 删除。`approvalRules` 不传则不注册——改审批策略是敏感能力，不默认开启。

关键设计是**写盘时机**：唯一写盘时机是「我们自己改动时」（`upsert`/`remove`/`setUserRules` 触发回调）。原先放在优雅停机时写，会用启动时读入的内存副本**覆盖掉运行期间用户手改规则文件**的那次修改——把别人的改动悄悄抹掉。改成改动即写后，停机不再需要写盘，那类覆盖随之消失。测试直接断言这条性质：外部改文件 → 停机 → 改动仍在。

配套：`validateApprovalRule` 同时用于 API 入口与磁盘加载，避免出现「手写文件被拦、API 却能塞进去」；`MAX_USER_RULES = 500`；内置规则不可经接口写入或删除。

**3. MCP 桥** — 已完成

外部 stdio MCP 服务器的工具接入会话，改配置热生效不重启。工具注册进 `ToolRegistry`（`source: "dynamic"`），能被 `set_tool_enabled` 开关；子进程归 `addDisposer()` 回收。

**已知硬约束**：MCP 工具**只对新建会话生效**。`createAgentSession({tools})` 会变成 SDK 的 `allowedToolNames` 硬白名单，`_refreshToolRegistry()` 会把不在白名单里的名字整个滤掉，而 SDK **没有提供**修改该白名单的公开方法（只有 `setActiveToolsByName` 切启用状态，`_refreshToolRegistry` 是私有的）。因此 `resolveToolList()` 刻意在 `createSession` **内部**求值——白名单按会话重算，重开会话（`open_conversation`）即生效，不用重启进程。

> 注意这个白名单是**我们自己传的**（`sessionToolPolicy` 按工具档位生成），不是 SDK 强制的。不传就没有白名单，但也失去按档位限制工具的能力。现状是两者取舍的结果。

**4. 计划模式** — 已完成

会话级「只规划、不写」：写类工具经 `tool_call` 钩子被拦（`block: true`）且拒绝原因可操作；`before_agent_start` 注入提示词软约束；模式随转录落盘，重启后保持；按会话隔离（不串到别的对话）。走 `src/extensions/` 接入，不改内核。目标审查循环不做。

**5. 多把 API 密钥** — 已完成

按 provider 存多把 key、按名字寻址。铁律：**原始 key 值与掩码永不出服务端**——测试逐个抓响应体断言连 key 前缀都不出现。切换走 `SessionHub.setModel()` 单一真源路径，不绕过（否则 REST 与 WS 会分裂）。OAuth 与整座模型管理台不做。

**6. `turn_start` / `turn_end`** — 已完成

补齐最后两类 SDK 事件，轨迹序号自增、停止原因与工具数透传、边界推进快照 rev。`entry_appended` 仍只刷新标题、不发协议帧。

**7. 进程执行** — 已完成

`coding` 档才有 `exec` / `exec_jobs` / `exec_stop`。前台等到退出或超时；后台立刻返回 id，可列出、读已捕获输出、停止。超时和中止杀掉进程树。工作目录先做字面路径检查，再 `realpath`，解析失败即拒绝。Windows 用 cmd.exe，其它平台用 `/bin/sh`，不依赖 Git Bash。不是 PTY：没有 vim / top，没有终端尺寸。SDK 提供的是 `bash` 工具和 `executeBash`，不是交互式终端；交互式 PTY 不在 SDK 里，不做。

**8. 会话编辑** — 已完成

`rename_conversation` / `rollback_conversation` / `edit_message` / `fork_conversation`。底下是 SDK 的会话树：`appendSessionInfo`、`branch`、`resetLeaf`、`appendCustomEntry`、`createBranchedSession`。没有另写一套会话库。

回退和编辑只对已经打开的对话。正在生成时拒绝。只 `branch()` 不写标记的话，重新打开会回到文件最后一条，所以会再追加一条 `pi-starter.tree` 标记。普通 custom 不进模型上下文。编辑把原文经 `edit_ready` 交回，不自动再发一轮。分叉先 `SessionManager.open` 出一个用完即丢的副本再调 `createBranchedSession`，不在正在对话的那个 manager 上调。没有助手回复时 SDK 不落盘，直接说明原因。客户端仍然只传会话 id 和记录 id。

**先不做**

目标审查循环 / 审查委派（SDK 没有这个工作流，那是对照项目 pi-web-ui 的产品层）、SCM、交互式 PTY、附件与视觉、插件市场（SDK 有扩展加载和包安装器，没有市场界面；本脚手架不自动扫描本机扩展目录）、第二引擎、桌面端、定时任务、登录。

斜杠命令里那 4 条内置项仍是硬编码。可以以后从已有命令推导，不是缺口。

每做完一项跑：

```powershell
npm run verify
npm run lint:unused
```

新测试要能在改回缺陷时变红，不能只在修好的代码上是绿的。

---

## 6. 复核时钉死的事实

这些曾经被说错，留在这里是为了不再从旧稿里抄回去。

| 事实 | 不要再说成 |
|---|---|
| 扩展点是 `attachWebSocket(..., { commands })` | 不存在 `registerCommands()` |
| `defineCommand()` 只给编译器看 | 它不会在运行时挡住非法 payload |
| `entry_appended` 只刷新标题 | 它不是和 `run_end` 并列的前端信号 |
| 规则**有**编辑接口（`/approval/rules`），且**改动即落盘** | 「规则只有停机才写」「没有编辑 API」——曾经如此，现已改掉 |
| 设置：解析失败回落；字段非法则剔除。API 仍拒绝未知字段 | 「文件坏了都能起来」曾经没覆盖字段非法，服务会起不来。这一点已修 |
| 规则的 `value` 必须是非空字符串 | 缺 `value` 的 glob 会在匹配时抛；缺 `value` 的 regex 会去匹配字面量 `undefined`。已修 |
| `realpath` 失败就拒绝 | 失败时放行是错的：字面路径在目录内，不等于真实路径在目录内。已修 |
| 恢复会话时客户端只给 id | 不接受客户端传来的 `sessionFile`。路径只从索引查，并且 `resolveSessionManager` 在 `open` 之前再校验一次 |
| Web 会话目录是 SDK 目录的兄弟目录 | 不和 `pi` CLI 共用一个目录，也不自己重写 cwd 编码 |
| 索引按会话目录一份，cwd 对不上就整份忽略 | 不是全局一份 `pi-starter-sessions.json`，后退出的工作区不会抹掉另一个 |
| 另一个连接已经打开的对话不能被抢走 | 恢复不是过户 |
| 回退必须追加 custom 标记，不能只 `branch()` | 只改内存叶子的话，重启会回到文件最后一条 |
| 分叉不能调用正在使用的那个 manager 的 `createBranchedSession` | 那个调用会把当前会话的 id 和文件换成新的 |
| 会话索引有 500 条上限，淘汰最旧 | 不是无界增长，且每次写入不再重新读回文件 |
| 测试等待用 `waitFor(条件)` | 固定 `sleep` 在全量并发时会偶发失败——这是「和改动无关」的假象来源 |
| MCP 工具只对新建会话生效 | `tools` 参数会变成 SDK 不可变的 `allowedToolNames`；白名单必须按会话重算，不可在 build 时算一次存起来 |
| MCP 子进程启动失败要同步抛错 | `command` 写错时 Node 只发 `error` 不发 `exit`，只监听 `exit` 会变成未捕获异常 + 永久挂起 |
| 子代理要在排队后与建会话后各查一次 `signal.aborted` | 只在监听器挂上后检查会漏掉排队期间发出的 abort —— 用户已停手而子代理继续烧 token |
| 审批/计划的按会话分键，拿不到 id 时退回 cwd | 全部塌缩成同一个键会让不同会话共享策略（审批那边踩过） |
| 六种匹配器在持久化那笔提交之前就有 | 「缺 glob / contains / prefix」是照文档推测的 |
| 技能目录只由 SDK `buildSystemPrompt` 注入一份（含 `read` 时） | 「`{{skills}}` 层是清单来源」——已删，双重目录 | 
| 提示词模板清单来自 `loader.getPrompts()` | 「自解 frontmatter」是最初的实现，已改回官方 |
| 回退 `summarize` 走 `navigateTree`，缺方法回落 `branch()` | 不是「只有 branch」，也不是「必须 navigateTree」 |
| HITL 反问基于官方 `ctx.ui`/`ExtensionUIContext`，只是补 WS 这个官方未实现的模式 | 「SDK 没有反问人类的能力，得自己造 waiter」——官方有接口，只是不给 WS 配回答者 |
| 注入 uiContext 用 `extensionRunner.setUIContext(uiContext, mode)` | 「用 `session.bindExtensions({ uiContext })`」——那也能设，但每次会重放 `session_start`+跑 `resources_discover`，给审计/守卫/审批重复触发启动逻辑，非纯增量 |
| HITL 后端能力完备、不依赖前端（任何 WS 客户端发 `extension_ui_response` 即可应答） | 「没前端这功能就用不到」——错：审批同理，`ctx.ui`/gate 是后端机制，前端只是一个输入通道，CLI/HTTP/程序化调用皆可 |
| Settings 字段名以 `dist/*.d.ts` 为准（`compaction{enabled,reserveTokens,keepRecentTokens}` 等） | 接口文档正文写的 `compaction.threshold/keepRecentTurns`、`images.maxDimension/quality` 是近似名，与真源有出入，按真实字段实现 |
| 导入外部会话 = 官方 `SessionManager.forkFrom` 复制进本目录再登记 | “直接引用外部路径打开”——错：导入必须落在允许根内（全新 id），与恢复同一道 fail-closed 闸 |

`c60b282` 的提交说明后半段仍粘着上一笔的旧文字，里面还有「避免通过 API 做的编辑丢失」。以本节和源码为准，不以那段提交说明为准。

---

## 7. 这份文档吸收了谁

下列文件**已删除**，内容收在本文里。它们互相矛盾（基线还停在「单 session、并发 429」「设置不落盘」「缺 run_end」「缺文件服务」），留着只会被当成过时结论的依据；需要追溯时看 git 历史。

| 已删除的旧稿 | 吸收了什么 | 为什么删 |
|---|---|---|
| `项目功能全面分析.md` | 早期脚手架的定位、分层、刻意不做的边界 | 写于 WS / 审批 / 文件服务之前，当现状读是错的 |
| `pi-web-ui后端功能全景分析.md` | 第 2 节的对方边界、八个功能域、不照搬的理由 | 文首与差距矩阵把我们已经补上的骨架仍写成缺口 |
| `docs/功能边界对比.md` | 「我们更好、不要退回」的九条；差距来自定位而不是谁更全 | 第一梯队已完成，文中仍写成待办 |
| `docs/待补功能清单.md` | 第 5 节的顺序；复核记录 | 前半段把已完成项仍写成「还缺」，并再次写出不存在的 `registerCommands()` |
| `docs/backend-architecture-plan.md` | 第 1 节的五条原则；对方八域里哪些是专用装备 | 验证数字停在 54 项测试，方案状态已过时 |
| `docs/quality-assessment.md` | 非 loopback 必须加鉴权；默认档位保持安全；「声明了必须有人消费」 | 「设置不落盘」已不成立；SQL 绕过问题后续已收紧 |

> `docs/` 下的分工（2026-10-10 核对文件系统）：**手写、随版本维护**的是本文件（能力与边界）、[`使用指南.md`](使用指南.md)（三种形态怎么跑）、[`嵌入指南.md`](嵌入指南.md)（装进已有服务）、[`智能体视角评估.md`](智能体视角评估.md)（一轮完整自评：做得好的 / 当前真实问题 / 提升顺序）；**由 `scripts/visualization/*` 生成**的是 `参考手册.md`（接口面唯一清单）、`architecture.svg` / `sse-protocol.svg` / `knowledge-retrieval.svg`，以及 `project_overview/`（`index.html` 全景页 + `script.js` + `style.css` + `project_card.html` 单图名片，后者自带导出 PNG）——数值都出自同一份 `scripts/visualization/metrics.mjs`，`npm run docs:check` 挡漂移；**参考稿 / 历史稿（不随版本更新）**是 `官方SDK接口文档.md`、`前端调研.md`、`assistant-ui.md`（后两篇写于前端落地之前，现状见 `web/`）、`engineering-review.md`（走查快照）、`项目分析报告.md`（已归档）。原先这里写的「静态全景页 `可视化总览.html`」从未存在过，真实路径是 `docs/project_overview/`。

`README`、`README.zh-CN`、`CHANGELOG`、`CONTRIBUTING`、`FAQ`、`SECURITY` 不属于这份对照稿，各自维护。
