--- name: commondev-discipline description: commondev 主管式多仓库开发流水线的使用与开发纪律。当你要为某个工程开一条分支、把工程派给一个下属 agent、或用 pi 拉起执行 agent 干活时必读。核心动作:为每个工程对话新建驻留式子 agent 并把该工程的一对通道划给它(否则所有工程都挤在根 agent 上)。**git 托管平台的操作(Gitea/GitHub/GitLab)不在插件里,而在本文** —— 用 enc_shell + 保存的凭据调 curl/CLI 完成。也含 hmap 插件开发与部署纪律。 version: 2.0.0 author: HomeAgent --- # commondev 纪律(主管模式 · 工程对话 · 平台操作) 本技能三部分,**都可能与你的任务直接相关**: - **第一部分:使用者纪律** —— 怎么开工程、怎么派活。 - **第二部分:★ git 平台操作** —— **本文是权威**。开 PR、审批、分支保护怎么做。 - **第三部分:开发与部署纪律** —— 怎么不踩坑。 > ## ★ 架构边界(用户拍板 2026-10-15) > > | 层 | 负责 | > |---|---| > | **插件 commondev** | 项目台账、工作目录、pi 会话与通道、判据与变异复核、冲突预检、凭据保存与安全执行 | > | **本文(skill)** | **各 git 托管平台的认证头、API 端点、PR 与分支保护流程** | > > **插件里没有平台逻辑。** 操作 Gitea/GitHub/GitLab 等平台一律: > 读本文 → 用 `enc_shell` + `@{凭据名}` 执行 `curl`/`gh`/`tea`。 > > ⇒ **新增一个托管平台只需改本文,不需要改插件、不需要重编译、不需要重启服务。** > > 原本这些在插件里(`platformSpec` / `commondev_account_*` / `commondev_pr_*` / > `commondev_protect_branch` / `commondev_repo_search`),代价是: > 每接一个平台都要改 Go 代码 → 重编译 → 重打包 → 重启, > 且**未列出的平台直接报错**(GitLab/Gitee/Bitbucket 全不支持)—— > 而 agent 用 curl 本来什么平台都能打。这 10 个工具已移除。 > 看不懂某个词时的权威文档(按顺序读): > 1. `docs/zh/commondev-design.md` —— 目标机制、台账 schema、判据与变异、安全(§12 是架构变更)。 > 2. `docs/zh/commondev-agent-runtime-design.md` —— 运行时与内核接入(续篇)。 > 3. `docs/zh/kernel-secret-firewall-design.md` —— 凭据模块(enc_* 的底座)。 > 4. `docs/zh/resident-subagent-design.md` —— 驻留子设计的原始文档。 > > **本文与文档冲突时,以文档为准,并回来修正本文。** --- ## Setup: 第二部分 git 平台操作(本文是权威,不要凭记忆写 API) ### 11. 所有平台都是同一个形状 「带认证头发一个 REST 请求」。用 `enc_shell` + `curl` 即可: ``` enc_shell(command="curl -s -H \"<认证头>\" <路径>") ``` **差异只有两个维度:API 前缀 与 认证头。** 其余是 REST 常规。 ### 12. 认证头与 API 前缀(逐平台,★ 四种形状互不相同) | 平台 | API 前缀 | 认证头 | |---|---|---| | **Gitea** | `/api/v1` | `Authorization: token ` ← **不是 Bearer** | | **GitHub** | `https://api.github.com` | `Authorization: Bearer ` | | **GitLab** | `/api/v4` | `PRIVATE-TOKEN: ` ← **不是 Authorization** | | **GitCode** | `https://api.gitcode.com/api/v5` | `Authorization: Bearer ` | | **Gitee** | `https://gitee.com/api/v5` | `access_token=`(**查询参数**,不是头) | ⚠ 头名、前缀、甚至「用头还是用查询参数」都不同。**未知平台先查官方文档,不要猜形状。** ### 13. 先保存凭据,再用它 ``` enc_store(alias="gitea-token", value="${GITEA_TOKEN}") # 值优先走 ${ENV} 或 @file: enc_shell(command="curl -s -H \"Authorization: token @{gitea-token}\" http://192.168.2.106:3000/api/v1/user") ``` ⚠ **优先用 `enc_store`,不要用 `commondev_account_add`** —— 后者已不存在 (它内建了平台语义:platform/role/base_url,那正是被移出的东西)。 **先验凭据再干活**(早失败,别推了一半才发现 token 错): ``` enc_shell(command="curl -s -o /dev/null -w '%{http_code}' -H \"Authorization: token @{gitea-token}\" http://192.168.2.106:3000/api/v1/user") # 期望 200;401/403 = 凭据无效或权限不足 ``` ### 14. 常见操作(以 Gitea 为例,其他平台换前缀与头) ```bash # 搜仓库 curl -s -H "Authorization: token @{t}" "http://host/api/v1/repos/search?q=myname&limit=20" # 看仓库 / 分支 curl -s -H "Authorization: token @{t}" "http://host/api/v1/repos/jianf/MyProj" curl -s -H "Authorization: token @{t}" "http://host/api/v1/repos/jianf/MyProj/branches" # 开 PR(head=你的分支,base=目标分支) curl -s -X POST -H "Authorization: token @{t}" -H 'Content-Type: application/json' -d '{"title":"...","head":"feat/x","base":"main"}' "http://host/api/v1/repos/jianf/MyProj/pulls" # 查 PR 状态 curl -s -H "Authorization: token @{t}" "http://host/api/v1/repos/jianf/MyProj/pulls/7" # 合 PR curl -s -X POST -H "Authorization: token @{t}" -H 'Content-Type: application/json' -d '{"Do":"merge"}' "http://host/api/v1/repos/jianf/MyProj/pulls/7/merge" ``` **push 时凭据不进 remote URL**: ```bash # ✅ 一次性注入(仅本次命令);clone 出来的 remote 保持干净 enc_shell(command="cd && git -c http.extraheader=\"Authorization: token @{t}\" push origin feat/x") ``` ⚠ 若改用 `https://token@host/...` 形式,token 会**持久化进 `.git/config`** —— 删掉临时目录也删不掉凭据(`git remote -v` 就能读出来)。设计 §7.2 明令禁止。 ### 15. Gitea 分支保护:让「必须复审」成为**机制**而非礼貌 **光有流程约定不算机制** —— 执行 agent 可以直接推主干。必须让**平台**拦。 ```bash curl -s -X POST -H "Authorization: token @{t}" -H 'Content-Type: application/json' -d '{"enable_push":false,"required_approvals":1,"block_admin_merge_override":true,"dismiss_stale_approvals":true}' "http://host/api/v1/repos/jianf/MyProj/branch_protections?name=main" ``` 四个开关的意义(缺一个机制就有洞): | 开关 | 作用 | 缺了会怎样 | |---|---|---| | `enable_push: false` | 禁止直接推送 | 执行 agent 直推主干,绕过评审 | | `required_approvals: ≥1` | 至少一个审批 | 自己就能合 | | `block_admin_merge_override: true` | admin 也不能绕过 | 用 admin token 的人绕过一切 | | `dismiss_stale_approvals: true` | 新提交作废旧审批 | 先批准再改代码,批准白拿 | **真机实测(Gitea 1.24.2)**: | 动作 | 普通用户 token | admin token | |---|---|---| | push `main` | ❌ `pre-receive hook declined` | ⚠ 可能成功(见下)| | push `feat/*` | ✅ | ✅ | | 自合 PR | ❌ `Does not have enough approvals` | ⚠ 取决于开关 | | 审批 → 合 PR | — | ✅ | ⇒ **推论:执行侧必须用普通用户 token。** 用 admin token 会**静默绕过分支保护** (看起来正常,实际机制失效且无报错)—— 这是最容易搞错的一点。 ⚠ 分支保护已存在时用 **PATCH**(POST 会 403 `already exist`)。 ### 16. 审批:三个必踩的 API 坑(实测) 1. **`event` 必须是 `APPROVED`,不是 `APPROVE`。** 传 `APPROVE` 时请求**返回 200 且不报错**,但 review 状态落成 `PENDING` —— 于是 `required_approvals` 永远不满足,合并时报 `Does not have enough approvals`。 表象(权限不足)与真因(枚举值少个 `D`)毫无关联,极难自查。 2. **必须带 `commit_id`** —— 不带时同样落成 `PENDING`(与 `event` 是两个独立条件)。 3. **`required_approvals` 也约束 admin** —— 别指望用 admin 跳过审批。 ```bash # ① 先取 head sha(审批必须绑定**具体提交**,不是"移动的目标") enc_shell(command="curl -s -H \"Authorization: token @{t}\" http://host/api/v1/repos/jianf/MyProj/pulls/7 | grep -o '\"sha\":\"[a-f0-9]*\"' | head -1") # ② 审批(event 必须 APPROVED,且带 commit_id) enc_shell(command="curl -s -X POST -H \"Authorization: token @{t}\" -H 'Content-Type: application/json' -d '{\"body\":\"通过\",\"event\":\"APPROVED\",\"commit_id\":\"\"}' http://host/api/v1/repos/jianf/MyProj/pulls/7/reviews") ``` **失败时保留平台原话**:`Does not have enough approvals` 是区分「机制生效」 与「配置错了」的唯一线索,别把它翻译掉或吞掉。 --- ## Usage: 第一部分 使用者纪律 > **★ 设计总纲 [已定]**:插件对 pi 做的事,本质就是 > **「把给 pi 发消息包装一下、把 pi 返回包装一下」**,深度融合内核的 IO 抽象与中断系统 > —— 即 **pi = 一个 Device**。**不要自己造调度器 / 会话路由 / 中断栈**, > 全部接到内核已有的那套上(`InjectInputTo` / `RegisterOutputChannel` / `InjectInterrupt*Opts`)。 ### 21. 角色与单位 | 角色 | 是谁 | 干什么 | |---|---|---| | **人** | 老板 | 只与主管对接 | | **主管** | 你(长期驻留的 HomeAgent 主 agent) | 拆任务、**定义判据**、复审、决定合入 | | **执行 agent** | 按工程拉起的一个 pi / dsh 实例 | 在某棵工作树里干活 | | **commondev 插件** | 外部独立插件 | 仓库/凭据/分支/PR/测试,全部机械化操作 | **为何能用内核机制**(而不是自己造): | 内核概念 | pi 侧对应 | 落点(内核已有) | |---|---|---| | **给 pi 发消息** | `pi.prompt` / `steer` / `follow_up` | `InjectInputTo(source, "commondev/", …)` | | **pi 返回** | sidecar 回调 / `tool_execution_*` / `agent_settled` | `InjectTextTo(source, "commondev/", …)`(按 inputch Owner 路由) | | **中断 pi** | `steer`(插到当前轮之后) | `InjectInterruptTextOpts(..., Priority: L3)` | | **音声/图像产物** | pi 可产出的媒体 | `OutputCapabilities()` 声明 | ⇒ **插件只做两件额外事**:① 把 pi 接成 Device;② 台账记 `session_id` 并断言未变。 调度/中断/路由/记忆策略**全是内核已经在做的事**。 ### 22. 「工程对话」是一等对象 它是四元组,缺一不可: ``` 工程对话 (id = br--) ├─ workdir <插件数据目录>/program/- │ ★ hash = H(项目+分支) 是**唯一性来源** │ ⇒ **同一仓库的不同分支必然是不同目录**(否则两 agent 共用工作树、改动互相污染) │ ★ name = 分支名派生,**仅做可读性**,不承担唯一性 │ (派生后可能撞:`feat/a` 与 `feat.a` 都成 `feat_a`,所以必须带 hash) │ 独立 clone(非 worktree、非 --reference);合入后**整目录删除** ├─ session_id pi 会话(★ 续投的唯一依据) ├─ runtime local(默认,内嵌) | ssh(远端同样建目录拉 pi) ├─ host runtime=ssh 时的目标主机(**branch_open 时给**,见 §24.1) ├─ agent_cli 执行后端(默认 pi;其他 CLI 需自备 sidecar,见 §24.2) └─ channels 插件为该工程注册的一对通道 ├─ inputch commondev/ └─ outputch commondev_ ``` **一个仓库可并行开发**:每分支一份独立 clone,互不污染、可同时推进。 但并行有前提——必须是**互不相干的模块**;有依赖的任务请先拆边界。 > **为何不能用 worktree / `--reference`**(实测):worktree 删后有 > `.git/worktrees/` 残留(需 `prune`);`--reference` 有 alternates 外部依赖, > mirror 被清即损坏。**本地 clone 对象为硬链接**(实测 inode 相同)⇒ 省磁盘、 > 无外部依赖、刪掉即彻底。代价:失去 worktree「同分支不可双挂」的保护 > ⇒ 台账层必须补**唯一性闸**(同名未结分支响亮拒绝)。 ### 23. ★ 必做:为每个工程对话新建驻留子,并把通道划给它 **★ 顺序很关键**:通道是**拉起 pi 拿到 session 之后**才注册的,所以你的动作是: ``` 建工程 ──► commondev_agent_up(拉起 pi,拿到 session_id) └─► 插件**此刻才**注册通道 inputch commondev/ + outputch commondev_ └─► ★ 你建驻留子并把这对通道划给它 ``` **为何必须先有 session**:通道是“与某个具体 pi 会话对话的入口”,没 session 就是空壳。 顺序错了(先建子)会拿到一个指向不存在通道的 `output_send__…`。 **插件只能把通道登记出来;「划入(assign)」只有内核能做。** 所以这一步**必须由你(agent) 自己调内核工具完成**,插件无法代劳。 ``` resident_agents create id = dev- input_chs = commondev/ ← 划入该 inputch(可用斜杠) allowed_outputs = commondev_ ← 授权该 outputch(★ 下划线,见下) capacity = 64 task_prompt = "你在 推进 。 下任务给自己用 output_send__commondev_(payload=…), pi 的结果会回投到你的 commondev/; 有结论用 notify_parent 汇报。" ``` **★ 下任务不需要新工具**:给 pi 发消息就是子调**内核原生的输出门** `output_send__commondev_(payload="…")` ⇒ 打到插件的通道 handler ⇒ 插件 查台账拿到 `session_id` 后 `sidecar.prompt()`。**插件不提供 `agent_send` 之类的工具**。 **⚠️ 通道命名(很容易踩)**: | | 能用 `/` 吗 | 为什么 | |---|---|---| | **inputch** | ✅ | 内核自己就用 `child/` | | **outputch** | ❌ **绝对不行** | 它会拼成 `output_send__`,上游要求函数名 `^[a-zA-Z0-9_-]{1,64}$`;违反的后果不是“这个工具不可用”,而是**整条请求 400、agent 整体失语** | ⇒ 所以 inputch 叫 `commondev/`、outputch 叫 `commondev_`。 `` 本身也只能用 `[A-Za-z0-9_-]`。 **为什么必须这么做(不是可选的优化):** 1. inputch 是**最基本的输入路由单位**,路由按 `Owner` 在**进内核之前**完成。 划给子之后,**该工程的输入只流向那个子**,根 agent 看不到它 —— 这正是 多工程并行能做到"互不串台"的原因。 2. **不划会的代价**(如果你偷懒):所有工程对话的输入都涌向**根 agent**, 根成了瓶颈、上下文被 N 条工程的细节淹没,多工程并行**等于没做**。 现场教训(`resident-subagent-design.md`):子挂着 `inputch=[timer]` 而 timer 的输入打在父身上,**子的轮次永远是 0** —— 登记了不等于划了。 **落地判据**(怎么知道做对了): - 投一条输入到 `commondev/`,应只被 `dev-` 处理; - 根 agent 的轮次**不**因这条输入增加; - `input_channels` 工具(内核自带,单工具多视图)用 `view=by_agent` 能看到划分总览。 ### 24. 形态选择、自包含运行时与远端环境 **默认本地内嵌**(进程内 pi 实例);通过配置 `runtime=ssh` 切到**远程**, 远端**同样创建目录并在该目录拉起 pi**。 **★ 自包含运行时**:插件**自带独立的 node 与独立的 pi**,都装在**插件工作目录**, **pi 的插件也装到这个目录** —— 不靠宿主机的 node/pi,也不污染用户的 `~/.pi`。 **独立 node/pi 随包分发**(不首次下载、不依赖用户装)。 ``` <插件数据目录>/runtime/ node/ pi/ agentdir/ ← 每个内层 pi 必须传 PI_CODING_AGENT_DIR 指这里 settings.json ← pi 自己的配置(偏好 / 默认 model / 全局包声明) models.json ← pi 自己的配置(★ LLM 源写这里) auth.json ← pi 自己的配置(key / OAuth,0600) mcp.json ← pi 自己的配置(MCP 服务器) skills/ prompts/ themes/ extensions/ ``` #### 24.1 远端(ssh)怎么开 两个轴**正交**,开线时一起给: ``` commondev_branch_open(project="p", branch="feat/x", runtime="ssh", ssh_host="build@10.0.0.9") ``` - `runtime=ssh` 而**没给 `ssh_host`** → 直接拒绝(不会拖到拉起时才报) - **未知 runtime**(比如 `k8s`)→ 响亮拒绝,**不静默降级为 local** (静默降级会让"我以为跑在远端"一路错下去) - 远端前置条件(**先配好,不要指望交互输密码**): - 从本机到目标机的**免密**(`BatchMode=yes`,卡在密码提示会超时) - 远端有 node(不在 PATH 里就配 `ssh_node_path`,例如 `/opt/node/bin/node`) - **必配两个根目录**(缺任一,远端线开不起来): - `ssh_workdir_root` — 远端放**工程工作目录**的根 - `ssh_runtime_root` — 远端放 **sidecar 脚本**的目录 - **★ 工作目录在远端准备**:本地 `clone --mirror` 出来的目录远端看不到, 所以远端线走的是**远端建目录 → 远端共享 mirror(`/.mirrors/`)→ 远端 clone → 远端切分支**。同一远端上的多条分支线共享一份 mirror。 台账里的 `workdir` 会被改写成**远端路径**(不是本地路径)。 远端拉取要求**远端自己能认证该仓库** —— 本插件**不把凭据推到远端** (凭据不进工作目录、不进 remote URL)。拉不动就响亮报错。 - 每次拉起会 `scp` 把 sidecar 推过去(幂等)——**不推就会跑上一次的旧脚本**, 症状是"改了没生效",极难归因。推送失败**响亮报错**,不静默用旧脚本。 - 用 `commondev_agent_state` 看 `ssh_host` 确认打到哪台机。 #### 24.2 接其他 CLI(zcode 等)——**不改插件核心** 执行后端是**声明式**的:放一个目录就多一个后端。 ``` <插件数据目录>/runtime// driver.json ← 描述:脚本名 / 参数 / 环境变量 / LLM 源策略 sidecar.mjs ← 实现 sidecar 契约(ping / prompt / state / ...) ``` **脚本查找链**(按优先级,go 侧 `locateSidecarScript`): | 顺序 | 位置 | 用途 | |---|---|---| | ① | `<插件数据>/runtime//