Beads 编码代理内存库bd reopen命令完整指南:重新打开已关闭 issue 的语义、参数与底层实现
【免费下载链接】beadsBeads - A memory upgrade for your coding agent项目地址: https://gitcode.com/GitHub_Trending/beads1/beads
bd reopen是 Beads(一个为编码代理提供"记忆升级"的 issue 跟踪工具)CLI 中专门用于重新打开已关闭 issue的命令。本文以其官方 CLI 参考文档 docs/cli-reference/reopen.md 为核心,结合命令入口、存储层事务与跨后端一致性契约的源码实现,完整讲解它的语法、--reason参数语义、与bd update --status open的本质区别、底层状态迁移规则(含自定义状态类别)、事件记录机制与并发安全保证,让你在代理驱动的回归、需求变更等工作流中正确、安全地使用它。
命令概览:语法与核心语义
bd reopen的官方定义(生成自bd help --doc reopen)只有一句话,却包含了三层关键信息:
Reopen closed issues by setting status to 'open' and clearing the closed_at timestamp. This is more explicit than 'bd update --status open' and emits a Reopened event.
命令语法:
bd reopen [id...] [flags]其核心语义可拆解为三点:
- 状态迁移:将 issue 的
status置为open; - 清除关闭记录:清空
closed_at时间戳(从 internal/storage/issueops/reopen.go 的 UPDATE 语句看,实际清除的是closed_at、close_reason、closed_by_session、defer_until四个字段——关闭产生的完整"痕迹"会被一次性抹掉); - 显式事件:发出一个
reopened事件(internal/types/types.go 中定义为EventReopened = "reopened"),这正是它与普通更新命令的本质差异。
为什么比
bd update --status open更"显式"?bd update --status open是一个通用补丁操作,只改状态字段;而bd reopen是生命周期(Lifecycle)级别的语义操作:它同时完成状态迁移、关闭痕迹清理、事件记录、租赁(lease)清理与阻塞状态重算。从 issueops/issueops.go 中Lifecycle.Reopen的契约看,它被定义为"受保护的 issue 变更"之一,与Create、Update、Close并列,是生命周期闭环中与Close严格对称的另一半。
参数详解
位置参数:[id...]
命令要求至少一个 issue ID(源码 cmd/bd/reopen.go 中Args: cobra.MinimumNArgs(1)),支持一次重新打开多个 issue。
ID 解析走的是前缀路由(prefix routing):源码 cmd/bd/reopen.go 调用resolveAndGetIssueForMutation解析每个 ID,注释明确说明"supports cross-rig reopens likebd reopen xe-5ls"——即你只需要提供可唯一定位的前缀,系统会解析到完整 ID,并且支持跨"rig"(不同数据库实例)解析。解析失败(如 ID 不存在)会向 stderr 输出Error resolving <id>: ...并继续处理其余 ID。
标志(Flags)
| 标志 | 简写 | 类型 | 默认值 | 说明 |
|---|---|---|---|---|
--reason | -r | string | 空 | 重新打开的原因 |
--reason的底层去向值得注意(issueops/issueops.go):原因不写入 issue 的字段,而是记录在 issue 的事件历史中——挂在本次迁移自己产生的reopened事件条目上。因此查询原因需要读取 issue 的事件流,而不是bd show的字段。从存储层实现 internal/storage/issueops/reopen.go 看,非空 reason 除了写入reopened事件外,还会额外添加一条评论(AddCommentEventInTx),让原因同时出现在评论流中便于人读。未提供 reason 的 reopen 同样会记录事件条目,只是不携带原因文本。
实战用法:从单个到批量
以下示例基于技能文档 plugins/beads/skills/beads/commands/reopen.md 与 CLI 实现整理:
# 1. 重新打开单个 issue bd reopen bd-42 # 2. 一次重新打开多个 issue bd reopen bd-42 bd-43 bd-44 # 3. 携带原因重新打开 bd reopen bd-42 --reason "Found regression" # 4. 前缀路由(跨 rig 解析) bd reopen xe-5ls常见的重新打开原因(技能文档建议的典型场景):
- Regression found(回归缺陷被发现)
- Requirements changed(需求变更)
- Incomplete implementation(实现不完整)
- New information discovered(发现新信息)
命令输出解读
非 JSON 模式下,成功的 reopen 输出(cmd/bd/reopen.go):
↻ Reopened bd-42: Found regression带原因时会在 ID 后追加: <reason>。命令在只读模式(CheckReadonly("reopen"),见 cmd/bd/reopen.go)或数据库未初始化时会被拒绝执行。
JSON 输出
配合全局 JSON 输出开关时,命令会输出重新打开后 issue 的操作后状态快照(post-state snapshot),而不是重新读取数据库——源码注释明确这是"operation's own post-state snapshot replaces the re-read"(cmd/bd/reopen.go),并会丢弃依赖记录(updated.Dependencies = nil),因为bd reopen从不打印依赖。
幂等与 no-op 行为
bd reopen对非关闭状态是安全的 no-op,分为两种情况(cmd/bd/reopen.go):
- 目标已是
open:输出<id> is already open; - 目标是其他非 done 状态(如进行中、自定义 active 状态):输出
<id> is not closed (status: <X>); nothing to do。
这两种情况都不会写库、不会提交、不会触发 hook——契约 backend/conformance/lifecycle_close_reopen_contract.go 专门用RunLifecycleReopenLeavesNonDoneStatusesUnchanged钉死了这一规则:"非 done"比"已打开"范围更宽,wip 内建状态与自定义 active 状态同样原样保留,且带 holder 的行(如in_progress+ assignee)整体前后比对,防止实现顺手清掉 assignee。
源码级拆解:一次 reopen 事务里发生了什么
从存储层核心函数reopenIssueInTx(internal/storage/issueops/reopen.go)可以完整看到一次 reopen 在单个事务内的全部步骤:
- 平面路由:
IsActiveWispInTx判断目标是 durable issue 还是 ephemeral wisp,再通过WispTableRouting选择对应的 issues/wisps 与事件表; - 状态分类:读取当前
status,用ReopenCategoryInTx解析其类别,只有done类别的状态才会被重新打开(字面量closed以及通过status.custom配置的自定义 done 状态); - 影响集计算:
AffectedByStatusChangeInTx/AffectedByStatusChangeForWispInTx找出本次状态变更会影响到的所有行(用于后续阻塞状态重算); - 条件更新:执行带
WHERE id = ? AND status = ?的条件 UPDATE,将status置为 open,同时清空closed_at、close_reason、closed_by_session、defer_until,刷新updated_at并重铸row_lock(乐观并发令牌); - 行数守卫与并发重试:若
RowsAffected() == 0,说明状态已被并发修改——重读最新状态再次分类,若已是 done 类别则递归重试一次,否则返回 no-op 结果;重试仍失败则报status changed concurrently; - 租赁清理:
DeleteLeaseInTx删除该行持有的 claim 租赁记录; - 事件记录:
RecordEventInTable写入reopened事件(携带 actor 与 reason);若 reason 非空,再AddCommentEventInTx追加一条评论; - 阻塞状态重算:
RecomputeIsBlockedInTxWithResult重算受影响行的is_blocked列——因为 reopen 跨越 closed/pinned 边界,必须满足BlockedStateInvariant(每个生命周期动词都必须让受影响行的阻塞列在提交前保持正确); - 历史记录:
RecordEventInTx(ctx, tx, EventUpdate, ...)将这次变更作为一次update记入版本控制历史(reopen 本质是状态变更,因此归类为 update)。
Holder 保留:契约测试RunLifecycleCloseAndReopenKeepTheClaimHolder(backend/conformance/lifecycle_close_reopen_contract.go)钉死了一个容易被忽略的行为——reopen 清除的是关闭记录(close_reason、closed_by_session、closed_at),而assignee 不被清除。也就是说,代理 A 持有并关闭的 issue 被 reopen 后,仍然归属 A,工作可以交还给同一 holder。
自定义状态类别(configured done category)
Beads 支持通过status.custom配置自定义状态词汇,格式如name:category。契约测试使用的示例值是"lcrtriage:active,lcrarchived:done"(backend/conformance/lifecycle_close_reopen_contract.go),其中lcrarchived被归类为done。
这对bd reopen的影响(RunLifecycleCloseAndReopenSpanTheConfiguredDoneCategory,同文件 [L806-L845]):reopen 处理的是配置的 done 类别而非字面量 closed。一个处于自定义 done 状态的 issue 用bd reopen会真实迁移到open,Changed = true——若把它当作"已最终完成"而跳过,会让行停留在没有任何内建查询匹配的状态上。
并发安全:ExpectedVersion 与检查顺序
ReopenRequest支持ExpectedVersion(乐观并发前置条件,issueops/issueops.go),规则如下:
- 令牌不透明、仅做相等比较,每次生命周期写入都会重铸(非递增),因此无法回答"新旧"问题;
- 它守卫的是行的生命周期状态(status、assignee、started_at 写入会重铸),而非图的边;
nil表示禁用检查;不匹配时返回ErrVersionMismatch且不写入任何内容。
关键检查顺序(契约测试RunLifecycleExpectedVersionIsCheckedBeforeTheNoOps,backend/conformance/lifecycle_close_reopen_contract.go):
- 对本会是 no-op的 reopen(目标非 done),
ExpectedVersion依然先于no-op 判定被检查——即携带过期令牌的重开请求会收到ErrVersionMismatch而非"nothing to do"的成功响应,避免丢失更新前置条件被静默跳过; - 但行解析先于版本检查:不存在的 ID 永远返回
ErrNotFound而不是ErrVersionMismatch——两者的含义不同("重新读取并重试" vs "这个 ID 是错的"),契约同时断言了ErrNotFound不会同时匹配ErrVersionMismatch。
HTTP API 面:bd serve下的 reopen
当bd以服务器模式运行(bd serve)时,reopen 通过 HTTP API 暴露,对应处理器handleReopen(internal/httpapi/reopen.go):
- 请求体成员白名单为
actor、reason、expected_version,未知成员会被拒绝; - 响应包含
Issue(操作后快照)、AlreadyOpen(!Changed的线上名称,幂等语义同 re-claim、re-close)与Revision(行重开后的并发令牌,供"reopen 后再 close"的恢复流程组合下一次expected_version); - 失败路径中
ErrVersionMismatch映射为版本前置条件 4xx 响应; - 其历史记录 provenance 固定为
"bd serve: reopen issue"(同文件 [L27]),保证无论后端是 store-backed 还是 unit-of-work,历史条目的拼写一致。
常见使用场景与注意事项
回归工作流:代理验证发现已关闭的 issue 出现回归 →bd reopen bd-42 --reason "Regression found"→ issue 回到 open 且关闭痕迹被清除 → 可再次 claim 处理。此时bd reopen比bd update --status open更合适,因为后者不会清理closed_at——一个开着却带着完成时间戳的行会污染周期时间(cycle-time)与燃尽图统计,契约测试明确指出了这一点(backend/conformance/lifecycle_close_reopen_contract.go)。
批量恢复:误关闭多个 issue 时bd reopen bd-42 bd-43 bd-44一次恢复;单个解析失败不影响其他项继续执行,最终以非零退出码标记部分失败(SilentExit)。
约束提醒:
- 只对
closed及自定义 done 状态生效;open/wip 等状态是安全的 no-op; - 命令是写操作,在只读模式下被拒;
- reason 的归宿是事件历史与评论,不在 issue 字段上;如需取回,读取 issue 的事件流;
- reopen 会清理 claim 租赁(lease),但它不改变 assignee——holder 保持原状。
验证与测试覆盖
如果你想深入验证上述行为,仓库提供了三层测试:
- 契约层:backend/conformance/lifecycle_close_reopen_contract.go —— 在全部后端(server-backed、embedded、unit-of-work)上运行,覆盖关闭痕迹清理、holder 保留、非 done no-op、自定义 done 类别、ExpectedVersion 顺序等语义;
- CLI 层:cmd/bd/reopen_test.go 与 cmd/bd/reopen_embedded_test.go —— 验证 reopen 后状态为 open、
ClosedAt为 nil、原因正确落库; - 存储层:internal/storage/issueops/reopen_test.go —— 针对 SQL 分支(含自定义状态)的单元测试。
这些测试文件既是行为规范,也是你改造或扩展 reopen 行为时必须保持绿灯的基线。
【免费下载链接】beadsBeads - A memory upgrade for your coding agent项目地址: https://gitcode.com/GitHub_Trending/beads1/beads
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考