@shgroup/dsh-serenity-hooks 1.56.9 → 1.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +2 -2
  2. package/assets/README.md +4 -4
  3. package/assets/serenity-voyage.html +3 -4
  4. package/dsh.plugin.json +3 -3
  5. package/lib/agent-idle.d.ts +2 -2
  6. package/lib/api.d.ts +0 -1
  7. package/lib/{ccc-DXky9UZu.js → ccc-B0ZGL9lx.js} +30 -13
  8. package/lib/{ccc-roots-Bo0HKLlM.js → ccc-roots-BC8mn8iM.js} +3 -3
  9. package/lib/ccc-roots.d.ts +1 -1
  10. package/lib/ccc.d.ts +34 -12
  11. package/lib/cheap-worker-ops.d.ts +49 -0
  12. package/lib/cheap-worker-preset-inherit.d.ts +35 -0
  13. package/lib/client.js +11 -99
  14. package/lib/cro-guide.d.ts +4 -2
  15. package/lib/git-ops.d.ts +1 -1
  16. package/lib/host/access.d.ts +2 -2
  17. package/lib/im-weixin.d.ts +10 -3
  18. package/lib/index.js +211 -592
  19. package/lib/invariant.d.ts +1 -1
  20. package/lib/invariant.js +1 -1
  21. package/lib/localstore-ops.d.ts +1 -1
  22. package/lib/{message-source-BExngUPX.js → message-source-BcCRsEOh.js} +3 -3
  23. package/lib/rebuild.d.ts +1 -1
  24. package/lib/seams/context.d.ts +2 -2
  25. package/lib/skiff-core.d.ts +1 -1
  26. package/lib/{skiff-role-BSg6Qu9k.js → skiff-role-B615L9MV.js} +3 -3
  27. package/lib/skiff-role.d.ts +2 -2
  28. package/lib/skills-discovery.d.ts +2 -3
  29. package/lib/status.d.ts +2 -2
  30. package/lib/tools/cheap-worker.d.ts +21 -0
  31. package/lib/tools/skiff-admin.d.ts +2 -2
  32. package/lib/unattended-ops.d.ts +4 -4
  33. package/lib/voyage-page.d.ts +3 -4
  34. package/lib/{weixin-route-BNyH5ghC.js → weixin-route-B9gU1YXz.js} +22 -6
  35. package/lib/weixin-send-api.d.ts +1 -1
  36. package/package.json +2 -2
  37. package/lib/handyman-ops.d.ts +0 -97
  38. package/lib/handyman-preset-inherit.d.ts +0 -35
  39. package/lib/tools/handyman.d.ts +0 -24
package/lib/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
2
2
  import { a as hostSessions, c as hostWeb, i as hostService, l as hostWebServer, n as hostInjected, o as hostSettings, r as hostLlm, s as hostSubagents, t as hostAgents } from "./access-CNccNtgf.js";
3
- import { _ as resolveInside, a as findSerenityRoot$1, c as matchBlacklist, d as readCccName$1, f as readExclusiveTools, h as readTrajectorySkills, i as findGitRoot, l as pathInside, m as readSafeModeHiddenTools, n as SAFE_MODE_MARKER, o as isSafeModeOn, p as readHandymanConfig, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-DXky9UZu.js";
4
- import { i as cccRootForExec, n as agentCwdFor, o as listCccs, r as cccRootForCwd, t as NO_CCC_FROM_AGENT_CWD } from "./ccc-roots-Bo0HKLlM.js";
5
- import { A as localIdStamp, C as setActiveSessionInfo, D as isoLocal, E as useSession, O as localFileStamp, S as sessionsRoot, T as summarize, _ as namingTitleFor, a as pruneMissingBindings, b as resolveSessionByTitle, c as setBindingStore, d as clearActiveSessionInfo, f as createSession, g as listSessions, h as getActiveSessionInfo, i as listBoundSessionIds, k as localHuman, l as DEFAULT_SESSION_SCOPE, m as findSession, n as PLUGIN_SOURCE, o as readLastBound, p as extractSessionMdPathFromText, r as appendBound, s as resolveSessionTrajectoryLabel, t as ACC_MESSAGE_KIND, u as TRAJECTORY_ACTIONS, v as parseSessionContextFromEvents, w as showSession, x as sessionEvents$1, y as readActiveSessionMd } from "./message-source-BExngUPX.js";
6
- import { a as readWeixinCredential, c as weixinInboundDir, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, g as runLocalStore, h as readStore, i as matchWeixinRoute, l as weixinSessionIdFor, m as readGitTrack, n as extractWeixinText, o as readWeixinSettings, p as localstorePath, r as hasVoiceItem, s as sanitizeFileName$1, t as extractWeixinMedia } from "./weixin-route-BNyH5ghC.js";
3
+ import { _ as resolveInside, a as findSerenityRoot$1, c as matchBlacklist, d as readCccName$1, f as readCheapWorkerConfig, h as readTrajectorySkills, i as findGitRoot, l as pathInside, m as readSafeModeHiddenTools, n as SAFE_MODE_MARKER, o as isSafeModeOn, p as readExclusiveTools, r as classifyPath, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS, u as readBlacklist } from "./ccc-B0ZGL9lx.js";
4
+ import { i as cccRootForExec, n as agentCwdFor, o as listCccs, r as cccRootForCwd, t as NO_CCC_FROM_AGENT_CWD } from "./ccc-roots-BC8mn8iM.js";
5
+ import { A as localIdStamp, C as setActiveSessionInfo, D as isoLocal, E as useSession, O as localFileStamp, S as sessionsRoot, T as summarize, _ as namingTitleFor, a as pruneMissingBindings, b as resolveSessionByTitle, c as setBindingStore, d as clearActiveSessionInfo, f as createSession, g as listSessions, h as getActiveSessionInfo, i as listBoundSessionIds, k as localHuman, l as DEFAULT_SESSION_SCOPE, m as findSession, n as PLUGIN_SOURCE, o as readLastBound, p as extractSessionMdPathFromText, r as appendBound, s as resolveSessionTrajectoryLabel, t as ACC_MESSAGE_KIND, u as TRAJECTORY_ACTIONS, v as parseSessionContextFromEvents, w as showSession, x as sessionEvents$1, y as readActiveSessionMd } from "./message-source-BcCRsEOh.js";
6
+ import { a as readWeixinCredential, c as weixinInboundDir, d as LOCALSTORE_SCOPES, f as checkLocalstoreGitCompliance, g as runLocalStore, h as readStore, i as matchWeixinRoute, l as weixinSessionIdFor, m as readGitTrack, n as extractWeixinText, o as readWeixinSettings, p as localstorePath, r as hasVoiceItem, s as sanitizeFileName$1, t as extractWeixinMedia } from "./weixin-route-B9gU1YXz.js";
7
7
  import { a as unregisterSkiffSession$1, i as skiffSessionSnapshot$1, n as skiffRoleFor$1, o as ACC_VERSION, r as skiffSessionInfo$1, t as registerSkiffSession$1 } from "./skiff-registry-BjIX_KD1.js";
8
- import { a as readSkiffRoles, c as roleMsmWhitelist, d as systemPromptSource, f as trajectorySubset, i as isSkiffSessionId, l as roleToolWhitelist, n as buildSkiffBasePrompt, o as resolveRoleSystemPrompt, r as createRolePromptReader, s as resolveSkiffKind } from "./skiff-role-BSg6Qu9k.js";
8
+ import { a as readSkiffRoles, c as roleMsmWhitelist, d as systemPromptSource, f as trajectorySubset, i as isSkiffSessionId, l as roleToolWhitelist, n as buildSkiffBasePrompt, o as resolveRoleSystemPrompt, r as createRolePromptReader, s as resolveSkiffKind } from "./skiff-role-B615L9MV.js";
9
9
  import { a as ACP_HTTP_PORT, c as MAIN_WEB_PORT, d as WEIXIN_SEND_PORT, l as MECHANISM_PORTS, n as readSimpleSettings, o as FACE_PORTS, r as registerSettingsSection, s as GATEWAY_PORT, u as SKIFF_DEBUG_PORT } from "./settings-section-xHABl8nk.js";
10
10
  import { REGISTERED_TOOLS } from "./invariant.js";
11
11
  import { a as markdownToPlainText, c as sendTyping, i as getUpdates, l as sniffImageExt, n as downloadMedia, o as sendFileMessage, r as getConfig, s as sendTextMessage, t as TypingStatus } from "./weixin-api-BrRcPiQs.js";
@@ -623,7 +623,7 @@ function createKitTool(ctx) {
623
623
  * - diff:git diff [--cached] [<ref>] [-- <path>]
624
624
  * 保留 dsp 增强:localstore git 合规联动(S134:deny 且 .gitignore 未覆盖 → status 提示 / commit 拒绝)。
625
625
  *
626
- * 🔴 2026-09-26 修(tiangong-serenity 报障):**push 曾把"超时被杀"报成成功**。
626
+ * 🔴 2026-09-26 修(由另一个 CCC 实例报障):**push 曾把"超时被杀"报成成功**。
627
627
  * 机制(**实测**,非推断):`spawnSync` 超时**不设 `err.killed`**(实测 `code=ETIMEDOUT` /
628
628
  * `signal=SIGTERM` / `status=null` / `stderr` 长度 0),而 `execFileSync` **成功时丢弃 stderr**;
629
629
  * 于是"超时被杀"与"真成功"都交出 `{stdout:'', stderr:''}` —— 旧判据 `if (!stderr) → 成功`
@@ -642,7 +642,7 @@ const GIT_ACTIONS = [
642
642
  ];
643
643
  /** 本地动作超时(ms) */
644
644
  const DEFAULT_LOCAL_TIMEOUT_MS = 3e4;
645
- /** 网络动作(fetch/push/pull)超时(ms)——大体量推送可能远超 30s(tiangong 实例:>60s) */
645
+ /** 网络动作(fetch/push/pull)超时(ms)——大体量推送可能远超 30s(实测某实例 >60s) */
646
646
  const DEFAULT_NET_TIMEOUT_MS = 18e4;
647
647
  /**
648
648
  * 超时可覆盖(`DSH_localTimeout()` / `DSH_netTimeout()`)。
@@ -1083,14 +1083,14 @@ Directory of ACC capabilities — each area lists where to go for details (guide
1083
1083
  与 praxis ontology 的分工 = 那个是**方法文本**、这个是**能跑的那个**)
1084
1084
  ③ 工具与执行 → container_fs(容器文件系统 15 子命令)/ container_git(git)/ dashboard(health 含注册表检查/time/wait)
1085
1085
  acc_msm 已并入 msm(执行+发现)与 container_admin(管理)——msm("<name>", ["<args>"]);inspect=true 查用法
1086
- handyman(杂工 agent 编排——guide 子命令:handyman guide)
1086
+ cheap-worker(杂工 agent 编排——guide 子命令:cheap-worker guide)
1087
1087
  专属工具(**默认对所有 CCC 隐藏**,只有在自己 exclusiveTools 里点名的 CCC 可见)→ acc-diag(运行态诊断)
1088
1088
  ④ 角色与对外面 → container_admin role(Skiff 认知子集角色:guide/validate/apply/list——skiff_admin 已并入)
1089
1089
  ACP(程序化 JSON-RPC 3100)/ Skiff 问答页(公网 ask)——面板「外部能力」组
1090
1090
  ⑤ 自主与接入 → container_trajectory send-now(跨轨迹的**即时**递送,**不排队**:live 目标在跑就当场注入当前轮、空闲则立即起轮;非 live 冷载入——等效于直接 wake;**有同步回执**)
1091
1091
  container_trajectory send-later(跨轨迹的"未来时刻 + 一条 message";fire-and-forget,无回执无回收)
1092
1092
  weixin 微信桥(配置/扫码/路由/消息 hook——CCC 侧 weixin-doctor MSM:msm("weixin-doctor", ["guide"]))
1093
- ⑥ CCC 配置总览 → container_admin msm ccc-config(9 段:handyman/sessionKeeper/localstore/hooks/safeMode/skiff/trajectory/weixin/**exclusiveTools**)
1093
+ ⑥ CCC 配置总览 → container_admin msm ccc-config(9 段:cheap-worker/sessionKeeper/localstore/hooks/safeMode/skiff/trajectory/weixin/**exclusiveTools**)
1094
1094
  ⑦ 注册表与安全 → mech-registry.json 由 container_admin msm register/deregister 管理(写保护——不可直接编辑)
1095
1095
  dashboard health 输出 registry 完整性检查;损坏恢复指引见 health registry.issues
1096
1096
  CCC 级开关:exclusiveTools(声明专属工具)/ safeMode.blacklist(写路径黑名单)
@@ -1158,24 +1158,27 @@ const CCC_CONFIG_REFERENCE = `═══ CCC Configuration Reference ═══
1158
1158
  CCC-level features are configured in .opencode/serenity.json.
1159
1159
  Below are all available configuration sections.
1160
1160
 
1161
- ── 1. handyman.models ──
1162
- handyman(杂工)工具可用模型白名单(provider/model 列表);未配置时 handyman 报错要求配置。
1163
- 缺省模型 = models[0](可用 handyman.defaultModel 指定,必须 ∈ models)。
1164
- **CCC 应把该白名单指向低成本模型**——handyman 是批量执行通道(v1.31.3 双模式共用同一白名单)。
1161
+ ── 1. cheap-worker.models ──
1162
+ cheap-worker(杂工)工具可用模型白名单(provider/model 列表);未配置时 cheap-worker 报错要求配置。
1163
+ 缺省模型 = models[0](可用 cheap-worker.defaultModel 指定,必须 ∈ models)。
1164
+ **CCC 应把该白名单指向低成本模型**——cheap-worker 是批量执行通道。
1165
+ 🔴 模型**不可由调用方指定**(工具无 model 参数)⇒ 本白名单是模型选择的唯一来源。
1165
1166
 
1166
- Modes(v1.31.3,缺省 foreground):
1167
- mode="foreground"(缺省):一次前台串行委派(宿主委派服务 ctx.subagents.start("spawn") +
1167
+ Behaviour(2026-10-14:background 模式已删除,只剩前台委派):
1168
+ cheap-worker(task, [label]):一次前台委派(宿主委派服务 ctx.subagents.start("spawn") +
1168
1169
  agentOptions 注入模型)——不循环、不校验完成码、不写进度文件;返回子 agent 最终文本。
1169
- 适合"一次调用一次结果"(逐用例回归、单次转换)。
1170
- mode="background":既有循环校验实现(内部 while 硬循环 + 随机完成码唯一判据 + 轮次上限
1171
- 默认 100 可续跑 + 异常自动重启 ≤100 + 进度文件 AGENT_SESSIONS/handyman-<label>.md/.json +
1172
- jobs 并行 maxParallel 默认 10)。适合长任务/无人值守/需抗提前收工。
1170
+ cheap-worker(jobs=[{task,label?},...]):并行扇出,上限 10(硬编码),结果保持传入顺序。
1171
+ ⇒ **无 background 模式、无进度文件、无续跑**(owner 2026-10-14 令「删除后台运行能力」)。
1172
+
1173
+ Old key(兼容):
1174
+ 读不到 "cheap-worker" 时**回落到旧键 "handyman"**(同形状:models / defaultModel)。
1175
+ 旧键里的 "maxRounds"/"maxParallel" 已随 background 退场,留着无害但不再生效。
1173
1176
 
1174
1177
  Config:
1175
- { "handyman": { "models": ["provider/model-name"] } }
1178
+ { "cheap-worker": { "models": ["provider/model-name"] } }
1176
1179
 
1177
1180
  Example:
1178
- { "handyman": { "models": ["minimax-cn-coding-plan/MiniMax-M3"], "defaultModel": "minimax-cn-coding-plan/MiniMax-M3", "maxRounds": 100, "maxParallel": 10 } }
1181
+ { "cheap-worker": { "models": ["minimax-cn-coding-plan/MiniMax-M3"], "defaultModel": "minimax-cn-coding-plan/MiniMax-M3" } }
1179
1182
 
1180
1183
  ── 2. sessionKeeper.threshold / sessionKeeper.sessionMdMaxKB ──
1181
1184
  trajectory-assistant(TRAJECTORY-ASSISTANT · CHECKPOINT)提醒机制的积分阈值(非 headless 主 agent)。
@@ -1324,7 +1327,7 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
1324
1327
  "autoReplyWithLastMessage": true,
1325
1328
  "fallbackOnNoSend": false,
1326
1329
  "accounts": [{ "accountId": "wechat-1", "name": "家庭助手", "enabled": true }],
1327
- "routes": [{ "user": "*", "role": "zhaocai" }]
1330
+ "routes": [{ "user": "*", "role": "my-role" }]
1328
1331
  } }
1329
1332
 
1330
1333
  ▸ weixin.hook(消息记录 hook,v1.27.13):
@@ -1347,7 +1350,7 @@ CCC 级微信个人号接入(iLink 协议):dsh 一进程多 CCC,每 CCC
1347
1350
  事件 schema(**不含任何会话凭据**——context_token/bot_token/token/aes_key 均不出现):
1348
1351
  { "event": "incoming", "ts": 1788359941488, "cccRoot": "/path/ccc",
1349
1352
  "accountId": "wechat-1", "userId": "u1@im.wechat",
1350
- "sessionId": "skiff-weixin-xxx", "role": "zhaocai",
1353
+ "sessionId": "skiff-weixin-xxx", "role": "my-role",
1351
1354
  "message": { "text": "你好", "media": [{ "kind": "image", "relPath": "_tmp/weixin-inbound/<hash>/img_x.jpg" }] } }
1352
1355
  { "event": "outgoing", "ts": ..., "cccRoot": ..., "accountId": ...,
1353
1356
  "userId": ..., "sessionId": ..., "role": ...,
@@ -1821,77 +1824,14 @@ async function runMsmAsync(root, args) {
1821
1824
  }
1822
1825
  }
1823
1826
  //#endregion
1824
- //#region src/handyman-ops.ts
1827
+ //#region src/cheap-worker-ops.ts
1825
1828
  /**
1826
- * handyman-ops.ts — handyman(杂工)纯逻辑层(零 DSH 依赖,可独立单测)
1829
+ * cheap-worker-ops.ts — cheap-worker(杂工)纯逻辑层(零 DSH 依赖,可独立单测)
1827
1830
  *
1828
- * v1.24.0:loop(牛马)→ handyman(杂工)重命名。语义对齐 osp loop:
1829
- * 进度文件(handyman-<label>.md/.json)、续跑、轮次 prompt 结构、stop token。
1830
- * 不兼容旧 loop- 进度文件(用户拍板:仅新 handyman- 前缀)。
1831
+ * 🆕 2026-10-14(owner 令「删除后台运行能力」):**进度文件 / stop token / 轮次循环**整族已退场 ——
1832
+ * 它们**只**服务于 background 模式。本模块现存 = 前台委派真正需要的三件:
1833
+ * 模型串解析 / 白名单校验 / provider 可用性判读(+ 使用指引文本)。
1831
1834
  */
1832
- /** label 脱敏(Windows 审计问题 17):非法字符 → '-',去尾点/空格,限长(按码点截断,修复代理对切散 U+FFFD) */
1833
- function sanitizeLabel(label) {
1834
- return [...label.replace(/[<>:"/\\|?*\u0000-\u001f]/g, "-").replace(/[ .]+$/g, "")].slice(0, 50).join("");
1835
- }
1836
- function handymanProgressPaths(root, label) {
1837
- const dir = join(root, "AGENT_SESSIONS");
1838
- const safe = sanitizeLabel(label);
1839
- return {
1840
- md: join(dir, `handyman-${safe}.md`),
1841
- json: join(dir, `handyman-${safe}.json`)
1842
- };
1843
- }
1844
- /** 读取进度(续跑);无文件返回 round 0 */
1845
- function readProgress(root, label) {
1846
- const { json } = handymanProgressPaths(root, label);
1847
- if (!existsSync(json)) return null;
1848
- try {
1849
- return JSON.parse(readFileSync(json, "utf-8"));
1850
- } catch {
1851
- return null;
1852
- }
1853
- }
1854
- function writeProgress(root, label, p) {
1855
- const { md, json } = handymanProgressPaths(root, label);
1856
- mkdirSync(join(root, "AGENT_SESSIONS"), { recursive: true });
1857
- writeFileSync(json, JSON.stringify({
1858
- ...p,
1859
- status: p.status ?? "running",
1860
- updated: isoLocal()
1861
- }, null, 2) + "\n", "utf-8");
1862
- const lines = [
1863
- `# handyman: ${label}`,
1864
- `- Model: ${p.model}`,
1865
- `- Round: ${p.round}`,
1866
- `- Done: ${p.done}`,
1867
- "",
1868
- `## Latest response`,
1869
- "",
1870
- p.lastResponse,
1871
- ""
1872
- ];
1873
- writeFileSync(md, lines.join("\n"), "utf-8");
1874
- }
1875
- /** 失败状态落盘(对齐 osp writeFailedStatus:done=true / status=failed / errorCode) */
1876
- function writeFailedStatus(root, label, info) {
1877
- const { json } = handymanProgressPaths(root, label);
1878
- mkdirSync(join(root, "AGENT_SESSIONS"), { recursive: true });
1879
- const prev = readProgress(root, label);
1880
- writeFileSync(json, JSON.stringify({
1881
- round: prev?.round ?? 0,
1882
- done: true,
1883
- label,
1884
- model: prev?.model ?? "",
1885
- status: "failed",
1886
- errorCode: info.errorCode,
1887
- errorMessage: info.errorMessage,
1888
- updated: isoLocal(),
1889
- lastResponse: prev?.lastResponse ?? ""
1890
- }, null, 2) + "\n", "utf-8");
1891
- }
1892
- function newStopToken() {
1893
- return `SERENITY_HANDYMAN_DONE_${randomBytes(8).toString("hex")}`;
1894
- }
1895
1835
  /** 解析 model 字符串(provider/model)→ {provider, model};无 / 视为 model-only */
1896
1836
  function splitModel(model) {
1897
1837
  const idx = model.indexOf("/");
@@ -1906,13 +1846,13 @@ function splitModel(model) {
1906
1846
  }
1907
1847
  /** 校验模型在白名单内;不在 → 抛错(用户拍板:只能使用 CCC 配置的模型) */
1908
1848
  function requireWhitelistedModel(model, models) {
1909
- if (!models.includes(model)) throw new Error(`handyman: model "${model}" is not in the CCC whitelist. Configure .opencode/serenity.json "handyman.models" with one of: ${models.join(", ")}`);
1849
+ if (!models.includes(model)) throw new Error(`cheap-worker: model "${model}" is not in the CCC whitelist. Configure .opencode/serenity.json "cheap-worker.models" with one of: ${models.join(", ")}`);
1910
1850
  }
1911
1851
  /**
1912
1852
  * providerAvailabilityError — 判定「**本机有没有该 provider 的 adapter**」,并产出**可执行**的错误文本。
1913
1853
  * 返回 `null` = 全部可用(调用方照常继续)。
1914
1854
  *
1915
- * 🔴 为什么需要它(S142 2026-09-25,owner 在 **Mac** 上实测):`handyman` 的模型白名单写在
1855
+ * 🔴 为什么需要它(S142 2026-09-25,owner 在 **Mac** 上实测):`cheap-worker` 的模型白名单写在
1916
1856
  * **CCC**(`.opencode/serenity.json`,**随 git 走**),而 provider adapter 由**每台机器的宿主**
1917
1857
  * 注册(profile 配置 / settings 命名空间)⇒ 同一份白名单在一台机器可用、在另一台必失败。
1918
1858
  * 实测症状:宿主抛 `no adapter registered for provider "<p>"`(`dsh-llm`,code `NO_ADAPTER`),
@@ -1938,11 +1878,11 @@ function providerAvailabilityError(models, facts) {
1938
1878
  const configPath = facts.configPath ?? "<ccc>/.opencode/serenity.json";
1939
1879
  const missingList = missing.map((p) => `"${p}"`).join(", ");
1940
1880
  return [
1941
- `handyman: provider ${missingList} has no adapter registered on this machine.`,
1881
+ `cheap-worker: provider ${missingList} has no adapter registered on this machine.`,
1942
1882
  "",
1943
1883
  " Requested model(s) :",
1944
1884
  ...requested.map((e) => ` ${e.raw}`),
1945
- ` Configured in : ${configPath} → "handyman.models" / "handyman.defaultModel"`,
1885
+ ` Configured in : ${configPath} → "cheap-worker.models" / "cheap-worker.defaultModel"`,
1946
1886
  "",
1947
1887
  " Why this happens : the whitelist lives in the CCC (it travels with the repository), while",
1948
1888
  " provider adapters are registered per machine by the host. An entry that",
@@ -1956,135 +1896,75 @@ function providerAvailabilityError(models, facts) {
1956
1896
  "",
1957
1897
  " Fix (pick one, then retry):",
1958
1898
  ` 1. Point the whitelist at a provider that IS registered here — edit ${configPath}`,
1959
- " (\"handyman.models\", and \"handyman.defaultModel\" if that is what you relied on).",
1899
+ " (\"cheap-worker.models\", and \"cheap-worker.defaultModel\" if that is what you relied on).",
1960
1900
  ` 2. Register/activate provider ${missingList} on this machine (host profile config,`,
1961
1901
  " or the settings namespace listed above).",
1962
1902
  "",
1963
1903
  " Do not retry unchanged — the same call fails identically."
1964
1904
  ].join("\n");
1965
1905
  }
1966
- /** 轮次 prompt(对齐老 loop 结构:回顾进度 → 自由工作 → 汇报;S134 EAP 化:固定详尽) */
1967
- function buildRoundPrompt(opts) {
1968
- const { root, session, label, round, stopToken, progress, task } = opts;
1969
- const resumeNote = progress && progress.round > 0 ? `Previous round (round ${progress.round}) completed: ${progress.lastResponse.slice(0, 300)}\nAlways continue from where you left off; never redo completed work.` : "This is the first round.";
1970
- return `# ${label} — handyman round ${round}
1971
-
1972
- CCC root: ${root}
1973
- ${session ? `Work session: ${session} (progress recorded in AGENT_SESSIONS/${session}/SESSION.md)` : ""}
1974
- ${task ? `Task: ${task}` : `Task: follow the work corresponding to label "${label}" (if a work session exists, read SESSION.md first to clarify the goal)`}
1975
- ${resumeNote}
1976
-
1977
- ## Work rules (fixed every round, must follow)
1978
- 1. Work freely within this round: read files, modify code, execute commands — use every means to advance the task.
1979
- 2. If this task is **reading/curating or text-writing work** (extracting from files, summarizing, writing docs, generating text, etc.),
1980
- first load eap (acc-eap skill) and organize output per the EAP standard:
1981
- - E↑ Explicit: entities/variables clearly defined, relationships with direction and cardinality, boundaries drawn, no ambiguous words
1982
- - R↓ Reconstructable: key conclusions record sources and reasoning, rebuildable by later agents
1983
- - S↑ Stable: output structure regenerates repeatably, no reliance on implicit context
1984
- 3. Reports must be concrete and verifiable — no filler.
1985
-
1986
- ## Per-round report (fixed format, answer each item)
1987
- 1. What was done this round (concrete)
1988
- 2. Next-step plan
1989
- 3. Whether the task is complete (if complete, output only ${stopToken})
1990
-
1991
- If the task is complete, output only ${stopToken}.`;
1992
- }
1993
1906
  /**
1994
- * handyman 规模化使用指引(guide 子命令输出;S134 继承 + v1.24.0 更新):
1995
- * 使用 handyman 前必须先加载 eap 设计方案;并行策略(jobs 编排);提示词规范(详尽固定 EAP);
1996
- * 阅读/文字编写类 handyman 内部也加载 eap。
1907
+ * cheap-worker 规模化使用指引(guide 子命令输出)。
1908
+ *
1909
+ * 🆕 2026-10-14:**双模式段已整段退场**(owner 令「删除后台运行能力」)。本指引只描述现行机制:
1910
+ * 前台委派(单任务)/前台并行扇出(jobs)/白名单与 provider 预检。
1997
1911
  */
1998
- const HANDYMAN_GUIDE = `# handyman — Scale-Up Usage Guide (guide)
1912
+ const CHEAP_WORKER_GUIDE = `# cheap-worker — Scale-Up Usage Guide (guide)
1913
+
1914
+ ## What it is
1915
+ One delegation primitive: **foreground spawn**. The host starts child agent(s) through its delegation
1916
+ service, awaits them, and returns their final text. There is no loop, no completion code, no progress
1917
+ file, and no unattended/background run.
1999
1918
 
2000
- ## Two modes (v1.31.3; default = foreground)
2001
- | | foreground (default) | background |
1919
+ | | single | parallel (jobs) |
2002
1920
  |---|---|---|
2003
- | What | child agent(s) run the task ONCE and return their final text (jobs=[...] ⇒ parallel, cap 10) | loop-validated worker: hard while-loop + random completion code |
2004
- | Use when | you need result(s) back now (per-case testers, single conversions, parallel fan-out) | long/unattended work needing anti-early-finish, resumability, or parallel jobs |
2005
- | Loop / validation | none | stop-token is the ONLY completion condition; round cap (default 100); auto-restart (≤100) |
2006
- | Progress file | none | AGENT_SESSIONS/handyman-<label>.md/.json (same label resumes) |
2007
- | Parallel jobs | **yes** — jobs=[{task,label?,model?},...], **cap 10 (fixed)** | yes (jobs=[...], handyman.maxParallel default 10) |
2008
- | Mechanism | host delegation service ctx.subagents.start("spawn") + agentOptions | ctx.agents.create + internal while-loop |
2009
- | Model source | CCC handyman.models whitelist (same for both) | same |
1921
+ | Call | cheap-worker(task, [label]) | cheap-worker(jobs=[{task, label?}, ...]) |
1922
+ | Behaviour | one child runs the task once | all children start at once, results keep your order |
1923
+ | Cap | — | **10 (fixed, hardcoded)** |
1924
+ | Model | CCC config (single source) | same |
2010
1925
 
2011
1926
  ## ⚠️ Before using: load eap and design the plan
2012
- Before calling handyman, load eap (acc-eap skill) and design the "scale-up handyman plan" based on the EAP framework.
1927
+ Before calling cheap-worker, load eap (acc-eap skill) and design the "scale-up cheap-worker plan" based on the EAP framework.
2013
1928
 
2014
1929
  ### 1. Task decomposition (E↑ Explicit)
2015
1930
  - Split large tasks into explicit subtasks: each subtask defines goal / input / boundaries (what to do, what not to do) / acceptance criteria
2016
- - Make dependencies explicit: dependent tasks run serially, independent ones can run in parallel (both modes)
1931
+ - Make dependencies explicit: dependent tasks run serially, independent ones can run in parallel via \`jobs\`
2017
1932
 
2018
- ### 2. Prompt design (handyman's task parameter)
1933
+ ### 2. Prompt design (cheap-worker's task parameter)
2019
1934
  - task must be detailed, fixed, and EAP-compliant: clear goal, drawn boundaries, decidable acceptance criteria
2020
- - foreground mode: the child does NOT share your conversation — restate everything it needs in task
1935
+ - the child does NOT share your conversation — restate everything it needs in task
2021
1936
  - Anti-example "handle this file" — ambiguous; good example "read <path>, extract all rows of the「关键决策」table,
2022
1937
  output a JSON array (fields id/conclusion/evidence), do not modify the original file"
2023
1938
  - Reading/curating or text-writing work (extracting from files, summarizing, writing docs, generating text, etc.):
2024
- the handyman-internal agent is also required to load eap and organize output per the EAP standard
1939
+ the cheap-worker-internal agent is also required to load eap and organize output per the EAP standard
2025
1940
 
2026
- ### 3. Model whitelist (CCC-configured, mandatory)
2027
- - handyman only uses models listed in .opencode/serenity.json "handyman.models" — never arbitrary models
2028
- - The CCC is expected to point this whitelist at LOW-COST models: handyman is the bulk-execution channel
2029
- - background mode: recursive subagents inside a worker inherit the worker's model automatically (DSH native)
2030
- - foreground mode: the child is started through the host delegation service with the resolved model
1941
+ ### 3. Model (CCC-configured, not selectable per call)
1942
+ - The model is read from .opencode/serenity.json "cheap-worker.models" — **the caller cannot pass one**
1943
+ (there is no "model" parameter). "cheap-worker.defaultModel" names which entry is used; it falls back to models[0].
1944
+ - The CCC is expected to point this whitelist at LOW-COST models: cheap-worker is the bulk-execution channel
1945
+ - A child's own recursive subagents inherit the child's model automatically (DSH native)
2031
1946
 
2032
1947
  ### 3b. 🔴 Provider availability (per-machine — the whitelist is NOT enough)
2033
1948
  - The whitelist lives in the **CCC** ("serenity.json", it **travels with the repository**), but provider
2034
1949
  **adapters are registered per machine by the host** (profile config / settings namespace). An entry that
2035
1950
  works on one machine can fail on another with: "no adapter registered for provider <p>".
2036
- - Before spawning, handyman verifies the provider of the resolved model against the adapters registered
1951
+ - Before spawning, cheap-worker verifies the provider of the resolved model against the adapters registered
2037
1952
  **on this machine**; if it is missing it reports the requested model, where it is configured, **the
2038
1953
  providers that ARE available here**, any declared-but-dormant route (with its settings namespace), and the
2039
- exact fix — foreground puts this in "diagnostic", background throws it.
2040
- - ⇒ If you see "has no adapter registered on this machine": edit ".opencode/serenity.json" "handyman.models"
2041
- (and "handyman.defaultModel") to a provider listed as available, or activate/register the provider on this
1954
+ exact fix — delivered in the result's "diagnostic" field (never an opaque failure, never a created child).
1955
+ - ⇒ If you see "has no adapter registered on this machine": edit ".opencode/serenity.json" "cheap-worker.models"
1956
+ (and "cheap-worker.defaultModel") to a provider listed as available, or activate/register the provider on this
2042
1957
  machine. **Do not retry the same call unchanged** — it fails identically.
2043
1958
 
2044
- ### 4. Parallel strategy (both modes)
2045
- - Independent subtasks can run in parallel:
2046
- · **foreground** — handyman(jobs=[{task,label?,model?},...]) ⇒ fan-out of custom-model subagents, **cap 10**,
2047
- results come back **in the order you passed them** (label is only a display name here; per-job "model" allowed)
2048
- · **background** — handyman(mode="background", jobs=[...]): each job gets its own label + task + stop token + progress file
2049
- - Concurrency safety guaranteed: unique sessionId (handyman-<label>-<uuid>), progress files isolated per label
2050
- (AGENT_SESSIONS/handyman-<label>.json) — same label resumes, different labels never interfere
2051
- - Parallel cap: **foreground 10 (fixed)** / background handyman.maxParallel (default 10 — cheap models are cheap)
2052
- - Aggregation: foreground returns all outputs in one result; background: after each parallel job produces progress, the main agent merges (or spawns one aggregation handyman)
2053
- - For programmable pipeline/phase orchestration at scale, use the platform's workflow tool instead
1959
+ ### 4. Parallel strategy
1960
+ - Independent subtasks: cheap-worker(jobs=[{task,label?},...]) ⇒ fan-out, **cap 10**, results come back
1961
+ **in the order you passed them** (label is only a display name).
1962
+ - For programmable pipeline/phase orchestration at scale, use the platform's workflow tool instead.
2054
1963
 
2055
1964
  ## Completion criteria
2056
- - foreground: the child's run settles; done=true only when stopReason="completed" (otherwise read diagnostic)
2057
- - background (osp loop standard): the only completion condition = the worker echoes this round's random verification
2058
- code (stop token); dialogue round cap (default 100, osp fail-safe, forced stop beyond the cap, resumable);
2059
- automatic restart on abnormal agent stop (≤100 restarts, anti-infinite-loop)
2060
-
2061
- ## Waiting UI
2062
- - The WebUI session-header Serenity detail card shows running background handymen's progress
2063
- (label / round / last response), one line per parallel job, ~3s refresh (foreground mode writes no progress)
1965
+ - The child's run settles; done=true only when stopReason="completed" — otherwise read "diagnostic"
1966
+ (and "output" for whatever partial text was produced).
2064
1967
  `;
2065
- /** 列出 AGENT_SESSIONS/handyman-*.json 的全部进度(按 updated 倒序;坏文件跳过) */
2066
- function listActiveHandymen(root) {
2067
- const dir = join(root, "AGENT_SESSIONS");
2068
- if (!existsSync(dir)) return [];
2069
- const out = [];
2070
- for (const entry of readdirSync(dir)) {
2071
- if (!entry.startsWith("handyman-") || !entry.endsWith(".json")) continue;
2072
- try {
2073
- const data = JSON.parse(readFileSync(join(dir, entry), "utf-8"));
2074
- if (typeof data.label !== "string" || typeof data.round !== "number") continue;
2075
- out.push({
2076
- label: data.label,
2077
- round: data.round,
2078
- done: data.done === true,
2079
- model: typeof data.model === "string" ? data.model : "",
2080
- updated: typeof data.updated === "string" ? data.updated : "",
2081
- lastResponse: typeof data.lastResponse === "string" ? data.lastResponse : ""
2082
- });
2083
- } catch {}
2084
- }
2085
- out.sort((a, b) => Date.parse(b.updated) - Date.parse(a.updated));
2086
- return out;
2087
- }
2088
1968
  //#endregion
2089
1969
  //#region src/agent-idle.ts
2090
1970
  /**
@@ -2132,11 +2012,10 @@ function waitAgentIdle(ctx, agent) {
2132
2012
  * skills-discovery.ts — CCC 入口 skill 自动发现(纯逻辑,零 DSH 依赖)
2133
2013
  *
2134
2014
  * 发现全部入口技能(原文全量,不截断)—— **现行三个来源**(原为四个,**来源 3 已于 2026-09-21 删除**):
2135
- * 0. **`.serenity` 记号文件内容 = 顶层入口 skill 名**(CCC 记号文件的权威语义,
2136
- * tiangong-serenity 的 .serenity 内容为 `tg-serenity`)—— 最高优先
2015
+ * 0. **`.serenity` 记号文件内容 = 顶层入口 skill 名**(CCC 记号文件的权威语义)—— 最高优先
2137
2016
  * 1. `.dsh/entry-skill` 指针文件(内容 = skill 名)—— 兼容旧约定
2138
2017
  * 2. `.opencode/skills/*-serenity/SKILL.md` —— 自动扫描该 CCC 的顶层入口
2139
- * (home-serenity / tg-serenity / pangu-serenity …,命名模式 `*-serenity`)
2018
+ * (命名模式 `*-serenity`)
2140
2019
  * ~~3. `.dsh/skills/*-serenity/SKILL.md`(扫 ACC 装进 CCC 的技能副本)~~ —— 🔴 **已删**:
2141
2020
  * owner 2026-09-21 令「连模板和安装命令一起删」(B 案)。**判据、实测证据与日期逐字记在
2142
2021
  * `findEntrySkills()` 体内那一段注释里**——本头部只留结论,**不复述判据**(避免两处会漂的真相源)。
@@ -3447,7 +3326,7 @@ function toolsBlock() {
3447
3326
  " msm — execute a registered CCC MSM: msm(name, args); partial name returns candidates; inspect=true shows usage",
3448
3327
  " praxis — actionable theory injection: praxis (index) / praxis eap / praxis neat / praxis cce / praxis ontology",
3449
3328
  " ontology — ontology engineering ENGINE (executable): init (write template) / check (M1-M10 structural; level=strict adds L1-L5 hints) / explain (summary) / verify (V1-V4 runtime compliance) / review (semantic-review prompt) / compliance (axiom-vs-skill checklist). Sibling of `praxis ontology` — that one is the METHOD TEXT, this one RUNS it. path must stay inside the container; the receipt carries the engine exit code (0/1/2/4) and the engine text verbatim, so read it rather than assuming success",
3450
- " handyman — delegate work to a worker agent on a CCC-whitelisted model; mode=\"foreground\" (default) = child agent(s) run once and return their final text (jobs=[...] fans out in parallel, cap 10, per-job model); mode=\"background\" = loop-validated worker (completion code + round cap + restart + progress file), jobs=[] orchestrates parallel work",
3329
+ " cheap-worker — delegate work to a worker agent on the CCC-configured model; one serial child per task, returns its final text. `jobs=[...]` fans out in parallel (cap 10, results keep your order). No loop, no completion code, no progress file. The model is NOT selectable per call (read from the CCC config); a worker's tool set excludes cheap-worker itself",
3451
3330
  " serenity-skill-creator — creator for serenity-skills (this container's enhanced skill standard): guide (authoring/optimization method — read first) / create <name> (scaffold SKILL.md + skill.ttl + references/, base-chain import path computed) / check <name> (A1 structural + A2 base-chain; three states: modeled-ok / plain-skill / modeled-but-check-could-not-run) / review <name> (C tier: axiom-vs-text compliance question list, half-mechanical) / index <name> (preview the second-layer resource list the model will see) / status (which skills are modeled). A skill is \"modeled\" iff it has a file named exactly skill.ttl next to SKILL.md; a plain skill is NOT an anomaly. Only `create` writes, and it refuses an existing directory",
3452
3331
  " localstore — ACC local credential/config storage (CCC-root localstore.json; git policy localstore.gitTrack default deny)",
3453
3332
  " container_admin — container administration (the maintenance bay): role (Skiff roles: guide/validate/apply/list) / msm (register/deregister/check/guide/catalog/ccc-config) / config (view)",
@@ -4017,7 +3896,7 @@ const AUTO_BOUND_NOTE_PREFIX = "auto-created for skiff role";
4017
3896
  * 注入 agent 系统提示词(用户对话面不可见)。
4018
3897
  *
4019
3898
  * v1.30.13(S142 诊断 D5):写入纪律**不点名具体工具**。旧文案写死 "with write/edit",
4020
- * 而 zhaocai 角色的工具白名单已移除 write/edit(写能力收归 CCC 的 session-write MSM)
3899
+ * 而某角色的工具白名单已移除 write/edit(写能力收归 CCC 的 session-write MSM)
4021
3900
  * → 指令指向它根本没有的工具(注入纪律与能力面矛盾,模型只能猜)。
4022
3901
  * 现文案改为「用本角色被授权的写入通道」——ACC 只声明义务(写进工作台),
4023
3902
  * 具体通道由 CCC 的角色配置与角色提示词决定(ACC/CCC 归属二分;也不把某个 CCC 的
@@ -4153,7 +4032,7 @@ function ensureSkiffSession(root, agent, roleName, role, persistent = true) {
4153
4032
  return mdPath;
4154
4033
  }
4155
4034
  /** Skiff agent 挂载的 DSH preset(v1.25.3 修复:read/grep/glob 等平台工具由 preset 决定工具面;
4156
- * handyman 经 composeFrom 继承父、skiff 无父上下文——直接挂 DSH 默认 standard preset;
4035
+ * cheap-worker 经 composeFrom 继承父、skiff 无父上下文——直接挂 DSH 默认 standard preset;
4157
4036
  * guard 角色白名单再按角色过滤可见/可用面——白名单外工具仍 deny) */
4158
4037
  const SKIFF_PRESET = "standard";
4159
4038
  /**
@@ -4316,8 +4195,8 @@ function sessionEvents(session) {
4316
4195
  if (typeof s.snapshotEvents === "function") return s.snapshotEvents();
4317
4196
  return s.events ?? [];
4318
4197
  }
4319
- /** 读会话最后一个 assistant/message 文本(handyman 同款) */
4320
- function lastAssistantText$2(agent) {
4198
+ /** 读会话最后一个 assistant/message 文本(cheap-worker 同款) */
4199
+ function lastAssistantText$1(agent) {
4321
4200
  const events = sessionEvents(agent.session);
4322
4201
  for (let i = events.length - 1; i >= 0; i--) {
4323
4202
  const e = events[i];
@@ -4391,7 +4270,7 @@ async function askSkiff(ctx, agent, question, eventsStart, options) {
4391
4270
  source: PLUGIN_SOURCE
4392
4271
  }));
4393
4272
  await waitAgentIdle(ctx, agent);
4394
- const answer = lastAssistantText$2(agent);
4273
+ const answer = lastAssistantText$1(agent);
4395
4274
  const trajectory = options?.includeTrajectory === false ? [] : eventsToTrajectory(sessionEvents(agent.session).slice(before));
4396
4275
  return {
4397
4276
  answer,
@@ -7391,7 +7270,7 @@ function cmdCompliance(args) {
7391
7270
  *
7392
7271
  * 2026-09-14 S188 修(缺陷④):旧实现把 `skillDependsOn(A, B)` 的两端都收进 targets,
7393
7272
  * 于是按生成指引逐 target 审会得到一半**方向错误的伪 Fail**(如"home-memory 未声明
7394
- * 依赖 zhaocai-memory"——B 端根本不欠任何声明)。同时修缺陷⑤:合取式
7273
+ * 依赖 `roleX-memory`"——B 端根本不欠任何声明)。同时修缺陷⑤:合取式
7395
7274
  * (`dependsOn(X,A) and dependsOn(X,B)`)里 X 出现两次,旧实现不去重。
7396
7275
  */
7397
7276
  function findDependsOnDeclarers(node) {
@@ -9307,49 +9186,23 @@ function createSkillCreatorTool() {
9307
9186
  });
9308
9187
  }
9309
9188
  //#endregion
9310
- //#region src/handyman-preset-inherit.ts
9189
+ //#region src/tools/cheap-worker.ts
9311
9190
  /**
9312
- * 解析 handyman worker 从父 agent 继承的 preset,并组装创建 setup 钩子。
9313
- * @param parentCtx - 发起 handyman 的 agent 的 scope ctx;无(headless/非 agent 上下文)时为 undefined。
9314
- * @returns 继承结果:meta 用的 agentPreset 与创建 setup 钩子。
9315
- */
9316
- function handymanPresetInheritance(parentCtx) {
9317
- if (parentCtx === void 0) return { setup: (childCtx) => {
9318
- childCtx.tools.restrict({ deny: ["handyman"] });
9319
- } };
9320
- const agentPreset = parentCtx.get("agentPresets")?.composedPreset(parentCtx);
9321
- if (agentPreset === void 0) return { setup: (childCtx) => {
9322
- childCtx.tools.restrict({ deny: ["handyman"] });
9323
- } };
9324
- return {
9325
- agentPreset,
9326
- setup: (childCtx) => {
9327
- childCtx.get("agentPresets")?.composeFrom(childCtx, parentCtx);
9328
- childCtx.tools.restrict({ deny: ["handyman"] });
9329
- }
9330
- };
9331
- }
9332
- //#endregion
9333
- //#region src/tools/handyman.ts
9334
- /**
9335
- * handyman.ts — handyman(杂工)真实 DSH 工具(v1.24.0:loop 牛马 → handyman 杂工)
9191
+ * cheap-worker.ts — cheap-worker(杂工)真实 DSH 工具
9336
9192
  *
9337
- * 用户需求(S142 拍板):
9338
- * - **同步**(用户明确:这个事一般不会异步——工具阻塞到完成)
9339
- * - **指定模型**(CCC 白名单内——handyman.models,未配置报错)
9340
- * - **自主循环到完成**(stop-token 唯一完成判据,对齐 osp loop)
9341
- * - **内部递归同样低能 subagent**(worker 工具面含 subagent,DSH 原生模型继承)
9342
- * - **workflow 能力**(jobs 编排:主 agent 一次并行多个杂工,maxParallel 默认 10)
9343
- * - **worker 不含 handyman 本身**(递归编排归主 agent,防无限嵌套)
9344
- * - **不兼容旧 loop 进度文件**(仅 handyman- 前缀)
9193
+ * v1.24.0:loop(牛马)→ handyman(杂工);2026-10-14:handyman → cheap-worker。
9194
+ * 🆕 2026-10-14(owner 令「删除后台运行能力」):**background 模式整条退场** ——
9195
+ * 轮次循环 / stop-token 完成码 / 进度文件 / 重启计数 / maxRounds/maxParallel 全部删除。
9196
+ * 现存只有**前台委派**一种形态:宿主起子 agent、等它跑完、取回最终文本。
9345
9197
  *
9346
- * 机制:ctx.agents.create()(带 setup 钩子)创建专用 agent(进程内),
9347
- * 每轮 followup → agent/status idle → 读 session.events 响应 → 写进度 → stop token 检查 →
9348
- * 未完成继续下一轮;followup/等待空闲抛错(非正常停止)→ dispose 并重新 create agent
9349
- * (重启计数,≤100),同一轮重试。工厂模式:apply 时闭包捕获插件 ctx(工具 execute 无 ctx 参数)。
9198
+ * 机制:走宿主**委派正门** `ctx.subagents.start('spawn', …)`(深度上限、子 agent 所有权、
9199
+ * `subagent/start|end` 事件、`dispose()` 收尾全部沿用宿主实现),模型经 `agentOptions` 注入。
9200
+ * 前置:`exec.agent` 必须存在(前台委派要有发起方)。
9350
9201
  *
9351
- * preset 继承 + 工具收窄:setup 钩子里对子 agent 执行 agentPresets.composeFrom(对齐
9352
- * subagent 先例)+ tools.restrict deny handyman(worker 内部看不到 handyman 工具)。
9202
+ * 🔴 **模型不可由调用方指定**(owner 令):**没有 `model` 参数**,一律读 CCC 配置
9203
+ * `cheap-worker.models` / `cheap-worker.defaultModel`(单一真相源)。
9204
+ *
9205
+ * 递归防护:子 agent 的工具面 deny `cheap-worker` 本身(编排归主 agent,防无限嵌套)。
9353
9206
  */
9354
9207
  function renderText$4(value) {
9355
9208
  return [{
@@ -9357,52 +9210,18 @@ function renderText$4(value) {
9357
9210
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
9358
9211
  }];
9359
9212
  }
9360
- /** 读取会话最后一个 assistant/message 文本 */
9361
- function lastAssistantText$1(agent) {
9362
- const s = agent.session;
9363
- const events = typeof s.snapshotEvents === "function" ? s.snapshotEvents() : s.events ?? [];
9364
- for (let i = events.length - 1; i >= 0; i--) {
9365
- const e = events[i];
9366
- if (e && e.type === "assistant/message") {
9367
- const data = e.data;
9368
- const text = (data?.message?.content ?? data?.content ?? []).filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
9369
- if (text) return text;
9370
- }
9371
- }
9372
- return "";
9373
- }
9374
- /** 非正常停止时重启 agent 的次数上限(防死循环保险阀):对话轮次有上限,重启有上限 */
9375
- const HANDYMAN_MAX_RESTARTS = 100;
9376
- /** 运行时解析 jobs 参数(DSH schema 不支持 object items 的 required/校验 → 手工校验类型) */
9377
- function parseJobs(raw) {
9378
- if (!Array.isArray(raw) || raw.length === 0) return null;
9379
- const jobs = [];
9380
- for (const item of raw) {
9381
- if (typeof item !== "object" || item === null) return null;
9382
- const rec = item;
9383
- if (typeof rec.task !== "string" || typeof rec.label !== "string") return null;
9384
- jobs.push({
9385
- task: rec.task,
9386
- label: rec.label,
9387
- model: typeof rec.model === "string" ? rec.model : void 0
9388
- });
9389
- }
9390
- return jobs;
9391
- }
9392
9213
  /**
9393
- * 🔴 **foreground 并行上限 = 10**(owner 2026-09-29 具名令:「这个版本顺手帮我把 handyman 并行度从 5 调整成 10」;
9214
+ * 🔴 **前台并行上限 = 10**(owner 2026-09-29 具名令:「这个版本顺手帮我把 handyman 并行度从 5 调整成 10」;
9394
9215
  * 原值 5 出自 owner 2026-09-27 的具名令「最大 5;就当是支持自定模型的 subagent 用了,急需」)。
9395
9216
  *
9396
9217
  * 为什么是**硬编码**而不是又一个配置键:owner 两次都是**直接给数**(同 `REBUILD-TODO` 那次的裁决口径
9397
9218
  * —— 设定项能不加就不加,见 S142 的「可砍设定项」)。若将来还要调,仍按具名令改这个常量。
9398
9219
  *
9399
- * 为什么 foreground 仍与 background 分开(后台 = `handyman.maxParallel`,缺省 **10**):**foreground 会占住
9400
- * 主 agent 的这一轮**(调用方在等结果),并发越高越容易把宿主的并发面与用量推上去。
9401
- * ⚠️ 现在两档**数值相等但语义不同**(前台 = 占住本轮 / 后台 = 不占)⇒ **刻意不合并成一个常量**:
9402
- * 合并会让"哪天要给前台单独降档"变成一次语义手术。
9220
+ * 为什么是 10 而不是更高:**前台委派会占住主 agent 的这一轮**(调用方在等结果),
9221
+ * 并发越高越容易把宿主的并发面与用量推上去。
9403
9222
  */
9404
9223
  const FOREGROUND_MAX_PARALLEL = 10;
9405
- /** foreground 的 jobs 解析(与 background 的差别:**label 可选** —— 前台 label 只是显示名,不命名进度文件) */
9224
+ /** foreground 的 jobs 解析(label 可选 —— 前台 label 只是显示名,不命名任何文件) */
9406
9225
  function parseForegroundJobs(raw) {
9407
9226
  if (!Array.isArray(raw) || raw.length === 0) return null;
9408
9227
  const jobs = [];
@@ -9412,8 +9231,7 @@ function parseForegroundJobs(raw) {
9412
9231
  if (typeof rec.task !== "string" || rec.task.trim() === "") return null;
9413
9232
  jobs.push({
9414
9233
  task: rec.task,
9415
- ...typeof rec.label === "string" && rec.label !== "" ? { label: rec.label } : {},
9416
- ...typeof rec.model === "string" && rec.model !== "" ? { model: rec.model } : {}
9234
+ ...typeof rec.label === "string" && rec.label !== "" ? { label: rec.label } : {}
9417
9235
  });
9418
9236
  }
9419
9237
  return jobs;
@@ -9430,8 +9248,8 @@ function subagentOutputText(output) {
9430
9248
  * ⇒ 一律**不阻断**,保持旧行为 —— 增强不得变成新的失败源)。
9431
9249
  *
9432
9250
  * 为什么必须放在"创建之前":宿主对未知 provider 抛 `no adapter registered for provider "…"`
9433
- * (`dsh-llm`,code `NO_ADAPTER`)。若照常创建:前台只拿回 `stopReason:"error"` + **空
9434
- * `diagnostic`**(实测),后台整条失败且**不带原因** ⇒ 调用方(LLM)无法自我修正配置。
9251
+ * (`dsh-llm`,code `NO_ADAPTER`)。若照常创建:只拿回 `stopReason:"error"` + **空
9252
+ * `diagnostic`**(实测)⇒ 调用方(LLM)无法自我修正配置。
9435
9253
  */
9436
9254
  function providerPreflight(ctx, model) {
9437
9255
  const llm = hostLlm(ctx);
@@ -9456,14 +9274,13 @@ function providerPreflight(ctx, model) {
9456
9274
  });
9457
9275
  }
9458
9276
  /**
9459
- * foreground 模式:单次前台串行委派(用户裁决"本次所需的简单实现")。
9277
+ * 单次前台串行委派。
9460
9278
  *
9461
- * 与 background 的差别(R↓):
9462
9279
  * - 走宿主**委派正门** `ctx.subagents.start('spawn', …)`——深度上限、子 agent 所有权、
9463
9280
  * `subagent/start|end` 生命周期事件、`dispose()` 收尾全部沿用宿主实现(不重写 agent 创建);
9464
9281
  * - 模型经 `agentOptions` 注入(spawn 后端能力位 `agentOptions: true`),因此**不依赖**
9465
9282
  * 宿主未放开的 `subagent-model-selection` 设置;
9466
- * - 不循环、不写进度文件、不做完成码校验——"一次调用一次结果"。
9283
+ * - 不循环、不写文件、不做完成码校验——"一次调用一次结果"。
9467
9284
  *
9468
9285
  * 前置条件:`opts.parent` 必须是发起 agent(由调用方 `execute` 在解析 CCC 之前校验;
9469
9286
  * 本函数不再重复检查——单一真相源)。
@@ -9472,7 +9289,7 @@ async function runForegroundJob(ctx, opts) {
9472
9289
  const { task, label, model, models, parent, signal } = opts;
9473
9290
  requireWhitelistedModel(model, models);
9474
9291
  const subagents = hostSubagents(ctx);
9475
- if (!subagents?.start) throw new Error("handyman foreground: host subagents service unavailable — neither ctx.subagents nor ctx.get(\"subagents\") yielded a start() function. Check `dashboard health` (host-contract section, service \"subagents\"); mode=\"background\" does not need this service and still works.");
9292
+ if (!subagents?.start) throw new Error("cheap-worker: host subagents service unavailable — neither ctx.subagents nor ctx.get(\"subagents\") yielded a start() function. Check `dashboard health` (host-contract section, service \"subagents\").");
9476
9293
  const preflight = providerPreflight(ctx, model);
9477
9294
  if (preflight !== null) return {
9478
9295
  mode: "foreground",
@@ -9495,7 +9312,7 @@ async function runForegroundJob(ctx, opts) {
9495
9312
  ...provider === void 0 ? {} : { provider },
9496
9313
  model: modelName
9497
9314
  },
9498
- toolFilter: { deny: ["handyman"] }
9315
+ toolFilter: { deny: ["cheap-worker"] }
9499
9316
  });
9500
9317
  try {
9501
9318
  const result = run.result === void 0 ? {} : await run.result;
@@ -9513,165 +9330,28 @@ async function runForegroundJob(ctx, opts) {
9513
9330
  if (run.dispose) await run.dispose().catch(() => {});
9514
9331
  }
9515
9332
  }
9516
- /**
9517
- * 驱动单个 handyman worker:创建 agent → while 循环(stop-token 完成判据)→ dispose。
9518
- * 同步阻塞直到完成/保险阀。jobs 编排时多个 worker 并行(Promise.all)。
9519
- */
9520
- async function runHandymanJob(ctx, opts) {
9521
- const { root, job, defaultModel, models, maxRounds, parentSession, parentCtx, session } = opts;
9522
- const label = job.label;
9523
- const model = job.model ?? defaultModel;
9524
- requireWhitelistedModel(model, models);
9525
- const preflightError = providerPreflight(ctx, model);
9526
- if (preflightError !== null) throw new Error(preflightError);
9527
- const { provider, model: modelName } = splitModel(model);
9528
- const stopToken = newStopToken();
9529
- let progress = readProgress(root, label);
9530
- const startRound = progress ? progress.round + 1 : 1;
9531
- const inherited = handymanPresetInheritance(parentCtx);
9532
- if (!ctx.agents) throw new Error("handyman: ctx.agents unavailable");
9533
- let handle;
9534
- let workerAgent;
9535
- const spawnAgent = async () => {
9536
- const sessionId = `handyman-${label}-${randomUUID()}`;
9537
- handle = await ctx.agents.create({
9538
- sessionId,
9539
- meta: {
9540
- cwd: root,
9541
- origin: "subagent",
9542
- ...parentSession === void 0 ? {} : { parentSession },
9543
- ...inherited.agentPreset === void 0 ? {} : { agentPreset: inherited.agentPreset }
9544
- },
9545
- agentOptions: {
9546
- provider,
9547
- model: modelName
9548
- },
9549
- ...inherited.setup === void 0 ? {} : { setup: inherited.setup }
9550
- });
9551
- workerAgent = handle.agent;
9552
- };
9553
- await spawnAgent();
9554
- let done = false;
9555
- let lastResponse = progress?.lastResponse ?? "";
9556
- let finalRound = startRound - 1;
9557
- let restarts = 0;
9558
- let finishReason = "done";
9559
- try {
9560
- let round = startRound;
9561
- while (true) {
9562
- if (round > maxRounds) {
9563
- finishReason = "max_rounds";
9564
- break;
9565
- }
9566
- finalRound = round;
9567
- const prompt = buildRoundPrompt({
9568
- root,
9569
- session,
9570
- label,
9571
- round,
9572
- stopToken,
9573
- progress,
9574
- task: job.task
9575
- });
9576
- try {
9577
- workerAgent.followup(createUserMessage({
9578
- content: [{
9579
- type: "text",
9580
- text: prompt
9581
- }],
9582
- source: PLUGIN_SOURCE
9583
- }));
9584
- await waitAgentIdle(ctx, workerAgent);
9585
- lastResponse = lastAssistantText$1(workerAgent);
9586
- } catch {
9587
- restarts++;
9588
- if (restarts > HANDYMAN_MAX_RESTARTS) {
9589
- finishReason = "restart_exceeded";
9590
- break;
9591
- }
9592
- await handle.dispose().catch(() => {});
9593
- await spawnAgent();
9594
- continue;
9595
- }
9596
- progress = {
9597
- round,
9598
- done: false,
9599
- label,
9600
- model,
9601
- updated: isoLocal(),
9602
- lastResponse
9603
- };
9604
- writeProgress(root, label, progress);
9605
- if (lastResponse.includes(stopToken)) {
9606
- done = true;
9607
- finishReason = "done";
9608
- break;
9609
- }
9610
- round++;
9611
- }
9612
- writeProgress(root, label, {
9613
- round: finalRound,
9614
- done,
9615
- label,
9616
- model,
9617
- updated: isoLocal(),
9618
- lastResponse,
9619
- status: done ? "done" : "running"
9620
- });
9621
- if (finishReason !== "done") writeFailedStatus(root, label, {
9622
- errorCode: finishReason,
9623
- errorMessage: finishReason === "max_rounds" ? `Reached ${maxRounds} rounds without completion — resume with same label to continue.` : `Agent restarted ${HANDYMAN_MAX_RESTARTS} times without progress — resume with same label to continue.`
9624
- });
9625
- } finally {
9626
- await handle.dispose().catch(() => {});
9627
- }
9628
- const { json } = handymanProgressPaths(root, label);
9629
- return {
9630
- label,
9631
- done,
9632
- rounds: finalRound,
9633
- finishReason,
9634
- restarts,
9635
- model,
9636
- lastResponse: lastResponse.slice(0, 2e3),
9637
- progressFile: json
9638
- };
9639
- }
9640
- /** 创建 handyman 工具(闭包捕获插件 ctx → 可访问 ctx.agents) */
9641
- function createHandymanTool(ctx) {
9333
+ /** 创建 cheap-worker 工具(闭包捕获插件 ctx → 可访问 ctx.subagents) */
9334
+ function createCheapWorkerTool(ctx) {
9642
9335
  return defineTool({
9643
- name: "handyman",
9644
- description: "The handyman (杂工): delegate work to a worker agent on a CCC-configured model. Two modes (v1.31.3).\nMode \"foreground\" (DEFAULT): one serial child agent runs the task once and returns its final text — no loop, no completion-code validation, no progress file; use it for one-shot serial delegation (e.g. per-case testers that just gather evidence). Usage: handyman(task, [label], [model]).\nMode \"background\": the loop-validated worker — internal hard while-loop whose ONLY completion condition is the worker echoing this round's random completion code (stop token, prevents low-intelligence models from finishing early), round cap (default 100, resumable), automatic restart on abnormal stop (≤100), progress file AGENT_SESSIONS/handyman-<label>.md/.json (same label resumes), and parallel jobs (handyman.maxParallel, default 10). Usage: handyman(mode=\"background\", task, label, [model]) or handyman(mode=\"background\", jobs=[{task,label,model?},...]).\nChoosing: need exactly one result back now → foreground; need anti-early-finish guarantees or resumability → background; need N results back now (parallel, per-job model) → foreground with jobs (cap 10).\nModel: only models whitelisted in .opencode/serenity.json \"handyman.models\" (missing config = error); default reads handyman.defaultModel. Both modes share the same whitelist and default.\nProvider check: the whitelist lives in the CCC (it travels with the repo) while provider adapters are registered per machine by the host — so before spawning, the provider of the resolved model is verified against the adapters present on THIS machine. If it is missing you get the available providers plus the exact fix (foreground: in `diagnostic`; background: thrown), never an opaque failure.\nRecursion: a worker's tool set excludes handyman itself (orchestration belongs to the main agent).\nGuide: handyman(guide=true) prints the scale-up usage guide.",
9336
+ name: "cheap-worker",
9337
+ description: "The cheap-worker (杂工): delegate work to a worker agent on the CCC-configured model, and return its final text. One serial child per task; `jobs` fans out in parallel.\nUsage: cheap-worker(task, [label]) for a single delegation; cheap-worker(jobs=[{task,label?},...]) for parallel fan-out (cap 10, results keep your order).\nThe child does NOT share this conversation — the task must be self-contained.\nNo loop, no completion-code validation, no progress file, no unattended/background run.\nModel: NOT selectable per call — it is read from .opencode/serenity.json \"cheap-worker.models\" (missing config = error); \"cheap-worker.defaultModel\" names the one used, falling back to models[0].\nProvider check: the whitelist lives in the CCC (it travels with the repo) while provider adapters are registered per machine by the host — so before spawning, the provider of the resolved model is verified against the adapters present on THIS machine. If it is missing you get the available providers plus the exact fix in `diagnostic`, and no child is created.\nRecursion: a worker's tool set excludes cheap-worker itself (orchestration belongs to the main agent).\nGuide: cheap-worker(guide=true) prints the scale-up usage guide.",
9645
9338
  parameters: {
9646
- mode: {
9647
- type: "string",
9648
- description: "Delegation mode: \"foreground\" (default — child agent(s) run once and return their final text; accepts `jobs` for parallel fan-out, cap 10) or \"background\" (loop-validated worker with completion-code check, round cap, auto-restart, progress file, parallel jobs)"
9649
- },
9650
9339
  task: {
9651
9340
  type: "string",
9652
- description: "The task goal to complete (required in both modes; foreground needs a self-contained prompt — the child does not share this conversation)"
9341
+ description: "The task goal to complete (must be self-contained — the child does not share this conversation)"
9653
9342
  },
9654
9343
  label: {
9655
9344
  type: "string",
9656
- description: "Task label (background: 1-50 chars, names the progress file; foreground: optional child display name)"
9657
- },
9658
- session: {
9659
- type: "string",
9660
- description: "Work session S### (context hint, progress reference)"
9661
- },
9662
- model: {
9663
- type: "string",
9664
- description: "provider/model — must be in the CCC whitelist handyman.models; default reads handyman.defaultModel"
9345
+ description: "Optional child display name"
9665
9346
  },
9666
9347
  jobs: {
9667
9348
  type: "array",
9668
- description: "Multi-job orchestration (workflow capability): [{task, label?, model?}, ...] — runs jobs in parallel. Cap: foreground 10 (fixed) / background handyman.maxParallel (default 10). Results keep the order you passed them in.",
9349
+ description: "Multi-job orchestration (workflow capability): [{task, label?}, ...] — runs jobs in parallel, cap 10 (fixed). Results keep the order you passed them in.",
9669
9350
  items: {
9670
9351
  type: "object",
9671
9352
  properties: {
9672
9353
  task: { type: "string" },
9673
- label: { type: "string" },
9674
- model: { type: "string" }
9354
+ label: { type: "string" }
9675
9355
  },
9676
9356
  additionalProperties: false
9677
9357
  }
@@ -9686,93 +9366,51 @@ function createHandymanTool(ctx) {
9686
9366
  render: (_args, value) => renderText$4(value)
9687
9367
  },
9688
9368
  async execute(args, exec) {
9689
- if (args.guide) return { guide: HANDYMAN_GUIDE };
9690
- if ((args.mode ?? "foreground") === "foreground" && !exec.agent) throw new Error("handyman foreground: requires a calling agent (exec.agent was undefined)");
9369
+ if (args.guide) return { guide: CHEAP_WORKER_GUIDE };
9370
+ if (!exec.agent) throw new Error("cheap-worker: requires a calling agent (exec.agent was undefined)");
9691
9371
  const root = cccRootForExec(exec);
9692
9372
  if (!root) throw new Error(NO_CCC_FROM_AGENT_CWD);
9693
- const hc = readHandymanConfig(root, DEFAULT_SERENITY_CONFIG_PATHS);
9694
- if (hc === null) throw new Error("handyman requires a model whitelist: configure .opencode/serenity.json \"handyman.models\" (e.g. {\"handyman\": {\"models\": [\"minimax-cn-coding-plan/MiniMax-M3\"], \"defaultModel\": \"minimax-cn-coding-plan/MiniMax-M3\"}})");
9695
- const mode = args.mode === "background" ? "background" : "foreground";
9696
- if (args.mode !== void 0 && args.mode !== "background" && args.mode !== "foreground") throw new Error("handyman: mode must be \"foreground\" (default) or \"background\"");
9697
- if (mode === "foreground") {
9698
- if (args.jobs !== void 0) {
9699
- const jobs = parseForegroundJobs(args.jobs);
9700
- if (jobs === null) throw new Error("handyman foreground: jobs must be [{task, label?, model?}, ...] (every job requires a non-empty task)");
9701
- if (jobs.length > FOREGROUND_MAX_PARALLEL) throw new Error(`handyman foreground: ${jobs.length} jobs exceed the foreground parallel cap ${FOREGROUND_MAX_PARALLEL} (split into batches, or use mode="background" whose cap is handyman.maxParallel)`);
9702
- const results = await Promise.all(jobs.map((job) => runForegroundJob(ctx, {
9703
- task: job.task,
9704
- ...job.label === void 0 ? {} : { label: job.label },
9705
- model: job.model ?? (typeof args.model === "string" && args.model !== "" ? args.model : hc.defaultModel),
9706
- models: hc.models,
9707
- parent: exec.agent,
9708
- signal: exec.signal
9709
- })));
9710
- const done = results.every((r) => r.done);
9711
- return {
9712
- mode: "foreground",
9713
- done,
9714
- jobs: results,
9715
- usage: {
9716
- how: `foreground parallel mode starts ${results.length} child agents at once through the host delegation service (ctx.subagents.start "spawn"), awaits all of them, and returns each child's final text. No loop, no completion-code validation, no progress file. Cap = ${FOREGROUND_MAX_PARALLEL}.`,
9717
- model: "restricted to the CCC whitelist handyman.models; per-job model overrides the call-level model; default reads handyman.defaultModel",
9718
- next: done ? "All children completed; use each job's output directly (results are in the same order as the jobs you passed)" : "Some children did not complete — inspect each job's diagnostic/output (results keep the order you passed); for anti-early-finish guarantees use mode=\"background\""
9719
- }
9720
- };
9721
- }
9722
- if (typeof args.task !== "string" || args.task.trim() === "") throw new Error("handyman foreground: task is required (and must be self-contained — the child does not share this conversation)");
9723
- const model = typeof args.model === "string" && args.model !== "" ? args.model : hc.defaultModel;
9724
- const res = await runForegroundJob(ctx, {
9725
- task: args.task,
9726
- ...typeof args.label === "string" && args.label !== "" ? { label: args.label } : {},
9727
- model,
9373
+ const hc = readCheapWorkerConfig(root, DEFAULT_SERENITY_CONFIG_PATHS);
9374
+ if (hc === null) throw new Error("cheap-worker requires a model whitelist: configure .opencode/serenity.json \"cheap-worker.models\" (e.g. {\"cheap-worker\": {\"models\": [\"minimax-cn-coding-plan/MiniMax-M3\"], \"defaultModel\": \"minimax-cn-coding-plan/MiniMax-M3\"}})");
9375
+ if (args.jobs !== void 0) {
9376
+ const jobs = parseForegroundJobs(args.jobs);
9377
+ if (jobs === null) throw new Error("cheap-worker: jobs must be [{task, label?}, ...] (every job requires a non-empty task)");
9378
+ if (jobs.length > FOREGROUND_MAX_PARALLEL) throw new Error(`cheap-worker: ${jobs.length} jobs exceed the parallel cap ${FOREGROUND_MAX_PARALLEL} (split into batches)`);
9379
+ const results = await Promise.all(jobs.map((job) => runForegroundJob(ctx, {
9380
+ task: job.task,
9381
+ ...job.label === void 0 ? {} : { label: job.label },
9382
+ model: hc.defaultModel,
9728
9383
  models: hc.models,
9729
9384
  parent: exec.agent,
9730
9385
  signal: exec.signal
9731
- });
9386
+ })));
9387
+ const done = results.every((r) => r.done);
9732
9388
  return {
9733
- ...res,
9389
+ mode: "foreground",
9390
+ done,
9391
+ jobs: results,
9734
9392
  usage: {
9735
- how: "foreground mode starts ONE serial child agent through the host delegation service (ctx.subagents.start \"spawn\") with the CCC-configured model; it awaits the run and returns the child's final text. No loop, no completion-code validation, no progress file.",
9736
- model: "restricted to the CCC whitelist handyman.models; default reads handyman.defaultModel",
9737
- next: res.done ? "Child completed; use its output directly" : `Child did not complete (stopReason=${res.stopReason}); inspect diagnostic/output and decide whether to retry, switch model, or use mode="background"`
9393
+ how: `foreground parallel mode starts ${results.length} child agents at once through the host delegation service (ctx.subagents.start "spawn"), awaits all of them, and returns each child's final text. No loop, no completion-code validation, no progress file. Cap = ${FOREGROUND_MAX_PARALLEL}.`,
9394
+ model: "NOT selectable per call — resolved from the CCC config cheap-worker.models (cheap-worker.defaultModel, else models[0])",
9395
+ next: done ? "All children completed; use each job's output directly (results are in the same order as the jobs you passed)" : "Some children did not complete — inspect each job's diagnostic/output (results keep the order you passed)"
9738
9396
  }
9739
9397
  };
9740
9398
  }
9741
- const jobs = args.jobs !== void 0 ? (() => {
9742
- const parsed = parseJobs(args.jobs);
9743
- if (parsed === null) throw new Error("handyman: jobs must be [{task, label, model?}, ...] (every job requires task and label)");
9744
- return parsed;
9745
- })() : (() => {
9746
- if (typeof args.task !== "string" || typeof args.label !== "string") throw new Error("handyman: task and label are required (single-job mode)");
9747
- return [{
9748
- task: args.task,
9749
- label: args.label,
9750
- ...typeof args.model === "string" && args.model !== "" ? { model: args.model } : {}
9751
- }];
9752
- })();
9753
- if (jobs.length === 0) throw new Error("handyman: no jobs provided");
9754
- if (jobs.length > hc.maxParallel) throw new Error(`handyman: ${jobs.length} jobs exceed maxParallel ${hc.maxParallel} (configure handyman.maxParallel to raise)`);
9755
- if (jobs.some((j) => !j.task || !j.label)) throw new Error("handyman: every job requires task and label");
9756
- const parentCtx = exec.agent?.ctx;
9757
- const parentSession = (exec.agent?.session)?.id;
9758
- const results = await Promise.all(jobs.map((job) => runHandymanJob(ctx, {
9759
- root,
9760
- job,
9761
- defaultModel: hc.defaultModel,
9399
+ if (typeof args.task !== "string" || args.task.trim() === "") throw new Error("cheap-worker: task is required (and must be self-contained — the child does not share this conversation)");
9400
+ const res = await runForegroundJob(ctx, {
9401
+ task: args.task,
9402
+ ...typeof args.label === "string" && args.label !== "" ? { label: args.label } : {},
9403
+ model: hc.defaultModel,
9762
9404
  models: hc.models,
9763
- maxRounds: hc.maxRounds,
9764
- parentSession,
9765
- parentCtx,
9766
- session: typeof args.session === "string" ? args.session : void 0
9767
- })));
9405
+ parent: exec.agent,
9406
+ signal: exec.signal
9407
+ });
9768
9408
  return {
9769
- done: results.every((r) => r.done),
9770
- jobs: results.map((r) => ({ ...r })),
9409
+ ...res,
9771
9410
  usage: {
9772
- how: "handyman drives each worker agent round by round via an internal hard while-loop; the worker terminates by echoing the random completion code; round cap (default 100, osp fail-safe), automatic restart on abnormal stop (≤100 times); model restricted to the CCC whitelist handyman.models",
9773
- progress: "Progress in AGENT_SESSIONS/handyman-<label>.md and .json; calling handyman again with the same label resumes from the next round (no redo)",
9774
- constraints: "handyman workers carry full Serenity constraints (ACC identity / entry-skill system prompt / guards / trajectory-steward); workers include the subagent tool (same-model inheritance) but NOT handyman itself",
9775
- next: results.every((r) => r.done) ? "All jobs completed; check the progress files to wrap up" : "Some jobs incomplete (max_rounds / restart_exceeded fail-safe); resume with the same labels"
9411
+ how: "foreground mode starts ONE serial child agent through the host delegation service (ctx.subagents.start \"spawn\") with the CCC-configured model; it awaits the run and returns the child's final text. No loop, no completion-code validation, no progress file.",
9412
+ model: "NOT selectable per call — resolved from the CCC config cheap-worker.models (cheap-worker.defaultModel, else models[0])",
9413
+ next: res.done ? "Child completed; use its output directly" : `Child did not complete (stopReason=${res.stopReason}); inspect diagnostic/output and decide whether to retry`
9776
9414
  }
9777
9415
  };
9778
9416
  }
@@ -11985,13 +11623,15 @@ function registerWakeScheduler(ctx) {
11985
11623
  /**
11986
11624
  * 样例快照输入 —— **代表性数据**(不是真实某条轨迹的现状)。
11987
11625
  *
11988
- * 取值刻意让它能演示所有者举的 S185 场景(天亮/有人/非高峰 + 已在干活 + 日志过大)。
11626
+ * 取值刻意让它能演示所有者举的"天亮/有人/非高峰 + 已在干活 + 日志过大"场景。
11627
+ * 🔒 **路径与轨迹名一律用中性占位**(`/home/user/workspace`、`<S###>` 式示例),
11628
+ * 不写任何真实机器路径或私有会话名 —— 本文件随 npm 包发布(2026-10-14 隐私核查)。
11989
11629
  */
11990
11630
  const CRO_SAMPLE_SNAPSHOT_INPUT = {
11991
- dirName: "2026-09-13--S185--desk-camera",
11992
- cccRoot: "/home/yh/home/home-serenity",
11631
+ dirName: "2026-01-01--S001--example-trajectory",
11632
+ cccRoot: "/home/user/workspace",
11993
11633
  nowMs: 17898e8,
11994
- sessionMdPath: "/home/yh/home/home-serenity/AGENT_SESSIONS/2026-09-13--S185--desk-camera/SESSION.md",
11634
+ sessionMdPath: "/home/user/workspace/AGENT_SESSIONS/2026-01-01--S001--example-trajectory/SESSION.md",
11995
11635
  sessionMdBytes: 182e3,
11996
11636
  sessionMdMtimeMs: 1789799e6,
11997
11637
  references: [{
@@ -12007,10 +11647,10 @@ const CRO_SAMPLE_SNAPSHOT_INPUT = {
12007
11647
  liveSessionIds: ["session-adccc461-9f32-4d9a-a688-9da81fa1ca35"],
12008
11648
  runningSessionIds: [],
12009
11649
  pendingWakes: [{
12010
- id: "w-20260919-0900-aaaa",
12011
- at: "2026-09-19T09:30:00+08:00",
12012
- createdAt: "2026-09-19T07:00:00+08:00",
12013
- createdBy: "S185"
11650
+ id: "w-20260101-0900-aaaa",
11651
+ at: "2026-01-01T09:30:00+08:00",
11652
+ createdAt: "2026-01-01T07:00:00+08:00",
11653
+ createdBy: "S001"
12014
11654
  }],
12015
11655
  scheduler: {
12016
11656
  armed: true,
@@ -12031,7 +11671,7 @@ function renderCroSampleSnapshot() {
12031
11671
  /**
12032
11672
  * CRO 编写指南正文(ACC 内置手册;由工具的 guide 动作输出)。
12033
11673
  *
12034
- * 结构照既有 guide 惯例(`MSM_GUIDE` / `handyman(guide=true)`):
11674
+ * 结构照既有 guide 惯例(`MSM_GUIDE` / `cheap-worker(guide=true)`):
12035
11675
  * 用途 → 位置 → 我给什么 → 你给我什么 → 骨架 → 常见模式 → 坑 → **自测**。
12036
11676
  */
12037
11677
  const CRO_GUIDE = `CRO(Continuous Re-Occurrence · 持续再发生)编写指南
@@ -12558,7 +12198,7 @@ function renderText$2(value) {
12558
12198
  }
12559
12199
  const localstoreTool = defineTool({
12560
12200
  name: "localstore",
12561
- description: "ACC standard local credential/config storage: one tool manages two namespaces — credential (credentials) and config (local preferences). Stored in localstore.json at the CCC root (JSON format, directly readable by MSMs). Git policy: .opencode/serenity.json localstore.gitTrack (allow commits / deny commits, default deny; .dsh fallback) — when deny, writes ensure .gitignore contains this file (physical guarantee), container_git commit checks and refuses. Subcommands: list (list keys, credentials never return values) / get <name> (read value) / set <name> <value> (write) / unset <name> (delete) / show <name> (metadata, credentials never print values) / doc (output the storage spec — path/format/key conventions/git policy; agents may operate the file directly with read/write per the spec). Default scope=credential; config requires --scope config (path is section.key, e.g. handyman.models).",
12201
+ description: "ACC standard local credential/config storage: one tool manages two namespaces — credential (credentials) and config (local preferences). Stored in localstore.json at the CCC root (JSON format, directly readable by MSMs). Git policy: .opencode/serenity.json localstore.gitTrack (allow commits / deny commits, default deny; .dsh fallback) — when deny, writes ensure .gitignore contains this file (physical guarantee), container_git commit checks and refuses. Subcommands: list (list keys, credentials never return values) / get <name> (read value) / set <name> <value> (write) / unset <name> (delete) / show <name> (metadata, credentials never print values) / doc (output the storage spec — path/format/key conventions/git policy; agents may operate the file directly with read/write per the spec). Default scope=credential; config requires --scope config (path is section.key, e.g. cheap-worker.models).",
12562
12202
  parameters: {
12563
12203
  action: {
12564
12204
  type: "string",
@@ -12602,7 +12242,7 @@ dsp 只提供机制(双白名单强制 + 基础提示词 + 调试问答页)
12602
12242
  ── 角色配置 schema ──
12603
12243
  { "skiff": { "roles": {
12604
12244
  "<role-name>": {
12605
- "model": "provider/model", // 角色模型(CCC 直接指定,无白名单校验;缺省回退 handyman.defaultModel)
12245
+ "model": "provider/model", // 角色模型(CCC 直接指定,无白名单校验;缺省回退 cheap-worker.defaultModel)
12606
12246
  "msms": ["msm-a", "msm-b"], // MSM 白名单(独立):msm 工具只能执行这些;register/deregister 必拒
12607
12247
  "tools": ["read","grep","glob",...], // 非 MSM 工具白名单(独立):白名单外工具一律不可用(guard 强制)
12608
12248
  "trajectory": { "session": false, "keeper": false, "rebuild": false }, // 轨迹纪律子集(缺省全关 = 完全独立)
@@ -12646,7 +12286,7 @@ dsp 只提供机制(双白名单强制 + 基础提示词 + 调试问答页)
12646
12286
  会话追问(v1.25.10):调试页持有当前会话,追问自动续接(同会话上下文延续);
12647
12287
  「新对话」按钮开新会话;进程重启后旧会话不可续(WebUI 仍可见历史)
12648
12288
  注意:skill 加载对 skiff **恒可用**(不设白名单——读知识面,无写能力)`;
12649
- /** 校验当前 CCC 的 skiff 配置:roles schema 合法 / msms 均已注册 / model ∈ handyman.models / systemPrompt 非空 */
12289
+ /** 校验当前 CCC 的 skiff 配置:roles schema 合法 / msms 均已注册 / model ∈ cheap-worker.models / systemPrompt 非空 */
12650
12290
  function validateSkiffConfig(root) {
12651
12291
  const roles = readSkiffRoles(root);
12652
12292
  const issues = [];
@@ -12657,7 +12297,7 @@ function validateSkiffConfig(root) {
12657
12297
  note: "no skiff roles defined (skiff disabled — zero impact)"
12658
12298
  };
12659
12299
  const registered = new Set(loadMsmEntries(root).map((e) => e.name));
12660
- const hc = readHandymanConfig(root);
12300
+ const hc = readCheapWorkerConfig(root);
12661
12301
  const allowedModels = hc ? new Set(hc.models) : null;
12662
12302
  for (const [name, role] of roles) {
12663
12303
  try {
@@ -12666,7 +12306,7 @@ function validateSkiffConfig(root) {
12666
12306
  issues.push(`role "${name}": ${String(err?.message ?? err)}`);
12667
12307
  }
12668
12308
  for (const m of role.msms ?? []) if (!registered.has(m)) issues.push(`role "${name}": msms "${m}" is not registered in mech-registry.json`);
12669
- if (role.model && allowedModels && !allowedModels.has(role.model)) issues.push(`role "${name}": model "${role.model}" not in handyman.models (${[...allowedModels].join(", ") || "(none)"})`);
12309
+ if (role.model && allowedModels && !allowedModels.has(role.model)) issues.push(`role "${name}": model "${role.model}" not in cheap-worker.models (${[...allowedModels].join(", ") || "(none)"})`);
12670
12310
  }
12671
12311
  return {
12672
12312
  ok: issues.length === 0,
@@ -12683,7 +12323,7 @@ function listSkiffRoles(root) {
12683
12323
  };
12684
12324
  const items = [...roles.entries()].map(([name, role]) => ({
12685
12325
  name,
12686
- model: role.model ?? "(handyman default)",
12326
+ model: role.model ?? "(cheap-worker default)",
12687
12327
  msms: role.msms ?? [],
12688
12328
  tools: role.tools ?? [],
12689
12329
  trajectory: {
@@ -12913,7 +12553,7 @@ function createContainerAdminTool(ctx) {
12913
12553
  * 注入文本**自带**"请回复 ABCD" ⇒ 模型**复述指令**即**假完成**(高概率,不是小概率)
12914
12554
  * ⇒ 用**每轮新生成**的 nonce + **包装 + 独占一行**(`[[UNATTENDED-DONE:<hex>]]`),
12915
12555
  * 明写「**旧码作废**」,校验**只认当前轮**(防上下文里的旧码复活)。
12916
- * 先例 = `handyman` background 的 `SERENITY_HANDYMAN_DONE_<16hex>`(同族实现)。
12556
+ * 先例 = `cheap-worker` background 的 `SERENITY_CHEAP_WORKER_DONE_<16hex>`(同族实现)。
12917
12557
  */
12918
12558
  /** 完成码包装前缀(**独占一行**才有意义;见文件头"禁用 4 位字母码") */
12919
12559
  const UNATTENDED_DONE_PREFIX = "[[UNATTENDED-DONE:";
@@ -13020,14 +12660,14 @@ function claimsUserNotification(text) {
13020
12660
  /**
13021
12661
  * **不许开**的会话(设计 §3 的"建议写死",本版**确实写死**):
13022
12662
  * `skiff-`(F4 问答 / **微信桥会话也是这个前缀** —— 桥那侧的"用户"是**家人**,
13023
- * 代理替家人说话 = **冒充**)| `acp-`(对外程序化面)| `handyman-`(自主 worker,已有自己的完成码循环)。
12663
+ * 代理替家人说话 = **冒充**)| `acp-`(对外程序化面)| `cheap-worker-`(自主 worker,已有自己的完成码循环)。
13024
12664
  *
13025
12665
  * ⚠️ **`rebuild-` 不在排除面**(与 `output-guard-seam.ts` 的取向**刻意不同**):重建会话是
13026
12666
  * **同一轨迹的载体**(内部维护面),不是对外面;把它排除会让"重建后就没人推了"。
13027
12667
  */
13028
12668
  function isProxyExcludedSessionId(sessionId) {
13029
12669
  if (typeof sessionId !== "string" || sessionId === "") return true;
13030
- return isSkiffSessionId(sessionId) || sessionId.startsWith("acp-") || sessionId.startsWith("handyman-");
12670
+ return isSkiffSessionId(sessionId) || sessionId.startsWith("acp-") || sessionId.startsWith("cheap-worker-");
13031
12671
  }
13032
12672
  /**
13033
12673
  * 一次判定的**唯一入口**(纯函数)。
@@ -14137,7 +13777,7 @@ function createEpochPromotion(promoteEvents, requiredSignals = 1, maxRoundsFallb
14137
13777
  promoted: true
14138
13778
  };
14139
13779
  const loopSid = session.id;
14140
- if (typeof loopSid === "string" && (loopSid.startsWith("handyman-") || isSkiffSessionId(loopSid))) return {
13780
+ if (typeof loopSid === "string" && (loopSid.startsWith("cheap-worker-") || isSkiffSessionId(loopSid))) return {
14141
13781
  boundary: -1,
14142
13782
  promoted: true
14143
13783
  };
@@ -14241,7 +13881,7 @@ function registerBootstrap(ctx) {
14241
13881
  const session = agent.session;
14242
13882
  const depth = session?.header?.delegationDepth ?? 0;
14243
13883
  const sid = typeof session?.id === "string" ? session.id : void 0;
14244
- if (sid !== void 0 && (sid.startsWith("handyman-") || isSkiffSessionId(sid))) return;
13884
+ if (sid !== void 0 && (sid.startsWith("cheap-worker-") || isSkiffSessionId(sid))) return;
14245
13885
  if (depth === 0) {
14246
13886
  if (hasUserMessageHistory(session)) return;
14247
13887
  } else if (sid !== void 0 && anchoredSessions.has(sid)) return;
@@ -14782,7 +14422,7 @@ function accIdentityText(root, configPaths = DEFAULT_SERENITY_CONFIG_PATHS, entr
14782
14422
  }
14783
14423
  /**
14784
14424
  * ACC 注入消息(S134 去重):**只含简短身份锚点**([ACC] 已激活 + CCC 根 + 约束 + Phase 2)。
14785
- * ⚠️ 原文案含「+ handyman 模型」,该注入已于 2026-09-16 删除(§0e-G)⇒ 此处同步订正(散文级残留)。
14425
+ * ⚠️ 原文案含「+ cheap-worker 模型」,该注入已于 2026-09-16 删除(§0e-G)⇒ 此处同步订正(散文级残留)。
14786
14426
  * 完整身份(ACC 5 块 + CCE + Constraints + EAP + SKILL 全文 + Session 块)由**系统提示词层**
14787
14427
  * (systemPrompt.section,每轮 prompt 装配自动注入,含 subagent)承担——对话消息流/压缩重注入
14788
14428
  * 不再重复注入同一内容(token 双倍浪费,见 S134 注入方案梳理)。
@@ -14810,7 +14450,7 @@ function agentScope(agent) {
14810
14450
  * 只有"conversation 根会话"才自动恢复最近激活的宁静号会话——
14811
14451
  * - subagent:session header `origin === 'subagent'`(DSH 路由语义,agent-lookup.ts)
14812
14452
  * - 派生会话:`parentSession` 存在(任何子会话)
14813
- * - handyman 杂工:sessionId 固定 `handyman-` 前缀(tools/handyman.ts 生成)
14453
+ * - cheap-worker 杂工:sessionId 固定 `cheap-worker-` 前缀(tools/cheap-worker.ts 生成)
14814
14454
  * 三者都不恢复(避免把主会话激活注入子上下文,违背 v1.16.2 scope 隔离)。
14815
14455
  */
14816
14456
  function shouldAutoRestore(agent) {
@@ -14818,7 +14458,7 @@ function shouldAutoRestore(agent) {
14818
14458
  if (!session) return false;
14819
14459
  if (session.header?.origin === "subagent") return false;
14820
14460
  if (session.header?.parentSession) return false;
14821
- if (session.id?.startsWith("handyman-")) return false;
14461
+ if (session.id?.startsWith("cheap-worker-")) return false;
14822
14462
  if (isSkiffSessionId(session.id)) return false;
14823
14463
  return true;
14824
14464
  }
@@ -14979,7 +14619,7 @@ function registerCompactRetention(ctx, opts = {}) {
14979
14619
  /**
14980
14620
  * status.ts — 状态与安全模式操作(纯逻辑,零 DSH 依赖,可独立单测)
14981
14621
  *
14982
- * WebUI 停靠栏的数据源:ACC 版本 / CCC 根 / safe-mode 状态 / 黑名单 / keeper 阈值 / handyman 模型。
14622
+ * WebUI 停靠栏的数据源:ACC 版本 / CCC 根 / safe-mode 状态 / 黑名单 / keeper 阈值 / cheap-worker 模型。
14983
14623
  * setSafeMode 直接读写 .serenity-safe-on 标记(守卫实时读取,写即生效)。
14984
14624
  */
14985
14625
  /** 宿主包名 —— 从运行入口反推包根时的**身份核对**用(见 {@link dshVersionFromEntry}) */
@@ -15064,7 +14704,7 @@ function getStatus(cwd, configPaths = DEFAULT_SERENITY_CONFIG_PATHS) {
15064
14704
  safeModeOn: false,
15065
14705
  blacklist: [],
15066
14706
  threshold: null,
15067
- handymanModel: null,
14707
+ cheapWorkerModel: null,
15068
14708
  restrict
15069
14709
  };
15070
14710
  const cfg = loadSerenityConfig(root, configPaths);
@@ -15074,7 +14714,7 @@ function getStatus(cwd, configPaths = DEFAULT_SERENITY_CONFIG_PATHS) {
15074
14714
  safeModeOn: isSafeModeOn(root),
15075
14715
  blacklist: readBlacklist(root, configPaths),
15076
14716
  threshold: cfg.sessionKeeper?.threshold ?? null,
15077
- handymanModel: readHandymanConfig(root, configPaths)?.defaultModel ?? null,
14717
+ cheapWorkerModel: readCheapWorkerConfig(root, configPaths)?.defaultModel ?? null,
15078
14718
  restrict
15079
14719
  };
15080
14720
  }
@@ -15089,7 +14729,6 @@ function setSafeMode(root, on) {
15089
14729
  //#endregion
15090
14730
  //#region src/api.ts
15091
14731
  const ROUTE_PATH = "/serenity/status";
15092
- const HANDYMEN_PATH = "/serenity/handymen";
15093
14732
  const UPLOAD_PATH = "/serenity/image-upload";
15094
14733
  const FILE_UPLOAD_PATH = "/serenity/file-upload";
15095
14734
  const CONFIG_PATH = "/serenity/config";
@@ -15244,31 +14883,6 @@ function resolveWorkspace(ctx, params) {
15244
14883
  }
15245
14884
  function registerStatusApi(ctx, opts = {}) {
15246
14885
  const configPaths = opts.configPaths ?? DEFAULT_SERENITY_CONFIG_PATHS;
15247
- ctx.webServer.register({
15248
- kind: "exact",
15249
- path: HANDYMEN_PATH,
15250
- handler: async (req, res) => {
15251
- try {
15252
- if (req.method !== "GET") {
15253
- sendJson$4(res, 405, { error: "method not allowed" });
15254
- return;
15255
- }
15256
- const url = new URL(req.url ?? "/", "http://127.0.0.1");
15257
- const workspace = resolveWorkspace(ctx, {
15258
- sessionId: url.searchParams.get("sessionId") ?? void 0,
15259
- workspace: url.searchParams.get("workspace") ?? void 0
15260
- });
15261
- const root = cccRootForCwd(workspace);
15262
- if (!root) {
15263
- sendJson$4(res, 200, { handymen: [] });
15264
- return;
15265
- }
15266
- sendJson$4(res, 200, { handymen: listActiveHandymen(root) });
15267
- } catch (err) {
15268
- sendJson$4(res, 400, { error: err.message ?? String(err) });
15269
- }
15270
- }
15271
- });
15272
14886
  ctx.webServer.register({
15273
14887
  kind: "exact",
15274
14888
  path: UPLOAD_PATH,
@@ -15482,11 +15096,11 @@ function registerStatusApi(ctx, opts = {}) {
15482
15096
  sendJson$4(res, 400, { error: chk.error });
15483
15097
  return;
15484
15098
  }
15485
- const { readWeixinSettings } = await import("./weixin-route-BNyH5ghC.js").then((n) => n.u);
15099
+ const { readWeixinSettings } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
15486
15100
  const { weixinBridgeStatus } = await Promise.resolve().then(() => weixin_bridge_exports);
15487
15101
  const settings = readWeixinSettings(chk.root);
15488
15102
  const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
15489
- const { readWeixinCredential } = await import("./weixin-route-BNyH5ghC.js").then((n) => n.u);
15103
+ const { readWeixinCredential } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
15490
15104
  const accounts = (settings.accounts ?? []).map((a) => ({
15491
15105
  accountId: a.accountId,
15492
15106
  name: a.name ?? void 0,
@@ -15518,7 +15132,7 @@ function registerStatusApi(ctx, opts = {}) {
15518
15132
  const { fetchQRCode } = await import("./weixin-api-BrRcPiQs.js").then((n) => n.u);
15519
15133
  const { randomUUID } = await import("node:crypto");
15520
15134
  for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
15521
- const { readWeixinSettings } = await import("./weixin-route-BNyH5ghC.js").then((n) => n.u);
15135
+ const { readWeixinSettings } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
15522
15136
  const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
15523
15137
  const loginKey = randomUUID();
15524
15138
  weixinLogins.set(loginKey, {
@@ -15539,7 +15153,7 @@ function registerStatusApi(ctx, opts = {}) {
15539
15153
  sendJson$4(res, 400, { error: "missing accountId" });
15540
15154
  return;
15541
15155
  }
15542
- const { removeWeixinAccount } = await import("./weixin-route-BNyH5ghC.js").then((n) => n.u);
15156
+ const { removeWeixinAccount } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
15543
15157
  const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
15544
15158
  removeWeixinAccount(root, body.accountId);
15545
15159
  syncCccBridge(ctx, root);
@@ -15552,8 +15166,8 @@ function registerStatusApi(ctx, opts = {}) {
15552
15166
  sendJson$4(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
15553
15167
  return;
15554
15168
  }
15555
- const { saveWeixinRoutes } = await import("./weixin-route-BNyH5ghC.js").then((n) => n.u);
15556
- const { readSkiffRoles } = await import("./skiff-role-BSg6Qu9k.js").then((n) => n.u);
15169
+ const { saveWeixinRoutes } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
15170
+ const { readSkiffRoles } = await import("./skiff-role-B615L9MV.js").then((n) => n.u);
15557
15171
  const roles = readSkiffRoles(root);
15558
15172
  for (const r of routes) if (!roles.has(r.role)) {
15559
15173
  sendJson$4(res, 400, { error: `unknown role: ${r.role} (not in ${root} skiff.roles)` });
@@ -15565,7 +15179,7 @@ function registerStatusApi(ctx, opts = {}) {
15565
15179
  }
15566
15180
  if (body.action === "set-enabled") {
15567
15181
  const enabled = body.enabled === true;
15568
- const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-BNyH5ghC.js").then((n) => n.u);
15182
+ const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
15569
15183
  const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
15570
15184
  const settings = readWeixinSettings(root);
15571
15185
  if (enabled && (settings.accounts ?? []).length === 0) {
@@ -15609,7 +15223,7 @@ function registerStatusApi(ctx, opts = {}) {
15609
15223
  return;
15610
15224
  }
15611
15225
  const { pollQRStatus } = await import("./weixin-api-BrRcPiQs.js").then((n) => n.u);
15612
- const { readWeixinSettings } = await import("./weixin-route-BNyH5ghC.js").then((n) => n.u);
15226
+ const { readWeixinSettings } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
15613
15227
  const settings = readWeixinSettings(login.root);
15614
15228
  const status = await pollQRStatus({
15615
15229
  baseUrl: void 0,
@@ -15624,7 +15238,7 @@ function registerStatusApi(ctx, opts = {}) {
15624
15238
  });
15625
15239
  return;
15626
15240
  }
15627
- const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-BNyH5ghC.js").then((n) => n.u);
15241
+ const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
15628
15242
  const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
15629
15243
  const accountId = nextWeixinAccountId(settings);
15630
15244
  upsertWeixinAccount(login.root, {
@@ -15988,7 +15602,7 @@ const HOST_SERVICES = [
15988
15602
  name: "get",
15989
15603
  kind: "function"
15990
15604
  }],
15991
- impact: "skiff/handyman 无法创建或复用 agent",
15605
+ impact: "skiff/cheap-worker 无法创建或复用 agent",
15992
15606
  required: true
15993
15607
  },
15994
15608
  {
@@ -16070,7 +15684,7 @@ const HOST_SERVICES = [
16070
15684
  name: "agentLoop",
16071
15685
  access: "injected",
16072
15686
  members: [],
16073
- impact: "handyman worker agent 无法装配 preset",
15687
+ impact: "cheap-worker worker agent 无法装配 preset",
16074
15688
  required: false
16075
15689
  },
16076
15690
  {
@@ -16081,7 +15695,7 @@ const HOST_SERVICES = [
16081
15695
  name: "start",
16082
15696
  kind: "function"
16083
15697
  }],
16084
- impact: "handyman foreground 模式不可用(响亮报错,不影响 background 模式)",
15698
+ impact: "cheap-worker 委派不可用(前台委派的正门,响亮报错)",
16085
15699
  required: false
16086
15700
  },
16087
15701
  {
@@ -17551,7 +17165,7 @@ function sessionNameFromMdPath(mdPath) {
17551
17165
  * v1.30.13(S142 用户"微信桥 + skiff 会话锚定要准确",诊断 D4):**移除**原 ④「全局最新未完成
17552
17166
  * 会话」(`findLatestActiveSessionMd`)。理由(R↓):④ 是**跨轨迹**猜测——候选①②③全空时,
17553
17167
  * 它会把 rebuild 静默接到 AGENT_SESSIONS 里最新的另一个会话(临时 skiff 会话 persistent=false、
17554
- * 重启后首条消息前、Danica 新会话都命中该形态),而"接错轨迹"比重建失败危险得多
17168
+ * 重启后首条消息前、新会话首条消息都命中该形态),而"接错轨迹"比重建失败危险得多
17555
17169
  * (错误轨迹被继续写入 + 正确轨迹静默丢失)。现在全部候选失败 → 返回 null → 调用方
17556
17170
  * 报错引导用户显式 `container_trajectory use`(响亮失败优于静默接错)。
17557
17171
  * 备选:④ 仅在"同 scope 确无绑定"时兜底——同样会接错轨迹,故不采纳。
@@ -19891,7 +19505,7 @@ async function handle$1(ctx, defaultRoot, webPort, req, res) {
19891
19505
  agent = live;
19892
19506
  continued = true;
19893
19507
  }
19894
- const hc = readHandymanConfig(ccc);
19508
+ const hc = readCheapWorkerConfig(ccc);
19895
19509
  if (!agent) agent = (await createSkiffAgent(ctx, ccc, roleName, role, hc?.defaultModel)).agent;
19896
19510
  const result = await askSkiff(ctx, agent, question, 0);
19897
19511
  sendJson$2(res, 200, {
@@ -20416,7 +20030,7 @@ var AcpServer = class {
20416
20030
  continued: true
20417
20031
  };
20418
20032
  }
20419
- const hc = readHandymanConfig(ccc);
20033
+ const hc = readCheapWorkerConfig(ccc);
20420
20034
  return {
20421
20035
  sessionId: (await createSkiffAgent(this.ctx, ccc, roleName, role, hc?.defaultModel)).sessionId,
20422
20036
  role: roleName,
@@ -20761,7 +20375,7 @@ async function handleAskParsed(ctx, ccc, body, res, ip) {
20761
20375
  continued = true;
20762
20376
  }
20763
20377
  }
20764
- if (!agent) agent = (await createSkiffAgent(ctx, root, effectiveRole, effectiveCfg, readHandymanConfig(root)?.defaultModel)).agent;
20378
+ if (!agent) agent = (await createSkiffAgent(ctx, root, effectiveRole, effectiveCfg, readCheapWorkerConfig(root)?.defaultModel)).agent;
20765
20379
  const result = await askSkiff(ctx, agent, question, void 0, { includeTrajectory: false });
20766
20380
  sendJson$1(res, 200, {
20767
20381
  answer: result.answer,
@@ -21742,7 +21356,7 @@ async function runAccountLoop(ctx, root, accountId, cred, loop, machine = false)
21742
21356
  *
21743
21357
  * 边界(R↓):ACC 只提供**机制与数据**——"桥不转发最终文本"这个事实 + 三个已填好的参数值
21744
21358
  * (CCC 根 / 账号 / 用户 id)。**纪律措辞归 CCC**:怎么写、必须怎么做、后果是什么,全部写在
21745
- * CCC 的角色提示词文件里(如 `.opencode/skiff/zhaocai.md` 的「输出通道」节),CCC 可随时改,
21359
+ * CCC 的角色提示词文件里(如 `.opencode/skiff/<role>.md` 的「输出通道」节),CCC 可随时改,
21746
21360
  * 且 v1.30.15 起角色提示词热重载——改完立即生效,无需 ACC 发版。
21747
21361
  *
21748
21362
  * 为什么不用 ACC 写措辞(历史教训):v1.30.10~v1.30.15 由 ACC 内嵌一段纪律文案,用户实测
@@ -21780,7 +21394,7 @@ function manualOutputFallbackNeeded(input) {
21780
21394
  */
21781
21395
  async function resolveCccRootByAlias(ctx, alias) {
21782
21396
  try {
21783
- const { listCccs } = await import("./ccc-roots-Bo0HKLlM.js").then((n) => n.a);
21397
+ const { listCccs } = await import("./ccc-roots-BC8mn8iM.js").then((n) => n.a);
21784
21398
  const entries = await listCccs(ctx);
21785
21399
  const byName = entries.find((e) => e.name === alias);
21786
21400
  if (byName) return byName.root;
@@ -21929,7 +21543,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21929
21543
  try {
21930
21544
  const sessionId = weixinSessionIdFor(fromUserId);
21931
21545
  const existing = getSkiffAgent(sessionId);
21932
- const hc = readHandymanConfig(root);
21546
+ const hc = readCheapWorkerConfig(root);
21933
21547
  if (existing) try {
21934
21548
  ensureSkiffSession(root, existing, roleName, role, true);
21935
21549
  } catch (err) {
@@ -22279,7 +21893,7 @@ function syncHumanChannel(ctx) {
22279
21893
  (async () => {
22280
21894
  try {
22281
21895
  stopMachineLoops();
22282
- const { listCccs } = await import("./ccc-roots-Bo0HKLlM.js").then((n) => n.a);
21896
+ const { listCccs } = await import("./ccc-roots-BC8mn8iM.js").then((n) => n.a);
22283
21897
  for (const entry of await listCccs(ctx)) syncCccBridge(ctx, entry.root);
22284
21898
  } catch {}
22285
21899
  })();
@@ -22447,7 +22061,7 @@ function parseSendRequest(body) {
22447
22061
  if (!user.includes("@")) return {
22448
22062
  ok: false,
22449
22063
  error: `invalid "user": ${user}`,
22450
- remediation: "需为 iLink from_user_id(含 @);别名(yh/danica)请在调用方解析"
22064
+ remediation: "需为 iLink from_user_id(含 @);别名请在调用方解析"
22451
22065
  };
22452
22066
  const text = typeof o.text === "string" ? o.text.trim() : "";
22453
22067
  if (text === "") return {
@@ -22515,7 +22129,7 @@ function matchCcc(input, candidates) {
22515
22129
  }
22516
22130
  /** 组装候选列表(`ccc-roots.listCccs` 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
22517
22131
  async function collectCandidates(ctx) {
22518
- const { listCccs } = await import("./ccc-roots-Bo0HKLlM.js").then((n) => n.a);
22132
+ const { listCccs } = await import("./ccc-roots-BC8mn8iM.js").then((n) => n.a);
22519
22133
  const entries = await listCccs(ctx, { defaultRoot: process.cwd() });
22520
22134
  const seen = /* @__PURE__ */ new Set();
22521
22135
  const out = [];
@@ -23698,14 +23312,19 @@ function registerDeepseekVisionPatch(ctx, enabled) {
23698
23312
  * - `sendFile` 走 iLink 上传(`weixin-api.sendFileMessage`)+ 同一 outgoing hook(带 `file` 元数据)
23699
23313
  */
23700
23314
  /**
23701
- * 别名 → CCC localstore 凭据键(与 v1.30.9 CCC MSM 逐字一致,迁移零认知成本)。
23702
- * 别名解析属"数据填值",归 ACC(D23)。
23315
+ * 解析该 CCC 的别名表(v1.56.10)。
23316
+ *
23317
+ * 🔴 **为什么不再硬编码**(2026-10-14 隐私核查):别名是**使用者自己的称呼**,属 CCC 的数据
23318
+ * (D23:加一个别名不该要发版)。此前硬编码在 ACC 源码 ⇒ **这些称呼随 npm 包公开发布**。
23319
+ * 现在别名**只**来自该 CCC 的 `.opencode/serenity.json` → `weixin.aliases`;
23320
+ * ACC 不认识任何具体名字。未配置 ⇒ `{}`(此时只能用裸 iLink 用户 id)。
23321
+ *
23322
+ * ⚠️ **迁移**:升级前用过别名的部署,需把原表补进自己的 CCC 配置,否则别名解析不到
23323
+ * (见 CHANGELOG v1.56.10 的迁移段;裸 `xxx@im.wechat` 形式始终可用)。
23703
23324
  */
23704
- const WEIXIN_ALIAS_MAP = {
23705
- yh: "WEIXIN_USER_YH",
23706
- danica: "WEIXIN_USER_DANICA",
23707
- xiaowang: "WEIXIN_USER_DANICA"
23708
- };
23325
+ function weixinAliasMap(root) {
23326
+ return readWeixinSettings(root).aliases ?? {};
23327
+ }
23709
23328
  /** 读该 CCC 的一个凭据值(空/缺失 → null) */
23710
23329
  function credentialValue(root, key) {
23711
23330
  try {
@@ -23728,7 +23347,7 @@ const weixinChannel = {
23728
23347
  const raw = input.trim();
23729
23348
  if (raw === "") return null;
23730
23349
  const alias = raw.toLowerCase();
23731
- const key = WEIXIN_ALIAS_MAP[alias];
23350
+ const key = weixinAliasMap(root)[alias];
23732
23351
  if (key) {
23733
23352
  const id = credentialValue(root, key);
23734
23353
  return id ? {
@@ -23744,7 +23363,7 @@ const weixinChannel = {
23744
23363
  },
23745
23364
  listUsers(root) {
23746
23365
  const out = [];
23747
- for (const [alias, key] of Object.entries(WEIXIN_ALIAS_MAP)) {
23366
+ for (const [alias, key] of Object.entries(weixinAliasMap(root))) {
23748
23367
  const id = credentialValue(root, key);
23749
23368
  if (id) out.push({
23750
23369
  alias,
@@ -23868,7 +23487,7 @@ function renderText(value) {
23868
23487
  function createImBridgeTool() {
23869
23488
  return defineTool({
23870
23489
  name: "im-bridge",
23871
- description: `Send messages to family members through an IM channel configured for THIS container (currently: weixin / 微信). Every successful send is recorded in the container log automatically; this tool only acts on the current container (no target container parameter). It is hidden entirely when this container has no IM channel configured. Channels: ${imChannelIds().join(", ") || "(none registered)"}. Actions: send (text) / send-file (a file inside this container) / users (list configured recipients) / status (channel health). user accepts a configured alias (e.g. yh, danica) or a raw channel user id. Failures return a stable code plus a remediation hint.`,
23490
+ description: `Send messages to family members through an IM channel configured for THIS container (currently: weixin / 微信). Every successful send is recorded in the container log automatically; this tool only acts on the current container (no target container parameter). It is hidden entirely when this container has no IM channel configured. Channels: ${imChannelIds().join(", ") || "(none registered)"}. Actions: send (text) / send-file (a file inside this container) / users (list configured recipients) / status (channel health). user accepts a configured alias (e.g. user-a, user-b) or a raw channel user id. Failures return a stable code plus a remediation hint.`,
23872
23491
  parameters: {
23873
23492
  channel: {
23874
23493
  type: "string",
@@ -23883,7 +23502,7 @@ function createImBridgeTool() {
23883
23502
  },
23884
23503
  user: {
23885
23504
  type: "string",
23886
- description: "Recipient: configured alias (yh, danica) or raw channel user id (send / send-file)"
23505
+ description: "Recipient: configured alias (user-a, user-b) or raw channel user id (send / send-file)"
23887
23506
  },
23888
23507
  text: {
23889
23508
  type: "string",
@@ -25401,7 +25020,7 @@ function apply(ctx, config) {
25401
25020
  ctx.tools.register(praxisTool);
25402
25021
  ctx.tools.register(createOntologyTool());
25403
25022
  ctx.tools.register(createSkillCreatorTool());
25404
- ctx.tools.register(createHandymanTool(ctx));
25023
+ ctx.tools.register(createCheapWorkerTool(ctx));
25405
25024
  ctx.tools.register(localstoreTool);
25406
25025
  ctx.tools.register(createContainerAdminTool(ctx));
25407
25026
  ctx.tools.register(createImBridgeTool());