Skip to content
Back to skills

session-fork

BSecurity

把一个会话的工作现场(上下文、已确认结论、已做步骤、工具结果)整体复制成独立分支,供用户从任意节点换方向重走、并行试几条路、或保住原线不被带偏;分叉对象是工作现场而非聊天记录,对话 / 任务 / 方案 / 代码 / 写作 / 调研均可。当你说"打分支""会话分叉""把这个任务复制成新分支""以某条回复为界新建对话""并行试几条路""把对话截断复制",或提到 fork this session / branch this task 时使用。底层为 fork-core 通用引擎,跨产品可用。

  • 3 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 19, 2026
ai-agentspythongoshellbashsqlgitapibackend

Works with

  • claude code
  • cli
  • api

Security analysis

B75/100
  • criticalSends environment variables or credentials to an external URL

Pro scans all 19 files and shows the line behind each finding

Scanned September 29, 2026

npx -y skills add yamingmou/session-fork-core --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of session-fork?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for session-fork
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/yamingmou-session-fork/badge)](https://www.skillsdirectory.com/skills/yamingmou-session-fork)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: session-fork
slug: session-fork
displayName: 会话分叉(打分支)
display_name: 会话分叉(打分支)
display_name_en: Session Fork
description: 把一个会话的工作现场(上下文、已确认结论、已做步骤、工具结果)整体复制成独立分支,供用户从任意节点换方向重走、并行试几条路、或保住原线不被带偏;分叉对象是工作现场而非聊天记录,对话 / 任务 / 方案 / 代码 / 写作 / 调研均可。当你说"打分支""会话分叉""把这个任务复制成新分支""以某条回复为界新建对话""并行试几条路""把对话截断复制",或提到 fork this session / branch this task 时使用。底层为 fork-core 通用引擎,跨产品可用。
description_zh: 把走到一半的工作整体复制成独立分支——上下文、已确认的结论、做过的步骤、工具结果都跟着走,从任意节点接着推进,原线不受影响。不只是对话:任务、方案、代码、写作、调研都能分叉(如「这条方向走岔了,回到上一轮重新来」「同一个任务并行试几条路」)。注意:分叉复制的是会话上下文,工作区产物不会跟着回退——除非产物自身有版本记录(如 git),否则只有当前最终态。
description_en: Duplicate any work-in-progress into an independent branch — not just conversations, but tasks, plans, code, writing and research. Context, confirmed conclusions, completed steps and tool results all come along; resume from any point while the original line stays untouched and keeps running. Note that a fork copies the session context only; workspace artifacts are not rolled back — unless they are version-controlled (e.g. git), only their final state exists.
version: 2.4.20
author: OfferKuai (Offer快) Team
license: MIT
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
metadata: {"openclaw":{"emoji":"🔀","requires":{"anyBins":["python3","python","py"]}}}
tags: [workbuddy, claude-code, codex, hermes, openclaw, pi-coding-agent, session, fork, conversation, task, 会话分叉, 打分支, 任务分叉, 并行探索, 办公效率, 会话管理, 对话管理, 效率]
agent_created: true
---

<h1><img src="https://raw.githubusercontent.com/yamingmou/session-fork-core/main/logo.png" width="40" height="40" alt="Fork Logo" style="vertical-align: middle;"> Session Fork(会话分叉 · 打分支)</h1>

**语言 / Language** — 本技能面向中文用户编写。英文总览见 [README.en.md](https://github.com/yamingmou/session-fork-core/blob/main/README.en.md);中文总览见 [README.md](https://github.com/yamingmou/session-fork-core/blob/main/README.md)。
*Written in Chinese. For an English overview see the linked README.en.md.*

把一个会话的**工作现场整体复制**成独立分支:取源会话 transcript 的前 1..截断点行 → 递归改写其中的会话 id → 写入新会话文件 → 在会话索引与谱系索引注册。原会话零改动。

- **分叉对象 = 工作现场**(上下文 + 已确认结论 + 已做步骤 + 工具结果),**不是"聊天记录"**。因此对话、**任务**、方案、代码、写作、调研都可以分叉——用户说"把这个任务分个支""同一个任务并行试几条路"同样属于本技能职责。
- **它替你省下的(一句话)**:不用重讲一遍背景、不用重跑一遍前面的步骤,原线也不会被带偏。
- **默认行为**:截断点 = 上一轮输出的结束;用户一旦给出断点信息,必须改用 `--match` / `--line` / `--request-id`(见「工作流程 · Step 2」)。

## ⚠️ 分叉边界:上下文会回去,产物不一定(必读)

**分叉自由,产物不自由。** 分叉复制的是**会话的工作现场**(transcript 的 `1..截断点` 前缀,含工具结果的**记录文本**,不是把工具重跑一遍),外加会话索引 / 谱系登记——**它不复制、也不回退工作区文件,更不撤销任何已经发生的外部动作**。

所以分支落地后可能出现这种错位:**分支里的上下文停在切点,而磁盘上的产物已经是"现在"的样子(最终态)**。产物能不能跟着回到那一刻,取决于它自己有没有版本记录:

| 产物形态 | 能否回到那一刻 | 怎么做 |
|---|---|---|
| 在 git 仓库里(已提交) | ✅ 能 | 分支侧 `git switch -c <名> <sha>` 或 `git worktree add <目录> <sha>`,把工作区切到与切点对应的提交,上下文与产物重新对齐 |
| 有版本记录的载体(云文档版本历史、系统快照 / Time Machine、备份、日志) | ✅ 通常能 | 手动回溯到对应时间点 |
| 无任何版本记录的本地文件 | ❌ 只有最终态 | 分叉给到的是「旧上下文 + 新文件」;需要旧版本只能事先备份 |
| 已发生的外部副作用(发出的消息 / 邮件、已发布页面、已 push 的提交、API 写入、装好的依赖) | ❌ 不可撤销 | 分叉不能取消已发生的事,需在外部自行补救 |

**打分支时的约定(每次都要执行):**

1. **每次打分支都用一句话交代边界**(一句话即可,不要长篇):分叉的是会话上下文;工作区文件不会跟着回退,除非它在 git 等有版本记录的载体里。**不得**任何措辞暗示分叉能"时光倒流";没把握时宁可说得保守。
2. **动手前看一眼工作目录**(一条命令的事):
   - `git rev-parse --show-toplevel` 成功 → 是仓库:报告当前 `HEAD`(短 sha + 提交主题)并告知"分支侧可用 `git worktree` 对齐到切点";若 `git status --porcelain` 非空,**明确提示这些未提交改动不会被分叉带走**;
   - 失败/非仓库 → 明确告知**该目录下的文件只有当前最终态,分叉无法回退它们**。
3. **用户真实意图是"回到旧产物"(而不只是"从旧上下文继续")时,不要只打一个会话分支了事**——先建议(用户同意后执行)git 检查点(`commit` / `stash`)或文件备份,再分叉。
4. 分支是**快照**:分叉点之后原会话新增的消息不会进分支——这是设计行为,向用户说明时不要误称为"丢数据"。

## 功能特性

- **零配置默认模式**:用户只说"打分支"即可,截断点自动 = 上一轮对话的输出结束,无需提供任何拆分点文本;
- **精确指定模式**:按用户引用的某条回复特征文本(`--match`)、行号(`--line`)或请求ID(`--request-id`)截断;
- **存储级复制**:新 jsonl 文件 + 会话索引新行记录,不是"链接/指向"——原会话后续写入不会污染分支;
- **谱系可追溯**:`parent_id` + `at_seq`(快照点)记录分支从哪派生,`--list --tree` 展示分叉树;
- **快照点可回**:分支可再派生(从分支再 fork = 新投影继续演进);
- **内置安全**:执行前自动备份(仅源 jsonl),递归 id 替换(rawContent/rawResponse 等原始内容黑名单不碰),自带完整性校验;
- **可预览**:`--dry-run` 先确认截断点定位,再正式执行。

## 实现结构(通用引擎)

脚本基于 **fork-core 通用引擎 + 产品 adapter** 设计(实现细节,不影响使用):

```
session-fork/
├── SKILL.md                    # 技能定义
├── scripts/
│   └── create_branch.py        # 唯一入口(调用它即可)
├── fork_core/                  # 通用引擎(与产品无关)
│   ├── engine.py               # 截断点定位/截取/备份/验证/汇报
│   ├── models.py               # SessionMeta / ForkResult 契约
│   ├── cli.py                  # 命令行解析与输出
│   ├── adapters.py             # adapter 注册表(工厂)
│   ├── adapter_base.py         # TranscriptionAdapter 接口
│   ├── adapter_workbuddy.py         # WorkBuddy(默认)
│   ├── adapter_claude_code.py       # Claude Code
│   ├── adapter_codex.py             # Codex
│   ├── adapter_hermes.py            # Hermes
│   ├── adapter_pi.py                # pi
│   ├── adapter_openclaw.py          # OpenClaw ≤2026.6.x(JSONL 后端)
│   └── adapter_openclaw_sqlite.py   # OpenClaw ≥2026.9.x(SQLite 后端)
└── tests/                      # 自测(仅源码仓库有;安装包里不含)
```

- 核心逻辑(默认/--match/--line/--request-id 定位、结构化 id 替换、完整性校验)全部在引擎层,与存储格式无关;
- 每个产品只实现一个 adapter(接口组:定位/消息判定/读写/注册/文案/体检),格式差异被完全隔离;
- 未来新增产品支持 = 新增一个 adapter 文件,引擎零改动(旧 adapter 亦无需改动,走基类默认实现)。

## 触发条件

用户**明确要求创建/执行**"打分支 / 会话分叉 / 对话分支 / **任务分叉** / 复制对话成新分支 / **把任务复制成新分支** / **同一个任务并行试几条路** / 分支会话 / split session / fork session / branch this task / 新建分支 / 从这里分叉"。

**只需记住这一条(其余交给工具):**
> **没贴 conversation ID → 打当前对话的分支;贴了 conversation ID("复制请求 ID"的 JSON)→ 打那个对话的分支。**

- **默认**:用户只说"打分支"或"打分支,命名『X』"(没贴任何 ID)——源会话 = 当前对话,截断点 = 上一轮对话输出结束。
- **从某条回复打**:用户贴了复制的 ID(可能来自本对话或任何其他对话),或说了"从『XXX』那条回复打"——按用户给的信息定位源会话和断点。
- **命令映射**(按用户给的信息直接判断,不把选择抛给用户):

> **怎么选 flag(一句话口诀)**:
> **用户贴了 ID** → 用 `--request-id`(和 `--session` 一起);
> **用户只是在说文字**(哪怕他说的是"那条**回复**")→ 用 **`--match "<那段文字>"`**。
> ⚠️ `--request-id` **只能装 ID**,不能装文字。

> **⚠️ 动手前先做这一件事**:下面的命令**统一写成 WorkBuddy 形式**(`python3 "${SK}/scripts/create_branch.py" …`)。**若你不在 WorkBuddy 里**,先读「**Step 0 · 先判定你在哪个产品里**」,把前缀换成 `fork`(pip 渠道)或你所在产品的技能目录路径,**并补上 `--adapter <你的产品>`**——其余参数完全相同。
>
> **入口路径先用命令取,不要写死**(技能目录可能带市场后缀,如 `session-fork__skillhub/`;写死会指向不存在的路径,等于跑到别的目录的副本上):
> ```bash
> SK=$(ls -d ~/.workbuddy/skills/session-fork*/ 2>/dev/null | head -1)
> SK="${SK%/}"        # 去掉尾斜杠:下面统一用 ${SK}/scripts/…,两平台拼法都成立
> [ -n "$SK" ] && [ -f "${SK}/scripts/create_branch.py" ] || echo "⚠️ 找不到技能入口,先确认技能装在哪"
> echo "入口: ${SK}/scripts/create_branch.py"
> ```

| 用户给了什么 | 用什么命令 |
|---|---|
| 什么都没贴(当前对话) | `python3 "${SK}/scripts/create_branch.py" --session current`(默认截断点) |
| @引用了一段内容(long-text quote)说打分支 | 从引用 JSON 提取 id(格式 `<sessionId>-<requestId>`,如 `"ec48e1ae-…-e683a22a…"`)→ `python3 "${SK}/scripts/create_branch.py" --session <sessionId> --request-id <requestId>` |
| 贴完整 JSON(conversationId + conversationRequestId) | `python3 "${SK}/scripts/create_branch.py" --session <conversationId> --request-id <conversationRequestId>`(conversationId = 会话 ID,直接定位) |
| 只贴了 conversationRequestId / traceId | `python3 "${SK}/scripts/create_branch.py" --request-id <id>`(自动反查该 ID 所属会话,跨工作区) |
| 说了文本/行号 | `python3 "${SK}/scripts/create_branch.py" --session current --match "XXX"` 或 `--line N` |
| **只是在聊天里"提到"某段文字**(没有复制任何 ID) | 同上:`--match "<那段文字>"` ⚠️ **不要**把它当 `--session`,也**不要**自己造一个 `--request-id` |

> **⚠️ 关键规则**:用户给了**任何形式的引用**(@long-text 引用 / 复制的请求 ID JSON / 纯 requestId / 指向别处的会话内容)→ **源会话 = 引用所指的那个会话,不要默认用 `--session current` 打当前对话**。识别引用 ID:JSON 里找 `conversationId`,或 @引用内容里找 `"id": "<sessionId>-<requestId>"` 双段拼接格式。拿不准时先 `--dry-run` 展示将要打源会话名 + 断点,问用户确认再正式执行——**不要反复试错创建分支**。

> **⚠️ 三个最常见的错误**:
> ① **把"用户口头提到的文字"当成会话 ID**:用户说"从『teal』那条回复打分支"→ 用 `--match "teal"`,**不是** `--session teal`;
> ② **自己造 `--request-id`**:用户没贴任何 ID 时,本来就**没有** request-id 可用;"某段文字"要放进 **`--match`**,**不是** `--request-id`;
> ③ **命令入口写错产品**:在 WorkBuddy 里不要输出裸 `fork …`(要写完整路径);在 pip 渠道**不要**输出 `~/.workbuddy/skills/…`(那台机器上没有这个路径)。入口按「**Step 0**」定。

**命令示例**(左边是用户的话,右边是完整命令;前缀按 Step 0 替换):

| 用户这么说 | 对应命令 |
|---|---|
| 打分支,命名『论文讨论』 | `python3 "${SK}/scripts/create_branch.py" --session current --name "论文讨论"` |
| 从『teal』那条回复开始打分支 | `python3 "${SK}/scripts/create_branch.py" --session current --match "teal"` |
| 打分支,从第 6 行截断 | `python3 "${SK}/scripts/create_branch.py" --session current --line 6` |
| (贴了 conversationId + conversationRequestId) | `python3 "${SK}/scripts/create_branch.py" --session "<conversationId>" --request-id "<conversationRequestId>"` |

### 排除(不触发执行,只回答问题)

以下场景用户只是**咨询/了解**,不应执行分叉操作:

- "什么是分叉 / 分叉有什么用 / 怎么用分叉功能" → 解释功能,不执行
- "帮我看看当前有没有分支 / 列出分支" → 用 `--list` 查询,不创建
- "这个对话太长了" / "对话需要整理" → 不自动推断要分叉,询问用户意图
- "能不能回到之前的某个点" → 解释可以用分叉实现,询问是否执行
- 用户在讨论分叉的概念/原理/对比 → 只回答,不执行

## 工作流程

### Step 0 — 先判定"你在哪个产品里"(决定命令入口,不要写死)

本技能在 **6 个产品**可用,但**命令入口不一样**。动手前先定下来你是哪一种,再照后面的示例写命令:

| 你在哪 | 命令入口 |
|---|---|
| **WorkBuddy**(技能市场安装) | `python3 "${SK}/scripts/create_branch.py" <参数>` |
| **其他产品的技能目录**(如从 ClawHub 装进该产品的 skills 目录) | `python3 <该技能目录>/scripts/create_branch.py <参数>` |
| **pip 安装的命令行**(Claude Code / Codex / Hermes / pi / OpenClaw 在终端里的路径) | `fork <参数>` |

**判定顺序(30 秒内定下来,不要猜、不要两套都试)**:

1. `fork --version` 能跑通 → 用 **`fork`**;
2. 否则 `ls -d ~/.workbuddy/skills/session-fork*/ 2>/dev/null | head -1` **有命中、且该目录下 `scripts/create_branch.py` 存在** → 用 **WorkBuddy 形式**(真入口 = `${SK}/scripts/create_branch.py`,见上文取法;⚠️ 目录名可能带市场后缀 `__skillhub`,**不要**写死成 `session-fork/`);
3. 否则看你所在产品的 skills 目录里有没有本技能;
4. 都不确定 → **问用户一句**:"你是在 WorkBuddy 里用,还是本机命令行(pip 装的)?"

**adapter 对应**:WorkBuddy 是默认(不写 `--adapter`);其余五种**必须写** —— `--adapter claude-code` / `codex` / `hermes` / `pi` / `openclaw`。

### Step 0.5 — 平台差异(决定「命令怎么写」)

**先判平台**:`uname` 能跑 → **macOS / Linux / WSL**(POSIX,本文其余命令直接可用,无需替换);报"不是内部或外部命令" → 你在**原生 Windows**,按下表换写法。

| 用途 | POSIX(macOS / Linux / WSL) | 原生 Windows(PowerShell / CMD) |
|---|---|---|
| 跑入口 | `python3 "<技能目录>/scripts/create_branch.py"` | `py -3 "<技能目录>\scripts\create_branch.py"` |
| 取技能目录 | `SK=$(ls -d ~/.workbuddy/skills/session-fork*/ \| head -1)` | `$SK=(Get-ChildItem "$env:USERPROFILE\.workbuddy\skills" -Dir -Filter "session-fork*")[0].FullName` |
| 家目录 | `~` / `$HOME` | `$env:USERPROFILE`(PS)· `%USERPROFILE%`(CMD) |
| 设环境变量 | `export CODEX_HOME=/x` | PS `$env:CODEX_HOME="C:\x"` · CMD `set CODEX_HOME=C:\x` |
| 查文件内容 | `grep -n "词" f.jsonl` | PS `Select-String -Path f.jsonl -Pattern "词"` · CMD `findstr /n "词" f.jsonl` |
| 设文件只读 | `chmod 444 f` | `attrib +R f` |

**Windows 上的四个坑**(均有官方依据):

1. **`python3` 不保证存在** —— Python 官方把它定位为"捕获 POSIX 习惯误用"的兼容命令,**不推荐使用** ⇒ 优先 **`py -3`**,其次 `python`。
2. **`python` 可能被 Microsoft Store 应用别名拦截**(敲了没反应、或弹出商店)⇒ 同样优先 `py -3`。
3. **PowerShell 执行策略**可能拦住全局 npm 装的 CLI(报 `npm.ps1 cannot be loaded because running scripts is disabled`)。修法是**用户自行**执行 `Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser` —— **本技能不会替你改系统设置**。
4. **WSL 与原生 Windows 的会话库互不共享**(`/home/u/.codex` ↔ `C:\Users\u\.codex`)—— 你在哪边跑,就用哪边的库。

> **目录位置两平台同构**(`~/.claude` ↔ `%USERPROFILE%\.claude`;`~/.codex` ↔ `%USERPROFILE%\.codex`)⇒ **`--adapter` 与其余参数完全一致**,平台差异只在"命令怎么写"。
> **输出编码**:本技能的 CLI 会自动把 stdout 固定为 UTF-8(**仅当当前编码容纳不了中文/emoji 时**),所以 Windows 上中文与 emoji 都能正常输出,不需要你额外设环境变量。若你的控制台仍显示乱码,那是控制台自身的代码页问题——`chcp 65001` 切到 UTF-8 即可。

### 分支怎么出现(**按产品**,与工具输出一致;不要凭记忆)

脚本创建后会在输出里给一行 `ACTION : 分支已创建——…`,那句话是**按产品适配**的。**照它转述给用户**;下表是同一份内容的备查(有反直觉项,不要凭印象):

| 产品 | 怎么看到分支 |
|---|---|
| **WorkBuddy** | 左侧会话列表**非实时刷新** → 重启客户端(macOS:⌘Q 重开 或 `open -a WorkBuddy`;Windows:托盘图标右键 → 退出后重开) |
| **Claude Code** | 在终端重跑 `claude`,用 **`/resume`** 选择该会话(**无需重启其他程序**) |
| **Codex** | ① 可直接续跑:`codex exec resume <新 id> "…"`;② **桌面版会话列表也要重启才可见**(macOS ⌘Q / Windows 托盘退出);③ 想被 `codex fork --last` 选中,用 `codex exec resume` 跑一轮 |
| **Hermes** | **无需重启、无需修复命令**:`hermes sessions list` 即可看到;`hermes chat --resume <分支 id>` 续跑 |
| **pi** | **无需重启**:会话选择器(`/resume`)或 `/tree` 刷新即可 |
| **OpenClaw** | `openclaw sessions --json` **立即可列出**;若 Gateway 正在运行,重启一次以加载 |

**只对 WorkBuddy 成立**的四件事(不要套到其他产品上):① 界面上的"**复制请求 ID**"按钮;② 「重启后左侧才出现」这个**具体表现**;③ `--fix`(**仅 workbuddy**,其他 adapter 会直接报错;⚠️ 这是**本技能**的参数,与 OpenClaw 官方的 **`openclaw doctor --fix`** 不是一回事);④ 项目日志写 `.workbuddy/memory/`。

### Step 1 — 确认源会话

按上表映射,先看用户贴了什么:
- 什么都没贴 → `--session current`(当前对话,脚本自己判定,含义见下);
- 贴了**完整 JSON**(UI 复制请求 ID 的原始格式)→ 取 `conversationId` 作为源会话(WorkBuddy / Claude Code 这类按 id 命名的存储里,conversationId 就是会话文件名,直接定位、不扫描;其他产品交给引擎按 id 定位);`conversationRequestId` 作为断点;
- 贴了**纯 requestId/traceId**(无 conversationId)→ 用 `--request-id` 自动反查该 ID 属于哪个会话(跨工作区兜底,按 mtime 新→旧搜)。
- **`--session current` 的含义**:**你正在其中的这个对话**。唯一判据 = 执行环境里的会话标识(`CLAUDE_SESSION_ID` / `CODEBUDDY_SESSION_ID` / `BAGGAGE`);其他产品由各自 adapter 定义。**不需要自己去翻数据库找"当前会话"**——交给 `--session current`。
- **⚠️ `current` 不猜**:拿不到会话标识时脚本**硬失败**,不会替你选一个"最近活跃的会话"。那**不是**你正在其中的对话,打出来会是**另一个对话的分支**。确实想从"最近活跃的会话"打时,用**显式**的 `--session latest-working`(语义即其名)。
- **会话存在哪,不用你记;也❌不要把 WorkBuddy 的路径套到其他产品上**:

  - **WorkBuddy**:`~/.workbuddy/projects/<workspace-slug>/<session-id>.jsonl`(slug = cwd 去 `/` 后 `/` 换 `-`);元数据在 `~/.workbuddy/workbuddy.db` 的 `sessions` 表。
  - **其他产品**:**由 adapter 自动探测**——Claude Code / Codex / Hermes / pi / OpenClaw 的目录与格式各不相同(JSONL 与 SQLite 都有)。需要覆盖位置时用环境变量(如 `CODEX_HOME` / `HERMES_HOME`),**不要手改会话文件**。

### Step 2 — 确认截断点(默认规则优先)

**⚠️ 最高优先级规则:用户说了断点 → 必须用 `--match` 或 `--line`,不要用默认模式。**

默认模式只在用户**完全没提断点**(只说"打分支")时才用。一旦用户描述了任何断点信息("截断到 XXX"、"从 XXX 之前分叉"、"到 XXX 产生处"),就必须用指定模式。

**三种场景的处理方式:**

| 用户说的 | 用什么 | 怎么做 |
|---|---|---|
| "打分支"(没提断点) | 默认模式 | 脚本自动定位最后一条 assistant 回复 |
| "截断到这条回复" + 提供了请求ID | `--request-id` | **最精确**,直接匹配 conversationRequestId |
| "截断到 XXX" | `--match "XXX"` | 取最后一条包含 XXX 的 assistant 回复 |
| "截断到 XXX 产生处" | `--line N` | 先用 grep/Read 在源会话 jsonl 中搜索 XXX 所在行号,再用 `--line` |

**⚠️ 最精确的截断点 = 请求ID(`--request-id`)——但"怎么拿到它"只有 WorkBuddy 有现成按钮:**

- **WorkBuddy**:① 在 UI 中点击目标断点处的**助手回复**;② 点"**复制请求ID**",得到 JSON 如 `{"traceId":"...","conversationRequestId":"abc123","conversationId":"..."}`;③ 用 `--request-id abc123` 截断——**唯一标识一条回复,不会误匹配**。

**⚠️ 关键:当用户口头描述断点但没给精确文本时(如"截断到项目思维规则模板产生处"),必须先在源会话 jsonl 中搜索确认行号,然后用 `--line`,不要忽略断点用默认模式。**

**断点定位步骤(指定模式):**
1. 在源会话 jsonl 中搜索用户描述的关键词(`grep -n "关键词" <session-id>.jsonl`);
2. 确认匹配行是 assistant 消息且有 output_text(必须是完整回复,下一行是 user 消息或 EOF);
3. 用 `--line N` 或 `--match "精确文本"` 截断;
4. 跑 `--dry-run` 验证定位正确后,再正式执行。

**默认模式规则(仅当用户完全没提断点时):**
- **会话内打分支**(你自己就是执行者,脚本跑在被 fork 的那个会话里):
  截断点 = **上一轮对话的输出结束**(用户最后一条 user 消息之前的最后一条完整 assistant 回复)。
  本轮"打分支"这条指令与你此刻的叙述**都不进分支**。
- **从外部打分支**(人在终端执行、或显式指定另一个会话):截断点 = **整份复制**到文件末尾最后一条完整 assistant 回复(此时没有"本轮"要排除)。
- 两种语义由脚本按"执行环境里有没有本次会话的标识"自动判定,**不需要你指定**;
  `--dry-run` 会打印走了哪一种(`in-session` / `whole`)以及锚在哪一行、锚点原文是什么——**照它核一遍再正式执行**。
- 要覆盖判定结果,只在会话内才需要:加 **`--whole`** 强制整份复制。

### Step 3 — 执行创建(用脚本,不要手写)

**打分支的固定顺序(脚本已内置,不要绕过或手写等价流程):**

1. 写入 `.tmp` 文件(零外部可见副作用)
2. **立即自检**(校验磁盘上的字节,不是内存数据)
3. 自检通过才 `os.replace` 到正式路径(原子落位)
4. 最后登记 db 与谱系

自检失败 = **什么都没发生**(只留可忽略的 .tmp);登记失败 = 干净回退(文件回滚 + 痕迹清除)。
**不要**「先落 db 再验证」——那会让坏分支出现在侧边栏。
dry-run 也走完整校验(会真写 .tmp + 自检,只是不落位/不登记)——`Verify: OK` 才是真 OK。

**执行方式:只用这一条命令**(不需要 pip、不依赖 cwd,整行复制即可用):

- **WorkBuddy**:`python3 "${SK}/scripts/create_branch.py" <参数>`
- **其他产品的技能目录**:`python3 <该技能目录>/scripts/create_branch.py <参数>`
- **pip 命令行**:`fork <参数>`

> **⚠️ 两条不要做**:① **不要把两套入口混用**(在 WorkBuddy 里不要用裸 `fork`;在 pip 渠道**不要**写 `~/.workbuddy/skills/...`——那台机器上没有);② **不要执行 `fork_core/cli.py`**(唯一入口是 `scripts/create_branch.py`)。
> 下面示例统一写 **WorkBuddy 形式**,按 Step 0 换成你的入口(参数完全相同)。

```bash
# 场景 1:当前对话打分支(用户没贴任何 ID)
python3 "${SK}/scripts/create_branch.py" --session current --name "<分支名>"                 # 默认截断到上一轮输出结束
python3 "${SK}/scripts/create_branch.py" --session current --match "<拆分点特征文本>"          # 从当前对话某条回复打

# 场景 2:从贴的 conversation ID 打(用户贴了"复制请求 ID",可能来自任何对话/工作区)
python3 "${SK}/scripts/create_branch.py" --session "<conversationId>" --request-id "<conversationRequestId>" --name "<分支名>"
#   ↑ conversationId 即 UI 复制 JSON 里的 conversationId(= 源会话 ID),直接定位
# 只拿到 requestId/traceId 时:自动反查所属会话(无需 --session)
python3 "${SK}/scripts/create_branch.py" --request-id "<conversationRequestId>" --name "<分支名>"

# 运维类
python3 "${SK}/scripts/create_branch.py" --list                                        # 查询分支
python3 "${SK}/scripts/create_branch.py" --fix <分支会话ID>                              # 修复被追加消息的分支(仅 workbuddy)
python3 "${SK}/scripts/create_branch.py" --verify                                      # 本机会话库自检(环境变化或换机器后建议跑一次),别名 --doctor
python3 "${SK}/scripts/create_branch.py" --session current --adapter pi                # pi
python3 "${SK}/scripts/create_branch.py" --session current --adapter claude-code       # Claude Code
python3 "${SK}/scripts/create_branch.py" --session current --adapter codex             # Codex
python3 "${SK}/scripts/create_branch.py" --session current --adapter hermes            # Hermes
python3 "${SK}/scripts/create_branch.py" --session current --adapter openclaw          # OpenClaw(≤2026.6.x / ≥2026.9.x 自动识别后端)
# OpenClaw 后端也可强制:FORK_OPENCLAW_BACKEND=jsonl|sqlite(默认 auto)
```

**执行规范**:
- **先交代产物边界 + 看一眼工作目录的 git 状态**(见「分叉边界 · 打分支时的约定」第 1/2 条)——这一步在**创建之前**做,不要等到用户发现产物没跟着回退才解释;
- **⚠️ 回读 `Source` 行(必做)**:脚本输出的第一行是
  `Source   : <会话id>  「<会话名>」`
  **先确认这个源会话就是你(和用户)所在的那个对话**。父 id 对不上时肉眼看不出来,**名字**能一眼看出来。不一致 → **立即停手**,把"你在的对话 / 实际被当成源会话的对话"两个名字摆给用户,问清要哪条;**不要继续,更不要交付一个"来历正确但对话不对"的分支**;
- **先 `--dry-run` 确认截断点,再正式执行**(推荐,防打错位置);
- **`python3 "${SK}/scripts/create_branch.py" --verify` 自检**:环境变化或换机器后建议跑一次;打分支前也可以先跑。输出分三档:
  - **`❌ 失败` = 要修**(前缀内残留 / 结构性 `sessionId` 残留等):以非 0 退出(表示确有需要处理的问题);
  - **`⚠️ 需复核` = 不用修,看一眼即可**:典型情形是**你在分支里记录了血缘**("我的父会话是谁"),
    于是 fork **之后**追加的内容里出现了父会话 id。它**不会**让体检失败、不影响退出码。
    想彻底消解就**重打一次该分支**——新分支会带上"前缀指纹"(`prefix_fp`),此后体检能证明
    "边界之内仍是 fork 原样产物",不再提示。两处边界要知道:
    ① **追加区必须先开一轮新 user 消息**(你接着说下一句)——若紧接快照点的**不是** user 消息,
    这段按"疑似前缀残留"**从严判失败**(宁可在这一处误报,也不让真污染借"追加区"溜过);
    ② 指纹证据**只覆盖快照点之前那 N 行**,对追加区不作任何担保,不要把它读成"整条分支都没问题";
  - **`✅ 通过`**。
  (Claude adapter 在没有真实 CLI 会话时,这一档属预期情况)
- 脚本自动完成备份 → 截取 → 会话 id 替换 → 注册 → 校验,无需手工介入。

### Step 4 — 验证与汇报

脚本自带验证(行数/解析/sessionId 一致性/零残留/末条完整)。汇报**必须使用以下固定模板**:

```
✅ 分支创建完成

📋 分支信息
- 分支 ID:<new-session-id>
- 名称:<custom_title>
- 截断位置:第 N 行 / 共 M 行(默认模式注明"上一轮输出结束")
- 分支行数:N 行

📦 原会话不受影响
- 源会话:<src_id>「<源会话名>」(= 你所在的那个对话,**已核对名称**)
- 原会话继续正常使用,不受影响

💡 查看新分支
- <原样复制脚本输出里那一行 `ACTION : 分支已创建——…`,它已按产品适配(逐产品要点见 Step 0「分支怎么出现」表)>
- **不要凭记忆写"重启 WorkBuddy"**——非 WorkBuddy 渠道那样说就是错的

📌 下一步
- 按上面那条提示回到产品里打开该分支
- 或在当前对话继续(分支已独立保存,不会丢失)
```

查询分支时(`--list`)使用:

```
📂 当前工作区的分支列表
- <id> | <名称> | <状态> | <创建时间>
- ...
(共 N 个分支,无分支时提示"当前工作区暂无分支")
```

### Step 5 — 记录(可选)

建议把分支创建记录追加到**项目自己的日志**里:新 id、行数、截断点、`custom_title`、备份路径——便于日后回溯"这个分支从哪条线分出来的"。

- **WorkBuddy**:日志惯例是 `.workbuddy/memory/YYYY-MM-DD.md`;
- **其他产品**:用它自己的记忆 / 日志约定,**不要**凭空创建 `.workbuddy/` 目录。

## 分支命名建议(可自定义)

- 分支体系用序号区分:`XX·分支A|描述` / `分支B|描述` / `分支C|描述`;
- `custom_title` 建议格式:`<主题>·分支X|<用途>`,如 `重构登录模块·分支A|接口拆分`;
- status 一律 `terminated`(无活跃 agent 的正常终态,不影响打开查看)。

## 关键坑位

1. **用户说了断点 → 必须用 `--match` / `--line`,不要用默认模式**:默认模式只回答"这一轮之前 / 整份"两种问题,**不是**用户指定的中间位置——用它去执行"截断到 XXX 产生处",会把**不该有的后续对话也带进分支**(事后只能用 `--fix` 修)。
2. **默认截断点是两种语义,由脚本自动判定**:
   - **会话内打分支**(最常见:你在对话里说"打分支",由 agent 执行脚本)→ 切在**上一轮输出结束**(最后一条 user 消息的完整回复末尾)。本轮指令与叙述不进分支。
   - **从外部执行**(终端、定时任务、显式指定别家会话)→ **整份复制**到末尾最后一条完整 assistant 回复。
   - ⚠️ **不要**在会话内自己判断"上一轮结束在哪":**本轮你自己的叙述已经在同一份文件里**,按"最后一条 assistant 文本"看,看到的必然是自己。这个判定由脚本按环境自动完成,不需要人工介入——人工判定还会随对话继续而滑动。以 `--dry-run` 打印的锚点为准(会打出行号与锚点原文)。要强制整份:`--whole`。
   - 这就是"默认模式在会话内为什么锚在上一轮结束"的原因:脚本必须先定位**最近一条 user 消息**,再取它之前的最后一条完整 assistant 回复。任何"倒扫全文件最后一条 assistant 文本"的做法,都会把本轮内容一起吞进来。
3. **嵌套字段旧 id 残留**:只改顶层 `sessionId` 不够——`output.text` / `arguments` / `argumentsDisplayText` / `toolResult.renderer.value` / `error.message` 等字段都会出现旧 id。v2.4.3 起使用**递归 id 替换**(覆盖全部可读字段,仅 rawContent/rawResponse 等原始内容黑名单不碰),且**在引擎层统一实现,所有产品一致**——一次打分支通常要改上千处,漏一处就会被体检拦下。
4. **指定模式边界**:用户引用文本可能出现在多条回复里,取最后一条;且必须确认该回复是完整收尾(下一行是 user 消息)。
5. **WorkBuddy 会追加消息到分支文件**(**这条只对 WorkBuddy 成立,不要套到其他产品**):脚本创建分支后,WorkBuddy 主进程可能仍向该 jsonl 追加新消息。脚本不自动锁只读(执行后提示用户自行设为只读),已有分支可用 `--fix` 修复(**仅 workbuddy**)。
6. **快照分支特性**:复制发生在读取时刻,原会话之后的新消息不会进分支——这是正常行为,不是丢数据。
7. **附属目录 tool-results/**:是运行时输出缓存,jsonl 已内嵌完整 function_call_result,分支**不需要**复制附属目录(或建空目录即可)。
8. **先备份再动手**:脚本已内置备份,不要跳过。备份默认**落在你所用产品自己的目录里**——WorkBuddy `~/.workbuddy/backups`;Claude Code `~/.claude/fork-backups`;Codex `~/.codex/fork-backups`;Hermes `$HERMES_HOME/fork-backups`;pi `~/.pi/agent/fork-backups`;OpenClaw `<agent 目录>/fork-backups`。想统一改到别处:设环境变量 **`FORK_BACKUP_DIR`**(对所有产品生效)。(Windows 上这些路径里的 `~` 一律指 `%USERPROFILE%`。)

## 边界

### 血缘只认谱系文件,不认转录(重要)

**fork 产物的正文里读不出"它从谁来"**——因为本引擎做的是**递归 id 替换**:被复制正文里出现的**源会话 id**,会被一并改写成**分支自己的 id**(覆盖全部可读字段,仅 `rawContent` / `rawResponse` 等原始内容黑名单不碰)。

⇒ **拿转录去证"我来自谁"必然得出错误结论**(父 id 在产物里已经被改写掉了)。
**唯一可信的出处是谱系索引**:本产品目录下的谱系文件(WorkBuddy 为 `~/.workbuddy/fork.lineage.json`)里的 `parent_id`、`at_seq`(快照点)与 `prefix_fp`(该前缀的指纹)—— `--list --tree` 读的是它。
(`at_seq` 与 `prefix_fp` 还让体检能**核对**"边界是否可信":前 `at_seq` 行的指纹与 fork 时一致 ⇒ 那段确是复制出来的原样产物,之后的差异只可能来自 fork 之后。)

> 于是会出现这种情形:分支自己的文件里,标记全部指向**它自己**;父会话文件里,标记也只指向父、**不指向该分支**。
> **两边都只剩"自己"** ⇒ 光看转录,会得出"它来自自己"这种荒谬结论。

### 任务分支 vs 身份分化:**同一个工具,两种语义**

| | **任务分支** | **身份分化** |
|---|---|---|
| 用户意图 | 同一段经历换个窗口继续走 | **造出一个新个体** |
| 产物是什么 | 一份可继续的工作现场 | **一个成员**(有自己的身份与记忆) |
| 要多做的一件事 | 交代分支怎么打开、原线不受影响 | **提醒用户:新个体需要"自报身份" + 它自己的记忆文件** |

⚠️ **身份分化时最容易出错的一件事**:让新个体**与来源共用同一份记忆 / 档案文件** ⇒ 两边互相覆盖,**比丢记忆更糟**。
来源的记忆对新个体是"**继承来的上下文**",不是"**它的履历**"。

- **产物边界(最重要的一条,详见上文「分叉边界」)**:分叉只复制**会话上下文**,不回退**工作区文件**与**已发生的外部副作用**。会话可以分叉到任意节点,产物则只有在**自身有版本记录**(git 提交 / 云文档版本历史 / 快照备份)时才能对齐回切点;**没有记录的就只有最终态**。因此"回到过去"这件事,上限由产物的版本记录决定,不由分叉决定。
- 本技能做的是**存储级复制**(JSONL 后端 = 新 jsonl 文件;SQLite 后端 = 库内新 session_id 的一整套行 + 会话索引记录),不是"链接/指向"——链接会让原会话后续写入污染分支,且无法表达"截断到某行为止"。
- 分支创建后如需删除,由用户决定,不擅动。
- 适配边界:**准入判据是「会话转录在静息态可表达为**有序的 JSON 条目序列**,且能独立完整表达会话」**——引擎的读/切/写建立在这个形状上;**承载介质由适配器的行存储接口决定**(v2.4.8 起,不再要求"必须是文本文件")。
  - **已在真机 / 真库验证**:**WorkBuddy**(默认);**pi**(用官方包造真实会话后 fork,再用官方 API 回验)。
  - **已被产品自己认下(引擎产出能被产品读取、继续使用)**:**Claude Code** · **Codex** · **Hermes** · **OpenClaw**
    - **Claude Code**:隔离 home 真机跑出会话 → 引擎 fork → **决定性自包含测试**(把源会话重命名隐藏后 `--resume` 仍能取到被复制的全部前缀消息);并用原生 `--fork-session` 做对照裁定(复制式 + 线性化修剪 + 切点不可指定)。
    - **Codex**:真机 `codex exec` 跑出会话 → 引擎 fork(自包含:丢弃源的 `history_base` 引用)→ 产品自己读回该分支并 `resume` 成功。原生分叉是**零拷贝引用式**(只写 `history_base` 指针、不复制历史),我们的自包含方案与之并存,**两种取值产品都接受**。
    - **Hermes**:隔离 `HERMES_HOME` 真机跑出会话 → **纯 SQL** 写入 `state.db`(`sessions` + `messages` 两表)→ 产品 `sessions list` **列出该分支**、`chat --resume` **续跑并答对被复制前缀里的信息**;随后产品自己往分支追加消息(**已接管**)。**无需任何产品侧修复命令**。
    - **OpenClaw**(两世代):
      - `≤2026.6.x`(JSONL 后端):真机跑出会话 → 引擎 fork → 官方 CLI `openclaw sessions` 列出 → **在分支上续跑成功**。
      - `≥2026.9.x`(SQLite 后端):转录落在 `agents/<id>/agent/openclaw-agent.sqlite` 的 `transcript_events` 表(`(session_id, seq) → event_json`,**与 JSONL 的行序→条目完全同构**)。真机 fork 后官方 CLI 列出、**分支续跑成功**,且 OpenClaw 自己接管并维护该分支的投影进度(`indexed_seq` 随续跑推进)。
        写入涉及 7 张表**缺一不可**,且须跑一次 **`openclaw doctor --fix`**。
        ⚠️ **这是 OpenClaw 官方的命令**(让官方校验并置 `entry_valid`),**与本技能自己的 `--fix` 参数完全无关**——两者同名不同物,**不要混用**(前者在 OpenClaw 里跑,后者只在 WorkBuddy 里跑)。
  - **不在范围**:C 端云优先 SaaS(元宝 / 千问等)本地无转录。

## 数据与权限边界(本技能会读写什么)

> 这一节写给**要在受管环境里评估本技能的人**(安全审查 / 平台审核 / 团队管理员)。照实写,不夸大也不含糊。

**读**:本产品自己的会话记录(WorkBuddy / Claude Code / Codex / Hermes / pi 的 transcript 文件,或 OpenClaw 的 SQLite 库),以及**只读**打开的产品会话索引。
**写**(只有这三处,且**从不修改源会话**):
1. **新建**的分支会话(源会话逐字节不动);
2. **旁路**谱系索引(如 `~/.workbuddy/fork.lineage.json`)——不写产品官方 schema;
3. 产品自己的会话索引(`sessions.json` / `sessions` 表等)**新增一行**指向该分支;**写前自动备份**(`.bak.<时间戳>`)。

**明确不做**:不联网、不上传任何数据、不读凭据或密钥文件、**不请求提权(不调用 `sudo` / `root`)**、不改系统配置、不安装任何东西。
**可先验证再执行**:`--dry-run` **不落位、不登记、不改源会话**——它会写一个临时文件用于**磁盘字节校验**(校验完即清理),所以运行处需要写权限;`--verify` 是**只读**体检。
**文件权限**:分支文件要不要设为只读,**完全由你自己决定**;本技能只**建议**、不代为修改。
**语言**:本技能面向中文用户编写;同时提供英文字段 `display_name_en` / `description_en`(英文说明见仓库 [README.en.md](https://github.com/yamingmou/session-fork-core/blob/main/README.en.md))。

## 故障排查(现象 → 原因 → 处理)

| 适用 | 现象 | 原因 | 处理 |
|---|---|---|---|
| 全部 | `VERIFY FAILED: old session id still present in non-raw fields` | 替换引擎漏了某可读字段(真残留,会拦)| 属创建阻断——v2.4.3 起验证前置:失败自动清理已写文件与注册,**不留脏分支**;排查残留字段是否在新字段类型上 |
| 全部 | `VERIFY FAILED` 但分支已出现在产品里 | **旧版本的问题**(v2.4.3 及更早:verify 在注册后才跑,失败也留脏分支)| 删掉该脏分支;升级到 v2.4.3+,新失败不再留痕 |
| **仅 WorkBuddy** | 分支建好但**左侧没出现** | WorkBuddy 会话列表**非实时刷新**(正常行为)| 重启客户端(macOS:⌘Q 重开 或 `open -a WorkBuddy`;Windows:托盘图标右键 → 退出后重开)后即可看到 |
| 非 WorkBuddy | 分支建好但**看不到** | 各产品不同,且**有反直觉项**(如 Codex 桌面版也要 ⌘Q 重开)| **照脚本输出的 `ACTION` 行做**;逐产品要点见 **Step 0「分支怎么出现」表**——不要照搬 WorkBuddy 的说法 |
| **仅 WorkBuddy** | 分支打开后**末尾多了一段不是我的内容** | 主进程在创建后继续向分支文件追加了消息(脚本不锁只读)| 用 `--fix <分支会话ID>` 重截断到谱系记录的 `at_seq`(**仅 workbuddy 支持**) |
| 全部 | 原会话之后的新消息没进分支 | 快照特性(复制发生在读取时刻)——正常行为,不是丢数据 | 无需处理 |
| 全部 | 分支文件找不到 / 索引有记录但文件被删 | 外部清理 | 无恢复必要则删该索引记录(分支是快照,内容已在源会话) |
| 全部 | `--verify` 体检某分支报"源 id 残留" | 两种可能:①(v2.4.3 及更早)该分支在 v2.2.0 前创建,替换不彻底;②**误报**——fork 后产品追加的消息里恰好出现源 id(如一次 `ls` 输出中恰好有个同名目录,WorkBuddy 的实例是 `ls ~/.workbuddy/tasks`)。v2.4.4 起追加区**只对工具/函数 I/O 字段宽容**,正文等其余字段仍从严 | ①历史脏分支:重新打分支后删除旧分支(备份后);②升级到 v2.4.4+ 即可。若该分支谱系 `at_seq` 缺失或与实际不符,校验会**故意退回更宽的旧边界**——宁可误报也不漏检,此时重打一次该分支即可 |

---

## 安装 / 升级(按产品,各一句话)

- **WorkBuddy**:技能市场搜「会话分叉」,或 SkillHub 搜 `session-fork`;**升级 = 重新装一次**(装完是静态副本,不会自动更新)。

- 源码:https://github.com/yamingmou/session-fork-core · 作者 **OfferKuai(Offer快)团队** · License **MIT**(自由使用、修改与再分发,保留署名)。

## 变更历史

见仓库根 **`CHANGELOG.md`**(每版按:价值 / 实现 / 修改 / 检查)。本文件只留「怎么执行」——把历史塞进每次执行都要读的文档里,是噪音也是成本。**当前版本不在此处手写**:唯一权威是仓库的 tag / Release 与包内 frontmatter 的 `version`。

Files in this skill

  • CHANGELOG.md40 KB
  • README.en.md10.8 KB
  • SKILL.md44.6 KB
  • fork_core/__init__.py814 B
  • fork_core/adapter_base.py28.2 KB
  • fork_core/adapter_claude_code.py12.3 KB
  • fork_core/adapter_codex.py33.2 KB
  • fork_core/adapter_hermes.py29.2 KB
  • fork_core/adapter_openclaw.py12.7 KB
  • fork_core/adapter_openclaw_sqlite.py23.8 KB
  • fork_core/adapter_pi.py20.3 KB
  • fork_core/adapter_workbuddy.py23 KB
  • fork_core/adapters.py5.1 KB
  • fork_core/cli.py14.3 KB
  • fork_core/engine.py56.3 KB
  • fork_core/models.py2.7 KB
  • pyproject.toml1.1 KB
  • scripts/build_skill.py12.5 KB
  • scripts/create_branch.py1.1 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…