feat(commondev): 主管式多仓库开发流水线 + sidecar 后端抽象(pi/zcode)

## 是什么

agent 统筹开发流程的插件:登记项目 → 开工程线(每条线一个隔离工作目录 +
一个 sidecar 子进程 + 一对通道)→ 投递任务 → 同会话续投 → 评审/合入。

38 个工具:project_*/branch_*/workdir_*/job_*/review_*/test_*/
endpoint_*/agent_*/pi_*/conflict_check。

## sidecar 后端抽象(本轮核心)

传输层(local/ssh)与后端(pi/zcode/自定义)**正交**,四个自由组合:

  driver.json 描述一个后端:script / cliPath / env / args / llmConfig
  三级查找:插件数据目录 → **随包 rawfile** → 内置 pi 描述
  占位符:{workdir} {dataDir} {pluginDir} {rawfile} {sessionId}

★ 内置 pi 回退**与默认后端解耦**(2026-10-17 修):原实现写
`if cli == defaultAgentCLI { return builtinPiDriver() }`,把"给谁兜底"
和"默认选谁"耦合到同一常量 —— 一旦有人改默认,pi 当场失去兜底,
显式 cli="pi" 开始报「驱动不存在」。现在判断字面 "pi"。

默认后端保持 **pi**(我们本地主用);交付方用 zcode 走两种局部开关:
 · 单线:commondev_branch_open(cli="zcode")
 · 按机器:commondev_endpoint_set(host=..., cli="zcode")

## zcode 接入(三条真机判据,全部要求真改工程文件)

 · TestZcodeFullPipeline(29-34s):随包 driver + 真实 ZCode CLI
 · TestZcodeOverSSH(39s):真 ssh + scp,远端 node 跑 zcode
 · TestZcodeOverSSHWithPerHostEndpoints(68s):两个 host 别名配不同
   cli_path,走端点表而不是全局 settings

★ 会话库必须隔离到工作区(sidecar.mjs 的 ensureWorkspaceSessionStore):
实测 ZCode 的会话库路径**硬编码 homedir()**,`ZCODE_DATA_BASE_DIR` 对它
完全无效(26 个会话全落全局库)。但 `<workdir>/.zcode/config.json` 的
`storage.sessionDbPath` **确实生效**。所以启动前写一份工作区配置,
一举解决:多线不共用历史 / 不污染全局 ~/.zcode / 删线不留孤儿会话。
只在不存在时写 —— 用户手工放的要尊重。

## 其他

 · rawfile:项目根 rawfile/ 默认全收,解到 <插件目录>/rawfile/,绝对路径拒绝
 · 凭证:命名引用 + ${ENV_VAR},独立 0600 文件,绝不进 config.db(0644)
 · secretshell:给不受信 agent 用的安全 shell(ackey + safeshell)
 · 冲突检查 / 评审判据 / 镜像 / 每条线独立 workdir(hash 命名,合并后删除)

判据 30+ 条,含 4 条「判据本身的判据」(防止判据假绿)。
This commit is contained in:
JianFeeeee
2026-10-17 16:16:04 +08:00
parent 65d2bd9e61
commit c2474f57a2
55 changed files with 16071 additions and 0 deletions

View File

@ -0,0 +1,84 @@
package main
import "testing"
// 钉住文档 §2.2 的命名约束:
// inputch 可含斜杠;outputch **必须**只剩 [A-Za-z0-9_-],
// 否则会产出非法的 output_send__<name>(上游 400、agent 整体失语)。
func TestOutputChannelNameIsToolSafe(t *testing.T) {
cases := []string{
"br-TrueAgent-feat_x",
"br-p1-feat/x", // 分支名带斜杠
"br-p1-feat.x", // 点
"br-p1-中文", // 非 ASCII
"br-p1-a b", // 空格
}
for _, id := range cases {
got := outputChannelName(id)
if !validToolName("output_send__" + got) {
t.Errorf("id=%q → outputch=%q ⇒ 工具名 %q 非法(会触发上游 400)",
id, got, "output_send__"+got)
}
// inputch 则允许斜杠(内核自己就用 child/<id>)
if in := inputChannelName(id); in != "commondev/"+id {
t.Errorf("inputch 应原样保留: %q", in)
}
}
}
// validToolName 复刻上游对函数名的约束。
func validToolName(name string) bool {
if len(name) == 0 || len(name) > 64 {
return false
}
for _, r := range name {
switch {
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z',
r >= '0' && r <= '9', r == '_', r == '-':
default:
return false
}
}
return true
}
// 台账往返:保存后能读回,且损坏时拒绝静默重建。
func TestLedgerRoundTrip(t *testing.T) {
p := &Plugin{name: "commondev"}
p.path = t.TempDir() + "/ledger.json"
if err := p.loadLedger(); err != nil {
t.Fatal(err)
}
p.ledger.Projects["demo"] = &Project{Name: "demo", RemoteURL: "https://x/y.git"}
if err := p.saveLocked(); err != nil {
t.Fatal(err)
}
q := &Plugin{name: "commondev", path: p.path}
if err := q.loadLedger(); err != nil {
t.Fatal(err)
}
if q.ledger.Projects["demo"] == nil {
t.Fatal("台账未往返")
}
}
// 钉住名字校验方向(这条是**真 bug 的回归测试**)。
//
// 初版用排除式正则 `[^a-zA-Z0-9_-]` 配 MatchString 判"合法",恰好反了:
// 合法名不含违法字符 ⇒ 返回 false ⇒ 把 `cdtest` 判非法。
// 是端到端跑时由 agent 调工具发现的 —— 所以它必须有回归判据。
func TestValidName(t *testing.T) {
legal := []string{"cdtest", "TrueAgent", "feat_x", "a-b", "A1"}
for _, s := range legal {
if !validName(s) {
t.Errorf("合法名被拒: %q(校验方向反了?)", s)
}
}
illegal := []string{"feat/x", "a b", "中文", "a.b", "", "a/b/c"}
for _, s := range illegal {
if validName(s) {
t.Errorf("非法名被放行: %q", s)
}
}
}