pactive 0.0.1-alpha.1 → 0.0.3-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
@@ -1,9 +1,10 @@
1
1
  /**
2
- * Acti — Pi extension (the single shipped entry (Pi loads this .ts directly via jiti; there is no dist copy)).
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: `acti_status`, `acti_schedule`
6
- * - command: `/acti status|tasks|schedule <when> <prompt>|help`
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)
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
9
10
  *
@@ -21,12 +22,12 @@
21
22
  */
22
23
 
23
24
  /** Minimal mirror of the host API (kept local so the extension stays dependency-free). */
24
- export interface ActiToolResult {
25
+ export interface PactiveToolResult {
25
26
  content: Array<{ type: "text"; text: string }>;
26
27
  details?: Record<string, unknown>;
27
28
  }
28
29
 
29
- export interface ActiToolDefinition {
30
+ export interface PactiveToolDefinition {
30
31
  name: string;
31
32
  label?: string;
32
33
  description: string;
@@ -37,22 +38,22 @@ export interface ActiToolDefinition {
37
38
  signal?: AbortSignal,
38
39
  onUpdate?: (update: unknown) => void,
39
40
  ctx?: unknown,
40
- ): Promise<ActiToolResult>;
41
+ ): Promise<PactiveToolResult>;
41
42
  }
42
43
 
43
- export interface ActiExtensionAPI {
44
- registerTool(tool: ActiToolDefinition): void;
44
+ export interface PactiveExtensionAPI {
45
+ registerTool(tool: PactiveToolDefinition): void;
45
46
  registerCommand(
46
47
  name: string,
47
48
  definition: {
48
49
  description: string;
49
- handler(args: string, ctx: ActiUiContext): Promise<void> | void;
50
+ handler(args: string, ctx: PactiveUiContext): Promise<void> | void;
50
51
  },
51
52
  ): void;
52
53
  on(event: string, handler: (event: any, ctx?: any) => void): void;
53
54
  }
54
55
 
55
- export interface ActiUiContext {
56
+ export interface PactiveUiContext {
56
57
  ui?: { notify?: (message: string, level?: string) => void };
57
58
  sessionManager?: { getSessionId?: () => string | undefined };
58
59
  cwd?: string;
@@ -73,7 +74,7 @@ function errorText(err: unknown): string {
73
74
  async function loadCore(): Promise<any> {
74
75
  if (cachedCore) return cachedCore;
75
76
  const env = process.env;
76
- const explicit = env.ACTI_CORE_MODULE;
77
+ const explicit = env.PACTIVE_CORE_MODULE;
77
78
  const candidates = explicit ? [explicit, ...CORE_CANDIDATES] : CORE_CANDIDATES;
78
79
  const tried: string[] = [];
79
80
  for (const candidate of candidates) {
@@ -92,7 +93,7 @@ async function loadCore(): Promise<any> {
92
93
  }
93
94
  }
94
95
  throw new Error(
95
- `acti: core module not found (tried: ${tried.join(", ")}). Build src/core before loading the extension.`,
96
+ `pactive: core module not found (tried: ${tried.join(", ")}). Build src/core before loading the extension.`,
96
97
  );
97
98
  }
98
99
 
@@ -189,7 +190,7 @@ function parseWhen(when: unknown, now = Date.now()): { kind: string; when: strin
189
190
  return null;
190
191
  }
191
192
 
192
- function textResult(text: string, details?: Record<string, unknown>): ActiToolResult {
193
+ function textResult(text: string, details?: Record<string, unknown>): PactiveToolResult {
193
194
  return details ? { content: [{ type: "text", text }], details } : { content: [{ type: "text", text }] };
194
195
  }
195
196
 
@@ -203,6 +204,14 @@ const SCHEDULE_PARAMETERS = {
203
204
  required: ["prompt"],
204
205
  additionalProperties: false,
205
206
  };
207
+ const CONFIG_PARAMETERS = {
208
+ type: "object",
209
+ properties: {
210
+ key: { type: "string", description: "配置键;缺省返回全部(idle.enabled / idle.thresholdMs / policy.*)" },
211
+ value: { type: "string", description: "想改成的新值;给了就只返回 `pactive config set <key> <value> --confirm`,不落盘" },
212
+ },
213
+ additionalProperties: false,
214
+ };
206
215
 
207
216
  /** Mark an activity kind; never throws. */
208
217
  async function markActivity(kind: "user" | "agent" | "session-start" | "session-end" | "tool"): Promise<void> {
@@ -214,7 +223,7 @@ async function markActivity(kind: "user" | "agent" | "session-start" | "session-
214
223
  return next && typeof next === "object" ? next : state;
215
224
  });
216
225
  } catch (err) {
217
- process.stderr.write(`[acti] markActivity(${kind}): ${errorText(err)}\n`);
226
+ process.stderr.write(`[pactive] markActivity(${kind}): ${errorText(err)}\n`);
218
227
  }
219
228
  }
220
229
 
@@ -228,16 +237,17 @@ async function markControlReturned(): Promise<void> {
228
237
  return next && typeof next === "object" ? next : state;
229
238
  });
230
239
  } catch (err) {
231
- process.stderr.write(`[acti] markControlReturned: ${errorText(err)}\n`);
240
+ process.stderr.write(`[pactive] markControlReturned: ${errorText(err)}\n`);
232
241
  }
233
242
  }
234
243
 
235
- export default function activate(pi: ActiExtensionAPI): void {
244
+ export default function activate(pi: PactiveExtensionAPI): void {
236
245
  pi.registerTool({
237
- name: "acti_status",
238
- label: "Acti Status",
246
+ name: "pactive_status",
247
+ label: "Pactive Status",
239
248
  description:
240
- "Acti 主动机制状态:空闲判定与 reason、护栏四闸门、定时任务数、最近活动。只读。",
249
+ "当用户问「我现在闲不闲」「主动提醒开启了吗」「为什么没触发」「刚才主动做了什么」时用。" +
250
+ "→ 读当前空闲判定、reason、护栏四闸门、任务数与最近活动。只读,不落盘。",
241
251
  parameters: STATUS_PARAMETERS,
242
252
  async execute() {
243
253
  try {
@@ -268,24 +278,27 @@ export default function activate(pi: ActiExtensionAPI): void {
268
278
  { host: HOST_KIND },
269
279
  );
270
280
  } catch (err) {
271
- return textResult(`acti_status 失败:${errorText(err)}`, { error: true });
281
+ return textResult(`pactive_status 失败:${errorText(err)}`, { error: true });
272
282
  }
273
283
  },
274
284
  });
275
285
 
276
286
  pi.registerTool({
277
- name: "acti_schedule",
278
- label: "Acti Schedule",
279
- description: "登记一个定时任务(真正落盘)。when 支持时长(10m)、时刻(18:30)、ISO 时间、every 1h。",
287
+ name: "pactive_schedule",
288
+ label: "Pactive Schedule",
289
+ description:
290
+ "当用户说「10 分钟后提醒我喝水」「18:30 提醒我开会」「每小时检查一次」时用。" +
291
+ "→ 登记一条定时任务并立即落盘;when 支持 10m / 18:30 / ISO 8601 / every 1h。" +
292
+ "会写盘,需要人工确认(只在用户明确要求时调用)。",
280
293
  parameters: SCHEDULE_PARAMETERS,
281
294
  async execute(_toolCallId: string, params: Record<string, any>) {
282
295
  try {
283
296
  const prompt = typeof params?.prompt === "string" ? params.prompt.trim() : "";
284
- if (prompt === "") return textResult("acti_schedule 失败:prompt 必填", { error: true });
297
+ if (prompt === "") return textResult("pactive_schedule 失败:prompt 必填", { error: true });
285
298
  const now = Date.now();
286
299
  const spec = parseWhen(params?.when, now);
287
300
  if (!spec) {
288
- return textResult(`acti_schedule 失败:无法解析 when(${String(params?.when)})`, { error: true });
301
+ return textResult(`pactive_schedule 失败:无法解析 when(${String(params?.when)})`, { error: true });
289
302
  }
290
303
  const core = await loadCore();
291
304
  const paths = core.resolvePaths(HOST_KIND, process.env);
@@ -316,21 +329,70 @@ export default function activate(pi: ActiExtensionAPI): void {
316
329
  { taskId: created?.id ?? null },
317
330
  );
318
331
  } catch (err) {
319
- return textResult(`acti_schedule 失败:${errorText(err)}`, { error: true });
332
+ return textResult(`pactive_schedule 失败:${errorText(err)}`, { error: true });
320
333
  }
321
334
  },
322
335
  });
323
336
 
324
- pi.registerCommand("acti", {
325
- description: "Acti:status | tasks | schedule <when> <prompt> | help",
326
- async handler(args: string, ctx: ActiUiContext) {
337
+ pi.registerTool({
338
+ name: "pactive_config",
339
+ label: "Pactive Config",
340
+ description:
341
+ "当用户问「主动提醒的开关开着吗」「阈值是多少」「安静时间是几点」「帮我把主动提醒打开」时用。" +
342
+ "→ 读配置值与默认值;要改配置时只返回一条需要人工确认的可复制命令。只读,不落盘。",
343
+ parameters: CONFIG_PARAMETERS,
344
+ async execute(_toolCallId: string, params: Record<string, any>) {
345
+ try {
346
+ // Read-only by construction: a write request returns the command, never a write (contract §2.3).
347
+ const core = await loadCore();
348
+ const described = core.config?.describe ? core.config.describe() : { schema: {}, defaults: {} };
349
+ const known: string[] = Object.keys(described.defaults);
350
+ const key = typeof params?.key === "string" ? params.key.trim() : "";
351
+ const rawValue = params?.value === undefined || params?.value === null ? "" : String(params.value).trim();
352
+ if (key !== "" && !known.includes(key)) {
353
+ return textResult(`pactive_config 失败:未知配置键(${key});已知:${known.join(", ")}`, { error: true });
354
+ }
355
+ const paths = core.resolvePaths(HOST_KIND, process.env);
356
+ const state = (await readState(core, paths)) ?? core.defaultState();
357
+ const values = (key === "" ? known : [key]).map((k: string) => ({
358
+ key: k,
359
+ value: core.config?.get ? core.config.get(state, k) : null,
360
+ default: described.defaults[k] ?? null,
361
+ humanGated: described.schema?.[k]?.humanGated === true,
362
+ }));
363
+ if (rawValue === "") {
364
+ return textResult(JSON.stringify({ ok: true, readOnly: true, wrote: false, values }, null, 2), { readOnly: true });
365
+ }
366
+ return textResult(
367
+ JSON.stringify(
368
+ {
369
+ ok: false,
370
+ wrote: false,
371
+ reason: "need_human_confirmation",
372
+ command: `pactive config set ${key} ${rawValue} --confirm`,
373
+ hint: "不要自己改状态文件;把 command 原样给用户,等他执行后下一轮用 pactive_status 复核。",
374
+ values,
375
+ },
376
+ null,
377
+ 2,
378
+ ),
379
+ { needHumanConfirmation: true },
380
+ );
381
+ } catch (err) {
382
+ return textResult(`pactive_config 失败:${errorText(err)}`, { error: true });
383
+ }
384
+ },
385
+ });
386
+ pi.registerCommand("pactive", {
387
+ description: "Pactive:status | tasks | schedule <when> <prompt> | help",
388
+ async handler(args: string, ctx: PactiveUiContext) {
327
389
  const notify = (message: string, level = "info") => {
328
390
  try {
329
391
  ctx?.ui?.notify?.(message, level);
330
392
  } catch {
331
393
  /* the host may not have a UI in non-interactive modes */
332
394
  }
333
- process.stderr.write(`[acti] ${message}\n`);
395
+ process.stderr.write(`[pactive] ${message}\n`);
334
396
  };
335
397
  const parts = String(args ?? "").trim().split(/\s+/).filter(Boolean);
336
398
  const sub = parts[0] ?? "status";
@@ -357,7 +419,7 @@ export default function activate(pi: ActiExtensionAPI): void {
357
419
  notify(
358
420
  list.length === 0
359
421
  ? "没有定时任务"
360
- : `acti: ${list.length} 个定时任务\n` +
422
+ : `pactive: ${list.length} 个定时任务\n` +
361
423
  list.map((task: any) => `- [${task?.id ?? "?"}] ${task?.prompt ?? task?.content ?? ""}`).join("\n"),
362
424
  );
363
425
  return;
@@ -366,12 +428,12 @@ export default function activate(pi: ActiExtensionAPI): void {
366
428
  const when = parts[1] ?? "10m";
367
429
  const prompt = parts.slice(2).join(" ");
368
430
  if (prompt === "") {
369
- notify("用法:/acti schedule <when> <prompt>", "warning");
431
+ notify("用法:/pactive schedule <when> <prompt>", "warning");
370
432
  return;
371
433
  }
372
434
  const spec = parseWhen(when, now);
373
435
  if (!spec) {
374
- notify(`acti: 无法解析 when(${when})`, "warning");
436
+ notify(`pactive: 无法解析 when(${when})`, "warning");
375
437
  return;
376
438
  }
377
439
  const next = await mutateState(core, paths, (current) => {
@@ -385,12 +447,12 @@ export default function activate(pi: ActiExtensionAPI): void {
385
447
  });
386
448
  const tasks = core.schedule?.list ? core.schedule.list(next) : (next.tasks ?? []);
387
449
  const created = Array.isArray(tasks) ? tasks[tasks.length - 1] : null;
388
- notify(`acti: 已登记定时任务 ${created?.id ?? "-"}(${when})`);
450
+ notify(`pactive: 已登记定时任务 ${created?.id ?? "-"}(${when})`);
389
451
  return;
390
452
  }
391
- notify("用法:/acti status | tasks | schedule <when> <prompt> | help", "warning");
453
+ notify("用法:/pactive status | tasks | schedule <when> <prompt> | help", "warning");
392
454
  } catch (err) {
393
- notify(`acti 失败:${errorText(err)}`, "error");
455
+ notify(`pactive 失败:${errorText(err)}`, "error");
394
456
  }
395
457
  },
396
458
  });
@@ -0,0 +1,70 @@
1
+ ---
2
+ name: pactive
3
+ description: >-
4
+ 用自然语言操作 pactive(主动提醒 / 定时任务 / 跨会话消息 / 事件账本)。用户说「我现在闲不闲」
5
+ 「10 分钟后提醒我喝水」「帮我开启主动提醒」「刚才主动提醒都发生了什么」时加载本技能。
6
+ 写操作一律不静默落盘:给出可复制命令 `pactive config set … --confirm`,用户执行后下一轮用
7
+ `pactive_status` / `pactive_config` 复核;含 reason 枚举、「when」语法与排障。
8
+ ---
9
+
10
+ # pactive:用户会怎么说,你就怎么做
11
+
12
+ **默认全部用自然语言。** 用户不会说「把 `idle.enabled` 置为 true」,他会说「帮我开启主动提醒」。
13
+ 你的工作是把这句话翻译成**一次只读查询**,或**一条可复制的命令**——不是替他改状态文件。
14
+
15
+ ## 用户会怎么说 → 你做什么
16
+
17
+ | 用户说 | 你做什么 |
18
+ |---|---|
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
+
27
+ ## 写请求的三步(硬规则)
28
+
29
+ 1. **不静默写状态。** 开关、阈值、取消任务一律不由你落盘:不要改 `state.json`,不要绕过 `--confirm`。
30
+ 模型侧改配置只会得到 `need_human_confirmation`,这是设计,不是故障。
31
+ 2. **给一条可直接复制的命令。** 只给一条,不要列一堆选项:
32
+ ```
33
+ pactive config set idle.enabled true --confirm
34
+ pactive cancel <taskId> --confirm
35
+ ```
36
+ 同时说明它会写盘、写到哪里(默认 `~/.pactive/state.json`,可用 `PACTIVE_STATE_DIR` 覆盖)。
37
+ 3. **下一轮复核。** 用户说「好了/执行了」之后,再调 `pactive_status`(或 `pactive_config`)确认真的生效,
38
+ 然后回报实测值。**没复核就说「已开启」等于撒谎。**
39
+
40
+ ## 关键语义(别踩)
41
+
42
+ - **默认不打扰**:`idle.enabled` 默认 `false`。没打开开关,空闲永远不会触发主动行为。
43
+ - **`reason` 是枚举**:`idle | disabled | no-control | busy | threshold | quiet | hourly-limit | min-interval | cooldown | requires-action`。
44
+ 没触发时先看 `reason`,不要猜。
45
+ - **四闸门缺一不可**:控制权已交回 + 不忙 + 超过阈值 + 护栏通过,才可能触发。
46
+ - **`queued` ≠ 已送达**:对端离线时消息进 outbox,等对端上线再补投;回执写 `queued` 就是还没送到。
47
+ - **配置是人工闸门**:只有人能加 `--confirm`;你给命令,他执行。
48
+ - **Stop hook 最多阻断一次**:同一轮里只阻断一次,避免死循环;`UserPromptSubmit` 会重置这个计数。
49
+
50
+ ## `when` 语法
51
+
52
+ `10m` / `2h` / `30s`(相对时长)、`18:30`(当天时刻,已过则顺延到明天)、
53
+ ISO 8601 时间戳(绝对时刻)、`every 1h`(周期)。缺省 `10m`。
54
+
55
+ ## 排障(先查,再说话)
56
+
57
+ - `reason=disabled` → 开关没开;`reason=busy` → 宿主还在忙;`reason=threshold` → 还没到阈值。
58
+ - 任务登记了但没触发 → 先看 `pactive_history` 里有没有 `triggered` 记录;没有说明调度器没跑到。
59
+ - 消息一直是 `queued` → 对端不在线;看 `pactive_status` 的 peers。
60
+ - 「没记录」只说明**没有证据**,不等于「没问题」——照实这么说。
61
+
62
+ ## 命令面(给人用的,你只在需要时引用)
63
+
64
+ ```
65
+ pactive config get [<key>] # 只读:当前值与默认值
66
+ pactive config set <key> <value> --confirm # 写;缺 --confirm → exit 2 且零写盘
67
+ pactive schedule "<prompt>" --when <when> [--confirm]
68
+ pactive tasks # id / 类型 / 下次到期 / 归属
69
+ pactive cancel <taskId> [--confirm]
70
+ ```
@@ -0,0 +1,70 @@
1
+ ---
2
+ name: pactive
3
+ description: >-
4
+ 用自然语言操作 pactive(主动提醒 / 定时任务 / 跨会话消息 / 事件账本)。用户说「我现在闲不闲」
5
+ 「10 分钟后提醒我喝水」「帮我开启主动提醒」「刚才主动提醒都发生了什么」时加载本技能。
6
+ 写操作一律不静默落盘:给出可复制命令 `pactive config set … --confirm`,用户执行后下一轮用
7
+ `pactive_status` / `pactive_config` 复核;含 reason 枚举、「when」语法与排障。
8
+ ---
9
+
10
+ # pactive:用户会怎么说,你就怎么做
11
+
12
+ **默认全部用自然语言。** 用户不会说「把 `idle.enabled` 置为 true」,他会说「帮我开启主动提醒」。
13
+ 你的工作是把这句话翻译成**一次只读查询**,或**一条可复制的命令**——不是替他改状态文件。
14
+
15
+ ## 用户会怎么说 → 你做什么
16
+
17
+ | 用户说 | 你做什么 |
18
+ |---|---|
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
+
27
+ ## 写请求的三步(硬规则)
28
+
29
+ 1. **不静默写状态。** 开关、阈值、取消任务一律不由你落盘:不要改 `state.json`,不要绕过 `--confirm`。
30
+ 模型侧改配置只会得到 `need_human_confirmation`,这是设计,不是故障。
31
+ 2. **给一条可直接复制的命令。** 只给一条,不要列一堆选项:
32
+ ```
33
+ pactive config set idle.enabled true --confirm
34
+ pactive cancel <taskId> --confirm
35
+ ```
36
+ 同时说明它会写盘、写到哪里(默认 `~/.pactive/state.json`,可用 `PACTIVE_STATE_DIR` 覆盖)。
37
+ 3. **下一轮复核。** 用户说「好了/执行了」之后,再调 `pactive_status`(或 `pactive_config`)确认真的生效,
38
+ 然后回报实测值。**没复核就说「已开启」等于撒谎。**
39
+
40
+ ## 关键语义(别踩)
41
+
42
+ - **默认不打扰**:`idle.enabled` 默认 `false`。没打开开关,空闲永远不会触发主动行为。
43
+ - **`reason` 是枚举**:`idle | disabled | no-control | busy | threshold | quiet | hourly-limit | min-interval | cooldown | requires-action`。
44
+ 没触发时先看 `reason`,不要猜。
45
+ - **四闸门缺一不可**:控制权已交回 + 不忙 + 超过阈值 + 护栏通过,才可能触发。
46
+ - **`queued` ≠ 已送达**:对端离线时消息进 outbox,等对端上线再补投;回执写 `queued` 就是还没送到。
47
+ - **配置是人工闸门**:只有人能加 `--confirm`;你给命令,他执行。
48
+ - **Stop hook 最多阻断一次**:同一轮里只阻断一次,避免死循环;`UserPromptSubmit` 会重置这个计数。
49
+
50
+ ## `when` 语法
51
+
52
+ `10m` / `2h` / `30s`(相对时长)、`18:30`(当天时刻,已过则顺延到明天)、
53
+ ISO 8601 时间戳(绝对时刻)、`every 1h`(周期)。缺省 `10m`。
54
+
55
+ ## 排障(先查,再说话)
56
+
57
+ - `reason=disabled` → 开关没开;`reason=busy` → 宿主还在忙;`reason=threshold` → 还没到阈值。
58
+ - 任务登记了但没触发 → 先看 `pactive_history` 里有没有 `triggered` 记录;没有说明调度器没跑到。
59
+ - 消息一直是 `queued` → 对端不在线;看 `pactive_status` 的 peers。
60
+ - 「没记录」只说明**没有证据**,不等于「没问题」——照实这么说。
61
+
62
+ ## 命令面(给人用的,你只在需要时引用)
63
+
64
+ ```
65
+ pactive config get [<key>] # 只读:当前值与默认值
66
+ pactive config set <key> <value> --confirm # 写;缺 --confirm → exit 2 且零写盘
67
+ pactive schedule "<prompt>" --when <when> [--confirm]
68
+ pactive tasks # id / 类型 / 下次到期 / 归属
69
+ pactive cancel <taskId> [--confirm]
70
+ ```
@@ -1,4 +1,4 @@
1
- import { actiError } from './errors.mjs';
1
+ import { pactiveError } from './errors.mjs';
2
2
 
3
3
  export const CONFIG_SCHEMA = {
4
4
  'idle.enabled': { type: 'boolean', default: false, humanGated: true },
@@ -45,7 +45,7 @@ export const config = {
45
45
  function specOf(key) {
46
46
  const spec = Object.prototype.hasOwnProperty.call(CONFIG_SCHEMA, key) ? CONFIG_SCHEMA[key] : null;
47
47
  if (!spec) {
48
- throw actiError('CONFIG_KEY_UNKNOWN', `unknown config key: ${key}`);
48
+ throw pactiveError('CONFIG_KEY_UNKNOWN', `unknown config key: ${key}`);
49
49
  }
50
50
  return spec;
51
51
  }
@@ -1,7 +1,7 @@
1
1
  // Shared error helpers. The contract freezes one public error-code constant.
2
2
  export const NOT_IMPLEMENTED = 'NOT_IMPLEMENTED';
3
3
 
4
- export function actiError(code, message) {
4
+ export function pactiveError(code, message) {
5
5
  const err = new Error(message);
6
6
  err.code = code;
7
7
  return err;
@@ -2,7 +2,7 @@ import crypto from 'node:crypto';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
 
5
- import { actiError } from './errors.mjs';
5
+ import { pactiveError } from './errors.mjs';
6
6
 
7
7
  /**
8
8
  * Stable identity of an agent session.
@@ -14,7 +14,7 @@ export function makeIdentity({ name, hostKind, sessionId, cwd } = {}) {
14
14
  const h = typeof hostKind === 'string' ? hostKind.trim() : '';
15
15
  const s = typeof sessionId === 'string' ? sessionId.trim() : '';
16
16
  if (!n || !h || !s) {
17
- throw actiError('IDENTITY_INCOMPLETE', 'name, hostKind and sessionId are required');
17
+ throw pactiveError('IDENTITY_INCOMPLETE', 'name, hostKind and sessionId are required');
18
18
  }
19
19
  const projectHash = hashProject(cwd ?? process.cwd());
20
20
  return {
@@ -1,14 +1,14 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
- import { actiError } from './errors.mjs';
4
+ import { pactiveError } from './errors.mjs';
5
5
 
6
6
  let tmpSeq = 0;
7
7
 
8
8
  /** Append-only event ledger: one JSON object per line, private file. */
9
9
  export const ledger = {
10
10
  append(file, event) {
11
- if (typeof file !== 'string' || !file) throw actiError('LEDGER_PATH_REQUIRED', 'ledger path is required');
11
+ if (typeof file !== 'string' || !file) throw pactiveError('LEDGER_PATH_REQUIRED', 'ledger path is required');
12
12
  const dir = path.dirname(file);
13
13
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
14
14
  try {
package/src/core/lock.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
- import { actiError } from './errors.mjs';
4
+ import { pactiveError } from './errors.mjs';
5
5
 
6
6
  export const LOCK_STALE_MS = 30000;
7
7
  export const LOCK_TIMEOUT_MS = 10000;
@@ -53,7 +53,7 @@ export async function withLock(locksDir, name, fn, opts = {}) {
53
53
  continue;
54
54
  }
55
55
  if (Date.now() > deadline) {
56
- throw actiError('LOCK_TIMEOUT', `could not acquire lock ${lockFile}`);
56
+ throw pactiveError('LOCK_TIMEOUT', `could not acquire lock ${lockFile}`);
57
57
  }
58
58
  await sleep(20);
59
59
  }
@@ -2,9 +2,9 @@ import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
 
5
- import { actiError } from './errors.mjs';
5
+ import { pactiveError } from './errors.mjs';
6
6
 
7
- export const DEFAULT_ROOT_DIRNAME = '.acti';
7
+ export const DEFAULT_ROOT_DIRNAME = '.pactive';
8
8
 
9
9
  /**
10
10
  * Resolve the state root and every file location derived from it.
@@ -12,9 +12,9 @@ export const DEFAULT_ROOT_DIRNAME = '.acti';
12
12
  */
13
13
  export function resolvePaths(hostKind, env = process.env) {
14
14
  const kind = typeof hostKind === 'string' ? hostKind.trim() : '';
15
- if (!kind) throw actiError('HOST_KIND_REQUIRED', 'hostKind is required');
15
+ if (!kind) throw pactiveError('HOST_KIND_REQUIRED', 'hostKind is required');
16
16
 
17
- const source = env && typeof env.ACTI_STATE_DIR === 'string' ? env.ACTI_STATE_DIR.trim() : '';
17
+ const source = env && typeof env.PACTIVE_STATE_DIR === 'string' ? env.PACTIVE_STATE_DIR.trim() : '';
18
18
  const root = path.resolve(source || path.join(os.homedir(), DEFAULT_ROOT_DIRNAME));
19
19
 
20
20
  let stat = null;
@@ -25,12 +25,12 @@ export function resolvePaths(hostKind, env = process.env) {
25
25
  }
26
26
  if (stat) {
27
27
  if (!stat.isDirectory()) {
28
- throw actiError('STATE_ROOT_INVALID', `state root is not a directory: ${root}`);
28
+ throw pactiveError('STATE_ROOT_INVALID', `state root is not a directory: ${root}`);
29
29
  }
30
30
  try {
31
31
  fs.accessSync(root, fs.constants.W_OK);
32
32
  } catch {
33
- throw actiError('STATE_ROOT_NOT_WRITABLE', `state root is not writable: ${root}`);
33
+ throw pactiveError('STATE_ROOT_NOT_WRITABLE', `state root is not writable: ${root}`);
34
34
  }
35
35
  }
36
36
 
@@ -1,4 +1,4 @@
1
- import { actiError } from './errors.mjs';
1
+ import { pactiveError } from './errors.mjs';
2
2
 
3
3
  export const TASK_KINDS = ['in', 'at', 'every'];
4
4
 
@@ -10,28 +10,30 @@ let seq = 0;
10
10
  */
11
11
  export const schedule = {
12
12
  add(state, spec, now = Date.now()) {
13
- if (!spec || typeof spec !== 'object') throw actiError('TASK_SPEC_REQUIRED', 'a task spec is required');
13
+ if (!spec || typeof spec !== 'object') throw pactiveError('TASK_SPEC_REQUIRED', 'a task spec is required');
14
14
  const ts = Number.isFinite(now) ? now : Date.now();
15
15
  const kind = spec.kind ?? (Number.isFinite(spec.at) ? 'at' : 'in');
16
16
  if (!TASK_KINDS.includes(kind)) {
17
- throw actiError('TASK_KIND_UNKNOWN', `unknown task kind: ${kind}`);
17
+ throw pactiveError('TASK_KIND_UNKNOWN', `unknown task kind: ${kind}`);
18
18
  }
19
19
  if (!Array.isArray(state.tasks)) state.tasks = [];
20
20
 
21
21
  let dueAt;
22
22
  let everyMs = 0;
23
23
  if (kind === 'at') {
24
- if (!Number.isFinite(spec.at)) throw actiError('TASK_AT_REQUIRED', 'an "at" task needs spec.at');
24
+ if (!Number.isFinite(spec.at)) throw pactiveError('TASK_AT_REQUIRED', 'an "at" task needs spec.at');
25
25
  dueAt = spec.at;
26
26
  } else if (kind === 'every') {
27
27
  everyMs = Number.isFinite(spec.everyMs) ? spec.everyMs : Number.isFinite(spec.intervalMs) ? spec.intervalMs : 0;
28
- if (everyMs <= 0) throw actiError('TASK_INTERVAL_REQUIRED', 'an "every" task needs a positive interval');
28
+ if (everyMs <= 0) throw pactiveError('TASK_INTERVAL_REQUIRED', 'an "every" task needs a positive interval');
29
29
  dueAt = ts + everyMs;
30
30
  } else {
31
+ // A relative delay may arrive either as a duration (delayMs/afterMs/ms) or already
32
+ // resolved to an absolute time (spec.at — what every host-surface parseWhen produces).
31
33
  const delay = Number.isFinite(spec.delayMs) ? spec.delayMs
32
34
  : Number.isFinite(spec.afterMs) ? spec.afterMs
33
- : Number.isFinite(spec.ms) ? spec.ms : 0;
34
- dueAt = ts + delay;
35
+ : Number.isFinite(spec.ms) ? spec.ms : null;
36
+ dueAt = delay === null && Number.isFinite(spec.at) ? spec.at : ts + (delay ?? 0);
35
37
  }
36
38
 
37
39
  const owner = spec.owner ?? state.identity?.ownerKey ?? '';
@@ -48,6 +50,8 @@ export const schedule = {
48
50
  everyMs,
49
51
  label: spec.label ?? null,
50
52
  payload: spec.payload ?? null,
53
+ // The prompt is what the trigger injects later; dropping it registers an empty reminder.
54
+ prompt: spec.prompt ?? spec.content ?? null,
51
55
  createdAt: ts,
52
56
  lastRunAt: 0,
53
57
  active: true,
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
- import { actiError } from './errors.mjs';
4
+ import { pactiveError } from './errors.mjs';
5
5
  import { ensureDir, withLock } from './lock.mjs';
6
6
  import { ledger } from './lineage.mjs';
7
7
 
@@ -38,7 +38,7 @@ export function defaultState() {
38
38
 
39
39
  /** Accept either a paths object (from resolvePaths) or a state file path. */
40
40
  export function toPaths(target) {
41
- if (!target) throw actiError('STATE_TARGET_REQUIRED', 'a paths object or state file path is required');
41
+ if (!target) throw pactiveError('STATE_TARGET_REQUIRED', 'a paths object or state file path is required');
42
42
  if (typeof target === 'string') {
43
43
  const stateFile = path.resolve(target);
44
44
  const root = path.dirname(stateFile);
@@ -54,7 +54,7 @@ export function toPaths(target) {
54
54
  };
55
55
  }
56
56
  if (typeof target.stateFile !== 'string' || !target.stateFile) {
57
- throw actiError('STATE_TARGET_REQUIRED', 'a paths object or state file path is required');
57
+ throw pactiveError('STATE_TARGET_REQUIRED', 'a paths object or state file path is required');
58
58
  }
59
59
  const root = target.root ?? path.dirname(target.stateFile);
60
60
  return {
@@ -111,7 +111,7 @@ export function loadState(target) {
111
111
  try {
112
112
  const parsed = JSON.parse(raw);
113
113
  if (!parsed || typeof parsed !== 'object' || parsed.schemaVersion !== SCHEMA_VERSION) {
114
- throw actiError('STATE_SCHEMA_UNSUPPORTED', `unsupported schemaVersion in ${paths.stateFile}`);
114
+ throw pactiveError('STATE_SCHEMA_UNSUPPORTED', `unsupported schemaVersion in ${paths.stateFile}`);
115
115
  }
116
116
  return normalize(parsed);
117
117
  } catch (err) {
@@ -143,10 +143,10 @@ function quarantine(paths, cause) {
143
143
  export function saveState(target, state) {
144
144
  const paths = toPaths(target);
145
145
  if (!state || typeof state !== 'object') {
146
- throw actiError('STATE_REQUIRED', 'a state object is required');
146
+ throw pactiveError('STATE_REQUIRED', 'a state object is required');
147
147
  }
148
148
  if (state.schemaVersion !== SCHEMA_VERSION) {
149
- throw actiError('STATE_SCHEMA_UNSUPPORTED', `expected schemaVersion ${SCHEMA_VERSION}`);
149
+ throw pactiveError('STATE_SCHEMA_UNSUPPORTED', `expected schemaVersion ${SCHEMA_VERSION}`);
150
150
  }
151
151
  ensureStateDirs(paths);
152
152
  const dir = path.dirname(paths.stateFile);
@@ -168,7 +168,7 @@ export function saveState(target, state) {
168
168
 
169
169
  /** Read-modify-write under a lock; the callback may be async and may mutate in place. */
170
170
  export async function mutateState(target, fn) {
171
- if (typeof fn !== 'function') throw actiError('MUTATOR_REQUIRED', 'mutateState needs a function');
171
+ if (typeof fn !== 'function') throw pactiveError('MUTATOR_REQUIRED', 'mutateState needs a function');
172
172
  const paths = toPaths(target);
173
173
  ensureStateDirs(paths);
174
174
  return withLock(paths.locksDir, 'state', async () => {