@shgroup/dsh-serenity-hooks 1.23.8 → 1.24.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/README.md CHANGED
@@ -14,7 +14,7 @@ dsh plugin --profile web add @shgroup/dsh-serenity-hooks
14
14
 
15
15
  ## 工具
16
16
 
17
- `cc_fs` / `session` / `acc_kit` / `cc_git` / `acc_msm` / `eap` / `neat` / `cce` / `loop`
17
+ `cc_fs` / `session` / `acc_kit` / `cc_git` / `acc_msm` / `eap` / `neat` / `cce` / `handyman`
18
18
 
19
19
  ## 系统提示词
20
20
 
@@ -26,7 +26,7 @@ dsh plugin --profile web add @shgroup/dsh-serenity-hooks
26
26
 
27
27
  ```jsonc
28
28
  {
29
- "loop": { "defaultModel": "provider/model" },
29
+ "handyman": { "models": ["provider/model"], "defaultModel": "provider/model" },
30
30
  "sessionKeeper": { "threshold": 100 },
31
31
  "safeMode": { "blacklist": [".secrets/", "regex:\\.env$"] }
32
32
  }
package/dsh.plugin.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "id": "dsh-serenity-hooks",
3
- "version": "1.23.8",
3
+ "version": "1.24.0",
4
4
  "main": "lib/index.js",
5
- "description": "宁静号 ACC harness(Native Cordis 插件):真实 DSH 工具 cc_fs/session/acc_msm/eap/neat/cce/loop/session_rebuild + 拦截缝机械约束(safe-mode/路径守卫)+ 高级设定面板(双端口网关/账号管理)。适配 DSH 公开版(0.1.0-rc,deepseek-ai/deepseek-harness)。",
5
+ "description": "宁静号 ACC harness(Native Cordis 插件):真实 DSH 工具 cc_fs/session/acc_msm/eap/neat/cce/handyman/session_rebuild + 拦截缝机械约束(safe-mode/路径守卫)+ 高级设定面板(双端口网关/账号管理)。适配 DSH 公开版(0.1.0-rc,deepseek-ai/deepseek-harness)。",
6
6
  "engines": {
7
7
  "dsh": ">=0.1.0-rc.5"
8
8
  },
@@ -16,7 +16,7 @@
16
16
  "eap",
17
17
  "neat",
18
18
  "cce",
19
- "loop",
19
+ "handyman",
20
20
  "session_rebuild",
21
21
  "localstore"
22
22
  ],
package/lib/client.js CHANGED
@@ -8,7 +8,7 @@ window.__ModuleLoader__.load({
8
8
  let react = require("react");
9
9
  let react_jsx_runtime = require("react/jsx-runtime");
10
10
  //#region \0sp-css:/home/yh/home/home-serenity/AI_LAB/dsh-serenity-plugin/hooks/dsh-serenity-hooks/src/client/SafeModePanel
11
- const css$3 = "/* SafeModePanel — 会话头部 Serenity 状态卡片(v1.22 UX 重构)\n * 卡片 → 点击 → 官方 Modal(body portal 遮罩 + 居中)\n * 模态内:tab 栏 + 分组(groupTitle 标题 + rowCard 行卡)——看齐官方 settings 设计语言\n * 遵循 web-styling.md:颜色只用 --dsw-alias-* 语义 token(明暗自适应);\n * 前缀 sp- 防冲突。 */\n\n/* ── 头部状态卡片(可点击) ── */\n\n.sp-card {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 26px;\n padding: 0 10px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n background: var(--dsw-alias-bg-module-platform);\n color: inherit;\n font-size: 12px;\n cursor: pointer;\n transition: background var(--ds-transition-duration-fast, 0.1s) var(--ds-ease-in-out, ease),\n border-color var(--ds-transition-duration-fast, 0.1s) var(--ds-ease-in-out, ease);\n}\n\n.sp-card:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n border-color: var(--dsw-alias-border-l2);\n}\n\n.sp-card:focus-visible {\n outline: 2px solid var(--dsw-alias-button-ghost-active-border);\n outline-offset: 1px;\n}\n\n.sp-card:disabled {\n opacity: 0.6;\n cursor: default;\n}\n\n.sp-dot {\n flex: none;\n width: 7px;\n height: 7px;\n border-radius: 50%;\n}\n\n.sp-dotOn {\n background: var(--dsw-alias-state-success-primary);\n}\n\n.sp-dotOff {\n background: var(--dsw-alias-label-dimmed);\n}\n\n/* safe-mode tag(实心方形标签,无 icon——从外面一眼可见;ON 红底 SAFE / OFF 灰底 OFF) */\n.sp-tag {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n height: 16px;\n padding: 0 7px;\n border-radius: 4px;\n font-size: 10px;\n font-weight: 700;\n line-height: 1;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n}\n\n.sp-tagOn {\n background: var(--dsw-alias-state-error-primary);\n color: var(--dsw-alias-label-primary-foreground);\n border: 1px solid var(--dsw-alias-state-error-primary);\n}\n\n.sp-tagOff {\n background: var(--dsw-alias-interactive-bg-active, rgba(128, 128, 128, 0.16));\n color: var(--dsw-alias-label-secondary);\n border: 1px solid var(--dsw-alias-border-l1);\n}\n\n.sp-brand {\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n\n.sp-chev {\n color: var(--dsw-alias-label-dimmed);\n}\n\n/* ── 官方 Modal 定制(宽度 + 内容紧凑) ── */\n\n.sp-modal {\n width: 520px;\n max-width: calc(100vw - 48px);\n}\n\n/* ── tab 栏 ── */\n\n.sp-tabs {\n display: flex;\n gap: 6px;\n padding: 0 2px 12px;\n border-bottom: 1px solid var(--dsw-alias-border-l1);\n}\n\n.sp-tab {\n flex: 1;\n padding: 8px 0;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n transition: background var(--ds-transition-duration-fast, 0.1s) var(--ds-ease-in-out, ease);\n}\n\n.sp-tab:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.sp-tabOn {\n background: var(--dsw-alias-interactive-bg-active, rgba(128, 128, 128, 0.2));\n color: var(--dsw-alias-label-primary);\n font-weight: 600;\n}\n\n/* ── 模态内容 ── */\n\n.sp-modalBody {\n display: flex;\n flex-direction: column;\n gap: 16px;\n padding-top: 14px;\n /* 内容超出视口时 body 区滚动(兜底——不再截断);整卡不滚动 */\n overflow-y: auto;\n max-height: calc(100vh - 200px);\n}\n\n/* ── 分组(官方 settings 语言:groupTitle + rows) ── */\n\n.sp-group {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.sp-groupHead {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n\n.sp-groupTitle {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n font-weight: 600;\n letter-spacing: 0.02em;\n text-transform: uppercase;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sp-rows {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n/* 行卡(rowCard:border + radius + padding;值与 label 分离、长内容可换行) */\n.sp-row {\n display: flex;\n align-items: baseline;\n gap: 12px;\n min-width: 0;\n padding: 8px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-module);\n}\n\n.sp-label {\n flex: none;\n width: 76px;\n font-size: 12px;\n font-weight: 500;\n color: var(--dsw-alias-label-secondary);\n white-space: nowrap;\n}\n\n.sp-value {\n flex: 1;\n min-width: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n font-family: var(--ds-font-family-code, monospace);\n word-break: break-all;\n overflow-wrap: anywhere;\n}\n\n/* 安全模式动作区 */\n.sp-actions {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 8px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-module);\n}\n\n.sp-toggle {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 24px;\n padding: 0 10px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 6px;\n font-size: 12px;\n cursor: pointer;\n transition: background var(--ds-transition-duration-fast, 0.1s) var(--ds-ease-in-out, ease);\n}\n\n.sp-toggleLg {\n height: 30px;\n padding: 0 16px;\n font-size: 13px;\n}\n\n.sp-toggleOn {\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 14%, transparent);\n color: var(--dsw-alias-state-error-primary);\n border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary) 45%, transparent);\n}\n\n.sp-toggleOff {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n\n.sp-toggleIcon {\n flex: none;\n}\n\n.sp-toggleDesc {\n flex: 1;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* loop 运行状态区块 */\n\n.sp-loopsToggle {\n flex: none;\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 11px;\n cursor: pointer;\n}\n\n.sp-loopsToggle:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.sp-loopsList {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin-top: 2px;\n}\n\n.sp-loopItem {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 8px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n min-width: 0;\n}\n\n.sp-loopHead {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n}\n\n.sp-loopLabel {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 12px;\n font-weight: 500;\n color: var(--dsw-alias-label-primary);\n}\n\n.sp-loopRound {\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-label-dimmed);\n}\n\n.sp-loopTime {\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-label-dimmed);\n}\n\n.sp-loopResp {\n font-size: 11px;\n line-height: 16px;\n color: var(--dsw-alias-label-secondary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.sp-loopsMore {\n font-size: 11px;\n color: var(--dsw-alias-label-dimmed);\n padding: 4px 12px;\n}\n\n/* 底部提示(配置入口引导) */\n.sp-hint {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-word;\n overflow-wrap: anywhere;\n padding: 8px 12px;\n border: 1px dashed var(--dsw-alias-border-l2);\n border-radius: 10px;\n}\n";
11
+ const css$3 = "/* SafeModePanel — 会话头部 Serenity 状态卡片(v1.22 UX 重构)\n * 卡片 → 点击 → 官方 Modal(body portal 遮罩 + 居中)\n * 模态内:tab 栏 + 分组(groupTitle 标题 + rowCard 行卡)——看齐官方 settings 设计语言\n * 遵循 web-styling.md:颜色只用 --dsw-alias-* 语义 token(明暗自适应);\n * 前缀 sp- 防冲突。 */\n\n/* ── 头部状态卡片(可点击) ── */\n\n.sp-card {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 26px;\n padding: 0 10px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n background: var(--dsw-alias-bg-module-platform);\n color: inherit;\n font-size: 12px;\n cursor: pointer;\n transition: background var(--ds-transition-duration-fast, 0.1s) var(--ds-ease-in-out, ease),\n border-color var(--ds-transition-duration-fast, 0.1s) var(--ds-ease-in-out, ease);\n}\n\n.sp-card:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n border-color: var(--dsw-alias-border-l2);\n}\n\n.sp-card:focus-visible {\n outline: 2px solid var(--dsw-alias-button-ghost-active-border);\n outline-offset: 1px;\n}\n\n.sp-card:disabled {\n opacity: 0.6;\n cursor: default;\n}\n\n.sp-dot {\n flex: none;\n width: 7px;\n height: 7px;\n border-radius: 50%;\n}\n\n.sp-dotOn {\n background: var(--dsw-alias-state-success-primary);\n}\n\n.sp-dotOff {\n background: var(--dsw-alias-label-dimmed);\n}\n\n/* safe-mode tag(实心方形标签,无 icon——从外面一眼可见;ON 红底 SAFE / OFF 灰底 OFF) */\n.sp-tag {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n height: 16px;\n padding: 0 7px;\n border-radius: 4px;\n font-size: 10px;\n font-weight: 700;\n line-height: 1;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n}\n\n.sp-tagOn {\n background: var(--dsw-alias-state-error-primary);\n color: var(--dsw-alias-label-primary-foreground);\n border: 1px solid var(--dsw-alias-state-error-primary);\n}\n\n.sp-tagOff {\n background: var(--dsw-alias-interactive-bg-active, rgba(128, 128, 128, 0.16));\n color: var(--dsw-alias-label-secondary);\n border: 1px solid var(--dsw-alias-border-l1);\n}\n\n.sp-brand {\n color: var(--dsw-alias-label-primary);\n font-weight: 500;\n}\n\n.sp-chev {\n color: var(--dsw-alias-label-dimmed);\n}\n\n/* ── 官方 Modal 定制(宽度 + 内容紧凑) ── */\n\n.sp-modal {\n width: 520px;\n max-width: calc(100vw - 48px);\n}\n\n/* ── tab 栏 ── */\n\n.sp-tabs {\n display: flex;\n gap: 6px;\n padding: 0 2px 12px;\n border-bottom: 1px solid var(--dsw-alias-border-l1);\n}\n\n.sp-tab {\n flex: 1;\n padding: 8px 0;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n transition: background var(--ds-transition-duration-fast, 0.1s) var(--ds-ease-in-out, ease);\n}\n\n.sp-tab:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.sp-tabOn {\n background: var(--dsw-alias-interactive-bg-active, rgba(128, 128, 128, 0.2));\n color: var(--dsw-alias-label-primary);\n font-weight: 600;\n}\n\n/* ── 模态内容 ── */\n\n.sp-modalBody {\n display: flex;\n flex-direction: column;\n gap: 16px;\n padding-top: 14px;\n /* 内容超出视口时 body 区滚动(兜底——不再截断);整卡不滚动 */\n overflow-y: auto;\n max-height: calc(100vh - 200px);\n}\n\n/* ── 分组(官方 settings 语言:groupTitle + rows) ── */\n\n.sp-group {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.sp-groupHead {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n\n.sp-groupTitle {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n font-weight: 600;\n letter-spacing: 0.02em;\n text-transform: uppercase;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sp-rows {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n/* 行卡(rowCard:border + radius + padding;值与 label 分离、长内容可换行) */\n.sp-row {\n display: flex;\n align-items: baseline;\n gap: 12px;\n min-width: 0;\n padding: 8px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-module);\n}\n\n.sp-label {\n flex: none;\n width: 76px;\n font-size: 12px;\n font-weight: 500;\n color: var(--dsw-alias-label-secondary);\n white-space: nowrap;\n}\n\n.sp-value {\n flex: 1;\n min-width: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n font-family: var(--ds-font-family-code, monospace);\n word-break: break-all;\n overflow-wrap: anywhere;\n}\n\n/* 安全模式动作区 */\n.sp-actions {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 8px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-module);\n}\n\n.sp-toggle {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n height: 24px;\n padding: 0 10px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 6px;\n font-size: 12px;\n cursor: pointer;\n transition: background var(--ds-transition-duration-fast, 0.1s) var(--ds-ease-in-out, ease);\n}\n\n.sp-toggleLg {\n height: 30px;\n padding: 0 16px;\n font-size: 13px;\n}\n\n.sp-toggleOn {\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 14%, transparent);\n color: var(--dsw-alias-state-error-primary);\n border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary) 45%, transparent);\n}\n\n.sp-toggleOff {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n\n.sp-toggleIcon {\n flex: none;\n}\n\n.sp-toggleDesc {\n flex: 1;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* handyman 运行状态区块 */\n\n.sp-handymenToggle {\n flex: none;\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 11px;\n cursor: pointer;\n}\n\n.sp-handymenToggle:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.sp-handymenList {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin-top: 2px;\n}\n\n.sp-handymanItem {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 8px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n min-width: 0;\n}\n\n.sp-handymanHead {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n}\n\n.sp-handymanLabel {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 12px;\n font-weight: 500;\n color: var(--dsw-alias-label-primary);\n}\n\n.sp-handymanRound {\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-label-dimmed);\n}\n\n.sp-handymanTime {\n flex: none;\n font-size: 11px;\n color: var(--dsw-alias-label-dimmed);\n}\n\n.sp-handymanResp {\n font-size: 11px;\n line-height: 16px;\n color: var(--dsw-alias-label-secondary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.sp-handymenMore {\n font-size: 11px;\n color: var(--dsw-alias-label-dimmed);\n padding: 4px 12px;\n}\n\n/* 底部提示(配置入口引导) */\n.sp-hint {\n margin: 0;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-word;\n overflow-wrap: anywhere;\n padding: 8px 12px;\n border: 1px dashed var(--dsw-alias-border-l2);\n border-radius: 10px;\n}\n";
12
12
  if (typeof document !== "undefined" && document.querySelector("style[data-sp-css=\"SafeModePanel\"]") === null) {
13
13
  const tag = document.createElement("style");
14
14
  tag.setAttribute("data-sp-css", "SafeModePanel");
@@ -18,7 +18,7 @@ window.__ModuleLoader__.load({
18
18
  //#endregion
19
19
  //#region src/client/SafeModePanel.tsx
20
20
  const API = "/serenity/status";
21
- const LOOPS_API = "/serenity/loops";
21
+ const HANDYMEN_API = "/serenity/handymen";
22
22
  /** 拼接类名(等价 clsx 最小子集,避免额外依赖) */
23
23
  function cx(...parts) {
24
24
  return parts.filter(Boolean).join(" ");
@@ -28,8 +28,8 @@ window.__ModuleLoader__.load({
28
28
  const [status, setStatus] = (0, react.useState)(null);
29
29
  const [busy, setBusy] = (0, react.useState)(false);
30
30
  const [open, setOpen] = (0, react.useState)(false);
31
- const [loops, setLoops] = (0, react.useState)([]);
32
- const [loopsOpen, setLoopsOpen] = (0, react.useState)(false);
31
+ const [handymen, setHandymen] = (0, react.useState)([]);
32
+ const [handymenOpen, setHandymenOpen] = (0, react.useState)(false);
33
33
  const sessionId = props.sessionId;
34
34
  const refresh = (0, react.useCallback)(async () => {
35
35
  const qs = sessionId ? `?sessionId=${encodeURIComponent(sessionId)}` : "";
@@ -49,10 +49,10 @@ window.__ModuleLoader__.load({
49
49
  const qs = sessionId ? `?sessionId=${encodeURIComponent(sessionId)}` : "";
50
50
  const tick = async () => {
51
51
  try {
52
- const data = await (await fetch(`${LOOPS_API}${qs}`, { headers: { accept: "application/json" } })).json();
53
- if (alive) setLoops(data.loops ?? []);
52
+ const data = await (await fetch(`${HANDYMEN_API}${qs}`, { headers: { accept: "application/json" } })).json();
53
+ if (alive) setHandymen(data.handymen ?? []);
54
54
  } catch {
55
- if (alive) setLoops([]);
55
+ if (alive) setHandymen([]);
56
56
  }
57
57
  };
58
58
  tick();
@@ -91,7 +91,7 @@ window.__ModuleLoader__.load({
91
91
  })]
92
92
  });
93
93
  const inCcc = status.root !== null;
94
- const runningLoops = loops.filter((l) => !l.done);
94
+ const runningHandymen = handymen.filter((l) => !l.done);
95
95
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
96
96
  type: "button",
97
97
  className: cx("sp-card"),
@@ -147,11 +147,11 @@ window.__ModuleLoader__.load({
147
147
  className: cx("sp-row"),
148
148
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
149
149
  className: cx("sp-label"),
150
- children: "Loop 模型"
150
+ children: "Handyman 模型"
151
151
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
152
152
  className: cx("sp-value"),
153
- title: status.loopModel ? `loop.defaultModel: ${status.loopModel}` : void 0,
154
- children: status.loopModel ?? "未配置"
153
+ title: status.handymanModel ? `handyman.models: ${status.handymanModel}` : void 0,
154
+ children: status.handymanModel ?? "未配置"
155
155
  })]
156
156
  }),
157
157
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -205,12 +205,12 @@ window.__ModuleLoader__.load({
205
205
  className: cx("sp-groupHead"),
206
206
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
207
207
  className: cx("sp-groupTitle"),
208
- children: "Loop 运行"
209
- }), loops.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
208
+ children: "Handyman 运行"
209
+ }), handymen.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
210
210
  type: "button",
211
- className: cx("sp-loopsToggle"),
212
- onClick: () => setLoopsOpen((v) => !v),
213
- children: loopsOpen ? "收起" : "详情"
211
+ className: cx("sp-handymenToggle"),
212
+ onClick: () => setHandymenOpen((v) => !v),
213
+ children: handymenOpen ? "收起" : "详情"
214
214
  })]
215
215
  }),
216
216
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -222,42 +222,42 @@ window.__ModuleLoader__.load({
222
222
  children: "状态"
223
223
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
224
224
  className: cx("sp-value"),
225
- children: runningLoops.length > 0 ? `运行中 ${runningLoops.length}${loops.length > 0 ? ` / 共 ${loops.length}` : ""}` : "无运行中 loop"
225
+ children: runningHandymen.length > 0 ? `运行中 ${runningHandymen.length}${handymen.length > 0 ? ` / 共 ${handymen.length}` : ""}` : "无运行中 handyman"
226
226
  })]
227
227
  })
228
228
  }),
229
- loopsOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
230
- className: cx("sp-loopsList"),
231
- children: [loops.slice(0, 3).map((l) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
232
- className: cx("sp-loopItem"),
229
+ handymenOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
230
+ className: cx("sp-handymenList"),
231
+ children: [handymen.slice(0, 3).map((l) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
232
+ className: cx("sp-handymanItem"),
233
233
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
234
- className: cx("sp-loopHead"),
234
+ className: cx("sp-handymanHead"),
235
235
  children: [
236
236
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
237
- className: cx("sp-loopLabel"),
237
+ className: cx("sp-handymanLabel"),
238
238
  title: l.label,
239
239
  children: l.label
240
240
  }),
241
241
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
242
- className: cx("sp-loopRound"),
242
+ className: cx("sp-handymanRound"),
243
243
  children: l.done ? "✓ 完成" : `R${l.round}`
244
244
  }),
245
245
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
246
- className: cx("sp-loopTime"),
246
+ className: cx("sp-handymanTime"),
247
247
  children: new Date(l.updated).toLocaleTimeString()
248
248
  })
249
249
  ]
250
250
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
251
- className: cx("sp-loopResp"),
251
+ className: cx("sp-handymanResp"),
252
252
  title: l.lastResponse,
253
253
  children: l.lastResponse.slice(0, 80) || (l.done ? "已完成" : "(等待首轮响应)")
254
254
  })]
255
- }, l.label)), loops.length > 3 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
256
- className: cx("sp-loopsMore"),
255
+ }, l.label)), handymen.length > 3 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
256
+ className: cx("sp-handymenMore"),
257
257
  children: [
258
258
  "… 共 ",
259
- loops.length,
260
- " 个 loop(其余见等待界面)"
259
+ handymen.length,
260
+ " 个 handyman(其余见等待界面)"
261
261
  ]
262
262
  })]
263
263
  })
package/lib/index.js CHANGED
@@ -69,6 +69,24 @@ function resolveInside(root, p) {
69
69
  return abs;
70
70
  }
71
71
  /**
72
+ * 解析 handyman 配置(v1.24.0:取代 loop.defaultModel;不兼容旧 loop 配置——用户拍板)。
73
+ * @returns 白名单 + 缺省模型 + 上限;models 未配置返回 null(工具应报错要求配置)
74
+ */
75
+ function readHandymanConfig(root, paths = DEFAULT_SERENITY_CONFIG_PATHS) {
76
+ const cfg = loadSerenityConfig(root, paths);
77
+ const models = cfg.handyman?.models;
78
+ if (!Array.isArray(models) || models.length === 0) return null;
79
+ const clean = models.map((m) => m.trim()).filter(Boolean);
80
+ if (clean.length === 0) return null;
81
+ const defaultModel = cfg.handyman?.defaultModel?.trim();
82
+ return {
83
+ models: clean,
84
+ defaultModel: defaultModel !== void 0 && clean.includes(defaultModel) ? defaultModel : clean[0],
85
+ maxRounds: cfg.handyman?.maxRounds ?? 100,
86
+ maxParallel: cfg.handyman?.maxParallel ?? 10
87
+ };
88
+ }
89
+ /**
72
90
  * ACC 依赖的 CCC 配置路径(S134 修正:兼容历史——历史在 .opencode,规范位置 = .opencode;
73
91
  * .dsh 仅作 dsh 运行时回退,不优先)。
74
92
  */
@@ -782,7 +800,7 @@ function registerGuards(ctx, opts = {}) {
782
800
  /**
783
801
  * status.ts — 状态与安全模式操作(纯逻辑,零 DSH 依赖,可独立单测)
784
802
  *
785
- * WebUI 停靠栏的数据源:ACC 版本 / CCC 根 / safe-mode 状态 / 黑名单 / keeper 阈值 / loop 模型。
803
+ * WebUI 停靠栏的数据源:ACC 版本 / CCC 根 / safe-mode 状态 / 黑名单 / keeper 阈值 / handyman 模型。
786
804
  * setSafeMode 直接读写 .serenity-safe-on 标记(守卫实时读取,写即生效)。
787
805
  */
788
806
  /**
@@ -815,7 +833,7 @@ function getStatus(cwd, configPaths = DEFAULT_SERENITY_CONFIG_PATHS) {
815
833
  safeModeOn: false,
816
834
  blacklist: [],
817
835
  threshold: null,
818
- loopModel: null,
836
+ handymanModel: null,
819
837
  restrict
820
838
  };
821
839
  const cfg = loadSerenityConfig(root, configPaths);
@@ -825,7 +843,7 @@ function getStatus(cwd, configPaths = DEFAULT_SERENITY_CONFIG_PATHS) {
825
843
  safeModeOn: isSafeModeOn(root),
826
844
  blacklist: readBlacklist(root, configPaths),
827
845
  threshold: cfg.sessionKeeper?.threshold ?? null,
828
- loopModel: cfg.loop?.defaultModel ?? null,
846
+ handymanModel: readHandymanConfig(root, configPaths)?.defaultModel ?? null,
829
847
  restrict
830
848
  };
831
849
  }
@@ -979,7 +997,7 @@ const kitTool = defineTool({
979
997
  * 存储结构(JSON 顶层分节):
980
998
  * { "credentials": { "HOME_GITLAB_TOKEN": "xxx" }, "<config节>": { "<key>": "v" } }
981
999
  * credentials 为保留节(credential 命名空间,key 大写蛇形);其余节归 config 命名空间
982
- * (path = section.key,如 loop.defaultModel)。
1000
+ * (path = section.key,如 handyman.models)。
983
1001
  */
984
1002
  const LOCALSTORE_SCOPES = ["credential", "config"];
985
1003
  /** 存储文件名(CCC 根根目录) */
@@ -1066,7 +1084,7 @@ function assertCredentialKey(key) {
1066
1084
  /** 校验 config 路径(节.key);不合法抛错 */
1067
1085
  function assertConfigPath(path) {
1068
1086
  const idx = path.indexOf(".");
1069
- if (idx <= 0 || idx === path.length - 1) throw new Error(`config path "${path}" must be section.key (e.g. loop.defaultModel)`);
1087
+ if (idx <= 0 || idx === path.length - 1) throw new Error(`config path "${path}" must be section.key (e.g. handyman.models)`);
1070
1088
  const section = path.slice(0, idx);
1071
1089
  const key = path.slice(idx + 1);
1072
1090
  if (!CONFIG_SECTION_RE.test(section)) throw new Error(`config section "${section}" must match ^[a-z][a-z0-9-]*$`);
@@ -1150,8 +1168,8 @@ function docText(root) {
1150
1168
  " \"credentials\": {",
1151
1169
  " \"HOME_GITLAB_TOKEN\": \"xxx\"",
1152
1170
  " },",
1153
- " \"loop\": {",
1154
- " \"defaultModel\": \"minimax-cn-coding-plan/MiniMax-M3\"",
1171
+ " \"handyman\": {",
1172
+ " \"models\": [\"minimax-cn-coding-plan/MiniMax-M3\"]",
1155
1173
  " }",
1156
1174
  "}",
1157
1175
  "```",
@@ -1159,7 +1177,7 @@ function docText(root) {
1159
1177
  "",
1160
1178
  "## Key conventions",
1161
1179
  "- credential key: ^[A-Z][A-Z0-9_]*$ (e.g. HOME_GITLAB_TOKEN)",
1162
- "- config path: section.key (section ^[a-z][a-z0-9-]*$, key lowerCamel ^[a-z][a-zA-Z0-9_]*$, e.g. loop.defaultModel)",
1180
+ "- config path: section.key (section ^[a-z][a-z0-9-]*$, key lowerCamel ^[a-z][a-zA-Z0-9_]*$, e.g. handyman.models)",
1163
1181
  "",
1164
1182
  "## Reading (agents may use fs tools directly)",
1165
1183
  `- Read ${path} with the read tool → JSON.parse`,
@@ -1583,14 +1601,15 @@ const CCC_CONFIG_REFERENCE = `═══ CCC Configuration Reference ═══
1583
1601
  CCC-level features are configured in .opencode/serenity.json.
1584
1602
  Below are all available configuration sections.
1585
1603
 
1586
- ── 1. loop.defaultModel ──
1587
- loop 工具缺省模型(未配置且未传 model 参数时 loop 报错)。
1604
+ ── 1. handyman.models ──
1605
+ handyman(杂工)工具可用模型白名单(provider/model 列表);未配置时 handyman 报错要求配置。
1606
+ 缺省模型 = models[0](可用 handyman.defaultModel 指定,必须 ∈ models)。
1588
1607
 
1589
1608
  Config:
1590
- { "loop": { "defaultModel": "provider/model-name" } }
1609
+ { "handyman": { "models": ["provider/model-name"] } }
1591
1610
 
1592
1611
  Example:
1593
- { "loop": { "defaultModel": "minimax-cn-coding-plan/MiniMax-M3" } }
1612
+ { "handyman": { "models": ["minimax-cn-coding-plan/MiniMax-M3"], "defaultModel": "minimax-cn-coding-plan/MiniMax-M3", "maxRounds": 100, "maxParallel": 10 } }
1594
1613
 
1595
1614
  ── 2. sessionKeeper.threshold ──
1596
1615
  SESSION-KEEPER 提醒机制的积分阈值(非 headless 主 agent)。
@@ -2014,7 +2033,7 @@ function renderText$7(value) {
2014
2033
  }
2015
2034
  const msmTool = defineTool({
2016
2035
  name: "acc_msm",
2017
- description: "MSM (Mech & Semi-Mech) framework: list lists registered MSMs (header+flags display); exec executes (600s timeout, path-escape + symlink blocking, injects SERENITY_ROOT/CCC/VERSION env, appends --help TIP on failure; first arg --list/--schema/--format=json is protocol); register/deregister manage the registry (path inside root + script exists + globally unique validation, auto git precision commit); check quality checks DC-M1~M4; guide development manual; ccc-config CCC config reference (loop.defaultModel/sessionKeeper.threshold/localstore.gitTrack/hooks.autoRestoreSession). Reuses the CCC mech-registry.json.",
2036
+ description: "MSM (Mech & Semi-Mech) framework: list lists registered MSMs (header+flags display); exec executes (600s timeout, path-escape + symlink blocking, injects SERENITY_ROOT/CCC/VERSION env, appends --help TIP on failure; first arg --list/--schema/--format=json is protocol); register/deregister manage the registry (path inside root + script exists + globally unique validation, auto git precision commit); check quality checks DC-M1~M4; guide development manual; ccc-config CCC config reference (handyman.models/sessionKeeper.threshold/localstore.gitTrack/hooks.autoRestoreSession). Reuses the CCC mech-registry.json.",
2018
2037
  parameters: {
2019
2038
  action: {
2020
2039
  type: "string",
@@ -2287,46 +2306,52 @@ const cceTool = defineTool({
2287
2306
  }
2288
2307
  });
2289
2308
  //#endregion
2290
- //#region src/loop-preset-inherit.ts
2309
+ //#region src/handyman-preset-inherit.ts
2291
2310
  /**
2292
- * 解析 loop agent 从父 agent 继承的 preset,并组装创建 setup 钩子。
2293
- * @param parentCtx - 发起 loop 的 agent 的 scope ctx;无(headless/非 agent 上下文)时为 undefined。
2311
+ * 解析 handyman worker 从父 agent 继承的 preset,并组装创建 setup 钩子。
2312
+ * @param parentCtx - 发起 handyman 的 agent 的 scope ctx;无(headless/非 agent 上下文)时为 undefined。
2294
2313
  * @returns 继承结果:meta 用的 agentPreset 与创建 setup 钩子。
2295
2314
  */
2296
- function loopPresetInheritance(parentCtx) {
2297
- if (parentCtx === void 0) return {};
2315
+ function handymanPresetInheritance(parentCtx) {
2316
+ if (parentCtx === void 0) return { setup: (childCtx) => {
2317
+ childCtx.tools.restrict({ deny: ["handyman"] });
2318
+ } };
2298
2319
  const agentPreset = parentCtx.get("agentPresets")?.composedPreset(parentCtx);
2299
- if (agentPreset === void 0) return {};
2320
+ if (agentPreset === void 0) return { setup: (childCtx) => {
2321
+ childCtx.tools.restrict({ deny: ["handyman"] });
2322
+ } };
2300
2323
  return {
2301
2324
  agentPreset,
2302
2325
  setup: (childCtx) => {
2303
2326
  childCtx.get("agentPresets")?.composeFrom(childCtx, parentCtx);
2327
+ childCtx.tools.restrict({ deny: ["handyman"] });
2304
2328
  }
2305
2329
  };
2306
2330
  }
2307
2331
  //#endregion
2308
- //#region src/loop-ops.ts
2332
+ //#region src/handyman-ops.ts
2309
2333
  /**
2310
- * loop-ops.ts — acc_loop 纯逻辑层(零 DSH 依赖,可独立单测)
2334
+ * handyman-ops.ts — handyman(杂工)纯逻辑层(零 DSH 依赖,可独立单测)
2311
2335
  *
2312
- * 对齐 opencode-serenity-plugin loop 语义:进度文件(loop-<label>.md/.json)、
2313
- * 续跑、轮次 prompt 结构、stop token。
2336
+ * v1.24.0:loop(牛马)→ handyman(杂工)重命名。语义对齐 osp loop
2337
+ * 进度文件(handyman-<label>.md/.json)、续跑、轮次 prompt 结构、stop token。
2338
+ * 不兼容旧 loop- 进度文件(用户拍板:仅新 handyman- 前缀)。
2314
2339
  */
2315
2340
  /** label 脱敏(Windows 审计问题 17):非法字符 → '-',去尾点/空格,限长 */
2316
2341
  function sanitizeLabel(label) {
2317
2342
  return label.replace(/[<>:"/\\|?*\u0000-\u001f]/g, "-").replace(/[ .]+$/g, "").slice(0, 50);
2318
2343
  }
2319
- function loopProgressPaths(root, label) {
2344
+ function handymanProgressPaths(root, label) {
2320
2345
  const dir = join(root, "AGENT_SESSIONS");
2321
2346
  const safe = sanitizeLabel(label);
2322
2347
  return {
2323
- md: join(dir, `loop-${safe}.md`),
2324
- json: join(dir, `loop-${safe}.json`)
2348
+ md: join(dir, `handyman-${safe}.md`),
2349
+ json: join(dir, `handyman-${safe}.json`)
2325
2350
  };
2326
2351
  }
2327
2352
  /** 读取进度(续跑);无文件返回 round 0 */
2328
2353
  function readProgress(root, label) {
2329
- const { json } = loopProgressPaths(root, label);
2354
+ const { json } = handymanProgressPaths(root, label);
2330
2355
  if (!existsSync(json)) return null;
2331
2356
  try {
2332
2357
  return JSON.parse(readFileSync(json, "utf-8"));
@@ -2335,7 +2360,7 @@ function readProgress(root, label) {
2335
2360
  }
2336
2361
  }
2337
2362
  function writeProgress(root, label, p) {
2338
- const { md, json } = loopProgressPaths(root, label);
2363
+ const { md, json } = handymanProgressPaths(root, label);
2339
2364
  mkdirSync(join(root, "AGENT_SESSIONS"), { recursive: true });
2340
2365
  writeFileSync(json, JSON.stringify({
2341
2366
  ...p,
@@ -2343,7 +2368,7 @@ function writeProgress(root, label, p) {
2343
2368
  updated: (/* @__PURE__ */ new Date()).toISOString()
2344
2369
  }, null, 2) + "\n", "utf-8");
2345
2370
  const lines = [
2346
- `# loop: ${label}`,
2371
+ `# handyman: ${label}`,
2347
2372
  `- Model: ${p.model}`,
2348
2373
  `- Round: ${p.round}`,
2349
2374
  `- Done: ${p.done}`,
@@ -2357,7 +2382,7 @@ function writeProgress(root, label, p) {
2357
2382
  }
2358
2383
  /** 失败状态落盘(对齐 osp writeFailedStatus:done=true / status=failed / errorCode) */
2359
2384
  function writeFailedStatus(root, label, info) {
2360
- const { json } = loopProgressPaths(root, label);
2385
+ const { json } = handymanProgressPaths(root, label);
2361
2386
  mkdirSync(join(root, "AGENT_SESSIONS"), { recursive: true });
2362
2387
  const prev = readProgress(root, label);
2363
2388
  writeFileSync(json, JSON.stringify({
@@ -2373,7 +2398,7 @@ function writeFailedStatus(root, label, info) {
2373
2398
  }, null, 2) + "\n", "utf-8");
2374
2399
  }
2375
2400
  function newStopToken() {
2376
- return `SERENITY_LOOP_DONE_${randomBytes(8).toString("hex")}`;
2401
+ return `SERENITY_HANDYMAN_DONE_${randomBytes(8).toString("hex")}`;
2377
2402
  }
2378
2403
  /** 解析 model 字符串(provider/model)→ {provider, model};无 / 视为 model-only */
2379
2404
  function splitModel(model) {
@@ -2387,11 +2412,15 @@ function splitModel(model) {
2387
2412
  model: model.slice(idx + 1)
2388
2413
  };
2389
2414
  }
2415
+ /** 校验模型在白名单内;不在 → 抛错(用户拍板:只能使用 CCC 配置的模型) */
2416
+ function requireWhitelistedModel(model, models) {
2417
+ 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(", ")}`);
2418
+ }
2390
2419
  /** 轮次 prompt(对齐老 loop 结构:回顾进度 → 自由工作 → 汇报;S134 EAP 化:固定详尽) */
2391
2420
  function buildRoundPrompt(opts) {
2392
2421
  const { root, session, label, round, stopToken, progress, task } = opts;
2393
2422
  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.";
2394
- return `# ${label} — workhorse loop round ${round}
2423
+ return `# ${label} — handyman round ${round}
2395
2424
 
2396
2425
  CCC root: ${root}
2397
2426
  ${session ? `Work session: ${session} (progress recorded in AGENT_SESSIONS/${session}/SESSION.md)` : ""}
@@ -2415,49 +2444,53 @@ ${resumeNote}
2415
2444
  If the task is complete, output only ${stopToken}.`;
2416
2445
  }
2417
2446
  /**
2418
- * loop 规模化使用指引(guide 子命令输出;S134):
2419
- * 使用 loop 前必须先加载 eap 设计方案;并行策略;提示词规范(详尽固定 EAP);
2420
- * 阅读/文字编写类 loop 内部也加载 eap。
2447
+ * handyman 规模化使用指引(guide 子命令输出;S134 继承 + v1.24.0 更新):
2448
+ * 使用 handyman 前必须先加载 eap 设计方案;并行策略(jobs 编排);提示词规范(详尽固定 EAP);
2449
+ * 阅读/文字编写类 handyman 内部也加载 eap。
2421
2450
  */
2422
- const LOOP_GUIDE = `# loop — Scale-Up Usage Guide (guide)
2451
+ const HANDYMAN_GUIDE = `# handyman — Scale-Up Usage Guide (guide)
2423
2452
 
2424
2453
  ## ⚠️ Before using: load eap and design the plan
2425
- Before calling loop, load eap (acc-eap skill) and design the "scale-up loop plan" based on the EAP framework.
2454
+ Before calling handyman, load eap (acc-eap skill) and design the "scale-up handyman plan" based on the EAP framework.
2426
2455
 
2427
2456
  ### 1. Task decomposition (E↑ Explicit)
2428
2457
  - Split large tasks into explicit subtasks: each subtask defines goal / input / boundaries (what to do, what not to do) / acceptance criteria
2429
2458
  - Make dependencies explicit: dependent tasks run serially, independent ones can run in parallel
2430
2459
 
2431
- ### 2. Prompt design (loop's task parameter)
2460
+ ### 2. Prompt design (handyman's task parameter)
2432
2461
  - task must be detailed, fixed, and EAP-compliant: clear goal, drawn boundaries, decidable acceptance criteria
2433
2462
  - Anti-example "handle this file" — ambiguous; good example "read <path>, extract all rows of the「关键决策」table,
2434
2463
  output a JSON array (fields id/conclusion/evidence), do not modify the original file"
2435
2464
  - Reading/curating or text-writing work (extracting from files, summarizing, writing docs, generating text, etc.):
2436
- the loop-internal agent is also required to load eap and organize output per the EAP standard
2465
+ the handyman-internal agent is also required to load eap and organize output per the EAP standard
2437
2466
 
2438
- ### 3. Parallel strategy (scale-up)
2439
- - Independent subtasks can run in parallel: each subtask gets its own loop (own label + task)
2440
- - Parallel execution:
2441
- a. Background subagents: each subagent calls loop (own label), subagents run in parallel
2442
- b. workflow: parallel phase drives multiple loop sub-agents
2443
- - Concurrency safety guaranteed: unique sessionId (loop-<label>-<uuid>), progress files isolated per label
2444
- (AGENT_SESSIONS/loop-<label>.json) — same label resumes, different labels never interfere
2445
- - Aggregation: after each parallel loop produces progress, the main agent merges (or spawns one aggregation loop)
2467
+ ### 3. Model whitelist (CCC-configured, mandatory)
2468
+ - handyman only uses models listed in .opencode/serenity.json "handyman.models" never arbitrary models
2469
+ - Recursive subagents inside a handyman inherit the handyman's model automatically (DSH native)
2470
+ - Keep the subagent tool instance free of a fixed agentOptions, or model inheritance breaks
2446
2471
 
2447
- ## Completion criteria
2448
- - The only completion condition = the loop-internal agent echoes this round's random verification code (stop token); dialogue round cap 100 (osp fail-safe, forced stop beyond the cap, resumable)
2472
+ ### 4. Parallel strategy (jobs orchestration, workflow capability)
2473
+ - Independent subtasks can run in parallel via handyman(jobs=[...]): each job gets its own label + task + stop token + progress file
2474
+ - Concurrency safety guaranteed: unique sessionId (handyman-<label>-<uuid>), progress files isolated per label
2475
+ (AGENT_SESSIONS/handyman-<label>.json) — same label resumes, different labels never interfere
2476
+ - Parallel cap: handyman.maxParallel (default 10 — cheap models are cheap)
2477
+ - Aggregation: after each parallel job produces progress, the main agent merges (or spawns one aggregation handyman)
2478
+ - For programmable pipeline/phase orchestration at scale, use the platform's workflow tool instead
2479
+
2480
+ ## Completion criteria (osp loop standard)
2481
+ - The only completion condition = the handyman-internal agent echoes this round's random verification code (stop token); dialogue round cap (default 100, osp fail-safe, forced stop beyond the cap, resumable)
2449
2482
  - Automatic restart on abnormal agent stop (≤100 restarts, anti-infinite-loop)
2450
2483
 
2451
2484
  ## Waiting UI
2452
- - The WebUI session-header Serenity detail card shows running loops' progress (label / round / last response), one line per parallel task, ~3s refresh
2485
+ - The WebUI session-header Serenity detail card shows running handymen's progress (label / round / last response), one line per parallel job, ~3s refresh
2453
2486
  `;
2454
- /** 列出 AGENT_SESSIONS/loop-*.json 的全部进度(按 updated 倒序;坏文件跳过) */
2455
- function listActiveLoops(root) {
2487
+ /** 列出 AGENT_SESSIONS/handyman-*.json 的全部进度(按 updated 倒序;坏文件跳过) */
2488
+ function listActiveHandymen(root) {
2456
2489
  const dir = join(root, "AGENT_SESSIONS");
2457
2490
  if (!existsSync(dir)) return [];
2458
2491
  const out = [];
2459
2492
  for (const entry of readdirSync(dir)) {
2460
- if (!entry.startsWith("loop-") || !entry.endsWith(".json")) continue;
2493
+ if (!entry.startsWith("handyman-") || !entry.endsWith(".json")) continue;
2461
2494
  try {
2462
2495
  const data = JSON.parse(readFileSync(join(dir, entry), "utf-8"));
2463
2496
  if (typeof data.label !== "string" || typeof data.round !== "number") continue;
@@ -2475,26 +2508,26 @@ function listActiveLoops(root) {
2475
2508
  return out;
2476
2509
  }
2477
2510
  //#endregion
2478
- //#region src/tools/loop.ts
2511
+ //#region src/tools/handyman.ts
2479
2512
  /**
2480
- * loop.ts — acc_loop 真实 DSH 工具(老 loop 等效:廉价模型牛马循环)
2513
+ * handyman.ts — handyman(杂工)真实 DSH 工具(v1.24.0:loop 牛马 → handyman 杂工)
2481
2514
  *
2482
- * 语义对齐 opencode-serenity-plugin 老 loop:
2483
- * label(必)任务标签 → 进度文件 loop-<label>.md/.json
2484
- * session(选)工作会话 S###(上下文提示)
2485
- * model(选)provider/model(如 minimax-cn-coding-plan/MiniMax-M3);缺省读 loop.defaultModel
2486
- * (S134 修正:轮次不需要调用者指定——内部 while 驱动 agent 逐轮推进,
2487
- * 对话轮次**无上限**(不完成不返回);agent 非正常停止时自动重启,
2488
- * 重启次数上限 LOOP_MAX_RESTARTS=100(防死循环保险阀))
2515
+ * 用户需求(S142 拍板):
2516
+ * - **同步**(用户明确:这个事一般不会异步——工具阻塞到完成)
2517
+ * - **指定模型**(CCC 白名单内——handyman.models,未配置报错)
2518
+ * - **自主循环到完成**(stop-token 唯一完成判据,对齐 osp loop
2519
+ * - **内部递归同样低能 subagent**(worker 工具面含 subagent,DSH 原生模型继承)
2520
+ * - **workflow 能力**(jobs 编排:主 agent 一次并行多个杂工,maxParallel 默认 10)
2521
+ * - **worker 不含 handyman 本身**(递归编排归主 agent,防无限嵌套)
2522
+ * - **不兼容旧 loop 进度文件**(仅 handyman- 前缀)
2489
2523
  *
2490
2524
  * 机制:ctx.agents.create()(带 setup 钩子)创建专用 agent(进程内),
2491
2525
  * 每轮 followup → agent/status idle → 读 session.events 响应 → 写进度 → stop token 检查 →
2492
2526
  * 未完成继续下一轮;followup/waitIdle 抛错(非正常停止)→ dispose 并重新 create agent
2493
2527
  * (重启计数,≤100),同一轮重试。工厂模式:apply 时闭包捕获插件 ctx(工具 execute 无 ctx 参数)。
2494
2528
  *
2495
- * preset 继承:setup 钩子里对子 agent 执行 agentPresets.composeFrom(对齐 subagent 先例),
2496
- * 使 loop agent 继承发起方会话的 agent preset 工具(read/write/edit 等 preset 层工具)。
2497
- * agentPresets 是可选服务——无 preset 装配的环境(无 roster 部署)退化为空工具层(历史行为)。
2529
+ * preset 继承 + 工具收窄:setup 钩子里对子 agent 执行 agentPresets.composeFrom(对齐
2530
+ * subagent 先例)+ tools.restrict deny handyman(worker 内部看不到 handyman 工具)。
2498
2531
  */
2499
2532
  function agentCwd$4(exec) {
2500
2533
  return (exec.agent?.session)?.header?.cwd ?? process.cwd();
@@ -2505,7 +2538,7 @@ function renderText$3(value) {
2505
2538
  text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
2506
2539
  }];
2507
2540
  }
2508
- /** 等待 agent 空闲(agent/status → idle);无超时(loop 可永续,agent 工作多久等多久) */
2541
+ /** 等待 agent 空闲(agent/status → idle);无超时(handyman 可永续,agent 工作多久等多久) */
2509
2542
  function waitIdle(ctx, agent) {
2510
2543
  return new Promise((resolve) => {
2511
2544
  let settled = false;
@@ -2533,22 +2566,160 @@ function lastAssistantText(agent) {
2533
2566
  }
2534
2567
  return "";
2535
2568
  }
2536
- /** 创建 loop 工具(闭包捕获插件 ctx可访问 ctx.agentLoop) */
2537
- /** 创建 loop 工具(闭包捕获插件 ctx → 可访问 ctx.agentLoop) */
2538
- function createLoopTool(ctx) {
2569
+ /** 运行时解析 jobs 参数(DSH schema 不支持 object items 的 required/校验 手工校验类型) */
2570
+ function parseJobs(raw) {
2571
+ if (!Array.isArray(raw) || raw.length === 0) return null;
2572
+ const jobs = [];
2573
+ for (const item of raw) {
2574
+ if (typeof item !== "object" || item === null) return null;
2575
+ const rec = item;
2576
+ if (typeof rec.task !== "string" || typeof rec.label !== "string") return null;
2577
+ jobs.push({
2578
+ task: rec.task,
2579
+ label: rec.label,
2580
+ model: typeof rec.model === "string" ? rec.model : void 0
2581
+ });
2582
+ }
2583
+ return jobs;
2584
+ }
2585
+ /**
2586
+ * 驱动单个 handyman worker:创建 agent → while 循环(stop-token 完成判据)→ dispose。
2587
+ * 同步阻塞直到完成/保险阀。jobs 编排时多个 worker 并行(Promise.all)。
2588
+ */
2589
+ async function runHandymanJob(ctx, opts) {
2590
+ const { root, job, defaultModel, models, maxRounds, parentSession, parentCtx, session } = opts;
2591
+ const label = job.label;
2592
+ const model = job.model ?? defaultModel;
2593
+ requireWhitelistedModel(model, models);
2594
+ const { provider, model: modelName } = splitModel(model);
2595
+ const stopToken = newStopToken();
2596
+ let progress = readProgress(root, label);
2597
+ const startRound = progress ? progress.round + 1 : 1;
2598
+ const inherited = handymanPresetInheritance(parentCtx);
2599
+ if (!ctx.agents) throw new Error("handyman: ctx.agents unavailable");
2600
+ let handle;
2601
+ let workerAgent;
2602
+ const spawnAgent = async () => {
2603
+ const sessionId = `handyman-${label}-${randomUUID()}`;
2604
+ handle = await ctx.agents.create({
2605
+ sessionId,
2606
+ meta: {
2607
+ cwd: root,
2608
+ origin: "subagent",
2609
+ ...parentSession === void 0 ? {} : { parentSession },
2610
+ ...inherited.agentPreset === void 0 ? {} : { agentPreset: inherited.agentPreset }
2611
+ },
2612
+ agentOptions: {
2613
+ provider,
2614
+ model: modelName
2615
+ },
2616
+ ...inherited.setup === void 0 ? {} : { setup: inherited.setup }
2617
+ });
2618
+ workerAgent = handle.agent;
2619
+ };
2620
+ await spawnAgent();
2621
+ let done = false;
2622
+ let lastResponse = progress?.lastResponse ?? "";
2623
+ let finalRound = startRound - 1;
2624
+ let restarts = 0;
2625
+ let finishReason = "done";
2626
+ try {
2627
+ let round = startRound;
2628
+ while (true) {
2629
+ if (round > maxRounds) {
2630
+ finishReason = "max_rounds";
2631
+ break;
2632
+ }
2633
+ finalRound = round;
2634
+ const prompt = buildRoundPrompt({
2635
+ root,
2636
+ session,
2637
+ label,
2638
+ round,
2639
+ stopToken,
2640
+ progress,
2641
+ task: job.task
2642
+ });
2643
+ try {
2644
+ workerAgent.followup(createUserMessage({
2645
+ content: [{
2646
+ type: "text",
2647
+ text: prompt
2648
+ }],
2649
+ source: {
2650
+ kind: "plugin",
2651
+ plugin: "dsh-serenity-hooks"
2652
+ }
2653
+ }));
2654
+ await waitIdle(ctx, workerAgent);
2655
+ lastResponse = lastAssistantText(workerAgent);
2656
+ } catch {
2657
+ restarts++;
2658
+ if (restarts > 100) {
2659
+ finishReason = "restart_exceeded";
2660
+ break;
2661
+ }
2662
+ await handle.dispose().catch(() => {});
2663
+ await spawnAgent();
2664
+ continue;
2665
+ }
2666
+ progress = {
2667
+ round,
2668
+ done: false,
2669
+ label,
2670
+ model,
2671
+ updated: (/* @__PURE__ */ new Date()).toISOString(),
2672
+ lastResponse
2673
+ };
2674
+ writeProgress(root, label, progress);
2675
+ if (lastResponse.includes(stopToken)) {
2676
+ done = true;
2677
+ finishReason = "done";
2678
+ break;
2679
+ }
2680
+ round++;
2681
+ }
2682
+ writeProgress(root, label, {
2683
+ round: finalRound,
2684
+ done,
2685
+ label,
2686
+ model,
2687
+ updated: (/* @__PURE__ */ new Date()).toISOString(),
2688
+ lastResponse,
2689
+ status: done ? "done" : "running"
2690
+ });
2691
+ if (finishReason !== "done") writeFailedStatus(root, label, {
2692
+ errorCode: finishReason,
2693
+ errorMessage: finishReason === "max_rounds" ? `Reached ${maxRounds} rounds without completion — resume with same label to continue.` : `Agent restarted 100 times without progress — resume with same label to continue.`
2694
+ });
2695
+ } finally {
2696
+ await handle.dispose().catch(() => {});
2697
+ }
2698
+ const { json } = handymanProgressPaths(root, label);
2699
+ return {
2700
+ label,
2701
+ done,
2702
+ rounds: finalRound,
2703
+ finishReason,
2704
+ restarts,
2705
+ model,
2706
+ lastResponse: lastResponse.slice(0, 2e3),
2707
+ progressFile: json
2708
+ };
2709
+ }
2710
+ /** 创建 handyman 工具(闭包捕获插件 ctx → 可访问 ctx.agents) */
2711
+ function createHandymanTool(ctx) {
2539
2712
  return defineTool({
2540
- name: "loop",
2541
- description: "The workhorse loop (equivalent to the legacy loop): create a dedicated agent with the specified model and run it repeatedly until the task is done.\nUsage: loop guide (print the scale-up usage guide — load eap to design a scale-up plan before using); loop accepts task (the goal to complete) or relies on session context; default model reads loop.defaultModel from .opencode/serenity.json (currently minimax-cn-coding-plan/MiniMax-M3, the cheap workhorse).\nBehavior: an internal hard while-loop drives the agent round by round, each round waits with no timeout (the agent works as long as it needs). The only completion condition = the agent echoes this round's random completion code (stop token), preventing low-intelligence models from finishing early. Rounds need no caller input — dialogue round cap 100 (osp fail-safe; forced stop beyond the cap, resumable), automatic restart on abnormal stop (≤100 restarts, anti-infinite-loop fail-safe).\nProgress: written to AGENT_SESSIONS/loop-<label>.md/.json; calling again with the same label resumes from the last round (no redo).\nConstraints: loop agents carry full Serenity constraints (ACC identity / entry-skill system prompt / guards / trajectory-steward).\nExample: loop 执行「扫描 SQC 并修复 DC 问题」 (run SQC scan and fix DC issues), label: sqc-scan; loop guide",
2713
+ name: "handyman",
2714
+ description: "The handyman (杂工): delegate a do-everything worker agent to a CCC-configured model and run it synchronously in rounds until the task is done.\nUsage: handyman guide (print the scale-up usage guide — load eap to design a plan before using); single task: handyman(task, label, [model]); multi-job orchestration (workflow capability): handyman(jobs=[{task,label,model?},...]).\nModel: only models whitelisted in .opencode/serenity.json \"handyman.models\" (missing config = error); default reads handyman.defaultModel; jobs run in parallel (handyman.maxParallel, default 10 cheap models are cheap).\nBehavior: each worker runs synchronously (this call blocks until done) with an internal hard while-loop; the only completion condition = the worker echoes this round's random completion code (stop token), preventing low-intelligence models from finishing early. Round cap (default 100, osp fail-safe; resumable), automatic restart on abnormal stop (≤100 restarts).\nRecursion: a worker's tool set includes the subagent tool (DSH-native model inheritance — its subagents use the same model), but NOT handyman itself (orchestration belongs to the main agent).\nProgress: AGENT_SESSIONS/handyman-<label>.md/.json; same label resumes from the last round (no redo). Legacy loop- progress files are NOT compatible (v1.24.0).\nExample: handyman(task=\"扫描 SQC 并修复 DC 问题\", label=\"sqc-scan\"); handyman(jobs=[{task:\"扫描 A\",label:\"scan-a\"},{task:\"扫描 B\",label:\"scan-b\"}]); handyman guide",
2542
2715
  parameters: {
2543
2716
  task: {
2544
2717
  type: "string",
2545
- required: true,
2546
- description: "The task goal to complete (required: tell the loop agent what to do)"
2718
+ description: "The task goal to complete (required for single-task mode; must be detailed and EAP-compliant)"
2547
2719
  },
2548
2720
  label: {
2549
2721
  type: "string",
2550
- required: true,
2551
- description: "Task label (1-50 chars; progress file named loop-<label>.md/.json)"
2722
+ description: "Task label (1-50 chars; progress file named handyman-<label>.md/.json)"
2552
2723
  },
2553
2724
  session: {
2554
2725
  type: "string",
@@ -2556,11 +2727,24 @@ function createLoopTool(ctx) {
2556
2727
  },
2557
2728
  model: {
2558
2729
  type: "string",
2559
- description: "provider/model (e.g. minimax-cn-coding-plan/MiniMax-M3); default reads loop.defaultModel"
2730
+ description: "provider/model — must be in the CCC whitelist handyman.models; default reads handyman.defaultModel"
2731
+ },
2732
+ jobs: {
2733
+ type: "array",
2734
+ description: "Multi-job orchestration (workflow capability): [{task, label, model?}, ...] — runs jobs in parallel (cap handyman.maxParallel, default 10)",
2735
+ items: {
2736
+ type: "object",
2737
+ properties: {
2738
+ task: { type: "string" },
2739
+ label: { type: "string" },
2740
+ model: { type: "string" }
2741
+ },
2742
+ additionalProperties: false
2743
+ }
2560
2744
  },
2561
2745
  guide: {
2562
2746
  type: "boolean",
2563
- description: "Print the scale-up usage guide (creates no agent; read before using loop — includes eap scale-up design requirements / parallelism strategy / prompt conventions)"
2747
+ description: "Print the scale-up usage guide (creates no agent; includes eap design requirements / whitelist rules / jobs parallel strategy / prompt conventions)"
2564
2748
  }
2565
2749
  },
2566
2750
  output: {
@@ -2568,135 +2752,45 @@ function createLoopTool(ctx) {
2568
2752
  render: (_args, value) => renderText$3(value)
2569
2753
  },
2570
2754
  async execute(args, exec) {
2571
- if (args.guide) return { guide: LOOP_GUIDE };
2755
+ if (args.guide) return { guide: HANDYMAN_GUIDE };
2572
2756
  const root = findSerenityRoot(agentCwd$4(exec));
2573
2757
  if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
2574
- const cfg = loadSerenityConfig(root, DEFAULT_SERENITY_CONFIG_PATHS);
2575
- const model = args.model ?? cfg.loop?.defaultModel;
2576
- if (!model) throw new Error("loop requires a model: pass --model or configure .opencode/serenity.json loop.defaultModel");
2577
- const label = args.label;
2578
- if (!ctx.agentLoop) throw new Error("loop: ctx.agentLoop unavailable");
2579
- const { provider, model: modelName } = splitModel(model);
2580
- const stopToken = newStopToken();
2581
- let progress = readProgress(root, label);
2582
- const startRound = progress ? progress.round + 1 : 1;
2758
+ const hc = readHandymanConfig(root, DEFAULT_SERENITY_CONFIG_PATHS);
2759
+ 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\"}})");
2760
+ const jobs = args.jobs !== void 0 ? (() => {
2761
+ const parsed = parseJobs(args.jobs);
2762
+ if (parsed === null) throw new Error("handyman: jobs must be [{task, label, model?}, ...] (every job requires task and label)");
2763
+ return parsed;
2764
+ })() : (() => {
2765
+ if (typeof args.task !== "string" || typeof args.label !== "string") throw new Error("handyman: task and label are required (single-job mode)");
2766
+ return [{
2767
+ task: args.task,
2768
+ label: args.label
2769
+ }];
2770
+ })();
2771
+ if (jobs.length === 0) throw new Error("handyman: no jobs provided");
2772
+ if (jobs.length > hc.maxParallel) throw new Error(`handyman: ${jobs.length} jobs exceed maxParallel ${hc.maxParallel} (configure handyman.maxParallel to raise)`);
2773
+ if (jobs.some((j) => !j.task || !j.label)) throw new Error("handyman: every job requires task and label");
2583
2774
  const parentCtx = exec.agent?.ctx;
2584
- const inherited = loopPresetInheritance(parentCtx);
2585
- if (!ctx.agents) throw new Error("loop: ctx.agents unavailable");
2586
2775
  const parentSession = (exec.agent?.session)?.id;
2587
- let handle;
2588
- let loopAgent;
2589
- const spawnAgent = async () => {
2590
- const sessionId = `loop-${label}-${randomUUID()}`;
2591
- handle = await ctx.agents.create({
2592
- sessionId,
2593
- meta: {
2594
- cwd: root,
2595
- origin: "subagent",
2596
- ...parentSession === void 0 ? {} : { parentSession },
2597
- ...inherited.agentPreset === void 0 ? {} : { agentPreset: inherited.agentPreset }
2598
- },
2599
- agentOptions: {
2600
- provider,
2601
- model: modelName
2602
- },
2603
- ...inherited.setup === void 0 ? {} : { setup: inherited.setup }
2604
- });
2605
- loopAgent = handle.agent;
2606
- };
2607
- await spawnAgent();
2608
- let done = false;
2609
- let lastResponse = progress?.lastResponse ?? "";
2610
- let finalRound = startRound - 1;
2611
- let restarts = 0;
2612
- let finishReason = "done";
2613
- try {
2614
- let round = startRound;
2615
- while (true) {
2616
- if (round > 100) {
2617
- finishReason = "max_rounds";
2618
- break;
2619
- }
2620
- finalRound = round;
2621
- const prompt = buildRoundPrompt({
2622
- root,
2623
- session: args.session,
2624
- label,
2625
- round,
2626
- stopToken,
2627
- progress,
2628
- task: args.task
2629
- });
2630
- try {
2631
- loopAgent.followup(createUserMessage({
2632
- content: [{
2633
- type: "text",
2634
- text: prompt
2635
- }],
2636
- source: {
2637
- kind: "plugin",
2638
- plugin: "dsh-serenity-hooks"
2639
- }
2640
- }));
2641
- await waitIdle(ctx, loopAgent);
2642
- lastResponse = lastAssistantText(loopAgent);
2643
- } catch {
2644
- restarts++;
2645
- if (restarts > 100) {
2646
- finishReason = "restart_exceeded";
2647
- break;
2648
- }
2649
- await handle.dispose().catch(() => {});
2650
- await spawnAgent();
2651
- continue;
2652
- }
2653
- progress = {
2654
- round,
2655
- done: false,
2656
- label,
2657
- model,
2658
- updated: (/* @__PURE__ */ new Date()).toISOString(),
2659
- lastResponse
2660
- };
2661
- writeProgress(root, label, progress);
2662
- if (lastResponse.includes(stopToken)) {
2663
- done = true;
2664
- finishReason = "done";
2665
- break;
2666
- }
2667
- round++;
2668
- }
2669
- writeProgress(root, label, {
2670
- round: finalRound,
2671
- done,
2672
- label,
2673
- model,
2674
- updated: (/* @__PURE__ */ new Date()).toISOString(),
2675
- lastResponse,
2676
- status: done ? "done" : "running"
2677
- });
2678
- if (finishReason !== "done") writeFailedStatus(root, label, {
2679
- errorCode: finishReason,
2680
- errorMessage: finishReason === "max_rounds" ? `Reached 100 rounds without completion — resume with same label to continue.` : `Agent restarted 100 times without progress — resume with same label to continue.`
2681
- });
2682
- } finally {
2683
- await handle.dispose().catch(() => {});
2684
- }
2685
- const { json } = loopProgressPaths(root, label);
2776
+ const results = await Promise.all(jobs.map((job) => runHandymanJob(ctx, {
2777
+ root,
2778
+ job,
2779
+ defaultModel: hc.defaultModel,
2780
+ models: hc.models,
2781
+ maxRounds: hc.maxRounds,
2782
+ parentSession,
2783
+ parentCtx,
2784
+ session: typeof args.session === "string" ? args.session : void 0
2785
+ })));
2686
2786
  return {
2687
- done,
2688
- rounds: finalRound,
2689
- finishReason,
2690
- restarts,
2691
- model,
2692
- label,
2693
- progressFile: json,
2694
- lastResponse: lastResponse.slice(0, 2e3),
2787
+ done: results.every((r) => r.done),
2788
+ jobs: results.map((r) => ({ ...r })),
2695
2789
  usage: {
2696
- how: `loop drives the agent round by round via an internal hard while-loop; the agent terminates by echoing the random completion code; dialogue round cap 100 (osp fail-safe), automatic restart on abnormal stop (≤100 times)`,
2697
- progress: `Progress in AGENT_SESSIONS/loop-${label}.md and .json; calling loop again with the same label resumes from the next round (no redo)`,
2698
- constraints: "loop agents carry full Serenity constraints (ACC identity / entry-skill system prompt / guards / trajectory-steward)",
2699
- next: done ? "Task completed; check the progress file to wrap up" : `Task incomplete (${finishReason}, hit 100 rounds or 100 restart fail-safe); resume with the same label`
2790
+ 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",
2791
+ progress: "Progress in AGENT_SESSIONS/handyman-<label>.md and .json; calling handyman again with the same label resumes from the next round (no redo)",
2792
+ 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",
2793
+ 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"
2700
2794
  }
2701
2795
  };
2702
2796
  }
@@ -5762,7 +5856,7 @@ function createEpochPromotion(promoteEvents, requiredSignals = 1, maxRoundsFallb
5762
5856
  promoted: true
5763
5857
  };
5764
5858
  const loopSid = session.id;
5765
- if (typeof loopSid === "string" && loopSid.startsWith("loop-")) return {
5859
+ if (typeof loopSid === "string" && loopSid.startsWith("handyman-")) return {
5766
5860
  boundary: -1,
5767
5861
  promoted: true
5768
5862
  };
@@ -5866,7 +5960,7 @@ function registerBootstrap(ctx) {
5866
5960
  const session = agent.session;
5867
5961
  const depth = session?.header?.delegationDepth ?? 0;
5868
5962
  const sid = typeof session?.id === "string" ? session.id : void 0;
5869
- if (sid !== void 0 && sid.startsWith("loop-")) return;
5963
+ if (sid !== void 0 && sid.startsWith("handyman-")) return;
5870
5964
  if (depth === 0) {
5871
5965
  if (session?.events?.some((event) => event.type === "user/message")) return;
5872
5966
  } else if (sid !== void 0 && anchoredSessions.has(sid)) return;
@@ -6218,7 +6312,7 @@ function renderText(value) {
6218
6312
  }
6219
6313
  const localstoreTool = defineTool({
6220
6314
  name: "localstore",
6221
- 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), cc_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. loop.defaultModel).",
6315
+ 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), cc_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).",
6222
6316
  parameters: {
6223
6317
  action: {
6224
6318
  type: "string",
@@ -6818,7 +6912,7 @@ function accBlock(root) {
6818
6912
  " eap — return the full EAP cognitive quality framework",
6819
6913
  " neat — return the full Neat design collaboration protocol",
6820
6914
  " cce — return the full Cognitive Continuity Engineering framework",
6821
- " loop run a dedicated model-specific agent in repeated rounds toward a goal",
6915
+ " handyman delegate a do-everything worker agent (CCC-whitelisted model) to run synchronously in rounds until done, recursing into same-model subagents; jobs=[] orchestrates parallel work",
6822
6916
  " session_rebuild — rebuild this conversation in place from SESSION.md when the trajectory-tracker trips",
6823
6917
  " localstore — ACC local credential/config storage (CCC-root localstore.json, JSON format; git policy localstore.gitTrack default deny); doc subcommand outputs the spec",
6824
6918
  "",
@@ -7270,7 +7364,8 @@ function registerEntrySkillSection(agent, root) {
7270
7364
  //#region src/seams/context.ts
7271
7365
  const DEFAULT_ENTRY_SKILL_MAX_CHARS = 3e4;
7272
7366
  function accIdentityText(root, configPaths = DEFAULT_SERENITY_CONFIG_PATHS, entrySkillMaxChars = DEFAULT_ENTRY_SKILL_MAX_CHARS) {
7273
- const loop = loadSerenityConfig(root, configPaths).loop?.defaultModel;
7367
+ loadSerenityConfig(root, configPaths);
7368
+ const defaultModel = readHandymanConfig(root, configPaths)?.defaultModel;
7274
7369
  const phase2 = existsSync(resolve(root, ".dsh", "PHASE2-PROMPT.md"));
7275
7370
  const lines = [
7276
7371
  `[ACC] Serenity cognitive container active (dsh-serenity-hooks v${ACC_VERSION})`,
@@ -7278,7 +7373,7 @@ function accIdentityText(root, configPaths = DEFAULT_SERENITY_CONFIG_PATHS, entr
7278
7373
  `- Constraints: path isolation (P3, fs sandbox) + session tracking (AGENT_SESSIONS/)`,
7279
7374
  `- Knowledge: load the acc-serenity entry skill; use acc-eap / acc-neat for design collaboration`
7280
7375
  ];
7281
- if (loop) lines.push(`- loop default model: ${loop}`);
7376
+ if (defaultModel) lines.push(`- handyman default model: ${defaultModel}`);
7282
7377
  if (phase2) {
7283
7378
  lines.push("- ⚠️ **Phase 2 cognitive alignment interview pending**: work through the 5 Topics below and record answers in an AGENT_SESSIONS/ session");
7284
7379
  try {
@@ -7293,7 +7388,7 @@ const PLUGIN_SOURCE = {
7293
7388
  plugin: "dsh-serenity-hooks"
7294
7389
  };
7295
7390
  /**
7296
- * ACC 注入消息(S134 去重):**只含简短身份锚点**([ACC] 已激活 + CCC 根 + 约束 + loop 模型 + Phase 2)。
7391
+ * ACC 注入消息(S134 去重):**只含简短身份锚点**([ACC] 已激活 + CCC 根 + 约束 + handyman 模型 + Phase 2)。
7297
7392
  * 完整身份(ACC 5 块 + CCE + Constraints + EAP + SKILL 全文 + Session 块)由**系统提示词层**
7298
7393
  * (systemPrompt.section,每轮 prompt 装配自动注入,含 subagent)承担——对话消息流/压缩重注入
7299
7394
  * 不再重复注入同一内容(token 双倍浪费,见 S134 注入方案梳理)。
@@ -7321,7 +7416,7 @@ function agentScope(agent) {
7321
7416
  * 只有"conversation 根会话"才自动恢复最近激活的宁静号会话——
7322
7417
  * - subagent:session header `origin === 'subagent'`(DSH 路由语义,agent-lookup.ts)
7323
7418
  * - 派生会话:`parentSession` 存在(任何子会话)
7324
- * - loop 牛马:sessionId 固定 `loop-` 前缀(tools/loop.ts 生成)
7419
+ * - handyman 杂工:sessionId 固定 `handyman-` 前缀(tools/handyman.ts 生成)
7325
7420
  * 三者都不恢复(避免把主会话激活注入子上下文,违背 v1.16.2 scope 隔离)。
7326
7421
  */
7327
7422
  function shouldAutoRestore(agent) {
@@ -7329,7 +7424,7 @@ function shouldAutoRestore(agent) {
7329
7424
  if (!session) return false;
7330
7425
  if (session.header?.origin === "subagent") return false;
7331
7426
  if (session.header?.parentSession) return false;
7332
- if (session.id?.startsWith("loop-")) return false;
7427
+ if (session.id?.startsWith("handyman-")) return false;
7333
7428
  return true;
7334
7429
  }
7335
7430
  /**
@@ -7417,7 +7512,7 @@ function registerCompactRetention(ctx, opts = {}) {
7417
7512
  //#endregion
7418
7513
  //#region src/api.ts
7419
7514
  const ROUTE_PATH = "/serenity/status";
7420
- const LOOPS_PATH = "/serenity/loops";
7515
+ const HANDYMEN_PATH = "/serenity/handymen";
7421
7516
  const UPLOAD_PATH = "/serenity/image-upload";
7422
7517
  const CONFIG_PATH = "/serenity/config";
7423
7518
  /** 图片落盘目录(CCC 根相对;S142 图片自动识别基础设施——粘贴图片落盘供 agent 经 CCC vlm MSM 自主处理) */
@@ -7500,7 +7595,7 @@ function registerStatusApi(ctx, opts = {}) {
7500
7595
  const configPaths = opts.configPaths ?? DEFAULT_SERENITY_CONFIG_PATHS;
7501
7596
  ctx.webServer.register({
7502
7597
  kind: "exact",
7503
- path: LOOPS_PATH,
7598
+ path: HANDYMEN_PATH,
7504
7599
  handler: async (req, res) => {
7505
7600
  try {
7506
7601
  if (req.method !== "GET") {
@@ -7513,10 +7608,10 @@ function registerStatusApi(ctx, opts = {}) {
7513
7608
  workspace: url.searchParams.get("workspace") ?? void 0
7514
7609
  }));
7515
7610
  if (!root) {
7516
- sendJson(res, 200, { loops: [] });
7611
+ sendJson(res, 200, { handymen: [] });
7517
7612
  return;
7518
7613
  }
7519
- sendJson(res, 200, { loops: listActiveLoops(root) });
7614
+ sendJson(res, 200, { handymen: listActiveHandymen(root) });
7520
7615
  } catch (err) {
7521
7616
  sendJson(res, 400, { error: err.message ?? String(err) });
7522
7617
  }
@@ -8546,7 +8641,7 @@ function apply(ctx, config) {
8546
8641
  ctx.tools.register(eapTool);
8547
8642
  ctx.tools.register(neatTool);
8548
8643
  ctx.tools.register(cceTool);
8549
- ctx.tools.register(createLoopTool(ctx));
8644
+ ctx.tools.register(createHandymanTool(ctx));
8550
8645
  ctx.tools.register(createRebuildTool(ctx));
8551
8646
  ctx.tools.register(localstoreTool);
8552
8647
  }
package/lib/invariant.js CHANGED
@@ -41,7 +41,7 @@ const REGISTERED_TOOLS = [
41
41
  "eap",
42
42
  "neat",
43
43
  "cce",
44
- "loop"
44
+ "handyman"
45
45
  ];
46
46
  //#endregion
47
47
  export { REGISTERED_TOOLS, readPluginManifest, verifyToolConsistency };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shgroup/dsh-serenity-hooks",
3
- "version": "1.23.8",
3
+ "version": "1.24.0",
4
4
  "description": "宁静号 ACC harness — Native Cordis 插件(DeepSeek Harness 运行时)。真实 DSH 工具注册(cc_fs/session/acc_msm 等 9 工具)+ 拦截缝机械约束(safe-mode/路径守卫/会话落盘)+ 系统提示词注入(ACC/CCE/Constraints/SKILL/Session 五块)。适配 DSH 公开版(deepseek-ai/deepseek-harness 0.1.0-rc)。",
5
5
  "license": "MIT",
6
6
  "repository": {