# pi-starter · 智能体视角评估

> 审查对象：`pi-starter` 工作区（含当前未提交改动）。
> 审查方式：**实际读取源码 + 实际执行全部门禁**，不是对 README 自述的复述。
> 审查立场：见 §1。本文档与既有的 [`项目分析报告.md`](项目分析报告.md) 是两份独立文档；后者已系统性过期（见附录 C），**本文档不重复其结论**，凡引用均标注是否仍成立。
> 实测日期：2026-10-10。

---

## 0. 一句话结论

**这是一个「工程纪律明显高于同类脚手架平均水准」的项目，但它当前最大的问题不是代码，而是「自我描述」与「自我认知」的失真。**

拆开说三句：

1. **作为壳子，它是我见过的最扎实的一类。** 门禁链条完整且真实（505 后端用例 + 19 前端用例 + 23 项 WS 冒烟 + 44 项真进程 e2e + 8 项嵌入验证，另有 `docs:check` 挡六面生成物漂移，全绿），fail-closed 原则贯穿每一条 I/O 路径，注释写的是「为什么」而不是「做了什么」，很多条目附了真实踩坑记录。
2. **作为「通用智能体」，它目前是一个骨架完整但手脚偏少的个体。** 默认档我手里只有 7 个工具，**没有任何联网能力**，**没有跨会话记忆**。而 `rules.md` 第 1 条又要求我「凡是需要事实的地方必须调用工具核实、不要编造」——对外部世界的事实，我既无工具可核、又被规则禁止编造。这是「通用」这个定位下最实质的能力缺口。（这句是**最初走查时**的判断：联网与记忆两项已在下面 §0.2 的 P7-1 / P7-2 记为已修，默认档工具数也随之下变；现状以那两行为准。）
3. **它 3 天写了 2.7 万行。** 59 个提交，从 `2026-10-08 00:31` 到 `2026-10-10`。这个速度本身是能力证明，也解释了为什么「文档一致性」和「注释与实现漂移」会成为主要缺陷——**代码跑在了文档前面，而且跑得很远**。

---

## 0.1 首版之后已处理 / 需要更正的地方（2026-10-10 同日回写）

**这一节存在的理由**：本文档 §4 P6 批评的正是「代码跑在文档前面」，所以它自己不能变成下一个过期文档。下面把「已处理」和「我写错了」都标出来；**§2–§7 的正文保留原样**，作为当时的判断记录。

### 已处理（含第一手验证）

| 原条目 | 处理 | 落点 |
|---|---|---|
| **P1-1** Unix `exec` 主路径继承 `process.env` | **已修**。改走 `childProcessEnv()`；`taskkill` 分支的注释同步对账 | `src/exec/runner.ts`；新增 `src/exec/runner.test.ts` 用例（断言 `PI_API_KEY` 读不到、`PATH` 必须保留） |
| **P1-2** 注释与实现矛盾 | **已修**（随 P1-1 一起对账） | 同上 |
| **P2-1** `getSessionStats()` 每周期全量过滤会话文件 | **已缓解**。按事件失效、周期内复用（只有流式增量不作废） | `src/session-hub.ts` 的 `sessionStats` / `stats()`；`src/integration.test.ts` 用例 |
| **P2-2** `estimateTokensCached` 注释谎称 O(new) | **已修**。签名改模板串（少两次分配）；注释改成「O(消息条数) 次 WeakMap 查询」这个事实 | `src/conversation/conversation.ts` |
| **P3-2** `navigateTree` 私有断言残留 | **已修**。收进 `sdk-adapter.ts` 的 `sdkNavigateTree()` | `src/sdk-adapter.ts` |
| **P3-4 / P6-4** README 数字手写、口径不一 | **已修**。改成脚本生成 + `docs:numbers:check` 进 `verify` 与两套 CI（实测：改了前端用例数后该检查立刻报漂移，说明它真的在挡） | `scripts/visualization/generate_readme_numbers.mjs` |
| **P5-1** 无覆盖率门槛 | **已修**。`scripts/coverage.mjs`，Node 内置覆盖率（零依赖），棘轮阈值 lines 92 / branches 81 / functions 85，只看 `src/**` | 已接入两套 CI |
| **P5-3** 前端依赖未审计 | **已修且加严**。两套 CI 都补了前端审计；因一条**无可用修复**的开发依赖通告会让裸 `npm audit` 恒红，现改为基线化闸门：生产依赖 0 高危（硬门）+ 开发依赖已知通告登记、只对新增报错（`web/scripts/check-audit.mjs`）。`npm run verify` 也补了同名步骤，本地与远端口径一致 | `ci.yml` / `.cnb.yml` / `web/scripts/check-audit.mjs` / `scripts/verify-audit.mjs` |
| **P6-1 / P6-2** `项目分析报告.md` 系统性过期且自相矛盾 | **已修**。改为归档页：标注历史快照 + 「当时判断 → 现在状态」逐条对照 + 指向三个现状来源 | `docs/项目分析报告.md` |
| **P6-3** `能力与边界.md` 三处过期 | **已修**（基线、HITL 前端已接、docs 清单） | `docs/能力与边界.md` |
| **P7-2** 无跨会话记忆 | **已修（默认开）**。新增 `src/memory/store.ts` 与 `remember` / `recall` 两个工具，落盘成 JSONL（默认 `~/.pi/agent/pi-starter-memory.jsonl`，原子写），重启后仍在。有界：单条 4KB / 总量 2000 条 / 文件 4MB；同正文重复写入是覆盖不是新增。可注入（`buildAgent({ memory })`）、可关（`PI_MEMORY=off`，此时两个工具都不注册）。`GET/POST/DELETE /memory` 暴露同一份 store。这是 §6 里点名的「让『通用』这个词兑现的最后两块砖」的第二块 | `src/memory/store.ts`、`src/tools/memory.ts`、`src/http/routes.ts`；e2e `scripts/e2e-restart.mjs` 断言「写 → 落盘 → 重启后仍在」，8 项新增检查 |
| **P7-1** 无联网工具 | **已修（默认关）**。`web_fetch` / `web_search` + 可注入 `WebClient` + SSRF 防护（含 DNS 与重定向后校验）+ 字节/超时上限。默认关的理由与 `coding` 档一致：出站网络是数据外泄通道 | `src/tools/web.ts`；`SECURITY.md` / `.env.example` / 两 README 都已注明**未被缓解**的部分 |
| **P4-1** 前端适配层零测试 | **部分处理**。新增 `logClient.test.ts`（8 例，前端用例 11 → 19）。`usePiRuntime.ts` 仍是 React hook，无头测试需要 DOM 测试环境（jsdom + testing-library），属于**要引入依赖的独立决定**，未擅自加 | `web/src/pi/logClient.test.ts` |
| **§5 第 1 步**：按覆盖率点补测试 | **已做两批，命中率极高**。第一批 `file-routes.ts` 43% → 100%（挖出 4 个真 bug，见下）；第二批 `prompt-images` 44% → 100%、`audit` 56% → 100%、`ask-user-question` 56% → 100%、`knowledge` 71% → 100%、`database` 73% → 98.7%、`approval-routes` 71% → 100%、`current-time` 70.6% → 100%（又挖出 1 个：模型写 `UTC+8` 会直接抛 `RangeError`）。全项目覆盖率节节抬高（2026-10-10 实测 `npm run test:coverage`：all files lines 92.90 / branches 81.51 / functions 86.30，505 用例；阈值住在 `scripts/coverage-thresholds.mjs`，以它的实时输出为准） | 见 `CHANGELOG` 的 `[Unreleased]` |
| **§4 P6 的根因**：能力面跑赢了文档面 | **已修**。新增 `docs/参考手册.md`（**从源码生成**）：34 条 WS 客户端命令 + 21 个服务端帧（含分组与 JSDoc 摘要）、47 条 REST 路由处理器、工具清单（按装配档位 + 能力标签 + 风险）、全部 58 个环境变量、全部 npm 脚本、模块地图。此前：WS 协议有 55 个消息类型（34 + 21）却**没有任何参考文档**，REST 有几十条路由而 README 只列了 4 个，58 个环境变量里有 22 个在两份 README 里一个字都没有。**注**：路由数早先手写的是「60+」，那是生成器用裸 `.get(` 计数的结果——`search.get("limit")` 这类 URLSearchParams 读取被算了进去；现在 47 出自 `scripts/visualization/metrics.mjs` 的 `app.` / `router.` 口径，README 表、参考手册、全景页三处同一份 | `scripts/visualization/generate_reference.mjs`、`scripts/visualization/metrics.mjs`；生成器自带一致性断言（见 §0.2） |

### 我写错了 / 需要更正的（重要）

- **§4 P5-5「`release.mjs` 不提供回滚」不准确。** `--dry-run` 早就存在（`scripts/release.mjs` 第 2 步就跳过门禁并预览回写、不写盘不提交不推送），推送失败时也已经给出精确的补推命令。我漏看了。本轮只补了「撤销本地 tag / 回退回写提交」的步骤说明。
- **§4 P3-3「应删掉 `createSessionHub` 位置参数旧签名」是错建议。** 它是**有意保留**的兼容壳（已 `@deprecated`，并有具名替代 `createSessionHubFromOptions`），删掉是破坏性变更，与 CHANGELOG 里「既有嵌入方零改动」的承诺直接冲突。**保持原样**。
- **§4 P1-3 的措辞批评部分不成立。** README 与 SECURITY.md **在 0.3.0 里已经降调**（明确写了「不是沙箱、`bash` 命令体无路径约束、正则可被绕过、要真隔离用容器」，CHANGELOG 有「文档降调」条目）。README 第 84 行的 "Hardened by default" 只列举 HTTP 层硬化（loopback / 安全头 / body 上限 / 超时 / CTE SQL），**没有**声称能约束 shell，所以那句话本身是准确的。
- **§4 P2-3 / P2-4 属于「写了但不值得做」。** `currentMessages()` 的两趟是**语义必需**（工具结果必须在投影第一条 assistant 消息**之前**全部收集齐，否则无法挂载）；`SnapshotEmitter` 的逐元素比较换成「长度比较」会**削弱一个正确性检查**，换来的收益在噪声量级。两条都不动，注释已写清原因。
- **§4 P3-5（子代理并发/队列硬编码）不是问题**，是「限额写死在代码里、便于审计」的取舍，改走 settings 只会让上限变成运行时可变。
- **附录 B 的规模数字已过时**（本轮新增了 `src/tools/web.ts` 等）。**不要再引用本文档的数字**，以 README 的 Numbers 表为准。
- **§2.4 表格里「运行时设置 | 否，重启重置」是错的（2026-10-10 走查补正，已就地改掉该行）。** 随包提供的 `src/server.ts:97-109` 给 `SettingsService` 注入的是 `fileSettingsPort(defaultSettingsFile())`，落盘到 `~/.pi/agent/pi-starter-settings.json`（`src/settings.ts:186`、`:258-261`，原子写 + 损坏回落），项目自己的 e2e 就断言了「改 → 落盘 → 重启后生效」（`scripts/e2e-restart.mjs:408-446`）。正确的边界是：**只有「库内直接 `new SettingsService()` 且不传 port」才是纯内存**。当时那句 `README 亦声明` 引的是英文 README 里同样错误的一句话，现已一并更正。原先只在本清单里补正、正文表格仍留着错话，等于同一份文档里既有错误结论又有它的更正；本轮把 §2.4 那一行与 §5 第 15 条的下游引用一起就地改对。

### 0.2 参考手册的生成器不只是文档，还是一个检查

`generate_reference.mjs` 在生成过程中带**硬断言**，任何一条不成立就非零退出：

- 从 `ClientMessage` union 解析出的命令集合，必须与 `protocol.ts` 里那个有编译期守卫
  （`Exclude<ClientMessage["type"], typeof CLIENT_MESSAGE_TYPES[number]> extends never`）的常量完全一致
  —— 等于拿编译期保证来校验这个解析器本身。
- `src/tools/*.ts` 里出现的每个工具名，必须在生成器的分组表里被登记；反之亦然。
  **新增一个工具却忘了归类，门禁直接红**，而不是让文档悄悄少一条。
- 代码里引用的 `PI_*` 必须都在 `.env.example` 里登记过。

写这个解析器时踩到一个真实的坑，记录在此：`rollback_conversation` 分支**内部**有一个单行 JSDoc
（`/** 走 SDK 官方 navigateTree ... */`），而按「找下一行 `*/`」的写法会把后面十几个分支全部吞掉 ——
表现是解析结果莫名少了 19 条命令。断言就是在这种情况下救的场：它不会静默生成一份缺条目的文档。

另外**刻意没做**的一件事：给 34 条协议命令强制要求 JSDoc。当前说明覆盖率是客户端 4/34、服务端 7/21，
手册如实把覆盖率印出来；但把它设成门禁只会逼出一批「为了过检查而补的空说明」，那比没有说明更糟。

### 0.3 关于覆盖率的两个坑（避免下一轮白干活）

1. **`ws.ts`（76%）与 `agent.ts`（66.5%）的数字是误导性的**，别照着它去补单元测试。它们的未覆盖段（`ws.ts` 的 191-263 / 275-304 / 307-342 等、`agent.ts` 的 112-343 即 `buildAgent()` 本体）**是被 `npm run smoke` 与 `npm run e2e` 覆盖的** —— 那两个套件跑在**独立进程**里，所以它们建立的覆盖率不会进入 `npm test` 的统计。真实覆盖率不低，只是「在这里看不见」。
   要在进程内覆盖它们，需要给 `buildAgent` 加一个会话工厂注入点、或引入 `--experimental-test-module-mocks` 去桩掉 SDK —— 都是**有设计代价的改动**，收益却不明确（已经有真进程 e2e 在验它们）。所以在没有明确缺陷驱动之前，不建议动。
2. **「覆盖率低」和「值得补测试」不是一回事。** 这一轮真正有价值的是 `file-routes.ts` 与 `current-time.ts` 这类：**代码在关键路径上、行为有分支、却从未被执行过**。而 `embeddings-transformers.ts`（53.77%）低是因为它需要一个可选依赖，`exec.ts`（78.97%）低是因为进程树操作难以在单测里稳定复现 —— 这两类补测试的性价比明显低得多。

### 0.4 第三轮：把「只测了假后端 / 只测了跨进程」的两处补成真实执行

第二轮之后剩下的两个可疑点是**测试真实度**问题，而不是覆盖率问题：

- `web.ts`（我新写的联网模块）**只测了假后端** —— `fetchPage` 里的字节上限、重定向、超时、content-type 判定**从未真实执行过**。补测时用本地 HTTP 服务跑真实抓取（注入 host 策略，否则回环地址会被自己的 SSRF 防护挡住），**当场看出两个 bug**：
  1. `readCapped` 写的是 `total + value.length >= maxBytes`，于是正文**恰好等于**上限时误报「已截断」——一个字都没丢却对模型说截断了。
  2. **重定向在 `fetch` 跟完之后才校验**：`redirect: "follow"` 意味着 `302` 到内网时请求**已经发出去了**，事后检查只能阻止「读回内容」，挡不住「内网端点被触发」。已改为 `manual` 自己跟、**每跳发请求前校验**、共用超时、`MAX_REDIRECTS` 封顶；回归用例断言「被禁主机收到 0 个请求」。
- `ws.ts` 的低覆盖率不是没测，而是**只被跨进程的 smoke 覆盖**。补了进程内分派层测试后，确认错误翻译是对的（顶层 catch → `clientErrorMessage`），并锁住「一连串畸形输入之后 `ping` 仍必须得到 `pong`」。

结论仍与 §0.3 一致，但更具体：**「测过」要问是哪种测法** —— 假后端测不出真实字节/重定向行为，跨进程测不进程内统计。问「这段代码有没有被真实执行过」，比问「覆盖率多少」有效得多。

覆盖率 91.43% → **92.26%**（当时 443 用例，棘轮随后一路抬到现在的 lines 92 / branches 81 / functions 85，见 `scripts/coverage-thresholds.mjs`）。

### 0.5 工具层的坑：`--experimental-test-coverage` 的「未覆盖行号」不可信

`node --test` 是多进程的，覆盖率按进程采集后汇总。实测发现**百分比大致可信，但「uncovered lines」那一列不可信**：同一份 `routes.ts`，只跑 `app.test.ts` 时报 `142-152` 未覆盖，全量跑时报 `143 146-151` —— 两次的列表互相都不是对方的子集，而那些行明明都被执行过（对应用例是通过的）。

**所以选目标时只看文件级百分比，把行号列表当提示而不是依据。** 这条与 §0.3 是同一个教训的两面：覆盖率的**数字**和**位置**都会骗人，唯一可靠的判据是「这段代码有没有被真实执行过、断言够不够硬」。

顺带一个正面结论：`docs/check` + 覆盖率棘轮这套门禁本身是有效的（数字一变就红），问题只在于**解读**它。

### 0.6 第四轮：本地 Windows 与远端 Linux 的口径不对称

这一轮的每一条几乎都派生自同一件事：**真正生效的远端门禁 `.cnb.yml` 跑在单个 `node:24`（Linux）容器里**（`.github/workflows/ci.yml` 虽写了三 OS 矩阵，但本账号的 Actions 因计费跑不起来，一行都没跑），而开发机是 Windows。于是「本地根本跑不过、远端一直绿」攒了七处，全是本轮才第一次被执行到。**反向的那一侧也补上了一刀**：`docs:overview:check` 后来在远端红过一次而本地全绿，成因是 CI 容器不继承开发机的 `core.quotePath`，`git ls-files` 把中文文件名转义了——所以本地绿与远端绿是两个独立的命题，两个方向都要实测。

| 发现 | 根因 | 处置 | 一手依据 |
|---|---|---|---|
| `docs:check` 在 Windows 假报漂移 | `path.relative` 把反斜杠写进生成物 | 两处 `toPosix()` | 摘掉 `tools/web.ts` 的登记后，断言只报 `web_fetch, web_search` —— 它确实在挡 |
| `docs:reference:check` 把 13 个工具全报成「未登记」 | 同上，但坏的是比对键：`toolsByFile` 的键是 `\`，`TOOL_TIERS` 里手写的是 `/` | 同上 | 真漏登记的那条原先会被淹死 |
| `setup` 两例权限断言必红 | Windows 的 `chmod` 只置只读位，`0600` 与 `0644` 在 `stat.mode` 上不可区分（实测恒 `0o666`） | 功能面全平台跑，权限面 `t.skip(原因)` | 14 例：12 通过 / 2 显式跳过 / 0 失败 |
| `guard` 的符号链接用例 `EPERM` 硬失败 | 无特权建不了文件符号链接，而 junction 只对目录有效，换不成「字面名无害 → 真实目标是 `.env`」这个形状 | `t.skip(原因)` | 同上 |
| `files` 的 denyNames 符号链接用例 | `catch { return; }` —— 从未执行过断言的用例显示成通过 | 改 `t.skip` | 这是 §0 那条禁令在本仓的最后一处违例 |
| `verify:audit` 恒红，而 `check:audit:ci` 恒**绿** | 同一次坏掉的 spawn 两个方向都错：`execFileSync("npm")` ENOENT；裸 `catch` 报成「审计失败」；`shell: true` 又让 cmd.exe 的「命令不存在」伪装成退出码 1，被归进「网络故障」退 0 | 调用前先探 `npm --version`（`scripts/npm-invocation.mjs`），通过 / 有通告 / **无法执行** 三种结论分离，「无法执行」永不退 0 | 负向：候选路径全改坏 → 两者都退 2（原先一个恒红、一个恒绿）。正向：Windows 上审计真跑通，生产依赖 0 高危、1 条已登记通告在基线内 |
| `publish:local` 的 CNB 后端「预检通过 → 写 Release 时崩」 | `cnbReady` 经 shell、`cnbRun/cnbQuery` 不经 shell；且 `cnbQuery` 把 spawn 失败 `catch` 成「Release 不存在」，dry-run 预览因此说「将新建」 | 从 `.cmd` shim 取 JS 入口直跑；取不到按无法执行退 2 | PATH 去掉 cnb → 退 2；`--dry-run` 能真查到 `v0.3.0` 的既有 Release |
| 基线烂掉不报 | 已登记的通告不再出现时只打印一句提示 | 判失败，且**需复跑确认** | 实测撞见过一次 `npm audit` 返回**不含通告**的报告，与「上游修好了」完全同形 —— 单次定罪就是假阳性。正向 EXIT=0；负向塞一条假 GHSA → EXIT=1 |

同轮还改掉一条与平台无关的：**4xx 的错误行带完整堆栈**（`toLogFields` 原先无条件 `stack`）。栈顶永远是校验器自己那一帧，零排障信息，却能按请求规模放大；证据就留在仓库里 —— 一份被遗留的测试 stderr 中，两条**通过**的用例各吐了一整份 `AppError` 堆栈。现在只有 `>= 500` 或原文刻意不出服务端才带栈，`message / details / request_id` 与级别都不变。

两条**没做到**的，如实标注：

- `README` 的截图 TODO 仍在。环境里内置浏览器没有可见 surface（`viewport=0x0`、`visibilityState=hidden`），拿不到真截图；不用伪造图，也不为这条引入 playwright/puppeteer。运行时验证做到了另一层：`npm run web` 真启动，`GET /health/ready` 三项 `ok`（model `modelscope/deepseek-ai/DeepSeek-V4-Flash-0731`、database `sqlite :memory:`、knowledge `1 docs`），前端结构快照显示 35 条历史会话被接回、模型下拉 54 项、上下文与费用条、日志面板入口都在。
- 覆盖率棘轮的余量变薄了：`lines 92.88 / branches 81.52 / functions 86.38`（阈值 92 / 81 / 85）。上调之前先跑一次看真实数字，别凭印象。

### 仍未处理（明确留给下一轮）

`P7-3` 目标任务循环 /`P7-4` 多模态入口 /`P7-5` 跨客户端会话接管 / `P4-3` i18n·a11y·离线重发 / `P4-5` 前端 e2e / `P4-1` 的 `usePiRuntime` 部分 / `P1-5` 收窄 FileService root / `P2-1` 的根治（需向 SDK 提 issue）。理由见 §5 对应段落：这些要么需要业务决定，要么需要先有 DOM 测试基建。

`integration.test.ts` 的到达等待已全部改成条件等待（17 处 `settle(N)` → `waitError / waitErrors / waitFrame / waitState`，都建在 `test-server.ts` 的 `waitFor` 上）；只剩 2 处 `settle` 是**负向命题**的等待（「服务端处理完这次 close 之后仍然没有多余帧」），各自带注释。**并跑两个全量测试**从「两边各崩 4 例」变成「两边 `505 用例：498 通过 / 7 平台跳过 / 0 失败`」（那 7 例是 Windows 上权限位与符号链接的平台跳过，逐条打印原因，不算通过），单跑该文件从 9.2s 降到 2.8s。

> 原先这里还列着 `P3-1` 拆 `session-hub.ts` —— 已经做完了：`session-hub.ts` 从 2102 行降到 492 行，`Conversation` 与 `ClientSession` 分别迁到 `src/conversation/conversation.ts`（1137）与 `src/client-session.ts`（541），§5 第 4 步的标注是准的，是这一行没跟着改。

---

## 1. 审查立场：什么叫「站在智能体的角度」

我按两层来评：

- **第一层：我在这里当智能体是什么体验。** 我的身份从哪来、手里有什么、被什么挡住、能记住什么、出错会怎样、能不能看见自己。这一层决定「这个壳子作为通用智能体是否成立」。
- **第二层：造这个壳子的工程本身做得怎么样。** 架构、安全、性能、可维护性、测试、文档、交付。

两层结论会放在一起，因为它们互相印证：**壳子造得很硬，个体体感却很薄**——这不是矛盾，是「脚手架」这个定位的自然结果，但需要被明确指出来。

---

## 2. 我在这里的自画像（第一层）

### 2.1 我的身份从哪来

| 项 | 事实 |
|---|---|
| 人格 | [`src/prompts/persona.md`](../src/prompts/persona.md)，703 字节 |
| 工作约束 | [`src/prompts/rules.md`](../src/prompts/rules.md)，1,561 字节，9 条 |
| 系统提示词组装 | `src/prompts/composer.ts`（4,267 字节） |
| 隔离策略 | `noExtensions: true` / `noSkills: true` / `noPromptTemplates: true` / `noContextFiles: !includeAgentsFiles` |

`persona.md` 给我的定义是：「你是 **Pi Starter**，一个基于 pi-agent SDK 构建的通用智能助手」。回答风格要求「结论先行、简洁结构化、用中文」。

**评价：身份定义是「最小可用」的。** 它没有领域知识、没有语气样例、没有价值观/拒答边界（拒答边界只靠 `rules.md` 第 4 条「安全边界」一条兜着），也没有「我是谁、我不做什么」的负面清单。作为脚手架这是**正确的取舍**（业务要覆盖的就是这段），但作为「你就是这个产品」的默认体验，它读起来像占位符而不是人格。

`rules.md` 反而写得很好：9 条全是**可执行的动作约束**，不是空话。「不编造数据」「单步原则」「工具失败先读错误再重试一次」「exec 不是交互式终端」「相对路径相对技能目录解析」——这些是我真正能照着做的规则。**这是这个项目里我最欣赏的一小段文本。**

### 2.2 我默认手上有什么

默认档 `PI_BUILTIN_TOOLS=off`，`sessionToolPolicy("off")` 返回 `{ tools: ["read", ...extra] }`（`src/config.ts:602-615`）。合并后的默认工具面：

| 工具 | 来源 | 能力 |
|---|---|---|
| `read` | SDK 白名单（off 档唯一内置） | 读文件；**技能正文必须靠它加载**（`<available_skills>` 只给目录） |
| `current_time` | `src/tools/current-time.ts` | 取当前时间 |
| `ask_user_question` | `src/tools/ask-user-question.ts` | 向人反问（HITL，前端已接弹窗） |
| `search_knowledge` / `read_knowledge` | `src/tools/knowledge.ts` | 检索/读取 `src/knowledge/*.md` |
| `db_status` / `db_query` | `src/tools/database.ts` | 连通性 / 只读 SELECT |

**要点：**

- **没有 `bash` / `edit` / `write`**（要 `PI_BUILTIN_TOOLS=coding` 显式打开）。这个默认是对的：README 的理由很实在——「我们**总是**传 `tools`，它就成了 SDK 的硬白名单；不传才是开启全套」。所以必须显式打开，而不是靠「没传」意外获得。
- **没有 `exec` / `exec_jobs` / `exec_stop`**（同样只在 coding 档）。
- **完全没有联网工具。** 全仓库 `src/tools/` 里没有任何 `fetch` / HTTP 客户端类工具，MCP 桥虽然存在但默认不接任何 server。

**这是「通用智能体」定位下最实质的缺口。** 我能读本地文件、查本地知识库、查本地 SQLite、问人、看时间——但**我看不见互联网**。当我被问到「这个库最新版是多少」「这个 API 现在怎么用」「今天发生了什么」时，我的正确行为是 `rules.md` 第 1 条：承认无法核实。这个行为是对的，但它意味着**我在通用场景下的可用半径被压得很窄**。

### 2.3 我被允许做什么 / 被什么挡住

四层拦截叠加，顺序即优先级（`src/extensions/index.ts` + `src/approval/` + `src/modes/plan-mode.ts`）：

```
SDK tool_call
 ├─ guard 扩展     ：危险命令正则（extensions/shell-rules.ts 单一事实源）
 │                   + realpath 路径双重校验（guard.ts:94-103）
 │                   + 敏感文件名拦截（secret-files.ts）
 │                   + exec 的 cwd 越界
 ├─ plan-mode 扩展 ：写类工具 → 硬 { block }
 ├─ approval 扩展  ：deny（不可被策略覆盖）/ ask（问人，超时判拒）/ allow
 └─ 执行 → tool_result → audit 扩展（耗时日志）
```

还有一层**非拦截的资源闸门**：`ToolWatchdog`（工具超时默认 20 分钟 → abort）。

**值得单独表扬的设计：**

- **`deny` 不可被策略覆盖**（`src/approval/policy.ts`）。这是真正的安全属性，不是「照抄参考实现」。很多同类项目的审批是「策略可覆盖一切」，那样 `deny` 就退化成了默认值。
- **审批超时判拒而不是判允**（fail-closed 的正确方向）。
- **危险命令表已从两份漂移的正则合并为单一事实源**（`src/extensions/shell-rules.ts`，guard 与 approval 共用）。
- **realpath 校验已补齐**：`guard.ts` 现在会解析符号链接（`realpathOfNearestExisting`），与 `FileService` 同强度——**旧报告里「三层保护强度不一致」这条已经不成立**。

**但当前仍有三个真实缺口（已核实，非旧报告残留）：**

1. **`bash` 命令体本身没有路径约束。** `src/extensions/guard.ts:128` 命中正则后 `if (isToolCallEventType("bash", event)) return undefined;` 直接放行——命令里 `cd /`、`cat /etc/passwd`、`curl` 外网都不经过 `isPathInsideCwd`。项目**自己声明过这不是沙箱**（README 与 SECURITY.md 都写了），所以这是「已声明的取舍」而非隐瞒；但 README 的 "Hardened by default" 措辞容易让人高估强度。
2. **审批默认关闭**（`src/settings.ts:265-266`：`toolApprovalEnabled: false` + `approvalMode: "off"`）。叠加 `off` 档内置工具默认关闭，整体风险可控；但要说清楚：**默认态下真正生效的只有 guard 的正则与 `deny` 类规则**，`rm -rf` 这类 `ask` 规则是静默放行的（靠 guard 正则兜住）。
3. **Unix `exec` 主路径仍继承 `process.env`**（`src/exec/runner.ts:432-434`），而 `src/child-env.ts` 的「剔除 `PI_API_KEY*`」只接进了 MCP（`mcp/client.ts:80`）与 Windows 分支（`runner.ts:426`）。**且 `runner.ts:474` 的注释声称「凡 spawn 的地方口径一致，避免漏一个」，恰好漏的就是 Linux/macOS 主路径。** 这是本轮评估里最值得优先级最高的一条——它同时是安全问题、也是文档一致性问题。

### 2.4 我的记忆边界

| 类型 | 是否持久 | 证据 |
|---|---|---|
| 会话历史（消息、工具往返） | **是**（`inMemory: false` + `sessionDir` + `SessionCatalog`；重启后 `open_conversation` 可接回） | `src/sessions/store.ts`、`src/agent.ts:650-670` |
| 会话树编辑（改名/回退/编辑/分叉/真删除） | **是** | `src/sessions/edit.ts` |
| 条目标签（书签） | **是**（`SessionManager.appendLabelChange`） | `src/session-hub.ts` `labels()` / `setLabel` |
| 运行时设置（template / disabledTools / thinkingLevel / approval） | **是**（随包 server 落盘；只有库内直接 `new SettingsService()` 且不传 port 才是内存） | `src/server.ts:97-109` 注入 `fileSettingsPort(defaultSettingsFile())`；`src/settings.ts:186,258-261`；e2e `scripts/e2e-restart.mjs:408-446` |
| 跨会话长期记忆 / 用户画像 | **不存在** | 全仓库无此机制 |

**这是第二大能力缺口：我没有「上一次」的概念。** 会话历史可以把**同一段对话**接回来（而且做得很好，e2e 里专门验证了工具调用配对不悬空），但**新开一段对话我就是从零开始的陌生人**。对通用助手来说，「记住用户偏好、记住上次结论」是体验的核心部分，这里完全没有。

### 2.5 我出错时会发生什么

这一层做得比大多数项目细：

- **错误分层**：`AppError.safeToExpose` 默认 `false`；`internal` 的真实文案只进服务端日志，客户端拿通用文案。
- **SSE 内的错误翻译**：`/chat` 状态码已提交后不能改状态码，项目**在 SSE 帧内复用同一套暴露规则**翻译错误——这个坑踩得很清楚（README 与代码注释都记录了）。
- **自动重试可见**：SDK 的 `auto_retry_start` / `auto_retry_end` 被翻译成 notice 帧，我重试时用户看得见「第 N 次恢复 / 最终失败」，而不是静默卡住。
- **工具超时**：watchdog 到点 abort 本轮并推 notice。
- **优雅停机**：`src/graceful.ts` 的兜底定时器**先建后跑步骤**（注释记录了原实现「进程永久挂死」的真 bug）。
- **原子写 + 损坏回落**：settings / approval rules / provider-keys / session index / plan-mode 五处统一（tmp + rename + 失败告警但不回滚内存）。

### 2.6 我能不能看见自己

能，而且看得比大多数项目清楚：

- **结构化日志 + 自动值级脱敏**（`src/log.ts`）：递归深度受限、Error 特殊处理（否则 spread 会丢 `message`）。
- **14 个运行时指标**，JSON 与 Prometheus 双格式（`/metrics`）。
- **liveness / readiness 分离**（`/health` 不碰依赖，`/health/ready` 503 报依赖故障）——这个区分让编排器不会「模型挂了就重启进程」，是成熟做法。
- **日志查询/分页 REST**（`src/http/log-routes.ts`，443 行，含字节偏移无损分页）。
- **AI 运行观测**：`logRunSummary()` 记录 model / stopReason / tokens / cost / contextUsage，且读数来自与快照同一个 estimator，保证「日志不会和客户端看到的不一致」。

---

## 3. 做得好的地方（第二层，按证据分 5 组）

### 3.1 架构与抽象

1. **协议单源 + 编译期完备性断言。** `src/protocol.ts` 是唯一协议源，新增命令漏登记会**编译失败**（`satisfies` + `Exclude extends never` 手法）。前端直接把 `src/protocol.ts` 映射成 `@pi/protocol`（`web/vite.config.ts:16-17`），两侧类型不可能漂移。这是全项目最干净的一处设计。
2. **双通道行为已收敛为共享函数。** `src/capabilities.ts`（REST `/capabilities` 与 WS `capabilities` 帧共用）与 `src/models.ts` 的 `toUiModel`——**旧报告「REST 与 WS 各写一份整形」已不成立**。更进一步，`agent.ts` 的 `resourceLoaderOptions()` 被 REST/WS 与官方 RPC 三条装配路径共用（`agent.ts:558-609`），从签名上消除「三条路径在隔离/人设/工具上分叉」。
3. **SDK 私有形状收敛到 `src/sdk-adapter.ts`。** 原先把 `session.compact` / `session.agent.state.messages` / `session.sessionManager` 等私有访问散在各处、每处各写一遍 `typeof` 兜底；现在收敛为 7 个带存在性检查的访问器（`sdk-adapter.ts:37-95`）。**残留 1 处**：`session-hub.ts:1190-1197` 的 `navigateTree` 仍就地断言。
4. **`resolveToolList()` 刻意在 `createSession` 内部求值**（`agent.ts:546-550`）。因为 SDK 的 `tools` 是构造后不可改的硬白名单，运行期新增的 MCP 工具只能「下次建会话生效」——这个反直觉的取舍被正确实现并注释。
5. **前端适配层设计扎实**（`web/src/pi/`，自研核心约 2.7k 行）：跨会话帧过滤（后端把每条已打开对话的帧都推到同一 socket，前端必须按 `conversationId` 过滤）、修订链断链自愈（`rev`/`baseRev` 不连续即请求全量快照）、指数退避重连（句柄可取消）、快照到达时以快照为准回填流式文本。这些都有回归测试。

### 3.2 安全与 fail-closed

6. **密钥纪律是教科书式的。** `provider-keys.ts` 的 `describe()` **在类型层面就不可能返回 `apiKey`**；落盘 0600；临时文件也 chmod。这堵住了「调试接口顺手把 key 吐出去」这一整类事故。
7. **路径校验双重且 fail-closed。** `assertSessionFileAllowed`（绝对路径 + `.jsonl` + realpath 在允许根内，**根 realpath 失败就丢弃这个根**而不是退回字面路径）在 `newConversation` 与 `resolveSessionManager` 各查一次——不依赖调用方注释。
8. **敏感文件名黑名单已补齐且有单源。** `src/secret-files.ts` 的 `DEFAULT_DENY_NAMES`（`.env` / `.env.*` / `*.pem` / `*.key` / `id_rsa` / `credentials*` / `auth.json`）同时支撑 `FileService.denyNames` 与 `guard`，连**符号链接的真实目标名**也拦（`files/service.ts:146-158`）。**旧报告 P1-1 的主体已修复。**
9. **`deny` 不可被策略覆盖**（`approval/policy.ts`）。
10. **子进程环境已做白名单裁剪**（`src/child-env.ts`）——MCP 与 Windows 路径已接入，Unix `exec` 主路径未接入（见 §2.3 第 3 条，这是残留）。
11. **不扫描本机 `~/.pi` 的扩展/技能/提示词模板**（`noExtensions` / `noSkills` / `noPromptTemplates`），避免「别人的机器配置污染这个垂类 agent」。这是很清醒的一条：本地 pi 扩展可以重新注册 `bash`。

### 3.3 资源与韧性

12. **封顶成体系且每一处都「报截断而不是静默失败」**：`MAX_SNAPSHOT_MESSAGES=500` / `MAX_INDEX_ENTRIES=500` / 每查询 200 行 / 每个知识文档 512KB / `MAX_JOBS` / `MAX_FINISHED` / `MAX_OUTPUT_BYTES` / `MAX_COMMAND_CHARS` / `MAX_USER_RULES` / `MAX_MCP_SERVERS` / 并发子代理 3 + 队列上限 32（`subagents/index.ts:48`）。
13. **背压处理在产品层**：快照可丢、慢客户端终止（smoke 里专门验证了「持续背压丢快照」与「慢客户端断开」）。
14. **工具看门狗**：挂死的工具不会永久阻塞一轮。
15. **审批超时判拒**；**`deny` 不可覆盖**；**plan mode 是硬闸门不是提示词软约束**。
16. **MCP `onError` 永远挂载**（`mcp/client.ts:83-85`）——因为 ChildProcess 的未处理 `error` 会直接带走进程。这个细节很少人做对。
17. **`node:sqlite` 只读 SQL 扫描是分词式的**，堵住了 `WITH x AS (DELETE …) SELECT …` 这类 CTE 写入绕过；且**已改为 `stmt.iterate()` 边取边判**（`db/index.ts:333-352`），不再先全量载入——**旧报告 P2-1 已修复，且注释与实现现已对齐**。

### 3.4 测试与门禁

18. **门禁链条完整且我实测全绿**（见附录 A）：`npm run verify` = typecheck → lint:unused（专杀「声明了但没接线」这类 bug）→ 后端用例 → 前端用例 → **docs:check**（README 数字表 / 参考手册 / 三张 SVG / 全景页与名片，六面逐字节比对，漂移即红）→ verify:audit（依赖审计，三种结论里「无法执行」永不退 0）→ 23 项 WS 冒烟 → build → 8 项嵌入验证；`npm run verify:all` 再加真进程 e2e（44 项：43 通过 / 1 平台跳过）。覆盖率棘轮是 CI 单独一步（`test:coverage`），不在 `verify` 里。
19. **e2e 打真进程重启**，并且**检查工具调用配对**：「每个 tool_call 都有对应结果（无悬空调用）」「没有孤立的结果」。这个断言很硬核——它验证的是恢复后的历史能被模型正确消费，不是「文件存在」。
20. **`verify-embed.mjs` 把「可嵌入」从口号变成事实**：8 项断言覆盖 `createApp` 返回值契约、内核路由、以及**三种鉴权挂法的实测结论**（挂在 `createApp` 之后无效、挂在 `configure` 里无效、挂父应用有效）。README 里那段「⚠️ Auth must sit on the parent app」是有实测背书的警告，不是经验之谈。
21. **`lint:unused` 与 `check-registry-sync.mjs` 是两类很有针对性的门禁。** 后者把前端官方 registry 组件落盘并**基线化**：只对**新增**偏离报错，已登记偏离放行，拉不到 registry 时退出 2（`--tolerate-network` 降级为 0）——避免了「网络抖动 = 假红」。
22. **用契约忠实的 session 替身驱动真实编排栈**（`integration.test.ts`），无需网络与 API Key 就能覆盖 `SessionHub → ClientSession → Conversation → SnapshotEmitter → WebSocket` 的 wiring 回归。这个测试策略选择是对的。

### 3.5 交付与文档（工程侧）

23. **`Dockerfile` 正确**：`node:22-bookworm-slim` 两阶段（先构建 `web/dist` 再拷入）、**非 root**（uid 10001）、并在文件头**反复强调必须 `-p 127.0.0.1:`**（服务无鉴权）。
24. **`SECURITY.md` / `CONTRIBUTING.md` / `FAQ.md` / `CHANGELOG.md` 质量明显高于同类。** SECURITY.md 明确列出「未受保护项」（guard 非沙箱、HTTP 无鉴权、单会话）；CHANGELOG 313 行按轮次记录，含第三方审计。CONTRIBUTING 的自检命令与 `package.json` scripts 对齐。
25. **注释解释「为什么」，且大量条目附真实踩坑记录。** 例如「`res.on("close")` 而非 `req.on("close")`——`req` 的 close 在 body 读完就触发，会误杀刚起步的一轮」「API key 写完不做 `port.load()` 回读，纯浪费 IO」「兜底定时器必须先建后跑步骤，否则进程永久挂死」。**这种注释密度是我评测时最信任的证据来源**——它让我能区分「作者想过」和「作者抄的」。
26. **文档可视化是生成的**：`docs/architecture.svg` / `sse-protocol.svg` / `knowledge-retrieval.svg` 三个图由 `scripts/visualization/generate_*.mjs` **从源码读取计数与事件名**生成，不硬编码。这是「从根上消除文档漂移」的正确方向。

---

## 4. 做得不好的地方（已逐条核实，只列**当前**成立的问题）

### P1 — 安全与一致性（建议本迭代处理）

| # | 问题 | 证据 | 性质 |
|---|---|---|---|
| P1-1 | **Unix `exec` 主路径仍继承 `process.env`**，`PI_API_KEY` 可被 `echo` | `src/exec/runner.ts:432-434`；`child-env.ts` 只接进了 MCP 与 Windows 分支 | 已宣布修好、实际漏了主平台 |
| P1-2 | **注释与实现直接矛盾**：`runner.ts:474` 称「凡是 spawn 的地方口径一致，避免漏一个」 | 同文件 `:434`（Linux/macOS 主执行路径 `env: process.env`）就是漏的那个；`:426` 与 `:475` 都用了 `childProcessEnv()` | 一致性 |
| P1-3 | **`bash` 命令体无路径约束**，正则表可被常规手段绕过 | `src/extensions/guard.ts:128` | **已声明的取舍**，但 README 措辞高估强度 |
| P1-4 | **审批默认关闭** → `ask` 类规则默认不生效 | `src/settings.ts:265-266` | 默认风险可接受，但需显式说明 |
| P1-5 | **`FileService` root 仍是 `process.cwd()`**，整个工作目录暴露为 Web 文件服务 | `src/server.ts:346` | 已有 `denyNames` 兜住已知敏感文件，但暴露面仍偏大 |

### P2 — 性能（快照热路径）

| # | 问题 | 证据 |
|---|---|---|
| P2-1 | **已缓解**：`getSessionStats()` 曾每周期调一次（SDK 内部是全量过滤）。现按事件作废、周期内复用（`stats()` 缓存） | `src/conversation/conversation.ts:172-179,572-575` |
| P2-2 | `estimateTokensCached()` 仍是全量循环（逐条 WeakMap 缓存，但未维护增量总和）——**刻意不动**，理由见下方说明 | `src/conversation/conversation.ts:707` |
| P2-3 | `currentMessages()` 每周期 2 趟 `session.messages`（收工具结果 + 投影）——**语义必需**，不是缺陷 | `src/conversation/conversation.ts:582` |
| P2-4 | `SnapshotEmitter` 增量判定仍是逐元素 O(n) 比较（可改为长度比较，投影引用稳定）——**刻意不动**，换掉会削弱一个正确性检查 | `src/snapshot.ts:81` |

> 说明：投影 + 会话树遍历的**双遍问题已修复**（`contextIndex()` 一趟同时取 entryIds 与 labels；`overBudget` 已用同一个总数短路，见 §7）。P2-1 后来**在项目侧做了缓存**：`stats()` 按事件作废、周期内复用，避开了「每周期全量过滤」，同时用「事件变更即失效」解决了当时担心的过期值问题（`src/conversation/conversation.ts:172-179,572-575`）。向 SDK 提 issue（建议内部增量维护）仍然值得，但那已经不是本项目内的成本。

### P3 — 可维护性

| # | 问题 | 证据 |
|---|---|---|
| P3-1 | **已解决**：`src/session-hub.ts` 曾 2,151 行三合一（`Conversation` / `ClientSession` / `SessionHub`）。现已拆分为 **492 行**，`Conversation` 与消息投影移到 `src/conversation/{conversation.ts,messages.ts}`，文件头注明职责边界 | `wc -l src/session-hub.ts` = 492；`src/conversation/` |
| P3-2 | **已解决**：`navigateTree` 连同它的私有断言已收进 `sdk-adapter`，本文件里不再有绕过封装的私有访问 | `grep -n navigateTree src/session-hub.ts` 无输出 |
| P3-3 | `createSessionHub` 8 位置参数旧签名仍在（已 `@deprecated`，已有具名替代 `createSessionHubFromOptions`，但旧入口未删）——**有意保留**的兼容壳，删它是破坏性变更，与「既有嵌入方零改动」的承诺冲突 | `session-hub.ts:472`（旧签名）/ `:447`（具名形式） |
| P3-4 | **README 里所有可验证数字仍是手写**（测试数、文件数、端点表），仓库只有**图**是生成的 | `README.md:50,349,230-236,537-` |
| P3-5 | 子代理的「并发 3 + 队列 32」是硬编码常量，未走 settings | `src/subagents/index.ts:48` |

### P4 — 前端（`web/`）

| # | 问题 | 证据 |
|---|---|---|
| P4-1 | **测试面很窄**：仅 `web/src/pi/client.test.ts`（7 用例）覆盖 `client.ts` 纯逻辑；**`usePiRuntime.ts` 适配层、`logClient.ts`、全部 UI 组件零测试** | `web/src/pi/client.test.ts`（309 行） |
| P4-2 | 未测的关键路径：重连退避/定时器取消、`hello`/`ready` 握手时序、`onRaw` JSON 容错、`tool_delta` 截断上限、`deleteConversation`/`approvalResponse` 正常路径 | 同上 |
| P4-3 | **无 i18n**（文案硬编码中文）、**无 a11y 体系**（仅零星 `aria-*`）、**无离线重发**（离线只提示不排队） | `web/src/App.tsx:87,91`；`client.ts:481-485` |
| P4-4 | **约 2/3 代码是官方 registry / shadcn 生成件**（`tool-fallback.aui.tsx` 1000 行、`thread.aui.tsx` 912 行…），自研核心约 2.7k 行 → 统计「前端规模」时容易误读 | `web/src/components/` |
| P4-5 | 无前端 e2e / mock（`web/package.json` 无相关依赖） | `web/package.json` |

> 值得记一笔的正面项：**ErrorBoundary 并未缺失**（`web/src/App.tsx:25-27,52`），且把适配层创建挪进边界**内部**（`App.tsx:33-47`，注释记录了「原先写在 App 自己的渲染里，边界抓不到」这个坑）——这与多数脚手架的常见缺口相反。

### P5 — CI / 发布

| # | 问题 | 证据 |
|---|---|---|
| P5-1 | **无覆盖率门槛**（无覆盖率工具与阈值） | 仓库全局 |
| P5-2 | **无 CodeQL / SAST** | `.github/workflows/ci.yml` |
| P5-3 | **已修**：`web/` 有独立 lockfile，当初只有根依赖被审计。现已加前端审计（基线化闸门，见 §0.1 的 P5-3 行） | `web/scripts/check-audit.mjs`、`scripts/verify-audit.mjs` |
| P5-4 | **`publish.yml` 的 npm 发布 job 被注释掉**，即「能上 npm」是未启用状态；GitHub Actions 因账号计费锁定不可用，真实门禁 / 发布靠 `.cnb.yml` + `npm run publish:local` | `.github/workflows/publish.yml:60-79`、`.cnb.yml:1-3` |
| P5-5 | `release.mjs` 会**自动 commit（`--no-verify`）+ 打 tag + 推送**，且**不提供回滚** | `scripts/release.mjs:282-357` |

### P6 — 文档一致性（**本项目当前最大短板**）

| # | 问题 | 证据 |
|---|---|---|
| P6-1 | **`docs/项目分析报告.md` 系统性过期**：把 0.3.0 已修的安全问题（`.env` 泄露、`PI_API_KEY` 继承、`db.query()` 全量载入、子代理队列无上限、metrics 抛错、`/files/upload` 二进制损坏、`rev: 0` 死字段）**仍列为「当前问题」** | `项目分析报告.md:20-26` |
| P6-2 | **同一文档内部自相矛盾**：§0 记 `smoke 23/23`、`e2e 39/0跳/0失败`，§8 表却记 `smoke 17/17`、`e2e 35通过/1跳过`；§1.2 标题写「90 个 .ts（56 非测试 + 34 测试）」，同节树状注释又写「42 个 `*.test.ts`」，而实际是 **118 个 `.ts`（含 42 个测试）** | `项目分析报告.md:14,318,321,36,51` |
| P6-3 | **`docs/能力与边界.md` 过期**：文首基线写 `e24c4c7（2026-10-08）`；称「HITL 前端未接（当前只完善后端）」但前端已挂 `HitlDialog`；称「现在 `docs/` 下只有三份活文档」与实际目录不符（且有文件已不存在） | `能力与边界.md:5,115,276`；`web/src/App.tsx:44` |
| P6-4 | **测试计数口径不统一**：`能力与边界.md` 记 387（376+11）、`项目分析报告.md` 记 376、CHANGELOG 记「后端 376 / 前端 11 / smoke 23」 | 三处文档 |
| P6-5 | `docs/` 未标注「生成 vs 手写」，读者无法判断哪些可再生成、哪些会过期 | `docs/` 全目录 |

> **这一组的严重性不该被低估。** 一个把「fail-closed」「可验证」当卖点的项目，它的自有文档却在把已修的安全问题当作现状陈述——这会让任何认真读完文档的人得出**比实际更差**的结论，也会让维护者重复劳动。代码跑在了文档前面，而且跑了很远（3 天 2.7 万行）。

### P7 — 通用智能体的能力缺口（针对「通用」这个定位）

| # | 缺口 | 影响 |
|---|---|---|
| P7-1 | **无联网工具**（无 `web_search` / `web_fetch` / HTTP 工具） | `rules.md` 要求「不编造、必须工具核实」，但对**外部世界**的事实无工具可核 → 通用场景可用半径被压窄 |
| P7-2 | **无跨会话长期记忆 / 用户画像** | 每次新对话从零开始，「记住我」不可能 |
| P7-3 | **无目标任务循环**（plan mode 是写操作的硬闸门，不是 goal-review loop） | 无法「定目标 → 拆解 → 跟踪进度 → 复盘」；README 已明确声明未做 |
| P7-4 | **无多模态入口**（无附件、无视觉桥） | 图片类通用任务不可达；README 已声明未做 |
| P7-5 | **无跨客户端会话接管** | 换设备接不回同一段对话（单客户端多对话，靠 `conversationId` 过滤） |
| P7-6 | **无鉴权**（声明过的取舍，但需重复强调） | 任何能连到端口的人 = 拿到 agent + 它的工具 |

---

## 5. 需要提升的地方（可落地建议，按优先级）

### 第 1 步：让文档回到诚实（半天，收益最高）

1. **修 `docs/项目分析报告.md`**：要么整体重写为本轮现状，要么**显式标记为「0.3.0 之前的快照，已过期」并指向本文档**。当前状态下它会主动误导读者，是最该先动的一处。
2. **修 `docs/能力与边界.md`** 的三处过期（基线 commit、HITL 前端未接、docs 清单）。
3. **统一测试计数口径**：建议固定为「后端 376 + 前端 11（+ 冒烟 23 / e2e 39 / 嵌入 8）」一种写法，并在 README、能力与边界、CHANGELOG 三处对齐。
4. **把 README 的可验证数字交给脚本**：既然已有 `scripts/visualization/generate_*.mjs` 的先例，把「测试文件数 / 用例数 / 端点数」也纳入生成，从根上消除 P3-4 与 P6-4。

### 第 2 步：补齐两处安全一致性（1 天）

5. **`runner.ts:434` 换成 `childProcessEnv()`**，并删掉/修正 `:474` 的注释。这是「一句话改动 + 注释对账」，性价比最高。
6. **把 `FileService` 的 root 从 `process.cwd()` 收到显式子目录**（如 `workspace/`），或至少在 `server.ts` 处对 `.env` 之外的高风险后缀做二次确认。`denyNames` 只堵已知名单，收 root 是收口。
7. **文档层面对齐强度措辞**：把 README 的 "Hardened by default" 换成准确描述（「有基础正则拦截 + realpath 路径校验，**不是沙箱**」），并在 `coding` 档显著提示「bash 命令体无路径约束」。

### 第 3 步：性能收尾（半天）——部分已做

8. **已缓解**：`getSessionStats()` 现按事件作废、周期内复用（见 §2 P2-1）。向上游 SDK 提 issue 仍然值得（建议 SDK 内部增量维护），但本项目侧不再每周期全量过滤。
9. `estimateTokensCached` 改为**维护增量总和**——**刻意不做**。投影引用虽然稳定，但 `branch_summary` / `compaction` 条目的 usage 会在消息数组不变时变化，维护增量总和会给出过期的 token 数（用户直接看得到的显示值），得不偿失。
10. `currentMessages()` 的「收工具结果」一趟可按 `(messages 数组引用, 长度)` 记忆化——**刻意不做**。两趟是**语义必需**（工具结果必须在投影第一条 assistant 消息之前全部收集齐），记忆化只省常数，不改变复杂度。
11. `SnapshotEmitter` 增量判定改为**记录 `emittedLength`** 而非逐元素比较——**刻意不做**。逐元素比较是一个正确性检查，换成长度比较会削弱它，收益在噪声量级。

### 第 4 步：拆 `session-hub.ts`（1–2 天，纯重构）——**已完成**

12. **已完成**：`session-hub.ts` 从 2,151 行拆到 **492 行**，`Conversation` 与消息投影落到 `src/conversation/{conversation.ts,messages.ts}`，文件头注明职责边界。拆分未按本条原计划逐个子文件切（events / state / tree 未独立成文件），而是按「职责边界清晰、私有 SDK 形状收在 sdk-adapter」这一更实际的标准落地。
13. **已完成一半**：`navigateTree` 已收进 `sdk-adapter.ts`（P3-2 解决）。`createSessionHub` 的位置参数旧签名**刻意保留**（P3-3 的判断已更正为「不应删」——它是兼容壳，删了是破坏性变更，与本项目对既有嵌入方的承诺冲突）。

### 第 5 步：补通用智能体的手脚（按业务决定，但建议至少做前两项）

14. **加联网能力**：`web_search` + `web_fetch` 两个工具（走可注入的 `Retriever` 同款模式，允许业务换成自己的搜索后端）。这是把「通用智能体」从自称变成事实的**唯一必需项**。
15. **加跨会话记忆**：至少做到「用户偏好 + 上次结论摘要」持久化（settings 落盘 + 一个 `memory` 目录 + 两个工具 `remember` / `recall`）。当前 settings 会落盘（`~/.pi/agent/pi-starter-settings.json`），但缺的是**跨会话的记忆**——换一段对话就不知道用户偏好与上次结论，这一层必须补。
16. **加覆盖率门槛**（P5-1）与**前端依赖审计**（P5-3）；给 `web/` 补 `usePiRuntime.ts` 的适配层测试（P4-1）——它是自研核心，目前零测试。
17. **`release.mjs` 的自动化边界**：`--no-verify` + 自动 tag + 推送 + 无回滚是合理但危险的组合，建议加 `--dry-run` 与「推送失败时输出回滚清单」。

---

## 6. 关于「我没有个人业务」——正面回答

**我不认为这是错的，而且我认为你的论证比多数人以为的更站得住。** 三条理由：

**理由一：通用性本身就是产品属性，不是「还没想好做什么」。**
这个项目的价值主张写得很清楚：**「垂直 Agent 的起跑线」**。判据不是「它自己会不会卖货」，而是**「把它接到一个业务上要多久」**。项目自己给出了这个判据的验证方式——`scripts/verify-embed.mjs` 8 项断言，覆盖 `createApp()` 返回值契约、内核路由、以及**三种鉴权挂法的实测结论**。这不是口号，是可执行的事实。

**理由二：它已经在做「没有业务也能活」的实事。**
默认档能读文件、查知识库、查 SQLite、问人、看时间；能重启后接回历史对话（且验证了工具调用不悬空）；能多对话并发（cap 8 + LRU）；能编辑/回退/分叉会话树；能改标签做书签。**这些都不是「某个业务的功能」，而是「任何业务都会需要的底座」。** 把它们做扎实，本身就是交付。

**理由三：真正的风险不在「没有业务」，而在「通用与可用之间的缝」。**
我在 §4 P7 列的那六条缺口里，**P7-1（无联网）与 P7-2（无记忆）是两条会被人拿来质疑「通用」这个词的**。不是因为它们让项目变差，而是因为：
- `rules.md` 明确要求「不编造数据、必须工具核实」——**这个要求把联网从「加分项」变成了「能力闭环的必需品」**。没有它，我的正确行为只能是频繁说「我无法核实」，这与「通用智能助手」的默认预期有落差。
- 没有跨会话记忆，「通用」就只是「无状态」的另一种说法。

**所以我的判断是：定位没问题，方向没问题，缺的是「让通用这个词兑现」的最后两块砖。** 补上联网与记忆，这个项目从「优秀的脚手架」变成「优秀的通用智能体」的门槛就跨过去了。

---

## 7. 本轮（2026-10-10）已完成的性能修复记录

作为附注，本轮审查期间对快照热路径（`buildState`）做了三处修复，均已实测通过：

| # | 修改 | 效果 |
|---|---|---|
| 1 | `contextIndex()`：一趟 `buildContextEntries()` 同时取「消息→条目 id」与「官方标签」 | 每周期由 2 遍会话树遍历 → 1 遍（SDK 的 `buildContextEntries()` 每次都先 `getEntries()` 全量过滤，并不便宜） |
| 2 | `overBudget` 用同一个 `totalTokens` 短路 + 复用 `contextTokens` | 去掉每周期一次全量 token 累加，以及一份马上被丢弃的 trim 计划数组 |
| 3 | `listConversations()` 的休眠段按「索引数组引用 + 在册 id」缓存 | 每周期少分配约 500 个对象；索引 upsert/remove 换新数组即自然失效，无需失效钩子 |

净效果：`buildState` 由「1 遍投影 + 2 遍树遍历 + 2 次 token 全量累加」降为「1 遍投影 + 1 遍树遍历 + 1 次 token 累加」。残留见 §4 P2。

---

## 附录 A · 本轮实测验证记录（第一手，2026-10-10）

| 命令 | 结果 |
|---|---|
| `npm run typecheck` | 通过（0 错） |
| `npm run lint:unused` | 通过（未使用符号 0） |
| `npm test` | **391 通过 / 0 失败 / 0 跳过** |
| `npm run test:web` | **19 通过 / 0 失败** |
| `npm run test:coverage` | **lines 89.80 / branches 79.09 / functions 83.16**（阈值 88 / 77 / 82，通过） |
| `npm run smoke` | **23/23 runtime checks passed** |
| `npm run build` | 通过（EXIT=0，产出 `dist/`） |
| `npm run verify:embed` | **8 项通过** |
| `npm run e2e` | **39 通过 / 0 跳过 / 0 失败** |

> 上表是**首版当时**的实测记录；本轮改动后 `npm test` / `test:web` 的行已回写为最新值，`typecheck` / `lint:unused` / `build` / `smoke 23/23` / `verify:embed 8/8` / `e2e 39/39` 均复测通过（`npm run verify:all` EXIT=0）。
> 未跑：`npm run rag:smoke`（可选，需本地嵌入模型/网络）、`npm run probe:providers`（需真实 key）。

## 附录 B · 规模与结构数据

| 项 | 数值 |
|---|---|
| `src/` 下 `.ts` | 118 个（非测试 76 + 测试 **42**） |
| `src/` 非测试代码 | 18,366 行 |
| `src/` 测试代码 | 8,905 行 |
| `src/` 合计 | 27,271 行 |
| `web/src/` 下 ts/tsx | 35 个文件 / 8,593 行（自研核心约 2.7k，其余为 registry/shadcn 生成件） |
| 最大单文件 | `src/session-hub.ts` **2,151 行** |
| 提交数 | **59**，author 2 个（`LPK3215` 50 / `cnb.lpk` 9） |
| 开发时长 | `2026-10-08 00:31` → `2026-10-10`（约 **3 天**） |
| `CHANGELOG.md` | 313 行 |
| `docs/` 手写文档 | 6 个 `.md`，1,789 行；另有 3 个**生成**的 SVG + 1 个静态全景页 |
| 运行时依赖 | 6 个（`pi-agent-core` / `pi-ai` / `pi-coding-agent` 锁 `0.83.0`、`express` 5、`typebox`、`ws`） |

**模块分层（`src/`）**：

```
入口层   index.ts(CLI) / server.ts(Web) / lib.ts(库) / setup.ts / rpc.ts(官方 RPC)
组装层   agent.ts ★ / app.ts ★ / config.ts / cli-args.ts / models.ts / capabilities.ts
编排层   session-hub.ts ★(492) / conversation/{conversation,messages}.ts / snapshot.ts / protocol.ts(协议单源) / sdk-adapter.ts
传输层   sse.ts / transport/ws.ts
会话树   sessions/{store,edit}.ts + sessions/cap
能力层   tools/ skills/ knowledge/ prompt-templates/ prompts/{persona,rules,composer}
策略层   approval/{rules,policy,gate,watchdog} modes/plan-mode
集成层   mcp/{client,bridge} subagents/ exec/runner
基础设施 log.ts metrics.ts settings.ts provider-keys.ts graceful.ts child-env.ts
         secret-files.ts files/service.ts context/budget.ts http/*(13 文件) extensions/*
```

## 附录 C · 既有文档的一致性问题（建议优先处理）

见 §4 P6。**核心结论：本项目的代码质量与文档质量已经出现明显落差，而文档是唯一会让读者得出「比实际更差」结论的部分。** 修文档的投入产出比，高于本轮任何一条代码优化。

---

*本文档基于工作区实际源码与实跑门禁写成；所有结论均附 `文件:行` 或命令输出证据。P2/P3 中的性能与可维护性条目为优化建议，不构成当前缺陷；P7 中的能力缺口为定位相关的取舍，是否推进由业务决定。*
