caveman CLI 终端交互契约:有界内联 UX、管道安全输出与诚实度量标签的实现解析
本文围绕 caveman 仓库中的 packages/cli/TERMINAL_UX.md 展开:它定义了 caveman 命令行工具的整体验示契约——有界内联终端 UI、机器输出管道安全、以及"节省量永远标注 inferred、永不换算成美元"的诚实度量规则。读完本文,你将理解 caveman CLI 如何在不引入任何运行时依赖的前提下实现交互式 learn 体验,其四个"人机四问"输出原则如何落到 --plain/--json/--md/--all 等具体开关上,并能对照 learn-tui.ts 与 index.ts 的源码验证每一条契约的实现方式。
一、契约的定位与总方向
TERMINAL_UX.md 开篇就划定了边界:该文件只定义演示(presentation)契约——命令行为、安全门控、机器输出格式和退出码仍由代码与产品规格拥有。这意味着它不是一份"功能需求文档",而是一份"输出纪律文档",约束的是终端里"长什么样",而不是"做什么"。
核心方向可以概括为一条反直觉的原则:使用有界的内联终端 UI(bounded inline terminal UI)。具体包含四个要素:
- 短小的分节(short sections);
- 状态面板(status panels);
- 工作运行时的进度反馈(progress while work runs);
- 需要用户选择时,至多一个键盘选择器(one keyboard picker)。
同时明确禁止三样东西:全屏应用(full-screen app)、备用屏幕缓冲区(alternate-screen buffer)、常驻仪表盘(persistent dashboard)。这与很多 TUI 工具走"接管整个终端"的路线相反——caveman 的立场是 CLI 输出随时可被复制、管道、截取。
交互库 Clack(@clack/prompts)只服务于交互式 learn。它在构建期被打包进一个约 25 KB 的懒加载 chunk,因此发布的 npm 包运行时依赖仍为零,普通命令启动时不会加载任何 UI 库。这一点可以在仓库中直接验证:
- package.json 中
"dependencies": {}为空,而@clack/prompts: 1.7.0位于devDependencies; - bundle-tui.mjs 用 esbuild 把 src/learn-tui.ts 打包为
dist/learn-tui.js,bundle: true且minify: true,banner 注释明确写着 "@clack/promptsremains a build-time dependency only"; - CLAUDE.md 的 Gotchas 一节进一步固化了这条纪律:"Published runtime dependencies stay zero. TUI libraries must be bundled, lazy-loaded, and measured; do not move them onto ordinary command startup."
二、人机四问:每条命令输出的固定顺序
文档要求每条面向人的命令按固定顺序回答四个问题:
- What happened?(发生了什么)
- What matters?(什么重要)
- What should I do next?(我接下来该做什么)
- Where can I inspect full detail?(去哪里看完整细节)
默认输出保持有界;细节从不消失,只是移动到 --all、--verbose、--json、--md 或命名子命令背后。这个设计保证了两种读者各取所需:人读到的是摘要加下一步动作,脚本读到的是完整数据。
在源码中能看到这条原则的直接体现。renderLearnPlan(index.ts#L13257-L13321)在默认模式下只输出 Setup Score、来源行、top moves 摘要、protected 行和固定的 next: / details: 指引两行;而 --all 模式才追加 per-repo 块与 Advanced 页脚,最后始终输出 report: <路径> 一行——这正是第四个问题"去哪里看完整细节"的代码化回答。
三、Surface Map:每个命令面的呈现规格
文档的 "Surface map" 一节是整个契约的主体,逐条规定了 CLI 每个表面的呈现方式。以下按原貌完整继承并结合源码补充:
3.1 caveman / --help
帮助输出只展示四个"瓷面"(porcelain)任务,按 run / understand / connect / more 分组。从 CLAUDE.md 可以确认这四个瓷面动词是 run、learn、login、status 加上 agent 快捷入口,且 porcelain 被硬性封顶在"四个动词 + agent 快捷 + 恰好两个命名空间";tools 与 cloud 各自最多打印 15 个动词(当前分别为 15 和 14),第五个动词或第 16 个打印动词需要一次"退役决策"。
3.2 caveman <agent>:安静的启动横幅
caveman claude 这类 agent 快捷命令启动时只打印一个安静的启动横幅(quiet launch banner):显示模式、首个 blocking/off 状态、仅当新安装了 loadout 时才列出,然后把控制权交给 agent。明确禁止"重复的能力倾倒"(no repeated capability dump)。
3.3 首次运行(first run)
首次运行体验的规格在文档中写得很细:
- 每台机器一次、仅 TTY 下触发;未展示过可经由未打印(unprinted)的
caveman welcome重放; - 词标(wordmark)渐显动画;
- 30 天回溯扫描运行期间的 spinner(该扫描对本地会话日志是只读的);
- tokens sent / would-have-cut 的数字滚动(count-up)揭示,附 family 分解与 inferred/tokens-only 标签;
- 遥测披露行;
- 最后一个
[y/N]的账户问题; - 任何失败或空历史都降级为一行暗色文字;被包裹的 agent 始终会启动;
- 非 TTY、
CAVEMAN_PLAIN=1、TERM=dumb会静默跳过整个时刻。
CLAUDE.md 补充了实现细节:首次运行由 config 中的 firstRunAt 标记保证每机器一次;30 天回溯扫描走 caveman-proxy learn scan --retro,基线 pass 上限 20 秒、retro pass 上限 60 秒、子进程超时 90 秒含余量;揭示的 tokens 数字是"对扫描会话求和、绝不外推",sent 按每个 API 响应去重一次,且永不把 tokens 换算成美元。
3.4 caveman learn:交互式学习体验
learn 是本契约下最复杂的表面,文档规定其完整序列:
- 动画扫描(animated scan);
- Setup Score 卡片;
- source/session 作用域行;
- 至多三张 top-move 卡片;
- 分组后的 recurring context;
- 受保护的 load-bearing 基线;
- 键盘动作菜单:implement / details / report / done。
完整 sink ids 与检测器细节放在 --all 下;--plain 禁用交互 UI。由于当前 proxy 会从同一份 plan 写出可视化报告,前台只做一次完整分析。
Portfolio 输出只提升一个"最佳下一步":它的具体 top sink 作为标题、fix 标签作为 kind、测量置信度保持可见。已记录的修复出现在 confirmed 区段,带应用日期、修复前后单位、修复后会话数与纵向结论(verdict)。TUI 中的 confirmed 计数提示指向 caveman learn --all,后者还会追加一个 per-repository 块,该块只报告会话数、dumbzone 百分比与中位上下文——不虚构每仓库分数。
这些规格在源码里逐条可查:
- buildLearnTuiModel 把 proxy 返回的 LearnPlan 转换为 TUI 视图模型:
score仅在存在recurring_context类 sink 时取cave_score.score,否则为null(此时 TUI 渲染 "LEARN RESULT" 而非 "SETUP SCORE" 卡片,见 learn-tui.ts#L102-L117);protected 行文案为 "included in score, never auto-fixed";findings为 sink 总数。 - learnScoreBar 实现 24 格宽度的分数条(
━填充、─余量),分数先被钳制到 0–100 再取整,这对应了"分数不越界、不猜测"的输出纪律。 - renderLearnTui 的动作菜单与文档一致:有 moves 才出现
implement(hint 为 "Claude Code or Codex · approval before edits"),findings > 0才出现details,report与done恒在;选择 implement 后还会弹一个可选 focus 文本框,最终返回{ action, focus? }供主流程分派。 - renderLearnConfirmed 渲染 confirmed 区段:每个条目带 ✓/·/! 符号、sink_id、修复前后数值与单位、修复后会话数、verdict 与应用日期;注释专门强调"测量方式要跟着数字走——没有归因信息的 confirmed 行会显得比实际证据更强"。
- renderLearnRepos 的 per-repo 行恰好是三个字段:
${repo} · N sessions · dumbzone X% · median context ~Y,与文档"不发明每仓库分数"的约束一致。
3.5 caveman learn implement
文档规定:选择 Claude Code 或 Codex、安装缺失的 caveman-learn 指南,然后带着当前报告与可选的用户 focus 启动 agent;agent 在每次编辑前都要询问,load-bearing 发现永不被编辑。
源码侧的实现链条(index.ts#L13673-L13763):
learnImplementPrompt生成的提示词逐条复述安全规则:"Work through selected fixes one at a time. Never edit load_bearing findings."、"Show the proposed diff and before → after token count, ask before every edit, apply only approved changes, then verify the reduction and any recall path."、"Keep every local savings claim labeled inferred and never attach currency."——安全约束从演示层写进了交给 agent 的提示词本身;ensureLearnAgentGuide对 claude 写入<cwd>/.claude/skills/caveman-learn/SKILL.md,对 codex 写入~/.codex/skills/caveman-learn/SKILL.md,内容来自仓库内置的 skills/caveman-learn/SKILL.md 编译产物,已存在则不覆盖;chooseLearnAgent在未指定 agent 且本地装了两个候选时,才走键盘选择器,且前提是learnTuiTerminal()为真——非 TTY 直接报 usage 并以退出码 2 结束。
3.6 caveman status、tools、cloud 与错误面
caveman status:today、off 状态、account/config 状态、一个下一步动作;caveman tools:本地命令按 think / remember / execute / inspect 分组,默认停在 15 条,内部进阶面走caveman help tools --all;caveman cloud:已连接命令按 account / evidence / governance 分组,login 是明确的起始动作;- errors:一个问题、一个最可能的纠正、一个帮助指针,不打印堆栈。
这四项与 CLAUDE.md 中的命令面描述互证:tools 上限 15 个打印动词、cloud 上限 15(当前 14),内部动词(如 shrink-hook、practices、check)保持可调用但不打印。
四、输出契约(Output contracts):六条硬规则
文档 "Output contracts" 一节给出六条规则,是全文约束力最强的部分,可整理为:
| 规则 | 说明 | 源码印证 |
|---|---|---|
| 机器输出管道安全 | JSON、Markdown、压缩字节、recipes 与被委托命令的输出不加任何装饰或 prompt | learn 的 --json/--md 分支直接 process.stdout.write(planRaw)(index.ts#L13886-L13889) |
| 交互 UI 要求三个 TTY | stdin、stdout、stderr 均须为 TTY;非 TTY 路径保持稳定的紧凑文本 | learnTuiTerminal() 检查 interactive()、process.stdout.isTTY、TERM !== "dumb" 与 CAVEMAN_PLAIN(index.ts#L13765-L13770) |
| 降级开关 | --plain、CAVEMAN_PLAIN=1、TERM=dumb 禁用动画与键盘输入;NO_COLOR 禁用颜色 |
learnTuiEnabled 同时排除 --plain/--json/--md/--all/--verbose 任一参数(index.ts#L13772-L13775);learn-tui.ts#L27-L32 中 color = !NO_COLOR && stdout.isTTY |
| 本地节省保持 inferred | learn 中永不出现货币,绝不从每日数据外推月度 |
渲染文案固定带 "basis: inferred (local sessions, not billed spend)"(index.ts#L13279-L13291) |
| 速率与总量严格分开 | tokens/day 是前向速率,tokens observed 是历史窗口总量;Learn 分别渲染,永不相加、永不把观测总量描述为速率 |
buildLearnTuiModel 中 scope 行固定为 "local setup · inferred · not billed spend · separate from org Cave Score"(index.ts#L13212) |
| 未知值保持缺席 | 不造合成零、不猜状态 | learnMeasureValue 对 undefined/非有限数返回 "?" 而非 0(index.ts#L13423-L13427);分数卡片 score === null 时整体改走 "LEARN RESULT" 分支 |
最后一条在 TUI 中还有对应呈现:learnScoreBody 在 model.score === null 时不画分数条,只输出会话来源行与状态行——"未知值缺席"而不显示 0 分。
五、懒加载机制:交互式与非交互式的分叉点
learn 是演示契约最重要的实现现场,其分叉逻辑在 index.ts#L13829-L13899:
learnTuiEnabled(rest)判定是否走 TUI:必须真终端 + 无机器模式参数;- TUI 路径:
await import("./learn-tui.js")动态导入 25 KB 的 Clack chunk,创建 spinner 并在 proxy 的learn scan --write-report输出上驱动progress.update("Reading Claude Code and Codex sessions");扫描完成后以"report token/mtime 证明本次 proxy 是否真的写了报告"做一次防御性校验(旧版 proxy 会忽略未知 flag),必要时回退到learn report --json;随后renderLearnTui按用户选择分派:implement→ 复用learnImplement,details→renderLearnPlan的 verbose+all 全量文本,report→ 打开本地报告文件; - 非 TUI 路径:
proxyExecLearn同步执行同样的扫描命令,仅当interactive()且非机器模式时向 stderr 写一行 "Learning from local sessions…" 提示——装饰走 stderr、数据走 stdout 的管道安全纪律。
两条路径共享同一个数据源:caveman-proxy learn scan。这正对应文档所述"当前 proxy 从同一份 plan 写出可视化报告,所以前台只执行一次完整分析"——TUI 不是另起炉灶的第二分析器,只是同一 plan 的另一种渲染。
关于子命令的机器可读面,learn 的分派(index.ts#L13779-L13824)也遵循契约:export/reconcile 注释明确写着"两者都是 inspect-before-you-act 的面,因此保持机器可读",直接透传 JSON;applied/simulate 同理。
测试侧,tests/learn-tui.runtime.mjs 与 tests/cohesive-ux.runtime.mjs 使用 node --test 运行时测试套件拉起构建产物验证这些路径,CLAUDE.md 亦要求"Piped/non-interactive paths remain plain; runtime tests assert them"——管道与非交互路径的"纯文本"性质是被测试断言的,而非口头约定。
六、度量契约(Measurement):遥测的披露与边界
文档最后一节规定了遥测契约:
- 披露且可退出(disclosed opt-out)的
cli/v1事件覆盖命令的曝光/结果/失败,加上无内容的本地 Engine 会话聚合; - CI/非 TTY 下默认关闭,所有 kill switch 生效;
implement是白名单子命令; - 遥测永不包含:prompts、argv 值、报告内容、sink ids、路径、provider/model/account/request/session ID、哈希、美元金额;
- 产品负责人把匿名遥测与本地 ClickHouse/运行时证据并排审阅;任何产品决策都不单独依据匿名遥测。
"Unknown values stay absent" 的原则在这里同样适用:宁可数据缺席,不可合成填充。配合前文的 inferred 标签规则,可以概括出 caveman CLI 度量呈现的一条主线——数字必须携带它的证据级别:tokens/day(前向速率)与 tokens observed(历史窗口)分栏渲染;本地节省标 inferred、永不标货币;per-repo 块只报可测的三量;confirmed 行必须带归因方法与置信度。
七、如何验证本文所述行为
在只读查看当前仓库之外,以下命令可在具备 Node ≥ 22.13 的本地环境复现契约行为(构建与运行方式见 CLAUDE.md 的 Conventions 一节,本地安装入口为仓库根的 scripts/install-local-cli.sh / scripts/install-local-cli.ps1):
# 构建 CLI(tsc + delegate/TUI bundle + shebang)
pnpm --filter @caveman-ai/cli build
# 交互 TUI(真终端下)
node packages/cli/dist/index.js learn
# 降级路径:纯文本 / 机器模式
node packages/cli/dist/index.js learn --plain
node packages/cli/dist/index.js learn --json | jq . # 管道安全,无装饰
CAVEMAN_PLAIN=1 TERM=dumb node packages/cli/dist/index.js learn # 静默跳过所有动画
# 完整细节面
node packages/cli/dist/index.js learn --all
预期行为:--json 输出不含 spinner、颜色或 prompt 前缀;非 TTY 下 learn 不弹出任何键盘菜单;--all 输出末尾出现 per-repo 与 Advanced 页脚——分别对应本文第三节与第四节的契约条目。
小结
TERMINAL_UX.md 用一页篇幅为整个 CLI 立下了三条可检验的纪律:输出有界(细节退居 --all/--json/--md 之后)、机器面纯净(管道输出零装饰、三 TTY 才允许交互)、度量诚实(inferred 标签、速率与总量分离、未知值缺席、本地节省永不货币化)。配合零运行时依赖 + 25 KB 懒加载 Clack chunk 的构建策略(package.json、bundle-tui.mjs),这套契约保证了 caveman 在"给人看的终端"与"给脚本用的管道"两种身份间零成本切换——这也是其 src/learn-tui.ts 与 src/index.ts 中每一处渲染分支所共同遵守的底层约束。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00