@shgroup/dsh-serenity-hooks 1.24.12 → 1.25.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/dsh.plugin.json +4 -3
- package/lib/index.js +1167 -444
- package/lib/invariant.js +4 -1
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -510,10 +510,10 @@ function runCcFs(root, args) {
|
|
|
510
510
|
* 进程内注册(取代 v0.1 的 bash spawn runner):zod/schemastery 参数校验、
|
|
511
511
|
* 规范 JSON 输出、纯 render 投影。逻辑在 fs-ops.ts(可单测)。
|
|
512
512
|
*/
|
|
513
|
-
function agentCwd$
|
|
513
|
+
function agentCwd$9(exec) {
|
|
514
514
|
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
515
515
|
}
|
|
516
|
-
function renderText$
|
|
516
|
+
function renderText$11(value) {
|
|
517
517
|
return [{
|
|
518
518
|
type: "text",
|
|
519
519
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -585,10 +585,10 @@ const ccFsTool = defineTool({
|
|
|
585
585
|
},
|
|
586
586
|
output: {
|
|
587
587
|
schema: { type: "json" },
|
|
588
|
-
render: (args, value) => renderText$
|
|
588
|
+
render: (args, value) => renderText$11(value)
|
|
589
589
|
},
|
|
590
590
|
async execute(args, exec) {
|
|
591
|
-
const root = findSerenityRoot(agentCwd$
|
|
591
|
+
const root = findSerenityRoot(agentCwd$9(exec));
|
|
592
592
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
593
593
|
return runCcFs(root, args);
|
|
594
594
|
}
|
|
@@ -609,6 +609,109 @@ const ACC_VERSION = (() => {
|
|
|
609
609
|
}
|
|
610
610
|
})();
|
|
611
611
|
//#endregion
|
|
612
|
+
//#region src/skiff-role.ts
|
|
613
|
+
/**
|
|
614
|
+
* skiff-role.ts — Skiff(F4,v1.25.0 实验性)角色层纯逻辑(零 DSH 依赖,可独立单测)
|
|
615
|
+
*
|
|
616
|
+
* 概念(S142 用户拍板):Skiff = 完整宁静号 trajectory(在宁静号内全知全能)的
|
|
617
|
+
* **任意子集**——CCC 通过角色配置定义:能力面(tools 非 MSM 工具白名单 + msms
|
|
618
|
+
* MSM 白名单,双白名单独立,白名单外全隐藏)+ 轨迹纪律面(trajectory 子集)+
|
|
619
|
+
* 系统提示词(CCC 完整定义,dsp 只给基础部分)。
|
|
620
|
+
*
|
|
621
|
+
* 实验性质:未配置任何角色 → Skiff 完全零影响(无监听、无 agent 创建、guard 无规则)。
|
|
622
|
+
*/
|
|
623
|
+
/** Skiff agent 会话 id 前缀(agents.create 生成;seams 旁路/白名单判定用) */
|
|
624
|
+
const SKIFF_SESSION_PREFIX = "skiff-";
|
|
625
|
+
/** 判定 sessionId 是否为 Skiff 会话(仿 handyman- 前缀排除模式) */
|
|
626
|
+
function isSkiffSessionId(sessionId) {
|
|
627
|
+
return typeof sessionId === "string" && sessionId.startsWith("skiff-");
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* 读取 CCC 的 Skiff 角色配置(.opencode/serenity.json skiff.roles)。
|
|
631
|
+
* @returns 名 → 角色配置 的 Map;未配置(无 skiff 段/空 roles)返回空 Map(Skiff 未启用)
|
|
632
|
+
*/
|
|
633
|
+
function readSkiffRoles(root, paths = DEFAULT_SERENITY_CONFIG_PATHS) {
|
|
634
|
+
const out = /* @__PURE__ */ new Map();
|
|
635
|
+
try {
|
|
636
|
+
const roles = loadSerenityConfig(root, paths).skiff?.roles;
|
|
637
|
+
if (!roles || typeof roles !== "object") return out;
|
|
638
|
+
for (const [name, role] of Object.entries(roles)) {
|
|
639
|
+
if (!role || typeof role !== "object") continue;
|
|
640
|
+
if (name.trim() === "") continue;
|
|
641
|
+
out.set(name.trim(), {
|
|
642
|
+
model: typeof role.model === "string" ? role.model : void 0,
|
|
643
|
+
msms: Array.isArray(role.msms) ? role.msms.filter((m) => typeof m === "string") : void 0,
|
|
644
|
+
tools: Array.isArray(role.tools) ? role.tools.filter((t) => typeof t === "string") : void 0,
|
|
645
|
+
trajectory: role.trajectory && typeof role.trajectory === "object" ? {
|
|
646
|
+
session: role.trajectory.session === true,
|
|
647
|
+
keeper: role.trajectory.keeper === true,
|
|
648
|
+
rebuild: role.trajectory.rebuild === true
|
|
649
|
+
} : void 0,
|
|
650
|
+
systemPrompt: typeof role.systemPrompt === "string" ? role.systemPrompt : void 0
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
} catch {}
|
|
654
|
+
return out;
|
|
655
|
+
}
|
|
656
|
+
function trajectorySubset(role) {
|
|
657
|
+
return {
|
|
658
|
+
session: role?.trajectory?.session === true,
|
|
659
|
+
keeper: role?.trajectory?.keeper === true,
|
|
660
|
+
rebuild: role?.trajectory?.rebuild === true
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
/** 角色可用工具面(白名单并集):tools + acc_msm(msms 非空时作为 MSM 通道自动可用) */
|
|
664
|
+
function roleToolWhitelist(role) {
|
|
665
|
+
const out = /* @__PURE__ */ new Set();
|
|
666
|
+
for (const t of role?.tools ?? []) out.add(t);
|
|
667
|
+
if ((role?.msms?.length ?? 0) > 0) out.add("acc_msm");
|
|
668
|
+
return out;
|
|
669
|
+
}
|
|
670
|
+
/** 角色允许的 MSM 白名单(acc_msm exec 校验 / msm_list 过滤用;独立于 tools 白名单) */
|
|
671
|
+
function roleMsmWhitelist(role) {
|
|
672
|
+
return new Set(role?.msms ?? []);
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Skiff 基础提示词(dsp 只给这部分;CCC 的 systemPrompt 段由调用方拼接):
|
|
676
|
+
* 身份 + 可用 MSM/工具清单 + 调用协议 + 边界声明。动态生成(清单来自角色白名单)。
|
|
677
|
+
*/
|
|
678
|
+
function buildSkiffBasePrompt(roleName, role) {
|
|
679
|
+
const msms = role?.msms ?? [];
|
|
680
|
+
const tools = role?.tools ?? [];
|
|
681
|
+
const lines = [
|
|
682
|
+
"=== Serenity Skiff ===",
|
|
683
|
+
`Role: ${roleName} (defined by this CCC)`,
|
|
684
|
+
"You interact with this CCC ONLY through the exposed surface below:"
|
|
685
|
+
];
|
|
686
|
+
if (msms.length > 0) lines.push(` MSMs: ${msms.join(", ")} (call acc_msm exec <name> [args...]; pass --help as the first arg for usage)`);
|
|
687
|
+
else lines.push(" MSMs: (none)");
|
|
688
|
+
lines.push(` Tools: ${tools.length > 0 ? tools.join(", ") : "(none)"}`);
|
|
689
|
+
lines.push("No other tools are available. Your capability boundary is this surface.");
|
|
690
|
+
lines.push("");
|
|
691
|
+
lines.push("---");
|
|
692
|
+
lines.push("");
|
|
693
|
+
return lines.join("\n");
|
|
694
|
+
}
|
|
695
|
+
//#endregion
|
|
696
|
+
//#region src/skiff-registry.ts
|
|
697
|
+
/**
|
|
698
|
+
* skiff-registry.ts — Skiff 会话注册表(sessionId → role;零 DSH 依赖,可被任何 seams 安全 import)
|
|
699
|
+
*
|
|
700
|
+
* F4b 会话映射:Skiff agent 创建时注册(skiff-core),guards/seams 按 sessionId 查角色。
|
|
701
|
+
* 独立成模块的原因:guards.ts 等拦截缝需要查角色而不引入 skiff-core 的运行时依赖
|
|
702
|
+
* (skiff-core 依赖 @deepseek-ai/dsh-llm,测试/装配级联成本高)——注册表本身纯内存 Map。
|
|
703
|
+
*
|
|
704
|
+
* 生命周期:进程内存态;连接/页面关闭时 unregister;进程重启自然清空(skiff 会话不复存在)。
|
|
705
|
+
*/
|
|
706
|
+
const skiffSessions = /* @__PURE__ */ new Map();
|
|
707
|
+
/** 查 sessionId 的 Skiff 角色名(无 → null) */
|
|
708
|
+
function skiffRoleFor$1(sessionId) {
|
|
709
|
+
return skiffSessions.get(sessionId) ?? null;
|
|
710
|
+
}
|
|
711
|
+
function registerSkiffSession$1(sessionId, role) {
|
|
712
|
+
skiffSessions.set(sessionId, role);
|
|
713
|
+
}
|
|
714
|
+
//#endregion
|
|
612
715
|
//#region src/seams/guards.ts
|
|
613
716
|
/**
|
|
614
717
|
* guards.ts — 拦截缝:安全模式 + 路径守卫(P3 语义的机械层)
|
|
@@ -652,6 +755,13 @@ function isWriteTool(toolName, action) {
|
|
|
652
755
|
*/
|
|
653
756
|
function decideGuard(input) {
|
|
654
757
|
const { root, toolName, safeModeOn, blacklist, pathArg, action } = input;
|
|
758
|
+
if (input.skiffSessionId !== void 0 && isSkiffSessionId(input.skiffSessionId)) {
|
|
759
|
+
const roleName = input.skiffSessionId ? skiffRoleFor$1(input.skiffSessionId) : null;
|
|
760
|
+
if (!roleToolWhitelist(roleName ? readSkiffRoles(root).get(roleName) : void 0).has(toolName)) return {
|
|
761
|
+
deny: "tool not allowed in this skiff role",
|
|
762
|
+
kind: "deny"
|
|
763
|
+
};
|
|
764
|
+
}
|
|
655
765
|
if (safeModeOn && toolName === "bash") return {
|
|
656
766
|
deny: `bash: no such tool`,
|
|
657
767
|
kind: "deny"
|
|
@@ -783,13 +893,15 @@ function registerGuards(ctx, opts = {}) {
|
|
|
783
893
|
const blacklist = readBlacklist(root, configPaths);
|
|
784
894
|
const pathArg = extractPathArg(exec);
|
|
785
895
|
const action = extractAction(exec);
|
|
896
|
+
const sessionId = exec?.agent?.session?.id;
|
|
786
897
|
return decideGuard({
|
|
787
898
|
root,
|
|
788
899
|
toolName: exec.name,
|
|
789
900
|
safeModeOn,
|
|
790
901
|
blacklist,
|
|
791
902
|
pathArg,
|
|
792
|
-
action
|
|
903
|
+
action,
|
|
904
|
+
skiffSessionId: sessionId
|
|
793
905
|
});
|
|
794
906
|
};
|
|
795
907
|
ctx.on("tools/pre-execute", async (exec, next) => {
|
|
@@ -956,10 +1068,10 @@ async function runKit(root, args) {
|
|
|
956
1068
|
/**
|
|
957
1069
|
* kit.ts — acc_kit 真实 DSH 工具定义(defineTool)
|
|
958
1070
|
*/
|
|
959
|
-
function agentCwd$
|
|
1071
|
+
function agentCwd$8(exec) {
|
|
960
1072
|
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
961
1073
|
}
|
|
962
|
-
function renderText$
|
|
1074
|
+
function renderText$10(value) {
|
|
963
1075
|
return [{
|
|
964
1076
|
type: "text",
|
|
965
1077
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -982,10 +1094,10 @@ const kitTool = defineTool({
|
|
|
982
1094
|
},
|
|
983
1095
|
output: {
|
|
984
1096
|
schema: { type: "json" },
|
|
985
|
-
render: (args, value) => renderText$
|
|
1097
|
+
render: (args, value) => renderText$10(value)
|
|
986
1098
|
},
|
|
987
1099
|
async execute(args, exec) {
|
|
988
|
-
return await runKit(findSerenityRoot(agentCwd$
|
|
1100
|
+
return await runKit(findSerenityRoot(agentCwd$8(exec)), args);
|
|
989
1101
|
}
|
|
990
1102
|
});
|
|
991
1103
|
//#endregion
|
|
@@ -1445,10 +1557,10 @@ function runGit(root, args) {
|
|
|
1445
1557
|
/**
|
|
1446
1558
|
* git.ts — cc_git 真实 DSH 工具定义(defineTool)
|
|
1447
1559
|
*/
|
|
1448
|
-
function agentCwd$
|
|
1560
|
+
function agentCwd$7(exec) {
|
|
1449
1561
|
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
1450
1562
|
}
|
|
1451
|
-
function renderText$
|
|
1563
|
+
function renderText$9(value) {
|
|
1452
1564
|
return [{
|
|
1453
1565
|
type: "text",
|
|
1454
1566
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -1487,10 +1599,10 @@ const gitTool = defineTool({
|
|
|
1487
1599
|
},
|
|
1488
1600
|
output: {
|
|
1489
1601
|
schema: { type: "json" },
|
|
1490
|
-
render: (args, value) => renderText$
|
|
1602
|
+
render: (args, value) => renderText$9(value)
|
|
1491
1603
|
},
|
|
1492
1604
|
async execute(args, exec) {
|
|
1493
|
-
const root = findSerenityRoot(agentCwd$
|
|
1605
|
+
const root = findSerenityRoot(agentCwd$7(exec));
|
|
1494
1606
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
1495
1607
|
return runGit(root, args);
|
|
1496
1608
|
}
|
|
@@ -2055,118 +2167,482 @@ async function runMsmAsync(root, args) {
|
|
|
2055
2167
|
}
|
|
2056
2168
|
}
|
|
2057
2169
|
//#endregion
|
|
2058
|
-
//#region src/
|
|
2170
|
+
//#region src/handyman-ops.ts
|
|
2059
2171
|
/**
|
|
2060
|
-
*
|
|
2172
|
+
* handyman-ops.ts — handyman(杂工)纯逻辑层(零 DSH 依赖,可独立单测)
|
|
2173
|
+
*
|
|
2174
|
+
* v1.24.0:loop(牛马)→ handyman(杂工)重命名。语义对齐 osp loop:
|
|
2175
|
+
* 进度文件(handyman-<label>.md/.json)、续跑、轮次 prompt 结构、stop token。
|
|
2176
|
+
* 不兼容旧 loop- 进度文件(用户拍板:仅新 handyman- 前缀)。
|
|
2061
2177
|
*/
|
|
2062
|
-
|
|
2063
|
-
|
|
2178
|
+
/** label 脱敏(Windows 审计问题 17):非法字符 → '-',去尾点/空格,限长(按码点截断,修复代理对切散 U+FFFD) */
|
|
2179
|
+
function sanitizeLabel(label) {
|
|
2180
|
+
return [...label.replace(/[<>:"/\\|?*\u0000-\u001f]/g, "-").replace(/[ .]+$/g, "")].slice(0, 50).join("");
|
|
2064
2181
|
}
|
|
2065
|
-
function
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2182
|
+
function handymanProgressPaths(root, label) {
|
|
2183
|
+
const dir = join(root, "AGENT_SESSIONS");
|
|
2184
|
+
const safe = sanitizeLabel(label);
|
|
2185
|
+
return {
|
|
2186
|
+
md: join(dir, `handyman-${safe}.md`),
|
|
2187
|
+
json: join(dir, `handyman-${safe}.json`)
|
|
2188
|
+
};
|
|
2070
2189
|
}
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
description: "Subcommand: list/exec/register/deregister/check/guide/ccc-config"
|
|
2080
|
-
},
|
|
2081
|
-
name: {
|
|
2082
|
-
type: "string",
|
|
2083
|
-
description: "MSM name (exec/register/deregister)"
|
|
2084
|
-
},
|
|
2085
|
-
args: {
|
|
2086
|
-
type: "array",
|
|
2087
|
-
items: { type: "string" },
|
|
2088
|
-
description: "exec business args (first arg --list/--schema <n>/--format=json is protocol flag, rest passed through losslessly)"
|
|
2089
|
-
},
|
|
2090
|
-
skill: {
|
|
2091
|
-
type: "string",
|
|
2092
|
-
description: "register owning skill"
|
|
2093
|
-
},
|
|
2094
|
-
path: {
|
|
2095
|
-
type: "string",
|
|
2096
|
-
description: "register script relative path (must be inside root and exist)"
|
|
2097
|
-
},
|
|
2098
|
-
category: {
|
|
2099
|
-
type: "string",
|
|
2100
|
-
description: "register category (mech/semi-mech)"
|
|
2101
|
-
},
|
|
2102
|
-
description: {
|
|
2103
|
-
type: "string",
|
|
2104
|
-
description: "register description"
|
|
2105
|
-
},
|
|
2106
|
-
flags: {
|
|
2107
|
-
type: "string",
|
|
2108
|
-
description: "register flags JSON string (new-style object array; type:\"path\" enables escape validation)"
|
|
2109
|
-
},
|
|
2110
|
-
usage: {
|
|
2111
|
-
type: "string",
|
|
2112
|
-
description: "register custom usage (auto-generated by default)"
|
|
2113
|
-
}
|
|
2114
|
-
},
|
|
2115
|
-
output: {
|
|
2116
|
-
schema: { type: "json" },
|
|
2117
|
-
render: (args, value) => renderText$7(value)
|
|
2118
|
-
},
|
|
2119
|
-
async execute(args, exec) {
|
|
2120
|
-
const root = findSerenityRoot(agentCwd$5(exec));
|
|
2121
|
-
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
2122
|
-
return runMsmAsync(root, args);
|
|
2190
|
+
/** 读取进度(续跑);无文件返回 round 0 */
|
|
2191
|
+
function readProgress(root, label) {
|
|
2192
|
+
const { json } = handymanProgressPaths(root, label);
|
|
2193
|
+
if (!existsSync(json)) return null;
|
|
2194
|
+
try {
|
|
2195
|
+
return JSON.parse(readFileSync(json, "utf-8"));
|
|
2196
|
+
} catch {
|
|
2197
|
+
return null;
|
|
2123
2198
|
}
|
|
2124
|
-
}
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
*/
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2199
|
+
}
|
|
2200
|
+
function writeProgress(root, label, p) {
|
|
2201
|
+
const { md, json } = handymanProgressPaths(root, label);
|
|
2202
|
+
mkdirSync(join(root, "AGENT_SESSIONS"), { recursive: true });
|
|
2203
|
+
writeFileSync(json, JSON.stringify({
|
|
2204
|
+
...p,
|
|
2205
|
+
status: p.status ?? "running",
|
|
2206
|
+
updated: (/* @__PURE__ */ new Date()).toISOString()
|
|
2207
|
+
}, null, 2) + "\n", "utf-8");
|
|
2208
|
+
const lines = [
|
|
2209
|
+
`# handyman: ${label}`,
|
|
2210
|
+
`- Model: ${p.model}`,
|
|
2211
|
+
`- Round: ${p.round}`,
|
|
2212
|
+
`- Done: ${p.done}`,
|
|
2213
|
+
"",
|
|
2214
|
+
`## Latest response`,
|
|
2215
|
+
"",
|
|
2216
|
+
p.lastResponse,
|
|
2217
|
+
""
|
|
2218
|
+
];
|
|
2219
|
+
writeFileSync(md, lines.join("\n"), "utf-8");
|
|
2220
|
+
}
|
|
2221
|
+
/** 失败状态落盘(对齐 osp writeFailedStatus:done=true / status=failed / errorCode) */
|
|
2222
|
+
function writeFailedStatus(root, label, info) {
|
|
2223
|
+
const { json } = handymanProgressPaths(root, label);
|
|
2224
|
+
mkdirSync(join(root, "AGENT_SESSIONS"), { recursive: true });
|
|
2225
|
+
const prev = readProgress(root, label);
|
|
2226
|
+
writeFileSync(json, JSON.stringify({
|
|
2227
|
+
round: prev?.round ?? 0,
|
|
2228
|
+
done: true,
|
|
2229
|
+
label,
|
|
2230
|
+
model: prev?.model ?? "",
|
|
2231
|
+
status: "failed",
|
|
2232
|
+
errorCode: info.errorCode,
|
|
2233
|
+
errorMessage: info.errorMessage,
|
|
2234
|
+
updated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2235
|
+
lastResponse: prev?.lastResponse ?? ""
|
|
2236
|
+
}, null, 2) + "\n", "utf-8");
|
|
2237
|
+
}
|
|
2238
|
+
function newStopToken() {
|
|
2239
|
+
return `SERENITY_HANDYMAN_DONE_${randomBytes(8).toString("hex")}`;
|
|
2240
|
+
}
|
|
2241
|
+
/** 解析 model 字符串(provider/model)→ {provider, model};无 / 视为 model-only */
|
|
2242
|
+
function splitModel(model) {
|
|
2243
|
+
const idx = model.indexOf("/");
|
|
2244
|
+
if (idx < 0) return {
|
|
2245
|
+
provider: void 0,
|
|
2246
|
+
model
|
|
2247
|
+
};
|
|
2248
|
+
return {
|
|
2249
|
+
provider: model.slice(0, idx),
|
|
2250
|
+
model: model.slice(idx + 1)
|
|
2251
|
+
};
|
|
2252
|
+
}
|
|
2253
|
+
/** 校验模型在白名单内;不在 → 抛错(用户拍板:只能使用 CCC 配置的模型) */
|
|
2254
|
+
function requireWhitelistedModel(model, models) {
|
|
2255
|
+
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(", ")}`);
|
|
2256
|
+
}
|
|
2257
|
+
/** 轮次 prompt(对齐老 loop 结构:回顾进度 → 自由工作 → 汇报;S134 EAP 化:固定详尽) */
|
|
2258
|
+
function buildRoundPrompt(opts) {
|
|
2259
|
+
const { root, session, label, round, stopToken, progress, task } = opts;
|
|
2260
|
+
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.";
|
|
2261
|
+
return `# ${label} — handyman round ${round}
|
|
2135
2262
|
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
| R↓ Reconstructability | Cost of rebuilding the original reasoning later | Record decision rationale, context, constraints, alternatives |
|
|
2141
|
-
| S↑ Stability | Degree to which the same input repeatedly produces consistent output | Fix structures, protocolize, avoid relying on implicit context |
|
|
2263
|
+
CCC root: ${root}
|
|
2264
|
+
${session ? `Work session: ${session} (progress recorded in AGENT_SESSIONS/${session}/SESSION.md)` : ""}
|
|
2265
|
+
${task ? `Task: ${task}` : `Task: follow the work corresponding to label "${label}" (if a work session exists, read SESSION.md first to clarify the goal)`}
|
|
2266
|
+
${resumeNote}
|
|
2142
2267
|
|
|
2143
|
-
##
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
-
|
|
2148
|
-
-
|
|
2149
|
-
-
|
|
2150
|
-
|
|
2268
|
+
## Work rules (fixed every round, must follow)
|
|
2269
|
+
1. Work freely within this round: read files, modify code, execute commands — use every means to advance the task.
|
|
2270
|
+
2. If this task is **reading/curating or text-writing work** (extracting from files, summarizing, writing docs, generating text, etc.),
|
|
2271
|
+
first load eap (acc-eap skill) and organize output per the EAP standard:
|
|
2272
|
+
- E↑ Explicit: entities/variables clearly defined, relationships with direction and cardinality, boundaries drawn, no ambiguous words
|
|
2273
|
+
- R↓ Reconstructable: key conclusions record sources and reasoning, rebuildable by later agents
|
|
2274
|
+
- S↑ Stable: output structure regenerates repeatably, no reliance on implicit context
|
|
2275
|
+
3. Reports must be concrete and verifiable — no filler.
|
|
2151
2276
|
|
|
2152
|
-
##
|
|
2153
|
-
|
|
2154
|
-
|
|
2277
|
+
## Per-round report (fixed format, answer each item)
|
|
2278
|
+
1. What was done this round (concrete)
|
|
2279
|
+
2. Next-step plan
|
|
2280
|
+
3. Whether the task is complete (if complete, output only ${stopToken})
|
|
2155
2281
|
|
|
2156
|
-
|
|
2157
|
-
https://github.com/tellmewhattodo/theory-eap`;
|
|
2158
|
-
function renderText$6(value) {
|
|
2159
|
-
return [{
|
|
2160
|
-
type: "text",
|
|
2161
|
-
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
2162
|
-
}];
|
|
2282
|
+
If the task is complete, output only ${stopToken}.`;
|
|
2163
2283
|
}
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2284
|
+
/**
|
|
2285
|
+
* handyman 规模化使用指引(guide 子命令输出;S134 继承 + v1.24.0 更新):
|
|
2286
|
+
* 使用 handyman 前必须先加载 eap 设计方案;并行策略(jobs 编排);提示词规范(详尽固定 EAP);
|
|
2287
|
+
* 阅读/文字编写类 handyman 内部也加载 eap。
|
|
2288
|
+
*/
|
|
2289
|
+
const HANDYMAN_GUIDE = `# handyman — Scale-Up Usage Guide (guide)
|
|
2290
|
+
|
|
2291
|
+
## ⚠️ Before using: load eap and design the plan
|
|
2292
|
+
Before calling handyman, load eap (acc-eap skill) and design the "scale-up handyman plan" based on the EAP framework.
|
|
2293
|
+
|
|
2294
|
+
### 1. Task decomposition (E↑ Explicit)
|
|
2295
|
+
- Split large tasks into explicit subtasks: each subtask defines goal / input / boundaries (what to do, what not to do) / acceptance criteria
|
|
2296
|
+
- Make dependencies explicit: dependent tasks run serially, independent ones can run in parallel
|
|
2297
|
+
|
|
2298
|
+
### 2. Prompt design (handyman's task parameter)
|
|
2299
|
+
- task must be detailed, fixed, and EAP-compliant: clear goal, drawn boundaries, decidable acceptance criteria
|
|
2300
|
+
- Anti-example "handle this file" — ambiguous; good example "read <path>, extract all rows of the「关键决策」table,
|
|
2301
|
+
output a JSON array (fields id/conclusion/evidence), do not modify the original file"
|
|
2302
|
+
- Reading/curating or text-writing work (extracting from files, summarizing, writing docs, generating text, etc.):
|
|
2303
|
+
the handyman-internal agent is also required to load eap and organize output per the EAP standard
|
|
2304
|
+
|
|
2305
|
+
### 3. Model whitelist (CCC-configured, mandatory)
|
|
2306
|
+
- handyman only uses models listed in .opencode/serenity.json "handyman.models" — never arbitrary models
|
|
2307
|
+
- Recursive subagents inside a handyman inherit the handyman's model automatically (DSH native)
|
|
2308
|
+
- Keep the subagent tool instance free of a fixed agentOptions, or model inheritance breaks
|
|
2309
|
+
|
|
2310
|
+
### 4. Parallel strategy (jobs orchestration, workflow capability)
|
|
2311
|
+
- Independent subtasks can run in parallel via handyman(jobs=[...]): each job gets its own label + task + stop token + progress file
|
|
2312
|
+
- Concurrency safety guaranteed: unique sessionId (handyman-<label>-<uuid>), progress files isolated per label
|
|
2313
|
+
(AGENT_SESSIONS/handyman-<label>.json) — same label resumes, different labels never interfere
|
|
2314
|
+
- Parallel cap: handyman.maxParallel (default 10 — cheap models are cheap)
|
|
2315
|
+
- Aggregation: after each parallel job produces progress, the main agent merges (or spawns one aggregation handyman)
|
|
2316
|
+
- For programmable pipeline/phase orchestration at scale, use the platform's workflow tool instead
|
|
2317
|
+
|
|
2318
|
+
## Completion criteria (osp loop standard)
|
|
2319
|
+
- 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)
|
|
2320
|
+
- Automatic restart on abnormal agent stop (≤100 restarts, anti-infinite-loop)
|
|
2321
|
+
|
|
2322
|
+
## Waiting UI
|
|
2323
|
+
- The WebUI session-header Serenity detail card shows running handymen's progress (label / round / last response), one line per parallel job, ~3s refresh
|
|
2324
|
+
`;
|
|
2325
|
+
/** 列出 AGENT_SESSIONS/handyman-*.json 的全部进度(按 updated 倒序;坏文件跳过) */
|
|
2326
|
+
function listActiveHandymen(root) {
|
|
2327
|
+
const dir = join(root, "AGENT_SESSIONS");
|
|
2328
|
+
if (!existsSync(dir)) return [];
|
|
2329
|
+
const out = [];
|
|
2330
|
+
for (const entry of readdirSync(dir)) {
|
|
2331
|
+
if (!entry.startsWith("handyman-") || !entry.endsWith(".json")) continue;
|
|
2332
|
+
try {
|
|
2333
|
+
const data = JSON.parse(readFileSync(join(dir, entry), "utf-8"));
|
|
2334
|
+
if (typeof data.label !== "string" || typeof data.round !== "number") continue;
|
|
2335
|
+
out.push({
|
|
2336
|
+
label: data.label,
|
|
2337
|
+
round: data.round,
|
|
2338
|
+
done: data.done === true,
|
|
2339
|
+
model: typeof data.model === "string" ? data.model : "",
|
|
2340
|
+
updated: typeof data.updated === "string" ? data.updated : "",
|
|
2341
|
+
lastResponse: typeof data.lastResponse === "string" ? data.lastResponse : ""
|
|
2342
|
+
});
|
|
2343
|
+
} catch {}
|
|
2344
|
+
}
|
|
2345
|
+
out.sort((a, b) => a.updated < b.updated ? 1 : -1);
|
|
2346
|
+
return out;
|
|
2347
|
+
}
|
|
2348
|
+
//#endregion
|
|
2349
|
+
//#region src/skiff-core.ts
|
|
2350
|
+
const PLUGIN_SOURCE$3 = {
|
|
2351
|
+
kind: "plugin",
|
|
2352
|
+
plugin: "dsh-serenity-hooks"
|
|
2353
|
+
};
|
|
2354
|
+
const skiffAgents = /* @__PURE__ */ new Map();
|
|
2355
|
+
/** 查 sessionId 的 Skiff 角色名(无 → null) */
|
|
2356
|
+
function skiffRoleFor(sessionId) {
|
|
2357
|
+
return skiffRoleFor$1(sessionId);
|
|
2358
|
+
}
|
|
2359
|
+
function registerSkiffSession(sessionId, role, agent) {
|
|
2360
|
+
skiffAgents.set(sessionId, agent);
|
|
2361
|
+
registerSkiffSession$1(sessionId, role);
|
|
2362
|
+
}
|
|
2363
|
+
/**
|
|
2364
|
+
* 创建 Skiff agent:标准 DSH agent + cwd=CCC root + 角色模型 +
|
|
2365
|
+
* scoped 系统提示词(基础提示词 + CCC 定义段,全替换 ACC 默认注入)。
|
|
2366
|
+
*/
|
|
2367
|
+
async function createSkiffAgent(ctx, root, roleName, role, defaultModel) {
|
|
2368
|
+
if (!ctx.agents) throw new Error("skiff: ctx.agents unavailable");
|
|
2369
|
+
const model = role.model?.trim() || defaultModel || "";
|
|
2370
|
+
const sessionId = `${SKIFF_SESSION_PREFIX}${roleName}-${randomUUID()}`;
|
|
2371
|
+
const handle = await ctx.agents.create({
|
|
2372
|
+
sessionId,
|
|
2373
|
+
meta: { cwd: root },
|
|
2374
|
+
...model ? { agentOptions: splitModel(model) } : {}
|
|
2375
|
+
});
|
|
2376
|
+
const agent = handle.agent;
|
|
2377
|
+
try {
|
|
2378
|
+
agent.ctx.systemPrompt.section({
|
|
2379
|
+
name: "serenity-skiff",
|
|
2380
|
+
order: -60,
|
|
2381
|
+
text: () => [buildSkiffBasePrompt(roleName, role), role.systemPrompt ?? ""].filter(Boolean).join("\n")
|
|
2382
|
+
});
|
|
2383
|
+
} catch (err) {
|
|
2384
|
+
console.warn(`[serenity-hooks] skiff 系统提示词注册失败: ${String(err?.message ?? err)}`);
|
|
2385
|
+
}
|
|
2386
|
+
registerSkiffSession(sessionId, roleName, agent);
|
|
2387
|
+
return {
|
|
2388
|
+
handle,
|
|
2389
|
+
agent,
|
|
2390
|
+
sessionId
|
|
2391
|
+
};
|
|
2392
|
+
}
|
|
2393
|
+
/** 等待 agent 空闲(agent/status → idle);无超时(agent 工作多久等多久,handyman 同款) */
|
|
2394
|
+
function waitIdle$1(ctx, agent) {
|
|
2395
|
+
return new Promise((resolve) => {
|
|
2396
|
+
let settled = false;
|
|
2397
|
+
let dispose = () => {};
|
|
2398
|
+
const finish = () => {
|
|
2399
|
+
if (settled) return;
|
|
2400
|
+
settled = true;
|
|
2401
|
+
dispose();
|
|
2402
|
+
resolve();
|
|
2403
|
+
};
|
|
2404
|
+
dispose = ctx.on("agent/status", (payload) => {
|
|
2405
|
+
if (payload.agent === agent && payload.status === "idle") finish();
|
|
2406
|
+
});
|
|
2407
|
+
});
|
|
2408
|
+
}
|
|
2409
|
+
/** 读会话最后一个 assistant/message 文本(handyman 同款) */
|
|
2410
|
+
function lastAssistantText$1(agent) {
|
|
2411
|
+
const events = agent.session.events;
|
|
2412
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
2413
|
+
const e = events[i];
|
|
2414
|
+
if (e && e.type === "assistant/message") {
|
|
2415
|
+
const text = (e.data?.message?.content ?? e.data?.content ?? []).filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
|
|
2416
|
+
if (text) return text;
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
return "";
|
|
2420
|
+
}
|
|
2421
|
+
/** events → 可读轨迹(user/assistant 文本 + 工具调用 + 工具结果;单条解析失败跳过) */
|
|
2422
|
+
function eventsToTrajectory(events) {
|
|
2423
|
+
const out = [];
|
|
2424
|
+
for (const raw of events) try {
|
|
2425
|
+
const ev = raw;
|
|
2426
|
+
if (ev.type === "user/message") {
|
|
2427
|
+
const text = extractText(ev.data);
|
|
2428
|
+
if (text) out.push({
|
|
2429
|
+
role: "user",
|
|
2430
|
+
text
|
|
2431
|
+
});
|
|
2432
|
+
} else if (ev.type === "assistant/message") {
|
|
2433
|
+
const d = ev.data;
|
|
2434
|
+
const text = (d?.message?.content ?? d?.content ?? []).filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
|
|
2435
|
+
const calls = d?.message?.tool_calls ?? [];
|
|
2436
|
+
if (text) out.push({
|
|
2437
|
+
role: "assistant",
|
|
2438
|
+
text
|
|
2439
|
+
});
|
|
2440
|
+
for (const c of calls ?? []) {
|
|
2441
|
+
const args = typeof c.arguments === "string" ? c.arguments : JSON.stringify(c.arguments ?? {});
|
|
2442
|
+
out.push({
|
|
2443
|
+
role: "assistant",
|
|
2444
|
+
text: `→ ${c.name ?? "(tool)"} ${truncate(args, 300)}`,
|
|
2445
|
+
tool: c.name
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2448
|
+
} else if (ev.type === "tool/result") {
|
|
2449
|
+
const outText = extractText(ev.data);
|
|
2450
|
+
if (outText) out.push({
|
|
2451
|
+
role: "tool",
|
|
2452
|
+
text: truncate(outText, 500),
|
|
2453
|
+
tool: String(ev.data?.name ?? "")
|
|
2454
|
+
});
|
|
2455
|
+
}
|
|
2456
|
+
} catch {}
|
|
2457
|
+
return out;
|
|
2458
|
+
}
|
|
2459
|
+
function extractText(data) {
|
|
2460
|
+
const content = data?.content;
|
|
2461
|
+
if (!Array.isArray(content)) return "";
|
|
2462
|
+
return content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
|
|
2463
|
+
}
|
|
2464
|
+
function truncate(s, n) {
|
|
2465
|
+
return s.length > n ? `${s.slice(0, n)}…` : s;
|
|
2466
|
+
}
|
|
2467
|
+
/**
|
|
2468
|
+
* 提问一轮:followup → 等 idle → 读答案 + 本 turn 轨迹(增量 = followup 前 events 之后)。
|
|
2469
|
+
* @param eventsStart 本轮开始前的 events 长度(0 = 全量轨迹)
|
|
2470
|
+
*/
|
|
2471
|
+
async function askSkiff(ctx, agent, question, eventsStart = 0) {
|
|
2472
|
+
const before = eventsStart > 0 ? eventsStart : agent.session.events.length;
|
|
2473
|
+
agent.followup(createUserMessage({
|
|
2474
|
+
content: [{
|
|
2475
|
+
type: "text",
|
|
2476
|
+
text: question
|
|
2477
|
+
}],
|
|
2478
|
+
source: PLUGIN_SOURCE$3
|
|
2479
|
+
}));
|
|
2480
|
+
await waitIdle$1(ctx, agent);
|
|
2481
|
+
const answer = lastAssistantText$1(agent);
|
|
2482
|
+
const trajectory = eventsToTrajectory(agent.session.events.slice(before));
|
|
2483
|
+
return {
|
|
2484
|
+
answer,
|
|
2485
|
+
sessionId: String(agent.session.id ?? ""),
|
|
2486
|
+
trajectory
|
|
2487
|
+
};
|
|
2488
|
+
}
|
|
2489
|
+
/**
|
|
2490
|
+
* Skiff 会话的轨迹纪律参与判定:非 skiff 会话恒 true(正常参与);
|
|
2491
|
+
* skiff 会话按角色 trajectory 子集(session/keeper/rebuild)决定;
|
|
2492
|
+
* 注册表缺失(进程重启遗留等)→ 保守旁路(false,完全独立)。
|
|
2493
|
+
*/
|
|
2494
|
+
function skiffTrajectoryEnabled(root, sessionId, key) {
|
|
2495
|
+
if (!isSkiffSessionId(sessionId)) return true;
|
|
2496
|
+
const roleName = sessionId ? skiffRoleFor(sessionId) : null;
|
|
2497
|
+
if (!roleName) return false;
|
|
2498
|
+
return trajectorySubset(readSkiffRoles(root).get(roleName))[key];
|
|
2499
|
+
}
|
|
2500
|
+
/**
|
|
2501
|
+
* acc_msm 的 Skiff 门控:非 skiff 会话恒放行;skiff 会话——
|
|
2502
|
+
* exec 非白名单 MSM 拒绝(不列名单)、register/deregister 必拒、
|
|
2503
|
+
* list 白名单过滤、check/guide/ccc-config 只读放行。
|
|
2504
|
+
*/
|
|
2505
|
+
function skiffMsmGate(root, sessionId, action, name) {
|
|
2506
|
+
if (!isSkiffSessionId(sessionId)) return {};
|
|
2507
|
+
const roleName = sessionId ? skiffRoleFor(sessionId) : null;
|
|
2508
|
+
const role = roleName ? readSkiffRoles(root).get(roleName) : void 0;
|
|
2509
|
+
if (!roleName || !role) return { reject: "MSM not allowed in this skiff session" };
|
|
2510
|
+
if (action === "register" || action === "deregister") return { reject: "register/deregister is not allowed in skiff sessions" };
|
|
2511
|
+
if (action === "exec") {
|
|
2512
|
+
if (!name || !(role.msms ?? []).includes(name)) return { reject: "MSM not allowed" };
|
|
2513
|
+
return {};
|
|
2514
|
+
}
|
|
2515
|
+
if (action === "list") return { whitelist: roleMsmWhitelist(role) };
|
|
2516
|
+
return {};
|
|
2517
|
+
}
|
|
2518
|
+
//#endregion
|
|
2519
|
+
//#region src/tools/msm.ts
|
|
2520
|
+
/**
|
|
2521
|
+
* msm.ts — acc_msm 真实 DSH 工具定义(defineTool)
|
|
2522
|
+
*/
|
|
2523
|
+
function agentCwd$6(exec) {
|
|
2524
|
+
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
2525
|
+
}
|
|
2526
|
+
function agentSessionId$1(exec) {
|
|
2527
|
+
return exec.agent?.session?.id ?? "";
|
|
2528
|
+
}
|
|
2529
|
+
function renderText$8(value) {
|
|
2530
|
+
return [{
|
|
2531
|
+
type: "text",
|
|
2532
|
+
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
2533
|
+
}];
|
|
2534
|
+
}
|
|
2535
|
+
const msmTool = defineTool({
|
|
2536
|
+
name: "acc_msm",
|
|
2537
|
+
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.",
|
|
2538
|
+
parameters: {
|
|
2539
|
+
action: {
|
|
2540
|
+
type: "string",
|
|
2541
|
+
enum: [...MSM_ACTIONS],
|
|
2542
|
+
required: true,
|
|
2543
|
+
description: "Subcommand: list/exec/register/deregister/check/guide/ccc-config"
|
|
2544
|
+
},
|
|
2545
|
+
name: {
|
|
2546
|
+
type: "string",
|
|
2547
|
+
description: "MSM name (exec/register/deregister)"
|
|
2548
|
+
},
|
|
2549
|
+
args: {
|
|
2550
|
+
type: "array",
|
|
2551
|
+
items: { type: "string" },
|
|
2552
|
+
description: "exec business args (first arg --list/--schema <n>/--format=json is protocol flag, rest passed through losslessly)"
|
|
2553
|
+
},
|
|
2554
|
+
skill: {
|
|
2555
|
+
type: "string",
|
|
2556
|
+
description: "register owning skill"
|
|
2557
|
+
},
|
|
2558
|
+
path: {
|
|
2559
|
+
type: "string",
|
|
2560
|
+
description: "register script relative path (must be inside root and exist)"
|
|
2561
|
+
},
|
|
2562
|
+
category: {
|
|
2563
|
+
type: "string",
|
|
2564
|
+
description: "register category (mech/semi-mech)"
|
|
2565
|
+
},
|
|
2566
|
+
description: {
|
|
2567
|
+
type: "string",
|
|
2568
|
+
description: "register description"
|
|
2569
|
+
},
|
|
2570
|
+
flags: {
|
|
2571
|
+
type: "string",
|
|
2572
|
+
description: "register flags JSON string (new-style object array; type:\"path\" enables escape validation)"
|
|
2573
|
+
},
|
|
2574
|
+
usage: {
|
|
2575
|
+
type: "string",
|
|
2576
|
+
description: "register custom usage (auto-generated by default)"
|
|
2577
|
+
}
|
|
2578
|
+
},
|
|
2579
|
+
output: {
|
|
2580
|
+
schema: { type: "json" },
|
|
2581
|
+
render: (args, value) => renderText$8(value)
|
|
2582
|
+
},
|
|
2583
|
+
async execute(args, exec) {
|
|
2584
|
+
const root = findSerenityRoot(agentCwd$6(exec));
|
|
2585
|
+
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
2586
|
+
const gate = skiffMsmGate(root, agentSessionId$1(exec), args.action, args.name);
|
|
2587
|
+
if (gate.reject) throw new Error(gate.reject);
|
|
2588
|
+
if (args.action === "list" && gate.whitelist) {
|
|
2589
|
+
const out = runMsm(root, args);
|
|
2590
|
+
const text = typeof out === "string" ? out : JSON.stringify(out);
|
|
2591
|
+
const header = text.split("\n")[0] ?? "";
|
|
2592
|
+
const lines = text.split("\n").slice(1).filter((line) => {
|
|
2593
|
+
const name = line.split(" | ")[0]?.trim();
|
|
2594
|
+
return name !== void 0 && name !== "" && gate.whitelist.has(name);
|
|
2595
|
+
});
|
|
2596
|
+
return `${header}\n${lines.length > 0 ? lines.join("\n") : "(no MSM allowed in this role)"}`;
|
|
2597
|
+
}
|
|
2598
|
+
return runMsmAsync(root, args);
|
|
2599
|
+
}
|
|
2600
|
+
});
|
|
2601
|
+
//#endregion
|
|
2602
|
+
//#region src/tools/eap.ts
|
|
2603
|
+
/**
|
|
2604
|
+
* eap.ts — EAP 认知质量框架工具(渐进式披露,ACC 标准工具化)
|
|
2605
|
+
*
|
|
2606
|
+
* 内嵌框架内容(自包含,不依赖已安装技能);可选 section 参数聚焦某原则。
|
|
2607
|
+
*/
|
|
2608
|
+
const EAP_CONTENT = `# EAP Cognitive Quality Framework (Explicit Abstraction Principle)
|
|
2609
|
+
|
|
2610
|
+
> "The functional value of a thought is proportional to its external reconstructability."
|
|
2611
|
+
|
|
2612
|
+
## Three Variables
|
|
2613
|
+
| Variable | Meaning | How to improve |
|
|
2614
|
+
|----------|---------|----------------|
|
|
2615
|
+
| E↑ Explicitness | Degree to which variables/entities/relations are clearly defined | Define variables, state relationship direction & cardinality, draw boundaries |
|
|
2616
|
+
| R↓ Reconstructability | Cost of rebuilding the original reasoning later | Record decision rationale, context, constraints, alternatives |
|
|
2617
|
+
| S↑ Stability | Degree to which the same input repeatedly produces consistent output | Fix structures, protocolize, avoid relying on implicit context |
|
|
2618
|
+
|
|
2619
|
+
## Pre-Output Self-Check Checklist
|
|
2620
|
+
- [ ] Variables/entities clearly defined (E↑)
|
|
2621
|
+
- [ ] Relationships state direction/cardinality (E↑)
|
|
2622
|
+
- [ ] Boundaries drawn — what is in scope / what is not (E↑)
|
|
2623
|
+
- [ ] No ambiguous words: "handle" "optimize" "problem" → be specific (E↑)
|
|
2624
|
+
- [ ] Key decisions record rationale and alternatives (R↓)
|
|
2625
|
+
- [ ] No level-skipping — align the upper layer before descending (R↓)
|
|
2626
|
+
- [ ] Structures can be regenerated repeatably (S↑)
|
|
2627
|
+
|
|
2628
|
+
## Relationship with ACC
|
|
2629
|
+
ACC (plugin/template) encodes structure as code (E↑); generating a CCC from ACC is deterministic (R↓); consistent across multiple CCCs (S↑).
|
|
2630
|
+
CCC (home-serenity etc.) encodes cognitive content as skills/SESSIONs/design docs. Use this checklist to self-check outputs.
|
|
2631
|
+
|
|
2632
|
+
## Reference
|
|
2633
|
+
https://github.com/tellmewhattodo/theory-eap`;
|
|
2634
|
+
function renderText$7(value) {
|
|
2635
|
+
return [{
|
|
2636
|
+
type: "text",
|
|
2637
|
+
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
2638
|
+
}];
|
|
2639
|
+
}
|
|
2640
|
+
const eapTool = defineTool({
|
|
2641
|
+
name: "eap",
|
|
2642
|
+
description: "EAP cognitive quality framework (progressive disclosure): defines E↑ explicitness / R↓ reconstructability / S↑ stability + pre-output self-check checklist. No section returns the full framework; specify a section to focus.",
|
|
2643
|
+
parameters: { section: {
|
|
2644
|
+
type: "string",
|
|
2645
|
+
enum: [
|
|
2170
2646
|
"variables",
|
|
2171
2647
|
"checklist",
|
|
2172
2648
|
"acc"
|
|
@@ -2175,7 +2651,7 @@ const eapTool = defineTool({
|
|
|
2175
2651
|
} },
|
|
2176
2652
|
output: {
|
|
2177
2653
|
schema: { type: "string" },
|
|
2178
|
-
render: (_args, value) => renderText$
|
|
2654
|
+
render: (_args, value) => renderText$7(value)
|
|
2179
2655
|
},
|
|
2180
2656
|
async execute(args) {
|
|
2181
2657
|
if (args.section === "variables") return EAP_CONTENT.split("## Pre-Output Self-Check Checklist")[0];
|
|
@@ -2216,7 +2692,7 @@ Requirements → Scope → Solution → Interface → Implementation
|
|
|
2216
2692
|
2. Wait for confirmation or correction (small step)
|
|
2217
2693
|
3. After confirmation, record the decision (into session SESSION.md or a design doc)
|
|
2218
2694
|
4. Move to the next decision point`;
|
|
2219
|
-
function renderText$
|
|
2695
|
+
function renderText$6(value) {
|
|
2220
2696
|
return [{
|
|
2221
2697
|
type: "text",
|
|
2222
2698
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -2232,7 +2708,7 @@ const neatTool = defineTool({
|
|
|
2232
2708
|
} },
|
|
2233
2709
|
output: {
|
|
2234
2710
|
schema: { type: "string" },
|
|
2235
|
-
render: (_args, value) => renderText$
|
|
2711
|
+
render: (_args, value) => renderText$6(value)
|
|
2236
2712
|
},
|
|
2237
2713
|
async execute(args) {
|
|
2238
2714
|
if (args.section === "rules") return NEAT_CONTENT.match(/## Four Iron Rules[\s\S]*?(?=## )/)?.[0] ?? NEAT_CONTENT;
|
|
@@ -2274,275 +2750,96 @@ Does not measure total entropy (not operational); it measures only the **excess
|
|
|
2274
2750
|
> H_op(C, t) = cost(task | C, t) − cost(task | ideal)
|
|
2275
2751
|
Continuity condition: **H_op(C, t) ≤ H_critical** — agents can still complete tasks at reasonable cost
|
|
2276
2752
|
|
|
2277
|
-
## Continuity Maintenance Condition
|
|
2278
|
-
> **ΔH_org ≥ ΔH_in** — organization must at minimum keep pace with accumulation
|
|
2279
|
-
|
|
2280
|
-
## Six-Phase Lifecycle
|
|
2281
|
-
Experience → Accumulation → Organization → Abstraction → Reconstruction → Evolution →(loop)
|
|
2282
|
-
| Phase | Engineering Concern |
|
|
2283
|
-
|-------|---------------------|
|
|
2284
|
-
| Experience | Does input carry enough structure |
|
|
2285
|
-
| Accumulation | Is information stored losslessly |
|
|
2286
|
-
| Organization | Entropy management — ΔH_org offsets ΔH_in |
|
|
2287
|
-
| Abstraction | Is abstraction explicitly encoded |
|
|
2288
|
-
| Reconstruction | Can reasoning structure be recovered from artifacts |
|
|
2289
|
-
| Evolution | Does evolution stay coherent or introduce drift |
|
|
2290
|
-
|
|
2291
|
-
## Relationship with EAP
|
|
2292
|
-
EAP answers "how a piece of knowledge should be structured" (explicitness E↑ / reconstructability R↓ / stability S↑);
|
|
2293
|
-
CCE answers "how structured knowledge should keep evolving across time without losing coherence". They complement each other: EAP is static quality, CCE is dynamic persistence.
|
|
2294
|
-
|
|
2295
|
-
## Relationship with Serenity
|
|
2296
|
-
Serenity's session system, session tracking, and entropy management mechanisms (SQC quality loop) are all engineering implementations of CCE;
|
|
2297
|
-
the behavioral constraints embedded in CCC system prompts come from CCE.`;
|
|
2298
|
-
function renderText$4(value) {
|
|
2299
|
-
return [{
|
|
2300
|
-
type: "text",
|
|
2301
|
-
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
2302
|
-
}];
|
|
2303
|
-
}
|
|
2304
|
-
const cceTool = defineTool({
|
|
2305
|
-
name: "cce",
|
|
2306
|
-
description: "CCE cognitive continuity engineering (progressive disclosure): the engineering discipline of maintaining a cognitive entity's identity/accessibility/evolution under bounded resources and irreversible uncertainty. No section returns the full framework; specify a section to focus.",
|
|
2307
|
-
parameters: { section: {
|
|
2308
|
-
type: "string",
|
|
2309
|
-
enum: [
|
|
2310
|
-
"container",
|
|
2311
|
-
"entropy",
|
|
2312
|
-
"lifecycle",
|
|
2313
|
-
"eap"
|
|
2314
|
-
],
|
|
2315
|
-
description: "Focus section: container (5 cognitive-container properties) / entropy (operational entropy H_op) / lifecycle (six phases) / eap (relationship with EAP)"
|
|
2316
|
-
} },
|
|
2317
|
-
output: {
|
|
2318
|
-
schema: { type: "string" },
|
|
2319
|
-
render: (_args, value) => renderText$4(value)
|
|
2320
|
-
},
|
|
2321
|
-
async execute(args) {
|
|
2322
|
-
const section = args.section;
|
|
2323
|
-
const blocks = {
|
|
2324
|
-
container: {
|
|
2325
|
-
start: "## Cognitive Container",
|
|
2326
|
-
end: "## Operational Cognitive Entropy"
|
|
2327
|
-
},
|
|
2328
|
-
entropy: {
|
|
2329
|
-
start: "## Operational Cognitive Entropy (H_op)",
|
|
2330
|
-
end: "## Continuity Maintenance Condition"
|
|
2331
|
-
},
|
|
2332
|
-
lifecycle: { start: "## Six-Phase Lifecycle" },
|
|
2333
|
-
eap: { start: "## Relationship with EAP" }
|
|
2334
|
-
};
|
|
2335
|
-
if (section) {
|
|
2336
|
-
const b = blocks[section];
|
|
2337
|
-
if (b) {
|
|
2338
|
-
const startIdx = CCE_CONTENT.indexOf(b.start);
|
|
2339
|
-
if (startIdx >= 0) return (b.end ? CCE_CONTENT.slice(startIdx, CCE_CONTENT.indexOf(b.end, startIdx)) : CCE_CONTENT.slice(startIdx)).trim();
|
|
2340
|
-
}
|
|
2341
|
-
}
|
|
2342
|
-
return CCE_CONTENT;
|
|
2343
|
-
}
|
|
2344
|
-
});
|
|
2345
|
-
//#endregion
|
|
2346
|
-
//#region src/handyman-preset-inherit.ts
|
|
2347
|
-
/**
|
|
2348
|
-
* 解析 handyman worker 从父 agent 继承的 preset,并组装创建 setup 钩子。
|
|
2349
|
-
* @param parentCtx - 发起 handyman 的 agent 的 scope ctx;无(headless/非 agent 上下文)时为 undefined。
|
|
2350
|
-
* @returns 继承结果:meta 用的 agentPreset 与创建 setup 钩子。
|
|
2351
|
-
*/
|
|
2352
|
-
function handymanPresetInheritance(parentCtx) {
|
|
2353
|
-
if (parentCtx === void 0) return { setup: (childCtx) => {
|
|
2354
|
-
childCtx.tools.restrict({ deny: ["handyman"] });
|
|
2355
|
-
} };
|
|
2356
|
-
const agentPreset = parentCtx.get("agentPresets")?.composedPreset(parentCtx);
|
|
2357
|
-
if (agentPreset === void 0) return { setup: (childCtx) => {
|
|
2358
|
-
childCtx.tools.restrict({ deny: ["handyman"] });
|
|
2359
|
-
} };
|
|
2360
|
-
return {
|
|
2361
|
-
agentPreset,
|
|
2362
|
-
setup: (childCtx) => {
|
|
2363
|
-
childCtx.get("agentPresets")?.composeFrom(childCtx, parentCtx);
|
|
2364
|
-
childCtx.tools.restrict({ deny: ["handyman"] });
|
|
2365
|
-
}
|
|
2366
|
-
};
|
|
2367
|
-
}
|
|
2368
|
-
//#endregion
|
|
2369
|
-
//#region src/handyman-ops.ts
|
|
2370
|
-
/**
|
|
2371
|
-
* handyman-ops.ts — handyman(杂工)纯逻辑层(零 DSH 依赖,可独立单测)
|
|
2372
|
-
*
|
|
2373
|
-
* v1.24.0:loop(牛马)→ handyman(杂工)重命名。语义对齐 osp loop:
|
|
2374
|
-
* 进度文件(handyman-<label>.md/.json)、续跑、轮次 prompt 结构、stop token。
|
|
2375
|
-
* 不兼容旧 loop- 进度文件(用户拍板:仅新 handyman- 前缀)。
|
|
2376
|
-
*/
|
|
2377
|
-
/** label 脱敏(Windows 审计问题 17):非法字符 → '-',去尾点/空格,限长(按码点截断,修复代理对切散 U+FFFD) */
|
|
2378
|
-
function sanitizeLabel(label) {
|
|
2379
|
-
return [...label.replace(/[<>:"/\\|?*\u0000-\u001f]/g, "-").replace(/[ .]+$/g, "")].slice(0, 50).join("");
|
|
2380
|
-
}
|
|
2381
|
-
function handymanProgressPaths(root, label) {
|
|
2382
|
-
const dir = join(root, "AGENT_SESSIONS");
|
|
2383
|
-
const safe = sanitizeLabel(label);
|
|
2384
|
-
return {
|
|
2385
|
-
md: join(dir, `handyman-${safe}.md`),
|
|
2386
|
-
json: join(dir, `handyman-${safe}.json`)
|
|
2387
|
-
};
|
|
2388
|
-
}
|
|
2389
|
-
/** 读取进度(续跑);无文件返回 round 0 */
|
|
2390
|
-
function readProgress(root, label) {
|
|
2391
|
-
const { json } = handymanProgressPaths(root, label);
|
|
2392
|
-
if (!existsSync(json)) return null;
|
|
2393
|
-
try {
|
|
2394
|
-
return JSON.parse(readFileSync(json, "utf-8"));
|
|
2395
|
-
} catch {
|
|
2396
|
-
return null;
|
|
2397
|
-
}
|
|
2398
|
-
}
|
|
2399
|
-
function writeProgress(root, label, p) {
|
|
2400
|
-
const { md, json } = handymanProgressPaths(root, label);
|
|
2401
|
-
mkdirSync(join(root, "AGENT_SESSIONS"), { recursive: true });
|
|
2402
|
-
writeFileSync(json, JSON.stringify({
|
|
2403
|
-
...p,
|
|
2404
|
-
status: p.status ?? "running",
|
|
2405
|
-
updated: (/* @__PURE__ */ new Date()).toISOString()
|
|
2406
|
-
}, null, 2) + "\n", "utf-8");
|
|
2407
|
-
const lines = [
|
|
2408
|
-
`# handyman: ${label}`,
|
|
2409
|
-
`- Model: ${p.model}`,
|
|
2410
|
-
`- Round: ${p.round}`,
|
|
2411
|
-
`- Done: ${p.done}`,
|
|
2412
|
-
"",
|
|
2413
|
-
`## Latest response`,
|
|
2414
|
-
"",
|
|
2415
|
-
p.lastResponse,
|
|
2416
|
-
""
|
|
2417
|
-
];
|
|
2418
|
-
writeFileSync(md, lines.join("\n"), "utf-8");
|
|
2419
|
-
}
|
|
2420
|
-
/** 失败状态落盘(对齐 osp writeFailedStatus:done=true / status=failed / errorCode) */
|
|
2421
|
-
function writeFailedStatus(root, label, info) {
|
|
2422
|
-
const { json } = handymanProgressPaths(root, label);
|
|
2423
|
-
mkdirSync(join(root, "AGENT_SESSIONS"), { recursive: true });
|
|
2424
|
-
const prev = readProgress(root, label);
|
|
2425
|
-
writeFileSync(json, JSON.stringify({
|
|
2426
|
-
round: prev?.round ?? 0,
|
|
2427
|
-
done: true,
|
|
2428
|
-
label,
|
|
2429
|
-
model: prev?.model ?? "",
|
|
2430
|
-
status: "failed",
|
|
2431
|
-
errorCode: info.errorCode,
|
|
2432
|
-
errorMessage: info.errorMessage,
|
|
2433
|
-
updated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2434
|
-
lastResponse: prev?.lastResponse ?? ""
|
|
2435
|
-
}, null, 2) + "\n", "utf-8");
|
|
2436
|
-
}
|
|
2437
|
-
function newStopToken() {
|
|
2438
|
-
return `SERENITY_HANDYMAN_DONE_${randomBytes(8).toString("hex")}`;
|
|
2439
|
-
}
|
|
2440
|
-
/** 解析 model 字符串(provider/model)→ {provider, model};无 / 视为 model-only */
|
|
2441
|
-
function splitModel(model) {
|
|
2442
|
-
const idx = model.indexOf("/");
|
|
2443
|
-
if (idx < 0) return {
|
|
2444
|
-
provider: void 0,
|
|
2445
|
-
model
|
|
2446
|
-
};
|
|
2447
|
-
return {
|
|
2448
|
-
provider: model.slice(0, idx),
|
|
2449
|
-
model: model.slice(idx + 1)
|
|
2450
|
-
};
|
|
2451
|
-
}
|
|
2452
|
-
/** 校验模型在白名单内;不在 → 抛错(用户拍板:只能使用 CCC 配置的模型) */
|
|
2453
|
-
function requireWhitelistedModel(model, models) {
|
|
2454
|
-
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(", ")}`);
|
|
2455
|
-
}
|
|
2456
|
-
/** 轮次 prompt(对齐老 loop 结构:回顾进度 → 自由工作 → 汇报;S134 EAP 化:固定详尽) */
|
|
2457
|
-
function buildRoundPrompt(opts) {
|
|
2458
|
-
const { root, session, label, round, stopToken, progress, task } = opts;
|
|
2459
|
-
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.";
|
|
2460
|
-
return `# ${label} — handyman round ${round}
|
|
2461
|
-
|
|
2462
|
-
CCC root: ${root}
|
|
2463
|
-
${session ? `Work session: ${session} (progress recorded in AGENT_SESSIONS/${session}/SESSION.md)` : ""}
|
|
2464
|
-
${task ? `Task: ${task}` : `Task: follow the work corresponding to label "${label}" (if a work session exists, read SESSION.md first to clarify the goal)`}
|
|
2465
|
-
${resumeNote}
|
|
2466
|
-
|
|
2467
|
-
## Work rules (fixed every round, must follow)
|
|
2468
|
-
1. Work freely within this round: read files, modify code, execute commands — use every means to advance the task.
|
|
2469
|
-
2. If this task is **reading/curating or text-writing work** (extracting from files, summarizing, writing docs, generating text, etc.),
|
|
2470
|
-
first load eap (acc-eap skill) and organize output per the EAP standard:
|
|
2471
|
-
- E↑ Explicit: entities/variables clearly defined, relationships with direction and cardinality, boundaries drawn, no ambiguous words
|
|
2472
|
-
- R↓ Reconstructable: key conclusions record sources and reasoning, rebuildable by later agents
|
|
2473
|
-
- S↑ Stable: output structure regenerates repeatably, no reliance on implicit context
|
|
2474
|
-
3. Reports must be concrete and verifiable — no filler.
|
|
2475
|
-
|
|
2476
|
-
## Per-round report (fixed format, answer each item)
|
|
2477
|
-
1. What was done this round (concrete)
|
|
2478
|
-
2. Next-step plan
|
|
2479
|
-
3. Whether the task is complete (if complete, output only ${stopToken})
|
|
2480
|
-
|
|
2481
|
-
If the task is complete, output only ${stopToken}.`;
|
|
2482
|
-
}
|
|
2483
|
-
/**
|
|
2484
|
-
* handyman 规模化使用指引(guide 子命令输出;S134 继承 + v1.24.0 更新):
|
|
2485
|
-
* 使用 handyman 前必须先加载 eap 设计方案;并行策略(jobs 编排);提示词规范(详尽固定 EAP);
|
|
2486
|
-
* 阅读/文字编写类 handyman 内部也加载 eap。
|
|
2487
|
-
*/
|
|
2488
|
-
const HANDYMAN_GUIDE = `# handyman — Scale-Up Usage Guide (guide)
|
|
2489
|
-
|
|
2490
|
-
## ⚠️ Before using: load eap and design the plan
|
|
2491
|
-
Before calling handyman, load eap (acc-eap skill) and design the "scale-up handyman plan" based on the EAP framework.
|
|
2492
|
-
|
|
2493
|
-
### 1. Task decomposition (E↑ Explicit)
|
|
2494
|
-
- Split large tasks into explicit subtasks: each subtask defines goal / input / boundaries (what to do, what not to do) / acceptance criteria
|
|
2495
|
-
- Make dependencies explicit: dependent tasks run serially, independent ones can run in parallel
|
|
2496
|
-
|
|
2497
|
-
### 2. Prompt design (handyman's task parameter)
|
|
2498
|
-
- task must be detailed, fixed, and EAP-compliant: clear goal, drawn boundaries, decidable acceptance criteria
|
|
2499
|
-
- Anti-example "handle this file" — ambiguous; good example "read <path>, extract all rows of the「关键决策」table,
|
|
2500
|
-
output a JSON array (fields id/conclusion/evidence), do not modify the original file"
|
|
2501
|
-
- Reading/curating or text-writing work (extracting from files, summarizing, writing docs, generating text, etc.):
|
|
2502
|
-
the handyman-internal agent is also required to load eap and organize output per the EAP standard
|
|
2503
|
-
|
|
2504
|
-
### 3. Model whitelist (CCC-configured, mandatory)
|
|
2505
|
-
- handyman only uses models listed in .opencode/serenity.json "handyman.models" — never arbitrary models
|
|
2506
|
-
- Recursive subagents inside a handyman inherit the handyman's model automatically (DSH native)
|
|
2507
|
-
- Keep the subagent tool instance free of a fixed agentOptions, or model inheritance breaks
|
|
2508
|
-
|
|
2509
|
-
### 4. Parallel strategy (jobs orchestration, workflow capability)
|
|
2510
|
-
- Independent subtasks can run in parallel via handyman(jobs=[...]): each job gets its own label + task + stop token + progress file
|
|
2511
|
-
- Concurrency safety guaranteed: unique sessionId (handyman-<label>-<uuid>), progress files isolated per label
|
|
2512
|
-
(AGENT_SESSIONS/handyman-<label>.json) — same label resumes, different labels never interfere
|
|
2513
|
-
- Parallel cap: handyman.maxParallel (default 10 — cheap models are cheap)
|
|
2514
|
-
- Aggregation: after each parallel job produces progress, the main agent merges (or spawns one aggregation handyman)
|
|
2515
|
-
- For programmable pipeline/phase orchestration at scale, use the platform's workflow tool instead
|
|
2753
|
+
## Continuity Maintenance Condition
|
|
2754
|
+
> **ΔH_org ≥ ΔH_in** — organization must at minimum keep pace with accumulation
|
|
2516
2755
|
|
|
2517
|
-
##
|
|
2518
|
-
|
|
2519
|
-
|
|
2756
|
+
## Six-Phase Lifecycle
|
|
2757
|
+
Experience → Accumulation → Organization → Abstraction → Reconstruction → Evolution →(loop)
|
|
2758
|
+
| Phase | Engineering Concern |
|
|
2759
|
+
|-------|---------------------|
|
|
2760
|
+
| Experience | Does input carry enough structure |
|
|
2761
|
+
| Accumulation | Is information stored losslessly |
|
|
2762
|
+
| Organization | Entropy management — ΔH_org offsets ΔH_in |
|
|
2763
|
+
| Abstraction | Is abstraction explicitly encoded |
|
|
2764
|
+
| Reconstruction | Can reasoning structure be recovered from artifacts |
|
|
2765
|
+
| Evolution | Does evolution stay coherent or introduce drift |
|
|
2520
2766
|
|
|
2521
|
-
##
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2767
|
+
## Relationship with EAP
|
|
2768
|
+
EAP answers "how a piece of knowledge should be structured" (explicitness E↑ / reconstructability R↓ / stability S↑);
|
|
2769
|
+
CCE answers "how structured knowledge should keep evolving across time without losing coherence". They complement each other: EAP is static quality, CCE is dynamic persistence.
|
|
2770
|
+
|
|
2771
|
+
## Relationship with Serenity
|
|
2772
|
+
Serenity's session system, session tracking, and entropy management mechanisms (SQC quality loop) are all engineering implementations of CCE;
|
|
2773
|
+
the behavioral constraints embedded in CCC system prompts come from CCE.`;
|
|
2774
|
+
function renderText$5(value) {
|
|
2775
|
+
return [{
|
|
2776
|
+
type: "text",
|
|
2777
|
+
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
2778
|
+
}];
|
|
2779
|
+
}
|
|
2780
|
+
const cceTool = defineTool({
|
|
2781
|
+
name: "cce",
|
|
2782
|
+
description: "CCE cognitive continuity engineering (progressive disclosure): the engineering discipline of maintaining a cognitive entity's identity/accessibility/evolution under bounded resources and irreversible uncertainty. No section returns the full framework; specify a section to focus.",
|
|
2783
|
+
parameters: { section: {
|
|
2784
|
+
type: "string",
|
|
2785
|
+
enum: [
|
|
2786
|
+
"container",
|
|
2787
|
+
"entropy",
|
|
2788
|
+
"lifecycle",
|
|
2789
|
+
"eap"
|
|
2790
|
+
],
|
|
2791
|
+
description: "Focus section: container (5 cognitive-container properties) / entropy (operational entropy H_op) / lifecycle (six phases) / eap (relationship with EAP)"
|
|
2792
|
+
} },
|
|
2793
|
+
output: {
|
|
2794
|
+
schema: { type: "string" },
|
|
2795
|
+
render: (_args, value) => renderText$5(value)
|
|
2796
|
+
},
|
|
2797
|
+
async execute(args) {
|
|
2798
|
+
const section = args.section;
|
|
2799
|
+
const blocks = {
|
|
2800
|
+
container: {
|
|
2801
|
+
start: "## Cognitive Container",
|
|
2802
|
+
end: "## Operational Cognitive Entropy"
|
|
2803
|
+
},
|
|
2804
|
+
entropy: {
|
|
2805
|
+
start: "## Operational Cognitive Entropy (H_op)",
|
|
2806
|
+
end: "## Continuity Maintenance Condition"
|
|
2807
|
+
},
|
|
2808
|
+
lifecycle: { start: "## Six-Phase Lifecycle" },
|
|
2809
|
+
eap: { start: "## Relationship with EAP" }
|
|
2810
|
+
};
|
|
2811
|
+
if (section) {
|
|
2812
|
+
const b = blocks[section];
|
|
2813
|
+
if (b) {
|
|
2814
|
+
const startIdx = CCE_CONTENT.indexOf(b.start);
|
|
2815
|
+
if (startIdx >= 0) return (b.end ? CCE_CONTENT.slice(startIdx, CCE_CONTENT.indexOf(b.end, startIdx)) : CCE_CONTENT.slice(startIdx)).trim();
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
return CCE_CONTENT;
|
|
2543
2819
|
}
|
|
2544
|
-
|
|
2545
|
-
|
|
2820
|
+
});
|
|
2821
|
+
//#endregion
|
|
2822
|
+
//#region src/handyman-preset-inherit.ts
|
|
2823
|
+
/**
|
|
2824
|
+
* 解析 handyman worker 从父 agent 继承的 preset,并组装创建 setup 钩子。
|
|
2825
|
+
* @param parentCtx - 发起 handyman 的 agent 的 scope ctx;无(headless/非 agent 上下文)时为 undefined。
|
|
2826
|
+
* @returns 继承结果:meta 用的 agentPreset 与创建 setup 钩子。
|
|
2827
|
+
*/
|
|
2828
|
+
function handymanPresetInheritance(parentCtx) {
|
|
2829
|
+
if (parentCtx === void 0) return { setup: (childCtx) => {
|
|
2830
|
+
childCtx.tools.restrict({ deny: ["handyman"] });
|
|
2831
|
+
} };
|
|
2832
|
+
const agentPreset = parentCtx.get("agentPresets")?.composedPreset(parentCtx);
|
|
2833
|
+
if (agentPreset === void 0) return { setup: (childCtx) => {
|
|
2834
|
+
childCtx.tools.restrict({ deny: ["handyman"] });
|
|
2835
|
+
} };
|
|
2836
|
+
return {
|
|
2837
|
+
agentPreset,
|
|
2838
|
+
setup: (childCtx) => {
|
|
2839
|
+
childCtx.get("agentPresets")?.composeFrom(childCtx, parentCtx);
|
|
2840
|
+
childCtx.tools.restrict({ deny: ["handyman"] });
|
|
2841
|
+
}
|
|
2842
|
+
};
|
|
2546
2843
|
}
|
|
2547
2844
|
//#endregion
|
|
2548
2845
|
//#region src/tools/handyman.ts
|
|
@@ -2566,10 +2863,10 @@ function listActiveHandymen(root) {
|
|
|
2566
2863
|
* preset 继承 + 工具收窄:setup 钩子里对子 agent 执行 agentPresets.composeFrom(对齐
|
|
2567
2864
|
* subagent 先例)+ tools.restrict deny handyman(worker 内部看不到 handyman 工具)。
|
|
2568
2865
|
*/
|
|
2569
|
-
function agentCwd$
|
|
2866
|
+
function agentCwd$5(exec) {
|
|
2570
2867
|
return (exec.agent?.session)?.header?.cwd ?? process.cwd();
|
|
2571
2868
|
}
|
|
2572
|
-
function renderText$
|
|
2869
|
+
function renderText$4(value) {
|
|
2573
2870
|
return [{
|
|
2574
2871
|
type: "text",
|
|
2575
2872
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -2786,11 +3083,11 @@ function createHandymanTool(ctx) {
|
|
|
2786
3083
|
},
|
|
2787
3084
|
output: {
|
|
2788
3085
|
schema: { type: "json" },
|
|
2789
|
-
render: (_args, value) => renderText$
|
|
3086
|
+
render: (_args, value) => renderText$4(value)
|
|
2790
3087
|
},
|
|
2791
3088
|
async execute(args, exec) {
|
|
2792
3089
|
if (args.guide) return { guide: HANDYMAN_GUIDE };
|
|
2793
|
-
const root = findSerenityRoot(agentCwd$
|
|
3090
|
+
const root = findSerenityRoot(agentCwd$5(exec));
|
|
2794
3091
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
2795
3092
|
const hc = readHandymanConfig(root, DEFAULT_SERENITY_CONFIG_PATHS);
|
|
2796
3093
|
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\"}})");
|
|
@@ -4885,7 +5182,9 @@ const simpleSettingsSchema = z.object({
|
|
|
4885
5182
|
gatewayEnabled: z.boolean().default(false),
|
|
4886
5183
|
rebuildEnabled: z.boolean().default(true),
|
|
4887
5184
|
rebuildThreshold: z.number().min(.01).max(1).default(.9),
|
|
4888
|
-
namingEnabled: z.boolean().default(true)
|
|
5185
|
+
namingEnabled: z.boolean().default(true),
|
|
5186
|
+
skiffEnabled: z.boolean().default(false),
|
|
5187
|
+
skiffDebugPort: z.number().min(1024).max(65535).default(3099)
|
|
4889
5188
|
});
|
|
4890
5189
|
/** 从插件 Config 提取 entry 默认(settings base 层) */
|
|
4891
5190
|
function entryDefaults(config) {
|
|
@@ -4893,7 +5192,9 @@ function entryDefaults(config) {
|
|
|
4893
5192
|
gatewayEnabled: config.gateway?.enabled ?? false,
|
|
4894
5193
|
rebuildEnabled: config.rebuild?.enabled ?? true,
|
|
4895
5194
|
rebuildThreshold: config.rebuild?.thresholdRatio ?? .9,
|
|
4896
|
-
namingEnabled: config.naming?.enabled ?? true
|
|
5195
|
+
namingEnabled: config.naming?.enabled ?? true,
|
|
5196
|
+
skiffEnabled: config.skiff?.enabled ?? false,
|
|
5197
|
+
skiffDebugPort: config.skiff?.debugPort ?? 3099
|
|
4897
5198
|
};
|
|
4898
5199
|
}
|
|
4899
5200
|
/** 运行时源(installSettingsSection 注入:settings scope 或 entry fallback) */
|
|
@@ -4904,7 +5205,9 @@ function defaultSimpleSettings() {
|
|
|
4904
5205
|
gatewayEnabled: false,
|
|
4905
5206
|
rebuildEnabled: true,
|
|
4906
5207
|
rebuildThreshold: .9,
|
|
4907
|
-
namingEnabled: true
|
|
5208
|
+
namingEnabled: true,
|
|
5209
|
+
skiffEnabled: false,
|
|
5210
|
+
skiffDebugPort: 3099
|
|
4908
5211
|
};
|
|
4909
5212
|
}
|
|
4910
5213
|
/**
|
|
@@ -5565,7 +5868,7 @@ function qaCheck(root, key) {
|
|
|
5565
5868
|
* 同步把当前 dsh 会话重命名为该 SESSION 目录名**(sessionTitle.rename,user source
|
|
5566
5869
|
* pin 住标题)。非创建时预命名。
|
|
5567
5870
|
*/
|
|
5568
|
-
function agentCwd$
|
|
5871
|
+
function agentCwd$4(exec) {
|
|
5569
5872
|
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
5570
5873
|
}
|
|
5571
5874
|
/** 当前 dsh 会话 id(use/close 按会话隔离的 scope) */
|
|
@@ -5628,7 +5931,7 @@ function renameDshSessionOnUse(deps, session, titles, active) {
|
|
|
5628
5931
|
};
|
|
5629
5932
|
}
|
|
5630
5933
|
}
|
|
5631
|
-
function renderText$
|
|
5934
|
+
function renderText$3(value) {
|
|
5632
5935
|
return [{
|
|
5633
5936
|
type: "text",
|
|
5634
5937
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -5762,10 +6065,10 @@ function createSessionTool(ctx) {
|
|
|
5762
6065
|
},
|
|
5763
6066
|
output: {
|
|
5764
6067
|
schema: { type: "json" },
|
|
5765
|
-
render: (args, value) => renderText$
|
|
6068
|
+
render: (args, value) => renderText$3(value)
|
|
5766
6069
|
},
|
|
5767
6070
|
async execute(args, exec) {
|
|
5768
|
-
const root = findSerenityRoot(agentCwd$
|
|
6071
|
+
const root = findSerenityRoot(agentCwd$4(exec));
|
|
5769
6072
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
5770
6073
|
const entries = loadMsmEntries(root);
|
|
5771
6074
|
const hasSessionTool = entries.some((e) => e.name === "session-tool");
|
|
@@ -5934,7 +6237,7 @@ function createEpochPromotion(promoteEvents, requiredSignals = 1, maxRoundsFallb
|
|
|
5934
6237
|
promoted: true
|
|
5935
6238
|
};
|
|
5936
6239
|
const loopSid = session.id;
|
|
5937
|
-
if (typeof loopSid === "string" && loopSid.startsWith("handyman-")) return {
|
|
6240
|
+
if (typeof loopSid === "string" && (loopSid.startsWith("handyman-") || isSkiffSessionId(loopSid))) return {
|
|
5938
6241
|
boundary: -1,
|
|
5939
6242
|
promoted: true
|
|
5940
6243
|
};
|
|
@@ -6038,7 +6341,7 @@ function registerBootstrap(ctx) {
|
|
|
6038
6341
|
const session = agent.session;
|
|
6039
6342
|
const depth = session?.header?.delegationDepth ?? 0;
|
|
6040
6343
|
const sid = typeof session?.id === "string" ? session.id : void 0;
|
|
6041
|
-
if (sid !== void 0 && sid.startsWith("handyman-")) return;
|
|
6344
|
+
if (sid !== void 0 && (sid.startsWith("handyman-") || isSkiffSessionId(sid))) return;
|
|
6042
6345
|
if (depth === 0) {
|
|
6043
6346
|
if (session?.events?.some((event) => event.type === "user/message")) return;
|
|
6044
6347
|
} else if (sid !== void 0 && anchoredSessions.has(sid)) return;
|
|
@@ -6351,13 +6654,13 @@ function registerRebuildTurnHook(ctx) {
|
|
|
6351
6654
|
* (turn 结束前所有 tool/result 已 append → 无孤儿;S141 INVALID_REQUEST 根治)。
|
|
6352
6655
|
* 服务端逻辑在 ../rebuild.ts(queueRebuild/performRebuild/registerRebuildTurnHook)。
|
|
6353
6656
|
*/
|
|
6354
|
-
function agentCwd$
|
|
6657
|
+
function agentCwd$3(exec) {
|
|
6355
6658
|
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
6356
6659
|
}
|
|
6357
6660
|
function agentSessionId(exec) {
|
|
6358
6661
|
return exec.agent?.session?.id ?? "";
|
|
6359
6662
|
}
|
|
6360
|
-
function renderText$
|
|
6663
|
+
function renderText$2(value) {
|
|
6361
6664
|
return [{
|
|
6362
6665
|
type: "text",
|
|
6363
6666
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -6374,17 +6677,17 @@ function createRebuildTool(ctx) {
|
|
|
6374
6677
|
} },
|
|
6375
6678
|
output: {
|
|
6376
6679
|
schema: { type: "json" },
|
|
6377
|
-
render: (args, value) => renderText$
|
|
6680
|
+
render: (args, value) => renderText$2(value)
|
|
6378
6681
|
},
|
|
6379
6682
|
async execute(args, exec) {
|
|
6380
|
-
const root = findSerenityRoot(agentCwd$
|
|
6683
|
+
const root = findSerenityRoot(agentCwd$3(exec));
|
|
6381
6684
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
6382
6685
|
const dshSessionId = agentSessionId(exec);
|
|
6383
6686
|
if (!dshSessionId) throw new Error("Unable to determine the current dsh session id");
|
|
6384
6687
|
const result = await queueRebuild(ctx, {
|
|
6385
6688
|
root,
|
|
6386
6689
|
note: args.note,
|
|
6387
|
-
agentCwd: agentCwd$
|
|
6690
|
+
agentCwd: agentCwd$3(exec),
|
|
6388
6691
|
dshSessionId
|
|
6389
6692
|
});
|
|
6390
6693
|
return {
|
|
@@ -6414,10 +6717,10 @@ const ACTIONS = [
|
|
|
6414
6717
|
"show",
|
|
6415
6718
|
"doc"
|
|
6416
6719
|
];
|
|
6417
|
-
function agentCwd$
|
|
6720
|
+
function agentCwd$2(exec) {
|
|
6418
6721
|
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
6419
6722
|
}
|
|
6420
|
-
function renderText(value) {
|
|
6723
|
+
function renderText$1(value) {
|
|
6421
6724
|
return [{
|
|
6422
6725
|
type: "text",
|
|
6423
6726
|
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
@@ -6447,6 +6750,139 @@ const localstoreTool = defineTool({
|
|
|
6447
6750
|
description: "Namespace credential|config (default credential)"
|
|
6448
6751
|
}
|
|
6449
6752
|
},
|
|
6753
|
+
output: {
|
|
6754
|
+
schema: { type: "json" },
|
|
6755
|
+
render: (args, value) => renderText$1(value)
|
|
6756
|
+
},
|
|
6757
|
+
async execute(args, exec) {
|
|
6758
|
+
const root = findSerenityRoot(agentCwd$2(exec));
|
|
6759
|
+
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
6760
|
+
return runLocalStore(root, args);
|
|
6761
|
+
}
|
|
6762
|
+
});
|
|
6763
|
+
//#endregion
|
|
6764
|
+
//#region src/tools/skiff-admin.ts
|
|
6765
|
+
/**
|
|
6766
|
+
* skiff-admin.ts — skiff_admin ACC 工具(F4a',v1.25.0 实验性,第 12 个工具)
|
|
6767
|
+
*
|
|
6768
|
+
* 教 CCC 如何定义 Skiff 角色(仿 session 工具 hook-develop-guide 的 SEP 教学模式,
|
|
6769
|
+
* 用户拍板 2026-08-28:guide 定义教程 / validate 配置校验 / list 角色摘要)。
|
|
6770
|
+
*
|
|
6771
|
+
* 归属:ACC 机制工具(教会 + 校验),角色内容仍归 CCC 配置(.opencode/serenity.json skiff.roles)。
|
|
6772
|
+
*/
|
|
6773
|
+
function agentCwd$1(exec) {
|
|
6774
|
+
return exec.agent?.session?.header?.cwd ?? process.cwd();
|
|
6775
|
+
}
|
|
6776
|
+
function renderText(value) {
|
|
6777
|
+
return [{
|
|
6778
|
+
type: "text",
|
|
6779
|
+
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
6780
|
+
}];
|
|
6781
|
+
}
|
|
6782
|
+
/** Skiff 定义教程(核心):概念 / schema / 认知 MSM 写法 / 双白名单 / 轨迹纪律 / 示例角色 */
|
|
6783
|
+
const SKIFF_GUIDE = `═══ Skiff Definition Guide (F4, experimental) ═══
|
|
6784
|
+
|
|
6785
|
+
Skiff = 宁静号放出的独立小艇——完整 trajectory(在宁静号内全知全能)的**任意子集角色**。
|
|
6786
|
+
由本 CCC(.opencode/serenity.json skiff.roles)定义:能力面(双白名单)+ 轨迹纪律子集 + 系统提示词。
|
|
6787
|
+
dsp 只提供机制(双白名单强制 + 基础提示词 + 调试问答页);角色内容完全由 CCC 发挥。
|
|
6788
|
+
|
|
6789
|
+
── 角色配置 schema ──
|
|
6790
|
+
{ "skiff": { "roles": {
|
|
6791
|
+
"<role-name>": {
|
|
6792
|
+
"model": "provider/model", // 角色模型(CCC 直接指定,无白名单校验;缺省回退 handyman.defaultModel)
|
|
6793
|
+
"msms": ["msm-a", "msm-b"], // MSM 白名单(独立):acc_msm exec 只能跑这些;register/deregister 必拒;list 只显示这些
|
|
6794
|
+
"tools": ["read","grep","glob",...], // 非 MSM 工具白名单(独立):白名单外工具一律不可用(guard 强制)
|
|
6795
|
+
"trajectory": { "session": false, "keeper": false, "rebuild": false }, // 轨迹纪律子集(缺省全关 = 完全独立)
|
|
6796
|
+
"systemPrompt": "..." // 角色人格/认知边界/风格(CCC 完整定义;dsp 只给基础提示词)
|
|
6797
|
+
}
|
|
6798
|
+
} } }
|
|
6799
|
+
|
|
6800
|
+
── 双白名单语义(全按白名单暴露,白名单外全隐藏)──
|
|
6801
|
+
- tools 空 + msms 非空 = 纯 MSM 角色(认知问答典型形态)
|
|
6802
|
+
- msms 非空 → acc_msm 工具自动可用(MSM 通道)
|
|
6803
|
+
- 白名单外工具即使 DSH 未来新增也自动被挡(guard 按角色判定,不枚举工具名——完备性)
|
|
6804
|
+
|
|
6805
|
+
── 认知 MSM 写法(读知识 / 操作能力)──
|
|
6806
|
+
- 读知识:脚本读 CCC 内文件(SERENITY_ROOT env 注入)→ 输出答案/摘要(如 cognitive-qa)
|
|
6807
|
+
- 操作能力:脚本调用既有家庭工具/服务(SSH/API/文件操作)——角色能力上限 = 白名单 MSM 的实际行为
|
|
6808
|
+
- 例:cognitive-qa(读 docs/references 回答)、review-scan(读代码出审查意见)、review-fix(写修复)
|
|
6809
|
+
- 注意:MSM 在脚本层执行(bun),文件访问不受 agent 工具面约束——"只读"语义靠 CCC 自写 MSM 自觉
|
|
6810
|
+
|
|
6811
|
+
── 轨迹纪律子集(trajectory)──
|
|
6812
|
+
- session/keeper/rebuild 默认 false:Skiff 完全独立(不建 SESSION.md、无 keeper 提醒、无 rebuild 压力检测)
|
|
6813
|
+
- 开启某项 = 该角色参与对应轨迹机制(如 keeper=true 计分提醒按角色生效)
|
|
6814
|
+
|
|
6815
|
+
── 示例角色 ──
|
|
6816
|
+
qa-readonly: { "msms": ["cognitive-qa"], "tools": [], "trajectory": {} }
|
|
6817
|
+
→ 认知问答(仅 MSM 通道,无直接工具,完全独立)
|
|
6818
|
+
code-review: { "msms": ["review-scan","review-fix"], "tools": ["read","grep","glob","write","edit"], "trajectory": { "keeper": true } }
|
|
6819
|
+
→ 有操作能力(可写修复),参与 keeper 轨迹机制
|
|
6820
|
+
|
|
6821
|
+
── 运行 ──
|
|
6822
|
+
调试:设置面板「Serenity」页 Skiff 区块开启 → http://127.0.0.1:<debugPort> 问答页实测
|
|
6823
|
+
(v1.25.0 唯一客户端面;ACP stdio 协议 F4c 后续,与调试页共用同一会话核心)
|
|
6824
|
+
启停 = 人工(设置面板开关,不随插件加载自动启动);未开启零资源占用`;
|
|
6825
|
+
/** 校验当前 CCC 的 skiff 配置:roles schema 合法 / msms 均已注册 / model ∈ handyman.models / systemPrompt 非空 */
|
|
6826
|
+
function validateSkiffConfig(root) {
|
|
6827
|
+
const roles = readSkiffRoles(root);
|
|
6828
|
+
const issues = [];
|
|
6829
|
+
if (roles.size === 0) return {
|
|
6830
|
+
ok: true,
|
|
6831
|
+
issues: [],
|
|
6832
|
+
roleCount: 0,
|
|
6833
|
+
note: "no skiff roles defined (skiff disabled — zero impact)"
|
|
6834
|
+
};
|
|
6835
|
+
const registered = new Set(loadMsmEntries(root).map((e) => e.name));
|
|
6836
|
+
const hc = readHandymanConfig(root);
|
|
6837
|
+
const allowedModels = hc ? new Set(hc.models) : null;
|
|
6838
|
+
for (const [name, role] of roles) {
|
|
6839
|
+
if (!role.systemPrompt?.trim()) issues.push(`role "${name}": systemPrompt is empty (CCC should fully define the role persona)`);
|
|
6840
|
+
for (const m of role.msms ?? []) if (!registered.has(m)) issues.push(`role "${name}": msms "${m}" is not registered in mech-registry.json`);
|
|
6841
|
+
if (role.model && allowedModels && !allowedModels.has(role.model)) issues.push(`role "${name}": model "${role.model}" not in handyman.models (${[...allowedModels].join(", ") || "(none)"})`);
|
|
6842
|
+
}
|
|
6843
|
+
return {
|
|
6844
|
+
ok: issues.length === 0,
|
|
6845
|
+
issues,
|
|
6846
|
+
roleCount: roles.size
|
|
6847
|
+
};
|
|
6848
|
+
}
|
|
6849
|
+
/** 列出当前 CCC 已定义角色(名 / 模型 / msms / tools / 轨迹纪律摘要) */
|
|
6850
|
+
function listSkiffRoles(root) {
|
|
6851
|
+
const roles = readSkiffRoles(root);
|
|
6852
|
+
if (roles.size === 0) return {
|
|
6853
|
+
roles: [],
|
|
6854
|
+
note: "no skiff roles defined (skiff disabled)"
|
|
6855
|
+
};
|
|
6856
|
+
const items = [...roles.entries()].map(([name, role]) => ({
|
|
6857
|
+
name,
|
|
6858
|
+
model: role.model ?? "(handyman default)",
|
|
6859
|
+
msms: role.msms ?? [],
|
|
6860
|
+
tools: role.tools ?? [],
|
|
6861
|
+
trajectory: {
|
|
6862
|
+
session: role.trajectory?.session === true,
|
|
6863
|
+
keeper: role.trajectory?.keeper === true,
|
|
6864
|
+
rebuild: role.trajectory?.rebuild === true
|
|
6865
|
+
},
|
|
6866
|
+
hasSystemPrompt: Boolean(role.systemPrompt?.trim())
|
|
6867
|
+
}));
|
|
6868
|
+
return {
|
|
6869
|
+
roles: items,
|
|
6870
|
+
count: items.length
|
|
6871
|
+
};
|
|
6872
|
+
}
|
|
6873
|
+
const skiffAdminTool = defineTool({
|
|
6874
|
+
name: "skiff_admin",
|
|
6875
|
+
description: "Skiff (F4, experimental): CCC cognitive-subset roles (subsets of the full serenity trajectory). guide: definition tutorial (concept/role schema/cognitive MSM writing/dual whitelist/trajectory subset/examples); validate: check the CCC skiff config (roles schema legal / msms registered / model in handyman.models / systemPrompt non-empty); list: role summary (name/model/msms/tools/trajectory). Roles are defined by the CCC in .opencode/serenity.json skiff.roles.",
|
|
6876
|
+
parameters: { action: {
|
|
6877
|
+
type: "string",
|
|
6878
|
+
enum: [
|
|
6879
|
+
"guide",
|
|
6880
|
+
"validate",
|
|
6881
|
+
"list"
|
|
6882
|
+
],
|
|
6883
|
+
required: true,
|
|
6884
|
+
description: "Subcommand: guide (definition tutorial) / validate (config check) / list (role summary)"
|
|
6885
|
+
} },
|
|
6450
6886
|
output: {
|
|
6451
6887
|
schema: { type: "json" },
|
|
6452
6888
|
render: (args, value) => renderText(value)
|
|
@@ -6454,7 +6890,12 @@ const localstoreTool = defineTool({
|
|
|
6454
6890
|
async execute(args, exec) {
|
|
6455
6891
|
const root = findSerenityRoot(agentCwd$1(exec));
|
|
6456
6892
|
if (!root) throw new Error("No CCC found: no .serenity file from agent cwd");
|
|
6457
|
-
|
|
6893
|
+
switch (args.action) {
|
|
6894
|
+
case "guide": return { guide: SKIFF_GUIDE };
|
|
6895
|
+
case "validate": return validateSkiffConfig(root);
|
|
6896
|
+
case "list": return listSkiffRoles(root);
|
|
6897
|
+
default: throw new Error("skiff_admin requires action: guide | validate | list");
|
|
6898
|
+
}
|
|
6458
6899
|
}
|
|
6459
6900
|
});
|
|
6460
6901
|
//#endregion
|
|
@@ -6563,9 +7004,13 @@ function registerKeeper(ctx, opts = {}) {
|
|
|
6563
7004
|
};
|
|
6564
7005
|
ctx.on("tools/post-execute", async (exec, _result, next) => {
|
|
6565
7006
|
if (!exec.agent) return next();
|
|
6566
|
-
|
|
7007
|
+
const root = findSerenityRoot(exec.agent?.session?.header?.cwd ?? process.cwd());
|
|
7008
|
+
if (!root) return next();
|
|
7009
|
+
const sessionId = exec.agent?.session?.id;
|
|
7010
|
+
const skiffKeeper = skiffTrajectoryEnabled(root, sessionId, "keeper");
|
|
7011
|
+
const skiffRebuild = skiffTrajectoryEnabled(root, sessionId, "rebuild");
|
|
6567
7012
|
const tracker = trackerFor(exec);
|
|
6568
|
-
const shouldRemind = tracker.step(exec.name);
|
|
7013
|
+
const shouldRemind = skiffKeeper ? tracker.step(exec.name) : false;
|
|
6569
7014
|
const downstream = await next();
|
|
6570
7015
|
const blocks = [];
|
|
6571
7016
|
if (shouldRemind) {
|
|
@@ -6575,7 +7020,7 @@ function registerKeeper(ctx, opts = {}) {
|
|
|
6575
7020
|
text: reminderText(code, tracker.currentScore)
|
|
6576
7021
|
});
|
|
6577
7022
|
}
|
|
6578
|
-
if (readSimpleSettings().rebuildEnabled) {
|
|
7023
|
+
if (skiffRebuild && readSimpleSettings().rebuildEnabled) {
|
|
6579
7024
|
const session = exec.agent?.session;
|
|
6580
7025
|
if (session) {
|
|
6581
7026
|
const pressure = readContextPressure(ctx, session);
|
|
@@ -7040,6 +7485,7 @@ function accBlock(root) {
|
|
|
7040
7485
|
" 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",
|
|
7041
7486
|
" session_rebuild — rebuild this conversation in place from SESSION.md when the trajectory-tracker trips",
|
|
7042
7487
|
" localstore — ACC local credential/config storage (CCC-root localstore.json, JSON format; git policy localstore.gitTrack default deny); doc subcommand outputs the spec",
|
|
7488
|
+
" skiff_admin — Skiff (F4, experimental): CCC cognitive-subset roles — guide (definition tutorial) / validate (config check) / list (role summary)",
|
|
7043
7489
|
"",
|
|
7044
7490
|
" ℹ️ Use relative paths from the CCC root for CCC-internal file operations (read/write/edit/glob/grep etc.), e.g. AGENT_SESSIONS/2026-08-14--S134--x/SESSION.md; Root / absolute SESSION.md paths are identifiers only, not tool arguments",
|
|
7045
7491
|
"",
|
|
@@ -7442,7 +7888,9 @@ function registerEntrySkillSectionGlobal(ctx) {
|
|
|
7442
7888
|
if (!cwd) return "";
|
|
7443
7889
|
const root = findSerenityRoot(cwd);
|
|
7444
7890
|
if (!root) return "";
|
|
7445
|
-
const
|
|
7891
|
+
const scope = agentScope$1(context);
|
|
7892
|
+
if (isSkiffSessionId(scope)) return "";
|
|
7893
|
+
const base = serenitySystemPrompt(root, scope);
|
|
7446
7894
|
const codeLine = codeModeAdaptationLine(ctx, context.scope);
|
|
7447
7895
|
return codeLine ? `${base}\n${codeLine}` : base;
|
|
7448
7896
|
}
|
|
@@ -7474,6 +7922,7 @@ function registerEntrySkillSection(agent, root) {
|
|
|
7474
7922
|
name: "serenity-entry",
|
|
7475
7923
|
order: -50,
|
|
7476
7924
|
text: (context) => {
|
|
7925
|
+
if (isSkiffSessionId(scope)) return "";
|
|
7477
7926
|
const base = serenitySystemPrompt(root, scope);
|
|
7478
7927
|
const codeLine = codeModeAdaptationLine(agent.ctx, context.scope);
|
|
7479
7928
|
return codeLine ? `${base}\n${codeLine}` : base;
|
|
@@ -7550,6 +7999,7 @@ function shouldAutoRestore(agent) {
|
|
|
7550
7999
|
if (session.header?.origin === "subagent") return false;
|
|
7551
8000
|
if (session.header?.parentSession) return false;
|
|
7552
8001
|
if (session.id?.startsWith("handyman-")) return false;
|
|
8002
|
+
if (isSkiffSessionId(session.id)) return false;
|
|
7553
8003
|
return true;
|
|
7554
8004
|
}
|
|
7555
8005
|
/**
|
|
@@ -7569,6 +8019,7 @@ function registerContext(ctx, opts = {}) {
|
|
|
7569
8019
|
const root = findSerenityRoot(agent.session?.header?.cwd ?? process.cwd());
|
|
7570
8020
|
if (!root) return;
|
|
7571
8021
|
const key = agentKey(agent);
|
|
8022
|
+
if (isSkiffSessionId(key)) return;
|
|
7572
8023
|
registerEntrySkillSection(agent, root);
|
|
7573
8024
|
const scope = agentScope(agent);
|
|
7574
8025
|
if (shouldRestoreActive(agent) && getActiveSessionInfo(scope) === null) try {
|
|
@@ -7601,6 +8052,7 @@ function registerContext(ctx, opts = {}) {
|
|
|
7601
8052
|
const root = findSerenityRoot(agent.session?.header?.cwd ?? process.cwd());
|
|
7602
8053
|
const key = agentKey(agent);
|
|
7603
8054
|
const downstream = await next();
|
|
8055
|
+
if (isSkiffSessionId(key)) return downstream;
|
|
7604
8056
|
if (root) {
|
|
7605
8057
|
try {
|
|
7606
8058
|
syncSafeModeRestriction(agent, root);
|
|
@@ -7631,6 +8083,7 @@ function registerCompactRetention(ctx, opts = {}) {
|
|
|
7631
8083
|
ctx.on("session/event", (session, event) => {
|
|
7632
8084
|
if (event.type !== "compaction/end") return;
|
|
7633
8085
|
if (event.data.error) return;
|
|
8086
|
+
if (isSkiffSessionId(String(session.id ?? ""))) return;
|
|
7634
8087
|
const agent = ctx.agents.get(session.id);
|
|
7635
8088
|
if (!agent) return;
|
|
7636
8089
|
const root = findSerenityRoot(agent.session?.header?.cwd ?? process.cwd());
|
|
@@ -7729,7 +8182,7 @@ function saveFileToTmp(root, fileName, data) {
|
|
|
7729
8182
|
writeFileSync(join(dir, filename), bytes);
|
|
7730
8183
|
return `${FILE_UPLOAD_DIR}/${filename}`;
|
|
7731
8184
|
}
|
|
7732
|
-
function readBody$
|
|
8185
|
+
function readBody$2(req, maxBytes) {
|
|
7733
8186
|
return new Promise((resolve, reject) => {
|
|
7734
8187
|
let data = "";
|
|
7735
8188
|
req.on("data", (chunk) => {
|
|
@@ -7743,7 +8196,7 @@ function readBody$1(req, maxBytes) {
|
|
|
7743
8196
|
req.on("error", reject);
|
|
7744
8197
|
});
|
|
7745
8198
|
}
|
|
7746
|
-
function sendJson(res, code, body) {
|
|
8199
|
+
function sendJson$1(res, code, body) {
|
|
7747
8200
|
const payload = JSON.stringify(body);
|
|
7748
8201
|
res.writeHead(code, {
|
|
7749
8202
|
"content-type": "application/json; charset=utf-8",
|
|
@@ -7781,7 +8234,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
7781
8234
|
handler: async (req, res) => {
|
|
7782
8235
|
try {
|
|
7783
8236
|
if (req.method !== "GET") {
|
|
7784
|
-
sendJson(res, 405, { error: "method not allowed" });
|
|
8237
|
+
sendJson$1(res, 405, { error: "method not allowed" });
|
|
7785
8238
|
return;
|
|
7786
8239
|
}
|
|
7787
8240
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
@@ -7790,12 +8243,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
7790
8243
|
workspace: url.searchParams.get("workspace") ?? void 0
|
|
7791
8244
|
}));
|
|
7792
8245
|
if (!root) {
|
|
7793
|
-
sendJson(res, 200, { handymen: [] });
|
|
8246
|
+
sendJson$1(res, 200, { handymen: [] });
|
|
7794
8247
|
return;
|
|
7795
8248
|
}
|
|
7796
|
-
sendJson(res, 200, { handymen: listActiveHandymen(root) });
|
|
8249
|
+
sendJson$1(res, 200, { handymen: listActiveHandymen(root) });
|
|
7797
8250
|
} catch (err) {
|
|
7798
|
-
sendJson(res, 400, { error: err.message ?? String(err) });
|
|
8251
|
+
sendJson$1(res, 400, { error: err.message ?? String(err) });
|
|
7799
8252
|
}
|
|
7800
8253
|
}
|
|
7801
8254
|
});
|
|
@@ -7805,14 +8258,14 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
7805
8258
|
handler: async (req, res) => {
|
|
7806
8259
|
try {
|
|
7807
8260
|
if (req.method !== "POST") {
|
|
7808
|
-
sendJson(res, 405, { error: "method not allowed" });
|
|
8261
|
+
sendJson$1(res, 405, { error: "method not allowed" });
|
|
7809
8262
|
return;
|
|
7810
8263
|
}
|
|
7811
8264
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
7812
|
-
sendJson(res, 403, { error: "图片落盘仅限 WebUI(client 专用)" });
|
|
8265
|
+
sendJson$1(res, 403, { error: "图片落盘仅限 WebUI(client 专用)" });
|
|
7813
8266
|
return;
|
|
7814
8267
|
}
|
|
7815
|
-
const raw = await readBody$
|
|
8268
|
+
const raw = await readBody$2(req, 20971520);
|
|
7816
8269
|
const body = JSON.parse(raw);
|
|
7817
8270
|
const workspace = resolveWorkspace(ctx, {
|
|
7818
8271
|
sessionId: body.sessionId,
|
|
@@ -7820,12 +8273,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
7820
8273
|
});
|
|
7821
8274
|
const root = findSerenityRoot(workspace);
|
|
7822
8275
|
if (!root) {
|
|
7823
|
-
sendJson(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
8276
|
+
sendJson$1(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
7824
8277
|
return;
|
|
7825
8278
|
}
|
|
7826
|
-
sendJson(res, 200, { path: saveImageToTmp(root, body.mediaType ?? "", body.data ?? "") });
|
|
8279
|
+
sendJson$1(res, 200, { path: saveImageToTmp(root, body.mediaType ?? "", body.data ?? "") });
|
|
7827
8280
|
} catch (err) {
|
|
7828
|
-
sendJson(res, 400, { error: err.message ?? String(err) });
|
|
8281
|
+
sendJson$1(res, 400, { error: err.message ?? String(err) });
|
|
7829
8282
|
}
|
|
7830
8283
|
}
|
|
7831
8284
|
});
|
|
@@ -7835,14 +8288,14 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
7835
8288
|
handler: async (req, res) => {
|
|
7836
8289
|
try {
|
|
7837
8290
|
if (req.method !== "POST") {
|
|
7838
|
-
sendJson(res, 405, { error: "method not allowed" });
|
|
8291
|
+
sendJson$1(res, 405, { error: "method not allowed" });
|
|
7839
8292
|
return;
|
|
7840
8293
|
}
|
|
7841
8294
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
7842
|
-
sendJson(res, 403, { error: "文件落盘仅限 WebUI(client 专用)" });
|
|
8295
|
+
sendJson$1(res, 403, { error: "文件落盘仅限 WebUI(client 专用)" });
|
|
7843
8296
|
return;
|
|
7844
8297
|
}
|
|
7845
|
-
const raw = await readBody$
|
|
8298
|
+
const raw = await readBody$2(req, 20971520);
|
|
7846
8299
|
const body = JSON.parse(raw);
|
|
7847
8300
|
const workspace = resolveWorkspace(ctx, {
|
|
7848
8301
|
sessionId: body.sessionId,
|
|
@@ -7850,12 +8303,12 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
7850
8303
|
});
|
|
7851
8304
|
const root = findSerenityRoot(workspace);
|
|
7852
8305
|
if (!root) {
|
|
7853
|
-
sendJson(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
8306
|
+
sendJson$1(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
7854
8307
|
return;
|
|
7855
8308
|
}
|
|
7856
|
-
sendJson(res, 200, { path: saveFileToTmp(root, body.name ?? "", body.data ?? "") });
|
|
8309
|
+
sendJson$1(res, 200, { path: saveFileToTmp(root, body.name ?? "", body.data ?? "") });
|
|
7857
8310
|
} catch (err) {
|
|
7858
|
-
sendJson(res, 400, { error: err.message ?? String(err) });
|
|
8311
|
+
sendJson$1(res, 400, { error: err.message ?? String(err) });
|
|
7859
8312
|
}
|
|
7860
8313
|
}
|
|
7861
8314
|
});
|
|
@@ -7871,7 +8324,7 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
7871
8324
|
workspace: url.searchParams.get("workspace") ?? void 0
|
|
7872
8325
|
}), configPaths);
|
|
7873
8326
|
const runtime = ctx.get("codeRuntime");
|
|
7874
|
-
sendJson(res, 200, {
|
|
8327
|
+
sendJson$1(res, 200, {
|
|
7875
8328
|
...status,
|
|
7876
8329
|
...runtime === void 0 ? { codeRuntime: null } : { codeRuntime: {
|
|
7877
8330
|
language: runtime.language,
|
|
@@ -7882,10 +8335,10 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
7882
8335
|
}
|
|
7883
8336
|
if (req.method === "POST") {
|
|
7884
8337
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
7885
|
-
sendJson(res, 403, { error: "safe-mode 切换仅限 WebUI(agent 不可自行开关)" });
|
|
8338
|
+
sendJson$1(res, 403, { error: "safe-mode 切换仅限 WebUI(agent 不可自行开关)" });
|
|
7886
8339
|
return;
|
|
7887
8340
|
}
|
|
7888
|
-
const raw = await readBody$
|
|
8341
|
+
const raw = await readBody$2(req, 65536);
|
|
7889
8342
|
const body = JSON.parse(raw);
|
|
7890
8343
|
const workspace = resolveWorkspace(ctx, {
|
|
7891
8344
|
sessionId: body.sessionId,
|
|
@@ -7893,19 +8346,19 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
7893
8346
|
});
|
|
7894
8347
|
const root = findSerenityRoot(workspace);
|
|
7895
8348
|
if (!root) {
|
|
7896
|
-
sendJson(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
8349
|
+
sendJson$1(res, 404, { error: `no CCC found from workspace: ${workspace}` });
|
|
7897
8350
|
return;
|
|
7898
8351
|
}
|
|
7899
8352
|
const result = setSafeMode(root, body.on === true);
|
|
7900
|
-
sendJson(res, 200, {
|
|
8353
|
+
sendJson$1(res, 200, {
|
|
7901
8354
|
...getStatus(workspace, configPaths),
|
|
7902
8355
|
...result
|
|
7903
8356
|
});
|
|
7904
8357
|
return;
|
|
7905
8358
|
}
|
|
7906
|
-
sendJson(res, 405, { error: "method not allowed" });
|
|
8359
|
+
sendJson$1(res, 405, { error: "method not allowed" });
|
|
7907
8360
|
} catch (err) {
|
|
7908
|
-
sendJson(res, 400, { error: err.message ?? String(err) });
|
|
8361
|
+
sendJson$1(res, 400, { error: err.message ?? String(err) });
|
|
7909
8362
|
}
|
|
7910
8363
|
}
|
|
7911
8364
|
});
|
|
@@ -7915,25 +8368,25 @@ function registerStatusApi(ctx, opts = {}) {
|
|
|
7915
8368
|
handler: async (req, res) => {
|
|
7916
8369
|
try {
|
|
7917
8370
|
if (req.headers["x-serenity-ui"] !== "1") {
|
|
7918
|
-
sendJson(res, 403, { error: "高级设定仅限 WebUI(client 专用)" });
|
|
8371
|
+
sendJson$1(res, 403, { error: "高级设定仅限 WebUI(client 专用)" });
|
|
7919
8372
|
return;
|
|
7920
8373
|
}
|
|
7921
8374
|
if (req.method === "GET") {
|
|
7922
|
-
sendJson(res, 200, { config: toWire(readAdvancedSettings()) });
|
|
8375
|
+
sendJson$1(res, 200, { config: toWire(readAdvancedSettings()) });
|
|
7923
8376
|
return;
|
|
7924
8377
|
}
|
|
7925
8378
|
if (req.method === "PUT") {
|
|
7926
|
-
const raw = await readBody$
|
|
8379
|
+
const raw = await readBody$2(req, 131072);
|
|
7927
8380
|
const saved = applyWirePatch(JSON.parse(raw).config ?? {});
|
|
7928
8381
|
try {
|
|
7929
8382
|
ctx.emit?.("serenity/config-updated");
|
|
7930
8383
|
} catch {}
|
|
7931
|
-
sendJson(res, 200, { config: toWire(saved) });
|
|
8384
|
+
sendJson$1(res, 200, { config: toWire(saved) });
|
|
7932
8385
|
return;
|
|
7933
8386
|
}
|
|
7934
|
-
sendJson(res, 405, { error: "method not allowed" });
|
|
8387
|
+
sendJson$1(res, 405, { error: "method not allowed" });
|
|
7935
8388
|
} catch (err) {
|
|
7936
|
-
sendJson(res, 400, { error: err.message ?? String(err) });
|
|
8389
|
+
sendJson$1(res, 400, { error: err.message ?? String(err) });
|
|
7937
8390
|
}
|
|
7938
8391
|
}
|
|
7939
8392
|
});
|
|
@@ -8389,7 +8842,7 @@ function buildProxyHeaders(reqHeaders, mainPort, bodyOverride) {
|
|
|
8389
8842
|
//#endregion
|
|
8390
8843
|
//#region src/gateway.ts
|
|
8391
8844
|
/** 读取请求体(≤ maxBytes;超限 reject) */
|
|
8392
|
-
function readBody(req, maxBytes) {
|
|
8845
|
+
function readBody$1(req, maxBytes) {
|
|
8393
8846
|
return new Promise((resolve, reject) => {
|
|
8394
8847
|
let data = "";
|
|
8395
8848
|
req.on("data", (chunk) => {
|
|
@@ -8522,7 +8975,7 @@ function startGateway(config, getAccounts) {
|
|
|
8522
8975
|
if (session !== void 0) {
|
|
8523
8976
|
if (req.method === "POST" && url.pathname === "/api/workspace.create") {
|
|
8524
8977
|
if (!allowWorkspaceCreate) {
|
|
8525
|
-
readBody(req, 131072).then((body) => {
|
|
8978
|
+
readBody$1(req, 131072).then((body) => {
|
|
8526
8979
|
let rpcId = "unknown";
|
|
8527
8980
|
try {
|
|
8528
8981
|
const parsed = JSON.parse(body);
|
|
@@ -8558,7 +9011,7 @@ function startGateway(config, getAccounts) {
|
|
|
8558
9011
|
});
|
|
8559
9012
|
return;
|
|
8560
9013
|
}
|
|
8561
|
-
readBody(req, 131072).then((body) => {
|
|
9014
|
+
readBody$1(req, 131072).then((body) => {
|
|
8562
9015
|
let rpcId = "unknown";
|
|
8563
9016
|
let path;
|
|
8564
9017
|
try {
|
|
@@ -8839,6 +9292,214 @@ function registerGateway(ctx) {
|
|
|
8839
9292
|
});
|
|
8840
9293
|
}
|
|
8841
9294
|
//#endregion
|
|
9295
|
+
//#region src/skiff-debug.ts
|
|
9296
|
+
/**
|
|
9297
|
+
* skiff-debug.ts — Skiff 调试问答页(F4a',v1.25.0 实验性)
|
|
9298
|
+
*
|
|
9299
|
+
* node:http 调试端口(默认关,仅监听 127.0.0.1;启停 = 人工——设置面板「Serenity」
|
|
9300
|
+
* 页 Skiff 区块开关,不随插件加载自动启动)。
|
|
9301
|
+
*
|
|
9302
|
+
* - GET / → 问答 HTML 页(角色下拉 + 输入框 + 答案区 + 轨迹区 + WebUI 链接)
|
|
9303
|
+
* - POST /ask → {role, question} → 走会话核心(skiff-core)→ {answer, sessionId, trajectory}
|
|
9304
|
+
*
|
|
9305
|
+
* 与 ACP stdio 协议(F4c 后续)共用同一会话核心(createSkiffAgent + askSkiff),
|
|
9306
|
+
* 协议层后加不返工。轨迹 = session.events 结构化返回(与 dsh WebUI 同源数据),
|
|
9307
|
+
* 页面 JS 渲染成对话时间线;同时保留原生 WebUI 会话链接供完整交互。
|
|
9308
|
+
*
|
|
9309
|
+
* 实验性质:未开启时零资源占用(无监听、无 agent 创建)。
|
|
9310
|
+
*/
|
|
9311
|
+
/** 运行中的调试服务(单实例;进程级) */
|
|
9312
|
+
let active = null;
|
|
9313
|
+
function readBody(req) {
|
|
9314
|
+
return new Promise((resolve, reject) => {
|
|
9315
|
+
const chunks = [];
|
|
9316
|
+
req.on("data", (c) => chunks.push(c));
|
|
9317
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
9318
|
+
req.on("error", reject);
|
|
9319
|
+
});
|
|
9320
|
+
}
|
|
9321
|
+
function sendJson(res, status, payload) {
|
|
9322
|
+
const body = JSON.stringify(payload);
|
|
9323
|
+
res.writeHead(status, {
|
|
9324
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
9325
|
+
"Cache-Control": "no-store"
|
|
9326
|
+
});
|
|
9327
|
+
res.end(body);
|
|
9328
|
+
}
|
|
9329
|
+
function sendHtml(res, html) {
|
|
9330
|
+
res.writeHead(200, {
|
|
9331
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
9332
|
+
"Cache-Control": "no-store"
|
|
9333
|
+
});
|
|
9334
|
+
res.end(html);
|
|
9335
|
+
}
|
|
9336
|
+
/** 问答页 HTML:角色下拉 + 输入 + 答案区 + 轨迹区(JS 渲染)+ WebUI 链接 */
|
|
9337
|
+
function skiffDebugPage(roles, webPort) {
|
|
9338
|
+
const roleOptions = [...roles.entries()].map(([name, r]) => `<option value="${escapeHtml(name)}">${escapeHtml(name)}${r.model ? ` (${escapeHtml(r.model)})` : ""}</option>`).join("\n");
|
|
9339
|
+
const webUrl = `http://127.0.0.1:${webPort}`;
|
|
9340
|
+
return `<!DOCTYPE html>
|
|
9341
|
+
<html lang="zh">
|
|
9342
|
+
<head>
|
|
9343
|
+
<meta charset="utf-8">
|
|
9344
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
9345
|
+
<title>Skiff Debug — CCC cognitive subset roles</title>
|
|
9346
|
+
<style>
|
|
9347
|
+
:root { color-scheme: light dark; }
|
|
9348
|
+
body { font-family: system-ui, -apple-system, sans-serif; margin: 0; padding: 24px; background: #f6f7f9; color: #1f2328; }
|
|
9349
|
+
@media (prefers-color-scheme: dark) { body { background: #1a1b1e; color: #e6e6e6; } }
|
|
9350
|
+
main { max-width: 720px; margin: 0 auto; }
|
|
9351
|
+
h1 { font-size: 18px; margin: 0 0 4px; }
|
|
9352
|
+
.sub { opacity: .65; font-size: 13px; margin-bottom: 16px; }
|
|
9353
|
+
label { font-size: 13px; font-weight: 600; display: block; margin: 12px 0 4px; }
|
|
9354
|
+
select, textarea { width: 100%; box-sizing: border-box; padding: 8px 10px; border-radius: 8px; border: 1px solid #d0d7de; background: #fff; color: inherit; font-size: 14px; }
|
|
9355
|
+
@media (prefers-color-scheme: dark) { select, textarea { background: #26282c; border-color: #3a3d42; } }
|
|
9356
|
+
textarea { min-height: 72px; resize: vertical; }
|
|
9357
|
+
button { margin-top: 12px; padding: 9px 18px; border-radius: 8px; border: 0; background: #0ba875; color: #fff; font-size: 14px; font-weight: 600; cursor: pointer; }
|
|
9358
|
+
button:disabled { opacity: .55; cursor: wait; }
|
|
9359
|
+
#answer { white-space: pre-wrap; background: #fff; border: 1px solid #d0d7de; border-radius: 8px; padding: 12px; margin-top: 16px; font-size: 14px; line-height: 1.55; min-height: 48px; }
|
|
9360
|
+
@media (prefers-color-scheme: dark) { #answer { background: #26282c; border-color: #3a3d42; } }
|
|
9361
|
+
#trajectory { margin-top: 12px; font-size: 13px; }
|
|
9362
|
+
.t-entry { border-left: 2px solid #d0d7de; padding: 6px 10px; margin: 6px 0; border-radius: 0 6px 6px 0; background: rgba(127,127,127,.06); white-space: pre-wrap; word-break: break-word; }
|
|
9363
|
+
.t-user { border-left-color: #0ba875; }
|
|
9364
|
+
.t-tool { border-left-color: #d29922; opacity: .85; font-family: ui-monospace, monospace; font-size: 12px; }
|
|
9365
|
+
.t-role { font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; opacity: .6; margin-bottom: 2px; }
|
|
9366
|
+
.muted { opacity: .6; font-size: 13px; }
|
|
9367
|
+
a { color: #0ba875; }
|
|
9368
|
+
.err { color: #cf222e; white-space: pre-wrap; }
|
|
9369
|
+
</style>
|
|
9370
|
+
</head>
|
|
9371
|
+
<body>
|
|
9372
|
+
<main>
|
|
9373
|
+
<h1>Skiff Debug</h1>
|
|
9374
|
+
<div class="sub">宁静号 trajectory 子集角色问答页(v1.25.0 实验性)— 走 DSH agent-loop 会话核心,轨迹与 WebUI 同源</div>
|
|
9375
|
+
<label for="role">角色</label>
|
|
9376
|
+
<select id="role">${roleOptions || "<option value=\"\">(未配置角色)</option>"}</select>
|
|
9377
|
+
<label for="q">问题</label>
|
|
9378
|
+
<textarea id="q" placeholder="向该角色提问…"></textarea>
|
|
9379
|
+
<button id="ask">提问</button>
|
|
9380
|
+
<div id="answer" class="muted">等待提问…</div>
|
|
9381
|
+
<div id="trajectory"></div>
|
|
9382
|
+
<p class="muted"><a href="${webUrl}" target="_blank" rel="noopener">在 dsh WebUI 查看完整会话</a>(会话列表搜索 sessionId;WebUI 有完整交互)</p>
|
|
9383
|
+
</main>
|
|
9384
|
+
<script>
|
|
9385
|
+
const btn = document.getElementById('ask')
|
|
9386
|
+
const answer = document.getElementById('answer')
|
|
9387
|
+
const traj = document.getElementById('trajectory')
|
|
9388
|
+
const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]))
|
|
9389
|
+
btn.addEventListener('click', async () => {
|
|
9390
|
+
const role = document.getElementById('role').value
|
|
9391
|
+
const q = document.getElementById('q').value.trim()
|
|
9392
|
+
if (!role || !q) { answer.className = 'err'; answer.textContent = '请选择角色并输入问题'; return }
|
|
9393
|
+
btn.disabled = true
|
|
9394
|
+
answer.className = 'muted'
|
|
9395
|
+
answer.textContent = '运行中…'
|
|
9396
|
+
traj.innerHTML = ''
|
|
9397
|
+
try {
|
|
9398
|
+
const res = await fetch('/ask', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ role, question: q }) })
|
|
9399
|
+
const data = await res.json()
|
|
9400
|
+
if (!res.ok) throw new Error(data.error || ('HTTP ' + res.status))
|
|
9401
|
+
answer.className = ''
|
|
9402
|
+
answer.textContent = data.answer || '(空回答)'
|
|
9403
|
+
renderTrajectory(data.trajectory || [], data.sessionId || '')
|
|
9404
|
+
} catch (err) {
|
|
9405
|
+
answer.className = 'err'
|
|
9406
|
+
answer.textContent = String(err.message || err)
|
|
9407
|
+
} finally {
|
|
9408
|
+
btn.disabled = false
|
|
9409
|
+
}
|
|
9410
|
+
})
|
|
9411
|
+
function renderTrajectory(entries, sessionId) {
|
|
9412
|
+
if (entries.length === 0) { traj.innerHTML = '<div class="muted">(本轮无轨迹)</div>'; return }
|
|
9413
|
+
traj.innerHTML = '<div class="sub">本轮轨迹 · ' + esc(sessionId) + '</div>' + entries.map((e) => {
|
|
9414
|
+
const cls = e.role === 'user' ? 't-user' : (e.role === 'tool' ? 't-tool' : '')
|
|
9415
|
+
const role = e.role === 'tool' ? 'tool' + (e.tool ? ' · ' + esc(e.tool) : '') : e.role
|
|
9416
|
+
return '<div class="t-entry ' + cls + '"><div class="t-role">' + role + '</div>' + esc(e.text) + '</div>'
|
|
9417
|
+
}).join('')
|
|
9418
|
+
}
|
|
9419
|
+
<\/script>
|
|
9420
|
+
</body>
|
|
9421
|
+
</html>`;
|
|
9422
|
+
}
|
|
9423
|
+
function escapeHtml(s) {
|
|
9424
|
+
return s.replace(/[&<>"']/g, (c) => ({
|
|
9425
|
+
"&": "&",
|
|
9426
|
+
"<": "<",
|
|
9427
|
+
">": ">",
|
|
9428
|
+
"\"": """,
|
|
9429
|
+
"'": "'"
|
|
9430
|
+
})[c] ?? c);
|
|
9431
|
+
}
|
|
9432
|
+
/**
|
|
9433
|
+
* 启动调试问答服务(单实例;重复启动幂等返回既有实例)。
|
|
9434
|
+
* @param root 当前 CCC 根(角色配置读取 + skiff agent cwd)
|
|
9435
|
+
* @param port 调试端口(仅 127.0.0.1)
|
|
9436
|
+
* @param webPort 主 WebUI 端口(WebUI 链接)
|
|
9437
|
+
*/
|
|
9438
|
+
async function startSkiffDebugServer(ctx, root, port, webPort) {
|
|
9439
|
+
if (active) return;
|
|
9440
|
+
const roles = readSkiffRoles(root);
|
|
9441
|
+
const server = createServer((req, res) => {
|
|
9442
|
+
handle(ctx, root, roles, webPort, req, res);
|
|
9443
|
+
});
|
|
9444
|
+
await new Promise((resolve, reject) => {
|
|
9445
|
+
server.once("error", reject);
|
|
9446
|
+
server.listen(port, "127.0.0.1", () => resolve());
|
|
9447
|
+
});
|
|
9448
|
+
active = {
|
|
9449
|
+
server,
|
|
9450
|
+
port
|
|
9451
|
+
};
|
|
9452
|
+
console.log(`[serenity-hooks] ✓ Skiff 调试问答页: http://127.0.0.1:${port}(角色: ${roles.size},WebUI: ${webPort})`);
|
|
9453
|
+
}
|
|
9454
|
+
function stopSkiffDebugServer() {
|
|
9455
|
+
if (!active) return;
|
|
9456
|
+
try {
|
|
9457
|
+
active.server.close();
|
|
9458
|
+
} catch {}
|
|
9459
|
+
active = null;
|
|
9460
|
+
}
|
|
9461
|
+
async function handle(ctx, root, roles, webPort, req, res) {
|
|
9462
|
+
try {
|
|
9463
|
+
const url = (req.url ?? "/").split("?")[0] ?? "/";
|
|
9464
|
+
if (req.method === "GET" && url === "/") {
|
|
9465
|
+
sendHtml(res, skiffDebugPage(roles, webPort));
|
|
9466
|
+
return;
|
|
9467
|
+
}
|
|
9468
|
+
if (req.method === "POST" && url === "/ask") {
|
|
9469
|
+
let body;
|
|
9470
|
+
try {
|
|
9471
|
+
const parsed = JSON.parse(await readBody(req));
|
|
9472
|
+
if (parsed === null || typeof parsed !== "object") throw new Error("not an object");
|
|
9473
|
+
body = parsed;
|
|
9474
|
+
} catch {
|
|
9475
|
+
sendJson(res, 400, { error: "invalid JSON body" });
|
|
9476
|
+
return;
|
|
9477
|
+
}
|
|
9478
|
+
const roleName = typeof body.role === "string" ? body.role : "";
|
|
9479
|
+
const question = typeof body.question === "string" ? body.question : "";
|
|
9480
|
+
const role = roles.get(roleName);
|
|
9481
|
+
if (!roleName || !role) {
|
|
9482
|
+
sendJson(res, 400, { error: `unknown role: ${roleName}` });
|
|
9483
|
+
return;
|
|
9484
|
+
}
|
|
9485
|
+
if (!question.trim()) {
|
|
9486
|
+
sendJson(res, 400, { error: "empty question" });
|
|
9487
|
+
return;
|
|
9488
|
+
}
|
|
9489
|
+
const result = await askSkiff(ctx, (await createSkiffAgent(ctx, root, roleName, role, readHandymanConfig(root)?.defaultModel)).agent, question);
|
|
9490
|
+
sendJson(res, 200, {
|
|
9491
|
+
answer: result.answer,
|
|
9492
|
+
sessionId: result.sessionId,
|
|
9493
|
+
trajectory: result.trajectory
|
|
9494
|
+
});
|
|
9495
|
+
return;
|
|
9496
|
+
}
|
|
9497
|
+
sendJson(res, 404, { error: "not found" });
|
|
9498
|
+
} catch (err) {
|
|
9499
|
+
sendJson(res, 500, { error: err?.message ?? String(err) });
|
|
9500
|
+
}
|
|
9501
|
+
}
|
|
9502
|
+
//#endregion
|
|
8842
9503
|
//#region src/index.ts
|
|
8843
9504
|
const name = "dsh-serenity-hooks";
|
|
8844
9505
|
/** 主动调用的服务;其余(agent 事件)随 harness 装配必然存在 */
|
|
@@ -8870,7 +9531,11 @@ const Config = z.object({
|
|
|
8870
9531
|
enabled: z.boolean().default(true),
|
|
8871
9532
|
thresholdRatio: z.number().min(.01).max(1).default(.9)
|
|
8872
9533
|
}),
|
|
8873
|
-
naming: z.object({ enabled: z.boolean().default(true) })
|
|
9534
|
+
naming: z.object({ enabled: z.boolean().default(true) }),
|
|
9535
|
+
skiff: z.object({
|
|
9536
|
+
enabled: z.boolean().default(false),
|
|
9537
|
+
debugPort: z.number().min(1024).max(65535).default(3099)
|
|
9538
|
+
})
|
|
8874
9539
|
});
|
|
8875
9540
|
function apply(ctx, config) {
|
|
8876
9541
|
if (config.tools) {
|
|
@@ -8885,6 +9550,7 @@ function apply(ctx, config) {
|
|
|
8885
9550
|
ctx.tools.register(createHandymanTool(ctx));
|
|
8886
9551
|
ctx.tools.register(createRebuildTool(ctx));
|
|
8887
9552
|
ctx.tools.register(localstoreTool);
|
|
9553
|
+
ctx.tools.register(skiffAdminTool);
|
|
8888
9554
|
}
|
|
8889
9555
|
if (config.guards) registerGuards(ctx, { configPaths: config.serenityConfigPaths });
|
|
8890
9556
|
if (config.keeper) registerKeeper(ctx, {
|
|
@@ -8907,6 +9573,63 @@ function apply(ctx, config) {
|
|
|
8907
9573
|
if (config.env) registerEnv(ctx);
|
|
8908
9574
|
if (config.opencodeSkills) registerOpencodeSkills(ctx);
|
|
8909
9575
|
registerBootstrap(ctx);
|
|
9576
|
+
registerSkiff(ctx);
|
|
9577
|
+
}
|
|
9578
|
+
/**
|
|
9579
|
+
* F4 Skiff 调试服务装配:启停 = 人工(设置面板 Skiff 区块开关,settings 持久化)。
|
|
9580
|
+
* settings-changed 事件触发同步(skiffEnabled 开 → 启动调试服务;关 → 停止)。
|
|
9581
|
+
* 角色配置(skiff.roles)从当前 CCC 根读取(进程 cwd 优先,live 会话兜底)。
|
|
9582
|
+
*/
|
|
9583
|
+
function registerSkiff(ctx) {
|
|
9584
|
+
let started = false;
|
|
9585
|
+
const sync = () => {
|
|
9586
|
+
const s = readSimpleSettings();
|
|
9587
|
+
if (s.skiffEnabled && !started) {
|
|
9588
|
+
const root = resolveSkiffRoot(ctx);
|
|
9589
|
+
if (!root) {
|
|
9590
|
+
console.warn("[serenity-hooks] ✗ Skiff 调试服务未启动:无法定位 CCC root(进程 cwd 与 live 会话均无 .serenity)");
|
|
9591
|
+
return;
|
|
9592
|
+
}
|
|
9593
|
+
const webPort = readWebPort(ctx);
|
|
9594
|
+
startSkiffDebugServer(ctx, root, s.skiffDebugPort, webPort).then(() => {
|
|
9595
|
+
started = true;
|
|
9596
|
+
}).catch((err) => {
|
|
9597
|
+
console.error(`[serenity-hooks] ✗ Skiff 调试服务启动失败: ${String(err?.message ?? err)}`);
|
|
9598
|
+
});
|
|
9599
|
+
} else if (!s.skiffEnabled && started) {
|
|
9600
|
+
stopSkiffDebugServer();
|
|
9601
|
+
started = false;
|
|
9602
|
+
}
|
|
9603
|
+
};
|
|
9604
|
+
try {
|
|
9605
|
+
ctx.on("serenity/settings-changed", sync);
|
|
9606
|
+
} catch {}
|
|
9607
|
+
sync();
|
|
9608
|
+
}
|
|
9609
|
+
/** 解析当前 CCC 根:进程 cwd 上溯 .serenity 优先,live 会话 cwd 兜底 */
|
|
9610
|
+
function resolveSkiffRoot(ctx) {
|
|
9611
|
+
const fromCwd = findSerenityRoot(process.cwd());
|
|
9612
|
+
if (fromCwd) return fromCwd;
|
|
9613
|
+
try {
|
|
9614
|
+
const sessions = ctx.sessions;
|
|
9615
|
+
for (const s of sessions?.list?.() ?? []) {
|
|
9616
|
+
const cwd = s?.header?.cwd;
|
|
9617
|
+
if (typeof cwd === "string") {
|
|
9618
|
+
const r = findSerenityRoot(cwd);
|
|
9619
|
+
if (r) return r;
|
|
9620
|
+
}
|
|
9621
|
+
}
|
|
9622
|
+
} catch {}
|
|
9623
|
+
return null;
|
|
9624
|
+
}
|
|
9625
|
+
/** 主 WebUI 端口(WebUI 链接;webServer 未装配回退 3080) */
|
|
9626
|
+
function readWebPort(ctx) {
|
|
9627
|
+
try {
|
|
9628
|
+
const ws = ctx.webServer;
|
|
9629
|
+
return typeof ws?.port === "number" ? ws.port : 3080;
|
|
9630
|
+
} catch {
|
|
9631
|
+
return 3080;
|
|
9632
|
+
}
|
|
8910
9633
|
}
|
|
8911
9634
|
//#endregion
|
|
8912
9635
|
export { Config, apply, inject, name };
|