diff --git a/Makefile b/Makefile index 6f4ecf74..9ac13b2e 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,11 @@ GO=go GOCACHE=/tmp/gocache export GOPATH=/tmp/gopath BUILD_DIR=build +# GRAPH_FILE 是启动依赖图的制品名(必须与 kernelboot.GraphFileName 一致)。 +# +# ★ 图的来源是**各模块自己的 Requires()/Provides()**(不再有手工 JSON 清单): +# 生成器 import internal/kernelmod/all(各模块 init 自注册)⇒ 算分层 ⇒ 落图。 +GRAPH_FILE=depgraph.bin PROJECT_ROOT := $(CURDIR) # 版本号来源(见 docs/git-branching.md §2.1): # @@ -281,7 +286,36 @@ check-csrc-full: check-csrc csrc-fuzz build: @mkdir -p $(BUILD_DIR) - CGO_ENABLED=1 $(GO) build $(TAG_ARGS) -trimpath -installsuffix dynlink -ldflags '$(LDFLAGS)' -o $(BUILD_DIR)/$(BINARY) ./cmd/homed/ + @# ★ 编译期定序:先由生成器(import 模块登记表)算出启动顺序图, + @# 取 digest,再构建 homed 并把 digest 注入二进制。 + @# + @# 为何必须"先出图后构建"(设计 §2.5.2):启动时 homed 比对 + @# 「磁盘图的 digest」与「内嵌 digest」,不一致 ⇒ **拒绝启动**。 + @# 两边必须来自**同一次构建**。 + @# + @# 为何要这个保护:它能拦住最危险的一类故障 —— 图文件自身完好 + @# (自校验通过),但描述的是**旧代码**(改了 Requires 没重出图) + @# ⇒ 按旧图调度 ⇒ 顺序错(正是要消灭的"静默顺序错")。 + @# + @# ⚠ 本段必须写成**一条 shell 命令**(\ 续行): + @# Make 的每行 recipe 是独立 shell,普通变量跨行不保留。 + @# + @# ★★ ldflags 的引号必须让 $${GRAPH_HASH} **能展开**(2026-10-17 实测缺陷): + @# 原写法把整串包在**单引号**里 ⇒ shell 里单引号不做变量展开 + @# ⇒ 注入进二进制的是**字面量** `$$GRAPH_HASH` ⇒ 内核启动时比对 + @# 「磁盘图 digest」vs「内嵌的 $$GRAPH_HASH」必然不等 ⇒ **拒绝启动**。 + @# 而 `make build` 日志全绿(图生成了、hash 也"注入"了), + @# 只有真跑内核才暴露。修法:用**双引号**包 ldflags。 + @# + @# ⚠ 上面这条注释**不能挪到 recipe 中间**:注释行会打断 \ 续行链, + @# 使后半段成为**独立的 shell**(变量丢失 ⇒ hash 为空)。 + @GRAPH_HASH=""; \ + CGO_ENABLED=1 $(GO) build $(TAG_ARGS) -o $(BUILD_DIR)/homed-depgraph ./cmd/homed-depgraph/ && \ + GRAPH_HASH=$$($(BUILD_DIR)/homed-depgraph -digest) && \ + $(BUILD_DIR)/homed-depgraph -print && \ + $(BUILD_DIR)/homed-depgraph -out $(BUILD_DIR)/$(GRAPH_FILE) && \ + echo "已注入启动依赖图 hash: $$GRAPH_HASH" ; \ + CGO_ENABLED=1 $(GO) build $(TAG_ARGS) -trimpath -installsuffix dynlink -ldflags "$(LDFLAGS) -X github.com/JianFeeeee/HomeAgent/internal/kernelboot.EmbeddedGraphHash=$$GRAPH_HASH" -o $(BUILD_DIR)/$(BINARY) ./cmd/homed/ @echo "Built: $(BUILD_DIR)/$(BINARY) ($(VERSION), tags='$(HOMED_TAGS)')" @go version -m $(BUILD_DIR)/$(BINARY) | grep -q 'onnxruntime' \ || echo "WARN: 本次构建不含 onnxruntime,本地向量空间不可用(HOMED_TAGS= 显式关掉时才符合预期)" diff --git a/assets/skills/commondev-discipline/SKILL.md b/assets/skills/commondev-discipline/SKILL.md new file mode 100644 index 00000000..9d665310 --- /dev/null +++ b/assets/skills/commondev-discipline/SKILL.md @@ -0,0 +1,871 @@ +--- +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//