git:20260916.b57377e to git:20260925.32ce6c0

101 added, 58 removed. Audit A to A.

---
name: workbuddy-context-handoff
- description: 给 WorkBuddy(桌面端 CodeBuddy Code 运行时)配置基于 hooks 的上下文 handoff 门禁 —— 上下文占用达阈值时自动强制产出 handoff 文档,新会话自动注入继续干活。也用于排查 WorkBuddy hooks / 上下文压缩阈值 / transcript 用量相关问题。
+ description: "给 WorkBuddy(桌面端 CodeBuddy Code 运行时)配置基于 hooks 的上下文 handoff 门禁 —— 上下文占用达阈值时自动强制产出 handoff 文档,新会话自动注入继续干活。也用于排查 WorkBuddy hooks / 上下文压缩阈值 / transcript 用量相关问题。触发场景:上下文到 N% 自动生成 handoff、交接文档、新会话续接、hooks 怎么配、为什么自动压缩了、上下文用量怎么算、context handoff、auto compact。注意:本技能的平台路径与 hook 事件名是 WorkBuddy 专有,不适用于其它 agent 运行时。"
agent_created: true
---
# WorkBuddy 上下文 handoff 门禁
- ## 何时用
+ ## Purpose
- - 用户要"上下文到 N% 自动生成 handoff / 交接文档 / 新会话续接"
- - 用户问 WorkBuddy hooks 怎么配、为什么自动压缩了、上下文用量怎么算
- - 需要定位 WorkBuddy 桌面端的运行时行为(阈值、config option、hook 事件)
+ 在 WorkBuddy 桌面端搭一条"上下文快满 -> 强制产出 handoff 文档 -> 新会话自动续接"的流水线。
- ## 关键事实(已实测,别再猜)
+ - **触发端**:Stop hook,按 transcript 里的 token 用量判断是否到阈值,到了就用
+ `{"continue": false, "reason": "..."}` 强制模型先写 handoff。
+ - **注入端**:SessionStart hook,把本项目最新未被消费的 handoff 注入新会话上下文。
+ - 附带用途:解释/排查 WorkBuddy 的自动压缩阈值、hooks 事件与 transcript 用量计算。
- 1. **运行时**:桌面端 agent 跑的是 app.asar 里的 `cli/dist/codebuddy.js`(CodeBuddy Code)。
- 查行为:解包 asar 后 grep 这个文件。
- 2. **hooks 位置**:settings 的 `hooks` 键。
- - 用户级 `~/.workbuddy/settings.json`(PRODUCT_LOWERCASE_NAME=workbuddy)
- - 项目级 `<proj>/.workbuddy/settings.json`
- - hooks 跨 scope 合并,同事件并行执行
- 3. **已实现的 hook 事件**:PreToolUse / PostToolUse / PostToolUseFailure / Notification /
- UserPromptSubmit / SessionStart / SessionEnd / Stop / SubagentStart / SubagentStop /
- PreCompact / PermissionRequest / WorktreeCreate / WorktreeRemove
- 4. **matcher 是正则**:`new RegExp(matcher).test(matchValue)`。matchValue 按事件:
- SessionStart→`source`(startup/resume/clear/compact)、PreCompact→`trigger`(manual/auto)、
- SessionEnd→`reason`、PreToolUse/PostToolUse→tool_name。
- 5. **hook 入参**没有 token 用量:`{session_id, transcript_path, cwd, permission_mode, hook_event_name, ...}`
- 6. **transcript**:`~/.workbuddy/projects/<cwd-slug>/<session-id>.jsonl`。
- 逐行 JSON,取最后一次 `message.usage.input_tokens`(+`output_tokens`)即上下文体积。
- `cache_read_input_tokens` 是子集,**不要**再加一遍。
- 7. **内置压缩阈值**(product.json `tokenUsageThresholds.inputTokens`):
- preMessage 0.5 / warning 0.6 / critical 0.7 / emergency 0.9。
- → **约 50% 就 pre-message 自动压缩**,原生到不了 80%。
- 8. **pre-message 压缩不受 `autoCompactEnabled` 控制**,也不读 settings;它读
- `session.meta.preMessageCompactPct`(ACP `session/set_config_option`)。
- 桌面端 **Max Mode** 开关把它设成 `"0"` 关掉压缩,关掉开关设回 `""`(默认 0.5)。
- 环境变量 `CODEBUDDY_PRE_MESSAGE_COMPACT_PCT` 也能覆盖(值 1-100)。
- 9. **PreCompact hook 不能做 handoff 触发器**:stdout 只进压缩指令
- (`hookContexts.preCompactInstructions`);exit 2 只给用户看消息。
- 10. **能强制模型继续的只有 Stop hook**:`{"continue": false, "reason": "..."}`。
- **能注入新会话上下文的**:SessionStart hook 的 stdout 或
- `hookSpecificOutput.additionalContext`。
+ ## When to Use
- ## 落地方案
+ - 用户要"上下文到 N% 自动生成 handoff / 交接文档 / 新会话续接"。
+ - 用户问 WorkBuddy hooks 怎么配、为什么自动压缩了、上下文用量怎么算。
+ - 需要定位 WorkBuddy 桌面端的运行时行为(阈值、config option、hook 事件)。
+ - 触发词:上下文交接、handoff、上下文压缩、自动压缩、hooks 配置、新会话续接、
+ context handoff、auto compact。
- 三个文件放 `~/.workbuddy/hooks/`,然后在 `~/.workbuddy/settings.json` 注册:
+ ## When NOT to Use
+ - 用户只是想知道"我现在用了多少上下文" -> 直接按 transcript 算一次回答,不必配 hook。
+ - 目标运行时不是 WorkBuddy 桌面端(路径、事件名、环境变量都不同)-> 不适用,先确认运行时。
+ - 用户要的是一份手工交接文档 -> 直接写,不需要这套门禁。
+
+ ## Workflow
+
+ 1. **确认位置与备份**:定位 settings(用户级 `~/.workbuddy/settings.json` 或项目级
+ `<proj>/.workbuddy/settings.json`),先备份,再读现有 `hooks` 键。
+ 2. **写三个脚本**到 `~/.workbuddy/hooks/`:`handoff-trigger.mjs`(Stop)、
+ `handoff-resume.mjs`(SessionStart)、`run-hook.sh`。各文件职责与防死循环要求见
+ `references/hook-implementation.md`。
+ 3. **注册 hooks**(合并进 settings 的 `hooks` 键,不要整体覆盖):
+
```json
"hooks": {
"Stop": [
{ "hooks": [ { "type": "command", "command": "<abs>/run-hook.sh <abs>/handoff-trigger.mjs", "timeout": 15 } ] }
],
"SessionStart": [
{ "matcher": "startup|clear",
"hooks": [ { "type": "command", "command": "<abs>/run-hook.sh <abs>/handoff-resume.mjs", "timeout": 15 } ] }
]
}
```
- - `handoff-trigger.mjs`(Stop):算 `input_tokens / window`,>= threshold 就输出
- `continue:false` + 写 handoff 的指令。必须做两件防死循环:
- `stop_hook_active===true` 直接 return;每 session 只触发一次(`state/<sid>.fired`)。
- - `handoff-resume.mjs`(SessionStart):注入本项目最新未消费的 handoff,
- 注入后写 `<file>.consumed` 保证只注入一次。`source==="compact"` 时跳过。
- - `run-hook.sh`:解析 node 绝对路径(managed → homebrew → PATH),
- 找不到也 exit 0。**所有 hook 脚本异常都要 exit 0**,否则会卡住对话。
- - handoff 目录按 `sha1(cwd).slice(0,12)` 分项目,别依赖 app 的 cwd-slug
- (不同版本大小写规则不一致)。
-
- 阈值可通过 `handoff.config.json` 调;`contextWindow` 留 null 时按
- `settings.autoCompactWindow` → `CODEBUDDY_AUTO_COMPACT_WINDOW` → 200000 兜底。
+ 4. **校验 JSON**:用 `JSON.parse` 解析整个 settings 文件,确认其余键未丢。
+ 5. **实测触发**:手动复现单个 hook —— `echo '<payload json>' | /bin/bash -lc '<command>'`,
+ 再跑一轮真实会话确认 handoff 产出与注入都发生。
+ 6. **清理测试产物**:删掉 `hooks/state/` 与 `handoffs/` 下的测试文件。
- ## 排错
+ ## Decision Rules
- - **hook 不触发**:确认 `~/.workbuddy/settings.json` JSON 合法、命令用绝对路径、
- 脚本可执行。手动复现:`echo '<payload json>' | /bin/bash -lc '<command>'`。
- - **到不了阈值**:几乎一定是第 8 条 —— 50% 就被压缩了。开 Max Mode,或设
- `CODEBUDDY_PRE_MESSAGE_COMPACT_PCT=80`,或降低 handoff 阈值。
- - **grep app.asar 搜不到字符串**:284MB 大文件直接 grep 会漏。用
- `npx @electron/asar extract <asar> <dir>`(3-4s,~800MB)再 grep。
+ - **触发点只能选 Stop**:它是唯一能强制模型继续的事件;**注入点只能选 SessionStart**:它的 stdout
+ 或 `hookSpecificOutput.additionalContext` 是唯一能进新会话上下文的位置。**PreCompact 不能当
+ handoff 触发器**(stdout 只进压缩指令,exit 2 只给用户看消息)。
+ - **阈值调整优先级**:先看是不是被内置 50% pre-message 压缩截断 —— 开桌面端 **Max Mode**,
+ 或设 `CODEBUDDY_PRE_MESSAGE_COMPACT_PCT=80`;确认能到目标比例后,再调
+ `handoff.config.json` 里的 handoff 阈值。
+ - **contextWindow 兜底顺序**:`handoff.config.json.contextWindow` 留 null 时依次取
+ `settings.autoCompactWindow` -> `CODEBUDDY_AUTO_COMPACT_WINDOW` -> `200000`。
+ - **需要确认事件名 / matcher 语义 / hook 入参 / 阈值常量** -> 读 `references/runtime-facts.md`。
+ - **要写或改 hook 脚本本身** -> 读 `references/hook-implementation.md`。
+ - **hook 不触发 / 到不了阈值 / 搜不到字符串** -> 见 `## Troubleshooting`。
- ## 交付规范
+ ## Constraints
- - 改 `~/.workbuddy/settings.json` 前先备份,改完用 `JSON.parse` 校验且确认其余键未丢。
- - 脚本内不写 Unicode 制表/装饰字符(乱码)。中文可正常写。
+ - 改 `~/.workbuddy/settings.json` 前先备份;改完必须 `JSON.parse` 校验,并确认其余键未丢。
+ - **所有 hook 脚本的异常分支都必须 `exit 0`**,否则会卡住对话。
+ - 每 session 只触发一次:Stop 脚本要检查 `stop_hook_active===true` 直接 return,并用
+ `state/<sid>.fired` 去重。
+ - handoff 目录按 `sha1(cwd).slice(0,12)` 分项目,不要依赖 app 的 cwd-slug(不同版本大小写规则不一致)。
+ - 脚本内不写 Unicode 制表/装饰字符(会乱码);中文可正常写。
- 测完清理 `hooks/state/` 与 `handoffs/` 下的测试产物。
+ - **不要用 PreCompact 做 handoff 触发**;不要把 hook 的 stdout 当成能进用户可见消息的通道。
+
+ ## Priority
+
+ 1. **不卡住对话**(hook 任何异常一律 `exit 0`)> "一定要触发 handoff"。
+ 2. **先解除原生 50% 压缩**,再调 handoff 阈值 —— 否则阈值永远达不到。
+ 3. **防死循环**(`stop_hook_active` 检查 + 每 session 一次)> 尽量多触发。
+
+ ## Stop Conditions
+
+ - handoff 到不了阈值 -> 不要反复下调阈值;先确认是否被 50% 原生压缩截断、或用户没开 Max Mode。
+ - `grep app.asar` 搜不到目标字符串 -> 不要反复 grep;改用
+ `npx @electron/asar extract <asar> <dir>` 后再 grep。
+ - 改完 settings 后 `JSON.parse` 失败 -> 立即还原备份,不要继续追加改动。
+
+ ## Verification
+
+ 交付前逐项确认:
+
+ - [ ] `~/.workbuddy/settings.json` 已备份,且 `JSON.parse` 通过、其余键未丢。
+ - [ ] 三个脚本就位且可执行;每个异常分支都 `exit 0`。
+ - [ ] Stop 注册带 `stop_hook_active` 与 `state/<sid>.fired` 双重防死循环。
+ - [ ] SessionStart 注册的 matcher 为 `startup|clear`,且 `source==="compact"` 时跳过。
+ - [ ] 实测确认:达标时产出 handoff,新会话读到且只读一次。
+ - [ ] `hooks/state/` 与 `handoffs/` 下的测试产物已清理。
+
+ ## Output
+
+ - **Outcome**:一条可用的 handoff 门禁 —— 上下文达阈值时自动产出 handoff 文档,
+ 新会话(startup / clear)自动注入该文档继续干活。
+ - **Done when**:`## Verification` 全部勾选,且用户能在真实会话里看到 handoff 产出与注入。
+ - **Evidence**:settings 中新增的 `hooks` 段;`handoffs/` 下带 `<file>.consumed` 标记的文件;
+ `state/<sid>.fired` 标记;一轮实测会话的前后对比。
+
+ ## References
+
+ 按需加载,不要预先全读:
+
+ - `references/runtime-facts.md` — 需要确认 hook 事件名、matcher 语义、hook 入参、transcript 路径
+ 或内置压缩阈值常量时读。
+ - `references/hook-implementation.md` — 要写或修改 `handoff-trigger.mjs` / `handoff-resume.mjs` /
+ `run-hook.sh` 时读。
+
+ ## Troubleshooting
+
+ - **hook 不触发**:确认 `~/.workbuddy/settings.json` JSON 合法、命令用绝对路径、脚本可执行。
+ 手动复现:`echo '<payload json>' | /bin/bash -lc '<command>'`。
+ - **到不了阈值**:几乎一定是被内置 50% pre-message 压缩截断 —— 开 Max Mode,或设
+ `CODEBUDDY_PRE_MESSAGE_COMPACT_PCT=80`,或降低 handoff 阈值。
+ - **grep app.asar 搜不到字符串**:284MB 大文件直接 grep 会漏。用
+ `npx @electron/asar extract <asar> <dir>`(3-4s,约 800MB)解包后再 grep。