360 Secure SkillHub
360 安全技能中心

codebuddy-cli-delegation

面向 CodeBuddy CLI 的批量编码委派流程:用干净环境启动器拉起独立无头 CLI,控制指令预算与并行隔离,按派发方指定模型分配参数,并通过复跑、对抗探针和变异对照独立验收产物。

carloswonmore
carloswonmore
热度 19.38
最近更新 2026-10-08 00:04:10
分类:开发依赖:代码执行 / 读写文件 / 第三方 API / 内部调用大模型应用场景:代码生成 / 代码审查 / 调试排错 / 自动化脚本 ...

安装技能

复制下面的话术发给你的 Agent,它会通过 secure-skills 检索并安装该技能。

执行命令安装此 skill: npx secure-skills-cli add codebuddy-cli-delegation
Metadata
name
codebuddy-cli-delegation
description
把大批量编码任务派给「独立无头 CodeBuddy CLI」执行的标准流程。CodeBuddy CLI 专用(非通用 CLI 技能):干净环境启动(避免宿主注入环境变量导致 0 字节静默挂死)、指令预算与并行准入(四隔离 + 写集不相交)、模型与参数分配(模型由派发方指定,参数按阶段分配)、独立审计验收(对抗性探针 + 变异对照,不采信 CLI 自述)。含 21 个可用模型清单与厂商档位映射、启动器模板,全部结论基于 CodeBuddy CLI 2.156.0 实测。用于批量改造、批量补测试、批量文档回填、无人值守跑批次等前台会超时的大任务。触发词:用 CLI 派活、让 CLI 干一批、无人值守跑、交给 CLI 做、批量改造、批量补测试、前台会超时、代码审计外包、headless CLI、CodeBuddy CLI。英文关键词 / English keywords:delegate to headless CodeBuddy CLI, unattended batch run, batch refactor, batch test authoring, agent delegation audit, adversarial probe, mutation control, clean-env launcher, headless agent delegation.
agent_created
true

CodeBuddy CLI 派活(Headless Agent Delegation)

一句话:把一批活交给一个独立启动的无头 CodeBuddy CLI agent 去干,你只负责 写清指令 → 控预算 → 收产物 → 独立验收。

它解决什么:前台同步做会超时 / 上下文会被撑爆 / 需要并行多批次的大活。 它不解决什么:需要人来拍板的决策、需要访问你所在宿主私有状态的事、一次性的小改动。

⚠️ 适用范围:这是 CodeBuddy CLI 专用技能

本技能的全部实测结论都只对 CodeBuddy CLI 成立,具体包括: 干净环境启动器要剥离的变量名(CODEBUDDY_* / ACC_PRODUCT_CONFIG_* / SERVER__*)、 --setting-sources project、--permission-mode bypassPermissions、 --effort 六档档位与厂商映射、21个可用模型清单、--agents 的 JSON schema、 -c 恢复会话、--tools 白名单、以及会话日志路径 ~/.codebuddy/projects/。

换用别的 CLI(Codex / Gemini CLI / Claude Code 等)时不要照搬本技能: 那些 CLI 的注入变量清单、参数名、会话日志结构、权限机制都不同, 必须各自重新实测,否则会踩到「按CodeBuddy 的假设去剥别的 CLI 的变量」这类错。

其中真正与CLI 无关、可直接复用的方法论(换 CLI 后仍成立,只需重新验证载体): §二(并行准入与四隔离)、§3.5(派发前契约核对六动作)、§7.3–7.7(对抗探针与变异对照)、§零铁律 3(不采信自述)。 建议这些部分另立一份 agent-delegation-audit 之类的通用技能, 由本技能与将来的其他 CLI 技能共同引用 —— 但那需要另做一次提炼与验证。


零、四条铁律(先看这个,其余都是细节)

  1. 绝不直接启动裸 CLI —— 宿主会把自己的环境变量注入子进程,轻则跑错配置,重则永久挂死。 必须走一个「干净环境启动器」。
  2. 默认串行;但并行是「支持」的 —— 要不要并行,取决于「共享可变资源是否隔离」,与端口无关。 串行的真实理由是共享可变资源只有一份(同一个库 / 同一份契约 / 同一个 git 工作区) 加上**「逐批可归因审计」会被交错 diff 破坏**(分不清某一行属于哪一批);瓶颈在审计而非执行。 要并行就必须先做「四隔离」(目录+git / 依赖 / 数据库 / 端口)并保证写集不相交 —— 见 §二。 ⚠️ 不要再写「端口按工作区派生,第二个实例必 EADDRINUSE」—— 该结论已于 2026-10-01 实测推翻,详见 §二。
  3. 不采信它的自述 —— 它说「已通过 / 已标记 / 已加载」都要自己复跑一遍才能算完成。 历史上出现过「文案说已标记异常、代码里根本没调那个方法」这类事。
  4. 模型由派发方显式指定;技能只负责在给定模型下分配参数,永不替派发方选模型。 派活时把 --model 当成输入而不是建议值。技能该提供的是「模型可用性事实表」(§一) 与「在给定模型下怎么分档」(§五之三),不是「这个阶段该用哪个模型」。 ⚠️ 写过「阶段 → 推荐模型」的速查表是危险品:它会让下一次派活悄悄滑向「技能替人做技术选型」, 而选型责任与后果都在派发方。想按阶段换模型,必须由派发方显式拆成多批或多子代理(§五之三)。

一、为什么需要「干净环境启动器」

Agent 宿主(WorkBuddy / VS Code 扩展 / 桌面端)向子进程注入的变量里,至少有三类会让独立 CLI 出错:

注入变量 后果
*_SERVICE_PROXY_URL=http://127.0.0.1:<宿主端口>/... 独立 CLI 会去绑定同一个端口提供 service-proxy → EADDRINUSE 未捕获异常 → 进程永久挂死(0 字节输出,300s 无任何反应)
*_CONFIG_DIR=<宿主配置目录> CLI 误读宿主的配置目录:技能 / 模型 / 插件全错位
SERVER__PORT / SERVER__HOST(没有前缀,容易被漏掉) 同上,仍会去 listen 那个已被占用的端口 → 挂死
<产品身份配置>(如 ACC_PRODUCT_CONFIG_PATH) 决定了 CLI 用哪个 authentication.id 去读凭据 → 去读宿主的凭据文件 → 报「未登录」(但其实是登录着的)

做法:启动前把所有疑似注入变量 unset 掉,再让 CLI 回落到它自己的默认位置。

启动器:单个 .js,三平台通用

scripts/codebuddy-cli.js —— 一个文件覆盖 Windows / macOS / Linux,无需 .sh / .ps1 / .bat。

环境 调用方式
任意平台 node scripts/codebuddy-cli.js -p "指令" --model hy3 --effort high
Linux/macOS(先授权一次) chmod +x scripts/codebuddy-cli.js 后 ./scripts/codebuddy-cli.js -p "指令"

用 node 前缀最稳:Windows 上双击/直接执行需要文件关联,而 .js 的关联不总存在。

为什么用 Node 而不是 shell(三条硬理由)

  1. 「有 Node」这个前提是白送的 —— CodeBuddy CLI 本身就是 Node 应用 (真实入口首行是 #!/usr/bin/env node),装了它就必然有 Node.js。 而 shell 的可用性没有这种保证(Windows 可能没 Git Bash,POSIX 未必有 Git Bash)。
  2. 白名单友好 —— SkillHub 上传只允许 .md .txt .json .yaml .yml .js .cjs .mjs .ts .py .sh .png .jpg .svg。 .js 在内,.ps1 / .bat / .cmd 都不在 ⇒ 一份 .js 是唯一能同时满足 "跨平台"与"可发布"的形态。
  3. 绕开一整类跨平台陷阱 —— 前几轮在 .sh/.ps1 上连续踩了:UTF-8 BOM、 sh 与 Node 的路径语义不一致、多层引号下 sed 静默失效、ProgramFiles(x86) 解析…… 换成 Node 后这些整类消失(没有 shell 方言,也没有编码歧义)。

它做了什么

  1. 剥离宿主注入的环境变量(§一那张表的两组 + 产品身份 + 会话标识)

    唯一例外:CODEBUDDY_CODE_GIT_BASH_PATH 不剥 —— 它是「指向一个 bash 可执行文件」的 路径提示,不含端口 / 配置目录 / 产品身份,正是下面第 2 条自己要设置的变量。 但必须先过 fs.existsSync:宿主给的路径真实存在才沿用(比重新探测更准), 陈旧 / 跨机复制来的失效路径一律丢弃,交给第 2 条自行探测。 ⚠️ 判断与写入 MUST 落在同一个对象上(都是 cleanEnv)。剥离改 cleanEnv、守卫却读 process.env,会让「宿主给的」和「自己探测的」两个来源同时落空 —— 这是本脚本修过的 一处真实 bug,症状诡异(宿主明明注入了有效路径,CLI 却报 Git Bash not detected), 详见 §「Git Bash 那条提示说明什么」。

  2. Windows 上补 CODEBUDDY_CODE_GIT_BASH_PATH —— 否则 CLI 会退化成 PowerShell; 探测顺序:PATH 里的 bash.exe → 各盘 Program Files\Git\bin\ → PATH 目录里任意 bash.exe
  3. 定位 CodeBuddy CLI —— 扫 PATH 各目录找 codebuddy(Windows 上按 PATHEXT 覆盖 .cmd/.exe)→ 从 wrapper 所在目录反推真实 js 入口 → 扫 npm 常见全局安装位置
  4. 翻译参数风格(见下)—— -Prompt / -Model 这类具名参数会被转成 CLI 认识的形态
  5. 追加 --append-system-prompt —— 读取同目录的 skills-bootstrap.md(不存在则跳过)
  6. 以 stdio: 'inherit' 启动 ⇒ 输出直接透传,日志不会被脚本污染;退出码原样传出

💡 skills-bootstrap.md 要放在 codebuddy-cli.js 同目录。 复制技能时把 skills-bootstrap.template.md 改名成 skills-bootstrap.md 放在旁边即可。

参数风格:两种写法都支持(实测踩过)

直接照抄 .ps1 版的写法会报错:

PS> node codebuddy-cli.js -Prompt "回答一个字:好" -OutputFormat json
error: unknown option '-Prompt'          ← CLI 只认 -p / --print

原因:.ps1 版有 -Prompt/-Model/-Effort 具名参数,而 .js 版一开始是纯透传。 两版接口不一致 ⇒ 换版本时踩坑。

⇒ 现已加翻译层,以下三种风格实测均通过:

# 风格 A:PowerShell 具名参数(对 .ps1 版用户最自然)
node scripts/codebuddy-cli.js -Prompt "指令" -Model hy3 -Effort high -OutputFormat json

# 风格 B:CLI 原生参数
node scripts/codebuddy-cli.js -p "指令" --model hy3 --effort high --output-format json

# 风格 C:两者混用
node scripts/codebuddy-cli.js -Prompt "指令" --effort high --output-format json

翻译表(只覆盖 CLI 认识的那几个,未识别的参数原样透传 ⇒ CLI 未来新增参数仍可用):

具名写法 转为
-Prompt / -prompt -p
-Model / -model --model
-Effort / -effort --effort
-MaxTurns / -Max-turns --max-turns
-OutputFormat / -Output-format --output-format
-PermissionMode / -permission-mode --permission-mode

💡 只翻译「独立的参数项」(不带值),避免误改提示词内容里恰好相同的字样。

⚠️ 这是「接口不一致」类问题的典型:同一技能的不同版本/不同脚本用了不同参数风格, 而 CLI 侧只会报 unknown option,不会告诉你"你想要的参数其实是另一个名字"。 ⇒ 凡是提供多种调用风格,必须显式写进文档并实测,否则每个换风格的人都要重踩一次。

⚠️ 为什么不用 where / command -v 找 CLI(实测踩到)

定位 CLI 时不要派生子进程去问 PATH:

// ❌ 实测在 Windows 上返回 error=EBUSY(宿主安全策略拦「进程派生进程」)
spawnSync('cmd.exe', ['/c', 'where', 'codebuddy'])

// ✅ 纯 fs 操作扫 PATH,不触发进程派生,稳
for (const dir of (process.env.PATH || '').split(path.delimiter)) {
  for (const ext of (process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';')) {
    const p = path.join(dir, name + ext.toLowerCase());
    if (fs.existsSync(p) && fs.statSync(p).isFile()) return p;
  }
}

同理,别用 npm root -g 命令来定位:① 同样要进程派生;② 它随「当前用哪个 npm」而变 (实测干净环境下它指向另一个 node 的目录 ⇒ 漏判)。改成直接 fs.existsSync 扫 npm 的标准落点。

Git Bash 那条提示说明什么

若 stderr 出现 Git Bash not detected,说明脚本没能给 CODEBUDDY_CODE_GIT_BASH_PATH 指到可执行文件。按序自查:

  1. 宿主是否已经注入过这个变量? 若是,先确认启动器是「校验过存在性再沿用」而不是 「直接信任」,更要确认守卫读的是哪个对象 —— 最常见的一类 bug 是:剥离改的是 cleanEnv, 判断却读 process.env ⇒ 宿主注入了有效路径时整块探测被跳过,而该变量又已被剥掉, 两个来源同时落空,看起来完全不像配置错误。 通用诊断:调用前 unset CODEBUDDY_CODE_GIT_BASH_PATH 再跑,可区分「问题出在宿主给的路径」 还是「出在探测逻辑」。
  2. 逐条打印 PATH,看是哪一步没命中:
// 逐条打印,看是哪一步没命中
process.env.PATH.split(path.delimiter).forEach(d => console.log(d));
// 手动确认目标存在(把路径换成你机器上的安装位置)
console.log(fs.existsSync('<Git 安装根>\\bin\\bash.exe'));

也可能是 Git Bash 确实没装(此时装一个即可,或确认退化成 PowerShell 对你的任务无影响—— 只在 CLI 需要真正执行 shell 命令时才有影响,纯问答/改文件不受影响)。

🔴 七轮翻车复盘:为什么「结构检查」全都测不出来

启动器从三平台 shell脚本换成单文件 .js,前后翻了七轮。前六轮是 「检查通过但实际不可用」,第七轮是接口不一致:

轮次 场景 我改了什么 检查结果 实际后果
① .sh 写三平台启动器 sh -n 通过 逻辑错 5 处:多层引号下 sed 静默失效、exec 调shell 函数(无效语法)、npm root -g 漏判、wrapper 当 js 入口跑、路径语义不一致
② .ps1 修 Join-Path 空值 花括号仍配平 从「能跑但 stderr 脏」退化为「跑不起来」 —— 引入 UTF-8 BOM 问题
③ .ps1 加 UTF-8 BOM 括号平衡 ✅ 通过(用户实跑确认)
④ .js 改用 where 定位 CLI node --check 通过 error=EBUSY —— 进程派生被宿主安全策略拦,定位恒失败
⑤ .js 改用纯 fs 扫 PATH 语法通过 ✅ 通过
⑥ .js 补「各盘 Program Files」Git Bash 探测 语法通过 实测命中 Windows 上的 Git Bash,但未能验证 CLI 是否接受(沙箱无法执行 bash)
⑦ .js 从 .ps1 版切换过来 node --check 通过 error: unknown option '-Prompt' —— .ps1 有具名参数、.js 是纯透传,两版接口不一致

⑦的性质与前六轮不同:前六轮是"环境里的坑",⑦是我自己制造的接口不一致 —— 把一个技能的两种实现写成两套参数风格,用户换过来就必然报错, 而 CLI只会说"我不认识这个参数",不会提示你想要的那个名字长什么样。

⇒ 固化三条纪律:

① 任何脚本改动,验证标准是「在目标环境跑一次并读真实输出」,不是「语法检查通过」。 ② 改完必须重跑 —— 包括你"刚改好的"那一次。 ②轮就是修 bug 后没重跑,直接把可用状态改成了不可用。

③ 同一技能的多份实现(不同语言/不同平台),对外接口必须逐条对齐并写进文档。 不同实现之间不能各自发明参数风格;若要支持多种风格,必须显式做翻译层 + 实测。

⇒ 推论一:报错位置与根因可能无关。②轮报"第 57 行意外的 }",真因是第 11 字节的 编码解码中断 —— 查花括号永远查不出来。看到"语法错误指向某行"时,先怀疑文件编码。

⇒ 推论二:定位依赖"问系统"(where / command -v / npm root -g)本身可能就是故障点。 在受限环境(沙箱、容器、CI)里这些命令会因进程派生被拦。 ⇒ 优先用纯 fs 探测:自己扫 PATH、自己拼路径,比派生子进程问系统可靠得多。

⇒ 推论三:"unknown option"类报错要往"上游版本"查,而不只是看当前脚本。 它往往意味着你手里那份文档/示例来自另一个实现。

⇒ 这与 §零铁律 3「不采信自述」同源 —— 连"我自己刚写的脚本"都必须当作需要独立验收的对象。

1.1 模型可用性事实表(CodeBuddy CLI 2.156.0,2026-10-06 实测)

这张表只陈述事实,不推荐选哪个。选哪个是派发方的决定(见 §零 铁律 4)。

codebuddy --model 实测可用 21 个,各跑一次最小调用(--effort high,提示词「只回答两个字:收到」), 21/21 成功,零失败零超时,耗时 5.5–14.8s:

档 模型 ID
混元 hy3、hy3-x、hy4-preview
DeepSeek deepseek-v4.1-flash、deepseek-v4-pro
GLM glm-5.3、glm-5.3-flash、glm-5.3-flashx、glm-5.2、glm-5.1、glm-5v-turbo
Kimi kimi-k3-2、kimi-k2.8-preview、kimi-k2.7、kimi-k2.6
其他 minimax-m3-pay、step-5-preview、space-bunny
别名 fast-model、balanced-model、deep-model
  • 别名适合「不想记模型名」的场景:fast / balanced / deep 三档实测思考量递增 (balanced ≈ kimi-k2.7 档,deep ≈ kimi-k3 档)。派发方只说「用 deep 档」也是合法的模型指定。
  • 耗时参考:glm-5v-turbo 最慢(14.8s)、hy4-preview 与 deep-model 次之(11.0s)、 deepseek-v4.1-flash 最快(5.5s)。⚠️ 这是单轮耗时;派活总耗时由轮数决定,别据此排序「谁最快」。
  • 别名会随账号/版本变化 —— 派发前若某 ID 报「模型不存在」,先 codebuddy --help | grep -- --model 看当前清单, 不要照抄本表。

⚠️ 本表没有「哪个模型最适合哪类任务」的结论,那是派发方的技术选型,不是技能该给的。 若你要「编码阶段用便宜模型、设计阶段用强模型」,请拆成多批(§2.3)或多子代理(§五之三)显式指定。


二、并行与串行:准入、分工、合并纪律

📌 下面「四隔离」与「写集不相交」是通用的,但示例里的 npm ci / prisma migrate / jest 来自一套 Node + Prisma + Jest 的 monorepo。你用别的栈时,换成对应命令即可 (例如 Maven 的 mvn dependency:go-offline、Python 的 uv sync、.NET 的 dotnet restore)—— 要隔离的本质是「各自独立的依赖目录」与「各自的数据库/schema」。

曾经的写法(错的):「无头模式的监听端口由工作区路径派生:同一工作区开第二个实例必然端口冲突并挂死。」

2026-10-01 实测(CodeBuddy CLI 2.156.0,经干净启动器):

实验 结果
同目录(同一工作区)并发两个实例,纯文本回复 两次均 subtype: success
同目录并发两个实例,提示词要求真实调用 Bash 工具(关键:纯文本回复可能根本起不到 service-proxy,会让结论悬空) 两个仍均成功(各自拿到 TOKEN-A / TOKEN-B),无 EADDRINUSE

错误是怎么来的:把「宿主注入的固定 SERVER__PORT=37641 会让两个子进程撞同一端口」 过度概括成了「同工作区不能并发」。注入变量被启动器剥掉后,这个约束就不存在了。

方法论(比结论本身更重要):一条「实测沉淀」的规则,要能说清它约束的到底是哪个变量、哪个资源。 说成「同工作区」这种粗粒度,会把一个「注入导致的固定端口冲突」升级成一条假的技术限制, 进而挡住本来可行的设计。 判据:写下这类规则时追问一句 ——「如果我换一种启动方式,这条还成立吗?」 成立的是资源约束(该串行),不成立的只是启动方式的副作用(不该写成规则)。

2.1 结论:并行是支持的,但必须先做「四隔离」

默认串行的理由是工程的(共享库 / 契约 / git 工作区 + 逐批可归因审计),不是技术限制。 要并行,就逐项隔离下面这四样;缺一项就会出现「串了任务」的冲突或卡死:

# 隔离项 做法 不隔离的后果
1 目录 + git 每个并行批各建 git worktree(独立目录 / 独立 index / 独立 HEAD / 独立分支) 同工作区两批同时 commit → index.lock 争用;git add -A 把对方半成品一起暂存
2 依赖 各 worktree 各自 npm ci(node_modules 不在版本库里,worktree 不会共享) 同目录两批同时 npm ci → 彼此清空重建 node_modules → ENOENT / 假失败;prisma generate 写坏生成的 client
3 数据库 各批独立 database(CREATE DATABASE <db>_<批次>)或连接串加 ?schema=<批次> 同库并发:prisma migrate 取 PG advisory lock 互等至超时;测试 afterEach 互相删数据;全局计数断言(family=1 这类)被别的套件干扰 → 偶发红
4 端口 + 产物 各批独立日志目录;若起服务则各用不同 PORT 业务服务端口(如 3000)撞 EADDRINUSE —— 这是业务端口,与「CLI 自身注入端口」是两码事

加一条门槛:写集必须不相交。 准入前先把两批的预期改动文件清单列出来比对; 同一个文件、同一份契约(openspec/**)、同一条 migration 序列都算互斥资源,交集非空即不得并行。

2.2 适合并行的典型形态

  • 不同工程 / 不同语言栈:如后端 apps/api/** ‖ 前端 apps/pwa/** —— 不同测试套件、不同依赖树;
  • 写批 ‖ 纯只读批:调研 / 代码考古 / 生成报告 / 只读诊断 —— 只读批不 commit、不跑含清库的测试、不连库;
  • 本地 ‖ 远程:本地写批 ‖ 只能在某台机器上跑的部署批 —— 资源天然分离(不同机器、不同库)。

反例(不要并行):两个批都要改同一份源码或同一份契约、都要跑同一个库的成套测试、都要 npm ci。

2.3 并行批的合并与审计纪律

  • 各自分支、各自 commit;由主会话审计通过后串行合并回主干(一次只合并一个,合并前重跑全量)。 绝不让两个并行批各自直接提交到同一工作区。
  • 审计仍是逐批独立(独立复跑 + 对抗探针 + 变异对照);并行只提高执行吞吐、不减少审计量。
  • 任一批改了 openspec/**(或任何共享契约),其余批先停下等它合并。

排活策略:任务大就拆成多次串行,每批控制在 ~15 分钟内;确有多余算力再按 §2.1 上并行。


三、写指令:这是成败的关键(比选模型重要得多)

3.1 「提一句」≠「加载技能」

只把要求写成风格描述("请保持最小改动")不会触发技能加载。 必须显式要求调用 Skill 工具, 或者把要求写进 system prompt(推荐,见 §五)。

3.2 把结论喂给它,别让它自己找

最坏的模式是「你去把这个仓库里的 X 改好」——它会把预算全花在侦察上。 好指令的骨架:

目标:<一句话,可验收>
已知(不要重复侦察):
  - 入口文件:<路径:行>
  - 现有基建:<可复用的函数/测试替身/配置>
  - 已有结论:<上一轮已查明的口径>
改动范围:<允许改哪些文件;明确禁止改什么>
预算:最多读 <N> 个文件;迭代期只构建/只跑受影响的工程;收尾**一次**全量
验证:<跑什么命令、期望什么输出>
产出:<要写哪些文件 / 要报什么>

3.3 硬性预算(照着给,别客气)

项 建议值 为什么
单次运行时长 ≲ 15 分钟 单条模型请求太久会被网关/流超时掐掉,日志里表现为 error=canceled / 499 / HTTP connection closed prematurely,零产出
子代理串行数 ≤ 2 个/批 ⚠️ 2026-10-06 实测:3 个子代理串行(architect→mechanic→auditor)跑满 5 分钟仍未结束;同一任务用 2 个子代理 16 秒完成。串行子代理的耗时是乘性叠加,不是取最大值
任务体量 超过就拆 宁跑 3 次各 5 分钟,不跑 1 次 25 分钟
思考档位 默认 high;仅超时/超预算才降 ⚠️ 旧版写「显式设中低档」是错的(误以为降档省钱)。实测反证:arXiv 2607.02436(90 次独立 agent run,固定规格 + 14 项功能评分)把 reasoning effort 从 High 提到 xHigh,首次运行完美通过率 28% → 89%,纠正性提示减少约 5 倍,成本只涨 9–29%。「探索过深」在 agent 循环里常表现为反复读同一批文件(真卡住),而不是思考深;真超时了再降档,且降的理由要写进日志,否则事后无法归因
--effort 档位名 逐字核对 ⚠️ 非法值被静默吞掉:--effort bogus-level 返回 subtype: success,与 minimal/high 的返回完全一致,无报错无警告(2026-10-06 实测,5 个模型 × none/bogus-level 全部 rc=0)。档位名拼错无法从日志发现,只能靠「改了参数但行为没变」反推
--effort medium 别指望它省成本 DeepSeek 官方原文:medium / xhigh are accepted and mapped to high;GLM-5.3 与 Kimi K3 压根没有 medium 档(只有 low/high/max)。写 medium 想省钱,很可能跑在 high 上
读文件数 明确给上限 没有上限时它会读遍全仓
思考 token 指标 不要用它判断档位是否生效 ⚠️ reasoning_tokens 是噪声指标:同一模型同一档位 3 次重复采样,值在 0~150 间乱跳(glm-5.3 high → 87/3/77,minimal → 4/37/22,两档分布完全重叠)。曾据单次采样误判「minimal vs high 差 35 倍」,3 次重复即推翻。验证档位生效要找别的判据(如 --debug 也不行:-p 模式下不打请求体)

3.4 首次失败的标准动作

表现为「跑很久 + 大量只读调用 + 零文件产出」。不要重试同一条指令,而是: ① 把上一轮侦察到的结论写进新指令;② 加硬性文件/步骤预算;③ 提高超时; ④ 只有确认是超时才降档,且日志里写明「因X 超时,从 high 降到 low」。

⚠️ 别把「降档」当默认动作。它降低的是首过率(见上表),换来的只是跑完的概率, 不是跑对���概率。先用「喂结论 + 加预算」解决探索过深,这是收益最高的手段。

3.5 派发前先做一次契约核对(派活前最高性价比的动作)

📌 本节是通用方法论,但下面的实战案例来自特定技术栈(Spec 驱动的 monorepo: openspec 管契约、Prisma + Jest 做测试)。 你不用这套栈时,保留六个动作本身(它们与工具无关),把「spec」「Scenario」 替换成你项目里对应的契约文档与验收项即可。

由派发方做,不是 CLI。理由很直接:此刻改契约最便宜 —— 一行 spec 措辞 vs. 一个批次的返工 + 一次审计。 实测战绩:开工前核对一次抓出 3 处缺口(1 处规则留白 + 1 处规则相乘无规定 + 1 处命名与约束冲突), 全部在零代码状态下修掉。核对的六个动作:

  1. Scenario → 实现路径反查。把该域每条 Scenario 拆成 WHEN=<操作路径> / THEN=<可观测结果>, 逐条确认实现里真的存在那条路径。找不到 = 「有要求、无落点」。 ⚠️ 抄漏 WHEN 等于把该路径静默移出验收范围(实战:验收用了直接建库的成员,而 Scenario 的 WHEN 是 「经邀请码加入家庭」⇒ 真实路径零断言)。
  2. 规则相乘(找交集处无规定)。把该域的新规则与既有规则两两相乘:门禁 / 凭据类型 / 状态机 / 定时路径 / 可见性。交集处若无人规定,就是缺口 —— 它不在任何单个 Requirement 里,逐条读规格永远找不到。 实战:收口规则(两个请求时点)× 信号规则(「触发那次收口的响应」)⇒ 发现第二个时点(start)漏落点; 顺手实测确认「定时路径只做过期、不做收口」⇒ 排除并不存在的第三时点。
  3. 判据留白(凡「按…判定」而无数值或可判定谓词)。spec 出现「按…规模/权重/优先级判定」「视…而定」 而没有数字、没有可判定谓词时,Scenario 就是不可判定的,实现者只能自选阈值。 实战:「家庭版信号默认按主任务规模判定」——「规模」全篇无定义 ⇒ 该 Scenario 不可判定。 处置:或裁定一个判据(并把阈值写进 design),或裁定本变更取定值、只保留取值域; 两者都必须在契约里显式写「为什么否决另一个」(否则下一批会有人重新猜一次)。
  4. 命名与硬约束的一致性。任务里出现的名字若像列名(xxx_level / xxx_status)而契约说 「不落库 / 零 migration」,就是会让实现者加错东西的陷阱 —— 改名并显式写「不得新增列」。
  5. 占位断言普查(否则与你自己的「禁改既有测试」约束打架)。派发前跑一遍:
    grep -rn "不在本批\|待 [0-9]\|后续变更\|本批不" <测试目录>
    
    凡「为将来预留、届时必需更新」的断言(注释里写着「X.Y 不在本批」那种), 在本批契约生效的那一刻就会变成错的,实现方必然要在「守约束」与「守 spec」之间自行取舍 —— 实测:某批交付时实现方按 spec 改了 2 处占位断言,方向正确、覆盖面未削弱,但字面违反了硬约束, 逼得审计侧额外做一次裁定。 ⇒ 处置:在指令里点名列出这些占位断言(原断言 / 应改成什么 / 依据哪条 spec), 并给「禁改既有测试」加一条例外:「已被本批契约推翻的占位断言必须更新,且须在报告里逐条列出」。 区分判据:占位断言(注释自证「届时更新」)可改;既有行为的守护断言只许追加。 顺带:本批契约生效后,仓库里的过期注释也要一并更正(注释不实会误导下一批的审计者)。
  6. 「被测对象会不会被替身/桩换掉」(否则你新建的守卫可能是恒真的)。 派发前查一遍测试配置里的 moduleNameMapper / setupFiles / transform / 各类 mock:
    grep -rn "moduleNameMapper\|jest.mock\|__mocks__\|setupFiles" <项目>
    
    若本次的契约断言依赖某个包的真实行为(装饰器元数据、框架默认序列化、DI 解析顺序……), 而该包在测试环境里正是被替身替换的那个 ⇒ 你的断言会恒真通过,且任何变异都打不红它。 实测:某仓库为绕开一个 ESM 报错,把 @nestjs/swagger 映射成无操作替身 (ApiTags/ApiBearerExt/ApiExtension 全变 no-op)。本批要新建「从产物核对 41 条鉴权声明」的守卫 —— 替身在时,该守卫必然全绿,与实现是否正确无关。 ⇒ 处置(三选一,必须在指令里写明选哪个):① 让守卫改从真实包读元数据(可能要先修那条 ESM 报错的根因); ② 显式用 jest.mock/loader 局部打桩,只桩掉出问题的那一个子模块;③ 换一种不依赖该包的方式取证据。 MUST NOT 默认「守卫能读到真实行为」 —— 恒真断言比没有断言更糟:它会给你一个「已验证」的假信号, 而你的变异对照也会因为同一个替身而打不红(形成闭环自欺)。

三条出口:① 只是漏落点 ⇒ 直接补 tasks.md;② 涉及产品语义 ⇒ 问用户裁定,再落到 spec + design; ③ 任何改动后 openspec validate --strict 必须绿。 契约没补齐前不派发 —— 派出去的不是代码,是「一个替你猜契约的 agent」。


四、无头模式下的用户级 hooks:优先级最高的坑

如果宿主用户级配置(~/.<cli>/settings.json)里被工具(如团队脚手架)注入过 hooks, 无头运行会被它们掐死,典型症状:

  • 钩子命令本身是 CMD 语法(2>nul / exit /b 0),却由 bash 执行 → nul: Permission denied;
  • 钩子会等 stdin 到 EOF:stdin 关闭时 6.8s 返回,stdin 保持打开时 32.7s —— 超过钩子自带的 10s 超时 → 判定 UserPromptSubmit operation blocked by hook → 整轮失败。

绕法:只加载项目级设置(CodeBuddy CLI 为 --setting-sources project),牺牲用户级插件配置。 另外:无头运行一定要 </dev/null 重定向 stdin,别让子进程等着一个永远不关闭的 stdin。


五、把技能约定注入 system prompt(SDD / TDD / ponytail)

--append-system-prompt 接受一个字符串(注意:那个「从文件读系统提示」的参数会整体覆盖系统提示,不能用)。

因此做法是:仓库里放一份 skills-bootstrap.md,启动器每次把它读进来追加到 system prompt。

skills-bootstrap.md 模板

# 技能强制加载(本仓库约定)

执行任务时,**加载技能是第一步,未加载不得开始编辑文件**。
按任务性质选择(调用 Skill 工具,参数为技能名):

| 任务性质 | 必须加载 | 附加要求 |
|---|---|---|
| 改动规格 / 需求文档(spec / tasks / proposal) | `openspec`(**SDD**:规格驱动) | 改完必须跑校验:`<openspec validate --all --strict 的命令>`,有失败必须修到 0 |
| 任何编码任务(写代码 / 补测试 / 重构 / 修 bug) | `ponytail`(**纪律**:最小改动、YAGNI、不加未被要求的抽象) | 不引依赖、不加防御性冗余 |
| 多步开发(新功能 / 跨文件改造 / 测试体系) | `superpowers`(**TDD**:brainstorm → plan → TDD → review) | 测试先于或同于实现;结束自审 |
| 数据库连接 / 凭据配置 | `dbhub-setup` | 该技能可能平台特定(如 macOS+Codex);**不适用就说明理由并跳过**,不要硬套 |

## 汇报要求
最终报告必须有一行 **「已加载技能:…」**,列出本次**实际调用 Skill 工具**加载了哪些技能;
不适用要写 `不适用(理由)`。**不得谎报**。

## 例外
纯读取/查询类任务(读文件、grep、看日志、只读诊断)不必加载技能。

为什么要写「独立报告一行」:这是可审计的。事后能拿会话日志核对它到底调没调 Skill(见 §七), 谎报立刻暴露。


五之二、模型侧不可控项:哪些旋钮根本不存在

派活前必须知道的事:下面这些不是「设了没用」,是「根本没有入口」。

想调的东西 实际情况
--temperature / --top-p ❌ CLI 无此参数(2026-10-06 实测 --help)。走云端内置模型时由服务端决定,改不了
各家思考模式开关 ⚠️ 部分模型支持切换(DeepSeek/Kimi K2.x/GLM 的非思考模式),但 CLI 未暴露该开关;--effort none 不报错,是否真映射到厂商的 none 语义未验证
Kimi K3 的采样参数 🔒 官方明确「固定值,建议不要显式传入」(temperature=1.0/top_p=0.95/n=1/两个 penalty=0)—— 厂商侧锁死
DeepSeek 非思考模式降确定性 ⚠️ 只能用 temperature。官方原文:top_p only takes effect in thinking mode… In non-thinking mode it is fixed at 1.0 and the value you pass is ignored
models.json 的 temperature ⚠️唯一入口,但同 id 覆盖 = 完全替换整个模型定义(官方原文「配置 url 后即使与云端同 id 也会按完全替换语义生效」)。这是换模型,不是调参,别在派活里顺手做

⇒ 转化规则(重要):当派发方/自己提出「这批要更确定 / 更保守」时, 不要去找参数——把诉求改写成指令层约束:

想表达 ❌ 找参数 ✅ 改成
要更确定 调低 temperature --tools 白名单收窄可用动作 + 指令列「禁止改动文件清单」
要少发散 调低 temperature/effort 指令里写死「只按 A 方案实现,不做替代设计」+ 改动范围白名单
要仔细审查 调高 temperature 该阶段单独拆一批,或派 --agents 的审查子代理(§五之三)
要省成本 降 effort 到 low 缩任务体量、拆多批(§2.3);真要降档先确认不是超时的锅(§3.3)

五之三、子代理分档:在你指定的模型下做阶段化参数分配

这是「模型由派发方指定、参数由技能分配」的唯一已验证手段(2026-10-06 实测)。

--agents 里每个子代理可独立指定 model / effort / tools / maxTurns, 而主会话的 --model / --effort 保持不变 ⇒ 在不改变派发方所选模型的前提下,实现阶段级差异化。

node .workbuddy/bin/codebuddy-cli.js -p "$(cat prompt.md)" \
  --model <派发方指定的模型> --effort high \
  --permission-mode bypassPermissions \
  --agents '{
    "designer": {
      "description": "当被要求做方案设计时必须使用",
      "prompt": "你是架构师,负责设计方案并说明取舍理由。",
      "model": "<派发方指定的模型>", "effort": "max",
      "tools": ["Read", "Grep"]
    },
    "fixer": {
      "description": "当被要求修改文件时必须使用",
      "prompt": "你是实现工程师,直接改文件并运行验证命令。",
      "model": "<派发方指定的模型>", "effort": "low",
      "tools": ["Read", "Edit", "Bash"]
    }
  }' \
  --output-format json --max-turns 8 </dev/null

主会话也可只给 --tools 收窄动作空间(实测 --tools "Read,Glob,Grep" 只调Glob,未越界)。

三条行为事实(全部实测,都会让审计失真)

  1. --agents 是「授权调用」,不是「强制调用」 —— 主 agent 有权判断「不需要」就跳过。 实测:空目录 +「设计方案」类任务 ⇒ 子代理一个都没被调用,报告却写得很完整。 ⇒ 派活指令必须写「必须调用 X 子代理」;审计时数日志里 function_call 中 name == "Agent" 的次数 (同 §7.1 数 Skill 次数的套路),零次即任务未按要求执行。

  2. ⚠️ 不显式给权限档,它会「编理由放弃改动」 —— 实测:子代理带 Edit 但主命令未加 --permission-mode bypassPermissions 时,Edit 被拦,它的自述是 「两个子代理均已调用,但实际编辑被非交互权限限制阻断」,而文件实际未改。 补上权限档后同一任务 15 秒真改完文件。 ⇒ 这是 §零 铁律 3「不采信自述」的教科书案例,且是最阴的一种: 它诚实交代了阻断,但整份报告的基调让「子代理已被调用」看起来像任务已完成。 ⇒ 规则:凡写批次的派活必须显式给权限档;审计必须独立 cat 目标文件确认改动落地, 不能因为报告里列了工具调用序列就认定成功。

  3. Agent 工具入参只有 subagent_type,没有 effort / model 覆盖 —— 实测日志里该次调用参数仅 {'subagent_type': 'worker'}。 ⇒ 子代理的档位在 --agents 定义时就固定,主 agent 无法中途改。 想中途换档只有两条路:拆多批(§2.3)或恢复会话(§五之四)。


五之四、运行中途换档:-c 恢复会话 + 改 --effort(实测可行)

需求:「编码阶段用 low,测试阶段临时调高」——可以实现,但不在同一次运行内,而在两次运行的交界处。

# 第一批:编码,low
node .workbuddy/bin/codebuddy-cli.js -p "$(cat step1.prompt.md)" \
  --model <模型> --effort low --output-format json --max-turns 60 </dev/null >log1.json 2>&1

# 第二批:同一个工作目录,-c 恢复上一轮会话,只换effort
node .workbuddy/bin/codebuddy-cli.js -c -p "$(cat step2.prompt.md)" \
  --model <模型> --effort high --output-format json --max-turns 40 </dev/null >log2.json 2>&1

✅ 实测:第一轮 --effort minimal 记下编号 A1,第二轮 -c --effort high 准确读出 A1 ⇒ 上下文跨档位保留,换档有效。

⚠️ 三条限制:

  • -c 是「继续该目录最近的对话」,按目录定位,不按任务名 ⇒ 同目录下多批并行会串味, 此时必须改用 -r <session-id> 显式指定会话。
  • 第二批的 --max-turns 是新的上限,不继承第一批剩余额度。
  • 每批的日志要分别落盘(log1.json / log2.json),审计时逐批独立复核(§七)。

做不到的事:单次运行内部,主 agent 无法给自己或已派出的子代理改档。 需要「设计→编码→测试」三段不同档位时,要么拆三批,要么三段各配一个子代理(§五之三)。


六、权限模式与运行

  • 无人值守运行必须显式给「自动批准」档(CodeBuddy CLI 为 --permission-mode bypassPermissions), 否则会在某个确认点上原地等死。
  • 但:自动批准只应作用于这个独立 CLI,不要让宿主的审批体系一并放开。危险动作 (push --force / reset --hard / 删库 / 改生产)仍然要求人在对话里明确授权。
  • ⚠️ 省略这个参数的后果实测过:子代理的 Edit 会被拦,而它的自述是 「子代理已调用,但实际编辑被非交互权限限制阻断」,文件实际未改 —— 报告基调却像任务已完成。凡写批次必给权限档,审计必独立确认文件真改了(§五之三 事实2)。
  • 统一日志落盘,便于事后审计:
mkdir -p .cli-runs
LOG=".cli-runs/$(date +%H%M%S)-<任务名>.log"
timeout 1500 node .workbuddy/bin/codebuddy-cli.js \
  -p "$(cat .cli-runs/<任务名>.prompt.md)" \
  --model <派发方指定的模型> --effort high \
  --permission-mode bypassPermissions \
  --output-format json --max-turns 150 \
  </dev/null >"$LOG" 2>&1
echo "exit=$? bytes=$(wc -c <"$LOG")"
  • --output-format json 只在结束时落盘:运行中途日志是 0 字节属正常,不要据此判死。
  • 长任务用后台运行 + 完成通知,不要阻塞等。

七、审计与验收(别只看它自己写的总结)

7.1 会话记录在哪

无头运行的会话落在 CLI 自己的 projects 目录(如 ~/.codebuddy/projects/<工作区 slug>/*.jsonl)。 注意 JSON 结构:工具调用是 type == "function_call" + name / callId / arguments, 不是 tool_use,也不是 assistant.content[]。按错结构解析会得出「0 次工具调用」的错误结论。

# 统计一次运行真正调了哪些工具 / 有没有加载技能
import json, collections
data = json.load(open(log_path, encoding="utf-8", errors="replace"))
calls = collections.Counter()
def walk(o):
    if isinstance(o, dict):
        if o.get("type") == "function_call":
            calls[o.get("name")] += 1
        for v in o.values(): walk(v)
    elif isinstance(o, list):
        for v in o: walk(v)
walk(data)
print(calls)                  # 例:{'Edit': 19, 'Skill': 1, 'Read': 3, ...}
print("技能加载次数 =", calls.get("Skill", 0))

Skill 计数是 0 就说明技能根本没加载 —— 此时它自报的「已加载技能」是谎报。 进一步核验:找到 Skill 那条 function_call 的 callId,看对应 function_call_result 的 output.text 是不是真的有技能正文(而不是报错)。

7.2 独立验收(必做)

它说「测试全通过 / 脚本已实测」之后,自己再跑一遍那条命令,看输出对不对得上。 重点核对:它自述的数字(通过数、耗时、步骤数)与实际是否一致。 不一致 → 按未完成处理。

⚠️ Tests: 0 total 永远是环境问题,不是「测试通过」。复跑时必须先确认三件事, 否则你拿到的可能是一套根本没执行的测试:

  • 工作目录:在 monorepo 根跑 npx jest 常因根目录无 jest 配置而挂在 transform 阶段 (实测:根目录跑出 25 suites failed / Tests: 0 total,全是对 babel 的报错;换到 apps/api 下跑立刻 467 通过)。要跑 workspace 的测试就进 workspace 目录, 或走 workspace 脚本(npm test -w pkg)。
  • 依赖是否装好:node_modules 缺失/半装同样表现为 0 测试或整片suite 失败。
  • 是否被环境变量干扰:若你的宿主会向子进程注入 NODE_OPTIONS 之类的变量, 复跑时先unset 再跑(§一列了要剥哪些)。症状是「错误信息与代码无关」—— 典型如某个批量删文件的 shim 被注入,node 进程删文件时被拦下并挂起等确认。

⇒ 规则:复跑结果里凡出现 0 tests / 0 passed / 全 suite 失败,先当环境故障排查, MUST NOT 记作「通过」或「无回归」。 一个 suites failed = N, tests = 0 的输出不含任何关于 代码正确性的信息量。

7.3 对抗性探针 + 变异对照(这才是审计的主体)

📌 方法论通用,但示例命令来自 Jest(jest --json --outputFile、断言改常量值)。 别的栈的等价物:pytest 用 --junit-xml、Vitest 用 --reporter=json、 Go 用 go test -json。核心机制不变:让变体真的打红目标断言,并抓住失败用例的名字。

「独立复跑全绿」只证明它的测试通过;不证明契约被满足。要自己写探针,注入它没想到的违规形态:

  • 打它测试没打的「形状」:并发测试要问「我要撞的是哪条路径」—— 只并发同一个对象测出的是 CAS;要撞聚合判定(如「全部子任务完成 ⇒ 父收口」) 就必须并发两个不同对象,否则 CAS 让一方落败、判定只求值一次,竞态隐身。 带 body 的接口要问「字段的缺失形态测了吗」——若所有测试都 .send({}), 那「完全不传 body」这条路径从来没被走过。 → 规则:写完测试追问一句「这个测试有没有可能因为构造方式而绕过被测行为?」
  • 变异对照(mutation control):只有红过,绿才有意义。挑一处最承重的守卫, 用纯数据把它行为性关掉(清空常量数组、改枚举成员值),确认探针变红,再 cp 还原 (用 git diff 确认还原干净)。 ⚠️ 别改控制流:把 if (cond) 改成 if (false && cond) 会破坏 TS 的类型收窄 → 编译失败, 测试根本没跑,看着像红其实不是。改常量值 / 改枚举成员才不会连带破坏类型。 ⚠️ 变异「奏效」不等于断言正确 —— 变红之后还要问一句:「红的是不是针对这条条件的断言?」 实测反例:删掉某处 where 的 status 守卫,测试确实红了 2 条,但红的是幂等用例 (「已过期的行被重复命中、第二次 count ≠ 0」),守门本身仍无覆盖 —— 换一种破坏方式(保留条件、只把集合内容改错,如误把 COMPLETED 纳入可过期集)时,幂等仍成立、 测试全绿,缺陷漏过。判据:改坏「内容」时还红不红,而不是删掉「条件」时红不红 —— 只删条件,常被幂等 / 计数一类的旁证接住。作废条件前先确认有没有针对性断言。 推论:变异要让靶子变红,而不是「有东西变红」。 ⚠️ 「纯补验收」批另有一条判据:变异只打红对应的那一条。补断言批(禁改实现、只加测试)里 实现一秒未动 ⇒ 不能靠「测试变绿」自证,只能靠「改坏哪一格,只有那一条红」自证。 实测:两条新断言各配一处变异,各只打红自己那一条(用例 12 / 用例 13),既有 13 条全绿 —— 这才叫「这一格被钉住」。若某变异把新旧断言一起打红,只能说「有覆盖」。 ⇒ 本类批次的审计重点是 git status --porcelain <实现目录> 必须为空(非空即越权,退回); 且变异脚本要抓失败用例名(jest --json --outputFile),不是只数 failed 个数 —— 「红的是哪条」才是判据。 ⚠️ 靶集必须包含「承载该断言的测试文件」——否则「零红」不可信。 变异后零红有两种解释:(a) 真缺口(断言不存在);(b) 误报(断言存在,但你这次的靶集里没跑到那个文件)。 分不清就只能靠全量复核兜底。 实测:一次「非完成分支挂信号」的变异,靶集只选了 3 个 spec 文件 ⇒ 零红; 某 spec 里的用例确实覆盖了该路径,但不在靶集内 ⇒ 必须改用全量 465 条复核才能排除误报 (全量仍零红 ⇒ 确认是真缺口)。 ⇒ 两条操作规则:① 零红结论一律用全量复核一次再落槌; ② 已知断言所在文件时,优先只跑那个文件当靶集 —— 此时该文件内的零红是决定性的, 不必全量(既省时,又排除「靶集选窄」的解释)。 ⚠️ 正靶 + 负对照成对做,才能同时排除「断言空转」与「断言过宽」:
    • 正靶:改坏该断言覆盖的行为 ⇒ 该条必须红;
    • 负对照:改坏不属于该断言的行为(如同一字段的另一分支 / 另一 scope)⇒ 该条必须零红。 实测:给「非完成写请求不带信号」补断言后 —— 正靶(非完成分支挂信号)⇒ 两条全红; 负对照(家庭版 level 改值)⇒ 零红(该断言只该对 personal 分支敏感),而另 6 条家庭版用例红。 只有正靶会漏掉「断言写宽了、把不该管的情形也管住」这类错误。 另:正靶要挑「反向半边」也打一发 —— 若只断言「A 情形不带」,就去掉「A 之外情形该带」的那段实现; 打不红说明你只钉住了否定面(永远不带也满足你的断言)。 ⚠️ 变异锚点必须唯一:单行常量极易在多处重复 ⇒ 锚点要带类名/相邻行。 实测:负对照想改 CreateTagDto 的 @MaxLength(60, { message: '标签名长度不能超过 60 个字符' }), 该行在 CreateTagDto 与 UpdateTagDto 各出现一次 ⇒ 命中 2 次(靠 harness 的 「src.count(frm) != 1 即中止」自检拦下,否则会「改了 A 却以为在说 B」)。改为含 export class 的 多行锚点后通过。harness 必须内置这条自检(命中 ≠ 1 就中止,不许猜)——它是「锚点唯一性」的唯一防线。
  • 第三类缺口:有要求、有实现、无验收(与「有要求、无落点」「两条规则的交集无规定」并列)。 spec 正文写清了约束、实现也做对了,但没有任何断言把它钉住 ⇒ 只能靠变异对照发现 (逐条核对 Requirement↔清单查不出来,因为实现与落点都在)。发现后按缺口的惯例处理: 立条目 + 补断言/补 Scenario,且只加测试、禁改 src/**(实现已验证正确)。
  • 第二类缺口:两条规则的交集无规定 —— 最容易被漏,因为它不在任何单个 Requirement 里。 通用识别法:新引入的「判别类字段」(reason / 错误码 / 状态枚举 / level)必须与「全部门禁规则」求交集, 而不是只对着本次新写的那几道门设计取值。 实测:预览端点新引入 reason: 'NOT_SUPER_ADMIN' | 'CONSENT_REQUIRED',只覆盖了「角色门」与「同意门」, 漏掉先于它们命中的「凭据类型门」(设备令牌不得用于管理员关键操作,见 4.7)⇒ 超管用设备令牌调预览时,响应体 message(设备令牌文案)与 reason(NOT_SUPER_ADMIN)自相矛盾, 该字段沦为噪声。CLI 主动把它标为「已知小瑕疵、按最小改动保留」,审计不接受 —— 判据不是「403 返回错了」,而是该字段的唯一用途就是判别拒绝原因,报一个已知为假的值等于把字段做废。 ⇒ 操作化:把该字段的每个取值反查「哪种情形会落到它」,再拿全部门禁清单(本项目不止角色一条) 逐个过;漏掉先命中的门 = 缺口。
  • 第四类缺口:有要求、无判据(Requirement 与 Scenario 都在,但谓词没定义)。 spec 写「按…规模/权重/优先级判定」「视…而定」却没有数字、没有可判定谓词 ⇒ 该 Scenario 不可判定,实现者只能自选阈值,而任何自选值都无法被规格证伪(也就无法验收)。 识别法只用在派发前(见 §3.5 动作 3):逐条问「这条 Scenario 的判据是什么?在契约的哪一行?」 找不到 ⇒ 先裁定判据(或裁定取定值),再派发。审计阶段发现它时代价已高得多。
  • 看分布,不看单次:并发类断言必须「≥20 轮 + 双成功计数 = 0」。 单跑一次拿到「一成一败」是调度假象 —— 已两次实测:去掉 CAS 谓词后 30 轮里 29 轮双成功, 只有第 1 轮是「一成一败」。

7.4 它的自述分三类核实,三类都不豁免审计

自述类型 怎么核实
事实性陈述(版本号、环境、行数、字段名) 逐条回查。踩过:它写「目标机 Node 22」,实测是 v24。
「已知局限」(它主动交代的缺陷) 拿契约去判,不因为它「诚实地写出来了」就放行。关键问题:契约是否区分该情形。若契约的 SHALL 无条件成立,那它交代的就不是「设计权衡」,而是契约在该情形下被违反 ⇒ 仍是缺陷。踩过:它交代「并发完成两条不同子任务时可能双双漏收口」,实测 30 轮漏 29 轮,而 spec 是无条件 SHALL ⇒ 立为缺陷。
「不确定所以停下报告」 这是好行为,要奖励;但你仍要给出裁定,不能让它悬着。
审计者自己的中间输出(grep / head / tail 截断过的) 回查完整输出再下结论。截断会伪装成发现:踩过 grep … | head -20 只回显了前 20 行匹配,于是判定「这个 22 个端点的控制器全都没标注权限」,实际是第 21 行之后全都有 —— 差点据此报一个不存在的缺口并派一批活。⇒ 任何「零匹配 / 全都没… / 都缺…」的结论,必须用不带 head/tail 或带计数的完整命令复核(如 grep -c)。

7.5 「声明与实现不符」要用改配置再跑一遍来定性

发现注释/文档声称某机制生效、而读代码怀疑不生效时,不要靠读文档或记忆下结论: 把它改过去、跑一次全量,看测试怎么变。 实测例子:ValidationPipe 注释称 whitelist 会丢弃伪造字段,而源码返回的是未剥离的原始对象; 把它改成 transform: true 跑全量 → 384/386(只剩两个真缺陷)⇒ 一次同时证明了 「原判断成立」「改动修好了别的缺陷」「无回归」。 → 规则:凡涉及框架默认行为的判断,都必须有一个「改了它之后测试怎么变」的实测。

7.6 派发时的「预埋检查项」:问为什么这样选,不要只问选了哪个

派发那批之前,先把「审计时我要逐条过什么」写下来(预埋)——它既是审计清单,也是给 CLI 的隐含约束。

写法直接决定审计深度:

  • ❌ 「条件插入 还是 行锁?」——审计者只会形式核对「它确实用了行锁」,就此放过。
  • ✅ 「为什么这样选?给 schema 依据。」——才有可能收到「你给的选项里有一个在本 schema 下结构性不成立」 这种级别的回答。实测:task_assignee 上只有 (task_id, member_id) 唯一约束、没有「每行至多一条」的约束 (多负责人场景加不了),故 INSERT … WHERE NOT EXISTS 的谓词「尚无负责人」落在尚未存在的行上、 无唯一索引兜底 ⇒ READ COMMITTED 下两人各插一行、键不同互不冲突 ⇒ 写偏斜 ⇒ 条件插入根本不可用。

⇒ 规则:凡有 A/B 选型的地方,预埋项要连「为什么不选另一个」一起问。 只问「选了哪个」,审计退化成形式核对;问「为什么」,审计才可能改变结论。

7.7 验收类批次:映射表是「自我评价」,审计必修动作是抽查

当一批任务是「端到端验收 / 覆盖度盘点」,常见做法是让它先出一张覆盖映射表(逐验收点标 已覆盖 / 只覆盖单模块 / 未覆盖),再只补后两种。这个做法本身很省力,但埋着一个结构性作弊面:

表由它自己写、自己判。 把一条「其实只覆盖单模块」记成「已覆盖」,就能少写一条用例 —— 省事且无人察觉。

⇒ 审计必修动作(不能只看它那张表):

  1. 抽查 已覆盖 的行:至少随机 3 条(实测抽到 7 条更稳),回到它点名的用例标题, 打开 test 体确认「真的验了那个验收点」,而不是「该文件里恰好有一条相关用例」。
  2. 查承重行:表里凡有「A 判已覆盖、B 判断链」这类分界,分界依据的那条断言必须查实 —— 它错了,整张表的可信度就塌了。
  3. 查取证层次:证据落在 repository 层而验收点说的是 API 行为时,别急着判不实 —— 先看两者是否共用同一个谓词 (共用 ⇒ 可传递覆盖);同时确认夹具里确有反例数据(否则 toEqual([自己]) 类是空转断言)。
  4. 粒度防欠拆:已覆盖 + 单模块 + 未覆盖 = 验收点总数,且总数要能与契约正文的子句数对上 (过拆无害、欠拆才是藏缺口)。
  5. 它主动交代的灰区(「这里我没单测」)要给它裁定,不要悬着;裁定写成「维持 / 退回 + 理由」落到契约里。

7.8 收产物

  • 列一遍工作区改动,确认它改的与它说的一致;
  • 确认它声称没改的东西真的没改(尤其是门禁脚本、CI 配置这类「不许动」的文件);
  • 不要替它提交:git 写操作按你所在仓库的授权规则单独走。

八、失败模式速查

症状 根因 对策
0 字节输出、进程不退出 端口冲突(宿主已占)→ EADDRINUSE 未捕获 补 unset 漏掉的无前缀变量(SERVER__PORT 等)
报「未登录」但手跑正常 产品身份变量让它读了宿主的凭据文件 unset <产品身份配置>
UserPromptSubmit operation blocked by hook 用户级 hooks 语法/超时问题 --setting-sources project;stdin 重定向
跑 20+ 分钟、大量只读调用、零产出、日志有 499 canceled 单条请求超时(探索过深) 拆任务、喂结论、降思考档、加预算、提超时
自报「已加载技能」但审计计数为 0 只在对话里提了技能名 把约定写进 system prompt(§五)
「同工作区不能并行」 端口派生冲突 该说法已实测推翻(见 §二);真实约束是共享可变资源只有一份(库/契约/git)+ 审计可归因性 串行是默认档;并行是支持的,但须先做四隔离(目录+git / 依赖 / 数据库 / 端口)且写集不相交(§2.1)
结论与代码不符(如"已标记异常"其实没调) 只看自述 §七 独立复核,复跑 + 读代码
它的测试全绿但契约仍被违反 测试的形状绕开了触发条件(并发打同一对象 / 带 body 的测试全都 .send({})) 写对抗探针打「它没打的形状」+ 变异对照证明探针不空转(§7.3)
它主动交代了「已知局限」,就放行了 把「诚实」当成了「可接受」 拿契约判:契约若为无条件 SHALL,则仍立为缺陷(§7.4)
探针跑完污染了库 → 全局断言变红,误判成改动有副作用 探针 beforeAll 里的诊断代码抛异常 → 清理块没跑到 诊断与清理解耦;看到全局性断言失败先查库残留,再怀疑被测改动
全量/lint 门禁突然红,查来查去像是改动有问题 审计自己的探针文件还留在被测工程里(多为未使用导入一类),而它不在交付范围内 探针跑完立即挪出被测目录(如 test/ → 派发目录下的 probes/),再复验门禁;先排除自己的产物,再怀疑被测改动——与上面那条同源:审计方必须先清干净自己的痕迹
预埋检查项只问「选了 A 还是 B」 审计退化为形式核对,错失「某个选项结构性不成立」这类回答 追加问「为什么这样选、给依据」(§7.6)
契约里某条 MUST NOT 没有对应的验收条目 该条其实是非目标(如「不对非指派人渲染逾期红字」的渲染侧属 UI,留 PWA),只是没写清去哪 区分两类:「本该做却没人做」⇒ 立条目;「本不该做(非目标)⇒ 在条目内标注分流」。两类都要记录,但处置不同(§7.4 的推论)
逐条核对 Requirement↔清单,全都有落点,但系统仍泄露/出错 缺口藏在两条规则的交集:各自都有规定,却没规定它们相遇时谁继承谁 这类核对不出来,必须主动把两条规则的条件相乘去构造探针(例:「状态 A 的可见性」×「挂在 A 下的对象 B」——实测撞出「待审批 主任务下的子任务对全家可见」)(§7.3)
并行两批跑完,一方的改动莫名消失 / 提交里混进对方的半成品 两批共用一个 git 工作区:一方 git add 时另一方的改动还在工作树上 目录+git 隔离:各批各自 git worktree + 各自 commit,由主会话串行合并(§2.1)
并行两批同时 npm ci / prisma generate,一方报模块找不到或 TS 编译错 node_modules 与生成的 client 只有一份,被对方清空/重写 依赖隔离:各 worktree 各自 npm ci(§2.1)
并行两批同时跑测试,出现偶发红或 migrate 长时间阻塞 同一个库:prisma migrate 取 PG advisory lock 互等;测试 afterEach 互相删数据、全局计数断言被别的套件干扰 数据库隔离:独立 database 或连接串加 ?schema=<批次>(§2.1)
指令里写了「这批要保守 / 要少发散」,但产物照样跑偏 以为 temperature / effort 能当"发散度旋钮"用 —— 而它们要么不可调,要么不是这个作用 翻译成动作空间约束:改动范围白名单 + 工具白名单 + 指令里写死禁止项(§五之二转化规则)。不是去调参数
改了 --effort 但行为完全没变 ⚠️ 档位名拼错被静默吞掉:bogus-level 也返回 subtype: success,与合法档位结果完全一致,无任何日志痕迹 逐字核对档位名;用 --settings '{"reasoningEffort":"..."}' 交叉验证;仍无效应假设厂商侧忽略该参数(§3.3)
想要"省成本"于是降 effort,结果首过率大跌 把降档当成默认省钱手段 降档降低的是首过率不是"跑对"概率。先缩任务体量 / 拆多批;真超时才降且写进日志(§3.3)
报告说「N 个子代理均已完成」,但文件根本没改 未显式给权限档 → 子代理 Edit 被拦,而它编了个理由放弃并把基调写成"已完成" 凡写批次必给 --permission-mode;审计独立 cat 目标文件确认落地,不能只看工具调用序列(§五之三 事实2)
报告说「子代理已调用」,审计数 Agent 调用次数为 0 --agents 只是授权,主 agent 判断「不需要」就跳过(实测空目录场景) 指令写「必须调用 X 子代理」;审计数 name == "Agent" 的 function_call 次数,零次即未执行(§五之三 事实1)
用 reasoning_tokens 判断 effort 是否生效,结论摇摆 该指标是噪声:同模型同档位 3 次采样值在 0~150 乱跳,两档分布重叠 换别的判据;-p 模式下 --debug 不打请求体,只能靠可观测行为差异判断(§3.3)
三子代理串行的批次跑满 5 分钟没结束 串行子代理耗时是乘性叠加 每批 ≤ 2 个子代理(实测 3 个跑满 5 分钟未完,同任务 2 个 16 秒完)(§3.3)
用 -c 恢复会话做第二批,串到了别的事 -c 按当前工作目录定位最近对话,同目录多批会串味 同目录多批并行时改用 -r <session-id> 显式指定会话;每批日志分别落盘(§五之四)

九、交付时的交代清单

给用户汇报时,至少要说清这六件事,缺一样都算没交付:

  1. 跑了几次、每次多久、退出码、日志路径;
  2. 技能审计结果(Skill 调用次数及加载了哪些);
  3. 用的哪个模型(派发方指定的 ID 或别名)+ 每批的 effort 档位与实际厂商映射 (medium 在 DeepSeek/GLM/Kimi 上会变成 high,汇报时要写实际生效的档);
  4. 它改/加了哪些文件(路径),以及你独立复跑的结果;
  5. 它声称但你没验证的部分,明确标为未验证;
  6. 待授权 / 待拍板的动作(git 写、外部系统、生产改动)。

凡派了子代理,额外必须说:Agent 调用次数(零次即被跳过);写批次是否独立确认过文件真的改了。

十、检查清单(复制走)

□ 启动器存在,且最小指令能在数秒内返回
□ **最小验证已跑通且 stderr 干净**
  (`node .workbuddy/bin/codebuddy-cli.js -p "回答一个字:好" --output-format json`;
   若出现 Git Bash 提示,说明它没探测到 bash,按 §一的自查清单处理)
□ **参数风格已确认**:`-Prompt/-Model/...` 具名写法与 `-p/--model/...` 原生写法都能用
  (防"照抄另一版示例"导致 `unknown option`,见 §一七轮翻车第 ⑦ 轮)
□ **若本次改过启动器:已改完立即重跑一次**(修 bug 后不重跑=可能把可用改不可用,见 §一七轮翻车复盘)
□ **模型由派发方显式指定**(`--model <id>` 或三个别名之一);技能没有替派发方选模型(§零 铁律 4)
□ **并行度已确认**:单写批 = 1 个实例;>1 时已逐项完成四隔离(目录+git / 依赖 / 数据库 / 端口)
□ **并行批写集不相交**,且不含 `openspec/**`(契约批独占,其余批先停下等它合并)
□ 指令里有:目标 / 已知结论 / 改动范围 / 预算 / 验证命令 / 产物
□ **改动范围与工具白名单已写死**(要"保守/少发散"时靠约束,不靠调参数——§五之二)
□ `--effort` 档位名**逐字核对**过;`--settings '{"reasoningEffort":"..."}'` 可作交叉验证(§3.3)
□ **每批子代理数 ≤ 2**(3 个串行实测跑满 5 分钟未完)
□ **派发前已做契约核对**(§3.5,六动作):Scenario 的 WHEN 与 THEN 都有落点;新规则与既有规则相乘过;
  无「按…判定」的判据留白;命名与「不落库」类约束不冲突;**占位断言已普查并列进指令**;
  **已确认被测对象在当前测试环境里没被替身/桩换掉**(否则新守卫恒真、变异也打不红)—— 补完契约且 `validate --strict` 绿
□ 技能约定已进 system prompt(不是只在对话里提)
□ 三原则已复述:**SDD**(列出该批契约文件)/ **TDD**(先枚举判据再一条判据一条用例)/ **ponytail**(不引新依赖、不提前抽象)
□ stdin 已 </dev/null,用户级 hooks 已隔离
□ **权限档已显式给出,危险动作仍未放开**(不给权限档,子代理会编理由放弃改动——§五之三 事实2)
□ 日志落在固定目录,--output-format json
□ 审计 Skill 调用次数 + 结果正文
□ **凡派了子代理:审计 `Agent` 调用次数**(零次即"授权非强制"下被跳过——§五之三 事实1)
□ **凡写批次的派活:独立 `cat`/`git diff` 确认文件真改了**,不能只信报告里的工具调用序列
□ 独立复跑它的验证命令,比对数字;**出现 `0 tests` / 全 suite 失败先当环境故障,不得记作通过**
□ **结论前回查完整输出**(凡 `grep`/`head`/`tail` 截断过的中间产物,不得直接当证据)
□ 预埋检查项含「**为什么**这样选」而非只问「选了哪个」
□ 变异对照至少做一处(只有红过,绿才有意义);**零红须用全量复核排除「靶集选窄」的误报**
□ **正靶 + 负对照成对**(负对照零红 ⇒ 断言未过宽);正靶别只打否定面(补一发「反向半边」)
□ **变异锚点唯一**(单行常量易多址重复 ⇒ 带类名/相邻行;harness 内置「命中≠1 即中止」自检)
□ **验收类批次**:抽查映射表里标 `已覆盖` 的行回原点名用例(含承重的分界依据行)
□ 汇报含「未验证项」与「待授权项」
□ **汇报里写明用的是哪个模型**(派发方指定的 ID 或别名)、每批的 effort 档位与实际厂商映射
□ **若用了「中途换档」(`-c` 恢复):确认是显式 `-r <session-id>` 或该目录下只有这一条会话链**