@shgroup/dsh-serenity-hooks 1.26.16 → 1.27.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.
@@ -0,0 +1,13 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __defProp = Object.defineProperty;
3
+ var __exportAll = (all, no_symbols) => {
4
+ let target = {};
5
+ for (var name in all) __defProp(target, name, {
6
+ get: all[name],
7
+ enumerable: true
8
+ });
9
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
10
+ return target;
11
+ };
12
+ //#endregion
13
+ export { __exportAll as t };
@@ -0,0 +1,424 @@
1
+ import { a as resolveRoleSystemPrompt, i as readSkiffRoles, n as buildSkiffBasePrompt, o as roleMsmWhitelist, r as isSkiffSessionId, u as trajectorySubset } from "./skiff-role-Dw7UKCUm.js";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
5
+ import { randomBytes, randomUUID } from "node:crypto";
6
+ //#region src/skiff-registry.ts
7
+ const skiffSessions = /* @__PURE__ */ new Map();
8
+ /** 查 sessionId 的 Skiff 角色名(无 → null)——向后兼容(guards/seams 依赖) */
9
+ function skiffRoleFor$1(sessionId) {
10
+ return skiffSessions.get(sessionId)?.role ?? null;
11
+ }
12
+ /** 查 sessionId 的完整绑定(role + ccc;无 → null)——v1.25.10 会话追问校验 */
13
+ function skiffSessionInfo$1(sessionId) {
14
+ return skiffSessions.get(sessionId) ?? null;
15
+ }
16
+ function registerSkiffSession$1(sessionId, role, ccc) {
17
+ skiffSessions.set(sessionId, {
18
+ role,
19
+ ccc
20
+ });
21
+ }
22
+ function unregisterSkiffSession$1(sessionId) {
23
+ skiffSessions.delete(sessionId);
24
+ }
25
+ /** 测试/调试:注册表快照(sessionId → {role, ccc}) */
26
+ function skiffSessionSnapshot$1() {
27
+ return new Map(skiffSessions);
28
+ }
29
+ //#endregion
30
+ //#region src/handyman-ops.ts
31
+ /**
32
+ * handyman-ops.ts — handyman(杂工)纯逻辑层(零 DSH 依赖,可独立单测)
33
+ *
34
+ * v1.24.0:loop(牛马)→ handyman(杂工)重命名。语义对齐 osp loop:
35
+ * 进度文件(handyman-<label>.md/.json)、续跑、轮次 prompt 结构、stop token。
36
+ * 不兼容旧 loop- 进度文件(用户拍板:仅新 handyman- 前缀)。
37
+ */
38
+ /** label 脱敏(Windows 审计问题 17):非法字符 → '-',去尾点/空格,限长(按码点截断,修复代理对切散 U+FFFD) */
39
+ function sanitizeLabel(label) {
40
+ return [...label.replace(/[<>:"/\\|?*\u0000-\u001f]/g, "-").replace(/[ .]+$/g, "")].slice(0, 50).join("");
41
+ }
42
+ function handymanProgressPaths(root, label) {
43
+ const dir = join(root, "AGENT_SESSIONS");
44
+ const safe = sanitizeLabel(label);
45
+ return {
46
+ md: join(dir, `handyman-${safe}.md`),
47
+ json: join(dir, `handyman-${safe}.json`)
48
+ };
49
+ }
50
+ /** 读取进度(续跑);无文件返回 round 0 */
51
+ function readProgress(root, label) {
52
+ const { json } = handymanProgressPaths(root, label);
53
+ if (!existsSync(json)) return null;
54
+ try {
55
+ return JSON.parse(readFileSync(json, "utf-8"));
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+ function writeProgress(root, label, p) {
61
+ const { md, json } = handymanProgressPaths(root, label);
62
+ mkdirSync(join(root, "AGENT_SESSIONS"), { recursive: true });
63
+ writeFileSync(json, JSON.stringify({
64
+ ...p,
65
+ status: p.status ?? "running",
66
+ updated: (/* @__PURE__ */ new Date()).toISOString()
67
+ }, null, 2) + "\n", "utf-8");
68
+ const lines = [
69
+ `# handyman: ${label}`,
70
+ `- Model: ${p.model}`,
71
+ `- Round: ${p.round}`,
72
+ `- Done: ${p.done}`,
73
+ "",
74
+ `## Latest response`,
75
+ "",
76
+ p.lastResponse,
77
+ ""
78
+ ];
79
+ writeFileSync(md, lines.join("\n"), "utf-8");
80
+ }
81
+ /** 失败状态落盘(对齐 osp writeFailedStatus:done=true / status=failed / errorCode) */
82
+ function writeFailedStatus(root, label, info) {
83
+ const { json } = handymanProgressPaths(root, label);
84
+ mkdirSync(join(root, "AGENT_SESSIONS"), { recursive: true });
85
+ const prev = readProgress(root, label);
86
+ writeFileSync(json, JSON.stringify({
87
+ round: prev?.round ?? 0,
88
+ done: true,
89
+ label,
90
+ model: prev?.model ?? "",
91
+ status: "failed",
92
+ errorCode: info.errorCode,
93
+ errorMessage: info.errorMessage,
94
+ updated: (/* @__PURE__ */ new Date()).toISOString(),
95
+ lastResponse: prev?.lastResponse ?? ""
96
+ }, null, 2) + "\n", "utf-8");
97
+ }
98
+ function newStopToken() {
99
+ return `SERENITY_HANDYMAN_DONE_${randomBytes(8).toString("hex")}`;
100
+ }
101
+ /** 解析 model 字符串(provider/model)→ {provider, model};无 / 视为 model-only */
102
+ function splitModel(model) {
103
+ const idx = model.indexOf("/");
104
+ if (idx < 0) return {
105
+ provider: void 0,
106
+ model
107
+ };
108
+ return {
109
+ provider: model.slice(0, idx),
110
+ model: model.slice(idx + 1)
111
+ };
112
+ }
113
+ /** 校验模型在白名单内;不在 → 抛错(用户拍板:只能使用 CCC 配置的模型) */
114
+ function requireWhitelistedModel(model, models) {
115
+ 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(", ")}`);
116
+ }
117
+ /** 轮次 prompt(对齐老 loop 结构:回顾进度 → 自由工作 → 汇报;S134 EAP 化:固定详尽) */
118
+ function buildRoundPrompt(opts) {
119
+ const { root, session, label, round, stopToken, progress, task } = opts;
120
+ 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.";
121
+ return `# ${label} — handyman round ${round}
122
+
123
+ CCC root: ${root}
124
+ ${session ? `Work session: ${session} (progress recorded in AGENT_SESSIONS/${session}/SESSION.md)` : ""}
125
+ ${task ? `Task: ${task}` : `Task: follow the work corresponding to label "${label}" (if a work session exists, read SESSION.md first to clarify the goal)`}
126
+ ${resumeNote}
127
+
128
+ ## Work rules (fixed every round, must follow)
129
+ 1. Work freely within this round: read files, modify code, execute commands — use every means to advance the task.
130
+ 2. If this task is **reading/curating or text-writing work** (extracting from files, summarizing, writing docs, generating text, etc.),
131
+ first load eap (acc-eap skill) and organize output per the EAP standard:
132
+ - E↑ Explicit: entities/variables clearly defined, relationships with direction and cardinality, boundaries drawn, no ambiguous words
133
+ - R↓ Reconstructable: key conclusions record sources and reasoning, rebuildable by later agents
134
+ - S↑ Stable: output structure regenerates repeatably, no reliance on implicit context
135
+ 3. Reports must be concrete and verifiable — no filler.
136
+
137
+ ## Per-round report (fixed format, answer each item)
138
+ 1. What was done this round (concrete)
139
+ 2. Next-step plan
140
+ 3. Whether the task is complete (if complete, output only ${stopToken})
141
+
142
+ If the task is complete, output only ${stopToken}.`;
143
+ }
144
+ /**
145
+ * handyman 规模化使用指引(guide 子命令输出;S134 继承 + v1.24.0 更新):
146
+ * 使用 handyman 前必须先加载 eap 设计方案;并行策略(jobs 编排);提示词规范(详尽固定 EAP);
147
+ * 阅读/文字编写类 handyman 内部也加载 eap。
148
+ */
149
+ const HANDYMAN_GUIDE = `# handyman — Scale-Up Usage Guide (guide)
150
+
151
+ ## ⚠️ Before using: load eap and design the plan
152
+ Before calling handyman, load eap (acc-eap skill) and design the "scale-up handyman plan" based on the EAP framework.
153
+
154
+ ### 1. Task decomposition (E↑ Explicit)
155
+ - Split large tasks into explicit subtasks: each subtask defines goal / input / boundaries (what to do, what not to do) / acceptance criteria
156
+ - Make dependencies explicit: dependent tasks run serially, independent ones can run in parallel
157
+
158
+ ### 2. Prompt design (handyman's task parameter)
159
+ - task must be detailed, fixed, and EAP-compliant: clear goal, drawn boundaries, decidable acceptance criteria
160
+ - Anti-example "handle this file" — ambiguous; good example "read <path>, extract all rows of the「关键决策」table,
161
+ output a JSON array (fields id/conclusion/evidence), do not modify the original file"
162
+ - Reading/curating or text-writing work (extracting from files, summarizing, writing docs, generating text, etc.):
163
+ the handyman-internal agent is also required to load eap and organize output per the EAP standard
164
+
165
+ ### 3. Model whitelist (CCC-configured, mandatory)
166
+ - handyman only uses models listed in .opencode/serenity.json "handyman.models" — never arbitrary models
167
+ - Recursive subagents inside a handyman inherit the handyman's model automatically (DSH native)
168
+ - Keep the subagent tool instance free of a fixed agentOptions, or model inheritance breaks
169
+
170
+ ### 4. Parallel strategy (jobs orchestration, workflow capability)
171
+ - Independent subtasks can run in parallel via handyman(jobs=[...]): each job gets its own label + task + stop token + progress file
172
+ - Concurrency safety guaranteed: unique sessionId (handyman-<label>-<uuid>), progress files isolated per label
173
+ (AGENT_SESSIONS/handyman-<label>.json) — same label resumes, different labels never interfere
174
+ - Parallel cap: handyman.maxParallel (default 10 — cheap models are cheap)
175
+ - Aggregation: after each parallel job produces progress, the main agent merges (or spawns one aggregation handyman)
176
+ - For programmable pipeline/phase orchestration at scale, use the platform's workflow tool instead
177
+
178
+ ## Completion criteria (osp loop standard)
179
+ - 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)
180
+ - Automatic restart on abnormal agent stop (≤100 restarts, anti-infinite-loop)
181
+
182
+ ## Waiting UI
183
+ - The WebUI session-header Serenity detail card shows running handymen's progress (label / round / last response), one line per parallel job, ~3s refresh
184
+ `;
185
+ /** 列出 AGENT_SESSIONS/handyman-*.json 的全部进度(按 updated 倒序;坏文件跳过) */
186
+ function listActiveHandymen(root) {
187
+ const dir = join(root, "AGENT_SESSIONS");
188
+ if (!existsSync(dir)) return [];
189
+ const out = [];
190
+ for (const entry of readdirSync(dir)) {
191
+ if (!entry.startsWith("handyman-") || !entry.endsWith(".json")) continue;
192
+ try {
193
+ const data = JSON.parse(readFileSync(join(dir, entry), "utf-8"));
194
+ if (typeof data.label !== "string" || typeof data.round !== "number") continue;
195
+ out.push({
196
+ label: data.label,
197
+ round: data.round,
198
+ done: data.done === true,
199
+ model: typeof data.model === "string" ? data.model : "",
200
+ updated: typeof data.updated === "string" ? data.updated : "",
201
+ lastResponse: typeof data.lastResponse === "string" ? data.lastResponse : ""
202
+ });
203
+ } catch {}
204
+ }
205
+ out.sort((a, b) => a.updated < b.updated ? 1 : -1);
206
+ return out;
207
+ }
208
+ //#endregion
209
+ //#region src/skiff-core.ts
210
+ const PLUGIN_SOURCE = {
211
+ kind: "plugin",
212
+ plugin: "dsh-serenity-hooks"
213
+ };
214
+ const skiffAgents = /* @__PURE__ */ new Map();
215
+ /** 查 sessionId 的 Skiff 角色名(无 → null) */
216
+ function skiffRoleFor(sessionId) {
217
+ return skiffRoleFor$1(sessionId);
218
+ }
219
+ /** 查 sessionId 的会话绑定(role + ccc;无 → null)——v1.25.10 追问校验 */
220
+ function skiffSessionInfo(sessionId) {
221
+ return skiffSessionInfo$1(sessionId);
222
+ }
223
+ /** 查 sessionId 的活体 agent(进程内会话延续用;未注册/已清理 → undefined) */
224
+ function getSkiffAgent(sessionId) {
225
+ return skiffAgents.get(sessionId);
226
+ }
227
+ function registerSkiffSession(sessionId, role, ccc, agent) {
228
+ skiffAgents.set(sessionId, agent);
229
+ registerSkiffSession$1(sessionId, role, ccc);
230
+ }
231
+ function unregisterSkiffSession(sessionId) {
232
+ skiffAgents.delete(sessionId);
233
+ unregisterSkiffSession$1(sessionId);
234
+ }
235
+ /** 测试/调试:注册表快照(sessionId → {role} 兼容展示) */
236
+ function skiffSessionSnapshot() {
237
+ const out = /* @__PURE__ */ new Map();
238
+ for (const [id, b] of skiffSessionSnapshot$1()) out.set(id, {
239
+ role: b.role,
240
+ ccc: b.ccc
241
+ });
242
+ return out;
243
+ }
244
+ /** Skiff agent 挂载的 DSH preset(v1.25.3 修复:read/grep/glob 等平台工具由 preset 决定工具面;
245
+ * handyman 经 composeFrom 继承父、skiff 无父上下文——直接挂 DSH 默认 standard preset;
246
+ * guard 角色白名单再按角色过滤可见/可用面——白名单外工具仍 deny) */
247
+ const SKIFF_PRESET = "standard";
248
+ /**
249
+ * 创建 Skiff agent:标准 DSH agent + cwd=CCC root + 角色模型 +
250
+ * standard preset(平台工具面)+ scoped 系统提示词(基础提示词 + CCC 定义段,全替换 ACC 默认注入)。
251
+ * @param sessionId 指定会话 id(可选;微信桥等外部面用固定 id 实现用户↔会话长期映射——
252
+ * 不传则随机生成 skiff-<role>-<uuid>,ACP/调试页默认路径)
253
+ */
254
+ async function createSkiffAgent(ctx, root, roleName, role, defaultModel, sessionId) {
255
+ if (!ctx.agents) throw new Error("skiff: ctx.agents unavailable");
256
+ const model = role.model?.trim() || defaultModel || "";
257
+ const id = sessionId ?? `skiff-${roleName}-${randomUUID()}`;
258
+ const handle = await ctx.agents.create({
259
+ sessionId: id,
260
+ meta: {
261
+ cwd: root,
262
+ agentPreset: SKIFF_PRESET
263
+ },
264
+ setup: async (agentCtx) => {
265
+ try {
266
+ await agentCtx.get("agentPresets")?.mount?.(agentCtx, SKIFF_PRESET);
267
+ } catch {}
268
+ },
269
+ ...model ? { agentOptions: splitModel(model) } : {}
270
+ });
271
+ const agent = handle.agent;
272
+ let cccPrompt = "";
273
+ try {
274
+ cccPrompt = resolveRoleSystemPrompt(root, role);
275
+ } catch (err) {
276
+ console.warn(`[serenity-hooks] skiff 角色 "${roleName}" 系统提示词解析失败(回退仅基础段): ${String(err?.message ?? err)}`);
277
+ }
278
+ try {
279
+ agent.ctx.systemPrompt.section({
280
+ name: "serenity-skiff",
281
+ order: -60,
282
+ text: () => [buildSkiffBasePrompt(roleName, role), cccPrompt].filter(Boolean).join("\n")
283
+ });
284
+ } catch (err) {
285
+ console.warn(`[serenity-hooks] skiff 系统提示词注册失败: ${String(err?.message ?? err)}`);
286
+ }
287
+ registerSkiffSession(id, roleName, root, agent);
288
+ return {
289
+ handle,
290
+ agent,
291
+ sessionId: id
292
+ };
293
+ }
294
+ /** 等待 agent 空闲(agent/status → idle);无超时(agent 工作多久等多久,handyman 同款) */
295
+ function waitIdle(ctx, agent) {
296
+ return new Promise((resolve) => {
297
+ let settled = false;
298
+ let dispose = () => {};
299
+ const finish = () => {
300
+ if (settled) return;
301
+ settled = true;
302
+ dispose();
303
+ resolve();
304
+ };
305
+ dispose = ctx.on("agent/status", (payload) => {
306
+ if (payload.agent === agent && payload.status === "idle") finish();
307
+ });
308
+ });
309
+ }
310
+ /** 读会话最后一个 assistant/message 文本(handyman 同款) */
311
+ function lastAssistantText(agent) {
312
+ const events = agent.session.events;
313
+ for (let i = events.length - 1; i >= 0; i--) {
314
+ const e = events[i];
315
+ if (e && e.type === "assistant/message") {
316
+ const text = (e.data?.message?.content ?? e.data?.content ?? []).filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
317
+ if (text) return text;
318
+ }
319
+ }
320
+ return "";
321
+ }
322
+ /** events → 可读轨迹(user/assistant 文本 + 工具调用 + 工具结果;单条解析失败跳过) */
323
+ function eventsToTrajectory(events) {
324
+ const out = [];
325
+ for (const raw of events) try {
326
+ const ev = raw;
327
+ if (ev.type === "user/message") {
328
+ const text = extractText(ev.data);
329
+ if (text) out.push({
330
+ role: "user",
331
+ text
332
+ });
333
+ } else if (ev.type === "assistant/message") {
334
+ const d = ev.data;
335
+ const text = (d?.message?.content ?? d?.content ?? []).filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
336
+ const calls = d?.message?.tool_calls ?? [];
337
+ if (text) out.push({
338
+ role: "assistant",
339
+ text
340
+ });
341
+ for (const c of calls ?? []) {
342
+ const args = typeof c.arguments === "string" ? c.arguments : JSON.stringify(c.arguments ?? {});
343
+ out.push({
344
+ role: "assistant",
345
+ text: `→ ${c.name ?? "(tool)"} ${truncate(args, 300)}`,
346
+ tool: c.name
347
+ });
348
+ }
349
+ } else if (ev.type === "tool/result") {
350
+ const outText = extractText(ev.data);
351
+ if (outText) out.push({
352
+ role: "tool",
353
+ text: truncate(outText, 500),
354
+ tool: String(ev.data?.name ?? "")
355
+ });
356
+ }
357
+ } catch {}
358
+ return out;
359
+ }
360
+ function extractText(data) {
361
+ const content = data?.content;
362
+ if (!Array.isArray(content)) return "";
363
+ return content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
364
+ }
365
+ function truncate(s, n) {
366
+ return s.length > n ? `${s.slice(0, n)}…` : s;
367
+ }
368
+ /**
369
+ * 提问一轮:followup → 等 idle → 读答案 + 轨迹。
370
+ * @param eventsStart 轨迹起点:显式传 0 = 全量轨迹(会话追问时页面重绘完整时间线,
371
+ * v1.25.10 用户拍板);不传(undefined)= 本轮增量(followup 前 events 之后)
372
+ * @param options.includeTrajectory 是否计算轨迹(v1.26.10:**3100 对外只提供问答**——
373
+ * 公开问答页 / ACP JSON-RPC 不返回 trajectory,传 false 跳过计算;3099 调试页默认 true 保留)
374
+ */
375
+ async function askSkiff(ctx, agent, question, eventsStart, options) {
376
+ const before = eventsStart === void 0 ? agent.session.events.length : eventsStart;
377
+ agent.followup(createUserMessage({
378
+ content: [{
379
+ type: "text",
380
+ text: question
381
+ }],
382
+ source: PLUGIN_SOURCE
383
+ }));
384
+ await waitIdle(ctx, agent);
385
+ const answer = lastAssistantText(agent);
386
+ const trajectory = options?.includeTrajectory === false ? [] : eventsToTrajectory(agent.session.events.slice(before));
387
+ return {
388
+ answer,
389
+ sessionId: String(agent.session.id ?? ""),
390
+ trajectory
391
+ };
392
+ }
393
+ /**
394
+ * Skiff 会话的轨迹纪律参与判定:非 skiff 会话恒 true(正常参与);
395
+ * skiff 会话按角色 trajectory 子集(session/keeper/rebuild)决定;
396
+ * 注册表缺失(进程重启遗留等)→ 保守旁路(false,完全独立)。
397
+ */
398
+ function skiffTrajectoryEnabled(root, sessionId, key) {
399
+ if (!isSkiffSessionId(sessionId)) return true;
400
+ const roleName = sessionId ? skiffRoleFor(sessionId) : null;
401
+ if (!roleName) return false;
402
+ const role = readSkiffRoles(root).get(roleName);
403
+ return trajectorySubset(role)[key];
404
+ }
405
+ /**
406
+ * acc_msm 的 Skiff 门控:非 skiff 会话恒放行;skiff 会话——
407
+ * exec 非白名单 MSM 拒绝(不列名单)、register/deregister 必拒、
408
+ * list 白名单过滤、check/guide/ccc-config 只读放行。
409
+ */
410
+ function skiffMsmGate(root, sessionId, action, name) {
411
+ if (!isSkiffSessionId(sessionId)) return {};
412
+ const roleName = sessionId ? skiffRoleFor(sessionId) : null;
413
+ const role = roleName ? readSkiffRoles(root).get(roleName) : void 0;
414
+ if (!roleName || !role) return { reject: "MSM not allowed in this skiff session" };
415
+ if (action === "register" || action === "deregister") return { reject: "register/deregister is not allowed in skiff sessions" };
416
+ if (action === "exec") {
417
+ if (!name || !(role.msms ?? []).includes(name)) return { reject: "MSM not allowed" };
418
+ return {};
419
+ }
420
+ if (action === "list") return { whitelist: roleMsmWhitelist(role) };
421
+ return {};
422
+ }
423
+ //#endregion
424
+ export { writeFailedStatus as _, skiffSessionInfo as a, unregisterSkiffSession as c, handymanProgressPaths as d, listActiveHandymen as f, splitModel as g, requireWhitelistedModel as h, skiffMsmGate as i, HANDYMAN_GUIDE as l, readProgress as m, createSkiffAgent as n, skiffSessionSnapshot as o, newStopToken as p, getSkiffAgent as r, skiffTrajectoryEnabled as s, askSkiff as t, buildRoundPrompt as u, writeProgress as v, skiffRoleFor$1 as y };
@@ -35,8 +35,10 @@ export interface SkiffAgentRef {
35
35
  /**
36
36
  * 创建 Skiff agent:标准 DSH agent + cwd=CCC root + 角色模型 +
37
37
  * standard preset(平台工具面)+ scoped 系统提示词(基础提示词 + CCC 定义段,全替换 ACC 默认注入)。
38
+ * @param sessionId 指定会话 id(可选;微信桥等外部面用固定 id 实现用户↔会话长期映射——
39
+ * 不传则随机生成 skiff-<role>-<uuid>,ACP/调试页默认路径)
38
40
  */
39
- export declare function createSkiffAgent(ctx: Context, root: string, roleName: string, role: SkiffRoleConfig, defaultModel?: string): Promise<SkiffAgentRef>;
41
+ export declare function createSkiffAgent(ctx: Context, root: string, roleName: string, role: SkiffRoleConfig, defaultModel?: string, sessionId?: string): Promise<SkiffAgentRef>;
40
42
  export interface SkiffTrajectoryEntry {
41
43
  role: 'user' | 'assistant' | 'tool';
42
44
  text: string;
@@ -0,0 +1,126 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
+ import { f as readUtf8, p as resolveInside, s as loadSerenityConfig, t as DEFAULT_SERENITY_CONFIG_PATHS } from "./ccc-CfDrlfA7.js";
3
+ import { existsSync } from "node:fs";
4
+ //#region src/skiff-role.ts
5
+ /**
6
+ * skiff-role.ts — Skiff(F4,v1.25.0 实验性)角色层纯逻辑(零 DSH 依赖,可独立单测)
7
+ *
8
+ * 概念(S142 用户拍板):Skiff = 完整宁静号 trajectory(在宁静号内全知全能)的
9
+ * **任意子集**——CCC 通过角色配置定义:能力面(tools 非 MSM 工具白名单 + msms
10
+ * MSM 白名单,双白名单独立,白名单外全隐藏)+ 轨迹纪律面(trajectory 子集)+
11
+ * 系统提示词(CCC 完整定义,dsp 只给基础部分)。
12
+ *
13
+ * 实验性质:未配置任何角色 → Skiff 完全零影响(无监听、无 agent 创建、guard 无规则)。
14
+ */
15
+ var skiff_role_exports = /* @__PURE__ */ __exportAll({
16
+ SKIFF_SESSION_PREFIX: () => SKIFF_SESSION_PREFIX,
17
+ buildSkiffBasePrompt: () => buildSkiffBasePrompt,
18
+ isSkiffSessionId: () => isSkiffSessionId,
19
+ readSkiffRoles: () => readSkiffRoles,
20
+ resolveRoleSystemPrompt: () => resolveRoleSystemPrompt,
21
+ roleMsmWhitelist: () => roleMsmWhitelist,
22
+ roleToolWhitelist: () => roleToolWhitelist,
23
+ systemPromptSource: () => systemPromptSource,
24
+ trajectorySubset: () => trajectorySubset
25
+ });
26
+ /** Skiff agent 会话 id 前缀(agents.create 生成;seams 旁路/白名单判定用) */
27
+ const SKIFF_SESSION_PREFIX = "skiff-";
28
+ /** 判定 sessionId 是否为 Skiff 会话(仿 handyman- 前缀排除模式) */
29
+ function isSkiffSessionId(sessionId) {
30
+ return typeof sessionId === "string" && sessionId.startsWith("skiff-");
31
+ }
32
+ /**
33
+ * 读取 CCC 的 Skiff 角色配置(.opencode/serenity.json skiff.roles)。
34
+ * @returns 名 → 角色配置 的 Map;未配置(无 skiff 段/空 roles)返回空 Map(Skiff 未启用)
35
+ */
36
+ function readSkiffRoles(root, paths = DEFAULT_SERENITY_CONFIG_PATHS) {
37
+ const out = /* @__PURE__ */ new Map();
38
+ try {
39
+ const roles = loadSerenityConfig(root, paths).skiff?.roles;
40
+ if (!roles || typeof roles !== "object") return out;
41
+ for (const [name, role] of Object.entries(roles)) {
42
+ if (!role || typeof role !== "object") continue;
43
+ if (name.trim() === "") continue;
44
+ out.set(name.trim(), {
45
+ model: typeof role.model === "string" ? role.model : void 0,
46
+ msms: Array.isArray(role.msms) ? role.msms.filter((m) => typeof m === "string") : void 0,
47
+ tools: Array.isArray(role.tools) ? role.tools.filter((t) => typeof t === "string") : void 0,
48
+ trajectory: role.trajectory && typeof role.trajectory === "object" ? {
49
+ session: role.trajectory.session === true,
50
+ keeper: role.trajectory.keeper === true,
51
+ rebuild: role.trajectory.rebuild === true
52
+ } : void 0,
53
+ systemPrompt: typeof role.systemPrompt === "string" ? role.systemPrompt : void 0,
54
+ systemPromptFile: typeof role.systemPromptFile === "string" ? role.systemPromptFile : void 0
55
+ });
56
+ }
57
+ } catch {}
58
+ return out;
59
+ }
60
+ function trajectorySubset(role) {
61
+ return {
62
+ session: role?.trajectory?.session === true,
63
+ keeper: role?.trajectory?.keeper === true,
64
+ rebuild: role?.trajectory?.rebuild === true
65
+ };
66
+ }
67
+ /** 角色可用工具面(白名单并集):tools + acc_msm(msms 非空时作为 MSM 通道自动可用) */
68
+ function roleToolWhitelist(role) {
69
+ const out = /* @__PURE__ */ new Set();
70
+ for (const t of role?.tools ?? []) out.add(t);
71
+ if ((role?.msms?.length ?? 0) > 0) out.add("acc_msm");
72
+ return out;
73
+ }
74
+ /** 角色允许的 MSM 白名单(acc_msm exec 校验 / msm_list 过滤用;独立于 tools 白名单) */
75
+ function roleMsmWhitelist(role) {
76
+ return new Set(role?.msms ?? []);
77
+ }
78
+ /**
79
+ * 解析角色的系统提示词全文(v1.25.10,S142 用户:超长提示词 JSON 内嵌不可读):
80
+ * ① `systemPromptFile` 存在 → 读取文件内容(**推荐配置方法**;相对 CCC 根,
81
+ * 路径逃逸拒绝(resolveInside)+ BOM 剥除(readUtf8)+ 存在性校验)
82
+ * ② 否则 → 内嵌 `systemPrompt`(兼容旧配置)
83
+ * ③ 都无 → 空字符串
84
+ * 文件缺失/逃逸 → 抛错(调用方 catch 降级 + validate 报 issue)。
85
+ * 懒读取:readSkiffRoles 不读文件(guards/seams 每次工具调用查询的热路径零 IO),
86
+ * 仅在本函数(创建 agent / validate / list 时)读取。
87
+ */
88
+ function resolveRoleSystemPrompt(root, role) {
89
+ if (!role) return "";
90
+ const file = role.systemPromptFile?.trim();
91
+ if (file) {
92
+ const abs = resolveInside(root, file);
93
+ if (!existsSync(abs)) throw new Error(`skiff role "${role.systemPrompt ?? "(unnamed)"}": systemPromptFile "${file}" not found (resolved: ${abs})`);
94
+ return readUtf8(abs).trim();
95
+ }
96
+ return role.systemPrompt ?? "";
97
+ }
98
+ /** 角色系统提示词来源(validate/list 展示用) */
99
+ function systemPromptSource(role) {
100
+ if (role?.systemPromptFile?.trim()) return "file";
101
+ if (role?.systemPrompt?.trim()) return "inline";
102
+ return "none";
103
+ }
104
+ /**
105
+ * Skiff 基础提示词(dsp 只给这部分;CCC 的 systemPrompt 段由调用方拼接):
106
+ * 身份 + 可用 MSM/工具清单 + 调用协议 + 边界声明。动态生成(清单来自角色白名单)。
107
+ */
108
+ function buildSkiffBasePrompt(roleName, role) {
109
+ const msms = role?.msms ?? [];
110
+ const tools = role?.tools ?? [];
111
+ const lines = [
112
+ "=== Serenity Skiff ===",
113
+ `Role: ${roleName} (defined by this CCC)`,
114
+ "You interact with this CCC ONLY through the exposed surface below:"
115
+ ];
116
+ if (msms.length > 0) lines.push(` MSMs: ${msms.join(", ")} (call acc_msm exec <name> [args...]; pass --help as the first arg for usage)`);
117
+ else lines.push(" MSMs: (none)");
118
+ lines.push(` Tools: ${tools.length > 0 ? tools.join(", ") : "(none)"}`);
119
+ lines.push("No other tools are available. Your capability boundary is this surface.");
120
+ lines.push("");
121
+ lines.push("---");
122
+ lines.push("");
123
+ return lines.join("\n");
124
+ }
125
+ //#endregion
126
+ export { resolveRoleSystemPrompt as a, skiff_role_exports as c, readSkiffRoles as i, systemPromptSource as l, buildSkiffBasePrompt as n, roleMsmWhitelist as o, isSkiffSessionId as r, roleToolWhitelist as s, SKIFF_SESSION_PREFIX as t, trajectorySubset as u };