Documentation
¶
Overview ¶
本文件枚举本机可广告的单播地址,供 init 配对片段的 addr 使用。
职责:
- 从网卡地址里筛出可给对端抄的 IPv4(排除 loopback / link-local)
- 把 listen 上的通配地址换成排序后的第一条,拼进配对 addr
边界:
- **不写 listen、不改配置**:探到的 IP 只出现在配对片段。绑到某一张 网卡会让 127.0.0.1 连不上,DHCP / Tailscale 一变 agentd 也起不来。
- 本期只要 IPv4;IPv6 留给后续(本仓库远程场景是 Tailscale CGNAT)
本文件实现 handoff agentd 子命令:加载配置、初始化统一日志、打开 SQLite 存储、 构建 HTTP/WS 服务并监听。agentd 是本机/配对主机上的长驻服务,是任务的执行入口。
职责:
- 按序完成 bootstrap:config.Load → logx.Setup + slog.SetDefault → pathenv.Apply(PATH 补全,先于一切 fork 子进程)→ store.Open → agentd.NewServer
- 对外服务前做启动恢复(RecoverOnStartup):探活未终结任务的执行器,重建订阅或转 failed
- 启动任务卡住看门狗 goroutine(RunWatchdog),长时间无事件产出触发 stalled 唤醒协调者
- 监听配置中的 Listen 地址,进程生命周期与 HTTP server 一致
- 经 agentd.Shutdown 提供优雅关停:SIGINT/SIGTERM 停收新连接 → 等在途请求 → 停看门狗 → 关库 → 放锁;正常关停 exit 0,供进程管理器据此拉起新版
边界:
- 不创建任务/工单:任务生命周期由 manager 驱动(executor 按 --executor 挂载)
- 不决定何时停机:信号与进程内触发都汇到 agentd.Shutdown,本文件只接线
本文件实现 handoff attach 子命令:在终端跟随任务实况。
职责:
- 从 agentd 的 render 流式接口取任务实况,原样打印到 stdout,Ctrl+C 退出
边界:
- 不解析实况内容:render.log 是模型回合文本原样增量,这里只做搬运
- 不连 executor、不碰任务目录:一切经 agentd 的 HTTP 接口
为什么不再 exec 外部命令:旧实现用 syscall.Exec 换进程进 tmux(本机), 或 ssh -t <host> tmux attach(远程)。tmux 拆除后实况改由 agentd 落盘 + 流式吐出,attach 退化成一个普通 HTTP 客户端——顺带拿到三个收益: 远程不再需要 ssh(复用 agentd 连接与鉴权,配置里的 user 字段对 attach 不再必要)、 Windows 协调者可用(syscall.Exec 在 Windows 上直接返回 EWINDOWS)、 断线可凭已收字节数续传。
本文件实现 handoff continue 子命令:向任务续发修改指令。
职责:
- 把协调者的修改指令经 client.Continue 原样透传给 executor(同一会话续接, 上下文完整保留;任务必须处于 waiting_review)
- 成功时单行输出 {"ok":true}(供上层脚本解析)
边界:
- 不解释指令语义,原文透传;任务状态校验由 agentd 判定并返回错误
本文件实现 handoff diff 子命令:取任务分支相对基准分支的审阅素材(git diff + 提交列表)。
职责:
- 调 client.Diff 拉取 diff 文本并原文输出到 stdout(协调者阅读/管道分析用)
边界:
- 不做 diff 语义判断;基准分支可经 --base 指定,缺省由 agentd 按仓库默认分支推导
本文件实现 handoff dispatch 子命令:把本地 plan 文件(或 --prompt 直接指令) 派发到 agentd 执行。
职责:
- 读取本地 plan 文件并 base64 编码,连同项目身份/计划名/target/执行者/模型/ 分支/worktree 等参数一并 POST 给 agentd(body {project_id, plan_b64, prompt, ...})
- 派发的项目由 cwd 识别:读当前目录 git 仓库的 origin 离线算出 project_id, cwd 不是目标项目时用 --project <名字> 显式指定
- 远程派发时采集本地 HEAD 作基线随请求上送,并校验本地工作区完整性 (已跟踪改动拒发、未跟踪警告;--no-sync-check 关掉整块,--allow-dirty 只关拒发)
- 派发成功后在 stderr 打一行基线摘要(起点短号 + 任务仓库领先的提交数)
- 派发成功后在 stderr 提示执行机仓库的未提交改动(managed 工作树不含它们)
- 成功时单行输出任务 JSON(state=running,供上层脚本解析任务 id)
边界:
- 只做文件读取与上传,不校验计划内容语义(解析与执行由 executor 负责)
- --no-terminal 在本文件只注册 flag 并参与「是否弹终端」的判定骨架; 弹终端默认**不弹**(cfg.Terminal.Auto 默认 false),配置 auto: true 时 才在 darwin 弹窗,--no-terminal 用于逐次关闭
本文件实现 dispatch 的本地工作区完整性校验(backlog B29)。
职责:
- 把 git status --porcelain 的输出分成「已跟踪改动」与「未跟踪文件」两类
- 已跟踪改动拒发(--allow-dirty 可放行),未跟踪只警告
- 全部提示走调用方给的 stderr writer
边界:
- 只看当前工作目录(cwd)这一棵树;agentd 侧任务仓库的脏检查是另一回事, 由 internal/agentd 的 ensureCleanWorktree 负责,两者互不替代
- 不发起任何网络请求:拒发必须发生在 HTTP 请求之前
- 不解释 git 的退出码:status 本身失败时降级放行,不把派发挡死
本文件实现 handoff done 子命令:归档任务。
职责:
- 审核通过后调用 client.Done 把任务置为 completed 并回收 executor(任务必须 处于 waiting_review)
- 成功时单行输出 {"ok":true}(供上层脚本解析)
- 携带可选完成说明(--note)并在 stderr 提示保存结果
边界:
- 不做 push 等归档后动作(按任务配置决定是否 push 不在 MVP 范围)
- 不做说明内容的校验与加工(只校验长度)
本文件定义 CLI 的退出码语义:把「失败的类别」编码进进程退出码。
职责:
- 提供 exitCodeError 包装,让特定失败带上专属退出码
- 提供 ExitCode,供 main 把 Execute 返回的错误换算成退出码
边界:
- 不打印任何东西(错误文本由 cobra 打到 stderr)
- 不决定「什么算失败」,只决定「这次失败对外表达成几号」
本文件实现 handoff fetch 子命令:读取任务仓库内文件内容(审核取上下文用)。
职责:
- 调 client.Fetch 拉取仓库内相对路径文件并原文输出到 stdout
边界:
- 不修改文件;路径由协调者指定,逃逸路径由 agentd 拒绝
footprint.go —— `handoff footprint` 命令:体检全部任务的进程足迹。
职责:
- 拉取对端全部任务(含已归档)的进程占用与判定结论并渲染
边界:
- **只数不杀**:本命令不回收任何进程。清扫由 agentd 在 executor 判死时 自动完成(见 spec §3.4),本命令只负责让人看见
- 不改任何任务状态、不发事件
本文件实现 handoff init 子命令:一台新机器的问答式配置。
职责:
- 探测四家 executor 的状态并成表打印
- 按角色分支问配置问题,把答案写进 config.yaml
- 末尾打印本机 token 与现成的配对 yaml 片段
边界:
- **不发起任何真实模型调用**:探测一律用轻量本地判据(见 internal/toolchain)
- **不主动装服务,但会问**:角色含执行机且 stdin 是终端时,init 会追问一句 是否托管,答 y 则调 installService(与 handoff service install 同一条路径)。 托管是「重启后 agentd 还回得来」的唯一保障,只留一行提示的触达率不够(B71)。 Linux 上非 root 时一律不代跑,只打印 sudo 命令
- **不阻断任何选择**:探测结果只影响默认值与标注;没装任何 executor 也能配完 (纯协调者机的正常情况),选了「未登录」的执行者只警告不拦
- stdin 非 tty 时一问不问:init 会被 install.sh 经管道调起,问了没人答, 卡住比不问糟得多
本文件是 init 在真终端上的 huh 问答实现。
职责:
- 用 huh 的 Select / Input / Confirm 实现 prompter
- 把用户取消(Ctrl-C、huh.ErrUserAborted、context 取消)译成 errPromptCanceled
边界:
- **只服务 TTY**:测试不得走这里。CI 没有真终端,huh 会挂死; 测试经 newInteractivePrompter 缝换成脚本化实现
- **取消 / 失败绝不写配置**:本文件只返回错误。写盘是 RunE 的事, 见到错就不 Save,避免留下一份只配了一半的 config.yaml
- 不负责问题集合;问什么仍由 init.go 的 askAll 决定
本文件实现 handoff permission-mcp 隐藏子命令:Claude Code 的权限裁决 MCP server。
职责:
- 以 stdio JSON-RPC 提供一个 ask 工具,claude 经 --permission-prompt-tool 调用它
- 把每次授权请求经 unix socket 转给 agentd 侧的 adapter,阻塞等待人工/审批者裁决
- 把裁决还原成 claude 认识的 {"behavior":"allow"|"deny"} 返回
边界:
- 不读 handoff 配置、不连 agentd HTTP:唯一对外面就是 --sock 指定的路径, 被监管的 executor 因此拿不到 agentd token
- 不做任何审批判断:连不上就一直重试等待,绝不自作主张放行(fail-closed)
为什么是隐藏子命令而不是独立二进制:claude 侧只需要一个可执行文件路径, 复用 handoff 自身避免了额外分发与版本漂移。
日志例外(本文件唯一允许不用 slog 的地方):stdout 是 JSON-RPC 通道,任何 非协议内容混入都会让 claude 侧解析失败,且本进程是被 claude 拉起的短命子进程, 不接 agentd 的 logger——诊断只能走 stderr 的 fmt.Fprintf。
本文件实现 handoff project 子命令族:把一个项目登记到本机与(可选的)一台 远程开发机上,并维护「项目 × 机器」的位置表。
职责:
- project add:把 cwd 登记为本机位置;--target 时一并登记到那台机器
- project ls:列出位置,并显示每条的实际状态(登记与磁盘漂移时看得见)
- project rm:注销位置
边界:
- 不自己 ssh、不自己 clone:clone 由目标机上的 agentd 执行,用它自己的 git 凭据
- 不删磁盘上的仓库:rm 只删登记
- 不决定「项目在那台机器的哪个目录」:远程落点由那台机器的 repo_root 决定, 本机一个远程细节都不需要知道(spec §6.2)
本文件是 init 问答的通道:接口 + 按行读答案的脚本化实现。
职责:
- 定义 prompter(Select / Input / Confirm)
- 提供 scriptedPrompter:从 Reader 按行读,空行 / EOF 取默认 (测试与 CI 用;真终端走 init_huh.go)
边界:
- **不写配置**:只返回用户(或脚本)的答案,不碰 config.yaml
- **不探测工具链**:选项列表由调用方传入,这里不调 toolchain.Detect
- 不负责问题集合;问什么仍由 init.go 的 askAll 决定
本文件实现 handoff pull 子命令:把远程执行机上的任务分支同步到本地仓库。
职责:
- 查任务拿到 target/仓库路径/分支,换算出 ssh 形式的远程地址并 fetch 到本地同名分支
边界:
- 只 fetch,不 checkout、不合并(合并是协调者的决定)
- 本机任务(无 target)无需同步:代码本来就在同一台机器上
reclaim.go —— handoff reclaim 子命令:回收终态任务残留的 managed worktree。
职责:
- 无参:列出仍占着 managed worktree 的终态任务(净/脏/元数据残留/判不出)
- 带任务 id:回收那一个;脏树默认拒绝并报出改动清单,--force 才强删
边界:
- 不删任务分支(协调者的工作成果),每次成功输出都明说这一点
- 不删任务目录(失败任务的排查素材还在里面)
- 不改任务状态:回收前后 handoff show 看到的状态一致
本文件实现 handoff reply 子命令:回答一个待办工单(权限门/提问)。
职责:
- 把协调者的裁决转成应答原文:--approve → "allow"、--deny [--reason] → "deny[:原因]"、 --answer 原样透传,经 client.Reply 交给 agentd
- 成功时单行输出 {"ok":true}(供上层脚本解析)
边界:
- 不解释应答语义:answer 原样落库,含义由 manager 侧解释(allow → once / 其余 → reject)
- 不校验任务状态(工单是否存在、已回答等由 agentd 判定并返回错误)
本文件实现 handoff resume 子命令:解开卡死的任务。
职责:
- 调用 client.Resume 让 agentd 重投「已落库但未送达 executor」的应答, 并对断连窗口内丢失的回合终态做会话对账(B38)
- 原样输出恢复报告 JSON(重投条数 / 对账结果 / executor 是否已不在 / 收尾状态 / 结论)
边界:
- 不自己判断任务是否卡死,也不改任何状态:判定与收尾全在 agentd 侧 (Manager.RecoverStuck),CLI 只负责发起与呈现
- 与 continue/done 的分工:那两条要求任务已在待审核;本条专治两类中间态 ——「reply 拿到 502 之后 reply/continue/done 三条路全封死」,以及 「agentd 与 executor 断连期间回合已完结、终态事件丢失、任务冻死在 running」
Package cmd 提供 handoff 的 cobra 命令行入口。
职责:
- 定义根命令与全局 flag(--agentd / --target / --config)
- 提供 TargetEndpoint 辅助函数,供各子命令换算实际 agentd 端点
边界:
- 不包含具体业务逻辑(dispatch/gate 等子命令由后续任务补充)
- 不在此处初始化日志,由各子命令按需调用 logx.Setup
本文件实现 handoff run 子命令:在任务仓库远程执行审阅命令(跑测试/lint)。
职责:
- 把命令原文透传给 agentd 执行(sh -c,10min 超时),合并输出原文打印; 非零退出码以错误返回(cobra 打印到 stderr),输出已先行打印
边界:
- 只透传命令,不解释输出语义;命令执行于任务仓库,由 agentd 限时回收
本文件实现 handoff service 子命令:把本机 agentd 交给进程管理器托管。
职责:
- install:解析当前二进制与配置路径,生成并安装服务单元,复核起来了
- uninstall:停止并移除单元
- status:报告托管状态
边界:
- 不启动/停止 agentd 进程本身:那是管理器的事,本命令只管单元
- 不改 handoff 的配置文件:托管与配置是两件事,配置走 handoff init
- 托管之后 agentd 的形态会变:手动 Ctrl-C 会被管理器拉回,停服务要用 systemctl stop / launchctl bootout。install 成功时会把这句打给用户
本文件实现隐藏子命令 handoff _shim:执行者进程的承载壳。
职责:
- 解析 --spec,把控制权交给 prochost.RunShim(阻塞到执行者退出)
边界:
- 不做任何业务判断:全部逻辑在 prochost.RunShim 里,本文件只是 cobra 包装
- 不面向用户:Hidden=true,不出现在 help 里。它由 agentd 自己拉起, 人手动跑没有意义(缺 spec.json 就什么都做不了)
本文件实现 handoff show 子命令:输出任务的完整现场快照。
职责:
- 调用 client.Attach 拉取任务 + 待办工单 + 最近事件,单行输出完整 AttachInfo JSON—— pending_tickets 是协调者恢复现场(「我还没答哪些」)的关键数据源
边界:
- 只读快照,不修改任何状态
- 二期起快照命令从一期 attach 更名而来:attach 改为终端实况(见 attach.go), 本命令是协调者会话恢复的关键数据源,供 wait/tasks/show 之外的脚本解析
本文件实现 handoff skill:报告与同步内嵌 skill 的安装状态。
职责:
- handoff skill:逐落点报告是否与当前二进制一致
- handoff skill install:把内嵌内容装到本机各家 agent
边界:
- 不含安装逻辑本身(在 internal/skill):本层只做参数、打印与退出码
- 不装到远端:skill 服务于协调者,协调者在本机
本文件实现 handoff status 子命令:一条命令回答「这个 agentd 能不能用、是什么」。
职责:
- 调 client.Status 取服务端聚合结果,渲染人读文本(默认)或 JSON(--json)
- 把老 agentd 的 404 直译成一条**成功的**诊断结论
- 退出码只回答「能不能用」:0=可达且鉴权通过,1=够不着
边界:
- 不做探活:判据在各 adapter 里,服务端已经做完,本层只渲染
- 不因两边版本不一致而阻断:handoff 没有兼容矩阵,revision 不同不等于 不兼容,并列报出交给人判
本文件实现 handoff stop 子命令:主动中止一个还在跑的任务。
职责:
- 调用 agentd 的 stop 路由,停 executor、作废挂起工单、任务落 failed
- 依据响应体 worktree_removed 打印与实际行为一致的提示:managed worktree (agentd 建的)已删则如实告知,用户自带 worktree / 原地模式则说明保留
边界:
- 不删任务分支(那是协调者的工作成果,审阅/回滚仍可切回分支)
- 不做「停完再重派」:重派是独立决定,由协调者显式 dispatch
本文件实现 handoff tasks 子命令:列出全部任务。
职责:
- 调用 client.ListTasks 拉取任务列表,每行输出一个任务 JSON(供上层脚本逐行解析)
边界:
- 只做列表展示,不做任何状态判断与筛选
本文件实现 handoff upgrade:一条命令巡检并升级本机与全部 target。
职责:
- 不带参数(或 --check):巡检表——列出所有机器的版本与结论
- --now:升级所有落后的机器(远端全部处理完,本机最后);--target 只升那一台
- --force:越过闸一(活跃任务)。**永不越过闸二(非托管)**
- --rollback:本机回滚(不接 --target,回滚是单机应急动作)
数据流(spec §4.2):本机下载各机平台的资产并校验 → POST /api/update 把 tar.gz 原文推给远端(执行机无需出网)→ agentd 复检两道闸、再校验、解包、 自检、原子换版 → 触发优雅关停由进程管理器拉起新版 → 本 CLI 轮询 status 确认新版本上线。
边界:
- 会通过接口触发 agentd 重启(本机最后:它会重启操作者正用着的 agentd)
- 部分失败不中断其余:机器之间没有事务关系,逐行报告,任一台失败退出码非零
- 处置建议必须对症:非托管不给 --force(它不越过闸二),够不着只报原文不编处置
本文件是 handoff upgrade 的**唯一判据来源**(B64)。
职责:
- 把一台机器的探测结果收敛成单一结论(verdict)
- 结论之间的优先级在此定义一次,供只读巡检与 --now 两个消费方共用
边界:
- 纯函数:不做 I/O、不打日志、不产出面向操作者的文案
- 不判 busy:活跃任务是「要不要现在换」的闸,不是「这台机器是什么状态」的 结论;它只在 verdictNeedsUpgrade 之后由 process 施加(spec §4.3)
为什么必须只有一处:B64 的病根是 renderCheckRow 与 process 各维护一套分支表, 两套的分支集合与优先级不一致,于是同一台机器有两套说法。
本文件实现 handoff version 子命令:打印本二进制的版本标识。
职责:
- 首行输出纯版本字符串,供机器精确比对
- 其后输出 revision / Go 版本 / 平台三行,供人排障
边界:
- 不联网、不读配置文件:这条命令只回答「我是谁」。它必须能在一台刚装完、 还没有 ~/.handoff/config.yaml 的机器上跑通
- **首行格式是对外契约**:B54.3 的自更新自检会拉起新下载的二进制跑本命令, 把首行与期望 tag 精确比对(见 spec §4.6 步骤 ⑤)。改这一行的格式等于改 协议,必须同步改自检侧
本文件实现 handoff wait 子命令:阻塞等待任务的下一个可动作事件并输出单行 JSON。
职责:
- 调用 client.WaitEvent(progress 不唤醒、断线自动退避重连、cursor 续拉), 事件到达时把完整事件 JSON 单行输出到 stdout(供上层脚本解析)
- --notify:事件到达时发 macOS 系统通知(spec §7 风险#4 的兜底:协调者会话 不在时提醒其重新拉起),失败仅 Warn 不影响主流程
- 收到 SIGINT(Ctrl+C)时由进程默认行为终止,WaitEvent 随 ctx 取消退出
- 任务结束事件到达时自动同步远程任务分支到本地(输出走 stderr,不污染 stdout 的事件 JSON 契约)
- --follow:持续订阅同一任务的事件流,每条事件单行输出,直到任务终结 (failed 事件或被 done 归档)。此模式下 --timeout 的语义是**空闲**上限 ——距上一次收到任何帧(含被过滤掉的 progress)的时长,且跨重连累计
- --follow 每次建连前先对账:本机 cursor 之后有积压时吐**一行** backlog_summary (带 missed/stale/actionable),把 cursor 推到当前水位,积压事件不再逐条重放 ——stdout 每行是一次会话唤醒,逐条重放会把一次重连变成 N 次唤醒
边界:
- 不做事件语义判断与审批(审批在协调者脑中),事件原样输出
- 不覆盖「协调者会话被关闭」:Monitor 是会话级的,会话没了订阅就没了, 本命令给不出任何补救(spec §7.2 明确接受的边界)
Index ¶
Constants ¶
const ( ExitFailure = 1 ExitTimeout = 124 )
退出码约定。
为什么不全用 1:wait 的无人值守场景(cron/脚本挂在后台等唤醒)拿不到 stderr, 只能看退出码。全是 1 的话,「等满了时限」与「token 没同步导致鉴权失败」这两件 处置完全不同的事,脚本无从区分——前者该继续等,后者该立刻报警。 124 沿用 timeout(1) 的惯例,也与 handoff run 里被杀命令的退出码一致。
Variables ¶
This section is empty.
Functions ¶
func ExecuteContext ¶
ExecuteContext 是带 ctx 的 Execute(同样先清理单次执行的残留状态)。
参数:
- ctx: 传给命令 RunE 的上下文(取消即中断长驻命令,如 wait)
func ExitCode ¶
ExitCode 把 Execute 返回的错误换算成进程退出码。
参数:
- err: Execute 的返回值(nil 表示成功)
返回:
- 0(成功)、错误自带的专属退出码,或通用失败码 ExitFailure
func LocalEndpoint ¶
LocalEndpoint 返回**本机** agentd 的地址与令牌,忽略 --target。
返回:
- addr: 本机 agentd 完整地址(含 http:// 前缀)
- token: 本机令牌
- err: 配置加载失败或本机 token 为空时返回
为什么需要它而不是复用 TargetEndpoint:登记是**两跳**(本机 + 目标机, spec §6.1),而 TargetEndpoint 读的是包级 targetName,指定了 --target 时 拿不到本机端点。两跳都要发,就必须有一个不受 --target 影响的取端点入口。
func TargetEndpoint ¶
TargetEndpoint 根据 --target / --agentd / --config 换算实际请求的 agentd 端点与令牌。
参数(读取全局 flag):
- --target 为空(本机模式):token 一律取本地配置 cfg.Token(服务端无条件要求 Bearer,无 token 的本机调用必然 401);地址由 localDialAddr 决议(loopback 照拨,通配/单网卡改拨 127.0.0.1,B85);显式 --agentd 优先;cfg.Token 为空时 返回错误
- --target 非空:从配置 Targets 中查出 addr/token(远程配对)
返回:
- addr: agentd 完整地址(含 http:// 前缀)
- token: 访问令牌
- err: 配置加载失败、target 未定义或本机 token 为空时返回
Types ¶
Source Files
¶
- advertise.go
- agentd.go
- attach.go
- continue.go
- diff.go
- dispatch.go
- dispatch_dirty.go
- done.go
- exit.go
- fetch.go
- footprint.go
- init.go
- init_huh.go
- permission_mcp.go
- project.go
- prompter.go
- pull.go
- reclaim.go
- reply.go
- resume.go
- root.go
- run.go
- service.go
- shim.go
- show.go
- skill.go
- status.go
- stop.go
- tasks.go
- upgrade.go
- upgrade_verdict.go
- version.go
- wait.go