@trevonistrevon/pi-loop 0.5.9 → 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 (121) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/AGENTS.md +52 -12
  3. package/CHANGELOG.md +59 -0
  4. package/CONTRIBUTING.md +13 -3
  5. package/README.md +47 -0
  6. package/coverage/coverage-final.json +35 -32
  7. package/coverage/index.html +64 -49
  8. package/coverage/src/api.ts.html +193 -0
  9. package/coverage/src/commands/index.html +32 -32
  10. package/coverage/src/commands/loop-command.ts.html +170 -170
  11. package/coverage/src/commands/tasks-command.ts.html +129 -129
  12. package/coverage/src/coordinator.ts.html +26 -26
  13. package/coverage/src/index.html +61 -106
  14. package/coverage/src/index.ts.html +224 -86
  15. package/coverage/src/loop-parse.ts.html +106 -73
  16. package/coverage/src/loop-reducer.ts.html +21 -21
  17. package/coverage/src/monitor-completion-coordinator.ts.html +1 -1
  18. package/coverage/src/monitor-manager.ts.html +95 -71
  19. package/coverage/src/monitor-reducer.ts.html +15 -15
  20. package/coverage/src/notification-reducer.ts.html +6 -6
  21. package/coverage/src/reducer-backed-store.ts.html +57 -57
  22. package/coverage/src/rpc/channels.ts.html +376 -0
  23. package/coverage/src/rpc/cross-extension-rpc.ts.html +541 -0
  24. package/coverage/src/rpc/index.html +131 -0
  25. package/coverage/src/runtime/index.html +68 -23
  26. package/coverage/src/{goal-coordinator.ts.html → runtime/loop-events.ts.html} +79 -73
  27. package/coverage/src/runtime/monitor-ondone-runtime.ts.html +5 -5
  28. package/coverage/src/runtime/native-task-rpc.ts.html +502 -0
  29. package/coverage/src/runtime/notification-runtime.ts.html +22 -22
  30. package/coverage/src/runtime/scope.ts.html +14 -14
  31. package/coverage/src/runtime/session-runtime.ts.html +32 -32
  32. package/coverage/src/runtime/task-backlog-runtime.ts.html +65 -23
  33. package/coverage/src/runtime/task-events.ts.html +2 -2
  34. package/coverage/src/runtime/task-mutations.ts.html +394 -0
  35. package/coverage/src/runtime/task-rpc.ts.html +102 -120
  36. package/coverage/src/scheduler.ts.html +29 -29
  37. package/coverage/src/store.ts.html +27 -27
  38. package/coverage/src/task-backlog-coordinator.ts.html +8 -8
  39. package/coverage/src/task-reducer.ts.html +42 -42
  40. package/coverage/src/task-store.ts.html +49 -49
  41. package/coverage/src/tools/index.html +39 -24
  42. package/coverage/src/tools/loop-tools.ts.html +12 -21
  43. package/coverage/src/tools/monitor-tools.ts.html +12 -21
  44. package/coverage/src/tools/native-task-tools.ts.html +57 -120
  45. package/coverage/src/tools/tool-result.ts.html +97 -0
  46. package/coverage/src/trigger-system.ts.html +12 -12
  47. package/coverage/src/ui/index.html +1 -1
  48. package/coverage/src/ui/widget.ts.html +23 -23
  49. package/dist/api.d.ts +11 -0
  50. package/dist/api.js +10 -0
  51. package/dist/commands/loop-command.js +2 -2
  52. package/dist/commands/tasks-command.js +3 -3
  53. package/dist/coordinator.d.ts +1 -1
  54. package/dist/index.js +48 -3
  55. package/dist/loop-parse.js +14 -5
  56. package/dist/reducer-backed-store.d.ts +1 -1
  57. package/dist/reducer-backed-store.js +1 -1
  58. package/dist/rpc/channels.d.ts +73 -0
  59. package/dist/rpc/channels.js +30 -0
  60. package/dist/rpc/cross-extension-rpc.d.ts +59 -0
  61. package/dist/rpc/cross-extension-rpc.js +117 -0
  62. package/dist/runtime/loop-events.d.ts +26 -0
  63. package/dist/runtime/loop-events.js +26 -0
  64. package/dist/runtime/native-task-rpc.d.ts +22 -0
  65. package/dist/runtime/native-task-rpc.js +50 -0
  66. package/dist/runtime/task-backlog-runtime.d.ts +3 -0
  67. package/dist/runtime/task-backlog-runtime.js +4 -1
  68. package/dist/runtime/task-mutations.d.ts +39 -0
  69. package/dist/runtime/task-mutations.js +68 -0
  70. package/dist/runtime/task-rpc.d.ts +4 -0
  71. package/dist/runtime/task-rpc.js +40 -58
  72. package/dist/scheduler.js +1 -1
  73. package/dist/tools/loop-tools.js +1 -3
  74. package/dist/tools/monitor-tools.js +1 -3
  75. package/dist/tools/native-task-tools.d.ts +2 -6
  76. package/dist/tools/native-task-tools.js +25 -55
  77. package/dist/tools/tool-result.d.ts +8 -0
  78. package/dist/tools/tool-result.js +4 -0
  79. package/docs/architecture/state-machine-migration.md +0 -44
  80. package/docs/architecture/state-machine-reducer-event-model.md +2 -3
  81. package/package.json +6 -1
  82. package/src/api.ts +36 -0
  83. package/src/commands/loop-command.ts +2 -2
  84. package/src/commands/tasks-command.ts +3 -3
  85. package/src/coordinator.ts +1 -1
  86. package/src/index.ts +49 -3
  87. package/src/loop-parse.ts +18 -7
  88. package/src/reducer-backed-store.ts +3 -3
  89. package/src/rpc/channels.ts +97 -0
  90. package/src/rpc/cross-extension-rpc.ts +152 -0
  91. package/src/runtime/loop-events.ts +60 -0
  92. package/src/runtime/native-task-rpc.ts +139 -0
  93. package/src/runtime/task-backlog-runtime.ts +14 -0
  94. package/src/runtime/task-mutations.ts +103 -0
  95. package/src/runtime/task-rpc.ts +48 -54
  96. package/src/scheduler.ts +1 -1
  97. package/src/tools/loop-tools.ts +1 -4
  98. package/src/tools/monitor-tools.ts +1 -4
  99. package/src/tools/native-task-tools.ts +31 -52
  100. package/src/tools/tool-result.ts +4 -0
  101. package/src/trigger-system.ts +4 -4
  102. package/vitest.config.ts +7 -7
  103. package/coverage/src/goal-reducer.ts.html +0 -982
  104. package/coverage/src/goal-store.ts.html +0 -742
  105. package/coverage/src/goal-verifier.ts.html +0 -808
  106. package/dist/goal-coordinator.d.ts +0 -22
  107. package/dist/goal-coordinator.js +0 -28
  108. package/dist/goal-reducer.d.ts +0 -98
  109. package/dist/goal-reducer.js +0 -190
  110. package/dist/goal-store.d.ts +0 -22
  111. package/dist/goal-store.js +0 -188
  112. package/dist/goal-types.d.ts +0 -82
  113. package/dist/goal-types.js +0 -1
  114. package/dist/goal-verifier.d.ts +0 -20
  115. package/dist/goal-verifier.js +0 -198
  116. package/docs/architecture/goal-state-schema.md +0 -505
  117. package/src/goal-coordinator.ts +0 -58
  118. package/src/goal-reducer.ts +0 -299
  119. package/src/goal-store.ts +0 -219
  120. package/src/goal-types.ts +0 -104
  121. package/src/goal-verifier.ts +0 -241
@@ -224,15 +224,15 @@
224
224
  <span class="cline-any cline-neutral">&nbsp;</span>
225
225
  <span class="cline-any cline-neutral">&nbsp;</span>
226
226
  <span class="cline-any cline-neutral">&nbsp;</span>
227
- <span class="cline-any cline-yes">123x</span>
228
- <span class="cline-any cline-yes">123x</span>
229
- <span class="cline-any cline-yes">123x</span>
227
+ <span class="cline-any cline-yes">126x</span>
228
+ <span class="cline-any cline-yes">126x</span>
229
+ <span class="cline-any cline-yes">126x</span>
230
230
  <span class="cline-any cline-neutral">&nbsp;</span>
231
231
  <span class="cline-any cline-neutral">&nbsp;</span>
232
- <span class="cline-any cline-yes">123x</span>
233
- <span class="cline-any cline-yes">123x</span>
234
- <span class="cline-any cline-yes">123x</span>
235
- <span class="cline-any cline-yes">123x</span>
232
+ <span class="cline-any cline-yes">126x</span>
233
+ <span class="cline-any cline-yes">126x</span>
234
+ <span class="cline-any cline-yes">126x</span>
235
+ <span class="cline-any cline-yes">126x</span>
236
236
  <span class="cline-any cline-neutral">&nbsp;</span>
237
237
  <span class="cline-any cline-neutral">&nbsp;</span>
238
238
  <span class="cline-any cline-neutral">&nbsp;</span>
@@ -431,7 +431,7 @@ export class TriggerSystem {
431
431
  }
432
432
  const subs = this.eventSubscriptions.get(source)!;
433
433
  &nbsp;
434
- const unsub = this.pi.events.on(source as any, (data: any) =&gt; {
434
+ const unsub = this.pi.events.on(source, (data: unknown) =&gt; {
435
435
  if (entry.trigger.type === "hybrid") {
436
436
  this.handleHybridFire(entry, data);
437
437
  } else {
@@ -444,7 +444,7 @@ export class TriggerSystem {
444
444
  subs.set(entry.id, unsub);
445
445
  }
446
446
  &nbsp;
447
- private handleHybridFire(entry: LoopEntry, _data: any): void {
447
+ private handleHybridFire(entry: LoopEntry, _data: unknown): void {
448
448
  const now = Date.now();
449
449
  const last = this.lastFireTime.get(entry.id) ?? 0;
450
450
  const debounceMs = entry.trigger.type === "hybrid" ? entry.trigger.debounceMs : <span class="branch-1 cbranch-no" title="branch not covered" >0;</span>
@@ -496,7 +496,7 @@ export class TriggerSystem {
496
496
  }
497
497
  }
498
498
  &nbsp;
499
- private matchesFilter(data: any, filter?: string): boolean {
499
+ private matchesFilter(data: unknown, filter?: string): boolean {
500
500
  if (!filter) return true;
501
501
  &nbsp;
502
502
  if (filter.startsWith("regex:")) {
@@ -511,7 +511,7 @@ export class TriggerSystem {
511
511
  try {
512
512
  const parsed = JSON.parse(filter);
513
513
  for (const [key, value] of Object.entries(parsed)) {
514
- const dataValue = <span class="cstat-no" title="statement not covered" >data?.[key];</span>
514
+ const dataValue = (<span class="cstat-no" title="statement not covered" >data as Record&lt;string, unknown&gt; | undefined)?.[key];</span>
515
515
  <span class="cstat-no" title="statement not covered" > if (dataValue === undefined) <span class="cstat-no" title="statement not covered" >return false;</span></span>
516
516
  <span class="cstat-no" title="statement not covered" > if (typeof value === "object" &amp;&amp; typeof dataValue === "object") {</span>
517
517
  <span class="cstat-no" title="statement not covered" > if (JSON.stringify(value) !== JSON.stringify(dataValue)) <span class="cstat-no" title="statement not covered" >return false;</span></span>
@@ -532,7 +532,7 @@ export class TriggerSystem {
532
532
  <div class='footer quiet pad2 space-top1 center small'>
533
533
  Code coverage generated by
534
534
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
535
- at 2026-06-23T21:49:18.784Z
535
+ at 2026-07-02T19:31:50.067Z
536
536
  </div>
537
537
  <script src="../prettify.js"></script>
538
538
  <script>
@@ -101,7 +101,7 @@
101
101
  <div class='footer quiet pad2 space-top1 center small'>
102
102
  Code coverage generated by
103
103
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
104
- at 2026-06-23T21:49:18.784Z
104
+ at 2026-07-02T19:31:50.067Z
105
105
  </div>
106
106
  <script src="../../prettify.js"></script>
107
107
  <script>
@@ -147,44 +147,44 @@
147
147
  <span class="cline-any cline-neutral">&nbsp;</span>
148
148
  <span class="cline-any cline-neutral">&nbsp;</span>
149
149
  <span class="cline-any cline-neutral">&nbsp;</span>
150
- <span class="cline-any cline-yes">70x</span>
151
- <span class="cline-any cline-yes">70x</span>
150
+ <span class="cline-any cline-yes">72x</span>
151
+ <span class="cline-any cline-yes">72x</span>
152
152
  <span class="cline-any cline-neutral">&nbsp;</span>
153
153
  <span class="cline-any cline-neutral">&nbsp;</span>
154
154
  <span class="cline-any cline-neutral">&nbsp;</span>
155
- <span class="cline-any cline-yes">107x</span>
155
+ <span class="cline-any cline-yes">108x</span>
156
156
  <span class="cline-any cline-neutral">&nbsp;</span>
157
157
  <span class="cline-any cline-neutral">&nbsp;</span>
158
158
  <span class="cline-any cline-neutral">&nbsp;</span>
159
- <span class="cline-any cline-yes">46x</span>
159
+ <span class="cline-any cline-yes">47x</span>
160
160
  <span class="cline-any cline-neutral">&nbsp;</span>
161
161
  <span class="cline-any cline-neutral">&nbsp;</span>
162
162
  <span class="cline-any cline-neutral">&nbsp;</span>
163
- <span class="cline-any cline-yes">65x</span>
163
+ <span class="cline-any cline-yes">67x</span>
164
164
  <span class="cline-any cline-neutral">&nbsp;</span>
165
165
  <span class="cline-any cline-neutral">&nbsp;</span>
166
166
  <span class="cline-any cline-neutral">&nbsp;</span>
167
- <span class="cline-any cline-yes">189x</span>
168
- <span class="cline-any cline-yes">164x</span>
167
+ <span class="cline-any cline-yes">191x</span>
168
+ <span class="cline-any cline-yes">168x</span>
169
169
  <span class="cline-any cline-neutral">&nbsp;</span>
170
170
  <span class="cline-any cline-neutral">&nbsp;</span>
171
171
  <span class="cline-any cline-neutral">&nbsp;</span>
172
- <span class="cline-any cline-yes">164x</span>
173
- <span class="cline-any cline-yes">164x</span>
174
- <span class="cline-any cline-yes">164x</span>
172
+ <span class="cline-any cline-yes">168x</span>
173
+ <span class="cline-any cline-yes">168x</span>
174
+ <span class="cline-any cline-yes">168x</span>
175
175
  <span class="cline-any cline-neutral">&nbsp;</span>
176
- <span class="cline-any cline-yes">164x</span>
177
- <span class="cline-any cline-yes">68x</span>
176
+ <span class="cline-any cline-yes">168x</span>
177
+ <span class="cline-any cline-yes">70x</span>
178
178
  <span class="cline-any cline-neutral">&nbsp;</span>
179
179
  <span class="cline-any cline-neutral">&nbsp;</span>
180
- <span class="cline-any cline-yes">96x</span>
181
- <span class="cline-any cline-yes">96x</span>
182
- <span class="cline-any cline-yes">96x</span>
183
- <span class="cline-any cline-yes">96x</span>
180
+ <span class="cline-any cline-yes">98x</span>
181
+ <span class="cline-any cline-yes">98x</span>
182
+ <span class="cline-any cline-yes">98x</span>
183
+ <span class="cline-any cline-yes">98x</span>
184
184
  <span class="cline-any cline-neutral">&nbsp;</span>
185
- <span class="cline-any cline-yes">96x</span>
186
- <span class="cline-any cline-yes">96x</span>
187
- <span class="cline-any cline-yes">96x</span>
185
+ <span class="cline-any cline-yes">98x</span>
186
+ <span class="cline-any cline-yes">98x</span>
187
+ <span class="cline-any cline-yes">98x</span>
188
188
  <span class="cline-any cline-neutral">&nbsp;</span>
189
189
  <span class="cline-any cline-neutral">&nbsp;</span>
190
190
  <span class="cline-any cline-neutral">&nbsp;</span>
@@ -193,12 +193,12 @@
193
193
  <span class="cline-any cline-neutral">&nbsp;</span>
194
194
  <span class="cline-any cline-neutral">&nbsp;</span>
195
195
  <span class="cline-any cline-neutral">&nbsp;</span>
196
- <span class="cline-any cline-yes">120x</span>
196
+ <span class="cline-any cline-yes">117x</span>
197
197
  <span class="cline-any cline-neutral">&nbsp;</span>
198
198
  <span class="cline-any cline-neutral">&nbsp;</span>
199
199
  <span class="cline-any cline-neutral">&nbsp;</span>
200
- <span class="cline-any cline-yes">45x</span>
201
- <span class="cline-any cline-yes">44x</span>
200
+ <span class="cline-any cline-yes">40x</span>
201
+ <span class="cline-any cline-yes">39x</span>
202
202
  <span class="cline-any cline-yes">6x</span>
203
203
  <span class="cline-any cline-neutral">&nbsp;</span>
204
204
  <span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
@@ -277,7 +277,7 @@ function isStatusVisibleLoop(loop: LoopEntry): boolean {
277
277
  <div class='footer quiet pad2 space-top1 center small'>
278
278
  Code coverage generated by
279
279
  <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
280
- at 2026-06-23T21:49:18.784Z
280
+ at 2026-07-02T19:31:50.067Z
281
281
  </div>
282
282
  <script src="../../prettify.js"></script>
283
283
  <script>
package/dist/api.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Public API surface for sibling extensions (imported as
3
+ * `@trevonistrevon/pi-loop/api`). Everything else under src/ is internal —
4
+ * the package `exports` map deliberately blocks deep imports.
5
+ */
6
+ export { type CleanReply, type CreateTaskParams, type CreateTaskReply, type PendingReply, type PingReply, replyChannel, type SpawnParams, type SpawnReply, SUBAGENTS_RPC, TASK_EVENTS, TASKS_RPC, type TaskEntryWire, type TaskStatusWire, type UpdateTaskParams, type UpdateTaskReply, } from "./rpc/channels.js";
7
+ export { type HandleRpcOptions, handleRpc, PROTOCOL_VERSION, RpcError, type RpcEventBus, type RpcReply, rpcCall, rpcProbe, } from "./rpc/cross-extension-rpc.js";
8
+ export { NATIVE_TASKS_PROVIDER } from "./runtime/native-task-rpc.js";
9
+ export { resolveLoopStorePath, resolveTaskStorePath } from "./runtime/scope.js";
10
+ export { TaskStore } from "./task-store.js";
11
+ export type { TaskEntry, TaskStatus, TaskStoreData } from "./task-types.js";
package/dist/api.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Public API surface for sibling extensions (imported as
3
+ * `@trevonistrevon/pi-loop/api`). Everything else under src/ is internal —
4
+ * the package `exports` map deliberately blocks deep imports.
5
+ */
6
+ export { replyChannel, SUBAGENTS_RPC, TASK_EVENTS, TASKS_RPC, } from "./rpc/channels.js";
7
+ export { handleRpc, PROTOCOL_VERSION, RpcError, rpcCall, rpcProbe, } from "./rpc/cross-extension-rpc.js";
8
+ export { NATIVE_TASKS_PROVIDER } from "./runtime/native-task-rpc.js";
9
+ export { resolveLoopStorePath, resolveTaskStorePath } from "./runtime/scope.js";
10
+ export { TaskStore } from "./task-store.js";
@@ -53,7 +53,7 @@ export function registerLoopCommand(options) {
53
53
  if (!selected || selected === "< Back")
54
54
  return;
55
55
  const match = selected.match(/#(\d+)/);
56
- if (match) {
56
+ if (match?.[1]) {
57
57
  const entry = getStore().get(match[1]);
58
58
  if (entry) {
59
59
  const actions = ["x Delete"];
@@ -114,7 +114,7 @@ export function registerLoopCommand(options) {
114
114
  }
115
115
  const intervalMatch = trimmed.match(/^(\d+\s*[smhdS]\b)/i);
116
116
  if (intervalMatch) {
117
- const interval = intervalMatch[1];
117
+ const interval = intervalMatch[1] ?? intervalMatch[0];
118
118
  const prompt = trimmed.slice(intervalMatch[0].length).trim();
119
119
  if (!prompt) {
120
120
  ui.notify("Provide a prompt after the interval, e.g., /loop 5m check the deploy", "warning");
@@ -47,10 +47,10 @@ export function registerTasksCommand(options) {
47
47
  await createNativeTaskInteractively(ui);
48
48
  return viewNativeTasks(ui);
49
49
  }
50
- const match = selected.match(/#(\d+)/);
51
- if (!match)
50
+ const taskId = selected.match(/#(\d+)/)?.[1];
51
+ if (!taskId)
52
52
  return viewNativeTasks(ui);
53
- const task = taskStore.get(match[1]);
53
+ const task = taskStore.get(taskId);
54
54
  if (!task)
55
55
  return viewNativeTasks(ui);
56
56
  const actions = ["x Delete"];
@@ -1,5 +1,5 @@
1
1
  export type ReducerSource = "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
2
- export type ReducerEntityType = "task" | "loop" | "monitor" | "notification" | "goal";
2
+ export type ReducerEntityType = "task" | "loop" | "monitor" | "notification";
3
3
  export interface ReducerEvent<TType extends string = string, TPayload = unknown> {
4
4
  type: TType;
5
5
  at: number;
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ import { registerTasksCommand } from "./commands/tasks-command.js";
18
18
  import { atMaxFires } from "./loop-reducer.js";
19
19
  import { MonitorManager } from "./monitor-manager.js";
20
20
  import { createMonitorOnDoneRuntime } from "./runtime/monitor-ondone-runtime.js";
21
+ import { registerNativeTaskRpc } from "./runtime/native-task-rpc.js";
21
22
  import { createNotificationRuntime, } from "./runtime/notification-runtime.js";
22
23
  import { resolveLoopStorePath, resolveTaskStorePath } from "./runtime/scope.js";
23
24
  import { registerSessionRuntimeHooks } from "./runtime/session-runtime.js";
@@ -53,7 +54,9 @@ export default function (pi) {
53
54
  // call), so stale monitors don't linger in the count between turns.
54
55
  monitorManager.setOnChange(() => widget.update());
55
56
  widget.setTaskSummaryProvider(() => {
56
- if (!nativeTaskStore)
57
+ // Once pi-tasks owns the channels, any native store created during the
58
+ // detection window is a shadow — never surface it in the status line.
59
+ if (tasksAvailable || !nativeTaskStore)
57
60
  return { count: 0 };
58
61
  const tasks = nativeTaskStore.list().filter(t => t.status === "pending" || t.status === "in_progress");
59
62
  const active = tasks.find(t => t.status === "in_progress");
@@ -69,8 +72,18 @@ export default function (pi) {
69
72
  triggerSystem = new TriggerSystem(pi, scheduler, store, onLoopFire);
70
73
  // ── pi-tasks integration ──
71
74
  let tasksAvailable = false;
75
+ let tasksDetectionSettled = false;
72
76
  let nativeTaskStore;
73
77
  let nativeTasksRegistered = false;
78
+ function getOrCreateNativeTaskStore() {
79
+ // pi-tasks owns the task channels; don't create a shadow store.
80
+ if (tasksAvailable)
81
+ return undefined;
82
+ if (!nativeTaskStore) {
83
+ nativeTaskStore = new TaskStore(resolveTaskStorePath(getScopeOptions(), _sessionId));
84
+ }
85
+ return nativeTaskStore;
86
+ }
74
87
  const taskRuntime = createTaskRuntimeBridge({
75
88
  pi,
76
89
  isTasksAvailable: () => tasksAvailable,
@@ -86,6 +99,29 @@ export default function (pi) {
86
99
  widget.update();
87
100
  await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
88
101
  },
102
+ onDetectionStarted: () => {
103
+ tasksDetectionSettled = false;
104
+ },
105
+ onDetectionSettled: () => {
106
+ tasksDetectionSettled = true;
107
+ },
108
+ debug,
109
+ });
110
+ // The RPC server registers at init (not behind the 6s tool-fallback timer) so
111
+ // early cross-extension calls never race the timer; it stands down via
112
+ // isEnabled once an external pi-tasks is detected. checkTasksVersion ignores
113
+ // this server's own ping reply via its provider field. Mutating verbs stay
114
+ // silent until the detection probe settles, so a co-resident pi-tasks can
115
+ // never race the native server into creating divergent task state.
116
+ registerNativeTaskRpc({
117
+ pi,
118
+ getNativeTaskStore: getOrCreateNativeTaskStore,
119
+ isEnabled: () => !tasksAvailable,
120
+ isDetectionSettled: () => tasksDetectionSettled,
121
+ evaluateTaskBacklog: (taskStore, pendingCount) => evaluateTaskBacklog(taskStore, pendingCount),
122
+ updateWidget: () => {
123
+ widget.update();
124
+ },
89
125
  debug,
90
126
  });
91
127
  taskRuntime.checkTasksVersion();
@@ -156,6 +192,12 @@ export default function (pi) {
156
192
  hasPendingTasks: () => hasPendingTasks(),
157
193
  bootstrapTaskLoop: (entry) => maybeBootstrapTaskLoop(entry),
158
194
  triggerHasEventSource,
195
+ emitLoopAutodeleted: (payload) => {
196
+ pi.events.emit("loops:autodeleted", payload);
197
+ },
198
+ emitTaskBacklogEmpty: (payload) => {
199
+ pi.events.emit("tasks:backlog_empty", payload);
200
+ },
159
201
  debug,
160
202
  });
161
203
  const flushPendingNotifications = notificationRuntime.flushPendingNotifications;
@@ -266,10 +308,14 @@ export default function (pi) {
266
308
  },
267
309
  });
268
310
  // ── Native task tools (only when pi-tasks is absent) ──
311
+ // Only tool/command registration stays deferred: the tool names collide with
312
+ // pi-tasks and cannot be unregistered. The RPC server is already live (above).
269
313
  const nativeTaskFallbackTimer = setTimeout(() => {
270
314
  if (tasksAvailable || nativeTasksRegistered)
271
315
  return;
272
- const taskStore = new TaskStore(resolveTaskStorePath(getScopeOptions(), _sessionId));
316
+ const taskStore = getOrCreateNativeTaskStore();
317
+ if (!taskStore)
318
+ return;
273
319
  try {
274
320
  registerTasksCommand({
275
321
  pi,
@@ -295,7 +341,6 @@ export default function (pi) {
295
341
  }
296
342
  throw error;
297
343
  }
298
- nativeTaskStore = taskStore;
299
344
  nativeTasksRegistered = true;
300
345
  debug("native task tools registered (pi-tasks not detected)");
301
346
  }, 6000);
@@ -21,6 +21,8 @@ const COMMON_INTERVALS = {
21
21
  86400: "0 0 * * *",
22
22
  };
23
23
  function roundToNearestCommon(seconds) {
24
+ // COMMON_INTERVALS is a non-empty const table, so keys[0] and
25
+ // COMMON_INTERVALS[best] below are true invariants, not runtime fallbacks.
24
26
  const keys = Object.keys(COMMON_INTERVALS).map(Number).sort((a, b) => a - b);
25
27
  let best = keys[0];
26
28
  for (const k of keys) {
@@ -55,8 +57,8 @@ export function parseInterval(input) {
55
57
  }
56
58
  const match = trimmed.match(/^(\d+)\s*(s|m|h|d)$/i);
57
59
  if (match) {
58
- const value = parseInt(match[1], 10);
59
- const unit = match[2].toLowerCase();
60
+ const value = parseInt(match[1] ?? "", 10);
61
+ const unit = (match[2] ?? "").toLowerCase();
60
62
  const totalSec = value * (UNIT_TO_CRON[unit] ?? 60);
61
63
  if (totalSec < 60) {
62
64
  return { cron: `*/1 * * * *`, description: `${totalSec} seconds (rounded to 1 minute)` };
@@ -70,6 +72,13 @@ export function cronToNextFire(cronExpr, fromDate = new Date()) {
70
72
  if (parts.length !== 5)
71
73
  throw new Error(`Invalid cron expression: ${cronExpr}`);
72
74
  const [minF, hourF, dayF, monthF, dowF] = parts;
75
+ if (minF === undefined ||
76
+ hourF === undefined ||
77
+ dayF === undefined ||
78
+ monthF === undefined ||
79
+ dowF === undefined) {
80
+ throw new Error(`Invalid cron expression: ${cronExpr}`);
81
+ }
73
82
  const now = new Date(fromDate);
74
83
  now.setSeconds(0, 0);
75
84
  for (let minutesAdvanced = 1; minutesAdvanced < 525600; minutesAdvanced++) {
@@ -96,7 +105,7 @@ function cronFieldMatches(field, value) {
96
105
  if (part === "*")
97
106
  return true;
98
107
  if (part.includes("/")) {
99
- const [range, stepStr] = part.split("/");
108
+ const [range = "", stepStr = ""] = part.split("/");
100
109
  const step = parseInt(stepStr, 10);
101
110
  let rangeMin;
102
111
  let rangeMax;
@@ -105,7 +114,7 @@ function cronFieldMatches(field, value) {
105
114
  rangeMax = 59;
106
115
  }
107
116
  else if (range.includes("-")) {
108
- const [minS, maxS] = range.split("-");
117
+ const [minS = "", maxS = ""] = range.split("-");
109
118
  rangeMin = parseInt(minS, 10);
110
119
  rangeMax = parseInt(maxS, 10);
111
120
  }
@@ -121,7 +130,7 @@ function cronFieldMatches(field, value) {
121
130
  continue;
122
131
  }
123
132
  if (part.includes("-")) {
124
- const [minS, maxS] = part.split("-");
133
+ const [minS = "", maxS = ""] = part.split("-");
125
134
  const min = parseInt(minS, 10);
126
135
  const max = parseInt(maxS, 10);
127
136
  if (value >= min && value <= max)
@@ -25,7 +25,7 @@ export interface ReducerBackedStoreConfig<TEntry, TState, TEvent, TData> {
25
25
  }
26
26
  /**
27
27
  * Shared persistence + reducer-dispatch machinery for the file-backed entity
28
- * stores (loops, tasks, goals). Owns file locking, signature-gated load, atomic
28
+ * stores (loops, tasks). Owns file locking, signature-gated load, atomic
29
29
  * save, and reducer application; subclasses add only their entity-specific
30
30
  * command methods.
31
31
  *
@@ -48,7 +48,7 @@ function isProcessRunning(pid) {
48
48
  }
49
49
  /**
50
50
  * Shared persistence + reducer-dispatch machinery for the file-backed entity
51
- * stores (loops, tasks, goals). Owns file locking, signature-gated load, atomic
51
+ * stores (loops, tasks). Owns file locking, signature-gated load, atomic
52
52
  * save, and reducer application; subclasses add only their entity-specific
53
53
  * command methods.
54
54
  *
@@ -0,0 +1,73 @@
1
+ /** Cross-extension RPC channels served by a tasks provider (pi-tasks or pi-loop native). */
2
+ export declare const TASKS_RPC: {
3
+ readonly ping: "tasks:rpc:ping";
4
+ readonly create: "tasks:rpc:create";
5
+ readonly update: "tasks:rpc:update";
6
+ readonly pending: "tasks:rpc:pending";
7
+ readonly clean: "tasks:rpc:clean";
8
+ };
9
+ /** Cross-extension RPC channels served by @tintinweb/pi-subagents. */
10
+ export declare const SUBAGENTS_RPC: {
11
+ readonly ping: "subagents:rpc:ping";
12
+ readonly spawn: "subagents:rpc:spawn";
13
+ readonly stop: "subagents:rpc:stop";
14
+ };
15
+ /** Broadcast (fire-and-forget) task lifecycle events. */
16
+ export declare const TASK_EVENTS: {
17
+ readonly ready: "tasks:ready";
18
+ readonly created: "tasks:created";
19
+ readonly started: "tasks:started";
20
+ readonly completed: "tasks:completed";
21
+ readonly reopened: "tasks:reopened";
22
+ readonly updated: "tasks:updated";
23
+ readonly deleted: "tasks:deleted";
24
+ };
25
+ export declare function replyChannel(channel: string, requestId: string): string;
26
+ export type TaskStatusWire = "pending" | "in_progress" | "completed";
27
+ export interface TaskEntryWire {
28
+ id: string;
29
+ subject: string;
30
+ description: string;
31
+ status: TaskStatusWire;
32
+ createdAt: number;
33
+ updatedAt: number;
34
+ completedAt?: number;
35
+ metadata?: Record<string, unknown>;
36
+ }
37
+ export interface CreateTaskParams {
38
+ subject: string;
39
+ description: string;
40
+ metadata?: Record<string, unknown>;
41
+ }
42
+ export interface CreateTaskReply {
43
+ id: string;
44
+ task: TaskEntryWire;
45
+ }
46
+ export interface UpdateTaskParams {
47
+ id: string;
48
+ status?: TaskStatusWire;
49
+ subject?: string;
50
+ description?: string;
51
+ }
52
+ export interface UpdateTaskReply {
53
+ task: TaskEntryWire;
54
+ }
55
+ export interface PendingReply {
56
+ pending: number;
57
+ }
58
+ export interface CleanReply {
59
+ pruned: number;
60
+ }
61
+ export interface PingReply {
62
+ version: number;
63
+ /** Identifies which extension answered; lets a provider ignore its own reply. */
64
+ provider?: string;
65
+ }
66
+ export interface SpawnParams {
67
+ type: string;
68
+ prompt: string;
69
+ options?: Record<string, unknown>;
70
+ }
71
+ export interface SpawnReply {
72
+ id: string;
73
+ }
@@ -0,0 +1,30 @@
1
+ // VENDORED MODULE — canonical copy shared verbatim by pi-loop and pi-orca.
2
+ // If you edit this file, copy it to the sibling repo and bump VENDOR_REV.
3
+ // VENDOR_REV: 1
4
+ /** Cross-extension RPC channels served by a tasks provider (pi-tasks or pi-loop native). */
5
+ export const TASKS_RPC = {
6
+ ping: "tasks:rpc:ping",
7
+ create: "tasks:rpc:create",
8
+ update: "tasks:rpc:update",
9
+ pending: "tasks:rpc:pending",
10
+ clean: "tasks:rpc:clean",
11
+ };
12
+ /** Cross-extension RPC channels served by @tintinweb/pi-subagents. */
13
+ export const SUBAGENTS_RPC = {
14
+ ping: "subagents:rpc:ping",
15
+ spawn: "subagents:rpc:spawn",
16
+ stop: "subagents:rpc:stop",
17
+ };
18
+ /** Broadcast (fire-and-forget) task lifecycle events. */
19
+ export const TASK_EVENTS = {
20
+ ready: "tasks:ready",
21
+ created: "tasks:created",
22
+ started: "tasks:started",
23
+ completed: "tasks:completed",
24
+ reopened: "tasks:reopened",
25
+ updated: "tasks:updated",
26
+ deleted: "tasks:deleted",
27
+ };
28
+ export function replyChannel(channel, requestId) {
29
+ return `${channel}:reply:${requestId}`;
30
+ }
@@ -0,0 +1,59 @@
1
+ import { type PingReply } from "./channels.js";
2
+ /**
3
+ * Cross-extension request/reply over the in-process pi event bus.
4
+ *
5
+ * Wire contract (matches the pi-mono convention used by @tintinweb/pi-subagents):
6
+ * request: emit(channel, { requestId, ...params })
7
+ * reply: emit(`${channel}:reply:${requestId}`, RpcReply)
8
+ */
9
+ export declare const PROTOCOL_VERSION = 2;
10
+ export type RpcReply<T = void> = {
11
+ success: true;
12
+ data?: T;
13
+ } | {
14
+ success: false;
15
+ error: string;
16
+ };
17
+ export declare class RpcError extends Error {
18
+ readonly channel: string;
19
+ readonly timedOut: boolean;
20
+ constructor(channel: string, message: string, timedOut?: boolean);
21
+ }
22
+ /** Minimal event-bus surface — satisfied by pi.events. */
23
+ export interface RpcEventBus {
24
+ on(event: string, handler: (data: unknown) => void): () => void;
25
+ emit(event: string, data: unknown): void;
26
+ }
27
+ /**
28
+ * Call a remote handler and await its reply.
29
+ *
30
+ * Rejects with RpcError on a failure envelope or on timeout (timedOut=true).
31
+ * Callers that treat failure as "fall back" wrap this in try/catch at the
32
+ * layer that knows the fallback — the RPC layer itself never returns sentinels.
33
+ */
34
+ export declare function rpcCall<T = void>(bus: RpcEventBus, channel: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<T>;
35
+ /**
36
+ * Detection probe: resolves undefined when nobody answers the ping channel.
37
+ * Absence of a provider is expected, not an error.
38
+ *
39
+ * First reply wins. If the probing extension also serves this channel, its
40
+ * own reply will settle the probe — in that case listen on the reply channel
41
+ * directly for the full window and filter by PingReply.provider instead.
42
+ */
43
+ export declare function rpcProbe(bus: RpcEventBus, pingChannel: string, timeoutMs?: number): Promise<PingReply | undefined>;
44
+ export interface HandleRpcOptions {
45
+ /**
46
+ * When false, the handler is a silent no-op: another extension owns the
47
+ * channel and will reply; emitting a failure here would race its reply.
48
+ */
49
+ enabled?: () => boolean;
50
+ debug?: (...args: unknown[]) => void;
51
+ }
52
+ /**
53
+ * Register a server handler for one RPC channel. Returns the unsubscribe fn.
54
+ *
55
+ * Malformed requests get a failure reply, never a silent drop — the only
56
+ * silent case is a missing requestId, which leaves no reply address.
57
+ * A thrown/rejected fn becomes a failure envelope.
58
+ */
59
+ export declare function handleRpc<P extends object, R = unknown>(bus: RpcEventBus, channel: string, fn: (params: P) => R | Promise<R>, opts?: HandleRpcOptions): () => void;