pactive 0.0.2-alpha.1 → 0.0.4-alpha.1

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/pi/extension.ts CHANGED
@@ -2,7 +2,8 @@
2
2
  * Pactive — Pi extension (the single shipped entry (Pi loads this .ts directly via jiti; there is no dist copy)).
3
3
  *
4
4
  * Surface:
5
- * - tools: `pactive_status`, `pactive_schedule`
5
+ * - tools: `pactive_status`, `pactive_schedule`, `pactive_config` (read-only: proposes a
6
+ * `pactive config set … --confirm` command instead of writing — contract §2.3 / I59)
6
7
  * - command: `/pactive status|tasks|schedule <when> <prompt>|help`
7
8
  * - events: session_start / input / agent_start / tool_execution_start /
8
9
  * message_end / agent_settled / session_shutdown → activity + control tracking
@@ -193,16 +194,81 @@ function textResult(text: string, details?: Record<string, unknown>): PactiveToo
193
194
  return details ? { content: [{ type: "text", text }], details } : { content: [{ type: "text", text }] };
194
195
  }
195
196
 
196
- const STATUS_PARAMETERS = { type: "object", properties: {}, additionalProperties: false };
197
+ const STATUS_PARAMETERS = {
198
+ type: "object",
199
+ properties: { assistant: { type: "string", description: "只读:助理名或 id;不传 = 全部助理概览" } },
200
+ additionalProperties: false,
201
+ };
202
+ const ASSISTANTS_PARAMETERS = { type: "object", properties: {}, additionalProperties: false };
203
+ const FLEET_PARAMETERS = { type: "object", properties: {}, additionalProperties: false };
197
204
  const SCHEDULE_PARAMETERS = {
198
205
  type: "object",
199
206
  properties: {
200
207
  prompt: { type: "string", description: "任务正文(到点注入给会话的内容)" },
201
208
  when: { type: "string", description: "触发时间:时长(10m)/ 时刻(18:30)/ ISO 8601 / every 1h" },
209
+ assistant: { type: "string", description: "记在哪个助理名下(名字或 id);不传 = 默认助理" },
202
210
  },
203
211
  required: ["prompt"],
204
212
  additionalProperties: false,
205
213
  };
214
+ const CONFIG_PARAMETERS = {
215
+ type: "object",
216
+ properties: {
217
+ key: { type: "string", description: "配置键;缺省返回全部(idle.enabled / idle.thresholdMs / policy.* / globalGuard.hourlyLimit)" },
218
+ value: { type: "string", description: "想改成的新值;给了就只返回 `pactive config set <key> <value> --confirm`,不落盘" },
219
+ kind: { type: "string", description: "提议哪种写操作:assistant.add | assistant.remove | assistant.enable(只返回命令,不落盘)" },
220
+ name: { type: "string", description: "kind=assistant.* 时要操作的助理名或 id(缺 id 时用名字)" },
221
+ },
222
+ additionalProperties: false,
223
+ };
224
+
225
+ /** One assistant as the model needs to see it: switch, pending work, next action. */
226
+ function assistantView(core: any, state: any, record: any): Record<string, unknown> {
227
+ const all = core.schedule?.list ? core.schedule.list(state, record.assistantId) : [];
228
+ const active = (Array.isArray(all) ? all : []).filter((t: any) => t && t.active !== false);
229
+ const dueAt = active.map((t: any) => t.dueAt).filter((n: unknown) => Number.isFinite(n));
230
+ return {
231
+ assistantId: record.assistantId,
232
+ name: record.name,
233
+ title: record.title,
234
+ enabled: record.enabled === true,
235
+ role: record.role ?? "",
236
+ tasks: active.length,
237
+ nextDueAt: dueAt.length > 0 ? Math.min(...dueAt) : null,
238
+ lastFiredAt: record.lastFiredAt ?? 0,
239
+ firedCount: record.firedCount ?? 0,
240
+ policy: record.policy ?? {},
241
+ };
242
+ }
243
+
244
+ function assistantOverview(core: any, state: any): any[] {
245
+ const list = core.assistants?.list ? core.assistants.list(state) : [];
246
+ return list.map((record: any) => assistantView(core, state, record));
247
+ }
248
+
249
+ /**
250
+ * I66: the model only *proposes* — every assistant write comes back as one copy-pasteable
251
+ * command whose `--confirm` a human has to type. Nothing here writes.
252
+ */
253
+ function assistCommandFor(kind: string, name: string, value: string, fleet: any[]): { ok: boolean; command?: string; reason?: string } {
254
+ const known = fleet.map((a) => a.name);
255
+ if (kind === "assistant.add") {
256
+ if (name === "") return { ok: false, reason: "kind=assistant.add 需要 name" };
257
+ if (known.includes(name)) return { ok: false, reason: `助理已经存在(${name}),已有:${known.join(", ")}` };
258
+ return { ok: true, command: `pactive assistant add ${name} --confirm` };
259
+ }
260
+ if (kind === "assistant.remove" || kind === "assistant.enable") {
261
+ if (name === "") return { ok: false, reason: `kind=${kind} 需要 name` };
262
+ const hit = fleet.find((a) => a.name === name || a.assistantId === name);
263
+ if (!hit) return { ok: false, reason: `未知助理(${name});已有:${known.join(", ") || "(none yet)"}` };
264
+ if (kind === "assistant.enable") {
265
+ const on = value === "" ? !hit.enabled : value === "true" || value === "1" || value === "on";
266
+ return { ok: true, command: `pactive assistant ${on ? "enable" : "disable"} ${hit.name} --confirm` };
267
+ }
268
+ return { ok: true, command: `pactive assistant rm ${hit.name} --confirm` };
269
+ }
270
+ return { ok: false, reason: `未知 kind(${kind});支持 assistant.add / assistant.remove / assistant.enable` };
271
+ }
206
272
 
207
273
  /** Mark an activity kind; never throws. */
208
274
  async function markActivity(kind: "user" | "agent" | "session-start" | "session-end" | "tool"): Promise<void> {
@@ -237,28 +303,59 @@ export default function activate(pi: PactiveExtensionAPI): void {
237
303
  name: "pactive_status",
238
304
  label: "Pactive Status",
239
305
  description:
240
- "Pactive 主动机制状态:空闲判定与 reason、护栏四闸门、定时任务数、最近活动。只读。",
306
+ "当用户问「我现在闲不闲」「主动提醒开启了吗」「为什么没触发」「刚才主动做了什么」「盯盘那个助理怎么样了」时用。" +
307
+ "→ 读空闲判定、reason、护栏闸门与任务数;给 assistant 看该助理,不给看全部助理概览。只读,不落盘。",
241
308
  parameters: STATUS_PARAMETERS,
242
- async execute() {
309
+ async execute(_toolCallId: string, params: Record<string, any>) {
243
310
  try {
244
311
  const core = await loadCore();
245
312
  const now = Date.now();
246
313
  const paths = core.resolvePaths(HOST_KIND, process.env);
247
314
  const state = (await readState(core, paths)) ?? core.defaultState();
315
+ const globalGuard = state.globalGuard ?? null;
316
+ const activity = core.getActivity ? core.getActivity(state) : null;
317
+ const requested = typeof params?.assistant === "string" ? params.assistant.trim() : "";
318
+ if (requested !== "") {
319
+ const record = core.assistants?.resolve ? core.assistants.resolve(state, requested) : null;
320
+ if (!record) {
321
+ const names = assistantOverview(core, state).map((a) => a.name);
322
+ return textResult(
323
+ `pactive_status 失败:未知助理(${requested});已知:${names.join(", ") || "(none yet)"}`,
324
+ { error: true },
325
+ );
326
+ }
327
+ const idle = core.evaluateIdle
328
+ ? core.evaluateIdle(state, now, {}, record.assistantId)
329
+ : { idle: false, reason: "requires-action" };
330
+ const guard = core.checkGuard
331
+ ? core.checkGuard(state, now, record.assistantId)
332
+ : { pass: false, reason: "requires-action" };
333
+ return textResult(
334
+ JSON.stringify(
335
+ { host: HOST_KIND, now, assistant: assistantView(core, state, record), idle, guard, globalGuard, activity },
336
+ null,
337
+ 2,
338
+ ),
339
+ { host: HOST_KIND },
340
+ );
341
+ }
248
342
  const idle = core.evaluateIdle
249
343
  ? core.evaluateIdle(state, now, state.policy ?? {})
250
344
  : { idle: false, reason: "requires-action" };
251
345
  const guard = core.checkGuard ? core.checkGuard(state, now) : { pass: false, reason: "requires-action" };
252
346
  const tasks = core.schedule?.list ? core.schedule.list(state) : (state.tasks ?? []);
253
- const activity = core.getActivity ? core.getActivity(state) : null;
347
+ const fleet = assistantOverview(core, state);
254
348
  return textResult(
255
349
  JSON.stringify(
256
350
  {
257
351
  host: HOST_KIND,
258
352
  now,
259
353
  enabled: state.idle?.enabled ?? false,
354
+ assistants: fleet,
355
+ assistantCount: fleet.length,
260
356
  idle,
261
357
  guard,
358
+ globalGuard,
262
359
  tasks: Array.isArray(tasks) ? tasks.length : 0,
263
360
  activity,
264
361
  },
@@ -273,10 +370,56 @@ export default function activate(pi: PactiveExtensionAPI): void {
273
370
  },
274
371
  });
275
372
 
373
+ pi.registerTool({
374
+ name: "pactive_assistants",
375
+ label: "Pactive Assistants",
376
+ description:
377
+ "当用户问「我有几个助理」「有哪些助理」「盯盘助理开着吗」「各自下次什么时候动作」时用。" +
378
+ "→ 列出全部助理的名称、开关、任务数、下次到期与触发次数。只读,不落盘。",
379
+ parameters: ASSISTANTS_PARAMETERS,
380
+ async execute() {
381
+ try {
382
+ const core = await loadCore();
383
+ const paths = core.resolvePaths(HOST_KIND, process.env);
384
+ const state = (await readState(core, paths)) ?? core.defaultState();
385
+ const fleet = assistantOverview(core, state);
386
+ return textResult(JSON.stringify({ ok: true, count: fleet.length, assistants: fleet }, null, 2), {
387
+ count: fleet.length,
388
+ });
389
+ } catch (err) {
390
+ return textResult(`pactive_assistants 失败:${errorText(err)}`, { error: true });
391
+ }
392
+ },
393
+ });
394
+
395
+ pi.registerTool({
396
+ name: "pactive_fleet",
397
+ label: "Pactive Fleet",
398
+ description:
399
+ "当用户问「我有几个助理在跑」「盯盘那个助理在哪个 pane」「助理的窗口还活着吗」「上次主动投递送达了吗」时用。" +
400
+ "→ 读助理 ↔ herdr pane 绑定、agent 状态与最近一次投递;pane 状态来自 herdr,未观测到就写 unobserved。只读,不落盘。",
401
+ parameters: FLEET_PARAMETERS,
402
+ async execute() {
403
+ try {
404
+ const core = await loadCore();
405
+ const fleetView = await import(new URL("../src/hosts/herdr/fleet-view.mjs", import.meta.url).href);
406
+ const paths = core.resolvePaths(HOST_KIND, process.env);
407
+ const state = (await readState(core, paths)) ?? core.defaultState();
408
+ const payload = fleetView.fleetPayload(core, state, paths, process.env);
409
+ return textResult(JSON.stringify(payload, null, 2), { count: payload.count });
410
+ } catch (err) {
411
+ return textResult(`pactive_fleet 失败:${errorText(err)}`, { error: true });
412
+ }
413
+ },
414
+ });
415
+
276
416
  pi.registerTool({
277
417
  name: "pactive_schedule",
278
418
  label: "Pactive Schedule",
279
- description: "登记一个定时任务(真正落盘)。when 支持时长(10m)、时刻(18:30)、ISO 时间、every 1h。",
419
+ description:
420
+ "当用户说「10 分钟后提醒我喝水」「18:30 提醒我开会」「每小时检查一次」时用。" +
421
+ "→ 登记一条定时任务并立即落盘;when 支持 10m / 18:30 / ISO 8601 / every 1h;assistant 不填就记在默认助理名下。" +
422
+ "会写盘,需要人工确认(只在用户明确要求时调用)。",
280
423
  parameters: SCHEDULE_PARAMETERS,
281
424
  async execute(_toolCallId: string, params: Record<string, any>) {
282
425
  try {
@@ -289,22 +432,40 @@ export default function activate(pi: PactiveExtensionAPI): void {
289
432
  }
290
433
  const core = await loadCore();
291
434
  const paths = core.resolvePaths(HOST_KIND, process.env);
435
+ const requested = typeof params?.assistant === "string" ? params.assistant.trim() : "";
436
+ let assistantId: string | null = null;
437
+ if (requested !== "") {
438
+ const before = (await readState(core, paths)) ?? core.defaultState();
439
+ const record = core.assistants?.resolve ? core.assistants.resolve(before, requested) : null;
440
+ if (!record) {
441
+ const names = assistantOverview(core, before).map((a) => a.name);
442
+ return textResult(
443
+ `pactive_schedule 失败:未知助理(${requested});已知:${names.join(", ") || "(none yet)"}`,
444
+ { error: true },
445
+ );
446
+ }
447
+ assistantId = record.assistantId;
448
+ }
292
449
  const next = await mutateState(core, paths, (state) => {
450
+ const targetId =
451
+ assistantId ??
452
+ (core.assistants?.ensureDefault ? core.assistants.ensureDefault(state).assistantId : null);
293
453
  const out = core.schedule.add(
294
454
  state,
295
- { ...spec, prompt, content: prompt, owner: state.identity?.agentId },
455
+ { ...spec, prompt, content: prompt, owner: state.identity?.agentId, assistantId: targetId },
296
456
  now,
297
457
  );
298
458
  if (out && typeof out === "object" && out.state) return out.state;
299
459
  return out && Array.isArray(out.tasks) ? out : state;
300
460
  });
301
- const tasks = core.schedule?.list ? core.schedule.list(next) : (next.tasks ?? []);
461
+ const tasks = core.schedule?.list ? core.schedule.list(next, assistantId ?? undefined) : (next.tasks ?? []);
302
462
  const created = Array.isArray(tasks) ? tasks[tasks.length - 1] : null;
303
463
  return textResult(
304
464
  JSON.stringify(
305
465
  {
306
466
  ok: true,
307
467
  taskId: created?.id ?? created?.taskId ?? null,
468
+ assistantId: created?.assistantId ?? assistantId,
308
469
  dueAt: created?.dueAt ?? spec.at ?? null,
309
470
  kind: spec.kind,
310
471
  when: spec.when,
@@ -321,6 +482,83 @@ export default function activate(pi: PactiveExtensionAPI): void {
321
482
  },
322
483
  });
323
484
 
485
+ pi.registerTool({
486
+ name: "pactive_config",
487
+ label: "Pactive Config",
488
+ description:
489
+ "当用户问「主动提醒的开关开着吗」「阈值是多少」「安静时间是几点」「帮我把主动提醒打开」「再给我加一个盯盘的助理」时用。" +
490
+ "→ 读配置值与默认值;要改配置或增删 / 开关助理时,只返回一条需要人工确认的可复制命令。只读,不落盘。",
491
+ parameters: CONFIG_PARAMETERS,
492
+ async execute(_toolCallId: string, params: Record<string, any>) {
493
+ try {
494
+ // Read-only by construction: a write request returns the command, never a write (contract §2.3 / I66).
495
+ const core = await loadCore();
496
+ const described = core.config?.describe ? core.config.describe() : { schema: {}, defaults: {} };
497
+ const known: string[] = Object.keys(described.defaults);
498
+ const key = typeof params?.key === "string" ? params.key.trim() : "";
499
+ const rawValue = params?.value === undefined || params?.value === null ? "" : String(params.value).trim();
500
+ const kind = typeof params?.kind === "string" ? params.kind.trim() : "";
501
+ const name = typeof params?.name === "string" ? params.name.trim() : "";
502
+ const paths = core.resolvePaths(HOST_KIND, process.env);
503
+ const state = (await readState(core, paths)) ?? core.defaultState();
504
+ const fleet = assistantOverview(core, state);
505
+
506
+ if (kind !== "") {
507
+ const command = assistCommandFor(kind, name, rawValue, fleet);
508
+ if (!command.ok) return textResult(`pactive_config 失败:${command.reason}`, { error: true });
509
+ return textResult(
510
+ JSON.stringify(
511
+ {
512
+ ok: false,
513
+ wrote: false,
514
+ reason: "need_human_confirmation",
515
+ command: command.command,
516
+ kind,
517
+ before: { assistants: fleet.length, names: fleet.map((a) => a.name) },
518
+ hint: "不要自己改状态文件;把 command 原样给用户,等他执行后下一轮用 pactive_assistants 复核。",
519
+ },
520
+ null,
521
+ 2,
522
+ ),
523
+ { needHumanConfirmation: true },
524
+ );
525
+ }
526
+
527
+ if (key !== "" && !known.includes(key)) {
528
+ return textResult(`pactive_config 失败:未知配置键(${key});已知:${known.join(", ")}`, { error: true });
529
+ }
530
+ const values = (key === "" ? known : [key]).map((k: string) => ({
531
+ key: k,
532
+ value: core.config?.get ? core.config.get(state, k) : null,
533
+ default: described.defaults[k] ?? null,
534
+ humanGated: described.schema?.[k]?.humanGated === true,
535
+ }));
536
+ if (rawValue === "") {
537
+ return textResult(
538
+ JSON.stringify({ ok: true, readOnly: true, wrote: false, values, assistants: fleet.length }, null, 2),
539
+ { readOnly: true },
540
+ );
541
+ }
542
+ return textResult(
543
+ JSON.stringify(
544
+ {
545
+ ok: false,
546
+ wrote: false,
547
+ reason: "need_human_confirmation",
548
+ command: `pactive config set ${key} ${rawValue} --confirm`,
549
+ hint: "不要自己改状态文件;把 command 原样给用户,等他执行后下一轮用 pactive_status 复核。",
550
+ values,
551
+ },
552
+ null,
553
+ 2,
554
+ ),
555
+ { needHumanConfirmation: true },
556
+ );
557
+ } catch (err) {
558
+ return textResult(`pactive_config 失败:${errorText(err)}`, { error: true });
559
+ }
560
+ },
561
+ });
324
562
  pi.registerCommand("pactive", {
325
563
  description: "Pactive:status | tasks | schedule <when> <prompt> | help",
326
564
  async handler(args: string, ctx: PactiveUiContext) {
@@ -1,28 +1,44 @@
1
1
  ---
2
2
  name: pactive
3
3
  description: >-
4
- Correct usage of the pactive proactive-assistant extension (tools pactive_status / pactive_schedule /
5
- pactive_tell / pactive_history and the "/pactive" command family). Use when the task involves checking
6
- whether the idle trigger fired or why it did not, scheduling work for later (one-shot or
7
- repeating), reading the pactive event ledger, or sending a message to another session on the same
8
- machine. Covers the semantics traps (queued is not delivered, idle is disabled by default,
9
- human-gated config needs CLI --confirm, the Stop hook blocks at most once per turn) and the
10
- "when" syntax (10m / 18:30 / ISO 8601 / every 1h).
4
+ 用自然语言操作 pactive(主动提醒 / 定时任务 / 跨会话消息 / 事件账本)。用户说「我现在闲不闲」
5
+ 「10 分钟后提醒我喝水」「帮我开启主动提醒」「刚才主动提醒都发生了什么」时加载本技能。
6
+ 写操作一律不静默落盘:给出可复制命令 `pactive config set … --confirm`,用户执行后下一轮用
7
+ `pactive_status` / `pactive_config` 复核;含 reason 枚举、「when」语法与排障。
11
8
  ---
12
9
 
13
- # pactive:怎么正确用它
10
+ # pactive:用户会怎么说,你就怎么做
14
11
 
15
- pactive 给会话加**主动机制**:空闲判定(到点才动手)、**定时任务**、**跨会话消息**与**事件账本**。
12
+ **默认全部用自然语言。** 用户不会说「把 `idle.enabled` 置为 true」,他会说「帮我开启主动提醒」。
13
+ 你的工作是把这句话翻译成**一次只读查询**,或**一条可复制的命令**——不是替他改状态文件。
16
14
 
17
- ## 工具与命令
15
+ ## 用户会怎么说 → 你做什么
18
16
 
19
- | 入口 | 作用 |
17
+ | 用户说 | 你做什么 |
20
18
  |---|---|
21
- | `pactive_status` / `/pactive status` | 空闲判定与 `reason`、护栏四闸门、定时任务数、最近活动(只读) |
22
- | `pactive_schedule` / `/pactive schedule <when> <prompt>` | 登记一个定时任务(真正落盘,重启后仍在) |
23
- | `pactive_tell <to> <body>` | 给同机另一个会话投递消息(离线时排队,不伪装成功) |
24
- | `pactive_history [n]` | 读事件账本尾部:触发 → 投递 → 回应 → 结账 |
25
- | `/pactive tasks` | 列出待触发的定时任务 |
19
+ | 「我现在闲不闲?」「为什么没触发?」 | 调 `pactive_status`,读 `idle.reason` 与护栏结论,照实解释 |
20
+ | 「主动提醒开启了吗?」「阈值是多少?」 | 调 `pactive_config`(只读),报当前值与默认值 |
21
+ | 「10 分钟后提醒我喝水」「18:30 提醒我开会」「每小时检查一次」 | 调 `pactive_schedule` `{prompt, when}`(用户已明确要求,会写盘) |
22
+ | 「帮我开启主动提醒」「把阈值改成一小时」 | **不写**:调 `pactive_config` 拿命令,把**一条**命令交给用户 |
23
+ | 「把那个定时任务取消」 | **不写**:给出 `pactive cancel <taskId> --confirm`,让用户自己执行 |
24
+ | 「刚才主动提醒都发生了什么?」 | 调 `pactive_history`,只报可核验的事实(时间戳 + 事件种类) |
25
+ | 「告诉另一个会话我有空」 | 调 `pactive_tell` `{to, body}`;回执是 `queued` 就照实说**还没送到** |
26
+ | 「我有几个助理?」「盯盘助理开着吗?」 | 调 `pactive_assistants`(只读);要单个详情调 `pactive_status` `{assistant}` |
27
+ | 「再给我加一个盯盘的助理」「把巡检关掉」 | **不写**:调 `pactive_config` `{kind:"assistant.add"\|"assistant.remove"\|"assistant.enable", name}` 拿命令 |
28
+ | 「10 分钟后提醒我看盘片」并且有多个助理 | 调 `pactive_schedule` `{prompt, when, assistant}`,把任务记到指定助理名下 |
29
+
30
+ ## 写请求的三步(硬规则)
31
+
32
+ 1. **不静默写状态。** 开关、阈值、取消任务一律不由你落盘:不要改 `state.json`,不要绕过 `--confirm`。
33
+ 模型侧改配置只会得到 `need_human_confirmation`,这是设计,不是故障。
34
+ 2. **给一条可直接复制的命令。** 只给一条,不要列一堆选项:
35
+ ```
36
+ pactive config set idle.enabled true --confirm
37
+ pactive cancel <taskId> --confirm
38
+ ```
39
+ 同时说明它会写盘、写到哪里(默认 `~/.pactive/state.json`,可用 `PACTIVE_STATE_DIR` 覆盖)。
40
+ 3. **下一轮复核。** 用户说「好了/执行了」之后,再调 `pactive_status`(或 `pactive_config`)确认真的生效,
41
+ 然后回报实测值。**没复核就说「已开启」等于撒谎。**
26
42
 
27
43
  ## 关键语义(别踩)
28
44
 
@@ -31,8 +47,12 @@ pactive 给会话加**主动机制**:空闲判定(到点才动手)、**定
31
47
  没触发时先看 `reason`,不要猜。
32
48
  - **四闸门缺一不可**:控制权已交回 + 不忙 + 超过阈值 + 护栏通过,才可能触发。
33
49
  - **`queued` ≠ 已送达**:对端离线时消息进 outbox,等对端上线再补投;回执写 `queued` 就是还没送到。
34
- - **配置是人工闸门**:`config` 只能由人用 CLI `--confirm` 改;模型/MCP 侧改配置一律得到
35
- `need_human_confirmation`,且配置不变。
50
+ - **配置是人工闸门**:只有人能加 `--confirm`;你给命令,他执行。
51
+ - **助理是一等对象**(I61–I66):`assistantId` 形如 `盯盘#<短哈希>`;建/删/开关都走 `pactive assistant … --confirm`;
52
+ 新建助理默认 `enabled=false`(装上不等于打扰);删助理会连带删掉它的任务,**不留孤儿**;重命名不改 id。
53
+ - **按助理隔离 + 全局闸门**:每个助理各有一份计数/冷却,A 的冷却压不住 B;但全局每小时总投递有上限(默认 24),
54
+ 超限时每个助理都记 `reason=global-limit`,不会静默丢。
55
+ - **`tick` 是最小驱动**:`pactive tick` 跑一轮「到期任务 + 空闲判定」,可投递项落 ledger(`delivered` / `blocked`+reason)。
36
56
  - **Stop hook 最多阻断一次**:同一轮里只阻断一次,避免死循环;`UserPromptSubmit` 会重置这个计数。
37
57
 
38
58
  ## `when` 语法
@@ -40,8 +60,25 @@ pactive 给会话加**主动机制**:空闲判定(到点才动手)、**定
40
60
  `10m` / `2h` / `30s`(相对时长)、`18:30`(当天时刻,已过则顺延到明天)、
41
61
  ISO 8601 时间戳(绝对时刻)、`every 1h`(周期)。缺省 `10m`。
42
62
 
43
- ## 排障
63
+ ## 排障(先查,再说话)
44
64
 
45
- - 状态面板里 `reason=disabled` → 开关没开;`reason=busy` → 宿主还在忙;`reason=threshold` → 还没到阈值。
65
+ - `reason=disabled` → 开关没开;`reason=busy` → 宿主还在忙;`reason=threshold` → 还没到阈值。
46
66
  - 任务登记了但没触发 → 先看 `pactive_history` 里有没有 `triggered` 记录;没有说明调度器没跑到。
47
67
  - 消息一直是 `queued` → 对端不在线;看 `pactive_status` 的 peers。
68
+ - 「没记录」只说明**没有证据**,不等于「没问题」——照实这么说。
69
+
70
+ ## 命令面(给人用的,你只在需要时引用)
71
+
72
+ ```
73
+ pactive config get [<key>] # 只读:当前值与默认值
74
+ pactive config set <key> <value> --confirm # 写;缺 --confirm → exit 2 且零写盘
75
+ pactive schedule "<prompt>" --when <when> [--assistant <名字>] [--confirm]
76
+ pactive tasks [--assistant <名字>] # id / 类型 / 下次到期 / 归属
77
+ pactive cancel <taskId> [--confirm]
78
+ pactive tick [--assistant <名字>] # 跑一轮:到期任务 + 空闲判定 → ledger
79
+ pactive assistant add <name> [--title …] [--role …] [--scope <glob>] --confirm
80
+ pactive assistant ls | show <name|id> # 只读
81
+ pactive assistant enable|disable <name|id> --confirm
82
+ pactive assistant rename <name|id> <newName> --confirm
83
+ pactive assistant rm <name|id> --confirm # 连带删它的任务
84
+ ```
@@ -1,28 +1,44 @@
1
1
  ---
2
2
  name: pactive
3
3
  description: >-
4
- Correct usage of the pactive proactive-assistant extension (tools pactive_status / pactive_schedule /
5
- pactive_tell / pactive_history and the "/pactive" command family). Use when the task involves checking
6
- whether the idle trigger fired or why it did not, scheduling work for later (one-shot or
7
- repeating), reading the pactive event ledger, or sending a message to another session on the same
8
- machine. Covers the semantics traps (queued is not delivered, idle is disabled by default,
9
- human-gated config needs CLI --confirm, the Stop hook blocks at most once per turn) and the
10
- "when" syntax (10m / 18:30 / ISO 8601 / every 1h).
4
+ 用自然语言操作 pactive(主动提醒 / 定时任务 / 跨会话消息 / 事件账本)。用户说「我现在闲不闲」
5
+ 「10 分钟后提醒我喝水」「帮我开启主动提醒」「刚才主动提醒都发生了什么」时加载本技能。
6
+ 写操作一律不静默落盘:给出可复制命令 `pactive config set … --confirm`,用户执行后下一轮用
7
+ `pactive_status` / `pactive_config` 复核;含 reason 枚举、「when」语法与排障。
11
8
  ---
12
9
 
13
- # pactive:怎么正确用它
10
+ # pactive:用户会怎么说,你就怎么做
14
11
 
15
- pactive 给会话加**主动机制**:空闲判定(到点才动手)、**定时任务**、**跨会话消息**与**事件账本**。
12
+ **默认全部用自然语言。** 用户不会说「把 `idle.enabled` 置为 true」,他会说「帮我开启主动提醒」。
13
+ 你的工作是把这句话翻译成**一次只读查询**,或**一条可复制的命令**——不是替他改状态文件。
16
14
 
17
- ## 工具与命令
15
+ ## 用户会怎么说 → 你做什么
18
16
 
19
- | 入口 | 作用 |
17
+ | 用户说 | 你做什么 |
20
18
  |---|---|
21
- | `pactive_status` / `/pactive status` | 空闲判定与 `reason`、护栏四闸门、定时任务数、最近活动(只读) |
22
- | `pactive_schedule` / `/pactive schedule <when> <prompt>` | 登记一个定时任务(真正落盘,重启后仍在) |
23
- | `pactive_tell <to> <body>` | 给同机另一个会话投递消息(离线时排队,不伪装成功) |
24
- | `pactive_history [n]` | 读事件账本尾部:触发 → 投递 → 回应 → 结账 |
25
- | `/pactive tasks` | 列出待触发的定时任务 |
19
+ | 「我现在闲不闲?」「为什么没触发?」 | 调 `pactive_status`,读 `idle.reason` 与护栏结论,照实解释 |
20
+ | 「主动提醒开启了吗?」「阈值是多少?」 | 调 `pactive_config`(只读),报当前值与默认值 |
21
+ | 「10 分钟后提醒我喝水」「18:30 提醒我开会」「每小时检查一次」 | 调 `pactive_schedule` `{prompt, when}`(用户已明确要求,会写盘) |
22
+ | 「帮我开启主动提醒」「把阈值改成一小时」 | **不写**:调 `pactive_config` 拿命令,把**一条**命令交给用户 |
23
+ | 「把那个定时任务取消」 | **不写**:给出 `pactive cancel <taskId> --confirm`,让用户自己执行 |
24
+ | 「刚才主动提醒都发生了什么?」 | 调 `pactive_history`,只报可核验的事实(时间戳 + 事件种类) |
25
+ | 「告诉另一个会话我有空」 | 调 `pactive_tell` `{to, body}`;回执是 `queued` 就照实说**还没送到** |
26
+ | 「我有几个助理?」「盯盘助理开着吗?」 | 调 `pactive_assistants`(只读);要单个详情调 `pactive_status` `{assistant}` |
27
+ | 「再给我加一个盯盘的助理」「把巡检关掉」 | **不写**:调 `pactive_config` `{kind:"assistant.add"\|"assistant.remove"\|"assistant.enable", name}` 拿命令 |
28
+ | 「10 分钟后提醒我看盘片」并且有多个助理 | 调 `pactive_schedule` `{prompt, when, assistant}`,把任务记到指定助理名下 |
29
+
30
+ ## 写请求的三步(硬规则)
31
+
32
+ 1. **不静默写状态。** 开关、阈值、取消任务一律不由你落盘:不要改 `state.json`,不要绕过 `--confirm`。
33
+ 模型侧改配置只会得到 `need_human_confirmation`,这是设计,不是故障。
34
+ 2. **给一条可直接复制的命令。** 只给一条,不要列一堆选项:
35
+ ```
36
+ pactive config set idle.enabled true --confirm
37
+ pactive cancel <taskId> --confirm
38
+ ```
39
+ 同时说明它会写盘、写到哪里(默认 `~/.pactive/state.json`,可用 `PACTIVE_STATE_DIR` 覆盖)。
40
+ 3. **下一轮复核。** 用户说「好了/执行了」之后,再调 `pactive_status`(或 `pactive_config`)确认真的生效,
41
+ 然后回报实测值。**没复核就说「已开启」等于撒谎。**
26
42
 
27
43
  ## 关键语义(别踩)
28
44
 
@@ -31,8 +47,12 @@ pactive 给会话加**主动机制**:空闲判定(到点才动手)、**定
31
47
  没触发时先看 `reason`,不要猜。
32
48
  - **四闸门缺一不可**:控制权已交回 + 不忙 + 超过阈值 + 护栏通过,才可能触发。
33
49
  - **`queued` ≠ 已送达**:对端离线时消息进 outbox,等对端上线再补投;回执写 `queued` 就是还没送到。
34
- - **配置是人工闸门**:`config` 只能由人用 CLI `--confirm` 改;模型/MCP 侧改配置一律得到
35
- `need_human_confirmation`,且配置不变。
50
+ - **配置是人工闸门**:只有人能加 `--confirm`;你给命令,他执行。
51
+ - **助理是一等对象**(I61–I66):`assistantId` 形如 `盯盘#<短哈希>`;建/删/开关都走 `pactive assistant … --confirm`;
52
+ 新建助理默认 `enabled=false`(装上不等于打扰);删助理会连带删掉它的任务,**不留孤儿**;重命名不改 id。
53
+ - **按助理隔离 + 全局闸门**:每个助理各有一份计数/冷却,A 的冷却压不住 B;但全局每小时总投递有上限(默认 24),
54
+ 超限时每个助理都记 `reason=global-limit`,不会静默丢。
55
+ - **`tick` 是最小驱动**:`pactive tick` 跑一轮「到期任务 + 空闲判定」,可投递项落 ledger(`delivered` / `blocked`+reason)。
36
56
  - **Stop hook 最多阻断一次**:同一轮里只阻断一次,避免死循环;`UserPromptSubmit` 会重置这个计数。
37
57
 
38
58
  ## `when` 语法
@@ -40,8 +60,25 @@ pactive 给会话加**主动机制**:空闲判定(到点才动手)、**定
40
60
  `10m` / `2h` / `30s`(相对时长)、`18:30`(当天时刻,已过则顺延到明天)、
41
61
  ISO 8601 时间戳(绝对时刻)、`every 1h`(周期)。缺省 `10m`。
42
62
 
43
- ## 排障
63
+ ## 排障(先查,再说话)
44
64
 
45
- - 状态面板里 `reason=disabled` → 开关没开;`reason=busy` → 宿主还在忙;`reason=threshold` → 还没到阈值。
65
+ - `reason=disabled` → 开关没开;`reason=busy` → 宿主还在忙;`reason=threshold` → 还没到阈值。
46
66
  - 任务登记了但没触发 → 先看 `pactive_history` 里有没有 `triggered` 记录;没有说明调度器没跑到。
47
67
  - 消息一直是 `queued` → 对端不在线;看 `pactive_status` 的 peers。
68
+ - 「没记录」只说明**没有证据**,不等于「没问题」——照实这么说。
69
+
70
+ ## 命令面(给人用的,你只在需要时引用)
71
+
72
+ ```
73
+ pactive config get [<key>] # 只读:当前值与默认值
74
+ pactive config set <key> <value> --confirm # 写;缺 --confirm → exit 2 且零写盘
75
+ pactive schedule "<prompt>" --when <when> [--assistant <名字>] [--confirm]
76
+ pactive tasks [--assistant <名字>] # id / 类型 / 下次到期 / 归属
77
+ pactive cancel <taskId> [--confirm]
78
+ pactive tick [--assistant <名字>] # 跑一轮:到期任务 + 空闲判定 → ledger
79
+ pactive assistant add <name> [--title …] [--role …] [--scope <glob>] --confirm
80
+ pactive assistant ls | show <name|id> # 只读
81
+ pactive assistant enable|disable <name|id> --confirm
82
+ pactive assistant rename <name|id> <newName> --confirm
83
+ pactive assistant rm <name|id> --confirm # 连带删它的任务
84
+ ```