@shgroup/dsh-serenity-hooks 1.56.10 → 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.
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-CDW2dO0n.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-CQNfQaTx.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-CxCciqQl.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-Det66X16.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";
@@ -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)。
@@ -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)
1999
1913
 
2000
- ## Two modes (v1.31.3; default = foreground)
2001
- | | foreground (default) | background |
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.
1918
+
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
  /**
@@ -3446,7 +3326,7 @@ function toolsBlock() {
3446
3326
  " msm — execute a registered CCC MSM: msm(name, args); partial name returns candidates; inspect=true shows usage",
3447
3327
  " praxis — actionable theory injection: praxis (index) / praxis eap / praxis neat / praxis cce / praxis ontology",
3448
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",
3449
- " 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",
3450
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",
3451
3331
  " localstore — ACC local credential/config storage (CCC-root localstore.json; git policy localstore.gitTrack default deny)",
3452
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)",
@@ -4152,7 +4032,7 @@ function ensureSkiffSession(root, agent, roleName, role, persistent = true) {
4152
4032
  return mdPath;
4153
4033
  }
4154
4034
  /** Skiff agent 挂载的 DSH preset(v1.25.3 修复:read/grep/glob 等平台工具由 preset 决定工具面;
4155
- * handyman 经 composeFrom 继承父、skiff 无父上下文——直接挂 DSH 默认 standard preset;
4035
+ * cheap-worker 经 composeFrom 继承父、skiff 无父上下文——直接挂 DSH 默认 standard preset;
4156
4036
  * guard 角色白名单再按角色过滤可见/可用面——白名单外工具仍 deny) */
4157
4037
  const SKIFF_PRESET = "standard";
4158
4038
  /**
@@ -4315,8 +4195,8 @@ function sessionEvents(session) {
4315
4195
  if (typeof s.snapshotEvents === "function") return s.snapshotEvents();
4316
4196
  return s.events ?? [];
4317
4197
  }
4318
- /** 读会话最后一个 assistant/message 文本(handyman 同款) */
4319
- function lastAssistantText$2(agent) {
4198
+ /** 读会话最后一个 assistant/message 文本(cheap-worker 同款) */
4199
+ function lastAssistantText$1(agent) {
4320
4200
  const events = sessionEvents(agent.session);
4321
4201
  for (let i = events.length - 1; i >= 0; i--) {
4322
4202
  const e = events[i];
@@ -4390,7 +4270,7 @@ async function askSkiff(ctx, agent, question, eventsStart, options) {
4390
4270
  source: PLUGIN_SOURCE
4391
4271
  }));
4392
4272
  await waitAgentIdle(ctx, agent);
4393
- const answer = lastAssistantText$2(agent);
4273
+ const answer = lastAssistantText$1(agent);
4394
4274
  const trajectory = options?.includeTrajectory === false ? [] : eventsToTrajectory(sessionEvents(agent.session).slice(before));
4395
4275
  return {
4396
4276
  answer,
@@ -9306,49 +9186,23 @@ function createSkillCreatorTool() {
9306
9186
  });
9307
9187
  }
9308
9188
  //#endregion
9309
- //#region src/handyman-preset-inherit.ts
9189
+ //#region src/tools/cheap-worker.ts
9310
9190
  /**
9311
- * 解析 handyman worker 从父 agent 继承的 preset,并组装创建 setup 钩子。
9312
- * @param parentCtx - 发起 handyman 的 agent 的 scope ctx;无(headless/非 agent 上下文)时为 undefined。
9313
- * @returns 继承结果:meta 用的 agentPreset 与创建 setup 钩子。
9314
- */
9315
- function handymanPresetInheritance(parentCtx) {
9316
- if (parentCtx === void 0) return { setup: (childCtx) => {
9317
- childCtx.tools.restrict({ deny: ["handyman"] });
9318
- } };
9319
- const agentPreset = parentCtx.get("agentPresets")?.composedPreset(parentCtx);
9320
- if (agentPreset === void 0) return { setup: (childCtx) => {
9321
- childCtx.tools.restrict({ deny: ["handyman"] });
9322
- } };
9323
- return {
9324
- agentPreset,
9325
- setup: (childCtx) => {
9326
- childCtx.get("agentPresets")?.composeFrom(childCtx, parentCtx);
9327
- childCtx.tools.restrict({ deny: ["handyman"] });
9328
- }
9329
- };
9330
- }
9331
- //#endregion
9332
- //#region src/tools/handyman.ts
9333
- /**
9334
- * handyman.ts — handyman(杂工)真实 DSH 工具(v1.24.0:loop 牛马 → handyman 杂工)
9191
+ * cheap-worker.ts — cheap-worker(杂工)真实 DSH 工具
9192
+ *
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、等它跑完、取回最终文本。
9335
9197
  *
9336
- * 用户需求(S142 拍板):
9337
- * - **同步**(用户明确:这个事一般不会异步——工具阻塞到完成)
9338
- * - **指定模型**(CCC 白名单内——handyman.models,未配置报错)
9339
- * - **自主循环到完成**(stop-token 唯一完成判据,对齐 osp loop)
9340
- * - **内部递归同样低能 subagent**(worker 工具面含 subagent,DSH 原生模型继承)
9341
- * - **workflow 能力**(jobs 编排:主 agent 一次并行多个杂工,maxParallel 默认 10)
9342
- * - **worker 不含 handyman 本身**(递归编排归主 agent,防无限嵌套)
9343
- * - **不兼容旧 loop 进度文件**(仅 handyman- 前缀)
9198
+ * 机制:走宿主**委派正门** `ctx.subagents.start('spawn', …)`(深度上限、子 agent 所有权、
9199
+ * `subagent/start|end` 事件、`dispose()` 收尾全部沿用宿主实现),模型经 `agentOptions` 注入。
9200
+ * 前置:`exec.agent` 必须存在(前台委派要有发起方)。
9344
9201
  *
9345
- * 机制:ctx.agents.create()(带 setup 钩子)创建专用 agent(进程内),
9346
- * 每轮 followup → agent/status idle → 读 session.events 响应 → 写进度 → stop token 检查 →
9347
- * 未完成继续下一轮;followup/等待空闲抛错(非正常停止)→ dispose 并重新 create agent
9348
- * (重启计数,≤100),同一轮重试。工厂模式:apply 时闭包捕获插件 ctx(工具 execute 无 ctx 参数)。
9202
+ * 🔴 **模型不可由调用方指定**(owner 令):**没有 `model` 参数**,一律读 CCC 配置
9203
+ * `cheap-worker.models` / `cheap-worker.defaultModel`(单一真相源)。
9349
9204
  *
9350
- * preset 继承 + 工具收窄:setup 钩子里对子 agent 执行 agentPresets.composeFrom(对齐
9351
- * subagent 先例)+ tools.restrict deny handyman(worker 内部看不到 handyman 工具)。
9205
+ * 递归防护:子 agent 的工具面 deny `cheap-worker` 本身(编排归主 agent,防无限嵌套)。
9352
9206
  */
9353
9207
  function renderText$4(value) {
9354
9208
  return [{
@@ -9356,52 +9210,18 @@ function renderText$4(value) {
9356
9210
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
9357
9211
  }];
9358
9212
  }
9359
- /** 读取会话最后一个 assistant/message 文本 */
9360
- function lastAssistantText$1(agent) {
9361
- const s = agent.session;
9362
- const events = typeof s.snapshotEvents === "function" ? s.snapshotEvents() : s.events ?? [];
9363
- for (let i = events.length - 1; i >= 0; i--) {
9364
- const e = events[i];
9365
- if (e && e.type === "assistant/message") {
9366
- const data = e.data;
9367
- const text = (data?.message?.content ?? data?.content ?? []).filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
9368
- if (text) return text;
9369
- }
9370
- }
9371
- return "";
9372
- }
9373
- /** 非正常停止时重启 agent 的次数上限(防死循环保险阀):对话轮次有上限,重启有上限 */
9374
- const HANDYMAN_MAX_RESTARTS = 100;
9375
- /** 运行时解析 jobs 参数(DSH schema 不支持 object items 的 required/校验 → 手工校验类型) */
9376
- function parseJobs(raw) {
9377
- if (!Array.isArray(raw) || raw.length === 0) return null;
9378
- const jobs = [];
9379
- for (const item of raw) {
9380
- if (typeof item !== "object" || item === null) return null;
9381
- const rec = item;
9382
- if (typeof rec.task !== "string" || typeof rec.label !== "string") return null;
9383
- jobs.push({
9384
- task: rec.task,
9385
- label: rec.label,
9386
- model: typeof rec.model === "string" ? rec.model : void 0
9387
- });
9388
- }
9389
- return jobs;
9390
- }
9391
9213
  /**
9392
- * 🔴 **foreground 并行上限 = 10**(owner 2026-09-29 具名令:「这个版本顺手帮我把 handyman 并行度从 5 调整成 10」;
9214
+ * 🔴 **前台并行上限 = 10**(owner 2026-09-29 具名令:「这个版本顺手帮我把 handyman 并行度从 5 调整成 10」;
9393
9215
  * 原值 5 出自 owner 2026-09-27 的具名令「最大 5;就当是支持自定模型的 subagent 用了,急需」)。
9394
9216
  *
9395
9217
  * 为什么是**硬编码**而不是又一个配置键:owner 两次都是**直接给数**(同 `REBUILD-TODO` 那次的裁决口径
9396
9218
  * —— 设定项能不加就不加,见 S142 的「可砍设定项」)。若将来还要调,仍按具名令改这个常量。
9397
9219
  *
9398
- * 为什么 foreground 仍与 background 分开(后台 = `handyman.maxParallel`,缺省 **10**):**foreground 会占住
9399
- * 主 agent 的这一轮**(调用方在等结果),并发越高越容易把宿主的并发面与用量推上去。
9400
- * ⚠️ 现在两档**数值相等但语义不同**(前台 = 占住本轮 / 后台 = 不占)⇒ **刻意不合并成一个常量**:
9401
- * 合并会让"哪天要给前台单独降档"变成一次语义手术。
9220
+ * 为什么是 10 而不是更高:**前台委派会占住主 agent 的这一轮**(调用方在等结果),
9221
+ * 并发越高越容易把宿主的并发面与用量推上去。
9402
9222
  */
9403
9223
  const FOREGROUND_MAX_PARALLEL = 10;
9404
- /** foreground 的 jobs 解析(与 background 的差别:**label 可选** —— 前台 label 只是显示名,不命名进度文件) */
9224
+ /** foreground 的 jobs 解析(label 可选 —— 前台 label 只是显示名,不命名任何文件) */
9405
9225
  function parseForegroundJobs(raw) {
9406
9226
  if (!Array.isArray(raw) || raw.length === 0) return null;
9407
9227
  const jobs = [];
@@ -9411,8 +9231,7 @@ function parseForegroundJobs(raw) {
9411
9231
  if (typeof rec.task !== "string" || rec.task.trim() === "") return null;
9412
9232
  jobs.push({
9413
9233
  task: rec.task,
9414
- ...typeof rec.label === "string" && rec.label !== "" ? { label: rec.label } : {},
9415
- ...typeof rec.model === "string" && rec.model !== "" ? { model: rec.model } : {}
9234
+ ...typeof rec.label === "string" && rec.label !== "" ? { label: rec.label } : {}
9416
9235
  });
9417
9236
  }
9418
9237
  return jobs;
@@ -9429,8 +9248,8 @@ function subagentOutputText(output) {
9429
9248
  * ⇒ 一律**不阻断**,保持旧行为 —— 增强不得变成新的失败源)。
9430
9249
  *
9431
9250
  * 为什么必须放在"创建之前":宿主对未知 provider 抛 `no adapter registered for provider "…"`
9432
- * (`dsh-llm`,code `NO_ADAPTER`)。若照常创建:前台只拿回 `stopReason:"error"` + **空
9433
- * `diagnostic`**(实测),后台整条失败且**不带原因** ⇒ 调用方(LLM)无法自我修正配置。
9251
+ * (`dsh-llm`,code `NO_ADAPTER`)。若照常创建:只拿回 `stopReason:"error"` + **空
9252
+ * `diagnostic`**(实测)⇒ 调用方(LLM)无法自我修正配置。
9434
9253
  */
9435
9254
  function providerPreflight(ctx, model) {
9436
9255
  const llm = hostLlm(ctx);
@@ -9455,14 +9274,13 @@ function providerPreflight(ctx, model) {
9455
9274
  });
9456
9275
  }
9457
9276
  /**
9458
- * foreground 模式:单次前台串行委派(用户裁决"本次所需的简单实现")。
9277
+ * 单次前台串行委派。
9459
9278
  *
9460
- * 与 background 的差别(R↓):
9461
9279
  * - 走宿主**委派正门** `ctx.subagents.start('spawn', …)`——深度上限、子 agent 所有权、
9462
9280
  * `subagent/start|end` 生命周期事件、`dispose()` 收尾全部沿用宿主实现(不重写 agent 创建);
9463
9281
  * - 模型经 `agentOptions` 注入(spawn 后端能力位 `agentOptions: true`),因此**不依赖**
9464
9282
  * 宿主未放开的 `subagent-model-selection` 设置;
9465
- * - 不循环、不写进度文件、不做完成码校验——"一次调用一次结果"。
9283
+ * - 不循环、不写文件、不做完成码校验——"一次调用一次结果"。
9466
9284
  *
9467
9285
  * 前置条件:`opts.parent` 必须是发起 agent(由调用方 `execute` 在解析 CCC 之前校验;
9468
9286
  * 本函数不再重复检查——单一真相源)。
@@ -9471,7 +9289,7 @@ async function runForegroundJob(ctx, opts) {
9471
9289
  const { task, label, model, models, parent, signal } = opts;
9472
9290
  requireWhitelistedModel(model, models);
9473
9291
  const subagents = hostSubagents(ctx);
9474
- 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\").");
9475
9293
  const preflight = providerPreflight(ctx, model);
9476
9294
  if (preflight !== null) return {
9477
9295
  mode: "foreground",
@@ -9494,7 +9312,7 @@ async function runForegroundJob(ctx, opts) {
9494
9312
  ...provider === void 0 ? {} : { provider },
9495
9313
  model: modelName
9496
9314
  },
9497
- toolFilter: { deny: ["handyman"] }
9315
+ toolFilter: { deny: ["cheap-worker"] }
9498
9316
  });
9499
9317
  try {
9500
9318
  const result = run.result === void 0 ? {} : await run.result;
@@ -9512,165 +9330,28 @@ async function runForegroundJob(ctx, opts) {
9512
9330
  if (run.dispose) await run.dispose().catch(() => {});
9513
9331
  }
9514
9332
  }
9515
- /**
9516
- * 驱动单个 handyman worker:创建 agent → while 循环(stop-token 完成判据)→ dispose。
9517
- * 同步阻塞直到完成/保险阀。jobs 编排时多个 worker 并行(Promise.all)。
9518
- */
9519
- async function runHandymanJob(ctx, opts) {
9520
- const { root, job, defaultModel, models, maxRounds, parentSession, parentCtx, session } = opts;
9521
- const label = job.label;
9522
- const model = job.model ?? defaultModel;
9523
- requireWhitelistedModel(model, models);
9524
- const preflightError = providerPreflight(ctx, model);
9525
- if (preflightError !== null) throw new Error(preflightError);
9526
- const { provider, model: modelName } = splitModel(model);
9527
- const stopToken = newStopToken();
9528
- let progress = readProgress(root, label);
9529
- const startRound = progress ? progress.round + 1 : 1;
9530
- const inherited = handymanPresetInheritance(parentCtx);
9531
- if (!ctx.agents) throw new Error("handyman: ctx.agents unavailable");
9532
- let handle;
9533
- let workerAgent;
9534
- const spawnAgent = async () => {
9535
- const sessionId = `handyman-${label}-${randomUUID()}`;
9536
- handle = await ctx.agents.create({
9537
- sessionId,
9538
- meta: {
9539
- cwd: root,
9540
- origin: "subagent",
9541
- ...parentSession === void 0 ? {} : { parentSession },
9542
- ...inherited.agentPreset === void 0 ? {} : { agentPreset: inherited.agentPreset }
9543
- },
9544
- agentOptions: {
9545
- provider,
9546
- model: modelName
9547
- },
9548
- ...inherited.setup === void 0 ? {} : { setup: inherited.setup }
9549
- });
9550
- workerAgent = handle.agent;
9551
- };
9552
- await spawnAgent();
9553
- let done = false;
9554
- let lastResponse = progress?.lastResponse ?? "";
9555
- let finalRound = startRound - 1;
9556
- let restarts = 0;
9557
- let finishReason = "done";
9558
- try {
9559
- let round = startRound;
9560
- while (true) {
9561
- if (round > maxRounds) {
9562
- finishReason = "max_rounds";
9563
- break;
9564
- }
9565
- finalRound = round;
9566
- const prompt = buildRoundPrompt({
9567
- root,
9568
- session,
9569
- label,
9570
- round,
9571
- stopToken,
9572
- progress,
9573
- task: job.task
9574
- });
9575
- try {
9576
- workerAgent.followup(createUserMessage({
9577
- content: [{
9578
- type: "text",
9579
- text: prompt
9580
- }],
9581
- source: PLUGIN_SOURCE
9582
- }));
9583
- await waitAgentIdle(ctx, workerAgent);
9584
- lastResponse = lastAssistantText$1(workerAgent);
9585
- } catch {
9586
- restarts++;
9587
- if (restarts > HANDYMAN_MAX_RESTARTS) {
9588
- finishReason = "restart_exceeded";
9589
- break;
9590
- }
9591
- await handle.dispose().catch(() => {});
9592
- await spawnAgent();
9593
- continue;
9594
- }
9595
- progress = {
9596
- round,
9597
- done: false,
9598
- label,
9599
- model,
9600
- updated: isoLocal(),
9601
- lastResponse
9602
- };
9603
- writeProgress(root, label, progress);
9604
- if (lastResponse.includes(stopToken)) {
9605
- done = true;
9606
- finishReason = "done";
9607
- break;
9608
- }
9609
- round++;
9610
- }
9611
- writeProgress(root, label, {
9612
- round: finalRound,
9613
- done,
9614
- label,
9615
- model,
9616
- updated: isoLocal(),
9617
- lastResponse,
9618
- status: done ? "done" : "running"
9619
- });
9620
- if (finishReason !== "done") writeFailedStatus(root, label, {
9621
- errorCode: finishReason,
9622
- 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.`
9623
- });
9624
- } finally {
9625
- await handle.dispose().catch(() => {});
9626
- }
9627
- const { json } = handymanProgressPaths(root, label);
9628
- return {
9629
- label,
9630
- done,
9631
- rounds: finalRound,
9632
- finishReason,
9633
- restarts,
9634
- model,
9635
- lastResponse: lastResponse.slice(0, 2e3),
9636
- progressFile: json
9637
- };
9638
- }
9639
- /** 创建 handyman 工具(闭包捕获插件 ctx → 可访问 ctx.agents) */
9640
- function createHandymanTool(ctx) {
9333
+ /** 创建 cheap-worker 工具(闭包捕获插件 ctx → 可访问 ctx.subagents) */
9334
+ function createCheapWorkerTool(ctx) {
9641
9335
  return defineTool({
9642
- name: "handyman",
9643
- 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.",
9644
9338
  parameters: {
9645
- mode: {
9646
- type: "string",
9647
- 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)"
9648
- },
9649
9339
  task: {
9650
9340
  type: "string",
9651
- 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)"
9652
9342
  },
9653
9343
  label: {
9654
9344
  type: "string",
9655
- description: "Task label (background: 1-50 chars, names the progress file; foreground: optional child display name)"
9656
- },
9657
- session: {
9658
- type: "string",
9659
- description: "Work session S### (context hint, progress reference)"
9660
- },
9661
- model: {
9662
- type: "string",
9663
- description: "provider/model — must be in the CCC whitelist handyman.models; default reads handyman.defaultModel"
9345
+ description: "Optional child display name"
9664
9346
  },
9665
9347
  jobs: {
9666
9348
  type: "array",
9667
- 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.",
9668
9350
  items: {
9669
9351
  type: "object",
9670
9352
  properties: {
9671
9353
  task: { type: "string" },
9672
- label: { type: "string" },
9673
- model: { type: "string" }
9354
+ label: { type: "string" }
9674
9355
  },
9675
9356
  additionalProperties: false
9676
9357
  }
@@ -9685,93 +9366,51 @@ function createHandymanTool(ctx) {
9685
9366
  render: (_args, value) => renderText$4(value)
9686
9367
  },
9687
9368
  async execute(args, exec) {
9688
- if (args.guide) return { guide: HANDYMAN_GUIDE };
9689
- 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)");
9690
9371
  const root = cccRootForExec(exec);
9691
9372
  if (!root) throw new Error(NO_CCC_FROM_AGENT_CWD);
9692
- const hc = readHandymanConfig(root, DEFAULT_SERENITY_CONFIG_PATHS);
9693
- 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\"}})");
9694
- const mode = args.mode === "background" ? "background" : "foreground";
9695
- if (args.mode !== void 0 && args.mode !== "background" && args.mode !== "foreground") throw new Error("handyman: mode must be \"foreground\" (default) or \"background\"");
9696
- if (mode === "foreground") {
9697
- if (args.jobs !== void 0) {
9698
- const jobs = parseForegroundJobs(args.jobs);
9699
- if (jobs === null) throw new Error("handyman foreground: jobs must be [{task, label?, model?}, ...] (every job requires a non-empty task)");
9700
- 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)`);
9701
- const results = await Promise.all(jobs.map((job) => runForegroundJob(ctx, {
9702
- task: job.task,
9703
- ...job.label === void 0 ? {} : { label: job.label },
9704
- model: job.model ?? (typeof args.model === "string" && args.model !== "" ? args.model : hc.defaultModel),
9705
- models: hc.models,
9706
- parent: exec.agent,
9707
- signal: exec.signal
9708
- })));
9709
- const done = results.every((r) => r.done);
9710
- return {
9711
- mode: "foreground",
9712
- done,
9713
- jobs: results,
9714
- usage: {
9715
- 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}.`,
9716
- model: "restricted to the CCC whitelist handyman.models; per-job model overrides the call-level model; default reads handyman.defaultModel",
9717
- 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\""
9718
- }
9719
- };
9720
- }
9721
- 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)");
9722
- const model = typeof args.model === "string" && args.model !== "" ? args.model : hc.defaultModel;
9723
- const res = await runForegroundJob(ctx, {
9724
- task: args.task,
9725
- ...typeof args.label === "string" && args.label !== "" ? { label: args.label } : {},
9726
- 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,
9727
9383
  models: hc.models,
9728
9384
  parent: exec.agent,
9729
9385
  signal: exec.signal
9730
- });
9386
+ })));
9387
+ const done = results.every((r) => r.done);
9731
9388
  return {
9732
- ...res,
9389
+ mode: "foreground",
9390
+ done,
9391
+ jobs: results,
9733
9392
  usage: {
9734
- 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.",
9735
- model: "restricted to the CCC whitelist handyman.models; default reads handyman.defaultModel",
9736
- 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)"
9737
9396
  }
9738
9397
  };
9739
9398
  }
9740
- const jobs = args.jobs !== void 0 ? (() => {
9741
- const parsed = parseJobs(args.jobs);
9742
- if (parsed === null) throw new Error("handyman: jobs must be [{task, label, model?}, ...] (every job requires task and label)");
9743
- return parsed;
9744
- })() : (() => {
9745
- if (typeof args.task !== "string" || typeof args.label !== "string") throw new Error("handyman: task and label are required (single-job mode)");
9746
- return [{
9747
- task: args.task,
9748
- label: args.label,
9749
- ...typeof args.model === "string" && args.model !== "" ? { model: args.model } : {}
9750
- }];
9751
- })();
9752
- if (jobs.length === 0) throw new Error("handyman: no jobs provided");
9753
- if (jobs.length > hc.maxParallel) throw new Error(`handyman: ${jobs.length} jobs exceed maxParallel ${hc.maxParallel} (configure handyman.maxParallel to raise)`);
9754
- if (jobs.some((j) => !j.task || !j.label)) throw new Error("handyman: every job requires task and label");
9755
- const parentCtx = exec.agent?.ctx;
9756
- const parentSession = (exec.agent?.session)?.id;
9757
- const results = await Promise.all(jobs.map((job) => runHandymanJob(ctx, {
9758
- root,
9759
- job,
9760
- 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,
9761
9404
  models: hc.models,
9762
- maxRounds: hc.maxRounds,
9763
- parentSession,
9764
- parentCtx,
9765
- session: typeof args.session === "string" ? args.session : void 0
9766
- })));
9405
+ parent: exec.agent,
9406
+ signal: exec.signal
9407
+ });
9767
9408
  return {
9768
- done: results.every((r) => r.done),
9769
- jobs: results.map((r) => ({ ...r })),
9409
+ ...res,
9770
9410
  usage: {
9771
- 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",
9772
- progress: "Progress in AGENT_SESSIONS/handyman-<label>.md and .json; calling handyman again with the same label resumes from the next round (no redo)",
9773
- 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",
9774
- 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`
9775
9414
  }
9776
9415
  };
9777
9416
  }
@@ -12032,7 +11671,7 @@ function renderCroSampleSnapshot() {
12032
11671
  /**
12033
11672
  * CRO 编写指南正文(ACC 内置手册;由工具的 guide 动作输出)。
12034
11673
  *
12035
- * 结构照既有 guide 惯例(`MSM_GUIDE` / `handyman(guide=true)`):
11674
+ * 结构照既有 guide 惯例(`MSM_GUIDE` / `cheap-worker(guide=true)`):
12036
11675
  * 用途 → 位置 → 我给什么 → 你给我什么 → 骨架 → 常见模式 → 坑 → **自测**。
12037
11676
  */
12038
11677
  const CRO_GUIDE = `CRO(Continuous Re-Occurrence · 持续再发生)编写指南
@@ -12559,7 +12198,7 @@ function renderText$2(value) {
12559
12198
  }
12560
12199
  const localstoreTool = defineTool({
12561
12200
  name: "localstore",
12562
- 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).",
12563
12202
  parameters: {
12564
12203
  action: {
12565
12204
  type: "string",
@@ -12603,7 +12242,7 @@ dsp 只提供机制(双白名单强制 + 基础提示词 + 调试问答页)
12603
12242
  ── 角色配置 schema ──
12604
12243
  { "skiff": { "roles": {
12605
12244
  "<role-name>": {
12606
- "model": "provider/model", // 角色模型(CCC 直接指定,无白名单校验;缺省回退 handyman.defaultModel)
12245
+ "model": "provider/model", // 角色模型(CCC 直接指定,无白名单校验;缺省回退 cheap-worker.defaultModel)
12607
12246
  "msms": ["msm-a", "msm-b"], // MSM 白名单(独立):msm 工具只能执行这些;register/deregister 必拒
12608
12247
  "tools": ["read","grep","glob",...], // 非 MSM 工具白名单(独立):白名单外工具一律不可用(guard 强制)
12609
12248
  "trajectory": { "session": false, "keeper": false, "rebuild": false }, // 轨迹纪律子集(缺省全关 = 完全独立)
@@ -12647,7 +12286,7 @@ dsp 只提供机制(双白名单强制 + 基础提示词 + 调试问答页)
12647
12286
  会话追问(v1.25.10):调试页持有当前会话,追问自动续接(同会话上下文延续);
12648
12287
  「新对话」按钮开新会话;进程重启后旧会话不可续(WebUI 仍可见历史)
12649
12288
  注意:skill 加载对 skiff **恒可用**(不设白名单——读知识面,无写能力)`;
12650
- /** 校验当前 CCC 的 skiff 配置:roles schema 合法 / msms 均已注册 / model ∈ handyman.models / systemPrompt 非空 */
12289
+ /** 校验当前 CCC 的 skiff 配置:roles schema 合法 / msms 均已注册 / model ∈ cheap-worker.models / systemPrompt 非空 */
12651
12290
  function validateSkiffConfig(root) {
12652
12291
  const roles = readSkiffRoles(root);
12653
12292
  const issues = [];
@@ -12658,7 +12297,7 @@ function validateSkiffConfig(root) {
12658
12297
  note: "no skiff roles defined (skiff disabled — zero impact)"
12659
12298
  };
12660
12299
  const registered = new Set(loadMsmEntries(root).map((e) => e.name));
12661
- const hc = readHandymanConfig(root);
12300
+ const hc = readCheapWorkerConfig(root);
12662
12301
  const allowedModels = hc ? new Set(hc.models) : null;
12663
12302
  for (const [name, role] of roles) {
12664
12303
  try {
@@ -12667,7 +12306,7 @@ function validateSkiffConfig(root) {
12667
12306
  issues.push(`role "${name}": ${String(err?.message ?? err)}`);
12668
12307
  }
12669
12308
  for (const m of role.msms ?? []) if (!registered.has(m)) issues.push(`role "${name}": msms "${m}" is not registered in mech-registry.json`);
12670
- 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)"})`);
12671
12310
  }
12672
12311
  return {
12673
12312
  ok: issues.length === 0,
@@ -12684,7 +12323,7 @@ function listSkiffRoles(root) {
12684
12323
  };
12685
12324
  const items = [...roles.entries()].map(([name, role]) => ({
12686
12325
  name,
12687
- model: role.model ?? "(handyman default)",
12326
+ model: role.model ?? "(cheap-worker default)",
12688
12327
  msms: role.msms ?? [],
12689
12328
  tools: role.tools ?? [],
12690
12329
  trajectory: {
@@ -12914,7 +12553,7 @@ function createContainerAdminTool(ctx) {
12914
12553
  * 注入文本**自带**"请回复 ABCD" ⇒ 模型**复述指令**即**假完成**(高概率,不是小概率)
12915
12554
  * ⇒ 用**每轮新生成**的 nonce + **包装 + 独占一行**(`[[UNATTENDED-DONE:<hex>]]`),
12916
12555
  * 明写「**旧码作废**」,校验**只认当前轮**(防上下文里的旧码复活)。
12917
- * 先例 = `handyman` background 的 `SERENITY_HANDYMAN_DONE_<16hex>`(同族实现)。
12556
+ * 先例 = `cheap-worker` background 的 `SERENITY_CHEAP_WORKER_DONE_<16hex>`(同族实现)。
12918
12557
  */
12919
12558
  /** 完成码包装前缀(**独占一行**才有意义;见文件头"禁用 4 位字母码") */
12920
12559
  const UNATTENDED_DONE_PREFIX = "[[UNATTENDED-DONE:";
@@ -13021,14 +12660,14 @@ function claimsUserNotification(text) {
13021
12660
  /**
13022
12661
  * **不许开**的会话(设计 §3 的"建议写死",本版**确实写死**):
13023
12662
  * `skiff-`(F4 问答 / **微信桥会话也是这个前缀** —— 桥那侧的"用户"是**家人**,
13024
- * 代理替家人说话 = **冒充**)| `acp-`(对外程序化面)| `handyman-`(自主 worker,已有自己的完成码循环)。
12663
+ * 代理替家人说话 = **冒充**)| `acp-`(对外程序化面)| `cheap-worker-`(自主 worker,已有自己的完成码循环)。
13025
12664
  *
13026
12665
  * ⚠️ **`rebuild-` 不在排除面**(与 `output-guard-seam.ts` 的取向**刻意不同**):重建会话是
13027
12666
  * **同一轨迹的载体**(内部维护面),不是对外面;把它排除会让"重建后就没人推了"。
13028
12667
  */
13029
12668
  function isProxyExcludedSessionId(sessionId) {
13030
12669
  if (typeof sessionId !== "string" || sessionId === "") return true;
13031
- return isSkiffSessionId(sessionId) || sessionId.startsWith("acp-") || sessionId.startsWith("handyman-");
12670
+ return isSkiffSessionId(sessionId) || sessionId.startsWith("acp-") || sessionId.startsWith("cheap-worker-");
13032
12671
  }
13033
12672
  /**
13034
12673
  * 一次判定的**唯一入口**(纯函数)。
@@ -14138,7 +13777,7 @@ function createEpochPromotion(promoteEvents, requiredSignals = 1, maxRoundsFallb
14138
13777
  promoted: true
14139
13778
  };
14140
13779
  const loopSid = session.id;
14141
- if (typeof loopSid === "string" && (loopSid.startsWith("handyman-") || isSkiffSessionId(loopSid))) return {
13780
+ if (typeof loopSid === "string" && (loopSid.startsWith("cheap-worker-") || isSkiffSessionId(loopSid))) return {
14142
13781
  boundary: -1,
14143
13782
  promoted: true
14144
13783
  };
@@ -14242,7 +13881,7 @@ function registerBootstrap(ctx) {
14242
13881
  const session = agent.session;
14243
13882
  const depth = session?.header?.delegationDepth ?? 0;
14244
13883
  const sid = typeof session?.id === "string" ? session.id : void 0;
14245
- if (sid !== void 0 && (sid.startsWith("handyman-") || isSkiffSessionId(sid))) return;
13884
+ if (sid !== void 0 && (sid.startsWith("cheap-worker-") || isSkiffSessionId(sid))) return;
14246
13885
  if (depth === 0) {
14247
13886
  if (hasUserMessageHistory(session)) return;
14248
13887
  } else if (sid !== void 0 && anchoredSessions.has(sid)) return;
@@ -14783,7 +14422,7 @@ function accIdentityText(root, configPaths = DEFAULT_SERENITY_CONFIG_PATHS, entr
14783
14422
  }
14784
14423
  /**
14785
14424
  * ACC 注入消息(S134 去重):**只含简短身份锚点**([ACC] 已激活 + CCC 根 + 约束 + Phase 2)。
14786
- * ⚠️ 原文案含「+ handyman 模型」,该注入已于 2026-09-16 删除(§0e-G)⇒ 此处同步订正(散文级残留)。
14425
+ * ⚠️ 原文案含「+ cheap-worker 模型」,该注入已于 2026-09-16 删除(§0e-G)⇒ 此处同步订正(散文级残留)。
14787
14426
  * 完整身份(ACC 5 块 + CCE + Constraints + EAP + SKILL 全文 + Session 块)由**系统提示词层**
14788
14427
  * (systemPrompt.section,每轮 prompt 装配自动注入,含 subagent)承担——对话消息流/压缩重注入
14789
14428
  * 不再重复注入同一内容(token 双倍浪费,见 S134 注入方案梳理)。
@@ -14811,7 +14450,7 @@ function agentScope(agent) {
14811
14450
  * 只有"conversation 根会话"才自动恢复最近激活的宁静号会话——
14812
14451
  * - subagent:session header `origin === 'subagent'`(DSH 路由语义,agent-lookup.ts)
14813
14452
  * - 派生会话:`parentSession` 存在(任何子会话)
14814
- * - handyman 杂工:sessionId 固定 `handyman-` 前缀(tools/handyman.ts 生成)
14453
+ * - cheap-worker 杂工:sessionId 固定 `cheap-worker-` 前缀(tools/cheap-worker.ts 生成)
14815
14454
  * 三者都不恢复(避免把主会话激活注入子上下文,违背 v1.16.2 scope 隔离)。
14816
14455
  */
14817
14456
  function shouldAutoRestore(agent) {
@@ -14819,7 +14458,7 @@ function shouldAutoRestore(agent) {
14819
14458
  if (!session) return false;
14820
14459
  if (session.header?.origin === "subagent") return false;
14821
14460
  if (session.header?.parentSession) return false;
14822
- if (session.id?.startsWith("handyman-")) return false;
14461
+ if (session.id?.startsWith("cheap-worker-")) return false;
14823
14462
  if (isSkiffSessionId(session.id)) return false;
14824
14463
  return true;
14825
14464
  }
@@ -14980,7 +14619,7 @@ function registerCompactRetention(ctx, opts = {}) {
14980
14619
  /**
14981
14620
  * status.ts — 状态与安全模式操作(纯逻辑,零 DSH 依赖,可独立单测)
14982
14621
  *
14983
- * WebUI 停靠栏的数据源:ACC 版本 / CCC 根 / safe-mode 状态 / 黑名单 / keeper 阈值 / handyman 模型。
14622
+ * WebUI 停靠栏的数据源:ACC 版本 / CCC 根 / safe-mode 状态 / 黑名单 / keeper 阈值 / cheap-worker 模型。
14984
14623
  * setSafeMode 直接读写 .serenity-safe-on 标记(守卫实时读取,写即生效)。
14985
14624
  */
14986
14625
  /** 宿主包名 —— 从运行入口反推包根时的**身份核对**用(见 {@link dshVersionFromEntry}) */
@@ -15065,7 +14704,7 @@ function getStatus(cwd, configPaths = DEFAULT_SERENITY_CONFIG_PATHS) {
15065
14704
  safeModeOn: false,
15066
14705
  blacklist: [],
15067
14706
  threshold: null,
15068
- handymanModel: null,
14707
+ cheapWorkerModel: null,
15069
14708
  restrict
15070
14709
  };
15071
14710
  const cfg = loadSerenityConfig(root, configPaths);
@@ -15075,7 +14714,7 @@ function getStatus(cwd, configPaths = DEFAULT_SERENITY_CONFIG_PATHS) {
15075
14714
  safeModeOn: isSafeModeOn(root),
15076
14715
  blacklist: readBlacklist(root, configPaths),
15077
14716
  threshold: cfg.sessionKeeper?.threshold ?? null,
15078
- handymanModel: readHandymanConfig(root, configPaths)?.defaultModel ?? null,
14717
+ cheapWorkerModel: readCheapWorkerConfig(root, configPaths)?.defaultModel ?? null,
15079
14718
  restrict
15080
14719
  };
15081
14720
  }
@@ -15090,7 +14729,6 @@ function setSafeMode(root, on) {
15090
14729
  //#endregion
15091
14730
  //#region src/api.ts
15092
14731
  const ROUTE_PATH = "/serenity/status";
15093
- const HANDYMEN_PATH = "/serenity/handymen";
15094
14732
  const UPLOAD_PATH = "/serenity/image-upload";
15095
14733
  const FILE_UPLOAD_PATH = "/serenity/file-upload";
15096
14734
  const CONFIG_PATH = "/serenity/config";
@@ -15245,31 +14883,6 @@ function resolveWorkspace(ctx, params) {
15245
14883
  }
15246
14884
  function registerStatusApi(ctx, opts = {}) {
15247
14885
  const configPaths = opts.configPaths ?? DEFAULT_SERENITY_CONFIG_PATHS;
15248
- ctx.webServer.register({
15249
- kind: "exact",
15250
- path: HANDYMEN_PATH,
15251
- handler: async (req, res) => {
15252
- try {
15253
- if (req.method !== "GET") {
15254
- sendJson$4(res, 405, { error: "method not allowed" });
15255
- return;
15256
- }
15257
- const url = new URL(req.url ?? "/", "http://127.0.0.1");
15258
- const workspace = resolveWorkspace(ctx, {
15259
- sessionId: url.searchParams.get("sessionId") ?? void 0,
15260
- workspace: url.searchParams.get("workspace") ?? void 0
15261
- });
15262
- const root = cccRootForCwd(workspace);
15263
- if (!root) {
15264
- sendJson$4(res, 200, { handymen: [] });
15265
- return;
15266
- }
15267
- sendJson$4(res, 200, { handymen: listActiveHandymen(root) });
15268
- } catch (err) {
15269
- sendJson$4(res, 400, { error: err.message ?? String(err) });
15270
- }
15271
- }
15272
- });
15273
14886
  ctx.webServer.register({
15274
14887
  kind: "exact",
15275
14888
  path: UPLOAD_PATH,
@@ -15483,11 +15096,11 @@ function registerStatusApi(ctx, opts = {}) {
15483
15096
  sendJson$4(res, 400, { error: chk.error });
15484
15097
  return;
15485
15098
  }
15486
- const { readWeixinSettings } = await import("./weixin-route-CxCciqQl.js").then((n) => n.u);
15099
+ const { readWeixinSettings } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
15487
15100
  const { weixinBridgeStatus } = await Promise.resolve().then(() => weixin_bridge_exports);
15488
15101
  const settings = readWeixinSettings(chk.root);
15489
15102
  const bridge = weixinBridgeStatus().find((b) => b.ccc === chk.root);
15490
- const { readWeixinCredential } = await import("./weixin-route-CxCciqQl.js").then((n) => n.u);
15103
+ const { readWeixinCredential } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
15491
15104
  const accounts = (settings.accounts ?? []).map((a) => ({
15492
15105
  accountId: a.accountId,
15493
15106
  name: a.name ?? void 0,
@@ -15519,7 +15132,7 @@ function registerStatusApi(ctx, opts = {}) {
15519
15132
  const { fetchQRCode } = await import("./weixin-api-BrRcPiQs.js").then((n) => n.u);
15520
15133
  const { randomUUID } = await import("node:crypto");
15521
15134
  for (const [key, v] of weixinLogins) if (Date.now() - v.startedAt > WEIXIN_LOGIN_TTL_MS) weixinLogins.delete(key);
15522
- const { readWeixinSettings } = await import("./weixin-route-CxCciqQl.js").then((n) => n.u);
15135
+ const { readWeixinSettings } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
15523
15136
  const qr = await fetchQRCode({ botType: readWeixinSettings(root).botType ?? void 0 });
15524
15137
  const loginKey = randomUUID();
15525
15138
  weixinLogins.set(loginKey, {
@@ -15540,7 +15153,7 @@ function registerStatusApi(ctx, opts = {}) {
15540
15153
  sendJson$4(res, 400, { error: "missing accountId" });
15541
15154
  return;
15542
15155
  }
15543
- const { removeWeixinAccount } = await import("./weixin-route-CxCciqQl.js").then((n) => n.u);
15156
+ const { removeWeixinAccount } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
15544
15157
  const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
15545
15158
  removeWeixinAccount(root, body.accountId);
15546
15159
  syncCccBridge(ctx, root);
@@ -15553,8 +15166,8 @@ function registerStatusApi(ctx, opts = {}) {
15553
15166
  sendJson$4(res, 400, { error: "invalid routes (expected [{user, role}, ...])" });
15554
15167
  return;
15555
15168
  }
15556
- const { saveWeixinRoutes } = await import("./weixin-route-CxCciqQl.js").then((n) => n.u);
15557
- const { readSkiffRoles } = await import("./skiff-role-Det66X16.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);
15558
15171
  const roles = readSkiffRoles(root);
15559
15172
  for (const r of routes) if (!roles.has(r.role)) {
15560
15173
  sendJson$4(res, 400, { error: `unknown role: ${r.role} (not in ${root} skiff.roles)` });
@@ -15566,7 +15179,7 @@ function registerStatusApi(ctx, opts = {}) {
15566
15179
  }
15567
15180
  if (body.action === "set-enabled") {
15568
15181
  const enabled = body.enabled === true;
15569
- const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-CxCciqQl.js").then((n) => n.u);
15182
+ const { readWeixinSettings, setWeixinEnabled } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
15570
15183
  const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
15571
15184
  const settings = readWeixinSettings(root);
15572
15185
  if (enabled && (settings.accounts ?? []).length === 0) {
@@ -15610,7 +15223,7 @@ function registerStatusApi(ctx, opts = {}) {
15610
15223
  return;
15611
15224
  }
15612
15225
  const { pollQRStatus } = await import("./weixin-api-BrRcPiQs.js").then((n) => n.u);
15613
- const { readWeixinSettings } = await import("./weixin-route-CxCciqQl.js").then((n) => n.u);
15226
+ const { readWeixinSettings } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
15614
15227
  const settings = readWeixinSettings(login.root);
15615
15228
  const status = await pollQRStatus({
15616
15229
  baseUrl: void 0,
@@ -15625,7 +15238,7 @@ function registerStatusApi(ctx, opts = {}) {
15625
15238
  });
15626
15239
  return;
15627
15240
  }
15628
- const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-CxCciqQl.js").then((n) => n.u);
15241
+ const { upsertWeixinAccount, writeWeixinCredential, nextWeixinAccountId } = await import("./weixin-route-B9gU1YXz.js").then((n) => n.u);
15629
15242
  const { syncCccBridge } = await Promise.resolve().then(() => weixin_bridge_exports);
15630
15243
  const accountId = nextWeixinAccountId(settings);
15631
15244
  upsertWeixinAccount(login.root, {
@@ -15989,7 +15602,7 @@ const HOST_SERVICES = [
15989
15602
  name: "get",
15990
15603
  kind: "function"
15991
15604
  }],
15992
- impact: "skiff/handyman 无法创建或复用 agent",
15605
+ impact: "skiff/cheap-worker 无法创建或复用 agent",
15993
15606
  required: true
15994
15607
  },
15995
15608
  {
@@ -16071,7 +15684,7 @@ const HOST_SERVICES = [
16071
15684
  name: "agentLoop",
16072
15685
  access: "injected",
16073
15686
  members: [],
16074
- impact: "handyman worker agent 无法装配 preset",
15687
+ impact: "cheap-worker worker agent 无法装配 preset",
16075
15688
  required: false
16076
15689
  },
16077
15690
  {
@@ -16082,7 +15695,7 @@ const HOST_SERVICES = [
16082
15695
  name: "start",
16083
15696
  kind: "function"
16084
15697
  }],
16085
- impact: "handyman foreground 模式不可用(响亮报错,不影响 background 模式)",
15698
+ impact: "cheap-worker 委派不可用(前台委派的正门,响亮报错)",
16086
15699
  required: false
16087
15700
  },
16088
15701
  {
@@ -19892,7 +19505,7 @@ async function handle$1(ctx, defaultRoot, webPort, req, res) {
19892
19505
  agent = live;
19893
19506
  continued = true;
19894
19507
  }
19895
- const hc = readHandymanConfig(ccc);
19508
+ const hc = readCheapWorkerConfig(ccc);
19896
19509
  if (!agent) agent = (await createSkiffAgent(ctx, ccc, roleName, role, hc?.defaultModel)).agent;
19897
19510
  const result = await askSkiff(ctx, agent, question, 0);
19898
19511
  sendJson$2(res, 200, {
@@ -20417,7 +20030,7 @@ var AcpServer = class {
20417
20030
  continued: true
20418
20031
  };
20419
20032
  }
20420
- const hc = readHandymanConfig(ccc);
20033
+ const hc = readCheapWorkerConfig(ccc);
20421
20034
  return {
20422
20035
  sessionId: (await createSkiffAgent(this.ctx, ccc, roleName, role, hc?.defaultModel)).sessionId,
20423
20036
  role: roleName,
@@ -20762,7 +20375,7 @@ async function handleAskParsed(ctx, ccc, body, res, ip) {
20762
20375
  continued = true;
20763
20376
  }
20764
20377
  }
20765
- 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;
20766
20379
  const result = await askSkiff(ctx, agent, question, void 0, { includeTrajectory: false });
20767
20380
  sendJson$1(res, 200, {
20768
20381
  answer: result.answer,
@@ -21781,7 +21394,7 @@ function manualOutputFallbackNeeded(input) {
21781
21394
  */
21782
21395
  async function resolveCccRootByAlias(ctx, alias) {
21783
21396
  try {
21784
- const { listCccs } = await import("./ccc-roots-CDW2dO0n.js").then((n) => n.a);
21397
+ const { listCccs } = await import("./ccc-roots-BC8mn8iM.js").then((n) => n.a);
21785
21398
  const entries = await listCccs(ctx);
21786
21399
  const byName = entries.find((e) => e.name === alias);
21787
21400
  if (byName) return byName.root;
@@ -21930,7 +21543,7 @@ async function handleIncoming(ctx, root, accountId, cred, msg, preset) {
21930
21543
  try {
21931
21544
  const sessionId = weixinSessionIdFor(fromUserId);
21932
21545
  const existing = getSkiffAgent(sessionId);
21933
- const hc = readHandymanConfig(root);
21546
+ const hc = readCheapWorkerConfig(root);
21934
21547
  if (existing) try {
21935
21548
  ensureSkiffSession(root, existing, roleName, role, true);
21936
21549
  } catch (err) {
@@ -22280,7 +21893,7 @@ function syncHumanChannel(ctx) {
22280
21893
  (async () => {
22281
21894
  try {
22282
21895
  stopMachineLoops();
22283
- const { listCccs } = await import("./ccc-roots-CDW2dO0n.js").then((n) => n.a);
21896
+ const { listCccs } = await import("./ccc-roots-BC8mn8iM.js").then((n) => n.a);
22284
21897
  for (const entry of await listCccs(ctx)) syncCccBridge(ctx, entry.root);
22285
21898
  } catch {}
22286
21899
  })();
@@ -22516,7 +22129,7 @@ function matchCcc(input, candidates) {
22516
22129
  }
22517
22130
  /** 组装候选列表(`ccc-roots.listCccs` 投影 + `.serenity` 名;动态 import 保持本模块静态依赖轻量) */
22518
22131
  async function collectCandidates(ctx) {
22519
- const { listCccs } = await import("./ccc-roots-CDW2dO0n.js").then((n) => n.a);
22132
+ const { listCccs } = await import("./ccc-roots-BC8mn8iM.js").then((n) => n.a);
22520
22133
  const entries = await listCccs(ctx, { defaultRoot: process.cwd() });
22521
22134
  const seen = /* @__PURE__ */ new Set();
22522
22135
  const out = [];
@@ -25407,7 +25020,7 @@ function apply(ctx, config) {
25407
25020
  ctx.tools.register(praxisTool);
25408
25021
  ctx.tools.register(createOntologyTool());
25409
25022
  ctx.tools.register(createSkillCreatorTool());
25410
- ctx.tools.register(createHandymanTool(ctx));
25023
+ ctx.tools.register(createCheapWorkerTool(ctx));
25411
25024
  ctx.tools.register(localstoreTool);
25412
25025
  ctx.tools.register(createContainerAdminTool(ctx));
25413
25026
  ctx.tools.register(createImBridgeTool());