Local Run Provider
为 wujie run 接入新的本地 CLI provider 的工程规范。
Local Run Provider
Local run 是 wujie run <issue> -- <command...> 背后的本地执行能力。它把一个本地 CLI 会话绑定到 Wujie issue,负责创建 local run、启动本地 agent CLI、同步执行日志、同步用户输入和最终回复评论。
需要先讨论、后建 Issue 时,使用:
wujie discuss -- codex
# 短别名
wujie ds -- codex讨论开始时没有 Issue,也没有 local run。用户与 provider 完成方案讨论后,可以输入 /wujiecreateissue,或使用 Codex、Claude、Cursor、Kiro、AGY 原生 Plan 模式的“在当前上下文实现 / 清空上下文实现”动作。
在交互终端中,只有当前编辑行从行首完整达到 /wujie 时,Wujie 才会显示内联命令候选;空输入、仅输入 / 或更短前缀都不显示。普通字符、CSI-u 字符、退格和 bracketed paste 结束都会重新检查精确触发值,因此从 /wujiecr 连续退格回 /wujie、关闭后清空重输或任意分块输入都能再次打开菜单。粘贴期间不会提前暂停 provider;Wujie 先把完整的 ESC[201~ 结束帧写入 PTY,再等待回显并打开菜单。OSC、DCS、APC、PM、SOS 控制字符串支持 BEL 或 ST 终止,CPR、DA、DSR、模式报告和窗口尺寸等 CSI 终端回复也会被完整识别;这些回复始终原样转发给 provider,但不会进入当前用户输入状态,也不会关闭已经打开的菜单。因此完成一次正常问答后,第一次输入 /wujie 就能稳定触发。菜单打开时仍会忽略终端焦点切换和 SGR/X10 鼠标事件,不关闭菜单、不改变输入状态,也不把这些事件延迟转发给 provider。
触发时,Wujie 会先把输入写入 provider PTY;检测到新的可见输出后再等待 10ms 静默,最多等待 100ms,然后才暂停 provider 并绘制菜单。因此终端会先完整回显 /wujie;provider 没有输出或持续输出到上限时也会继续,不会卡住菜单。菜单按注册顺序展示全部候选,当前项保留 > 标记并使用 ANSI 亮蓝色文字(94,以 39 复位前景色),不使用反色或背景色;↑/↓ 循环选择,Enter 执行,Esc 关闭并保留已输入内容。Tab 只补全、不执行:Wujie 会先用 Ctrl+U 清空 provider 当前编辑行,再写回完整、规范化的 /wujiecreateissue,因此结果不依赖 provider 在暂停前消费了多少输入。非交互终端或无法暂停 provider 时不会显示菜单,但仍可手动输入完整命令或使用 /wujie… + Tab 补全。
候选菜单和 Issue 确认卡共用输入框下方的专用插入区。打开时保存原光标,先向下越过两行 provider 自有区域,再在新的锚点插入所需行;这两行只是 top margin,不属于 Wujie 区域,也不会额外插入或在关闭时删除。菜单预留“候选数 + 帮助行”,确认卡从终端可用高度中扣除原光标行和这两行 margin 后使用剩余区域。重绘和异常清理使用同一锚点;关闭、完成、取消或失败时只删除插入行并恢复打开前的光标行列,不覆盖 provider 历史,也不进入 alternate screen。确认卡先滚动检查完整方案,再选择项目、负责人、标签等 Issue 字段,最后确认创建。普通 /wujiecreateissue 可以选择继续当前 CLI、交给 Agent/Squad 或只创建 Issue;provider 原生动作会锁定原生执行方式,不再重复询问。
这份文档面向 Wujie 代码贡献者。它不是 CLI 使用教程,而是未来接入 Copilot、Hermes、OpenCode 等本地 CLI 时必须遵守的工程规范。
架构边界
Local run 分成两层:
| 层级 | 负责什么 | 不应该负责什么 |
|---|---|---|
| 通用生命周期 | 解析 issue、创建 local run、heartbeat、注入环境变量、最终状态上报、通用 PTY runner、消息 reporter | 任何 provider 特有协议、session 文件规则、日志解析 |
| Provider | 启动对应 CLI、接入该 CLI 的事件源、把事件映射成 local run message、处理该 CLI 的 session/resume 特性 | 创建 local run、更新最终状态、改前端展示类型 |
通用入口在 server/cmd/wujie/cmd_run.go。Provider 注册和基础接口在 server/cmd/wujie/cmd_run_provider.go。外部插件 provider 的 manifest、发现和执行逻辑在 server/cmd/wujie/cmd_run_plugin.go。新增 provider 时,不要把 CLI 特有逻辑写回 cmd_run.go。
前置讨论仍遵守这个边界:父 Wujie CLI 持有 discussion、Issue 和 local run 生命周期;provider 子进程只收到稳定 session ID 和 workspace 运行环境。选择继续当前上下文时,父进程只切换 reporter、usage 和 heartbeat 的绑定目标,不修改已运行子进程的环境,也不注入 Issue 描述或要求重新读取 Issue。可选 v2 handoff 只把 issue_id / issue_key / run_id 作为绑定结果返回给 adapter,不传上下文正文。
Issue 前置讨论
wujie discuss 的生命周期是:
- 创建或按
provider_session_id恢复一个未完成 discussion。 - 以原生交互方式运行 provider;
WUJIE_LOCALRUN_SESSION_ID使用稳定的 discussion ID。 - 服务端只保存用户可见的逐轮问答与最终方案;thinking、tool use、tool result、状态事件不进入讨论记录。
/wujiecreateissue或 provider 原生 implement-plan 动作触发 Wujie 统一维护的内联终端卡片。卡片先越过两行 provider 自有区域,再按扣除这两行后的终端可用高度预留插入区;首屏可滚动查看完整方案,之后编辑执行方式、标题、项目、负责人和标签;状态、优先级、起止日期、父 Issue 放在“更多设置”中。项目、负责人和标签支持过滤,CLI flags 和最近项目只作为可编辑初值。窄终端使用纵向、自适应布局,不依赖固定列宽;Shift+Tab返回。普通/wujiecreateissue可选择“继续讨论”,按Ctrl+C也会返回讨论;provider 原生动作不提供“继续讨论”,按Ctrl+C会结束当前 provider,但保留未完成 discussion,之后可从原生 session 恢复。- 服务端使用已保存的方案消息创建 Issue,保证 Issue description 与确认时的 Markdown 一致,并把 discussion 作为来源关联到 Issue。
- 选择“当前上下文”时,父进程创建 local run、切换 reporter 目标,然后让原会话直接执行;选择 Agent/Squad 时走既有 Issue 分配执行;选择“只创建”时结束本地 provider。
表单创建失败时会保留全部输入。普通 /wujiecreateissue 提供重试、返回修改和继续讨论;provider 原生动作只提供重试和返回修改,加载失败页只提供重试。两种路径都可按 Ctrl+C 取消:普通入口返回讨论,原生入口结束 provider 并以成功状态保存 discussion。成功、取消、重试和错误退出都会幂等删除插入区并恢复原光标。Issue 创建成功后,父 CLI 会等 provider 恢复、放行原生 handoff,并使用独立于命令菜单回显的稳定窗口等待重绘输出结束,再输出 【标题】Issue MYT-22 创建成功 和 Issue 链接;链接使用与候选菜单一致的 ANSI 亮蓝色 94。继续运行时,父 CLI 会临时为 provider PTY 扣除底部两行,并从提示首次真正显示开始提供至少 20 秒的保护窗口;窗口内 provider 继续输出或清屏后会在输出稳定时补画提示。窗口到期只会释放底部两行并恢复完整 PTY 高度,不主动擦除当前提示;之后提示若被 provider 覆盖则不再恢复。只创建或交给其他执行方时,则等 provider 退出并完成 PTY 输出后再提示。该提示只写本地终端,不保存到 discussion,也不写入 provider PTY;提示本身不会触发强制重绘。非交互终端会自动使用可访问的逐项输入模式,保持原有输出方式;API、表单或 hook 的真实错误仍按失败返回。
Web 和 Desktop 的 Issue 详情页会在来源频道旁显示讨论来源入口。讨论详情只展示已保存的可见问答,不展示推理或工具事件。
未完成的讨论不会因 CLI 退出而丢失。只要 provider 支持恢复并返回同一 session ID,再次执行相应的 wujie discuss -- <provider resume ...> 就会恢复同一个 discussion;CLI 同时会恢复服务端最后保存的方案,因此可以直接继续确认。
Provider 合约
每个 provider 实现 localRunProvider:
type localRunProvider interface {
Name() string
Run(args []string, cwd string, env localCLIEnv, reporter *localRunReporter, usageReporter *localRunUsageReporter) (int, error)
}注册 provider 时,把实现加入 localRunProviders:
var localRunProviders = []localRunProvider{
codexLocalRunProvider{},
claudeLocalRunProvider{},
newCLIProvider{},
}Provider 的 Run 方法必须遵守这些规则:
args[0]是用户传入的 CLI 可执行文件路径;不要改写为固定命令名。cwd是 local run 的工作目录;子进程、hook server、transcript resolver 都必须以它为准。env必须通过localCLIProcessEnv注入到子进程,避免泄漏父进程的真实 workspace token。- 前置讨论期间只依赖
WUJIE_LOCALRUN_SESSION_ID;不要假定WUJIE_RUN_ID或WUJIE_ISSUE_ID已存在。 reporter是唯一允许写 local run message 的出口。Provider 不直接调用 HTTP API。- Provider 可以使用
runProviderPTY复用通用 PTY 行为;如果 CLI 需要特殊代理、hook 或 websocket,可以自行管理子进程,但仍要复用环境变量和 reporter 约定。
外部插件形态
外部 provider 插件由 manifest 和本地 adapter 可执行文件组成:
如果你要从零开发 external provider 插件,先读 LocalRun 插件开发;本节只记录 Wujie 代码库侧的工程边界。
~/.wujie/localrun-providers/
copilot/
provider.json
wujie-localrun-copilotprovider.json 示例:
{
"schema_version": 1,
"name": "copilot",
"display_name": "GitHub Copilot",
"command": "./wujie-localrun-copilot",
"protocol": "wujie-localrun-provider.v2",
"capabilities": {
"interactive_pty": true,
"structured_events": true,
"issue_context": "env",
"usage": false,
"native_plan_handoff": false
}
}用户可以通过 wujie local-run provider install ./copilot-provider 安装,也可以在本地配置中手动引用开发中的 manifest:
{
"localrun_providers": {
"hermes": {
"manifest_path": "/Users/me/dev/hermes-wujie-provider/provider.json",
"enabled": true
},
"grok": {
"manifest_path": "/Users/me/dev/wujie/localrun-providers/grok/provider.json",
"enabled": true
}
}
}仓库内置样板插件:
- Hermes:
localrun-providers/hermes/ - Grok Build:
localrun-providers/grok/
安装示例:
wujie local-run provider install ./localrun-providers/grok
wujie local-run provider list
wujie run OPE-xxxx -- grok运行方式 provider 查找顺序是:
- 内置 provider,例如
codex、claude、agy、cursor、kiro-cli - 当前 profile/config 的
localrun_providers - 当前 profile/config state dir 下的
localrun-providers/*/provider.json
外部插件不能覆盖内置 provider 名称;发现重名时直接报错。
插件 adapter 把标准事件以 NDJSON 写到 stdout。Wujie 主 CLI 读取事件并通过 reporter 写入 local run message。普通 Issue-first 执行模式下,插件进程会收到 WUJIE_RUN_ID、WUJIE_ISSUE_ID、WUJIE_WORKSPACE_ID、WUJIE_SERVER_URL、WUJIE_TOKEN,以及 WUJIE_LOCALRUN_PROVIDER_* 元数据。
协议版本直接决定前置讨论能力:
wujie-localrun-provider.v1始终保持 Issue-first 合约,不能用于wujie discuss。wujie-localrun-provider.v2必须支持前置讨论;Issue 创建前只提供稳定的WUJIE_LOCALRUN_SESSION_ID,不能依赖WUJIE_RUN_ID/WUJIE_ISSUE_ID。- v2 的
native_plan_handoff是可选能力。声明后,Adapter 先发handoff_ready,Wujie 完成绑定后再回handoff_decision;控制帧只携带绑定 ID,不携带或重复注入方案正文。
执行日志语义
Local run 的执行日志必须对齐对应平台 Agent backend,而不是发明一套前端专用事件。
| Provider | 对齐对象 | 主要事件 |
|---|---|---|
| Codex | server/pkg/agent/codex.go | text、tool_use、tool_result;工具名使用 exec_command、patch_apply |
| Claude | server/pkg/agent/claude.go | text、thinking、tool_use、tool_result;工具名保留 Claude 原始名称,如 Bash、Read、Edit |
| Cursor | 内置 transcript mapper | text、tool_use、tool_result、user_input、final;从原生 session transcript 同步 |
| AGY | 内置 transcript mapper | tool_use、user_input、final;只把 PLANNER_RESPONSE 作为模型回复 |
| Kiro CLI | server/pkg/agent/kiro.go | text、tool_use、tool_result;从 ~/.kiro/sessions/cli/*.jsonl 同步 kiro-cli chat 会话 |
| Hermes | 外部插件 localrun-providers/hermes | user_input、tool_use、tool_result、final;hook 事件转 NDJSON |
| Grok Build | 外部插件 localrun-providers/grok | text、thinking、tool_use、tool_result、user_input、final;从 ~/.grok/sessions/<url-encoded-cwd>/<sessionId>/updates.jsonl 同步;usage 来自 turn_completed |
Reporter 消息字段:
reporter.Post(localCLIMessage{
Type: "tool_use",
Tool: "Bash",
Input: map[string]any{"command": "go test ./cmd/wujie"},
Source: "provider-source",
SourceKey: "stable-provider-event-id",
})通用规则:
text:agent 面向用户的文本输出。thinking:只有平台 backend 已经写入该类型的 provider 才能写,例如 Claude。tool_use:工具开始执行,Tool必须使用平台 backend 的同名工具语义。tool_result:工具执行结果,Output应匹配平台 backend 的字符串化方式。error:只有平台 backend 也会把同类错误写成 task message 时才写。否则错误应该进入 local run 最终状态的error字段。- 不新增
event、session、thread等前端专属类型。生命周期事件通常不进入执行日志。
Source 和 SourceKey 用于幂等。SourceKey 必须稳定、唯一,并来自 provider 的真实事件 ID、session ID、line UUID、tool call ID 等确定性信息。不要用当前时间、随机数或递增内存计数作为唯一依据。
评论同步
Local run 额外保留两个不属于平台执行日志的消息类型:
| 类型 | 用途 |
|---|---|
user_input | 把用户在本地 CLI 里输入的普通消息同步成 Wujie 用户评论 |
final | 把 agent 对该用户输入的最终回复同步成本地 Codex/Claude 回复评论 |
评论同步规则:
- Provider 注入的 issue context 不写
user_input,也不写final。 - Slash command 不写
user_input,也不应该让后续输出写成final。 - Tool result 不是用户输入,不能写成评论。
final只在当前 turn 有可评论的user_input时写。text/thinking/ tool 日志仍然可以进入执行日志,即使当前 turn 不写评论。
Provider 需要自己识别该 CLI 的用户输入和最终回复边界。如果 CLI 没有明确 final 事件,可以用该 CLI 的完整 assistant message 作为 final,但不能把 token delta 或状态文案当最终回复。
Session 和 Transcript 跟踪
不要通过“扫描 cwd 下最新 session 文件”来追踪本地 CLI。这个策略会被新的终端会话污染:同一个项目目录里,用户在另一个终端开启新会话时,旧 local run 可能误读新 session 文件。
Provider 必须使用可验证的会话来源:
- CLI 提供 remote/app-server/stdout stream:优先直接从协议事件读取。
- CLI 提供 hook:用 hook 上报的
session_id/transcript_path精确追踪。 - CLI 只提供 transcript 文件:必须有明确的 session ID 或启动时返回的文件路径;不能按“最新文件”猜测。
Claude 当前使用 SessionStart hook 获取准确的 JSONL transcript。恢复会话、清空上下文、新开 session 时,都以 hook 上报为准。为了兼容 CLI 继续写旧 session 文件的情况,tracker 可以同时跟踪多个 hook 上报过的 session,但不能跟踪没有被 hook 明确确认过的文件。
新 Provider Checklist
接入新的本地 CLI 前,先完成能力调研:
- CLI 是否支持交互式 TUI?
- 是否支持 stream JSON、app-server、remote proxy、hook、transcript JSONL 或其它结构化事件源?
- 是否支持启动时传入 bootstrap prompt?
- 是否支持 resume / clear / new session?这些行为会如何改变 session ID 或 transcript 文件?
- 是否能在没有 Issue/Run ID 时启动,并使用
WUJIE_LOCALRUN_SESSION_ID保持 discussion 会话稳定? - 是否支持原生 Plan 模式,并能在真正执行前暴露 current-context / clear-context 选择?
- 是否有用户输入、assistant final、tool use、tool result 的稳定 ID?
实现时按这个顺序做:
- 新增 provider 文件,例如
cmd_run_copilot.go,只放 provider 特有逻辑。 - 实现
localRunProvider,注册到localRunProviders。 - 启动 CLI 时复用
localCLIProcessEnv,并明确校验 Wujie 必须托管的 CLI flag。 - 选择结构化事件源,禁止依赖终端文本解析作为主要日志来源。
- 编写 mapper,把事件映射到平台 backend 已使用的消息语义。
- 为 bootstrap、slash command、普通 user input、final reply、tool use/result、error/session 切换写测试。
- 跑
go test ./cmd/wujie和 local run handler 相关测试。
如果是外部插件 provider,还需要:
- 提供
provider.json;只支持 Issue-first 时使用 v1,需要前置讨论时使用 v2。 - Adapter stdout 只能输出标准 NDJSON 事件;普通诊断日志写 stderr。
- 使用稳定的
source/source_key,禁止用当前时间、随机数或内存序号作为唯一依据。 - 通过
wujie local-run provider install或localrun_providers手动配置接入。
新增 provider 时,前端通常不需要改动。如果需要前端改动,先检查是否是 provider 泄漏了专属事件类型;只有平台已有通用展示能力不足时,才考虑扩展前端。
当前 Provider
Codex
Codex local run 通过 Codex app-server 和本地 websocket proxy 采集结构化事件。执行日志主语义对齐平台 Codex backend:
- command execution →
tool_use/tool_result,tool=exec_command - file change →
tool_use/tool_result,tool=patch_apply - agent message →
text
Local run 额外使用 user_input 和 final 做评论同步。为了兼容 Codex app-server 事件差异,mapper 保留少量 fallback,但用户可见主语义仍按平台 Codex backend。
Codex app-server 和本地 websocket proxy 是 TUI 进程的 sidecar。TUI 退出后,provider 必须关闭 proxy、断开 active websocket,并终止 app-server 进程组来释放端口和子进程资源。不要通过解析 slash command 来判断退出:/clear 会启动新的 thread,/resume 会切回旧 thread,它们都属于同一个 local run 会话内的 session/thread 切换,不能触发 sidecar 清理。
Claude
Claude local run 保留原生 TUI,通过临时 --settings 注入 SessionStart hook。hook 调用隐藏命令 wujie __claude-session-hook,把 Claude 上报的 session 信息转发给本地 hook server。tracker 只读取 hook 明确上报的 transcript 文件。
前置讨论会在没有显式 --permission-mode 时追加 --permission-mode plan,并临时注入 PostToolUse: ExitPlanMode hook。Claude 批准原生 Plan 动作后,hook 会把原始方案写为结构化 plan 消息并同步等待 Issue/Run 绑定;绑定成功后返回原 hook,让同一个 Claude session 继续。原生确认卡按 Ctrl+C 时,hook 阻止该动作并结束 Claude provider。这里不增加 Claude 专属 PreToolUse、上下文注入或 /clear 编排;hook 控制响应不携带 Issue 描述,Claude 不会重新读取 Issue。
执行日志语义对齐平台 Claude backend:
- text block →
text - thinking block →
thinking - tool use block →
tool_use,保留 Claude 原始工具名 - tool result block →
tool_result,通过tool_use_id回填原始工具名
这个实现避免了按项目目录猜最新 session 文件,也避免了清空上下文、恢复会话或另一个终端新开会话时串线。
Cursor、Kiro 与 AGY
这三个内置 provider 在前置讨论中同样使用各自原生 Plan 模式:
- Cursor 在用户没有显式
--mode时追加--mode=plan。 - Kiro CLI 启动
kiro-cli chat后发送原生/plan。 - AGY 在用户没有显式
--mode时追加--mode=plan。
它们从各自结构化 session/transcript 识别 provider 生成的 implement-plan 动作。父 CLI 在确认卡和 Issue/Run 绑定期间暂停原 provider 子进程,绑定后恢复同一个进程和 session;原生确认卡按 Ctrl+C 时会终止 provider,保留 discussion,并忽略终止信号产生的退出码。这个路径不解析 TUI 渲染文本,也不会把 Issue description 重新注入子进程。