@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 +2 -2
- package/dsh.plugin.json +3 -3
- package/lib/client.js +30 -30
- package/lib/index.js +309 -214
- package/lib/invariant.js +1 -1
- package/package.json +1 -1
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` / `
|
|
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
|
-
"
|
|
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.
|
|
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/
|
|
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
|
-
"
|
|
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/*
|
|
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
|
|
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 [
|
|
32
|
-
const [
|
|
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(`${
|
|
53
|
-
if (alive)
|
|
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)
|
|
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
|
|
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: "
|
|
150
|
+
children: "Handyman 模型"
|
|
151
151
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
152
152
|
className: cx("sp-value"),
|
|
153
|
-
title: status.
|
|
154
|
-
children: status.
|
|
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: "
|
|
209
|
-
}),
|
|
208
|
+
children: "Handyman 运行"
|
|
209
|
+
}), handymen.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
210
210
|
type: "button",
|
|
211
|
-
className: cx("sp-
|
|
212
|
-
onClick: () =>
|
|
213
|
-
children:
|
|
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:
|
|
225
|
+
children: runningHandymen.length > 0 ? `运行中 ${runningHandymen.length}${handymen.length > 0 ? ` / 共 ${handymen.length}` : ""}` : "无运行中 handyman"
|
|
226
226
|
})]
|
|
227
227
|
})
|
|
228
228
|
}),
|
|
229
|
-
|
|
230
|
-
className: cx("sp-
|
|
231
|
-
children: [
|
|
232
|
-
className: cx("sp-
|
|
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-
|
|
234
|
+
className: cx("sp-handymanHead"),
|
|
235
235
|
children: [
|
|
236
236
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
237
|
-
className: cx("sp-
|
|
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-
|
|
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-
|
|
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-
|
|
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)),
|
|
256
|
-
className: cx("sp-
|
|
255
|
+
}, l.label)), handymen.length > 3 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
256
|
+
className: cx("sp-handymenMore"),
|
|
257
257
|
children: [
|
|
258
258
|
"… 共 ",
|
|
259
|
-
|
|
260
|
-
" 个
|
|
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 阈值 /
|
|
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
|
-
|
|
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
|
-
|
|
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,如
|
|
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.
|
|
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
|
-
" \"
|
|
1154
|
-
" \"
|
|
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.
|
|
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.
|
|
1587
|
-
|
|
1604
|
+
── 1. handyman.models ──
|
|
1605
|
+
handyman(杂工)工具可用模型白名单(provider/model 列表);未配置时 handyman 报错要求配置。
|
|
1606
|
+
缺省模型 = models[0](可用 handyman.defaultModel 指定,必须 ∈ models)。
|
|
1588
1607
|
|
|
1589
1608
|
Config:
|
|
1590
|
-
{ "
|
|
1609
|
+
{ "handyman": { "models": ["provider/model-name"] } }
|
|
1591
1610
|
|
|
1592
1611
|
Example:
|
|
1593
|
-
{ "
|
|
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 (
|
|
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/
|
|
2309
|
+
//#region src/handyman-preset-inherit.ts
|
|
2291
2310
|
/**
|
|
2292
|
-
* 解析
|
|
2293
|
-
* @param parentCtx - 发起
|
|
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
|
|
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/
|
|
2332
|
+
//#region src/handyman-ops.ts
|
|
2309
2333
|
/**
|
|
2310
|
-
*
|
|
2334
|
+
* handyman-ops.ts — handyman(杂工)纯逻辑层(零 DSH 依赖,可独立单测)
|
|
2311
2335
|
*
|
|
2312
|
-
*
|
|
2313
|
-
*
|
|
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
|
|
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, `
|
|
2324
|
-
json: join(dir, `
|
|
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 } =
|
|
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 } =
|
|
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
|
-
`#
|
|
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 } =
|
|
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 `
|
|
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} —
|
|
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
|
-
*
|
|
2419
|
-
* 使用
|
|
2420
|
-
* 阅读/文字编写类
|
|
2447
|
+
* handyman 规模化使用指引(guide 子命令输出;S134 继承 + v1.24.0 更新):
|
|
2448
|
+
* 使用 handyman 前必须先加载 eap 设计方案;并行策略(jobs 编排);提示词规范(详尽固定 EAP);
|
|
2449
|
+
* 阅读/文字编写类 handyman 内部也加载 eap。
|
|
2421
2450
|
*/
|
|
2422
|
-
const
|
|
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
|
|
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 (
|
|
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
|
|
2465
|
+
the handyman-internal agent is also required to load eap and organize output per the EAP standard
|
|
2437
2466
|
|
|
2438
|
-
### 3.
|
|
2439
|
-
-
|
|
2440
|
-
-
|
|
2441
|
-
|
|
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
|
-
|
|
2448
|
-
-
|
|
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
|
|
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/
|
|
2455
|
-
function
|
|
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("
|
|
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/
|
|
2511
|
+
//#region src/tools/handyman.ts
|
|
2479
2512
|
/**
|
|
2480
|
-
*
|
|
2513
|
+
* handyman.ts — handyman(杂工)真实 DSH 工具(v1.24.0:loop 牛马 → handyman 杂工)
|
|
2481
2514
|
*
|
|
2482
|
-
*
|
|
2483
|
-
*
|
|
2484
|
-
*
|
|
2485
|
-
*
|
|
2486
|
-
*
|
|
2487
|
-
*
|
|
2488
|
-
*
|
|
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
|
|
2496
|
-
*
|
|
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);无超时(
|
|
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
|
-
/**
|
|
2537
|
-
|
|
2538
|
-
|
|
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: "
|
|
2541
|
-
description: "The
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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;
|
|
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:
|
|
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
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
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
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
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
|
-
|
|
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:
|
|
2697
|
-
progress:
|
|
2698
|
-
constraints: "
|
|
2699
|
-
next: done ? "
|
|
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("
|
|
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("
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
|
|
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 (
|
|
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 根 + 约束 +
|
|
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
|
-
* -
|
|
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("
|
|
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
|
|
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:
|
|
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, {
|
|
7611
|
+
sendJson(res, 200, { handymen: [] });
|
|
7517
7612
|
return;
|
|
7518
7613
|
}
|
|
7519
|
-
sendJson(res, 200, {
|
|
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(
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shgroup/dsh-serenity-hooks",
|
|
3
|
-
"version": "1.
|
|
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": {
|