wezard 1.4.2 → 2.0.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/CLAUDE.md +12 -7
- package/README.md +121 -67
- package/dist/daemon/approval.js +9 -3
- package/dist/daemon/approval.js.map +1 -1
- package/dist/daemon/cc-bridge.js.map +1 -1
- package/dist/daemon/chat-name.js +8 -18
- package/dist/daemon/chat-name.js.map +1 -1
- package/dist/daemon/detail.js +53 -12
- package/dist/daemon/detail.js.map +1 -1
- package/dist/daemon/graph.js +5 -7
- package/dist/daemon/graph.js.map +1 -1
- package/dist/daemon/inbound.js +135 -158
- package/dist/daemon/inbound.js.map +1 -1
- package/dist/daemon/index.js +381 -192
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/jobs.js +2 -2
- package/dist/daemon/mirror-bridge.js +221 -134
- package/dist/daemon/mirror-bridge.js.map +1 -1
- package/dist/daemon/mirror-store.js.map +1 -1
- package/dist/daemon/model-select.js +74 -0
- package/dist/daemon/model-select.js.map +1 -0
- package/dist/daemon/peers.js +10 -16
- package/dist/daemon/peers.js.map +1 -1
- package/dist/daemon/spawn-tmux.js +27 -4
- package/dist/daemon/spawn-tmux.js.map +1 -1
- package/dist/daemon/task-gate.js +97 -0
- package/dist/daemon/task-gate.js.map +1 -0
- package/dist/daemon/task-registry.js +132 -0
- package/dist/daemon/task-registry.js.map +1 -0
- package/dist/daemon/tasks.js +126 -54
- package/dist/daemon/tasks.js.map +1 -1
- package/dist/daemon/wizard-memory.js +57 -0
- package/dist/daemon/wizard-memory.js.map +1 -0
- package/dist/daemon/wizard.js +84 -38
- package/dist/daemon/wizard.js.map +1 -1
- package/dist/mcp/server.js +138 -76
- package/dist/mcp/server.js.map +1 -1
- package/dist/shared/chat-http.js +185 -108
- package/dist/shared/chat-http.js.map +1 -1
- package/dist/shared/chat-render.js +26 -34
- package/dist/shared/chat-render.js.map +1 -1
- package/dist/shared/chat-view.js +14 -2
- package/dist/shared/chat-view.js.map +1 -1
- package/dist/shared/config.js +23 -4
- package/dist/shared/config.js.map +1 -1
- package/dist/shared/detail-render.js +12 -6
- package/dist/shared/detail-render.js.map +1 -1
- package/dist/shared/detail-store.js.map +1 -1
- package/dist/shared/role-render.js +52 -0
- package/dist/shared/role-render.js.map +1 -0
- package/dist/shared/role-view.js +210 -0
- package/dist/shared/role-view.js.map +1 -0
- package/dist/shared/schedule-spec.js +92 -116
- package/dist/shared/schedule-spec.js.map +1 -1
- package/dist/shared/session-label.js +103 -72
- package/dist/shared/session-label.js.map +1 -1
- package/dist/shared/std.js +3 -0
- package/dist/shared/std.js.map +1 -1
- package/dist/shared/task-file.js +120 -0
- package/dist/shared/task-file.js.map +1 -0
- package/dist/shared/trigger.js +232 -0
- package/dist/shared/trigger.js.map +1 -0
- package/dist/shared/world.js +7 -5
- package/dist/shared/world.js.map +1 -1
- package/dist/svr/index.js +67 -2
- package/dist/svr/index.js.map +1 -1
- package/package.json +1 -1
- package/web/chat.css +407 -212
- package/web/chat.js +932 -321
package/dist/mcp/server.js
CHANGED
|
@@ -85,6 +85,33 @@ const detectTmuxSession = () => new Promise((resolve) => {
|
|
|
85
85
|
p.on("close", (code) => resolve(code === 0 ? out.trim() || undefined : undefined));
|
|
86
86
|
});
|
|
87
87
|
const server = new McpServer({ name: "wezard", version: "0.0.1" }, { capabilities: { tools: {} } });
|
|
88
|
+
const withLegacyAddress = (shape) => {
|
|
89
|
+
const extra = {
|
|
90
|
+
...("name" in shape ? { tag: z.string().optional().describe("旧名, 同 `name`。") } : {}),
|
|
91
|
+
...("names" in shape ? { tags: z.array(z.string()).optional().describe("旧名, 同 `names`。") } : {}),
|
|
92
|
+
};
|
|
93
|
+
const name = shape.name;
|
|
94
|
+
return { ...shape, ...(name && !name.isOptional() ? { name: name.optional() } : {}), ...extra };
|
|
95
|
+
};
|
|
96
|
+
const foldLegacyAddress = (required) => (args) => {
|
|
97
|
+
const { tag, tags, ...rest } = args;
|
|
98
|
+
const next = {
|
|
99
|
+
...rest,
|
|
100
|
+
...(rest.name === undefined && tag !== undefined ? { name: tag } : {}),
|
|
101
|
+
...(rest.names === undefined && tags !== undefined ? { names: tags } : {}),
|
|
102
|
+
};
|
|
103
|
+
if (required && next.name === undefined)
|
|
104
|
+
throw new Error("missing `name` (the wizard's global name, e.g. 'fix')");
|
|
105
|
+
return next;
|
|
106
|
+
};
|
|
107
|
+
const registerTool = server.registerTool.bind(server);
|
|
108
|
+
server.registerTool = ((toolName, config, cb) => {
|
|
109
|
+
const shape = config.inputSchema;
|
|
110
|
+
if (!shape || !("name" in shape || "names" in shape))
|
|
111
|
+
return registerTool(toolName, config, cb);
|
|
112
|
+
const fold = foldLegacyAddress(!!shape.name && !shape.name.isOptional());
|
|
113
|
+
return registerTool(toolName, { ...config, inputSchema: withLegacyAddress(shape) }, ((args, extra) => cb(fold(args), extra)));
|
|
114
|
+
});
|
|
88
115
|
// Accept user-friendly prefixes from the LLM: vid:<id> → user:<id>, chatid:<id> → chat:<id>.
|
|
89
116
|
// Pass anything else (already user:/chat:/group:, or empty) through unchanged.
|
|
90
117
|
const normalizeTarget = (raw) => {
|
|
@@ -139,19 +166,27 @@ server.registerTool("wrc", {
|
|
|
139
166
|
// the switch.
|
|
140
167
|
server.registerTool("set_workspace", {
|
|
141
168
|
title: "Switch workspace directory",
|
|
142
|
-
description: "一步换掉这个 wizard 的**工作区**: 杀掉当前 pane, 在给定目录下重开一个全新的会话 —— 等价于往那个目录 `/new`。群里收到新会话的 📂 项目信息气泡当回执; 对话上下文**不会**带过去 (和 /new 一样是全新会话, 但身份的系统提示还在)。调用方就是被替换的那一个时, 它在调用当口就被终结 —— 这是预期行为, 群里那条气泡就是回执。用绝对路径 (或 `~` 开头)。想换目录又想保住手上的上下文: 先 wizard_handoff_self 把工作压成简报, 或者 spawn_clone({inherit:false, cwd})
|
|
169
|
+
description: "一步换掉这个 wizard 的**工作区**: 杀掉当前 pane, 在给定目录下重开一个全新的会话 —— 等价于往那个目录 `/new`。群里收到新会话的 📂 项目信息气泡当回执; 对话上下文**不会**带过去 (和 /new 一样是全新会话, 但身份的系统提示还在)。调用方就是被替换的那一个时, 它在调用当口就被终结 —— 这是预期行为, 群里那条气泡就是回执。用绝对路径 (或 `~` 开头)。想换目录又想保住手上的上下文: 先 wizard_handoff_self 把工作压成简报, 或者 spawn_clone({inherit:false, cwd}) 让一个分身去那边干。\n"
|
|
170
|
+
+ "**`keep:true` 是另一支**: 人回答「不用换, 就用现在这个目录」时调它 —— 不重开会话, 只把「这个工作区是人认过的」记下来, 此后新会话的开局不再问这件事。一个新聊天的第一个 wizard 落在默认兜底目录里, 开局会被要求先问一句要去哪个项目; 人给路径就走 `cwd`, 人说不用换就走 `keep`。",
|
|
143
171
|
inputSchema: {
|
|
144
|
-
cwd: z.string().describe("Absolute project path, e.g. /Users/foo/projects/bar. ~ is expanded."),
|
|
172
|
+
cwd: z.string().optional().describe("Absolute project path, e.g. /Users/foo/projects/bar. ~ is expanded. 与 `keep` 二选一。"),
|
|
173
|
+
keep: z
|
|
174
|
+
.boolean()
|
|
175
|
+
.optional()
|
|
176
|
+
.describe("人回答了「不用换, 就用现在这个目录」时传 `true` (不要传 cwd)。只记一笔确认 —— 不重开会话、不动上下文, 此后再没人问这件事。"),
|
|
145
177
|
target: z
|
|
146
178
|
.string()
|
|
147
179
|
.optional()
|
|
148
180
|
.describe('Optional target override. "vid:<userid>" / "chatid:<chatid>" / raw "user:<id>"/"chat:<id>". Empty → derive from the calling session.'),
|
|
149
181
|
},
|
|
150
|
-
}, async ({ cwd, target }) => {
|
|
182
|
+
}, async ({ cwd, keep, target }) => {
|
|
183
|
+
if (!cwd && !keep)
|
|
184
|
+
return fail("set_workspace 要么给 cwd (换到那个目录), 要么给 keep:true (确认沿用当前目录)");
|
|
151
185
|
const { sessionId, tmuxPane } = selfRef();
|
|
152
186
|
const normalizedTarget = normalizeTarget(target);
|
|
153
187
|
const { j } = await daemonPost("/mirror/workspace", {
|
|
154
|
-
cwd,
|
|
188
|
+
...(cwd ? { cwd } : {}),
|
|
189
|
+
...(keep ? { keep: true } : {}),
|
|
155
190
|
...(normalizedTarget ? { target: normalizedTarget } : {}),
|
|
156
191
|
...(sessionId ? { sessionId } : {}),
|
|
157
192
|
...(tmuxPane ? { tmuxPane } : {}),
|
|
@@ -160,7 +195,7 @@ server.registerTool("set_workspace", {
|
|
|
160
195
|
const queued = j.pendingCwd ? ` (switch queued for ${j.pendingCwd} — send /new from WeCom to apply)` : "";
|
|
161
196
|
return fail(`set_workspace failed: ${j.reason ?? "unknown"}${queued}`);
|
|
162
197
|
}
|
|
163
|
-
return ok({ ok: true, target: j.target, sessionId: j.sessionId, cwd: j.cwd });
|
|
198
|
+
return ok({ ok: true, target: j.target, sessionId: j.sessionId, cwd: j.cwd, ...(j.confirmed ? { confirmed: true } : {}) });
|
|
164
199
|
});
|
|
165
200
|
// 企业微信 doc / smartsheet / contact MCP 桥接。把 daemon 远端的 MCP 转发
|
|
166
201
|
// 给本地 Claude: list_tools 列工具, call_tool 调用 (创建文档 / 写表格 / 读
|
|
@@ -225,8 +260,8 @@ server.registerTool("switch_claude_session", {
|
|
|
225
260
|
return unwrap("switch_claude_session", await daemonPost("/sessions/switch", { sessionId }));
|
|
226
261
|
});
|
|
227
262
|
// ── wizard ↔ wizard (一个聊天里的同伴) ────────────────────────────────────
|
|
228
|
-
//
|
|
229
|
-
// 各自可以跑不同的 CLI / 模型 / 项目。它们互为同伴 (peer): 看得见彼此, 也驱动得动
|
|
263
|
+
// 每个 wizard 有一个全局唯一的名字, 人在群里写 `.fix`、wizard 之间传 `fix` 就叫到它,
|
|
264
|
+
// 与它住在哪个群无关; 各自可以跑不同的 CLI / 模型 / 项目。它们互为同伴 (peer): 看得见彼此, 也驱动得动
|
|
230
265
|
// 彼此 —— 下面这组工具就是那条通路。本进程只看得见**自己**, 所以任何关于同伴的
|
|
231
266
|
// 问题都要问守护进程, 它才是持有全部 attachment 的那个。
|
|
232
267
|
//
|
|
@@ -257,19 +292,18 @@ const daemonGet = async (path) => {
|
|
|
257
292
|
const unwrap = (name, { j, status }, pick = (x) => x) => j.ok ? ok(pick(j)) : fail(`${name} failed: ${(j.reason ?? j.error ?? `http ${status}`)}`);
|
|
258
293
|
// 地址语法只有一套, 每个吃地址的工具都把它原样重述一遍: 模型单看一个 schema
|
|
259
294
|
// 时没有别的地方能学到它, 而猜出来的地址会安静地指向另一个 wizard 的终端。
|
|
260
|
-
const ADDRESS_DOC = "wizard
|
|
261
|
-
// 造一个 wizard
|
|
262
|
-
//
|
|
263
|
-
// 聊天里, 这正是给聊天起名换来的东西。
|
|
295
|
+
const ADDRESS_DOC = "wizard 的名字 —— 全机唯一, 它就是地址: `'fix'` 或 `'.fix'` 都行, 不分它住在哪个群。`''` = 你这个聊天的默认 wizard。永远别自己拼 key: wizard_roster / list_peers / spawn_clone 返回的 `name` (或 409 里的 `address`) 原样传回来。老的 `聊天名#tag` 写法仍然认, 但别再写。";
|
|
296
|
+
// 造一个 wizard: 它的 home 默认是调用方自己的聊天 (所以走 `selfRef`); 给了 `chat`
|
|
297
|
+
// 就落在另一个**起过名字**的聊天里。名字全局唯一, 与 home 无关。
|
|
264
298
|
server.registerTool("new_claude_session", {
|
|
265
299
|
title: "Spawn a blank new wizard",
|
|
266
|
-
description: "在指定项目目录下长出一个**全新的 wizard** —— 自己的 tmux pane
|
|
300
|
+
description: "在指定项目目录下长出一个**全新的 wizard** —— 自己的 tmux pane、自己的全局名字 (`.name`), home 默认是你这个聊天, 等价于人在群里敲 `/new .name`。它**不继承任何上下文**(白纸一张): 要一个开局就带着你读过的材料的分身, 用 spawn_clone({inherit:true})。新 wizard 在群里说话时气泡头是 `emoji .name`, 之后用 wizard_roster / peek_peer / send_peer / wait_peer 驱动它。用户说「在 /path 下新建一个会话」「帮我在 xxx 目录起个 agent」时调它。给 `chat` 就把它的 home 设在**另一个**聊天里 —— 那个聊天必须起过名字(list_chats 能看到)。目录不存在会自动创建。绝不会顶掉一个聊天的默认 wizard。名字全局唯一: 撞上一个还在的 wizard 返回 409, 返回里的 `name` 是最终落定的名字。",
|
|
267
301
|
inputSchema: {
|
|
268
302
|
cwd: z.string().describe("Absolute project path to start the new session in, e.g. /Users/foo/projects/bar. Created if missing."),
|
|
269
|
-
|
|
303
|
+
name: z
|
|
270
304
|
.string()
|
|
271
305
|
.optional()
|
|
272
|
-
.describe("新 wizard
|
|
306
|
+
.describe("新 wizard 的名字 (如 'fix'、'docs', 带不带 '.' 都行) —— 全机唯一, 它就是地址, 挑一个说明它干什么的短词, 之后用它 send_peer / peek_peer。省略则按目录名生成。"),
|
|
273
307
|
chat: z
|
|
274
308
|
.string()
|
|
275
309
|
.optional()
|
|
@@ -281,23 +315,27 @@ server.registerTool("new_claude_session", {
|
|
|
281
315
|
model: z
|
|
282
316
|
.string()
|
|
283
317
|
.optional()
|
|
284
|
-
.describe("这个 wizard
|
|
318
|
+
.describe("这个 wizard 跑在哪个模型上, 口语化随便写 ('opus' / 'sonnet' / '最新的 opus' / 'claude-sonnet-5' 都行) —— 不是直接塞给启动参数, 而是等 pane 起来后真的敲 `/model` 校验, 认不出就翻一遍这台 CLI 此刻的目录再重试, 所以返回里的 `model` 才是真正落地的那个, 可能跟你传的字符串不完全一样; 万一没匹配上会带 `modelWarning`, 那时 wizard 还在跑但停在了原来的模型上, 不是失败。省略用该 CLI 的默认。同一个聊天里的 wizard 可以各跑各的模型 —— 又长又要判断的活给 opus, 跑腿的 lint/grep 给 haiku。"),
|
|
319
|
+
keepalive: z
|
|
320
|
+
.boolean()
|
|
321
|
+
.optional()
|
|
322
|
+
.describe("这个 wizard 要不要被 keepalive 心跳保温 (空闲时定期 ping 一下防 prompt cache 过期)。false = 永远不保温, 省下那份 ping 的钱 —— 适合跑腿一次就收工的临时会话; true = 明确要保温。省略则按 daemon 配置的默认值。"),
|
|
285
323
|
},
|
|
286
|
-
}, async ({ cwd,
|
|
324
|
+
}, async ({ cwd, name, chat, cli, model, keepalive }) => unwrap("new_claude_session", await daemonPost("/sessions/new", {
|
|
287
325
|
cwd,
|
|
288
|
-
...(
|
|
326
|
+
...(name ? { name } : {}),
|
|
289
327
|
...(chat ? { chat } : {}),
|
|
290
328
|
...(cli ? { cli } : {}),
|
|
291
329
|
...(model ? { model } : {}),
|
|
330
|
+
...(keepalive !== undefined ? { keepalive } : {}),
|
|
292
331
|
})));
|
|
293
|
-
// ── Chat naming
|
|
294
|
-
// A WeCom chat's identity is an unreadable `chat:wrkS…` id
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
// pass, which is what makes `daily#fix` — and spawning into `daily` — possible.
|
|
332
|
+
// ── Chat naming ────────────────────────────────────────────────────────────
|
|
333
|
+
// A WeCom chat's identity is an unreadable `chat:wrkS…` id. Wizards are addressed
|
|
334
|
+
// by their own global names; a chat name is what notify / new_claude_session
|
|
335
|
+
// point at, and what a chat's default wizard is born named after.
|
|
298
336
|
server.registerTool("name_chat", {
|
|
299
337
|
title: "Name this WeCom chat",
|
|
300
|
-
description: "
|
|
338
|
+
description: "给**这个聊天**起个短名字: notify 的收件人、new_claude_session / spawn_clone 的 `chat` 从此能指到这里。没起过名字的聊天不会一直没名字 —— 第一次有人用到时守护进程按它的工作区自动补一个 (`~/develop/foo` → `foo`, 撞名加序号), 所以这个工具的用途是**起一个更好的名字**。这里的默认 wizard 出生时取聊天名; 它若还叫旧聊天名, 会跟着改 (wizard 的名字全局唯一, 撞名加后缀, 返回里的 `wizardRenamed` 就是它的新名字)。wizard 自己的名字用 wizard_identity 改, 与这里无关。用户说「给这个群起名叫 daily」「这个群叫什么」(不传 `name` 就是读) 「取消命名」(传 '-') 时调它。名字全机唯一、大小写不敏感。",
|
|
301
339
|
inputSchema: {
|
|
302
340
|
name: z
|
|
303
341
|
.string()
|
|
@@ -309,68 +347,72 @@ server.registerTool("name_chat", {
|
|
|
309
347
|
: unwrap("name_chat", await daemonPost("/chats/name", { name })));
|
|
310
348
|
server.registerTool("list_chats", {
|
|
311
349
|
title: "List every chat and its sessions",
|
|
312
|
-
description: "跨聊天目录: 守护进程知道的每一个企微聊天、它的名字 (空 = 没起名)、是不是你住的那个 (`self`),
|
|
350
|
+
description: "跨聊天目录: 守护进程知道的每一个企微聊天、它的名字 (空 = 没起名)、是不是你住的那个 (`self`), 以及以每个聊天为 home 的 wizard 及其名字。用户指向这个聊天之外的活时调它 —— 「别的群有谁在跑」「把这个结论发到 daily 群」「在 sanitizer 群里开个会话」。wizard 本身不必经过聊天就能叫到 (名字全局唯一); 聊天名用于 notify 与在那边生新 wizard。",
|
|
313
351
|
inputSchema: {},
|
|
314
352
|
}, async () => unwrap("list_chats", await daemonPost("/chats/list", {})));
|
|
315
353
|
server.registerTool("list_peers", {
|
|
316
354
|
title: "List the wizards sharing this chat",
|
|
317
|
-
description: "
|
|
355
|
+
description: "和你同一个 home 聊天的其他 wizard (默认 wizard 加任意多个分身), 各有各的 pane、CLI、模型和工作区。每一个返回: 名字 (`address`, 即传给其他工具的那个串)、emoji、工作区、CLI、pane 是否还活着、此刻是否在生成 (`busy`)、最后动过是什么时候、最近在聊什么的一行摘要; `self: true` 是你自己。另外返回 `foreignPeers`: 别的聊天里的 wizard —— 名字全局唯一, 一样叫得到。用户提到另一个 agent 时先调它 —— 「.fix 进展如何」「还有谁在跑」「让 .docs 也看看」—— 再用 peek_peer / send_peer / wait_peer 真正协作。想连**职责、家谱**一起看, 用 wizard_roster。",
|
|
318
356
|
inputSchema: {},
|
|
319
357
|
}, async () => unwrap("list_peers", await daemonPost("/peers/list", {})));
|
|
320
358
|
server.registerTool("peek_peer", {
|
|
321
359
|
title: "Read what another wizard has been saying",
|
|
322
|
-
description: "**不打扰**地观察另一个 wizard: 返回 `dialog` —— 它最近 N 轮真实对话 (从它的 transcript 读的, `▸` 是别人说的, `◂` 是它答的), 外加它此刻是否在生成 (`busy`) 与它最后一条完整回复 (`lastText`)。这是「它和驱动它的人到底说了什么」的可读记录:
|
|
360
|
+
description: "**不打扰**地观察另一个 wizard: 返回 `dialog` —— 它最近 N 轮真实对话 (从它的 transcript 读的, `▸` 是别人说的, `◂` 是它答的), 外加它此刻是否在生成 (`busy`) 与它最后一条完整回复 (`lastText`)。这是「它和驱动它的人到底说了什么」的可读记录: 回答「.fix 进展如何」「它们聊到哪了」, 或者判断要不要推它一把, 都读这里。用户消息里写的 `.name` 指的就是那个 wizard —— 守护进程会在消息尾部挂一条 system-reminder 点名每一个解析得出的名字, 所以 prompt 里的 `.b` 是 wizard `b`: 去 peek 它, 别猜它在干嘛, 更别替它回答。它还没有可读 transcript 时, `pane` 兜底给它终端的原始尾巴。`foreign: true` 表示它的 home 在别的聊天。只读, 随便轮询。",
|
|
323
361
|
inputSchema: {
|
|
324
|
-
|
|
362
|
+
name: z.string().describe(ADDRESS_DOC),
|
|
325
363
|
turns: z.number().optional().describe("How many recent conversation turns to return (1-40, default 6)."),
|
|
326
364
|
},
|
|
327
|
-
}, async ({
|
|
365
|
+
}, async ({ name, turns }) => unwrap("peek_peer", await daemonPost("/peers/peek", { name, ...(turns ? { turns } : {}) })));
|
|
328
366
|
server.registerTool("send_peer", {
|
|
329
367
|
title: "Say something to another wizard",
|
|
330
|
-
description: "跟另一个 wizard 说话 —— 文本原样落进它的输入框, 它当成新的一轮接手。这是你**驱动**同伴的唯一方式: 派活、解它的阻塞、回答它的提问、叫它继续。「推动
|
|
331
|
-
"
|
|
368
|
+
description: "跟另一个 wizard 说话 —— 文本原样落进它的输入框, 它当成新的一轮接手。这是你**驱动**同伴的唯一方式: 派活、解它的阻塞、回答它的提问、叫它继续。「推动 .fix 干到底」的典型循环: peek_peer 看它在哪 → send_peer 说该说的 → wait_peer 等它停下 → 再 peek。名字全局唯一, 目标住在哪个群都一样叫。对方还不存在就自己造: 要它继承你的上下文用 spawn_clone, 要一个白纸一张的新 wizard 用 new_claude_session。\n" +
|
|
369
|
+
"**默认是私聊**: 不出任何群气泡, 只记在你们双方的 rolepage 里; 它那一轮的回复也不进群, 用 wait_peer 取。**`public:true` 则在公开频道说** —— 你这一轮所在的群里出一条 `.你 → .它` 的气泡, 它那一轮的回复也发进这个群。公开与否由你判断: 需要人知道的、或本该当着人讨论的 (关键决策、给人的结论、要人拍板的分歧) 用 public; 过程性的派活、催进度、对齐细节用私聊。无论哪种都直说: 要什么、给什么、结论是什么, 不用寒暄、不用引用原文。拒绝对自己发送。",
|
|
332
370
|
inputSchema: {
|
|
333
|
-
|
|
371
|
+
name: z.string().describe(ADDRESS_DOC),
|
|
334
372
|
text: z.string().describe("Message to inject. Plain prompt text; slash commands like '/clear' also work."),
|
|
335
373
|
when: z
|
|
336
374
|
.enum(["now", "idle"])
|
|
337
375
|
.optional()
|
|
338
376
|
.describe("什么时候投。`now` (默认) 立刻投 —— 对方正在生成时这句话会排在它这一轮后面, 回答它的提问、打断它、催它都该用这个。`idle` 先等它闲下来再投: **派一件新活给一个正在忙的同伴时用它**, 否则你和别人的两段文本会挤进同一个输入框被当成一轮读掉。返回里的 `wasBusy` 告诉你投的时候它忙不忙。"),
|
|
339
377
|
waitSec: z.number().optional().describe("`when:'idle'` 最多等多少秒 (10-3600, 默认 600)。等不到就返回失败, 不会强行投。"),
|
|
340
|
-
|
|
378
|
+
public: z
|
|
379
|
+
.boolean()
|
|
380
|
+
.optional()
|
|
381
|
+
.describe("true = 在公开频道 (你这一轮所在的群) 里说: 群里出 `.你 → .它` 气泡, 它的回复也进群。默认 false = 私聊, 只记在 rolepage。需要人知道 / 该当着人讨论才用 true。"),
|
|
382
|
+
job: z.string().optional().describe("这次派活归到某个工单名下 (open_job 给的 id) —— 收工那一条会把各自那段活列出来。"),
|
|
341
383
|
},
|
|
342
|
-
}, async ({
|
|
384
|
+
}, async ({ name, text, when, waitSec, job, public: pub }) => unwrap("send_peer", await daemonPost("/peers/send", { name, text, ...(when ? { when } : {}), ...(waitSec ? { waitSec } : {}), ...(job ? { job } : {}), ...(pub ? { public: true } : {}) })));
|
|
343
385
|
server.registerTool("notify", {
|
|
344
386
|
title: "Post a message into a chat for people to read",
|
|
345
387
|
description: "把一段 markdown 贴进一个企微聊天**给人看**。和 send_peer 分工明确: send_peer 是把话塞进另一个 agent 的输入框 (驱动它干活), notify 是说给人听 —— 不会触发任何一轮对话。\n" +
|
|
346
|
-
"`to` 省略 =
|
|
388
|
+
"`to` 省略 = 你这一轮所在的群 (人从哪个群叫的你就是哪个; 私聊轮则是你的 home 群); 要发到别的群就写聊天名 (`list_chats` 里那个), 一次可以写多个。气泡头自动写成 `emoji .你的名字` 并挂上你的 rolepage 链接, 那边的人一眼知道是谁。\n" +
|
|
347
389
|
"什么时候用: 长活跑完了要通知另一个群的人; 一批分身收工后把汇总播给发起那个群; 定时任务 (schedule_task) 到点跑完把结论送到该看的人那里。别拿它跟同群的人说话 —— 那是你的正常回复。",
|
|
348
390
|
inputSchema: {
|
|
349
391
|
to: z
|
|
350
392
|
.array(z.string())
|
|
351
393
|
.optional()
|
|
352
|
-
.describe("
|
|
394
|
+
.describe("收件聊天: 聊天名 (`daily`) 或裸 principal (`chat:wr…` / `user:…`)。省略 = 你这一轮所在的群。认不出的整条拒绝并列出来, 不会部分送达。"),
|
|
353
395
|
markdown: z.string().describe("正文, markdown。头 (是谁发的) 由守护进程自动加, 别自己写。"),
|
|
354
396
|
},
|
|
355
397
|
}, async ({ to, markdown }) => unwrap("notify", await daemonPost("/notify", { ...(to ? { to } : {}), markdown })));
|
|
356
398
|
server.registerTool("wait_peer", {
|
|
357
399
|
title: "Wait until another wizard stops working",
|
|
358
|
-
description: "挂起, 直到点名的 wizard 停下来 (它的终端不再显示中断提示), 然后返回它最新的回复。send_peer 之后就该用它 —— 这样你拿到的是写完的答案, 而不是写了一半的。超时先到则返回 `idle: false` 与原因: 它只是还在干, 你可以 peek 一眼再等。很便宜: 守护进程轮询的是 pane, 不烧 token
|
|
359
|
-
"**派了一批活就用 `
|
|
400
|
+
description: "挂起, 直到点名的 wizard 停下来 (它的终端不再显示中断提示), 然后返回它最新的回复。send_peer 之后就该用它 —— 这样你拿到的是写完的答案, 而不是写了一半的。超时先到则返回 `idle: false` 与原因: 它只是还在干, 你可以 peek 一眼再等。很便宜: 守护进程轮询的是 pane, 不烧 token。私聊轮的回复不进群 —— 人需要知道结论时, 用你自己的话收口给人, 别把原话再念一遍; 公开轮的回复已经在群里了, 更不必复述。\n" +
|
|
401
|
+
"**派了一批活就用 `names` 一次等一组**, 别一个一个等: 它们本来在同时干活, 串行等的墙钟是所有人之和, 并行等只等最慢的那一个。`results` 按你给的顺序逐个回 `idle` / `lastText`。`need` 决定满几个就返回 (默认全部; `need:1` = 谁先完事就先处理谁, 剩下的还在跑, 再调一次接着等)。",
|
|
360
402
|
inputSchema: {
|
|
361
|
-
|
|
362
|
-
|
|
403
|
+
name: z.string().optional().describe(`${ADDRESS_DOC} 等一组时改用 \`names\`。`),
|
|
404
|
+
names: z
|
|
363
405
|
.array(z.string())
|
|
364
406
|
.optional()
|
|
365
|
-
.describe("一次等多个 wizard
|
|
407
|
+
.describe("一次等多个 wizard 的名字 (最多 16 个), 每个的写法同 `name`。fan-out 之后的 join 用它 —— 五个分身并行等只花最慢那一个的时间。"),
|
|
366
408
|
need: z
|
|
367
409
|
.number()
|
|
368
410
|
.optional()
|
|
369
411
|
.describe("满几个就返回 (1 到地址个数, 默认全部)。`1` = 任意一个先完事就返回; 中间值 = 法定人数。满足后剩下的等待会被撤掉, 它们照常继续干活, 结果里 `idle:false`。"),
|
|
370
412
|
timeoutSec: z.number().optional().describe("Max seconds to wait (10-7200, default 900)."),
|
|
371
413
|
},
|
|
372
|
-
}, async ({
|
|
373
|
-
...(
|
|
414
|
+
}, async ({ name, names, need, timeoutSec }) => unwrap("wait_peer", await daemonPost("/peers/wait", {
|
|
415
|
+
...(names?.length ? { names } : { name: name ?? "" }),
|
|
374
416
|
...(need ? { need } : {}),
|
|
375
417
|
...(timeoutSec ? { timeoutSec } : {}),
|
|
376
418
|
})));
|
|
@@ -382,7 +424,7 @@ server.registerTool("run_agent_graph", {
|
|
|
382
424
|
.array(z.object({
|
|
383
425
|
tag: z.string().describe("wizard 的 tag, 不带 '#', 如 'fix'。"),
|
|
384
426
|
cli: z.enum(["claude", "claude-internal", "codebuddy"]).optional().describe("这个 wizard 用哪个 CLI。省略则继承本聊天的。"),
|
|
385
|
-
model: z.string().optional().describe("要现造这个 wizard
|
|
427
|
+
model: z.string().optional().describe("要现造这个 wizard 时跑在哪个模型上, 口语化随便写 ('opus' / 'haiku' / '最新的 opus' 都行) —— 起来后会用 `/model` 实测校验。已经在跑的不受影响。"),
|
|
386
428
|
cwd: z.string().optional().describe("这个 wizard 的工作区绝对路径。省略则继承本聊天的。"),
|
|
387
429
|
}))
|
|
388
430
|
.describe("参与的 wizard。每个 step 的 `to` 都必须点到这里面的某个 tag。"),
|
|
@@ -420,16 +462,16 @@ server.registerTool("stop_graph", {
|
|
|
420
462
|
}, async ({ runId }) => unwrap("stop_graph", await daemonPost("/graph/stop", { runId })));
|
|
421
463
|
server.registerTool("handoff", {
|
|
422
464
|
title: "Hand another wizard's work over to a fresh context",
|
|
423
|
-
description: "给**另一个** wizard 做交接, 原地完成: 守护进程让它把当前工作压成一份自洽的交接简报, 等它写完抓取, 再往**同一个 pane** 注入 `/clear` (上下文清零、新 sessionId、cwd 不变、身份的系统提示还在), 然后把简报作为新会话的第一条消息贴回去。它的上下文撑不住了、或者用户说「让
|
|
465
|
+
description: "给**另一个** wizard 做交接, 原地完成: 守护进程让它把当前工作压成一份自洽的交接简报, 等它写完抓取, 再往**同一个 pane** 注入 `/clear` (上下文清零、新 sessionId、cwd 不变、身份的系统提示还在), 然后把简报作为新会话的第一条消息贴回去。它的上下文撑不住了、或者用户说「让 .fix 交接一下」「叫它压缩上下文重开」时用。按 tmux `pane` id (`%5`, 来自 wizard_roster / list_claude_sessions) 或按 `name` 寻址。**要交接的是你自己就用 wizard_handoff_self** —— 这里拒绝对自身操作 (会死锁: 你没法在自己生成的当口再被问一次)。返回被带过去的那份简报。",
|
|
424
466
|
inputSchema: {
|
|
425
|
-
pane: z.string().optional().describe("目标 tmux pane id, 如 '%5'。优先于
|
|
426
|
-
|
|
467
|
+
pane: z.string().optional().describe("目标 tmux pane id, 如 '%5'。优先于 name。从 wizard_roster / list_claude_sessions 拿。"),
|
|
468
|
+
name: z.string().optional().describe(`${ADDRESS_DOC} 给了 pane 就忽略它。`),
|
|
427
469
|
focus: z.string().optional().describe("交接简报里要特别交代的点, 如 '重点交代还没跑通的测试'。可选。"),
|
|
428
470
|
timeoutSec: z.number().optional().describe("Max seconds to wait for the summary before aborting (30-7200, default 600)."),
|
|
429
471
|
},
|
|
430
|
-
}, async ({ pane,
|
|
472
|
+
}, async ({ pane, name, focus, timeoutSec }) => unwrap("handoff", await daemonPost("/handoff", {
|
|
431
473
|
...(pane ? { pane } : {}),
|
|
432
|
-
...(
|
|
474
|
+
...(name !== undefined ? { name } : {}),
|
|
433
475
|
...(focus ? { focus } : {}),
|
|
434
476
|
...(timeoutSec ? { timeoutSec } : {}),
|
|
435
477
|
})));
|
|
@@ -438,24 +480,37 @@ server.registerTool("handoff", {
|
|
|
438
480
|
// 定时器给不了的那一半: 它们的循环活在会话里, 会话一死就没了; 这个活在 daemon 里。
|
|
439
481
|
server.registerTool("schedule_task", {
|
|
440
482
|
title: "Schedule a prompt to run in a wizard session, on a recurring or one-off schedule",
|
|
441
|
-
description: "
|
|
483
|
+
description: "排一个**到点自动执行**的活, **归在你名下** (你的 rolepage「日程」里看得到): 到时间了, daemon 起一个**全新的白板 wizard** (名字 `<你>-task-xxxx`, 挂在你的家谱下), 把 `prompt` 原样说给它听, 它干完活自动收掉 —— 产出照常落在群里。守护进程级, 跨 CLI 重启/会话结束仍在。用户说「每个工作日晚上 9:30 自动跑一下 xxx」「每天早上帮我看看 yyy」「每 2 小时同步一次 zzz」「明早 9 点提醒并整理 www」时调它。\n" +
|
|
484
|
+
"**每条任务落成一份你能直接改的代码文件** `~/.wezard/tasks/<id>.task.mjs` (返回值里的 `file`)。这个工具只管最常见的那条路 (什么时候 + 说什么); 要更细的东西就**用 Read/Edit 改那个文件**, 存盘即生效, 不用 reload:\n" +
|
|
485
|
+
"· 触发条件可组合 —— `when: ({every, daily, at, between, onDays, and, not}) => and(every(\"1h\"), between(\"08:00\",\"20:00\"), onDays(\"工作日\"))`。`every/daily/at` 产生时刻, `between/onDays/not` 只做筛选。\n" +
|
|
486
|
+
"· **放枪前先探一眼**用 `gate` —— 一段在 daemon 侧跑的异步函数, 返回 `false` 这一轮就不放枪 (不起 wizard、群里不出声), 返回 `{vars}` 则填进 prompt 里的 `{{名字}}`, 返回 `{state}` 下一轮还拿得到。`gate: async ({sh, state}) => { const out = await sh(\"git fetch -q && git log --oneline HEAD..@{u}\"); return out.trim() ? {vars:{commits: out}} : false; }`。「有新东西才处理」这类需求必须用它, 别写成「到点起个 wizard 让它自己看一眼没有就退出」—— 那是每次空转一个 pane 加一份上下文。\n" +
|
|
487
|
+
"· `enabled: false` 暂停而不删。\n" +
|
|
488
|
+
"**默认新建, 不在任何已有会话里续**: 定时的活是一件独立的事, 塞进一个常驻 wizard 会把两件不相关的事挤进同一个 transcript, 那个 wizard 正忙时还会连触发时刻一起被拖走。只有用户明确说了「在 .foo 里继续 / 让 .foo 每天…」才传 `name` 点名它 —— 这时到点直接投进它那一轮 (它正忙则仍然另起白板执行)。`prompt` 本身就在说「建两个 wizard 去干 xxx」时, 就算给了 `name` 也照新建办 —— 守护进程自己认这句话, 那个名字只决定在谁的聊天/目录下办。要强行在它那一轮里续, 显式传 `fresh:false`。\n" +
|
|
489
|
+
"`when` 用人话原样写, 别自己翻译成 cron: 「每个工作日晚上9:30」「每天 8:00」「每周三下午3点」「每隔两个小时」「每小时」「每 30 分钟」「20 分钟后」「明早 9 点」都认, 还能叠时间窗口 —— 「白天每隔一个小时」「工作时间每半小时」「8点到20点每小时」。解析不出会报错并列出能认的说法 —— 这时把原话回给用户让他重说, 别自己猜一个时间存进去。\n" +
|
|
490
|
+
"存成功后**必须把回显的 `when` 和 `next` 念给用户**确认 (例: 「每个工作日 21:30, 下次 2026-09-21 21:30」)。`prompt` 要写成一句完整的、零上下文也能执行的指令 —— 到点接活的多半是个刚出生的白板 wizard, 它只看得见这句话。",
|
|
442
491
|
inputSchema: {
|
|
443
|
-
when: z.string().describe("什么时候跑, 人话原样传: 「每个工作日晚上9:30」「每天早上9
|
|
444
|
-
prompt: z.string().describe("到点要说给那个 wizard 听的话。写成自洽的完整指令 (要做什么、在哪个目录/文件上、做完怎么汇报),
|
|
445
|
-
|
|
446
|
-
|
|
492
|
+
when: z.string().describe("什么时候跑, 人话原样传: 「每个工作日晚上9:30」「每天早上9点」「白天每隔一个小时」「工作时间每半小时」「每30分钟」「20分钟后」「明早9点」。时间窗口 (白天 / 工作时间 / 8点到20点) 会被解析成筛子, 窗外的那些枪直接吞掉。"),
|
|
493
|
+
prompt: z.string().describe("到点要说给那个 wizard 听的话。写成自洽的完整指令 (要做什么、在哪个目录/文件上、做完怎么汇报), 别依赖当前对话的上下文。里面可以留 `{{名字}}` 占位, 由任务文件里的 gate 填。"),
|
|
494
|
+
name: z.string().optional().describe(`点名在**哪个已有 wizard**里跑 —— 只有用户要求「在它那儿继续」时才传。省略 = 到点新建一个白板 wizard 干完就收 (默认, 也是「定时新建 wizard 干 xxx」要的那个)。传了它还想要新建, 再加 \`fresh:true\`: 那时它只当模板, 新 wizard 继承它的聊天/目录/模型。${ADDRESS_DOC}`),
|
|
495
|
+
fresh: z.boolean().optional().describe("覆盖默认: true = 每次到点新建白板 wizard 执行 (给了 name 时用来表达「在它的目录下新开一个干」), false = 注入 name 指向的已有会话。默认由 name 推断 (给了 name = false, 没给 = true)。"),
|
|
496
|
+
id: z.string().optional().describe("任务 id, 同时也是文件名 (`~/.wezard/tasks/<id>.task.mjs`)。省略则从 note/prompt 生成。取个好认的短名 —— 之后你要改这条任务, 改的就是那个文件。"),
|
|
497
|
+
note: z.string().optional().describe("给人看的一句话备注, 在 list_tasks 里回显。"),
|
|
447
498
|
},
|
|
448
|
-
}, async ({ when, prompt,
|
|
499
|
+
}, async ({ when, prompt, name, note, fresh, id }) => unwrap("schedule_task", await daemonPost("/tasks/schedule", {
|
|
500
|
+
when, prompt, name: name ?? "", note: note ?? "", id: id ?? "",
|
|
501
|
+
...(fresh === undefined ? {} : { fresh }),
|
|
502
|
+
})));
|
|
449
503
|
server.registerTool("list_tasks", {
|
|
450
504
|
title: "List scheduled tasks on this host",
|
|
451
|
-
description: "列出本机所有定时任务: `id` (取消要用)、`when` (人话回显)、`next` (下次触发时刻)、`lastFired
|
|
505
|
+
description: "列出本机所有定时任务: `id` (取消要用)、`when` (人话回显)、`next` (下次触发时刻)、`lastFired`、`runIn` (到点是新建白板 wizard 还是注入已有会话)、目标 wizard 的 `address` 与 `prompt`。用户问「有哪些定时任务」「我设了什么定时」「下次什么时候跑」时调它。只读。`mine:true` 只看排给你自己的。\n" +
|
|
506
|
+
"`file` 是这条任务的源码 (`~/.wezard/tasks/<id>.task.mjs`) —— 要改触发条件、加 gate、暂停 (`enabled:false`) 就 Read/Edit 它, 存盘即生效。`gate` 说明这条任务有没有放枪前的探查以及上一轮的去向 (go 放了 / skip 没活 / error 炸了), `lastError` 与 `loadError` 是它上次为什么没跑成 —— 改坏了的文件也会出现在列表里, 带着理由。",
|
|
452
507
|
inputSchema: {
|
|
453
508
|
mine: z.boolean().optional().describe("true = 只列排给调用方自己的任务。默认列全机。"),
|
|
454
509
|
},
|
|
455
510
|
}, async ({ mine }) => unwrap("list_tasks", await daemonPost("/tasks/list", { mine: mine === true })));
|
|
456
511
|
server.registerTool("cancel_task", {
|
|
457
512
|
title: "Cancel one scheduled task by id",
|
|
458
|
-
description: "按 id 删掉一条定时任务 (id 从 list_tasks 拿)
|
|
513
|
+
description: "按 id 删掉一条定时任务 (id 从 list_tasks 拿), 连同它那份任务文件一起。用户说「取消那个定时」「别再每天跑了」时调它: 先 list_tasks 把候选念给用户确认是哪一条, 再删。删的是日程本身, 不影响任何正在跑的活。**一条定时的 prompt 要求「没活就停掉这个定时」时, 到点接活的那个 wizard 就该自己调它**: list_tasks 按 note/prompt 认出是哪一条, 再 cancel_task。只想暂时停就改任务文件的 `enabled: false`, 留档。",
|
|
459
514
|
inputSchema: {
|
|
460
515
|
id: z.string().describe("Schedule id from list_tasks."),
|
|
461
516
|
},
|
|
@@ -490,14 +545,14 @@ server.registerTool("config_set", {
|
|
|
490
545
|
// 分身还剩几个、别人是谁。
|
|
491
546
|
server.registerTool("wizard_whoami", {
|
|
492
547
|
title: "Who am I",
|
|
493
|
-
description: "你自己是谁:
|
|
548
|
+
description: "你自己是谁: 全局唯一的名字 (即地址, 别人用它找你)、home 聊天、工作区、职责、记忆、家谱 (谁生的你、你生了谁), 以及此刻的 contextTokens 与 handoffSuggested。用户问「你是谁」「你叫什么」「你在哪个目录」「你有几个分身」时先调它; 要做任何编排之前也先调它 —— 你得知道自己的工作区在哪、手里已经有哪些分身。handoffSuggested=true 表示上下文该交接了 (见 wizard_handoff_self)。",
|
|
494
549
|
inputSchema: {},
|
|
495
550
|
}, async () => unwrap("wizard_whoami", await daemonPost("/wizard/whoami", {})));
|
|
496
551
|
server.registerTool("wizard_identity", {
|
|
497
552
|
title: "Name yourself / declare your job",
|
|
498
|
-
description: "
|
|
553
|
+
description: "改名字、写职责。名字就是别人喊你的那个词 (`.name`), **全机唯一**, 与聊天名无关 (给聊天起名用 name_chat): 撞名时自动加 `-N` 后缀, 返回里的 `name` 是落定的那个, `renamed` 会告诉你发生了改写。职责是一句话的「我是干什么的」—— 别的 wizard 在名册里读到它, 据此决定该不该找你。用户说「你以后叫 X」「你负责 X」时调它; 你自己发现 whoami 里职责是空的, 也应当主动补上。只传要改的那个字段。",
|
|
499
554
|
inputSchema: {
|
|
500
|
-
name: z.string().optional().describe("新名字, 1-32 位字母/数字/`_`/`-`,
|
|
555
|
+
name: z.string().optional().describe("新名字, 1-32 位字母/数字/`_`/`-`, 全机唯一。不改就别传。"),
|
|
501
556
|
description: z.string().optional().describe("一句话职责, 例如 '盯 wezard 主仓的重构与发版'。不改就别传。"),
|
|
502
557
|
},
|
|
503
558
|
}, async ({ name, description }) => unwrap("wizard_identity", await daemonPost("/wizard/identity", {
|
|
@@ -506,10 +561,10 @@ server.registerTool("wizard_identity", {
|
|
|
506
561
|
})));
|
|
507
562
|
server.registerTool("wizard_roster", {
|
|
508
563
|
title: "Every wizard and clone",
|
|
509
|
-
description: "这个世界上所有的 wizard 与 clone:
|
|
564
|
+
description: "这个世界上所有的 wizard 与 clone: 每一个的名字 (全局唯一, 即地址)、**home 聊天**、**工作区**、职责、忙闲 (busy)、是否还活着 (alive)、最近在干嘛 (summary), 以及家谱 (parent / clones / ancestors)。跨聊天的也在里面。这是你感知同伴的唯一入口 —— 用户说「还有谁在跑」「谁在弄那个项目」「让懂 X 的那个来看看」时先调它, 拿到目标的名字再 send_peer / peek_peer / wait_peer; 要往某个群里对人说话则把那个 `chat` 交给 notify。\n" +
|
|
510
565
|
"**这是一张索引, 不是一份名单**: 整台机器上可能有几百个会话, 所以默认只回最相关的一页 (自己 → 活着的 → 最近动过的), 并告诉你 `total` / `matched` 有多少。找人就带上条件: `query` 匹配名字/职责/地址, `cwd` 匹配工作区路径 (「谁在这个目录里干活」), `chat` 限定某个聊天, `alive:true` 只看还活着的。别不带条件硬拉全表。",
|
|
511
566
|
inputSchema: {
|
|
512
|
-
query: z.string().optional().describe("在名字 / 职责 /
|
|
567
|
+
query: z.string().optional().describe("在名字 / 职责 / target 里做子串匹配 (不分大小写)。「让懂 X 的那个来看看」就把 X 写在这里。"),
|
|
513
568
|
chat: z.string().optional().describe("只看某个聊天里的 wizard: 聊天名, 或者聊天 principal 的一段 (无名聊天用它)。"),
|
|
514
569
|
cwd: z.string().optional().describe("只看工作区路径包含这一段的 wizard —— 「谁在 /path 下干活」的反查。"),
|
|
515
570
|
alive: z.boolean().optional().describe("true = 只看 pane 还活着的。默认全给 (冷会话发消息就会被唤醒)。"),
|
|
@@ -524,37 +579,42 @@ server.registerTool("wizard_roster", {
|
|
|
524
579
|
})));
|
|
525
580
|
server.registerTool("spawn_clone", {
|
|
526
581
|
title: "Spawn a clone of yourself",
|
|
527
|
-
description: "生一个分身 —— 一个新的 wizard,
|
|
582
|
+
description: "生一个分身 —— 一个新的 wizard, home 在你的聊天 (或指名的另一个聊天), 有自己的 tmux pane、自己的全局名字 (`.name`)、自己的职责, 归你管。\n" +
|
|
528
583
|
"`inherit` 必填, 它决定这是哪一种分身:\n" +
|
|
529
584
|
"• inherit=true —— **fork 你此刻的上下文**: 它开局就拥有你已经读过的一切 (规范、目录结构、刚啃完的那份文档), 不必重读。代价是它必须留在你当前的工作区 (换 cwd 会自动退化成 false)。这是编排一组「共享同一批材料」的任务的正确姿势: 你先把公共材料读进自己的上下文, 再 fork 出 N 个分身, 材料只读一遍却进了 N 份上下文。\n" +
|
|
530
585
|
"• inherit=false —— 空白分身: 只继承身份, 不继承上下文。适合干一件与你手头无关的事, 或者要在别的目录/别的聊天里干活。\n" +
|
|
531
|
-
"带上 `task`
|
|
586
|
+
"带上 `task` 可以在它就位的同时把第一件活派下去 (私聊), 省掉一次 send_peer。名字全局唯一: 撞上一个还在的 wizard 返回 409 (附它的死活), 返回里的 `name` 是落定的名字。之后用 send_peer 继续派活、wait_peer 等它做完、stop_wizard 收掉它。分身自己也能再 spawn_clone, 层级不限。分身是有成本的 (一个 pane + 一份上下文), 任务少于两三件时你自己做完更快。",
|
|
532
587
|
inputSchema: {
|
|
533
588
|
inherit: z
|
|
534
589
|
.boolean()
|
|
535
590
|
.describe("true = fork 你此刻的上下文 (它开局就有你读过的材料, 必须留在同一工作区); false = 空白分身, 只继承身份。必填, 没有默认值。"),
|
|
536
591
|
description: z.string().describe("这个分身负责什么, 一句话。它会写进分身的系统提示, 也会出现在名册里让别人看到。"),
|
|
537
|
-
|
|
592
|
+
name: z.string().optional().describe("分身的名字 (如 'docs'、'fix', 带不带 '.' 都行)。全机唯一。省略则按 description 首词生成。"),
|
|
538
593
|
task: z.string().optional().describe("就位后立刻派下去的第一件活。省略则它就位待命。"),
|
|
539
594
|
cwd: z.string().optional().describe("分身的工作区绝对路径。只在 inherit=false 时有意义 —— 换目录与继承上下文互斥。"),
|
|
540
595
|
chat: z.string().optional().describe("把分身生在另一个聊天里 (list_chats 里的名字)。省略 = 你自己的聊天, 这是绝大多数情况。"),
|
|
541
596
|
cli: z.enum(["claude", "claude-internal", "codebuddy"]).optional().describe("分身用哪个 CLI。省略则继承。"),
|
|
542
|
-
model: z.string().optional().describe("
|
|
597
|
+
model: z.string().optional().describe("分身跑在哪个模型上, 口语化随便写 ('opus' / 'sonnet' / '最新的 opus' / 'claude-sonnet-5' 都行) —— 不是直接塞给启动参数, 而是等 pane 起来后真的敲 `/model` 校验, 认不出就翻一遍这台 CLI 此刻的目录再重试, 所以返回里的 `model` 才是真正落地的那个, 可能跟你传的字符串不完全一样; 万一没匹配上会带 `modelWarning`, 那时分身还在跑但停在了原来的模型上, 不是失败。省略用该 CLI 的默认。分身可以和你跑在不同模型上: 要判断力的那一路给 opus, 跑腿的 (grep、跑测试、照着清单改) 给 haiku —— 一批分身不必齐步走。"),
|
|
543
598
|
job: z
|
|
544
599
|
.string()
|
|
545
600
|
.optional()
|
|
546
|
-
.describe("把这个分身归到某个工单名下 (open_job 给的 id)
|
|
601
|
+
.describe("把这个分身归到某个工单名下 (open_job 给的 id)。它们攒到 close_job 那一条里一起交代, 过程在各自的 rolepage; close_job 还会把它们整批回收掉。"),
|
|
602
|
+
keepalive: z
|
|
603
|
+
.boolean()
|
|
604
|
+
.optional()
|
|
605
|
+
.describe("这个分身要不要被 keepalive 心跳保温 (空闲时定期 ping 一下防 prompt cache 过期)。false = 永远不保温, 省下那份 ping 的钱 —— 适合跑腿一次就收工的临时分身; true = 明确要保温 —— 适合会长期挂着、随时可能被叫醒接手的分身。省略则按 daemon 配置的默认值。"),
|
|
547
606
|
},
|
|
548
|
-
}, async ({ inherit, description,
|
|
607
|
+
}, async ({ inherit, description, name, task, cwd, chat, cli, model, job, keepalive }) => unwrap("spawn_clone", await daemonPost("/wizard/clone", {
|
|
549
608
|
inherit,
|
|
550
609
|
description,
|
|
551
|
-
...(
|
|
610
|
+
...(name ? { name } : {}),
|
|
552
611
|
...(task ? { task } : {}),
|
|
553
612
|
...(job ? { job } : {}),
|
|
554
613
|
...(cwd ? { cwd } : {}),
|
|
555
614
|
...(chat ? { chat } : {}),
|
|
556
615
|
...(cli ? { cli } : {}),
|
|
557
616
|
...(model ? { model } : {}),
|
|
617
|
+
...(keepalive !== undefined ? { keepalive } : {}),
|
|
558
618
|
})));
|
|
559
619
|
// ── Job: 一次 fan-out 的工单 ────────────────────────────────────────────────
|
|
560
620
|
// 工单不是第二个编排器: 控制流始终在你自己的上下文里 (你自己 spawn、自己 wait、
|
|
@@ -563,9 +623,9 @@ server.registerTool("spawn_clone", {
|
|
|
563
623
|
server.registerTool("open_job", {
|
|
564
624
|
title: "Open a job for a fan-out",
|
|
565
625
|
description: "开一个**工单**: 你接下来要同时派出两个以上的分身干同一件事时, 先开它。返回一个 id, 把这个 id 传给 spawn_clone / send_peer 的 `job` 参数, 它们就归到这个工单名下。\n" +
|
|
566
|
-
"开了工单之后有三件事不一样: ①
|
|
626
|
+
"开了工单之后有三件事不一样: ① 群里出「开工」与 close_job 的「收工」两条气泡, 人据此读得出这批活的结构 (过程在各自的 rolepage 里, 收工那条会把成员和各自那段活列出来)。② close_job 会把为这个工单生出来的分身**整批回收**, 不必一个个 stop_wizard —— 忘记回收是常态, 每个分身都占着一个 pane 和一份上下文。③ list_jobs 能看到还开着哪些活。\n" +
|
|
567
627
|
"派活时顺手让每个分身**把结论收口成一行** `RESULT: …` (交付物写进文件就回传路径): wait_peer 会把这一行单独摘出来放进 `result`, 你汇总时不必再从八百字里找结论。\n" +
|
|
568
|
-
"只派一个分身、或者只是推某个同伴一把,
|
|
628
|
+
"只派一个分身、或者只是推某个同伴一把, 不用开工单。",
|
|
569
629
|
inputSchema: {
|
|
570
630
|
title: z.string().describe("一句话说清这个工单要干成什么 —— 它会出现在群里的开工气泡上。"),
|
|
571
631
|
plan: z.string().optional().describe("要在开工气泡里一并说明的计划 (打算分几路、各干什么)。省略则只出标题。"),
|
|
@@ -573,7 +633,7 @@ server.registerTool("open_job", {
|
|
|
573
633
|
}, async ({ title, plan }) => unwrap("open_job", await daemonPost("/jobs/open", { title, ...(plan ? { plan } : {}) })));
|
|
574
634
|
server.registerTool("close_job", {
|
|
575
635
|
title: "Close a job and recycle its clones",
|
|
576
|
-
description: "收工: 把汇总结论发进群 (连同成员清单和各自那段活, 每个名字挂它自己的
|
|
636
|
+
description: "收工: 把汇总结论发进群 (连同成员清单和各自那段活, 每个名字挂它自己的 rolepage), 并**把为这个工单生出来的分身整批回收**。被拉来帮忙的长期 wizard 不在回收之列, 你自己也不会被收。\n" +
|
|
577
637
|
"拿到所有分身的结果、汇总完就调它 —— 分身留着不收, 下一次编排就会撞到分身上限。`stop:false` 只结账不回收 (那些分身后面还有用)。",
|
|
578
638
|
inputSchema: {
|
|
579
639
|
job: z.string().describe("open_job 返回的工单 id。"),
|
|
@@ -588,21 +648,23 @@ server.registerTool("list_jobs", {
|
|
|
588
648
|
}, async () => unwrap("list_jobs", await daemonPost("/jobs/list", {})));
|
|
589
649
|
server.registerTool("stop_wizard", {
|
|
590
650
|
title: "Interrupt or end another wizard",
|
|
591
|
-
description: "收掉一个 wizard/分身。mode='interrupt' 只打断它当前这一轮 (等价于群里的 /stop, 它还活着, 可以继续派活); mode='end' 结束它并回收 tmux pane (等价于 /kill, 之后再找它会重新长出一个空白会话)。活干完了就把临时分身 end 掉 —— 每个分身都占着一个 pane 和一份上下文。加 forget=true 连它的身份记录一起抹掉 (名字、职责、记忆), 只在它彻底不会再回来时用。用户说「让
|
|
651
|
+
description: "收掉一个 wizard/分身。mode='interrupt' 只打断它当前这一轮 (等价于群里的 /stop, 它还活着, 可以继续派活); mode='end' 结束它并回收 tmux pane (等价于 /kill, 之后再找它会重新长出一个空白会话)。活干完了就把临时分身 end 掉 —— 每个分身都占着一个 pane 和一份上下文。加 forget=true 连它的身份记录一起抹掉 (名字、职责、记忆), 只在它彻底不会再回来时用。用户说「让 .x 停下」「把那些分身收了」时调它。终结自己也是合法的 (分身干完活自我了结), 只是这次调用不会返回 —— 群里的通知就是回执。",
|
|
592
652
|
inputSchema: {
|
|
593
|
-
|
|
653
|
+
name: z.string().describe(ADDRESS_DOC),
|
|
594
654
|
mode: z.enum(["end", "interrupt"]).optional().describe("'end' 结束并回收 pane (默认); 'interrupt' 只打断当前这一轮。"),
|
|
595
655
|
forget: z.boolean().optional().describe("仅对 end 有效: 连身份记录 (名字/职责/记忆) 一起删除。默认 false —— 身份留着, 下次它回来还是它。"),
|
|
596
656
|
},
|
|
597
|
-
}, async ({
|
|
657
|
+
}, async ({ name, mode, forget }) => unwrap("stop_wizard", await daemonPost("/wizard/stop", { name, ...(mode ? { mode } : {}), ...(forget ? { forget } : {}) })));
|
|
598
658
|
server.registerTool("wizard_remember", {
|
|
599
659
|
title: "Write something into your long-term memory",
|
|
600
|
-
description: "
|
|
660
|
+
description: "写一条长期记忆。它不在对话里 —— 每次 wizard (重)开会话时重新压进系统提示, 所以能跨 /clear、跨交接、跨重启活下来。三种作用域: `self` (默认) 只属于你; `chat` 是**本群共享**的记忆 (这个群里人的习惯、约定), 在这个群出生的每个 wizard 都会读到; `workspace` 是**本工作区共享**的记忆 (这个仓库的硬约束、踩过的坑、'发版前必须更新 CHANGELOG' 这种规矩), 在这个目录下干活的每个 wizard 都会读到。后两者存成 markdown (`~/.wezard/memory/…`), 人也会直接改。一条一句话, 越具体越有用; 选对作用域 —— 属于群/仓库的别只记在自己身上。传 forget (子串匹配) 删掉过时的那条。别拿它存这次任务的临时状态 —— 那种东西属于交接简报。",
|
|
601
661
|
inputSchema: {
|
|
602
662
|
note: z.string().optional().describe("要记住的一句话。"),
|
|
603
663
|
forget: z.string().optional().describe("要忘掉的记忆里的一个子串, 命中的整条删除。"),
|
|
664
|
+
scope: z.enum(["self", "chat", "workspace"]).optional().describe("记在哪: self (默认, 只属于你) / chat (本群共享) / workspace (本工作区共享)。"),
|
|
604
665
|
},
|
|
605
|
-
}, async ({ note, forget }) => unwrap("wizard_remember", await daemonPost("/wizard/remember", {
|
|
666
|
+
}, async ({ note, forget, scope }) => unwrap("wizard_remember", await daemonPost("/wizard/remember", {
|
|
667
|
+
...(scope ? { scope } : {}),
|
|
606
668
|
...(note ? { note } : {}),
|
|
607
669
|
...(forget ? { forget } : {}),
|
|
608
670
|
})));
|