Metadata-Version: 2.4
Name: xiaoyu-agent
Version: 0.14.0
Summary: 小羽 — a harness coding agent
Project-URL: Repository, https://github.com/pholex/xiaoyu
Project-URL: Issues, https://github.com/pholex/xiaoyu/issues
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: openai==2.52.0
Requires-Dist: tree-sitter>=0.22
Requires-Dist: tree-sitter-bash>=0.21

# 小羽 · Xiaoyu

[![ci](https://github.com/pholex/xiaoyu/actions/workflows/ci.yml/badge.svg)](https://github.com/pholex/xiaoyu/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/xiaoyu-agent)](https://pypi.org/project/xiaoyu-agent/)

> Weaving code, connecting dots, and showing you the best harness architecture.

一个自建的 harness coding agent。运行依赖极简（openai SDK + tree-sitter-bash，
后者做权限判定的 bash 语法白名单解析、缺了会自动降级），
Windows / macOS / Linux 全平台，`pip install xiaoyu-agent` 即用。

名字取自董永传说里的七仙女**天羽**——织女织布，小羽织代码。羽毛在传统语义里是飞升与轻盈，
对应这个 agent 想要的手感：行云流水、轻量、无负担。

顺带一个巧合：**harness 本身就是织机的部件**（提综装置，控制经线升降的那套框架）。
所以"织女 + harness"不是比附，是同一个词。

## 现在能做什么（v0.14）

- 任意 OpenAI 兼容端点（LiteLLM / vLLM / 各家官方 API），流式输出
- **七个基础工具**（`explore` 与 `skill` 另见下方）：
  - `read_file`（支持 `offset` / `limit` 只读一段）
  - `grep`（正则搜索，自动跳过 `.git` / `node_modules` / `__pycache__` 等噪声）
  - `list_files`（glob 列文件，输出统一正斜杠）
  - `str_replace`（精确替换，主要编辑手段）
  - `write_file`（整文件覆盖，只用于新建或全量重写）
  - `bash`（Windows 上自动换 PowerShell 执行，工具描述与 system prompt 按平台生成；
    超时不当错误——已产生的输出照样返回（exit 124）；超长输出保头保尾砍中段；
    子进程剔除 `LD_*`/`DYLD_*`、独立会话、禁 core dump）
  - `update_plan`（任务清单，学 codex：全量替换、返回恒为"已更新计划"、
    状态机约束靠 prompt 不靠代码）
- **`explore` 子 agent**：便宜模型 + 只读工具做检索，返回带 `路径:行号` + 原文行的结论。
  详见下方「explore 与实测数据」
- **编辑护栏**：改已有文件必须先**完整** `read_file`（`bash cat` 不算，只读一段也不算）；
  读完之后文件被外部改动会拒绝写入并要求重读；`old_str` 不唯一或匹配不上会带行号提示打回；
  行中间开始 + 多行替换会被拦（必然破坏缩进）；
  **容错匹配**（学 codex apply_patch）：精确匹配落空后逐级放宽——忽略行尾空白 →
  忽略首尾空白 → Unicode 标点归一（弯引号/全角空格），任何一级**唯一命中**才接受，
  并按原文缩进修正 new_str；彻底找不到时回显模型给的 old_str 帮它自查
- **分层上下文回收**：超阈值先 **microcompact**（把较早的大块 `read_file`/`grep`/`bash`
  输出替换成占位符——不花模型调用、不磨损结论，够用就不做摘要）；
  不够再全量摘要——早期历史交给便宜模型总结。
  原始任务永久保留，**被压区间的用户原话按预算原文备份**（摘要有损，用户消息最不可丢），
  摘要定位成"另一个模型的交接产物 + 文件系统还在"（学 codex 的 summary prefix），
  摘要不层层累加，压缩后反而更大则放弃；
  **断路器**：连续两次压缩省不到 10% 就暂停自动压缩（手动 `/compact` 不受限）；
  **失败降级阶梯**（学 Amazon Q CLI）：摘要调用失败先把 transcript 减半再试一次，
  仍失败才放弃；撞到断路器时给出自救命令清单（`/compact` `/context` `/clear`）
- **token 记账**（学 codex）：服务端 usage 作为权威锚点，本地只估算锚点之后新增的
  部分——误差不随会话累积；不回 usage 的模型（Mantle 系）退化为纯本地估算
- **历史惰性修复**（学 codex normalize_history）：每次发请求前自动补齐缺失结果的
  tool_call、删除孤儿 tool 消息——中断打在任何位置都不会造出 400 的历史
- **权限规则**：`allow bash(git *)` / `deny bash(curl *)` / `allow write_file(src/*)` 这类
  规则免逐次确认或直接拦截；**deny 在任何模式下都生效（包括 `--yolo`）**；
  复合命令每一段都要被 allow 覆盖；
  **allow 判定走 tree-sitter-bash 白名单解析**（学 codex）：只认"纯字面量简单命令
  用 `&&` `||` `;` `|` 连接"这一种形状，重定向 / 命令替换 / 变量展开 / 子 shell /
  heredoc 等任何白名单外语法 → 看不懂 → 退回人工确认（黑名单永远列不全，白名单
  漏判的代价只是多问一次）；引号感知——`git commit -m "a && b"` 不会被错切成两段；
  缺 tree-sitter 或 Windows（PowerShell 语法）时自动回退字符串黑名单路径；
  规则放用户级 `permissions.txt` 或仓库 `.xiaoyu/permissions.txt`，REPL 里
  `/allow` `/deny` 直接写入、`/perm` 查看；确认框答 `a` = 本会话该工具不再问；
  **持久 allow 不许覆盖任意代码执行入口**（`bash -c`/`python`/`env`/`sudo`/`rm` 等，
  学 codex 的 banned prefixes——一条 `allow bash(python *)` 就等于永久废掉权限系统）；
  **参数注入防护**：`git -c core.pager=…`、`find -exec`、`rg --pre`、`tar --to-command`
  这类"前缀合法、参数逃逸"的命令不吃 allow 规则，退回人工确认；
  `sudo`/`env`/`bash -c`/`trap` 包着的强制 `rm` 会被递归剥出来识别；
  规则文件里可写 `#test allow bash git status` 自测断言，加载时立即验证
- **危险命令硬拦截**：`rm -rf /`、fork bomb、`mkfs`、`dd` 直写块设备、Windows `format`
  等不可撤销操作在任何模式下都不执行，**包括 `--yolo`**——审批是"用户想不想"，这层是"绝不"
- **macOS 沙箱**（Seatbelt / `sandbox-exec`，默认开启）：bash 命令跑在内核级强制
  访问控制里，**只能写工作区、临时目录和构建缓存**，写其它路径直接被内核拒
  （家目录文件删不掉、`~/.zshrc` 覆写不了、包装不进系统 Python）；策略对所有
  子孙进程生效，模型再怎么套壳也跑不出去。取舍：**读全盘放行**（收紧读要踩不完的
  动态链接/locale/runtime 坑，且读不造成不可逆损失——凭据保护靠 bash 逐次确认那层）、
  **网络默认放行**（断网会静默打断 `pip install`/`npm install`/`git push`）。
  `--no-sandbox` 关掉、`--no-network` 连网络一起断、`XIAOYU_SANDBOX_WRITABLE`
  追加可写根目录；被拦时给模型附一段说明（边界在哪、别反复重试），
  否则它会把 `Operation not permitted` 当成命令写错而空转。非 macOS 自动跳过
- **项目级指令文件**：读仓库根目录的 `AGENTS.md`（或 `XIAOYU.md` / `CLAUDE.md`，首个命中）
  进 system prompt——项目自带的规范（怎么跑测试、代码约定）跟着仓库走，不用每次口头交代
- **插件工具**：第三方包在 entry point 组 `xiaoyu.tools` 里声明工厂函数，
  `pip install` 后自动挂载（这是接内部工具的代码层通道）；坏插件只警告不拦启动、
  不许覆盖内置工具、未声明的能力按需要确认处理（fail-closed）
- **会话落盘与 `xiaoyu resume`**：交互与一次性执行的每条消息 append 到用户目录
  `sessions/*.jsonl`（首行 meta 带格式版本；压缩事件携带压缩后的完整历史，
  学 codex 的 replacement_history——恢复时不必理解压缩语义）；
  `xiaoyu resume` 列出当前工作区的历史会话（只读文件头几行做标题）供选择，
  `--last` 直接续最近一个，`--all` 跨工作区；恢复的消息复制进新会话文件，
  每个文件自包含、可再次 resume；遇到更新的格式版本明确拒绝而非静默错乱；
  历史里用过、现已不在注册表的工具（插件卸载/技能关闭）恢复时点名预警——
  OpenAI 兼容端点不校验历史工具名（不必像 Amazon Q 那样造 dummy 哨兵工具），
  模型若再调用会收到列出可用工具的明确报错并自行改道
- **SKILL.md 技能**：扫描 `~/.agents/skills/`（跨客户端规范库）与用户配置目录 `skills/`，
  与 Anthropic / agentskills.io 同形态；渐进披露——索引进 system prompt，
  正文由模型用 `skill` 工具按需加载，`/skills` 查看；
  索引有预算（单条描述 ≤250 字符、总量 ≤上下文窗口 1%），技能装再多也不吃常驻上下文
- **错误分类与自动恢复**：限流/瞬时错误按分类指数退避重试（±25% jitter 错峰，
  服务端给了 `Retry-After` 就听它的），重试只在这一层（SDK 层已关，不会 3×3 叠加）；
  上下文超限先强制压缩再重试，鉴权错误直接报清楚不空转；
  中断（Ctrl-C）后全量扫描补齐悬空的 tool 结果、半截流式回答也入历史，随时能继续对话；
  分类清单有遍历自测锁住（学 Amazon Q CLI 的 `all_errors()` 模式：每类可构造、hint 非空）
- **备用模型降级链**（学 Kiro issue 自动化 + Amazon Q CLI 的恢复路径）：配置
  `XIAOYU_FALLBACK_MODELS=模型A,模型B` 后，主模型重试耗尽（持续限流/持续 5xx）
  自动依次切备用模型，**同一份会话原样重发、状态一点不丢**。层序：retry 在内层
  （瞬时错误该等），换模型在外层（模型级故障才切）；鉴权/致命错误不切（配置问题
  换模型没用），上下文超限走压缩不走切换；切换是**粘性**的（否则主模型宕机期间
  每次请求都要白等一轮退避），`/model` 可切回；全链失败时报清楚并保留会话——
  REPL 接住异常不崩，稍后重发即可继续（最外层保底：降级为非 LLM 行为）
- **循环护栏**：撞到单轮工具调用上限时让模型收尾交代（做了什么/剩什么/建议），
  不静默截断；连续相同 (工具, 参数) 调用第 3 次附加提示、第 5 次拒绝执行——
  便宜模型容易原地打转，得在 harness 层刹住
- **工具可用性探测**：工具可挂 `check_fn`，探测不过就不进 schemas、拒绝执行
  （`/tools` 里标记 `[不可用]`）
- 写文件和执行命令**默认逐个人工确认**，`str_replace` 显示 `-/+` 差异预览；
  需确认的工具 schema 注入 `__tool_use_purpose`（学 Amazon Q CLI）——模型自述的
  调用目的显示在确认框上方，"这条命令要干嘛"不用人肉猜；
  **拒绝即反馈**：确认框里任意其它输入 = 拒绝理由，原文回灌模型让它照着改道
  （学 Amazon Q CLI 的"拒绝即改指令"）；deny 规则拦截时点名命中的规则原文，
  用户知道该改哪条、模型知道该绕哪条
- 交互 REPL（`/help` `/tools` `/skills` `/model` `/usage` `/context` `/compact`
  `/perm` `/allow` `/deny` `/clear`）
  + 一次性执行模式 + 启动横幅；`xiaoyu config` 配置向导（全平台固定路径，免找 `.env`）
- **按模型分开记账**的 token 统计；eval 可横向扫 12 个候选模型并算成本
- **跨平台**：Windows（PowerShell 分派、输出统一 UTF-8）/ macOS / Linux，
  CI 三平台 × 两 Python 版本矩阵验证

还没做（按优先级）：
- **Linux 沙箱**：`bwrap` 包装（`--unshare-net` 断网 + 六步挂载顺序），
  macOS 那套已在 v0.13 落地，Linux 补齐后跨平台一致
- **两段式提权**：沙箱内跑 → 疑似被拒 → 问用户一次 → 无沙箱重跑；
  以及借沙箱兜底把"工作区内的写"从逐个确认改成免确认——
  **沙箱的真正价值是减少弹窗，不是增加安全**，这一步才兑现它
- 接内部工具（飞书 / EDW / Amazon 运营；SKILL.md + 插件 entry point 两条载体已就绪。
  若第三条走 MCP，照抄 Amazon Q CLI `mcp_client/` 的实现清单：懒加载不阻塞启动、
  server 子进程 stderr 只进独立日志不进终端、工具名消毒+冲突改名、`${env:VAR}` 展开）
- **plan/execute 分离（dry-run）**：工具拆成"生成改动计划"与"执行"两段，
  纯函数与副作用分离后 dry-run 是免费的——先展示全部将做的改动再一次性放行，
  与"沙箱兜底减弹窗"同方向（逐条弹窗 → 看全貌批量确认）。
  另一条更省事的路（学 Amazon Q CLI `/checkpoint`）：影子 bare git 仓库，
  每次工具执行打 tag，对话快照与文件快照绑定回滚——"随时可整体回滚"与
  "先看全貌再放行"殊途同归，都是减弹窗，可作 dry-run 化的兜底或替代
- TUI（架构方向学 Amazon Q CLI 二代：headless agent + 开放事件协议（AG-UI/ACP），
  UI 只是可替换前端）

顺带记录：深读 Amazon Q CLI（7.7 万行 Rust，已进维护模式）反向验证了几个已有选择——
它**没有任何回合上限/连续错误上限**（预留了 `MaxRequests` 从未实现）、用 `len/4` 估
token 且靠服务端报错才触发压缩、每回合把整个会话 JSON 覆盖写 sqlite 单行、工具执行
无超时；这四处 xiaoyu 现有的循环护栏、usage 锚点校准、JSONL append、`bash_timeout`
都是更对的做法，保持不动。它二代重写时权限逻辑迁移不完整（`ExecuteCmd` 恒放行），
再次印证"安全函数不复制、防护要集中"的教训。

## 测试

```bash
# 单元测试（369 个，全部不打网络；CI 在三平台 × py3.11/3.14 跑同一套）
.venv/bin/python -m unittest discover -s tests -t .

# eval：真实调模型跑端到端任务
.venv/bin/xiaoyu-eval --list
.venv/bin/xiaoyu-eval                            # 全部 case
.venv/bin/xiaoyu-eval --case targeted_edit -v    # 单个 case + 完整输出
.venv/bin/xiaoyu-eval --model bedrock-claude-opus-5 --repeat 3
```

### eval 集在测什么

| case | 卡的是什么 |
|---|---|
| `fix_and_test` | 修 bug + 加注解 + **自己写测试并真的跑通** |
| `targeted_edit` | 130 行文件里定点改 —— 用 diff 行数上限抓"整文件重写" |
| `readonly_answer` | 只读任务一个字都不许改 —— 抓"手痒乱动文件" |
| `multi_file_rename` | 跨 3 文件重命名，改完测试还得过 —— 抓"改一半" |

判据全部机械可判（文件内容、diff 规模、测试退出码、用了哪个工具），没有主观评分。
结果存到当前目录 `xiaoyu-eval-results/*.json`，含 token、耗时、工具调用序列，用来比较改 prompt / 换模型前后的差异。
失败的 case 会额外保存现场（transcript + 最终文件内容）——临时工作区跑完就删，不留现场就没法诊断。

**写新 case 的铁律：断言必须双向自证**。先喂"已知正确答案"确认全 PASS，
再喂"看似完成但实际错"确认能 FAIL。这条已经固化成 `tests/test_eval_assertions.py`，
不打网络就能跑，加 case 时顺手补上正反两个 fixture。

踩过的三个坑（都会让你误判成"agent 不行"）：

- 初始文件因为 `textwrap.dedent` 找不到公共前缀而带着缩进写进去，语法直接错
- 用 `file_contains("2 ")` 判断指数退避，占位函数里的 `return value * 2` 也命中，等于永远通过
- 用 `file_contains("ZeroDivisionError")` 判断"处理了除零"——模型抛 `ValueError` 是同样合理的设计，
  指令里没规定异常类型，这个断言等于偷偷加了一条没提的要求。**判行为，别判字面**

判行为用 `python_snippet_ok`：探针脚本写到工作区之外的临时文件、以工作区为 cwd 和 `PYTHONPATH` 运行，
既避开 shell 引号地狱，也不会污染 `nothing_written` / `unchanged_except` 的快照。

## 安装

```bash
pip install xiaoyu-agent      # 或 pipx install xiaoyu-agent
xiaoyu --version              # 验证装上了（多 Python 并存时也能确认升级生效在哪个环境）
```

升级：

```bash
pip install --upgrade xiaoyu-agent    # pipx 装的用：pipx upgrade xiaoyu-agent
```

开发模式：

```bash
git clone https://github.com/pholex/xiaoyu.git && cd xiaoyu
python3 -m venv .venv
.venv/bin/pip install -e .
```

## 配置

首次使用直接跑配置向导（全平台，写到固定的用户级路径，从此不用找 `.env` 放哪）：

```bash
xiaoyu config            # 交互向导：端点、模型、key
xiaoyu config --show     # 查看生效配置与各项来源（key 永不回显）
xiaoyu config --path     # 打印用户级配置文件路径
xiaoyu config --set XIAOYU_MODEL=deepseek-v4-pro   # 非交互写入，可重复
```

用户级配置文件的位置：macOS / Linux 在 `~/.config/xiaoyu/.env`（跟随 `$XDG_CONFIG_HOME`），
**Windows 在 `%APPDATA%\xiaoyu\.env`**。

也可以手动在任意工作目录放 `.env`（零依赖自解析，仓库里的已被 `.gitignore` 排除）：

```ini
XIAOYU_BASE_URL=https://<你的网关>/v1
XIAOYU_MODEL=bedrock-claude-sonnet-5
XIAOYU_API_KEY=<你的-key>
```

优先级：**真实环境变量 > 当前目录 `.env` > 项目根 `.env` > 用户级 `.env`**，所以临时覆盖很方便：

```bash
XIAOYU_MODEL=bedrock-claude-opus-5 xiaoyu
```

macOS 上 key 也可以不落盘，改用 Keychain（`.env` 里留空即可，会自动回退去读；Windows 上请用 `.env` 或环境变量）：

```bash
security add-generic-password -a "$USER" -s "XIAOYU_API_KEY" -U -w
```

| 变量 | 默认值 | 说明 |
|---|---|---|
| `XIAOYU_BASE_URL` | —（必填） | 任意 OpenAI 兼容 `/v1` 端点：LiteLLM、vLLM、各家官方 API… |
| `XIAOYU_MODEL` | `deepseek-v4-pro` | 见下方"选模型" |
| `XIAOYU_FALLBACK_MODELS` | —（不降级） | 备用模型降级链，逗号分隔；主模型重试耗尽后依次自动切换 |
| `XIAOYU_API_KEY` | — | 端点的 API key |
| `XIAOYU_ENV_FILE` | — | 指定 `.env` 路径，等价于 `--env-file` |

## 用

```bash
xiaoyu                                  # 交互模式（xy 是等价缩写）
xy "把 utils.py 里的类型注解补全"          # 一次性执行
xiaoyu --model bedrock-claude-opus-5    # 指定模型
```

REPL 里：`/help` `/tools` `/skills` `/model` `/usage` `/context` `/compact` `/clear` `/exit`

## 选模型

默认 **主模型 `deepseek-v4-pro` + 摘要 `deepseek-v4-flash`**，国产便宜模型优先。

依据是 12 个候选模型 × 4 个 case 的实测（`xiaoyu/evals/results/*sweep.json`）：

| 模型 | 相对输入单价 | 4 case | 单个 case 成本 |
|---|---|---|---|
| deepseek-v4-flash | 1× | 4/4 | $0.0026 |
| qwen3.7-plus | 2× | 4/4 | $0.0052 |
| **deepseek-v4-pro** | **3×** | **4/4** | **$0.0061** |
| glm-5.2 | 8× | 4/4 | $0.011 |
| gpt-5.6-luna | 7× | 4/4 | $0.016 |
| qwen3.7-max | 12× | 4/4 | $0.030 |
| kimi-k3 | 20× | 4/4 | $0.034 |
| gpt-5.6-terra | 19× | 3/4 | $0.037 |
| bedrock-claude-sonnet-5 | 20× | 4/4 | $0.062 |
| gpt-5.6-sol | 37× | 4/4 | $0.099 |
| bedrock-claude-opus-5 | 37× | 4/4 | $0.124 |
| bedrock-claude-fable-5 | 68× | 4/4 | $0.147 |

同一个任务，**最贵的比最便宜的高 57 倍**，而通过率没差别 —— 所以默认取便宜的。

⚠️ **但这张表不能用来证明"便宜模型够用"**：12 个模型几乎全部满分，说明
**当前 eval 集没有区分度**，4 个 case 都是单文件小改或机械重命名，任何能正常调工具的模型都做得到。
用"全都满分"的 eval 选模型等于抛硬币。真要有依据，得补能让模型露馅的 case：
需要迭代调试的、大文件多处精确编辑的、指令自相矛盾需要顶回来的、长上下文触发压缩的、
以及"不该动的别动"。这件事按当前模型能力性价比不高，暂时搁置。

**硬活手动升级**，别指望默认模型包打天下：

```bash
xiaoyu --model bedrock-claude-opus-5     # 启动时指定
# 或 REPL 里随时切：/model bedrock-claude-opus-5
```

关于 token usage：**12 个候选全都回传 usage**（含 `gpt-5.6-*`），所以压缩的 token 校准
在所有模型上都有效。注意这跟"Mantle / Responses API 不回 usage"的经验相反 ——
经 `/v1/chat/completions` + `stream_options.include_usage` 这条路是回的。

## explore 与实测数据

`explore` 把检索委托给便宜模型的只读子 agent（默认 `deepseek-v4-flash`），
它只有 `read_file` / `grep` / `list_files`——**绝不给 bash**，否则「只读」是空话
（有测试实测跑完这三个工具后整个工作区字节不变）。

在一个「4 层间接跳转 + 每层都有诱饵常量」的多跳追踪任务上量了五组：

| 组 | 配置 | 主模型 in tok | 总成本 | 是否用了 explore |
|---|---|---|---|---|
| A | 关闭 explore | 24165 | $0.01168 | — |
| B | 开启，弱引导 | 25398 (+5%) | $0.01238 (+6%) | 没用 |
| C | 开启，**强制**使用 | **11925 (-51%)** | **$0.00957 (-18%)** | 用了 |
| D | 强引导，自主 | 29097 (+20%) | $0.01640 (+40%) | 用了，但又重读了 8 个文件 |
| E | 修好证据行 + offset | 25804 (+7%) | $0.01237 (+6%) | 没用 |

五组数据给出的结论，每一条都反直觉：

1. **用了确实有效**（C）：主模型上下文砍一半、总成本降 18%。主 agent 从 9 次工具调用降到 3 次。
   主模型越贵收益越大——flash 是 1×、`deepseek-v4-pro` 是 3×，换成 opus-5（37×）差距会拉到十几倍。
2. **靠 prompt 引导的采用率只有 1/3**（B、D、E 三次里只有 D 主动用了）。措辞劝不动模型。
3. **挂上不用也要付钱**（B）：工具 schema 每轮随请求发送，光是存在就 +5%。工具不能无限加。
4. **D 组暴露的是真 bug**：模型给 `read_file` 传了 `offset` 参数（主流 harness 的标准签名），
   我们没实现 → 8 次读有 4 次报废。**只有走「explore 之后再重读」这条路径才会触发**，前三组碰不到。
5. **模型重读是合理的**：D 组它自己说「链已经清晰了，但让我验证 FORWARD_TO 确实被使用而非 FALLBACK」。
   当时 explore 只返回路径行号、没有原文，而任务里警告了有诱饵——**不信是对的**。
   所以现在要求子 agent 必须给出**原文证据行**，并说明排除了哪些干扰项。

因为第 2 条，采用率改成 **harness 层面强制**而不是继续改措辞：
连续 3 次 `read_file` 追加提示，连续 5 次直接拦截并要求改用 `explore`；
**用任何其它工具即重置计数**——这样「读那几个马上要改的文件」不会被误伤。
没挂 `explore` 时该机制完全不触发（劝它用一个不存在的工具是荒谬的）。

> 方法论上最值钱的一条：**「没被调用」不等于「没有用」**。A、B 两组里 explore 一次没被调用，
> 当时差点直接删掉；是 C 组「强制用一次」才量出 -51%。**功能没被采用**和**功能没有价值**
> 是两个独立问题，必须分开验证。

## ⚠️ 安全

`bash` 工具会在你机器上执行模型给出的任意命令。默认每条都要你确认，这是主要防线；
`deny` 权限规则和危险命令硬拦截在 `--yolo` 下仍然生效，但覆盖面有限。
`--yolo` 会关掉逐条确认——只在一次性、可丢弃的目录里用。

**已经踩过一次**：eval 是无人值守 + `--yolo` 跑的，某个模型跑 pytest 失败后执行了
`pip install pytest`，装进了系统 Python 的 site-packages（那个目录 admin 组可写、免 sudo）。
现在 eval 会注入 `PIP_REQUIRE_VIRTUALENV=true` 挡住这条路，但要清楚：
**这只堵了一个具体出口，不是沙箱**。`read_file` / `str_replace` / `write_file` 有工作区边界检查，
`bash` 没有——它仍能写你有权限的任何地方。真隔离要靠容器或 `sandbox-exec`。

## 结构

```
xiaoyu/                     仓库根
├── pyproject.toml          注册 xiaoyu / xy / xiaoyu-eval 三个命令；依赖精确锁版本
├── .env                    运行配置（含 key，已 gitignore）
├── .github/workflows/      CI（三平台矩阵 + 打包验证）与 release（tag → PyPI 自动发布）
├── .githooks/pre-push      本地兜底：push 前跑全部测试
├── experiments/            可复现实验脚本（README 里的数字出处）
├── tests/                  单元测试 369 个，全部不打网络
│   ├── test_tools.py             工具层、编辑护栏、连续读拦截、硬拦截、平台分派
│   ├── test_context.py           token 估算、压缩、断路器、消息序列合法性
│   ├── test_microcompact.py      microcompact 分层回收、压缩摘要 prompt 结构
│   ├── test_permissions.py       权限规则解析、判定管线、会话授权、Agent 集成
│   ├── test_plugins.py           entry_points 插件加载、fail-closed 默认、工具顺序稳定
│   ├── test_readonly_tools.py    grep / list_files、explore 只读边界
│   ├── test_agent_paths.py       主循环、中断恢复、配对补齐、项目指令、打转检测（假 client）
│   ├── test_errors.py            错误分类器、Retry-After/jitter、重试/压缩恢复路径
│   ├── test_skills.py            SKILL.md 解析、扫描、渐进披露、索引预算、check_fn
│   ├── test_user_config.py       xiaoyu config、用户级 .env、平台路径
│   ├── test_session_log.py       会话落盘
│   ├── test_banner.py            启动横幅
│   ├── test_models.py            候选模型、成本计算、横向对比排序
│   └── test_eval_assertions.py   eval 断言的双向自证
└── xiaoyu/                 包
    ├── config.py           运行配置 + .env 解析链 + key 读取（永不回显）
    ├── tools.py            工具注册表、基础工具、护栏、硬拦截、插件加载、平台分派
    ├── permissions.py      权限规则：allow/deny、bash 前缀、路径 glob、会话授权
    ├── agent.py            主循环：流式、tool_calls 累加、审批、分层回收、恢复、循环护栏
    ├── errors.py           API 错误分类器（限流/瞬时/超限/鉴权）+ Retry-After 解析
    ├── explore.py          explore 子 agent（便宜模型 + 只读工具）
    ├── skills.py           SKILL.md 技能：扫描、frontmatter、渐进披露
    ├── compaction.py       上下文压缩：切点、摘要、回退保护、断路器
    ├── session_log.py      会话落盘（JSONL）
    ├── tokens.py           本地 token 估算 + 用真实 usage 校准
    ├── cli.py              REPL、斜杠命令、config 子命令、确认交互
    ├── banner.py           启动横幅（窄终端降级）
    ├── ui.py               ANSI 输出、Windows VT/UTF-8 适配
    └── evals/              eval 集（放包内，避免占用 `evals` 这个通用顶层名）
        ├── harness.py      Case/Context + 断言原语
        ├── cases.py        具体任务
        ├── models.py       12 个候选模型 + 成本计算
        ├── prices.json     单价（随包分发；改价需手工更新）
        ├── runner.py       执行器（支持 --sweep 横向扫模型）
        └── results/        历史跑分归档（仅在仓库；新结果写到当前目录 xiaoyu-eval-results/）
```

外层 `xiaoyu/` 是仓库、内层是包，这是 Python 的标准形态（同 `requests/requests`），不是冗余。

## 路线（按价值排，不按容易排）

1. ~~**`str_replace` 编辑工具**~~ — v0.2（严格匹配 + 唯一性校验 + 先读再改 + 失败带提示回错）
2. ~~**eval 集**~~ — v0.2 建成，但**当前没有区分度**（12 个模型几乎全满分），
   要有依据得补「需要迭代调试 / 大文件多处精确编辑 / 指令自相矛盾 / 长上下文 / 该克制不动」这类硬 case。
   按当前模型能力性价比不高，**已搁置**。
3. ~~**上下文压缩**~~ — v0.3（本地估算 + usage 校准 + 安全切点 + 摘要不累加 + 变大则回退）
4. ~~**模型路由**~~ — v0.3/v0.4（摘要与 explore 走便宜模型；主模型默认换成国产便宜模型）
5. ~~**`explore` 子 agent**~~ — v0.4（便宜模型 + 只读工具 + harness 层面强制采用）
6. **接内部工具** — 飞书、EDW、Amazon 运营那套。**这才是自建 harness 相对 Codex 的真实价值**，
   下一个大动作。载体已就绪：v0.8 起支持 SKILL.md（与 `~/.agents/skills/` 规范库直接互通）；
   v0.10 起有代码层通道——entry point 组 `xiaoyu.tools`，内部工具包 `pip install` 即挂载。
7. 真沙箱隔离（macOS `sandbox-exec` / Linux `bwrap`）——v0.7 先落了危险命令硬拦截，
   v0.11 又加了参数注入防护与 allow 黑名单，但 `--yolo` 下仍无真正边界，
   沙箱才是完整答案（做完还能把"workspace 内的写"从逐个确认改成沙箱兜底不问）。
8. ~~CI / 发布流水线~~ — v0.9.1 全部就位：GitHub Actions 三平台 × 两 Python 版本测试 +
   打包验证；推 `vX.Y.Z` tag 即经 PyPI Trusted Publishing（OIDC，无长期 token）自动发布，
   含 tag 与 `__version__` 一致性检查；本地 `pre-push` hook 兜底。
   发版流程 = 改 `__init__.py` 版本号 → commit → 推 tag，其余全自动。
9. ~~`/resume` 恢复会话~~ — v0.11（`xiaoyu resume`：压缩事件带 replacement 全量历史，
   重放即恢复）。剩：`prices.json` 自动同步、TUI 精致化。
