@trevonistrevon/pi-loop 0.5.8 → 0.6.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.
Files changed (127) hide show
  1. package/.release-please-manifest.json +3 -0
  2. package/AGENTS.md +52 -12
  3. package/CHANGELOG.md +59 -0
  4. package/CONTRIBUTING.md +13 -3
  5. package/Dockerfile +17 -0
  6. package/README.md +47 -0
  7. package/coverage/coverage-final.json +35 -31
  8. package/coverage/index.html +64 -49
  9. package/coverage/src/api.ts.html +193 -0
  10. package/coverage/src/commands/index.html +32 -32
  11. package/coverage/src/commands/loop-command.ts.html +170 -170
  12. package/coverage/src/commands/tasks-command.ts.html +135 -135
  13. package/coverage/src/coordinator.ts.html +26 -26
  14. package/coverage/src/index.html +61 -106
  15. package/coverage/src/index.ts.html +280 -94
  16. package/coverage/src/loop-parse.ts.html +90 -57
  17. package/coverage/src/loop-reducer.ts.html +25 -25
  18. package/coverage/src/monitor-completion-coordinator.ts.html +1 -1
  19. package/coverage/src/monitor-manager.ts.html +95 -71
  20. package/coverage/src/monitor-reducer.ts.html +15 -15
  21. package/coverage/src/notification-reducer.ts.html +7 -7
  22. package/coverage/src/reducer-backed-store.ts.html +57 -57
  23. package/coverage/src/rpc/channels.ts.html +376 -0
  24. package/coverage/src/rpc/cross-extension-rpc.ts.html +541 -0
  25. package/coverage/src/rpc/index.html +131 -0
  26. package/coverage/src/runtime/index.html +89 -29
  27. package/coverage/src/{goal-coordinator.ts.html → runtime/loop-events.ts.html} +79 -73
  28. package/coverage/src/runtime/monitor-ondone-runtime.ts.html +5 -5
  29. package/coverage/src/runtime/native-task-rpc.ts.html +502 -0
  30. package/coverage/src/runtime/notification-runtime.ts.html +14 -14
  31. package/coverage/src/runtime/scope.ts.html +17 -17
  32. package/coverage/src/runtime/session-runtime.ts.html +40 -40
  33. package/coverage/src/runtime/task-backlog-runtime.ts.html +66 -24
  34. package/coverage/src/runtime/task-events.ts.html +208 -0
  35. package/coverage/src/runtime/task-mutations.ts.html +394 -0
  36. package/coverage/src/runtime/task-rpc.ts.html +106 -118
  37. package/coverage/src/scheduler.ts.html +12 -12
  38. package/coverage/src/store.ts.html +20 -20
  39. package/coverage/src/task-backlog-coordinator.ts.html +8 -8
  40. package/coverage/src/task-reducer.ts.html +42 -42
  41. package/coverage/src/task-store.ts.html +49 -49
  42. package/coverage/src/tools/index.html +39 -24
  43. package/coverage/src/tools/loop-tools.ts.html +12 -21
  44. package/coverage/src/tools/monitor-tools.ts.html +12 -21
  45. package/coverage/src/tools/native-task-tools.ts.html +52 -94
  46. package/coverage/src/tools/tool-result.ts.html +97 -0
  47. package/coverage/src/trigger-system.ts.html +12 -12
  48. package/coverage/src/ui/index.html +1 -1
  49. package/coverage/src/ui/widget.ts.html +23 -23
  50. package/dist/api.d.ts +11 -0
  51. package/dist/api.js +10 -0
  52. package/dist/commands/loop-command.js +2 -2
  53. package/dist/commands/tasks-command.js +3 -3
  54. package/dist/coordinator.d.ts +1 -1
  55. package/dist/index.js +80 -20
  56. package/dist/loop-parse.js +14 -5
  57. package/dist/monitor-manager.d.ts +4 -1
  58. package/dist/monitor-manager.js +5 -3
  59. package/dist/reducer-backed-store.d.ts +1 -1
  60. package/dist/reducer-backed-store.js +1 -1
  61. package/dist/rpc/channels.d.ts +73 -0
  62. package/dist/rpc/channels.js +30 -0
  63. package/dist/rpc/cross-extension-rpc.d.ts +59 -0
  64. package/dist/rpc/cross-extension-rpc.js +117 -0
  65. package/dist/runtime/loop-events.d.ts +26 -0
  66. package/dist/runtime/loop-events.js +26 -0
  67. package/dist/runtime/native-task-rpc.d.ts +22 -0
  68. package/dist/runtime/native-task-rpc.js +50 -0
  69. package/dist/runtime/task-backlog-runtime.d.ts +3 -0
  70. package/dist/runtime/task-backlog-runtime.js +4 -1
  71. package/dist/runtime/task-mutations.d.ts +39 -0
  72. package/dist/runtime/task-mutations.js +68 -0
  73. package/dist/runtime/task-rpc.d.ts +4 -0
  74. package/dist/runtime/task-rpc.js +40 -58
  75. package/dist/scheduler.js +1 -1
  76. package/dist/tools/loop-tools.js +1 -3
  77. package/dist/tools/monitor-tools.js +1 -3
  78. package/dist/tools/native-task-tools.d.ts +2 -6
  79. package/dist/tools/native-task-tools.js +25 -55
  80. package/dist/tools/tool-result.d.ts +8 -0
  81. package/dist/tools/tool-result.js +4 -0
  82. package/docker-compose.yml +12 -0
  83. package/docs/architecture/state-machine-migration.md +0 -44
  84. package/docs/architecture/state-machine-reducer-event-model.md +2 -3
  85. package/package.json +6 -1
  86. package/release-please-config.json +9 -0
  87. package/src/api.ts +36 -0
  88. package/src/commands/loop-command.ts +2 -2
  89. package/src/commands/tasks-command.ts +3 -3
  90. package/src/coordinator.ts +1 -1
  91. package/src/index.ts +84 -22
  92. package/src/loop-parse.ts +18 -7
  93. package/src/monitor-manager.ts +11 -3
  94. package/src/reducer-backed-store.ts +3 -3
  95. package/src/rpc/channels.ts +97 -0
  96. package/src/rpc/cross-extension-rpc.ts +152 -0
  97. package/src/runtime/loop-events.ts +60 -0
  98. package/src/runtime/native-task-rpc.ts +139 -0
  99. package/src/runtime/task-backlog-runtime.ts +14 -0
  100. package/src/runtime/task-mutations.ts +103 -0
  101. package/src/runtime/task-rpc.ts +48 -54
  102. package/src/scheduler.ts +1 -1
  103. package/src/tools/loop-tools.ts +1 -4
  104. package/src/tools/monitor-tools.ts +1 -4
  105. package/src/tools/native-task-tools.ts +31 -52
  106. package/src/tools/tool-result.ts +4 -0
  107. package/src/trigger-system.ts +4 -4
  108. package/vitest.config.ts +11 -7
  109. package/coverage/src/goal-reducer.ts.html +0 -982
  110. package/coverage/src/goal-store.ts.html +0 -742
  111. package/coverage/src/goal-verifier.ts.html +0 -808
  112. package/dist/goal-coordinator.d.ts +0 -22
  113. package/dist/goal-coordinator.js +0 -28
  114. package/dist/goal-reducer.d.ts +0 -98
  115. package/dist/goal-reducer.js +0 -190
  116. package/dist/goal-store.d.ts +0 -22
  117. package/dist/goal-store.js +0 -188
  118. package/dist/goal-types.d.ts +0 -82
  119. package/dist/goal-types.js +0 -1
  120. package/dist/goal-verifier.d.ts +0 -20
  121. package/dist/goal-verifier.js +0 -198
  122. package/docs/architecture/goal-state-schema.md +0 -505
  123. package/src/goal-coordinator.ts +0 -58
  124. package/src/goal-reducer.ts +0 -299
  125. package/src/goal-store.ts +0 -219
  126. package/src/goal-types.ts +0 -104
  127. package/src/goal-verifier.ts +0 -241
@@ -1,7 +1,17 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
+ import {
4
+ type CleanReply,
5
+ type CreateTaskReply,
6
+ type PendingReply,
7
+ type PingReply,
8
+ replyChannel,
9
+ TASKS_RPC,
10
+ } from "../rpc/channels.js";
11
+ import { type RpcReply, rpcCall } from "../rpc/cross-extension-rpc.js";
3
12
  import type { TaskStore } from "../task-store.js";
4
13
  import type { LoopEntry } from "../types.js";
14
+ import { NATIVE_TASKS_PROVIDER } from "./native-task-rpc.js";
5
15
  import { emitNativeTaskEvent } from "./task-events.js";
6
16
 
7
17
  export interface TaskRuntimeBridgeOptions {
@@ -11,6 +21,10 @@ export interface TaskRuntimeBridgeOptions {
11
21
  getNativeTaskStore: () => TaskStore | undefined;
12
22
  onNativeTaskCreated?: (taskStore: TaskStore) => void;
13
23
  onNativeTasksPruned?: (taskStore: TaskStore) => Promise<void> | void;
24
+ /** Called when a detection window opens. */
25
+ onDetectionStarted?: () => void;
26
+ /** Called when a detection window closes (provider found or probe timed out). */
27
+ onDetectionSettled?: () => void;
14
28
  debug?: (...args: unknown[]) => void;
15
29
  }
16
30
 
@@ -29,48 +43,51 @@ export function createTaskRuntimeBridge(options: TaskRuntimeBridgeOptions): Task
29
43
  getNativeTaskStore,
30
44
  onNativeTaskCreated,
31
45
  onNativeTasksPruned,
46
+ onDetectionStarted,
47
+ onDetectionSettled,
32
48
  debug,
33
49
  } = options;
50
+ let detectionEpoch = 0;
34
51
 
35
52
  function checkTasksVersion() {
53
+ // Not rpcProbe: pi-loop's own native server also answers this ping, and a
54
+ // first-reply-wins probe would always settle on that self-reply. Keep the
55
+ // listener open for the whole window and skip self-replies so a slower
56
+ // external provider (pi-tasks) is still detected.
57
+ const epoch = ++detectionEpoch;
58
+ onDetectionStarted?.();
59
+ const settleCurrentDetection = () => {
60
+ if (epoch !== detectionEpoch) return;
61
+ onDetectionSettled?.();
62
+ };
36
63
  const requestId = randomUUID();
37
64
  const timer = setTimeout(() => {
38
65
  unsub();
66
+ settleCurrentDetection();
39
67
  }, 5000);
40
- const unsub = pi.events.on(`tasks:rpc:ping:reply:${requestId}`, (raw: unknown) => {
68
+ const unsub = pi.events.on(replyChannel(TASKS_RPC.ping, requestId), (raw: unknown) => {
69
+ const reply = raw as RpcReply<PingReply> | undefined;
70
+ if (!reply?.success || !reply.data) return;
71
+ if (reply.data.provider === NATIVE_TASKS_PROVIDER) return;
72
+ if (reply.data.version === undefined) return;
41
73
  unsub();
42
74
  clearTimeout(timer);
43
- const remoteVersion = (raw as { data?: { version?: number } })?.data?.version;
44
- if (remoteVersion !== undefined) setTasksAvailable(true);
75
+ setTasksAvailable(true);
76
+ settleCurrentDetection();
45
77
  });
46
- pi.events.emit("tasks:rpc:ping", { requestId });
78
+ pi.events.emit(TASKS_RPC.ping, { requestId });
47
79
  }
48
80
 
49
81
  async function autoCreateTask(entry: LoopEntry): Promise<string | undefined> {
50
82
  if (!entry.autoTask) return undefined;
51
83
  if (isTasksAvailable()) {
52
84
  try {
53
- const requestId = randomUUID();
54
- const taskId = await new Promise<string | undefined>((resolve) => {
55
- const timer = setTimeout(() => {
56
- unsub();
57
- resolve(undefined);
58
- }, 5000);
59
- const unsub = pi.events.on(`tasks:rpc:create:reply:${requestId}`, (raw: unknown) => {
60
- unsub();
61
- clearTimeout(timer);
62
- const reply = raw as { success: boolean; data?: { id: string } };
63
- if (reply.success && reply.data) resolve(reply.data.id);
64
- else resolve(undefined);
65
- });
66
- pi.events.emit("tasks:rpc:create", {
67
- requestId,
68
- subject: entry.prompt.slice(0, 80),
69
- description: `Auto-created from loop #${entry.id}`,
70
- metadata: { loopId: entry.id, trigger: entry.trigger },
71
- });
72
- });
73
- return taskId;
85
+ const reply = await rpcCall<CreateTaskReply>(pi.events, TASKS_RPC.create, {
86
+ subject: entry.prompt.slice(0, 80),
87
+ description: `Auto-created from loop #${entry.id}`,
88
+ metadata: { loopId: entry.id, trigger: entry.trigger },
89
+ }, 5000);
90
+ return reply.id;
74
91
  } catch {
75
92
  return undefined;
76
93
  }
@@ -90,22 +107,11 @@ export function createTaskRuntimeBridge(options: TaskRuntimeBridgeOptions): Task
90
107
 
91
108
  async function hasPendingTasks(): Promise<number> {
92
109
  if (isTasksAvailable()) {
110
+ // -1 is this bridge's "unknown" sentinel, consumed by notification-runtime;
111
+ // the RPC layer itself rejects on failure.
93
112
  try {
94
- const requestId = randomUUID();
95
- const count = await new Promise<number>((resolve) => {
96
- const timer = setTimeout(() => {
97
- unsub();
98
- resolve(-1);
99
- }, 3000);
100
- const unsub = pi.events.on(`tasks:rpc:pending:reply:${requestId}`, (raw: unknown) => {
101
- unsub();
102
- clearTimeout(timer);
103
- const reply = raw as { success: boolean; data?: { pending: number } };
104
- resolve(reply.success && reply.data ? reply.data.pending : -1);
105
- });
106
- pi.events.emit("tasks:rpc:pending", { requestId });
107
- });
108
- return count;
113
+ const reply = await rpcCall<PendingReply>(pi.events, TASKS_RPC.pending, {}, 3000);
114
+ return reply.pending;
109
115
  } catch {
110
116
  return -1;
111
117
  }
@@ -117,20 +123,8 @@ export function createTaskRuntimeBridge(options: TaskRuntimeBridgeOptions): Task
117
123
  async function cleanDoneTasks(): Promise<void> {
118
124
  if (isTasksAvailable()) {
119
125
  try {
120
- const requestId = randomUUID();
121
- await new Promise<void>((resolve) => {
122
- const timer = setTimeout(() => {
123
- unsub();
124
- resolve();
125
- }, 3000);
126
- const unsub = pi.events.on(`tasks:rpc:clean:reply:${requestId}`, () => {
127
- unsub();
128
- clearTimeout(timer);
129
- debug?.("tasks:rpc:clean — done tasks swept");
130
- resolve();
131
- });
132
- pi.events.emit("tasks:rpc:clean", { requestId });
133
- });
126
+ await rpcCall<CleanReply>(pi.events, TASKS_RPC.clean, {}, 3000);
127
+ debug?.(`${TASKS_RPC.clean} done tasks swept`);
134
128
  } catch {
135
129
  // timeout or error, ignore
136
130
  }
package/src/scheduler.ts CHANGED
@@ -50,7 +50,7 @@ export class CronScheduler {
50
50
  const _scheduleExpr = entry.trigger.type === "hybrid" ? entry.trigger.cron : (entry.trigger as { schedule: string }).schedule;
51
51
 
52
52
  const nextFire = computeNextFire(entry);
53
- const minuteField = _scheduleExpr.trim().split(/\s+/)[0];
53
+ const minuteField = _scheduleExpr.trim().split(/\s+/)[0] ?? "";
54
54
  const minuteStep = minuteField.startsWith("*/") ? parseInt(minuteField.slice(2), 10) || 30 : 30;
55
55
  const jitter = computeJitter(entry.id, entry.recurring, minuteStep);
56
56
  const fireTime = nextFire.getTime() + jitter;
@@ -2,6 +2,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "typebox";
3
3
  import { parseInterval } from "../loop-parse.js";
4
4
  import type { LoopEntry, Trigger } from "../types.js";
5
+ import { textResult } from "./tool-result.js";
5
6
 
6
7
  interface LoopStoreLike {
7
8
  list(): LoopEntry[];
@@ -46,10 +47,6 @@ export interface LoopToolsOptions {
46
47
  isTaskSystemReady: () => boolean;
47
48
  }
48
49
 
49
- function textResult(msg: string) {
50
- return { content: [{ type: "text" as const, text: msg }], details: undefined as any };
51
- }
52
-
53
50
  function validateTrigger(trigger: Trigger): string | null {
54
51
  if (trigger.type === "cron") {
55
52
  const parts = trigger.schedule.trim().split(/\s+/);
@@ -1,6 +1,7 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "typebox";
3
3
  import type { LoopEntry, MonitorEntry, Trigger } from "../types.js";
4
+ import { textResult } from "./tool-result.js";
4
5
 
5
6
  interface MonitorManagerLike {
6
7
  list(): MonitorEntry[];
@@ -26,10 +27,6 @@ export interface MonitorToolsOptions {
26
27
  handleMonitorDoneLoop: (doneLoop: LoopEntry, monitorId: string) => void;
27
28
  }
28
29
 
29
- function textResult(msg: string) {
30
- return { content: [{ type: "text" as const, text: msg }], details: undefined as any };
31
- }
32
-
33
30
  function formatRemaining(ms: number): string {
34
31
  if (ms < 60000) return `${Math.round(ms / 1000)}s`;
35
32
  if (ms < 3600000) return `${Math.round(ms / 60000)}m`;
@@ -1,12 +1,17 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "typebox";
3
- import { emitNativeTaskEvent } from "../runtime/task-events.js";
3
+ import {
4
+ createTask,
5
+ deleteTask,
6
+ type TaskBacklogResult,
7
+ type TaskMutationContext,
8
+ updateTask,
9
+ } from "../runtime/task-mutations.js";
4
10
  import { TaskStore } from "../task-store.js";
11
+ import type { TaskStatus } from "../task-types.js";
12
+ import { textResult } from "./tool-result.js";
5
13
 
6
- export interface TaskBacklogResult {
7
- created: boolean;
8
- entry?: { id: string };
9
- }
14
+ export type { TaskBacklogResult };
10
15
 
11
16
  export interface NativeTaskToolsOptions {
12
17
  pi: ExtensionAPI;
@@ -15,12 +20,15 @@ export interface NativeTaskToolsOptions {
15
20
  updateWidget: () => void;
16
21
  }
17
22
 
18
- function textResult(msg: string) {
19
- return { content: [{ type: "text" as const, text: msg }], details: undefined as any };
23
+ function backlogSuffix(backlog: TaskBacklogResult): string {
24
+ return backlog.created && backlog.entry
25
+ ? `\nBacklog worker loop #${backlog.entry.id} created`
26
+ : "";
20
27
  }
21
28
 
22
29
  export function registerNativeTaskTools(options: NativeTaskToolsOptions): void {
23
30
  const { pi, taskStore, evaluateTaskBacklog, updateWidget } = options;
31
+ const mutationCtx: TaskMutationContext = { pi, taskStore, evaluateTaskBacklog, updateWidget };
24
32
 
25
33
  pi.registerTool({
26
34
  name: "TaskCreate",
@@ -47,15 +55,11 @@ Fields:
47
55
  description: Type.String({ description: "Detailed description of what needs to be done" }),
48
56
  }),
49
57
  async execute(_toolCallId, params) {
50
- const entry = taskStore.create(params.subject, params.description);
51
- emitNativeTaskEvent(pi, "tasks:created", entry);
52
- const backlog = await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
53
- updateWidget();
54
-
55
- const autoLoopMsg = backlog.created && backlog.entry
56
- ? `\nBacklog worker loop #${backlog.entry.id} created`
57
- : "";
58
- return Promise.resolve(textResult(`Task #${entry.id} created: ${entry.subject}${autoLoopMsg}`));
58
+ const { entry, backlog } = await createTask(mutationCtx, {
59
+ subject: params.subject,
60
+ description: params.description,
61
+ });
62
+ return textResult(`Task #${entry.id} created: ${entry.subject}${backlogSuffix(backlog)}`);
59
63
  },
60
64
  });
61
65
 
@@ -104,34 +108,15 @@ Parameters: id (required), status, subject, description`,
104
108
  }),
105
109
  async execute(_toolCallId, params) {
106
110
  const { id, status, subject, description } = params;
107
- let entry = taskStore.get(id);
108
- if (!entry) return Promise.resolve(textResult(`Task #${id} not found`));
109
-
110
- const previousStatus = entry.status;
111
- if (status === "in_progress") {
112
- entry = taskStore.start(id);
113
- if (entry) emitNativeTaskEvent(pi, "tasks:started", entry, previousStatus);
114
- } else if (status === "completed") {
115
- entry = taskStore.complete(id);
116
- if (entry) emitNativeTaskEvent(pi, "tasks:completed", entry, previousStatus);
117
- } else if (status === "pending") {
118
- entry = taskStore.reopen(id);
119
- if (entry) emitNativeTaskEvent(pi, "tasks:reopened", entry, previousStatus);
120
- }
121
-
122
- if (!entry) return Promise.resolve(textResult(`Task #${id} not found`));
123
- if (subject !== undefined || description !== undefined) {
124
- entry = taskStore.updateDetails(id, { subject, description });
125
- if (entry) emitNativeTaskEvent(pi, "tasks:updated", entry, previousStatus);
126
- }
127
- if (!entry) return Promise.resolve(textResult(`Task #${id} not found`));
128
- updateWidget();
129
- const backlog = await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
111
+ const result = await updateTask(mutationCtx, {
112
+ id,
113
+ status: status as TaskStatus | undefined,
114
+ subject,
115
+ description,
116
+ });
117
+ if (!result) return textResult(`Task #${id} not found`);
130
118
  const statusMsg = status ? ` → ${status}` : "";
131
- const autoLoopMsg = backlog.created && backlog.entry
132
- ? `\nBacklog worker loop #${backlog.entry.id} created`
133
- : "";
134
- return Promise.resolve(textResult(`Task #${id} updated${statusMsg}${autoLoopMsg}`));
119
+ return textResult(`Task #${id} updated${statusMsg}${backlogSuffix(result.backlog)}`);
135
120
  },
136
121
  });
137
122
 
@@ -143,15 +128,9 @@ Parameters: id (required), status, subject, description`,
143
128
  id: Type.String({ description: "Task ID to delete" }),
144
129
  }),
145
130
  async execute(_toolCallId, params) {
146
- const existing = taskStore.get(params.id);
147
- const deleted = taskStore.delete(params.id);
148
- updateWidget();
149
- if (deleted) {
150
- if (existing) emitNativeTaskEvent(pi, "tasks:deleted", existing, existing.status);
151
- await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
152
- return Promise.resolve(textResult(`Task #${params.id} deleted`));
153
- }
154
- return Promise.resolve(textResult(`Task #${params.id} not found`));
131
+ const result = await deleteTask(mutationCtx, params.id);
132
+ if (!result) return textResult(`Task #${params.id} not found`);
133
+ return textResult(`Task #${params.id} deleted`);
155
134
  },
156
135
  });
157
136
  }
@@ -0,0 +1,4 @@
1
+ /** Plain-text tool result in the shape pi's registerTool expects. */
2
+ export function textResult(msg: string) {
3
+ return { content: [{ type: "text" as const, text: msg }], details: undefined };
4
+ }
@@ -58,7 +58,7 @@ export class TriggerSystem {
58
58
  }
59
59
  const subs = this.eventSubscriptions.get(source)!;
60
60
 
61
- const unsub = this.pi.events.on(source as any, (data: any) => {
61
+ const unsub = this.pi.events.on(source, (data: unknown) => {
62
62
  if (entry.trigger.type === "hybrid") {
63
63
  this.handleHybridFire(entry, data);
64
64
  } else {
@@ -71,7 +71,7 @@ export class TriggerSystem {
71
71
  subs.set(entry.id, unsub);
72
72
  }
73
73
 
74
- private handleHybridFire(entry: LoopEntry, _data: any): void {
74
+ private handleHybridFire(entry: LoopEntry, _data: unknown): void {
75
75
  const now = Date.now();
76
76
  const last = this.lastFireTime.get(entry.id) ?? 0;
77
77
  const debounceMs = entry.trigger.type === "hybrid" ? entry.trigger.debounceMs : 0;
@@ -123,7 +123,7 @@ export class TriggerSystem {
123
123
  }
124
124
  }
125
125
 
126
- private matchesFilter(data: any, filter?: string): boolean {
126
+ private matchesFilter(data: unknown, filter?: string): boolean {
127
127
  if (!filter) return true;
128
128
 
129
129
  if (filter.startsWith("regex:")) {
@@ -138,7 +138,7 @@ export class TriggerSystem {
138
138
  try {
139
139
  const parsed = JSON.parse(filter);
140
140
  for (const [key, value] of Object.entries(parsed)) {
141
- const dataValue = data?.[key];
141
+ const dataValue = (data as Record<string, unknown> | undefined)?.[key];
142
142
  if (dataValue === undefined) return false;
143
143
  if (typeof value === "object" && typeof dataValue === "object") {
144
144
  if (JSON.stringify(value) !== JSON.stringify(dataValue)) return false;
package/vitest.config.ts CHANGED
@@ -5,20 +5,24 @@ export default defineConfig({
5
5
  environment: "node",
6
6
  include: ["test/**/*.test.ts"],
7
7
  exclude: ["node_modules", "dist"],
8
+ // CI runners are slower than local; 15s gives real-child-process
9
+ // tests (monitor stop, lifecycle) enough headroom.
10
+ testTimeout: 15000,
11
+ hookTimeout: 15000,
8
12
  coverage: {
9
13
  provider: "v8",
10
14
  reporter: ["text-summary", "html", "json"],
11
15
  include: ["src/**/*.ts"],
12
16
  // Type-only modules and test helpers carry no executable logic worth gating.
13
17
  exclude: ["src/**/*-types.ts", "src/types.ts"],
14
- // Floors set just below current actuals (stmts 84%, branches 75%,
15
- // funcs 95%, lines 86%) to catch regressions. Raised in Phase 4 after the
16
- // runtime/ + tools/ suites landed.
18
+ // Floors set just below current actuals (stmts 91%, branches 83%,
19
+ // funcs 98%, lines 94%) to catch regressions. Re-anchored after the
20
+ // goal-subsystem removal and the rpc/native-task-rpc/command suites landed.
17
21
  thresholds: {
18
- statements: 82,
19
- branches: 73,
20
- functions: 94,
21
- lines: 84,
22
+ statements: 89,
23
+ branches: 80,
24
+ functions: 96,
25
+ lines: 91,
22
26
  },
23
27
  },
24
28
  },