@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/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$8(exec) {
513
+ function agentCwd$9(exec) {
514
514
  return exec.agent?.session?.header?.cwd ?? process.cwd();
515
515
  }
516
- function renderText$10(value) {
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$10(value)
588
+ render: (args, value) => renderText$11(value)
589
589
  },
590
590
  async execute(args, exec) {
591
- const root = findSerenityRoot(agentCwd$8(exec));
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$7(exec) {
1071
+ function agentCwd$8(exec) {
960
1072
  return exec.agent?.session?.header?.cwd ?? process.cwd();
961
1073
  }
962
- function renderText$9(value) {
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$9(value)
1097
+ render: (args, value) => renderText$10(value)
986
1098
  },
987
1099
  async execute(args, exec) {
988
- return await runKit(findSerenityRoot(agentCwd$7(exec)), args);
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$6(exec) {
1560
+ function agentCwd$7(exec) {
1449
1561
  return exec.agent?.session?.header?.cwd ?? process.cwd();
1450
1562
  }
1451
- function renderText$8(value) {
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$8(value)
1602
+ render: (args, value) => renderText$9(value)
1491
1603
  },
1492
1604
  async execute(args, exec) {
1493
- const root = findSerenityRoot(agentCwd$6(exec));
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/tools/msm.ts
2170
+ //#region src/handyman-ops.ts
2059
2171
  /**
2060
- * msm.ts — acc_msm 真实 DSH 工具定义(defineTool)
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
- function agentCwd$5(exec) {
2063
- return exec.agent?.session?.header?.cwd ?? process.cwd();
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 renderText$7(value) {
2066
- return [{
2067
- type: "text",
2068
- text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
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
- const msmTool = defineTool({
2072
- name: "acc_msm",
2073
- 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.",
2074
- parameters: {
2075
- action: {
2076
- type: "string",
2077
- enum: [...MSM_ACTIONS],
2078
- required: true,
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
- //#endregion
2126
- //#region src/tools/eap.ts
2127
- /**
2128
- * eap.ts — EAP 认知质量框架工具(渐进式披露,ACC 标准工具化)
2129
- *
2130
- * 内嵌框架内容(自包含,不依赖已安装技能);可选 section 参数聚焦某原则。
2131
- */
2132
- const EAP_CONTENT = `# EAP Cognitive Quality Framework (Explicit Abstraction Principle)
2133
-
2134
- > "The functional value of a thought is proportional to its external reconstructability."
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
- ## Three Variables
2137
- | Variable | Meaning | How to improve |
2138
- |----------|---------|----------------|
2139
- | E↑ Explicitness | Degree to which variables/entities/relations are clearly defined | Define variables, state relationship direction & cardinality, draw boundaries |
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
- ## Pre-Output Self-Check Checklist
2144
- - [ ] Variables/entities clearly defined (E↑)
2145
- - [ ] Relationships state direction/cardinality (E↑)
2146
- - [ ] Boundaries drawn — what is in scope / what is not (E↑)
2147
- - [ ] No ambiguous words: "handle" "optimize" "problem" → be specific (E↑)
2148
- - [ ] Key decisions record rationale and alternatives (R↓)
2149
- - [ ] No level-skipping — align the upper layer before descending (R↓)
2150
- - [ ] Structures can be regenerated repeatably (S↑)
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
- ## Relationship with ACC
2153
- ACC (plugin/template) encodes structure as code (E↑); generating a CCC from ACC is deterministic (R↓); consistent across multiple CCCs (S↑).
2154
- CCC (home-serenity etc.) encodes cognitive content as skills/SESSIONs/design docs. Use this checklist to self-check outputs.
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
- ## Reference
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
- const eapTool = defineTool({
2165
- name: "eap",
2166
- 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.",
2167
- parameters: { section: {
2168
- type: "string",
2169
- enum: [
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$6(value)
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$5(value) {
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$5(value)
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
- ## Completion criteria (osp loop standard)
2518
- - 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)
2519
- - Automatic restart on abnormal agent stop (≤100 restarts, anti-infinite-loop)
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
- ## Waiting UI
2522
- - The WebUI session-header Serenity detail card shows running handymen's progress (label / round / last response), one line per parallel job, ~3s refresh
2523
- `;
2524
- /** 列出 AGENT_SESSIONS/handyman-*.json 的全部进度(按 updated 倒序;坏文件跳过) */
2525
- function listActiveHandymen(root) {
2526
- const dir = join(root, "AGENT_SESSIONS");
2527
- if (!existsSync(dir)) return [];
2528
- const out = [];
2529
- for (const entry of readdirSync(dir)) {
2530
- if (!entry.startsWith("handyman-") || !entry.endsWith(".json")) continue;
2531
- try {
2532
- const data = JSON.parse(readFileSync(join(dir, entry), "utf-8"));
2533
- if (typeof data.label !== "string" || typeof data.round !== "number") continue;
2534
- out.push({
2535
- label: data.label,
2536
- round: data.round,
2537
- done: data.done === true,
2538
- model: typeof data.model === "string" ? data.model : "",
2539
- updated: typeof data.updated === "string" ? data.updated : "",
2540
- lastResponse: typeof data.lastResponse === "string" ? data.lastResponse : ""
2541
- });
2542
- } catch {}
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
- out.sort((a, b) => a.updated < b.updated ? 1 : -1);
2545
- return out;
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$4(exec) {
2866
+ function agentCwd$5(exec) {
2570
2867
  return (exec.agent?.session)?.header?.cwd ?? process.cwd();
2571
2868
  }
2572
- function renderText$3(value) {
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$3(value)
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$4(exec));
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$3(exec) {
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$2(value) {
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$2(value)
6068
+ render: (args, value) => renderText$3(value)
5766
6069
  },
5767
6070
  async execute(args, exec) {
5768
- const root = findSerenityRoot(agentCwd$3(exec));
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$2(exec) {
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$1(value) {
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$1(value)
6680
+ render: (args, value) => renderText$2(value)
6378
6681
  },
6379
6682
  async execute(args, exec) {
6380
- const root = findSerenityRoot(agentCwd$2(exec));
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$2(exec),
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$1(exec) {
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
- return runLocalStore(root, args);
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
- if (!findSerenityRoot(exec.agent?.session?.header?.cwd ?? process.cwd())) return next();
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 base = serenitySystemPrompt(root, agentScope$1(context));
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$1(req, maxBytes) {
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$1(req, 20971520);
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$1(req, 20971520);
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$1(req, 65536);
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$1(req, 131072);
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) => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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
+ "&": "&amp;",
9426
+ "<": "&lt;",
9427
+ ">": "&gt;",
9428
+ "\"": "&quot;",
9429
+ "'": "&#39;"
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 };